UNPKG

@controlplane/cli

Version:

Control Plane Corporation CLI

164 lines 7.28 kB
"use strict"; 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