| 1 | import { AuthSessionId, ThreadId, type AuthEnvironmentScope } from "@t3tools/contracts"; |
| 2 | import { assert, describe, it } from "@effect/vitest"; |
| 3 | import * as DateTime from "effect/DateTime"; |
| 4 | import * as Effect from "effect/Effect"; |
| 5 | import * as Layer from "effect/Layer"; |
| 6 | import * as SqlClient from "effect/sql/SqlClient"; |
| 7 | |
| 8 | import * as AuthPairingLinks from "./AuthPairingLinks.ts"; |
| 9 | import * as AuthSessions from "./AuthSessions.ts"; |
| 10 | import * as PersistenceErrors from "./Errors.ts"; |
| 11 | import * as SqlitePersistence from "./Sqlite.ts"; |
| 12 | import * as ProviderSessionRuntime from "./ProviderSessionRuntime.ts"; |
| 13 | |
| 14 | const issuedAt = DateTime.makeUnsafe("2026-06-20T00:00:00.000Z"); |
| 15 | const expiresAt = DateTime.makeUnsafe("2027-06-20T00:00:00.000Z"); |
| 16 | const now = DateTime.makeUnsafe("2026-06-21T00:00:00.000Z"); |
| 17 | const scopes: ReadonlyArray<AuthEnvironmentScope> = ["access:read"]; |
| 18 | |
| 19 | const layerAuthSession = AuthSessions.layer.pipe(Layer.provideMerge(SqlitePersistence.layerMemory)); |
| 20 | const layerAuthPairingLink = AuthPairingLinks.layer.pipe( |
| 21 | Layer.provideMerge(SqlitePersistence.layerMemory), |
| 22 | ); |
| 23 | const layerProviderSessionRuntime = ProviderSessionRuntime.layer.pipe( |
| 24 | Layer.provideMerge(SqlitePersistence.layerMemory), |
| 25 | ); |
| 26 | |
| 27 | describe("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 | }); |