| author | |
| committer | |
| log | a56496c7f20d53369ef87101681ee2a99b3a4337 |
| tree | ab5c0f6d14af5fcadeba0246578336b94c18855a |
| parent | b76f161d551235e1de89ed85c747f14041ac5d99 |
| signature | Signed by PGP key B5690EEEBB952194 |
20 files changed, 744 insertions(+), 232 deletions(-)
infra/relay/src/agentActivity/AgentActivityRows.ts+21-13| ... | ... | @@ -1,7 +1,6 @@ |
| 1 | 1 | import type { RelayAgentActivityState } from "@t3tools/contracts/relay"; |
| 2 | 2 | import { RelayAgentActivityState as RelayAgentActivityStateSchema } from "@t3tools/contracts/relay"; |
| 3 | 3 | import * as Context from "effect/Context"; |
| 4 | import * as Data from "effect/Data"; | |
| 5 | 4 | import * as DateTime from "effect/DateTime"; |
| 6 | 5 | import * as Effect from "effect/Effect"; |
| 7 | 6 | import { cast } from "effect/Function"; |
| ... | ... | @@ -13,23 +12,32 @@ import { and, desc, eq, isNull } from "drizzle-orm"; |
| 13 | 12 | import { RelayDb } from "../db.ts"; |
| 14 | 13 | import { relayAgentActivityRows, relayEnvironmentLinks } from "../persistence/schema.ts"; |
| 15 | 14 | |
| 16 | export class AgentActivityRowUpsertPersistenceError extends Data.TaggedError( | |
| 15 | export class AgentActivityRowUpsertPersistenceError extends Schema.TaggedErrorClass<AgentActivityRowUpsertPersistenceError>()( | |
| 17 | 16 | "AgentActivityRowUpsertPersistenceError", |
| 18 | )<{ | |
| 19 | readonly cause: unknown; | |
| 20 | }> {} | |
| 17 | { cause: Schema.Defect() }, | |
| 18 | ) { | |
| 19 | override get message(): string { | |
| 20 | return "Failed to persist agent activity state"; | |
| 21 | } | |
| 22 | } | |
| 21 | 23 | |
| 22 | export class AgentActivityRowDeletePersistenceError extends Data.TaggedError( | |
| 24 | export class AgentActivityRowDeletePersistenceError extends Schema.TaggedErrorClass<AgentActivityRowDeletePersistenceError>()( | |
| 23 | 25 | "AgentActivityRowDeletePersistenceError", |
| 24 | )<{ | |
| 25 | readonly cause: unknown; | |
| 26 | }> {} | |
| 26 | { cause: Schema.Defect() }, | |
| 27 | ) { | |
| 28 | override get message(): string { | |
| 29 | return "Failed to delete agent activity state"; | |
| 30 | } | |
| 31 | } | |
| 27 | 32 | |
| 28 | export class AgentActivityRowListPersistenceError extends Data.TaggedError( | |
| 33 | export class AgentActivityRowListPersistenceError extends Schema.TaggedErrorClass<AgentActivityRowListPersistenceError>()( | |
| 29 | 34 | "AgentActivityRowListPersistenceError", |
| 30 | )<{ | |
| 31 | readonly cause: unknown; | |
| 32 | }> {} | |
| 35 | { cause: Schema.Defect() }, | |
| 36 | ) { | |
| 37 | override get message(): string { | |
| 38 | return "Failed to list agent activity state"; | |
| 39 | } | |
| 40 | } | |
| 33 | 41 | |
| 34 | 42 | export interface AgentActivityRowsShape { |
| 35 | 43 | readonly upsert: (input: { |
infra/relay/src/agentActivity/ApnsClient.ts+32-18| ... | ... | @@ -2,20 +2,13 @@ import * as NodeCrypto from "node:crypto"; |
| 2 | 2 | |
| 3 | 3 | import type { RelayAgentActivityAggregateState } from "@t3tools/contracts/relay"; |
| 4 | 4 | import * as Context from "effect/Context"; |
| 5 | import * as Data from "effect/Data"; | |
| 6 | 5 | import * as Effect from "effect/Effect"; |
| 7 | 6 | import * as Encoding from "effect/Encoding"; |
| 8 | 7 | import * as Layer from "effect/Layer"; |
| 9 | 8 | import * as Option from "effect/Option"; |
| 10 | 9 | import * as Redacted from "effect/Redacted"; |
| 11 | 10 | import * as Schema from "effect/Schema"; |
| 12 | import { | |
| 13 | Headers, | |
| 14 | HttpClient, | |
| 15 | HttpClientRequest, | |
| 16 | type HttpBody, | |
| 17 | type HttpClientError, | |
| 18 | } from "effect/unstable/http"; | |
| 11 | import { Headers, HttpClient, HttpClientRequest } from "effect/unstable/http"; | |
| 19 | 12 | import type { ApnsCredentials } from "../Config.ts"; |
| 20 | 13 | import type { ApnsNotificationPayload } from "./apnsDeliveryJobs.ts"; |
| 21 | 14 | |
| ... | ... | @@ -45,18 +38,39 @@ export interface ApnsDeliveryResult { |
| 45 | 38 | readonly apnsId: string | null; |
| 46 | 39 | } |
| 47 | 40 | |
| 48 | export class ApnsSigningError extends Data.TaggedError("ApnsSigningError")<{ | |
| 49 | readonly phase: "encoding" | "signing"; | |
| 50 | readonly cause: unknown; | |
| 51 | }> {} | |
| 41 | export class ApnsSigningError extends Schema.TaggedErrorClass<ApnsSigningError>()( | |
| 42 | "ApnsSigningError", | |
| 43 | { | |
| 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 | } | |
| 52 | 52 | |
| 53 | export class ApnsHttpRequestError extends Data.TaggedError("ApnsHttpRequestError")<{ | |
| 54 | readonly cause: HttpClientError.HttpClientError | HttpBody.HttpBodyError; | |
| 55 | }> {} | |
| 53 | export class ApnsHttpRequestError extends Schema.TaggedErrorClass<ApnsHttpRequestError>()( | |
| 54 | "ApnsHttpRequestError", | |
| 55 | { | |
| 56 | cause: Schema.Defect(), | |
| 57 | }, | |
| 58 | ) { | |
| 59 | override get message(): string { | |
| 60 | return "APNs HTTP request failed"; | |
| 61 | } | |
| 62 | } | |
| 56 | 63 | |
| 57 | export class ApnsInvalidResponseError extends Data.TaggedError("ApnsInvalidResponseError")<{ | |
| 58 | readonly cause: unknown; | |
| 59 | }> {} | |
| 64 | export class ApnsInvalidResponseError extends Schema.TaggedErrorClass<ApnsInvalidResponseError>()( | |
| 65 | "ApnsInvalidResponseError", | |
| 66 | { | |
| 67 | cause: Schema.Defect(), | |
| 68 | }, | |
| 69 | ) { | |
| 70 | override get message(): string { | |
| 71 | return "APNs returned an invalid response"; | |
| 72 | } | |
| 73 | } | |
| 60 | 74 | |
| 61 | 75 | export type ApnsError = ApnsSigningError | ApnsHttpRequestError | ApnsInvalidResponseError; |
| 62 | 76 |
infra/relay/src/agentActivity/ApnsDeliveries.ts+10-4| ... | ... | @@ -9,7 +9,6 @@ import { |
| 9 | 9 | RelayAgentAwarenessPreferences as RelayAgentAwarenessPreferencesSchema, |
| 10 | 10 | } from "@t3tools/contracts/relay"; |
| 11 | 11 | import * as Context from "effect/Context"; |
| 12 | import * as Data from "effect/Data"; | |
| 13 | 12 | import * as DateTime from "effect/DateTime"; |
| 14 | 13 | import * as Effect from "effect/Effect"; |
| 15 | 14 | import * as Layer from "effect/Layer"; |
| ... | ... | @@ -74,9 +73,16 @@ export type ApnsDeliveryError = |
| 74 | 73 | | LiveActivities.LiveActivityTargetListPersistenceError |
| 75 | 74 | | LiveActivities.LiveActivityDeliveryMarkPersistenceError; |
| 76 | 75 | |
| 77 | export class ApnsDeliveryJobClaimInFlight extends Data.TaggedError("ApnsDeliveryJobClaimInFlight")<{ | |
| 78 | readonly sourceJobId: string; | |
| 79 | }> {} | |
| 76 | export class ApnsDeliveryJobClaimInFlight extends Schema.TaggedErrorClass<ApnsDeliveryJobClaimInFlight>()( | |
| 77 | "ApnsDeliveryJobClaimInFlight", | |
| 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 | } | |
| 80 | 86 | |
| 81 | 87 | const decodeRelayAgentActivityAggregateStateJson = Schema.decodeUnknownOption( |
| 82 | 88 | Schema.fromJsonString(RelayAgentActivityAggregateStateSchema), |
infra/relay/src/agentActivity/ApnsDeliveryQueue.ts+9-4| ... | ... | @@ -2,10 +2,10 @@ import * as Alchemy from "alchemy"; |
| 2 | 2 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 3 | 3 | import * as Crypto from "effect/Crypto"; |
| 4 | 4 | import * as Context from "effect/Context"; |
| 5 | import * as Data from "effect/Data"; | |
| 6 | 5 | import * as DateTime from "effect/DateTime"; |
| 7 | 6 | import * as Effect from "effect/Effect"; |
| 8 | 7 | import * as Layer from "effect/Layer"; |
| 8 | import * as Schema from "effect/Schema"; | |
| 9 | 9 | |
| 10 | 10 | import type { RelayDeliveryResult } from "@t3tools/contracts/relay"; |
| 11 | 11 | |
| ... | ... | @@ -22,9 +22,14 @@ import { |
| 22 | 22 | } from "./apnsDeliveryJobs.ts"; |
| 23 | 23 | import * as RelayConfiguration from "../Config.ts"; |
| 24 | 24 | |
| 25 | export class ApnsDeliveryQueueSendError extends Data.TaggedError("ApnsDeliveryQueueSendError")<{ | |
| 26 | readonly cause: unknown; | |
| 27 | }> {} | |
| 25 | export class ApnsDeliveryQueueSendError extends Schema.TaggedErrorClass<ApnsDeliveryQueueSendError>()( | |
| 26 | "ApnsDeliveryQueueSendError", | |
| 27 | { cause: Schema.Defect() }, | |
| 28 | ) { | |
| 29 | override get message(): string { | |
| 30 | return "Failed to enqueue APNs delivery"; | |
| 31 | } | |
| 32 | } | |
| 28 | 33 | |
| 29 | 34 | export type ApnsDeliveryQueueError = ApnsDeliveryQueueSendError; |
| 30 | 35 |
infra/relay/src/agentActivity/DeliveryAttempts.ts+8-5| ... | ... | @@ -1,20 +1,23 @@ |
| 1 | 1 | import * as Context from "effect/Context"; |
| 2 | import * as Data from "effect/Data"; | |
| 3 | 2 | import * as DateTime from "effect/DateTime"; |
| 4 | 3 | import * as Effect from "effect/Effect"; |
| 5 | 4 | import * as Layer from "effect/Layer"; |
| 6 | 5 | import * as Option from "effect/Option"; |
| 7 | 6 | import { and, eq, isNull } from "drizzle-orm"; |
| 8 | 7 | import * as Crypto from "effect/Crypto"; |
| 8 | import * as Schema from "effect/Schema"; | |
| 9 | 9 | |
| 10 | 10 | import { RelayDb } from "../db.ts"; |
| 11 | 11 | import { relayDeliveryAttempts } from "../persistence/schema.ts"; |
| 12 | 12 | |
| 13 | export class DeliveryAttemptRecordPersistenceError extends Data.TaggedError( | |
| 13 | export class DeliveryAttemptRecordPersistenceError extends Schema.TaggedErrorClass<DeliveryAttemptRecordPersistenceError>()( | |
| 14 | 14 | "DeliveryAttemptRecordPersistenceError", |
| 15 | )<{ | |
| 16 | readonly cause: unknown; | |
| 17 | }> {} | |
| 15 | { cause: Schema.Defect() }, | |
| 16 | ) { | |
| 17 | override get message(): string { | |
| 18 | return "Failed to persist APNs delivery attempt"; | |
| 19 | } | |
| 20 | } | |
| 18 | 21 | |
| 19 | 22 | export interface DeliveryAttemptInput { |
| 20 | 23 | readonly userId: string | null; |
infra/relay/src/agentActivity/Devices.ts+23-12| ... | ... | @@ -3,31 +3,42 @@ import type { |
| 3 | 3 | RelayDeviceRegistrationRequest, |
| 4 | 4 | } from "@t3tools/contracts/relay"; |
| 5 | 5 | import * as Context from "effect/Context"; |
| 6 | import * as Data from "effect/Data"; | |
| 7 | 6 | import * as DateTime from "effect/DateTime"; |
| 8 | 7 | import * as Effect from "effect/Effect"; |
| 9 | 8 | import * as Layer from "effect/Layer"; |
| 9 | import * as Schema from "effect/Schema"; | |
| 10 | 10 | import { and, eq } from "drizzle-orm"; |
| 11 | 11 | import { sql } from "drizzle-orm"; |
| 12 | 12 | |
| 13 | 13 | import { RelayDb } from "../db.ts"; |
| 14 | 14 | import { relayLiveActivities, relayMobileDevices } from "../persistence/schema.ts"; |
| 15 | 15 | |
| 16 | export class DeviceRegistrationPersistenceError extends Data.TaggedError( | |
| 16 | export class DeviceRegistrationPersistenceError extends Schema.TaggedErrorClass<DeviceRegistrationPersistenceError>()( | |
| 17 | 17 | "DeviceRegistrationPersistenceError", |
| 18 | )<{ | |
| 19 | readonly cause: unknown; | |
| 20 | }> {} | |
| 18 | { cause: Schema.Defect() }, | |
| 19 | ) { | |
| 20 | override get message(): string { | |
| 21 | return "Failed to persist mobile device registration"; | |
| 22 | } | |
| 23 | } | |
| 21 | 24 | |
| 22 | export class DeviceUnregistrationPersistenceError extends Data.TaggedError( | |
| 25 | export class DeviceUnregistrationPersistenceError extends Schema.TaggedErrorClass<DeviceUnregistrationPersistenceError>()( | |
| 23 | 26 | "DeviceUnregistrationPersistenceError", |
| 24 | )<{ | |
| 25 | readonly cause: unknown; | |
| 26 | }> {} | |
| 27 | { cause: Schema.Defect() }, | |
| 28 | ) { | |
| 29 | override get message(): string { | |
| 30 | return "Failed to unregister mobile device"; | |
| 31 | } | |
| 32 | } | |
| 27 | 33 | |
| 28 | export class DeviceListPersistenceError extends Data.TaggedError("DeviceListPersistenceError")<{ | |
| 29 | readonly cause: unknown; | |
| 30 | }> {} | |
| 34 | export class DeviceListPersistenceError extends Schema.TaggedErrorClass<DeviceListPersistenceError>()( | |
| 35 | "DeviceListPersistenceError", | |
| 36 | { cause: Schema.Defect() }, | |
| 37 | ) { | |
| 38 | override get message(): string { | |
| 39 | return "Failed to list mobile devices"; | |
| 40 | } | |
| 41 | } | |
| 31 | 42 | |
| 32 | 43 | export interface DevicesShape { |
| 33 | 44 | readonly register: (input: { |
infra/relay/src/agentActivity/LiveActivities.ts+21-13| ... | ... | @@ -5,7 +5,6 @@ import type { |
| 5 | 5 | } from "@t3tools/contracts/relay"; |
| 6 | 6 | import { RelayAgentActivityAggregateState as RelayAgentActivityAggregateStateSchema } from "@t3tools/contracts/relay"; |
| 7 | 7 | import * as Context from "effect/Context"; |
| 8 | import * as Data from "effect/Data"; | |
| 9 | 8 | import * as DateTime from "effect/DateTime"; |
| 10 | 9 | import * as Effect from "effect/Effect"; |
| 11 | 10 | import { cast } from "effect/Function"; |
| ... | ... | @@ -16,23 +15,32 @@ import { and, eq, sql } from "drizzle-orm"; |
| 16 | 15 | import { RelayDb } from "../db.ts"; |
| 17 | 16 | import { relayLiveActivities, relayMobileDevices } from "../persistence/schema.ts"; |
| 18 | 17 | |
| 19 | export class LiveActivityRegistrationPersistenceError extends Data.TaggedError( | |
| 18 | export class LiveActivityRegistrationPersistenceError extends Schema.TaggedErrorClass<LiveActivityRegistrationPersistenceError>()( | |
| 20 | 19 | "LiveActivityRegistrationPersistenceError", |
| 21 | )<{ | |
| 22 | readonly cause: unknown; | |
| 23 | }> {} | |
| 20 | { cause: Schema.Defect() }, | |
| 21 | ) { | |
| 22 | override get message(): string { | |
| 23 | return "Failed to persist Live Activity registration"; | |
| 24 | } | |
| 25 | } | |
| 24 | 26 | |
| 25 | export class LiveActivityTargetListPersistenceError extends Data.TaggedError( | |
| 27 | export class LiveActivityTargetListPersistenceError extends Schema.TaggedErrorClass<LiveActivityTargetListPersistenceError>()( | |
| 26 | 28 | "LiveActivityTargetListPersistenceError", |
| 27 | )<{ | |
| 28 | readonly cause: unknown; | |
| 29 | }> {} | |
| 29 | { cause: Schema.Defect() }, | |
| 30 | ) { | |
| 31 | override get message(): string { | |
| 32 | return "Failed to list Live Activity delivery targets"; | |
| 33 | } | |
| 34 | } | |
| 30 | 35 | |
| 31 | export class LiveActivityDeliveryMarkPersistenceError extends Data.TaggedError( | |
| 36 | export class LiveActivityDeliveryMarkPersistenceError extends Schema.TaggedErrorClass<LiveActivityDeliveryMarkPersistenceError>()( | |
| 32 | 37 | "LiveActivityDeliveryMarkPersistenceError", |
| 33 | )<{ | |
| 34 | readonly cause: unknown; | |
| 35 | }> {} | |
| 38 | { cause: Schema.Defect() }, | |
| 39 | ) { | |
| 40 | override get message(): string { | |
| 41 | return "Failed to persist Live Activity delivery state"; | |
| 42 | } | |
| 43 | } | |
| 36 | 44 | |
| 37 | 45 | export interface DeviceRow { |
| 38 | 46 | readonly user_id: string; |
infra/relay/src/agentActivity/apnsDeliveryJobs.ts+17-8| ... | ... | @@ -2,7 +2,6 @@ import * as NodeCrypto from "node:crypto"; |
| 2 | 2 | |
| 3 | 3 | import { RelayAgentActivityAggregateState, type RelayDeliveryKind } from "@t3tools/contracts/relay"; |
| 4 | 4 | import { stableStringify } from "@t3tools/shared/relaySigning"; |
| 5 | import * as Data from "effect/Data"; | |
| 6 | 5 | import * as DateTime from "effect/DateTime"; |
| 7 | 6 | import * as Option from "effect/Option"; |
| 8 | 7 | import * as Redacted from "effect/Redacted"; |
| ... | ... | @@ -50,13 +49,23 @@ export const SignedApnsDeliveryJob = Schema.Struct({ |
| 50 | 49 | }); |
| 51 | 50 | export type SignedApnsDeliveryJob = typeof SignedApnsDeliveryJob.Type; |
| 52 | 51 | |
| 53 | export class ApnsDeliveryJobInvalid extends Data.TaggedError("ApnsDeliveryJobInvalid")<{ | |
| 54 | readonly message: string; | |
| 55 | }> {} | |
| 56 | ||
| 57 | export class ApnsDeliveryJobExpired extends Data.TaggedError("ApnsDeliveryJobExpired")<{ | |
| 58 | readonly expiresAt: string; | |
| 59 | }> {} | |
| 52 | export class ApnsDeliveryJobInvalid extends Schema.TaggedErrorClass<ApnsDeliveryJobInvalid>()( | |
| 53 | "ApnsDeliveryJobInvalid", | |
| 54 | { | |
| 55 | message: Schema.String, | |
| 56 | }, | |
| 57 | ) {} | |
| 58 | ||
| 59 | export 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 | } | |
| 60 | 69 | |
| 61 | 70 | export type ApnsDeliveryJobVerificationError = ApnsDeliveryJobInvalid | ApnsDeliveryJobExpired; |
| 62 | 71 |
infra/relay/src/auth/DpopProofs.ts+10-5| ... | ... | @@ -1,8 +1,8 @@ |
| 1 | 1 | import * as Context from "effect/Context"; |
| 2 | import * as Data from "effect/Data"; | |
| 3 | 2 | import * as DateTime from "effect/DateTime"; |
| 4 | 3 | import * as Effect from "effect/Effect"; |
| 5 | 4 | import * as Layer from "effect/Layer"; |
| 5 | import * as Schema from "effect/Schema"; | |
| 6 | 6 | import * as HttpApiError from "effect/unstable/httpapi/HttpApiError"; |
| 7 | 7 | import { lt } from "drizzle-orm"; |
| 8 | 8 | |
| ... | ... | @@ -10,11 +10,16 @@ import { verifyDpopProof } from "@t3tools/shared/dpop"; |
| 10 | 10 | import { RelayDb } from "../db.ts"; |
| 11 | 11 | import { relayDpopProofs } from "../persistence/schema.ts"; |
| 12 | 12 | |
| 13 | export class DpopProofReplayPersistenceError extends Data.TaggedError( | |
| 13 | export class DpopProofReplayPersistenceError extends Schema.TaggedErrorClass<DpopProofReplayPersistenceError>()( | |
| 14 | 14 | "DpopProofReplayPersistenceError", |
| 15 | )<{ | |
| 16 | readonly cause: unknown; | |
| 17 | }> {} | |
| 15 | { | |
| 16 | cause: Schema.Defect(), | |
| 17 | }, | |
| 18 | ) { | |
| 19 | override get message(): string { | |
| 20 | return "Failed to persist DPoP proof replay state"; | |
| 21 | } | |
| 22 | } | |
| 18 | 23 | |
| 19 | 24 | export interface DpopProofReplayShape { |
| 20 | 25 | readonly verifyAndConsume: (input: { |
infra/relay/src/environments/EnvironmentConnector.ts+109-26| ... | ... | @@ -28,7 +28,6 @@ import { |
| 28 | 28 | import { stableStringify } from "@t3tools/shared/relaySigning"; |
| 29 | 29 | import * as Context from "effect/Context"; |
| 30 | 30 | import * as Crypto from "effect/Crypto"; |
| 31 | import * as Data from "effect/Data"; | |
| 32 | 31 | import * as DateTime from "effect/DateTime"; |
| 33 | 32 | import * as Duration from "effect/Duration"; |
| 34 | 33 | import * as Effect from "effect/Effect"; |
| ... | ... | @@ -42,28 +41,53 @@ import * as EnvironmentLinks from "./EnvironmentLinks.ts"; |
| 42 | 41 | import * as ManagedEndpointAllocations from "./ManagedEndpointAllocations.ts"; |
| 43 | 42 | import * as RelayConfiguration from "../Config.ts"; |
| 44 | 43 | |
| 45 | export class EnvironmentConnectNotAuthorized extends Data.TaggedError( | |
| 44 | export class EnvironmentConnectNotAuthorized extends Schema.TaggedErrorClass<EnvironmentConnectNotAuthorized>()( | |
| 46 | 45 | "EnvironmentConnectNotAuthorized", |
| 47 | )<{ | |
| 48 | readonly environmentId: string; | |
| 49 | }> {} | |
| 46 | { | |
| 47 | environmentId: Schema.String, | |
| 48 | }, | |
| 49 | ) { | |
| 50 | override get message(): string { | |
| 51 | return `Environment '${this.environmentId}' is not authorized to connect`; | |
| 52 | } | |
| 53 | } | |
| 50 | 54 | |
| 51 | export class EnvironmentMintRequestFailed extends Data.TaggedError("EnvironmentMintRequestFailed")<{ | |
| 52 | readonly cause: unknown; | |
| 53 | }> {} | |
| 55 | export class EnvironmentMintRequestFailed extends Schema.TaggedErrorClass<EnvironmentMintRequestFailed>()( | |
| 56 | "EnvironmentMintRequestFailed", | |
| 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 | } | |
| 54 | 67 | |
| 55 | export class EnvironmentMintRequestTimedOut extends Data.TaggedError( | |
| 68 | export class EnvironmentMintRequestTimedOut extends Schema.TaggedErrorClass<EnvironmentMintRequestTimedOut>()( | |
| 56 | 69 | "EnvironmentMintRequestTimedOut", |
| 57 | )<{ | |
| 58 | readonly environmentId: string; | |
| 59 | readonly timeoutMs: number; | |
| 60 | }> {} | |
| 70 | { | |
| 71 | environmentId: Schema.String, | |
| 72 | timeoutMs: Schema.Number, | |
| 73 | }, | |
| 74 | ) { | |
| 75 | override get message(): string { | |
| 76 | return `Environment '${this.environmentId}' mint request timed out after ${this.timeoutMs}ms`; | |
| 77 | } | |
| 78 | } | |
| 61 | 79 | |
| 62 | export class EnvironmentMintResponseInvalid extends Data.TaggedError( | |
| 80 | export class EnvironmentMintResponseInvalid extends Schema.TaggedErrorClass<EnvironmentMintResponseInvalid>()( | |
| 63 | 81 | "EnvironmentMintResponseInvalid", |
| 64 | )<{ | |
| 65 | readonly environmentId: string; | |
| 66 | }> {} | |
| 82 | { | |
| 83 | environmentId: Schema.String, | |
| 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 | } | |
| 67 | 91 | |
| 68 | 92 | export type EnvironmentConnectorError = |
| 69 | 93 | | EnvironmentConnectNotAuthorized |
| ... | ... | @@ -277,14 +301,28 @@ const make = Effect.gen(function* () { |
| 277 | 301 | const now = yield* DateTime.now; |
| 278 | 302 | const expiresAt = DateTime.add(now, { minutes: 2 }); |
| 279 | 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 | 313 | const payload = { |
| 283 | 314 | iss: relayIssuer, |
| 284 | 315 | aud: `t3-env:${link.environmentId}`, |
| 285 | 316 | sub: input.userId, |
| 286 | 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 | 327 | iat: Math.floor(now.epochMilliseconds / 1_000), |
| 290 | 328 | exp: Math.floor(expiresAt.epochMilliseconds / 1_000), |
| ... | ... | @@ -296,7 +334,16 @@ const make = Effect.gen(function* () { |
| 296 | 334 | privateKey: Redacted.value(settings.cloudMintPrivateKey), |
| 297 | 335 | typ: RELAY_HEALTH_REQUEST_TYP, |
| 298 | 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 | 347 | const checkedAt = DateTime.formatIso(now); |
| 301 | 348 | const environmentClient = yield* makeEnvironmentClient(endpoint.httpBaseUrl); |
| 302 | 349 | const responseOption = yield* environmentClient.cloud.health({ payload: { proof } }).pipe( |
| ... | ... | @@ -336,7 +383,10 @@ const make = Effect.gen(function* () { |
| 336 | 383 | now: yield* DateTime.now, |
| 337 | 384 | }); |
| 338 | 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 | 391 | return { |
| 342 | 392 | environmentId: link.environmentId, |
| ... | ... | @@ -364,14 +414,28 @@ const make = Effect.gen(function* () { |
| 364 | 414 | const now = yield* DateTime.now; |
| 365 | 415 | const expiresAt = DateTime.add(now, { minutes: 2 }); |
| 366 | 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 | 426 | const payload = { |
| 370 | 427 | iss: relayIssuer, |
| 371 | 428 | aud: `t3-env:${link.environmentId}`, |
| 372 | 429 | sub: input.userId, |
| 373 | 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 | 440 | iat: Math.floor(now.epochMilliseconds / 1_000), |
| 377 | 441 | exp: Math.floor(expiresAt.epochMilliseconds / 1_000), |
| ... | ... | @@ -386,11 +450,27 @@ const make = Effect.gen(function* () { |
| 386 | 450 | privateKey: Redacted.value(settings.cloudMintPrivateKey), |
| 387 | 451 | typ: RELAY_MINT_REQUEST_TYP, |
| 388 | 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 | 463 | const environmentClient = yield* makeEnvironmentClient(endpoint.httpBaseUrl); |
| 391 | 464 | const decoded = yield* environmentClient.cloud.t3MintCredential({ payload: { proof } }).pipe( |
| 392 | 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 | 474 | Effect.timeoutOption(Duration.millis(ENVIRONMENT_MINT_REQUEST_TIMEOUT_MS)), |
| 395 | 475 | Effect.flatMap( |
| 396 | 476 | Option.match({ |
| ... | ... | @@ -415,7 +495,10 @@ const make = Effect.gen(function* () { |
| 415 | 495 | nowEpochSeconds: Math.floor(now.epochMilliseconds / 1_000), |
| 416 | 496 | }); |
| 417 | 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 | 503 | return { |
| 421 | 504 | environmentId: link.environmentId, |
infra/relay/src/environments/EnvironmentCredentials.ts+22-13| ... | ... | @@ -1,33 +1,42 @@ |
| 1 | 1 | import * as Context from "effect/Context"; |
| 2 | 2 | import * as Crypto from "effect/Crypto"; |
| 3 | import * as Data from "effect/Data"; | |
| 4 | 3 | import * as DateTime from "effect/DateTime"; |
| 5 | 4 | import * as Effect from "effect/Effect"; |
| 6 | 5 | import * as Encoding from "effect/Encoding"; |
| 7 | 6 | import * as Layer from "effect/Layer"; |
| 8 | 7 | import * as Option from "effect/Option"; |
| 8 | import * as Schema from "effect/Schema"; | |
| 9 | 9 | import { and, eq, isNull, ne, notExists } from "drizzle-orm"; |
| 10 | 10 | |
| 11 | 11 | import { RelayDb } from "../db.ts"; |
| 12 | 12 | import { relayEnvironmentCredentials, relayEnvironmentLinks } from "../persistence/schema.ts"; |
| 13 | 13 | |
| 14 | export class EnvironmentCredentialCreatePersistenceError extends Data.TaggedError( | |
| 14 | export class EnvironmentCredentialCreatePersistenceError extends Schema.TaggedErrorClass<EnvironmentCredentialCreatePersistenceError>()( | |
| 15 | 15 | "EnvironmentCredentialCreatePersistenceError", |
| 16 | )<{ | |
| 17 | readonly cause: unknown; | |
| 18 | }> {} | |
| 16 | { cause: Schema.Defect() }, | |
| 17 | ) { | |
| 18 | override get message(): string { | |
| 19 | return "Failed to persist environment credential"; | |
| 20 | } | |
| 21 | } | |
| 19 | 22 | |
| 20 | export class EnvironmentCredentialAuthenticatePersistenceError extends Data.TaggedError( | |
| 23 | export class EnvironmentCredentialAuthenticatePersistenceError extends Schema.TaggedErrorClass<EnvironmentCredentialAuthenticatePersistenceError>()( | |
| 21 | 24 | "EnvironmentCredentialAuthenticatePersistenceError", |
| 22 | )<{ | |
| 23 | readonly cause: unknown; | |
| 24 | }> {} | |
| 25 | { cause: Schema.Defect() }, | |
| 26 | ) { | |
| 27 | override get message(): string { | |
| 28 | return "Failed to authenticate environment credential"; | |
| 29 | } | |
| 30 | } | |
| 25 | 31 | |
| 26 | export class EnvironmentCredentialRevokePersistenceError extends Data.TaggedError( | |
| 32 | export class EnvironmentCredentialRevokePersistenceError extends Schema.TaggedErrorClass<EnvironmentCredentialRevokePersistenceError>()( | |
| 27 | 33 | "EnvironmentCredentialRevokePersistenceError", |
| 28 | )<{ | |
| 29 | readonly cause: unknown; | |
| 30 | }> {} | |
| 34 | { cause: Schema.Defect() }, | |
| 35 | ) { | |
| 36 | override get message(): string { | |
| 37 | return "Failed to revoke environment credential"; | |
| 38 | } | |
| 39 | } | |
| 31 | 40 | |
| 32 | 41 | export interface EnvironmentCredentialPrincipal { |
| 33 | 42 | readonly credentialId: string; |
infra/relay/src/environments/EnvironmentLinker.ts+22-9| ... | ... | @@ -1,6 +1,6 @@ |
| 1 | 1 | import { |
| 2 | 2 | RelayEnvironmentLinkProofPayload, |
| 3 | type RelayEnvironmentLinkProofInvalidReason, | |
| 3 | RelayEnvironmentLinkProofInvalidReason, | |
| 4 | 4 | type RelayEnvironmentLinkRequest, |
| 5 | 5 | } from "@t3tools/contracts/relay"; |
| 6 | 6 | import { |
| ... | ... | @@ -10,7 +10,6 @@ import { |
| 10 | 10 | verifyRelayJwt, |
| 11 | 11 | } from "@t3tools/shared/relayJwt"; |
| 12 | 12 | import * as Context from "effect/Context"; |
| 13 | import * as Data from "effect/Data"; | |
| 14 | 13 | import * as DateTime from "effect/DateTime"; |
| 15 | 14 | import * as Effect from "effect/Effect"; |
| 16 | 15 | import * as Layer from "effect/Layer"; |
| ... | ... | @@ -23,14 +22,28 @@ import * as EnvironmentLinks from "./EnvironmentLinks.ts"; |
| 23 | 22 | import * as ManagedEndpointProvider from "./ManagedEndpointProvider.ts"; |
| 24 | 23 | import * as RelayConfiguration from "../Config.ts"; |
| 25 | 24 | |
| 26 | export class EnvironmentLinkProofExpired extends Data.TaggedError("EnvironmentLinkProofExpired")<{ | |
| 27 | readonly expiresAt: string; | |
| 28 | }> {} | |
| 25 | export class EnvironmentLinkProofExpired extends Schema.TaggedErrorClass<EnvironmentLinkProofExpired>()( | |
| 26 | "EnvironmentLinkProofExpired", | |
| 27 | { | |
| 28 | expiresAt: Schema.String, | |
| 29 | }, | |
| 30 | ) { | |
| 31 | override get message(): string { | |
| 32 | return `Environment link proof expired at ${this.expiresAt}`; | |
| 33 | } | |
| 34 | } | |
| 29 | 35 | |
| 30 | export class EnvironmentLinkProofInvalid extends Data.TaggedError("EnvironmentLinkProofInvalid")<{ | |
| 31 | readonly environmentId: string; | |
| 32 | readonly reason: RelayEnvironmentLinkProofInvalidReason; | |
| 33 | }> {} | |
| 36 | export class EnvironmentLinkProofInvalid extends Schema.TaggedErrorClass<EnvironmentLinkProofInvalid>()( | |
| 37 | "EnvironmentLinkProofInvalid", | |
| 38 | { | |
| 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 | } | |
| 34 | 47 | |
| 35 | 48 | export type EnvironmentLinkError = |
| 36 | 49 | | EnvironmentLinkProofExpired |
infra/relay/src/environments/EnvironmentLinks.ts+43-25| ... | ... | @@ -5,10 +5,10 @@ import type { |
| 5 | 5 | RelayManagedEndpoint, |
| 6 | 6 | } from "@t3tools/contracts/relay"; |
| 7 | 7 | import * as Context from "effect/Context"; |
| 8 | import * as Data from "effect/Data"; | |
| 9 | 8 | import * as DateTime from "effect/DateTime"; |
| 10 | 9 | import * as Effect from "effect/Effect"; |
| 11 | 10 | import * as Layer from "effect/Layer"; |
| 11 | import * as Schema from "effect/Schema"; | |
| 12 | 12 | import { and, eq, isNull, or } from "drizzle-orm"; |
| 13 | 13 | |
| 14 | 14 | import { RelayDb } from "../db.ts"; |
| ... | ... | @@ -24,41 +24,59 @@ export interface AgentAwarenessDeliveryUserRecord { |
| 24 | 24 | readonly liveActivitiesEnabled: boolean; |
| 25 | 25 | } |
| 26 | 26 | |
| 27 | export class EnvironmentLinkUpsertPersistenceError extends Data.TaggedError( | |
| 27 | export class EnvironmentLinkUpsertPersistenceError extends Schema.TaggedErrorClass<EnvironmentLinkUpsertPersistenceError>()( | |
| 28 | 28 | "EnvironmentLinkUpsertPersistenceError", |
| 29 | )<{ | |
| 30 | readonly cause: unknown; | |
| 31 | }> {} | |
| 29 | { cause: Schema.Defect() }, | |
| 30 | ) { | |
| 31 | override get message(): string { | |
| 32 | return "Failed to persist environment link"; | |
| 33 | } | |
| 34 | } | |
| 32 | 35 | |
| 33 | export class EnvironmentLinkUserListPersistenceError extends Data.TaggedError( | |
| 36 | export class EnvironmentLinkUserListPersistenceError extends Schema.TaggedErrorClass<EnvironmentLinkUserListPersistenceError>()( | |
| 34 | 37 | "EnvironmentLinkUserListPersistenceError", |
| 35 | )<{ | |
| 36 | readonly cause: unknown; | |
| 37 | }> {} | |
| 38 | { cause: Schema.Defect() }, | |
| 39 | ) { | |
| 40 | override get message(): string { | |
| 41 | return "Failed to list users linked to environment"; | |
| 42 | } | |
| 43 | } | |
| 38 | 44 | |
| 39 | export class EnvironmentPublicKeyListPersistenceError extends Data.TaggedError( | |
| 45 | export class EnvironmentPublicKeyListPersistenceError extends Schema.TaggedErrorClass<EnvironmentPublicKeyListPersistenceError>()( | |
| 40 | 46 | "EnvironmentPublicKeyListPersistenceError", |
| 41 | )<{ | |
| 42 | readonly cause: unknown; | |
| 43 | }> {} | |
| 47 | { cause: Schema.Defect() }, | |
| 48 | ) { | |
| 49 | override get message(): string { | |
| 50 | return "Failed to list environment public keys"; | |
| 51 | } | |
| 52 | } | |
| 44 | 53 | |
| 45 | export class EnvironmentLinkListPersistenceError extends Data.TaggedError( | |
| 54 | export class EnvironmentLinkListPersistenceError extends Schema.TaggedErrorClass<EnvironmentLinkListPersistenceError>()( | |
| 46 | 55 | "EnvironmentLinkListPersistenceError", |
| 47 | )<{ | |
| 48 | readonly cause: unknown; | |
| 49 | }> {} | |
| 56 | { cause: Schema.Defect() }, | |
| 57 | ) { | |
| 58 | override get message(): string { | |
| 59 | return "Failed to list environment links"; | |
| 60 | } | |
| 61 | } | |
| 50 | 62 | |
| 51 | export class EnvironmentLinkLookupPersistenceError extends Data.TaggedError( | |
| 63 | export class EnvironmentLinkLookupPersistenceError extends Schema.TaggedErrorClass<EnvironmentLinkLookupPersistenceError>()( | |
| 52 | 64 | "EnvironmentLinkLookupPersistenceError", |
| 53 | )<{ | |
| 54 | readonly cause: unknown; | |
| 55 | }> {} | |
| 65 | { cause: Schema.Defect() }, | |
| 66 | ) { | |
| 67 | override get message(): string { | |
| 68 | return "Failed to look up environment link"; | |
| 69 | } | |
| 70 | } | |
| 56 | 71 | |
| 57 | export class EnvironmentLinkRevokePersistenceError extends Data.TaggedError( | |
| 72 | export class EnvironmentLinkRevokePersistenceError extends Schema.TaggedErrorClass<EnvironmentLinkRevokePersistenceError>()( | |
| 58 | 73 | "EnvironmentLinkRevokePersistenceError", |
| 59 | )<{ | |
| 60 | readonly cause: unknown; | |
| 61 | }> {} | |
| 74 | { cause: Schema.Defect() }, | |
| 75 | ) { | |
| 76 | override get message(): string { | |
| 77 | return "Failed to revoke environment link"; | |
| 78 | } | |
| 79 | } | |
| 62 | 80 | |
| 63 | 81 | export interface EnvironmentLinksShape { |
| 64 | 82 | readonly upsert: (input: { |
infra/relay/src/environments/EnvironmentPublishSignatures.ts+27-13| ... | ... | @@ -11,7 +11,6 @@ import { |
| 11 | 11 | import { stableStringify } from "@t3tools/shared/relaySigning"; |
| 12 | 12 | import * as Context from "effect/Context"; |
| 13 | 13 | import * as Crypto from "effect/Crypto"; |
| 14 | import * as Data from "effect/Data"; | |
| 15 | 14 | import * as DateTime from "effect/DateTime"; |
| 16 | 15 | import * as Effect from "effect/Effect"; |
| 17 | 16 | import * as Encoding from "effect/Encoding"; |
| ... | ... | @@ -21,23 +20,38 @@ import * as Schema from "effect/Schema"; |
| 21 | 20 | import * as DpopProofs from "../auth/DpopProofs.ts"; |
| 22 | 21 | import * as RelayConfiguration from "../Config.ts"; |
| 23 | 22 | |
| 24 | export class EnvironmentPublishSignatureExpired extends Data.TaggedError( | |
| 23 | export class EnvironmentPublishSignatureExpired extends Schema.TaggedErrorClass<EnvironmentPublishSignatureExpired>()( | |
| 25 | 24 | "EnvironmentPublishSignatureExpired", |
| 26 | )<{ | |
| 27 | readonly expiresAt: string; | |
| 28 | }> {} | |
| 25 | { | |
| 26 | expiresAt: Schema.String, | |
| 27 | }, | |
| 28 | ) { | |
| 29 | override get message(): string { | |
| 30 | return `Environment publish signature expired at ${this.expiresAt}`; | |
| 31 | } | |
| 32 | } | |
| 29 | 33 | |
| 30 | export class EnvironmentPublishSignatureInvalid extends Data.TaggedError( | |
| 34 | export class EnvironmentPublishSignatureInvalid extends Schema.TaggedErrorClass<EnvironmentPublishSignatureInvalid>()( | |
| 31 | 35 | "EnvironmentPublishSignatureInvalid", |
| 32 | )<{ | |
| 33 | readonly environmentId: string; | |
| 34 | }> {} | |
| 36 | { | |
| 37 | environmentId: Schema.String, | |
| 38 | }, | |
| 39 | ) { | |
| 40 | override get message(): string { | |
| 41 | return `Environment '${this.environmentId}' publish signature is invalid`; | |
| 42 | } | |
| 43 | } | |
| 35 | 44 | |
| 36 | export class EnvironmentPublishPublicKeyMissing extends Data.TaggedError( | |
| 45 | export class EnvironmentPublishPublicKeyMissing extends Schema.TaggedErrorClass<EnvironmentPublishPublicKeyMissing>()( | |
| 37 | 46 | "EnvironmentPublishPublicKeyMissing", |
| 38 | )<{ | |
| 39 | readonly environmentId: string; | |
| 40 | }> {} | |
| 47 | { | |
| 48 | environmentId: Schema.String, | |
| 49 | }, | |
| 50 | ) { | |
| 51 | override get message(): string { | |
| 52 | return `Environment '${this.environmentId}' has no publish public key`; | |
| 53 | } | |
| 54 | } | |
| 41 | 55 | |
| 42 | 56 | export type EnvironmentPublishSignatureError = |
| 43 | 57 | | EnvironmentPublishSignatureExpired |
infra/relay/src/environments/ManagedEndpointAllocations.ts+12-6| ... | ... | @@ -1,10 +1,10 @@ |
| 1 | 1 | import type { RelayManagedEndpoint } from "@t3tools/contracts/relay"; |
| 2 | 2 | import { and, eq } from "drizzle-orm"; |
| 3 | 3 | import * as Context from "effect/Context"; |
| 4 | import * as Data from "effect/Data"; | |
| 5 | 4 | import * as DateTime from "effect/DateTime"; |
| 6 | 5 | import * as Effect from "effect/Effect"; |
| 7 | 6 | import * as Layer from "effect/Layer"; |
| 7 | import * as Schema from "effect/Schema"; | |
| 8 | 8 | |
| 9 | 9 | import { RelayDb } from "../db.ts"; |
| 10 | 10 | import { isManagedEndpointHostname, managedEndpointForHostname } from "../deploymentConfig.ts"; |
| ... | ... | @@ -36,11 +36,17 @@ export function resolveReadyManagedEndpoint(input: { |
| 36 | 36 | return managedEndpointForHostname(input.allocation.hostname); |
| 37 | 37 | } |
| 38 | 38 | |
| 39 | export class ManagedEndpointAllocationPersistenceError extends Data.TaggedError( | |
| 39 | export class ManagedEndpointAllocationPersistenceError extends Schema.TaggedErrorClass<ManagedEndpointAllocationPersistenceError>()( | |
| 40 | 40 | "ManagedEndpointAllocationPersistenceError", |
| 41 | )<{ | |
| 42 | readonly cause: unknown; | |
| 43 | }> {} | |
| 41 | { cause: Schema.Defect() }, | |
| 42 | ) { | |
| 43 | override get message(): string { | |
| 44 | return "Failed to persist managed endpoint allocation"; | |
| 45 | } | |
| 46 | } | |
| 47 | const isManagedEndpointAllocationPersistenceError = Schema.is( | |
| 48 | ManagedEndpointAllocationPersistenceError, | |
| 49 | ); | |
| 44 | 50 | |
| 45 | 51 | interface ManagedEndpointAllocationKey { |
| 46 | 52 | readonly userId: string; |
| ... | ... | @@ -98,7 +104,7 @@ const whereAllocation = (input: ManagedEndpointAllocationKey) => |
| 98 | 104 | ); |
| 99 | 105 | |
| 100 | 106 | const persistenceError = (cause: unknown) => |
| 101 | cause instanceof ManagedEndpointAllocationPersistenceError | |
| 107 | isManagedEndpointAllocationPersistenceError(cause) | |
| 102 | 108 | ? cause |
| 103 | 109 | : new ManagedEndpointAllocationPersistenceError({ cause }); |
| 104 | 110 |
infra/relay/src/environments/ManagedEndpointProvider.ts+46-24| ... | ... | @@ -3,12 +3,12 @@ import * as Cloudflare from "alchemy/Cloudflare"; |
| 3 | 3 | import * as Arr from "effect/Array"; |
| 4 | 4 | import * as Context from "effect/Context"; |
| 5 | 5 | import * as Crypto from "effect/Crypto"; |
| 6 | import * as Data from "effect/Data"; | |
| 7 | 6 | import * as Effect from "effect/Effect"; |
| 8 | 7 | import * as Encoding from "effect/Encoding"; |
| 9 | 8 | import * as Layer from "effect/Layer"; |
| 10 | 9 | import * as Option from "effect/Option"; |
| 11 | 10 | import * as Result from "effect/Result"; |
| 11 | import * as Schema from "effect/Schema"; | |
| 12 | 12 | |
| 13 | 13 | import type { |
| 14 | 14 | RelayManagedEndpoint, |
| ... | ... | @@ -25,28 +25,44 @@ import { |
| 25 | 25 | } from "../deploymentConfig.ts"; |
| 26 | 26 | import { ManagedEndpointAllocations } from "./ManagedEndpointAllocations.ts"; |
| 27 | 27 | |
| 28 | export class ManagedEndpointProvisioningNotConfigured extends Data.TaggedError( | |
| 28 | export class ManagedEndpointProvisioningNotConfigured extends Schema.TaggedErrorClass<ManagedEndpointProvisioningNotConfigured>()( | |
| 29 | 29 | "ManagedEndpointProvisioningNotConfigured", |
| 30 | )<{}> {} | |
| 30 | {}, | |
| 31 | ) { | |
| 32 | override get message(): string { | |
| 33 | return "Managed endpoint provisioning is not configured"; | |
| 34 | } | |
| 35 | } | |
| 31 | 36 | |
| 32 | export class ManagedEndpointProvisioningFailed extends Data.TaggedError( | |
| 37 | export class ManagedEndpointProvisioningFailed extends Schema.TaggedErrorClass<ManagedEndpointProvisioningFailed>()( | |
| 33 | 38 | "ManagedEndpointProvisioningFailed", |
| 34 | )<{ | |
| 35 | readonly cause: unknown; | |
| 36 | }> {} | |
| 39 | { cause: Schema.Defect() }, | |
| 40 | ) { | |
| 41 | override get message(): string { | |
| 42 | return "Managed endpoint provisioning failed"; | |
| 43 | } | |
| 44 | } | |
| 37 | 45 | |
| 38 | export class ManagedEndpointDeprovisioningFailed extends Data.TaggedError( | |
| 46 | export class ManagedEndpointDeprovisioningFailed extends Schema.TaggedErrorClass<ManagedEndpointDeprovisioningFailed>()( | |
| 39 | 47 | "ManagedEndpointDeprovisioningFailed", |
| 40 | )<{ | |
| 41 | readonly cause: unknown; | |
| 42 | }> {} | |
| 48 | { cause: Schema.Defect() }, | |
| 49 | ) { | |
| 50 | override get message(): string { | |
| 51 | return "Managed endpoint deprovisioning failed"; | |
| 52 | } | |
| 53 | } | |
| 43 | 54 | |
| 44 | export class ManagedEndpointOriginNotAllowed extends Data.TaggedError( | |
| 55 | export class ManagedEndpointOriginNotAllowed extends Schema.TaggedErrorClass<ManagedEndpointOriginNotAllowed>()( | |
| 45 | 56 | "ManagedEndpointOriginNotAllowed", |
| 46 | )<{ | |
| 47 | readonly host: string; | |
| 48 | readonly port: number; | |
| 49 | }> {} | |
| 57 | { | |
| 58 | host: Schema.String, | |
| 59 | port: Schema.Number, | |
| 60 | }, | |
| 61 | ) { | |
| 62 | override get message(): string { | |
| 63 | return `Managed endpoint origin '${this.host}:${this.port}' is not allowed`; | |
| 64 | } | |
| 65 | } | |
| 50 | 66 | |
| 51 | 67 | export type ManagedEndpointProviderError = |
| 52 | 68 | | ManagedEndpointProvisioningNotConfigured |
| ... | ... | @@ -80,11 +96,14 @@ interface ManagedEndpointTunnel { |
| 80 | 96 | readonly name?: string | null; |
| 81 | 97 | } |
| 82 | 98 | |
| 83 | export class ManagedEndpointTunnelClientError extends Data.TaggedError( | |
| 99 | export class ManagedEndpointTunnelClientError extends Schema.TaggedErrorClass<ManagedEndpointTunnelClientError>()( | |
| 84 | 100 | "ManagedEndpointTunnelClientError", |
| 85 | )<{ | |
| 86 | readonly cause: unknown; | |
| 87 | }> {} | |
| 101 | { cause: Schema.Defect() }, | |
| 102 | ) { | |
| 103 | override get message(): string { | |
| 104 | return "Managed endpoint tunnel provider request failed"; | |
| 105 | } | |
| 106 | } | |
| 88 | 107 | |
| 89 | 108 | export interface ManagedEndpointTunnelClientShape { |
| 90 | 109 | readonly list: (request: { |
| ... | ... | @@ -124,11 +143,14 @@ interface ManagedEndpointCnameRecordInput { |
| 124 | 143 | readonly proxied: true; |
| 125 | 144 | } |
| 126 | 145 | |
| 127 | export class ManagedEndpointDnsClientError extends Data.TaggedError( | |
| 146 | export class ManagedEndpointDnsClientError extends Schema.TaggedErrorClass<ManagedEndpointDnsClientError>()( | |
| 128 | 147 | "ManagedEndpointDnsClientError", |
| 129 | )<{ | |
| 130 | readonly cause: unknown; | |
| 131 | }> {} | |
| 148 | { cause: Schema.Defect() }, | |
| 149 | ) { | |
| 150 | override get message(): string { | |
| 151 | return "Managed endpoint DNS provider request failed"; | |
| 152 | } | |
| 153 | } | |
| 132 | 154 | |
| 133 | 155 | export interface ManagedEndpointDnsClientShape { |
| 134 | 156 | readonly listRecords: ( |
infra/relay/src/http/Api.ts+11-8| ... | ... | @@ -1,6 +1,5 @@ |
| 1 | 1 | import { createClerkClient, verifyToken } from "@clerk/backend"; |
| 2 | 2 | import { sql as drizzleSql } from "drizzle-orm"; |
| 3 | import * as Data from "effect/Data"; | |
| 4 | 3 | import * as Crypto from "effect/Crypto"; |
| 5 | 4 | import * as Context from "effect/Context"; |
| 6 | 5 | import * as DateTime from "effect/DateTime"; |
| ... | ... | @@ -813,9 +812,16 @@ export const serverApi = HttpApiBuilder.group( |
| 813 | 812 | }), |
| 814 | 813 | ); |
| 815 | 814 | |
| 816 | class ClerkTokenVerificationFailed extends Data.TaggedError("ClerkTokenVerificationFailed")<{ | |
| 817 | readonly cause: unknown; | |
| 818 | }> {} | |
| 815 | class ClerkTokenVerificationFailed extends Schema.TaggedErrorClass<ClerkTokenVerificationFailed>()( | |
| 816 | "ClerkTokenVerificationFailed", | |
| 817 | { | |
| 818 | cause: Schema.Defect(), | |
| 819 | }, | |
| 820 | ) { | |
| 821 | override get message(): string { | |
| 822 | return "Clerk token verification failed"; | |
| 823 | } | |
| 824 | } | |
| 819 | 825 | |
| 820 | 826 | const isHttpUnauthorized = Schema.is(HttpApiError.Unauthorized); |
| 821 | 827 | |
| ... | ... | @@ -846,16 +852,13 @@ const COMMON_AUTH_INVALID_REASONS = [ |
| 846 | 852 | DeliveryAttempts.DeliveryAttemptRecordPersistenceError, |
| 847 | 853 | ] as const; |
| 848 | 854 | type RelayCommonPersistenceError = InstanceType<(typeof COMMON_AUTH_INVALID_REASONS)[number]>; |
| 855 | const isRelayCommonPersistenceError = Schema.is(Schema.Union(COMMON_AUTH_INVALID_REASONS)); | |
| 849 | 856 | |
| 850 | 857 | type MapRelayCommonApiError<E> = |
| 851 | 858 | | Exclude<E, HttpApiError.Unauthorized | RelayCommonPersistenceError> |
| 852 | 859 | | (Extract<E, HttpApiError.Unauthorized> extends never ? never : RelayAuthInvalidError) |
| 853 | 860 | | (Extract<E, RelayCommonPersistenceError> extends never ? never : RelayInternalError); |
| 854 | 861 | |
| 855 | function isRelayCommonPersistenceError(error: unknown): error is RelayCommonPersistenceError { | |
| 856 | return COMMON_AUTH_INVALID_REASONS.some((ErrorType) => error instanceof ErrorType); | |
| 857 | } | |
| 858 | ||
| 859 | 862 | function relayInternalErrorResponse(reason: RelayInternalError["reason"]) { |
| 860 | 863 | return currentTraceId.pipe( |
| 861 | 864 | Effect.flatMap((traceId) => |
infra/relay/src/observability.test.ts created+86| ... | ... | @@ -0,0 +1,86 @@ |
| 1 | import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer"; | |
| 2 | import { expect, it } from "@effect/vitest"; | |
| 3 | import * as Deferred from "effect/Deferred"; | |
| 4 | import * as Effect from "effect/Effect"; | |
| 5 | import * as Redacted from "effect/Redacted"; | |
| 6 | import * as Schema from "effect/Schema"; | |
| 7 | import * as HttpServer from "effect/unstable/http/HttpServer"; | |
| 8 | import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest"; | |
| 9 | import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; | |
| 10 | import type { OtlpTracer } from "effect/unstable/observability"; | |
| 11 | ||
| 12 | import { EnvironmentMintRequestFailed } from "./environments/EnvironmentConnector.ts"; | |
| 13 | import { makeRelayTraceLayer } from "./observability.ts"; | |
| 14 | ||
| 15 | interface ExportedRequest { | |
| 16 | readonly authorization: string | undefined; | |
| 17 | readonly body: string; | |
| 18 | readonly dataset: string | undefined; | |
| 19 | } | |
| 20 | ||
| 21 | const 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 | ||
| 28 | const decodeJson = Schema.decodeUnknownEffect(Schema.UnknownFromJsonString); | |
| 29 | ||
| 30 | it.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 | 1 | import * as Alchemy from "alchemy"; |
| 2 | 2 | import * as Axiom from "alchemy/Axiom"; |
| 3 | 3 | import * as Output from "alchemy/Output"; |
| 4 | import * as Layer from "effect/Layer"; | |
| 4 | import * as Cause from "effect/Cause"; | |
| 5 | 5 | import * as Effect from "effect/Effect"; |
| 6 | import * as Exit from "effect/Exit"; | |
| 7 | import * as Layer from "effect/Layer"; | |
| 8 | import * as Option from "effect/Option"; | |
| 6 | 9 | import * as Redacted from "effect/Redacted"; |
| 10 | import * as Schema from "effect/Schema"; | |
| 11 | import * as Tracer from "effect/Tracer"; | |
| 7 | 12 | import { OtlpSerialization, OtlpTracer } from "effect/unstable/observability"; |
| 8 | 13 | |
| 9 | 14 | import { relayResourceNameForStage } from "./deploymentConfig.ts"; |
| ... | ... | @@ -54,23 +59,163 @@ export const withSpanAttributes = |
| 54 | 59 | Effect.andThen(effect.pipe(Effect.annotateSpans(attributes))), |
| 55 | 60 | ); |
| 56 | 61 | |
| 62 | const 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 | ||
| 86 | const 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 | ||
| 116 | const 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 | ||
| 136 | class 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 | ||
| 193 | const 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 | ||
| 57 | 199 | export const makeRelayTraceLayer = (input: { |
| 58 | 200 | readonly tracesEndpoint: string; |
| 59 | 201 | readonly tracesDatasetName: string; |
| 60 | 202 | readonly ingestToken: Redacted.Redacted<string>; |
| 61 | 203 | }) => |
| 62 | OtlpTracer.layer({ | |
| 63 | url: input.tracesEndpoint, | |
| 64 | resource: { | |
| 65 | serviceName: "t3-code-relay-worker", | |
| 66 | attributes: { | |
| 67 | "service.runtime": "cloudflare-worker", | |
| 68 | "service.component": "relay", | |
| 204 | Layer.effect( | |
| 205 | Tracer.Tracer, | |
| 206 | OtlpTracer.make({ | |
| 207 | url: input.tracesEndpoint, | |
| 208 | resource: { | |
| 209 | serviceName: "t3-code-relay-worker", | |
| 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 | }, | |
| 71 | headers: { | |
| 72 | Authorization: `Bearer ${Redacted.value(input.ingestToken)}`, | |
| 73 | "X-Axiom-Dataset": input.tracesDatasetName, | |
| 74 | }, | |
| 75 | exportInterval: "1 second", | |
| 76 | }).pipe(Layer.provide(OtlpSerialization.layerJson)); | |
| 219 | exportInterval: "1 second", | |
| 220 | }).pipe(Effect.map(withSchemaErrorAttributes)), | |
| 221 | ).pipe(Layer.provide(OtlpSerialization.layerJson)); |
packages/contracts/src/relay.ts+55-11| ... | ... | @@ -329,7 +329,11 @@ export class RelayAuthInvalidError extends Schema.TaggedErrorClass<RelayAuthInva |
| 329 | 329 | traceId: TrimmedNonEmptyString, |
| 330 | 330 | }, |
| 331 | 331 | { httpApiStatus: 401 }, |
| 332 | ) {} | |
| 332 | ) { | |
| 333 | override get message(): string { | |
| 334 | return `Relay authentication failed: ${this.reason}`; | |
| 335 | } | |
| 336 | } | |
| 333 | 337 | |
| 334 | 338 | export class RelayEnvironmentLinkProofExpiredError extends Schema.TaggedErrorClass<RelayEnvironmentLinkProofExpiredError>()( |
| 335 | 339 | "RelayEnvironmentLinkProofExpiredError", |
| ... | ... | @@ -338,7 +342,11 @@ export class RelayEnvironmentLinkProofExpiredError extends Schema.TaggedErrorCla |
| 338 | 342 | traceId: TrimmedNonEmptyString, |
| 339 | 343 | }, |
| 340 | 344 | { httpApiStatus: 401 }, |
| 341 | ) {} | |
| 345 | ) { | |
| 346 | override get message(): string { | |
| 347 | return "Relay environment link proof expired"; | |
| 348 | } | |
| 349 | } | |
| 342 | 350 | |
| 343 | 351 | export class RelayEnvironmentLinkProofInvalidError extends Schema.TaggedErrorClass<RelayEnvironmentLinkProofInvalidError>()( |
| 344 | 352 | "RelayEnvironmentLinkProofInvalidError", |
| ... | ... | @@ -348,7 +356,11 @@ export class RelayEnvironmentLinkProofInvalidError extends Schema.TaggedErrorCla |
| 348 | 356 | traceId: TrimmedNonEmptyString, |
| 349 | 357 | }, |
| 350 | 358 | { httpApiStatus: 400 }, |
| 351 | ) {} | |
| 359 | ) { | |
| 360 | override get message(): string { | |
| 361 | return `Relay environment link proof is invalid: ${this.reason}`; | |
| 362 | } | |
| 363 | } | |
| 352 | 364 | |
| 353 | 365 | export class RelayEnvironmentConnectNotAuthorizedError extends Schema.TaggedErrorClass<RelayEnvironmentConnectNotAuthorizedError>()( |
| 354 | 366 | "RelayEnvironmentConnectNotAuthorizedError", |
| ... | ... | @@ -357,7 +369,11 @@ export class RelayEnvironmentConnectNotAuthorizedError extends Schema.TaggedErro |
| 357 | 369 | traceId: TrimmedNonEmptyString, |
| 358 | 370 | }, |
| 359 | 371 | { httpApiStatus: 403 }, |
| 360 | ) {} | |
| 372 | ) { | |
| 373 | override get message(): string { | |
| 374 | return "Relay environment connection is not authorized"; | |
| 375 | } | |
| 376 | } | |
| 361 | 377 | |
| 362 | 378 | export class RelayEnvironmentEndpointUnavailableError extends Schema.TaggedErrorClass<RelayEnvironmentEndpointUnavailableError>()( |
| 363 | 379 | "RelayEnvironmentEndpointUnavailableError", |
| ... | ... | @@ -367,7 +383,11 @@ export class RelayEnvironmentEndpointUnavailableError extends Schema.TaggedError |
| 367 | 383 | traceId: TrimmedNonEmptyString, |
| 368 | 384 | }, |
| 369 | 385 | { httpApiStatus: 502 }, |
| 370 | ) {} | |
| 386 | ) { | |
| 387 | override get message(): string { | |
| 388 | return `Relay environment endpoint is unavailable: ${this.reason}`; | |
| 389 | } | |
| 390 | } | |
| 371 | 391 | |
| 372 | 392 | export class RelayEnvironmentEndpointTimedOutError extends Schema.TaggedErrorClass<RelayEnvironmentEndpointTimedOutError>()( |
| 373 | 393 | "RelayEnvironmentEndpointTimedOutError", |
| ... | ... | @@ -376,7 +396,11 @@ export class RelayEnvironmentEndpointTimedOutError extends Schema.TaggedErrorCla |
| 376 | 396 | traceId: TrimmedNonEmptyString, |
| 377 | 397 | }, |
| 378 | 398 | { httpApiStatus: 504 }, |
| 379 | ) {} | |
| 399 | ) { | |
| 400 | override get message(): string { | |
| 401 | return "Relay environment endpoint request timed out"; | |
| 402 | } | |
| 403 | } | |
| 380 | 404 | |
| 381 | 405 | export class RelayEnvironmentLinkFailedError extends Schema.TaggedErrorClass<RelayEnvironmentLinkFailedError>()( |
| 382 | 406 | "RelayEnvironmentLinkFailedError", |
| ... | ... | @@ -386,7 +410,11 @@ export class RelayEnvironmentLinkFailedError extends Schema.TaggedErrorClass<Rel |
| 386 | 410 | traceId: TrimmedNonEmptyString, |
| 387 | 411 | }, |
| 388 | 412 | { httpApiStatus: 500 }, |
| 389 | ) {} | |
| 413 | ) { | |
| 414 | override get message(): string { | |
| 415 | return `Relay environment link failed: ${this.reason}`; | |
| 416 | } | |
| 417 | } | |
| 390 | 418 | |
| 391 | 419 | export class RelayEnvironmentLinkUnavailableError extends Schema.TaggedErrorClass<RelayEnvironmentLinkUnavailableError>()( |
| 392 | 420 | "RelayEnvironmentLinkUnavailableError", |
| ... | ... | @@ -396,7 +424,11 @@ export class RelayEnvironmentLinkUnavailableError extends Schema.TaggedErrorClas |
| 396 | 424 | traceId: TrimmedNonEmptyString, |
| 397 | 425 | }, |
| 398 | 426 | { httpApiStatus: 503 }, |
| 399 | ) {} | |
| 427 | ) { | |
| 428 | override get message(): string { | |
| 429 | return `Relay environment link is unavailable: ${this.reason}`; | |
| 430 | } | |
| 431 | } | |
| 400 | 432 | |
| 401 | 433 | export class RelayAgentActivityPublishProofExpiredError extends Schema.TaggedErrorClass<RelayAgentActivityPublishProofExpiredError>()( |
| 402 | 434 | "RelayAgentActivityPublishProofExpiredError", |
| ... | ... | @@ -405,7 +437,11 @@ export class RelayAgentActivityPublishProofExpiredError extends Schema.TaggedErr |
| 405 | 437 | traceId: TrimmedNonEmptyString, |
| 406 | 438 | }, |
| 407 | 439 | { httpApiStatus: 401 }, |
| 408 | ) {} | |
| 440 | ) { | |
| 441 | override get message(): string { | |
| 442 | return "Relay agent activity publish proof expired"; | |
| 443 | } | |
| 444 | } | |
| 409 | 445 | |
| 410 | 446 | export class RelayAgentActivityPublishProofInvalidError extends Schema.TaggedErrorClass<RelayAgentActivityPublishProofInvalidError>()( |
| 411 | 447 | "RelayAgentActivityPublishProofInvalidError", |
| ... | ... | @@ -415,7 +451,11 @@ export class RelayAgentActivityPublishProofInvalidError extends Schema.TaggedErr |
| 415 | 451 | traceId: TrimmedNonEmptyString, |
| 416 | 452 | }, |
| 417 | 453 | { httpApiStatus: 401 }, |
| 418 | ) {} | |
| 454 | ) { | |
| 455 | override get message(): string { | |
| 456 | return `Relay agent activity publish proof is invalid: ${this.reason}`; | |
| 457 | } | |
| 458 | } | |
| 419 | 459 | |
| 420 | 460 | export class RelayInternalError extends Schema.TaggedErrorClass<RelayInternalError>()( |
| 421 | 461 | "RelayInternalError", |
| ... | ... | @@ -425,7 +465,11 @@ export class RelayInternalError extends Schema.TaggedErrorClass<RelayInternalErr |
| 425 | 465 | traceId: TrimmedNonEmptyString, |
| 426 | 466 | }, |
| 427 | 467 | { httpApiStatus: 500 }, |
| 428 | ) {} | |
| 468 | ) { | |
| 469 | override get message(): string { | |
| 470 | return `Relay internal error: ${this.reason}`; | |
| 471 | } | |
| 472 | } | |
| 429 | 473 | |
| 430 | 474 | export const RelayProtectedError = Schema.Union([ |
| 431 | 475 | RelayAuthInvalidError, |