Skip to content
Source
Sign inSign up

relay.ts

1,329 lines49.2 KB

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 }
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 }
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 }
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 }
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 }
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 }
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 }
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 }
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 }
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 }
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 }
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 }
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"),
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"),
1024 .annotate(OpenApi.Description, "OAuth and DPoP discovery metadata.");
1025​
1026export const RelayRegisterDeviceEndpoint = HttpApiEndpoint.post(
1027 "registerDevice",
1028 "/v1/mobile/devices",
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",
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",
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",
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,
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",
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",
1127 headers: RelayBearerRequestHeaders,
1128 params: RelayEnvironmentUnlinkParams,
1129 success: RelayOkResponse,
1130 error: RelayAuthAndInternalErrors,
1131 },
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"),
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",
1161 headers: RelayDpopProofRequestHeaders,
1162 payload: RelayDpopAccessTokenRequest,
1163 success: RelayDpopAccessTokenResponse,
1164 error: RelayAuthAndInternalErrors,
1165 },
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",
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",
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",
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",
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",
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 ),
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),
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 }),
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,
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;