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