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)),
);
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;
}
// -- --