UNPKG

firebase-admin

Version:
231 lines (230 loc) 12.2 kB
/*! firebase-admin v13.2.0 */ /*! * 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'; Object.defineProperty(exports, "__esModule", { value: true }); exports.ConditionEvaluator = void 0; const remote_config_api_1 = require("./remote-config-api"); const crypto_1 = require("crypto"); /** * Encapsulates condition evaluation logic to simplify organization and * facilitate testing. * * @internal */ class ConditionEvaluator { evaluateConditions(namedConditions, context) { // 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; } evaluateCondition(condition, context, nestingLevel = 0) { 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); } if (condition.customSignal) { return this.evaluateCustomSignalCondition(condition.customSignal, context); } // TODO: add logging once we have a wrapped logger. return false; } evaluateOrCondition(orCondition, context, nestingLevel) { 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; } evaluateAndCondition(andCondition, context, nestingLevel) { 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; } evaluatePercentCondition(percentCondition, context) { 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 hash = ConditionEvaluator.hashSeededRandomizationId(stringToHash); const instanceMicroPercentile = hash % BigInt(100 * 1000000); switch (percentOperator) { case remote_config_api_1.PercentConditionOperator.LESS_OR_EQUAL: return instanceMicroPercentile <= normalizedMicroPercent; case remote_config_api_1.PercentConditionOperator.GREATER_THAN: return instanceMicroPercentile > normalizedMicroPercent; case remote_config_api_1.PercentConditionOperator.BETWEEN: return instanceMicroPercentile > normalizedMicroPercentLowerBound && instanceMicroPercentile <= normalizedMicroPercentUpperBound; case remote_config_api_1.PercentConditionOperator.UNKNOWN: default: break; } // TODO: add logging once we have a wrapped logger. return false; } static hashSeededRandomizationId(seededRandomizationId) { const hex = (0, crypto_1.createHash)('sha256').update(seededRandomizationId).digest('hex'); return BigInt(`0x${hex}`); } evaluateCustomSignalCondition(customSignalCondition, context) { const { customSignalOperator, customSignalKey, targetCustomSignalValues, } = customSignalCondition; if (!customSignalOperator || !customSignalKey || !targetCustomSignalValues) { // TODO: add logging once we have a wrapped logger. return false; } if (!targetCustomSignalValues.length) { return false; } // Extract the value of the signal from the evaluation context. const actualCustomSignalValue = context[customSignalKey]; if (actualCustomSignalValue == undefined) { return false; } switch (customSignalOperator) { case remote_config_api_1.CustomSignalOperator.STRING_CONTAINS: return compareStrings(targetCustomSignalValues, actualCustomSignalValue, (target, actual) => actual.includes(target)); case remote_config_api_1.CustomSignalOperator.STRING_DOES_NOT_CONTAIN: return !compareStrings(targetCustomSignalValues, actualCustomSignalValue, (target, actual) => actual.includes(target)); case remote_config_api_1.CustomSignalOperator.STRING_EXACTLY_MATCHES: return compareStrings(targetCustomSignalValues, actualCustomSignalValue, (target, actual) => actual.trim() === target.trim()); case remote_config_api_1.CustomSignalOperator.STRING_CONTAINS_REGEX: return compareStrings(targetCustomSignalValues, actualCustomSignalValue, (target, actual) => new RegExp(target).test(actual)); // For numeric operators only one target value is allowed. case remote_config_api_1.CustomSignalOperator.NUMERIC_LESS_THAN: return compareNumbers(actualCustomSignalValue, targetCustomSignalValues[0], (r) => r < 0); case remote_config_api_1.CustomSignalOperator.NUMERIC_LESS_EQUAL: return compareNumbers(actualCustomSignalValue, targetCustomSignalValues[0], (r) => r <= 0); case remote_config_api_1.CustomSignalOperator.NUMERIC_EQUAL: return compareNumbers(actualCustomSignalValue, targetCustomSignalValues[0], (r) => r === 0); case remote_config_api_1.CustomSignalOperator.NUMERIC_NOT_EQUAL: return compareNumbers(actualCustomSignalValue, targetCustomSignalValues[0], (r) => r !== 0); case remote_config_api_1.CustomSignalOperator.NUMERIC_GREATER_THAN: return compareNumbers(actualCustomSignalValue, targetCustomSignalValues[0], (r) => r > 0); case remote_config_api_1.CustomSignalOperator.NUMERIC_GREATER_EQUAL: return compareNumbers(actualCustomSignalValue, targetCustomSignalValues[0], (r) => r >= 0); // For semantic operators only one target value is allowed. case remote_config_api_1.CustomSignalOperator.SEMANTIC_VERSION_LESS_THAN: return compareSemanticVersions(actualCustomSignalValue, targetCustomSignalValues[0], (r) => r < 0); case remote_config_api_1.CustomSignalOperator.SEMANTIC_VERSION_LESS_EQUAL: return compareSemanticVersions(actualCustomSignalValue, targetCustomSignalValues[0], (r) => r <= 0); case remote_config_api_1.CustomSignalOperator.SEMANTIC_VERSION_EQUAL: return compareSemanticVersions(actualCustomSignalValue, targetCustomSignalValues[0], (r) => r === 0); case remote_config_api_1.CustomSignalOperator.SEMANTIC_VERSION_NOT_EQUAL: return compareSemanticVersions(actualCustomSignalValue, targetCustomSignalValues[0], (r) => r !== 0); case remote_config_api_1.CustomSignalOperator.SEMANTIC_VERSION_GREATER_THAN: return compareSemanticVersions(actualCustomSignalValue, targetCustomSignalValues[0], (r) => r > 0); case remote_config_api_1.CustomSignalOperator.SEMANTIC_VERSION_GREATER_EQUAL: return compareSemanticVersions(actualCustomSignalValue, targetCustomSignalValues[0], (r) => r >= 0); } // TODO: add logging once we have a wrapped logger. return false; } } exports.ConditionEvaluator = ConditionEvaluator; ConditionEvaluator.MAX_CONDITION_RECURSION_DEPTH = 10; // Compares the actual string value of a signal against a list of target // values. If any of the target values are a match, returns true. function compareStrings(targetValues, actualValue, predicateFn) { const actual = String(actualValue); return targetValues.some((target) => predicateFn(target, actual)); } // Compares two numbers against each other. // Calls the predicate function with -1, 0, 1 if actual is less than, equal to, or greater than target. function compareNumbers(actualValue, targetValue, predicateFn) { const target = Number(targetValue); const actual = Number(actualValue); if (isNaN(target) || isNaN(actual)) { return false; } return predicateFn(actual < target ? -1 : actual > target ? 1 : 0); } // Max number of segments a numeric version can have. This is enforced by the server as well. const MAX_LENGTH = 5; // Compares semantic version strings against each other. // Calls the predicate function with -1, 0, 1 if actual is less than, equal to, or greater than target. function compareSemanticVersions(actualValue, targetValue, predicateFn) { const version1 = String(actualValue).split('.').map(Number); const version2 = targetValue.split('.').map(Number); if (version1.length > MAX_LENGTH || version2.length > MAX_LENGTH) { return false; } for (let i = 0; i < MAX_LENGTH; i++) { // Check to see if segments are present. Note that these may be present and be NaN. const version1HasSegment = version1[i] !== undefined; const version2HasSegment = version2[i] !== undefined; // Insert zeros if undefined for easier comparison. if (!version1HasSegment) version1[i] = 0; if (!version2HasSegment) version2[i] = 0; // At this point, if either segment is NaN, we return false directly. if (isNaN(version1[i]) || isNaN(version2[i])) return false; // Check if we have a difference in segments. Otherwise continue to next segment. if (version1[i] < version2[i]) return predicateFn(-1); if (version1[i] > version2[i]) return predicateFn(1); } // If this point is reached, the semantic versions are equal. return predicateFn(0); }