| author | |
| committer | |
| log | df1540f3ac8fd70877a83e91bd0be182ebf5022c |
| tree | dedf5c2a850411924d11d29b414e518c59700184 |
| parent | d5a72d5be105d16a95df6dc18ad08f0b0cdb7efb |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: codex <codex@users.noreply.github.com>19 files changed, 997 insertions(+), 1060 deletions(-)
apps/server/integration/providerService.integration.test.ts+2-2| ... | ... | @@ -25,7 +25,7 @@ import { |
| 25 | 25 | import { ServerSettingsService } from "../src/serverSettings.ts"; |
| 26 | 26 | import { AnalyticsService } from "../src/telemetry/Services/AnalyticsService.ts"; |
| 27 | 27 | import { SqlitePersistenceMemory } from "../src/persistence/Layers/Sqlite.ts"; |
| 28 | import { ProviderSessionRuntimeRepositoryLive } from "../src/persistence/Layers/ProviderSessionRuntime.ts"; | |
| 28 | import * as ProviderSessionRuntime from "../src/persistence/ProviderSessionRuntime.ts"; | |
| 29 | 29 | |
| 30 | 30 | import { |
| 31 | 31 | makeTestProviderAdapterHarness, |
| ... | ... | @@ -63,7 +63,7 @@ const makeIntegrationFixture = Effect.gen(function* () { |
| 63 | 63 | }); |
| 64 | 64 | |
| 65 | 65 | const directoryLayer = ProviderSessionDirectoryLive.pipe( |
| 66 | Layer.provide(ProviderSessionRuntimeRepositoryLive), | |
| 66 | Layer.provide(ProviderSessionRuntime.layer), | |
| 67 | 67 | ); |
| 68 | 68 | |
| 69 | 69 | const shared = Layer.mergeAll( |
apps/server/src/auth/PairingGrantStore.ts+3-4| ... | ... | @@ -18,8 +18,7 @@ import * as Stream from "effect/Stream"; |
| 18 | 18 | import * as Option from "effect/Option"; |
| 19 | 19 | |
| 20 | 20 | import { ServerConfig } from "../config.ts"; |
| 21 | import { AuthPairingLinkRepositoryLive } from "../persistence/Layers/AuthPairingLinks.ts"; | |
| 22 | import { AuthPairingLinkRepository } from "../persistence/Services/AuthPairingLinks.ts"; | |
| 21 | import * as AuthPairingLinks from "../persistence/AuthPairingLinks.ts"; | |
| 23 | 22 | |
| 24 | 23 | export interface BootstrapGrant { |
| 25 | 24 | readonly method: ServerAuthBootstrapMethod; |
| ... | ... | @@ -126,7 +125,7 @@ const internalBootstrapCredentialError = (message: string, cause: unknown) => |
| 126 | 125 | export const make = Effect.fn("makePairingGrantStore")(function* () { |
| 127 | 126 | const crypto = yield* Crypto.Crypto; |
| 128 | 127 | const config = yield* ServerConfig; |
| 129 | const pairingLinks = yield* AuthPairingLinkRepository; | |
| 128 | const pairingLinks = yield* AuthPairingLinks.AuthPairingLinkRepository; | |
| 130 | 129 | const seededGrantsRef = yield* Ref.make(new Map<string, StoredBootstrapGrant>()); |
| 131 | 130 | const changesPubSub = yield* PubSub.unbounded<BootstrapCredentialChange>(); |
| 132 | 131 | const generatePairingToken = Effect.gen(function* () { |
| ... | ... | @@ -417,5 +416,5 @@ export const make = Effect.fn("makePairingGrantStore")(function* () { |
| 417 | 416 | }); |
| 418 | 417 | |
| 419 | 418 | export const layer = Layer.effect(PairingGrantStore, make()).pipe( |
| 420 | Layer.provideMerge(AuthPairingLinkRepositoryLive), | |
| 419 | Layer.provideMerge(AuthPairingLinks.layer), | |
| 421 | 420 | ); |
apps/server/src/auth/SessionStore.test.ts+1-1| ... | ... | @@ -8,7 +8,7 @@ import * as TestClock from "effect/testing/TestClock"; |
| 8 | 8 | import * as ServerConfig from "../config.ts"; |
| 9 | 9 | import { PersistenceSqlError } from "../persistence/Errors.ts"; |
| 10 | 10 | import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts"; |
| 11 | import * as AuthSessions from "../persistence/Services/AuthSessions.ts"; | |
| 11 | import * as AuthSessions from "../persistence/AuthSessions.ts"; | |
| 12 | 12 | import * as SessionStore from "./SessionStore.ts"; |
| 13 | 13 | import * as ServerSecretStore from "./ServerSecretStore.ts"; |
| 14 | 14 |
apps/server/src/auth/SessionStore.ts+3-4| ... | ... | @@ -22,8 +22,7 @@ import * as Stream from "effect/Stream"; |
| 22 | 22 | import * as Option from "effect/Option"; |
| 23 | 23 | |
| 24 | 24 | import { ServerConfig } from "../config.ts"; |
| 25 | import { AuthSessionRepositoryLive } from "../persistence/Layers/AuthSessions.ts"; | |
| 26 | import { AuthSessionRepository } from "../persistence/Services/AuthSessions.ts"; | |
| 25 | import * as AuthSessions from "../persistence/AuthSessions.ts"; | |
| 27 | 26 | import * as ServerSecretStore from "./ServerSecretStore.ts"; |
| 28 | 27 | import { |
| 29 | 28 | base64UrlDecodeUtf8, |
| ... | ... | @@ -195,7 +194,7 @@ export const make = Effect.fn("makeSessionStore")(function* () { |
| 195 | 194 | const crypto = yield* Crypto.Crypto; |
| 196 | 195 | const serverConfig = yield* ServerConfig; |
| 197 | 196 | const secretStore = yield* ServerSecretStore.ServerSecretStore; |
| 198 | const authSessions = yield* AuthSessionRepository; | |
| 197 | const authSessions = yield* AuthSessions.AuthSessionRepository; | |
| 199 | 198 | const signingSecret = yield* secretStore.getOrCreateRandom(SIGNING_SECRET_NAME, 32); |
| 200 | 199 | const connectedSessionsRef = yield* Ref.make(new Map<string, number>()); |
| 201 | 200 | const changesPubSub = yield* PubSub.unbounded<SessionCredentialChange>(); |
| ... | ... | @@ -640,5 +639,5 @@ export const make = Effect.fn("makeSessionStore")(function* () { |
| 640 | 639 | }); |
| 641 | 640 | |
| 642 | 641 | export const layer = Layer.effect(SessionStore, make()).pipe( |
| 643 | Layer.provideMerge(AuthSessionRepositoryLive), | |
| 642 | Layer.provideMerge(AuthSessions.layer), | |
| 644 | 643 | ); |
apps/server/src/persistence/AuthPairingLinks.ts created+284| ... | ... | @@ -0,0 +1,284 @@ |
| 1 | import * as Context from "effect/Context"; | |
| 2 | import * as Effect from "effect/Effect"; | |
| 3 | import * as Layer from "effect/Layer"; | |
| 4 | import * as Option from "effect/Option"; | |
| 5 | import * as Schema from "effect/Schema"; | |
| 6 | import * as SqlClient from "effect/unstable/sql/SqlClient"; | |
| 7 | import * as SqlSchema from "effect/unstable/sql/SqlSchema"; | |
| 8 | ||
| 9 | import { AuthEnvironmentScopes } from "@t3tools/contracts"; | |
| 10 | ||
| 11 | import { | |
| 12 | type AuthPairingLinkRepositoryError, | |
| 13 | toPersistenceDecodeError, | |
| 14 | toPersistenceSqlError, | |
| 15 | } from "./Errors.ts"; | |
| 16 | ||
| 17 | export const AuthPairingLinkRecord = Schema.Struct({ | |
| 18 | id: Schema.String, | |
| 19 | credential: Schema.String, | |
| 20 | method: Schema.Literals(["desktop-bootstrap", "one-time-token"]), | |
| 21 | scopes: Schema.fromJsonString(AuthEnvironmentScopes), | |
| 22 | subject: Schema.String, | |
| 23 | label: Schema.NullOr(Schema.String), | |
| 24 | proofKeyThumbprint: Schema.NullOr(Schema.String), | |
| 25 | createdAt: Schema.DateTimeUtcFromString, | |
| 26 | expiresAt: Schema.DateTimeUtcFromString, | |
| 27 | consumedAt: Schema.NullOr(Schema.DateTimeUtcFromString), | |
| 28 | revokedAt: Schema.NullOr(Schema.DateTimeUtcFromString), | |
| 29 | }); | |
| 30 | export type AuthPairingLinkRecord = typeof AuthPairingLinkRecord.Type; | |
| 31 | ||
| 32 | export const CreateAuthPairingLinkInput = Schema.Struct({ | |
| 33 | id: Schema.String, | |
| 34 | credential: Schema.String, | |
| 35 | method: Schema.Literals(["desktop-bootstrap", "one-time-token"]), | |
| 36 | scopes: AuthEnvironmentScopes, | |
| 37 | subject: Schema.String, | |
| 38 | label: Schema.NullOr(Schema.String), | |
| 39 | proofKeyThumbprint: Schema.NullOr(Schema.String), | |
| 40 | createdAt: Schema.DateTimeUtcFromString, | |
| 41 | expiresAt: Schema.DateTimeUtcFromString, | |
| 42 | }); | |
| 43 | export type CreateAuthPairingLinkInput = typeof CreateAuthPairingLinkInput.Type; | |
| 44 | ||
| 45 | export const ConsumeAuthPairingLinkInput = Schema.Struct({ | |
| 46 | credential: Schema.String, | |
| 47 | proofKeyThumbprint: Schema.NullOr(Schema.String), | |
| 48 | consumedAt: Schema.DateTimeUtcFromString, | |
| 49 | now: Schema.DateTimeUtcFromString, | |
| 50 | }); | |
| 51 | export type ConsumeAuthPairingLinkInput = typeof ConsumeAuthPairingLinkInput.Type; | |
| 52 | ||
| 53 | export const ListActiveAuthPairingLinksInput = Schema.Struct({ | |
| 54 | now: Schema.DateTimeUtcFromString, | |
| 55 | }); | |
| 56 | export type ListActiveAuthPairingLinksInput = typeof ListActiveAuthPairingLinksInput.Type; | |
| 57 | ||
| 58 | export const RevokeAuthPairingLinkInput = Schema.Struct({ | |
| 59 | id: Schema.String, | |
| 60 | revokedAt: Schema.DateTimeUtcFromString, | |
| 61 | }); | |
| 62 | export type RevokeAuthPairingLinkInput = typeof RevokeAuthPairingLinkInput.Type; | |
| 63 | ||
| 64 | export const GetAuthPairingLinkByCredentialInput = Schema.Struct({ | |
| 65 | credential: Schema.String, | |
| 66 | }); | |
| 67 | export type GetAuthPairingLinkByCredentialInput = typeof GetAuthPairingLinkByCredentialInput.Type; | |
| 68 | ||
| 69 | export class AuthPairingLinkRepository extends Context.Service< | |
| 70 | AuthPairingLinkRepository, | |
| 71 | { | |
| 72 | readonly create: ( | |
| 73 | input: CreateAuthPairingLinkInput, | |
| 74 | ) => Effect.Effect<void, AuthPairingLinkRepositoryError>; | |
| 75 | readonly consumeAvailable: ( | |
| 76 | input: ConsumeAuthPairingLinkInput, | |
| 77 | ) => Effect.Effect<Option.Option<AuthPairingLinkRecord>, AuthPairingLinkRepositoryError>; | |
| 78 | readonly listActive: ( | |
| 79 | input: ListActiveAuthPairingLinksInput, | |
| 80 | ) => Effect.Effect<ReadonlyArray<AuthPairingLinkRecord>, AuthPairingLinkRepositoryError>; | |
| 81 | readonly revoke: ( | |
| 82 | input: RevokeAuthPairingLinkInput, | |
| 83 | ) => Effect.Effect<boolean, AuthPairingLinkRepositoryError>; | |
| 84 | readonly getByCredential: ( | |
| 85 | input: GetAuthPairingLinkByCredentialInput, | |
| 86 | ) => Effect.Effect<Option.Option<AuthPairingLinkRecord>, AuthPairingLinkRepositoryError>; | |
| 87 | } | |
| 88 | >()("t3/persistence/AuthPairingLinks/AuthPairingLinkRepository") {} | |
| 89 | ||
| 90 | function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) { | |
| 91 | return (cause: unknown): AuthPairingLinkRepositoryError => | |
| 92 | Schema.isSchemaError(cause) | |
| 93 | ? toPersistenceDecodeError(decodeOperation)(cause) | |
| 94 | : toPersistenceSqlError(sqlOperation)(cause); | |
| 95 | } | |
| 96 | ||
| 97 | export const make = Effect.gen(function* () { | |
| 98 | const sql = yield* SqlClient.SqlClient; | |
| 99 | ||
| 100 | const createPairingLinkRow = SqlSchema.void({ | |
| 101 | Request: CreateAuthPairingLinkInput, | |
| 102 | execute: (input) => | |
| 103 | sql` | |
| 104 | INSERT INTO auth_pairing_links ( | |
| 105 | id, | |
| 106 | credential, | |
| 107 | method, | |
| 108 | scopes, | |
| 109 | subject, | |
| 110 | label, | |
| 111 | proof_key_thumbprint, | |
| 112 | created_at, | |
| 113 | expires_at, | |
| 114 | consumed_at, | |
| 115 | revoked_at | |
| 116 | ) | |
| 117 | VALUES ( | |
| 118 | ${input.id}, | |
| 119 | ${input.credential}, | |
| 120 | ${input.method}, | |
| 121 | ${JSON.stringify(input.scopes)}, | |
| 122 | ${input.subject}, | |
| 123 | ${input.label}, | |
| 124 | ${input.proofKeyThumbprint}, | |
| 125 | ${input.createdAt}, | |
| 126 | ${input.expiresAt}, | |
| 127 | NULL, | |
| 128 | NULL | |
| 129 | ) | |
| 130 | `, | |
| 131 | }); | |
| 132 | ||
| 133 | const consumeAvailablePairingLinkRow = SqlSchema.findOneOption({ | |
| 134 | Request: ConsumeAuthPairingLinkInput, | |
| 135 | Result: AuthPairingLinkRecord, | |
| 136 | execute: ({ credential, proofKeyThumbprint, consumedAt, now }) => | |
| 137 | sql` | |
| 138 | UPDATE auth_pairing_links | |
| 139 | SET consumed_at = ${consumedAt} | |
| 140 | WHERE credential = ${credential} | |
| 141 | AND revoked_at IS NULL | |
| 142 | AND consumed_at IS NULL | |
| 143 | AND expires_at > ${now} | |
| 144 | AND ( | |
| 145 | proof_key_thumbprint IS NULL | |
| 146 | OR proof_key_thumbprint = ${proofKeyThumbprint} | |
| 147 | ) | |
| 148 | RETURNING | |
| 149 | id AS "id", | |
| 150 | credential AS "credential", | |
| 151 | method AS "method", | |
| 152 | scopes AS "scopes", | |
| 153 | subject AS "subject", | |
| 154 | label AS "label", | |
| 155 | proof_key_thumbprint AS "proofKeyThumbprint", | |
| 156 | created_at AS "createdAt", | |
| 157 | expires_at AS "expiresAt", | |
| 158 | consumed_at AS "consumedAt", | |
| 159 | revoked_at AS "revokedAt" | |
| 160 | `, | |
| 161 | }); | |
| 162 | ||
| 163 | const listActivePairingLinkRows = SqlSchema.findAll({ | |
| 164 | Request: ListActiveAuthPairingLinksInput, | |
| 165 | Result: AuthPairingLinkRecord, | |
| 166 | execute: ({ now }) => | |
| 167 | sql` | |
| 168 | SELECT | |
| 169 | id AS "id", | |
| 170 | credential AS "credential", | |
| 171 | method AS "method", | |
| 172 | scopes AS "scopes", | |
| 173 | subject AS "subject", | |
| 174 | label AS "label", | |
| 175 | proof_key_thumbprint AS "proofKeyThumbprint", | |
| 176 | created_at AS "createdAt", | |
| 177 | expires_at AS "expiresAt", | |
| 178 | consumed_at AS "consumedAt", | |
| 179 | revoked_at AS "revokedAt" | |
| 180 | FROM auth_pairing_links | |
| 181 | WHERE revoked_at IS NULL | |
| 182 | AND consumed_at IS NULL | |
| 183 | AND expires_at > ${now} | |
| 184 | ORDER BY created_at DESC, id DESC | |
| 185 | `, | |
| 186 | }); | |
| 187 | ||
| 188 | const revokePairingLinkRow = SqlSchema.findAll({ | |
| 189 | Request: RevokeAuthPairingLinkInput, | |
| 190 | Result: Schema.Struct({ id: Schema.String }), | |
| 191 | execute: ({ id, revokedAt }) => | |
| 192 | sql` | |
| 193 | UPDATE auth_pairing_links | |
| 194 | SET revoked_at = ${revokedAt} | |
| 195 | WHERE id = ${id} | |
| 196 | AND revoked_at IS NULL | |
| 197 | AND consumed_at IS NULL | |
| 198 | RETURNING id AS "id" | |
| 199 | `, | |
| 200 | }); | |
| 201 | ||
| 202 | const getPairingLinkRowByCredential = SqlSchema.findOneOption({ | |
| 203 | Request: GetAuthPairingLinkByCredentialInput, | |
| 204 | Result: AuthPairingLinkRecord, | |
| 205 | execute: ({ credential }) => | |
| 206 | sql` | |
| 207 | SELECT | |
| 208 | id AS "id", | |
| 209 | credential AS "credential", | |
| 210 | method AS "method", | |
| 211 | scopes AS "scopes", | |
| 212 | subject AS "subject", | |
| 213 | label AS "label", | |
| 214 | proof_key_thumbprint AS "proofKeyThumbprint", | |
| 215 | created_at AS "createdAt", | |
| 216 | expires_at AS "expiresAt", | |
| 217 | consumed_at AS "consumedAt", | |
| 218 | revoked_at AS "revokedAt" | |
| 219 | FROM auth_pairing_links | |
| 220 | WHERE credential = ${credential} | |
| 221 | `, | |
| 222 | }); | |
| 223 | ||
| 224 | const create: AuthPairingLinkRepository["Service"]["create"] = (input) => | |
| 225 | createPairingLinkRow(input).pipe( | |
| 226 | Effect.mapError( | |
| 227 | toPersistenceSqlOrDecodeError( | |
| 228 | "AuthPairingLinkRepository.create:query", | |
| 229 | "AuthPairingLinkRepository.create:encodeRequest", | |
| 230 | ), | |
| 231 | ), | |
| 232 | ); | |
| 233 | ||
| 234 | const consumeAvailable: AuthPairingLinkRepository["Service"]["consumeAvailable"] = (input) => | |
| 235 | consumeAvailablePairingLinkRow(input).pipe( | |
| 236 | Effect.mapError( | |
| 237 | toPersistenceSqlOrDecodeError( | |
| 238 | "AuthPairingLinkRepository.consumeAvailable:query", | |
| 239 | "AuthPairingLinkRepository.consumeAvailable:decodeRow", | |
| 240 | ), | |
| 241 | ), | |
| 242 | ); | |
| 243 | ||
| 244 | const listActive: AuthPairingLinkRepository["Service"]["listActive"] = (input) => | |
| 245 | listActivePairingLinkRows(input).pipe( | |
| 246 | Effect.mapError( | |
| 247 | toPersistenceSqlOrDecodeError( | |
| 248 | "AuthPairingLinkRepository.listActive:query", | |
| 249 | "AuthPairingLinkRepository.listActive:decodeRows", | |
| 250 | ), | |
| 251 | ), | |
| 252 | ); | |
| 253 | ||
| 254 | const revoke: AuthPairingLinkRepository["Service"]["revoke"] = (input) => | |
| 255 | revokePairingLinkRow(input).pipe( | |
| 256 | Effect.mapError( | |
| 257 | toPersistenceSqlOrDecodeError( | |
| 258 | "AuthPairingLinkRepository.revoke:query", | |
| 259 | "AuthPairingLinkRepository.revoke:decodeRows", | |
| 260 | ), | |
| 261 | ), | |
| 262 | Effect.map((rows) => rows.length > 0), | |
| 263 | ); | |
| 264 | ||
| 265 | const getByCredential: AuthPairingLinkRepository["Service"]["getByCredential"] = (input) => | |
| 266 | getPairingLinkRowByCredential(input).pipe( | |
| 267 | Effect.mapError( | |
| 268 | toPersistenceSqlOrDecodeError( | |
| 269 | "AuthPairingLinkRepository.getByCredential:query", | |
| 270 | "AuthPairingLinkRepository.getByCredential:decodeRow", | |
| 271 | ), | |
| 272 | ), | |
| 273 | ); | |
| 274 | ||
| 275 | return { | |
| 276 | create, | |
| 277 | consumeAvailable, | |
| 278 | listActive, | |
| 279 | revoke, | |
| 280 | getByCredential, | |
| 281 | } satisfies AuthPairingLinkRepository["Service"]; | |
| 282 | }); | |
| 283 | ||
| 284 | export const layer = Layer.effect(AuthPairingLinkRepository, make); |
apps/server/src/persistence/AuthSessions.ts created+361| ... | ... | @@ -0,0 +1,361 @@ |
| 1 | import * as Context from "effect/Context"; | |
| 2 | import * as Effect from "effect/Effect"; | |
| 3 | import * as Layer from "effect/Layer"; | |
| 4 | import * as Option from "effect/Option"; | |
| 5 | import * as Schema from "effect/Schema"; | |
| 6 | import * as SqlClient from "effect/unstable/sql/SqlClient"; | |
| 7 | import * as SqlSchema from "effect/unstable/sql/SqlSchema"; | |
| 8 | ||
| 9 | import { | |
| 10 | AuthClientMetadataDeviceType, | |
| 11 | AuthEnvironmentScopes, | |
| 12 | AuthSessionId, | |
| 13 | ServerAuthSessionMethod, | |
| 14 | } from "@t3tools/contracts"; | |
| 15 | ||
| 16 | import { | |
| 17 | type AuthSessionRepositoryError, | |
| 18 | toPersistenceDecodeError, | |
| 19 | toPersistenceSqlError, | |
| 20 | } from "./Errors.ts"; | |
| 21 | ||
| 22 | export const AuthSessionClientMetadataRecord = Schema.Struct({ | |
| 23 | label: Schema.NullOr(Schema.String), | |
| 24 | ipAddress: Schema.NullOr(Schema.String), | |
| 25 | userAgent: Schema.NullOr(Schema.String), | |
| 26 | deviceType: AuthClientMetadataDeviceType, | |
| 27 | os: Schema.NullOr(Schema.String), | |
| 28 | browser: Schema.NullOr(Schema.String), | |
| 29 | }); | |
| 30 | export type AuthSessionClientMetadataRecord = typeof AuthSessionClientMetadataRecord.Type; | |
| 31 | ||
| 32 | export const AuthSessionRecord = Schema.Struct({ | |
| 33 | sessionId: AuthSessionId, | |
| 34 | subject: Schema.String, | |
| 35 | scopes: AuthEnvironmentScopes, | |
| 36 | method: ServerAuthSessionMethod, | |
| 37 | client: AuthSessionClientMetadataRecord, | |
| 38 | issuedAt: Schema.DateTimeUtcFromString, | |
| 39 | expiresAt: Schema.DateTimeUtcFromString, | |
| 40 | lastConnectedAt: Schema.NullOr(Schema.DateTimeUtcFromString), | |
| 41 | revokedAt: Schema.NullOr(Schema.DateTimeUtcFromString), | |
| 42 | }); | |
| 43 | export type AuthSessionRecord = typeof AuthSessionRecord.Type; | |
| 44 | ||
| 45 | export const CreateAuthSessionInput = Schema.Struct({ | |
| 46 | sessionId: AuthSessionId, | |
| 47 | subject: Schema.String, | |
| 48 | scopes: AuthEnvironmentScopes, | |
| 49 | method: ServerAuthSessionMethod, | |
| 50 | client: AuthSessionClientMetadataRecord, | |
| 51 | issuedAt: Schema.DateTimeUtcFromString, | |
| 52 | expiresAt: Schema.DateTimeUtcFromString, | |
| 53 | }); | |
| 54 | export type CreateAuthSessionInput = typeof CreateAuthSessionInput.Type; | |
| 55 | ||
| 56 | export const GetAuthSessionByIdInput = Schema.Struct({ | |
| 57 | sessionId: AuthSessionId, | |
| 58 | }); | |
| 59 | export type GetAuthSessionByIdInput = typeof GetAuthSessionByIdInput.Type; | |
| 60 | ||
| 61 | export const ListActiveAuthSessionsInput = Schema.Struct({ | |
| 62 | now: Schema.DateTimeUtcFromString, | |
| 63 | }); | |
| 64 | export type ListActiveAuthSessionsInput = typeof ListActiveAuthSessionsInput.Type; | |
| 65 | ||
| 66 | export const RevokeAuthSessionInput = Schema.Struct({ | |
| 67 | sessionId: AuthSessionId, | |
| 68 | revokedAt: Schema.DateTimeUtcFromString, | |
| 69 | }); | |
| 70 | export type RevokeAuthSessionInput = typeof RevokeAuthSessionInput.Type; | |
| 71 | ||
| 72 | export const RevokeOtherAuthSessionsInput = Schema.Struct({ | |
| 73 | currentSessionId: AuthSessionId, | |
| 74 | revokedAt: Schema.DateTimeUtcFromString, | |
| 75 | }); | |
| 76 | export type RevokeOtherAuthSessionsInput = typeof RevokeOtherAuthSessionsInput.Type; | |
| 77 | ||
| 78 | export const SetAuthSessionLastConnectedAtInput = Schema.Struct({ | |
| 79 | sessionId: AuthSessionId, | |
| 80 | lastConnectedAt: Schema.DateTimeUtcFromString, | |
| 81 | }); | |
| 82 | export type SetAuthSessionLastConnectedAtInput = typeof SetAuthSessionLastConnectedAtInput.Type; | |
| 83 | ||
| 84 | export class AuthSessionRepository extends Context.Service< | |
| 85 | AuthSessionRepository, | |
| 86 | { | |
| 87 | readonly create: ( | |
| 88 | input: CreateAuthSessionInput, | |
| 89 | ) => Effect.Effect<void, AuthSessionRepositoryError>; | |
| 90 | readonly getById: ( | |
| 91 | input: GetAuthSessionByIdInput, | |
| 92 | ) => Effect.Effect<Option.Option<AuthSessionRecord>, AuthSessionRepositoryError>; | |
| 93 | readonly listActive: ( | |
| 94 | input: ListActiveAuthSessionsInput, | |
| 95 | ) => Effect.Effect<ReadonlyArray<AuthSessionRecord>, AuthSessionRepositoryError>; | |
| 96 | readonly revoke: ( | |
| 97 | input: RevokeAuthSessionInput, | |
| 98 | ) => Effect.Effect<boolean, AuthSessionRepositoryError>; | |
| 99 | readonly revokeAllExcept: ( | |
| 100 | input: RevokeOtherAuthSessionsInput, | |
| 101 | ) => Effect.Effect<ReadonlyArray<AuthSessionId>, AuthSessionRepositoryError>; | |
| 102 | readonly setLastConnectedAt: ( | |
| 103 | input: SetAuthSessionLastConnectedAtInput, | |
| 104 | ) => Effect.Effect<void, AuthSessionRepositoryError>; | |
| 105 | } | |
| 106 | >()("t3/persistence/AuthSessions/AuthSessionRepository") {} | |
| 107 | ||
| 108 | const AuthSessionDbRow = Schema.Struct({ | |
| 109 | sessionId: AuthSessionId, | |
| 110 | subject: Schema.String, | |
| 111 | scopes: Schema.fromJsonString(AuthEnvironmentScopes), | |
| 112 | method: ServerAuthSessionMethod, | |
| 113 | clientLabel: Schema.NullOr(Schema.String), | |
| 114 | clientIpAddress: Schema.NullOr(Schema.String), | |
| 115 | clientUserAgent: Schema.NullOr(Schema.String), | |
| 116 | clientDeviceType: Schema.Literals(["desktop", "mobile", "tablet", "bot", "unknown"]), | |
| 117 | clientOs: Schema.NullOr(Schema.String), | |
| 118 | clientBrowser: Schema.NullOr(Schema.String), | |
| 119 | issuedAt: Schema.DateTimeUtcFromString, | |
| 120 | expiresAt: Schema.DateTimeUtcFromString, | |
| 121 | lastConnectedAt: Schema.NullOr(Schema.DateTimeUtcFromString), | |
| 122 | revokedAt: Schema.NullOr(Schema.DateTimeUtcFromString), | |
| 123 | }); | |
| 124 | ||
| 125 | function toAuthSessionRecord(row: typeof AuthSessionDbRow.Type): AuthSessionRecord { | |
| 126 | return { | |
| 127 | sessionId: row.sessionId, | |
| 128 | subject: row.subject, | |
| 129 | scopes: row.scopes, | |
| 130 | method: row.method, | |
| 131 | client: { | |
| 132 | label: row.clientLabel, | |
| 133 | ipAddress: row.clientIpAddress, | |
| 134 | userAgent: row.clientUserAgent, | |
| 135 | deviceType: row.clientDeviceType, | |
| 136 | os: row.clientOs, | |
| 137 | browser: row.clientBrowser, | |
| 138 | }, | |
| 139 | issuedAt: row.issuedAt, | |
| 140 | expiresAt: row.expiresAt, | |
| 141 | lastConnectedAt: row.lastConnectedAt, | |
| 142 | revokedAt: row.revokedAt, | |
| 143 | }; | |
| 144 | } | |
| 145 | ||
| 146 | function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) { | |
| 147 | return (cause: unknown): AuthSessionRepositoryError => | |
| 148 | Schema.isSchemaError(cause) | |
| 149 | ? toPersistenceDecodeError(decodeOperation)(cause) | |
| 150 | : toPersistenceSqlError(sqlOperation)(cause); | |
| 151 | } | |
| 152 | ||
| 153 | export const make = Effect.gen(function* () { | |
| 154 | const sql = yield* SqlClient.SqlClient; | |
| 155 | ||
| 156 | const createSessionRow = SqlSchema.void({ | |
| 157 | Request: CreateAuthSessionInput, | |
| 158 | execute: (input) => | |
| 159 | sql` | |
| 160 | INSERT INTO auth_sessions ( | |
| 161 | session_id, | |
| 162 | subject, | |
| 163 | scopes, | |
| 164 | method, | |
| 165 | client_label, | |
| 166 | client_ip_address, | |
| 167 | client_user_agent, | |
| 168 | client_device_type, | |
| 169 | client_os, | |
| 170 | client_browser, | |
| 171 | issued_at, | |
| 172 | expires_at, | |
| 173 | revoked_at | |
| 174 | ) | |
| 175 | VALUES ( | |
| 176 | ${input.sessionId}, | |
| 177 | ${input.subject}, | |
| 178 | ${JSON.stringify(input.scopes)}, | |
| 179 | ${input.method}, | |
| 180 | ${input.client.label}, | |
| 181 | ${input.client.ipAddress}, | |
| 182 | ${input.client.userAgent}, | |
| 183 | ${input.client.deviceType}, | |
| 184 | ${input.client.os}, | |
| 185 | ${input.client.browser}, | |
| 186 | ${input.issuedAt}, | |
| 187 | ${input.expiresAt}, | |
| 188 | NULL | |
| 189 | ) | |
| 190 | `, | |
| 191 | }); | |
| 192 | ||
| 193 | const getSessionRowById = SqlSchema.findOneOption({ | |
| 194 | Request: GetAuthSessionByIdInput, | |
| 195 | Result: AuthSessionDbRow, | |
| 196 | execute: ({ sessionId }) => | |
| 197 | sql` | |
| 198 | SELECT | |
| 199 | session_id AS "sessionId", | |
| 200 | subject AS "subject", | |
| 201 | scopes AS "scopes", | |
| 202 | method AS "method", | |
| 203 | client_label AS "clientLabel", | |
| 204 | client_ip_address AS "clientIpAddress", | |
| 205 | client_user_agent AS "clientUserAgent", | |
| 206 | client_device_type AS "clientDeviceType", | |
| 207 | client_os AS "clientOs", | |
| 208 | client_browser AS "clientBrowser", | |
| 209 | issued_at AS "issuedAt", | |
| 210 | expires_at AS "expiresAt", | |
| 211 | last_connected_at AS "lastConnectedAt", | |
| 212 | revoked_at AS "revokedAt" | |
| 213 | FROM auth_sessions | |
| 214 | WHERE session_id = ${sessionId} | |
| 215 | `, | |
| 216 | }); | |
| 217 | ||
| 218 | const listActiveSessionRows = SqlSchema.findAll({ | |
| 219 | Request: ListActiveAuthSessionsInput, | |
| 220 | Result: AuthSessionDbRow, | |
| 221 | execute: ({ now }) => | |
| 222 | sql` | |
| 223 | SELECT | |
| 224 | session_id AS "sessionId", | |
| 225 | subject AS "subject", | |
| 226 | scopes AS "scopes", | |
| 227 | method AS "method", | |
| 228 | client_label AS "clientLabel", | |
| 229 | client_ip_address AS "clientIpAddress", | |
| 230 | client_user_agent AS "clientUserAgent", | |
| 231 | client_device_type AS "clientDeviceType", | |
| 232 | client_os AS "clientOs", | |
| 233 | client_browser AS "clientBrowser", | |
| 234 | issued_at AS "issuedAt", | |
| 235 | expires_at AS "expiresAt", | |
| 236 | last_connected_at AS "lastConnectedAt", | |
| 237 | revoked_at AS "revokedAt" | |
| 238 | FROM auth_sessions | |
| 239 | WHERE revoked_at IS NULL | |
| 240 | AND expires_at > ${now} | |
| 241 | ORDER BY issued_at DESC, session_id DESC | |
| 242 | `, | |
| 243 | }); | |
| 244 | ||
| 245 | const setLastConnectedAtRow = SqlSchema.void({ | |
| 246 | Request: SetAuthSessionLastConnectedAtInput, | |
| 247 | execute: ({ sessionId, lastConnectedAt }) => | |
| 248 | sql` | |
| 249 | UPDATE auth_sessions | |
| 250 | SET last_connected_at = ${lastConnectedAt} | |
| 251 | WHERE session_id = ${sessionId} | |
| 252 | AND revoked_at IS NULL | |
| 253 | `, | |
| 254 | }); | |
| 255 | ||
| 256 | const revokeSessionRows = SqlSchema.findAll({ | |
| 257 | Request: RevokeAuthSessionInput, | |
| 258 | Result: Schema.Struct({ sessionId: AuthSessionId }), | |
| 259 | execute: ({ sessionId, revokedAt }) => | |
| 260 | sql` | |
| 261 | UPDATE auth_sessions | |
| 262 | SET revoked_at = ${revokedAt} | |
| 263 | WHERE session_id = ${sessionId} | |
| 264 | AND revoked_at IS NULL | |
| 265 | RETURNING session_id AS "sessionId" | |
| 266 | `, | |
| 267 | }); | |
| 268 | ||
| 269 | const revokeOtherSessionRows = SqlSchema.findAll({ | |
| 270 | Request: RevokeOtherAuthSessionsInput, | |
| 271 | Result: Schema.Struct({ sessionId: AuthSessionId }), | |
| 272 | execute: ({ currentSessionId, revokedAt }) => | |
| 273 | sql` | |
| 274 | UPDATE auth_sessions | |
| 275 | SET revoked_at = ${revokedAt} | |
| 276 | WHERE session_id <> ${currentSessionId} | |
| 277 | AND revoked_at IS NULL | |
| 278 | RETURNING session_id AS "sessionId" | |
| 279 | `, | |
| 280 | }); | |
| 281 | ||
| 282 | const create: AuthSessionRepository["Service"]["create"] = (input) => | |
| 283 | createSessionRow(input).pipe( | |
| 284 | Effect.mapError( | |
| 285 | toPersistenceSqlOrDecodeError( | |
| 286 | "AuthSessionRepository.create:query", | |
| 287 | "AuthSessionRepository.create:encodeRequest", | |
| 288 | ), | |
| 289 | ), | |
| 290 | ); | |
| 291 | ||
| 292 | const getById: AuthSessionRepository["Service"]["getById"] = (input) => | |
| 293 | getSessionRowById(input).pipe( | |
| 294 | Effect.mapError( | |
| 295 | toPersistenceSqlOrDecodeError( | |
| 296 | "AuthSessionRepository.getById:query", | |
| 297 | "AuthSessionRepository.getById:decodeRow", | |
| 298 | ), | |
| 299 | ), | |
| 300 | Effect.flatMap((rowOption) => | |
| 301 | Option.match(rowOption, { | |
| 302 | onNone: () => Effect.succeed(Option.none()), | |
| 303 | onSome: (row) => Effect.succeed(Option.some(toAuthSessionRecord(row))), | |
| 304 | }), | |
| 305 | ), | |
| 306 | ); | |
| 307 | ||
| 308 | const listActive: AuthSessionRepository["Service"]["listActive"] = (input) => | |
| 309 | listActiveSessionRows(input).pipe( | |
| 310 | Effect.mapError( | |
| 311 | toPersistenceSqlOrDecodeError( | |
| 312 | "AuthSessionRepository.listActive:query", | |
| 313 | "AuthSessionRepository.listActive:decodeRows", | |
| 314 | ), | |
| 315 | ), | |
| 316 | Effect.flatMap((rows) => Effect.succeed(rows.map((row) => toAuthSessionRecord(row)))), | |
| 317 | ); | |
| 318 | ||
| 319 | const revoke: AuthSessionRepository["Service"]["revoke"] = (input) => | |
| 320 | revokeSessionRows(input).pipe( | |
| 321 | Effect.mapError( | |
| 322 | toPersistenceSqlOrDecodeError( | |
| 323 | "AuthSessionRepository.revoke:query", | |
| 324 | "AuthSessionRepository.revoke:decodeRows", | |
| 325 | ), | |
| 326 | ), | |
| 327 | Effect.map((rows) => rows.length > 0), | |
| 328 | ); | |
| 329 | ||
| 330 | const revokeAllExcept: AuthSessionRepository["Service"]["revokeAllExcept"] = (input) => | |
| 331 | revokeOtherSessionRows(input).pipe( | |
| 332 | Effect.mapError( | |
| 333 | toPersistenceSqlOrDecodeError( | |
| 334 | "AuthSessionRepository.revokeAllExcept:query", | |
| 335 | "AuthSessionRepository.revokeAllExcept:decodeRows", | |
| 336 | ), | |
| 337 | ), | |
| 338 | Effect.map((rows) => rows.map((row) => row.sessionId)), | |
| 339 | ); | |
| 340 | ||
| 341 | const setLastConnectedAt: AuthSessionRepository["Service"]["setLastConnectedAt"] = (input) => | |
| 342 | setLastConnectedAtRow(input).pipe( | |
| 343 | Effect.mapError( | |
| 344 | toPersistenceSqlOrDecodeError( | |
| 345 | "AuthSessionRepository.setLastConnectedAt:query", | |
| 346 | "AuthSessionRepository.setLastConnectedAt:encodeRequest", | |
| 347 | ), | |
| 348 | ), | |
| 349 | ); | |
| 350 | ||
| 351 | return { | |
| 352 | create, | |
| 353 | getById, | |
| 354 | listActive, | |
| 355 | revoke, | |
| 356 | revokeAllExcept, | |
| 357 | setLastConnectedAt, | |
| 358 | } satisfies AuthSessionRepository["Service"]; | |
| 359 | }); | |
| 360 | ||
| 361 | export const layer = Layer.effect(AuthSessionRepository, make); |
apps/server/src/persistence/Layers/AuthPairingLinks.ts deleted-220| ... | ... | @@ -1,220 +0,0 @@ |
| 1 | import * as SqlClient from "effect/unstable/sql/SqlClient"; | |
| 2 | import * as SqlSchema from "effect/unstable/sql/SqlSchema"; | |
| 3 | import * as Effect from "effect/Effect"; | |
| 4 | import * as Layer from "effect/Layer"; | |
| 5 | import * as Schema from "effect/Schema"; | |
| 6 | ||
| 7 | import { | |
| 8 | toPersistenceDecodeError, | |
| 9 | toPersistenceSqlError, | |
| 10 | type AuthPairingLinkRepositoryError, | |
| 11 | } from "../Errors.ts"; | |
| 12 | import { | |
| 13 | AuthPairingLinkRecord, | |
| 14 | AuthPairingLinkRepository, | |
| 15 | type AuthPairingLinkRepositoryShape, | |
| 16 | ConsumeAuthPairingLinkInput, | |
| 17 | CreateAuthPairingLinkInput, | |
| 18 | GetAuthPairingLinkByCredentialInput, | |
| 19 | ListActiveAuthPairingLinksInput, | |
| 20 | RevokeAuthPairingLinkInput, | |
| 21 | } from "../Services/AuthPairingLinks.ts"; | |
| 22 | ||
| 23 | function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) { | |
| 24 | return (cause: unknown): AuthPairingLinkRepositoryError => | |
| 25 | Schema.isSchemaError(cause) | |
| 26 | ? toPersistenceDecodeError(decodeOperation)(cause) | |
| 27 | : toPersistenceSqlError(sqlOperation)(cause); | |
| 28 | } | |
| 29 | ||
| 30 | const makeAuthPairingLinkRepository = Effect.gen(function* () { | |
| 31 | const sql = yield* SqlClient.SqlClient; | |
| 32 | ||
| 33 | const createPairingLinkRow = SqlSchema.void({ | |
| 34 | Request: CreateAuthPairingLinkInput, | |
| 35 | execute: (input) => | |
| 36 | sql` | |
| 37 | INSERT INTO auth_pairing_links ( | |
| 38 | id, | |
| 39 | credential, | |
| 40 | method, | |
| 41 | scopes, | |
| 42 | subject, | |
| 43 | label, | |
| 44 | proof_key_thumbprint, | |
| 45 | created_at, | |
| 46 | expires_at, | |
| 47 | consumed_at, | |
| 48 | revoked_at | |
| 49 | ) | |
| 50 | VALUES ( | |
| 51 | ${input.id}, | |
| 52 | ${input.credential}, | |
| 53 | ${input.method}, | |
| 54 | ${JSON.stringify(input.scopes)}, | |
| 55 | ${input.subject}, | |
| 56 | ${input.label}, | |
| 57 | ${input.proofKeyThumbprint}, | |
| 58 | ${input.createdAt}, | |
| 59 | ${input.expiresAt}, | |
| 60 | NULL, | |
| 61 | NULL | |
| 62 | ) | |
| 63 | `, | |
| 64 | }); | |
| 65 | ||
| 66 | const consumeAvailablePairingLinkRow = SqlSchema.findOneOption({ | |
| 67 | Request: ConsumeAuthPairingLinkInput, | |
| 68 | Result: AuthPairingLinkRecord, | |
| 69 | execute: ({ credential, proofKeyThumbprint, consumedAt, now }) => | |
| 70 | sql` | |
| 71 | UPDATE auth_pairing_links | |
| 72 | SET consumed_at = ${consumedAt} | |
| 73 | WHERE credential = ${credential} | |
| 74 | AND revoked_at IS NULL | |
| 75 | AND consumed_at IS NULL | |
| 76 | AND expires_at > ${now} | |
| 77 | AND ( | |
| 78 | proof_key_thumbprint IS NULL | |
| 79 | OR proof_key_thumbprint = ${proofKeyThumbprint} | |
| 80 | ) | |
| 81 | RETURNING | |
| 82 | id AS "id", | |
| 83 | credential AS "credential", | |
| 84 | method AS "method", | |
| 85 | scopes AS "scopes", | |
| 86 | subject AS "subject", | |
| 87 | label AS "label", | |
| 88 | proof_key_thumbprint AS "proofKeyThumbprint", | |
| 89 | created_at AS "createdAt", | |
| 90 | expires_at AS "expiresAt", | |
| 91 | consumed_at AS "consumedAt", | |
| 92 | revoked_at AS "revokedAt" | |
| 93 | `, | |
| 94 | }); | |
| 95 | ||
| 96 | const listActivePairingLinkRows = SqlSchema.findAll({ | |
| 97 | Request: ListActiveAuthPairingLinksInput, | |
| 98 | Result: AuthPairingLinkRecord, | |
| 99 | execute: ({ now }) => | |
| 100 | sql` | |
| 101 | SELECT | |
| 102 | id AS "id", | |
| 103 | credential AS "credential", | |
| 104 | method AS "method", | |
| 105 | scopes AS "scopes", | |
| 106 | subject AS "subject", | |
| 107 | label AS "label", | |
| 108 | proof_key_thumbprint AS "proofKeyThumbprint", | |
| 109 | created_at AS "createdAt", | |
| 110 | expires_at AS "expiresAt", | |
| 111 | consumed_at AS "consumedAt", | |
| 112 | revoked_at AS "revokedAt" | |
| 113 | FROM auth_pairing_links | |
| 114 | WHERE revoked_at IS NULL | |
| 115 | AND consumed_at IS NULL | |
| 116 | AND expires_at > ${now} | |
| 117 | ORDER BY created_at DESC, id DESC | |
| 118 | `, | |
| 119 | }); | |
| 120 | ||
| 121 | const revokePairingLinkRow = SqlSchema.findAll({ | |
| 122 | Request: RevokeAuthPairingLinkInput, | |
| 123 | Result: Schema.Struct({ id: Schema.String }), | |
| 124 | execute: ({ id, revokedAt }) => | |
| 125 | sql` | |
| 126 | UPDATE auth_pairing_links | |
| 127 | SET revoked_at = ${revokedAt} | |
| 128 | WHERE id = ${id} | |
| 129 | AND revoked_at IS NULL | |
| 130 | AND consumed_at IS NULL | |
| 131 | RETURNING id AS "id" | |
| 132 | `, | |
| 133 | }); | |
| 134 | ||
| 135 | const getPairingLinkRowByCredential = SqlSchema.findOneOption({ | |
| 136 | Request: GetAuthPairingLinkByCredentialInput, | |
| 137 | Result: AuthPairingLinkRecord, | |
| 138 | execute: ({ credential }) => | |
| 139 | sql` | |
| 140 | SELECT | |
| 141 | id AS "id", | |
| 142 | credential AS "credential", | |
| 143 | method AS "method", | |
| 144 | scopes AS "scopes", | |
| 145 | subject AS "subject", | |
| 146 | label AS "label", | |
| 147 | proof_key_thumbprint AS "proofKeyThumbprint", | |
| 148 | created_at AS "createdAt", | |
| 149 | expires_at AS "expiresAt", | |
| 150 | consumed_at AS "consumedAt", | |
| 151 | revoked_at AS "revokedAt" | |
| 152 | FROM auth_pairing_links | |
| 153 | WHERE credential = ${credential} | |
| 154 | `, | |
| 155 | }); | |
| 156 | ||
| 157 | const create: AuthPairingLinkRepositoryShape["create"] = (input) => | |
| 158 | createPairingLinkRow(input).pipe( | |
| 159 | Effect.mapError( | |
| 160 | toPersistenceSqlOrDecodeError( | |
| 161 | "AuthPairingLinkRepository.create:query", | |
| 162 | "AuthPairingLinkRepository.create:encodeRequest", | |
| 163 | ), | |
| 164 | ), | |
| 165 | ); | |
| 166 | ||
| 167 | const consumeAvailable: AuthPairingLinkRepositoryShape["consumeAvailable"] = (input) => | |
| 168 | consumeAvailablePairingLinkRow(input).pipe( | |
| 169 | Effect.mapError( | |
| 170 | toPersistenceSqlOrDecodeError( | |
| 171 | "AuthPairingLinkRepository.consumeAvailable:query", | |
| 172 | "AuthPairingLinkRepository.consumeAvailable:decodeRow", | |
| 173 | ), | |
| 174 | ), | |
| 175 | ); | |
| 176 | ||
| 177 | const listActive: AuthPairingLinkRepositoryShape["listActive"] = (input) => | |
| 178 | listActivePairingLinkRows(input).pipe( | |
| 179 | Effect.mapError( | |
| 180 | toPersistenceSqlOrDecodeError( | |
| 181 | "AuthPairingLinkRepository.listActive:query", | |
| 182 | "AuthPairingLinkRepository.listActive:decodeRows", | |
| 183 | ), | |
| 184 | ), | |
| 185 | ); | |
| 186 | ||
| 187 | const revoke: AuthPairingLinkRepositoryShape["revoke"] = (input) => | |
| 188 | revokePairingLinkRow(input).pipe( | |
| 189 | Effect.mapError( | |
| 190 | toPersistenceSqlOrDecodeError( | |
| 191 | "AuthPairingLinkRepository.revoke:query", | |
| 192 | "AuthPairingLinkRepository.revoke:decodeRows", | |
| 193 | ), | |
| 194 | ), | |
| 195 | Effect.map((rows) => rows.length > 0), | |
| 196 | ); | |
| 197 | ||
| 198 | const getByCredential: AuthPairingLinkRepositoryShape["getByCredential"] = (input) => | |
| 199 | getPairingLinkRowByCredential(input).pipe( | |
| 200 | Effect.mapError( | |
| 201 | toPersistenceSqlOrDecodeError( | |
| 202 | "AuthPairingLinkRepository.getByCredential:query", | |
| 203 | "AuthPairingLinkRepository.getByCredential:decodeRow", | |
| 204 | ), | |
| 205 | ), | |
| 206 | ); | |
| 207 | ||
| 208 | return { | |
| 209 | create, | |
| 210 | consumeAvailable, | |
| 211 | listActive, | |
| 212 | revoke, | |
| 213 | getByCredential, | |
| 214 | } satisfies AuthPairingLinkRepositoryShape; | |
| 215 | }); | |
| 216 | ||
| 217 | export const AuthPairingLinkRepositoryLive = Layer.effect( | |
| 218 | AuthPairingLinkRepository, | |
| 219 | makeAuthPairingLinkRepository, | |
| 220 | ); |
apps/server/src/persistence/Layers/AuthSessions.ts deleted-282| ... | ... | @@ -1,282 +0,0 @@ |
| 1 | import { AuthEnvironmentScopes, AuthSessionId, ServerAuthSessionMethod } from "@t3tools/contracts"; | |
| 2 | import * as SqlClient from "effect/unstable/sql/SqlClient"; | |
| 3 | import * as SqlSchema from "effect/unstable/sql/SqlSchema"; | |
| 4 | import * as Effect from "effect/Effect"; | |
| 5 | import * as Layer from "effect/Layer"; | |
| 6 | import * as Option from "effect/Option"; | |
| 7 | import * as Schema from "effect/Schema"; | |
| 8 | ||
| 9 | import { | |
| 10 | toPersistenceDecodeError, | |
| 11 | toPersistenceSqlError, | |
| 12 | type AuthSessionRepositoryError, | |
| 13 | } from "../Errors.ts"; | |
| 14 | import { | |
| 15 | AuthSessionRecord, | |
| 16 | AuthSessionRepository, | |
| 17 | type AuthSessionRepositoryShape, | |
| 18 | CreateAuthSessionInput, | |
| 19 | GetAuthSessionByIdInput, | |
| 20 | ListActiveAuthSessionsInput, | |
| 21 | RevokeAuthSessionInput, | |
| 22 | RevokeOtherAuthSessionsInput, | |
| 23 | SetAuthSessionLastConnectedAtInput, | |
| 24 | } from "../Services/AuthSessions.ts"; | |
| 25 | ||
| 26 | const AuthSessionDbRow = Schema.Struct({ | |
| 27 | sessionId: AuthSessionId, | |
| 28 | subject: Schema.String, | |
| 29 | scopes: Schema.fromJsonString(AuthEnvironmentScopes), | |
| 30 | method: ServerAuthSessionMethod, | |
| 31 | clientLabel: Schema.NullOr(Schema.String), | |
| 32 | clientIpAddress: Schema.NullOr(Schema.String), | |
| 33 | clientUserAgent: Schema.NullOr(Schema.String), | |
| 34 | clientDeviceType: Schema.Literals(["desktop", "mobile", "tablet", "bot", "unknown"]), | |
| 35 | clientOs: Schema.NullOr(Schema.String), | |
| 36 | clientBrowser: Schema.NullOr(Schema.String), | |
| 37 | issuedAt: Schema.DateTimeUtcFromString, | |
| 38 | expiresAt: Schema.DateTimeUtcFromString, | |
| 39 | lastConnectedAt: Schema.NullOr(Schema.DateTimeUtcFromString), | |
| 40 | revokedAt: Schema.NullOr(Schema.DateTimeUtcFromString), | |
| 41 | }); | |
| 42 | ||
| 43 | function toAuthSessionRecord(row: typeof AuthSessionDbRow.Type): AuthSessionRecord { | |
| 44 | return { | |
| 45 | sessionId: row.sessionId, | |
| 46 | subject: row.subject, | |
| 47 | scopes: row.scopes, | |
| 48 | method: row.method, | |
| 49 | client: { | |
| 50 | label: row.clientLabel, | |
| 51 | ipAddress: row.clientIpAddress, | |
| 52 | userAgent: row.clientUserAgent, | |
| 53 | deviceType: row.clientDeviceType, | |
| 54 | os: row.clientOs, | |
| 55 | browser: row.clientBrowser, | |
| 56 | }, | |
| 57 | issuedAt: row.issuedAt, | |
| 58 | expiresAt: row.expiresAt, | |
| 59 | lastConnectedAt: row.lastConnectedAt, | |
| 60 | revokedAt: row.revokedAt, | |
| 61 | }; | |
| 62 | } | |
| 63 | ||
| 64 | function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) { | |
| 65 | return (cause: unknown): AuthSessionRepositoryError => | |
| 66 | Schema.isSchemaError(cause) | |
| 67 | ? toPersistenceDecodeError(decodeOperation)(cause) | |
| 68 | : toPersistenceSqlError(sqlOperation)(cause); | |
| 69 | } | |
| 70 | ||
| 71 | const makeAuthSessionRepository = Effect.gen(function* () { | |
| 72 | const sql = yield* SqlClient.SqlClient; | |
| 73 | ||
| 74 | const createSessionRow = SqlSchema.void({ | |
| 75 | Request: CreateAuthSessionInput, | |
| 76 | execute: (input) => | |
| 77 | sql` | |
| 78 | INSERT INTO auth_sessions ( | |
| 79 | session_id, | |
| 80 | subject, | |
| 81 | scopes, | |
| 82 | method, | |
| 83 | client_label, | |
| 84 | client_ip_address, | |
| 85 | client_user_agent, | |
| 86 | client_device_type, | |
| 87 | client_os, | |
| 88 | client_browser, | |
| 89 | issued_at, | |
| 90 | expires_at, | |
| 91 | revoked_at | |
| 92 | ) | |
| 93 | VALUES ( | |
| 94 | ${input.sessionId}, | |
| 95 | ${input.subject}, | |
| 96 | ${JSON.stringify(input.scopes)}, | |
| 97 | ${input.method}, | |
| 98 | ${input.client.label}, | |
| 99 | ${input.client.ipAddress}, | |
| 100 | ${input.client.userAgent}, | |
| 101 | ${input.client.deviceType}, | |
| 102 | ${input.client.os}, | |
| 103 | ${input.client.browser}, | |
| 104 | ${input.issuedAt}, | |
| 105 | ${input.expiresAt}, | |
| 106 | NULL | |
| 107 | ) | |
| 108 | `, | |
| 109 | }); | |
| 110 | ||
| 111 | const getSessionRowById = SqlSchema.findOneOption({ | |
| 112 | Request: GetAuthSessionByIdInput, | |
| 113 | Result: AuthSessionDbRow, | |
| 114 | execute: ({ sessionId }) => | |
| 115 | sql` | |
| 116 | SELECT | |
| 117 | session_id AS "sessionId", | |
| 118 | subject AS "subject", | |
| 119 | scopes AS "scopes", | |
| 120 | method AS "method", | |
| 121 | client_label AS "clientLabel", | |
| 122 | client_ip_address AS "clientIpAddress", | |
| 123 | client_user_agent AS "clientUserAgent", | |
| 124 | client_device_type AS "clientDeviceType", | |
| 125 | client_os AS "clientOs", | |
| 126 | client_browser AS "clientBrowser", | |
| 127 | issued_at AS "issuedAt", | |
| 128 | expires_at AS "expiresAt", | |
| 129 | last_connected_at AS "lastConnectedAt", | |
| 130 | revoked_at AS "revokedAt" | |
| 131 | FROM auth_sessions | |
| 132 | WHERE session_id = ${sessionId} | |
| 133 | `, | |
| 134 | }); | |
| 135 | ||
| 136 | const listActiveSessionRows = SqlSchema.findAll({ | |
| 137 | Request: ListActiveAuthSessionsInput, | |
| 138 | Result: AuthSessionDbRow, | |
| 139 | execute: ({ now }) => | |
| 140 | sql` | |
| 141 | SELECT | |
| 142 | session_id AS "sessionId", | |
| 143 | subject AS "subject", | |
| 144 | scopes AS "scopes", | |
| 145 | method AS "method", | |
| 146 | client_label AS "clientLabel", | |
| 147 | client_ip_address AS "clientIpAddress", | |
| 148 | client_user_agent AS "clientUserAgent", | |
| 149 | client_device_type AS "clientDeviceType", | |
| 150 | client_os AS "clientOs", | |
| 151 | client_browser AS "clientBrowser", | |
| 152 | issued_at AS "issuedAt", | |
| 153 | expires_at AS "expiresAt", | |
| 154 | last_connected_at AS "lastConnectedAt", | |
| 155 | revoked_at AS "revokedAt" | |
| 156 | FROM auth_sessions | |
| 157 | WHERE revoked_at IS NULL | |
| 158 | AND expires_at > ${now} | |
| 159 | ORDER BY issued_at DESC, session_id DESC | |
| 160 | `, | |
| 161 | }); | |
| 162 | ||
| 163 | const setLastConnectedAtRow = SqlSchema.void({ | |
| 164 | Request: SetAuthSessionLastConnectedAtInput, | |
| 165 | execute: ({ sessionId, lastConnectedAt }) => | |
| 166 | sql` | |
| 167 | UPDATE auth_sessions | |
| 168 | SET last_connected_at = ${lastConnectedAt} | |
| 169 | WHERE session_id = ${sessionId} | |
| 170 | AND revoked_at IS NULL | |
| 171 | `, | |
| 172 | }); | |
| 173 | ||
| 174 | const revokeSessionRows = SqlSchema.findAll({ | |
| 175 | Request: RevokeAuthSessionInput, | |
| 176 | Result: Schema.Struct({ sessionId: AuthSessionId }), | |
| 177 | execute: ({ sessionId, revokedAt }) => | |
| 178 | sql` | |
| 179 | UPDATE auth_sessions | |
| 180 | SET revoked_at = ${revokedAt} | |
| 181 | WHERE session_id = ${sessionId} | |
| 182 | AND revoked_at IS NULL | |
| 183 | RETURNING session_id AS "sessionId" | |
| 184 | `, | |
| 185 | }); | |
| 186 | ||
| 187 | const revokeOtherSessionRows = SqlSchema.findAll({ | |
| 188 | Request: RevokeOtherAuthSessionsInput, | |
| 189 | Result: Schema.Struct({ sessionId: AuthSessionId }), | |
| 190 | execute: ({ currentSessionId, revokedAt }) => | |
| 191 | sql` | |
| 192 | UPDATE auth_sessions | |
| 193 | SET revoked_at = ${revokedAt} | |
| 194 | WHERE session_id <> ${currentSessionId} | |
| 195 | AND revoked_at IS NULL | |
| 196 | RETURNING session_id AS "sessionId" | |
| 197 | `, | |
| 198 | }); | |
| 199 | ||
| 200 | const create: AuthSessionRepositoryShape["create"] = (input) => | |
| 201 | createSessionRow(input).pipe( | |
| 202 | Effect.mapError( | |
| 203 | toPersistenceSqlOrDecodeError( | |
| 204 | "AuthSessionRepository.create:query", | |
| 205 | "AuthSessionRepository.create:encodeRequest", | |
| 206 | ), | |
| 207 | ), | |
| 208 | ); | |
| 209 | ||
| 210 | const getById: AuthSessionRepositoryShape["getById"] = (input) => | |
| 211 | getSessionRowById(input).pipe( | |
| 212 | Effect.mapError( | |
| 213 | toPersistenceSqlOrDecodeError( | |
| 214 | "AuthSessionRepository.getById:query", | |
| 215 | "AuthSessionRepository.getById:decodeRow", | |
| 216 | ), | |
| 217 | ), | |
| 218 | Effect.flatMap((rowOption) => | |
| 219 | Option.match(rowOption, { | |
| 220 | onNone: () => Effect.succeed(Option.none()), | |
| 221 | onSome: (row) => Effect.succeed(Option.some(toAuthSessionRecord(row))), | |
| 222 | }), | |
| 223 | ), | |
| 224 | ); | |
| 225 | ||
| 226 | const listActive: AuthSessionRepositoryShape["listActive"] = (input) => | |
| 227 | listActiveSessionRows(input).pipe( | |
| 228 | Effect.mapError( | |
| 229 | toPersistenceSqlOrDecodeError( | |
| 230 | "AuthSessionRepository.listActive:query", | |
| 231 | "AuthSessionRepository.listActive:decodeRows", | |
| 232 | ), | |
| 233 | ), | |
| 234 | Effect.flatMap((rows) => Effect.succeed(rows.map((row) => toAuthSessionRecord(row)))), | |
| 235 | ); | |
| 236 | ||
| 237 | const revoke: AuthSessionRepositoryShape["revoke"] = (input) => | |
| 238 | revokeSessionRows(input).pipe( | |
| 239 | Effect.mapError( | |
| 240 | toPersistenceSqlOrDecodeError( | |
| 241 | "AuthSessionRepository.revoke:query", | |
| 242 | "AuthSessionRepository.revoke:decodeRows", | |
| 243 | ), | |
| 244 | ), | |
| 245 | Effect.map((rows) => rows.length > 0), | |
| 246 | ); | |
| 247 | ||
| 248 | const revokeAllExcept: AuthSessionRepositoryShape["revokeAllExcept"] = (input) => | |
| 249 | revokeOtherSessionRows(input).pipe( | |
| 250 | Effect.mapError( | |
| 251 | toPersistenceSqlOrDecodeError( | |
| 252 | "AuthSessionRepository.revokeAllExcept:query", | |
| 253 | "AuthSessionRepository.revokeAllExcept:decodeRows", | |
| 254 | ), | |
| 255 | ), | |
| 256 | Effect.map((rows) => rows.map((row) => row.sessionId)), | |
| 257 | ); | |
| 258 | ||
| 259 | const setLastConnectedAt: AuthSessionRepositoryShape["setLastConnectedAt"] = (input) => | |
| 260 | setLastConnectedAtRow(input).pipe( | |
| 261 | Effect.mapError( | |
| 262 | toPersistenceSqlOrDecodeError( | |
| 263 | "AuthSessionRepository.setLastConnectedAt:query", | |
| 264 | "AuthSessionRepository.setLastConnectedAt:encodeRequest", | |
| 265 | ), | |
| 266 | ), | |
| 267 | ); | |
| 268 | ||
| 269 | return { | |
| 270 | create, | |
| 271 | getById, | |
| 272 | listActive, | |
| 273 | revoke, | |
| 274 | revokeAllExcept, | |
| 275 | setLastConnectedAt, | |
| 276 | } satisfies AuthSessionRepositoryShape; | |
| 277 | }); | |
| 278 | ||
| 279 | export const AuthSessionRepositoryLive = Layer.effect( | |
| 280 | AuthSessionRepository, | |
| 281 | makeAuthSessionRepository, | |
| 282 | ); |
apps/server/src/persistence/Layers/ProviderSessionRuntime.ts+2-208| ... | ... | @@ -1,208 +1,2 @@ |
| 1 | import { ThreadId } from "@t3tools/contracts"; | |
| 2 | import * as SqlClient from "effect/unstable/sql/SqlClient"; | |
| 3 | import * as SqlSchema from "effect/unstable/sql/SqlSchema"; | |
| 4 | import * as Effect from "effect/Effect"; | |
| 5 | import * as Layer from "effect/Layer"; | |
| 6 | import * as Option from "effect/Option"; | |
| 7 | import * as Schema from "effect/Schema"; | |
| 8 | import * as Struct from "effect/Struct"; | |
| 9 | ||
| 10 | import { | |
| 11 | toPersistenceDecodeError, | |
| 12 | toPersistenceSqlError, | |
| 13 | type ProviderSessionRuntimeRepositoryError, | |
| 14 | } from "../Errors.ts"; | |
| 15 | import { | |
| 16 | ProviderSessionRuntime, | |
| 17 | ProviderSessionRuntimeRepository, | |
| 18 | type ProviderSessionRuntimeRepositoryShape, | |
| 19 | } from "../Services/ProviderSessionRuntime.ts"; | |
| 20 | ||
| 21 | const ProviderSessionRuntimeDbRowSchema = ProviderSessionRuntime.mapFields( | |
| 22 | Struct.assign({ | |
| 23 | resumeCursor: Schema.NullOr(Schema.fromJsonString(Schema.Unknown)), | |
| 24 | runtimePayload: Schema.NullOr(Schema.fromJsonString(Schema.Unknown)), | |
| 25 | }), | |
| 26 | ); | |
| 27 | ||
| 28 | const decodeRuntime = Schema.decodeUnknownEffect(ProviderSessionRuntime); | |
| 29 | ||
| 30 | const GetRuntimeRequestSchema = Schema.Struct({ | |
| 31 | threadId: ThreadId, | |
| 32 | }); | |
| 33 | ||
| 34 | const DeleteRuntimeRequestSchema = GetRuntimeRequestSchema; | |
| 35 | ||
| 36 | function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) { | |
| 37 | return (cause: unknown): ProviderSessionRuntimeRepositoryError => | |
| 38 | Schema.isSchemaError(cause) | |
| 39 | ? toPersistenceDecodeError(decodeOperation)(cause) | |
| 40 | : toPersistenceSqlError(sqlOperation)(cause); | |
| 41 | } | |
| 42 | ||
| 43 | const makeProviderSessionRuntimeRepository = Effect.gen(function* () { | |
| 44 | const sql = yield* SqlClient.SqlClient; | |
| 45 | ||
| 46 | const upsertRuntimeRow = SqlSchema.void({ | |
| 47 | Request: ProviderSessionRuntimeDbRowSchema, | |
| 48 | execute: (runtime) => | |
| 49 | sql` | |
| 50 | INSERT INTO provider_session_runtime ( | |
| 51 | thread_id, | |
| 52 | provider_name, | |
| 53 | provider_instance_id, | |
| 54 | adapter_key, | |
| 55 | runtime_mode, | |
| 56 | status, | |
| 57 | last_seen_at, | |
| 58 | resume_cursor_json, | |
| 59 | runtime_payload_json | |
| 60 | ) | |
| 61 | VALUES ( | |
| 62 | ${runtime.threadId}, | |
| 63 | ${runtime.providerName}, | |
| 64 | ${runtime.providerInstanceId}, | |
| 65 | ${runtime.adapterKey}, | |
| 66 | ${runtime.runtimeMode}, | |
| 67 | ${runtime.status}, | |
| 68 | ${runtime.lastSeenAt}, | |
| 69 | ${runtime.resumeCursor}, | |
| 70 | ${runtime.runtimePayload} | |
| 71 | ) | |
| 72 | ON CONFLICT (thread_id) | |
| 73 | DO UPDATE SET | |
| 74 | provider_name = excluded.provider_name, | |
| 75 | provider_instance_id = excluded.provider_instance_id, | |
| 76 | adapter_key = excluded.adapter_key, | |
| 77 | runtime_mode = excluded.runtime_mode, | |
| 78 | status = excluded.status, | |
| 79 | last_seen_at = excluded.last_seen_at, | |
| 80 | resume_cursor_json = excluded.resume_cursor_json, | |
| 81 | runtime_payload_json = excluded.runtime_payload_json | |
| 82 | `, | |
| 83 | }); | |
| 84 | ||
| 85 | const getRuntimeRowByThreadId = SqlSchema.findOneOption({ | |
| 86 | Request: GetRuntimeRequestSchema, | |
| 87 | Result: ProviderSessionRuntimeDbRowSchema, | |
| 88 | execute: ({ threadId }) => | |
| 89 | sql` | |
| 90 | SELECT | |
| 91 | thread_id AS "threadId", | |
| 92 | provider_name AS "providerName", | |
| 93 | provider_instance_id AS "providerInstanceId", | |
| 94 | adapter_key AS "adapterKey", | |
| 95 | runtime_mode AS "runtimeMode", | |
| 96 | status, | |
| 97 | last_seen_at AS "lastSeenAt", | |
| 98 | resume_cursor_json AS "resumeCursor", | |
| 99 | runtime_payload_json AS "runtimePayload" | |
| 100 | FROM provider_session_runtime | |
| 101 | WHERE thread_id = ${threadId} | |
| 102 | `, | |
| 103 | }); | |
| 104 | ||
| 105 | const listRuntimeRows = SqlSchema.findAll({ | |
| 106 | Request: Schema.Void, | |
| 107 | Result: ProviderSessionRuntimeDbRowSchema, | |
| 108 | execute: () => | |
| 109 | sql` | |
| 110 | SELECT | |
| 111 | thread_id AS "threadId", | |
| 112 | provider_name AS "providerName", | |
| 113 | provider_instance_id AS "providerInstanceId", | |
| 114 | adapter_key AS "adapterKey", | |
| 115 | runtime_mode AS "runtimeMode", | |
| 116 | status, | |
| 117 | last_seen_at AS "lastSeenAt", | |
| 118 | resume_cursor_json AS "resumeCursor", | |
| 119 | runtime_payload_json AS "runtimePayload" | |
| 120 | FROM provider_session_runtime | |
| 121 | ORDER BY last_seen_at ASC, thread_id ASC | |
| 122 | `, | |
| 123 | }); | |
| 124 | ||
| 125 | const deleteRuntimeByThreadId = SqlSchema.void({ | |
| 126 | Request: DeleteRuntimeRequestSchema, | |
| 127 | execute: ({ threadId }) => | |
| 128 | sql` | |
| 129 | DELETE FROM provider_session_runtime | |
| 130 | WHERE thread_id = ${threadId} | |
| 131 | `, | |
| 132 | }); | |
| 133 | ||
| 134 | const upsert: ProviderSessionRuntimeRepositoryShape["upsert"] = (runtime) => | |
| 135 | upsertRuntimeRow(runtime).pipe( | |
| 136 | Effect.mapError( | |
| 137 | toPersistenceSqlOrDecodeError( | |
| 138 | "ProviderSessionRuntimeRepository.upsert:query", | |
| 139 | "ProviderSessionRuntimeRepository.upsert:encodeRequest", | |
| 140 | ), | |
| 141 | ), | |
| 142 | ); | |
| 143 | ||
| 144 | const getByThreadId: ProviderSessionRuntimeRepositoryShape["getByThreadId"] = (input) => | |
| 145 | getRuntimeRowByThreadId(input).pipe( | |
| 146 | Effect.mapError( | |
| 147 | toPersistenceSqlOrDecodeError( | |
| 148 | "ProviderSessionRuntimeRepository.getByThreadId:query", | |
| 149 | "ProviderSessionRuntimeRepository.getByThreadId:decodeRow", | |
| 150 | ), | |
| 151 | ), | |
| 152 | Effect.flatMap((runtimeRowOption) => | |
| 153 | Option.match(runtimeRowOption, { | |
| 154 | onNone: () => Effect.succeed(Option.none()), | |
| 155 | onSome: (row) => | |
| 156 | decodeRuntime(row).pipe( | |
| 157 | Effect.mapError( | |
| 158 | toPersistenceDecodeError( | |
| 159 | "ProviderSessionRuntimeRepository.getByThreadId:rowToRuntime", | |
| 160 | ), | |
| 161 | ), | |
| 162 | Effect.map((runtime) => Option.some(runtime)), | |
| 163 | ), | |
| 164 | }), | |
| 165 | ), | |
| 166 | ); | |
| 167 | ||
| 168 | const list: ProviderSessionRuntimeRepositoryShape["list"] = () => | |
| 169 | listRuntimeRows(undefined).pipe( | |
| 170 | Effect.mapError( | |
| 171 | toPersistenceSqlOrDecodeError( | |
| 172 | "ProviderSessionRuntimeRepository.list:query", | |
| 173 | "ProviderSessionRuntimeRepository.list:decodeRows", | |
| 174 | ), | |
| 175 | ), | |
| 176 | Effect.flatMap((rows) => | |
| 177 | Effect.forEach( | |
| 178 | rows, | |
| 179 | (row) => | |
| 180 | decodeRuntime(row).pipe( | |
| 181 | Effect.mapError( | |
| 182 | toPersistenceDecodeError("ProviderSessionRuntimeRepository.list:rowToRuntime"), | |
| 183 | ), | |
| 184 | ), | |
| 185 | { concurrency: "unbounded" }, | |
| 186 | ), | |
| 187 | ), | |
| 188 | ); | |
| 189 | ||
| 190 | const deleteByThreadId: ProviderSessionRuntimeRepositoryShape["deleteByThreadId"] = (input) => | |
| 191 | deleteRuntimeByThreadId(input).pipe( | |
| 192 | Effect.mapError( | |
| 193 | toPersistenceSqlError("ProviderSessionRuntimeRepository.deleteByThreadId:query"), | |
| 194 | ), | |
| 195 | ); | |
| 196 | ||
| 197 | return { | |
| 198 | upsert, | |
| 199 | getByThreadId, | |
| 200 | list, | |
| 201 | deleteByThreadId, | |
| 202 | } satisfies ProviderSessionRuntimeRepositoryShape; | |
| 203 | }); | |
| 204 | ||
| 205 | export const ProviderSessionRuntimeRepositoryLive = Layer.effect( | |
| 206 | ProviderSessionRuntimeRepository, | |
| 207 | makeProviderSessionRuntimeRepository, | |
| 208 | ); | |
| 1 | /** @deprecated Compatibility alias for the excluded orchestration integration harness. */ | |
| 2 | export { layer as ProviderSessionRuntimeRepositoryLive } from "../ProviderSessionRuntime.ts"; |
apps/server/src/persistence/NodeSqliteClient.ts+5-23| ... | ... | @@ -29,11 +29,6 @@ export const TypeId: TypeId = "~local/sqlite-node/SqliteClient"; |
| 29 | 29 | |
| 30 | 30 | export type TypeId = "~local/sqlite-node/SqliteClient"; |
| 31 | 31 | |
| 32 | /** | |
| 33 | * SqliteClient - Effect service tag for the sqlite SQL client. | |
| 34 | */ | |
| 35 | export const SqliteClient = Context.Service<Client.SqlClient>("t3/persistence/NodeSqliteClient"); | |
| 36 | ||
| 37 | 32 | export interface SqliteClientConfig { |
| 38 | 33 | readonly filename: string; |
| 39 | 34 | readonly readonly?: boolean | undefined; |
| ... | ... | @@ -251,25 +246,12 @@ const makeMemory = ( |
| 251 | 246 | export const layerConfig = ( |
| 252 | 247 | config: Config.Wrap<SqliteClientConfig>, |
| 253 | 248 | ): Layer.Layer<Client.SqlClient, Config.ConfigError> => |
| 254 | Layer.effectContext( | |
| 255 | Config.unwrap(config).pipe( | |
| 256 | Effect.flatMap(make), | |
| 257 | Effect.map((client) => | |
| 258 | Context.make(SqliteClient, client).pipe(Context.add(Client.SqlClient, client)), | |
| 259 | ), | |
| 260 | ), | |
| 261 | ).pipe(Layer.provide(Reactivity.layer)); | |
| 249 | Layer.effect(Client.SqlClient, Config.unwrap(config).pipe(Effect.flatMap(make))).pipe( | |
| 250 | Layer.provide(Reactivity.layer), | |
| 251 | ); | |
| 262 | 252 | |
| 263 | 253 | export const layer = (config: SqliteClientConfig): Layer.Layer<Client.SqlClient> => |
| 264 | Layer.effectContext( | |
| 265 | Effect.map(make(config), (client) => | |
| 266 | Context.make(SqliteClient, client).pipe(Context.add(Client.SqlClient, client)), | |
| 267 | ), | |
| 268 | ).pipe(Layer.provide(Reactivity.layer)); | |
| 254 | Layer.effect(Client.SqlClient, make(config)).pipe(Layer.provide(Reactivity.layer)); | |
| 269 | 255 | |
| 270 | 256 | export const layerMemory = (config: SqliteMemoryClientConfig = {}): Layer.Layer<Client.SqlClient> => |
| 271 | Layer.effectContext( | |
| 272 | Effect.map(makeMemory(config), (client) => | |
| 273 | Context.make(SqliteClient, client).pipe(Context.add(Client.SqlClient, client)), | |
| 274 | ), | |
| 275 | ).pipe(Layer.provide(Reactivity.layer)); | |
| 257 | Layer.effect(Client.SqlClient, makeMemory(config)).pipe(Layer.provide(Reactivity.layer)); |
apps/server/src/persistence/ProviderSessionRuntime.ts created+288| ... | ... | @@ -0,0 +1,288 @@ |
| 1 | import * as Context from "effect/Context"; | |
| 2 | import * as Effect from "effect/Effect"; | |
| 3 | import * as Layer from "effect/Layer"; | |
| 4 | import * as Option from "effect/Option"; | |
| 5 | import * as Schema from "effect/Schema"; | |
| 6 | import * as Struct from "effect/Struct"; | |
| 7 | import * as SqlClient from "effect/unstable/sql/SqlClient"; | |
| 8 | import * as SqlSchema from "effect/unstable/sql/SqlSchema"; | |
| 9 | ||
| 10 | import { | |
| 11 | IsoDateTime, | |
| 12 | ProviderInstanceId, | |
| 13 | ProviderSessionRuntimeStatus, | |
| 14 | RuntimeMode, | |
| 15 | ThreadId, | |
| 16 | } from "@t3tools/contracts"; | |
| 17 | ||
| 18 | import { | |
| 19 | type ProviderSessionRuntimeRepositoryError, | |
| 20 | toPersistenceDecodeError, | |
| 21 | toPersistenceSqlError, | |
| 22 | } from "./Errors.ts"; | |
| 23 | ||
| 24 | /** | |
| 25 | * ProviderSessionRuntimeRepository - Repository interface for provider runtime sessions. | |
| 26 | * | |
| 27 | * Owns persistence operations for provider runtime metadata and resume cursors. | |
| 28 | * | |
| 29 | * @module ProviderSessionRuntimeRepository | |
| 30 | */ | |
| 31 | ||
| 32 | export const ProviderSessionRuntime = Schema.Struct({ | |
| 33 | threadId: ThreadId, | |
| 34 | providerName: Schema.String, | |
| 35 | /** | |
| 36 | * User-defined routing key for the configured provider instance that | |
| 37 | * owns this session. Nullable only at the storage/migration boundary: | |
| 38 | * rows persisted before the driver/instance split carry only | |
| 39 | * `providerName`. Repository consumers must materialize a concrete | |
| 40 | * instance id before routing. | |
| 41 | */ | |
| 42 | providerInstanceId: Schema.NullOr(ProviderInstanceId), | |
| 43 | adapterKey: Schema.String, | |
| 44 | runtimeMode: RuntimeMode, | |
| 45 | status: ProviderSessionRuntimeStatus, | |
| 46 | lastSeenAt: IsoDateTime, | |
| 47 | resumeCursor: Schema.NullOr(Schema.Unknown), | |
| 48 | runtimePayload: Schema.NullOr(Schema.Unknown), | |
| 49 | }); | |
| 50 | export type ProviderSessionRuntime = typeof ProviderSessionRuntime.Type; | |
| 51 | ||
| 52 | export const GetProviderSessionRuntimeInput = Schema.Struct({ threadId: ThreadId }); | |
| 53 | export type GetProviderSessionRuntimeInput = typeof GetProviderSessionRuntimeInput.Type; | |
| 54 | ||
| 55 | export const DeleteProviderSessionRuntimeInput = Schema.Struct({ threadId: ThreadId }); | |
| 56 | export type DeleteProviderSessionRuntimeInput = typeof DeleteProviderSessionRuntimeInput.Type; | |
| 57 | ||
| 58 | /** | |
| 59 | * ProviderSessionRuntimeRepository - Service tag for provider runtime persistence. | |
| 60 | */ | |
| 61 | export class ProviderSessionRuntimeRepository extends Context.Service< | |
| 62 | ProviderSessionRuntimeRepository, | |
| 63 | { | |
| 64 | /** | |
| 65 | * Insert or replace a provider runtime row. | |
| 66 | * | |
| 67 | * Upserts by canonical `threadId`, including JSON payload/cursor fields. | |
| 68 | */ | |
| 69 | readonly upsert: ( | |
| 70 | runtime: ProviderSessionRuntime, | |
| 71 | ) => Effect.Effect<void, ProviderSessionRuntimeRepositoryError>; | |
| 72 | ||
| 73 | /** | |
| 74 | * Read provider runtime state by canonical thread id. | |
| 75 | */ | |
| 76 | readonly getByThreadId: ( | |
| 77 | input: GetProviderSessionRuntimeInput, | |
| 78 | ) => Effect.Effect< | |
| 79 | Option.Option<ProviderSessionRuntime>, | |
| 80 | ProviderSessionRuntimeRepositoryError | |
| 81 | >; | |
| 82 | ||
| 83 | /** | |
| 84 | * List all provider runtime rows. | |
| 85 | * | |
| 86 | * Returned in ascending last-seen order. | |
| 87 | */ | |
| 88 | readonly list: () => Effect.Effect< | |
| 89 | ReadonlyArray<ProviderSessionRuntime>, | |
| 90 | ProviderSessionRuntimeRepositoryError | |
| 91 | >; | |
| 92 | ||
| 93 | /** | |
| 94 | * Delete provider runtime state by canonical thread id. | |
| 95 | */ | |
| 96 | readonly deleteByThreadId: ( | |
| 97 | input: DeleteProviderSessionRuntimeInput, | |
| 98 | ) => Effect.Effect<void, ProviderSessionRuntimeRepositoryError>; | |
| 99 | } | |
| 100 | >()("t3/persistence/ProviderSessionRuntime/ProviderSessionRuntimeRepository") {} | |
| 101 | ||
| 102 | const ProviderSessionRuntimeDbRowSchema = ProviderSessionRuntime.mapFields( | |
| 103 | Struct.assign({ | |
| 104 | resumeCursor: Schema.NullOr(Schema.fromJsonString(Schema.Unknown)), | |
| 105 | runtimePayload: Schema.NullOr(Schema.fromJsonString(Schema.Unknown)), | |
| 106 | }), | |
| 107 | ); | |
| 108 | ||
| 109 | const decodeRuntime = Schema.decodeUnknownEffect(ProviderSessionRuntime); | |
| 110 | ||
| 111 | const GetRuntimeRequestSchema = Schema.Struct({ | |
| 112 | threadId: ThreadId, | |
| 113 | }); | |
| 114 | ||
| 115 | const DeleteRuntimeRequestSchema = GetRuntimeRequestSchema; | |
| 116 | ||
| 117 | function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) { | |
| 118 | return (cause: unknown): ProviderSessionRuntimeRepositoryError => | |
| 119 | Schema.isSchemaError(cause) | |
| 120 | ? toPersistenceDecodeError(decodeOperation)(cause) | |
| 121 | : toPersistenceSqlError(sqlOperation)(cause); | |
| 122 | } | |
| 123 | ||
| 124 | export const make = Effect.gen(function* () { | |
| 125 | const sql = yield* SqlClient.SqlClient; | |
| 126 | ||
| 127 | const upsertRuntimeRow = SqlSchema.void({ | |
| 128 | Request: ProviderSessionRuntimeDbRowSchema, | |
| 129 | execute: (runtime) => | |
| 130 | sql` | |
| 131 | INSERT INTO provider_session_runtime ( | |
| 132 | thread_id, | |
| 133 | provider_name, | |
| 134 | provider_instance_id, | |
| 135 | adapter_key, | |
| 136 | runtime_mode, | |
| 137 | status, | |
| 138 | last_seen_at, | |
| 139 | resume_cursor_json, | |
| 140 | runtime_payload_json | |
| 141 | ) | |
| 142 | VALUES ( | |
| 143 | ${runtime.threadId}, | |
| 144 | ${runtime.providerName}, | |
| 145 | ${runtime.providerInstanceId}, | |
| 146 | ${runtime.adapterKey}, | |
| 147 | ${runtime.runtimeMode}, | |
| 148 | ${runtime.status}, | |
| 149 | ${runtime.lastSeenAt}, | |
| 150 | ${runtime.resumeCursor}, | |
| 151 | ${runtime.runtimePayload} | |
| 152 | ) | |
| 153 | ON CONFLICT (thread_id) | |
| 154 | DO UPDATE SET | |
| 155 | provider_name = excluded.provider_name, | |
| 156 | provider_instance_id = excluded.provider_instance_id, | |
| 157 | adapter_key = excluded.adapter_key, | |
| 158 | runtime_mode = excluded.runtime_mode, | |
| 159 | status = excluded.status, | |
| 160 | last_seen_at = excluded.last_seen_at, | |
| 161 | resume_cursor_json = excluded.resume_cursor_json, | |
| 162 | runtime_payload_json = excluded.runtime_payload_json | |
| 163 | `, | |
| 164 | }); | |
| 165 | ||
| 166 | const getRuntimeRowByThreadId = SqlSchema.findOneOption({ | |
| 167 | Request: GetRuntimeRequestSchema, | |
| 168 | Result: ProviderSessionRuntimeDbRowSchema, | |
| 169 | execute: ({ threadId }) => | |
| 170 | sql` | |
| 171 | SELECT | |
| 172 | thread_id AS "threadId", | |
| 173 | provider_name AS "providerName", | |
| 174 | provider_instance_id AS "providerInstanceId", | |
| 175 | adapter_key AS "adapterKey", | |
| 176 | runtime_mode AS "runtimeMode", | |
| 177 | status, | |
| 178 | last_seen_at AS "lastSeenAt", | |
| 179 | resume_cursor_json AS "resumeCursor", | |
| 180 | runtime_payload_json AS "runtimePayload" | |
| 181 | FROM provider_session_runtime | |
| 182 | WHERE thread_id = ${threadId} | |
| 183 | `, | |
| 184 | }); | |
| 185 | ||
| 186 | const listRuntimeRows = SqlSchema.findAll({ | |
| 187 | Request: Schema.Void, | |
| 188 | Result: ProviderSessionRuntimeDbRowSchema, | |
| 189 | execute: () => | |
| 190 | sql` | |
| 191 | SELECT | |
| 192 | thread_id AS "threadId", | |
| 193 | provider_name AS "providerName", | |
| 194 | provider_instance_id AS "providerInstanceId", | |
| 195 | adapter_key AS "adapterKey", | |
| 196 | runtime_mode AS "runtimeMode", | |
| 197 | status, | |
| 198 | last_seen_at AS "lastSeenAt", | |
| 199 | resume_cursor_json AS "resumeCursor", | |
| 200 | runtime_payload_json AS "runtimePayload" | |
| 201 | FROM provider_session_runtime | |
| 202 | ORDER BY last_seen_at ASC, thread_id ASC | |
| 203 | `, | |
| 204 | }); | |
| 205 | ||
| 206 | const deleteRuntimeByThreadId = SqlSchema.void({ | |
| 207 | Request: DeleteRuntimeRequestSchema, | |
| 208 | execute: ({ threadId }) => | |
| 209 | sql` | |
| 210 | DELETE FROM provider_session_runtime | |
| 211 | WHERE thread_id = ${threadId} | |
| 212 | `, | |
| 213 | }); | |
| 214 | ||
| 215 | const upsert: ProviderSessionRuntimeRepository["Service"]["upsert"] = (runtime) => | |
| 216 | upsertRuntimeRow(runtime).pipe( | |
| 217 | Effect.mapError( | |
| 218 | toPersistenceSqlOrDecodeError( | |
| 219 | "ProviderSessionRuntimeRepository.upsert:query", | |
| 220 | "ProviderSessionRuntimeRepository.upsert:encodeRequest", | |
| 221 | ), | |
| 222 | ), | |
| 223 | ); | |
| 224 | ||
| 225 | const getByThreadId: ProviderSessionRuntimeRepository["Service"]["getByThreadId"] = (input) => | |
| 226 | getRuntimeRowByThreadId(input).pipe( | |
| 227 | Effect.mapError( | |
| 228 | toPersistenceSqlOrDecodeError( | |
| 229 | "ProviderSessionRuntimeRepository.getByThreadId:query", | |
| 230 | "ProviderSessionRuntimeRepository.getByThreadId:decodeRow", | |
| 231 | ), | |
| 232 | ), | |
| 233 | Effect.flatMap((runtimeRowOption) => | |
| 234 | Option.match(runtimeRowOption, { | |
| 235 | onNone: () => Effect.succeed(Option.none()), | |
| 236 | onSome: (row) => | |
| 237 | decodeRuntime(row).pipe( | |
| 238 | Effect.mapError( | |
| 239 | toPersistenceDecodeError( | |
| 240 | "ProviderSessionRuntimeRepository.getByThreadId:rowToRuntime", | |
| 241 | ), | |
| 242 | ), | |
| 243 | Effect.map((runtime) => Option.some(runtime)), | |
| 244 | ), | |
| 245 | }), | |
| 246 | ), | |
| 247 | ); | |
| 248 | ||
| 249 | const list: ProviderSessionRuntimeRepository["Service"]["list"] = () => | |
| 250 | listRuntimeRows(undefined).pipe( | |
| 251 | Effect.mapError( | |
| 252 | toPersistenceSqlOrDecodeError( | |
| 253 | "ProviderSessionRuntimeRepository.list:query", | |
| 254 | "ProviderSessionRuntimeRepository.list:decodeRows", | |
| 255 | ), | |
| 256 | ), | |
| 257 | Effect.flatMap((rows) => | |
| 258 | Effect.forEach( | |
| 259 | rows, | |
| 260 | (row) => | |
| 261 | decodeRuntime(row).pipe( | |
| 262 | Effect.mapError( | |
| 263 | toPersistenceDecodeError("ProviderSessionRuntimeRepository.list:rowToRuntime"), | |
| 264 | ), | |
| 265 | ), | |
| 266 | { concurrency: "unbounded" }, | |
| 267 | ), | |
| 268 | ), | |
| 269 | ); | |
| 270 | ||
| 271 | const deleteByThreadId: ProviderSessionRuntimeRepository["Service"]["deleteByThreadId"] = ( | |
| 272 | input, | |
| 273 | ) => | |
| 274 | deleteRuntimeByThreadId(input).pipe( | |
| 275 | Effect.mapError( | |
| 276 | toPersistenceSqlError("ProviderSessionRuntimeRepository.deleteByThreadId:query"), | |
| 277 | ), | |
| 278 | ); | |
| 279 | ||
| 280 | return { | |
| 281 | upsert, | |
| 282 | getByThreadId, | |
| 283 | list, | |
| 284 | deleteByThreadId, | |
| 285 | } satisfies ProviderSessionRuntimeRepository["Service"]; | |
| 286 | }); | |
| 287 | ||
| 288 | export const layer = Layer.effect(ProviderSessionRuntimeRepository, make); |
apps/server/src/persistence/Services/AuthPairingLinks.ts deleted-82| ... | ... | @@ -1,82 +0,0 @@ |
| 1 | import * as Option from "effect/Option"; | |
| 2 | import * as Schema from "effect/Schema"; | |
| 3 | import * as Context from "effect/Context"; | |
| 4 | import type * as Effect from "effect/Effect"; | |
| 5 | import { AuthEnvironmentScopes } from "@t3tools/contracts"; | |
| 6 | ||
| 7 | import type { AuthPairingLinkRepositoryError } from "../Errors.ts"; | |
| 8 | ||
| 9 | export const AuthPairingLinkRecord = Schema.Struct({ | |
| 10 | id: Schema.String, | |
| 11 | credential: Schema.String, | |
| 12 | method: Schema.Literals(["desktop-bootstrap", "one-time-token"]), | |
| 13 | scopes: Schema.fromJsonString(AuthEnvironmentScopes), | |
| 14 | subject: Schema.String, | |
| 15 | label: Schema.NullOr(Schema.String), | |
| 16 | proofKeyThumbprint: Schema.NullOr(Schema.String), | |
| 17 | createdAt: Schema.DateTimeUtcFromString, | |
| 18 | expiresAt: Schema.DateTimeUtcFromString, | |
| 19 | consumedAt: Schema.NullOr(Schema.DateTimeUtcFromString), | |
| 20 | revokedAt: Schema.NullOr(Schema.DateTimeUtcFromString), | |
| 21 | }); | |
| 22 | export type AuthPairingLinkRecord = typeof AuthPairingLinkRecord.Type; | |
| 23 | ||
| 24 | export const CreateAuthPairingLinkInput = Schema.Struct({ | |
| 25 | id: Schema.String, | |
| 26 | credential: Schema.String, | |
| 27 | method: Schema.Literals(["desktop-bootstrap", "one-time-token"]), | |
| 28 | scopes: AuthEnvironmentScopes, | |
| 29 | subject: Schema.String, | |
| 30 | label: Schema.NullOr(Schema.String), | |
| 31 | proofKeyThumbprint: Schema.NullOr(Schema.String), | |
| 32 | createdAt: Schema.DateTimeUtcFromString, | |
| 33 | expiresAt: Schema.DateTimeUtcFromString, | |
| 34 | }); | |
| 35 | export type CreateAuthPairingLinkInput = typeof CreateAuthPairingLinkInput.Type; | |
| 36 | ||
| 37 | export const ConsumeAuthPairingLinkInput = Schema.Struct({ | |
| 38 | credential: Schema.String, | |
| 39 | proofKeyThumbprint: Schema.NullOr(Schema.String), | |
| 40 | consumedAt: Schema.DateTimeUtcFromString, | |
| 41 | now: Schema.DateTimeUtcFromString, | |
| 42 | }); | |
| 43 | export type ConsumeAuthPairingLinkInput = typeof ConsumeAuthPairingLinkInput.Type; | |
| 44 | ||
| 45 | export const ListActiveAuthPairingLinksInput = Schema.Struct({ | |
| 46 | now: Schema.DateTimeUtcFromString, | |
| 47 | }); | |
| 48 | export type ListActiveAuthPairingLinksInput = typeof ListActiveAuthPairingLinksInput.Type; | |
| 49 | ||
| 50 | export const RevokeAuthPairingLinkInput = Schema.Struct({ | |
| 51 | id: Schema.String, | |
| 52 | revokedAt: Schema.DateTimeUtcFromString, | |
| 53 | }); | |
| 54 | export type RevokeAuthPairingLinkInput = typeof RevokeAuthPairingLinkInput.Type; | |
| 55 | ||
| 56 | export const GetAuthPairingLinkByCredentialInput = Schema.Struct({ | |
| 57 | credential: Schema.String, | |
| 58 | }); | |
| 59 | export type GetAuthPairingLinkByCredentialInput = typeof GetAuthPairingLinkByCredentialInput.Type; | |
| 60 | ||
| 61 | export interface AuthPairingLinkRepositoryShape { | |
| 62 | readonly create: ( | |
| 63 | input: CreateAuthPairingLinkInput, | |
| 64 | ) => Effect.Effect<void, AuthPairingLinkRepositoryError>; | |
| 65 | readonly consumeAvailable: ( | |
| 66 | input: ConsumeAuthPairingLinkInput, | |
| 67 | ) => Effect.Effect<Option.Option<AuthPairingLinkRecord>, AuthPairingLinkRepositoryError>; | |
| 68 | readonly listActive: ( | |
| 69 | input: ListActiveAuthPairingLinksInput, | |
| 70 | ) => Effect.Effect<ReadonlyArray<AuthPairingLinkRecord>, AuthPairingLinkRepositoryError>; | |
| 71 | readonly revoke: ( | |
| 72 | input: RevokeAuthPairingLinkInput, | |
| 73 | ) => Effect.Effect<boolean, AuthPairingLinkRepositoryError>; | |
| 74 | readonly getByCredential: ( | |
| 75 | input: GetAuthPairingLinkByCredentialInput, | |
| 76 | ) => Effect.Effect<Option.Option<AuthPairingLinkRecord>, AuthPairingLinkRepositoryError>; | |
| 77 | } | |
| 78 | ||
| 79 | export class AuthPairingLinkRepository extends Context.Service< | |
| 80 | AuthPairingLinkRepository, | |
| 81 | AuthPairingLinkRepositoryShape | |
| 82 | >()("t3/persistence/Services/AuthPairingLinks/AuthPairingLinkRepository") {} |
apps/server/src/persistence/Services/AuthSessions.ts deleted-100| ... | ... | @@ -1,100 +0,0 @@ |
| 1 | import { | |
| 2 | AuthClientMetadataDeviceType, | |
| 3 | AuthEnvironmentScopes, | |
| 4 | AuthSessionId, | |
| 5 | ServerAuthSessionMethod, | |
| 6 | } from "@t3tools/contracts"; | |
| 7 | import * as Option from "effect/Option"; | |
| 8 | import * as Schema from "effect/Schema"; | |
| 9 | import * as Context from "effect/Context"; | |
| 10 | import type * as Effect from "effect/Effect"; | |
| 11 | ||
| 12 | import type { AuthSessionRepositoryError } from "../Errors.ts"; | |
| 13 | ||
| 14 | export const AuthSessionClientMetadataRecord = Schema.Struct({ | |
| 15 | label: Schema.NullOr(Schema.String), | |
| 16 | ipAddress: Schema.NullOr(Schema.String), | |
| 17 | userAgent: Schema.NullOr(Schema.String), | |
| 18 | deviceType: AuthClientMetadataDeviceType, | |
| 19 | os: Schema.NullOr(Schema.String), | |
| 20 | browser: Schema.NullOr(Schema.String), | |
| 21 | }); | |
| 22 | export type AuthSessionClientMetadataRecord = typeof AuthSessionClientMetadataRecord.Type; | |
| 23 | ||
| 24 | export const AuthSessionRecord = Schema.Struct({ | |
| 25 | sessionId: AuthSessionId, | |
| 26 | subject: Schema.String, | |
| 27 | scopes: AuthEnvironmentScopes, | |
| 28 | method: ServerAuthSessionMethod, | |
| 29 | client: AuthSessionClientMetadataRecord, | |
| 30 | issuedAt: Schema.DateTimeUtcFromString, | |
| 31 | expiresAt: Schema.DateTimeUtcFromString, | |
| 32 | lastConnectedAt: Schema.NullOr(Schema.DateTimeUtcFromString), | |
| 33 | revokedAt: Schema.NullOr(Schema.DateTimeUtcFromString), | |
| 34 | }); | |
| 35 | export type AuthSessionRecord = typeof AuthSessionRecord.Type; | |
| 36 | ||
| 37 | export const CreateAuthSessionInput = Schema.Struct({ | |
| 38 | sessionId: AuthSessionId, | |
| 39 | subject: Schema.String, | |
| 40 | scopes: AuthEnvironmentScopes, | |
| 41 | method: ServerAuthSessionMethod, | |
| 42 | client: AuthSessionClientMetadataRecord, | |
| 43 | issuedAt: Schema.DateTimeUtcFromString, | |
| 44 | expiresAt: Schema.DateTimeUtcFromString, | |
| 45 | }); | |
| 46 | export type CreateAuthSessionInput = typeof CreateAuthSessionInput.Type; | |
| 47 | ||
| 48 | export const GetAuthSessionByIdInput = Schema.Struct({ | |
| 49 | sessionId: AuthSessionId, | |
| 50 | }); | |
| 51 | export type GetAuthSessionByIdInput = typeof GetAuthSessionByIdInput.Type; | |
| 52 | ||
| 53 | export const ListActiveAuthSessionsInput = Schema.Struct({ | |
| 54 | now: Schema.DateTimeUtcFromString, | |
| 55 | }); | |
| 56 | export type ListActiveAuthSessionsInput = typeof ListActiveAuthSessionsInput.Type; | |
| 57 | ||
| 58 | export const RevokeAuthSessionInput = Schema.Struct({ | |
| 59 | sessionId: AuthSessionId, | |
| 60 | revokedAt: Schema.DateTimeUtcFromString, | |
| 61 | }); | |
| 62 | export type RevokeAuthSessionInput = typeof RevokeAuthSessionInput.Type; | |
| 63 | ||
| 64 | export const RevokeOtherAuthSessionsInput = Schema.Struct({ | |
| 65 | currentSessionId: AuthSessionId, | |
| 66 | revokedAt: Schema.DateTimeUtcFromString, | |
| 67 | }); | |
| 68 | export type RevokeOtherAuthSessionsInput = typeof RevokeOtherAuthSessionsInput.Type; | |
| 69 | ||
| 70 | export const SetAuthSessionLastConnectedAtInput = Schema.Struct({ | |
| 71 | sessionId: AuthSessionId, | |
| 72 | lastConnectedAt: Schema.DateTimeUtcFromString, | |
| 73 | }); | |
| 74 | export type SetAuthSessionLastConnectedAtInput = typeof SetAuthSessionLastConnectedAtInput.Type; | |
| 75 | ||
| 76 | export interface AuthSessionRepositoryShape { | |
| 77 | readonly create: ( | |
| 78 | input: CreateAuthSessionInput, | |
| 79 | ) => Effect.Effect<void, AuthSessionRepositoryError>; | |
| 80 | readonly getById: ( | |
| 81 | input: GetAuthSessionByIdInput, | |
| 82 | ) => Effect.Effect<Option.Option<AuthSessionRecord>, AuthSessionRepositoryError>; | |
| 83 | readonly listActive: ( | |
| 84 | input: ListActiveAuthSessionsInput, | |
| 85 | ) => Effect.Effect<ReadonlyArray<AuthSessionRecord>, AuthSessionRepositoryError>; | |
| 86 | readonly revoke: ( | |
| 87 | input: RevokeAuthSessionInput, | |
| 88 | ) => Effect.Effect<boolean, AuthSessionRepositoryError>; | |
| 89 | readonly revokeAllExcept: ( | |
| 90 | input: RevokeOtherAuthSessionsInput, | |
| 91 | ) => Effect.Effect<ReadonlyArray<AuthSessionId>, AuthSessionRepositoryError>; | |
| 92 | readonly setLastConnectedAt: ( | |
| 93 | input: SetAuthSessionLastConnectedAtInput, | |
| 94 | ) => Effect.Effect<void, AuthSessionRepositoryError>; | |
| 95 | } | |
| 96 | ||
| 97 | export class AuthSessionRepository extends Context.Service< | |
| 98 | AuthSessionRepository, | |
| 99 | AuthSessionRepositoryShape | |
| 100 | >()("t3/persistence/Services/AuthSessions/AuthSessionRepository") {} |
apps/server/src/persistence/Services/ProviderSessionRuntime.ts deleted-92| ... | ... | @@ -1,92 +0,0 @@ |
| 1 | /** | |
| 2 | * ProviderSessionRuntimeRepository - Repository interface for provider runtime sessions. | |
| 3 | * | |
| 4 | * Owns persistence operations for provider runtime metadata and resume cursors. | |
| 5 | * | |
| 6 | * @module ProviderSessionRuntimeRepository | |
| 7 | */ | |
| 8 | import { | |
| 9 | IsoDateTime, | |
| 10 | ProviderInstanceId, | |
| 11 | ProviderSessionRuntimeStatus, | |
| 12 | RuntimeMode, | |
| 13 | ThreadId, | |
| 14 | } from "@t3tools/contracts"; | |
| 15 | import * as Option from "effect/Option"; | |
| 16 | import * as Schema from "effect/Schema"; | |
| 17 | import * as Context from "effect/Context"; | |
| 18 | import type * as Effect from "effect/Effect"; | |
| 19 | ||
| 20 | import type { ProviderSessionRuntimeRepositoryError } from "../Errors.ts"; | |
| 21 | ||
| 22 | export const ProviderSessionRuntime = Schema.Struct({ | |
| 23 | threadId: ThreadId, | |
| 24 | providerName: Schema.String, | |
| 25 | /** | |
| 26 | * User-defined routing key for the configured provider instance that | |
| 27 | * owns this session. Nullable only at the storage/migration boundary: | |
| 28 | * rows persisted before the driver/instance split carry only | |
| 29 | * `providerName`. Repository consumers must materialize a concrete | |
| 30 | * instance id before routing. | |
| 31 | */ | |
| 32 | providerInstanceId: Schema.NullOr(ProviderInstanceId), | |
| 33 | adapterKey: Schema.String, | |
| 34 | runtimeMode: RuntimeMode, | |
| 35 | status: ProviderSessionRuntimeStatus, | |
| 36 | lastSeenAt: IsoDateTime, | |
| 37 | resumeCursor: Schema.NullOr(Schema.Unknown), | |
| 38 | runtimePayload: Schema.NullOr(Schema.Unknown), | |
| 39 | }); | |
| 40 | export type ProviderSessionRuntime = typeof ProviderSessionRuntime.Type; | |
| 41 | ||
| 42 | export const GetProviderSessionRuntimeInput = Schema.Struct({ threadId: ThreadId }); | |
| 43 | export type GetProviderSessionRuntimeInput = typeof GetProviderSessionRuntimeInput.Type; | |
| 44 | ||
| 45 | export const DeleteProviderSessionRuntimeInput = Schema.Struct({ threadId: ThreadId }); | |
| 46 | export type DeleteProviderSessionRuntimeInput = typeof DeleteProviderSessionRuntimeInput.Type; | |
| 47 | ||
| 48 | /** | |
| 49 | * ProviderSessionRuntimeRepositoryShape - Service API for provider runtime records. | |
| 50 | */ | |
| 51 | export interface ProviderSessionRuntimeRepositoryShape { | |
| 52 | /** | |
| 53 | * Insert or replace a provider runtime row. | |
| 54 | * | |
| 55 | * Upserts by canonical `threadId`, including JSON payload/cursor fields. | |
| 56 | */ | |
| 57 | readonly upsert: ( | |
| 58 | runtime: ProviderSessionRuntime, | |
| 59 | ) => Effect.Effect<void, ProviderSessionRuntimeRepositoryError>; | |
| 60 | ||
| 61 | /** | |
| 62 | * Read provider runtime state by canonical thread id. | |
| 63 | */ | |
| 64 | readonly getByThreadId: ( | |
| 65 | input: GetProviderSessionRuntimeInput, | |
| 66 | ) => Effect.Effect<Option.Option<ProviderSessionRuntime>, ProviderSessionRuntimeRepositoryError>; | |
| 67 | ||
| 68 | /** | |
| 69 | * List all provider runtime rows. | |
| 70 | * | |
| 71 | * Returned in ascending last-seen order. | |
| 72 | */ | |
| 73 | readonly list: () => Effect.Effect< | |
| 74 | ReadonlyArray<ProviderSessionRuntime>, | |
| 75 | ProviderSessionRuntimeRepositoryError | |
| 76 | >; | |
| 77 | ||
| 78 | /** | |
| 79 | * Delete provider runtime state by canonical thread id. | |
| 80 | */ | |
| 81 | readonly deleteByThreadId: ( | |
| 82 | input: DeleteProviderSessionRuntimeInput, | |
| 83 | ) => Effect.Effect<void, ProviderSessionRuntimeRepositoryError>; | |
| 84 | } | |
| 85 | ||
| 86 | /** | |
| 87 | * ProviderSessionRuntimeRepository - Service tag for provider runtime persistence. | |
| 88 | */ | |
| 89 | export class ProviderSessionRuntimeRepository extends Context.Service< | |
| 90 | ProviderSessionRuntimeRepository, | |
| 91 | ProviderSessionRuntimeRepositoryShape | |
| 92 | >()("t3/persistence/Services/ProviderSessionRuntime/ProviderSessionRuntimeRepository") {} |
apps/server/src/provider/Layers/ProviderService.test.ts+16-17| ... | ... | @@ -53,8 +53,7 @@ import { makeProviderServiceLive } from "./ProviderService.ts"; |
| 53 | 53 | import { NoOpProviderEventLoggers, ProviderEventLoggers } from "./ProviderEventLoggers.ts"; |
| 54 | 54 | import { ProviderSessionDirectoryLive } from "./ProviderSessionDirectory.ts"; |
| 55 | 55 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 56 | import { ProviderSessionRuntimeRepositoryLive } from "../../persistence/Layers/ProviderSessionRuntime.ts"; | |
| 57 | import { ProviderSessionRuntimeRepository } from "../../persistence/Services/ProviderSessionRuntime.ts"; | |
| 56 | import * as ProviderSessionRuntime from "../../persistence/ProviderSessionRuntime.ts"; | |
| 58 | 57 | import { |
| 59 | 58 | makeSqlitePersistenceLive, |
| 60 | 59 | SqlitePersistenceMemory, |
| ... | ... | @@ -282,7 +281,7 @@ function makeProviderServiceLayer() { |
| 282 | 281 | }); |
| 283 | 282 | |
| 284 | 283 | const providerAdapterLayer = Layer.succeed(ProviderAdapterRegistry, registry); |
| 285 | const runtimeRepositoryLayer = ProviderSessionRuntimeRepositoryLive.pipe( | |
| 284 | const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe( | |
| 286 | 285 | Layer.provide(SqlitePersistenceMemory), |
| 287 | 286 | ); |
| 288 | 287 | const directoryLayer = ProviderSessionDirectoryLive.pipe(Layer.provide(runtimeRepositoryLayer)); |
| ... | ... | @@ -327,7 +326,7 @@ it.effect("ProviderServiceLive catches stopAll failures during shutdown", () => |
| 327 | 326 | [CODEX_DRIVER]: codex.adapter, |
| 328 | 327 | }); |
| 329 | 328 | const providerAdapterLayer = Layer.succeed(ProviderAdapterRegistry, registry); |
| 330 | const runtimeRepositoryLayer = ProviderSessionRuntimeRepositoryLive.pipe( | |
| 329 | const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe( | |
| 331 | 330 | Layer.provide(SqlitePersistenceMemory), |
| 332 | 331 | ); |
| 333 | 332 | const directoryLayer = ProviderSessionDirectoryLive.pipe(Layer.provide(runtimeRepositoryLayer)); |
| ... | ... | @@ -379,7 +378,7 @@ it.effect("ProviderServiceLive rejects new sessions for disabled providers", () |
| 379 | 378 | : registryBase.getInstanceInfo(instanceId), |
| 380 | 379 | }; |
| 381 | 380 | const providerAdapterLayer = Layer.succeed(ProviderAdapterRegistry, registry); |
| 382 | const runtimeRepositoryLayer = ProviderSessionRuntimeRepositoryLive.pipe( | |
| 381 | const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe( | |
| 383 | 382 | Layer.provide(SqlitePersistenceMemory), |
| 384 | 383 | ); |
| 385 | 384 | const directoryLayer = ProviderSessionDirectoryLive.pipe(Layer.provide(runtimeRepositoryLayer)); |
| ... | ... | @@ -453,7 +452,7 @@ it.effect( |
| 453 | 452 | }, |
| 454 | 453 | }, |
| 455 | 454 | }); |
| 456 | const runtimeRepositoryLayer = ProviderSessionRuntimeRepositoryLive.pipe( | |
| 455 | const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe( | |
| 457 | 456 | Layer.provide(SqlitePersistenceMemory), |
| 458 | 457 | ); |
| 459 | 458 | const directoryLayer = ProviderSessionDirectoryLive.pipe( |
| ... | ... | @@ -517,7 +516,7 @@ it.effect("ProviderServiceLive rejects new sessions for disabled custom instance |
| 517 | 516 | ), |
| 518 | 517 | }; |
| 519 | 518 | const providerAdapterLayer = Layer.succeed(ProviderAdapterRegistry, registry); |
| 520 | const runtimeRepositoryLayer = ProviderSessionRuntimeRepositoryLive.pipe( | |
| 519 | const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe( | |
| 521 | 520 | Layer.provide(SqlitePersistenceMemory), |
| 522 | 521 | ); |
| 523 | 522 | const directoryLayer = ProviderSessionDirectoryLive.pipe(Layer.provide(runtimeRepositoryLayer)); |
| ... | ... | @@ -557,7 +556,7 @@ it.effect("ProviderServiceLive writes canonical events to the emitting thread se |
| 557 | 556 | const registry = makeAdapterRegistryMock({ |
| 558 | 557 | [ProviderDriverKind.make("codex")]: codex.adapter, |
| 559 | 558 | }); |
| 560 | const runtimeRepositoryLayer = ProviderSessionRuntimeRepositoryLive.pipe( | |
| 559 | const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe( | |
| 561 | 560 | Layer.provide(SqlitePersistenceMemory), |
| 562 | 561 | ); |
| 563 | 562 | const directoryLayer = ProviderSessionDirectoryLive.pipe(Layer.provide(runtimeRepositoryLayer)); |
| ... | ... | @@ -612,7 +611,7 @@ it.effect("ProviderServiceLive keeps persisted resumable sessions on startup", ( |
| 612 | 611 | }); |
| 613 | 612 | |
| 614 | 613 | const persistenceLayer = makeSqlitePersistenceLive(dbPath); |
| 615 | const runtimeRepositoryLayer = ProviderSessionRuntimeRepositoryLive.pipe( | |
| 614 | const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe( | |
| 616 | 615 | Layer.provide(persistenceLayer), |
| 617 | 616 | ); |
| 618 | 617 | const directoryLayer = ProviderSessionDirectoryLive.pipe(Layer.provide(runtimeRepositoryLayer)); |
| ... | ... | @@ -643,7 +642,7 @@ it.effect("ProviderServiceLive keeps persisted resumable sessions on startup", ( |
| 643 | 642 | assert.equal(persistedProvider, "codex"); |
| 644 | 643 | |
| 645 | 644 | const runtime = yield* Effect.gen(function* () { |
| 646 | const repository = yield* ProviderSessionRuntimeRepository; | |
| 645 | const repository = yield* ProviderSessionRuntime.ProviderSessionRuntimeRepository; | |
| 647 | 646 | return yield* repository.getByThreadId({ |
| 648 | 647 | threadId: asThreadId("thread-stale"), |
| 649 | 648 | }); |
| ... | ... | @@ -671,7 +670,7 @@ it.effect( |
| 671 | 670 | const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-provider-service-restart-")); |
| 672 | 671 | const dbPath = path.join(tempDir, "orchestration.sqlite"); |
| 673 | 672 | const persistenceLayer = makeSqlitePersistenceLive(dbPath); |
| 674 | const runtimeRepositoryLayer = ProviderSessionRuntimeRepositoryLive.pipe( | |
| 673 | const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe( | |
| 675 | 674 | Layer.provide(persistenceLayer), |
| 676 | 675 | ); |
| 677 | 676 | |
| ... | ... | @@ -717,7 +716,7 @@ it.effect( |
| 717 | 716 | }).pipe(Effect.provide(firstProviderLayer)); |
| 718 | 717 | |
| 719 | 718 | const persistedAfterStopAll = yield* Effect.gen(function* () { |
| 720 | const repository = yield* ProviderSessionRuntimeRepository; | |
| 719 | const repository = yield* ProviderSessionRuntime.ProviderSessionRuntimeRepository; | |
| 721 | 720 | return yield* repository.getByThreadId({ |
| 722 | 721 | threadId: startedSession.threadId, |
| 723 | 722 | }); |
| ... | ... | @@ -909,7 +908,7 @@ routing.layer("ProviderServiceLive routing", (it) => { |
| 909 | 908 | it.effect("preserves the persisted binding when stopping a session", () => |
| 910 | 909 | Effect.gen(function* () { |
| 911 | 910 | const provider = yield* ProviderService; |
| 912 | const runtimeRepository = yield* ProviderSessionRuntimeRepository; | |
| 911 | const runtimeRepository = yield* ProviderSessionRuntime.ProviderSessionRuntimeRepository; | |
| 913 | 912 | |
| 914 | 913 | const initial = yield* provider.startSession(asThreadId("thread-reap-preserve"), { |
| 915 | 914 | provider: ProviderDriverKind.make("codex"), |
| ... | ... | @@ -1179,7 +1178,7 @@ routing.layer("ProviderServiceLive routing", (it) => { |
| 1179 | 1178 | it.effect("persists runtime status transitions in provider_session_runtime", () => |
| 1180 | 1179 | Effect.gen(function* () { |
| 1181 | 1180 | const provider = yield* ProviderService; |
| 1182 | const runtimeRepository = yield* ProviderSessionRuntimeRepository; | |
| 1181 | const runtimeRepository = yield* ProviderSessionRuntime.ProviderSessionRuntimeRepository; | |
| 1183 | 1182 | |
| 1184 | 1183 | const threadId = asThreadId("thread-runtime-status"); |
| 1185 | 1184 | const session = yield* provider.startSession(threadId, { |
| ... | ... | @@ -1226,7 +1225,7 @@ routing.layer("ProviderServiceLive routing", (it) => { |
| 1226 | 1225 | const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-provider-service-start-")); |
| 1227 | 1226 | const dbPath = path.join(tempDir, "orchestration.sqlite"); |
| 1228 | 1227 | const persistenceLayer = makeSqlitePersistenceLive(dbPath); |
| 1229 | const runtimeRepositoryLayer = ProviderSessionRuntimeRepositoryLive.pipe( | |
| 1228 | const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe( | |
| 1230 | 1229 | Layer.provide(persistenceLayer), |
| 1231 | 1230 | ); |
| 1232 | 1231 | |
| ... | ... | @@ -1316,7 +1315,7 @@ routing.layer("ProviderServiceLive routing", (it) => { |
| 1316 | 1315 | const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-provider-service-cwd-")); |
| 1317 | 1316 | const dbPath = path.join(tempDir, "orchestration.sqlite"); |
| 1318 | 1317 | const persistenceLayer = makeSqlitePersistenceLive(dbPath); |
| 1319 | const runtimeRepositoryLayer = ProviderSessionRuntimeRepositoryLive.pipe( | |
| 1318 | const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe( | |
| 1320 | 1319 | Layer.provide(persistenceLayer), |
| 1321 | 1320 | ); |
| 1322 | 1321 | |
| ... | ... | @@ -1761,7 +1760,7 @@ validation.layer("ProviderServiceLive validation", (it) => { |
| 1761 | 1760 | it.effect("accepts startSession when adapter has not emitted provider thread id yet", () => |
| 1762 | 1761 | Effect.gen(function* () { |
| 1763 | 1762 | const provider = yield* ProviderService; |
| 1764 | const runtimeRepository = yield* ProviderSessionRuntimeRepository; | |
| 1763 | const runtimeRepository = yield* ProviderSessionRuntime.ProviderSessionRuntimeRepository; | |
| 1765 | 1764 | |
| 1766 | 1765 | validation.codex.startSession.mockImplementationOnce((input: ProviderSessionStartInput) => |
| 1767 | 1766 | Effect.sync(() => { |
apps/server/src/provider/Layers/ProviderSessionDirectory.test.ts+6-9| ... | ... | @@ -16,15 +16,12 @@ import { |
| 16 | 16 | makeSqlitePersistenceLive, |
| 17 | 17 | SqlitePersistenceMemory, |
| 18 | 18 | } from "../../persistence/Layers/Sqlite.ts"; |
| 19 | import { ProviderSessionRuntimeRepositoryLive } from "../../persistence/Layers/ProviderSessionRuntime.ts"; | |
| 20 | import { ProviderSessionRuntimeRepository } from "../../persistence/Services/ProviderSessionRuntime.ts"; | |
| 19 | import * as ProviderSessionRuntime from "../../persistence/ProviderSessionRuntime.ts"; | |
| 21 | 20 | import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts"; |
| 22 | 21 | import { ProviderSessionDirectoryLive } from "./ProviderSessionDirectory.ts"; |
| 23 | 22 | |
| 24 | 23 | function makeDirectoryLayer<E, R>(persistenceLayer: Layer.Layer<SqlClient.SqlClient, E, R>) { |
| 25 | const runtimeRepositoryLayer = ProviderSessionRuntimeRepositoryLive.pipe( | |
| 26 | Layer.provide(persistenceLayer), | |
| 27 | ); | |
| 24 | const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe(Layer.provide(persistenceLayer)); | |
| 28 | 25 | return Layer.mergeAll( |
| 29 | 26 | runtimeRepositoryLayer, |
| 30 | 27 | ProviderSessionDirectoryLive.pipe(Layer.provide(runtimeRepositoryLayer)), |
| ... | ... | @@ -36,7 +33,7 @@ it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryL |
| 36 | 33 | it("upserts and reads thread bindings", () => |
| 37 | 34 | Effect.gen(function* () { |
| 38 | 35 | const directory = yield* ProviderSessionDirectory; |
| 39 | const runtimeRepository = yield* ProviderSessionRuntimeRepository; | |
| 36 | const runtimeRepository = yield* ProviderSessionRuntime.ProviderSessionRuntimeRepository; | |
| 40 | 37 | |
| 41 | 38 | const initialThreadId = ThreadId.make("thread-1"); |
| 42 | 39 | |
| ... | ... | @@ -83,7 +80,7 @@ it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryL |
| 83 | 80 | it("persists runtime fields and merges payload updates", () => |
| 84 | 81 | Effect.gen(function* () { |
| 85 | 82 | const directory = yield* ProviderSessionDirectory; |
| 86 | const runtimeRepository = yield* ProviderSessionRuntimeRepository; | |
| 83 | const runtimeRepository = yield* ProviderSessionRuntime.ProviderSessionRuntimeRepository; | |
| 87 | 84 | |
| 88 | 85 | const threadId = ThreadId.make("thread-runtime"); |
| 89 | 86 | |
| ... | ... | @@ -128,7 +125,7 @@ it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryL |
| 128 | 125 | it("lists persisted bindings with metadata in oldest-first order", () => |
| 129 | 126 | Effect.gen(function* () { |
| 130 | 127 | const directory = yield* ProviderSessionDirectory; |
| 131 | const runtimeRepository = yield* ProviderSessionRuntimeRepository; | |
| 128 | const runtimeRepository = yield* ProviderSessionRuntime.ProviderSessionRuntimeRepository; | |
| 132 | 129 | |
| 133 | 130 | const olderThreadId = ThreadId.make("thread-runtime-older"); |
| 134 | 131 | const newerThreadId = ThreadId.make("thread-runtime-newer"); |
| ... | ... | @@ -202,7 +199,7 @@ it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryL |
| 202 | 199 | it("resets adapterKey to the new provider when provider changes without an explicit adapter key", () => |
| 203 | 200 | Effect.gen(function* () { |
| 204 | 201 | const directory = yield* ProviderSessionDirectory; |
| 205 | const runtimeRepository = yield* ProviderSessionRuntimeRepository; | |
| 202 | const runtimeRepository = yield* ProviderSessionRuntime.ProviderSessionRuntimeRepository; | |
| 206 | 203 | const threadId = ThreadId.make("thread-provider-change"); |
| 207 | 204 | |
| 208 | 205 | yield* runtimeRepository.upsert({ |
apps/server/src/provider/Layers/ProviderSessionDirectory.ts+3-4| ... | ... | @@ -5,8 +5,7 @@ import * as Layer from "effect/Layer"; |
| 5 | 5 | import * as Option from "effect/Option"; |
| 6 | 6 | import * as Schema from "effect/Schema"; |
| 7 | 7 | |
| 8 | import type { ProviderSessionRuntime } from "../../persistence/Services/ProviderSessionRuntime.ts"; | |
| 9 | import { ProviderSessionRuntimeRepository } from "../../persistence/Services/ProviderSessionRuntime.ts"; | |
| 8 | import * as ProviderSessionRuntime from "../../persistence/ProviderSessionRuntime.ts"; | |
| 10 | 9 | import { ProviderSessionDirectoryPersistenceError, ProviderValidationError } from "../Errors.ts"; |
| 11 | 10 | import { |
| 12 | 11 | ProviderSessionDirectory, |
| ... | ... | @@ -59,7 +58,7 @@ function mergeRuntimePayload( |
| 59 | 58 | } |
| 60 | 59 | |
| 61 | 60 | function toRuntimeBinding( |
| 62 | runtime: ProviderSessionRuntime, | |
| 61 | runtime: ProviderSessionRuntime.ProviderSessionRuntime, | |
| 63 | 62 | operation: string, |
| 64 | 63 | ): Effect.Effect<ProviderRuntimeBindingWithMetadata, ProviderSessionDirectoryPersistenceError> { |
| 65 | 64 | return decodeProviderDriverKind(runtime.providerName, operation).pipe( |
| ... | ... | @@ -85,7 +84,7 @@ function toRuntimeBinding( |
| 85 | 84 | } |
| 86 | 85 | |
| 87 | 86 | const makeProviderSessionDirectory = Effect.gen(function* () { |
| 88 | const repository = yield* ProviderSessionRuntimeRepository; | |
| 87 | const repository = yield* ProviderSessionRuntime.ProviderSessionRuntimeRepository; | |
| 89 | 88 | |
| 90 | 89 | const getBinding = (threadId: ThreadId) => |
| 91 | 90 | repository.getByThreadId({ threadId }).pipe( |
apps/server/src/provider/Layers/ProviderSessionReaper.test.ts+21-10| ... | ... | @@ -19,8 +19,7 @@ import { afterEach, describe, expect, it, vi } from "vite-plus/test"; |
| 19 | 19 | |
| 20 | 20 | import { ProjectionSnapshotQuery } from "../../orchestration/Services/ProjectionSnapshotQuery.ts"; |
| 21 | 21 | import { SqlitePersistenceMemory } from "../../persistence/Layers/Sqlite.ts"; |
| 22 | import { ProviderSessionRuntimeRepositoryLive } from "../../persistence/Layers/ProviderSessionRuntime.ts"; | |
| 23 | import { ProviderSessionRuntimeRepository } from "../../persistence/Services/ProviderSessionRuntime.ts"; | |
| 22 | import * as ProviderSessionRuntime from "../../persistence/ProviderSessionRuntime.ts"; | |
| 24 | 23 | import { ProviderValidationError } from "../Errors.ts"; |
| 25 | 24 | import { ProviderSessionReaper } from "../Services/ProviderSessionReaper.ts"; |
| 26 | 25 | import { ProviderService, type ProviderServiceShape } from "../Services/ProviderService.ts"; |
| ... | ... | @@ -118,7 +117,7 @@ function makeReadModel( |
| 118 | 117 | |
| 119 | 118 | describe("ProviderSessionReaper", () => { |
| 120 | 119 | let runtime: ManagedRuntime.ManagedRuntime< |
| 121 | ProviderSessionReaper | ProviderSessionRuntimeRepository, | |
| 120 | ProviderSessionReaper | ProviderSessionRuntime.ProviderSessionRuntimeRepository, | |
| 122 | 121 | unknown |
| 123 | 122 | > | null = null; |
| 124 | 123 | let scope: Scope.Closeable | null = null; |
| ... | ... | @@ -176,7 +175,7 @@ describe("ProviderSessionReaper", () => { |
| 176 | 175 | streamEvents: Stream.empty, |
| 177 | 176 | }; |
| 178 | 177 | |
| 179 | const runtimeRepositoryLayer = ProviderSessionRuntimeRepositoryLive.pipe( | |
| 178 | const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe( | |
| 180 | 179 | Layer.provide(SqlitePersistenceMemory), |
| 181 | 180 | ); |
| 182 | 181 | const providerSessionDirectoryLayer = ProviderSessionDirectoryLive.pipe( |
| ... | ... | @@ -238,7 +237,9 @@ describe("ProviderSessionReaper", () => { |
| 238 | 237 | }, |
| 239 | 238 | ]), |
| 240 | 239 | }); |
| 241 | const repository = await runtime!.runPromise(Effect.service(ProviderSessionRuntimeRepository)); | |
| 240 | const repository = await runtime!.runPromise( | |
| 241 | Effect.service(ProviderSessionRuntime.ProviderSessionRuntimeRepository), | |
| 242 | ); | |
| 242 | 243 | |
| 243 | 244 | await runtime!.runPromise( |
| 244 | 245 | repository.upsert({ |
| ... | ... | @@ -286,7 +287,9 @@ describe("ProviderSessionReaper", () => { |
| 286 | 287 | }, |
| 287 | 288 | ]), |
| 288 | 289 | }); |
| 289 | const repository = await runtime!.runPromise(Effect.service(ProviderSessionRuntimeRepository)); | |
| 290 | const repository = await runtime!.runPromise( | |
| 291 | Effect.service(ProviderSessionRuntime.ProviderSessionRuntimeRepository), | |
| 292 | ); | |
| 290 | 293 | |
| 291 | 294 | await runtime!.runPromise( |
| 292 | 295 | repository.upsert({ |
| ... | ... | @@ -333,7 +336,9 @@ describe("ProviderSessionReaper", () => { |
| 333 | 336 | }, |
| 334 | 337 | ]), |
| 335 | 338 | }); |
| 336 | const repository = await runtime!.runPromise(Effect.service(ProviderSessionRuntimeRepository)); | |
| 339 | const repository = await runtime!.runPromise( | |
| 340 | Effect.service(ProviderSessionRuntime.ProviderSessionRuntimeRepository), | |
| 341 | ); | |
| 337 | 342 | |
| 338 | 343 | await runtime!.runPromise( |
| 339 | 344 | repository.upsert({ |
| ... | ... | @@ -380,7 +385,9 @@ describe("ProviderSessionReaper", () => { |
| 380 | 385 | }, |
| 381 | 386 | ]), |
| 382 | 387 | }); |
| 383 | const repository = await runtime!.runPromise(Effect.service(ProviderSessionRuntimeRepository)); | |
| 388 | const repository = await runtime!.runPromise( | |
| 389 | Effect.service(ProviderSessionRuntime.ProviderSessionRuntimeRepository), | |
| 390 | ); | |
| 384 | 391 | |
| 385 | 392 | await runtime!.runPromise( |
| 386 | 393 | repository.upsert({ |
| ... | ... | @@ -449,7 +456,9 @@ describe("ProviderSessionReaper", () => { |
| 449 | 456 | ) |
| 450 | 457 | : Effect.void, |
| 451 | 458 | }); |
| 452 | const repository = await runtime!.runPromise(Effect.service(ProviderSessionRuntimeRepository)); | |
| 459 | const repository = await runtime!.runPromise( | |
| 460 | Effect.service(ProviderSessionRuntime.ProviderSessionRuntimeRepository), | |
| 461 | ); | |
| 453 | 462 | |
| 454 | 463 | await runtime!.runPromise( |
| 455 | 464 | repository.upsert({ |
| ... | ... | @@ -530,7 +539,9 @@ describe("ProviderSessionReaper", () => { |
| 530 | 539 | ? Effect.die(new Error("simulated stop defect")) |
| 531 | 540 | : Effect.void, |
| 532 | 541 | }); |
| 533 | const repository = await runtime!.runPromise(Effect.service(ProviderSessionRuntimeRepository)); | |
| 542 | const repository = await runtime!.runPromise( | |
| 543 | Effect.service(ProviderSessionRuntime.ProviderSessionRuntimeRepository), | |
| 544 | ); | |
| 534 | 545 | |
| 535 | 546 | await runtime!.runPromise( |
| 536 | 547 | repository.upsert({ |
apps/server/src/server.ts+2-2| ... | ... | @@ -19,7 +19,7 @@ import { layerConfig as SqlitePersistenceLayerLive } from "./persistence/Layers/ |
| 19 | 19 | import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts"; |
| 20 | 20 | import { AnalyticsServiceLayerLive } from "./telemetry/Layers/AnalyticsService.ts"; |
| 21 | 21 | import { ProviderSessionDirectoryLive } from "./provider/Layers/ProviderSessionDirectory.ts"; |
| 22 | import { ProviderSessionRuntimeRepositoryLive } from "./persistence/Layers/ProviderSessionRuntime.ts"; | |
| 22 | import * as ProviderSessionRuntime from "./persistence/ProviderSessionRuntime.ts"; | |
| 23 | 23 | import { ProviderAdapterRegistryLive } from "./provider/Layers/ProviderAdapterRegistry.ts"; |
| 24 | 24 | import * as ProviderEventLoggers from "./provider/Layers/ProviderEventLoggers.ts"; |
| 25 | 25 | import { ProviderServiceLive } from "./provider/Layers/ProviderService.ts"; |
| ... | ... | @@ -166,7 +166,7 @@ const ReactorLayerLive = Layer.empty.pipe( |
| 166 | 166 | ); |
| 167 | 167 | |
| 168 | 168 | const ProviderSessionDirectoryLayerLive = ProviderSessionDirectoryLive.pipe( |
| 169 | Layer.provide(ProviderSessionRuntimeRepositoryLive), | |
| 169 | Layer.provide(ProviderSessionRuntime.layer), | |
| 170 | 170 | ); |
| 171 | 171 | |
| 172 | 172 | // `ProviderAdapterRegistryLive` is now a facade that resolves kind → adapter |