| ... | ... | @@ -7,15 +7,9 @@ |
| 7 | 7 | import * as NodeCrypto from "node:crypto"; |
| 8 | 8 | import { |
| 9 | 9 | AuthStandardClientScopes, |
| 10 | | EnvironmentCloudEndpointUnavailableError, |
| 11 | 10 | type EnvironmentCloudLinkStateResult, |
| 12 | 11 | type EnvironmentCloudPreferencesRequest, |
| 13 | 12 | type EnvironmentCloudRelayConfigResult, |
| 14 | | EnvironmentHttpBadRequestError, |
| 15 | | EnvironmentHttpConflictError, |
| 16 | | type EnvironmentHttpForbiddenError, |
| 17 | | EnvironmentHttpInternalServerError, |
| 18 | | EnvironmentHttpUnauthorizedError, |
| 19 | 13 | } from "@t3tools/contracts"; |
| 20 | 14 | import { |
| 21 | 15 | RelayCloudEnvironmentHealthProofPayload, |
| ... | ... | @@ -106,7 +100,12 @@ import { |
| 106 | 100 | import { getOrCreateEnvironmentKeyPairFromSecretStore } from "./environmentKeys.ts"; |
| 107 | 101 | import * as ManagedEndpointRuntime from "./ManagedEndpointRuntime.ts"; |
| 108 | 102 | import { relayUrlConfig } from "./publicConfig.ts"; |
| 109 | | import { filterRelayResponse, relayRequestError, shouldRetryCloudLink } from "./relayResponse.ts"; |
| 103 | import { |
| 104 | filterRelayResponse, |
| 105 | relayRequestError, |
| 106 | type RelayRequestError, |
| 107 | shouldRetryRelayRequest, |
| 108 | } from "./relayResponse.ts"; |
| 110 | 109 | import { |
| 111 | 110 | CLOUD_HEALTH_JTI_PREFIX, |
| 112 | 111 | CLOUD_HEALTH_NONCE_PREFIX, |
| ... | ... | @@ -126,15 +125,187 @@ import { |
| 126 | 125 | import { desktopUpdateRestartPending, pendingUpdateHandoffExists } from "./updateHandoff.ts"; |
| 127 | 126 | |
| 128 | 127 | const MANAGED_ENDPOINT_PROVISION_REQUEST_TIMEOUT = Duration.minutes(2); |
| 129 | | const failEnvironmentCloudInternalError = |
| 130 | | (message: string) => |
| 131 | | (cause: unknown): Effect.Effect<never, EnvironmentHttpInternalServerError> => |
| 132 | | Effect.logError(message, { cause }).pipe( |
| 133 | | Effect.flatMap(() => Effect.fail(new EnvironmentHttpInternalServerError({ message }))), |
| 134 | | ); |
| 128 | const RELAY_CONFIG_FIELD_MESSAGES = { |
| 129 | relayUrl: "Relay URL must be a secure absolute HTTPS URL.", |
| 130 | relayIssuer: "Relay issuer must be a secure absolute HTTPS URL.", |
| 131 | environmentCredential: "Relay environment credential is required.", |
| 132 | cloudUserId: "Cloud user id is required.", |
| 133 | cloudMintPublicKey: "Cloud mint public key must be a valid Ed25519 public key.", |
| 134 | } as const; |
| 135 | |
| 136 | /** The relay sent a link configuration this environment cannot install. */ |
| 137 | export class CloudLinkRelayConfigInvalidError extends Schema.TaggedError<CloudLinkRelayConfigInvalidError>()( |
| 138 | "CloudLinkRelayConfigInvalidError", |
| 139 | { |
| 140 | field: Schema.Literals([ |
| 141 | "relayUrl", |
| 142 | "relayIssuer", |
| 143 | "environmentCredential", |
| 144 | "cloudUserId", |
| 145 | "cloudMintPublicKey", |
| 146 | ]), |
| 147 | }, |
| 148 | ) { |
| 149 | override get message(): string { |
| 150 | return RELAY_CONFIG_FIELD_MESSAGES[this.field]; |
| 151 | } |
| 152 | } |
| 153 | |
| 154 | /** |
| 155 | * A link was asked to point at an origin it may not serve: a link proof request |
| 156 | * that did not reach this server directly on loopback (`endpoint`), or a local |
| 157 | * origin that is not a bare http(s) origin (`local`). |
| 158 | */ |
| 159 | export class CloudLinkOriginInvalidError extends Schema.TaggedError<CloudLinkOriginInvalidError>()( |
| 160 | "CloudLinkOriginInvalidError", |
| 161 | { origin: Schema.Literals(["endpoint", "local"]) }, |
| 162 | ) { |
| 163 | override get message(): string { |
| 164 | return this.origin === "endpoint" |
| 165 | ? "Invalid managed endpoint origin." |
| 166 | : "Could not resolve local environment origin."; |
| 167 | } |
| 168 | } |
| 169 | |
| 170 | export class CloudLinkNotLinkedError extends Schema.TaggedError<CloudLinkNotLinkedError>()( |
| 171 | "CloudLinkNotLinkedError", |
| 172 | {}, |
| 173 | ) { |
| 174 | override get message(): string { |
| 175 | return "Link this environment to T3 Connect first."; |
| 176 | } |
| 177 | } |
| 178 | |
| 179 | export class CloudLinkAccountMismatchError extends Schema.TaggedError<CloudLinkAccountMismatchError>()( |
| 180 | "CloudLinkAccountMismatchError", |
| 181 | {}, |
| 182 | ) { |
| 183 | override get message(): string { |
| 184 | return "This environment is already linked to a different cloud account. Unlink it before switching accounts."; |
| 185 | } |
| 186 | } |
| 187 | |
| 188 | /** Linking from the CLI needs the authorization `t3 connect link` stores. */ |
| 189 | export class CloudLinkAuthorizationMissingError extends Schema.TaggedError<CloudLinkAuthorizationMissingError>()( |
| 190 | "CloudLinkAuthorizationMissingError", |
| 191 | {}, |
| 192 | ) { |
| 193 | override get message(): string { |
| 194 | return "Run `t3 connect link` to authorize this environment."; |
| 195 | } |
| 196 | } |
| 197 | |
| 198 | const SignedRelayRequest = Schema.Literals(["health", "mint"]); |
| 135 | 199 | |
| 136 | | const failCloudCliTokenManagerError = (error: CliTokenManager.CloudCliTokenManagerError) => |
| 137 | | failEnvironmentCloudInternalError(error.message)(error); |
| 200 | /** A signed relay request whose proof does not verify for this environment. */ |
| 201 | export class CloudLinkProofRejectedError extends Schema.TaggedError<CloudLinkProofRejectedError>()( |
| 202 | "CloudLinkProofRejectedError", |
| 203 | { request: SignedRelayRequest }, |
| 204 | ) { |
| 205 | override get message(): string { |
| 206 | return `Invalid cloud ${this.request} request.`; |
| 207 | } |
| 208 | } |
| 209 | |
| 210 | /** A signed relay request whose proof was already used once. */ |
| 211 | export class CloudLinkProofReplayedError extends Schema.TaggedError<CloudLinkProofReplayedError>()( |
| 212 | "CloudLinkProofReplayedError", |
| 213 | { request: SignedRelayRequest }, |
| 214 | ) { |
| 215 | override get message(): string { |
| 216 | return `Cloud ${this.request} request was already consumed.`; |
| 217 | } |
| 218 | } |
| 219 | |
| 220 | /** The stored tunnel changed while its registration was in flight. */ |
| 221 | export class CloudLinkTunnelSupersededError extends Schema.TaggedError<CloudLinkTunnelSupersededError>()( |
| 222 | "CloudLinkTunnelSupersededError", |
| 223 | {}, |
| 224 | ) { |
| 225 | override get message(): string { |
| 226 | return "The managed tunnel configuration changed during registration."; |
| 227 | } |
| 228 | } |
| 229 | |
| 230 | /** |
| 231 | * The managed tunnel is not serving: its connector did not start |
| 232 | * (`runtime-not-started`, with the runtime's status), or the relay never |
| 233 | * confirmed this server's origin (`origin-unconfirmed`). |
| 234 | */ |
| 235 | export class CloudLinkEndpointUnavailableError extends Schema.TaggedError<CloudLinkEndpointUnavailableError>()( |
| 236 | "CloudLinkEndpointUnavailableError", |
| 237 | { |
| 238 | reason: Schema.Literals(["runtime-not-started", "origin-unconfirmed"]), |
| 239 | endpointRuntimeStatus: Schema.Unknown, |
| 240 | }, |
| 241 | ) { |
| 242 | override get message(): string { |
| 243 | return this.reason === "runtime-not-started" |
| 244 | ? "Managed endpoint runtime could not be started." |
| 245 | : "Managed endpoint origin could not be confirmed."; |
| 246 | } |
| 247 | } |
| 248 | |
| 249 | const INTERNAL_OPERATION_MESSAGES = { |
| 250 | "relay-url-unconfigured": |
| 251 | "T3CODE_RELAY_URL must be configured as a secure absolute HTTPS origin.", |
| 252 | "generate-link-proof": "Could not generate environment link proof.", |
| 253 | "persist-relay-config": "Could not persist environment relay configuration.", |
| 254 | "register-endpoint-origin": "Could not register the managed endpoint origin.", |
| 255 | "resolve-server-origin": "Could not resolve the local server origin.", |
| 256 | "persist-desired-link": "Could not persist desired T3 Connect link state.", |
| 257 | "sign-recovery-proof": "Could not sign the managed tunnel recovery request.", |
| 258 | "unsupported-recovered-tunnel": |
| 259 | "T3 Connect returned an unsupported managed tunnel configuration.", |
| 260 | "persist-recovered-tunnel": "Could not persist the recovered managed tunnel configuration.", |
| 261 | "read-relay-config": "Could not read environment relay configuration.", |
| 262 | "remove-relay-config": "Could not remove environment relay configuration.", |
| 263 | "update-webhook-settings": "Could not update T3 Connect webhook settings.", |
| 264 | "read-preferences": "Could not read environment cloud preferences.", |
| 265 | "persist-preferences": "Could not persist environment cloud preferences.", |
| 266 | "answer-health": "Could not answer cloud health request.", |
| 267 | "issue-credential": "Could not issue cloud connection credential.", |
| 268 | } as const; |
| 269 | |
| 270 | /** A link step failed on this machine: storage, signing, or an unexpected relay answer. */ |
| 271 | export class CloudLinkInternalError extends Schema.TaggedError<CloudLinkInternalError>()( |
| 272 | "CloudLinkInternalError", |
| 273 | { |
| 274 | operation: Schema.Literals( |
| 275 | Object.keys(INTERNAL_OPERATION_MESSAGES) as [ |
| 276 | keyof typeof INTERNAL_OPERATION_MESSAGES, |
| 277 | ...Array<keyof typeof INTERNAL_OPERATION_MESSAGES>, |
| 278 | ], |
| 279 | ), |
| 280 | cause: Schema.optional(Schema.Defect()), |
| 281 | }, |
| 282 | ) { |
| 283 | override get message(): string { |
| 284 | return INTERNAL_OPERATION_MESSAGES[this.operation]; |
| 285 | } |
| 286 | } |
| 287 | |
| 288 | const internalError = |
| 289 | (operation: CloudLinkInternalError["operation"]) => |
| 290 | (cause: unknown): Effect.Effect<never, CloudLinkInternalError> => |
| 291 | Effect.fail(new CloudLinkInternalError({ operation, cause })); |
| 292 | |
| 293 | const isPermanentLinkError = Schema.is( |
| 294 | Schema.Union([ |
| 295 | CloudLinkRelayConfigInvalidError, |
| 296 | CloudLinkOriginInvalidError, |
| 297 | CloudLinkNotLinkedError, |
| 298 | CloudLinkAccountMismatchError, |
| 299 | CloudLinkAuthorizationMissingError, |
| 300 | CloudLinkProofRejectedError, |
| 301 | CloudLinkProofReplayedError, |
| 302 | CloudLinkTunnelSupersededError, |
| 303 | ]), |
| 304 | ); |
| 305 | |
| 306 | /** Whether a failed link step may succeed on retry: not when the relay or this server refused it. */ |
| 307 | export const shouldRetryCloudLink = (error: unknown): boolean => |
| 308 | shouldRetryRelayRequest(error) && !isPermanentLinkError(error); |
| 138 | 309 | |
| 139 | 310 | /** A failed rollback leaves a setting changed; it is logged, not hidden. */ |
| 140 | 311 | const rollbackFailed = (cause: unknown) => |
| ... | ... | @@ -142,10 +313,7 @@ const rollbackFailed = (cause: unknown) => |
| 142 | 313 | |
| 143 | 314 | const requireRelayUrl = relayUrlConfig.pipe( |
| 144 | 315 | Effect.mapError( |
| 145 | | () => |
| 146 | | new EnvironmentHttpInternalServerError({ |
| 147 | | message: "T3CODE_RELAY_URL must be configured as a secure absolute HTTPS origin.", |
| 148 | | }), |
| 316 | (cause) => new CloudLinkInternalError({ operation: "relay-url-unconfigured", cause }), |
| 149 | 317 | ), |
| 150 | 318 | ); |
| 151 | 319 | |
| ... | ... | @@ -159,58 +327,37 @@ function stringToBytes(value: string): Uint8Array { |
| 159 | 327 | |
| 160 | 328 | function validateCloudMintPublicKey( |
| 161 | 329 | publicKey: string, |
| 162 | | ): Effect.Effect<void, EnvironmentHttpBadRequestError> { |
| 330 | ): Effect.Effect<void, CloudLinkRelayConfigInvalidError> { |
| 331 | const invalid = new CloudLinkRelayConfigInvalidError({ field: "cloudMintPublicKey" }); |
| 163 | 332 | return Effect.try({ |
| 164 | 333 | try: () => NodeCrypto.createPublicKey(publicKey.replace(/\\n/g, "\n")), |
| 165 | | catch: () => |
| 166 | | new EnvironmentHttpBadRequestError({ |
| 167 | | message: "Cloud mint public key must be a valid Ed25519 public key.", |
| 168 | | }), |
| 334 | catch: () => invalid, |
| 169 | 335 | }).pipe( |
| 170 | 336 | Effect.flatMap((key) => |
| 171 | | key.asymmetricKeyType === "ed25519" |
| 172 | | ? Effect.void |
| 173 | | : Effect.fail( |
| 174 | | new EnvironmentHttpBadRequestError({ |
| 175 | | message: "Cloud mint public key must be a valid Ed25519 public key.", |
| 176 | | }), |
| 177 | | ), |
| 337 | key.asymmetricKeyType === "ed25519" ? Effect.void : Effect.fail(invalid), |
| 178 | 338 | ), |
| 179 | 339 | ); |
| 180 | 340 | } |
| 181 | 341 | |
| 182 | | function validateRelayConfigPayload( |
| 342 | function invalidRelayConfigField( |
| 183 | 343 | payload: RelayEnvironmentConfigRequest, |
| 184 | | ): Effect.Effect<void, EnvironmentHttpBadRequestError> { |
| 185 | | if (!isSecureRelayUrl(payload.relayUrl)) { |
| 186 | | return Effect.fail( |
| 187 | | new EnvironmentHttpBadRequestError({ |
| 188 | | message: "Relay URL must be a secure absolute HTTPS URL.", |
| 189 | | }), |
| 190 | | ); |
| 191 | | } |
| 344 | ): CloudLinkRelayConfigInvalidError["field"] | null { |
| 345 | if (!isSecureRelayUrl(payload.relayUrl)) return "relayUrl"; |
| 192 | 346 | if (payload.relayIssuer !== undefined && !isSecureRelayUrl(payload.relayIssuer)) { |
| 193 | | return Effect.fail( |
| 194 | | new EnvironmentHttpBadRequestError({ |
| 195 | | message: "Relay issuer must be a secure absolute HTTPS URL.", |
| 196 | | }), |
| 197 | | ); |
| 198 | | } |
| 199 | | if (payload.environmentCredential.trim().length === 0) { |
| 200 | | return Effect.fail( |
| 201 | | new EnvironmentHttpBadRequestError({ |
| 202 | | message: "Relay environment credential is required.", |
| 203 | | }), |
| 204 | | ); |
| 347 | return "relayIssuer"; |
| 205 | 348 | } |
| 206 | | if (payload.cloudUserId.trim().length === 0) { |
| 207 | | return Effect.fail( |
| 208 | | new EnvironmentHttpBadRequestError({ |
| 209 | | message: "Cloud user id is required.", |
| 210 | | }), |
| 211 | | ); |
| 212 | | } |
| 213 | | return Effect.void; |
| 349 | if (payload.environmentCredential.trim().length === 0) return "environmentCredential"; |
| 350 | if (payload.cloudUserId.trim().length === 0) return "cloudUserId"; |
| 351 | return null; |
| 352 | } |
| 353 | |
| 354 | function validateRelayConfigPayload( |
| 355 | payload: RelayEnvironmentConfigRequest, |
| 356 | ): Effect.Effect<void, CloudLinkRelayConfigInvalidError> { |
| 357 | const field = invalidRelayConfigField(payload); |
| 358 | return field === null |
| 359 | ? Effect.void |
| 360 | : Effect.fail(new CloudLinkRelayConfigInvalidError({ field })); |
| 214 | 361 | } |
| 215 | 362 | |
| 216 | 363 | function normalizePemForSignedPayload(value: string): string { |
| ... | ... | @@ -233,13 +380,25 @@ type ManagedTunnelRecoveryProofInput = { |
| 233 | 380 | | { readonly action: "recover"; readonly origin: RelayManagedEndpointOrigin } |
| 234 | 381 | ); |
| 235 | 382 | |
| 236 | | /** Failures of the link work that runs outside a request: startup, recovery and shutdown. */ |
| 383 | /** Every failure CloudLink constructs itself. */ |
| 384 | export type CloudLinkError = |
| 385 | | CloudLinkAccountMismatchError |
| 386 | | CloudLinkAuthorizationMissingError |
| 387 | | CloudLinkEndpointUnavailableError |
| 388 | | CloudLinkInternalError |
| 389 | | CloudLinkNotLinkedError |
| 390 | | CloudLinkOriginInvalidError |
| 391 | | CloudLinkProofRejectedError |
| 392 | | CloudLinkProofReplayedError |
| 393 | | CloudLinkRelayConfigInvalidError |
| 394 | | CloudLinkTunnelSupersededError; |
| 395 | |
| 396 | /** Failures of the link work that talks to the relay: linking, recovery and shutdown. */ |
| 237 | 397 | type CloudLinkBackgroundError = |
| 238 | | | EnvironmentCloudEndpointUnavailableError |
| 239 | | | EnvironmentHttpBadRequestError |
| 240 | | | EnvironmentHttpForbiddenError |
| 241 | | | EnvironmentHttpInternalServerError |
| 242 | | | EnvironmentHttpUnauthorizedError |
| 398 | | CloudLinkEndpointUnavailableError |
| 399 | | CloudLinkInternalError |
| 400 | | CloudLinkOriginInvalidError |
| 401 | | RelayRequestError |
| 243 | 402 | | ServerSecretStore.SecretStoreError |
| 244 | 403 | | Schema.SchemaError |
| 245 | 404 | | PlatformError.PlatformError; |
| ... | ... | @@ -264,29 +423,28 @@ export class CloudLink extends Context.Service< |
| 264 | 423 | httpRequest: HttpServerRequest.HttpServerRequest, |
| 265 | 424 | ) => Effect.Effect< |
| 266 | 425 | RelayEnvironmentLinkProof, |
| 267 | | EnvironmentHttpBadRequestError | EnvironmentHttpInternalServerError |
| 426 | CloudLinkOriginInvalidError | CloudLinkInternalError | EnvironmentAuth.ServerAuthInternalError |
| 268 | 427 | >; |
| 269 | 428 | /** Installs the link the relay returned and, for a managed tunnel, confirms its origin. */ |
| 270 | 429 | readonly applyRelayConfig: ( |
| 271 | 430 | payload: RelayEnvironmentConfigRequest, |
| 272 | 431 | ) => Effect.Effect< |
| 273 | 432 | EnvironmentCloudRelayConfigResult, |
| 274 | | | EnvironmentCloudEndpointUnavailableError |
| 275 | | | EnvironmentHttpBadRequestError |
| 276 | | | EnvironmentHttpConflictError |
| 277 | | | EnvironmentHttpForbiddenError |
| 278 | | | EnvironmentHttpInternalServerError |
| 279 | | | EnvironmentHttpUnauthorizedError |
| 433 | | CloudLinkAccountMismatchError |
| 434 | | CloudLinkEndpointUnavailableError |
| 435 | | CloudLinkInternalError |
| 436 | | CloudLinkOriginInvalidError |
| 437 | | CloudLinkRelayConfigInvalidError |
| 438 | | CloudLinkTunnelSupersededError |
| 439 | | EnvironmentAuth.ServerAuthInternalError |
| 440 | | RelayRequestError |
| 280 | 441 | >; |
| 281 | 442 | readonly linkState: () => Effect.Effect< |
| 282 | 443 | EnvironmentCloudLinkStateResult, |
| 283 | | EnvironmentHttpInternalServerError |
| 444 | CloudLinkInternalError |
| 284 | 445 | >; |
| 285 | 446 | /** Stops the tunnel and forgets the link, including the CLI's wish to keep it. */ |
| 286 | | readonly unlink: () => Effect.Effect< |
| 287 | | EnvironmentCloudRelayConfigResult, |
| 288 | | EnvironmentHttpInternalServerError |
| 289 | | >; |
| 447 | readonly unlink: () => Effect.Effect<EnvironmentCloudRelayConfigResult, CloudLinkInternalError>; |
| 290 | 448 | /** |
| 291 | 449 | * Saves this environment's T3 Connect preferences, all or nothing, and |
| 292 | 450 | * returns the link state. The activity setting is saved first. Holding |
| ... | ... | @@ -298,25 +456,27 @@ export class CloudLink extends Context.Service< |
| 298 | 456 | input: EnvironmentCloudPreferencesRequest, |
| 299 | 457 | ) => Effect.Effect< |
| 300 | 458 | EnvironmentCloudLinkStateResult, |
| 301 | | EnvironmentHttpBadRequestError | EnvironmentHttpInternalServerError |
| 459 | CloudLinkNotLinkedError | CloudLinkInternalError |
| 302 | 460 | >; |
| 303 | 461 | /** Answers the relay's signed health check once per proof. */ |
| 304 | 462 | readonly answerHealthRequest: ( |
| 305 | 463 | request: RelayCloudEnvironmentHealthRequest, |
| 306 | 464 | ) => Effect.Effect< |
| 307 | 465 | RelayEnvironmentHealthResponse, |
| 308 | | | EnvironmentHttpConflictError |
| 309 | | | EnvironmentHttpInternalServerError |
| 310 | | | EnvironmentHttpUnauthorizedError |
| 466 | | CloudLinkProofRejectedError |
| 467 | | CloudLinkProofReplayedError |
| 468 | | CloudLinkInternalError |
| 469 | | EnvironmentAuth.ServerAuthInternalError |
| 311 | 470 | >; |
| 312 | 471 | /** Issues a short-lived pairing credential for a client the relay vouched for, once per proof. */ |
| 313 | 472 | readonly mintCredential: ( |
| 314 | 473 | request: RelayCloudMintCredentialRequest, |
| 315 | 474 | ) => Effect.Effect< |
| 316 | 475 | RelayEnvironmentMintResponse, |
| 317 | | | EnvironmentHttpConflictError |
| 318 | | | EnvironmentHttpInternalServerError |
| 319 | | | EnvironmentHttpUnauthorizedError |
| 476 | | CloudLinkProofRejectedError |
| 477 | | CloudLinkProofReplayedError |
| 478 | | CloudLinkInternalError |
| 479 | | EnvironmentAuth.ServerAuthInternalError |
| 320 | 480 | >; |
| 321 | 481 | /** Links this environment with the stored CLI authorization and records the CLI's wish. */ |
| 322 | 482 | readonly reconcileDesiredLink: ( |
| ... | ... | @@ -324,9 +484,11 @@ export class CloudLink extends Context.Service< |
| 324 | 484 | ) => Effect.Effect< |
| 325 | 485 | CliDesiredLinkMode, |
| 326 | 486 | | CloudLinkBackgroundError |
| 327 | | | EnvironmentHttpConflictError |
| 487 | | CloudLinkAccountMismatchError |
| 488 | | CloudLinkAuthorizationMissingError |
| 489 | | CloudLinkRelayConfigInvalidError |
| 328 | 490 | | EnvironmentAuth.ServerAuthInternalError |
| 329 | | | CliTokenManager.CloudCliAuthorizationDeniedError |
| 491 | | CliTokenManager.CloudCliTokenManagerError |
| 330 | 492 | >; |
| 331 | 493 | /** As `reconcileDesiredLink`, but returns null when the CLI no longer wants a link. */ |
| 332 | 494 | readonly reconcileDesiredLinkIfStillDesired: ( |
| ... | ... | @@ -334,9 +496,11 @@ export class CloudLink extends Context.Service< |
| 334 | 496 | ) => Effect.Effect< |
| 335 | 497 | CliDesiredLinkMode | null, |
| 336 | 498 | | CloudLinkBackgroundError |
| 337 | | | EnvironmentHttpConflictError |
| 499 | | CloudLinkAccountMismatchError |
| 500 | | CloudLinkAuthorizationMissingError |
| 501 | | CloudLinkRelayConfigInvalidError |
| 338 | 502 | | EnvironmentAuth.ServerAuthInternalError |
| 339 | | | CliTokenManager.CloudCliAuthorizationDeniedError |
| 503 | | CliTokenManager.CloudCliTokenManagerError |
| 340 | 504 | >; |
| 341 | 505 | /** Tells the relay which local origin the stored tunnel serves, then starts it. */ |
| 342 | 506 | readonly registerManagedTunnelRecovery: ( |
| ... | ... | @@ -359,8 +523,8 @@ export class CloudLink extends Context.Service< |
| 359 | 523 | options?: { readonly requireConfirmedOrigin?: boolean }, |
| 360 | 524 | ) => Effect.Effect< |
| 361 | 525 | boolean, |
| 362 | | | EnvironmentCloudEndpointUnavailableError |
| 363 | | | EnvironmentHttpBadRequestError |
| 526 | | CloudLinkEndpointUnavailableError |
| 527 | | CloudLinkOriginInvalidError |
| 364 | 528 | | ServerSecretStore.SecretStoreError |
| 365 | 529 | >; |
| 366 | 530 | /** Deletes a CLI-managed tunnel when this server goes offline for good. */ |
| ... | ... | @@ -399,7 +563,7 @@ const make = Effect.gen(function* () { |
| 399 | 563 | |
| 400 | 564 | const validateLinkedCloudUser = ( |
| 401 | 565 | cloudUserId: string, |
| 402 | | ): Effect.Effect<void, EnvironmentAuth.ServerAuthInternalError | EnvironmentHttpConflictError> => |
| 566 | ): Effect.Effect<void, EnvironmentAuth.ServerAuthInternalError | CloudLinkAccountMismatchError> => |
| 403 | 567 | secrets.get(CLOUD_LINKED_USER_ID).pipe( |
| 404 | 568 | Effect.mapError( |
| 405 | 569 | (cause) => |
| ... | ... | @@ -414,12 +578,7 @@ const make = Effect.gen(function* () { |
| 414 | 578 | const existingCloudUserId = bytesToString(existing.value); |
| 415 | 579 | return existingCloudUserId === cloudUserId |
| 416 | 580 | ? Effect.void |
| 417 | | : Effect.fail( |
| 418 | | new EnvironmentHttpConflictError({ |
| 419 | | message: |
| 420 | | "This environment is already linked to a different cloud account. Unlink it before switching accounts.", |
| 421 | | }), |
| 422 | | ); |
| 581 | : Effect.fail(new CloudLinkAccountMismatchError({})); |
| 423 | 582 | }), |
| 424 | 583 | ); |
| 425 | 584 | |
| ... | ... | @@ -450,9 +609,7 @@ const make = Effect.gen(function* () { |
| 450 | 609 | requestUrl, |
| 451 | 610 | }) |
| 452 | 611 | ) { |
| 453 | | return yield* new EnvironmentHttpBadRequestError({ |
| 454 | | message: "Invalid managed endpoint origin.", |
| 455 | | }); |
| 612 | return yield* new CloudLinkOriginInvalidError({ origin: "endpoint" }); |
| 456 | 613 | } |
| 457 | 614 | const now = yield* DateTime.now; |
| 458 | 615 | const expiresAt = DateTime.add(now, { minutes: 5 }); |
| ... | ... | @@ -491,24 +648,13 @@ const make = Effect.gen(function* () { |
| 491 | 648 | function* (request: RelayLinkProofRequest, httpRequest: HttpServerRequest.HttpServerRequest) { |
| 492 | 649 | const requestUrl = requestAbsoluteUrl(httpRequest); |
| 493 | 650 | if (requestUrl === null || hasForwardedAuthorityHeaders(httpRequest)) { |
| 494 | | return yield* new EnvironmentHttpBadRequestError({ |
| 495 | | message: "Invalid managed endpoint origin.", |
| 496 | | }); |
| 651 | return yield* new CloudLinkOriginInvalidError({ origin: "endpoint" }); |
| 497 | 652 | } |
| 498 | 653 | const proof = yield* makeCloudLinkProof(request, requestUrl); |
| 499 | 654 | return proof satisfies RelayEnvironmentLinkProof; |
| 500 | 655 | }, |
| 501 | | Effect.catchIf(EnvironmentAuth.isServerAuthInternalError, (error) => |
| 502 | | failEnvironmentCloudInternalError(error.message)(error), |
| 503 | | ), |
| 504 | | Effect.catchIf( |
| 505 | | ServerSecretStore.isSecretStoreError, |
| 506 | | failEnvironmentCloudInternalError("Could not generate environment link proof."), |
| 507 | | ), |
| 508 | | Effect.catchTag( |
| 509 | | "PlatformError", |
| 510 | | failEnvironmentCloudInternalError("Could not generate environment link proof."), |
| 511 | | ), |
| 656 | Effect.catchIf(ServerSecretStore.isSecretStoreError, internalError("generate-link-proof")), |
| 657 | Effect.catchTag("PlatformError", internalError("generate-link-proof")), |
| 512 | 658 | ); |
| 513 | 659 | |
| 514 | 660 | const activateManagedTunnel = Effect.fn("environment.cloud.activateManagedTunnel")( |
| ... | ... | @@ -528,8 +674,8 @@ const make = Effect.gen(function* () { |
| 528 | 674 | } |
| 529 | 675 | const status = yield* endpointRuntime.applyConfig(input.config); |
| 530 | 676 | if (status.status !== "running") { |
| 531 | | return yield* new EnvironmentCloudEndpointUnavailableError({ |
| 532 | | message: "Managed endpoint runtime could not be started.", |
| 677 | return yield* new CloudLinkEndpointUnavailableError({ |
| 678 | reason: "runtime-not-started", |
| 533 | 679 | endpointRuntimeStatus: status, |
| 534 | 680 | }); |
| 535 | 681 | } |
| ... | ... | @@ -557,7 +703,7 @@ const make = Effect.gen(function* () { |
| 557 | 703 | ? activate.pipe( |
| 558 | 704 | Effect.retry({ |
| 559 | 705 | while: (error) => |
| 560 | | error._tag === "EnvironmentCloudEndpointUnavailableError" && |
| 706 | error._tag === "CloudLinkEndpointUnavailableError" && |
| 561 | 707 | ManagedEndpointRuntime.isRetryableManagedEndpointRuntimeStatus( |
| 562 | 708 | error.endpointRuntimeStatus, |
| 563 | 709 | ), |
| ... | ... | @@ -578,10 +724,7 @@ const make = Effect.gen(function* () { |
| 578 | 724 | const requireConfirmedOrigin = options?.requireConfirmedOrigin ?? true; |
| 579 | 725 | const parsedOrigin = yield* Effect.try({ |
| 580 | 726 | try: () => parseManagedEndpointLocalOrigin(localOrigin), |
| 581 | | catch: () => |
| 582 | | new EnvironmentHttpBadRequestError({ |
| 583 | | message: "Could not resolve local environment origin.", |
| 584 | | }), |
| 727 | catch: () => new CloudLinkOriginInvalidError({ origin: "local" }), |
| 585 | 728 | }); |
| 586 | 729 | return yield* endpointRuntime.withLinkStateLock( |
| 587 | 730 | Effect.gen(function* () { |
| ... | ... | @@ -610,8 +753,8 @@ const make = Effect.gen(function* () { |
| 610 | 753 | } |
| 611 | 754 | const status = yield* endpointRuntime.applyConfig(config); |
| 612 | 755 | if (status.status !== "running") { |
| 613 | | return yield* new EnvironmentCloudEndpointUnavailableError({ |
| 614 | | message: "Managed endpoint runtime could not be started.", |
| 756 | return yield* new CloudLinkEndpointUnavailableError({ |
| 757 | reason: "runtime-not-started", |
| 615 | 758 | endpointRuntimeStatus: status, |
| 616 | 759 | }); |
| 617 | 760 | } |
| ... | ... | @@ -637,8 +780,8 @@ const make = Effect.gen(function* () { |
| 637 | 780 | payload.endpointRuntime !== null && |
| 638 | 781 | payload.endpointRuntime.providerKind !== "cloudflare_tunnel" |
| 639 | 782 | ) { |
| 640 | | return yield* new EnvironmentCloudEndpointUnavailableError({ |
| 641 | | message: "Managed endpoint runtime could not be started.", |
| 783 | return yield* new CloudLinkEndpointUnavailableError({ |
| 784 | reason: "runtime-not-started", |
| 642 | 785 | endpointRuntimeStatus: { |
| 643 | 786 | status: "unsupported", |
| 644 | 787 | providerKind: payload.endpointRuntime.providerKind, |
| ... | ... | @@ -674,8 +817,8 @@ const make = Effect.gen(function* () { |
| 674 | 817 | } |
| 675 | 818 | const endpointRuntimeStatus = yield* endpointRuntime.applyConfig(payload.endpointRuntime); |
| 676 | 819 | if (endpointRuntimeStatus.status !== "running") { |
| 677 | | return yield* new EnvironmentCloudEndpointUnavailableError({ |
| 678 | | message: "Managed endpoint runtime could not be started.", |
| 820 | return yield* new CloudLinkEndpointUnavailableError({ |
| 821 | reason: "runtime-not-started", |
| 679 | 822 | endpointRuntimeStatus, |
| 680 | 823 | }); |
| 681 | 824 | } |
| ... | ... | @@ -711,20 +854,12 @@ const make = Effect.gen(function* () { |
| 711 | 854 | function* (localOrigin: string) { |
| 712 | 855 | const parsedOrigin = yield* Effect.try({ |
| 713 | 856 | try: () => parseManagedEndpointLocalOrigin(localOrigin), |
| 714 | | catch: () => |
| 715 | | new EnvironmentHttpBadRequestError({ |
| 716 | | message: "Could not resolve local environment origin.", |
| 717 | | }), |
| 857 | catch: () => new CloudLinkOriginInvalidError({ origin: "local" }), |
| 718 | 858 | }); |
| 719 | 859 | const token = yield* cliTokenManager.getExisting.pipe( |
| 720 | 860 | Effect.flatMap( |
| 721 | 861 | Option.match({ |
| 722 | | onNone: () => |
| 723 | | Effect.fail( |
| 724 | | new EnvironmentHttpUnauthorizedError({ |
| 725 | | message: "Run `t3 connect link` to authorize this environment.", |
| 726 | | }), |
| 727 | | ), |
| 862 | onNone: () => Effect.fail(new CloudLinkAuthorizationMissingError({})), |
| 728 | 863 | onSome: Effect.succeed, |
| 729 | 864 | }), |
| 730 | 865 | ), |
| ... | ... | @@ -786,17 +921,7 @@ const make = Effect.gen(function* () { |
| 786 | 921 | // actually used, not from a value read before the relay round trip. |
| 787 | 922 | return mode; |
| 788 | 923 | }, |
| 789 | | Effect.catchIf( |
| 790 | | ServerSecretStore.isSecretStoreError, |
| 791 | | failEnvironmentCloudInternalError("Could not persist desired T3 Connect link state."), |
| 792 | | ), |
| 793 | | Effect.catchTags({ |
| 794 | | CloudCliCredentialRemovalError: failCloudCliTokenManagerError, |
| 795 | | CloudCliCredentialRefreshError: failCloudCliTokenManagerError, |
| 796 | | CloudCliCredentialReadError: failCloudCliTokenManagerError, |
| 797 | | CloudCliAuthorizationError: failCloudCliTokenManagerError, |
| 798 | | CloudCliAuthorizationTimeoutError: failCloudCliTokenManagerError, |
| 799 | | }), |
| 924 | Effect.catchIf(ServerSecretStore.isSecretStoreError, internalError("persist-desired-link")), |
| 800 | 925 | ); |
| 801 | 926 | |
| 802 | 927 | const reconcileDesiredLink = Effect.fn("environment.cloud.reconcileDesiredLink")(function* ( |
| ... | ... | @@ -853,10 +978,7 @@ const make = Effect.gen(function* () { |
| 853 | 978 | payload, |
| 854 | 979 | }).pipe( |
| 855 | 980 | Effect.mapError( |
| 856 | | () => |
| 857 | | new EnvironmentHttpInternalServerError({ |
| 858 | | message: "Could not sign the managed tunnel recovery request.", |
| 859 | | }), |
| 981 | (cause) => new CloudLinkInternalError({ operation: "sign-recovery-proof", cause }), |
| 860 | 982 | ), |
| 861 | 983 | ); |
| 862 | 984 | }); |
| ... | ... | @@ -886,10 +1008,7 @@ const make = Effect.gen(function* () { |
| 886 | 1008 | |
| 887 | 1009 | const parsedOrigin = yield* Effect.try({ |
| 888 | 1010 | try: () => parseManagedEndpointLocalOrigin(localOrigin), |
| 889 | | catch: () => |
| 890 | | new EnvironmentHttpBadRequestError({ |
| 891 | | message: "Could not resolve local environment origin.", |
| 892 | | }), |
| 1011 | catch: () => new CloudLinkOriginInvalidError({ origin: "local" }), |
| 893 | 1012 | }); |
| 894 | 1013 | if (config.tunnelId === undefined) { |
| 895 | 1014 | return { status: "recovery_required" as const, config }; |
| ... | ... | @@ -967,10 +1086,7 @@ const make = Effect.gen(function* () { |
| 967 | 1086 | |
| 968 | 1087 | const parsedOrigin = yield* Effect.try({ |
| 969 | 1088 | try: () => parseManagedEndpointLocalOrigin(localOrigin), |
| 970 | | catch: () => |
| 971 | | new EnvironmentHttpBadRequestError({ |
| 972 | | message: "Could not resolve local environment origin.", |
| 973 | | }), |
| 1089 | catch: () => new CloudLinkOriginInvalidError({ origin: "local" }), |
| 974 | 1090 | }); |
| 975 | 1091 | |
| 976 | 1092 | const environmentId = yield* environment.getEnvironmentId; |
| ... | ... | @@ -996,17 +1112,12 @@ const make = Effect.gen(function* () { |
| 996 | 1112 | timeout: MANAGED_ENDPOINT_PROVISION_REQUEST_TIMEOUT, |
| 997 | 1113 | }); |
| 998 | 1114 | if (recovered.endpointRuntime.providerKind !== "cloudflare_tunnel") { |
| 999 | | return yield* new EnvironmentHttpInternalServerError({ |
| 1000 | | message: "T3 Connect returned an unsupported managed tunnel configuration.", |
| 1001 | | }); |
| 1115 | return yield* new CloudLinkInternalError({ operation: "unsupported-recovered-tunnel" }); |
| 1002 | 1116 | } |
| 1003 | 1117 | |
| 1004 | 1118 | const encoded = yield* encodeEndpointRuntimeConfigJson(recovered.endpointRuntime).pipe( |
| 1005 | 1119 | Effect.mapError( |
| 1006 | | () => |
| 1007 | | new EnvironmentHttpInternalServerError({ |
| 1008 | | message: "Could not persist the recovered managed tunnel configuration.", |
| 1009 | | }), |
| 1120 | (cause) => new CloudLinkInternalError({ operation: "persist-recovered-tunnel", cause }), |
| 1010 | 1121 | ), |
| 1011 | 1122 | ); |
| 1012 | 1123 | const stored = yield* endpointRuntime.withLinkStateLock( |
| ... | ... | @@ -1041,9 +1152,7 @@ const make = Effect.gen(function* () { |
| 1041 | 1152 | if (payload.endpointRuntime?.providerKind === "cloudflare_tunnel") { |
| 1042 | 1153 | const address = httpServer.address; |
| 1043 | 1154 | if (typeof address === "string" || !("port" in address)) { |
| 1044 | | return yield* new EnvironmentHttpInternalServerError({ |
| 1045 | | message: "Could not resolve the local server origin.", |
| 1046 | | }); |
| 1155 | return yield* new CloudLinkInternalError({ operation: "resolve-server-origin" }); |
| 1047 | 1156 | } |
| 1048 | 1157 | const registration = yield* registerManagedTunnelRecovery( |
| 1049 | 1158 | `http://127.0.0.1:${address.port}`, |
| ... | ... | @@ -1051,21 +1160,18 @@ const make = Effect.gen(function* () { |
| 1051 | 1160 | Effect.retry({ |
| 1052 | 1161 | times: 2, |
| 1053 | 1162 | while: (error) => |
| 1054 | | shouldRetryCloudLink(error) && |
| 1055 | | error._tag !== "EnvironmentCloudEndpointUnavailableError", |
| 1163 | shouldRetryCloudLink(error) && error._tag !== "CloudLinkEndpointUnavailableError", |
| 1056 | 1164 | }), |
| 1057 | 1165 | ); |
| 1058 | 1166 | if (registration.status === "superseded") { |
| 1059 | | return yield* new EnvironmentHttpConflictError({ |
| 1060 | | message: "The managed tunnel configuration changed during registration.", |
| 1061 | | }); |
| 1167 | return yield* new CloudLinkTunnelSupersededError({}); |
| 1062 | 1168 | } |
| 1063 | 1169 | if (registration.status === "recovery_required") { |
| 1064 | 1170 | yield* endpointRuntime.requestRecovery(registration.config); |
| 1065 | 1171 | } |
| 1066 | 1172 | if (registration.status !== "ready") { |
| 1067 | | return yield* new EnvironmentCloudEndpointUnavailableError({ |
| 1068 | | message: "Managed endpoint origin could not be confirmed.", |
| 1173 | return yield* new CloudLinkEndpointUnavailableError({ |
| 1174 | reason: "origin-unconfirmed", |
| 1069 | 1175 | endpointRuntimeStatus: { status: "disabled" }, |
| 1070 | 1176 | }); |
| 1071 | 1177 | } |
| ... | ... | @@ -1076,20 +1182,10 @@ const make = Effect.gen(function* () { |
| 1076 | 1182 | } |
| 1077 | 1183 | return result; |
| 1078 | 1184 | }, |
| 1079 | | Effect.catchIf(EnvironmentAuth.isServerAuthInternalError, (error) => |
| 1080 | | failEnvironmentCloudInternalError(error.message)(error), |
| 1081 | | ), |
| 1082 | | Effect.catchIf( |
| 1083 | | ServerSecretStore.isSecretStoreError, |
| 1084 | | failEnvironmentCloudInternalError("Could not persist environment relay configuration."), |
| 1085 | | ), |
| 1185 | Effect.catchIf(ServerSecretStore.isSecretStoreError, internalError("persist-relay-config")), |
| 1086 | 1186 | Effect.catchTags({ |
| 1087 | | SchemaError: failEnvironmentCloudInternalError( |
| 1088 | | "Could not persist environment relay configuration.", |
| 1089 | | ), |
| 1090 | | PlatformError: failEnvironmentCloudInternalError( |
| 1091 | | "Could not register the managed endpoint origin.", |
| 1092 | | ), |
| 1187 | SchemaError: internalError("persist-relay-config"), |
| 1188 | PlatformError: internalError("register-endpoint-origin"), |
| 1093 | 1189 | }), |
| 1094 | 1190 | ); |
| 1095 | 1191 | |
| ... | ... | @@ -1216,10 +1312,7 @@ const make = Effect.gen(function* () { |
| 1216 | 1312 | function* () { |
| 1217 | 1313 | return yield* readCloudLinkState(); |
| 1218 | 1314 | }, |
| 1219 | | Effect.catchIf( |
| 1220 | | ServerSecretStore.isSecretStoreError, |
| 1221 | | failEnvironmentCloudInternalError("Could not read environment relay configuration."), |
| 1222 | | ), |
| 1315 | Effect.catchIf(ServerSecretStore.isSecretStoreError, internalError("read-relay-config")), |
| 1223 | 1316 | ); |
| 1224 | 1317 | |
| 1225 | 1318 | const unlink = Effect.fn("environment.cloud.unlink")( |
| ... | ... | @@ -1246,19 +1339,14 @@ const make = Effect.gen(function* () { |
| 1246 | 1339 | }), |
| 1247 | 1340 | ); |
| 1248 | 1341 | }, |
| 1249 | | Effect.catchIf( |
| 1250 | | ServerSecretStore.isSecretStoreError, |
| 1251 | | failEnvironmentCloudInternalError("Could not remove environment relay configuration."), |
| 1252 | | ), |
| 1342 | Effect.catchIf(ServerSecretStore.isSecretStoreError, internalError("remove-relay-config")), |
| 1253 | 1343 | ); |
| 1254 | 1344 | |
| 1255 | 1345 | const pushHoldWebhooksWhileOffline = Effect.fn("CloudPreferences.pushHoldWebhooksWhileOffline")( |
| 1256 | 1346 | function* (holdWebhooksWhileOffline: boolean) { |
| 1257 | 1347 | const connection = yield* readRelayConnection.pipe(withSecrets); |
| 1258 | 1348 | if (connection === null) { |
| 1259 | | return yield* new EnvironmentHttpBadRequestError({ |
| 1260 | | message: "Link this environment to T3 Connect first.", |
| 1261 | | }); |
| 1349 | return yield* new CloudLinkNotLinkedError({}); |
| 1262 | 1350 | } |
| 1263 | 1351 | const environmentId = yield* environment.getEnvironmentId; |
| 1264 | 1352 | const client = yield* makeRelayEnvironmentClient(connection); |
| ... | ... | @@ -1267,23 +1355,14 @@ const make = Effect.gen(function* () { |
| 1267 | 1355 | params: { environmentId }, |
| 1268 | 1356 | payload: { holdWebhooksWhileOffline }, |
| 1269 | 1357 | }) |
| 1270 | | .pipe( |
| 1271 | | Effect.timeout("10 seconds"), |
| 1272 | | Effect.catch( |
| 1273 | | failEnvironmentCloudInternalError("Could not update T3 Connect webhook settings."), |
| 1274 | | ), |
| 1275 | | ); |
| 1358 | .pipe(Effect.timeout("10 seconds"), Effect.catch(internalError("update-webhook-settings"))); |
| 1276 | 1359 | }, |
| 1277 | 1360 | ); |
| 1278 | 1361 | |
| 1279 | 1362 | const savePreference = (name: string, value: boolean) => |
| 1280 | 1363 | secrets |
| 1281 | 1364 | .set(name, stringToBytes(String(value))) |
| 1282 | | .pipe( |
| 1283 | | Effect.catch( |
| 1284 | | failEnvironmentCloudInternalError("Could not persist environment cloud preferences."), |
| 1285 | | ), |
| 1286 | | ); |
| 1365 | .pipe(Effect.catch(internalError("persist-preferences"))); |
| 1287 | 1366 | |
| 1288 | 1367 | // One update at a time, so two requests can't each leave one setting behind. |
| 1289 | 1368 | const preferencesLock = yield* Semaphore.make(1); |
| ... | ... | @@ -1295,13 +1374,7 @@ const make = Effect.gen(function* () { |
| 1295 | 1374 | // current values are read up front; a failed read stops here, before |
| 1296 | 1375 | // anything changes, because a guessed value would be the rollback target. |
| 1297 | 1376 | const readCurrent = (name: string) => |
| 1298 | | secrets |
| 1299 | | .get(name) |
| 1300 | | .pipe( |
| 1301 | | Effect.catch( |
| 1302 | | failEnvironmentCloudInternalError("Could not read environment cloud preferences."), |
| 1303 | | ), |
| 1304 | | ); |
| 1377 | secrets.get(name).pipe(Effect.catch(internalError("read-preferences"))); |
| 1305 | 1378 | const previousActivity = yield* readCurrent(PUBLISH_AGENT_ACTIVITY_SECRET); |
| 1306 | 1379 | const previousHold = yield* readCurrent(HOLD_WEBHOOKS_WHILE_OFFLINE_SECRET); |
| 1307 | 1380 | yield* savePreference(PUBLISH_AGENT_ACTIVITY_SECRET, input.publishAgentActivity); |
| ... | ... | @@ -1342,10 +1415,7 @@ const make = Effect.gen(function* () { |
| 1342 | 1415 | yield* savePreferences(input); |
| 1343 | 1416 | return yield* readCloudLinkState(); |
| 1344 | 1417 | }, |
| 1345 | | Effect.catchIf( |
| 1346 | | ServerSecretStore.isSecretStoreError, |
| 1347 | | failEnvironmentCloudInternalError("Could not read environment cloud preferences."), |
| 1348 | | ), |
| 1418 | Effect.catchIf(ServerSecretStore.isSecretStoreError, internalError("read-preferences")), |
| 1349 | 1419 | ); |
| 1350 | 1420 | |
| 1351 | 1421 | const answerHealthRequest = Effect.fn("environment.cloud.health")( |
| ... | ... | @@ -1397,9 +1467,7 @@ const make = Effect.gen(function* () { |
| 1397 | 1467 | !hasBoundedCloudProofLifetime({ ...proofOption.value, nowSeconds }) || |
| 1398 | 1468 | !hasExactScope({ scopes: proofOption.value.scope, expected: "environment:status" }) |
| 1399 | 1469 | ) { |
| 1400 | | return yield* new EnvironmentHttpUnauthorizedError({ |
| 1401 | | message: "Invalid cloud health request.", |
| 1402 | | }); |
| 1470 | return yield* new CloudLinkProofRejectedError({ request: "health" }); |
| 1403 | 1471 | } |
| 1404 | 1472 | const proof = proofOption.value; |
| 1405 | 1473 | |
| ... | ... | @@ -1411,9 +1479,7 @@ const make = Effect.gen(function* () { |
| 1411 | 1479 | value: stringToBytes(DateTime.formatIso(now)), |
| 1412 | 1480 | }); |
| 1413 | 1481 | if (!consumedReplayGuards) { |
| 1414 | | return yield* new EnvironmentHttpConflictError({ |
| 1415 | | message: "Cloud health request was already consumed.", |
| 1416 | | }); |
| 1482 | return yield* new CloudLinkProofReplayedError({ request: "health" }); |
| 1417 | 1483 | } |
| 1418 | 1484 | |
| 1419 | 1485 | const keyPair = yield* getOrCreateEnvironmentKeyPairFromSecretStore(secrets); |
| ... | ... | @@ -1452,17 +1518,8 @@ const make = Effect.gen(function* () { |
| 1452 | 1518 | proof: responseProof, |
| 1453 | 1519 | } satisfies RelayEnvironmentHealthResponse; |
| 1454 | 1520 | }, |
| 1455 | | Effect.catchIf(EnvironmentAuth.isServerAuthInternalError, (error) => |
| 1456 | | failEnvironmentCloudInternalError(error.message)(error), |
| 1457 | | ), |
| 1458 | | Effect.catchIf( |
| 1459 | | ServerSecretStore.isSecretStoreError, |
| 1460 | | failEnvironmentCloudInternalError("Could not answer cloud health request."), |
| 1461 | | ), |
| 1462 | | Effect.catchTag( |
| 1463 | | "PlatformError", |
| 1464 | | failEnvironmentCloudInternalError("Could not answer cloud health request."), |
| 1465 | | ), |
| 1521 | Effect.catchIf(ServerSecretStore.isSecretStoreError, internalError("answer-health")), |
| 1522 | Effect.catchTag("PlatformError", internalError("answer-health")), |
| 1466 | 1523 | ); |
| 1467 | 1524 | |
| 1468 | 1525 | const mintCredential = Effect.fn("environment.cloud.mintCredential")( |
| ... | ... | @@ -1515,9 +1572,7 @@ const make = Effect.gen(function* () { |
| 1515 | 1572 | !hasBoundedCloudProofLifetime({ ...proofOption.value, nowSeconds }) || |
| 1516 | 1573 | !hasExactScope({ scopes: proofOption.value.scope, expected: "environment:connect" }) |
| 1517 | 1574 | ) { |
| 1518 | | return yield* new EnvironmentHttpUnauthorizedError({ |
| 1519 | | message: "Invalid cloud mint request.", |
| 1520 | | }); |
| 1575 | return yield* new CloudLinkProofRejectedError({ request: "mint" }); |
| 1521 | 1576 | } |
| 1522 | 1577 | const proof = proofOption.value; |
| 1523 | 1578 | |
| ... | ... | @@ -1529,9 +1584,7 @@ const make = Effect.gen(function* () { |
| 1529 | 1584 | value: stringToBytes(DateTime.formatIso(now)), |
| 1530 | 1585 | }); |
| 1531 | 1586 | if (!consumedReplayGuards) { |
| 1532 | | return yield* new EnvironmentHttpConflictError({ |
| 1533 | | message: "Cloud mint request was already consumed.", |
| 1534 | | }); |
| 1587 | return yield* new CloudLinkProofReplayedError({ request: "mint" }); |
| 1535 | 1588 | } |
| 1536 | 1589 | |
| 1537 | 1590 | const keyPair = yield* getOrCreateEnvironmentKeyPairFromSecretStore(secrets); |
| ... | ... | @@ -1572,17 +1625,8 @@ const make = Effect.gen(function* () { |
| 1572 | 1625 | proof: responseProof, |
| 1573 | 1626 | } satisfies RelayEnvironmentMintResponse; |
| 1574 | 1627 | }, |
| 1575 | | Effect.catchIf(EnvironmentAuth.isServerAuthInternalError, (error) => |
| 1576 | | failEnvironmentCloudInternalError(error.message)(error), |
| 1577 | | ), |
| 1578 | | Effect.catchIf( |
| 1579 | | ServerSecretStore.isSecretStoreError, |
| 1580 | | failEnvironmentCloudInternalError("Could not issue cloud connection credential."), |
| 1581 | | ), |
| 1582 | | Effect.catchTag( |
| 1583 | | "PlatformError", |
| 1584 | | failEnvironmentCloudInternalError("Could not issue cloud connection credential."), |
| 1585 | | ), |
| 1628 | Effect.catchIf(ServerSecretStore.isSecretStoreError, internalError("issue-credential")), |
| 1629 | Effect.catchTag("PlatformError", internalError("issue-credential")), |
| 1586 | 1630 | ); |
| 1587 | 1631 | |
| 1588 | 1632 | return CloudLink.of({ |