authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-19 21:14:18-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-19 21:14:18-07:00
logdf1540f3ac8fd70877a83e91bd0be182ebf5022c
treededf5c2a850411924d11d29b414e518c59700184
parentd5a72d5be105d16a95df6dc18ad08f0b0cdb7efb
signature Signed by PGP key B5690EEEBB952194

[codex] align persistence Effect service modules (#3184)

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 {
25import { ServerSettingsService } from "../src/serverSettings.ts";25import { ServerSettingsService } from "../src/serverSettings.ts";
26import { AnalyticsService } from "../src/telemetry/Services/AnalyticsService.ts";26import { AnalyticsService } from "../src/telemetry/Services/AnalyticsService.ts";
27import { SqlitePersistenceMemory } from "../src/persistence/Layers/Sqlite.ts";27import { SqlitePersistenceMemory } from "../src/persistence/Layers/Sqlite.ts";
28import { ProviderSessionRuntimeRepositoryLive } from "../src/persistence/Layers/ProviderSessionRuntime.ts";28import * as ProviderSessionRuntime from "../src/persistence/ProviderSessionRuntime.ts";
2929
30import {30import {
31 makeTestProviderAdapterHarness,31 makeTestProviderAdapterHarness,
...@@ -63,7 +63,7 @@ const makeIntegrationFixture = Effect.gen(function* () {...@@ -63,7 +63,7 @@ const makeIntegrationFixture = Effect.gen(function* () {
63 });63 });
6464
65 const directoryLayer = ProviderSessionDirectoryLive.pipe(65 const directoryLayer = ProviderSessionDirectoryLive.pipe(
66 Layer.provide(ProviderSessionRuntimeRepositoryLive),66 Layer.provide(ProviderSessionRuntime.layer),
67 );67 );
6868
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";
18import * as Option from "effect/Option";18import * as Option from "effect/Option";
1919
20import { ServerConfig } from "../config.ts";20import { ServerConfig } from "../config.ts";
21import { AuthPairingLinkRepositoryLive } from "../persistence/Layers/AuthPairingLinks.ts";21import * as AuthPairingLinks from "../persistence/AuthPairingLinks.ts";
22import { AuthPairingLinkRepository } from "../persistence/Services/AuthPairingLinks.ts";
2322
24export interface BootstrapGrant {23export 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) =>
126export const make = Effect.fn("makePairingGrantStore")(function* () {125export 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});
418417
419export const layer = Layer.effect(PairingGrantStore, make()).pipe(418export 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";
8import * as ServerConfig from "../config.ts";8import * as ServerConfig from "../config.ts";
9import { PersistenceSqlError } from "../persistence/Errors.ts";9import { PersistenceSqlError } from "../persistence/Errors.ts";
10import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";10import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";
11import * as AuthSessions from "../persistence/Services/AuthSessions.ts";11import * as AuthSessions from "../persistence/AuthSessions.ts";
12import * as SessionStore from "./SessionStore.ts";12import * as SessionStore from "./SessionStore.ts";
13import * as ServerSecretStore from "./ServerSecretStore.ts";13import * as ServerSecretStore from "./ServerSecretStore.ts";
1414
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";
22import * as Option from "effect/Option";22import * as Option from "effect/Option";
2323
24import { ServerConfig } from "../config.ts";24import { ServerConfig } from "../config.ts";
25import { AuthSessionRepositoryLive } from "../persistence/Layers/AuthSessions.ts";25import * as AuthSessions from "../persistence/AuthSessions.ts";
26import { AuthSessionRepository } from "../persistence/Services/AuthSessions.ts";
27import * as ServerSecretStore from "./ServerSecretStore.ts";26import * as ServerSecretStore from "./ServerSecretStore.ts";
28import {27import {
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});
641640
642export const layer = Layer.effect(SessionStore, make()).pipe(641export 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 @@
1import * as Context from "effect/Context";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as Option from "effect/Option";
5import * as Schema from "effect/Schema";
6import * as SqlClient from "effect/unstable/sql/SqlClient";
7import * as SqlSchema from "effect/unstable/sql/SqlSchema";
8
9import { AuthEnvironmentScopes } from "@t3tools/contracts";
10
11import {
12 type AuthPairingLinkRepositoryError,
13 toPersistenceDecodeError,
14 toPersistenceSqlError,
15} from "./Errors.ts";
16
17export 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});
30export type AuthPairingLinkRecord = typeof AuthPairingLinkRecord.Type;
31
32export 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});
43export type CreateAuthPairingLinkInput = typeof CreateAuthPairingLinkInput.Type;
44
45export const ConsumeAuthPairingLinkInput = Schema.Struct({
46 credential: Schema.String,
47 proofKeyThumbprint: Schema.NullOr(Schema.String),
48 consumedAt: Schema.DateTimeUtcFromString,
49 now: Schema.DateTimeUtcFromString,
50});
51export type ConsumeAuthPairingLinkInput = typeof ConsumeAuthPairingLinkInput.Type;
52
53export const ListActiveAuthPairingLinksInput = Schema.Struct({
54 now: Schema.DateTimeUtcFromString,
55});
56export type ListActiveAuthPairingLinksInput = typeof ListActiveAuthPairingLinksInput.Type;
57
58export const RevokeAuthPairingLinkInput = Schema.Struct({
59 id: Schema.String,
60 revokedAt: Schema.DateTimeUtcFromString,
61});
62export type RevokeAuthPairingLinkInput = typeof RevokeAuthPairingLinkInput.Type;
63
64export const GetAuthPairingLinkByCredentialInput = Schema.Struct({
65 credential: Schema.String,
66});
67export type GetAuthPairingLinkByCredentialInput = typeof GetAuthPairingLinkByCredentialInput.Type;
68
69export 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
90function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) {
91 return (cause: unknown): AuthPairingLinkRepositoryError =>
92 Schema.isSchemaError(cause)
93 ? toPersistenceDecodeError(decodeOperation)(cause)
94 : toPersistenceSqlError(sqlOperation)(cause);
95}
96
97export 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
284export const layer = Layer.effect(AuthPairingLinkRepository, make);
apps/server/src/persistence/AuthSessions.ts created+361
...@@ -0,0 +1,361 @@
1import * as Context from "effect/Context";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as Option from "effect/Option";
5import * as Schema from "effect/Schema";
6import * as SqlClient from "effect/unstable/sql/SqlClient";
7import * as SqlSchema from "effect/unstable/sql/SqlSchema";
8
9import {
10 AuthClientMetadataDeviceType,
11 AuthEnvironmentScopes,
12 AuthSessionId,
13 ServerAuthSessionMethod,
14} from "@t3tools/contracts";
15
16import {
17 type AuthSessionRepositoryError,
18 toPersistenceDecodeError,
19 toPersistenceSqlError,
20} from "./Errors.ts";
21
22export 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});
30export type AuthSessionClientMetadataRecord = typeof AuthSessionClientMetadataRecord.Type;
31
32export 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});
43export type AuthSessionRecord = typeof AuthSessionRecord.Type;
44
45export 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});
54export type CreateAuthSessionInput = typeof CreateAuthSessionInput.Type;
55
56export const GetAuthSessionByIdInput = Schema.Struct({
57 sessionId: AuthSessionId,
58});
59export type GetAuthSessionByIdInput = typeof GetAuthSessionByIdInput.Type;
60
61export const ListActiveAuthSessionsInput = Schema.Struct({
62 now: Schema.DateTimeUtcFromString,
63});
64export type ListActiveAuthSessionsInput = typeof ListActiveAuthSessionsInput.Type;
65
66export const RevokeAuthSessionInput = Schema.Struct({
67 sessionId: AuthSessionId,
68 revokedAt: Schema.DateTimeUtcFromString,
69});
70export type RevokeAuthSessionInput = typeof RevokeAuthSessionInput.Type;
71
72export const RevokeOtherAuthSessionsInput = Schema.Struct({
73 currentSessionId: AuthSessionId,
74 revokedAt: Schema.DateTimeUtcFromString,
75});
76export type RevokeOtherAuthSessionsInput = typeof RevokeOtherAuthSessionsInput.Type;
77
78export const SetAuthSessionLastConnectedAtInput = Schema.Struct({
79 sessionId: AuthSessionId,
80 lastConnectedAt: Schema.DateTimeUtcFromString,
81});
82export type SetAuthSessionLastConnectedAtInput = typeof SetAuthSessionLastConnectedAtInput.Type;
83
84export 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
108const 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
125function 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
146function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) {
147 return (cause: unknown): AuthSessionRepositoryError =>
148 Schema.isSchemaError(cause)
149 ? toPersistenceDecodeError(decodeOperation)(cause)
150 : toPersistenceSqlError(sqlOperation)(cause);
151}
152
153export 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
361export const layer = Layer.effect(AuthSessionRepository, make);
apps/server/src/persistence/Layers/AuthPairingLinks.ts deleted-220
...@@ -1,220 +0,0 @@
1import * as SqlClient from "effect/unstable/sql/SqlClient";
2import * as SqlSchema from "effect/unstable/sql/SqlSchema";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Schema from "effect/Schema";
6
7import {
8 toPersistenceDecodeError,
9 toPersistenceSqlError,
10 type AuthPairingLinkRepositoryError,
11} from "../Errors.ts";
12import {
13 AuthPairingLinkRecord,
14 AuthPairingLinkRepository,
15 type AuthPairingLinkRepositoryShape,
16 ConsumeAuthPairingLinkInput,
17 CreateAuthPairingLinkInput,
18 GetAuthPairingLinkByCredentialInput,
19 ListActiveAuthPairingLinksInput,
20 RevokeAuthPairingLinkInput,
21} from "../Services/AuthPairingLinks.ts";
22
23function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) {
24 return (cause: unknown): AuthPairingLinkRepositoryError =>
25 Schema.isSchemaError(cause)
26 ? toPersistenceDecodeError(decodeOperation)(cause)
27 : toPersistenceSqlError(sqlOperation)(cause);
28}
29
30const 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
217export const AuthPairingLinkRepositoryLive = Layer.effect(
218 AuthPairingLinkRepository,
219 makeAuthPairingLinkRepository,
220);
apps/server/src/persistence/Layers/AuthSessions.ts deleted-282
...@@ -1,282 +0,0 @@
1import { AuthEnvironmentScopes, AuthSessionId, ServerAuthSessionMethod } from "@t3tools/contracts";
2import * as SqlClient from "effect/unstable/sql/SqlClient";
3import * as SqlSchema from "effect/unstable/sql/SqlSchema";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as Option from "effect/Option";
7import * as Schema from "effect/Schema";
8
9import {
10 toPersistenceDecodeError,
11 toPersistenceSqlError,
12 type AuthSessionRepositoryError,
13} from "../Errors.ts";
14import {
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
26const 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
43function 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
64function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) {
65 return (cause: unknown): AuthSessionRepositoryError =>
66 Schema.isSchemaError(cause)
67 ? toPersistenceDecodeError(decodeOperation)(cause)
68 : toPersistenceSqlError(sqlOperation)(cause);
69}
70
71const 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
279export 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 @@
1import { ThreadId } from "@t3tools/contracts";1/** @deprecated Compatibility alias for the excluded orchestration integration harness. */
2import * as SqlClient from "effect/unstable/sql/SqlClient";2export { layer as ProviderSessionRuntimeRepositoryLive } from "../ProviderSessionRuntime.ts";
3import * as SqlSchema from "effect/unstable/sql/SqlSchema";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as Option from "effect/Option";
7import * as Schema from "effect/Schema";
8import * as Struct from "effect/Struct";
9
10import {
11 toPersistenceDecodeError,
12 toPersistenceSqlError,
13 type ProviderSessionRuntimeRepositoryError,
14} from "../Errors.ts";
15import {
16 ProviderSessionRuntime,
17 ProviderSessionRuntimeRepository,
18 type ProviderSessionRuntimeRepositoryShape,
19} from "../Services/ProviderSessionRuntime.ts";
20
21const 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
28const decodeRuntime = Schema.decodeUnknownEffect(ProviderSessionRuntime);
29
30const GetRuntimeRequestSchema = Schema.Struct({
31 threadId: ThreadId,
32});
33
34const DeleteRuntimeRequestSchema = GetRuntimeRequestSchema;
35
36function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) {
37 return (cause: unknown): ProviderSessionRuntimeRepositoryError =>
38 Schema.isSchemaError(cause)
39 ? toPersistenceDecodeError(decodeOperation)(cause)
40 : toPersistenceSqlError(sqlOperation)(cause);
41}
42
43const 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
205export 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";
2929
30export type TypeId = "~local/sqlite-node/SqliteClient";30export type TypeId = "~local/sqlite-node/SqliteClient";
3131
32/**
33 * SqliteClient - Effect service tag for the sqlite SQL client.
34 */
35export const SqliteClient = Context.Service<Client.SqlClient>("t3/persistence/NodeSqliteClient");
36
37export interface SqliteClientConfig {32export 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 = (
251export const layerConfig = (246export 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));
262252
263export const layer = (config: SqliteClientConfig): Layer.Layer<Client.SqlClient> =>253export 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));
269255
270export const layerMemory = (config: SqliteMemoryClientConfig = {}): Layer.Layer<Client.SqlClient> =>256export 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 @@
1import * as Context from "effect/Context";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as Option from "effect/Option";
5import * as Schema from "effect/Schema";
6import * as Struct from "effect/Struct";
7import * as SqlClient from "effect/unstable/sql/SqlClient";
8import * as SqlSchema from "effect/unstable/sql/SqlSchema";
9
10import {
11 IsoDateTime,
12 ProviderInstanceId,
13 ProviderSessionRuntimeStatus,
14 RuntimeMode,
15 ThreadId,
16} from "@t3tools/contracts";
17
18import {
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
32export 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});
50export type ProviderSessionRuntime = typeof ProviderSessionRuntime.Type;
51
52export const GetProviderSessionRuntimeInput = Schema.Struct({ threadId: ThreadId });
53export type GetProviderSessionRuntimeInput = typeof GetProviderSessionRuntimeInput.Type;
54
55export const DeleteProviderSessionRuntimeInput = Schema.Struct({ threadId: ThreadId });
56export type DeleteProviderSessionRuntimeInput = typeof DeleteProviderSessionRuntimeInput.Type;
57
58/**
59 * ProviderSessionRuntimeRepository - Service tag for provider runtime persistence.
60 */
61export 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
102const 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
109const decodeRuntime = Schema.decodeUnknownEffect(ProviderSessionRuntime);
110
111const GetRuntimeRequestSchema = Schema.Struct({
112 threadId: ThreadId,
113});
114
115const DeleteRuntimeRequestSchema = GetRuntimeRequestSchema;
116
117function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) {
118 return (cause: unknown): ProviderSessionRuntimeRepositoryError =>
119 Schema.isSchemaError(cause)
120 ? toPersistenceDecodeError(decodeOperation)(cause)
121 : toPersistenceSqlError(sqlOperation)(cause);
122}
123
124export 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
288export const layer = Layer.effect(ProviderSessionRuntimeRepository, make);
apps/server/src/persistence/Services/AuthPairingLinks.ts deleted-82
...@@ -1,82 +0,0 @@
1import * as Option from "effect/Option";
2import * as Schema from "effect/Schema";
3import * as Context from "effect/Context";
4import type * as Effect from "effect/Effect";
5import { AuthEnvironmentScopes } from "@t3tools/contracts";
6
7import type { AuthPairingLinkRepositoryError } from "../Errors.ts";
8
9export 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});
22export type AuthPairingLinkRecord = typeof AuthPairingLinkRecord.Type;
23
24export 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});
35export type CreateAuthPairingLinkInput = typeof CreateAuthPairingLinkInput.Type;
36
37export const ConsumeAuthPairingLinkInput = Schema.Struct({
38 credential: Schema.String,
39 proofKeyThumbprint: Schema.NullOr(Schema.String),
40 consumedAt: Schema.DateTimeUtcFromString,
41 now: Schema.DateTimeUtcFromString,
42});
43export type ConsumeAuthPairingLinkInput = typeof ConsumeAuthPairingLinkInput.Type;
44
45export const ListActiveAuthPairingLinksInput = Schema.Struct({
46 now: Schema.DateTimeUtcFromString,
47});
48export type ListActiveAuthPairingLinksInput = typeof ListActiveAuthPairingLinksInput.Type;
49
50export const RevokeAuthPairingLinkInput = Schema.Struct({
51 id: Schema.String,
52 revokedAt: Schema.DateTimeUtcFromString,
53});
54export type RevokeAuthPairingLinkInput = typeof RevokeAuthPairingLinkInput.Type;
55
56export const GetAuthPairingLinkByCredentialInput = Schema.Struct({
57 credential: Schema.String,
58});
59export type GetAuthPairingLinkByCredentialInput = typeof GetAuthPairingLinkByCredentialInput.Type;
60
61export 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
79export 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 @@
1import {
2 AuthClientMetadataDeviceType,
3 AuthEnvironmentScopes,
4 AuthSessionId,
5 ServerAuthSessionMethod,
6} from "@t3tools/contracts";
7import * as Option from "effect/Option";
8import * as Schema from "effect/Schema";
9import * as Context from "effect/Context";
10import type * as Effect from "effect/Effect";
11
12import type { AuthSessionRepositoryError } from "../Errors.ts";
13
14export 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});
22export type AuthSessionClientMetadataRecord = typeof AuthSessionClientMetadataRecord.Type;
23
24export 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});
35export type AuthSessionRecord = typeof AuthSessionRecord.Type;
36
37export 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});
46export type CreateAuthSessionInput = typeof CreateAuthSessionInput.Type;
47
48export const GetAuthSessionByIdInput = Schema.Struct({
49 sessionId: AuthSessionId,
50});
51export type GetAuthSessionByIdInput = typeof GetAuthSessionByIdInput.Type;
52
53export const ListActiveAuthSessionsInput = Schema.Struct({
54 now: Schema.DateTimeUtcFromString,
55});
56export type ListActiveAuthSessionsInput = typeof ListActiveAuthSessionsInput.Type;
57
58export const RevokeAuthSessionInput = Schema.Struct({
59 sessionId: AuthSessionId,
60 revokedAt: Schema.DateTimeUtcFromString,
61});
62export type RevokeAuthSessionInput = typeof RevokeAuthSessionInput.Type;
63
64export const RevokeOtherAuthSessionsInput = Schema.Struct({
65 currentSessionId: AuthSessionId,
66 revokedAt: Schema.DateTimeUtcFromString,
67});
68export type RevokeOtherAuthSessionsInput = typeof RevokeOtherAuthSessionsInput.Type;
69
70export const SetAuthSessionLastConnectedAtInput = Schema.Struct({
71 sessionId: AuthSessionId,
72 lastConnectedAt: Schema.DateTimeUtcFromString,
73});
74export type SetAuthSessionLastConnectedAtInput = typeof SetAuthSessionLastConnectedAtInput.Type;
75
76export 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
97export 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 */
8import {
9 IsoDateTime,
10 ProviderInstanceId,
11 ProviderSessionRuntimeStatus,
12 RuntimeMode,
13 ThreadId,
14} from "@t3tools/contracts";
15import * as Option from "effect/Option";
16import * as Schema from "effect/Schema";
17import * as Context from "effect/Context";
18import type * as Effect from "effect/Effect";
19
20import type { ProviderSessionRuntimeRepositoryError } from "../Errors.ts";
21
22export 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});
40export type ProviderSessionRuntime = typeof ProviderSessionRuntime.Type;
41
42export const GetProviderSessionRuntimeInput = Schema.Struct({ threadId: ThreadId });
43export type GetProviderSessionRuntimeInput = typeof GetProviderSessionRuntimeInput.Type;
44
45export const DeleteProviderSessionRuntimeInput = Schema.Struct({ threadId: ThreadId });
46export type DeleteProviderSessionRuntimeInput = typeof DeleteProviderSessionRuntimeInput.Type;
47
48/**
49 * ProviderSessionRuntimeRepositoryShape - Service API for provider runtime records.
50 */
51export 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 */
89export 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";
53import { NoOpProviderEventLoggers, ProviderEventLoggers } from "./ProviderEventLoggers.ts";53import { NoOpProviderEventLoggers, ProviderEventLoggers } from "./ProviderEventLoggers.ts";
54import { ProviderSessionDirectoryLive } from "./ProviderSessionDirectory.ts";54import { ProviderSessionDirectoryLive } from "./ProviderSessionDirectory.ts";
55import * as NodeServices from "@effect/platform-node/NodeServices";55import * as NodeServices from "@effect/platform-node/NodeServices";
56import { ProviderSessionRuntimeRepositoryLive } from "../../persistence/Layers/ProviderSessionRuntime.ts";56import * as ProviderSessionRuntime from "../../persistence/ProviderSessionRuntime.ts";
57import { ProviderSessionRuntimeRepository } from "../../persistence/Services/ProviderSessionRuntime.ts";
58import {57import {
59 makeSqlitePersistenceLive,58 makeSqlitePersistenceLive,
60 SqlitePersistenceMemory,59 SqlitePersistenceMemory,
...@@ -282,7 +281,7 @@ function makeProviderServiceLayer() {...@@ -282,7 +281,7 @@ function makeProviderServiceLayer() {
282 });281 });
283282
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 });
613612
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");
644643
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 );
677676
...@@ -717,7 +716,7 @@ it.effect(...@@ -717,7 +716,7 @@ it.effect(
717 }).pipe(Effect.provide(firstProviderLayer));716 }).pipe(Effect.provide(firstProviderLayer));
718717
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;
913912
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;
11831182
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 );
12321231
...@@ -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 );
13221321
...@@ -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;
17651764
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";
19import { ProviderSessionRuntimeRepositoryLive } from "../../persistence/Layers/ProviderSessionRuntime.ts";19import * as ProviderSessionRuntime from "../../persistence/ProviderSessionRuntime.ts";
20import { ProviderSessionRuntimeRepository } from "../../persistence/Services/ProviderSessionRuntime.ts";
21import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts";20import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts";
22import { ProviderSessionDirectoryLive } from "./ProviderSessionDirectory.ts";21import { ProviderSessionDirectoryLive } from "./ProviderSessionDirectory.ts";
2322
24function makeDirectoryLayer<E, R>(persistenceLayer: Layer.Layer<SqlClient.SqlClient, E, R>) {23function 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;
4037
41 const initialThreadId = ThreadId.make("thread-1");38 const initialThreadId = ThreadId.make("thread-1");
4239
...@@ -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;
8784
88 const threadId = ThreadId.make("thread-runtime");85 const threadId = ThreadId.make("thread-runtime");
8986
...@@ -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;
132129
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");
207204
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";
5import * as Option from "effect/Option";5import * as Option from "effect/Option";
6import * as Schema from "effect/Schema";6import * as Schema from "effect/Schema";
77
8import type { ProviderSessionRuntime } from "../../persistence/Services/ProviderSessionRuntime.ts";8import * as ProviderSessionRuntime from "../../persistence/ProviderSessionRuntime.ts";
9import { ProviderSessionRuntimeRepository } from "../../persistence/Services/ProviderSessionRuntime.ts";
10import { ProviderSessionDirectoryPersistenceError, ProviderValidationError } from "../Errors.ts";9import { ProviderSessionDirectoryPersistenceError, ProviderValidationError } from "../Errors.ts";
11import {10import {
12 ProviderSessionDirectory,11 ProviderSessionDirectory,
...@@ -59,7 +58,7 @@ function mergeRuntimePayload(...@@ -59,7 +58,7 @@ function mergeRuntimePayload(
59}58}
6059
61function toRuntimeBinding(60function 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}
8685
87const makeProviderSessionDirectory = Effect.gen(function* () {86const makeProviderSessionDirectory = Effect.gen(function* () {
88 const repository = yield* ProviderSessionRuntimeRepository;87 const repository = yield* ProviderSessionRuntime.ProviderSessionRuntimeRepository;
8988
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";
1919
20import { ProjectionSnapshotQuery } from "../../orchestration/Services/ProjectionSnapshotQuery.ts";20import { ProjectionSnapshotQuery } from "../../orchestration/Services/ProjectionSnapshotQuery.ts";
21import { SqlitePersistenceMemory } from "../../persistence/Layers/Sqlite.ts";21import { SqlitePersistenceMemory } from "../../persistence/Layers/Sqlite.ts";
22import { ProviderSessionRuntimeRepositoryLive } from "../../persistence/Layers/ProviderSessionRuntime.ts";22import * as ProviderSessionRuntime from "../../persistence/ProviderSessionRuntime.ts";
23import { ProviderSessionRuntimeRepository } from "../../persistence/Services/ProviderSessionRuntime.ts";
24import { ProviderValidationError } from "../Errors.ts";23import { ProviderValidationError } from "../Errors.ts";
25import { ProviderSessionReaper } from "../Services/ProviderSessionReaper.ts";24import { ProviderSessionReaper } from "../Services/ProviderSessionReaper.ts";
26import { ProviderService, type ProviderServiceShape } from "../Services/ProviderService.ts";25import { ProviderService, type ProviderServiceShape } from "../Services/ProviderService.ts";
...@@ -118,7 +117,7 @@ function makeReadModel(...@@ -118,7 +117,7 @@ function makeReadModel(
118117
119describe("ProviderSessionReaper", () => {118describe("ProviderSessionReaper", () => {
120 let runtime: ManagedRuntime.ManagedRuntime<119 let runtime: ManagedRuntime.ManagedRuntime<
121 ProviderSessionReaper | ProviderSessionRuntimeRepository,120 ProviderSessionReaper | ProviderSessionRuntime.ProviderSessionRuntimeRepository,
122 unknown121 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 };
178177
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 );
242243
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 );
290293
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 );
337342
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 );
384391
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 );
453462
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 );
534545
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/
19import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts";19import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts";
20import { AnalyticsServiceLayerLive } from "./telemetry/Layers/AnalyticsService.ts";20import { AnalyticsServiceLayerLive } from "./telemetry/Layers/AnalyticsService.ts";
21import { ProviderSessionDirectoryLive } from "./provider/Layers/ProviderSessionDirectory.ts";21import { ProviderSessionDirectoryLive } from "./provider/Layers/ProviderSessionDirectory.ts";
22import { ProviderSessionRuntimeRepositoryLive } from "./persistence/Layers/ProviderSessionRuntime.ts";22import * as ProviderSessionRuntime from "./persistence/ProviderSessionRuntime.ts";
23import { ProviderAdapterRegistryLive } from "./provider/Layers/ProviderAdapterRegistry.ts";23import { ProviderAdapterRegistryLive } from "./provider/Layers/ProviderAdapterRegistry.ts";
24import * as ProviderEventLoggers from "./provider/Layers/ProviderEventLoggers.ts";24import * as ProviderEventLoggers from "./provider/Layers/ProviderEventLoggers.ts";
25import { ProviderServiceLive } from "./provider/Layers/ProviderService.ts";25import { 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);
167167
168const ProviderSessionDirectoryLayerLive = ProviderSessionDirectoryLive.pipe(168const ProviderSessionDirectoryLayerLive = ProviderSessionDirectoryLive.pipe(
169 Layer.provide(ProviderSessionRuntimeRepositoryLive),169 Layer.provide(ProviderSessionRuntime.layer),
170);170);
171171
172// `ProviderAdapterRegistryLive` is now a facade that resolves kind → adapter172// `ProviderAdapterRegistryLive` is now a facade that resolves kind → adapter