From 3a877d23aac4f973da2170db59eeee61ff0e0288 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 08:19:01 -0700 Subject: [PATCH 1/2] Remove persistence error constructor wrappers Co-authored-by: codex --- .../src/persistence/AuthPairingLinks.ts | 8 ++--- apps/server/src/persistence/AuthSessions.ts | 8 ++--- apps/server/src/persistence/Errors.test.ts | 34 ++++++++++++++++++ apps/server/src/persistence/Errors.ts | 35 ++++++++++--------- .../src/persistence/ProviderSessionRuntime.ts | 26 +++++++++----- 5 files changed, 77 insertions(+), 34 deletions(-) create mode 100644 apps/server/src/persistence/Errors.test.ts diff --git a/apps/server/src/persistence/AuthPairingLinks.ts b/apps/server/src/persistence/AuthPairingLinks.ts index add90f04803..c29b023d1d8 100644 --- a/apps/server/src/persistence/AuthPairingLinks.ts +++ b/apps/server/src/persistence/AuthPairingLinks.ts @@ -10,8 +10,8 @@ import { AuthEnvironmentScopes } from "@t3tools/contracts"; import { type AuthPairingLinkRepositoryError, - toPersistenceDecodeError, - toPersistenceSqlError, + PersistenceDecodeError, + PersistenceSqlError, } from "./Errors.ts"; export const AuthPairingLinkRecord = Schema.Struct({ @@ -90,8 +90,8 @@ export class AuthPairingLinkRepository extends Context.Service< function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) { return (cause: unknown): AuthPairingLinkRepositoryError => Schema.isSchemaError(cause) - ? toPersistenceDecodeError(decodeOperation)(cause) - : toPersistenceSqlError(sqlOperation)(cause); + ? PersistenceDecodeError.fromSchemaError(decodeOperation, cause) + : new PersistenceSqlError({ operation: sqlOperation, cause }); } export const make = Effect.gen(function* () { diff --git a/apps/server/src/persistence/AuthSessions.ts b/apps/server/src/persistence/AuthSessions.ts index e3e8a19f5d0..17f76042d0a 100644 --- a/apps/server/src/persistence/AuthSessions.ts +++ b/apps/server/src/persistence/AuthSessions.ts @@ -15,8 +15,8 @@ import { import { type AuthSessionRepositoryError, - toPersistenceDecodeError, - toPersistenceSqlError, + PersistenceDecodeError, + PersistenceSqlError, } from "./Errors.ts"; export const AuthSessionClientMetadataRecord = Schema.Struct({ @@ -146,8 +146,8 @@ function toAuthSessionRecord(row: typeof AuthSessionDbRow.Type): AuthSessionReco function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) { return (cause: unknown): AuthSessionRepositoryError => Schema.isSchemaError(cause) - ? toPersistenceDecodeError(decodeOperation)(cause) - : toPersistenceSqlError(sqlOperation)(cause); + ? PersistenceDecodeError.fromSchemaError(decodeOperation, cause) + : new PersistenceSqlError({ operation: sqlOperation, cause }); } export const make = Effect.gen(function* () { diff --git a/apps/server/src/persistence/Errors.test.ts b/apps/server/src/persistence/Errors.test.ts new file mode 100644 index 00000000000..9dd48bb322c --- /dev/null +++ b/apps/server/src/persistence/Errors.test.ts @@ -0,0 +1,34 @@ +import { assert, it } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import * as Schema from "effect/Schema"; + +import { PersistenceDecodeError, PersistenceSqlError } from "./Errors.ts"; + +const decodeString = Schema.decodeUnknownEffect(Schema.String); + +it("keeps SQL operation context without a tautological detail", () => { + const cause = new Error("database unavailable"); + const error = new PersistenceSqlError({ + operation: "AuthSessionRepository.list:query", + cause, + }); + + assert.equal(error.operation, "AuthSessionRepository.list:query"); + assert.equal(error.detail, undefined); + assert.equal(error.cause, cause); + assert.equal(error.message, "SQL error in AuthSessionRepository.list:query"); +}); + +it.effect("maps schema errors with their formatted issue and exact cause", () => + Effect.gen(function* () { + const cause = yield* Effect.flip(decodeString(42)); + const error = PersistenceDecodeError.fromSchemaError( + "ProviderSessionRuntimeRepository.list:decodeRows", + cause, + ); + + assert.equal(error.operation, "ProviderSessionRuntimeRepository.list:decodeRows"); + assert.equal(error.cause, cause); + assert.include(error.issue, "Expected string"); + }), +); diff --git a/apps/server/src/persistence/Errors.ts b/apps/server/src/persistence/Errors.ts index 2a3d7aff189..27127249bb9 100644 --- a/apps/server/src/persistence/Errors.ts +++ b/apps/server/src/persistence/Errors.ts @@ -1,6 +1,8 @@ import * as Schema from "effect/Schema"; import * as SchemaIssue from "effect/SchemaIssue"; +const formatSchemaIssue = SchemaIssue.makeFormatterDefault(); + // =============================== // Core Persistence Errors // =============================== @@ -9,12 +11,14 @@ export class PersistenceSqlError extends Schema.TaggedErrorClass new PersistenceSqlError({ @@ -42,22 +55,10 @@ export function toPersistenceSqlError(operation: string) { }); } +// Kept for orchestration/projection call sites, which are being revamped separately. export function toPersistenceDecodeError(operation: string) { - return (error: Schema.SchemaError): PersistenceDecodeError => - new PersistenceDecodeError({ - operation, - issue: SchemaIssue.makeFormatterDefault()(error.issue), - cause: error, - }); -} - -export function toPersistenceDecodeCauseError(operation: string) { - return (cause: unknown): PersistenceDecodeError => - new PersistenceDecodeError({ - operation, - issue: `Failed to execute ${operation}`, - cause, - }); + return (cause: Schema.SchemaError): PersistenceDecodeError => + PersistenceDecodeError.fromSchemaError(operation, cause); } export const isPersistenceError = (u: unknown) => diff --git a/apps/server/src/persistence/ProviderSessionRuntime.ts b/apps/server/src/persistence/ProviderSessionRuntime.ts index 6bbbfbd4e19..af48efdb50e 100644 --- a/apps/server/src/persistence/ProviderSessionRuntime.ts +++ b/apps/server/src/persistence/ProviderSessionRuntime.ts @@ -16,9 +16,9 @@ import { } from "@t3tools/contracts"; import { + PersistenceDecodeError, + PersistenceSqlError, type ProviderSessionRuntimeRepositoryError, - toPersistenceDecodeError, - toPersistenceSqlError, } from "./Errors.ts"; /** @@ -117,8 +117,8 @@ const DeleteRuntimeRequestSchema = GetRuntimeRequestSchema; function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) { return (cause: unknown): ProviderSessionRuntimeRepositoryError => Schema.isSchemaError(cause) - ? toPersistenceDecodeError(decodeOperation)(cause) - : toPersistenceSqlError(sqlOperation)(cause); + ? PersistenceDecodeError.fromSchemaError(decodeOperation, cause) + : new PersistenceSqlError({ operation: sqlOperation, cause }); } export const make = Effect.gen(function* () { @@ -235,9 +235,10 @@ export const make = Effect.gen(function* () { onNone: () => Effect.succeed(Option.none()), onSome: (row) => decodeRuntime(row).pipe( - Effect.mapError( - toPersistenceDecodeError( + Effect.mapError((cause) => + PersistenceDecodeError.fromSchemaError( "ProviderSessionRuntimeRepository.getByThreadId:rowToRuntime", + cause, ), ), Effect.map((runtime) => Option.some(runtime)), @@ -259,8 +260,11 @@ export const make = Effect.gen(function* () { rows, (row) => decodeRuntime(row).pipe( - Effect.mapError( - toPersistenceDecodeError("ProviderSessionRuntimeRepository.list:rowToRuntime"), + Effect.mapError((cause) => + PersistenceDecodeError.fromSchemaError( + "ProviderSessionRuntimeRepository.list:rowToRuntime", + cause, + ), ), ), { concurrency: "unbounded" }, @@ -273,7 +277,11 @@ export const make = Effect.gen(function* () { ) => deleteRuntimeByThreadId(input).pipe( Effect.mapError( - toPersistenceSqlError("ProviderSessionRuntimeRepository.deleteByThreadId:query"), + (cause) => + new PersistenceSqlError({ + operation: "ProviderSessionRuntimeRepository.deleteByThreadId:query", + cause, + }), ), ); From dd36e2698dea15da20050d6b200784f09fcd2390 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 10:06:01 -0700 Subject: [PATCH 2/2] fix(server): redact persistence decode issues Co-authored-by: codex --- apps/server/src/persistence/Errors.test.ts | 23 ++++++++++++++++++---- apps/server/src/persistence/Errors.ts | 16 +++++++++++++-- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/apps/server/src/persistence/Errors.test.ts b/apps/server/src/persistence/Errors.test.ts index 9dd48bb322c..680a362e20a 100644 --- a/apps/server/src/persistence/Errors.test.ts +++ b/apps/server/src/persistence/Errors.test.ts @@ -4,7 +4,13 @@ import * as Schema from "effect/Schema"; import { PersistenceDecodeError, PersistenceSqlError } from "./Errors.ts"; -const decodeString = Schema.decodeUnknownEffect(Schema.String); +const decodeRuntimePayload = Schema.decodeUnknownEffect( + Schema.Struct({ + runtimePayload: Schema.Struct({ + attempt: Schema.Number, + }), + }), +); it("keeps SQL operation context without a tautological detail", () => { const cause = new Error("database unavailable"); @@ -19,9 +25,16 @@ it("keeps SQL operation context without a tautological detail", () => { assert.equal(error.message, "SQL error in AuthSessionRepository.list:query"); }); -it.effect("maps schema errors with their formatted issue and exact cause", () => +it.effect("maps schema errors without copying rejected payloads into diagnostics", () => Effect.gen(function* () { - const cause = yield* Effect.flip(decodeString(42)); + const rejectedPayload = "runtime-payload-secret-sentinel"; + const cause = yield* Effect.flip( + decodeRuntimePayload({ + runtimePayload: { + attempt: rejectedPayload, + }, + }), + ); const error = PersistenceDecodeError.fromSchemaError( "ProviderSessionRuntimeRepository.list:decodeRows", cause, @@ -29,6 +42,8 @@ it.effect("maps schema errors with their formatted issue and exact cause", () => assert.equal(error.operation, "ProviderSessionRuntimeRepository.list:decodeRows"); assert.equal(error.cause, cause); - assert.include(error.issue, "Expected string"); + assert.notInclude(error.issue, rejectedPayload); + assert.notInclude(error.message, rejectedPayload); + assert.include(error.issue, "InvalidType"); }), ); diff --git a/apps/server/src/persistence/Errors.ts b/apps/server/src/persistence/Errors.ts index 27127249bb9..e7d081c8f72 100644 --- a/apps/server/src/persistence/Errors.ts +++ b/apps/server/src/persistence/Errors.ts @@ -1,7 +1,19 @@ import * as Schema from "effect/Schema"; import * as SchemaIssue from "effect/SchemaIssue"; -const formatSchemaIssue = SchemaIssue.makeFormatterDefault(); +function summarizeSchemaIssue(issue: SchemaIssue.Issue): string { + switch (issue._tag) { + case "Filter": + case "Encoding": + case "Pointer": + return `${issue._tag}(${summarizeSchemaIssue(issue.issue)})`; + case "Composite": + case "AnyOf": + return `${issue._tag}(${issue.issues.map(summarizeSchemaIssue).join(",")})`; + default: + return issue._tag; + } +} // =============================== // Core Persistence Errors @@ -33,7 +45,7 @@ export class PersistenceDecodeError extends Schema.TaggedErrorClass