| 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,7 +25,7 @@ import { |
| 25 | import { ServerSettingsService } from "../src/serverSettings.ts"; | 25 | import { ServerSettingsService } from "../src/serverSettings.ts"; |
| 26 | import { AnalyticsService } from "../src/telemetry/Services/AnalyticsService.ts"; | 26 | import { AnalyticsService } from "../src/telemetry/Services/AnalyticsService.ts"; |
| 27 | import { SqlitePersistenceMemory } from "../src/persistence/Layers/Sqlite.ts"; | 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 | import { | 30 | import { |
| 31 | makeTestProviderAdapterHarness, | 31 | makeTestProviderAdapterHarness, |
| ... | @@ -63,7 +63,7 @@ const makeIntegrationFixture = Effect.gen(function* () { | ... | @@ -63,7 +63,7 @@ const makeIntegrationFixture = Effect.gen(function* () { |
| 63 | }); | 63 | }); |
| 64 | 64 | ||
| 65 | const directoryLayer = ProviderSessionDirectoryLive.pipe( | 65 | const directoryLayer = ProviderSessionDirectoryLive.pipe( |
| 66 | Layer.provide(ProviderSessionRuntimeRepositoryLive), | 66 | Layer.provide(ProviderSessionRuntime.layer), |
| 67 | ); | 67 | ); |
| 68 | 68 | ||
| 69 | const shared = Layer.mergeAll( | 69 | const shared = Layer.mergeAll( |
apps/server/src/auth/PairingGrantStore.ts+3-4| ... | @@ -18,8 +18,7 @@ import * as Stream from "effect/Stream"; | ... | @@ -18,8 +18,7 @@ import * as Stream from "effect/Stream"; |
| 18 | import * as Option from "effect/Option"; | 18 | import * as Option from "effect/Option"; |
| 19 | 19 | ||
| 20 | import { ServerConfig } from "../config.ts"; | 20 | import { ServerConfig } from "../config.ts"; |
| 21 | import { AuthPairingLinkRepositoryLive } from "../persistence/Layers/AuthPairingLinks.ts"; | 21 | import * as AuthPairingLinks from "../persistence/AuthPairingLinks.ts"; |
| 22 | import { AuthPairingLinkRepository } from "../persistence/Services/AuthPairingLinks.ts"; | ||
| 23 | 22 | ||
| 24 | export interface BootstrapGrant { | 23 | export interface BootstrapGrant { |
| 25 | readonly method: ServerAuthBootstrapMethod; | 24 | readonly method: ServerAuthBootstrapMethod; |
| ... | @@ -126,7 +125,7 @@ const internalBootstrapCredentialError = (message: string, cause: unknown) => | ... | @@ -126,7 +125,7 @@ const internalBootstrapCredentialError = (message: string, cause: unknown) => |
| 126 | export const make = Effect.fn("makePairingGrantStore")(function* () { | 125 | export const make = Effect.fn("makePairingGrantStore")(function* () { |
| 127 | const crypto = yield* Crypto.Crypto; | 126 | const crypto = yield* Crypto.Crypto; |
| 128 | const config = yield* ServerConfig; | 127 | const config = yield* ServerConfig; |
| 129 | const pairingLinks = yield* AuthPairingLinkRepository; | 128 | const pairingLinks = yield* AuthPairingLinks.AuthPairingLinkRepository; |
| 130 | const seededGrantsRef = yield* Ref.make(new Map<string, StoredBootstrapGrant>()); | 129 | const seededGrantsRef = yield* Ref.make(new Map<string, StoredBootstrapGrant>()); |
| 131 | const changesPubSub = yield* PubSub.unbounded<BootstrapCredentialChange>(); | 130 | const changesPubSub = yield* PubSub.unbounded<BootstrapCredentialChange>(); |
| 132 | const generatePairingToken = Effect.gen(function* () { | 131 | const generatePairingToken = Effect.gen(function* () { |
| ... | @@ -417,5 +416,5 @@ export const make = Effect.fn("makePairingGrantStore")(function* () { | ... | @@ -417,5 +416,5 @@ export const make = Effect.fn("makePairingGrantStore")(function* () { |
| 417 | }); | 416 | }); |
| 418 | 417 | ||
| 419 | export const layer = Layer.effect(PairingGrantStore, make()).pipe( | 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,7 +8,7 @@ import * as TestClock from "effect/testing/TestClock"; |
| 8 | import * as ServerConfig from "../config.ts"; | 8 | import * as ServerConfig from "../config.ts"; |
| 9 | import { PersistenceSqlError } from "../persistence/Errors.ts"; | 9 | import { PersistenceSqlError } from "../persistence/Errors.ts"; |
| 10 | import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts"; | 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 | import * as SessionStore from "./SessionStore.ts"; | 12 | import * as SessionStore from "./SessionStore.ts"; |
| 13 | import * as ServerSecretStore from "./ServerSecretStore.ts"; | 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,8 +22,7 @@ import * as Stream from "effect/Stream"; |
| 22 | import * as Option from "effect/Option"; | 22 | import * as Option from "effect/Option"; |
| 23 | 23 | ||
| 24 | import { ServerConfig } from "../config.ts"; | 24 | import { ServerConfig } from "../config.ts"; |
| 25 | import { AuthSessionRepositoryLive } from "../persistence/Layers/AuthSessions.ts"; | 25 | import * as AuthSessions from "../persistence/AuthSessions.ts"; |
| 26 | import { AuthSessionRepository } from "../persistence/Services/AuthSessions.ts"; | ||
| 27 | import * as ServerSecretStore from "./ServerSecretStore.ts"; | 26 | import * as ServerSecretStore from "./ServerSecretStore.ts"; |
| 28 | import { | 27 | import { |
| 29 | base64UrlDecodeUtf8, | 28 | base64UrlDecodeUtf8, |
| ... | @@ -195,7 +194,7 @@ export const make = Effect.fn("makeSessionStore")(function* () { | ... | @@ -195,7 +194,7 @@ export const make = Effect.fn("makeSessionStore")(function* () { |
| 195 | const crypto = yield* Crypto.Crypto; | 194 | const crypto = yield* Crypto.Crypto; |
| 196 | const serverConfig = yield* ServerConfig; | 195 | const serverConfig = yield* ServerConfig; |
| 197 | const secretStore = yield* ServerSecretStore.ServerSecretStore; | 196 | const secretStore = yield* ServerSecretStore.ServerSecretStore; |
| 198 | const authSessions = yield* AuthSessionRepository; | 197 | const authSessions = yield* AuthSessions.AuthSessionRepository; |
| 199 | const signingSecret = yield* secretStore.getOrCreateRandom(SIGNING_SECRET_NAME, 32); | 198 | const signingSecret = yield* secretStore.getOrCreateRandom(SIGNING_SECRET_NAME, 32); |
| 200 | const connectedSessionsRef = yield* Ref.make(new Map<string, number>()); | 199 | const connectedSessionsRef = yield* Ref.make(new Map<string, number>()); |
| 201 | const changesPubSub = yield* PubSub.unbounded<SessionCredentialChange>(); | 200 | const changesPubSub = yield* PubSub.unbounded<SessionCredentialChange>(); |
| ... | @@ -640,5 +639,5 @@ export const make = Effect.fn("makeSessionStore")(function* () { | ... | @@ -640,5 +639,5 @@ export const make = Effect.fn("makeSessionStore")(function* () { |
| 640 | }); | 639 | }); |
| 641 | 640 | ||
| 642 | export const layer = Layer.effect(SessionStore, make()).pipe( | 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,208 +1,2 @@ |
| 1 | import { ThreadId } from "@t3tools/contracts"; | 1 | /** @deprecated Compatibility alias for the excluded orchestration integration harness. */ |
| 2 | import * as SqlClient from "effect/unstable/sql/SqlClient"; | 2 | export { layer as ProviderSessionRuntimeRepositoryLive } from "../ProviderSessionRuntime.ts"; |
| 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 | ); |
apps/server/src/persistence/NodeSqliteClient.ts+5-23| ... | @@ -29,11 +29,6 @@ export const TypeId: TypeId = "~local/sqlite-node/SqliteClient"; | ... | @@ -29,11 +29,6 @@ export const TypeId: TypeId = "~local/sqlite-node/SqliteClient"; |
| 29 | 29 | ||
| 30 | export type TypeId = "~local/sqlite-node/SqliteClient"; | 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 | export interface SqliteClientConfig { | 32 | export interface SqliteClientConfig { |
| 38 | readonly filename: string; | 33 | readonly filename: string; |
| 39 | readonly readonly?: boolean | undefined; | 34 | readonly readonly?: boolean | undefined; |
| ... | @@ -251,25 +246,12 @@ const makeMemory = ( | ... | @@ -251,25 +246,12 @@ const makeMemory = ( |
| 251 | export const layerConfig = ( | 246 | export const layerConfig = ( |
| 252 | config: Config.Wrap<SqliteClientConfig>, | 247 | config: Config.Wrap<SqliteClientConfig>, |
| 253 | ): Layer.Layer<Client.SqlClient, Config.ConfigError> => | 248 | ): Layer.Layer<Client.SqlClient, Config.ConfigError> => |
| 254 | Layer.effectContext( | 249 | Layer.effect(Client.SqlClient, Config.unwrap(config).pipe(Effect.flatMap(make))).pipe( |
| 255 | Config.unwrap(config).pipe( | 250 | Layer.provide(Reactivity.layer), |
| 256 | Effect.flatMap(make), | 251 | ); |
| 257 | Effect.map((client) => | ||
| 258 | Context.make(SqliteClient, client).pipe(Context.add(Client.SqlClient, client)), | ||
| 259 | ), | ||
| 260 | ), | ||
| 261 | ).pipe(Layer.provide(Reactivity.layer)); | ||
| 262 | 252 | ||
| 263 | export const layer = (config: SqliteClientConfig): Layer.Layer<Client.SqlClient> => | 253 | export const layer = (config: SqliteClientConfig): Layer.Layer<Client.SqlClient> => |
| 264 | Layer.effectContext( | 254 | Layer.effect(Client.SqlClient, make(config)).pipe(Layer.provide(Reactivity.layer)); |
| 265 | Effect.map(make(config), (client) => | ||
| 266 | Context.make(SqliteClient, client).pipe(Context.add(Client.SqlClient, client)), | ||
| 267 | ), | ||
| 268 | ).pipe(Layer.provide(Reactivity.layer)); | ||
| 269 | 255 | ||
| 270 | export const layerMemory = (config: SqliteMemoryClientConfig = {}): Layer.Layer<Client.SqlClient> => | 256 | export const layerMemory = (config: SqliteMemoryClientConfig = {}): Layer.Layer<Client.SqlClient> => |
| 271 | Layer.effectContext( | 257 | Layer.effect(Client.SqlClient, makeMemory(config)).pipe(Layer.provide(Reactivity.layer)); |
| 272 | Effect.map(makeMemory(config), (client) => | ||
| 273 | Context.make(SqliteClient, client).pipe(Context.add(Client.SqlClient, client)), | ||
| 274 | ), | ||
| 275 | ).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,8 +53,7 @@ import { makeProviderServiceLive } from "./ProviderService.ts"; |
| 53 | import { NoOpProviderEventLoggers, ProviderEventLoggers } from "./ProviderEventLoggers.ts"; | 53 | import { NoOpProviderEventLoggers, ProviderEventLoggers } from "./ProviderEventLoggers.ts"; |
| 54 | import { ProviderSessionDirectoryLive } from "./ProviderSessionDirectory.ts"; | 54 | import { ProviderSessionDirectoryLive } from "./ProviderSessionDirectory.ts"; |
| 55 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 55 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 56 | import { ProviderSessionRuntimeRepositoryLive } from "../../persistence/Layers/ProviderSessionRuntime.ts"; | 56 | import * as ProviderSessionRuntime from "../../persistence/ProviderSessionRuntime.ts"; |
| 57 | import { ProviderSessionRuntimeRepository } from "../../persistence/Services/ProviderSessionRuntime.ts"; | ||
| 58 | import { | 57 | import { |
| 59 | makeSqlitePersistenceLive, | 58 | makeSqlitePersistenceLive, |
| 60 | SqlitePersistenceMemory, | 59 | SqlitePersistenceMemory, |
| ... | @@ -282,7 +281,7 @@ function makeProviderServiceLayer() { | ... | @@ -282,7 +281,7 @@ function makeProviderServiceLayer() { |
| 282 | }); | 281 | }); |
| 283 | 282 | ||
| 284 | const providerAdapterLayer = Layer.succeed(ProviderAdapterRegistry, registry); | 283 | const providerAdapterLayer = Layer.succeed(ProviderAdapterRegistry, registry); |
| 285 | const runtimeRepositoryLayer = ProviderSessionRuntimeRepositoryLive.pipe( | 284 | const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe( |
| 286 | Layer.provide(SqlitePersistenceMemory), | 285 | Layer.provide(SqlitePersistenceMemory), |
| 287 | ); | 286 | ); |
| 288 | const directoryLayer = ProviderSessionDirectoryLive.pipe(Layer.provide(runtimeRepositoryLayer)); | 287 | const directoryLayer = ProviderSessionDirectoryLive.pipe(Layer.provide(runtimeRepositoryLayer)); |
| ... | @@ -327,7 +326,7 @@ it.effect("ProviderServiceLive catches stopAll failures during shutdown", () => | ... | @@ -327,7 +326,7 @@ it.effect("ProviderServiceLive catches stopAll failures during shutdown", () => |
| 327 | [CODEX_DRIVER]: codex.adapter, | 326 | [CODEX_DRIVER]: codex.adapter, |
| 328 | }); | 327 | }); |
| 329 | const providerAdapterLayer = Layer.succeed(ProviderAdapterRegistry, registry); | 328 | const providerAdapterLayer = Layer.succeed(ProviderAdapterRegistry, registry); |
| 330 | const runtimeRepositoryLayer = ProviderSessionRuntimeRepositoryLive.pipe( | 329 | const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe( |
| 331 | Layer.provide(SqlitePersistenceMemory), | 330 | Layer.provide(SqlitePersistenceMemory), |
| 332 | ); | 331 | ); |
| 333 | const directoryLayer = ProviderSessionDirectoryLive.pipe(Layer.provide(runtimeRepositoryLayer)); | 332 | const directoryLayer = ProviderSessionDirectoryLive.pipe(Layer.provide(runtimeRepositoryLayer)); |
| ... | @@ -379,7 +378,7 @@ it.effect("ProviderServiceLive rejects new sessions for disabled providers", () | ... | @@ -379,7 +378,7 @@ it.effect("ProviderServiceLive rejects new sessions for disabled providers", () |
| 379 | : registryBase.getInstanceInfo(instanceId), | 378 | : registryBase.getInstanceInfo(instanceId), |
| 380 | }; | 379 | }; |
| 381 | const providerAdapterLayer = Layer.succeed(ProviderAdapterRegistry, registry); | 380 | const providerAdapterLayer = Layer.succeed(ProviderAdapterRegistry, registry); |
| 382 | const runtimeRepositoryLayer = ProviderSessionRuntimeRepositoryLive.pipe( | 381 | const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe( |
| 383 | Layer.provide(SqlitePersistenceMemory), | 382 | Layer.provide(SqlitePersistenceMemory), |
| 384 | ); | 383 | ); |
| 385 | const directoryLayer = ProviderSessionDirectoryLive.pipe(Layer.provide(runtimeRepositoryLayer)); | 384 | const directoryLayer = ProviderSessionDirectoryLive.pipe(Layer.provide(runtimeRepositoryLayer)); |
| ... | @@ -453,7 +452,7 @@ it.effect( | ... | @@ -453,7 +452,7 @@ it.effect( |
| 453 | }, | 452 | }, |
| 454 | }, | 453 | }, |
| 455 | }); | 454 | }); |
| 456 | const runtimeRepositoryLayer = ProviderSessionRuntimeRepositoryLive.pipe( | 455 | const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe( |
| 457 | Layer.provide(SqlitePersistenceMemory), | 456 | Layer.provide(SqlitePersistenceMemory), |
| 458 | ); | 457 | ); |
| 459 | const directoryLayer = ProviderSessionDirectoryLive.pipe( | 458 | const directoryLayer = ProviderSessionDirectoryLive.pipe( |
| ... | @@ -517,7 +516,7 @@ it.effect("ProviderServiceLive rejects new sessions for disabled custom instance | ... | @@ -517,7 +516,7 @@ it.effect("ProviderServiceLive rejects new sessions for disabled custom instance |
| 517 | ), | 516 | ), |
| 518 | }; | 517 | }; |
| 519 | const providerAdapterLayer = Layer.succeed(ProviderAdapterRegistry, registry); | 518 | const providerAdapterLayer = Layer.succeed(ProviderAdapterRegistry, registry); |
| 520 | const runtimeRepositoryLayer = ProviderSessionRuntimeRepositoryLive.pipe( | 519 | const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe( |
| 521 | Layer.provide(SqlitePersistenceMemory), | 520 | Layer.provide(SqlitePersistenceMemory), |
| 522 | ); | 521 | ); |
| 523 | const directoryLayer = ProviderSessionDirectoryLive.pipe(Layer.provide(runtimeRepositoryLayer)); | 522 | const directoryLayer = ProviderSessionDirectoryLive.pipe(Layer.provide(runtimeRepositoryLayer)); |
| ... | @@ -557,7 +556,7 @@ it.effect("ProviderServiceLive writes canonical events to the emitting thread se | ... | @@ -557,7 +556,7 @@ it.effect("ProviderServiceLive writes canonical events to the emitting thread se |
| 557 | const registry = makeAdapterRegistryMock({ | 556 | const registry = makeAdapterRegistryMock({ |
| 558 | [ProviderDriverKind.make("codex")]: codex.adapter, | 557 | [ProviderDriverKind.make("codex")]: codex.adapter, |
| 559 | }); | 558 | }); |
| 560 | const runtimeRepositoryLayer = ProviderSessionRuntimeRepositoryLive.pipe( | 559 | const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe( |
| 561 | Layer.provide(SqlitePersistenceMemory), | 560 | Layer.provide(SqlitePersistenceMemory), |
| 562 | ); | 561 | ); |
| 563 | const directoryLayer = ProviderSessionDirectoryLive.pipe(Layer.provide(runtimeRepositoryLayer)); | 562 | const directoryLayer = ProviderSessionDirectoryLive.pipe(Layer.provide(runtimeRepositoryLayer)); |
| ... | @@ -612,7 +611,7 @@ it.effect("ProviderServiceLive keeps persisted resumable sessions on startup", ( | ... | @@ -612,7 +611,7 @@ it.effect("ProviderServiceLive keeps persisted resumable sessions on startup", ( |
| 612 | }); | 611 | }); |
| 613 | 612 | ||
| 614 | const persistenceLayer = makeSqlitePersistenceLive(dbPath); | 613 | const persistenceLayer = makeSqlitePersistenceLive(dbPath); |
| 615 | const runtimeRepositoryLayer = ProviderSessionRuntimeRepositoryLive.pipe( | 614 | const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe( |
| 616 | Layer.provide(persistenceLayer), | 615 | Layer.provide(persistenceLayer), |
| 617 | ); | 616 | ); |
| 618 | const directoryLayer = ProviderSessionDirectoryLive.pipe(Layer.provide(runtimeRepositoryLayer)); | 617 | const directoryLayer = ProviderSessionDirectoryLive.pipe(Layer.provide(runtimeRepositoryLayer)); |
| ... | @@ -643,7 +642,7 @@ it.effect("ProviderServiceLive keeps persisted resumable sessions on startup", ( | ... | @@ -643,7 +642,7 @@ it.effect("ProviderServiceLive keeps persisted resumable sessions on startup", ( |
| 643 | assert.equal(persistedProvider, "codex"); | 642 | assert.equal(persistedProvider, "codex"); |
| 644 | 643 | ||
| 645 | const runtime = yield* Effect.gen(function* () { | 644 | const runtime = yield* Effect.gen(function* () { |
| 646 | const repository = yield* ProviderSessionRuntimeRepository; | 645 | const repository = yield* ProviderSessionRuntime.ProviderSessionRuntimeRepository; |
| 647 | return yield* repository.getByThreadId({ | 646 | return yield* repository.getByThreadId({ |
| 648 | threadId: asThreadId("thread-stale"), | 647 | threadId: asThreadId("thread-stale"), |
| 649 | }); | 648 | }); |
| ... | @@ -671,7 +670,7 @@ it.effect( | ... | @@ -671,7 +670,7 @@ it.effect( |
| 671 | const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-provider-service-restart-")); | 670 | const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-provider-service-restart-")); |
| 672 | const dbPath = path.join(tempDir, "orchestration.sqlite"); | 671 | const dbPath = path.join(tempDir, "orchestration.sqlite"); |
| 673 | const persistenceLayer = makeSqlitePersistenceLive(dbPath); | 672 | const persistenceLayer = makeSqlitePersistenceLive(dbPath); |
| 674 | const runtimeRepositoryLayer = ProviderSessionRuntimeRepositoryLive.pipe( | 673 | const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe( |
| 675 | Layer.provide(persistenceLayer), | 674 | Layer.provide(persistenceLayer), |
| 676 | ); | 675 | ); |
| 677 | 676 | ||
| ... | @@ -717,7 +716,7 @@ it.effect( | ... | @@ -717,7 +716,7 @@ it.effect( |
| 717 | }).pipe(Effect.provide(firstProviderLayer)); | 716 | }).pipe(Effect.provide(firstProviderLayer)); |
| 718 | 717 | ||
| 719 | const persistedAfterStopAll = yield* Effect.gen(function* () { | 718 | const persistedAfterStopAll = yield* Effect.gen(function* () { |
| 720 | const repository = yield* ProviderSessionRuntimeRepository; | 719 | const repository = yield* ProviderSessionRuntime.ProviderSessionRuntimeRepository; |
| 721 | return yield* repository.getByThreadId({ | 720 | return yield* repository.getByThreadId({ |
| 722 | threadId: startedSession.threadId, | 721 | threadId: startedSession.threadId, |
| 723 | }); | 722 | }); |
| ... | @@ -909,7 +908,7 @@ routing.layer("ProviderServiceLive routing", (it) => { | ... | @@ -909,7 +908,7 @@ routing.layer("ProviderServiceLive routing", (it) => { |
| 909 | it.effect("preserves the persisted binding when stopping a session", () => | 908 | it.effect("preserves the persisted binding when stopping a session", () => |
| 910 | Effect.gen(function* () { | 909 | Effect.gen(function* () { |
| 911 | const provider = yield* ProviderService; | 910 | const provider = yield* ProviderService; |
| 912 | const runtimeRepository = yield* ProviderSessionRuntimeRepository; | 911 | const runtimeRepository = yield* ProviderSessionRuntime.ProviderSessionRuntimeRepository; |
| 913 | 912 | ||
| 914 | const initial = yield* provider.startSession(asThreadId("thread-reap-preserve"), { | 913 | const initial = yield* provider.startSession(asThreadId("thread-reap-preserve"), { |
| 915 | provider: ProviderDriverKind.make("codex"), | 914 | provider: ProviderDriverKind.make("codex"), |
| ... | @@ -1179,7 +1178,7 @@ routing.layer("ProviderServiceLive routing", (it) => { | ... | @@ -1179,7 +1178,7 @@ routing.layer("ProviderServiceLive routing", (it) => { |
| 1179 | it.effect("persists runtime status transitions in provider_session_runtime", () => | 1178 | it.effect("persists runtime status transitions in provider_session_runtime", () => |
| 1180 | Effect.gen(function* () { | 1179 | Effect.gen(function* () { |
| 1181 | const provider = yield* ProviderService; | 1180 | const provider = yield* ProviderService; |
| 1182 | const runtimeRepository = yield* ProviderSessionRuntimeRepository; | 1181 | const runtimeRepository = yield* ProviderSessionRuntime.ProviderSessionRuntimeRepository; |
| 1183 | 1182 | ||
| 1184 | const threadId = asThreadId("thread-runtime-status"); | 1183 | const threadId = asThreadId("thread-runtime-status"); |
| 1185 | const session = yield* provider.startSession(threadId, { | 1184 | const session = yield* provider.startSession(threadId, { |
| ... | @@ -1226,7 +1225,7 @@ routing.layer("ProviderServiceLive routing", (it) => { | ... | @@ -1226,7 +1225,7 @@ routing.layer("ProviderServiceLive routing", (it) => { |
| 1226 | const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-provider-service-start-")); | 1225 | const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-provider-service-start-")); |
| 1227 | const dbPath = path.join(tempDir, "orchestration.sqlite"); | 1226 | const dbPath = path.join(tempDir, "orchestration.sqlite"); |
| 1228 | const persistenceLayer = makeSqlitePersistenceLive(dbPath); | 1227 | const persistenceLayer = makeSqlitePersistenceLive(dbPath); |
| 1229 | const runtimeRepositoryLayer = ProviderSessionRuntimeRepositoryLive.pipe( | 1228 | const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe( |
| 1230 | Layer.provide(persistenceLayer), | 1229 | Layer.provide(persistenceLayer), |
| 1231 | ); | 1230 | ); |
| 1232 | 1231 | ||
| ... | @@ -1316,7 +1315,7 @@ routing.layer("ProviderServiceLive routing", (it) => { | ... | @@ -1316,7 +1315,7 @@ routing.layer("ProviderServiceLive routing", (it) => { |
| 1316 | const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-provider-service-cwd-")); | 1315 | const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-provider-service-cwd-")); |
| 1317 | const dbPath = path.join(tempDir, "orchestration.sqlite"); | 1316 | const dbPath = path.join(tempDir, "orchestration.sqlite"); |
| 1318 | const persistenceLayer = makeSqlitePersistenceLive(dbPath); | 1317 | const persistenceLayer = makeSqlitePersistenceLive(dbPath); |
| 1319 | const runtimeRepositoryLayer = ProviderSessionRuntimeRepositoryLive.pipe( | 1318 | const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe( |
| 1320 | Layer.provide(persistenceLayer), | 1319 | Layer.provide(persistenceLayer), |
| 1321 | ); | 1320 | ); |
| 1322 | 1321 | ||
| ... | @@ -1761,7 +1760,7 @@ validation.layer("ProviderServiceLive validation", (it) => { | ... | @@ -1761,7 +1760,7 @@ validation.layer("ProviderServiceLive validation", (it) => { |
| 1761 | it.effect("accepts startSession when adapter has not emitted provider thread id yet", () => | 1760 | it.effect("accepts startSession when adapter has not emitted provider thread id yet", () => |
| 1762 | Effect.gen(function* () { | 1761 | Effect.gen(function* () { |
| 1763 | const provider = yield* ProviderService; | 1762 | const provider = yield* ProviderService; |
| 1764 | const runtimeRepository = yield* ProviderSessionRuntimeRepository; | 1763 | const runtimeRepository = yield* ProviderSessionRuntime.ProviderSessionRuntimeRepository; |
| 1765 | 1764 | ||
| 1766 | validation.codex.startSession.mockImplementationOnce((input: ProviderSessionStartInput) => | 1765 | validation.codex.startSession.mockImplementationOnce((input: ProviderSessionStartInput) => |
| 1767 | Effect.sync(() => { | 1766 | Effect.sync(() => { |
apps/server/src/provider/Layers/ProviderSessionDirectory.test.ts+6-9| ... | @@ -16,15 +16,12 @@ import { | ... | @@ -16,15 +16,12 @@ import { |
| 16 | makeSqlitePersistenceLive, | 16 | makeSqlitePersistenceLive, |
| 17 | SqlitePersistenceMemory, | 17 | SqlitePersistenceMemory, |
| 18 | } from "../../persistence/Layers/Sqlite.ts"; | 18 | } from "../../persistence/Layers/Sqlite.ts"; |
| 19 | import { ProviderSessionRuntimeRepositoryLive } from "../../persistence/Layers/ProviderSessionRuntime.ts"; | 19 | import * as ProviderSessionRuntime from "../../persistence/ProviderSessionRuntime.ts"; |
| 20 | import { ProviderSessionRuntimeRepository } from "../../persistence/Services/ProviderSessionRuntime.ts"; | ||
| 21 | import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts"; | 20 | import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts"; |
| 22 | import { ProviderSessionDirectoryLive } from "./ProviderSessionDirectory.ts"; | 21 | import { ProviderSessionDirectoryLive } from "./ProviderSessionDirectory.ts"; |
| 23 | 22 | ||
| 24 | function makeDirectoryLayer<E, R>(persistenceLayer: Layer.Layer<SqlClient.SqlClient, E, R>) { | 23 | function makeDirectoryLayer<E, R>(persistenceLayer: Layer.Layer<SqlClient.SqlClient, E, R>) { |
| 25 | const runtimeRepositoryLayer = ProviderSessionRuntimeRepositoryLive.pipe( | 24 | const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe(Layer.provide(persistenceLayer)); |
| 26 | Layer.provide(persistenceLayer), | ||
| 27 | ); | ||
| 28 | return Layer.mergeAll( | 25 | return Layer.mergeAll( |
| 29 | runtimeRepositoryLayer, | 26 | runtimeRepositoryLayer, |
| 30 | ProviderSessionDirectoryLive.pipe(Layer.provide(runtimeRepositoryLayer)), | 27 | ProviderSessionDirectoryLive.pipe(Layer.provide(runtimeRepositoryLayer)), |
| ... | @@ -36,7 +33,7 @@ it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryL | ... | @@ -36,7 +33,7 @@ it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryL |
| 36 | it("upserts and reads thread bindings", () => | 33 | it("upserts and reads thread bindings", () => |
| 37 | Effect.gen(function* () { | 34 | Effect.gen(function* () { |
| 38 | const directory = yield* ProviderSessionDirectory; | 35 | const directory = yield* ProviderSessionDirectory; |
| 39 | const runtimeRepository = yield* ProviderSessionRuntimeRepository; | 36 | const runtimeRepository = yield* ProviderSessionRuntime.ProviderSessionRuntimeRepository; |
| 40 | 37 | ||
| 41 | const initialThreadId = ThreadId.make("thread-1"); | 38 | const initialThreadId = ThreadId.make("thread-1"); |
| 42 | 39 | ||
| ... | @@ -83,7 +80,7 @@ it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryL | ... | @@ -83,7 +80,7 @@ it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryL |
| 83 | it("persists runtime fields and merges payload updates", () => | 80 | it("persists runtime fields and merges payload updates", () => |
| 84 | Effect.gen(function* () { | 81 | Effect.gen(function* () { |
| 85 | const directory = yield* ProviderSessionDirectory; | 82 | const directory = yield* ProviderSessionDirectory; |
| 86 | const runtimeRepository = yield* ProviderSessionRuntimeRepository; | 83 | const runtimeRepository = yield* ProviderSessionRuntime.ProviderSessionRuntimeRepository; |
| 87 | 84 | ||
| 88 | const threadId = ThreadId.make("thread-runtime"); | 85 | const threadId = ThreadId.make("thread-runtime"); |
| 89 | 86 | ||
| ... | @@ -128,7 +125,7 @@ it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryL | ... | @@ -128,7 +125,7 @@ it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryL |
| 128 | it("lists persisted bindings with metadata in oldest-first order", () => | 125 | it("lists persisted bindings with metadata in oldest-first order", () => |
| 129 | Effect.gen(function* () { | 126 | Effect.gen(function* () { |
| 130 | const directory = yield* ProviderSessionDirectory; | 127 | const directory = yield* ProviderSessionDirectory; |
| 131 | const runtimeRepository = yield* ProviderSessionRuntimeRepository; | 128 | const runtimeRepository = yield* ProviderSessionRuntime.ProviderSessionRuntimeRepository; |
| 132 | 129 | ||
| 133 | const olderThreadId = ThreadId.make("thread-runtime-older"); | 130 | const olderThreadId = ThreadId.make("thread-runtime-older"); |
| 134 | const newerThreadId = ThreadId.make("thread-runtime-newer"); | 131 | const newerThreadId = ThreadId.make("thread-runtime-newer"); |
| ... | @@ -202,7 +199,7 @@ it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryL | ... | @@ -202,7 +199,7 @@ it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryL |
| 202 | it("resets adapterKey to the new provider when provider changes without an explicit adapter key", () => | 199 | it("resets adapterKey to the new provider when provider changes without an explicit adapter key", () => |
| 203 | Effect.gen(function* () { | 200 | Effect.gen(function* () { |
| 204 | const directory = yield* ProviderSessionDirectory; | 201 | const directory = yield* ProviderSessionDirectory; |
| 205 | const runtimeRepository = yield* ProviderSessionRuntimeRepository; | 202 | const runtimeRepository = yield* ProviderSessionRuntime.ProviderSessionRuntimeRepository; |
| 206 | const threadId = ThreadId.make("thread-provider-change"); | 203 | const threadId = ThreadId.make("thread-provider-change"); |
| 207 | 204 | ||
| 208 | yield* runtimeRepository.upsert({ | 205 | yield* runtimeRepository.upsert({ |
apps/server/src/provider/Layers/ProviderSessionDirectory.ts+3-4| ... | @@ -5,8 +5,7 @@ import * as Layer from "effect/Layer"; | ... | @@ -5,8 +5,7 @@ import * as Layer from "effect/Layer"; |
| 5 | import * as Option from "effect/Option"; | 5 | import * as Option from "effect/Option"; |
| 6 | import * as Schema from "effect/Schema"; | 6 | import * as Schema from "effect/Schema"; |
| 7 | 7 | ||
| 8 | import type { ProviderSessionRuntime } from "../../persistence/Services/ProviderSessionRuntime.ts"; | 8 | import * as ProviderSessionRuntime from "../../persistence/ProviderSessionRuntime.ts"; |
| 9 | import { ProviderSessionRuntimeRepository } from "../../persistence/Services/ProviderSessionRuntime.ts"; | ||
| 10 | import { ProviderSessionDirectoryPersistenceError, ProviderValidationError } from "../Errors.ts"; | 9 | import { ProviderSessionDirectoryPersistenceError, ProviderValidationError } from "../Errors.ts"; |
| 11 | import { | 10 | import { |
| 12 | ProviderSessionDirectory, | 11 | ProviderSessionDirectory, |
| ... | @@ -59,7 +58,7 @@ function mergeRuntimePayload( | ... | @@ -59,7 +58,7 @@ function mergeRuntimePayload( |
| 59 | } | 58 | } |
| 60 | 59 | ||
| 61 | function toRuntimeBinding( | 60 | function toRuntimeBinding( |
| 62 | runtime: ProviderSessionRuntime, | 61 | runtime: ProviderSessionRuntime.ProviderSessionRuntime, |
| 63 | operation: string, | 62 | operation: string, |
| 64 | ): Effect.Effect<ProviderRuntimeBindingWithMetadata, ProviderSessionDirectoryPersistenceError> { | 63 | ): Effect.Effect<ProviderRuntimeBindingWithMetadata, ProviderSessionDirectoryPersistenceError> { |
| 65 | return decodeProviderDriverKind(runtime.providerName, operation).pipe( | 64 | return decodeProviderDriverKind(runtime.providerName, operation).pipe( |
| ... | @@ -85,7 +84,7 @@ function toRuntimeBinding( | ... | @@ -85,7 +84,7 @@ function toRuntimeBinding( |
| 85 | } | 84 | } |
| 86 | 85 | ||
| 87 | const makeProviderSessionDirectory = Effect.gen(function* () { | 86 | const makeProviderSessionDirectory = Effect.gen(function* () { |
| 88 | const repository = yield* ProviderSessionRuntimeRepository; | 87 | const repository = yield* ProviderSessionRuntime.ProviderSessionRuntimeRepository; |
| 89 | 88 | ||
| 90 | const getBinding = (threadId: ThreadId) => | 89 | const getBinding = (threadId: ThreadId) => |
| 91 | repository.getByThreadId({ threadId }).pipe( | 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,8 +19,7 @@ import { afterEach, describe, expect, it, vi } from "vite-plus/test"; |
| 19 | 19 | ||
| 20 | import { ProjectionSnapshotQuery } from "../../orchestration/Services/ProjectionSnapshotQuery.ts"; | 20 | import { ProjectionSnapshotQuery } from "../../orchestration/Services/ProjectionSnapshotQuery.ts"; |
| 21 | import { SqlitePersistenceMemory } from "../../persistence/Layers/Sqlite.ts"; | 21 | import { SqlitePersistenceMemory } from "../../persistence/Layers/Sqlite.ts"; |
| 22 | import { ProviderSessionRuntimeRepositoryLive } from "../../persistence/Layers/ProviderSessionRuntime.ts"; | 22 | import * as ProviderSessionRuntime from "../../persistence/ProviderSessionRuntime.ts"; |
| 23 | import { ProviderSessionRuntimeRepository } from "../../persistence/Services/ProviderSessionRuntime.ts"; | ||
| 24 | import { ProviderValidationError } from "../Errors.ts"; | 23 | import { ProviderValidationError } from "../Errors.ts"; |
| 25 | import { ProviderSessionReaper } from "../Services/ProviderSessionReaper.ts"; | 24 | import { ProviderSessionReaper } from "../Services/ProviderSessionReaper.ts"; |
| 26 | import { ProviderService, type ProviderServiceShape } from "../Services/ProviderService.ts"; | 25 | import { ProviderService, type ProviderServiceShape } from "../Services/ProviderService.ts"; |
| ... | @@ -118,7 +117,7 @@ function makeReadModel( | ... | @@ -118,7 +117,7 @@ function makeReadModel( |
| 118 | 117 | ||
| 119 | describe("ProviderSessionReaper", () => { | 118 | describe("ProviderSessionReaper", () => { |
| 120 | let runtime: ManagedRuntime.ManagedRuntime< | 119 | let runtime: ManagedRuntime.ManagedRuntime< |
| 121 | ProviderSessionReaper | ProviderSessionRuntimeRepository, | 120 | ProviderSessionReaper | ProviderSessionRuntime.ProviderSessionRuntimeRepository, |
| 122 | unknown | 121 | unknown |
| 123 | > | null = null; | 122 | > | null = null; |
| 124 | let scope: Scope.Closeable | null = null; | 123 | let scope: Scope.Closeable | null = null; |
| ... | @@ -176,7 +175,7 @@ describe("ProviderSessionReaper", () => { | ... | @@ -176,7 +175,7 @@ describe("ProviderSessionReaper", () => { |
| 176 | streamEvents: Stream.empty, | 175 | streamEvents: Stream.empty, |
| 177 | }; | 176 | }; |
| 178 | 177 | ||
| 179 | const runtimeRepositoryLayer = ProviderSessionRuntimeRepositoryLive.pipe( | 178 | const runtimeRepositoryLayer = ProviderSessionRuntime.layer.pipe( |
| 180 | Layer.provide(SqlitePersistenceMemory), | 179 | Layer.provide(SqlitePersistenceMemory), |
| 181 | ); | 180 | ); |
| 182 | const providerSessionDirectoryLayer = ProviderSessionDirectoryLive.pipe( | 181 | const providerSessionDirectoryLayer = ProviderSessionDirectoryLive.pipe( |
| ... | @@ -238,7 +237,9 @@ describe("ProviderSessionReaper", () => { | ... | @@ -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 | await runtime!.runPromise( | 244 | await runtime!.runPromise( |
| 244 | repository.upsert({ | 245 | repository.upsert({ |
| ... | @@ -286,7 +287,9 @@ describe("ProviderSessionReaper", () => { | ... | @@ -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 | await runtime!.runPromise( | 294 | await runtime!.runPromise( |
| 292 | repository.upsert({ | 295 | repository.upsert({ |
| ... | @@ -333,7 +336,9 @@ describe("ProviderSessionReaper", () => { | ... | @@ -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 | await runtime!.runPromise( | 343 | await runtime!.runPromise( |
| 339 | repository.upsert({ | 344 | repository.upsert({ |
| ... | @@ -380,7 +385,9 @@ describe("ProviderSessionReaper", () => { | ... | @@ -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 | await runtime!.runPromise( | 392 | await runtime!.runPromise( |
| 386 | repository.upsert({ | 393 | repository.upsert({ |
| ... | @@ -449,7 +456,9 @@ describe("ProviderSessionReaper", () => { | ... | @@ -449,7 +456,9 @@ describe("ProviderSessionReaper", () => { |
| 449 | ) | 456 | ) |
| 450 | : Effect.void, | 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 | await runtime!.runPromise( | 463 | await runtime!.runPromise( |
| 455 | repository.upsert({ | 464 | repository.upsert({ |
| ... | @@ -530,7 +539,9 @@ describe("ProviderSessionReaper", () => { | ... | @@ -530,7 +539,9 @@ describe("ProviderSessionReaper", () => { |
| 530 | ? Effect.die(new Error("simulated stop defect")) | 539 | ? Effect.die(new Error("simulated stop defect")) |
| 531 | : Effect.void, | 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 | await runtime!.runPromise( | 546 | await runtime!.runPromise( |
| 536 | repository.upsert({ | 547 | repository.upsert({ |
apps/server/src/server.ts+2-2| ... | @@ -19,7 +19,7 @@ import { layerConfig as SqlitePersistenceLayerLive } from "./persistence/Layers/ | ... | @@ -19,7 +19,7 @@ import { layerConfig as SqlitePersistenceLayerLive } from "./persistence/Layers/ |
| 19 | import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts"; | 19 | import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts"; |
| 20 | import { AnalyticsServiceLayerLive } from "./telemetry/Layers/AnalyticsService.ts"; | 20 | import { AnalyticsServiceLayerLive } from "./telemetry/Layers/AnalyticsService.ts"; |
| 21 | import { ProviderSessionDirectoryLive } from "./provider/Layers/ProviderSessionDirectory.ts"; | 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 | import { ProviderAdapterRegistryLive } from "./provider/Layers/ProviderAdapterRegistry.ts"; | 23 | import { ProviderAdapterRegistryLive } from "./provider/Layers/ProviderAdapterRegistry.ts"; |
| 24 | import * as ProviderEventLoggers from "./provider/Layers/ProviderEventLoggers.ts"; | 24 | import * as ProviderEventLoggers from "./provider/Layers/ProviderEventLoggers.ts"; |
| 25 | import { ProviderServiceLive } from "./provider/Layers/ProviderService.ts"; | 25 | import { ProviderServiceLive } from "./provider/Layers/ProviderService.ts"; |
| ... | @@ -166,7 +166,7 @@ const ReactorLayerLive = Layer.empty.pipe( | ... | @@ -166,7 +166,7 @@ const ReactorLayerLive = Layer.empty.pipe( |
| 166 | ); | 166 | ); |
| 167 | 167 | ||
| 168 | const ProviderSessionDirectoryLayerLive = ProviderSessionDirectoryLive.pipe( | 168 | const ProviderSessionDirectoryLayerLive = ProviderSessionDirectoryLive.pipe( |
| 169 | Layer.provide(ProviderSessionRuntimeRepositoryLive), | 169 | Layer.provide(ProviderSessionRuntime.layer), |
| 170 | ); | 170 | ); |
| 171 | 171 | ||
| 172 | // `ProviderAdapterRegistryLive` is now a facade that resolves kind → adapter | 172 | // `ProviderAdapterRegistryLive` is now a facade that resolves kind → adapter |