From 93f10f40863d452cf5d8bb980dae9bd83360c928 Mon Sep 17 00:00:00 2001 From: Liubin Jiang Date: Wed, 30 Oct 2024 13:09:22 -0700 Subject: [PATCH 1/7] Added linkDomain support for ActionCodeSettings Added mobileLinksConfig support for updating project configs Added integration tests --- etc/firebase-admin.auth.api.md | 17 ++ src/auth/action-code-settings-builder.ts | 22 ++ src/auth/auth-config.ts | 54 ++++ src/auth/base-auth.ts | 6 +- src/auth/index.ts | 2 + src/auth/project-config.ts | 30 +++ src/utils/error.ts | 7 + test/integration/auth.spec.ts | 241 +++++++++++++++++- .../auth/action-code-settings-builder.spec.ts | 21 ++ test/unit/auth/auth-api-request.spec.ts | 1 + test/unit/auth/auth-config.spec.ts | 29 +++ test/unit/auth/auth.spec.ts | 1 + test/unit/auth/project-config.spec.ts | 61 ++++- 13 files changed, 486 insertions(+), 6 deletions(-) diff --git a/etc/firebase-admin.auth.api.md b/etc/firebase-admin.auth.api.md index 7962ca5baa..8713b1bda7 100644 --- a/etc/firebase-admin.auth.api.md +++ b/etc/firebase-admin.auth.api.md @@ -13,11 +13,13 @@ export interface ActionCodeSettings { installApp?: boolean; minimumVersion?: string; }; + // @deprecated dynamicLinkDomain?: string; handleCodeInApp?: boolean; iOS?: { bundleId: string; }; + linkDomain?: string; url: string; } @@ -221,6 +223,11 @@ export class AuthClientErrorCode { message: string; }; // (undocumented) + static INVALID_HOSTING_LINK_DOMAIN: { + code: string; + message: string; + }; + // (undocumented) static INVALID_ID_TOKEN: { code: string; message: string; @@ -757,6 +764,14 @@ export interface ListUsersResult { users: UserRecord[]; } +// @public +export interface MobileLinksConfig { + domain?: MobileLinksDomain; +} + +// @public +export type MobileLinksDomain = 'HOSTING_DOMAIN' | 'FIREBASE_DYNAMIC_LINK_DOMAIN'; + // @public export interface MultiFactorConfig { factorIds?: AuthFactorType[]; @@ -847,6 +862,7 @@ export class PhoneMultiFactorInfo extends MultiFactorInfo { // @public export class ProjectConfig { readonly emailPrivacyConfig?: EmailPrivacyConfig; + readonly mobileLinksConfig?: MobileLinksConfig; get multiFactorConfig(): MultiFactorConfig | undefined; readonly passwordPolicyConfig?: PasswordPolicyConfig; get recaptchaConfig(): RecaptchaConfig | undefined; @@ -996,6 +1012,7 @@ export interface UpdatePhoneMultiFactorInfoRequest extends BaseUpdateMultiFactor // @public export interface UpdateProjectConfigRequest { emailPrivacyConfig?: EmailPrivacyConfig; + mobileLinksConfig?: MobileLinksConfig; multiFactorConfig?: MultiFactorConfig; passwordPolicyConfig?: PasswordPolicyConfig; recaptchaConfig?: RecaptchaConfig; diff --git a/src/auth/action-code-settings-builder.ts b/src/auth/action-code-settings-builder.ts index 4ebc4df19c..f8ea0bcaf3 100644 --- a/src/auth/action-code-settings-builder.ts +++ b/src/auth/action-code-settings-builder.ts @@ -94,8 +94,19 @@ export interface ActionCodeSettings { * configured per project. This field provides the ability to explicitly choose * configured per project. This fields provides the ability explicitly choose * one. If none is provided, the oldest domain is used by default. + * @deprecated use linkDomain instead */ dynamicLinkDomain?: string; + + /** + * Defines the custom Firebase Hosting domain to use when the link is to be opened + * via a specified mobile app, + * This is a replacement of Firebase Dynamic Link. + * If none is provided, + * a default hosting domain will be used (for example, `example.firebaseapp.com`) + */ + + linkDomain?: string; } /** Defines the email action code server request. */ @@ -103,6 +114,7 @@ interface EmailActionCodeRequest { continueUrl?: string; canHandleCodeInApp?: boolean; dynamicLinkDomain?: string; + linkDomain?: string; androidPackageName?: string; androidMinimumVersion: string; androidInstallApp?: boolean; @@ -123,6 +135,7 @@ export class ActionCodeSettingsBuilder { private ibi?: string; private canHandleCodeInApp?: boolean; private dynamicLinkDomain?: string; + private linkDomain?: string; /** * ActionCodeSettingsBuilder constructor. @@ -166,6 +179,14 @@ export class ActionCodeSettingsBuilder { } this.dynamicLinkDomain = actionCodeSettings.dynamicLinkDomain; + if (typeof actionCodeSettings.linkDomain !== 'undefined' && + !validator.isNonEmptyString(actionCodeSettings.linkDomain)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_HOSTING_LINK_DOMAIN, + ); + } + this.linkDomain = actionCodeSettings.linkDomain; + if (typeof actionCodeSettings.iOS !== 'undefined') { if (!validator.isNonNullObject(actionCodeSettings.iOS)) { throw new FirebaseAuthError( @@ -230,6 +251,7 @@ export class ActionCodeSettingsBuilder { continueUrl: this.continueUrl, canHandleCodeInApp: this.canHandleCodeInApp, dynamicLinkDomain: this.dynamicLinkDomain, + linkDomain: this.linkDomain, androidPackageName: this.apn, androidMinimumVersion: this.amv, androidInstallApp: this.installApp, diff --git a/src/auth/auth-config.ts b/src/auth/auth-config.ts index 23895d60f3..7338e60872 100644 --- a/src/auth/auth-config.ts +++ b/src/auth/auth-config.ts @@ -2137,6 +2137,60 @@ export interface PasswordPolicyConfig { constraints?: CustomStrengthOptionsConfig; } +/** + * Configuration for settings related to univeral links (iOS) + * and app links (Android). + */ +export interface MobileLinksConfig { + /** + * Use firebase Hosting or dynamic link domain as the out-of-band code domain. + */ + domain?: MobileLinksDomain; +} + +/** + * Open code in app domain to use for app links and universal links. + */ +export type MobileLinksDomain = 'HOSTING_DOMAIN' | 'FIREBASE_DYNAMIC_LINK_DOMAIN'; + +/** + * Defines the MobileLinksAuthConfig class used for validation. + * + * @internal + */ +export class MobileLinksAuthConfig { + public static validate(options: MobileLinksConfig): void { + if (!validator.isNonNullObject(options)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"MobileLinksConfig" must be a non-null object.', + ); + } + + const validKeys = { + domain: true, + }; + + for (const key in options) { + if (!(key in validKeys)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + `"${key}" is not a valid "MobileLinksConfig" parameter.`, + ); + } + } + + if (typeof options.domain !== 'undefined' + && options.domain !== 'HOSTING_DOMAIN' + && options.domain !== 'FIREBASE_DYNAMIC_LINK_DOMAIN') { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"MobileLinksConfig.domain" must be either "HOSTING_DOMAIN" or "FIREBASE_DYNAMIC_LINK_DOMAIN".', + ); + } + } +} + /** * A password policy's enforcement state. */ diff --git a/src/auth/base-auth.ts b/src/auth/base-auth.ts index 6f77e088f8..56941056d9 100644 --- a/src/auth/base-auth.ts +++ b/src/auth/base-auth.ts @@ -749,7 +749,7 @@ export abstract class BaseAuth { * minimumVersion: '12' * }, * handleCodeInApp: true, - * dynamicLinkDomain: 'custom.page.link' + * linkDomain: 'project-id.firebaseapp.com' * }; * admin.auth() * .generatePasswordResetLink('user@example.com', actionCodeSettings) @@ -802,7 +802,7 @@ export abstract class BaseAuth { * minimumVersion: '12' * }, * handleCodeInApp: true, - * dynamicLinkDomain: 'custom.page.link' + * linkDomain: 'project-id.firebaseapp.com' * }; * admin.auth() * .generateEmailVerificationLink('user@example.com', actionCodeSettings) @@ -883,7 +883,7 @@ export abstract class BaseAuth { * minimumVersion: '12' * }, * handleCodeInApp: true, - * dynamicLinkDomain: 'custom.page.link' + * linkDomain: 'project-id.firebaseapp.com' * }; * admin.auth() * .generateEmailVerificationLink('user@example.com', actionCodeSettings) diff --git a/src/auth/index.ts b/src/auth/index.ts index 52538eb7d5..69cb95bf2a 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -104,6 +104,8 @@ export { PasswordPolicyEnforcementState, CustomStrengthOptionsConfig, EmailPrivacyConfig, + MobileLinksConfig, + MobileLinksDomain, } from './auth-config'; export { diff --git a/src/auth/project-config.ts b/src/auth/project-config.ts index 4a40e0c012..259f6eebb2 100644 --- a/src/auth/project-config.ts +++ b/src/auth/project-config.ts @@ -29,6 +29,8 @@ import { PasswordPolicyConfig, EmailPrivacyConfig, EmailPrivacyAuthConfig, + MobileLinksConfig, + MobileLinksAuthConfig, } from './auth-config'; import { deepCopy } from '../utils/deep-copy'; @@ -60,6 +62,11 @@ export interface UpdateProjectConfigRequest { * The email privacy configuration to update on the project */ emailPrivacyConfig?: EmailPrivacyConfig; + + /** + * The mobile links configuration for the project + */ + mobileLinksConfig?: MobileLinksConfig; } /** @@ -71,6 +78,7 @@ export interface ProjectConfigServerResponse { recaptchaConfig?: RecaptchaAuthServerConfig; passwordPolicyConfig?: PasswordPolicyAuthServerConfig; emailPrivacyConfig?: EmailPrivacyConfig; + mobileLinksConfig?: MobileLinksConfig; } /** @@ -82,6 +90,7 @@ export interface ProjectConfigClientRequest { recaptchaConfig?: RecaptchaAuthServerConfig; passwordPolicyConfig?: PasswordPolicyAuthServerConfig; emailPrivacyConfig?: EmailPrivacyConfig; + mobileLinksConfig?: MobileLinksConfig; } /** @@ -128,6 +137,11 @@ export class ProjectConfig { */ public readonly emailPrivacyConfig?: EmailPrivacyConfig; + /** + * The mobile links configuration for the project + */ + public readonly mobileLinksConfig?: MobileLinksConfig + /** * Validates a project config options object. Throws an error on failure. * @@ -146,6 +160,7 @@ export class ProjectConfig { recaptchaConfig: true, passwordPolicyConfig: true, emailPrivacyConfig: true, + mobileLinksConfig: true, } // Check for unsupported top level attributes. for (const key in request) { @@ -179,6 +194,11 @@ export class ProjectConfig { if (typeof request.emailPrivacyConfig !== 'undefined') { EmailPrivacyAuthConfig.validate(request.emailPrivacyConfig); } + + // Validate Mobile Links Config if provided. + if (typeof request.mobileLinksConfig !== 'undefined') { + MobileLinksAuthConfig.validate(request.mobileLinksConfig); + } } /** @@ -206,6 +226,9 @@ export class ProjectConfig { if (typeof configOptions.emailPrivacyConfig !== 'undefined') { request.emailPrivacyConfig = configOptions.emailPrivacyConfig; } + if (typeof configOptions.mobileLinksConfig !== 'undefined') { + request.mobileLinksConfig = configOptions.mobileLinksConfig; + } return request; } @@ -234,6 +257,9 @@ export class ProjectConfig { if (typeof response.emailPrivacyConfig !== 'undefined') { this.emailPrivacyConfig = response.emailPrivacyConfig; } + if (typeof response.mobileLinksConfig !== 'undefined') { + this.mobileLinksConfig = response.mobileLinksConfig; + } } /** * Returns a JSON-serializable representation of this object. @@ -248,6 +274,7 @@ export class ProjectConfig { recaptchaConfig: deepCopy(this.recaptchaConfig), passwordPolicyConfig: deepCopy(this.passwordPolicyConfig), emailPrivacyConfig: deepCopy(this.emailPrivacyConfig), + mobileLinksConfig: deepCopy(this.mobileLinksConfig), }; if (typeof json.smsRegionConfig === 'undefined') { delete json.smsRegionConfig; @@ -264,6 +291,9 @@ export class ProjectConfig { if (typeof json.emailPrivacyConfig === 'undefined') { delete json.emailPrivacyConfig; } + if (typeof json.mobileLinksConfig === 'undefined') { + delete json.mobileLinksConfig; + } return json; } } diff --git a/src/utils/error.ts b/src/utils/error.ts index ef56431a03..cf3736adcb 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -471,6 +471,11 @@ export class AuthClientErrorCode { 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.', @@ -933,6 +938,8 @@ const AUTH_SERVER_TO_CLIENT_CODE: ServerToClientCode = { 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. diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 8a6374363d..8c224c19d8 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -32,7 +32,7 @@ import { AuthProviderConfig, CreateTenantRequest, DeleteUsersResult, PhoneMultiFactorInfo, TenantAwareAuth, UpdatePhoneMultiFactorInfoRequest, UpdateTenantRequest, UserImportOptions, UserImportRecord, UserRecord, getAuth, UpdateProjectConfigRequest, UserMetadata, MultiFactorConfig, - PasswordPolicyConfig, SmsRegionConfig, RecaptchaConfig, + PasswordPolicyConfig, SmsRegionConfig, RecaptchaConfig, ActionCodeSettings, } from '../../lib/auth/index'; import * as sinon from 'sinon'; import * as sinonChai from 'sinon-chai'; @@ -75,10 +75,25 @@ const mockUserData = { photoURL: 'http://www.example.com/' + newUserUid + '/photo.png', disabled: false, }; -const actionCodeSettings = { +const actionCodeSettings : ActionCodeSettings = { url: 'http://localhost/?a=1&b=2#c=3', handleCodeInApp: false, }; +const actionCodeSettingsWithCustomDomain: ActionCodeSettings = { + url: 'http://localhost/?a=1&b=2#c=3', + handleCodeInApp: true, + linkDomain: 'kobayashimaru.testdomaindonotuse.com', + iOS: { + bundleId: 'testBundleId', + }, +} +const actionCodeSettingsForFdlLinks: ActionCodeSettings = { + url: 'http://localhost/?a=1&b=2#c=3', + handleCodeInApp: true, + iOS: { + bundleId: 'testBundleId', + }, +} let deleteQueue = Promise.resolve(); interface UserImportTest { @@ -1105,6 +1120,13 @@ describe('admin.auth', () => { // Create the test user before running this suite of tests. before(() => { + // Update project config to have HOSTING_DOMAIN as mobileLinksConfig after each test + const updateMobileLinksRequest: UpdateProjectConfigRequest = { + mobileLinksConfig: { + domain: 'HOSTING_DOMAIN', + }, + }; + getAuth().projectConfigManager().updateProjectConfig(updateMobileLinksRequest); return getAuth().createUser(userData); }); @@ -1199,6 +1221,162 @@ describe('admin.auth', () => { expect(result.user!.emailVerified).to.be.true; }); }); + + it('generateSignInWithEmailLink() with custom linkDomain should return error in case of invalid hosting domain', + function () { + if (authEmulatorHost) { + return this.skip(); // Not yet supported in Auth Emulator. + } + const actionCodeSettingsWithInvalidLinkDomain = deepCopy(actionCodeSettings); + actionCodeSettingsWithInvalidLinkDomain.linkDomain = 'invaliddomain.firebaseapp.com'; + return getAuth().generateSignInWithEmailLink(email, actionCodeSettingsWithInvalidLinkDomain) + .catch((error) => { + expect(error.code).to.equal('auth/invalid-hosting-link-domain'); + }); + }); + + it('generatePasswordResetLink() should return a password reset link with custom domain', function () { + if (authEmulatorHost) { + return this.skip(); // Not yet supported in Auth Emulator. + } + // Ensure old password set on created user. + return getAuth().updateUser(uid, { password: 'password' }) + .then(() => { + return getAuth().generatePasswordResetLink(email, actionCodeSettingsWithCustomDomain); + }) + .then((link) => { + const code = getActionCode(link); + expect(getContinueUrlForInAppRequest(link)).equal(actionCodeSettings.url); + expect(getHostName(link)).equal(actionCodeSettingsWithCustomDomain.linkDomain); + return clientAuth().confirmPasswordReset(code, newPassword); + }) + .then(() => { + return clientAuth().signInWithEmailAndPassword(email, newPassword); + }) + .then((result) => { + expect(result.user).to.exist; + expect(result.user!.email).to.equal(email); + // Password reset also verifies the user's email. + expect(result.user!.emailVerified).to.be.true; + }); + }); + + it('generateEmailVerificationLink() should return a verification link with custom domain', function () { + if (authEmulatorHost) { + return this.skip(); // Not yet supported in Auth Emulator. + } + // Ensure the user's email is unverified. + return getAuth().updateUser(uid, { password: '123456', emailVerified: false }) + .then((userRecord) => { + expect(userRecord.emailVerified).to.be.false; + return getAuth().generateEmailVerificationLink(email, actionCodeSettingsWithCustomDomain); + }) + .then((link) => { + const code = getActionCode(link); + expect(getContinueUrlForInAppRequest(link)).equal(actionCodeSettings.url); + expect(getHostName(link)).equal(actionCodeSettingsWithCustomDomain.linkDomain); + return clientAuth().applyActionCode(code); + }) + .then(() => { + return clientAuth().signInWithEmailAndPassword(email, userData.password); + }) + .then((result) => { + expect(result.user).to.exist; + expect(result.user!.email).to.equal(email); + expect(result.user!.emailVerified).to.be.true; + }); + }); + + it('generateSignInWithEmailLink() should return a sign-in link with custom domain', function () { + if (authEmulatorHost) { + return this.skip(); // Not yet supported in Auth Emulator. + } + return getAuth().generateSignInWithEmailLink(email, actionCodeSettingsWithCustomDomain) + .then((link) => { + expect(getContinueUrlForInAppRequest(link)).equal(actionCodeSettingsWithCustomDomain.url); + expect(getHostName(link)).equal(actionCodeSettingsWithCustomDomain.linkDomain); + return clientAuth().signInWithEmailLink(email, link); + }) + .then((result) => { + expect(result.user).to.exist; + expect(result.user!.email).to.equal(email); + expect(result.user!.emailVerified).to.be.true; + }); + }); + + it('generateVerifyAndChangeEmailLink() should return a verification link with custom domain', function () { + if (authEmulatorHost) { + return this.skip(); // Not yet supported in Auth Emulator. + } + // Ensure the user's email is verified. + return getAuth().updateUser(uid, { password: '123456', emailVerified: true }) + .then((userRecord) => { + expect(userRecord.emailVerified).to.be.true; + return getAuth().generateVerifyAndChangeEmailLink(email, newEmail, actionCodeSettingsWithCustomDomain); + }) + .then((link) => { + const code = getActionCode(link); + expect(getContinueUrlForInAppRequest(link)).equal(actionCodeSettings.url); + expect(getHostName(link)).equal(actionCodeSettingsWithCustomDomain.linkDomain); + return clientAuth().applyActionCode(code); + }) + .then(() => { + return clientAuth().signInWithEmailAndPassword(newEmail, 'password'); + }) + .then((result) => { + expect(result.user).to.exist; + expect(result.user!.email).to.equal(newEmail); + expect(result.user!.emailVerified).to.be.true; + }); + }); + + it('generateSignInWithEmailLink() should return a FDL sign-in' + + 'link with mobileLinksConfig set to FIREBASE_DYNAMIC_LINK_DOMAIN', function () { + if (authEmulatorHost) { + return this.skip(); // Not yet supported in Auth Emulator. + } + + const updateMobileLinksRequest: UpdateProjectConfigRequest = { + mobileLinksConfig: { + domain: 'FIREBASE_DYNAMIC_LINK_DOMAIN', + } + }; + return getAuth().projectConfigManager().updateProjectConfig(updateMobileLinksRequest) + .then((projectConfig) => { + expect(projectConfig?.mobileLinksConfig?.domain).equal('FIREBASE_DYNAMIC_LINK_DOMAIN'); + return getAuth().generateSignInWithEmailLink(email, actionCodeSettingsForFdlLinks); + }).then((link) => { + expectFDLLink(link); + return clientAuth().signInWithEmailLink(email, link); + }).then((result) => { + expect(result.user).to.exist; + expect(result.user!.email).to.equal(email); + expect(result.user!.emailVerified).to.be.true; + }); + }); + + it('generateSignInWithEmailLink() should return a FDL sign-in link with empty mobileLinksConfig', function () { + if (authEmulatorHost) { + return this.skip(); // Not yet supported in Auth Emulator. + } + + const updateMobileLinksRequest: UpdateProjectConfigRequest = { + mobileLinksConfig: { + } + }; + return getAuth().projectConfigManager().updateProjectConfig(updateMobileLinksRequest) + .then((projectConfig) => { + expect(projectConfig.mobileLinksConfig).is.empty; + return getAuth().generateSignInWithEmailLink(email, actionCodeSettingsForFdlLinks); + }).then((link) => { + expectFDLLink(link); + return clientAuth().signInWithEmailLink(email, link); + }).then((result) => { + expect(result.user).to.exist; + expect(result.user!.email).to.equal(email); + expect(result.user!.emailVerified).to.be.true; + }); + }); }); describe('Project config management operations', () => { @@ -1302,6 +1480,9 @@ describe('admin.auth', () => { recaptchaConfig: recaptchaStateAuditConfig, emailPrivacyConfig: { enableImprovedEmailPrivacy: true, + }, + mobileLinksConfig: { + domain: 'HOSTING_DOMAIN', } }; const projectConfigOption2: UpdateProjectConfigRequest = { @@ -1323,6 +1504,9 @@ describe('admin.auth', () => { emailPrivacyConfig: { enableImprovedEmailPrivacy: true, }, + mobileLinksConfig: { + domain: 'HOSTING_DOMAIN', + }, }; const expectedRecaptchaOffConfig: any = { @@ -1350,6 +1534,9 @@ describe('admin.auth', () => { passwordPolicyConfig: passwordConfig, recaptchaConfig: expectedRecaptchaOffConfig, emailPrivacyConfig: {}, + mobileLinksConfig: { + domain: 'HOSTING_DOMAIN', + }, }; const expectedProjectConfigSmsEnabledTotpDisabled: any = { @@ -1358,6 +1545,9 @@ describe('admin.auth', () => { passwordPolicyConfig: passwordConfig, recaptchaConfig: expectedRecaptchaOffConfig, emailPrivacyConfig: {}, + mobileLinksConfig: { + domain: 'HOSTING_DOMAIN', + }, }; it('updateProjectConfig() should resolve with the updated project config', () => { @@ -3231,6 +3421,53 @@ function getContinueUrl(link: string): string { return continueUrl!; } +/** + * Returns the host name corresponding to the link. + * + * @param link The link to parse for hostname + * @returns Hostname in the link + */ +function getHostName(link: string): string { + const parsedUrl = new url.URL(link); + return parsedUrl.hostname; +} + +/** + * Returns continue URL for handling in app requests. + * URL will be of the form, http://abc/__/auth/link?link= + * Coninue URL will be part of action link url + * + * @param link The link to parse for continue url + * @returns + */ +function getContinueUrlForInAppRequest(link: string): string { + // Extract action url from link param + const parsedUrl = new url.URL(link); + const linkParam = parsedUrl.searchParams.get('link') ?? ''; + expect(linkParam).is.not.empty; + + // Extract continueUrl param from action url + const actionUrl = new url.URL(linkParam); + const continueUrl = actionUrl.searchParams.get('continueUrl'); + expect(continueUrl).to.exist; + return continueUrl!; +} + +/** + * Verify if the generated link is generated by FDL + * We leverage the params created by FDL to test whether a given link is FDL + * + * @param link Link to check whether it is FDL + */ +function expectFDLLink(link: string): void { + const parsedUrl = new url.URL(link); + // For ios, FDL creates a fallback url with param ifl + // We leverage that to test whether a given link is FDL link + // Note: This param does not exist when the link is generated for HOSTING_DOMAIN + const iflParam = parsedUrl.searchParams.get('ifl'); + expect(iflParam).is.not.null; +} + /** * Returns the tenant ID corresponding to the link. * diff --git a/test/unit/auth/action-code-settings-builder.spec.ts b/test/unit/auth/action-code-settings-builder.spec.ts index ceadde3e9b..4608f711f1 100644 --- a/test/unit/auth/action-code-settings-builder.spec.ts +++ b/test/unit/auth/action-code-settings-builder.spec.ts @@ -28,6 +28,7 @@ chai.use(sinonChai); chai.use(chaiAsPromised); const expect = chai.expect; +const TEST_LINK_DOMAIN = 'project-id.firebaseapp.com'; describe('ActionCodeSettingsBuilder', () => { describe('constructor', () => { @@ -43,7 +44,10 @@ describe('ActionCodeSettingsBuilder', () => { installApp: true, minimumVersion: '6', }, + // not removing this test since we are going to accept both dynamicLinkDomain + // and linkDomain for the onboarding phase. dynamicLinkDomain: 'custom.page.link', + linkDomain: TEST_LINK_DOMAIN, })).not.to.throw; }); @@ -69,6 +73,7 @@ describe('ActionCodeSettingsBuilder', () => { minimumVersion: '6', }, dynamicLinkDomain: 'custom.page.link', + linkDomain: TEST_LINK_DOMAIN, } as any); }).to.throw(AuthClientErrorCode.MISSING_CONTINUE_URI.message); }); @@ -109,6 +114,20 @@ describe('ActionCodeSettingsBuilder', () => { }); }); + const invalidHostingDomains = [null, NaN, 0, 1, true, false, '', + [TEST_LINK_DOMAIN], [], {}, { a: 1 }, _.noop]; + invalidHostingDomains.forEach((domain) => { + it('should throw on invalid linkDomain:' + JSON.stringify(domain), () => { + expect(() => { + return new ActionCodeSettingsBuilder({ + url: 'https://www.example.com/path/file?a=1&b=2', + handleCodeInApp: true, + linkDomain: domain, + } as any); + }).to.throw(AuthClientErrorCode.INVALID_HOSTING_LINK_DOMAIN.message); + }); + }); + const invalidIOSSettings = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; invalidIOSSettings.forEach((settings) => { it('should throw on invalid iOS object:' + JSON.stringify(settings), () => { @@ -228,11 +247,13 @@ describe('ActionCodeSettingsBuilder', () => { minimumVersion: '6', }, dynamicLinkDomain: 'custom.page.link', + linkDomain: TEST_LINK_DOMAIN, }); const expectedRequest = { continueUrl: 'https://www.example.com/path/file?a=1&b=2', canHandleCodeInApp: true, dynamicLinkDomain: 'custom.page.link', + linkDomain: TEST_LINK_DOMAIN, androidPackageName: 'com.example.android', androidMinimumVersion: '6', androidInstallApp: true, diff --git a/test/unit/auth/auth-api-request.spec.ts b/test/unit/auth/auth-api-request.spec.ts index bd789421b8..9a6d624b88 100644 --- a/test/unit/auth/auth-api-request.spec.ts +++ b/test/unit/auth/auth-api-request.spec.ts @@ -3081,6 +3081,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { minimumVersion: '6', }, dynamicLinkDomain: 'custom.page.link', + linkDomain: 'project-id.firebaseapp.com', }; const expectedActionCodeSettingsRequest = new ActionCodeSettingsBuilder(actionCodeSettings).buildRequest(); const expectedLink = 'https://custom.page.link?link=' + diff --git a/test/unit/auth/auth-config.spec.ts b/test/unit/auth/auth-config.spec.ts index 8894bdc5ff..c5bdb7c445 100644 --- a/test/unit/auth/auth-config.spec.ts +++ b/test/unit/auth/auth-config.spec.ts @@ -28,6 +28,8 @@ import { MAXIMUM_TEST_PHONE_NUMBERS, PasswordPolicyAuthConfig, CustomStrengthOptionsConfig, + MobileLinksAuthConfig, + MobileLinksConfig, } from '../../../src/auth/auth-config'; import { SAMLUpdateAuthProviderRequest, OIDCUpdateAuthProviderRequest, @@ -1322,3 +1324,30 @@ describe('PasswordPolicyAuthConfig',() => { }); }); }); + +describe('MobileLinksAuthConfig',() => { + describe('validate',() => { + it('should throw an error on invalid MobileLinksConfig key',() => { + const config: any = { + link: 'HOSTING_DOMAIN' + }; + expect(() => + MobileLinksAuthConfig.validate(config) + ).to.throw('"link" is not a valid "MobileLinksConfig" parameter.'); + }); + + it('should throw an error on invalid MobileLinksDomain',() => { + const config: any = { + domain: 'WRONG_DOMAIN' + }; + expect(() => MobileLinksAuthConfig.validate(config)) + .to.throw('"MobileLinksConfig.domain" must be either "HOSTING_DOMAIN" or "FIREBASE_DYNAMIC_LINK_DOMAIN".'); + });// + it('should now throw an error on valid MobileLinksConfig',() => { + const config: MobileLinksConfig = { + domain: 'HOSTING_DOMAIN' + }; + expect(() => MobileLinksAuthConfig.validate(config)).not.to.throw(); + }); + }); +}); diff --git a/test/unit/auth/auth.spec.ts b/test/unit/auth/auth.spec.ts index 4924cb5b12..34d4c3d9a6 100644 --- a/test/unit/auth/auth.spec.ts +++ b/test/unit/auth/auth.spec.ts @@ -2887,6 +2887,7 @@ AUTH_CONFIGS.forEach((testConfig) => { minimumVersion: '6', }, dynamicLinkDomain: 'custom.page.link', + linkDomain: 'project-id.firebaseapp.com', }; const expectedLink = 'https://custom.page.link?link=' + encodeURIComponent('https://projectId.firebaseapp.com/__/auth/action?oobCode=CODE') + diff --git a/test/unit/auth/project-config.spec.ts b/test/unit/auth/project-config.spec.ts index f116647568..6c39c721a0 100644 --- a/test/unit/auth/project-config.spec.ts +++ b/test/unit/auth/project-config.spec.ts @@ -50,6 +50,18 @@ describe('ProjectConfig', () => { }, ], }, + recaptchaConfig: { + emailPasswordEnforcementState: 'AUDIT', + managedRules: [ { + endScore: 0.2, + action: 'BLOCK' + } ], + recaptchaKeys: [ { + type: 'WEB', + key: 'test-key-1' } + ], + useAccountDefender: true, + }, passwordPolicyConfig: { passwordPolicyEnforcementState: 'ENFORCE', forceUpgradeOnSignin: true, @@ -87,7 +99,10 @@ describe('ProjectConfig', () => { useAccountDefender: true, useSmsBotScore: true, useSmsTollFraudProtection: true, - } + }, + mobileLinksConfig: { + domain: 'FIREBASE_DYNAMIC_LINK_DOMAIN', + }, }; const updateProjectConfigRequest1: UpdateProjectConfigRequest = { @@ -111,6 +126,9 @@ describe('ProjectConfig', () => { emailPrivacyConfig: { enableImprovedEmailPrivacy: false, }, + mobileLinksConfig: { + domain: 'HOSTING_DOMAIN' + }, }; const updateProjectConfigRequest2: UpdateProjectConfigRequest = { @@ -550,6 +568,22 @@ describe('ProjectConfig', () => { }).to.throw('"EmailPrivacyConfig.enableImprovedEmailPrivacy" must be a valid boolean value.'); }); + it('should throw on invalid MobileLinksConfig attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.mobileLinksConfig.invalidParameter = 'invalid'; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"invalidParameter" is not a valid "MobileLinksConfig" parameter.'); + }); + + it('should throw on invalid domain attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.mobileLinksConfig.domain = 'random domain'; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"MobileLinksConfig.domain" must be either "HOSTING_DOMAIN" or "FIREBASE_DYNAMIC_LINK_DOMAIN".'); + }); + const nonObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; nonObjects.forEach((request) => { it('should throw on invalid UpdateProjectConfigRequest:' + JSON.stringify(request), () => { @@ -647,9 +681,32 @@ describe('ProjectConfig', () => { }; expect(projectConfig.emailPrivacyConfig).to.deep.equal(expectedEmailPrivacyConfig); }); + + it('should set readonly property mobileLinksConfig', () => { + const expectedMobileLinksConfig = { + domain: 'FIREBASE_DYNAMIC_LINK_DOMAIN', + }; + expect(projectConfig.mobileLinksConfig).to.deep.equal(expectedMobileLinksConfig); + }); }); describe('toJSON()', () => { + // server output and toJson does not have the same format + const passwordPolicyJson: any = { + enforcementState: 'ENFORCE', + constraints: { + requireLowercase: true, + requireUppercase: true, + requireNonAlphanumeric: true, + requireNumeric: true, + minLength: 8, + maxLength: 30 + }, + forceUpgradeOnSignin: true + }; + const multiFactorJson: any = deepCopy(serverResponse.mfa); + // factorIDs were added by default. + multiFactorJson['factorIds'] = []; const serverResponseCopy: ProjectConfigServerResponse = deepCopy(serverResponse); it('should return the expected object representation of project config', () => { expect(new ProjectConfig(serverResponseCopy).toJSON()).to.deep.equal({ @@ -704,6 +761,7 @@ describe('ProjectConfig', () => { emailPrivacyConfig: { enableImprovedEmailPrivacy: true, }, + mobileLinksConfig: deepCopy(serverResponse.mobileLinksConfig), }); }); @@ -720,6 +778,7 @@ describe('ProjectConfig', () => { delete serverResponseOptionalCopy.recaptchaConfig?.useSmsTollFraudProtection delete serverResponseOptionalCopy.passwordPolicyConfig; delete serverResponseOptionalCopy.emailPrivacyConfig; + delete serverResponseOptionalCopy.mobileLinksConfig; expect(new ProjectConfig(serverResponseOptionalCopy).toJSON()).to.deep.equal({ recaptchaConfig: { recaptchaKeys: deepCopy(serverResponse.recaptchaConfig?.recaptchaKeys), From f9fa6b201451b154c7f92d415cbdfabcfb662475 Mon Sep 17 00:00:00 2001 From: Liubin Jiang Date: Wed, 30 Oct 2024 14:46:39 -0700 Subject: [PATCH 2/7] fix merge conflict --- test/unit/auth/project-config.spec.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/test/unit/auth/project-config.spec.ts b/test/unit/auth/project-config.spec.ts index 6c39c721a0..fed8ba2c7d 100644 --- a/test/unit/auth/project-config.spec.ts +++ b/test/unit/auth/project-config.spec.ts @@ -691,22 +691,6 @@ describe('ProjectConfig', () => { }); describe('toJSON()', () => { - // server output and toJson does not have the same format - const passwordPolicyJson: any = { - enforcementState: 'ENFORCE', - constraints: { - requireLowercase: true, - requireUppercase: true, - requireNonAlphanumeric: true, - requireNumeric: true, - minLength: 8, - maxLength: 30 - }, - forceUpgradeOnSignin: true - }; - const multiFactorJson: any = deepCopy(serverResponse.mfa); - // factorIDs were added by default. - multiFactorJson['factorIds'] = []; const serverResponseCopy: ProjectConfigServerResponse = deepCopy(serverResponse); it('should return the expected object representation of project config', () => { expect(new ProjectConfig(serverResponseCopy).toJSON()).to.deep.equal({ From b582260a09a76881acc038c11ae990f8e8cd1f97 Mon Sep 17 00:00:00 2001 From: Liubin Jiang Date: Wed, 30 Oct 2024 14:52:28 -0700 Subject: [PATCH 3/7] remove duplicate recaptchaConfig --- test/unit/auth/project-config.spec.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/test/unit/auth/project-config.spec.ts b/test/unit/auth/project-config.spec.ts index fed8ba2c7d..7857deb398 100644 --- a/test/unit/auth/project-config.spec.ts +++ b/test/unit/auth/project-config.spec.ts @@ -50,18 +50,6 @@ describe('ProjectConfig', () => { }, ], }, - recaptchaConfig: { - emailPasswordEnforcementState: 'AUDIT', - managedRules: [ { - endScore: 0.2, - action: 'BLOCK' - } ], - recaptchaKeys: [ { - type: 'WEB', - key: 'test-key-1' } - ], - useAccountDefender: true, - }, passwordPolicyConfig: { passwordPolicyEnforcementState: 'ENFORCE', forceUpgradeOnSignin: true, From 834e4325fd905b39c60603712f12c7ebfb0ff9f0 Mon Sep 17 00:00:00 2001 From: Pavan Shankar Date: Wed, 6 Nov 2024 16:55:37 +0000 Subject: [PATCH 4/7] Fix failing FDL deprecation integration tests (#2762) --- test/integration/auth.spec.ts | 68 ++++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 21 deletions(-) diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 8c224c19d8..560cd47005 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -1110,6 +1110,7 @@ describe('admin.auth', () => { const uid = generateRandomString(20).toLowerCase(); const email = uid + '@example.com'; const newEmail = uid + 'new@example.com'; + const newEmail2 = uid + 'new2@example.com'; const newPassword = 'newPassword'; const userData = { uid, @@ -1242,20 +1243,20 @@ describe('admin.auth', () => { // Ensure old password set on created user. return getAuth().updateUser(uid, { password: 'password' }) .then(() => { - return getAuth().generatePasswordResetLink(email, actionCodeSettingsWithCustomDomain); + return getAuth().generatePasswordResetLink(newEmail, actionCodeSettingsWithCustomDomain); }) .then((link) => { - const code = getActionCode(link); + const code = getActionCodeForInAppRequest(link); expect(getContinueUrlForInAppRequest(link)).equal(actionCodeSettings.url); expect(getHostName(link)).equal(actionCodeSettingsWithCustomDomain.linkDomain); return clientAuth().confirmPasswordReset(code, newPassword); }) .then(() => { - return clientAuth().signInWithEmailAndPassword(email, newPassword); + return clientAuth().signInWithEmailAndPassword(newEmail, newPassword); }) .then((result) => { expect(result.user).to.exist; - expect(result.user!.email).to.equal(email); + expect(result.user!.email).to.equal(newEmail); // Password reset also verifies the user's email. expect(result.user!.emailVerified).to.be.true; }); @@ -1266,23 +1267,23 @@ describe('admin.auth', () => { return this.skip(); // Not yet supported in Auth Emulator. } // Ensure the user's email is unverified. - return getAuth().updateUser(uid, { password: '123456', emailVerified: false }) + return getAuth().updateUser(uid, { password: 'password', emailVerified: false }) .then((userRecord) => { expect(userRecord.emailVerified).to.be.false; - return getAuth().generateEmailVerificationLink(email, actionCodeSettingsWithCustomDomain); + return getAuth().generateEmailVerificationLink(newEmail, actionCodeSettingsWithCustomDomain); }) .then((link) => { - const code = getActionCode(link); + const code = getActionCodeForInAppRequest(link); expect(getContinueUrlForInAppRequest(link)).equal(actionCodeSettings.url); expect(getHostName(link)).equal(actionCodeSettingsWithCustomDomain.linkDomain); return clientAuth().applyActionCode(code); }) .then(() => { - return clientAuth().signInWithEmailAndPassword(email, userData.password); + return clientAuth().signInWithEmailAndPassword(newEmail, userData.password); }) .then((result) => { expect(result.user).to.exist; - expect(result.user!.email).to.equal(email); + expect(result.user!.email).to.equal(newEmail); expect(result.user!.emailVerified).to.be.true; }); }); @@ -1309,23 +1310,23 @@ describe('admin.auth', () => { return this.skip(); // Not yet supported in Auth Emulator. } // Ensure the user's email is verified. - return getAuth().updateUser(uid, { password: '123456', emailVerified: true }) + return getAuth().updateUser(uid, { password: 'password', emailVerified: true }) .then((userRecord) => { expect(userRecord.emailVerified).to.be.true; - return getAuth().generateVerifyAndChangeEmailLink(email, newEmail, actionCodeSettingsWithCustomDomain); + return getAuth().generateVerifyAndChangeEmailLink(newEmail, newEmail2, actionCodeSettingsWithCustomDomain); }) .then((link) => { - const code = getActionCode(link); + const code = getActionCodeForInAppRequest(link); expect(getContinueUrlForInAppRequest(link)).equal(actionCodeSettings.url); expect(getHostName(link)).equal(actionCodeSettingsWithCustomDomain.linkDomain); return clientAuth().applyActionCode(code); }) .then(() => { - return clientAuth().signInWithEmailAndPassword(newEmail, 'password'); + return clientAuth().signInWithEmailAndPassword(newEmail2, 'password'); }) .then((result) => { expect(result.user).to.exist; - expect(result.user!.email).to.equal(newEmail); + expect(result.user!.email).to.equal(newEmail2); expect(result.user!.emailVerified).to.be.true; }); }); @@ -3438,19 +3439,44 @@ function getHostName(link: string): string { * Coninue URL will be part of action link url * * @param link The link to parse for continue url - * @returns + * @returns Link's corresponding continueUrl */ function getContinueUrlForInAppRequest(link: string): string { + const actionUrl = extractLinkUrl(link); + const continueUrl = actionUrl.searchParams.get('continueUrl'); + expect(continueUrl).to.exist; + return continueUrl!; +} + +/** + * Returns the action code corresponding to the link for in app requests. + * URL will be of the form, http://abc/__/auth/link?link= + * oobCode will be part of action link url + * + * @param link The link to parse for the action code. + * @return The link's corresponding action code. + */ +function getActionCodeForInAppRequest(link: string): string { + const actionUrl = extractLinkUrl(link); + const oobCode = actionUrl.searchParams.get('oobCode'); + expect(oobCode).to.exist; + return oobCode!; +} + +/** + * Extract URL in link parameter from the full link + * URL will be of the form, http://abc/__/auth/link?link= + * + * @param link The link to parse for the param + * @returns URL inside link param + */ +function extractLinkUrl(link: string): url.URL { // Extract action url from link param const parsedUrl = new url.URL(link); const linkParam = parsedUrl.searchParams.get('link') ?? ''; expect(linkParam).is.not.empty; - - // Extract continueUrl param from action url - const actionUrl = new url.URL(linkParam); - const continueUrl = actionUrl.searchParams.get('continueUrl'); - expect(continueUrl).to.exist; - return continueUrl!; + + return new url.URL(linkParam); } /** From a579cee61862fd14236b0d1be9ab3d5aede2d767 Mon Sep 17 00:00:00 2001 From: Liubin Jiang Date: Mon, 3 Feb 2025 16:31:54 -0800 Subject: [PATCH 5/7] address tw review --- etc/firebase-admin.api.md | 6 ++++-- etc/firebase-admin.app-check.api.md | 2 ++ etc/firebase-admin.app.api.md | 2 ++ etc/firebase-admin.auth.api.md | 2 ++ etc/firebase-admin.data-connect.api.md | 2 ++ etc/firebase-admin.database.api.md | 2 ++ etc/firebase-admin.eventarc.api.md | 2 ++ etc/firebase-admin.extensions.api.md | 2 ++ etc/firebase-admin.firestore.api.md | 2 ++ etc/firebase-admin.functions.api.md | 2 ++ etc/firebase-admin.installations.api.md | 2 ++ etc/firebase-admin.instance-id.api.md | 2 ++ etc/firebase-admin.machine-learning.api.md | 2 ++ etc/firebase-admin.messaging.api.md | 2 ++ etc/firebase-admin.project-management.api.md | 2 ++ etc/firebase-admin.remote-config.api.md | 2 ++ etc/firebase-admin.security-rules.api.md | 2 ++ etc/firebase-admin.storage.api.md | 7 ++++--- src/auth/auth-config.ts | 2 +- 19 files changed, 41 insertions(+), 6 deletions(-) diff --git a/etc/firebase-admin.api.md b/etc/firebase-admin.api.md index e9d4bb0e49..6a89edcac2 100644 --- a/etc/firebase-admin.api.md +++ b/etc/firebase-admin.api.md @@ -4,6 +4,8 @@ ```ts +/// + import { Agent } from 'http'; import { Bucket } from '@google-cloud/storage'; import { FirebaseDatabase } from '@firebase/database-types'; @@ -499,8 +501,8 @@ export function storage(app?: App): storage.Storage; // @public (undocumented) export namespace storage { - // Warning: (ae-forgotten-export) The symbol "Storage_2" needs to be exported by the entry point default-namespace.d.ts - export type Storage = Storage_2; + // Warning: (ae-forgotten-export) The symbol "Storage" needs to be exported by the entry point default-namespace.d.ts + export type Storage = Storage; } ``` diff --git a/etc/firebase-admin.app-check.api.md b/etc/firebase-admin.app-check.api.md index 8486d60ac7..7c883d5f38 100644 --- a/etc/firebase-admin.app-check.api.md +++ b/etc/firebase-admin.app-check.api.md @@ -4,6 +4,8 @@ ```ts +/// + import { Agent } from 'http'; // @public diff --git a/etc/firebase-admin.app.api.md b/etc/firebase-admin.app.api.md index 97ab9667ce..4546fe7824 100644 --- a/etc/firebase-admin.app.api.md +++ b/etc/firebase-admin.app.api.md @@ -4,6 +4,8 @@ ```ts +/// + import { Agent } from 'http'; // @public diff --git a/etc/firebase-admin.auth.api.md b/etc/firebase-admin.auth.api.md index 8713b1bda7..824f8347f7 100644 --- a/etc/firebase-admin.auth.api.md +++ b/etc/firebase-admin.auth.api.md @@ -4,6 +4,8 @@ ```ts +/// + import { Agent } from 'http'; // @public diff --git a/etc/firebase-admin.data-connect.api.md b/etc/firebase-admin.data-connect.api.md index bf3f3b6b19..ab253d17eb 100644 --- a/etc/firebase-admin.data-connect.api.md +++ b/etc/firebase-admin.data-connect.api.md @@ -4,6 +4,8 @@ ```ts +/// + import { Agent } from 'http'; // @public diff --git a/etc/firebase-admin.database.api.md b/etc/firebase-admin.database.api.md index 0cd7d55e9b..1778f75d51 100644 --- a/etc/firebase-admin.database.api.md +++ b/etc/firebase-admin.database.api.md @@ -4,6 +4,8 @@ ```ts +/// + import { Agent } from 'http'; import { DataSnapshot } from '@firebase/database-types'; import { EventType } from '@firebase/database-types'; diff --git a/etc/firebase-admin.eventarc.api.md b/etc/firebase-admin.eventarc.api.md index 4a6020f33a..a070f8b391 100644 --- a/etc/firebase-admin.eventarc.api.md +++ b/etc/firebase-admin.eventarc.api.md @@ -4,6 +4,8 @@ ```ts +/// + import { Agent } from 'http'; // @public diff --git a/etc/firebase-admin.extensions.api.md b/etc/firebase-admin.extensions.api.md index 9082d602b9..127754c9c9 100644 --- a/etc/firebase-admin.extensions.api.md +++ b/etc/firebase-admin.extensions.api.md @@ -4,6 +4,8 @@ ```ts +/// + import { Agent } from 'http'; // @public diff --git a/etc/firebase-admin.firestore.api.md b/etc/firebase-admin.firestore.api.md index e8b2932326..8f29baeab3 100644 --- a/etc/firebase-admin.firestore.api.md +++ b/etc/firebase-admin.firestore.api.md @@ -4,6 +4,8 @@ ```ts +/// + import { AddPrefixToKeys } from '@google-cloud/firestore'; import { Agent } from 'http'; import { AggregateField } from '@google-cloud/firestore'; diff --git a/etc/firebase-admin.functions.api.md b/etc/firebase-admin.functions.api.md index 10eee3fef9..87f8656b4a 100644 --- a/etc/firebase-admin.functions.api.md +++ b/etc/firebase-admin.functions.api.md @@ -4,6 +4,8 @@ ```ts +/// + import { Agent } from 'http'; // @public diff --git a/etc/firebase-admin.installations.api.md b/etc/firebase-admin.installations.api.md index 2bc45b3e14..56496ef135 100644 --- a/etc/firebase-admin.installations.api.md +++ b/etc/firebase-admin.installations.api.md @@ -4,6 +4,8 @@ ```ts +/// + import { Agent } from 'http'; // Warning: (ae-forgotten-export) The symbol "FirebaseError" needs to be exported by the entry point index.d.ts diff --git a/etc/firebase-admin.instance-id.api.md b/etc/firebase-admin.instance-id.api.md index 5f7852c15d..9c28852a2c 100644 --- a/etc/firebase-admin.instance-id.api.md +++ b/etc/firebase-admin.instance-id.api.md @@ -4,6 +4,8 @@ ```ts +/// + import { Agent } from 'http'; // Warning: (ae-forgotten-export) The symbol "FirebaseError" needs to be exported by the entry point index.d.ts diff --git a/etc/firebase-admin.machine-learning.api.md b/etc/firebase-admin.machine-learning.api.md index d39e2d2a9d..b6c3569de9 100644 --- a/etc/firebase-admin.machine-learning.api.md +++ b/etc/firebase-admin.machine-learning.api.md @@ -4,6 +4,8 @@ ```ts +/// + import { Agent } from 'http'; // @public (undocumented) diff --git a/etc/firebase-admin.messaging.api.md b/etc/firebase-admin.messaging.api.md index b4ee30a9f0..1b30e00ed5 100644 --- a/etc/firebase-admin.messaging.api.md +++ b/etc/firebase-admin.messaging.api.md @@ -4,6 +4,8 @@ ```ts +/// + import { Agent } from 'http'; // @public diff --git a/etc/firebase-admin.project-management.api.md b/etc/firebase-admin.project-management.api.md index 4fed0d715a..8f59f54642 100644 --- a/etc/firebase-admin.project-management.api.md +++ b/etc/firebase-admin.project-management.api.md @@ -4,6 +4,8 @@ ```ts +/// + import { Agent } from 'http'; // @public diff --git a/etc/firebase-admin.remote-config.api.md b/etc/firebase-admin.remote-config.api.md index 68d760468c..ea0fddd63c 100644 --- a/etc/firebase-admin.remote-config.api.md +++ b/etc/firebase-admin.remote-config.api.md @@ -4,6 +4,8 @@ ```ts +/// + import { Agent } from 'http'; // @public diff --git a/etc/firebase-admin.security-rules.api.md b/etc/firebase-admin.security-rules.api.md index 13c3be8289..890da538d1 100644 --- a/etc/firebase-admin.security-rules.api.md +++ b/etc/firebase-admin.security-rules.api.md @@ -4,6 +4,8 @@ ```ts +/// + import { Agent } from 'http'; // Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts diff --git a/etc/firebase-admin.storage.api.md b/etc/firebase-admin.storage.api.md index db5e336a07..ab798e8fca 100644 --- a/etc/firebase-admin.storage.api.md +++ b/etc/firebase-admin.storage.api.md @@ -4,6 +4,8 @@ ```ts +/// + import { Agent } from 'http'; import { Bucket } from '@google-cloud/storage'; import { File as File_2 } from '@google-cloud/storage'; @@ -14,13 +16,12 @@ export function getDownloadURL(file: File_2): Promise; // Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts // // @public -export function getStorage(app?: App): Storage_2; +export function getStorage(app?: App): Storage; // @public -class Storage_2 { +export class Storage { get app(): App; bucket(name?: string): Bucket; } -export { Storage_2 as Storage } ``` diff --git a/src/auth/auth-config.ts b/src/auth/auth-config.ts index 7338e60872..d2d07e9536 100644 --- a/src/auth/auth-config.ts +++ b/src/auth/auth-config.ts @@ -2143,7 +2143,7 @@ export interface PasswordPolicyConfig { */ export interface MobileLinksConfig { /** - * Use firebase Hosting or dynamic link domain as the out-of-band code domain. + * Use Firebase Hosting or dynamic link domain as the out-of-band code domain. */ domain?: MobileLinksDomain; } From a1c3ef1ba1fb54357920cf11626a582cbfa2b289 Mon Sep 17 00:00:00 2001 From: Liubin Jiang Date: Tue, 4 Feb 2025 07:48:22 -0800 Subject: [PATCH 6/7] fix api-extractor issue --- etc/firebase-admin.api.md | 6 ++---- etc/firebase-admin.app-check.api.md | 2 -- etc/firebase-admin.app.api.md | 2 -- etc/firebase-admin.auth.api.md | 2 -- etc/firebase-admin.data-connect.api.md | 2 -- etc/firebase-admin.database.api.md | 2 -- etc/firebase-admin.eventarc.api.md | 2 -- etc/firebase-admin.extensions.api.md | 2 -- etc/firebase-admin.firestore.api.md | 2 -- etc/firebase-admin.functions.api.md | 2 -- etc/firebase-admin.installations.api.md | 2 -- etc/firebase-admin.instance-id.api.md | 2 -- etc/firebase-admin.machine-learning.api.md | 2 -- etc/firebase-admin.messaging.api.md | 2 -- etc/firebase-admin.project-management.api.md | 2 -- etc/firebase-admin.remote-config.api.md | 2 -- etc/firebase-admin.security-rules.api.md | 2 -- etc/firebase-admin.storage.api.md | 7 +++---- 18 files changed, 5 insertions(+), 40 deletions(-) diff --git a/etc/firebase-admin.api.md b/etc/firebase-admin.api.md index 6a89edcac2..e9d4bb0e49 100644 --- a/etc/firebase-admin.api.md +++ b/etc/firebase-admin.api.md @@ -4,8 +4,6 @@ ```ts -/// - import { Agent } from 'http'; import { Bucket } from '@google-cloud/storage'; import { FirebaseDatabase } from '@firebase/database-types'; @@ -501,8 +499,8 @@ export function storage(app?: App): storage.Storage; // @public (undocumented) export namespace storage { - // Warning: (ae-forgotten-export) The symbol "Storage" needs to be exported by the entry point default-namespace.d.ts - export type Storage = Storage; + // Warning: (ae-forgotten-export) The symbol "Storage_2" needs to be exported by the entry point default-namespace.d.ts + export type Storage = Storage_2; } ``` diff --git a/etc/firebase-admin.app-check.api.md b/etc/firebase-admin.app-check.api.md index 7c883d5f38..8486d60ac7 100644 --- a/etc/firebase-admin.app-check.api.md +++ b/etc/firebase-admin.app-check.api.md @@ -4,8 +4,6 @@ ```ts -/// - import { Agent } from 'http'; // @public diff --git a/etc/firebase-admin.app.api.md b/etc/firebase-admin.app.api.md index 4546fe7824..97ab9667ce 100644 --- a/etc/firebase-admin.app.api.md +++ b/etc/firebase-admin.app.api.md @@ -4,8 +4,6 @@ ```ts -/// - import { Agent } from 'http'; // @public diff --git a/etc/firebase-admin.auth.api.md b/etc/firebase-admin.auth.api.md index 824f8347f7..8713b1bda7 100644 --- a/etc/firebase-admin.auth.api.md +++ b/etc/firebase-admin.auth.api.md @@ -4,8 +4,6 @@ ```ts -/// - import { Agent } from 'http'; // @public diff --git a/etc/firebase-admin.data-connect.api.md b/etc/firebase-admin.data-connect.api.md index ab253d17eb..bf3f3b6b19 100644 --- a/etc/firebase-admin.data-connect.api.md +++ b/etc/firebase-admin.data-connect.api.md @@ -4,8 +4,6 @@ ```ts -/// - import { Agent } from 'http'; // @public diff --git a/etc/firebase-admin.database.api.md b/etc/firebase-admin.database.api.md index 1778f75d51..0cd7d55e9b 100644 --- a/etc/firebase-admin.database.api.md +++ b/etc/firebase-admin.database.api.md @@ -4,8 +4,6 @@ ```ts -/// - import { Agent } from 'http'; import { DataSnapshot } from '@firebase/database-types'; import { EventType } from '@firebase/database-types'; diff --git a/etc/firebase-admin.eventarc.api.md b/etc/firebase-admin.eventarc.api.md index a070f8b391..4a6020f33a 100644 --- a/etc/firebase-admin.eventarc.api.md +++ b/etc/firebase-admin.eventarc.api.md @@ -4,8 +4,6 @@ ```ts -/// - import { Agent } from 'http'; // @public diff --git a/etc/firebase-admin.extensions.api.md b/etc/firebase-admin.extensions.api.md index 127754c9c9..9082d602b9 100644 --- a/etc/firebase-admin.extensions.api.md +++ b/etc/firebase-admin.extensions.api.md @@ -4,8 +4,6 @@ ```ts -/// - import { Agent } from 'http'; // @public diff --git a/etc/firebase-admin.firestore.api.md b/etc/firebase-admin.firestore.api.md index 8f29baeab3..e8b2932326 100644 --- a/etc/firebase-admin.firestore.api.md +++ b/etc/firebase-admin.firestore.api.md @@ -4,8 +4,6 @@ ```ts -/// - import { AddPrefixToKeys } from '@google-cloud/firestore'; import { Agent } from 'http'; import { AggregateField } from '@google-cloud/firestore'; diff --git a/etc/firebase-admin.functions.api.md b/etc/firebase-admin.functions.api.md index 87f8656b4a..10eee3fef9 100644 --- a/etc/firebase-admin.functions.api.md +++ b/etc/firebase-admin.functions.api.md @@ -4,8 +4,6 @@ ```ts -/// - import { Agent } from 'http'; // @public diff --git a/etc/firebase-admin.installations.api.md b/etc/firebase-admin.installations.api.md index 56496ef135..2bc45b3e14 100644 --- a/etc/firebase-admin.installations.api.md +++ b/etc/firebase-admin.installations.api.md @@ -4,8 +4,6 @@ ```ts -/// - import { Agent } from 'http'; // Warning: (ae-forgotten-export) The symbol "FirebaseError" needs to be exported by the entry point index.d.ts diff --git a/etc/firebase-admin.instance-id.api.md b/etc/firebase-admin.instance-id.api.md index 9c28852a2c..5f7852c15d 100644 --- a/etc/firebase-admin.instance-id.api.md +++ b/etc/firebase-admin.instance-id.api.md @@ -4,8 +4,6 @@ ```ts -/// - import { Agent } from 'http'; // Warning: (ae-forgotten-export) The symbol "FirebaseError" needs to be exported by the entry point index.d.ts diff --git a/etc/firebase-admin.machine-learning.api.md b/etc/firebase-admin.machine-learning.api.md index b6c3569de9..d39e2d2a9d 100644 --- a/etc/firebase-admin.machine-learning.api.md +++ b/etc/firebase-admin.machine-learning.api.md @@ -4,8 +4,6 @@ ```ts -/// - import { Agent } from 'http'; // @public (undocumented) diff --git a/etc/firebase-admin.messaging.api.md b/etc/firebase-admin.messaging.api.md index 1b30e00ed5..b4ee30a9f0 100644 --- a/etc/firebase-admin.messaging.api.md +++ b/etc/firebase-admin.messaging.api.md @@ -4,8 +4,6 @@ ```ts -/// - import { Agent } from 'http'; // @public diff --git a/etc/firebase-admin.project-management.api.md b/etc/firebase-admin.project-management.api.md index 8f59f54642..4fed0d715a 100644 --- a/etc/firebase-admin.project-management.api.md +++ b/etc/firebase-admin.project-management.api.md @@ -4,8 +4,6 @@ ```ts -/// - import { Agent } from 'http'; // @public diff --git a/etc/firebase-admin.remote-config.api.md b/etc/firebase-admin.remote-config.api.md index ea0fddd63c..68d760468c 100644 --- a/etc/firebase-admin.remote-config.api.md +++ b/etc/firebase-admin.remote-config.api.md @@ -4,8 +4,6 @@ ```ts -/// - import { Agent } from 'http'; // @public diff --git a/etc/firebase-admin.security-rules.api.md b/etc/firebase-admin.security-rules.api.md index 890da538d1..13c3be8289 100644 --- a/etc/firebase-admin.security-rules.api.md +++ b/etc/firebase-admin.security-rules.api.md @@ -4,8 +4,6 @@ ```ts -/// - import { Agent } from 'http'; // Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts diff --git a/etc/firebase-admin.storage.api.md b/etc/firebase-admin.storage.api.md index ab798e8fca..db5e336a07 100644 --- a/etc/firebase-admin.storage.api.md +++ b/etc/firebase-admin.storage.api.md @@ -4,8 +4,6 @@ ```ts -/// - import { Agent } from 'http'; import { Bucket } from '@google-cloud/storage'; import { File as File_2 } from '@google-cloud/storage'; @@ -16,12 +14,13 @@ export function getDownloadURL(file: File_2): Promise; // Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts // // @public -export function getStorage(app?: App): Storage; +export function getStorage(app?: App): Storage_2; // @public -export class Storage { +class Storage_2 { get app(): App; bucket(name?: string): Bucket; } +export { Storage_2 as Storage } ``` From 57524dc275981d21b1870703e386588a55cf906d Mon Sep 17 00:00:00 2001 From: Liubin Jiang Date: Tue, 4 Feb 2025 10:43:51 -0800 Subject: [PATCH 7/7] rephrase the deprecated annotation --- src/auth/action-code-settings-builder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth/action-code-settings-builder.ts b/src/auth/action-code-settings-builder.ts index f8ea0bcaf3..13dbcbc725 100644 --- a/src/auth/action-code-settings-builder.ts +++ b/src/auth/action-code-settings-builder.ts @@ -94,7 +94,7 @@ export interface ActionCodeSettings { * configured per project. This field provides the ability to explicitly choose * configured per project. This fields provides the ability explicitly choose * one. If none is provided, the oldest domain is used by default. - * @deprecated use linkDomain instead + * @deprecated use `linkDomain` instead */ dynamicLinkDomain?: string;