284 lines · 9.2 KB · TypeScript
RawDownload
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<void, AuthPairingLinkRepositoryError>;    readonly consumeAvailable: (      input: ConsumeAuthPairingLinkInput,    ) => Effect.Effect<Option.Option<AuthPairingLinkRecord>, AuthPairingLinkRepositoryError>;    readonly listActive: (      input: ListActiveAuthPairingLinksInput,    ) => Effect.Effect<ReadonlyArray<AuthPairingLinkRecord>, AuthPairingLinkRepositoryError>;    readonly revoke: (      input: RevokeAuthPairingLinkInput,    ) => Effect.Effect<boolean, AuthPairingLinkRepositoryError>;    readonly getByCredential: (      input: GetAuthPairingLinkByCredentialInput,    ) => Effect.Effect<Option.Option<AuthPairingLinkRecord>, 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);