import { Either, getRequiredEnvVars, getStdout, getStdoutMany, type IEither, type ITraceable, type LogMetricTraceSupplier, Metric, TraceUtil, } from '@emprespresso/pengueno'; import { randomUUID } from 'node:crypto'; import { mkdirSync } from 'node:fs'; import path from 'node:path'; import { InfisicalSDK } from '@infisical/sdk'; // -- -- export interface SecretItem { name: string; } export interface LoginItem extends SecretItem { login: { username: string; password: string; }; } export interface SecureNote extends SecretItem { notes: string; } export interface IVault { unlock: (client: TClient) => Promise>; lock: (client: TClient, key: TKey) => Promise>; fetchSecret: (client: TClient, key: TKey, item: TItemId) => Promise>; } // -- -- // -- -- type TClient = ITraceable; export type BitwardenKey = { BW_SESSION: string; BITWARDENCLI_APPDATA_DIR: string; }; type TItemId = string; export class Bitwarden implements IVault { constructor(private readonly config: BitwardenConfig) {} public unlock(client: TClient) { return client .move(this.config) .flatMap(TraceUtil.withMetricTrace(Bitwarden.loginMetric)) .map((tConfig) => Either.fromFailable< Error, { config: BitwardenConfig; key: Pick } >(() => { const sessionPath = path.join(this.config.sessionBaseDirectory, randomUUID()); mkdirSync(sessionPath, { recursive: true }); return { config: tConfig.get(), key: { BITWARDENCLI_APPDATA_DIR: sessionPath } }; }), ) .map((tEitherConfig) => tEitherConfig .get() .flatMapAsync(({ config: { server }, key }) => getStdoutMany( tEitherConfig.move([ `bw config server ${server}`, `bw login --apikey --quiet`, `bw unlock --passwordenv BW_PASSWORD --raw`, ]), { env: key }, ).then((res) => res.mapRight((out) => ({ ...key, BW_SESSION: out.at(-1)! }))), ), ) .peek(TraceUtil.promiseify(TraceUtil.traceResultingEither(Bitwarden.loginMetric))) .get(); } public fetchSecret( client: TClient, key: BitwardenKey, item: string, ): Promise> { return client .move(key) .flatMap(TraceUtil.withMetricTrace(Bitwarden.fetchSecretMetric)) .peek((tSession) => tSession.trace.trace(`looking for your secret ${item} (⑅˘꒳˘)`)) .flatMap((tSession) => tSession.move(`bw list items --search ${item}`).map((listCmd) => getStdout(listCmd, { env: key })), ) .map( TraceUtil.promiseify((tEitherItemsJson) => tEitherItemsJson .get() .flatMap( (itemsJson): IEither> => Either.fromFailable(() => JSON.parse(itemsJson)), ) .flatMap((itemsList): IEither => { const secret = itemsList.find(({ name }) => name === item); if (!secret) { return Either.left(new Error(`couldn't find the item ${item} (。•́︿•̀。)`)); } return Either.right(secret); }), ), ) .flatMapAsync(TraceUtil.promiseify(TraceUtil.traceResultingEither(Bitwarden.fetchSecretMetric))) .get(); } public lock(client: TClient, key: BitwardenKey) { return client .move(key) .flatMap(TraceUtil.withMetricTrace(Bitwarden.lockVaultMetric)) .peek((tSession) => tSession.trace.trace(`taking care of locking the vault :3`)) .flatMap((tSession) => tSession.move('bw lock && bw logout').map((lockCmd) => getStdout(lockCmd, { env: key })), ) .peek(TraceUtil.promiseify(TraceUtil.traceResultingEither(Bitwarden.lockVaultMetric))) .peek( TraceUtil.promiseify((tEitherWithLocked) => tEitherWithLocked .get() .mapRight(() => tEitherWithLocked.trace.trace('all locked up and secure now~ (。•̀ᴗ-)✧')), ), ) .map(TraceUtil.promiseify((e) => e.get().mapRight(() => key))) .get(); } public static getConfigFromEnvironment(sessionBaseDirectory = '/tmp/secret'): IEither { return getRequiredEnvVars(['BW_SERVER', 'BW_CLIENTSECRET', 'BW_CLIENTID', 'BW_PASSWORD']).mapRight( ({ BW_SERVER, BW_CLIENTSECRET, BW_CLIENTID }) => ({ sessionBaseDirectory, clientId: BW_CLIENTID, secret: BW_CLIENTSECRET, server: BW_SERVER, }), ); } private static loginMetric = Metric.fromName('Bitwarden.login').asResult(); private static unlockVaultMetric = Metric.fromName('Bitwarden.unlockVault').asResult(); private static fetchSecretMetric = Metric.fromName('Bitwarden.fetchSecret').asResult(); private static lockVaultMetric = Metric.fromName('Bitwarden.lock').asResult(); } export interface BitwardenConfig { sessionBaseDirectory: string; server: string; secret: string; clientId: string; } // -- -- // -- -- export type InfisicalKey = { client: InfisicalSDK; }; export class Infisical implements IVault { constructor(private readonly config: InfisicalConfig) {} public async unlock(client: TClient): Promise> { return client .move(this.config) .flatMap(TraceUtil.withMetricTrace(Infisical.loginMetric)) .peek((tConfig) => tConfig.trace.trace('initializing infisical client (⑅˘꒳˘)')) .map(async (tConfig) => { try { const sdk = new InfisicalSDK({ siteUrl: tConfig.get().siteUrl, }); const authenticatedClient = await sdk.auth().universalAuth.login({ clientId: tConfig.get().clientId, clientSecret: tConfig.get().clientSecret, }); return Either.right({ client: authenticatedClient }); } catch (error) { return Either.left( error instanceof Error ? error : new Error(`failed to authenticate: ${error}`), ); } }) .flatMapAsync(TraceUtil.promiseify(TraceUtil.traceResultingEither(Infisical.loginMetric))) .get(); } public async fetchSecret( client: TClient, key: InfisicalKey, item: string, ): Promise> { return client .move(key) .flatMap(TraceUtil.withMetricTrace(Infisical.fetchSecretMetric)) .peek((tSession) => tSession.trace.trace(`looking for your secret ${item} (⑅˘꒳˘)`)) .map(async (tSession) => { const eitherSecret = await Either.fromFailableAsync(() => tSession.get().client.secrets().getSecret({ secretName: item, projectId: this.config.projectId, environment: this.config.environment, }), ); return eitherSecret.flatMapAsync(async (secret) => { if (!secret || !secret.secretValue) { return Either.left(new Error(`couldn't find the secret ${item} (。•́︿•̀。)`)); } const parsedSecret = await Either.fromFailableAsync(() => Promise.resolve().then(() => JSON.parse(secret.secretValue)), ); parsedSecret.fold( () => tSession.trace.trace(`secret ${item} is plain text, using as notes`), () => tSession.trace.trace(`secret ${item} parsed as JSON successfully (◕ᴗ◕✿)`), ); const secretItem = parsedSecret.fold( () => ({ name: secret.secretKey, notes: secret.secretValue, }), (parsed: any) => ({ name: secret.secretKey, ...parsed, }), ); return Either.right(secretItem as unknown as T); }); }) .flatMapAsync(TraceUtil.promiseify(TraceUtil.traceResultingEither(Infisical.fetchSecretMetric))) .get(); } public async lock(client: TClient, key: InfisicalKey): Promise> { return client .move(key) .flatMap(TraceUtil.withMetricTrace(Infisical.lockVaultMetric)) .peek((tSession) => tSession.trace.trace('closing infisical client :3')) .map(async (tSession) => { try { // Infisical SDK doesn't require explicit logout, but we can cleanup here if needed return Either.right(tSession.get()); } catch (error) { return Either.left( error instanceof Error ? error : new Error(`failed to close client: ${error}`), ); } }) .peek(TraceUtil.promiseify(TraceUtil.traceResultingEither(Infisical.lockVaultMetric))) .peek( TraceUtil.promiseify((tEitherWithLocked) => tEitherWithLocked .get() .mapRight(() => tEitherWithLocked.trace.trace('all cleaned up now~ (。•̀ᴗ-)✧')), ), ) .map(TraceUtil.promiseify((e) => e.get().mapRight(() => key))) .get(); } public static getConfigFromEnvironment(): IEither { return getRequiredEnvVars([ 'INFISICAL_CLIENT_ID', 'INFISICAL_CLIENT_SECRET', 'INFISICAL_PROJECT_ID', 'INFISICAL_ENVIRONMENT', 'INFISICAL_SITE_URL', ]).mapRight( ({ INFISICAL_CLIENT_ID, INFISICAL_CLIENT_SECRET, INFISICAL_PROJECT_ID, INFISICAL_ENVIRONMENT, INFISICAL_SITE_URL, }) => ({ clientId: INFISICAL_CLIENT_ID, clientSecret: INFISICAL_CLIENT_SECRET, projectId: INFISICAL_PROJECT_ID, environment: INFISICAL_ENVIRONMENT, siteUrl: INFISICAL_SITE_URL, }), ); } private static loginMetric = Metric.fromName('Infisical.login').asResult(); private static fetchSecretMetric = Metric.fromName('Infisical.fetchSecret').asResult(); private static lockVaultMetric = Metric.fromName('Infisical.lock').asResult(); } export interface InfisicalConfig { clientId: string; clientSecret: string; projectId: string; environment: string; siteUrl: string; } // -- --