UNPKG

@salesforce/core

Version:

Core libraries to interact with SFDX projects, orgs, and APIs.

354 lines 12.7 kB
"use strict"; /* * Copyright (c) 2020, salesforce.com, inc. * All rights reserved. * Licensed under the BSD 3-Clause license. * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ Object.defineProperty(exports, "__esModule", { value: true }); exports.BaseConfigStore = void 0; const kit_1 = require("@salesforce/kit"); const ts_types_1 = require("@salesforce/ts-types"); const ts_types_2 = require("@salesforce/ts-types"); const crypto_1 = require("../crypto/crypto"); const sfError_1 = require("../sfError"); const time_1 = require("../util/time"); const lwwMap_1 = require("./lwwMap"); /** * An abstract class that implements all the config management functions but * none of the storage functions. * * **Note:** To see the interface, look in typescripts autocomplete help or the npm package's ConfigStore.d.ts file. */ class BaseConfigStore extends kit_1.AsyncOptionalCreatable { // If encryptedKeys is an array of RegExps, they should not contain the /g (global) or /y (sticky) flags to avoid stateful issues. static encryptedKeys = []; options; crypto; // Initialized in setContents contents = new lwwMap_1.LWWMap(); statics = this.constructor; /** * Constructor. * * @param options The options for the class instance. * @ignore */ constructor(options) { super(options); this.options = options ?? {}; this.setContents(this.initialContents()); } /** * Returns an array of {@link ConfigEntry} for each element in the config. */ entries() { return (0, ts_types_2.definiteEntriesOf)(this.contents.value ?? {}); } // NEXT_RELEASE: update types to specify return can be | undefined get(key, decrypt = false) { const rawValue = this.contents.get(key); if (this.hasEncryption() && decrypt) { if ((0, ts_types_2.isJsonMap)(rawValue)) { return this.recursiveDecrypt(structuredClone(rawValue), key); } else if (this.isCryptoKey(key)) { return this.decrypt(rawValue); } } // NEXT_RELEASE: update types to specify return can be | undefined return rawValue; } /** * Returns the list of keys that contain a value. * * @param value The value to filter keys on. */ getKeysByValue(value) { const matchedEntries = this.entries().filter((entry) => entry[1] === value); // Only return the keys return matchedEntries.map((entry) => entry[0]); } /** * Returns a boolean asserting whether a value has been associated to the key in the config object or not. * */ has(key) { return this.contents.has(key) ?? false; } /** * Returns an array that contains the keys for each element in the config object. */ keys() { return Object.keys(this.contents.value ?? {}); } /** * Sets the value for the key in the config object. This will override the existing value. * To do a partial update, use {@link BaseConfigStore.update}. * * @param key The key. * @param value The value. */ set(key, value) { let resolvedValue = value; if (this.hasEncryption()) { if ((0, ts_types_2.isJsonMap)(resolvedValue)) { resolvedValue = this.recursiveEncrypt(resolvedValue, key); } else if (this.isCryptoKey(key)) { resolvedValue = this.encrypt(resolvedValue); } } // set(key, undefined) means unset if (resolvedValue === undefined) { this.unset(key); } else { this.contents.set(key, resolvedValue); } } /** * Updates the value for the key in the config object. If the value is an object, it * will be merged with the existing object. * * @param key The key. * @param value The value. */ update(key, value) { const existingValue = this.get(key, true); if ((0, ts_types_1.isPlainObject)(existingValue) && (0, ts_types_1.isPlainObject)(value)) { const mergedValue = Object.assign({}, existingValue, value); this.set(key, mergedValue); } else { this.set(key, value); } } /** * Returns `true` if an element in the config object existed and has been removed, or `false` if the element does not * exist. {@link BaseConfigStore.has} will return false afterwards. * * @param key The key */ unset(key) { if (this.has(key)) { this.contents.delete(key); return true; } return false; } /** * Returns `true` if all elements in the config object existed and have been removed, or `false` if all the elements * do not exist (some may have been removed). {@link BaseConfigStore.has(key)} will return false afterwards. * * @param keys The keys */ unsetAll(keys) { return keys.map((key) => this.unset(key)).every(Boolean); } /** * Removes all key/value pairs from the config object. */ clear() { this.keys().map((key) => this.unset(key)); } /** * Returns an array that contains the values for each element in the config object. */ values() { return (0, ts_types_2.definiteValuesOf)(this.contents.value ?? {}); } /** * Returns the entire config contents. * * *NOTE:* Data will still be encrypted unless decrypt is passed in. A clone of * the data will be returned to prevent storing un-encrypted data in memory and * potentially saving to the file system. * * @param decrypt: decrypt all data in the config. A clone of the data will be returned. * */ getContents(decrypt = false) { if (this.hasEncryption() && decrypt) { return this.recursiveDecrypt(structuredClone(this.contents?.value ?? {})); } return this.contents?.value ?? {}; } /** * Invokes `actionFn` once for each key-value pair present in the config object. * * @param {function} actionFn The function `(key: string, value: ConfigValue) => void` to be called for each element. */ forEach(actionFn) { this.entries().map((entry) => actionFn(entry[0], entry[1])); } /** * Convert the config object to a JSON object. Returns the config contents. * Same as calling {@link ConfigStore.getContents} */ toObject() { return this.contents.value ?? {}; } /** * Convert an object to a {@link ConfigContents} and set it as the config contents. * * @param obj The object. */ setContentsFromObject(obj) { const objForWrite = this.hasEncryption() ? this.recursiveEncrypt(obj) : obj; (0, ts_types_1.entriesOf)(objForWrite).map(([key, value]) => { this.set(key, value); }); } /** * Keep ConfigFile concurrency-friendly. * Avoid using this unless you're reading the file for the first time * and guaranteed to no be cross-saving existing contents * */ setContentsFromFileContents(contents, timestamp) { const state = (0, lwwMap_1.stateFromContents)(contents, timestamp ?? (0, time_1.nowBigInt)()); this.contents = new lwwMap_1.LWWMap(state); } /** * Sets the entire config contents. * * @param contents The contents. */ setContents(contents = {}) { const maybeEncryptedContents = this.hasEncryption() ? this.recursiveEncrypt(contents) : contents; (0, ts_types_1.entriesOf)(maybeEncryptedContents).map(([key, value]) => { this.contents.set(key, value); }); } getEncryptedKeys() { return [...(this.options?.encryptedKeys ?? []), ...(this.statics?.encryptedKeys ?? [])]; } /** * This config file has encrypted keys and it should attempt to encrypt them. * * @returns Has encrypted keys */ hasEncryption() { return this.getEncryptedKeys().length > 0; } // eslint-disable-next-line class-methods-use-this initialContents() { return {}; } /** * Used to initialize asynchronous components. */ async init() { if (this.hasEncryption()) { await this.initCrypto(); } } /** * Initialize the crypto dependency. */ async initCrypto() { if (!this.crypto) { this.crypto = await crypto_1.Crypto.create(); } } /** * Closes the crypto dependency. Crypto should be close after it's used and no longer needed. */ // eslint-disable-next-line @typescript-eslint/require-await async clearCrypto() { if (this.crypto) { this.crypto.close(); delete this.crypto; } } /** * Should the given key be encrypted on set methods and decrypted on get methods. * * @param key The key. Supports query key like `a.b[0]`. * @returns Should encrypt/decrypt */ isCryptoKey(key) { function resolveProperty() { // Handle query keys const dotAccessor = /\.([a-zA-Z0-9@._-]+)$/; const singleQuoteAccessor = /\['([a-zA-Z0-9@._-]+)'\]$/; const doubleQuoteAccessor = /\["([a-zA-Z0-9@._-]+)"\]$/; const matcher = dotAccessor.exec(key) ?? singleQuoteAccessor.exec(key) ?? doubleQuoteAccessor.exec(key); return matcher ? matcher[1] : key; } // Any keys named the following should be encrypted/decrypted return (this.statics.encryptedKeys || []).find((keyOrExp) => { const property = resolveProperty(); if (keyOrExp instanceof RegExp) { return keyOrExp.test(property); } else { return keyOrExp === property; } }); } encrypt(value) { if (!value) return; if (!this.crypto) throw new sfError_1.SfError('crypto is not initialized', 'CryptoNotInitializedError'); if (!(0, ts_types_2.isString)(value)) throw new sfError_1.SfError(`can only encrypt strings but found: ${typeof value} : ${JSON.stringify(value)}`, 'InvalidCryptoValueError'); return this.crypto.isEncrypted(value) ? value : this.crypto.encrypt(value); } decrypt(value) { if (!value) return; if (!this.crypto) throw new sfError_1.SfError('crypto is not initialized', 'CryptoNotInitializedError'); if (!(0, ts_types_2.isString)(value)) throw new sfError_1.SfError(`can only encrypt strings but found: ${typeof value} : ${JSON.stringify(value)}`, 'InvalidCryptoValueError'); return this.crypto.isEncrypted(value) ? this.crypto.decrypt(value) : value; } /** * Encrypt all values in a nested JsonMap. * * @param keyPaths: The complete path of the (nested) data * @param data: The current (nested) data being worked on. */ recursiveEncrypt(data, parentKey) { for (const key of Object.keys(data)) { this.recursiveCrypto(this.encrypt.bind(this), [...(parentKey ? [parentKey] : []), key], data); } return data; } /** * Decrypt all values in a nested JsonMap. * * @param keyPaths: The complete path of the (nested) data * @param data: The current (nested) data being worked on. */ recursiveDecrypt(data, parentKey) { for (const key of Object.keys(data)) { this.recursiveCrypto(this.decrypt.bind(this), [...(parentKey ? [parentKey] : []), key], data); } return data; } /** * Encrypt/Decrypt all values in a nested JsonMap. * * @param keyPaths: The complete path of the (nested) data * @param data: The current (nested) data being worked on. */ recursiveCrypto(method, keyPaths, data) { const key = keyPaths.pop(); const value = data[key]; if ((0, ts_types_2.isJsonMap)(value)) { for (const newKey of Object.keys(value)) { this.recursiveCrypto(method, [...keyPaths, key, newKey], value); } } else if (this.isCryptoKey(key)) { // I think this side effect is intentional // eslint-disable-next-line no-param-reassign data[key] = method(value); } } } exports.BaseConfigStore = BaseConfigStore; //# sourceMappingURL=configStore.js.map