| 1 | import * as Context from "effect/Context"; |
| 2 | import type * as DateTime from "effect/DateTime"; |
| 3 | import * as Schema from "effect/Schema"; |
| 4 | import * as HttpApi from "effect/http-api/HttpApi"; |
| 5 | import * as HttpApiEndpoint from "effect/http-api/HttpApiEndpoint"; |
| 6 | import * as HttpApiGroup from "effect/http-api/HttpApiGroup"; |
| 7 | import * as HttpApiMiddleware from "effect/http-api/HttpApiMiddleware"; |
| 8 | import * as HttpApiSchema from "effect/http-api/HttpApiSchema"; |
| 9 | import * as HttpServerRespondable from "effect/http/HttpServerRespondable"; |
| 10 | import * as HttpServerResponse from "effect/http/HttpServerResponse"; |
| 11 | |
| 12 | import { |
| 13 | AuthAccessTokenResult, |
| 14 | AuthBrowserSessionRequest, |
| 15 | AuthBrowserSessionResult, |
| 16 | AuthClientSession, |
| 17 | AuthCreatePairingCredentialInput, |
| 18 | AuthCreateDelegatedSessionInput, |
| 19 | AuthMcpApprovalDecisionRequest, |
| 20 | AuthMcpApprovalDetails, |
| 21 | AuthMcpApprovalError, |
| 22 | AuthMcpApprovalRedirect, |
| 23 | AuthMcpAuthorizationRequest, |
| 24 | AuthMcpAuthorizationServerMetadata, |
| 25 | AuthMcpClientRegistration, |
| 26 | AuthMcpProtectedResourceMetadata, |
| 27 | AuthMcpRegisteredClient, |
| 28 | AuthMcpRegistrationError, |
| 29 | AuthMcpTokenError, |
| 30 | AuthMcpTokenRequest, |
| 31 | AuthMcpTokenResult, |
| 32 | AuthPairingCredentialResult, |
| 33 | AuthPairingLink, |
| 34 | AuthRevokeClientSessionInput, |
| 35 | AuthRevokePairingLinkInput, |
| 36 | AuthEnvironmentScope, |
| 37 | AuthTokenExchangeRequest, |
| 38 | AuthSessionState, |
| 39 | AuthWebSocketTicketResult, |
| 40 | ServerAuthSessionMethod, |
| 41 | } from "./auth.ts"; |
| 42 | import { |
| 43 | ExecutionEnvironmentDescriptor, |
| 44 | ORCHESTRATION_PROTOCOL_HEADER, |
| 45 | ORCHESTRATION_PROTOCOL_VERSION_TEXT, |
| 46 | } from "./environment.ts"; |
| 47 | import { |
| 48 | DpopFailureReason, |
| 49 | AuthSessionId, |
| 50 | ThreadId, |
| 51 | TrimmedNonEmptyString, |
| 52 | } from "./baseSchemas.ts"; |
| 53 | import { |
| 54 | OrchestrationV2ShellSnapshot, |
| 55 | OrchestrationV2ThreadBoundedSnapshot, |
| 56 | OrchestrationV2ThreadDetailSnapshot, |
| 57 | OrchestrationV2ThreadHistoryPage, |
| 58 | OrchestrationV2ThreadTranscript, |
| 59 | } from "./orchestrationV2.ts"; |
| 60 | import { Project, ProjectMutation, ProjectSnapshot } from "./project.ts"; |
| 61 | import { |
| 62 | PullRequestDiffInput, |
| 63 | PullRequestDiffResult, |
| 64 | PullRequestOperationError, |
| 65 | PullRequestUnavailableError, |
| 66 | } from "./pullRequest.ts"; |
| 67 | import { |
| 68 | RelayCloudEnvironmentHealthRequest, |
| 69 | RelayCloudMintCredentialRequest, |
| 70 | RelayEnvironmentConfigRequest, |
| 71 | RelayEnvironmentHealthResponse, |
| 72 | RelayEnvironmentLinkProof, |
| 73 | RelayEnvironmentMintResponse, |
| 74 | RelayLinkProofRequest, |
| 75 | } from "./relay.ts"; |
| 76 | |
| 77 | const OptionalBearerHeaders = Schema.Struct({ |
| 78 | "x-csrf-token": Schema.optionalKey(Schema.String), |
| 79 | authorization: Schema.optionalKey(Schema.String), |
| 80 | dpop: Schema.optionalKey(Schema.String), |
| 81 | }); |
| 82 | |
| 83 | const OrchestrationProtocolHeaders = Schema.Struct({ |
| 84 | "x-csrf-token": Schema.optionalKey(Schema.String), |
| 85 | authorization: Schema.optionalKey(Schema.String), |
| 86 | dpop: Schema.optionalKey(Schema.String), |
| 87 | [ORCHESTRATION_PROTOCOL_HEADER]: Schema.Literal(ORCHESTRATION_PROTOCOL_VERSION_TEXT), |
| 88 | }); |
| 89 | |
| 90 | const OptionalDpopProofHeaders = Schema.Struct({ |
| 91 | "x-csrf-token": Schema.optionalKey(Schema.String), |
| 92 | dpop: Schema.optionalKey(Schema.String), |
| 93 | }); |
| 94 | |
| 95 | export const EnvironmentRequestInvalidReason = Schema.Literals([ |
| 96 | "invalid_scope", |
| 97 | "scope_not_granted", |
| 98 | "invalid_command", |
| 99 | "invalid_history_cursor", |
| 100 | "thread_transcript_too_large", |
| 101 | ]); |
| 102 | export type EnvironmentRequestInvalidReason = typeof EnvironmentRequestInvalidReason.Type; |
| 103 | |
| 104 | export const EnvironmentAuthInvalidReason = Schema.Literals([ |
| 105 | "missing_credential", |
| 106 | "invalid_credential", |
| 107 | ]); |
| 108 | export type EnvironmentAuthInvalidReason = typeof EnvironmentAuthInvalidReason.Type; |
| 109 | |
| 110 | export const EnvironmentOperationForbiddenReason = Schema.Literals([ |
| 111 | "current_session_revoke_not_allowed", |
| 112 | ]); |
| 113 | export type EnvironmentOperationForbiddenReason = typeof EnvironmentOperationForbiddenReason.Type; |
| 114 | |
| 115 | export const EnvironmentInternalErrorReason = Schema.Literals([ |
| 116 | "bootstrap_validation_failed", |
| 117 | "browser_session_issuance_failed", |
| 118 | "browser_session_cookie_failed", |
| 119 | "access_token_issuance_failed", |
| 120 | "websocket_ticket_issuance_failed", |
| 121 | "pairing_credential_issuance_failed", |
| 122 | "pairing_links_load_failed", |
| 123 | "pairing_link_revoke_failed", |
| 124 | "client_sessions_load_failed", |
| 125 | "client_session_revoke_failed", |
| 126 | "project_snapshot_failed", |
| 127 | "project_mutation_failed", |
| 128 | "orchestration_snapshot_failed", |
| 129 | "orchestration_thread_snapshot_failed", |
| 130 | "orchestration_thread_bounded_snapshot_failed", |
| 131 | "orchestration_thread_history_failed", |
| 132 | "internal_error", |
| 133 | ]); |
| 134 | export type EnvironmentInternalErrorReason = typeof EnvironmentInternalErrorReason.Type; |
| 135 | |
| 136 | export class EnvironmentRequestInvalidError extends Schema.TaggedError<EnvironmentRequestInvalidError>()( |
| 137 | "EnvironmentRequestInvalidError", |
| 138 | { |
| 139 | code: Schema.Literal("invalid_request"), |
| 140 | reason: EnvironmentRequestInvalidReason, |
| 141 | traceId: TrimmedNonEmptyString, |
| 142 | }, |
| 143 | { httpApiStatus: 400 }, |
| 144 | ) { |
| 145 | [HttpServerRespondable.symbol]() { |
| 146 | return HttpServerResponse.schemaJson(EnvironmentRequestInvalidError)(this, { status: 400 }); |
| 147 | } |
| 148 | |
| 149 | override get message(): string { |
| 150 | return `The environment rejected the request (${this.reason}).`; |
| 151 | } |
| 152 | } |
| 153 | |
| 154 | export class EnvironmentAuthInvalidError extends Schema.TaggedError<EnvironmentAuthInvalidError>()( |
| 155 | "EnvironmentAuthInvalidError", |
| 156 | { |
| 157 | code: Schema.Literal("auth_invalid"), |
| 158 | reason: EnvironmentAuthInvalidReason, |
| 159 | // Older servers do not send a DPoP failure category. |
| 160 | dpopFailureReason: Schema.optionalKey(DpopFailureReason), |
| 161 | traceId: TrimmedNonEmptyString, |
| 162 | }, |
| 163 | { httpApiStatus: 401 }, |
| 164 | ) { |
| 165 | [HttpServerRespondable.symbol]() { |
| 166 | return HttpServerResponse.schemaJson(EnvironmentAuthInvalidError)(this, { status: 401 }); |
| 167 | } |
| 168 | |
| 169 | override get message(): string { |
| 170 | return `The environment rejected this client's credentials (${this.reason}).`; |
| 171 | } |
| 172 | } |
| 173 | |
| 174 | export class EnvironmentScopeRequiredError extends Schema.TaggedError<EnvironmentScopeRequiredError>()( |
| 175 | "EnvironmentScopeRequiredError", |
| 176 | { |
| 177 | code: Schema.Literal("insufficient_scope"), |
| 178 | requiredScope: AuthEnvironmentScope, |
| 179 | requiredPermission: Schema.optionalKey(Schema.String), |
| 180 | traceId: TrimmedNonEmptyString, |
| 181 | }, |
| 182 | { httpApiStatus: 403 }, |
| 183 | ) { |
| 184 | [HttpServerRespondable.symbol]() { |
| 185 | return HttpServerResponse.schemaJson(EnvironmentScopeRequiredError)(this, { status: 403 }); |
| 186 | } |
| 187 | |
| 188 | override get message(): string { |
| 189 | return `This request needs the ${this.requiredPermission ?? this.requiredScope} scope, which this client does not have.`; |
| 190 | } |
| 191 | } |
| 192 | |
| 193 | export class EnvironmentOperationForbiddenError extends Schema.TaggedError<EnvironmentOperationForbiddenError>()( |
| 194 | "EnvironmentOperationForbiddenError", |
| 195 | { |
| 196 | code: Schema.Literal("operation_forbidden"), |
| 197 | reason: EnvironmentOperationForbiddenReason, |
| 198 | traceId: TrimmedNonEmptyString, |
| 199 | }, |
| 200 | { httpApiStatus: 403 }, |
| 201 | ) { |
| 202 | [HttpServerRespondable.symbol]() { |
| 203 | return HttpServerResponse.schemaJson(EnvironmentOperationForbiddenError)(this, { status: 403 }); |
| 204 | } |
| 205 | |
| 206 | override get message(): string { |
| 207 | return `The environment refused this operation (${this.reason}).`; |
| 208 | } |
| 209 | } |
| 210 | |
| 211 | export class EnvironmentInternalError extends Schema.TaggedError<EnvironmentInternalError>()( |
| 212 | "EnvironmentInternalError", |
| 213 | { |
| 214 | code: Schema.Literal("internal_error"), |
| 215 | reason: EnvironmentInternalErrorReason, |
| 216 | traceId: TrimmedNonEmptyString, |
| 217 | }, |
| 218 | { httpApiStatus: 500 }, |
| 219 | ) { |
| 220 | [HttpServerRespondable.symbol]() { |
| 221 | return HttpServerResponse.schemaJson(EnvironmentInternalError)(this, { status: 500 }); |
| 222 | } |
| 223 | |
| 224 | override get message(): string { |
| 225 | return `The environment failed to answer this request (${this.reason}).`; |
| 226 | } |
| 227 | } |
| 228 | |
| 229 | export const EnvironmentResourceNotFoundReason = Schema.Literals(["thread_not_found"]); |
| 230 | export type EnvironmentResourceNotFoundReason = typeof EnvironmentResourceNotFoundReason.Type; |
| 231 | |
| 232 | export class EnvironmentResourceNotFoundError extends Schema.TaggedError<EnvironmentResourceNotFoundError>()( |
| 233 | "EnvironmentResourceNotFoundError", |
| 234 | { |
| 235 | code: Schema.Literal("not_found"), |
| 236 | reason: EnvironmentResourceNotFoundReason, |
| 237 | traceId: TrimmedNonEmptyString, |
| 238 | }, |
| 239 | { httpApiStatus: 404 }, |
| 240 | ) { |
| 241 | [HttpServerRespondable.symbol]() { |
| 242 | return HttpServerResponse.schemaJson(EnvironmentResourceNotFoundError)(this, { status: 404 }); |
| 243 | } |
| 244 | |
| 245 | override get message(): string { |
| 246 | return `The environment could not find what this request named (${this.reason}).`; |
| 247 | } |
| 248 | } |
| 249 | |
| 250 | export const EnvironmentHttpCommonError = Schema.Union([ |
| 251 | EnvironmentRequestInvalidError, |
| 252 | EnvironmentAuthInvalidError, |
| 253 | EnvironmentScopeRequiredError, |
| 254 | EnvironmentOperationForbiddenError, |
| 255 | EnvironmentResourceNotFoundError, |
| 256 | EnvironmentInternalError, |
| 257 | ]); |
| 258 | export type EnvironmentHttpCommonError = typeof EnvironmentHttpCommonError.Type; |
| 259 | |
| 260 | const EnvironmentAuthenticationErrors = [ |
| 261 | EnvironmentAuthInvalidError, |
| 262 | EnvironmentInternalError, |
| 263 | ] as const; |
| 264 | |
| 265 | export class EnvironmentHttpBadRequestError extends Schema.TaggedError<EnvironmentHttpBadRequestError>()( |
| 266 | "EnvironmentHttpBadRequestError", |
| 267 | { |
| 268 | message: Schema.String, |
| 269 | }, |
| 270 | { httpApiStatus: 400 }, |
| 271 | ) { |
| 272 | [HttpServerRespondable.symbol]() { |
| 273 | return HttpServerResponse.schemaJson(EnvironmentHttpBadRequestError)(this, { status: 400 }); |
| 274 | } |
| 275 | } |
| 276 | |
| 277 | export class EnvironmentHttpUnauthorizedError extends Schema.TaggedError<EnvironmentHttpUnauthorizedError>()( |
| 278 | "EnvironmentHttpUnauthorizedError", |
| 279 | { |
| 280 | message: Schema.String, |
| 281 | }, |
| 282 | { httpApiStatus: 401 }, |
| 283 | ) { |
| 284 | [HttpServerRespondable.symbol]() { |
| 285 | return HttpServerResponse.schemaJson(EnvironmentHttpUnauthorizedError)(this, { status: 401 }); |
| 286 | } |
| 287 | } |
| 288 | |
| 289 | export class EnvironmentHttpForbiddenError extends Schema.TaggedError<EnvironmentHttpForbiddenError>()( |
| 290 | "EnvironmentHttpForbiddenError", |
| 291 | { |
| 292 | message: Schema.String, |
| 293 | }, |
| 294 | { httpApiStatus: 403 }, |
| 295 | ) { |
| 296 | [HttpServerRespondable.symbol]() { |
| 297 | return HttpServerResponse.schemaJson(EnvironmentHttpForbiddenError)(this, { status: 403 }); |
| 298 | } |
| 299 | } |
| 300 | |
| 301 | export class EnvironmentHttpInternalServerError extends Schema.TaggedError<EnvironmentHttpInternalServerError>()( |
| 302 | "EnvironmentHttpInternalServerError", |
| 303 | { |
| 304 | message: Schema.String, |
| 305 | }, |
| 306 | { httpApiStatus: 500 }, |
| 307 | ) { |
| 308 | [HttpServerRespondable.symbol]() { |
| 309 | return HttpServerResponse.schemaJson(EnvironmentHttpInternalServerError)(this, { status: 500 }); |
| 310 | } |
| 311 | } |
| 312 | |
| 313 | export class EnvironmentHttpConflictError extends Schema.TaggedError<EnvironmentHttpConflictError>()( |
| 314 | "EnvironmentHttpConflictError", |
| 315 | { |
| 316 | message: Schema.String, |
| 317 | }, |
| 318 | { httpApiStatus: 409 }, |
| 319 | ) { |
| 320 | [HttpServerRespondable.symbol]() { |
| 321 | return HttpServerResponse.schemaJson(EnvironmentHttpConflictError)(this, { status: 409 }); |
| 322 | } |
| 323 | } |
| 324 | |
| 325 | export class EnvironmentCloudEndpointUnavailableError extends Schema.TaggedError<EnvironmentCloudEndpointUnavailableError>()( |
| 326 | "EnvironmentCloudEndpointUnavailableError", |
| 327 | { |
| 328 | message: Schema.String, |
| 329 | endpointRuntimeStatus: Schema.Unknown, |
| 330 | }, |
| 331 | { httpApiStatus: 503 }, |
| 332 | ) { |
| 333 | [HttpServerRespondable.symbol]() { |
| 334 | return HttpServerResponse.schemaJson(EnvironmentCloudEndpointUnavailableError)(this, { |
| 335 | status: 503, |
| 336 | }); |
| 337 | } |
| 338 | } |
| 339 | const EnvironmentSessionCreationErrors = [ |
| 340 | EnvironmentAuthInvalidError, |
| 341 | EnvironmentInternalError, |
| 342 | ] as const; |
| 343 | const EnvironmentTokenExchangeErrors = [ |
| 344 | EnvironmentRequestInvalidError, |
| 345 | EnvironmentAuthInvalidError, |
| 346 | EnvironmentInternalError, |
| 347 | ] as const; |
| 348 | const EnvironmentScopedOperationErrors = [ |
| 349 | EnvironmentScopeRequiredError, |
| 350 | EnvironmentInternalError, |
| 351 | ] as const; |
| 352 | const EnvironmentPairingCredentialErrors = [ |
| 353 | EnvironmentRequestInvalidError, |
| 354 | ...EnvironmentScopedOperationErrors, |
| 355 | ] as const; |
| 356 | const EnvironmentSessionRevokeErrors = [ |
| 357 | EnvironmentScopeRequiredError, |
| 358 | EnvironmentOperationForbiddenError, |
| 359 | EnvironmentInternalError, |
| 360 | ] as const; |
| 361 | const EnvironmentProjectSnapshotErrors = [ |
| 362 | EnvironmentScopeRequiredError, |
| 363 | EnvironmentInternalError, |
| 364 | ] as const; |
| 365 | const EnvironmentOrchestrationSnapshotErrors = [ |
| 366 | EnvironmentScopeRequiredError, |
| 367 | EnvironmentInternalError, |
| 368 | ] as const; |
| 369 | const EnvironmentOrchestrationThreadSnapshotErrors = [ |
| 370 | EnvironmentScopeRequiredError, |
| 371 | EnvironmentResourceNotFoundError, |
| 372 | EnvironmentInternalError, |
| 373 | ] as const; |
| 374 | const EnvironmentProjectMutationErrors = [ |
| 375 | EnvironmentRequestInvalidError, |
| 376 | EnvironmentScopeRequiredError, |
| 377 | EnvironmentInternalError, |
| 378 | ] as const; |
| 379 | |
| 380 | export interface EnvironmentSessionPrincipalShape { |
| 381 | readonly parentSessionId?: AuthSessionId; |
| 382 | readonly sessionId: AuthSessionId; |
| 383 | readonly subject: string; |
| 384 | readonly method: ServerAuthSessionMethod; |
| 385 | readonly scopes: ReadonlySet<AuthEnvironmentScope>; |
| 386 | readonly proofKeyThumbprint?: string; |
| 387 | readonly expiresAt?: DateTime.DateTime; |
| 388 | } |
| 389 | |
| 390 | export class EnvironmentAuthenticatedPrincipal extends Context.Service< |
| 391 | EnvironmentAuthenticatedPrincipal, |
| 392 | EnvironmentSessionPrincipalShape |
| 393 | >()("@t3tools/contracts/environmentHttp/EnvironmentAuthenticatedPrincipal") {} |
| 394 | |
| 395 | export class EnvironmentAuthenticatedAuth extends HttpApiMiddleware.Service< |
| 396 | EnvironmentAuthenticatedAuth, |
| 397 | { provides: EnvironmentAuthenticatedPrincipal } |
| 398 | >()("EnvironmentAuthenticatedAuth", { |
| 399 | error: EnvironmentAuthenticationErrors, |
| 400 | }) {} |
| 401 | |
| 402 | const EnvironmentHttpCloudErrors = [ |
| 403 | EnvironmentHttpBadRequestError, |
| 404 | EnvironmentHttpUnauthorizedError, |
| 405 | EnvironmentHttpForbiddenError, |
| 406 | EnvironmentHttpConflictError, |
| 407 | EnvironmentHttpInternalServerError, |
| 408 | EnvironmentScopeRequiredError, |
| 409 | ] as const; |
| 410 | |
| 411 | export const EnvironmentCloudRelayConfigResult = Schema.Struct({ |
| 412 | ok: Schema.Boolean, |
| 413 | endpointRuntimeStatus: Schema.Unknown, |
| 414 | }); |
| 415 | export type EnvironmentCloudRelayConfigResult = typeof EnvironmentCloudRelayConfigResult.Type; |
| 416 | |
| 417 | export const EnvironmentCloudLinkStateResult = Schema.Struct({ |
| 418 | linked: Schema.Boolean, |
| 419 | cloudUserId: Schema.NullOr(Schema.String), |
| 420 | relayUrl: Schema.NullOr(Schema.String), |
| 421 | relayIssuer: Schema.NullOr(Schema.String), |
| 422 | // A managed Cloudflare tunnel is provisioned for this link. False for a |
| 423 | // publish-only link (activity publishing without a relay-managed tunnel), so |
| 424 | // clients can present the two capabilities as independent settings. |
| 425 | // Optional so newer clients tolerate older environment servers. |
| 426 | managedTunnelActive: Schema.optional(Schema.Boolean), |
| 427 | publishAgentActivity: Schema.Boolean, |
| 428 | // Opt-in: T3 Connect holds webhook requests while this environment is |
| 429 | // offline. Optional so newer clients tolerate older environment servers. |
| 430 | holdWebhooksWhileOffline: Schema.optional(Schema.Boolean), |
| 431 | // The relay rejected this link's credential, usually because the environment |
| 432 | // was deregistered from T3 Connect. The link stays stored until it is linked |
| 433 | // again. Optional so newer clients tolerate older environment servers. |
| 434 | linkRevoked: Schema.optional(Schema.Boolean), |
| 435 | }); |
| 436 | export type EnvironmentCloudLinkStateResult = typeof EnvironmentCloudLinkStateResult.Type; |
| 437 | |
| 438 | export const EnvironmentCloudPreferencesRequest = Schema.Struct({ |
| 439 | publishAgentActivity: Schema.Boolean, |
| 440 | // Omit to leave the current value unchanged. |
| 441 | holdWebhooksWhileOffline: Schema.optional(Schema.Boolean), |
| 442 | }); |
| 443 | export type EnvironmentCloudPreferencesRequest = typeof EnvironmentCloudPreferencesRequest.Type; |
| 444 | |
| 445 | export const AuthPairingLinkRevokeResult = Schema.Struct({ |
| 446 | revoked: Schema.Boolean, |
| 447 | }); |
| 448 | export type AuthPairingLinkRevokeResult = typeof AuthPairingLinkRevokeResult.Type; |
| 449 | |
| 450 | export const AuthClientSessionRevokeResult = Schema.Struct({ |
| 451 | revoked: Schema.Boolean, |
| 452 | }); |
| 453 | export type AuthClientSessionRevokeResult = typeof AuthClientSessionRevokeResult.Type; |
| 454 | |
| 455 | export const AuthOtherClientSessionsRevokeResult = Schema.Struct({ |
| 456 | revokedCount: Schema.Number, |
| 457 | }); |
| 458 | export type AuthOtherClientSessionsRevokeResult = typeof AuthOtherClientSessionsRevokeResult.Type; |
| 459 | |
| 460 | class EnvironmentMetadataHttpApi extends HttpApiGroup.make("metadata").add( |
| 461 | HttpApiEndpoint.get("descriptor", "/.well-known/t3/environment", { |
| 462 | success: ExecutionEnvironmentDescriptor, |
| 463 | }), |
| 464 | ) {} |
| 465 | |
| 466 | class EnvironmentAuthHttpApi extends HttpApiGroup.make("auth") |
| 467 | .add( |
| 468 | HttpApiEndpoint.get("session", "/api/auth/session", { |
| 469 | headers: OptionalBearerHeaders, |
| 470 | success: AuthSessionState, |
| 471 | error: [EnvironmentInternalError], |
| 472 | }), |
| 473 | ) |
| 474 | .add( |
| 475 | HttpApiEndpoint.post("browserSession", "/api/auth/browser-session", { |
| 476 | payload: AuthBrowserSessionRequest, |
| 477 | success: AuthBrowserSessionResult, |
| 478 | error: EnvironmentSessionCreationErrors, |
| 479 | }), |
| 480 | ) |
| 481 | .add( |
| 482 | HttpApiEndpoint.post("token", "/oauth/token", { |
| 483 | headers: OptionalDpopProofHeaders, |
| 484 | payload: AuthTokenExchangeRequest, |
| 485 | success: AuthAccessTokenResult, |
| 486 | error: EnvironmentTokenExchangeErrors, |
| 487 | }), |
| 488 | ) |
| 489 | .add( |
| 490 | HttpApiEndpoint.post("webSocketTicket", "/api/auth/websocket-ticket", { |
| 491 | headers: OptionalBearerHeaders, |
| 492 | success: AuthWebSocketTicketResult, |
| 493 | error: [EnvironmentInternalError], |
| 494 | }).middleware(EnvironmentAuthenticatedAuth), |
| 495 | ) |
| 496 | .add( |
| 497 | HttpApiEndpoint.post("pairingCredential", "/api/auth/pairing-token", { |
| 498 | headers: OptionalBearerHeaders, |
| 499 | payload: AuthCreatePairingCredentialInput, |
| 500 | success: AuthPairingCredentialResult, |
| 501 | error: EnvironmentPairingCredentialErrors, |
| 502 | }).middleware(EnvironmentAuthenticatedAuth), |
| 503 | ) |
| 504 | .add( |
| 505 | HttpApiEndpoint.post("delegatedSession", "/api/auth/delegated-session", { |
| 506 | headers: OptionalBearerHeaders, |
| 507 | payload: AuthCreateDelegatedSessionInput, |
| 508 | success: AuthAccessTokenResult, |
| 509 | error: EnvironmentPairingCredentialErrors, |
| 510 | }).middleware(EnvironmentAuthenticatedAuth), |
| 511 | ) |
| 512 | .add( |
| 513 | HttpApiEndpoint.get("pairingLinks", "/api/auth/pairing-links", { |
| 514 | headers: OptionalBearerHeaders, |
| 515 | success: Schema.Array(AuthPairingLink), |
| 516 | error: EnvironmentScopedOperationErrors, |
| 517 | }).middleware(EnvironmentAuthenticatedAuth), |
| 518 | ) |
| 519 | .add( |
| 520 | HttpApiEndpoint.post("revokePairingLink", "/api/auth/pairing-links/revoke", { |
| 521 | headers: OptionalBearerHeaders, |
| 522 | payload: AuthRevokePairingLinkInput, |
| 523 | success: AuthPairingLinkRevokeResult, |
| 524 | error: EnvironmentScopedOperationErrors, |
| 525 | }).middleware(EnvironmentAuthenticatedAuth), |
| 526 | ) |
| 527 | .add( |
| 528 | HttpApiEndpoint.get("clients", "/api/auth/clients", { |
| 529 | headers: OptionalBearerHeaders, |
| 530 | success: Schema.Array(AuthClientSession), |
| 531 | error: EnvironmentScopedOperationErrors, |
| 532 | }).middleware(EnvironmentAuthenticatedAuth), |
| 533 | ) |
| 534 | .add( |
| 535 | HttpApiEndpoint.post("revokeClient", "/api/auth/clients/revoke", { |
| 536 | headers: OptionalBearerHeaders, |
| 537 | payload: AuthRevokeClientSessionInput, |
| 538 | success: AuthClientSessionRevokeResult, |
| 539 | error: EnvironmentSessionRevokeErrors, |
| 540 | }).middleware(EnvironmentAuthenticatedAuth), |
| 541 | ) |
| 542 | .add( |
| 543 | HttpApiEndpoint.post("revokeOtherClients", "/api/auth/clients/revoke-others", { |
| 544 | headers: OptionalBearerHeaders, |
| 545 | success: AuthOtherClientSessionsRevokeResult, |
| 546 | error: EnvironmentScopedOperationErrors, |
| 547 | }).middleware(EnvironmentAuthenticatedAuth), |
| 548 | ) {} |
| 549 | |
| 550 | /** |
| 551 | * The OAuth authorization server outside agents use to sign in to `/mcp`. |
| 552 | * `authorize` is where the agent sends the browser: it answers a redirect to |
| 553 | * the web app's approval page, or a plain error page for a request that names |
| 554 | * an unverified client or redirect, so the response is not a schema. |
| 555 | */ |
| 556 | class EnvironmentMcpOAuthHttpApi extends HttpApiGroup.make("mcpOAuth") |
| 557 | .add( |
| 558 | HttpApiEndpoint.get("protectedResource", "/.well-known/oauth-protected-resource", { |
| 559 | success: AuthMcpProtectedResourceMetadata, |
| 560 | }), |
| 561 | ) |
| 562 | .add( |
| 563 | HttpApiEndpoint.get("mcpProtectedResource", "/.well-known/oauth-protected-resource/mcp", { |
| 564 | success: AuthMcpProtectedResourceMetadata, |
| 565 | }), |
| 566 | ) |
| 567 | .add( |
| 568 | HttpApiEndpoint.get("authorizationServer", "/.well-known/oauth-authorization-server", { |
| 569 | success: AuthMcpAuthorizationServerMetadata, |
| 570 | }), |
| 571 | ) |
| 572 | .add( |
| 573 | HttpApiEndpoint.post("register", "/oauth/mcp/register", { |
| 574 | payload: AuthMcpClientRegistration, |
| 575 | success: AuthMcpRegisteredClient, |
| 576 | error: AuthMcpRegistrationError, |
| 577 | }), |
| 578 | ) |
| 579 | .add(HttpApiEndpoint.get("authorize", "/oauth/mcp/authorize")) |
| 580 | .add( |
| 581 | HttpApiEndpoint.post("approval", "/oauth/mcp/approval", { |
| 582 | payload: AuthMcpAuthorizationRequest, |
| 583 | success: [AuthMcpApprovalDetails, AuthMcpApprovalRedirect], |
| 584 | error: AuthMcpApprovalError, |
| 585 | }), |
| 586 | ) |
| 587 | .add( |
| 588 | HttpApiEndpoint.post("decision", "/oauth/mcp/decision", { |
| 589 | payload: AuthMcpApprovalDecisionRequest, |
| 590 | success: AuthMcpApprovalRedirect, |
| 591 | error: AuthMcpApprovalError, |
| 592 | }), |
| 593 | ) |
| 594 | .add( |
| 595 | HttpApiEndpoint.post("token", "/oauth/mcp/token", { |
| 596 | payload: AuthMcpTokenRequest, |
| 597 | success: AuthMcpTokenResult, |
| 598 | error: AuthMcpTokenError, |
| 599 | }), |
| 600 | ) {} |
| 601 | |
| 602 | const EnvironmentOrchestrationThreadSnapshotParams = Schema.Struct({ |
| 603 | threadId: ThreadId, |
| 604 | }); |
| 605 | |
| 606 | /** |
| 607 | * `compactTurnItems=1` opts into `turnItemsOmitLocalVisible`. Other values are |
| 608 | * ignored, and older servers ignore the whole query. |
| 609 | */ |
| 610 | const EnvironmentOrchestrationThreadBoundedSnapshotQuery = Schema.Struct({ |
| 611 | compactTurnItems: Schema.optionalKey(Schema.String), |
| 612 | }); |
| 613 | |
| 614 | const EnvironmentOrchestrationThreadHistoryQuery = Schema.Struct({ |
| 615 | cursor: TrimmedNonEmptyString, |
| 616 | throughEntryId: Schema.optional(TrimmedNonEmptyString), |
| 617 | view: Schema.optional(Schema.Literals(["conversation", "activity"])), |
| 618 | }); |
| 619 | |
| 620 | const EnvironmentOrchestrationThreadHistoryErrors = [ |
| 621 | EnvironmentRequestInvalidError, |
| 622 | EnvironmentScopeRequiredError, |
| 623 | EnvironmentResourceNotFoundError, |
| 624 | EnvironmentInternalError, |
| 625 | ] as const; |
| 626 | |
| 627 | class EnvironmentOrchestrationHttpApi extends HttpApiGroup.make("orchestration") |
| 628 | .add( |
| 629 | HttpApiEndpoint.get("shellSnapshot", "/api/orchestration/shell", { |
| 630 | headers: OrchestrationProtocolHeaders, |
| 631 | success: OrchestrationV2ShellSnapshot, |
| 632 | error: EnvironmentOrchestrationSnapshotErrors, |
| 633 | }).middleware(EnvironmentAuthenticatedAuth), |
| 634 | ) |
| 635 | .add( |
| 636 | HttpApiEndpoint.get("threadSnapshot", "/api/orchestration/threads/:threadId", { |
| 637 | headers: OrchestrationProtocolHeaders, |
| 638 | params: EnvironmentOrchestrationThreadSnapshotParams, |
| 639 | success: OrchestrationV2ThreadDetailSnapshot, |
| 640 | error: EnvironmentOrchestrationThreadSnapshotErrors, |
| 641 | }).middleware(EnvironmentAuthenticatedAuth), |
| 642 | ) |
| 643 | .add( |
| 644 | HttpApiEndpoint.get("threadTranscript", "/api/orchestration/threads/:threadId/transcript", { |
| 645 | headers: OrchestrationProtocolHeaders, |
| 646 | params: EnvironmentOrchestrationThreadSnapshotParams, |
| 647 | success: OrchestrationV2ThreadTranscript, |
| 648 | error: [EnvironmentRequestInvalidError, ...EnvironmentOrchestrationThreadSnapshotErrors], |
| 649 | }).middleware(EnvironmentAuthenticatedAuth), |
| 650 | ) |
| 651 | .add( |
| 652 | HttpApiEndpoint.get("threadBoundedSnapshot", "/api/orchestration/threads/:threadId/bounded", { |
| 653 | headers: OrchestrationProtocolHeaders, |
| 654 | params: EnvironmentOrchestrationThreadSnapshotParams, |
| 655 | query: EnvironmentOrchestrationThreadBoundedSnapshotQuery, |
| 656 | success: OrchestrationV2ThreadBoundedSnapshot, |
| 657 | error: EnvironmentOrchestrationThreadSnapshotErrors, |
| 658 | }).middleware(EnvironmentAuthenticatedAuth), |
| 659 | ) |
| 660 | .add( |
| 661 | HttpApiEndpoint.get("threadHistoryPage", "/api/orchestration/threads/:threadId/history", { |
| 662 | headers: OrchestrationProtocolHeaders, |
| 663 | params: EnvironmentOrchestrationThreadSnapshotParams, |
| 664 | query: EnvironmentOrchestrationThreadHistoryQuery, |
| 665 | success: OrchestrationV2ThreadHistoryPage, |
| 666 | error: EnvironmentOrchestrationThreadHistoryErrors, |
| 667 | }).middleware(EnvironmentAuthenticatedAuth), |
| 668 | ) {} |
| 669 | |
| 670 | class EnvironmentProjectsHttpApi extends HttpApiGroup.make("projects") |
| 671 | .add( |
| 672 | HttpApiEndpoint.get("snapshot", "/api/projects", { |
| 673 | headers: OptionalBearerHeaders, |
| 674 | success: ProjectSnapshot, |
| 675 | error: EnvironmentProjectSnapshotErrors, |
| 676 | }).middleware(EnvironmentAuthenticatedAuth), |
| 677 | ) |
| 678 | .add( |
| 679 | HttpApiEndpoint.post("mutate", "/api/projects/mutate", { |
| 680 | headers: OptionalBearerHeaders, |
| 681 | payload: ProjectMutation, |
| 682 | success: Project, |
| 683 | error: EnvironmentProjectMutationErrors, |
| 684 | }).middleware(EnvironmentAuthenticatedAuth), |
| 685 | ) {} |
| 686 | |
| 687 | /** Large, compressible pull-request payloads travel over HTTP rather than the RPC socket. */ |
| 688 | class EnvironmentPullRequestsHttpApi extends HttpApiGroup.make("pullRequests").add( |
| 689 | HttpApiEndpoint.post("diff", "/api/pull-requests/diff", { |
| 690 | headers: OptionalBearerHeaders, |
| 691 | payload: PullRequestDiffInput, |
| 692 | success: PullRequestDiffResult, |
| 693 | error: [ |
| 694 | PullRequestUnavailableError, |
| 695 | PullRequestOperationError, |
| 696 | EnvironmentAuthInvalidError, |
| 697 | EnvironmentScopeRequiredError, |
| 698 | EnvironmentInternalError, |
| 699 | ], |
| 700 | }).middleware(EnvironmentAuthenticatedAuth), |
| 701 | ) {} |
| 702 | |
| 703 | class EnvironmentConnectHttpApi extends HttpApiGroup.make("connect") |
| 704 | .add( |
| 705 | HttpApiEndpoint.post("linkProof", "/api/connect/link-proof", { |
| 706 | headers: OptionalBearerHeaders, |
| 707 | payload: RelayLinkProofRequest, |
| 708 | success: RelayEnvironmentLinkProof, |
| 709 | error: EnvironmentHttpCloudErrors, |
| 710 | }).middleware(EnvironmentAuthenticatedAuth), |
| 711 | ) |
| 712 | .add( |
| 713 | HttpApiEndpoint.post("relayConfig", "/api/connect/relay-config", { |
| 714 | headers: OptionalBearerHeaders, |
| 715 | payload: RelayEnvironmentConfigRequest, |
| 716 | success: EnvironmentCloudRelayConfigResult, |
| 717 | error: [...EnvironmentHttpCloudErrors, EnvironmentCloudEndpointUnavailableError], |
| 718 | }).middleware(EnvironmentAuthenticatedAuth), |
| 719 | ) |
| 720 | .add( |
| 721 | HttpApiEndpoint.get("linkState", "/api/connect/link-state", { |
| 722 | headers: OptionalBearerHeaders, |
| 723 | success: EnvironmentCloudLinkStateResult, |
| 724 | error: EnvironmentHttpCloudErrors, |
| 725 | }).middleware(EnvironmentAuthenticatedAuth), |
| 726 | ) |
| 727 | .add( |
| 728 | HttpApiEndpoint.post("unlink", "/api/connect/unlink", { |
| 729 | headers: OptionalBearerHeaders, |
| 730 | success: EnvironmentCloudRelayConfigResult, |
| 731 | error: EnvironmentHttpCloudErrors, |
| 732 | }).middleware(EnvironmentAuthenticatedAuth), |
| 733 | ) |
| 734 | .add( |
| 735 | HttpApiEndpoint.post("preferences", "/api/connect/preferences", { |
| 736 | headers: OptionalBearerHeaders, |
| 737 | payload: EnvironmentCloudPreferencesRequest, |
| 738 | success: EnvironmentCloudLinkStateResult, |
| 739 | error: EnvironmentHttpCloudErrors, |
| 740 | }).middleware(EnvironmentAuthenticatedAuth), |
| 741 | ) |
| 742 | .add( |
| 743 | HttpApiEndpoint.post("health", "/api/t3-connect/health", { |
| 744 | payload: RelayCloudEnvironmentHealthRequest, |
| 745 | success: RelayEnvironmentHealthResponse, |
| 746 | error: EnvironmentHttpCloudErrors, |
| 747 | }), |
| 748 | ) |
| 749 | .add( |
| 750 | HttpApiEndpoint.post("mintCredential", "/api/connect/mint-credential", { |
| 751 | payload: RelayCloudMintCredentialRequest, |
| 752 | success: RelayEnvironmentMintResponse, |
| 753 | error: EnvironmentHttpCloudErrors, |
| 754 | }), |
| 755 | ) |
| 756 | .add( |
| 757 | HttpApiEndpoint.post("t3MintCredential", "/api/t3-connect/mint-credential", { |
| 758 | payload: RelayCloudMintCredentialRequest, |
| 759 | success: RelayEnvironmentMintResponse, |
| 760 | error: EnvironmentHttpCloudErrors, |
| 761 | }), |
| 762 | ) {} |
| 763 | |
| 764 | /** |
| 765 | * Public entry point for webhook tasks. Unauthenticated by design: the token |
| 766 | * in the path, and an optional body signature, are the credential. The handler |
| 767 | * reads the raw body itself so a signature is checked over the exact bytes. |
| 768 | */ |
| 769 | const WebhookParams = Schema.Struct({ |
| 770 | hookId: TrimmedNonEmptyString, |
| 771 | token: TrimmedNonEmptyString, |
| 772 | }); |
| 773 | const WebhookAccepted = Schema.Struct({ deliveryId: TrimmedNonEmptyString }).pipe( |
| 774 | HttpApiSchema.status(202), |
| 775 | ); |
| 776 | const webhookEndpoint = { |
| 777 | params: WebhookParams, |
| 778 | success: WebhookAccepted, |
| 779 | } as const; |
| 780 | const WEBHOOK_PATH = "/api/hooks/:hookId/:token"; |
| 781 | |
| 782 | class EnvironmentWebhooksHttpApi extends HttpApiGroup.make("webhooks") |
| 783 | .add(HttpApiEndpoint.post("webhookPost", WEBHOOK_PATH, webhookEndpoint)) |
| 784 | .add(HttpApiEndpoint.put("webhookPut", WEBHOOK_PATH, webhookEndpoint)) |
| 785 | .add(HttpApiEndpoint.patch("webhookPatch", WEBHOOK_PATH, webhookEndpoint)) |
| 786 | .add(HttpApiEndpoint.get("webhookGet", WEBHOOK_PATH, webhookEndpoint)) {} |
| 787 | |
| 788 | export class EnvironmentHttpApi extends HttpApi.make("environment") |
| 789 | .add(EnvironmentMetadataHttpApi) |
| 790 | .add(EnvironmentAuthHttpApi) |
| 791 | .add(EnvironmentMcpOAuthHttpApi) |
| 792 | .add(EnvironmentOrchestrationHttpApi) |
| 793 | .add(EnvironmentPullRequestsHttpApi) |
| 794 | .add(EnvironmentProjectsHttpApi) |
| 795 | .add(EnvironmentConnectHttpApi) |
| 796 | .add(EnvironmentWebhooksHttpApi) {} |