@controlplane/cli
Version:
Control Plane Corporation CLI
164 lines • 7.28 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.SecretResource = void 0;
const resolver_1 = require("../commands/resolver");
const links_1 = require("../util/links");
// ANCHOR - Constants
// The types whose `encoding` entry describes the other fields instead of holding
// a value of its own; a dictionary may carry a real entry of that name.
const ENCODING_IS_METADATA = new Set(['opaque', 'userpass']);
// ANCHOR - SecretResource
/**
* One secret in the org, revealed on demand with the caller's own credentials.
* A revealed copy is reused only while the secret's version is unchanged, so a
* rotation behind a long-lived instance is never served stale.
*/
class SecretResource {
constructor(name, session, client) {
this.revealed = null;
this.name = name;
this.link = (0, resolver_1.kindResolver)('secret').resourceLink(name, session.context);
this.client = client;
}
// Public Static Methods //
/**
* The secret a link names: cpln://secret/<name>, //secret/<name>, or
* /org/<org>/secret/<name>. A .<field> selector after the name is value()'s
* to read; this names the secret itself.
*
* @param {string} link - The link as the user wrote it.
* @param {Session} session - The session the secret resolves against.
* @param {RestClient} client - The authenticated API client.
* @returns {SecretResource} The secret the link names.
*/
static fromLink(link, session, client) {
const parsed = (0, links_1.parseLink)(link);
// A secret is org-scoped, so a gvc-scoped path cannot name one
if (parsed === null || parsed.kind !== 'secret' || parsed.gvc !== '') {
throw new Error(`ERROR: '${link}' is not a valid secret link.`);
}
// A self link naming another org cannot resolve against this session
if (parsed.org !== '' && parsed.org !== session.context.org) {
throw new Error(`ERROR: '${link}' names org "${parsed.org}", but this session runs against org "${session.context.org}".`);
}
return new SecretResource(parsed.name, session, client);
}
// Public Methods //
/**
* Reads the secret without revealing its data.
*
* @returns {Promise<Secret>} The secret, data excluded.
*/
async get() {
return this.client.get(this.link);
}
/**
* Reveals the secret. A revealed copy is reused only while the secret's
* version is unchanged; a failed request carries the request error as thrown,
* and the CLI's error rendering owns its presentation.
*
* @returns {Promise<Secret>} The revealed secret.
*/
async reveal() {
// The plain read answers whether the revealed copy is still current
if (this.revealed !== null) {
const current = await this.get();
if (current.version === this.revealed.version) {
return this.revealed;
}
}
this.revealed = await this.client.get(`${this.link}/-reveal`);
return this.revealed;
}
/**
* Reveals the value the secret holds: the whole payload for an opaque or
* single-document secret, or one field of a structured one.
*
* @param {string} field - (Optional) The field to select; empty selects the whole payload.
* @returns {Promise<string>} The value, decoded when the secret declares base64 encoding.
*/
async getValueFromKey(field = '') {
const secret = await this.reveal();
return this.selectValue(secret, field);
}
// Private Methods //
/**
* Reads the value a field selects out of the revealed secret, the way a
* workload env value resolves one.
*
* @param {Secret} secret - The revealed secret.
* @param {string} field - The selected field, empty for the whole payload.
* @returns {string} The selected value.
*/
selectValue(secret, field) {
if (secret.data === undefined) {
throw new Error(`ERROR: secret "${this.link}" revealed no value.`);
}
// The types whose whole payload is one document (docker, gcp, azure-sdk) reveal a string.
if (typeof secret.data === 'string') {
if (field !== '') {
throw new Error(`ERROR: secret "${this.link}" holds a single ${secret.type} value, so reference it without a field.`);
}
return secret.data;
}
const fields = secret.data;
const revealable = this.revealableFields(secret.type, fields);
if (field !== '') {
const value = revealable.includes(field) ? fields[field] : undefined;
if (typeof value !== 'string') {
throw new Error(`ERROR: secret "${this.link}" has no field "${field}" (fields: ${revealable.join(', ') || 'none'}).`);
}
return this.decode(value, this.declaredEncoding(secret.type, fields));
}
if (secret.type === 'opaque') {
const opaque = secret.data;
if (opaque.payload === undefined) {
throw new Error(`ERROR: secret "${this.link}" revealed no value.`);
}
return this.decode(opaque.payload, opaque.encoding);
}
throw new Error(`ERROR: secret "${this.name}" is a ${secret.type} secret, so name the field you need: ` +
`cpln://secret/${this.name}.<field> (fields: ${revealable.join(', ') || 'none'}).`);
}
/**
* Names the fields a reference may select: the string-valued entries, minus
* `encoding` for the types that declare it as metadata.
*
* @param {string | undefined} type - The secret's type.
* @param {Record<string, unknown>} fields - The revealed data entries.
* @returns {string[]} The selectable field names.
*/
revealableFields(type, fields) {
const names = Object.keys(fields).filter((field) => typeof fields[field] === 'string');
if (!ENCODING_IS_METADATA.has(type !== null && type !== void 0 ? type : '')) {
return names;
}
return names.filter((field) => field !== 'encoding');
}
/**
* Reads the encoding a secret declares, for the types that define one.
*
* @param {string | undefined} type - The secret's type.
* @param {Record<string, unknown>} fields - The revealed data entries.
* @returns {string | undefined} The declared encoding, or undefined when the type has none.
*/
declaredEncoding(type, fields) {
if (!ENCODING_IS_METADATA.has(type !== null && type !== void 0 ? type : '')) {
return undefined;
}
const encoding = fields['encoding'];
return typeof encoding === 'string' ? encoding : undefined;
}
/**
* Decodes a revealed value when the secret declares base64 encoding.
*
* @param {string} value - The stored value.
* @param {string | undefined} encoding - The secret's declared encoding.
* @returns {string} The decoded value.
*/
decode(value, encoding) {
return encoding === 'base64' ? Buffer.from(value, 'base64').toString('utf-8') : value;
}
}
exports.SecretResource = SecretResource;
//# sourceMappingURL=secret.js.map