import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; import * as SqlClient from "effect/unstable/sql/SqlClient"; import * as SqlSchema from "effect/unstable/sql/SqlSchema"; import { AuthEnvironmentScopes } from "@t3tools/contracts"; import { type AuthPairingLinkRepositoryError, PersistenceDecodeError, type PersistenceErrorCorrelation, PersistenceSqlError, } from "./Errors.ts"; export const AuthPairingLinkRecord = Schema.Struct({ id: Schema.String, credential: Schema.String, method: Schema.Literals(["desktop-bootstrap", "one-time-token"]), scopes: Schema.fromJsonString(AuthEnvironmentScopes), subject: Schema.String, label: Schema.NullOr(Schema.String), proofKeyThumbprint: Schema.NullOr(Schema.String), createdAt: Schema.DateTimeUtcFromString, expiresAt: Schema.DateTimeUtcFromString, consumedAt: Schema.NullOr(Schema.DateTimeUtcFromString), revokedAt: Schema.NullOr(Schema.DateTimeUtcFromString), }); export type AuthPairingLinkRecord = typeof AuthPairingLinkRecord.Type; export const CreateAuthPairingLinkInput = Schema.Struct({ id: Schema.String, credential: Schema.String, method: Schema.Literals(["desktop-bootstrap", "one-time-token"]), scopes: AuthEnvironmentScopes, subject: Schema.String, label: Schema.NullOr(Schema.String), proofKeyThumbprint: Schema.NullOr(Schema.String), createdAt: Schema.DateTimeUtcFromString, expiresAt: Schema.DateTimeUtcFromString, }); export type CreateAuthPairingLinkInput = typeof CreateAuthPairingLinkInput.Type; export const ConsumeAuthPairingLinkInput = Schema.Struct({ credential: Schema.String, proofKeyThumbprint: Schema.NullOr(Schema.String), consumedAt: Schema.DateTimeUtcFromString, now: Schema.DateTimeUtcFromString, }); export type ConsumeAuthPairingLinkInput = typeof ConsumeAuthPairingLinkInput.Type; export const ListActiveAuthPairingLinksInput = Schema.Struct({ now: Schema.DateTimeUtcFromString, }); export type ListActiveAuthPairingLinksInput = typeof ListActiveAuthPairingLinksInput.Type; export const RevokeAuthPairingLinkInput = Schema.Struct({ id: Schema.String, revokedAt: Schema.DateTimeUtcFromString, }); export type RevokeAuthPairingLinkInput = typeof RevokeAuthPairingLinkInput.Type; export const GetAuthPairingLinkByCredentialInput = Schema.Struct({ credential: Schema.String, }); export type GetAuthPairingLinkByCredentialInput = typeof GetAuthPairingLinkByCredentialInput.Type; const AuthPairingLinkRawDbRow = Schema.Struct({ id: Schema.String, credential: Schema.Unknown, method: Schema.Unknown, scopes: Schema.Unknown, subject: Schema.Unknown, label: Schema.Unknown, proofKeyThumbprint: Schema.Unknown, createdAt: Schema.Unknown, expiresAt: Schema.Unknown, consumedAt: Schema.Unknown, revokedAt: Schema.Unknown, }); const decodeAuthPairingLinkDbRow = Schema.decodeUnknownEffect(AuthPairingLinkRecord); export class AuthPairingLinkRepository extends Context.Service< AuthPairingLinkRepository, { readonly create: ( input: CreateAuthPairingLinkInput, ) => Effect.Effect; readonly consumeAvailable: ( input: ConsumeAuthPairingLinkInput, ) => Effect.Effect, AuthPairingLinkRepositoryError>; readonly listActive: ( input: ListActiveAuthPairingLinksInput, ) => Effect.Effect, AuthPairingLinkRepositoryError>; readonly revoke: ( input: RevokeAuthPairingLinkInput, ) => Effect.Effect; readonly getByCredential: ( input: GetAuthPairingLinkByCredentialInput, ) => Effect.Effect, AuthPairingLinkRepositoryError>; } >()("t3/persistence/AuthPairingLinks/AuthPairingLinkRepository") {} function toPersistenceSqlOrDecodeError( sqlOperation: string, decodeOperation: string, correlation?: PersistenceErrorCorrelation, ) { return (cause: unknown): AuthPairingLinkRepositoryError => Schema.isSchemaError(cause) ? PersistenceDecodeError.fromSchemaError(decodeOperation, cause, correlation) : new PersistenceSqlError({ operation: sqlOperation, ...(correlation === undefined ? {} : { correlation }), cause, }); } /** @public Service construction is part of the canonical Effect module API. */ export const make = Effect.gen(function* () { const sql = yield* SqlClient.SqlClient; const createPairingLinkRow = SqlSchema.void({ Request: CreateAuthPairingLinkInput, execute: (input) => sql` INSERT INTO auth_pairing_links ( id, credential, method, scopes, subject, label, proof_key_thumbprint, created_at, expires_at, consumed_at, revoked_at ) VALUES ( ${input.id}, ${input.credential}, ${input.method}, ${JSON.stringify(input.scopes)}, ${input.subject}, ${input.label}, ${input.proofKeyThumbprint}, ${input.createdAt}, ${input.expiresAt}, NULL, NULL ) `, }); const consumeAvailablePairingLinkRow = SqlSchema.findOneOption({ Request: ConsumeAuthPairingLinkInput, Result: AuthPairingLinkRawDbRow, execute: ({ credential, proofKeyThumbprint, consumedAt, now }) => sql` UPDATE auth_pairing_links SET consumed_at = ${consumedAt} WHERE credential = ${credential} AND revoked_at IS NULL AND consumed_at IS NULL AND expires_at > ${now} AND ( proof_key_thumbprint IS NULL OR proof_key_thumbprint = ${proofKeyThumbprint} ) RETURNING id AS "id", credential AS "credential", method AS "method", scopes AS "scopes", subject AS "subject", label AS "label", proof_key_thumbprint AS "proofKeyThumbprint", created_at AS "createdAt", expires_at AS "expiresAt", consumed_at AS "consumedAt", revoked_at AS "revokedAt" `, }); const listActivePairingLinkRows = SqlSchema.findAll({ Request: ListActiveAuthPairingLinksInput, Result: AuthPairingLinkRawDbRow, execute: ({ now }) => sql` SELECT id AS "id", credential AS "credential", method AS "method", scopes AS "scopes", subject AS "subject", label AS "label", proof_key_thumbprint AS "proofKeyThumbprint", created_at AS "createdAt", expires_at AS "expiresAt", consumed_at AS "consumedAt", revoked_at AS "revokedAt" FROM auth_pairing_links WHERE revoked_at IS NULL AND consumed_at IS NULL AND expires_at > ${now} ORDER BY created_at DESC, id DESC `, }); const revokePairingLinkRow = SqlSchema.findAll({ Request: RevokeAuthPairingLinkInput, Result: Schema.Struct({ id: Schema.String }), execute: ({ id, revokedAt }) => sql` UPDATE auth_pairing_links SET revoked_at = ${revokedAt} WHERE id = ${id} AND revoked_at IS NULL AND consumed_at IS NULL RETURNING id AS "id" `, }); const getPairingLinkRowByCredential = SqlSchema.findOneOption({ Request: GetAuthPairingLinkByCredentialInput, Result: AuthPairingLinkRawDbRow, execute: ({ credential }) => sql` SELECT id AS "id", credential AS "credential", method AS "method", scopes AS "scopes", subject AS "subject", label AS "label", proof_key_thumbprint AS "proofKeyThumbprint", created_at AS "createdAt", expires_at AS "expiresAt", consumed_at AS "consumedAt", revoked_at AS "revokedAt" FROM auth_pairing_links WHERE credential = ${credential} `, }); const create: AuthPairingLinkRepository["Service"]["create"] = (input) => createPairingLinkRow(input).pipe( Effect.mapError( toPersistenceSqlOrDecodeError( "AuthPairingLinkRepository.create:query", "AuthPairingLinkRepository.create:encodeRequest", { pairingLinkId: input.id }, ), ), ); const consumeAvailable: AuthPairingLinkRepository["Service"]["consumeAvailable"] = (input) => consumeAvailablePairingLinkRow(input).pipe( Effect.mapError( toPersistenceSqlOrDecodeError( "AuthPairingLinkRepository.consumeAvailable:query", "AuthPairingLinkRepository.consumeAvailable:decodeRow", ), ), Effect.flatMap((rowOption) => Option.match(rowOption, { onNone: () => Effect.succeed(Option.none()), onSome: (row) => decodeAuthPairingLinkDbRow(row).pipe( Effect.mapError((cause) => PersistenceDecodeError.fromSchemaError( "AuthPairingLinkRepository.consumeAvailable:decodeRow", cause, { pairingLinkId: row.id }, ), ), Effect.map(Option.some), ), }), ), ); const listActive: AuthPairingLinkRepository["Service"]["listActive"] = (input) => listActivePairingLinkRows(input).pipe( Effect.mapError( toPersistenceSqlOrDecodeError( "AuthPairingLinkRepository.listActive:query", "AuthPairingLinkRepository.listActive:decodeRows", ), ), Effect.flatMap((rows) => Effect.forEach(rows, (row) => decodeAuthPairingLinkDbRow(row).pipe( Effect.mapError((cause) => PersistenceDecodeError.fromSchemaError( "AuthPairingLinkRepository.listActive:decodeRows", cause, { pairingLinkId: row.id }, ), ), ), ), ), ); const revoke: AuthPairingLinkRepository["Service"]["revoke"] = (input) => revokePairingLinkRow(input).pipe( Effect.mapError( toPersistenceSqlOrDecodeError( "AuthPairingLinkRepository.revoke:query", "AuthPairingLinkRepository.revoke:decodeRows", { pairingLinkId: input.id }, ), ), Effect.map((rows) => rows.length > 0), ); const getByCredential: AuthPairingLinkRepository["Service"]["getByCredential"] = (input) => getPairingLinkRowByCredential(input).pipe( Effect.mapError( toPersistenceSqlOrDecodeError( "AuthPairingLinkRepository.getByCredential:query", "AuthPairingLinkRepository.getByCredential:decodeRow", ), ), Effect.flatMap((rowOption) => Option.match(rowOption, { onNone: () => Effect.succeed(Option.none()), onSome: (row) => decodeAuthPairingLinkDbRow(row).pipe( Effect.mapError((cause) => PersistenceDecodeError.fromSchemaError( "AuthPairingLinkRepository.getByCredential:decodeRow", cause, { pairingLinkId: row.id }, ), ), Effect.map(Option.some), ), }), ), ); return { create, consumeAvailable, listActive, revoke, getByCredential, } satisfies AuthPairingLinkRepository["Service"]; }); export const layer = Layer.effect(AuthPairingLinkRepository, make);