| ... | ... | @@ -21,15 +21,57 @@ import { |
| 21 | 21 | |
| 22 | 22 | import { PrimaryEnvironmentHttpClient } from "./httpClient"; |
| 23 | 23 | import { runPrimaryHttp } from "../../lib/runtime"; |
| 24 | | import * as Data from "effect/Data"; |
| 25 | | import * as Predicate from "effect/Predicate"; |
| 26 | | |
| 27 | | export class BootstrapHttpError extends Data.TaggedError("BootstrapHttpError")<{ |
| 28 | | readonly message: string; |
| 29 | | readonly status: number; |
| 30 | | }> {} |
| 31 | | const isBootstrapHttpError = (u: unknown): u is BootstrapHttpError => |
| 32 | | Predicate.isTagged(u, "BootstrapHttpError"); |
| 24 | |
| 25 | const 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 | ]); |
| 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 | 75 | const isEnvironmentHttpCommonError = Schema.is(EnvironmentHttpCommonError); |
| 34 | 76 | |
| 35 | 77 | export interface ServerPairingLinkRecord { |
| ... | ... | @@ -106,10 +148,10 @@ export async function fetchSessionState(): Promise<AuthSessionState> { |
| 106 | 148 | ), |
| 107 | 149 | ); |
| 108 | 150 | } 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}).`, |
| 113 | 155 | }); |
| 114 | 156 | } |
| 115 | 157 | }); |
| ... | ... | @@ -183,11 +225,11 @@ async function exchangeBootstrapCredential(credential: string): Promise<AuthBrow |
| 183 | 225 | ), |
| 184 | 226 | ); |
| 185 | 227 | } 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), |
| 191 | 233 | }); |
| 192 | 234 | } |
| 193 | 235 | }); |
| ... | ... | @@ -240,7 +282,7 @@ function waitForBootstrapRetry(delayMs: number): Promise<void> { |
| 240 | 282 | } |
| 241 | 283 | |
| 242 | 284 | function isTransientBootstrapError(error: unknown): boolean { |
| 243 | | if (isBootstrapHttpError(error)) { |
| 285 | if (isPrimaryEnvironmentRequestError(error)) { |
| 244 | 286 | return TRANSIENT_BOOTSTRAP_STATUS_CODES.has(error.status); |
| 245 | 287 | } |
| 246 | 288 | |
| ... | ... | @@ -310,13 +352,11 @@ export async function createServerPairingCredential(input?: { |
| 310 | 352 | ), |
| 311 | 353 | ); |
| 312 | 354 | } 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 | }); |
| 320 | 360 | } |
| 321 | 361 | } |
| 322 | 362 | |
| ... | ... | @@ -353,13 +393,11 @@ export async function listServerPairingLinks(): Promise<ReadonlyArray<ServerPair |
| 353 | 393 | }; |
| 354 | 394 | }); |
| 355 | 395 | } 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 | }); |
| 363 | 401 | } |
| 364 | 402 | } |
| 365 | 403 | |
| ... | ... | @@ -371,13 +409,12 @@ export async function revokeServerPairingLink(id: string): Promise<void> { |
| 371 | 409 | ), |
| 372 | 410 | ); |
| 373 | 411 | } 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 | }); |
| 381 | 418 | } |
| 382 | 419 | } |
| 383 | 420 | |
| ... | ... | @@ -406,13 +443,11 @@ export async function listServerClientSessions(): Promise< |
| 406 | 443 | current: clientSession.current, |
| 407 | 444 | })); |
| 408 | 445 | } 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 | }); |
| 416 | 451 | } |
| 417 | 452 | } |
| 418 | 453 | |
| ... | ... | @@ -426,13 +461,12 @@ export async function revokeServerClientSession(sessionId: AuthSessionId): Promi |
| 426 | 461 | ), |
| 427 | 462 | ); |
| 428 | 463 | } 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 | }); |
| 436 | 470 | } |
| 437 | 471 | } |
| 438 | 472 | |
| ... | ... | @@ -445,13 +479,11 @@ export async function revokeOtherServerClientSessions(): Promise<number> { |
| 445 | 479 | ); |
| 446 | 480 | return result.revokedCount; |
| 447 | 481 | } 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 | }); |
| 455 | 487 | } |
| 456 | 488 | } |
| 457 | 489 | |