authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 16:15:16-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 16:15:16-07:00
log57d25c934b74986423896a7dc433595ed09b9c8d
treef44703de106959cfe7d190f811cce0fdbcd92fb1
parenta26f0dc89fd46f88529823511cdd068c9cd2c151
signature Signed by PGP key B5690EEEBB952194

[codex] Structure persistence error correlation (#3439)

Co-authored-by: codex <codex@users.noreply.github.com>

5 files changed, 451 insertions(+), 32 deletions(-)

apps/server/src/persistence/AuthPairingLinks.ts+78-6
......@@ -11,6 +11,7 @@ import { AuthEnvironmentScopes } from "@t3tools/contracts";
1111import {
1212 type AuthPairingLinkRepositoryError,
1313 PersistenceDecodeError,
14 type PersistenceErrorCorrelation,
1415 PersistenceSqlError,
1516} from "./Errors.ts";
1617
......@@ -66,6 +67,22 @@ export const GetAuthPairingLinkByCredentialInput = Schema.Struct({
6667});
6768export type GetAuthPairingLinkByCredentialInput = typeof GetAuthPairingLinkByCredentialInput.Type;
6869
70const AuthPairingLinkRawDbRow = Schema.Struct({
71 id: Schema.String,
72 credential: Schema.Unknown,
73 method: Schema.Unknown,
74 scopes: Schema.Unknown,
75 subject: Schema.Unknown,
76 label: Schema.Unknown,
77 proofKeyThumbprint: Schema.Unknown,
78 createdAt: Schema.Unknown,
79 expiresAt: Schema.Unknown,
80 consumedAt: Schema.Unknown,
81 revokedAt: Schema.Unknown,
82});
83
84const decodeAuthPairingLinkDbRow = Schema.decodeUnknownEffect(AuthPairingLinkRecord);
85
6986export class AuthPairingLinkRepository extends Context.Service<
7087 AuthPairingLinkRepository,
7188 {
......@@ -87,11 +104,19 @@ export class AuthPairingLinkRepository extends Context.Service<
87104 }
88105>()("t3/persistence/AuthPairingLinks/AuthPairingLinkRepository") {}
89106
90function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) {
107function toPersistenceSqlOrDecodeError(
108 sqlOperation: string,
109 decodeOperation: string,
110 correlation?: PersistenceErrorCorrelation,
111) {
91112 return (cause: unknown): AuthPairingLinkRepositoryError =>
92113 Schema.isSchemaError(cause)
93 ? PersistenceDecodeError.fromSchemaError(decodeOperation, cause)
94 : new PersistenceSqlError({ operation: sqlOperation, cause });
114 ? PersistenceDecodeError.fromSchemaError(decodeOperation, cause, correlation)
115 : new PersistenceSqlError({
116 operation: sqlOperation,
117 ...(correlation === undefined ? {} : { correlation }),
118 cause,
119 });
95120}
96121
97122export const make = Effect.gen(function* () {
......@@ -132,7 +157,7 @@ export const make = Effect.gen(function* () {
132157
133158 const consumeAvailablePairingLinkRow = SqlSchema.findOneOption({
134159 Request: ConsumeAuthPairingLinkInput,
135 Result: AuthPairingLinkRecord,
160 Result: AuthPairingLinkRawDbRow,
136161 execute: ({ credential, proofKeyThumbprint, consumedAt, now }) =>
137162 sql`
138163 UPDATE auth_pairing_links
......@@ -162,7 +187,7 @@ export const make = Effect.gen(function* () {
162187
163188 const listActivePairingLinkRows = SqlSchema.findAll({
164189 Request: ListActiveAuthPairingLinksInput,
165 Result: AuthPairingLinkRecord,
190 Result: AuthPairingLinkRawDbRow,
166191 execute: ({ now }) =>
167192 sql`
168193 SELECT
......@@ -201,7 +226,7 @@ export const make = Effect.gen(function* () {
201226
202227 const getPairingLinkRowByCredential = SqlSchema.findOneOption({
203228 Request: GetAuthPairingLinkByCredentialInput,
204 Result: AuthPairingLinkRecord,
229 Result: AuthPairingLinkRawDbRow,
205230 execute: ({ credential }) =>
206231 sql`
207232 SELECT
......@@ -227,6 +252,7 @@ export const make = Effect.gen(function* () {
227252 toPersistenceSqlOrDecodeError(
228253 "AuthPairingLinkRepository.create:query",
229254 "AuthPairingLinkRepository.create:encodeRequest",
255 { pairingLinkId: input.id },
230256 ),
231257 ),
232258 );
......@@ -239,6 +265,22 @@ export const make = Effect.gen(function* () {
239265 "AuthPairingLinkRepository.consumeAvailable:decodeRow",
240266 ),
241267 ),
268 Effect.flatMap((rowOption) =>
269 Option.match(rowOption, {
270 onNone: () => Effect.succeed(Option.none()),
271 onSome: (row) =>
272 decodeAuthPairingLinkDbRow(row).pipe(
273 Effect.mapError((cause) =>
274 PersistenceDecodeError.fromSchemaError(
275 "AuthPairingLinkRepository.consumeAvailable:decodeRow",
276 cause,
277 { pairingLinkId: row.id },
278 ),
279 ),
280 Effect.map(Option.some),
281 ),
282 }),
283 ),
242284 );
243285
244286 const listActive: AuthPairingLinkRepository["Service"]["listActive"] = (input) =>
......@@ -249,6 +291,19 @@ export const make = Effect.gen(function* () {
249291 "AuthPairingLinkRepository.listActive:decodeRows",
250292 ),
251293 ),
294 Effect.flatMap((rows) =>
295 Effect.forEach(rows, (row) =>
296 decodeAuthPairingLinkDbRow(row).pipe(
297 Effect.mapError((cause) =>
298 PersistenceDecodeError.fromSchemaError(
299 "AuthPairingLinkRepository.listActive:decodeRows",
300 cause,
301 { pairingLinkId: row.id },
302 ),
303 ),
304 ),
305 ),
306 ),
252307 );
253308
254309 const revoke: AuthPairingLinkRepository["Service"]["revoke"] = (input) =>
......@@ -257,6 +312,7 @@ export const make = Effect.gen(function* () {
257312 toPersistenceSqlOrDecodeError(
258313 "AuthPairingLinkRepository.revoke:query",
259314 "AuthPairingLinkRepository.revoke:decodeRows",
315 { pairingLinkId: input.id },
260316 ),
261317 ),
262318 Effect.map((rows) => rows.length > 0),
......@@ -270,6 +326,22 @@ export const make = Effect.gen(function* () {
270326 "AuthPairingLinkRepository.getByCredential:decodeRow",
271327 ),
272328 ),
329 Effect.flatMap((rowOption) =>
330 Option.match(rowOption, {
331 onNone: () => Effect.succeed(Option.none()),
332 onSome: (row) =>
333 decodeAuthPairingLinkDbRow(row).pipe(
334 Effect.mapError((cause) =>
335 PersistenceDecodeError.fromSchemaError(
336 "AuthPairingLinkRepository.getByCredential:decodeRow",
337 cause,
338 { pairingLinkId: row.id },
339 ),
340 ),
341 Effect.map(Option.some),
342 ),
343 }),
344 ),
273345 );
274346
275347 return {
apps/server/src/persistence/AuthSessions.ts+63-7
......@@ -16,6 +16,7 @@ import {
1616import {
1717 type AuthSessionRepositoryError,
1818 PersistenceDecodeError,
19 type PersistenceErrorCorrelation,
1920 PersistenceSqlError,
2021} from "./Errors.ts";
2122
......@@ -122,6 +123,25 @@ const AuthSessionDbRow = Schema.Struct({
122123 revokedAt: Schema.NullOr(Schema.DateTimeUtcFromString),
123124});
124125
126const AuthSessionRawDbRow = Schema.Struct({
127 sessionId: Schema.String,
128 subject: Schema.Unknown,
129 scopes: Schema.Unknown,
130 method: Schema.Unknown,
131 clientLabel: Schema.Unknown,
132 clientIpAddress: Schema.Unknown,
133 clientUserAgent: Schema.Unknown,
134 clientDeviceType: Schema.Unknown,
135 clientOs: Schema.Unknown,
136 clientBrowser: Schema.Unknown,
137 issuedAt: Schema.Unknown,
138 expiresAt: Schema.Unknown,
139 lastConnectedAt: Schema.Unknown,
140 revokedAt: Schema.Unknown,
141});
142
143const decodeAuthSessionDbRow = Schema.decodeUnknownEffect(AuthSessionDbRow);
144
125145function toAuthSessionRecord(row: typeof AuthSessionDbRow.Type): AuthSessionRecord {
126146 return {
127147 sessionId: row.sessionId,
......@@ -143,11 +163,19 @@ function toAuthSessionRecord(row: typeof AuthSessionDbRow.Type): AuthSessionReco
143163 };
144164}
145165
146function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) {
166function toPersistenceSqlOrDecodeError(
167 sqlOperation: string,
168 decodeOperation: string,
169 correlation?: PersistenceErrorCorrelation,
170) {
147171 return (cause: unknown): AuthSessionRepositoryError =>
148172 Schema.isSchemaError(cause)
149 ? PersistenceDecodeError.fromSchemaError(decodeOperation, cause)
150 : new PersistenceSqlError({ operation: sqlOperation, cause });
173 ? PersistenceDecodeError.fromSchemaError(decodeOperation, cause, correlation)
174 : new PersistenceSqlError({
175 operation: sqlOperation,
176 ...(correlation === undefined ? {} : { correlation }),
177 cause,
178 });
151179}
152180
153181export const make = Effect.gen(function* () {
......@@ -192,7 +220,7 @@ export const make = Effect.gen(function* () {
192220
193221 const getSessionRowById = SqlSchema.findOneOption({
194222 Request: GetAuthSessionByIdInput,
195 Result: AuthSessionDbRow,
223 Result: AuthSessionRawDbRow,
196224 execute: ({ sessionId }) =>
197225 sql`
198226 SELECT
......@@ -217,7 +245,7 @@ export const make = Effect.gen(function* () {
217245
218246 const listActiveSessionRows = SqlSchema.findAll({
219247 Request: ListActiveAuthSessionsInput,
220 Result: AuthSessionDbRow,
248 Result: AuthSessionRawDbRow,
221249 execute: ({ now }) =>
222250 sql`
223251 SELECT
......@@ -285,6 +313,7 @@ export const make = Effect.gen(function* () {
285313 toPersistenceSqlOrDecodeError(
286314 "AuthSessionRepository.create:query",
287315 "AuthSessionRepository.create:encodeRequest",
316 { sessionId: input.sessionId },
288317 ),
289318 ),
290319 );
......@@ -295,12 +324,23 @@ export const make = Effect.gen(function* () {
295324 toPersistenceSqlOrDecodeError(
296325 "AuthSessionRepository.getById:query",
297326 "AuthSessionRepository.getById:decodeRow",
327 { sessionId: input.sessionId },
298328 ),
299329 ),
300330 Effect.flatMap((rowOption) =>
301331 Option.match(rowOption, {
302332 onNone: () => Effect.succeed(Option.none()),
303 onSome: (row) => Effect.succeed(Option.some(toAuthSessionRecord(row))),
333 onSome: (row) =>
334 decodeAuthSessionDbRow(row).pipe(
335 Effect.mapError((cause) =>
336 PersistenceDecodeError.fromSchemaError(
337 "AuthSessionRepository.getById:decodeRow",
338 cause,
339 { sessionId: input.sessionId },
340 ),
341 ),
342 Effect.map((decodedRow) => Option.some(toAuthSessionRecord(decodedRow))),
343 ),
304344 }),
305345 ),
306346 );
......@@ -313,7 +353,20 @@ export const make = Effect.gen(function* () {
313353 "AuthSessionRepository.listActive:decodeRows",
314354 ),
315355 ),
316 Effect.flatMap((rows) => Effect.succeed(rows.map((row) => toAuthSessionRecord(row)))),
356 Effect.flatMap((rows) =>
357 Effect.forEach(rows, (row) =>
358 decodeAuthSessionDbRow(row).pipe(
359 Effect.mapError((cause) =>
360 PersistenceDecodeError.fromSchemaError(
361 "AuthSessionRepository.listActive:decodeRows",
362 cause,
363 { sessionId: row.sessionId },
364 ),
365 ),
366 Effect.map(toAuthSessionRecord),
367 ),
368 ),
369 ),
317370 );
318371
319372 const revoke: AuthSessionRepository["Service"]["revoke"] = (input) =>
......@@ -322,6 +375,7 @@ export const make = Effect.gen(function* () {
322375 toPersistenceSqlOrDecodeError(
323376 "AuthSessionRepository.revoke:query",
324377 "AuthSessionRepository.revoke:decodeRows",
378 { sessionId: input.sessionId },
325379 ),
326380 ),
327381 Effect.map((rows) => rows.length > 0),
......@@ -333,6 +387,7 @@ export const make = Effect.gen(function* () {
333387 toPersistenceSqlOrDecodeError(
334388 "AuthSessionRepository.revokeAllExcept:query",
335389 "AuthSessionRepository.revokeAllExcept:decodeRows",
390 { currentSessionId: input.currentSessionId },
336391 ),
337392 ),
338393 Effect.map((rows) => rows.map((row) => row.sessionId)),
......@@ -344,6 +399,7 @@ export const make = Effect.gen(function* () {
344399 toPersistenceSqlOrDecodeError(
345400 "AuthSessionRepository.setLastConnectedAt:query",
346401 "AuthSessionRepository.setLastConnectedAt:encodeRequest",
402 { sessionId: input.sessionId },
347403 ),
348404 ),
349405 );
apps/server/src/persistence/Errors.ts+16-1
......@@ -19,11 +19,20 @@ function summarizeSchemaIssue(issue: SchemaIssue.Issue): string {
1919// Core Persistence Errors
2020// ===============================
2121
22export const PersistenceErrorCorrelation = Schema.Union([
23 Schema.Struct({ sessionId: Schema.String }),
24 Schema.Struct({ currentSessionId: Schema.String }),
25 Schema.Struct({ pairingLinkId: Schema.String }),
26 Schema.Struct({ threadId: Schema.String }),
27]);
28export type PersistenceErrorCorrelation = typeof PersistenceErrorCorrelation.Type;
29
2230export class PersistenceSqlError extends Schema.TaggedErrorClass<PersistenceSqlError>()(
2331 "PersistenceSqlError",
2432 {
2533 operation: Schema.String,
2634 detail: Schema.optional(Schema.String),
35 correlation: Schema.optional(PersistenceErrorCorrelation),
2736 cause: Schema.optional(Schema.Defect()),
2837 },
2938) {
......@@ -39,13 +48,19 @@ export class PersistenceDecodeError extends Schema.TaggedErrorClass<PersistenceD
3948 {
4049 operation: Schema.String,
4150 issue: Schema.String,
51 correlation: Schema.optional(PersistenceErrorCorrelation),
4252 cause: Schema.optional(Schema.Defect()),
4353 },
4454) {
45 static fromSchemaError(operation: string, cause: Schema.SchemaError): PersistenceDecodeError {
55 static fromSchemaError(
56 operation: string,
57 cause: Schema.SchemaError,
58 correlation?: PersistenceErrorCorrelation,
59 ): PersistenceDecodeError {
4660 return new PersistenceDecodeError({
4761 operation,
4862 issue: summarizeSchemaIssue(cause.issue),
63 ...(correlation === undefined ? {} : { correlation }),
4964 cause,
5065 });
5166 }
apps/server/src/persistence/ProviderSessionRuntime.ts+41-18
......@@ -17,6 +17,7 @@ import {
1717
1818import {
1919 PersistenceDecodeError,
20 type PersistenceErrorCorrelation,
2021 PersistenceSqlError,
2122 type ProviderSessionRuntimeRepositoryError,
2223} from "./Errors.ts";
......@@ -106,7 +107,19 @@ const ProviderSessionRuntimeDbRowSchema = ProviderSessionRuntime.mapFields(
106107 }),
107108);
108109
109const decodeRuntime = Schema.decodeUnknownEffect(ProviderSessionRuntime);
110const ProviderSessionRuntimeRawDbRowSchema = Schema.Struct({
111 threadId: Schema.String,
112 providerName: Schema.Unknown,
113 providerInstanceId: Schema.Unknown,
114 adapterKey: Schema.Unknown,
115 runtimeMode: Schema.Unknown,
116 status: Schema.Unknown,
117 lastSeenAt: Schema.Unknown,
118 resumeCursor: Schema.Unknown,
119 runtimePayload: Schema.Unknown,
120});
121
122const decodeRuntimeRow = Schema.decodeUnknownEffect(ProviderSessionRuntimeDbRowSchema);
110123
111124const GetRuntimeRequestSchema = Schema.Struct({
112125 threadId: ThreadId,
......@@ -114,11 +127,19 @@ const GetRuntimeRequestSchema = Schema.Struct({
114127
115128const DeleteRuntimeRequestSchema = GetRuntimeRequestSchema;
116129
117function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) {
130function toPersistenceSqlOrDecodeError(
131 sqlOperation: string,
132 decodeOperation: string,
133 correlation?: PersistenceErrorCorrelation,
134) {
118135 return (cause: unknown): ProviderSessionRuntimeRepositoryError =>
119136 Schema.isSchemaError(cause)
120 ? PersistenceDecodeError.fromSchemaError(decodeOperation, cause)
121 : new PersistenceSqlError({ operation: sqlOperation, cause });
137 ? PersistenceDecodeError.fromSchemaError(decodeOperation, cause, correlation)
138 : new PersistenceSqlError({
139 operation: sqlOperation,
140 ...(correlation === undefined ? {} : { correlation }),
141 cause,
142 });
122143}
123144
124145export const make = Effect.gen(function* () {
......@@ -165,7 +186,7 @@ export const make = Effect.gen(function* () {
165186
166187 const getRuntimeRowByThreadId = SqlSchema.findOneOption({
167188 Request: GetRuntimeRequestSchema,
168 Result: ProviderSessionRuntimeDbRowSchema,
189 Result: ProviderSessionRuntimeRawDbRowSchema,
169190 execute: ({ threadId }) =>
170191 sql`
171192 SELECT
......@@ -185,7 +206,7 @@ export const make = Effect.gen(function* () {
185206
186207 const listRuntimeRows = SqlSchema.findAll({
187208 Request: Schema.Void,
188 Result: ProviderSessionRuntimeDbRowSchema,
209 Result: ProviderSessionRuntimeRawDbRowSchema,
189210 execute: () =>
190211 sql`
191212 SELECT
......@@ -218,6 +239,7 @@ export const make = Effect.gen(function* () {
218239 toPersistenceSqlOrDecodeError(
219240 "ProviderSessionRuntimeRepository.upsert:query",
220241 "ProviderSessionRuntimeRepository.upsert:encodeRequest",
242 { threadId: runtime.threadId },
221243 ),
222244 ),
223245 );
......@@ -228,17 +250,19 @@ export const make = Effect.gen(function* () {
228250 toPersistenceSqlOrDecodeError(
229251 "ProviderSessionRuntimeRepository.getByThreadId:query",
230252 "ProviderSessionRuntimeRepository.getByThreadId:decodeRow",
253 { threadId: input.threadId },
231254 ),
232255 ),
233256 Effect.flatMap((runtimeRowOption) =>
234257 Option.match(runtimeRowOption, {
235258 onNone: () => Effect.succeed(Option.none()),
236259 onSome: (row) =>
237 decodeRuntime(row).pipe(
260 decodeRuntimeRow(row).pipe(
238261 Effect.mapError((cause) =>
239262 PersistenceDecodeError.fromSchemaError(
240 "ProviderSessionRuntimeRepository.getByThreadId:rowToRuntime",
263 "ProviderSessionRuntimeRepository.getByThreadId:decodeRow",
241264 cause,
265 { threadId: input.threadId },
242266 ),
243267 ),
244268 Effect.map((runtime) => Option.some(runtime)),
......@@ -256,18 +280,16 @@ export const make = Effect.gen(function* () {
256280 ),
257281 ),
258282 Effect.flatMap((rows) =>
259 Effect.forEach(
260 rows,
261 (row) =>
262 decodeRuntime(row).pipe(
263 Effect.mapError((cause) =>
264 PersistenceDecodeError.fromSchemaError(
265 "ProviderSessionRuntimeRepository.list:rowToRuntime",
266 cause,
267 ),
283 Effect.forEach(rows, (row) =>
284 decodeRuntimeRow(row).pipe(
285 Effect.mapError((cause) =>
286 PersistenceDecodeError.fromSchemaError(
287 "ProviderSessionRuntimeRepository.list:decodeRows",
288 cause,
289 { threadId: row.threadId },
268290 ),
269291 ),
270 { concurrency: "unbounded" },
292 ),
271293 ),
272294 ),
273295 );
......@@ -280,6 +302,7 @@ export const make = Effect.gen(function* () {
280302 (cause) =>
281303 new PersistenceSqlError({
282304 operation: "ProviderSessionRuntimeRepository.deleteByThreadId:query",
305 correlation: { threadId: input.threadId },
283306 cause,
284307 }),
285308 ),
apps/server/src/persistence/RepositoryErrorCorrelation.test.ts created+253
......@@ -0,0 +1,253 @@
1import { AuthSessionId, ThreadId, type AuthEnvironmentScope } from "@t3tools/contracts";
2import { assert, describe, it } from "@effect/vitest";
3import * as DateTime from "effect/DateTime";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as SqlClient from "effect/unstable/sql/SqlClient";
7
8import * as AuthPairingLinks from "./AuthPairingLinks.ts";
9import * as AuthSessions from "./AuthSessions.ts";
10import * as PersistenceErrors from "./Errors.ts";
11import { SqlitePersistenceMemory } from "./Layers/Sqlite.ts";
12import * as ProviderSessionRuntime from "./ProviderSessionRuntime.ts";
13
14const issuedAt = DateTime.makeUnsafe("2026-06-20T00:00:00.000Z");
15const expiresAt = DateTime.makeUnsafe("2027-06-20T00:00:00.000Z");
16const now = DateTime.makeUnsafe("2026-06-21T00:00:00.000Z");
17const scopes: ReadonlyArray<AuthEnvironmentScope> = ["access:read"];
18
19const authSessionLayer = AuthSessions.layer.pipe(Layer.provideMerge(SqlitePersistenceMemory));
20const authPairingLinkLayer = AuthPairingLinks.layer.pipe(
21 Layer.provideMerge(SqlitePersistenceMemory),
22);
23const providerSessionRuntimeLayer = ProviderSessionRuntime.layer.pipe(
24 Layer.provideMerge(SqlitePersistenceMemory),
25);
26
27describe("persistence error correlation", () => {
28 it.effect("correlates auth session SQL and row-decode failures without sensitive fields", () =>
29 Effect.gen(function* () {
30 const sessions = yield* AuthSessions.AuthSessionRepository;
31 const sql = yield* SqlClient.SqlClient;
32 const sessionId = AuthSessionId.make("session-correlation");
33 const currentSessionId = AuthSessionId.make("current-session-correlation");
34 const subject = "session-subject-secret-sentinel";
35
36 yield* sessions.create({
37 sessionId,
38 subject,
39 scopes,
40 method: "browser-session-cookie",
41 client: {
42 label: null,
43 ipAddress: null,
44 userAgent: null,
45 deviceType: "desktop",
46 os: null,
47 browser: null,
48 },
49 issuedAt,
50 expiresAt,
51 });
52 yield* sql`
53 UPDATE auth_sessions
54 SET scopes = ${"session-scopes-secret-sentinel"}
55 WHERE session_id = ${sessionId}
56 `;
57
58 const decodeError = yield* Effect.flip(sessions.listActive({ now }));
59 assert.instanceOf(decodeError, PersistenceErrors.PersistenceDecodeError);
60 assert.deepStrictEqual(decodeError.correlation, { sessionId });
61 assert.equal(
62 decodeError.message,
63 `Decode error in AuthSessionRepository.listActive:decodeRows: ${decodeError.issue}`,
64 );
65 assert.notInclude(decodeError.issue, subject);
66 assert.notInclude(decodeError.issue, "session-scopes-secret-sentinel");
67 assert.notInclude(decodeError.message, subject);
68
69 yield* sql`DROP TABLE auth_sessions`;
70 const createError = yield* Effect.flip(
71 sessions.create({
72 sessionId,
73 subject,
74 scopes,
75 method: "browser-session-cookie",
76 client: {
77 label: null,
78 ipAddress: null,
79 userAgent: null,
80 deviceType: "desktop",
81 os: null,
82 browser: null,
83 },
84 issuedAt,
85 expiresAt,
86 }),
87 );
88 assert.instanceOf(createError, PersistenceErrors.PersistenceSqlError);
89 assert.deepStrictEqual(createError.correlation, { sessionId });
90 assert.equal(createError.message, "SQL error in AuthSessionRepository.create:query");
91 assert.notInclude(createError.message, subject);
92 assert.notInclude(createError.message, DateTime.formatIso(issuedAt));
93
94 const revokeOtherError = yield* Effect.flip(
95 sessions.revokeAllExcept({ currentSessionId, revokedAt: now }),
96 );
97 assert.instanceOf(revokeOtherError, PersistenceErrors.PersistenceSqlError);
98 assert.deepStrictEqual(revokeOtherError.correlation, { currentSessionId });
99 assert.equal(
100 revokeOtherError.message,
101 "SQL error in AuthSessionRepository.revokeAllExcept:query",
102 );
103 assert.notInclude(revokeOtherError.message, DateTime.formatIso(now));
104 }).pipe(Effect.provide(authSessionLayer)),
105 );
106
107 it.effect("correlates pairing-link create and revoke failures by id only", () =>
108 Effect.gen(function* () {
109 const pairingLinks = yield* AuthPairingLinks.AuthPairingLinkRepository;
110 const sql = yield* SqlClient.SqlClient;
111 const id = "pairing-link-correlation";
112 const credential = "pairing-credential-secret-sentinel";
113 const subject = "pairing-subject-secret-sentinel";
114 const scopesPayload = "pairing-scopes-secret-sentinel";
115
116 yield* sql`
117 INSERT INTO auth_pairing_links (
118 id,
119 credential,
120 method,
121 scopes,
122 subject,
123 label,
124 proof_key_thumbprint,
125 created_at,
126 expires_at,
127 consumed_at,
128 revoked_at
129 )
130 VALUES (
131 ${id},
132 ${credential},
133 ${"one-time-token"},
134 ${scopesPayload},
135 ${subject},
136 NULL,
137 NULL,
138 ${DateTime.formatIso(issuedAt)},
139 ${DateTime.formatIso(expiresAt)},
140 NULL,
141 NULL
142 )
143 `;
144
145 const decodeError = yield* Effect.flip(pairingLinks.getByCredential({ credential }));
146 assert.instanceOf(decodeError, PersistenceErrors.PersistenceDecodeError);
147 assert.deepStrictEqual(decodeError.correlation, { pairingLinkId: id });
148 assert.equal(
149 decodeError.message,
150 `Decode error in AuthPairingLinkRepository.getByCredential:decodeRow: ${decodeError.issue}`,
151 );
152 assert.notInclude(decodeError.issue, credential);
153 assert.notInclude(decodeError.issue, subject);
154 assert.notInclude(decodeError.issue, scopesPayload);
155 assert.notInclude(decodeError.message, DateTime.formatIso(issuedAt));
156
157 yield* sql`DROP TABLE auth_pairing_links`;
158 const createError = yield* Effect.flip(
159 pairingLinks.create({
160 id,
161 credential,
162 method: "one-time-token",
163 scopes,
164 subject,
165 label: null,
166 proofKeyThumbprint: null,
167 createdAt: issuedAt,
168 expiresAt,
169 }),
170 );
171 assert.instanceOf(createError, PersistenceErrors.PersistenceSqlError);
172 assert.deepStrictEqual(createError.correlation, { pairingLinkId: id });
173 assert.notInclude(createError.message, credential);
174 assert.notInclude(createError.message, subject);
175 assert.notInclude(createError.message, DateTime.formatIso(issuedAt));
176
177 const revokeError = yield* Effect.flip(pairingLinks.revoke({ id, revokedAt: now }));
178 assert.instanceOf(revokeError, PersistenceErrors.PersistenceSqlError);
179 assert.deepStrictEqual(revokeError.correlation, { pairingLinkId: id });
180 assert.notInclude(revokeError.message, credential);
181 assert.notInclude(revokeError.message, DateTime.formatIso(now));
182 }).pipe(Effect.provide(authPairingLinkLayer)),
183 );
184
185 it.effect("correlates provider runtime SQL and per-row decode failures by thread", () =>
186 Effect.gen(function* () {
187 const runtimes = yield* ProviderSessionRuntime.ProviderSessionRuntimeRepository;
188 const sql = yield* SqlClient.SqlClient;
189 const threadId = ThreadId.make("thread-correlation");
190 const runtimePayload = "runtime-payload-secret-sentinel";
191 const lastSeenAt = "2026-06-20T00:00:00.000Z";
192
193 yield* sql`
194 INSERT INTO provider_session_runtime (
195 thread_id,
196 provider_name,
197 provider_instance_id,
198 adapter_key,
199 runtime_mode,
200 status,
201 last_seen_at,
202 resume_cursor_json,
203 runtime_payload_json
204 )
205 VALUES (
206 ${threadId},
207 ${"codex"},
208 NULL,
209 ${"codex"},
210 ${"invalid-runtime-mode"},
211 ${"running"},
212 ${lastSeenAt},
213 NULL,
214 ${`{"secret":"${runtimePayload}"}`}
215 )
216 `;
217
218 const decodeError = yield* Effect.flip(runtimes.list());
219 assert.instanceOf(decodeError, PersistenceErrors.PersistenceDecodeError);
220 assert.deepStrictEqual(decodeError.correlation, { threadId });
221 assert.equal(
222 decodeError.message,
223 `Decode error in ProviderSessionRuntimeRepository.list:decodeRows: ${decodeError.issue}`,
224 );
225 assert.notInclude(decodeError.issue, runtimePayload);
226 assert.notInclude(decodeError.message, runtimePayload);
227 assert.notInclude(decodeError.message, lastSeenAt);
228
229 yield* sql`DROP TABLE provider_session_runtime`;
230 const sqlFailure = yield* Effect.flip(
231 runtimes.upsert({
232 threadId,
233 providerName: "codex",
234 providerInstanceId: null,
235 adapterKey: "codex",
236 runtimeMode: "full-access",
237 status: "running",
238 lastSeenAt,
239 resumeCursor: null,
240 runtimePayload: { secret: runtimePayload },
241 }),
242 );
243 assert.instanceOf(sqlFailure, PersistenceErrors.PersistenceSqlError);
244 assert.deepStrictEqual(sqlFailure.correlation, { threadId });
245 assert.equal(
246 sqlFailure.message,
247 "SQL error in ProviderSessionRuntimeRepository.upsert:query",
248 );
249 assert.notInclude(sqlFailure.message, runtimePayload);
250 assert.notInclude(sqlFailure.message, lastSeenAt);
251 }).pipe(Effect.provide(providerSessionRuntimeLayer)),
252 );
253});