1import * as Context from "effect/Context";
2import * as Schema from "effect/Schema";
3import * as HttpApi from "effect/http-api/HttpApi";
4import * as HttpApiEndpoint from "effect/http-api/HttpApiEndpoint";
5import * as HttpApiGroup from "effect/http-api/HttpApiGroup";
6import * as HttpApiMiddleware from "effect/http-api/HttpApiMiddleware";
7import * as HttpApiSchema from "effect/http-api/HttpApiSchema";
8import * as HttpApiSecurity from "effect/http-api/HttpApiSecurity";
9import * as OpenApi from "effect/http-api/OpenApi";
10
11import {
12 DpopFailureReason,
13 EnvironmentId,
14 ThreadId,
15 TrimmedNonEmptyString,
16} from "./baseSchemas.ts";
17import { ExecutionEnvironmentDescriptor } from "./environment.ts";
18
19export const RelayAgentAwarenessPlatform = Schema.Literals(["ios", "android"]);
20export type RelayAgentAwarenessPlatform = typeof RelayAgentAwarenessPlatform.Type;
21
22export const RelayAgentAwarenessPhase = Schema.Literals([
23 "starting",
24 "running",
25 "waiting_for_approval",
26 "waiting_for_input",
27 "completed",
28 "failed",
29 "stale",
30]);
31export type RelayAgentAwarenessPhase = typeof RelayAgentAwarenessPhase.Type;
32
33export const RelayAgentAwarenessPreferences = Schema.Struct({
34 liveActivitiesEnabled: Schema.Boolean,
35 notificationsEnabled: Schema.Boolean,
36 notifyOnApproval: Schema.Boolean,
37 notifyOnInput: Schema.Boolean,
38 notifyOnCompletion: Schema.Boolean,
39 notifyOnFailure: Schema.Boolean,
40});
41export type RelayAgentAwarenessPreferences = typeof RelayAgentAwarenessPreferences.Type;
42
43export const RelayApnsEnvironment = Schema.Literals(["sandbox", "production"]);
44export type RelayApnsEnvironment = typeof RelayApnsEnvironment.Type;
45
46export const RelayDeviceRegistrationRequest = Schema.Struct({
47 deviceId: TrimmedNonEmptyString,
48 label: TrimmedNonEmptyString,
49 platform: RelayAgentAwarenessPlatform,
50 iosMajorVersion: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(18))),
51 androidApiLevel: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(24))),
52 appVersion: Schema.optional(TrimmedNonEmptyString),
53 // APNs topic must match the variant's bundle id; development-signed builds use sandbox tokens.
54 bundleId: Schema.optional(TrimmedNonEmptyString),
55 apsEnvironment: Schema.optional(RelayApnsEnvironment),
56 pushToken: Schema.optional(TrimmedNonEmptyString),
57 pushToStartToken: Schema.optional(TrimmedNonEmptyString),
58 preferences: RelayAgentAwarenessPreferences,
59}).check(
60 Schema.makeFilter((device) =>
61 device.platform === "ios"
62 ? device.iosMajorVersion !== undefined
63 : device.androidApiLevel !== undefined &&
64 device.iosMajorVersion === undefined &&
65 device.apsEnvironment === undefined &&
66 device.pushToStartToken === undefined,
67 ),
68);
69export type RelayDeviceRegistrationRequest = typeof RelayDeviceRegistrationRequest.Type;
70
71export const RelayClientDeviceRecord = Schema.Struct({
72 deviceId: TrimmedNonEmptyString,
73 label: TrimmedNonEmptyString,
74 platform: RelayAgentAwarenessPlatform,
75 iosMajorVersion: Schema.NullOr(Schema.Int.check(Schema.isGreaterThanOrEqualTo(18))),
76 androidApiLevel: Schema.optional(Schema.NullOr(Schema.Int)),
77 appVersion: Schema.NullOr(TrimmedNonEmptyString),
78 notifications: Schema.Struct({
79 enabled: Schema.Boolean,
80 notifyOnApproval: Schema.Boolean,
81 notifyOnInput: Schema.Boolean,
82 notifyOnCompletion: Schema.Boolean,
83 notifyOnFailure: Schema.Boolean,
84 }),
85 liveActivities: Schema.Struct({
86 enabled: Schema.Boolean,
87 }),
88 updatedAt: TrimmedNonEmptyString,
89});
90export type RelayClientDeviceRecord = typeof RelayClientDeviceRecord.Type;
91
92export const RelayListDevicesResponse = Schema.Struct({
93 devices: Schema.Array(RelayClientDeviceRecord),
94});
95export type RelayListDevicesResponse = typeof RelayListDevicesResponse.Type;
96
97// Installed clients decode v1 as iOS-only. Keep that response contract frozen.
98export const RelayListDevicesResponseV1 = Schema.Struct({
99 devices: Schema.Array(
100 Schema.Struct({
101 ...RelayClientDeviceRecord.fields,
102 platform: Schema.Literal("ios"),
103 iosMajorVersion: Schema.Int.check(Schema.isGreaterThanOrEqualTo(18)),
104 }),
105 ),
106});
107
108export const RelayLiveActivityRegistrationRequest = Schema.Struct({
109 deviceId: TrimmedNonEmptyString,
110 activityPushToken: TrimmedNonEmptyString,
111});
112export type RelayLiveActivityRegistrationRequest = typeof RelayLiveActivityRegistrationRequest.Type;
113
114export const RelayDeviceUnregistrationParams = Schema.Struct({
115 deviceId: TrimmedNonEmptyString,
116});
117export type RelayDeviceUnregistrationParams = typeof RelayDeviceUnregistrationParams.Type;
118
119export const RelayAgentActivityState = Schema.Struct({
120 environmentId: EnvironmentId,
121 threadId: ThreadId,
122 projectTitle: TrimmedNonEmptyString,
123 threadTitle: TrimmedNonEmptyString,
124 phase: RelayAgentAwarenessPhase,
125 headline: TrimmedNonEmptyString,
126 detail: Schema.optional(TrimmedNonEmptyString),
127 modelTitle: TrimmedNonEmptyString,
128 updatedAt: TrimmedNonEmptyString,
129 deepLink: TrimmedNonEmptyString,
130});
131export type RelayAgentActivityState = typeof RelayAgentActivityState.Type;
132
133export const RelayAgentActivityAggregateRow = Schema.Struct({
134 environmentId: EnvironmentId,
135 threadId: ThreadId,
136 projectTitle: TrimmedNonEmptyString,
137 threadTitle: TrimmedNonEmptyString,
138 modelTitle: TrimmedNonEmptyString,
139 phase: RelayAgentAwarenessPhase,
140 status: TrimmedNonEmptyString,
141 updatedAt: TrimmedNonEmptyString,
142 deepLink: TrimmedNonEmptyString,
143});
144export type RelayAgentActivityAggregateRow = typeof RelayAgentActivityAggregateRow.Type;
145
146export const RelayAgentActivityAggregateState = Schema.Struct({
147 title: TrimmedNonEmptyString,
148 subtitle: TrimmedNonEmptyString,
149 activeCount: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
150 updatedAt: TrimmedNonEmptyString,
151 activities: Schema.Array(RelayAgentActivityAggregateRow),
152});
153export type RelayAgentActivityAggregateState = typeof RelayAgentActivityAggregateState.Type;
154
155export const RelayManagedEndpointProviderKind = Schema.Literals([
156 "manual",
157 "cloudflare_tunnel",
158 "t3_relay",
159]);
160export type RelayManagedEndpointProviderKind = typeof RelayManagedEndpointProviderKind.Type;
161
162export const RelayManagedEndpoint = Schema.Struct({
163 httpBaseUrl: TrimmedNonEmptyString,
164 wsBaseUrl: TrimmedNonEmptyString,
165 providerKind: RelayManagedEndpointProviderKind,
166});
167export type RelayManagedEndpoint = typeof RelayManagedEndpoint.Type;
168
169export const RelayManagedEndpointOrigin = Schema.Struct({
170 localHttpHost: TrimmedNonEmptyString,
171 localHttpPort: Schema.Int.check(
172 Schema.isGreaterThanOrEqualTo(1),
173 Schema.isLessThanOrEqualTo(65_535),
174 ),
175});
176export type RelayManagedEndpointOrigin = typeof RelayManagedEndpointOrigin.Type;
177
178export const RelayManagedEndpointRuntimeConfig = Schema.Struct({
179 providerKind: RelayManagedEndpointProviderKind,
180 connectorToken: TrimmedNonEmptyString,
181 tunnelId: Schema.optional(TrimmedNonEmptyString),
182 tunnelName: Schema.optional(TrimmedNonEmptyString),
183});
184export type RelayManagedEndpointRuntimeConfig = typeof RelayManagedEndpointRuntimeConfig.Type;
185
186export const RelayManagedEndpointRecoveryRequest = Schema.Struct({
187 cloudUserId: TrimmedNonEmptyString,
188 origin: RelayManagedEndpointOrigin,
189 proof: TrimmedNonEmptyString,
190});
191export type RelayManagedEndpointRecoveryRequest = typeof RelayManagedEndpointRecoveryRequest.Type;
192
193export const RelayManagedEndpointRecoveryRegistrationRequest = Schema.Struct({
194 cloudUserId: TrimmedNonEmptyString,
195 tunnelId: TrimmedNonEmptyString,
196 origin: RelayManagedEndpointOrigin,
197 proof: TrimmedNonEmptyString,
198});
199export type RelayManagedEndpointRecoveryRegistrationRequest =
200 typeof RelayManagedEndpointRecoveryRegistrationRequest.Type;
201
202export const RelayManagedEndpointRecoveryRegistrationResponse = Schema.Struct({
203 status: Schema.Literals(["ready", "recovery_required"]),
204});
205export type RelayManagedEndpointRecoveryRegistrationResponse =
206 typeof RelayManagedEndpointRecoveryRegistrationResponse.Type;
207
208export const RelayManagedEndpointRecoveryResponse = Schema.Struct({
209 endpoint: RelayManagedEndpoint,
210 endpointRuntime: RelayManagedEndpointRuntimeConfig,
211});
212export type RelayManagedEndpointRecoveryResponse = typeof RelayManagedEndpointRecoveryResponse.Type;
213
214export const RelayLinkProofRequest = Schema.Struct({
215 challenge: Schema.String,
216 relayIssuer: Schema.String,
217 endpoint: RelayManagedEndpoint,
218 origin: RelayManagedEndpointOrigin,
219});
220export type RelayLinkProofRequest = typeof RelayLinkProofRequest.Type;
221
222export const RelayEnvironmentConfigRequest = Schema.Struct({
223 relayUrl: Schema.String,
224 relayIssuer: Schema.optional(Schema.String),
225 cloudUserId: Schema.String,
226 environmentCredential: Schema.String,
227 cloudMintPublicKey: Schema.String,
228 endpointRuntime: Schema.NullOr(RelayManagedEndpointRuntimeConfig),
229});
230export type RelayEnvironmentConfigRequest = typeof RelayEnvironmentConfigRequest.Type;
231
232const RelaySignedJwtRegisteredClaims = {
233 iss: TrimmedNonEmptyString,
234 aud: TrimmedNonEmptyString,
235 sub: TrimmedNonEmptyString,
236 jti: TrimmedNonEmptyString,
237 iat: Schema.Int,
238 exp: Schema.Int,
239} as const;
240
241export const RelayManagedEndpointRecoveryProofPayload = Schema.Union([
242 Schema.Struct({
243 ...RelaySignedJwtRegisteredClaims,
244 action: Schema.Literal("register"),
245 environmentId: EnvironmentId,
246 cloudUserId: TrimmedNonEmptyString,
247 tunnelId: TrimmedNonEmptyString,
248 origin: RelayManagedEndpointOrigin,
249 }),
250 Schema.Struct({
251 ...RelaySignedJwtRegisteredClaims,
252 action: Schema.Literal("recover"),
253 environmentId: EnvironmentId,
254 cloudUserId: TrimmedNonEmptyString,
255 origin: RelayManagedEndpointOrigin,
256 }),
257]);
258export type RelayManagedEndpointRecoveryProofPayload =
259 typeof RelayManagedEndpointRecoveryProofPayload.Type;
260
261export const RelayAgentActivityPublishProofPayload = Schema.Struct({
262 ...RelaySignedJwtRegisteredClaims,
263 environmentId: EnvironmentId,
264 threadId: ThreadId,
265 state: Schema.NullOr(RelayAgentActivityState),
266});
267export type RelayAgentActivityPublishProofPayload =
268 typeof RelayAgentActivityPublishProofPayload.Type;
269
270export const RelayAgentActivityPublishRequest = Schema.Struct({
271 state: Schema.NullOr(RelayAgentActivityState).annotate({
272 description: "Current agent-awareness state, or null to remove the published state.",
273 }),
274 proof: TrimmedNonEmptyString.annotate({
275 description: "Environment-signed JWT covering this published activity state.",
276 }),
277}).annotate({ description: "Publishes a signed agent-awareness update from an environment." });
278export type RelayAgentActivityPublishRequest = typeof RelayAgentActivityPublishRequest.Type;
279
280export const RelayEnvironmentLinkScope = Schema.Literals([
281 "agent_activity_notifications",
282 "managed_tunnels",
283]);
284export type RelayEnvironmentLinkScope = typeof RelayEnvironmentLinkScope.Type;
285
286export const RelayEnvironmentLinkProofPayload = Schema.Struct({
287 ...RelaySignedJwtRegisteredClaims,
288 challenge: TrimmedNonEmptyString,
289 descriptor: ExecutionEnvironmentDescriptor,
290 environmentId: EnvironmentId,
291 environmentPublicKey: TrimmedNonEmptyString,
292 endpoint: RelayManagedEndpoint,
293 origin: RelayManagedEndpointOrigin,
294 scopes: Schema.Array(RelayEnvironmentLinkScope),
295});
296export type RelayEnvironmentLinkProofPayload = typeof RelayEnvironmentLinkProofPayload.Type;
297
298export const RelayEnvironmentLinkProof = TrimmedNonEmptyString;
299export type RelayEnvironmentLinkProof = typeof RelayEnvironmentLinkProof.Type;
300
301export const RelayEnvironmentLinkChallengeRequest = Schema.Struct({
302 notificationsEnabled: Schema.Boolean.annotate({
303 description: "Whether this link may deliver push notifications.",
304 }),
305 liveActivitiesEnabled: Schema.Boolean.annotate({
306 description: "Whether this link may update Live Activities.",
307 }),
308 managedTunnelsEnabled: Schema.Boolean.annotate({
309 description: "Whether the relay should provision a managed tunnel for this environment.",
310 }),
311}).annotate({ description: "Requested capabilities for a new environment-link challenge." });
312export type RelayEnvironmentLinkChallengeRequest = typeof RelayEnvironmentLinkChallengeRequest.Type;
313
314export const RelayEnvironmentLinkChallengeResponse = Schema.Struct({
315 challenge: TrimmedNonEmptyString,
316 expiresAt: TrimmedNonEmptyString,
317});
318export type RelayEnvironmentLinkChallengeResponse =
319 typeof RelayEnvironmentLinkChallengeResponse.Type;
320
321export const RelayEnvironmentLinkRequest = Schema.Struct({
322 deviceId: Schema.optional(
323 TrimmedNonEmptyString.annotate({
324 description: "Optional client device identifier associated with this link.",
325 }),
326 ),
327 proof: RelayEnvironmentLinkProof.annotate({
328 description: "Environment-signed proof bound to a previously issued link challenge.",
329 }),
330 notificationsEnabled: Schema.Boolean,
331 liveActivitiesEnabled: Schema.Boolean,
332 managedTunnelsEnabled: Schema.Boolean,
333}).annotate({ description: "Links an authenticated cloud user to a T3 environment." });
334export type RelayEnvironmentLinkRequest = typeof RelayEnvironmentLinkRequest.Type;
335
336export const RelayEnvironmentLinkResponse = Schema.Struct({
337 ok: Schema.Boolean,
338 cloudUserId: TrimmedNonEmptyString,
339 environmentId: EnvironmentId,
340 endpoint: RelayManagedEndpoint,
341 endpointRuntime: Schema.NullOr(RelayManagedEndpointRuntimeConfig),
342 relayIssuer: TrimmedNonEmptyString,
343 environmentCredential: TrimmedNonEmptyString,
344 cloudMintPublicKey: TrimmedNonEmptyString,
345});
346export type RelayEnvironmentLinkResponse = typeof RelayEnvironmentLinkResponse.Type;
347
348export const RelayEnvironmentLinkProofInvalidReason = Schema.Literals([
349 "invalid_signature_or_scope",
350 "descriptor_mismatch",
351 "replayed_nonce",
352 "challenge_invalid",
353 "origin_not_allowed",
354 "endpoint_not_secure",
355]);
356export type RelayEnvironmentLinkProofInvalidReason =
357 typeof RelayEnvironmentLinkProofInvalidReason.Type;
358
359export const RelayEnvironmentLinkFailedReason = Schema.Literals([
360 "link_persistence_failed",
361 "credential_persistence_failed",
362 "replay_persistence_failed",
363 "internal_error",
364]);
365export type RelayEnvironmentLinkFailedReason = typeof RelayEnvironmentLinkFailedReason.Type;
366
367export const RelayEnvironmentLinkUnavailableReason = Schema.Literals([
368 "managed_endpoint_not_configured",
369 "managed_endpoint_provisioning_failed",
370]);
371export type RelayEnvironmentLinkUnavailableReason =
372 typeof RelayEnvironmentLinkUnavailableReason.Type;
373
374export const RelayEnvironmentEndpointUnavailableReason = Schema.Literals([
375 "endpoint_request_failed",
376 "endpoint_response_invalid",
377]);
378export type RelayEnvironmentEndpointUnavailableReason =
379 typeof RelayEnvironmentEndpointUnavailableReason.Type;
380
381/**
382 * Which part of the path from the relay to the environment failed. `tunnel_down`
383 * means Cloudflare has no live connector for the tunnel; `origin_unreachable`
384 * means the tunnel answered but T3 Code behind it did not. Optional on the wire
385 * so old relays and old clients keep working.
386 */
387export const RelayEnvironmentEndpointFailure = Schema.Literals([
388 "tunnel_down",
389 "origin_unreachable",
390 "dns",
391 "tls",
392 "refused",
393 "timeout",
394 "invalid_response",
395 "unknown",
396]);
397export type RelayEnvironmentEndpointFailure = typeof RelayEnvironmentEndpointFailure.Type;
398
399export const RelayAgentActivityPublishProofInvalidReason = Schema.Literals([
400 "invalid_signature_or_payload",
401 "replayed_nonce",
402]);
403export type RelayAgentActivityPublishProofInvalidReason =
404 typeof RelayAgentActivityPublishProofInvalidReason.Type;
405
406export const RelayAuthInvalidReason = Schema.Literals([
407 "missing_bearer",
408 "invalid_bearer",
409 "invalid_dpop",
410 "not_authorized",
411]);
412export type RelayAuthInvalidReason = typeof RelayAuthInvalidReason.Type;
413
414export const RelayDpopFailureReason = DpopFailureReason;
415export type RelayDpopFailureReason = typeof RelayDpopFailureReason.Type;
416
417export const RelayInternalErrorReason = Schema.Literals([
418 "database_unavailable",
419 "persistence_failed",
420 "upstream_unavailable",
421 "internal_error",
422]);
423export type RelayInternalErrorReason = typeof RelayInternalErrorReason.Type;
424
425export class RelayAuthInvalidError extends Schema.TaggedError<RelayAuthInvalidError>()(
426 "RelayAuthInvalidError",
427 {
428 code: Schema.Literal("auth_invalid"),
429 reason: RelayAuthInvalidReason,
430 dpopFailureReason: Schema.optionalKey(RelayDpopFailureReason),
431 traceId: TrimmedNonEmptyString,
432 },
433 { httpApiStatus: 401 },
434) {
435 override get message(): string {
436 return `Relay authentication failed: ${this.reason}`;
437 }
438}
439
440export class RelayEnvironmentLinkProofExpiredError extends Schema.TaggedError<RelayEnvironmentLinkProofExpiredError>()(
441 "RelayEnvironmentLinkProofExpiredError",
442 {
443 code: Schema.Literal("environment_link_proof_expired"),
444 traceId: TrimmedNonEmptyString,
445 },
446 { httpApiStatus: 401 },
447) {
448 override get message(): string {
449 return "Relay environment link proof expired";
450 }
451}
452
453export class RelayEnvironmentLinkProofInvalidError extends Schema.TaggedError<RelayEnvironmentLinkProofInvalidError>()(
454 "RelayEnvironmentLinkProofInvalidError",
455 {
456 code: Schema.Literal("environment_link_proof_invalid"),
457 reason: RelayEnvironmentLinkProofInvalidReason,
458 traceId: TrimmedNonEmptyString,
459 },
460 { httpApiStatus: 400 },
461) {
462 override get message(): string {
463 return `Relay environment link proof is invalid: ${this.reason}`;
464 }
465}
466
467export const RelayEnvironmentConnectNotAuthorizedReason = Schema.Literals([
468 "client_proof_key_thumbprint_missing",
469 "environment_link_not_found",
470 "endpoint_provider_not_managed",
471 "managed_endpoint_allocation_not_found",
472 "managed_endpoint_base_domain_not_configured",
473 "managed_endpoint_allocation_not_ready",
474 "managed_endpoint_hostname_invalid",
475 "managed_endpoint_mismatch",
476]);
477export type RelayEnvironmentConnectNotAuthorizedReason =
478 typeof RelayEnvironmentConnectNotAuthorizedReason.Type;
479
480export class RelayEnvironmentConnectNotAuthorizedError extends Schema.TaggedError<RelayEnvironmentConnectNotAuthorizedError>()(
481 "RelayEnvironmentConnectNotAuthorizedError",
482 {
483 code: Schema.Literal("environment_connect_not_authorized"),
484 reason: Schema.optional(RelayEnvironmentConnectNotAuthorizedReason),
485 traceId: TrimmedNonEmptyString,
486 },
487 { httpApiStatus: 403 },
488) {
489 override get message(): string {
490 return this.reason
491 ? `Relay environment connection is not authorized: ${this.reason}`
492 : "Relay environment connection is not authorized";
493 }
494}
495
496export class RelayEnvironmentEndpointUnavailableError extends Schema.TaggedError<RelayEnvironmentEndpointUnavailableError>()(
497 "RelayEnvironmentEndpointUnavailableError",
498 {
499 code: Schema.Literal("environment_endpoint_unavailable"),
500 reason: RelayEnvironmentEndpointUnavailableReason,
501 endpointFailure: Schema.optionalKey(RelayEnvironmentEndpointFailure),
502 endpointStatus: Schema.optionalKey(Schema.Number),
503 traceId: TrimmedNonEmptyString,
504 },
505 { httpApiStatus: 502 },
506) {
507 override get message(): string {
508 return `Relay environment endpoint is unavailable: ${this.reason}`;
509 }
510}
511
512export class RelayEnvironmentEndpointTimedOutError extends Schema.TaggedError<RelayEnvironmentEndpointTimedOutError>()(
513 "RelayEnvironmentEndpointTimedOutError",
514 {
515 code: Schema.Literal("environment_endpoint_timed_out"),
516 traceId: TrimmedNonEmptyString,
517 },
518 { httpApiStatus: 504 },
519) {
520 override get message(): string {
521 return "Relay environment endpoint request timed out";
522 }
523}
524
525export class RelayEnvironmentLinkFailedError extends Schema.TaggedError<RelayEnvironmentLinkFailedError>()(
526 "RelayEnvironmentLinkFailedError",
527 {
528 code: Schema.Literal("environment_link_failed"),
529 reason: RelayEnvironmentLinkFailedReason,
530 traceId: TrimmedNonEmptyString,
531 },
532 { httpApiStatus: 500 },
533) {
534 override get message(): string {
535 return `Relay environment link failed: ${this.reason}`;
536 }
537}
538
539export class RelayEnvironmentLinkUnavailableError extends Schema.TaggedError<RelayEnvironmentLinkUnavailableError>()(
540 "RelayEnvironmentLinkUnavailableError",
541 {
542 code: Schema.Literal("environment_link_unavailable"),
543 reason: RelayEnvironmentLinkUnavailableReason,
544 provisioningStage: Schema.optionalKey(TrimmedNonEmptyString),
545 traceId: TrimmedNonEmptyString,
546 },
547 { httpApiStatus: 503 },
548) {
549 override get message(): string {
550 return `Relay environment link is unavailable: ${this.reason}`;
551 }
552}
553
554export class RelayEnvironmentLinkLimitExceededError extends Schema.TaggedError<RelayEnvironmentLinkLimitExceededError>()(
555 "RelayEnvironmentLinkLimitExceededError",
556 {
557 code: Schema.Literal("environment_link_limit_exceeded"),
558 maxTunnels: Schema.Number,
559 traceId: TrimmedNonEmptyString,
560 },
561 { httpApiStatus: 403 },
562) {
563 override get message(): string {
564 return `Relay managed tunnel limit reached: this account allows at most ${this.maxTunnels} tunnels`;
565 }
566}
567
568export class RelayAgentActivityPublishProofExpiredError extends Schema.TaggedError<RelayAgentActivityPublishProofExpiredError>()(
569 "RelayAgentActivityPublishProofExpiredError",
570 {
571 code: Schema.Literal("agent_activity_publish_proof_expired"),
572 traceId: TrimmedNonEmptyString,
573 },
574 { httpApiStatus: 401 },
575) {
576 override get message(): string {
577 return "Relay agent activity publish proof expired";
578 }
579}
580
581export class RelayAgentActivityPublishProofInvalidError extends Schema.TaggedError<RelayAgentActivityPublishProofInvalidError>()(
582 "RelayAgentActivityPublishProofInvalidError",
583 {
584 code: Schema.Literal("agent_activity_publish_proof_invalid"),
585 reason: RelayAgentActivityPublishProofInvalidReason,
586 traceId: TrimmedNonEmptyString,
587 },
588 { httpApiStatus: 401 },
589) {
590 override get message(): string {
591 return `Relay agent activity publish proof is invalid: ${this.reason}`;
592 }
593}
594
595export class RelayInternalError extends Schema.TaggedError<RelayInternalError>()(
596 "RelayInternalError",
597 {
598 code: Schema.Literal("internal_error"),
599 reason: RelayInternalErrorReason,
600 traceId: TrimmedNonEmptyString,
601 },
602 { httpApiStatus: 500 },
603) {
604 override get message(): string {
605 return `Relay internal error: ${this.reason}`;
606 }
607}
608
609export const RelayProtectedError = Schema.Union([
610 RelayAuthInvalidError,
611 RelayEnvironmentLinkProofExpiredError,
612 RelayEnvironmentLinkProofInvalidError,
613 RelayEnvironmentConnectNotAuthorizedError,
614 RelayEnvironmentEndpointUnavailableError,
615 RelayEnvironmentEndpointTimedOutError,
616 RelayEnvironmentLinkFailedError,
617 RelayEnvironmentLinkUnavailableError,
618 RelayEnvironmentLinkLimitExceededError,
619 RelayAgentActivityPublishProofExpiredError,
620 RelayAgentActivityPublishProofInvalidError,
621 RelayInternalError,
622]);
623export type RelayProtectedError = typeof RelayProtectedError.Type;
624
625const RelayAuthAndInternalErrors = [RelayAuthInvalidError, RelayInternalError] as const;
626
627const RelayEnvironmentLinkErrors = [
628 RelayAuthInvalidError,
629 RelayEnvironmentLinkProofExpiredError,
630 RelayEnvironmentLinkProofInvalidError,
631 RelayEnvironmentLinkUnavailableError,
632 RelayEnvironmentLinkLimitExceededError,
633 RelayEnvironmentLinkFailedError,
634 RelayInternalError,
635] as const;
636
637const RelayEnvironmentConnectErrors = [
638 RelayAuthInvalidError,
639 RelayEnvironmentConnectNotAuthorizedError,
640 RelayEnvironmentEndpointUnavailableError,
641 RelayEnvironmentEndpointTimedOutError,
642 RelayInternalError,
643] as const;
644
645const RelayAgentActivityPublishErrors = [
646 RelayAuthInvalidError,
647 RelayAgentActivityPublishProofExpiredError,
648 RelayAgentActivityPublishProofInvalidError,
649 RelayInternalError,
650] as const;
651
652export class RelayClientPrincipal extends Context.Service<
653 RelayClientPrincipal,
654 {
655 readonly userId: string;
656 readonly token: string;
657 readonly proofKeyThumbprint?: string;
658 readonly dpopScopes?: ReadonlyArray<RelayDpopAccessTokenScope>;
659 }
660>()("@t3tools/contracts/relay/RelayClientPrincipal") {}
661
662export class RelayEnvironmentPrincipal extends Context.Service<
663 RelayEnvironmentPrincipal,
664 {
665 readonly environmentId: string;
666 readonly environmentPublicKey: string;
667 }
668>()("@t3tools/contracts/relay/RelayEnvironmentPrincipal") {}
669
670const RelayClientBearerAuthorization = HttpApiSecurity.http({ scheme: "bearer" }).pipe(
671 HttpApiSecurity.annotate(
672 OpenApi.Description,
673 "Clerk session or OAuth bearer token for the signed-in T3 Connect user.",
674 ),
675);
676
677export class RelayClientAuth extends HttpApiMiddleware.Service<
678 RelayClientAuth,
679 { provides: RelayClientPrincipal }
680>()("RelayClientAuth", {
681 error: RelayAuthInvalidError,
682 security: { clientBearer: RelayClientBearerAuthorization },
683}) {}
684
685const RelayEnvironmentBearerAuthorization = HttpApiSecurity.http({ scheme: "bearer" }).pipe(
686 HttpApiSecurity.annotate(
687 OpenApi.Description,
688 "Relay-issued environment credential installed when the environment is linked.",
689 ),
690);
691
692export class RelayEnvironmentAuth extends HttpApiMiddleware.Service<
693 RelayEnvironmentAuth,
694 { provides: RelayEnvironmentPrincipal }
695>()("RelayEnvironmentAuth", {
696 error: [RelayAuthInvalidError, RelayInternalError],
697 security: { environmentBearer: RelayEnvironmentBearerAuthorization },
698}) {}
699
700const RelayDpopAuthorization = HttpApiSecurity.http({ scheme: "DPoP" }).pipe(
701 HttpApiSecurity.annotate(
702 OpenApi.Description,
703 "DPoP-bound access token. Requests must also include the DPoP proof JWT header.",
704 ),
705);
706
707export class RelayDpopClientAuth extends HttpApiMiddleware.Service<
708 RelayDpopClientAuth,
709 { provides: RelayClientPrincipal }
710>()("RelayDpopClientAuth", {
711 error: RelayAuthInvalidError,
712 security: { relayDpop: RelayDpopAuthorization },
713}) {}
714
715export const RelayClientEnvironmentRecord = Schema.Struct({
716 environmentId: EnvironmentId,
717 label: TrimmedNonEmptyString,
718 endpoint: RelayManagedEndpoint,
719 linkedAt: TrimmedNonEmptyString,
720});
721export type RelayClientEnvironmentRecord = typeof RelayClientEnvironmentRecord.Type;
722
723export const RelayListEnvironmentsResponse = Schema.Struct({
724 environments: Schema.Array(RelayClientEnvironmentRecord),
725});
726export type RelayListEnvironmentsResponse = typeof RelayListEnvironmentsResponse.Type;
727
728export const RelayEnvironmentConnectRequest = Schema.Struct({
729 deviceId: Schema.optional(
730 TrimmedNonEmptyString.annotate({
731 description: "Optional client device identifier requesting the connection.",
732 }),
733 ),
734 clientKeyThumbprint: Schema.optional(
735 TrimmedNonEmptyString.annotate({
736 description: "Deprecated alias for clientProofKeyThumbprint.",
737 }),
738 ),
739 clientProofKeyThumbprint: Schema.optional(
740 TrimmedNonEmptyString.annotate({
741 description: "JWK thumbprint that the minted environment credential must be bound to.",
742 }),
743 ),
744}).annotate({ description: "Requests a short-lived credential for connecting to an environment." });
745export type RelayEnvironmentConnectRequest = typeof RelayEnvironmentConnectRequest.Type;
746
747export const RelayEnvironmentConnectScope = "environment:connect" as const;
748export const RelayEnvironmentStatusScope = "environment:status" as const;
749export const RelayMobileRegistrationScope = "mobile:registration" as const;
750export const RelayDpopAccessTokenScope = Schema.Literals([
751 RelayEnvironmentConnectScope,
752 RelayEnvironmentStatusScope,
753 RelayMobileRegistrationScope,
754]);
755export type RelayDpopAccessTokenScope = typeof RelayDpopAccessTokenScope.Type;
756
757export const RelayDpopTokenExchangeGrantType =
758 "urn:ietf:params:oauth:grant-type:token-exchange" as const;
759export const RelayJwtSubjectTokenType = "urn:ietf:params:oauth:token-type:jwt" as const;
760export const RelayAccessTokenType = "urn:ietf:params:oauth:token-type:access_token" as const;
761export const RelayPublicClientId = Schema.Literals(["t3-mobile", "t3-web"]);
762export type RelayPublicClientId = typeof RelayPublicClientId.Type;
763export const RelayMobileClientId = "t3-mobile" as const;
764export const RelayWebClientId = "t3-web" as const;
765
766export const RelayDpopAccessTokenRequest = Schema.Struct({
767 grant_type: Schema.Literal(RelayDpopTokenExchangeGrantType),
768 subject_token: TrimmedNonEmptyString.annotate({
769 description: "Clerk bearer token for the signed-in cloud user.",
770 }),
771 subject_token_type: Schema.Literal(RelayJwtSubjectTokenType),
772 requested_token_type: Schema.Literal(RelayAccessTokenType),
773 resource: TrimmedNonEmptyString.annotate({
774 description: "Relay issuer URL that will receive the DPoP-bound access token.",
775 }),
776 scope: TrimmedNonEmptyString.annotate({
777 description: "Space-separated relay scopes requested by the client.",
778 }),
779 client_id: RelayPublicClientId,
780})
781 .annotate({ description: "OAuth token exchange request for a DPoP-bound relay access token." })
782 .pipe(HttpApiSchema.asFormUrlEncoded());
783export type RelayDpopAccessTokenRequest = typeof RelayDpopAccessTokenRequest.Type;
784
785export const RelayDpopAccessTokenResponse = Schema.Struct({
786 access_token: TrimmedNonEmptyString,
787 issued_token_type: Schema.Literal(RelayAccessTokenType),
788 token_type: Schema.Literal("DPoP"),
789 expires_in: Schema.Int.check(Schema.isGreaterThan(0)),
790 scope: TrimmedNonEmptyString,
791});
792export type RelayDpopAccessTokenResponse = typeof RelayDpopAccessTokenResponse.Type;
793
794export const RelayBearerRequestHeaders = Schema.Struct({
795 authorization: TrimmedNonEmptyString,
796});
797
798export const RelayDpopProofRequestHeaders = Schema.Struct({
799 dpop: TrimmedNonEmptyString,
800});
801
802export const RelayDpopRequestHeaders = Schema.Struct({
803 authorization: TrimmedNonEmptyString,
804 dpop: TrimmedNonEmptyString,
805});
806
807export const RelayAuthorizationServerMetadata = Schema.Struct({
808 issuer: TrimmedNonEmptyString,
809 token_endpoint: TrimmedNonEmptyString,
810 grant_types_supported: Schema.Array(Schema.Literal(RelayDpopTokenExchangeGrantType)),
811 token_endpoint_auth_methods_supported: Schema.Array(Schema.Literal("none")),
812 dpop_signing_alg_values_supported: Schema.Array(Schema.Literal("ES256")),
813 scopes_supported: Schema.Array(RelayDpopAccessTokenScope),
814});
815
816export const RelayProtectedResourceMetadata = Schema.Struct({
817 resource: TrimmedNonEmptyString,
818 authorization_servers: Schema.Array(TrimmedNonEmptyString),
819 scopes_supported: Schema.Array(RelayDpopAccessTokenScope),
820 dpop_bound_access_tokens_required: Schema.Boolean,
821 dpop_signing_alg_values_supported: Schema.Array(Schema.Literal("ES256")),
822});
823
824export const RelayEnvironmentUnlinkParams = Schema.Struct({
825 environmentId: EnvironmentId,
826});
827export type RelayEnvironmentUnlinkParams = typeof RelayEnvironmentUnlinkParams.Type;
828
829export const RelayEnvironmentConnectResponse = Schema.Struct({
830 environmentId: EnvironmentId,
831 endpoint: RelayManagedEndpoint,
832 credential: TrimmedNonEmptyString,
833 expiresAt: TrimmedNonEmptyString,
834});
835export type RelayEnvironmentConnectResponse = typeof RelayEnvironmentConnectResponse.Type;
836
837export const RelayEnvironmentStatusValue = Schema.Literals(["online", "offline"]);
838export type RelayEnvironmentStatusValue = typeof RelayEnvironmentStatusValue.Type;
839
840const RelayEnvironmentOfflineReason = Schema.Literals(["tunnel_released"]);
841type RelayEnvironmentOfflineReason = typeof RelayEnvironmentOfflineReason.Type;
842
843export const RelayEnvironmentStatusResponse = Schema.Struct({
844 environmentId: EnvironmentId,
845 endpoint: RelayManagedEndpoint,
846 status: RelayEnvironmentStatusValue,
847 checkedAt: TrimmedNonEmptyString,
848 descriptor: Schema.optional(ExecutionEnvironmentDescriptor),
849 error: Schema.optional(TrimmedNonEmptyString),
850 endpointFailure: Schema.optional(RelayEnvironmentEndpointFailure),
851 endpointStatus: Schema.optional(Schema.Number),
852 offlineReason: Schema.optional(RelayEnvironmentOfflineReason),
853 traceId: Schema.optional(TrimmedNonEmptyString),
854});
855export type RelayEnvironmentStatusResponse = typeof RelayEnvironmentStatusResponse.Type;
856
857export const RelayCloudMintCredentialProofPayload = Schema.Struct({
858 ...RelaySignedJwtRegisteredClaims,
859 environmentId: EnvironmentId,
860 clientProofKeyThumbprint: TrimmedNonEmptyString,
861 cnf: Schema.Struct({
862 jkt: TrimmedNonEmptyString,
863 }),
864 deviceId: Schema.optional(TrimmedNonEmptyString),
865 nonce: TrimmedNonEmptyString,
866 scope: Schema.Array(Schema.Literal("environment:connect")),
867});
868export type RelayCloudMintCredentialProofPayload = typeof RelayCloudMintCredentialProofPayload.Type;
869
870export const RelayCloudMintCredentialProof = TrimmedNonEmptyString;
871export type RelayCloudMintCredentialProof = typeof RelayCloudMintCredentialProof.Type;
872
873export const RelayCloudMintCredentialRequest = Schema.Struct({
874 proof: RelayCloudMintCredentialProof,
875});
876export type RelayCloudMintCredentialRequest = typeof RelayCloudMintCredentialRequest.Type;
877
878export const RelayCloudEnvironmentHealthProofPayload = Schema.Struct({
879 ...RelaySignedJwtRegisteredClaims,
880 environmentId: EnvironmentId,
881 nonce: TrimmedNonEmptyString,
882 scope: Schema.Array(Schema.Literal("environment:status")),
883});
884export type RelayCloudEnvironmentHealthProofPayload =
885 typeof RelayCloudEnvironmentHealthProofPayload.Type;
886
887/** Verify x-t3-relay-delivery with the relay mint key before trusting delivery id, time, or trace context. */
888export const RelayHookDeliveryProofPayload = Schema.Struct({
889 ...RelaySignedJwtRegisteredClaims,
890 environmentId: EnvironmentId,
891 deliveryId: TrimmedNonEmptyString,
892 receivedAt: TrimmedNonEmptyString,
893 hookId: TrimmedNonEmptyString,
894});
895export type RelayHookDeliveryProofPayload = typeof RelayHookDeliveryProofPayload.Type;
896
897export const RelayCloudEnvironmentHealthProof = TrimmedNonEmptyString;
898export type RelayCloudEnvironmentHealthProof = typeof RelayCloudEnvironmentHealthProof.Type;
899
900export const RelayCloudEnvironmentHealthRequest = Schema.Struct({
901 proof: RelayCloudEnvironmentHealthProof,
902});
903export type RelayCloudEnvironmentHealthRequest = typeof RelayCloudEnvironmentHealthRequest.Type;
904
905export const RelayEnvironmentHealthResponseProofPayload = Schema.Struct({
906 ...RelaySignedJwtRegisteredClaims,
907 environmentId: EnvironmentId,
908 requestNonce: TrimmedNonEmptyString,
909 status: Schema.Literal("online"),
910 descriptor: ExecutionEnvironmentDescriptor,
911 checkedAt: TrimmedNonEmptyString,
912});
913export type RelayEnvironmentHealthResponseProofPayload =
914 typeof RelayEnvironmentHealthResponseProofPayload.Type;
915
916export const RelayEnvironmentHealthResponse = Schema.Struct({
917 environmentId: EnvironmentId,
918 status: Schema.Literal("online"),
919 descriptor: ExecutionEnvironmentDescriptor,
920 checkedAt: TrimmedNonEmptyString,
921 proof: TrimmedNonEmptyString,
922});
923export type RelayEnvironmentHealthResponse = typeof RelayEnvironmentHealthResponse.Type;
924
925export const RelayEnvironmentMintResponseProofPayload = Schema.Struct({
926 ...RelaySignedJwtRegisteredClaims,
927 environmentId: EnvironmentId,
928 clientProofKeyThumbprint: TrimmedNonEmptyString,
929 requestNonce: TrimmedNonEmptyString,
930 credential: TrimmedNonEmptyString,
931});
932export type RelayEnvironmentMintResponseProofPayload =
933 typeof RelayEnvironmentMintResponseProofPayload.Type;
934
935export const RelayEnvironmentMintResponse = Schema.Struct({
936 credential: TrimmedNonEmptyString,
937 expiresAt: TrimmedNonEmptyString,
938 proof: TrimmedNonEmptyString,
939});
940export type RelayEnvironmentMintResponse = typeof RelayEnvironmentMintResponse.Type;
941
942export const RelayDeliveryKind = Schema.Literals([
943 "live_activity_start",
944 "live_activity_update",
945 "live_activity_end",
946 "push_notification",
947]);
948export type RelayDeliveryKind = typeof RelayDeliveryKind.Type;
949
950export const RelayDeliveryResult = Schema.Struct({
951 deviceId: TrimmedNonEmptyString,
952 kind: RelayDeliveryKind,
953 ok: Schema.Boolean,
954 queued: Schema.optional(Schema.Boolean),
955 apnsStatus: Schema.NullOr(Schema.Number),
956 apnsReason: Schema.NullOr(Schema.String),
957 apnsId: Schema.NullOr(Schema.String),
958});
959export type RelayDeliveryResult = typeof RelayDeliveryResult.Type;
960
961export const RelayOkResponse = Schema.Struct({
962 ok: Schema.Boolean,
963});
964export type RelayOkResponse = typeof RelayOkResponse.Type;
965
966export const RelayT3ChatPlan = Schema.Literals(["free", "pro", "premier"]);
967export type RelayT3ChatPlan = typeof RelayT3ChatPlan.Type;
968
969export const DEFAULT_MANAGED_TUNNEL_LIMIT = 3;
970
971// A linked paid T3 Chat plan raises the default managed tunnel cap.
972export const T3CHAT_PLAN_TUNNEL_LIMITS = { pro: 10, premier: 100 } as const;
973
974export const RelayT3ChatLink = Schema.Struct({
975 email: Schema.String,
976 plan: RelayT3ChatPlan,
977});
978export type RelayT3ChatLink = typeof RelayT3ChatLink.Type;
979
980export const RelayT3ChatLinkResponse = Schema.Struct({
981 // False when this relay has no T3 Chat secret, so clients hide the feature.
982 available: Schema.Boolean,
983 link: Schema.NullOr(RelayT3ChatLink),
984 // A manual cap that replaces the plan's; T3 Chat upgrades do not raise it.
985 maxTunnelsOverride: Schema.optionalKey(Schema.Number),
986});
987export type RelayT3ChatLinkResponse = typeof RelayT3ChatLinkResponse.Type;
988
989export const RelayT3ChatLinkRequestResponse = Schema.Struct({
990 url: TrimmedNonEmptyString,
991});
992export type RelayT3ChatLinkRequestResponse = typeof RelayT3ChatLinkRequestResponse.Type;
993
994export const RelayPublishResponse = Schema.Struct({
995 ok: Schema.Boolean,
996 deliveries: Schema.Array(RelayDeliveryResult),
997});
998export type RelayPublishResponse = typeof RelayPublishResponse.Type;
999
1000export const RelayHealthResponse = Schema.Struct({
1001 ok: Schema.Boolean,
1002 service: Schema.Literal("relay"),
1003});
1004export type RelayHealthResponse = typeof RelayHealthResponse.Type;
1005
1006const RelayHealthGroup = HttpApiGroup.make("health")
1007 .add(
1008 HttpApiEndpoint.get("health", "/health", {
1009 success: RelayHealthResponse,
1010 error: RelayInternalError,
1011 }).annotate(OpenApi.Summary, "Check relay health"),
1012 )
1013 .annotate(OpenApi.Description, "Service health and readiness.");
1014
1015const RelayMetadataGroup = HttpApiGroup.make("metadata")
1016 .add(
1017 HttpApiEndpoint.get("authorizationServer", "/.well-known/oauth-authorization-server", {
1018 success: RelayAuthorizationServerMetadata,
1019 }).annotate(OpenApi.Summary, "Read OAuth authorization-server metadata"),
1020 HttpApiEndpoint.get("protectedResource", "/.well-known/oauth-protected-resource", {
1021 success: RelayProtectedResourceMetadata,
1022 }).annotate(OpenApi.Summary, "Read OAuth protected-resource metadata"),
1023 )
1024 .annotate(OpenApi.Description, "OAuth and DPoP discovery metadata.");
1025
1026export const RelayRegisterDeviceEndpoint = HttpApiEndpoint.post(
1027 "registerDevice",
1028 "/v1/mobile/devices",
1029 {
1030 headers: RelayDpopRequestHeaders,
1031 payload: RelayDeviceRegistrationRequest,
1032 success: RelayOkResponse,
1033 error: RelayAuthAndInternalErrors,
1034 },
1035).annotate(OpenApi.Summary, "Register or update a mobile device");
1036
1037export const RelayRegisterLiveActivityEndpoint = HttpApiEndpoint.post(
1038 "registerLiveActivity",
1039 "/v1/mobile/live-activities",
1040 {
1041 headers: RelayDpopRequestHeaders,
1042 payload: RelayLiveActivityRegistrationRequest,
1043 success: RelayOkResponse,
1044 error: RelayAuthAndInternalErrors,
1045 },
1046).annotate(OpenApi.Summary, "Register a Live Activity push token");
1047
1048export const RelayAgentActivitySnapshotResponse = Schema.Struct({
1049 aggregate: Schema.NullOr(RelayAgentActivityAggregateState),
1050});
1051export type RelayAgentActivitySnapshotResponse = typeof RelayAgentActivitySnapshotResponse.Type;
1052
1053export const RelayAgentActivitySnapshotEndpoint = HttpApiEndpoint.get(
1054 "getAgentActivitySnapshot",
1055 "/v1/mobile/agent-activity",
1056 {
1057 headers: RelayDpopRequestHeaders,
1058 success: RelayAgentActivitySnapshotResponse,
1059 error: RelayAuthAndInternalErrors,
1060 },
1061).annotate(OpenApi.Summary, "Read the current Live Activity aggregate");
1062
1063export const RelayUnregisterDeviceEndpoint = HttpApiEndpoint.delete(
1064 "unregisterDevice",
1065 "/v1/mobile/devices/:deviceId",
1066 {
1067 headers: RelayDpopRequestHeaders,
1068 params: RelayDeviceUnregistrationParams,
1069 success: RelayOkResponse,
1070 error: RelayAuthAndInternalErrors,
1071 },
1072).annotate(OpenApi.Summary, "Unregister a mobile device");
1073
1074const RelayMobileGroup = HttpApiGroup.make("mobile")
1075 .add(
1076 RelayRegisterDeviceEndpoint,
1077 RelayRegisterLiveActivityEndpoint,
1078 RelayAgentActivitySnapshotEndpoint,
1079 RelayUnregisterDeviceEndpoint,
1080 )
1081 .annotate(OpenApi.Description, "Mobile push-notification and Live Activity registration.")
1082 .middleware(RelayDpopClientAuth);
1083
1084const RelayClientGroup = HttpApiGroup.make("client")
1085 .add(
1086 HttpApiEndpoint.get("listEnvironments", "/v1/environments", {
1087 headers: RelayBearerRequestHeaders,
1088 success: RelayListEnvironmentsResponse,
1089 error: RelayAuthAndInternalErrors,
1090 }).annotate(OpenApi.Summary, "List linked environments"),
1091 HttpApiEndpoint.get("listDevices", "/v1/client/devices", {
1092 headers: RelayBearerRequestHeaders,
1093 success: RelayListDevicesResponseV1,
1094 error: RelayAuthAndInternalErrors,
1095 }).annotate(OpenApi.Summary, "List registered iOS devices (legacy clients)"),
1096 HttpApiEndpoint.get("listDevicesV2", "/v2/client/devices", {
1097 headers: RelayBearerRequestHeaders,
1098 success: RelayListDevicesResponse,
1099 error: RelayAuthAndInternalErrors,
1100 }).annotate(OpenApi.Summary, "List registered mobile devices"),
1101 HttpApiEndpoint.post("linkEnvironment", "/v1/client/environment-links", {
1102 headers: RelayBearerRequestHeaders,
1103 payload: RelayEnvironmentLinkRequest,
1104 success: RelayEnvironmentLinkResponse,
1105 error: RelayEnvironmentLinkErrors,
1106 }).annotate(OpenApi.Summary, "Link an environment"),
1107 HttpApiEndpoint.post(
1108 "createEnvironmentLinkChallenge",
1109 "/v1/client/environment-link-challenges",
1110 {
1111 headers: RelayBearerRequestHeaders,
1112 payload: RelayEnvironmentLinkChallengeRequest,
1113 success: RelayEnvironmentLinkChallengeResponse,
1114 error: RelayAuthAndInternalErrors,
1115 },
1116 ).annotate(OpenApi.Summary, "Create an environment-link challenge"),
1117 HttpApiEndpoint.delete("unlinkEnvironment", "/v1/client/environment-links/:environmentId", {
1118 headers: RelayBearerRequestHeaders,
1119 params: RelayEnvironmentUnlinkParams,
1120 success: RelayOkResponse,
1121 error: RelayAuthAndInternalErrors,
1122 }).annotate(OpenApi.Summary, "Unlink an environment"),
1123 HttpApiEndpoint.delete(
1124 "releaseEnvironmentTunnel",
1125 "/v1/client/environment-links/:environmentId/tunnel",
1126 {
1127 headers: RelayBearerRequestHeaders,
1128 params: RelayEnvironmentUnlinkParams,
1129 success: RelayOkResponse,
1130 error: RelayAuthAndInternalErrors,
1131 },
1132 )
1133 .annotate(OpenApi.Summary, "Release an environment's managed tunnel")
1134 .annotate(
1135 OpenApi.Description,
1136 "Deletes the provisioned Cloudflare tunnel while keeping the environment link and its hostname reservation, so a later link re-provisions the tunnel under the same URL. Environments call this when they shut down; Cloudflare bills per provisioned tunnel, so idle tunnels should not outlive their environment.",
1137 ),
1138 HttpApiEndpoint.get("getT3ChatLink", "/v1/client/t3chat-link", {
1139 headers: RelayBearerRequestHeaders,
1140 success: RelayT3ChatLinkResponse,
1141 error: RelayAuthAndInternalErrors,
1142 }).annotate(OpenApi.Summary, "Get the linked T3 Chat account"),
1143 HttpApiEndpoint.post("createT3ChatLinkRequest", "/v1/client/t3chat-link-requests", {
1144 headers: RelayBearerRequestHeaders,
1145 success: RelayT3ChatLinkRequestResponse,
1146 error: RelayAuthAndInternalErrors,
1147 }).annotate(OpenApi.Summary, "Start linking a T3 Chat account"),
1148 HttpApiEndpoint.delete("unlinkT3Chat", "/v1/client/t3chat-link", {
1149 headers: RelayBearerRequestHeaders,
1150 success: RelayOkResponse,
1151 error: RelayAuthAndInternalErrors,
1152 }).annotate(OpenApi.Summary, "Unlink the T3 Chat account"),
1153 )
1154 .annotate(OpenApi.Description, "Cloud-user environment links and registered devices.")
1155 .middleware(RelayClientAuth);
1156
1157export const RelayExchangeDpopAccessTokenEndpoint = HttpApiEndpoint.post(
1158 "exchangeDpopAccessToken",
1159 "/v1/client/dpop-token",
1160 {
1161 headers: RelayDpopProofRequestHeaders,
1162 payload: RelayDpopAccessTokenRequest,
1163 success: RelayDpopAccessTokenResponse,
1164 error: RelayAuthAndInternalErrors,
1165 },
1166)
1167 .annotate(OpenApi.Summary, "Exchange a Clerk token for a DPoP access token")
1168 .annotate(
1169 OpenApi.Description,
1170 "Bootstrap endpoint. Send the DPoP proof JWT in the dpop header and the Clerk token in subject_token. The returned access token is bound to the proof key.",
1171 );
1172
1173const RelayTokenGroup = HttpApiGroup.make("token")
1174 .add(RelayExchangeDpopAccessTokenEndpoint)
1175 .annotate(OpenApi.Description, "OAuth token exchange for DPoP-bound client access.");
1176
1177export const RelayConnectEnvironmentEndpoint = HttpApiEndpoint.post(
1178 "connectEnvironment",
1179 "/v1/environments/:environmentId/connect",
1180 {
1181 headers: RelayDpopRequestHeaders,
1182 params: Schema.Struct({
1183 environmentId: EnvironmentId,
1184 }),
1185 payload: RelayEnvironmentConnectRequest,
1186 success: RelayEnvironmentConnectResponse,
1187 error: RelayEnvironmentConnectErrors,
1188 },
1189).annotate(OpenApi.Summary, "Connect to an environment");
1190
1191export const RelayGetEnvironmentStatusEndpoint = HttpApiEndpoint.post(
1192 "getEnvironmentStatus",
1193 "/v1/environments/:environmentId/status",
1194 {
1195 headers: RelayDpopRequestHeaders,
1196 params: Schema.Struct({
1197 environmentId: EnvironmentId,
1198 }),
1199 success: RelayEnvironmentStatusResponse,
1200 error: RelayEnvironmentConnectErrors,
1201 },
1202).annotate(OpenApi.Summary, "Check environment status");
1203
1204const RelayDpopClientGroup = HttpApiGroup.make("dpopClient")
1205 .add(RelayConnectEnvironmentEndpoint, RelayGetEnvironmentStatusEndpoint)
1206 .annotate(OpenApi.Description, "DPoP-authenticated client access to linked environments.")
1207 .middleware(RelayDpopClientAuth);
1208
1209export const RelayEnvironmentLinkPreferencesRequest = Schema.Struct({
1210 holdWebhooksWhileOffline: Schema.Boolean,
1211});
1212export type RelayEnvironmentLinkPreferencesRequest =
1213 typeof RelayEnvironmentLinkPreferencesRequest.Type;
1214
1215export const RelayWakeHeldHooksResponse = Schema.Struct({
1216 pending: Schema.Boolean,
1217});
1218export type RelayWakeHeldHooksResponse = typeof RelayWakeHeldHooksResponse.Type;
1219
1220const RelayServerGroup = HttpApiGroup.make("server")
1221 .add(
1222 HttpApiEndpoint.post(
1223 "registerManagedEndpointRecovery",
1224 "/v1/environments/:environmentId/tunnel/recovery",
1225 {
1226 params: Schema.Struct({
1227 environmentId: EnvironmentId,
1228 }),
1229 payload: RelayManagedEndpointRecoveryRegistrationRequest,
1230 success: RelayManagedEndpointRecoveryRegistrationResponse,
1231 error: RelayAuthAndInternalErrors,
1232 },
1233 ).annotate(OpenApi.Summary, "Register managed tunnel recovery without provisioning"),
1234 HttpApiEndpoint.post("recoverManagedEndpoint", "/v1/environments/:environmentId/tunnel", {
1235 params: Schema.Struct({
1236 environmentId: EnvironmentId,
1237 }),
1238 payload: RelayManagedEndpointRecoveryRequest,
1239 success: RelayManagedEndpointRecoveryResponse,
1240 error: RelayAuthAndInternalErrors,
1241 }).annotate(OpenApi.Summary, "Recover an environment's managed tunnel"),
1242 HttpApiEndpoint.post(
1243 "publishAgentActivity",
1244 "/v1/environments/:environmentId/threads/:threadId/agent-activity",
1245 {
1246 params: Schema.Struct({
1247 environmentId: EnvironmentId,
1248 threadId: ThreadId,
1249 }),
1250 payload: RelayAgentActivityPublishRequest,
1251 success: RelayPublishResponse,
1252 error: RelayAgentActivityPublishErrors,
1253 },
1254 ).annotate(OpenApi.Summary, "Publish agent activity"),
1255 HttpApiEndpoint.post(
1256 "updateLinkPreferences",
1257 "/v1/environments/:environmentId/link-preferences",
1258 {
1259 params: Schema.Struct({ environmentId: EnvironmentId }),
1260 payload: RelayEnvironmentLinkPreferencesRequest,
1261 success: RelayEnvironmentLinkPreferencesRequest,
1262 error: RelayAuthAndInternalErrors,
1263 },
1264 ).annotate(OpenApi.Summary, "Update an environment's link preferences"),
1265 HttpApiEndpoint.post("wakeHeldHooks", "/v1/environments/:environmentId/hooks/wake", {
1266 params: Schema.Struct({ environmentId: EnvironmentId }),
1267 success: RelayWakeHeldHooksResponse,
1268 error: RelayAuthAndInternalErrors,
1269 }).annotate(
1270 OpenApi.Summary,
1271 "Deliver webhook requests held while the environment was offline now",
1272 ),
1273 )
1274 .annotate(OpenApi.Description, "Environment-authenticated activity publication.")
1275 .middleware(RelayEnvironmentAuth);
1276
1277// Path token authenticates; forward raw bytes for signature verification.
1278// endpointKey is the managed endpoint hash, not the account-linkable environment id.
1279const RelayHookParams = Schema.Struct({
1280 endpointKey: Schema.String,
1281 hookId: Schema.String,
1282 token: Schema.String,
1283});
1284const RELAY_HOOK_PATH = "/v1/hooks/:endpointKey/:hookId/:token";
1285const relayHookEndpoint = { params: RelayHookParams } as const;
1286const RelayHooksGroup = HttpApiGroup.make("hooks")
1287 .add(
1288 HttpApiEndpoint.post("forwardPost", RELAY_HOOK_PATH, relayHookEndpoint),
1289 HttpApiEndpoint.put("forwardPut", RELAY_HOOK_PATH, relayHookEndpoint),
1290 HttpApiEndpoint.patch("forwardPatch", RELAY_HOOK_PATH, relayHookEndpoint),
1291 HttpApiEndpoint.get("forwardGet", RELAY_HOOK_PATH, relayHookEndpoint),
1292 )
1293 .annotate(OpenApi.Description, "Forward webhook requests to an environment.");
1294
1295export const RelayMobileTelemetrySignal = Schema.Literals(["logs", "metrics", "traces"]);
1296export type RelayMobileTelemetrySignal = typeof RelayMobileTelemetrySignal.Type;
1297
1298// expo-observe appends `<easProjectId>/v1/<signal>` to its endpoint URL and cannot send credentials.
1299const RelayMobileTelemetryParams = Schema.Struct({
1300 projectId: Schema.String.check(Schema.isGUID()),
1301 signal: RelayMobileTelemetrySignal,
1302});
1303const RelayMobileTelemetryGroup = HttpApiGroup.make("mobileTelemetry")
1304 .add(
1305 HttpApiEndpoint.post("ingest", "/v1/mobile/observe/:projectId/v1/:signal", {
1306 params: RelayMobileTelemetryParams,
1307 }),
1308 )
1309 .annotate(OpenApi.Description, "Receive mobile app telemetry from expo-observe.");
1310
1311export const RelayApi = HttpApi.make("RelayApi")
1312 .add(
1313 RelayHealthGroup,
1314 RelayMetadataGroup,
1315 RelayMobileGroup,
1316 RelayClientGroup,
1317 RelayTokenGroup,
1318 RelayDpopClientGroup,
1319 RelayServerGroup,
1320 RelayHooksGroup,
1321 RelayMobileTelemetryGroup,
1322 )
1323 .annotate(OpenApi.Title, "T3 Code Relay API")
1324 .annotate(OpenApi.Version, "1.0.0")
1325 .annotate(
1326 OpenApi.Description,
1327 "Control-plane API for linking T3 environments, connecting authorized clients, and publishing agent activity.",
1328 );
1329export type RelayApi = typeof RelayApi;