From e0bace74d2bad41fad3116934407cd846624f6e4 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 02:34:34 -0700 Subject: [PATCH 1/3] audit relay persistence error context Co-authored-by: codex --- infra/relay/src/auth/DpopProofs.test.ts | 26 ++ infra/relay/src/auth/DpopProofs.ts | 41 ++- .../auth/DpopProofs.verifyAndConsume.test.ts | 14 +- .../EnvironmentCredentials.test.ts | 82 ++++++ .../environments/EnvironmentCredentials.ts | 268 +++++++++++------- .../src/environments/EnvironmentLinks.test.ts | 70 +++++ .../src/environments/EnvironmentLinks.ts | 225 ++++++++++----- infra/relay/src/http/Api.test.ts | 1 + infra/relay/src/http/Api.ts | 11 +- 9 files changed, 548 insertions(+), 190 deletions(-) diff --git a/infra/relay/src/auth/DpopProofs.test.ts b/infra/relay/src/auth/DpopProofs.test.ts index b294ba396b6..fba64586e28 100644 --- a/infra/relay/src/auth/DpopProofs.test.ts +++ b/infra/relay/src/auth/DpopProofs.test.ts @@ -96,4 +96,30 @@ describe("DpopProofReplay", () => { Effect.provide(DpopProofs.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)))), ); }); + + it.effect("retains the prune cutoff and database failure", () => { + const cause = new Error("database unavailable"); + const fakeDb = { + delete: (table: unknown) => { + expect(table).toBe(relayDpopProofs); + return { + where: () => Effect.fail(cause), + }; + }, + } as unknown as RelayDb.RelayDb["Service"]; + + return Effect.gen(function* () { + const replay = yield* DpopProofs.DpopProofReplay; + const error = yield* Effect.flip(replay.pruneExpired); + + expect(error).toMatchObject({ + _tag: "DpopProofReplayPersistenceError", + operation: "prune-expired", + }); + expect(Date.parse(error.expiresBefore ?? "")).not.toBeNaN(); + expect(error.cause).toBe(cause); + }).pipe( + Effect.provide(DpopProofs.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)))), + ); + }); }); diff --git a/infra/relay/src/auth/DpopProofs.ts b/infra/relay/src/auth/DpopProofs.ts index cf3f7a4cf5a..fa784eb639b 100644 --- a/infra/relay/src/auth/DpopProofs.ts +++ b/infra/relay/src/auth/DpopProofs.ts @@ -13,11 +13,16 @@ import { relayDpopProofs } from "../persistence/schema.ts"; export class DpopProofReplayPersistenceError extends Schema.TaggedErrorClass()( "DpopProofReplayPersistenceError", { + operation: Schema.Literals(["consume", "prune-expired"]), + thumbprint: Schema.optionalKey(Schema.String), + jti: Schema.optionalKey(Schema.String), + iat: Schema.optionalKey(Schema.Number), + expiresBefore: Schema.optionalKey(Schema.String), cause: Schema.Defect(), }, ) { override get message(): string { - return "Failed to persist DPoP proof replay state"; + return `Failed to persist DPoP proof replay state during '${this.operation}'`; } } @@ -58,10 +63,21 @@ const make = Effect.gen(function* () { createdAt, }) .onConflictDoNothing() - .returning({ jti: relayDpopProofs.jti }); + .returning({ jti: relayDpopProofs.jti }) + .pipe( + Effect.mapError( + (cause) => + new DpopProofReplayPersistenceError({ + operation: "consume", + thumbprint: input.thumbprint, + jti: input.jti, + iat: input.iat, + cause, + }), + ), + ); return inserted.length > 0; }, - Effect.mapError((cause) => new DpopProofReplayPersistenceError({ cause })), ); const verifyAndConsume: DpopProofReplay["Service"]["verifyAndConsume"] = Effect.fn( @@ -114,11 +130,20 @@ const make = Effect.gen(function* () { const pruneExpired: DpopProofReplay["Service"]["pruneExpired"] = Effect.gen(function* () { const now = DateTime.formatIso(yield* DateTime.now); yield* Effect.annotateCurrentSpan({ "relay.dpop_prune.before": now }); - yield* db.delete(relayDpopProofs).where(lt(relayDpopProofs.expiresAt, now)); - }).pipe( - Effect.withSpan("relay.dpop_proofs.prune_expired"), - Effect.mapError((cause) => new DpopProofReplayPersistenceError({ cause })), - ); + yield* db + .delete(relayDpopProofs) + .where(lt(relayDpopProofs.expiresAt, now)) + .pipe( + Effect.mapError( + (cause) => + new DpopProofReplayPersistenceError({ + operation: "prune-expired", + expiresBefore: now, + cause, + }), + ), + ); + }).pipe(Effect.withSpan("relay.dpop_proofs.prune_expired")); return DpopProofReplay.of({ verifyAndConsume, diff --git a/infra/relay/src/auth/DpopProofs.verifyAndConsume.test.ts b/infra/relay/src/auth/DpopProofs.verifyAndConsume.test.ts index ecb33f1fc06..7663e874879 100644 --- a/infra/relay/src/auth/DpopProofs.verifyAndConsume.test.ts +++ b/infra/relay/src/auth/DpopProofs.verifyAndConsume.test.ts @@ -163,7 +163,7 @@ describe("DpopProofReplay.verifyAndConsume", () => { iat: Math.floor(now.epochMilliseconds / 1_000), jti: "proof-persistence-failure", }); - const cause = "database unavailable"; + const cause = { _tag: "DatabaseUnavailable" } as const; return Effect.gen(function* () { const replay = yield* DpopProofs.DpopProofReplay; @@ -177,8 +177,16 @@ describe("DpopProofReplay.verifyAndConsume", () => { }), ); - expect(error).toEqual(new DpopProofs.DpopProofReplayPersistenceError({ cause })); - }).pipe(Effect.provide(layer(() => Effect.fail({ _tag: cause })))); + expect(error).toMatchObject({ + _tag: "DpopProofReplayPersistenceError", + operation: "consume", + thumbprint: proof.thumbprint, + jti: "proof-persistence-failure", + iat: Math.floor(now.epochMilliseconds / 1_000), + }); + expect(error.cause).toBe(cause); + expect(error).not.toHaveProperty("proof"); + }).pipe(Effect.provide(layer(() => Effect.fail(cause)))); }); it.effect("accepts proofs bound to the access token hash", () => { diff --git a/infra/relay/src/environments/EnvironmentCredentials.test.ts b/infra/relay/src/environments/EnvironmentCredentials.test.ts index 733658cbb5e..4e12dabe831 100644 --- a/infra/relay/src/environments/EnvironmentCredentials.test.ts +++ b/infra/relay/src/environments/EnvironmentCredentials.test.ts @@ -9,6 +9,88 @@ import { relayEnvironmentCredentials } from "../persistence/schema.ts"; import * as EnvironmentCredentials from "./EnvironmentCredentials.ts"; describe("EnvironmentCredentials", () => { + it.effect("reports the credential creation persistence stage and preserves its cause", () => { + const cause = new Error("database unavailable"); + const fakeDb = { + insert: (table: unknown) => { + expect(table).toBe(relayEnvironmentCredentials); + return { + values: () => Effect.void, + }; + }, + update: (table: unknown) => { + expect(table).toBe(relayEnvironmentCredentials); + return { + set: () => ({ + where: () => Effect.fail(cause), + }), + }; + }, + } as unknown as RelayDb.RelayDb["Service"]; + + return Effect.gen(function* () { + const credentials = yield* EnvironmentCredentials.EnvironmentCredentials; + const error = yield* Effect.flip( + credentials.create({ + environmentId: "env_test", + environmentPublicKey: "sensitive-public-key-material", + }), + ); + + expect(error).toMatchObject({ + _tag: "EnvironmentCredentialCreatePersistenceError", + stage: "revoke-previous-credentials", + environmentId: "env_test", + }); + expect(error.credentialId).toMatch(/^[0-9a-f]{64}$/); + expect(error.cause).toBe(cause); + expect(error).not.toHaveProperty("environmentPublicKey"); + }).pipe( + Effect.provide( + EnvironmentCredentials.layer.pipe( + Layer.provide(NodeCryptoLayer.layer), + Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)), + ), + ), + ); + }); + + it.effect("does not retain credential tokens when lookup persistence fails", () => { + const cause = new Error("database unavailable"); + const token = "t3env_sensitive-credential-token"; + const fakeDb = { + select: () => ({ + from: (table: unknown) => { + expect(table).toBe(relayEnvironmentCredentials); + return { + where: () => ({ + limit: () => Effect.fail(cause), + }), + }; + }, + }), + } as unknown as RelayDb.RelayDb["Service"]; + + return Effect.gen(function* () { + const credentials = yield* EnvironmentCredentials.EnvironmentCredentials; + const error = yield* Effect.flip(credentials.authenticate(token)); + + expect(error).toMatchObject({ + _tag: "EnvironmentCredentialAuthenticatePersistenceError", + stage: "lookup-credential", + }); + expect(error.cause).toBe(cause); + expect(error).not.toHaveProperty("token"); + }).pipe( + Effect.provide( + EnvironmentCredentials.layer.pipe( + Layer.provide(NodeCryptoLayer.layer), + Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)), + ), + ), + ); + }); + it.effect( "creates opaque credentials and revokes only older credentials for the same key", () => { diff --git a/infra/relay/src/environments/EnvironmentCredentials.ts b/infra/relay/src/environments/EnvironmentCredentials.ts index e318ce1e098..39f40d941b8 100644 --- a/infra/relay/src/environments/EnvironmentCredentials.ts +++ b/infra/relay/src/environments/EnvironmentCredentials.ts @@ -13,28 +13,44 @@ import { relayEnvironmentCredentials, relayEnvironmentLinks } from "../persisten export class EnvironmentCredentialCreatePersistenceError extends Schema.TaggedErrorClass()( "EnvironmentCredentialCreatePersistenceError", - { cause: Schema.Defect() }, + { + stage: Schema.Literals([ + "generate-credential", + "hash-token", + "insert-credential", + "revoke-previous-credentials", + ]), + environmentId: Schema.String, + credentialId: Schema.optionalKey(Schema.String), + cause: Schema.Defect(), + }, ) { override get message(): string { - return "Failed to persist environment credential"; + return `Environment credential creation failed during '${this.stage}' for environment '${this.environmentId}'${this.credentialId === undefined ? "" : `, credential '${this.credentialId}'`}`; } } export class EnvironmentCredentialAuthenticatePersistenceError extends Schema.TaggedErrorClass()( "EnvironmentCredentialAuthenticatePersistenceError", - { cause: Schema.Defect() }, + { + stage: Schema.Literals(["hash-token", "lookup-credential"]), + cause: Schema.Defect(), + }, ) { override get message(): string { - return "Failed to authenticate environment credential"; + return `Environment credential authentication failed during '${this.stage}'`; } } export class EnvironmentCredentialRevokePersistenceError extends Schema.TaggedErrorClass()( "EnvironmentCredentialRevokePersistenceError", - { cause: Schema.Defect() }, + { + environmentId: Schema.String, + cause: Schema.Defect(), + }, ) { override get message(): string { - return "Failed to revoke environment credential"; + return `Failed to revoke credentials for environment '${this.environmentId}'`; } } @@ -85,13 +101,33 @@ const make = Effect.gen(function* () { }); return EnvironmentCredentials.of({ - create: Effect.fn("relay.environment_credentials.create")( - function* (input) { - yield* Effect.annotateCurrentSpan({ "relay.environment_id": input.environmentId }); - const credential = yield* makeCredential(); - const credentialHash = yield* hashToken(credential.token); - const now = DateTime.formatIso(yield* DateTime.now); - yield* db.insert(relayEnvironmentCredentials).values({ + create: Effect.fn("relay.environment_credentials.create")(function* (input) { + yield* Effect.annotateCurrentSpan({ "relay.environment_id": input.environmentId }); + const credential = yield* makeCredential().pipe( + Effect.mapError( + (cause) => + new EnvironmentCredentialCreatePersistenceError({ + stage: "generate-credential", + environmentId: input.environmentId, + cause, + }), + ), + ); + const credentialHash = yield* hashToken(credential.token).pipe( + Effect.mapError( + (cause) => + new EnvironmentCredentialCreatePersistenceError({ + stage: "hash-token", + environmentId: input.environmentId, + credentialId: credential.credentialId, + cause, + }), + ), + ); + const now = DateTime.formatIso(yield* DateTime.now); + yield* db + .insert(relayEnvironmentCredentials) + .values({ credentialId: credential.credentialId, environmentId: input.environmentId, environmentPublicKey: input.environmentPublicKey, @@ -99,96 +135,136 @@ const make = Effect.gen(function* () { revokedAt: null, createdAt: now, updatedAt: now, - }); - yield* db - .update(relayEnvironmentCredentials) - .set({ - revokedAt: now, - updatedAt: now, - }) - .where( - and( - eq(relayEnvironmentCredentials.environmentId, input.environmentId), - eq(relayEnvironmentCredentials.environmentPublicKey, input.environmentPublicKey), - ne(relayEnvironmentCredentials.credentialId, credential.credentialId), - isNull(relayEnvironmentCredentials.revokedAt), - ), - ); - return credential.token; - }, - Effect.mapError((cause) => new EnvironmentCredentialCreatePersistenceError({ cause })), - ), + }) + .pipe( + Effect.mapError( + (cause) => + new EnvironmentCredentialCreatePersistenceError({ + stage: "insert-credential", + environmentId: input.environmentId, + credentialId: credential.credentialId, + cause, + }), + ), + ); + yield* db + .update(relayEnvironmentCredentials) + .set({ + revokedAt: now, + updatedAt: now, + }) + .where( + and( + eq(relayEnvironmentCredentials.environmentId, input.environmentId), + eq(relayEnvironmentCredentials.environmentPublicKey, input.environmentPublicKey), + ne(relayEnvironmentCredentials.credentialId, credential.credentialId), + isNull(relayEnvironmentCredentials.revokedAt), + ), + ) + .pipe( + Effect.mapError( + (cause) => + new EnvironmentCredentialCreatePersistenceError({ + stage: "revoke-previous-credentials", + environmentId: input.environmentId, + credentialId: credential.credentialId, + cause, + }), + ), + ); + return credential.token; + }), - authenticate: Effect.fn("relay.environment_credentials.authenticate")( - function* (token) { - const credentialHash = yield* hashToken(token); - const rows = yield* db - .select({ - credentialId: relayEnvironmentCredentials.credentialId, - environmentId: relayEnvironmentCredentials.environmentId, - environmentPublicKey: relayEnvironmentCredentials.environmentPublicKey, + authenticate: Effect.fn("relay.environment_credentials.authenticate")(function* (token) { + const credentialHash = yield* hashToken(token).pipe( + Effect.mapError( + (cause) => + new EnvironmentCredentialAuthenticatePersistenceError({ + stage: "hash-token", + cause, + }), + ), + ); + const rows = yield* db + .select({ + credentialId: relayEnvironmentCredentials.credentialId, + environmentId: relayEnvironmentCredentials.environmentId, + environmentPublicKey: relayEnvironmentCredentials.environmentPublicKey, + }) + .from(relayEnvironmentCredentials) + .where( + and( + eq(relayEnvironmentCredentials.credentialHash, credentialHash), + isNull(relayEnvironmentCredentials.revokedAt), + ), + ) + .limit(1) + .pipe( + Effect.mapError( + (cause) => + new EnvironmentCredentialAuthenticatePersistenceError({ + stage: "lookup-credential", + cause, + }), + ), + ); + const row = rows[0]; + if (row) { + yield* Effect.annotateCurrentSpan({ "relay.environment_id": row.environmentId }); + } + return row + ? Option.some({ + credentialId: row.credentialId, + environmentId: row.environmentId, + environmentPublicKey: row.environmentPublicKey, }) - .from(relayEnvironmentCredentials) - .where( - and( - eq(relayEnvironmentCredentials.credentialHash, credentialHash), - isNull(relayEnvironmentCredentials.revokedAt), - ), - ) - .limit(1); - const row = rows[0]; - if (row) { - yield* Effect.annotateCurrentSpan({ "relay.environment_id": row.environmentId }); - } - return row - ? Option.some({ - credentialId: row.credentialId, - environmentId: row.environmentId, - environmentPublicKey: row.environmentPublicKey, - }) - : Option.none(); - }, - Effect.mapError((cause) => new EnvironmentCredentialAuthenticatePersistenceError({ cause })), - ), + : Option.none(); + }), revokeForEnvironmentPublicKey: Effect.fn( "relay.environment_credentials.revoke_for_environment_public_key", - )( - function* (input) { - yield* Effect.annotateCurrentSpan({ "relay.environment_id": input.environmentId }); - const revokedAt = DateTime.formatIso(yield* DateTime.now); - const rows = yield* db - .update(relayEnvironmentCredentials) - .set({ - revokedAt, - updatedAt: revokedAt, - }) - .where( - and( - eq(relayEnvironmentCredentials.environmentId, input.environmentId), - eq(relayEnvironmentCredentials.environmentPublicKey, input.environmentPublicKey), - isNull(relayEnvironmentCredentials.revokedAt), - notExists( - db - .select({ userId: relayEnvironmentLinks.userId }) - .from(relayEnvironmentLinks) - .where( - and( - eq(relayEnvironmentLinks.environmentId, input.environmentId), - eq(relayEnvironmentLinks.environmentPublicKey, input.environmentPublicKey), - isNull(relayEnvironmentLinks.revokedAt), - ), + )(function* (input) { + yield* Effect.annotateCurrentSpan({ "relay.environment_id": input.environmentId }); + const revokedAt = DateTime.formatIso(yield* DateTime.now); + const rows = yield* db + .update(relayEnvironmentCredentials) + .set({ + revokedAt, + updatedAt: revokedAt, + }) + .where( + and( + eq(relayEnvironmentCredentials.environmentId, input.environmentId), + eq(relayEnvironmentCredentials.environmentPublicKey, input.environmentPublicKey), + isNull(relayEnvironmentCredentials.revokedAt), + notExists( + db + .select({ userId: relayEnvironmentLinks.userId }) + .from(relayEnvironmentLinks) + .where( + and( + eq(relayEnvironmentLinks.environmentId, input.environmentId), + eq(relayEnvironmentLinks.environmentPublicKey, input.environmentPublicKey), + isNull(relayEnvironmentLinks.revokedAt), ), - ), + ), ), - ) - .returning({ - credentialId: relayEnvironmentCredentials.credentialId, - }); - return rows.length > 0; - }, - Effect.mapError((cause) => new EnvironmentCredentialRevokePersistenceError({ cause })), - ), + ), + ) + .returning({ + credentialId: relayEnvironmentCredentials.credentialId, + }) + .pipe( + Effect.mapError( + (cause) => + new EnvironmentCredentialRevokePersistenceError({ + environmentId: input.environmentId, + cause, + }), + ), + ); + return rows.length > 0; + }), }); }); diff --git a/infra/relay/src/environments/EnvironmentLinks.test.ts b/infra/relay/src/environments/EnvironmentLinks.test.ts index 346daef44a6..dccb9e39f60 100644 --- a/infra/relay/src/environments/EnvironmentLinks.test.ts +++ b/infra/relay/src/environments/EnvironmentLinks.test.ts @@ -8,6 +8,76 @@ import { relayEnvironmentLinks } from "../persistence/schema.ts"; import * as EnvironmentLinks from "./EnvironmentLinks.ts"; describe("EnvironmentLinks", () => { + it.effect("retains link lookup failures with user and environment identity", () => { + const cause = new Error("database unavailable"); + const fakeDb = { + select: () => ({ + from: (table: unknown) => { + expect(table).toBe(relayEnvironmentLinks); + return { + where: () => ({ + limit: () => Effect.fail(cause), + }), + }; + }, + }), + } as unknown as RelayDb.RelayDb["Service"]; + + return Effect.gen(function* () { + const links = yield* EnvironmentLinks.EnvironmentLinks; + const error = yield* Effect.flip( + links.getForUser({ userId: "user-1", environmentId: "env-1" }), + ); + + expect(error).toMatchObject({ + _tag: "EnvironmentLinkLookupPersistenceError", + userId: "user-1", + environmentId: "env-1", + }); + expect(error.cause).toBe(cause); + }).pipe( + Effect.provide( + EnvironmentLinks.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb))), + ), + ); + }); + + it.effect("identifies delivery-user list failures without retaining key material", () => { + const cause = new Error("database unavailable"); + const fakeDb = { + select: () => ({ + from: (table: unknown) => { + expect(table).toBe(relayEnvironmentLinks); + return { + where: () => Effect.fail(cause), + }; + }, + }), + } as unknown as RelayDb.RelayDb["Service"]; + + return Effect.gen(function* () { + const links = yield* EnvironmentLinks.EnvironmentLinks; + const error = yield* Effect.flip( + links.listDeliveryUsersForEnvironment({ + environmentId: "env-1", + environmentPublicKey: "sensitive-public-key-material", + }), + ); + + expect(error).toMatchObject({ + _tag: "EnvironmentLinkUserListPersistenceError", + operation: "list-delivery-users", + environmentId: "env-1", + }); + expect(error.cause).toBe(cause); + expect(error).not.toHaveProperty("environmentPublicKey"); + }).pipe( + Effect.provide( + EnvironmentLinks.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb))), + ), + ); + }); + it.effect("selects users when either notifications or Live Activities are enabled", () => { const whereConditions: Array = []; const fakeDb = { diff --git a/infra/relay/src/environments/EnvironmentLinks.ts b/infra/relay/src/environments/EnvironmentLinks.ts index ee7019656cc..6630af0a11b 100644 --- a/infra/relay/src/environments/EnvironmentLinks.ts +++ b/infra/relay/src/environments/EnvironmentLinks.ts @@ -26,55 +26,78 @@ export interface AgentAwarenessDeliveryUserRecord { export class EnvironmentLinkUpsertPersistenceError extends Schema.TaggedErrorClass()( "EnvironmentLinkUpsertPersistenceError", - { cause: Schema.Defect() }, + { + userId: Schema.String, + environmentId: Schema.String, + deviceId: Schema.optionalKey(Schema.String), + cause: Schema.Defect(), + }, ) { override get message(): string { - return "Failed to persist environment link"; + return `Failed to persist environment link for user '${this.userId}', environment '${this.environmentId}'`; } } export class EnvironmentLinkUserListPersistenceError extends Schema.TaggedErrorClass()( "EnvironmentLinkUserListPersistenceError", - { cause: Schema.Defect() }, + { + operation: Schema.Literals(["list-users", "list-delivery-users"]), + environmentId: Schema.String, + cause: Schema.Defect(), + }, ) { override get message(): string { - return "Failed to list users linked to environment"; + return `Environment link user query '${this.operation}' failed for environment '${this.environmentId}'`; } } export class EnvironmentPublicKeyListPersistenceError extends Schema.TaggedErrorClass()( "EnvironmentPublicKeyListPersistenceError", - { cause: Schema.Defect() }, + { + environmentId: Schema.String, + cause: Schema.Defect(), + }, ) { override get message(): string { - return "Failed to list environment public keys"; + return `Failed to list public keys for environment '${this.environmentId}'`; } } export class EnvironmentLinkListPersistenceError extends Schema.TaggedErrorClass()( "EnvironmentLinkListPersistenceError", - { cause: Schema.Defect() }, + { + userId: Schema.String, + cause: Schema.Defect(), + }, ) { override get message(): string { - return "Failed to list environment links"; + return `Failed to list environment links for user '${this.userId}'`; } } export class EnvironmentLinkLookupPersistenceError extends Schema.TaggedErrorClass()( "EnvironmentLinkLookupPersistenceError", - { cause: Schema.Defect() }, + { + userId: Schema.String, + environmentId: Schema.String, + cause: Schema.Defect(), + }, ) { override get message(): string { - return "Failed to look up environment link"; + return `Failed to look up environment link for user '${this.userId}', environment '${this.environmentId}'`; } } export class EnvironmentLinkRevokePersistenceError extends Schema.TaggedErrorClass()( "EnvironmentLinkRevokePersistenceError", - { cause: Schema.Defect() }, + { + userId: Schema.String, + environmentId: Schema.String, + cause: Schema.Defect(), + }, ) { override get message(): string { - return "Failed to revoke environment link"; + return `Failed to revoke environment link for user '${this.userId}', environment '${this.environmentId}'`; } } @@ -142,22 +165,37 @@ const make = Effect.gen(function* () { const db = yield* RelayDb.RelayDb; return EnvironmentLinks.of({ - upsert: Effect.fn("relay.environment_links.upsert")( - function* (input) { - yield* Effect.annotateCurrentSpan({ - "relay.environment_id": input.proof.environmentId, - }); - const now = DateTime.formatIso(yield* DateTime.now); - const { request, proof } = input; - const environmentId = proof.environmentId; - const { endpoint } = input; - yield* db - .insert(relayEnvironmentLinks) - .values({ - userId: input.userId, - environmentId, - environmentLabel: proof.descriptor.label, + upsert: Effect.fn("relay.environment_links.upsert")(function* (input) { + yield* Effect.annotateCurrentSpan({ + "relay.environment_id": input.proof.environmentId, + }); + const now = DateTime.formatIso(yield* DateTime.now); + const { request, proof } = input; + const environmentId = proof.environmentId; + const { endpoint } = input; + yield* db + .insert(relayEnvironmentLinks) + .values({ + userId: input.userId, + environmentId, + environmentLabel: proof.descriptor.label, + environmentPublicKey: proof.environmentPublicKey, + endpointHttpBaseUrl: endpoint.httpBaseUrl, + endpointWsBaseUrl: endpoint.wsBaseUrl, + endpointProviderKind: endpoint.providerKind, + notificationsEnabled: request.notificationsEnabled, + liveActivitiesEnabled: request.liveActivitiesEnabled, + managedTunnelsEnabled: request.managedTunnelsEnabled, + createdByDeviceId: request.deviceId ?? null, + revokedAt: null, + createdAt: now, + updatedAt: now, + }) + .onConflictDoUpdate({ + target: [relayEnvironmentLinks.userId, relayEnvironmentLinks.environmentId], + set: { environmentPublicKey: proof.environmentPublicKey, + environmentLabel: proof.descriptor.label, endpointHttpBaseUrl: endpoint.httpBaseUrl, endpointWsBaseUrl: endpoint.wsBaseUrl, endpointProviderKind: endpoint.providerKind, @@ -166,28 +204,21 @@ const make = Effect.gen(function* () { managedTunnelsEnabled: request.managedTunnelsEnabled, createdByDeviceId: request.deviceId ?? null, revokedAt: null, - createdAt: now, updatedAt: now, - }) - .onConflictDoUpdate({ - target: [relayEnvironmentLinks.userId, relayEnvironmentLinks.environmentId], - set: { - environmentPublicKey: proof.environmentPublicKey, - environmentLabel: proof.descriptor.label, - endpointHttpBaseUrl: endpoint.httpBaseUrl, - endpointWsBaseUrl: endpoint.wsBaseUrl, - endpointProviderKind: endpoint.providerKind, - notificationsEnabled: request.notificationsEnabled, - liveActivitiesEnabled: request.liveActivitiesEnabled, - managedTunnelsEnabled: request.managedTunnelsEnabled, - createdByDeviceId: request.deviceId ?? null, - revokedAt: null, - updatedAt: now, - }, - }); - }, - Effect.mapError((cause) => new EnvironmentLinkUpsertPersistenceError({ cause })), - ), + }, + }) + .pipe( + Effect.mapError( + (cause) => + new EnvironmentLinkUpsertPersistenceError({ + userId: input.userId, + environmentId, + ...(request.deviceId === undefined ? {} : { deviceId: request.deviceId }), + cause, + }), + ), + ); + }), listUsersForEnvironment: Effect.fn("relay.environment_links.list_users_for_environment")( function* (input) { @@ -198,7 +229,14 @@ const make = Effect.gen(function* () { .where(agentAwarenessDeliveryUserCondition(input.environmentId)) .pipe( Effect.map((rows) => rows.map((row) => row.userId)), - Effect.mapError((cause) => new EnvironmentLinkUserListPersistenceError({ cause })), + Effect.mapError( + (cause) => + new EnvironmentLinkUserListPersistenceError({ + operation: "list-users", + environmentId: input.environmentId, + cause, + }), + ), ); }, ), @@ -223,7 +261,14 @@ const make = Effect.gen(function* () { liveActivitiesEnabled: row.liveActivitiesEnabled, })), ), - Effect.mapError((cause) => new EnvironmentLinkUserListPersistenceError({ cause })), + Effect.mapError( + (cause) => + new EnvironmentLinkUserListPersistenceError({ + operation: "list-delivery-users", + environmentId: input.environmentId, + cause, + }), + ), ); }), @@ -244,7 +289,13 @@ const make = Effect.gen(function* () { Effect.map((rows) => [ ...new Set(rows.map((row) => row.environmentPublicKey).filter((key) => key.length > 0)), ]), - Effect.mapError((cause) => new EnvironmentPublicKeyListPersistenceError({ cause })), + Effect.mapError( + (cause) => + new EnvironmentPublicKeyListPersistenceError({ + environmentId: input.environmentId, + cause, + }), + ), ); }), @@ -280,7 +331,13 @@ const make = Effect.gen(function* () { linkedAt: row.createdAt, })), ), - Effect.mapError((cause) => new EnvironmentLinkListPersistenceError({ cause })), + Effect.mapError( + (cause) => + new EnvironmentLinkListPersistenceError({ + userId: input.userId, + cause, + }), + ), ); }), @@ -328,34 +385,48 @@ const make = Effect.gen(function* () { } : null; }), - Effect.mapError((cause) => new EnvironmentLinkLookupPersistenceError({ cause })), + Effect.mapError( + (cause) => + new EnvironmentLinkLookupPersistenceError({ + userId: input.userId, + environmentId: input.environmentId, + cause, + }), + ), ); }), - revokeForUser: Effect.fn("relay.environment_links.revoke_for_user")( - function* (input) { - yield* Effect.annotateCurrentSpan({ - "relay.environment_id": input.environmentId, - }); - const revokedAt = DateTime.formatIso(yield* DateTime.now); - const rows = yield* db - .update(relayEnvironmentLinks) - .set({ - revokedAt, - updatedAt: revokedAt, - }) - .where( - and( - eq(relayEnvironmentLinks.userId, input.userId), - eq(relayEnvironmentLinks.environmentId, input.environmentId), - isNull(relayEnvironmentLinks.revokedAt), - ), - ) - .returning({ environmentId: relayEnvironmentLinks.environmentId }); - return rows.length > 0; - }, - Effect.mapError((cause) => new EnvironmentLinkRevokePersistenceError({ cause })), - ), + revokeForUser: Effect.fn("relay.environment_links.revoke_for_user")(function* (input) { + yield* Effect.annotateCurrentSpan({ + "relay.environment_id": input.environmentId, + }); + const revokedAt = DateTime.formatIso(yield* DateTime.now); + const rows = yield* db + .update(relayEnvironmentLinks) + .set({ + revokedAt, + updatedAt: revokedAt, + }) + .where( + and( + eq(relayEnvironmentLinks.userId, input.userId), + eq(relayEnvironmentLinks.environmentId, input.environmentId), + isNull(relayEnvironmentLinks.revokedAt), + ), + ) + .returning({ environmentId: relayEnvironmentLinks.environmentId }) + .pipe( + Effect.mapError( + (cause) => + new EnvironmentLinkRevokePersistenceError({ + userId: input.userId, + environmentId: input.environmentId, + cause, + }), + ), + ); + return rows.length > 0; + }), }); }); diff --git a/infra/relay/src/http/Api.test.ts b/infra/relay/src/http/Api.test.ts index 6061c6e8174..158bcec9803 100644 --- a/infra/relay/src/http/Api.test.ts +++ b/infra/relay/src/http/Api.test.ts @@ -108,6 +108,7 @@ describe("relay client authentication", () => { describe("relay environment authentication", () => { it.effect("preserves credential lookup persistence failures as internal errors", () => { const failure = new EnvironmentCredentials.EnvironmentCredentialAuthenticatePersistenceError({ + stage: "lookup-credential", cause: "database unavailable", }); const credentials: EnvironmentCredentials.EnvironmentCredentials["Service"] = { diff --git a/infra/relay/src/http/Api.ts b/infra/relay/src/http/Api.ts index 33bcd187c26..1f0ee6f8ae0 100644 --- a/infra/relay/src/http/Api.ts +++ b/infra/relay/src/http/Api.ts @@ -238,13 +238,12 @@ export const relayEnvironmentAuthLayer = Layer.effect( { credential }, ) { const token = readHttpAuthorizationCredential(credential); - const principal = yield* credentials - .authenticate(token) - .pipe( - Effect.catchTag("EnvironmentCredentialAuthenticatePersistenceError", () => + const principal = yield* credentials.authenticate(token).pipe( + Effect.catchTags({ + EnvironmentCredentialAuthenticatePersistenceError: () => relayInternalErrorResponse("persistence_failed"), - ), - ); + }), + ); if (principal._tag === "None") { return yield* relayAuthInvalidError("not_authorized"); } From f72fe4d5e45497c355b0da1ed35b4ad00758ef01 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 06:45:25 -0700 Subject: [PATCH 2/3] Inline relay APNS error mappings Co-authored-by: codex --- infra/relay/src/http/Api.ts | 85 ++++++++++++++++++++++++++++--------- 1 file changed, 66 insertions(+), 19 deletions(-) diff --git a/infra/relay/src/http/Api.ts b/infra/relay/src/http/Api.ts index 1f0ee6f8ae0..29e2026de3c 100644 --- a/infra/relay/src/http/Api.ts +++ b/infra/relay/src/http/Api.ts @@ -776,17 +776,72 @@ export const serverApi = HttpApiBuilder.group( reason: "persistence_failed", traceId, }), - ApnsDeliveryJobQueuePayloadInvalid: mapApnsDeliveryJobInternalError, - ApnsDeliveryJobLiveActivityAggregateMissing: mapApnsDeliveryJobInternalError, - ApnsDeliveryJobLiveActivityNotificationUnexpected: mapApnsDeliveryJobInternalError, - ApnsDeliveryJobPushNotificationMissing: mapApnsDeliveryJobInternalError, - ApnsDeliveryJobPushNotificationAggregateUnexpected: mapApnsDeliveryJobInternalError, - ApnsDeliveryJobCreatedAtInvalid: mapApnsDeliveryJobInternalError, - ApnsDeliveryJobExpiresAtInvalid: mapApnsDeliveryJobInternalError, - ApnsDeliveryJobTimeWindowInvalid: mapApnsDeliveryJobInternalError, - ApnsDeliveryJobTimeWindowTooLong: mapApnsDeliveryJobInternalError, - ApnsDeliveryJobSignatureInvalid: mapApnsDeliveryJobInternalError, - ApnsDeliveryJobExpired: mapApnsDeliveryJobInternalError, + ApnsDeliveryJobQueuePayloadInvalid: (_error, traceId) => + new RelayInternalError({ + code: "internal_error", + reason: "internal_error", + traceId, + }), + ApnsDeliveryJobLiveActivityAggregateMissing: (_error, traceId) => + new RelayInternalError({ + code: "internal_error", + reason: "internal_error", + traceId, + }), + ApnsDeliveryJobLiveActivityNotificationUnexpected: (_error, traceId) => + new RelayInternalError({ + code: "internal_error", + reason: "internal_error", + traceId, + }), + ApnsDeliveryJobPushNotificationMissing: (_error, traceId) => + new RelayInternalError({ + code: "internal_error", + reason: "internal_error", + traceId, + }), + ApnsDeliveryJobPushNotificationAggregateUnexpected: (_error, traceId) => + new RelayInternalError({ + code: "internal_error", + reason: "internal_error", + traceId, + }), + ApnsDeliveryJobCreatedAtInvalid: (_error, traceId) => + new RelayInternalError({ + code: "internal_error", + reason: "internal_error", + traceId, + }), + ApnsDeliveryJobExpiresAtInvalid: (_error, traceId) => + new RelayInternalError({ + code: "internal_error", + reason: "internal_error", + traceId, + }), + ApnsDeliveryJobTimeWindowInvalid: (_error, traceId) => + new RelayInternalError({ + code: "internal_error", + reason: "internal_error", + traceId, + }), + ApnsDeliveryJobTimeWindowTooLong: (_error, traceId) => + new RelayInternalError({ + code: "internal_error", + reason: "internal_error", + traceId, + }), + ApnsDeliveryJobSignatureInvalid: (_error, traceId) => + new RelayInternalError({ + code: "internal_error", + reason: "internal_error", + traceId, + }), + ApnsDeliveryJobExpired: (_error, traceId) => + new RelayInternalError({ + code: "internal_error", + reason: "internal_error", + traceId, + }), ApnsDeliveryJobClaimInFlight: (_error, traceId) => new RelayInternalError({ code: "internal_error", @@ -891,14 +946,6 @@ function mapRelayCommonApiErrors(authReason: RelayAuthInvalidReason) { ): Effect.Effect, R> => effect.pipe(Effect.catch(mapError)); } -function mapApnsDeliveryJobInternalError(_error: unknown, traceId: string) { - return new RelayInternalError({ - code: "internal_error", - reason: "internal_error", - traceId, - }); -} - type TaggedErrorTag = Extract["_tag"]; type MapErrorTagCases = { From cda9aed54e2249df579400be9790607373095d46 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 20 Jun 2026 07:21:08 -0700 Subject: [PATCH 3/3] Rerun Effect service convention checks Co-authored-by: codex