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", { 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", { 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", { 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; readonly authenticate: ( token: string, ) => Effect.Effect< Option.Option, EnvironmentCredentialAuthenticatePersistenceError >; readonly revokeForEnvironmentPublicKey: (input: { readonly environmentId: string; readonly environmentPublicKey: string; }) => Effect.Effect; } >()("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);