/*! firebase-admin v12.1.1 */ "use strict"; /*! * Copyright 2020 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. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.RemoteConfig = void 0; const validator = require("../utils/validator"); const remote_config_api_client_internal_1 = require("./remote-config-api-client-internal"); const condition_evaluator_internal_1 = require("./condition-evaluator-internal"); const value_impl_1 = require("./internal/value-impl"); /** * The Firebase `RemoteConfig` service interface. */ class RemoteConfig { /** * @param app - The app for this RemoteConfig service. * @constructor * @internal */ constructor(app) { this.app = app; this.client = new remote_config_api_client_internal_1.RemoteConfigApiClient(app); } /** * Gets the current active version of the {@link RemoteConfigTemplate} of the project. * * @returns A promise that fulfills with a `RemoteConfigTemplate`. */ getTemplate() { return this.client.getTemplate() .then((templateResponse) => { return new RemoteConfigTemplateImpl(templateResponse); }); } /** * Gets the requested version of the {@link RemoteConfigTemplate} of the project. * * @param versionNumber - Version number of the Remote Config template to look up. * * @returns A promise that fulfills with a `RemoteConfigTemplate`. */ getTemplateAtVersion(versionNumber) { return this.client.getTemplateAtVersion(versionNumber) .then((templateResponse) => { return new RemoteConfigTemplateImpl(templateResponse); }); } /** * Validates a {@link RemoteConfigTemplate}. * * @param template - The Remote Config template to be validated. * @returns A promise that fulfills with the validated `RemoteConfigTemplate`. */ validateTemplate(template) { return this.client.validateTemplate(template) .then((templateResponse) => { return new RemoteConfigTemplateImpl(templateResponse); }); } /** * Publishes a Remote Config template. * * @param template - The Remote Config template to be published. * @param options - Optional options object when publishing a Remote Config template: * - `force`: Setting this to `true` forces the Remote Config template to * be updated and circumvent the ETag. This approach is not recommended * because it risks causing the loss of updates to your Remote Config * template if multiple clients are updating the Remote Config template. * See {@link https://firebase.google.com/docs/remote-config/use-config-rest#etag_usage_and_forced_updates | * ETag usage and forced updates}. * * @returns A Promise that fulfills with the published `RemoteConfigTemplate`. */ publishTemplate(template, options) { return this.client.publishTemplate(template, options) .then((templateResponse) => { return new RemoteConfigTemplateImpl(templateResponse); }); } /** * Rolls back a project's published Remote Config template to the specified version. * A rollback is equivalent to getting a previously published Remote Config * template and re-publishing it using a force update. * * @param versionNumber - The version number of the Remote Config template to roll back to. * The specified version number must be lower than the current version number, and not have * been deleted due to staleness. Only the last 300 versions are stored. * All versions that correspond to non-active Remote Config templates (that is, all except the * template that is being fetched by clients) are also deleted if they are more than 90 days old. * @returns A promise that fulfills with the published `RemoteConfigTemplate`. */ rollback(versionNumber) { return this.client.rollback(versionNumber) .then((templateResponse) => { return new RemoteConfigTemplateImpl(templateResponse); }); } /** * Gets a list of Remote Config template versions that have been published, sorted in reverse * chronological order. Only the last 300 versions are stored. * All versions that correspond to non-active Remote Config templates (i.e., all except the * template that is being fetched by clients) are also deleted if they are older than 90 days. * * @param options - Optional options object for getting a list of versions. * @returns A promise that fulfills with a `ListVersionsResult`. */ listVersions(options) { return this.client.listVersions(options) .then((listVersionsResponse) => { return { versions: listVersionsResponse.versions?.map(version => new VersionImpl(version)) ?? [], nextPageToken: listVersionsResponse.nextPageToken, }; }); } /** * Creates and returns a new Remote Config template from a JSON string. * * @param json - The JSON string to populate a Remote Config template. * * @returns A new template instance. */ createTemplateFromJSON(json) { if (!validator.isNonEmptyString(json)) { throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('invalid-argument', 'JSON string must be a valid non-empty string'); } let template; try { template = JSON.parse(json); } catch (e) { throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('invalid-argument', `Failed to parse the JSON string: ${json}. ` + e); } return new RemoteConfigTemplateImpl(template); } /** * Instantiates {@link ServerTemplate} and then fetches and caches the latest * template version of the project. */ async getServerTemplate(options) { const template = this.initServerTemplate(options); await template.load(); return template; } /** * Synchronously instantiates {@link ServerTemplate}. */ initServerTemplate(options) { const template = new ServerTemplateImpl(this.client, new condition_evaluator_internal_1.ConditionEvaluator(), options?.defaultConfig); if (options?.template) { template.set(options?.template); } return template; } } exports.RemoteConfig = RemoteConfig; /** * Remote Config template internal implementation. */ class RemoteConfigTemplateImpl { constructor(config) { if (!validator.isNonNullObject(config) || !validator.isNonEmptyString(config.etag)) { throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('invalid-argument', `Invalid Remote Config template: ${JSON.stringify(config)}`); } this.etagInternal = config.etag; if (typeof config.parameters !== 'undefined') { if (!validator.isNonNullObject(config.parameters)) { throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('invalid-argument', 'Remote Config parameters must be a non-null object'); } this.parameters = config.parameters; } else { this.parameters = {}; } if (typeof config.parameterGroups !== 'undefined') { if (!validator.isNonNullObject(config.parameterGroups)) { throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('invalid-argument', 'Remote Config parameter groups must be a non-null object'); } this.parameterGroups = config.parameterGroups; } else { this.parameterGroups = {}; } if (typeof config.conditions !== 'undefined') { if (!validator.isArray(config.conditions)) { throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('invalid-argument', 'Remote Config conditions must be an array'); } this.conditions = config.conditions; } else { this.conditions = []; } if (typeof config.version !== 'undefined') { this.version = new VersionImpl(config.version); } } /** * Gets the ETag of the template. * * @returns The ETag of the Remote Config template. */ get etag() { return this.etagInternal; } /** * Returns a JSON-serializable representation of this object. * * @returns A JSON-serializable representation of this object. */ toJSON() { return { conditions: this.conditions, parameters: this.parameters, parameterGroups: this.parameterGroups, etag: this.etag, version: this.version, }; } } /** * Remote Config dataplane template data implementation. */ class ServerTemplateImpl { constructor(apiClient, conditionEvaluator, defaultConfig = {}) { this.apiClient = apiClient; this.conditionEvaluator = conditionEvaluator; this.defaultConfig = defaultConfig; this.stringifiedDefaultConfig = {}; // 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}. */ load() { return this.apiClient.getServerTemplate() .then((template) => { this.cache = new ServerTemplateDataImpl(template); }); } /** * Parses a {@link ServerTemplateDataType} and caches it. */ set(template) { let parsed; if (validator.isString(template)) { try { parsed = JSON.parse(template); } catch (e) { // Transforms JSON parse errors to Firebase error. throw new remote_config_api_client_internal_1.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}. */ evaluate(context = {}) { 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 remote_config_api_client_internal_1.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 configValues = {}; // Initializes config Value objects with default values. for (const key in this.stringifiedDefaultConfig) { configValues[key] = new value_impl_1.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 } = parameter; // Supports parameters with no conditional values. const normalizedConditionalValues = conditionalValues || {}; let parameterValueWrapper = 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.useInAppDefault) { // TODO: add logging once we have a wrapped logger. continue; } if (parameterValueWrapper) { const parameterValue = parameterValueWrapper.value; configValues[key] = new value_impl_1.ValueImpl('remote', parameterValue); continue; } if (!defaultValue) { // TODO: add logging once we have a wrapped logger. continue; } if (defaultValue.useInAppDefault) { // TODO: add logging once we have a wrapped logger. continue; } const parameterDefaultValue = defaultValue.value; configValues[key] = new value_impl_1.ValueImpl('remote', parameterDefaultValue); } return new ServerConfigImpl(configValues); } /** * @returns JSON representation of the server template */ toJSON() { return this.cache; } } class ServerConfigImpl { constructor(configValues) { this.configValues = configValues; } getBoolean(key) { return this.getValue(key).asBoolean(); } getNumber(key) { return this.getValue(key).asNumber(); } getString(key) { return this.getValue(key).asString(); } getValue(key) { return this.configValues[key] || new value_impl_1.ValueImpl('static'); } } /** * Remote Config dataplane template data implementation. */ class ServerTemplateDataImpl { constructor(template) { if (!validator.isNonNullObject(template) || !validator.isNonEmptyString(template.etag)) { throw new remote_config_api_client_internal_1.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 remote_config_api_client_internal_1.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 remote_config_api_client_internal_1.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. */ class VersionImpl { constructor(version) { if (!validator.isNonNullObject(version)) { throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('invalid-argument', `Invalid Remote Config version instance: ${JSON.stringify(version)}`); } if (typeof version.versionNumber !== 'undefined') { if (!validator.isNonEmptyString(version.versionNumber) && !validator.isNumber(version.versionNumber)) { throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('invalid-argument', 'Version number must be a non-empty string in int64 format or a number'); } if (!Number.isInteger(Number(version.versionNumber))) { throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('invalid-argument', 'Version number must be an integer or a string in int64 format'); } this.versionNumber = version.versionNumber; } if (typeof version.updateOrigin !== 'undefined') { if (!validator.isNonEmptyString(version.updateOrigin)) { throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('invalid-argument', 'Version update origin must be a non-empty string'); } this.updateOrigin = version.updateOrigin; } if (typeof version.updateType !== 'undefined') { if (!validator.isNonEmptyString(version.updateType)) { throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('invalid-argument', 'Version update type must be a non-empty string'); } this.updateType = version.updateType; } if (typeof version.updateUser !== 'undefined') { if (!validator.isNonNullObject(version.updateUser)) { throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('invalid-argument', 'Version update user must be a non-null object'); } this.updateUser = version.updateUser; } if (typeof version.description !== 'undefined') { if (!validator.isNonEmptyString(version.description)) { throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('invalid-argument', 'Version description must be a non-empty string'); } this.description = version.description; } if (typeof version.rollbackSource !== 'undefined') { if (!validator.isNonEmptyString(version.rollbackSource)) { throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('invalid-argument', 'Version rollback source must be a non-empty string'); } this.rollbackSource = version.rollbackSource; } if (typeof version.isLegacy !== 'undefined') { if (!validator.isBoolean(version.isLegacy)) { throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('invalid-argument', 'Version.isLegacy must be a boolean'); } this.isLegacy = version.isLegacy; } // The backend API provides timestamps in ISO date strings. The Admin SDK exposes timestamps // in UTC date strings. If a developer uses a previously obtained template with UTC timestamps // we could still validate it below. if (typeof version.updateTime !== 'undefined') { if (!this.isValidTimestamp(version.updateTime)) { throw new remote_config_api_client_internal_1.FirebaseRemoteConfigError('invalid-argument', 'Version update time must be a valid date string'); } this.updateTime = new Date(version.updateTime).toUTCString(); } } /** * @returns A JSON-serializable representation of this object. */ toJSON() { return { versionNumber: this.versionNumber, updateOrigin: this.updateOrigin, updateType: this.updateType, updateUser: this.updateUser, description: this.description, rollbackSource: this.rollbackSource, isLegacy: this.isLegacy, updateTime: this.updateTime, }; } isValidTimestamp(timestamp) { // This validation fails for timestamps earlier than January 1, 1970 and considers strings // such as "1.2" as valid timestamps. return validator.isNonEmptyString(timestamp) && (new Date(timestamp)).getTime() > 0; } }