UNPKG

@freakyfelt/secrets-js

Version:

Client library to make secrets fetching easy and safe

322 lines (318 loc) 8.99 kB
// src/errors.ts var SecretsError = class extends Error { constructor(msg, details) { super(msg); this.details = details; } }; var InvalidSecretError = class extends SecretsError { constructor(msg, arn) { super(msg, { arn }); } }; var SecretParseError = class extends InvalidSecretError { }; var UnsupportedOperationError = class extends InvalidSecretError { }; // src/utils/secret-content.ts function getSecretContent(input) { if (typeof input.SecretString === "string") { if (typeof input.SecretBinary !== "undefined") { throw new InvalidSecretError( "Both SecretString and SecretBinary defined", input.ARN ?? null ); } return { type: "string", SecretString: input.SecretString }; } else if (typeof input.SecretBinary !== "undefined") { return { type: "binary", SecretBinary: input.SecretBinary }; } else { return null; } } // src/utils/secret-copier.ts function deepCopySecretValueCommandOutput(input) { const { CreatedDate, SecretBinary, VersionStages, $metadata, ...rest } = input; const res = { ...rest }; if (CreatedDate instanceof Date) { res["CreatedDate"] = new Date(CreatedDate); } if (typeof SecretBinary !== "undefined") { res["SecretBinary"] = Buffer.from(SecretBinary); } if (Array.isArray(VersionStages)) { res["VersionStages"] = [...VersionStages]; } if (typeof $metadata !== "undefined") { res["$metadata"] = { ...$metadata }; } return res; } // src/secret-value.ts var SecretValue = class { constructor(input) { this.#input = deepCopySecretValueCommandOutput(input); this.#arn = input.ARN ?? null; this.#content = getSecretContent(input); } #input; #arn; #content; /** * The ARN of the secret */ get ARN() { return this.#mustGetInputString("ARN"); } /** * The user-specified friendly name of the secret */ get Name() { return this.#mustGetInputString("Name"); } /** * The AWS-specified VersionId of the secret */ get VersionId() { return this.#mustGetInputString("VersionId"); } /** * The array of version stages associated with the secret * * See also {@link isAWSCurrentVersion} and {@link hasVersionStage} */ get VersionStages() { return [ ...this.#unsafeMustGetInputField( "VersionStages", (value) => Array.isArray(value) ) ]; } get CreatedDate() { const createdDate = this.#unsafeMustGetInputField( "CreatedDate", (value) => value instanceof Date ); return new Date(createdDate); } /** * True if the secret is the current version per AWS (via the AWSCURRENT version stage) */ isAWSCurrentVersion() { return this.hasVersionStage("AWSCURRENT"); } /** * True if the secret is the upcoming/pending version per AWS (via the AWSPENDING version stage) */ isAWSPendingVersion() { return this.hasVersionStage("AWSPENDING"); } /** * True if the secret is the previous/outgoing version per AWS (via the AWSPREVIOUS version stage) */ isAWSPreviousVersion() { return this.hasVersionStage("AWSPREVIOUS"); } /** * Checks if the secret has the specified version stage * * @param stage The stage name to search for * @returns True if the secret has the specified version stage */ hasVersionStage(stage) { return this.#input.VersionStages?.includes(stage) ?? false; } /** * Whether the secret payload was created a string (present as SecretString) or binary (present as SecretBinary) */ get payloadType() { if (this.#content === null) { throw new InvalidSecretError("Invalid content payload", this.#arn); } return this.#content.type; } /** * Returns non-sensitive metadata about the secret * * Current fields: * * - ARN * - Name * - VersionId * - VersionStages * - CreatedDate */ metadata() { const { ARN, CreatedDate, Name, VersionId, VersionStages } = this.#input; return deepCopySecretValueCommandOutput({ ARN, Name, VersionId, VersionStages, CreatedDate }); } /** * Converts the SecretString or SecretBinary content into a Buffer */ async bytes() { if (this.#content === null) { throw new InvalidSecretError("Invalid content payload", this.#arn); } switch (this.#content.type) { case "binary": return Buffer.from(this.#content.SecretBinary); case "string": return Buffer.from(this.#content.SecretString); } } /** * Parses the contents of SecretString as JSON, throwing a SecretParseError on failure to do so * * NOTE: The JSON string is always re-parsed on each request * * @throws {UnsupportedOperationError} the SecretString field is not populated * @throws {SecretParseError} the contents of SecretString is not valid JSON */ async json() { const str = this.#mustGetSecretString(); try { return JSON.parse(str); } catch (err) { throw new SecretParseError("Could not parse secret as JSON", this.#arn); } } /** * Returns the raw response body from the GetSecretValue API call * * NOTE: The API response will usually be a {@link GetSecretCommandOutput} that implements GetSecretValueResponse */ async raw() { return deepCopySecretValueCommandOutput(this.#input); } /** * Returns a string with the contents of `SecretString` */ async text() { return this.#mustGetSecretString(); } /** * Calls {@link #mustGetInputField}, ensures the value is a string, and returns it * * NOTE: Strings are immutable, so we can safely return the instance of the field. * * @param fieldName The name of the field on GetSecretValueResponse to fetch * @returns the string value of the field */ #mustGetInputString(fieldName) { return this.#unsafeMustGetInputField( fieldName, (value) => typeof value === "string" ); } /** * Fetches the value of a field from the input object, ensuring it is of the expected type. * * WARNING: unsafe because this returns the instance of the field. Consumers must create a copy of the value before returning it. * * @param fieldName The name of the field on GetSecretValueResponse to fetch * @param typeVerifier A function that verifies the type of the field value * @returns The value of the field, cast to the expected type */ #unsafeMustGetInputField(fieldName, typeVerifier) { if (!Object.hasOwn(this.#input, fieldName)) { throw new InvalidSecretError( `Missing ${fieldName} in response`, this.#arn ); } const value = this.#input[fieldName]; if (!typeVerifier(value)) { throw new InvalidSecretError( `Invalid ${fieldName} in response`, this.#arn ); } return value; } /** * Fetches the value of the SecretString field or throws UnsupportedOperationError if the secret is binary * * @throws UnsupportedOperationError if the secret is binary */ #mustGetSecretString() { if (this.#content?.type !== "string") { throw new UnsupportedOperationError( "Cannot convert binary secrets to text", this.#arn ); } return this.#content.SecretString; } /** * Custom inspect method to show only the following information in console.log() calls: * * - Name * - VersionId * * The output will also indicate the content type ("string", "binary") in parenthesis */ [Symbol.for("nodejs.util.inspect.custom")](depth, options, inspect) { const contentType = this.#content?.type ?? "unknown"; if (depth <= 0) { return options.stylize(`[SecretValue(${contentType})]`, "special"); } const newOptions = Object.assign({}, options, { depth: options.depth === null ? null : options.depth - 1 }); const { Name, VersionId } = this.#input; const inner = inspect({ Name, VersionId }, newOptions); return `${options.stylize(`SecretValue(${contentType})`, "special")} ${inner}`; } }; // src/fetcher.ts var SecretsFetcher = class { constructor(client) { this.client = client; } /** * Shorthand method for fetching the string representation of the SecretString * * @param input * @returns the resolved secret */ async fetchString(SecretId, opts) { const res = await this.fetch(SecretId, opts); return res.text(); } /** * Shorthand method for fetching the JSON representation of the SecretString */ async fetchJson(SecretId, opts) { const res = await this.fetch(SecretId, opts); return res.json(); } async fetch(SecretId, opts) { const res = await this.client.getSecretValue({ ...opts, SecretId }); return new SecretValue(res); } }; export { InvalidSecretError, SecretParseError, SecretValue, SecretsError, SecretsFetcher, UnsupportedOperationError }; //# sourceMappingURL=index.js.map