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";
1313import * as Layer from "effect/Layer";
1414import * as Redacted from "effect/Redacted";
1515import * as Result from "effect/Result";
16import * as Schema from "effect/Schema";
1617
1718import * as DpopProofs from "../auth/DpopProofs.ts";
1819import * as RelayConfiguration from "../Config.ts";
......@@ -51,6 +52,9 @@ const state: RelayAgentActivityState = {
5152 updatedAt: "2026-05-25T00:00:00.000Z",
5253 deepLink: "/threads/env/thread",
5354};
55const isEnvironmentPublishSignatureInvalid = Schema.is(
56 EnvironmentPublishSignatures.EnvironmentPublishSignatureInvalid,
57);
5458
5559function signTestJwt(payload: object, privateKey: string): string {
5660 const header = Buffer.from(
......@@ -145,6 +149,49 @@ describe("EnvironmentPublishSignatures", () => {
145149 }),
146150 );
147151 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 }
148195 }).pipe(Effect.provide(layer())),
149196 );
150197
......@@ -161,6 +208,17 @@ describe("EnvironmentPublishSignatures", () => {
161208 }),
162209 );
163210 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 }
164222 }).pipe(Effect.provide(layer({ consume: () => Effect.succeed(false) }))),
165223 );
166224});
infra/relay/src/environments/EnvironmentPublishSignatures.ts+51-5
......@@ -1,5 +1,6 @@
11import {
22 RelayAgentActivityPublishProofPayload,
3 RelayAgentActivityPublishProofInvalidReason,
34 type RelayAgentActivityPublishRequest,
45} from "@t3tools/contracts/relay";
56import {
......@@ -23,11 +24,13 @@ import * as RelayConfiguration from "../Config.ts";
2324export class EnvironmentPublishSignatureExpired extends Schema.TaggedErrorClass<EnvironmentPublishSignatureExpired>()(
2425 "EnvironmentPublishSignatureExpired",
2526 {
27 environmentId: Schema.String,
28 threadId: Schema.String,
2629 expiresAt: Schema.String,
2730 },
2831) {
2932 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}`;
3134 }
3235}
3336
......@@ -35,10 +38,21 @@ export class EnvironmentPublishSignatureInvalid extends Schema.TaggedErrorClass<
3538 "EnvironmentPublishSignatureInvalid",
3639 {
3740 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()),
3852 },
3953) {
4054 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}`;
4256 }
4357}
4458
......@@ -102,13 +116,22 @@ const make = Effect.gen(function* () {
102116 const now = yield* DateTime.now;
103117 const decoded = yield* Effect.try({
104118 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 }),
106127 });
107128 if (
108129 typeof decoded.exp === "number" &&
109130 decoded.exp <= Math.floor(now.epochMilliseconds / 1_000)
110131 ) {
111132 return yield* new EnvironmentPublishSignatureExpired({
133 environmentId: input.environmentId,
134 threadId: input.threadId,
112135 expiresAt: DateTime.formatIso(DateTime.makeUnsafe(decoded.exp * 1_000)),
113136 });
114137 }
......@@ -122,7 +145,14 @@ const make = Effect.gen(function* () {
122145 }).pipe(
123146 Effect.flatMap(decodeProof),
124147 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 }),
126156 ),
127157 );
128158 if (
......@@ -136,12 +166,18 @@ const make = Effect.gen(function* () {
136166 ) {
137167 return yield* new EnvironmentPublishSignatureInvalid({
138168 environmentId: input.environmentId,
169 threadId: input.threadId,
170 reason: "invalid_signature_or_payload",
171 stage: "validate_claims",
139172 });
140173 }
141174 const expiresAt = DateTime.make(proof.exp * 1_000);
142175 if (expiresAt._tag === "None") {
143176 return yield* new EnvironmentPublishSignatureInvalid({
144177 environmentId: input.environmentId,
178 threadId: input.threadId,
179 reason: "invalid_signature_or_payload",
180 stage: "validate_expiration",
145181 });
146182 }
147183 const thumbprint = yield* crypto
......@@ -155,7 +191,14 @@ const make = Effect.gen(function* () {
155191 .pipe(
156192 Effect.map(formatEnvironmentPublishReplayThumbprint),
157193 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 }),
159202 ),
160203 );
161204 const consumedNonce = yield* proofReplay.consume({
......@@ -167,6 +210,9 @@ const make = Effect.gen(function* () {
167210 if (!consumedNonce) {
168211 return yield* new EnvironmentPublishSignatureInvalid({
169212 environmentId: input.environmentId,
213 threadId: input.threadId,
214 reason: "replayed_nonce",
215 stage: "consume_nonce",
170216 });
171217 }
172218 }),