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) {