authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 11:12:31-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 11:12:31-07:00
log716ae73c40e988b462ab73f2bd7aaca789054e35
treef443fa84e3f5d7995566238173ca8e41bf7afb60
parent300d4d566a1044dc401bcd7ebf43def08621229e
signature Signed by PGP key B5690EEEBB952194

[codex] Structure relay publish signature errors (#3335)

Co-authored-by: codex <codex@users.noreply.github.com>

2 files changed, 109 insertions(+), 5 deletions(-)

infra/relay/src/environments/EnvironmentPublishSignatures.test.ts+58
...@@ -13,6 +13,7 @@ import * as Effect from "effect/Effect";...@@ -13,6 +13,7 @@ import * as Effect from "effect/Effect";
13import * as Layer from "effect/Layer";13import * as Layer from "effect/Layer";
14import * as Redacted from "effect/Redacted";14import * as Redacted from "effect/Redacted";
15import * as Result from "effect/Result";15import * as Result from "effect/Result";
16import * as Schema from "effect/Schema";
1617
17import * as DpopProofs from "../auth/DpopProofs.ts";18import * as DpopProofs from "../auth/DpopProofs.ts";
18import * as RelayConfiguration from "../Config.ts";19import * as RelayConfiguration from "../Config.ts";
...@@ -51,6 +52,9 @@ const state: RelayAgentActivityState = {...@@ -51,6 +52,9 @@ const state: RelayAgentActivityState = {
51 updatedAt: "2026-05-25T00:00:00.000Z",52 updatedAt: "2026-05-25T00:00:00.000Z",
52 deepLink: "/threads/env/thread",53 deepLink: "/threads/env/thread",
53};54};
55const isEnvironmentPublishSignatureInvalid = Schema.is(
56 EnvironmentPublishSignatures.EnvironmentPublishSignatureInvalid,
57);
5458
55function signTestJwt(payload: object, privateKey: string): string {59function signTestJwt(payload: object, privateKey: string): string {
56 const header = Buffer.from(60 const header = Buffer.from(
...@@ -145,6 +149,49 @@ describe("EnvironmentPublishSignatures", () => {...@@ -145,6 +149,49 @@ describe("EnvironmentPublishSignatures", () => {
145 }),149 }),
146 );150 );
147 expect(Result.isFailure(result)).toBe(true);151 expect(Result.isFailure(result)).toBe(true);
152 if (Result.isFailure(result)) {
153 expect(isEnvironmentPublishSignatureInvalid(result.failure)).toBe(true);
154 if (isEnvironmentPublishSignatureInvalid(result.failure)) {
155 expect(result.failure).toMatchObject({
156 environmentId: state.environmentId,
157 threadId: state.threadId,
158 reason: "invalid_signature_or_payload",
159 stage: "validate_claims",
160 });
161 }
162 }
163 }).pipe(Effect.provide(layer())),
164 );
165
166 it.effect("preserves the JWT verification failure", () =>
167 Effect.gen(function* () {
168 const request = yield* freshRequest;
169 const segments = request.proof.split(".");
170 const signature = segments[2]!;
171 segments[2] = `${signature.startsWith("A") ? "B" : "A"}${signature.slice(1)}`;
172 const signatures = yield* EnvironmentPublishSignatures.EnvironmentPublishSignatures;
173 const result = yield* Effect.result(
174 signatures.verify({
175 environmentId: state.environmentId,
176 environmentPublicKey: keyPair.publicKey,
177 threadId: state.threadId,
178 request: { ...request, proof: segments.join(".") },
179 }),
180 );
181
182 expect(Result.isFailure(result)).toBe(true);
183 if (Result.isFailure(result)) {
184 expect(isEnvironmentPublishSignatureInvalid(result.failure)).toBe(true);
185 if (isEnvironmentPublishSignatureInvalid(result.failure)) {
186 expect(result.failure).toMatchObject({
187 environmentId: state.environmentId,
188 threadId: state.threadId,
189 reason: "invalid_signature_or_payload",
190 stage: "verify_proof",
191 cause: { _tag: "RelayJwtError" },
192 });
193 }
194 }
148 }).pipe(Effect.provide(layer())),195 }).pipe(Effect.provide(layer())),
149 );196 );
150197
...@@ -161,6 +208,17 @@ describe("EnvironmentPublishSignatures", () => {...@@ -161,6 +208,17 @@ describe("EnvironmentPublishSignatures", () => {
161 }),208 }),
162 );209 );
163 expect(Result.isFailure(result)).toBe(true);210 expect(Result.isFailure(result)).toBe(true);
211 if (Result.isFailure(result)) {
212 expect(isEnvironmentPublishSignatureInvalid(result.failure)).toBe(true);
213 if (isEnvironmentPublishSignatureInvalid(result.failure)) {
214 expect(result.failure).toMatchObject({
215 environmentId: state.environmentId,
216 threadId: state.threadId,
217 reason: "replayed_nonce",
218 stage: "consume_nonce",
219 });
220 }
221 }
164 }).pipe(Effect.provide(layer({ consume: () => Effect.succeed(false) }))),222 }).pipe(Effect.provide(layer({ consume: () => Effect.succeed(false) }))),
165 );223 );
166});224});
infra/relay/src/environments/EnvironmentPublishSignatures.ts+51-5
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1import {1import {
2 RelayAgentActivityPublishProofPayload,2 RelayAgentActivityPublishProofPayload,
3 RelayAgentActivityPublishProofInvalidReason,
3 type RelayAgentActivityPublishRequest,4 type RelayAgentActivityPublishRequest,
4} from "@t3tools/contracts/relay";5} from "@t3tools/contracts/relay";
5import {6import {
...@@ -23,11 +24,13 @@ import * as RelayConfiguration from "../Config.ts";...@@ -23,11 +24,13 @@ import * as RelayConfiguration from "../Config.ts";
23export class EnvironmentPublishSignatureExpired extends Schema.TaggedErrorClass<EnvironmentPublishSignatureExpired>()(24export class EnvironmentPublishSignatureExpired extends Schema.TaggedErrorClass<EnvironmentPublishSignatureExpired>()(
24 "EnvironmentPublishSignatureExpired",25 "EnvironmentPublishSignatureExpired",
25 {26 {
27 environmentId: Schema.String,
28 threadId: Schema.String,
26 expiresAt: Schema.String,29 expiresAt: Schema.String,
27 },30 },
28) {31) {
29 override get message(): string {32 override get message(): string {
30 return `Environment publish signature expired at ${this.expiresAt}`;33 return `Environment '${this.environmentId}' publish signature for thread '${this.threadId}' expired at ${this.expiresAt}`;
31 }34 }
32}35}
3336
...@@ -35,10 +38,21 @@ export class EnvironmentPublishSignatureInvalid extends Schema.TaggedErrorClass<...@@ -35,10 +38,21 @@ export class EnvironmentPublishSignatureInvalid extends Schema.TaggedErrorClass<
35 "EnvironmentPublishSignatureInvalid",38 "EnvironmentPublishSignatureInvalid",
36 {39 {
37 environmentId: Schema.String,40 environmentId: Schema.String,
41 threadId: Schema.String,
42 reason: RelayAgentActivityPublishProofInvalidReason,
43 stage: Schema.Literals([
44 "decode_token",
45 "verify_proof",
46 "validate_claims",
47 "validate_expiration",
48 "generate_replay_thumbprint",
49 "consume_nonce",
50 ]),
51 cause: Schema.optional(Schema.Defect()),
38 },52 },
39) {53) {
40 override get message(): string {54 override get message(): string {
41 return `Environment '${this.environmentId}' publish signature is invalid`;55 return `Environment '${this.environmentId}' publish signature for thread '${this.threadId}' is invalid during ${this.stage}: ${this.reason}`;
42 }56 }
43}57}
4458
...@@ -102,13 +116,22 @@ const make = Effect.gen(function* () {...@@ -102,13 +116,22 @@ const make = Effect.gen(function* () {
102 const now = yield* DateTime.now;116 const now = yield* DateTime.now;
103 const decoded = yield* Effect.try({117 const decoded = yield* Effect.try({
104 try: () => decodeRelayJwt(input.request.proof),118 try: () => decodeRelayJwt(input.request.proof),
105 catch: () => new EnvironmentPublishSignatureInvalid({ environmentId: input.environmentId }),119 catch: (cause) =>
120 new EnvironmentPublishSignatureInvalid({
121 environmentId: input.environmentId,
122 threadId: input.threadId,
123 reason: "invalid_signature_or_payload",
124 stage: "decode_token",
125 cause,
126 }),
106 });127 });
107 if (128 if (
108 typeof decoded.exp === "number" &&129 typeof decoded.exp === "number" &&
109 decoded.exp <= Math.floor(now.epochMilliseconds / 1_000)130 decoded.exp <= Math.floor(now.epochMilliseconds / 1_000)
110 ) {131 ) {
111 return yield* new EnvironmentPublishSignatureExpired({132 return yield* new EnvironmentPublishSignatureExpired({
133 environmentId: input.environmentId,
134 threadId: input.threadId,
112 expiresAt: DateTime.formatIso(DateTime.makeUnsafe(decoded.exp * 1_000)),135 expiresAt: DateTime.formatIso(DateTime.makeUnsafe(decoded.exp * 1_000)),
113 });136 });
114 }137 }
...@@ -122,7 +145,14 @@ const make = Effect.gen(function* () {...@@ -122,7 +145,14 @@ const make = Effect.gen(function* () {
122 }).pipe(145 }).pipe(
123 Effect.flatMap(decodeProof),146 Effect.flatMap(decodeProof),
124 Effect.mapError(147 Effect.mapError(
125 () => new EnvironmentPublishSignatureInvalid({ environmentId: input.environmentId }),148 (cause) =>
149 new EnvironmentPublishSignatureInvalid({
150 environmentId: input.environmentId,
151 threadId: input.threadId,
152 reason: "invalid_signature_or_payload",
153 stage: "verify_proof",
154 cause,
155 }),
126 ),156 ),
127 );157 );
128 if (158 if (
...@@ -136,12 +166,18 @@ const make = Effect.gen(function* () {...@@ -136,12 +166,18 @@ const make = Effect.gen(function* () {
136 ) {166 ) {
137 return yield* new EnvironmentPublishSignatureInvalid({167 return yield* new EnvironmentPublishSignatureInvalid({
138 environmentId: input.environmentId,168 environmentId: input.environmentId,
169 threadId: input.threadId,
170 reason: "invalid_signature_or_payload",
171 stage: "validate_claims",
139 });172 });
140 }173 }
141 const expiresAt = DateTime.make(proof.exp * 1_000);174 const expiresAt = DateTime.make(proof.exp * 1_000);
142 if (expiresAt._tag === "None") {175 if (expiresAt._tag === "None") {
143 return yield* new EnvironmentPublishSignatureInvalid({176 return yield* new EnvironmentPublishSignatureInvalid({
144 environmentId: input.environmentId,177 environmentId: input.environmentId,
178 threadId: input.threadId,
179 reason: "invalid_signature_or_payload",
180 stage: "validate_expiration",
145 });181 });
146 }182 }
147 const thumbprint = yield* crypto183 const thumbprint = yield* crypto
...@@ -155,7 +191,14 @@ const make = Effect.gen(function* () {...@@ -155,7 +191,14 @@ const make = Effect.gen(function* () {
155 .pipe(191 .pipe(
156 Effect.map(formatEnvironmentPublishReplayThumbprint),192 Effect.map(formatEnvironmentPublishReplayThumbprint),
157 Effect.mapError(193 Effect.mapError(
158 () => new EnvironmentPublishSignatureInvalid({ environmentId: input.environmentId }),194 (cause) =>
195 new EnvironmentPublishSignatureInvalid({
196 environmentId: input.environmentId,
197 threadId: input.threadId,
198 reason: "invalid_signature_or_payload",
199 stage: "generate_replay_thumbprint",
200 cause,
201 }),
159 ),202 ),
160 );203 );
161 const consumedNonce = yield* proofReplay.consume({204 const consumedNonce = yield* proofReplay.consume({
...@@ -167,6 +210,9 @@ const make = Effect.gen(function* () {...@@ -167,6 +210,9 @@ const make = Effect.gen(function* () {
167 if (!consumedNonce) {210 if (!consumedNonce) {
168 return yield* new EnvironmentPublishSignatureInvalid({211 return yield* new EnvironmentPublishSignatureInvalid({
169 environmentId: input.environmentId,212 environmentId: input.environmentId,
213 threadId: input.threadId,
214 reason: "replayed_nonce",
215 stage: "consume_nonce",
170 });216 });
171 }217 }
172 }),218 }),