diff --git a/.eslintrc.js b/.eslintrc.js index 1d4c74bd43..03e844dbf9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -71,6 +71,15 @@ module.exports = { "selector": "variable", "format": ["camelCase", "UPPER_CASE"] }, + { + "selector": "variable", + "modifiers": ["const"], + "format": ["PascalCase", "camelCase", "UPPER_CASE"], + "filter": { + "regex": "ErrorCode$", // Matches only if it ends exactly with ErrorCode + "match": true, + }, + }, { "selector": "parameter", "format": ["camelCase"], diff --git a/etc/firebase-admin.api.md b/etc/firebase-admin.api.md index e4675c2009..9853b8c0c3 100644 --- a/etc/firebase-admin.api.md +++ b/etc/firebase-admin.api.md @@ -220,6 +220,14 @@ export namespace database { const ServerValue: rtdb.ServerValue; } +// @public +export interface ErrorInfo { + cause?: Error; + code: string; + httpResponse?: HttpResponse; + message: string; +} + // @public export interface FirebaseArrayIndexError { error: FirebaseError; @@ -228,12 +236,19 @@ export interface FirebaseArrayIndexError { // @public export interface FirebaseError { + cause?: Error; code: string; + httpResponse?: HttpResponse; message: string; stack?: string; toJSON(): object; } +// @public +export class FirebaseError extends Error implements FirebaseError { + constructor(errorInfo: ErrorInfo); +} + // @public (undocumented) export function firestore(app?: App): _firestore.Firestore; @@ -299,6 +314,15 @@ export interface GoogleOAuthAccessToken { expires_in: number; } +// @public +export interface HttpResponse { + data?: string | object; + headers: { + [key: string]: any; + }; + status: number; +} + // @public (undocumented) export function initializeApp(options?: AppOptions, name?: string): app.App; diff --git a/etc/firebase-admin.app-check.api.md b/etc/firebase-admin.app-check.api.md index 8486d60ac7..3786a8aa88 100644 --- a/etc/firebase-admin.app-check.api.md +++ b/etc/firebase-admin.app-check.api.md @@ -16,6 +16,22 @@ export class AppCheck { verifyToken(appCheckToken: string, options?: VerifyAppCheckTokenOptions): Promise; } +// @public +export const AppCheckErrorCode: { + readonly ABORTED: "aborted"; + readonly INVALID_ARGUMENT: "invalid-argument"; + readonly INVALID_CREDENTIAL: "invalid-credential"; + readonly INTERNAL: "internal-error"; + readonly PERMISSION_DENIED: "permission-denied"; + readonly UNAUTHENTICATED: "unauthenticated"; + readonly NOT_FOUND: "not-found"; + readonly APP_CHECK_TOKEN_EXPIRED: "app-check-token-expired"; + readonly UNKNOWN: "unknown-error"; +}; + +// @public +export type AppCheckErrorCode = typeof AppCheckErrorCode[keyof typeof AppCheckErrorCode]; + // @public export interface AppCheckToken { token: string; @@ -39,6 +55,14 @@ export interface DecodedAppCheckToken { sub: string; } +// Warning: (ae-forgotten-export) The symbol "PrefixedFirebaseError" needs to be exported by the entry point index.d.ts +// +// @public +export class FirebaseAppCheckError extends PrefixedFirebaseError { + // Warning: (ae-forgotten-export) The symbol "ErrorInfo" needs to be exported by the entry point index.d.ts + constructor(info: ErrorInfo, message?: string); +} + // @public export function getAppCheck(app?: App): AppCheck; diff --git a/etc/firebase-admin.app.api.md b/etc/firebase-admin.app.api.md index f653df5a7b..7f5e00ee3e 100644 --- a/etc/firebase-admin.app.api.md +++ b/etc/firebase-admin.app.api.md @@ -13,30 +13,22 @@ export interface App { } // @public -export class AppErrorCodes { - // (undocumented) - static APP_DELETED: string; - // (undocumented) - static DUPLICATE_APP: string; - // (undocumented) - static INTERNAL_ERROR: string; - // (undocumented) - static INVALID_APP_NAME: string; - // (undocumented) - static INVALID_APP_OPTIONS: string; - // (undocumented) - static INVALID_ARGUMENT: string; - // (undocumented) - static INVALID_CREDENTIAL: string; - // (undocumented) - static NETWORK_ERROR: string; - // (undocumented) - static NETWORK_TIMEOUT: string; - // (undocumented) - static NO_APP: string; - // (undocumented) - static UNABLE_TO_PARSE_RESPONSE: string; -} +export const AppErrorCode: { + readonly APP_DELETED: "app-deleted"; + readonly DUPLICATE_APP: "duplicate-app"; + readonly INVALID_ARGUMENT: "invalid-argument"; + readonly INTERNAL_ERROR: "internal-error"; + readonly INVALID_APP_NAME: "invalid-app-name"; + readonly INVALID_APP_OPTIONS: "invalid-app-options"; + readonly INVALID_CREDENTIAL: "invalid-credential"; + readonly NETWORK_ERROR: "network-error"; + readonly NETWORK_TIMEOUT: "network-timeout"; + readonly NO_APP: "no-app"; + readonly UNABLE_TO_PARSE_RESPONSE: "unable-to-parse-response"; +}; + +// @public +export type AppErrorCode = typeof AppErrorCode[keyof typeof AppErrorCode]; // @public export function applicationDefault(httpAgent?: Agent): Credential; @@ -63,10 +55,19 @@ export interface Credential { // @public export function deleteApp(app: App): Promise; +// @public +export interface ErrorInfo { + cause?: Error; + code: string; + httpResponse?: HttpResponse; + message: string; +} + // Warning: (ae-forgotten-export) The symbol "PrefixedFirebaseError" needs to be exported by the entry point index.d.ts // // @public export class FirebaseAppError extends PrefixedFirebaseError { + constructor(info: ErrorInfo, message?: string); } // @public @@ -77,12 +78,19 @@ export interface FirebaseArrayIndexError { // @public export interface FirebaseError { + cause?: Error; code: string; + httpResponse?: HttpResponse; message: string; stack?: string; toJSON(): object; } +// @public +export class FirebaseError extends Error implements FirebaseError { + constructor(errorInfo: ErrorInfo); +} + // @public export function getApp(appName?: string): App; @@ -97,6 +105,15 @@ export interface GoogleOAuthAccessToken { expires_in: number; } +// @public +export interface HttpResponse { + data?: string | object; + headers: { + [key: string]: any; + }; + status: number; +} + // @public export function initializeApp(options?: AppOptions, appName?: string): App; diff --git a/etc/firebase-admin.auth.api.md b/etc/firebase-admin.auth.api.md index 8713b1bda7..08b790af47 100644 --- a/etc/firebase-admin.auth.api.md +++ b/etc/firebase-admin.auth.api.md @@ -56,483 +56,106 @@ export class Auth extends BaseAuth { } // @public -export class AuthClientErrorCode { - // (undocumented) - static AUTH_BLOCKING_TOKEN_EXPIRED: { - code: string; - message: string; - }; - // (undocumented) - static BILLING_NOT_ENABLED: { - code: string; - message: string; - }; - // (undocumented) - static CLAIMS_TOO_LARGE: { - code: string; - message: string; - }; - // (undocumented) - static CONFIGURATION_EXISTS: { - code: string; - message: string; - }; - // (undocumented) - static CONFIGURATION_NOT_FOUND: { - code: string; - message: string; - }; - // (undocumented) - static EMAIL_ALREADY_EXISTS: { - code: string; - message: string; - }; - // (undocumented) - static EMAIL_NOT_FOUND: { - code: string; - message: string; - }; - // (undocumented) - static FORBIDDEN_CLAIM: { - code: string; - message: string; - }; - // (undocumented) - static ID_TOKEN_EXPIRED: { - code: string; - message: string; - }; - // (undocumented) - static ID_TOKEN_REVOKED: { - code: string; - message: string; - }; - // (undocumented) - static INSUFFICIENT_PERMISSION: { - code: string; - message: string; - }; - // (undocumented) - static INTERNAL_ERROR: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_ARGUMENT: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_CLAIMS: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_CONFIG: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_CONTINUE_URI: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_CREATION_TIME: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_CREDENTIAL: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_DISABLED_FIELD: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_DISPLAY_NAME: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_DYNAMIC_LINK_DOMAIN: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_EMAIL: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_EMAIL_VERIFIED: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_ENROLLED_FACTORS: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_ENROLLMENT_TIME: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_HASH_ALGORITHM: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_HASH_BLOCK_SIZE: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_HASH_DERIVED_KEY_LENGTH: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_HASH_KEY: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_HASH_MEMORY_COST: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_HASH_PARALLELIZATION: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_HASH_ROUNDS: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_HASH_SALT_SEPARATOR: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_HOSTING_LINK_DOMAIN: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_ID_TOKEN: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_LAST_SIGN_IN_TIME: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_NAME: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_NEW_EMAIL: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_OAUTH_CLIENT_ID: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_OAUTH_RESPONSETYPE: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_PAGE_TOKEN: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_PASSWORD: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_PASSWORD_HASH: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_PASSWORD_SALT: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_PHONE_NUMBER: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_PHOTO_URL: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_PROJECT_ID: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_PROVIDER_DATA: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_PROVIDER_ID: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_PROVIDER_UID: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_RECAPTCHA_ACTION: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_RECAPTCHA_ENFORCEMENT_STATE: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_SESSION_COOKIE_DURATION: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_TENANT_ID: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_TENANT_TYPE: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_TESTING_PHONE_NUMBER: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_TOKENS_VALID_AFTER_TIME: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_UID: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_USER_IMPORT: { - code: string; - message: string; - }; - // (undocumented) - static MAXIMUM_TEST_PHONE_NUMBER_EXCEEDED: { - code: string; - message: string; - }; - // (undocumented) - static MAXIMUM_USER_COUNT_EXCEEDED: { - code: string; - message: string; - }; - // (undocumented) - static MISMATCHING_TENANT_ID: { - code: string; - message: string; - }; - // (undocumented) - static MISSING_ANDROID_PACKAGE_NAME: { - code: string; - message: string; - }; - // (undocumented) - static MISSING_CONFIG: { - code: string; - message: string; - }; - // (undocumented) - static MISSING_CONTINUE_URI: { - code: string; - message: string; - }; - // (undocumented) - static MISSING_DISPLAY_NAME: { - code: string; - message: string; - }; - // (undocumented) - static MISSING_EMAIL: { - code: string; - message: string; - }; - // (undocumented) - static MISSING_HASH_ALGORITHM: { - code: string; - message: string; - }; - // (undocumented) - static MISSING_IOS_BUNDLE_ID: { - code: string; - message: string; - }; - // (undocumented) - static MISSING_ISSUER: { - code: string; - message: string; - }; - // (undocumented) - static MISSING_OAUTH_CLIENT_ID: { - code: string; - message: string; - }; - // (undocumented) - static MISSING_OAUTH_CLIENT_SECRET: { - code: string; - message: string; - }; - // (undocumented) - static MISSING_PROVIDER_ID: { - code: string; - message: string; - }; - // (undocumented) - static MISSING_SAML_RELYING_PARTY_CONFIG: { - code: string; - message: string; - }; - // (undocumented) - static MISSING_UID: { - code: string; - message: string; - }; - // (undocumented) - static NOT_FOUND: { - code: string; - message: string; - }; - // (undocumented) - static OPERATION_NOT_ALLOWED: { - code: string; - message: string; - }; - // (undocumented) - static PHONE_NUMBER_ALREADY_EXISTS: { - code: string; - message: string; - }; - // (undocumented) - static PROJECT_NOT_FOUND: { - code: string; - message: string; - }; - // (undocumented) - static QUOTA_EXCEEDED: { - code: string; - message: string; - }; - // (undocumented) - static RECAPTCHA_NOT_ENABLED: { - code: string; - message: string; - }; - // (undocumented) - static SECOND_FACTOR_LIMIT_EXCEEDED: { - code: string; - message: string; - }; - // (undocumented) - static SECOND_FACTOR_UID_ALREADY_EXISTS: { - code: string; - message: string; - }; - // (undocumented) - static SESSION_COOKIE_EXPIRED: { - code: string; - message: string; - }; - // (undocumented) - static SESSION_COOKIE_REVOKED: { - code: string; - message: string; - }; - // (undocumented) - static TENANT_NOT_FOUND: { - code: string; - message: string; - }; - // (undocumented) - static UID_ALREADY_EXISTS: { - code: string; - message: string; - }; - // (undocumented) - static UNAUTHORIZED_DOMAIN: { - code: string; - message: string; - }; - // (undocumented) - static UNSUPPORTED_FIRST_FACTOR: { - code: string; - message: string; - }; - // (undocumented) - static UNSUPPORTED_SECOND_FACTOR: { - code: string; - message: string; - }; - // (undocumented) - static UNSUPPORTED_TENANT_OPERATION: { - code: string; - message: string; - }; - // (undocumented) - static UNVERIFIED_EMAIL: { - code: string; - message: string; - }; - // (undocumented) - static USER_DISABLED: { - code: string; - message: string; - }; - // (undocumented) - static USER_NOT_DISABLED: { - code: string; - message: string; - }; - // (undocumented) - static USER_NOT_FOUND: { - code: string; - message: string; - }; -} +export const AuthErrorCode: { + readonly AUTH_BLOCKING_TOKEN_EXPIRED: "auth-blocking-token-expired"; + readonly BILLING_NOT_ENABLED: "billing-not-enabled"; + readonly CLAIMS_TOO_LARGE: "claims-too-large"; + readonly CONFIGURATION_EXISTS: "configuration-exists"; + readonly CONFIGURATION_NOT_FOUND: "configuration-not-found"; + readonly ID_TOKEN_EXPIRED: "id-token-expired"; + readonly INVALID_ARGUMENT: "argument-error"; + readonly INVALID_CONFIG: "invalid-config"; + readonly EMAIL_ALREADY_EXISTS: "email-already-exists"; + readonly EMAIL_NOT_FOUND: "email-not-found"; + readonly FORBIDDEN_CLAIM: "reserved-claim"; + readonly INVALID_ID_TOKEN: "invalid-id-token"; + readonly ID_TOKEN_REVOKED: "id-token-revoked"; + readonly INTERNAL_ERROR: "internal-error"; + readonly INVALID_CLAIMS: "invalid-claims"; + readonly INVALID_CONTINUE_URI: "invalid-continue-uri"; + readonly INVALID_CREATION_TIME: "invalid-creation-time"; + readonly INVALID_CREDENTIAL: "invalid-credential"; + readonly INVALID_DISABLED_FIELD: "invalid-disabled-field"; + readonly INVALID_DISPLAY_NAME: "invalid-display-name"; + readonly INVALID_DYNAMIC_LINK_DOMAIN: "invalid-dynamic-link-domain"; + readonly INVALID_HOSTING_LINK_DOMAIN: "invalid-hosting-link-domain"; + readonly INVALID_EMAIL_VERIFIED: "invalid-email-verified"; + readonly INVALID_EMAIL: "invalid-email"; + readonly INVALID_NEW_EMAIL: "invalid-new-email"; + readonly INVALID_ENROLLED_FACTORS: "invalid-enrolled-factors"; + readonly INVALID_ENROLLMENT_TIME: "invalid-enrollment-time"; + readonly INVALID_HASH_ALGORITHM: "invalid-hash-algorithm"; + readonly INVALID_HASH_BLOCK_SIZE: "invalid-hash-block-size"; + readonly INVALID_HASH_DERIVED_KEY_LENGTH: "invalid-hash-derived-key-length"; + readonly INVALID_HASH_KEY: "invalid-hash-key"; + readonly INVALID_HASH_MEMORY_COST: "invalid-hash-memory-cost"; + readonly INVALID_HASH_PARALLELIZATION: "invalid-hash-parallelization"; + readonly INVALID_HASH_ROUNDS: "invalid-hash-rounds"; + readonly INVALID_HASH_SALT_SEPARATOR: "invalid-hash-salt-separator"; + readonly INVALID_LAST_SIGN_IN_TIME: "invalid-last-sign-in-time"; + readonly INVALID_NAME: "invalid-name"; + readonly INVALID_OAUTH_CLIENT_ID: "invalid-oauth-client-id"; + readonly INVALID_PAGE_TOKEN: "invalid-page-token"; + readonly INVALID_PASSWORD: "invalid-password"; + readonly INVALID_PASSWORD_HASH: "invalid-password-hash"; + readonly INVALID_PASSWORD_SALT: "invalid-password-salt"; + readonly INVALID_PHONE_NUMBER: "invalid-phone-number"; + readonly INVALID_PHOTO_URL: "invalid-photo-url"; + readonly INVALID_PROJECT_ID: "invalid-project-id"; + readonly INVALID_PROVIDER_DATA: "invalid-provider-data"; + readonly INVALID_PROVIDER_ID: "invalid-provider-id"; + readonly INVALID_PROVIDER_UID: "invalid-provider-uid"; + readonly INVALID_OAUTH_RESPONSETYPE: "invalid-oauth-responsetype"; + readonly INVALID_SESSION_COOKIE_DURATION: "invalid-session-cookie-duration"; + readonly INVALID_TENANT_ID: "invalid-tenant-id"; + readonly INVALID_TENANT_TYPE: "invalid-tenant-type"; + readonly INVALID_TESTING_PHONE_NUMBER: "invalid-testing-phone-number"; + readonly INVALID_UID: "invalid-uid"; + readonly INVALID_USER_IMPORT: "invalid-user-import"; + readonly INVALID_TOKENS_VALID_AFTER_TIME: "invalid-tokens-valid-after-time"; + readonly MISMATCHING_TENANT_ID: "mismatching-tenant-id"; + readonly MISSING_ANDROID_PACKAGE_NAME: "missing-android-package-name"; + readonly MISSING_CONFIG: "missing-config"; + readonly MISSING_CONTINUE_URI: "missing-continue-uri"; + readonly MISSING_DISPLAY_NAME: "missing-display-name"; + readonly MISSING_EMAIL: "missing-email"; + readonly MISSING_IOS_BUNDLE_ID: "missing-ios-bundle-id"; + readonly MISSING_ISSUER: "missing-issuer"; + readonly MISSING_HASH_ALGORITHM: "missing-hash-algorithm"; + readonly MISSING_OAUTH_CLIENT_ID: "missing-oauth-client-id"; + readonly MISSING_OAUTH_CLIENT_SECRET: "missing-oauth-client-secret"; + readonly MISSING_PROVIDER_ID: "missing-provider-id"; + readonly MISSING_SAML_RELYING_PARTY_CONFIG: "missing-saml-relying-party-config"; + readonly MAXIMUM_TEST_PHONE_NUMBER_EXCEEDED: "test-phone-number-limit-exceeded"; + readonly MAXIMUM_USER_COUNT_EXCEEDED: "maximum-user-count-exceeded"; + readonly MISSING_UID: "missing-uid"; + readonly OPERATION_NOT_ALLOWED: "operation-not-allowed"; + readonly PHONE_NUMBER_ALREADY_EXISTS: "phone-number-already-exists"; + readonly PROJECT_NOT_FOUND: "project-not-found"; + readonly INSUFFICIENT_PERMISSION: "insufficient-permission"; + readonly QUOTA_EXCEEDED: "quota-exceeded"; + readonly SECOND_FACTOR_LIMIT_EXCEEDED: "second-factor-limit-exceeded"; + readonly SECOND_FACTOR_UID_ALREADY_EXISTS: "second-factor-uid-already-exists"; + readonly SESSION_COOKIE_EXPIRED: "session-cookie-expired"; + readonly SESSION_COOKIE_REVOKED: "session-cookie-revoked"; + readonly TENANT_NOT_FOUND: "tenant-not-found"; + readonly UID_ALREADY_EXISTS: "uid-already-exists"; + readonly UNAUTHORIZED_DOMAIN: "unauthorized-continue-uri"; + readonly UNSUPPORTED_FIRST_FACTOR: "unsupported-first-factor"; + readonly UNSUPPORTED_SECOND_FACTOR: "unsupported-second-factor"; + readonly UNSUPPORTED_TENANT_OPERATION: "unsupported-tenant-operation"; + readonly UNVERIFIED_EMAIL: "unverified-email"; + readonly USER_NOT_FOUND: "user-not-found"; + readonly NOT_FOUND: "not-found"; + readonly USER_DISABLED: "user-disabled"; + readonly USER_NOT_DISABLED: "user-not-disabled"; + readonly INVALID_RECAPTCHA_ACTION: "invalid-recaptcha-action"; + readonly INVALID_RECAPTCHA_ENFORCEMENT_STATE: "invalid-recaptcha-enforcement-state"; + readonly RECAPTCHA_NOT_ENABLED: "recaptcha-not-enabled"; +}; + +// @public +export type AuthErrorCode = typeof AuthErrorCode[keyof typeof AuthErrorCode]; // @public export type AuthFactorType = 'phone'; @@ -732,6 +355,8 @@ export interface EmailSignInProviderConfig { // // @public export class FirebaseAuthError extends PrefixedFirebaseError { + // Warning: (ae-forgotten-export) The symbol "ErrorInfo" needs to be exported by the entry point index.d.ts + constructor(info: ErrorInfo, message?: string); } // @public diff --git a/etc/firebase-admin.data-connect.api.md b/etc/firebase-admin.data-connect.api.md index 4ac9e170cd..c86afd866b 100644 --- a/etc/firebase-admin.data-connect.api.md +++ b/etc/firebase-admin.data-connect.api.md @@ -38,6 +38,22 @@ export class DataConnect { upsertMany>(tableName: string, variables: Variables): Promise>; } +// @public +export const DataConnectErrorCode: { + readonly ABORTED: "aborted"; + readonly INVALID_ARGUMENT: "invalid-argument"; + readonly INVALID_CREDENTIAL: "invalid-credential"; + readonly INTERNAL: "internal-error"; + readonly PERMISSION_DENIED: "permission-denied"; + readonly UNAUTHENTICATED: "unauthenticated"; + readonly NOT_FOUND: "not-found"; + readonly UNKNOWN: "unknown-error"; + readonly QUERY_ERROR: "query-error"; +}; + +// @public +export type DataConnectErrorCode = typeof DataConnectErrorCode[keyof typeof DataConnectErrorCode]; + // @public export interface ExecuteGraphqlResponse { data: GraphqlResponse; @@ -48,6 +64,14 @@ export interface ExecuteOperationResponse { data: GraphqlResponse; } +// Warning: (ae-forgotten-export) The symbol "PrefixedFirebaseError" needs to be exported by the entry point index.d.ts +// +// @public +export class FirebaseDataConnectError extends PrefixedFirebaseError { + // Warning: (ae-forgotten-export) The symbol "ErrorInfo" needs to be exported by the entry point index.d.ts + constructor(info: ErrorInfo, message?: string); +} + // @public export function getDataConnect(connectorConfig: ConnectorConfig, app?: App): DataConnect; diff --git a/etc/firebase-admin.database.api.md b/etc/firebase-admin.database.api.md index 0cd7d55e9b..1b3b1f86dc 100644 --- a/etc/firebase-admin.database.api.md +++ b/etc/firebase-admin.database.api.md @@ -28,10 +28,12 @@ export const enableLogging: typeof rtdb.enableLogging; export { EventType } -// Warning: (ae-forgotten-export) The symbol "FirebaseError" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "PrefixedFirebaseError" needs to be exported by the entry point index.d.ts // // @public -export class FirebaseDatabaseError extends FirebaseError { +export class FirebaseDatabaseError extends PrefixedFirebaseError { + // Warning: (ae-forgotten-export) The symbol "ErrorInfo" needs to be exported by the entry point index.d.ts + constructor(info: ErrorInfo, message?: string); } // Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts diff --git a/etc/firebase-admin.eventarc.api.md b/etc/firebase-admin.eventarc.api.md index 4a6020f33a..8db2134e01 100644 --- a/etc/firebase-admin.eventarc.api.md +++ b/etc/firebase-admin.eventarc.api.md @@ -43,6 +43,23 @@ export class Eventarc { channel(options?: ChannelOptions): Channel; } +// @public +export const EventarcErrorCode: { + readonly UNKNOWN_ERROR: "unknown-error"; + readonly INVALID_ARGUMENT: "invalid-argument"; +}; + +// @public +export type EventarcErrorCode = typeof EventarcErrorCode[keyof typeof EventarcErrorCode]; + +// Warning: (ae-forgotten-export) The symbol "PrefixedFirebaseError" needs to be exported by the entry point index.d.ts +// +// @public +export class FirebaseEventarcError extends PrefixedFirebaseError { + // Warning: (ae-forgotten-export) The symbol "ErrorInfo" needs to be exported by the entry point index.d.ts + constructor(info: ErrorInfo, message?: string); +} + // @public export function getEventarc(app?: App): Eventarc; diff --git a/etc/firebase-admin.extensions.api.md b/etc/firebase-admin.extensions.api.md index 9082d602b9..3056b3c345 100644 --- a/etc/firebase-admin.extensions.api.md +++ b/etc/firebase-admin.extensions.api.md @@ -15,6 +15,26 @@ export class Extensions { runtime(): Runtime; } +// @public +export const ExtensionsErrorCode: { + readonly INVALID_ARGUMENT: "invalid-argument"; + readonly NOT_FOUND: "not-found"; + readonly FORBIDDEN: "forbidden"; + readonly INTERNAL_ERROR: "internal-error"; + readonly UNKNOWN_ERROR: "unknown-error"; +}; + +// @public +export type ExtensionsErrorCode = typeof ExtensionsErrorCode[keyof typeof ExtensionsErrorCode]; + +// Warning: (ae-forgotten-export) The symbol "PrefixedFirebaseError" needs to be exported by the entry point index.d.ts +// +// @public +export class FirebaseExtensionsError extends PrefixedFirebaseError { + // Warning: (ae-forgotten-export) The symbol "ErrorInfo" needs to be exported by the entry point index.d.ts + constructor(info: ErrorInfo, message?: string); +} + // @public export function getExtensions(app?: App): Extensions; diff --git a/etc/firebase-admin.firestore.api.md b/etc/firebase-admin.firestore.api.md index e8b2932326..983462539a 100644 --- a/etc/firebase-admin.firestore.api.md +++ b/etc/firebase-admin.firestore.api.md @@ -100,10 +100,12 @@ export { FieldValue } export { Filter } -// Warning: (ae-forgotten-export) The symbol "FirebaseError" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "PrefixedFirebaseError" needs to be exported by the entry point index.d.ts // // @public -export class FirebaseFirestoreError extends FirebaseError { +export class FirebaseFirestoreError extends PrefixedFirebaseError { + // Warning: (ae-forgotten-export) The symbol "ErrorInfo" needs to be exported by the entry point index.d.ts + constructor(info: ErrorInfo, message?: string); } export { Firestore } diff --git a/etc/firebase-admin.functions.api.md b/etc/firebase-admin.functions.api.md index 10eee3fef9..6620991ebc 100644 --- a/etc/firebase-admin.functions.api.md +++ b/etc/firebase-admin.functions.api.md @@ -23,6 +23,14 @@ export interface DelayDelivery { // @public export type DeliverySchedule = DelayDelivery | AbsoluteDelivery; +// Warning: (ae-forgotten-export) The symbol "PrefixedFirebaseError" needs to be exported by the entry point index.d.ts +// +// @public +export class FirebaseFunctionsError extends PrefixedFirebaseError { + // Warning: (ae-forgotten-export) The symbol "ErrorInfo" needs to be exported by the entry point index.d.ts + constructor(info: ErrorInfo, message?: string); +} + // @public export class Functions { // Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts @@ -32,6 +40,23 @@ export class Functions { taskQueue>(functionName: string, extensionId?: string): TaskQueue; } +// @public +export const FunctionsErrorCode: { + readonly ABORTED: "aborted"; + readonly INVALID_ARGUMENT: "invalid-argument"; + readonly INVALID_CREDENTIAL: "invalid-credential"; + readonly INTERNAL_ERROR: "internal-error"; + readonly FAILED_PRECONDITION: "failed-precondition"; + readonly PERMISSION_DENIED: "permission-denied"; + readonly UNAUTHENTICATED: "unauthenticated"; + readonly NOT_FOUND: "not-found"; + readonly UNKNOWN_ERROR: "unknown-error"; + readonly TASK_ALREADY_EXISTS: "task-already-exists"; +}; + +// @public +export type FunctionsErrorCode = typeof FunctionsErrorCode[keyof typeof FunctionsErrorCode]; + // @public export function getFunctions(app?: App): Functions; diff --git a/etc/firebase-admin.installations.api.md b/etc/firebase-admin.installations.api.md index 2bc45b3e14..a04a8893eb 100644 --- a/etc/firebase-admin.installations.api.md +++ b/etc/firebase-admin.installations.api.md @@ -6,10 +6,12 @@ import { Agent } from 'http'; -// Warning: (ae-forgotten-export) The symbol "FirebaseError" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "PrefixedFirebaseError" needs to be exported by the entry point index.d.ts // // @public -export class FirebaseInstallationsError extends FirebaseError { +export class FirebaseInstallationsError extends PrefixedFirebaseError { + // Warning: (ae-forgotten-export) The symbol "ErrorInfo" needs to be exported by the entry point index.d.ts + constructor(info: ErrorInfo, message?: string); } // Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts @@ -23,28 +25,15 @@ export class Installations { deleteInstallation(fid: string): Promise; } -// @public (undocumented) -export class InstallationsClientErrorCode { - // (undocumented) - static API_ERROR: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_ARGUMENT: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_INSTALLATION_ID: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_PROJECT_ID: { - code: string; - message: string; - }; -} +// @public +export const InstallationsErrorCode: { + readonly INVALID_ARGUMENT: "invalid-argument"; + readonly INVALID_PROJECT_ID: "invalid-project-id"; + readonly INVALID_INSTALLATION_ID: "invalid-installation-id"; + readonly API_ERROR: "api-error"; +}; + +// @public +export type InstallationsErrorCode = typeof InstallationsErrorCode[keyof typeof InstallationsErrorCode]; ``` diff --git a/etc/firebase-admin.instance-id.api.md b/etc/firebase-admin.instance-id.api.md index 5f7852c15d..52ce830004 100644 --- a/etc/firebase-admin.instance-id.api.md +++ b/etc/firebase-admin.instance-id.api.md @@ -6,10 +6,12 @@ import { Agent } from 'http'; -// Warning: (ae-forgotten-export) The symbol "FirebaseError" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "PrefixedFirebaseError" needs to be exported by the entry point index.d.ts // // @public -export class FirebaseInstanceIdError extends FirebaseError { +export class FirebaseInstanceIdError extends PrefixedFirebaseError { + // Warning: (ae-forgotten-export) The symbol "ErrorInfo" needs to be exported by the entry point index.d.ts + constructor(info: ErrorInfo, message?: string); } // Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts @@ -23,15 +25,16 @@ export class InstanceId { deleteInstanceId(instanceId: string): Promise; } -// Warning: (ae-forgotten-export) The symbol "InstallationsClientErrorCode" needs to be exported by the entry point index.d.ts -// -// @public (undocumented) -export class InstanceIdClientErrorCode extends InstallationsClientErrorCode { - // (undocumented) - static INVALID_INSTANCE_ID: { - code: string; - message: string; - }; -} +// @public +export const InstanceIdErrorCode: { + readonly INVALID_ARGUMENT: "invalid-argument"; + readonly INVALID_PROJECT_ID: "invalid-project-id"; + readonly INVALID_INSTALLATION_ID: "invalid-installation-id"; + readonly API_ERROR: "api-error"; + readonly INVALID_INSTANCE_ID: "invalid-instance-id"; +}; + +// @public +export type InstanceIdErrorCode = typeof InstanceIdErrorCode[keyof typeof InstanceIdErrorCode]; ``` diff --git a/etc/firebase-admin.machine-learning.api.md b/etc/firebase-admin.machine-learning.api.md index d39e2d2a9d..04a390cf9d 100644 --- a/etc/firebase-admin.machine-learning.api.md +++ b/etc/firebase-admin.machine-learning.api.md @@ -6,6 +6,14 @@ import { Agent } from 'http'; +// Warning: (ae-forgotten-export) The symbol "PrefixedFirebaseError" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export class FirebaseMachineLearningError extends PrefixedFirebaseError { + // Warning: (ae-forgotten-export) The symbol "ErrorInfo" needs to be exported by the entry point index.d.ts + constructor(info: ErrorInfo, message?: string); +} + // @public (undocumented) export interface GcsTfliteModelOptions extends ModelOptionsBase { // (undocumented) @@ -44,6 +52,30 @@ export class MachineLearning { updateModel(modelId: string, model: ModelOptions): Promise; } +// @public +export const MachineLearningErrorCode: { + readonly ALREADY_EXISTS: "already-exists"; + readonly AUTHENTICATION_ERROR: "authentication-error"; + readonly INTERNAL_ERROR: "internal-error"; + readonly INVALID_ARGUMENT: "invalid-argument"; + readonly INVALID_SERVER_RESPONSE: "invalid-server-response"; + readonly NOT_FOUND: "not-found"; + readonly RESOURCE_EXHAUSTED: "resource-exhausted"; + readonly SERVICE_UNAVAILABLE: "service-unavailable"; + readonly UNKNOWN_ERROR: "unknown-error"; + readonly CANCELLED: "cancelled"; + readonly DEADLINE_EXCEEDED: "deadline-exceeded"; + readonly PERMISSION_DENIED: "permission-denied"; + readonly FAILED_PRECONDITION: "failed-precondition"; + readonly ABORTED: "aborted"; + readonly OUT_OF_RANGE: "out-of-range"; + readonly DATA_LOSS: "data-loss"; + readonly UNAUTHENTICATED: "unauthenticated"; +}; + +// @public +export type MachineLearningErrorCode = typeof MachineLearningErrorCode[keyof typeof MachineLearningErrorCode]; + // @public export class Model { get createTime(): string; diff --git a/etc/firebase-admin.messaging.api.md b/etc/firebase-admin.messaging.api.md index b93ba7e58e..4c13ff1d27 100644 --- a/etc/firebase-admin.messaging.api.md +++ b/etc/firebase-admin.messaging.api.md @@ -172,6 +172,8 @@ export interface FcmOptions { // // @public export class FirebaseMessagingError extends PrefixedFirebaseError { + // Warning: (ae-forgotten-export) The symbol "ErrorInfo" needs to be exported by the entry point index.d.ts + constructor(info: ErrorInfo, message?: string); } // Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts @@ -202,103 +204,30 @@ export class Messaging { } // @public -export class MessagingClientErrorCode { - // (undocumented) - static AUTHENTICATION_ERROR: { - code: string; - message: string; - }; - // (undocumented) - static DEVICE_MESSAGE_RATE_EXCEEDED: { - code: string; - message: string; - }; - // (undocumented) - static INTERNAL_ERROR: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_ARGUMENT: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_DATA_PAYLOAD_KEY: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_OPTIONS: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_PACKAGE_NAME: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_PAYLOAD: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_RECIPIENT: { - code: string; - message: string; - }; - // (undocumented) - static INVALID_REGISTRATION_TOKEN: { - code: string; - message: string; - }; - // (undocumented) - static MESSAGE_RATE_EXCEEDED: { - code: string; - message: string; - }; - // (undocumented) - static MISMATCHED_CREDENTIAL: { - code: string; - message: string; - }; - // (undocumented) - static PAYLOAD_SIZE_LIMIT_EXCEEDED: { - code: string; - message: string; - }; - // (undocumented) - static REGISTRATION_TOKEN_NOT_REGISTERED: { - code: string; - message: string; - }; - // (undocumented) - static SERVER_UNAVAILABLE: { - code: string; - message: string; - }; - // (undocumented) - static THIRD_PARTY_AUTH_ERROR: { - code: string; - message: string; - }; - // (undocumented) - static TOO_MANY_TOPICS: { - code: string; - message: string; - }; - // (undocumented) - static TOPICS_MESSAGE_RATE_EXCEEDED: { - code: string; - message: string; - }; - // (undocumented) - static UNKNOWN_ERROR: { - code: string; - message: string; - }; -} +export const MessagingErrorCode: { + readonly INVALID_ARGUMENT: "invalid-argument"; + readonly INVALID_RECIPIENT: "invalid-recipient"; + readonly INVALID_PAYLOAD: "invalid-payload"; + readonly INVALID_DATA_PAYLOAD_KEY: "invalid-data-payload-key"; + readonly PAYLOAD_SIZE_LIMIT_EXCEEDED: "payload-size-limit-exceeded"; + readonly INVALID_OPTIONS: "invalid-options"; + readonly INVALID_REGISTRATION_TOKEN: "invalid-registration-token"; + readonly REGISTRATION_TOKEN_NOT_REGISTERED: "registration-token-not-registered"; + readonly MISMATCHED_CREDENTIAL: "mismatched-credential"; + readonly INVALID_PACKAGE_NAME: "invalid-package-name"; + readonly DEVICE_MESSAGE_RATE_EXCEEDED: "device-message-rate-exceeded"; + readonly TOPICS_MESSAGE_RATE_EXCEEDED: "topics-message-rate-exceeded"; + readonly MESSAGE_RATE_EXCEEDED: "message-rate-exceeded"; + readonly THIRD_PARTY_AUTH_ERROR: "third-party-auth-error"; + readonly TOO_MANY_TOPICS: "too-many-topics"; + readonly AUTHENTICATION_ERROR: "authentication-error"; + readonly SERVER_UNAVAILABLE: "server-unavailable"; + readonly INTERNAL_ERROR: "internal-error"; + readonly UNKNOWN_ERROR: "unknown-error"; +}; + +// @public +export type MessagingErrorCode = typeof MessagingErrorCode[keyof typeof MessagingErrorCode]; // @public export interface MessagingOptions { diff --git a/etc/firebase-admin.phone-number-verification.api.md b/etc/firebase-admin.phone-number-verification.api.md index 90b118a661..9195516d62 100644 --- a/etc/firebase-admin.phone-number-verification.api.md +++ b/etc/firebase-admin.phone-number-verification.api.md @@ -6,6 +6,14 @@ import { Agent } from 'http'; +// Warning: (ae-forgotten-export) The symbol "PrefixedFirebaseError" needs to be exported by the entry point index.d.ts +// +// @public +export class FirebasePhoneNumberVerificationError extends PrefixedFirebaseError { + // Warning: (ae-forgotten-export) The symbol "ErrorInfo" needs to be exported by the entry point index.d.ts + constructor(info: ErrorInfo, message?: string); +} + // Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts // // @public @@ -17,6 +25,16 @@ export class PhoneNumberVerification { verifyToken(jwt: string): Promise; } +// @public +export const PhoneNumberVerificationErrorCode: { + readonly INVALID_ARGUMENT: "invalid-argument"; + readonly INVALID_TOKEN: "invalid-token"; + readonly EXPIRED_TOKEN: "expired-token"; +}; + +// @public +export type PhoneNumberVerificationErrorCode = typeof PhoneNumberVerificationErrorCode[keyof typeof PhoneNumberVerificationErrorCode]; + // @public export interface PhoneNumberVerificationToken { [key: string]: any; diff --git a/etc/firebase-admin.project-management.api.md b/etc/firebase-admin.project-management.api.md index 4fed0d715a..061aaa7c38 100644 --- a/etc/firebase-admin.project-management.api.md +++ b/etc/firebase-admin.project-management.api.md @@ -45,6 +45,8 @@ export enum AppPlatform { // // @public export class FirebaseProjectManagementError extends PrefixedFirebaseError { + // Warning: (ae-forgotten-export) The symbol "ErrorInfo" needs to be exported by the entry point index.d.ts + constructor(info: ErrorInfo, message?: string); } // Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts @@ -83,8 +85,21 @@ export class ProjectManagement { shaCertificate(shaHash: string): ShaCertificate; } -// @public (undocumented) -export type ProjectManagementErrorCode = 'already-exists' | 'authentication-error' | 'internal-error' | 'invalid-argument' | 'invalid-project-id' | 'invalid-server-response' | 'not-found' | 'service-unavailable' | 'unknown-error'; +// @public +export const ProjectManagementErrorCode: { + readonly ALREADY_EXISTS: "already-exists"; + readonly AUTHENTICATION_ERROR: "authentication-error"; + readonly INTERNAL_ERROR: "internal-error"; + readonly INVALID_ARGUMENT: "invalid-argument"; + readonly INVALID_PROJECT_ID: "invalid-project-id"; + readonly INVALID_SERVER_RESPONSE: "invalid-server-response"; + readonly NOT_FOUND: "not-found"; + readonly SERVICE_UNAVAILABLE: "service-unavailable"; + readonly UNKNOWN_ERROR: "unknown-error"; +}; + +// @public +export type ProjectManagementErrorCode = typeof ProjectManagementErrorCode[keyof typeof ProjectManagementErrorCode]; // @public export class ShaCertificate { diff --git a/etc/firebase-admin.remote-config.api.md b/etc/firebase-admin.remote-config.api.md index 675fdf54a3..eb46af4793 100644 --- a/etc/firebase-admin.remote-config.api.md +++ b/etc/firebase-admin.remote-config.api.md @@ -90,6 +90,14 @@ export interface FetchResponseData { status: number; } +// Warning: (ae-forgotten-export) The symbol "PrefixedFirebaseError" needs to be exported by the entry point index.d.ts +// +// @public +export class FirebaseRemoteConfigError extends PrefixedFirebaseError { + // Warning: (ae-forgotten-export) The symbol "ErrorInfo" needs to be exported by the entry point index.d.ts + constructor(info: ErrorInfo, message?: string); +} + // Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts // // @public @@ -210,6 +218,24 @@ export interface RemoteConfigCondition { tagColor?: TagColor; } +// @public +export const RemoteConfigErrorCode: { + readonly ABORTED: "aborted"; + readonly ALREADY_EXISTS: "already-exists"; + readonly FAILED_PRECONDITION: "failed-precondition"; + readonly INTERNAL_ERROR: "internal-error"; + readonly INVALID_ARGUMENT: "invalid-argument"; + readonly NOT_FOUND: "not-found"; + readonly OUT_OF_RANGE: "out-of-range"; + readonly PERMISSION_DENIED: "permission-denied"; + readonly RESOURCE_EXHAUSTED: "resource-exhausted"; + readonly UNAUTHENTICATED: "unauthenticated"; + readonly UNKNOWN_ERROR: "unknown-error"; +}; + +// @public +export type RemoteConfigErrorCode = typeof RemoteConfigErrorCode[keyof typeof RemoteConfigErrorCode]; + // @public export class RemoteConfigFetchResponse { constructor(app: App, serverConfig: ServerConfig, requestEtag?: string); diff --git a/etc/firebase-admin.security-rules.api.md b/etc/firebase-admin.security-rules.api.md index 13c3be8289..c9569fdde0 100644 --- a/etc/firebase-admin.security-rules.api.md +++ b/etc/firebase-admin.security-rules.api.md @@ -6,6 +6,14 @@ import { Agent } from 'http'; +// Warning: (ae-forgotten-export) The symbol "PrefixedFirebaseError" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export class FirebaseSecurityRulesError extends PrefixedFirebaseError { + // Warning: (ae-forgotten-export) The symbol "ErrorInfo" needs to be exported by the entry point index.d.ts + constructor(info: ErrorInfo, message?: string); +} + // Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts // // @public @@ -56,4 +64,20 @@ export class SecurityRules { releaseStorageRulesetFromSource(source: string | Buffer, bucket?: string): Promise; } +// @public +export const SecurityRulesErrorCode: { + readonly ALREADY_EXISTS: "already-exists"; + readonly AUTHENTICATION_ERROR: "authentication-error"; + readonly INTERNAL_ERROR: "internal-error"; + readonly INVALID_ARGUMENT: "invalid-argument"; + readonly INVALID_SERVER_RESPONSE: "invalid-server-response"; + readonly NOT_FOUND: "not-found"; + readonly RESOURCE_EXHAUSTED: "resource-exhausted"; + readonly SERVICE_UNAVAILABLE: "service-unavailable"; + readonly UNKNOWN_ERROR: "unknown-error"; +}; + +// @public +export type SecurityRulesErrorCode = typeof SecurityRulesErrorCode[keyof typeof SecurityRulesErrorCode]; + ``` diff --git a/src/app-check/app-check-api-client-internal.ts b/src/app-check/app-check-api-client-internal.ts index 8a1afa872e..5dd2f17333 100644 --- a/src/app-check/app-check-api-client-internal.ts +++ b/src/app-check/app-check-api-client-internal.ts @@ -20,10 +20,11 @@ import { FirebaseApp } from '../app/firebase-app'; import { HttpRequestConfig, HttpClient, RequestResponseError, AuthorizedHttpClient, RequestResponse } from '../utils/api-request'; -import { PrefixedFirebaseError } from '../utils/error'; +import { PrefixedFirebaseError, toHttpResponse } from '../utils/error'; +import { FirebaseAppCheckError, AppCheckErrorCode, APP_CHECK_ERROR_CODE_MAPPING } from './error'; import * as utils from '../utils/index'; import * as validator from '../utils/validator'; -import { AppCheckToken } from './app-check-api' +import { AppCheckToken } from './app-check-api'; // App Check backend constants const FIREBASE_APP_CHECK_V1_API_URL_FORMAT = 'https://firebaseappcheck.googleapis.com/v1/projects/{projectId}/apps/{appId}:exchangeCustomToken'; @@ -44,9 +45,10 @@ export class AppCheckApiClient { constructor(private readonly app: App) { if (!validator.isNonNullObject(app) || !('options' in app)) { - throw new FirebaseAppCheckError( - 'invalid-argument', - 'First argument passed to admin.appCheck() must be a valid Firebase app instance.'); + throw new FirebaseAppCheckError({ + code: 'invalid-argument', + message: 'First argument passed to admin.appCheck() must be a valid Firebase app instance.' + }); } this.httpClient = new AuthorizedHttpClient(app as FirebaseApp); } @@ -60,14 +62,16 @@ export class AppCheckApiClient { */ public exchangeToken(customToken: string, appId: string): Promise { if (!validator.isNonEmptyString(appId)) { - throw new FirebaseAppCheckError( - 'invalid-argument', - '`appId` must be a non-empty string.'); + throw new FirebaseAppCheckError({ + code: 'invalid-argument', + message: '`appId` must be a non-empty string.' + }); } if (!validator.isNonEmptyString(customToken)) { - throw new FirebaseAppCheckError( - 'invalid-argument', - '`customToken` must be a non-empty string.'); + throw new FirebaseAppCheckError({ + code: 'invalid-argument', + message: '`customToken` must be a non-empty string.' + }); } return this.getUrl(appId) .then((url) => { @@ -89,9 +93,10 @@ export class AppCheckApiClient { public verifyReplayProtection(token: string): Promise { if (!validator.isNonEmptyString(token)) { - throw new FirebaseAppCheckError( - 'invalid-argument', - '`token` must be a non-empty string.'); + throw new FirebaseAppCheckError({ + code: 'invalid-argument', + message: '`token` must be a non-empty string.' + }); } return this.getVerifyTokenUrl() .then((url) => { @@ -106,8 +111,11 @@ export class AppCheckApiClient { .then((resp) => { if (typeof resp.data.alreadyConsumed !== 'undefined' && !validator.isBoolean(resp.data?.alreadyConsumed)) { - throw new FirebaseAppCheckError( - 'invalid-argument', '`alreadyConsumed` must be a boolean value.'); + throw new FirebaseAppCheckError({ + code: 'invalid-argument', + message: '`alreadyConsumed` must be a boolean value.', + httpResponse: toHttpResponse(resp) + }); } return resp.data.alreadyConsumed || false; }) @@ -146,11 +154,12 @@ export class AppCheckApiClient { return utils.findProjectId(this.app) .then((projectId) => { if (!validator.isNonEmptyString(projectId)) { - throw new FirebaseAppCheckError( - 'unknown-error', - 'Failed to determine project ID. Initialize the ' - + 'SDK with service account credentials or set project ID as an app option. ' - + 'Alternatively, set the GOOGLE_CLOUD_PROJECT environment variable.'); + throw new FirebaseAppCheckError({ + code: 'unknown-error', + message: 'Failed to determine project ID. Initialize the ' + + 'SDK with service account credentials or set project ID as an app option. ' + + 'Alternatively, set the GOOGLE_CLOUD_PROJECT environment variable.' + }); } this.projectId = projectId; return projectId; @@ -164,18 +173,21 @@ export class AppCheckApiClient { const response = err.response; if (!response.isJson()) { - return new FirebaseAppCheckError( - 'unknown-error', - `Unexpected response with status: ${response.status} and body: ${response.text}`); + return new FirebaseAppCheckError({ + code: 'unknown-error', + message: `Unexpected response with status: ${response.status} and body: ${response.text}`, + httpResponse: toHttpResponse(response), + cause: err + }); } - const error: Error = (response.data as ErrorResponse).error || {}; + const error: AppCheckApiError = (response.data as ErrorResponse).error || {}; let code: AppCheckErrorCode = 'unknown-error'; if (error.status && error.status in APP_CHECK_ERROR_CODE_MAPPING) { code = APP_CHECK_ERROR_CODE_MAPPING[error.status]; } - const message = error.message || `Unknown server error: ${response.text}`; - return new FirebaseAppCheckError(code, message); + const message = error.message || 'Unknown server error'; + return new FirebaseAppCheckError({ code, message, httpResponse: toHttpResponse(response), cause: err }); } /** @@ -192,7 +204,7 @@ export class AppCheckApiClient { return { token, ttlMillis - } + }; } /** @@ -207,8 +219,10 @@ export class AppCheckApiClient { */ private stringToMilliseconds(duration: string): number { if (!validator.isNonEmptyString(duration) || !duration.endsWith('s')) { - throw new FirebaseAppCheckError( - 'invalid-argument', '`ttl` must be a valid duration string with the suffix `s`.'); + throw new FirebaseAppCheckError({ + code: 'invalid-argument', + message: '`ttl` must be a valid duration string with the suffix `s`.' + }); } const seconds = duration.slice(0, -1); return Math.floor(Number(seconds) * 1000); @@ -216,52 +230,11 @@ export class AppCheckApiClient { } interface ErrorResponse { - error?: Error; + error?: AppCheckApiError; } -interface Error { +interface AppCheckApiError { code?: number; message?: string; status?: string; } - -export const APP_CHECK_ERROR_CODE_MAPPING: { [key: string]: AppCheckErrorCode } = { - ABORTED: 'aborted', - INVALID_ARGUMENT: 'invalid-argument', - INVALID_CREDENTIAL: 'invalid-credential', - INTERNAL: 'internal-error', - PERMISSION_DENIED: 'permission-denied', - UNAUTHENTICATED: 'unauthenticated', - NOT_FOUND: 'not-found', - UNKNOWN: 'unknown-error', -}; - -export type AppCheckErrorCode = - 'aborted' - | 'invalid-argument' - | 'invalid-credential' - | 'internal-error' - | 'permission-denied' - | 'unauthenticated' - | 'not-found' - | 'app-check-token-expired' - | 'unknown-error'; - -/** - * Firebase App Check error code structure. This extends PrefixedFirebaseError. - * - * @param code - The error code. - * @param message - The error message. - * @constructor - */ -export class FirebaseAppCheckError extends PrefixedFirebaseError { - constructor(code: AppCheckErrorCode, message: string) { - super('app-check', code, message); - - /* tslint:disable:max-line-length */ - // Set the prototype explicitly. See the following link for more details: - // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work - /* tslint:enable:max-line-length */ - (this as any).__proto__ = FirebaseAppCheckError.prototype; - } -} diff --git a/src/app-check/app-check.ts b/src/app-check/app-check.ts index c7e6cadbd1..99a2b010d3 100644 --- a/src/app-check/app-check.ts +++ b/src/app-check/app-check.ts @@ -18,7 +18,8 @@ import * as validator from '../utils/validator'; import { App } from '../app'; -import { AppCheckApiClient, FirebaseAppCheckError } from './app-check-api-client-internal'; +import { AppCheckApiClient } from './app-check-api-client-internal'; +import { FirebaseAppCheckError } from './error'; import { appCheckErrorFromCryptoSignerError, AppCheckTokenGenerator, } from './token-generator'; @@ -110,9 +111,10 @@ export class AppCheck { return; } if (!validator.isNonNullObject(options)) { - throw new FirebaseAppCheckError( - 'invalid-argument', - 'VerifyAppCheckTokenOptions must be a non-null object.'); + throw new FirebaseAppCheckError({ + code: 'invalid-argument', + message: 'VerifyAppCheckTokenOptions must be a non-null object.' + }); } } } diff --git a/src/app-check/error.ts b/src/app-check/error.ts new file mode 100644 index 0000000000..5b25b29ba4 --- /dev/null +++ b/src/app-check/error.ts @@ -0,0 +1,62 @@ +/*! + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PrefixedFirebaseError, ErrorInfo } from '../utils/error'; + +/** @const {Record} App Check server to client error code mapping. */ +export const APP_CHECK_ERROR_CODE_MAPPING: Record = { + ABORTED: 'aborted', + INVALID_ARGUMENT: 'invalid-argument', + INVALID_CREDENTIAL: 'invalid-credential', + INTERNAL: 'internal-error', + PERMISSION_DENIED: 'permission-denied', + UNAUTHENTICATED: 'unauthenticated', + NOT_FOUND: 'not-found', + UNKNOWN: 'unknown-error', +}; + +/** + * The constant mapping for valid App Check client error codes. + */ +export const AppCheckErrorCode = { + ABORTED: 'aborted', + INVALID_ARGUMENT: 'invalid-argument', + INVALID_CREDENTIAL: 'invalid-credential', + INTERNAL: 'internal-error', + PERMISSION_DENIED: 'permission-denied', + UNAUTHENTICATED: 'unauthenticated', + NOT_FOUND: 'not-found', + APP_CHECK_TOKEN_EXPIRED: 'app-check-token-expired', + UNKNOWN: 'unknown-error', +} as const; + +/** + * The type definition for valid App Check client error codes. + */ +export type AppCheckErrorCode = typeof AppCheckErrorCode[keyof typeof AppCheckErrorCode]; + +/** + * Firebase App Check error type. This extends PrefixedFirebaseError. + */ +export class FirebaseAppCheckError extends PrefixedFirebaseError { + /** + * @param info - The error code info. + * @param message - The error message. If provided, this will override the default message. + */ + constructor(info: ErrorInfo, message?: string) { + super('app-check', info.code, message || info.message, info.httpResponse, info.cause); + } +} diff --git a/src/app-check/index.ts b/src/app-check/index.ts index 7b4ba04709..4d9605f132 100644 --- a/src/app-check/index.ts +++ b/src/app-check/index.ts @@ -68,3 +68,5 @@ export function getAppCheck(app?: App): AppCheck { const firebaseApp: FirebaseApp = app as FirebaseApp; return firebaseApp.getOrInitService('appCheck', (app) => new AppCheck(app)); } + +export { FirebaseAppCheckError, AppCheckErrorCode } from './error'; \ No newline at end of file diff --git a/src/app-check/token-generator.ts b/src/app-check/token-generator.ts index be3383d9d0..08bf9a7579 100644 --- a/src/app-check/token-generator.ts +++ b/src/app-check/token-generator.ts @@ -22,9 +22,10 @@ import { FirebaseAppCheckError, AppCheckErrorCode, APP_CHECK_ERROR_CODE_MAPPING, -} from './app-check-api-client-internal'; +} from './error'; import { AppCheckTokenOptions } from './app-check-api'; import { RequestResponseError } from '../utils/api-request'; +import { toHttpResponse } from '../utils/error'; const ONE_MINUTE_IN_SECONDS = 60; const ONE_MINUTE_IN_MILLIS = ONE_MINUTE_IN_SECONDS * 1000; @@ -50,9 +51,10 @@ export class AppCheckTokenGenerator { */ constructor(signer: CryptoSigner) { if (!validator.isNonNullObject(signer)) { - throw new FirebaseAppCheckError( - 'invalid-argument', - 'INTERNAL ASSERT: Must provide a CryptoSigner to use AppCheckTokenGenerator.'); + throw new FirebaseAppCheckError({ + code: 'invalid-argument', + message: 'INTERNAL ASSERT: Must provide a CryptoSigner to use AppCheckTokenGenerator.' + }); } this.signer = signer; } @@ -67,9 +69,10 @@ export class AppCheckTokenGenerator { */ public createCustomToken(appId: string, options?: AppCheckTokenOptions): Promise { if (!validator.isNonEmptyString(appId)) { - throw new FirebaseAppCheckError( - 'invalid-argument', - '`appId` must be a non-empty string.'); + throw new FirebaseAppCheckError({ + code: 'invalid-argument', + message: '`appId` must be a non-empty string.' + }); } let customOptions = {}; if (typeof options !== 'undefined') { @@ -114,20 +117,24 @@ export class AppCheckTokenGenerator { */ private validateTokenOptions(options: AppCheckTokenOptions): {[key: string]: any} { if (!validator.isNonNullObject(options)) { - throw new FirebaseAppCheckError( - 'invalid-argument', - 'AppCheckTokenOptions must be a non-null object.'); + throw new FirebaseAppCheckError({ + code: 'invalid-argument', + message: 'AppCheckTokenOptions must be a non-null object.' + }); } if (typeof options.ttlMillis !== 'undefined') { if (!validator.isNumber(options.ttlMillis)) { - throw new FirebaseAppCheckError('invalid-argument', - 'ttlMillis must be a duration in milliseconds.'); + throw new FirebaseAppCheckError({ + code: 'invalid-argument', + message: 'ttlMillis must be a duration in milliseconds.' + }); } // ttlMillis must be between 30 minutes and 7 days (inclusive) if (options.ttlMillis < (ONE_MINUTE_IN_MILLIS * 30) || options.ttlMillis > (ONE_DAY_IN_MILLIS * 7)) { - throw new FirebaseAppCheckError( - 'invalid-argument', - 'ttlMillis must be a duration in milliseconds between 30 minutes and 7 days (inclusive).'); + throw new FirebaseAppCheckError({ + code: 'invalid-argument', + message: 'ttlMillis must be a duration in milliseconds between 30 minutes and 7 days (inclusive).' + }); } return { ttl: transformMillisecondsToSecondsString(options.ttlMillis) }; } @@ -151,21 +158,31 @@ export function appCheckErrorFromCryptoSignerError(err: Error): Error { const errorResponse = httpError.response.data; if (errorResponse?.error) { const status = errorResponse.error.status; - const description = errorResponse.error.message || JSON.stringify(httpError.response); + const description = errorResponse.error.message || 'Unknown server error'; let code: AppCheckErrorCode = 'unknown-error'; if (status && status in APP_CHECK_ERROR_CODE_MAPPING) { code = APP_CHECK_ERROR_CODE_MAPPING[status]; } - return new FirebaseAppCheckError(code, - `Error returned from server while signing a custom token: ${description}` - ); + return new FirebaseAppCheckError({ + code, + message: `Error returned from server while signing a custom token: ${description}`, + httpResponse: toHttpResponse(httpError.response), + cause: err + }); } - return new FirebaseAppCheckError('internal-error', - 'Error returned from server: ' + JSON.stringify(errorResponse) + '.' - ); + return new FirebaseAppCheckError({ + code: 'internal-error', + message: 'Error returned from server.', + httpResponse: toHttpResponse(httpError.response), + cause: err + }); } - return new FirebaseAppCheckError(mapToAppCheckErrorCode(err.code), err.message); + return new FirebaseAppCheckError({ + code: mapToAppCheckErrorCode(err.code), + message: err.message, + cause: err + }); } function mapToAppCheckErrorCode(code: string): AppCheckErrorCode { diff --git a/src/app-check/token-verifier.ts b/src/app-check/token-verifier.ts index 074e556307..e1c5d58504 100644 --- a/src/app-check/token-verifier.ts +++ b/src/app-check/token-verifier.ts @@ -16,7 +16,7 @@ import * as validator from '../utils/validator'; import * as util from '../utils/index'; -import { FirebaseAppCheckError } from './app-check-api-client-internal'; +import { FirebaseAppCheckError } from './error'; import { ALGORITHM_RS256, DecodedToken, decodeJwt, JwtError, JwtErrorCode, PublicKeySignatureVerifier, SignatureVerifier @@ -48,10 +48,10 @@ export class AppCheckTokenVerifier { */ public verifyToken(token: string): Promise { if (!validator.isString(token)) { - throw new FirebaseAppCheckError( - 'invalid-argument', - 'App check token must be a non-null string.', - ); + throw new FirebaseAppCheckError({ + code: 'invalid-argument', + message: 'App check token must be a non-null string.' + }); } return this.ensureProjectId() @@ -69,11 +69,11 @@ export class AppCheckTokenVerifier { return util.findProjectId(this.app) .then((projectId) => { if (!validator.isNonEmptyString(projectId)) { - throw new FirebaseAppCheckError( - 'invalid-credential', - 'Must initialize app with a cert credential or set your Firebase project ID as the ' + - 'GOOGLE_CLOUD_PROJECT environment variable to verify an App Check token.' - ); + throw new FirebaseAppCheckError({ + code: 'invalid-credential', + message: 'Must initialize app with a cert credential or set your Firebase project ID as the ' + + 'GOOGLE_CLOUD_PROJECT environment variable to verify an App Check token.' + }); } return projectId; }) @@ -93,7 +93,10 @@ export class AppCheckTokenVerifier { .catch(() => { const errorMessage = 'Decoding App Check token failed. Make sure you passed ' + 'the entire string JWT which represents the Firebase App Check token.'; - throw new FirebaseAppCheckError('invalid-argument', errorMessage); + throw new FirebaseAppCheckError({ + code: 'invalid-argument', + message: errorMessage + }); }); } @@ -126,7 +129,10 @@ export class AppCheckTokenVerifier { errorMessage = 'The provided App Check token has an empty string "sub" (subject) claim.'; } if (errorMessage) { - throw new FirebaseAppCheckError('invalid-argument', errorMessage); + throw new FirebaseAppCheckError({ + code: 'invalid-argument', + message: errorMessage + }); } } @@ -148,16 +154,32 @@ export class AppCheckTokenVerifier { if (error.code === JwtErrorCode.TOKEN_EXPIRED) { const errorMessage = 'The provided App Check token has expired. Get a fresh App Check token' + ' from your client app and try again.' - return new FirebaseAppCheckError('app-check-token-expired', errorMessage); + return new FirebaseAppCheckError({ + code: 'app-check-token-expired', + message: errorMessage, + cause: error + }); } else if (error.code === JwtErrorCode.INVALID_SIGNATURE) { const errorMessage = 'The provided App Check token has invalid signature.'; - return new FirebaseAppCheckError('invalid-argument', errorMessage); + return new FirebaseAppCheckError({ + code: 'invalid-argument', + message: errorMessage, + cause: error + }); } else if (error.code === JwtErrorCode.NO_MATCHING_KID) { const errorMessage = 'The provided App Check token has "kid" claim which does not ' + 'correspond to a known public key. Most likely the provided App Check token ' + 'is expired, so get a fresh token from your client app and try again.'; - return new FirebaseAppCheckError('invalid-argument', errorMessage); + return new FirebaseAppCheckError({ + code: 'invalid-argument', + message: errorMessage, + cause: error + }); } - return new FirebaseAppCheckError('invalid-argument', error.message); + return new FirebaseAppCheckError({ + code: 'invalid-argument', + message: error.message, + cause: error + }); } } diff --git a/src/app/core.ts b/src/app/core.ts index c229b20408..a2e5d46d37 100644 --- a/src/app/core.ts +++ b/src/app/core.ts @@ -18,6 +18,7 @@ import { Agent } from 'http'; import { Credential } from './credential'; +import { FirebaseError } from '../utils/error'; /** * Available options to pass to {@link firebase-admin.app#initializeApp}. @@ -126,47 +127,7 @@ export interface App { options: AppOptions; } -/** - * `FirebaseError` is a subclass of the standard JavaScript `Error` object. In - * addition to a message string and stack trace, it contains a string code. - */ -export interface FirebaseError { - - /** - * Error codes are strings using the following format: `"service/string-code"`. - * Some examples include `"auth/invalid-uid"` and - * `"messaging/invalid-recipient"`. - * - * While the message for a given error can change, the code will remain the same - * between backward-compatible versions of the Firebase SDK. - */ - code: string; - - /** - * An explanatory message for the error that just occurred. - * - * This message is designed to be helpful to you, the developer. Because - * it generally does not convey meaningful information to end users, - * this message should not be displayed in your application. - */ - message: string; - - /** - * A string value containing the execution backtrace when the error originally - * occurred. - * - * This information can be useful for troubleshooting the cause of the error with - * {@link https://firebase.google.com/support | Firebase Support}. - */ - stack?: string; - /** - * Returns a JSON-serializable object representation of this error. - * - * @returns A JSON-serializable representation of this object. - */ - toJSON(): object; -} /** * Composite type which includes both a `FirebaseError` object and an index diff --git a/src/app/credential-internal.ts b/src/app/credential-internal.ts index 8ce61b4edd..fe89fc3360 100644 --- a/src/app/credential-internal.ts +++ b/src/app/credential-internal.ts @@ -20,7 +20,7 @@ import fs = require('fs'); import { Credentials as GoogleAuthCredentials, GoogleAuth, Compute, AnyAuthClient } from 'google-auth-library' import { Agent } from 'http'; import { Credential, GoogleOAuthAccessToken } from './credential'; -import { AppErrorCodes, FirebaseAppError } from '../utils/error'; +import { AppErrorCode, FirebaseAppError } from './error'; import * as util from '../utils/validator'; const SCOPES = [ @@ -94,10 +94,10 @@ export class ApplicationDefaultCredential implements Credential { return (this.authClient as Compute).fetchIdToken(audience); } else { - throw new FirebaseAppError( - AppErrorCodes.INVALID_CREDENTIAL, - 'Credentials type should be Compute Engine Credentials.', - ); + throw new FirebaseAppError({ + code: AppErrorCode.INVALID_CREDENTIAL, + message: 'Credentials type should be Compute Engine Credentials.' + }); } } @@ -182,19 +182,20 @@ class ServiceAccount { return new ServiceAccount(JSON.parse(fs.readFileSync(filePath, 'utf8'))); } catch (error) { // Throw a nicely formed error message if the file contents cannot be parsed - throw new FirebaseAppError( - AppErrorCodes.INVALID_CREDENTIAL, - 'Failed to parse service account json file: ' + error, - ); + throw new FirebaseAppError({ + code: AppErrorCode.INVALID_CREDENTIAL, + message: `Failed to parse service account json file: ${(error as Error).message}`, + cause: error, + }); } } constructor(json: object) { if (!util.isNonNullObject(json)) { - throw new FirebaseAppError( - AppErrorCodes.INVALID_CREDENTIAL, - 'Service account must be an object.', - ); + throw new FirebaseAppError({ + code: AppErrorCode.INVALID_CREDENTIAL, + message: 'Service account must be an object.' + }); } copyAttr(this, json, 'projectId', 'project_id'); @@ -211,7 +212,7 @@ class ServiceAccount { } if (typeof errorMessage !== 'undefined') { - throw new FirebaseAppError(AppErrorCodes.INVALID_CREDENTIAL, errorMessage); + throw new FirebaseAppError({ code: AppErrorCode.INVALID_CREDENTIAL, message: errorMessage }); } // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -219,9 +220,11 @@ class ServiceAccount { try { forge.pki.privateKeyFromPem(this.privateKey); } catch (error) { - throw new FirebaseAppError( - AppErrorCodes.INVALID_CREDENTIAL, - 'Failed to parse private key: ' + error); + throw new FirebaseAppError({ + code: AppErrorCode.INVALID_CREDENTIAL, + message: 'Failed to parse private key.', + cause: error, + }); } } } @@ -292,10 +295,11 @@ class RefreshToken { RefreshToken.validateFromJSON(JSON.parse(fs.readFileSync(filePath, 'utf8'))); } catch (error) { // Throw a nicely formed error message if the file contents cannot be parsed - throw new FirebaseAppError( - AppErrorCodes.INVALID_CREDENTIAL, - 'Failed to parse refresh token file: ' + error, - ); + throw new FirebaseAppError({ + code: AppErrorCode.INVALID_CREDENTIAL, + message: 'Failed to parse refresh token file.', + cause: error, + }); } } @@ -320,7 +324,7 @@ class RefreshToken { } if (typeof errorMessage !== 'undefined') { - throw new FirebaseAppError(AppErrorCodes.INVALID_CREDENTIAL, errorMessage); + throw new FirebaseAppError({ code: AppErrorCode.INVALID_CREDENTIAL, message: errorMessage }); } } } @@ -389,10 +393,11 @@ class ImpersonatedServiceAccount { ImpersonatedServiceAccount.validateFromJSON(JSON.parse(fs.readFileSync(filePath, 'utf8'))); } catch (error) { // Throw a nicely formed error message if the file contents cannot be parsed - throw new FirebaseAppError( - AppErrorCodes.INVALID_CREDENTIAL, - 'Failed to parse impersonated service account file: ' + error, - ); + throw new FirebaseAppError({ + code: AppErrorCode.INVALID_CREDENTIAL, + message: 'Failed to parse impersonated service account file.', + cause: error, + }); } } @@ -413,7 +418,7 @@ class ImpersonatedServiceAccount { } if (typeof errorMessage !== 'undefined') { - throw new FirebaseAppError(AppErrorCodes.INVALID_CREDENTIAL, errorMessage); + throw new FirebaseAppError({ code: AppErrorCode.INVALID_CREDENTIAL, message: errorMessage }); } } } @@ -468,10 +473,10 @@ function populateGoogleAuth(keyFile: string | object, httpAgent?: Agent) if (typeof keyFile === 'object') { if (!util.isNonNullObject(keyFile)) { - throw new FirebaseAppError( - AppErrorCodes.INVALID_CREDENTIAL, - 'Service account must be an object.', - ); + throw new FirebaseAppError({ + code: AppErrorCode.INVALID_CREDENTIAL, + message: 'Service account must be an object.' + }); } copyAttr(keyFile, keyFile, 'project_id', 'projectId'); copyAttr(keyFile, keyFile, 'private_key', 'privateKey'); @@ -490,15 +495,15 @@ function populateCredential(credentials?: GoogleAuthCredentials): GoogleOAuthAcc const expiryDate = credentials?.expiry_date; if (typeof accessToken !== 'string') - throw new FirebaseAppError( - AppErrorCodes.INVALID_CREDENTIAL, - 'Failed to parse Google auth credential: access_token must be a non empty string.', - ); + throw new FirebaseAppError({ + code: AppErrorCode.INVALID_CREDENTIAL, + message: 'Failed to parse Google auth credential: access_token must be a non empty string.' + }); if (typeof expiryDate !== 'number') - throw new FirebaseAppError( - AppErrorCodes.INVALID_CREDENTIAL, - 'Failed to parse Google auth credential: Invalid expiry_date.', - ); + throw new FirebaseAppError({ + code: AppErrorCode.INVALID_CREDENTIAL, + message: 'Failed to parse Google auth credential: Invalid expiry_date.' + }); return { ...credentials, diff --git a/src/app/error.ts b/src/app/error.ts new file mode 100644 index 0000000000..76122c397d --- /dev/null +++ b/src/app/error.ts @@ -0,0 +1,55 @@ +/*! + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ErrorInfo, PrefixedFirebaseError } from '../utils/error'; + +/** + * Firebase App error code structure. This extends PrefixedFirebaseError. + */ +export class FirebaseAppError extends PrefixedFirebaseError { + /** + * @param info - The error code info. + * @param message - The error message. This will override the default message if provided. + */ + constructor(info: ErrorInfo, message?: string) { + // Override default message if custom message provided. + super('app', info.code, message || info.message, info.httpResponse, info.cause); + } +} + +/** + * The constant mapping for valid App client error codes. + */ +export const AppErrorCode = { + APP_DELETED: 'app-deleted', + DUPLICATE_APP: 'duplicate-app', + INVALID_ARGUMENT: 'invalid-argument', + INTERNAL_ERROR: 'internal-error', + INVALID_APP_NAME: 'invalid-app-name', + INVALID_APP_OPTIONS: 'invalid-app-options', + INVALID_CREDENTIAL: 'invalid-credential', + NETWORK_ERROR: 'network-error', + NETWORK_TIMEOUT: 'network-timeout', + NO_APP: 'no-app', + UNABLE_TO_PARSE_RESPONSE: 'unable-to-parse-response', +} as const; + +/** + * The type definition for valid App client error codes. + */ +export type AppErrorCode = typeof AppErrorCode[keyof typeof AppErrorCode]; + + diff --git a/src/app/firebase-app.ts b/src/app/firebase-app.ts index dcd30d8a1d..bc48772f9f 100644 --- a/src/app/firebase-app.ts +++ b/src/app/firebase-app.ts @@ -21,7 +21,7 @@ import { Credential } from './credential'; import { getApplicationDefault } from './credential-internal'; import * as validator from '../utils/validator'; import { deepCopy } from '../utils/deep-copy'; -import { AppErrorCodes, FirebaseAppError } from '../utils/error'; +import { AppErrorCode, FirebaseAppError } from './error'; const TOKEN_EXPIRY_THRESHOLD_MILLIS = 5 * 60 * 1000; @@ -69,12 +69,12 @@ export class FirebaseAppInternals { if (!validator.isNonNullObject(result) || typeof result.expires_in !== 'number' || typeof result.access_token !== 'string') { - throw new FirebaseAppError( - AppErrorCodes.INVALID_CREDENTIAL, - `Invalid access token generated: "${JSON.stringify(result)}". Valid access ` + - 'tokens must be an object with the "expires_in" (number) and "access_token" ' + + throw new FirebaseAppError({ + code: AppErrorCode.INVALID_CREDENTIAL, + message: `Invalid access token generated: "${JSON.stringify(result)}". Valid access ` + + 'tokens must be an object with the "expires_in" (number) and "access_token" ' + '(string) properties.', - ); + }); } const token = { @@ -110,7 +110,11 @@ export class FirebaseAppInternals { 'https://console.firebase.google.com/project/_/settings/serviceaccounts/adminsdk.'; } - throw new FirebaseAppError(AppErrorCodes.INVALID_CREDENTIAL, errorMessage); + throw new FirebaseAppError({ + code: AppErrorCode.INVALID_CREDENTIAL, + message: errorMessage, + cause: error as Error + }); }) .finally(() => { this.isRefreshing = false; @@ -166,11 +170,11 @@ export class FirebaseApp implements App { this.autoInit_ = autoInit; if (!validator.isNonNullObject(this.options_)) { - throw new FirebaseAppError( - AppErrorCodes.INVALID_APP_OPTIONS, - 'Invalid Firebase app options passed as the first argument to initializeApp() for the ' + - `app named "${this.name_}". Options must be a non-null object.`, - ); + throw new FirebaseAppError({ + code: AppErrorCode.INVALID_APP_OPTIONS, + message: 'Invalid Firebase app options passed as the first argument to initializeApp() for the ' + + `app named "${this.name_}". Options must be a non-null object.` + }); } const hasCredential = ('credential' in this.options_); @@ -181,12 +185,12 @@ export class FirebaseApp implements App { const credential = this.options_.credential; if (typeof credential !== 'object' || credential === null || typeof credential.getAccessToken !== 'function') { - throw new FirebaseAppError( - AppErrorCodes.INVALID_APP_OPTIONS, - 'Invalid Firebase app options passed as the first argument to initializeApp() for the ' + - `app named "${this.name_}". The "credential" property must be an object which implements ` + - 'the Credential interface.', - ); + throw new FirebaseAppError({ + code: AppErrorCode.INVALID_APP_OPTIONS, + message: 'Invalid Firebase app options passed as the first argument to initializeApp() for the ' + + `app named "${this.name_}". The "credential" property must be an object which implements ` + + 'the Credential interface.' + }); } this.INTERNAL = new FirebaseAppInternals(credential); @@ -279,10 +283,10 @@ export class FirebaseApp implements App { // eslint-disable-next-line @typescript-eslint/naming-convention private checkDestroyed_(): void { if (this.isDeleted_) { - throw new FirebaseAppError( - AppErrorCodes.APP_DELETED, - `Firebase app named "${this.name_}" has already been deleted.`, - ); + throw new FirebaseAppError({ + code: AppErrorCode.APP_DELETED, + message: `Firebase app named "${this.name_}" has already been deleted.` + }); } } } diff --git a/src/app/index.ts b/src/app/index.ts index b10323b8f9..b491c0ad93 100644 --- a/src/app/index.ts +++ b/src/app/index.ts @@ -23,12 +23,13 @@ import { getSdkVersion } from '../utils'; * @packageDocumentation */ -export { App, AppOptions, FirebaseArrayIndexError, FirebaseError } from './core' +export { App, AppOptions, FirebaseArrayIndexError } from './core'; export { initializeApp, getApp, getApps, deleteApp } from './lifecycle'; export { Credential, ServiceAccount, GoogleOAuthAccessToken } from './credential'; export { applicationDefault, cert, refreshToken } from './credential-factory'; -export { FirebaseAppError, AppErrorCodes } from '../utils/error'; +export { FirebaseError, ErrorInfo, HttpResponse } from '../utils/error'; +export { FirebaseAppError, AppErrorCode } from './error'; export const SDK_VERSION = getSdkVersion(); diff --git a/src/app/lifecycle.ts b/src/app/lifecycle.ts index d2b5898451..b2414ab3b6 100644 --- a/src/app/lifecycle.ts +++ b/src/app/lifecycle.ts @@ -18,7 +18,7 @@ import fs = require('fs'); import * as validator from '../utils/validator'; -import { AppErrorCodes, FirebaseAppError } from '../utils/error'; +import { AppErrorCode, FirebaseAppError } from './error'; import { App, AppOptions } from './core'; import { getApplicationDefault } from './credential-internal'; import { FirebaseApp } from './firebase-app'; @@ -51,10 +51,10 @@ export class AppStore { const currentApp = this.appStore.get(appName)!; // Ensure the `autoInit` state matches the existing app's. If not, throw. if (currentApp.autoInit() !== autoInit) { - throw new FirebaseAppError( - AppErrorCodes.INVALID_APP_OPTIONS, - `A Firebase app named "${appName}" already exists with a different configuration.` - ) + throw new FirebaseAppError({ + code: AppErrorCode.INVALID_APP_OPTIONS, + message: `A Firebase app named "${appName}" already exists with a different configuration.` + }); } if (autoInit) { @@ -72,10 +72,10 @@ export class AppStore { const currentAppOptions = { ...currentApp.options }; delete currentAppOptions.credential; if (!fastDeepEqual(options, currentAppOptions)) { - throw new FirebaseAppError( - AppErrorCodes.DUPLICATE_APP, - `A Firebase app named "${appName}" already exists with a different configuration.` - ); + throw new FirebaseAppError({ + code: AppErrorCode.DUPLICATE_APP, + message: `A Firebase app named "${appName}" already exists with a different configuration.` + }); } @@ -89,7 +89,7 @@ export class AppStore { ? 'The default Firebase app does not exist. ' : `Firebase app named "${appName}" does not exist. `; errorMessage += 'Make sure you call initializeApp() before using any of the Firebase services.'; - throw new FirebaseAppError(AppErrorCodes.NO_APP, errorMessage); + throw new FirebaseAppError({ code: AppErrorCode.NO_APP, message: errorMessage }); } return this.appStore.get(appName)!; @@ -102,7 +102,7 @@ export class AppStore { public deleteApp(app: App): Promise { if (typeof app !== 'object' || app === null || !('options' in app)) { - throw new FirebaseAppError(AppErrorCodes.INVALID_ARGUMENT, 'Invalid app argument.'); + throw new FirebaseAppError({ code: AppErrorCode.INVALID_ARGUMENT, message: 'Invalid app argument.' }); } // Make sure the given app already exists. @@ -151,38 +151,38 @@ function validateAppOptionsSupportDeepEquals( // http.Agent checks. if (typeof requestedOptions.httpAgent !== 'undefined') { - throw new FirebaseAppError( - AppErrorCodes.INVALID_APP_OPTIONS, - `Firebase app named "${existingApp.name}" already exists and initializeApp was` + - ' invoked with an optional http.Agent. The SDK cannot confirm the equality' + - ' of http.Agent objects with the existing app. Please use getApp or getApps to reuse' + - ' the existing app instead.' - ); + throw new FirebaseAppError({ + code: AppErrorCode.INVALID_APP_OPTIONS, + message: `Firebase app named "${existingApp.name}" already exists and initializeApp was` + + ' invoked with an optional http.Agent. The SDK cannot confirm the equality' + + ' of http.Agent objects with the existing app. Please use getApp or getApps to reuse' + + ' the existing app instead.' + }); } else if (typeof existingApp.options.httpAgent !== 'undefined') { - throw new FirebaseAppError( - AppErrorCodes.INVALID_APP_OPTIONS, - `An existing app named "${existingApp.name}" already exists with a different` + - ' options configuration: httpAgent.' - ); + throw new FirebaseAppError({ + code: AppErrorCode.INVALID_APP_OPTIONS, + message: `An existing app named "${existingApp.name}" already exists with a different` + + ' options configuration: httpAgent.' + }); } // Credential checks. if (typeof requestedOptions.credential !== 'undefined') { - throw new FirebaseAppError( - AppErrorCodes.INVALID_APP_OPTIONS, - `Firebase app named "${existingApp.name}" already exists and initializeApp was` + - ' invoked with an optional Credential. The SDK cannot confirm the equality' + - ' of Credential objects with the existing app. Please use getApp or getApps' + - ' to reuse the existing app instead.' - ); + throw new FirebaseAppError({ + code: AppErrorCode.INVALID_APP_OPTIONS, + message: `Firebase app named "${existingApp.name}" already exists and initializeApp was` + + ' invoked with an optional Credential. The SDK cannot confirm the equality' + + ' of Credential objects with the existing app. Please use getApp or getApps' + + ' to reuse the existing app instead.' + }); } if (existingApp.customCredential()) { - throw new FirebaseAppError( - AppErrorCodes.INVALID_APP_OPTIONS, - `An existing app named "${existingApp.name}" already exists with a different` + - ' options configuration: Credential.' - ); + throw new FirebaseAppError({ + code: AppErrorCode.INVALID_APP_OPTIONS, + message: `An existing app named "${existingApp.name}" already exists with a different` + + ' options configuration: Credential.' + }); } } @@ -198,10 +198,10 @@ function validateAppOptionsSupportDeepEquals( */ function validateAppNameFormat(appName: string): void { if (!validator.isNonEmptyString(appName)) { - throw new FirebaseAppError( - AppErrorCodes.INVALID_APP_NAME, - `Invalid Firebase app name "${appName}" provided. App name must be a non-empty string.`, - ); + throw new FirebaseAppError({ + code: AppErrorCode.INVALID_APP_NAME, + message: `Invalid Firebase app name "${appName}" provided. App name must be a non-empty string.` + }); } } @@ -320,9 +320,10 @@ function loadOptionsFromEnvVar(): AppOptions { return JSON.parse(contents) as AppOptions; } catch (error) { // Throw a nicely formed error message if the file contents cannot be parsed - throw new FirebaseAppError( - AppErrorCodes.INVALID_APP_OPTIONS, - 'Failed to parse app options file: ' + error, - ); + throw new FirebaseAppError({ + code: AppErrorCode.INVALID_APP_OPTIONS, + message: `Failed to parse app options file: ${(error as Error).message}`, + cause: error as Error + }); } } diff --git a/src/auth/action-code-settings-builder.ts b/src/auth/action-code-settings-builder.ts index 5ffd72d048..f10fda20c1 100644 --- a/src/auth/action-code-settings-builder.ts +++ b/src/auth/action-code-settings-builder.ts @@ -15,7 +15,7 @@ */ import * as validator from '../utils/validator'; -import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; +import { authClientErrorCode, FirebaseAuthError } from './error'; /** * This is the interface that defines the required continue/state URL with @@ -147,17 +147,17 @@ export class ActionCodeSettingsBuilder { constructor(actionCodeSettings: ActionCodeSettings) { if (!validator.isNonNullObject(actionCodeSettings)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '"ActionCodeSettings" must be a non-null object.', ); } if (typeof actionCodeSettings.url === 'undefined') { throw new FirebaseAuthError( - AuthClientErrorCode.MISSING_CONTINUE_URI, + authClientErrorCode.MISSING_CONTINUE_URI, ); } else if (!validator.isURL(actionCodeSettings.url)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONTINUE_URI, + authClientErrorCode.INVALID_CONTINUE_URI, ); } this.continueUrl = actionCodeSettings.url; @@ -165,7 +165,7 @@ export class ActionCodeSettingsBuilder { if (typeof actionCodeSettings.handleCodeInApp !== 'undefined' && !validator.isBoolean(actionCodeSettings.handleCodeInApp)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '"ActionCodeSettings.handleCodeInApp" must be a boolean.', ); } @@ -174,7 +174,7 @@ export class ActionCodeSettingsBuilder { if (typeof actionCodeSettings.dynamicLinkDomain !== 'undefined' && !validator.isNonEmptyString(actionCodeSettings.dynamicLinkDomain)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_DYNAMIC_LINK_DOMAIN, + authClientErrorCode.INVALID_DYNAMIC_LINK_DOMAIN, ); } this.dynamicLinkDomain = actionCodeSettings.dynamicLinkDomain; @@ -182,7 +182,7 @@ export class ActionCodeSettingsBuilder { if (typeof actionCodeSettings.linkDomain !== 'undefined' && !validator.isNonEmptyString(actionCodeSettings.linkDomain)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_HOSTING_LINK_DOMAIN, + authClientErrorCode.INVALID_HOSTING_LINK_DOMAIN, ); } this.linkDomain = actionCodeSettings.linkDomain; @@ -190,16 +190,16 @@ export class ActionCodeSettingsBuilder { if (typeof actionCodeSettings.iOS !== 'undefined') { if (!validator.isNonNullObject(actionCodeSettings.iOS)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '"ActionCodeSettings.iOS" must be a valid non-null object.', ); } else if (typeof actionCodeSettings.iOS.bundleId === 'undefined') { throw new FirebaseAuthError( - AuthClientErrorCode.MISSING_IOS_BUNDLE_ID, + authClientErrorCode.MISSING_IOS_BUNDLE_ID, ); } else if (!validator.isNonEmptyString(actionCodeSettings.iOS.bundleId)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '"ActionCodeSettings.iOS.bundleId" must be a valid non-empty string.', ); } @@ -209,28 +209,28 @@ export class ActionCodeSettingsBuilder { if (typeof actionCodeSettings.android !== 'undefined') { if (!validator.isNonNullObject(actionCodeSettings.android)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '"ActionCodeSettings.android" must be a valid non-null object.', ); } else if (typeof actionCodeSettings.android.packageName === 'undefined') { throw new FirebaseAuthError( - AuthClientErrorCode.MISSING_ANDROID_PACKAGE_NAME, + authClientErrorCode.MISSING_ANDROID_PACKAGE_NAME, ); } else if (!validator.isNonEmptyString(actionCodeSettings.android.packageName)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '"ActionCodeSettings.android.packageName" must be a valid non-empty string.', ); } else if (typeof actionCodeSettings.android.minimumVersion !== 'undefined' && !validator.isNonEmptyString(actionCodeSettings.android.minimumVersion)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '"ActionCodeSettings.android.minimumVersion" must be a valid non-empty string.', ); } else if (typeof actionCodeSettings.android.installApp !== 'undefined' && !validator.isBoolean(actionCodeSettings.android.installApp)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '"ActionCodeSettings.android.installApp" must be a valid boolean.', ); } diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index 8e979d1aed..04eb358a65 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -20,9 +20,10 @@ import * as validator from '../utils/validator'; import { App } from '../app/index'; import { FirebaseApp } from '../app/firebase-app'; import { deepCopy, deepExtend } from '../utils/deep-copy'; -import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; +import { authClientErrorCode, FirebaseAuthError } from './error'; +import { toHttpResponse } from '../utils/error'; import { - ApiSettings, AuthorizedHttpClient, HttpRequestConfig, RequestResponseError, + ApiSettings, AuthorizedHttpClient, HttpRequestConfig, RequestResponseError, RequestResponse, } from '../utils/api-request'; import * as utils from '../utils/index'; @@ -34,7 +35,7 @@ import { ActionCodeSettings, ActionCodeSettingsBuilder } from './action-code-set import { Tenant, TenantServerResponse, CreateTenantRequest, UpdateTenantRequest } from './tenant'; import { isUidIdentifier, isEmailIdentifier, isPhoneIdentifier, isProviderIdentifier, - UserIdentifier, UidIdentifier, EmailIdentifier,PhoneIdentifier, ProviderIdentifier, + UserIdentifier, UidIdentifier, EmailIdentifier, PhoneIdentifier, ProviderIdentifier, } from './identifier'; import { SAMLConfig, OIDCConfig, OIDCConfigServerResponse, SAMLConfigServerResponse, @@ -89,7 +90,7 @@ const MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE = 100; /** The Firebase Auth backend base URL format. */ const FIREBASE_AUTH_BASE_URL_FORMAT = - 'https://identitytoolkit.googleapis.com/{version}/projects/{projectId}{api}'; + 'https://identitytoolkit.googleapis.com/{version}/projects/{projectId}{api}'; /** Firebase Auth base URlLformat when using the auth emultor. */ const FIREBASE_AUTH_EMULATOR_BASE_URL_FORMAT = @@ -171,10 +172,10 @@ class AuthResourceUrlBuilder { .then((projectId) => { if (!validator.isNonEmptyString(projectId)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CREDENTIAL, + authClientErrorCode.INVALID_CREDENTIAL, 'Failed to determine project ID for Auth. Initialize the ' - + 'SDK with service account credentials or set project ID as an app option. ' - + 'Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.', + + 'SDK with service account credentials or set project ID as an app option. ' + + 'Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.', ); } @@ -260,27 +261,27 @@ function validateAuthFactorInfo(request: AuthFactorInfo): void { } // No enrollment ID is available for signupNewUser. Use another identifier. const authFactorInfoIdentifier = - request.mfaEnrollmentId || request.phoneInfo || JSON.stringify(request); + request.mfaEnrollmentId || request.phoneInfo || JSON.stringify(request); // Enrollment uid may or may not be specified for update operations. if (typeof request.mfaEnrollmentId !== 'undefined' && - !validator.isNonEmptyString(request.mfaEnrollmentId)) { + !validator.isNonEmptyString(request.mfaEnrollmentId)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_UID, + authClientErrorCode.INVALID_UID, 'The second factor "uid" must be a valid non-empty string.', ); } if (typeof request.displayName !== 'undefined' && - !validator.isString(request.displayName)) { + !validator.isString(request.displayName)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_DISPLAY_NAME, + authClientErrorCode.INVALID_DISPLAY_NAME, `The second factor "displayName" for "${authFactorInfoIdentifier}" must be a valid string.`, ); } // enrolledAt must be a valid UTC date string. if (typeof request.enrolledAt !== 'undefined' && - !validator.isISODateString(request.enrolledAt)) { + !validator.isISODateString(request.enrolledAt)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ENROLLMENT_TIME, + authClientErrorCode.INVALID_ENROLLMENT_TIME, `The second factor "enrollmentTime" for "${authFactorInfoIdentifier}" must be a valid ` + 'UTC date string.'); } @@ -289,7 +290,7 @@ function validateAuthFactorInfo(request: AuthFactorInfo): void { // phoneNumber should be a string and a valid phone number. if (!validator.isPhoneNumber(request.phoneInfo)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_PHONE_NUMBER, + authClientErrorCode.INVALID_PHONE_NUMBER, `The second factor "phoneNumber" for "${authFactorInfoIdentifier}" must be a non-empty ` + 'E.164 standard compliant identifier string.'); } @@ -297,7 +298,7 @@ function validateAuthFactorInfo(request: AuthFactorInfo): void { // Invalid second factor. For example, a phone second factor may have been provided without // a phone number. A TOTP based second factor may require a secret key, etc. throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ENROLLED_FACTORS, + authClientErrorCode.INVALID_ENROLLED_FACTORS, 'MFAInfo object provided is invalid.'); } } @@ -325,12 +326,12 @@ function validateProviderUserInfo(request: any): void { } } if (!validator.isNonEmptyString(request.providerId)) { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID); + throw new FirebaseAuthError(authClientErrorCode.INVALID_PROVIDER_ID); } if (typeof request.displayName !== 'undefined' && - typeof request.displayName !== 'string') { + typeof request.displayName !== 'string') { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_DISPLAY_NAME, + authClientErrorCode.INVALID_DISPLAY_NAME, `The provider "displayName" for "${request.providerId}" must be a valid string.`, ); } @@ -338,24 +339,24 @@ function validateProviderUserInfo(request: any): void { // This is called localId on the backend but the developer specifies this as // uid externally. So the error message should use the client facing name. throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_UID, + authClientErrorCode.INVALID_UID, `The provider "uid" for "${request.providerId}" must be a valid non-empty string.`, ); } // email should be a string and a valid email. if (typeof request.email !== 'undefined' && !validator.isEmail(request.email)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_EMAIL, + authClientErrorCode.INVALID_EMAIL, `The provider "email" for "${request.providerId}" must be a valid email string.`, ); } // photoUrl should be a URL. if (typeof request.photoUrl !== 'undefined' && - !validator.isURL(request.photoUrl)) { + !validator.isURL(request.photoUrl)) { // This is called photoUrl on the backend but the developer specifies this as // photoURL externally. So the error message should use the client facing name. throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_PHOTO_URL, + authClientErrorCode.INVALID_PHOTO_URL, `The provider "photoURL" for "${request.providerId}" must be a valid URL string.`, ); } @@ -409,80 +410,80 @@ function validateCreateEditRequest(request: any, writeOperationType: WriteOperat } } if (typeof request.tenantId !== 'undefined' && - !validator.isNonEmptyString(request.tenantId)) { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID); + !validator.isNonEmptyString(request.tenantId)) { + throw new FirebaseAuthError(authClientErrorCode.INVALID_TENANT_ID); } // For any invalid parameter, use the external key name in the error description. // displayName should be a string. if (typeof request.displayName !== 'undefined' && - !validator.isString(request.displayName)) { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_DISPLAY_NAME); + !validator.isString(request.displayName)) { + throw new FirebaseAuthError(authClientErrorCode.INVALID_DISPLAY_NAME); } if ((typeof request.localId !== 'undefined' || uploadAccountRequest) && - !validator.isUid(request.localId)) { + !validator.isUid(request.localId)) { // This is called localId on the backend but the developer specifies this as // uid externally. So the error message should use the client facing name. - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_UID); + throw new FirebaseAuthError(authClientErrorCode.INVALID_UID); } // email should be a string and a valid email. if (typeof request.email !== 'undefined' && !validator.isEmail(request.email)) { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL); + throw new FirebaseAuthError(authClientErrorCode.INVALID_EMAIL); } // phoneNumber should be a string and a valid phone number. if (typeof request.phoneNumber !== 'undefined' && - !validator.isPhoneNumber(request.phoneNumber)) { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER); + !validator.isPhoneNumber(request.phoneNumber)) { + throw new FirebaseAuthError(authClientErrorCode.INVALID_PHONE_NUMBER); } // password should be a string and a minimum of 6 chars. if (typeof request.password !== 'undefined' && - !validator.isPassword(request.password)) { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD); + !validator.isPassword(request.password)) { + throw new FirebaseAuthError(authClientErrorCode.INVALID_PASSWORD); } // rawPassword should be a string and a minimum of 6 chars. if (typeof request.rawPassword !== 'undefined' && - !validator.isPassword(request.rawPassword)) { + !validator.isPassword(request.rawPassword)) { // This is called rawPassword on the backend but the developer specifies this as // password externally. So the error message should use the client facing name. - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD); + throw new FirebaseAuthError(authClientErrorCode.INVALID_PASSWORD); } // emailVerified should be a boolean. if (typeof request.emailVerified !== 'undefined' && - typeof request.emailVerified !== 'boolean') { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL_VERIFIED); + typeof request.emailVerified !== 'boolean') { + throw new FirebaseAuthError(authClientErrorCode.INVALID_EMAIL_VERIFIED); } // photoUrl should be a URL. if (typeof request.photoUrl !== 'undefined' && - !validator.isURL(request.photoUrl)) { + !validator.isURL(request.photoUrl)) { // This is called photoUrl on the backend but the developer specifies this as // photoURL externally. So the error message should use the client facing name. - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PHOTO_URL); + throw new FirebaseAuthError(authClientErrorCode.INVALID_PHOTO_URL); } // disabled should be a boolean. if (typeof request.disabled !== 'undefined' && - typeof request.disabled !== 'boolean') { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_DISABLED_FIELD); + typeof request.disabled !== 'boolean') { + throw new FirebaseAuthError(authClientErrorCode.INVALID_DISABLED_FIELD); } // validSince should be a number. if (typeof request.validSince !== 'undefined' && - !validator.isNumber(request.validSince)) { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_TOKENS_VALID_AFTER_TIME); + !validator.isNumber(request.validSince)) { + throw new FirebaseAuthError(authClientErrorCode.INVALID_TOKENS_VALID_AFTER_TIME); } // createdAt should be a number. if (typeof request.createdAt !== 'undefined' && - !validator.isNumber(request.createdAt)) { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_CREATION_TIME); + !validator.isNumber(request.createdAt)) { + throw new FirebaseAuthError(authClientErrorCode.INVALID_CREATION_TIME); } // lastSignInAt should be a number. if (typeof request.lastLoginAt !== 'undefined' && - !validator.isNumber(request.lastLoginAt)) { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_LAST_SIGN_IN_TIME); + !validator.isNumber(request.lastLoginAt)) { + throw new FirebaseAuthError(authClientErrorCode.INVALID_LAST_SIGN_IN_TIME); } // disableUser should be a boolean. if (typeof request.disableUser !== 'undefined' && - typeof request.disableUser !== 'boolean') { + typeof request.disableUser !== 'boolean') { // This is called disableUser on the backend but the developer specifies this as // disabled externally. So the error message should use the client facing name. - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_DISABLED_FIELD); + throw new FirebaseAuthError(authClientErrorCode.INVALID_DISABLED_FIELD); } // customAttributes should be stringified JSON with no blacklisted claims. // The payload should not exceed 1KB. @@ -494,7 +495,7 @@ function validateCreateEditRequest(request: any, writeOperationType: WriteOperat // JSON parsing error. This should never happen as we stringify the claims internally. // However, we still need to check since setAccountInfo via edit requests could pass // this field. - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_CLAIMS, error.message); + throw new FirebaseAuthError(authClientErrorCode.INVALID_CLAIMS, error.message); } const invalidClaims: string[] = []; // Check for any invalid claims. @@ -506,7 +507,7 @@ function validateCreateEditRequest(request: any, writeOperationType: WriteOperat // Throw an error if an invalid claim is detected. if (invalidClaims.length > 0) { throw new FirebaseAuthError( - AuthClientErrorCode.FORBIDDEN_CLAIM, + authClientErrorCode.FORBIDDEN_CLAIM, invalidClaims.length > 1 ? `Developer claims "${invalidClaims.join('", "')}" are reserved and cannot be specified.` : `Developer claim "${invalidClaims[0]}" is reserved and cannot be specified.`, @@ -515,25 +516,25 @@ function validateCreateEditRequest(request: any, writeOperationType: WriteOperat // Check claims payload does not exceed maxmimum size. if (request.customAttributes.length > MAX_CLAIMS_PAYLOAD_SIZE) { throw new FirebaseAuthError( - AuthClientErrorCode.CLAIMS_TOO_LARGE, + authClientErrorCode.CLAIMS_TOO_LARGE, `Developer claims payload should not exceed ${MAX_CLAIMS_PAYLOAD_SIZE} characters.`, ); } } // passwordHash has to be a base64 encoded string. if (typeof request.passwordHash !== 'undefined' && - !validator.isString(request.passwordHash)) { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD_HASH); + !validator.isString(request.passwordHash)) { + throw new FirebaseAuthError(authClientErrorCode.INVALID_PASSWORD_HASH); } // salt has to be a base64 encoded string. if (typeof request.salt !== 'undefined' && - !validator.isString(request.salt)) { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD_SALT); + !validator.isString(request.salt)) { + throw new FirebaseAuthError(authClientErrorCode.INVALID_PASSWORD_SALT); } // providerUserInfo has to be an array of valid UserInfo requests. if (typeof request.providerUserInfo !== 'undefined' && - !validator.isArray(request.providerUserInfo)) { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_DATA); + !validator.isArray(request.providerUserInfo)) { + throw new FirebaseAuthError(authClientErrorCode.INVALID_PROVIDER_DATA); } else if (validator.isArray(request.providerUserInfo)) { request.providerUserInfo.forEach((providerUserInfoEntry: any) => { validateProviderUserInfo(providerUserInfoEntry); @@ -556,7 +557,7 @@ function validateCreateEditRequest(request: any, writeOperationType: WriteOperat } if (enrollments) { if (!validator.isArray(enrollments)) { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_ENROLLED_FACTORS); + throw new FirebaseAuthError(authClientErrorCode.INVALID_ENROLLED_FACTORS); } enrollments.forEach((authFactorInfoEntry: AuthFactorInfo) => { validateAuthFactorInfo(authFactorInfoEntry); @@ -571,27 +572,30 @@ function validateCreateEditRequest(request: any, writeOperationType: WriteOperat * @internal */ export const FIREBASE_AUTH_CREATE_SESSION_COOKIE = - new ApiSettings(':createSessionCookie', 'POST') + new ApiSettings(':createSessionCookie', 'POST') // Set request validator. - .setRequestValidator((request: any) => { - // Validate the ID token is a non-empty string. - if (!validator.isNonEmptyString(request.idToken)) { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_ID_TOKEN); - } - // Validate the custom session cookie duration. - if (!validator.isNumber(request.validDuration) || - request.validDuration < MIN_SESSION_COOKIE_DURATION_SECS || - request.validDuration > MAX_SESSION_COOKIE_DURATION_SECS) { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_SESSION_COOKIE_DURATION); - } - }) + .setRequestValidator((request: any) => { + // Validate the ID token is a non-empty string. + if (!validator.isNonEmptyString(request.idToken)) { + throw new FirebaseAuthError(authClientErrorCode.INVALID_ID_TOKEN); + } + // Validate the custom session cookie duration. + if (!validator.isNumber(request.validDuration) || + request.validDuration < MIN_SESSION_COOKIE_DURATION_SECS || + request.validDuration > MAX_SESSION_COOKIE_DURATION_SECS) { + throw new FirebaseAuthError(authClientErrorCode.INVALID_SESSION_COOKIE_DURATION); + } + }) // Set response validator. - .setResponseValidator((response: any) => { - // Response should always contain the session cookie. - if (!validator.isNonEmptyString(response.sessionCookie)) { - throw new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR); - } - }); + .setResponseValidator((response: RequestResponse) => { + // Response should always contain the session cookie. + if (!validator.isNonEmptyString(response.data?.sessionCookie)) { + throw new FirebaseAuthError({ + ...authClientErrorCode.INTERNAL_ERROR, + httpResponse: toHttpResponse(response), + }); + } + }); /** @@ -612,15 +616,15 @@ export const FIREBASE_AUTH_DOWNLOAD_ACCOUNT = new ApiSettings('/accounts:batchGe .setRequestValidator((request: any) => { // Validate next page token. if (typeof request.nextPageToken !== 'undefined' && - !validator.isNonEmptyString(request.nextPageToken)) { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PAGE_TOKEN); + !validator.isNonEmptyString(request.nextPageToken)) { + throw new FirebaseAuthError(authClientErrorCode.INVALID_PAGE_TOKEN); } // Validate max results. if (!validator.isNumber(request.maxResults) || - request.maxResults <= 0 || - request.maxResults > MAX_DOWNLOAD_ACCOUNT_PAGE_SIZE) { + request.maxResults <= 0 || + request.maxResults > MAX_DOWNLOAD_ACCOUNT_PAGE_SIZE) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, 'Required "maxResults" must be a positive integer that does not exceed ' + `${MAX_DOWNLOAD_ACCOUNT_PAGE_SIZE}.`, ); @@ -647,14 +651,18 @@ export const FIREBASE_AUTH_GET_ACCOUNT_INFO = new ApiSettings('/accounts:lookup' .setRequestValidator((request: GetAccountInfoRequest) => { if (!request.localId && !request.email && !request.phoneNumber && !request.federatedUserId) { throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Server request is missing user identifier'); } }) // Set response validator. - .setResponseValidator((response: any) => { - if (!response.users || !response.users.length) { - throw new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + .setResponseValidator((response: RequestResponse) => { + const data = response.data; + if (!data.users || !data.users.length) { + throw new FirebaseAuthError({ + ...authClientErrorCode.USER_NOT_FOUND, + httpResponse: toHttpResponse(response), + }); } }); @@ -669,7 +677,7 @@ export const FIREBASE_AUTH_GET_ACCOUNTS_INFO = new ApiSettings('/accounts:lookup .setRequestValidator((request: GetAccountInfoRequest) => { if (!request.localId && !request.email && !request.phoneNumber && !request.federatedUserId) { throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Server request is missing user identifier'); } }); @@ -685,7 +693,7 @@ export const FIREBASE_AUTH_DELETE_ACCOUNT = new ApiSettings('/accounts:delete', .setRequestValidator((request: any) => { if (!request.localId) { throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Server request is missing user identifier'); } }); @@ -712,27 +720,32 @@ export const FIREBASE_AUTH_BATCH_DELETE_ACCOUNTS = new ApiSettings('/accounts:ba .setRequestValidator((request: BatchDeleteAccountsRequest) => { if (!request.localIds) { throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Server request is missing user identifiers'); } if (typeof request.force === 'undefined' || request.force !== true) { throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Server request is missing force=true field'); } }) - .setResponseValidator((response: BatchDeleteAccountsResponse) => { - const errors = response.errors || []; - errors.forEach((batchDeleteErrorInfo) => { + .setResponseValidator((response: RequestResponse) => { + const data: BatchDeleteAccountsResponse = response.data; + const errors: BatchDeleteErrorInfo[] = data.errors || []; + errors.forEach((batchDeleteErrorInfo: BatchDeleteErrorInfo) => { if (typeof batchDeleteErrorInfo.index === 'undefined') { - throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - 'INTERNAL ASSERT FAILED: Server BatchDeleteAccountResponse is missing an errors.index field'); + throw new FirebaseAuthError({ + ...authClientErrorCode.INTERNAL_ERROR, + message: 'INTERNAL ASSERT FAILED: Server BatchDeleteAccountResponse is missing an errors.index field', + httpResponse: toHttpResponse(response), + }); } if (!batchDeleteErrorInfo.localId) { - throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - 'INTERNAL ASSERT FAILED: Server BatchDeleteAccountResponse is missing an errors.localId field'); + throw new FirebaseAuthError({ + ...authClientErrorCode.INTERNAL_ERROR, + message: 'INTERNAL ASSERT FAILED: Server BatchDeleteAccountResponse is missing an errors.localId field', + httpResponse: toHttpResponse(response), + }); } // Allow the (error) message to be missing/undef. }); @@ -749,22 +762,26 @@ export const FIREBASE_AUTH_SET_ACCOUNT_INFO = new ApiSettings('/accounts:update' // localId is a required parameter. if (typeof request.localId === 'undefined') { throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Server request is missing user identifier'); } // Throw error when tenantId is passed in POST body. if (typeof request.tenantId !== 'undefined') { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '"tenantId" is an invalid "UpdateRequest" property.'); } validateCreateEditRequest(request, WriteOperationType.Update); }) // Set response validator. - .setResponseValidator((response: any) => { + .setResponseValidator((response: RequestResponse) => { + const data = response.data; // If the localId is not returned, then the request failed. - if (!response.localId) { - throw new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + if (!data?.localId) { + throw new FirebaseAuthError({ + ...authClientErrorCode.USER_NOT_FOUND, + httpResponse: toHttpResponse(response), + }); } }); @@ -780,32 +797,35 @@ export const FIREBASE_AUTH_SIGN_UP_NEW_USER = new ApiSettings('/accounts', 'POST // signupNewUser does not support customAttributes. if (typeof request.customAttributes !== 'undefined') { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '"customAttributes" cannot be set when creating a new user.', ); } // signupNewUser does not support validSince. if (typeof request.validSince !== 'undefined') { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '"validSince" cannot be set when creating a new user.', ); } // Throw error when tenantId is passed in POST body. if (typeof request.tenantId !== 'undefined') { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '"tenantId" is an invalid "CreateRequest" property.'); } validateCreateEditRequest(request, WriteOperationType.Create); }) // Set response validator. - .setResponseValidator((response: any) => { + .setResponseValidator((response: RequestResponse) => { + const data = response.data; // If the localId is not returned, then the request failed. - if (!response.localId) { - throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - 'INTERNAL ASSERT FAILED: Unable to create new user'); + if (!data?.localId) { + throw new FirebaseAuthError({ + ...authClientErrorCode.INTERNAL_ERROR, + message: 'INTERNAL ASSERT FAILED: Unable to create new user', + httpResponse: toHttpResponse(response), + }); } }); @@ -814,28 +834,31 @@ const FIREBASE_AUTH_GET_OOB_CODE = new ApiSettings('/accounts:sendOobCode', 'POS .setRequestValidator((request: any) => { if (!validator.isEmail(request.email)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_EMAIL, + authClientErrorCode.INVALID_EMAIL, ); } if (typeof request.newEmail !== 'undefined' && !validator.isEmail(request.newEmail)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_NEW_EMAIL, + authClientErrorCode.INVALID_NEW_EMAIL, ); } if (EMAIL_ACTION_REQUEST_TYPES.indexOf(request.requestType) === -1) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, `"${request.requestType}" is not a supported email action request type.`, ); } }) // Set response validator. - .setResponseValidator((response: any) => { + .setResponseValidator((response: RequestResponse) => { + const data = response.data; // If the oobLink is not returned, then the request failed. - if (!response.oobLink) { - throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - 'INTERNAL ASSERT FAILED: Unable to create the email action link'); + if (!data?.oobLink) { + throw new FirebaseAuthError({ + ...authClientErrorCode.INTERNAL_ERROR, + message: 'INTERNAL ASSERT FAILED: Unable to create the email action link', + httpResponse: toHttpResponse(response), + }); } }); @@ -846,13 +869,15 @@ const FIREBASE_AUTH_GET_OOB_CODE = new ApiSettings('/accounts:sendOobCode', 'POS */ const GET_OAUTH_IDP_CONFIG = new ApiSettings('/oauthIdpConfigs/{providerId}', 'GET') // Set response validator. - .setResponseValidator((response: any) => { + .setResponseValidator((response: RequestResponse) => { + const data = response.data; // Response should always contain the OIDC provider resource name. - if (!validator.isNonEmptyString(response.name)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - 'INTERNAL ASSERT FAILED: Unable to get OIDC configuration', - ); + if (!validator.isNonEmptyString(data?.name)) { + throw new FirebaseAuthError({ + ...authClientErrorCode.INTERNAL_ERROR, + message: 'INTERNAL ASSERT FAILED: Unable to get OIDC configuration', + httpResponse: toHttpResponse(response), + }); } }); @@ -870,13 +895,15 @@ const DELETE_OAUTH_IDP_CONFIG = new ApiSettings('/oauthIdpConfigs/{providerId}', */ const CREATE_OAUTH_IDP_CONFIG = new ApiSettings('/oauthIdpConfigs?oauthIdpConfigId={providerId}', 'POST') // Set response validator. - .setResponseValidator((response: any) => { + .setResponseValidator((response: RequestResponse) => { + const data = response.data; // Response should always contain the OIDC provider resource name. - if (!validator.isNonEmptyString(response.name)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - 'INTERNAL ASSERT FAILED: Unable to create new OIDC configuration', - ); + if (!validator.isNonEmptyString(data?.name)) { + throw new FirebaseAuthError({ + ...authClientErrorCode.INTERNAL_ERROR, + message: 'INTERNAL ASSERT FAILED: Unable to create new OIDC configuration', + httpResponse: toHttpResponse(response), + }); } }); @@ -887,13 +914,15 @@ const CREATE_OAUTH_IDP_CONFIG = new ApiSettings('/oauthIdpConfigs?oauthIdpConfig */ const UPDATE_OAUTH_IDP_CONFIG = new ApiSettings('/oauthIdpConfigs/{providerId}?updateMask={updateMask}', 'PATCH') // Set response validator. - .setResponseValidator((response: any) => { + .setResponseValidator((response: RequestResponse) => { + const data = response.data; // Response should always contain the configuration resource name. - if (!validator.isNonEmptyString(response.name)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - 'INTERNAL ASSERT FAILED: Unable to update OIDC configuration', - ); + if (!validator.isNonEmptyString(data?.name)) { + throw new FirebaseAuthError({ + ...authClientErrorCode.INTERNAL_ERROR, + message: 'INTERNAL ASSERT FAILED: Unable to update OIDC configuration', + httpResponse: toHttpResponse(response), + }); } }); @@ -907,15 +936,15 @@ const LIST_OAUTH_IDP_CONFIGS = new ApiSettings('/oauthIdpConfigs', 'GET') .setRequestValidator((request: any) => { // Validate next page token. if (typeof request.pageToken !== 'undefined' && - !validator.isNonEmptyString(request.pageToken)) { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PAGE_TOKEN); + !validator.isNonEmptyString(request.pageToken)) { + throw new FirebaseAuthError(authClientErrorCode.INVALID_PAGE_TOKEN); } // Validate max results. if (!validator.isNumber(request.pageSize) || - request.pageSize <= 0 || - request.pageSize > MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE) { + request.pageSize <= 0 || + request.pageSize > MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, 'Required "maxResults" must be a positive integer that does not exceed ' + `${MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE}.`, ); @@ -929,13 +958,15 @@ const LIST_OAUTH_IDP_CONFIGS = new ApiSettings('/oauthIdpConfigs', 'GET') */ const GET_INBOUND_SAML_CONFIG = new ApiSettings('/inboundSamlConfigs/{providerId}', 'GET') // Set response validator. - .setResponseValidator((response: any) => { + .setResponseValidator((response: RequestResponse) => { + const data = response.data; // Response should always contain the SAML provider resource name. - if (!validator.isNonEmptyString(response.name)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - 'INTERNAL ASSERT FAILED: Unable to get SAML configuration', - ); + if (!validator.isNonEmptyString(data?.name)) { + throw new FirebaseAuthError({ + ...authClientErrorCode.INTERNAL_ERROR, + message: 'INTERNAL ASSERT FAILED: Unable to get SAML configuration', + httpResponse: toHttpResponse(response), + }); } }); @@ -953,13 +984,15 @@ const DELETE_INBOUND_SAML_CONFIG = new ApiSettings('/inboundSamlConfigs/{provide */ const CREATE_INBOUND_SAML_CONFIG = new ApiSettings('/inboundSamlConfigs?inboundSamlConfigId={providerId}', 'POST') // Set response validator. - .setResponseValidator((response: any) => { + .setResponseValidator((response: RequestResponse) => { + const data = response.data; // Response should always contain the SAML provider resource name. - if (!validator.isNonEmptyString(response.name)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - 'INTERNAL ASSERT FAILED: Unable to create new SAML configuration', - ); + if (!validator.isNonEmptyString(data?.name)) { + throw new FirebaseAuthError({ + ...authClientErrorCode.INTERNAL_ERROR, + message: 'INTERNAL ASSERT FAILED: Unable to create new SAML configuration', + httpResponse: toHttpResponse(response), + }); } }); @@ -970,13 +1003,15 @@ const CREATE_INBOUND_SAML_CONFIG = new ApiSettings('/inboundSamlConfigs?inboundS */ const UPDATE_INBOUND_SAML_CONFIG = new ApiSettings('/inboundSamlConfigs/{providerId}?updateMask={updateMask}', 'PATCH') // Set response validator. - .setResponseValidator((response: any) => { + .setResponseValidator((response: RequestResponse) => { + const data = response.data; // Response should always contain the configuration resource name. - if (!validator.isNonEmptyString(response.name)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - 'INTERNAL ASSERT FAILED: Unable to update SAML configuration', - ); + if (!validator.isNonEmptyString(data?.name)) { + throw new FirebaseAuthError({ + ...authClientErrorCode.INTERNAL_ERROR, + message: 'INTERNAL ASSERT FAILED: Unable to update SAML configuration', + httpResponse: toHttpResponse(response), + }); } }); @@ -990,15 +1025,15 @@ const LIST_INBOUND_SAML_CONFIGS = new ApiSettings('/inboundSamlConfigs', 'GET') .setRequestValidator((request: any) => { // Validate next page token. if (typeof request.pageToken !== 'undefined' && - !validator.isNonEmptyString(request.pageToken)) { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PAGE_TOKEN); + !validator.isNonEmptyString(request.pageToken)) { + throw new FirebaseAuthError(authClientErrorCode.INVALID_PAGE_TOKEN); } // Validate max results. if (!validator.isNumber(request.pageSize) || - request.pageSize <= 0 || - request.pageSize > MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE) { + request.pageSize <= 0 || + request.pageSize > MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, 'Required "maxResults" must be a positive integer that does not exceed ' + `${MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE}.`, ); @@ -1026,7 +1061,7 @@ export abstract class AbstractAuthRequestHandler { private static addUidToRequest(id: UidIdentifier, request: GetAccountInfoRequest): GetAccountInfoRequest { if (!validator.isUid(id.uid)) { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_UID); + throw new FirebaseAuthError(authClientErrorCode.INVALID_UID); } request.localId ? request.localId.push(id.uid) : request.localId = [id.uid]; return request; @@ -1034,7 +1069,7 @@ export abstract class AbstractAuthRequestHandler { private static addEmailToRequest(id: EmailIdentifier, request: GetAccountInfoRequest): GetAccountInfoRequest { if (!validator.isEmail(id.email)) { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL); + throw new FirebaseAuthError(authClientErrorCode.INVALID_EMAIL); } request.email ? request.email.push(id.email) : request.email = [id.email]; return request; @@ -1042,7 +1077,7 @@ export abstract class AbstractAuthRequestHandler { private static addPhoneToRequest(id: PhoneIdentifier, request: GetAccountInfoRequest): GetAccountInfoRequest { if (!validator.isPhoneNumber(id.phoneNumber)) { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER); + throw new FirebaseAuthError(authClientErrorCode.INVALID_PHONE_NUMBER); } request.phoneNumber ? request.phoneNumber.push(id.phoneNumber) : request.phoneNumber = [id.phoneNumber]; return request; @@ -1050,10 +1085,10 @@ export abstract class AbstractAuthRequestHandler { private static addProviderToRequest(id: ProviderIdentifier, request: GetAccountInfoRequest): GetAccountInfoRequest { if (!validator.isNonEmptyString(id.providerId)) { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID); + throw new FirebaseAuthError(authClientErrorCode.INVALID_PROVIDER_ID); } if (!validator.isNonEmptyString(id.providerUid)) { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_UID); + throw new FirebaseAuthError(authClientErrorCode.INVALID_PROVIDER_UID); } const federatedUserId = { providerId: id.providerId, @@ -1072,7 +1107,7 @@ export abstract class AbstractAuthRequestHandler { constructor(protected readonly app: App) { if (typeof app !== 'object' || app === null || !('options' in app)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, 'First argument passed to admin.auth() must be a valid Firebase app instance.', ); } @@ -1108,7 +1143,7 @@ export abstract class AbstractAuthRequestHandler { */ public getAccountInfoByUid(uid: string): Promise { if (!validator.isUid(uid)) { - return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_UID)); + return Promise.reject(new FirebaseAuthError(authClientErrorCode.INVALID_UID)); } const request = { @@ -1125,7 +1160,7 @@ export abstract class AbstractAuthRequestHandler { */ public getAccountInfoByEmail(email: string): Promise { if (!validator.isEmail(email)) { - return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL)); + return Promise.reject(new FirebaseAuthError(authClientErrorCode.INVALID_EMAIL)); } const request = { @@ -1142,7 +1177,7 @@ export abstract class AbstractAuthRequestHandler { */ public getAccountInfoByPhoneNumber(phoneNumber: string): Promise { if (!validator.isPhoneNumber(phoneNumber)) { - return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER)); + return Promise.reject(new FirebaseAuthError(authClientErrorCode.INVALID_PHONE_NUMBER)); } const request = { @@ -1153,7 +1188,7 @@ export abstract class AbstractAuthRequestHandler { public getAccountInfoByFederatedUid(providerId: string, rawId: string): Promise { if (!validator.isNonEmptyString(providerId) || !validator.isNonEmptyString(rawId)) { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID); + throw new FirebaseAuthError(authClientErrorCode.INVALID_PROVIDER_ID); } const request = { @@ -1179,7 +1214,7 @@ export abstract class AbstractAuthRequestHandler { return Promise.resolve({ users: [] }); } else if (identifiers.length > MAX_GET_ACCOUNTS_BATCH_SIZE) { throw new FirebaseAuthError( - AuthClientErrorCode.MAXIMUM_USER_COUNT_EXCEEDED, + authClientErrorCode.MAXIMUM_USER_COUNT_EXCEEDED, '`identifiers` parameter must have <= ' + MAX_GET_ACCOUNTS_BATCH_SIZE + ' entries.'); } @@ -1196,7 +1231,7 @@ export abstract class AbstractAuthRequestHandler { request = AbstractAuthRequestHandler.addProviderToRequest(id, request); } else { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, 'Unrecognized identifier: ' + id); } } @@ -1219,7 +1254,7 @@ export abstract class AbstractAuthRequestHandler { */ public downloadAccount( maxResults: number = MAX_DOWNLOAD_ACCOUNT_PAGE_SIZE, - pageToken?: string): Promise<{users: object[]; nextPageToken?: string}> { + pageToken?: string): Promise<{ users: object[]; nextPageToken?: string; }> { // Construct request. const request = { maxResults, @@ -1235,7 +1270,7 @@ export abstract class AbstractAuthRequestHandler { if (!response.users) { response.users = []; } - return response as {users: object[]; nextPageToken?: string}; + return response as { users: object[]; nextPageToken?: string; }; }); } @@ -1266,7 +1301,7 @@ export abstract class AbstractAuthRequestHandler { // Fail quickly if more users than allowed are to be imported. if (validator.isArray(users) && users.length > MAX_UPLOAD_ACCOUNT_BATCH_SIZE) { throw new FirebaseAuthError( - AuthClientErrorCode.MAXIMUM_USER_COUNT_EXCEEDED, + authClientErrorCode.MAXIMUM_USER_COUNT_EXCEEDED, `A maximum of ${MAX_UPLOAD_ACCOUNT_BATCH_SIZE} users can be imported at once.`, ); } @@ -1278,7 +1313,7 @@ export abstract class AbstractAuthRequestHandler { return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_UPLOAD_ACCOUNT, request) .then((response: any) => { // No error object is returned if no error encountered. - const failedUploads = (response.error || []) as Array<{index: number; message: string}>; + const failedUploads = (response.error || []) as Array<{ index: number; message: string; }>; // Rewrite response as UserImportResult and re-insert client previously detected errors. return userImportBuilder.buildResponse(failedUploads); }); @@ -1292,7 +1327,7 @@ export abstract class AbstractAuthRequestHandler { */ public deleteAccount(uid: string): Promise { if (!validator.isUid(uid)) { - return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_UID)); + return Promise.reject(new FirebaseAuthError(authClientErrorCode.INVALID_UID)); } const request = { @@ -1306,7 +1341,7 @@ export abstract class AbstractAuthRequestHandler { return Promise.resolve({}); } else if (uids.length > MAX_DELETE_ACCOUNTS_BATCH_SIZE) { throw new FirebaseAuthError( - AuthClientErrorCode.MAXIMUM_USER_COUNT_EXCEEDED, + authClientErrorCode.MAXIMUM_USER_COUNT_EXCEEDED, '`uids` parameter must have <= ' + MAX_DELETE_ACCOUNTS_BATCH_SIZE + ' entries.'); } @@ -1317,7 +1352,7 @@ export abstract class AbstractAuthRequestHandler { uids.forEach((uid) => { if (!validator.isUid(uid)) { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_UID); + throw new FirebaseAuthError(authClientErrorCode.INVALID_UID); } request.localIds!.push(uid); }); @@ -1336,11 +1371,11 @@ export abstract class AbstractAuthRequestHandler { public setCustomUserClaims(uid: string, customUserClaims: object | null): Promise { // Validate user UID. if (!validator.isUid(uid)) { - return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_UID)); + return Promise.reject(new FirebaseAuthError(authClientErrorCode.INVALID_UID)); } else if (!validator.isObject(customUserClaims)) { return Promise.reject( new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, 'CustomUserClaims argument must be an object or null.', ), ); @@ -1370,11 +1405,11 @@ export abstract class AbstractAuthRequestHandler { */ public updateExistingAccount(uid: string, properties: UpdateRequest): Promise { if (!validator.isUid(uid)) { - return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_UID)); + return Promise.reject(new FirebaseAuthError(authClientErrorCode.INVALID_UID)); } else if (!validator.isNonNullObject(properties)) { return Promise.reject( new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, 'Properties argument must be a non-null object.', ), ); @@ -1383,25 +1418,25 @@ export abstract class AbstractAuthRequestHandler { // validateProviderUserInfo. It may be possible to refactor a bit. if (!validator.isNonEmptyString(properties.providerToLink.providerId)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, 'providerToLink.providerId of properties argument must be a non-empty string.'); } if (!validator.isNonEmptyString(properties.providerToLink.uid)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, 'providerToLink.uid of properties argument must be a non-empty string.'); } } else if (typeof properties.providersToUnlink !== 'undefined') { if (!validator.isArray(properties.providersToUnlink)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, 'providersToUnlink of properties argument must be an array of strings.'); } properties.providersToUnlink.forEach((providerId) => { if (!validator.isNonEmptyString(providerId)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, 'providersToUnlink of properties argument must be an array of strings.'); } }); @@ -1417,7 +1452,7 @@ export abstract class AbstractAuthRequestHandler { // Parameters that are deletable and their deleteAttribute names. // Use client facing names, photoURL instead of photoUrl. - const deletableParams: {[key: string]: string} = { + const deletableParams: { [key: string]: string; } = { displayName: 'DISPLAY_NAME', photoURL: 'PHOTO_URL', }; @@ -1444,7 +1479,7 @@ export abstract class AbstractAuthRequestHandler { delete request.phoneNumber; } - if (typeof(request.providerToLink) !== 'undefined') { + if (typeof (request.providerToLink) !== 'undefined') { request.linkProviderUserInfo = deepCopy(request.providerToLink); delete request.providerToLink; @@ -1452,7 +1487,7 @@ export abstract class AbstractAuthRequestHandler { delete request.linkProviderUserInfo.uid; } - if (typeof(request.providersToUnlink) !== 'undefined') { + if (typeof (request.providersToUnlink) !== 'undefined') { if (!validator.isArray(request.deleteProvider)) { request.deleteProvider = []; } @@ -1514,7 +1549,7 @@ export abstract class AbstractAuthRequestHandler { public revokeRefreshTokens(uid: string): Promise { // Validate user UID. if (!validator.isUid(uid)) { - return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_UID)); + return Promise.reject(new FirebaseAuthError(authClientErrorCode.INVALID_UID)); } const request: any = { localId: uid, @@ -1538,7 +1573,7 @@ export abstract class AbstractAuthRequestHandler { if (!validator.isNonNullObject(properties)) { return Promise.reject( new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, 'Properties argument must be a non-null object.', ), ); @@ -1571,11 +1606,11 @@ export abstract class AbstractAuthRequestHandler { // They will automatically be provisioned server side. if ('enrollmentTime' in multiFactorInfo) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '"enrollmentTime" is not supported when adding second factors via "createUser()"'); } else if ('uid' in multiFactorInfo) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '"uid" is not supported when adding second factors via "createUser()"'); } mfaInfo.push(convertMultiFactorInfoToServerFormat(multiFactorInfo)); @@ -1611,9 +1646,9 @@ export abstract class AbstractAuthRequestHandler { public getEmailActionLink( requestType: string, email: string, actionCodeSettings?: ActionCodeSettings, newEmail?: string): Promise { - let request = { - requestType, - email, + let request = { + requestType, + email, returnOobLink: true, ...(typeof newEmail !== 'undefined') && { newEmail }, }; @@ -1622,7 +1657,7 @@ export abstract class AbstractAuthRequestHandler { if (typeof actionCodeSettings === 'undefined' && requestType === 'EMAIL_SIGNIN') { return Promise.reject( new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, "`actionCodeSettings` is required when `requestType` === 'EMAIL_SIGNIN'", ), ); @@ -1638,7 +1673,7 @@ export abstract class AbstractAuthRequestHandler { if (requestType === 'VERIFY_AND_CHANGE_EMAIL' && typeof newEmail === 'undefined') { return Promise.reject( new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, "`newEmail` is required when `requestType` === 'VERIFY_AND_CHANGE_EMAIL'", ), ); @@ -1658,7 +1693,7 @@ export abstract class AbstractAuthRequestHandler { */ public getOAuthIdpConfig(providerId: string): Promise { if (!OIDCConfig.isProviderId(providerId)) { - return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)); + return Promise.reject(new FirebaseAuthError(authClientErrorCode.INVALID_PROVIDER_ID)); } return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), GET_OAUTH_IDP_CONFIG, {}, { providerId }); } @@ -1679,7 +1714,7 @@ export abstract class AbstractAuthRequestHandler { public listOAuthIdpConfigs( maxResults: number = MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE, pageToken?: string): Promise { - const request: {pageSize: number; pageToken?: string} = { + const request: { pageSize: number; pageToken?: string; } = { pageSize: maxResults, }; // Add next page token if provided. @@ -1692,7 +1727,7 @@ export abstract class AbstractAuthRequestHandler { response.oauthIdpConfigs = []; delete response.nextPageToken; } - return response as {oauthIdpConfigs: object[]; nextPageToken?: string}; + return response as { oauthIdpConfigs: object[]; nextPageToken?: string; }; }); } @@ -1704,7 +1739,7 @@ export abstract class AbstractAuthRequestHandler { */ public deleteOAuthIdpConfig(providerId: string): Promise { if (!OIDCConfig.isProviderId(providerId)) { - return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)); + return Promise.reject(new FirebaseAuthError(authClientErrorCode.INVALID_PROVIDER_ID)); } return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), DELETE_OAUTH_IDP_CONFIG, {}, { providerId }) .then(() => { @@ -1733,7 +1768,7 @@ export abstract class AbstractAuthRequestHandler { .then((response: any) => { if (!OIDCConfig.getProviderIdFromResourceName(response.name)) { throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Unable to create new OIDC provider configuration'); } return response as OIDCConfigServerResponse; @@ -1751,7 +1786,7 @@ export abstract class AbstractAuthRequestHandler { public updateOAuthIdpConfig( providerId: string, options: OIDCUpdateAuthProviderRequest): Promise { if (!OIDCConfig.isProviderId(providerId)) { - return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)); + return Promise.reject(new FirebaseAuthError(authClientErrorCode.INVALID_PROVIDER_ID)); } // Construct backend request. let request: OIDCConfigServerRequest; @@ -1766,7 +1801,7 @@ export abstract class AbstractAuthRequestHandler { .then((response: any) => { if (!OIDCConfig.getProviderIdFromResourceName(response.name)) { throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Unable to update OIDC provider configuration'); } return response as OIDCConfigServerResponse; @@ -1781,7 +1816,7 @@ export abstract class AbstractAuthRequestHandler { */ public getInboundSamlConfig(providerId: string): Promise { if (!SAMLConfig.isProviderId(providerId)) { - return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)); + return Promise.reject(new FirebaseAuthError(authClientErrorCode.INVALID_PROVIDER_ID)); } return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), GET_INBOUND_SAML_CONFIG, {}, { providerId }); } @@ -1802,7 +1837,7 @@ export abstract class AbstractAuthRequestHandler { public listInboundSamlConfigs( maxResults: number = MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE, pageToken?: string): Promise { - const request: {pageSize: number; pageToken?: string} = { + const request: { pageSize: number; pageToken?: string; } = { pageSize: maxResults, }; // Add next page token if provided. @@ -1815,7 +1850,7 @@ export abstract class AbstractAuthRequestHandler { response.inboundSamlConfigs = []; delete response.nextPageToken; } - return response as {inboundSamlConfigs: object[]; nextPageToken?: string}; + return response as { inboundSamlConfigs: object[]; nextPageToken?: string; }; }); } @@ -1827,7 +1862,7 @@ export abstract class AbstractAuthRequestHandler { */ public deleteInboundSamlConfig(providerId: string): Promise { if (!SAMLConfig.isProviderId(providerId)) { - return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)); + return Promise.reject(new FirebaseAuthError(authClientErrorCode.INVALID_PROVIDER_ID)); } return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), DELETE_INBOUND_SAML_CONFIG, {}, { providerId }) .then(() => { @@ -1856,7 +1891,7 @@ export abstract class AbstractAuthRequestHandler { .then((response: any) => { if (!SAMLConfig.getProviderIdFromResourceName(response.name)) { throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Unable to create new SAML provider configuration'); } return response as SAMLConfigServerResponse; @@ -1874,7 +1909,7 @@ export abstract class AbstractAuthRequestHandler { public updateInboundSamlConfig( providerId: string, options: SAMLUpdateAuthProviderRequest): Promise { if (!SAMLConfig.isProviderId(providerId)) { - return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)); + return Promise.reject(new FirebaseAuthError(authClientErrorCode.INVALID_PROVIDER_ID)); } // Construct backend request. let request: SAMLConfigServerRequest; @@ -1889,7 +1924,7 @@ export abstract class AbstractAuthRequestHandler { .then((response: any) => { if (!SAMLConfig.getProviderIdFromResourceName(response.name)) { throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Unable to update SAML provider configuration'); } return response as SAMLConfigServerResponse; @@ -1925,26 +1960,28 @@ export abstract class AbstractAuthRequestHandler { }; return this.httpClient.send(req); }) + .then((response) => { - // Validate response. const responseValidator = apiSettings.getResponseValidator(); - responseValidator(response.data); - // Return entire response. + if (responseValidator) { + responseValidator(response); + } + // Return entire response data. return response.data; }) .catch((err) => { if (err instanceof RequestResponseError) { - const error = err.response.data; - const errorCode = AbstractAuthRequestHandler.getErrorCode(error); + const errorCode = AbstractAuthRequestHandler.getErrorCode(err.response.data); if (!errorCode) { - throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - 'Error returned from server: ' + error + '. Additionally, an ' + - 'internal error occurred while attempting to extract the ' + - 'errorcode from the error.', - ); + // Fallback for unexpected server error responses without a parseable error code. + throw new FirebaseAuthError({ + ...authClientErrorCode.INTERNAL_ERROR, + message: 'An internal error occurred while attempting to extract the errorcode from the error.', + cause: err, + httpResponse: toHttpResponse(err.response), + }); } - throw FirebaseAuthError.fromServerError(errorCode, /* message */ undefined, error); + throw FirebaseAuthError.fromServerError(errorCode, /* message */ undefined, err); } throw err; }); @@ -1983,38 +2020,44 @@ export abstract class AbstractAuthRequestHandler { /** Instantiates the getConfig endpoint settings. */ const GET_PROJECT_CONFIG = new ApiSettings('/config', 'GET') - .setResponseValidator((response: any) => { + .setResponseValidator((response: RequestResponse) => { + const data = response.data; // Response should always contain at least the config name. - if (!validator.isNonEmptyString(response.name)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - 'INTERNAL ASSERT FAILED: Unable to get project config', - ); + if (!validator.isNonEmptyString(data?.name)) { + throw new FirebaseAuthError({ + ...authClientErrorCode.INTERNAL_ERROR, + message: 'INTERNAL ASSERT FAILED: Unable to get project config', + httpResponse: toHttpResponse(response), + }); } }); /** Instantiates the updateConfig endpoint settings. */ const UPDATE_PROJECT_CONFIG = new ApiSettings('/config?updateMask={updateMask}', 'PATCH') - .setResponseValidator((response: any) => { + .setResponseValidator((response: RequestResponse) => { + const data = response.data; // Response should always contain at least the config name. - if (!validator.isNonEmptyString(response.name)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - 'INTERNAL ASSERT FAILED: Unable to update project config', - ); + if (!validator.isNonEmptyString(data?.name)) { + throw new FirebaseAuthError({ + ...authClientErrorCode.INTERNAL_ERROR, + message: 'INTERNAL ASSERT FAILED: Unable to update project config', + httpResponse: toHttpResponse(response), + }); } }); /** Instantiates the getTenant endpoint settings. */ const GET_TENANT = new ApiSettings('/tenants/{tenantId}', 'GET') -// Set response validator. - .setResponseValidator((response: any) => { + // Set response validator. + .setResponseValidator((response: RequestResponse) => { + const data = response.data; // Response should always contain at least the tenant name. - if (!validator.isNonEmptyString(response.name)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - 'INTERNAL ASSERT FAILED: Unable to get tenant', - ); + if (!validator.isNonEmptyString(data?.name)) { + throw new FirebaseAuthError({ + ...authClientErrorCode.INTERNAL_ERROR, + message: 'INTERNAL ASSERT FAILED: Unable to get tenant', + httpResponse: toHttpResponse(response), + }); } }); @@ -2024,14 +2067,16 @@ const DELETE_TENANT = new ApiSettings('/tenants/{tenantId}', 'DELETE'); /** Instantiates the updateTenant endpoint settings. */ const UPDATE_TENANT = new ApiSettings('/tenants/{tenantId}?updateMask={updateMask}', 'PATCH') // Set response validator. - .setResponseValidator((response: any) => { + .setResponseValidator((response: RequestResponse) => { + const data = response.data; // Response should always contain at least the tenant name. - if (!validator.isNonEmptyString(response.name) || - !Tenant.getTenantIdFromResourceName(response.name)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - 'INTERNAL ASSERT FAILED: Unable to update tenant', - ); + if (!validator.isNonEmptyString(data?.name) || + !Tenant.getTenantIdFromResourceName(data?.name)) { + throw new FirebaseAuthError({ + ...authClientErrorCode.INTERNAL_ERROR, + message: 'INTERNAL ASSERT FAILED: Unable to update tenant', + httpResponse: toHttpResponse(response), + }); } }); @@ -2041,15 +2086,15 @@ const LIST_TENANTS = new ApiSettings('/tenants', 'GET') .setRequestValidator((request: any) => { // Validate next page token. if (typeof request.pageToken !== 'undefined' && - !validator.isNonEmptyString(request.pageToken)) { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PAGE_TOKEN); + !validator.isNonEmptyString(request.pageToken)) { + throw new FirebaseAuthError(authClientErrorCode.INVALID_PAGE_TOKEN); } // Validate max results. if (!validator.isNumber(request.pageSize) || - request.pageSize <= 0 || - request.pageSize > MAX_LIST_TENANT_PAGE_SIZE) { + request.pageSize <= 0 || + request.pageSize > MAX_LIST_TENANT_PAGE_SIZE) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, 'Required "maxResults" must be a positive non-zero number that does not exceed ' + `the allowed ${MAX_LIST_TENANT_PAGE_SIZE}.`, ); @@ -2058,15 +2103,17 @@ const LIST_TENANTS = new ApiSettings('/tenants', 'GET') /** Instantiates the createTenant endpoint settings. */ const CREATE_TENANT = new ApiSettings('/tenants', 'POST') -// Set response validator. - .setResponseValidator((response: any) => { + // Set response validator. + .setResponseValidator((response: RequestResponse) => { + const data = response.data; // Response should always contain at least the tenant name. - if (!validator.isNonEmptyString(response.name) || - !Tenant.getTenantIdFromResourceName(response.name)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - 'INTERNAL ASSERT FAILED: Unable to create new tenant', - ); + if (!validator.isNonEmptyString(data?.name) || + !Tenant.getTenantIdFromResourceName(data?.name)) { + throw new FirebaseAuthError({ + ...authClientErrorCode.INTERNAL_ERROR, + message: 'INTERNAL ASSERT FAILED: Unable to create tenant', + httpResponse: toHttpResponse(response), + }); } }); @@ -2088,7 +2135,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { */ constructor(app: App) { super(app); - this.authResourceUrlBuilder = new AuthResourceUrlBuilder(app, 'v2'); + this.authResourceUrlBuilder = new AuthResourceUrlBuilder(app, 'v2'); } /** @@ -2142,7 +2189,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { */ public getTenant(tenantId: string): Promise { if (!validator.isNonEmptyString(tenantId)) { - return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID)); + return Promise.reject(new FirebaseAuthError(authClientErrorCode.INVALID_TENANT_ID)); } return this.invokeRequestHandler(this.authResourceUrlBuilder, GET_TENANT, {}, { tenantId }) .then((response: any) => { @@ -2165,7 +2212,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { */ public listTenants( maxResults: number = MAX_LIST_TENANT_PAGE_SIZE, - pageToken?: string): Promise<{tenants: TenantServerResponse[]; nextPageToken?: string}> { + pageToken?: string): Promise<{ tenants: TenantServerResponse[]; nextPageToken?: string; }> { const request = { pageSize: maxResults, pageToken, @@ -2180,7 +2227,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { response.tenants = []; delete response.nextPageToken; } - return response as {tenants: TenantServerResponse[]; nextPageToken?: string}; + return response as { tenants: TenantServerResponse[]; nextPageToken?: string; }; }); } @@ -2192,7 +2239,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { */ public deleteTenant(tenantId: string): Promise { if (!validator.isNonEmptyString(tenantId)) { - return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID)); + return Promise.reject(new FirebaseAuthError(authClientErrorCode.INVALID_TENANT_ID)); } return this.invokeRequestHandler(this.authResourceUrlBuilder, DELETE_TENANT, undefined, { tenantId }) .then(() => { @@ -2228,7 +2275,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { */ public updateTenant(tenantId: string, tenantOptions: UpdateTenantRequest): Promise { if (!validator.isNonEmptyString(tenantId)) { - return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID)); + return Promise.reject(new FirebaseAuthError(authClientErrorCode.INVALID_TENANT_ID)); } try { // Construct backend request. @@ -2300,9 +2347,9 @@ export class TenantAwareAuthRequestHandler extends AbstractAuthRequestHandler { // Add additional check to match tenant ID of imported user records. users.forEach((user: UserImportRecord, index: number) => { if (validator.isNonEmptyString(user.tenantId) && - user.tenantId !== this.tenantId) { + user.tenantId !== this.tenantId) { throw new FirebaseAuthError( - AuthClientErrorCode.MISMATCHING_TENANT_ID, + authClientErrorCode.MISMATCHING_TENANT_ID, `UserRecord of index "${index}" has mismatching tenant ID "${user.tenantId}"`); } }); @@ -2311,7 +2358,7 @@ export class TenantAwareAuthRequestHandler extends AbstractAuthRequestHandler { } function emulatorHost(): string | undefined { - return process.env.FIREBASE_AUTH_EMULATOR_HOST + return process.env.FIREBASE_AUTH_EMULATOR_HOST; } /** diff --git a/src/auth/auth-config.ts b/src/auth/auth-config.ts index 1dd5565a88..3310a8441e 100644 --- a/src/auth/auth-config.ts +++ b/src/auth/auth-config.ts @@ -16,7 +16,7 @@ import * as validator from '../utils/validator'; import { deepCopy } from '../utils/deep-copy'; -import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; +import { authClientErrorCode, FirebaseAuthError } from './error'; /** * Interface representing base properties of a user-enrolled second factor for a @@ -605,7 +605,7 @@ export class MultiFactorAuthConfig implements MultiFactorConfig { }; if (!validator.isNonNullObject(options)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"MultiFactorConfig" must be a non-null object.', ); } @@ -613,7 +613,7 @@ export class MultiFactorAuthConfig implements MultiFactorConfig { for (const key in options) { if (!(key in validKeys)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, `"${key}" is not a valid MultiFactorConfig parameter.`, ); } @@ -623,7 +623,7 @@ export class MultiFactorAuthConfig implements MultiFactorConfig { options.state !== 'ENABLED' && options.state !== 'DISABLED') { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"MultiFactorConfig.state" must be either "ENABLED" or "DISABLED".', ); } @@ -631,7 +631,7 @@ export class MultiFactorAuthConfig implements MultiFactorConfig { if (typeof options.factorIds !== 'undefined') { if (!validator.isArray(options.factorIds)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"MultiFactorConfig.factorIds" must be an array of valid "AuthFactorTypes".', ); } @@ -640,7 +640,7 @@ export class MultiFactorAuthConfig implements MultiFactorConfig { options.factorIds.forEach((factorId) => { if (typeof AUTH_FACTOR_CLIENT_TO_SERVER_TYPE[factorId] === 'undefined') { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, `"${factorId}" is not a valid "AuthFactorType".`, ); } @@ -650,7 +650,7 @@ export class MultiFactorAuthConfig implements MultiFactorConfig { if (typeof options.providerConfigs !== 'undefined') { if (!validator.isArray(options.providerConfigs)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"MultiFactorConfig.providerConfigs" must be an array of valid "MultiFactorProviderConfig."', ); } @@ -658,7 +658,7 @@ export class MultiFactorAuthConfig implements MultiFactorConfig { options.providerConfigs.forEach((multiFactorProviderConfig) => { if (typeof multiFactorProviderConfig === 'undefined' || !validator.isObject(multiFactorProviderConfig)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, `"${multiFactorProviderConfig}" is not a valid "MultiFactorProviderConfig" type.` ) } @@ -669,7 +669,7 @@ export class MultiFactorAuthConfig implements MultiFactorConfig { for (const key in multiFactorProviderConfig) { if (!(key in validProviderConfigKeys)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, `"${key}" is not a valid ProviderConfig parameter.`, ); } @@ -678,14 +678,14 @@ export class MultiFactorAuthConfig implements MultiFactorConfig { (multiFactorProviderConfig.state !== 'ENABLED' && multiFactorProviderConfig.state !== 'DISABLED')) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"MultiFactorConfig.providerConfigs.state" must be either "ENABLED" or "DISABLED".', ) } // Since TOTP is the only provider config available right now, not defining it will lead into an error if (typeof multiFactorProviderConfig.totpProviderConfig === 'undefined') { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"MultiFactorConfig.providerConfigs.totpProviderConfig" must be defined.' ) } @@ -695,7 +695,7 @@ export class MultiFactorAuthConfig implements MultiFactorConfig { for (const key in multiFactorProviderConfig.totpProviderConfig) { if (!(key in validTotpProviderConfigKeys)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, `"${key}" is not a valid TotpProviderConfig parameter.`, ); } @@ -704,7 +704,7 @@ export class MultiFactorAuthConfig implements MultiFactorConfig { if (typeof adjIntervals !== 'undefined' && (!Number.isInteger(adjIntervals) || adjIntervals < 0 || adjIntervals > 10)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '"MultiFactorConfig.providerConfigs.totpProviderConfig.adjacentIntervals" must' + ' be a valid number between 0 and 10 (both inclusive).' ) @@ -724,7 +724,7 @@ export class MultiFactorAuthConfig implements MultiFactorConfig { constructor(response: MultiFactorAuthServerConfig) { if (typeof response.state === 'undefined') { throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Invalid multi-factor configuration response'); } this.state = response.state; @@ -744,7 +744,7 @@ export class MultiFactorAuthConfig implements MultiFactorConfig { (typeof providerConfig.totpProviderConfig.adjacentIntervals !== 'undefined' && typeof providerConfig.totpProviderConfig.adjacentIntervals !== 'number')) { throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Invalid multi-factor configuration response'); } this.providerConfigs.push(providerConfig); @@ -773,18 +773,18 @@ export function validateTestPhoneNumbers( ): void { if (!validator.isObject(testPhoneNumbers)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '"testPhoneNumbers" must be a map of phone number / code pairs.', ); } if (Object.keys(testPhoneNumbers).length > MAXIMUM_TEST_PHONE_NUMBERS) { - throw new FirebaseAuthError(AuthClientErrorCode.MAXIMUM_TEST_PHONE_NUMBER_EXCEEDED); + throw new FirebaseAuthError(authClientErrorCode.MAXIMUM_TEST_PHONE_NUMBER_EXCEEDED); } for (const phoneNumber in testPhoneNumbers) { // Validate phone number. if (!validator.isPhoneNumber(phoneNumber)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_TESTING_PHONE_NUMBER, + authClientErrorCode.INVALID_TESTING_PHONE_NUMBER, `"${phoneNumber}" is not a valid E.164 standard compliant phone number.` ); } @@ -793,7 +793,7 @@ export function validateTestPhoneNumbers( if (!validator.isString(testPhoneNumbers[phoneNumber]) || !/^[\d]{6}$/.test(testPhoneNumbers[phoneNumber])) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_TESTING_PHONE_NUMBER, + authClientErrorCode.INVALID_TESTING_PHONE_NUMBER, `"${testPhoneNumbers[phoneNumber]}" is not a valid 6 digit code string.` ); } @@ -860,7 +860,7 @@ export class EmailSignInConfig implements EmailSignInProviderConfig { }; if (!validator.isNonNullObject(options)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '"EmailSignInConfig" must be a non-null object.', ); } @@ -868,7 +868,7 @@ export class EmailSignInConfig implements EmailSignInProviderConfig { for (const key in options) { if (!(key in validKeys)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, `"${key}" is not a valid EmailSignInConfig parameter.`, ); } @@ -877,14 +877,14 @@ export class EmailSignInConfig implements EmailSignInProviderConfig { if (typeof options.enabled !== 'undefined' && !validator.isBoolean(options.enabled)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '"EmailSignInConfig.enabled" must be a boolean.', ); } if (typeof options.passwordRequired !== 'undefined' && !validator.isBoolean(options.passwordRequired)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '"EmailSignInConfig.passwordRequired" must be a boolean.', ); } @@ -900,7 +900,7 @@ export class EmailSignInConfig implements EmailSignInProviderConfig { constructor(response: {[key: string]: any}) { if (typeof response.allowPasswordSignup === 'undefined') { throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Invalid email sign-in configuration response'); } this.enabled = response.allowPasswordSignup; @@ -1165,7 +1165,7 @@ export class SAMLConfig implements SAMLAuthProviderConfig { }; if (!validator.isNonNullObject(options)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"SAMLAuthProviderConfig" must be a valid non-null object.', ); } @@ -1173,7 +1173,7 @@ export class SAMLConfig implements SAMLAuthProviderConfig { for (const key in options) { if (!(key in validKeys)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, `"${key}" is not a valid SAML config parameter.`, ); } @@ -1182,57 +1182,57 @@ export class SAMLConfig implements SAMLAuthProviderConfig { if (validator.isNonEmptyString(options.providerId)) { if (options.providerId.indexOf('saml.') !== 0) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_PROVIDER_ID, + authClientErrorCode.INVALID_PROVIDER_ID, '"SAMLAuthProviderConfig.providerId" must be a valid non-empty string prefixed with "saml.".', ); } } else if (!ignoreMissingFields) { // providerId is required and not provided correctly. throw new FirebaseAuthError( - !options.providerId ? AuthClientErrorCode.MISSING_PROVIDER_ID : AuthClientErrorCode.INVALID_PROVIDER_ID, + !options.providerId ? authClientErrorCode.MISSING_PROVIDER_ID : authClientErrorCode.INVALID_PROVIDER_ID, '"SAMLAuthProviderConfig.providerId" must be a valid non-empty string prefixed with "saml.".', ); } if (!(ignoreMissingFields && typeof options.idpEntityId === 'undefined') && !validator.isNonEmptyString(options.idpEntityId)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"SAMLAuthProviderConfig.idpEntityId" must be a valid non-empty string.', ); } if (!(ignoreMissingFields && typeof options.ssoURL === 'undefined') && !validator.isURL(options.ssoURL)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"SAMLAuthProviderConfig.ssoURL" must be a valid URL string.', ); } if (!(ignoreMissingFields && typeof options.rpEntityId === 'undefined') && !validator.isNonEmptyString(options.rpEntityId)) { throw new FirebaseAuthError( - !options.rpEntityId ? AuthClientErrorCode.MISSING_SAML_RELYING_PARTY_CONFIG : - AuthClientErrorCode.INVALID_CONFIG, + !options.rpEntityId ? authClientErrorCode.MISSING_SAML_RELYING_PARTY_CONFIG : + authClientErrorCode.INVALID_CONFIG, '"SAMLAuthProviderConfig.rpEntityId" must be a valid non-empty string.', ); } if (!(ignoreMissingFields && typeof options.callbackURL === 'undefined') && !validator.isURL(options.callbackURL)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"SAMLAuthProviderConfig.callbackURL" must be a valid URL string.', ); } if (!(ignoreMissingFields && typeof options.x509Certificates === 'undefined') && !validator.isArray(options.x509Certificates)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"SAMLAuthProviderConfig.x509Certificates" must be a valid array of X509 certificate strings.', ); } (options.x509Certificates || []).forEach((cert: string) => { if (!validator.isNonEmptyString(cert)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"SAMLAuthProviderConfig.x509Certificates" must be a valid array of X509 certificate strings.', ); } @@ -1240,21 +1240,21 @@ export class SAMLConfig implements SAMLAuthProviderConfig { if (typeof (options as any).enableRequestSigning !== 'undefined' && !validator.isBoolean((options as any).enableRequestSigning)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"SAMLAuthProviderConfig.enableRequestSigning" must be a boolean.', ); } if (typeof options.enabled !== 'undefined' && !validator.isBoolean(options.enabled)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"SAMLAuthProviderConfig.enabled" must be a boolean.', ); } if (typeof options.displayName !== 'undefined' && !validator.isString(options.displayName)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"SAMLAuthProviderConfig.displayName" must be a valid string.', ); } @@ -1277,14 +1277,14 @@ export class SAMLConfig implements SAMLAuthProviderConfig { !(validator.isString(response.name) && SAMLConfig.getProviderIdFromResourceName(response.name))) { throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Invalid SAML configuration response'); } const providerId = SAMLConfig.getProviderIdFromResourceName(response.name); if (!providerId) { throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Invalid SAML configuration response'); } this.providerId = providerId; @@ -1418,7 +1418,7 @@ export class OIDCConfig implements OIDCAuthProviderConfig { }; if (!validator.isNonNullObject(options)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"OIDCAuthProviderConfig" must be a valid non-null object.', ); } @@ -1426,7 +1426,7 @@ export class OIDCConfig implements OIDCAuthProviderConfig { for (const key in options) { if (!(key in validKeys)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, `"${key}" is not a valid OIDC config parameter.`, ); } @@ -1435,48 +1435,48 @@ export class OIDCConfig implements OIDCAuthProviderConfig { if (validator.isNonEmptyString(options.providerId)) { if (options.providerId.indexOf('oidc.') !== 0) { throw new FirebaseAuthError( - !options.providerId ? AuthClientErrorCode.MISSING_PROVIDER_ID : AuthClientErrorCode.INVALID_PROVIDER_ID, + !options.providerId ? authClientErrorCode.MISSING_PROVIDER_ID : authClientErrorCode.INVALID_PROVIDER_ID, '"OIDCAuthProviderConfig.providerId" must be a valid non-empty string prefixed with "oidc.".', ); } } else if (!ignoreMissingFields) { throw new FirebaseAuthError( - !options.providerId ? AuthClientErrorCode.MISSING_PROVIDER_ID : AuthClientErrorCode.INVALID_PROVIDER_ID, + !options.providerId ? authClientErrorCode.MISSING_PROVIDER_ID : authClientErrorCode.INVALID_PROVIDER_ID, '"OIDCAuthProviderConfig.providerId" must be a valid non-empty string prefixed with "oidc.".', ); } if (!(ignoreMissingFields && typeof options.clientId === 'undefined') && !validator.isNonEmptyString(options.clientId)) { throw new FirebaseAuthError( - !options.clientId ? AuthClientErrorCode.MISSING_OAUTH_CLIENT_ID : AuthClientErrorCode.INVALID_OAUTH_CLIENT_ID, + !options.clientId ? authClientErrorCode.MISSING_OAUTH_CLIENT_ID : authClientErrorCode.INVALID_OAUTH_CLIENT_ID, '"OIDCAuthProviderConfig.clientId" must be a valid non-empty string.', ); } if (!(ignoreMissingFields && typeof options.issuer === 'undefined') && !validator.isURL(options.issuer)) { throw new FirebaseAuthError( - !options.issuer ? AuthClientErrorCode.MISSING_ISSUER : AuthClientErrorCode.INVALID_CONFIG, + !options.issuer ? authClientErrorCode.MISSING_ISSUER : authClientErrorCode.INVALID_CONFIG, '"OIDCAuthProviderConfig.issuer" must be a valid URL string.', ); } if (typeof options.enabled !== 'undefined' && !validator.isBoolean(options.enabled)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"OIDCAuthProviderConfig.enabled" must be a boolean.', ); } if (typeof options.displayName !== 'undefined' && !validator.isString(options.displayName)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"OIDCAuthProviderConfig.displayName" must be a valid string.', ); } if (typeof options.clientSecret !== 'undefined' && !validator.isNonEmptyString(options.clientSecret)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"OIDCAuthProviderConfig.clientSecret" must be a valid string.', ); } @@ -1484,7 +1484,7 @@ export class OIDCConfig implements OIDCAuthProviderConfig { Object.keys(options.responseType).forEach((key) => { if (!(key in validResponseTypes)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, `"${key}" is not a valid OAuthResponseType parameter.`, ); } @@ -1493,7 +1493,7 @@ export class OIDCConfig implements OIDCAuthProviderConfig { const idToken = options.responseType.idToken; if (typeof idToken !== 'undefined' && !validator.isBoolean(idToken)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '"OIDCAuthProviderConfig.responseType.idToken" must be a boolean.', ); } @@ -1501,14 +1501,14 @@ export class OIDCConfig implements OIDCAuthProviderConfig { if (typeof code !== 'undefined') { if (!validator.isBoolean(code)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '"OIDCAuthProviderConfig.responseType.code" must be a boolean.', ); } // If code flow is enabled, client secret must be provided. if (code && typeof options.clientSecret === 'undefined') { throw new FirebaseAuthError( - AuthClientErrorCode.MISSING_OAUTH_CLIENT_SECRET, + authClientErrorCode.MISSING_OAUTH_CLIENT_SECRET, 'The OAuth configuration client secret is required to enable OIDC code flow.', ); } @@ -1519,7 +1519,7 @@ export class OIDCConfig implements OIDCAuthProviderConfig { // Only one of OAuth response types can be set to true. if (allKeys > 1 && enabledCount !== 1) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_OAUTH_RESPONSETYPE, + authClientErrorCode.INVALID_OAUTH_RESPONSETYPE, 'Only exactly one OAuth responseType should be set to true.', ); } @@ -1540,14 +1540,14 @@ export class OIDCConfig implements OIDCAuthProviderConfig { !(validator.isString(response.name) && OIDCConfig.getProviderIdFromResourceName(response.name))) { throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Invalid OIDC configuration response'); } const providerId = OIDCConfig.getProviderIdFromResourceName(response.name); if (!providerId) { throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Invalid SAML configuration response'); } this.providerId = providerId; @@ -1649,7 +1649,7 @@ export class SmsRegionsAuthConfig { public static validate(options: SmsRegionConfig): void { if (!validator.isNonNullObject(options)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"SmsRegionConfig" must be a non-null object.', ); } @@ -1662,7 +1662,7 @@ export class SmsRegionsAuthConfig { for (const key in options) { if (!(key in validKeys)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, `"${key}" is not a valid SmsRegionConfig parameter.`, ); } @@ -1671,7 +1671,7 @@ export class SmsRegionsAuthConfig { // validate mutual exclusiveness of allowByDefault and allowlistOnly if (typeof options.allowByDefault !== 'undefined' && typeof options.allowlistOnly !== 'undefined') { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, 'SmsRegionConfig cannot have both "allowByDefault" and "allowlistOnly" parameters.', ); } @@ -1683,7 +1683,7 @@ export class SmsRegionsAuthConfig { for (const key in options.allowByDefault) { if (!(key in allowByDefaultValidKeys)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, `"${key}" is not a valid SmsRegionConfig.allowByDefault parameter.`, ); } @@ -1692,7 +1692,7 @@ export class SmsRegionsAuthConfig { if (typeof options.allowByDefault.disallowedRegions !== 'undefined' && !validator.isArray(options.allowByDefault.disallowedRegions)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"SmsRegionConfig.allowByDefault.disallowedRegions" must be a valid string array.', ); } @@ -1705,7 +1705,7 @@ export class SmsRegionsAuthConfig { for (const key in options.allowlistOnly) { if (!(key in allowListOnlyValidKeys)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, `"${key}" is not a valid SmsRegionConfig.allowlistOnly parameter.`, ); } @@ -1715,7 +1715,7 @@ export class SmsRegionsAuthConfig { if (typeof options.allowlistOnly.allowedRegions !== 'undefined' && !validator.isArray(options.allowlistOnly.allowedRegions)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"SmsRegionConfig.allowlistOnly.allowedRegions" must be a valid string array.', ); } @@ -1941,7 +1941,7 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { if (!validator.isNonNullObject(options)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"RecaptchaConfig" must be a non-null object.', ); } @@ -1949,7 +1949,7 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { for (const key in options) { if (!(key in validKeys)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, `"${key}" is not a valid RecaptchaConfig parameter.`, ); } @@ -1959,7 +1959,7 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { if (typeof options.emailPasswordEnforcementState !== 'undefined') { if (!validator.isNonEmptyString(options.emailPasswordEnforcementState)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '"RecaptchaConfig.emailPasswordEnforcementState" must be a valid non-empty string.', ); } @@ -1968,7 +1968,7 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { options.emailPasswordEnforcementState !== 'AUDIT' && options.emailPasswordEnforcementState !== 'ENFORCE') { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"RecaptchaConfig.emailPasswordEnforcementState" must be either "OFF", "AUDIT" or "ENFORCE".', ); } @@ -1977,7 +1977,7 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { if (typeof options.phoneEnforcementState !== 'undefined') { if (!validator.isNonEmptyString(options.phoneEnforcementState)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '"RecaptchaConfig.phoneEnforcementState" must be a valid non-empty string.', ); } @@ -1986,7 +1986,7 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { options.phoneEnforcementState !== 'AUDIT' && options.phoneEnforcementState !== 'ENFORCE') { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"RecaptchaConfig.phoneEnforcementState" must be either "OFF", "AUDIT" or "ENFORCE".', ); } @@ -1996,7 +1996,7 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { // Validate array if (!validator.isArray(options.managedRules)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"RecaptchaConfig.managedRules" must be an array of valid "RecaptchaManagedRule".', ); } @@ -2009,7 +2009,7 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { if (typeof options.useAccountDefender !== 'undefined') { if (!validator.isBoolean(options.useAccountDefender)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"RecaptchaConfig.useAccountDefender" must be a boolean value".', ); } @@ -2018,7 +2018,7 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { if (typeof options.useSmsBotScore !== 'undefined') { if (!validator.isBoolean(options.useSmsBotScore)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"RecaptchaConfig.useSmsBotScore" must be a boolean value".', ); } @@ -2027,7 +2027,7 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { if (typeof options.useSmsTollFraudProtection !== 'undefined') { if (!validator.isBoolean(options.useSmsTollFraudProtection)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"RecaptchaConfig.useSmsTollFraudProtection" must be a boolean value".', ); } @@ -2037,7 +2037,7 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { // Validate array if (!validator.isArray(options.smsTollFraudManagedRules)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"RecaptchaConfig.smsTollFraudManagedRules" must be an array of valid "RecaptchaTollFraudManagedRule".', ); } @@ -2059,7 +2059,7 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { } if (!validator.isNonNullObject(options)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"RecaptchaManagedRule" must be a non-null object.', ); } @@ -2067,7 +2067,7 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { for (const key in options) { if (!(key in validKeys)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, `"${key}" is not a valid RecaptchaManagedRule parameter.`, ); } @@ -2077,7 +2077,7 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { if (typeof options.action !== 'undefined' && options.action !== 'BLOCK') { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"RecaptchaManagedRule.action" must be "BLOCK".', ); } @@ -2094,7 +2094,7 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { } if (!validator.isNonNullObject(options)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"RecaptchaTollFraudManagedRule" must be a non-null object.', ); } @@ -2102,7 +2102,7 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { for (const key in options) { if (!(key in validKeys)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, `"${key}" is not a valid RecaptchaTollFraudManagedRule parameter.`, ); } @@ -2112,7 +2112,7 @@ export class RecaptchaAuthConfig implements RecaptchaConfig { if (typeof options.action !== 'undefined' && options.action !== 'BLOCK') { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"RecaptchaTollFraudManagedRule.action" must be "BLOCK".', ); } @@ -2162,7 +2162,7 @@ export class MobileLinksAuthConfig { public static validate(options: MobileLinksConfig): void { if (!validator.isNonNullObject(options)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"MobileLinksConfig" must be a non-null object.', ); } @@ -2174,7 +2174,7 @@ export class MobileLinksAuthConfig { for (const key in options) { if (!(key in validKeys)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, `"${key}" is not a valid "MobileLinksConfig" parameter.`, ); } @@ -2184,7 +2184,7 @@ export class MobileLinksAuthConfig { && options.domain !== 'HOSTING_DOMAIN' && options.domain !== 'FIREBASE_DYNAMIC_LINK_DOMAIN') { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"MobileLinksConfig.domain" must be either "HOSTING_DOMAIN" or "FIREBASE_DYNAMIC_LINK_DOMAIN".', ); } @@ -2314,7 +2314,7 @@ export class PasswordPolicyAuthConfig implements PasswordPolicyConfig { }; if (!validator.isNonNullObject(options)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"PasswordPolicyConfig" must be a non-null object.', ); } @@ -2322,7 +2322,7 @@ export class PasswordPolicyAuthConfig implements PasswordPolicyConfig { for (const key in options) { if (!(key in validKeys)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, `"${key}" is not a valid PasswordPolicyConfig parameter.`, ); } @@ -2332,7 +2332,7 @@ export class PasswordPolicyAuthConfig implements PasswordPolicyConfig { !(options.enforcementState === 'ENFORCE' || options.enforcementState === 'OFF')) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"PasswordPolicyConfig.enforcementState" must be either "ENFORCE" or "OFF".', ); } @@ -2340,7 +2340,7 @@ export class PasswordPolicyAuthConfig implements PasswordPolicyConfig { if (typeof options.forceUpgradeOnSignin !== 'undefined') { if (!validator.isBoolean(options.forceUpgradeOnSignin)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"PasswordPolicyConfig.forceUpgradeOnSignin" must be a boolean.', ); } @@ -2349,7 +2349,7 @@ export class PasswordPolicyAuthConfig implements PasswordPolicyConfig { if (typeof options.constraints !== 'undefined') { if (options.enforcementState === 'ENFORCE' && !validator.isNonNullObject(options.constraints)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"PasswordPolicyConfig.constraints" must be a non-empty object.', ); } @@ -2367,7 +2367,7 @@ export class PasswordPolicyAuthConfig implements PasswordPolicyConfig { for (const key in options.constraints) { if (!(key in validCharKeys)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, `"${key}" is not a valid PasswordPolicyConfig.constraints parameter.`, ); } @@ -2375,21 +2375,21 @@ export class PasswordPolicyAuthConfig implements PasswordPolicyConfig { if (typeof options.constraints.requireUppercase !== 'undefined' && !validator.isBoolean(options.constraints.requireUppercase)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"PasswordPolicyConfig.constraints.requireUppercase" must be a boolean.', ); } if (typeof options.constraints.requireLowercase !== 'undefined' && !validator.isBoolean(options.constraints.requireLowercase)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"PasswordPolicyConfig.constraints.requireLowercase" must be a boolean.', ); } if (typeof options.constraints.requireNonAlphanumeric !== 'undefined' && !validator.isBoolean(options.constraints.requireNonAlphanumeric)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"PasswordPolicyConfig.constraints.requireNonAlphanumeric"' + ' must be a boolean.', ); @@ -2397,7 +2397,7 @@ export class PasswordPolicyAuthConfig implements PasswordPolicyConfig { if (typeof options.constraints.requireNumeric !== 'undefined' && !validator.isBoolean(options.constraints.requireNumeric)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"PasswordPolicyConfig.constraints.requireNumeric" must be a boolean.', ); } @@ -2405,14 +2405,14 @@ export class PasswordPolicyAuthConfig implements PasswordPolicyConfig { options.constraints.minLength = 6; } else if (!validator.isNumber(options.constraints.minLength)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"PasswordPolicyConfig.constraints.minLength" must be a number.', ); } else { if (!(options.constraints.minLength >= 6 && options.constraints.minLength <= 30)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"PasswordPolicyConfig.constraints.minLength"' + ' must be an integer between 6 and 30, inclusive.', ); @@ -2422,14 +2422,14 @@ export class PasswordPolicyAuthConfig implements PasswordPolicyConfig { options.constraints.maxLength = 4096; } else if (!validator.isNumber(options.constraints.maxLength)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"PasswordPolicyConfig.constraints.maxLength" must be a number.', ); } else { if (!(options.constraints.maxLength >= options.constraints.minLength && options.constraints.maxLength <= 4096)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"PasswordPolicyConfig.constraints.maxLength"' + ' must be greater than or equal to minLength and at max 4096.', ); @@ -2438,7 +2438,7 @@ export class PasswordPolicyAuthConfig implements PasswordPolicyConfig { } else { if (options.enforcementState === 'ENFORCE') { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"PasswordPolicyConfig.constraints" must be defined.', ); } @@ -2456,7 +2456,7 @@ export class PasswordPolicyAuthConfig implements PasswordPolicyConfig { constructor(response: PasswordPolicyAuthServerConfig) { if (typeof response.passwordPolicyEnforcementState === 'undefined') { throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Invalid password policy configuration response'); } this.enforcementState = response.passwordPolicyEnforcementState; @@ -2525,7 +2525,7 @@ export class EmailPrivacyAuthConfig { public static validate(options: EmailPrivacyConfig): void { if (!validator.isNonNullObject(options)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"EmailPrivacyConfig" must be a non-null object.', ); } @@ -2537,7 +2537,7 @@ export class EmailPrivacyAuthConfig { for (const key in options) { if (!(key in validKeys)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, `"${key}" is not a valid "EmailPrivacyConfig" parameter.`, ); } @@ -2546,7 +2546,7 @@ export class EmailPrivacyAuthConfig { if (typeof options.enableImprovedEmailPrivacy !== 'undefined' && !validator.isBoolean(options.enableImprovedEmailPrivacy)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"EmailPrivacyConfig.enableImprovedEmailPrivacy" must be a valid boolean value.', ); } diff --git a/src/auth/base-auth.ts b/src/auth/base-auth.ts index 25e1a3db2b..1d71210fe1 100644 --- a/src/auth/base-auth.ts +++ b/src/auth/base-auth.ts @@ -15,7 +15,8 @@ */ import { App, FirebaseArrayIndexError } from '../app'; -import { AuthClientErrorCode, ErrorInfo, FirebaseAuthError } from '../utils/error'; +import { authClientErrorCode, FirebaseAuthError } from './error'; +import { ErrorInfo } from '../utils/error'; import { deepCopy } from '../utils/deep-copy'; import * as validator from '../utils/validator'; @@ -217,7 +218,7 @@ export abstract class BaseAuth { if (checkRevoked || isEmulator) { return this.verifyDecodedJWTNotRevokedOrDisabled( decodedIdToken, - AuthClientErrorCode.ID_TOKEN_REVOKED); + authClientErrorCode.ID_TOKEN_REVOKED); } return decodedIdToken; }); @@ -331,7 +332,7 @@ export abstract class BaseAuth { public getUsers(identifiers: UserIdentifier[]): Promise { if (!validator.isArray(identifiers)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, '`identifiers` parameter must be an array'); + authClientErrorCode.INVALID_ARGUMENT, '`identifiers` parameter must be an array'); } return this.authRequestHandler .getAccountInfoByIdentifiers(identifiers) @@ -355,7 +356,7 @@ export abstract class BaseAuth { return !!matchingUserInfo && id.providerUid === matchingUserInfo.uid; } else { throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'Unhandled identifier type'); } }); @@ -427,7 +428,7 @@ export abstract class BaseAuth { if (error.code === 'auth/user-not-found') { // Something must have happened after creating the user and then retrieving it. throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'Unable to create the user record provided.'); } throw error; @@ -477,7 +478,7 @@ export abstract class BaseAuth { public deleteUsers(uids: string[]): Promise { if (!validator.isArray(uids)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, '`uids` parameter must be an array'); + authClientErrorCode.INVALID_ARGUMENT, '`uids` parameter must be an array'); } return this.authRequestHandler.deleteAccounts(uids, /*force=*/true) .then((batchDeleteAccountsResponse) => { @@ -496,7 +497,7 @@ export abstract class BaseAuth { result.errors = batchDeleteAccountsResponse.errors.map((batchDeleteErrorInfo) => { if (batchDeleteErrorInfo.index === undefined) { throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'Corrupt BatchDeleteAccountsResponse detected'); } @@ -504,7 +505,7 @@ export abstract class BaseAuth { // We unconditionally set force=true, so the 'NOT_DISABLED' error // should not be possible. const code = msg && msg.startsWith('NOT_DISABLED') ? - AuthClientErrorCode.USER_NOT_DISABLED : AuthClientErrorCode.INTERNAL_ERROR; + authClientErrorCode.USER_NOT_DISABLED : authClientErrorCode.INTERNAL_ERROR; return new FirebaseAuthError(code, batchDeleteErrorInfo.message); }; @@ -544,7 +545,7 @@ export abstract class BaseAuth { if (properties.providerToLink.providerId === 'email') { if (typeof properties.email !== 'undefined') { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, "Both UpdateRequest.email and UpdateRequest.providerToLink.providerId='email' were set. To " + 'link to the email/password provider, only specify the UpdateRequest.email field.'); } @@ -553,7 +554,7 @@ export abstract class BaseAuth { } else if (properties.providerToLink.providerId === 'phone') { if (typeof properties.phoneNumber !== 'undefined') { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, "Both UpdateRequest.phoneNumber and UpdateRequest.providerToLink.providerId='phone' were set. To " + 'link to a phone provider, only specify the UpdateRequest.phoneNumber field.'); } @@ -569,7 +570,7 @@ export abstract class BaseAuth { // to relax this restriction and just unlink it. if (properties.phoneNumber === null) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, "Both UpdateRequest.phoneNumber=null and UpdateRequest.providersToUnlink=['phone'] were set. To " + 'unlink from a phone provider, only specify the UpdateRequest.phoneNumber=null field.'); } @@ -683,7 +684,7 @@ export abstract class BaseAuth { // Return rejected promise if expiresIn is not available. if (!validator.isNonNullObject(sessionCookieOptions) || !validator.isNumber(sessionCookieOptions.expiresIn)) { - return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_SESSION_COOKIE_DURATION)); + return Promise.reject(new FirebaseAuthError(authClientErrorCode.INVALID_SESSION_COOKIE_DURATION)); } return this.authRequestHandler.createSessionCookie( idToken, sessionCookieOptions.expiresIn); @@ -723,7 +724,7 @@ export abstract class BaseAuth { if (checkRevoked || isEmulator) { return this.verifyDecodedJWTNotRevokedOrDisabled( decodedIdToken, - AuthClientErrorCode.SESSION_COOKIE_REVOKED); + authClientErrorCode.SESSION_COOKIE_REVOKED); } return decodedIdToken; }); @@ -966,7 +967,7 @@ export abstract class BaseAuth { } return Promise.reject( new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '"AuthProviderConfigFilter.type" must be either "saml" or "oidc"')); } @@ -997,7 +998,7 @@ export abstract class BaseAuth { return new SAMLConfig(response); }); } - return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)); + return Promise.reject(new FirebaseAuthError(authClientErrorCode.INVALID_PROVIDER_ID)); } /** @@ -1019,7 +1020,7 @@ export abstract class BaseAuth { } else if (SAMLConfig.isProviderId(providerId)) { return this.authRequestHandler.deleteInboundSamlConfig(providerId); } - return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)); + return Promise.reject(new FirebaseAuthError(authClientErrorCode.INVALID_PROVIDER_ID)); } /** @@ -1041,7 +1042,7 @@ export abstract class BaseAuth { providerId: string, updatedConfig: UpdateAuthProviderRequest): Promise { if (!validator.isNonNullObject(updatedConfig)) { return Promise.reject(new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, 'Request is missing "UpdateAuthProviderRequest" configuration.', )); } @@ -1056,7 +1057,7 @@ export abstract class BaseAuth { return new SAMLConfig(response); }); } - return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)); + return Promise.reject(new FirebaseAuthError(authClientErrorCode.INVALID_PROVIDER_ID)); } /** @@ -1073,7 +1074,7 @@ export abstract class BaseAuth { public createProviderConfig(config: AuthProviderConfig): Promise { if (!validator.isNonNullObject(config)) { return Promise.reject(new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, 'Request is missing "AuthProviderConfig" configuration.', )); } @@ -1088,7 +1089,7 @@ export abstract class BaseAuth { return new SAMLConfig(response); }); } - return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)); + return Promise.reject(new FirebaseAuthError(authClientErrorCode.INVALID_PROVIDER_ID)); } /** @alpha */ @@ -1121,7 +1122,7 @@ export abstract class BaseAuth { .then((user: UserRecord) => { if (user.disabled) { throw new FirebaseAuthError( - AuthClientErrorCode.USER_DISABLED, + authClientErrorCode.USER_DISABLED, 'The user record is disabled.'); } // If no tokens valid after time available, token is not revoked. diff --git a/src/auth/error.ts b/src/auth/error.ts new file mode 100644 index 0000000000..3d29020ee2 --- /dev/null +++ b/src/auth/error.ts @@ -0,0 +1,695 @@ +/*! + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PrefixedFirebaseError, toHttpResponse, ErrorInfo } from '../utils/error'; +import { RequestResponseError } from '../utils/api-request'; +import { deepCopy } from '../utils/deep-copy'; + +/** + * The constant mapping for valid Auth client error codes. + */ +export const AuthErrorCode = { + AUTH_BLOCKING_TOKEN_EXPIRED: 'auth-blocking-token-expired', + BILLING_NOT_ENABLED: 'billing-not-enabled', + CLAIMS_TOO_LARGE: 'claims-too-large', + CONFIGURATION_EXISTS: 'configuration-exists', + CONFIGURATION_NOT_FOUND: 'configuration-not-found', + ID_TOKEN_EXPIRED: 'id-token-expired', + INVALID_ARGUMENT: 'argument-error', + INVALID_CONFIG: 'invalid-config', + EMAIL_ALREADY_EXISTS: 'email-already-exists', + EMAIL_NOT_FOUND: 'email-not-found', + FORBIDDEN_CLAIM: 'reserved-claim', + INVALID_ID_TOKEN: 'invalid-id-token', + ID_TOKEN_REVOKED: 'id-token-revoked', + INTERNAL_ERROR: 'internal-error', + INVALID_CLAIMS: 'invalid-claims', + INVALID_CONTINUE_URI: 'invalid-continue-uri', + INVALID_CREATION_TIME: 'invalid-creation-time', + INVALID_CREDENTIAL: 'invalid-credential', + INVALID_DISABLED_FIELD: 'invalid-disabled-field', + INVALID_DISPLAY_NAME: 'invalid-display-name', + INVALID_DYNAMIC_LINK_DOMAIN: 'invalid-dynamic-link-domain', + INVALID_HOSTING_LINK_DOMAIN: 'invalid-hosting-link-domain', + INVALID_EMAIL_VERIFIED: 'invalid-email-verified', + INVALID_EMAIL: 'invalid-email', + INVALID_NEW_EMAIL: 'invalid-new-email', + INVALID_ENROLLED_FACTORS: 'invalid-enrolled-factors', + INVALID_ENROLLMENT_TIME: 'invalid-enrollment-time', + INVALID_HASH_ALGORITHM: 'invalid-hash-algorithm', + INVALID_HASH_BLOCK_SIZE: 'invalid-hash-block-size', + INVALID_HASH_DERIVED_KEY_LENGTH: 'invalid-hash-derived-key-length', + INVALID_HASH_KEY: 'invalid-hash-key', + INVALID_HASH_MEMORY_COST: 'invalid-hash-memory-cost', + INVALID_HASH_PARALLELIZATION: 'invalid-hash-parallelization', + INVALID_HASH_ROUNDS: 'invalid-hash-rounds', + INVALID_HASH_SALT_SEPARATOR: 'invalid-hash-salt-separator', + INVALID_LAST_SIGN_IN_TIME: 'invalid-last-sign-in-time', + INVALID_NAME: 'invalid-name', + INVALID_OAUTH_CLIENT_ID: 'invalid-oauth-client-id', + INVALID_PAGE_TOKEN: 'invalid-page-token', + INVALID_PASSWORD: 'invalid-password', + INVALID_PASSWORD_HASH: 'invalid-password-hash', + INVALID_PASSWORD_SALT: 'invalid-password-salt', + INVALID_PHONE_NUMBER: 'invalid-phone-number', + INVALID_PHOTO_URL: 'invalid-photo-url', + INVALID_PROJECT_ID: 'invalid-project-id', + INVALID_PROVIDER_DATA: 'invalid-provider-data', + INVALID_PROVIDER_ID: 'invalid-provider-id', + INVALID_PROVIDER_UID: 'invalid-provider-uid', + INVALID_OAUTH_RESPONSETYPE: 'invalid-oauth-responsetype', + INVALID_SESSION_COOKIE_DURATION: 'invalid-session-cookie-duration', + INVALID_TENANT_ID: 'invalid-tenant-id', + INVALID_TENANT_TYPE: 'invalid-tenant-type', + INVALID_TESTING_PHONE_NUMBER: 'invalid-testing-phone-number', + INVALID_UID: 'invalid-uid', + INVALID_USER_IMPORT: 'invalid-user-import', + INVALID_TOKENS_VALID_AFTER_TIME: 'invalid-tokens-valid-after-time', + MISMATCHING_TENANT_ID: 'mismatching-tenant-id', + MISSING_ANDROID_PACKAGE_NAME: 'missing-android-package-name', + MISSING_CONFIG: 'missing-config', + MISSING_CONTINUE_URI: 'missing-continue-uri', + MISSING_DISPLAY_NAME: 'missing-display-name', + MISSING_EMAIL: 'missing-email', + MISSING_IOS_BUNDLE_ID: 'missing-ios-bundle-id', + MISSING_ISSUER: 'missing-issuer', + MISSING_HASH_ALGORITHM: 'missing-hash-algorithm', + MISSING_OAUTH_CLIENT_ID: 'missing-oauth-client-id', + MISSING_OAUTH_CLIENT_SECRET: 'missing-oauth-client-secret', + MISSING_PROVIDER_ID: 'missing-provider-id', + MISSING_SAML_RELYING_PARTY_CONFIG: 'missing-saml-relying-party-config', + MAXIMUM_TEST_PHONE_NUMBER_EXCEEDED: 'test-phone-number-limit-exceeded', + MAXIMUM_USER_COUNT_EXCEEDED: 'maximum-user-count-exceeded', + MISSING_UID: 'missing-uid', + OPERATION_NOT_ALLOWED: 'operation-not-allowed', + PHONE_NUMBER_ALREADY_EXISTS: 'phone-number-already-exists', + PROJECT_NOT_FOUND: 'project-not-found', + INSUFFICIENT_PERMISSION: 'insufficient-permission', + QUOTA_EXCEEDED: 'quota-exceeded', + SECOND_FACTOR_LIMIT_EXCEEDED: 'second-factor-limit-exceeded', + SECOND_FACTOR_UID_ALREADY_EXISTS: 'second-factor-uid-already-exists', + SESSION_COOKIE_EXPIRED: 'session-cookie-expired', + SESSION_COOKIE_REVOKED: 'session-cookie-revoked', + TENANT_NOT_FOUND: 'tenant-not-found', + UID_ALREADY_EXISTS: 'uid-already-exists', + UNAUTHORIZED_DOMAIN: 'unauthorized-continue-uri', + UNSUPPORTED_FIRST_FACTOR: 'unsupported-first-factor', + UNSUPPORTED_SECOND_FACTOR: 'unsupported-second-factor', + UNSUPPORTED_TENANT_OPERATION: 'unsupported-tenant-operation', + UNVERIFIED_EMAIL: 'unverified-email', + USER_NOT_FOUND: 'user-not-found', + NOT_FOUND: 'not-found', + USER_DISABLED: 'user-disabled', + USER_NOT_DISABLED: 'user-not-disabled', + INVALID_RECAPTCHA_ACTION: 'invalid-recaptcha-action', + INVALID_RECAPTCHA_ENFORCEMENT_STATE: 'invalid-recaptcha-enforcement-state', + RECAPTCHA_NOT_ENABLED: 'recaptcha-not-enabled', +} as const; + +/** + * The type definition for valid Auth client error codes. + */ +export type AuthErrorCode = typeof AuthErrorCode[keyof typeof AuthErrorCode]; + +/** + * Internal Auth client error code mapping used to construct ErrorInfo. + */ +export const authClientErrorCode: { readonly [K in keyof typeof AuthErrorCode]: ErrorInfo } = { + AUTH_BLOCKING_TOKEN_EXPIRED: { + code: AuthErrorCode.AUTH_BLOCKING_TOKEN_EXPIRED, + message: 'The provided Firebase Auth Blocking token is expired.', + }, + BILLING_NOT_ENABLED: { + code: AuthErrorCode.BILLING_NOT_ENABLED, + message: 'Feature requires billing to be enabled.', + }, + CLAIMS_TOO_LARGE: { + code: AuthErrorCode.CLAIMS_TOO_LARGE, + message: 'Developer claims maximum payload size exceeded.', + }, + CONFIGURATION_EXISTS: { + code: AuthErrorCode.CONFIGURATION_EXISTS, + message: 'A configuration already exists with the provided identifier.', + }, + CONFIGURATION_NOT_FOUND: { + code: AuthErrorCode.CONFIGURATION_NOT_FOUND, + message: 'There is no configuration corresponding to the provided identifier.', + }, + ID_TOKEN_EXPIRED: { + code: AuthErrorCode.ID_TOKEN_EXPIRED, + message: 'The provided Firebase ID token is expired.', + }, + INVALID_ARGUMENT: { + code: AuthErrorCode.INVALID_ARGUMENT, + message: 'Invalid argument provided.', + }, + INVALID_CONFIG: { + code: AuthErrorCode.INVALID_CONFIG, + message: 'The provided configuration is invalid.', + }, + EMAIL_ALREADY_EXISTS: { + code: AuthErrorCode.EMAIL_ALREADY_EXISTS, + message: 'The email address is already in use by another account.', + }, + EMAIL_NOT_FOUND: { + code: AuthErrorCode.EMAIL_NOT_FOUND, + message: 'There is no user record corresponding to the provided email.', + }, + FORBIDDEN_CLAIM: { + code: AuthErrorCode.FORBIDDEN_CLAIM, + message: 'The specified developer claim is reserved and cannot be specified.', + }, + INVALID_ID_TOKEN: { + code: AuthErrorCode.INVALID_ID_TOKEN, + message: 'The provided ID token is not a valid Firebase ID token.', + }, + ID_TOKEN_REVOKED: { + code: AuthErrorCode.ID_TOKEN_REVOKED, + message: 'The Firebase ID token has been revoked.', + }, + INTERNAL_ERROR: { + code: AuthErrorCode.INTERNAL_ERROR, + message: 'An internal error has occurred.', + }, + INVALID_CLAIMS: { + code: AuthErrorCode.INVALID_CLAIMS, + message: 'The provided custom claim attributes are invalid.', + }, + INVALID_CONTINUE_URI: { + code: AuthErrorCode.INVALID_CONTINUE_URI, + message: 'The continue URL must be a valid URL string.', + }, + INVALID_CREATION_TIME: { + code: AuthErrorCode.INVALID_CREATION_TIME, + message: 'The creation time must be a valid UTC date string.', + }, + INVALID_CREDENTIAL: { + code: AuthErrorCode.INVALID_CREDENTIAL, + message: 'Invalid credential object provided.', + }, + INVALID_DISABLED_FIELD: { + code: AuthErrorCode.INVALID_DISABLED_FIELD, + message: 'The disabled field must be a boolean.', + }, + INVALID_DISPLAY_NAME: { + code: AuthErrorCode.INVALID_DISPLAY_NAME, + message: 'The displayName field must be a valid string.', + }, + INVALID_DYNAMIC_LINK_DOMAIN: { + code: AuthErrorCode.INVALID_DYNAMIC_LINK_DOMAIN, + message: 'The provided dynamic link domain is not configured or authorized for the current project.', + }, + INVALID_HOSTING_LINK_DOMAIN: { + code: AuthErrorCode.INVALID_HOSTING_LINK_DOMAIN, + message: 'The provided hosting link domain is not configured in Firebase Hosting or ' + + 'is not owned by the current project.', + }, + INVALID_EMAIL_VERIFIED: { + code: AuthErrorCode.INVALID_EMAIL_VERIFIED, + message: 'The emailVerified field must be a boolean.', + }, + INVALID_EMAIL: { + code: AuthErrorCode.INVALID_EMAIL, + message: 'The email address is improperly formatted.', + }, + INVALID_NEW_EMAIL: { + code: AuthErrorCode.INVALID_NEW_EMAIL, + message: 'The new email address is improperly formatted.', + }, + INVALID_ENROLLED_FACTORS: { + code: AuthErrorCode.INVALID_ENROLLED_FACTORS, + message: 'The enrolled factors must be a valid array of MultiFactorInfo objects.', + }, + INVALID_ENROLLMENT_TIME: { + code: AuthErrorCode.INVALID_ENROLLMENT_TIME, + message: 'The second factor enrollment time must be a valid UTC date string.', + }, + INVALID_HASH_ALGORITHM: { + code: AuthErrorCode.INVALID_HASH_ALGORITHM, + message: 'The hash algorithm must match one of the strings in the list of supported algorithms.', + }, + INVALID_HASH_BLOCK_SIZE: { + code: AuthErrorCode.INVALID_HASH_BLOCK_SIZE, + message: 'The hash block size must be a valid number.', + }, + INVALID_HASH_DERIVED_KEY_LENGTH: { + code: AuthErrorCode.INVALID_HASH_DERIVED_KEY_LENGTH, + message: 'The hash derived key length must be a valid number.', + }, + INVALID_HASH_KEY: { + code: AuthErrorCode.INVALID_HASH_KEY, + message: 'The hash key must a valid byte buffer.', + }, + INVALID_HASH_MEMORY_COST: { + code: AuthErrorCode.INVALID_HASH_MEMORY_COST, + message: 'The hash memory cost must be a valid number.', + }, + INVALID_HASH_PARALLELIZATION: { + code: AuthErrorCode.INVALID_HASH_PARALLELIZATION, + message: 'The hash parallelization must be a valid number.', + }, + INVALID_HASH_ROUNDS: { + code: AuthErrorCode.INVALID_HASH_ROUNDS, + message: 'The hash rounds must be a valid number.', + }, + INVALID_HASH_SALT_SEPARATOR: { + code: AuthErrorCode.INVALID_HASH_SALT_SEPARATOR, + message: 'The hashing algorithm salt separator field must be a valid byte buffer.', + }, + INVALID_LAST_SIGN_IN_TIME: { + code: AuthErrorCode.INVALID_LAST_SIGN_IN_TIME, + message: 'The last sign-in time must be a valid UTC date string.', + }, + INVALID_NAME: { + code: AuthErrorCode.INVALID_NAME, + message: 'The resource name provided is invalid.', + }, + INVALID_OAUTH_CLIENT_ID: { + code: AuthErrorCode.INVALID_OAUTH_CLIENT_ID, + message: 'The provided OAuth client ID is invalid.', + }, + INVALID_PAGE_TOKEN: { + code: AuthErrorCode.INVALID_PAGE_TOKEN, + message: 'The page token must be a valid non-empty string.', + }, + INVALID_PASSWORD: { + code: AuthErrorCode.INVALID_PASSWORD, + message: 'The password must be a string with at least 6 characters.', + }, + INVALID_PASSWORD_HASH: { + code: AuthErrorCode.INVALID_PASSWORD_HASH, + message: 'The password hash must be a valid byte buffer.', + }, + INVALID_PASSWORD_SALT: { + code: AuthErrorCode.INVALID_PASSWORD_SALT, + message: 'The password salt must be a valid byte buffer.', + }, + INVALID_PHONE_NUMBER: { + code: AuthErrorCode.INVALID_PHONE_NUMBER, + message: 'The phone number must be a non-empty E.164 standard compliant identifier string.', + }, + INVALID_PHOTO_URL: { + code: AuthErrorCode.INVALID_PHOTO_URL, + message: 'The photoURL field must be a valid URL.', + }, + INVALID_PROJECT_ID: { + code: AuthErrorCode.INVALID_PROJECT_ID, + message: 'Invalid parent project. Either parent project doesn\'t exist or didn\'t enable multi-tenancy.', + }, + INVALID_PROVIDER_DATA: { + code: AuthErrorCode.INVALID_PROVIDER_DATA, + message: 'The providerData must be a valid array of UserInfo objects.', + }, + INVALID_PROVIDER_ID: { + code: AuthErrorCode.INVALID_PROVIDER_ID, + message: 'The providerId must be a valid supported provider identifier string.', + }, + INVALID_PROVIDER_UID: { + code: AuthErrorCode.INVALID_PROVIDER_UID, + message: 'The providerUid must be a valid provider uid string.', + }, + INVALID_OAUTH_RESPONSETYPE: { + code: AuthErrorCode.INVALID_OAUTH_RESPONSETYPE, + message: 'Only exactly one OAuth responseType should be set to true.', + }, + INVALID_SESSION_COOKIE_DURATION: { + code: AuthErrorCode.INVALID_SESSION_COOKIE_DURATION, + message: 'The session cookie duration must be a valid number in milliseconds between 5 minutes and 2 weeks.', + }, + INVALID_TENANT_ID: { + code: AuthErrorCode.INVALID_TENANT_ID, + message: 'The tenant ID must be a valid non-empty string.', + }, + INVALID_TENANT_TYPE: { + code: AuthErrorCode.INVALID_TENANT_TYPE, + message: 'Tenant type must be either "full_service" or "lightweight".', + }, + INVALID_TESTING_PHONE_NUMBER: { + code: AuthErrorCode.INVALID_TESTING_PHONE_NUMBER, + message: 'Invalid testing phone number or invalid test code provided.', + }, + INVALID_UID: { + code: AuthErrorCode.INVALID_UID, + message: 'The uid must be a non-empty string with at most 128 characters.', + }, + INVALID_USER_IMPORT: { + code: AuthErrorCode.INVALID_USER_IMPORT, + message: 'The user record to import is invalid.', + }, + INVALID_TOKENS_VALID_AFTER_TIME: { + code: AuthErrorCode.INVALID_TOKENS_VALID_AFTER_TIME, + message: 'The tokensValidAfterTime must be a valid UTC number in seconds.', + }, + MISMATCHING_TENANT_ID: { + code: AuthErrorCode.MISMATCHING_TENANT_ID, + message: 'User tenant ID does not match with the current TenantAwareAuth tenant ID.', + }, + MISSING_ANDROID_PACKAGE_NAME: { + code: AuthErrorCode.MISSING_ANDROID_PACKAGE_NAME, + message: 'An Android Package Name must be provided if the Android App is required to be installed.', + }, + MISSING_CONFIG: { + code: AuthErrorCode.MISSING_CONFIG, + message: 'The provided configuration is missing required attributes.', + }, + MISSING_CONTINUE_URI: { + code: AuthErrorCode.MISSING_CONTINUE_URI, + message: 'A valid continue URL must be provided in the request.', + }, + MISSING_DISPLAY_NAME: { + code: AuthErrorCode.MISSING_DISPLAY_NAME, + message: 'The resource being created or edited is missing a valid display name.', + }, + MISSING_EMAIL: { + code: AuthErrorCode.MISSING_EMAIL, + message: 'The email is required for the specified action. For example, a multi-factor ' + + 'user requires a verified email.', + }, + MISSING_IOS_BUNDLE_ID: { + code: AuthErrorCode.MISSING_IOS_BUNDLE_ID, + message: 'The request is missing an iOS Bundle ID.', + }, + MISSING_ISSUER: { + code: AuthErrorCode.MISSING_ISSUER, + message: 'The OAuth/OIDC configuration issuer must not be empty.', + }, + MISSING_HASH_ALGORITHM: { + code: AuthErrorCode.MISSING_HASH_ALGORITHM, + message: 'Importing users with password hashes requires that the hashing algorithm and its parameters be provided.', + }, + MISSING_OAUTH_CLIENT_ID: { + code: AuthErrorCode.MISSING_OAUTH_CLIENT_ID, + message: 'The OAuth/OIDC configuration client ID must not be empty.', + }, + MISSING_OAUTH_CLIENT_SECRET: { + code: AuthErrorCode.MISSING_OAUTH_CLIENT_SECRET, + message: 'The OAuth configuration client secret is required to enable OIDC code flow.', + }, + MISSING_PROVIDER_ID: { + code: AuthErrorCode.MISSING_PROVIDER_ID, + message: 'A valid provider ID must be provided in the request.', + }, + MISSING_SAML_RELYING_PARTY_CONFIG: { + code: AuthErrorCode.MISSING_SAML_RELYING_PARTY_CONFIG, + message: 'The SAML configuration provided is missing a relying party configuration.', + }, + MAXIMUM_TEST_PHONE_NUMBER_EXCEEDED: { + code: AuthErrorCode.MAXIMUM_TEST_PHONE_NUMBER_EXCEEDED, + message: 'The maximum allowed number of test phone number / code pairs has been exceeded.', + }, + MAXIMUM_USER_COUNT_EXCEEDED: { + code: AuthErrorCode.MAXIMUM_USER_COUNT_EXCEEDED, + message: 'The maximum allowed number of users to import has been exceeded.', + }, + MISSING_UID: { + code: AuthErrorCode.MISSING_UID, + message: 'A uid identifier is required for the current operation.', + }, + OPERATION_NOT_ALLOWED: { + code: AuthErrorCode.OPERATION_NOT_ALLOWED, + message: 'The given sign-in provider is disabled for this Firebase project. Enable it in the ' + + 'Firebase console, under the sign-in method tab of the Auth section.', + }, + PHONE_NUMBER_ALREADY_EXISTS: { + code: AuthErrorCode.PHONE_NUMBER_ALREADY_EXISTS, + message: 'The user with the provided phone number already exists.', + }, + PROJECT_NOT_FOUND: { + code: AuthErrorCode.PROJECT_NOT_FOUND, + message: 'No Firebase project was found for the provided credential.', + }, + INSUFFICIENT_PERMISSION: { + code: AuthErrorCode.INSUFFICIENT_PERMISSION, + message: 'Credential implementation provided to initializeApp() via the "credential" property has insufficient permission to access the requested resource. See https://firebase.google.com/docs/admin/setup for details on how to authenticate this SDK with appropriate permissions.', + }, + QUOTA_EXCEEDED: { + code: AuthErrorCode.QUOTA_EXCEEDED, + message: 'The project quota for the specified operation has been exceeded.', + }, + SECOND_FACTOR_LIMIT_EXCEEDED: { + code: AuthErrorCode.SECOND_FACTOR_LIMIT_EXCEEDED, + message: 'The maximum number of allowed second factors on a user has been exceeded.', + }, + SECOND_FACTOR_UID_ALREADY_EXISTS: { + code: AuthErrorCode.SECOND_FACTOR_UID_ALREADY_EXISTS, + message: 'The specified second factor "uid" already exists.', + }, + SESSION_COOKIE_EXPIRED: { + code: AuthErrorCode.SESSION_COOKIE_EXPIRED, + message: 'The Firebase session cookie is expired.', + }, + SESSION_COOKIE_REVOKED: { + code: AuthErrorCode.SESSION_COOKIE_REVOKED, + message: 'The Firebase session cookie has been revoked.', + }, + TENANT_NOT_FOUND: { + code: AuthErrorCode.TENANT_NOT_FOUND, + message: 'There is no tenant corresponding to the provided identifier.', + }, + UID_ALREADY_EXISTS: { + code: AuthErrorCode.UID_ALREADY_EXISTS, + message: 'The user with the provided uid already exists.', + }, + UNAUTHORIZED_DOMAIN: { + code: AuthErrorCode.UNAUTHORIZED_DOMAIN, + message: 'The domain of the continue URL is not whitelisted. Whitelist the domain in the Firebase console.', + }, + UNSUPPORTED_FIRST_FACTOR: { + code: AuthErrorCode.UNSUPPORTED_FIRST_FACTOR, + message: 'A multi-factor user requires a supported first factor.', + }, + UNSUPPORTED_SECOND_FACTOR: { + code: AuthErrorCode.UNSUPPORTED_SECOND_FACTOR, + message: 'The request specified an unsupported type of second factor.', + }, + UNSUPPORTED_TENANT_OPERATION: { + code: AuthErrorCode.UNSUPPORTED_TENANT_OPERATION, + message: 'This operation is not supported in a multi-tenant context.', + }, + UNVERIFIED_EMAIL: { + code: AuthErrorCode.UNVERIFIED_EMAIL, + message: 'A verified email is required for the specified action. For example, a ' + + 'multi-factor user requires a verified email.', + }, + USER_NOT_FOUND: { + code: AuthErrorCode.USER_NOT_FOUND, + message: 'There is no user record corresponding to the provided identifier.', + }, + NOT_FOUND: { + code: AuthErrorCode.NOT_FOUND, + message: 'The requested resource was not found.', + }, + USER_DISABLED: { + code: AuthErrorCode.USER_DISABLED, + message: 'The user record is disabled.', + }, + USER_NOT_DISABLED: { + code: AuthErrorCode.USER_NOT_DISABLED, + message: 'The user must be disabled in order to bulk delete it (or you must pass force=true).', + }, + INVALID_RECAPTCHA_ACTION: { + code: AuthErrorCode.INVALID_RECAPTCHA_ACTION, + message: 'reCAPTCHA action must be "BLOCK".', + }, + INVALID_RECAPTCHA_ENFORCEMENT_STATE: { + code: AuthErrorCode.INVALID_RECAPTCHA_ENFORCEMENT_STATE, + message: 'reCAPTCHA enforcement state must be either "OFF", "AUDIT" or "ENFORCE".', + }, + RECAPTCHA_NOT_ENABLED: { + code: AuthErrorCode.RECAPTCHA_NOT_ENABLED, + message: 'reCAPTCHA enterprise is not enabled.', + }, +}; + +/** @const {Record} Auth server to client enum error codes. */ +const AUTH_SERVER_TO_CLIENT_CODE: Record = { + // Feature being configured or used requires a billing account. + BILLING_NOT_ENABLED: 'BILLING_NOT_ENABLED', + // Claims payload is too large. + CLAIMS_TOO_LARGE: 'CLAIMS_TOO_LARGE', + // Configuration being added already exists. + CONFIGURATION_EXISTS: 'CONFIGURATION_EXISTS', + // Configuration not found. + CONFIGURATION_NOT_FOUND: 'CONFIGURATION_NOT_FOUND', + // Provided credential has insufficient permissions. + INSUFFICIENT_PERMISSION: 'INSUFFICIENT_PERMISSION', + // Provided configuration has invalid fields. + INVALID_CONFIG: 'INVALID_CONFIG', + // Provided configuration identifier is invalid. + INVALID_CONFIG_ID: 'INVALID_PROVIDER_ID', + // ActionCodeSettings missing continue URL. + INVALID_CONTINUE_URI: 'INVALID_CONTINUE_URI', + // Dynamic link domain in provided ActionCodeSettings is not authorized. + INVALID_DYNAMIC_LINK_DOMAIN: 'INVALID_DYNAMIC_LINK_DOMAIN', + // Hosting link domain in provided ActionCodeSettings is not owned by the current project. + INVALID_HOSTING_LINK_DOMAIN: 'INVALID_HOSTING_LINK_DOMAIN', + // uploadAccount provides an email that already exists. + DUPLICATE_EMAIL: 'EMAIL_ALREADY_EXISTS', + // uploadAccount provides a localId that already exists. + DUPLICATE_LOCAL_ID: 'UID_ALREADY_EXISTS', + // Request specified a multi-factor enrollment ID that already exists. + DUPLICATE_MFA_ENROLLMENT_ID: 'SECOND_FACTOR_UID_ALREADY_EXISTS', + // setAccountInfo email already exists. + EMAIL_EXISTS: 'EMAIL_ALREADY_EXISTS', + // /accounts:sendOobCode for password reset when user is not found. + EMAIL_NOT_FOUND: 'EMAIL_NOT_FOUND', + // Reserved claim name. + FORBIDDEN_CLAIM: 'FORBIDDEN_CLAIM', + // Invalid claims provided. + INVALID_CLAIMS: 'INVALID_CLAIMS', + // Invalid session cookie duration. + INVALID_DURATION: 'INVALID_SESSION_COOKIE_DURATION', + // Invalid email provided. + INVALID_EMAIL: 'INVALID_EMAIL', + // Invalid new email provided. + INVALID_NEW_EMAIL: 'INVALID_NEW_EMAIL', + // Invalid tenant display name. This can be thrown on CreateTenant and UpdateTenant. + INVALID_DISPLAY_NAME: 'INVALID_DISPLAY_NAME', + // Invalid ID token provided. + INVALID_ID_TOKEN: 'INVALID_ID_TOKEN', + // Invalid tenant/parent resource name. + INVALID_NAME: 'INVALID_NAME', + // OIDC configuration has an invalid OAuth client ID. + INVALID_OAUTH_CLIENT_ID: 'INVALID_OAUTH_CLIENT_ID', + // Invalid page token. + INVALID_PAGE_SELECTION: 'INVALID_PAGE_TOKEN', + // Invalid phone number. + INVALID_PHONE_NUMBER: 'INVALID_PHONE_NUMBER', + // Invalid agent project. Either agent project doesn't exist or didn't enable multi-tenancy. + INVALID_PROJECT_ID: 'INVALID_PROJECT_ID', + // Invalid provider ID. + INVALID_PROVIDER_ID: 'INVALID_PROVIDER_ID', + // Invalid service account. + INVALID_SERVICE_ACCOUNT: 'INVALID_CREDENTIAL', + // Invalid testing phone number. + INVALID_TESTING_PHONE_NUMBER: 'INVALID_TESTING_PHONE_NUMBER', + // Invalid tenant type. + INVALID_TENANT_TYPE: 'INVALID_TENANT_TYPE', + // Missing Android package name. + MISSING_ANDROID_PACKAGE_NAME: 'MISSING_ANDROID_PACKAGE_NAME', + // Missing configuration. + MISSING_CONFIG: 'MISSING_CONFIG', + // Missing configuration identifier. + MISSING_CONFIG_ID: 'MISSING_PROVIDER_ID', + // Missing tenant display name: This can be thrown on CreateTenant and UpdateTenant. + MISSING_DISPLAY_NAME: 'MISSING_DISPLAY_NAME', + // Email is required for the specified action. For example a multi-factor user requires + // a verified email. + MISSING_EMAIL: 'MISSING_EMAIL', + // Missing iOS bundle ID. + MISSING_IOS_BUNDLE_ID: 'MISSING_IOS_BUNDLE_ID', + // Missing OIDC issuer. + MISSING_ISSUER: 'MISSING_ISSUER', + // No localId provided (deleteAccount missing localId). + MISSING_LOCAL_ID: 'MISSING_UID', + // OIDC configuration is missing an OAuth client ID. + MISSING_OAUTH_CLIENT_ID: 'MISSING_OAUTH_CLIENT_ID', + // Missing provider ID. + MISSING_PROVIDER_ID: 'MISSING_PROVIDER_ID', + // Missing SAML RP config. + MISSING_SAML_RELYING_PARTY_CONFIG: 'MISSING_SAML_RELYING_PARTY_CONFIG', + // Empty user list in uploadAccount. + MISSING_USER_ACCOUNT: 'MISSING_UID', + // Password auth disabled in console. + OPERATION_NOT_ALLOWED: 'OPERATION_NOT_ALLOWED', + // Provided credential has insufficient permissions. + PERMISSION_DENIED: 'INSUFFICIENT_PERMISSION', + // Phone number already exists. + PHONE_NUMBER_EXISTS: 'PHONE_NUMBER_ALREADY_EXISTS', + // Project not found. + PROJECT_NOT_FOUND: 'PROJECT_NOT_FOUND', + // In multi-tenancy context: project creation quota exceeded. + QUOTA_EXCEEDED: 'QUOTA_EXCEEDED', + // Currently only 5 second factors can be set on the same user. + SECOND_FACTOR_LIMIT_EXCEEDED: 'SECOND_FACTOR_LIMIT_EXCEEDED', + // Tenant not found. + TENANT_NOT_FOUND: 'TENANT_NOT_FOUND', + // Tenant ID mismatch. + TENANT_ID_MISMATCH: 'MISMATCHING_TENANT_ID', + // Token expired error. + TOKEN_EXPIRED: 'ID_TOKEN_EXPIRED', + // Continue URL provided in ActionCodeSettings has a domain that is not whitelisted. + UNAUTHORIZED_DOMAIN: 'UNAUTHORIZED_DOMAIN', + // A multi-factor user requires a supported first factor. + UNSUPPORTED_FIRST_FACTOR: 'UNSUPPORTED_FIRST_FACTOR', + // The request specified an unsupported type of second factor. + UNSUPPORTED_SECOND_FACTOR: 'UNSUPPORTED_SECOND_FACTOR', + // Operation is not supported in a multi-tenant context. + UNSUPPORTED_TENANT_OPERATION: 'UNSUPPORTED_TENANT_OPERATION', + // A verified email is required for the specified action. For example a multi-factor user + // requires a verified email. + UNVERIFIED_EMAIL: 'UNVERIFIED_EMAIL', + // User on which action is to be performed is not found. + USER_NOT_FOUND: 'USER_NOT_FOUND', + // User record is disabled. + USER_DISABLED: 'USER_DISABLED', + // Password provided is too weak. + WEAK_PASSWORD: 'INVALID_PASSWORD', + // Unrecognized reCAPTCHA action. + INVALID_RECAPTCHA_ACTION: 'INVALID_RECAPTCHA_ACTION', + // Unrecognized reCAPTCHA enforcement state. + INVALID_RECAPTCHA_ENFORCEMENT_STATE: 'INVALID_RECAPTCHA_ENFORCEMENT_STATE', + // reCAPTCHA is not enabled for account defender. + RECAPTCHA_NOT_ENABLED: 'RECAPTCHA_NOT_ENABLED' +}; + +/** + * Firebase Auth error code structure. This extends PrefixedFirebaseError. + */ +export class FirebaseAuthError extends PrefixedFirebaseError { + /** + * Creates the developer-facing error corresponding to the backend error code. + * + * @param serverErrorCode - The server error code. + * @param [message] The error message. The default message is used + * if not provided. + * @param [serverError] The error's raw server response. + * @returns The corresponding developer-facing error. + * @internal + */ + public static fromServerError( + serverErrorCode: string, + message?: string, + serverError?: RequestResponseError, + ): FirebaseAuthError { + // serverErrorCode could contain additional details: + // ERROR_CODE : Detailed message which can also contain colons + const colonSeparator = (serverErrorCode || '').indexOf(':'); + let customMessage = null; + if (colonSeparator !== -1) { + customMessage = serverErrorCode.substring(colonSeparator + 1).trim(); + serverErrorCode = serverErrorCode.substring(0, colonSeparator).trim(); + } + // If not found, default to internal error. + const clientCodeKey = AUTH_SERVER_TO_CLIENT_CODE[serverErrorCode] || 'INTERNAL_ERROR'; + const error: ErrorInfo = deepCopy((authClientErrorCode as any)[clientCodeKey]); + // Server detailed message should have highest priority. + error.message = customMessage || message || error.message; + error.cause = serverError; + error.httpResponse = serverError?.response ? toHttpResponse(serverError.response) : undefined; + return new FirebaseAuthError(error); + } + + /** + * @param info - The error code info. + * @param message - The error message. This will override the default message if provided. + */ + constructor(info: ErrorInfo, message?: string) { + // Override default message if custom message provided. + super('auth', info.code, message || info.message, info.httpResponse, info.cause); + + } +} diff --git a/src/auth/index.ts b/src/auth/index.ts index 4650b25e27..ccef8194d2 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -170,5 +170,5 @@ export { export { FirebaseAuthError, - AuthClientErrorCode, -} from '../utils/error'; + AuthErrorCode, +} from './error'; diff --git a/src/auth/project-config.ts b/src/auth/project-config.ts index 1695ec3a0d..4028dbb515 100644 --- a/src/auth/project-config.ts +++ b/src/auth/project-config.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import * as validator from '../utils/validator'; -import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; +import { authClientErrorCode, FirebaseAuthError } from './error'; import { SmsRegionsAuthConfig, SmsRegionConfig, @@ -150,7 +150,7 @@ export class ProjectConfig { private static validate(request: UpdateProjectConfigRequest): void { if (!validator.isNonNullObject(request)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '"UpdateProjectConfigRequest" must be a valid non-null object.', ); } @@ -166,7 +166,7 @@ export class ProjectConfig { for (const key in request) { if (!(key in validKeys)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, `"${key}" is not a valid UpdateProjectConfigRequest parameter.`, ); } diff --git a/src/auth/tenant-manager.ts b/src/auth/tenant-manager.ts index 19da5b4c83..654d9241f1 100644 --- a/src/auth/tenant-manager.ts +++ b/src/auth/tenant-manager.ts @@ -17,7 +17,7 @@ import * as validator from '../utils/validator'; import { App } from '../app'; import * as utils from '../utils/index'; -import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; +import { authClientErrorCode, FirebaseAuthError } from './error'; import { BaseAuth, createFirebaseTokenGenerator, SessionCookieOptions } from './base-auth'; import { Tenant, TenantServerResponse, CreateTenantRequest, UpdateTenantRequest } from './tenant'; @@ -93,7 +93,7 @@ export class TenantAwareAuth extends BaseAuth { .then((decodedClaims) => { // Validate tenant ID. if (decodedClaims.firebase.tenant !== this.tenantId) { - throw new FirebaseAuthError(AuthClientErrorCode.MISMATCHING_TENANT_ID); + throw new FirebaseAuthError(authClientErrorCode.MISMATCHING_TENANT_ID); } return decodedClaims; }); @@ -106,11 +106,11 @@ export class TenantAwareAuth extends BaseAuth { idToken: string, sessionCookieOptions: SessionCookieOptions): Promise { // Validate arguments before processing. if (!validator.isNonEmptyString(idToken)) { - return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ID_TOKEN)); + return Promise.reject(new FirebaseAuthError(authClientErrorCode.INVALID_ID_TOKEN)); } if (!validator.isNonNullObject(sessionCookieOptions) || !validator.isNumber(sessionCookieOptions.expiresIn)) { - return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_SESSION_COOKIE_DURATION)); + return Promise.reject(new FirebaseAuthError(authClientErrorCode.INVALID_SESSION_COOKIE_DURATION)); } // This will verify the ID token and then match the tenant ID before creating the session cookie. return this.verifyIdToken(idToken) @@ -127,7 +127,7 @@ export class TenantAwareAuth extends BaseAuth { return super.verifySessionCookie(sessionCookie, checkRevoked) .then((decodedClaims) => { if (decodedClaims.firebase.tenant !== this.tenantId) { - throw new FirebaseAuthError(AuthClientErrorCode.MISMATCHING_TENANT_ID); + throw new FirebaseAuthError(authClientErrorCode.MISMATCHING_TENANT_ID); } return decodedClaims; }); @@ -171,7 +171,7 @@ export class TenantManager { */ public authForTenant(tenantId: string): TenantAwareAuth { if (!validator.isNonEmptyString(tenantId)) { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID); + throw new FirebaseAuthError(authClientErrorCode.INVALID_TENANT_ID); } if (typeof this.tenantsMap[tenantId] === 'undefined') { this.tenantsMap[tenantId] = new TenantAwareAuth(this.app, tenantId); diff --git a/src/auth/tenant.ts b/src/auth/tenant.ts index 19812c02e6..ea46eed609 100644 --- a/src/auth/tenant.ts +++ b/src/auth/tenant.ts @@ -16,7 +16,7 @@ import * as validator from '../utils/validator'; import { deepCopy } from '../utils/deep-copy'; -import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; +import { authClientErrorCode, FirebaseAuthError } from './error'; import { EmailSignInConfig, EmailSignInConfigServerRequest, MultiFactorAuthServerConfig, @@ -258,7 +258,7 @@ export class Tenant { const label = createRequest ? 'CreateTenantRequest' : 'UpdateTenantRequest'; if (!validator.isNonNullObject(request)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, `"${label}" must be a valid non-null object.`, ); } @@ -266,7 +266,7 @@ export class Tenant { for (const key in request) { if (!(key in validKeys)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, `"${key}" is not a valid ${label} parameter.`, ); } @@ -275,7 +275,7 @@ export class Tenant { if (typeof request.displayName !== 'undefined' && !validator.isNonEmptyString(request.displayName)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, `"${label}.displayName" must be a valid non-empty string.`, ); } @@ -291,7 +291,7 @@ export class Tenant { } else if (request.testPhoneNumbers === null && createRequest) { // null allowed only for update operations. throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, `"${label}.testPhoneNumbers" must be a non-null object.`, ); } @@ -330,7 +330,7 @@ export class Tenant { const tenantId = Tenant.getTenantIdFromResourceName(response.name); if (!tenantId) { throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Invalid tenant response', ); } diff --git a/src/auth/token-generator.ts b/src/auth/token-generator.ts index 3df1dcebe2..4a41a50158 100644 --- a/src/auth/token-generator.ts +++ b/src/auth/token-generator.ts @@ -15,7 +15,8 @@ * limitations under the License. */ -import { AuthClientErrorCode, ErrorInfo, FirebaseAuthError } from '../utils/error'; +import { authClientErrorCode, FirebaseAuthError } from './error'; +import { ErrorInfo } from '../utils/error'; import { RequestResponseError } from '../utils/api-request'; import { CryptoSigner, CryptoSignerError, CryptoSignerErrorCode } from '../utils/crypto-signer'; @@ -99,13 +100,13 @@ export class FirebaseTokenGenerator { constructor(signer: CryptoSigner, public readonly tenantId?: string) { if (!validator.isNonNullObject(signer)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CREDENTIAL, + authClientErrorCode.INVALID_CREDENTIAL, 'INTERNAL ASSERT: Must provide a CryptoSigner to use FirebaseTokenGenerator.', ); } if (typeof this.tenantId !== 'undefined' && !validator.isNonEmptyString(this.tenantId)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '`tenantId` argument must be a non-empty string.'); } this.signer = signer; @@ -131,7 +132,7 @@ export class FirebaseTokenGenerator { } if (errorMessage) { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage); + throw new FirebaseAuthError(authClientErrorCode.INVALID_ARGUMENT, errorMessage); } const claims: {[key: string]: any} = {}; @@ -141,7 +142,7 @@ export class FirebaseTokenGenerator { if (Object.prototype.hasOwnProperty.call(developerClaims, key)) { if (BLACKLISTED_CLAIMS.indexOf(key) !== -1) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, `Developer claim "${key}" is reserved and cannot be specified.`, ); } @@ -212,32 +213,32 @@ export function handleCryptoSignerError(err: Error): Error { return err; } if (err.code === CryptoSignerErrorCode.SERVER_ERROR && validator.isNonNullObject(err.cause)) { - const httpError = err.cause; - const errorResponse = (httpError as RequestResponseError).response.data; + const httpError = err.cause as RequestResponseError; + const errorResponse = httpError.response.data; if (validator.isNonNullObject(errorResponse) && errorResponse.error) { const errorCode = errorResponse.error.status; const description = 'Please refer to https://firebase.google.com/docs/auth/admin/create-custom-tokens ' + 'for more details on how to use and troubleshoot this feature.'; const errorMsg = `${errorResponse.error.message}; ${description}`; - return FirebaseAuthError.fromServerError(errorCode, errorMsg, errorResponse); + return FirebaseAuthError.fromServerError(errorCode, errorMsg, httpError); } - return new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR, + return new FirebaseAuthError(authClientErrorCode.INTERNAL_ERROR, 'Error returned from server: ' + errorResponse + '. Additionally, an ' + 'internal error occurred while attempting to extract the ' + 'errorcode from the error.' ); } - return new FirebaseAuthError(mapToAuthClientErrorCode(err.code), err.message); + return new FirebaseAuthError(mapToAuthErrorInfo(err.code), err.message); } -function mapToAuthClientErrorCode(code: string): ErrorInfo { +function mapToAuthErrorInfo(code: string): ErrorInfo { switch (code) { case CryptoSignerErrorCode.INVALID_CREDENTIAL: - return AuthClientErrorCode.INVALID_CREDENTIAL; + return authClientErrorCode.INVALID_CREDENTIAL; case CryptoSignerErrorCode.INVALID_ARGUMENT: - return AuthClientErrorCode.INVALID_ARGUMENT; + return authClientErrorCode.INVALID_ARGUMENT; default: - return AuthClientErrorCode.INTERNAL_ERROR; + return authClientErrorCode.INTERNAL_ERROR; } } diff --git a/src/auth/token-verifier.ts b/src/auth/token-verifier.ts index 64da8ac516..112b82fc32 100644 --- a/src/auth/token-verifier.ts +++ b/src/auth/token-verifier.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { AuthClientErrorCode, FirebaseAuthError, ErrorInfo } from '../utils/error'; +import { authClientErrorCode, FirebaseAuthError } from './error'; +import { ErrorInfo } from '../utils/error'; import * as util from '../utils/index'; import * as validator from '../utils/validator'; import { @@ -274,7 +275,7 @@ export const ID_TOKEN_INFO: FirebaseTokenInfo = { verifyApiName: 'verifyIdToken()', jwtName: 'Firebase ID token', shortName: 'ID token', - expiredErrorCode: AuthClientErrorCode.ID_TOKEN_EXPIRED, + expiredErrorCode: authClientErrorCode.ID_TOKEN_EXPIRED, }; /** @@ -287,7 +288,7 @@ export const AUTH_BLOCKING_TOKEN_INFO: FirebaseTokenInfo = { verifyApiName: '_verifyAuthBlockingToken()', jwtName: 'Firebase Auth Blocking token', shortName: 'Auth Blocking token', - expiredErrorCode: AuthClientErrorCode.AUTH_BLOCKING_TOKEN_EXPIRED, + expiredErrorCode: authClientErrorCode.AUTH_BLOCKING_TOKEN_EXPIRED, }; /** @@ -300,7 +301,7 @@ export const SESSION_COOKIE_INFO: FirebaseTokenInfo = { verifyApiName: 'verifySessionCookie()', jwtName: 'Firebase session cookie', shortName: 'session cookie', - expiredErrorCode: AuthClientErrorCode.SESSION_COOKIE_EXPIRED, + expiredErrorCode: authClientErrorCode.SESSION_COOKIE_EXPIRED, }; /** @@ -336,42 +337,42 @@ export class FirebaseTokenVerifier { if (!validator.isURL(clientCertUrl)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, 'The provided public client certificate URL is an invalid URL.', ); } else if (!validator.isURL(issuer)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, 'The provided JWT issuer is an invalid URL.', ); } else if (!validator.isNonNullObject(tokenInfo)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, 'The provided JWT information is not an object or null.', ); } else if (!validator.isURL(tokenInfo.url)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, 'The provided JWT verification documentation URL is invalid.', ); } else if (!validator.isNonEmptyString(tokenInfo.verifyApiName)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, 'The JWT verify API name must be a non-empty string.', ); } else if (!validator.isNonEmptyString(tokenInfo.jwtName)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, 'The JWT public full name must be a non-empty string.', ); } else if (!validator.isNonEmptyString(tokenInfo.shortName)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, 'The JWT public short name must be a non-empty string.', ); } else if (!validator.isNonNullObject(tokenInfo.expiredErrorCode) || !('code' in tokenInfo.expiredErrorCode)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, 'The JWT expiration error code must be a non-null ErrorInfo object.', ); } @@ -393,7 +394,7 @@ export class FirebaseTokenVerifier { public verifyJWT(jwtToken: string, isEmulator = false): Promise { if (!validator.isString(jwtToken)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, `First argument to ${this.tokenInfo.verifyApiName} must be a ${this.tokenInfo.jwtName} string.`, ); } @@ -417,7 +418,7 @@ export class FirebaseTokenVerifier { audience: string | undefined): Promise { if (!validator.isString(jwtToken)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, `First argument to ${this.tokenInfo.verifyApiName} must be a ${this.tokenInfo.jwtName} string.`, ); } @@ -441,7 +442,7 @@ export class FirebaseTokenVerifier { .then((projectId) => { if (!validator.isNonEmptyString(projectId)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CREDENTIAL, + authClientErrorCode.INVALID_CREDENTIAL, 'Must initialize app with a cert credential or set your Firebase project ID as the ' + `GOOGLE_CLOUD_PROJECT environment variable to call ${this.tokenInfo.verifyApiName}.`, ); @@ -472,10 +473,10 @@ export class FirebaseTokenVerifier { const errorMessage = `Decoding ${this.tokenInfo.jwtName} failed. Make sure you passed ` + `the entire string JWT which represents ${this.shortNameArticle} ` + `${this.tokenInfo.shortName}.` + verifyJwtTokenDocsMessage; - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, + throw new FirebaseAuthError(authClientErrorCode.INVALID_ARGUMENT, errorMessage); } - throw new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR, err.message); + throw new FirebaseAuthError(authClientErrorCode.INTERNAL_ERROR, err.message); }); } @@ -544,7 +545,7 @@ export class FirebaseTokenVerifier { } } if (errorMessage) { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage); + throw new FirebaseAuthError(authClientErrorCode.INVALID_ARGUMENT, errorMessage); } } @@ -573,14 +574,14 @@ export class FirebaseTokenVerifier { return new FirebaseAuthError(this.tokenInfo.expiredErrorCode, errorMessage); } else if (error.code === JwtErrorCode.INVALID_SIGNATURE) { const errorMessage = `${this.tokenInfo.jwtName} has invalid signature.` + verifyJwtTokenDocsMessage; - return new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage); + return new FirebaseAuthError(authClientErrorCode.INVALID_ARGUMENT, errorMessage); } else if (error.code === JwtErrorCode.NO_MATCHING_KID) { const errorMessage = `${this.tokenInfo.jwtName} has "kid" claim which does not ` + `correspond to a known public key. Most likely the ${this.tokenInfo.shortName} ` + 'is expired, so get a fresh token from your client app and try again.'; - return new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage); + return new FirebaseAuthError(authClientErrorCode.INVALID_ARGUMENT, errorMessage); } - return new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, error.message); + return new FirebaseAuthError(authClientErrorCode.INVALID_ARGUMENT, error.message); } } diff --git a/src/auth/user-import-builder.ts b/src/auth/user-import-builder.ts index f396066b49..4554187b10 100644 --- a/src/auth/user-import-builder.ts +++ b/src/auth/user-import-builder.ts @@ -18,7 +18,7 @@ import { FirebaseArrayIndexError } from '../app/index'; import { deepCopy, deepExtend } from '../utils/deep-copy'; import * as utils from '../utils'; import * as validator from '../utils/validator'; -import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; +import { authClientErrorCode, FirebaseAuthError } from './error'; import { UpdateMultiFactorInfoRequest, UpdatePhoneMultiFactorInfoRequest, MultiFactorUpdateSettings } from './auth-config'; @@ -329,7 +329,7 @@ export function convertMultiFactorInfoToServerFormat(multiFactorInfo: UpdateMult enrolledAt = new Date(multiFactorInfo.enrollmentTime).toISOString(); } else { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ENROLLMENT_TIME, + authClientErrorCode.INVALID_ENROLLMENT_TIME, `The second factor "enrollmentTime" for "${multiFactorInfo.uid}" must be a valid ` + 'UTC date string.'); } @@ -353,7 +353,7 @@ export function convertMultiFactorInfoToServerFormat(multiFactorInfo: UpdateMult } else { // Unsupported second factor. throw new FirebaseAuthError( - AuthClientErrorCode.UNSUPPORTED_SECOND_FACTOR, + authClientErrorCode.UNSUPPORTED_SECOND_FACTOR, `Unsupported second factor "${JSON.stringify(multiFactorInfo)}" provided.`); } } @@ -401,7 +401,7 @@ function populateUploadAccountUser( if (typeof user.passwordHash !== 'undefined') { if (!validator.isBuffer(user.passwordHash)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_PASSWORD_HASH, + authClientErrorCode.INVALID_PASSWORD_HASH, ); } result.passwordHash = utils.toWebSafeBase64(user.passwordHash); @@ -409,7 +409,7 @@ function populateUploadAccountUser( if (typeof user.passwordSalt !== 'undefined') { if (!validator.isBuffer(user.passwordSalt)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_PASSWORD_SALT, + authClientErrorCode.INVALID_PASSWORD_SALT, ); } result.salt = utils.toWebSafeBase64(user.passwordSalt); @@ -527,7 +527,7 @@ export class UserImportBuilder { // Map backend request index to original developer provided array index. index: this.indexMap[failedUpload.index], error: new FirebaseAuthError( - AuthClientErrorCode.INVALID_USER_IMPORT, + authClientErrorCode.INVALID_USER_IMPORT, failedUpload.message, ), }); @@ -555,20 +555,20 @@ export class UserImportBuilder { } if (!validator.isNonNullObject(options)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '"UserImportOptions" are required when importing users with passwords.', ); } if (!validator.isNonNullObject(options.hash)) { throw new FirebaseAuthError( - AuthClientErrorCode.MISSING_HASH_ALGORITHM, + authClientErrorCode.MISSING_HASH_ALGORITHM, '"hash.algorithm" is missing from the provided "UserImportOptions".', ); } if (typeof options.hash.algorithm === 'undefined' || !validator.isNonEmptyString(options.hash.algorithm)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_HASH_ALGORITHM, + authClientErrorCode.INVALID_HASH_ALGORITHM, '"hash.algorithm" must be a string matching the list of supported algorithms.', ); } @@ -581,7 +581,7 @@ export class UserImportBuilder { case 'HMAC_MD5': if (!validator.isBuffer(options.hash.key)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_HASH_KEY, + authClientErrorCode.INVALID_HASH_KEY, 'A non-empty "hash.key" byte buffer must be provided for ' + `hash algorithm ${options.hash.algorithm}.`, ); @@ -601,7 +601,7 @@ export class UserImportBuilder { const minRounds = options.hash.algorithm === 'MD5' ? 0 : 1; if (isNaN(rounds) || rounds < minRounds || rounds > 8192) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_HASH_ROUNDS, + authClientErrorCode.INVALID_HASH_ROUNDS, `A valid "hash.rounds" number between ${minRounds} and 8192 must be provided for ` + `hash algorithm ${options.hash.algorithm}.`, ); @@ -617,7 +617,7 @@ export class UserImportBuilder { rounds = getNumberField(options.hash, 'rounds'); if (isNaN(rounds) || rounds < 0 || rounds > 120000) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_HASH_ROUNDS, + authClientErrorCode.INVALID_HASH_ROUNDS, 'A valid "hash.rounds" number between 0 and 120000 must be provided for ' + `hash algorithm ${options.hash.algorithm}.`, ); @@ -631,7 +631,7 @@ export class UserImportBuilder { case 'SCRYPT': { if (!validator.isBuffer(options.hash.key)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_HASH_KEY, + authClientErrorCode.INVALID_HASH_KEY, 'A "hash.key" byte buffer must be provided for ' + `hash algorithm ${options.hash.algorithm}.`, ); @@ -639,7 +639,7 @@ export class UserImportBuilder { rounds = getNumberField(options.hash, 'rounds'); if (isNaN(rounds) || rounds <= 0 || rounds > 8) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_HASH_ROUNDS, + authClientErrorCode.INVALID_HASH_ROUNDS, 'A valid "hash.rounds" number between 1 and 8 must be provided for ' + `hash algorithm ${options.hash.algorithm}.`, ); @@ -647,7 +647,7 @@ export class UserImportBuilder { const memoryCost = getNumberField(options.hash, 'memoryCost'); if (isNaN(memoryCost) || memoryCost <= 0 || memoryCost > 14) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_HASH_MEMORY_COST, + authClientErrorCode.INVALID_HASH_MEMORY_COST, 'A valid "hash.memoryCost" number between 1 and 14 must be provided for ' + `hash algorithm ${options.hash.algorithm}.`, ); @@ -655,7 +655,7 @@ export class UserImportBuilder { if (typeof options.hash.saltSeparator !== 'undefined' && !validator.isBuffer(options.hash.saltSeparator)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_HASH_SALT_SEPARATOR, + authClientErrorCode.INVALID_HASH_SALT_SEPARATOR, '"hash.saltSeparator" must be a byte buffer.', ); } @@ -678,7 +678,7 @@ export class UserImportBuilder { const cpuMemCost = getNumberField(options.hash, 'memoryCost'); if (isNaN(cpuMemCost)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_HASH_MEMORY_COST, + authClientErrorCode.INVALID_HASH_MEMORY_COST, 'A valid "hash.memoryCost" number must be provided for ' + `hash algorithm ${options.hash.algorithm}.`, ); @@ -686,7 +686,7 @@ export class UserImportBuilder { const parallelization = getNumberField(options.hash, 'parallelization'); if (isNaN(parallelization)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_HASH_PARALLELIZATION, + authClientErrorCode.INVALID_HASH_PARALLELIZATION, 'A valid "hash.parallelization" number must be provided for ' + `hash algorithm ${options.hash.algorithm}.`, ); @@ -694,7 +694,7 @@ export class UserImportBuilder { const blockSize = getNumberField(options.hash, 'blockSize'); if (isNaN(blockSize)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_HASH_BLOCK_SIZE, + authClientErrorCode.INVALID_HASH_BLOCK_SIZE, 'A valid "hash.blockSize" number must be provided for ' + `hash algorithm ${options.hash.algorithm}.`, ); @@ -702,7 +702,7 @@ export class UserImportBuilder { const dkLen = getNumberField(options.hash, 'derivedKeyLength'); if (isNaN(dkLen)) { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_HASH_DERIVED_KEY_LENGTH, + authClientErrorCode.INVALID_HASH_DERIVED_KEY_LENGTH, 'A valid "hash.derivedKeyLength" number must be provided for ' + `hash algorithm ${options.hash.algorithm}.`, ); @@ -718,7 +718,7 @@ export class UserImportBuilder { } default: throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_HASH_ALGORITHM, + authClientErrorCode.INVALID_HASH_ALGORITHM, `Unsupported hash algorithm provider "${options.hash.algorithm}".`, ); } diff --git a/src/auth/user-record.ts b/src/auth/user-record.ts index 2310968849..1807ffd01c 100644 --- a/src/auth/user-record.ts +++ b/src/auth/user-record.ts @@ -18,7 +18,7 @@ import { deepCopy } from '../utils/deep-copy'; import { isNonNullObject } from '../utils/validator'; import * as utils from '../utils'; -import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; +import { authClientErrorCode, FirebaseAuthError } from './error'; /** * 'REDACTED', encoded as a base64 string. @@ -188,7 +188,7 @@ export abstract class MultiFactorInfo { const factorId = response && this.getFactorId(response); if (!factorId || !response || !response.mfaEnrollmentId) { throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Invalid multi-factor info response'); } utils.addReadonlyGetter(this, 'uid', response.mfaEnrollmentId); @@ -330,7 +330,7 @@ export class MultiFactorSettings { const parsedEnrolledFactors: MultiFactorInfo[] = []; if (!isNonNullObject(response)) { throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Invalid multi-factor response'); } else if (response.mfaInfo) { response.mfaInfo.forEach((factorResponse) => { @@ -457,7 +457,7 @@ export class UserInfo { // Provider user id and provider id are required. if (!response.rawId || !response.providerId) { throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Invalid user info response'); } @@ -592,7 +592,7 @@ export class UserRecord { // The Firebase user id is required. if (!response.localId) { throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Invalid user response'); } diff --git a/src/data-connect/data-connect-api-client-internal.ts b/src/data-connect/data-connect-api-client-internal.ts index 6292d023aa..cc96f3b4fd 100644 --- a/src/data-connect/data-connect-api-client-internal.ts +++ b/src/data-connect/data-connect-api-client-internal.ts @@ -20,7 +20,8 @@ import { FirebaseApp } from '../app/firebase-app'; import { HttpRequestConfig, HttpClient, RequestResponseError, AuthorizedHttpClient } from '../utils/api-request'; -import { PrefixedFirebaseError } from '../utils/error'; +import { PrefixedFirebaseError, toHttpResponse } from '../utils/error'; +import { FirebaseDataConnectError, DataConnectErrorCode, DATA_CONNECT_ERROR_CODE_MAPPING } from './error'; import * as utils from '../utils/index'; import * as validator from '../utils/validator'; import { ConnectorConfig, ExecuteGraphqlResponse, GraphqlOptions, OperationOptions } from './data-connect-api'; @@ -106,9 +107,10 @@ export class DataConnectApiClient { constructor(private readonly connectorConfig: ConnectorConfig, private readonly app: App) { if (!validator.isNonNullObject(app) || !('options' in app)) { - throw new FirebaseDataConnectError( - DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, - 'First argument passed to getDataConnect() must be a valid Firebase app instance.'); + throw new FirebaseDataConnectError({ + code: DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + message: 'First argument passed to getDataConnect() must be a valid Firebase app instance.' + }); } this.httpClient = new DataConnectHttpClient(app as FirebaseApp); } @@ -165,15 +167,17 @@ export class DataConnectApiClient { options?: GraphqlOptions, ): Promise> { if (!validator.isNonEmptyString(query)) { - throw new FirebaseDataConnectError( - DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, - '`query` must be a non-empty string.'); + throw new FirebaseDataConnectError({ + code: DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + message: '`query` must be a non-empty string.' + }); } if (typeof options !== 'undefined') { if (!validator.isNonNullObject(options)) { - throw new FirebaseDataConnectError( - DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, - 'GraphqlOptions must be a non-null object'); + throw new FirebaseDataConnectError({ + code: DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + message: 'GraphqlOptions must be a non-null object' + }); } } const data = { @@ -242,17 +246,18 @@ export class DataConnectApiClient { typeof name === 'undefined' || !validator.isNonEmptyString(name) ) { - throw new FirebaseDataConnectError( - DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, - '`name` must be a non-empty string.' - ); + throw new FirebaseDataConnectError({ + code: DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + message: '`name` must be a non-empty string.' + }); } if (this.connectorConfig.connector === undefined || this.connectorConfig.connector === '') { - throw new FirebaseDataConnectError( - DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, - `The 'connectorConfig.connector' field used to instantiate your Data Connect - instance must be a non-empty string (the connectorId) when calling executeQuery or executeMutation.`); + throw new FirebaseDataConnectError({ + code: DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + message: `The 'connectorConfig.connector' field used to instantiate your Data Connect + instance must be a non-empty string (the connectorId) when calling executeQuery or executeMutation.` + }); } const data = { @@ -348,11 +353,12 @@ export class DataConnectApiClient { return utils.findProjectId(this.app) .then((projectId) => { if (!validator.isNonEmptyString(projectId)) { - throw new FirebaseDataConnectError( - DATA_CONNECT_ERROR_CODE_MAPPING.UNKNOWN, - 'Failed to determine project ID. Initialize the ' - + 'SDK with service account credentials or set project ID as an app option. ' - + 'Alternatively, set the GOOGLE_CLOUD_PROJECT environment variable.'); + throw new FirebaseDataConnectError({ + code: DATA_CONNECT_ERROR_CODE_MAPPING.UNKNOWN, + message: 'Failed to determine project ID. Initialize the ' + + 'SDK with service account credentials or set project ID as an app option. ' + + 'Alternatively, set the GOOGLE_CLOUD_PROJECT environment variable.' + }); } this.projectId = projectId; return projectId; @@ -377,8 +383,11 @@ export class DataConnectApiClient { const resp = await this.httpClient.send(request); if (resp.data.errors && validator.isNonEmptyArray(resp.data.errors)) { const allMessages = resp.data.errors.map((error: { message: any; }) => error.message).join(' '); - throw new FirebaseDataConnectError( - DATA_CONNECT_ERROR_CODE_MAPPING.QUERY_ERROR, allMessages); + throw new FirebaseDataConnectError({ + code: DATA_CONNECT_ERROR_CODE_MAPPING.QUERY_ERROR, + message: allMessages, + httpResponse: toHttpResponse(resp), + }); } return Promise.resolve({ data: resp.data.data as GraphqlResponse, @@ -392,9 +401,12 @@ export class DataConnectApiClient { const response = err.response; if (!response.isJson()) { - return new FirebaseDataConnectError( - DATA_CONNECT_ERROR_CODE_MAPPING.UNKNOWN, - `Unexpected response with status: ${response.status} and body: ${response.text}`); + return new FirebaseDataConnectError({ + code: DATA_CONNECT_ERROR_CODE_MAPPING.UNKNOWN, + message: `Unexpected response with status: ${response.status} and body: ${response.text}`, + httpResponse: toHttpResponse(response), + cause: err + }); } const error: ServerError = (response.data as ErrorResponse).error || {}; @@ -402,8 +414,13 @@ export class DataConnectApiClient { if (error.status && error.status in DATA_CONNECT_ERROR_CODE_MAPPING) { code = DATA_CONNECT_ERROR_CODE_MAPPING[error.status]; } - const message = error.message || `Unknown server error: ${response.text}`; - return new FirebaseDataConnectError(code, message); + const message = error.message || 'Unknown server error'; + return new FirebaseDataConnectError({ + code, + message, + httpResponse: toHttpResponse(response), + cause: err, + }); } /** @@ -460,9 +477,12 @@ export class DataConnectApiClient { private handleBulkImportErrors(err: FirebaseDataConnectError): never { if (err.code === `data-connect/${DATA_CONNECT_ERROR_CODE_MAPPING.QUERY_ERROR}`){ - throw new FirebaseDataConnectError( - DATA_CONNECT_ERROR_CODE_MAPPING.QUERY_ERROR, - `${err.message}. Make sure that your table name passed in matches the type name in your GraphQL schema file.`); + throw new FirebaseDataConnectError({ + code: DATA_CONNECT_ERROR_CODE_MAPPING.QUERY_ERROR, + message: `${err.message}. Make sure that your table name passed in matches the type name in your ` + + 'GraphQL schema file.', + cause: err, + }); } throw err; } @@ -475,19 +495,23 @@ export class DataConnectApiClient { data: Variables, ): Promise> { if (!validator.isNonEmptyString(tableName)) { - throw new FirebaseDataConnectError( - DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, - '`tableName` must be a non-empty string.'); + throw new FirebaseDataConnectError({ + code: DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + message: '`tableName` must be a non-empty string.' + }); } if (validator.isArray(data)) { - throw new FirebaseDataConnectError( - DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, - '`data` must be an object, not an array, for single insert. For arrays, please use `insertMany` function.'); + throw new FirebaseDataConnectError({ + code: DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + message: '`data` must be an object, not an array, for single insert. For arrays, please use ' + + '`insertMany` function.' + }); } if (!validator.isNonNullObject(data)) { - throw new FirebaseDataConnectError( - DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, - '`data` must be a non-null object.'); + throw new FirebaseDataConnectError({ + code: DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + message: '`data` must be a non-null object.' + }); } try { @@ -497,9 +521,11 @@ export class DataConnectApiClient { // Use internal executeGraphql return this.executeGraphql(mutation).catch(this.handleBulkImportErrors); } catch (e: any) { - throw new FirebaseDataConnectError( - DATA_CONNECT_ERROR_CODE_MAPPING.INTERNAL, - `Failed to construct insert mutation: ${e.message}`); + throw new FirebaseDataConnectError({ + code: DATA_CONNECT_ERROR_CODE_MAPPING.INTERNAL, + message: `Failed to construct insert mutation: ${e.message}`, + cause: e, + }); } } @@ -511,14 +537,16 @@ export class DataConnectApiClient { data: Variables, ): Promise> { if (!validator.isNonEmptyString(tableName)) { - throw new FirebaseDataConnectError( - DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, - '`tableName` must be a non-empty string.'); + throw new FirebaseDataConnectError({ + code: DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + message: '`tableName` must be a non-empty string.' + }); } if (!validator.isNonEmptyArray(data)) { - throw new FirebaseDataConnectError( - DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, - '`data` must be a non-empty array for insertMany.'); + throw new FirebaseDataConnectError({ + code: DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + message: '`data` must be a non-empty array for insertMany.', + }); } try { @@ -528,8 +556,11 @@ export class DataConnectApiClient { // Use internal executeGraphql return this.executeGraphql(mutation).catch(this.handleBulkImportErrors); } catch (e: any) { - throw new FirebaseDataConnectError(DATA_CONNECT_ERROR_CODE_MAPPING.INTERNAL, - `Failed to construct insertMany mutation: ${e.message}`); + throw new FirebaseDataConnectError({ + code: DATA_CONNECT_ERROR_CODE_MAPPING.INTERNAL, + message: `Failed to construct insertMany mutation: ${e.message}`, + cause: e, + }); } } @@ -541,19 +572,23 @@ export class DataConnectApiClient { data: Variables, ): Promise> { if (!validator.isNonEmptyString(tableName)) { - throw new FirebaseDataConnectError( - DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, - '`tableName` must be a non-empty string.'); + throw new FirebaseDataConnectError({ + code: DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + message: '`tableName` must be a non-empty string.' + }); } if (validator.isArray(data)) { - throw new FirebaseDataConnectError( - DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, - '`data` must be an object, not an array, for single upsert. For arrays, please use `upsertMany` function.'); + throw new FirebaseDataConnectError({ + code: DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + message: '`data` must be an object, not an array, for single upsert. For arrays, please use ' + + '`upsertMany` function.' + }); } if (!validator.isNonNullObject(data)) { - throw new FirebaseDataConnectError( - DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, - '`data` must be a non-null object.'); + throw new FirebaseDataConnectError({ + code: DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + message: '`data` must be a non-null object.' + }); } try { @@ -563,9 +598,11 @@ export class DataConnectApiClient { // Use internal executeGraphql return this.executeGraphql(mutation).catch(this.handleBulkImportErrors); } catch (e: any) { - throw new FirebaseDataConnectError( - DATA_CONNECT_ERROR_CODE_MAPPING.INTERNAL, - `Failed to construct upsert mutation: ${e.message}`); + throw new FirebaseDataConnectError({ + code: DATA_CONNECT_ERROR_CODE_MAPPING.INTERNAL, + message: `Failed to construct upsert mutation: ${e.message}`, + cause: e, + }); } } @@ -577,14 +614,16 @@ export class DataConnectApiClient { data: Variables, ): Promise> { if (!validator.isNonEmptyString(tableName)) { - throw new FirebaseDataConnectError( - DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, - '`tableName` must be a non-empty string.'); + throw new FirebaseDataConnectError({ + code: DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + message: '`tableName` must be a non-empty string.' + }); } if (!validator.isNonEmptyArray(data)) { - throw new FirebaseDataConnectError( - DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, - '`data` must be a non-empty array for upsertMany.'); + throw new FirebaseDataConnectError({ + code: DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + message: '`data` must be a non-empty array for upsertMany.' + }); } try { @@ -594,9 +633,11 @@ export class DataConnectApiClient { // Use internal executeGraphql return this.executeGraphql(mutation).catch(this.handleBulkImportErrors); } catch (e: any) { - throw new FirebaseDataConnectError( - DATA_CONNECT_ERROR_CODE_MAPPING.INTERNAL, - `Failed to construct upsertMany mutation: ${e.message}`); + throw new FirebaseDataConnectError({ + code: DATA_CONNECT_ERROR_CODE_MAPPING.INTERNAL, + message: `Failed to construct upsertMany mutation: ${e.message}`, + cause: e, + }); } } } @@ -638,45 +679,3 @@ interface ServerError { message?: string; status?: string; } - -export const DATA_CONNECT_ERROR_CODE_MAPPING: { [key: string]: DataConnectErrorCode } = { - ABORTED: 'aborted', - INVALID_ARGUMENT: 'invalid-argument', - INVALID_CREDENTIAL: 'invalid-credential', - INTERNAL: 'internal-error', - PERMISSION_DENIED: 'permission-denied', - UNAUTHENTICATED: 'unauthenticated', - NOT_FOUND: 'not-found', - UNKNOWN: 'unknown-error', - QUERY_ERROR: 'query-error', -}; - -export type DataConnectErrorCode = - 'aborted' - | 'invalid-argument' - | 'invalid-credential' - | 'internal-error' - | 'permission-denied' - | 'unauthenticated' - | 'not-found' - | 'unknown-error' - | 'query-error'; - -/** - * Firebase Data Connect error code structure. This extends PrefixedFirebaseError. - * - * @param code - The error code. - * @param message - The error message. - * @constructor - */ -export class FirebaseDataConnectError extends PrefixedFirebaseError { - constructor(code: DataConnectErrorCode, message: string) { - super('data-connect', code, message); - - /* tslint:disable:max-line-length */ - // Set the prototype explicitly. See the following link for more details: - // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work - /* tslint:enable:max-line-length */ - (this as any).__proto__ = FirebaseDataConnectError.prototype; - } -} diff --git a/src/data-connect/error.ts b/src/data-connect/error.ts new file mode 100644 index 0000000000..f20ac6cd48 --- /dev/null +++ b/src/data-connect/error.ts @@ -0,0 +1,63 @@ +/*! + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PrefixedFirebaseError, ErrorInfo } from '../utils/error'; + +/** @const {Record} Data Connect server to client error code mapping. */ +export const DATA_CONNECT_ERROR_CODE_MAPPING: Record = { + ABORTED: 'aborted', + INVALID_ARGUMENT: 'invalid-argument', + INVALID_CREDENTIAL: 'invalid-credential', + INTERNAL: 'internal-error', + PERMISSION_DENIED: 'permission-denied', + UNAUTHENTICATED: 'unauthenticated', + NOT_FOUND: 'not-found', + UNKNOWN: 'unknown-error', + QUERY_ERROR: 'query-error', +}; + +/** + * The constant mapping for valid Data Connect client error codes. + */ +export const DataConnectErrorCode = { + ABORTED: 'aborted', + INVALID_ARGUMENT: 'invalid-argument', + INVALID_CREDENTIAL: 'invalid-credential', + INTERNAL: 'internal-error', + PERMISSION_DENIED: 'permission-denied', + UNAUTHENTICATED: 'unauthenticated', + NOT_FOUND: 'not-found', + UNKNOWN: 'unknown-error', + QUERY_ERROR: 'query-error', +} as const; + +/** + * The type definition for valid Data Connect client error codes. + */ +export type DataConnectErrorCode = typeof DataConnectErrorCode[keyof typeof DataConnectErrorCode]; + +/** + * Firebase Data Connect error type. This extends PrefixedFirebaseError. + */ +export class FirebaseDataConnectError extends PrefixedFirebaseError { + /** + * @param info - The error code info. + * @param message - The error message. If provided, this will override the default message. + */ + constructor(info: ErrorInfo, message?: string) { + super('data-connect', info.code, message || info.message, info.httpResponse, info.cause); + } +} diff --git a/src/data-connect/index.ts b/src/data-connect/index.ts index 95d9f26da2..a8adac447f 100644 --- a/src/data-connect/index.ts +++ b/src/data-connect/index.ts @@ -89,3 +89,5 @@ export function getDataConnect(connectorConfig: ConnectorConfig, app?: App): Dat * @internal */ export const validateAdminArgs = _validateAdminArgs; + +export { FirebaseDataConnectError, DataConnectErrorCode } from './error'; \ No newline at end of file diff --git a/src/data-connect/validate-admin-args.ts b/src/data-connect/validate-admin-args.ts index f4332b5ddd..8c4a210e0e 100644 --- a/src/data-connect/validate-admin-args.ts +++ b/src/data-connect/validate-admin-args.ts @@ -21,7 +21,7 @@ import { ConnectorConfig, OperationOptions } from './data-connect-api'; import { DATA_CONNECT_ERROR_CODE_MAPPING, FirebaseDataConnectError, -} from './data-connect-api-client-internal'; +} from './error'; /** * @internal @@ -82,10 +82,10 @@ export function _validateAdminArgs( } if (!dcInstance || (!realVars && validateVars)) { - throw new FirebaseDataConnectError( - DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, - 'Variables required.' - ); + throw new FirebaseDataConnectError({ + code: DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + message: 'Variables required.' + }); } return { dc: dcInstance, vars: realVars, options: realOptions }; } diff --git a/src/database/database.ts b/src/database/database.ts index 355c43b33a..4abea5f7a2 100644 --- a/src/database/database.ts +++ b/src/database/database.ts @@ -18,7 +18,9 @@ import { URL } from 'url'; import * as path from 'path'; import { FirebaseDatabase } from '@firebase/database-types'; -import { FirebaseDatabaseError, AppErrorCodes, FirebaseAppError } from '../utils/error'; +import { FirebaseDatabaseError } from './error'; +import { AppErrorCode, FirebaseAppError } from '../app/error'; +import { toHttpResponse } from '../utils/error'; import { Database as DatabaseImpl } from '@firebase/database-compat/standalone'; import { App } from '../app'; @@ -225,7 +227,10 @@ class DatabaseRulesClient { return this.httpClient.send(req) .then((resp) => { if (!resp.text) { - throw new FirebaseAppError(AppErrorCodes.INTERNAL_ERROR, 'HTTP response missing data.'); + throw new FirebaseAppError({ + code: AppErrorCode.INTERNAL_ERROR, + message: 'HTTP response missing data.' + }); } return resp.text; }) @@ -294,8 +299,10 @@ class DatabaseRulesClient { private handleError(err: Error): Error { if (err instanceof RequestResponseError) { return new FirebaseDatabaseError({ - code: AppErrorCodes.INTERNAL_ERROR, + code: AppErrorCode.INTERNAL_ERROR, message: this.getErrorMessage(err), + httpResponse: toHttpResponse(err.response), + cause: err, }); } return err; @@ -304,7 +311,7 @@ class DatabaseRulesClient { private getErrorMessage(err: RequestResponseError): string { const intro = 'Error while accessing security rules'; try { - const body: { error?: string } = err.response.data; + const body: { error?: string; } = err.response.data; if (body && body.error) { return `${intro}: ${body.error.trim()}`; } diff --git a/src/database/error.ts b/src/database/error.ts new file mode 100644 index 0000000000..15bbd45bea --- /dev/null +++ b/src/database/error.ts @@ -0,0 +1,32 @@ +/*! + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PrefixedFirebaseError, ErrorInfo } from '../utils/error'; + +/** + * Firebase Database error code structure. This extends PrefixedFirebaseError. + */ +export class FirebaseDatabaseError extends PrefixedFirebaseError { + /** + * @param info - The error code info. + * @param message - The error message. This will override the default + * message if provided. + */ + constructor(info: ErrorInfo, message?: string) { + // Override default message if custom message provided. + super('database', info.code, message || info.message, info.httpResponse, info.cause); + } +} diff --git a/src/database/index.ts b/src/database/index.ts index c078893d2c..747633f819 100644 --- a/src/database/index.ts +++ b/src/database/index.ts @@ -126,4 +126,4 @@ function getDatabaseInstance(options: { url?: string; app?: App }): Database { return dbService.getDatabase(options.url); } -export { FirebaseDatabaseError } from '../utils/error'; +export { FirebaseDatabaseError } from './error'; diff --git a/src/eventarc/error.ts b/src/eventarc/error.ts new file mode 100644 index 0000000000..6d938af093 --- /dev/null +++ b/src/eventarc/error.ts @@ -0,0 +1,43 @@ +/*! + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PrefixedFirebaseError, ErrorInfo } from '../utils/error'; + +/** + * The constant mapping for valid Eventarc client error codes. + */ +export const EventarcErrorCode = { + UNKNOWN_ERROR: 'unknown-error', + INVALID_ARGUMENT: 'invalid-argument', +} as const; + +/** + * The type definition for valid Eventarc client error codes. + */ +export type EventarcErrorCode = typeof EventarcErrorCode[keyof typeof EventarcErrorCode]; + +/** + * Firebase Eventarc error code structure. This extends PrefixedFirebaseError. + */ +export class FirebaseEventarcError extends PrefixedFirebaseError { + /** + * @param info - The error code info. + * @param message - The error message. If provided, this will override the default message. + */ + constructor(info: ErrorInfo, message?: string) { + super('eventarc', info.code, message || info.message, info.httpResponse, info.cause); + } +} diff --git a/src/eventarc/eventarc-client-internal.ts b/src/eventarc/eventarc-client-internal.ts index e4e5bda0a4..4c517343b1 100644 --- a/src/eventarc/eventarc-client-internal.ts +++ b/src/eventarc/eventarc-client-internal.ts @@ -16,7 +16,8 @@ */ import * as validator from '../utils/validator'; -import { FirebaseEventarcError, toCloudEventProtoFormat } from './eventarc-utils'; +import { toCloudEventProtoFormat } from './eventarc-utils'; +import { FirebaseEventarcError } from './error'; import { App } from '../app'; import { Channel } from './eventarc'; import { @@ -24,7 +25,7 @@ import { } from '../utils/api-request'; import { FirebaseApp } from '../app/firebase-app'; import * as utils from '../utils'; -import { PrefixedFirebaseError } from '../utils/error'; +import { PrefixedFirebaseError, toHttpResponse } from '../utils/error'; import { CloudEvent } from './cloudevent'; const EVENTARC_API = 'https://eventarcpublishing.googleapis.com/v1'; @@ -46,9 +47,10 @@ export class EventarcApiClient { constructor(private readonly app: App, private readonly channel: Channel) { if (!validator.isNonNullObject(app) || !('options' in app)) { - throw new FirebaseEventarcError( - 'invalid-argument', - 'First argument passed to Channel() must be a valid Eventarc service instance.'); + throw new FirebaseEventarcError({ + code: 'invalid-argument', + message: 'First argument passed to Channel() must be a valid Eventarc service instance.' + }); } this.httpClient = new AuthorizedHttpClient(app as FirebaseApp); this.resolvedChannelName = this.resolveChannelName(channel.name); @@ -61,11 +63,12 @@ export class EventarcApiClient { return utils.findProjectId(this.app) .then((projectId) => { if (!validator.isNonEmptyString(projectId)) { - throw new FirebaseEventarcError( - 'unknown-error', - 'Failed to determine project ID. Initialize the ' - + 'SDK with service account credentials or set project ID as an app option. ' - + 'Alternatively, set the GOOGLE_CLOUD_PROJECT environment variable.'); + throw new FirebaseEventarcError({ + code: 'unknown-error', + message: 'Failed to determine project ID. Initialize the ' + + 'SDK with service account credentials or set project ID as an app option. ' + + 'Alternatively, set the GOOGLE_CLOUD_PROJECT environment variable.' + }); } this.projectId = projectId; return projectId; @@ -123,9 +126,12 @@ export class EventarcApiClient { } const response = err.response; - return new FirebaseEventarcError( - 'unknown-error', - `Unexpected response with status: ${response.status} and body: ${response.text}`); + return new FirebaseEventarcError({ + code: 'unknown-error', + message: `Unexpected response with status: ${response.status}.`, + httpResponse: toHttpResponse(response), + cause: err, + }); } private resolveChannelName(name: string): Promise { @@ -136,7 +142,10 @@ export class EventarcApiClient { } else { const match = CHANNEL_NAME_REGEX.exec(name); if (match === null || match.length < 4) { - throw new FirebaseEventarcError('invalid-argument', 'Invalid channel name format.'); + throw new FirebaseEventarcError({ + code: 'invalid-argument', + message: 'Invalid channel name format.' + }); } const projectId = match[2]; const location = match[3]; diff --git a/src/eventarc/eventarc-utils.ts b/src/eventarc/eventarc-utils.ts index 3737e5a906..a57894157a 100644 --- a/src/eventarc/eventarc-utils.ts +++ b/src/eventarc/eventarc-utils.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { PrefixedFirebaseError } from '../utils/error'; +import { FirebaseEventarcError } from './error'; import { CloudEvent } from './cloudevent'; import { v4 as uuid } from 'uuid'; import * as validator from '../utils/validator'; @@ -25,28 +25,19 @@ import * as validator from '../utils/validator'; const TOP_LEVEL_CE_ATTRS: string[] = ['id', 'type', 'specversion', 'source', 'data', 'time', 'datacontenttype', 'subject']; -export type EventarcErrorCode = 'unknown-error' | 'invalid-argument' - -/** - * Firebase Eventarc error code structure. This extends PrefixedFirebaseError. - * - * @param code - The error code. - * @param message - The error message. - * @constructor - */ -export class FirebaseEventarcError extends PrefixedFirebaseError { - constructor(code: EventarcErrorCode, message: string) { - super('eventarc', code, message); - } -} - export function toCloudEventProtoFormat(ce: CloudEvent): any { const source = ce.source ?? process.env.EVENTARC_CLOUD_EVENT_SOURCE; if (typeof source === 'undefined' || !validator.isNonEmptyString(source)) { - throw new FirebaseEventarcError('invalid-argument', "CloudEvent 'source' is required."); + throw new FirebaseEventarcError({ + code: 'invalid-argument', + message: "CloudEvent 'source' is required." + }); } if (!validator.isNonEmptyString(ce.type)) { - throw new FirebaseEventarcError('invalid-argument', "CloudEvent 'type' is required."); + throw new FirebaseEventarcError({ + code: 'invalid-argument', + message: "CloudEvent 'type' is required." + }); } const out: Record = { '@type': 'type.googleapis.com/io.cloudevents.v1.CloudEvent', @@ -58,8 +49,10 @@ export function toCloudEventProtoFormat(ce: CloudEvent): any { if (typeof ce.time !== 'undefined') { if (!validator.isISODateString(ce.time)) { - throw new FirebaseEventarcError( - 'invalid-argument', "CloudEvent 'tyme' must be in ISO date format."); + throw new FirebaseEventarcError({ + code: 'invalid-argument', + message: "CloudEvent 'tyme' must be in ISO date format." + }); } setAttribute(out, 'time', { 'ceTimestamp': ce.time @@ -71,9 +64,10 @@ export function toCloudEventProtoFormat(ce: CloudEvent): any { } if (typeof ce.datacontenttype !== 'undefined') { if (!validator.isNonEmptyString(ce.datacontenttype)) { - throw new FirebaseEventarcError( - 'invalid-argument', - "CloudEvent 'datacontenttype' if specified must be non-empty string."); + throw new FirebaseEventarcError({ + code: 'invalid-argument', + message: "CloudEvent 'datacontenttype' if specified must be non-empty string." + }); } setAttribute(out, 'datacontenttype', { 'ceString': ce.datacontenttype @@ -81,9 +75,10 @@ export function toCloudEventProtoFormat(ce: CloudEvent): any { } if (ce.subject) { if (!validator.isNonEmptyString(ce.subject)) { - throw new FirebaseEventarcError( - 'invalid-argument', - "CloudEvent 'subject' if specified must be non-empty string."); + throw new FirebaseEventarcError({ + code: 'invalid-argument', + message: "CloudEvent 'subject' if specified must be non-empty string." + }); } setAttribute(out, 'subject', { 'ceString': ce.subject @@ -91,7 +86,10 @@ export function toCloudEventProtoFormat(ce: CloudEvent): any { } if (typeof ce.data === 'undefined') { - throw new FirebaseEventarcError('invalid-argument', "CloudEvent 'data' is required."); + throw new FirebaseEventarcError({ + code: 'invalid-argument', + message: "CloudEvent 'data' is required." + }); } if (validator.isObject(ce.data)) { out['textData'] = JSON.stringify(ce.data); @@ -108,9 +106,10 @@ export function toCloudEventProtoFormat(ce: CloudEvent): any { }); } } else { - throw new FirebaseEventarcError( - 'invalid-argument', - `CloudEvent 'data' must be string or an object (which are converted to JSON), got '${typeof ce.data}'.`); + throw new FirebaseEventarcError({ + code: 'invalid-argument', + message: `CloudEvent 'data' must be string or an object (which are converted to JSON), got '${typeof ce.data}'.` + }); } for (const attr in ce) { @@ -118,9 +117,10 @@ export function toCloudEventProtoFormat(ce: CloudEvent): any { continue; } if (!validator.isNonEmptyString(ce[attr])) { - throw new FirebaseEventarcError( - 'invalid-argument', - `CloudEvent extension attributes ('${attr}') must be string.`); + throw new FirebaseEventarcError({ + code: 'invalid-argument', + message: `CloudEvent extension attributes ('${attr}') must be string.` + }); } setAttribute(out, attr, { 'ceString': ce[attr] diff --git a/src/eventarc/eventarc.ts b/src/eventarc/eventarc.ts index c27e0b3acf..6276248ccb 100644 --- a/src/eventarc/eventarc.ts +++ b/src/eventarc/eventarc.ts @@ -17,7 +17,7 @@ import { App } from '../app'; import * as validator from '../utils/validator'; -import { FirebaseEventarcError } from './eventarc-utils'; +import { FirebaseEventarcError } from './error'; import { CloudEvent } from './cloudevent'; import { EventarcApiClient } from './eventarc-client-internal'; @@ -45,10 +45,10 @@ export class Eventarc { */ constructor(app: App) { if (!validator.isNonNullObject(app) || !('options' in app)) { - throw new FirebaseEventarcError( - 'invalid-argument', - 'First argument passed to Eventarc() must be a valid Firebase app instance.', - ); + throw new FirebaseEventarcError({ + code: 'invalid-argument', + message: 'First argument passed to Eventarc() must be a valid Firebase app instance.' + }); } this.appInternal = app; @@ -117,10 +117,10 @@ export class Eventarc { } else if (validator.isArray(opts?.allowedEventTypes)) { allowedEventTypes = opts?.allowedEventTypes as string[]; } else if (typeof opts?.allowedEventTypes !== 'undefined') { - throw new FirebaseEventarcError( - 'invalid-argument', - 'AllowedEventTypes must be either an array of strings or a comma separated string.', - ); + throw new FirebaseEventarcError({ + code: 'invalid-argument', + message: 'AllowedEventTypes must be either an array of strings or a comma separated string.' + }); } return new Channel(this, channel, allowedEventTypes); } @@ -145,15 +145,16 @@ export class Channel { */ constructor(eventarc: Eventarc, name: string, allowedEventTypes?: string[]) { if (!validator.isNonNullObject(eventarc)) { - throw new FirebaseEventarcError( - 'invalid-argument', - 'First argument passed to Channel() must be a valid Eventarc service instance.', - ); + throw new FirebaseEventarcError({ + code: 'invalid-argument', + message: 'First argument passed to Channel() must be a valid Eventarc service instance.' + }); } if (!validator.isNonEmptyString(name)) { - throw new FirebaseEventarcError( - 'invalid-argument', 'name is required.', - ); + throw new FirebaseEventarcError({ + code: 'invalid-argument', + message: 'name is required.' + }); } this.nameInternal = name; diff --git a/src/eventarc/index.ts b/src/eventarc/index.ts index f3bb023947..6e8a18720a 100644 --- a/src/eventarc/index.ts +++ b/src/eventarc/index.ts @@ -63,3 +63,5 @@ export function getEventarc(app?: App): Eventarc { const firebaseApp: FirebaseApp = app as FirebaseApp; return firebaseApp.getOrInitService('eventarc', (app) => new Eventarc(app)); } + +export { FirebaseEventarcError, EventarcErrorCode } from './error'; \ No newline at end of file diff --git a/src/extensions/error.ts b/src/extensions/error.ts new file mode 100644 index 0000000000..14b7f024ac --- /dev/null +++ b/src/extensions/error.ts @@ -0,0 +1,46 @@ +/*! + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PrefixedFirebaseError, ErrorInfo } from '../utils/error'; + +/** + * The constant mapping for valid Extensions client error codes. + */ +export const ExtensionsErrorCode = { + INVALID_ARGUMENT: 'invalid-argument', + NOT_FOUND: 'not-found', + FORBIDDEN: 'forbidden', + INTERNAL_ERROR: 'internal-error', + UNKNOWN_ERROR: 'unknown-error', +} as const; + +/** + * The type definition for valid Extensions client error codes. + */ +export type ExtensionsErrorCode = typeof ExtensionsErrorCode[keyof typeof ExtensionsErrorCode]; + +/** + * Firebase Extensions error code structure. This extends PrefixedFirebaseError. + */ +export class FirebaseExtensionsError extends PrefixedFirebaseError { + /** + * @param info - The error code info. + * @param message - The error message. If provided, this will override the default message. + */ + constructor(info: ErrorInfo, message?: string) { + super('Extensions', info.code, message || info.message, info.httpResponse, info.cause); + } +} diff --git a/src/extensions/extensions-api-client-internal.ts b/src/extensions/extensions-api-client-internal.ts index a90c5b34fd..e89915f0bb 100644 --- a/src/extensions/extensions-api-client-internal.ts +++ b/src/extensions/extensions-api-client-internal.ts @@ -18,7 +18,9 @@ import { App } from '../app'; import { FirebaseApp } from '../app/firebase-app'; import { AuthorizedHttpClient, HttpClient, RequestResponseError, HttpRequestConfig } from '../utils/api-request'; -import { FirebaseAppError, PrefixedFirebaseError } from '../utils/error'; +import { PrefixedFirebaseError, toHttpResponse } from '../utils/error'; +import { FirebaseExtensionsError } from './error'; +import { FirebaseAppError } from '../app/error'; import * as validator from '../utils/validator'; import * as utils from '../utils'; @@ -39,9 +41,10 @@ export class ExtensionsApiClient { constructor(private readonly app: App) { if (!validator.isNonNullObject(app) || !('options' in app)) { - throw new FirebaseAppError( - 'invalid-argument', - 'First argument passed to getExtensions() must be a valid Firebase app instance.'); + throw new FirebaseAppError({ + code: 'invalid-argument', + message: 'First argument passed to getExtensions() must be a valid Firebase app instance.' + }); } this.httpClient = new AuthorizedHttpClient(this.app as FirebaseApp); } @@ -83,21 +86,44 @@ export class ExtensionsApiClient { const response = err.response; if (!response?.isJson()) { - return new FirebaseExtensionsError( - 'unknown-error', - `Unexpected response with status: ${response.status} and body: ${response.text}`); + return new FirebaseExtensionsError({ + code: 'unknown-error', + message: `Unexpected response with status: ${response.status} and body: ${response.text}`, + httpResponse: toHttpResponse(response), + cause: err + }); } const error = response.data?.error; - const message = error?.message || `Unknown server error: ${response.text}`; + const message = error?.message || 'Unknown server error'; switch (error.code) { case 403: - return new FirebaseExtensionsError('forbidden', message); + return new FirebaseExtensionsError({ + code: 'forbidden', + message, + httpResponse: toHttpResponse(response), + cause: err, + }); case 404: - return new FirebaseExtensionsError('not-found', message); + return new FirebaseExtensionsError({ + code: 'not-found', + message, + httpResponse: toHttpResponse(response), + cause: err, + }); case 500: - return new FirebaseExtensionsError('internal-error', message); + return new FirebaseExtensionsError({ + code: 'internal-error', + message, + httpResponse: toHttpResponse(response), + cause: err, + }); } - return new FirebaseExtensionsError('unknown-error', message); + return new FirebaseExtensionsError({ + code: 'unknown-error', + message, + httpResponse: toHttpResponse(response), + cause: err, + }); } } @@ -128,23 +154,3 @@ type State = 'STATE_UNSPECIFIED' | 'PROCESSING_COMPLETE' | 'PROCESSING_WARNING' | 'PROCESSING_FAILED'; - -type ExtensionsErrorCode = 'invalid-argument' | 'not-found' | 'forbidden' | 'internal-error' | 'unknown-error'; -/** - * Firebase Extensions error code structure. This extends PrefixedFirebaseError. - * - * @param code - The error code. - * @param message - The error message. - * @constructor - */ -export class FirebaseExtensionsError extends PrefixedFirebaseError { - constructor(code: ExtensionsErrorCode, message: string) { - super('Extensions', code, message); - - /* tslint:disable:max-line-length */ - // Set the prototype explicitly. See the following link for more details: - // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work - /* tslint:enable:max-line-length */ - (this as any).__proto__ = FirebaseExtensionsError.prototype; - } -} diff --git a/src/extensions/extensions.ts b/src/extensions/extensions.ts index f148d68dcb..3c99bc62a5 100644 --- a/src/extensions/extensions.ts +++ b/src/extensions/extensions.ts @@ -17,7 +17,8 @@ import { App } from '../app'; import { SettableProcessingState } from './extensions-api'; -import { ExtensionsApiClient, FirebaseExtensionsError } from './extensions-api-client-internal'; +import { ExtensionsApiClient } from './extensions-api-client-internal'; +import { FirebaseExtensionsError } from './error'; import * as validator from '../utils/validator'; /** @@ -62,16 +63,17 @@ export class Runtime { constructor(client: ExtensionsApiClient) { this.projectId = this.getProjectId(); if (!validator.isNonEmptyString(process.env['EXT_INSTANCE_ID'])) { - throw new FirebaseExtensionsError( - 'invalid-argument', - 'Runtime is only available from within a running Extension instance.' - ); + throw new FirebaseExtensionsError({ + code: 'invalid-argument', + message: 'Runtime is only available from within a running Extension instance.' + }); } this.extensionInstanceId = process.env['EXT_INSTANCE_ID']; if (!validator.isNonNullObject(client) || !('updateRuntimeData' in client)) { - throw new FirebaseExtensionsError( - 'invalid-argument', - 'Must provide a valid ExtensionsApiClient instance to create a new Runtime.'); + throw new FirebaseExtensionsError({ + code: 'invalid-argument', + message: 'Must provide a valid ExtensionsApiClient instance to create a new Runtime.' + }); } this.client = client; } @@ -118,10 +120,10 @@ export class Runtime { */ public async setFatalError(errorMessage: string): Promise { if (!validator.isNonEmptyString(errorMessage)) { - throw new FirebaseExtensionsError( - 'invalid-argument', - 'errorMessage must not be empty' - ); + throw new FirebaseExtensionsError({ + code: 'invalid-argument', + message: 'errorMessage must not be empty' + }); } await this.client.updateRuntimeData( this.projectId, @@ -137,10 +139,10 @@ export class Runtime { private getProjectId(): string { const projectId = process.env['PROJECT_ID']; if (!validator.isNonEmptyString(projectId)) { - throw new FirebaseExtensionsError( - 'invalid-argument', - 'PROJECT_ID must not be undefined in Extensions runtime environment' - ); + throw new FirebaseExtensionsError({ + code: 'invalid-argument', + message: 'PROJECT_ID must not be undefined in Extensions runtime environment' + }); } return projectId; } diff --git a/src/extensions/index.ts b/src/extensions/index.ts index e96401a774..04955071db 100644 --- a/src/extensions/index.ts +++ b/src/extensions/index.ts @@ -62,3 +62,5 @@ export function getExtensions(app?: App): Extensions { const firebaseApp: FirebaseApp = app as FirebaseApp; return firebaseApp.getOrInitService('extensions', (app) => new Extensions(app)); } + +export { FirebaseExtensionsError, ExtensionsErrorCode } from './error'; \ No newline at end of file diff --git a/src/firebase-namespace-api.ts b/src/firebase-namespace-api.ts index 3808876910..0d908f4915 100644 --- a/src/firebase-namespace-api.ts +++ b/src/firebase-namespace-api.ts @@ -29,7 +29,7 @@ import { storage } from './storage/storage-namespace'; import { App as AppCore, AppOptions } from './app/index'; -export { AppOptions, FirebaseError, FirebaseArrayIndexError } from './app/index'; +export { AppOptions, FirebaseError, FirebaseArrayIndexError, ErrorInfo, HttpResponse } from './app/index'; // eslint-disable-next-line @typescript-eslint/no-namespace export namespace app { diff --git a/src/firestore/error.ts b/src/firestore/error.ts new file mode 100644 index 0000000000..6207cbda31 --- /dev/null +++ b/src/firestore/error.ts @@ -0,0 +1,32 @@ +/*! + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PrefixedFirebaseError, ErrorInfo } from '../utils/error'; + +/** + * Firebase Firestore error code structure. This extends PrefixedFirebaseError. + */ +export class FirebaseFirestoreError extends PrefixedFirebaseError { + /** + * @param info - The error code info. + * @param message - The error message. This will override the default + * message if provided. + */ + constructor(info: ErrorInfo, message?: string) { + // Override default message if custom message provided. + super('firestore', info.code, message || info.message, info.httpResponse, info.cause); + } +} diff --git a/src/firestore/firestore-internal.ts b/src/firestore/firestore-internal.ts index 95c6e74414..3642521943 100644 --- a/src/firestore/firestore-internal.ts +++ b/src/firestore/firestore-internal.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { FirebaseFirestoreError } from '../utils/error'; +import { FirebaseFirestoreError } from './error'; import { ServiceAccountCredential, isApplicationDefault } from '../app/credential-internal'; import { Firestore, Settings } from '@google-cloud/firestore'; @@ -165,8 +165,8 @@ function initFirestore(app: App, databaseId: string, firestoreSettings?: Firesto throw new FirebaseFirestoreError({ code: 'missing-dependencies', message: 'Failed to import the Cloud Firestore client library for Node.js. ' - + 'Make sure to install the "@google-cloud/firestore" npm package. ' - + `Original error: ${err}`, + + 'Make sure to install the "@google-cloud/firestore" npm package.', + cause: err as Error, }); } diff --git a/src/firestore/index.ts b/src/firestore/index.ts index c41fd5ea1a..5fe0c3f803 100644 --- a/src/firestore/index.ts +++ b/src/firestore/index.ts @@ -221,4 +221,4 @@ export function initializeFirestore( return firestoreService.initializeDatabase(databaseId, settings); } -export { FirebaseFirestoreError } from '../utils/error'; +export { FirebaseFirestoreError } from './error'; diff --git a/src/functions/error.ts b/src/functions/error.ts new file mode 100644 index 0000000000..a59e00d3d1 --- /dev/null +++ b/src/functions/error.ts @@ -0,0 +1,64 @@ +/*! + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PrefixedFirebaseError, ErrorInfo } from '../utils/error'; + +/** @const {Record} Functions server to client error code mapping. */ +export const FUNCTIONS_ERROR_CODE_MAPPING: Record = { + ABORTED: 'aborted', + INVALID_ARGUMENT: 'invalid-argument', + INVALID_CREDENTIAL: 'invalid-credential', + INTERNAL: 'internal-error', + FAILED_PRECONDITION: 'failed-precondition', + PERMISSION_DENIED: 'permission-denied', + UNAUTHENTICATED: 'unauthenticated', + NOT_FOUND: 'not-found', + UNKNOWN: 'unknown-error', +}; + +/** + * The constant mapping for valid Functions client error codes. + */ +export const FunctionsErrorCode = { + ABORTED: 'aborted', + INVALID_ARGUMENT: 'invalid-argument', + INVALID_CREDENTIAL: 'invalid-credential', + INTERNAL_ERROR: 'internal-error', + FAILED_PRECONDITION: 'failed-precondition', + PERMISSION_DENIED: 'permission-denied', + UNAUTHENTICATED: 'unauthenticated', + NOT_FOUND: 'not-found', + UNKNOWN_ERROR: 'unknown-error', + TASK_ALREADY_EXISTS: 'task-already-exists', +} as const; + +/** + * The type definition for valid Functions client error codes. + */ +export type FunctionsErrorCode = typeof FunctionsErrorCode[keyof typeof FunctionsErrorCode]; + +/** + * Firebase Functions error code structure. This extends PrefixedFirebaseError. + */ +export class FirebaseFunctionsError extends PrefixedFirebaseError { + /** + * @param info - The error code info. + * @param message - The error message. If provided, this will override the default message. + */ + constructor(info: ErrorInfo, message?: string) { + super('functions', info.code, message || info.message, info.httpResponse, info.cause); + } +} diff --git a/src/functions/functions-api-client-internal.ts b/src/functions/functions-api-client-internal.ts index d68a0b1c72..34e55e5ddc 100644 --- a/src/functions/functions-api-client-internal.ts +++ b/src/functions/functions-api-client-internal.ts @@ -20,7 +20,8 @@ import { FirebaseApp } from '../app/firebase-app'; import { HttpRequestConfig, HttpClient, RequestResponseError, AuthorizedHttpClient } from '../utils/api-request'; -import { PrefixedFirebaseError } from '../utils/error'; +import { PrefixedFirebaseError, toHttpResponse } from '../utils/error'; +import { FirebaseFunctionsError, FunctionsErrorCode, FUNCTIONS_ERROR_CODE_MAPPING } from './error'; import * as utils from '../utils/index'; import * as validator from '../utils/validator'; import { TaskOptions } from './functions-api'; @@ -50,9 +51,10 @@ export class FunctionsApiClient { constructor(private readonly app: App) { if (!validator.isNonNullObject(app) || !('options' in app)) { - throw new FirebaseFunctionsError( - 'invalid-argument', - 'First argument passed to getFunctions() must be a valid Firebase app instance.'); + throw new FirebaseFunctionsError({ + code: 'invalid-argument', + message: 'First argument passed to getFunctions() must be a valid Firebase app instance.' + }); } this.httpClient = new FunctionsHttpClient(app as FirebaseApp); } @@ -65,27 +67,36 @@ export class FunctionsApiClient { */ public async delete(id: string, functionName: string, extensionId?: string): Promise { if (!validator.isNonEmptyString(functionName)) { - throw new FirebaseFunctionsError( - 'invalid-argument', 'Function name must be a non empty string'); + throw new FirebaseFunctionsError({ + code: 'invalid-argument', + message: 'Function name must be a non empty string' + }); } if (!validator.isTaskId(id)) { - throw new FirebaseFunctionsError( - 'invalid-argument', 'id can contain only letters ([A-Za-z]), numbers ([0-9]), ' - + 'hyphens (-), or underscores (_). The maximum length is 500 characters.'); + throw new FirebaseFunctionsError({ + code: 'invalid-argument', + message: 'id can contain only letters ([A-Za-z]), numbers ([0-9]), ' + + 'hyphens (-), or underscores (_). The maximum length is 500 characters.' + }); } let resources: utils.ParsedResource; try { resources = utils.parseResourceName(functionName, 'functions'); } catch (err) { - throw new FirebaseFunctionsError( - 'invalid-argument', 'Function name must be a single string or a qualified resource name'); + throw new FirebaseFunctionsError({ + code: 'invalid-argument', + message: 'Function name must be a single string or a qualified resource name', + cause: err as Error, + }); } resources.projectId = resources.projectId || await this.getProjectId(); resources.locationId = resources.locationId || DEFAULT_LOCATION; if (!validator.isNonEmptyString(resources.resourceId)) { - throw new FirebaseFunctionsError( - 'invalid-argument', 'No valid function name specified to enqueue tasks for.'); + throw new FirebaseFunctionsError({ + code: 'invalid-argument', + message: 'No valid function name specified to enqueue tasks for.' + }); } if (typeof extensionId !== 'undefined' && validator.isNonEmptyString(extensionId)) { resources.resourceId = `ext-${extensionId}-${resources.resourceId}`; @@ -123,22 +134,29 @@ export class FunctionsApiClient { */ public async enqueue(data: any, functionName: string, extensionId?: string, opts?: TaskOptions): Promise { if (!validator.isNonEmptyString(functionName)) { - throw new FirebaseFunctionsError( - 'invalid-argument', 'Function name must be a non empty string'); + throw new FirebaseFunctionsError({ + code: 'invalid-argument', + message: 'Function name must be a non empty string' + }); } let resources: utils.ParsedResource; try { resources = utils.parseResourceName(functionName, 'functions'); } catch (err) { - throw new FirebaseFunctionsError( - 'invalid-argument', 'Function name must be a single string or a qualified resource name'); + throw new FirebaseFunctionsError({ + code: 'invalid-argument', + message: 'Function name must be a single string or a qualified resource name', + cause: err as Error, + }); } resources.projectId = resources.projectId || await this.getProjectId(); resources.locationId = resources.locationId || DEFAULT_LOCATION; if (!validator.isNonEmptyString(resources.resourceId)) { - throw new FirebaseFunctionsError( - 'invalid-argument', 'No valid function name specified to enqueue tasks for.'); + throw new FirebaseFunctionsError({ + code: 'invalid-argument', + message: 'No valid function name specified to enqueue tasks for.' + }); } if (typeof extensionId !== 'undefined' && validator.isNonEmptyString(extensionId)) { resources.resourceId = `ext-${extensionId}-${resources.resourceId}`; @@ -163,7 +181,12 @@ export class FunctionsApiClient { } catch (err: unknown) { if (err instanceof RequestResponseError) { if (err.response.status === 409) { - throw new FirebaseFunctionsError('task-already-exists', `A task with ID ${opts?.id} already exists`); + throw new FirebaseFunctionsError({ + code: 'task-already-exists', + message: `A task with ID ${opts?.id} already exists`, + httpResponse: toHttpResponse(err.response), + cause: err, + }); } else { throw this.toFirebaseError(err); } @@ -206,11 +229,12 @@ export class FunctionsApiClient { return utils.findProjectId(this.app) .then((projectId) => { if (!validator.isNonEmptyString(projectId)) { - throw new FirebaseFunctionsError( - 'unknown-error', - 'Failed to determine project ID. Initialize the ' - + 'SDK with service account credentials or set project ID as an app option. ' - + 'Alternatively, set the GOOGLE_CLOUD_PROJECT environment variable.'); + throw new FirebaseFunctionsError({ + code: 'unknown-error', + message: 'Failed to determine project ID. Initialize the ' + + 'SDK with service account credentials or set project ID as an app option. ' + + 'Alternatively, set the GOOGLE_CLOUD_PROJECT environment variable.' + }); } this.projectId = projectId; return projectId; @@ -224,10 +248,11 @@ export class FunctionsApiClient { return utils.findServiceAccountEmail(this.app) .then((accountId) => { if (!validator.isNonEmptyString(accountId)) { - throw new FirebaseFunctionsError( - 'unknown-error', - 'Failed to determine service account. Initialize the ' - + 'SDK with service account credentials or set service account ID as an app option.'); + throw new FirebaseFunctionsError({ + code: 'unknown-error', + message: 'Failed to determine service account. Initialize the ' + + 'SDK with service account credentials or set service account ID as an app option.' + }); } this.accountId = accountId; return accountId; @@ -251,25 +276,32 @@ export class FunctionsApiClient { if (typeof opts !== 'undefined') { if (!validator.isNonNullObject(opts)) { - throw new FirebaseFunctionsError( - 'invalid-argument', 'TaskOptions must be a non-null object'); + throw new FirebaseFunctionsError({ + code: 'invalid-argument', + message: 'TaskOptions must be a non-null object' + }); } if ('scheduleTime' in opts && 'scheduleDelaySeconds' in opts) { - throw new FirebaseFunctionsError( - 'invalid-argument', 'Both scheduleTime and scheduleDelaySeconds are provided. ' - + 'Only one value should be set.'); + throw new FirebaseFunctionsError({ + code: 'invalid-argument', + message: 'Both scheduleTime and scheduleDelaySeconds are provided. Only one value should be set.' + }); } if ('scheduleTime' in opts && typeof opts.scheduleTime !== 'undefined') { if (!(opts.scheduleTime instanceof Date)) { - throw new FirebaseFunctionsError( - 'invalid-argument', 'scheduleTime must be a valid Date object.'); + throw new FirebaseFunctionsError({ + code: 'invalid-argument', + message: 'scheduleTime must be a valid Date object.' + }); } task.scheduleTime = opts.scheduleTime.toISOString(); } if ('scheduleDelaySeconds' in opts && typeof opts.scheduleDelaySeconds !== 'undefined') { if (!validator.isNumber(opts.scheduleDelaySeconds) || opts.scheduleDelaySeconds < 0) { - throw new FirebaseFunctionsError( - 'invalid-argument', 'scheduleDelaySeconds must be a non-negative duration in seconds.'); + throw new FirebaseFunctionsError({ + code: 'invalid-argument', + message: 'scheduleDelaySeconds must be a non-negative duration in seconds.' + }); } const date = new Date(); date.setSeconds(date.getSeconds() + opts.scheduleDelaySeconds); @@ -278,17 +310,21 @@ export class FunctionsApiClient { if (typeof opts.dispatchDeadlineSeconds !== 'undefined') { if (!validator.isNumber(opts.dispatchDeadlineSeconds) || opts.dispatchDeadlineSeconds < 15 || opts.dispatchDeadlineSeconds > 1800) { - throw new FirebaseFunctionsError( - 'invalid-argument', 'dispatchDeadlineSeconds must be a non-negative duration in seconds ' - + 'and must be in the range of 15s to 30 mins.'); + throw new FirebaseFunctionsError({ + code: 'invalid-argument', + message: 'dispatchDeadlineSeconds must be a non-negative duration in seconds ' + + 'and must be in the range of 15s to 30 mins.' + }); } task.dispatchDeadline = `${opts.dispatchDeadlineSeconds}s`; } if ('id' in opts && typeof opts.id !== 'undefined') { if (!validator.isTaskId(opts.id)) { - throw new FirebaseFunctionsError( - 'invalid-argument', 'id can contain only letters ([A-Za-z]), numbers ([0-9]), ' - + 'hyphens (-), or underscores (_). The maximum length is 500 characters.'); + throw new FirebaseFunctionsError({ + code: 'invalid-argument', + message: 'id can contain only letters ([A-Za-z]), numbers ([0-9]), ' + + 'hyphens (-), or underscores (_). The maximum length is 500 characters.' + }); } const resourcePath = utils.formatString(CLOUD_TASKS_API_RESOURCE_PATH, { projectId: resources.projectId, @@ -299,8 +335,10 @@ export class FunctionsApiClient { } if (typeof opts.uri !== 'undefined') { if (!validator.isURL(opts.uri)) { - throw new FirebaseFunctionsError( - 'invalid-argument', 'uri must be a valid URL string.'); + throw new FirebaseFunctionsError({ + code: 'invalid-argument', + message: 'uri must be a valid URL string.' + }); } task.httpRequest.url = opts.uri; } @@ -347,18 +385,21 @@ export class FunctionsApiClient { const response = err.response; if (!response.isJson()) { - return new FirebaseFunctionsError( - 'unknown-error', - `Unexpected response with status: ${response.status} and body: ${response.text}`); + return new FirebaseFunctionsError({ + code: 'unknown-error', + message: `Unexpected response with status: ${response.status} and body: ${response.text}`, + httpResponse: toHttpResponse(response), + cause: err + }); } - const error: Error = (response.data as ErrorResponse).error || {}; + const error: FunctionsApiError = (response.data as ErrorResponse).error || {}; let code: FunctionsErrorCode = 'unknown-error'; if (error.status && error.status in FUNCTIONS_ERROR_CODE_MAPPING) { code = FUNCTIONS_ERROR_CODE_MAPPING[error.status]; } - const message = error.message || `Unknown server error: ${response.text}`; - return new FirebaseFunctionsError(code, message); + const message = error.message || 'Unknown server error'; + return new FirebaseFunctionsError({ code, message, httpResponse: toHttpResponse(response), cause: err }); } } @@ -376,10 +417,10 @@ class FunctionsHttpClient extends AuthorizedHttpClient { } interface ErrorResponse { - error?: Error; + error?: FunctionsApiError; } -interface Error { +interface FunctionsApiError { code?: number; message?: string; status?: string; @@ -407,49 +448,6 @@ export interface Task { }; } -export const FUNCTIONS_ERROR_CODE_MAPPING: { [key: string]: FunctionsErrorCode } = { - ABORTED: 'aborted', - INVALID_ARGUMENT: 'invalid-argument', - INVALID_CREDENTIAL: 'invalid-credential', - INTERNAL: 'internal-error', - FAILED_PRECONDITION: 'failed-precondition', - PERMISSION_DENIED: 'permission-denied', - UNAUTHENTICATED: 'unauthenticated', - NOT_FOUND: 'not-found', - UNKNOWN: 'unknown-error', -}; - -export type FunctionsErrorCode = - 'aborted' - | 'invalid-argument' - | 'invalid-credential' - | 'internal-error' - | 'failed-precondition' - | 'permission-denied' - | 'unauthenticated' - | 'not-found' - | 'unknown-error' - | 'task-already-exists'; - -/** - * Firebase Functions error code structure. This extends PrefixedFirebaseError. - * - * @param code - The error code. - * @param message - The error message. - * @constructor - */ -export class FirebaseFunctionsError extends PrefixedFirebaseError { - constructor(code: FunctionsErrorCode, message: string) { - super('functions', code, message); - - /* tslint:disable:max-line-length */ - // Set the prototype explicitly. See the following link for more details: - // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work - /* tslint:enable:max-line-length */ - (this as any).__proto__ = FirebaseFunctionsError.prototype; - } -} - function tasksEmulatorUrl(resources: utils.ParsedResource): string | undefined { if (process.env.CLOUD_TASKS_EMULATOR_HOST) { return `http://${process.env.CLOUD_TASKS_EMULATOR_HOST}/projects/${resources.projectId}/locations/${resources.locationId}/queues/${resources.resourceId}/tasks`; diff --git a/src/functions/functions.ts b/src/functions/functions.ts index c8c0a30a71..805654aaa6 100644 --- a/src/functions/functions.ts +++ b/src/functions/functions.ts @@ -16,7 +16,8 @@ */ import { App } from '../app'; -import { FirebaseFunctionsError, FunctionsApiClient } from './functions-api-client-internal'; +import { FunctionsApiClient } from './functions-api-client-internal'; +import { FirebaseFunctionsError } from './error'; import { TaskOptions } from './functions-api'; import * as validator from '../utils/validator'; @@ -75,19 +76,22 @@ export class TaskQueue> { constructor(private readonly functionName: string, private readonly client: FunctionsApiClient, private readonly extensionId?: string) { if (!validator.isNonEmptyString(functionName)) { - throw new FirebaseFunctionsError( - 'invalid-argument', - '`functionName` must be a non-empty string.'); + throw new FirebaseFunctionsError({ + code: 'invalid-argument', + message: '`functionName` must be a non-empty string.' + }); } if (!validator.isNonNullObject(client) || !('enqueue' in client)) { - throw new FirebaseFunctionsError( - 'invalid-argument', - 'Must provide a valid FunctionsApiClient instance to create a new TaskQueue.'); + throw new FirebaseFunctionsError({ + code: 'invalid-argument', + message: 'Must provide a valid FunctionsApiClient instance to create a new TaskQueue.' + }); } if (typeof extensionId !== 'undefined' && !validator.isString(extensionId)) { - throw new FirebaseFunctionsError( - 'invalid-argument', - '`extensionId` must be a string.'); + throw new FirebaseFunctionsError({ + code: 'invalid-argument', + message: '`extensionId` must be a string.' + }); } } diff --git a/src/functions/index.ts b/src/functions/index.ts index bd86d88c2d..0894d244a6 100644 --- a/src/functions/index.ts +++ b/src/functions/index.ts @@ -71,3 +71,5 @@ export function getFunctions(app?: App): Functions { const firebaseApp: FirebaseApp = app as FirebaseApp; return firebaseApp.getOrInitService('functions', (app) => new Functions(app)); } + +export { FirebaseFunctionsError, FunctionsErrorCode } from './error'; \ No newline at end of file diff --git a/src/installations/error.ts b/src/installations/error.ts new file mode 100644 index 0000000000..8f53d5c874 --- /dev/null +++ b/src/installations/error.ts @@ -0,0 +1,70 @@ +/*! + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PrefixedFirebaseError, ErrorInfo } from '../utils/error'; + +/** + * The constant mapping for valid Installations client error codes. + */ +export const InstallationsErrorCode = { + INVALID_ARGUMENT: 'invalid-argument', + INVALID_PROJECT_ID: 'invalid-project-id', + INVALID_INSTALLATION_ID: 'invalid-installation-id', + API_ERROR: 'api-error', +} as const; + +/** + * The type definition for valid Installations client error codes. + */ +export type InstallationsErrorCode = typeof InstallationsErrorCode[keyof typeof InstallationsErrorCode]; + +/** + * Internal Installations client error code mapping used to construct ErrorInfo. + */ +export const installationsClientErrorCode: { readonly [K in keyof typeof InstallationsErrorCode]: ErrorInfo } = { + INVALID_ARGUMENT: { + code: InstallationsErrorCode.INVALID_ARGUMENT, + message: 'Invalid argument provided.', + }, + INVALID_PROJECT_ID: { + code: InstallationsErrorCode.INVALID_PROJECT_ID, + message: 'Invalid project ID provided.', + }, + INVALID_INSTALLATION_ID: { + code: InstallationsErrorCode.INVALID_INSTALLATION_ID, + message: 'Invalid installation ID provided.', + }, + API_ERROR: { + code: InstallationsErrorCode.API_ERROR, + message: 'Installation ID API call failed.', + }, +}; + +/** + * Firebase Installations service error code structure. This extends `PrefixedFirebaseError`. + */ +export class FirebaseInstallationsError extends PrefixedFirebaseError { + /** + * + * @param info - The error code info. + * @param message - The error message. This will override the default + * message if provided. + */ + constructor(info: ErrorInfo, message?: string) { + // Override default message if custom message provided. + super('installations', info.code, message || info.message, info.httpResponse, info.cause); + } +} diff --git a/src/installations/index.ts b/src/installations/index.ts index e7fc00ab78..567d27f2be 100644 --- a/src/installations/index.ts +++ b/src/installations/index.ts @@ -62,4 +62,7 @@ export function getInstallations(app?: App): Installations { return firebaseApp.getOrInitService('installations', (app) => new Installations(app)); } -export { FirebaseInstallationsError, InstallationsClientErrorCode } from '../utils/error'; +export { + FirebaseInstallationsError, + InstallationsErrorCode, +} from './error'; diff --git a/src/installations/installations-request-handler.ts b/src/installations/installations-request-handler.ts index 67c0f6f46f..a5fceda6bc 100644 --- a/src/installations/installations-request-handler.ts +++ b/src/installations/installations-request-handler.ts @@ -17,7 +17,8 @@ import { App } from '../app/index'; import { FirebaseApp } from '../app/firebase-app'; -import { FirebaseInstallationsError, InstallationsClientErrorCode } from '../utils/error'; +import { installationsClientErrorCode, FirebaseInstallationsError } from './error'; +import { toHttpResponse } from '../utils/error'; import { ApiSettings, AuthorizedHttpClient, HttpRequestConfig, RequestResponseError, } from '../utils/api-request'; @@ -33,7 +34,7 @@ const FIREBASE_IID_PATH = '/v1/'; const FIREBASE_IID_TIMEOUT = 10000; /** HTTP error codes raised by the backend server. */ -const ERROR_CODES: {[key: number]: string} = { +const ERROR_CODES: { [key: number]: string; } = { 400: 'Malformed installation ID argument.', 401: 'Request not authorized.', 403: 'Project does not match installation ID or the client does not have sufficient privileges.', @@ -66,7 +67,7 @@ export class FirebaseInstallationsRequestHandler { public deleteInstallation(fid: string): Promise { if (!validator.isNonEmptyString(fid)) { return Promise.reject(new FirebaseInstallationsError( - InstallationsClientErrorCode.INVALID_INSTALLATION_ID, + installationsClientErrorCode.INVALID_INSTALLATION_ID, 'Installation ID must be a non-empty string.', )); } @@ -100,7 +101,12 @@ export class FirebaseInstallationsRequestHandler { const template: string = ERROR_CODES[response.status]; const message: string = template ? `Installation ID "${apiSettings.getEndpoint()}": ${template}` : errorMessage; - throw new FirebaseInstallationsError(InstallationsClientErrorCode.API_ERROR, message); + throw new FirebaseInstallationsError({ + ...installationsClientErrorCode.API_ERROR, + message, + httpResponse: toHttpResponse(response), + cause: err, + }); } // In case of timeouts and other network errors, the HttpClient returns a // FirebaseError wrapped in the response. Simply throw it here. @@ -118,7 +124,7 @@ export class FirebaseInstallationsRequestHandler { if (!validator.isNonEmptyString(projectId)) { // Assert for an explicit projct ID (either via AppOptions or the cert itself). throw new FirebaseInstallationsError( - InstallationsClientErrorCode.INVALID_PROJECT_ID, + installationsClientErrorCode.INVALID_PROJECT_ID, 'Failed to determine project ID for Installations. Initialize the ' + 'SDK with service account credentials or set project ID as an app option. ' + 'Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.', diff --git a/src/installations/installations.ts b/src/installations/installations.ts index fb2bb5fa15..f677c8dda3 100644 --- a/src/installations/installations.ts +++ b/src/installations/installations.ts @@ -15,7 +15,7 @@ */ import { App } from '../app/index'; -import { FirebaseInstallationsError, InstallationsClientErrorCode } from '../utils/error'; +import { installationsClientErrorCode, FirebaseInstallationsError } from './error'; import { FirebaseInstallationsRequestHandler } from './installations-request-handler'; import * as validator from '../utils/validator'; @@ -35,7 +35,7 @@ export class Installations { constructor(app: App) { if (!validator.isNonNullObject(app) || !('options' in app)) { throw new FirebaseInstallationsError( - InstallationsClientErrorCode.INVALID_ARGUMENT, + installationsClientErrorCode.INVALID_ARGUMENT, 'First argument passed to admin.installations() must be a valid Firebase app instance.', ); } diff --git a/src/instance-id/error.ts b/src/instance-id/error.ts new file mode 100644 index 0000000000..e0cda50167 --- /dev/null +++ b/src/instance-id/error.ts @@ -0,0 +1,61 @@ +/*! + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PrefixedFirebaseError, ErrorInfo } from '../utils/error'; +import { installationsClientErrorCode } from '../installations/error'; + +/** + * The constant mapping for valid Instance ID client error codes. + */ +export const InstanceIdErrorCode = { + INVALID_ARGUMENT: 'invalid-argument', + INVALID_PROJECT_ID: 'invalid-project-id', + INVALID_INSTALLATION_ID: 'invalid-installation-id', + API_ERROR: 'api-error', + INVALID_INSTANCE_ID: 'invalid-instance-id', +} as const; + +/** + * The type definition for valid Instance ID client error codes. + */ +export type InstanceIdErrorCode = typeof InstanceIdErrorCode[keyof typeof InstanceIdErrorCode]; + +/** + * Internal Instance ID client error code mapping used to construct ErrorInfo. + */ +export const instanceIdClientErrorCode: { readonly [K in keyof typeof InstanceIdErrorCode]: ErrorInfo } = { + ...installationsClientErrorCode, + INVALID_INSTANCE_ID: { + code: InstanceIdErrorCode.INVALID_INSTANCE_ID, + message: 'Invalid instance ID provided.', + }, +}; + +/** + * Firebase Instance ID service error code structure. This extends `PrefixedFirebaseError`. + */ +export class FirebaseInstanceIdError extends PrefixedFirebaseError { + /** + * + * @param info - The error code info. + * @param message - The error message. This will override the default + * message if provided. + */ + constructor(info: ErrorInfo, message?: string) { + // Override default message if custom message provided. + super('instance-id', info.code, message || info.message, info.httpResponse, info.cause); + } +} diff --git a/src/instance-id/index.ts b/src/instance-id/index.ts index 0486a927af..a774c0a75b 100644 --- a/src/instance-id/index.ts +++ b/src/instance-id/index.ts @@ -71,4 +71,7 @@ export function getInstanceId(app?: App): InstanceId { return firebaseApp.getOrInitService('instanceId', (app) => new InstanceId(app)); } -export { FirebaseInstanceIdError, InstanceIdClientErrorCode } from '../utils/error'; +export { + FirebaseInstanceIdError, + InstanceIdErrorCode, +} from './error'; diff --git a/src/instance-id/instance-id.ts b/src/instance-id/instance-id.ts index 3a5baac09f..4564cd56ed 100644 --- a/src/instance-id/instance-id.ts +++ b/src/instance-id/instance-id.ts @@ -15,11 +15,9 @@ */ import { getInstallations } from '../installations'; +import { FirebaseInstallationsError, installationsClientErrorCode } from '../installations/error'; import { App } from '../app/index'; -import { - FirebaseInstallationsError, FirebaseInstanceIdError, - InstallationsClientErrorCode, InstanceIdClientErrorCode, -} from '../utils/error'; +import { FirebaseInstanceIdError, instanceIdClientErrorCode } from './error'; import * as validator from '../utils/validator'; /** @@ -40,7 +38,7 @@ export class InstanceId { constructor(app: App) { if (!validator.isNonNullObject(app) || !('options' in app)) { throw new FirebaseInstanceIdError( - InstanceIdClientErrorCode.INVALID_ARGUMENT, + instanceIdClientErrorCode.INVALID_ARGUMENT, 'First argument passed to instanceId() must be a valid Firebase app instance.', ); } @@ -67,8 +65,8 @@ export class InstanceId { .catch((err) => { if (err instanceof FirebaseInstallationsError) { let code = err.code.replace('installations/', ''); - if (code === InstallationsClientErrorCode.INVALID_INSTALLATION_ID.code) { - code = InstanceIdClientErrorCode.INVALID_INSTANCE_ID.code; + if (code === installationsClientErrorCode.INVALID_INSTALLATION_ID.code) { + code = instanceIdClientErrorCode.INVALID_INSTANCE_ID.code; } throw new FirebaseInstanceIdError({ code, message: err.message }); diff --git a/src/machine-learning/error.ts b/src/machine-learning/error.ts new file mode 100644 index 0000000000..b8a2a0e102 --- /dev/null +++ b/src/machine-learning/error.ts @@ -0,0 +1,78 @@ +/*! + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PrefixedFirebaseError, ErrorInfo } from '../utils/error'; + +/** + * The constant mapping for valid Machine Learning client error codes. + */ +export const MachineLearningErrorCode = { + ALREADY_EXISTS: 'already-exists', + AUTHENTICATION_ERROR: 'authentication-error', + INTERNAL_ERROR: 'internal-error', + INVALID_ARGUMENT: 'invalid-argument', + INVALID_SERVER_RESPONSE: 'invalid-server-response', + NOT_FOUND: 'not-found', + RESOURCE_EXHAUSTED: 'resource-exhausted', + SERVICE_UNAVAILABLE: 'service-unavailable', + UNKNOWN_ERROR: 'unknown-error', + CANCELLED: 'cancelled', + DEADLINE_EXCEEDED: 'deadline-exceeded', + PERMISSION_DENIED: 'permission-denied', + FAILED_PRECONDITION: 'failed-precondition', + ABORTED: 'aborted', + OUT_OF_RANGE: 'out-of-range', + DATA_LOSS: 'data-loss', + UNAUTHENTICATED: 'unauthenticated', +} as const; + +/** + * The type definition for valid Machine Learning client error codes. + */ +export type MachineLearningErrorCode = typeof MachineLearningErrorCode[keyof typeof MachineLearningErrorCode]; + +export class FirebaseMachineLearningError extends PrefixedFirebaseError { + /** @internal */ + public static fromOperationError(code: number, message: string): FirebaseMachineLearningError { + switch (code) { + case 1: return new FirebaseMachineLearningError({ code: 'cancelled', message }); + case 2: return new FirebaseMachineLearningError({ code: 'unknown-error', message }); + case 3: return new FirebaseMachineLearningError({ code: 'invalid-argument', message }); + case 4: return new FirebaseMachineLearningError({ code: 'deadline-exceeded', message }); + case 5: return new FirebaseMachineLearningError({ code: 'not-found', message }); + case 6: return new FirebaseMachineLearningError({ code: 'already-exists', message }); + case 7: return new FirebaseMachineLearningError({ code: 'permission-denied', message }); + case 8: return new FirebaseMachineLearningError({ code: 'resource-exhausted', message }); + case 9: return new FirebaseMachineLearningError({ code: 'failed-precondition', message }); + case 10: return new FirebaseMachineLearningError({ code: 'aborted', message }); + case 11: return new FirebaseMachineLearningError({ code: 'out-of-range', message }); + case 13: return new FirebaseMachineLearningError({ code: 'internal-error', message }); + case 14: return new FirebaseMachineLearningError({ code: 'service-unavailable', message }); + case 15: return new FirebaseMachineLearningError({ code: 'data-loss', message }); + case 16: return new FirebaseMachineLearningError({ code: 'unauthenticated', message }); + default: + return new FirebaseMachineLearningError({ code: 'unknown-error', message }); + } + } + + /** + * @param info - The error code info. + * @param message - The error message. If provided, this will override the default message. + */ + constructor(info: ErrorInfo, message?: string) { + super('machine-learning', info.code, message || info.message, info.httpResponse, info.cause); + } +} diff --git a/src/machine-learning/index.ts b/src/machine-learning/index.ts index 2945c43d35..0ebb351a5d 100644 --- a/src/machine-learning/index.ts +++ b/src/machine-learning/index.ts @@ -71,3 +71,5 @@ export function getMachineLearning(app?: App): MachineLearning { const firebaseApp: FirebaseApp = app as FirebaseApp; return firebaseApp.getOrInitService('machineLearning', (app) => new MachineLearning(app)); } + +export { FirebaseMachineLearningError, MachineLearningErrorCode } from './error'; \ No newline at end of file diff --git a/src/machine-learning/machine-learning-api-client.ts b/src/machine-learning/machine-learning-api-client.ts index b8fd5301f7..1e8576d577 100644 --- a/src/machine-learning/machine-learning-api-client.ts +++ b/src/machine-learning/machine-learning-api-client.ts @@ -19,10 +19,10 @@ import { FirebaseApp } from '../app/firebase-app'; import { HttpRequestConfig, HttpClient, RequestResponseError, AuthorizedHttpClient, ExponentialBackoffPoller } from '../utils/api-request'; -import { PrefixedFirebaseError } from '../utils/error'; +import { PrefixedFirebaseError, toHttpResponse } from '../utils/error'; import * as utils from '../utils/index'; import * as validator from '../utils/validator'; -import { FirebaseMachineLearningError, MachineLearningErrorCode } from './machine-learning-utils'; +import { FirebaseMachineLearningError, MachineLearningErrorCode } from './error'; /** * Firebase ML Model input objects @@ -139,10 +139,11 @@ export class MachineLearningApiClient { constructor(private readonly app: App) { if (!validator.isNonNullObject(app) || !('options' in app)) { - throw new FirebaseMachineLearningError( - 'invalid-argument', - 'First argument passed to admin.machineLearning() must be a valid ' - + 'Firebase app instance.'); + throw new FirebaseMachineLearningError({ + code: 'invalid-argument', + message: 'First argument passed to admin.machineLearning() must be a valid ' + + 'Firebase app instance.' + }); } this.httpClient = new AuthorizedHttpClient(app as FirebaseApp); @@ -151,7 +152,7 @@ export class MachineLearningApiClient { public createModel(model: ModelOptions): Promise { if (!validator.isNonNullObject(model) || !validator.isNonEmptyString(model.displayName)) { - const err = new FirebaseMachineLearningError('invalid-argument', 'Invalid model content.'); + const err = new FirebaseMachineLearningError({ code: 'invalid-argument', message: 'Invalid model content.' }); return Promise.reject(err); } return this.getProjectUrl() @@ -169,7 +170,10 @@ export class MachineLearningApiClient { if (!validator.isNonEmptyString(modelId) || !validator.isNonNullObject(model) || !validator.isNonEmptyArray(updateMask)) { - const err = new FirebaseMachineLearningError('invalid-argument', 'Invalid model or mask content.'); + const err = new FirebaseMachineLearningError({ + code: 'invalid-argument', + message: 'Invalid model or mask content.', + }); return Promise.reject(err); } return this.getProjectUrl() @@ -202,27 +206,31 @@ export class MachineLearningApiClient { public listModels(options: ListModelsOptions = {}): Promise { if (!validator.isNonNullObject(options)) { - const err = new FirebaseMachineLearningError('invalid-argument', 'Invalid ListModelsOptions'); + const err = new FirebaseMachineLearningError({ code: 'invalid-argument', message: 'Invalid ListModelsOptions' }); return Promise.reject(err); } if (typeof options.filter !== 'undefined' && !validator.isNonEmptyString(options.filter)) { - const err = new FirebaseMachineLearningError('invalid-argument', 'Invalid list filter.'); + const err = new FirebaseMachineLearningError({ code: 'invalid-argument', message: 'Invalid list filter.' }); return Promise.reject(err); } if (typeof options.pageSize !== 'undefined') { if (!validator.isNumber(options.pageSize)) { - const err = new FirebaseMachineLearningError('invalid-argument', 'Invalid page size.'); + const err = new FirebaseMachineLearningError({ code: 'invalid-argument', message: 'Invalid page size.' }); return Promise.reject(err); } if (options.pageSize < 1 || options.pageSize > 100) { - const err = new FirebaseMachineLearningError( - 'invalid-argument', 'Page size must be between 1 and 100.'); + const err = new FirebaseMachineLearningError({ + code: 'invalid-argument', + message: 'Page size must be between 1 and 100.' + }); return Promise.reject(err); } } if (typeof options.pageToken !== 'undefined' && !validator.isNonEmptyString(options.pageToken)) { - const err = new FirebaseMachineLearningError( - 'invalid-argument', 'Next page token must be a non-empty string.'); + const err = new FirebaseMachineLearningError({ + code: 'invalid-argument', + message: 'Next page token must be a non-empty string.' + }); return Promise.reject(err); } return this.getProjectUrl() @@ -273,8 +281,10 @@ export class MachineLearningApiClient { } // Done operations must have either a response or an error. - throw new FirebaseMachineLearningError('invalid-server-response', - 'Invalid operation response.'); + throw new FirebaseMachineLearningError({ + code: 'invalid-server-response', + message: 'Invalid operation response.' + }); } // Operation is not done @@ -285,8 +295,10 @@ export class MachineLearningApiClient { const metadata = op.metadata || {}; const metadataType: string = metadata['@type'] || ''; if (!metadataType.includes('ModelOperationMetadata')) { - throw new FirebaseMachineLearningError('invalid-server-response', - `Unknown Metadata type: ${JSON.stringify(metadata)}`); + throw new FirebaseMachineLearningError({ + code: 'invalid-server-response', + message: `Unknown Metadata type: ${JSON.stringify(metadata)}` + }); } return this.getModel(extractModelId(metadata.name)); @@ -376,9 +388,12 @@ export class MachineLearningApiClient { const response = err.response; if (!response.isJson()) { - return new FirebaseMachineLearningError( - 'unknown-error', - `Unexpected response with status: ${response.status} and body: ${response.text}`); + return new FirebaseMachineLearningError({ + code: 'unknown-error', + message: `Unexpected response with status: ${response.status} and body: ${response.text}`, + httpResponse: toHttpResponse(response), + cause: err + }); } const error: Error = (response.data as ErrorResponse).error || {}; @@ -386,8 +401,13 @@ export class MachineLearningApiClient { if (error.status && error.status in ERROR_CODE_MAPPING) { code = ERROR_CODE_MAPPING[error.status]; } - const message = error.message || `Unknown server error: ${response.text}`; - return new FirebaseMachineLearningError(code, message); + const message = error.message || 'Unknown server error'; + return new FirebaseMachineLearningError({ + code, + message, + httpResponse: toHttpResponse(response), + cause: err + }); } private getProjectUrl(): Promise { @@ -405,11 +425,12 @@ export class MachineLearningApiClient { return utils.findProjectId(this.app) .then((projectId) => { if (!validator.isNonEmptyString(projectId)) { - throw new FirebaseMachineLearningError( - 'invalid-argument', - 'Failed to determine project ID. Initialize the SDK with service account credentials, or ' - + 'set project ID as an app option. Alternatively, set the GOOGLE_CLOUD_PROJECT ' - + 'environment variable.'); + throw new FirebaseMachineLearningError({ + code: 'invalid-argument', + message: 'Failed to determine project ID. Initialize the SDK with service account credentials, or ' + + 'set project ID as an app option. Alternatively, set the GOOGLE_CLOUD_PROJECT ' + + 'environment variable.' + }); } this.projectIdPrefix = `projects/${projectId}`; @@ -419,13 +440,17 @@ export class MachineLearningApiClient { private getModelName(modelId: string): string { if (!validator.isNonEmptyString(modelId)) { - throw new FirebaseMachineLearningError( - 'invalid-argument', 'Model ID must be a non-empty string.'); + throw new FirebaseMachineLearningError({ + code: 'invalid-argument', + message: 'Model ID must be a non-empty string.' + }); } if (modelId.indexOf('/') !== -1) { - throw new FirebaseMachineLearningError( - 'invalid-argument', 'Model ID must not contain any "/" characters.'); + throw new FirebaseMachineLearningError({ + code: 'invalid-argument', + message: 'Model ID must not contain any "/" characters.' + }); } return `models/${modelId}`; diff --git a/src/machine-learning/machine-learning-utils.ts b/src/machine-learning/machine-learning-utils.ts deleted file mode 100644 index 3e2236975c..0000000000 --- a/src/machine-learning/machine-learning-utils.ts +++ /dev/null @@ -1,64 +0,0 @@ -/*! - * Copyright 2020 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { PrefixedFirebaseError } from '../utils/error'; - -export type MachineLearningErrorCode = - 'already-exists' - | 'authentication-error' - | 'internal-error' - | 'invalid-argument' - | 'invalid-server-response' - | 'not-found' - | 'resource-exhausted' - | 'service-unavailable' - | 'unknown-error' - | 'cancelled' - | 'deadline-exceeded' - | 'permission-denied' - | 'failed-precondition' - | 'aborted' - | 'out-of-range' - | 'data-loss' - | 'unauthenticated'; - -export class FirebaseMachineLearningError extends PrefixedFirebaseError { - public static fromOperationError(code: number, message: string): FirebaseMachineLearningError { - switch (code) { - case 1: return new FirebaseMachineLearningError('cancelled', message); - case 2: return new FirebaseMachineLearningError('unknown-error', message); - case 3: return new FirebaseMachineLearningError('invalid-argument', message); - case 4: return new FirebaseMachineLearningError('deadline-exceeded', message); - case 5: return new FirebaseMachineLearningError('not-found', message); - case 6: return new FirebaseMachineLearningError('already-exists', message); - case 7: return new FirebaseMachineLearningError('permission-denied', message); - case 8: return new FirebaseMachineLearningError('resource-exhausted', message); - case 9: return new FirebaseMachineLearningError('failed-precondition', message); - case 10: return new FirebaseMachineLearningError('aborted', message); - case 11: return new FirebaseMachineLearningError('out-of-range', message); - case 13: return new FirebaseMachineLearningError('internal-error', message); - case 14: return new FirebaseMachineLearningError('service-unavailable', message); - case 15: return new FirebaseMachineLearningError('data-loss', message); - case 16: return new FirebaseMachineLearningError('unauthenticated', message); - default: - return new FirebaseMachineLearningError('unknown-error', message); - } - } - - constructor(code: MachineLearningErrorCode, message: string) { - super('machine-learning', code, message); - } -} diff --git a/src/machine-learning/machine-learning.ts b/src/machine-learning/machine-learning.ts index a83bda71cc..ca080643dd 100644 --- a/src/machine-learning/machine-learning.ts +++ b/src/machine-learning/machine-learning.ts @@ -24,7 +24,7 @@ import { MachineLearningApiClient, ModelResponse, ModelUpdateOptions, isGcsTfliteModelOptions, ListModelsOptions, ModelOptions, } from './machine-learning-api-client'; -import { FirebaseMachineLearningError } from './machine-learning-utils'; +import { FirebaseMachineLearningError } from './error'; /** Response object for a listModels operation. */ export interface ListModelsResult { @@ -163,9 +163,10 @@ export class MachineLearning { return this.client.listModels(options) .then((resp) => { if (!validator.isNonNullObject(resp)) { - throw new FirebaseMachineLearningError( - 'invalid-argument', - `Invalid ListModels response: ${JSON.stringify(resp)}`); + throw new FirebaseMachineLearningError({ + code: 'invalid-argument', + message: `Invalid ListModels response: ${JSON.stringify(resp)}` + }); } let models: Model[] = []; if (resp.models) { @@ -205,9 +206,11 @@ export class MachineLearning { return modelOptions; }) .catch((err: Error) => { - throw new FirebaseMachineLearningError( - 'internal-error', - `Error during signing upload url: ${err.message}`); + throw new FirebaseMachineLearningError({ + code: 'internal-error', + message: `Error during signing upload url: ${err.message}`, + cause: err, + }); }); } return Promise.resolve(modelOptions); @@ -220,9 +223,10 @@ export class MachineLearning { const gcsRegex = /^gs:\/\/([a-z0-9_.-]{3,63})\/(.+)$/; const matches = gcsRegex.exec(unsignedUrl); if (!matches) { - throw new FirebaseMachineLearningError( - 'invalid-argument', - `Invalid unsigned url: ${unsignedUrl}`); + throw new FirebaseMachineLearningError({ + code: 'invalid-argument', + message: `Invalid unsigned url: ${unsignedUrl}` + }); } const bucketName = matches[1]; const blobName = matches[2]; @@ -384,9 +388,10 @@ export class Model { !validator.isNonEmptyString(model.updateTime) || !validator.isNonEmptyString(model.displayName) || !validator.isNonEmptyString(model.etag)) { - throw new FirebaseMachineLearningError( - 'invalid-server-response', - `Invalid Model response: ${JSON.stringify(model)}`); + throw new FirebaseMachineLearningError({ + code: 'invalid-server-response', + message: `Invalid Model response: ${JSON.stringify(model)}` + }); } const tmpModel = deepCopy(model); diff --git a/src/messaging/error.ts b/src/messaging/error.ts new file mode 100644 index 0000000000..749479e085 --- /dev/null +++ b/src/messaging/error.ts @@ -0,0 +1,318 @@ +/*! + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ErrorInfo, PrefixedFirebaseError, toHttpResponse } from '../utils/error'; +import { RequestResponseError } from '../utils/api-request'; +import { deepCopy } from '../utils/deep-copy'; +import { BatchResponse } from './messaging-api'; + +/** + * The constant mapping for valid Messaging client error codes. + */ +export const MessagingErrorCode = { + INVALID_ARGUMENT: 'invalid-argument', + INVALID_RECIPIENT: 'invalid-recipient', + INVALID_PAYLOAD: 'invalid-payload', + INVALID_DATA_PAYLOAD_KEY: 'invalid-data-payload-key', + PAYLOAD_SIZE_LIMIT_EXCEEDED: 'payload-size-limit-exceeded', + INVALID_OPTIONS: 'invalid-options', + INVALID_REGISTRATION_TOKEN: 'invalid-registration-token', + REGISTRATION_TOKEN_NOT_REGISTERED: 'registration-token-not-registered', + MISMATCHED_CREDENTIAL: 'mismatched-credential', + INVALID_PACKAGE_NAME: 'invalid-package-name', + DEVICE_MESSAGE_RATE_EXCEEDED: 'device-message-rate-exceeded', + TOPICS_MESSAGE_RATE_EXCEEDED: 'topics-message-rate-exceeded', + MESSAGE_RATE_EXCEEDED: 'message-rate-exceeded', + THIRD_PARTY_AUTH_ERROR: 'third-party-auth-error', + TOO_MANY_TOPICS: 'too-many-topics', + AUTHENTICATION_ERROR: 'authentication-error', + SERVER_UNAVAILABLE: 'server-unavailable', + INTERNAL_ERROR: 'internal-error', + UNKNOWN_ERROR: 'unknown-error', +} as const; + +/** + * The type definition for valid Messaging client error codes. + */ +export type MessagingErrorCode = typeof MessagingErrorCode[keyof typeof MessagingErrorCode]; + +/** + * Internal Messaging client error code mapping used to construct ErrorInfo. + */ +export const messagingClientErrorCode: { readonly [K in keyof typeof MessagingErrorCode]: ErrorInfo } = { + INVALID_ARGUMENT: { + code: MessagingErrorCode.INVALID_ARGUMENT, + message: 'Invalid argument provided.', + }, + INVALID_RECIPIENT: { + code: MessagingErrorCode.INVALID_RECIPIENT, + message: 'Invalid message recipient provided.', + }, + INVALID_PAYLOAD: { + code: MessagingErrorCode.INVALID_PAYLOAD, + message: 'Invalid message payload provided.', + }, + INVALID_DATA_PAYLOAD_KEY: { + code: MessagingErrorCode.INVALID_DATA_PAYLOAD_KEY, + message: 'The data message payload contains an invalid key. See the reference ' + + 'documentation for the DataMessagePayload type for restricted keys.', + }, + PAYLOAD_SIZE_LIMIT_EXCEEDED: { + code: MessagingErrorCode.PAYLOAD_SIZE_LIMIT_EXCEEDED, + message: 'The provided message payload exceeds the FCM size limits. See the ' + + 'error documentation for more details.', + }, + INVALID_OPTIONS: { + code: MessagingErrorCode.INVALID_OPTIONS, + message: 'Invalid message options provided.', + }, + INVALID_REGISTRATION_TOKEN: { + code: MessagingErrorCode.INVALID_REGISTRATION_TOKEN, + message: 'Invalid registration token provided. Make sure it matches the ' + + 'registration token the client app receives from registering with FCM.', + }, + REGISTRATION_TOKEN_NOT_REGISTERED: { + code: MessagingErrorCode.REGISTRATION_TOKEN_NOT_REGISTERED, + message: 'The provided registration token is not registered. A ' + + 'previously valid registration token can be unregistered for a variety of reasons. See the ' + + 'error documentation for more details. Remove this registration token and stop using it to ' + + 'send messages.', + }, + MISMATCHED_CREDENTIAL: { + code: MessagingErrorCode.MISMATCHED_CREDENTIAL, + message: 'The credential used to authenticate this SDK does not have permission ' + + 'to send messages to the device corresponding to the provided registration token. Make sure the ' + + 'credential and registration token both belong to the same Firebase project.', + }, + INVALID_PACKAGE_NAME: { + code: MessagingErrorCode.INVALID_PACKAGE_NAME, + message: 'The message was addressed to a registration token whose package name does ' + + 'not match the provided "restrictedPackageName" option.', + }, + DEVICE_MESSAGE_RATE_EXCEEDED: { + code: MessagingErrorCode.DEVICE_MESSAGE_RATE_EXCEEDED, + message: 'The rate of messages to a particular device is too high. Reduce ' + + 'the number of messages sent to this device and do not immediately retry sending to this device.', + }, + TOPICS_MESSAGE_RATE_EXCEEDED: { + code: MessagingErrorCode.TOPICS_MESSAGE_RATE_EXCEEDED, + message: 'The rate of messages to subscribers to a particular topic is too ' + + 'high. Reduce the number of messages sent for this topic, and do not immediately retry sending ' + + 'to this topic.', + }, + MESSAGE_RATE_EXCEEDED: { + code: MessagingErrorCode.MESSAGE_RATE_EXCEEDED, + message: 'Sending limit exceeded for the message target.', + }, + THIRD_PARTY_AUTH_ERROR: { + code: MessagingErrorCode.THIRD_PARTY_AUTH_ERROR, + message: 'A message targeted to an iOS device could not be sent because the ' + + 'required APNs SSL certificate was not uploaded or has expired. Check the validity of your ' + + 'development and production certificates.', + }, + TOO_MANY_TOPICS: { + code: MessagingErrorCode.TOO_MANY_TOPICS, + message: 'The maximum number of topics the provided registration token can be ' + + 'subscribed to has been exceeded.', + }, + AUTHENTICATION_ERROR: { + code: MessagingErrorCode.AUTHENTICATION_ERROR, + message: 'An error occurred when trying to authenticate to the FCM servers. Make ' + + 'sure the credential used to authenticate this SDK has the proper permissions. See ' + + 'https://firebase.google.com/docs/admin/setup for setup instructions.', + }, + SERVER_UNAVAILABLE: { + code: MessagingErrorCode.SERVER_UNAVAILABLE, + message: 'The FCM server could not process the request in time. See the error ' + + 'documentation for more details.', + }, + INTERNAL_ERROR: { + code: MessagingErrorCode.INTERNAL_ERROR, + message: 'An internal error has occurred. Please retry the request.', + }, + UNKNOWN_ERROR: { + code: MessagingErrorCode.UNKNOWN_ERROR, + message: 'An unknown server error was returned.', + }, +}; + +/** @const {Record} Messaging server to client enum error codes. */ +const MESSAGING_SERVER_TO_CLIENT_CODE: Record = { + /* GENERIC ERRORS */ + // Generic invalid message parameter provided. + InvalidParameters: 'INVALID_ARGUMENT', + // Mismatched sender ID. + MismatchSenderId: 'MISMATCHED_CREDENTIAL', + // FCM server unavailable. + Unavailable: 'SERVER_UNAVAILABLE', + // FCM server internal error. + InternalServerError: 'INTERNAL_ERROR', + + /* SEND ERRORS */ + // Invalid registration token format. + InvalidRegistration: 'INVALID_REGISTRATION_TOKEN', + // Registration token is not registered. + NotRegistered: 'REGISTRATION_TOKEN_NOT_REGISTERED', + // Registration token does not match restricted package name. + InvalidPackageName: 'INVALID_PACKAGE_NAME', + // Message payload size limit exceeded. + MessageTooBig: 'PAYLOAD_SIZE_LIMIT_EXCEEDED', + // Invalid key in the data message payload. + InvalidDataKey: 'INVALID_DATA_PAYLOAD_KEY', + // Invalid time to live option. + InvalidTtl: 'INVALID_OPTIONS', + // Device message rate exceeded. + DeviceMessageRateExceeded: 'DEVICE_MESSAGE_RATE_EXCEEDED', + // Topics message rate exceeded. + TopicsMessageRateExceeded: 'TOPICS_MESSAGE_RATE_EXCEEDED', + // Invalid APNs credentials. + InvalidApnsCredential: 'THIRD_PARTY_AUTH_ERROR', + + /* FCM v1 canonical error codes */ + NOT_FOUND: 'REGISTRATION_TOKEN_NOT_REGISTERED', + PERMISSION_DENIED: 'MISMATCHED_CREDENTIAL', + RESOURCE_EXHAUSTED: 'MESSAGE_RATE_EXCEEDED', + UNAUTHENTICATED: 'THIRD_PARTY_AUTH_ERROR', + + /* FCM v1 new error codes */ + APNS_AUTH_ERROR: 'THIRD_PARTY_AUTH_ERROR', + INTERNAL: 'INTERNAL_ERROR', + INVALID_ARGUMENT: 'INVALID_ARGUMENT', + QUOTA_EXCEEDED: 'MESSAGE_RATE_EXCEEDED', + SENDER_ID_MISMATCH: 'MISMATCHED_CREDENTIAL', + THIRD_PARTY_AUTH_ERROR: 'THIRD_PARTY_AUTH_ERROR', + UNAVAILABLE: 'SERVER_UNAVAILABLE', + UNREGISTERED: 'REGISTRATION_TOKEN_NOT_REGISTERED', + UNSPECIFIED_ERROR: 'UNKNOWN_ERROR', +}; + +/** + * @const {Record} Topic management (IID) + * server to client enum error codes. + */ +const TOPIC_MGT_SERVER_TO_CLIENT_CODE: Record = { + /* TOPIC SUBSCRIPTION MANAGEMENT ERRORS */ + NOT_FOUND: 'REGISTRATION_TOKEN_NOT_REGISTERED', + INVALID_ARGUMENT: 'INVALID_REGISTRATION_TOKEN', + TOO_MANY_TOPICS: 'TOO_MANY_TOPICS', + RESOURCE_EXHAUSTED: 'TOO_MANY_TOPICS', + PERMISSION_DENIED: 'AUTHENTICATION_ERROR', + DEADLINE_EXCEEDED: 'SERVER_UNAVAILABLE', + INTERNAL: 'INTERNAL_ERROR', + UNKNOWN: 'UNKNOWN_ERROR', +}; + +/** + * Firebase Messaging error code structure. This extends `PrefixedFirebaseError`. + */ +export class FirebaseMessagingError extends PrefixedFirebaseError { + /** + * Creates the developer-facing error corresponding to the backend error code. + * + * @param serverErrorCode - The server error code. + * @param [message] The error message. The default message is used + * if not provided. + * @param [serverError] The error's raw server response. + * @returns The corresponding developer-facing error. + * @internal + */ + public static fromServerError( + serverErrorCode: string | null, + message?: string | null, + serverError?: RequestResponseError, + ): FirebaseMessagingError { + // If not found, default to unknown error. + let clientCodeKey = 'UNKNOWN_ERROR'; + if (serverErrorCode && serverErrorCode in MESSAGING_SERVER_TO_CLIENT_CODE) { + clientCodeKey = MESSAGING_SERVER_TO_CLIENT_CODE[serverErrorCode]; + } + const error: ErrorInfo = deepCopy((messagingClientErrorCode as any)[clientCodeKey]); + error.message = message || error.message; + + const rawData = serverError?.response?.data; + if (clientCodeKey === 'UNKNOWN_ERROR' && typeof rawData !== 'undefined') { + try { + error.message += ` Raw server response: "${typeof rawData === 'string' ? rawData : JSON.stringify(rawData)}"`; + } catch (e) { + // Ignore JSON parsing error. + } + } + + error.cause = serverError; + error.httpResponse = serverError?.response ? toHttpResponse(serverError.response) : undefined; + return new FirebaseMessagingError(error); + } + + /** + * @internal + */ + public static fromTopicManagementServerError( + serverErrorCode: string, + message?: string, + serverError?: RequestResponseError, + ): FirebaseMessagingError { + // If not found, default to unknown error. + const clientCodeKey = TOPIC_MGT_SERVER_TO_CLIENT_CODE[serverErrorCode] || 'UNKNOWN_ERROR'; + const error: ErrorInfo = deepCopy((messagingClientErrorCode as any)[clientCodeKey]); + error.message = message || error.message; + + const rawData = serverError?.response?.data; + if (clientCodeKey === 'UNKNOWN_ERROR' && typeof rawData !== 'undefined') { + try { + error.message += ` Raw server response: "${typeof rawData === 'string' ? rawData : JSON.stringify(rawData)}"`; + } catch (e) { + // Ignore JSON parsing error. + } + } + + error.cause = serverError; + error.httpResponse = serverError?.response ? toHttpResponse(serverError.response) : undefined; + return new FirebaseMessagingError(error); + } + + /** + * + * @param info - The error code info. + * @param message - The error message. This will override the default message if provided. + */ + constructor(info: ErrorInfo, message?: string) { + // Override default message if custom message provided. + super('messaging', info.code, message || info.message, info.httpResponse, info.cause); + } +} + +export class FirebaseMessagingSessionError extends FirebaseMessagingError { + public pendingBatchResponse?: Promise; + /** + * + * @param info - The error code info. + * @param message - The error message. This will override the default message if provided. + * @param pendingBatchResponse - BatchResponse for pending messages when session error occured. + */ + constructor(info: ErrorInfo, message?: string, pendingBatchResponse?: Promise) { + // Override default message if custom message provided. + super(info, message || info.message); + this.pendingBatchResponse = pendingBatchResponse; + } + + /** @returns The object representation of the error. */ + public toJSON(): object { + return { + ...super.toJSON(), + pendingBatchResponse: this.pendingBatchResponse, + }; + } +} diff --git a/src/messaging/index.ts b/src/messaging/index.ts index 16054c2c38..48ddbd0333 100644 --- a/src/messaging/index.ts +++ b/src/messaging/index.ts @@ -96,4 +96,7 @@ export function getMessaging(app?: App): Messaging { return firebaseApp.getOrInitService('messaging', (app) => new Messaging(app)); } -export { FirebaseMessagingError, MessagingClientErrorCode } from '../utils/error'; +export { + FirebaseMessagingError, + MessagingErrorCode, +} from './error'; diff --git a/src/messaging/messaging-errors-internal.ts b/src/messaging/messaging-errors-internal.ts index 41aa667b1d..d759b14876 100644 --- a/src/messaging/messaging-errors-internal.ts +++ b/src/messaging/messaging-errors-internal.ts @@ -15,7 +15,8 @@ */ import { RequestResponseError } from '../utils/api-request'; -import { FirebaseMessagingError, MessagingClientErrorCode } from '../utils/error'; +import { FirebaseMessagingError, messagingClientErrorCode } from './error'; +import { toHttpResponse } from '../utils/error'; import * as validator from '../utils/validator'; /** @@ -31,33 +32,35 @@ export function createFirebaseError(err: RequestResponseError): FirebaseMessagin const json = err.response.data; const errorCode = getErrorCode(json); const errorMessage = getErrorMessage(json); - return FirebaseMessagingError.fromServerError(errorCode, errorMessage, json); + return FirebaseMessagingError.fromServerError(errorCode, errorMessage, err); } // Non-JSON response let error: {code: string; message: string}; switch (err.response.status) { case 400: - error = MessagingClientErrorCode.INVALID_ARGUMENT; + error = messagingClientErrorCode.INVALID_ARGUMENT; break; case 401: case 403: - error = MessagingClientErrorCode.AUTHENTICATION_ERROR; + error = messagingClientErrorCode.AUTHENTICATION_ERROR; break; case 500: - error = MessagingClientErrorCode.INTERNAL_ERROR; + error = messagingClientErrorCode.INTERNAL_ERROR; break; case 503: - error = MessagingClientErrorCode.SERVER_UNAVAILABLE; + error = messagingClientErrorCode.SERVER_UNAVAILABLE; break; default: // Treat non-JSON responses with unexpected status codes as unknown errors. - error = MessagingClientErrorCode.UNKNOWN_ERROR; + error = messagingClientErrorCode.UNKNOWN_ERROR; } return new FirebaseMessagingError({ code: error.code, message: `${ error.message } Raw server response: "${ err.response.text }". Status code: ` + `${ err.response.status }.`, + httpResponse: toHttpResponse(err.response), + cause: err, }); } diff --git a/src/messaging/messaging-internal.ts b/src/messaging/messaging-internal.ts index 6427b98e0a..8139c87b94 100644 --- a/src/messaging/messaging-internal.ts +++ b/src/messaging/messaging-internal.ts @@ -15,7 +15,7 @@ */ import { renameProperties, transformMillisecondsToSecondsString } from '../utils/index'; -import { MessagingClientErrorCode, FirebaseMessagingError, } from '../utils/error'; +import { messagingClientErrorCode, FirebaseMessagingError } from './error'; import * as validator from '../utils/validator'; import { @@ -42,7 +42,7 @@ export const BLACKLISTED_OPTIONS_KEYS = [ export function validateMessage(message: Message): void { if (!validator.isNonNullObject(message)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'Message must be a non-null object'); + messagingClientErrorCode.INVALID_PAYLOAD, 'Message must be a non-null object'); } const anyMessage = message as any; @@ -54,14 +54,14 @@ export function validateMessage(message: Message): void { // Checks for illegal characters and empty string. if (!/^[a-zA-Z0-9-_.~%]+$/.test(anyMessage.topic)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'Malformed topic name'); + messagingClientErrorCode.INVALID_PAYLOAD, 'Malformed topic name'); } } const targets = [anyMessage.token, anyMessage.topic, anyMessage.condition]; if (targets.filter((v) => validator.isNonEmptyString(v)).length !== 1) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, + messagingClientErrorCode.INVALID_PAYLOAD, 'Exactly one of topic, token or condition is required'); } @@ -84,12 +84,12 @@ function validateStringMap(map: { [key: string]: any } | undefined, label: strin return; } else if (!validator.isNonNullObject(map)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, `${label} must be a non-null object`); + messagingClientErrorCode.INVALID_PAYLOAD, `${label} must be a non-null object`); } Object.keys(map).forEach((key) => { if (!validator.isString(map[key])) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, `${label} must only contain string values`); + messagingClientErrorCode.INVALID_PAYLOAD, `${label} must only contain string values`); } }); } @@ -104,7 +104,7 @@ function validateWebpushConfig(config: WebpushConfig | undefined): void { return; } else if (!validator.isNonNullObject(config)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'webpush must be a non-null object'); + messagingClientErrorCode.INVALID_PAYLOAD, 'webpush must be a non-null object'); } validateStringMap(config.headers, 'webpush.headers'); validateStringMap(config.data, 'webpush.data'); @@ -121,7 +121,7 @@ function validateApnsConfig(config: ApnsConfig | undefined): void { return; } else if (!validator.isNonNullObject(config)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'apns must be a non-null object'); + messagingClientErrorCode.INVALID_PAYLOAD, 'apns must be a non-null object'); } validateApnsLiveActivityToken(config.liveActivityToken); validateStringMap(config.headers, 'apns.headers'); @@ -140,12 +140,12 @@ function validateApnsLiveActivityToken(liveActivityToken: ApnsConfig['liveActivi return; } else if (!validator.isString(liveActivityToken)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, + messagingClientErrorCode.INVALID_PAYLOAD, 'apns.liveActivityToken must be a string value', ); } else if (!validator.isNonEmptyString(liveActivityToken)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, + messagingClientErrorCode.INVALID_PAYLOAD, 'apns.liveActivityToken must be a non-empty string', ); } @@ -161,19 +161,19 @@ function validateApnsFcmOptions(fcmOptions: ApnsFcmOptions | undefined): void { return; } else if (!validator.isNonNullObject(fcmOptions)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'fcmOptions must be a non-null object'); + messagingClientErrorCode.INVALID_PAYLOAD, 'fcmOptions must be a non-null object'); } if (typeof fcmOptions.imageUrl !== 'undefined' && !validator.isURL(fcmOptions.imageUrl)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, + messagingClientErrorCode.INVALID_PAYLOAD, 'imageUrl must be a valid URL string'); } if (typeof fcmOptions.analyticsLabel !== 'undefined' && !validator.isString(fcmOptions.analyticsLabel)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'analyticsLabel must be a string value'); + messagingClientErrorCode.INVALID_PAYLOAD, 'analyticsLabel must be a string value'); } const propertyMappings: { [key: string]: string } = { @@ -182,7 +182,7 @@ function validateApnsFcmOptions(fcmOptions: ApnsFcmOptions | undefined): void { Object.keys(propertyMappings).forEach((key) => { if (key in fcmOptions && propertyMappings[key] in fcmOptions) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, + messagingClientErrorCode.INVALID_PAYLOAD, `Multiple specifications for ${key} in ApnsFcmOptions`); } }); @@ -199,12 +199,12 @@ function validateFcmOptions(fcmOptions: FcmOptions | undefined): void { return; } else if (!validator.isNonNullObject(fcmOptions)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'fcmOptions must be a non-null object'); + messagingClientErrorCode.INVALID_PAYLOAD, 'fcmOptions must be a non-null object'); } if (typeof fcmOptions.analyticsLabel !== 'undefined' && !validator.isString(fcmOptions.analyticsLabel)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'analyticsLabel must be a string value'); + messagingClientErrorCode.INVALID_PAYLOAD, 'analyticsLabel must be a string value'); } } @@ -218,12 +218,12 @@ function validateNotification(notification: Notification | undefined): void { return; } else if (!validator.isNonNullObject(notification)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'notification must be a non-null object'); + messagingClientErrorCode.INVALID_PAYLOAD, 'notification must be a non-null object'); } if (typeof notification.imageUrl !== 'undefined' && !validator.isURL(notification.imageUrl)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'notification.imageUrl must be a valid URL string'); + messagingClientErrorCode.INVALID_PAYLOAD, 'notification.imageUrl must be a valid URL string'); } const propertyMappings: { [key: string]: string } = { @@ -232,7 +232,7 @@ function validateNotification(notification: Notification | undefined): void { Object.keys(propertyMappings).forEach((key) => { if (key in notification && propertyMappings[key] in notification) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, + messagingClientErrorCode.INVALID_PAYLOAD, `Multiple specifications for ${key} in Notification`); } }); @@ -249,7 +249,7 @@ function validateApnsPayload(payload: ApnsPayload | undefined): void { return; } else if (!validator.isNonNullObject(payload)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload must be a non-null object'); + messagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload must be a non-null object'); } validateAps(payload.aps); } @@ -265,7 +265,7 @@ function validateAps(aps: Aps): void { return; } else if (!validator.isNonNullObject(aps)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload.aps must be a non-null object'); + messagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload.aps must be a non-null object'); } validateApsAlert(aps.alert); validateApsSound(aps.sound); @@ -278,7 +278,7 @@ function validateAps(aps: Aps): void { Object.keys(propertyMappings).forEach((key) => { if (key in aps && propertyMappings[key] in aps) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, `Multiple specifications for ${key} in Aps`); + messagingClientErrorCode.INVALID_PAYLOAD, `Multiple specifications for ${key} in Aps`); } }); renameProperties(aps, propertyMappings); @@ -307,25 +307,25 @@ function validateApsSound(sound: string | CriticalSound | undefined): void { return; } else if (!validator.isNonNullObject(sound)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, + messagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload.aps.sound must be a non-empty string or a non-null object'); } if (!validator.isNonEmptyString(sound.name)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, + messagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload.aps.sound.name must be a non-empty string'); } const volume = sound.volume; if (typeof volume !== 'undefined') { if (!validator.isNumber(volume)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, + messagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload.aps.sound.volume must be a number'); } if (volume < 0 || volume > 1) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, + messagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload.aps.sound.volume must be in the interval [0, 1]'); } } @@ -353,7 +353,7 @@ function validateApsAlert(alert: string | ApsAlert | undefined): void { return; } else if (!validator.isNonNullObject(alert)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, + messagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload.aps.alert must be a string or a non-null object'); } @@ -361,19 +361,19 @@ function validateApsAlert(alert: string | ApsAlert | undefined): void { if (validator.isNonEmptyArray(apsAlert.locArgs) && !validator.isNonEmptyString(apsAlert.locKey)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, + messagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload.aps.alert.locKey is required when specifying locArgs'); } if (validator.isNonEmptyArray(apsAlert.titleLocArgs) && !validator.isNonEmptyString(apsAlert.titleLocKey)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, + messagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload.aps.alert.titleLocKey is required when specifying titleLocArgs'); } if (validator.isNonEmptyArray(apsAlert.subtitleLocArgs) && !validator.isNonEmptyString(apsAlert.subtitleLocKey)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, + messagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload.aps.alert.subtitleLocKey is required when specifying subtitleLocArgs'); } @@ -402,13 +402,13 @@ function validateAndroidConfig(config: AndroidConfig | undefined): void { return; } else if (!validator.isNonNullObject(config)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'android must be a non-null object'); + messagingClientErrorCode.INVALID_PAYLOAD, 'android must be a non-null object'); } if (typeof config.ttl !== 'undefined') { if (!validator.isNumber(config.ttl) || config.ttl < 0) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, + messagingClientErrorCode.INVALID_PAYLOAD, 'TTL must be a non-negative duration in milliseconds'); } const duration: string = transformMillisecondsToSecondsString(config.ttl); @@ -440,36 +440,36 @@ function validateAndroidNotification(notification: AndroidNotification | undefin return; } else if (!validator.isNonNullObject(notification)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification must be a non-null object'); + messagingClientErrorCode.INVALID_PAYLOAD, 'android.notification must be a non-null object'); } if (typeof notification.color !== 'undefined' && !/^#[0-9a-fA-F]{6}$/.test(notification.color)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.color must be in the form #RRGGBB'); + messagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.color must be in the form #RRGGBB'); } if (validator.isNonEmptyArray(notification.bodyLocArgs) && !validator.isNonEmptyString(notification.bodyLocKey)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, + messagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.bodyLocKey is required when specifying bodyLocArgs'); } if (validator.isNonEmptyArray(notification.titleLocArgs) && !validator.isNonEmptyString(notification.titleLocKey)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, + messagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.titleLocKey is required when specifying titleLocArgs'); } if (typeof notification.imageUrl !== 'undefined' && !validator.isURL(notification.imageUrl)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, + messagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.imageUrl must be a valid URL string'); } if (typeof notification.eventTimestamp !== 'undefined') { if (!(notification.eventTimestamp instanceof Date)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.eventTimestamp must be a valid `Date` object'); + messagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.eventTimestamp must be a valid `Date` object'); } // Convert timestamp to RFC3339 UTC "Zulu" format, example "2014-10-02T15:01:23.045123456Z" const zuluTimestamp = notification.eventTimestamp.toISOString(); @@ -479,14 +479,14 @@ function validateAndroidNotification(notification: AndroidNotification | undefin if (typeof notification.vibrateTimingsMillis !== 'undefined') { if (!validator.isNonEmptyArray(notification.vibrateTimingsMillis)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, + messagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.vibrateTimingsMillis must be a non-empty array of numbers'); } const vibrateTimings: string[] = []; notification.vibrateTimingsMillis.forEach((value) => { if (!validator.isNumber(value) || value < 0) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, + messagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.vibrateTimingsMillis must be non-negative durations in milliseconds'); } const duration = transformMillisecondsToSecondsString(value); @@ -545,12 +545,12 @@ function validateLightSettings(lightSettings?: LightSettings): void { return; } else if (!validator.isNonNullObject(lightSettings)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.lightSettings must be a non-null object'); + messagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.lightSettings must be a non-null object'); } if (!validator.isNumber(lightSettings.lightOnDurationMillis) || lightSettings.lightOnDurationMillis < 0) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, + messagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.lightSettings.lightOnDurationMillis must be a non-negative duration in milliseconds'); } const durationOn = transformMillisecondsToSecondsString(lightSettings.lightOnDurationMillis); @@ -558,7 +558,7 @@ function validateLightSettings(lightSettings?: LightSettings): void { if (!validator.isNumber(lightSettings.lightOffDurationMillis) || lightSettings.lightOffDurationMillis < 0) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, + messagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.lightSettings.lightOffDurationMillis must be a non-negative duration in milliseconds'); } const durationOff = transformMillisecondsToSecondsString(lightSettings.lightOffDurationMillis); @@ -567,14 +567,14 @@ function validateLightSettings(lightSettings?: LightSettings): void { if (!validator.isString(lightSettings.color) || (!/^#[0-9a-fA-F]{6}$/.test(lightSettings.color) && !/^#[0-9a-fA-F]{8}$/.test(lightSettings.color))) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, + messagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.lightSettings.color must be in the form #RRGGBB or #RRGGBBAA format'); } const colorString = lightSettings.color.length === 7 ? lightSettings.color + 'FF' : lightSettings.color; const rgb = /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/i.exec(colorString); if (!rgb || rgb.length < 4) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INTERNAL_ERROR, + messagingClientErrorCode.INTERNAL_ERROR, 'regex to extract rgba values from ' + colorString + ' failed.'); } const color = { @@ -602,11 +602,11 @@ function validateAndroidFcmOptions(fcmOptions: AndroidFcmOptions | undefined): v return; } else if (!validator.isNonNullObject(fcmOptions)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'fcmOptions must be a non-null object'); + messagingClientErrorCode.INVALID_PAYLOAD, 'fcmOptions must be a non-null object'); } if (typeof fcmOptions.analyticsLabel !== 'undefined' && !validator.isString(fcmOptions.analyticsLabel)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_PAYLOAD, 'analyticsLabel must be a string value'); + messagingClientErrorCode.INVALID_PAYLOAD, 'analyticsLabel must be a string value'); } } diff --git a/src/messaging/messaging.ts b/src/messaging/messaging.ts index 05367128f4..f05a4e537d 100644 --- a/src/messaging/messaging.ts +++ b/src/messaging/messaging.ts @@ -17,9 +17,10 @@ import { App } from '../app'; import { deepCopy } from '../utils/deep-copy'; -import { - ErrorInfo, MessagingClientErrorCode, FirebaseMessagingError, FirebaseMessagingSessionError -} from '../utils/error'; +import { + messagingClientErrorCode, FirebaseMessagingError +} from './error'; +import { ErrorInfo } from '../utils/error'; import * as utils from '../utils'; import * as validator from '../utils/validator'; import { validateMessage } from './messaging-internal'; @@ -98,7 +99,7 @@ export class Messaging { constructor(app: App) { if (!validator.isNonNullObject(app) || !('options' in app)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_ARGUMENT, + messagingClientErrorCode.INVALID_ARGUMENT, 'First argument passed to admin.messaging() must be a valid Firebase app instance.', ); } @@ -152,11 +153,11 @@ export class Messaging { validateMessage(copy); if (typeof dryRun !== 'undefined' && !validator.isBoolean(dryRun)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_ARGUMENT, 'dryRun must be a boolean'); + messagingClientErrorCode.INVALID_ARGUMENT, 'dryRun must be a boolean'); } return this.getUrlPath() .then((urlPath) => { - const request: { message: Message; validate_only?: boolean } = { message: copy }; + const request: { message: Message; validate_only?: boolean; } = { message: copy }; if (dryRun) { request.validate_only = true; } @@ -196,73 +197,74 @@ export class Messaging { const copy: Message[] = deepCopy(messages); if (!validator.isNonEmptyArray(copy)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_ARGUMENT, 'messages must be a non-empty array'); + messagingClientErrorCode.INVALID_ARGUMENT, 'messages must be a non-empty array'); } if (copy.length > FCM_MAX_BATCH_SIZE) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_ARGUMENT, + messagingClientErrorCode.INVALID_ARGUMENT, `messages list must not contain more than ${FCM_MAX_BATCH_SIZE} items`); } if (typeof dryRun !== 'undefined' && !validator.isBoolean(dryRun)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_ARGUMENT, 'dryRun must be a boolean'); + messagingClientErrorCode.INVALID_ARGUMENT, 'dryRun must be a boolean'); } const http2SessionHandler = this.useLegacyTransport ? undefined : new Http2SessionHandler(`https://${FCM_SEND_HOST}`); return this.getUrlPath() .then((urlPath) => { - if (http2SessionHandler) { - let sendResponsePromise: Promise[]>; - return new Promise((resolve: (result: PromiseSettledResult[]) => void, reject) => { - // Start session listeners - http2SessionHandler.invoke().catch((error) => { - const pendingBatchResponse = - sendResponsePromise ? sendResponsePromise.then(this.parseSendResponses) : undefined; - reject(new FirebaseMessagingSessionError(error, undefined, pendingBatchResponse)); - }); - - // Start making requests - const requests: Promise[] = copy.map(async (message) => { - validateMessage(message); - const request: { message: Message; validate_only?: boolean; } = { message }; - if (dryRun) { - request.validate_only = true; - } - return this.messagingRequestHandler.invokeHttp2RequestHandlerForSendResponse( - FCM_SEND_HOST, urlPath, request, http2SessionHandler); - }); - - // Resolve once all requests have completed - sendResponsePromise = Promise.allSettled(requests); - sendResponsePromise.then(resolve); - }); - } else { - const requests: Promise[] = copy.map(async (message) => { - validateMessage(message); - const request: { message: Message; validate_only?: boolean; } = { message }; - if (dryRun) { - request.validate_only = true; - } + const requests: Promise[] = copy.map(async (message) => { + validateMessage(message); + const request: { message: Message; validate_only?: boolean; } = { message }; + if (dryRun) { + request.validate_only = true; + } + if (http2SessionHandler) { + return this.messagingRequestHandler.invokeHttp2RequestHandlerForSendResponse( + FCM_SEND_HOST, urlPath, request, http2SessionHandler); + } else { return this.messagingRequestHandler.invokeHttpRequestHandlerForSendResponse( FCM_SEND_HOST, urlPath, request); - }); - return Promise.allSettled(requests); - } + } + }); + return Promise.allSettled(requests); + }) + .then((results) => { + const sessionErrors = http2SessionHandler ? http2SessionHandler.getErrors() : []; + return this.parseSendResponses(results, sessionErrors); }) - .then(this.parseSendResponses) .finally(() => { http2SessionHandler?.close(); }); } - private parseSendResponses(results: PromiseSettledResult[]): BatchResponse { + private parseSendResponses( + results: PromiseSettledResult[], + sessionErrors: Error[] = [] + ): BatchResponse { const responses: SendResponse[] = []; results.forEach(result => { if (result.status === 'fulfilled') { responses.push(result.value); } else { // rejected - responses.push({ success: false, error: result.reason }); + let error = result.reason; + if (sessionErrors.length > 0) { + // Combine the original stream error and all session errors + const allErrors = [result.reason, ...sessionErrors]; + // TODO: AggregateError is supported in Node 18+ but only included in the ES2021+ + // We use (global as any).AggregateError as a workaround to access it in ES2020. + const cause = new (global as any).AggregateError(allErrors, 'Stream failure and session failures occurred'); + + const streamMessage = result.reason.message || 'Unknown stream error'; + const sessionMessage = `. Session failures: ${sessionErrors.map(e => e.message).join(', ')}`; + + error = new FirebaseMessagingError({ + code: messagingClientErrorCode.UNKNOWN_ERROR.code, + message: `${streamMessage}${sessionMessage}`, + cause: cause + }); + } + responses.push({ success: false, error }); } }); const successCount: number = responses.filter((resp) => resp.success).length; @@ -295,15 +297,15 @@ export class Messaging { const copy: MulticastMessage = deepCopy(message); if (!validator.isNonNullObject(copy)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_ARGUMENT, 'MulticastMessage must be a non-null object'); + messagingClientErrorCode.INVALID_ARGUMENT, 'MulticastMessage must be a non-null object'); } if (!validator.isNonEmptyArray(copy.tokens)) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_ARGUMENT, 'tokens must be a non-empty array'); + messagingClientErrorCode.INVALID_ARGUMENT, 'tokens must be a non-empty array'); } if (copy.tokens.length > FCM_MAX_BATCH_SIZE) { throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_ARGUMENT, + messagingClientErrorCode.INVALID_ARGUMENT, `tokens list must not contain more than ${FCM_MAX_BATCH_SIZE} items`); } @@ -385,7 +387,7 @@ export class Messaging { if (!validator.isNonEmptyString(projectId)) { // Assert for an explicit project ID (either via AppOptions or the cert itself). throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_ARGUMENT, + messagingClientErrorCode.INVALID_ARGUMENT, 'Failed to determine project ID for Messaging. Initialize the ' + 'SDK with service account credentials or set project ID as an app option. ' + 'Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.', @@ -458,7 +460,7 @@ export class Messaging { private validateRegistrationTokensType( registrationTokenOrTokens: string | string[], methodName: string, - errorInfo: ErrorInfo = MessagingClientErrorCode.INVALID_ARGUMENT, + errorInfo: ErrorInfo = messagingClientErrorCode.INVALID_ARGUMENT, ): void { if (!validator.isNonEmptyArray(registrationTokenOrTokens) && !validator.isNonEmptyString(registrationTokenOrTokens)) { @@ -481,7 +483,7 @@ export class Messaging { private validateRegistrationTokens( registrationTokenOrTokens: string | string[], methodName: string, - errorInfo: ErrorInfo = MessagingClientErrorCode.INVALID_ARGUMENT, + errorInfo: ErrorInfo = messagingClientErrorCode.INVALID_ARGUMENT, ): void { if (validator.isArray(registrationTokenOrTokens)) { // Validate the array contains no more than 1,000 registration tokens. @@ -516,7 +518,7 @@ export class Messaging { private validateTopicType( topic: string | string[], methodName: string, - errorInfo: ErrorInfo = MessagingClientErrorCode.INVALID_ARGUMENT, + errorInfo: ErrorInfo = messagingClientErrorCode.INVALID_ARGUMENT, ): void { if (!validator.isNonEmptyString(topic)) { throw new FirebaseMessagingError( @@ -537,7 +539,7 @@ export class Messaging { private validateTopic( topic: string, methodName: string, - errorInfo: ErrorInfo = MessagingClientErrorCode.INVALID_ARGUMENT, + errorInfo: ErrorInfo = messagingClientErrorCode.INVALID_ARGUMENT, ): void { if (!validator.isTopic(topic)) { throw new FirebaseMessagingError( diff --git a/src/phone-number-verification/error.ts b/src/phone-number-verification/error.ts new file mode 100644 index 0000000000..898f890005 --- /dev/null +++ b/src/phone-number-verification/error.ts @@ -0,0 +1,46 @@ +/*! + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PrefixedFirebaseError, ErrorInfo } from '../utils/error'; + +/** + * The constant mapping for valid Phone Number Verification client error codes. + */ +export const PhoneNumberVerificationErrorCode = { + INVALID_ARGUMENT: 'invalid-argument', + INVALID_TOKEN: 'invalid-token', + EXPIRED_TOKEN: 'expired-token', +} as const; + +/** + * The type definition for valid Phone Number Verification client error codes. + */ +export type PhoneNumberVerificationErrorCode = + typeof PhoneNumberVerificationErrorCode[keyof typeof PhoneNumberVerificationErrorCode]; + +export const FPNV_ERROR_CODE_MAPPING = PhoneNumberVerificationErrorCode; + +/** + * Firebase Phone Number Verification error code structure. This extends `PrefixedFirebaseError`. + * + * @param info - The error code info. + * @param message - The error message. If provided, this will override the default message. + */ +export class FirebasePhoneNumberVerificationError extends PrefixedFirebaseError { + constructor(info: ErrorInfo, message?: string) { + super('phone-number-verification', info.code, message || info.message, info.httpResponse, info.cause); + } +} diff --git a/src/phone-number-verification/index.ts b/src/phone-number-verification/index.ts index 2d2de73f8a..f27db5700c 100644 --- a/src/phone-number-verification/index.ts +++ b/src/phone-number-verification/index.ts @@ -64,3 +64,8 @@ export function getPhoneNumberVerification(app?: App): PhoneNumberVerification { const firebaseApp: FirebaseApp = app as FirebaseApp; return firebaseApp.getOrInitService('phone-number-verification', (app) => new PhoneNumberVerification(app)); } + +export { + FirebasePhoneNumberVerificationError, + PhoneNumberVerificationErrorCode, +} from './error'; diff --git a/src/phone-number-verification/phone-number-verification-api-client-internal.ts b/src/phone-number-verification/phone-number-verification-api-client-internal.ts index be0a372f33..484f8e8a33 100644 --- a/src/phone-number-verification/phone-number-verification-api-client-internal.ts +++ b/src/phone-number-verification/phone-number-verification-api-client-internal.ts @@ -15,8 +15,6 @@ * limitations under the License. */ -import { PrefixedFirebaseError } from '../utils/error'; - export interface FirebasePhoneNumberTokenInfo { /** Documentation URL. */ url: string; @@ -39,34 +37,3 @@ export const FPNV_TOKEN_INFO: FirebasePhoneNumberTokenInfo = { shortName: 'FPNV token', typ: 'JWT', }; - -export const FPNV_ERROR_CODE_MAPPING = { - INVALID_ARGUMENT: 'invalid-argument', - INVALID_TOKEN: 'invalid-token', - EXPIRED_TOKEN: 'expired-token', -} satisfies Record; - -export type PhoneNumberVerificationErrorCode = - | 'invalid-argument' - | 'invalid-token' - | 'expired-token' - -/** - * Firebase Phone Number Verification error code structure. This extends `PrefixedFirebaseError`. - * - * @param code - The error code. - * @param message - The error message. - * @constructor - */ -export class FirebasePhoneNumberVerificationError extends PrefixedFirebaseError { - constructor(code: PhoneNumberVerificationErrorCode, message: string) { - super('phone-number-verification', code, message); - - /* tslint:disable:max-line-length */ - // Set the prototype explicitly. See the following link for more details: - // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work - /* tslint:enable:max-line-length */ - (this as any).__proto__ = FirebasePhoneNumberVerificationError.prototype; - } -} - diff --git a/src/phone-number-verification/phone-number-verification-api.ts b/src/phone-number-verification/phone-number-verification-api.ts index e3c937b728..a3527d78d6 100644 --- a/src/phone-number-verification/phone-number-verification-api.ts +++ b/src/phone-number-verification/phone-number-verification-api.ts @@ -82,5 +82,5 @@ export interface PhoneNumberVerificationToken { export { PhoneNumberVerificationErrorCode, FirebasePhoneNumberVerificationError, -} from './phone-number-verification-api-client-internal'; +} from './error'; diff --git a/src/phone-number-verification/token-verifier.ts b/src/phone-number-verification/token-verifier.ts index 706a99d68c..3eda268599 100644 --- a/src/phone-number-verification/token-verifier.ts +++ b/src/phone-number-verification/token-verifier.ts @@ -23,7 +23,8 @@ import { DecodedToken, decodeJwt, JwtError, JwtErrorCode, PublicKeySignatureVerifier, ALGORITHM_ES256, SignatureVerifier, } from '../utils/jwt'; -import { FirebasePhoneNumberTokenInfo, FPNV_ERROR_CODE_MAPPING } from './phone-number-verification-api-client-internal'; +import { FirebasePhoneNumberTokenInfo } from './phone-number-verification-api-client-internal'; +import { FPNV_ERROR_CODE_MAPPING } from './error'; export class PhoneNumberTokenVerifier { private readonly shortNameArticle: string; @@ -37,40 +38,40 @@ export class PhoneNumberTokenVerifier { ) { if (!validator.isURL(jwksUrl)) { - throw new FirebasePhoneNumberVerificationError( - FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, - 'The provided public client certificate URL is an invalid URL.', - ); + throw new FirebasePhoneNumberVerificationError({ + code: FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + message: 'The provided public client certificate URL is an invalid URL.', + }); } else if (!validator.isURL(issuer)) { - throw new FirebasePhoneNumberVerificationError( - FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, - 'The provided JWT issuer is an invalid URL.', - ); + throw new FirebasePhoneNumberVerificationError({ + code: FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + message: 'The provided JWT issuer is an invalid URL.', + }); } else if (!validator.isNonNullObject(tokenInfo)) { - throw new FirebasePhoneNumberVerificationError( - FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, - 'The provided JWT information is not an object or null.', - ); + throw new FirebasePhoneNumberVerificationError({ + code: FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + message: 'The provided JWT information is not an object or null.', + }); } else if (!validator.isURL(tokenInfo.url)) { - throw new FirebasePhoneNumberVerificationError( - FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, - 'The provided JWT verification documentation URL is invalid.', - ); + throw new FirebasePhoneNumberVerificationError({ + code: FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + message: 'The provided JWT verification documentation URL is invalid.', + }); } else if (!validator.isNonEmptyString(tokenInfo.verifyApiName)) { - throw new FirebasePhoneNumberVerificationError( - FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, - 'The JWT verify API name must be a non-empty string.', - ); + throw new FirebasePhoneNumberVerificationError({ + code: FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + message: 'The JWT verify API name must be a non-empty string.', + }); } else if (!validator.isNonEmptyString(tokenInfo.jwtName)) { - throw new FirebasePhoneNumberVerificationError( - FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, - 'The JWT public full name must be a non-empty string.', - ); + throw new FirebasePhoneNumberVerificationError({ + code: FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + message: 'The JWT public full name must be a non-empty string.', + }); } else if (!validator.isNonEmptyString(tokenInfo.shortName)) { - throw new FirebasePhoneNumberVerificationError( - FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, - 'The JWT public short name must be a non-empty string.', - ); + throw new FirebasePhoneNumberVerificationError({ + code: FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + message: 'The JWT public short name must be a non-empty string.', + }); } this.shortNameArticle = tokenInfo.shortName.charAt(0).match(/[aeiou]/i) ? 'an' : 'a'; @@ -82,10 +83,10 @@ export class PhoneNumberTokenVerifier { public async verifyJWT(jwtToken: string): Promise { if (!validator.isString(jwtToken)) { - throw new FirebasePhoneNumberVerificationError( - FPNV_ERROR_CODE_MAPPING.INVALID_TOKEN, - `First argument to ${this.tokenInfo.verifyApiName} must be a string.`, - ); + throw new FirebasePhoneNumberVerificationError({ + code: FPNV_ERROR_CODE_MAPPING.INVALID_TOKEN, + message: `First argument to ${this.tokenInfo.verifyApiName} must be a string.`, + }); } const projectId = await this.ensureProjectId(); @@ -99,10 +100,11 @@ export class PhoneNumberTokenVerifier { private async ensureProjectId(): Promise { const projectId = await util.findProjectId(this.app); if (!validator.isNonEmptyString(projectId)) { - throw new FirebasePhoneNumberVerificationError( - FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, - 'Must initialize app with a cert credential or set your Firebase project ID as the ' + - `GOOGLE_CLOUD_PROJECT environment variable to call ${this.tokenInfo.verifyApiName}.`); + throw new FirebasePhoneNumberVerificationError({ + code: FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + message: 'Must initialize app with a cert credential or set your Firebase project ID as the ' + + `GOOGLE_CLOUD_PROJECT environment variable to call ${this.tokenInfo.verifyApiName}.` + }); } return projectId; @@ -121,17 +123,22 @@ export class PhoneNumberTokenVerifier { private async safeDecode(jwtToken: string): Promise { try { return await decodeJwt(jwtToken); - } catch (err) { + } catch (err: any) { if (err.code === JwtErrorCode.INVALID_ARGUMENT) { const verifyJwtTokenDocsMessage = ` See ${this.tokenInfo.url} ` + `for details on how to retrieve ${this.shortNameArticle} ${this.tokenInfo.shortName}.`; const errorMessage = `Decoding ${this.tokenInfo.jwtName} failed. Make sure you passed ` + `the entire string JWT which represents ${this.shortNameArticle} ` + `${this.tokenInfo.shortName}.` + verifyJwtTokenDocsMessage; - throw new FirebasePhoneNumberVerificationError(FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, - errorMessage); + throw new FirebasePhoneNumberVerificationError({ + code: FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + message: errorMessage, + }); } - throw new FirebasePhoneNumberVerificationError(FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, err.message); + throw new FirebasePhoneNumberVerificationError({ + code: FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + message: err.message, + }); } } @@ -179,14 +186,17 @@ export class PhoneNumberTokenVerifier { } if (errorMessage) { - throw new FirebasePhoneNumberVerificationError(FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, errorMessage); + throw new FirebasePhoneNumberVerificationError({ + code: FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + message: errorMessage, + }); } } private async verifySignature(jwtToken: string): Promise { try { return await this.signatureVerifier.verify(jwtToken); - } catch (error) { + } catch (error: any) { throw this.mapJwtErrorToAuthError(error); } } @@ -197,17 +207,29 @@ export class PhoneNumberTokenVerifier { if (error.code === JwtErrorCode.TOKEN_EXPIRED) { const errorMessage = `${this.tokenInfo.jwtName} has expired. Get a fresh ${this.tokenInfo.shortName}` + ` from your client app and try again. ${verifyJwtTokenDocsMessage}`; - return new FirebasePhoneNumberVerificationError(FPNV_ERROR_CODE_MAPPING.EXPIRED_TOKEN, errorMessage); + return new FirebasePhoneNumberVerificationError({ + code: FPNV_ERROR_CODE_MAPPING.EXPIRED_TOKEN, + message: errorMessage, + }); } else if (error.code === JwtErrorCode.INVALID_SIGNATURE) { const errorMessage = `${this.tokenInfo.jwtName} has invalid signature. ${verifyJwtTokenDocsMessage}`; - return new FirebasePhoneNumberVerificationError(FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, errorMessage); + return new FirebasePhoneNumberVerificationError({ + code: FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + message: errorMessage, + }); } else if (error.code === JwtErrorCode.NO_MATCHING_KID) { const errorMessage = `${this.tokenInfo.jwtName} has "kid" claim which does not ` + `correspond to a known public key. Most likely the ${this.tokenInfo.shortName} ` + 'is expired, so get a fresh token from your client app and try again.'; - return new FirebasePhoneNumberVerificationError(FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, errorMessage); + return new FirebasePhoneNumberVerificationError({ + code: FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + message: errorMessage, + }); } - return new FirebasePhoneNumberVerificationError(FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, error.message); + return new FirebasePhoneNumberVerificationError({ + code: FPNV_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + message: error.message, + }); } } diff --git a/src/project-management/android-app.ts b/src/project-management/android-app.ts index e4e503e946..0d28d0afd5 100644 --- a/src/project-management/android-app.ts +++ b/src/project-management/android-app.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { FirebaseProjectManagementError } from '../utils/error'; +import { FirebaseProjectManagementError } from './error'; import * as validator from '../utils/validator'; import { ProjectManagementRequestHandler, assertServerResponse } from './project-management-api-request-internal'; import { AppMetadata, AppPlatform } from './app-metadata'; @@ -55,8 +55,10 @@ export class AndroidApp { public readonly appId: string, private readonly requestHandler: ProjectManagementRequestHandler) { if (!validator.isNonEmptyString(appId)) { - throw new FirebaseProjectManagementError( - 'invalid-argument', 'appId must be a non-empty string.'); + throw new FirebaseProjectManagementError({ + code: 'invalid-argument', + message: 'appId must be a non-empty string.' + }); } this.resourceName = `projects/-/androidApps/${appId}`; @@ -167,10 +169,11 @@ export class AndroidApp { */ public deleteShaCertificate(certificateToDelete: ShaCertificate): Promise { if (!certificateToDelete.resourceName) { - throw new FirebaseProjectManagementError( - 'invalid-argument', - 'Specified certificate does not include a resourceName. (Use AndroidApp.getShaCertificates() to retrieve ' + - 'certificates with a resourceName.'); + throw new FirebaseProjectManagementError({ + code: 'invalid-argument', + message: 'Specified certificate does not include a resourceName. (Use AndroidApp.getShaCertificates() to ' + + 'retrieve certificates with a resourceName.' + }); } return this.requestHandler.deleteResource(certificateToDelete.resourceName); } @@ -243,8 +246,10 @@ export class ShaCertificate { } else if (/^[a-fA-F0-9]{64}$/.test(shaHash)) { this.certType = 'sha256'; } else { - throw new FirebaseProjectManagementError( - 'invalid-argument', 'shaHash must be either a sha256 hash or a sha1 hash.'); + throw new FirebaseProjectManagementError({ + code: 'invalid-argument', + message: 'shaHash must be either a sha256 hash or a sha1 hash.' + }); } } } diff --git a/src/project-management/error.ts b/src/project-management/error.ts new file mode 100644 index 0000000000..7c734372de --- /dev/null +++ b/src/project-management/error.ts @@ -0,0 +1,51 @@ +/*! + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PrefixedFirebaseError, ErrorInfo } from '../utils/error'; + +/** + * The constant mapping for valid Project Management client error codes. + */ +export const ProjectManagementErrorCode = { + ALREADY_EXISTS: 'already-exists', + AUTHENTICATION_ERROR: 'authentication-error', + INTERNAL_ERROR: 'internal-error', + INVALID_ARGUMENT: 'invalid-argument', + INVALID_PROJECT_ID: 'invalid-project-id', + INVALID_SERVER_RESPONSE: 'invalid-server-response', + NOT_FOUND: 'not-found', + SERVICE_UNAVAILABLE: 'service-unavailable', + UNKNOWN_ERROR: 'unknown-error', +} as const; + +/** + * The type definition for valid Project Management client error codes. + */ +export type ProjectManagementErrorCode = typeof ProjectManagementErrorCode[keyof typeof ProjectManagementErrorCode]; + +/** + * Firebase project management error code structure. This extends PrefixedFirebaseError. + */ +export class FirebaseProjectManagementError extends PrefixedFirebaseError { + /** + * @param info - The error code info. + * @param message - The error message. This will override the default message if provided. + */ + constructor(info: ErrorInfo, message?: string) { + // Override default message if custom message provided. + super('project-management', info.code, message || info.message, info.httpResponse, info.cause); + } +} diff --git a/src/project-management/index.ts b/src/project-management/index.ts index 87a6d570ce..d60da77f53 100644 --- a/src/project-management/index.ts +++ b/src/project-management/index.ts @@ -63,4 +63,4 @@ export function getProjectManagement(app?: App): ProjectManagement { return firebaseApp.getOrInitService('projectManagement', (app) => new ProjectManagement(app)); } -export { FirebaseProjectManagementError, ProjectManagementErrorCode } from '../utils/error'; +export { FirebaseProjectManagementError, ProjectManagementErrorCode } from './error'; diff --git a/src/project-management/ios-app.ts b/src/project-management/ios-app.ts index 775d19a120..b969110b09 100644 --- a/src/project-management/ios-app.ts +++ b/src/project-management/ios-app.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { FirebaseProjectManagementError } from '../utils/error'; +import { FirebaseProjectManagementError } from './error'; import * as validator from '../utils/validator'; import { ProjectManagementRequestHandler, assertServerResponse } from './project-management-api-request-internal'; import { AppMetadata, AppPlatform } from './app-metadata'; @@ -52,8 +52,10 @@ export class IosApp { public readonly appId: string, private readonly requestHandler: ProjectManagementRequestHandler) { if (!validator.isNonEmptyString(appId)) { - throw new FirebaseProjectManagementError( - 'invalid-argument', 'appId must be a non-empty string.'); + throw new FirebaseProjectManagementError({ + code: 'invalid-argument', + message: 'appId must be a non-empty string.' + }); } this.resourceName = `projects/-/iosApps/${appId}`; diff --git a/src/project-management/project-management-api-request-internal.ts b/src/project-management/project-management-api-request-internal.ts index bc502a5ebd..588586a94e 100644 --- a/src/project-management/project-management-api-request-internal.ts +++ b/src/project-management/project-management-api-request-internal.ts @@ -19,7 +19,10 @@ import { FirebaseApp } from '../app/firebase-app'; import { AuthorizedHttpClient, RequestResponseError, HttpMethod, HttpRequestConfig, ExponentialBackoffPoller, } from '../utils/api-request'; -import { FirebaseProjectManagementError, ProjectManagementErrorCode } from '../utils/error'; +import { + FirebaseProjectManagementError, ProjectManagementErrorCode, +} from './error'; +import { HttpResponse, toHttpResponse } from '../utils/error'; import { getSdkVersion } from '../utils/index'; import * as validator from '../utils/validator'; import { ShaCertificate } from './android-app'; @@ -48,9 +51,10 @@ const CERT_TYPE_API_MAP = { export function assertServerResponse( condition: boolean, responseData: object, message: string): void { if (!condition) { - throw new FirebaseProjectManagementError( - 'invalid-server-response', - `${message} Response data: ${JSON.stringify(responseData, null, 2)}`); + throw new FirebaseProjectManagementError({ + code: 'invalid-server-response', + message: `${message} Response data: ${JSON.stringify(responseData, null, 2)}` + }); } } @@ -67,7 +71,7 @@ export class ProjectManagementRequestHandler { `https://${PROJECT_MANAGEMENT_HOST_AND_PORT}${PROJECT_MANAGEMENT_BETA_PATH}`; private readonly httpClient: AuthorizedHttpClient; - private static wrapAndRethrowHttpError(errStatusCode: number, errText?: string): void { + private static wrapAndRethrowHttpError(errStatusCode: number, errText?: string, httpResponse?: HttpResponse): void { let errorCode: ProjectManagementErrorCode; let errorMessage: string; @@ -108,9 +112,15 @@ export class ProjectManagementRequestHandler { if (!errText) { errText = ''; } - throw new FirebaseProjectManagementError( - errorCode, - `${ errorMessage } Status code: ${ errStatusCode }. Raw server response: "${ errText }".`); + throw new FirebaseProjectManagementError({ + code: errorCode, + message: errorMessage, + httpResponse: httpResponse || { + status: errStatusCode, + headers: {}, + data: errText, + }, + }); } /** @@ -330,7 +340,7 @@ export class ProjectManagementRequestHandler { .catch((err) => { if (err instanceof RequestResponseError) { ProjectManagementRequestHandler.wrapAndRethrowHttpError( - err.response.status, err.response.text); + err.response.status, err.response.text, toHttpResponse(err.response)); } throw err; }); diff --git a/src/project-management/project-management.ts b/src/project-management/project-management.ts index c29589be86..e3380b6527 100644 --- a/src/project-management/project-management.ts +++ b/src/project-management/project-management.ts @@ -15,7 +15,7 @@ */ import { App } from '../app'; -import { FirebaseProjectManagementError } from '../utils/error'; +import { FirebaseProjectManagementError } from './error'; import * as utils from '../utils/index'; import * as validator from '../utils/validator'; import { AndroidApp, ShaCertificate } from './android-app'; @@ -38,10 +38,11 @@ export class ProjectManagement { */ constructor(readonly app: App) { if (!validator.isNonNullObject(app) || !('options' in app)) { - throw new FirebaseProjectManagementError( - 'invalid-argument', - 'First argument passed to admin.projectManagement() must be a valid Firebase app ' - + 'instance.'); + throw new FirebaseProjectManagementError({ + code: 'invalid-argument', + message: 'First argument passed to admin.projectManagement() must be a valid Firebase app ' + + 'instance.' + }); } this.requestHandler = new ProjectManagementRequestHandler(app); @@ -240,11 +241,12 @@ export class ProjectManagement { .then((projectId) => { // Assert that a specific project ID was provided within the app. if (!validator.isNonEmptyString(projectId)) { - throw new FirebaseProjectManagementError( - 'invalid-project-id', - 'Failed to determine project ID. Initialize the SDK with service account credentials, or ' + throw new FirebaseProjectManagementError({ + code: 'invalid-project-id', + message: 'Failed to determine project ID. Initialize the SDK with service account credentials, or ' + 'set project ID as an app option. Alternatively, set the GOOGLE_CLOUD_PROJECT ' - + 'environment variable.'); + + 'environment variable.' + }); } this.projectId = projectId; diff --git a/src/remote-config/error.ts b/src/remote-config/error.ts new file mode 100644 index 0000000000..ae25bce881 --- /dev/null +++ b/src/remote-config/error.ts @@ -0,0 +1,67 @@ +/*! + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PrefixedFirebaseError, ErrorInfo } from '../utils/error'; + +/** @const {Record} Remote Config server to client error code mapping. */ +export const ERROR_CODE_MAPPING: Record = { + ABORTED: 'aborted', + ALREADY_EXISTS: 'already-exists', + INVALID_ARGUMENT: 'invalid-argument', + INTERNAL: 'internal-error', + FAILED_PRECONDITION: 'failed-precondition', + NOT_FOUND: 'not-found', + OUT_OF_RANGE: 'out-of-range', + PERMISSION_DENIED: 'permission-denied', + RESOURCE_EXHAUSTED: 'resource-exhausted', + UNAUTHENTICATED: 'unauthenticated', + UNKNOWN: 'unknown-error', +}; + +/** + * The constant mapping for valid Remote Config client error codes. + */ +export const RemoteConfigErrorCode = { + ABORTED: 'aborted', + ALREADY_EXISTS: 'already-exists', + FAILED_PRECONDITION: 'failed-precondition', + INTERNAL_ERROR: 'internal-error', + INVALID_ARGUMENT: 'invalid-argument', + NOT_FOUND: 'not-found', + OUT_OF_RANGE: 'out-of-range', + PERMISSION_DENIED: 'permission-denied', + RESOURCE_EXHAUSTED: 'resource-exhausted', + UNAUTHENTICATED: 'unauthenticated', + UNKNOWN_ERROR: 'unknown-error', +} as const; + +/** + * The type definition for valid Remote Config client error codes. + */ +export type RemoteConfigErrorCode = typeof RemoteConfigErrorCode[keyof typeof RemoteConfigErrorCode]; + +/** + * Firebase Remote Config error code structure. This extends PrefixedFirebaseError. + */ +export class FirebaseRemoteConfigError extends PrefixedFirebaseError { + /** + * @param info - The error code info. + * @param message - The error message. If provided, this will override the default message. + */ + constructor(info: ErrorInfo, message?: string) { + super('remote-config', info.code, message || info.message, info.httpResponse, info.cause); + } +} diff --git a/src/remote-config/index.ts b/src/remote-config/index.ts index e44606f193..94a74bbffe 100644 --- a/src/remote-config/index.ts +++ b/src/remote-config/index.ts @@ -106,3 +106,5 @@ export function getRemoteConfig(app?: App): RemoteConfig { const firebaseApp: FirebaseApp = app as FirebaseApp; return firebaseApp.getOrInitService('remoteConfig', (app) => new RemoteConfig(app)); } + +export { FirebaseRemoteConfigError, RemoteConfigErrorCode } from './error'; \ No newline at end of file diff --git a/src/remote-config/remote-config-api-client-internal.ts b/src/remote-config/remote-config-api-client-internal.ts index 199c974c7d..4898491096 100644 --- a/src/remote-config/remote-config-api-client-internal.ts +++ b/src/remote-config/remote-config-api-client-internal.ts @@ -16,10 +16,11 @@ import { App } from '../app'; import { FirebaseApp } from '../app/firebase-app'; -import { - HttpRequestConfig, HttpClient, RequestResponseError, AuthorizedHttpClient,RequestResponse +import { + HttpRequestConfig, HttpClient, RequestResponseError, AuthorizedHttpClient, RequestResponse } from '../utils/api-request'; -import { PrefixedFirebaseError } from '../utils/error'; +import { PrefixedFirebaseError, toHttpResponse } from '../utils/error'; +import { FirebaseRemoteConfigError, RemoteConfigErrorCode, ERROR_CODE_MAPPING } from './error'; import * as utils from '../utils/index'; import * as validator from '../utils/validator'; import { deepCopy } from '../utils/deep-copy'; @@ -57,9 +58,10 @@ export class RemoteConfigApiClient { constructor(private readonly app: App) { if (!validator.isNonNullObject(app) || !('options' in app)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - 'First argument passed to admin.remoteConfig() must be a valid Firebase app instance.'); + throw new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: 'First argument passed to admin.remoteConfig() must be a valid Firebase app instance.' + }); } this.httpClient = new AuthorizedHttpClient(app as FirebaseApp); @@ -118,7 +120,7 @@ export class RemoteConfigApiClient { }); } - public publishTemplate(template: RemoteConfigTemplate, options?: { force: boolean }): Promise { + public publishTemplate(template: RemoteConfigTemplate, options?: { force: boolean; }): Promise { template = this.validateInputRemoteConfigTemplate(template); let ifMatch: string = template.etag; if (options && options.force === true) { @@ -236,11 +238,12 @@ export class RemoteConfigApiClient { return utils.findProjectId(this.app) .then((projectId) => { if (!validator.isNonEmptyString(projectId)) { - throw new FirebaseRemoteConfigError( - 'unknown-error', - 'Failed to determine project ID. Initialize the SDK with service account credentials, or ' - + 'set project ID as an app option. Alternatively, set the GOOGLE_CLOUD_PROJECT ' - + 'environment variable.'); + throw new FirebaseRemoteConfigError({ + code: 'unknown-error', + message: 'Failed to determine project ID. Initialize the SDK with service account credentials, or ' + + 'set project ID as an app option. Alternatively, set the GOOGLE_CLOUD_PROJECT ' + + 'environment variable.' + }); } this.projectIdPrefix = `projects/${projectId}`; @@ -255,18 +258,21 @@ export class RemoteConfigApiClient { const response = err.response; if (!response.isJson()) { - return new FirebaseRemoteConfigError( - 'unknown-error', - `Unexpected response with status: ${response.status} and body: ${response.text}`); + return new FirebaseRemoteConfigError({ + code: 'unknown-error', + message: `Unexpected response with status: ${response.status} and body: ${response.text}`, + httpResponse: toHttpResponse(response), + cause: err + }); } - const error: Error = (response.data as ErrorResponse).error || {}; + const error: RemoteConfigApiError = (response.data as ErrorResponse).error || {}; let code: RemoteConfigErrorCode = 'unknown-error'; if (error.status && error.status in ERROR_CODE_MAPPING) { code = ERROR_CODE_MAPPING[error.status]; } - const message = error.message || `Unknown server error: ${response.text}`; - return new FirebaseRemoteConfigError(code, message); + const message = error.message || 'Unknown server error'; + return new FirebaseRemoteConfigError({ code, message, httpResponse: toHttpResponse(response), cause: err }); } /** @@ -318,29 +324,34 @@ export class RemoteConfigApiClient { private validateInputRemoteConfigTemplate(template: RemoteConfigTemplate): RemoteConfigTemplate { const templateCopy = deepCopy(template); if (!validator.isNonNullObject(templateCopy)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - `Invalid Remote Config template: ${JSON.stringify(templateCopy)}`); + throw new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: `Invalid Remote Config template: ${JSON.stringify(templateCopy)}` + }); } if (!validator.isNonEmptyString(templateCopy.etag)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - 'ETag must be a non-empty string.'); + throw new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: 'ETag must be a non-empty string.' + }); } if (!validator.isNonNullObject(templateCopy.parameters)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - 'Remote Config parameters must be a non-null object'); + throw new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: 'Remote Config parameters must be a non-null object' + }); } if (!validator.isNonNullObject(templateCopy.parameterGroups)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - 'Remote Config parameter groups must be a non-null object'); + throw new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: 'Remote Config parameter groups must be a non-null object' + }); } if (!validator.isArray(templateCopy.conditions)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - 'Remote Config conditions must be an array'); + throw new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: 'Remote Config conditions must be an array' + }); } if (typeof templateCopy.version !== 'undefined') { // exclude output only properties and keep the only input property: description @@ -361,23 +372,26 @@ export class RemoteConfigApiClient { private validateVersionNumber(versionNumber: string | number, propertyName = 'versionNumber'): string { if (!validator.isNonEmptyString(versionNumber) && !validator.isNumber(versionNumber)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - `${propertyName} must be a non-empty string in int64 format or a number`); + throw new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: `${propertyName} must be a non-empty string in int64 format or a number` + }); } if (!Number.isInteger(Number(versionNumber))) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - `${propertyName} must be an integer or a string in int64 format`); + throw new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: `${propertyName} must be an integer or a string in int64 format` + }); } return versionNumber.toString(); } private validateEtag(etag?: string): void { if (!validator.isNonEmptyString(etag)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - 'ETag header is not present in the server response.'); + throw new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: 'ETag header is not present in the server response.' + }); } } @@ -393,31 +407,40 @@ export class RemoteConfigApiClient { private validateListVersionsOptions(options: ListVersionsOptions): ListVersionsOptions { const optionsCopy = deepCopy(options); if (!validator.isNonNullObject(optionsCopy)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - 'ListVersionsOptions must be a non-null object.'); + throw new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: 'ListVersionsOptions must be a non-null object.' + }); } if (typeof optionsCopy.pageSize !== 'undefined') { if (!validator.isNumber(optionsCopy.pageSize)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', 'pageSize must be a number.'); + throw new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: 'pageSize must be a number.' + }); } if (optionsCopy.pageSize < 1 || optionsCopy.pageSize > 300) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', 'pageSize must be a number between 1 and 300 (inclusive).'); + throw new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: 'pageSize must be a number between 1 and 300 (inclusive).' + }); } } if (typeof optionsCopy.pageToken !== 'undefined' && !validator.isNonEmptyString(optionsCopy.pageToken)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', 'pageToken must be a string value.'); + throw new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: 'pageToken must be a string value.' + }); } if (typeof optionsCopy.endVersionNumber !== 'undefined') { optionsCopy.endVersionNumber = this.validateVersionNumber(optionsCopy.endVersionNumber, 'endVersionNumber'); } if (typeof optionsCopy.startTime !== 'undefined') { if (!(optionsCopy.startTime instanceof Date) && !validator.isUTCDateString(optionsCopy.startTime)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', 'startTime must be a valid Date object or a UTC date string.'); + throw new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: 'startTime must be a valid Date object or a UTC date string.' + }); } // Convert startTime to RFC3339 UTC "Zulu" format. if (optionsCopy.startTime instanceof Date) { @@ -428,8 +451,10 @@ export class RemoteConfigApiClient { } if (typeof optionsCopy.endTime !== 'undefined') { if (!(optionsCopy.endTime instanceof Date) && !validator.isUTCDateString(optionsCopy.endTime)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', 'endTime must be a valid Date object or a UTC date string.'); + throw new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: 'endTime must be a valid Date object or a UTC date string.' + }); } // Convert endTime to RFC3339 UTC "Zulu" format. if (optionsCopy.endTime instanceof Date) { @@ -447,51 +472,11 @@ export class RemoteConfigApiClient { } interface ErrorResponse { - error?: Error; + error?: RemoteConfigApiError; } -interface Error { +interface RemoteConfigApiError { code?: number; message?: string; status?: string; } - -const ERROR_CODE_MAPPING: { [key: string]: RemoteConfigErrorCode } = { - ABORTED: 'aborted', - ALREADY_EXISTS: 'already-exists', - INVALID_ARGUMENT: 'invalid-argument', - INTERNAL: 'internal-error', - FAILED_PRECONDITION: 'failed-precondition', - NOT_FOUND: 'not-found', - OUT_OF_RANGE: 'out-of-range', - PERMISSION_DENIED: 'permission-denied', - RESOURCE_EXHAUSTED: 'resource-exhausted', - UNAUTHENTICATED: 'unauthenticated', - UNKNOWN: 'unknown-error', -}; - -export type RemoteConfigErrorCode = - 'aborted' - | 'already-exists' - | 'failed-precondition' - | 'internal-error' - | 'invalid-argument' - | 'not-found' - | 'out-of-range' - | 'permission-denied' - | 'resource-exhausted' - | 'unauthenticated' - | 'unknown-error'; - -/** - * Firebase Remote Config error code structure. This extends PrefixedFirebaseError. - * - * @param {RemoteConfigErrorCode} code The error code. - * @param {string} message The error message. - * @constructor - */ -export class FirebaseRemoteConfigError extends PrefixedFirebaseError { - constructor(code: RemoteConfigErrorCode, message: string) { - super('remote-config', code, message); - } -} diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts index 983af8b887..7e74fc9381 100644 --- a/src/remote-config/remote-config.ts +++ b/src/remote-config/remote-config.ts @@ -17,7 +17,8 @@ import { App } from '../app'; import * as utils from '../utils/index'; import * as validator from '../utils/validator'; -import { FirebaseRemoteConfigError, RemoteConfigApiClient } from './remote-config-api-client-internal'; +import { RemoteConfigApiClient } from './remote-config-api-client-internal'; +import { FirebaseRemoteConfigError } from './error'; import { ConditionEvaluator } from './condition-evaluator-internal'; import { ValueImpl } from './internal/value-impl'; import { @@ -169,19 +170,21 @@ export class RemoteConfig { */ public createTemplateFromJSON(json: string): RemoteConfigTemplate { if (!validator.isNonEmptyString(json)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - 'JSON string must be a valid non-empty string'); + throw new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: 'JSON string must be a valid non-empty string' + }); } let template: RemoteConfigTemplate; try { template = JSON.parse(json); } catch (e) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - `Failed to parse the JSON string: ${json}. ` + e - ); + throw new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: `Failed to parse the JSON string: ${json}.`, + cause: e + }); } return new RemoteConfigTemplateImpl(template); @@ -226,18 +229,20 @@ class RemoteConfigTemplateImpl implements RemoteConfigTemplate { constructor(config: RemoteConfigTemplate) { if (!validator.isNonNullObject(config) || !validator.isNonEmptyString(config.etag)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - `Invalid Remote Config template: ${JSON.stringify(config)}`); + throw new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: `Invalid Remote Config template: ${JSON.stringify(config)}` + }); } this.etagInternal = config.etag; if (typeof config.parameters !== 'undefined') { if (!validator.isNonNullObject(config.parameters)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - 'Remote Config parameters must be a non-null object'); + throw new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: 'Remote Config parameters must be a non-null object' + }); } this.parameters = config.parameters; } else { @@ -248,9 +253,10 @@ class RemoteConfigTemplateImpl implements RemoteConfigTemplate { if (typeof config.parameterGroups !== 'undefined') { if (!validator.isNonNullObject(config.parameterGroups)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - 'Remote Config parameter groups must be a non-null object'); + throw new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: 'Remote Config parameter groups must be a non-null object' + }); } this.parameterGroups = config.parameterGroups; } else { @@ -265,9 +271,10 @@ class RemoteConfigTemplateImpl implements RemoteConfigTemplate { if (typeof config.conditions !== 'undefined') { if (!validator.isArray(config.conditions)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - 'Remote Config conditions must be an array'); + throw new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: 'Remote Config conditions must be an array' + }); } this.conditions = config.conditions; } else { @@ -344,9 +351,11 @@ class ServerTemplateImpl implements ServerTemplate { parsed = JSON.parse(template); } catch (e) { // Transforms JSON parse errors to Firebase error. - throw new FirebaseRemoteConfigError( - 'invalid-argument', - `Failed to parse the JSON string: ${template}. ` + e); + throw new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: `Failed to parse the JSON string: ${template}.`, + cause: e + }); } } else { parsed = template; @@ -364,9 +373,10 @@ class ServerTemplateImpl implements ServerTemplate { // This is the only place we should throw during evaluation, since it's under the // control of application logic. To preserve forward-compatibility, we should only // return false in cases where the SDK is unsure how to evaluate the fetched template. - throw new FirebaseRemoteConfigError( - 'failed-precondition', - 'No Remote Config Server template in cache. Call load() before calling evaluate().'); + throw new FirebaseRemoteConfigError({ + code: 'failed-precondition', + message: 'No Remote Config Server template in cache. Call load() before calling evaluate().' + }); } const evaluatedConditions = this.conditionEvaluator.evaluateConditions( @@ -497,9 +507,10 @@ function validateExperimentExposurePercent( // Enforce public contract: numeric and within [0, 100]. if (!validator.isNumber(exposurePercent) || !Number.isFinite(exposurePercent) || exposurePercent < 0 || exposurePercent > 100) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - `Experiment exposure percent must be between 0 and 100 (${parameterName})`); + throw new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: `Experiment exposure percent must be between 0 and 100 (${parameterName})` + }); } } @@ -516,17 +527,19 @@ class ServerTemplateDataImpl implements ServerTemplateData { constructor(template: ServerTemplateData) { if (!validator.isNonNullObject(template) || !validator.isNonEmptyString(template.etag)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - `Invalid Remote Config template: ${JSON.stringify(template)}`); + throw new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: `Invalid Remote Config template: ${JSON.stringify(template)}` + }); } this.etag = template.etag; if (typeof template.parameters !== 'undefined') { if (!validator.isNonNullObject(template.parameters)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - 'Remote Config parameters must be a non-null object'); + throw new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: 'Remote Config parameters must be a non-null object' + }); } this.parameters = template.parameters; } else { @@ -535,9 +548,10 @@ class ServerTemplateDataImpl implements ServerTemplateData { if (typeof template.conditions !== 'undefined') { if (!validator.isArray(template.conditions)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - 'Remote Config conditions must be an array'); + throw new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: 'Remote Config conditions must be an array' + }); } this.conditions = template.conditions; } else { @@ -567,76 +581,85 @@ class VersionImpl implements Version { constructor(version: Version) { if (!validator.isNonNullObject(version)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - `Invalid Remote Config version instance: ${JSON.stringify(version)}`); + throw new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: `Invalid Remote Config version instance: ${JSON.stringify(version)}` + }); } if (typeof version.versionNumber !== 'undefined') { if (!validator.isNonEmptyString(version.versionNumber) && !validator.isNumber(version.versionNumber)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - 'Version number must be a non-empty string in int64 format or a number'); + throw new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: 'Version number must be a non-empty string in int64 format or a number' + }); } if (!Number.isInteger(Number(version.versionNumber))) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - 'Version number must be an integer or a string in int64 format'); + throw new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: 'Version number must be an integer or a string in int64 format' + }); } this.versionNumber = version.versionNumber; } if (typeof version.updateOrigin !== 'undefined') { if (!validator.isNonEmptyString(version.updateOrigin)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - 'Version update origin must be a non-empty string'); + throw new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: 'Version update origin must be a non-empty string' + }); } this.updateOrigin = version.updateOrigin; } if (typeof version.updateType !== 'undefined') { if (!validator.isNonEmptyString(version.updateType)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - 'Version update type must be a non-empty string'); + throw new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: 'Version update type must be a non-empty string' + }); } this.updateType = version.updateType; } if (typeof version.updateUser !== 'undefined') { if (!validator.isNonNullObject(version.updateUser)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - 'Version update user must be a non-null object'); + throw new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: 'Version update user must be a non-null object' + }); } this.updateUser = version.updateUser; } if (typeof version.description !== 'undefined') { if (!validator.isNonEmptyString(version.description)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - 'Version description must be a non-empty string'); + throw new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: 'Version description must be a non-empty string' + }); } this.description = version.description; } if (typeof version.rollbackSource !== 'undefined') { if (!validator.isNonEmptyString(version.rollbackSource)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - 'Version rollback source must be a non-empty string'); + throw new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: 'Version rollback source must be a non-empty string' + }); } this.rollbackSource = version.rollbackSource; } if (typeof version.isLegacy !== 'undefined') { if (!validator.isBoolean(version.isLegacy)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - 'Version.isLegacy must be a boolean'); + throw new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: 'Version.isLegacy must be a boolean' + }); } this.isLegacy = version.isLegacy; } @@ -646,9 +669,10 @@ class VersionImpl implements Version { // we could still validate it below. if (typeof version.updateTime !== 'undefined') { if (!this.isValidTimestamp(version.updateTime)) { - throw new FirebaseRemoteConfigError( - 'invalid-argument', - 'Version update time must be a valid date string'); + throw new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: 'Version update time must be a valid date string' + }); } this.updateTime = new Date(version.updateTime).toUTCString(); } diff --git a/src/security-rules/error.ts b/src/security-rules/error.ts new file mode 100644 index 0000000000..37b327c9bb --- /dev/null +++ b/src/security-rules/error.ts @@ -0,0 +1,47 @@ +/*! + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PrefixedFirebaseError, ErrorInfo } from '../utils/error'; + +/** + * The constant mapping for valid Security Rules client error codes. + */ +export const SecurityRulesErrorCode = { + ALREADY_EXISTS: 'already-exists', + AUTHENTICATION_ERROR: 'authentication-error', + INTERNAL_ERROR: 'internal-error', + INVALID_ARGUMENT: 'invalid-argument', + INVALID_SERVER_RESPONSE: 'invalid-server-response', + NOT_FOUND: 'not-found', + RESOURCE_EXHAUSTED: 'resource-exhausted', + SERVICE_UNAVAILABLE: 'service-unavailable', + UNKNOWN_ERROR: 'unknown-error', +} as const; + +/** + * The type definition for valid Security Rules client error codes. + */ +export type SecurityRulesErrorCode = typeof SecurityRulesErrorCode[keyof typeof SecurityRulesErrorCode]; + +export class FirebaseSecurityRulesError extends PrefixedFirebaseError { + /** + * @param info - The error code info. + * @param message - The error message. If provided, this will override the default message. + */ + constructor(info: ErrorInfo, message?: string) { + super('security-rules', info.code, message || info.message, info.httpResponse, info.cause); + } +} diff --git a/src/security-rules/index.ts b/src/security-rules/index.ts index 23d6ed10fd..4da1c7ea9d 100644 --- a/src/security-rules/index.ts +++ b/src/security-rules/index.ts @@ -65,3 +65,5 @@ export function getSecurityRules(app?: App): SecurityRules { const firebaseApp: FirebaseApp = app as FirebaseApp; return firebaseApp.getOrInitService('securityRules', (app) => new SecurityRules(app)); } + +export { FirebaseSecurityRulesError, SecurityRulesErrorCode } from './error'; \ No newline at end of file diff --git a/src/security-rules/security-rules-api-client-internal.ts b/src/security-rules/security-rules-api-client-internal.ts index 33ad1ee886..8f447a9593 100644 --- a/src/security-rules/security-rules-api-client-internal.ts +++ b/src/security-rules/security-rules-api-client-internal.ts @@ -15,8 +15,8 @@ */ import { HttpRequestConfig, HttpClient, RequestResponseError, AuthorizedHttpClient } from '../utils/api-request'; -import { PrefixedFirebaseError } from '../utils/error'; -import { FirebaseSecurityRulesError, SecurityRulesErrorCode } from './security-rules-internal'; +import { PrefixedFirebaseError, toHttpResponse } from '../utils/error'; +import { FirebaseSecurityRulesError, SecurityRulesErrorCode } from './error'; import * as utils from '../utils/index'; import * as validator from '../utils/validator'; import { FirebaseApp } from '../app/firebase-app'; @@ -62,10 +62,11 @@ export class SecurityRulesApiClient { constructor(private readonly app: App) { if (!validator.isNonNullObject(app) || !('options' in app)) { - throw new FirebaseSecurityRulesError( - 'invalid-argument', - 'First argument passed to admin.securityRules() must be a valid Firebase app ' - + 'instance.'); + throw new FirebaseSecurityRulesError({ + code: 'invalid-argument', + message: 'First argument passed to admin.securityRules() must be a valid Firebase app ' + + 'instance.' + }); } this.httpClient = new AuthorizedHttpClient(app as FirebaseApp); @@ -86,7 +87,10 @@ export class SecurityRulesApiClient { !validator.isNonNullObject(ruleset.source) || !validator.isNonEmptyArray(ruleset.source.files)) { - const err = new FirebaseSecurityRulesError('invalid-argument', 'Invalid rules content.'); + const err = new FirebaseSecurityRulesError({ + code: 'invalid-argument', + message: 'Invalid rules content.' + }); return Promise.reject(err); } @@ -95,8 +99,10 @@ export class SecurityRulesApiClient { !validator.isNonEmptyString(rf.name) || !validator.isNonEmptyString(rf.content)) { - const err = new FirebaseSecurityRulesError( - 'invalid-argument', `Invalid rules file argument: ${JSON.stringify(rf)}`); + const err = new FirebaseSecurityRulesError({ + code: 'invalid-argument', + message: `Invalid rules file argument: ${JSON.stringify(rf)}` + }); return Promise.reject(err); } } @@ -126,17 +132,24 @@ export class SecurityRulesApiClient { public listRulesets(pageSize = 100, pageToken?: string): Promise { if (!validator.isNumber(pageSize)) { - const err = new FirebaseSecurityRulesError('invalid-argument', 'Invalid page size.'); + const err = new FirebaseSecurityRulesError({ + code: 'invalid-argument', + message: 'Invalid page size.' + }); return Promise.reject(err); } if (pageSize < 1 || pageSize > 100) { - const err = new FirebaseSecurityRulesError( - 'invalid-argument', 'Page size must be between 1 and 100.'); + const err = new FirebaseSecurityRulesError({ + code: 'invalid-argument', + message: 'Page size must be between 1 and 100.' + }); return Promise.reject(err); } if (typeof pageToken !== 'undefined' && !validator.isNonEmptyString(pageToken)) { - const err = new FirebaseSecurityRulesError( - 'invalid-argument', 'Next page token must be a non-empty string.'); + const err = new FirebaseSecurityRulesError({ + code: 'invalid-argument', + message: 'Next page token must be a non-empty string.' + }); return Promise.reject(err); } @@ -218,11 +231,12 @@ export class SecurityRulesApiClient { return utils.findProjectId(this.app) .then((projectId) => { if (!validator.isNonEmptyString(projectId)) { - throw new FirebaseSecurityRulesError( - 'invalid-argument', - 'Failed to determine project ID. Initialize the SDK with service account credentials, or ' - + 'set project ID as an app option. Alternatively, set the GOOGLE_CLOUD_PROJECT ' - + 'environment variable.'); + throw new FirebaseSecurityRulesError({ + code: 'invalid-argument', + message: 'Failed to determine project ID. Initialize the SDK with service account credentials, or ' + + 'set project ID as an app option. Alternatively, set the GOOGLE_CLOUD_PROJECT ' + + 'environment variable.' + }); } this.projectIdPrefix = `projects/${projectId}`; @@ -260,13 +274,17 @@ export class SecurityRulesApiClient { private getRulesetName(name: string): string { if (!validator.isNonEmptyString(name)) { - throw new FirebaseSecurityRulesError( - 'invalid-argument', 'Ruleset name must be a non-empty string.'); + throw new FirebaseSecurityRulesError({ + code: 'invalid-argument', + message: 'Ruleset name must be a non-empty string.' + }); } if (name.indexOf('/') !== -1) { - throw new FirebaseSecurityRulesError( - 'invalid-argument', 'Ruleset name must not contain any "/" characters.'); + throw new FirebaseSecurityRulesError({ + code: 'invalid-argument', + message: 'Ruleset name must not contain any "/" characters.' + }); } return `rulesets/${name}`; @@ -290,26 +308,29 @@ export class SecurityRulesApiClient { const response = err.response; if (!response.isJson()) { - return new FirebaseSecurityRulesError( - 'unknown-error', - `Unexpected response with status: ${response.status} and body: ${response.text}`); + return new FirebaseSecurityRulesError({ + code: 'unknown-error', + message: `Unexpected response with status: ${response.status} and body: ${response.text}`, + httpResponse: toHttpResponse(response), + cause: err + }); } - const error: Error = (response.data as ErrorResponse).error || {}; + const error: SecurityApiError = (response.data as ErrorResponse).error || {}; let code: SecurityRulesErrorCode = 'unknown-error'; if (error.status && error.status in ERROR_CODE_MAPPING) { code = ERROR_CODE_MAPPING[error.status]; } - const message = error.message || `Unknown server error: ${response.text}`; - return new FirebaseSecurityRulesError(code, message); + const message = error.message || 'Unknown server error'; + return new FirebaseSecurityRulesError({ code, message, httpResponse: toHttpResponse(response), cause: err }); } } interface ErrorResponse { - error?: Error; + error?: SecurityApiError; } -interface Error { +interface SecurityApiError { code?: number; message?: string; status?: string; diff --git a/src/security-rules/security-rules-internal.ts b/src/security-rules/security-rules-internal.ts deleted file mode 100644 index 539359cb63..0000000000 --- a/src/security-rules/security-rules-internal.ts +++ /dev/null @@ -1,34 +0,0 @@ -/*! - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { PrefixedFirebaseError } from '../utils/error'; - -export type SecurityRulesErrorCode = - 'already-exists' - | 'authentication-error' - | 'internal-error' - | 'invalid-argument' - | 'invalid-server-response' - | 'not-found' - | 'resource-exhausted' - | 'service-unavailable' - | 'unknown-error'; - -export class FirebaseSecurityRulesError extends PrefixedFirebaseError { - constructor(code: SecurityRulesErrorCode, message: string) { - super('security-rules', code, message); - } -} diff --git a/src/security-rules/security-rules.ts b/src/security-rules/security-rules.ts index 8dcd55c836..9f30732299 100644 --- a/src/security-rules/security-rules.ts +++ b/src/security-rules/security-rules.ts @@ -19,7 +19,7 @@ import * as validator from '../utils/validator'; import { SecurityRulesApiClient, RulesetResponse, RulesetContent, ListRulesetsResponse, } from './security-rules-api-client-internal'; -import { FirebaseSecurityRulesError } from './security-rules-internal'; +import { FirebaseSecurityRulesError } from './error'; /** * A source file containing some Firebase security rules. The content includes raw @@ -66,9 +66,10 @@ export class RulesetMetadataList { */ constructor(response: ListRulesetsResponse) { if (!validator.isNonNullObject(response) || !validator.isArray(response.rulesets)) { - throw new FirebaseSecurityRulesError( - 'invalid-argument', - `Invalid ListRulesets response: ${JSON.stringify(response)}`); + throw new FirebaseSecurityRulesError({ + code: 'invalid-argument', + message: `Invalid ListRulesets response: ${JSON.stringify(response)}` + }); } this.rulesets = response.rulesets.map((rs) => { @@ -109,9 +110,10 @@ export class Ruleset implements RulesetMetadata { !validator.isNonEmptyString(ruleset.name) || !validator.isNonEmptyString(ruleset.createTime) || !validator.isNonNullObject(ruleset.source)) { - throw new FirebaseSecurityRulesError( - 'invalid-argument', - `Invalid Ruleset response: ${JSON.stringify(ruleset)}`); + throw new FirebaseSecurityRulesError({ + code: 'invalid-argument', + message: `Invalid Ruleset response: ${JSON.stringify(ruleset)}` + }); } this.name = stripProjectIdPrefix(ruleset.name); @@ -287,8 +289,10 @@ export class SecurityRules { */ public createRulesFileFromSource(name: string, source: string | Buffer): RulesFile { if (!validator.isNonEmptyString(name)) { - throw new FirebaseSecurityRulesError( - 'invalid-argument', 'Name must be a non-empty string.'); + throw new FirebaseSecurityRulesError({ + code: 'invalid-argument', + message: 'Name must be a non-empty string.' + }); } let content: string; @@ -297,8 +301,10 @@ export class SecurityRules { } else if (validator.isBuffer(source)) { content = source.toString('utf-8'); } else { - throw new FirebaseSecurityRulesError( - 'invalid-argument', 'Source must be a non-empty string or a Buffer.'); + throw new FirebaseSecurityRulesError({ + code: 'invalid-argument', + message: 'Source must be a non-empty string or a Buffer.' + }); } return { @@ -361,8 +367,10 @@ export class SecurityRules { .then((release) => { const rulesetName = release.rulesetName; if (!validator.isNonEmptyString(rulesetName)) { - throw new FirebaseSecurityRulesError( - 'not-found', `Ruleset name not found for ${releaseName}.`); + throw new FirebaseSecurityRulesError({ + code: 'not-found', + message: `Ruleset name not found for ${releaseName}.` + }); } return this.getRuleset(stripProjectIdPrefix(rulesetName)); @@ -372,8 +380,10 @@ export class SecurityRules { private releaseRuleset(ruleset: string | RulesetMetadata, releaseName: string): Promise { if (!validator.isNonEmptyString(ruleset) && (!validator.isNonNullObject(ruleset) || !validator.isNonEmptyString(ruleset.name))) { - const err = new FirebaseSecurityRulesError( - 'invalid-argument', 'ruleset must be a non-empty name or a RulesetMetadata object.'); + const err = new FirebaseSecurityRulesError({ + code: 'invalid-argument', + message: 'ruleset must be a non-empty name or a RulesetMetadata object.' + }); return Promise.reject(err); } @@ -387,12 +397,12 @@ export class SecurityRules { private getBucketName(bucket?: string): string { const bucketName = (typeof bucket !== 'undefined') ? bucket : this.app.options.storageBucket; if (!validator.isNonEmptyString(bucketName)) { - throw new FirebaseSecurityRulesError( - 'invalid-argument', - 'Bucket name not specified or invalid. Specify a default bucket name via the ' + - 'storageBucket option when initializing the app, or specify the bucket name ' + - 'explicitly when calling the rules API.', - ); + throw new FirebaseSecurityRulesError({ + code: 'invalid-argument', + message: 'Bucket name not specified or invalid. Specify a default bucket name via the ' + + 'storageBucket option when initializing the app, or specify the bucket name ' + + 'explicitly when calling the rules API.' + }); } return bucketName; diff --git a/src/utils/api-request.ts b/src/utils/api-request.ts index 9616b7a39f..b6b76ceb9c 100644 --- a/src/utils/api-request.ts +++ b/src/utils/api-request.ts @@ -16,7 +16,8 @@ */ import { FirebaseApp } from '../app/firebase-app'; -import { AppErrorCodes, FirebaseAppError } from './error'; +import { toHttpResponse } from './error'; +import { AppErrorCode, FirebaseAppError } from '../app/error'; import * as validator from './validator'; import http = require('http'); @@ -31,8 +32,10 @@ import { getMetricsHeader } from '../utils/index'; /** Http method type definition. */ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD'; -/** API callback function type definition. */ -export type ApiCallbackFunction = (data: object) => void; +/** API request callback function type definition. */ +export type ApiRequestCallback = (data: any) => void; +/** API response callback function type definition. */ +export type ApiResponseCallback = (response: RequestResponse) => void; /** * Base configuration for constructing a new request. @@ -41,7 +44,7 @@ export interface BaseRequestConfig { method: HttpMethod; /** Target URL of the request. Should be a well-formed URL including protocol, hostname, port and path. */ url: string; - headers?: {[key: string]: string}; + headers?: { [key: string]: string; }; data?: string | object | Buffer | null; /** Connect and read timeout (in milliseconds) for the outgoing request. */ timeout?: number; @@ -61,7 +64,7 @@ export interface Http2RequestConfig extends BaseRequestConfig { http2SessionHandler: Http2SessionHandler; } -type RequestConfig = HttpRequestConfig | Http2RequestConfig +type RequestConfig = HttpRequestConfig | Http2RequestConfig; /** * Represents an HTTP or HTTP/2 response received from a remote server. @@ -94,15 +97,15 @@ interface LowLevelHttpResponse extends BaseLowLevelResponse { config: HttpRequestConfig; } -type IncomingHttp2Headers = http2.IncomingHttpHeaders & http2.IncomingHttpStatusHeader +type IncomingHttp2Headers = http2.IncomingHttpHeaders & http2.IncomingHttpStatusHeader; interface LowLevelHttp2Response extends BaseLowLevelResponse { - headers: IncomingHttp2Headers + headers: IncomingHttp2Headers; request: http2.ClientHttp2Stream | null; config: Http2RequestConfig; } -type LowLevelResponse = LowLevelHttpResponse | LowLevelHttp2Response +type LowLevelResponse = LowLevelHttpResponse | LowLevelHttp2Response; interface BaseLowLevelError extends Error { code?: string; @@ -120,7 +123,7 @@ interface LowLevelHttp2Error extends BaseLowLevelError { response?: LowLevelHttp2Response; } -type LowLevelError = LowLevelHttpError | LowLevelHttp2Error +type LowLevelError = LowLevelHttpError | LowLevelHttp2Error; class DefaultRequestResponse implements RequestResponse { @@ -130,7 +133,6 @@ class DefaultRequestResponse implements RequestResponse { private readonly parsedData: any; private readonly parseError: Error; - private readonly request: string; /** * Constructs a new `RequestResponse` from the given `LowLevelResponse`. @@ -141,26 +143,25 @@ class DefaultRequestResponse implements RequestResponse { this.text = resp.data; try { if (!resp.data) { - throw new FirebaseAppError(AppErrorCodes.INTERNAL_ERROR, 'HTTP response missing data.'); + throw new FirebaseAppError({ code: AppErrorCode.INTERNAL_ERROR, message: 'HTTP response missing data.' }); } this.parsedData = JSON.parse(resp.data); } catch (err) { this.parsedData = undefined; this.parseError = err; } - this.request = `${resp.config.method} ${resp.config.url}`; } get data(): any { if (this.isJson()) { return this.parsedData; } - throw new FirebaseAppError( - AppErrorCodes.UNABLE_TO_PARSE_RESPONSE, - `Error while parsing response data: "${ this.parseError.toString() }". Raw server ` + - `response: "${ this.text }". Status code: "${ this.status }". Outgoing ` + - `request: "${ this.request }."`, - ); + throw new FirebaseAppError({ + code: AppErrorCode.UNABLE_TO_PARSE_RESPONSE, + message: 'Error while parsing response data', + cause: this.parseError as Error, + httpResponse: toHttpResponse(this) + }); } public isJson(): boolean { @@ -185,17 +186,17 @@ class MultipartRequestResponse implements RequestResponse { } get text(): string { - throw new FirebaseAppError( - AppErrorCodes.UNABLE_TO_PARSE_RESPONSE, - 'Unable to parse multipart payload as text', - ); + throw new FirebaseAppError({ + code: AppErrorCode.UNABLE_TO_PARSE_RESPONSE, + message: 'Unable to parse multipart payload as text' + }); } get data(): any { - throw new FirebaseAppError( - AppErrorCodes.UNABLE_TO_PARSE_RESPONSE, - 'Unable to parse multipart payload as JSON', - ); + throw new FirebaseAppError({ + code: AppErrorCode.UNABLE_TO_PARSE_RESPONSE, + message: 'Unable to parse multipart payload as JSON' + }); } public isJson(): boolean { @@ -258,28 +259,34 @@ export function defaultRetryConfig(): RetryConfig { */ function validateRetryConfig(retry: RetryConfig): void { if (!validator.isNumber(retry.maxRetries) || retry.maxRetries < 0) { - throw new FirebaseAppError( - AppErrorCodes.INVALID_ARGUMENT, 'maxRetries must be a non-negative integer'); + throw new FirebaseAppError({ + code: AppErrorCode.INVALID_ARGUMENT, + message: 'maxRetries must be a non-negative integer' + }); } if (typeof retry.backOffFactor !== 'undefined') { if (!validator.isNumber(retry.backOffFactor) || retry.backOffFactor < 0) { - throw new FirebaseAppError( - AppErrorCodes.INVALID_ARGUMENT, 'backOffFactor must be a non-negative number'); + throw new FirebaseAppError({ + code: AppErrorCode.INVALID_ARGUMENT, + message: 'backOffFactor must be a non-negative number' + }); } } if (!validator.isNumber(retry.maxDelayInMillis) || retry.maxDelayInMillis < 0) { - throw new FirebaseAppError( - AppErrorCodes.INVALID_ARGUMENT, 'maxDelayInMillis must be a non-negative integer'); + throw new FirebaseAppError({ + code: AppErrorCode.INVALID_ARGUMENT, + message: 'maxDelayInMillis must be a non-negative integer' + }); } if (typeof retry.statusCodes !== 'undefined' && !validator.isArray(retry.statusCodes)) { - throw new FirebaseAppError(AppErrorCodes.INVALID_ARGUMENT, 'statusCodes must be an array'); + throw new FirebaseAppError({ code: AppErrorCode.INVALID_ARGUMENT, message: 'statusCodes must be an array' }); } if (typeof retry.ioErrorCodes !== 'undefined' && !validator.isArray(retry.ioErrorCodes)) { - throw new FirebaseAppError(AppErrorCodes.INVALID_ARGUMENT, 'ioErrorCodes must be an array'); + throw new FirebaseAppError({ code: AppErrorCode.INVALID_ARGUMENT, message: 'ioErrorCodes must be an array' }); } } @@ -288,7 +295,7 @@ export class RequestClient { constructor(retry: RetryConfig | null = defaultRetryConfig()) { if (retry) { - this.retry = retry + this.retry = retry; validateRetryConfig(this.retry); } } @@ -379,7 +386,7 @@ export class RequestClient { } if (!this.retry) { - throw new FirebaseAppError(AppErrorCodes.INTERNAL_ERROR, 'Expected this.retry to exist.'); + throw new FirebaseAppError({ code: AppErrorCode.INTERNAL_ERROR, message: 'Expected this.retry to exist.' }); } const backOffFactor = this.retry.backOffFactor || 0; @@ -391,7 +398,7 @@ export class RequestClient { export class HttpClient extends RequestClient { constructor(retry?: RetryConfig | null) { - super(retry) + super(retry); } /** @@ -439,13 +446,17 @@ export class HttpClient extends RequestClient { } if (err.code === 'ETIMEDOUT') { - throw new FirebaseAppError( - AppErrorCodes.NETWORK_TIMEOUT, - `Error while making request: ${err.message}.`); + throw new FirebaseAppError({ + code: AppErrorCode.NETWORK_TIMEOUT, + message: `Error while making request: ${err.message}.`, + cause: err + }); } - throw new FirebaseAppError( - AppErrorCodes.NETWORK_ERROR, - `Error while making request: ${err.message}. Error code: ${err.code}`); + throw new FirebaseAppError({ + code: AppErrorCode.NETWORK_ERROR, + message: `Error while making request: ${err.message}. Error code: ${err.code}`, + cause: err + }); }); } } @@ -501,13 +512,17 @@ export class Http2Client extends RequestClient { } if (err.code === 'ETIMEDOUT') { - throw new FirebaseAppError( - AppErrorCodes.NETWORK_TIMEOUT, - `Error while making request: ${err.message}.`); + throw new FirebaseAppError({ + code: AppErrorCode.NETWORK_TIMEOUT, + message: `Error while making request: ${err.message}.`, + cause: err + }); } - throw new FirebaseAppError( - AppErrorCodes.NETWORK_ERROR, - `Error while making request: ${err.message}. Error code: ${err.code}`); + throw new FirebaseAppError({ + code: AppErrorCode.NETWORK_ERROR, + message: `Error while making request: ${err.message}. Error code: ${err.code}`, + cause: err + }); }); } } @@ -530,7 +545,7 @@ export function parseHttpResponse( const statusLine: string = headerLines[0]; const status: string = statusLine.trim().split(/\s/)[1]; - const headers: {[key: string]: string} = {}; + const headers: { [key: string]: string; } = {}; headerLines.slice(1).forEach((line) => { const colonPos = line.indexOf(':'); const name = line.substring(0, colonPos).trim().toLowerCase(); @@ -554,7 +569,7 @@ export function parseHttpResponse( request: null, }; if (!validator.isNumber(lowLevelResponse.status)) { - throw new FirebaseAppError(AppErrorCodes.INTERNAL_ERROR, 'Malformed HTTP status line.'); + throw new FirebaseAppError({ code: AppErrorCode.INTERNAL_ERROR, message: 'Malformed HTTP status line.' }); } return new DefaultRequestResponse(lowLevelResponse); } @@ -570,7 +585,7 @@ class AsyncRequestCall { protected entity: Buffer | undefined; protected promise: Promise; - constructor(private readonly configImpl: HttpRequestConfigImpl | Http2RequestConfigImpl) {} + constructor(private readonly configImpl: HttpRequestConfigImpl | Http2RequestConfigImpl) { } /** * Extracts multipart boundary from the HTTP header. The content-type header of a multipart @@ -586,13 +601,13 @@ class AsyncRequestCall { } const segments: string[] = contentType.split(';'); - const emptyObject: {[key: string]: string} = {}; + const emptyObject: { [key: string]: string; } = {}; const headerParams = segments.slice(1) .map((segment) => segment.trim().split('=')) .reduce((curr, params) => { // Parse key=value pairs in the content-type header into properties of an object. if (params.length === 2) { - const keyValuePair: {[key: string]: string} = {}; + const keyValuePair: { [key: string]: string; } = {}; keyValuePair[params[0]] = params[1]; return Object.assign(curr, keyValuePair); } @@ -726,7 +741,7 @@ class AsyncHttpCall extends AsyncRequestCall { private constructor(config: HttpRequestConfig) { const httpConfigImpl = new HttpRequestConfigImpl(config); - super(httpConfigImpl) + super(httpConfigImpl); try { this.httpConfigImpl = httpConfigImpl; this.options = this.httpConfigImpl.buildRequestOptions(); @@ -778,9 +793,10 @@ class AsyncHttpCall extends AsyncRequestCall { } if (!res.statusCode) { - throw new FirebaseAppError( - AppErrorCodes.INTERNAL_ERROR, - 'Expected a statusCode on the response from a ClientRequest'); + throw new FirebaseAppError({ + code: AppErrorCode.INTERNAL_ERROR, + message: 'Expected a statusCode on the response from a ClientRequest' + }); } const response: LowLevelResponse = { @@ -816,7 +832,7 @@ class AsyncHttpCall extends AsyncRequestCall { } class AsyncHttp2Call extends AsyncRequestCall { - private readonly http2ConfigImpl: Http2RequestConfigImpl + private readonly http2ConfigImpl: Http2RequestConfigImpl; /** * Sends an HTTP2 request based on the provided configuration. @@ -827,7 +843,7 @@ class AsyncHttp2Call extends AsyncRequestCall { private constructor(config: Http2RequestConfig) { const http2ConfigImpl = new Http2RequestConfigImpl(config); - super(http2ConfigImpl) + super(http2ConfigImpl); try { this.http2ConfigImpl = http2ConfigImpl; this.options = this.http2ConfigImpl.buildRequestOptions(); @@ -879,15 +895,16 @@ class AsyncHttp2Call extends AsyncRequestCall { req.end(this.entity); } - private handleHttp2Response(headers: IncomingHttp2Headers, stream: http2.ClientHttp2Stream): void{ + private handleHttp2Response(headers: IncomingHttp2Headers, stream: http2.ClientHttp2Stream): void { if (stream.aborted) { return; } if (!headers[':status']) { - throw new FirebaseAppError( - AppErrorCodes.INTERNAL_ERROR, - 'Expected a statusCode on the response from a ClientRequest'); + throw new FirebaseAppError({ + code: AppErrorCode.INTERNAL_ERROR, + message: 'Expected a statusCode on the response from a ClientRequest' + }); } const response: LowLevelHttp2Response = { @@ -929,7 +946,7 @@ class AsyncHttp2Call extends AsyncRequestCall { class BaseRequestConfigImpl implements BaseRequestConfig { constructor(protected readonly config: RequestConfig) { - this.config = config + this.config = config; } get method(): HttpMethod { @@ -940,7 +957,7 @@ class BaseRequestConfigImpl implements BaseRequestConfig { return this.config.url; } - get headers(): {[key: string]: string} | undefined { + get headers(): { [key: string]: string; } | undefined { return this.config.headers; } @@ -984,7 +1001,7 @@ class BaseRequestConfigImpl implements BaseRequestConfig { } // Parse URL and append data to query string. const parsedUrl = new url.URL(fullUrl); - const dataObj = this.data as {[key: string]: string}; + const dataObj = this.data as { [key: string]: string; }; for (const key in dataObj) { if (Object.prototype.hasOwnProperty.call(dataObj, key)) { parsedUrl.searchParams.append(key, dataObj[key]); @@ -1017,7 +1034,7 @@ class BaseRequestConfigImpl implements BaseRequestConfig { class HttpRequestConfigImpl extends BaseRequestConfigImpl implements HttpRequestConfig { constructor(private readonly httpConfig: HttpRequestConfig) { - super(httpConfig) + super(httpConfig); } get httpAgent(): http.Agent | undefined { @@ -1051,7 +1068,7 @@ class HttpRequestConfigImpl extends BaseRequestConfigImpl implements HttpRequest class Http2RequestConfigImpl extends BaseRequestConfigImpl implements Http2RequestConfig { constructor(private readonly http2Config: Http2RequestConfig) { - super(http2Config) + super(http2Config); } get http2SessionHandler(): Http2SessionHandler { @@ -1099,7 +1116,7 @@ export class AuthorizedHttpClient extends HttpClient { } if (!requestCopy.headers['X-Goog-Api-Client']) { - requestCopy.headers['X-Goog-Api-Client'] = getMetricsHeader() + requestCopy.headers['X-Goog-Api-Client'] = getMetricsHeader(); } return super.send(requestCopy); @@ -1134,13 +1151,13 @@ export class AuthorizedHttp2Client extends Http2Client { requestCopy.headers['x-goog-user-project'] = quotaProjectId; } - if (!requestCopy.headers['X-Goog-Api-Client']) { - requestCopy.headers['X-Goog-Api-Client'] = getMetricsHeader() + if (!requestCopy.headers['X-Goog-Api-Client']) { + requestCopy.headers['X-Goog-Api-Client'] = getMetricsHeader(); } return super.send(requestCopy); }); - } + } protected getToken(): Promise { return this.app.INTERNAL.getToken() @@ -1156,8 +1173,8 @@ export class AuthorizedHttp2Client extends Http2Client { * @constructor */ export class ApiSettings { - private requestValidator: ApiCallbackFunction; - private responseValidator: ApiCallbackFunction; + private requestValidator: ApiRequestCallback; + private responseValidator: ApiResponseCallback; constructor(private endpoint: string, private httpMethod: HttpMethod = 'POST') { this.setRequestValidator(null) @@ -1178,14 +1195,14 @@ export class ApiSettings { * @param requestValidator - The request validator. * @returns The current API settings instance. */ - public setRequestValidator(requestValidator: ApiCallbackFunction | null): ApiSettings { - const nullFunction: ApiCallbackFunction = () => undefined; + public setRequestValidator(requestValidator: ApiRequestCallback | null): ApiSettings { + const nullFunction: ApiRequestCallback = () => undefined; this.requestValidator = requestValidator || nullFunction; return this; } /** @returns The request validator. */ - public getRequestValidator(): ApiCallbackFunction { + public getRequestValidator(): ApiRequestCallback { return this.requestValidator; } @@ -1193,14 +1210,14 @@ export class ApiSettings { * @param responseValidator - The response validator. * @returns The current API settings instance. */ - public setResponseValidator(responseValidator: ApiCallbackFunction | null): ApiSettings { - const nullFunction: ApiCallbackFunction = () => undefined; + public setResponseValidator(responseValidator: ApiResponseCallback | null): ApiSettings { + const nullFunction: ApiResponseCallback = () => undefined; this.responseValidator = responseValidator || nullFunction; return this; } /** @returns The response validator. */ - public getResponseValidator(): ApiCallbackFunction { + public getResponseValidator(): ApiResponseCallback { return this.responseValidator; } } @@ -1241,9 +1258,9 @@ export class ExponentialBackoffPoller extends EventEmitter { private reject: (err: object) => void; constructor( - private readonly initialPollingDelayMillis: number = 1000, - private readonly maxPollingDelayMillis: number = 10000, - private readonly masterTimeoutMillis: number = 60000) { + private readonly initialPollingDelayMillis: number = 1000, + private readonly maxPollingDelayMillis: number = 10000, + private readonly masterTimeoutMillis: number = 60000) { super(); } @@ -1289,7 +1306,7 @@ export class ExponentialBackoffPoller extends EventEmitter { if (!result) { this.repollTimer = - setTimeout(() => this.emit('poll'), this.getPollingDelayMillis()); + setTimeout(() => this.emit('poll'), this.getPollingDelayMillis()); this.numTries++; return; } @@ -1325,71 +1342,63 @@ export class ExponentialBackoffPoller extends EventEmitter { export class Http2SessionHandler { - private http2Session: http2.ClientHttp2Session - protected promise: Promise - protected resolve: () => void; - protected reject: (_: any) => void; + private http2Session: http2.ClientHttp2Session; + private sessionErrors: Error[] = []; - constructor(url: string){ - this.promise = new Promise((resolve, reject) => { - this.resolve = resolve; - this.reject = reject; - this.http2Session = this.createSession(url) - }); + constructor(url: string) { + this.createSession(url); } public createSession(url: string): http2.ClientHttp2Session { - if (!this.http2Session || this.isClosed ) { + if (!this.http2Session || this.isCurrentSessionClosed) { + this.sessionErrors = []; const opts: http2.SecureClientSessionOptions = { // Set local max concurrent stream limit to respect backend limit peerMaxConcurrentStreams: 100, ALPNProtocols: ['h2'] - } - const http2Session = http2.connect(url, opts) - - http2Session.on('goaway', (errorCode, _, opaqueData) => { - this.reject(new FirebaseAppError( - AppErrorCodes.NETWORK_ERROR, - `Error while making requests: GOAWAY - ${opaqueData?.toString()}, Error code: ${errorCode}` - )); - }) + }; + this.http2Session = http2.connect(url, opts); + + this.http2Session.on('goaway', (errorCode, _, opaqueData) => { + const error = new FirebaseAppError({ + code: AppErrorCode.NETWORK_ERROR, + message: `Error while making requests: GOAWAY - ${opaqueData?.toString()}, Error code: ${errorCode}` + }); + this.sessionErrors.push(error); + }); - http2Session.on('error', (error: any) => { + this.http2Session.on('error', (error: any) => { let errorMessage: any; - if (error.name == 'AggregateError' && error.errors) { - errorMessage = `Session error while making requests: ${error.code} - ${error.name}: ` + - `[${error.errors.map((e: any) => e.message).join(', ')}]` + if (error?.name === 'AggregateError' && Array.isArray(error.errors)) { + errorMessage = `Session error while making requests: ${error?.code} - ${error?.name}: ` + + `[${error.errors.map((e: any) => e.message).join(', ')}]`; } else { - errorMessage = `Session error while making requests: ${error.code} - ${error.message} ` + errorMessage = `Session error while making requests: ${error?.code} - ${error?.message} `; } - this.reject(new FirebaseAppError( - AppErrorCodes.NETWORK_ERROR, - errorMessage - )); - }) - - http2Session.on('close', () => { - // Resolve current promise - this.resolve() + const appError = new FirebaseAppError({ + code: AppErrorCode.NETWORK_ERROR, + message: errorMessage, + cause: error, + }); + this.sessionErrors.push(appError); }); - return http2Session } - return this.http2Session + return this.http2Session; } - public invoke(): Promise { - return this.promise + public getErrors(): Error[] { + return this.sessionErrors; } get session(): http2.ClientHttp2Session { - return this.http2Session + return this.http2Session; } - get isClosed(): boolean { - return this.http2Session.closed + get isCurrentSessionClosed(): boolean { + return !!this.http2Session?.closed; } public close(): void { - this.http2Session.close() + this.http2Session?.close(); } } \ No newline at end of file diff --git a/src/utils/crypto-signer.ts b/src/utils/crypto-signer.ts index a4b1f20be5..74680ce10c 100644 --- a/src/utils/crypto-signer.ts +++ b/src/utils/crypto-signer.ts @@ -226,11 +226,6 @@ export class CryptoSignerError extends Error { constructor(private errorInfo: ExtendedErrorInfo) { super(errorInfo.message); - /* tslint:disable:max-line-length */ - // Set the prototype explicitly. See the following link for more details: - // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work - /* tslint:enable:max-line-length */ - (this as any).__proto__ = CryptoSignerError.prototype; } /** @returns The error code. */ diff --git a/src/utils/error.ts b/src/utils/error.ts index db013b8967..15ea1f5b98 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -15,1133 +15,176 @@ * limitations under the License. */ -import { FirebaseError as FirebaseErrorInterface } from '../app'; -import { BatchResponse } from '../messaging/messaging-api'; -import { deepCopy } from '../utils/deep-copy'; +import { RequestResponse } from './api-request'; /** - * Defines error info type. This includes a code and message string. + * Represents the raw HTTP response object. */ -export interface ErrorInfo { - code: string; - message: string; +export interface HttpResponse { + /** The HTTP status code of the response. */ + status: number; + /** The HTTP headers of the response. */ + headers: { [key: string]: any; }; + /** The response data payload. */ + data?: string | object; } /** - * Defines a type that stores all server to client codes (string enum). + * Maps a RequestResponse to a clean HttpResponse, preserving raw text if not JSON. + * + * @param resp - The RequestResponse to map. + * @returns A clean HttpResponse object. + * @internal */ -interface ServerToClientCode { - [code: string]: string; +export function toHttpResponse(resp: RequestResponse): HttpResponse { + return { + status: resp.status, + headers: resp.headers, + data: resp.isJson() ? resp.data : resp.text, + }; } /** - * Firebase error code structure. This extends Error. + * Defines error info type. This includes a code and message string. */ -export class FirebaseError extends Error implements FirebaseErrorInterface { - /** - * @param errorInfo - The error information (code and message). - * @constructor - * @internal - */ - constructor(private errorInfo: ErrorInfo) { - super(errorInfo.message); - - /* tslint:disable:max-line-length */ - // Set the prototype explicitly. See the following link for more details: - // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work - /* tslint:enable:max-line-length */ - (this as any).__proto__ = FirebaseError.prototype; - } - - /** @returns The error code. */ - public get code(): string { - return this.errorInfo?.code; - } - - /** @returns The error message. */ - public get message(): string { - return this.errorInfo?.message; - } - - /** @returns The object representation of the error. */ - public toJSON(): object { - return { - code: this.code, - message: this.message, - }; - } +export interface ErrorInfo { + /** The string error code. */ + code: string; + /** The error message. */ + message: string; + /** The HTTP response associated with this error, if any. */ + httpResponse?: HttpResponse; + /** The original wrapped error that triggered this error, if any. */ + cause?: Error; } /** - * A FirebaseError with a prefix in front of the error code. + * `FirebaseError` is a subclass of the standard JavaScript `Error` object. In + * addition to a message string and stack trace, it contains a string code. */ -export class PrefixedFirebaseError extends FirebaseError { - /** - * @param codePrefix - The prefix to apply to the error code. - * @param code - The error code. - * @param message - The error message. - * @constructor - * @internal - */ - constructor(private codePrefix: string, code: string, message: string) { - super({ - code: `${codePrefix}/${code}`, - message, - }); - - /* tslint:disable:max-line-length */ - // Set the prototype explicitly. See the following link for more details: - // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work - /* tslint:enable:max-line-length */ - (this as any).__proto__ = PrefixedFirebaseError.prototype; - } - +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +export interface FirebaseError { /** - * Allows the error type to be checked without needing to know implementation details - * of the code prefixing. + * Error codes are strings using the following format: `"service/string-code"`. + * Some examples include `"auth/invalid-uid"` and + * `"messaging/invalid-recipient"`. * - * @param code - The non-prefixed error code to test against. - * @returns True if the code matches, false otherwise. - */ - public hasCode(code: string): boolean { - return `${this.codePrefix}/${code}` === this.code; - } -} - -/** - * Firebase App error code structure. This extends PrefixedFirebaseError. - */ -export class FirebaseAppError extends PrefixedFirebaseError { - /** - * @param code - The error code. - * @param message - The error message. - * @constructor - * @internal + * While the message for a given error can change, the code will remain the same + * between backward-compatible versions of the Firebase SDK. */ - constructor(code: string, message: string) { - super('app', code, message); - - /* tslint:disable:max-line-length */ - // Set the prototype explicitly. See the following link for more details: - // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work - /* tslint:enable:max-line-length */ - (this as any).__proto__ = FirebaseAppError.prototype; - } -} + code: string; -/** - * Firebase Auth error code structure. This extends PrefixedFirebaseError. - */ -export class FirebaseAuthError extends PrefixedFirebaseError { /** - * Creates the developer-facing error corresponding to the backend error code. + * An explanatory message for the error that just occurred. * - * @param serverErrorCode - The server error code. - * @param [message] The error message. The default message is used - * if not provided. - * @param [rawServerResponse] The error's raw server response. - * @returns The corresponding developer-facing error. - * @internal + * This message is designed to be helpful to you, the developer. Because + * it generally does not convey meaningful information to end users, + * this message should not be displayed in your application. */ - public static fromServerError( - serverErrorCode: string, - message?: string, - rawServerResponse?: object, - ): FirebaseAuthError { - // serverErrorCode could contain additional details: - // ERROR_CODE : Detailed message which can also contain colons - const colonSeparator = (serverErrorCode || '').indexOf(':'); - let customMessage = null; - if (colonSeparator !== -1) { - customMessage = serverErrorCode.substring(colonSeparator + 1).trim(); - serverErrorCode = serverErrorCode.substring(0, colonSeparator).trim(); - } - // If not found, default to internal error. - const clientCodeKey = AUTH_SERVER_TO_CLIENT_CODE[serverErrorCode] || 'INTERNAL_ERROR'; - const error: ErrorInfo = deepCopy((AuthClientErrorCode as any)[clientCodeKey]); - // Server detailed message should have highest priority. - error.message = customMessage || message || error.message; - - if (clientCodeKey === 'INTERNAL_ERROR' && typeof rawServerResponse !== 'undefined') { - try { - error.message += ` Raw server response: "${JSON.stringify(rawServerResponse)}"`; - } catch (e) { - // Ignore JSON parsing error. - } - } - - return new FirebaseAuthError(error); - } - - /** - * @param info - The error code info. - * @param message - The error message. This will override the default message if provided. - * @constructor - * @internal - */ - constructor(info: ErrorInfo, message?: string) { - // Override default message if custom message provided. - super('auth', info.code, message || info.message); - - /* tslint:disable:max-line-length */ - // Set the prototype explicitly. See the following link for more details: - // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work - /* tslint:enable:max-line-length */ - (this as any).__proto__ = FirebaseAuthError.prototype; - } -} + message: string; -/** - * Firebase Database error code structure. This extends FirebaseError. - */ -export class FirebaseDatabaseError extends FirebaseError { /** - * @param info - The error code info. - * @param message - The error message. This will override the default - * message if provided. - * @constructor - * @internal + * A string value containing the execution backtrace when the error originally + * occurred. + * + * This information can be useful for troubleshooting the cause of the error with + * {@link https://firebase.google.com/support | Firebase Support}. */ - constructor(info: ErrorInfo, message?: string) { - // Override default message if custom message provided. - super({ code: 'database/' + info.code, message: message || info.message }); - } -} + stack?: string; -/** - * Firebase Firestore error code structure. This extends FirebaseError. - */ -export class FirebaseFirestoreError extends FirebaseError { /** - * @param info - The error code info. - * @param message - The error message. This will override the default - * message if provided. - * @constructor - * @internal + * The HTTP response associated with this error, if any. */ - constructor(info: ErrorInfo, message?: string) { - // Override default message if custom message provided. - super({ code: 'firestore/' + info.code, message: message || info.message }); - } -} + httpResponse?: HttpResponse; -/** - * Firebase instance ID error code structure. This extends FirebaseError. - */ -export class FirebaseInstanceIdError extends FirebaseError { /** - * - * @param info - The error code info. - * @param message - The error message. This will override the default - * message if provided. - * @constructor - * @internal + * The original wrapped error that triggered this error, if any. */ - constructor(info: ErrorInfo, message?: string) { - // Override default message if custom message provided. - super({ code: 'instance-id/' + info.code, message: message || info.message }); - (this as any).__proto__ = FirebaseInstanceIdError.prototype; - } -} + cause?: Error; -/** - * Firebase Installations service error code structure. This extends `FirebaseError`. - */ -export class FirebaseInstallationsError extends FirebaseError { /** - * - * @param info - The error code info. - * @param message - The error message. This will override the default - * message if provided. - * @constructor - * @internal + * Returns a JSON-serializable object representation of this error. + * + * @returns A JSON-serializable representation of this object. */ - constructor(info: ErrorInfo, message?: string) { - // Override default message if custom message provided. - super({ code: 'installations/' + info.code, message: message || info.message }); - (this as any).__proto__ = FirebaseInstallationsError.prototype; - } + toJSON(): object; } - /** - * Firebase Messaging error code structure. This extends PrefixedFirebaseError. + * Firebase error code structure. This extends Error. */ -export class FirebaseMessagingError extends PrefixedFirebaseError { +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +export class FirebaseError extends Error implements FirebaseError { /** - * Creates the developer-facing error corresponding to the backend error code. - * - * @param serverErrorCode - The server error code. - * @param [message] The error message. The default message is used - * if not provided. - * @param [rawServerResponse] The error's raw server response. - * @returns The corresponding developer-facing error. - * @internal + * @param errorInfo - The error information (code and message). */ - public static fromServerError( - serverErrorCode: string | null, - message?: string | null, - rawServerResponse?: object, - ): FirebaseMessagingError { - // If not found, default to unknown error. - let clientCodeKey = 'UNKNOWN_ERROR'; - if (serverErrorCode && serverErrorCode in MESSAGING_SERVER_TO_CLIENT_CODE) { - clientCodeKey = MESSAGING_SERVER_TO_CLIENT_CODE[serverErrorCode]; + constructor(errorInfo: ErrorInfo) { + super(errorInfo.message); + this.code = errorInfo.code; + if (errorInfo.cause !== undefined) { + this.cause = errorInfo.cause; } - const error: ErrorInfo = deepCopy((MessagingClientErrorCode as any)[clientCodeKey]); - error.message = message || error.message; - - if (clientCodeKey === 'UNKNOWN_ERROR' && typeof rawServerResponse !== 'undefined') { - try { - error.message += ` Raw server response: "${JSON.stringify(rawServerResponse)}"`; - } catch (e) { - // Ignore JSON parsing error. - } + if (errorInfo.httpResponse !== undefined) { + this.httpResponse = errorInfo.httpResponse; } - - return new FirebaseMessagingError(error); - } - - /** - * @internal - */ - public static fromTopicManagementServerError( - serverErrorCode: string, - message?: string, - rawServerResponse?: object, - ): FirebaseMessagingError { - // If not found, default to unknown error. - const clientCodeKey = TOPIC_MGT_SERVER_TO_CLIENT_CODE[serverErrorCode] || 'UNKNOWN_ERROR'; - const error: ErrorInfo = deepCopy((MessagingClientErrorCode as any)[clientCodeKey]); - error.message = message || error.message; - - if (clientCodeKey === 'UNKNOWN_ERROR' && typeof rawServerResponse !== 'undefined') { - try { - error.message += ` Raw server response: "${JSON.stringify(rawServerResponse)}"`; - } catch (e) { - // Ignore JSON parsing error. - } - } - - return new FirebaseMessagingError(error); - } - - /** - * - * @param info - The error code info. - * @param message - The error message. This will override the default message if provided. - * @constructor - * @internal - */ - constructor(info: ErrorInfo, message?: string) { - // Override default message if custom message provided. - super('messaging', info.code, message || info.message); - - /* tslint:disable:max-line-length */ - // Set the prototype explicitly. See the following link for more details: - // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work - /* tslint:enable:max-line-length */ - (this as any).__proto__ = FirebaseMessagingError.prototype; - } -} - -export class FirebaseMessagingSessionError extends FirebaseMessagingError { - public pendingBatchResponse?: Promise; - /** - * - * @param info - The error code info. - * @param message - The error message. This will override the default message if provided. - * @param pendingBatchResponse - BatchResponse for pending messages when session error occured. - * @constructor - * @internal - */ - constructor(info: ErrorInfo, message?: string, pendingBatchResponse?: Promise) { - // Override default message if custom message provided. - super(info, message || info.message); - this.pendingBatchResponse = pendingBatchResponse; - - /* tslint:disable:max-line-length */ - // Set the prototype explicitly. See the following link for more details: - // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work - /* tslint:enable:max-line-length */ - (this as any).__proto__ = FirebaseMessagingSessionError.prototype; } /** @returns The object representation of the error. */ public toJSON(): object { - return { + const json: any = { code: this.code, message: this.message, - pendingBatchResponse: this.pendingBatchResponse, }; + if (this.httpResponse) { + json.httpResponse = { + status: this.httpResponse.status, + headers: this.httpResponse.headers, + data: this.httpResponse.data, + }; + } + if (this.cause) { + json.cause = { + name: this.cause.name || 'Error', + message: this.cause.message || String(this.cause), + stack: this.cause.stack + }; + } + return json; } } /** - * Firebase project management error code structure. This extends PrefixedFirebaseError. + * A FirebaseError with a prefix in front of the error code. */ -export class FirebaseProjectManagementError extends PrefixedFirebaseError { +export class PrefixedFirebaseError extends FirebaseError { /** + * @param codePrefix - The prefix to apply to the error code. * @param code - The error code. * @param message - The error message. - * @constructor * @internal */ - constructor(code: ProjectManagementErrorCode, message: string) { - super('project-management', code, message); - - /* tslint:disable:max-line-length */ - // Set the prototype explicitly. See the following link for more details: - // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work - /* tslint:enable:max-line-length */ - (this as any).__proto__ = FirebaseProjectManagementError.prototype; + constructor(private codePrefix: string, code: string, message: string, httpResponse?: HttpResponse, cause?: Error) { + const errorInfo: ErrorInfo = { + code: `${codePrefix}/${code}`, + message, + }; + if (httpResponse !== undefined) { + errorInfo.httpResponse = httpResponse; + } + if (cause !== undefined) { + errorInfo.cause = cause; + } + super(errorInfo); } -} - -/** - * App client error codes and their default messages. - */ -export class AppErrorCodes { - public static APP_DELETED = 'app-deleted'; - public static DUPLICATE_APP = 'duplicate-app'; - public static INVALID_ARGUMENT = 'invalid-argument'; - public static INTERNAL_ERROR = 'internal-error'; - public static INVALID_APP_NAME = 'invalid-app-name'; - public static INVALID_APP_OPTIONS = 'invalid-app-options'; - public static INVALID_CREDENTIAL = 'invalid-credential'; - public static NETWORK_ERROR = 'network-error'; - public static NETWORK_TIMEOUT = 'network-timeout'; - public static NO_APP = 'no-app'; - public static UNABLE_TO_PARSE_RESPONSE = 'unable-to-parse-response'; -} -/** - * Auth client error codes and their default messages. - */ -export class AuthClientErrorCode { - public static AUTH_BLOCKING_TOKEN_EXPIRED = { - code: 'auth-blocking-token-expired', - message: 'The provided Firebase Auth Blocking token is expired.', - }; - public static BILLING_NOT_ENABLED = { - code: 'billing-not-enabled', - message: 'Feature requires billing to be enabled.', - }; - public static CLAIMS_TOO_LARGE = { - code: 'claims-too-large', - message: 'Developer claims maximum payload size exceeded.', - }; - public static CONFIGURATION_EXISTS = { - code: 'configuration-exists', - message: 'A configuration already exists with the provided identifier.', - }; - public static CONFIGURATION_NOT_FOUND = { - code: 'configuration-not-found', - message: 'There is no configuration corresponding to the provided identifier.', - }; - public static ID_TOKEN_EXPIRED = { - code: 'id-token-expired', - message: 'The provided Firebase ID token is expired.', - }; - public static INVALID_ARGUMENT = { - code: 'argument-error', - message: 'Invalid argument provided.', - }; - public static INVALID_CONFIG = { - code: 'invalid-config', - message: 'The provided configuration is invalid.', - }; - public static EMAIL_ALREADY_EXISTS = { - code: 'email-already-exists', - message: 'The email address is already in use by another account.', - }; - public static EMAIL_NOT_FOUND = { - code: 'email-not-found', - message: 'There is no user record corresponding to the provided email.', - }; - public static FORBIDDEN_CLAIM = { - code: 'reserved-claim', - message: 'The specified developer claim is reserved and cannot be specified.', - }; - public static INVALID_ID_TOKEN = { - code: 'invalid-id-token', - message: 'The provided ID token is not a valid Firebase ID token.', - }; - public static ID_TOKEN_REVOKED = { - code: 'id-token-revoked', - message: 'The Firebase ID token has been revoked.', - }; - public static INTERNAL_ERROR = { - code: 'internal-error', - message: 'An internal error has occurred.', - }; - public static INVALID_CLAIMS = { - code: 'invalid-claims', - message: 'The provided custom claim attributes are invalid.', - }; - public static INVALID_CONTINUE_URI = { - code: 'invalid-continue-uri', - message: 'The continue URL must be a valid URL string.', - }; - public static INVALID_CREATION_TIME = { - code: 'invalid-creation-time', - message: 'The creation time must be a valid UTC date string.', - }; - public static INVALID_CREDENTIAL = { - code: 'invalid-credential', - message: 'Invalid credential object provided.', - }; - public static INVALID_DISABLED_FIELD = { - code: 'invalid-disabled-field', - message: 'The disabled field must be a boolean.', - }; - public static INVALID_DISPLAY_NAME = { - code: 'invalid-display-name', - message: 'The displayName field must be a valid string.', - }; - public static INVALID_DYNAMIC_LINK_DOMAIN = { - code: 'invalid-dynamic-link-domain', - message: 'The provided dynamic link domain is not configured or authorized ' + - 'for the current project.', - }; - public static INVALID_HOSTING_LINK_DOMAIN = { - code: 'invalid-hosting-link-domain', - message: 'The provided hosting link domain is not configured in Firebase ' + - 'Hosting or is not owned by the current project.', - }; - public static INVALID_EMAIL_VERIFIED = { - code: 'invalid-email-verified', - message: 'The emailVerified field must be a boolean.', - }; - public static INVALID_EMAIL = { - code: 'invalid-email', - message: 'The email address is improperly formatted.', - }; - public static INVALID_NEW_EMAIL = { - code: 'invalid-new-email', - message: 'The new email address is improperly formatted.', - }; - public static INVALID_ENROLLED_FACTORS = { - code: 'invalid-enrolled-factors', - message: 'The enrolled factors must be a valid array of MultiFactorInfo objects.', - }; - public static INVALID_ENROLLMENT_TIME = { - code: 'invalid-enrollment-time', - message: 'The second factor enrollment time must be a valid UTC date string.', - }; - public static INVALID_HASH_ALGORITHM = { - code: 'invalid-hash-algorithm', - message: 'The hash algorithm must match one of the strings in the list of ' + - 'supported algorithms.', - }; - public static INVALID_HASH_BLOCK_SIZE = { - code: 'invalid-hash-block-size', - message: 'The hash block size must be a valid number.', - }; - public static INVALID_HASH_DERIVED_KEY_LENGTH = { - code: 'invalid-hash-derived-key-length', - message: 'The hash derived key length must be a valid number.', - }; - public static INVALID_HASH_KEY = { - code: 'invalid-hash-key', - message: 'The hash key must a valid byte buffer.', - }; - public static INVALID_HASH_MEMORY_COST = { - code: 'invalid-hash-memory-cost', - message: 'The hash memory cost must be a valid number.', - }; - public static INVALID_HASH_PARALLELIZATION = { - code: 'invalid-hash-parallelization', - message: 'The hash parallelization must be a valid number.', - }; - public static INVALID_HASH_ROUNDS = { - code: 'invalid-hash-rounds', - message: 'The hash rounds must be a valid number.', - }; - public static INVALID_HASH_SALT_SEPARATOR = { - code: 'invalid-hash-salt-separator', - message: 'The hashing algorithm salt separator field must be a valid byte buffer.', - }; - public static INVALID_LAST_SIGN_IN_TIME = { - code: 'invalid-last-sign-in-time', - message: 'The last sign-in time must be a valid UTC date string.', - }; - public static INVALID_NAME = { - code: 'invalid-name', - message: 'The resource name provided is invalid.', - }; - public static INVALID_OAUTH_CLIENT_ID = { - code: 'invalid-oauth-client-id', - message: 'The provided OAuth client ID is invalid.', - }; - public static INVALID_PAGE_TOKEN = { - code: 'invalid-page-token', - message: 'The page token must be a valid non-empty string.', - }; - public static INVALID_PASSWORD = { - code: 'invalid-password', - message: 'The password must be a string with at least 6 characters.', - }; - public static INVALID_PASSWORD_HASH = { - code: 'invalid-password-hash', - message: 'The password hash must be a valid byte buffer.', - }; - public static INVALID_PASSWORD_SALT = { - code: 'invalid-password-salt', - message: 'The password salt must be a valid byte buffer.', - }; - public static INVALID_PHONE_NUMBER = { - code: 'invalid-phone-number', - message: 'The phone number must be a non-empty E.164 standard compliant identifier ' + - 'string.', - }; - public static INVALID_PHOTO_URL = { - code: 'invalid-photo-url', - message: 'The photoURL field must be a valid URL.', - }; - public static INVALID_PROJECT_ID = { - code: 'invalid-project-id', - message: 'Invalid parent project. Either parent project doesn\'t exist or didn\'t enable multi-tenancy.', - }; - public static INVALID_PROVIDER_DATA = { - code: 'invalid-provider-data', - message: 'The providerData must be a valid array of UserInfo objects.', - }; - public static INVALID_PROVIDER_ID = { - code: 'invalid-provider-id', - message: 'The providerId must be a valid supported provider identifier string.', - }; - public static INVALID_PROVIDER_UID = { - code: 'invalid-provider-uid', - message: 'The providerUid must be a valid provider uid string.', - }; - public static INVALID_OAUTH_RESPONSETYPE = { - code: 'invalid-oauth-responsetype', - message: 'Only exactly one OAuth responseType should be set to true.', - }; - public static INVALID_SESSION_COOKIE_DURATION = { - code: 'invalid-session-cookie-duration', - message: 'The session cookie duration must be a valid number in milliseconds ' + - 'between 5 minutes and 2 weeks.', - }; - public static INVALID_TENANT_ID = { - code: 'invalid-tenant-id', - message: 'The tenant ID must be a valid non-empty string.', - }; - public static INVALID_TENANT_TYPE = { - code: 'invalid-tenant-type', - message: 'Tenant type must be either "full_service" or "lightweight".', - }; - public static INVALID_TESTING_PHONE_NUMBER = { - code: 'invalid-testing-phone-number', - message: 'Invalid testing phone number or invalid test code provided.', - }; - public static INVALID_UID = { - code: 'invalid-uid', - message: 'The uid must be a non-empty string with at most 128 characters.', - }; - public static INVALID_USER_IMPORT = { - code: 'invalid-user-import', - message: 'The user record to import is invalid.', - }; - public static INVALID_TOKENS_VALID_AFTER_TIME = { - code: 'invalid-tokens-valid-after-time', - message: 'The tokensValidAfterTime must be a valid UTC number in seconds.', - }; - public static MISMATCHING_TENANT_ID = { - code: 'mismatching-tenant-id', - message: 'User tenant ID does not match with the current TenantAwareAuth tenant ID.', - }; - public static MISSING_ANDROID_PACKAGE_NAME = { - code: 'missing-android-pkg-name', - message: 'An Android Package Name must be provided if the Android App is ' + - 'required to be installed.', - }; - public static MISSING_CONFIG = { - code: 'missing-config', - message: 'The provided configuration is missing required attributes.', - }; - public static MISSING_CONTINUE_URI = { - code: 'missing-continue-uri', - message: 'A valid continue URL must be provided in the request.', - }; - public static MISSING_DISPLAY_NAME = { - code: 'missing-display-name', - message: 'The resource being created or edited is missing a valid display name.', - }; - public static MISSING_EMAIL = { - code: 'missing-email', - message: 'The email is required for the specified action. For example, a multi-factor user ' + - 'requires a verified email.', - }; - public static MISSING_IOS_BUNDLE_ID = { - code: 'missing-ios-bundle-id', - message: 'The request is missing an iOS Bundle ID.', - }; - public static MISSING_ISSUER = { - code: 'missing-issuer', - message: 'The OAuth/OIDC configuration issuer must not be empty.', - }; - public static MISSING_HASH_ALGORITHM = { - code: 'missing-hash-algorithm', - message: 'Importing users with password hashes requires that the hashing ' + - 'algorithm and its parameters be provided.', - }; - public static MISSING_OAUTH_CLIENT_ID = { - code: 'missing-oauth-client-id', - message: 'The OAuth/OIDC configuration client ID must not be empty.', - }; - public static MISSING_OAUTH_CLIENT_SECRET = { - code: 'missing-oauth-client-secret', - message: 'The OAuth configuration client secret is required to enable OIDC code flow.', - }; - public static MISSING_PROVIDER_ID = { - code: 'missing-provider-id', - message: 'A valid provider ID must be provided in the request.', - }; - public static MISSING_SAML_RELYING_PARTY_CONFIG = { - code: 'missing-saml-relying-party-config', - message: 'The SAML configuration provided is missing a relying party configuration.', - }; - public static MAXIMUM_TEST_PHONE_NUMBER_EXCEEDED = { - code: 'test-phone-number-limit-exceeded', - message: 'The maximum allowed number of test phone number / code pairs has been exceeded.', - }; - public static MAXIMUM_USER_COUNT_EXCEEDED = { - code: 'maximum-user-count-exceeded', - message: 'The maximum allowed number of users to import has been exceeded.', - }; - public static MISSING_UID = { - code: 'missing-uid', - message: 'A uid identifier is required for the current operation.', - }; - public static OPERATION_NOT_ALLOWED = { - code: 'operation-not-allowed', - message: 'The given sign-in provider is disabled for this Firebase project. ' + - 'Enable it in the Firebase console, under the sign-in method tab of the ' + - 'Auth section.', - }; - public static PHONE_NUMBER_ALREADY_EXISTS = { - code: 'phone-number-already-exists', - message: 'The user with the provided phone number already exists.', - }; - public static PROJECT_NOT_FOUND = { - code: 'project-not-found', - message: 'No Firebase project was found for the provided credential.', - }; - public static INSUFFICIENT_PERMISSION = { - code: 'insufficient-permission', - message: 'Credential implementation provided to initializeApp() via the "credential" property ' + - 'has insufficient permission to access the requested resource. See ' + - 'https://firebase.google.com/docs/admin/setup for details on how to authenticate this SDK ' + - 'with appropriate permissions.', - }; - public static QUOTA_EXCEEDED = { - code: 'quota-exceeded', - message: 'The project quota for the specified operation has been exceeded.', - }; - public static SECOND_FACTOR_LIMIT_EXCEEDED = { - code: 'second-factor-limit-exceeded', - message: 'The maximum number of allowed second factors on a user has been exceeded.', - }; - public static SECOND_FACTOR_UID_ALREADY_EXISTS = { - code: 'second-factor-uid-already-exists', - message: 'The specified second factor "uid" already exists.', - }; - public static SESSION_COOKIE_EXPIRED = { - code: 'session-cookie-expired', - message: 'The Firebase session cookie is expired.', - }; - public static SESSION_COOKIE_REVOKED = { - code: 'session-cookie-revoked', - message: 'The Firebase session cookie has been revoked.', - }; - public static TENANT_NOT_FOUND = { - code: 'tenant-not-found', - message: 'There is no tenant corresponding to the provided identifier.', - }; - public static UID_ALREADY_EXISTS = { - code: 'uid-already-exists', - message: 'The user with the provided uid already exists.', - }; - public static UNAUTHORIZED_DOMAIN = { - code: 'unauthorized-continue-uri', - message: 'The domain of the continue URL is not whitelisted. Whitelist the domain in the ' + - 'Firebase console.', - }; - public static UNSUPPORTED_FIRST_FACTOR = { - code: 'unsupported-first-factor', - message: 'A multi-factor user requires a supported first factor.', - }; - public static UNSUPPORTED_SECOND_FACTOR = { - code: 'unsupported-second-factor', - message: 'The request specified an unsupported type of second factor.', - }; - public static UNSUPPORTED_TENANT_OPERATION = { - code: 'unsupported-tenant-operation', - message: 'This operation is not supported in a multi-tenant context.', - }; - public static UNVERIFIED_EMAIL = { - code: 'unverified-email', - message: 'A verified email is required for the specified action. For example, a multi-factor user ' + - 'requires a verified email.', - }; - public static USER_NOT_FOUND = { - code: 'user-not-found', - message: 'There is no user record corresponding to the provided identifier.', - }; - public static NOT_FOUND = { - code: 'not-found', - message: 'The requested resource was not found.', - }; - public static USER_DISABLED = { - code: 'user-disabled', - message: 'The user record is disabled.', - } - public static USER_NOT_DISABLED = { - code: 'user-not-disabled', - message: 'The user must be disabled in order to bulk delete it (or you must pass force=true).', - }; - public static INVALID_RECAPTCHA_ACTION = { - code: 'invalid-recaptcha-action', - message: 'reCAPTCHA action must be "BLOCK".' - } - public static INVALID_RECAPTCHA_ENFORCEMENT_STATE = { - code: 'invalid-recaptcha-enforcement-state', - message: 'reCAPTCHA enforcement state must be either "OFF", "AUDIT" or "ENFORCE".' - } - public static RECAPTCHA_NOT_ENABLED = { - code: 'racaptcha-not-enabled', - message: 'reCAPTCHA enterprise is not enabled.' + /** + * Allows the error type to be checked without needing to know implementation details + * of the code prefixing. + * + * @param code - The non-prefixed error code to test against. + * @returns True if the code matches, false otherwise. + */ + public hasCode(code: string): boolean { + return `${this.codePrefix}/${code}` === this.code; } } - -/** - * Messaging client error codes and their default messages. - */ -export class MessagingClientErrorCode { - public static INVALID_ARGUMENT = { - code: 'invalid-argument', - message: 'Invalid argument provided.', - }; - public static INVALID_RECIPIENT = { - code: 'invalid-recipient', - message: 'Invalid message recipient provided.', - }; - public static INVALID_PAYLOAD = { - code: 'invalid-payload', - message: 'Invalid message payload provided.', - }; - public static INVALID_DATA_PAYLOAD_KEY = { - code: 'invalid-data-payload-key', - message: 'The data message payload contains an invalid key. See the reference documentation ' + - 'for the DataMessagePayload type for restricted keys.', - }; - public static PAYLOAD_SIZE_LIMIT_EXCEEDED = { - code: 'payload-size-limit-exceeded', - message: 'The provided message payload exceeds the FCM size limits. See the error documentation ' + - 'for more details.', - }; - public static INVALID_OPTIONS = { - code: 'invalid-options', - message: 'Invalid message options provided.', - }; - public static INVALID_REGISTRATION_TOKEN = { - code: 'invalid-registration-token', - message: 'Invalid registration token provided. Make sure it matches the registration token ' + - 'the client app receives from registering with FCM.', - }; - public static REGISTRATION_TOKEN_NOT_REGISTERED = { - code: 'registration-token-not-registered', - message: 'The provided registration token is not registered. A previously valid registration ' + - 'token can be unregistered for a variety of reasons. See the error documentation for more ' + - 'details. Remove this registration token and stop using it to send messages.', - }; - public static MISMATCHED_CREDENTIAL = { - code: 'mismatched-credential', - message: 'The credential used to authenticate this SDK does not have permission to send ' + - 'messages to the device corresponding to the provided registration token. Make sure the ' + - 'credential and registration token both belong to the same Firebase project.', - }; - public static INVALID_PACKAGE_NAME = { - code: 'invalid-package-name', - message: 'The message was addressed to a registration token whose package name does not match ' + - 'the provided "restrictedPackageName" option.', - }; - public static DEVICE_MESSAGE_RATE_EXCEEDED = { - code: 'device-message-rate-exceeded', - message: 'The rate of messages to a particular device is too high. Reduce the number of ' + - 'messages sent to this device and do not immediately retry sending to this device.', - }; - public static TOPICS_MESSAGE_RATE_EXCEEDED = { - code: 'topics-message-rate-exceeded', - message: 'The rate of messages to subscribers to a particular topic is too high. Reduce the ' + - 'number of messages sent for this topic, and do not immediately retry sending to this topic.', - }; - public static MESSAGE_RATE_EXCEEDED = { - code: 'message-rate-exceeded', - message: 'Sending limit exceeded for the message target.', - }; - public static THIRD_PARTY_AUTH_ERROR = { - code: 'third-party-auth-error', - message: 'A message targeted to an iOS device could not be sent because the required APNs ' + - 'SSL certificate was not uploaded or has expired. Check the validity of your development ' + - 'and production certificates.', - }; - public static TOO_MANY_TOPICS = { - code: 'too-many-topics', - message: 'The maximum number of topics the provided registration token can be subscribed to ' + - 'has been exceeded.', - }; - public static AUTHENTICATION_ERROR = { - code: 'authentication-error', - message: 'An error occurred when trying to authenticate to the FCM servers. Make sure the ' + - 'credential used to authenticate this SDK has the proper permissions. See ' + - 'https://firebase.google.com/docs/admin/setup for setup instructions.', - }; - public static SERVER_UNAVAILABLE = { - code: 'server-unavailable', - message: 'The FCM server could not process the request in time. See the error documentation ' + - 'for more details.', - }; - public static INTERNAL_ERROR = { - code: 'internal-error', - message: 'An internal error has occurred. Please retry the request.', - }; - public static UNKNOWN_ERROR = { - code: 'unknown-error', - message: 'An unknown server error was returned.', - }; -} - -export class InstallationsClientErrorCode { - public static INVALID_ARGUMENT = { - code: 'invalid-argument', - message: 'Invalid argument provided.', - }; - public static INVALID_PROJECT_ID = { - code: 'invalid-project-id', - message: 'Invalid project ID provided.', - }; - public static INVALID_INSTALLATION_ID = { - code: 'invalid-installation-id', - message: 'Invalid installation ID provided.', - }; - public static API_ERROR = { - code: 'api-error', - message: 'Installation ID API call failed.', - }; -} - -export class InstanceIdClientErrorCode extends InstallationsClientErrorCode { - public static INVALID_INSTANCE_ID = { - code: 'invalid-instance-id', - message: 'Invalid instance ID provided.', - }; -} - -export type ProjectManagementErrorCode = - 'already-exists' - | 'authentication-error' - | 'internal-error' - | 'invalid-argument' - | 'invalid-project-id' - | 'invalid-server-response' - | 'not-found' - | 'service-unavailable' - | 'unknown-error'; - -/** @const {ServerToClientCode} Auth server to client enum error codes. */ -const AUTH_SERVER_TO_CLIENT_CODE: ServerToClientCode = { - // Feature being configured or used requires a billing account. - BILLING_NOT_ENABLED: 'BILLING_NOT_ENABLED', - // Claims payload is too large. - CLAIMS_TOO_LARGE: 'CLAIMS_TOO_LARGE', - // Configuration being added already exists. - CONFIGURATION_EXISTS: 'CONFIGURATION_EXISTS', - // Configuration not found. - CONFIGURATION_NOT_FOUND: 'CONFIGURATION_NOT_FOUND', - // Provided credential has insufficient permissions. - INSUFFICIENT_PERMISSION: 'INSUFFICIENT_PERMISSION', - // Provided configuration has invalid fields. - INVALID_CONFIG: 'INVALID_CONFIG', - // Provided configuration identifier is invalid. - INVALID_CONFIG_ID: 'INVALID_PROVIDER_ID', - // ActionCodeSettings missing continue URL. - INVALID_CONTINUE_URI: 'INVALID_CONTINUE_URI', - // Dynamic link domain in provided ActionCodeSettings is not authorized. - INVALID_DYNAMIC_LINK_DOMAIN: 'INVALID_DYNAMIC_LINK_DOMAIN', - // Hosting link domain in provided ActionCodeSettings is not owned by the current project. - INVALID_HOSTING_LINK_DOMAIN: 'INVALID_HOSTING_LINK_DOMAIN', - // uploadAccount provides an email that already exists. - DUPLICATE_EMAIL: 'EMAIL_ALREADY_EXISTS', - // uploadAccount provides a localId that already exists. - DUPLICATE_LOCAL_ID: 'UID_ALREADY_EXISTS', - // Request specified a multi-factor enrollment ID that already exists. - DUPLICATE_MFA_ENROLLMENT_ID: 'SECOND_FACTOR_UID_ALREADY_EXISTS', - // setAccountInfo email already exists. - EMAIL_EXISTS: 'EMAIL_ALREADY_EXISTS', - // /accounts:sendOobCode for password reset when user is not found. - EMAIL_NOT_FOUND: 'EMAIL_NOT_FOUND', - // Reserved claim name. - FORBIDDEN_CLAIM: 'FORBIDDEN_CLAIM', - // Invalid claims provided. - INVALID_CLAIMS: 'INVALID_CLAIMS', - // Invalid session cookie duration. - INVALID_DURATION: 'INVALID_SESSION_COOKIE_DURATION', - // Invalid email provided. - INVALID_EMAIL: 'INVALID_EMAIL', - // Invalid new email provided. - INVALID_NEW_EMAIL: 'INVALID_NEW_EMAIL', - // Invalid tenant display name. This can be thrown on CreateTenant and UpdateTenant. - INVALID_DISPLAY_NAME: 'INVALID_DISPLAY_NAME', - // Invalid ID token provided. - INVALID_ID_TOKEN: 'INVALID_ID_TOKEN', - // Invalid tenant/parent resource name. - INVALID_NAME: 'INVALID_NAME', - // OIDC configuration has an invalid OAuth client ID. - INVALID_OAUTH_CLIENT_ID: 'INVALID_OAUTH_CLIENT_ID', - // Invalid page token. - INVALID_PAGE_SELECTION: 'INVALID_PAGE_TOKEN', - // Invalid phone number. - INVALID_PHONE_NUMBER: 'INVALID_PHONE_NUMBER', - // Invalid agent project. Either agent project doesn't exist or didn't enable multi-tenancy. - INVALID_PROJECT_ID: 'INVALID_PROJECT_ID', - // Invalid provider ID. - INVALID_PROVIDER_ID: 'INVALID_PROVIDER_ID', - // Invalid service account. - INVALID_SERVICE_ACCOUNT: 'INVALID_SERVICE_ACCOUNT', - // Invalid testing phone number. - INVALID_TESTING_PHONE_NUMBER: 'INVALID_TESTING_PHONE_NUMBER', - // Invalid tenant type. - INVALID_TENANT_TYPE: 'INVALID_TENANT_TYPE', - // Missing Android package name. - MISSING_ANDROID_PACKAGE_NAME: 'MISSING_ANDROID_PACKAGE_NAME', - // Missing configuration. - MISSING_CONFIG: 'MISSING_CONFIG', - // Missing configuration identifier. - MISSING_CONFIG_ID: 'MISSING_PROVIDER_ID', - // Missing tenant display name: This can be thrown on CreateTenant and UpdateTenant. - MISSING_DISPLAY_NAME: 'MISSING_DISPLAY_NAME', - // Email is required for the specified action. For example a multi-factor user requires - // a verified email. - MISSING_EMAIL: 'MISSING_EMAIL', - // Missing iOS bundle ID. - MISSING_IOS_BUNDLE_ID: 'MISSING_IOS_BUNDLE_ID', - // Missing OIDC issuer. - MISSING_ISSUER: 'MISSING_ISSUER', - // No localId provided (deleteAccount missing localId). - MISSING_LOCAL_ID: 'MISSING_UID', - // OIDC configuration is missing an OAuth client ID. - MISSING_OAUTH_CLIENT_ID: 'MISSING_OAUTH_CLIENT_ID', - // Missing provider ID. - MISSING_PROVIDER_ID: 'MISSING_PROVIDER_ID', - // Missing SAML RP config. - MISSING_SAML_RELYING_PARTY_CONFIG: 'MISSING_SAML_RELYING_PARTY_CONFIG', - // Empty user list in uploadAccount. - MISSING_USER_ACCOUNT: 'MISSING_UID', - // Password auth disabled in console. - OPERATION_NOT_ALLOWED: 'OPERATION_NOT_ALLOWED', - // Provided credential has insufficient permissions. - PERMISSION_DENIED: 'INSUFFICIENT_PERMISSION', - // Phone number already exists. - PHONE_NUMBER_EXISTS: 'PHONE_NUMBER_ALREADY_EXISTS', - // Project not found. - PROJECT_NOT_FOUND: 'PROJECT_NOT_FOUND', - // In multi-tenancy context: project creation quota exceeded. - QUOTA_EXCEEDED: 'QUOTA_EXCEEDED', - // Currently only 5 second factors can be set on the same user. - SECOND_FACTOR_LIMIT_EXCEEDED: 'SECOND_FACTOR_LIMIT_EXCEEDED', - // Tenant not found. - TENANT_NOT_FOUND: 'TENANT_NOT_FOUND', - // Tenant ID mismatch. - TENANT_ID_MISMATCH: 'MISMATCHING_TENANT_ID', - // Token expired error. - TOKEN_EXPIRED: 'ID_TOKEN_EXPIRED', - // Continue URL provided in ActionCodeSettings has a domain that is not whitelisted. - UNAUTHORIZED_DOMAIN: 'UNAUTHORIZED_DOMAIN', - // A multi-factor user requires a supported first factor. - UNSUPPORTED_FIRST_FACTOR: 'UNSUPPORTED_FIRST_FACTOR', - // The request specified an unsupported type of second factor. - UNSUPPORTED_SECOND_FACTOR: 'UNSUPPORTED_SECOND_FACTOR', - // Operation is not supported in a multi-tenant context. - UNSUPPORTED_TENANT_OPERATION: 'UNSUPPORTED_TENANT_OPERATION', - // A verified email is required for the specified action. For example a multi-factor user - // requires a verified email. - UNVERIFIED_EMAIL: 'UNVERIFIED_EMAIL', - // User on which action is to be performed is not found. - USER_NOT_FOUND: 'USER_NOT_FOUND', - // User record is disabled. - USER_DISABLED: 'USER_DISABLED', - // Password provided is too weak. - WEAK_PASSWORD: 'INVALID_PASSWORD', - // Unrecognized reCAPTCHA action. - INVALID_RECAPTCHA_ACTION: 'INVALID_RECAPTCHA_ACTION', - // Unrecognized reCAPTCHA enforcement state. - INVALID_RECAPTCHA_ENFORCEMENT_STATE: 'INVALID_RECAPTCHA_ENFORCEMENT_STATE', - // reCAPTCHA is not enabled for account defender. - RECAPTCHA_NOT_ENABLED: 'RECAPTCHA_NOT_ENABLED' -}; - -/** @const {ServerToClientCode} Messaging server to client enum error codes. */ -const MESSAGING_SERVER_TO_CLIENT_CODE: ServerToClientCode = { - /* GENERIC ERRORS */ - // Generic invalid message parameter provided. - InvalidParameters: 'INVALID_ARGUMENT', - // Mismatched sender ID. - MismatchSenderId: 'MISMATCHED_CREDENTIAL', - // FCM server unavailable. - Unavailable: 'SERVER_UNAVAILABLE', - // FCM server internal error. - InternalServerError: 'INTERNAL_ERROR', - - /* SEND ERRORS */ - // Invalid registration token format. - InvalidRegistration: 'INVALID_REGISTRATION_TOKEN', - // Registration token is not registered. - NotRegistered: 'REGISTRATION_TOKEN_NOT_REGISTERED', - // Registration token does not match restricted package name. - InvalidPackageName: 'INVALID_PACKAGE_NAME', - // Message payload size limit exceeded. - MessageTooBig: 'PAYLOAD_SIZE_LIMIT_EXCEEDED', - // Invalid key in the data message payload. - InvalidDataKey: 'INVALID_DATA_PAYLOAD_KEY', - // Invalid time to live option. - InvalidTtl: 'INVALID_OPTIONS', - // Device message rate exceeded. - DeviceMessageRateExceeded: 'DEVICE_MESSAGE_RATE_EXCEEDED', - // Topics message rate exceeded. - TopicsMessageRateExceeded: 'TOPICS_MESSAGE_RATE_EXCEEDED', - // Invalid APNs credentials. - InvalidApnsCredential: 'THIRD_PARTY_AUTH_ERROR', - - /* FCM v1 canonical error codes */ - NOT_FOUND: 'REGISTRATION_TOKEN_NOT_REGISTERED', - PERMISSION_DENIED: 'MISMATCHED_CREDENTIAL', - RESOURCE_EXHAUSTED: 'MESSAGE_RATE_EXCEEDED', - UNAUTHENTICATED: 'THIRD_PARTY_AUTH_ERROR', - - /* FCM v1 new error codes */ - APNS_AUTH_ERROR: 'THIRD_PARTY_AUTH_ERROR', - INTERNAL: 'INTERNAL_ERROR', - INVALID_ARGUMENT: 'INVALID_ARGUMENT', - QUOTA_EXCEEDED: 'MESSAGE_RATE_EXCEEDED', - SENDER_ID_MISMATCH: 'MISMATCHED_CREDENTIAL', - THIRD_PARTY_AUTH_ERROR: 'THIRD_PARTY_AUTH_ERROR', - UNAVAILABLE: 'SERVER_UNAVAILABLE', - UNREGISTERED: 'REGISTRATION_TOKEN_NOT_REGISTERED', - UNSPECIFIED_ERROR: 'UNKNOWN_ERROR', -}; - -/** @const {ServerToClientCode} Topic management (IID) server to client enum error codes. */ -const TOPIC_MGT_SERVER_TO_CLIENT_CODE: ServerToClientCode = { - /* TOPIC SUBSCRIPTION MANAGEMENT ERRORS */ - NOT_FOUND: 'REGISTRATION_TOKEN_NOT_REGISTERED', - INVALID_ARGUMENT: 'INVALID_REGISTRATION_TOKEN', - TOO_MANY_TOPICS: 'TOO_MANY_TOPICS', - RESOURCE_EXHAUSTED: 'TOO_MANY_TOPICS', - PERMISSION_DENIED: 'AUTHENTICATION_ERROR', - DEADLINE_EXCEEDED: 'SERVER_UNAVAILABLE', - INTERNAL: 'INTERNAL_ERROR', - UNKNOWN: 'UNKNOWN_ERROR', -}; diff --git a/src/utils/jwt.ts b/src/utils/jwt.ts index 41433c4f06..af09e45dda 100644 --- a/src/utils/jwt.ts +++ b/src/utils/jwt.ts @@ -355,7 +355,6 @@ export function decodeJwt(jwtToken: string): Promise { export class JwtError extends Error { constructor(readonly code: JwtErrorCode, readonly message: string) { super(message); - (this as any).__proto__ = JwtError.prototype; } } diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 3c538d3319..ac3bac3653 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -3549,6 +3549,8 @@ async function deleteUsersWithDelay(uids: string[]): Promise return getAuth().deleteUsers(uids); } + + /** * Asserts actual object is equal to expected object while ignoring key order. * This is useful since to.deep.equal fails when order differs. diff --git a/test/unit/app-check/app-check-api-client-internal.spec.ts b/test/unit/app-check/app-check-api-client-internal.spec.ts index b6a5060bb5..e9e2bfad8b 100644 --- a/test/unit/app-check/app-check-api-client-internal.spec.ts +++ b/test/unit/app-check/app-check-api-client-internal.spec.ts @@ -26,8 +26,10 @@ import * as mocks from '../../resources/mocks'; import { getMetricsHeader, getSdkVersion } from '../../../src/utils'; import { FirebaseApp } from '../../../src/app/firebase-app'; -import { AppCheckApiClient, FirebaseAppCheckError } from '../../../src/app-check/app-check-api-client-internal'; -import { FirebaseAppError } from '../../../src/utils/error'; +import { AppCheckApiClient } from '../../../src/app-check/app-check-api-client-internal'; +import { FirebaseAppCheckError } from '../../../src/app-check/error'; +import { toHttpResponse } from '../../../src/utils/error'; +import { FirebaseAppError } from '../../../src/app/error'; import { deepCopy } from '../../../src/utils/deep-copy'; const expect = chai.expect; @@ -141,38 +143,61 @@ describe('AppCheckApiClient', () => { }); it('should reject when a full platform error response is received', () => { + const mockErr = utils.errorFrom(ERROR_RESPONSE, 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom(ERROR_RESPONSE, 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseAppCheckError('not-found', 'Requested entity not found'); + const expected = new FirebaseAppCheckError({ + code: 'not-found', + message: 'Requested entity not found', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.exchangeToken(TEST_TOKEN_TO_EXCHANGE, APP_ID) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should reject with unknown-error when error code is not present', () => { + const mockErr = utils.errorFrom({}, 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom({}, 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseAppCheckError('unknown-error', 'Unknown server error: {}'); + const expected = new FirebaseAppCheckError({ + code: 'unknown-error', + message: 'Unknown server error: {}', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.exchangeToken(TEST_TOKEN_TO_EXCHANGE, APP_ID) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should reject with unknown-error for non-json response', () => { + const mockErr = utils.errorFrom('not json', 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom('not json', 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseAppCheckError( - 'unknown-error', 'Unexpected response with status: 404 and body: not json'); + const expected = new FirebaseAppCheckError({ + code: 'unknown-error', + message: 'Unexpected response with status: 404 and body: not json', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.exchangeToken(TEST_TOKEN_TO_EXCHANGE, APP_ID) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should reject when rejected with a FirebaseAppError', () => { - const expected = new FirebaseAppError('network-error', 'socket hang up'); + const expected = new FirebaseAppError({ code: 'network-error', message: 'socket hang up' }); const stub = sinon .stub(HttpClient.prototype, 'send') .rejects(expected); @@ -190,8 +215,10 @@ describe('AppCheckApiClient', () => { .stub(HttpClient.prototype, 'send') .resolves(utils.responseFrom(response, 200)); stubs.push(stub); - const expected = new FirebaseAppCheckError( - 'invalid-argument', '`ttl` must be a valid duration string with the suffix `s`.'); + const expected = new FirebaseAppCheckError({ + code: 'invalid-argument', + message: '`ttl` must be a valid duration string with the suffix `s`.' + }); return apiClient.exchangeToken(TEST_TOKEN_TO_EXCHANGE, APP_ID) .should.eventually.be.rejected.and.deep.include(expected); }); @@ -264,38 +291,61 @@ describe('AppCheckApiClient', () => { }); it('should reject when a full platform error response is received', () => { + const mockErr = utils.errorFrom(ERROR_RESPONSE, 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom(ERROR_RESPONSE, 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseAppCheckError('not-found', 'Requested entity not found'); + const expected = new FirebaseAppCheckError({ + code: 'not-found', + message: 'Requested entity not found', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.verifyReplayProtection(TEST_TOKEN_TO_EXCHANGE) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should reject with unknown-error when error code is not present', () => { + const mockErr = utils.errorFrom({}, 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom({}, 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseAppCheckError('unknown-error', 'Unknown server error: {}'); + const expected = new FirebaseAppCheckError({ + code: 'unknown-error', + message: 'Unknown server error: {}', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.verifyReplayProtection(TEST_TOKEN_TO_EXCHANGE) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should reject with unknown-error for non-json response', () => { + const mockErr = utils.errorFrom('not json', 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom('not json', 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseAppCheckError( - 'unknown-error', 'Unexpected response with status: 404 and body: not json'); + const expected = new FirebaseAppCheckError({ + code: 'unknown-error', + message: 'Unexpected response with status: 404 and body: not json', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.verifyReplayProtection(TEST_TOKEN_TO_EXCHANGE) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should reject when rejected with a FirebaseAppError', () => { - const expected = new FirebaseAppError('network-error', 'socket hang up'); + const expected = new FirebaseAppError({ code: 'network-error', message: 'socket hang up' }); const stub = sinon .stub(HttpClient.prototype, 'send') .rejects(expected); @@ -312,10 +362,14 @@ describe('AppCheckApiClient', () => { .stub(HttpClient.prototype, 'send') .resolves(utils.responseFrom(response, 200)); stubs.push(stub); - const expected = new FirebaseAppCheckError( - 'invalid-argument', '`alreadyConsumed` must be a boolean value.'); + const expected = new FirebaseAppCheckError({ + code: 'invalid-argument', + message: '`alreadyConsumed` must be a boolean value.' + }); return apiClient.verifyReplayProtection(TEST_TOKEN_TO_EXCHANGE) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.nested.property('httpResponse.status', 200); }); }); diff --git a/test/unit/app-check/app-check.spec.ts b/test/unit/app-check/app-check.spec.ts index e849be9850..0c42fd61d2 100644 --- a/test/unit/app-check/app-check.spec.ts +++ b/test/unit/app-check/app-check.spec.ts @@ -24,7 +24,8 @@ import * as mocks from '../../resources/mocks'; import { FirebaseApp } from '../../../src/app/firebase-app'; import { AppCheck } from '../../../src/app-check/index'; -import { AppCheckApiClient, FirebaseAppCheckError } from '../../../src/app-check/app-check-api-client-internal'; +import { AppCheckApiClient } from '../../../src/app-check/app-check-api-client-internal'; +import { FirebaseAppCheckError } from '../../../src/app-check/error'; import { AppCheckTokenGenerator } from '../../../src/app-check/token-generator'; import { HttpClient } from '../../../src/utils/api-request'; import { ServiceAccountSigner } from '../../../src/utils/crypto-signer'; @@ -34,7 +35,7 @@ const expect = chai.expect; describe('AppCheck', () => { - const INTERNAL_ERROR = new FirebaseAppCheckError('internal-error', 'message'); + const INTERNAL_ERROR = new FirebaseAppCheckError({ code: 'internal-error', message: 'message' }); const APP_ID = '1:1234:android:1234'; const TEST_TOKEN_TO_EXCHANGE = 'signed-custom-token'; diff --git a/test/unit/app-check/token-generator.spec.ts b/test/unit/app-check/token-generator.spec.ts index a2e72e9f08..edfc57a3e0 100644 --- a/test/unit/app-check/token-generator.spec.ts +++ b/test/unit/app-check/token-generator.spec.ts @@ -33,7 +33,7 @@ import { CryptoSignerError, CryptoSignerErrorCode, ServiceAccountSigner } from '../../../src/utils/crypto-signer'; import { ServiceAccountCredential } from '../../../src/app/credential-internal'; -import { FirebaseAppCheckError } from '../../../src/app-check/app-check-api-client-internal'; +import { FirebaseAppCheckError } from '../../../src/app-check/error'; import * as utils from '../utils'; chai.should(); @@ -335,8 +335,7 @@ describe('AppCheckTokenGenerator', () => { expect(appCheckError).to.be.an.instanceof(FirebaseAppCheckError); expect(appCheckError).to.have.property('code', 'app-check/unknown-error'); expect(appCheckError).to.have.property('message', - 'Error returned from server while signing a custom token: '+ - '{"status":500,"headers":{},"data":{"error":{}},"text":"{\\"error\\":{}}"}'); + 'Error returned from server while signing a custom token: Unknown server error'); }); it('should convert CryptoSignerError HttpError with no errorcode to FirebaseAppCheckError', () => { @@ -349,7 +348,7 @@ describe('AppCheckTokenGenerator', () => { expect(appCheckError).to.be.an.instanceof(FirebaseAppCheckError); expect(appCheckError).to.have.property('code', 'app-check/internal-error'); expect(appCheckError).to.have.property('message', - 'Error returned from server: null.'); + 'Error returned from server.'); }); }); }); diff --git a/test/unit/app/credential-internal.spec.ts b/test/unit/app/credential-internal.spec.ts index d57cc5e91a..2d56a74dd0 100644 --- a/test/unit/app/credential-internal.spec.ts +++ b/test/unit/app/credential-internal.spec.ts @@ -37,6 +37,7 @@ import { getApplicationDefault, isApplicationDefault, ImpersonatedServiceAccountCredential, ApplicationDefaultCredential } from '../../../src/app/credential-internal'; import { deepCopy } from '../../../src/utils/deep-copy'; +import { FirebaseAppError } from '../../../src/app/error'; chai.should(); chai.use(sinonChai); @@ -84,19 +85,37 @@ describe('Credential', () => { }); it('should throw if called with the path to a non-existent file', () => { - expect(() => new ServiceAccountCredential('invalid-file')) - .to.throw('Failed to parse service account json file: Error: ENOENT: no such file or directory'); + try { + new ServiceAccountCredential('invalid-file'); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err).to.be.instanceOf(FirebaseAppError); + expect(err.message).to.match(/^Failed to parse service account json file: /); + expect(err.cause).to.have.property('code', 'ENOENT'); + } }); it('should throw if called with the path to an invalid file', () => { const invalidPath = path.resolve(__dirname, '../../resources/unparsable.key.json'); - expect(() => new ServiceAccountCredential(invalidPath)) - .to.throw('Failed to parse service account json file: SyntaxError'); + try { + new ServiceAccountCredential(invalidPath); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err).to.be.instanceOf(FirebaseAppError); + expect(err.message).to.match(/^Failed to parse service account json file: /); + expect(err.cause).to.be.instanceOf(SyntaxError); + } }); it('should throw if called with an empty string path', () => { - expect(() => new ServiceAccountCredential('')) - .to.throw('Failed to parse service account json file: Error: ENOENT: no such file or directory'); + try { + new ServiceAccountCredential(''); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err).to.be.instanceOf(FirebaseAppError); + expect(err.message).to.match(/^Failed to parse service account json file: /); + expect(err.cause).to.have.property('code', 'ENOENT'); + } }); it('should throw given an object without a "project_id" property', () => { diff --git a/test/unit/app/firebase-app.spec.ts b/test/unit/app/firebase-app.spec.ts index 56a7edae39..ce2b07cdbc 100644 --- a/test/unit/app/firebase-app.spec.ts +++ b/test/unit/app/firebase-app.spec.ts @@ -35,7 +35,7 @@ import { auth, messaging, machineLearning, storage, firestore, database, instanceId, installations, projectManagement, securityRules, remoteConfig, appCheck, } from '../../../src/firebase-namespace-api'; -import { FirebaseAppError, AppErrorCodes } from '../../../src/utils/error'; +import { FirebaseAppError, AppErrorCode } from '../../../src/app/error'; import Auth = auth.Auth; import Database = database.Database; @@ -173,30 +173,50 @@ describe('FirebaseApp', () => { it('should throw when the environment variable points to non existing file', () => { process.env[FIREBASE_CONFIG_VAR] = './test/resources/non_existant.json'; - expect(() => { + try { firebaseNamespace.initializeApp(); - }).to.throw('Failed to parse app options file: Error: ENOENT: no such file or directory'); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err).to.be.instanceOf(FirebaseAppError); + expect(err.message).to.match(/^Failed to parse app options file: /); + expect(err.cause).to.have.property('code', 'ENOENT'); + } }); it('should throw when the environment variable contains bad json', () => { process.env[FIREBASE_CONFIG_VAR] = '{,,'; - expect(() => { + try { firebaseNamespace.initializeApp(); - }).to.throw(/Failed to parse app options file: SyntaxError:/); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err).to.be.instanceOf(FirebaseAppError); + expect(err.message).to.match(/^Failed to parse app options file: /); + expect(err.cause).to.be.instanceOf(SyntaxError); + } }); it('should throw when the environment variable points to an empty file', () => { process.env[FIREBASE_CONFIG_VAR] = './test/resources/firebase_config_empty.json'; - expect(() => { + try { firebaseNamespace.initializeApp(); - }).to.throw('Failed to parse app options file'); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err).to.be.instanceOf(FirebaseAppError); + expect(err.message).to.match(/^Failed to parse app options file: /); + expect(err.cause).to.be.instanceOf(SyntaxError); + } }); it('should throw when the environment variable points to bad json', () => { process.env[FIREBASE_CONFIG_VAR] = './test/resources/firebase_config_bad.json'; - expect(() => { + try { firebaseNamespace.initializeApp(); - }).to.throw('Failed to parse app options file'); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err).to.be.instanceOf(FirebaseAppError); + expect(err.message).to.match(/^Failed to parse app options file: /); + expect(err.cause).to.be.instanceOf(SyntaxError); + } }); it('should ignore a bad config key in the config file', () => { @@ -827,20 +847,31 @@ describe('FirebaseApp', () => { }) }); - it('Includes the original error in exception', () => { + it('Includes the original error in exception', async () => { getTokenStub.restore(); - const mockError = new FirebaseAppError( - AppErrorCodes.INVALID_CREDENTIAL, 'Something went wrong'); + const mockError = new FirebaseAppError({ + code: AppErrorCode.INVALID_CREDENTIAL, + message: 'Something went wrong' + }); getTokenStub = sinon.stub(ServiceAccountCredential.prototype, 'getAccessToken').rejects(mockError); const detailedMessage = 'Credential implementation provided to initializeApp() via the "credential" property' + ' failed to fetch a valid Google OAuth2 access token with the following error: "Something went wrong".'; - expect(mockApp.INTERNAL.getToken(true)).to.be.rejectedWith(detailedMessage); + + try { + await mockApp.INTERNAL.getToken(true); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err.message).to.equal(detailedMessage); + expect(err.cause).to.equal(mockError); + } }); - it('Returns a detailed message when an error is due to an invalid_grant', () => { + it('Returns a detailed message when an error is due to an invalid_grant', async () => { getTokenStub.restore(); - const mockError = new FirebaseAppError( - AppErrorCodes.INVALID_CREDENTIAL, 'Failed to get credentials: invalid_grant (reason)'); + const mockError = new FirebaseAppError({ + code: AppErrorCode.INVALID_CREDENTIAL, + message: 'Failed to get credentials: invalid_grant (reason)' + }); getTokenStub = sinon.stub(ServiceAccountCredential.prototype, 'getAccessToken').rejects(mockError); const detailedMessage = 'Credential implementation provided to initializeApp() via the "credential" property' + ' failed to fetch a valid Google OAuth2 access token with the following error: "Failed to get credentials:' @@ -849,7 +880,14 @@ describe('FirebaseApp', () => { + ' make sure the key ID for your key file is still present at ' + 'https://console.firebase.google.com/iam-admin/serviceaccounts/project. If not, generate a new key file ' + 'at https://console.firebase.google.com/project/_/settings/serviceaccounts/adminsdk.'; - expect(mockApp.INTERNAL.getToken(true)).to.be.rejectedWith(detailedMessage); + + try { + await mockApp.INTERNAL.getToken(true); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err.message).to.equal(detailedMessage); + expect(err.cause).to.equal(mockError); + } }); }); diff --git a/test/unit/auth/action-code-settings-builder.spec.ts b/test/unit/auth/action-code-settings-builder.spec.ts index f8eed33d74..eb3fadf774 100644 --- a/test/unit/auth/action-code-settings-builder.spec.ts +++ b/test/unit/auth/action-code-settings-builder.spec.ts @@ -20,7 +20,7 @@ import * as sinonChai from 'sinon-chai'; import * as chaiAsPromised from 'chai-as-promised'; import { ActionCodeSettingsBuilder } from '../../../src/auth/action-code-settings-builder'; -import { AuthClientErrorCode } from '../../../src/utils/error'; +import { authClientErrorCode } from '../../../src/auth/error'; chai.should(); @@ -75,7 +75,7 @@ describe('ActionCodeSettingsBuilder', () => { dynamicLinkDomain: 'custom.page.link', linkDomain: TEST_LINK_DOMAIN, } as any); - }).to.throw(AuthClientErrorCode.MISSING_CONTINUE_URI.message); + }).to.throw(authClientErrorCode.MISSING_CONTINUE_URI.message); }); const invalidUrls = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; @@ -85,7 +85,7 @@ describe('ActionCodeSettingsBuilder', () => { return new ActionCodeSettingsBuilder({ url, } as any); - }).to.throw(AuthClientErrorCode.INVALID_CONTINUE_URI.message); + }).to.throw(authClientErrorCode.INVALID_CONTINUE_URI.message); }); }); @@ -110,7 +110,7 @@ describe('ActionCodeSettingsBuilder', () => { handleCodeInApp: true, dynamicLinkDomain: domain, } as any); - }).to.throw(AuthClientErrorCode.INVALID_DYNAMIC_LINK_DOMAIN.message); + }).to.throw(authClientErrorCode.INVALID_DYNAMIC_LINK_DOMAIN.message); }); }); @@ -124,7 +124,7 @@ describe('ActionCodeSettingsBuilder', () => { handleCodeInApp: true, linkDomain: domain, } as any); - }).to.throw(AuthClientErrorCode.INVALID_HOSTING_LINK_DOMAIN.message); + }).to.throw(authClientErrorCode.INVALID_HOSTING_LINK_DOMAIN.message); }); }); @@ -148,7 +148,7 @@ describe('ActionCodeSettingsBuilder', () => { handleCodeInApp: true, iOS: {}, } as any); - }).to.throw(AuthClientErrorCode.MISSING_IOS_BUNDLE_ID.message); + }).to.throw(authClientErrorCode.MISSING_IOS_BUNDLE_ID.message); }); const invalidBundleIds = [null, NaN, 0, 1, true, false, '', ['com.example.ios'], _.noop]; @@ -184,7 +184,7 @@ describe('ActionCodeSettingsBuilder', () => { handleCodeInApp: true, android: {}, } as any); - }).to.throw(AuthClientErrorCode.MISSING_ANDROID_PACKAGE_NAME.message); + }).to.throw(authClientErrorCode.MISSING_ANDROID_PACKAGE_NAME.message); }); const invalidPackageNames = [null, NaN, 0, 1, true, false, '', ['com.example.android'], _.noop]; diff --git a/test/unit/auth/auth-api-request.spec.ts b/test/unit/auth/auth-api-request.spec.ts index 5f8e45b458..36872c4498 100644 --- a/test/unit/auth/auth-api-request.spec.ts +++ b/test/unit/auth/auth-api-request.spec.ts @@ -38,7 +38,7 @@ import { EMAIL_ACTION_REQUEST_TYPES, TenantAwareAuthRequestHandler, AbstractAuthRequestHandler, } from '../../../src/auth/auth-api-request'; import { UserImportBuilder } from '../../../src/auth/user-import-builder'; -import { AuthClientErrorCode, FirebaseAuthError } from '../../../src/utils/error'; +import { authClientErrorCode, FirebaseAuthError } from '../../../src/auth/error'; import { ActionCodeSettingsBuilder } from '../../../src/auth/action-code-settings-builder'; import { SAMLConfigServerResponse } from '../../../src/auth/auth-config'; import { expectUserImportResult } from './user-import-builder.spec'; @@ -187,13 +187,13 @@ describe('FIREBASE_AUTH_CREATE_SESSION_COOKIE', () => { describe('responseValidator', () => { const responseValidator = FIREBASE_AUTH_CREATE_SESSION_COOKIE.getResponseValidator(); it('should succeed with sessionCookie returned', () => { - const validResponse = { sessionCookie: 'SESSION_COOKIE' }; + const validResponse = utils.responseFrom({ sessionCookie: 'SESSION_COOKIE' }); expect(() => { return responseValidator(validResponse); }).not.to.throw(); }); it('should fail when no session cookie is returned', () => { - const invalidResponse = {}; + const invalidResponse = utils.responseFrom({}); expect(() => { responseValidator(invalidResponse); }).to.throw(); @@ -220,7 +220,7 @@ describe('FIREBASE_AUTH_UPLOAD_ACCOUNT', () => { it('should return empty response validator', () => { expect(FIREBASE_AUTH_UPLOAD_ACCOUNT.getResponseValidator()).to.not.be.null; expect(() => { - const emptyResponse = {}; + const emptyResponse = utils.responseFrom({}); const responseValidator = FIREBASE_AUTH_UPLOAD_ACCOUNT.getResponseValidator(); responseValidator(emptyResponse); }).not.to.throw(); @@ -251,7 +251,7 @@ describe('FIREBASE_AUTH_DOWNLOAD_ACCOUNT', () => { it('should return empty response validator', () => { expect(FIREBASE_AUTH_DOWNLOAD_ACCOUNT.getResponseValidator()).to.not.be.null; expect(() => { - const emptyResponse = {}; + const emptyResponse = utils.responseFrom({}); const responseValidator = FIREBASE_AUTH_DOWNLOAD_ACCOUNT.getResponseValidator(); responseValidator(emptyResponse); }).not.to.throw(); @@ -369,19 +369,19 @@ describe('FIREBASE_AUTH_GET_ACCOUNT_INFO', () => { describe('responseValidator', () => { const responseValidator = FIREBASE_AUTH_GET_ACCOUNT_INFO.getResponseValidator(); it('should succeed with users returned', () => { - const validResponse: object = { users: [{ localId: 'foo' }] }; + const validResponse = utils.responseFrom({ users: [{ localId: 'foo' }] }); expect(() => { return responseValidator(validResponse); }).not.to.throw(); }); it('should fail when the response object is empty', () => { - const invalidResponse = {}; + const invalidResponse = utils.responseFrom({}); expect(() => { responseValidator(invalidResponse); }).to.throw(); }); it('should fail when the response object has an empty list of users', () => { - const invalidResponse = { users: [] }; + const invalidResponse = utils.responseFrom({ users: [] }); expect(() => { responseValidator(invalidResponse); }).to.throw(); @@ -445,13 +445,13 @@ describe('FIREBASE_AUTH_GET_ACCOUNTS_INFO', () => { describe('responseValidator', () => { const responseValidator = FIREBASE_AUTH_GET_ACCOUNTS_INFO.getResponseValidator(); it('should succeed with users returned', () => { - const validResponse: object = { users: [] }; + const validResponse = utils.responseFrom({ users: [] }); expect(() => { return responseValidator(validResponse); }).not.to.throw(); }); it('should succeed even if users are not returned', () => { - const invalidResponse = {}; + const invalidResponse = utils.responseFrom({}); expect(() => { responseValidator(invalidResponse); }).not.to.throw(); @@ -469,7 +469,7 @@ describe('FIREBASE_AUTH_DELETE_ACCOUNT', () => { it('should return empty response validator', () => { expect(FIREBASE_AUTH_DELETE_ACCOUNT.getResponseValidator()).to.not.be.null; expect(() => { - const emptyResponse = {}; + const emptyResponse = utils.responseFrom({}); const responseValidator = FIREBASE_AUTH_DELETE_ACCOUNT.getResponseValidator(); responseValidator(emptyResponse); }).not.to.throw(); @@ -660,13 +660,13 @@ describe('FIREBASE_AUTH_SET_ACCOUNT_INFO', () => { describe('responseValidator', () => { const responseValidator = FIREBASE_AUTH_SET_ACCOUNT_INFO.getResponseValidator(); it('should succeed with localId returned', () => { - const validResponse = { localId: '1234' }; + const validResponse = utils.responseFrom({ localId: '1234' }); expect(() => { return responseValidator(validResponse); }).not.to.throw(); }); it('should fail when localId is not returned', () => { - const invalidResponse = {}; + const invalidResponse = utils.responseFrom({}); expect(() => { return responseValidator(invalidResponse); }).to.throw(); @@ -816,13 +816,13 @@ describe('FIREBASE_AUTH_SIGN_UP_NEW_USER', () => { describe('responseValidator', () => { const responseValidator = FIREBASE_AUTH_SIGN_UP_NEW_USER.getResponseValidator(); it('should succeed with localId returned', () => { - const validResponse = { localId: '1234' }; + const validResponse = utils.responseFrom({ localId: '1234' }); expect(() => { return responseValidator(validResponse); }).not.to.throw(); }); it('should fail when localId is not returned', () => { - const invalidResponse = {}; + const invalidResponse = utils.responseFrom({}); expect(() => { responseValidator(invalidResponse); }).to.throw(); @@ -1015,7 +1015,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); it('should be rejected given an invalid ID token', () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_ID_TOKEN, + authClientErrorCode.INVALID_ID_TOKEN, ); const requestHandler = handler.init(mockApp); @@ -1028,7 +1028,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); it('should be rejected given an invalid duration', () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_SESSION_COOKIE_DURATION, + authClientErrorCode.INVALID_SESSION_COOKIE_DURATION, ); const requestHandler = handler.init(mockApp); @@ -1041,7 +1041,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); it('should be rejected given a duration less than minimum allowed', () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_SESSION_COOKIE_DURATION, + authClientErrorCode.INVALID_SESSION_COOKIE_DURATION, ); const outOfBoundDuration = 60 * 1000 * 5 - 1; @@ -1055,7 +1055,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); it('should be rejected given a duration greater than maximum allowed', () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_SESSION_COOKIE_DURATION, + authClientErrorCode.INVALID_SESSION_COOKIE_DURATION, ); // Add more than a second since this value is Math.floor()'ed const outOfBoundDuration = 60 * 60 * 1000 * 24 * 14 + 1001; @@ -1074,7 +1074,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { message: 'INVALID_ID_TOKEN', }, }); - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_ID_TOKEN); + const expectedError = new FirebaseAuthError(authClientErrorCode.INVALID_ID_TOKEN); const data = { idToken: 'invalid-token', validDuration: durationInMs / 1000 }; const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedResult); stubs.push(stub); @@ -1120,7 +1120,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const expectedResult = utils.responseFrom({ kind: 'identitytoolkit#GetAccountInfoResponse', }); - const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + const expectedError = new FirebaseAuthError(authClientErrorCode.USER_NOT_FOUND); const data = { email: ['user@example.com'] }; const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); stubs.push(stub); @@ -1160,7 +1160,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const expectedResult = utils.responseFrom({ kind: 'identitytoolkit#GetAccountInfoResponse', }); - const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + const expectedError = new FirebaseAuthError(authClientErrorCode.USER_NOT_FOUND); const data = { localId: ['uid'] }; const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); stubs.push(stub); @@ -1232,7 +1232,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); it('should be rejected given an invalid phoneNumber', () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_PHONE_NUMBER); + authClientErrorCode.INVALID_PHONE_NUMBER); const stub = sinon.stub(HttpClient.prototype, 'send'); stubs.push(stub); @@ -1250,7 +1250,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const expectedResult = utils.responseFrom({ kind: 'identitytoolkit#GetAccountInfoResponse', }); - const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + const expectedError = new FirebaseAuthError(authClientErrorCode.USER_NOT_FOUND); const data = { phoneNumber: ['+11234567890'], }; @@ -1443,7 +1443,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should throw on invalid options without making an underlying API call', () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_HASH_ALGORITHM, + authClientErrorCode.INVALID_HASH_ALGORITHM, 'Unsupported hash algorithm provider "invalid".', ); const invalidOptions = { @@ -1463,7 +1463,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should throw when 1001 UserImportRecords are provided', () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.MAXIMUM_USER_COUNT_EXCEEDED, + authClientErrorCode.MAXIMUM_USER_COUNT_EXCEEDED, 'A maximum of 1000 users can be imported at once.', ); const stub = sinon.stub(HttpClient.prototype, 'send'); @@ -1490,7 +1490,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const mismatchIndex = 34; const mismatchTenantId = 'MISMATCHING-TENANT-ID'; const expectedError = new FirebaseAuthError( - AuthClientErrorCode.MISMATCHING_TENANT_ID, + authClientErrorCode.MISMATCHING_TENANT_ID, `UserRecord of index "${mismatchIndex}" has mismatching tenant ID "${mismatchTenantId}"`, ); const stub = sinon.stub(HttpClient.prototype, 'send'); @@ -1585,8 +1585,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { successCount: 0, failureCount: 2, errors: [ - { index: 0, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER) }, - { index: 1, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL) }, + { index: 0, error: new FirebaseAuthError(authClientErrorCode.INVALID_PHONE_NUMBER) }, + { index: 1, error: new FirebaseAuthError(authClientErrorCode.INVALID_EMAIL) }, ], }; const stub = sinon.stub(HttpClient.prototype, 'send'); @@ -1713,79 +1713,79 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { successCount: 0, failureCount: testUsers.length, errors: [ - { index: 0, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_DISPLAY_NAME) }, - { index: 1, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_UID) }, - { index: 2, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL) }, - { index: 3, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER) }, - { index: 4, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL_VERIFIED) }, - { index: 5, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PHOTO_URL) }, - { index: 6, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_DISABLED_FIELD) }, - { index: 7, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_CREATION_TIME) }, - { index: 8, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_LAST_SIGN_IN_TIME) }, + { index: 0, error: new FirebaseAuthError(authClientErrorCode.INVALID_DISPLAY_NAME) }, + { index: 1, error: new FirebaseAuthError(authClientErrorCode.INVALID_UID) }, + { index: 2, error: new FirebaseAuthError(authClientErrorCode.INVALID_EMAIL) }, + { index: 3, error: new FirebaseAuthError(authClientErrorCode.INVALID_PHONE_NUMBER) }, + { index: 4, error: new FirebaseAuthError(authClientErrorCode.INVALID_EMAIL_VERIFIED) }, + { index: 5, error: new FirebaseAuthError(authClientErrorCode.INVALID_PHOTO_URL) }, + { index: 6, error: new FirebaseAuthError(authClientErrorCode.INVALID_DISABLED_FIELD) }, + { index: 7, error: new FirebaseAuthError(authClientErrorCode.INVALID_CREATION_TIME) }, + { index: 8, error: new FirebaseAuthError(authClientErrorCode.INVALID_LAST_SIGN_IN_TIME) }, { index: 9, error: new FirebaseAuthError( - AuthClientErrorCode.FORBIDDEN_CLAIM, + authClientErrorCode.FORBIDDEN_CLAIM, 'Developer claim "aud" is reserved and cannot be specified.', ), }, - { index: 10, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD_HASH) }, - { index: 11, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD_SALT) }, + { index: 10, error: new FirebaseAuthError(authClientErrorCode.INVALID_PASSWORD_HASH) }, + { index: 11, error: new FirebaseAuthError(authClientErrorCode.INVALID_PASSWORD_SALT) }, { index: 12, error: new FirebaseAuthError( - AuthClientErrorCode.INVALID_UID, + authClientErrorCode.INVALID_UID, 'The provider "uid" for "google.com" must be a valid non-empty string.', ), }, { index: 13, error: new FirebaseAuthError( - AuthClientErrorCode.INVALID_DISPLAY_NAME, + authClientErrorCode.INVALID_DISPLAY_NAME, 'The provider "displayName" for "google.com" must be a valid string.', ), }, { index: 14, error: new FirebaseAuthError( - AuthClientErrorCode.INVALID_EMAIL, + authClientErrorCode.INVALID_EMAIL, 'The provider "email" for "google.com" must be a valid email string.', ), }, { index: 15, error: new FirebaseAuthError( - AuthClientErrorCode.INVALID_PHOTO_URL, + authClientErrorCode.INVALID_PHOTO_URL, 'The provider "photoURL" for "google.com" must be a valid URL string.', ), }, - { index: 16, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID) }, - { index: 17, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_UID) }, + { index: 16, error: new FirebaseAuthError(authClientErrorCode.INVALID_PROVIDER_ID) }, + { index: 17, error: new FirebaseAuthError(authClientErrorCode.INVALID_UID) }, { index: 18, error: new FirebaseAuthError( - AuthClientErrorCode.INVALID_UID, + authClientErrorCode.INVALID_UID, 'The second factor "uid" must be a valid non-empty string.', ), }, { index: 19, error: new FirebaseAuthError( - AuthClientErrorCode.INVALID_DISPLAY_NAME, + authClientErrorCode.INVALID_DISPLAY_NAME, 'The second factor "displayName" for "mfaUid1" must be a valid string.', ), }, { index: 20, error: new FirebaseAuthError( - AuthClientErrorCode.INVALID_ENROLLMENT_TIME, + authClientErrorCode.INVALID_ENROLLMENT_TIME, 'The second factor "enrollmentTime" for "mfaUid2" must be a valid UTC date string.', ), }, { index: 21, error: new FirebaseAuthError( - AuthClientErrorCode.INVALID_PHONE_NUMBER, + authClientErrorCode.INVALID_PHONE_NUMBER, 'The second factor "phoneNumber" for "mfaUid3" must be a non-empty ' + 'E.164 standard compliant identifier string.', ), @@ -1793,7 +1793,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { { index: 22, error: new FirebaseAuthError( - AuthClientErrorCode.UNSUPPORTED_SECOND_FACTOR, + authClientErrorCode.UNSUPPORTED_SECOND_FACTOR, `Unsupported second factor "${JSON.stringify(testUsers[22].multiFactor.enrolledFactors[0])}" provided.`, ), }, @@ -1817,7 +1817,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }, }); const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'An internal error has occurred. Raw server response: ' + `"${JSON.stringify(expectedServerError.response.data)}"`, ); @@ -1897,7 +1897,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); it('should be rejected given an invalid maxResults', () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, 'Required "maxResults" must be a positive integer that does not ' + 'exceed 1000.', ); @@ -1912,7 +1912,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); it('should be rejected given an invalid next page token', () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_PAGE_TOKEN, + authClientErrorCode.INVALID_PAGE_TOKEN, ); const requestHandler = handler.init(mockApp); @@ -2272,7 +2272,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given invalid parameters such as email', () => { // Expected error when an invalid email is provided. - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL); + const expectedError = new FirebaseAuthError(authClientErrorCode.INVALID_EMAIL); const requestHandler = handler.init(mockApp); // Send update request with invalid email. return requestHandler.updateExistingAccount(uid, invalidData) @@ -2294,7 +2294,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { { name: 'invalid second factor uid', error: new FirebaseAuthError( - AuthClientErrorCode.INVALID_UID, + authClientErrorCode.INVALID_UID, 'The second factor "uid" must be a valid non-empty string.', ), secondFactor: { @@ -2307,7 +2307,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { { name: 'invalid second factor display name', error: new FirebaseAuthError( - AuthClientErrorCode.INVALID_DISPLAY_NAME, + authClientErrorCode.INVALID_DISPLAY_NAME, 'The second factor "displayName" for "enrolledSecondFactor1" must be a valid string.', ), secondFactor: { @@ -2320,7 +2320,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { { name: 'invalid second factor phone number', error: new FirebaseAuthError( - AuthClientErrorCode.INVALID_PHONE_NUMBER, + authClientErrorCode.INVALID_PHONE_NUMBER, 'The second factor "phoneNumber" for "enrolledSecondFactor1" must be a non-empty ' + 'E.164 standard compliant identifier string.'), secondFactor: { @@ -2333,7 +2333,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { { name: 'invalid second factor enrollment time', error: new FirebaseAuthError( - AuthClientErrorCode.INVALID_ENROLLMENT_TIME, + authClientErrorCode.INVALID_ENROLLMENT_TIME, 'The second factor "enrollmentTime" for "enrolledSecondFactor1" must be a valid ' + 'UTC date string.'), secondFactor: { @@ -2347,7 +2347,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { { name: 'invalid second factor type', error: new FirebaseAuthError( - AuthClientErrorCode.UNSUPPORTED_SECOND_FACTOR, + authClientErrorCode.UNSUPPORTED_SECOND_FACTOR, `Unsupported second factor "${JSON.stringify(unsupportedSecondFactor)}" provided.`), secondFactor: unsupportedSecondFactor, }, @@ -2375,7 +2375,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { (dataWithModifiedTenantId as any).tenantId = 'MODIFIED-TENANT-ID'; // Expected error when a tenant ID is provided. const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '"tenantId" is an invalid "UpdateRequest" property.', ); const requestHandler = handler.init(mockApp); @@ -2391,7 +2391,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given invalid parameters such as phoneNumber', () => { // Expected error when an invalid phone number is provided. - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER); + const expectedError = new FirebaseAuthError(authClientErrorCode.INVALID_PHONE_NUMBER); const requestHandler = handler.init(mockApp); // Send update request with invalid phone number. return requestHandler.updateExistingAccount(uid, invalidPhoneNumberData) @@ -2480,7 +2480,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given invalid parameters such as uid', () => { // Expected error when an invalid uid is provided. - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_UID); + const expectedError = new FirebaseAuthError(authClientErrorCode.INVALID_UID); const requestHandler = handler.init(mockApp); // Send request with invalid uid. return requestHandler.setCustomUserClaims('', claims) @@ -2495,7 +2495,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given invalid parameters such as customClaims', () => { // Expected error when invalid claims are provided. const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, 'CustomUserClaims argument must be an object or null.', ); const requestHandler = handler.init(mockApp); @@ -2512,7 +2512,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given customClaims with blacklisted claims', () => { // Expected error when invalid claims are provided. const expectedError = new FirebaseAuthError( - AuthClientErrorCode.FORBIDDEN_CLAIM, + authClientErrorCode.FORBIDDEN_CLAIM, 'Developer claim "aud" is reserved and cannot be specified.', ); const requestHandler = handler.init(mockApp); @@ -2588,7 +2588,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); it('should be rejected given an invalid uid', () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_UID); + const expectedError = new FirebaseAuthError(authClientErrorCode.INVALID_UID); const invalidUid: any = { localId: uid }; const requestHandler = handler.init(mockApp); @@ -2730,7 +2730,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given invalid parameters such as email', () => { // Expected error when an invalid email is provided. - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL); + const expectedError = new FirebaseAuthError(authClientErrorCode.INVALID_EMAIL); const requestHandler = handler.init(mockApp); // Create new account with invalid email. return requestHandler.createNewAccount(invalidData) @@ -2778,7 +2778,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { { name: 'unsupported second factor uid', error: new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '"uid" is not supported when adding second factors via "createUser()"', ), secondFactor: { @@ -2791,7 +2791,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { { name: 'invalid second factor display name', error: new FirebaseAuthError( - AuthClientErrorCode.INVALID_DISPLAY_NAME, + authClientErrorCode.INVALID_DISPLAY_NAME, 'The second factor "displayName" for "+16505557348" must be a valid string.', ), secondFactor: { @@ -2803,7 +2803,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { { name: 'invalid second factor phone number', error: new FirebaseAuthError( - AuthClientErrorCode.INVALID_PHONE_NUMBER, + authClientErrorCode.INVALID_PHONE_NUMBER, 'The second factor "phoneNumber" for "invalid" must be a non-empty ' + 'E.164 standard compliant identifier string.'), secondFactor: { @@ -2815,7 +2815,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { { name: 'unsupported second factor enrollment time', error: new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '"enrollmentTime" is not supported when adding second factors via "createUser()"'), secondFactor: { phoneNumber: '+16505557348', @@ -2827,7 +2827,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { { name: 'invalid second factor type', error: new FirebaseAuthError( - AuthClientErrorCode.UNSUPPORTED_SECOND_FACTOR, + authClientErrorCode.UNSUPPORTED_SECOND_FACTOR, `Unsupported second factor "${JSON.stringify(unsupportedSecondFactor)}" provided.`), secondFactor: unsupportedSecondFactor, }, @@ -2857,7 +2857,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given tenantId in CreateRequest', () => { // Expected error when a tenantId is provided. const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '"tenantId" is an invalid "CreateRequest" property.'); const validDataWithTenantId = deepCopy(validData); (validDataWithTenantId as any).tenantId = TENANT_ID; @@ -2875,7 +2875,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given invalid parameters such as phoneNumber', () => { // Expected error when an invalid phone number is provided. - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER); + const expectedError = new FirebaseAuthError(authClientErrorCode.INVALID_PHONE_NUMBER); const requestHandler = handler.init(mockApp); // Create new account with invalid phone number. return requestHandler.createNewAccount(invalidPhoneNumberData) @@ -2889,7 +2889,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected when the backend returns a user exists error', () => { // Expected error when the uid already exists. - const expectedError = new FirebaseAuthError(AuthClientErrorCode.UID_ALREADY_EXISTS); + const expectedError = new FirebaseAuthError(authClientErrorCode.UID_ALREADY_EXISTS); const expectedResult = utils.errorFrom({ error: { message: 'DUPLICATE_LOCAL_ID', @@ -2913,7 +2913,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected when the backend returns an email exists error', () => { // Expected error when the email already exists. - const expectedError = new FirebaseAuthError(AuthClientErrorCode.EMAIL_ALREADY_EXISTS); + const expectedError = new FirebaseAuthError(authClientErrorCode.EMAIL_ALREADY_EXISTS); const expectedResult = utils.errorFrom({ error: { message: 'EMAIL_EXISTS', @@ -3014,7 +3014,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given invalid parameters such as email', () => { // Expected error when an invalid email is provided. const expectedError = - new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL); + new FirebaseAuthError(authClientErrorCode.INVALID_EMAIL); const requestHandler = handler.init(mockApp); // Send create new account request with invalid data. return requestHandler.createNewAccount(invalidData) @@ -3028,7 +3028,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given invalid parameters such as phone number', () => { // Expected error when an invalid phone number is provided. const expectedError = - new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER); + new FirebaseAuthError(authClientErrorCode.INVALID_PHONE_NUMBER); const requestHandler = handler.init(mockApp); // Send create new account request with invalid data. return requestHandler.createNewAccount(invalidPhoneNumberData) @@ -3182,7 +3182,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given requestType: VERIFY_AND_CHANGE and no new Email address', () => { const requestHandler = handler.init(mockApp); const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '`newEmail` is required when `requestType` === \'VERIFY_AND_CHANGE_EMAIL\'', ) @@ -3197,7 +3197,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given an invalid email', () => { const invalidEmail = 'invalid'; - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL); + const expectedError = new FirebaseAuthError(authClientErrorCode.INVALID_EMAIL); const requestHandler = handler.init(mockApp); return requestHandler.getEmailActionLink('PASSWORD_RESET', invalidEmail, actionCodeSettings) @@ -3211,7 +3211,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given an invalid new email', () => { const invalidNewEmail = 'invalid'; - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_NEW_EMAIL); + const expectedError = new FirebaseAuthError(authClientErrorCode.INVALID_NEW_EMAIL); const requestHandler = handler.init(mockApp); return requestHandler.getEmailActionLink('VERIFY_AND_CHANGE_EMAIL', email, actionCodeSettings, invalidNewEmail) @@ -3226,7 +3226,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given an invalid request type', () => { const invalidRequestType = 'invalid'; const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '"invalid" is not a supported email action request type.', ); @@ -3243,7 +3243,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given an invalid ActionCodeSettings object', () => { const invalidActionCodeSettings = 'invalid' as any; const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '"ActionCodeSettings" must be a non-null object.', ); @@ -3259,7 +3259,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected when the response does not contain a link', () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Unable to create the email action link'); const requestData = deepExtend({ requestType: 'VERIFY_EMAIL', @@ -3333,7 +3333,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { null, NaN, 0, 1, true, false, '', 'saml.provider', ['oidc.provider'], [], {}, { a: 1 }, _.noop]; invalidProviderIds.forEach((invalidProviderId) => { it('should be rejected given an invalid provider ID:' + JSON.stringify(invalidProviderId), () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID); + const expectedError = new FirebaseAuthError(authClientErrorCode.INVALID_PROVIDER_ID); const requestHandler = handler.init(mockApp); return requestHandler.getOAuthIdpConfig(invalidProviderId as any) @@ -3346,7 +3346,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); it('should be rejected given a backend error', () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.CONFIGURATION_NOT_FOUND); + const expectedError = new FirebaseAuthError(authClientErrorCode.CONFIGURATION_NOT_FOUND); const expectedServerError = utils.errorFrom({ error: { message: 'CONFIGURATION_NOT_FOUND', @@ -3433,7 +3433,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given an invalid maxResults', () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, 'Required "maxResults" must be a positive integer that does not ' + 'exceed 100.', ); @@ -3449,7 +3449,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given an invalid next page token', () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_PAGE_TOKEN, + authClientErrorCode.INVALID_PAGE_TOKEN, ); const requestHandler = handler.init(mockApp); @@ -3510,7 +3510,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { null, NaN, 0, 1, true, false, '', 'saml.provider', ['oidc.provider'], [], {}, { a: 1 }, _.noop]; invalidProviderIds.forEach((invalidProviderId) => { it('should be rejected given an invalid provider ID:' + JSON.stringify(invalidProviderId), () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID); + const expectedError = new FirebaseAuthError(authClientErrorCode.INVALID_PROVIDER_ID); const requestHandler = handler.init(mockApp); return requestHandler.deleteOAuthIdpConfig(invalidProviderId as any) @@ -3523,7 +3523,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); it('should be rejected given a backend error', () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.CONFIGURATION_NOT_FOUND); + const expectedError = new FirebaseAuthError(authClientErrorCode.CONFIGURATION_NOT_FOUND); const expectedServerError = utils.errorFrom({ error: { message: 'CONFIGURATION_NOT_FOUND', @@ -3615,7 +3615,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given invalid parameters', () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"OIDCAuthProviderConfig.issuer" must be a valid URL string.', ); const invalidOptions: OIDCAuthProviderConfig = deepCopy(configOptions); @@ -3632,7 +3632,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected when the backend returns a response missing name', () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Unable to create new OIDC configuration', ); const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({})); @@ -3655,7 +3655,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { message: 'INVALID_CONFIG', }, }); - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CONFIG); + const expectedError = new FirebaseAuthError(authClientErrorCode.INVALID_CONFIG); const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); stubs.push(stub); @@ -3800,7 +3800,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { null, NaN, 0, 1, true, false, '', 'saml.provider', ['oidc.provider'], [], {}, { a: 1 }, _.noop]; invalidProviderIds.forEach((invalidProviderId) => { it('should be rejected given an invalid provider ID:' + JSON.stringify(invalidProviderId), () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID); + const expectedError = new FirebaseAuthError(authClientErrorCode.INVALID_PROVIDER_ID); const requestHandler = handler.init(mockApp); return requestHandler.updateOAuthIdpConfig(invalidProviderId as any, configOptions) @@ -3814,7 +3814,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given invalid parameters', () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"OIDCAuthProviderConfig.issuer" must be a valid URL string.', ); const invalidOptions: OIDCUpdateAuthProviderRequest = deepCopy(configOptions); @@ -3832,7 +3832,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected when the backend returns a response missing name', () => { const expectedPath = path + '?updateMask=enabled,displayName,issuer,clientId'; const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Unable to update OIDC configuration', ); const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({})); @@ -3856,7 +3856,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { message: 'INVALID_CONFIG', }, }); - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CONFIG); + const expectedError = new FirebaseAuthError(authClientErrorCode.INVALID_CONFIG); const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); stubs.push(stub); @@ -3897,7 +3897,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { null, NaN, 0, 1, true, false, '', 'oidc.provider', ['saml.provider'], [], {}, { a: 1 }, _.noop]; invalidProviderIds.forEach((invalidProviderId) => { it('should be rejected given an invalid provider ID:' + JSON.stringify(invalidProviderId), () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID); + const expectedError = new FirebaseAuthError(authClientErrorCode.INVALID_PROVIDER_ID); const requestHandler = handler.init(mockApp); return requestHandler.getInboundSamlConfig(invalidProviderId as any) @@ -3910,7 +3910,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); it('should be rejected given a backend error', () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.CONFIGURATION_NOT_FOUND); + const expectedError = new FirebaseAuthError(authClientErrorCode.CONFIGURATION_NOT_FOUND); const expectedServerError = utils.errorFrom({ error: { message: 'CONFIGURATION_NOT_FOUND', @@ -3993,7 +3993,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given an invalid maxResults', () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, 'Required "maxResults" must be a positive integer that does not ' + 'exceed 100.', ); @@ -4009,7 +4009,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given an invalid next page token', () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_PAGE_TOKEN, + authClientErrorCode.INVALID_PAGE_TOKEN, ); const requestHandler = handler.init(mockApp); @@ -4068,7 +4068,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { null, NaN, 0, 1, true, false, '', 'oidc.provider', ['saml.provider'], [], {}, { a: 1 }, _.noop]; invalidProviderIds.forEach((invalidProviderId) => { it('should be rejected given an invalid provider ID:' + JSON.stringify(invalidProviderId), () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID); + const expectedError = new FirebaseAuthError(authClientErrorCode.INVALID_PROVIDER_ID); const requestHandler = handler.init(mockApp); return requestHandler.deleteInboundSamlConfig(invalidProviderId as any) @@ -4081,7 +4081,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); it('should be rejected given a backend error', () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.CONFIGURATION_NOT_FOUND); + const expectedError = new FirebaseAuthError(authClientErrorCode.CONFIGURATION_NOT_FOUND); const expectedServerError = utils.errorFrom({ error: { message: 'CONFIGURATION_NOT_FOUND', @@ -4152,7 +4152,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given invalid parameters', () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"SAMLAuthProviderConfig.callbackURL" must be a valid URL string.', ); const invalidOptions: SAMLAuthProviderConfig = deepCopy(configOptions); @@ -4169,7 +4169,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected when the backend returns a response missing name', () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Unable to create new SAML configuration', ); const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({})); @@ -4192,7 +4192,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { message: 'INVALID_CONFIG', }, }); - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CONFIG); + const expectedError = new FirebaseAuthError(authClientErrorCode.INVALID_CONFIG); const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); stubs.push(stub); @@ -4344,7 +4344,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { null, NaN, 0, 1, true, false, '', 'oidc.provider', ['saml.provider'], [], {}, { a: 1 }, _.noop]; invalidProviderIds.forEach((invalidProviderId) => { it('should be rejected given an invalid provider ID:' + JSON.stringify(invalidProviderId), () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID); + const expectedError = new FirebaseAuthError(authClientErrorCode.INVALID_PROVIDER_ID); const requestHandler = handler.init(mockApp); return requestHandler.updateInboundSamlConfig(invalidProviderId as any, configOptions) @@ -4358,7 +4358,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given invalid parameters', () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_CONFIG, + authClientErrorCode.INVALID_CONFIG, '"SAMLAuthProviderConfig.ssoURL" must be a valid URL string.', ); const invalidOptions: SAMLUpdateAuthProviderRequest = deepCopy(configOptions); @@ -4376,7 +4376,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected when the backend returns a response missing name', () => { const expectedPath = path + `?updateMask=${fullUpadateMask}`; const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Unable to update SAML configuration', ); const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({})); @@ -4400,7 +4400,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { message: 'INVALID_CONFIG', }, }); - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CONFIG); + const expectedError = new FirebaseAuthError(authClientErrorCode.INVALID_CONFIG); const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); stubs.push(stub); @@ -4440,7 +4440,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const invalidTenantIds = [null, NaN, 0, 1, true, false, '', ['tenant-id'], [], {}, { a: 1 }, _.noop]; invalidTenantIds.forEach((invalidTenantId) => { it('should be rejected given an invalid tenant ID:' + JSON.stringify(invalidTenantId), () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID); + const expectedError = new FirebaseAuthError(authClientErrorCode.INVALID_TENANT_ID); const requestHandler = handler.init(mockApp) as AuthRequestHandler; return requestHandler.getTenant(invalidTenantId as any) @@ -4453,7 +4453,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); it('should be rejected given a backend error', () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.TENANT_NOT_FOUND); + const expectedError = new FirebaseAuthError(authClientErrorCode.TENANT_NOT_FOUND); const expectedServerError = utils.errorFrom({ error: { message: 'TENANT_NOT_FOUND', @@ -4536,7 +4536,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given an invalid maxResults', () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, 'Required "maxResults" must be a positive non-zero number that does not ' + 'exceed the allowed 1000.', ); @@ -4552,7 +4552,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given an invalid next page token', () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_PAGE_TOKEN, + authClientErrorCode.INVALID_PAGE_TOKEN, ); const requestHandler = handler.init(mockApp) as AuthRequestHandler; @@ -4610,7 +4610,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const invalidTenantIds = [null, NaN, 0, 1, true, false, '', ['tenant-id'], [], {}, { a: 1 }, _.noop]; invalidTenantIds.forEach((invalidTenantId) => { it('should be rejected given an invalid tenant ID:' + JSON.stringify(invalidTenantId), () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID); + const expectedError = new FirebaseAuthError(authClientErrorCode.INVALID_TENANT_ID); const requestHandler = handler.init(mockApp) as AuthRequestHandler; return requestHandler.deleteTenant(invalidTenantId as any) @@ -4623,7 +4623,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); it('should be rejected given a backend error', () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.TENANT_NOT_FOUND); + const expectedError = new FirebaseAuthError(authClientErrorCode.TENANT_NOT_FOUND); const expectedServerError = utils.errorFrom({ error: { message: 'TENANT_NOT_FOUND', @@ -4692,7 +4692,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given invalid parameters', () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '"EmailSignInConfig" must be a non-null object.', ); const invalidOptions = deepCopy(tenantOptions); @@ -4709,7 +4709,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected when the backend returns a response missing name', () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Unable to create new tenant', ); const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({})); @@ -4727,7 +4727,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected when the backend returns a response missing tenant ID in response name', () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Unable to create new tenant', ); // Resource name should have /tenants/tenant-id in path. This should throw an error. @@ -4752,7 +4752,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }, }); const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'An internal error has occurred. Raw server response: ' + `"${JSON.stringify(expectedServerError.response.data)}"`, ); @@ -4864,7 +4864,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const invalidTenantIds = [null, NaN, 0, 1, true, false, '', ['tenant-id'], [], {}, { a: 1 }, _.noop]; invalidTenantIds.forEach((invalidTenantId) => { it('should be rejected given an invalid tenant ID:' + JSON.stringify(invalidTenantId), () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID); + const expectedError = new FirebaseAuthError(authClientErrorCode.INVALID_TENANT_ID); const requestHandler = handler.init(mockApp) as AuthRequestHandler; return requestHandler.updateTenant(invalidTenantId as any, tenantOptions) @@ -4878,7 +4878,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given invalid parameters', () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '"EmailSignInConfig" must be a non-null object.', ); const invalidOptions = deepCopy(tenantOptions); @@ -4897,7 +4897,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const expectedPath = path + '?updateMask=allowPasswordSignup,enableEmailLinkSignin,displayName,' + 'mfaConfig.state,mfaConfig.enabledProviders,testPhoneNumbers'; const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Unable to update tenant', ); const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({})); @@ -4918,7 +4918,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const expectedPath = path + '?updateMask=allowPasswordSignup,enableEmailLinkSignin,displayName,' + 'mfaConfig.state,mfaConfig.enabledProviders,testPhoneNumbers'; const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Unable to update tenant', ); // Resource name should have /tenants/tenant-id in path. This should throw an error. @@ -4946,7 +4946,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }, }); const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'An internal error has occurred. Raw server response: ' + `"${JSON.stringify(expectedServerError.response.data)}"`, ); diff --git a/test/unit/auth/auth.spec.ts b/test/unit/auth/auth.spec.ts index b9675e720b..f1c8202db9 100644 --- a/test/unit/auth/auth.spec.ts +++ b/test/unit/auth/auth.spec.ts @@ -31,7 +31,7 @@ import { FirebaseApp } from '../../../src/app/firebase-app'; import { AuthRequestHandler, TenantAwareAuthRequestHandler, AbstractAuthRequestHandler, } from '../../../src/auth/auth-api-request'; -import { AuthClientErrorCode, FirebaseAuthError } from '../../../src/utils/error'; +import { authClientErrorCode, FirebaseAuthError } from '../../../src/auth/error'; import * as validator from '../../../src/utils/validator'; import { DecodedAuthBlockingToken, FirebaseTokenVerifier } from '../../../src/auth/token-verifier'; @@ -497,7 +497,7 @@ AUTH_CONFIGS.forEach((testConfig) => { it('should reject when underlying idTokenVerifier.verifyJWT() rejects with expected error', () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, 'Decoding Firebase ID token failed'); + authClientErrorCode.INVALID_ARGUMENT, 'Decoding Firebase ID token failed'); // Restore verifyIdToken stub. stub.restore(); // Simulate ID token is invalid. @@ -650,7 +650,7 @@ AUTH_CONFIGS.forEach((testConfig) => { }); it('should be rejected with checkRevoked set to true if underlying RPC fails', () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + const expectedError = new FirebaseAuthError(authClientErrorCode.USER_NOT_FOUND); const getUserStub = sinon.stub(testConfig.Auth.prototype, 'getUser') .rejects(expectedError); stubs.push(getUserStub); @@ -689,7 +689,7 @@ AUTH_CONFIGS.forEach((testConfig) => { }); it('should be rejected with checkRevoked set to true using an invalid ID token', () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CREDENTIAL); + const expectedError = new FirebaseAuthError(authClientErrorCode.INVALID_CREDENTIAL); // Restore verifyIdToken stub. stub.restore(); // Simulate ID token is invalid. @@ -709,7 +709,7 @@ AUTH_CONFIGS.forEach((testConfig) => { if (testConfig.Auth === TenantAwareAuth) { it('should be rejected with ID token missing tenant ID', () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.MISMATCHING_TENANT_ID); + const expectedError = new FirebaseAuthError(authClientErrorCode.MISMATCHING_TENANT_ID); // Restore verifyIdToken stub. stub.restore(); // Simulate JWT does not contain tenant ID. @@ -726,7 +726,7 @@ AUTH_CONFIGS.forEach((testConfig) => { }); it('should be rejected with ID token containing mismatching tenant ID', () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.MISMATCHING_TENANT_ID); + const expectedError = new FirebaseAuthError(authClientErrorCode.MISMATCHING_TENANT_ID); // Restore verifyIdToken stub. stub.restore(); // Simulate JWT does not contain matching tenant ID. @@ -788,7 +788,7 @@ AUTH_CONFIGS.forEach((testConfig) => { it('should reject when underlying sessionCookieVerifier.verifyJWT() rejects with expected error', () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, 'Decoding Firebase session cookie failed'); + authClientErrorCode.INVALID_ARGUMENT, 'Decoding Firebase session cookie failed'); // Restore verifySessionCookie stub. stub.restore(); // Simulate session cookie is invalid. @@ -894,7 +894,7 @@ AUTH_CONFIGS.forEach((testConfig) => { }); it('should be rejected with checkRevoked set to true if underlying RPC fails', () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + const expectedError = new FirebaseAuthError(authClientErrorCode.USER_NOT_FOUND); const getUserStub = sinon.stub(testConfig.Auth.prototype, 'getUser') .rejects(expectedError); stubs.push(getUserStub); @@ -980,7 +980,7 @@ AUTH_CONFIGS.forEach((testConfig) => { }); it('should be rejected with checkRevoked set to true using an invalid session cookie', () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CREDENTIAL); + const expectedError = new FirebaseAuthError(authClientErrorCode.INVALID_CREDENTIAL); // Restore verifySessionCookie stub. stub.restore(); // Simulate session cookie is invalid. @@ -1000,7 +1000,7 @@ AUTH_CONFIGS.forEach((testConfig) => { if (testConfig.Auth === TenantAwareAuth) { it('should be rejected with session cookie missing tenant ID', () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.MISMATCHING_TENANT_ID); + const expectedError = new FirebaseAuthError(authClientErrorCode.MISMATCHING_TENANT_ID); // Restore verifyIdToken stub. stub.restore(); // Simulate JWT does not contain tenant ID.. @@ -1017,7 +1017,7 @@ AUTH_CONFIGS.forEach((testConfig) => { }); it('should be rejected with ID token containing mismatching tenant ID', () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.MISMATCHING_TENANT_ID); + const expectedError = new FirebaseAuthError(authClientErrorCode.MISMATCHING_TENANT_ID); // Restore verifyIdToken stub. stub.restore(); // Simulate JWT does not contain matching tenant ID.. @@ -1080,7 +1080,7 @@ AUTH_CONFIGS.forEach((testConfig) => { it('should reject when underlying idTokenVerifier._verifyAuthBlockingToken() rejects', () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, 'Decoding Firebase Auth Blocking token failed'); + authClientErrorCode.INVALID_ARGUMENT, 'Decoding Firebase Auth Blocking token failed'); // Restore _verifyAuthBlockingToken stub. stub.restore(); // Simulate Auth Blocking token is invalid. @@ -1135,7 +1135,7 @@ AUTH_CONFIGS.forEach((testConfig) => { const tenantId = testConfig.supportsTenantManagement ? undefined : TENANT_ID; const expectedGetAccountInfoResult = getValidGetAccountInfoResponse(tenantId); const expectedUserRecord = getValidUserRecord(expectedGetAccountInfoResult); - const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + const expectedError = new FirebaseAuthError(authClientErrorCode.USER_NOT_FOUND); // Stubs used to simulate underlying api calls. let stubs: sinon.SinonStub[] = []; @@ -1214,7 +1214,7 @@ AUTH_CONFIGS.forEach((testConfig) => { const tenantId = testConfig.supportsTenantManagement ? undefined : TENANT_ID; const expectedGetAccountInfoResult = getValidGetAccountInfoResponse(tenantId); const expectedUserRecord = getValidUserRecord(expectedGetAccountInfoResult); - const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + const expectedError = new FirebaseAuthError(authClientErrorCode.USER_NOT_FOUND); // Stubs used to simulate underlying api calls. let stubs: sinon.SinonStub[] = []; @@ -1293,7 +1293,7 @@ AUTH_CONFIGS.forEach((testConfig) => { const tenantId = testConfig.supportsTenantManagement ? undefined : TENANT_ID; const expectedGetAccountInfoResult = getValidGetAccountInfoResponse(tenantId); const expectedUserRecord = getValidUserRecord(expectedGetAccountInfoResult); - const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + const expectedError = new FirebaseAuthError(authClientErrorCode.USER_NOT_FOUND); // Stubs used to simulate underlying api calls. let stubs: sinon.SinonStub[] = []; @@ -1374,7 +1374,7 @@ AUTH_CONFIGS.forEach((testConfig) => { const tenantId = testConfig.supportsTenantManagement ? undefined : TENANT_ID; const expectedGetAccountInfoResult = getValidGetAccountInfoResponse(tenantId); const expectedUserRecord = getValidUserRecord(expectedGetAccountInfoResult); - const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + const expectedError = new FirebaseAuthError(authClientErrorCode.USER_NOT_FOUND); // Stubs used to simulate underlying api calls. let stubs: sinon.SinonStub[] = []; @@ -1562,7 +1562,7 @@ AUTH_CONFIGS.forEach((testConfig) => { describe('deleteUser()', () => { const uid = 'abcdefghijklmnopqrstuvwxyz'; const expectedDeleteAccountResult = { kind: 'identitytoolkit#DeleteAccountResponse' }; - const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + const expectedError = new FirebaseAuthError(authClientErrorCode.USER_NOT_FOUND); // Stubs used to simulate underlying api calls. let stubs: sinon.SinonStub[] = []; @@ -1700,10 +1700,10 @@ AUTH_CONFIGS.forEach((testConfig) => { const expectedGetAccountInfoResult = getValidGetAccountInfoResponse(tenantId); const expectedUserRecord = getValidUserRecord(expectedGetAccountInfoResult); const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'Unable to create the user record provided.'); const unableToCreateUserError = new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'Unable to create the user record provided.'); const propertiesToCreate = { displayName: expectedUserRecord.displayName, @@ -1793,7 +1793,7 @@ AUTH_CONFIGS.forEach((testConfig) => { const createUserStub = sinon.stub(testConfig.RequestHandler.prototype, 'createNewAccount') .resolves(uid); // Stub getAccountInfoByUid to throw user not found error. - const userNotFoundError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + const userNotFoundError = new FirebaseAuthError(authClientErrorCode.USER_NOT_FOUND); const getUserStub = sinon.stub(testConfig.RequestHandler.prototype, 'getAccountInfoByUid') .rejects(userNotFoundError); stubs.push(createUserStub); @@ -1837,7 +1837,7 @@ AUTH_CONFIGS.forEach((testConfig) => { const tenantId = testConfig.supportsTenantManagement ? undefined : TENANT_ID; const expectedGetAccountInfoResult = getValidGetAccountInfoResponse(tenantId); const expectedUserRecord = getValidUserRecord(expectedGetAccountInfoResult); - const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + const expectedError = new FirebaseAuthError(authClientErrorCode.USER_NOT_FOUND); const propertiesToEdit = { displayName: expectedUserRecord.displayName, photoURL: expectedUserRecord.photoURL, @@ -2258,7 +2258,7 @@ AUTH_CONFIGS.forEach((testConfig) => { describe('setCustomUserClaims()', () => { const uid = 'abcdefghijklmnopqrstuvwxyz'; - const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + const expectedError = new FirebaseAuthError(authClientErrorCode.USER_NOT_FOUND); const customClaims = { admin: true, groupId: '123456', @@ -2359,7 +2359,7 @@ AUTH_CONFIGS.forEach((testConfig) => { }); describe('listUsers()', () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR); + const expectedError = new FirebaseAuthError(authClientErrorCode.INTERNAL_ERROR); const pageToken = 'PAGE_TOKEN'; const maxResult = 500; const downloadAccountResponse: any = { @@ -2502,7 +2502,7 @@ AUTH_CONFIGS.forEach((testConfig) => { describe('revokeRefreshTokens()', () => { const uid = 'abcdefghijklmnopqrstuvwxyz'; - const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + const expectedError = new FirebaseAuthError(authClientErrorCode.USER_NOT_FOUND); // Stubs used to simulate underlying api calls. let stubs: sinon.SinonStub[] = []; beforeEach(() => { @@ -2590,11 +2590,11 @@ AUTH_CONFIGS.forEach((testConfig) => { }, }; const expectedUserImportResultError = - new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER); + new FirebaseAuthError(authClientErrorCode.INVALID_PHONE_NUMBER); const expectedOptionsError = - new FirebaseAuthError(AuthClientErrorCode.INVALID_HASH_ALGORITHM); + new FirebaseAuthError(authClientErrorCode.INVALID_HASH_ALGORITHM); const expectedServerError = - new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR); + new FirebaseAuthError(authClientErrorCode.INTERNAL_ERROR); const expectedUserImportResult = { successCount: 1, failureCount: 1, @@ -2706,7 +2706,7 @@ AUTH_CONFIGS.forEach((testConfig) => { const idToken = 'ID_TOKEN'; const options = { expiresIn: 60 * 60 * 24 * 1000 }; const sessionCookie = 'SESSION_COOKIE'; - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_ID_TOKEN); + const expectedError = new FirebaseAuthError(authClientErrorCode.INVALID_ID_TOKEN); const expectedUserRecord = getValidUserRecord(getValidGetAccountInfoResponse(tenantId)); // Set auth_time of token to expected user's tokensValidAfterTime. if (!expectedUserRecord.tokensValidAfterTime) { @@ -2892,7 +2892,7 @@ AUTH_CONFIGS.forEach((testConfig) => { const expectedLink = 'https://custom.page.link?link=' + encodeURIComponent('https://projectId.firebaseapp.com/__/auth/action?oobCode=CODE') + '&apn=com.example.android&ibi=com.example.ios'; - const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + const expectedError = new FirebaseAuthError(authClientErrorCode.USER_NOT_FOUND); // Stubs used to simulate underlying api calls. let stubs: sinon.SinonStub[] = []; afterEach(() => { @@ -3090,7 +3090,7 @@ AUTH_CONFIGS.forEach((testConfig) => { issuer: 'https://oidc.com/issuer', }; const expectedConfig = new OIDCConfig(serverResponse); - const expectedError = new FirebaseAuthError(AuthClientErrorCode.CONFIGURATION_NOT_FOUND); + const expectedError = new FirebaseAuthError(authClientErrorCode.CONFIGURATION_NOT_FOUND); it('should resolve with an OIDCConfig on success', () => { // Stub getOAuthIdpConfig to return expected result. @@ -3144,7 +3144,7 @@ AUTH_CONFIGS.forEach((testConfig) => { enabled: true, }; const expectedConfig = new SAMLConfig(serverResponse); - const expectedError = new FirebaseAuthError(AuthClientErrorCode.CONFIGURATION_NOT_FOUND); + const expectedError = new FirebaseAuthError(authClientErrorCode.CONFIGURATION_NOT_FOUND); it('should resolve with a SAMLConfig on success', () => { // Stub getInboundSamlConfig to return expected result. @@ -3218,7 +3218,7 @@ AUTH_CONFIGS.forEach((testConfig) => { }); describe('using OIDC type filter', () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR); + const expectedError = new FirebaseAuthError(authClientErrorCode.INTERNAL_ERROR); const pageToken = 'PAGE_TOKEN'; const maxResults = 50; const filterOptions: AuthProviderConfigFilter = { @@ -3313,7 +3313,7 @@ AUTH_CONFIGS.forEach((testConfig) => { }); describe('using SAML type filter', () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR); + const expectedError = new FirebaseAuthError(authClientErrorCode.INTERNAL_ERROR); const pageToken = 'PAGE_TOKEN'; const maxResults = 50; const filterOptions: AuthProviderConfigFilter = { @@ -3453,7 +3453,7 @@ AUTH_CONFIGS.forEach((testConfig) => { describe('using OIDC configurations', () => { const providerId = 'oidc.provider'; - const expectedError = new FirebaseAuthError(AuthClientErrorCode.CONFIGURATION_NOT_FOUND); + const expectedError = new FirebaseAuthError(authClientErrorCode.CONFIGURATION_NOT_FOUND); it('should resolve with void on success', () => { // Stub deleteOAuthIdpConfig to resolve. @@ -3488,7 +3488,7 @@ AUTH_CONFIGS.forEach((testConfig) => { describe('using SAML configurations', () => { const providerId = 'saml.provider'; - const expectedError = new FirebaseAuthError(AuthClientErrorCode.CONFIGURATION_NOT_FOUND); + const expectedError = new FirebaseAuthError(authClientErrorCode.CONFIGURATION_NOT_FOUND); it('should resolve with void on success', () => { // Stub deleteInboundSamlConfig to resolve. @@ -3598,7 +3598,7 @@ AUTH_CONFIGS.forEach((testConfig) => { issuer: 'https://oidc.com/issuer', }; const expectedConfig = new OIDCConfig(serverResponse); - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CONFIG); + const expectedError = new FirebaseAuthError(authClientErrorCode.INVALID_CONFIG); it('should resolve with an OIDCConfig on updateOAuthIdpConfig request success', () => { // Stub updateOAuthIdpConfig to return expected server response. @@ -3664,7 +3664,7 @@ AUTH_CONFIGS.forEach((testConfig) => { enabled: true, }; const expectedConfig = new SAMLConfig(serverResponse); - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CONFIG); + const expectedError = new FirebaseAuthError(authClientErrorCode.INVALID_CONFIG); it('should resolve with a SAMLConfig on updateInboundSamlConfig request success', () => { // Stub updateInboundSamlConfig to return expected server response. @@ -3764,7 +3764,7 @@ AUTH_CONFIGS.forEach((testConfig) => { issuer: 'https://oidc.com/issuer', }; const expectedConfig = new OIDCConfig(serverResponse); - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CONFIG); + const expectedError = new FirebaseAuthError(authClientErrorCode.INVALID_CONFIG); it('should resolve with an OIDCConfig on createOAuthIdpConfig request success', () => { // Stub createOAuthIdpConfig to return expected server response. @@ -3831,7 +3831,7 @@ AUTH_CONFIGS.forEach((testConfig) => { enabled: true, }; const expectedConfig = new SAMLConfig(serverResponse); - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CONFIG); + const expectedError = new FirebaseAuthError(authClientErrorCode.INVALID_CONFIG); it('should resolve with a SAMLConfig on createInboundSamlConfig request success', () => { // Stub createInboundSamlConfig to return expected server response. @@ -3975,4 +3975,6 @@ AUTH_CONFIGS.forEach((testConfig) => { }); }); }); + + }); diff --git a/test/unit/auth/project-config-manager.spec.ts b/test/unit/auth/project-config-manager.spec.ts index be7df54d99..8d56c34c0d 100644 --- a/test/unit/auth/project-config-manager.spec.ts +++ b/test/unit/auth/project-config-manager.spec.ts @@ -25,7 +25,7 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as mocks from '../../resources/mocks'; import { FirebaseApp } from '../../../src/app/firebase-app'; import { AuthRequestHandler } from '../../../src/auth/auth-api-request'; -import { AuthClientErrorCode, FirebaseAuthError } from '../../../src/utils/error'; +import { authClientErrorCode, FirebaseAuthError } from '../../../src/auth/error'; import { ProjectConfigManager } from '../../../src/auth/project-config-manager'; import { ProjectConfig, @@ -81,7 +81,7 @@ describe('ProjectConfigManager', () => { describe('getProjectConfig()', () => { const expectedProjectConfig = new ProjectConfig(GET_CONFIG_RESPONSE); - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CONFIG); + const expectedError = new FirebaseAuthError(authClientErrorCode.INVALID_CONFIG); // Stubs used to simulate underlying API calls. let stubs: sinon.SinonStub[] = []; afterEach(() => { @@ -152,7 +152,7 @@ describe('ProjectConfigManager', () => { }; const expectedProjectConfig = new ProjectConfig(GET_CONFIG_RESPONSE); const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, + authClientErrorCode.INTERNAL_ERROR, 'Unable to update the config provided.'); // Stubs used to simulate underlying API calls. let stubs: sinon.SinonStub[] = []; diff --git a/test/unit/auth/tenant-manager.spec.ts b/test/unit/auth/tenant-manager.spec.ts index 782e4f96bb..dd041419b7 100644 --- a/test/unit/auth/tenant-manager.spec.ts +++ b/test/unit/auth/tenant-manager.spec.ts @@ -26,7 +26,7 @@ import * as mocks from '../../resources/mocks'; import { FirebaseApp } from '../../../src/app/firebase-app'; import { AuthRequestHandler } from '../../../src/auth/auth-api-request'; import { TenantServerResponse } from '../../../src/auth/tenant'; -import { AuthClientErrorCode, FirebaseAuthError } from '../../../src/utils/error'; +import { authClientErrorCode, FirebaseAuthError } from '../../../src/auth/error'; import { CreateTenantRequest, UpdateTenantRequest, ListTenantsResult, Tenant, TenantManager, } from '../../../src/auth/index'; @@ -102,7 +102,7 @@ describe('TenantManager', () => { describe('getTenant()', () => { const tenantId = 'tenant-id'; const expectedTenant = new Tenant(GET_TENANT_RESPONSE); - const expectedError = new FirebaseAuthError(AuthClientErrorCode.TENANT_NOT_FOUND); + const expectedError = new FirebaseAuthError(authClientErrorCode.TENANT_NOT_FOUND); // Stubs used to simulate underlying API calls. let stubs: sinon.SinonStub[] = []; afterEach(() => { @@ -173,7 +173,7 @@ describe('TenantManager', () => { }); describe('listTenants()', () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR); + const expectedError = new FirebaseAuthError(authClientErrorCode.INTERNAL_ERROR); const pageToken = 'PAGE_TOKEN'; const maxResult = 500; const listTenantsResponse: any = { @@ -307,7 +307,7 @@ describe('TenantManager', () => { describe('deleteTenant()', () => { const tenantId = 'tenant-id'; - const expectedError = new FirebaseAuthError(AuthClientErrorCode.TENANT_NOT_FOUND); + const expectedError = new FirebaseAuthError(authClientErrorCode.TENANT_NOT_FOUND); // Stubs used to simulate underlying API calls. let stubs: sinon.SinonStub[] = []; afterEach(() => { @@ -389,9 +389,10 @@ describe('TenantManager', () => { anonymousSignInEnabled: true, }; const expectedTenant = new Tenant(GET_TENANT_RESPONSE); - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - 'Unable to create the tenant provided.'); + const expectedError = new FirebaseAuthError({ + code: authClientErrorCode.INTERNAL_ERROR.code, + message: 'Unable to create the tenant provided.' + }); // Stubs used to simulate underlying API calls. let stubs: sinon.SinonStub[] = []; afterEach(() => { @@ -482,9 +483,10 @@ describe('TenantManager', () => { anonymousSignInEnabled: true, }; const expectedTenant = new Tenant(GET_TENANT_RESPONSE); - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - 'Unable to update the tenant provided.'); + const expectedError = new FirebaseAuthError({ + code: authClientErrorCode.INTERNAL_ERROR.code, + message: 'Unable to update the tenant provided.' + }); // Stubs used to simulate underlying API calls. let stubs: sinon.SinonStub[] = []; afterEach(() => { diff --git a/test/unit/auth/token-generator.spec.ts b/test/unit/auth/token-generator.spec.ts index 1cc9bd44ab..3b2ac86c9a 100644 --- a/test/unit/auth/token-generator.spec.ts +++ b/test/unit/auth/token-generator.spec.ts @@ -31,7 +31,7 @@ import { import { CryptoSignerError, CryptoSignerErrorCode, ServiceAccountSigner } from '../../../src/utils/crypto-signer'; import { ServiceAccountCredential } from '../../../src/app/credential-internal'; -import { FirebaseAuthError } from '../../../src/utils/error'; +import { FirebaseAuthError } from '../../../src/auth/error'; import * as utils from '../utils'; chai.should(); @@ -360,7 +360,7 @@ describe('FirebaseTokenGenerator', () => { const authError = handleCryptoSignerError(cryptoError); expect(authError).to.be.an.instanceof(FirebaseAuthError); expect(authError).to.have.property('code', 'auth/internal-error'); - expect(authError).to.have.property('message', 'server error.; Please refer to https://firebase.google.com/docs/auth/admin/create-custom-tokens for more details on how to use and troubleshoot this feature. Raw server response: "{"error":{"message":"server error."}}"'); + expect(authError).to.have.property('message', 'server error.; Please refer to https://firebase.google.com/docs/auth/admin/create-custom-tokens for more details on how to use and troubleshoot this feature.'); }); it('should convert CryptoSignerError HttpError with no errorcode to FirebaseAuthError', () => { diff --git a/test/unit/auth/token-verifier.spec.ts b/test/unit/auth/token-verifier.spec.ts index 3dae6817cd..d90c055a2a 100644 --- a/test/unit/auth/token-verifier.spec.ts +++ b/test/unit/auth/token-verifier.spec.ts @@ -32,7 +32,7 @@ import { ServiceAccountSigner } from '../../../src/utils/crypto-signer'; import * as verifier from '../../../src/auth/token-verifier'; import { ServiceAccountCredential } from '../../../src/app/credential-internal'; import { FirebaseApp } from '../../../src/app/firebase-app'; -import { AuthClientErrorCode } from '../../../src/utils/error'; +import { authClientErrorCode } from '../../../src/auth/error'; import { JwtError, JwtErrorCode, PublicKeySignatureVerifier } from '../../../src/utils/jwt'; chai.should(); @@ -104,7 +104,7 @@ describe('FirebaseTokenVerifier', () => { verifyApiName: 'verifyToken()', jwtName: 'Important Token', shortName: 'token', - expiredErrorCode: AuthClientErrorCode.INVALID_ARGUMENT, + expiredErrorCode: authClientErrorCode.INVALID_ARGUMENT, }, app, ); @@ -151,7 +151,7 @@ describe('FirebaseTokenVerifier', () => { verifyApiName: invalidVerifyApiName as any, jwtName: 'Important Token', shortName: 'token', - expiredErrorCode: AuthClientErrorCode.INVALID_ARGUMENT, + expiredErrorCode: authClientErrorCode.INVALID_ARGUMENT, }, app, ); @@ -171,7 +171,7 @@ describe('FirebaseTokenVerifier', () => { verifyApiName: 'verifyToken()', jwtName: invalidJwtName as any, shortName: 'token', - expiredErrorCode: AuthClientErrorCode.INVALID_ARGUMENT, + expiredErrorCode: authClientErrorCode.INVALID_ARGUMENT, }, app, ); @@ -191,7 +191,7 @@ describe('FirebaseTokenVerifier', () => { verifyApiName: 'verifyToken()', jwtName: 'Important Token', shortName: invalidShortName as any, - expiredErrorCode: AuthClientErrorCode.INVALID_ARGUMENT, + expiredErrorCode: authClientErrorCode.INVALID_ARGUMENT, }, app, ); diff --git a/test/unit/auth/user-import-builder.spec.ts b/test/unit/auth/user-import-builder.spec.ts index 6be38ebe28..d6988b7c9a 100644 --- a/test/unit/auth/user-import-builder.spec.ts +++ b/test/unit/auth/user-import-builder.spec.ts @@ -22,7 +22,7 @@ import { deepCopy } from '../../../src/utils/deep-copy'; import { UserImportBuilder, ValidatorFunction, UploadAccountRequest, } from '../../../src/auth/user-import-builder'; -import { AuthClientErrorCode, FirebaseAuthError } from '../../../src/utils/error'; +import { authClientErrorCode, FirebaseAuthError } from '../../../src/auth/error'; import { toWebSafeBase64 } from '../../../src/utils'; import { UpdatePhoneMultiFactorInfoRequest, UserImportResult, UserImportRecord, @@ -55,7 +55,7 @@ describe('UserImportBuilder', () => { // Simulate a validation error is thrown for a specific user. if (request.localId === '5678') { throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_PHONE_NUMBER, + authClientErrorCode.INVALID_PHONE_NUMBER, ); } }; @@ -176,7 +176,7 @@ describe('UserImportBuilder', () => { invalidUserImportOptions.forEach((invalidOption) => { it(`should throw when non-object ${JSON.stringify(invalidOption)} UserImportOptions is provided`, () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, + authClientErrorCode.INVALID_ARGUMENT, '"UserImportOptions" are required when importing users with passwords.', ); expect(() => { @@ -187,7 +187,7 @@ describe('UserImportBuilder', () => { it('should throw when an empty hash algorithm is provided', () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.MISSING_HASH_ALGORITHM, + authClientErrorCode.MISSING_HASH_ALGORITHM, '"hash.algorithm" is missing from the provided "UserImportOptions".', ); expect(() => { @@ -197,7 +197,7 @@ describe('UserImportBuilder', () => { it('should throw when an invalid hash algorithm is provided', () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_HASH_ALGORITHM, + authClientErrorCode.INVALID_HASH_ALGORITHM, 'Unsupported hash algorithm provider "invalid".', ); const invalidOptions = { @@ -226,7 +226,7 @@ describe('UserImportBuilder', () => { invalidKeys.forEach((key) => { it(`should throw when non-Buffer ${JSON.stringify(key)} hash key is provided`, () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_HASH_KEY, + authClientErrorCode.INVALID_HASH_KEY, 'A non-empty "hash.key" byte buffer must be provided for ' + `hash algorithm ${algorithm}.`, ); @@ -289,7 +289,7 @@ describe('UserImportBuilder', () => { invalidRounds.forEach((rounds) => { it(`should throw when ${JSON.stringify(rounds)} rounds provided`, () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_HASH_ROUNDS, + authClientErrorCode.INVALID_HASH_ROUNDS, `A valid "hash.rounds" number between ${minRounds} and ${maxRounds} must be provided for ` + `hash algorithm ${algorithm}.`, ); @@ -334,7 +334,7 @@ describe('UserImportBuilder', () => { invalidKeys.forEach((key) => { it(`should throw when ${JSON.stringify(key)} key provided`, () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_HASH_KEY, + authClientErrorCode.INVALID_HASH_KEY, 'A "hash.key" byte buffer must be provided for ' + `hash algorithm ${algorithm}.`, ); @@ -355,7 +355,7 @@ describe('UserImportBuilder', () => { invalidRounds.forEach((rounds) => { it(`should throw when ${JSON.stringify(rounds)} rounds provided`, () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_HASH_ROUNDS, + authClientErrorCode.INVALID_HASH_ROUNDS, 'A valid "hash.rounds" number between 1 and 8 must be provided for ' + `hash algorithm ${algorithm}.`, ); @@ -376,7 +376,7 @@ describe('UserImportBuilder', () => { invalidMemoryCost.forEach((memoryCost) => { it(`should throw when ${JSON.stringify(memoryCost)} memoryCost provided`, () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_HASH_MEMORY_COST, + authClientErrorCode.INVALID_HASH_MEMORY_COST, 'A valid "hash.memoryCost" number between 1 and 14 must be provided for ' + `hash algorithm ${algorithm}.`, ); @@ -397,7 +397,7 @@ describe('UserImportBuilder', () => { invalidSaltSeparator.forEach((saltSeparator) => { it(`should throw when ${JSON.stringify(saltSeparator)} saltSeparator provided`, () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_HASH_SALT_SEPARATOR, + authClientErrorCode.INVALID_HASH_SALT_SEPARATOR, '"hash.saltSeparator" must be a byte buffer.', ); const invalidOptions = { @@ -465,7 +465,7 @@ describe('UserImportBuilder', () => { invalidMemoryCost.forEach((memoryCost) => { it(`should throw when ${JSON.stringify(memoryCost)} memoryCost provided`, () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_HASH_MEMORY_COST, + authClientErrorCode.INVALID_HASH_MEMORY_COST, 'A valid "hash.memoryCost" number must be provided for ' + `hash algorithm ${algorithm}.`, ); @@ -487,7 +487,7 @@ describe('UserImportBuilder', () => { invalidParallelization.forEach((parallelization) => { it(`should throw when ${JSON.stringify(parallelization)} parallelization provided`, () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_HASH_MEMORY_COST, + authClientErrorCode.INVALID_HASH_MEMORY_COST, 'A valid "hash.parallelization" number must be provided for ' + `hash algorithm ${algorithm}.`, ); @@ -509,7 +509,7 @@ describe('UserImportBuilder', () => { invalidBlockSize.forEach((blockSize) => { it(`should throw when ${JSON.stringify(blockSize)} blockSize provided`, () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_HASH_BLOCK_SIZE, + authClientErrorCode.INVALID_HASH_BLOCK_SIZE, 'A valid "hash.blockSize" number must be provided for ' + `hash algorithm ${algorithm}.`, ); @@ -531,7 +531,7 @@ describe('UserImportBuilder', () => { invalidDerivedKeyLength.forEach((derivedKeyLength) => { it(`should throw when ${JSON.stringify(derivedKeyLength)} dkLen provided`, () => { const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_HASH_DERIVED_KEY_LENGTH, + authClientErrorCode.INVALID_HASH_DERIVED_KEY_LENGTH, 'A valid "hash.derivedKeyLength" number must be provided for ' + `hash algorithm ${algorithm}.`, ); @@ -741,7 +741,7 @@ describe('UserImportBuilder', () => { // Index should match server error index. index: 1, error: new FirebaseAuthError( - AuthClientErrorCode.INVALID_USER_IMPORT, + authClientErrorCode.INVALID_USER_IMPORT, 'Some error occurred!', ), }, @@ -760,7 +760,7 @@ describe('UserImportBuilder', () => { successCount: 3, failureCount: 1, errors: [ - { index: 2, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER) }, + { index: 2, error: new FirebaseAuthError(authClientErrorCode.INVALID_PHONE_NUMBER) }, ], }; // userRequestValidatorWithError will throw on the 3rd user (index = 2). @@ -780,9 +780,9 @@ describe('UserImportBuilder', () => { const userRequestValidatorWithMultipleErrors: ValidatorFunction = (request) => { // Simulate a validation error is thrown for specific users. if (request.localId === 'USER2') { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL); + throw new FirebaseAuthError(authClientErrorCode.INVALID_EMAIL); } else if (request.localId === 'USER4') { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER); + throw new FirebaseAuthError(authClientErrorCode.INVALID_PHONE_NUMBER); } }; @@ -836,39 +836,39 @@ describe('UserImportBuilder', () => { failureCount: 8, errors: [ // Client side detected error. - { index: 1, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL) }, + { index: 1, error: new FirebaseAuthError(authClientErrorCode.INVALID_EMAIL) }, // Server side detected error. { index: 2, error: new FirebaseAuthError( - AuthClientErrorCode.INVALID_USER_IMPORT, + authClientErrorCode.INVALID_USER_IMPORT, 'Some error occurred in USER3!', ), }, // Client side detected error. - { index: 3, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER) }, + { index: 3, error: new FirebaseAuthError(authClientErrorCode.INVALID_PHONE_NUMBER) }, // Server side detected error. { index: 5, error: new FirebaseAuthError( - AuthClientErrorCode.INVALID_USER_IMPORT, + authClientErrorCode.INVALID_USER_IMPORT, 'Another error occurred in USER6!', ), }, // Client side errors. - { index: 6, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD_HASH) }, - { index: 7, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD_SALT) }, + { index: 6, error: new FirebaseAuthError(authClientErrorCode.INVALID_PASSWORD_HASH) }, + { index: 7, error: new FirebaseAuthError(authClientErrorCode.INVALID_PASSWORD_SALT) }, { index: 8, error: new FirebaseAuthError( - AuthClientErrorCode.INVALID_ENROLLMENT_TIME, + authClientErrorCode.INVALID_ENROLLMENT_TIME, 'The second factor "enrollmentTime" for "enrollmentId1" must be a valid ' + 'UTC date string.'), }, { index: 9, error: new FirebaseAuthError( - AuthClientErrorCode.UNSUPPORTED_SECOND_FACTOR, + authClientErrorCode.UNSUPPORTED_SECOND_FACTOR, `Unsupported second factor "${JSON.stringify(testUsers[9].multiFactor!.enrolledFactors[0])}" provided.`), }, ], diff --git a/test/unit/data-connect/data-connect-api-client-internal.spec.ts b/test/unit/data-connect/data-connect-api-client-internal.spec.ts index 7dc20080b1..5951c29264 100644 --- a/test/unit/data-connect/data-connect-api-client-internal.spec.ts +++ b/test/unit/data-connect/data-connect-api-client-internal.spec.ts @@ -24,12 +24,16 @@ import { } from '../../../src/utils/api-request'; import * as utils from '../utils'; import * as mocks from '../../resources/mocks'; -import { DATA_CONNECT_ERROR_CODE_MAPPING, DataConnectApiClient, FirebaseDataConnectError } - from '../../../src/data-connect/data-connect-api-client-internal'; +import { DataConnectApiClient } from '../../../src/data-connect/data-connect-api-client-internal'; +import { + FirebaseDataConnectError, + DATA_CONNECT_ERROR_CODE_MAPPING, +} from '../../../src/data-connect/error'; import { FirebaseApp } from '../../../src/app/firebase-app'; import { ConnectorConfig } from '../../../src/data-connect'; import { getMetricsHeader, getSdkVersion } from '../../../src/utils'; import { OperationOptions } from '../../../src/data-connect/data-connect-api'; +import { toHttpResponse } from '../../../src/utils/error'; describe('DataConnectApiClient', () => { @@ -159,35 +163,58 @@ describe('DataConnectApiClient', () => { }); it('should reject when a full platform error response is received', () => { + const mockErr = utils.errorFrom(ERROR_RESPONSE, 404); sandbox .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom(ERROR_RESPONSE, 404)); - const expected = new FirebaseDataConnectError('not-found', 'Requested entity not found'); + .rejects(mockErr); + const expected = new FirebaseDataConnectError({ + code: 'not-found', + message: 'Requested entity not found', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.executeGraphql('query', {}) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should reject with unknown-error when error code is not present', () => { + const mockErr = utils.errorFrom({}, 404); sandbox .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom({}, 404)); - const expected = new FirebaseDataConnectError('unknown-error', 'Unknown server error: {}'); + .rejects(mockErr); + const expected = new FirebaseDataConnectError({ + code: 'unknown-error', + message: 'Unknown server error: {}', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.executeGraphql('query', {}) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should reject with unknown-error for non-json response', () => { + const mockErr = utils.errorFrom('not json', 404); sandbox .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom('not json', 404)); - const expected = new FirebaseDataConnectError( - 'unknown-error', 'Unexpected response with status: 404 and body: not json'); + .rejects(mockErr); + const expected = new FirebaseDataConnectError({ + code: 'unknown-error', + message: 'Unexpected response with status: 404 and body: not json', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.executeGraphql('query', {}) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should reject when rejected with a FirebaseDataConnectError', () => { - const expected = new FirebaseDataConnectError('internal-error', 'socket hang up'); + const expected = new FirebaseDataConnectError({ code: 'internal-error', message: 'socket hang up' }); sandbox .stub(HttpClient.prototype, 'send') .rejects(expected); @@ -269,35 +296,58 @@ describe('DataConnectApiClient', () => { }); it('should reject when a full platform error response is received', () => { + const mockErr = utils.errorFrom(ERROR_RESPONSE, 404); sandbox .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom(ERROR_RESPONSE, 404)); - const expected = new FirebaseDataConnectError('not-found', 'Requested entity not found'); + .rejects(mockErr); + const expected = new FirebaseDataConnectError({ + code: 'not-found', + message: 'Requested entity not found', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.executeQuery('unauthenticated query', undefined, unauthenticatedOptions) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should reject with unknown-error when error code is not present', () => { + const mockErr = utils.errorFrom({}, 404); sandbox .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom({}, 404)); - const expected = new FirebaseDataConnectError('unknown-error', 'Unknown server error: {}'); + .rejects(mockErr); + const expected = new FirebaseDataConnectError({ + code: 'unknown-error', + message: 'Unknown server error: {}', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.executeQuery('unauthenticated query', undefined, unauthenticatedOptions) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should reject with unknown-error for non-json response', () => { + const mockErr = utils.errorFrom('not json', 404); sandbox .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom('not json', 404)); - const expected = new FirebaseDataConnectError( - 'unknown-error', 'Unexpected response with status: 404 and body: not json'); + .rejects(mockErr); + const expected = new FirebaseDataConnectError({ + code: 'unknown-error', + message: 'Unexpected response with status: 404 and body: not json', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.executeQuery('unauthenticated query', undefined, unauthenticatedOptions) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should reject when rejected with a FirebaseDataConnectError', () => { - const expected = new FirebaseDataConnectError('internal-error', 'socket hang up'); + const expected = new FirebaseDataConnectError({ code: 'internal-error', message: 'socket hang up' }); sandbox .stub(HttpClient.prototype, 'send') .rejects(expected); @@ -417,35 +467,58 @@ describe('DataConnectApiClient', () => { }); it('should reject when a full platform error response is received', () => { + const mockErr = utils.errorFrom(ERROR_RESPONSE, 404); sandbox .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom(ERROR_RESPONSE, 404)); - const expected = new FirebaseDataConnectError('not-found', 'Requested entity not found'); + .rejects(mockErr); + const expected = new FirebaseDataConnectError({ + code: 'not-found', + message: 'Requested entity not found', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.executeMutation('unauthenticated mutation', undefined, unauthenticatedOptions) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should reject with unknown-error when error code is not present', () => { + const mockErr = utils.errorFrom({}, 404); sandbox .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom({}, 404)); - const expected = new FirebaseDataConnectError('unknown-error', 'Unknown server error: {}'); + .rejects(mockErr); + const expected = new FirebaseDataConnectError({ + code: 'unknown-error', + message: 'Unknown server error: {}', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.executeMutation('unauthenticated mutation', undefined, unauthenticatedOptions) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should reject with unknown-error for non-json response', () => { + const mockErr = utils.errorFrom('not json', 404); sandbox .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom('not json', 404)); - const expected = new FirebaseDataConnectError( - 'unknown-error', 'Unexpected response with status: 404 and body: not json'); + .rejects(mockErr); + const expected = new FirebaseDataConnectError({ + code: 'unknown-error', + message: 'Unexpected response with status: 404 and body: not json', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.executeMutation('unauthenticated mutation', undefined, unauthenticatedOptions) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should reject when rejected with a FirebaseDataConnectError', () => { - const expected = new FirebaseDataConnectError('internal-error', 'socket hang up'); + const expected = new FirebaseDataConnectError({ code: 'internal-error', message: 'socket hang up' }); sandbox .stub(HttpClient.prototype, 'send') .rejects(expected); @@ -597,10 +670,10 @@ describe('DataConnectApiClient CRUD helpers', () => { const additionalErrorMessageForBulkImport = 'Make sure that your table name passed in matches the type name in your GraphQL schema file.'; - const expectedQueryError = new FirebaseDataConnectError( - DATA_CONNECT_ERROR_CODE_MAPPING.QUERY_ERROR, - serverErrorString - ); + const expectedQueryError = new FirebaseDataConnectError({ + code: DATA_CONNECT_ERROR_CODE_MAPPING.QUERY_ERROR, + message: serverErrorString + }); // Helper function to normalize GraphQL strings const normalizeGraphQLString = (str: string): string => { @@ -692,8 +765,14 @@ describe('DataConnectApiClient CRUD helpers', () => { }); it('should amend the message for query errors', async () => { - await expect(apiClientQueryError.insert(tableName, { data: 1 })) - .to.be.rejectedWith(FirebaseDataConnectError, `${serverErrorString}. ${additionalErrorMessageForBulkImport}`); + try { + await apiClientQueryError.insert(tableName, { data: 1 }); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err).to.be.instanceOf(FirebaseDataConnectError); + expect(err.message).to.equal(`${serverErrorString}. ${additionalErrorMessageForBulkImport}`); + expect(err.cause).to.equal(expectedQueryError); + } }); }); @@ -778,8 +857,14 @@ describe('DataConnectApiClient CRUD helpers', () => { }); it('should amend the message for query errors', async () => { - await expect(apiClientQueryError.insertMany(tableName, [{ data: 1 }])) - .to.be.rejectedWith(FirebaseDataConnectError, `${serverErrorString}. ${additionalErrorMessageForBulkImport}`); + try { + await apiClientQueryError.insertMany(tableName, [{ data: 1 }]); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err).to.be.instanceOf(FirebaseDataConnectError); + expect(err.message).to.equal(`${serverErrorString}. ${additionalErrorMessageForBulkImport}`); + expect(err.cause).to.equal(expectedQueryError); + } }); }); @@ -841,8 +926,14 @@ describe('DataConnectApiClient CRUD helpers', () => { }); it('should amend the message for query errors', async () => { - await expect(apiClientQueryError.upsert(tableName, { data: 1 })) - .to.be.rejectedWith(FirebaseDataConnectError, `${serverErrorString}. ${additionalErrorMessageForBulkImport}`); + try { + await apiClientQueryError.upsert(tableName, { data: 1 }); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err).to.be.instanceOf(FirebaseDataConnectError); + expect(err.message).to.equal(`${serverErrorString}. ${additionalErrorMessageForBulkImport}`); + expect(err.cause).to.equal(expectedQueryError); + } }); }); @@ -924,8 +1015,14 @@ describe('DataConnectApiClient CRUD helpers', () => { }); it('should amend the message for query errors', async () => { - await expect(apiClientQueryError.upsertMany(tableName, [{ data: 1 }])) - .to.be.rejectedWith(FirebaseDataConnectError, `${serverErrorString}. ${additionalErrorMessageForBulkImport}`); + try { + await apiClientQueryError.upsertMany(tableName, [{ data: 1 }]); + expect.fail('Should have failed'); + } catch (err: any) { + expect(err).to.be.instanceOf(FirebaseDataConnectError); + expect(err.message).to.equal(`${serverErrorString}. ${additionalErrorMessageForBulkImport}`); + expect(err.cause).to.equal(expectedQueryError); + } }); }); diff --git a/test/unit/data-connect/validate-admin-args.spec.ts b/test/unit/data-connect/validate-admin-args.spec.ts index 7c60718838..54248aa846 100644 --- a/test/unit/data-connect/validate-admin-args.spec.ts +++ b/test/unit/data-connect/validate-admin-args.spec.ts @@ -22,7 +22,7 @@ import { OperationOptions } from '../../../src/data-connect'; import { DATA_CONNECT_ERROR_CODE_MAPPING, FirebaseDataConnectError -} from '../../../src/data-connect/data-connect-api-client-internal'; +} from '../../../src/data-connect/error'; import * as dataConnectIndex from '../../../src/data-connect/index'; interface IdVars { @@ -42,10 +42,10 @@ describe('validateAdminArgs()', () => { const variables: IdVars = { id: 'stephenarosaj' }; const options: OperationOptions = { impersonate: { unauthenticated: true } }; - const invalidVariablesError = new FirebaseDataConnectError( - DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, - 'Variables required.' - ); + const invalidVariablesError = new FirebaseDataConnectError({ + code: DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + message: 'Variables required.' + }); const stubDcInstance = { connectorConfig: connectorConfig, source: 'STUB' } as unknown as DataConnect; beforeEach(() => { diff --git a/test/unit/eventarc/eventarc.spec.ts b/test/unit/eventarc/eventarc.spec.ts index 93ca71d28a..55cc853b4a 100644 --- a/test/unit/eventarc/eventarc.spec.ts +++ b/test/unit/eventarc/eventarc.spec.ts @@ -27,10 +27,17 @@ import * as mocks from '../../resources/mocks'; import * as utils from '../utils'; import * as chai from 'chai'; import chaiExclude from 'chai-exclude'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); +chai.use(chaiExclude); + import { getMetricsHeader, getSdkVersion } from '../../../src/utils/index'; const expect = chai.expect; -chai.use(chaiExclude); const TEST_EVENT1 : CloudEvent = { type: 'some.custom.event1', diff --git a/test/unit/extensions/extensions-api-client-internal.spec.ts b/test/unit/extensions/extensions-api-client-internal.spec.ts index 62cb8eafca..f331f6651b 100644 --- a/test/unit/extensions/extensions-api-client-internal.spec.ts +++ b/test/unit/extensions/extensions-api-client-internal.spec.ts @@ -15,13 +15,22 @@ * limitations under the License. */ -import { expect } from 'chai'; +import * as chai from 'chai'; import * as sinon from 'sinon'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; import * as utils from '../utils'; import * as mocks from '../../resources/mocks'; import { FirebaseApp } from '../../../src/app/firebase-app'; -import { ExtensionsApiClient, FirebaseExtensionsError } from '../../../src/extensions/extensions-api-client-internal'; +import { ExtensionsApiClient } from '../../../src/extensions/extensions-api-client-internal'; +import { FirebaseExtensionsError } from '../../../src/extensions/error'; import { HttpClient } from '../../../src/utils/api-request'; import { SettableProcessingState } from '../../../src/extensions/extensions-api'; import { getMetricsHeader, getSdkVersion } from '../../../src/utils'; diff --git a/test/unit/extensions/extensions.spec.ts b/test/unit/extensions/extensions.spec.ts index 6f2d75a2de..6e9f4f5fbc 100644 --- a/test/unit/extensions/extensions.spec.ts +++ b/test/unit/extensions/extensions.spec.ts @@ -15,17 +15,25 @@ * limitations under the License. */ +import * as chai from 'chai'; import * as sinon from 'sinon'; -import { expect } from 'chai'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; import * as mocks from '../../resources/mocks'; import * as utils from '../utils'; import { FirebaseApp } from '../../../src/app/firebase-app'; import { Extensions } from '../../../src/extensions/extensions'; -import { FirebaseAppError } from '../../../src/utils/error'; +import { FirebaseAppError } from '../../../src/app/error'; import { HttpClient, HttpRequestConfig } from '../../../src/utils/api-request'; import { SettableProcessingState } from '../../../src/extensions/extensions-api'; -import { FirebaseExtensionsError } from '../../../src/extensions/extensions-api-client-internal'; +import { FirebaseExtensionsError } from '../../../src/extensions/error'; describe('Extensions', () => { const mockOptions = { diff --git a/test/unit/functions/functions-api-client-internal.spec.ts b/test/unit/functions/functions-api-client-internal.spec.ts index 34cc0ffab9..8a6a42080f 100644 --- a/test/unit/functions/functions-api-client-internal.spec.ts +++ b/test/unit/functions/functions-api-client-internal.spec.ts @@ -25,9 +25,11 @@ import * as mocks from '../../resources/mocks'; import { getSdkVersion, getMetricsHeader } from '../../../src/utils'; import { FirebaseApp } from '../../../src/app/firebase-app'; -import { FirebaseFunctionsError, FunctionsApiClient, Task } from '../../../src/functions/functions-api-client-internal'; +import { FunctionsApiClient, Task } from '../../../src/functions/functions-api-client-internal'; +import { FirebaseFunctionsError } from '../../../src/functions/error'; import { HttpClient } from '../../../src/utils/api-request'; -import { FirebaseAppError } from '../../../src/utils/error'; +import { toHttpResponse } from '../../../src/utils/error'; +import { FirebaseAppError } from '../../../src/app/error'; import { deepCopy } from '../../../src/utils/deep-copy'; import { EMULATED_SERVICE_ACCOUNT_DEFAULT } from '../../../src/functions/functions-api-client-internal'; @@ -221,38 +223,61 @@ describe('FunctionsApiClient', () => { }); it('should reject when a full platform error response is received', () => { + const mockErr = utils.errorFrom(ERROR_RESPONSE, 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom(ERROR_RESPONSE, 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseFunctionsError('not-found', 'Requested entity not found'); + const expected = new FirebaseFunctionsError({ + code: 'not-found', + message: 'Requested entity not found', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.enqueue({}, FUNCTION_NAME) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should reject with unknown-error when error code is not present', () => { + const mockErr = utils.errorFrom({}, 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom({}, 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseFunctionsError('unknown-error', 'Unknown server error: {}'); + const expected = new FirebaseFunctionsError({ + code: 'unknown-error', + message: 'Unknown server error: {}', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.enqueue({}, FUNCTION_NAME) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should reject with unknown-error for non-json response', () => { + const mockErr = utils.errorFrom('not json', 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom('not json', 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseFunctionsError( - 'unknown-error', 'Unexpected response with status: 404 and body: not json'); + const expected = new FirebaseFunctionsError({ + code: 'unknown-error', + message: 'Unexpected response with status: 404 and body: not json', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.enqueue({}, FUNCTION_NAME) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should reject when rejected with a FirebaseAppError', () => { - const expected = new FirebaseAppError('network-error', 'socket hang up'); + const expected = new FirebaseAppError({ code: 'network-error', message: 'socket hang up' }); const stub = sinon .stub(HttpClient.prototype, 'send') .rejects(expected); @@ -262,16 +287,21 @@ describe('FunctionsApiClient', () => { }); it('should reject when a task with the same ID exists', () => { + const mockErr = utils.errorFrom({}, 409); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom({}, 409)); + .rejects(mockErr); stubs.push(stub); - expect(apiClient.enqueue({}, FUNCTION_NAME, undefined, { id: 'mock-task' })).to.eventually.throw( - new FirebaseFunctionsError( - 'task-already-exists', - 'A task with ID mock-task already exists' - ) - ) + const expected = new FirebaseFunctionsError({ + code: 'task-already-exists', + message: 'A task with ID mock-task already exists', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); + return apiClient.enqueue({}, FUNCTION_NAME, undefined, { id: 'mock-task' }) + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should resolve on success', () => { @@ -614,11 +644,21 @@ describe('FunctionsApiClient', () => { }); it('should throw on non-404 HTTP errors', () => { + const mockErr = utils.errorFrom({}, 500); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom({}, 500)); + .rejects(mockErr); stubs.push(stub); - expect(apiClient.delete('mock-task', FUNCTION_NAME)).to.eventually.throw(utils.errorFrom({}, 500)); + const expected = new FirebaseFunctionsError({ + code: 'unknown-error', + message: 'Unknown server error: {}', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); + return apiClient.delete('mock-task', FUNCTION_NAME) + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); }) }); diff --git a/test/unit/functions/functions.spec.ts b/test/unit/functions/functions.spec.ts index 48ed21c886..210a4905e8 100644 --- a/test/unit/functions/functions.spec.ts +++ b/test/unit/functions/functions.spec.ts @@ -23,7 +23,8 @@ import * as sinon from 'sinon'; import * as mocks from '../../resources/mocks'; import { FirebaseApp } from '../../../src/app/firebase-app'; -import { FunctionsApiClient, FirebaseFunctionsError } from '../../../src/functions/functions-api-client-internal'; +import { FunctionsApiClient } from '../../../src/functions/functions-api-client-internal'; +import { FirebaseFunctionsError } from '../../../src/functions/error'; import { HttpClient } from '../../../src/utils/api-request'; import { Functions, TaskQueue } from '../../../src/functions/functions'; @@ -110,7 +111,7 @@ describe('Functions', () => { }); describe('TaskQueue', () => { - const INTERNAL_ERROR = new FirebaseFunctionsError('internal-error', 'message'); + const INTERNAL_ERROR = new FirebaseFunctionsError({ code: 'internal-error', message: 'message' }); const FUNCTION_NAME = 'function-name'; let taskQueue: TaskQueue; diff --git a/test/unit/installations/installations.spec.ts b/test/unit/installations/installations.spec.ts index 71d0eeae9d..1b78f70553 100644 --- a/test/unit/installations/installations.spec.ts +++ b/test/unit/installations/installations.spec.ts @@ -29,7 +29,10 @@ import * as mocks from '../../resources/mocks'; import { Installations } from '../../../src/installations/installations'; import { FirebaseInstallationsRequestHandler } from '../../../src/installations/installations-request-handler'; import { FirebaseApp } from '../../../src/app/firebase-app'; -import { FirebaseInstallationsError, InstallationsClientErrorCode } from '../../../src/utils/error'; +import { + FirebaseInstallationsError, + installationsClientErrorCode, +} from '../../../src/installations/error'; chai.should(); chai.use(sinonChai); @@ -127,7 +130,7 @@ describe('Installations', () => { // Stubs used to simulate underlying api calls. let stubs: sinon.SinonStub[] = []; - const expectedError = new FirebaseInstallationsError(InstallationsClientErrorCode.API_ERROR); + const expectedError = new FirebaseInstallationsError(installationsClientErrorCode.API_ERROR); const testInstallationId = 'test-iid'; afterEach(() => { diff --git a/test/unit/instance-id/instance-id.spec.ts b/test/unit/instance-id/instance-id.spec.ts index f43d19d9d0..34eea41101 100644 --- a/test/unit/instance-id/instance-id.spec.ts +++ b/test/unit/instance-id/instance-id.spec.ts @@ -30,9 +30,13 @@ import { InstanceId } from '../../../src/instance-id/index'; import { Installations } from '../../../src/installations/index'; import { FirebaseApp } from '../../../src/app/firebase-app'; import { - FirebaseInstanceIdError, InstanceIdClientErrorCode, - FirebaseInstallationsError, InstallationsClientErrorCode, -} from '../../../src/utils/error'; + FirebaseInstanceIdError, + instanceIdClientErrorCode, +} from '../../../src/instance-id/error'; +import { + FirebaseInstallationsError, + installationsClientErrorCode, +} from '../../../src/installations/error'; chai.should(); chai.use(sinonChai); @@ -175,7 +179,7 @@ describe('InstanceId', () => { it('should throw a FirebaseInstanceIdError error when the backend returns an error', () => { // Stub deleteInstanceId to throw a backend error. - const originalError = new FirebaseInstallationsError(InstallationsClientErrorCode.API_ERROR); + const originalError = new FirebaseInstallationsError(installationsClientErrorCode.API_ERROR); const stub = sinon.stub(Installations.prototype, 'deleteInstallation') .rejects(originalError); stubs.push(stub); @@ -186,7 +190,7 @@ describe('InstanceId', () => { // Confirm underlying API called with expected parameters. expect(stub).to.have.been.calledOnce.and.calledWith(testInstanceId); // Confirm expected error returned. - const expectedError = new FirebaseInstanceIdError(InstanceIdClientErrorCode.API_ERROR); + const expectedError = new FirebaseInstanceIdError(instanceIdClientErrorCode.API_ERROR); expect(error).to.be.instanceOf(FirebaseInstanceIdError) expect(error).to.deep.include(expectedError); }); diff --git a/test/unit/machine-learning/machine-learning-api-client.spec.ts b/test/unit/machine-learning/machine-learning-api-client.spec.ts index ce99b85177..619f98edc2 100644 --- a/test/unit/machine-learning/machine-learning-api-client.spec.ts +++ b/test/unit/machine-learning/machine-learning-api-client.spec.ts @@ -19,11 +19,12 @@ import * as _ from 'lodash'; import * as chai from 'chai'; import * as sinon from 'sinon'; -import { FirebaseMachineLearningError } from '../../../src/machine-learning/machine-learning-utils'; +import { FirebaseMachineLearningError } from '../../../src/machine-learning/error'; import { HttpClient } from '../../../src/utils/api-request'; import * as utils from '../utils'; import * as mocks from '../../resources/mocks'; -import { FirebaseAppError } from '../../../src/utils/error'; +import { toHttpResponse } from '../../../src/utils/error'; +import { FirebaseAppError } from '../../../src/app/error'; import { FirebaseApp } from '../../../src/app/firebase-app'; import { getMetricsHeader, getSdkVersion } from '../../../src/utils/index'; import { MachineLearningApiClient } from '../../../src/machine-learning/machine-learning-api-client'; @@ -178,13 +179,21 @@ describe('MachineLearningApiClient', () => { }); it('should throw when an error response is received', () => { + const mockErr = utils.errorFrom(ERROR_RESPONSE, 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom(ERROR_RESPONSE, 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseMachineLearningError('not-found', 'Requested entity not found'); + const expected = new FirebaseMachineLearningError({ + code: 'not-found', + message: 'Requested entity not found', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.createModel(NAME_ONLY_OPTIONS) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should resolve with the created resource on success', () => { @@ -227,28 +236,43 @@ describe('MachineLearningApiClient', () => { }); it('should reject with unknown-error when error code is not present', () => { + const mockErr = utils.errorFrom({}, 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom({}, 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseMachineLearningError('unknown-error', 'Unknown server error: {}'); + const expected = new FirebaseMachineLearningError({ + code: 'unknown-error', + message: 'Unknown server error: {}', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.createModel(NAME_ONLY_OPTIONS) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should reject with unknown-error for non-json response', () => { + const mockErr = utils.errorFrom('not json', 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom('not json', 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseMachineLearningError( - 'unknown-error', 'Unexpected response with status: 404 and body: not json'); + const expected = new FirebaseMachineLearningError({ + code: 'unknown-error', + message: 'Unexpected response with status: 404 and body: not json', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.createModel(NAME_ONLY_OPTIONS) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should reject with when failed with a FirebaseAppError', () => { - const expected = new FirebaseAppError('network-error', 'socket hang up'); + const expected = new FirebaseAppError({ code: 'network-error', message: 'socket hang up' }); const stub = sinon .stub(HttpClient.prototype, 'send') .rejects(expected); @@ -294,13 +318,21 @@ describe('MachineLearningApiClient', () => { }); it('should throw when an error response is received', () => { + const mockErr = utils.errorFrom(ERROR_RESPONSE, 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom(ERROR_RESPONSE, 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseMachineLearningError('not-found', 'Requested entity not found'); + const expected = new FirebaseMachineLearningError({ + code: 'not-found', + message: 'Requested entity not found', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.updateModel(MODEL_ID, NAME_ONLY_OPTIONS, NAME_ONLY_MASK_LIST) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should resolve with the updated resource on success', () => { @@ -355,28 +387,43 @@ describe('MachineLearningApiClient', () => { }); it('should reject with unknown-error when error code is not present', () => { + const mockErr = utils.errorFrom({}, 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom({}, 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseMachineLearningError('unknown-error', 'Unknown server error: {}'); + const expected = new FirebaseMachineLearningError({ + code: 'unknown-error', + message: 'Unknown server error: {}', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.updateModel(MODEL_ID, NAME_ONLY_OPTIONS, NAME_ONLY_MASK_LIST) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should reject with unknown-error for non-json response', () => { + const mockErr = utils.errorFrom('not json', 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom('not json', 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseMachineLearningError( - 'unknown-error', 'Unexpected response with status: 404 and body: not json'); + const expected = new FirebaseMachineLearningError({ + code: 'unknown-error', + message: 'Unexpected response with status: 404 and body: not json', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.updateModel(MODEL_ID, NAME_ONLY_OPTIONS, NAME_ONLY_MASK_LIST) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should reject with when failed with a FirebaseAppError', () => { - const expected = new FirebaseAppError('network-error', 'socket hang up'); + const expected = new FirebaseAppError({ code: 'network-error', message: 'socket hang up' }); const stub = sinon .stub(HttpClient.prototype, 'send') .rejects(expected); @@ -424,38 +471,61 @@ describe('MachineLearningApiClient', () => { }); it('should reject when a full platform error response is received', () => { + const mockErr = utils.errorFrom(ERROR_RESPONSE, 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom(ERROR_RESPONSE, 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseMachineLearningError('not-found', 'Requested entity not found'); + const expected = new FirebaseMachineLearningError({ + code: 'not-found', + message: 'Requested entity not found', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.getModel(MODEL_ID) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should reject unknown-error when error code is not present', () => { + const mockErr = utils.errorFrom({}, 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom({}, 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseMachineLearningError('unknown-error', 'Unknown server error: {}'); + const expected = new FirebaseMachineLearningError({ + code: 'unknown-error', + message: 'Unknown server error: {}', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.getModel(MODEL_ID) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should reject unknown-error for non-json response', () => { + const mockErr = utils.errorFrom('not json', 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom('not json', 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseMachineLearningError( - 'unknown-error', 'Unexpected response with status: 404 and body: not json'); + const expected = new FirebaseMachineLearningError({ + code: 'unknown-error', + message: 'Unexpected response with status: 404 and body: not json', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.getModel(MODEL_ID) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should reject when failed with a FirebaseAppError', () => { - const expected = new FirebaseAppError('network-error', 'socket hang up'); + const expected = new FirebaseAppError({ code: 'network-error', message: 'socket hang up' }); const stub = sinon .stub(HttpClient.prototype, 'send') .rejects(expected); @@ -483,38 +553,61 @@ describe('MachineLearningApiClient', () => { }); it('should reject when a full platform error response is received', () => { + const mockErr = utils.errorFrom(ERROR_RESPONSE, 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom(ERROR_RESPONSE, 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseMachineLearningError('not-found', 'Requested entity not found'); + const expected = new FirebaseMachineLearningError({ + code: 'not-found', + message: 'Requested entity not found', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.getOperation(OPERATION_NAME) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should reject with unknown-error when error code is not present', () => { + const mockErr = utils.errorFrom({}, 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom({}, 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseMachineLearningError('unknown-error', 'Unknown server error: {}'); + const expected = new FirebaseMachineLearningError({ + code: 'unknown-error', + message: 'Unknown server error: {}', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.getOperation(OPERATION_NAME) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should reject with unknown-error for non-json response', () => { + const mockErr = utils.errorFrom('not json', 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom('not json', 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseMachineLearningError( - 'unknown-error', 'Unexpected response with status: 404 and body: not json'); + const expected = new FirebaseMachineLearningError({ + code: 'unknown-error', + message: 'Unexpected response with status: 404 and body: not json', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.getOperation(OPERATION_NAME) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should reject when failed with a FirebaseAppError', () => { - const expected = new FirebaseAppError('network-error', 'socket hang up'); + const expected = new FirebaseAppError({ code: 'network-error', message: 'socket hang up' }); const stub = sinon .stub(HttpClient.prototype, 'send') .rejects(expected); @@ -533,7 +626,7 @@ describe('MachineLearningApiClient', () => { }); it('handles a done operation with error', () => { - const expected = new FirebaseMachineLearningError('invalid-argument', STATUS_ERROR_MESSAGE); + const expected = new FirebaseMachineLearningError({ code: 'invalid-argument', message: STATUS_ERROR_MESSAGE }); return apiClient.handleOperation(OPERATION_ERROR_RESPONSE) .should.eventually.be.rejected.and.deep.include(expected); }); @@ -579,7 +672,7 @@ describe('MachineLearningApiClient', () => { stub.onCall(0).resolves(utils.responseFrom(OPERATION_NOT_DONE_RESPONSE)); stub.onCall(1).resolves(utils.responseFrom(OPERATION_ERROR_RESPONSE)); stubs.push(stub); - const expected = new FirebaseMachineLearningError('invalid-argument', STATUS_ERROR_MESSAGE); + const expected = new FirebaseMachineLearningError({ code: 'invalid-argument', message: STATUS_ERROR_MESSAGE }); return apiClient.handleOperation(OPERATION_NOT_DONE_RESPONSE, { wait: true, maxTimeMillis: 1000, @@ -704,38 +797,61 @@ describe('MachineLearningApiClient', () => { }); it('should throw when a full platform error response is received', () => { + const mockErr = utils.errorFrom(ERROR_RESPONSE, 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom(ERROR_RESPONSE, 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseMachineLearningError('not-found', 'Requested entity not found'); + const expected = new FirebaseMachineLearningError({ + code: 'not-found', + message: 'Requested entity not found', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.listModels() - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should throw unknown-error when error code is not present', () => { + const mockErr = utils.errorFrom({}, 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom({}, 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseMachineLearningError('unknown-error', 'Unknown server error: {}'); + const expected = new FirebaseMachineLearningError({ + code: 'unknown-error', + message: 'Unknown server error: {}', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.listModels() - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should throw unknown-error for non-json response', () => { + const mockErr = utils.errorFrom('not json', 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom('not json', 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseMachineLearningError( - 'unknown-error', 'Unexpected response with status: 404 and body: not json'); + const expected = new FirebaseMachineLearningError({ + code: 'unknown-error', + message: 'Unexpected response with status: 404 and body: not json', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.listModels() - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should throw when rejected with a FirebaseAppError', () => { - const expected = new FirebaseAppError('network-error', 'socket hang up'); + const expected = new FirebaseAppError({ code: 'network-error', message: 'socket hang up' }); const stub = sinon .stub(HttpClient.prototype, 'send') .rejects(expected); @@ -782,38 +898,61 @@ describe('MachineLearningApiClient', () => { }); it('should reject when a full platform error response is received', () => { + const mockErr = utils.errorFrom(ERROR_RESPONSE, 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom(ERROR_RESPONSE, 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseMachineLearningError('not-found', 'Requested entity not found'); + const expected = new FirebaseMachineLearningError({ + code: 'not-found', + message: 'Requested entity not found', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.deleteModel(MODEL_ID) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should reject with unknown-error when error code is not present', () => { + const mockErr = utils.errorFrom({}, 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom({}, 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseMachineLearningError('unknown-error', 'Unknown server error: {}'); + const expected = new FirebaseMachineLearningError({ + code: 'unknown-error', + message: 'Unknown server error: {}', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.deleteModel(MODEL_ID) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should reject with unknown-error for non-json response', () => { + const mockErr = utils.errorFrom('not json', 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom('not json', 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseMachineLearningError( - 'unknown-error', 'Unexpected response with status: 404 and body: not json'); + const expected = new FirebaseMachineLearningError({ + code: 'unknown-error', + message: 'Unexpected response with status: 404 and body: not json', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.deleteModel(MODEL_ID) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should reject when failed with a FirebaseAppError', () => { - const expected = new FirebaseAppError('network-error', 'socket hang up'); + const expected = new FirebaseAppError({ code: 'network-error', message: 'socket hang up' }); const stub = sinon .stub(HttpClient.prototype, 'send') .rejects(expected); diff --git a/test/unit/machine-learning/machine-learning.spec.ts b/test/unit/machine-learning/machine-learning.spec.ts index ef2ec15239..9394882dfa 100644 --- a/test/unit/machine-learning/machine-learning.spec.ts +++ b/test/unit/machine-learning/machine-learning.spec.ts @@ -27,7 +27,7 @@ import { ModelResponse, OperationResponse } from '../../../src/machine-learning/machine-learning-api-client'; -import { FirebaseMachineLearningError } from '../../../src/machine-learning/machine-learning-utils'; +import { FirebaseMachineLearningError } from '../../../src/machine-learning/error'; import { deepCopy } from '../../../src/utils/deep-copy'; import { MachineLearning, Model, ModelOptions } from '../../../src/machine-learning/index'; @@ -40,7 +40,7 @@ describe('MachineLearning', () => { const PROJECT_NUMBER = '987654'; const OPERATION_ID = '456789'; const OPERATION_NAME = `projects/${PROJECT_NUMBER}/operations/${OPERATION_ID}` - const EXPECTED_ERROR = new FirebaseMachineLearningError('internal-error', 'message'); + const EXPECTED_ERROR = new FirebaseMachineLearningError({ code: 'internal-error', message: 'message' }); const CREATE_TIME_UTC = 'Fri, 07 Feb 2020 23:45:23 GMT'; const UPDATE_TIME_UTC = 'Sat, 08 Feb 2020 23:45:23 GMT'; const MODEL_RESPONSE: { diff --git a/test/unit/messaging/messaging-errors-internal.spec.ts b/test/unit/messaging/messaging-errors-internal.spec.ts new file mode 100644 index 0000000000..360fe0716b --- /dev/null +++ b/test/unit/messaging/messaging-errors-internal.spec.ts @@ -0,0 +1,120 @@ +/*! + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as chai from 'chai'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +import { createFirebaseError } from '../../../src/messaging/messaging-errors-internal'; +import { RequestResponseError, RequestResponse } from '../../../src/utils/api-request'; +import { FirebaseMessagingError } from '../../../src/messaging/error'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('messaging-errors-internal', () => { + describe('createFirebaseError', () => { + it('should create FirebaseMessagingError for JSON response with error code', () => { + const mockResponse: Partial = { + status: 400, + headers: {}, + isJson: () => true, + data: { + error: { + status: 'INVALID_ARGUMENT', + message: 'Specific error message' + } + } + }; + const mockError = new RequestResponseError(mockResponse as RequestResponse); + + const error = createFirebaseError(mockError); + + expect(error).to.be.instanceOf(FirebaseMessagingError); + expect(error.code).to.equal('messaging/invalid-argument'); + expect(error.message).to.equal('Specific error message'); + expect(error.httpResponse).to.deep.equal({ + status: 400, + headers: {}, + data: mockResponse.data + }); + }); + + it('should create FirebaseMessagingError for non-JSON response (400)', () => { + const mockResponse: Partial = { + status: 400, + headers: {}, + isJson: () => false, + text: 'Raw error text' + }; + const mockError = new RequestResponseError(mockResponse as RequestResponse); + + const error = createFirebaseError(mockError); + + expect(error).to.be.instanceOf(FirebaseMessagingError); + expect(error.code).to.equal('messaging/invalid-argument'); + expect(error.message).to.contain('Raw server response: "Raw error text"'); + expect(error.httpResponse).to.deep.equal({ + status: 400, + headers: {}, + data: 'Raw error text' + }); + }); + + it('should create FirebaseMessagingError for non-JSON response (500)', () => { + const mockResponse: Partial = { + status: 500, + headers: {}, + isJson: () => false, + text: 'Internal error text' + }; + const mockError = new RequestResponseError(mockResponse as RequestResponse); + + const error = createFirebaseError(mockError); + + expect(error).to.be.instanceOf(FirebaseMessagingError); + expect(error.code).to.equal('messaging/internal-error'); + expect(error.message).to.contain('Raw server response: "Internal error text"'); + }); + + it('should not leak extra properties from RequestResponse in toJSON()', () => { + const mockResponse = { + status: 400, + headers: {}, + isJson: () => false, + text: 'Raw error text', + extraProp: 'should not be here' + }; + const mockError = new RequestResponseError(mockResponse as any); + + const error = createFirebaseError(mockError); + const json = error.toJSON() as any; + + expect(json.httpResponse).to.deep.equal({ + status: 400, + headers: {}, + data: 'Raw error text' + }); + expect(json.httpResponse.text).to.be.undefined; + expect(json.httpResponse.extraProp).to.be.undefined; + }); + }); +}); diff --git a/test/unit/messaging/messaging.spec.ts b/test/unit/messaging/messaging.spec.ts index af4dad1d4d..675b6e8430 100644 --- a/test/unit/messaging/messaging.spec.ts +++ b/test/unit/messaging/messaging.spec.ts @@ -34,7 +34,6 @@ import { import { HttpClient } from '../../../src/utils/api-request'; import { getMetricsHeader, getSdkVersion } from '../../../src/utils/index'; import * as utils from '../utils'; -import { FirebaseMessagingSessionError } from '../../../src/utils/error'; chai.should(); chai.use(sinonChai); @@ -82,10 +81,10 @@ function mockSendRequest(messageId = 'projects/projec_id/messages/message_id'): function mockHttp2SendRequestResponse(messageId = 'projects/projec_id/messages/message_id'): mocks.MockHttp2Response { return { headers: { - ':status': 200, + ':status': 200, }, data: Buffer.from(JSON.stringify({ name: `${messageId}` })), - } as mocks.MockHttp2Response + } as mocks.MockHttp2Response; } function mockSendError( @@ -119,14 +118,14 @@ function mockHttp2SendRequestError( 'content-type': contentType }, data: Buffer.from(response) - } as mocks.MockHttp2Response + } as mocks.MockHttp2Response; } -function mockHttp2Error(streamError?: any, sessionError?:any): mocks.MockHttp2Response { +function mockHttp2Error(streamError?: any, sessionError?: any): mocks.MockHttp2Response { return { streamError: streamError, sessionError: sessionError - } as mocks.MockHttp2Response + } as mocks.MockHttp2Response; } function mockErrorResponse( @@ -212,14 +211,14 @@ describe('Messaging', () => { let messaging: Messaging; let legacyMessaging: Messaging; let mockedRequests: nock.Scope[] = []; - let mockedHttp2Responses: mocks.MockHttp2Response[] = [] + let mockedHttp2Responses: mocks.MockHttp2Response[] = []; const http2Mocker: mocks.Http2Mocker = new mocks.Http2Mocker(); let httpsRequestStub: sinon.SinonStub; let getTokenStub: sinon.SinonStub; let nullAccessTokenMessaging: Messaging; - let messagingService: { [key: string]: any }; - let nullAccessTokenMessagingService: { [key: string]: any }; + let messagingService: { [key: string]: any; }; + let nullAccessTokenMessagingService: { [key: string]: any; }; const mockAccessToken: string = utils.generateRandomAccessToken(); const expectedHeaders = { @@ -251,7 +250,7 @@ describe('Messaging', () => { if (httpsRequestStub && httpsRequestStub.restore) { httpsRequestStub.restore(); } - http2Mocker.done() + http2Mocker.done(); mockedHttp2Responses = []; getTokenStub.restore(); return mockApp.delete(); @@ -519,7 +518,7 @@ describe('Messaging', () => { const messageIds = [ 'projects/projec_id/messages/1', ]; - messageIds.forEach(id => mockedRequests.push(mockSendRequest(id))) + messageIds.forEach(id => mockedRequests.push(mockSendRequest(id))); return legacyMessaging.sendEach([invalidMessage, validMessage]) .then((response: BatchResponse) => { expect(response.successCount).to.equal(1); @@ -542,7 +541,7 @@ describe('Messaging', () => { 'projects/projec_id/messages/2', 'projects/projec_id/messages/3', ]; - messageIds.forEach(id => mockedRequests.push(mockSendRequest(id))) + messageIds.forEach(id => mockedRequests.push(mockSendRequest(id))); return legacyMessaging.sendEach([validMessage, validMessage, validMessage]) .then((response: BatchResponse) => { expect(response.successCount).to.equal(3); @@ -561,7 +560,7 @@ describe('Messaging', () => { 'projects/projec_id/messages/2', 'projects/projec_id/messages/3', ]; - messageIds.forEach(id => mockedRequests.push(mockSendRequest(id))) + messageIds.forEach(id => mockedRequests.push(mockSendRequest(id))); const message = { token: 'a', android: { @@ -595,7 +594,7 @@ describe('Messaging', () => { 'projects/projec_id/messages/2', 'projects/projec_id/messages/3', ]; - messageIds.forEach(id => mockedRequests.push(mockSendRequest(id))) + messageIds.forEach(id => mockedRequests.push(mockSendRequest(id))); return legacyMessaging.sendEach([validMessage, validMessage, validMessage], true) .then((response: BatchResponse) => { expect(response.successCount).to.equal(3); @@ -620,8 +619,8 @@ describe('Messaging', () => { }, }, ]; - messageIds.forEach(id => mockedRequests.push(mockSendRequest(id))) - errors.forEach(error => mockedRequests.push(mockSendError(400, 'json', error))) + messageIds.forEach(id => mockedRequests.push(mockSendRequest(id))); + errors.forEach(error => mockedRequests.push(mockSendError(400, 'json', error))); return legacyMessaging.sendEach([validMessage, validMessage, validMessage], true) .then((response: BatchResponse) => { expect(response.successCount).to.equal(2); @@ -637,7 +636,7 @@ describe('Messaging', () => { }); it('should be fulfilled with a BatchResponse for all failures given an app which ' + - 'returns null access tokens', () => { + 'returns null access tokens', () => { return nullAccessTokenMessaging.sendEach( [validMessage, validMessage], ).then((response: BatchResponse) => { @@ -665,8 +664,8 @@ describe('Messaging', () => { }, }, ]; - messageIds.forEach(id => mockedRequests.push(mockSendRequest(id))) - errors.forEach(error => mockedRequests.push(mockSendError(404, 'json', error))) + messageIds.forEach(id => mockedRequests.push(mockSendRequest(id))); + errors.forEach(error => mockedRequests.push(mockSendError(404, 'json', error))); return legacyMessaging.sendEach([validMessage, validMessage], true) .then((response: BatchResponse) => { expect(response.successCount).to.equal(1); @@ -713,7 +712,7 @@ describe('Messaging', () => { const conditionMessage: ConditionMessage = { condition: 'test' }; const messages: Message[] = [tokenMessage, topicMessage, conditionMessage]; - messageIds.forEach(id => mockedRequests.push(mockSendRequest(id))) + messageIds.forEach(id => mockedRequests.push(mockSendRequest(id))); return legacyMessaging.sendEach(messages) .then((response: BatchResponse) => { @@ -734,8 +733,8 @@ describe('Messaging', () => { 'projects/projec_id/messages/3', ]; - messageIds.forEach(id => mockedHttp2Responses.push(mockHttp2SendRequestResponse(id))) - http2Mocker.http2Stub(mockedHttp2Responses) + messageIds.forEach(id => mockedHttp2Responses.push(mockHttp2SendRequestResponse(id))); + http2Mocker.http2Stub(mockedHttp2Responses); return messaging.sendEach([validMessage, validMessage, validMessage], false) .then((response: BatchResponse) => { @@ -756,8 +755,8 @@ describe('Messaging', () => { 'projects/projec_id/messages/3', ]; - messageIds.forEach(id => mockedHttp2Responses.push(mockHttp2SendRequestResponse(id))) - http2Mocker.http2Stub(mockedHttp2Responses) + messageIds.forEach(id => mockedHttp2Responses.push(mockHttp2SendRequestResponse(id))); + http2Mocker.http2Stub(mockedHttp2Responses); const message = { token: 'a', @@ -794,8 +793,8 @@ describe('Messaging', () => { 'projects/projec_id/messages/3', ]; - messageIds.forEach(id => mockedHttp2Responses.push(mockHttp2SendRequestResponse(id))) - http2Mocker.http2Stub(mockedHttp2Responses) + messageIds.forEach(id => mockedHttp2Responses.push(mockHttp2SendRequestResponse(id))); + http2Mocker.http2Stub(mockedHttp2Responses); return messaging.sendEach([validMessage, validMessage, validMessage], true) .then((response: BatchResponse) => { @@ -823,9 +822,9 @@ describe('Messaging', () => { }, ]; - messageIds.forEach(id => mockedHttp2Responses.push(mockHttp2SendRequestResponse(id))) - errors.forEach(error => mockedHttp2Responses.push(mockHttp2SendRequestError(400, 'json', error))) - http2Mocker.http2Stub(mockedHttp2Responses) + messageIds.forEach(id => mockedHttp2Responses.push(mockHttp2SendRequestResponse(id))); + errors.forEach(error => mockedHttp2Responses.push(mockHttp2SendRequestError(400, 'json', error))); + http2Mocker.http2Stub(mockedHttp2Responses); return messaging.sendEach([validMessage, validMessage, validMessage], true) .then((response: BatchResponse) => { @@ -843,7 +842,7 @@ describe('Messaging', () => { }); it('should be fulfilled with a BatchResponse for all failures given an app which ' + - 'returns null access tokens using HTTP/2', () => { + 'returns null access tokens using HTTP/2', () => { return nullAccessTokenMessaging.sendEach( [validMessage, validMessage], false).then((response: BatchResponse) => { expect(response.failureCount).to.equal(2); @@ -871,9 +870,9 @@ describe('Messaging', () => { }, ]; - messageIds.forEach(id => mockedHttp2Responses.push(mockHttp2SendRequestResponse(id))) - errors.forEach(error => mockedHttp2Responses.push(mockHttp2SendRequestError(400, 'json', error))) - http2Mocker.http2Stub(mockedHttp2Responses) + messageIds.forEach(id => mockedHttp2Responses.push(mockHttp2SendRequestResponse(id))); + errors.forEach(error => mockedHttp2Responses.push(mockHttp2SendRequestError(400, 'json', error))); + http2Mocker.http2Stub(mockedHttp2Responses); return messaging.sendEach([validMessage, validMessage], true) .then((response: BatchResponse) => { @@ -899,7 +898,7 @@ describe('Messaging', () => { mockedHttp2Responses.push(mockHttp2SendRequestError(404, 'json', error)); mockedHttp2Responses.push(mockHttp2SendRequestError(400, 'json', { error: 'test error message2' })); mockedHttp2Responses.push(mockHttp2SendRequestError(400, 'text', 'foo bar')); - http2Mocker.http2Stub(mockedHttp2Responses) + http2Mocker.http2Stub(mockedHttp2Responses); return messaging.sendEach( [validMessage, validMessage, validMessage], false @@ -913,29 +912,74 @@ describe('Messaging', () => { }); }); - it('should throw error with BatchResponse promise on session error event using HTTP/2', () => { - mockedHttp2Responses.push(mockHttp2SendRequestResponse('projects/projec_id/messages/1')) - const sessionError = 'MOCK_SESSION_ERROR' + it('should be fulfilled with a BatchResponse containing session errors when session fails using HTTP/2', () => { + mockedHttp2Responses.push(mockHttp2SendRequestResponse('projects/projec_id/messages/1')); + const sessionError = 'MOCK_SESSION_ERROR'; mockedHttp2Responses.push(mockHttp2Error( new Error(`MOCK_STREAM_ERROR caused by ${sessionError}`), new Error(sessionError) )); - http2Mocker.http2Stub(mockedHttp2Responses) + http2Mocker.http2Stub(mockedHttp2Responses); return messaging.sendEach( [validMessage, validMessage], true - ).catch(async (error: FirebaseMessagingSessionError) => { - expect(error.code).to.equal('messaging/app/network-error'); - expect(error.pendingBatchResponse).to.not.be.undefined; - await error.pendingBatchResponse?.then((response: BatchResponse) => { - expect(http2Mocker.requests.length).to.equal(2); - expect(response.failureCount).to.equal(1); - const responses = response.responses; - checkSendResponseSuccess(responses[0], 'projects/projec_id/messages/1'); - checkSendResponseFailure(responses[1], 'app/network-error'); - }) + ).then((response: BatchResponse) => { + expect(http2Mocker.requests.length).to.equal(2); + expect(response.successCount).to.equal(1); + expect(response.failureCount).to.equal(1); + const responses = response.responses; + checkSendResponseSuccess(responses[0], 'projects/projec_id/messages/1'); + checkSendResponseFailure( + responses[1], + 'messaging/unknown-error', + sessionError + ); + expect(responses[1].error!.message).to.contain(`MOCK_STREAM_ERROR caused by ${sessionError}`); + expect(responses[1].error!.cause!.constructor.name).to.equal('AggregateError'); + const cause = responses[1].error!.cause as any; + expect(cause.errors).to.be.an.instanceOf(Array); + expect(cause.errors.length).to.equal(2); + expect(cause.errors[0].message).to.contain('MOCK_STREAM_ERROR'); + expect(cause.errors[1].message).to.contain(sessionError); + }); + }); + + it('should be fulfilled with a BatchResponse containing AggregateError when multiple session errors occur' + + ' using HTTP/2', () => { + const sessionError1 = 'MOCK_SESSION_ERROR_1'; + const sessionError2 = 'MOCK_SESSION_ERROR_2'; + + mockedHttp2Responses.push(mockHttp2Error( + new Error('MOCK_STREAM_ERROR_1'), + new Error(sessionError1) + )); + mockedHttp2Responses.push(mockHttp2Error( + new Error('MOCK_STREAM_ERROR_2'), + new Error(sessionError2) + )); + + http2Mocker.http2Stub(mockedHttp2Responses); + + return messaging.sendEach( + [validMessage, validMessage], true + ).then((response: BatchResponse) => { + expect(http2Mocker.requests.length).to.equal(2); + expect(response.failureCount).to.equal(2); + + const failure = response.responses[0]; + expect(failure.success).to.be.false; + expect(failure.error!.code).to.equal('messaging/unknown-error'); + + const cause = failure.error!.cause; + expect(cause).to.not.be.undefined; + expect(cause!.constructor.name).to.equal('AggregateError'); + expect((cause as any).errors).to.be.an.instanceOf(Array); + expect((cause as any).errors.length).to.equal(3); + expect((cause as any).errors[0].message).to.contain('MOCK_STREAM_ERROR'); + expect((cause as any).errors[1].message).to.contain(sessionError1); + expect((cause as any).errors[2].message).to.contain(sessionError2); }); - }) + }); // This test was added to also verify https://github.com/firebase/firebase-admin-node/issues/1146 it('should be fulfilled when called with different message types using HTTP/2', () => { @@ -949,11 +993,11 @@ describe('Messaging', () => { const conditionMessage: ConditionMessage = { condition: 'test' }; const messages: Message[] = [tokenMessage, topicMessage, conditionMessage]; - messageIds.forEach(id => mockedHttp2Responses.push(mockHttp2SendRequestResponse(id))) - http2Mocker.http2Stub(mockedHttp2Responses) + messageIds.forEach(id => mockedHttp2Responses.push(mockHttp2SendRequestResponse(id))); + http2Mocker.http2Stub(mockedHttp2Responses); return messaging.sendEach(messages, false) - .then ((response: BatchResponse) => { + .then((response: BatchResponse) => { expect(http2Mocker.requests.length).to.equal(3); expect(response.successCount).to.equal(3); expect(response.failureCount).to.equal(0); @@ -1105,7 +1149,7 @@ describe('Messaging', () => { 'projects/projec_id/messages/2', 'projects/projec_id/messages/3', ]; - messageIds.forEach(id => mockedRequests.push(mockSendRequest(id))) + messageIds.forEach(id => mockedRequests.push(mockSendRequest(id))); return legacyMessaging.sendEachForMulticast({ tokens: ['a', 'b', 'c'], android: { ttl: 100 }, @@ -1130,7 +1174,7 @@ describe('Messaging', () => { 'projects/projec_id/messages/2', 'projects/projec_id/messages/3', ]; - messageIds.forEach(id => mockedRequests.push(mockSendRequest(id))) + messageIds.forEach(id => mockedRequests.push(mockSendRequest(id))); return legacyMessaging.sendEachForMulticast({ tokens: ['a', 'b', 'c'], android: { ttl: 100 }, @@ -1161,8 +1205,8 @@ describe('Messaging', () => { }, }, ]; - messageIds.forEach(id => mockedRequests.push(mockSendRequest(id))) - errors.forEach(err => mockedRequests.push(mockSendError(400, 'json', err))) + messageIds.forEach(id => mockedRequests.push(mockSendRequest(id))); + errors.forEach(err => mockedRequests.push(mockSendError(400, 'json', err))); return legacyMessaging.sendEachForMulticast({ tokens: ['a', 'b', 'c'] }) .then((response: BatchResponse) => { expect(response.successCount).to.equal(2); @@ -1178,7 +1222,7 @@ describe('Messaging', () => { }); it('should be fulfilled with a BatchResponse for all failures given an app which ' + - 'returns null access tokens', () => { + 'returns null access tokens', () => { return nullAccessTokenMessaging.sendEachForMulticast( { tokens: ['a', 'a'] }, ).then((response: BatchResponse) => { @@ -1206,8 +1250,8 @@ describe('Messaging', () => { }, }, ]; - messageIds.forEach(id => mockedRequests.push(mockSendRequest(id))) - errors.forEach(err => mockedRequests.push(mockSendError(400, 'json', err))) + messageIds.forEach(id => mockedRequests.push(mockSendRequest(id))); + errors.forEach(err => mockedRequests.push(mockSendError(400, 'json', err))); return legacyMessaging.sendEachForMulticast({ tokens: ['a', 'b'] }) .then((response: BatchResponse) => { expect(response.successCount).to.equal(1); @@ -1248,8 +1292,8 @@ describe('Messaging', () => { 'projects/projec_id/messages/2', 'projects/projec_id/messages/3', ]; - messageIds.forEach(id => mockedHttp2Responses.push(mockHttp2SendRequestResponse(id))) - http2Mocker.http2Stub(mockedHttp2Responses) + messageIds.forEach(id => mockedHttp2Responses.push(mockHttp2SendRequestResponse(id))); + http2Mocker.http2Stub(mockedHttp2Responses); return messaging.sendEachForMulticast({ tokens: ['a', 'b', 'c'], android: { ttl: 100 }, @@ -1274,8 +1318,8 @@ describe('Messaging', () => { 'projects/projec_id/messages/2', 'projects/projec_id/messages/3', ]; - messageIds.forEach(id => mockedHttp2Responses.push(mockHttp2SendRequestResponse(id))) - http2Mocker.http2Stub(mockedHttp2Responses) + messageIds.forEach(id => mockedHttp2Responses.push(mockHttp2SendRequestResponse(id))); + http2Mocker.http2Stub(mockedHttp2Responses); return messaging.sendEachForMulticast({ tokens: ['a', 'b', 'c'], android: { ttl: 100 }, @@ -1306,9 +1350,9 @@ describe('Messaging', () => { }, }, ]; - messageIds.forEach(id => mockedHttp2Responses.push(mockHttp2SendRequestResponse(id))) - errors.forEach(error => mockedHttp2Responses.push(mockHttp2SendRequestError(400, 'json', error))) - http2Mocker.http2Stub(mockedHttp2Responses) + messageIds.forEach(id => mockedHttp2Responses.push(mockHttp2SendRequestResponse(id))); + errors.forEach(error => mockedHttp2Responses.push(mockHttp2SendRequestError(400, 'json', error))); + http2Mocker.http2Stub(mockedHttp2Responses); return messaging.sendEachForMulticast({ tokens: ['a', 'b', 'c'] }, false) .then((response: BatchResponse) => { expect(response.successCount).to.equal(2); @@ -1324,7 +1368,7 @@ describe('Messaging', () => { }); it('should be fulfilled with a BatchResponse for all failures given an app which ' + - 'returns null access tokens using HTTP/2', () => { + 'returns null access tokens using HTTP/2', () => { return nullAccessTokenMessaging.sendEachForMulticast( { tokens: ['a', 'a'] }, false ).then((response: BatchResponse) => { @@ -1352,9 +1396,9 @@ describe('Messaging', () => { }, }, ]; - messageIds.forEach(id => mockedHttp2Responses.push(mockHttp2SendRequestResponse(id))) - errors.forEach(error => mockedHttp2Responses.push(mockHttp2SendRequestError(400, 'json', error))) - http2Mocker.http2Stub(mockedHttp2Responses) + messageIds.forEach(id => mockedHttp2Responses.push(mockHttp2SendRequestResponse(id))); + errors.forEach(error => mockedHttp2Responses.push(mockHttp2SendRequestError(400, 'json', error))); + http2Mocker.http2Stub(mockedHttp2Responses); return messaging.sendEachForMulticast({ tokens: ['a', 'b'] }, false) .then((response: BatchResponse) => { expect(response.successCount).to.equal(1); @@ -1378,7 +1422,7 @@ describe('Messaging', () => { mockedHttp2Responses.push(mockHttp2SendRequestError(404, 'json', error)); mockedHttp2Responses.push(mockHttp2SendRequestError(400, 'json', { error: 'test error message2' })); mockedHttp2Responses.push(mockHttp2SendRequestError(400, 'text', 'foo bar')); - http2Mocker.http2Stub(mockedHttp2Responses) + http2Mocker.http2Stub(mockedHttp2Responses); return messaging.sendEachForMulticast( { tokens: ['a', 'a', 'a'] }, false ).then((response: BatchResponse) => { @@ -1409,7 +1453,7 @@ describe('Messaging', () => { const invalidTtls = ['', 'abc', '123', '-123s', '1.2.3s', 'As', 's', '1s', -1]; invalidTtls.forEach((ttl) => { - it(`should throw given an invalid ttl: ${ ttl }`, () => { + it(`should throw given an invalid ttl: ${ttl}`, () => { const message: Message = { condition: 'topic-name', android: { @@ -1424,7 +1468,7 @@ describe('Messaging', () => { const invalidColors = ['', 'foo', '123', '#AABBCX', '112233', '#11223']; invalidColors.forEach((color) => { - it(`should throw given an invalid color: ${ color }`, () => { + it(`should throw given an invalid color: ${color}`, () => { const message: Message = { condition: 'topic-name', android: { @@ -1440,7 +1484,7 @@ describe('Messaging', () => { }); invalidImages.forEach((imageUrl) => { - it(`should throw given an invalid imageUrl: ${ imageUrl }`, () => { + it(`should throw given an invalid imageUrl: ${imageUrl}`, () => { const message: Message = { condition: 'topic-name', android: { @@ -1486,7 +1530,7 @@ describe('Messaging', () => { const invalidVibrateTimings = [[null, 500], [-100]]; invalidVibrateTimings.forEach((vibrateTimingsMillisMaybeNull) => { const vibrateTimingsMillis = vibrateTimingsMillisMaybeNull as number[]; - it(`should throw given an null or negative vibrateTimingsMillis: ${ vibrateTimingsMillis }`, () => { + it(`should throw given an null or negative vibrateTimingsMillis: ${vibrateTimingsMillis}`, () => { const message: Message = { condition: 'topic-name', android: { @@ -1516,7 +1560,7 @@ describe('Messaging', () => { }); invalidColors.forEach((color) => { - it(`should throw given an invalid color: ${ color }`, () => { + it(`should throw given an invalid color: ${color}`, () => { const message: Message = { condition: 'topic-name', android: { @@ -1725,14 +1769,14 @@ describe('Messaging', () => { }); }); - const invalidApnsLiveActivityTokens: any[] = [null, NaN, 0, 1, true, false] + const invalidApnsLiveActivityTokens: any[] = [null, NaN, 0, 1, true, false]; invalidApnsLiveActivityTokens.forEach((arg) => { it(`should throw given invalid apns live activity token: ${JSON.stringify(arg)}`, () => { expect(() => { messaging.send({ apns: { liveActivityToken: arg }, topic: 'test' }); }).to.throw('apns.liveActivityToken must be a string value'); }); - }) + }); it('should throw given empty apns live activity token', () => { expect(() => { @@ -2416,7 +2460,7 @@ describe('Messaging', () => { req: { apns: { liveActivityToken: 'live-activity-token', - headers:{ + headers: { 'apns-priority': '10' }, payload: { @@ -2441,7 +2485,7 @@ describe('Messaging', () => { expectedReq: { apns: { live_activity_token: 'live-activity-token', - headers:{ + headers: { 'apns-priority': '10' }, payload: { @@ -2469,7 +2513,7 @@ describe('Messaging', () => { req: { apns: { liveActivityToken: 'live-activity-token', - headers:{ + headers: { 'apns-priority': '10' }, payload: { @@ -2491,7 +2535,7 @@ describe('Messaging', () => { expectedReq: { apns: { live_activity_token: 'live-activity-token', - headers:{ + headers: { 'apns-priority': '10' }, payload: { @@ -2516,7 +2560,7 @@ describe('Messaging', () => { req: { apns: { liveActivityToken: 'live-activity-token', - 'headers':{ + 'headers': { 'apns-priority': '10' }, payload: { @@ -2539,7 +2583,7 @@ describe('Messaging', () => { expectedReq: { apns: { live_activity_token: 'live-activity-token', - 'headers':{ + 'headers': { 'apns-priority': '10' }, payload: { @@ -2666,7 +2710,7 @@ describe('Messaging', () => { const invalidTopics = [null, NaN, 0, 1, true, false, [], ['a', 1], {}, { a: 1 }, _.noop]; invalidTopics.forEach((invalidTopic) => { - it(`should throw given invalid type for topic argument: ${ JSON.stringify(invalidTopic) }`, () => { + it(`should throw given invalid type for topic argument: ${JSON.stringify(invalidTopic)}`, () => { expect(() => { messagingService[methodName](mocks.messaging.registrationToken, invalidTopic as string); }).to.throw(invalidTopicArgumentError); @@ -2687,7 +2731,7 @@ describe('Messaging', () => { const topicsWithInvalidCharacters = ['f*o*o', '/topics/f+o+o', 'foo/topics/foo', '$foo', '/topics/foo&']; topicsWithInvalidCharacters.forEach((invalidTopic) => { - it(`should be rejected given topic argument which has invalid characters: ${ invalidTopic }`, () => { + it(`should be rejected given topic argument which has invalid characters: ${invalidTopic}`, () => { return messagingService[methodName](mocks.messaging.registrationToken, invalidTopic) .should.eventually.be.rejected.and.have.property('code', 'messaging/invalid-argument'); }); @@ -2739,7 +2783,7 @@ describe('Messaging', () => { }); _.forEach(STATUS_CODE_TO_ERROR_MAP, (expectedError, statusCode) => { - it(`should be rejected given a ${ statusCode } text server response`, () => { + it(`should be rejected given a ${statusCode} text server response`, () => { mockedRequests.push(mockTopicSubscriptionRequestWithError(methodName, parseInt(statusCode, 10), 'text')); disableRetries(messaging); @@ -2772,7 +2816,7 @@ describe('Messaging', () => { }); it('should be fulfilled given a valid registration token and topic (topic name not prefixed ' + - 'with "/topics/")', () => { + 'with "/topics/")', () => { mockedRequests.push(mockTopicSubscriptionRequest(methodName, /* successCount */ 1)); return messagingService[methodName]( @@ -2782,7 +2826,7 @@ describe('Messaging', () => { }); it('should be fulfilled given a valid registration token and topic (topic name prefixed ' + - 'with "/topics/")', () => { + 'with "/topics/")', () => { mockedRequests.push(mockTopicSubscriptionRequest(methodName, /* successCount */ 1)); return messagingService[methodName]( @@ -2792,7 +2836,7 @@ describe('Messaging', () => { }); it('should be fulfilled given a valid array of registration tokens and topic (topic name not ' + - 'prefixed with "/topics/")', () => { + 'prefixed with "/topics/")', () => { mockedRequests.push(mockTopicSubscriptionRequest(methodName, /* successCount */ 3)); return messagingService[methodName]( @@ -2806,7 +2850,7 @@ describe('Messaging', () => { }); it('should be fulfilled given a valid array of registration tokens and topic (topic name ' + - 'prefixed with "/topics/")', () => { + 'prefixed with "/topics/")', () => { mockedRequests.push(mockTopicSubscriptionRequest(methodName, /* successCount */ 3)); return messagingService[methodName]( @@ -2820,7 +2864,7 @@ describe('Messaging', () => { }); it('should be fulfilled with the server response given a single registration token and topic ' + - '(topic name not prefixed with "/topics/")', () => { + '(topic name not prefixed with "/topics/")', () => { mockedRequests.push(mockTopicSubscriptionRequest(methodName, /* successCount */ 1)); return messagingService[methodName]( @@ -2834,7 +2878,7 @@ describe('Messaging', () => { }); it('should be fulfilled with the server response given a single registration token and topic ' + - '(topic name prefixed with "/topics/")', () => { + '(topic name prefixed with "/topics/")', () => { mockedRequests.push(mockTopicSubscriptionRequest(methodName, /* successCount */ 1)); return messagingService[methodName]( @@ -2848,7 +2892,7 @@ describe('Messaging', () => { }); it('should be fulfilled with the server response given an array of registration tokens ' + - 'and topic (topic name not prefixed with "/topics/")', () => { + 'and topic (topic name not prefixed with "/topics/")', () => { mockedRequests.push(mockTopicSubscriptionRequest(methodName, /* successCount */ 1, /* failureCount */ 2)); return messagingService[methodName]( @@ -2873,7 +2917,7 @@ describe('Messaging', () => { }); it('should be fulfilled with the server response given an array of registration tokens ' + - 'and topic (topic name prefixed with "/topics/")', () => { + 'and topic (topic name prefixed with "/topics/")', () => { mockedRequests.push(mockTopicSubscriptionRequest(methodName, /* successCount */ 1, /* failureCount */ 2)); return messagingService[methodName]( @@ -2898,7 +2942,7 @@ describe('Messaging', () => { }); it('should set the appropriate request data given a single registration token and topic ' + - '(topic name not prefixed with "/topics/")', () => { + '(topic name not prefixed with "/topics/")', () => { // Wait for the initial getToken() call to complete before stubbing https.request. return mockApp.INTERNAL.getToken() .then(() => { @@ -2919,7 +2963,7 @@ describe('Messaging', () => { }); it('should set the appropriate request data given a single registration token and topic ' + - '(topic name prefixed with "/topics/")', () => { + '(topic name prefixed with "/topics/")', () => { // Wait for the initial getToken() call to complete before stubbing https.request. return mockApp.INTERNAL.getToken() .then(() => { @@ -2940,7 +2984,7 @@ describe('Messaging', () => { }); it('should set the appropriate request data given an array of registration tokens and ' + - 'topic (topic name not prefixed with "/topics/")', () => { + 'topic (topic name not prefixed with "/topics/")', () => { const registrationTokens = [ mocks.messaging.registrationToken + '0', mocks.messaging.registrationToken + '1', @@ -2967,7 +3011,7 @@ describe('Messaging', () => { }); it('should set the appropriate request data given an array of registration tokens and ' + - 'topic (topic name prefixed with "/topics/")', () => { + 'topic (topic name prefixed with "/topics/")', () => { const registrationTokens = [ mocks.messaging.registrationToken + '0', mocks.messaging.registrationToken + '1', diff --git a/test/unit/phone-number-verification/phone-number-verification-api-client-internal.spec.ts b/test/unit/phone-number-verification/phone-number-verification-api-client-internal.spec.ts index 126e4c4978..021da9524b 100644 --- a/test/unit/phone-number-verification/phone-number-verification-api-client-internal.spec.ts +++ b/test/unit/phone-number-verification/phone-number-verification-api-client-internal.spec.ts @@ -19,11 +19,13 @@ import { expect } from 'chai'; import { - FirebasePhoneNumberVerificationError, JWKS_URL, - FPNV_TOKEN_INFO, - FPNV_ERROR_CODE_MAPPING + FPNV_TOKEN_INFO } from '../../../src/phone-number-verification/phone-number-verification-api-client-internal'; +import { + FirebasePhoneNumberVerificationError, + FPNV_ERROR_CODE_MAPPING +} from '../../../src/phone-number-verification/error'; import { PrefixedFirebaseError, FirebaseError } from '../../../src/utils/error'; const FPNV_PREFIX = 'phone-number-verification'; @@ -57,7 +59,7 @@ describe('FPNV Constants and Error Class', () => { const testMessage = 'The provided token is malformed or invalid.'; it('should correctly extend PrefixedFirebaseError', () => { - const error = new FirebasePhoneNumberVerificationError(testCode, testMessage); + const error = new FirebasePhoneNumberVerificationError({ code: testCode, message: testMessage }); expect(error).to.be.an.instanceOf(FirebasePhoneNumberVerificationError); expect(error).to.be.an.instanceOf(PrefixedFirebaseError); @@ -67,7 +69,7 @@ describe('FPNV Constants and Error Class', () => { it('should have the correct error properties on the instance', () => { - const error = new FirebasePhoneNumberVerificationError(testCode, testMessage); + const error = new FirebasePhoneNumberVerificationError({ code: testCode, message: testMessage }); expect(error.code).to.equal(`${FPNV_PREFIX}/${testCode}`); expect(error.message).to.equal(testMessage); @@ -77,7 +79,7 @@ describe('FPNV Constants and Error Class', () => { const codes = Object.values(FPNV_ERROR_CODE_MAPPING); codes.forEach(code => { - const error = new FirebasePhoneNumberVerificationError(code, `Test message for ${code}`); + const error = new FirebasePhoneNumberVerificationError({ code, message: `Test message for ${code}` }); expect(error.code).to.equal(`${FPNV_PREFIX}/${code}`); }); }); diff --git a/test/unit/phone-number-verification/token-verifier.spec.ts b/test/unit/phone-number-verification/token-verifier.spec.ts index 9cc68935e1..a7fa8d2ce7 100644 --- a/test/unit/phone-number-verification/token-verifier.spec.ts +++ b/test/unit/phone-number-verification/token-verifier.spec.ts @@ -27,8 +27,8 @@ import * as util from '../../../src/utils/index'; import { PhoneNumberTokenVerifier } from '../../../src/phone-number-verification/token-verifier'; import { FirebasePhoneNumberTokenInfo, - FPNV_ERROR_CODE_MAPPING } from '../../../src/phone-number-verification/phone-number-verification-api-client-internal'; +import { FPNV_ERROR_CODE_MAPPING } from '../../../src/phone-number-verification/error'; import * as mocks from '../../resources/mocks'; chai.use(chaiAsPromised); diff --git a/test/unit/project-management/android-app.spec.ts b/test/unit/project-management/android-app.spec.ts index 84807737cb..1b8898258d 100644 --- a/test/unit/project-management/android-app.spec.ts +++ b/test/unit/project-management/android-app.spec.ts @@ -24,7 +24,7 @@ import { ProjectManagementRequestHandler } from '../../../src/project-management/project-management-api-request-internal'; import { deepCopy } from '../../../src/utils/deep-copy'; -import { FirebaseProjectManagementError } from '../../../src/utils/error'; +import { FirebaseProjectManagementError } from '../../../src/project-management/error'; import * as mocks from '../../resources/mocks'; import { AndroidApp, AndroidAppMetadata, AppPlatform, ShaCertificate, @@ -33,7 +33,7 @@ import { const expect = chai.expect; const APP_ID = 'test-app-id'; -const EXPECTED_ERROR = new FirebaseProjectManagementError('internal-error', 'message'); +const EXPECTED_ERROR = new FirebaseProjectManagementError({ code: 'internal-error', message: 'message' }); const VALID_SHA_1_HASH = '0123456789abcdefABCDEF012345678901234567'; const VALID_SHA_256_HASH = '0123456789abcdefABCDEF01234567890123456701234567890123456789abcd'; diff --git a/test/unit/project-management/ios-app.spec.ts b/test/unit/project-management/ios-app.spec.ts index 58865dd821..09e214a52b 100644 --- a/test/unit/project-management/ios-app.spec.ts +++ b/test/unit/project-management/ios-app.spec.ts @@ -24,14 +24,14 @@ import { ProjectManagementRequestHandler } from '../../../src/project-management/project-management-api-request-internal'; import { deepCopy } from '../../../src/utils/deep-copy'; -import { FirebaseProjectManagementError } from '../../../src/utils/error'; +import { FirebaseProjectManagementError } from '../../../src/project-management/error'; import * as mocks from '../../resources/mocks'; import { AppPlatform, IosApp, IosAppMetadata } from '../../../src/project-management/index'; const expect = chai.expect; const APP_ID = 'test-app-id'; -const EXPECTED_ERROR = new FirebaseProjectManagementError('internal-error', 'message'); +const EXPECTED_ERROR = new FirebaseProjectManagementError({ code: 'internal-error', message: 'message' }); describe('IosApp', () => { // Stubs used to simulate underlying api calls. @@ -79,7 +79,7 @@ describe('IosApp', () => { }); describe('getMetadata', () => { - const expectedError = new FirebaseProjectManagementError('internal-error', 'message'); + const expectedError = new FirebaseProjectManagementError({ code: 'internal-error', message: 'message' }); const VALID_IOS_APP_METADATA_API_RESPONSE = { name: 'test-resource-name', diff --git a/test/unit/project-management/project-management.spec.ts b/test/unit/project-management/project-management.spec.ts index c0acea8027..25d1283f9b 100644 --- a/test/unit/project-management/project-management.spec.ts +++ b/test/unit/project-management/project-management.spec.ts @@ -23,7 +23,7 @@ import { FirebaseApp } from '../../../src/app/firebase-app'; import { ProjectManagementRequestHandler } from '../../../src/project-management/project-management-api-request-internal'; -import { FirebaseProjectManagementError } from '../../../src/utils/error'; +import { FirebaseProjectManagementError } from '../../../src/project-management/error'; import * as mocks from '../../resources/mocks'; import { AndroidApp, AppMetadata, AppPlatform, IosApp, ProjectManagement, @@ -38,7 +38,7 @@ const PACKAGE_NAME = 'test-package-name'; const BUNDLE_ID = 'test-bundle-id'; const DISPLAY_NAME_ANDROID = 'test-display-name-android'; const DISPLAY_NAME_IOS = 'test-display-name-ios'; -const EXPECTED_ERROR = new FirebaseProjectManagementError('internal-error', 'message'); +const EXPECTED_ERROR = new FirebaseProjectManagementError({ code: 'internal-error', message: 'message' }); const RESOURCE_NAME = 'projects/test/resources-name'; const RESOURCE_NAME_ANDROID = 'projects/test/resources-name:android'; const RESOURCE_NAME_IOS = 'projects/test/resources-name:ios'; diff --git a/test/unit/remote-config/remote-config-api-client.spec.ts b/test/unit/remote-config/remote-config-api-client.spec.ts index b5aa644d43..f515a76130 100644 --- a/test/unit/remote-config/remote-config-api-client.spec.ts +++ b/test/unit/remote-config/remote-config-api-client.spec.ts @@ -19,14 +19,13 @@ import * as _ from 'lodash'; import * as chai from 'chai'; import * as sinon from 'sinon'; -import { - FirebaseRemoteConfigError, - RemoteConfigApiClient -} from '../../../src/remote-config/remote-config-api-client-internal'; +import { RemoteConfigApiClient } from '../../../src/remote-config/remote-config-api-client-internal'; +import { FirebaseRemoteConfigError } from '../../../src/remote-config/error'; import { HttpClient } from '../../../src/utils/api-request'; import * as utils from '../utils'; import * as mocks from '../../resources/mocks'; -import { FirebaseAppError } from '../../../src/utils/error'; +import { toHttpResponse } from '../../../src/utils/error'; +import { FirebaseAppError } from '../../../src/app/error'; import { FirebaseApp } from '../../../src/app/firebase-app'; import { deepCopy } from '../../../src/utils/deep-copy'; import { getMetricsHeader, getSdkVersion } from '../../../src/utils/index'; @@ -333,19 +332,27 @@ describe('RemoteConfigApiClient', () => { VALIDATION_ERROR_MESSAGES.forEach((message) => { it('should reject with failed-precondition when a validation error occurres', () => { + const mockErr = utils.errorFrom({ + error: { + code: 400, + message: message, + status: 'FAILED_PRECONDITION' + } + }, 400); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom({ - error: { - code: 400, - message: message, - status: 'FAILED_PRECONDITION' - } - }, 400)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseRemoteConfigError('failed-precondition', message); + const expected = new FirebaseRemoteConfigError({ + code: 'failed-precondition', + message, + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.validateTemplate(REMOTE_CONFIG_TEMPLATE) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); }); }); @@ -428,19 +435,27 @@ describe('RemoteConfigApiClient', () => { VALIDATION_ERROR_MESSAGES.forEach((message) => { it('should reject with failed-precondition when a validation error occurs', () => { + const mockErr = utils.errorFrom({ + error: { + code: 400, + message: message, + status: 'FAILED_PRECONDITION' + } + }, 400); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom({ - error: { - code: 400, - message: message, - status: 'FAILED_PRECONDITION' - } - }, 400)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseRemoteConfigError('failed-precondition', message); + const expected = new FirebaseRemoteConfigError({ + code: 'failed-precondition', + message, + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.publishTemplate(REMOTE_CONFIG_TEMPLATE) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); }); }); @@ -715,8 +730,10 @@ describe('RemoteConfigApiClient', () => { .stub(HttpClient.prototype, 'send') .resolves(utils.responseFrom(TEST_RESPONSE)); stubs.push(stub); - const expected = new FirebaseRemoteConfigError('invalid-argument', - 'ETag header is not present in the server response.'); + const expected = new FirebaseRemoteConfigError({ + code: 'invalid-argument', + message: 'ETag header is not present in the server response.' + }); return rcOperation() .should.eventually.be.rejected.and.deep.include(expected); }); @@ -725,38 +742,61 @@ describe('RemoteConfigApiClient', () => { function runErrorResponseTests( rcOperation: () => Promise): void { it('should reject when a full platform error response is received', () => { + const mockErr = utils.errorFrom(ERROR_RESPONSE, 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom(ERROR_RESPONSE, 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseRemoteConfigError('not-found', 'Requested entity not found'); + const expected = new FirebaseRemoteConfigError({ + code: 'not-found', + message: 'Requested entity not found', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return rcOperation() - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should reject with unknown-error when error code is not present', () => { + const mockErr = utils.errorFrom({}, 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom({}, 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseRemoteConfigError('unknown-error', 'Unknown server error: {}'); + const expected = new FirebaseRemoteConfigError({ + code: 'unknown-error', + message: 'Unknown server error: {}', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return rcOperation() - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should reject with unknown-error for non-json response', () => { + const mockErr = utils.errorFrom('not json', 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom('not json', 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseRemoteConfigError( - 'unknown-error', 'Unexpected response with status: 404 and body: not json'); + const expected = new FirebaseRemoteConfigError({ + code: 'unknown-error', + message: 'Unexpected response with status: 404 and body: not json', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return rcOperation() - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should reject when rejected with a FirebaseAppError', () => { - const expected = new FirebaseAppError('network-error', 'socket hang up'); + const expected = new FirebaseAppError({ code: 'network-error', message: 'socket hang up' }); const stub = sinon .stub(HttpClient.prototype, 'send') .rejects(expected); diff --git a/test/unit/remote-config/remote-config.spec.ts b/test/unit/remote-config/remote-config.spec.ts index 737bb868fa..ffd65f79de 100644 --- a/test/unit/remote-config/remote-config.spec.ts +++ b/test/unit/remote-config/remote-config.spec.ts @@ -33,10 +33,8 @@ import { } from '../../../src/remote-config/index'; import { FirebaseApp } from '../../../src/app/firebase-app'; import * as mocks from '../../resources/mocks'; -import { - FirebaseRemoteConfigError, - RemoteConfigApiClient -} from '../../../src/remote-config/remote-config-api-client-internal'; +import { RemoteConfigApiClient } from '../../../src/remote-config/remote-config-api-client-internal'; +import { FirebaseRemoteConfigError } from '../../../src/remote-config/error'; import { deepCopy } from '../../../src/utils/deep-copy'; import { NamedCondition, ServerTemplate, ServerTemplateData, Version @@ -46,7 +44,7 @@ const expect = chai.expect; describe('RemoteConfig', () => { - const INTERNAL_ERROR = new FirebaseRemoteConfigError('internal-error', 'message'); + const INTERNAL_ERROR = new FirebaseRemoteConfigError({ code: 'internal-error', message: 'message' }); const PARAMETER_GROUPS = { new_menu: { description: 'Description of the group.', diff --git a/test/unit/security-rules/security-rules-api-client.spec.ts b/test/unit/security-rules/security-rules-api-client.spec.ts index c3599af8d0..d0dcf69b09 100644 --- a/test/unit/security-rules/security-rules-api-client.spec.ts +++ b/test/unit/security-rules/security-rules-api-client.spec.ts @@ -20,11 +20,12 @@ import * as _ from 'lodash'; import * as chai from 'chai'; import * as sinon from 'sinon'; import { SecurityRulesApiClient, RulesetContent } from '../../../src/security-rules/security-rules-api-client-internal'; -import { FirebaseSecurityRulesError } from '../../../src/security-rules/security-rules-internal'; +import { FirebaseSecurityRulesError } from '../../../src/security-rules/error'; import { HttpClient } from '../../../src/utils/api-request'; import * as utils from '../utils'; import * as mocks from '../../resources/mocks'; -import { FirebaseAppError } from '../../../src/utils/error'; +import { toHttpResponse } from '../../../src/utils/error'; +import { FirebaseAppError } from '../../../src/app/error'; import { FirebaseApp } from '../../../src/app/firebase-app'; import { getSdkVersion, getMetricsHeader } from '../../../src/utils/index'; @@ -119,38 +120,61 @@ describe('SecurityRulesApiClient', () => { }); it('should throw when a full platform error response is received', () => { + const mockErr = utils.errorFrom(ERROR_RESPONSE, 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom(ERROR_RESPONSE, 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseSecurityRulesError('not-found', 'Requested entity not found'); + const expected = new FirebaseSecurityRulesError({ + code: 'not-found', + message: 'Requested entity not found', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.getRuleset(RULESET_NAME) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should throw unknown-error when error code is not present', () => { + const mockErr = utils.errorFrom({}, 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom({}, 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseSecurityRulesError('unknown-error', 'Unknown server error: {}'); + const expected = new FirebaseSecurityRulesError({ + code: 'unknown-error', + message: 'Unknown server error: {}', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.getRuleset(RULESET_NAME) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should throw unknown-error for non-json response', () => { + const mockErr = utils.errorFrom('not json', 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom('not json', 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseSecurityRulesError( - 'unknown-error', 'Unexpected response with status: 404 and body: not json'); + const expected = new FirebaseSecurityRulesError({ + code: 'unknown-error', + message: 'Unexpected response with status: 404 and body: not json', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.getRuleset(RULESET_NAME) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should throw when rejected with a FirebaseAppError', () => { - const expected = new FirebaseAppError('network-error', 'socket hang up'); + const expected = new FirebaseAppError({ code: 'network-error', message: 'socket hang up' }); const stub = sinon .stub(HttpClient.prototype, 'send') .rejects(expected); @@ -233,13 +257,21 @@ describe('SecurityRulesApiClient', () => { }); it('should throw when a full platform error response is received', () => { + const mockErr = utils.errorFrom(ERROR_RESPONSE, 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom(ERROR_RESPONSE, 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseSecurityRulesError('not-found', 'Requested entity not found'); + const expected = new FirebaseSecurityRulesError({ + code: 'not-found', + message: 'Requested entity not found', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.createRuleset(RULES_CONTENT) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should throw when the rulesets limit reached', () => { @@ -250,38 +282,61 @@ describe('SecurityRulesApiClient', () => { status: 'RESOURCE_EXHAUSTED', }, }; + const mockErr = utils.errorFrom(resourceExhaustedError, 429); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom(resourceExhaustedError, 429)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseSecurityRulesError('resource-exhausted', resourceExhaustedError.error.message); + const expected = new FirebaseSecurityRulesError({ + code: 'resource-exhausted', + message: resourceExhaustedError.error.message, + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.createRuleset(RULES_CONTENT) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should throw unknown-error when error code is not present', () => { + const mockErr = utils.errorFrom({}, 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom({}, 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseSecurityRulesError('unknown-error', 'Unknown server error: {}'); + const expected = new FirebaseSecurityRulesError({ + code: 'unknown-error', + message: 'Unknown server error: {}', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.createRuleset(RULES_CONTENT) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should throw unknown-error for non-json response', () => { + const mockErr = utils.errorFrom('not json', 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom('not json', 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseSecurityRulesError( - 'unknown-error', 'Unexpected response with status: 404 and body: not json'); + const expected = new FirebaseSecurityRulesError({ + code: 'unknown-error', + message: 'Unexpected response with status: 404 and body: not json', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.createRuleset(RULES_CONTENT) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should throw when rejected with a FirebaseAppError', () => { - const expected = new FirebaseAppError('network-error', 'socket hang up'); + const expected = new FirebaseAppError({ code: 'network-error', message: 'socket hang up' }); const stub = sinon .stub(HttpClient.prototype, 'send') .rejects(expected); @@ -386,38 +441,61 @@ describe('SecurityRulesApiClient', () => { }); it('should throw when a full platform error response is received', () => { + const mockErr = utils.errorFrom(ERROR_RESPONSE, 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom(ERROR_RESPONSE, 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseSecurityRulesError('not-found', 'Requested entity not found'); + const expected = new FirebaseSecurityRulesError({ + code: 'not-found', + message: 'Requested entity not found', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.listRulesets() - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should throw unknown-error when error code is not present', () => { + const mockErr = utils.errorFrom({}, 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom({}, 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseSecurityRulesError('unknown-error', 'Unknown server error: {}'); + const expected = new FirebaseSecurityRulesError({ + code: 'unknown-error', + message: 'Unknown server error: {}', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.listRulesets() - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should throw unknown-error for non-json response', () => { + const mockErr = utils.errorFrom('not json', 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom('not json', 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseSecurityRulesError( - 'unknown-error', 'Unexpected response with status: 404 and body: not json'); + const expected = new FirebaseSecurityRulesError({ + code: 'unknown-error', + message: 'Unexpected response with status: 404 and body: not json', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.listRulesets() - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should throw when rejected with a FirebaseAppError', () => { - const expected = new FirebaseAppError('network-error', 'socket hang up'); + const expected = new FirebaseAppError({ code: 'network-error', message: 'socket hang up' }); const stub = sinon .stub(HttpClient.prototype, 'send') .rejects(expected); @@ -450,38 +528,61 @@ describe('SecurityRulesApiClient', () => { }); it('should throw when a full platform error response is received', () => { + const mockErr = utils.errorFrom(ERROR_RESPONSE, 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom(ERROR_RESPONSE, 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseSecurityRulesError('not-found', 'Requested entity not found'); + const expected = new FirebaseSecurityRulesError({ + code: 'not-found', + message: 'Requested entity not found', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.getRelease(RELEASE_NAME) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should throw unknown-error when error code is not present', () => { + const mockErr = utils.errorFrom({}, 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom({}, 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseSecurityRulesError('unknown-error', 'Unknown server error: {}'); + const expected = new FirebaseSecurityRulesError({ + code: 'unknown-error', + message: 'Unknown server error: {}', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.getRelease(RELEASE_NAME) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should throw unknown-error for non-json response', () => { + const mockErr = utils.errorFrom('not json', 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom('not json', 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseSecurityRulesError( - 'unknown-error', 'Unexpected response with status: 404 and body: not json'); + const expected = new FirebaseSecurityRulesError({ + code: 'unknown-error', + message: 'Unexpected response with status: 404 and body: not json', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.getRelease(RELEASE_NAME) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should throw when rejected with a FirebaseAppError', () => { - const expected = new FirebaseAppError('network-error', 'socket hang up'); + const expected = new FirebaseAppError({ code: 'network-error', message: 'socket hang up' }); const stub = sinon .stub(HttpClient.prototype, 'send') .rejects(expected); @@ -493,7 +594,7 @@ describe('SecurityRulesApiClient', () => { describe('updateOrCreateRelease', () => { it('should propagate API errors', () => { - const EXPECTED_ERROR = new FirebaseSecurityRulesError('internal-error', 'message'); + const EXPECTED_ERROR = new FirebaseSecurityRulesError({ code: 'internal-error', message: 'message' }); const stub = sinon .stub(SecurityRulesApiClient.prototype, 'updateRelease') .rejects(EXPECTED_ERROR); @@ -503,7 +604,7 @@ describe('SecurityRulesApiClient', () => { }); it('should create a new ruleset when update fails with a not-found error', () => { - const NOT_FOUND_ERROR = new FirebaseSecurityRulesError('not-found', 'message'); + const NOT_FOUND_ERROR = new FirebaseSecurityRulesError({ code: 'not-found', message: 'message' }); const updateRelease = sinon .stub(SecurityRulesApiClient.prototype, 'updateRelease') .rejects(NOT_FOUND_ERROR); @@ -549,38 +650,61 @@ describe('SecurityRulesApiClient', () => { }); it('should throw when a full platform error response is received', () => { + const mockErr = utils.errorFrom(ERROR_RESPONSE, 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom(ERROR_RESPONSE, 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseSecurityRulesError('not-found', 'Requested entity not found'); + const expected = new FirebaseSecurityRulesError({ + code: 'not-found', + message: 'Requested entity not found', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.updateRelease(RELEASE_NAME, RULESET_NAME) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should throw unknown-error when error code is not present', () => { + const mockErr = utils.errorFrom({}, 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom({}, 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseSecurityRulesError('unknown-error', 'Unknown server error: {}'); + const expected = new FirebaseSecurityRulesError({ + code: 'unknown-error', + message: 'Unknown server error: {}', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.updateRelease(RELEASE_NAME, RULESET_NAME) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should throw unknown-error for non-json response', () => { + const mockErr = utils.errorFrom('not json', 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom('not json', 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseSecurityRulesError( - 'unknown-error', 'Unexpected response with status: 404 and body: not json'); + const expected = new FirebaseSecurityRulesError({ + code: 'unknown-error', + message: 'Unexpected response with status: 404 and body: not json', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.updateRelease(RELEASE_NAME, RULESET_NAME) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should throw when rejected with a FirebaseAppError', () => { - const expected = new FirebaseAppError('network-error', 'socket hang up'); + const expected = new FirebaseAppError({ code: 'network-error', message: 'socket hang up' }); const stub = sinon .stub(HttpClient.prototype, 'send') .rejects(expected); @@ -617,38 +741,61 @@ describe('SecurityRulesApiClient', () => { }); it('should throw when a full platform error response is received', () => { + const mockErr = utils.errorFrom(ERROR_RESPONSE, 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom(ERROR_RESPONSE, 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseSecurityRulesError('not-found', 'Requested entity not found'); + const expected = new FirebaseSecurityRulesError({ + code: 'not-found', + message: 'Requested entity not found', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.createRelease(RELEASE_NAME, RULESET_NAME) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should throw unknown-error when error code is not present', () => { + const mockErr = utils.errorFrom({}, 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom({}, 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseSecurityRulesError('unknown-error', 'Unknown server error: {}'); + const expected = new FirebaseSecurityRulesError({ + code: 'unknown-error', + message: 'Unknown server error: {}', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.createRelease(RELEASE_NAME, RULESET_NAME) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should throw unknown-error for non-json response', () => { + const mockErr = utils.errorFrom('not json', 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom('not json', 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseSecurityRulesError( - 'unknown-error', 'Unexpected response with status: 404 and body: not json'); + const expected = new FirebaseSecurityRulesError({ + code: 'unknown-error', + message: 'Unexpected response with status: 404 and body: not json', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.createRelease(RELEASE_NAME, RULESET_NAME) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should throw when rejected with a FirebaseAppError', () => { - const expected = new FirebaseAppError('network-error', 'socket hang up'); + const expected = new FirebaseAppError({ code: 'network-error', message: 'socket hang up' }); const stub = sinon .stub(HttpClient.prototype, 'send') .rejects(expected); @@ -695,44 +842,57 @@ describe('SecurityRulesApiClient', () => { }); it('should throw when a full platform error response is received', () => { + const mockErr = utils.errorFrom(ERROR_RESPONSE, 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom(ERROR_RESPONSE, 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseSecurityRulesError('not-found', 'Requested entity not found'); + const expected = new FirebaseSecurityRulesError({ + code: 'not-found', + message: 'Requested entity not found', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.deleteRuleset(RULESET_NAME) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should throw unknown-error when error code is not present', () => { + const mockErr = utils.errorFrom({}, 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom({}, 404)); + .rejects(mockErr); stubs.push(stub); - const expected = new FirebaseSecurityRulesError('unknown-error', 'Unknown server error: {}'); + const expected = new FirebaseSecurityRulesError({ + code: 'unknown-error', + message: 'Unknown server error: {}', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.deleteRuleset(RULESET_NAME) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); it('should throw unknown-error for non-json response', () => { + const mockErr = utils.errorFrom('not json', 404); const stub = sinon .stub(HttpClient.prototype, 'send') - .rejects(utils.errorFrom('not json', 404)); - stubs.push(stub); - const expected = new FirebaseSecurityRulesError( - 'unknown-error', 'Unexpected response with status: 404 and body: not json'); - return apiClient.deleteRuleset(RULESET_NAME) - .should.eventually.be.rejected.and.deep.include(expected); - }); - - it('should throw when rejected with a FirebaseAppError', () => { - const expected = new FirebaseAppError('network-error', 'socket hang up'); - const stub = sinon - .stub(HttpClient.prototype, 'send') - .rejects(expected); + .rejects(mockErr); stubs.push(stub); + const expected = new FirebaseSecurityRulesError({ + code: 'unknown-error', + message: 'Unexpected response with status: 404 and body: not json', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); return apiClient.deleteRuleset(RULESET_NAME) - .should.eventually.be.rejected.and.deep.include(expected); + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); }); }); }); diff --git a/test/unit/security-rules/security-rules.spec.ts b/test/unit/security-rules/security-rules.spec.ts index e35b2d07b6..7b683ce96d 100644 --- a/test/unit/security-rules/security-rules.spec.ts +++ b/test/unit/security-rules/security-rules.spec.ts @@ -23,14 +23,14 @@ import { SecurityRules } from '../../../src/security-rules/index'; import { FirebaseApp } from '../../../src/app/firebase-app'; import * as mocks from '../../resources/mocks'; import { SecurityRulesApiClient, RulesetContent } from '../../../src/security-rules/security-rules-api-client-internal'; -import { FirebaseSecurityRulesError } from '../../../src/security-rules/security-rules-internal'; +import { FirebaseSecurityRulesError } from '../../../src/security-rules/error'; import { deepCopy } from '../../../src/utils/deep-copy'; const expect = chai.expect; describe('SecurityRules', () => { - const EXPECTED_ERROR = new FirebaseSecurityRulesError('internal-error', 'message'); + const EXPECTED_ERROR = new FirebaseSecurityRulesError({ code: 'internal-error', message: 'message' }); const FIRESTORE_RULESET_RESPONSE = { name: 'projects/test-project/rulesets/foo', createTime: '2019-03-08T23:45:23.288047Z', @@ -49,23 +49,25 @@ describe('SecurityRules', () => { }; const CREATE_TIME_UTC = 'Fri, 08 Mar 2019 23:45:23 GMT'; - const INVALID_RULESET_ERROR = new FirebaseSecurityRulesError( - 'invalid-argument', - 'ruleset must be a non-empty name or a RulesetMetadata object.', - ); + const INVALID_RULESET_ERROR = new FirebaseSecurityRulesError({ + code: 'invalid-argument', + message: 'ruleset must be a non-empty name or a RulesetMetadata object.' + }); const INVALID_RULESETS: any[] = [null, undefined, '', 1, true, {}, [], { name: '' }]; - const INVALID_BUCKET_ERROR = new FirebaseSecurityRulesError( - 'invalid-argument', - 'Bucket name not specified or invalid. Specify a default bucket name via the ' + + const INVALID_BUCKET_ERROR = new FirebaseSecurityRulesError({ + code: 'invalid-argument', + message: 'Bucket name not specified or invalid. Specify a default bucket name via the ' + 'storageBucket option when initializing the app, or specify the bucket name ' + - 'explicitly when calling the rules API.', - ); + 'explicitly when calling the rules API.' + }); const INVALID_BUCKET_NAMES: any[] = [null, '', true, false, 1, 0, {}, []]; const INVALID_SOURCES: any[] = [null, undefined, '', 1, true, {}, []]; - const INVALID_SOURCE_ERROR = new FirebaseSecurityRulesError( - 'invalid-argument', 'Source must be a non-empty string or a Buffer.'); + const INVALID_SOURCE_ERROR = new FirebaseSecurityRulesError({ + code: 'invalid-argument', + message: 'Source must be a non-empty string or a Buffer.' + }); let securityRules: SecurityRules; let mockApp: FirebaseApp; diff --git a/test/unit/utils/api-request.spec.ts b/test/unit/utils/api-request.spec.ts index 4636878f6b..887804545a 100644 --- a/test/unit/utils/api-request.spec.ts +++ b/test/unit/utils/api-request.spec.ts @@ -29,9 +29,10 @@ import * as mocks from '../../resources/mocks'; import { FirebaseApp } from '../../../src/app/firebase-app'; import { - ApiSettings, HttpClient, Http2Client, AuthorizedHttpClient, ApiCallbackFunction, HttpRequestConfig, + ApiSettings, HttpClient, Http2Client, AuthorizedHttpClient, HttpRequestConfig, parseHttpResponse, RetryConfig, defaultRetryConfig, Http2SessionHandler, Http2RequestConfig, RequestResponseError, RequestResponse, AuthorizedHttp2Client, + ApiRequestCallback, ApiResponseCallback, } from '../../../src/utils/api-request'; import { deepCopy } from '../../../src/utils/deep-copy'; import { Agent } from 'http'; @@ -274,7 +275,20 @@ describe('HttpClient', () => { expect(resp.status).to.equal(200); expect(resp.headers['content-type']).to.equal('text/plain'); expect(resp.text).to.equal(respData); - expect(() => { resp.data; }).to.throw('Error while parsing response data'); + + try { + resp.data; + expect.fail('Should have thrown'); + } catch (err: any) { + expect(err.code).to.equal('app/unable-to-parse-response'); + expect(err.message).to.equal('Error while parsing response data'); + expect(err.httpResponse).to.deep.equal({ + status: 200, + data: respData, + headers: { 'content-type': 'text/plain' } + }); + } + expect(resp.multipart).to.be.undefined; expect(resp.isJson()).to.be.false; }); @@ -2506,7 +2520,6 @@ describe('Http2Client', () => { it('should fail on session and stream errors', async () => { const reqData = { request: 'data' }; const streamError = 'Error while making request: test stream error. Error code: AWFUL_STREAM_ERROR'; - const sessionError = 'Session error while making requests: AWFUL_SESSION_ERROR - test session error' mockedHttp2Responses.push(mockHttp2Error( { message: 'test stream error', code: 'AWFUL_STREAM_ERROR' }, { message: 'test session error', code: 'AWFUL_SESSION_ERROR' } @@ -2535,15 +2548,17 @@ describe('Http2Client', () => { expect(http2Mocker.requests[0].headers.authorization).to.equal('Bearer token'); expect(http2Mocker.requests[0].headers['content-type']).to.contain('application/json'); expect(http2Mocker.requests[0].headers['My-Custom-Header']).to.equal('CustomValue'); + + const sessionErrors = http2SessionHandler.getErrors(); + expect(sessionErrors.length).to.equal(1); + const expectedError1 = 'Session error while making requests: AWFUL_SESSION_ERROR - test session error '; + expect(sessionErrors[0].message).to.equal(expectedError1); }); - - await http2SessionHandler.invoke().should.eventually.be.rejectedWith(sessionError) - .and.have.property('code', 'app/network-error') }); it('should unwrap aggregate session errors', async () => { const reqData = { request: 'data' }; - const streamError = { message: 'test stream error', code: 'AWFUL_STREAM_ERROR' } + const streamError = { message: 'test stream error', code: 'AWFUL_STREAM_ERROR' }; const expectedStreamErrorMessage = 'Error while making request: test stream error. Error code: AWFUL_STREAM_ERROR'; const aggregateSessionError = { name: 'AggregateError', @@ -2552,15 +2567,12 @@ describe('Http2Client', () => { { message: 'Error message 1' }, { message: 'Error message 2' }, ] - } - const expectedAggregateErrorMessage = 'Session error while making requests: AWFUL_SESSION_ERROR - ' + - 'AggregateError: [Error message 1, Error message 2]' - + }; mockedHttp2Responses.push(mockHttp2Error(streamError, aggregateSessionError)); http2Mocker.http2Stub(mockedHttp2Responses); const client = new Http2Client(); - http2SessionHandler = new Http2SessionHandler(mockHostUrl) + http2SessionHandler = new Http2SessionHandler(mockHostUrl); await client.send({ method: 'POST', @@ -2581,10 +2593,13 @@ describe('Http2Client', () => { expect(http2Mocker.requests[0].headers.authorization).to.equal('Bearer token'); expect(http2Mocker.requests[0].headers['content-type']).to.contain('application/json'); expect(http2Mocker.requests[0].headers['My-Custom-Header']).to.equal('CustomValue'); - }); - await http2SessionHandler.invoke().should.eventually.be.rejectedWith(expectedAggregateErrorMessage) - .and.have.property('code', 'app/network-error') + const sessionErrors = http2SessionHandler.getErrors(); + expect(sessionErrors.length).to.equal(1); + const expectedError2 = 'Session error while making requests: AWFUL_SESSION_ERROR - AggregateError: ' + + '[Error message 1, Error message 2]'; + expect(sessionErrors[0].message).to.equal(expectedError2); + }); }); }); @@ -2987,15 +3002,20 @@ describe('ApiSettings', () => { it('should not return null for responseValidator', () => { const validator = apiSettings.getResponseValidator(); expect(() => { - return validator({}); + return validator({ + // data: {}, + // status: 200, + // headers: {}, + // isJson: () => false, + } as RequestResponse); }).to.not.throw(); }); }); describe('with set properties', () => { const apiSettings: ApiSettings = new ApiSettings('getAccountInfo', 'GET'); // Set all apiSettings properties. - const requestValidator: ApiCallbackFunction = () => undefined; - const responseValidator: ApiCallbackFunction = () => undefined; + const requestValidator: ApiRequestCallback = () => undefined; + const responseValidator: ApiResponseCallback = () => undefined; apiSettings.setRequestValidator(requestValidator); apiSettings.setResponseValidator(responseValidator); it('should return the correct requestValidator', () => { diff --git a/test/unit/utils/error.spec.ts b/test/unit/utils/error.spec.ts index 3bc58c9173..72b65392e1 100644 --- a/test/unit/utils/error.spec.ts +++ b/test/unit/utils/error.spec.ts @@ -21,9 +21,12 @@ import * as chai from 'chai'; import * as sinonChai from 'sinon-chai'; import * as chaiAsPromised from 'chai-as-promised'; +import { FirebaseError } from '../../../src/utils/error'; +import { FirebaseAuthError } from '../../../src/auth/error'; import { - FirebaseError, FirebaseAuthError, FirebaseMessagingError, MessagingClientErrorCode, -} from '../../../src/utils/error'; + FirebaseMessagingError, + messagingClientErrorCode, +} from '../../../src/messaging/error'; chai.should(); chai.use(sinonChai); @@ -39,8 +42,25 @@ describe('FirebaseError', () => { const error = new FirebaseError(errorInfo); expect(error.code).to.be.equal(code); expect(error.message).to.be.equal(message); + expect(error instanceof FirebaseError).to.be.true; + expect(error instanceof Error).to.be.true; }); + it('should initialize correctly with httpResponse and cause', () => { + const errorInfoWithExtras = { + code, + message, + httpResponse: { status: 500, headers: {} }, + cause: new Error('low-level error') + }; + const error = new FirebaseError(errorInfoWithExtras); + expect(error.code).to.be.equal(code); + expect(error.message).to.be.equal(message); + expect(error.httpResponse?.status).to.be.equal(500); + expect(error.cause?.message).to.be.equal('low-level error'); + }); + + it('should throw if no error info is specified', () => { expect(() => { const firebaseErrorAny: any = FirebaseError; @@ -52,6 +72,33 @@ describe('FirebaseError', () => { const error = new FirebaseError(errorInfo); expect(error.toJSON()).to.deep.equal({ code, message }); }); + + it('toJSON() should not leak extra properties from RequestResponse', () => { + const mockHttpResponse = { + status: 200, + headers: { 'content-type': 'application/json' }, + data: { foo: 'bar' }, + text: '{"foo":"bar"}', // Extra property from RequestResponse + parsedData: { foo: 'bar' }, // Simulated private field + parseError: undefined, // Simulated private field + isJson: () => true, // Method from RequestResponse + }; + const error = new FirebaseError({ + code: 'code', + message: 'message', + httpResponse: mockHttpResponse as any + }); + + const json = error.toJSON() as any; + expect(json.httpResponse).to.deep.equal({ + status: 200, + headers: { 'content-type': 'application/json' }, + data: { foo: 'bar' } + }); + expect(json.httpResponse.text).to.be.undefined; + expect(json.httpResponse.parsedData).to.be.undefined; + expect(json.httpResponse.isJson).to.be.undefined; + }); }); describe('FirebaseAuthError', () => { @@ -63,6 +110,8 @@ describe('FirebaseAuthError', () => { const error = new FirebaseAuthError(errorCodeInfo); expect(error.code).to.be.equal('auth/code'); expect(error.message).to.be.equal('message'); + expect(error instanceof FirebaseAuthError).to.be.true; + expect(error instanceof FirebaseError).to.be.true; }); it('should initialize successfully with a message specified', () => { @@ -122,29 +171,82 @@ describe('FirebaseAuthError', () => { }); }); - describe('with raw server response specified', () => { - const mockRawServerResponse = { - error: { - code: 'UNEXPECTED_ERROR', - message: 'An unexpected error occurred.', + describe('with httpResponse specified', () => { + const mockHttpResponse = { + status: 400, + headers: {}, + data: { + error: { + code: 'UNEXPECTED_ERROR', + message: 'An unexpected error occurred.', + }, }, + isJson: () => true, }; + const mockError: any = { response: mockHttpResponse }; - it('should not include raw server response from an expected server code', () => { + it('should include httpResponse from an expected server code', () => { const error = FirebaseAuthError.fromServerError( - 'USER_NOT_FOUND', 'Invalid uid', mockRawServerResponse); + 'USER_NOT_FOUND', 'Invalid uid', mockError); expect(error.code).to.be.equal('auth/user-not-found'); expect(error.message).to.be.equal('Invalid uid'); + expect(error.httpResponse).to.deep.equal({ + status: mockHttpResponse.status, + headers: mockHttpResponse.headers, + data: mockHttpResponse.data, + }); }); - it('should include raw server response from an unexpected server code', () => { + it('should include httpResponse from an unexpected server code', () => { const error = FirebaseAuthError.fromServerError( - 'UNEXPECTED_ERROR', 'An unexpected error occurred.', mockRawServerResponse); + 'UNEXPECTED_ERROR', 'An unexpected error occurred.', mockError); expect(error.code).to.be.equal('auth/internal-error'); - expect(error.message).to.be.equal( - 'An unexpected error occurred. Raw server response: "' + - `${ JSON.stringify(mockRawServerResponse) }"`, - ); + expect(error.message).to.be.equal('An unexpected error occurred.'); + expect(error.httpResponse).to.deep.equal({ + status: mockHttpResponse.status, + headers: mockHttpResponse.headers, + data: mockHttpResponse.data, + }); + }); + + it('should include httpResponse from an expected server with server detailed message', () => { + const error = FirebaseAuthError.fromServerError( + 'CONFIGURATION_NOT_FOUND : more details', + 'Ignored message', mockError); + expect(error.code).to.be.equal('auth/configuration-not-found'); + expect(error.message).to.be.equal('more details'); + expect(error.httpResponse).to.deep.equal({ + status: mockHttpResponse.status, + headers: mockHttpResponse.headers, + data: mockHttpResponse.data, + }); + }); + + it('should not leak extra properties from RequestResponse in toJSON() via fromServerError', () => { + const mockRequestResponse = { + status: 400, + headers: {}, + data: { + error: { + code: 'UNEXPECTED_ERROR', + message: 'An unexpected error occurred.', + }, + }, + text: '{"error":...}', + isJson: () => true, + }; + const mockError: any = { response: mockRequestResponse }; + const error = FirebaseAuthError.fromServerError( + 'USER_NOT_FOUND', 'Invalid uid', mockError); + + const json = error.toJSON() as any; + expect(json.httpResponse).to.deep.equal({ + status: 400, + headers: {}, + data: mockRequestResponse.data + }); + expect(json.httpResponse.text).to.be.undefined; + expect(json.httpResponse.isJson).to.be.undefined; }); }); }); @@ -159,6 +261,8 @@ describe('FirebaseMessagingError', () => { const error = new FirebaseMessagingError(errorCodeInfo); expect(error.code).to.be.equal('messaging/code'); expect(error.message).to.be.equal('message'); + expect(error instanceof FirebaseMessagingError).to.be.true; + expect(error instanceof FirebaseError).to.be.true; }); it('should initialize successfully with a message specified', () => { @@ -175,14 +279,14 @@ describe('FirebaseMessagingError', () => { describe('without message specified', () => { it('should initialize an error from an expected server code', () => { const error = FirebaseMessagingError.fromServerError('InvalidRegistration'); - const expectedError = MessagingClientErrorCode.INVALID_REGISTRATION_TOKEN; + const expectedError = messagingClientErrorCode.INVALID_REGISTRATION_TOKEN; expect(error.code).to.equal('messaging/' + expectedError.code); expect(error.message).to.equal(expectedError.message); }); it('should initialize an error from an unexpected server code', () => { const error = FirebaseMessagingError.fromServerError('UNEXPECTED_ERROR'); - const expectedError = MessagingClientErrorCode.UNKNOWN_ERROR; + const expectedError = messagingClientErrorCode.UNKNOWN_ERROR; expect(error.code).to.equal('messaging/' + expectedError.code); expect(error.message).to.equal(expectedError.message); }); @@ -191,44 +295,50 @@ describe('FirebaseMessagingError', () => { describe('with message specified', () => { it('should initialize an error from an expected server code', () => { const error = FirebaseMessagingError.fromServerError('InvalidRegistration', 'Message override.'); - const expectedError = MessagingClientErrorCode.INVALID_REGISTRATION_TOKEN; + const expectedError = messagingClientErrorCode.INVALID_REGISTRATION_TOKEN; expect(error.code).to.equal('messaging/' + expectedError.code); expect(error.message).to.equal('Message override.'); }); it('should initialize an error from an unexpected server code', () => { const error = FirebaseMessagingError.fromServerError('UNEXPECTED_ERROR', 'Message override.'); - const expectedError = MessagingClientErrorCode.UNKNOWN_ERROR; + const expectedError = messagingClientErrorCode.UNKNOWN_ERROR; expect(error.code).to.equal('messaging/' + expectedError.code); expect(error.message).to.equal('Message override.'); }); }); - describe('with raw server response specified', () => { - const mockRawServerResponse = { - error: { - code: 'UNEXPECTED_ERROR', - message: 'Message override.', + describe('with server error specified', () => { + const mockHttpResponse = { + status: 400, + headers: {}, + data: { + error: { + code: 'UNEXPECTED_ERROR', + message: 'Message override.', + }, }, + isJson: () => true, }; + const mockError: any = { response: mockHttpResponse }; it('should not include raw server response from an expected server code', () => { const error = FirebaseMessagingError.fromServerError( - 'InvalidRegistration', /* message */ undefined, mockRawServerResponse, + 'InvalidRegistration', /* message */ undefined, mockError, ); - const expectedError = MessagingClientErrorCode.INVALID_REGISTRATION_TOKEN; + const expectedError = messagingClientErrorCode.INVALID_REGISTRATION_TOKEN; expect(error.code).to.equal('messaging/' + expectedError.code); expect(error.message).to.equal(expectedError.message); }); it('should include raw server response from an unexpected server code', () => { const error = FirebaseMessagingError.fromServerError( - 'UNEXPECTED_ERROR', /* message */ undefined, mockRawServerResponse, + 'UNEXPECTED_ERROR', /* message */ undefined, mockError, ); - const expectedError = MessagingClientErrorCode.UNKNOWN_ERROR; + const expectedError = messagingClientErrorCode.UNKNOWN_ERROR; expect(error.code).to.equal('messaging/' + expectedError.code); expect(error.message).to.be.equal( - `${ expectedError.message } Raw server response: "${ JSON.stringify(mockRawServerResponse) }"`, + `${expectedError.message} Raw server response: "${JSON.stringify(mockHttpResponse.data)}"`, ); }); });