| ... | @@ -21,15 +21,57 @@ import { | ... | @@ -21,15 +21,57 @@ import { |
| 21 | | 21 | |
| 22 | import { PrimaryEnvironmentHttpClient } from "./httpClient"; | 22 | import { PrimaryEnvironmentHttpClient } from "./httpClient"; |
| 23 | import { runPrimaryHttp } from "../../lib/runtime"; | 23 | import { runPrimaryHttp } from "../../lib/runtime"; |
| 24 | import * as Data from "effect/Data"; | 24 | |
| 25 | import * as Predicate from "effect/Predicate"; | 25 | const PrimaryEnvironmentRequestOperation = Schema.Literals([ |
| 26 | | 26 | "fetch-session-state", |
| 27 | export class BootstrapHttpError extends Data.TaggedError("BootstrapHttpError")<{ | 27 | "exchange-bootstrap-credential", |
| 28 | readonly message: string; | 28 | "fetch-environment-descriptor", |
| 29 | readonly status: number; | 29 | "create-pairing-credential", |
| 30 | }> {} | 30 | "list-pairing-links", |
| 31 | const isBootstrapHttpError = (u: unknown): u is BootstrapHttpError => | 31 | "revoke-pairing-link", |
| 32 | Predicate.isTagged(u, "BootstrapHttpError"); | 32 | "list-client-sessions", |
| | 33 | "revoke-client-session", |
| | 34 | "revoke-other-client-sessions", |
| | 35 | ]); |
| | 36 | type PrimaryEnvironmentRequestOperation = typeof PrimaryEnvironmentRequestOperation.Type; |
| | 37 | |
| | 38 | export 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 | |
| | 74 | export const isPrimaryEnvironmentRequestError = Schema.is(PrimaryEnvironmentRequestError); |
| 33 | const isEnvironmentHttpCommonError = Schema.is(EnvironmentHttpCommonError); | 75 | const isEnvironmentHttpCommonError = Schema.is(EnvironmentHttpCommonError); |
| 34 | | 76 | |
| 35 | export interface ServerPairingLinkRecord { | 77 | export interface ServerPairingLinkRecord { |
| ... | @@ -106,10 +148,10 @@ export async function fetchSessionState(): Promise<AuthSessionState> { | ... | @@ -106,10 +148,10 @@ export async function fetchSessionState(): Promise<AuthSessionState> { |
| 106 | ), | 148 | ), |
| 107 | ); | 149 | ); |
| 108 | } catch (error) { | 150 | } catch (error) { |
| 109 | const status = readHttpApiStatus(error); | 151 | throw PrimaryEnvironmentRequestError.fromCause({ |
| 110 | throw new BootstrapHttpError({ | 152 | operation: "fetch-session-state", |
| 111 | message: `Failed to load server auth session state (${status ?? "unknown"}).`, | 153 | cause: error, |
| 112 | status: status ?? 500, | 154 | fallbackMessage: (status) => `Failed to load server auth session state (${status}).`, |
| 113 | }); | 155 | }); |
| 114 | } | 156 | } |
| 115 | }); | 157 | }); |
| ... | @@ -183,11 +225,11 @@ async function exchangeBootstrapCredential(credential: string): Promise<AuthBrow | ... | @@ -183,11 +225,11 @@ async function exchangeBootstrapCredential(credential: string): Promise<AuthBrow |
| 183 | ), | 225 | ), |
| 184 | ); | 226 | ); |
| 185 | } catch (error) { | 227 | } catch (error) { |
| 186 | const status = readHttpApiStatus(error) ?? 500; | 228 | throw PrimaryEnvironmentRequestError.fromCause({ |
| 187 | const message = toFriendlyBootstrapErrorMessage(status, readHttpApiErrorMessage(error, "")); | 229 | operation: "exchange-bootstrap-credential", |
| 188 | throw new BootstrapHttpError({ | 230 | cause: error, |
| 189 | message: message || `Failed to bootstrap auth session (${status}).`, | 231 | fallbackMessage: (status) => `Failed to bootstrap auth session (${status}).`, |
| 190 | status, | 232 | formatDetail: (detail, status) => toFriendlyBootstrapErrorMessage(status, detail), |
| 191 | }); | 233 | }); |
| 192 | } | 234 | } |
| 193 | }); | 235 | }); |
| ... | @@ -240,7 +282,7 @@ function waitForBootstrapRetry(delayMs: number): Promise<void> { | ... | @@ -240,7 +282,7 @@ function waitForBootstrapRetry(delayMs: number): Promise<void> { |
| 240 | } | 282 | } |
| 241 | | 283 | |
| 242 | function isTransientBootstrapError(error: unknown): boolean { | 284 | function isTransientBootstrapError(error: unknown): boolean { |
| 243 | if (isBootstrapHttpError(error)) { | 285 | if (isPrimaryEnvironmentRequestError(error)) { |
| 244 | return TRANSIENT_BOOTSTRAP_STATUS_CODES.has(error.status); | 286 | return TRANSIENT_BOOTSTRAP_STATUS_CODES.has(error.status); |
| 245 | } | 287 | } |
| 246 | | 288 | |
| ... | @@ -310,13 +352,11 @@ export async function createServerPairingCredential(input?: { | ... | @@ -310,13 +352,11 @@ export async function createServerPairingCredential(input?: { |
| 310 | ), | 352 | ), |
| 311 | ); | 353 | ); |
| 312 | } catch (error) { | 354 | } catch (error) { |
| 313 | throw new Error( | 355 | throw PrimaryEnvironmentRequestError.fromCause({ |
| 314 | readHttpApiErrorMessage( | 356 | operation: "create-pairing-credential", |
| 315 | error, | 357 | cause: error, |
| 316 | `Failed to create pairing credential (${readHttpApiStatus(error) ?? "unknown"}).`, | 358 | fallbackMessage: (status) => `Failed to create pairing credential (${status}).`, |
| 317 | ), | 359 | }); |
| 318 | { cause: error }, | | |
| 319 | ); | | |
| 320 | } | 360 | } |
| 321 | } | 361 | } |
| 322 | | 362 | |
| ... | @@ -353,13 +393,11 @@ export async function listServerPairingLinks(): Promise<ReadonlyArray<ServerPair | ... | @@ -353,13 +393,11 @@ export async function listServerPairingLinks(): Promise<ReadonlyArray<ServerPair |
| 353 | }; | 393 | }; |
| 354 | }); | 394 | }); |
| 355 | } catch (error) { | 395 | } catch (error) { |
| 356 | throw new Error( | 396 | throw PrimaryEnvironmentRequestError.fromCause({ |
| 357 | readHttpApiErrorMessage( | 397 | operation: "list-pairing-links", |
| 358 | error, | 398 | cause: error, |
| 359 | `Failed to load pairing links (${readHttpApiStatus(error) ?? "unknown"}).`, | 399 | fallbackMessage: (status) => `Failed to load pairing links (${status}).`, |
| 360 | ), | 400 | }); |
| 361 | { cause: error }, | | |
| 362 | ); | | |
| 363 | } | 401 | } |
| 364 | } | 402 | } |
| 365 | | 403 | |
| ... | @@ -371,13 +409,12 @@ export async function revokeServerPairingLink(id: string): Promise<void> { | ... | @@ -371,13 +409,12 @@ export async function revokeServerPairingLink(id: string): Promise<void> { |
| 371 | ), | 409 | ), |
| 372 | ); | 410 | ); |
| 373 | } catch (error) { | 411 | } catch (error) { |
| 374 | throw new Error( | 412 | throw PrimaryEnvironmentRequestError.fromCause({ |
| 375 | readHttpApiErrorMessage( | 413 | operation: "revoke-pairing-link", |
| 376 | error, | 414 | pairingLinkId: id, |
| 377 | `Failed to revoke pairing link (${readHttpApiStatus(error) ?? "unknown"}).`, | 415 | cause: error, |
| 378 | ), | 416 | fallbackMessage: (status) => `Failed to revoke pairing link (${status}).`, |
| 379 | { cause: error }, | 417 | }); |
| 380 | ); | | |
| 381 | } | 418 | } |
| 382 | } | 419 | } |
| 383 | | 420 | |
| ... | @@ -406,13 +443,11 @@ export async function listServerClientSessions(): Promise< | ... | @@ -406,13 +443,11 @@ export async function listServerClientSessions(): Promise< |
| 406 | current: clientSession.current, | 443 | current: clientSession.current, |
| 407 | })); | 444 | })); |
| 408 | } catch (error) { | 445 | } catch (error) { |
| 409 | throw new Error( | 446 | throw PrimaryEnvironmentRequestError.fromCause({ |
| 410 | readHttpApiErrorMessage( | 447 | operation: "list-client-sessions", |
| 411 | error, | 448 | cause: error, |
| 412 | `Failed to load paired clients (${readHttpApiStatus(error) ?? "unknown"}).`, | 449 | fallbackMessage: (status) => `Failed to load paired clients (${status}).`, |
| 413 | ), | 450 | }); |
| 414 | { cause: error }, | | |
| 415 | ); | | |
| 416 | } | 451 | } |
| 417 | } | 452 | } |
| 418 | | 453 | |
| ... | @@ -426,13 +461,12 @@ export async function revokeServerClientSession(sessionId: AuthSessionId): Promi | ... | @@ -426,13 +461,12 @@ export async function revokeServerClientSession(sessionId: AuthSessionId): Promi |
| 426 | ), | 461 | ), |
| 427 | ); | 462 | ); |
| 428 | } catch (error) { | 463 | } catch (error) { |
| 429 | throw new Error( | 464 | throw PrimaryEnvironmentRequestError.fromCause({ |
| 430 | readHttpApiErrorMessage( | 465 | operation: "revoke-client-session", |
| 431 | error, | 466 | sessionId, |
| 432 | `Failed to revoke client session (${readHttpApiStatus(error) ?? "unknown"}).`, | 467 | cause: error, |
| 433 | ), | 468 | fallbackMessage: (status) => `Failed to revoke client session (${status}).`, |
| 434 | { cause: error }, | 469 | }); |
| 435 | ); | | |
| 436 | } | 470 | } |
| 437 | } | 471 | } |
| 438 | | 472 | |
| ... | @@ -445,13 +479,11 @@ export async function revokeOtherServerClientSessions(): Promise<number> { | ... | @@ -445,13 +479,11 @@ export async function revokeOtherServerClientSessions(): Promise<number> { |
| 445 | ); | 479 | ); |
| 446 | return result.revokedCount; | 480 | return result.revokedCount; |
| 447 | } catch (error) { | 481 | } catch (error) { |
| 448 | throw new Error( | 482 | throw PrimaryEnvironmentRequestError.fromCause({ |
| 449 | readHttpApiErrorMessage( | 483 | operation: "revoke-other-client-sessions", |
| 450 | error, | 484 | cause: error, |
| 451 | `Failed to revoke other client sessions (${readHttpApiStatus(error) ?? "unknown"}).`, | 485 | fallbackMessage: (status) => `Failed to revoke other client sessions (${status}).`, |
| 452 | ), | 486 | }); |
| 453 | { cause: error }, | | |
| 454 | ); | | |
| 455 | } | 487 | } |
| 456 | } | 488 | } |
| 457 | | 489 | |