From 55ec0fa708ee6011c6253fa72cc1aa775f3a2367 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 03:06:38 -0700 Subject: [PATCH 1/2] [codex] structure managed relay state errors Co-authored-by: codex --- .../src/relay/managedRelayState.test.ts | 129 ++++++++++++++---- .../src/relay/managedRelayState.ts | 118 +++++++++++++--- 2 files changed, 203 insertions(+), 44 deletions(-) diff --git a/packages/client-runtime/src/relay/managedRelayState.test.ts b/packages/client-runtime/src/relay/managedRelayState.test.ts index 49400d32aef..56a8b9714aa 100644 --- a/packages/client-runtime/src/relay/managedRelayState.test.ts +++ b/packages/client-runtime/src/relay/managedRelayState.test.ts @@ -5,6 +5,7 @@ import type { RelayEnvironmentStatusResponse, } from "@t3tools/contracts/relay"; import { describe, expect, it } from "@effect/vitest"; +import * as Cause from "effect/Cause"; import * as Effect from "effect/Effect"; import * as Fiber from "effect/Fiber"; import * as Layer from "effect/Layer"; @@ -17,6 +18,10 @@ import { createManagedRelayQueryManager, createManagedRelaySession, managedRelayAccountChanges, + ManagedRelaySessionUnavailableError, + ManagedRelayStatusEnvironmentMismatchError, + ManagedRelayTokenReadError, + ManagedRelayTokenUnavailableError, type ManagedRelayQueryEvent, managedRelaySessionAtom, readManagedRelaySnapshotState, @@ -148,6 +153,62 @@ describe("createManagedRelayQueryManager", () => { }), ); + it.effect("preserves token provider failure context", () => + Effect.gen(function* () { + const cause = new Error("Clerk session failed"); + const session = createManagedRelaySession({ + accountId: "account-1", + readClerkToken: () => Promise.reject(cause), + }); + + const error = yield* Effect.flip(session.readClerkToken()); + + expect(error).toBeInstanceOf(ManagedRelayTokenReadError); + expect(error).toMatchObject({ + _tag: "ManagedRelayTokenReadError", + accountId: "account-1", + cause, + }); + }), + ); + + it.effect("distinguishes unavailable tokens from unavailable sessions", () => + Effect.gen(function* () { + setManagedRelaySession(registry, { + accountId: "account-1", + readClerkToken: () => Promise.resolve(null), + }); + + const tokenError = yield* Effect.flip(waitForManagedRelayClerkToken(registry)); + expect(tokenError).toBeInstanceOf(ManagedRelayTokenUnavailableError); + expect(tokenError).toMatchObject({ + _tag: "ManagedRelayTokenUnavailableError", + accountId: "account-1", + }); + + setManagedRelaySession(registry, null); + const manager = createManager(); + const atom = manager.environmentsAtom("account-1"); + registry.get(atom); + + yield* Effect.promise(() => + vi.waitFor(() => { + const result = registry.get(atom); + expect(result._tag).toBe("Failure"); + if (result._tag !== "Failure") { + return; + } + const error = Cause.squash(result.cause); + expect(error).toBeInstanceOf(ManagedRelaySessionUnavailableError); + expect(error).toMatchObject({ + _tag: "ManagedRelaySessionUnavailableError", + requestedAccountId: "account-1", + }); + }), + ); + }), + ); + it.effect("updates the token provider without replacing a same-account session", () => Effect.gen(function* () { const firstRead = vi.fn(() => Promise.resolve(null)); @@ -197,30 +258,38 @@ describe("createManagedRelayQueryManager", () => { }), ); - it("emits credential changes only when the managed relay account changes", async () => { - setManagedRelaySession(registry, { - accountId: "account-1", - readClerkToken: () => Promise.resolve("first-token"), - }); - const changes = Effect.runPromise( - managedRelayAccountChanges(registry).pipe(Stream.take(2), Stream.runCollect), - ); - await vi.waitFor(() => { - expect(registry.getNodes().get(managedRelaySessionAtom)?.listeners.size).toBeGreaterThan(0); - }); + it.effect("emits credential changes only when the managed relay account changes", () => + Effect.gen(function* () { + setManagedRelaySession(registry, { + accountId: "account-1", + readClerkToken: () => Promise.resolve("first-token"), + }); + const changes = yield* managedRelayAccountChanges(registry).pipe( + Stream.take(2), + Stream.runCollect, + Effect.forkChild, + ); + yield* Effect.promise(() => + vi.waitFor(() => { + expect(registry.getNodes().get(managedRelaySessionAtom)?.listeners.size).toBeGreaterThan( + 0, + ); + }), + ); - setManagedRelaySession(registry, { - accountId: "account-1", - readClerkToken: () => Promise.resolve("refreshed-token"), - }); - setManagedRelaySession(registry, { - accountId: "account-2", - readClerkToken: () => Promise.resolve("second-token"), - }); - setManagedRelaySession(registry, null); + setManagedRelaySession(registry, { + accountId: "account-1", + readClerkToken: () => Promise.resolve("refreshed-token"), + }); + setManagedRelaySession(registry, { + accountId: "account-2", + readClerkToken: () => Promise.resolve("second-token"), + }); + setManagedRelaySession(registry, null); - expect(Array.from(await changes)).toEqual(["account-2", null]); - }); + expect(Array.from(yield* Fiber.join(changes))).toEqual(["account-2", null]); + }), + ); it("shares one Clerk token read across concurrent relay list and status queries", async () => { const secondEnvironment = { @@ -349,8 +418,20 @@ describe("createManagedRelayQueryManager", () => { registry.get(atom); await vi.waitFor(() => { - expect(readManagedRelaySnapshotState(registry.get(atom)).error).toBe( - "Relay returned status for a different environment.", + const result = registry.get(atom); + expect(result._tag).toBe("Failure"); + if (result._tag !== "Failure") { + return; + } + const error = Cause.squash(result.cause); + expect(error).toBeInstanceOf(ManagedRelayStatusEnvironmentMismatchError); + expect(error).toMatchObject({ + _tag: "ManagedRelayStatusEnvironmentMismatchError", + expectedEnvironmentId: environment.environmentId, + actualEnvironmentId: mismatchedStatus.environmentId, + }); + expect(readManagedRelaySnapshotState(result).error).toBe( + "Relay returned status for environment environment-2 instead of environment-1.", ); }); }); diff --git a/packages/client-runtime/src/relay/managedRelayState.ts b/packages/client-runtime/src/relay/managedRelayState.ts index ec6a0710dd1..a781d1731b4 100644 --- a/packages/client-runtime/src/relay/managedRelayState.ts +++ b/packages/client-runtime/src/relay/managedRelayState.ts @@ -2,16 +2,18 @@ import type { RelayClientEnvironmentRecord, RelayEnvironmentStatusResponse, } from "@t3tools/contracts/relay"; +import { EnvironmentId } from "@t3tools/contracts"; import { RelayEnvironmentConnectScope, RelayEnvironmentStatusScope, + RelayManagedEndpoint, } from "@t3tools/contracts/relay"; import { decodeRelayJwt } from "@t3tools/shared/relayJwt"; import * as Cause from "effect/Cause"; import * as Clock from "effect/Clock"; -import * as Data from "effect/Data"; import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; +import * as Schema from "effect/Schema"; import * as Stream from "effect/Stream"; import { AsyncResult, Atom, AtomRegistry } from "effect/unstable/reactivity"; @@ -55,14 +57,86 @@ export interface ManagedRelayQueryEvent { readonly traceId?: string | null; } -export class ManagedRelaySessionError extends Data.TaggedError("ManagedRelaySessionError")<{ - readonly message: string; - readonly cause?: unknown; -}> {} +export class ManagedRelayTokenReadError extends Schema.TaggedErrorClass()( + "ManagedRelayTokenReadError", + { + accountId: Schema.String, + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Could not obtain the T3 Cloud session token for account ${this.accountId}.`; + } +} + +export class ManagedRelayTokenUnavailableError extends Schema.TaggedErrorClass()( + "ManagedRelayTokenUnavailableError", + { accountId: Schema.String }, +) { + override get message(): string { + return `The T3 Cloud session token is unavailable for account ${this.accountId}.`; + } +} + +export class ManagedRelaySessionUnavailableError extends Schema.TaggedErrorClass()( + "ManagedRelaySessionUnavailableError", + { requestedAccountId: Schema.String }, +) { + override get message(): string { + return `Sign in to T3 Cloud as account ${this.requestedAccountId} before loading relay data.`; + } +} + +export const ManagedRelaySessionError = Schema.Union([ + ManagedRelayTokenReadError, + ManagedRelayTokenUnavailableError, + ManagedRelaySessionUnavailableError, +]); +export type ManagedRelaySessionError = typeof ManagedRelaySessionError.Type; + +export class ManagedRelayStatusEnvironmentMismatchError extends Schema.TaggedErrorClass()( + "ManagedRelayStatusEnvironmentMismatchError", + { + expectedEnvironmentId: EnvironmentId, + actualEnvironmentId: EnvironmentId, + }, +) { + override get message(): string { + return `Relay returned status for environment ${this.actualEnvironmentId} instead of ${this.expectedEnvironmentId}.`; + } +} + +export class ManagedRelayStatusEndpointMismatchError extends Schema.TaggedErrorClass()( + "ManagedRelayStatusEndpointMismatchError", + { + environmentId: EnvironmentId, + expectedEndpoint: RelayManagedEndpoint, + actualEndpoint: RelayManagedEndpoint, + }, +) { + override get message(): string { + return `Relay returned a different endpoint for environment ${this.environmentId}.`; + } +} + +export class ManagedRelayStatusDescriptorEnvironmentMismatchError extends Schema.TaggedErrorClass()( + "ManagedRelayStatusDescriptorEnvironmentMismatchError", + { + expectedEnvironmentId: EnvironmentId, + actualEnvironmentId: EnvironmentId, + }, +) { + override get message(): string { + return `Relay returned a descriptor for environment ${this.actualEnvironmentId} instead of ${this.expectedEnvironmentId}.`; + } +} -export class ManagedRelaySnapshotError extends Data.TaggedError("ManagedRelaySnapshotError")<{ - readonly message: string; -}> {} +export const ManagedRelaySnapshotError = Schema.Union([ + ManagedRelayStatusEnvironmentMismatchError, + ManagedRelayStatusEndpointMismatchError, + ManagedRelayStatusDescriptorEnvironmentMismatchError, +]); +export type ManagedRelaySnapshotError = typeof ManagedRelaySnapshotError.Type; export const managedRelaySessionAtom = Atom.make(null).pipe( Atom.keepAlive, @@ -122,8 +196,8 @@ export function createManagedRelaySession(input: ManagedRelaySessionInput): Mana return yield* Effect.tryPromise({ try: () => readCachedClerkToken(nowMillis), catch: (cause) => - new ManagedRelaySessionError({ - message: "Could not obtain the T3 Cloud session token.", + new ManagedRelayTokenReadError({ + accountId: input.accountId, cause, }), }); @@ -180,8 +254,8 @@ function readSessionClerkToken( token ? Effect.succeed(token) : Effect.fail( - new ManagedRelaySessionError({ - message: "The T3 Cloud session token is unavailable.", + new ManagedRelayTokenUnavailableError({ + accountId: session.accountId, }), ), ), @@ -225,8 +299,8 @@ function requireClerkToken( const session = get(managedRelaySessionAtom); if (!session || session.accountId !== accountId) { return Effect.fail( - new ManagedRelaySessionError({ - message: "Sign in to T3 Cloud before loading relay data.", + new ManagedRelaySessionUnavailableError({ + requestedAccountId: accountId, }), ); } @@ -267,22 +341,26 @@ function validateEnvironmentStatus( ): Effect.Effect { if (status.environmentId !== environment.environmentId) { return Effect.fail( - new ManagedRelaySnapshotError({ - message: "Relay returned status for a different environment.", + new ManagedRelayStatusEnvironmentMismatchError({ + expectedEnvironmentId: environment.environmentId, + actualEnvironmentId: status.environmentId, }), ); } if (!endpointMatches(status.endpoint, environment.endpoint)) { return Effect.fail( - new ManagedRelaySnapshotError({ - message: "Relay returned status for a different endpoint.", + new ManagedRelayStatusEndpointMismatchError({ + environmentId: environment.environmentId, + expectedEndpoint: environment.endpoint, + actualEndpoint: status.endpoint, }), ); } if (status.descriptor && status.descriptor.environmentId !== environment.environmentId) { return Effect.fail( - new ManagedRelaySnapshotError({ - message: "Relay returned status descriptor for a different environment.", + new ManagedRelayStatusDescriptorEnvironmentMismatchError({ + expectedEnvironmentId: environment.environmentId, + actualEnvironmentId: status.descriptor.environmentId, }), ); } From 5944baddc29b91e73fec2d68709475b13d644a70 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 10:03:50 -0700 Subject: [PATCH 2/2] Redact managed relay endpoint mismatches Co-authored-by: codex --- .../src/relay/managedRelayState.test.ts | 60 ++++++++++++++++++ .../src/relay/managedRelayState.ts | 61 +++++++++++++++++-- 2 files changed, 117 insertions(+), 4 deletions(-) diff --git a/packages/client-runtime/src/relay/managedRelayState.test.ts b/packages/client-runtime/src/relay/managedRelayState.test.ts index 56a8b9714aa..0c0793d966d 100644 --- a/packages/client-runtime/src/relay/managedRelayState.test.ts +++ b/packages/client-runtime/src/relay/managedRelayState.test.ts @@ -19,6 +19,7 @@ import { createManagedRelaySession, managedRelayAccountChanges, ManagedRelaySessionUnavailableError, + ManagedRelayStatusEndpointMismatchError, ManagedRelayStatusEnvironmentMismatchError, ManagedRelayTokenReadError, ManagedRelayTokenUnavailableError, @@ -114,6 +115,65 @@ function clerkToken(expiresAtSeconds: number): string { describe("createManagedRelayQueryManager", () => { afterEach(resetRegistry); + it("redacts endpoint mismatch secrets while retaining correlation fields", () => { + const expectedHttpBaseUrl = + "https://expected-user:expected-password@expected.example.test/private/catalog?token=expected-secret#expected-fragment"; + const expectedWsBaseUrl = + "wss://expected-user:expected-password@expected.example.test/private/socket?token=expected-secret#expected-fragment"; + const actualHttpBaseUrl = + "https://actual-user:actual-password@actual.example.test/private/catalog?token=actual-secret#actual-fragment"; + const actualWsBaseUrl = + "wss://actual-user:actual-password@actual.example.test/private/socket?token=actual-secret#actual-fragment"; + + const error = ManagedRelayStatusEndpointMismatchError.fromEndpoints({ + environmentId: environment.environmentId, + expectedEndpoint: { + providerKind: "cloudflare_tunnel", + httpBaseUrl: expectedHttpBaseUrl, + wsBaseUrl: expectedWsBaseUrl, + }, + actualEndpoint: { + providerKind: "cloudflare_tunnel", + httpBaseUrl: actualHttpBaseUrl, + wsBaseUrl: actualWsBaseUrl, + }, + }); + + expect(error).toMatchObject({ + expectedProviderKind: "cloudflare_tunnel", + expectedHttpBaseUrlInputLength: expectedHttpBaseUrl.length, + expectedHttpBaseUrlProtocol: "https:", + expectedHttpBaseUrlHostname: "expected.example.test", + expectedWsBaseUrlInputLength: expectedWsBaseUrl.length, + expectedWsBaseUrlProtocol: "wss:", + expectedWsBaseUrlHostname: "expected.example.test", + actualProviderKind: "cloudflare_tunnel", + actualHttpBaseUrlInputLength: actualHttpBaseUrl.length, + actualHttpBaseUrlProtocol: "https:", + actualHttpBaseUrlHostname: "actual.example.test", + actualWsBaseUrlInputLength: actualWsBaseUrl.length, + actualWsBaseUrlProtocol: "wss:", + actualWsBaseUrlHostname: "actual.example.test", + }); + expect(error).not.toHaveProperty("expectedEndpoint"); + expect(error).not.toHaveProperty("actualEndpoint"); + const exposedText = `${Object.values(error).join(" ")} ${error.message}`; + for (const secret of [ + "expected-user", + "expected-password", + "/private/catalog", + "/private/socket", + "expected-secret", + "expected-fragment", + "actual-user", + "actual-password", + "actual-secret", + "actual-fragment", + ]) { + expect(exposedText).not.toContain(secret); + } + }); + it.effect("waits for the current cloud session before reading its token", () => Effect.gen(function* () { const tokenFiber = yield* waitForManagedRelayClerkToken(registry).pipe(Effect.forkChild); diff --git a/packages/client-runtime/src/relay/managedRelayState.ts b/packages/client-runtime/src/relay/managedRelayState.ts index a781d1731b4..b65dbff002a 100644 --- a/packages/client-runtime/src/relay/managedRelayState.ts +++ b/packages/client-runtime/src/relay/managedRelayState.ts @@ -6,9 +6,10 @@ import { EnvironmentId } from "@t3tools/contracts"; import { RelayEnvironmentConnectScope, RelayEnvironmentStatusScope, - RelayManagedEndpoint, + RelayManagedEndpointProviderKind, } from "@t3tools/contracts/relay"; import { decodeRelayJwt } from "@t3tools/shared/relayJwt"; +import { getUrlDiagnostics } from "@t3tools/shared/urlDiagnostics"; import * as Cause from "effect/Cause"; import * as Clock from "effect/Clock"; import * as Effect from "effect/Effect"; @@ -110,10 +111,62 @@ export class ManagedRelayStatusEndpointMismatchError extends Schema.TaggedErrorC "ManagedRelayStatusEndpointMismatchError", { environmentId: EnvironmentId, - expectedEndpoint: RelayManagedEndpoint, - actualEndpoint: RelayManagedEndpoint, + expectedProviderKind: RelayManagedEndpointProviderKind, + expectedHttpBaseUrlInputLength: Schema.Number, + expectedHttpBaseUrlProtocol: Schema.optionalKey(Schema.String), + expectedHttpBaseUrlHostname: Schema.optionalKey(Schema.String), + expectedWsBaseUrlInputLength: Schema.Number, + expectedWsBaseUrlProtocol: Schema.optionalKey(Schema.String), + expectedWsBaseUrlHostname: Schema.optionalKey(Schema.String), + actualProviderKind: RelayManagedEndpointProviderKind, + actualHttpBaseUrlInputLength: Schema.Number, + actualHttpBaseUrlProtocol: Schema.optionalKey(Schema.String), + actualHttpBaseUrlHostname: Schema.optionalKey(Schema.String), + actualWsBaseUrlInputLength: Schema.Number, + actualWsBaseUrlProtocol: Schema.optionalKey(Schema.String), + actualWsBaseUrlHostname: Schema.optionalKey(Schema.String), }, ) { + static fromEndpoints(input: { + readonly environmentId: EnvironmentId; + readonly expectedEndpoint: RelayClientEnvironmentRecord["endpoint"]; + readonly actualEndpoint: RelayClientEnvironmentRecord["endpoint"]; + }): ManagedRelayStatusEndpointMismatchError { + const expectedHttp = getUrlDiagnostics(input.expectedEndpoint.httpBaseUrl); + const expectedWs = getUrlDiagnostics(input.expectedEndpoint.wsBaseUrl); + const actualHttp = getUrlDiagnostics(input.actualEndpoint.httpBaseUrl); + const actualWs = getUrlDiagnostics(input.actualEndpoint.wsBaseUrl); + return new ManagedRelayStatusEndpointMismatchError({ + environmentId: input.environmentId, + expectedProviderKind: input.expectedEndpoint.providerKind, + expectedHttpBaseUrlInputLength: expectedHttp.inputLength, + ...(expectedHttp.protocol === undefined + ? {} + : { expectedHttpBaseUrlProtocol: expectedHttp.protocol }), + ...(expectedHttp.hostname === undefined + ? {} + : { expectedHttpBaseUrlHostname: expectedHttp.hostname }), + expectedWsBaseUrlInputLength: expectedWs.inputLength, + ...(expectedWs.protocol === undefined + ? {} + : { expectedWsBaseUrlProtocol: expectedWs.protocol }), + ...(expectedWs.hostname === undefined + ? {} + : { expectedWsBaseUrlHostname: expectedWs.hostname }), + actualProviderKind: input.actualEndpoint.providerKind, + actualHttpBaseUrlInputLength: actualHttp.inputLength, + ...(actualHttp.protocol === undefined + ? {} + : { actualHttpBaseUrlProtocol: actualHttp.protocol }), + ...(actualHttp.hostname === undefined + ? {} + : { actualHttpBaseUrlHostname: actualHttp.hostname }), + actualWsBaseUrlInputLength: actualWs.inputLength, + ...(actualWs.protocol === undefined ? {} : { actualWsBaseUrlProtocol: actualWs.protocol }), + ...(actualWs.hostname === undefined ? {} : { actualWsBaseUrlHostname: actualWs.hostname }), + }); + } + override get message(): string { return `Relay returned a different endpoint for environment ${this.environmentId}.`; } @@ -349,7 +402,7 @@ function validateEnvironmentStatus( } if (!endpointMatches(status.endpoint, environment.endpoint)) { return Effect.fail( - new ManagedRelayStatusEndpointMismatchError({ + ManagedRelayStatusEndpointMismatchError.fromEndpoints({ environmentId: environment.environmentId, expectedEndpoint: environment.endpoint, actualEndpoint: status.endpoint,