| 1 | import * as Context from "effect/Context"; |
| 2 | import * as Schema from "effect/Schema"; |
| 3 | import * as HttpApi from "effect/http-api/HttpApi"; |
| 4 | import * as HttpApiEndpoint from "effect/http-api/HttpApiEndpoint"; |
| 5 | import * as HttpApiGroup from "effect/http-api/HttpApiGroup"; |
| 6 | import * as HttpApiMiddleware from "effect/http-api/HttpApiMiddleware"; |
| 7 | import * as HttpApiSchema from "effect/http-api/HttpApiSchema"; |
| 8 | import * as HttpApiSecurity from "effect/http-api/HttpApiSecurity"; |
| 9 | import * as OpenApi from "effect/http-api/OpenApi"; |
| 10 | |
| 11 | import { |
| 12 | DpopFailureReason, |
| 13 | EnvironmentId, |
| 14 | ThreadId, |
| 15 | TrimmedNonEmptyString, |
| 16 | } from "./baseSchemas.ts"; |
| 17 | import { ExecutionEnvironmentDescriptor } from "./environment.ts"; |
| 18 | |
| 19 | export const RelayAgentAwarenessPlatform = Schema.Literals(["ios", "android"]); |
| 20 | export type RelayAgentAwarenessPlatform = typeof RelayAgentAwarenessPlatform.Type; |
| 21 | |
| 22 | export const RelayAgentAwarenessPhase = Schema.Literals([ |
| 23 | "starting", |
| 24 | "running", |
| 25 | "waiting_for_approval", |
| 26 | "waiting_for_input", |
| 27 | "completed", |
| 28 | "failed", |
| 29 | "stale", |
| 30 | ]); |
| 31 | export type RelayAgentAwarenessPhase = typeof RelayAgentAwarenessPhase.Type; |
| 32 | |
| 33 | export 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 | }); |
| 41 | export type RelayAgentAwarenessPreferences = typeof RelayAgentAwarenessPreferences.Type; |
| 42 | |
| 43 | export const RelayApnsEnvironment = Schema.Literals(["sandbox", "production"]); |
| 44 | export type RelayApnsEnvironment = typeof RelayApnsEnvironment.Type; |
| 45 | |
| 46 | export 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 | ); |
| 72 | export type RelayDeviceRegistrationRequest = typeof RelayDeviceRegistrationRequest.Type; |
| 73 | |
| 74 | export 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 | }); |
| 93 | export type RelayClientDeviceRecord = typeof RelayClientDeviceRecord.Type; |
| 94 | |
| 95 | export const RelayListDevicesResponse = Schema.Struct({ |
| 96 | devices: Schema.Array(RelayClientDeviceRecord), |
| 97 | }); |
| 98 | export type RelayListDevicesResponse = typeof RelayListDevicesResponse.Type; |
| 99 | |
| 100 | // Installed clients decode v1 as iOS-only. Keep that response contract frozen. |
| 101 | export 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 | |
| 111 | export const RelayLiveActivityRegistrationRequest = Schema.Struct({ |
| 112 | deviceId: TrimmedNonEmptyString, |
| 113 | activityPushToken: TrimmedNonEmptyString, |
| 114 | }); |
| 115 | export type RelayLiveActivityRegistrationRequest = typeof RelayLiveActivityRegistrationRequest.Type; |
| 116 | |
| 117 | export const RelayDeviceUnregistrationParams = Schema.Struct({ |
| 118 | deviceId: TrimmedNonEmptyString, |
| 119 | }); |
| 120 | export type RelayDeviceUnregistrationParams = typeof RelayDeviceUnregistrationParams.Type; |
| 121 | |
| 122 | export 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 | }); |
| 134 | export type RelayAgentActivityState = typeof RelayAgentActivityState.Type; |
| 135 | |
| 136 | export 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 | }); |
| 147 | export type RelayAgentActivityAggregateRow = typeof RelayAgentActivityAggregateRow.Type; |
| 148 | |
| 149 | export 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 | }); |
| 156 | export type RelayAgentActivityAggregateState = typeof RelayAgentActivityAggregateState.Type; |
| 157 | |
| 158 | export const RelayManagedEndpointProviderKind = Schema.Literals([ |
| 159 | "manual", |
| 160 | "cloudflare_tunnel", |
| 161 | "t3_relay", |
| 162 | ]); |
| 163 | export type RelayManagedEndpointProviderKind = typeof RelayManagedEndpointProviderKind.Type; |
| 164 | |
| 165 | export const RelayManagedEndpoint = Schema.Struct({ |
| 166 | httpBaseUrl: TrimmedNonEmptyString, |
| 167 | wsBaseUrl: TrimmedNonEmptyString, |
| 168 | providerKind: RelayManagedEndpointProviderKind, |
| 169 | }); |
| 170 | export type RelayManagedEndpoint = typeof RelayManagedEndpoint.Type; |
| 171 | |
| 172 | export const RelayManagedEndpointOrigin = Schema.Struct({ |
| 173 | localHttpHost: TrimmedNonEmptyString, |
| 174 | localHttpPort: Schema.Int.check( |
| 175 | Schema.isGreaterThanOrEqualTo(1), |
| 176 | Schema.isLessThanOrEqualTo(65_535), |
| 177 | ), |
| 178 | }); |
| 179 | export type RelayManagedEndpointOrigin = typeof RelayManagedEndpointOrigin.Type; |
| 180 | |
| 181 | export const RelayManagedEndpointRuntimeConfig = Schema.Struct({ |
| 182 | providerKind: RelayManagedEndpointProviderKind, |
| 183 | connectorToken: TrimmedNonEmptyString, |
| 184 | tunnelId: Schema.optional(TrimmedNonEmptyString), |
| 185 | tunnelName: Schema.optional(TrimmedNonEmptyString), |
| 186 | }); |
| 187 | export type RelayManagedEndpointRuntimeConfig = typeof RelayManagedEndpointRuntimeConfig.Type; |
| 188 | |
| 189 | export const RelayManagedEndpointRecoveryRequest = Schema.Struct({ |
| 190 | cloudUserId: TrimmedNonEmptyString, |
| 191 | origin: RelayManagedEndpointOrigin, |
| 192 | proof: TrimmedNonEmptyString, |
| 193 | }); |
| 194 | export type RelayManagedEndpointRecoveryRequest = typeof RelayManagedEndpointRecoveryRequest.Type; |
| 195 | |
| 196 | export const RelayManagedEndpointRecoveryRegistrationRequest = Schema.Struct({ |
| 197 | cloudUserId: TrimmedNonEmptyString, |
| 198 | tunnelId: TrimmedNonEmptyString, |
| 199 | origin: RelayManagedEndpointOrigin, |
| 200 | proof: TrimmedNonEmptyString, |
| 201 | }); |
| 202 | export type RelayManagedEndpointRecoveryRegistrationRequest = |
| 203 | typeof RelayManagedEndpointRecoveryRegistrationRequest.Type; |
| 204 | |
| 205 | export const RelayManagedEndpointRecoveryRegistrationResponse = Schema.Struct({ |
| 206 | status: Schema.Literals(["ready", "recovery_required"]), |
| 207 | }); |
| 208 | export type RelayManagedEndpointRecoveryRegistrationResponse = |
| 209 | typeof RelayManagedEndpointRecoveryRegistrationResponse.Type; |
| 210 | |
| 211 | export const RelayManagedEndpointRecoveryResponse = Schema.Struct({ |
| 212 | endpoint: RelayManagedEndpoint, |
| 213 | endpointRuntime: RelayManagedEndpointRuntimeConfig, |
| 214 | }); |
| 215 | export type RelayManagedEndpointRecoveryResponse = typeof RelayManagedEndpointRecoveryResponse.Type; |
| 216 | |
| 217 | export const RelayLinkProofRequest = Schema.Struct({ |
| 218 | challenge: Schema.String, |
| 219 | relayIssuer: Schema.String, |
| 220 | endpoint: RelayManagedEndpoint, |
| 221 | origin: RelayManagedEndpointOrigin, |
| 222 | }); |
| 223 | export type RelayLinkProofRequest = typeof RelayLinkProofRequest.Type; |
| 224 | |
| 225 | export 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 | }); |
| 233 | export type RelayEnvironmentConfigRequest = typeof RelayEnvironmentConfigRequest.Type; |
| 234 | |
| 235 | const 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 | |
| 244 | export 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 | ]); |
| 261 | export type RelayManagedEndpointRecoveryProofPayload = |
| 262 | typeof RelayManagedEndpointRecoveryProofPayload.Type; |
| 263 | |
| 264 | export const RelayAgentActivityPublishProofPayload = Schema.Struct({ |
| 265 | ...RelaySignedJwtRegisteredClaims, |
| 266 | environmentId: EnvironmentId, |
| 267 | threadId: ThreadId, |
| 268 | state: Schema.NullOr(RelayAgentActivityState), |
| 269 | }); |
| 270 | export type RelayAgentActivityPublishProofPayload = |
| 271 | typeof RelayAgentActivityPublishProofPayload.Type; |
| 272 | export type RelayAgentActivityPublishProof = string; |
| 273 | |
| 274 | export 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." }); |
| 282 | export type RelayAgentActivityPublishRequest = typeof RelayAgentActivityPublishRequest.Type; |
| 283 | |
| 284 | export const RelayEnvironmentLinkScope = Schema.Literals([ |
| 285 | "agent_activity_notifications", |
| 286 | "managed_tunnels", |
| 287 | ]); |
| 288 | export type RelayEnvironmentLinkScope = typeof RelayEnvironmentLinkScope.Type; |
| 289 | |
| 290 | export 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 | }); |
| 300 | export type RelayEnvironmentLinkProofPayload = typeof RelayEnvironmentLinkProofPayload.Type; |
| 301 | |
| 302 | export const RelayEnvironmentLinkProof = TrimmedNonEmptyString; |
| 303 | export type RelayEnvironmentLinkProof = typeof RelayEnvironmentLinkProof.Type; |
| 304 | |
| 305 | export 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." }); |
| 316 | export type RelayEnvironmentLinkChallengeRequest = typeof RelayEnvironmentLinkChallengeRequest.Type; |
| 317 | |
| 318 | export const RelayEnvironmentLinkChallengeResponse = Schema.Struct({ |
| 319 | challenge: TrimmedNonEmptyString, |
| 320 | expiresAt: TrimmedNonEmptyString, |
| 321 | }); |
| 322 | export type RelayEnvironmentLinkChallengeResponse = |
| 323 | typeof RelayEnvironmentLinkChallengeResponse.Type; |
| 324 | |
| 325 | export 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." }); |
| 338 | export type RelayEnvironmentLinkRequest = typeof RelayEnvironmentLinkRequest.Type; |
| 339 | |
| 340 | export 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 | }); |
| 350 | export type RelayEnvironmentLinkResponse = typeof RelayEnvironmentLinkResponse.Type; |
| 351 | |
| 352 | export 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 | ]); |
| 360 | export type RelayEnvironmentLinkProofInvalidReason = |
| 361 | typeof RelayEnvironmentLinkProofInvalidReason.Type; |
| 362 | |
| 363 | export const RelayEnvironmentLinkFailedReason = Schema.Literals([ |
| 364 | "link_persistence_failed", |
| 365 | "credential_persistence_failed", |
| 366 | "replay_persistence_failed", |
| 367 | "internal_error", |
| 368 | ]); |
| 369 | export type RelayEnvironmentLinkFailedReason = typeof RelayEnvironmentLinkFailedReason.Type; |
| 370 | |
| 371 | export const RelayEnvironmentLinkUnavailableReason = Schema.Literals([ |
| 372 | "managed_endpoint_not_configured", |
| 373 | "managed_endpoint_provisioning_failed", |
| 374 | ]); |
| 375 | export type RelayEnvironmentLinkUnavailableReason = |
| 376 | typeof RelayEnvironmentLinkUnavailableReason.Type; |
| 377 | |
| 378 | export const RelayEnvironmentEndpointUnavailableReason = Schema.Literals([ |
| 379 | "endpoint_request_failed", |
| 380 | "endpoint_response_invalid", |
| 381 | ]); |
| 382 | export type RelayEnvironmentEndpointUnavailableReason = |
| 383 | typeof RelayEnvironmentEndpointUnavailableReason.Type; |
| 384 | |
| 385 | export const RelayAgentActivityPublishProofInvalidReason = Schema.Literals([ |
| 386 | "invalid_signature_or_payload", |
| 387 | "replayed_nonce", |
| 388 | ]); |
| 389 | export type RelayAgentActivityPublishProofInvalidReason = |
| 390 | typeof RelayAgentActivityPublishProofInvalidReason.Type; |
| 391 | |
| 392 | export const RelayAuthInvalidReason = Schema.Literals([ |
| 393 | "missing_bearer", |
| 394 | "invalid_bearer", |
| 395 | "invalid_dpop", |
| 396 | "not_authorized", |
| 397 | ]); |
| 398 | export type RelayAuthInvalidReason = typeof RelayAuthInvalidReason.Type; |
| 399 | |
| 400 | export const RelayDpopFailureReason = DpopFailureReason; |
| 401 | export type RelayDpopFailureReason = typeof RelayDpopFailureReason.Type; |
| 402 | |
| 403 | export const RelayInternalErrorReason = Schema.Literals([ |
| 404 | "database_unavailable", |
| 405 | "persistence_failed", |
| 406 | "upstream_unavailable", |
| 407 | "internal_error", |
| 408 | ]); |
| 409 | export type RelayInternalErrorReason = typeof RelayInternalErrorReason.Type; |
| 410 | |
| 411 | export 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 | |
| 427 | export 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 | |
| 440 | export 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 | |
| 454 | export 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 | ]); |
| 464 | export type RelayEnvironmentConnectNotAuthorizedReason = |
| 465 | typeof RelayEnvironmentConnectNotAuthorizedReason.Type; |
| 466 | |
| 467 | export 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 | |
| 485 | export 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 | |
| 499 | export 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 | |
| 512 | export 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 | |
| 526 | export 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 | |
| 540 | export 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 | |
| 554 | export 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 | |
| 567 | export 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 | |
| 581 | export 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 | |
| 595 | export 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 | ]); |
| 609 | export type RelayProtectedError = typeof RelayProtectedError.Type; |
| 610 | |
| 611 | const RelayAuthAndInternalErrors = [RelayAuthInvalidError, RelayInternalError] as const; |
| 612 | |
| 613 | const RelayEnvironmentLinkErrors = [ |
| 614 | RelayAuthInvalidError, |
| 615 | RelayEnvironmentLinkProofExpiredError, |
| 616 | RelayEnvironmentLinkProofInvalidError, |
| 617 | RelayEnvironmentLinkUnavailableError, |
| 618 | RelayEnvironmentLinkLimitExceededError, |
| 619 | RelayEnvironmentLinkFailedError, |
| 620 | RelayInternalError, |
| 621 | ] as const; |
| 622 | |
| 623 | const RelayEnvironmentConnectErrors = [ |
| 624 | RelayAuthInvalidError, |
| 625 | RelayEnvironmentConnectNotAuthorizedError, |
| 626 | RelayEnvironmentEndpointUnavailableError, |
| 627 | RelayEnvironmentEndpointTimedOutError, |
| 628 | RelayInternalError, |
| 629 | ] as const; |
| 630 | |
| 631 | const RelayAgentActivityPublishErrors = [ |
| 632 | RelayAuthInvalidError, |
| 633 | RelayAgentActivityPublishProofExpiredError, |
| 634 | RelayAgentActivityPublishProofInvalidError, |
| 635 | RelayInternalError, |
| 636 | ] as const; |
| 637 | |
| 638 | export 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 | |
| 648 | export class RelayEnvironmentPrincipal extends Context.Service< |
| 649 | RelayEnvironmentPrincipal, |
| 650 | { |
| 651 | readonly environmentId: string; |
| 652 | readonly environmentPublicKey: string; |
| 653 | } |
| 654 | >()("@t3tools/contracts/relay/RelayEnvironmentPrincipal") {} |
| 655 | |
| 656 | const 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 | |
| 663 | export class RelayClientAuth extends HttpApiMiddleware.Service< |
| 664 | RelayClientAuth, |
| 665 | { provides: RelayClientPrincipal } |
| 666 | >()("RelayClientAuth", { |
| 667 | error: RelayAuthInvalidError, |
| 668 | security: { clientBearer: RelayClientBearerAuthorization }, |
| 669 | }) {} |
| 670 | |
| 671 | const 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 | |
| 678 | export class RelayEnvironmentAuth extends HttpApiMiddleware.Service< |
| 679 | RelayEnvironmentAuth, |
| 680 | { provides: RelayEnvironmentPrincipal } |
| 681 | >()("RelayEnvironmentAuth", { |
| 682 | error: [RelayAuthInvalidError, RelayInternalError], |
| 683 | security: { environmentBearer: RelayEnvironmentBearerAuthorization }, |
| 684 | }) {} |
| 685 | |
| 686 | const 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 | |
| 693 | export class RelayDpopClientAuth extends HttpApiMiddleware.Service< |
| 694 | RelayDpopClientAuth, |
| 695 | { provides: RelayClientPrincipal } |
| 696 | >()("RelayDpopClientAuth", { |
| 697 | error: RelayAuthInvalidError, |
| 698 | security: { relayDpop: RelayDpopAuthorization }, |
| 699 | }) {} |
| 700 | |
| 701 | export const RelayClientEnvironmentRecord = Schema.Struct({ |
| 702 | environmentId: EnvironmentId, |
| 703 | label: TrimmedNonEmptyString, |
| 704 | endpoint: RelayManagedEndpoint, |
| 705 | linkedAt: TrimmedNonEmptyString, |
| 706 | }); |
| 707 | export type RelayClientEnvironmentRecord = typeof RelayClientEnvironmentRecord.Type; |
| 708 | |
| 709 | export const RelayListEnvironmentsResponse = Schema.Struct({ |
| 710 | environments: Schema.Array(RelayClientEnvironmentRecord), |
| 711 | }); |
| 712 | export type RelayListEnvironmentsResponse = typeof RelayListEnvironmentsResponse.Type; |
| 713 | |
| 714 | export 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." }); |
| 731 | export type RelayEnvironmentConnectRequest = typeof RelayEnvironmentConnectRequest.Type; |
| 732 | |
| 733 | export const RelayEnvironmentConnectScope = "environment:connect" as const; |
| 734 | export const RelayEnvironmentStatusScope = "environment:status" as const; |
| 735 | export const RelayMobileRegistrationScope = "mobile:registration" as const; |
| 736 | export const RelayDpopAccessTokenScope = Schema.Literals([ |
| 737 | RelayEnvironmentConnectScope, |
| 738 | RelayEnvironmentStatusScope, |
| 739 | RelayMobileRegistrationScope, |
| 740 | ]); |
| 741 | export type RelayDpopAccessTokenScope = typeof RelayDpopAccessTokenScope.Type; |
| 742 | |
| 743 | export const RelayDpopTokenExchangeGrantType = |
| 744 | "urn:ietf:params:oauth:grant-type:token-exchange" as const; |
| 745 | export const RelayJwtSubjectTokenType = "urn:ietf:params:oauth:token-type:jwt" as const; |
| 746 | export const RelayAccessTokenType = "urn:ietf:params:oauth:token-type:access_token" as const; |
| 747 | export const RelayPublicClientId = Schema.Literals(["t3-mobile", "t3-web"]); |
| 748 | export type RelayPublicClientId = typeof RelayPublicClientId.Type; |
| 749 | export const RelayMobileClientId = "t3-mobile" as const; |
| 750 | export const RelayWebClientId = "t3-web" as const; |
| 751 | |
| 752 | export 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()); |
| 769 | export type RelayDpopAccessTokenRequest = typeof RelayDpopAccessTokenRequest.Type; |
| 770 | |
| 771 | export 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 | }); |
| 778 | export type RelayDpopAccessTokenResponse = typeof RelayDpopAccessTokenResponse.Type; |
| 779 | |
| 780 | export const RelayBearerRequestHeaders = Schema.Struct({ |
| 781 | authorization: TrimmedNonEmptyString, |
| 782 | }); |
| 783 | |
| 784 | export const RelayDpopProofRequestHeaders = Schema.Struct({ |
| 785 | dpop: TrimmedNonEmptyString, |
| 786 | }); |
| 787 | |
| 788 | export const RelayDpopRequestHeaders = Schema.Struct({ |
| 789 | authorization: TrimmedNonEmptyString, |
| 790 | dpop: TrimmedNonEmptyString, |
| 791 | }); |
| 792 | |
| 793 | export 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 | |
| 802 | export 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 | |
| 810 | export const RelayEnvironmentUnlinkParams = Schema.Struct({ |
| 811 | environmentId: EnvironmentId, |
| 812 | }); |
| 813 | export type RelayEnvironmentUnlinkParams = typeof RelayEnvironmentUnlinkParams.Type; |
| 814 | |
| 815 | export const RelayEnvironmentConnectResponse = Schema.Struct({ |
| 816 | environmentId: EnvironmentId, |
| 817 | endpoint: RelayManagedEndpoint, |
| 818 | credential: TrimmedNonEmptyString, |
| 819 | expiresAt: TrimmedNonEmptyString, |
| 820 | }); |
| 821 | export type RelayEnvironmentConnectResponse = typeof RelayEnvironmentConnectResponse.Type; |
| 822 | |
| 823 | export const RelayEnvironmentStatusValue = Schema.Literals(["online", "offline"]); |
| 824 | export 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 | */ |
| 831 | const RelayEnvironmentOfflineReason = Schema.Literals(["tunnel_released"]); |
| 832 | type RelayEnvironmentOfflineReason = typeof RelayEnvironmentOfflineReason.Type; |
| 833 | |
| 834 | export 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 | }); |
| 844 | export type RelayEnvironmentStatusResponse = typeof RelayEnvironmentStatusResponse.Type; |
| 845 | |
| 846 | export 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 | }); |
| 857 | export type RelayCloudMintCredentialProofPayload = typeof RelayCloudMintCredentialProofPayload.Type; |
| 858 | |
| 859 | export const RelayCloudMintCredentialProof = TrimmedNonEmptyString; |
| 860 | export type RelayCloudMintCredentialProof = typeof RelayCloudMintCredentialProof.Type; |
| 861 | |
| 862 | export const RelayCloudMintCredentialRequest = Schema.Struct({ |
| 863 | proof: RelayCloudMintCredentialProof, |
| 864 | }); |
| 865 | export type RelayCloudMintCredentialRequest = typeof RelayCloudMintCredentialRequest.Type; |
| 866 | |
| 867 | export const RelayCloudEnvironmentHealthProofPayload = Schema.Struct({ |
| 868 | ...RelaySignedJwtRegisteredClaims, |
| 869 | environmentId: EnvironmentId, |
| 870 | nonce: TrimmedNonEmptyString, |
| 871 | scope: Schema.Array(Schema.Literal("environment:status")), |
| 872 | }); |
| 873 | export 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 | */ |
| 882 | export const RelayHookDeliveryProofPayload = Schema.Struct({ |
| 883 | ...RelaySignedJwtRegisteredClaims, |
| 884 | environmentId: EnvironmentId, |
| 885 | deliveryId: TrimmedNonEmptyString, |
| 886 | receivedAt: TrimmedNonEmptyString, |
| 887 | hookId: TrimmedNonEmptyString, |
| 888 | }); |
| 889 | export type RelayHookDeliveryProofPayload = typeof RelayHookDeliveryProofPayload.Type; |
| 890 | |
| 891 | export const RelayCloudEnvironmentHealthProof = TrimmedNonEmptyString; |
| 892 | export type RelayCloudEnvironmentHealthProof = typeof RelayCloudEnvironmentHealthProof.Type; |
| 893 | |
| 894 | export const RelayCloudEnvironmentHealthRequest = Schema.Struct({ |
| 895 | proof: RelayCloudEnvironmentHealthProof, |
| 896 | }); |
| 897 | export type RelayCloudEnvironmentHealthRequest = typeof RelayCloudEnvironmentHealthRequest.Type; |
| 898 | |
| 899 | export const RelayEnvironmentHealthResponseProofPayload = Schema.Struct({ |
| 900 | ...RelaySignedJwtRegisteredClaims, |
| 901 | environmentId: EnvironmentId, |
| 902 | requestNonce: TrimmedNonEmptyString, |
| 903 | status: Schema.Literal("online"), |
| 904 | descriptor: ExecutionEnvironmentDescriptor, |
| 905 | checkedAt: TrimmedNonEmptyString, |
| 906 | }); |
| 907 | export type RelayEnvironmentHealthResponseProofPayload = |
| 908 | typeof RelayEnvironmentHealthResponseProofPayload.Type; |
| 909 | |
| 910 | export const RelayEnvironmentHealthResponse = Schema.Struct({ |
| 911 | environmentId: EnvironmentId, |
| 912 | status: Schema.Literal("online"), |
| 913 | descriptor: ExecutionEnvironmentDescriptor, |
| 914 | checkedAt: TrimmedNonEmptyString, |
| 915 | proof: TrimmedNonEmptyString, |
| 916 | }); |
| 917 | export type RelayEnvironmentHealthResponse = typeof RelayEnvironmentHealthResponse.Type; |
| 918 | |
| 919 | export const RelayEnvironmentMintResponseProofPayload = Schema.Struct({ |
| 920 | ...RelaySignedJwtRegisteredClaims, |
| 921 | environmentId: EnvironmentId, |
| 922 | clientProofKeyThumbprint: TrimmedNonEmptyString, |
| 923 | requestNonce: TrimmedNonEmptyString, |
| 924 | credential: TrimmedNonEmptyString, |
| 925 | }); |
| 926 | export type RelayEnvironmentMintResponseProofPayload = |
| 927 | typeof RelayEnvironmentMintResponseProofPayload.Type; |
| 928 | |
| 929 | export const RelayEnvironmentMintResponse = Schema.Struct({ |
| 930 | credential: TrimmedNonEmptyString, |
| 931 | expiresAt: TrimmedNonEmptyString, |
| 932 | proof: TrimmedNonEmptyString, |
| 933 | }); |
| 934 | export type RelayEnvironmentMintResponse = typeof RelayEnvironmentMintResponse.Type; |
| 935 | |
| 936 | export const RelayDeliveryKind = Schema.Literals([ |
| 937 | "live_activity_start", |
| 938 | "live_activity_update", |
| 939 | "live_activity_end", |
| 940 | "push_notification", |
| 941 | ]); |
| 942 | export type RelayDeliveryKind = typeof RelayDeliveryKind.Type; |
| 943 | |
| 944 | export 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 | }); |
| 953 | export type RelayDeliveryResult = typeof RelayDeliveryResult.Type; |
| 954 | |
| 955 | export const RelayOkResponse = Schema.Struct({ |
| 956 | ok: Schema.Boolean, |
| 957 | }); |
| 958 | export type RelayOkResponse = typeof RelayOkResponse.Type; |
| 959 | |
| 960 | export const RelayPublishResponse = Schema.Struct({ |
| 961 | ok: Schema.Boolean, |
| 962 | deliveries: Schema.Array(RelayDeliveryResult), |
| 963 | }); |
| 964 | export type RelayPublishResponse = typeof RelayPublishResponse.Type; |
| 965 | |
| 966 | export const RelayHealthResponse = Schema.Struct({ |
| 967 | ok: Schema.Boolean, |
| 968 | service: Schema.Literal("relay"), |
| 969 | }); |
| 970 | export type RelayHealthResponse = typeof RelayHealthResponse.Type; |
| 971 | |
| 972 | const 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 | |
| 981 | const 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 | |
| 992 | export 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 | |
| 1003 | export 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 | |
| 1014 | export const RelayAgentActivitySnapshotResponse = Schema.Struct({ |
| 1015 | aggregate: Schema.NullOr(RelayAgentActivityAggregateState), |
| 1016 | }); |
| 1017 | export 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. |
| 1022 | export 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 | |
| 1032 | export 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 | |
| 1043 | const 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 | |
| 1053 | const 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 | |
| 1111 | export 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 | |
| 1127 | const RelayTokenGroup = HttpApiGroup.make("token") |
| 1128 | .add(RelayExchangeDpopAccessTokenEndpoint) |
| 1129 | .annotate(OpenApi.Description, "OAuth token exchange for DPoP-bound client access."); |
| 1130 | |
| 1131 | export 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 | |
| 1145 | export 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 | |
| 1158 | const RelayDpopClientGroup = HttpApiGroup.make("dpopClient") |
| 1159 | .add(RelayConnectEnvironmentEndpoint, RelayGetEnvironmentStatusEndpoint) |
| 1160 | .annotate(OpenApi.Description, "DPoP-authenticated client access to linked environments.") |
| 1161 | .middleware(RelayDpopClientAuth); |
| 1162 | |
| 1163 | export const RelayEnvironmentLinkPreferencesRequest = Schema.Struct({ |
| 1164 | holdWebhooksWhileOffline: Schema.Boolean, |
| 1165 | }); |
| 1166 | export type RelayEnvironmentLinkPreferencesRequest = |
| 1167 | typeof RelayEnvironmentLinkPreferencesRequest.Type; |
| 1168 | |
| 1169 | export const RelayWakeHeldHooksResponse = Schema.Struct({ |
| 1170 | /** True when the relay was holding requests and has started delivering them. */ |
| 1171 | pending: Schema.Boolean, |
| 1172 | }); |
| 1173 | export type RelayWakeHeldHooksResponse = typeof RelayWakeHeldHooksResponse.Type; |
| 1174 | |
| 1175 | const 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 | */ |
| 1240 | const RelayHookParams = Schema.Struct({ |
| 1241 | endpointKey: Schema.String, |
| 1242 | hookId: Schema.String, |
| 1243 | token: Schema.String, |
| 1244 | }); |
| 1245 | const RELAY_HOOK_PATH = "/v1/hooks/:endpointKey/:hookId/:token"; |
| 1246 | const relayHookEndpoint = { params: RelayHookParams } as const; |
| 1247 | const 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 | |
| 1256 | export 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 | ); |
| 1273 | export type RelayApi = typeof RelayApi; |