From 98f5c21aa65bbbca01a186a754249335b4afef57 Mon Sep 17 00:00:00 2001 From: Elizabeth Date: Mon, 2 Jun 2025 16:52:52 -0700 Subject: fixup the Either monad a bit for type safetyp --- deno.json | 8 +- server/ci.ts | 4 +- server/job.ts | 325 +++++++++++++++++++----------------- u/fn/either.ts | 183 +++++++++++--------- u/process/argv.ts | 13 +- u/server/activity/health.ts | 12 +- u/server/response.ts | 4 +- worker/executor.ts | 6 +- worker/jobs/ci_pipeline.run | 6 +- worker/scripts/build_docker_image | 16 +- worker/secret.ts | 343 ++++++++++++++++++++------------------ 11 files changed, 491 insertions(+), 429 deletions(-) diff --git a/deno.json b/deno.json index adeae7b..4a34a73 100644 --- a/deno.json +++ b/deno.json @@ -1,4 +1,10 @@ { "package": "@emprespresso/ci", - "workspace": ["./*"] + "workspace": ["./*"], + "tasks": { + "test": "cd u/tests && deno test --allow-env --allow-read --allow-write", + "test:trace": "cd u/tests && deno test --allow-env --allow-read --allow-write trace_test.ts", + "test:either": "cd u/tests && deno test --allow-env --allow-read --allow-write either_test.ts", + "test:coverage": "cd u/tests && deno test --coverage=coverage --allow-env --allow-read --allow-write" + } } diff --git a/server/ci.ts b/server/ci.ts index e1a9ca7..f8d4a17 100644 --- a/server/ci.ts +++ b/server/ci.ts @@ -23,8 +23,8 @@ export class CiHookServer { constructor( healthCheck: HealthChecker = _healthCheck, jobQueuer: IJobQueuer> = new LaminarJobQueuer( - getRequiredEnv("LAMINAR_URL").fold((err, val) => - err ? "https://ci.liz.coffee" : val!, + getRequiredEnv("LAMINAR_URL").fold(({ isLeft, value }) => + isLeft ? "https://ci.liz.coffee" : value, ), ), private readonly healthCheckActivity: IHealthCheckActivity = new HealthCheckActivityImpl( diff --git a/server/job.ts b/server/job.ts index 62582d6..4e12b45 100644 --- a/server/job.ts +++ b/server/job.ts @@ -1,21 +1,21 @@ import { - getStdout, - type Mapper, - memoize, - Either, - ErrorSource, - type IActivity, - type IEither, - type ITraceable, - jsonModel, - JsonResponse, - LogLevel, - Metric, - PenguenoError, - type PenguenoRequest, - type ServerTrace, - TraceUtil, - validateExecutionEntries, + getStdout, + type Mapper, + memoize, + Either, + ErrorSource, + type IActivity, + type IEither, + type ITraceable, + jsonModel, + JsonResponse, + LogLevel, + Metric, + PenguenoError, + type PenguenoRequest, + type ServerTrace, + TraceUtil, + validateExecutionEntries, } from "@emprespresso/pengueno"; import { isJob, type Job } from "@emprespresso/ci_model"; @@ -23,93 +23,98 @@ import { isJob, type Job } from "@emprespresso/ci_model"; const wellFormedJobMetric = Metric.fromName("Job.WellFormed"); const jobJsonTransformer = ( - j: ITraceable, + j: ITraceable, ): IEither => - j - .bimap(TraceUtil.withMetricTrace(wellFormedJobMetric)) - .map((tJson) => { - if (!isJob(tJson) || !validateExecutionEntries(tJson)) { - const err = "seems like a pwetty mawfomed job \\(-.-)/"; - tJson.trace.addTrace(LogLevel.WARN).trace(err); - return Either.left(new PenguenoError(err, 400)); - } - return Either.right(tJson); - }) - .peek((tJob) => - tJob.trace.trace( - tJob - .get() - .fold((err) => - err ? wellFormedJobMetric.failure : wellFormedJobMetric.success, - ), - ), - ) - .get(); + j + .bimap(TraceUtil.withMetricTrace(wellFormedJobMetric)) + .map((tJson) => { + if (!isJob(tJson) || !validateExecutionEntries(tJson)) { + const err = "seems like a pwetty mawfomed job \\(-.-)/"; + tJson.trace.addTrace(LogLevel.WARN).trace(err); + return Either.left( + new PenguenoError(err, 400), + ); + } + return Either.right(tJson); + }) + .peek((tJob) => + tJob.trace.trace( + tJob + .get() + .fold(({ isLeft }) => + isLeft + ? wellFormedJobMetric.failure + : wellFormedJobMetric.success, + ), + ), + ) + .get(); export interface IJobHookActivity { - processHook: IActivity; + processHook: IActivity; } const jobHookRequestMetric = Metric.fromName("JobHook.process"); export class JobHookActivityImpl implements IJobHookActivity { - constructor( - private readonly queuer: IJobQueuer>, - ) {} + constructor( + private readonly queuer: IJobQueuer>, + ) {} - private trace(r: ITraceable) { - return r - .bimap(TraceUtil.withClassTrace(this)) - .bimap(TraceUtil.withMetricTrace(jobHookRequestMetric)); - } + private trace(r: ITraceable) { + return r + .bimap(TraceUtil.withClassTrace(this)) + .bimap(TraceUtil.withMetricTrace(jobHookRequestMetric)); + } - public processHook(r: ITraceable) { - return this.trace(r) - .map(jsonModel(jobJsonTransformer)) - .map(async (tEitherJobJson) => { - const eitherJob = await tEitherJobJson.get(); - return eitherJob.flatMapAsync(async (job) => { - const eitherQueued = await tEitherJobJson - .move(job) - .map(this.queuer.queue) + public processHook(r: ITraceable) { + return this.trace(r) + .map(jsonModel(jobJsonTransformer)) + .map(async (tEitherJobJson) => { + const eitherJob = await tEitherJobJson.get(); + return eitherJob.flatMapAsync(async (job) => { + const eitherQueued = await tEitherJobJson + .move(job) + .map(this.queuer.queue) + .get(); + return eitherQueued.mapLeft( + (e) => new PenguenoError(e.message, 500), + ); + }); + }) + .peek( + TraceUtil.promiseify((tJob) => + tJob.get().fold(({ isRight, value }) => { + if (isRight) { + tJob.trace.trace(jobHookRequestMetric.success); + tJob.trace.trace( + `all queued up and weady to go :D !! ${value}`, + ); + return; + } + + tJob.trace.trace( + value.source === ErrorSource.SYSTEM + ? jobHookRequestMetric.failure + : jobHookRequestMetric.warn, + ); + tJob.trace.addTrace(value.source).trace(`${value}`); + }), + ), + ) + .map( + TraceUtil.promiseify( + (tEitherQueuedJob) => + new JsonResponse(r, tEitherQueuedJob.get(), { + status: tEitherQueuedJob + .get() + .fold(({ isRight, value }) => + isRight ? 200 : value.status, + ), + }), + ), + ) .get(); - return eitherQueued.mapLeft((e) => new PenguenoError(e.message, 500)); - }); - }) - .peek( - TraceUtil.promiseify((tJob) => - tJob - .get() - .fold( - (err: PenguenoError | undefined, _val: string | undefined) => { - if (!err) { - tJob.trace.trace(jobHookRequestMetric.success); - tJob.trace.trace( - `all queued up and weady to go :D !! ${_val}`, - ); - return; - } - tJob.trace.trace( - err.source === ErrorSource.SYSTEM - ? jobHookRequestMetric.failure - : jobHookRequestMetric.warn, - ); - tJob.trace.addTrace(err.source).trace(`${err}`); - }, - ), - ), - ) - .map( - TraceUtil.promiseify( - (tEitherQueuedJob) => - new JsonResponse(r, tEitherQueuedJob.get(), { - status: tEitherQueuedJob - .get() - .fold((err, _val) => (_val ? 200 : err?.status ?? 500)), - }), - ), - ) - .get(); - } + } } // -- -- @@ -118,76 +123,82 @@ export class JobHookActivityImpl implements IJobHookActivity { type QueuePosition = string; export class QueueError extends Error {} export interface IJobQueuer { - queue: Mapper>>; + queue: Mapper>>; } export class LaminarJobQueuer - implements IJobQueuer> + implements IJobQueuer> { - constructor(private readonly queuePositionPrefix: string) {} - - private static GetJobTypeTrace = (jobType: string) => - `LaminarJobQueue.Queue.${jobType}`; - private static JobTypeMetrics = memoize((jobType: string) => - Metric.fromName(LaminarJobQueuer.GetJobTypeTrace(jobType)), - ); + constructor(private readonly queuePositionPrefix: string) {} - public queue(j: ITraceable) { - const { type: jobType } = j.get(); - const trace = LaminarJobQueuer.GetJobTypeTrace(jobType); - const metric = LaminarJobQueuer.JobTypeMetrics(trace); + private static GetJobTypeTrace = (jobType: string) => + `LaminarJobQueue.Queue.${jobType}`; + private static JobTypeMetrics = memoize((jobType: string) => + Metric.fromName(LaminarJobQueuer.GetJobTypeTrace(jobType)), + ); - return j - .bimap(TraceUtil.withTrace(trace)) - .bimap(TraceUtil.withMetricTrace(metric)) - .map((j) => { - const { type: jobType, arguments: args } = j.get(); - const laminarCommand = [ - "laminarc", - "queue", - jobType, - ...Object.entries(args).map(([key, val]) => `"${key}"="${val}"`), - ]; - return laminarCommand; - }) - .peek((c) => - c.trace.trace( - `im so excited to see how this queue job will end!! (>ᴗ<): ${c - .get() - .toString()}`, - ), - ) - .map(getStdout) - .peek( - TraceUtil.promiseify((q) => - q.trace.trace( - q - .get() - .fold((err, _val) => (err ? metric.failure : metric.success)), - ), - ), - ) - .map( - TraceUtil.promiseify((q) => - q - .get() - .mapRight((stdout) => { - q.trace.addTrace(LogLevel.DEBUG).trace(`stdout ${stdout}`); - const [jobName, jobId] = stdout.split(":"); - const jobUrl = `${this.queuePositionPrefix}/jobs/${jobName}/${jobId}`; + public queue(j: ITraceable) { + const { type: jobType } = j.get(); + const trace = LaminarJobQueuer.GetJobTypeTrace(jobType); + const metric = LaminarJobQueuer.JobTypeMetrics(trace); - q.trace.trace( - `all queued up and weady to go~ (˘ω˘) => ${jobUrl}`, - ); - return jobUrl; + return j + .bimap(TraceUtil.withTrace(trace)) + .bimap(TraceUtil.withMetricTrace(metric)) + .map((j) => { + const { type: jobType, arguments: args } = j.get(); + const laminarCommand = [ + "laminarc", + "queue", + jobType, + ...Object.entries(args).map( + ([key, val]) => `"${key}"="${val}"`, + ), + ]; + return laminarCommand; }) - .mapLeft((err) => { - q.trace.addTrace(LogLevel.ERROR).trace(err.toString()); - return err; - }), - ), - ) - .get(); - } + .peek((c) => + c.trace.trace( + `im so excited to see how this queue job will end!! (>ᴗ<): ${c + .get() + .toString()}`, + ), + ) + .map(getStdout) + .peek( + TraceUtil.promiseify((q) => + q.trace.trace( + q + .get() + .fold(({ isLeft }) => + isLeft ? metric.failure : metric.success, + ), + ), + ), + ) + .map( + TraceUtil.promiseify((q) => + q.get().fold(({ isLeft, value }) => { + if (isLeft) { + q.trace + .addTrace(LogLevel.ERROR) + .trace(value.toString()); + return Either.left(value); + } + q.trace + .addTrace(LogLevel.DEBUG) + .trace(`stdout ${value}`); + const [jobName, jobId] = value.split(":"); + const jobUrl = `${this.queuePositionPrefix}/jobs/${jobName}/${jobId}`; + + q.trace.trace( + `all queued up and weady to go~ (˘ω˘) => ${jobUrl}`, + ); + return Either.right(jobUrl); + }), + ), + ) + .get(); + } } // -- -- diff --git a/u/fn/either.ts b/u/fn/either.ts index b228af2..124557c 100644 --- a/u/fn/either.ts +++ b/u/fn/either.ts @@ -1,97 +1,118 @@ -import type { BiMapper, Mapper, Supplier } from "@emprespresso/pengueno"; -import { isObject } from "../leftpadesque/mod.ts"; +import { type Mapper, type Supplier, isObject } from "@emprespresso/pengueno"; type IEitherTag = "IEither"; const iEitherTag: IEitherTag = "IEither"; +export interface _Either { + readonly isLeft: LeftT; + readonly isRight: RightT; + readonly value: T; +} +export type Left = _Either; +export type Right = _Either; + export interface IEither { - readonly _tag: IEitherTag; - mapBoth: ( - errBranch: Mapper, - okBranch: Mapper, - ) => IEither; - fold: (folder: (err: E | undefined, val: T | undefined) => Tt) => Tt; //BiMapper) => Tt;; - moveRight: (t: Tt) => IEither; - mapRight: (mapper: Mapper) => IEither; - mapLeft: (mapper: Mapper) => IEither; - flatMap: (mapper: Mapper>) => IEither; - flatMapAsync: ( - mapper: Mapper>>, - ) => Promise>; + readonly _tag: IEitherTag; + + mapBoth: <_E, _T>( + errBranch: Mapper, + okBranch: Mapper, + ) => IEither<_E, _T>; + fold: <_T>(folder: Mapper | Right, _T>) => _T; + moveRight: <_T>(t: _T) => IEither; + mapRight: <_T>(mapper: Mapper) => IEither; + mapLeft: <_E>(mapper: Mapper) => IEither<_E, T>; + flatMap: <_T>(mapper: Mapper>) => IEither; + flatMapAsync: <_T>( + mapper: Mapper>>, + ) => Promise>; } export class Either implements IEither { - private constructor( - private readonly err?: E, - private readonly ok?: T, - public readonly _tag: IEitherTag = iEitherTag, - ) {} - - public moveRight(t: Tt) { - return this.mapRight(() => t); - } - - public fold(folder: BiMapper): R { - return folder(this.err ?? undefined, this.ok ?? undefined); - } - - public mapBoth( - errBranch: Mapper, - okBranch: Mapper, - ): Either { - if (this.err !== undefined) return Either.left(errBranch(this.err)); - return Either.right(okBranch(this.ok!)); - } - - public flatMap(mapper: Mapper>): Either { - if (this.ok !== undefined) return mapper(this.ok); - return Either.left(this.err!); - } - - public mapRight(mapper: Mapper): IEither { - if (this.ok !== undefined) return Either.right(mapper(this.ok)); - return Either.left(this.err!); - } - - public mapLeft(mapper: Mapper): IEither { - if (this.err !== undefined) return Either.left(mapper(this.err)); - return Either.right(this.ok!); - } - - public async flatMapAsync( - mapper: Mapper>>, - ): Promise> { - if (this.err !== undefined) { - return Promise.resolve(Either.left(this.err)); + private readonly self: Left | Right; + + private constructor( + err?: E, + ok?: T, + public readonly _tag: IEitherTag = iEitherTag, + ) { + this.self = | Right>{ + isLeft: typeof err !== "undefined", + isRight: typeof ok !== "undefined", + value: typeof err !== "undefined" ? err : ok!, + }; + } + + public moveRight<_T>(t: _T) { + return this.mapRight(() => t); + } + + public fold<_T>(folder: Mapper | Right, _T>): _T { + return folder(this.self); + } + + public mapBoth<_E, _T>( + errBranch: Mapper, + okBranch: Mapper, + ): IEither<_E, _T> { + if (this.self.isLeft) return Either.left(errBranch(this.self.value)); + return Either.right(okBranch(this.self.value)); + } + + public flatMap<_T>(mapper: Mapper>): IEither { + if (this.self.isRight) return mapper(this.self.value); + return Either.left(this.self.value); + } + + public mapRight<_T>(mapper: Mapper): IEither { + if (this.self.isRight) + return Either.right(mapper(this.self.value)); + return Either.left(this.self.value); } - return await mapper(this.ok!).catch((err) => Either.left(err as E)); - } - - static left(e: E) { - return new Either(e); - } - - static right(t: T) { - return new Either(undefined, t); - } - - static fromFailable(s: Supplier) { - try { - return Either.right(s()); - } catch (e) { - return Either.left(e as E); + + public mapLeft<_E>(mapper: Mapper): IEither<_E, T> { + if (this.self.isLeft) + return Either.left<_E, T>(mapper(this.self.value)); + return Either.right<_E, T>(this.self.value); + } + + public async flatMapAsync<_T>( + mapper: Mapper>>, + ): Promise> { + if (this.self.isLeft) { + return Promise.resolve(Either.left(this.self.value)); + } + return await mapper(this.self.value).catch((err) => + Either.left(err), + ); + } + + static left(e: E): IEither { + return new Either(e, undefined); + } + static right(t: T): IEither { + return new Either(undefined, t); + } + + static fromFailable(s: Supplier): IEither { + try { + return Either.right(s()); + } catch (e) { + return Either.left(e as E); + } } - } - static async fromFailableAsync(s: Promise) { - try { - return Either.right(await s); - } catch (e) { - return Either.left(e as E); + static async fromFailableAsync( + s: Promise, + ): Promise> { + try { + return Either.right(await s); + } catch (e) { + return Either.left(e as E); + } } - } } export const isEither = (o: unknown): o is IEither => { - return isObject(o) && "_tag" in o && o._tag === "IEither"; + return isObject(o) && "_tag" in o && o._tag === "IEither"; }; diff --git a/u/process/argv.ts b/u/process/argv.ts index 657c9a7..7190531 100644 --- a/u/process/argv.ts +++ b/u/process/argv.ts @@ -37,14 +37,15 @@ export const argv = ( .map((arg) => [arg, getArg(arg, argv)] as [K, IEither]) .map(([arg, specified]): [K, IEither] => [ arg, - specified.fold((e, val) => { - const hasDefaultVal = e && defaultArgs && arg in defaultArgs; + specified.fold(({ isLeft, isRight, value}): IEither => { + if (isRight) { + return Either.right(value); + } + const hasDefaultVal = isLeft && defaultArgs && arg in defaultArgs; if (hasDefaultVal) { - return Either.right(defaultArgs[arg]!); - } else if (!val || e) { - return Either.left(e ?? new Error("unknown")); + return Either.right(defaultArgs[arg]!); } - return Either.right(val); + return Either.left(value); }), ]) .reduce( diff --git a/u/server/activity/health.ts b/u/server/activity/health.ts index 83be399..b9dedf9 100644 --- a/u/server/activity/health.ts +++ b/u/server/activity/health.ts @@ -38,10 +38,10 @@ export class HealthCheckActivityImpl implements IHealthCheckActivity { .flatMap((r) => r.move(HealthCheckInput.CHECK).map(this.check)) .peek( TraceUtil.promiseify((h) => - h.get().fold((err) => { - if (err) { + h.get().fold(({ isLeft, value }) => { + if (isLeft) { h.trace.trace(healthCheckMetric.failure); - h.trace.addTrace(LogLevel.ERROR).trace(`${err}`); + h.trace.addTrace(LogLevel.ERROR).trace(`${value}`); return; } h.trace.trace(healthCheckMetric.success); @@ -57,9 +57,9 @@ export class HealthCheckActivityImpl implements IHealthCheckActivity { () => "think im healthy!! (✿˘◡˘) ready to do work~", ) .fold( - (errMsg, okMsg) => - new JsonResponse(req, errMsg ?? okMsg, { - status: errMsg ? 500 : 200, + ({ isLeft, value: message }) => + new JsonResponse(req, message, { + status: isLeft ? 500 : 200, }), ), ), diff --git a/u/server/response.ts b/u/server/response.ts index 9022fed..4531157 100644 --- a/u/server/response.ts +++ b/u/server/response.ts @@ -67,7 +67,7 @@ export class JsonResponse extends PenguenoResponse { super( req, JSON.stringify( - e.fold((err, ok) => (err ? { error: err! } : { ok: ok! })), + e.fold(({ isLeft, value }) => (isLeft ? { error: value } : { ok: value })), ), optsWithJsonContentType, ); @@ -76,7 +76,7 @@ export class JsonResponse extends PenguenoResponse { super( req, JSON.stringify( - Math.floor(opts.status / 100) < 4 ? { ok: e } : { error: e }, + Math.floor(opts.status / 100) > 4 ? { error: e } : { ok: e }, ), optsWithJsonContentType, ); diff --git a/worker/executor.ts b/worker/executor.ts index faa40a6..ea580eb 100644 --- a/worker/executor.ts +++ b/worker/executor.ts @@ -36,8 +36,8 @@ export const executeJob = (tJob: ITraceable) => q .get() .fold( - (err, _val) => - jobTypeMetric(tJob.get().type)[err ? "failure" : "success"], + ({ isLeft }) => + jobTypeMetric(tJob.get().type)[isLeft ? "failure" : "success"], ), ), ), @@ -86,7 +86,7 @@ export const executePipeline = ( .get(), ), ); - const failures = jobResults.filter((e) => e.fold((err) => !!err)); + const failures = jobResults.filter((e) => e.fold(( { isLeft }) => isLeft)); if (failures.length > 0) { tJobs.trace.trace(pipelinesMetric.failure); return Either.left(new Error(failures.toString())); diff --git a/worker/jobs/ci_pipeline.run b/worker/jobs/ci_pipeline.run index 434850c..03d9d6d 100644 --- a/worker/jobs/ci_pipeline.run +++ b/worker/jobs/ci_pipeline.run @@ -123,9 +123,9 @@ await LogMetricTraceable.from(eitherJob).bimap(TraceUtil.withTrace(trace)) ) .get() .then((e) => - e.flatMap(() => eitherJob).fold((err, val) => { - if (!val || err) throw err; - return Deno.remove(getWorkingDirectoryForCiJob(val), { recursive: true }); + e.flatMap(() => eitherJob).fold(({isLeft, isRight, value}) => { + if (isLeft || !isRight) throw value; + return Deno.remove(getWorkingDirectoryForCiJob(value), { recursive: true }); }) ); diff --git a/worker/scripts/build_docker_image b/worker/scripts/build_docker_image index dc0e961..2e19111 100755 --- a/worker/scripts/build_docker_image +++ b/worker/scripts/build_docker_image @@ -38,7 +38,7 @@ await LogMetricTraceable.from(eitherJob) .bimap( (tEitherJob) => { const trace = "build_docker_image." + - tEitherJob.get().fold((_, v) => v?.arguments.buildTarget ?? ""); + tEitherJob.get().fold(({ isRight, value }) => isRight ? value.arguments.buildTarget : ""); return [tEitherJob.get(), trace]; }, ) @@ -83,8 +83,8 @@ await LogMetricTraceable.from(eitherJob) .peek(async (tEitherWithAuthdRegistry) => { const eitherWithAuthdRegistry = await tEitherWithAuthdRegistry.get(); return tEitherWithAuthdRegistry.trace.trace( - eitherWithAuthdRegistry.fold((err, _val) => - loginMetric[err ? "failure" : "success"] + eitherWithAuthdRegistry.fold(({ isLeft}) => + loginMetric[isLeft ? "failure" : "success"] ), ); }) @@ -112,17 +112,17 @@ await LogMetricTraceable.from(eitherJob) }) .peek(async (tEitherWithBuiltImage) => { const eitherWithBuiltImage = await tEitherWithBuiltImage.get(); - eitherWithBuiltImage.fold((err, val) => { + eitherWithBuiltImage.fold(({ isLeft, value}) => { tEitherWithBuiltImage.trace.trace( - buildImageMetric[err ? "failure" : "success"], + buildImageMetric[isLeft ? "failure" : "success"], ); - if (!val || err) { + if (isLeft) { tEitherWithBuiltImage.trace.addTrace(LogLevel.ERROR).trace( - `oh nyoo we couldn't buiwd the img :(( ${err}`, + `oh nyoo we couldn't buiwd the img :(( ${value}`, ); return; } - tEitherWithBuiltImage.trace.addTrace("buildOutput").trace(val.buildOutput); + tEitherWithBuiltImage.trace.addTrace("buildOutput").trace(value.buildOutput); }); }) .map(async (tEitherWithBuiltImage) => { diff --git a/worker/secret.ts b/worker/secret.ts index e0a4c5d..f5ae93f 100644 --- a/worker/secret.ts +++ b/worker/secret.ts @@ -1,36 +1,36 @@ import { - Either, - getRequiredEnvVars, - getStdout, - type IEither, - type ITraceable, - type LogMetricTraceSupplier, - Metric, - TraceUtil, + Either, + getRequiredEnvVars, + getStdout, + type IEither, + type ITraceable, + type LogMetricTraceSupplier, + Metric, + TraceUtil, } from "@emprespresso/pengueno"; // -- -- export interface LoginItem { - login: { - username: string; - password: string; - }; + login: { + username: string; + password: string; + }; } export interface SecureNote { - notes: string; + notes: string; } export type SecretItem = LoginItem | SecureNote; export interface IVault { - unlock: (client: TClient) => Promise>; - lock: (client: TClient, key: TKey) => Promise>; + unlock: (client: TClient) => Promise>; + lock: (client: TClient, key: TKey) => Promise>; - fetchSecret: ( - client: TClient, - key: TKey, - item: TItemId, - ) => Promise>; + fetchSecret: ( + client: TClient, + key: TKey, + item: TItemId, + ) => Promise>; } // -- -- @@ -39,155 +39,178 @@ type TClient = ITraceable; type TKey = string; type TItemId = string; export class Bitwarden implements IVault { - constructor(private readonly config: BitwardenConfig) {} + constructor(private readonly config: BitwardenConfig) {} - public unlock(client: TClient) { - return client - .move(this.config) - .bimap(TraceUtil.withMetricTrace(Bitwarden.loginMetric)) - .flatMap((tConfig) => - tConfig.move(`bw config server ${tConfig.get().server}`).map(getStdout), - ) - .map(async (tEitherWithConfig) => { - const eitherWithConfig = await tEitherWithConfig.get(); - tEitherWithConfig.trace.trace("logging in~ ^.^"); - return eitherWithConfig.flatMapAsync((_) => - tEitherWithConfig - .move("bw login --apikey --quiet") - .map(getStdout) - .get(), - ); - }) - .peek(async (tEitherWithAuthd) => { - const eitherWithAuthd = await tEitherWithAuthd.get(); - return tEitherWithAuthd.trace.trace( - eitherWithAuthd.fold( - (err, _val) => Bitwarden.loginMetric[err ? "failure" : "success"], - ), - ); - }) - .map(async (tEitherWithAuthd) => { - const eitherWithAuthd = await tEitherWithAuthd.get(); - tEitherWithAuthd.trace.trace("unlocking the secret vault~ (◕ᴗ◕✿)"); - return eitherWithAuthd.flatMapAsync((_) => - tEitherWithAuthd - .move("bw unlock --passwordenv BW_PASSWORD --raw") - .map(getStdout) - .get(), - ); - }) - .peek(async (tEitherWithSession) => { - const eitherWithAuthd = await tEitherWithSession.get(); - return tEitherWithSession.trace.trace( - eitherWithAuthd.fold( - (err, _val) => - Bitwarden.unlockVaultMetric[err ? "failure" : "success"], - ), - ); - }) - .get(); - } + public unlock(client: TClient) { + return client + .move(this.config) + .bimap(TraceUtil.withMetricTrace(Bitwarden.loginMetric)) + .flatMap((tConfig) => + tConfig + .move(`bw config server ${tConfig.get().server}`) + .map(getStdout), + ) + .map(async (tEitherWithConfig) => { + const eitherWithConfig = await tEitherWithConfig.get(); + tEitherWithConfig.trace.trace("logging in~ ^.^"); + return eitherWithConfig.flatMapAsync((_) => + tEitherWithConfig + .move("bw login --apikey --quiet") + .map(getStdout) + .get(), + ); + }) + .peek(async (tEitherWithAuthd) => { + const eitherWithAuthd = await tEitherWithAuthd.get(); + return tEitherWithAuthd.trace.trace( + eitherWithAuthd.fold( + ({ isLeft }) => + Bitwarden.loginMetric[ + isLeft ? "failure" : "success" + ], + ), + ); + }) + .map(async (tEitherWithAuthd) => { + const eitherWithAuthd = await tEitherWithAuthd.get(); + tEitherWithAuthd.trace.trace( + "unlocking the secret vault~ (◕ᴗ◕✿)", + ); + return eitherWithAuthd.flatMapAsync((_) => + tEitherWithAuthd + .move("bw unlock --passwordenv BW_PASSWORD --raw") + .map(getStdout) + .get(), + ); + }) + .peek(async (tEitherWithSession) => { + const eitherWithAuthd = await tEitherWithSession.get(); + return tEitherWithSession.trace.trace( + eitherWithAuthd.fold( + ({ isLeft }) => + Bitwarden.unlockVaultMetric[ + isLeft ? "failure" : "success" + ], + ), + ); + }) + .get(); + } - public fetchSecret( - client: TClient, - key: string, - item: string, - ): Promise> { - return client - .move(key) - .bimap(TraceUtil.withMetricTrace(Bitwarden.fetchSecretMetric)) - .peek((tSession) => - tSession.trace.trace(`looking for your secret ${item} (⑅˘꒳˘)`), - ) - .flatMap((tSession) => - tSession - .move("bw list items") - .map((listCmd) => - getStdout(listCmd, { env: { BW_SESSION: tSession.get() } }), - ), - ) - .map( - TraceUtil.promiseify((tEitherItemsJson) => - tEitherItemsJson - .get() - .flatMap( - (itemsJson): IEither> => - Either.fromFailable(() => JSON.parse(itemsJson)), + public fetchSecret( + client: TClient, + key: string, + item: string, + ): Promise> { + return client + .move(key) + .bimap(TraceUtil.withMetricTrace(Bitwarden.fetchSecretMetric)) + .peek((tSession) => + tSession.trace.trace(`looking for your secret ${item} (⑅˘꒳˘)`), + ) + .flatMap((tSession) => + tSession + .move("bw list items") + .map((listCmd) => + getStdout(listCmd, { + env: { BW_SESSION: tSession.get() }, + }), + ), + ) + .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); + }), + ), ) - .flatMap((itemsList): IEither => { - const secret = itemsList.find(({ name }) => name === item); - if (!secret) { - return Either.left( - new Error(`couldn't find the item ${item} (。•́︿•̀。)`), + .peek(async (tEitherWithSecret) => { + const eitherWithSecret = await tEitherWithSecret.get(); + return tEitherWithSecret.trace.trace( + eitherWithSecret.fold( + ({ isLeft }) => + Bitwarden.fetchSecretMetric[ + isLeft ? "failure" : "success" + ], + ), ); - } - return Either.right(secret); - }), - ), - ) - .peek(async (tEitherWithSecret) => { - const eitherWithSecret = await tEitherWithSecret.get(); - return tEitherWithSecret.trace.trace( - eitherWithSecret.fold( - (err, _val) => - Bitwarden.fetchSecretMetric[err ? "failure" : "success"], - ), - ); - }) - .get(); - } + }) + .get(); + } - public lock(client: TClient, key: TKey) { - return client - .move(key) - .bimap(TraceUtil.withMetricTrace(Bitwarden.lockVaultMetric)) - .peek((tSession) => - tSession.trace.trace(`taking care of locking the vault :3`), - ) - .flatMap((tSession) => - tSession - .move("bw lock") - .map((lockCmd) => - getStdout(lockCmd, { env: { BW_SESSION: tSession.get() } }), - ), - ) - .peek(async (tEitherWithLocked) => { - const eitherWithLocked = await tEitherWithLocked.get(); - return eitherWithLocked.fold((err, _val) => { - tEitherWithLocked.trace.trace( - Bitwarden.lockVaultMetric[err ? "failure" : "success"], - ); - if (err) return; - tEitherWithLocked.trace.trace( - "all locked up and secure now~ (。•̀ᴗ-)✧", - ); - }); - }) - .get(); - } + public lock(client: TClient, key: TKey) { + return client + .move(key) + .bimap(TraceUtil.withMetricTrace(Bitwarden.lockVaultMetric)) + .peek((tSession) => + tSession.trace.trace(`taking care of locking the vault :3`), + ) + .flatMap((tSession) => + tSession + .move("bw lock") + .map((lockCmd) => + getStdout(lockCmd, { + env: { BW_SESSION: tSession.get() }, + }), + ), + ) + .peek(async (tEitherWithLocked) => { + const eitherWithLocked = await tEitherWithLocked.get(); + return eitherWithLocked.fold(({ isLeft }) => { + tEitherWithLocked.trace.trace( + Bitwarden.lockVaultMetric[isLeft ? "failure" : "success"], + ); + if (isLeft) return; + tEitherWithLocked.trace.trace( + "all locked up and secure now~ (。•̀ᴗ-)✧", + ); + }); + }) + .get(); + } - public static getConfigFromEnvironment(): IEither { - return getRequiredEnvVars([ - "BW_SERVER", - "BW_CLIENTSECRET", - "BW_CLIENTID", - "BW_PASSWORD", - ]).mapRight(({ BW_SERVER, BW_CLIENTSECRET, BW_CLIENTID }) => ({ - clientId: BW_CLIENTID, - secret: BW_CLIENTSECRET, - server: BW_SERVER, - })); - } + public static getConfigFromEnvironment(): IEither { + return getRequiredEnvVars([ + "BW_SERVER", + "BW_CLIENTSECRET", + "BW_CLIENTID", + "BW_PASSWORD", + ]).mapRight(({ BW_SERVER, BW_CLIENTSECRET, BW_CLIENTID }) => ({ + clientId: BW_CLIENTID, + secret: BW_CLIENTSECRET, + server: BW_SERVER, + })); + } - private static loginMetric = Metric.fromName("Bitwarden.login"); - private static unlockVaultMetric = Metric.fromName("Bitwarden.unlockVault"); - private static fetchSecretMetric = Metric.fromName("Bitwarden.fetchSecret"); - private static lockVaultMetric = Metric.fromName("Bitwarden.lock"); + private static loginMetric = Metric.fromName("Bitwarden.login"); + private static unlockVaultMetric = Metric.fromName("Bitwarden.unlockVault"); + private static fetchSecretMetric = Metric.fromName("Bitwarden.fetchSecret"); + private static lockVaultMetric = Metric.fromName("Bitwarden.lock"); } export interface BitwardenConfig { - server: string; - secret: string; - clientId: string; + server: string; + secret: string; + clientId: string; } // -- -- -- cgit v1.2.3-70-g09d2