Skip to content
Source
Sign inSign up

environmentHttp.ts

1,031 lines36.5 KB

1import * as Context from "effect/Context";
2import type * as DateTime from "effect/DateTime";
3import * as Schema from "effect/Schema";
4import * as HttpApi from "effect/http-api/HttpApi";
5import * as HttpApiEndpoint from "effect/http-api/HttpApiEndpoint";
6import * as HttpApiGroup from "effect/http-api/HttpApiGroup";
7import * as HttpApiMiddleware from "effect/http-api/HttpApiMiddleware";
8import * as HttpApiSchema from "effect/http-api/HttpApiSchema";
9import * as HttpServerRespondable from "effect/http/HttpServerRespondable";
10import * as HttpServerResponse from "effect/http/HttpServerResponse";
11​
12import {
13 AuthAccessTokenResult,
14 AuthBrowserSessionRequest,
15 AuthBrowserSessionResult,
16 AuthCreatePairingCredentialInput,
17 AuthMcpApprovalDecisionRequest,
18 AuthMcpApprovalDetails,
19 AuthMcpApprovalError,
20 AuthMcpApprovalRedirect,
21 AuthMcpAuthorizationRequest,
22 AuthMcpAuthorizationServerMetadata,
23 AuthMcpClientRegistration,
24 AuthMcpProtectedResourceMetadata,
25 AuthMcpRegisteredClient,
26 AuthMcpRegistrationError,
27 AuthMcpTokenError,
28 AuthMcpTokenRequest,
29 AuthMcpTokenResult,
30 AuthPairingCredentialResult,
31 AuthRevokeClientSessionInput,
32 AuthRevokePairingLinkInput,
33 AuthEnvironmentScope,
34 AuthTokenExchangeRequest,
35 AuthSessionState,
36 AuthWebSocketTicketParams,
37 AuthWebSocketTicketResult,
38 ServerAuthSessionMethod,
39} from "./auth.ts";
40import {
41 ExecutionEnvironmentDescriptor,
42 ORCHESTRATION_PROTOCOL_HEADER,
43 ORCHESTRATION_PROTOCOL_VERSION_TEXT,
44} from "./environment.ts";
45import {
46 DpopFailureReason,
47 AuthSessionId,
48 ThreadId,
49 TrimmedNonEmptyString,
50} from "./baseSchemas.ts";
51import {
52 CloudSpeechApiKey,
53 CloudSpeechError,
54 CloudSpeechKeys,
55 CloudSpeechModelId,
56 CloudSpeechModels,
57 CloudSpeechProvider,
58 EnvironmentSpeechAccelerationRequest,
59 EnvironmentSpeechModelLinkRequest,
60 EnvironmentSpeechModelRequest,
61 EnvironmentSpeechModels,
62 EnvironmentSpeechModelUnloadTimeoutRequest,
63 EnvironmentSpeechPostProcessingRequest,
64 EnvironmentSpeechPostProcessingResult,
65 EnvironmentSpeechStatus,
66 EnvironmentSpeechTranscriptionResult,
67} from "./speech.ts";
68import { SecretReference } from "./secretReference.ts";
69import {
70 OrchestrationV2ShellHttpSnapshot,
71 OrchestrationV2ShellResumable,
72 OrchestrationV2ThreadBoundedSnapshot,
73 OrchestrationV2ThreadDetailSnapshot,
74 OrchestrationV2ThreadHistoryPage,
75 OrchestrationV2ThreadTranscript,
76} from "./orchestrationV2.ts";
77import { Project, ProjectMutation, ProjectSnapshot } from "./project.ts";
78import {
79 PullRequestDiffInput,
80 PullRequestDiffResult,
81 PullRequestOperationError,
82 PullRequestUnavailableError,
83} from "./pullRequest.ts";
84import {
85 RelayCloudEnvironmentHealthRequest,
86 RelayCloudMintCredentialRequest,
87 RelayEnvironmentConfigRequest,
88 RelayEnvironmentHealthResponse,
89 RelayEnvironmentLinkProof,
90 RelayEnvironmentMintResponse,
91 RelayLinkProofRequest,
92} from "./relay.ts";
93​
94const OptionalBearerHeaders = Schema.Struct({
95 authorization: Schema.optionalKey(Schema.String),
96 dpop: Schema.optionalKey(Schema.String),
97});
98​
99const OrchestrationProtocolHeaders = Schema.Struct({
100 authorization: Schema.optionalKey(Schema.String),
101 dpop: Schema.optionalKey(Schema.String),
102 [ORCHESTRATION_PROTOCOL_HEADER]: Schema.Literal(ORCHESTRATION_PROTOCOL_VERSION_TEXT),
103});
104​
105const OptionalDpopProofHeaders = Schema.Struct({
106 dpop: Schema.optionalKey(Schema.String),
107});
108​
109export const EnvironmentRequestInvalidReason = Schema.Literals([
110 "invalid_scope",
111 "scope_not_granted",
112 "invalid_command",
113 "invalid_history_cursor",
114 "thread_transcript_too_large",
115 "invalid_audio",
116 "speech_unavailable",
117 "speech_busy",
118]);
119export type EnvironmentRequestInvalidReason = typeof EnvironmentRequestInvalidReason.Type;
120​
121export const EnvironmentAuthInvalidReason = Schema.Literals([
122 "missing_credential",
123 "invalid_credential",
124]);
125export type EnvironmentAuthInvalidReason = typeof EnvironmentAuthInvalidReason.Type;
126​
127export const EnvironmentOperationForbiddenReason = Schema.Literals([
128 "current_session_revoke_not_allowed",
129]);
130export type EnvironmentOperationForbiddenReason = typeof EnvironmentOperationForbiddenReason.Type;
131​
132export const EnvironmentInternalErrorReason = Schema.Literals([
133 "bootstrap_validation_failed",
134 "browser_session_issuance_failed",
135 "browser_session_cookie_failed",
136 "access_token_issuance_failed",
137 "websocket_ticket_issuance_failed",
138 "pairing_credential_issuance_failed",
139 "pairing_links_load_failed",
140 "pairing_link_revoke_failed",
141 "client_sessions_load_failed",
142 "client_session_revoke_failed",
143 "project_snapshot_failed",
144 "project_mutation_failed",
145 "orchestration_snapshot_failed",
146 "orchestration_thread_snapshot_failed",
147 "orchestration_thread_bounded_snapshot_failed",
148 "orchestration_thread_history_failed",
149 "internal_error",
150]);
151export type EnvironmentInternalErrorReason = typeof EnvironmentInternalErrorReason.Type;
152​
153export class EnvironmentRequestInvalidError extends Schema.TaggedError<EnvironmentRequestInvalidError>()(
154 "EnvironmentRequestInvalidError",
155 {
156 code: Schema.Literal("invalid_request"),
157 reason: EnvironmentRequestInvalidReason,
158 traceId: TrimmedNonEmptyString,
159 },
160 { httpApiStatus: 400 },
161) {
162 [HttpServerRespondable.symbol]() {
163 return HttpServerResponse.schemaJson(EnvironmentRequestInvalidError)(this, { status: 400 });
164 }
165​
166 override get message(): string {
167 return `The environment rejected the request (${this.reason}).`;
168 }
170​
171export class EnvironmentAuthInvalidError extends Schema.TaggedError<EnvironmentAuthInvalidError>()(
172 "EnvironmentAuthInvalidError",
173 {
174 code: Schema.Literal("auth_invalid"),
175 reason: EnvironmentAuthInvalidReason,
176 dpopFailureReason: Schema.optionalKey(DpopFailureReason),
177 traceId: TrimmedNonEmptyString,
178 },
179 { httpApiStatus: 401 },
180) {
181 [HttpServerRespondable.symbol]() {
182 return HttpServerResponse.schemaJson(EnvironmentAuthInvalidError)(this, { status: 401 });
183 }
184​
185 override get message(): string {
186 return `The environment rejected this client's credentials (${this.reason}).`;
187 }
189​
190export class EnvironmentScopeRequiredError extends Schema.TaggedError<EnvironmentScopeRequiredError>()(
191 "EnvironmentScopeRequiredError",
192 {
193 code: Schema.Literal("insufficient_scope"),
194 requiredScope: AuthEnvironmentScope,
195 requiredPermission: Schema.optionalKey(Schema.String),
196 traceId: TrimmedNonEmptyString,
197 },
198 { httpApiStatus: 403 },
199) {
200 [HttpServerRespondable.symbol]() {
201 return HttpServerResponse.schemaJson(EnvironmentScopeRequiredError)(this, { status: 403 });
202 }
203​
204 override get message(): string {
205 return `This request needs the ${this.requiredPermission ?? this.requiredScope} scope, which this client does not have.`;
206 }
208​
209export class EnvironmentOperationForbiddenError extends Schema.TaggedError<EnvironmentOperationForbiddenError>()(
210 "EnvironmentOperationForbiddenError",
211 {
212 code: Schema.Literal("operation_forbidden"),
213 reason: EnvironmentOperationForbiddenReason,
214 traceId: TrimmedNonEmptyString,
215 },
216 { httpApiStatus: 403 },
217) {
218 [HttpServerRespondable.symbol]() {
219 return HttpServerResponse.schemaJson(EnvironmentOperationForbiddenError)(this, { status: 403 });
220 }
221​
222 override get message(): string {
223 return `The environment refused this operation (${this.reason}).`;
224 }
226​
227export class EnvironmentInternalError extends Schema.TaggedError<EnvironmentInternalError>()(
228 "EnvironmentInternalError",
229 {
230 code: Schema.Literal("internal_error"),
231 reason: EnvironmentInternalErrorReason,
232 traceId: TrimmedNonEmptyString,
233 },
234 { httpApiStatus: 500 },
235) {
236 [HttpServerRespondable.symbol]() {
237 return HttpServerResponse.schemaJson(EnvironmentInternalError)(this, { status: 500 });
238 }
239​
240 override get message(): string {
241 return `The environment failed to answer this request (${this.reason}).`;
242 }
244​
245export const EnvironmentResourceNotFoundReason = Schema.Literals(["thread_not_found"]);
246export type EnvironmentResourceNotFoundReason = typeof EnvironmentResourceNotFoundReason.Type;
247​
248export class EnvironmentResourceNotFoundError extends Schema.TaggedError<EnvironmentResourceNotFoundError>()(
249 "EnvironmentResourceNotFoundError",
250 {
251 code: Schema.Literal("not_found"),
252 reason: EnvironmentResourceNotFoundReason,
253 traceId: TrimmedNonEmptyString,
254 },
255 { httpApiStatus: 404 },
256) {
257 [HttpServerRespondable.symbol]() {
258 return HttpServerResponse.schemaJson(EnvironmentResourceNotFoundError)(this, { status: 404 });
259 }
260​
261 override get message(): string {
262 return `The environment could not find what this request named (${this.reason}).`;
263 }
265​
266export const EnvironmentHttpCommonError = Schema.Union([
267 EnvironmentRequestInvalidError,
268 EnvironmentAuthInvalidError,
269 EnvironmentScopeRequiredError,
270 EnvironmentOperationForbiddenError,
271 EnvironmentResourceNotFoundError,
272 EnvironmentInternalError,
273]);
274export type EnvironmentHttpCommonError = typeof EnvironmentHttpCommonError.Type;
275​
276const EnvironmentAuthenticationErrors = [
277 EnvironmentAuthInvalidError,
278 EnvironmentInternalError,
279] as const;
280​
281export class EnvironmentHttpBadRequestError extends Schema.TaggedError<EnvironmentHttpBadRequestError>()(
282 "EnvironmentHttpBadRequestError",
283 {
284 message: Schema.String,
285 },
286 { httpApiStatus: 400 },
287) {
288 [HttpServerRespondable.symbol]() {
289 return HttpServerResponse.schemaJson(EnvironmentHttpBadRequestError)(this, { status: 400 });
290 }
292​
293export class EnvironmentHttpUnauthorizedError extends Schema.TaggedError<EnvironmentHttpUnauthorizedError>()(
294 "EnvironmentHttpUnauthorizedError",
295 {
296 message: Schema.String,
297 },
298 { httpApiStatus: 401 },
299) {
300 [HttpServerRespondable.symbol]() {
301 return HttpServerResponse.schemaJson(EnvironmentHttpUnauthorizedError)(this, { status: 401 });
302 }
304​
305export class EnvironmentHttpForbiddenError extends Schema.TaggedError<EnvironmentHttpForbiddenError>()(
306 "EnvironmentHttpForbiddenError",
307 {
308 message: Schema.String,
309 },
310 { httpApiStatus: 403 },
311) {
312 [HttpServerRespondable.symbol]() {
313 return HttpServerResponse.schemaJson(EnvironmentHttpForbiddenError)(this, { status: 403 });
314 }
316​
317export class EnvironmentHttpInternalServerError extends Schema.TaggedError<EnvironmentHttpInternalServerError>()(
318 "EnvironmentHttpInternalServerError",
319 {
320 message: Schema.String,
321 },
322 { httpApiStatus: 500 },
323) {
324 [HttpServerRespondable.symbol]() {
325 return HttpServerResponse.schemaJson(EnvironmentHttpInternalServerError)(this, { status: 500 });
326 }
328​
329export class EnvironmentHttpConflictError extends Schema.TaggedError<EnvironmentHttpConflictError>()(
330 "EnvironmentHttpConflictError",
331 {
332 message: Schema.String,
333 },
334 { httpApiStatus: 409 },
335) {
336 [HttpServerRespondable.symbol]() {
337 return HttpServerResponse.schemaJson(EnvironmentHttpConflictError)(this, { status: 409 });
338 }
340​
341export class EnvironmentCloudEndpointUnavailableError extends Schema.TaggedError<EnvironmentCloudEndpointUnavailableError>()(
342 "EnvironmentCloudEndpointUnavailableError",
343 {
344 message: Schema.String,
345 endpointRuntimeStatus: Schema.Unknown,
346 },
347 { httpApiStatus: 503 },
348) {
349 [HttpServerRespondable.symbol]() {
350 return HttpServerResponse.schemaJson(EnvironmentCloudEndpointUnavailableError)(this, {
351 status: 503,
352 });
353 }
355const EnvironmentSessionCreationErrors = [
356 EnvironmentAuthInvalidError,
357 EnvironmentInternalError,
358] as const;
359const EnvironmentTokenExchangeErrors = [
360 EnvironmentRequestInvalidError,
361 EnvironmentAuthInvalidError,
362 EnvironmentInternalError,
363] as const;
364const EnvironmentScopedOperationErrors = [
365 EnvironmentScopeRequiredError,
366 EnvironmentInternalError,
367] as const;
368const EnvironmentPairingCredentialErrors = [
369 EnvironmentRequestInvalidError,
370 // The managed policy does not allow remote clients.
371 EnvironmentHttpForbiddenError,
372 ...EnvironmentScopedOperationErrors,
373] as const;
374const EnvironmentSessionRevokeErrors = [
375 EnvironmentScopeRequiredError,
376 EnvironmentOperationForbiddenError,
377 EnvironmentInternalError,
378] as const;
379const EnvironmentProjectSnapshotErrors = [
380 EnvironmentScopeRequiredError,
381 EnvironmentInternalError,
382] as const;
383const EnvironmentOrchestrationSnapshotErrors = [
384 EnvironmentScopeRequiredError,
385 EnvironmentInternalError,
386] as const;
387const EnvironmentOrchestrationThreadSnapshotErrors = [
388 EnvironmentScopeRequiredError,
389 EnvironmentResourceNotFoundError,
390 EnvironmentInternalError,
391] as const;
392const EnvironmentProjectMutationErrors = [
393 EnvironmentRequestInvalidError,
394 EnvironmentScopeRequiredError,
395 EnvironmentInternalError,
396] as const;
397​
398export interface EnvironmentSessionPrincipalShape {
399 readonly sessionId: AuthSessionId;
400 readonly subject: string;
401 readonly method: ServerAuthSessionMethod;
402 readonly scopes: ReadonlySet<AuthEnvironmentScope>;
403 readonly proofKeyThumbprint?: string;
404 readonly expiresAt?: DateTime.DateTime;
406​
407export class EnvironmentAuthenticatedPrincipal extends Context.Service<
408 EnvironmentAuthenticatedPrincipal,
409 EnvironmentSessionPrincipalShape
410>()("@t3tools/contracts/environmentHttp/EnvironmentAuthenticatedPrincipal") {}
411​
412export class EnvironmentAuthenticatedAuth extends HttpApiMiddleware.Service<
413 EnvironmentAuthenticatedAuth,
414 { provides: EnvironmentAuthenticatedPrincipal }
415>()("EnvironmentAuthenticatedAuth", {
416 error: EnvironmentAuthenticationErrors,
417}) {}
418​
419const EnvironmentHttpCloudErrors = [
420 EnvironmentHttpBadRequestError,
421 EnvironmentHttpUnauthorizedError,
422 EnvironmentHttpForbiddenError,
423 EnvironmentHttpConflictError,
424 EnvironmentHttpInternalServerError,
425 EnvironmentScopeRequiredError,
426] as const;
427​
428export const EnvironmentCloudRelayConfigResult = Schema.Struct({
429 ok: Schema.Boolean,
430 endpointRuntimeStatus: Schema.Unknown,
431});
432export type EnvironmentCloudRelayConfigResult = typeof EnvironmentCloudRelayConfigResult.Type;
433​
434export const EnvironmentConnectReachabilityCause = Schema.Literals([
435 "not_linked",
436 "connector_not_running",
437 "connector_not_ready",
438 "edge_unreachable",
439 "no_connector",
440 "origin_unreachable",
441 "dns_failed",
442 "timeout",
443 "unexpected_response",
444]);
445export type EnvironmentConnectReachabilityCause = typeof EnvironmentConnectReachabilityCause.Type;
446​
447/** Whether other devices can reach this environment through its T3 Connect tunnel. */
448export const EnvironmentConnectReachability = Schema.Union([
449 Schema.Struct({
450 status: Schema.Literal("reachable"),
451 checkedAt: Schema.String,
452 /** Null when cloudflared did not report a count. */
453 readyConnections: Schema.NullOr(Schema.Int),
454 protocol: Schema.NullOr(Schema.String),
455 }),
456 Schema.Struct({
457 status: Schema.Literal("unreachable"),
458 checkedAt: Schema.String,
459 cause: EnvironmentConnectReachabilityCause,
460 reason: Schema.String,
461 hint: Schema.NullOr(Schema.String),
462 fix: Schema.String,
463 }),
464]);
465export type EnvironmentConnectReachability = typeof EnvironmentConnectReachability.Type;
466​
467/** "Tunnel connected · 4 connections · QUIC" for a healthy check. */
468export function formatConnectReachable(
469 result: Extract<EnvironmentConnectReachability, { status: "reachable" }>,
470): string {
471 const count = result.readyConnections;
472 return [
473 "Tunnel connected",
474 ...(count === null ? [] : [`${count} connection${count === 1 ? "" : "s"}`]),
475 ...(result.protocol ? [result.protocol.toUpperCase()] : []),
476 ].join(" · ");
478​
479/**
480 * Sections of docs/user/t3-connect-troubleshooting.md. Each is the GitHub anchor of a
481 * heading there; the test fails when a heading is renamed without updating this list.
482 */
483export const T3_CONNECT_TROUBLESHOOTING_SECTIONS = [
484 "the-hosts-tunnel-is-offline",
485 "port-7844-is-blocked-on-the-host",
486 "the-tunnel-works-but-t3-code-does-not-answer",
487 "the-t3-connect-address-does-not-resolve-or-answer",
488 "this-device-cant-reach-t3-connect",
489 "sign-in-account-or-link-problems",
490 "the-clock-is-off",
491 "the-host-runs-an-older-t3-code",
492 "t3-connect-returned-an-error",
493] as const;
494export type T3ConnectTroubleshootingSection = (typeof T3_CONNECT_TROUBLESHOOTING_SECTIONS)[number];
495​
496/** Clients, the CLI, and server logs link here as "More info". */
497export function t3ConnectTroubleshootingUrl(section: T3ConnectTroubleshootingSection): string {
498 return `https://github.com/pingdotgg/t3code/blob/main/docs/user/t3-connect-troubleshooting.md#${section}`;
500​
501const REACHABILITY_SECTION = {
502 not_linked: "sign-in-account-or-link-problems",
503 connector_not_running: "the-hosts-tunnel-is-offline",
504 connector_not_ready: "the-hosts-tunnel-is-offline",
505 no_connector: "the-hosts-tunnel-is-offline",
506 edge_unreachable: "port-7844-is-blocked-on-the-host",
507 origin_unreachable: "the-tunnel-works-but-t3-code-does-not-answer",
508 unexpected_response: "the-tunnel-works-but-t3-code-does-not-answer",
509 dns_failed: "the-t3-connect-address-does-not-resolve-or-answer",
510 timeout: "the-t3-connect-address-does-not-resolve-or-answer",
511} satisfies Record<EnvironmentConnectReachabilityCause, T3ConnectTroubleshootingSection>;
512​
513export function connectReachabilityHelpUrl(cause: EnvironmentConnectReachabilityCause): string {
514 return t3ConnectTroubleshootingUrl(REACHABILITY_SECTION[cause]);
516​
517export const EnvironmentCloudLinkStateResult = Schema.Struct({
518 linked: Schema.Boolean,
519 cloudUserId: Schema.NullOr(Schema.String),
520 relayUrl: Schema.NullOr(Schema.String),
521 relayIssuer: Schema.NullOr(Schema.String),
522 managedTunnelActive: Schema.optional(Schema.Boolean),
523 publishAgentActivity: Schema.Boolean,
524 holdWebhooksWhileOffline: Schema.optional(Schema.Boolean),
525 linkRevoked: Schema.optional(Schema.Boolean),
526 /** The latest check the server ran on its own, absent until one finishes. */
527 reachability: Schema.optional(EnvironmentConnectReachability),
528 /** The tunnel's public hostname, once cloudflared has connected. Older servers omit it. */
529 tunnelHostname: Schema.optional(Schema.String),
530 /** Whether this is the desktop app's own session, which may link from where it is. */
531 linkableHere: Schema.optional(Schema.Boolean),
532});
533export type EnvironmentCloudLinkStateResult = typeof EnvironmentCloudLinkStateResult.Type;
534​
535export const EnvironmentCloudPreferencesRequest = Schema.Struct({
536 publishAgentActivity: Schema.Boolean,
537 holdWebhooksWhileOffline: Schema.optional(Schema.Boolean),
538});
539export type EnvironmentCloudPreferencesRequest = typeof EnvironmentCloudPreferencesRequest.Type;
540​
541export const AuthPairingLinkRevokeResult = Schema.Struct({
542 revoked: Schema.Boolean,
543});
544export type AuthPairingLinkRevokeResult = typeof AuthPairingLinkRevokeResult.Type;
545​
546export const AuthClientSessionRevokeResult = Schema.Struct({
547 revoked: Schema.Boolean,
548});
549export type AuthClientSessionRevokeResult = typeof AuthClientSessionRevokeResult.Type;
550​
551export const AuthOtherClientSessionsRevokeResult = Schema.Struct({
552 revokedCount: Schema.Number,
553});
554export type AuthOtherClientSessionsRevokeResult = typeof AuthOtherClientSessionsRevokeResult.Type;
555​
556class EnvironmentMetadataHttpApi extends HttpApiGroup.make("metadata").add(
557 HttpApiEndpoint.get("descriptor", "/.well-known/t3/environment", {
558 success: ExecutionEnvironmentDescriptor,
559 }),
560) {}
561​
562class EnvironmentAuthHttpApi extends HttpApiGroup.make("auth")
563 .add(
564 HttpApiEndpoint.get("session", "/api/auth/session", {
565 headers: OptionalBearerHeaders,
566 success: AuthSessionState,
567 error: [EnvironmentInternalError],
568 }),
569 )
570 .add(
571 HttpApiEndpoint.post("browserSession", "/api/auth/browser-session", {
572 payload: AuthBrowserSessionRequest,
573 success: AuthBrowserSessionResult,
574 error: EnvironmentSessionCreationErrors,
575 }),
576 )
577 .add(
578 HttpApiEndpoint.post("token", "/oauth/token", {
579 headers: OptionalDpopProofHeaders,
580 payload: AuthTokenExchangeRequest,
581 success: AuthAccessTokenResult,
582 error: EnvironmentTokenExchangeErrors,
583 }),
584 )
585 .add(
586 HttpApiEndpoint.post("webSocketTicket", "/api/auth/websocket-ticket", {
587 headers: OptionalBearerHeaders,
588 success: AuthWebSocketTicketResult,
589 error: [EnvironmentInternalError],
590 }).middleware(EnvironmentAuthenticatedAuth),
591 )
592 .add(
593 HttpApiEndpoint.post("webSocketTicketFor", "/api/auth/websocket-ticket/:use", {
594 headers: OptionalBearerHeaders,
595 params: AuthWebSocketTicketParams,
596 success: AuthWebSocketTicketResult,
597 error: [EnvironmentInternalError],
598 }).middleware(EnvironmentAuthenticatedAuth),
599 )
600 .add(
601 HttpApiEndpoint.post("pairingCredential", "/api/auth/pairing-token", {
602 headers: OptionalBearerHeaders,
603 payload: AuthCreatePairingCredentialInput,
604 success: AuthPairingCredentialResult,
605 error: EnvironmentPairingCredentialErrors,
606 }).middleware(EnvironmentAuthenticatedAuth),
607 )
608 .add(
609 HttpApiEndpoint.post("revokePairingLink", "/api/auth/pairing-links/revoke", {
610 headers: OptionalBearerHeaders,
611 payload: AuthRevokePairingLinkInput,
612 success: AuthPairingLinkRevokeResult,
613 error: EnvironmentScopedOperationErrors,
614 }).middleware(EnvironmentAuthenticatedAuth),
615 )
616 .add(
617 HttpApiEndpoint.post("revokeClient", "/api/auth/clients/revoke", {
618 headers: OptionalBearerHeaders,
619 payload: AuthRevokeClientSessionInput,
620 success: AuthClientSessionRevokeResult,
621 error: EnvironmentSessionRevokeErrors,
622 }).middleware(EnvironmentAuthenticatedAuth),
623 )
624 .add(
625 HttpApiEndpoint.post("revokeOtherClients", "/api/auth/clients/revoke-others", {
626 headers: OptionalBearerHeaders,
627 success: AuthOtherClientSessionsRevokeResult,
628 error: EnvironmentScopedOperationErrors,
629 }).middleware(EnvironmentAuthenticatedAuth),
630 ) {}
631​
632class EnvironmentMcpOAuthHttpApi extends HttpApiGroup.make("mcpOAuth")
633 .add(
634 HttpApiEndpoint.get("protectedResource", "/.well-known/oauth-protected-resource", {
635 success: AuthMcpProtectedResourceMetadata,
636 }),
637 )
638 .add(
639 HttpApiEndpoint.get("mcpProtectedResource", "/.well-known/oauth-protected-resource/mcp", {
640 success: AuthMcpProtectedResourceMetadata,
641 }),
642 )
643 .add(
644 HttpApiEndpoint.get("authorizationServer", "/.well-known/oauth-authorization-server", {
645 success: AuthMcpAuthorizationServerMetadata,
646 }),
647 )
648 .add(
649 HttpApiEndpoint.post("register", "/oauth/mcp/register", {
650 payload: AuthMcpClientRegistration,
651 success: AuthMcpRegisteredClient,
652 error: AuthMcpRegistrationError,
653 }),
654 )
655 .add(HttpApiEndpoint.get("authorize", "/oauth/mcp/authorize"))
656 .add(
657 HttpApiEndpoint.post("approval", "/oauth/mcp/approval", {
658 // The hosted app sends the owner's T3 Connect credential; the environment's own page uses its cookie.
659 headers: OptionalBearerHeaders,
660 payload: AuthMcpAuthorizationRequest,
661 success: [AuthMcpApprovalDetails, AuthMcpApprovalRedirect],
662 error: AuthMcpApprovalError,
663 }),
664 )
665 .add(
666 HttpApiEndpoint.post("decision", "/oauth/mcp/decision", {
667 // The hosted app sends the owner's T3 Connect credential; the environment's own page uses its cookie.
668 headers: OptionalBearerHeaders,
669 payload: AuthMcpApprovalDecisionRequest,
670 success: AuthMcpApprovalRedirect,
671 error: AuthMcpApprovalError,
672 }),
673 )
674 .add(
675 HttpApiEndpoint.post("token", "/oauth/mcp/token", {
676 payload: AuthMcpTokenRequest,
677 success: AuthMcpTokenResult,
678 error: AuthMcpTokenError,
679 }),
680 ) {}
681​
682const EnvironmentOrchestrationThreadSnapshotParams = Schema.Struct({
683 threadId: ThreadId,
684});
685​
686export const EnvironmentOrchestrationShellSnapshotQuery = Schema.Struct({
687 omitIdleSubagents: Schema.optionalKey(Schema.String),
688 omitOlderSettled: Schema.optionalKey(Schema.String),
689 /**
690 * The cursor of rows the client holds, loaded with the same omissions. The server answers
691 * `resumable` instead of a snapshot when the socket can replay from it.
692 */
693 resumeFrom: Schema.optionalKey(Schema.String),
694});
695​
696export const EnvironmentOrchestrationThreadBoundedSnapshotQuery = Schema.Struct({
697 deferWork: Schema.optionalKey(Schema.String),
698 compactTurnItems: Schema.optionalKey(Schema.String),
699 dedupeBodies: Schema.optionalKey(Schema.String),
700 compactIds: Schema.optionalKey(Schema.String),
701});
702​
703export const EnvironmentOrchestrationThreadHistoryQuery = Schema.Struct({
704 cursor: TrimmedNonEmptyString,
705 throughEntryId: Schema.optional(TrimmedNonEmptyString),
706 view: Schema.optional(Schema.Literals(["conversation", "activity", "work"])),
707 deferWork: Schema.optionalKey(Schema.String),
708 compactIds: Schema.optionalKey(Schema.String),
709});
710​
711const EnvironmentOrchestrationThreadHistoryErrors = [
712 EnvironmentRequestInvalidError,
713 EnvironmentScopeRequiredError,
714 EnvironmentResourceNotFoundError,
715 EnvironmentInternalError,
716] as const;
717​
718class EnvironmentOrchestrationHttpApi extends HttpApiGroup.make("orchestration")
719 .add(
720 HttpApiEndpoint.get("shellSnapshot", "/api/orchestration/shell", {
721 headers: OrchestrationProtocolHeaders,
722 query: EnvironmentOrchestrationShellSnapshotQuery,
723 success: [OrchestrationV2ShellHttpSnapshot, OrchestrationV2ShellResumable],
724 error: EnvironmentOrchestrationSnapshotErrors,
725 }).middleware(EnvironmentAuthenticatedAuth),
726 )
727 .add(
728 HttpApiEndpoint.get("threadSnapshot", "/api/orchestration/threads/:threadId", {
729 headers: OrchestrationProtocolHeaders,
730 params: EnvironmentOrchestrationThreadSnapshotParams,
731 success: OrchestrationV2ThreadDetailSnapshot,
732 error: EnvironmentOrchestrationThreadSnapshotErrors,
733 }).middleware(EnvironmentAuthenticatedAuth),
734 )
735 .add(
736 HttpApiEndpoint.get("threadTranscript", "/api/orchestration/threads/:threadId/transcript", {
737 headers: OrchestrationProtocolHeaders,
738 params: EnvironmentOrchestrationThreadSnapshotParams,
739 success: OrchestrationV2ThreadTranscript,
740 error: [EnvironmentRequestInvalidError, ...EnvironmentOrchestrationThreadSnapshotErrors],
741 }).middleware(EnvironmentAuthenticatedAuth),
742 )
743 .add(
744 HttpApiEndpoint.get("threadBoundedSnapshot", "/api/orchestration/threads/:threadId/bounded", {
745 headers: OrchestrationProtocolHeaders,
746 params: EnvironmentOrchestrationThreadSnapshotParams,
747 query: EnvironmentOrchestrationThreadBoundedSnapshotQuery,
748 success: OrchestrationV2ThreadBoundedSnapshot,
749 error: EnvironmentOrchestrationThreadSnapshotErrors,
750 }).middleware(EnvironmentAuthenticatedAuth),
751 )
752 .add(
753 HttpApiEndpoint.get("threadHistoryPage", "/api/orchestration/threads/:threadId/history", {
754 headers: OrchestrationProtocolHeaders,
755 params: EnvironmentOrchestrationThreadSnapshotParams,
756 query: EnvironmentOrchestrationThreadHistoryQuery,
757 success: OrchestrationV2ThreadHistoryPage,
758 error: EnvironmentOrchestrationThreadHistoryErrors,
759 }).middleware(EnvironmentAuthenticatedAuth),
760 ) {}
761​
762class EnvironmentProjectsHttpApi extends HttpApiGroup.make("projects")
763 .add(
764 HttpApiEndpoint.get("snapshot", "/api/projects", {
765 headers: OptionalBearerHeaders,
766 success: ProjectSnapshot,
767 error: EnvironmentProjectSnapshotErrors,
768 }).middleware(EnvironmentAuthenticatedAuth),
769 )
770 .add(
771 HttpApiEndpoint.post("mutate", "/api/projects/mutate", {
772 headers: OptionalBearerHeaders,
773 payload: ProjectMutation,
774 success: Project,
775 error: EnvironmentProjectMutationErrors,
776 }).middleware(EnvironmentAuthenticatedAuth),
777 ) {}
778​
779class EnvironmentPullRequestsHttpApi extends HttpApiGroup.make("pullRequests").add(
780 HttpApiEndpoint.post("diff", "/api/pull-requests/diff", {
781 headers: OptionalBearerHeaders,
782 payload: PullRequestDiffInput,
783 success: PullRequestDiffResult,
784 error: [
785 PullRequestUnavailableError,
786 PullRequestOperationError,
787 EnvironmentAuthInvalidError,
788 EnvironmentScopeRequiredError,
789 EnvironmentInternalError,
790 ],
791 }).middleware(EnvironmentAuthenticatedAuth),
792) {}
793​
794export class EnvironmentVoiceBodyLimit extends HttpApiMiddleware.Service<EnvironmentVoiceBodyLimit>()(
795 "EnvironmentVoiceBodyLimit",
796 { error: EnvironmentRequestInvalidError },
797) {}
798​
799export class EnvironmentVoiceHttpApi extends HttpApiGroup.make("voice")
800 .add(
801 HttpApiEndpoint.get("cloudKeys", "/api/voice/cloud/keys", {
802 headers: OptionalBearerHeaders,
803 success: CloudSpeechKeys,
804 error: EnvironmentScopedOperationErrors,
805 }).middleware(EnvironmentAuthenticatedAuth),
806 )
807 .add(
808 HttpApiEndpoint.post("setCloudKey", "/api/voice/cloud/:provider/key", {
809 headers: OptionalBearerHeaders,
810 params: Schema.Struct({ provider: CloudSpeechProvider }),
811 payload: Schema.Struct({
812 key: Schema.NullOr(
813 Schema.Union([Schema.RedactedFromValue(CloudSpeechApiKey), SecretReference]),
814 ),
815 }),
816 success: HttpApiSchema.NoContent,
817 error: EnvironmentScopedOperationErrors,
818 }).middleware(EnvironmentAuthenticatedAuth),
819 )
820 .add(
821 // Public catalog; carries no credential and no audio.
822 HttpApiEndpoint.get("cloudModels", "/api/voice/cloud/openrouter/models", {
823 headers: OptionalBearerHeaders,
824 success: CloudSpeechModels,
825 error: [...EnvironmentScopedOperationErrors, CloudSpeechError],
826 }).middleware(EnvironmentAuthenticatedAuth),
827 )
828 .add(
829 HttpApiEndpoint.post("transcribeCloud", "/api/voice/cloud/:provider/transcribe", {
830 headers: OptionalBearerHeaders,
831 params: Schema.Struct({ provider: CloudSpeechProvider }),
832 query: Schema.Struct({ model: Schema.optionalKey(CloudSpeechModelId) }),
833 payload: Schema.Uint8Array.pipe(HttpApiSchema.asUint8Array()),
834 success: EnvironmentSpeechTranscriptionResult,
835 error: [
836 ...EnvironmentScopedOperationErrors,
837 EnvironmentRequestInvalidError,
838 CloudSpeechError,
839 ],
840 })
841 .middleware(EnvironmentVoiceBodyLimit)
842 .middleware(EnvironmentAuthenticatedAuth),
843 )
844 .add(
845 HttpApiEndpoint.get("status", "/api/voice/status", {
846 headers: OptionalBearerHeaders,
847 success: EnvironmentSpeechStatus,
848 error: [...EnvironmentScopedOperationErrors, EnvironmentRequestInvalidError],
849 }).middleware(EnvironmentAuthenticatedAuth),
850 )
851 .add(
852 HttpApiEndpoint.post("prepareModel", "/api/voice/models/prepare", {
853 headers: OptionalBearerHeaders,
854 success: Schema.Void,
855 error: [...EnvironmentScopedOperationErrors, EnvironmentRequestInvalidError],
856 }).middleware(EnvironmentAuthenticatedAuth),
857 )
858 .add(
859 HttpApiEndpoint.get("models", "/api/voice/models", {
860 headers: OptionalBearerHeaders,
861 query: { benchmarks: Schema.optionalKey(Schema.Literal("1")) },
862 success: EnvironmentSpeechModels,
863 error: [...EnvironmentScopedOperationErrors, EnvironmentRequestInvalidError],
864 }).middleware(EnvironmentAuthenticatedAuth),
865 )
866 .add(
867 HttpApiEndpoint.post("downloadModel", "/api/voice/models/download", {
868 headers: OptionalBearerHeaders,
869 payload: EnvironmentSpeechModelRequest,
870 success: EnvironmentSpeechModels,
871 error: [...EnvironmentScopedOperationErrors, EnvironmentRequestInvalidError],
872 }).middleware(EnvironmentAuthenticatedAuth),
873 )
874 .add(
875 HttpApiEndpoint.post("selectModel", "/api/voice/models/select", {
876 headers: OptionalBearerHeaders,
877 payload: EnvironmentSpeechModelRequest,
878 success: EnvironmentSpeechModels,
879 error: [...EnvironmentScopedOperationErrors, EnvironmentRequestInvalidError],
880 }).middleware(EnvironmentAuthenticatedAuth),
881 )
882 .add(
883 HttpApiEndpoint.post("cancelModelDownload", "/api/voice/models/cancel", {
884 headers: OptionalBearerHeaders,
885 payload: EnvironmentSpeechModelRequest,
886 success: EnvironmentSpeechModels,
887 error: [...EnvironmentScopedOperationErrors, EnvironmentRequestInvalidError],
888 }).middleware(EnvironmentAuthenticatedAuth),
889 )
890 .add(
891 HttpApiEndpoint.post("removeModel", "/api/voice/models/remove", {
892 headers: OptionalBearerHeaders,
893 payload: EnvironmentSpeechModelRequest,
894 success: EnvironmentSpeechModels,
895 error: [...EnvironmentScopedOperationErrors, EnvironmentRequestInvalidError],
896 }).middleware(EnvironmentAuthenticatedAuth),
897 )
898 .add(
899 HttpApiEndpoint.post("linkModels", "/api/voice/models/link", {
900 headers: OptionalBearerHeaders,
901 payload: EnvironmentSpeechModelLinkRequest,
902 success: EnvironmentSpeechModels,
903 error: [...EnvironmentScopedOperationErrors, EnvironmentRequestInvalidError],
904 }).middleware(EnvironmentAuthenticatedAuth),
905 )
906 .add(
907 HttpApiEndpoint.post("updateAcceleration", "/api/voice/acceleration", {
908 headers: OptionalBearerHeaders,
909 payload: EnvironmentSpeechAccelerationRequest,
910 success: EnvironmentSpeechStatus,
911 error: [...EnvironmentScopedOperationErrors, EnvironmentRequestInvalidError],
912 }).middleware(EnvironmentAuthenticatedAuth),
913 )
914 .add(
915 HttpApiEndpoint.post("updateModelUnloadTimeout", "/api/voice/model-unload-timeout", {
916 headers: OptionalBearerHeaders,
917 payload: EnvironmentSpeechModelUnloadTimeoutRequest,
918 success: EnvironmentSpeechStatus,
919 error: [...EnvironmentScopedOperationErrors, EnvironmentRequestInvalidError],
920 }).middleware(EnvironmentAuthenticatedAuth),
921 )
922 .add(
923 HttpApiEndpoint.post("transcribe", "/api/voice/transcribe", {
924 headers: OptionalBearerHeaders,
925 payload: Schema.Uint8Array.pipe(HttpApiSchema.asUint8Array()),
926 success: EnvironmentSpeechTranscriptionResult,
927 error: [...EnvironmentScopedOperationErrors, EnvironmentRequestInvalidError],
928 })
929 .middleware(EnvironmentVoiceBodyLimit)
930 .middleware(EnvironmentAuthenticatedAuth),
931 )
932 .add(
933 HttpApiEndpoint.post("postProcess", "/api/voice/post-process", {
934 headers: OptionalBearerHeaders,
935 payload: EnvironmentSpeechPostProcessingRequest,
936 success: EnvironmentSpeechPostProcessingResult,
937 error: EnvironmentScopedOperationErrors,
938 }).middleware(EnvironmentAuthenticatedAuth),
939 ) {}
940​
941class EnvironmentConnectHttpApi extends HttpApiGroup.make("connect")
942 .add(
943 HttpApiEndpoint.post("linkProof", "/api/connect/link-proof", {
944 headers: OptionalBearerHeaders,
945 payload: RelayLinkProofRequest,
946 success: RelayEnvironmentLinkProof,
947 error: EnvironmentHttpCloudErrors,
948 }).middleware(EnvironmentAuthenticatedAuth),
949 )
950 .add(
951 HttpApiEndpoint.post("relayConfig", "/api/connect/relay-config", {
952 headers: OptionalBearerHeaders,
953 payload: RelayEnvironmentConfigRequest,
954 success: EnvironmentCloudRelayConfigResult,
955 error: [...EnvironmentHttpCloudErrors, EnvironmentCloudEndpointUnavailableError],
956 }).middleware(EnvironmentAuthenticatedAuth),
957 )
958 .add(
959 HttpApiEndpoint.get("linkState", "/api/connect/link-state", {
960 headers: OptionalBearerHeaders,
961 success: EnvironmentCloudLinkStateResult,
962 error: EnvironmentHttpCloudErrors,
963 }).middleware(EnvironmentAuthenticatedAuth),
964 )
965 .add(
966 HttpApiEndpoint.post("checkReachability", "/api/connect/reachability", {
967 headers: OptionalBearerHeaders,
968 success: EnvironmentConnectReachability,
969 error: EnvironmentHttpCloudErrors,
970 }).middleware(EnvironmentAuthenticatedAuth),
971 )
972 .add(
973 HttpApiEndpoint.post("unlink", "/api/connect/unlink", {
974 headers: OptionalBearerHeaders,
975 success: EnvironmentCloudRelayConfigResult,
976 error: EnvironmentHttpCloudErrors,
977 }).middleware(EnvironmentAuthenticatedAuth),
978 )
979 .add(
980 HttpApiEndpoint.post("preferences", "/api/connect/preferences", {
981 headers: OptionalBearerHeaders,
982 payload: EnvironmentCloudPreferencesRequest,
983 success: EnvironmentCloudLinkStateResult,
984 error: EnvironmentHttpCloudErrors,
985 }).middleware(EnvironmentAuthenticatedAuth),
986 )
987 .add(
988 HttpApiEndpoint.post("health", "/api/t3-connect/health", {
989 payload: RelayCloudEnvironmentHealthRequest,
990 success: RelayEnvironmentHealthResponse,
991 error: EnvironmentHttpCloudErrors,
992 }),
993 )
994 .add(
995 HttpApiEndpoint.post("t3MintCredential", "/api/t3-connect/mint-credential", {
996 payload: RelayCloudMintCredentialRequest,
997 success: RelayEnvironmentMintResponse,
998 error: EnvironmentHttpCloudErrors,
999 }),
1000 ) {}
1001​
1002// Path tokens and optional body signatures authenticate webhooks; read raw bytes to verify signatures.
1003const WebhookParams = Schema.Struct({
1004 hookId: TrimmedNonEmptyString,
1005 token: TrimmedNonEmptyString,
1006});
1007const WebhookAccepted = Schema.Struct({ deliveryId: TrimmedNonEmptyString }).pipe(
1008 HttpApiSchema.status(202),
1010const webhookEndpoint = {
1011 params: WebhookParams,
1012 success: WebhookAccepted,
1013} as const;
1014const WEBHOOK_PATH = "/api/hooks/:hookId/:token";
1015​
1016class EnvironmentWebhooksHttpApi extends HttpApiGroup.make("webhooks")
1017 .add(HttpApiEndpoint.post("webhookPost", WEBHOOK_PATH, webhookEndpoint))
1018 .add(HttpApiEndpoint.put("webhookPut", WEBHOOK_PATH, webhookEndpoint))
1019 .add(HttpApiEndpoint.patch("webhookPatch", WEBHOOK_PATH, webhookEndpoint))
1020 .add(HttpApiEndpoint.get("webhookGet", WEBHOOK_PATH, webhookEndpoint)) {}
1021​
1022export class EnvironmentHttpApi extends HttpApi.make("environment")
1023 .add(EnvironmentMetadataHttpApi)
1024 .add(EnvironmentAuthHttpApi)
1025 .add(EnvironmentMcpOAuthHttpApi)
1026 .add(EnvironmentOrchestrationHttpApi)
1027 .add(EnvironmentPullRequestsHttpApi)
1028 .add(EnvironmentProjectsHttpApi)
1029 .add(EnvironmentVoiceHttpApi)
1030 .add(EnvironmentConnectHttpApi)
1031 .add(EnvironmentWebhooksHttpApi) {}