@freakyfelt/secrets-js
Version:
Client library to make secrets fetching easy and safe
322 lines (318 loc) • 8.99 kB
JavaScript
// 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