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, toPersistenceDecodeError, toPersistenceSqlError, } 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; 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) { return (cause: unknown): AuthPairingLinkRepositoryError => Schema.isSchemaError(cause) ? toPersistenceDecodeError(decodeOperation)(cause) : toPersistenceSqlError(sqlOperation)(cause); } 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: AuthPairingLinkRecord, 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: AuthPairingLinkRecord, 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: AuthPairingLinkRecord, 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", ), ), ); const consumeAvailable: AuthPairingLinkRepository["Service"]["consumeAvailable"] = (input) => consumeAvailablePairingLinkRow(input).pipe( Effect.mapError( toPersistenceSqlOrDecodeError( "AuthPairingLinkRepository.consumeAvailable:query", "AuthPairingLinkRepository.consumeAvailable:decodeRow", ), ), ); const listActive: AuthPairingLinkRepository["Service"]["listActive"] = (input) => listActivePairingLinkRows(input).pipe( Effect.mapError( toPersistenceSqlOrDecodeError( "AuthPairingLinkRepository.listActive:query", "AuthPairingLinkRepository.listActive:decodeRows", ), ), ); const revoke: AuthPairingLinkRepository["Service"]["revoke"] = (input) => revokePairingLinkRow(input).pipe( Effect.mapError( toPersistenceSqlOrDecodeError( "AuthPairingLinkRepository.revoke:query", "AuthPairingLinkRepository.revoke:decodeRows", ), ), Effect.map((rows) => rows.length > 0), ); const getByCredential: AuthPairingLinkRepository["Service"]["getByCredential"] = (input) => getPairingLinkRowByCredential(input).pipe( Effect.mapError( toPersistenceSqlOrDecodeError( "AuthPairingLinkRepository.getByCredential:query", "AuthPairingLinkRepository.getByCredential:decodeRow", ), ), ); return { create, consumeAvailable, listActive, revoke, getByCredential, } satisfies AuthPairingLinkRepository["Service"]; }); export const layer = Layer.effect(AuthPairingLinkRepository, make);