authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-05 16:35:14-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-05 16:35:14-07:00
loga56496c7f20d53369ef87101681ee2a99b3a4337
treeab5c0f6d14af5fcadeba0246578336b94c18855a
parentb76f161d551235e1de89ed85c747f14041ac5d99
signature Signed by PGP key B5690EEEBB952194

Annotate relay error spans with schema fields (#2976)


20 files changed, 744 insertions(+), 232 deletions(-)

infra/relay/src/agentActivity/AgentActivityRows.ts+21-13
...@@ -1,7 +1,6 @@...@@ -1,7 +1,6 @@
1import type { RelayAgentActivityState } from "@t3tools/contracts/relay";1import type { RelayAgentActivityState } from "@t3tools/contracts/relay";
2import { RelayAgentActivityState as RelayAgentActivityStateSchema } from "@t3tools/contracts/relay";2import { RelayAgentActivityState as RelayAgentActivityStateSchema } from "@t3tools/contracts/relay";
3import * as Context from "effect/Context";3import * as Context from "effect/Context";
4import * as Data from "effect/Data";
5import * as DateTime from "effect/DateTime";4import * as DateTime from "effect/DateTime";
6import * as Effect from "effect/Effect";5import * as Effect from "effect/Effect";
7import { cast } from "effect/Function";6import { cast } from "effect/Function";
...@@ -13,23 +12,32 @@ import { and, desc, eq, isNull } from "drizzle-orm";...@@ -13,23 +12,32 @@ import { and, desc, eq, isNull } from "drizzle-orm";
13import { RelayDb } from "../db.ts";12import { RelayDb } from "../db.ts";
14import { relayAgentActivityRows, relayEnvironmentLinks } from "../persistence/schema.ts";13import { relayAgentActivityRows, relayEnvironmentLinks } from "../persistence/schema.ts";
1514
16export class AgentActivityRowUpsertPersistenceError extends Data.TaggedError(15export class AgentActivityRowUpsertPersistenceError extends Schema.TaggedErrorClass<AgentActivityRowUpsertPersistenceError>()(
17 "AgentActivityRowUpsertPersistenceError",16 "AgentActivityRowUpsertPersistenceError",
18)<{17 { cause: Schema.Defect() },
19 readonly cause: unknown;18) {
20}> {}19 override get message(): string {
20 return "Failed to persist agent activity state";
21 }
22}
2123
22export class AgentActivityRowDeletePersistenceError extends Data.TaggedError(24export class AgentActivityRowDeletePersistenceError extends Schema.TaggedErrorClass<AgentActivityRowDeletePersistenceError>()(
23 "AgentActivityRowDeletePersistenceError",25 "AgentActivityRowDeletePersistenceError",
24)<{26 { cause: Schema.Defect() },
25 readonly cause: unknown;27) {
26}> {}28 override get message(): string {
29 return "Failed to delete agent activity state";
30 }
31}
2732
28export class AgentActivityRowListPersistenceError extends Data.TaggedError(33export class AgentActivityRowListPersistenceError extends Schema.TaggedErrorClass<AgentActivityRowListPersistenceError>()(
29 "AgentActivityRowListPersistenceError",34 "AgentActivityRowListPersistenceError",
30)<{35 { cause: Schema.Defect() },
31 readonly cause: unknown;36) {
32}> {}37 override get message(): string {
38 return "Failed to list agent activity state";
39 }
40}
3341
34export interface AgentActivityRowsShape {42export interface AgentActivityRowsShape {
35 readonly upsert: (input: {43 readonly upsert: (input: {
infra/relay/src/agentActivity/ApnsClient.ts+32-18
...@@ -2,20 +2,13 @@ import * as NodeCrypto from "node:crypto";...@@ -2,20 +2,13 @@ import * as NodeCrypto from "node:crypto";
22
3import type { RelayAgentActivityAggregateState } from "@t3tools/contracts/relay";3import type { RelayAgentActivityAggregateState } from "@t3tools/contracts/relay";
4import * as Context from "effect/Context";4import * as Context from "effect/Context";
5import * as Data from "effect/Data";
6import * as Effect from "effect/Effect";5import * as Effect from "effect/Effect";
7import * as Encoding from "effect/Encoding";6import * as Encoding from "effect/Encoding";
8import * as Layer from "effect/Layer";7import * as Layer from "effect/Layer";
9import * as Option from "effect/Option";8import * as Option from "effect/Option";
10import * as Redacted from "effect/Redacted";9import * as Redacted from "effect/Redacted";
11import * as Schema from "effect/Schema";10import * as Schema from "effect/Schema";
12import {11import { Headers, HttpClient, HttpClientRequest } from "effect/unstable/http";
13 Headers,
14 HttpClient,
15 HttpClientRequest,
16 type HttpBody,
17 type HttpClientError,
18} from "effect/unstable/http";
19import type { ApnsCredentials } from "../Config.ts";12import type { ApnsCredentials } from "../Config.ts";
20import type { ApnsNotificationPayload } from "./apnsDeliveryJobs.ts";13import type { ApnsNotificationPayload } from "./apnsDeliveryJobs.ts";
2114
...@@ -45,18 +38,39 @@ export interface ApnsDeliveryResult {...@@ -45,18 +38,39 @@ export interface ApnsDeliveryResult {
45 readonly apnsId: string | null;38 readonly apnsId: string | null;
46}39}
4740
48export class ApnsSigningError extends Data.TaggedError("ApnsSigningError")<{41export class ApnsSigningError extends Schema.TaggedErrorClass<ApnsSigningError>()(
49 readonly phase: "encoding" | "signing";42 "ApnsSigningError",
50 readonly cause: unknown;43 {
51}> {}44 phase: Schema.Literals(["encoding", "signing"]),
45 cause: Schema.Defect(),
46 },
47) {
48 override get message(): string {
49 return `Failed during APNs JWT ${this.phase}`;
50 }
51}
5252
53export class ApnsHttpRequestError extends Data.TaggedError("ApnsHttpRequestError")<{53export class ApnsHttpRequestError extends Schema.TaggedErrorClass<ApnsHttpRequestError>()(
54 readonly cause: HttpClientError.HttpClientError | HttpBody.HttpBodyError;54 "ApnsHttpRequestError",
55}> {}55 {
56 cause: Schema.Defect(),
57 },
58) {
59 override get message(): string {
60 return "APNs HTTP request failed";
61 }
62}
5663
57export class ApnsInvalidResponseError extends Data.TaggedError("ApnsInvalidResponseError")<{64export class ApnsInvalidResponseError extends Schema.TaggedErrorClass<ApnsInvalidResponseError>()(
58 readonly cause: unknown;65 "ApnsInvalidResponseError",
59}> {}66 {
67 cause: Schema.Defect(),
68 },
69) {
70 override get message(): string {
71 return "APNs returned an invalid response";
72 }
73}
6074
61export type ApnsError = ApnsSigningError | ApnsHttpRequestError | ApnsInvalidResponseError;75export type ApnsError = ApnsSigningError | ApnsHttpRequestError | ApnsInvalidResponseError;
6276
infra/relay/src/agentActivity/ApnsDeliveries.ts+10-4
...@@ -9,7 +9,6 @@ import {...@@ -9,7 +9,6 @@ import {
9 RelayAgentAwarenessPreferences as RelayAgentAwarenessPreferencesSchema,9 RelayAgentAwarenessPreferences as RelayAgentAwarenessPreferencesSchema,
10} from "@t3tools/contracts/relay";10} from "@t3tools/contracts/relay";
11import * as Context from "effect/Context";11import * as Context from "effect/Context";
12import * as Data from "effect/Data";
13import * as DateTime from "effect/DateTime";12import * as DateTime from "effect/DateTime";
14import * as Effect from "effect/Effect";13import * as Effect from "effect/Effect";
15import * as Layer from "effect/Layer";14import * as Layer from "effect/Layer";
...@@ -74,9 +73,16 @@ export type ApnsDeliveryError =...@@ -74,9 +73,16 @@ export type ApnsDeliveryError =
74 | LiveActivities.LiveActivityTargetListPersistenceError73 | LiveActivities.LiveActivityTargetListPersistenceError
75 | LiveActivities.LiveActivityDeliveryMarkPersistenceError;74 | LiveActivities.LiveActivityDeliveryMarkPersistenceError;
7675
77export class ApnsDeliveryJobClaimInFlight extends Data.TaggedError("ApnsDeliveryJobClaimInFlight")<{76export class ApnsDeliveryJobClaimInFlight extends Schema.TaggedErrorClass<ApnsDeliveryJobClaimInFlight>()(
78 readonly sourceJobId: string;77 "ApnsDeliveryJobClaimInFlight",
79}> {}78 {
79 sourceJobId: Schema.String,
80 },
81) {
82 override get message(): string {
83 return `APNs delivery job '${this.sourceJobId}' is already in flight`;
84 }
85}
8086
81const decodeRelayAgentActivityAggregateStateJson = Schema.decodeUnknownOption(87const decodeRelayAgentActivityAggregateStateJson = Schema.decodeUnknownOption(
82 Schema.fromJsonString(RelayAgentActivityAggregateStateSchema),88 Schema.fromJsonString(RelayAgentActivityAggregateStateSchema),
infra/relay/src/agentActivity/ApnsDeliveryQueue.ts+9-4
...@@ -2,10 +2,10 @@ import * as Alchemy from "alchemy";...@@ -2,10 +2,10 @@ import * as Alchemy from "alchemy";
2import * as Cloudflare from "alchemy/Cloudflare";2import * as Cloudflare from "alchemy/Cloudflare";
3import * as Crypto from "effect/Crypto";3import * as Crypto from "effect/Crypto";
4import * as Context from "effect/Context";4import * as Context from "effect/Context";
5import * as Data from "effect/Data";
6import * as DateTime from "effect/DateTime";5import * as DateTime from "effect/DateTime";
7import * as Effect from "effect/Effect";6import * as Effect from "effect/Effect";
8import * as Layer from "effect/Layer";7import * as Layer from "effect/Layer";
8import * as Schema from "effect/Schema";
99
10import type { RelayDeliveryResult } from "@t3tools/contracts/relay";10import type { RelayDeliveryResult } from "@t3tools/contracts/relay";
1111
...@@ -22,9 +22,14 @@ import {...@@ -22,9 +22,14 @@ import {
22} from "./apnsDeliveryJobs.ts";22} from "./apnsDeliveryJobs.ts";
23import * as RelayConfiguration from "../Config.ts";23import * as RelayConfiguration from "../Config.ts";
2424
25export class ApnsDeliveryQueueSendError extends Data.TaggedError("ApnsDeliveryQueueSendError")<{25export class ApnsDeliveryQueueSendError extends Schema.TaggedErrorClass<ApnsDeliveryQueueSendError>()(
26 readonly cause: unknown;26 "ApnsDeliveryQueueSendError",
27}> {}27 { cause: Schema.Defect() },
28) {
29 override get message(): string {
30 return "Failed to enqueue APNs delivery";
31 }
32}
2833
29export type ApnsDeliveryQueueError = ApnsDeliveryQueueSendError;34export type ApnsDeliveryQueueError = ApnsDeliveryQueueSendError;
3035
infra/relay/src/agentActivity/DeliveryAttempts.ts+8-5
...@@ -1,20 +1,23 @@...@@ -1,20 +1,23 @@
1import * as Context from "effect/Context";1import * as Context from "effect/Context";
2import * as Data from "effect/Data";
3import * as DateTime from "effect/DateTime";2import * as DateTime from "effect/DateTime";
4import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";4import * as Layer from "effect/Layer";
6import * as Option from "effect/Option";5import * as Option from "effect/Option";
7import { and, eq, isNull } from "drizzle-orm";6import { and, eq, isNull } from "drizzle-orm";
8import * as Crypto from "effect/Crypto";7import * as Crypto from "effect/Crypto";
8import * as Schema from "effect/Schema";
99
10import { RelayDb } from "../db.ts";10import { RelayDb } from "../db.ts";
11import { relayDeliveryAttempts } from "../persistence/schema.ts";11import { relayDeliveryAttempts } from "../persistence/schema.ts";
1212
13export class DeliveryAttemptRecordPersistenceError extends Data.TaggedError(13export class DeliveryAttemptRecordPersistenceError extends Schema.TaggedErrorClass<DeliveryAttemptRecordPersistenceError>()(
14 "DeliveryAttemptRecordPersistenceError",14 "DeliveryAttemptRecordPersistenceError",
15)<{15 { cause: Schema.Defect() },
16 readonly cause: unknown;16) {
17}> {}17 override get message(): string {
18 return "Failed to persist APNs delivery attempt";
19 }
20}
1821
19export interface DeliveryAttemptInput {22export interface DeliveryAttemptInput {
20 readonly userId: string | null;23 readonly userId: string | null;
infra/relay/src/agentActivity/Devices.ts+23-12
...@@ -3,31 +3,42 @@ import type {...@@ -3,31 +3,42 @@ import type {
3 RelayDeviceRegistrationRequest,3 RelayDeviceRegistrationRequest,
4} from "@t3tools/contracts/relay";4} from "@t3tools/contracts/relay";
5import * as Context from "effect/Context";5import * as Context from "effect/Context";
6import * as Data from "effect/Data";
7import * as DateTime from "effect/DateTime";6import * as DateTime from "effect/DateTime";
8import * as Effect from "effect/Effect";7import * as Effect from "effect/Effect";
9import * as Layer from "effect/Layer";8import * as Layer from "effect/Layer";
9import * as Schema from "effect/Schema";
10import { and, eq } from "drizzle-orm";10import { and, eq } from "drizzle-orm";
11import { sql } from "drizzle-orm";11import { sql } from "drizzle-orm";
1212
13import { RelayDb } from "../db.ts";13import { RelayDb } from "../db.ts";
14import { relayLiveActivities, relayMobileDevices } from "../persistence/schema.ts";14import { relayLiveActivities, relayMobileDevices } from "../persistence/schema.ts";
1515
16export class DeviceRegistrationPersistenceError extends Data.TaggedError(16export class DeviceRegistrationPersistenceError extends Schema.TaggedErrorClass<DeviceRegistrationPersistenceError>()(
17 "DeviceRegistrationPersistenceError",17 "DeviceRegistrationPersistenceError",
18)<{18 { cause: Schema.Defect() },
19 readonly cause: unknown;19) {
20}> {}20 override get message(): string {
21 return "Failed to persist mobile device registration";
22 }
23}
2124
22export class DeviceUnregistrationPersistenceError extends Data.TaggedError(25export class DeviceUnregistrationPersistenceError extends Schema.TaggedErrorClass<DeviceUnregistrationPersistenceError>()(
23 "DeviceUnregistrationPersistenceError",26 "DeviceUnregistrationPersistenceError",
24)<{27 { cause: Schema.Defect() },
25 readonly cause: unknown;28) {
26}> {}29 override get message(): string {
30 return "Failed to unregister mobile device";
31 }
32}
2733
28export class DeviceListPersistenceError extends Data.TaggedError("DeviceListPersistenceError")<{34export class DeviceListPersistenceError extends Schema.TaggedErrorClass<DeviceListPersistenceError>()(
29 readonly cause: unknown;35 "DeviceListPersistenceError",
30}> {}36 { cause: Schema.Defect() },
37) {
38 override get message(): string {
39 return "Failed to list mobile devices";
40 }
41}
3142
32export interface DevicesShape {43export interface DevicesShape {
33 readonly register: (input: {44 readonly register: (input: {
infra/relay/src/agentActivity/LiveActivities.ts+21-13
...@@ -5,7 +5,6 @@ import type {...@@ -5,7 +5,6 @@ import type {
5} from "@t3tools/contracts/relay";5} from "@t3tools/contracts/relay";
6import { RelayAgentActivityAggregateState as RelayAgentActivityAggregateStateSchema } from "@t3tools/contracts/relay";6import { RelayAgentActivityAggregateState as RelayAgentActivityAggregateStateSchema } from "@t3tools/contracts/relay";
7import * as Context from "effect/Context";7import * as Context from "effect/Context";
8import * as Data from "effect/Data";
9import * as DateTime from "effect/DateTime";8import * as DateTime from "effect/DateTime";
10import * as Effect from "effect/Effect";9import * as Effect from "effect/Effect";
11import { cast } from "effect/Function";10import { cast } from "effect/Function";
...@@ -16,23 +15,32 @@ import { and, eq, sql } from "drizzle-orm";...@@ -16,23 +15,32 @@ import { and, eq, sql } from "drizzle-orm";
16import { RelayDb } from "../db.ts";15import { RelayDb } from "../db.ts";
17import { relayLiveActivities, relayMobileDevices } from "../persistence/schema.ts";16import { relayLiveActivities, relayMobileDevices } from "../persistence/schema.ts";
1817
19export class LiveActivityRegistrationPersistenceError extends Data.TaggedError(18export class LiveActivityRegistrationPersistenceError extends Schema.TaggedErrorClass<LiveActivityRegistrationPersistenceError>()(
20 "LiveActivityRegistrationPersistenceError",19 "LiveActivityRegistrationPersistenceError",
21)<{20 { cause: Schema.Defect() },
22 readonly cause: unknown;21) {
23}> {}22 override get message(): string {
23 return "Failed to persist Live Activity registration";
24 }
25}
2426
25export class LiveActivityTargetListPersistenceError extends Data.TaggedError(27export class LiveActivityTargetListPersistenceError extends Schema.TaggedErrorClass<LiveActivityTargetListPersistenceError>()(
26 "LiveActivityTargetListPersistenceError",28 "LiveActivityTargetListPersistenceError",
27)<{29 { cause: Schema.Defect() },
28 readonly cause: unknown;30) {
29}> {}31 override get message(): string {
32 return "Failed to list Live Activity delivery targets";
33 }
34}
3035
31export class LiveActivityDeliveryMarkPersistenceError extends Data.TaggedError(36export class LiveActivityDeliveryMarkPersistenceError extends Schema.TaggedErrorClass<LiveActivityDeliveryMarkPersistenceError>()(
32 "LiveActivityDeliveryMarkPersistenceError",37 "LiveActivityDeliveryMarkPersistenceError",
33)<{38 { cause: Schema.Defect() },
34 readonly cause: unknown;39) {
35}> {}40 override get message(): string {
41 return "Failed to persist Live Activity delivery state";
42 }
43}
3644
37export interface DeviceRow {45export interface DeviceRow {
38 readonly user_id: string;46 readonly user_id: string;
infra/relay/src/agentActivity/apnsDeliveryJobs.ts+17-8
...@@ -2,7 +2,6 @@ import * as NodeCrypto from "node:crypto";...@@ -2,7 +2,6 @@ import * as NodeCrypto from "node:crypto";
22
3import { RelayAgentActivityAggregateState, type RelayDeliveryKind } from "@t3tools/contracts/relay";3import { RelayAgentActivityAggregateState, type RelayDeliveryKind } from "@t3tools/contracts/relay";
4import { stableStringify } from "@t3tools/shared/relaySigning";4import { stableStringify } from "@t3tools/shared/relaySigning";
5import * as Data from "effect/Data";
6import * as DateTime from "effect/DateTime";5import * as DateTime from "effect/DateTime";
7import * as Option from "effect/Option";6import * as Option from "effect/Option";
8import * as Redacted from "effect/Redacted";7import * as Redacted from "effect/Redacted";
...@@ -50,13 +49,23 @@ export const SignedApnsDeliveryJob = Schema.Struct({...@@ -50,13 +49,23 @@ export const SignedApnsDeliveryJob = Schema.Struct({
50});49});
51export type SignedApnsDeliveryJob = typeof SignedApnsDeliveryJob.Type;50export type SignedApnsDeliveryJob = typeof SignedApnsDeliveryJob.Type;
5251
53export class ApnsDeliveryJobInvalid extends Data.TaggedError("ApnsDeliveryJobInvalid")<{52export class ApnsDeliveryJobInvalid extends Schema.TaggedErrorClass<ApnsDeliveryJobInvalid>()(
54 readonly message: string;53 "ApnsDeliveryJobInvalid",
55}> {}54 {
5655 message: Schema.String,
57export class ApnsDeliveryJobExpired extends Data.TaggedError("ApnsDeliveryJobExpired")<{56 },
58 readonly expiresAt: string;57) {}
59}> {}58
59export class ApnsDeliveryJobExpired extends Schema.TaggedErrorClass<ApnsDeliveryJobExpired>()(
60 "ApnsDeliveryJobExpired",
61 {
62 expiresAt: Schema.String,
63 },
64) {
65 override get message(): string {
66 return `APNs delivery job expired at ${this.expiresAt}`;
67 }
68}
6069
61export type ApnsDeliveryJobVerificationError = ApnsDeliveryJobInvalid | ApnsDeliveryJobExpired;70export type ApnsDeliveryJobVerificationError = ApnsDeliveryJobInvalid | ApnsDeliveryJobExpired;
6271
infra/relay/src/auth/DpopProofs.ts+10-5
...@@ -1,8 +1,8 @@...@@ -1,8 +1,8 @@
1import * as Context from "effect/Context";1import * as Context from "effect/Context";
2import * as Data from "effect/Data";
3import * as DateTime from "effect/DateTime";2import * as DateTime from "effect/DateTime";
4import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";4import * as Layer from "effect/Layer";
5import * as Schema from "effect/Schema";
6import * as HttpApiError from "effect/unstable/httpapi/HttpApiError";6import * as HttpApiError from "effect/unstable/httpapi/HttpApiError";
7import { lt } from "drizzle-orm";7import { lt } from "drizzle-orm";
88
...@@ -10,11 +10,16 @@ import { verifyDpopProof } from "@t3tools/shared/dpop";...@@ -10,11 +10,16 @@ import { verifyDpopProof } from "@t3tools/shared/dpop";
10import { RelayDb } from "../db.ts";10import { RelayDb } from "../db.ts";
11import { relayDpopProofs } from "../persistence/schema.ts";11import { relayDpopProofs } from "../persistence/schema.ts";
1212
13export class DpopProofReplayPersistenceError extends Data.TaggedError(13export class DpopProofReplayPersistenceError extends Schema.TaggedErrorClass<DpopProofReplayPersistenceError>()(
14 "DpopProofReplayPersistenceError",14 "DpopProofReplayPersistenceError",
15)<{15 {
16 readonly cause: unknown;16 cause: Schema.Defect(),
17}> {}17 },
18) {
19 override get message(): string {
20 return "Failed to persist DPoP proof replay state";
21 }
22}
1823
19export interface DpopProofReplayShape {24export interface DpopProofReplayShape {
20 readonly verifyAndConsume: (input: {25 readonly verifyAndConsume: (input: {
infra/relay/src/environments/EnvironmentConnector.ts+109-26
...@@ -28,7 +28,6 @@ import {...@@ -28,7 +28,6 @@ import {
28import { stableStringify } from "@t3tools/shared/relaySigning";28import { stableStringify } from "@t3tools/shared/relaySigning";
29import * as Context from "effect/Context";29import * as Context from "effect/Context";
30import * as Crypto from "effect/Crypto";30import * as Crypto from "effect/Crypto";
31import * as Data from "effect/Data";
32import * as DateTime from "effect/DateTime";31import * as DateTime from "effect/DateTime";
33import * as Duration from "effect/Duration";32import * as Duration from "effect/Duration";
34import * as Effect from "effect/Effect";33import * as Effect from "effect/Effect";
...@@ -42,28 +41,53 @@ import * as EnvironmentLinks from "./EnvironmentLinks.ts";...@@ -42,28 +41,53 @@ import * as EnvironmentLinks from "./EnvironmentLinks.ts";
42import * as ManagedEndpointAllocations from "./ManagedEndpointAllocations.ts";41import * as ManagedEndpointAllocations from "./ManagedEndpointAllocations.ts";
43import * as RelayConfiguration from "../Config.ts";42import * as RelayConfiguration from "../Config.ts";
4443
45export class EnvironmentConnectNotAuthorized extends Data.TaggedError(44export class EnvironmentConnectNotAuthorized extends Schema.TaggedErrorClass<EnvironmentConnectNotAuthorized>()(
46 "EnvironmentConnectNotAuthorized",45 "EnvironmentConnectNotAuthorized",
47)<{46 {
48 readonly environmentId: string;47 environmentId: Schema.String,
49}> {}48 },
49) {
50 override get message(): string {
51 return `Environment '${this.environmentId}' is not authorized to connect`;
52 }
53}
5054
51export class EnvironmentMintRequestFailed extends Data.TaggedError("EnvironmentMintRequestFailed")<{55export class EnvironmentMintRequestFailed extends Schema.TaggedErrorClass<EnvironmentMintRequestFailed>()(
52 readonly cause: unknown;56 "EnvironmentMintRequestFailed",
53}> {}57 {
58 environmentId: Schema.String,
59 operation: Schema.Literals(["connect", "status"]),
60 cause: Schema.Defect(),
61 },
62) {
63 override get message(): string {
64 return `Environment '${this.environmentId}' ${this.operation} request failed`;
65 }
66}
5467
55export class EnvironmentMintRequestTimedOut extends Data.TaggedError(68export class EnvironmentMintRequestTimedOut extends Schema.TaggedErrorClass<EnvironmentMintRequestTimedOut>()(
56 "EnvironmentMintRequestTimedOut",69 "EnvironmentMintRequestTimedOut",
57)<{70 {
58 readonly environmentId: string;71 environmentId: Schema.String,
59 readonly timeoutMs: number;72 timeoutMs: Schema.Number,
60}> {}73 },
74) {
75 override get message(): string {
76 return `Environment '${this.environmentId}' mint request timed out after ${this.timeoutMs}ms`;
77 }
78}
6179
62export class EnvironmentMintResponseInvalid extends Data.TaggedError(80export class EnvironmentMintResponseInvalid extends Schema.TaggedErrorClass<EnvironmentMintResponseInvalid>()(
63 "EnvironmentMintResponseInvalid",81 "EnvironmentMintResponseInvalid",
64)<{82 {
65 readonly environmentId: string;83 environmentId: Schema.String,
66}> {}84 operation: Schema.Literals(["connect", "status"]),
85 },
86) {
87 override get message(): string {
88 return `Environment '${this.environmentId}' returned an invalid ${this.operation} response`;
89 }
90}
6791
68export type EnvironmentConnectorError =92export type EnvironmentConnectorError =
69 | EnvironmentConnectNotAuthorized93 | EnvironmentConnectNotAuthorized
...@@ -277,14 +301,28 @@ const make = Effect.gen(function* () {...@@ -277,14 +301,28 @@ const make = Effect.gen(function* () {
277 const now = yield* DateTime.now;301 const now = yield* DateTime.now;
278 const expiresAt = DateTime.add(now, { minutes: 2 });302 const expiresAt = DateTime.add(now, { minutes: 2 });
279 const nonce = yield* crypto.randomUUIDv4.pipe(303 const nonce = yield* crypto.randomUUIDv4.pipe(
280 Effect.mapError((cause) => new EnvironmentMintRequestFailed({ cause })),304 Effect.mapError(
305 (cause) =>
306 new EnvironmentMintRequestFailed({
307 environmentId: input.environmentId,
308 operation: "status",
309 cause,
310 }),
311 ),
281 );312 );
282 const payload = {313 const payload = {
283 iss: relayIssuer,314 iss: relayIssuer,
284 aud: `t3-env:${link.environmentId}`,315 aud: `t3-env:${link.environmentId}`,
285 sub: input.userId,316 sub: input.userId,
286 jti: yield* crypto.randomUUIDv4.pipe(317 jti: yield* crypto.randomUUIDv4.pipe(
287 Effect.mapError((cause) => new EnvironmentMintRequestFailed({ cause })),318 Effect.mapError(
319 (cause) =>
320 new EnvironmentMintRequestFailed({
321 environmentId: input.environmentId,
322 operation: "status",
323 cause,
324 }),
325 ),
288 ),326 ),
289 iat: Math.floor(now.epochMilliseconds / 1_000),327 iat: Math.floor(now.epochMilliseconds / 1_000),
290 exp: Math.floor(expiresAt.epochMilliseconds / 1_000),328 exp: Math.floor(expiresAt.epochMilliseconds / 1_000),
...@@ -296,7 +334,16 @@ const make = Effect.gen(function* () {...@@ -296,7 +334,16 @@ const make = Effect.gen(function* () {
296 privateKey: Redacted.value(settings.cloudMintPrivateKey),334 privateKey: Redacted.value(settings.cloudMintPrivateKey),
297 typ: RELAY_HEALTH_REQUEST_TYP,335 typ: RELAY_HEALTH_REQUEST_TYP,
298 payload,336 payload,
299 }).pipe(Effect.mapError((cause) => new EnvironmentMintRequestFailed({ cause })));337 }).pipe(
338 Effect.mapError(
339 (cause) =>
340 new EnvironmentMintRequestFailed({
341 environmentId: input.environmentId,
342 operation: "status",
343 cause,
344 }),
345 ),
346 );
300 const checkedAt = DateTime.formatIso(now);347 const checkedAt = DateTime.formatIso(now);
301 const environmentClient = yield* makeEnvironmentClient(endpoint.httpBaseUrl);348 const environmentClient = yield* makeEnvironmentClient(endpoint.httpBaseUrl);
302 const responseOption = yield* environmentClient.cloud.health({ payload: { proof } }).pipe(349 const responseOption = yield* environmentClient.cloud.health({ payload: { proof } }).pipe(
...@@ -336,7 +383,10 @@ const make = Effect.gen(function* () {...@@ -336,7 +383,10 @@ const make = Effect.gen(function* () {
336 now: yield* DateTime.now,383 now: yield* DateTime.now,
337 });384 });
338 if (!verified) {385 if (!verified) {
339 return yield* new EnvironmentMintResponseInvalid({ environmentId: input.environmentId });386 return yield* new EnvironmentMintResponseInvalid({
387 environmentId: input.environmentId,
388 operation: "status",
389 });
340 }390 }
341 return {391 return {
342 environmentId: link.environmentId,392 environmentId: link.environmentId,
...@@ -364,14 +414,28 @@ const make = Effect.gen(function* () {...@@ -364,14 +414,28 @@ const make = Effect.gen(function* () {
364 const now = yield* DateTime.now;414 const now = yield* DateTime.now;
365 const expiresAt = DateTime.add(now, { minutes: 2 });415 const expiresAt = DateTime.add(now, { minutes: 2 });
366 const nonce = yield* crypto.randomUUIDv4.pipe(416 const nonce = yield* crypto.randomUUIDv4.pipe(
367 Effect.mapError((cause) => new EnvironmentMintRequestFailed({ cause })),417 Effect.mapError(
418 (cause) =>
419 new EnvironmentMintRequestFailed({
420 environmentId: input.environmentId,
421 operation: "connect",
422 cause,
423 }),
424 ),
368 );425 );
369 const payload = {426 const payload = {
370 iss: relayIssuer,427 iss: relayIssuer,
371 aud: `t3-env:${link.environmentId}`,428 aud: `t3-env:${link.environmentId}`,
372 sub: input.userId,429 sub: input.userId,
373 jti: yield* crypto.randomUUIDv4.pipe(430 jti: yield* crypto.randomUUIDv4.pipe(
374 Effect.mapError((cause) => new EnvironmentMintRequestFailed({ cause })),431 Effect.mapError(
432 (cause) =>
433 new EnvironmentMintRequestFailed({
434 environmentId: input.environmentId,
435 operation: "connect",
436 cause,
437 }),
438 ),
375 ),439 ),
376 iat: Math.floor(now.epochMilliseconds / 1_000),440 iat: Math.floor(now.epochMilliseconds / 1_000),
377 exp: Math.floor(expiresAt.epochMilliseconds / 1_000),441 exp: Math.floor(expiresAt.epochMilliseconds / 1_000),
...@@ -386,11 +450,27 @@ const make = Effect.gen(function* () {...@@ -386,11 +450,27 @@ const make = Effect.gen(function* () {
386 privateKey: Redacted.value(settings.cloudMintPrivateKey),450 privateKey: Redacted.value(settings.cloudMintPrivateKey),
387 typ: RELAY_MINT_REQUEST_TYP,451 typ: RELAY_MINT_REQUEST_TYP,
388 payload,452 payload,
389 }).pipe(Effect.mapError((cause) => new EnvironmentMintRequestFailed({ cause })));453 }).pipe(
454 Effect.mapError(
455 (cause) =>
456 new EnvironmentMintRequestFailed({
457 environmentId: input.environmentId,
458 operation: "connect",
459 cause,
460 }),
461 ),
462 );
390 const environmentClient = yield* makeEnvironmentClient(endpoint.httpBaseUrl);463 const environmentClient = yield* makeEnvironmentClient(endpoint.httpBaseUrl);
391 const decoded = yield* environmentClient.cloud.t3MintCredential({ payload: { proof } }).pipe(464 const decoded = yield* environmentClient.cloud.t3MintCredential({ payload: { proof } }).pipe(
392 withoutRedirects,465 withoutRedirects,
393 Effect.mapError((cause) => new EnvironmentMintRequestFailed({ cause })),466 Effect.mapError(
467 (cause) =>
468 new EnvironmentMintRequestFailed({
469 environmentId: input.environmentId,
470 operation: "connect",
471 cause,
472 }),
473 ),
394 Effect.timeoutOption(Duration.millis(ENVIRONMENT_MINT_REQUEST_TIMEOUT_MS)),474 Effect.timeoutOption(Duration.millis(ENVIRONMENT_MINT_REQUEST_TIMEOUT_MS)),
395 Effect.flatMap(475 Effect.flatMap(
396 Option.match({476 Option.match({
...@@ -415,7 +495,10 @@ const make = Effect.gen(function* () {...@@ -415,7 +495,10 @@ const make = Effect.gen(function* () {
415 nowEpochSeconds: Math.floor(now.epochMilliseconds / 1_000),495 nowEpochSeconds: Math.floor(now.epochMilliseconds / 1_000),
416 });496 });
417 if (!verified) {497 if (!verified) {
418 return yield* new EnvironmentMintResponseInvalid({ environmentId: input.environmentId });498 return yield* new EnvironmentMintResponseInvalid({
499 environmentId: input.environmentId,
500 operation: "connect",
501 });
419 }502 }
420 return {503 return {
421 environmentId: link.environmentId,504 environmentId: link.environmentId,
infra/relay/src/environments/EnvironmentCredentials.ts+22-13
...@@ -1,33 +1,42 @@...@@ -1,33 +1,42 @@
1import * as Context from "effect/Context";1import * as Context from "effect/Context";
2import * as Crypto from "effect/Crypto";2import * as Crypto from "effect/Crypto";
3import * as Data from "effect/Data";
4import * as DateTime from "effect/DateTime";3import * as DateTime from "effect/DateTime";
5import * as Effect from "effect/Effect";4import * as Effect from "effect/Effect";
6import * as Encoding from "effect/Encoding";5import * as Encoding from "effect/Encoding";
7import * as Layer from "effect/Layer";6import * as Layer from "effect/Layer";
8import * as Option from "effect/Option";7import * as Option from "effect/Option";
8import * as Schema from "effect/Schema";
9import { and, eq, isNull, ne, notExists } from "drizzle-orm";9import { and, eq, isNull, ne, notExists } from "drizzle-orm";
1010
11import { RelayDb } from "../db.ts";11import { RelayDb } from "../db.ts";
12import { relayEnvironmentCredentials, relayEnvironmentLinks } from "../persistence/schema.ts";12import { relayEnvironmentCredentials, relayEnvironmentLinks } from "../persistence/schema.ts";
1313
14export class EnvironmentCredentialCreatePersistenceError extends Data.TaggedError(14export class EnvironmentCredentialCreatePersistenceError extends Schema.TaggedErrorClass<EnvironmentCredentialCreatePersistenceError>()(
15 "EnvironmentCredentialCreatePersistenceError",15 "EnvironmentCredentialCreatePersistenceError",
16)<{16 { cause: Schema.Defect() },
17 readonly cause: unknown;17) {
18}> {}18 override get message(): string {
19 return "Failed to persist environment credential";
20 }
21}
1922
20export class EnvironmentCredentialAuthenticatePersistenceError extends Data.TaggedError(23export class EnvironmentCredentialAuthenticatePersistenceError extends Schema.TaggedErrorClass<EnvironmentCredentialAuthenticatePersistenceError>()(
21 "EnvironmentCredentialAuthenticatePersistenceError",24 "EnvironmentCredentialAuthenticatePersistenceError",
22)<{25 { cause: Schema.Defect() },
23 readonly cause: unknown;26) {
24}> {}27 override get message(): string {
28 return "Failed to authenticate environment credential";
29 }
30}
2531
26export class EnvironmentCredentialRevokePersistenceError extends Data.TaggedError(32export class EnvironmentCredentialRevokePersistenceError extends Schema.TaggedErrorClass<EnvironmentCredentialRevokePersistenceError>()(
27 "EnvironmentCredentialRevokePersistenceError",33 "EnvironmentCredentialRevokePersistenceError",
28)<{34 { cause: Schema.Defect() },
29 readonly cause: unknown;35) {
30}> {}36 override get message(): string {
37 return "Failed to revoke environment credential";
38 }
39}
3140
32export interface EnvironmentCredentialPrincipal {41export interface EnvironmentCredentialPrincipal {
33 readonly credentialId: string;42 readonly credentialId: string;
infra/relay/src/environments/EnvironmentLinker.ts+22-9
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import {1import {
2 RelayEnvironmentLinkProofPayload,2 RelayEnvironmentLinkProofPayload,
3 type RelayEnvironmentLinkProofInvalidReason,3 RelayEnvironmentLinkProofInvalidReason,
4 type RelayEnvironmentLinkRequest,4 type RelayEnvironmentLinkRequest,
5} from "@t3tools/contracts/relay";5} from "@t3tools/contracts/relay";
6import {6import {
...@@ -10,7 +10,6 @@ import {...@@ -10,7 +10,6 @@ import {
10 verifyRelayJwt,10 verifyRelayJwt,
11} from "@t3tools/shared/relayJwt";11} from "@t3tools/shared/relayJwt";
12import * as Context from "effect/Context";12import * as Context from "effect/Context";
13import * as Data from "effect/Data";
14import * as DateTime from "effect/DateTime";13import * as DateTime from "effect/DateTime";
15import * as Effect from "effect/Effect";14import * as Effect from "effect/Effect";
16import * as Layer from "effect/Layer";15import * as Layer from "effect/Layer";
...@@ -23,14 +22,28 @@ import * as EnvironmentLinks from "./EnvironmentLinks.ts";...@@ -23,14 +22,28 @@ import * as EnvironmentLinks from "./EnvironmentLinks.ts";
23import * as ManagedEndpointProvider from "./ManagedEndpointProvider.ts";22import * as ManagedEndpointProvider from "./ManagedEndpointProvider.ts";
24import * as RelayConfiguration from "../Config.ts";23import * as RelayConfiguration from "../Config.ts";
2524
26export class EnvironmentLinkProofExpired extends Data.TaggedError("EnvironmentLinkProofExpired")<{25export class EnvironmentLinkProofExpired extends Schema.TaggedErrorClass<EnvironmentLinkProofExpired>()(
27 readonly expiresAt: string;26 "EnvironmentLinkProofExpired",
28}> {}27 {
28 expiresAt: Schema.String,
29 },
30) {
31 override get message(): string {
32 return `Environment link proof expired at ${this.expiresAt}`;
33 }
34}
2935
30export class EnvironmentLinkProofInvalid extends Data.TaggedError("EnvironmentLinkProofInvalid")<{36export class EnvironmentLinkProofInvalid extends Schema.TaggedErrorClass<EnvironmentLinkProofInvalid>()(
31 readonly environmentId: string;37 "EnvironmentLinkProofInvalid",
32 readonly reason: RelayEnvironmentLinkProofInvalidReason;38 {
33}> {}39 environmentId: Schema.String,
40 reason: RelayEnvironmentLinkProofInvalidReason,
41 },
42) {
43 override get message(): string {
44 return `Environment '${this.environmentId}' link proof is invalid: ${this.reason}`;
45 }
46}
3447
35export type EnvironmentLinkError =48export type EnvironmentLinkError =
36 | EnvironmentLinkProofExpired49 | EnvironmentLinkProofExpired
infra/relay/src/environments/EnvironmentLinks.ts+43-25
...@@ -5,10 +5,10 @@ import type {...@@ -5,10 +5,10 @@ import type {
5 RelayManagedEndpoint,5 RelayManagedEndpoint,
6} from "@t3tools/contracts/relay";6} from "@t3tools/contracts/relay";
7import * as Context from "effect/Context";7import * as Context from "effect/Context";
8import * as Data from "effect/Data";
9import * as DateTime from "effect/DateTime";8import * as DateTime from "effect/DateTime";
10import * as Effect from "effect/Effect";9import * as Effect from "effect/Effect";
11import * as Layer from "effect/Layer";10import * as Layer from "effect/Layer";
11import * as Schema from "effect/Schema";
12import { and, eq, isNull, or } from "drizzle-orm";12import { and, eq, isNull, or } from "drizzle-orm";
1313
14import { RelayDb } from "../db.ts";14import { RelayDb } from "../db.ts";
...@@ -24,41 +24,59 @@ export interface AgentAwarenessDeliveryUserRecord {...@@ -24,41 +24,59 @@ export interface AgentAwarenessDeliveryUserRecord {
24 readonly liveActivitiesEnabled: boolean;24 readonly liveActivitiesEnabled: boolean;
25}25}
2626
27export class EnvironmentLinkUpsertPersistenceError extends Data.TaggedError(27export class EnvironmentLinkUpsertPersistenceError extends Schema.TaggedErrorClass<EnvironmentLinkUpsertPersistenceError>()(
28 "EnvironmentLinkUpsertPersistenceError",28 "EnvironmentLinkUpsertPersistenceError",
29)<{29 { cause: Schema.Defect() },
30 readonly cause: unknown;30) {
31}> {}31 override get message(): string {
32 return "Failed to persist environment link";
33 }
34}
3235
33export class EnvironmentLinkUserListPersistenceError extends Data.TaggedError(36export class EnvironmentLinkUserListPersistenceError extends Schema.TaggedErrorClass<EnvironmentLinkUserListPersistenceError>()(
34 "EnvironmentLinkUserListPersistenceError",37 "EnvironmentLinkUserListPersistenceError",
35)<{38 { cause: Schema.Defect() },
36 readonly cause: unknown;39) {
37}> {}40 override get message(): string {
41 return "Failed to list users linked to environment";
42 }
43}
3844
39export class EnvironmentPublicKeyListPersistenceError extends Data.TaggedError(45export class EnvironmentPublicKeyListPersistenceError extends Schema.TaggedErrorClass<EnvironmentPublicKeyListPersistenceError>()(
40 "EnvironmentPublicKeyListPersistenceError",46 "EnvironmentPublicKeyListPersistenceError",
41)<{47 { cause: Schema.Defect() },
42 readonly cause: unknown;48) {
43}> {}49 override get message(): string {
50 return "Failed to list environment public keys";
51 }
52}
4453
45export class EnvironmentLinkListPersistenceError extends Data.TaggedError(54export class EnvironmentLinkListPersistenceError extends Schema.TaggedErrorClass<EnvironmentLinkListPersistenceError>()(
46 "EnvironmentLinkListPersistenceError",55 "EnvironmentLinkListPersistenceError",
47)<{56 { cause: Schema.Defect() },
48 readonly cause: unknown;57) {
49}> {}58 override get message(): string {
59 return "Failed to list environment links";
60 }
61}
5062
51export class EnvironmentLinkLookupPersistenceError extends Data.TaggedError(63export class EnvironmentLinkLookupPersistenceError extends Schema.TaggedErrorClass<EnvironmentLinkLookupPersistenceError>()(
52 "EnvironmentLinkLookupPersistenceError",64 "EnvironmentLinkLookupPersistenceError",
53)<{65 { cause: Schema.Defect() },
54 readonly cause: unknown;66) {
55}> {}67 override get message(): string {
68 return "Failed to look up environment link";
69 }
70}
5671
57export class EnvironmentLinkRevokePersistenceError extends Data.TaggedError(72export class EnvironmentLinkRevokePersistenceError extends Schema.TaggedErrorClass<EnvironmentLinkRevokePersistenceError>()(
58 "EnvironmentLinkRevokePersistenceError",73 "EnvironmentLinkRevokePersistenceError",
59)<{74 { cause: Schema.Defect() },
60 readonly cause: unknown;75) {
61}> {}76 override get message(): string {
77 return "Failed to revoke environment link";
78 }
79}
6280
63export interface EnvironmentLinksShape {81export interface EnvironmentLinksShape {
64 readonly upsert: (input: {82 readonly upsert: (input: {
infra/relay/src/environments/EnvironmentPublishSignatures.ts+27-13
...@@ -11,7 +11,6 @@ import {...@@ -11,7 +11,6 @@ import {
11import { stableStringify } from "@t3tools/shared/relaySigning";11import { stableStringify } from "@t3tools/shared/relaySigning";
12import * as Context from "effect/Context";12import * as Context from "effect/Context";
13import * as Crypto from "effect/Crypto";13import * as Crypto from "effect/Crypto";
14import * as Data from "effect/Data";
15import * as DateTime from "effect/DateTime";14import * as DateTime from "effect/DateTime";
16import * as Effect from "effect/Effect";15import * as Effect from "effect/Effect";
17import * as Encoding from "effect/Encoding";16import * as Encoding from "effect/Encoding";
...@@ -21,23 +20,38 @@ import * as Schema from "effect/Schema";...@@ -21,23 +20,38 @@ import * as Schema from "effect/Schema";
21import * as DpopProofs from "../auth/DpopProofs.ts";20import * as DpopProofs from "../auth/DpopProofs.ts";
22import * as RelayConfiguration from "../Config.ts";21import * as RelayConfiguration from "../Config.ts";
2322
24export class EnvironmentPublishSignatureExpired extends Data.TaggedError(23export class EnvironmentPublishSignatureExpired extends Schema.TaggedErrorClass<EnvironmentPublishSignatureExpired>()(
25 "EnvironmentPublishSignatureExpired",24 "EnvironmentPublishSignatureExpired",
26)<{25 {
27 readonly expiresAt: string;26 expiresAt: Schema.String,
28}> {}27 },
28) {
29 override get message(): string {
30 return `Environment publish signature expired at ${this.expiresAt}`;
31 }
32}
2933
30export class EnvironmentPublishSignatureInvalid extends Data.TaggedError(34export class EnvironmentPublishSignatureInvalid extends Schema.TaggedErrorClass<EnvironmentPublishSignatureInvalid>()(
31 "EnvironmentPublishSignatureInvalid",35 "EnvironmentPublishSignatureInvalid",
32)<{36 {
33 readonly environmentId: string;37 environmentId: Schema.String,
34}> {}38 },
39) {
40 override get message(): string {
41 return `Environment '${this.environmentId}' publish signature is invalid`;
42 }
43}
3544
36export class EnvironmentPublishPublicKeyMissing extends Data.TaggedError(45export class EnvironmentPublishPublicKeyMissing extends Schema.TaggedErrorClass<EnvironmentPublishPublicKeyMissing>()(
37 "EnvironmentPublishPublicKeyMissing",46 "EnvironmentPublishPublicKeyMissing",
38)<{47 {
39 readonly environmentId: string;48 environmentId: Schema.String,
40}> {}49 },
50) {
51 override get message(): string {
52 return `Environment '${this.environmentId}' has no publish public key`;
53 }
54}
4155
42export type EnvironmentPublishSignatureError =56export type EnvironmentPublishSignatureError =
43 | EnvironmentPublishSignatureExpired57 | EnvironmentPublishSignatureExpired
infra/relay/src/environments/ManagedEndpointAllocations.ts+12-6
...@@ -1,10 +1,10 @@...@@ -1,10 +1,10 @@
1import type { RelayManagedEndpoint } from "@t3tools/contracts/relay";1import type { RelayManagedEndpoint } from "@t3tools/contracts/relay";
2import { and, eq } from "drizzle-orm";2import { and, eq } from "drizzle-orm";
3import * as Context from "effect/Context";3import * as Context from "effect/Context";
4import * as Data from "effect/Data";
5import * as DateTime from "effect/DateTime";4import * as DateTime from "effect/DateTime";
6import * as Effect from "effect/Effect";5import * as Effect from "effect/Effect";
7import * as Layer from "effect/Layer";6import * as Layer from "effect/Layer";
7import * as Schema from "effect/Schema";
88
9import { RelayDb } from "../db.ts";9import { RelayDb } from "../db.ts";
10import { isManagedEndpointHostname, managedEndpointForHostname } from "../deploymentConfig.ts";10import { isManagedEndpointHostname, managedEndpointForHostname } from "../deploymentConfig.ts";
...@@ -36,11 +36,17 @@ export function resolveReadyManagedEndpoint(input: {...@@ -36,11 +36,17 @@ export function resolveReadyManagedEndpoint(input: {
36 return managedEndpointForHostname(input.allocation.hostname);36 return managedEndpointForHostname(input.allocation.hostname);
37}37}
3838
39export class ManagedEndpointAllocationPersistenceError extends Data.TaggedError(39export class ManagedEndpointAllocationPersistenceError extends Schema.TaggedErrorClass<ManagedEndpointAllocationPersistenceError>()(
40 "ManagedEndpointAllocationPersistenceError",40 "ManagedEndpointAllocationPersistenceError",
41)<{41 { cause: Schema.Defect() },
42 readonly cause: unknown;42) {
43}> {}43 override get message(): string {
44 return "Failed to persist managed endpoint allocation";
45 }
46}
47const isManagedEndpointAllocationPersistenceError = Schema.is(
48 ManagedEndpointAllocationPersistenceError,
49);
4450
45interface ManagedEndpointAllocationKey {51interface ManagedEndpointAllocationKey {
46 readonly userId: string;52 readonly userId: string;
...@@ -98,7 +104,7 @@ const whereAllocation = (input: ManagedEndpointAllocationKey) =>...@@ -98,7 +104,7 @@ const whereAllocation = (input: ManagedEndpointAllocationKey) =>
98 );104 );
99105
100const persistenceError = (cause: unknown) =>106const persistenceError = (cause: unknown) =>
101 cause instanceof ManagedEndpointAllocationPersistenceError107 isManagedEndpointAllocationPersistenceError(cause)
102 ? cause108 ? cause
103 : new ManagedEndpointAllocationPersistenceError({ cause });109 : new ManagedEndpointAllocationPersistenceError({ cause });
104110
infra/relay/src/environments/ManagedEndpointProvider.ts+46-24
...@@ -3,12 +3,12 @@ import * as Cloudflare from "alchemy/Cloudflare";...@@ -3,12 +3,12 @@ import * as Cloudflare from "alchemy/Cloudflare";
3import * as Arr from "effect/Array";3import * as Arr from "effect/Array";
4import * as Context from "effect/Context";4import * as Context from "effect/Context";
5import * as Crypto from "effect/Crypto";5import * as Crypto from "effect/Crypto";
6import * as Data from "effect/Data";
7import * as Effect from "effect/Effect";6import * as Effect from "effect/Effect";
8import * as Encoding from "effect/Encoding";7import * as Encoding from "effect/Encoding";
9import * as Layer from "effect/Layer";8import * as Layer from "effect/Layer";
10import * as Option from "effect/Option";9import * as Option from "effect/Option";
11import * as Result from "effect/Result";10import * as Result from "effect/Result";
11import * as Schema from "effect/Schema";
1212
13import type {13import type {
14 RelayManagedEndpoint,14 RelayManagedEndpoint,
...@@ -25,28 +25,44 @@ import {...@@ -25,28 +25,44 @@ import {
25} from "../deploymentConfig.ts";25} from "../deploymentConfig.ts";
26import { ManagedEndpointAllocations } from "./ManagedEndpointAllocations.ts";26import { ManagedEndpointAllocations } from "./ManagedEndpointAllocations.ts";
2727
28export class ManagedEndpointProvisioningNotConfigured extends Data.TaggedError(28export class ManagedEndpointProvisioningNotConfigured extends Schema.TaggedErrorClass<ManagedEndpointProvisioningNotConfigured>()(
29 "ManagedEndpointProvisioningNotConfigured",29 "ManagedEndpointProvisioningNotConfigured",
30)<{}> {}30 {},
31) {
32 override get message(): string {
33 return "Managed endpoint provisioning is not configured";
34 }
35}
3136
32export class ManagedEndpointProvisioningFailed extends Data.TaggedError(37export class ManagedEndpointProvisioningFailed extends Schema.TaggedErrorClass<ManagedEndpointProvisioningFailed>()(
33 "ManagedEndpointProvisioningFailed",38 "ManagedEndpointProvisioningFailed",
34)<{39 { cause: Schema.Defect() },
35 readonly cause: unknown;40) {
36}> {}41 override get message(): string {
42 return "Managed endpoint provisioning failed";
43 }
44}
3745
38export class ManagedEndpointDeprovisioningFailed extends Data.TaggedError(46export class ManagedEndpointDeprovisioningFailed extends Schema.TaggedErrorClass<ManagedEndpointDeprovisioningFailed>()(
39 "ManagedEndpointDeprovisioningFailed",47 "ManagedEndpointDeprovisioningFailed",
40)<{48 { cause: Schema.Defect() },
41 readonly cause: unknown;49) {
42}> {}50 override get message(): string {
51 return "Managed endpoint deprovisioning failed";
52 }
53}
4354
44export class ManagedEndpointOriginNotAllowed extends Data.TaggedError(55export class ManagedEndpointOriginNotAllowed extends Schema.TaggedErrorClass<ManagedEndpointOriginNotAllowed>()(
45 "ManagedEndpointOriginNotAllowed",56 "ManagedEndpointOriginNotAllowed",
46)<{57 {
47 readonly host: string;58 host: Schema.String,
48 readonly port: number;59 port: Schema.Number,
49}> {}60 },
61) {
62 override get message(): string {
63 return `Managed endpoint origin '${this.host}:${this.port}' is not allowed`;
64 }
65}
5066
51export type ManagedEndpointProviderError =67export type ManagedEndpointProviderError =
52 | ManagedEndpointProvisioningNotConfigured68 | ManagedEndpointProvisioningNotConfigured
...@@ -80,11 +96,14 @@ interface ManagedEndpointTunnel {...@@ -80,11 +96,14 @@ interface ManagedEndpointTunnel {
80 readonly name?: string | null;96 readonly name?: string | null;
81}97}
8298
83export class ManagedEndpointTunnelClientError extends Data.TaggedError(99export class ManagedEndpointTunnelClientError extends Schema.TaggedErrorClass<ManagedEndpointTunnelClientError>()(
84 "ManagedEndpointTunnelClientError",100 "ManagedEndpointTunnelClientError",
85)<{101 { cause: Schema.Defect() },
86 readonly cause: unknown;102) {
87}> {}103 override get message(): string {
104 return "Managed endpoint tunnel provider request failed";
105 }
106}
88107
89export interface ManagedEndpointTunnelClientShape {108export interface ManagedEndpointTunnelClientShape {
90 readonly list: (request: {109 readonly list: (request: {
...@@ -124,11 +143,14 @@ interface ManagedEndpointCnameRecordInput {...@@ -124,11 +143,14 @@ interface ManagedEndpointCnameRecordInput {
124 readonly proxied: true;143 readonly proxied: true;
125}144}
126145
127export class ManagedEndpointDnsClientError extends Data.TaggedError(146export class ManagedEndpointDnsClientError extends Schema.TaggedErrorClass<ManagedEndpointDnsClientError>()(
128 "ManagedEndpointDnsClientError",147 "ManagedEndpointDnsClientError",
129)<{148 { cause: Schema.Defect() },
130 readonly cause: unknown;149) {
131}> {}150 override get message(): string {
151 return "Managed endpoint DNS provider request failed";
152 }
153}
132154
133export interface ManagedEndpointDnsClientShape {155export interface ManagedEndpointDnsClientShape {
134 readonly listRecords: (156 readonly listRecords: (
infra/relay/src/http/Api.ts+11-8
...@@ -1,6 +1,5 @@...@@ -1,6 +1,5 @@
1import { createClerkClient, verifyToken } from "@clerk/backend";1import { createClerkClient, verifyToken } from "@clerk/backend";
2import { sql as drizzleSql } from "drizzle-orm";2import { sql as drizzleSql } from "drizzle-orm";
3import * as Data from "effect/Data";
4import * as Crypto from "effect/Crypto";3import * as Crypto from "effect/Crypto";
5import * as Context from "effect/Context";4import * as Context from "effect/Context";
6import * as DateTime from "effect/DateTime";5import * as DateTime from "effect/DateTime";
...@@ -813,9 +812,16 @@ export const serverApi = HttpApiBuilder.group(...@@ -813,9 +812,16 @@ export const serverApi = HttpApiBuilder.group(
813 }),812 }),
814);813);
815814
816class ClerkTokenVerificationFailed extends Data.TaggedError("ClerkTokenVerificationFailed")<{815class ClerkTokenVerificationFailed extends Schema.TaggedErrorClass<ClerkTokenVerificationFailed>()(
817 readonly cause: unknown;816 "ClerkTokenVerificationFailed",
818}> {}817 {
818 cause: Schema.Defect(),
819 },
820) {
821 override get message(): string {
822 return "Clerk token verification failed";
823 }
824}
819825
820const isHttpUnauthorized = Schema.is(HttpApiError.Unauthorized);826const isHttpUnauthorized = Schema.is(HttpApiError.Unauthorized);
821827
...@@ -846,16 +852,13 @@ const COMMON_AUTH_INVALID_REASONS = [...@@ -846,16 +852,13 @@ const COMMON_AUTH_INVALID_REASONS = [
846 DeliveryAttempts.DeliveryAttemptRecordPersistenceError,852 DeliveryAttempts.DeliveryAttemptRecordPersistenceError,
847] as const;853] as const;
848type RelayCommonPersistenceError = InstanceType<(typeof COMMON_AUTH_INVALID_REASONS)[number]>;854type RelayCommonPersistenceError = InstanceType<(typeof COMMON_AUTH_INVALID_REASONS)[number]>;
855const isRelayCommonPersistenceError = Schema.is(Schema.Union(COMMON_AUTH_INVALID_REASONS));
849856
850type MapRelayCommonApiError<E> =857type MapRelayCommonApiError<E> =
851 | Exclude<E, HttpApiError.Unauthorized | RelayCommonPersistenceError>858 | Exclude<E, HttpApiError.Unauthorized | RelayCommonPersistenceError>
852 | (Extract<E, HttpApiError.Unauthorized> extends never ? never : RelayAuthInvalidError)859 | (Extract<E, HttpApiError.Unauthorized> extends never ? never : RelayAuthInvalidError)
853 | (Extract<E, RelayCommonPersistenceError> extends never ? never : RelayInternalError);860 | (Extract<E, RelayCommonPersistenceError> extends never ? never : RelayInternalError);
854861
855function isRelayCommonPersistenceError(error: unknown): error is RelayCommonPersistenceError {
856 return COMMON_AUTH_INVALID_REASONS.some((ErrorType) => error instanceof ErrorType);
857}
858
859function relayInternalErrorResponse(reason: RelayInternalError["reason"]) {862function relayInternalErrorResponse(reason: RelayInternalError["reason"]) {
860 return currentTraceId.pipe(863 return currentTraceId.pipe(
861 Effect.flatMap((traceId) =>864 Effect.flatMap((traceId) =>
infra/relay/src/observability.test.ts created+86
...@@ -0,0 +1,86 @@
1import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";
2import { expect, it } from "@effect/vitest";
3import * as Deferred from "effect/Deferred";
4import * as Effect from "effect/Effect";
5import * as Redacted from "effect/Redacted";
6import * as Schema from "effect/Schema";
7import * as HttpServer from "effect/unstable/http/HttpServer";
8import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";
9import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
10import type { OtlpTracer } from "effect/unstable/observability";
11
12import { EnvironmentMintRequestFailed } from "./environments/EnvironmentConnector.ts";
13import { makeRelayTraceLayer } from "./observability.ts";
14
15interface ExportedRequest {
16 readonly authorization: string | undefined;
17 readonly body: string;
18 readonly dataset: string | undefined;
19}
20
21const otlpAttributeValue = (value: {
22 readonly stringValue?: string | null;
23 readonly boolValue?: boolean | null;
24 readonly intValue?: number | null;
25 readonly doubleValue?: number | null;
26}) => value.stringValue ?? value.boolValue ?? value.intValue ?? value.doubleValue;
27
28const decodeJson = Schema.decodeUnknownEffect(Schema.UnknownFromJsonString);
29
30it.effect("exports schema error fields as span attributes", () =>
31 Effect.gen(function* () {
32 const exportedRequest = yield* Deferred.make<ExportedRequest>();
33 yield* HttpServer.serveEffect(
34 Effect.gen(function* () {
35 const request = yield* HttpServerRequest.HttpServerRequest;
36 yield* Deferred.succeed(exportedRequest, {
37 authorization: request.headers.authorization,
38 body: yield* request.text,
39 dataset: request.headers["x-axiom-dataset"],
40 });
41 return HttpServerResponse.empty({ status: 204 });
42 }),
43 );
44
45 yield* Effect.fail(
46 new EnvironmentMintRequestFailed({
47 environmentId: "environment-1",
48 operation: "connect",
49 cause: new Error("upstream unavailable"),
50 }),
51 ).pipe(
52 Effect.withSpan("relay.test.schema_error"),
53 Effect.exit,
54 Effect.provide(
55 makeRelayTraceLayer({
56 tracesEndpoint: "/v1/traces",
57 tracesDatasetName: "relay-test-traces",
58 ingestToken: Redacted.make("test-token"),
59 }),
60 ),
61 );
62
63 const request = yield* Deferred.await(exportedRequest).pipe(Effect.timeout("1 second"));
64 const payload = (yield* decodeJson(request.body)) as OtlpTracer.TraceData;
65 const span = payload.resourceSpans
66 .flatMap((resourceSpan) => resourceSpan.scopeSpans)
67 .flatMap((scopeSpan) => scopeSpan.spans)
68 .find((candidate) => candidate.name === "relay.test.schema_error");
69 const attributes = Object.fromEntries(
70 (span?.attributes ?? []).map((attribute) => [
71 attribute.key,
72 otlpAttributeValue(attribute.value),
73 ]),
74 );
75
76 expect(request.authorization).toBe("Bearer test-token");
77 expect(request.dataset).toBe("relay-test-traces");
78 expect(attributes).toMatchObject({
79 "error.type": "EnvironmentMintRequestFailed",
80 "error.environmentId": "environment-1",
81 "error.operation": "connect",
82 "error.cause.name": "Error",
83 "error.cause.message": "upstream unavailable",
84 });
85 }).pipe(Effect.provide(NodeHttpServer.layerTest), Effect.scoped),
86);
infra/relay/src/observability.ts+160-15
...@@ -1,9 +1,14 @@...@@ -1,9 +1,14 @@
1import * as Alchemy from "alchemy";1import * as Alchemy from "alchemy";
2import * as Axiom from "alchemy/Axiom";2import * as Axiom from "alchemy/Axiom";
3import * as Output from "alchemy/Output";3import * as Output from "alchemy/Output";
4import * as Layer from "effect/Layer";4import * as Cause from "effect/Cause";
5import * as Effect from "effect/Effect";5import * as Effect from "effect/Effect";
6import * as Exit from "effect/Exit";
7import * as Layer from "effect/Layer";
8import * as Option from "effect/Option";
6import * as Redacted from "effect/Redacted";9import * as Redacted from "effect/Redacted";
10import * as Schema from "effect/Schema";
11import * as Tracer from "effect/Tracer";
7import { OtlpSerialization, OtlpTracer } from "effect/unstable/observability";12import { OtlpSerialization, OtlpTracer } from "effect/unstable/observability";
813
9import { relayResourceNameForStage } from "./deploymentConfig.ts";14import { relayResourceNameForStage } from "./deploymentConfig.ts";
...@@ -54,23 +59,163 @@ export const withSpanAttributes =...@@ -54,23 +59,163 @@ export const withSpanAttributes =
54 Effect.andThen(effect.pipe(Effect.annotateSpans(attributes))),59 Effect.andThen(effect.pipe(Effect.annotateSpans(attributes))),
55 );60 );
5661
62const appendEncodedAttributes = (
63 attributes: Record<string, unknown>,
64 prefix: string,
65 value: unknown,
66): void => {
67 if (
68 value === null ||
69 typeof value === "string" ||
70 typeof value === "number" ||
71 typeof value === "boolean" ||
72 typeof value === "bigint" ||
73 Array.isArray(value)
74 ) {
75 attributes[prefix] = value;
76 return;
77 }
78 if (typeof value !== "object") {
79 return;
80 }
81 for (const [key, child] of Object.entries(value)) {
82 appendEncodedAttributes(attributes, `${prefix}.${key}`, child);
83 }
84};
85
86const schemaErrorAttributes = (error: unknown): Record<string, unknown> | undefined => {
87 if (typeof error !== "object" || error === null) {
88 return undefined;
89 }
90 const constructor = error.constructor;
91 if (!Schema.isSchema(constructor)) {
92 return undefined;
93 }
94 const encoded = Schema.encodeUnknownOption(constructor as unknown as Schema.Encoder<unknown>)(
95 error,
96 );
97 if (Option.isNone(encoded) || typeof encoded.value !== "object" || encoded.value === null) {
98 return undefined;
99 }
100 const tag = Reflect.get(encoded.value, "_tag");
101 if (typeof tag !== "string") {
102 return undefined;
103 }
104
105 const attributes: Record<string, unknown> = {
106 "error.type": tag,
107 };
108 for (const [key, value] of Object.entries(encoded.value)) {
109 if (key !== "_tag") {
110 appendEncodedAttributes(attributes, `error.${key}`, value);
111 }
112 }
113 return attributes;
114};
115
116const annotateSchemaError = (span: Tracer.Span, exit: Exit.Exit<unknown, unknown>): void => {
117 if (Exit.isSuccess(exit)) {
118 return;
119 }
120 for (const reason of exit.cause.reasons) {
121 const error = Cause.isFailReason(reason)
122 ? reason.error
123 : Cause.isDieReason(reason)
124 ? reason.defect
125 : undefined;
126 const attributes = schemaErrorAttributes(error);
127 if (attributes) {
128 for (const [key, value] of Object.entries(attributes)) {
129 span.attribute(key, value);
130 }
131 return;
132 }
133 }
134};
135
136class RelayTraceSpan implements Tracer.Span {
137 readonly _tag = "Span";
138 private readonly delegate: Tracer.Span;
139
140 constructor(delegate: Tracer.Span) {
141 this.delegate = delegate;
142 }
143
144 get name() {
145 return this.delegate.name;
146 }
147 get spanId() {
148 return this.delegate.spanId;
149 }
150 get traceId() {
151 return this.delegate.traceId;
152 }
153 get parent() {
154 return this.delegate.parent;
155 }
156 get annotations() {
157 return this.delegate.annotations;
158 }
159 get status() {
160 return this.delegate.status;
161 }
162 get attributes() {
163 return this.delegate.attributes;
164 }
165 get links() {
166 return this.delegate.links;
167 }
168 get sampled() {
169 return this.delegate.sampled;
170 }
171 get kind() {
172 return this.delegate.kind;
173 }
174
175 end(endTime: bigint, exit: Exit.Exit<unknown, unknown>): void {
176 annotateSchemaError(this.delegate, exit);
177 this.delegate.end(endTime, exit);
178 }
179
180 attribute(key: string, value: unknown): void {
181 this.delegate.attribute(key, value);
182 }
183
184 event(name: string, startTime: bigint, attributes?: Record<string, unknown>): void {
185 this.delegate.event(name, startTime, attributes);
186 }
187
188 addLinks(links: ReadonlyArray<Tracer.SpanLink>): void {
189 this.delegate.addLinks(links);
190 }
191}
192
193const withSchemaErrorAttributes = (delegate: Tracer.Tracer): Tracer.Tracer =>
194 Tracer.make({
195 span: (options) => new RelayTraceSpan(delegate.span(options)),
196 ...(delegate.context ? { context: delegate.context } : {}),
197 });
198
57export const makeRelayTraceLayer = (input: {199export const makeRelayTraceLayer = (input: {
58 readonly tracesEndpoint: string;200 readonly tracesEndpoint: string;
59 readonly tracesDatasetName: string;201 readonly tracesDatasetName: string;
60 readonly ingestToken: Redacted.Redacted<string>;202 readonly ingestToken: Redacted.Redacted<string>;
61}) =>203}) =>
62 OtlpTracer.layer({204 Layer.effect(
63 url: input.tracesEndpoint,205 Tracer.Tracer,
64 resource: {206 OtlpTracer.make({
65 serviceName: "t3-code-relay-worker",207 url: input.tracesEndpoint,
66 attributes: {208 resource: {
67 "service.runtime": "cloudflare-worker",209 serviceName: "t3-code-relay-worker",
68 "service.component": "relay",210 attributes: {
211 "service.runtime": "cloudflare-worker",
212 "service.component": "relay",
213 },
214 },
215 headers: {
216 Authorization: `Bearer ${Redacted.value(input.ingestToken)}`,
217 "X-Axiom-Dataset": input.tracesDatasetName,
69 },218 },
70 },219 exportInterval: "1 second",
71 headers: {220 }).pipe(Effect.map(withSchemaErrorAttributes)),
72 Authorization: `Bearer ${Redacted.value(input.ingestToken)}`,221 ).pipe(Layer.provide(OtlpSerialization.layerJson));
73 "X-Axiom-Dataset": input.tracesDatasetName,
74 },
75 exportInterval: "1 second",
76 }).pipe(Layer.provide(OtlpSerialization.layerJson));
packages/contracts/src/relay.ts+55-11
...@@ -329,7 +329,11 @@ export class RelayAuthInvalidError extends Schema.TaggedErrorClass<RelayAuthInva...@@ -329,7 +329,11 @@ export class RelayAuthInvalidError extends Schema.TaggedErrorClass<RelayAuthInva
329 traceId: TrimmedNonEmptyString,329 traceId: TrimmedNonEmptyString,
330 },330 },
331 { httpApiStatus: 401 },331 { httpApiStatus: 401 },
332) {}332) {
333 override get message(): string {
334 return `Relay authentication failed: ${this.reason}`;
335 }
336}
333337
334export class RelayEnvironmentLinkProofExpiredError extends Schema.TaggedErrorClass<RelayEnvironmentLinkProofExpiredError>()(338export class RelayEnvironmentLinkProofExpiredError extends Schema.TaggedErrorClass<RelayEnvironmentLinkProofExpiredError>()(
335 "RelayEnvironmentLinkProofExpiredError",339 "RelayEnvironmentLinkProofExpiredError",
...@@ -338,7 +342,11 @@ export class RelayEnvironmentLinkProofExpiredError extends Schema.TaggedErrorCla...@@ -338,7 +342,11 @@ export class RelayEnvironmentLinkProofExpiredError extends Schema.TaggedErrorCla
338 traceId: TrimmedNonEmptyString,342 traceId: TrimmedNonEmptyString,
339 },343 },
340 { httpApiStatus: 401 },344 { httpApiStatus: 401 },
341) {}345) {
346 override get message(): string {
347 return "Relay environment link proof expired";
348 }
349}
342350
343export class RelayEnvironmentLinkProofInvalidError extends Schema.TaggedErrorClass<RelayEnvironmentLinkProofInvalidError>()(351export class RelayEnvironmentLinkProofInvalidError extends Schema.TaggedErrorClass<RelayEnvironmentLinkProofInvalidError>()(
344 "RelayEnvironmentLinkProofInvalidError",352 "RelayEnvironmentLinkProofInvalidError",
...@@ -348,7 +356,11 @@ export class RelayEnvironmentLinkProofInvalidError extends Schema.TaggedErrorCla...@@ -348,7 +356,11 @@ export class RelayEnvironmentLinkProofInvalidError extends Schema.TaggedErrorCla
348 traceId: TrimmedNonEmptyString,356 traceId: TrimmedNonEmptyString,
349 },357 },
350 { httpApiStatus: 400 },358 { httpApiStatus: 400 },
351) {}359) {
360 override get message(): string {
361 return `Relay environment link proof is invalid: ${this.reason}`;
362 }
363}
352364
353export class RelayEnvironmentConnectNotAuthorizedError extends Schema.TaggedErrorClass<RelayEnvironmentConnectNotAuthorizedError>()(365export class RelayEnvironmentConnectNotAuthorizedError extends Schema.TaggedErrorClass<RelayEnvironmentConnectNotAuthorizedError>()(
354 "RelayEnvironmentConnectNotAuthorizedError",366 "RelayEnvironmentConnectNotAuthorizedError",
...@@ -357,7 +369,11 @@ export class RelayEnvironmentConnectNotAuthorizedError extends Schema.TaggedErro...@@ -357,7 +369,11 @@ export class RelayEnvironmentConnectNotAuthorizedError extends Schema.TaggedErro
357 traceId: TrimmedNonEmptyString,369 traceId: TrimmedNonEmptyString,
358 },370 },
359 { httpApiStatus: 403 },371 { httpApiStatus: 403 },
360) {}372) {
373 override get message(): string {
374 return "Relay environment connection is not authorized";
375 }
376}
361377
362export class RelayEnvironmentEndpointUnavailableError extends Schema.TaggedErrorClass<RelayEnvironmentEndpointUnavailableError>()(378export class RelayEnvironmentEndpointUnavailableError extends Schema.TaggedErrorClass<RelayEnvironmentEndpointUnavailableError>()(
363 "RelayEnvironmentEndpointUnavailableError",379 "RelayEnvironmentEndpointUnavailableError",
...@@ -367,7 +383,11 @@ export class RelayEnvironmentEndpointUnavailableError extends Schema.TaggedError...@@ -367,7 +383,11 @@ export class RelayEnvironmentEndpointUnavailableError extends Schema.TaggedError
367 traceId: TrimmedNonEmptyString,383 traceId: TrimmedNonEmptyString,
368 },384 },
369 { httpApiStatus: 502 },385 { httpApiStatus: 502 },
370) {}386) {
387 override get message(): string {
388 return `Relay environment endpoint is unavailable: ${this.reason}`;
389 }
390}
371391
372export class RelayEnvironmentEndpointTimedOutError extends Schema.TaggedErrorClass<RelayEnvironmentEndpointTimedOutError>()(392export class RelayEnvironmentEndpointTimedOutError extends Schema.TaggedErrorClass<RelayEnvironmentEndpointTimedOutError>()(
373 "RelayEnvironmentEndpointTimedOutError",393 "RelayEnvironmentEndpointTimedOutError",
...@@ -376,7 +396,11 @@ export class RelayEnvironmentEndpointTimedOutError extends Schema.TaggedErrorCla...@@ -376,7 +396,11 @@ export class RelayEnvironmentEndpointTimedOutError extends Schema.TaggedErrorCla
376 traceId: TrimmedNonEmptyString,396 traceId: TrimmedNonEmptyString,
377 },397 },
378 { httpApiStatus: 504 },398 { httpApiStatus: 504 },
379) {}399) {
400 override get message(): string {
401 return "Relay environment endpoint request timed out";
402 }
403}
380404
381export class RelayEnvironmentLinkFailedError extends Schema.TaggedErrorClass<RelayEnvironmentLinkFailedError>()(405export class RelayEnvironmentLinkFailedError extends Schema.TaggedErrorClass<RelayEnvironmentLinkFailedError>()(
382 "RelayEnvironmentLinkFailedError",406 "RelayEnvironmentLinkFailedError",
...@@ -386,7 +410,11 @@ export class RelayEnvironmentLinkFailedError extends Schema.TaggedErrorClass<Rel...@@ -386,7 +410,11 @@ export class RelayEnvironmentLinkFailedError extends Schema.TaggedErrorClass<Rel
386 traceId: TrimmedNonEmptyString,410 traceId: TrimmedNonEmptyString,
387 },411 },
388 { httpApiStatus: 500 },412 { httpApiStatus: 500 },
389) {}413) {
414 override get message(): string {
415 return `Relay environment link failed: ${this.reason}`;
416 }
417}
390418
391export class RelayEnvironmentLinkUnavailableError extends Schema.TaggedErrorClass<RelayEnvironmentLinkUnavailableError>()(419export class RelayEnvironmentLinkUnavailableError extends Schema.TaggedErrorClass<RelayEnvironmentLinkUnavailableError>()(
392 "RelayEnvironmentLinkUnavailableError",420 "RelayEnvironmentLinkUnavailableError",
...@@ -396,7 +424,11 @@ export class RelayEnvironmentLinkUnavailableError extends Schema.TaggedErrorClas...@@ -396,7 +424,11 @@ export class RelayEnvironmentLinkUnavailableError extends Schema.TaggedErrorClas
396 traceId: TrimmedNonEmptyString,424 traceId: TrimmedNonEmptyString,
397 },425 },
398 { httpApiStatus: 503 },426 { httpApiStatus: 503 },
399) {}427) {
428 override get message(): string {
429 return `Relay environment link is unavailable: ${this.reason}`;
430 }
431}
400432
401export class RelayAgentActivityPublishProofExpiredError extends Schema.TaggedErrorClass<RelayAgentActivityPublishProofExpiredError>()(433export class RelayAgentActivityPublishProofExpiredError extends Schema.TaggedErrorClass<RelayAgentActivityPublishProofExpiredError>()(
402 "RelayAgentActivityPublishProofExpiredError",434 "RelayAgentActivityPublishProofExpiredError",
...@@ -405,7 +437,11 @@ export class RelayAgentActivityPublishProofExpiredError extends Schema.TaggedErr...@@ -405,7 +437,11 @@ export class RelayAgentActivityPublishProofExpiredError extends Schema.TaggedErr
405 traceId: TrimmedNonEmptyString,437 traceId: TrimmedNonEmptyString,
406 },438 },
407 { httpApiStatus: 401 },439 { httpApiStatus: 401 },
408) {}440) {
441 override get message(): string {
442 return "Relay agent activity publish proof expired";
443 }
444}
409445
410export class RelayAgentActivityPublishProofInvalidError extends Schema.TaggedErrorClass<RelayAgentActivityPublishProofInvalidError>()(446export class RelayAgentActivityPublishProofInvalidError extends Schema.TaggedErrorClass<RelayAgentActivityPublishProofInvalidError>()(
411 "RelayAgentActivityPublishProofInvalidError",447 "RelayAgentActivityPublishProofInvalidError",
...@@ -415,7 +451,11 @@ export class RelayAgentActivityPublishProofInvalidError extends Schema.TaggedErr...@@ -415,7 +451,11 @@ export class RelayAgentActivityPublishProofInvalidError extends Schema.TaggedErr
415 traceId: TrimmedNonEmptyString,451 traceId: TrimmedNonEmptyString,
416 },452 },
417 { httpApiStatus: 401 },453 { httpApiStatus: 401 },
418) {}454) {
455 override get message(): string {
456 return `Relay agent activity publish proof is invalid: ${this.reason}`;
457 }
458}
419459
420export class RelayInternalError extends Schema.TaggedErrorClass<RelayInternalError>()(460export class RelayInternalError extends Schema.TaggedErrorClass<RelayInternalError>()(
421 "RelayInternalError",461 "RelayInternalError",
...@@ -425,7 +465,11 @@ export class RelayInternalError extends Schema.TaggedErrorClass<RelayInternalErr...@@ -425,7 +465,11 @@ export class RelayInternalError extends Schema.TaggedErrorClass<RelayInternalErr
425 traceId: TrimmedNonEmptyString,465 traceId: TrimmedNonEmptyString,
426 },466 },
427 { httpApiStatus: 500 },467 { httpApiStatus: 500 },
428) {}468) {
469 override get message(): string {
470 return `Relay internal error: ${this.reason}`;
471 }
472}
429473
430export const RelayProtectedError = Schema.Union([474export const RelayProtectedError = Schema.Union([
431 RelayAuthInvalidError,475 RelayAuthInvalidError,