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/sql/SqlClient";
7
8import * as AuthPairingLinks from "./AuthPairingLinks.ts";
9import * as AuthSessions from "./AuthSessions.ts";
10import * as PersistenceErrors from "./Errors.ts";
11import * as SqlitePersistence from "./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 layerAuthSession = AuthSessions.layer.pipe(Layer.provideMerge(SqlitePersistence.layerMemory));
20const layerAuthPairingLink = AuthPairingLinks.layer.pipe(
21 Layer.provideMerge(SqlitePersistence.layerMemory),
22);
23const layerProviderSessionRuntime = ProviderSessionRuntime.layer.pipe(
24 Layer.provideMerge(SqlitePersistence.layerMemory),
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(layerAuthSession)),
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(layerAuthPairingLink)),
183 );
184
185 it.effect("skips undecodable provider runtime rows and correlates SQL 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 validThreadId = ThreadId.make("thread-valid");
219 yield* runtimes.upsert({
220 threadId: validThreadId,
221 providerName: "codex",
222 providerInstanceId: null,
223 adapterKey: "codex",
224 runtimeMode: "full-access",
225 status: "running",
226 lastSeenAt,
227 resumeCursor: null,
228 runtimePayload: null,
229 });
230
231 const listed = yield* runtimes.list();
232 assert.deepStrictEqual(
233 listed.map((runtime) => runtime.threadId),
234 [validThreadId],
235 );
236
237 yield* sql`DROP TABLE provider_session_runtime`;
238 const sqlFailure = yield* Effect.flip(
239 runtimes.upsert({
240 threadId,
241 providerName: "codex",
242 providerInstanceId: null,
243 adapterKey: "codex",
244 runtimeMode: "full-access",
245 status: "running",
246 lastSeenAt,
247 resumeCursor: null,
248 runtimePayload: { secret: runtimePayload },
249 }),
250 );
251 assert.instanceOf(sqlFailure, PersistenceErrors.PersistenceSqlError);
252 assert.deepStrictEqual(sqlFailure.correlation, { threadId });
253 assert.equal(
254 sqlFailure.message,
255 "SQL error in ProviderSessionRuntimeRepository.upsert:query",
256 );
257 assert.notInclude(sqlFailure.message, runtimePayload);
258 assert.notInclude(sqlFailure.message, lastSeenAt);
259 }).pipe(Effect.provide(layerProviderSessionRuntime)),
260 );
261});