290 lines · 10.0 KB · TypeScript
RawDownload
import * as Context from "effect/Context";import * as Crypto from "effect/Crypto";import * as DateTime from "effect/DateTime";import * as Effect from "effect/Effect";import * as Encoding from "effect/Encoding";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Schema from "effect/Schema";import { and, eq, exists, isNull, ne, notExists } from "drizzle-orm";import { QueryBuilder } from "drizzle-orm/pg-core"; import * as RelayDb from "../db.ts";import { relayEnvironmentCredentials, relayEnvironmentLinks } from "../persistence/schema.ts"; export class EnvironmentCredentialCreatePersistenceError extends Schema.TaggedErrorClass<EnvironmentCredentialCreatePersistenceError>()(  "EnvironmentCredentialCreatePersistenceError",  {    stage: Schema.Literals([      "generate-credential",      "hash-token",      "insert-credential",      "revoke-previous-credentials",    ]),    environmentId: Schema.String,    credentialId: Schema.optionalKey(Schema.String),    cause: Schema.Defect(),  },) {  override get message(): string {    return `Environment credential creation failed during '${this.stage}' for environment '${this.environmentId}'${this.credentialId === undefined ? "" : `, credential '${this.credentialId}'`}`;  }} export class EnvironmentCredentialAuthenticatePersistenceError extends Schema.TaggedErrorClass<EnvironmentCredentialAuthenticatePersistenceError>()(  "EnvironmentCredentialAuthenticatePersistenceError",  {    stage: Schema.Literals(["hash-token", "lookup-credential"]),    cause: Schema.Defect(),  },) {  override get message(): string {    return `Environment credential authentication failed during '${this.stage}'`;  }} export class EnvironmentCredentialRevokePersistenceError extends Schema.TaggedErrorClass<EnvironmentCredentialRevokePersistenceError>()(  "EnvironmentCredentialRevokePersistenceError",  {    environmentId: Schema.String,    cause: Schema.Defect(),  },) {  override get message(): string {    return `Failed to revoke credentials for environment '${this.environmentId}'`;  }} export interface EnvironmentCredentialPrincipal {  readonly credentialId: string;  readonly environmentId: string;  readonly environmentPublicKey: string;} export class EnvironmentCredentials extends Context.Service<  EnvironmentCredentials,  {    readonly create: (input: {      readonly environmentId: string;      readonly environmentPublicKey: string;    }) => Effect.Effect<string, EnvironmentCredentialCreatePersistenceError>;    readonly authenticate: (      token: string,    ) => Effect.Effect<      Option.Option<EnvironmentCredentialPrincipal>,      EnvironmentCredentialAuthenticatePersistenceError    >;    readonly revokeForEnvironmentPublicKey: (input: {      readonly environmentId: string;      readonly environmentPublicKey: string;    }) => Effect.Effect<boolean, EnvironmentCredentialRevokePersistenceError>;  }>()("t3code-relay/environments/EnvironmentCredentials") {} const make = Effect.gen(function* () {  const db = yield* RelayDb.RelayDb;  const crypto = yield* Crypto.Crypto;  const hashToken = (token: string) =>    crypto      .digest("SHA-256", new TextEncoder().encode(token))      .pipe(Effect.map(Encoding.encodeBase64Url));  const randomTokenPart = (segments: number) =>    Effect.map(Effect.all(Array.from({ length: segments }, () => crypto.randomUUIDv4)), (values) =>      values.join("").replaceAll("-", ""),    );  const makeCredential = Effect.fnUntraced(function* () {    const credentialId = yield* randomTokenPart(2);    const secret = yield* randomTokenPart(3);    return {      credentialId,      token: `t3env_${credentialId}_${secret}`,    };  });   return EnvironmentCredentials.of({    create: Effect.fn("relay.environment_credentials.create")(function* (input) {      yield* Effect.annotateCurrentSpan({ "relay.environment_id": input.environmentId });      const credential = yield* makeCredential().pipe(        Effect.mapError(          (cause) =>            new EnvironmentCredentialCreatePersistenceError({              stage: "generate-credential",              environmentId: input.environmentId,              cause,            }),        ),      );      const credentialHash = yield* hashToken(credential.token).pipe(        Effect.mapError(          (cause) =>            new EnvironmentCredentialCreatePersistenceError({              stage: "hash-token",              environmentId: input.environmentId,              credentialId: credential.credentialId,              cause,            }),        ),      );      const now = DateTime.formatIso(yield* DateTime.now);      yield* db        .insert(relayEnvironmentCredentials)        .values({          credentialId: credential.credentialId,          environmentId: input.environmentId,          environmentPublicKey: input.environmentPublicKey,          credentialHash,          revokedAt: null,          createdAt: now,          updatedAt: now,        })        .pipe(          Effect.mapError(            (cause) =>              new EnvironmentCredentialCreatePersistenceError({                stage: "insert-credential",                environmentId: input.environmentId,                credentialId: credential.credentialId,                cause,              }),          ),        );      yield* db        .update(relayEnvironmentCredentials)        .set({          revokedAt: now,          updatedAt: now,        })        .where(          and(            eq(relayEnvironmentCredentials.environmentId, input.environmentId),            eq(relayEnvironmentCredentials.environmentPublicKey, input.environmentPublicKey),            ne(relayEnvironmentCredentials.credentialId, credential.credentialId),            isNull(relayEnvironmentCredentials.revokedAt),          ),        )        .pipe(          Effect.mapError(            (cause) =>              new EnvironmentCredentialCreatePersistenceError({                stage: "revoke-previous-credentials",                environmentId: input.environmentId,                credentialId: credential.credentialId,                cause,              }),          ),        );      return credential.token;    }),     authenticate: Effect.fn("relay.environment_credentials.authenticate")(function* (token) {      const credentialHash = yield* hashToken(token).pipe(        Effect.mapError(          (cause) =>            new EnvironmentCredentialAuthenticatePersistenceError({              stage: "hash-token",              cause,            }),        ),      );      const rows = yield* db        .select({          credentialId: relayEnvironmentCredentials.credentialId,          environmentId: relayEnvironmentCredentials.environmentId,          environmentPublicKey: relayEnvironmentCredentials.environmentPublicKey,        })        .from(relayEnvironmentCredentials)        .where(          and(            eq(relayEnvironmentCredentials.credentialHash, credentialHash),            isNull(relayEnvironmentCredentials.revokedAt),            exists(              new QueryBuilder()                .select({ userId: relayEnvironmentLinks.userId })                .from(relayEnvironmentLinks)                .where(                  and(                    eq(                      relayEnvironmentLinks.environmentId,                      relayEnvironmentCredentials.environmentId,                    ),                    eq(                      relayEnvironmentLinks.environmentPublicKey,                      relayEnvironmentCredentials.environmentPublicKey,                    ),                    isNull(relayEnvironmentLinks.revokedAt),                  ),                ),            ),          ),        )        .limit(1)        .pipe(          Effect.mapError(            (cause) =>              new EnvironmentCredentialAuthenticatePersistenceError({                stage: "lookup-credential",                cause,              }),          ),        );      const row = rows[0];      if (row) {        yield* Effect.annotateCurrentSpan({ "relay.environment_id": row.environmentId });      }      return row        ? Option.some({            credentialId: row.credentialId,            environmentId: row.environmentId,            environmentPublicKey: row.environmentPublicKey,          })        : Option.none();    }),     revokeForEnvironmentPublicKey: Effect.fn(      "relay.environment_credentials.revoke_for_environment_public_key",    )(function* (input) {      yield* Effect.annotateCurrentSpan({ "relay.environment_id": input.environmentId });      const revokedAt = DateTime.formatIso(yield* DateTime.now);      const rows = yield* db        .update(relayEnvironmentCredentials)        .set({          revokedAt,          updatedAt: revokedAt,        })        .where(          and(            eq(relayEnvironmentCredentials.environmentId, input.environmentId),            eq(relayEnvironmentCredentials.environmentPublicKey, input.environmentPublicKey),            isNull(relayEnvironmentCredentials.revokedAt),            notExists(              new QueryBuilder()                .select({ userId: relayEnvironmentLinks.userId })                .from(relayEnvironmentLinks)                .where(                  and(                    eq(relayEnvironmentLinks.environmentId, input.environmentId),                    eq(relayEnvironmentLinks.environmentPublicKey, input.environmentPublicKey),                    isNull(relayEnvironmentLinks.revokedAt),                  ),                ),            ),          ),        )        .returning({          credentialId: relayEnvironmentCredentials.credentialId,        })        .pipe(          Effect.mapError(            (cause) =>              new EnvironmentCredentialRevokePersistenceError({                environmentId: input.environmentId,                cause,              }),          ),        );      return rows.length > 0;    }),  });}); export const layer = Layer.effect(EnvironmentCredentials, make);