Skip to content
Source
Sign inSign up
Open History to see the last commit

environmentHttp.ts

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