From bd9d4d8f35e06bfaa30f998219f3a8797140e580 Mon Sep 17 00:00:00 2001
From: Erik Eldridge <erikeldridge@google.com>
Date: Wed, 14 Feb 2024 11:24:00 -0800
Subject: [PATCH 01/13] Define SSRC API (#2456)

Define SSRC API
---------

Co-authored-by: Xin Wei <trekforever@users.noreply.github.com>
---
 src/remote-config/remote-config-api.ts | 80 +++++++++++++++++++++++++-
 1 file changed, 79 insertions(+), 1 deletion(-)

diff --git a/src/remote-config/remote-config-api.ts b/src/remote-config/remote-config-api.ts
index 90f3bd4970..dd9f641034 100644
--- a/src/remote-config/remote-config-api.ts
+++ b/src/remote-config/remote-config-api.ts
@@ -54,6 +54,27 @@ export interface RemoteConfigCondition {
   tagColor?: TagColor;
 }
 
+/**
+ * Interface representing a Remote Config condition in the data-plane.
+ * A condition targets a specific group of users. A list of these conditions make up
+ * part of a Remote Config template.
+ */
+export interface RemoteConfigServerCondition {
+
+  /**
+   * A non-empty and unique name of this condition.
+   */
+  name: string;
+
+  /**
+   * The logic of this condition.
+   * See the documentation on
+   * {@link https://firebase.google.com/docs/remote-config/condition-reference | condition expressions}
+   * for the expected syntax of this field.
+   */
+  expression: string;
+}
+
 /**
  * Interface representing an explicit parameter value.
  */
@@ -135,7 +156,7 @@ export interface RemoteConfigParameterGroup {
 }
 
 /**
- * Interface representing a Remote Config template.
+ * Interface representing a Remote Config client template.
  */
 export interface RemoteConfigTemplate {
   /**
@@ -167,6 +188,58 @@ export interface RemoteConfigTemplate {
   version?: Version;
 }
 
+/**
+ * Interface representing the data in a Remote Config server template.
+ */
+export interface RemoteConfigServerTemplateData {
+  /**
+   * A list of conditions in descending order by priority.
+   */
+  conditions: RemoteConfigServerCondition[];
+
+  /**
+   * Map of parameter keys to their optional default values and optional conditional values.
+   */
+  parameters: { [key: string]: RemoteConfigParameter };
+
+  /**
+   * ETag of the current Remote Config template (readonly).
+   */
+  readonly etag: string;
+
+  /**
+   * Version information for the current Remote Config template.
+   */
+  version?: Version;
+}
+
+/**
+ * Interface representing a stateful abstraction for a Remote Config server template.
+ */
+export interface RemoteConfigServerTemplate {
+
+  /**
+   * Cached {@link RemoteConfigServerTemplateData}
+   */
+  cache: RemoteConfigServerTemplateData;
+
+  /**
+   * A {@link RemoteConfigServerConfig} containing default values for Config
+   */
+  defaultConfig: RemoteConfigServerConfig;
+
+  /**
+   * Evaluates the current template to produce a {@link RemoteConfigServerConfig}
+   */
+  evaluate(): RemoteConfigServerConfig;
+
+  /**
+   * Fetches and caches the current active version of the
+   * {@link RemoteConfigServerTemplate} of the project.
+   */
+  load(): Promise<void>;
+}
+
 /**
  * Interface representing a Remote Config user.
  */
@@ -289,3 +362,8 @@ export interface ListVersionsOptions {
    */
   endTime?: Date | string;
 }
+
+/**
+ * Type representing the configuration produced by evaluating a server template.
+ */
+export type RemoteConfigServerConfig = { [key: string]: string | boolean | number }

From aed5646cb74149ca1c57ba4994900afbbe50a102 Mon Sep 17 00:00:00 2001
From: Erik Eldridge <erikeldridge@google.com>
Date: Tue, 20 Feb 2024 08:51:00 -0800
Subject: [PATCH 02/13] Update SSRC API client (#2457)

Add API changes needed for SSRC

---------

Co-authored-by: Xin Wei <xinwei@google.com>
Co-authored-by: jen_h <harveyjen@google.com>
---
 .../remote-config-api-client-internal.ts      | 51 +++++++++++++++++--
 src/remote-config/remote-config-api.ts        | 20 ++++----
 .../remote-config-api-client.spec.ts          | 36 ++++++++++++-
 3 files changed, 92 insertions(+), 15 deletions(-)

diff --git a/src/remote-config/remote-config-api-client-internal.ts b/src/remote-config/remote-config-api-client-internal.ts
index b8cfe22fc4..6331eaa1b1 100644
--- a/src/remote-config/remote-config-api-client-internal.ts
+++ b/src/remote-config/remote-config-api-client-internal.ts
@@ -21,10 +21,19 @@ import { PrefixedFirebaseError } from '../utils/error';
 import * as utils from '../utils/index';
 import * as validator from '../utils/validator';
 import { deepCopy } from '../utils/deep-copy';
-import { ListVersionsOptions, ListVersionsResult, RemoteConfigTemplate } from './remote-config-api';
+import {
+  ListVersionsOptions,
+  ListVersionsResult,
+  RemoteConfigTemplate,
+  RemoteConfigServerTemplateData
+} from './remote-config-api';
 
 // Remote Config backend constants
-const FIREBASE_REMOTE_CONFIG_V1_API = 'https://firebaseremoteconfig.googleapis.com/v1';
+/**
+  * Allows the `FIREBASE_REMOTE_CONFIG_URL_BASE` environment
+  * variable to override the default API endpoint URL.
+  */
+const FIREBASE_REMOTE_CONFIG_URL_BASE = process.env.FIREBASE_REMOTE_CONFIG_URL_BASE || 'https://firebaseremoteconfig.googleapis.com';
 const FIREBASE_REMOTE_CONFIG_HEADERS = {
   'X-Firebase-Client': `fire-admin-node/${utils.getSdkVersion()}`,
   // There is a known issue in which the ETag is not properly returned in cases where the request
@@ -166,6 +175,24 @@ export class RemoteConfigApiClient {
       });
   }
 
+  public getServerTemplate(): Promise<RemoteConfigServerTemplateData> {
+    return this.getUrl()
+      .then((url) => {
+        const request: HttpRequestConfig = {
+          method: 'GET',
+          url: `${url}/namespaces/firebase-server/serverRemoteConfig`,
+          headers: FIREBASE_REMOTE_CONFIG_HEADERS
+        };
+        return this.httpClient.send(request);
+      })
+      .then((resp) => {
+        return this.toRemoteConfigServerTemplate(resp);
+      })
+      .catch((err) => {
+        throw this.toFirebaseError(err);
+      });
+  }
+
   private sendPutRequest(template: RemoteConfigTemplate, etag: string, validateOnly?: boolean): Promise<HttpResponse> {
     let path = 'remoteConfig';
     if (validateOnly) {
@@ -191,7 +218,7 @@ export class RemoteConfigApiClient {
   private getUrl(): Promise<string> {
     return this.getProjectIdPrefix()
       .then((projectIdPrefix) => {
-        return `${FIREBASE_REMOTE_CONFIG_V1_API}/${projectIdPrefix}`;
+        return `${FIREBASE_REMOTE_CONFIG_URL_BASE}/v1/${projectIdPrefix}`;
       });
   }
 
@@ -255,6 +282,24 @@ export class RemoteConfigApiClient {
     };
   }
 
+  /**
+   * Creates a RemoteConfigServerTemplate from the API response.
+   * If provided, customEtag is used instead of the etag returned in the API response.
+   *
+   * @param {HttpResponse} resp API response object.
+   * @param {string} customEtag A custom etag to replace the etag fom the API response (Optional).
+   */
+  private toRemoteConfigServerTemplate(resp: HttpResponse, customEtag?: string): RemoteConfigServerTemplateData {
+    const etag = (typeof customEtag === 'undefined') ? resp.headers['etag'] : customEtag;
+    this.validateEtag(etag);
+    return {
+      conditions: resp.data.conditions,
+      parameters: resp.data.parameters,
+      etag,
+      version: resp.data.version,
+    };
+  }
+
   /**
    * Checks if the given RemoteConfigTemplate object is valid.
    * The object must have valid parameters, parameter groups, conditions, and an etag.
diff --git a/src/remote-config/remote-config-api.ts b/src/remote-config/remote-config-api.ts
index dd9f641034..a27e29c81a 100644
--- a/src/remote-config/remote-config-api.ts
+++ b/src/remote-config/remote-config-api.ts
@@ -55,7 +55,7 @@ export interface RemoteConfigCondition {
 }
 
 /**
- * Interface representing a Remote Config condition in the data-plane.
+ * Represents a Remote Config condition in the dataplane.
  * A condition targets a specific group of users. A list of these conditions make up
  * part of a Remote Config template.
  */
@@ -156,7 +156,7 @@ export interface RemoteConfigParameterGroup {
 }
 
 /**
- * Interface representing a Remote Config client template.
+ * Represents a Remote Config client template.
  */
 export interface RemoteConfigTemplate {
   /**
@@ -189,7 +189,7 @@ export interface RemoteConfigTemplate {
 }
 
 /**
- * Interface representing the data in a Remote Config server template.
+ * Represents the data in a Remote Config server template.
  */
 export interface RemoteConfigServerTemplateData {
   /**
@@ -203,7 +203,7 @@ export interface RemoteConfigServerTemplateData {
   parameters: { [key: string]: RemoteConfigParameter };
 
   /**
-   * ETag of the current Remote Config template (readonly).
+   * Current Remote Config template ETag (read-only).
    */
   readonly etag: string;
 
@@ -214,28 +214,28 @@ export interface RemoteConfigServerTemplateData {
 }
 
 /**
- * Interface representing a stateful abstraction for a Remote Config server template.
+ * Represents a stateful abstraction for a Remote Config server template.
  */
 export interface RemoteConfigServerTemplate {
 
   /**
-   * Cached {@link RemoteConfigServerTemplateData}
+   * Cached {@link RemoteConfigServerTemplateData}.
    */
   cache: RemoteConfigServerTemplateData;
 
   /**
-   * A {@link RemoteConfigServerConfig} containing default values for Config
+   * A {@link RemoteConfigServerConfig} that contains default Config values.
    */
   defaultConfig: RemoteConfigServerConfig;
 
   /**
-   * Evaluates the current template to produce a {@link RemoteConfigServerConfig}
+   * Evaluates the current template to produce a {@link RemoteConfigServerConfig}.
    */
   evaluate(): RemoteConfigServerConfig;
 
   /**
    * Fetches and caches the current active version of the
-   * {@link RemoteConfigServerTemplate} of the project.
+   * project's {@link RemoteConfigServerTemplate}.
    */
   load(): Promise<void>;
 }
@@ -364,6 +364,6 @@ export interface ListVersionsOptions {
 }
 
 /**
- * Type representing the configuration produced by evaluating a server template.
+ * Represents the configuration produced by evaluating a server template.
  */
 export type RemoteConfigServerConfig = { [key: string]: string | boolean | number }
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 9c66f78a41..da2c87c639 100644
--- a/test/unit/remote-config/remote-config-api-client.spec.ts
+++ b/test/unit/remote-config/remote-config-api-client.spec.ts
@@ -33,6 +33,7 @@ import { getSdkVersion } from '../../../src/utils/index';
 import {
   RemoteConfigTemplate, Version, ListVersionsResult,
 } from '../../../src/remote-config/index';
+import { RemoteConfigServerTemplateData } from '../../../src/remote-config/remote-config-api';
 
 const expect = chai.expect;
 
@@ -661,6 +662,36 @@ describe('RemoteConfigApiClient', () => {
     });
   });
 
+  describe('getServerTemplate', () => {
+    it('should reject when project id is not available', () => {
+      return clientWithoutProjectId.getServerTemplate()
+        .should.eventually.be.rejectedWith(noProjectId);
+    });
+
+    // tests for api response validations
+    runEtagHeaderTests(() => apiClient.getServerTemplate());
+    runErrorResponseTests(() => apiClient.getServerTemplate());
+
+    it('should resolve with the latest template on success', () => {
+      const stub = sinon
+        .stub(HttpClient.prototype, 'send')
+        .resolves(utils.responseFrom(TEST_RESPONSE, 200, { etag: 'etag-123456789012-1' }));
+      stubs.push(stub);
+      return apiClient.getServerTemplate()
+        .then((resp) => {
+          expect(resp.conditions).to.deep.equal(TEST_RESPONSE.conditions);
+          expect(resp.parameters).to.deep.equal(TEST_RESPONSE.parameters);
+          expect(resp.etag).to.equal('etag-123456789012-1');
+          expect(resp.version).to.deep.equal(TEST_RESPONSE.version);
+          expect(stub).to.have.been.calledOnce.and.calledWith({
+            method: 'GET',
+            url: 'https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/namespaces/firebase-server/serverRemoteConfig',
+            headers: EXPECTED_HEADERS,
+          });
+        });
+    });
+  });
+
   function runTemplateVersionNumberTests(rcOperation: (v: string | number) => any): void {
     ['', null, NaN, true, [], {}].forEach((invalidVersion) => {
       it(`should reject if the versionNumber is: ${invalidVersion}`, () => {
@@ -677,7 +708,7 @@ describe('RemoteConfigApiClient', () => {
     });
   }
 
-  function runEtagHeaderTests(rcOperation: () => Promise<RemoteConfigTemplate>): void {
+  function runEtagHeaderTests(rcOperation: () => Promise<RemoteConfigTemplate | RemoteConfigServerTemplateData>): void {
     it('should reject when the etag is not present in the response', () => {
       const stub = sinon
         .stub(HttpClient.prototype, 'send')
@@ -690,7 +721,8 @@ describe('RemoteConfigApiClient', () => {
     });
   }
 
-  function runErrorResponseTests(rcOperation: () => Promise<RemoteConfigTemplate | ListVersionsResult>): void {
+  function runErrorResponseTests(
+    rcOperation: () => Promise<RemoteConfigTemplate | RemoteConfigServerTemplateData | ListVersionsResult>): void {
     it('should reject when a full platform error response is received', () => {
       const stub = sinon
         .stub(HttpClient.prototype, 'send')

From 5c9b6491d52ca76f1d243c7c0e5653915e29e0a2 Mon Sep 17 00:00:00 2001
From: Erik Eldridge <erikeldridge@google.com>
Date: Tue, 5 Mar 2024 14:14:49 -0800
Subject: [PATCH 03/13] Add public SSRC methods (#2458)

Add public SSRC methods

---------

Co-authored-by: Xin Wei <xinwei@google.com>
Co-authored-by: jen_h <harveyjen@google.com>
---
 etc/firebase-admin.remote-config.api.md       |  37 ++
 src/remote-config/index.ts                    |   5 +
 src/remote-config/remote-config-api.ts        |  21 +
 src/remote-config/remote-config.ts            | 165 +++++++
 test/unit/remote-config/remote-config.spec.ts | 460 ++++++++++++++++++
 5 files changed, 688 insertions(+)

diff --git a/etc/firebase-admin.remote-config.api.md b/etc/firebase-admin.remote-config.api.md
index fb07bfad76..614fc5dfeb 100644
--- a/etc/firebase-admin.remote-config.api.md
+++ b/etc/firebase-admin.remote-config.api.md
@@ -46,8 +46,10 @@ export class RemoteConfig {
     // (undocumented)
     readonly app: App;
     createTemplateFromJSON(json: string): RemoteConfigTemplate;
+    getServerTemplate(options?: RemoteConfigServerTemplateOptions): Promise<RemoteConfigServerTemplate>;
     getTemplate(): Promise<RemoteConfigTemplate>;
     getTemplateAtVersion(versionNumber: number | string): Promise<RemoteConfigTemplate>;
+    initServerTemplate(options?: RemoteConfigServerTemplateOptions): RemoteConfigServerTemplate;
     listVersions(options?: ListVersionsOptions): Promise<ListVersionsResult>;
     publishTemplate(template: RemoteConfigTemplate, options?: {
         force: boolean;
@@ -84,6 +86,41 @@ export interface RemoteConfigParameterGroup {
 // @public
 export type RemoteConfigParameterValue = ExplicitParameterValue | InAppDefaultValue;
 
+// @public
+export interface RemoteConfigServerCondition {
+    expression: string;
+    name: string;
+}
+
+// @public
+export type RemoteConfigServerConfig = {
+    [key: string]: string | boolean | number;
+};
+
+// @public
+export interface RemoteConfigServerTemplate {
+    cache: RemoteConfigServerTemplateData;
+    defaultConfig: RemoteConfigServerConfig;
+    evaluate(): RemoteConfigServerConfig;
+    load(): Promise<void>;
+}
+
+// @public
+export interface RemoteConfigServerTemplateData {
+    conditions: RemoteConfigServerCondition[];
+    readonly etag: string;
+    parameters: {
+        [key: string]: RemoteConfigParameter;
+    };
+    version?: Version;
+}
+
+// @public
+export interface RemoteConfigServerTemplateOptions {
+    defaultConfig?: RemoteConfigServerConfig;
+    template?: RemoteConfigServerTemplateData;
+}
+
 // @public
 export interface RemoteConfigTemplate {
     conditions: RemoteConfigCondition[];
diff --git a/src/remote-config/index.ts b/src/remote-config/index.ts
index e4719b2e43..194929641c 100644
--- a/src/remote-config/index.ts
+++ b/src/remote-config/index.ts
@@ -35,6 +35,11 @@ export {
   RemoteConfigParameterGroup,
   RemoteConfigParameterValue,
   RemoteConfigTemplate,
+  RemoteConfigServerCondition,
+  RemoteConfigServerConfig,
+  RemoteConfigServerTemplate,
+  RemoteConfigServerTemplateData,
+  RemoteConfigServerTemplateOptions,
   RemoteConfigUser,
   TagColor,
   Version,
diff --git a/src/remote-config/remote-config-api.ts b/src/remote-config/remote-config-api.ts
index a27e29c81a..a5d0287d80 100644
--- a/src/remote-config/remote-config-api.ts
+++ b/src/remote-config/remote-config-api.ts
@@ -213,6 +213,27 @@ export interface RemoteConfigServerTemplateData {
   version?: Version;
 }
 
+/**
+ * Represents optional arguments that can be used when instantiating {@link RemoteConfigServerTemplate}.
+ */
+export interface RemoteConfigServerTemplateOptions {
+
+  /**
+   * Defines in-app default parameter values, so that your app behaves as
+   * intended before it connects to the Remote Config backend, and so that
+   * default values are available if none are set on the backend.
+   */
+  defaultConfig?: RemoteConfigServerConfig,
+
+  /**
+   * Enables integrations to use template data loaded independently. For
+   * example, customers can reduce initialization latency by pre-fetching and
+   * caching template data and then using this option to initialize the SDK with
+   * that data.
+   */
+  template?: RemoteConfigServerTemplateData,
+}
+
 /**
  * Represents a stateful abstraction for a Remote Config server template.
  */
diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts
index 27cbd05793..cfd965d27b 100644
--- a/src/remote-config/remote-config.ts
+++ b/src/remote-config/remote-config.ts
@@ -23,9 +23,16 @@ import {
   RemoteConfigCondition,
   RemoteConfigParameter,
   RemoteConfigParameterGroup,
+  RemoteConfigServerTemplate,
   RemoteConfigTemplate,
   RemoteConfigUser,
   Version,
+  ExplicitParameterValue,
+  InAppDefaultValue,
+  ParameterValueType,
+  RemoteConfigServerConfig,
+  RemoteConfigServerTemplateData,
+  RemoteConfigServerTemplateOptions,
 } from './remote-config-api';
 
 /**
@@ -168,6 +175,27 @@ export class RemoteConfig {
 
     return new RemoteConfigTemplateImpl(template);
   }
+
+  /**
+   * Instantiates {@link RemoteConfigServerTemplate} and then fetches and caches the latest
+   * template version of the project.
+   */
+  public async getServerTemplate(options?: RemoteConfigServerTemplateOptions): Promise<RemoteConfigServerTemplate> {
+    const template = this.initServerTemplate(options);
+    await template.load();
+    return template;
+  }
+
+  /**
+   * Synchronously instantiates {@link RemoteConfigServerTemplate}.
+   */
+  public initServerTemplate(options?: RemoteConfigServerTemplateOptions): RemoteConfigServerTemplate {
+    const template = new RemoteConfigServerTemplateImpl(this.client, options?.defaultConfig);
+    if (options?.template) {
+      template.cache = options?.template;
+    }
+    return template;
+  }
 }
 
 /**
@@ -254,6 +282,143 @@ class RemoteConfigTemplateImpl implements RemoteConfigTemplate {
   }
 }
 
+/**
+ * Remote Config dataplane template data implementation.
+ */
+class RemoteConfigServerTemplateImpl implements RemoteConfigServerTemplate {
+  public cache: RemoteConfigServerTemplateData;
+
+  constructor(
+    private readonly apiClient: RemoteConfigApiClient,
+    public readonly defaultConfig: RemoteConfigServerConfig = {}
+  ) { }
+
+  /**
+   * Fetches and caches the current active version of the project's {@link RemoteConfigServerTemplate}.
+   */
+  public load(): Promise<void> {
+    return this.apiClient.getServerTemplate()
+      .then((template) => {
+        this.cache = new RemoteConfigServerTemplateDataImpl(template);
+      });
+  }
+
+  /**
+   * Evaluates the current template in cache to produce a {@link RemoteConfigServerConfig}.
+   */
+  public evaluate(): RemoteConfigServerConfig {
+    if (!this.cache) {
+      throw new FirebaseRemoteConfigError(
+        'failed-precondition',
+        'No Remote Config Server template in cache. Call load() before calling evaluate().');
+    }
+
+    const evaluatedConfig: RemoteConfigServerConfig = {};
+
+    for (const [key, parameter] of Object.entries(this.cache.parameters)) {
+      const { defaultValue, valueType } = parameter;
+
+      if (!defaultValue) {
+        // TODO: add logging once we have a wrapped logger.
+        continue;
+      }
+
+      if ((defaultValue as InAppDefaultValue).useInAppDefault) {
+        // TODO: add logging once we have a wrapped logger.
+        continue;
+      }
+
+      const parameterDefaultValue = (defaultValue as ExplicitParameterValue).value;
+
+      evaluatedConfig[key] = this.parseRemoteConfigParameterValue(valueType, parameterDefaultValue);
+    }
+
+    // Merges rendered config over default config.
+    const mergedConfig = Object.assign(this.defaultConfig, evaluatedConfig);
+
+    // Enables config to be a convenient object, but with the ability to perform additional
+    // functionality when a value is retrieved.
+    const proxyHandler = {
+      get(target: RemoteConfigServerConfig, prop: string) {
+        return target[prop];
+      }
+    };
+
+    return new Proxy(mergedConfig, proxyHandler);
+  }
+
+  /**
+   * Private helper method that processes and parses a parameter value based on {@link ParameterValueType}.
+   */
+  private parseRemoteConfigParameterValue(parameterType: ParameterValueType | undefined,
+    parameterDefaultValue: string): string | number | boolean {
+    const BOOLEAN_TRUTHY_VALUES = ['1', 'true', 't', 'yes', 'y', 'on'];
+    const DEFAULT_VALUE_FOR_NUMBER = 0;
+    const DEFAULT_VALUE_FOR_STRING = '';
+
+    if (parameterType === 'BOOLEAN') {
+      return BOOLEAN_TRUTHY_VALUES.indexOf(parameterDefaultValue) >= 0;
+    } else if (parameterType === 'NUMBER') {
+      const num = Number(parameterDefaultValue);
+      if (isNaN(num)) {
+        return DEFAULT_VALUE_FOR_NUMBER;
+      }
+      return num;
+    } else {
+      // Treat everything else as string
+      return parameterDefaultValue || DEFAULT_VALUE_FOR_STRING;
+    }
+  }
+}
+
+/**
+ * Remote Config dataplane template data implementation.
+ */
+class RemoteConfigServerTemplateDataImpl implements RemoteConfigServerTemplateData {
+  public parameters: { [key: string]: RemoteConfigParameter };
+  public parameterGroups: { [key: string]: RemoteConfigParameterGroup };
+  public conditions: RemoteConfigCondition[];
+  public readonly etag: string;
+  public version?: Version;
+
+  constructor(template: RemoteConfigServerTemplateData) {
+    if (!validator.isNonNullObject(template) ||
+      !validator.isNonEmptyString(template.etag)) {
+      throw new FirebaseRemoteConfigError(
+        'invalid-argument',
+        `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');
+      }
+      this.parameters = template.parameters;
+    } else {
+      this.parameters = {};
+    }
+
+    if (typeof template.conditions !== 'undefined') {
+      if (!validator.isArray(template.conditions)) {
+        throw new FirebaseRemoteConfigError(
+          'invalid-argument',
+          'Remote Config conditions must be an array');
+      }
+      this.conditions = template.conditions;
+    } else {
+      this.conditions = [];
+    }
+
+    if (typeof template.version !== 'undefined') {
+      this.version = new VersionImpl(template.version);
+    }
+  }
+}
+
 /**
 * Remote Config Version internal implementation.
 */
diff --git a/test/unit/remote-config/remote-config.spec.ts b/test/unit/remote-config/remote-config.spec.ts
index 5459ecd90c..71fcf87f84 100644
--- a/test/unit/remote-config/remote-config.spec.ts
+++ b/test/unit/remote-config/remote-config.spec.ts
@@ -34,6 +34,9 @@ import {
   RemoteConfigApiClient
 } from '../../../src/remote-config/remote-config-api-client-internal';
 import { deepCopy } from '../../../src/utils/deep-copy';
+import {
+  RemoteConfigServerCondition, RemoteConfigServerTemplate, RemoteConfigServerTemplateData
+} from '../../../src/remote-config/remote-config-api';
 
 const expect = chai.expect;
 
@@ -98,6 +101,34 @@ describe('RemoteConfig', () => {
     version: VERSION_INFO,
   };
 
+  const SERVER_REMOTE_CONFIG_RESPONSE: {
+    // This type is effectively a RemoteConfigServerTemplate, but with mutable fields
+    // to allow easier use from within the tests. An improvement would be to
+    // alter this into a helper that creates customized RemoteConfigTemplateContent based
+    // on the needs of the test, as that would ensure type-safety.
+    conditions?: Array<{ name: string; expression: string; }>;
+    parameters?: object | null;
+    etag: string;
+    version?: object;
+  } = {
+    conditions: [
+      {
+        name: 'ios',
+        expression: 'device.os == \'ios\''
+      },
+    ],
+    parameters: {
+      holiday_promo_enabled: {
+        defaultValue: { value: 'true' },
+        conditionalValues: { ios: { useInAppDefault: true } },
+        description: 'this is a promo',
+        valueType: 'BOOLEAN',
+      },
+    },
+    etag: 'etag-123456789012-5',
+    version: VERSION_INFO,
+  };
+
   const REMOTE_CONFIG_TEMPLATE: RemoteConfigTemplate = {
     conditions: [{
       name: 'ios',
@@ -511,6 +542,435 @@ describe('RemoteConfig', () => {
     });
   });
 
+  describe('getServerTemplate', () => {
+    const operationName = 'getServerTemplate';
+
+    it('should propagate API errors', () => {
+      const stub = sinon
+        .stub(RemoteConfigApiClient.prototype, operationName)
+        .rejects(INTERNAL_ERROR);
+      stubs.push(stub);
+
+      return remoteConfig.getServerTemplate().should.eventually.be.rejected.and.deep.equal(INTERNAL_ERROR);
+    });
+
+    it('should resolve a server template on success', () => {
+      const stub = sinon
+        .stub(RemoteConfigApiClient.prototype, operationName)
+        .resolves(SERVER_REMOTE_CONFIG_RESPONSE as RemoteConfigServerTemplateData);
+      stubs.push(stub);
+
+      return remoteConfig.getServerTemplate()
+        .then((template) => {
+          expect(template.cache.conditions.length).to.equal(1);
+          expect(template.cache.conditions[0].name).to.equal('ios');
+          expect(template.cache.conditions[0].expression).to.equal('device.os == \'ios\'');
+          expect(template.cache.etag).to.equal('etag-123456789012-5');
+
+          const version = template.cache.version!;
+          expect(version.versionNumber).to.equal('86');
+          expect(version.updateOrigin).to.equal('ADMIN_SDK_NODE');
+          expect(version.updateType).to.equal('INCREMENTAL_UPDATE');
+          expect(version.updateUser).to.deep.equal({
+            email: 'firebase-adminsdk@gserviceaccount.com'
+          });
+          expect(version.description).to.equal('production version');
+          expect(version.updateTime).to.equal('Mon, 15 Jun 2020 16:45:03 GMT');
+
+          const key = 'holiday_promo_enabled';
+          const p1 = template.cache.parameters[key];
+          expect(p1.defaultValue).deep.equals({ value: 'true' });
+          expect(p1.conditionalValues).deep.equals({ ios: { useInAppDefault: true } });
+          expect(p1.description).equals('this is a promo');
+          expect(p1.valueType).equals('BOOLEAN');
+
+          const c = template.cache.conditions.find((c) => c.name === 'ios');
+          expect(c).to.be.not.undefined;
+          const cond = c as RemoteConfigServerCondition;
+          expect(cond.name).to.equal('ios');
+          expect(cond.expression).to.equal('device.os == \'ios\'');
+
+          const parsed = JSON.parse(JSON.stringify(template.cache));
+          const expectedTemplate = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE);
+          const expectedVersion = deepCopy(VERSION_INFO);
+          expectedVersion.updateTime = new Date(expectedVersion.updateTime).toUTCString();
+          expectedTemplate.version = expectedVersion;
+          expect(parsed).deep.equals(expectedTemplate);
+        });
+    });
+
+    it('should set defaultConfig when passed', () => {
+      const defaultConfig = {
+        holiday_promo_enabled: false,
+        holiday_promo_discount: 20,
+      };
+
+      const stub = sinon
+        .stub(RemoteConfigApiClient.prototype, operationName)
+        .resolves(SERVER_REMOTE_CONFIG_RESPONSE as RemoteConfigServerTemplateData);
+      stubs.push(stub);
+
+      return remoteConfig.getServerTemplate({ defaultConfig })
+        .then((template) => {
+          expect(template.defaultConfig.holiday_promo_enabled).to.equal(false);
+          expect(template.defaultConfig.holiday_promo_discount).to.equal(20);
+        });
+    });
+  });
+
+  describe('initServerTemplate', () => {
+    it('should set and instantiates template when passed', () => {
+      const template = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as RemoteConfigServerTemplateData;
+      template.parameters = {
+        dog_type: {
+          defaultValue: {
+            value: 'shiba'
+          },
+          description: 'Type of dog breed',
+          valueType: 'STRING'
+        }
+      };
+      const initializedTemplate = remoteConfig.initServerTemplate({ template }).cache;
+      const parsed = JSON.parse(JSON.stringify(initializedTemplate));
+      expect(parsed).deep.equals(deepCopy(template));
+    });
+  });
+
+  describe('RemoteConfigServerTemplate', () => {
+    const SERVER_REMOTE_CONFIG_RESPONSE_2 = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE);
+    SERVER_REMOTE_CONFIG_RESPONSE_2.parameters = {
+      dog_type: {
+        defaultValue: {
+          value: 'corgi'
+        },
+        description: 'Type of dog breed',
+        valueType: 'STRING'
+      },
+      dog_type_enabled: {
+        defaultValue: {
+          value: 'true'
+        },
+        description: 'It\'s true or false',
+        valueType: 'BOOLEAN'
+      },
+      dog_age: {
+        defaultValue: {
+          value: '22'
+        },
+        description: 'Age',
+        valueType: 'NUMBER'
+      },
+      dog_jsonified: {
+        defaultValue: {
+          value: '{"name":"Taro","breed":"Corgi","age":1,"fluffiness":100}'
+        },
+        description: 'Dog Json Response',
+        valueType: 'JSON'
+      },
+      dog_use_inapp_default: {
+        defaultValue: {
+          useInAppDefault: true
+        },
+        description: 'Use in-app default dog',
+        valueType: 'STRING'
+      },
+      dog_no_remote_default_value: {
+        description: 'TIL: default values are optional!',
+        valueType: 'STRING'
+      }
+    };
+
+    describe('load', () => {
+      const operationName = 'getServerTemplate';
+
+      it('should propagate API errors', () => {
+        const stub = sinon
+          .stub(RemoteConfigApiClient.prototype, operationName)
+          .rejects(INTERNAL_ERROR);
+        stubs.push(stub);
+
+        return remoteConfig.getServerTemplate().should.eventually.be.rejected.and.deep.equal(INTERNAL_ERROR);
+      });
+
+      it('should reject when API response is invalid', () => {
+        const stub = sinon
+          .stub(RemoteConfigApiClient.prototype, operationName)
+          .resolves(undefined);
+        stubs.push(stub);
+        return remoteConfig.getServerTemplate().should.eventually.be.rejected.and.have.property(
+          'message', 'Invalid Remote Config template: undefined');
+      });
+
+      it('should reject when API response does not contain an ETag', () => {
+        const response = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE);
+        response.etag = '';
+        const stub = sinon
+          .stub(RemoteConfigApiClient.prototype, operationName)
+          .resolves(response as RemoteConfigServerTemplateData);
+        stubs.push(stub);
+        return remoteConfig.getServerTemplate()
+          .should.eventually.be.rejected.and.have.property(
+            'message', `Invalid Remote Config template: ${JSON.stringify(response)}`);
+      });
+
+      it('should reject when API response does not contain valid parameters', () => {
+        const response = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE);
+        response.parameters = null;
+        const stub = sinon
+          .stub(RemoteConfigApiClient.prototype, operationName)
+          .resolves(response as RemoteConfigServerTemplateData);
+        stubs.push(stub);
+        return remoteConfig.getServerTemplate()
+          .should.eventually.be.rejected.and.have.property(
+            'message', 'Remote Config parameters must be a non-null object');
+      });
+
+      it('should reject when API response does not contain valid conditions', () => {
+        const response = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE);
+        response.conditions = Object();
+        const stub = sinon
+          .stub(RemoteConfigApiClient.prototype, operationName)
+          .resolves(response as RemoteConfigServerTemplateData);
+        stubs.push(stub);
+        return remoteConfig.getServerTemplate()
+          .should.eventually.be.rejected.and.have.property(
+            'message', 'Remote Config conditions must be an array');
+      });
+
+      it('should resolve with parameters:{} when no parameters present in the response', () => {
+        const response = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE);
+        response.parameters = undefined;
+        const stub = sinon
+          .stub(RemoteConfigApiClient.prototype, operationName)
+          .resolves(response as RemoteConfigServerTemplateData);
+        stubs.push(stub);
+        return remoteConfig.getServerTemplate()
+          .then((template) => {
+            // If parameters are not present in the response, we set it to an empty object.
+            expect(template.cache.parameters).deep.equals({});
+          });
+      });
+
+      it('should resolve with conditions:[] when no conditions present in the response', () => {
+        const response = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE);
+        response.conditions = undefined;
+        const stub = sinon
+          .stub(RemoteConfigApiClient.prototype, operationName)
+          .resolves(response as RemoteConfigServerTemplateData);
+        stubs.push(stub);
+        return remoteConfig.getServerTemplate()
+          .then((template) => {
+            // If conditions are not present in the response, we set it to an empty array.
+            expect(template.cache.conditions).deep.equals([]);
+          });
+      });
+
+      it('should resolve a server template on success', () => {
+        const stub = sinon
+          .stub(RemoteConfigApiClient.prototype, operationName)
+          .resolves(SERVER_REMOTE_CONFIG_RESPONSE as RemoteConfigServerTemplateData);
+        stubs.push(stub);
+
+        return remoteConfig.getServerTemplate()
+          .then((template) => {
+            expect(template.cache.conditions.length).to.equal(1);
+            expect(template.cache.conditions[0].name).to.equal('ios');
+            expect(template.cache.conditions[0].expression).to.equal('device.os == \'ios\'');
+            expect(template.cache.etag).to.equal('etag-123456789012-5');
+
+            const version = template.cache.version!;
+            expect(version.versionNumber).to.equal('86');
+            expect(version.updateOrigin).to.equal('ADMIN_SDK_NODE');
+            expect(version.updateType).to.equal('INCREMENTAL_UPDATE');
+            expect(version.updateUser).to.deep.equal({
+              email: 'firebase-adminsdk@gserviceaccount.com'
+            });
+            expect(version.description).to.equal('production version');
+            expect(version.updateTime).to.equal('Mon, 15 Jun 2020 16:45:03 GMT');
+
+            const key = 'holiday_promo_enabled';
+            const p1 = template.cache.parameters[key];
+            expect(p1.defaultValue).deep.equals({ value: 'true' });
+            expect(p1.conditionalValues).deep.equals({ ios: { useInAppDefault: true } });
+            expect(p1.description).equals('this is a promo');
+            expect(p1.valueType).equals('BOOLEAN');
+
+            const c = template.cache.conditions.find((c) => c.name === 'ios');
+            expect(c).to.be.not.undefined;
+            const cond = c as RemoteConfigServerCondition;
+            expect(cond.name).to.equal('ios');
+            expect(cond.expression).to.equal('device.os == \'ios\'');
+
+            const parsed = JSON.parse(JSON.stringify(template.cache));
+            const expectedTemplate = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE);
+            const expectedVersion = deepCopy(VERSION_INFO);
+            expectedVersion.updateTime = new Date(expectedVersion.updateTime).toUTCString();
+            expectedTemplate.version = expectedVersion;
+            expect(parsed).deep.equals(expectedTemplate);
+          });
+      });
+
+      it('should resolve with template when Version updateTime contains 3 digits in fractional seconds', () => {
+        const response = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE);
+        const versionInfo = deepCopy(VERSION_INFO);
+        versionInfo.updateTime = '2020-10-03T17:14:10.203Z';
+        response.version = versionInfo;
+        const stub = sinon
+          .stub(RemoteConfigApiClient.prototype, operationName)
+          .resolves(response as RemoteConfigServerTemplateData);
+        stubs.push(stub);
+
+        return remoteConfig.getServerTemplate()
+          .then((template) => {
+            expect(template.cache.etag).to.equal('etag-123456789012-5');
+
+            const version = template.cache.version!;
+            expect(version.versionNumber).to.equal('86');
+            expect(version.updateOrigin).to.equal('ADMIN_SDK_NODE');
+            expect(version.updateType).to.equal('INCREMENTAL_UPDATE');
+            expect(version.updateUser).to.deep.equal({
+              email: 'firebase-adminsdk@gserviceaccount.com'
+            });
+            expect(version.description).to.equal('production version');
+            expect(version.updateTime).to.equal('Sat, 03 Oct 2020 17:14:10 GMT');
+          });
+      });
+
+      it('should resolve with template when Version updateTime contains 6 digits in fractional seconds', () => {
+        const response = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE);
+        const versionInfo = deepCopy(VERSION_INFO);
+        versionInfo.updateTime = '2020-08-14T17:01:36.541527Z';
+        response.version = versionInfo;
+        const stub = sinon
+          .stub(RemoteConfigApiClient.prototype, operationName)
+          .resolves(response as RemoteConfigServerTemplateData);
+        stubs.push(stub);
+
+        return remoteConfig.getServerTemplate()
+          .then((template) => {
+            expect(template.cache.etag).to.equal('etag-123456789012-5');
+
+            const version = template.cache.version!;
+            expect(version.versionNumber).to.equal('86');
+            expect(version.updateOrigin).to.equal('ADMIN_SDK_NODE');
+            expect(version.updateType).to.equal('INCREMENTAL_UPDATE');
+            expect(version.updateUser).to.deep.equal({
+              email: 'firebase-adminsdk@gserviceaccount.com'
+            });
+            expect(version.description).to.equal('production version');
+            expect(version.updateTime).to.equal('Fri, 14 Aug 2020 17:01:36 GMT');
+          });
+      });
+
+      it('should resolve with template when Version updateTime contains 9 digits in fractional seconds', () => {
+        const response = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE);
+        const versionInfo = deepCopy(VERSION_INFO);
+        versionInfo.updateTime = '2020-11-15T06:57:26.342763941Z';
+        response.version = versionInfo;
+        const stub = sinon
+          .stub(RemoteConfigApiClient.prototype, operationName)
+          .resolves(response as RemoteConfigServerTemplateData);
+        stubs.push(stub);
+
+        return remoteConfig.getServerTemplate()
+          .then((template) => {
+            expect(template.cache.etag).to.equal('etag-123456789012-5');
+
+            const version = template.cache.version!;
+            expect(version.versionNumber).to.equal('86');
+            expect(version.updateOrigin).to.equal('ADMIN_SDK_NODE');
+            expect(version.updateType).to.equal('INCREMENTAL_UPDATE');
+            expect(version.updateUser).to.deep.equal({
+              email: 'firebase-adminsdk@gserviceaccount.com'
+            });
+            expect(version.description).to.equal('production version');
+            expect(version.updateTime).to.equal('Sun, 15 Nov 2020 06:57:26 GMT');
+          });
+      });
+    });
+
+    describe('evaluate', () => {
+      it('returns a config when template is present in cache', () => {
+        const stub = sinon
+          .stub(RemoteConfigApiClient.prototype, 'getServerTemplate')
+          .resolves(SERVER_REMOTE_CONFIG_RESPONSE_2 as RemoteConfigServerTemplateData);
+        stubs.push(stub);
+        return remoteConfig.getServerTemplate()
+          .then((template: RemoteConfigServerTemplate) => {
+            const config = template.evaluate!();
+            expect(config.dog_type).to.equal('corgi');
+            expect(config.dog_type_enabled).to.equal(true);
+            expect(config.dog_age).to.equal(22);
+            expect(config.dog_jsonified).to.equal('{"name":"Taro","breed":"Corgi","age":1,"fluffiness":100}');
+          });
+      });
+
+      it('uses local default if parameter not in template', () => {
+        const stub = sinon
+          .stub(RemoteConfigApiClient.prototype, 'getServerTemplate')
+          .resolves(SERVER_REMOTE_CONFIG_RESPONSE_2 as RemoteConfigServerTemplateData);
+        stubs.push(stub);
+        return remoteConfig.getServerTemplate({
+          defaultConfig: {
+            dog_coat: 'blue merle',
+          }
+        })
+          .then((template: RemoteConfigServerTemplate) => {
+            const config = template.evaluate!();
+            expect(config.dog_coat).to.equal(template.defaultConfig.dog_coat);
+          });
+      });
+
+      it('uses local default when parameter is in template but default value is undefined', () => {
+        const stub = sinon
+          .stub(RemoteConfigApiClient.prototype, 'getServerTemplate')
+          .resolves(SERVER_REMOTE_CONFIG_RESPONSE_2 as RemoteConfigServerTemplateData);
+        stubs.push(stub);
+        return remoteConfig.getServerTemplate({
+          defaultConfig: {
+            dog_no_remote_default_value: 'local default'
+          }
+        })
+          .then((template: RemoteConfigServerTemplate) => {
+            const config = template.evaluate!();
+            expect(config.dog_no_remote_default_value).to.equal(template.defaultConfig.dog_no_remote_default_value);
+          });
+      });
+
+      it('uses local default when in-app default value specified', () => {
+        const stub = sinon
+          .stub(RemoteConfigApiClient.prototype, 'getServerTemplate')
+          .resolves(SERVER_REMOTE_CONFIG_RESPONSE_2 as RemoteConfigServerTemplateData);
+        stubs.push(stub);
+        return remoteConfig.getServerTemplate({
+          defaultConfig: {
+            dog_use_inapp_default: '🐕'
+          }
+        })
+          .then((template: RemoteConfigServerTemplate) => {
+            const config = template.evaluate!();
+            expect(config.dog_use_inapp_default).to.equal(template.defaultConfig.dog_use_inapp_default);
+          });
+      });
+
+      it('overrides local default when value exists', () => {
+        const stub = sinon
+          .stub(RemoteConfigApiClient.prototype, 'getServerTemplate')
+          .resolves(SERVER_REMOTE_CONFIG_RESPONSE_2 as RemoteConfigServerTemplateData);
+        stubs.push(stub);
+        return remoteConfig.getServerTemplate({
+          defaultConfig: {
+            dog_type_enabled: false
+          }
+        })
+          .then((template: RemoteConfigServerTemplate) => {
+            const config = template.evaluate!();
+            expect(config.dog_type_enabled).to.equal(template.defaultConfig.dog_type_enabled);
+          });
+      });
+    });
+  });
+
   function runInvalidResponseTests(rcOperation: () => Promise<RemoteConfigTemplate>,
     operationName: any): void {
     it('should propagate API errors', () => {

From f89632a880e2e95b20d93cc5389b004c4a6c9b74 Mon Sep 17 00:00:00 2001
From: Erik Eldridge <erikeldridge@google.com>
Date: Thu, 21 Mar 2024 13:22:25 -0700
Subject: [PATCH 04/13] Remove product prefix from SSRC types (#2496)

We have a Firebase AIP to avoid these prefixes.
---
 etc/firebase-admin.remote-config.api.md       | 69 +++++++++----------
 src/remote-config/index.ts                    | 10 +--
 .../remote-config-api-client-internal.ts      |  6 +-
 src/remote-config/remote-config-api.ts        | 40 +++++------
 src/remote-config/remote-config.ts            | 43 ++++++------
 .../remote-config-api-client.spec.ts          |  6 +-
 test/unit/remote-config/remote-config.spec.ts | 59 ++++++++--------
 7 files changed, 110 insertions(+), 123 deletions(-)

diff --git a/etc/firebase-admin.remote-config.api.md b/etc/firebase-admin.remote-config.api.md
index 614fc5dfeb..98c1883ab1 100644
--- a/etc/firebase-admin.remote-config.api.md
+++ b/etc/firebase-admin.remote-config.api.md
@@ -38,6 +38,11 @@ export interface ListVersionsResult {
     versions: Version[];
 }
 
+// @public
+export interface NamedCondition {
+    name: string;
+}
+
 // @public
 export type ParameterValueType = 'STRING' | 'BOOLEAN' | 'NUMBER' | 'JSON';
 
@@ -46,10 +51,10 @@ export class RemoteConfig {
     // (undocumented)
     readonly app: App;
     createTemplateFromJSON(json: string): RemoteConfigTemplate;
-    getServerTemplate(options?: RemoteConfigServerTemplateOptions): Promise<RemoteConfigServerTemplate>;
+    getServerTemplate(options?: ServerTemplateOptions): Promise<ServerTemplate>;
     getTemplate(): Promise<RemoteConfigTemplate>;
     getTemplateAtVersion(versionNumber: number | string): Promise<RemoteConfigTemplate>;
-    initServerTemplate(options?: RemoteConfigServerTemplateOptions): RemoteConfigServerTemplate;
+    initServerTemplate(options?: ServerTemplateOptions): ServerTemplate;
     listVersions(options?: ListVersionsOptions): Promise<ListVersionsResult>;
     publishTemplate(template: RemoteConfigTemplate, options?: {
         force: boolean;
@@ -87,28 +92,12 @@ export interface RemoteConfigParameterGroup {
 export type RemoteConfigParameterValue = ExplicitParameterValue | InAppDefaultValue;
 
 // @public
-export interface RemoteConfigServerCondition {
-    expression: string;
-    name: string;
-}
-
-// @public
-export type RemoteConfigServerConfig = {
-    [key: string]: string | boolean | number;
-};
-
-// @public
-export interface RemoteConfigServerTemplate {
-    cache: RemoteConfigServerTemplateData;
-    defaultConfig: RemoteConfigServerConfig;
-    evaluate(): RemoteConfigServerConfig;
-    load(): Promise<void>;
-}
-
-// @public
-export interface RemoteConfigServerTemplateData {
-    conditions: RemoteConfigServerCondition[];
+export interface RemoteConfigTemplate {
+    conditions: RemoteConfigCondition[];
     readonly etag: string;
+    parameterGroups: {
+        [key: string]: RemoteConfigParameterGroup;
+    };
     parameters: {
         [key: string]: RemoteConfigParameter;
     };
@@ -116,18 +105,29 @@ export interface RemoteConfigServerTemplateData {
 }
 
 // @public
-export interface RemoteConfigServerTemplateOptions {
-    defaultConfig?: RemoteConfigServerConfig;
-    template?: RemoteConfigServerTemplateData;
+export interface RemoteConfigUser {
+    email: string;
+    imageUrl?: string;
+    name?: string;
 }
 
 // @public
-export interface RemoteConfigTemplate {
-    conditions: RemoteConfigCondition[];
+export type ServerConfig = {
+    [key: string]: string | boolean | number;
+};
+
+// @public
+export interface ServerTemplate {
+    cache: ServerTemplateData;
+    defaultConfig: ServerConfig;
+    evaluate(): ServerConfig;
+    load(): Promise<void>;
+}
+
+// @public
+export interface ServerTemplateData {
+    conditions: NamedCondition[];
     readonly etag: string;
-    parameterGroups: {
-        [key: string]: RemoteConfigParameterGroup;
-    };
     parameters: {
         [key: string]: RemoteConfigParameter;
     };
@@ -135,10 +135,9 @@ export interface RemoteConfigTemplate {
 }
 
 // @public
-export interface RemoteConfigUser {
-    email: string;
-    imageUrl?: string;
-    name?: string;
+export interface ServerTemplateOptions {
+    defaultConfig?: ServerConfig;
+    template?: ServerTemplateData;
 }
 
 // @public
diff --git a/src/remote-config/index.ts b/src/remote-config/index.ts
index 194929641c..aa09a8e18a 100644
--- a/src/remote-config/index.ts
+++ b/src/remote-config/index.ts
@@ -29,18 +29,18 @@ export {
   InAppDefaultValue,
   ListVersionsOptions,
   ListVersionsResult,
+  NamedCondition,
   ParameterValueType,
   RemoteConfigCondition,
   RemoteConfigParameter,
   RemoteConfigParameterGroup,
   RemoteConfigParameterValue,
   RemoteConfigTemplate,
-  RemoteConfigServerCondition,
-  RemoteConfigServerConfig,
-  RemoteConfigServerTemplate,
-  RemoteConfigServerTemplateData,
-  RemoteConfigServerTemplateOptions,
   RemoteConfigUser,
+  ServerConfig,
+  ServerTemplate,
+  ServerTemplateData,
+  ServerTemplateOptions,
   TagColor,
   Version,
 } from './remote-config-api';
diff --git a/src/remote-config/remote-config-api-client-internal.ts b/src/remote-config/remote-config-api-client-internal.ts
index 6331eaa1b1..f1a0ad1c10 100644
--- a/src/remote-config/remote-config-api-client-internal.ts
+++ b/src/remote-config/remote-config-api-client-internal.ts
@@ -25,7 +25,7 @@ import {
   ListVersionsOptions,
   ListVersionsResult,
   RemoteConfigTemplate,
-  RemoteConfigServerTemplateData
+  ServerTemplateData
 } from './remote-config-api';
 
 // Remote Config backend constants
@@ -175,7 +175,7 @@ export class RemoteConfigApiClient {
       });
   }
 
-  public getServerTemplate(): Promise<RemoteConfigServerTemplateData> {
+  public getServerTemplate(): Promise<ServerTemplateData> {
     return this.getUrl()
       .then((url) => {
         const request: HttpRequestConfig = {
@@ -289,7 +289,7 @@ export class RemoteConfigApiClient {
    * @param {HttpResponse} resp API response object.
    * @param {string} customEtag A custom etag to replace the etag fom the API response (Optional).
    */
-  private toRemoteConfigServerTemplate(resp: HttpResponse, customEtag?: string): RemoteConfigServerTemplateData {
+  private toRemoteConfigServerTemplate(resp: HttpResponse, customEtag?: string): ServerTemplateData {
     const etag = (typeof customEtag === 'undefined') ? resp.headers['etag'] : customEtag;
     this.validateEtag(etag);
     return {
diff --git a/src/remote-config/remote-config-api.ts b/src/remote-config/remote-config-api.ts
index a5d0287d80..2ad15eedd5 100644
--- a/src/remote-config/remote-config-api.ts
+++ b/src/remote-config/remote-config-api.ts
@@ -59,20 +59,12 @@ export interface RemoteConfigCondition {
  * A condition targets a specific group of users. A list of these conditions make up
  * part of a Remote Config template.
  */
-export interface RemoteConfigServerCondition {
+export interface NamedCondition {
 
   /**
    * A non-empty and unique name of this condition.
    */
   name: string;
-
-  /**
-   * The logic of this condition.
-   * See the documentation on
-   * {@link https://firebase.google.com/docs/remote-config/condition-reference | condition expressions}
-   * for the expected syntax of this field.
-   */
-  expression: string;
 }
 
 /**
@@ -191,11 +183,11 @@ export interface RemoteConfigTemplate {
 /**
  * Represents the data in a Remote Config server template.
  */
-export interface RemoteConfigServerTemplateData {
+export interface ServerTemplateData {
   /**
    * A list of conditions in descending order by priority.
    */
-  conditions: RemoteConfigServerCondition[];
+  conditions: NamedCondition[];
 
   /**
    * Map of parameter keys to their optional default values and optional conditional values.
@@ -214,16 +206,16 @@ export interface RemoteConfigServerTemplateData {
 }
 
 /**
- * Represents optional arguments that can be used when instantiating {@link RemoteConfigServerTemplate}.
+ * Represents optional arguments that can be used when instantiating {@link ServerTemplate}.
  */
-export interface RemoteConfigServerTemplateOptions {
+export interface ServerTemplateOptions {
 
   /**
    * Defines in-app default parameter values, so that your app behaves as
    * intended before it connects to the Remote Config backend, and so that
    * default values are available if none are set on the backend.
    */
-  defaultConfig?: RemoteConfigServerConfig,
+  defaultConfig?: ServerConfig,
 
   /**
    * Enables integrations to use template data loaded independently. For
@@ -231,32 +223,32 @@ export interface RemoteConfigServerTemplateOptions {
    * caching template data and then using this option to initialize the SDK with
    * that data.
    */
-  template?: RemoteConfigServerTemplateData,
+  template?: ServerTemplateData,
 }
 
 /**
  * Represents a stateful abstraction for a Remote Config server template.
  */
-export interface RemoteConfigServerTemplate {
+export interface ServerTemplate {
 
   /**
-   * Cached {@link RemoteConfigServerTemplateData}.
+   * Cached {@link ServerTemplateData}.
    */
-  cache: RemoteConfigServerTemplateData;
+  cache: ServerTemplateData;
 
   /**
-   * A {@link RemoteConfigServerConfig} that contains default Config values.
+   * A {@link ServerConfig} that contains default Config values.
    */
-  defaultConfig: RemoteConfigServerConfig;
+  defaultConfig: ServerConfig;
 
   /**
-   * Evaluates the current template to produce a {@link RemoteConfigServerConfig}.
+   * Evaluates the current template to produce a {@link ServerConfig}.
    */
-  evaluate(): RemoteConfigServerConfig;
+  evaluate(): ServerConfig;
 
   /**
    * Fetches and caches the current active version of the
-   * project's {@link RemoteConfigServerTemplate}.
+   * project's {@link ServerTemplate}.
    */
   load(): Promise<void>;
 }
@@ -387,4 +379,4 @@ export interface ListVersionsOptions {
 /**
  * Represents the configuration produced by evaluating a server template.
  */
-export type RemoteConfigServerConfig = { [key: string]: string | boolean | number }
+export type ServerConfig = { [key: string]: string | boolean | number }
diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts
index cfd965d27b..afd9d68d3c 100644
--- a/src/remote-config/remote-config.ts
+++ b/src/remote-config/remote-config.ts
@@ -23,16 +23,17 @@ import {
   RemoteConfigCondition,
   RemoteConfigParameter,
   RemoteConfigParameterGroup,
-  RemoteConfigServerTemplate,
+  ServerTemplate,
   RemoteConfigTemplate,
   RemoteConfigUser,
   Version,
   ExplicitParameterValue,
   InAppDefaultValue,
   ParameterValueType,
-  RemoteConfigServerConfig,
-  RemoteConfigServerTemplateData,
-  RemoteConfigServerTemplateOptions,
+  ServerConfig,
+  ServerTemplateData,
+  ServerTemplateOptions,
+  NamedCondition,
 } from './remote-config-api';
 
 /**
@@ -177,20 +178,20 @@ export class RemoteConfig {
   }
 
   /**
-   * Instantiates {@link RemoteConfigServerTemplate} and then fetches and caches the latest
+   * Instantiates {@link ServerTemplate} and then fetches and caches the latest
    * template version of the project.
    */
-  public async getServerTemplate(options?: RemoteConfigServerTemplateOptions): Promise<RemoteConfigServerTemplate> {
+  public async getServerTemplate(options?: ServerTemplateOptions): Promise<ServerTemplate> {
     const template = this.initServerTemplate(options);
     await template.load();
     return template;
   }
 
   /**
-   * Synchronously instantiates {@link RemoteConfigServerTemplate}.
+   * Synchronously instantiates {@link ServerTemplate}.
    */
-  public initServerTemplate(options?: RemoteConfigServerTemplateOptions): RemoteConfigServerTemplate {
-    const template = new RemoteConfigServerTemplateImpl(this.client, options?.defaultConfig);
+  public initServerTemplate(options?: ServerTemplateOptions): ServerTemplate {
+    const template = new ServerTemplateImpl(this.client, options?.defaultConfig);
     if (options?.template) {
       template.cache = options?.template;
     }
@@ -285,35 +286,35 @@ class RemoteConfigTemplateImpl implements RemoteConfigTemplate {
 /**
  * Remote Config dataplane template data implementation.
  */
-class RemoteConfigServerTemplateImpl implements RemoteConfigServerTemplate {
-  public cache: RemoteConfigServerTemplateData;
+class ServerTemplateImpl implements ServerTemplate {
+  public cache: ServerTemplateData;
 
   constructor(
     private readonly apiClient: RemoteConfigApiClient,
-    public readonly defaultConfig: RemoteConfigServerConfig = {}
+    public readonly defaultConfig: ServerConfig = {}
   ) { }
 
   /**
-   * Fetches and caches the current active version of the project's {@link RemoteConfigServerTemplate}.
+   * Fetches and caches the current active version of the project's {@link ServerTemplate}.
    */
   public load(): Promise<void> {
     return this.apiClient.getServerTemplate()
       .then((template) => {
-        this.cache = new RemoteConfigServerTemplateDataImpl(template);
+        this.cache = new ServerTemplateDataImpl(template);
       });
   }
 
   /**
-   * Evaluates the current template in cache to produce a {@link RemoteConfigServerConfig}.
+   * Evaluates the current template in cache to produce a {@link ServerConfig}.
    */
-  public evaluate(): RemoteConfigServerConfig {
+  public evaluate(): ServerConfig {
     if (!this.cache) {
       throw new FirebaseRemoteConfigError(
         'failed-precondition',
         'No Remote Config Server template in cache. Call load() before calling evaluate().');
     }
 
-    const evaluatedConfig: RemoteConfigServerConfig = {};
+    const evaluatedConfig: ServerConfig = {};
 
     for (const [key, parameter] of Object.entries(this.cache.parameters)) {
       const { defaultValue, valueType } = parameter;
@@ -339,7 +340,7 @@ class RemoteConfigServerTemplateImpl implements RemoteConfigServerTemplate {
     // Enables config to be a convenient object, but with the ability to perform additional
     // functionality when a value is retrieved.
     const proxyHandler = {
-      get(target: RemoteConfigServerConfig, prop: string) {
+      get(target: ServerConfig, prop: string) {
         return target[prop];
       }
     };
@@ -374,14 +375,14 @@ class RemoteConfigServerTemplateImpl implements RemoteConfigServerTemplate {
 /**
  * Remote Config dataplane template data implementation.
  */
-class RemoteConfigServerTemplateDataImpl implements RemoteConfigServerTemplateData {
+class ServerTemplateDataImpl implements ServerTemplateData {
   public parameters: { [key: string]: RemoteConfigParameter };
   public parameterGroups: { [key: string]: RemoteConfigParameterGroup };
-  public conditions: RemoteConfigCondition[];
+  public conditions: NamedCondition[];
   public readonly etag: string;
   public version?: Version;
 
-  constructor(template: RemoteConfigServerTemplateData) {
+  constructor(template: ServerTemplateData) {
     if (!validator.isNonNullObject(template) ||
       !validator.isNonEmptyString(template.etag)) {
       throw new FirebaseRemoteConfigError(
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 da2c87c639..52abb968c1 100644
--- a/test/unit/remote-config/remote-config-api-client.spec.ts
+++ b/test/unit/remote-config/remote-config-api-client.spec.ts
@@ -33,7 +33,7 @@ import { getSdkVersion } from '../../../src/utils/index';
 import {
   RemoteConfigTemplate, Version, ListVersionsResult,
 } from '../../../src/remote-config/index';
-import { RemoteConfigServerTemplateData } from '../../../src/remote-config/remote-config-api';
+import { ServerTemplateData } from '../../../src/remote-config/remote-config-api';
 
 const expect = chai.expect;
 
@@ -708,7 +708,7 @@ describe('RemoteConfigApiClient', () => {
     });
   }
 
-  function runEtagHeaderTests(rcOperation: () => Promise<RemoteConfigTemplate | RemoteConfigServerTemplateData>): void {
+  function runEtagHeaderTests(rcOperation: () => Promise<RemoteConfigTemplate | ServerTemplateData>): void {
     it('should reject when the etag is not present in the response', () => {
       const stub = sinon
         .stub(HttpClient.prototype, 'send')
@@ -722,7 +722,7 @@ describe('RemoteConfigApiClient', () => {
   }
 
   function runErrorResponseTests(
-    rcOperation: () => Promise<RemoteConfigTemplate | RemoteConfigServerTemplateData | ListVersionsResult>): void {
+    rcOperation: () => Promise<RemoteConfigTemplate | ServerTemplateData | ListVersionsResult>): void {
     it('should reject when a full platform error response is received', () => {
       const stub = sinon
         .stub(HttpClient.prototype, 'send')
diff --git a/test/unit/remote-config/remote-config.spec.ts b/test/unit/remote-config/remote-config.spec.ts
index 71fcf87f84..a78febcc81 100644
--- a/test/unit/remote-config/remote-config.spec.ts
+++ b/test/unit/remote-config/remote-config.spec.ts
@@ -35,7 +35,7 @@ import {
 } from '../../../src/remote-config/remote-config-api-client-internal';
 import { deepCopy } from '../../../src/utils/deep-copy';
 import {
-  RemoteConfigServerCondition, RemoteConfigServerTemplate, RemoteConfigServerTemplateData
+  NamedCondition, ServerTemplate, ServerTemplateData
 } from '../../../src/remote-config/remote-config-api';
 
 const expect = chai.expect;
@@ -106,15 +106,14 @@ describe('RemoteConfig', () => {
     // to allow easier use from within the tests. An improvement would be to
     // alter this into a helper that creates customized RemoteConfigTemplateContent based
     // on the needs of the test, as that would ensure type-safety.
-    conditions?: Array<{ name: string; expression: string; }>;
+    conditions?: Array<{ name: string; }>;
     parameters?: object | null;
     etag: string;
     version?: object;
   } = {
     conditions: [
       {
-        name: 'ios',
-        expression: 'device.os == \'ios\''
+        name: 'ios'
       },
     ],
     parameters: {
@@ -557,14 +556,13 @@ describe('RemoteConfig', () => {
     it('should resolve a server template on success', () => {
       const stub = sinon
         .stub(RemoteConfigApiClient.prototype, operationName)
-        .resolves(SERVER_REMOTE_CONFIG_RESPONSE as RemoteConfigServerTemplateData);
+        .resolves(SERVER_REMOTE_CONFIG_RESPONSE as ServerTemplateData);
       stubs.push(stub);
 
       return remoteConfig.getServerTemplate()
         .then((template) => {
           expect(template.cache.conditions.length).to.equal(1);
           expect(template.cache.conditions[0].name).to.equal('ios');
-          expect(template.cache.conditions[0].expression).to.equal('device.os == \'ios\'');
           expect(template.cache.etag).to.equal('etag-123456789012-5');
 
           const version = template.cache.version!;
@@ -586,9 +584,8 @@ describe('RemoteConfig', () => {
 
           const c = template.cache.conditions.find((c) => c.name === 'ios');
           expect(c).to.be.not.undefined;
-          const cond = c as RemoteConfigServerCondition;
+          const cond = c as NamedCondition;
           expect(cond.name).to.equal('ios');
-          expect(cond.expression).to.equal('device.os == \'ios\'');
 
           const parsed = JSON.parse(JSON.stringify(template.cache));
           const expectedTemplate = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE);
@@ -607,7 +604,7 @@ describe('RemoteConfig', () => {
 
       const stub = sinon
         .stub(RemoteConfigApiClient.prototype, operationName)
-        .resolves(SERVER_REMOTE_CONFIG_RESPONSE as RemoteConfigServerTemplateData);
+        .resolves(SERVER_REMOTE_CONFIG_RESPONSE as ServerTemplateData);
       stubs.push(stub);
 
       return remoteConfig.getServerTemplate({ defaultConfig })
@@ -620,7 +617,7 @@ describe('RemoteConfig', () => {
 
   describe('initServerTemplate', () => {
     it('should set and instantiates template when passed', () => {
-      const template = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as RemoteConfigServerTemplateData;
+      const template = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData;
       template.parameters = {
         dog_type: {
           defaultValue: {
@@ -706,7 +703,7 @@ describe('RemoteConfig', () => {
         response.etag = '';
         const stub = sinon
           .stub(RemoteConfigApiClient.prototype, operationName)
-          .resolves(response as RemoteConfigServerTemplateData);
+          .resolves(response as ServerTemplateData);
         stubs.push(stub);
         return remoteConfig.getServerTemplate()
           .should.eventually.be.rejected.and.have.property(
@@ -718,7 +715,7 @@ describe('RemoteConfig', () => {
         response.parameters = null;
         const stub = sinon
           .stub(RemoteConfigApiClient.prototype, operationName)
-          .resolves(response as RemoteConfigServerTemplateData);
+          .resolves(response as ServerTemplateData);
         stubs.push(stub);
         return remoteConfig.getServerTemplate()
           .should.eventually.be.rejected.and.have.property(
@@ -730,7 +727,7 @@ describe('RemoteConfig', () => {
         response.conditions = Object();
         const stub = sinon
           .stub(RemoteConfigApiClient.prototype, operationName)
-          .resolves(response as RemoteConfigServerTemplateData);
+          .resolves(response as ServerTemplateData);
         stubs.push(stub);
         return remoteConfig.getServerTemplate()
           .should.eventually.be.rejected.and.have.property(
@@ -742,7 +739,7 @@ describe('RemoteConfig', () => {
         response.parameters = undefined;
         const stub = sinon
           .stub(RemoteConfigApiClient.prototype, operationName)
-          .resolves(response as RemoteConfigServerTemplateData);
+          .resolves(response as ServerTemplateData);
         stubs.push(stub);
         return remoteConfig.getServerTemplate()
           .then((template) => {
@@ -756,7 +753,7 @@ describe('RemoteConfig', () => {
         response.conditions = undefined;
         const stub = sinon
           .stub(RemoteConfigApiClient.prototype, operationName)
-          .resolves(response as RemoteConfigServerTemplateData);
+          .resolves(response as ServerTemplateData);
         stubs.push(stub);
         return remoteConfig.getServerTemplate()
           .then((template) => {
@@ -768,14 +765,13 @@ describe('RemoteConfig', () => {
       it('should resolve a server template on success', () => {
         const stub = sinon
           .stub(RemoteConfigApiClient.prototype, operationName)
-          .resolves(SERVER_REMOTE_CONFIG_RESPONSE as RemoteConfigServerTemplateData);
+          .resolves(SERVER_REMOTE_CONFIG_RESPONSE as ServerTemplateData);
         stubs.push(stub);
 
         return remoteConfig.getServerTemplate()
           .then((template) => {
             expect(template.cache.conditions.length).to.equal(1);
             expect(template.cache.conditions[0].name).to.equal('ios');
-            expect(template.cache.conditions[0].expression).to.equal('device.os == \'ios\'');
             expect(template.cache.etag).to.equal('etag-123456789012-5');
 
             const version = template.cache.version!;
@@ -797,9 +793,8 @@ describe('RemoteConfig', () => {
 
             const c = template.cache.conditions.find((c) => c.name === 'ios');
             expect(c).to.be.not.undefined;
-            const cond = c as RemoteConfigServerCondition;
+            const cond = c as NamedCondition;
             expect(cond.name).to.equal('ios');
-            expect(cond.expression).to.equal('device.os == \'ios\'');
 
             const parsed = JSON.parse(JSON.stringify(template.cache));
             const expectedTemplate = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE);
@@ -817,7 +812,7 @@ describe('RemoteConfig', () => {
         response.version = versionInfo;
         const stub = sinon
           .stub(RemoteConfigApiClient.prototype, operationName)
-          .resolves(response as RemoteConfigServerTemplateData);
+          .resolves(response as ServerTemplateData);
         stubs.push(stub);
 
         return remoteConfig.getServerTemplate()
@@ -843,7 +838,7 @@ describe('RemoteConfig', () => {
         response.version = versionInfo;
         const stub = sinon
           .stub(RemoteConfigApiClient.prototype, operationName)
-          .resolves(response as RemoteConfigServerTemplateData);
+          .resolves(response as ServerTemplateData);
         stubs.push(stub);
 
         return remoteConfig.getServerTemplate()
@@ -869,7 +864,7 @@ describe('RemoteConfig', () => {
         response.version = versionInfo;
         const stub = sinon
           .stub(RemoteConfigApiClient.prototype, operationName)
-          .resolves(response as RemoteConfigServerTemplateData);
+          .resolves(response as ServerTemplateData);
         stubs.push(stub);
 
         return remoteConfig.getServerTemplate()
@@ -893,10 +888,10 @@ describe('RemoteConfig', () => {
       it('returns a config when template is present in cache', () => {
         const stub = sinon
           .stub(RemoteConfigApiClient.prototype, 'getServerTemplate')
-          .resolves(SERVER_REMOTE_CONFIG_RESPONSE_2 as RemoteConfigServerTemplateData);
+          .resolves(SERVER_REMOTE_CONFIG_RESPONSE_2 as ServerTemplateData);
         stubs.push(stub);
         return remoteConfig.getServerTemplate()
-          .then((template: RemoteConfigServerTemplate) => {
+          .then((template: ServerTemplate) => {
             const config = template.evaluate!();
             expect(config.dog_type).to.equal('corgi');
             expect(config.dog_type_enabled).to.equal(true);
@@ -908,14 +903,14 @@ describe('RemoteConfig', () => {
       it('uses local default if parameter not in template', () => {
         const stub = sinon
           .stub(RemoteConfigApiClient.prototype, 'getServerTemplate')
-          .resolves(SERVER_REMOTE_CONFIG_RESPONSE_2 as RemoteConfigServerTemplateData);
+          .resolves(SERVER_REMOTE_CONFIG_RESPONSE_2 as ServerTemplateData);
         stubs.push(stub);
         return remoteConfig.getServerTemplate({
           defaultConfig: {
             dog_coat: 'blue merle',
           }
         })
-          .then((template: RemoteConfigServerTemplate) => {
+          .then((template: ServerTemplate) => {
             const config = template.evaluate!();
             expect(config.dog_coat).to.equal(template.defaultConfig.dog_coat);
           });
@@ -924,14 +919,14 @@ describe('RemoteConfig', () => {
       it('uses local default when parameter is in template but default value is undefined', () => {
         const stub = sinon
           .stub(RemoteConfigApiClient.prototype, 'getServerTemplate')
-          .resolves(SERVER_REMOTE_CONFIG_RESPONSE_2 as RemoteConfigServerTemplateData);
+          .resolves(SERVER_REMOTE_CONFIG_RESPONSE_2 as ServerTemplateData);
         stubs.push(stub);
         return remoteConfig.getServerTemplate({
           defaultConfig: {
             dog_no_remote_default_value: 'local default'
           }
         })
-          .then((template: RemoteConfigServerTemplate) => {
+          .then((template: ServerTemplate) => {
             const config = template.evaluate!();
             expect(config.dog_no_remote_default_value).to.equal(template.defaultConfig.dog_no_remote_default_value);
           });
@@ -940,14 +935,14 @@ describe('RemoteConfig', () => {
       it('uses local default when in-app default value specified', () => {
         const stub = sinon
           .stub(RemoteConfigApiClient.prototype, 'getServerTemplate')
-          .resolves(SERVER_REMOTE_CONFIG_RESPONSE_2 as RemoteConfigServerTemplateData);
+          .resolves(SERVER_REMOTE_CONFIG_RESPONSE_2 as ServerTemplateData);
         stubs.push(stub);
         return remoteConfig.getServerTemplate({
           defaultConfig: {
             dog_use_inapp_default: '🐕'
           }
         })
-          .then((template: RemoteConfigServerTemplate) => {
+          .then((template: ServerTemplate) => {
             const config = template.evaluate!();
             expect(config.dog_use_inapp_default).to.equal(template.defaultConfig.dog_use_inapp_default);
           });
@@ -956,14 +951,14 @@ describe('RemoteConfig', () => {
       it('overrides local default when value exists', () => {
         const stub = sinon
           .stub(RemoteConfigApiClient.prototype, 'getServerTemplate')
-          .resolves(SERVER_REMOTE_CONFIG_RESPONSE_2 as RemoteConfigServerTemplateData);
+          .resolves(SERVER_REMOTE_CONFIG_RESPONSE_2 as ServerTemplateData);
         stubs.push(stub);
         return remoteConfig.getServerTemplate({
           defaultConfig: {
             dog_type_enabled: false
           }
         })
-          .then((template: RemoteConfigServerTemplate) => {
+          .then((template: ServerTemplate) => {
             const config = template.evaluate!();
             expect(config.dog_type_enabled).to.equal(template.defaultConfig.dog_type_enabled);
           });

From 724652675311162934e26891d1653a92d45814a5 Mon Sep 17 00:00:00 2001
From: Erik Eldridge <erikeldridge@google.com>
Date: Thu, 21 Mar 2024 13:34:16 -0700
Subject: [PATCH 05/13] Fixes incorrect use of `Object.assign` when backfilling
 SSRC config with defaults. (#2503)

In the logic where we backfill config with defaults, the first argument to Object.assign should be an object to assign to, but the code passed the object containing the defaults.
---
 src/remote-config/remote-config.ts            |  6 +-
 test/unit/remote-config/remote-config.spec.ts | 66 +++++++++++++++++--
 2 files changed, 66 insertions(+), 6 deletions(-)

diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts
index afd9d68d3c..d603919a3a 100644
--- a/src/remote-config/remote-config.ts
+++ b/src/remote-config/remote-config.ts
@@ -334,8 +334,10 @@ class ServerTemplateImpl implements ServerTemplate {
       evaluatedConfig[key] = this.parseRemoteConfigParameterValue(valueType, parameterDefaultValue);
     }
 
-    // Merges rendered config over default config.
-    const mergedConfig = Object.assign(this.defaultConfig, evaluatedConfig);
+    const mergedConfig = {};
+
+    // Merges default config and rendered config, prioritizing the latter.
+    Object.assign(mergedConfig, this.defaultConfig, evaluatedConfig);
 
     // Enables config to be a convenient object, but with the ability to perform additional
     // functionality when a value is retrieved.
diff --git a/test/unit/remote-config/remote-config.spec.ts b/test/unit/remote-config/remote-config.spec.ts
index a78febcc81..39e7cf3b86 100644
--- a/test/unit/remote-config/remote-config.spec.ts
+++ b/test/unit/remote-config/remote-config.spec.ts
@@ -948,19 +948,77 @@ describe('RemoteConfig', () => {
           });
       });
 
-      it('overrides local default when value exists', () => {
+      it('uses local default when in-app default value specified after loading remote values', async () => {
+        // We had a bug caused by forgetting the first argument to
+        // Object.assign. This resulted in defaultConfig being overwritten
+        // by the remote values. So this test asserts we can use in-app
+        // default after loading remote values.
+        const template = remoteConfig.initServerTemplate({
+          defaultConfig: {
+            dog_type: 'corgi'
+          }
+        });
+
+        const response = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE);
+
+        response.parameters = {
+          dog_type: {
+            defaultValue: {
+              value: 'pug'
+            },
+            valueType: 'STRING'
+          },
+        }
+
+        template.cache = response as ServerTemplateData;
+
+        let config = template.evaluate();
+
+        expect(config.dog_type).to.equal('pug');
+
+        response.parameters = {
+          dog_type: {
+            defaultValue: {
+              useInAppDefault: true
+            },
+            valueType: 'STRING'
+          },
+        }
+
+        template.cache = response as ServerTemplateData;
+
+        config = template.evaluate();
+
+        expect(config.dog_type).to.equal('corgi');
+      });
+
+      it('overrides local default when remote value exists', () => {
+        const response = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE);
+        response.parameters = {
+          dog_type_enabled: {
+            defaultValue: {
+              // Defines remote value
+              value: 'true'
+            },
+            valueType: 'BOOLEAN'
+          },
+        }
+
         const stub = sinon
           .stub(RemoteConfigApiClient.prototype, 'getServerTemplate')
-          .resolves(SERVER_REMOTE_CONFIG_RESPONSE_2 as ServerTemplateData);
+          .resolves(response as ServerTemplateData);
         stubs.push(stub);
+
         return remoteConfig.getServerTemplate({
           defaultConfig: {
+            // Defines local default
             dog_type_enabled: false
           }
         })
           .then((template: ServerTemplate) => {
-            const config = template.evaluate!();
-            expect(config.dog_type_enabled).to.equal(template.defaultConfig.dog_type_enabled);
+            const config = template.evaluate();
+            // Asserts remote value overrides local default.
+            expect(config.dog_type_enabled).to.be.true;
           });
       });
     });

From 02c0559a0ad7c3b462a4fef208d93c19a391087e Mon Sep 17 00:00:00 2001
From: Erik Eldridge <erikeldridge@google.com>
Date: Thu, 21 Mar 2024 15:24:25 -0700
Subject: [PATCH 06/13] Support SSRC conditions (#2487)

---
 etc/firebase-admin.remote-config.api.md       |   49 +-
 package-lock.json                             | 1541 ++++++++++-------
 package.json                                  |    1 +
 .../condition-evaluator-internal.ts           |  168 ++
 src/remote-config/index.ts                    |    7 +
 src/remote-config/remote-config-api.ts        |  164 +-
 src/remote-config/remote-config.ts            |   54 +-
 test/unit/index.spec.ts                       |    1 +
 .../remote-config/condition-evaluator.spec.ts |  794 +++++++++
 test/unit/remote-config/remote-config.spec.ts |  132 +-
 10 files changed, 2261 insertions(+), 650 deletions(-)
 create mode 100644 src/remote-config/condition-evaluator-internal.ts
 create mode 100644 test/unit/remote-config/condition-evaluator.spec.ts

diff --git a/etc/firebase-admin.remote-config.api.md b/etc/firebase-admin.remote-config.api.md
index 98c1883ab1..6712175a60 100644
--- a/etc/firebase-admin.remote-config.api.md
+++ b/etc/firebase-admin.remote-config.api.md
@@ -8,6 +8,16 @@
 
 import { Agent } from 'http';
 
+// @public
+export interface AndCondition {
+    conditions?: Array<OneOfCondition>;
+}
+
+// @public
+export type EvaluationContext = {
+    randomizationId?: string;
+};
+
 // @public
 export interface ExplicitParameterValue {
     value: string;
@@ -38,14 +48,51 @@ export interface ListVersionsResult {
     versions: Version[];
 }
 
+// @public
+export interface MicroPercentRange {
+    microPercentLowerBound?: number;
+    microPercentUpperBound?: number;
+}
+
 // @public
 export interface NamedCondition {
+    condition: OneOfCondition;
     name: string;
 }
 
+// @public
+export interface OneOfCondition {
+    andCondition?: AndCondition;
+    false?: Record<string, never>;
+    orCondition?: OrCondition;
+    percent?: PercentCondition;
+    true?: Record<string, never>;
+}
+
+// @public
+export interface OrCondition {
+    conditions?: Array<OneOfCondition>;
+}
+
 // @public
 export type ParameterValueType = 'STRING' | 'BOOLEAN' | 'NUMBER' | 'JSON';
 
+// @public
+export interface PercentCondition {
+    microPercent?: number;
+    microPercentRange?: MicroPercentRange;
+    percentOperator?: PercentConditionOperator;
+    seed?: string;
+}
+
+// @public
+export enum PercentConditionOperator {
+    BETWEEN = "BETWEEN",
+    GREATER_THAN = "GREATER_THAN",
+    LESS_OR_EQUAL = "LESS_OR_EQUAL",
+    UNKNOWN = "UNKNOWN"
+}
+
 // @public
 export class RemoteConfig {
     // (undocumented)
@@ -120,7 +167,7 @@ export type ServerConfig = {
 export interface ServerTemplate {
     cache: ServerTemplateData;
     defaultConfig: ServerConfig;
-    evaluate(): ServerConfig;
+    evaluate(context?: EvaluationContext): ServerConfig;
     load(): Promise<void>;
 }
 
diff --git a/package-lock.json b/package-lock.json
index 5fe3a93dae..91e59b8fd5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,13 +11,13 @@
       "dev": true
     },
     "@ampproject/remapping": {
-      "version": "2.2.1",
-      "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz",
-      "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==",
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
+      "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
       "dev": true,
       "requires": {
-        "@jridgewell/gen-mapping": "^0.3.0",
-        "@jridgewell/trace-mapping": "^0.3.9"
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.24"
       }
     },
     "@babel/code-frame": {
@@ -95,21 +95,21 @@
       "dev": true
     },
     "@babel/core": {
-      "version": "7.23.5",
-      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.5.tgz",
-      "integrity": "sha512-Cwc2XjUrG4ilcfOw4wBAK+enbdgwAcAJCfGUItPBKR7Mjw4aEfAFYrLxeRp4jWgtNIKn3n2AlBOfwwafl+42/g==",
+      "version": "7.24.0",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz",
+      "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==",
       "dev": true,
       "requires": {
         "@ampproject/remapping": "^2.2.0",
         "@babel/code-frame": "^7.23.5",
-        "@babel/generator": "^7.23.5",
-        "@babel/helper-compilation-targets": "^7.22.15",
+        "@babel/generator": "^7.23.6",
+        "@babel/helper-compilation-targets": "^7.23.6",
         "@babel/helper-module-transforms": "^7.23.3",
-        "@babel/helpers": "^7.23.5",
-        "@babel/parser": "^7.23.5",
-        "@babel/template": "^7.22.15",
-        "@babel/traverse": "^7.23.5",
-        "@babel/types": "^7.23.5",
+        "@babel/helpers": "^7.24.0",
+        "@babel/parser": "^7.24.0",
+        "@babel/template": "^7.24.0",
+        "@babel/traverse": "^7.24.0",
+        "@babel/types": "^7.24.0",
         "convert-source-map": "^2.0.0",
         "debug": "^4.1.0",
         "gensync": "^1.0.0-beta.2",
@@ -132,26 +132,26 @@
       }
     },
     "@babel/generator": {
-      "version": "7.23.5",
-      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.5.tgz",
-      "integrity": "sha512-BPssCHrBD+0YrxviOa3QzpqwhNIXKEtOa2jQrm4FlmkC2apYgRnQcmPWiGZDlGxiNtltnUFolMe8497Esry+jA==",
+      "version": "7.23.6",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz",
+      "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==",
       "dev": true,
       "requires": {
-        "@babel/types": "^7.23.5",
+        "@babel/types": "^7.23.6",
         "@jridgewell/gen-mapping": "^0.3.2",
         "@jridgewell/trace-mapping": "^0.3.17",
         "jsesc": "^2.5.1"
       }
     },
     "@babel/helper-compilation-targets": {
-      "version": "7.22.15",
-      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz",
-      "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==",
+      "version": "7.23.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz",
+      "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==",
       "dev": true,
       "requires": {
-        "@babel/compat-data": "^7.22.9",
-        "@babel/helper-validator-option": "^7.22.15",
-        "browserslist": "^4.21.9",
+        "@babel/compat-data": "^7.23.5",
+        "@babel/helper-validator-option": "^7.23.5",
+        "browserslist": "^4.22.2",
         "lru-cache": "^5.1.1",
         "semver": "^6.3.1"
       },
@@ -263,14 +263,14 @@
       "dev": true
     },
     "@babel/helpers": {
-      "version": "7.23.5",
-      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.5.tgz",
-      "integrity": "sha512-oO7us8FzTEsG3U6ag9MfdF1iA/7Z6dz+MtFhifZk8C8o453rGJFFWUP1t+ULM9TUIAzC9uxXEiXjOiVMyd7QPg==",
+      "version": "7.24.0",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.0.tgz",
+      "integrity": "sha512-ulDZdc0Aj5uLc5nETsa7EPx2L7rM0YJM8r7ck7U73AXi7qOV44IHHRAYZHY6iU1rr3C5N4NtTmMRUJP6kwCWeA==",
       "dev": true,
       "requires": {
-        "@babel/template": "^7.22.15",
-        "@babel/traverse": "^7.23.5",
-        "@babel/types": "^7.23.5"
+        "@babel/template": "^7.24.0",
+        "@babel/traverse": "^7.24.0",
+        "@babel/types": "^7.24.0"
       }
     },
     "@babel/highlight": {
@@ -343,37 +343,37 @@
       }
     },
     "@babel/parser": {
-      "version": "7.23.5",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.5.tgz",
-      "integrity": "sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==",
+      "version": "7.24.0",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz",
+      "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==",
       "dev": true
     },
     "@babel/template": {
-      "version": "7.22.15",
-      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz",
-      "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==",
+      "version": "7.24.0",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz",
+      "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==",
       "dev": true,
       "requires": {
-        "@babel/code-frame": "^7.22.13",
-        "@babel/parser": "^7.22.15",
-        "@babel/types": "^7.22.15"
+        "@babel/code-frame": "^7.23.5",
+        "@babel/parser": "^7.24.0",
+        "@babel/types": "^7.24.0"
       }
     },
     "@babel/traverse": {
-      "version": "7.23.5",
-      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.5.tgz",
-      "integrity": "sha512-czx7Xy5a6sapWWRx61m1Ke1Ra4vczu1mCTtJam5zRTBOonfdJ+S/B6HYmGYu3fJtr8GGET3si6IhgWVBhJ/m8w==",
+      "version": "7.24.0",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.0.tgz",
+      "integrity": "sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw==",
       "dev": true,
       "requires": {
         "@babel/code-frame": "^7.23.5",
-        "@babel/generator": "^7.23.5",
+        "@babel/generator": "^7.23.6",
         "@babel/helper-environment-visitor": "^7.22.20",
         "@babel/helper-function-name": "^7.23.0",
         "@babel/helper-hoist-variables": "^7.22.5",
         "@babel/helper-split-export-declaration": "^7.22.6",
-        "@babel/parser": "^7.23.5",
-        "@babel/types": "^7.23.5",
-        "debug": "^4.1.0",
+        "@babel/parser": "^7.24.0",
+        "@babel/types": "^7.24.0",
+        "debug": "^4.3.1",
         "globals": "^11.1.0"
       },
       "dependencies": {
@@ -386,9 +386,9 @@
       }
     },
     "@babel/types": {
-      "version": "7.23.5",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.5.tgz",
-      "integrity": "sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w==",
+      "version": "7.24.0",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz",
+      "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==",
       "dev": true,
       "requires": {
         "@babel/helper-string-parser": "^7.23.4",
@@ -447,18 +447,35 @@
         "js-yaml": "^4.1.0",
         "minimatch": "^3.1.2",
         "strip-json-comments": "^3.1.1"
+      },
+      "dependencies": {
+        "minimatch": {
+          "version": "3.1.2",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+          "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+          "dev": true,
+          "requires": {
+            "brace-expansion": "^1.1.7"
+          }
+        },
+        "strip-json-comments": {
+          "version": "3.1.1",
+          "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+          "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+          "dev": true
+        }
       }
     },
     "@eslint/js": {
-      "version": "8.56.0",
-      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz",
-      "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==",
+      "version": "8.57.0",
+      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz",
+      "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==",
       "dev": true
     },
     "@fastify/busboy": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz",
-      "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA=="
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
+      "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="
     },
     "@firebase/api-documenter": {
       "version": "0.4.0",
@@ -477,9 +494,9 @@
       }
     },
     "@firebase/app": {
-      "version": "0.9.27",
-      "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.9.27.tgz",
-      "integrity": "sha512-p2Dvl1ge4kRsyK5+wWcmdAIE9MSwZ0pDKAYB51LZgZuz6wciUZk4E1yAEdkfQlRxuHehn+Ol9WP5Qk2XQZiHGg==",
+      "version": "0.9.28",
+      "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.9.28.tgz",
+      "integrity": "sha512-MS0+EtNixrwJbVDs5Bt/lhUhzeWGUtUoP6X+zYZck5GAZwI5g4F91noVA9oIXlFlpn6Q1xIbiaHA2GwGk7/7Ag==",
       "dev": true,
       "requires": {
         "@firebase/component": "0.6.5",
@@ -487,27 +504,6 @@
         "@firebase/util": "1.9.4",
         "idb": "7.1.1",
         "tslib": "^2.1.0"
-      },
-      "dependencies": {
-        "@firebase/component": {
-          "version": "0.6.5",
-          "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.5.tgz",
-          "integrity": "sha512-2tVDk1ixi12sbDmmfITK8lxSjmcb73BMF6Qwc3U44hN/J1Fi1QY/Hnnb6klFlbB9/G16a3J3d4nXykye2EADTw==",
-          "dev": true,
-          "requires": {
-            "@firebase/util": "1.9.4",
-            "tslib": "^2.1.0"
-          }
-        },
-        "@firebase/util": {
-          "version": "1.9.4",
-          "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.4.tgz",
-          "integrity": "sha512-WLonYmS1FGHT97TsUmRN3qnTh5TeeoJp1Gg5fithzuAgdZOUtsYECfy7/noQ3llaguios8r5BuXSEiK82+UrxQ==",
-          "dev": true,
-          "requires": {
-            "tslib": "^2.1.0"
-          }
-        }
       }
     },
     "@firebase/app-check-interop-types": {
@@ -516,37 +512,16 @@
       "integrity": "sha512-xAxHPZPIgFXnI+vb4sbBjZcde7ZluzPPaSK7Lx3/nmuVk4TjZvnL8ONnkd4ERQKL8WePQySU+pRcWkh8rDf5Sg=="
     },
     "@firebase/app-compat": {
-      "version": "0.2.27",
-      "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.27.tgz",
-      "integrity": "sha512-SYlqocfUDKPHR6MSFC8hree0BTiWFu5o8wbf6zFlYXyG41w7TcHp4wJi4H/EL5V6cM4kxwruXTJtqXX/fRAZtw==",
+      "version": "0.2.28",
+      "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.28.tgz",
+      "integrity": "sha512-Mr2NbeM1Oaayuw5unUAMzt+7/MN+e2uklT1l87D+ZLJl2UvhZAZmMt74GjEI9N3sDYKMeszSbszBqtJ1fGVafQ==",
       "dev": true,
       "requires": {
-        "@firebase/app": "0.9.27",
+        "@firebase/app": "0.9.28",
         "@firebase/component": "0.6.5",
         "@firebase/logger": "0.4.0",
         "@firebase/util": "1.9.4",
         "tslib": "^2.1.0"
-      },
-      "dependencies": {
-        "@firebase/component": {
-          "version": "0.6.5",
-          "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.5.tgz",
-          "integrity": "sha512-2tVDk1ixi12sbDmmfITK8lxSjmcb73BMF6Qwc3U44hN/J1Fi1QY/Hnnb6klFlbB9/G16a3J3d4nXykye2EADTw==",
-          "dev": true,
-          "requires": {
-            "@firebase/util": "1.9.4",
-            "tslib": "^2.1.0"
-          }
-        },
-        "@firebase/util": {
-          "version": "1.9.4",
-          "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.4.tgz",
-          "integrity": "sha512-WLonYmS1FGHT97TsUmRN3qnTh5TeeoJp1Gg5fithzuAgdZOUtsYECfy7/noQ3llaguios8r5BuXSEiK82+UrxQ==",
-          "dev": true,
-          "requires": {
-            "tslib": "^2.1.0"
-          }
-        }
       }
     },
     "@firebase/app-types": {
@@ -567,6 +542,25 @@
         "tslib": "^2.1.0"
       },
       "dependencies": {
+        "@firebase/component": {
+          "version": "0.6.4",
+          "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.4.tgz",
+          "integrity": "sha512-rLMyrXuO9jcAUCaQXCMjCMUsWrba5fzHlNK24xz5j2W6A/SRmK8mZJ/hn7V0fViLbxC0lPMtrK1eYzk6Fg03jA==",
+          "dev": true,
+          "requires": {
+            "@firebase/util": "1.9.3",
+            "tslib": "^2.1.0"
+          }
+        },
+        "@firebase/util": {
+          "version": "1.9.3",
+          "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.3.tgz",
+          "integrity": "sha512-DY02CRhOZwpzO36fHpuVysz6JZrscPiBXD0fXp6qSrL9oNOx5KWICKdR95C0lSITzxp0TZosVyHqzatE8JbcjA==",
+          "dev": true,
+          "requires": {
+            "tslib": "^2.1.0"
+          }
+        },
         "node-fetch": {
           "version": "2.6.7",
           "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
@@ -592,6 +586,25 @@
         "tslib": "^2.1.0"
       },
       "dependencies": {
+        "@firebase/component": {
+          "version": "0.6.4",
+          "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.4.tgz",
+          "integrity": "sha512-rLMyrXuO9jcAUCaQXCMjCMUsWrba5fzHlNK24xz5j2W6A/SRmK8mZJ/hn7V0fViLbxC0lPMtrK1eYzk6Fg03jA==",
+          "dev": true,
+          "requires": {
+            "@firebase/util": "1.9.3",
+            "tslib": "^2.1.0"
+          }
+        },
+        "@firebase/util": {
+          "version": "1.9.3",
+          "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.3.tgz",
+          "integrity": "sha512-DY02CRhOZwpzO36fHpuVysz6JZrscPiBXD0fXp6qSrL9oNOx5KWICKdR95C0lSITzxp0TZosVyHqzatE8JbcjA==",
+          "dev": true,
+          "requires": {
+            "tslib": "^2.1.0"
+          }
+        },
         "node-fetch": {
           "version": "2.6.7",
           "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
@@ -615,48 +628,48 @@
       "dev": true
     },
     "@firebase/component": {
-      "version": "0.6.4",
-      "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.4.tgz",
-      "integrity": "sha512-rLMyrXuO9jcAUCaQXCMjCMUsWrba5fzHlNK24xz5j2W6A/SRmK8mZJ/hn7V0fViLbxC0lPMtrK1eYzk6Fg03jA==",
+      "version": "0.6.5",
+      "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.5.tgz",
+      "integrity": "sha512-2tVDk1ixi12sbDmmfITK8lxSjmcb73BMF6Qwc3U44hN/J1Fi1QY/Hnnb6klFlbB9/G16a3J3d4nXykye2EADTw==",
       "requires": {
-        "@firebase/util": "1.9.3",
+        "@firebase/util": "1.9.4",
         "tslib": "^2.1.0"
       }
     },
     "@firebase/database": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.2.tgz",
-      "integrity": "sha512-8X6NBJgUQzDz0xQVaCISoOLINKat594N2eBbMR3Mu/MH/ei4WM+aAMlsNzngF22eljXu1SILP5G3evkyvsG3Ng==",
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.3.tgz",
+      "integrity": "sha512-9fjqLt9JzL46gw9+NRqsgQEMjgRwfd8XtzcKqG+UYyhVeFCdVRQ0Wp6Dw/dvYHnbH5vNEKzNv36dcB4p+PIAAA==",
       "requires": {
         "@firebase/app-check-interop-types": "0.3.0",
         "@firebase/auth-interop-types": "0.2.1",
-        "@firebase/component": "0.6.4",
+        "@firebase/component": "0.6.5",
         "@firebase/logger": "0.4.0",
-        "@firebase/util": "1.9.3",
+        "@firebase/util": "1.9.4",
         "faye-websocket": "0.11.4",
         "tslib": "^2.1.0"
       }
     },
     "@firebase/database-compat": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-1.0.2.tgz",
-      "integrity": "sha512-09ryJnXDvuycsxn8aXBzLhBTuCos3HEnCOBWY6hosxfYlNCGnLvG8YMlbSAt5eNhf7/00B095AEfDsdrrLjxqA==",
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-1.0.3.tgz",
+      "integrity": "sha512-7tHEOcMbK5jJzHWyphPux4osogH/adWwncxdMxdBpB9g1DNIyY4dcz1oJdlkXGM/i/AjUBesZsd5CuwTRTBNTw==",
       "requires": {
-        "@firebase/component": "0.6.4",
-        "@firebase/database": "1.0.2",
-        "@firebase/database-types": "1.0.0",
+        "@firebase/component": "0.6.5",
+        "@firebase/database": "1.0.3",
+        "@firebase/database-types": "1.0.1",
         "@firebase/logger": "0.4.0",
-        "@firebase/util": "1.9.3",
+        "@firebase/util": "1.9.4",
         "tslib": "^2.1.0"
       }
     },
     "@firebase/database-types": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.0.tgz",
-      "integrity": "sha512-SjnXStoE0Q56HcFgNQ+9SsmJc0c8TqGARdI/T44KXy+Ets3r6x/ivhQozT66bMnCEjJRywYoxNurRTMlZF8VNg==",
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.1.tgz",
+      "integrity": "sha512-Tmcmx5XgiI7UVF/4oGg2P3AOTfq3WKEPsm2yf+uXtN7uG/a4WTWhVMrXGYRY2ZUL1xPxv9V33wQRJ+CcrUhVXw==",
       "requires": {
         "@firebase/app-types": "0.9.0",
-        "@firebase/util": "1.9.3"
+        "@firebase/util": "1.9.4"
       }
     },
     "@firebase/logger": {
@@ -668,17 +681,17 @@
       }
     },
     "@firebase/util": {
-      "version": "1.9.3",
-      "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.3.tgz",
-      "integrity": "sha512-DY02CRhOZwpzO36fHpuVysz6JZrscPiBXD0fXp6qSrL9oNOx5KWICKdR95C0lSITzxp0TZosVyHqzatE8JbcjA==",
+      "version": "1.9.4",
+      "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.4.tgz",
+      "integrity": "sha512-WLonYmS1FGHT97TsUmRN3qnTh5TeeoJp1Gg5fithzuAgdZOUtsYECfy7/noQ3llaguios8r5BuXSEiK82+UrxQ==",
       "requires": {
         "tslib": "^2.1.0"
       }
     },
     "@google-cloud/firestore": {
-      "version": "7.3.0",
-      "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.3.0.tgz",
-      "integrity": "sha512-2IftQLAbCuVp0nTd3neeu+d3OYIegJpV/V9R4USQj51LzJcXPe8h8jZ7j3+svSNhJVGy6JsN0T1QqlJdMDhTwg==",
+      "version": "7.3.1",
+      "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.3.1.tgz",
+      "integrity": "sha512-YluLZbJK3dHXq6Ns5URCtr6hjBiG+6EM17QSivjaozPYDsv1R9a9mkWPz+jCQrb6Ewz6mxp3zavu6DXxvmSWLA==",
       "optional": true,
       "requires": {
         "fast-deep-equal": "^3.1.1",
@@ -710,9 +723,9 @@
       "optional": true
     },
     "@google-cloud/storage": {
-      "version": "7.7.0",
-      "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.7.0.tgz",
-      "integrity": "sha512-EMCEY+6JiIkx7Dt8NXVGGjy1vRdSGdHkoqZoqjJw7cEBkT7ZkX0c7puedfn1MamnzW5SX4xoa2jVq5u7OWBmkQ==",
+      "version": "7.8.0",
+      "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.8.0.tgz",
+      "integrity": "sha512-4q8rKdLp35z8msAtrhr0pbos7BeD8T0tr6rMbBINewp9cfrwj7ROIElVwBluU8fZ596OvwQcjb6QCyBzTmkMRQ==",
       "optional": true,
       "requires": {
         "@google-cloud/paginator": "^5.0.0",
@@ -721,11 +734,11 @@
         "abort-controller": "^3.0.0",
         "async-retry": "^1.3.3",
         "compressible": "^2.0.12",
-        "duplexify": "^4.0.0",
+        "duplexify": "^4.1.3",
         "ent": "^2.2.0",
         "fast-xml-parser": "^4.3.0",
         "gaxios": "^6.0.2",
-        "google-auth-library": "^9.0.0",
+        "google-auth-library": "^9.6.3",
         "mime": "^3.0.0",
         "mime-types": "^2.0.8",
         "p-limit": "^3.0.1",
@@ -743,13 +756,13 @@
       }
     },
     "@grpc/grpc-js": {
-      "version": "1.9.14",
-      "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.14.tgz",
-      "integrity": "sha512-nOpuzZ2G3IuMFN+UPPpKrC6NsLmWsTqSsm66IRfnBt1D4pwTqE27lmbpcPM+l2Ua4gE7PfjRHI6uedAy7hoXUw==",
+      "version": "1.10.2",
+      "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.10.2.tgz",
+      "integrity": "sha512-lSbgu8iayAod8O0YcoXK3+bMFGThY2svtN35Zlm9VepsB3jfyIcoupKknEht7Kh9Q8ITjsp0J4KpYo9l4+FhNg==",
       "optional": true,
       "requires": {
-        "@grpc/proto-loader": "^0.7.8",
-        "@types/node": ">=12.12.47"
+        "@grpc/proto-loader": "^0.7.10",
+        "@js-sdsl/ordered-map": "^4.4.2"
       }
     },
     "@grpc/proto-loader": {
@@ -868,26 +881,26 @@
       "dev": true
     },
     "@jridgewell/gen-mapping": {
-      "version": "0.3.3",
-      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
-      "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
+      "version": "0.3.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
+      "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
       "dev": true,
       "requires": {
-        "@jridgewell/set-array": "^1.0.1",
+        "@jridgewell/set-array": "^1.2.1",
         "@jridgewell/sourcemap-codec": "^1.4.10",
-        "@jridgewell/trace-mapping": "^0.3.9"
+        "@jridgewell/trace-mapping": "^0.3.24"
       }
     },
     "@jridgewell/resolve-uri": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
-      "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
       "dev": true
     },
     "@jridgewell/set-array": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
-      "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
+      "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
       "dev": true
     },
     "@jridgewell/sourcemap-codec": {
@@ -897,15 +910,21 @@
       "dev": true
     },
     "@jridgewell/trace-mapping": {
-      "version": "0.3.20",
-      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz",
-      "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==",
+      "version": "0.3.25",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
+      "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
       "dev": true,
       "requires": {
         "@jridgewell/resolve-uri": "^3.1.0",
         "@jridgewell/sourcemap-codec": "^1.4.14"
       }
     },
+    "@js-sdsl/ordered-map": {
+      "version": "4.4.2",
+      "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
+      "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==",
+      "optional": true
+    },
     "@mapbox/node-pre-gyp": {
       "version": "1.0.11",
       "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
@@ -945,19 +964,20 @@
       }
     },
     "@microsoft/api-extractor": {
-      "version": "7.39.4",
-      "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.39.4.tgz",
-      "integrity": "sha512-6YvfkpbEqRQ0UPdVBc+lOiq7VlXi9kw8U3w+RcXCFDVc/UljlXU5l9fHEyuBAW1GGO2opUe+yf9OscWhoHANhg==",
+      "version": "7.42.3",
+      "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.42.3.tgz",
+      "integrity": "sha512-JNLJFpGHz6ekjS6bvYXxUBeRGnSHeCMFNvRbCQ+7XXB/ZFrgLSMPwWtEq40AiWAy+oyG5a4RSNwdJTp0B2USvQ==",
       "dev": true,
       "requires": {
-        "@microsoft/api-extractor-model": "7.28.7",
+        "@microsoft/api-extractor-model": "7.28.13",
         "@microsoft/tsdoc": "0.14.2",
         "@microsoft/tsdoc-config": "~0.16.1",
-        "@rushstack/node-core-library": "3.64.2",
-        "@rushstack/rig-package": "0.5.1",
-        "@rushstack/ts-command-line": "4.17.1",
-        "colors": "~1.2.1",
+        "@rushstack/node-core-library": "4.0.2",
+        "@rushstack/rig-package": "0.5.2",
+        "@rushstack/terminal": "0.10.0",
+        "@rushstack/ts-command-line": "4.19.1",
         "lodash": "~4.17.15",
+        "minimatch": "~3.0.3",
         "resolve": "~1.22.1",
         "semver": "~7.5.4",
         "source-map": "~0.6.1",
@@ -971,12 +991,11 @@
           "dev": true
         },
         "@rushstack/node-core-library": {
-          "version": "3.64.2",
-          "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.64.2.tgz",
-          "integrity": "sha512-n1S2VYEklONiwKpUyBq/Fym6yAsfsCXrqFabuOMcCuj4C+zW+HyaspSHXJCKqkMxfjviwe/c9+DUqvRWIvSN9Q==",
+          "version": "4.0.2",
+          "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-4.0.2.tgz",
+          "integrity": "sha512-hyES82QVpkfQMeBMteQUnrhASL/KHPhd7iJ8euduwNJG4mu2GSOKybf0rOEjOm1Wz7CwJEUm9y0yD7jg2C1bfg==",
           "dev": true,
           "requires": {
-            "colors": "~1.2.1",
             "fs-extra": "~7.0.1",
             "import-lazy": "~4.0.0",
             "jju": "~1.4.0",
@@ -986,22 +1005,25 @@
           }
         },
         "@rushstack/ts-command-line": {
-          "version": "4.17.1",
-          "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.17.1.tgz",
-          "integrity": "sha512-2jweO1O57BYP5qdBGl6apJLB+aRIn5ccIRTPDyULh0KMwVzFqWtw6IZWt1qtUoZD/pD2RNkIOosH6Cq45rIYeg==",
+          "version": "4.19.1",
+          "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.19.1.tgz",
+          "integrity": "sha512-J7H768dgcpG60d7skZ5uSSwyCZs/S2HrWP1Ds8d1qYAyaaeJmpmmLr9BVw97RjFzmQPOYnoXcKA4GkqDCkduQg==",
           "dev": true,
           "requires": {
+            "@rushstack/terminal": "0.10.0",
             "@types/argparse": "1.0.38",
             "argparse": "~1.0.9",
-            "colors": "~1.2.1",
             "string-argv": "~0.3.1"
           }
         },
-        "colors": {
-          "version": "1.2.5",
-          "resolved": "https://registry.npmjs.org/colors/-/colors-1.2.5.tgz",
-          "integrity": "sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg==",
-          "dev": true
+        "semver": {
+          "version": "7.5.4",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+          "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+          "dev": true,
+          "requires": {
+            "lru-cache": "^6.0.0"
+          }
         },
         "typescript": {
           "version": "5.3.3",
@@ -1012,14 +1034,14 @@
       }
     },
     "@microsoft/api-extractor-model": {
-      "version": "7.28.7",
-      "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.28.7.tgz",
-      "integrity": "sha512-4gCGGEQGHmbQmarnDcEWS2cjj0LtNuD3D6rh3ZcAyAYTkceAugAk2eyQHGdTcGX8w3qMjWCTU1TPb8xHnMM+Kg==",
+      "version": "7.28.13",
+      "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.28.13.tgz",
+      "integrity": "sha512-39v/JyldX4MS9uzHcdfmjjfS6cYGAoXV+io8B5a338pkHiSt+gy2eXQ0Q7cGFJ7quSa1VqqlMdlPrB6sLR/cAw==",
       "dev": true,
       "requires": {
         "@microsoft/tsdoc": "0.14.2",
         "@microsoft/tsdoc-config": "~0.16.1",
-        "@rushstack/node-core-library": "3.64.2"
+        "@rushstack/node-core-library": "4.0.2"
       },
       "dependencies": {
         "@microsoft/tsdoc": {
@@ -1029,12 +1051,11 @@
           "dev": true
         },
         "@rushstack/node-core-library": {
-          "version": "3.64.2",
-          "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.64.2.tgz",
-          "integrity": "sha512-n1S2VYEklONiwKpUyBq/Fym6yAsfsCXrqFabuOMcCuj4C+zW+HyaspSHXJCKqkMxfjviwe/c9+DUqvRWIvSN9Q==",
+          "version": "4.0.2",
+          "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-4.0.2.tgz",
+          "integrity": "sha512-hyES82QVpkfQMeBMteQUnrhASL/KHPhd7iJ8euduwNJG4mu2GSOKybf0rOEjOm1Wz7CwJEUm9y0yD7jg2C1bfg==",
           "dev": true,
           "requires": {
-            "colors": "~1.2.1",
             "fs-extra": "~7.0.1",
             "import-lazy": "~4.0.0",
             "jju": "~1.4.0",
@@ -1043,11 +1064,14 @@
             "z-schema": "~5.0.2"
           }
         },
-        "colors": {
-          "version": "1.2.5",
-          "resolved": "https://registry.npmjs.org/colors/-/colors-1.2.5.tgz",
-          "integrity": "sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg==",
-          "dev": true
+        "semver": {
+          "version": "7.5.4",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+          "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+          "dev": true,
+          "requires": {
+            "lru-cache": "^6.0.0"
+          }
         }
       }
     },
@@ -1197,17 +1221,69 @@
           "resolved": "https://registry.npmjs.org/colors/-/colors-1.2.5.tgz",
           "integrity": "sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg==",
           "dev": true
+        },
+        "semver": {
+          "version": "7.5.4",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+          "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+          "dev": true,
+          "requires": {
+            "lru-cache": "^6.0.0"
+          }
         }
       }
     },
     "@rushstack/rig-package": {
-      "version": "0.5.1",
-      "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.5.1.tgz",
-      "integrity": "sha512-pXRYSe29TjRw7rqxD4WS3HN/sRSbfr+tJs4a9uuaSIBAITbUggygdhuG0VrO0EO+QqH91GhYMN4S6KRtOEmGVA==",
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.5.2.tgz",
+      "integrity": "sha512-mUDecIJeH3yYGZs2a48k+pbhM6JYwWlgjs2Ca5f2n1G2/kgdgP9D/07oglEGf6mRyXEnazhEENeYTSNDRCwdqA==",
       "dev": true,
       "requires": {
         "resolve": "~1.22.1",
         "strip-json-comments": "~3.1.1"
+      },
+      "dependencies": {
+        "strip-json-comments": {
+          "version": "3.1.1",
+          "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+          "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+          "dev": true
+        }
+      }
+    },
+    "@rushstack/terminal": {
+      "version": "0.10.0",
+      "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.10.0.tgz",
+      "integrity": "sha512-UbELbXnUdc7EKwfH2sb8ChqNgapUOdqcCIdQP4NGxBpTZV2sQyeekuK3zmfQSa/MN+/7b4kBogl2wq0vpkpYGw==",
+      "dev": true,
+      "requires": {
+        "@rushstack/node-core-library": "4.0.2",
+        "supports-color": "~8.1.1"
+      },
+      "dependencies": {
+        "@rushstack/node-core-library": {
+          "version": "4.0.2",
+          "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-4.0.2.tgz",
+          "integrity": "sha512-hyES82QVpkfQMeBMteQUnrhASL/KHPhd7iJ8euduwNJG4mu2GSOKybf0rOEjOm1Wz7CwJEUm9y0yD7jg2C1bfg==",
+          "dev": true,
+          "requires": {
+            "fs-extra": "~7.0.1",
+            "import-lazy": "~4.0.0",
+            "jju": "~1.4.0",
+            "resolve": "~1.22.1",
+            "semver": "~7.5.4",
+            "z-schema": "~5.0.2"
+          }
+        },
+        "semver": {
+          "version": "7.5.4",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+          "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+          "dev": true,
+          "requires": {
+            "lru-cache": "^6.0.0"
+          }
+        }
       }
     },
     "@rushstack/ts-command-line": {
@@ -1231,9 +1307,9 @@
       }
     },
     "@sinonjs/commons": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz",
-      "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==",
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz",
+      "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==",
       "dev": true,
       "requires": {
         "type-detect": "4.0.8"
@@ -1342,9 +1418,9 @@
       "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg=="
     },
     "@types/chai": {
-      "version": "4.3.11",
-      "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.11.tgz",
-      "integrity": "sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ==",
+      "version": "4.3.12",
+      "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.12.tgz",
+      "integrity": "sha512-zNKDHG/1yxm8Il6uCCVsm+dRdEsJlFoDu73X17y09bId6UwoYww+vFBsAcRzl8knM1sab3Dp1VRikFQwDOtDDw==",
       "dev": true
     },
     "@types/chai-as-promised": {
@@ -1376,9 +1452,9 @@
       }
     },
     "@types/express-serve-static-core": {
-      "version": "4.17.41",
-      "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz",
-      "integrity": "sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==",
+      "version": "4.17.43",
+      "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz",
+      "integrity": "sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==",
       "requires": {
         "@types/node": "*",
         "@types/qs": "*",
@@ -1413,9 +1489,9 @@
       }
     },
     "@types/lodash": {
-      "version": "4.14.202",
-      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz",
-      "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==",
+      "version": "4.17.0",
+      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz",
+      "integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==",
       "dev": true
     },
     "@types/long": {
@@ -1457,17 +1533,17 @@
       }
     },
     "@types/node": {
-      "version": "20.11.5",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.5.tgz",
-      "integrity": "sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==",
+      "version": "20.11.27",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.27.tgz",
+      "integrity": "sha512-qyUZfMnCg1KEz57r7pzFtSGt49f6RPkPBis3Vo4PbS7roQEDn22hiHzl/Lo1q4i4hDEgBJmBF/NTNg2XR0HbFg==",
       "requires": {
         "undici-types": "~5.26.4"
       }
     },
     "@types/qs": {
-      "version": "6.9.10",
-      "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.10.tgz",
-      "integrity": "sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw=="
+      "version": "6.9.12",
+      "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.12.tgz",
+      "integrity": "sha512-bZcOkJ6uWrL0Qb2NAWKa7TBU+mJHPzhx9jjLL1KHF+XpzEcR7EXHvjbHlGtR/IsP1vyPrehuS6XqkmaePy//mg=="
     },
     "@types/range-parser": {
       "version": "1.2.7",
@@ -1496,9 +1572,9 @@
       }
     },
     "@types/semver": {
-      "version": "7.5.6",
-      "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
-      "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==",
+      "version": "7.5.8",
+      "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
+      "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
       "dev": true
     },
     "@types/send": {
@@ -1677,9 +1753,9 @@
       }
     },
     "acorn": {
-      "version": "8.11.2",
-      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
-      "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==",
+      "version": "8.11.3",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
+      "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
       "dev": true
     },
     "acorn-jsx": {
@@ -2086,13 +2162,13 @@
       "dev": true
     },
     "array-buffer-byte-length": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz",
-      "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==",
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz",
+      "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==",
       "dev": true,
       "requires": {
-        "call-bind": "^1.0.2",
-        "is-array-buffer": "^3.0.1"
+        "call-bind": "^1.0.5",
+        "is-array-buffer": "^3.0.4"
       }
     },
     "array-differ": {
@@ -2180,17 +2256,18 @@
       "dev": true
     },
     "arraybuffer.prototype.slice": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz",
-      "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==",
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz",
+      "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==",
       "dev": true,
       "requires": {
-        "array-buffer-byte-length": "^1.0.0",
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.2.0",
-        "es-abstract": "^1.22.1",
-        "get-intrinsic": "^1.2.1",
-        "is-array-buffer": "^3.0.2",
+        "array-buffer-byte-length": "^1.0.1",
+        "call-bind": "^1.0.5",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.22.3",
+        "es-errors": "^1.2.1",
+        "get-intrinsic": "^1.2.3",
+        "is-array-buffer": "^3.0.4",
         "is-shared-array-buffer": "^1.0.2"
       }
     },
@@ -2274,10 +2351,13 @@
       "dev": true
     },
     "available-typed-arrays": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
-      "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==",
-      "dev": true
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+      "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
+      "dev": true,
+      "requires": {
+        "possible-typed-array-names": "^1.0.0"
+      }
     },
     "aws-sign2": {
       "version": "0.7.0",
@@ -2396,6 +2476,16 @@
         "file-uri-to-path": "1.0.0"
       }
     },
+    "bl": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+      "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+      "requires": {
+        "buffer": "^5.5.0",
+        "inherits": "^2.0.4",
+        "readable-stream": "^3.4.0"
+      }
+    },
     "bluebird": {
       "version": "3.7.2",
       "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
@@ -2428,26 +2518,24 @@
       "dev": true
     },
     "browserslist": {
-      "version": "4.22.2",
-      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz",
-      "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==",
+      "version": "4.23.0",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
+      "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
       "dev": true,
       "requires": {
-        "caniuse-lite": "^1.0.30001565",
-        "electron-to-chromium": "^1.4.601",
+        "caniuse-lite": "^1.0.30001587",
+        "electron-to-chromium": "^1.4.668",
         "node-releases": "^2.0.14",
         "update-browserslist-db": "^1.0.13"
       }
     },
     "buffer": {
-      "version": "4.9.2",
-      "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz",
-      "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==",
-      "dev": true,
+      "version": "5.7.1",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+      "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
       "requires": {
-        "base64-js": "^1.0.2",
-        "ieee754": "^1.1.4",
-        "isarray": "^1.0.0"
+        "base64-js": "^1.3.1",
+        "ieee754": "^1.1.13"
       }
     },
     "buffer-equal": {
@@ -2497,14 +2585,16 @@
       }
     },
     "call-bind": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
-      "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
+      "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
       "dev": true,
       "requires": {
+        "es-define-property": "^1.0.0",
+        "es-errors": "^1.3.0",
         "function-bind": "^1.1.2",
-        "get-intrinsic": "^1.2.1",
-        "set-function-length": "^1.1.1"
+        "get-intrinsic": "^1.2.4",
+        "set-function-length": "^1.2.1"
       }
     },
     "callsites": {
@@ -2520,9 +2610,9 @@
       "dev": true
     },
     "caniuse-lite": {
-      "version": "1.0.30001566",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001566.tgz",
-      "integrity": "sha512-ggIhCsTxmITBAMmK8yZjEhCO5/47jKXPu6Dha/wuCS4JePVL+3uiDEBuhu2aIoT+bqTOR8L76Ip1ARL9xYsEJA==",
+      "version": "1.0.30001597",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz",
+      "integrity": "sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==",
       "dev": true
     },
     "caseless": {
@@ -2572,6 +2662,17 @@
       "requires": {
         "ansi-styles": "^4.1.0",
         "supports-color": "^7.1.0"
+      },
+      "dependencies": {
+        "supports-color": {
+          "version": "7.2.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+          "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^4.0.0"
+          }
+        }
       }
     },
     "check-error": {
@@ -2705,10 +2806,9 @@
       }
     },
     "chownr": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
-      "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
-      "dev": true
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+      "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
     },
     "class-utils": {
       "version": "0.3.6",
@@ -3034,13 +3134,13 @@
       }
     },
     "d": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
-      "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz",
+      "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==",
       "dev": true,
       "requires": {
-        "es5-ext": "^0.10.50",
-        "type": "^1.0.1"
+        "es5-ext": "^0.10.64",
+        "type": "^2.7.2"
       }
     },
     "dashdash": {
@@ -3072,6 +3172,14 @@
       "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
       "dev": true
     },
+    "decompress-response": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+      "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+      "requires": {
+        "mimic-response": "^3.1.0"
+      }
+    },
     "deep-eql": {
       "version": "4.1.3",
       "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz",
@@ -3081,6 +3189,11 @@
         "type-detect": "^4.0.0"
       }
     },
+    "deep-extend": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+      "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="
+    },
     "deep-is": {
       "version": "0.1.4",
       "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -3128,14 +3241,14 @@
       "dev": true
     },
     "define-data-property": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
-      "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+      "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
       "dev": true,
       "requires": {
-        "get-intrinsic": "^1.2.1",
-        "gopd": "^1.0.1",
-        "has-property-descriptors": "^1.0.0"
+        "es-define-property": "^1.0.0",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.0.1"
       }
     },
     "define-properties": {
@@ -3207,8 +3320,7 @@
     "detect-libc": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
-      "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==",
-      "dev": true
+      "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="
     },
     "diff": {
       "version": "5.0.0",
@@ -3235,15 +3347,15 @@
       }
     },
     "duplexify": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz",
-      "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==",
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz",
+      "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==",
       "optional": true,
       "requires": {
         "end-of-stream": "^1.4.1",
         "inherits": "^2.0.3",
         "readable-stream": "^3.1.1",
-        "stream-shift": "^1.0.0"
+        "stream-shift": "^1.0.2"
       }
     },
     "each-props": {
@@ -3275,9 +3387,9 @@
       }
     },
     "electron-to-chromium": {
-      "version": "1.4.605",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.605.tgz",
-      "integrity": "sha512-V52j+P5z6cdRqTjPR/bYNxx7ETCHIkm5VIGuyCy3CMrfSnbEpIlLnk5oHmZo7gYvDfh2TfHeanB6rawyQ23ktg==",
+      "version": "1.4.703",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.703.tgz",
+      "integrity": "sha512-094ZZC4nHXPKl/OwPinSMtLN9+hoFkdfQGKnvXbY+3WEAYtVDpz9UhJIViiY6Zb8agvqxiaJzNG9M+pRZWvSZw==",
       "dev": true
     },
     "emoji-regex": {
@@ -3309,61 +3421,78 @@
       }
     },
     "es-abstract": {
-      "version": "1.22.3",
-      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz",
-      "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==",
-      "dev": true,
-      "requires": {
-        "array-buffer-byte-length": "^1.0.0",
-        "arraybuffer.prototype.slice": "^1.0.2",
-        "available-typed-arrays": "^1.0.5",
-        "call-bind": "^1.0.5",
-        "es-set-tostringtag": "^2.0.1",
+      "version": "1.22.5",
+      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.5.tgz",
+      "integrity": "sha512-oW69R+4q2wG+Hc3KZePPZxOiisRIqfKBVo/HLx94QcJeWGU/8sZhCvc829rd1kS366vlJbzBfXf9yWwf0+Ko7w==",
+      "dev": true,
+      "requires": {
+        "array-buffer-byte-length": "^1.0.1",
+        "arraybuffer.prototype.slice": "^1.0.3",
+        "available-typed-arrays": "^1.0.7",
+        "call-bind": "^1.0.7",
+        "es-define-property": "^1.0.0",
+        "es-errors": "^1.3.0",
+        "es-set-tostringtag": "^2.0.3",
         "es-to-primitive": "^1.2.1",
         "function.prototype.name": "^1.1.6",
-        "get-intrinsic": "^1.2.2",
-        "get-symbol-description": "^1.0.0",
+        "get-intrinsic": "^1.2.4",
+        "get-symbol-description": "^1.0.2",
         "globalthis": "^1.0.3",
         "gopd": "^1.0.1",
-        "has-property-descriptors": "^1.0.0",
-        "has-proto": "^1.0.1",
+        "has-property-descriptors": "^1.0.2",
+        "has-proto": "^1.0.3",
         "has-symbols": "^1.0.3",
-        "hasown": "^2.0.0",
-        "internal-slot": "^1.0.5",
-        "is-array-buffer": "^3.0.2",
+        "hasown": "^2.0.1",
+        "internal-slot": "^1.0.7",
+        "is-array-buffer": "^3.0.4",
         "is-callable": "^1.2.7",
-        "is-negative-zero": "^2.0.2",
+        "is-negative-zero": "^2.0.3",
         "is-regex": "^1.1.4",
-        "is-shared-array-buffer": "^1.0.2",
+        "is-shared-array-buffer": "^1.0.3",
         "is-string": "^1.0.7",
-        "is-typed-array": "^1.1.12",
+        "is-typed-array": "^1.1.13",
         "is-weakref": "^1.0.2",
         "object-inspect": "^1.13.1",
         "object-keys": "^1.1.1",
-        "object.assign": "^4.1.4",
-        "regexp.prototype.flags": "^1.5.1",
-        "safe-array-concat": "^1.0.1",
-        "safe-regex-test": "^1.0.0",
+        "object.assign": "^4.1.5",
+        "regexp.prototype.flags": "^1.5.2",
+        "safe-array-concat": "^1.1.0",
+        "safe-regex-test": "^1.0.3",
         "string.prototype.trim": "^1.2.8",
         "string.prototype.trimend": "^1.0.7",
         "string.prototype.trimstart": "^1.0.7",
-        "typed-array-buffer": "^1.0.0",
-        "typed-array-byte-length": "^1.0.0",
-        "typed-array-byte-offset": "^1.0.0",
-        "typed-array-length": "^1.0.4",
+        "typed-array-buffer": "^1.0.2",
+        "typed-array-byte-length": "^1.0.1",
+        "typed-array-byte-offset": "^1.0.2",
+        "typed-array-length": "^1.0.5",
         "unbox-primitive": "^1.0.2",
-        "which-typed-array": "^1.1.13"
+        "which-typed-array": "^1.1.14"
+      }
+    },
+    "es-define-property": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
+      "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
+      "dev": true,
+      "requires": {
+        "get-intrinsic": "^1.2.4"
       }
     },
+    "es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "dev": true
+    },
     "es-set-tostringtag": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz",
-      "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==",
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz",
+      "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==",
       "dev": true,
       "requires": {
-        "get-intrinsic": "^1.2.2",
-        "has-tostringtag": "^1.0.0",
-        "hasown": "^2.0.0"
+        "get-intrinsic": "^1.2.4",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.1"
       }
     },
     "es-to-primitive": {
@@ -3378,13 +3507,14 @@
       }
     },
     "es5-ext": {
-      "version": "0.10.62",
-      "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz",
-      "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==",
+      "version": "0.10.64",
+      "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz",
+      "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==",
       "dev": true,
       "requires": {
         "es6-iterator": "^2.0.3",
         "es6-symbol": "^3.1.3",
+        "esniff": "^2.0.1",
         "next-tick": "^1.1.0"
       }
     },
@@ -3406,13 +3536,13 @@
       }
     },
     "es6-symbol": {
-      "version": "3.1.3",
-      "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz",
-      "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==",
+      "version": "3.1.4",
+      "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz",
+      "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==",
       "dev": true,
       "requires": {
-        "d": "^1.0.1",
-        "ext": "^1.1.2"
+        "d": "^1.0.2",
+        "ext": "^1.7.0"
       }
     },
     "es6-weak-map": {
@@ -3428,9 +3558,9 @@
       }
     },
     "escalade": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
-      "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw=="
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
+      "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA=="
     },
     "escape-string-regexp": {
       "version": "4.0.0",
@@ -3439,16 +3569,16 @@
       "dev": true
     },
     "eslint": {
-      "version": "8.56.0",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz",
-      "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==",
+      "version": "8.57.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
+      "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
       "dev": true,
       "requires": {
         "@eslint-community/eslint-utils": "^4.2.0",
         "@eslint-community/regexpp": "^4.6.1",
         "@eslint/eslintrc": "^2.1.4",
-        "@eslint/js": "8.56.0",
-        "@humanwhocodes/config-array": "^0.11.13",
+        "@eslint/js": "8.57.0",
+        "@humanwhocodes/config-array": "^0.11.14",
         "@humanwhocodes/module-importer": "^1.0.1",
         "@nodelib/fs.walk": "^1.2.8",
         "@ungap/structured-clone": "^1.2.0",
@@ -3520,6 +3650,15 @@
             "is-glob": "^4.0.3"
           }
         },
+        "minimatch": {
+          "version": "3.1.2",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+          "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+          "dev": true,
+          "requires": {
+            "brace-expansion": "^1.1.7"
+          }
+        },
         "which": {
           "version": "2.0.2",
           "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -3547,6 +3686,18 @@
       "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
       "dev": true
     },
+    "esniff": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz",
+      "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
+      "dev": true,
+      "requires": {
+        "d": "^1.0.1",
+        "es5-ext": "^0.10.62",
+        "event-emitter": "^0.3.5",
+        "type": "^2.7.2"
+      }
+    },
     "espree": {
       "version": "9.6.1",
       "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
@@ -3610,6 +3761,16 @@
       "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
       "dev": true
     },
+    "event-emitter": {
+      "version": "0.3.5",
+      "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
+      "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
+      "dev": true,
+      "requires": {
+        "d": "1",
+        "es5-ext": "~0.10.14"
+      }
+    },
     "event-target-shim": {
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
@@ -3666,6 +3827,11 @@
         }
       }
     },
+    "expand-template": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
+      "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="
+    },
     "expand-tilde": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz",
@@ -3682,14 +3848,6 @@
       "dev": true,
       "requires": {
         "type": "^2.7.2"
-      },
-      "dependencies": {
-        "type": {
-          "version": "2.7.2",
-          "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz",
-          "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==",
-          "dev": true
-        }
       }
     },
     "extend": {
@@ -3782,6 +3940,15 @@
         "time-stamp": "^1.0.0"
       }
     },
+    "farmhash": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/farmhash/-/farmhash-3.3.0.tgz",
+      "integrity": "sha512-IZJWJXvX+TZJ4qZrcRZkDqI66s4VxrRD+NsduTSe0PZ9BGEDB53S0cd+e4rTXIWbL5k213W8cN6pMZuPVA+z0Q==",
+      "requires": {
+        "node-addon-api": "^5.1.0",
+        "prebuild-install": "^7.1.1"
+      }
+    },
     "fast-deep-equal": {
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -3813,18 +3980,18 @@
       "dev": true
     },
     "fast-xml-parser": {
-      "version": "4.3.2",
-      "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.3.2.tgz",
-      "integrity": "sha512-rmrXUXwbJedoXkStenj1kkljNF7ugn5ZjR9FJcwmCfcCbtOMDghPajbc+Tck6vE6F5XsDmx+Pr2le9fw8+pXBg==",
+      "version": "4.3.5",
+      "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.3.5.tgz",
+      "integrity": "sha512-sWvP1Pl8H03B8oFJpFR3HE31HUfwtX7Rlf9BNsvdpujD4n7WMhfmu8h9wOV2u+c1k0ZilTADhPqypzx2J690ZQ==",
       "optional": true,
       "requires": {
         "strnum": "^1.0.5"
       }
     },
     "fastq": {
-      "version": "1.15.0",
-      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
-      "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
+      "version": "1.17.1",
+      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
+      "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
       "dev": true,
       "requires": {
         "reusify": "^1.0.4"
@@ -4050,9 +4217,9 @@
       }
     },
     "flatted": {
-      "version": "3.2.9",
-      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz",
-      "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==",
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
+      "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
       "dev": true
     },
     "flush-write-stream": {
@@ -4184,6 +4351,11 @@
       "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==",
       "dev": true
     },
+    "fs-constants": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+      "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
+    },
     "fs-extra": {
       "version": "7.0.1",
       "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
@@ -4290,9 +4462,9 @@
       }
     },
     "gaxios": {
-      "version": "6.1.1",
-      "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.1.1.tgz",
-      "integrity": "sha512-bw8smrX+XlAoo9o1JAksBwX+hi/RG15J+NTSxmNPIclKC3ZVK6C2afwY8OSdRvOK0+ZLecUJYtj2MmjOt3Dm0w==",
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.3.0.tgz",
+      "integrity": "sha512-p+ggrQw3fBwH2F5N/PAI4k/G/y1art5OxKpb2J2chwNNHM4hHuAOtivjPuirMF4KNKwTTUal/lPfL2+7h2mEcg==",
       "optional": true,
       "requires": {
         "extend": "^3.0.2",
@@ -4329,11 +4501,12 @@
       "dev": true
     },
     "get-intrinsic": {
-      "version": "1.2.2",
-      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
-      "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==",
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
+      "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
       "dev": true,
       "requires": {
+        "es-errors": "^1.3.0",
         "function-bind": "^1.1.2",
         "has-proto": "^1.0.1",
         "has-symbols": "^1.0.3",
@@ -4353,13 +4526,14 @@
       "dev": true
     },
     "get-symbol-description": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
-      "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz",
+      "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==",
       "dev": true,
       "requires": {
-        "call-bind": "^1.0.2",
-        "get-intrinsic": "^1.1.1"
+        "call-bind": "^1.0.5",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.4"
       }
     },
     "get-value": {
@@ -4377,6 +4551,11 @@
         "assert-plus": "^1.0.0"
       }
     },
+    "github-from-package": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
+      "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="
+    },
     "glob": {
       "version": "7.2.3",
       "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -4389,6 +4568,17 @@
         "minimatch": "^3.1.1",
         "once": "^1.3.0",
         "path-is-absolute": "^1.0.0"
+      },
+      "dependencies": {
+        "minimatch": {
+          "version": "3.1.2",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+          "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+          "dev": true,
+          "requires": {
+            "brace-expansion": "^1.1.7"
+          }
+        }
       }
     },
     "glob-parent": {
@@ -4550,9 +4740,9 @@
       }
     },
     "google-auth-library": {
-      "version": "9.4.1",
-      "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.4.1.tgz",
-      "integrity": "sha512-Chs7cuzDuav8W/BXOoRgSXw4u0zxYtuqAHETDR5Q6dG1RwNwz7NUKjsDDHAsBV3KkiiJBtJqjbzy1XU1L41w1g==",
+      "version": "9.7.0",
+      "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.7.0.tgz",
+      "integrity": "sha512-I/AvzBiUXDzLOy4iIZ2W+Zq33W4lcukQv1nl7C8WUA6SQwyQwUwu3waNmWNAvzds//FG8SZ+DnKnW/2k6mQS8A==",
       "optional": true,
       "requires": {
         "base64-js": "^1.3.0",
@@ -4564,17 +4754,17 @@
       }
     },
     "google-gax": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.3.0.tgz",
-      "integrity": "sha512-SWHX72gbccNfpPoeTkNmZJxmLyKWeLr0+5Ch6qtrf4oAN8KFXnyXe5EixatILnJWufM3L59MRZ4hSJWVJ3IQqw==",
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.3.1.tgz",
+      "integrity": "sha512-qpSfslpwqToIgQ+Tf3MjWIDjYK4UFIZ0uz6nLtttlW9N1NQA4PhGf9tlGo6KDYJ4rgL2w4CjXVd0z5yeNpN/Iw==",
       "optional": true,
       "requires": {
-        "@grpc/grpc-js": "~1.9.6",
+        "@grpc/grpc-js": "~1.10.0",
         "@grpc/proto-loader": "^0.7.0",
         "@types/long": "^4.0.0",
         "abort-controller": "^3.0.0",
         "duplexify": "^4.0.0",
-        "google-auth-library": "^9.0.0",
+        "google-auth-library": "^9.3.0",
         "node-fetch": "^2.6.1",
         "object-hash": "^3.0.0",
         "proto3-json-serializer": "^2.0.0",
@@ -4605,9 +4795,9 @@
       "dev": true
     },
     "gtoken": {
-      "version": "7.0.1",
-      "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.0.1.tgz",
-      "integrity": "sha512-KcFVtoP1CVFtQu0aSk3AyAt2og66PFhZAlkUOuWKwzMLoulHXG5W5wE5xAnHb+yl3/wEFoqGW7/cDGMU8igDZQ==",
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
+      "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
       "optional": true,
       "requires": {
         "gaxios": "^6.0.0",
@@ -4870,18 +5060,18 @@
       "dev": true
     },
     "has-property-descriptors": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
-      "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+      "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
       "dev": true,
       "requires": {
-        "get-intrinsic": "^1.2.2"
+        "es-define-property": "^1.0.0"
       }
     },
     "has-proto": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
-      "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
+      "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
       "dev": true
     },
     "has-symbols": {
@@ -4891,12 +5081,12 @@
       "dev": true
     },
     "has-tostringtag": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
-      "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
       "dev": true,
       "requires": {
-        "has-symbols": "^1.0.2"
+        "has-symbols": "^1.0.3"
       }
     },
     "has-unicode": {
@@ -4976,9 +5166,9 @@
       }
     },
     "hasown": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
-      "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
       "dev": true,
       "requires": {
         "function-bind": "^1.1.2"
@@ -5022,6 +5212,19 @@
         "get-prop": "0.0.10",
         "minimist": "^1.2.0",
         "stream-buffers": "^3.0.0"
+      },
+      "dependencies": {
+        "buffer": {
+          "version": "4.9.2",
+          "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz",
+          "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==",
+          "dev": true,
+          "requires": {
+            "base64-js": "^1.0.2",
+            "ieee754": "^1.1.4",
+            "isarray": "^1.0.0"
+          }
+        }
       }
     },
     "http-parser-js": {
@@ -5063,9 +5266,9 @@
       }
     },
     "https-proxy-agent": {
-      "version": "7.0.2",
-      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz",
-      "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==",
+      "version": "7.0.4",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz",
+      "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==",
       "optional": true,
       "requires": {
         "agent-base": "^7.0.2",
@@ -5081,13 +5284,12 @@
     "ieee754": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
-      "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
-      "dev": true
+      "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
     },
     "ignore": {
-      "version": "5.3.0",
-      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz",
-      "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==",
+      "version": "5.3.1",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
+      "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==",
       "dev": true
     },
     "import-fresh": {
@@ -5136,16 +5338,15 @@
     "ini": {
       "version": "1.3.8",
       "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
-      "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
-      "dev": true
+      "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
     },
     "internal-slot": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz",
-      "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==",
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz",
+      "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==",
       "dev": true,
       "requires": {
-        "get-intrinsic": "^1.2.2",
+        "es-errors": "^1.3.0",
         "hasown": "^2.0.0",
         "side-channel": "^1.0.4"
       }
@@ -5182,14 +5383,13 @@
       }
     },
     "is-array-buffer": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz",
-      "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==",
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz",
+      "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==",
       "dev": true,
       "requires": {
         "call-bind": "^1.0.2",
-        "get-intrinsic": "^1.2.0",
-        "is-typed-array": "^1.1.10"
+        "get-intrinsic": "^1.2.1"
       }
     },
     "is-arrayish": {
@@ -5308,9 +5508,9 @@
       "dev": true
     },
     "is-negative-zero": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
-      "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==",
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
+      "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
       "dev": true
     },
     "is-number": {
@@ -5375,12 +5575,12 @@
       }
     },
     "is-shared-array-buffer": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz",
-      "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==",
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz",
+      "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==",
       "dev": true,
       "requires": {
-        "call-bind": "^1.0.2"
+        "call-bind": "^1.0.7"
       }
     },
     "is-stream": {
@@ -5407,12 +5607,12 @@
       }
     },
     "is-typed-array": {
-      "version": "1.1.12",
-      "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz",
-      "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==",
+      "version": "1.1.13",
+      "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz",
+      "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==",
       "dev": true,
       "requires": {
-        "which-typed-array": "^1.1.11"
+        "which-typed-array": "^1.1.14"
       }
     },
     "is-typedarray": {
@@ -5592,6 +5792,15 @@
           "requires": {
             "semver": "^7.5.3"
           }
+        },
+        "supports-color": {
+          "version": "7.2.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+          "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^4.0.0"
+          }
         }
       }
     },
@@ -5607,9 +5816,9 @@
       }
     },
     "istanbul-reports": {
-      "version": "3.1.6",
-      "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz",
-      "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==",
+      "version": "3.1.7",
+      "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz",
+      "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==",
       "dev": true,
       "requires": {
         "html-escaper": "^2.0.0",
@@ -5623,9 +5832,9 @@
       "dev": true
     },
     "jose": {
-      "version": "4.15.4",
-      "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz",
-      "integrity": "sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ=="
+      "version": "4.15.5",
+      "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.5.tgz",
+      "integrity": "sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg=="
     },
     "js-tokens": {
       "version": "4.0.0",
@@ -5779,9 +5988,9 @@
       "dev": true
     },
     "just-extend": {
-      "version": "4.2.1",
-      "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz",
-      "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==",
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz",
+      "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==",
       "dev": true
     },
     "jwa": {
@@ -5809,9 +6018,9 @@
       },
       "dependencies": {
         "@types/jsonwebtoken": {
-          "version": "9.0.5",
-          "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz",
-          "integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==",
+          "version": "9.0.6",
+          "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz",
+          "integrity": "sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==",
           "requires": {
             "@types/node": "*"
           }
@@ -6356,10 +6565,15 @@
         "mime-db": "1.52.0"
       }
     },
+    "mimic-response": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+      "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="
+    },
     "minimatch": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
-      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "version": "3.0.8",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz",
+      "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==",
       "dev": true,
       "requires": {
         "brace-expansion": "^1.1.7"
@@ -6368,8 +6582,7 @@
     "minimist": {
       "version": "1.2.8",
       "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
-      "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
-      "dev": true
+      "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="
     },
     "minipass": {
       "version": "5.0.0",
@@ -6425,10 +6638,15 @@
       "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
       "dev": true
     },
+    "mkdirp-classic": {
+      "version": "0.5.3",
+      "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+      "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
+    },
     "mocha": {
-      "version": "10.2.0",
-      "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz",
-      "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==",
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.3.0.tgz",
+      "integrity": "sha512-uF2XJs+7xSLsrmIvn37i/wnc91nw7XjOQB8ccyx5aEgdnohr7n+rEiZP23WkCYHjilR6+EboEnbq/ZQDz4LSbg==",
       "dev": true,
       "requires": {
         "ansi-colors": "4.1.1",
@@ -6438,13 +6656,12 @@
         "diff": "5.0.0",
         "escape-string-regexp": "4.0.0",
         "find-up": "5.0.0",
-        "glob": "7.2.0",
+        "glob": "8.1.0",
         "he": "1.2.0",
         "js-yaml": "4.1.0",
         "log-symbols": "4.1.0",
         "minimatch": "5.0.1",
         "ms": "2.1.3",
-        "nanoid": "3.3.3",
         "serialize-javascript": "6.0.0",
         "strip-json-comments": "3.1.1",
         "supports-color": "8.1.1",
@@ -6476,6 +6693,15 @@
           "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
           "dev": true
         },
+        "brace-expansion": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+          "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+          "dev": true,
+          "requires": {
+            "balanced-match": "^1.0.0"
+          }
+        },
         "chokidar": {
           "version": "3.5.3",
           "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
@@ -6511,28 +6737,16 @@
           "optional": true
         },
         "glob": {
-          "version": "7.2.0",
-          "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
-          "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
+          "version": "8.1.0",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
+          "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
           "dev": true,
           "requires": {
             "fs.realpath": "^1.0.0",
             "inflight": "^1.0.4",
             "inherits": "2",
-            "minimatch": "^3.0.4",
-            "once": "^1.3.0",
-            "path-is-absolute": "^1.0.0"
-          },
-          "dependencies": {
-            "minimatch": {
-              "version": "3.1.2",
-              "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
-              "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
-              "dev": true,
-              "requires": {
-                "brace-expansion": "^1.1.7"
-              }
-            }
+            "minimatch": "^5.0.1",
+            "once": "^1.3.0"
           }
         },
         "is-binary-path": {
@@ -6551,17 +6765,6 @@
           "dev": true,
           "requires": {
             "brace-expansion": "^2.0.1"
-          },
-          "dependencies": {
-            "brace-expansion": {
-              "version": "2.0.1",
-              "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
-              "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
-              "dev": true,
-              "requires": {
-                "balanced-match": "^1.0.0"
-              }
-            }
           }
         },
         "ms": {
@@ -6579,14 +6782,11 @@
             "picomatch": "^2.2.1"
           }
         },
-        "supports-color": {
-          "version": "8.1.1",
-          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
-          "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
-          "dev": true,
-          "requires": {
-            "has-flag": "^4.0.0"
-          }
+        "strip-json-comments": {
+          "version": "3.1.1",
+          "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+          "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+          "dev": true
         },
         "yargs": {
           "version": "16.2.0",
@@ -6647,18 +6847,12 @@
       }
     },
     "nan": {
-      "version": "2.18.0",
-      "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz",
-      "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==",
+      "version": "2.19.0",
+      "resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz",
+      "integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==",
       "dev": true,
       "optional": true
     },
-    "nanoid": {
-      "version": "3.3.3",
-      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz",
-      "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==",
-      "dev": true
-    },
     "nanomatch": {
       "version": "1.2.13",
       "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
@@ -6678,6 +6872,11 @@
         "to-regex": "^3.0.1"
       }
     },
+    "napi-build-utils": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
+      "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg=="
+    },
     "natural-compare": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@@ -6703,53 +6902,22 @@
       "dev": true
     },
     "nise": {
-      "version": "5.1.5",
-      "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.5.tgz",
-      "integrity": "sha512-VJuPIfUFaXNRzETTQEEItTOP8Y171ijr+JLq42wHes3DiryR8vT+1TXQW/Rx8JNUhyYYWyIvjXTU6dOhJcs9Nw==",
+      "version": "5.1.9",
+      "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz",
+      "integrity": "sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==",
       "dev": true,
       "requires": {
-        "@sinonjs/commons": "^2.0.0",
-        "@sinonjs/fake-timers": "^10.0.2",
-        "@sinonjs/text-encoding": "^0.7.1",
-        "just-extend": "^4.0.2",
-        "path-to-regexp": "^1.7.0"
-      },
-      "dependencies": {
-        "@sinonjs/commons": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz",
-          "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==",
-          "dev": true,
-          "requires": {
-            "type-detect": "4.0.8"
-          }
-        },
-        "@sinonjs/fake-timers": {
-          "version": "10.3.0",
-          "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz",
-          "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==",
-          "dev": true,
-          "requires": {
-            "@sinonjs/commons": "^3.0.0"
-          },
-          "dependencies": {
-            "@sinonjs/commons": {
-              "version": "3.0.0",
-              "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz",
-              "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==",
-              "dev": true,
-              "requires": {
-                "type-detect": "4.0.8"
-              }
-            }
-          }
-        }
+        "@sinonjs/commons": "^3.0.0",
+        "@sinonjs/fake-timers": "^11.2.2",
+        "@sinonjs/text-encoding": "^0.7.2",
+        "just-extend": "^6.2.0",
+        "path-to-regexp": "^6.2.1"
       }
     },
     "nock": {
-      "version": "13.5.1",
-      "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.1.tgz",
-      "integrity": "sha512-+s7b73fzj5KnxbKH4Oaqz07tQ8degcMilU4rrmnKvI//b0JMBU4wEXFQ8zqr+3+L4eWSfU3H/UoIVGUV0tue1Q==",
+      "version": "13.5.4",
+      "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.4.tgz",
+      "integrity": "sha512-yAyTfdeNJGGBFxWdzSKCBYxs5FxLbCg5X5Q4ets974hcQzG1+qCxvIyOo4j2Ry6MUlhWVMX4OoYDefAIIwupjw==",
       "dev": true,
       "requires": {
         "debug": "^4.1.0",
@@ -6757,11 +6925,18 @@
         "propagate": "^2.0.0"
       }
     },
+    "node-abi": {
+      "version": "3.56.0",
+      "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.56.0.tgz",
+      "integrity": "sha512-fZjdhDOeRcaS+rcpve7XuwHBmktS1nS1gzgghwKUQQ8nTy2FdSDr6ZT8k6YhvlJeHmmQMYiT/IH9hfco5zeW2Q==",
+      "requires": {
+        "semver": "^7.3.5"
+      }
+    },
     "node-addon-api": {
       "version": "5.1.0",
       "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
-      "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
-      "dev": true
+      "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA=="
     },
     "node-fetch": {
       "version": "2.7.0",
@@ -7524,21 +7699,10 @@
       "dev": true
     },
     "path-to-regexp": {
-      "version": "1.8.0",
-      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
-      "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
-      "dev": true,
-      "requires": {
-        "isarray": "0.0.1"
-      },
-      "dependencies": {
-        "isarray": {
-          "version": "0.0.1",
-          "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
-          "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
-          "dev": true
-        }
-      }
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz",
+      "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==",
+      "dev": true
     },
     "path-type": {
       "version": "4.0.0",
@@ -7663,6 +7827,31 @@
       "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==",
       "dev": true
     },
+    "possible-typed-array-names": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
+      "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==",
+      "dev": true
+    },
+    "prebuild-install": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz",
+      "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==",
+      "requires": {
+        "detect-libc": "^2.0.0",
+        "expand-template": "^2.0.3",
+        "github-from-package": "0.0.0",
+        "minimist": "^1.2.3",
+        "mkdirp-classic": "^0.5.3",
+        "napi-build-utils": "^1.0.1",
+        "node-abi": "^3.3.0",
+        "pump": "^3.0.0",
+        "rc": "^1.2.7",
+        "simple-get": "^4.0.0",
+        "tar-fs": "^2.0.0",
+        "tunnel-agent": "^0.6.0"
+      }
+    },
     "prelude-ls": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -7743,10 +7932,9 @@
       "dev": true
     },
     "pump": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
-      "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==",
-      "dev": true,
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
+      "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
       "requires": {
         "end-of-stream": "^1.1.0",
         "once": "^1.3.1"
@@ -7775,6 +7963,16 @@
             "stream-shift": "^1.0.0"
           }
         },
+        "pump": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
+          "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==",
+          "dev": true,
+          "requires": {
+            "end-of-stream": "^1.1.0",
+            "once": "^1.3.1"
+          }
+        },
         "readable-stream": {
           "version": "2.3.8",
           "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
@@ -7834,6 +8032,17 @@
         "safe-buffer": "^5.1.0"
       }
     },
+    "rc": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+      "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+      "requires": {
+        "deep-extend": "^0.6.0",
+        "ini": "~1.3.0",
+        "minimist": "^1.2.0",
+        "strip-json-comments": "~2.0.1"
+      }
+    },
     "read-pkg": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
@@ -8065,14 +8274,15 @@
       }
     },
     "regexp.prototype.flags": {
-      "version": "1.5.1",
-      "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
-      "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==",
+      "version": "1.5.2",
+      "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz",
+      "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==",
       "dev": true,
       "requires": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.2.0",
-        "set-function-name": "^2.0.0"
+        "call-bind": "^1.0.6",
+        "define-properties": "^1.2.1",
+        "es-errors": "^1.3.0",
+        "set-function-name": "^2.0.1"
       }
     },
     "release-zalgo": {
@@ -8274,13 +8484,12 @@
       "optional": true
     },
     "retry-request": {
-      "version": "7.0.1",
-      "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.1.tgz",
-      "integrity": "sha512-ZI6vJp9rfB71mrZpw+n9p/B6HCsd7QJlSEQftZ+xfJzr3cQ9EPGKw1FF0BnViJ0fYREX6FhymBD2CARpmsFciQ==",
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz",
+      "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==",
       "optional": true,
       "requires": {
         "@types/request": "^2.48.8",
-        "debug": "^4.1.1",
         "extend": "^3.0.2",
         "teeny-request": "^9.0.0"
       }
@@ -8419,13 +8628,13 @@
       }
     },
     "safe-array-concat": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz",
-      "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==",
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz",
+      "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==",
       "dev": true,
       "requires": {
-        "call-bind": "^1.0.2",
-        "get-intrinsic": "^1.2.1",
+        "call-bind": "^1.0.7",
+        "get-intrinsic": "^1.2.4",
         "has-symbols": "^1.0.3",
         "isarray": "^2.0.5"
       },
@@ -8453,13 +8662,13 @@
       }
     },
     "safe-regex-test": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz",
-      "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==",
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz",
+      "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==",
       "dev": true,
       "requires": {
-        "call-bind": "^1.0.2",
-        "get-intrinsic": "^1.1.3",
+        "call-bind": "^1.0.6",
+        "es-errors": "^1.3.0",
         "is-regex": "^1.1.4"
       }
     },
@@ -8470,9 +8679,9 @@
       "dev": true
     },
     "semver": {
-      "version": "7.5.4",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
-      "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+      "version": "7.6.0",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
+      "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
       "requires": {
         "lru-cache": "^6.0.0"
       }
@@ -8502,26 +8711,29 @@
       "dev": true
     },
     "set-function-length": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz",
-      "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==",
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+      "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
       "dev": true,
       "requires": {
-        "define-data-property": "^1.1.1",
-        "get-intrinsic": "^1.2.1",
+        "define-data-property": "^1.1.4",
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2",
+        "get-intrinsic": "^1.2.4",
         "gopd": "^1.0.1",
-        "has-property-descriptors": "^1.0.0"
+        "has-property-descriptors": "^1.0.2"
       }
     },
     "set-function-name": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz",
-      "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
+      "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
       "dev": true,
       "requires": {
-        "define-data-property": "^1.0.1",
+        "define-data-property": "^1.1.4",
+        "es-errors": "^1.3.0",
         "functions-have-names": "^1.2.3",
-        "has-property-descriptors": "^1.0.0"
+        "has-property-descriptors": "^1.0.2"
       }
     },
     "set-value": {
@@ -8569,14 +8781,15 @@
       "dev": true
     },
     "side-channel": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
-      "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
+      "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
       "dev": true,
       "requires": {
-        "call-bind": "^1.0.0",
-        "get-intrinsic": "^1.0.2",
-        "object-inspect": "^1.9.0"
+        "call-bind": "^1.0.7",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.4",
+        "object-inspect": "^1.13.1"
       }
     },
     "signal-exit": {
@@ -8585,6 +8798,21 @@
       "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
       "dev": true
     },
+    "simple-concat": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
+      "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="
+    },
+    "simple-get": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
+      "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
+      "requires": {
+        "decompress-response": "^6.0.0",
+        "once": "^1.3.1",
+        "simple-concat": "^1.0.0"
+      }
+    },
     "sinon": {
       "version": "17.0.1",
       "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz",
@@ -8600,10 +8828,19 @@
       },
       "dependencies": {
         "diff": {
-          "version": "5.1.0",
-          "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz",
-          "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==",
+          "version": "5.2.0",
+          "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
+          "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
           "dev": true
+        },
+        "supports-color": {
+          "version": "7.2.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+          "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+          "dev": true,
+          "requires": {
+            "has-flag": "^4.0.0"
+          }
         }
       }
     },
@@ -8795,9 +9032,9 @@
       }
     },
     "spdx-exceptions": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz",
-      "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==",
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
+      "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==",
       "dev": true
     },
     "spdx-expression-parse": {
@@ -8811,9 +9048,9 @@
       }
     },
     "spdx-license-ids": {
-      "version": "3.0.16",
-      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz",
-      "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==",
+      "version": "3.0.17",
+      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz",
+      "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==",
       "dev": true
     },
     "split-string": {
@@ -8903,9 +9140,9 @@
       "dev": true
     },
     "stream-shift": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz",
-      "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ=="
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz",
+      "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ=="
     },
     "streamfilter": {
       "version": "3.0.0",
@@ -9002,10 +9239,9 @@
       }
     },
     "strip-json-comments": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
-      "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
-      "dev": true
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+      "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="
     },
     "strnum": {
       "version": "1.0.5",
@@ -9020,9 +9256,9 @@
       "optional": true
     },
     "supports-color": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
-      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "version": "8.1.1",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+      "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
       "dev": true,
       "requires": {
         "has-flag": "^4.0.0"
@@ -9056,6 +9292,37 @@
         "minizlib": "^2.1.1",
         "mkdirp": "^1.0.3",
         "yallist": "^4.0.0"
+      },
+      "dependencies": {
+        "chownr": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+          "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
+          "dev": true
+        }
+      }
+    },
+    "tar-fs": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
+      "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
+      "requires": {
+        "chownr": "^1.1.1",
+        "mkdirp-classic": "^0.5.2",
+        "pump": "^3.0.0",
+        "tar-stream": "^2.1.4"
+      }
+    },
+    "tar-stream": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+      "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+      "requires": {
+        "bl": "^4.0.3",
+        "end-of-stream": "^1.4.1",
+        "fs-constants": "^1.0.0",
+        "inherits": "^2.0.3",
+        "readable-stream": "^3.1.1"
       }
     },
     "teeny-request": {
@@ -9327,7 +9594,6 @@
       "version": "0.6.0",
       "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
       "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
-      "dev": true,
       "requires": {
         "safe-buffer": "^5.0.1"
       }
@@ -9339,9 +9605,9 @@
       "dev": true
     },
     "type": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz",
-      "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==",
+      "version": "2.7.2",
+      "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz",
+      "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==",
       "dev": true
     },
     "type-check": {
@@ -9366,50 +9632,55 @@
       "dev": true
     },
     "typed-array-buffer": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz",
-      "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz",
+      "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==",
       "dev": true,
       "requires": {
-        "call-bind": "^1.0.2",
-        "get-intrinsic": "^1.2.1",
-        "is-typed-array": "^1.1.10"
+        "call-bind": "^1.0.7",
+        "es-errors": "^1.3.0",
+        "is-typed-array": "^1.1.13"
       }
     },
     "typed-array-byte-length": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz",
-      "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==",
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz",
+      "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==",
       "dev": true,
       "requires": {
-        "call-bind": "^1.0.2",
+        "call-bind": "^1.0.7",
         "for-each": "^0.3.3",
-        "has-proto": "^1.0.1",
-        "is-typed-array": "^1.1.10"
+        "gopd": "^1.0.1",
+        "has-proto": "^1.0.3",
+        "is-typed-array": "^1.1.13"
       }
     },
     "typed-array-byte-offset": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz",
-      "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz",
+      "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==",
       "dev": true,
       "requires": {
-        "available-typed-arrays": "^1.0.5",
-        "call-bind": "^1.0.2",
+        "available-typed-arrays": "^1.0.7",
+        "call-bind": "^1.0.7",
         "for-each": "^0.3.3",
-        "has-proto": "^1.0.1",
-        "is-typed-array": "^1.1.10"
+        "gopd": "^1.0.1",
+        "has-proto": "^1.0.3",
+        "is-typed-array": "^1.1.13"
       }
     },
     "typed-array-length": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz",
-      "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==",
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.5.tgz",
+      "integrity": "sha512-yMi0PlwuznKHxKmcpoOdeLwxBoVPkqZxd7q2FgMkmD3bNwvF5VW0+UlUQ1k1vmktTu4Yu13Q0RIxEP8+B+wloA==",
       "dev": true,
       "requires": {
-        "call-bind": "^1.0.2",
+        "call-bind": "^1.0.7",
         "for-each": "^0.3.3",
-        "is-typed-array": "^1.1.9"
+        "gopd": "^1.0.1",
+        "has-proto": "^1.0.3",
+        "is-typed-array": "^1.1.13",
+        "possible-typed-array-names": "^1.0.0"
       }
     },
     "typedarray": {
@@ -9814,16 +10085,16 @@
       "dev": true
     },
     "which-typed-array": {
-      "version": "1.1.13",
-      "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz",
-      "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==",
+      "version": "1.1.15",
+      "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz",
+      "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==",
       "dev": true,
       "requires": {
-        "available-typed-arrays": "^1.0.5",
-        "call-bind": "^1.0.4",
+        "available-typed-arrays": "^1.0.7",
+        "call-bind": "^1.0.7",
         "for-each": "^0.3.3",
         "gopd": "^1.0.1",
-        "has-tostringtag": "^1.0.0"
+        "has-tostringtag": "^1.0.2"
       }
     },
     "wide-align": {
diff --git a/package.json b/package.json
index 5391d11c81..c09bbbcd1f 100644
--- a/package.json
+++ b/package.json
@@ -200,6 +200,7 @@
     "@firebase/database-compat": "^1.0.2",
     "@firebase/database-types": "^1.0.0",
     "@types/node": "^20.10.3",
+    "farmhash": "^3.3.0",
     "jsonwebtoken": "^9.0.0",
     "jwks-rsa": "^3.0.1",
     "node-forge": "^1.3.1",
diff --git a/src/remote-config/condition-evaluator-internal.ts b/src/remote-config/condition-evaluator-internal.ts
new file mode 100644
index 0000000000..d36b787127
--- /dev/null
+++ b/src/remote-config/condition-evaluator-internal.ts
@@ -0,0 +1,168 @@
+/*!
+ * Copyright 2024 Google Inc.
+ *
+ * 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 {
+  AndCondition,
+  OneOfCondition,
+  EvaluationContext,
+  NamedCondition,
+  OrCondition,
+  PercentCondition,
+  PercentConditionOperator
+} from './remote-config-api';
+import * as farmhash from 'farmhash';
+
+/**
+ * Encapsulates condition evaluation logic to simplify organization and
+ * facilitate testing.
+ *
+ * @internal
+ */
+export class ConditionEvaluator {
+  private static MAX_CONDITION_RECURSION_DEPTH = 10;
+
+  public evaluateConditions(
+    namedConditions: NamedCondition[],
+    context: EvaluationContext): Map<string, boolean> {
+    // The order of the conditions is significant.
+    // A JS Map preserves the order of insertion ("Iteration happens in insertion order"
+    // - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map#description).
+    const evaluatedConditions = new Map();
+
+    for (const namedCondition of namedConditions) {
+      evaluatedConditions.set(
+        namedCondition.name,
+        this.evaluateCondition(namedCondition.condition, context));
+    }
+
+    return evaluatedConditions;
+  }
+
+  private evaluateCondition(
+    condition: OneOfCondition,
+    context: EvaluationContext,
+    nestingLevel = 0): boolean {
+    if (nestingLevel >= ConditionEvaluator.MAX_CONDITION_RECURSION_DEPTH) {
+      // TODO: add logging once we have a wrapped logger.
+      return false;
+    }
+    if (condition.orCondition) {
+      return this.evaluateOrCondition(condition.orCondition, context, nestingLevel + 1)
+    }
+    if (condition.andCondition) {
+      return this.evaluateAndCondition(condition.andCondition, context, nestingLevel + 1)
+    }
+    if (condition.true) {
+      return true;
+    }
+    if (condition.false) {
+      return false;
+    }
+    if (condition.percent) {
+      return this.evaluatePercentCondition(condition.percent, context);
+    }
+    // TODO: add logging once we have a wrapped logger.
+    return false;
+  }
+
+  private evaluateOrCondition(
+    orCondition: OrCondition,
+    context: EvaluationContext,
+    nestingLevel: number): boolean {
+
+    const subConditions = orCondition.conditions || [];
+
+    for (const subCondition of subConditions) {
+      // Recursive call.
+      const result = this.evaluateCondition(
+        subCondition, context, nestingLevel + 1);
+
+      // Short-circuit the evaluation result for true.
+      if (result) {
+        return result;
+      }
+    }
+    return false;
+  }
+
+  private evaluateAndCondition(
+    andCondition: AndCondition,
+    context: EvaluationContext,
+    nestingLevel: number): boolean {
+
+    const subConditions = andCondition.conditions || [];
+
+    for (const subCondition of subConditions) {
+      // Recursive call.
+      const result = this.evaluateCondition(
+        subCondition, context, nestingLevel + 1);
+
+      // Short-circuit the evaluation result for false.
+      if (!result) {
+        return result;
+      }
+    }
+    return true;
+  }
+
+  private evaluatePercentCondition(
+    percentCondition: PercentCondition,
+    context: EvaluationContext
+  ): boolean {
+    if (!context.randomizationId) {
+      // TODO: add logging once we have a wrapped logger.
+      return false;
+    }
+
+    // This is the entry point for processing percent condition data from the response.
+    // We're not using a proto library, so we can't assume undefined fields have
+    // default values.
+    const { seed, percentOperator, microPercent, microPercentRange } = percentCondition;
+
+    if (!percentOperator) {
+      // TODO: add logging once we have a wrapped logger.
+      return false;
+    }
+
+    const normalizedMicroPercent = microPercent || 0;
+    const normalizedMicroPercentUpperBound = microPercentRange?.microPercentUpperBound || 0;
+    const normalizedMicroPercentLowerBound = microPercentRange?.microPercentLowerBound || 0;
+
+    const seedPrefix = seed && seed.length > 0 ? `${seed}.` : '';
+    const stringToHash = `${seedPrefix}${context.randomizationId}`;
+    const hash64 = Math.abs(parseFloat(farmhash.fingerprint64(stringToHash)));
+
+    const instanceMicroPercentile = hash64 % (100 * 1_000_000);
+
+    switch (percentOperator) {
+    case PercentConditionOperator.LESS_OR_EQUAL:
+      return instanceMicroPercentile <= normalizedMicroPercent;
+    case PercentConditionOperator.GREATER_THAN:
+      return instanceMicroPercentile > normalizedMicroPercent;
+    case PercentConditionOperator.BETWEEN:
+      return instanceMicroPercentile > normalizedMicroPercentLowerBound
+        && instanceMicroPercentile <= normalizedMicroPercentUpperBound;
+    case PercentConditionOperator.UNKNOWN:
+    default:
+      break;
+    }
+
+    // TODO: add logging once we have a wrapped logger.
+    return false;
+  }
+}
diff --git a/src/remote-config/index.ts b/src/remote-config/index.ts
index aa09a8e18a..103ec462f3 100644
--- a/src/remote-config/index.ts
+++ b/src/remote-config/index.ts
@@ -25,12 +25,19 @@ import { FirebaseApp } from '../app/firebase-app';
 import { RemoteConfig } from './remote-config';
 
 export {
+  AndCondition,
+  EvaluationContext,
   ExplicitParameterValue,
   InAppDefaultValue,
   ListVersionsOptions,
   ListVersionsResult,
+  MicroPercentRange,
   NamedCondition,
+  OneOfCondition,
+  OrCondition,
   ParameterValueType,
+  PercentConditionOperator,
+  PercentCondition,
   RemoteConfigCondition,
   RemoteConfigParameter,
   RemoteConfigParameterGroup,
diff --git a/src/remote-config/remote-config-api.ts b/src/remote-config/remote-config-api.ts
index 2ad15eedd5..e102094cde 100644
--- a/src/remote-config/remote-config-api.ts
+++ b/src/remote-config/remote-config-api.ts
@@ -56,8 +56,8 @@ export interface RemoteConfigCondition {
 
 /**
  * Represents a Remote Config condition in the dataplane.
- * A condition targets a specific group of users. A list of these conditions make up
- * part of a Remote Config template.
+ * A condition targets a specific group of users. A list of these conditions
+ * comprise part of a Remote Config template.
  */
 export interface NamedCondition {
 
@@ -65,6 +65,152 @@ export interface NamedCondition {
    * A non-empty and unique name of this condition.
    */
   name: string;
+
+  /**
+   * The logic of this condition.
+   * See the documentation on
+   * {@link https://firebase.google.com/docs/remote-config/condition-reference | condition expressions}
+   * for the expected syntax of this field.
+   */
+  condition: OneOfCondition;
+}
+
+/**
+ * Represents a condition that may be one of several types.
+ * Only the first defined field will be processed.
+ */
+export interface OneOfCondition {
+
+  /**
+   * Makes this condition an OR condition.
+   */
+  orCondition?: OrCondition;
+
+  /**
+   * Makes this condition an AND condition.
+   */
+  andCondition?: AndCondition;
+
+  /**
+   * Makes this condition a constant true.
+   */
+  true?: Record<string, never>;
+
+  /**
+   * Makes this condition a constant false.
+   */
+  false?: Record<string, never>;
+
+  /**
+   * Makes this condition a percent condition.
+   */
+  percent?: PercentCondition;
+}
+
+/**
+ * Represents a collection of conditions that evaluate to true if all are true.
+ */
+export interface AndCondition {
+
+  /**
+   * The collection of conditions.
+   */
+  conditions?: Array<OneOfCondition>;
+}
+
+/**
+ * Represents a collection of conditions that evaluate to true if any are true.
+ */
+export interface OrCondition {
+
+  /**
+   * The collection of conditions.
+   */
+  conditions?: Array<OneOfCondition>;
+}
+
+/**
+ * Defines supported operators for percent conditions.
+ */
+export enum PercentConditionOperator {
+
+  /**
+   * A catchall error case.
+   */
+  UNKNOWN = 'UNKNOWN',
+
+  /**
+   * Target percentiles less than or equal to the target percent.
+   * A condition using this operator must specify microPercent.
+   */
+  LESS_OR_EQUAL = 'LESS_OR_EQUAL',
+
+  /**
+   * Target percentiles greater than the target percent.
+   * A condition using this operator must specify microPercent.
+   */
+  GREATER_THAN = 'GREATER_THAN',
+
+  /**
+   * Target percentiles within an interval defined by a lower bound and an
+   * upper bound. The lower bound is an exclusive (open) bound and the
+   * micro_percent_range_upper_bound is an inclusive (closed) bound.
+   * A condition using this operator must specify microPercentRange.
+   */
+  BETWEEN = 'BETWEEN'
+}
+
+/**
+ * Represents the limit of percentiles to target in micro-percents.
+ * The value must be in the range [0 and 100000000]
+ */
+export interface MicroPercentRange {
+
+  /**
+   * The lower limit of percentiles to target in micro-percents.
+   * The value must be in the range [0 and 100000000].
+   */
+  microPercentLowerBound?: number;
+
+  /**
+   * The upper limit of percentiles to target in micro-percents.
+   * The value must be in the range [0 and 100000000].
+   */
+  microPercentUpperBound?: number;
+}
+
+/**
+ * Represents a condition that compares the instance pseudo-random
+ * percentile to a given limit.
+ */
+export interface PercentCondition {
+
+  /**
+   * The choice of percent operator to determine how to compare targets
+   * to percent(s).
+   */
+  percentOperator?: PercentConditionOperator;
+
+  /**
+   * The limit of percentiles to target in micro-percents when
+   * using the LESS_OR_EQUAL and GREATER_THAN operators. The value must
+   * be in the range [0 and 100000000].
+   */
+  microPercent?: number;
+
+  /**
+   * The seed used when evaluating the hash function to map an instance to
+   * a value in the hash space. This is a string which can have 0 - 32
+   * characters and can contain ASCII characters [-_.0-9a-zA-Z].The string
+   * is case-sensitive.
+   */
+  seed?: string;
+
+  /**
+   * The micro-percent interval to be used with the
+   * BETWEEN operator.
+   */
+  microPercentRange?: MicroPercentRange;
 }
 
 /**
@@ -244,7 +390,7 @@ export interface ServerTemplate {
   /**
    * Evaluates the current template to produce a {@link ServerConfig}.
    */
-  evaluate(): ServerConfig;
+  evaluate(context?: EvaluationContext): ServerConfig;
 
   /**
    * Fetches and caches the current active version of the
@@ -253,6 +399,18 @@ export interface ServerTemplate {
   load(): Promise<void>;
 }
 
+/**
+ * Represents template evaluation input signals.
+ */
+export type EvaluationContext = {
+
+  /**
+   * Defines the identifier to use when splitting a group. For example,
+   * this is used by the percent condition.
+   */
+  randomizationId?: string
+};
+
 /**
  * Interface representing a Remote Config user.
  */
diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts
index d603919a3a..1a720f9220 100644
--- a/src/remote-config/remote-config.ts
+++ b/src/remote-config/remote-config.ts
@@ -17,6 +17,7 @@
 import { App } from '../app';
 import * as validator from '../utils/validator';
 import { FirebaseRemoteConfigError, RemoteConfigApiClient } from './remote-config-api-client-internal';
+import { ConditionEvaluator } from './condition-evaluator-internal';
 import {
   ListVersionsOptions,
   ListVersionsResult,
@@ -31,6 +32,8 @@ import {
   InAppDefaultValue,
   ParameterValueType,
   ServerConfig,
+  RemoteConfigParameterValue,
+  EvaluationContext,
   ServerTemplateData,
   ServerTemplateOptions,
   NamedCondition,
@@ -191,7 +194,8 @@ export class RemoteConfig {
    * Synchronously instantiates {@link ServerTemplate}.
    */
   public initServerTemplate(options?: ServerTemplateOptions): ServerTemplate {
-    const template = new ServerTemplateImpl(this.client, options?.defaultConfig);
+    const template = new ServerTemplateImpl(
+      this.client, new ConditionEvaluator(), options?.defaultConfig);
     if (options?.template) {
       template.cache = options?.template;
     }
@@ -291,6 +295,7 @@ class ServerTemplateImpl implements ServerTemplate {
 
   constructor(
     private readonly apiClient: RemoteConfigApiClient,
+    private readonly conditionEvaluator: ConditionEvaluator,
     public readonly defaultConfig: ServerConfig = {}
   ) { }
 
@@ -307,17 +312,49 @@ class ServerTemplateImpl implements ServerTemplate {
   /**
    * Evaluates the current template in cache to produce a {@link ServerConfig}.
    */
-  public evaluate(): ServerConfig {
+  public evaluate(context: EvaluationContext = {}): ServerConfig {
     if (!this.cache) {
+
+      // 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().');
     }
 
+    const evaluatedConditions = this.conditionEvaluator.evaluateConditions(
+      this.cache.conditions, context);
+
     const evaluatedConfig: ServerConfig = {};
 
     for (const [key, parameter] of Object.entries(this.cache.parameters)) {
-      const { defaultValue, valueType } = parameter;
+      const { conditionalValues, defaultValue, valueType } = parameter;
+
+      // Supports parameters with no conditional values.
+      const normalizedConditionalValues = conditionalValues || {};
+
+      let parameterValueWrapper: RemoteConfigParameterValue | undefined = undefined;
+
+      // Iterates in order over condition list. If there is a value associated
+      // with a condition, this checks if the condition is true.
+      for (const [conditionName, conditionEvaluation] of evaluatedConditions) {
+        if (normalizedConditionalValues[conditionName] && conditionEvaluation) {
+          parameterValueWrapper = normalizedConditionalValues[conditionName];
+          break;
+        }
+      }
+
+      if (parameterValueWrapper && (parameterValueWrapper as InAppDefaultValue).useInAppDefault) {
+        // TODO: add logging once we have a wrapped logger.
+        continue;
+      }
+
+      if (parameterValueWrapper) {
+        const parameterValue = (parameterValueWrapper as ExplicitParameterValue).value;
+        evaluatedConfig[key] = this.parseRemoteConfigParameterValue(valueType, parameterValue);
+        continue;
+      }
 
       if (!defaultValue) {
         // TODO: add logging once we have a wrapped logger.
@@ -330,7 +367,6 @@ class ServerTemplateImpl implements ServerTemplate {
       }
 
       const parameterDefaultValue = (defaultValue as ExplicitParameterValue).value;
-
       evaluatedConfig[key] = this.parseRemoteConfigParameterValue(valueType, parameterDefaultValue);
     }
 
@@ -351,25 +387,25 @@ class ServerTemplateImpl implements ServerTemplate {
   }
 
   /**
-   * Private helper method that processes and parses a parameter value based on {@link ParameterValueType}.
+   * Private helper method that coerces a parameter value string to the {@link ParameterValueType}.
    */
   private parseRemoteConfigParameterValue(parameterType: ParameterValueType | undefined,
-    parameterDefaultValue: string): string | number | boolean {
+    parameterValue: string): string | number | boolean {
     const BOOLEAN_TRUTHY_VALUES = ['1', 'true', 't', 'yes', 'y', 'on'];
     const DEFAULT_VALUE_FOR_NUMBER = 0;
     const DEFAULT_VALUE_FOR_STRING = '';
 
     if (parameterType === 'BOOLEAN') {
-      return BOOLEAN_TRUTHY_VALUES.indexOf(parameterDefaultValue) >= 0;
+      return BOOLEAN_TRUTHY_VALUES.indexOf(parameterValue) >= 0;
     } else if (parameterType === 'NUMBER') {
-      const num = Number(parameterDefaultValue);
+      const num = Number(parameterValue);
       if (isNaN(num)) {
         return DEFAULT_VALUE_FOR_NUMBER;
       }
       return num;
     } else {
       // Treat everything else as string
-      return parameterDefaultValue || DEFAULT_VALUE_FOR_STRING;
+      return parameterValue || DEFAULT_VALUE_FOR_STRING;
     }
   }
 }
diff --git a/test/unit/index.spec.ts b/test/unit/index.spec.ts
index c2ce02ff3f..29516d7a82 100644
--- a/test/unit/index.spec.ts
+++ b/test/unit/index.spec.ts
@@ -97,6 +97,7 @@ import './security-rules/security-rules-api-client.spec';
 import './remote-config/index.spec';
 import './remote-config/remote-config.spec';
 import './remote-config/remote-config-api-client.spec';
+import './remote-config/condition-evaluator.spec';
 
 // AppCheck
 import './app-check/app-check.spec';
diff --git a/test/unit/remote-config/condition-evaluator.spec.ts b/test/unit/remote-config/condition-evaluator.spec.ts
new file mode 100644
index 0000000000..3bd6d0f1fc
--- /dev/null
+++ b/test/unit/remote-config/condition-evaluator.spec.ts
@@ -0,0 +1,794 @@
+/*!
+ * Copyright 2024 Google Inc.
+ *
+ * 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 sinon from 'sinon';
+import { ConditionEvaluator } from '../../../src/remote-config/condition-evaluator-internal';
+import {
+  PercentConditionOperator,
+  PercentCondition
+} from '../../../src/remote-config/remote-config-api';
+import { v4 as uuidv4 } from 'uuid';
+import { clone } from 'lodash';
+import * as farmhash from 'farmhash';
+
+const expect = chai.expect;
+
+
+
+describe('ConditionEvaluator', () => {
+  let stubs: sinon.SinonStub[] = [];
+
+  afterEach(() => {
+    for (const stub of stubs) {
+      stub.restore();
+    }
+    stubs = [];
+  });
+
+  describe('evaluateConditions', () => {
+    it('should evaluate empty OR condition to false', () => {
+      const condition = {
+        name: 'is_enabled',
+        condition: {
+          orCondition: {
+          }
+        }
+      };
+      const context = {}
+      const evaluator = new ConditionEvaluator();
+      expect(evaluator.evaluateConditions([condition], context)).deep.equals(
+        new Map([['is_enabled', false]]));
+    });
+
+    it('should evaluate empty OR.AND condition to true', () => {
+      const condition = {
+        name: 'is_enabled',
+        condition: {
+          orCondition: {
+            conditions: [
+              {
+                andCondition: {
+                }
+              }
+            ]
+          }
+        }
+      };
+      const context = {}
+      const evaluator = new ConditionEvaluator();
+      expect(evaluator.evaluateConditions([condition], context)).deep.equals(
+        new Map([['is_enabled', true]]));
+    });
+
+    it('should evaluate OR.AND.TRUE condition to true', () => {
+      const condition = {
+        name: 'is_enabled',
+        condition: {
+          orCondition: {
+            conditions: [
+              {
+                andCondition: {
+                  conditions: [
+                    {
+                      true: {
+                      }
+                    }
+                  ]
+                }
+              }
+            ]
+          }
+        }
+      };
+      const context = {}
+      const evaluator = new ConditionEvaluator();
+      expect(evaluator.evaluateConditions([condition], context)).deep.equals(
+        new Map([['is_enabled', true]]));
+    });
+
+    it('should evaluate OR.AND.FALSE condition to false', () => {
+      const condition = {
+        name: 'is_enabled',
+        condition: {
+          orCondition: {
+            conditions: [
+              {
+                andCondition: {
+                  conditions: [
+                    {
+                      false: {
+                      }
+                    }
+                  ]
+                }
+              }
+            ]
+          }
+        }
+      };
+      const context = {}
+      const evaluator = new ConditionEvaluator();
+      expect(evaluator.evaluateConditions([condition], context)).deep.equals(
+        new Map([['is_enabled', false]]));
+    });
+
+    it('should evaluate non-OR top-level condition', () => {
+      // The server wraps conditions in OR.AND, but the evaluation logic
+      // is more general.
+      const condition = {
+        name: 'is_enabled',
+        condition: {
+          true: {
+          }
+        }
+      };
+      const context = {}
+      const evaluator = new ConditionEvaluator();
+      expect(evaluator.evaluateConditions([condition], context)).deep.equals(
+        new Map([['is_enabled', true]]));
+    });
+
+    describe('percentCondition', () => {
+      it('should evaluate an unknown operator to false', () => {
+        // Verifies future operators won't trigger errors.
+        const condition = {
+          name: 'is_enabled',
+          condition: {
+            orCondition: {
+              conditions: [{
+                andCondition: {
+                  conditions: [{
+                    percent: {
+                      percentOperator: PercentConditionOperator.UNKNOWN
+                    }
+                  }],
+                }
+              }]
+            }
+          }
+        };
+        const context = { randomizationId: '123' }
+        const evaluator = new ConditionEvaluator();
+        expect(evaluator.evaluateConditions([condition], context)).deep.equals(
+          new Map([['is_enabled', false]]));
+      });
+
+      it('should evaluate less or equal to max to true', () => {
+        const condition = {
+          name: 'is_enabled',
+          condition: {
+            orCondition: {
+              conditions: [{
+                andCondition: {
+                  conditions: [{
+                    percent: {
+                      percentOperator: PercentConditionOperator.LESS_OR_EQUAL,
+                      seed: 'abcdef',
+                      microPercent: 100_000_000
+                    }
+                  }],
+                }
+              }]
+            }
+          }
+        };
+        const context = { randomizationId: '123' }
+        const evaluator = new ConditionEvaluator();
+        expect(evaluator.evaluateConditions([condition], context)).deep.equals(
+          new Map([['is_enabled', true]]));
+      });
+
+      it('should evaluate less or equal to min to false', () => {
+        const condition = {
+          name: 'is_enabled',
+          condition: {
+            orCondition: {
+              conditions: [{
+                andCondition: {
+                  conditions: [{
+                    percent: {
+                      percentOperator: PercentConditionOperator.LESS_OR_EQUAL,
+                      seed: 'abcdef',
+                      microPercent: 0
+                    }
+                  }],
+                }
+              }]
+            }
+          }
+        };
+        const context = { randomizationId: '123' }
+        const evaluator = new ConditionEvaluator();
+        expect(evaluator.evaluateConditions([condition], context)).deep.equals(
+          new Map([['is_enabled', false]]));
+      });
+
+      it('should use zero for undefined microPercent', () => {
+        // Stubs ID hasher to return a number larger than zero.
+        const stub = sinon
+          .stub(farmhash, 'fingerprint64')
+          .returns('1');
+        stubs.push(stub);
+
+        const condition = {
+          name: 'is_enabled',
+          condition: {
+            orCondition: {
+              conditions: [{
+                andCondition: {
+                  conditions: [{
+                    percent: {
+                      percentOperator: PercentConditionOperator.LESS_OR_EQUAL,
+                      // Leaves microPercent undefined
+                    }
+                  }],
+                }
+              }]
+            }
+          }
+        };
+        const context = { randomizationId: '123' }
+        const evaluator = new ConditionEvaluator();
+        const actual = evaluator.evaluateConditions([condition], context)
+          .get('is_enabled');
+
+        // Evaluates false because 1 is not <= 0
+        expect(actual).to.be.false;
+      });
+
+      it('should use zeros for undefined microPercentRange', () => {
+        // Stubs ID hasher to return a number in range.
+        const stub = sinon
+          .stub(farmhash, 'fingerprint64')
+          .returns('1');
+        stubs.push(stub);
+
+        const condition = {
+          name: 'is_enabled',
+          condition: {
+            orCondition: {
+              conditions: [{
+                andCondition: {
+                  conditions: [{
+                    percent: {
+                      percentOperator: PercentConditionOperator.BETWEEN,
+                      // Leaves microPercentRange undefined
+                    }
+                  }],
+                }
+              }]
+            }
+          }
+        };
+        const context = { randomizationId: '123' }
+        const evaluator = new ConditionEvaluator();
+        const actual = evaluator.evaluateConditions([condition], context)
+          .get('is_enabled');
+
+        // Evaluates false because 1 is not in (0,0]
+        expect(actual).to.be.false;
+      });
+
+      it('should use zero for undefined microPercentUpperBound', () => {
+        // Stubs ID hasher to return a number outside range.
+        const stub = sinon
+          .stub(farmhash, 'fingerprint64')
+          .returns('1');
+        stubs.push(stub);
+
+        const condition = {
+          name: 'is_enabled',
+          condition: {
+            orCondition: {
+              conditions: [{
+                andCondition: {
+                  conditions: [{
+                    percent: {
+                      percentOperator: PercentConditionOperator.BETWEEN,
+                      microPercentRange: {
+                        microPercentLowerBound: 0
+                        // Leaves upper bound undefined
+                      }
+                    }
+                  }],
+                }
+              }]
+            }
+          }
+        };
+        const context = { randomizationId: '123' }
+        const evaluator = new ConditionEvaluator();
+        const actual = evaluator.evaluateConditions([condition], context)
+          .get('is_enabled');
+
+        // Evaluates false because 1 is not in (0,0]
+        expect(actual).to.be.false;
+      });
+
+      it('should use zero for undefined microPercentLowerBound', () => {
+        // Stubs ID hasher to return a number in range.
+        const stub = sinon
+          .stub(farmhash, 'fingerprint64')
+          .returns('1');
+        stubs.push(stub);
+
+        const condition = {
+          name: 'is_enabled',
+          condition: {
+            orCondition: {
+              conditions: [{
+                andCondition: {
+                  conditions: [{
+                    percent: {
+                      percentOperator: PercentConditionOperator.BETWEEN,
+                      microPercentRange: {
+                        microPercentUpperBound: 1
+                        // Leaves lower bound undefined
+                      }
+                    }
+                  }],
+                }
+              }]
+            }
+          }
+        };
+        const context = { randomizationId: '123' }
+        const evaluator = new ConditionEvaluator();
+        const actual = evaluator.evaluateConditions([condition], context)
+          .get('is_enabled');
+
+        // Evaluates true because 1 is in (0,1]
+        expect(actual).to.be.true;
+      });
+
+      it('should evaluate 9 as less or equal to 10', () => {
+        const stub = sinon
+          .stub(farmhash, 'fingerprint64')
+          .returns('9');
+
+        stubs.push(stub);
+        const condition = {
+          name: 'is_enabled',
+          condition: {
+            orCondition: {
+              conditions: [{
+                andCondition: {
+                  conditions: [{
+                    percent: {
+                      percentOperator: PercentConditionOperator.LESS_OR_EQUAL,
+                      seed: 'abcdef',
+                      microPercent: 10
+                    }
+                  }],
+                }
+              }]
+            }
+          }
+        };
+        const context = { randomizationId: '123' }
+        const evaluator = new ConditionEvaluator();
+        const actual = evaluator.evaluateConditions([condition], context)
+          .get('is_enabled');
+        expect(actual).to.be.true;
+      });
+
+      it('should evaluate 10 as less or equal to 10', () => {
+        const stub = sinon
+          .stub(farmhash, 'fingerprint64')
+          .returns('10');
+
+        stubs.push(stub);
+        const condition = {
+          name: 'is_enabled',
+          condition: {
+            orCondition: {
+              conditions: [{
+                andCondition: {
+                  conditions: [{
+                    percent: {
+                      percentOperator: PercentConditionOperator.LESS_OR_EQUAL,
+                      seed: 'abcdef',
+                      microPercent: 10
+                    }
+                  }],
+                }
+              }]
+            }
+          }
+        };
+        const context = { randomizationId: '123' }
+        const evaluator = new ConditionEvaluator();
+        const actual = evaluator.evaluateConditions([condition], context)
+          .get('is_enabled');
+        expect(actual).to.be.true;
+      });
+
+      it('should evaluate 11 as not less or equal to 10', () => {
+        const stub = sinon
+          .stub(farmhash, 'fingerprint64')
+          .returns('11');
+
+        stubs.push(stub);
+        const condition = {
+          name: 'is_enabled',
+          condition: {
+            orCondition: {
+              conditions: [{
+                andCondition: {
+                  conditions: [{
+                    percent: {
+                      percentOperator: PercentConditionOperator.LESS_OR_EQUAL,
+                      seed: 'abcdef',
+                      microPercent: 10
+                    }
+                  }],
+                }
+              }]
+            }
+          }
+        };
+        const context = { randomizationId: '123' }
+        const evaluator = new ConditionEvaluator();
+        const actual = evaluator.evaluateConditions([condition], context)
+          .get('is_enabled');
+        expect(actual).to.be.false;
+      });
+
+      it('should evaluate greater than min to true', () => {
+        const condition = {
+          name: 'is_enabled',
+          condition: {
+            orCondition: {
+              conditions: [{
+                andCondition: {
+                  conditions: [{
+                    percent: {
+                      percentOperator: PercentConditionOperator.GREATER_THAN,
+                      seed: 'abcdef',
+                      microPercent: 0
+                    }
+                  }],
+                }
+              }]
+            }
+          }
+        };
+        const context = { randomizationId: '123' }
+        const evaluator = new ConditionEvaluator();
+        expect(evaluator.evaluateConditions([condition], context)).deep.equals(
+          new Map([['is_enabled', true]]));
+      });
+
+      it('should evaluate 11M as greater than 10M', () => {
+        const stub = sinon
+          .stub(farmhash, 'fingerprint64')
+          .returns('11');
+
+        stubs.push(stub);
+        const condition = {
+          name: 'is_enabled',
+          condition: {
+            orCondition: {
+              conditions: [{
+                andCondition: {
+                  conditions: [{
+                    percent: {
+                      percentOperator: PercentConditionOperator.GREATER_THAN,
+                      seed: 'abcdef',
+                      microPercent: 10
+                    }
+                  }],
+                }
+              }]
+            }
+          }
+        };
+        const context = { randomizationId: '123' }
+        const evaluator = new ConditionEvaluator();
+        const actual = evaluator.evaluateConditions([condition], context)
+          .get('is_enabled');
+        expect(actual).to.be.true;
+      });
+
+      it('should evaluate 9 as not greater than 10', () => {
+        const stub = sinon
+          .stub(farmhash, 'fingerprint64')
+          .returns('9');
+        stubs.push(stub);
+
+        const condition = {
+          name: 'is_enabled',
+          condition: {
+            orCondition: {
+              conditions: [{
+                andCondition: {
+                  conditions: [{
+                    percent: {
+                      percentOperator: PercentConditionOperator.GREATER_THAN,
+                      seed: 'abcdef',
+                      microPercent: 10
+                    }
+                  }],
+                }
+              }]
+            }
+          }
+        };
+        const context = { randomizationId: '123' }
+        const evaluator = new ConditionEvaluator();
+        const actual = evaluator.evaluateConditions([condition], context)
+          .get('is_enabled');
+        expect(actual).to.be.false;
+      });
+
+      it('should evaluate greater than max to false', () => {
+        const condition = {
+          name: 'is_enabled',
+          condition: {
+            orCondition: {
+              conditions: [{
+                andCondition: {
+                  conditions: [{
+                    percent: {
+                      percentOperator: PercentConditionOperator.GREATER_THAN,
+                      seed: 'abcdef',
+                      microPercent: 100_000_000
+                    }
+                  }],
+                }
+              }]
+            }
+          }
+        };
+        const context = { randomizationId: '123' }
+        const evaluator = new ConditionEvaluator();
+        expect(evaluator.evaluateConditions([condition], context)).deep.equals(
+          new Map([['is_enabled', false]]));
+      });
+
+      it('should evaluate between min and max to true', () => {
+        const condition = {
+          name: 'is_enabled',
+          condition: {
+            orCondition: {
+              conditions: [{
+                andCondition: {
+                  conditions: [{
+                    percent: {
+                      percentOperator: PercentConditionOperator.BETWEEN,
+                      seed: 'abcdef',
+                      microPercentRange: {
+                        microPercentLowerBound: 0,
+                        microPercentUpperBound: 100_000_000
+                      }
+                    }
+                  }],
+                }
+              }]
+            }
+          }
+        };
+        const context = { randomizationId: '123' }
+        const evaluator = new ConditionEvaluator();
+        expect(evaluator.evaluateConditions([condition], context)).deep.equals(
+          new Map([['is_enabled', true]]));
+      });
+
+      it('should evaluate 10 as between 9 and 11', () => {
+        const stub = sinon
+          .stub(farmhash, 'fingerprint64')
+          .returns('10');
+        stubs.push(stub);
+
+        const condition = {
+          name: 'is_enabled',
+          condition: {
+            orCondition: {
+              conditions: [{
+                andCondition: {
+                  conditions: [{
+                    percent: {
+                      percentOperator: PercentConditionOperator.BETWEEN,
+                      seed: 'abcdef',
+                      microPercentRange: {
+                        microPercentLowerBound: 9,
+                        microPercentUpperBound: 11
+                      }
+                    }
+                  }],
+                }
+              }]
+            }
+          }
+        };
+        const context = { randomizationId: '123' }
+        const evaluator = new ConditionEvaluator();
+        const actual = evaluator.evaluateConditions([condition], context)
+          .get('is_enabled');
+        expect(actual).to.be.true;
+      });
+
+      it('should evaluate between equal bounds to false', () => {
+        const condition = {
+          name: 'is_enabled',
+          condition: {
+            orCondition: {
+              conditions: [{
+                andCondition: {
+                  conditions: [{
+                    percent: {
+                      percentOperator: PercentConditionOperator.BETWEEN,
+                      seed: 'abcdef',
+                      microPercentRange: {
+                        microPercentLowerBound: 50000000,
+                        microPercentUpperBound: 50000000
+                      }
+                    }
+                  }],
+                }
+              }]
+            }
+          }
+        };
+        const context = { randomizationId: '123' }
+        const evaluator = new ConditionEvaluator();
+        expect(evaluator.evaluateConditions([condition], context)).deep.equals(
+          new Map([['is_enabled', false]]));
+      });
+
+      it('should evaluate 12 as not between 9 and 11', () => {
+        const stub = sinon
+          .stub(farmhash, 'fingerprint64')
+          .returns('12');
+        stubs.push(stub);
+
+        const condition = {
+          name: 'is_enabled',
+          condition: {
+            orCondition: {
+              conditions: [{
+                andCondition: {
+                  conditions: [{
+                    percent: {
+                      percentOperator: PercentConditionOperator.BETWEEN,
+                      seed: 'abcdef',
+                      microPercentRange: {
+                        microPercentLowerBound: 9,
+                        microPercentUpperBound: 11
+                      }
+                    }
+                  }],
+                }
+              }]
+            }
+          }
+        };
+        const context = { randomizationId: '123' }
+        const evaluator = new ConditionEvaluator();
+        const actual = evaluator.evaluateConditions([condition], context)
+          .get('is_enabled');
+        expect(actual).to.be.false;
+      });
+
+      // The following tests are probablistic. They use tolerances based on
+      // standard deviations to balance accuracy and flakiness. Random IDs will
+      // hash to the target range + 3 standard deviations 99.7% of the time,
+      // which minimizes flakiness.
+      // Use python to calculate standard deviation. For example, for 100k
+      // trials with 50% probability:
+      //   from scipy.stats import binom
+      //   print(binom.std(100_000, 0.5) * 3)
+      it('should evaluate less or equal to 10% to approx 10%', () => {
+        const percentCondition = {
+          percentOperator: PercentConditionOperator.LESS_OR_EQUAL,
+          microPercent: 10_000_000 // 10%
+        };
+        const evaluator = new ConditionEvaluator();
+        const truthyAssignments = evaluateRandomAssignments(percentCondition, 100_000, evaluator);
+        // 284 is 3 standard deviations for 100k trials with 10% probability.
+        const tolerance = 284;
+        expect(truthyAssignments).to.be.greaterThanOrEqual(10000 - tolerance);
+        expect(truthyAssignments).to.be.lessThanOrEqual(10000 + tolerance);
+      });
+
+      it('should evaluate between 0 to 10% to approx 10%', () => {
+        const percentCondition = {
+          percentOperator: PercentConditionOperator.BETWEEN,
+          microPercentRange: {
+            microPercentLowerBound: 0,
+            microPercentUpperBound: 10_000_000
+          }
+        };
+        const evaluator = new ConditionEvaluator();
+        const truthyAssignments = evaluateRandomAssignments(percentCondition, 100_000, evaluator);
+        // 284 is 3 standard deviations for 100k trials with 10% probability.
+        const tolerance = 284;
+        expect(truthyAssignments).to.be.greaterThanOrEqual(10000 - tolerance);
+        expect(truthyAssignments).to.be.lessThanOrEqual(10000 + tolerance);
+      });
+
+      it('should evaluate greater than 10% to approx 90%', () => {
+        const percentCondition = {
+          percentOperator: PercentConditionOperator.GREATER_THAN,
+          microPercent: 10_000_000
+        };
+        const evaluator = new ConditionEvaluator();
+        const truthyAssignments = evaluateRandomAssignments(percentCondition, 100_000, evaluator);
+        // 284 is 3 standard deviations for 100k trials with 90% probability.
+        const tolerance = 284;
+        expect(truthyAssignments).to.be.greaterThanOrEqual(90000 - tolerance);
+        expect(truthyAssignments).to.be.lessThanOrEqual(90000 + tolerance);
+      });
+
+      it('should evaluate between 40% to 60% to approx 20%', () => {
+        const percentCondition = {
+          percentOperator: PercentConditionOperator.BETWEEN,
+          microPercentRange: {
+            microPercentLowerBound: 40_000_000,
+            microPercentUpperBound: 60_000_000
+          }
+        };
+        const evaluator = new ConditionEvaluator();
+        const truthyAssignments = evaluateRandomAssignments(percentCondition, 100_000, evaluator);
+        // 379 is 3 standard deviations for 100k trials with 20% probability.
+        const tolerance = 379;
+        expect(truthyAssignments).to.be.greaterThanOrEqual(20000 - tolerance);
+        expect(truthyAssignments).to.be.lessThanOrEqual(20000 + tolerance);
+      });
+
+      it('should evaluate between interquartile range to approx 50%', () => {
+        const percentCondition = {
+          percentOperator: PercentConditionOperator.BETWEEN,
+          microPercentRange: {
+            microPercentLowerBound: 25_000_000,
+            microPercentUpperBound: 75_000_000
+          }
+        };
+        const evaluator = new ConditionEvaluator();
+        const truthyAssignments = evaluateRandomAssignments(percentCondition, 100_000, evaluator);
+        // 474 is 3 standard deviations for 100k trials with 50% probability.
+        const tolerance = 474;
+        expect(truthyAssignments).to.be.greaterThanOrEqual(50000 - tolerance);
+        expect(truthyAssignments).to.be.lessThanOrEqual(50000 + tolerance);
+      });
+
+      // Returns the number of assignments which evaluate to true for the specified percent condition.
+      // This method randomly generates the ids for each assignment for this purpose.
+      function evaluateRandomAssignments(
+        condition: PercentCondition,
+        numOfAssignments: number,
+        conditionEvaluator: ConditionEvaluator): number {
+
+        let evalTrueCount = 0;
+        for (let i = 0; i < numOfAssignments; i++) {
+          const clonedCondition = {
+            ...clone(condition),
+            seed: 'seed'
+          };
+          const context = { randomizationId: uuidv4() }
+          if (conditionEvaluator.evaluateConditions([{
+            name: 'is_enabled',
+            condition: { percent: clonedCondition }
+          }], context).get('is_enabled') == true) { evalTrueCount++ }
+        }
+        return evalTrueCount;
+      }
+    });
+  });
+});
diff --git a/test/unit/remote-config/remote-config.spec.ts b/test/unit/remote-config/remote-config.spec.ts
index 39e7cf3b86..81e34fa4b7 100644
--- a/test/unit/remote-config/remote-config.spec.ts
+++ b/test/unit/remote-config/remote-config.spec.ts
@@ -106,14 +106,27 @@ describe('RemoteConfig', () => {
     // to allow easier use from within the tests. An improvement would be to
     // alter this into a helper that creates customized RemoteConfigTemplateContent based
     // on the needs of the test, as that would ensure type-safety.
-    conditions?: Array<{ name: string; }>;
+    conditions?: Array<NamedCondition>;
     parameters?: object | null;
     etag: string;
     version?: object;
   } = {
     conditions: [
       {
-        name: 'ios'
+        name: 'ios',
+        condition: {
+          orCondition: {
+            conditions: [
+              {
+                andCondition: {
+                  conditions: [
+                    { true: {} }
+                  ]
+                }
+              }
+            ]
+          }
+        }
       },
     ],
     parameters: {
@@ -795,6 +808,21 @@ describe('RemoteConfig', () => {
             expect(c).to.be.not.undefined;
             const cond = c as NamedCondition;
             expect(cond.name).to.equal('ios');
+            expect(cond.condition).deep.equals({
+              'orCondition': {
+                'conditions': [
+                  {
+                    'andCondition': {
+                      'conditions': [
+                        {
+                          'true': {}
+                        }
+                      ]
+                    }
+                  }
+                ]
+              }
+            });
 
             const parsed = JSON.parse(JSON.stringify(template.cache));
             const expectedTemplate = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE);
@@ -900,6 +928,106 @@ describe('RemoteConfig', () => {
           });
       });
 
+      it('returns conditional value', () => {
+        const condition = {
+          name: 'is_true',
+          condition: {
+            orCondition: {
+              conditions: [
+                {
+                  andCondition: {
+                    conditions: [
+                      {
+                        name: '',
+                        true: {
+                        }
+                      }
+                    ]
+                  }
+                }
+              ]
+            }
+          }
+        };
+        const template = remoteConfig.initServerTemplate({
+          template: {
+            conditions: [condition],
+            parameters: {
+              is_enabled: {
+                defaultValue: { value: 'false' },
+                conditionalValues: { is_true: { value: 'true' } },
+                valueType: 'BOOLEAN',
+              },
+            },
+            etag: '123'
+          }
+        });
+        const config = template.evaluate();
+        expect(config.is_enabled).to.be.true;
+      });
+
+      it('honors condition order', () => {
+        const template = remoteConfig.initServerTemplate({
+          template: {
+            conditions: [
+              {
+                name: 'is_true',
+                condition: {
+                  orCondition: {
+                    conditions: [
+                      {
+                        andCondition: {
+                          conditions: [
+                            {
+                              true: {
+                              }
+                            }
+                          ]
+                        }
+                      }
+                    ]
+                  }
+                }
+              },
+              {
+                name: 'is_true_too',
+                condition: {
+                  orCondition: {
+                    conditions: [
+                      {
+                        andCondition: {
+                          conditions: [
+                            {
+                              true: {
+                              }
+                            }
+                          ]
+                        }
+                      }
+                    ]
+                  }
+                }
+              }],
+            parameters: {
+              dog_type: {
+                defaultValue: { value: 'chihuahua' },
+                conditionalValues: {
+                  // The is_true and is_true_too conditions both return true,
+                  // but is_true is first in the list, so the corresponding
+                  // value is selected.
+                  is_true_too: { value: 'dachshund' },
+                  is_true: { value: 'corgi' }
+                },
+                valueType: 'STRING',
+              },
+            },
+            etag: '123'
+          }
+        });
+        const config = template.evaluate();
+        expect(config.dog_type).to.eq('corgi');
+      });
+
       it('uses local default if parameter not in template', () => {
         const stub = sinon
           .stub(RemoteConfigApiClient.prototype, 'getServerTemplate')

From ef4b2df88737a762d855c587a1ab339db7f0fdbc Mon Sep 17 00:00:00 2001
From: Erik Eldridge <erikeldridge@google.com>
Date: Tue, 2 Apr 2024 13:31:48 -0700
Subject: [PATCH 07/13] Remove defaultConfig from public SSRC API (#2505)

In practice, we only set default config using the initialization
methods, so make the internal field private to simplify the API.
---
 etc/firebase-admin.remote-config.api.md       |  1 -
 src/remote-config/remote-config-api.ts        |  5 --
 src/remote-config/remote-config.ts            |  2 +-
 test/unit/remote-config/remote-config.spec.ts | 82 ++++++++++++-------
 4 files changed, 54 insertions(+), 36 deletions(-)

diff --git a/etc/firebase-admin.remote-config.api.md b/etc/firebase-admin.remote-config.api.md
index 6712175a60..1727c250a1 100644
--- a/etc/firebase-admin.remote-config.api.md
+++ b/etc/firebase-admin.remote-config.api.md
@@ -166,7 +166,6 @@ export type ServerConfig = {
 // @public
 export interface ServerTemplate {
     cache: ServerTemplateData;
-    defaultConfig: ServerConfig;
     evaluate(context?: EvaluationContext): ServerConfig;
     load(): Promise<void>;
 }
diff --git a/src/remote-config/remote-config-api.ts b/src/remote-config/remote-config-api.ts
index e102094cde..e0f2af7738 100644
--- a/src/remote-config/remote-config-api.ts
+++ b/src/remote-config/remote-config-api.ts
@@ -382,11 +382,6 @@ export interface ServerTemplate {
    */
   cache: ServerTemplateData;
 
-  /**
-   * A {@link ServerConfig} that contains default Config values.
-   */
-  defaultConfig: ServerConfig;
-
   /**
    * Evaluates the current template to produce a {@link ServerConfig}.
    */
diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts
index 1a720f9220..505cb7ac5a 100644
--- a/src/remote-config/remote-config.ts
+++ b/src/remote-config/remote-config.ts
@@ -296,7 +296,7 @@ class ServerTemplateImpl implements ServerTemplate {
   constructor(
     private readonly apiClient: RemoteConfigApiClient,
     private readonly conditionEvaluator: ConditionEvaluator,
-    public readonly defaultConfig: ServerConfig = {}
+    private readonly defaultConfig: ServerConfig = {}
   ) { }
 
   /**
diff --git a/test/unit/remote-config/remote-config.spec.ts b/test/unit/remote-config/remote-config.spec.ts
index 81e34fa4b7..19a1aed3d2 100644
--- a/test/unit/remote-config/remote-config.spec.ts
+++ b/test/unit/remote-config/remote-config.spec.ts
@@ -610,20 +610,28 @@ describe('RemoteConfig', () => {
     });
 
     it('should set defaultConfig when passed', () => {
-      const defaultConfig = {
-        holiday_promo_enabled: false,
-        holiday_promo_discount: 20,
-      };
+      // Defines template with no parameters to demonstrate
+      // default config will be used instead,
+      const template = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData;
+      template.parameters = {};
 
       const stub = sinon
         .stub(RemoteConfigApiClient.prototype, operationName)
-        .resolves(SERVER_REMOTE_CONFIG_RESPONSE as ServerTemplateData);
+        .resolves(template);
       stubs.push(stub);
 
+      const defaultConfig = {
+        holiday_promo_enabled: false,
+        holiday_promo_discount: 20,
+      };
+
       return remoteConfig.getServerTemplate({ defaultConfig })
         .then((template) => {
-          expect(template.defaultConfig.holiday_promo_enabled).to.equal(false);
-          expect(template.defaultConfig.holiday_promo_discount).to.equal(20);
+          const config = template.evaluate();
+          expect(config.holiday_promo_enabled).to.equal(
+            defaultConfig.holiday_promo_enabled);
+          expect(config.holiday_promo_discount).to.equal(
+            defaultConfig.holiday_promo_discount);
         });
     });
   });
@@ -1029,50 +1037,66 @@ describe('RemoteConfig', () => {
       });
 
       it('uses local default if parameter not in template', () => {
+        const template = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData;
+        template.parameters = {};
+  
         const stub = sinon
           .stub(RemoteConfigApiClient.prototype, 'getServerTemplate')
-          .resolves(SERVER_REMOTE_CONFIG_RESPONSE_2 as ServerTemplateData);
+          .resolves(template);
         stubs.push(stub);
-        return remoteConfig.getServerTemplate({
-          defaultConfig: {
-            dog_coat: 'blue merle',
-          }
-        })
+  
+        const defaultConfig = {
+          dog_coat: 'blue merle',
+        };
+
+        return remoteConfig.getServerTemplate({ defaultConfig })
           .then((template: ServerTemplate) => {
-            const config = template.evaluate!();
-            expect(config.dog_coat).to.equal(template.defaultConfig.dog_coat);
+            const config = template.evaluate();
+            expect(config.dog_coat).to.equal(defaultConfig.dog_coat);
           });
       });
 
       it('uses local default when parameter is in template but default value is undefined', () => {
+        const template = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData;
+        template.parameters = {
+          dog_no_remote_default_value: {}
+        };
+  
         const stub = sinon
           .stub(RemoteConfigApiClient.prototype, 'getServerTemplate')
-          .resolves(SERVER_REMOTE_CONFIG_RESPONSE_2 as ServerTemplateData);
+          .resolves(template);
         stubs.push(stub);
-        return remoteConfig.getServerTemplate({
-          defaultConfig: {
-            dog_no_remote_default_value: 'local default'
-          }
-        })
+  
+        const defaultConfig = {
+          dog_no_remote_default_value: 'local default'
+        };
+
+        return remoteConfig.getServerTemplate({ defaultConfig })
           .then((template: ServerTemplate) => {
             const config = template.evaluate!();
-            expect(config.dog_no_remote_default_value).to.equal(template.defaultConfig.dog_no_remote_default_value);
+            expect(config.dog_no_remote_default_value).to.equal(defaultConfig.dog_no_remote_default_value);
           });
       });
 
       it('uses local default when in-app default value specified', () => {
+        const template = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData;
+        template.parameters = {
+          dog_no_remote_default_value: {}
+        };
+  
         const stub = sinon
           .stub(RemoteConfigApiClient.prototype, 'getServerTemplate')
-          .resolves(SERVER_REMOTE_CONFIG_RESPONSE_2 as ServerTemplateData);
+          .resolves(template);
         stubs.push(stub);
-        return remoteConfig.getServerTemplate({
-          defaultConfig: {
-            dog_use_inapp_default: '🐕'
-          }
-        })
+
+        const defaultConfig = {
+          dog_use_inapp_default: '🐕'
+        };
+
+        return remoteConfig.getServerTemplate({ defaultConfig })
           .then((template: ServerTemplate) => {
             const config = template.evaluate!();
-            expect(config.dog_use_inapp_default).to.equal(template.defaultConfig.dog_use_inapp_default);
+            expect(config.dog_use_inapp_default).to.equal(defaultConfig.dog_use_inapp_default);
           });
       });
 

From 69c12622a7ba467128066c86d1f2195ede6c0e50 Mon Sep 17 00:00:00 2001
From: Erik Eldridge <erikeldridge@google.com>
Date: Tue, 2 Apr 2024 13:36:46 -0700
Subject: [PATCH 08/13] Differentiate options for sync and async SSRC
 initialization (#2506)

---
 etc/firebase-admin.remote-config.api.md | 20 ++++++++++++--------
 src/remote-config/index.ts              |  3 ++-
 src/remote-config/remote-config-api.ts  |  9 ++++++++-
 src/remote-config/remote-config.ts      |  7 ++++---
 4 files changed, 26 insertions(+), 13 deletions(-)

diff --git a/etc/firebase-admin.remote-config.api.md b/etc/firebase-admin.remote-config.api.md
index 1727c250a1..746d72d914 100644
--- a/etc/firebase-admin.remote-config.api.md
+++ b/etc/firebase-admin.remote-config.api.md
@@ -28,11 +28,21 @@ export interface ExplicitParameterValue {
 // @public
 export function getRemoteConfig(app?: App): RemoteConfig;
 
+// @public
+export interface GetServerTemplateOptions {
+    defaultConfig?: ServerConfig;
+}
+
 // @public
 export interface InAppDefaultValue {
     useInAppDefault: boolean;
 }
 
+// @public
+export interface InitServerTemplateOptions extends GetServerTemplateOptions {
+    template?: ServerTemplateData;
+}
+
 // @public
 export interface ListVersionsOptions {
     endTime?: Date | string;
@@ -98,10 +108,10 @@ export class RemoteConfig {
     // (undocumented)
     readonly app: App;
     createTemplateFromJSON(json: string): RemoteConfigTemplate;
-    getServerTemplate(options?: ServerTemplateOptions): Promise<ServerTemplate>;
+    getServerTemplate(options?: GetServerTemplateOptions): Promise<ServerTemplate>;
     getTemplate(): Promise<RemoteConfigTemplate>;
     getTemplateAtVersion(versionNumber: number | string): Promise<RemoteConfigTemplate>;
-    initServerTemplate(options?: ServerTemplateOptions): ServerTemplate;
+    initServerTemplate(options?: InitServerTemplateOptions): ServerTemplate;
     listVersions(options?: ListVersionsOptions): Promise<ListVersionsResult>;
     publishTemplate(template: RemoteConfigTemplate, options?: {
         force: boolean;
@@ -180,12 +190,6 @@ export interface ServerTemplateData {
     version?: Version;
 }
 
-// @public
-export interface ServerTemplateOptions {
-    defaultConfig?: ServerConfig;
-    template?: ServerTemplateData;
-}
-
 // @public
 export type TagColor = 'BLUE' | 'BROWN' | 'CYAN' | 'DEEP_ORANGE' | 'GREEN' | 'INDIGO' | 'LIME' | 'ORANGE' | 'PINK' | 'PURPLE' | 'TEAL';
 
diff --git a/src/remote-config/index.ts b/src/remote-config/index.ts
index 103ec462f3..cb2e6285b8 100644
--- a/src/remote-config/index.ts
+++ b/src/remote-config/index.ts
@@ -28,7 +28,9 @@ export {
   AndCondition,
   EvaluationContext,
   ExplicitParameterValue,
+  GetServerTemplateOptions,
   InAppDefaultValue,
+  InitServerTemplateOptions,
   ListVersionsOptions,
   ListVersionsResult,
   MicroPercentRange,
@@ -47,7 +49,6 @@ export {
   ServerConfig,
   ServerTemplate,
   ServerTemplateData,
-  ServerTemplateOptions,
   TagColor,
   Version,
 } from './remote-config-api';
diff --git a/src/remote-config/remote-config-api.ts b/src/remote-config/remote-config-api.ts
index e0f2af7738..66d310daf8 100644
--- a/src/remote-config/remote-config-api.ts
+++ b/src/remote-config/remote-config-api.ts
@@ -354,7 +354,7 @@ export interface ServerTemplateData {
 /**
  * Represents optional arguments that can be used when instantiating {@link ServerTemplate}.
  */
-export interface ServerTemplateOptions {
+export interface GetServerTemplateOptions {
 
   /**
    * Defines in-app default parameter values, so that your app behaves as
@@ -362,6 +362,13 @@ export interface ServerTemplateOptions {
    * default values are available if none are set on the backend.
    */
   defaultConfig?: ServerConfig,
+}
+
+/**
+ * Represents optional arguments that can be used when instantiating
+ * {@link ServerTemplate} synchonously.
+ */
+export interface InitServerTemplateOptions extends GetServerTemplateOptions {
 
   /**
    * Enables integrations to use template data loaded independently. For
diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts
index 505cb7ac5a..887a8a1905 100644
--- a/src/remote-config/remote-config.ts
+++ b/src/remote-config/remote-config.ts
@@ -35,8 +35,9 @@ import {
   RemoteConfigParameterValue,
   EvaluationContext,
   ServerTemplateData,
-  ServerTemplateOptions,
   NamedCondition,
+  GetServerTemplateOptions,
+  InitServerTemplateOptions,
 } from './remote-config-api';
 
 /**
@@ -184,7 +185,7 @@ export class RemoteConfig {
    * Instantiates {@link ServerTemplate} and then fetches and caches the latest
    * template version of the project.
    */
-  public async getServerTemplate(options?: ServerTemplateOptions): Promise<ServerTemplate> {
+  public async getServerTemplate(options?: GetServerTemplateOptions): Promise<ServerTemplate> {
     const template = this.initServerTemplate(options);
     await template.load();
     return template;
@@ -193,7 +194,7 @@ export class RemoteConfig {
   /**
    * Synchronously instantiates {@link ServerTemplate}.
    */
-  public initServerTemplate(options?: ServerTemplateOptions): ServerTemplate {
+  public initServerTemplate(options?: InitServerTemplateOptions): ServerTemplate {
     const template = new ServerTemplateImpl(
       this.client, new ConditionEvaluator(), options?.defaultConfig);
     if (options?.template) {

From 36db2804754614e1a10c0baa109117fcb63b5944 Mon Sep 17 00:00:00 2001
From: amanda-xia <80780804+amanda-xia@users.noreply.github.com>
Date: Thu, 4 Apr 2024 09:45:16 -0700
Subject: [PATCH 09/13] Switch to longs for hash values for percent condition
 evaluation (#2507)

* Get And condition passing

* Support and.or condition

* Support true condition

* Support false condition, and fix tests

* Use or.and, not the other way around

* Integrate conditional values into evaluate method

* Test handling for multiple conditions

* Clean up logs

* Extract condition evaluation to class for testing

* Namespace condition names

* Iterate over ordered condition list

* Test condition ordering

* Differentiate named conditions

* Document condition types

* Generalize condition eval test and fix styling

* Replace log statement with todo

* Implement evaluate percent condition for RC server-side

* Apply lint fixes

* Add context param to evaluate method

* Add tests for percent condition eval

* Update evaluator tests to use context

* Increase threshold to +/- 500 for percent condition eval tests
to prevent some flaky tests.

* Clean up percentCondition tests a bit and add note on the tolerance used

* Apply suggestions from code review

Co-authored-by: jen_h <harveyjen@google.com>

* Update copyright date and remove stray log statement

* Mock farmhash in tests

* Add Math.abs for farmhash - to be consistent with the internal implementation

* Regenerate package-lock to fix Node 14 CI error re busboy

* Fix lint errors

* Rename "id" to "randomizationId" per discussion

* Extract API

* Only return false in cases of uknown template evaluation

* Remove product prefix from type names

* Remove product prefix from exported types

* Remove unused "expression" field from server condition

* Extract API

* Remove prefix from impl classes, for consistency

* Remove prefix from new internal classes

* Remove "server" prefix

* Remove prefix from NamedCondition

* Rename "or" and "and" fields to match API

* Rename "operator" field to "percentOperator" to match API

* Extract API after "and" and "or" rename

* use longjs library for hash

* re-run npm install

* re-attempt

* use node 14, re-attempt

* remove file

* Add comment, switch from lte to lt

---------

Co-authored-by: Erik Eldridge <erikeldridge@google.com>
Co-authored-by: Xin Wei <xinwei@google.com>
Co-authored-by: jen_h <harveyjen@google.com>
---
 package-lock.json                             | 406 +++++++++---------
 package.json                                  |   1 +
 .../condition-evaluator-internal.ts           |  22 +-
 .../remote-config/condition-evaluator.spec.ts |  31 ++
 4 files changed, 251 insertions(+), 209 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 91e59b8fd5..bc2633c726 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -21,94 +21,36 @@
       }
     },
     "@babel/code-frame": {
-      "version": "7.23.5",
-      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz",
-      "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==",
+      "version": "7.24.2",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz",
+      "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==",
       "dev": true,
       "requires": {
-        "@babel/highlight": "^7.23.4",
-        "chalk": "^2.4.2"
-      },
-      "dependencies": {
-        "ansi-styles": {
-          "version": "3.2.1",
-          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
-          "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
-          "dev": true,
-          "requires": {
-            "color-convert": "^1.9.0"
-          }
-        },
-        "chalk": {
-          "version": "2.4.2",
-          "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
-          "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
-          "dev": true,
-          "requires": {
-            "ansi-styles": "^3.2.1",
-            "escape-string-regexp": "^1.0.5",
-            "supports-color": "^5.3.0"
-          }
-        },
-        "color-convert": {
-          "version": "1.9.3",
-          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
-          "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
-          "dev": true,
-          "requires": {
-            "color-name": "1.1.3"
-          }
-        },
-        "color-name": {
-          "version": "1.1.3",
-          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-          "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
-          "dev": true
-        },
-        "escape-string-regexp": {
-          "version": "1.0.5",
-          "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-          "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
-          "dev": true
-        },
-        "has-flag": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
-          "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
-          "dev": true
-        },
-        "supports-color": {
-          "version": "5.5.0",
-          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
-          "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
-          "dev": true,
-          "requires": {
-            "has-flag": "^3.0.0"
-          }
-        }
+        "@babel/highlight": "^7.24.2",
+        "picocolors": "^1.0.0"
       }
     },
     "@babel/compat-data": {
-      "version": "7.23.5",
-      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz",
-      "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==",
+      "version": "7.24.1",
+      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.1.tgz",
+      "integrity": "sha512-Pc65opHDliVpRHuKfzI+gSA4zcgr65O4cl64fFJIWEEh8JoHIHh0Oez1Eo8Arz8zq/JhgKodQaxEwUPRtZylVA==",
       "dev": true
     },
     "@babel/core": {
-      "version": "7.24.0",
-      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz",
-      "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==",
+      "version": "7.24.3",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.3.tgz",
+      "integrity": "sha512-5FcvN1JHw2sHJChotgx8Ek0lyuh4kCKelgMTTqhYJJtloNvUfpAFMeNQUtdlIaktwrSV9LtCdqwk48wL2wBacQ==",
       "dev": true,
       "requires": {
         "@ampproject/remapping": "^2.2.0",
-        "@babel/code-frame": "^7.23.5",
-        "@babel/generator": "^7.23.6",
+        "@babel/code-frame": "^7.24.2",
+        "@babel/generator": "^7.24.1",
         "@babel/helper-compilation-targets": "^7.23.6",
         "@babel/helper-module-transforms": "^7.23.3",
-        "@babel/helpers": "^7.24.0",
-        "@babel/parser": "^7.24.0",
+        "@babel/helpers": "^7.24.1",
+        "@babel/parser": "^7.24.1",
         "@babel/template": "^7.24.0",
-        "@babel/traverse": "^7.24.0",
+        "@babel/traverse": "^7.24.1",
         "@babel/types": "^7.24.0",
         "convert-source-map": "^2.0.0",
         "debug": "^4.1.0",
@@ -132,14 +74,14 @@
       }
     },
     "@babel/generator": {
-      "version": "7.23.6",
-      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz",
-      "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==",
+      "version": "7.24.1",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.1.tgz",
+      "integrity": "sha512-DfCRfZsBcrPEHUfuBMgbJ1Ut01Y/itOs+hY2nFLgqsqXd52/iSiVq5TITtUasIUgm+IIKdY2/1I7auiQOEeC9A==",
       "dev": true,
       "requires": {
-        "@babel/types": "^7.23.6",
-        "@jridgewell/gen-mapping": "^0.3.2",
-        "@jridgewell/trace-mapping": "^0.3.17",
+        "@babel/types": "^7.24.0",
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.25",
         "jsesc": "^2.5.1"
       }
     },
@@ -205,12 +147,12 @@
       }
     },
     "@babel/helper-module-imports": {
-      "version": "7.22.15",
-      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz",
-      "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==",
+      "version": "7.24.3",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz",
+      "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==",
       "dev": true,
       "requires": {
-        "@babel/types": "^7.22.15"
+        "@babel/types": "^7.24.0"
       }
     },
     "@babel/helper-module-transforms": {
@@ -245,9 +187,9 @@
       }
     },
     "@babel/helper-string-parser": {
-      "version": "7.23.4",
-      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz",
-      "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==",
+      "version": "7.24.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz",
+      "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==",
       "dev": true
     },
     "@babel/helper-validator-identifier": {
@@ -263,25 +205,26 @@
       "dev": true
     },
     "@babel/helpers": {
-      "version": "7.24.0",
-      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.0.tgz",
-      "integrity": "sha512-ulDZdc0Aj5uLc5nETsa7EPx2L7rM0YJM8r7ck7U73AXi7qOV44IHHRAYZHY6iU1rr3C5N4NtTmMRUJP6kwCWeA==",
+      "version": "7.24.1",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.1.tgz",
+      "integrity": "sha512-BpU09QqEe6ZCHuIHFphEFgvNSrubve1FtyMton26ekZ85gRGi6LrTF7zArARp2YvyFxloeiRmtSCq5sjh1WqIg==",
       "dev": true,
       "requires": {
         "@babel/template": "^7.24.0",
-        "@babel/traverse": "^7.24.0",
+        "@babel/traverse": "^7.24.1",
         "@babel/types": "^7.24.0"
       }
     },
     "@babel/highlight": {
-      "version": "7.23.4",
-      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz",
-      "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==",
+      "version": "7.24.2",
+      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz",
+      "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==",
       "dev": true,
       "requires": {
         "@babel/helper-validator-identifier": "^7.22.20",
         "chalk": "^2.4.2",
-        "js-tokens": "^4.0.0"
+        "js-tokens": "^4.0.0",
+        "picocolors": "^1.0.0"
       },
       "dependencies": {
         "ansi-styles": {
@@ -343,9 +286,9 @@
       }
     },
     "@babel/parser": {
-      "version": "7.24.0",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz",
-      "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==",
+      "version": "7.24.1",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.1.tgz",
+      "integrity": "sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==",
       "dev": true
     },
     "@babel/template": {
@@ -360,18 +303,18 @@
       }
     },
     "@babel/traverse": {
-      "version": "7.24.0",
-      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.0.tgz",
-      "integrity": "sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw==",
+      "version": "7.24.1",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.1.tgz",
+      "integrity": "sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==",
       "dev": true,
       "requires": {
-        "@babel/code-frame": "^7.23.5",
-        "@babel/generator": "^7.23.6",
+        "@babel/code-frame": "^7.24.1",
+        "@babel/generator": "^7.24.1",
         "@babel/helper-environment-visitor": "^7.22.20",
         "@babel/helper-function-name": "^7.23.0",
         "@babel/helper-hoist-variables": "^7.22.5",
         "@babel/helper-split-export-declaration": "^7.22.6",
-        "@babel/parser": "^7.24.0",
+        "@babel/parser": "^7.24.1",
         "@babel/types": "^7.24.0",
         "debug": "^4.3.1",
         "globals": "^11.1.0"
@@ -494,9 +437,9 @@
       }
     },
     "@firebase/app": {
-      "version": "0.9.28",
-      "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.9.28.tgz",
-      "integrity": "sha512-MS0+EtNixrwJbVDs5Bt/lhUhzeWGUtUoP6X+zYZck5GAZwI5g4F91noVA9oIXlFlpn6Q1xIbiaHA2GwGk7/7Ag==",
+      "version": "0.9.29",
+      "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.9.29.tgz",
+      "integrity": "sha512-HbKTjfmILklasIu/ij6zKnFf3SgLYXkBDVN7leJfVGmohl+zA7Ig+eXM1ZkT1pyBJ8FTYR+mlOJer/lNEnUCtw==",
       "dev": true,
       "requires": {
         "@firebase/component": "0.6.5",
@@ -512,12 +455,12 @@
       "integrity": "sha512-xAxHPZPIgFXnI+vb4sbBjZcde7ZluzPPaSK7Lx3/nmuVk4TjZvnL8ONnkd4ERQKL8WePQySU+pRcWkh8rDf5Sg=="
     },
     "@firebase/app-compat": {
-      "version": "0.2.28",
-      "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.28.tgz",
-      "integrity": "sha512-Mr2NbeM1Oaayuw5unUAMzt+7/MN+e2uklT1l87D+ZLJl2UvhZAZmMt74GjEI9N3sDYKMeszSbszBqtJ1fGVafQ==",
+      "version": "0.2.29",
+      "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.29.tgz",
+      "integrity": "sha512-NqUdegXJfwphx9i/2bOE2CTZ55TC9bbDg+iwkxVShsPBJhD3CzQJkFhoDz4ccfbJaKZGsqjY3fisgX5kbDROnA==",
       "dev": true,
       "requires": {
-        "@firebase/app": "0.9.28",
+        "@firebase/app": "0.9.29",
         "@firebase/component": "0.6.5",
         "@firebase/logger": "0.4.0",
         "@firebase/util": "1.9.4",
@@ -689,9 +632,9 @@
       }
     },
     "@google-cloud/firestore": {
-      "version": "7.3.1",
-      "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.3.1.tgz",
-      "integrity": "sha512-YluLZbJK3dHXq6Ns5URCtr6hjBiG+6EM17QSivjaozPYDsv1R9a9mkWPz+jCQrb6Ewz6mxp3zavu6DXxvmSWLA==",
+      "version": "7.5.0",
+      "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.5.0.tgz",
+      "integrity": "sha512-bhFKaCybfK/jzqhVm1Y1o8p3wOHVEo8opj7IJGF2sdqS69xl6QD1zpnrgssi/4HUj9bxIqtcs33Ofz//deV+rg==",
       "optional": true,
       "requires": {
         "fast-deep-equal": "^3.1.1",
@@ -723,9 +666,9 @@
       "optional": true
     },
     "@google-cloud/storage": {
-      "version": "7.8.0",
-      "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.8.0.tgz",
-      "integrity": "sha512-4q8rKdLp35z8msAtrhr0pbos7BeD8T0tr6rMbBINewp9cfrwj7ROIElVwBluU8fZ596OvwQcjb6QCyBzTmkMRQ==",
+      "version": "7.9.0",
+      "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.9.0.tgz",
+      "integrity": "sha512-PlFl7g3r91NmXtZHXsSEfTZES5ysD3SSBWmX4iBdQ2TFH7tN/Vn/IhnVELCHtgh1vc+uYPZ7XvRYaqtDCdghIA==",
       "optional": true,
       "requires": {
         "@google-cloud/paginator": "^5.0.0",
@@ -756,9 +699,9 @@
       }
     },
     "@grpc/grpc-js": {
-      "version": "1.10.2",
-      "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.10.2.tgz",
-      "integrity": "sha512-lSbgu8iayAod8O0YcoXK3+bMFGThY2svtN35Zlm9VepsB3jfyIcoupKknEht7Kh9Q8ITjsp0J4KpYo9l4+FhNg==",
+      "version": "1.10.4",
+      "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.10.4.tgz",
+      "integrity": "sha512-MqBisuxTkYvPFnEiu+dag3xG/NBUDzSbAFAWlzfkGnQkjVZ6by3h4atbBc+Ikqup1z5BfB4BN18gKWR1YyppNw==",
       "optional": true,
       "requires": {
         "@grpc/proto-loader": "^0.7.10",
@@ -766,9 +709,9 @@
       }
     },
     "@grpc/proto-loader": {
-      "version": "0.7.10",
-      "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.10.tgz",
-      "integrity": "sha512-CAqDfoaQ8ykFd9zqBDn4k6iWT9loLAlc2ETmDFS9JCD70gDcnA4L3AFEo2iV7KyAtAAHFW9ftq1Fz+Vsgq80RQ==",
+      "version": "0.7.12",
+      "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.12.tgz",
+      "integrity": "sha512-DCVwMxqYzpUCiDMl7hQ384FqP4T3DbNpXU8pt681l3UWCip1WUiD5JrkImUwCB9a7f2cq4CUTmi5r/xIMRPY1Q==",
       "optional": true,
       "requires": {
         "lodash.camelcase": "^4.3.0",
@@ -964,9 +907,9 @@
       }
     },
     "@microsoft/api-extractor": {
-      "version": "7.42.3",
-      "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.42.3.tgz",
-      "integrity": "sha512-JNLJFpGHz6ekjS6bvYXxUBeRGnSHeCMFNvRbCQ+7XXB/ZFrgLSMPwWtEq40AiWAy+oyG5a4RSNwdJTp0B2USvQ==",
+      "version": "7.43.0",
+      "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.43.0.tgz",
+      "integrity": "sha512-GFhTcJpB+MI6FhvXEI9b2K0snulNLWHqC/BbcJtyNYcKUiw7l3Lgis5ApsYncJ0leALX7/of4XfmXk+maT111w==",
       "dev": true,
       "requires": {
         "@microsoft/api-extractor-model": "7.28.13",
@@ -981,7 +924,7 @@
         "resolve": "~1.22.1",
         "semver": "~7.5.4",
         "source-map": "~0.6.1",
-        "typescript": "5.3.3"
+        "typescript": "5.4.2"
       },
       "dependencies": {
         "@microsoft/tsdoc": {
@@ -1026,9 +969,9 @@
           }
         },
         "typescript": {
-          "version": "5.3.3",
-          "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
-          "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
+          "version": "5.4.2",
+          "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz",
+          "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==",
           "dev": true
         }
       }
@@ -1359,9 +1302,9 @@
       "optional": true
     },
     "@tsconfig/node10": {
-      "version": "1.0.9",
-      "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
-      "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==",
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
+      "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
       "dev": true
     },
     "@tsconfig/node12": {
@@ -1418,9 +1361,9 @@
       "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg=="
     },
     "@types/chai": {
-      "version": "4.3.12",
-      "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.12.tgz",
-      "integrity": "sha512-zNKDHG/1yxm8Il6uCCVsm+dRdEsJlFoDu73X17y09bId6UwoYww+vFBsAcRzl8knM1sab3Dp1VRikFQwDOtDDw==",
+      "version": "4.3.14",
+      "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.14.tgz",
+      "integrity": "sha512-Wj71sXE4Q4AkGdG9Tvq1u/fquNz9EdG4LIJMwVVII7ashjD/8cf8fyIfJAjRr6YcsXnSE8cOGQPq1gqeR8z+3w==",
       "dev": true
     },
     "@types/chai-as-promised": {
@@ -1533,17 +1476,17 @@
       }
     },
     "@types/node": {
-      "version": "20.11.27",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.27.tgz",
-      "integrity": "sha512-qyUZfMnCg1KEz57r7pzFtSGt49f6RPkPBis3Vo4PbS7roQEDn22hiHzl/Lo1q4i4hDEgBJmBF/NTNg2XR0HbFg==",
+      "version": "20.11.30",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz",
+      "integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==",
       "requires": {
         "undici-types": "~5.26.4"
       }
     },
     "@types/qs": {
-      "version": "6.9.12",
-      "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.12.tgz",
-      "integrity": "sha512-bZcOkJ6uWrL0Qb2NAWKa7TBU+mJHPzhx9jjLL1KHF+XpzEcR7EXHvjbHlGtR/IsP1vyPrehuS6XqkmaePy//mg=="
+      "version": "6.9.14",
+      "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.14.tgz",
+      "integrity": "sha512-5khscbd3SwWMhFqylJBLQ0zIu7c1K6Vz0uBIt915BI3zV0q1nfjRQD3RqSBcPaO6PHEF4ov/t9y89fSiyThlPA=="
     },
     "@types/range-parser": {
       "version": "1.2.7",
@@ -2610,9 +2553,9 @@
       "dev": true
     },
     "caniuse-lite": {
-      "version": "1.0.30001597",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz",
-      "integrity": "sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==",
+      "version": "1.0.30001600",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001600.tgz",
+      "integrity": "sha512-+2S9/2JFhYmYaDpZvo0lKkfvuKIglrx68MwOBqMGHhQsNkLjB5xtc/TGoEPs+MxjSyN/72qer2g97nzR641mOQ==",
       "dev": true
     },
     "caseless": {
@@ -3152,6 +3095,39 @@
         "assert-plus": "^1.0.0"
       }
     },
+    "data-view-buffer": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz",
+      "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==",
+      "dev": true,
+      "requires": {
+        "call-bind": "^1.0.6",
+        "es-errors": "^1.3.0",
+        "is-data-view": "^1.0.1"
+      }
+    },
+    "data-view-byte-length": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz",
+      "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==",
+      "dev": true,
+      "requires": {
+        "call-bind": "^1.0.7",
+        "es-errors": "^1.3.0",
+        "is-data-view": "^1.0.1"
+      }
+    },
+    "data-view-byte-offset": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz",
+      "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==",
+      "dev": true,
+      "requires": {
+        "call-bind": "^1.0.6",
+        "es-errors": "^1.3.0",
+        "is-data-view": "^1.0.1"
+      }
+    },
     "debug": {
       "version": "4.3.4",
       "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@@ -3318,9 +3294,9 @@
       "dev": true
     },
     "detect-libc": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
-      "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
+      "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="
     },
     "diff": {
       "version": "5.0.0",
@@ -3387,9 +3363,9 @@
       }
     },
     "electron-to-chromium": {
-      "version": "1.4.703",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.703.tgz",
-      "integrity": "sha512-094ZZC4nHXPKl/OwPinSMtLN9+hoFkdfQGKnvXbY+3WEAYtVDpz9UhJIViiY6Zb8agvqxiaJzNG9M+pRZWvSZw==",
+      "version": "1.4.719",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.719.tgz",
+      "integrity": "sha512-FbWy2Q2YgdFzkFUW/W5jBjE9dj+804+98E4Pup78JBPnbdb3pv6IneY2JCPKdeKLh3AOKHQeYf+KwLr7mxGh6Q==",
       "dev": true
     },
     "emoji-regex": {
@@ -3421,17 +3397,21 @@
       }
     },
     "es-abstract": {
-      "version": "1.22.5",
-      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.5.tgz",
-      "integrity": "sha512-oW69R+4q2wG+Hc3KZePPZxOiisRIqfKBVo/HLx94QcJeWGU/8sZhCvc829rd1kS366vlJbzBfXf9yWwf0+Ko7w==",
+      "version": "1.23.2",
+      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.2.tgz",
+      "integrity": "sha512-60s3Xv2T2p1ICykc7c+DNDPLDMm9t4QxCOUU0K9JxiLjM3C1zB9YVdN7tjxrFd4+AkZ8CdX1ovUga4P2+1e+/w==",
       "dev": true,
       "requires": {
         "array-buffer-byte-length": "^1.0.1",
         "arraybuffer.prototype.slice": "^1.0.3",
         "available-typed-arrays": "^1.0.7",
         "call-bind": "^1.0.7",
+        "data-view-buffer": "^1.0.1",
+        "data-view-byte-length": "^1.0.1",
+        "data-view-byte-offset": "^1.0.0",
         "es-define-property": "^1.0.0",
         "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.0.0",
         "es-set-tostringtag": "^2.0.3",
         "es-to-primitive": "^1.2.1",
         "function.prototype.name": "^1.1.6",
@@ -3442,10 +3422,11 @@
         "has-property-descriptors": "^1.0.2",
         "has-proto": "^1.0.3",
         "has-symbols": "^1.0.3",
-        "hasown": "^2.0.1",
+        "hasown": "^2.0.2",
         "internal-slot": "^1.0.7",
         "is-array-buffer": "^3.0.4",
         "is-callable": "^1.2.7",
+        "is-data-view": "^1.0.1",
         "is-negative-zero": "^2.0.3",
         "is-regex": "^1.1.4",
         "is-shared-array-buffer": "^1.0.3",
@@ -3456,17 +3437,17 @@
         "object-keys": "^1.1.1",
         "object.assign": "^4.1.5",
         "regexp.prototype.flags": "^1.5.2",
-        "safe-array-concat": "^1.1.0",
+        "safe-array-concat": "^1.1.2",
         "safe-regex-test": "^1.0.3",
-        "string.prototype.trim": "^1.2.8",
-        "string.prototype.trimend": "^1.0.7",
+        "string.prototype.trim": "^1.2.9",
+        "string.prototype.trimend": "^1.0.8",
         "string.prototype.trimstart": "^1.0.7",
         "typed-array-buffer": "^1.0.2",
         "typed-array-byte-length": "^1.0.1",
         "typed-array-byte-offset": "^1.0.2",
         "typed-array-length": "^1.0.5",
         "unbox-primitive": "^1.0.2",
-        "which-typed-array": "^1.1.14"
+        "which-typed-array": "^1.1.15"
       }
     },
     "es-define-property": {
@@ -3484,6 +3465,15 @@
       "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
       "dev": true
     },
+    "es-object-atoms": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
+      "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==",
+      "dev": true,
+      "requires": {
+        "es-errors": "^1.3.0"
+      }
+    },
     "es-set-tostringtag": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz",
@@ -3980,9 +3970,9 @@
       "dev": true
     },
     "fast-xml-parser": {
-      "version": "4.3.5",
-      "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.3.5.tgz",
-      "integrity": "sha512-sWvP1Pl8H03B8oFJpFR3HE31HUfwtX7Rlf9BNsvdpujD4n7WMhfmu8h9wOV2u+c1k0ZilTADhPqypzx2J690ZQ==",
+      "version": "4.3.6",
+      "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.3.6.tgz",
+      "integrity": "sha512-M2SovcRxD4+vC493Uc2GZVcZaj66CCJhWurC4viynVSTvrpErCShNcDz1lAho6n9REQKvL/ll4A4/fw6Y9z8nw==",
       "optional": true,
       "requires": {
         "strnum": "^1.0.5"
@@ -4754,9 +4744,9 @@
       }
     },
     "google-gax": {
-      "version": "4.3.1",
-      "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.3.1.tgz",
-      "integrity": "sha512-qpSfslpwqToIgQ+Tf3MjWIDjYK4UFIZ0uz6nLtttlW9N1NQA4PhGf9tlGo6KDYJ4rgL2w4CjXVd0z5yeNpN/Iw==",
+      "version": "4.3.2",
+      "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.3.2.tgz",
+      "integrity": "sha512-2mw7qgei2LPdtGrmd1zvxQviOcduTnsvAWYzCxhOWXK4IQKmQztHnDQwD0ApB690fBQJemFKSU7DnceAy3RLzw==",
       "optional": true,
       "requires": {
         "@grpc/grpc-js": "~1.10.0",
@@ -5456,6 +5446,15 @@
         "hasown": "^2.0.0"
       }
     },
+    "is-data-view": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz",
+      "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==",
+      "dev": true,
+      "requires": {
+        "is-typed-array": "^1.1.13"
+      }
+    },
     "is-date-object": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
@@ -6288,8 +6287,7 @@
     "long": {
       "version": "5.2.3",
       "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
-      "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==",
-      "optional": true
+      "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="
     },
     "loupe": {
       "version": "2.3.7",
@@ -6644,9 +6642,9 @@
       "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
     },
     "mocha": {
-      "version": "10.3.0",
-      "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.3.0.tgz",
-      "integrity": "sha512-uF2XJs+7xSLsrmIvn37i/wnc91nw7XjOQB8ccyx5aEgdnohr7n+rEiZP23WkCYHjilR6+EboEnbq/ZQDz4LSbg==",
+      "version": "10.4.0",
+      "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.4.0.tgz",
+      "integrity": "sha512-eqhGB8JKapEYcC4ytX/xrzKforgEc3j1pGlAXVy3eRwrtAy5/nIfT1SvgGzfN0XZZxeLq0aQWkOUAmqIJiv+bA==",
       "dev": true,
       "requires": {
         "ansi-colors": "4.1.1",
@@ -6688,9 +6686,9 @@
           }
         },
         "binary-extensions": {
-          "version": "2.2.0",
-          "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
-          "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+          "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
           "dev": true
         },
         "brace-expansion": {
@@ -9170,47 +9168,49 @@
       }
     },
     "string.prototype.padend": {
-      "version": "3.1.5",
-      "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.5.tgz",
-      "integrity": "sha512-DOB27b/2UTTD+4myKUFh+/fXWcu/UDyASIXfg+7VzoCNNGOfWvoyU/x5pvVHr++ztyt/oSYI1BcWBBG/hmlNjA==",
+      "version": "3.1.6",
+      "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.6.tgz",
+      "integrity": "sha512-XZpspuSB7vJWhvJc9DLSlrXl1mcA2BdoY5jjnS135ydXqLoqhs96JjDtCkjJEQHvfqZIp9hBuBMgI589peyx9Q==",
       "dev": true,
       "requires": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.2.0",
-        "es-abstract": "^1.22.1"
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.2",
+        "es-object-atoms": "^1.0.0"
       }
     },
     "string.prototype.trim": {
-      "version": "1.2.8",
-      "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz",
-      "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==",
+      "version": "1.2.9",
+      "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz",
+      "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==",
       "dev": true,
       "requires": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.2.0",
-        "es-abstract": "^1.22.1"
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.0",
+        "es-object-atoms": "^1.0.0"
       }
     },
     "string.prototype.trimend": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz",
-      "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==",
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz",
+      "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==",
       "dev": true,
       "requires": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.2.0",
-        "es-abstract": "^1.22.1"
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-object-atoms": "^1.0.0"
       }
     },
     "string.prototype.trimstart": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz",
-      "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==",
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz",
+      "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==",
       "dev": true,
       "requires": {
-        "call-bind": "^1.0.2",
-        "define-properties": "^1.2.0",
-        "es-abstract": "^1.22.1"
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-object-atoms": "^1.0.0"
       }
     },
     "string_decoder": {
@@ -9281,9 +9281,9 @@
       }
     },
     "tar": {
-      "version": "6.2.0",
-      "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz",
-      "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==",
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
+      "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
       "dev": true,
       "requires": {
         "chownr": "^2.0.0",
@@ -9670,9 +9670,9 @@
       }
     },
     "typed-array-length": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.5.tgz",
-      "integrity": "sha512-yMi0PlwuznKHxKmcpoOdeLwxBoVPkqZxd7q2FgMkmD3bNwvF5VW0+UlUQ1k1vmktTu4Yu13Q0RIxEP8+B+wloA==",
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz",
+      "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==",
       "dev": true,
       "requires": {
         "call-bind": "^1.0.7",
diff --git a/package.json b/package.json
index c09bbbcd1f..95e6a03557 100644
--- a/package.json
+++ b/package.json
@@ -203,6 +203,7 @@
     "farmhash": "^3.3.0",
     "jsonwebtoken": "^9.0.0",
     "jwks-rsa": "^3.0.1",
+    "long": "^5.2.3",
     "node-forge": "^1.3.1",
     "uuid": "^9.0.0"
   },
diff --git a/src/remote-config/condition-evaluator-internal.ts b/src/remote-config/condition-evaluator-internal.ts
index d36b787127..b23958cd77 100644
--- a/src/remote-config/condition-evaluator-internal.ts
+++ b/src/remote-config/condition-evaluator-internal.ts
@@ -26,6 +26,7 @@ import {
   PercentConditionOperator
 } from './remote-config-api';
 import * as farmhash from 'farmhash';
+import long = require('long');
 
 /**
  * Encapsulates condition evaluation logic to simplify organization and
@@ -145,18 +146,27 @@ export class ConditionEvaluator {
 
     const seedPrefix = seed && seed.length > 0 ? `${seed}.` : '';
     const stringToHash = `${seedPrefix}${context.randomizationId}`;
-    const hash64 = Math.abs(parseFloat(farmhash.fingerprint64(stringToHash)));
 
-    const instanceMicroPercentile = hash64 % (100 * 1_000_000);
 
+    // Using a 64-bit long for consistency with the Remote Config fetch endpoint.
+    let hash64 = long.fromString(farmhash.fingerprint64(stringToHash));
+
+    // Negate the hash if its value is less than 0. We handle this manually because the
+    // Long library doesn't provided an absolute value method.
+    if (hash64.lt(0)) {
+      hash64 = hash64.negate();
+    }
+
+    const instanceMicroPercentile = hash64.mod(100 * 1_000_000);
+    
     switch (percentOperator) {
     case PercentConditionOperator.LESS_OR_EQUAL:
-      return instanceMicroPercentile <= normalizedMicroPercent;
+      return instanceMicroPercentile.lte(normalizedMicroPercent);
     case PercentConditionOperator.GREATER_THAN:
-      return instanceMicroPercentile > normalizedMicroPercent;
+      return instanceMicroPercentile.gt(normalizedMicroPercent);
     case PercentConditionOperator.BETWEEN:
-      return instanceMicroPercentile > normalizedMicroPercentLowerBound
-        && instanceMicroPercentile <= normalizedMicroPercentUpperBound;
+      return instanceMicroPercentile.gt(normalizedMicroPercentLowerBound)
+        && instanceMicroPercentile.lte(normalizedMicroPercentUpperBound);
     case PercentConditionOperator.UNKNOWN:
     default:
       break;
diff --git a/test/unit/remote-config/condition-evaluator.spec.ts b/test/unit/remote-config/condition-evaluator.spec.ts
index 3bd6d0f1fc..fbf3ca4979 100644
--- a/test/unit/remote-config/condition-evaluator.spec.ts
+++ b/test/unit/remote-config/condition-evaluator.spec.ts
@@ -450,6 +450,37 @@ describe('ConditionEvaluator', () => {
         expect(actual).to.be.false;
       });
 
+      it('should negate -11 to 11 and evaluate as not less or equal to 10', () => {
+        const stub = sinon
+          .stub(farmhash, 'fingerprint64')
+          .returns('-11');
+
+        stubs.push(stub);
+        const condition = {
+          name: 'is_enabled',
+          condition: {
+            orCondition: {
+              conditions: [{
+                andCondition: {
+                  conditions: [{
+                    percent: {
+                      percentOperator: PercentConditionOperator.LESS_OR_EQUAL,
+                      seed: 'abcdef',
+                      microPercent: 10
+                    }
+                  }],
+                }
+              }]
+            }
+          }
+        };
+        const context = { randomizationId: '123' }
+        const evaluator = new ConditionEvaluator();
+        const actual = evaluator.evaluateConditions([condition], context)
+          .get('is_enabled');
+        expect(actual).to.be.false;
+      });
+
       it('should evaluate greater than min to true', () => {
         const condition = {
           name: 'is_enabled',

From 0aca056c9d5d1ee820c34fdc13aac1e58528169b Mon Sep 17 00:00:00 2001
From: Xin Wei <trekforever@users.noreply.github.com>
Date: Thu, 4 Apr 2024 10:07:25 -0700
Subject: [PATCH 10/13] Add functionality to accept a JSON template string when
 initializing a RC server template (#2520)

* Add support to pass in a json string for RC server template initialization

* Merge ssrc changes

* Apply lint changes

* Update comments

* Run api-extractor:local

* Update comments and address feedback

* Update inline comment and lint fixes

* Revert toJSON functionality for ServerTemplate
---
 etc/firebase-admin.remote-config.api.md       |  2 +-
 src/remote-config/remote-config-api.ts        |  4 +-
 src/remote-config/remote-config.ts            | 16 ++++-
 test/unit/remote-config/remote-config.spec.ts | 67 ++++++++++++++++---
 4 files changed, 77 insertions(+), 12 deletions(-)

diff --git a/etc/firebase-admin.remote-config.api.md b/etc/firebase-admin.remote-config.api.md
index 746d72d914..25a09b0bf4 100644
--- a/etc/firebase-admin.remote-config.api.md
+++ b/etc/firebase-admin.remote-config.api.md
@@ -40,7 +40,7 @@ export interface InAppDefaultValue {
 
 // @public
 export interface InitServerTemplateOptions extends GetServerTemplateOptions {
-    template?: ServerTemplateData;
+    template?: ServerTemplateData | string;
 }
 
 // @public
diff --git a/src/remote-config/remote-config-api.ts b/src/remote-config/remote-config-api.ts
index 66d310daf8..f8d1a07392 100644
--- a/src/remote-config/remote-config-api.ts
+++ b/src/remote-config/remote-config-api.ts
@@ -375,8 +375,10 @@ export interface InitServerTemplateOptions extends GetServerTemplateOptions {
    * example, customers can reduce initialization latency by pre-fetching and
    * caching template data and then using this option to initialize the SDK with
    * that data.
+   * The template can be initialized with either a {@link ServerTemplateData}
+   * object or a JSON string.
    */
-  template?: ServerTemplateData,
+  template?: ServerTemplateData|string,
 }
 
 /**
diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts
index 887a8a1905..e0e7692e28 100644
--- a/src/remote-config/remote-config.ts
+++ b/src/remote-config/remote-config.ts
@@ -39,6 +39,7 @@ import {
   GetServerTemplateOptions,
   InitServerTemplateOptions,
 } from './remote-config-api';
+import { isString } from 'lodash';
 
 /**
  * The Firebase `RemoteConfig` service interface.
@@ -198,7 +199,19 @@ export class RemoteConfig {
     const template = new ServerTemplateImpl(
       this.client, new ConditionEvaluator(), options?.defaultConfig);
     if (options?.template) {
-      template.cache = options?.template;
+      // Check and instantiates the template via a json string
+      if (isString(options?.template)) {
+        try {
+          template.cache = new ServerTemplateDataImpl(JSON.parse(options?.template));
+        } catch (e) {
+          throw new FirebaseRemoteConfigError(
+            'invalid-argument',
+            `Failed to parse the JSON string: ${options?.template}. ` + e
+          );
+        }
+      } else {
+        template.cache = options?.template;
+      }
     }
     return template;
   }
@@ -430,7 +443,6 @@ class ServerTemplateDataImpl implements ServerTemplateData {
     }
 
     this.etag = template.etag;
-
     if (typeof template.parameters !== 'undefined') {
       if (!validator.isNonNullObject(template.parameters)) {
         throw new FirebaseRemoteConfigError(
diff --git a/test/unit/remote-config/remote-config.spec.ts b/test/unit/remote-config/remote-config.spec.ts
index 19a1aed3d2..5f116363c8 100644
--- a/test/unit/remote-config/remote-config.spec.ts
+++ b/test/unit/remote-config/remote-config.spec.ts
@@ -35,7 +35,7 @@ import {
 } from '../../../src/remote-config/remote-config-api-client-internal';
 import { deepCopy } from '../../../src/utils/deep-copy';
 import {
-  NamedCondition, ServerTemplate, ServerTemplateData
+  NamedCondition, ServerTemplate, ServerTemplateData, Version
 } from '../../../src/remote-config/remote-config-api';
 
 const expect = chai.expect;
@@ -648,10 +648,61 @@ describe('RemoteConfig', () => {
           valueType: 'STRING'
         }
       };
-      const initializedTemplate = remoteConfig.initServerTemplate({ template }).cache;
-      const parsed = JSON.parse(JSON.stringify(initializedTemplate));
+      const initializedTemplate = remoteConfig.initServerTemplate({ template });
+      const parsed = JSON.parse(JSON.stringify(initializedTemplate.cache));
       expect(parsed).deep.equals(deepCopy(template));
     });
+
+    it('should set and instantiates template when json string is passed', () => {
+      const template = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData;
+      template.parameters = {
+        dog_type: {
+          defaultValue: {
+            value: 'shiba'
+          },
+          description: 'Type of dog breed',
+          valueType: 'STRING'
+        }
+      };
+      const templateJson = JSON.stringify(template);
+      const initializedTemplate = remoteConfig.initServerTemplate({ template: templateJson });
+      const parsed = JSON.parse(JSON.stringify(initializedTemplate.cache));
+      const expectedVersion = deepCopy(VERSION_INFO);
+      expectedVersion.updateTime = new Date(expectedVersion.updateTime).toUTCString();
+      template.version = expectedVersion as Version;
+      expect(parsed).deep.equals(deepCopy(template));
+    });
+
+    describe('should throw error if invalid template JSON is passed', () => {
+      const INVALID_PARAMETERS: any[] = [null, '', 'abc', 1, true, []];
+      const INVALID_CONDITIONS: any[] = [null, '', 'abc', 1, true, {}];
+
+      let sourceTemplate = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE);
+      const jsonString = '{invalidJson: null}';
+      it('should throw if template is an invalid JSON', () => {
+        expect(() => remoteConfig.initServerTemplate({ template: jsonString }))
+          .to.throw(/Failed to parse the JSON string: ([\D\w]*)\./);
+      });
+
+      INVALID_PARAMETERS.forEach((invalidParameter) => {
+        sourceTemplate.parameters = invalidParameter;
+        const jsonString = JSON.stringify(sourceTemplate);
+        it(`should throw if the parameters is ${JSON.stringify(invalidParameter)}`, () => {
+          expect(() => remoteConfig.initServerTemplate({ template: jsonString }))
+            .to.throw('Remote Config parameters must be a non-null object');
+        });
+      });
+
+      sourceTemplate = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE);
+      INVALID_CONDITIONS.forEach((invalidConditions) => {
+        sourceTemplate.conditions = invalidConditions;
+        const jsonString = JSON.stringify(sourceTemplate);
+        it(`should throw if the conditions is ${JSON.stringify(invalidConditions)}`, () => {
+          expect(() => remoteConfig.initServerTemplate({ template: jsonString }))
+            .to.throw('Remote Config conditions must be an array');
+        });
+      });
+    });
   });
 
   describe('RemoteConfigServerTemplate', () => {
@@ -1039,12 +1090,12 @@ describe('RemoteConfig', () => {
       it('uses local default if parameter not in template', () => {
         const template = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData;
         template.parameters = {};
-  
+
         const stub = sinon
           .stub(RemoteConfigApiClient.prototype, 'getServerTemplate')
           .resolves(template);
         stubs.push(stub);
-  
+
         const defaultConfig = {
           dog_coat: 'blue merle',
         };
@@ -1061,12 +1112,12 @@ describe('RemoteConfig', () => {
         template.parameters = {
           dog_no_remote_default_value: {}
         };
-  
+
         const stub = sinon
           .stub(RemoteConfigApiClient.prototype, 'getServerTemplate')
           .resolves(template);
         stubs.push(stub);
-  
+
         const defaultConfig = {
           dog_no_remote_default_value: 'local default'
         };
@@ -1083,7 +1134,7 @@ describe('RemoteConfig', () => {
         template.parameters = {
           dog_no_remote_default_value: {}
         };
-  
+
         const stub = sinon
           .stub(RemoteConfigApiClient.prototype, 'getServerTemplate')
           .resolves(template);

From 8dbf86f56accfd143676a37130542524898fb8a7 Mon Sep 17 00:00:00 2001
From: Erik Eldridge <erikeldridge@google.com>
Date: Thu, 4 Apr 2024 15:03:34 -0700
Subject: [PATCH 11/13] Define type-specific getters for SSRC (#2519)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

RC's existing SDKs define type-specific getters, like getBoolean.
These aren't idiomatic for TS/JS, but have a couple advantages:

1. RC param names, values and types are mutable remotely, so a
simple object can’t guarantee a strict type for application logic. A
formal schema would address this, but feels excessive for the
common case. Type-specific methods are consistent with RC's
current SDKs and ensure appropriate types for application logic.
2. RC Android and iOS SDKs log events when personalized values
are used. A method interface facilitates such additional functionality
---
 etc/firebase-admin.remote-config.api.md       |  27 ++-
 src/remote-config/index.ts                    |   3 +
 src/remote-config/internal/value-impl.ts      |  61 ++++++
 src/remote-config/remote-config-api.ts        |  98 +++++++++-
 src/remote-config/remote-config.ts            |  82 ++++----
 test/unit/index.spec.ts                       |   1 +
 .../remote-config/internal/value-impl.spec.ts |  75 ++++++++
 test/unit/remote-config/remote-config.spec.ts | 180 +++++++++++++-----
 8 files changed, 428 insertions(+), 99 deletions(-)
 create mode 100644 src/remote-config/internal/value-impl.ts
 create mode 100644 test/unit/remote-config/internal/value-impl.spec.ts

diff --git a/etc/firebase-admin.remote-config.api.md b/etc/firebase-admin.remote-config.api.md
index 25a09b0bf4..3d803d1563 100644
--- a/etc/firebase-admin.remote-config.api.md
+++ b/etc/firebase-admin.remote-config.api.md
@@ -13,6 +13,11 @@ export interface AndCondition {
     conditions?: Array<OneOfCondition>;
 }
 
+// @public
+export type DefaultConfig = {
+    [key: string]: string | number | boolean;
+};
+
 // @public
 export type EvaluationContext = {
     randomizationId?: string;
@@ -30,7 +35,7 @@ export function getRemoteConfig(app?: App): RemoteConfig;
 
 // @public
 export interface GetServerTemplateOptions {
-    defaultConfig?: ServerConfig;
+    defaultConfig?: DefaultConfig;
 }
 
 // @public
@@ -169,9 +174,12 @@ export interface RemoteConfigUser {
 }
 
 // @public
-export type ServerConfig = {
-    [key: string]: string | boolean | number;
-};
+export interface ServerConfig {
+    getBoolean(key: string): boolean;
+    getNumber(key: string): number;
+    getString(key: string): string;
+    getValue(key: string): Value;
+}
 
 // @public
 export interface ServerTemplate {
@@ -193,6 +201,17 @@ export interface ServerTemplateData {
 // @public
 export type TagColor = 'BLUE' | 'BROWN' | 'CYAN' | 'DEEP_ORANGE' | 'GREEN' | 'INDIGO' | 'LIME' | 'ORANGE' | 'PINK' | 'PURPLE' | 'TEAL';
 
+// @public
+export interface Value {
+    asBoolean(): boolean;
+    asNumber(): number;
+    asString(): string;
+    getSource(): ValueSource;
+}
+
+// @public
+export type ValueSource = 'static' | 'default' | 'remote';
+
 // @public
 export interface Version {
     description?: string;
diff --git a/src/remote-config/index.ts b/src/remote-config/index.ts
index cb2e6285b8..c703caf10b 100644
--- a/src/remote-config/index.ts
+++ b/src/remote-config/index.ts
@@ -26,6 +26,7 @@ import { RemoteConfig } from './remote-config';
 
 export {
   AndCondition,
+  DefaultConfig,
   EvaluationContext,
   ExplicitParameterValue,
   GetServerTemplateOptions,
@@ -50,6 +51,8 @@ export {
   ServerTemplate,
   ServerTemplateData,
   TagColor,
+  Value,
+  ValueSource,
   Version,
 } from './remote-config-api';
 export { RemoteConfig } from './remote-config';
diff --git a/src/remote-config/internal/value-impl.ts b/src/remote-config/internal/value-impl.ts
new file mode 100644
index 0000000000..6d71476538
--- /dev/null
+++ b/src/remote-config/internal/value-impl.ts
@@ -0,0 +1,61 @@
+/*!
+ * Copyright 2024 Google Inc.
+ *
+ * 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 {
+  Value,
+  ValueSource,
+} from '../remote-config-api';
+
+/**
+ * Implements type-safe getters for parameter values.
+ * 
+ * Visible for testing.
+ * 
+ * @internal
+ */
+export class ValueImpl implements Value {
+  public static readonly DEFAULT_VALUE_FOR_BOOLEAN = false;
+  public static readonly DEFAULT_VALUE_FOR_STRING = '';
+  public static readonly DEFAULT_VALUE_FOR_NUMBER = 0;
+  public static readonly BOOLEAN_TRUTHY_VALUES = ['1', 'true', 't', 'yes', 'y', 'on'];
+  constructor(
+    private readonly source: ValueSource,
+    private readonly value = ValueImpl.DEFAULT_VALUE_FOR_STRING) { }
+  asString(): string {
+    return this.value;
+  }
+  asBoolean(): boolean {
+    if (this.source === 'static') {
+      return ValueImpl.DEFAULT_VALUE_FOR_BOOLEAN;
+    }
+    return ValueImpl.BOOLEAN_TRUTHY_VALUES.indexOf(this.value.toLowerCase()) >= 0;
+  }
+  asNumber(): number {
+    if (this.source === 'static') {
+      return ValueImpl.DEFAULT_VALUE_FOR_NUMBER;
+    }
+    const num = Number(this.value);
+    if (isNaN(num)) {
+      return ValueImpl.DEFAULT_VALUE_FOR_NUMBER;
+    }
+    return num;
+  }
+  getSource(): ValueSource {
+    return this.source;
+  }
+}
diff --git a/src/remote-config/remote-config-api.ts b/src/remote-config/remote-config-api.ts
index f8d1a07392..898b0c3bbd 100644
--- a/src/remote-config/remote-config-api.ts
+++ b/src/remote-config/remote-config-api.ts
@@ -361,7 +361,7 @@ export interface GetServerTemplateOptions {
    * intended before it connects to the Remote Config backend, and so that
    * default values are available if none are set on the backend.
    */
-  defaultConfig?: ServerConfig,
+  defaultConfig?: DefaultConfig;
 }
 
 /**
@@ -541,4 +541,98 @@ export interface ListVersionsOptions {
 /**
  * Represents the configuration produced by evaluating a server template.
  */
-export type ServerConfig = { [key: string]: string | boolean | number }
+export interface ServerConfig {
+
+  /**
+   * Gets the value for the given key as a boolean.
+   *
+   * Convenience method for calling <code>serverConfig.getValue(key).asBoolean()</code>.
+   *
+   * @param key - The name of the parameter.
+   *
+   * @returns The value for the given key as a boolean.
+   */
+  getBoolean(key: string): boolean;
+
+  /**
+   * Gets the value for the given key as a number.
+   *
+   * Convenience method for calling <code>serverConfig.getValue(key).asNumber()</code>.
+   *
+   * @param key - The name of the parameter.
+   *
+   * @returns The value for the given key as a number.
+   */
+  getNumber(key: string): number;
+
+  /**
+   * Gets the value for the given key as a string.
+   * Convenience method for calling <code>serverConfig.getValue(key).asString()</code>.
+   *
+   * @param key - The name of the parameter.
+   *
+   * @returns The value for the given key as a string.
+   */
+  getString(key: string): string;
+
+  /**
+   * Gets the {@link Value} for the given key.
+   *
+   * Ensures application logic will always have a type-safe reference,
+   * even if the parameter is removed remotely.
+   *
+   * @param key - The name of the parameter.
+   *
+   * @returns The value for the given key.
+   */
+  getValue(key: string): Value;
+}
+
+/**
+ * Wraps a parameter value with metadata and type-safe getters.
+ *
+ * Type-safe getters insulate application logic from remote
+ * changes to parameter names and types.
+ */
+export interface Value {
+
+  /**
+   * Gets the value as a boolean.
+   *
+   * The following values (case insensitive) are interpreted as true:
+   * "1", "true", "t", "yes", "y", "on". Other values are interpreted as false.
+   */
+  asBoolean(): boolean;
+
+  /**
+   * Gets the value as a number. Comparable to calling <code>Number(value) || 0</code>.
+   */
+  asNumber(): number;
+
+  /**
+   * Gets the value as a string.
+   */
+  asString(): string;
+
+  /**
+   * Gets the {@link ValueSource} for the given key.
+   */
+  getSource(): ValueSource;
+}
+
+/**
+ * Indicates the source of a value.
+ *
+ * <ul>
+ *   <li>"static" indicates the value was defined by a static constant.</li>
+ *   <li>"default" indicates the value was defined by default config.</li>
+ *   <li>"remote" indicates the value was defined by config produced by
+ *   evaluating a template.</li>
+ * </ul>
+ */
+export type ValueSource = 'static' | 'default' | 'remote';
+
+/**
+ * Defines the format for in-app default parameter values.
+ */
+export type DefaultConfig = { [key: string]: string | number | boolean };
diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts
index e0e7692e28..f2a341b6d8 100644
--- a/src/remote-config/remote-config.ts
+++ b/src/remote-config/remote-config.ts
@@ -18,6 +18,7 @@ import { App } from '../app';
 import * as validator from '../utils/validator';
 import { FirebaseRemoteConfigError, RemoteConfigApiClient } from './remote-config-api-client-internal';
 import { ConditionEvaluator } from './condition-evaluator-internal';
+import { ValueImpl } from './internal/value-impl';
 import {
   ListVersionsOptions,
   ListVersionsResult,
@@ -30,12 +31,13 @@ import {
   Version,
   ExplicitParameterValue,
   InAppDefaultValue,
-  ParameterValueType,
   ServerConfig,
   RemoteConfigParameterValue,
   EvaluationContext,
   ServerTemplateData,
   NamedCondition,
+  Value,
+  DefaultConfig,
   GetServerTemplateOptions,
   InitServerTemplateOptions,
 } from './remote-config-api';
@@ -306,12 +308,20 @@ class RemoteConfigTemplateImpl implements RemoteConfigTemplate {
  */
 class ServerTemplateImpl implements ServerTemplate {
   public cache: ServerTemplateData;
+  private stringifiedDefaultConfig: {[key: string]: string} = {};
 
   constructor(
     private readonly apiClient: RemoteConfigApiClient,
     private readonly conditionEvaluator: ConditionEvaluator,
-    private readonly defaultConfig: ServerConfig = {}
-  ) { }
+    public readonly defaultConfig: DefaultConfig = {}
+  ) {
+    // RC stores all remote values as string, but it's more intuitive
+    // to declare default values with specific types, so this converts
+    // the external declaration to an internal string representation.
+    for (const key in defaultConfig) {
+      this.stringifiedDefaultConfig[key] = String(defaultConfig[key]);
+    }
+  }
 
   /**
    * Fetches and caches the current active version of the project's {@link ServerTemplate}.
@@ -340,10 +350,16 @@ class ServerTemplateImpl implements ServerTemplate {
     const evaluatedConditions = this.conditionEvaluator.evaluateConditions(
       this.cache.conditions, context);
 
-    const evaluatedConfig: ServerConfig = {};
+    const configValues: { [key: string]: Value } = {};
 
+    // Initializes config Value objects with default values.
+    for (const key in this.stringifiedDefaultConfig) {
+      configValues[key] = new ValueImpl('default', this.stringifiedDefaultConfig[key]);
+    }
+
+    // Overlays config Value objects derived by evaluating the template.
     for (const [key, parameter] of Object.entries(this.cache.parameters)) {
-      const { conditionalValues, defaultValue, valueType } = parameter;
+      const { conditionalValues, defaultValue } = parameter;
 
       // Supports parameters with no conditional values.
       const normalizedConditionalValues = conditionalValues || {};
@@ -366,7 +382,7 @@ class ServerTemplateImpl implements ServerTemplate {
 
       if (parameterValueWrapper) {
         const parameterValue = (parameterValueWrapper as ExplicitParameterValue).value;
-        evaluatedConfig[key] = this.parseRemoteConfigParameterValue(valueType, parameterValue);
+        configValues[key] = new ValueImpl('remote', parameterValue);
         continue;
       }
 
@@ -381,46 +397,28 @@ class ServerTemplateImpl implements ServerTemplate {
       }
 
       const parameterDefaultValue = (defaultValue as ExplicitParameterValue).value;
-      evaluatedConfig[key] = this.parseRemoteConfigParameterValue(valueType, parameterDefaultValue);
+      configValues[key] = new ValueImpl('remote', parameterDefaultValue);
     }
 
-    const mergedConfig = {};
-
-    // Merges default config and rendered config, prioritizing the latter.
-    Object.assign(mergedConfig, this.defaultConfig, evaluatedConfig);
-
-    // Enables config to be a convenient object, but with the ability to perform additional
-    // functionality when a value is retrieved.
-    const proxyHandler = {
-      get(target: ServerConfig, prop: string) {
-        return target[prop];
-      }
-    };
-
-    return new Proxy(mergedConfig, proxyHandler);
+    return new ServerConfigImpl(configValues);
   }
+}
 
-  /**
-   * Private helper method that coerces a parameter value string to the {@link ParameterValueType}.
-   */
-  private parseRemoteConfigParameterValue(parameterType: ParameterValueType | undefined,
-    parameterValue: string): string | number | boolean {
-    const BOOLEAN_TRUTHY_VALUES = ['1', 'true', 't', 'yes', 'y', 'on'];
-    const DEFAULT_VALUE_FOR_NUMBER = 0;
-    const DEFAULT_VALUE_FOR_STRING = '';
-
-    if (parameterType === 'BOOLEAN') {
-      return BOOLEAN_TRUTHY_VALUES.indexOf(parameterValue) >= 0;
-    } else if (parameterType === 'NUMBER') {
-      const num = Number(parameterValue);
-      if (isNaN(num)) {
-        return DEFAULT_VALUE_FOR_NUMBER;
-      }
-      return num;
-    } else {
-      // Treat everything else as string
-      return parameterValue || DEFAULT_VALUE_FOR_STRING;
-    }
+class ServerConfigImpl implements ServerConfig {
+  constructor(
+    private readonly configValues: { [key: string]: Value },
+  ){}
+  getBoolean(key: string): boolean {
+    return this.getValue(key).asBoolean();
+  }
+  getNumber(key: string): number {
+    return this.getValue(key).asNumber();
+  }
+  getString(key: string): string {
+    return this.getValue(key).asString();
+  }
+  getValue(key: string): Value {
+    return this.configValues[key] || new ValueImpl('static');
   }
 }
 
diff --git a/test/unit/index.spec.ts b/test/unit/index.spec.ts
index 29516d7a82..31efeaf979 100644
--- a/test/unit/index.spec.ts
+++ b/test/unit/index.spec.ts
@@ -98,6 +98,7 @@ import './remote-config/index.spec';
 import './remote-config/remote-config.spec';
 import './remote-config/remote-config-api-client.spec';
 import './remote-config/condition-evaluator.spec';
+import './remote-config/internal/value-impl.spec';
 
 // AppCheck
 import './app-check/app-check.spec';
diff --git a/test/unit/remote-config/internal/value-impl.spec.ts b/test/unit/remote-config/internal/value-impl.spec.ts
new file mode 100644
index 0000000000..b344d0c9d1
--- /dev/null
+++ b/test/unit/remote-config/internal/value-impl.spec.ts
@@ -0,0 +1,75 @@
+/*!
+ * Copyright 2024 Google Inc.
+ *
+ * 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 { ValueImpl } from '../../../../src/remote-config/internal/value-impl';
+
+const expect = chai.expect;
+
+describe('ValueImpl', () => {
+  describe('getSource', () => {
+    it('returns the source string', () => {
+      const value = new ValueImpl('static');
+      expect(value.getSource()).to.equal('static');
+    });
+  });
+
+  describe('asString', () => {
+    it('returns string value as a string', () => {
+      const value = new ValueImpl('default', 'shiba');
+      expect(value.asString()).to.equal('shiba');
+    });
+
+    it('defaults to empty string', () => {
+      const value = new ValueImpl('static');
+      expect(value.asString()).to.equal(ValueImpl.DEFAULT_VALUE_FOR_STRING);
+    });
+  });
+
+  describe('asNumber', () => {
+    it('returns numeric value as a number', () => {
+      const value = new ValueImpl('default', '123');
+      expect(value.asNumber()).to.equal(123);
+    });
+
+    it('defaults to zero for non-numeric value', () => {
+      const value = new ValueImpl('default', 'Hi, NaN!');
+      expect(value.asNumber()).to.equal(ValueImpl.DEFAULT_VALUE_FOR_NUMBER);
+    });
+  });
+
+  describe('asBoolean', () => {
+    it("returns true for any value in RC's list of truthy values", () => {
+      for (const truthyValue of ValueImpl.BOOLEAN_TRUTHY_VALUES) {
+        const value = new ValueImpl('default', truthyValue);
+        expect(value.asBoolean()).to.be.true;
+      }
+    });
+
+    it('is case-insensitive', () => {
+      const value = new ValueImpl('default', 'TRUE');
+      expect(value.asBoolean()).to.be.true;
+    });
+
+    it("returns false for any value not in RC's list of truthy values", () => {
+      const value = new ValueImpl('default', "I'm falsy");
+      expect(value.asBoolean()).to.be.false;
+    });
+  });
+});
+
diff --git a/test/unit/remote-config/remote-config.spec.ts b/test/unit/remote-config/remote-config.spec.ts
index 5f116363c8..2fe8f1cc43 100644
--- a/test/unit/remote-config/remote-config.spec.ts
+++ b/test/unit/remote-config/remote-config.spec.ts
@@ -132,9 +132,7 @@ describe('RemoteConfig', () => {
     parameters: {
       holiday_promo_enabled: {
         defaultValue: { value: 'true' },
-        conditionalValues: { ios: { useInAppDefault: true } },
-        description: 'this is a promo',
-        valueType: 'BOOLEAN',
+        conditionalValues: { ios: { useInAppDefault: true } }
       },
     },
     etag: 'etag-123456789012-5',
@@ -592,8 +590,6 @@ describe('RemoteConfig', () => {
           const p1 = template.cache.parameters[key];
           expect(p1.defaultValue).deep.equals({ value: 'true' });
           expect(p1.conditionalValues).deep.equals({ ios: { useInAppDefault: true } });
-          expect(p1.description).equals('this is a promo');
-          expect(p1.valueType).equals('BOOLEAN');
 
           const c = template.cache.conditions.find((c) => c.name === 'ios');
           expect(c).to.be.not.undefined;
@@ -628,9 +624,9 @@ describe('RemoteConfig', () => {
       return remoteConfig.getServerTemplate({ defaultConfig })
         .then((template) => {
           const config = template.evaluate();
-          expect(config.holiday_promo_enabled).to.equal(
+          expect(config.getBoolean('holiday_promo_enabled')).to.equal(
             defaultConfig.holiday_promo_enabled);
-          expect(config.holiday_promo_discount).to.equal(
+          expect(config.getNumber('holiday_promo_discount')).to.equal(
             defaultConfig.holiday_promo_discount);
         });
     });
@@ -643,9 +639,7 @@ describe('RemoteConfig', () => {
         dog_type: {
           defaultValue: {
             value: 'shiba'
-          },
-          description: 'Type of dog breed',
-          valueType: 'STRING'
+          }
         }
       };
       const initializedTemplate = remoteConfig.initServerTemplate({ template });
@@ -711,41 +705,29 @@ describe('RemoteConfig', () => {
       dog_type: {
         defaultValue: {
           value: 'corgi'
-        },
-        description: 'Type of dog breed',
-        valueType: 'STRING'
+        }
       },
       dog_type_enabled: {
         defaultValue: {
           value: 'true'
-        },
-        description: 'It\'s true or false',
-        valueType: 'BOOLEAN'
+        }
       },
       dog_age: {
         defaultValue: {
           value: '22'
-        },
-        description: 'Age',
-        valueType: 'NUMBER'
+        }
       },
       dog_jsonified: {
         defaultValue: {
           value: '{"name":"Taro","breed":"Corgi","age":1,"fluffiness":100}'
-        },
-        description: 'Dog Json Response',
-        valueType: 'JSON'
+        }
       },
       dog_use_inapp_default: {
         defaultValue: {
           useInAppDefault: true
-        },
-        description: 'Use in-app default dog',
-        valueType: 'STRING'
+        }
       },
       dog_no_remote_default_value: {
-        description: 'TIL: default values are optional!',
-        valueType: 'STRING'
       }
     };
 
@@ -860,8 +842,6 @@ describe('RemoteConfig', () => {
             const p1 = template.cache.parameters[key];
             expect(p1.defaultValue).deep.equals({ value: 'true' });
             expect(p1.conditionalValues).deep.equals({ ios: { useInAppDefault: true } });
-            expect(p1.description).equals('this is a promo');
-            expect(p1.valueType).equals('BOOLEAN');
 
             const c = template.cache.conditions.find((c) => c.name === 'ios');
             expect(c).to.be.not.undefined;
@@ -980,10 +960,9 @@ describe('RemoteConfig', () => {
         return remoteConfig.getServerTemplate()
           .then((template: ServerTemplate) => {
             const config = template.evaluate!();
-            expect(config.dog_type).to.equal('corgi');
-            expect(config.dog_type_enabled).to.equal(true);
-            expect(config.dog_age).to.equal(22);
-            expect(config.dog_jsonified).to.equal('{"name":"Taro","breed":"Corgi","age":1,"fluffiness":100}');
+            expect(config.getString('dog_type')).to.equal('corgi');
+            expect(config.getBoolean('dog_type_enabled')).to.equal(true);
+            expect(config.getNumber('dog_age')).to.equal(22);
           });
       });
 
@@ -1014,15 +993,14 @@ describe('RemoteConfig', () => {
             parameters: {
               is_enabled: {
                 defaultValue: { value: 'false' },
-                conditionalValues: { is_true: { value: 'true' } },
-                valueType: 'BOOLEAN',
+                conditionalValues: { is_true: { value: 'true' } }
               },
             },
             etag: '123'
           }
         });
         const config = template.evaluate();
-        expect(config.is_enabled).to.be.true;
+        expect(config.getBoolean('is_enabled')).to.be.true;
       });
 
       it('honors condition order', () => {
@@ -1076,15 +1054,14 @@ describe('RemoteConfig', () => {
                   // value is selected.
                   is_true_too: { value: 'dachshund' },
                   is_true: { value: 'corgi' }
-                },
-                valueType: 'STRING',
+                }
               },
             },
             etag: '123'
           }
         });
         const config = template.evaluate();
-        expect(config.dog_type).to.eq('corgi');
+        expect(config.getString('dog_type')).to.eq('corgi');
       });
 
       it('uses local default if parameter not in template', () => {
@@ -1103,7 +1080,7 @@ describe('RemoteConfig', () => {
         return remoteConfig.getServerTemplate({ defaultConfig })
           .then((template: ServerTemplate) => {
             const config = template.evaluate();
-            expect(config.dog_coat).to.equal(defaultConfig.dog_coat);
+            expect(config.getString('dog_coat')).to.equal(defaultConfig.dog_coat);
           });
       });
 
@@ -1125,7 +1102,8 @@ describe('RemoteConfig', () => {
         return remoteConfig.getServerTemplate({ defaultConfig })
           .then((template: ServerTemplate) => {
             const config = template.evaluate!();
-            expect(config.dog_no_remote_default_value).to.equal(defaultConfig.dog_no_remote_default_value);
+            expect(config.getString('dog_no_remote_default_value')).to.equal(
+              defaultConfig.dog_no_remote_default_value);
           });
       });
 
@@ -1147,7 +1125,8 @@ describe('RemoteConfig', () => {
         return remoteConfig.getServerTemplate({ defaultConfig })
           .then((template: ServerTemplate) => {
             const config = template.evaluate!();
-            expect(config.dog_use_inapp_default).to.equal(defaultConfig.dog_use_inapp_default);
+            expect(config.getString('dog_use_inapp_default')).to.equal(
+              defaultConfig.dog_use_inapp_default);
           });
       });
 
@@ -1168,8 +1147,7 @@ describe('RemoteConfig', () => {
           dog_type: {
             defaultValue: {
               value: 'pug'
-            },
-            valueType: 'STRING'
+            }
           },
         }
 
@@ -1177,14 +1155,13 @@ describe('RemoteConfig', () => {
 
         let config = template.evaluate();
 
-        expect(config.dog_type).to.equal('pug');
+        expect(config.getString('dog_type')).to.equal('pug');
 
         response.parameters = {
           dog_type: {
             defaultValue: {
               useInAppDefault: true
-            },
-            valueType: 'STRING'
+            }
           },
         }
 
@@ -1192,7 +1169,7 @@ describe('RemoteConfig', () => {
 
         config = template.evaluate();
 
-        expect(config.dog_type).to.equal('corgi');
+        expect(config.getString('dog_type')).to.equal('corgi');
       });
 
       it('overrides local default when remote value exists', () => {
@@ -1202,8 +1179,7 @@ describe('RemoteConfig', () => {
             defaultValue: {
               // Defines remote value
               value: 'true'
-            },
-            valueType: 'BOOLEAN'
+            }
           },
         }
 
@@ -1221,12 +1197,114 @@ describe('RemoteConfig', () => {
           .then((template: ServerTemplate) => {
             const config = template.evaluate();
             // Asserts remote value overrides local default.
-            expect(config.dog_type_enabled).to.be.true;
+            expect(config.getBoolean('dog_type_enabled')).to.be.true;
           });
       });
     });
   });
 
+  // Note the static source is set in the getValue() method, but the other sources
+  // are set in the evaluate() method, so these tests span a couple layers.
+  describe('ServerConfig', () => {
+    describe('getValue', () => {
+      it('should return static when default and remote are not defined', () => {
+        const templateData = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData;
+        // Omits remote parameter values.
+        templateData.parameters = {
+        };
+        // Omits in-app default values.
+        const template = remoteConfig.initServerTemplate({ template: templateData });
+        const config = template.evaluate();
+        const value = config.getValue('dog_type');
+        expect(value.asString()).to.equal('');
+        expect(value.getSource()).to.equal('static');
+      });
+  
+      it('should return default value when it is defined', () => {
+        const templateData = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData;
+        // Omits remote parameter values.
+        templateData.parameters = {
+        };
+        const template = remoteConfig.initServerTemplate({
+          template: templateData,
+          // Defines in-app default values.
+          defaultConfig: {
+            dog_type: 'shiba'
+          }
+        });
+        const config = template.evaluate();
+        const value = config.getValue('dog_type');
+        expect(value.asString()).to.equal('shiba');
+        expect(value.getSource()).to.equal('default');
+      });
+  
+      it('should return remote value when it is defined', () => {
+        const templateData = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData;
+        // Defines remote parameter values.
+        templateData.parameters = {
+          dog_type: {
+            defaultValue: {
+              value: 'pug'
+            }
+          }
+        };
+        const template = remoteConfig.initServerTemplate({
+          template: templateData,
+          // Defines in-app default values.
+          defaultConfig: {
+            dog_type: 'shiba'
+          }
+        });
+        const config = template.evaluate();
+        const value = config.getValue('dog_type');
+        expect(value.asString()).to.equal('pug');
+        expect(value.getSource()).to.equal('remote');
+      });
+    });
+
+    describe('getString', () => {
+      it('returns a string value', () => {
+        const templateData = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData;
+        const template = remoteConfig.initServerTemplate({
+          template: templateData,
+          defaultConfig: {
+            dog_type: 'shiba'
+          }
+        });
+        const config = template.evaluate();
+        expect(config.getString('dog_type')).to.equal('shiba');
+      });
+    });
+
+    describe('getNumber', () => {
+      it('returns a numeric value', () => {
+        const templateData = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData;
+        const template = remoteConfig.initServerTemplate({
+          template: templateData,
+          defaultConfig: {
+            dog_age: 12
+          }
+        });
+        const config = template.evaluate();
+        expect(config.getNumber('dog_age')).to.equal(12);
+      });
+    });
+
+    describe('getBoolean', () => {
+      it('returns a boolean value', () => {
+        const templateData = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData;
+        const template = remoteConfig.initServerTemplate({
+          template: templateData,
+          defaultConfig: {
+            dog_is_cute: true
+          }
+        });
+        const config = template.evaluate();
+        expect(config.getBoolean('dog_is_cute')).to.be.true;
+      });
+    });
+  });
+
   function runInvalidResponseTests(rcOperation: () => Promise<RemoteConfigTemplate>,
     operationName: any): void {
     it('should propagate API errors', () => {

From 86f4426050e12ab06b62125691f07f7d62316c62 Mon Sep 17 00:00:00 2001
From: Xin Wei <trekforever@users.noreply.github.com>
Date: Tue, 9 Apr 2024 10:44:37 -0700
Subject: [PATCH 12/13] Add #set and #toJSON to RC ServerTemplate (#2522)

* Add #set on ServerTemplate to allow for setting and caching a server template

* Simplify initServerTemplate to make use of the new setter

* Update tests

* Address comments and feedback

* Update API docs

* Add export for new type ServerTemplateDataType to index.ts

* Update some inline comments
---
 etc/firebase-admin.remote-config.api.md       |   8 +-
 src/remote-config/index.ts                    |   1 +
 src/remote-config/remote-config-api.ts        |  28 ++--
 src/remote-config/remote-config.ts            |  46 ++++--
 test/unit/remote-config/remote-config.spec.ts | 138 ++++++++++++++----
 5 files changed, 170 insertions(+), 51 deletions(-)

diff --git a/etc/firebase-admin.remote-config.api.md b/etc/firebase-admin.remote-config.api.md
index 3d803d1563..aeadcbf779 100644
--- a/etc/firebase-admin.remote-config.api.md
+++ b/etc/firebase-admin.remote-config.api.md
@@ -45,7 +45,7 @@ export interface InAppDefaultValue {
 
 // @public
 export interface InitServerTemplateOptions extends GetServerTemplateOptions {
-    template?: ServerTemplateData | string;
+    template?: ServerTemplateDataType;
 }
 
 // @public
@@ -183,9 +183,10 @@ export interface ServerConfig {
 
 // @public
 export interface ServerTemplate {
-    cache: ServerTemplateData;
     evaluate(context?: EvaluationContext): ServerConfig;
     load(): Promise<void>;
+    set(template: ServerTemplateDataType): void;
+    toJSON(): ServerTemplateData;
 }
 
 // @public
@@ -198,6 +199,9 @@ export interface ServerTemplateData {
     version?: Version;
 }
 
+// @public
+export type ServerTemplateDataType = ServerTemplateData | string;
+
 // @public
 export type TagColor = 'BLUE' | 'BROWN' | 'CYAN' | 'DEEP_ORANGE' | 'GREEN' | 'INDIGO' | 'LIME' | 'ORANGE' | 'PINK' | 'PURPLE' | 'TEAL';
 
diff --git a/src/remote-config/index.ts b/src/remote-config/index.ts
index c703caf10b..9198284b0e 100644
--- a/src/remote-config/index.ts
+++ b/src/remote-config/index.ts
@@ -50,6 +50,7 @@ export {
   ServerConfig,
   ServerTemplate,
   ServerTemplateData,
+  ServerTemplateDataType,
   TagColor,
   Value,
   ValueSource,
diff --git a/src/remote-config/remote-config-api.ts b/src/remote-config/remote-config-api.ts
index 898b0c3bbd..3ededc58c9 100644
--- a/src/remote-config/remote-config-api.ts
+++ b/src/remote-config/remote-config-api.ts
@@ -364,6 +364,13 @@ export interface GetServerTemplateOptions {
   defaultConfig?: DefaultConfig;
 }
 
+/**
+ * Represents the type of a Remote Config server template that can be set on
+ * {@link ServerTemplate}. This can either be a {@link ServerTemplateData} object
+ * or a template JSON string.
+ */
+export type ServerTemplateDataType = ServerTemplateData | string;
+
 /**
  * Represents optional arguments that can be used when instantiating
  * {@link ServerTemplate} synchonously.
@@ -375,22 +382,14 @@ export interface InitServerTemplateOptions extends GetServerTemplateOptions {
    * example, customers can reduce initialization latency by pre-fetching and
    * caching template data and then using this option to initialize the SDK with
    * that data.
-   * The template can be initialized with either a {@link ServerTemplateData}
-   * object or a JSON string.
    */
-  template?: ServerTemplateData|string,
+  template?: ServerTemplateDataType,
 }
 
 /**
  * Represents a stateful abstraction for a Remote Config server template.
  */
 export interface ServerTemplate {
-
-  /**
-   * Cached {@link ServerTemplateData}.
-   */
-  cache: ServerTemplateData;
-
   /**
    * Evaluates the current template to produce a {@link ServerConfig}.
    */
@@ -401,6 +400,17 @@ export interface ServerTemplate {
    * project's {@link ServerTemplate}.
    */
   load(): Promise<void>;
+
+  /**
+   * Sets and caches a {@link ServerTemplateData} or a JSON string representing
+   * the server template
+   */
+  set(template: ServerTemplateDataType): void;
+
+  /**
+   * Returns a JSON representation of {@link ServerTemplateData}
+   */
+  toJSON(): ServerTemplateData;
 }
 
 /**
diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts
index f2a341b6d8..bd23532013 100644
--- a/src/remote-config/remote-config.ts
+++ b/src/remote-config/remote-config.ts
@@ -40,6 +40,7 @@ import {
   DefaultConfig,
   GetServerTemplateOptions,
   InitServerTemplateOptions,
+  ServerTemplateDataType,
 } from './remote-config-api';
 import { isString } from 'lodash';
 
@@ -200,20 +201,9 @@ export class RemoteConfig {
   public initServerTemplate(options?: InitServerTemplateOptions): ServerTemplate {
     const template = new ServerTemplateImpl(
       this.client, new ConditionEvaluator(), options?.defaultConfig);
+
     if (options?.template) {
-      // Check and instantiates the template via a json string
-      if (isString(options?.template)) {
-        try {
-          template.cache = new ServerTemplateDataImpl(JSON.parse(options?.template));
-        } catch (e) {
-          throw new FirebaseRemoteConfigError(
-            'invalid-argument',
-            `Failed to parse the JSON string: ${options?.template}. ` + e
-          );
-        }
-      } else {
-        template.cache = options?.template;
-      }
+      template.set(options?.template);
     }
     return template;
   }
@@ -307,7 +297,7 @@ class RemoteConfigTemplateImpl implements RemoteConfigTemplate {
  * Remote Config dataplane template data implementation.
  */
 class ServerTemplateImpl implements ServerTemplate {
-  public cache: ServerTemplateData;
+  private cache: ServerTemplateData;
   private stringifiedDefaultConfig: {[key: string]: string} = {};
 
   constructor(
@@ -333,6 +323,27 @@ class ServerTemplateImpl implements ServerTemplate {
       });
   }
 
+  /**
+   * Parses a {@link ServerTemplateDataType} and caches it.
+   */
+  public set(template: ServerTemplateDataType): void {
+    let parsed;
+    if (isString(template)) {
+      try {
+        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);
+      }
+    } else {
+      parsed = template;
+    }
+    // Throws template parse errors.
+    this.cache = new ServerTemplateDataImpl(parsed);
+  }
+
   /**
    * Evaluates the current template in cache to produce a {@link ServerConfig}.
    */
@@ -402,6 +413,13 @@ class ServerTemplateImpl implements ServerTemplate {
 
     return new ServerConfigImpl(configValues);
   }
+
+  /**
+   * @returns JSON representation of the server template
+   */
+  public toJSON(): ServerTemplateData {
+    return this.cache;
+  }
 }
 
 class ServerConfigImpl implements ServerConfig {
diff --git a/test/unit/remote-config/remote-config.spec.ts b/test/unit/remote-config/remote-config.spec.ts
index 2fe8f1cc43..526dc0699e 100644
--- a/test/unit/remote-config/remote-config.spec.ts
+++ b/test/unit/remote-config/remote-config.spec.ts
@@ -572,11 +572,11 @@ describe('RemoteConfig', () => {
 
       return remoteConfig.getServerTemplate()
         .then((template) => {
-          expect(template.cache.conditions.length).to.equal(1);
-          expect(template.cache.conditions[0].name).to.equal('ios');
-          expect(template.cache.etag).to.equal('etag-123456789012-5');
+          expect(template.toJSON().conditions.length).to.equal(1);
+          expect(template.toJSON().conditions[0].name).to.equal('ios');
+          expect(template.toJSON().etag).to.equal('etag-123456789012-5');
 
-          const version = template.cache.version!;
+          const version = template.toJSON().version!;
           expect(version.versionNumber).to.equal('86');
           expect(version.updateOrigin).to.equal('ADMIN_SDK_NODE');
           expect(version.updateType).to.equal('INCREMENTAL_UPDATE');
@@ -587,16 +587,16 @@ describe('RemoteConfig', () => {
           expect(version.updateTime).to.equal('Mon, 15 Jun 2020 16:45:03 GMT');
 
           const key = 'holiday_promo_enabled';
-          const p1 = template.cache.parameters[key];
+          const p1 = template.toJSON().parameters[key];
           expect(p1.defaultValue).deep.equals({ value: 'true' });
           expect(p1.conditionalValues).deep.equals({ ios: { useInAppDefault: true } });
 
-          const c = template.cache.conditions.find((c) => c.name === 'ios');
+          const c = template.toJSON().conditions.find((c) => c.name === 'ios');
           expect(c).to.be.not.undefined;
           const cond = c as NamedCondition;
           expect(cond.name).to.equal('ios');
 
-          const parsed = JSON.parse(JSON.stringify(template.cache));
+          const parsed = template.toJSON();
           const expectedTemplate = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE);
           const expectedVersion = deepCopy(VERSION_INFO);
           expectedVersion.updateTime = new Date(expectedVersion.updateTime).toUTCString();
@@ -643,7 +643,10 @@ describe('RemoteConfig', () => {
         }
       };
       const initializedTemplate = remoteConfig.initServerTemplate({ template });
-      const parsed = JSON.parse(JSON.stringify(initializedTemplate.cache));
+      const parsed = initializedTemplate.toJSON();
+      const expectedVersion = deepCopy(VERSION_INFO);
+      expectedVersion.updateTime = new Date(expectedVersion.updateTime).toUTCString();
+      template.version = expectedVersion as Version;
       expect(parsed).deep.equals(deepCopy(template));
     });
 
@@ -660,7 +663,7 @@ describe('RemoteConfig', () => {
       };
       const templateJson = JSON.stringify(template);
       const initializedTemplate = remoteConfig.initServerTemplate({ template: templateJson });
-      const parsed = JSON.parse(JSON.stringify(initializedTemplate.cache));
+      const parsed = initializedTemplate.toJSON();
       const expectedVersion = deepCopy(VERSION_INFO);
       expectedVersion.updateTime = new Date(expectedVersion.updateTime).toUTCString();
       template.version = expectedVersion as Version;
@@ -798,7 +801,7 @@ describe('RemoteConfig', () => {
         return remoteConfig.getServerTemplate()
           .then((template) => {
             // If parameters are not present in the response, we set it to an empty object.
-            expect(template.cache.parameters).deep.equals({});
+            expect(template.toJSON().parameters).deep.equals({});
           });
       });
 
@@ -812,7 +815,7 @@ describe('RemoteConfig', () => {
         return remoteConfig.getServerTemplate()
           .then((template) => {
             // If conditions are not present in the response, we set it to an empty array.
-            expect(template.cache.conditions).deep.equals([]);
+            expect(template.toJSON().conditions).deep.equals([]);
           });
       });
 
@@ -824,11 +827,11 @@ describe('RemoteConfig', () => {
 
         return remoteConfig.getServerTemplate()
           .then((template) => {
-            expect(template.cache.conditions.length).to.equal(1);
-            expect(template.cache.conditions[0].name).to.equal('ios');
-            expect(template.cache.etag).to.equal('etag-123456789012-5');
+            expect(template.toJSON().conditions.length).to.equal(1);
+            expect(template.toJSON().conditions[0].name).to.equal('ios');
+            expect(template.toJSON().etag).to.equal('etag-123456789012-5');
 
-            const version = template.cache.version!;
+            const version = template.toJSON().version!;
             expect(version.versionNumber).to.equal('86');
             expect(version.updateOrigin).to.equal('ADMIN_SDK_NODE');
             expect(version.updateType).to.equal('INCREMENTAL_UPDATE');
@@ -839,11 +842,11 @@ describe('RemoteConfig', () => {
             expect(version.updateTime).to.equal('Mon, 15 Jun 2020 16:45:03 GMT');
 
             const key = 'holiday_promo_enabled';
-            const p1 = template.cache.parameters[key];
+            const p1 = template.toJSON().parameters[key];
             expect(p1.defaultValue).deep.equals({ value: 'true' });
             expect(p1.conditionalValues).deep.equals({ ios: { useInAppDefault: true } });
 
-            const c = template.cache.conditions.find((c) => c.name === 'ios');
+            const c = template.toJSON().conditions.find((c) => c.name === 'ios');
             expect(c).to.be.not.undefined;
             const cond = c as NamedCondition;
             expect(cond.name).to.equal('ios');
@@ -863,7 +866,7 @@ describe('RemoteConfig', () => {
               }
             });
 
-            const parsed = JSON.parse(JSON.stringify(template.cache));
+            const parsed = template.toJSON();
             const expectedTemplate = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE);
             const expectedVersion = deepCopy(VERSION_INFO);
             expectedVersion.updateTime = new Date(expectedVersion.updateTime).toUTCString();
@@ -884,9 +887,9 @@ describe('RemoteConfig', () => {
 
         return remoteConfig.getServerTemplate()
           .then((template) => {
-            expect(template.cache.etag).to.equal('etag-123456789012-5');
+            expect(template.toJSON().etag).to.equal('etag-123456789012-5');
 
-            const version = template.cache.version!;
+            const version = template.toJSON().version!;
             expect(version.versionNumber).to.equal('86');
             expect(version.updateOrigin).to.equal('ADMIN_SDK_NODE');
             expect(version.updateType).to.equal('INCREMENTAL_UPDATE');
@@ -910,9 +913,9 @@ describe('RemoteConfig', () => {
 
         return remoteConfig.getServerTemplate()
           .then((template) => {
-            expect(template.cache.etag).to.equal('etag-123456789012-5');
+            expect(template.toJSON().etag).to.equal('etag-123456789012-5');
 
-            const version = template.cache.version!;
+            const version = template.toJSON().version!;
             expect(version.versionNumber).to.equal('86');
             expect(version.updateOrigin).to.equal('ADMIN_SDK_NODE');
             expect(version.updateType).to.equal('INCREMENTAL_UPDATE');
@@ -936,9 +939,9 @@ describe('RemoteConfig', () => {
 
         return remoteConfig.getServerTemplate()
           .then((template) => {
-            expect(template.cache.etag).to.equal('etag-123456789012-5');
+            expect(template.toJSON().etag).to.equal('etag-123456789012-5');
 
-            const version = template.cache.version!;
+            const version = template.toJSON().version!;
             expect(version.versionNumber).to.equal('86');
             expect(version.updateOrigin).to.equal('ADMIN_SDK_NODE');
             expect(version.updateType).to.equal('INCREMENTAL_UPDATE');
@@ -951,6 +954,89 @@ describe('RemoteConfig', () => {
       });
     });
 
+    describe('set', () => {
+      it('should set template when passed', () => {
+        const template = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData;
+        template.parameters = {
+          dog_type: {
+            defaultValue: {
+              value: 'shiba'
+            },
+            description: 'Type of dog breed',
+            valueType: 'STRING'
+          }
+        };
+        template.version = {
+          ...deepCopy(VERSION_INFO),
+          updateTime: new Date(VERSION_INFO.updateTime).toUTCString()
+        } as Version;
+        const initializedTemplate = remoteConfig.initServerTemplate();
+        initializedTemplate.set(template);
+        const parsed = initializedTemplate.toJSON();
+        expect(parsed).deep.equals(template);
+      });
+
+      it('should set and instantiates template when json string is passed', () => {
+        const template = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData;
+        template.parameters = {
+          dog_type: {
+            defaultValue: {
+              value: 'shiba'
+            },
+            description: 'Type of dog breed',
+            valueType: 'STRING'
+          }
+        };
+        template.version = {
+          ...deepCopy(VERSION_INFO),
+          updateTime: new Date(VERSION_INFO.updateTime).toUTCString()
+        } as Version;
+        const templateJson = JSON.stringify(template);
+        const initializedTemplate = remoteConfig.initServerTemplate();
+        initializedTemplate.set(templateJson);
+        const parsed = initializedTemplate.toJSON();
+        expect(parsed).deep.equals(template);
+      });
+
+      describe('should throw error if there are any JSON or tempalte parsing errors', () => {
+        const INVALID_PARAMETERS: any[] = [null, '', 'abc', 1, true, []];
+        const INVALID_CONDITIONS: any[] = [null, '', 'abc', 1, true, {}];
+  
+        let sourceTemplate = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE);
+        const jsonString = '{invalidJson: null}';
+        it('should throw if template is an invalid JSON', () => {
+          expect(() => remoteConfig.initServerTemplate({ template: jsonString }))
+            .to.throw(/Failed to parse the JSON string: ([\D\w]*)\./);
+        });
+  
+        INVALID_PARAMETERS.forEach((invalidParameter) => {
+          sourceTemplate.parameters = invalidParameter;
+          const jsonString = JSON.stringify(sourceTemplate);
+          it(`should throw if the template is invalid - parameters is ${JSON.stringify(invalidParameter)}`, () => {
+            expect(() => remoteConfig.initServerTemplate({ template: jsonString }))
+              .to.throw('Remote Config parameters must be a non-null object');
+          });
+        });
+  
+        sourceTemplate = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE);
+        INVALID_CONDITIONS.forEach((invalidConditions) => {
+          sourceTemplate.conditions = invalidConditions;
+          const jsonString = JSON.stringify(sourceTemplate);
+          it(`should throw if the template is invalid - conditions is ${JSON.stringify(invalidConditions)}`, () => {
+            expect(() => remoteConfig.initServerTemplate({ template: jsonString }))
+              .to.throw('Remote Config conditions must be an array');
+          });
+        });
+      });
+
+      it('should throw if template is an invalid JSON', () => {
+        const jsonString = '{invalidJson: null}';
+        const initializedTemplate = remoteConfig.initServerTemplate();
+        expect(() => initializedTemplate.set(jsonString))
+          .to.throw(/Failed to parse the JSON string: ([\D\w]*)\./);
+      });
+    });
+
     describe('evaluate', () => {
       it('returns a config when template is present in cache', () => {
         const stub = sinon
@@ -1151,7 +1237,7 @@ describe('RemoteConfig', () => {
           },
         }
 
-        template.cache = response as ServerTemplateData;
+        template.set(response as ServerTemplateData);
 
         let config = template.evaluate();
 
@@ -1165,7 +1251,7 @@ describe('RemoteConfig', () => {
           },
         }
 
-        template.cache = response as ServerTemplateData;
+        template.set(response as ServerTemplateData);
 
         config = template.evaluate();
 

From 2d1649717389c8ae2bda4566eb116e6fa2bbbb34 Mon Sep 17 00:00:00 2001
From: Erik Eldridge <erikeldridge@google.com>
Date: Tue, 9 Apr 2024 13:43:09 -0700
Subject: [PATCH 13/13] Update SSRC to use validator.isString instead of lodash
 for non-dev environment (#2523)

---
 src/remote-config/remote-config.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts
index bd23532013..c529501315 100644
--- a/src/remote-config/remote-config.ts
+++ b/src/remote-config/remote-config.ts
@@ -42,7 +42,6 @@ import {
   InitServerTemplateOptions,
   ServerTemplateDataType,
 } from './remote-config-api';
-import { isString } from 'lodash';
 
 /**
  * The Firebase `RemoteConfig` service interface.
@@ -205,6 +204,7 @@ export class RemoteConfig {
     if (options?.template) {
       template.set(options?.template);
     }
+
     return template;
   }
 }
@@ -328,7 +328,7 @@ class ServerTemplateImpl implements ServerTemplate {
    */
   public set(template: ServerTemplateDataType): void {
     let parsed;
-    if (isString(template)) {
+    if (validator.isString(template)) {
       try {
         parsed = JSON.parse(template);
       } catch (e) {