authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 11:22:28-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 11:22:28-07:00
logc3e3e26844a591f2bb21e617db3c5494e700fbcc
treef407a8a4337511969217348b81dc3972ec80c628
parent1486a4a2b9c3f5a83d54f5ce591a2f60fba9316f
signature Signed by PGP key B5690EEEBB952194

[codex] Structure primary environment request failures (#3409)

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

4 files changed, 127 insertions(+), 81 deletions(-)

apps/web/src/authBootstrap.test.ts+27-11
......@@ -290,22 +290,38 @@ describe("resolveInitialServerAuthGateState", () => {
290290 });
291291
292292 it("surfaces a friendly error message when an invalid pairing token is submitted", async () => {
293 const cause = new EnvironmentAuthInvalidError({
294 code: "auth_invalid",
295 reason: "invalid_credential",
296 traceId: "trace-invalid-credential",
297 });
293298 const testApi = await installAuthApi({
294 browserSession: () =>
295 Effect.fail(
296 new EnvironmentAuthInvalidError({
297 code: "auth_invalid",
298 reason: "invalid_credential",
299 traceId: "trace-invalid-credential",
300 }),
301 ),
299 browserSession: () => Effect.fail(cause),
302300 });
303301
304 const { submitServerAuthCredential } = await import("./environments/primary");
302 const { isPrimaryEnvironmentRequestError, submitServerAuthCredential } =
303 await import("./environments/primary");
305304
306 await expect(submitServerAuthCredential("bad-token")).rejects.toThrow(
307 "Invalid pairing token. Check the token and try again.",
305 const error = await submitServerAuthCredential("bad-token").then(
306 () => null,
307 (failure: unknown) => failure,
308308 );
309 expect(error).toMatchObject({
310 _tag: "PrimaryEnvironmentRequestError",
311 operation: "exchange-bootstrap-credential",
312 status: 401,
313 detail: "Invalid pairing token. Check the token and try again.",
314 });
315 expect(isPrimaryEnvironmentRequestError(error)).toBe(true);
316 if (!isPrimaryEnvironmentRequestError(error)) {
317 throw new Error("Expected a structured primary environment request error.");
318 }
319 expect(error.cause).toMatchObject({
320 _tag: "EnvironmentAuthInvalidError",
321 code: "auth_invalid",
322 reason: "invalid_credential",
323 traceId: "trace-invalid-credential",
324 });
309325 expect(testApi.calls.browserSession).toEqual([{ credential: "bad-token" }]);
310326 });
311327
apps/web/src/environments/primary/auth.ts+93-61
......@@ -21,15 +21,57 @@ import {
2121
2222import { PrimaryEnvironmentHttpClient } from "./httpClient";
2323import { runPrimaryHttp } from "../../lib/runtime";
24import * as Data from "effect/Data";
25import * as Predicate from "effect/Predicate";
26
27export class BootstrapHttpError extends Data.TaggedError("BootstrapHttpError")<{
28 readonly message: string;
29 readonly status: number;
30}> {}
31const isBootstrapHttpError = (u: unknown): u is BootstrapHttpError =>
32 Predicate.isTagged(u, "BootstrapHttpError");
24
25const PrimaryEnvironmentRequestOperation = Schema.Literals([
26 "fetch-session-state",
27 "exchange-bootstrap-credential",
28 "fetch-environment-descriptor",
29 "create-pairing-credential",
30 "list-pairing-links",
31 "revoke-pairing-link",
32 "list-client-sessions",
33 "revoke-client-session",
34 "revoke-other-client-sessions",
35]);
36type PrimaryEnvironmentRequestOperation = typeof PrimaryEnvironmentRequestOperation.Type;
37
38export class PrimaryEnvironmentRequestError extends Schema.TaggedErrorClass<PrimaryEnvironmentRequestError>()(
39 "PrimaryEnvironmentRequestError",
40 {
41 operation: PrimaryEnvironmentRequestOperation,
42 status: Schema.Number,
43 detail: Schema.String,
44 pairingLinkId: Schema.optional(Schema.String),
45 sessionId: Schema.optional(Schema.String),
46 cause: Schema.Defect(),
47 },
48) {
49 static fromCause(input: {
50 readonly operation: PrimaryEnvironmentRequestOperation;
51 readonly cause: unknown;
52 readonly fallbackMessage: (status: number) => string;
53 readonly formatDetail?: (detail: string, status: number) => string;
54 readonly pairingLinkId?: string;
55 readonly sessionId?: string;
56 }): PrimaryEnvironmentRequestError {
57 const status = readHttpApiStatus(input.cause) ?? 500;
58 const rawDetail = readHttpApiErrorMessage(input.cause, input.fallbackMessage(status));
59 return new PrimaryEnvironmentRequestError({
60 operation: input.operation,
61 status,
62 detail: input.formatDetail?.(rawDetail, status) ?? rawDetail,
63 ...(input.pairingLinkId !== undefined ? { pairingLinkId: input.pairingLinkId } : {}),
64 ...(input.sessionId !== undefined ? { sessionId: input.sessionId } : {}),
65 cause: input.cause,
66 });
67 }
68
69 override get message(): string {
70 return this.detail;
71 }
72}
73
74export const isPrimaryEnvironmentRequestError = Schema.is(PrimaryEnvironmentRequestError);
3375const isEnvironmentHttpCommonError = Schema.is(EnvironmentHttpCommonError);
3476
3577export interface ServerPairingLinkRecord {
......@@ -106,10 +148,10 @@ export async function fetchSessionState(): Promise<AuthSessionState> {
106148 ),
107149 );
108150 } catch (error) {
109 const status = readHttpApiStatus(error);
110 throw new BootstrapHttpError({
111 message: `Failed to load server auth session state (${status ?? "unknown"}).`,
112 status: status ?? 500,
151 throw PrimaryEnvironmentRequestError.fromCause({
152 operation: "fetch-session-state",
153 cause: error,
154 fallbackMessage: (status) => `Failed to load server auth session state (${status}).`,
113155 });
114156 }
115157 });
......@@ -183,11 +225,11 @@ async function exchangeBootstrapCredential(credential: string): Promise<AuthBrow
183225 ),
184226 );
185227 } catch (error) {
186 const status = readHttpApiStatus(error) ?? 500;
187 const message = toFriendlyBootstrapErrorMessage(status, readHttpApiErrorMessage(error, ""));
188 throw new BootstrapHttpError({
189 message: message || `Failed to bootstrap auth session (${status}).`,
190 status,
228 throw PrimaryEnvironmentRequestError.fromCause({
229 operation: "exchange-bootstrap-credential",
230 cause: error,
231 fallbackMessage: (status) => `Failed to bootstrap auth session (${status}).`,
232 formatDetail: (detail, status) => toFriendlyBootstrapErrorMessage(status, detail),
191233 });
192234 }
193235 });
......@@ -240,7 +282,7 @@ function waitForBootstrapRetry(delayMs: number): Promise<void> {
240282}
241283
242284function isTransientBootstrapError(error: unknown): boolean {
243 if (isBootstrapHttpError(error)) {
285 if (isPrimaryEnvironmentRequestError(error)) {
244286 return TRANSIENT_BOOTSTRAP_STATUS_CODES.has(error.status);
245287 }
246288
......@@ -310,13 +352,11 @@ export async function createServerPairingCredential(input?: {
310352 ),
311353 );
312354 } catch (error) {
313 throw new Error(
314 readHttpApiErrorMessage(
315 error,
316 `Failed to create pairing credential (${readHttpApiStatus(error) ?? "unknown"}).`,
317 ),
318 { cause: error },
319 );
355 throw PrimaryEnvironmentRequestError.fromCause({
356 operation: "create-pairing-credential",
357 cause: error,
358 fallbackMessage: (status) => `Failed to create pairing credential (${status}).`,
359 });
320360 }
321361}
322362
......@@ -353,13 +393,11 @@ export async function listServerPairingLinks(): Promise<ReadonlyArray<ServerPair
353393 };
354394 });
355395 } catch (error) {
356 throw new Error(
357 readHttpApiErrorMessage(
358 error,
359 `Failed to load pairing links (${readHttpApiStatus(error) ?? "unknown"}).`,
360 ),
361 { cause: error },
362 );
396 throw PrimaryEnvironmentRequestError.fromCause({
397 operation: "list-pairing-links",
398 cause: error,
399 fallbackMessage: (status) => `Failed to load pairing links (${status}).`,
400 });
363401 }
364402}
365403
......@@ -371,13 +409,12 @@ export async function revokeServerPairingLink(id: string): Promise<void> {
371409 ),
372410 );
373411 } catch (error) {
374 throw new Error(
375 readHttpApiErrorMessage(
376 error,
377 `Failed to revoke pairing link (${readHttpApiStatus(error) ?? "unknown"}).`,
378 ),
379 { cause: error },
380 );
412 throw PrimaryEnvironmentRequestError.fromCause({
413 operation: "revoke-pairing-link",
414 pairingLinkId: id,
415 cause: error,
416 fallbackMessage: (status) => `Failed to revoke pairing link (${status}).`,
417 });
381418 }
382419}
383420
......@@ -406,13 +443,11 @@ export async function listServerClientSessions(): Promise<
406443 current: clientSession.current,
407444 }));
408445 } catch (error) {
409 throw new Error(
410 readHttpApiErrorMessage(
411 error,
412 `Failed to load paired clients (${readHttpApiStatus(error) ?? "unknown"}).`,
413 ),
414 { cause: error },
415 );
446 throw PrimaryEnvironmentRequestError.fromCause({
447 operation: "list-client-sessions",
448 cause: error,
449 fallbackMessage: (status) => `Failed to load paired clients (${status}).`,
450 });
416451 }
417452}
418453
......@@ -426,13 +461,12 @@ export async function revokeServerClientSession(sessionId: AuthSessionId): Promi
426461 ),
427462 );
428463 } catch (error) {
429 throw new Error(
430 readHttpApiErrorMessage(
431 error,
432 `Failed to revoke client session (${readHttpApiStatus(error) ?? "unknown"}).`,
433 ),
434 { cause: error },
435 );
464 throw PrimaryEnvironmentRequestError.fromCause({
465 operation: "revoke-client-session",
466 sessionId,
467 cause: error,
468 fallbackMessage: (status) => `Failed to revoke client session (${status}).`,
469 });
436470 }
437471}
438472
......@@ -445,13 +479,11 @@ export async function revokeOtherServerClientSessions(): Promise<number> {
445479 );
446480 return result.revokedCount;
447481 } catch (error) {
448 throw new Error(
449 readHttpApiErrorMessage(
450 error,
451 `Failed to revoke other client sessions (${readHttpApiStatus(error) ?? "unknown"}).`,
452 ),
453 { cause: error },
454 );
482 throw PrimaryEnvironmentRequestError.fromCause({
483 operation: "revoke-other-client-sessions",
484 cause: error,
485 fallbackMessage: (status) => `Failed to revoke other client sessions (${status}).`,
486 });
455487 }
456488}
457489
apps/web/src/environments/primary/context.ts+5-9
......@@ -5,9 +5,8 @@ import {
55} from "@t3tools/client-runtime/environment";
66import type { ExecutionEnvironmentDescriptor } from "@t3tools/contracts";
77import * as Effect from "effect/Effect";
8import { HttpClientError } from "effect/unstable/http";
98
10import { BootstrapHttpError, retryTransientBootstrap } from "./auth";
9import { PrimaryEnvironmentRequestError, retryTransientBootstrap } from "./auth";
1110import { PrimaryEnvironmentHttpClient } from "./httpClient";
1211
1312import { runPrimaryHttp } from "../../lib/runtime";
......@@ -44,13 +43,10 @@ async function fetchPrimaryEnvironmentDescriptor(): Promise<ExecutionEnvironment
4443 PrimaryEnvironmentHttpClient.pipe(Effect.flatMap((client) => client.metadata.descriptor())),
4544 );
4645 } catch (error) {
47 const status =
48 HttpClientError.isHttpClientError(error) && error.response !== undefined
49 ? error.response.status
50 : 500;
51 throw new BootstrapHttpError({
52 message: `Failed to load server environment descriptor (${status}).`,
53 status,
46 throw PrimaryEnvironmentRequestError.fromCause({
47 operation: "fetch-environment-descriptor",
48 cause: error,
49 fallbackMessage: (status) => `Failed to load server environment descriptor (${status}).`,
5450 });
5551 }
5652
apps/web/src/environments/primary/index.ts+2
......@@ -16,9 +16,11 @@ export {
1616export {
1717 createServerPairingCredential,
1818 fetchSessionState,
19 isPrimaryEnvironmentRequestError,
1920 listServerClientSessions,
2021 listServerPairingLinks,
2122 peekPairingTokenFromUrl,
23 PrimaryEnvironmentRequestError,
2224 resolveInitialServerAuthGateState,
2325 revokeOtherServerClientSessions,
2426 revokeServerClientSession,