authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-19 19:56:36-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-19 19:56:36-07:00
log53d0107759b743cdd7a9b6e777d4628650ed2131
tree819d1bdab1d3f9c903fa24e61ca6bab862dc4cfa
parent60b16d1f28104c95a3ab8e673cb439124a4a8668
signature Signed by PGP key B5690EEEBB952194

[codex] align relay foundation Effect services (#3182)

Co-authored-by: codex <codex@users.noreply.github.com>

36 files changed, 403 insertions(+), 385 deletions(-)

infra/relay/alchemy.run.ts+3-3
...@@ -7,7 +7,7 @@ import * as Effect from "effect/Effect";...@@ -7,7 +7,7 @@ import * as Effect from "effect/Effect";
7import * as Layer from "effect/Layer";7import * as Layer from "effect/Layer";
8import * as Planetscale from "alchemy/Planetscale";8import * as Planetscale from "alchemy/Planetscale";
99
10import { PlanetscaleDatabase, RelayHyperdrive } from "./src/db.ts";10import * as RelayDb from "./src/db.ts";
11import { RelayObservability } from "./src/observability.ts";11import { RelayObservability } from "./src/observability.ts";
12import { ManagedEndpointZone, RelayApiZone } from "./src/zone.ts";12import { ManagedEndpointZone, RelayApiZone } from "./src/zone.ts";
13import Api from "./src/worker.ts";13import Api from "./src/worker.ts";
...@@ -24,8 +24,8 @@ export default Alchemy.Stack(...@@ -24,8 +24,8 @@ export default Alchemy.Stack(
24 state: Cloudflare.state(),24 state: Cloudflare.state(),
25 },25 },
26 Effect.gen(function* () {26 Effect.gen(function* () {
27 const db = yield* PlanetscaleDatabase;27 const db = yield* RelayDb.PlanetscaleDatabase;
28 const hyperdrive = yield* RelayHyperdrive;28 const hyperdrive = yield* RelayDb.RelayHyperdrive;
29 const managedEndpointZone = yield* ManagedEndpointZone.pipe(Effect.orDie);29 const managedEndpointZone = yield* ManagedEndpointZone.pipe(Effect.orDie);
30 const relayApiZone = yield* RelayApiZone.pipe(Effect.orDie);30 const relayApiZone = yield* RelayApiZone.pipe(Effect.orDie);
31 const observability = yield* RelayObservability;31 const observability = yield* RelayObservability;
infra/relay/src/Config.ts+19-14
...@@ -1,4 +1,5 @@...@@ -1,4 +1,5 @@
1import * as Context from "effect/Context";1import * as Context from "effect/Context";
2import * as Layer from "effect/Layer";
2import * as Redacted from "effect/Redacted";3import * as Redacted from "effect/Redacted";
3import * as Schema from "effect/Schema";4import * as Schema from "effect/Schema";
45
...@@ -13,20 +14,24 @@ export interface ApnsCredentials {...@@ -13,20 +14,24 @@ export interface ApnsCredentials {
13 readonly environment: ApnsEnvironment;14 readonly environment: ApnsEnvironment;
14}15}
1516
16export interface RelayConfigurationShape {
17 readonly relayIssuer: string;
18 readonly apns: ApnsCredentials;
19 readonly clerkSecretKey: Redacted.Redacted<string>;
20 readonly clerkPublishableKey: string;
21 readonly clerkJwtAudience: string;
22 readonly apnsDeliveryJobSigningSecret: Redacted.Redacted<string>;
23 readonly cloudMintPrivateKey: Redacted.Redacted<string>;
24 readonly cloudMintPublicKey: string;
25 readonly managedEndpointBaseDomain: string | undefined;
26 readonly managedEndpointNamespace: string | undefined;
27}
28
29export class RelayConfiguration extends Context.Service<17export class RelayConfiguration extends Context.Service<
30 RelayConfiguration,18 RelayConfiguration,
31 RelayConfigurationShape19 {
20 readonly relayIssuer: string;
21 readonly apns: ApnsCredentials;
22 readonly clerkSecretKey: Redacted.Redacted<string>;
23 readonly clerkPublishableKey: string;
24 readonly clerkJwtAudience: string;
25 readonly apnsDeliveryJobSigningSecret: Redacted.Redacted<string>;
26 readonly cloudMintPrivateKey: Redacted.Redacted<string>;
27 readonly cloudMintPublicKey: string;
28 readonly managedEndpointBaseDomain: string | undefined;
29 readonly managedEndpointNamespace: string | undefined;
30 }
32>()("t3code-relay/Config/RelayConfiguration") {}31>()("t3code-relay/Config/RelayConfiguration") {}
32
33export const make = (configuration: RelayConfiguration["Service"]) =>
34 RelayConfiguration.of(configuration);
35
36export const layer = (configuration: RelayConfiguration["Service"]) =>
37 Layer.succeed(RelayConfiguration, make(configuration));
infra/relay/src/agentActivity/AgentActivityPublisher.test.ts+2-2
...@@ -66,8 +66,8 @@ function makeAgentActivityRows(...@@ -66,8 +66,8 @@ function makeAgentActivityRows(
66}66}
6767
68function makeEnvironmentLinks(68function makeEnvironmentLinks(
69 overrides: Partial<EnvironmentLinks.EnvironmentLinksShape> = {},69 overrides: Partial<EnvironmentLinks.EnvironmentLinks["Service"]> = {},
70): EnvironmentLinks.EnvironmentLinksShape {70): EnvironmentLinks.EnvironmentLinks["Service"] {
71 return {71 return {
72 upsert: () => Effect.void,72 upsert: () => Effect.void,
73 listUsersForEnvironment: () => Effect.succeed(["dev:julius"]),73 listUsersForEnvironment: () => Effect.succeed(["dev:julius"]),
infra/relay/src/agentActivity/AgentActivityRows.ts+2-2
...@@ -9,7 +9,7 @@ import * as Option from "effect/Option";...@@ -9,7 +9,7 @@ import * as Option from "effect/Option";
9import * as Schema from "effect/Schema";9import * as Schema from "effect/Schema";
10import { and, desc, eq, isNull } from "drizzle-orm";10import { and, desc, eq, isNull } from "drizzle-orm";
1111
12import { RelayDb } from "../db.ts";12import * as RelayDb from "../db.ts";
13import { relayAgentActivityRows, relayEnvironmentLinks } from "../persistence/schema.ts";13import { relayAgentActivityRows, relayEnvironmentLinks } from "../persistence/schema.ts";
1414
15export class AgentActivityRowUpsertPersistenceError extends Schema.TaggedErrorClass<AgentActivityRowUpsertPersistenceError>()(15export class AgentActivityRowUpsertPersistenceError extends Schema.TaggedErrorClass<AgentActivityRowUpsertPersistenceError>()(
...@@ -70,7 +70,7 @@ const decodeRelayAgentActivityStateJson = Schema.decodeUnknownOption(...@@ -70,7 +70,7 @@ const decodeRelayAgentActivityStateJson = Schema.decodeUnknownOption(
70);70);
7171
72const make = Effect.gen(function* () {72const make = Effect.gen(function* () {
73 const db = yield* RelayDb;73 const db = yield* RelayDb.RelayDb;
7474
75 return AgentActivityRows.of({75 return AgentActivityRows.of({
76 upsert: Effect.fn("relay.agent_activity_rows.upsert")(76 upsert: Effect.fn("relay.agent_activity_rows.upsert")(
infra/relay/src/agentActivity/ApnsClient.ts+6-6
...@@ -9,7 +9,7 @@ import * as Option from "effect/Option";...@@ -9,7 +9,7 @@ import * as Option from "effect/Option";
9import * as Redacted from "effect/Redacted";9import * as Redacted from "effect/Redacted";
10import * as Schema from "effect/Schema";10import * as Schema from "effect/Schema";
11import { Headers, HttpClient, HttpClientRequest } from "effect/unstable/http";11import { Headers, HttpClient, HttpClientRequest } from "effect/unstable/http";
12import type { ApnsCredentials } from "../Config.ts";12import * as RelayConfiguration from "../Config.ts";
13import type { ApnsNotificationPayload } from "./apnsDeliveryJobs.ts";13import type { ApnsNotificationPayload } from "./apnsDeliveryJobs.ts";
1414
15const LIVE_ACTIVITY_NAME = "AgentActivity";15const LIVE_ACTIVITY_NAME = "AgentActivity";
...@@ -99,9 +99,9 @@ const encodeApnsJwtPayloadJson = Schema.encodeEffect(...@@ -99,9 +99,9 @@ const encodeApnsJwtPayloadJson = Schema.encodeEffect(
99);99);
100100
101const makeApnsJwt = Effect.fn("relay.apns.make_jwt")(function* (input: {101const makeApnsJwt = Effect.fn("relay.apns.make_jwt")(function* (input: {
102 readonly teamId: ApnsCredentials["teamId"];102 readonly teamId: RelayConfiguration.ApnsCredentials["teamId"];
103 readonly keyId: ApnsCredentials["keyId"];103 readonly keyId: RelayConfiguration.ApnsCredentials["keyId"];
104 readonly privateKey: ApnsCredentials["privateKey"];104 readonly privateKey: RelayConfiguration.ApnsCredentials["privateKey"];
105 readonly issuedAtUnixSeconds: number;105 readonly issuedAtUnixSeconds: number;
106}) {106}) {
107 const headerJson = yield* encodeApnsJwtHeaderJson({ alg: "ES256", kid: input.keyId }).pipe(107 const headerJson = yield* encodeApnsJwtHeaderJson({ alg: "ES256", kid: input.keyId }).pipe(
...@@ -235,12 +235,12 @@ export interface ApnsClientShape {...@@ -235,12 +235,12 @@ export interface ApnsClientShape {
235 readonly makeLiveActivityRequest: typeof makeLiveActivityRequest;235 readonly makeLiveActivityRequest: typeof makeLiveActivityRequest;
236 readonly makePushNotificationRequest: typeof makePushNotificationRequest;236 readonly makePushNotificationRequest: typeof makePushNotificationRequest;
237 readonly sendLiveActivityRequest: (input: {237 readonly sendLiveActivityRequest: (input: {
238 readonly credentials: ApnsCredentials;238 readonly credentials: RelayConfiguration.ApnsCredentials;
239 readonly request: ApnsLiveActivityRequest;239 readonly request: ApnsLiveActivityRequest;
240 readonly issuedAtUnixSeconds: number;240 readonly issuedAtUnixSeconds: number;
241 }) => Effect.Effect<ApnsDeliveryResult, ApnsError>;241 }) => Effect.Effect<ApnsDeliveryResult, ApnsError>;
242 readonly sendPushNotificationRequest: (input: {242 readonly sendPushNotificationRequest: (input: {
243 readonly credentials: ApnsCredentials;243 readonly credentials: RelayConfiguration.ApnsCredentials;
244 readonly request: ApnsPushNotificationRequest;244 readonly request: ApnsPushNotificationRequest;
245 readonly issuedAtUnixSeconds: number;245 readonly issuedAtUnixSeconds: number;
246 }) => Effect.Effect<ApnsDeliveryResult, ApnsError>;246 }) => Effect.Effect<ApnsDeliveryResult, ApnsError>;
infra/relay/src/agentActivity/ApnsDeliveries.test.ts+2-2
...@@ -153,7 +153,7 @@ function makeLayer(input: {...@@ -153,7 +153,7 @@ function makeLayer(input: {
153 Parameters<LiveActivities.LiveActivitiesShape["invalidateDeliveryToken"]>[0]153 Parameters<LiveActivities.LiveActivitiesShape["invalidateDeliveryToken"]>[0]
154 >;154 >;
155 readonly currentTargets?: ReadonlyArray<LiveActivities.TargetRow>;155 readonly currentTargets?: ReadonlyArray<LiveActivities.TargetRow>;
156 readonly config?: RelayConfiguration.RelayConfigurationShape;156 readonly config?: RelayConfiguration.RelayConfiguration["Service"];
157 readonly execute?: (157 readonly execute?: (
158 request: HttpClientRequest.HttpClientRequest,158 request: HttpClientRequest.HttpClientRequest,
159 ) => Effect.Effect<HttpClientResponse.HttpClientResponse>;159 ) => Effect.Effect<HttpClientResponse.HttpClientResponse>;
...@@ -213,7 +213,7 @@ function makeLayer(input: {...@@ -213,7 +213,7 @@ function makeLayer(input: {
213 input.invalidatedTokens?.push(invalidated);213 input.invalidatedTokens?.push(invalidated);
214 }),214 }),
215 }),215 }),
216 Layer.succeed(RelayConfiguration.RelayConfiguration, input.config ?? config),216 RelayConfiguration.layer(input.config ?? config),
217 input.execute217 input.execute
218 ? Layer.succeed(HttpClient.HttpClient, HttpClient.make(input.execute))218 ? Layer.succeed(HttpClient.HttpClient, HttpClient.make(input.execute))
219 : FetchHttpClient.layer,219 : FetchHttpClient.layer,
infra/relay/src/agentActivity/DeliveryAttempts.test.ts+13-13
...@@ -3,7 +3,7 @@ import { describe, expect, it } from "@effect/vitest";...@@ -3,7 +3,7 @@ import { describe, expect, it } from "@effect/vitest";
3import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";4import * as Layer from "effect/Layer";
55
6import { RelayDb, type RelayDatabase } from "../db.ts";6import * as RelayDb from "../db.ts";
7import { relayDeliveryAttempts } from "../persistence/schema.ts";7import { relayDeliveryAttempts } from "../persistence/schema.ts";
8import * as DeliveryAttempts from "./DeliveryAttempts.ts";8import * as DeliveryAttempts from "./DeliveryAttempts.ts";
99
...@@ -20,7 +20,7 @@ describe("DeliveryAttempts", () => {...@@ -20,7 +20,7 @@ describe("DeliveryAttempts", () => {
20 },20 },
21 };21 };
22 },22 },
23 } as unknown as RelayDatabase;23 } as unknown as RelayDb.RelayDb["Service"];
2424
25 return Effect.gen(function* () {25 return Effect.gen(function* () {
26 const attempts = yield* DeliveryAttempts.DeliveryAttempts;26 const attempts = yield* DeliveryAttempts.DeliveryAttempts;
...@@ -52,7 +52,7 @@ describe("DeliveryAttempts", () => {...@@ -52,7 +52,7 @@ describe("DeliveryAttempts", () => {
52 Effect.provide(52 Effect.provide(
53 DeliveryAttempts.layer.pipe(53 DeliveryAttempts.layer.pipe(
54 Layer.provide(NodeCryptoLayer.layer),54 Layer.provide(NodeCryptoLayer.layer),
55 Layer.provide(Layer.succeed(RelayDb, fakeDb)),55 Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)),
56 ),56 ),
57 ),57 ),
58 );58 );
...@@ -78,7 +78,7 @@ describe("DeliveryAttempts", () => {...@@ -78,7 +78,7 @@ describe("DeliveryAttempts", () => {
78 },78 },
79 };79 };
80 },80 },
81 } as unknown as RelayDatabase;81 } as unknown as RelayDb.RelayDb["Service"];
8282
83 return Effect.gen(function* () {83 return Effect.gen(function* () {
84 const attempts = yield* DeliveryAttempts.DeliveryAttempts;84 const attempts = yield* DeliveryAttempts.DeliveryAttempts;
...@@ -104,7 +104,7 @@ describe("DeliveryAttempts", () => {...@@ -104,7 +104,7 @@ describe("DeliveryAttempts", () => {
104 Effect.provide(104 Effect.provide(
105 DeliveryAttempts.layer.pipe(105 DeliveryAttempts.layer.pipe(
106 Layer.provide(NodeCryptoLayer.layer),106 Layer.provide(NodeCryptoLayer.layer),
107 Layer.provide(Layer.succeed(RelayDb, fakeDb)),107 Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)),
108 ),108 ),
109 ),109 ),
110 );110 );
...@@ -135,7 +135,7 @@ describe("DeliveryAttempts", () => {...@@ -135,7 +135,7 @@ describe("DeliveryAttempts", () => {
135 }),135 }),
136 }),136 }),
137 }),137 }),
138 } as unknown as RelayDatabase;138 } as unknown as RelayDb.RelayDb["Service"];
139139
140 return Effect.gen(function* () {140 return Effect.gen(function* () {
141 const attempts = yield* DeliveryAttempts.DeliveryAttempts;141 const attempts = yield* DeliveryAttempts.DeliveryAttempts;
...@@ -154,7 +154,7 @@ describe("DeliveryAttempts", () => {...@@ -154,7 +154,7 @@ describe("DeliveryAttempts", () => {
154 Effect.provide(154 Effect.provide(
155 DeliveryAttempts.layer.pipe(155 DeliveryAttempts.layer.pipe(
156 Layer.provide(NodeCryptoLayer.layer),156 Layer.provide(NodeCryptoLayer.layer),
157 Layer.provide(Layer.succeed(RelayDb, fakeDb)),157 Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)),
158 ),158 ),
159 ),159 ),
160 );160 );
...@@ -185,7 +185,7 @@ describe("DeliveryAttempts", () => {...@@ -185,7 +185,7 @@ describe("DeliveryAttempts", () => {
185 }),185 }),
186 }),186 }),
187 }),187 }),
188 } as unknown as RelayDatabase;188 } as unknown as RelayDb.RelayDb["Service"];
189189
190 return Effect.gen(function* () {190 return Effect.gen(function* () {
191 const attempts = yield* DeliveryAttempts.DeliveryAttempts;191 const attempts = yield* DeliveryAttempts.DeliveryAttempts;
...@@ -204,7 +204,7 @@ describe("DeliveryAttempts", () => {...@@ -204,7 +204,7 @@ describe("DeliveryAttempts", () => {
204 Effect.provide(204 Effect.provide(
205 DeliveryAttempts.layer.pipe(205 DeliveryAttempts.layer.pipe(
206 Layer.provide(NodeCryptoLayer.layer),206 Layer.provide(NodeCryptoLayer.layer),
207 Layer.provide(Layer.succeed(RelayDb, fakeDb)),207 Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)),
208 ),208 ),
209 ),209 ),
210 );210 );
...@@ -246,7 +246,7 @@ describe("DeliveryAttempts", () => {...@@ -246,7 +246,7 @@ describe("DeliveryAttempts", () => {
246 };246 };
247 },247 },
248 }),248 }),
249 } as unknown as RelayDatabase;249 } as unknown as RelayDb.RelayDb["Service"];
250250
251 return Effect.gen(function* () {251 return Effect.gen(function* () {
252 const attempts = yield* DeliveryAttempts.DeliveryAttempts;252 const attempts = yield* DeliveryAttempts.DeliveryAttempts;
...@@ -266,7 +266,7 @@ describe("DeliveryAttempts", () => {...@@ -266,7 +266,7 @@ describe("DeliveryAttempts", () => {
266 Effect.provide(266 Effect.provide(
267 DeliveryAttempts.layer.pipe(267 DeliveryAttempts.layer.pipe(
268 Layer.provide(NodeCryptoLayer.layer),268 Layer.provide(NodeCryptoLayer.layer),
269 Layer.provide(Layer.succeed(RelayDb, fakeDb)),269 Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)),
270 ),270 ),
271 ),271 ),
272 );272 );
...@@ -290,7 +290,7 @@ describe("DeliveryAttempts", () => {...@@ -290,7 +290,7 @@ describe("DeliveryAttempts", () => {
290 },290 },
291 };291 };
292 },292 },
293 } as unknown as RelayDatabase;293 } as unknown as RelayDb.RelayDb["Service"];
294294
295 return Effect.gen(function* () {295 return Effect.gen(function* () {
296 const attempts = yield* DeliveryAttempts.DeliveryAttempts;296 const attempts = yield* DeliveryAttempts.DeliveryAttempts;
...@@ -315,7 +315,7 @@ describe("DeliveryAttempts", () => {...@@ -315,7 +315,7 @@ describe("DeliveryAttempts", () => {
315 Effect.provide(315 Effect.provide(
316 DeliveryAttempts.layer.pipe(316 DeliveryAttempts.layer.pipe(
317 Layer.provide(NodeCryptoLayer.layer),317 Layer.provide(NodeCryptoLayer.layer),
318 Layer.provide(Layer.succeed(RelayDb, fakeDb)),318 Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)),
319 ),319 ),
320 ),320 ),
321 );321 );
infra/relay/src/agentActivity/DeliveryAttempts.ts+2-2
...@@ -7,7 +7,7 @@ import { and, eq, isNull } from "drizzle-orm";...@@ -7,7 +7,7 @@ import { and, eq, isNull } from "drizzle-orm";
7import * as Crypto from "effect/Crypto";7import * as Crypto from "effect/Crypto";
8import * as Schema from "effect/Schema";8import * as Schema from "effect/Schema";
99
10import { RelayDb } from "../db.ts";10import * as RelayDb from "../db.ts";
11import { relayDeliveryAttempts } from "../persistence/schema.ts";11import { relayDeliveryAttempts } from "../persistence/schema.ts";
1212
13export class DeliveryAttemptRecordPersistenceError extends Schema.TaggedErrorClass<DeliveryAttemptRecordPersistenceError>()(13export class DeliveryAttemptRecordPersistenceError extends Schema.TaggedErrorClass<DeliveryAttemptRecordPersistenceError>()(
...@@ -84,7 +84,7 @@ function insertValues(...@@ -84,7 +84,7 @@ function insertValues(
84}84}
8585
86const make = Effect.gen(function* () {86const make = Effect.gen(function* () {
87 const db = yield* RelayDb;87 const db = yield* RelayDb.RelayDb;
88 const crypto = yield* Crypto.Crypto;88 const crypto = yield* Crypto.Crypto;
8989
90 const isExpiredClaim = (claimedAt: string | null, now: DateTime.DateTime) => {90 const isExpiredClaim = (claimedAt: string | null, now: DateTime.DateTime) => {
infra/relay/src/agentActivity/Devices.test.ts+13-7
...@@ -5,7 +5,7 @@ import { PgDialect } from "drizzle-orm/pg-core";...@@ -5,7 +5,7 @@ import { PgDialect } from "drizzle-orm/pg-core";
5import * as Effect from "effect/Effect";5import * as Effect from "effect/Effect";
6import * as Layer from "effect/Layer";6import * as Layer from "effect/Layer";
77
8import { RelayDb, type RelayDatabase } from "../db.ts";8import * as RelayDb from "../db.ts";
9import { relayLiveActivities, relayMobileDevices } from "../persistence/schema.ts";9import { relayLiveActivities, relayMobileDevices } from "../persistence/schema.ts";
10import * as Devices from "./Devices.ts";10import * as Devices from "./Devices.ts";
1111
...@@ -71,7 +71,7 @@ describe("Devices", () => {...@@ -71,7 +71,7 @@ describe("Devices", () => {
71 },71 },
72 };72 };
73 },73 },
74 } as unknown as RelayDatabase;74 } as unknown as RelayDb.RelayDb["Service"];
7575
76 return Effect.gen(function* () {76 return Effect.gen(function* () {
77 const devices = yield* Devices.Devices;77 const devices = yield* Devices.Devices;
...@@ -110,7 +110,9 @@ describe("Devices", () => {...@@ -110,7 +110,9 @@ describe("Devices", () => {
110 pushToStartToken: "push-to-start-token",110 pushToStartToken: "push-to-start-token",
111 }),111 }),
112 ]);112 ]);
113 }).pipe(Effect.provide(Devices.layer.pipe(Layer.provide(Layer.succeed(RelayDb, fakeDb)))));113 }).pipe(
114 Effect.provide(Devices.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)))),
115 );
114 });116 });
115117
116 it.effect("unregisters APNs state only for the current user device", () => {118 it.effect("unregisters APNs state only for the current user device", () => {
...@@ -130,7 +132,7 @@ describe("Devices", () => {...@@ -130,7 +132,7 @@ describe("Devices", () => {
130 },132 },
131 };133 };
132 },134 },
133 } as unknown as RelayDatabase;135 } as unknown as RelayDb.RelayDb["Service"];
134136
135 return Effect.gen(function* () {137 return Effect.gen(function* () {
136 const devices = yield* Devices.Devices;138 const devices = yield* Devices.Devices;
...@@ -156,7 +158,9 @@ describe("Devices", () => {...@@ -156,7 +158,9 @@ describe("Devices", () => {
156 params: ["user-2", "device-1"],158 params: ["user-2", "device-1"],
157 },159 },
158 ]);160 ]);
159 }).pipe(Effect.provide(Devices.layer.pipe(Layer.provide(Layer.succeed(RelayDb, fakeDb)))));161 }).pipe(
162 Effect.provide(Devices.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)))),
163 );
160 });164 });
161165
162 it.effect("lists safe notification state without exposing APNs tokens", () => {166 it.effect("lists safe notification state without exposing APNs tokens", () => {
...@@ -184,7 +188,7 @@ describe("Devices", () => {...@@ -184,7 +188,7 @@ describe("Devices", () => {
184 };188 };
185 },189 },
186 }),190 }),
187 } as unknown as RelayDatabase;191 } as unknown as RelayDb.RelayDb["Service"];
188192
189 return Effect.gen(function* () {193 return Effect.gen(function* () {
190 const devices = yield* Devices.Devices;194 const devices = yield* Devices.Devices;
...@@ -215,6 +219,8 @@ describe("Devices", () => {...@@ -215,6 +219,8 @@ describe("Devices", () => {
215 updatedAt: "2026-06-01T00:00:00.000Z",219 updatedAt: "2026-06-01T00:00:00.000Z",
216 },220 },
217 ]);221 ]);
218 }).pipe(Effect.provide(Devices.layer.pipe(Layer.provide(Layer.succeed(RelayDb, fakeDb)))));222 }).pipe(
223 Effect.provide(Devices.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)))),
224 );
219 });225 });
220});226});
infra/relay/src/agentActivity/Devices.ts+2-2
...@@ -10,7 +10,7 @@ import * as Schema from "effect/Schema";...@@ -10,7 +10,7 @@ import * 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 * as RelayDb from "../db.ts";
14import { relayLiveActivities, relayMobileDevices } from "../persistence/schema.ts";14import { relayLiveActivities, relayMobileDevices } from "../persistence/schema.ts";
1515
16export class DeviceRegistrationPersistenceError extends Schema.TaggedErrorClass<DeviceRegistrationPersistenceError>()(16export class DeviceRegistrationPersistenceError extends Schema.TaggedErrorClass<DeviceRegistrationPersistenceError>()(
...@@ -59,7 +59,7 @@ export class Devices extends Context.Service<Devices, DevicesShape>()(...@@ -59,7 +59,7 @@ export class Devices extends Context.Service<Devices, DevicesShape>()(
59) {}59) {}
6060
61const make = Effect.gen(function* () {61const make = Effect.gen(function* () {
62 const db = yield* RelayDb;62 const db = yield* RelayDb.RelayDb;
6363
64 return Devices.of({64 return Devices.of({
65 register: Effect.fn("relay.devices.register")(65 register: Effect.fn("relay.devices.register")(
infra/relay/src/agentActivity/LiveActivities.test.ts+9-5
...@@ -8,7 +8,7 @@ import { PgDialect } from "drizzle-orm/pg-core";...@@ -8,7 +8,7 @@ import { PgDialect } from "drizzle-orm/pg-core";
8import * as Effect from "effect/Effect";8import * as Effect from "effect/Effect";
9import * as Layer from "effect/Layer";9import * as Layer from "effect/Layer";
1010
11import { RelayDb, type RelayDatabase } from "../db.ts";11import * as RelayDb from "../db.ts";
12import { relayLiveActivities } from "../persistence/schema.ts";12import { relayLiveActivities } from "../persistence/schema.ts";
13import * as LiveActivities from "./LiveActivities.ts";13import * as LiveActivities from "./LiveActivities.ts";
1414
...@@ -88,7 +88,7 @@ describe("LiveActivities", () => {...@@ -88,7 +88,7 @@ describe("LiveActivities", () => {
88 },88 },
89 };89 };
90 },90 },
91 } as unknown as RelayDatabase;91 } as unknown as RelayDb.RelayDb["Service"];
9292
93 return Effect.gen(function* () {93 return Effect.gen(function* () {
94 const liveActivities = yield* LiveActivities.LiveActivities;94 const liveActivities = yield* LiveActivities.LiveActivities;
...@@ -138,7 +138,9 @@ describe("LiveActivities", () => {...@@ -138,7 +138,9 @@ describe("LiveActivities", () => {
138 }),138 }),
139 );139 );
140 }).pipe(140 }).pipe(
141 Effect.provide(LiveActivities.layer.pipe(Layer.provide(Layer.succeed(RelayDb, fakeDb)))),141 Effect.provide(
142 LiveActivities.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb))),
143 ),
142 );144 );
143 },145 },
144 );146 );
...@@ -164,7 +166,7 @@ describe("LiveActivities", () => {...@@ -164,7 +166,7 @@ describe("LiveActivities", () => {
164 },166 },
165 };167 };
166 },168 },
167 } as unknown as RelayDatabase;169 } as unknown as RelayDb.RelayDb["Service"];
168170
169 return Effect.gen(function* () {171 return Effect.gen(function* () {
170 const liveActivities = yield* LiveActivities.LiveActivities;172 const liveActivities = yield* LiveActivities.LiveActivities;
...@@ -190,7 +192,9 @@ describe("LiveActivities", () => {...@@ -190,7 +192,9 @@ describe("LiveActivities", () => {
190 }),192 }),
191 );193 );
192 }).pipe(194 }).pipe(
193 Effect.provide(LiveActivities.layer.pipe(Layer.provide(Layer.succeed(RelayDb, fakeDb)))),195 Effect.provide(
196 LiveActivities.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb))),
197 ),
194 );198 );
195 });199 });
196});200});
infra/relay/src/agentActivity/LiveActivities.ts+2-2
...@@ -12,7 +12,7 @@ import * as Layer from "effect/Layer";...@@ -12,7 +12,7 @@ import * as Layer from "effect/Layer";
12import * as Schema from "effect/Schema";12import * as Schema from "effect/Schema";
13import { and, eq, sql } from "drizzle-orm";13import { and, eq, sql } from "drizzle-orm";
1414
15import { RelayDb } from "../db.ts";15import * as RelayDb from "../db.ts";
16import { relayLiveActivities, relayMobileDevices } from "../persistence/schema.ts";16import { relayLiveActivities, relayMobileDevices } from "../persistence/schema.ts";
1717
18export class LiveActivityRegistrationPersistenceError extends Schema.TaggedErrorClass<LiveActivityRegistrationPersistenceError>()(18export class LiveActivityRegistrationPersistenceError extends Schema.TaggedErrorClass<LiveActivityRegistrationPersistenceError>()(
...@@ -108,7 +108,7 @@ const encodeRelayAgentActivityAggregateStateJson = Schema.encodeEffect(...@@ -108,7 +108,7 @@ const encodeRelayAgentActivityAggregateStateJson = Schema.encodeEffect(
108);108);
109109
110const make = Effect.gen(function* () {110const make = Effect.gen(function* () {
111 const db = yield* RelayDb;111 const db = yield* RelayDb.RelayDb;
112112
113 return LiveActivities.of({113 return LiveActivities.of({
114 register: Effect.fn("relay.live_activities.register")(114 register: Effect.fn("relay.live_activities.register")(
infra/relay/src/agentActivity/MobileRegistrations.test.ts+3-3
...@@ -86,8 +86,8 @@ function makeAgentActivityRows(...@@ -86,8 +86,8 @@ function makeAgentActivityRows(
86}86}
8787
88function makeEnvironmentLinks(88function makeEnvironmentLinks(
89 overrides: Partial<EnvironmentLinks.EnvironmentLinksShape> = {},89 overrides: Partial<EnvironmentLinks.EnvironmentLinks["Service"]> = {},
90): EnvironmentLinks.EnvironmentLinksShape {90): EnvironmentLinks.EnvironmentLinks["Service"] {
91 return {91 return {
92 upsert: () => Effect.void,92 upsert: () => Effect.void,
93 listUsersForEnvironment: () => Effect.succeed(["dev:julius"]),93 listUsersForEnvironment: () => Effect.succeed(["dev:julius"]),
...@@ -153,7 +153,7 @@ function makeRegistrationReplayLayer(input: {...@@ -153,7 +153,7 @@ function makeRegistrationReplayLayer(input: {
153 Layer.succeed(EnvironmentLinks.EnvironmentLinks, makeEnvironmentLinks()),153 Layer.succeed(EnvironmentLinks.EnvironmentLinks, makeEnvironmentLinks()),
154 Layer.succeed(LiveActivities.LiveActivities, input.liveActivities),154 Layer.succeed(LiveActivities.LiveActivities, input.liveActivities),
155 Layer.succeed(DeliveryAttempts.DeliveryAttempts, makeDeliveryAttempts()),155 Layer.succeed(DeliveryAttempts.DeliveryAttempts, makeDeliveryAttempts()),
156 Layer.succeed(RelayConfiguration.RelayConfiguration, config),156 RelayConfiguration.layer(config),
157 Layer.succeed(ApnsDeliveryQueue.ApnsDeliveryQueueSender, {157 Layer.succeed(ApnsDeliveryQueue.ApnsDeliveryQueueSender, {
158 send: (body) =>158 send: (body) =>
159 Effect.sync(() => {159 Effect.sync(() => {
infra/relay/src/auth/DpopProofs.test.ts+9-5
...@@ -4,7 +4,7 @@ import * as Effect from "effect/Effect";...@@ -4,7 +4,7 @@ import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";4import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";5import * as Option from "effect/Option";
66
7import { RelayDb, type RelayDatabase } from "../db.ts";7import * as RelayDb from "../db.ts";
8import { relayDpopProofs } from "../persistence/schema.ts";8import { relayDpopProofs } from "../persistence/schema.ts";
9import * as DpopProofs from "./DpopProofs.ts";9import * as DpopProofs from "./DpopProofs.ts";
1010
...@@ -41,7 +41,7 @@ describe("DpopProofReplay", () => {...@@ -41,7 +41,7 @@ describe("DpopProofReplay", () => {
41 },41 },
42 };42 };
43 },43 },
44 } as unknown as RelayDatabase;44 } as unknown as RelayDb.RelayDb["Service"];
4545
46 return Effect.gen(function* () {46 return Effect.gen(function* () {
47 const replay = yield* DpopProofs.DpopProofReplay;47 const replay = yield* DpopProofs.DpopProofReplay;
...@@ -67,7 +67,9 @@ describe("DpopProofReplay", () => {...@@ -67,7 +67,9 @@ describe("DpopProofReplay", () => {
67 expiresAt: "2026-05-25T12:00:00.000Z",67 expiresAt: "2026-05-25T12:00:00.000Z",
68 },68 },
69 ]);69 ]);
70 }).pipe(Effect.provide(DpopProofs.layer.pipe(Layer.provide(Layer.succeed(RelayDb, fakeDb)))));70 }).pipe(
71 Effect.provide(DpopProofs.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)))),
72 );
71 });73 });
7274
73 it.effect("prunes expired proof rows from the maintenance path", () => {75 it.effect("prunes expired proof rows from the maintenance path", () => {
...@@ -84,12 +86,14 @@ describe("DpopProofReplay", () => {...@@ -84,12 +86,14 @@ describe("DpopProofReplay", () => {
84 },86 },
85 };87 };
86 },88 },
87 } as unknown as RelayDatabase;89 } as unknown as RelayDb.RelayDb["Service"];
8890
89 return Effect.gen(function* () {91 return Effect.gen(function* () {
90 const replay = yield* DpopProofs.DpopProofReplay;92 const replay = yield* DpopProofs.DpopProofReplay;
91 yield* replay.pruneExpired;93 yield* replay.pruneExpired;
92 expect(calls).toEqual(["delete", "delete.where"]);94 expect(calls).toEqual(["delete", "delete.where"]);
93 }).pipe(Effect.provide(DpopProofs.layer.pipe(Layer.provide(Layer.succeed(RelayDb, fakeDb)))));95 }).pipe(
96 Effect.provide(DpopProofs.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)))),
97 );
94 });98 });
95});99});
infra/relay/src/auth/DpopProofs.ts+25-28
...@@ -7,7 +7,7 @@ import * as HttpApiError from "effect/unstable/httpapi/HttpApiError";...@@ -7,7 +7,7 @@ import * as HttpApiError from "effect/unstable/httpapi/HttpApiError";
7import { lt } from "drizzle-orm";7import { lt } from "drizzle-orm";
88
9import { verifyDpopProof } from "@t3tools/shared/dpop";9import { verifyDpopProof } from "@t3tools/shared/dpop";
10import { RelayDb } from "../db.ts";10import * as RelayDb from "../db.ts";
11import { relayDpopProofs } from "../persistence/schema.ts";11import { relayDpopProofs } from "../persistence/schema.ts";
1212
13export class DpopProofReplayPersistenceError extends Schema.TaggedErrorClass<DpopProofReplayPersistenceError>()(13export class DpopProofReplayPersistenceError extends Schema.TaggedErrorClass<DpopProofReplayPersistenceError>()(
...@@ -21,34 +21,31 @@ export class DpopProofReplayPersistenceError extends Schema.TaggedErrorClass<Dpo...@@ -21,34 +21,31 @@ export class DpopProofReplayPersistenceError extends Schema.TaggedErrorClass<Dpo
21 }21 }
22}22}
2323
24export interface DpopProofReplayShape {24export class DpopProofReplay extends Context.Service<
25 readonly verifyAndConsume: (input: {25 DpopProofReplay,
26 readonly proof: string | undefined;26 {
27 readonly method: string;27 readonly verifyAndConsume: (input: {
28 readonly url: string;28 readonly proof: string | undefined;
29 readonly expectedThumbprint?: string;29 readonly method: string;
30 readonly expectedAccessToken?: string;30 readonly url: string;
31 readonly now: DateTime.DateTime;31 readonly expectedThumbprint?: string;
32 }) => Effect.Effect<string, HttpApiError.Unauthorized | DpopProofReplayPersistenceError>;32 readonly expectedAccessToken?: string;
3333 readonly now: DateTime.DateTime;
34 readonly consume: (input: {34 }) => Effect.Effect<string, HttpApiError.Unauthorized | DpopProofReplayPersistenceError>;
35 readonly thumbprint: string;35 readonly consume: (input: {
36 readonly jti: string;36 readonly thumbprint: string;
37 readonly iat: number;37 readonly jti: string;
38 readonly expiresAt: DateTime.DateTime;38 readonly iat: number;
39 }) => Effect.Effect<boolean, DpopProofReplayPersistenceError>;39 readonly expiresAt: DateTime.DateTime;
4040 }) => Effect.Effect<boolean, DpopProofReplayPersistenceError>;
41 readonly pruneExpired: Effect.Effect<void, DpopProofReplayPersistenceError>;41 readonly pruneExpired: Effect.Effect<void, DpopProofReplayPersistenceError>;
42}42 }
4343>()("t3code-relay/auth/DpopProofs/DpopProofReplay") {}
44export class DpopProofReplay extends Context.Service<DpopProofReplay, DpopProofReplayShape>()(
45 "t3code-relay/auth/DpopProofs/DpopProofReplay",
46) {}
4744
48const make = Effect.gen(function* () {45const make = Effect.gen(function* () {
49 const db = yield* RelayDb;46 const db = yield* RelayDb.RelayDb;
5047
51 const consume: DpopProofReplayShape["consume"] = Effect.fn("relay.dpop_proofs.consume")(48 const consume: DpopProofReplay["Service"]["consume"] = Effect.fn("relay.dpop_proofs.consume")(
52 function* (input) {49 function* (input) {
53 const createdAt = DateTime.formatIso(yield* DateTime.now);50 const createdAt = DateTime.formatIso(yield* DateTime.now);
54 const inserted = yield* db51 const inserted = yield* db
...@@ -67,7 +64,7 @@ const make = Effect.gen(function* () {...@@ -67,7 +64,7 @@ const make = Effect.gen(function* () {
67 Effect.mapError((cause) => new DpopProofReplayPersistenceError({ cause })),64 Effect.mapError((cause) => new DpopProofReplayPersistenceError({ cause })),
68 );65 );
6966
70 const verifyAndConsume: DpopProofReplayShape["verifyAndConsume"] = Effect.fn(67 const verifyAndConsume: DpopProofReplay["Service"]["verifyAndConsume"] = Effect.fn(
71 "relay.dpop_proofs.verify_and_consume",68 "relay.dpop_proofs.verify_and_consume",
72 )(function* (input) {69 )(function* (input) {
73 yield* Effect.annotateCurrentSpan({70 yield* Effect.annotateCurrentSpan({
...@@ -114,7 +111,7 @@ const make = Effect.gen(function* () {...@@ -114,7 +111,7 @@ const make = Effect.gen(function* () {
114 return result.thumbprint;111 return result.thumbprint;
115 });112 });
116113
117 const pruneExpired: DpopProofReplayShape["pruneExpired"] = Effect.gen(function* () {114 const pruneExpired: DpopProofReplay["Service"]["pruneExpired"] = Effect.gen(function* () {
118 const now = DateTime.formatIso(yield* DateTime.now);115 const now = DateTime.formatIso(yield* DateTime.now);
119 yield* Effect.annotateCurrentSpan({ "relay.dpop_prune.before": now });116 yield* Effect.annotateCurrentSpan({ "relay.dpop_prune.before": now });
120 yield* db.delete(relayDpopProofs).where(lt(relayDpopProofs.expiresAt, now));117 yield* db.delete(relayDpopProofs).where(lt(relayDpopProofs.expiresAt, now));
infra/relay/src/auth/DpopProofs.verifyAndConsume.test.ts+3-3
...@@ -10,7 +10,7 @@ import * as DateTime from "effect/DateTime";...@@ -10,7 +10,7 @@ import * as DateTime from "effect/DateTime";
10import * as Effect from "effect/Effect";10import * as Effect from "effect/Effect";
11import * as Layer from "effect/Layer";11import * as Layer from "effect/Layer";
1212
13import { RelayDb, type RelayDatabase } from "../db.ts";13import * as RelayDb from "../db.ts";
14import { relayDpopProofs } from "../persistence/schema.ts";14import { relayDpopProofs } from "../persistence/schema.ts";
15import * as DpopProofs from "./DpopProofs.ts";15import * as DpopProofs from "./DpopProofs.ts";
1616
...@@ -78,8 +78,8 @@ function layer(...@@ -78,8 +78,8 @@ function layer(
78 }),78 }),
79 };79 };
80 },80 },
81 } as unknown as RelayDatabase;81 } as unknown as RelayDb.RelayDb["Service"];
82 return DpopProofs.layer.pipe(Layer.provide(Layer.succeed(RelayDb, fakeDb)));82 return DpopProofs.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)));
83}83}
8484
85function consumeEachProofOnce() {85function consumeEachProofOnce() {
infra/relay/src/auth/RelayTokens.test.ts+1-3
...@@ -33,9 +33,7 @@ const config = RelayConfiguration.RelayConfiguration.of({...@@ -33,9 +33,7 @@ const config = RelayConfiguration.RelayConfiguration.of({
33 managedEndpointNamespace: undefined,33 managedEndpointNamespace: undefined,
34});34});
3535
36const layer = RelayTokens.layer.pipe(36const layer = RelayTokens.layer.pipe(Layer.provide(RelayConfiguration.layer(config)));
37 Layer.provide(Layer.succeed(RelayConfiguration.RelayConfiguration, config)),
38);
3937
40describe("RelayTokens", () => {38describe("RelayTokens", () => {
41 it.effect("issues a user-bound environment link challenge", () =>39 it.effect("issues a user-bound environment link challenge", () =>
infra/relay/src/auth/RelayTokens.ts+36-37
...@@ -93,45 +93,44 @@ function resolveDpopAccessTokenScopes(input: {...@@ -93,45 +93,44 @@ function resolveDpopAccessTokenScopes(input: {
93 });93 });
94}94}
9595
96export interface RelayTokensShape {96export class RelayTokens extends Context.Service<
97 readonly resolveDpopAccessTokenScopes: typeof resolveDpopAccessTokenScopes;97 RelayTokens,
98 readonly issueLinkChallenge: (input: {98 {
99 readonly userId: string;99 readonly resolveDpopAccessTokenScopes: typeof resolveDpopAccessTokenScopes;
100 readonly request: RelayEnvironmentLinkChallengeRequest;100 readonly issueLinkChallenge: (input: {
101 readonly jti: string;101 readonly userId: string;
102 readonly issuedAtEpochSeconds: number;102 readonly request: RelayEnvironmentLinkChallengeRequest;
103 readonly expiresAtEpochSeconds: number;103 readonly jti: string;
104 }) => Effect.Effect<string, RelayJwtError>;104 readonly issuedAtEpochSeconds: number;
105 readonly verifyLinkChallenge: (input: {105 readonly expiresAtEpochSeconds: number;
106 readonly token: string;106 }) => Effect.Effect<string, RelayJwtError>;
107 readonly userId: string;107 readonly verifyLinkChallenge: (input: {
108 readonly request: RelayEnvironmentLinkChallengeRequest;108 readonly token: string;
109 readonly nowEpochSeconds: number;109 readonly userId: string;
110 }) => Effect.Effect<LinkChallengeClaims | null>;110 readonly request: RelayEnvironmentLinkChallengeRequest;
111 readonly issueDpopAccessToken: (input: {111 readonly nowEpochSeconds: number;
112 readonly userId: string;112 }) => Effect.Effect<LinkChallengeClaims | null>;
113 readonly proofKeyThumbprint: string;113 readonly issueDpopAccessToken: (input: {
114 readonly jti: string;114 readonly userId: string;
115 readonly issuedAtEpochSeconds: number;115 readonly proofKeyThumbprint: string;
116 readonly expiresAtEpochSeconds: number;116 readonly jti: string;
117 readonly clientId: RelayPublicClientId;117 readonly issuedAtEpochSeconds: number;
118 readonly scopes: ReadonlyArray<RelayDpopAccessTokenScope>;118 readonly expiresAtEpochSeconds: number;
119 }) => Effect.Effect<string, RelayJwtError>;119 readonly clientId: RelayPublicClientId;
120 readonly verifyDpopAccessToken: (input: {120 readonly scopes: ReadonlyArray<RelayDpopAccessTokenScope>;
121 readonly token: string;121 }) => Effect.Effect<string, RelayJwtError>;
122 readonly nowEpochSeconds: number;122 readonly verifyDpopAccessToken: (input: {
123 }) => Effect.Effect<RelayDpopAccessTokenClaims | null>;123 readonly token: string;
124}124 readonly nowEpochSeconds: number;
125125 }) => Effect.Effect<RelayDpopAccessTokenClaims | null>;
126export class RelayTokens extends Context.Service<RelayTokens, RelayTokensShape>()(126 }
127 "t3code-relay/auth/RelayTokens",127>()("t3code-relay/auth/RelayTokens") {}
128) {}
129128
130const make = Effect.gen(function* () {129const make = Effect.gen(function* () {
131 const config = yield* RelayConfiguration.RelayConfiguration;130 const config = yield* RelayConfiguration.RelayConfiguration;
132 const issuer = normalizeRelayIssuer(config.relayIssuer);131 const issuer = normalizeRelayIssuer(config.relayIssuer);
133132
134 const issueLinkChallenge: RelayTokensShape["issueLinkChallenge"] = Effect.fn(133 const issueLinkChallenge: RelayTokens["Service"]["issueLinkChallenge"] = Effect.fn(
135 "relay.tokens.issue_link_challenge",134 "relay.tokens.issue_link_challenge",
136 )(function* (input) {135 )(function* (input) {
137 return yield* signRelayJwt({136 return yield* signRelayJwt({
...@@ -150,7 +149,7 @@ const make = Effect.gen(function* () {...@@ -150,7 +149,7 @@ const make = Effect.gen(function* () {
150 });149 });
151 });150 });
152151
153 const verifyLinkChallenge: RelayTokensShape["verifyLinkChallenge"] = Effect.fn(152 const verifyLinkChallenge: RelayTokens["Service"]["verifyLinkChallenge"] = Effect.fn(
154 "relay.tokens.verify_link_challenge",153 "relay.tokens.verify_link_challenge",
155 )((input) =>154 )((input) =>
156 verifyRelayJwt({155 verifyRelayJwt({
...@@ -177,7 +176,7 @@ const make = Effect.gen(function* () {...@@ -177,7 +176,7 @@ const make = Effect.gen(function* () {
177 ),176 ),
178 );177 );
179178
180 const issueDpopAccessToken: RelayTokensShape["issueDpopAccessToken"] = Effect.fn(179 const issueDpopAccessToken: RelayTokens["Service"]["issueDpopAccessToken"] = Effect.fn(
181 "relay.tokens.issue_dpop_access_token",180 "relay.tokens.issue_dpop_access_token",
182 )(function* (input) {181 )(function* (input) {
183 return yield* signRelayJwt({182 return yield* signRelayJwt({
...@@ -197,7 +196,7 @@ const make = Effect.gen(function* () {...@@ -197,7 +196,7 @@ const make = Effect.gen(function* () {
197 });196 });
198 });197 });
199198
200 const verifyDpopAccessToken: RelayTokensShape["verifyDpopAccessToken"] = Effect.fn(199 const verifyDpopAccessToken: RelayTokens["Service"]["verifyDpopAccessToken"] = Effect.fn(
201 "relay.tokens.verify_dpop_access_token",200 "relay.tokens.verify_dpop_access_token",
202 )((input) =>201 )((input) =>
203 verifyRelayJwt({202 verifyRelayJwt({
infra/relay/src/db.ts+6-5
...@@ -10,11 +10,12 @@ import * as Effect from "effect/Effect";...@@ -10,11 +10,12 @@ import * as Effect from "effect/Effect";
1010
11import { relayDatabaseMode } from "./dbConfig.ts";11import { relayDatabaseMode } from "./dbConfig.ts";
1212
13export interface RelayDatabase extends EffectPgDatabase {13export class RelayDb extends Context.Service<
14 readonly $client: PgClient;14 RelayDb,
15}15 EffectPgDatabase & {
1616 readonly $client: PgClient;
17export class RelayDb extends Context.Service<RelayDb, RelayDatabase>()("t3code-relay/db/RelayDb") {}17 }
18>()("t3code-relay/db/RelayDb") {}
1819
19export const PlanetscaleDatabase = Effect.gen(function* () {20export const PlanetscaleDatabase = Effect.gen(function* () {
20 const { stage } = yield* Alchemy.Stack;21 const { stage } = yield* Alchemy.Stack;
infra/relay/src/environments/EnvironmentConnector.test.ts+5-5
...@@ -161,8 +161,8 @@ function connectorTestLayer(...@@ -161,8 +161,8 @@ function connectorTestLayer(
161 request: HttpClientRequest.HttpClientRequest,161 request: HttpClientRequest.HttpClientRequest,
162 ) => Effect.Effect<HttpClientResponse.HttpClientResponse>,162 ) => Effect.Effect<HttpClientResponse.HttpClientResponse>,
163 options?: {163 options?: {
164 readonly links?: EnvironmentLinks.EnvironmentLinksShape;164 readonly links?: EnvironmentLinks.EnvironmentLinks["Service"];
165 readonly allocations?: ManagedEndpointAllocations.ManagedEndpointAllocationsShape;165 readonly allocations?: ManagedEndpointAllocations.ManagedEndpointAllocations["Service"];
166 },166 },
167) {167) {
168 return EnvironmentConnector.layer.pipe(168 return EnvironmentConnector.layer.pipe(
...@@ -174,7 +174,7 @@ function connectorTestLayer(...@@ -174,7 +174,7 @@ function connectorTestLayer(
174 options?.allocations ?? makeAllocations(),174 options?.allocations ?? makeAllocations(),
175 ),175 ),
176 ),176 ),
177 Layer.provide(Layer.succeed(RelayConfiguration.RelayConfiguration, settings)),177 Layer.provide(RelayConfiguration.layer(settings)),
178 Layer.provide(Layer.succeed(HttpClient.HttpClient, HttpClient.make(execute))),178 Layer.provide(Layer.succeed(HttpClient.HttpClient, HttpClient.make(execute))),
179 );179 );
180}180}
...@@ -189,7 +189,7 @@ function makeAllocations(...@@ -189,7 +189,7 @@ function makeAllocations(
189 dnsRecordId: "dns-record-id",189 dnsRecordId: "dns-record-id",
190 readyAt: "2026-05-25T00:00:00.000Z",190 readyAt: "2026-05-25T00:00:00.000Z",
191 },191 },
192): ManagedEndpointAllocations.ManagedEndpointAllocationsShape {192): ManagedEndpointAllocations.ManagedEndpointAllocations["Service"] {
193 return {193 return {
194 get: () => Effect.succeed(allocation),194 get: () => Effect.succeed(allocation),
195 reserve: () => Effect.die("unused"),195 reserve: () => Effect.die("unused"),
...@@ -202,7 +202,7 @@ function makeAllocations(...@@ -202,7 +202,7 @@ function makeAllocations(
202202
203function makeLinks(203function makeLinks(
204 overrides: Partial<EnvironmentLinks.RelayLinkedEnvironmentRecord> = {},204 overrides: Partial<EnvironmentLinks.RelayLinkedEnvironmentRecord> = {},
205): EnvironmentLinks.EnvironmentLinksShape {205): EnvironmentLinks.EnvironmentLinks["Service"] {
206 return {206 return {
207 upsert: () => Effect.void,207 upsert: () => Effect.void,
208 listUsersForEnvironment: () => Effect.succeed([]),208 listUsersForEnvironment: () => Effect.succeed([]),
infra/relay/src/environments/EnvironmentConnector.ts+15-15
...@@ -35,7 +35,9 @@ import * as Layer from "effect/Layer";...@@ -35,7 +35,9 @@ import * as Layer from "effect/Layer";
35import * as Option from "effect/Option";35import * as Option from "effect/Option";
36import * as Redacted from "effect/Redacted";36import * as Redacted from "effect/Redacted";
37import * as Schema from "effect/Schema";37import * as Schema from "effect/Schema";
38import { FetchHttpClient, HttpClient, HttpClientError } from "effect/unstable/http";38import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
39import * as HttpClient from "effect/unstable/http/HttpClient";
40import * as HttpClientError from "effect/unstable/http/HttpClientError";
3941
40import * as EnvironmentLinks from "./EnvironmentLinks.ts";42import * as EnvironmentLinks from "./EnvironmentLinks.ts";
41import * as ManagedEndpointAllocations from "./ManagedEndpointAllocations.ts";43import * as ManagedEndpointAllocations from "./ManagedEndpointAllocations.ts";
...@@ -139,22 +141,20 @@ export type EnvironmentConnectorError =...@@ -139,22 +141,20 @@ export type EnvironmentConnectorError =
139export const ENVIRONMENT_MINT_REQUEST_TIMEOUT_MS = 10_000;141export const ENVIRONMENT_MINT_REQUEST_TIMEOUT_MS = 10_000;
140const ENVIRONMENT_HEALTH_CLOCK_SKEW_MILLIS = 60 * 1_000;142const ENVIRONMENT_HEALTH_CLOCK_SKEW_MILLIS = 60 * 1_000;
141143
142export interface EnvironmentConnectorShape {
143 readonly connect: (input: {
144 readonly userId: string;
145 readonly environmentId: string;
146 readonly clientProofKeyThumbprint: string;
147 readonly deviceId?: string;
148 }) => Effect.Effect<RelayEnvironmentConnectResponse, EnvironmentConnectorError>;
149 readonly status: (input: {
150 readonly userId: string;
151 readonly environmentId: string;
152 }) => Effect.Effect<RelayEnvironmentStatusResponse, EnvironmentConnectorError>;
153}
154
155export class EnvironmentConnector extends Context.Service<144export class EnvironmentConnector extends Context.Service<
156 EnvironmentConnector,145 EnvironmentConnector,
157 EnvironmentConnectorShape146 {
147 readonly connect: (input: {
148 readonly userId: string;
149 readonly environmentId: string;
150 readonly clientProofKeyThumbprint: string;
151 readonly deviceId?: string;
152 }) => Effect.Effect<RelayEnvironmentConnectResponse, EnvironmentConnectorError>;
153 readonly status: (input: {
154 readonly userId: string;
155 readonly environmentId: string;
156 }) => Effect.Effect<RelayEnvironmentStatusResponse, EnvironmentConnectorError>;
157 }
158>()("t3code-relay/environments/EnvironmentConnector") {}158>()("t3code-relay/environments/EnvironmentConnector") {}
159159
160const decodeMintResponseProof = Schema.decodeUnknownEffect(160const decodeMintResponseProof = Schema.decodeUnknownEffect(
infra/relay/src/environments/EnvironmentCredentials.test.ts+5-5
...@@ -4,7 +4,7 @@ import { PgDialect, QueryBuilder } from "drizzle-orm/pg-core";...@@ -4,7 +4,7 @@ import { PgDialect, QueryBuilder } from "drizzle-orm/pg-core";
4import * as Effect from "effect/Effect";4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";5import * as Layer from "effect/Layer";
66
7import { RelayDb, type RelayDatabase } from "../db.ts";7import * as RelayDb from "../db.ts";
8import { relayEnvironmentCredentials } from "../persistence/schema.ts";8import { relayEnvironmentCredentials } from "../persistence/schema.ts";
9import * as EnvironmentCredentials from "./EnvironmentCredentials.ts";9import * as EnvironmentCredentials from "./EnvironmentCredentials.ts";
1010
...@@ -47,7 +47,7 @@ describe("EnvironmentCredentials", () => {...@@ -47,7 +47,7 @@ describe("EnvironmentCredentials", () => {
47 }),47 }),
48 };48 };
49 },49 },
50 } as unknown as RelayDatabase;50 } as unknown as RelayDb.RelayDb["Service"];
5151
52 return Effect.gen(function* () {52 return Effect.gen(function* () {
53 const credentials = yield* EnvironmentCredentials.EnvironmentCredentials;53 const credentials = yield* EnvironmentCredentials.EnvironmentCredentials;
...@@ -87,7 +87,7 @@ describe("EnvironmentCredentials", () => {...@@ -87,7 +87,7 @@ describe("EnvironmentCredentials", () => {
87 Effect.provide(87 Effect.provide(
88 EnvironmentCredentials.layer.pipe(88 EnvironmentCredentials.layer.pipe(
89 Layer.provide(NodeCryptoLayer.layer),89 Layer.provide(NodeCryptoLayer.layer),
90 Layer.provide(Layer.succeed(RelayDb, fakeDb)),90 Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)),
91 ),91 ),
92 ),92 ),
93 );93 );
...@@ -118,7 +118,7 @@ describe("EnvironmentCredentials", () => {...@@ -118,7 +118,7 @@ describe("EnvironmentCredentials", () => {
118 },118 },
119 };119 };
120 },120 },
121 } as unknown as RelayDatabase;121 } as unknown as RelayDb.RelayDb["Service"];
122122
123 return Effect.gen(function* () {123 return Effect.gen(function* () {
124 const credentials = yield* EnvironmentCredentials.EnvironmentCredentials;124 const credentials = yield* EnvironmentCredentials.EnvironmentCredentials;
...@@ -150,7 +150,7 @@ describe("EnvironmentCredentials", () => {...@@ -150,7 +150,7 @@ describe("EnvironmentCredentials", () => {
150 Effect.provide(150 Effect.provide(
151 EnvironmentCredentials.layer.pipe(151 EnvironmentCredentials.layer.pipe(
152 Layer.provide(NodeCryptoLayer.layer),152 Layer.provide(NodeCryptoLayer.layer),
153 Layer.provide(Layer.succeed(RelayDb, fakeDb)),153 Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)),
154 ),154 ),
155 ),155 ),
156 );156 );
infra/relay/src/environments/EnvironmentCredentials.ts+18-20
...@@ -8,7 +8,7 @@ import * as Option from "effect/Option";...@@ -8,7 +8,7 @@ import * as Option from "effect/Option";
8import * as Schema from "effect/Schema";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 * as RelayDb from "../db.ts";
12import { relayEnvironmentCredentials, relayEnvironmentLinks } from "../persistence/schema.ts";12import { relayEnvironmentCredentials, relayEnvironmentLinks } from "../persistence/schema.ts";
1313
14export class EnvironmentCredentialCreatePersistenceError extends Schema.TaggedErrorClass<EnvironmentCredentialCreatePersistenceError>()(14export class EnvironmentCredentialCreatePersistenceError extends Schema.TaggedErrorClass<EnvironmentCredentialCreatePersistenceError>()(
...@@ -44,30 +44,28 @@ export interface EnvironmentCredentialPrincipal {...@@ -44,30 +44,28 @@ export interface EnvironmentCredentialPrincipal {
44 readonly environmentPublicKey: string;44 readonly environmentPublicKey: string;
45}45}
4646
47export interface EnvironmentCredentialsShape {
48 readonly create: (input: {
49 readonly environmentId: string;
50 readonly environmentPublicKey: string;
51 }) => Effect.Effect<string, EnvironmentCredentialCreatePersistenceError>;
52 readonly authenticate: (
53 token: string,
54 ) => Effect.Effect<
55 Option.Option<EnvironmentCredentialPrincipal>,
56 EnvironmentCredentialAuthenticatePersistenceError
57 >;
58 readonly revokeForEnvironmentPublicKey: (input: {
59 readonly environmentId: string;
60 readonly environmentPublicKey: string;
61 }) => Effect.Effect<boolean, EnvironmentCredentialRevokePersistenceError>;
62}
63
64export class EnvironmentCredentials extends Context.Service<47export class EnvironmentCredentials extends Context.Service<
65 EnvironmentCredentials,48 EnvironmentCredentials,
66 EnvironmentCredentialsShape49 {
50 readonly create: (input: {
51 readonly environmentId: string;
52 readonly environmentPublicKey: string;
53 }) => Effect.Effect<string, EnvironmentCredentialCreatePersistenceError>;
54 readonly authenticate: (
55 token: string,
56 ) => Effect.Effect<
57 Option.Option<EnvironmentCredentialPrincipal>,
58 EnvironmentCredentialAuthenticatePersistenceError
59 >;
60 readonly revokeForEnvironmentPublicKey: (input: {
61 readonly environmentId: string;
62 readonly environmentPublicKey: string;
63 }) => Effect.Effect<boolean, EnvironmentCredentialRevokePersistenceError>;
64 }
67>()("t3code-relay/environments/EnvironmentCredentials") {}65>()("t3code-relay/environments/EnvironmentCredentials") {}
6866
69const make = Effect.gen(function* () {67const make = Effect.gen(function* () {
70 const db = yield* RelayDb;68 const db = yield* RelayDb.RelayDb;
71 const crypto = yield* Crypto.Crypto;69 const crypto = yield* Crypto.Crypto;
72 const hashToken = (token: string) =>70 const hashToken = (token: string) =>
73 crypto71 crypto
infra/relay/src/environments/EnvironmentLinker.test.ts+3-3
...@@ -105,14 +105,14 @@ const makeRequest = Effect.gen(function* () {...@@ -105,14 +105,14 @@ const makeRequest = Effect.gen(function* () {
105});105});
106106
107function testLayer(input?: {107function testLayer(input?: {
108 readonly upsert?: EnvironmentLinks.EnvironmentLinksShape["upsert"];108 readonly upsert?: EnvironmentLinks.EnvironmentLinks["Service"]["upsert"];
109 readonly consume?: DpopProofs.DpopProofReplayShape["consume"];109 readonly consume?: DpopProofs.DpopProofReplay["Service"]["consume"];
110}) {110}) {
111 return EnvironmentLinker.layer.pipe(111 return EnvironmentLinker.layer.pipe(
112 Layer.provideMerge(RelayTokens.layer),112 Layer.provideMerge(RelayTokens.layer),
113 Layer.provide(113 Layer.provide(
114 Layer.mergeAll(114 Layer.mergeAll(
115 Layer.succeed(RelayConfiguration.RelayConfiguration, config),115 RelayConfiguration.layer(config),
116 Layer.succeed(DpopProofs.DpopProofReplay, {116 Layer.succeed(DpopProofs.DpopProofReplay, {
117 verifyAndConsume: () => Effect.die("unexpected DPoP proof verification"),117 verifyAndConsume: () => Effect.die("unexpected DPoP proof verification"),
118 consume: input?.consume ?? (() => Effect.succeed(true)),118 consume: input?.consume ?? (() => Effect.succeed(true)),
infra/relay/src/environments/EnvironmentLinker.ts+19-20
...@@ -53,26 +53,25 @@ export type EnvironmentLinkError =...@@ -53,26 +53,25 @@ export type EnvironmentLinkError =
53 | EnvironmentCredentials.EnvironmentCredentialCreatePersistenceError53 | EnvironmentCredentials.EnvironmentCredentialCreatePersistenceError
54 | ManagedEndpointProvider.ManagedEndpointProviderError;54 | ManagedEndpointProvider.ManagedEndpointProviderError;
5555
56export interface EnvironmentLinkerShape {56export class EnvironmentLinker extends Context.Service<
57 readonly link: (input: {57 EnvironmentLinker,
58 readonly userId: string;58 {
59 readonly request: RelayEnvironmentLinkRequest;59 readonly link: (input: {
60 }) => Effect.Effect<60 readonly userId: string;
61 {61 readonly request: RelayEnvironmentLinkRequest;
62 readonly environmentId: RelayEnvironmentLinkProofPayload["environmentId"];62 }) => Effect.Effect<
63 readonly endpoint: RelayEnvironmentLinkProofPayload["endpoint"];63 {
64 readonly endpointRuntime:64 readonly environmentId: RelayEnvironmentLinkProofPayload["environmentId"];
65 | ManagedEndpointProvider.ManagedEndpointProvisioningResult["runtime"]65 readonly endpoint: RelayEnvironmentLinkProofPayload["endpoint"];
66 | null;66 readonly endpointRuntime:
67 readonly environmentCredential: string;67 | ManagedEndpointProvider.ManagedEndpointProvisioningResult["runtime"]
68 },68 | null;
69 EnvironmentLinkError69 readonly environmentCredential: string;
70 >;70 },
71}71 EnvironmentLinkError
7272 >;
73export class EnvironmentLinker extends Context.Service<EnvironmentLinker, EnvironmentLinkerShape>()(73 }
74 "t3code-relay/environments/EnvironmentLinker",74>()("t3code-relay/environments/EnvironmentLinker") {}
75) {}
7675
77const decodeProof = Schema.decodeUnknownEffect(RelayEnvironmentLinkProofPayload);76const decodeProof = Schema.decodeUnknownEffect(RelayEnvironmentLinkProofPayload);
7877
infra/relay/src/environments/EnvironmentLinks.test.ts+16-8
...@@ -3,9 +3,9 @@ import * as Effect from "effect/Effect";...@@ -3,9 +3,9 @@ import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";3import * as Layer from "effect/Layer";
4import { PgDialect } from "drizzle-orm/pg-core";4import { PgDialect } from "drizzle-orm/pg-core";
55
6import { RelayDb, type RelayDatabase } from "../db.ts";6import * as RelayDb from "../db.ts";
7import { relayEnvironmentLinks } from "../persistence/schema.ts";7import { relayEnvironmentLinks } from "../persistence/schema.ts";
8import { EnvironmentLinks, layer } from "./EnvironmentLinks.ts";8import * as EnvironmentLinks from "./EnvironmentLinks.ts";
99
10describe("EnvironmentLinks", () => {10describe("EnvironmentLinks", () => {
11 it.effect("selects users when either notifications or Live Activities are enabled", () => {11 it.effect("selects users when either notifications or Live Activities are enabled", () => {
...@@ -25,10 +25,10 @@ describe("EnvironmentLinks", () => {...@@ -25,10 +25,10 @@ describe("EnvironmentLinks", () => {
25 },25 },
26 };26 };
27 },27 },
28 } as unknown as RelayDatabase;28 } as unknown as RelayDb.RelayDb["Service"];
2929
30 return Effect.gen(function* () {30 return Effect.gen(function* () {
31 const links = yield* EnvironmentLinks;31 const links = yield* EnvironmentLinks.EnvironmentLinks;
32 expect(yield* links.listUsersForEnvironment({ environmentId: "env-1" })).toEqual([]);32 expect(yield* links.listUsersForEnvironment({ environmentId: "env-1" })).toEqual([]);
33 expect(whereConditions).toHaveLength(1);33 expect(whereConditions).toHaveLength(1);
3434
...@@ -39,7 +39,11 @@ describe("EnvironmentLinks", () => {...@@ -39,7 +39,11 @@ describe("EnvironmentLinks", () => {
39 expect(query.sql).toContain('"relay_environment_links"."live_activities_enabled" = $3');39 expect(query.sql).toContain('"relay_environment_links"."live_activities_enabled" = $3');
40 expect(query.sql).toContain(" or ");40 expect(query.sql).toContain(" or ");
41 expect(query.params).toEqual(["env-1", true, true]);41 expect(query.params).toEqual(["env-1", true, true]);
42 }).pipe(Effect.provide(layer.pipe(Layer.provide(Layer.succeed(RelayDb, fakeDb)))));42 }).pipe(
43 Effect.provide(
44 EnvironmentLinks.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb))),
45 ),
46 );
43 });47 });
4448
45 it.effect("revokes only the active link owned by the requesting user", () => {49 it.effect("revokes only the active link owned by the requesting user", () => {
...@@ -65,10 +69,10 @@ describe("EnvironmentLinks", () => {...@@ -65,10 +69,10 @@ describe("EnvironmentLinks", () => {
65 },69 },
66 };70 };
67 },71 },
68 } as unknown as RelayDatabase;72 } as unknown as RelayDb.RelayDb["Service"];
6973
70 return Effect.gen(function* () {74 return Effect.gen(function* () {
71 const links = yield* EnvironmentLinks;75 const links = yield* EnvironmentLinks.EnvironmentLinks;
72 const revoked = yield* links.revokeForUser({76 const revoked = yield* links.revokeForUser({
73 userId: "user-1",77 userId: "user-1",
74 environmentId: "env-1",78 environmentId: "env-1",
...@@ -86,6 +90,10 @@ describe("EnvironmentLinks", () => {...@@ -86,6 +90,10 @@ describe("EnvironmentLinks", () => {
86 expect(query.sql).toContain('"relay_environment_links"."environment_id" = $2');90 expect(query.sql).toContain('"relay_environment_links"."environment_id" = $2');
87 expect(query.sql).toContain('"relay_environment_links"."revoked_at" is null');91 expect(query.sql).toContain('"relay_environment_links"."revoked_at" is null');
88 expect(query.params).toEqual(["user-1", "env-1"]);92 expect(query.params).toEqual(["user-1", "env-1"]);
89 }).pipe(Effect.provide(layer.pipe(Layer.provide(Layer.succeed(RelayDb, fakeDb)))));93 }).pipe(
94 Effect.provide(
95 EnvironmentLinks.layer.pipe(Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb))),
96 ),
97 );
90 });98 });
91});99});
infra/relay/src/environments/EnvironmentLinks.ts+40-41
...@@ -11,7 +11,7 @@ import * as Layer from "effect/Layer";...@@ -11,7 +11,7 @@ import * as Layer from "effect/Layer";
11import * as Schema from "effect/Schema";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 * as RelayDb from "../db.ts";
15import { relayEnvironmentLinks } from "../persistence/schema.ts";15import { relayEnvironmentLinks } from "../persistence/schema.ts";
1616
17export interface RelayLinkedEnvironmentRecord extends RelayClientEnvironmentRecord {17export interface RelayLinkedEnvironmentRecord extends RelayClientEnvironmentRecord {
...@@ -78,45 +78,44 @@ export class EnvironmentLinkRevokePersistenceError extends Schema.TaggedErrorCla...@@ -78,45 +78,44 @@ export class EnvironmentLinkRevokePersistenceError extends Schema.TaggedErrorCla
78 }78 }
79}79}
8080
81export interface EnvironmentLinksShape {81export class EnvironmentLinks extends Context.Service<
82 readonly upsert: (input: {82 EnvironmentLinks,
83 readonly userId: string;83 {
84 readonly request: RelayEnvironmentLinkRequest;84 readonly upsert: (input: {
85 readonly proof: RelayEnvironmentLinkProofPayload;85 readonly userId: string;
86 readonly endpoint: RelayManagedEndpoint;86 readonly request: RelayEnvironmentLinkRequest;
87 }) => Effect.Effect<void, EnvironmentLinkUpsertPersistenceError>;87 readonly proof: RelayEnvironmentLinkProofPayload;
88 readonly listUsersForEnvironment: (input: {88 readonly endpoint: RelayManagedEndpoint;
89 readonly environmentId: string;89 }) => Effect.Effect<void, EnvironmentLinkUpsertPersistenceError>;
90 }) => Effect.Effect<ReadonlyArray<string>, EnvironmentLinkUserListPersistenceError>;90 readonly listUsersForEnvironment: (input: {
91 readonly listDeliveryUsersForEnvironment: (input: {91 readonly environmentId: string;
92 readonly environmentId: string;92 }) => Effect.Effect<ReadonlyArray<string>, EnvironmentLinkUserListPersistenceError>;
93 readonly environmentPublicKey: string;93 readonly listDeliveryUsersForEnvironment: (input: {
94 }) => Effect.Effect<94 readonly environmentId: string;
95 ReadonlyArray<AgentAwarenessDeliveryUserRecord>,95 readonly environmentPublicKey: string;
96 EnvironmentLinkUserListPersistenceError96 }) => Effect.Effect<
97 >;97 ReadonlyArray<AgentAwarenessDeliveryUserRecord>,
98 readonly listPublicKeysForEnvironment: (input: {98 EnvironmentLinkUserListPersistenceError
99 readonly environmentId: string;99 >;
100 }) => Effect.Effect<ReadonlyArray<string>, EnvironmentPublicKeyListPersistenceError>;100 readonly listPublicKeysForEnvironment: (input: {
101 readonly listForUser: (input: {101 readonly environmentId: string;
102 readonly userId: string;102 }) => Effect.Effect<ReadonlyArray<string>, EnvironmentPublicKeyListPersistenceError>;
103 }) => Effect.Effect<103 readonly listForUser: (input: {
104 ReadonlyArray<RelayClientEnvironmentRecord>,104 readonly userId: string;
105 EnvironmentLinkListPersistenceError105 }) => Effect.Effect<
106 >;106 ReadonlyArray<RelayClientEnvironmentRecord>,
107 readonly getForUser: (input: {107 EnvironmentLinkListPersistenceError
108 readonly userId: string;108 >;
109 readonly environmentId: string;109 readonly getForUser: (input: {
110 }) => Effect.Effect<RelayLinkedEnvironmentRecord | null, EnvironmentLinkLookupPersistenceError>;110 readonly userId: string;
111 readonly revokeForUser: (input: {111 readonly environmentId: string;
112 readonly userId: string;112 }) => Effect.Effect<RelayLinkedEnvironmentRecord | null, EnvironmentLinkLookupPersistenceError>;
113 readonly environmentId: string;113 readonly revokeForUser: (input: {
114 }) => Effect.Effect<boolean, EnvironmentLinkRevokePersistenceError>;114 readonly userId: string;
115}115 readonly environmentId: string;
116116 }) => Effect.Effect<boolean, EnvironmentLinkRevokePersistenceError>;
117export class EnvironmentLinks extends Context.Service<EnvironmentLinks, EnvironmentLinksShape>()(117 }
118 "t3code-relay/environments/EnvironmentLinks",118>()("t3code-relay/environments/EnvironmentLinks") {}
119) {}
120119
121function agentAwarenessDeliveryUserCondition(environmentId: string) {120function agentAwarenessDeliveryUserCondition(environmentId: string) {
122 return and(121 return and(
...@@ -140,7 +139,7 @@ function agentAwarenessDeliveryUserKeyCondition(input: {...@@ -140,7 +139,7 @@ function agentAwarenessDeliveryUserKeyCondition(input: {
140}139}
141140
142const make = Effect.gen(function* () {141const make = Effect.gen(function* () {
143 const db = yield* RelayDb;142 const db = yield* RelayDb.RelayDb;
144143
145 return EnvironmentLinks.of({144 return EnvironmentLinks.of({
146 upsert: Effect.fn("relay.environment_links.upsert")(145 upsert: Effect.fn("relay.environment_links.upsert")(
infra/relay/src/environments/EnvironmentPublishSignatures.test.ts+2-2
...@@ -80,11 +80,11 @@ const freshRequest = Effect.gen(function* () {...@@ -80,11 +80,11 @@ const freshRequest = Effect.gen(function* () {
80 } satisfies RelayAgentActivityPublishRequest;80 } satisfies RelayAgentActivityPublishRequest;
81});81});
8282
83function layer(replay?: Partial<DpopProofs.DpopProofReplayShape>) {83function layer(replay?: Partial<DpopProofs.DpopProofReplay["Service"]>) {
84 return EnvironmentPublishSignatures.layer.pipe(84 return EnvironmentPublishSignatures.layer.pipe(
85 Layer.provide(85 Layer.provide(
86 Layer.merge(86 Layer.merge(
87 Layer.succeed(RelayConfiguration.RelayConfiguration, config),87 RelayConfiguration.layer(config),
88 Layer.succeed(DpopProofs.DpopProofReplay, {88 Layer.succeed(DpopProofs.DpopProofReplay, {
89 verifyAndConsume:89 verifyAndConsume:
90 replay?.verifyAndConsume ?? (() => Effect.die("unexpected DPoP proof verification")),90 replay?.verifyAndConsume ?? (() => Effect.die("unexpected DPoP proof verification")),
infra/relay/src/environments/EnvironmentPublishSignatures.ts+8-10
...@@ -59,18 +59,16 @@ export type EnvironmentPublishSignatureError =...@@ -59,18 +59,16 @@ export type EnvironmentPublishSignatureError =
59 | EnvironmentPublishPublicKeyMissing59 | EnvironmentPublishPublicKeyMissing
60 | DpopProofs.DpopProofReplayPersistenceError;60 | DpopProofs.DpopProofReplayPersistenceError;
6161
62export interface EnvironmentPublishSignaturesShape {
63 readonly verify: (input: {
64 readonly environmentId: string;
65 readonly environmentPublicKey: string;
66 readonly threadId: string;
67 readonly request: RelayAgentActivityPublishRequest;
68 }) => Effect.Effect<void, EnvironmentPublishSignatureError>;
69}
70
71export class EnvironmentPublishSignatures extends Context.Service<62export class EnvironmentPublishSignatures extends Context.Service<
72 EnvironmentPublishSignatures,63 EnvironmentPublishSignatures,
73 EnvironmentPublishSignaturesShape64 {
65 readonly verify: (input: {
66 readonly environmentId: string;
67 readonly environmentPublicKey: string;
68 readonly threadId: string;
69 readonly request: RelayAgentActivityPublishRequest;
70 }) => Effect.Effect<void, EnvironmentPublishSignatureError>;
71 }
74>()("t3code-relay/environments/EnvironmentPublishSignatures") {}72>()("t3code-relay/environments/EnvironmentPublishSignatures") {}
7573
76const decodeProof = Schema.decodeUnknownEffect(RelayAgentActivityPublishProofPayload);74const decodeProof = Schema.decodeUnknownEffect(RelayAgentActivityPublishProofPayload);
infra/relay/src/environments/ManagedEndpointAllocations.ts+26-28
...@@ -6,7 +6,7 @@ import * as Effect from "effect/Effect";...@@ -6,7 +6,7 @@ import * as Effect from "effect/Effect";
6import * as Layer from "effect/Layer";6import * as Layer from "effect/Layer";
7import * as Schema from "effect/Schema";7import * as Schema from "effect/Schema";
88
9import { RelayDb } from "../db.ts";9import * as RelayDb from "../db.ts";
10import { isManagedEndpointHostname, managedEndpointForHostname } from "../deploymentConfig.ts";10import { isManagedEndpointHostname, managedEndpointForHostname } from "../deploymentConfig.ts";
11import { relayManagedEndpointAllocations } from "../persistence/schema.ts";11import { relayManagedEndpointAllocations } from "../persistence/schema.ts";
1212
...@@ -66,26 +66,29 @@ interface RecordManagedEndpointDnsInput extends ManagedEndpointAllocationKey {...@@ -66,26 +66,29 @@ interface RecordManagedEndpointDnsInput extends ManagedEndpointAllocationKey {
66 readonly dnsRecordId: string;66 readonly dnsRecordId: string;
67}67}
6868
69export interface ManagedEndpointAllocationsShape {69export class ManagedEndpointAllocations extends Context.Service<
70 readonly get: (70 ManagedEndpointAllocations,
71 input: ManagedEndpointAllocationKey,71 {
72 ) => Effect.Effect<ManagedEndpointAllocation | null, ManagedEndpointAllocationPersistenceError>;72 readonly get: (
73 readonly reserve: (73 input: ManagedEndpointAllocationKey,
74 input: ReserveManagedEndpointAllocationInput,74 ) => Effect.Effect<ManagedEndpointAllocation | null, ManagedEndpointAllocationPersistenceError>;
75 ) => Effect.Effect<ManagedEndpointAllocation, ManagedEndpointAllocationPersistenceError>;75 readonly reserve: (
76 readonly recordTunnel: (76 input: ReserveManagedEndpointAllocationInput,
77 input: RecordManagedEndpointTunnelInput,77 ) => Effect.Effect<ManagedEndpointAllocation, ManagedEndpointAllocationPersistenceError>;
78 ) => Effect.Effect<void, ManagedEndpointAllocationPersistenceError>;78 readonly recordTunnel: (
79 readonly recordDns: (79 input: RecordManagedEndpointTunnelInput,
80 input: RecordManagedEndpointDnsInput,80 ) => Effect.Effect<void, ManagedEndpointAllocationPersistenceError>;
81 ) => Effect.Effect<void, ManagedEndpointAllocationPersistenceError>;81 readonly recordDns: (
82 readonly markReady: (82 input: RecordManagedEndpointDnsInput,
83 input: ManagedEndpointAllocationKey,83 ) => Effect.Effect<void, ManagedEndpointAllocationPersistenceError>;
84 ) => Effect.Effect<void, ManagedEndpointAllocationPersistenceError>;84 readonly markReady: (
85 readonly remove: (85 input: ManagedEndpointAllocationKey,
86 input: ManagedEndpointAllocationKey,86 ) => Effect.Effect<void, ManagedEndpointAllocationPersistenceError>;
87 ) => Effect.Effect<void, ManagedEndpointAllocationPersistenceError>;87 readonly remove: (
88}88 input: ManagedEndpointAllocationKey,
89 ) => Effect.Effect<void, ManagedEndpointAllocationPersistenceError>;
90 }
91>()("t3code-relay/environments/ManagedEndpointAllocations") {}
8992
90const allocationSelection = {93const allocationSelection = {
91 userId: relayManagedEndpointAllocations.userId,94 userId: relayManagedEndpointAllocations.userId,
...@@ -109,7 +112,7 @@ const persistenceError = (cause: unknown) =>...@@ -109,7 +112,7 @@ const persistenceError = (cause: unknown) =>
109 : new ManagedEndpointAllocationPersistenceError({ cause });112 : new ManagedEndpointAllocationPersistenceError({ cause });
110113
111const make = Effect.gen(function* () {114const make = Effect.gen(function* () {
112 const db = yield* RelayDb;115 const db = yield* RelayDb.RelayDb;
113116
114 return ManagedEndpointAllocations.of({117 return ManagedEndpointAllocations.of({
115 get: Effect.fn("relay.managed_endpoint_allocations.get")(function* (118 get: Effect.fn("relay.managed_endpoint_allocations.get")(function* (
...@@ -198,9 +201,4 @@ const make = Effect.gen(function* () {...@@ -198,9 +201,4 @@ const make = Effect.gen(function* () {
198 });201 });
199});202});
200203
201export class ManagedEndpointAllocations extends Context.Service<204export const layer = Layer.effect(ManagedEndpointAllocations, make);
202 ManagedEndpointAllocations,
203 ManagedEndpointAllocationsShape
204>()("t3code-relay/environments/ManagedEndpointAllocations") {
205 static readonly layer = Layer.effect(this, make);
206}
infra/relay/src/environments/ManagedEndpointProvider.test.ts+3-3
...@@ -204,9 +204,9 @@ function providerLayer(...@@ -204,9 +204,9 @@ function providerLayer(
204) {204) {
205 return ManagedEndpointProvider.layer.pipe(205 return ManagedEndpointProvider.layer.pipe(
206 Layer.provideMerge(NodeServices.layer),206 Layer.provideMerge(NodeServices.layer),
207 Layer.provide(Layer.succeed(RelayConfiguration.RelayConfiguration, config)),207 Layer.provide(RelayConfiguration.layer(config)),
208 Layer.provide(Layer.succeed(ManagedEndpointProvider.ManagedEndpointTunnelClient, tunnelClient)),208 Layer.provide(ManagedEndpointProvider.layerTunnelClient(tunnelClient)),
209 Layer.provide(Layer.succeed(ManagedEndpointProvider.ManagedEndpointDnsClient, dnsClient)),209 Layer.provide(ManagedEndpointProvider.layerDnsClient(dnsClient)),
210 Layer.provide(210 Layer.provide(
211 Layer.succeed(ManagedEndpointAllocations.ManagedEndpointAllocations, allocations),211 Layer.succeed(ManagedEndpointAllocations.ManagedEndpointAllocations, allocations),
212 ),212 ),
infra/relay/src/environments/ManagedEndpointProvider.ts+69-63
...@@ -23,7 +23,7 @@ import {...@@ -23,7 +23,7 @@ import {
23 managedEndpointHostname,23 managedEndpointHostname,
24 managedEndpointTunnelName,24 managedEndpointTunnelName,
25} from "../deploymentConfig.ts";25} from "../deploymentConfig.ts";
26import { ManagedEndpointAllocations } from "./ManagedEndpointAllocations.ts";26import * as ManagedEndpointAllocations from "./ManagedEndpointAllocations.ts";
2727
28export class ManagedEndpointProvisioningNotConfigured extends Schema.TaggedErrorClass<ManagedEndpointProvisioningNotConfigured>()(28export class ManagedEndpointProvisioningNotConfigured extends Schema.TaggedErrorClass<ManagedEndpointProvisioningNotConfigured>()(
29 "ManagedEndpointProvisioningNotConfigured",29 "ManagedEndpointProvisioningNotConfigured",
...@@ -74,21 +74,19 @@ export interface ManagedEndpointProvisioningResult {...@@ -74,21 +74,19 @@ export interface ManagedEndpointProvisioningResult {
74 readonly runtime: RelayManagedEndpointRuntimeConfig;74 readonly runtime: RelayManagedEndpointRuntimeConfig;
75}75}
7676
77export interface ManagedEndpointProviderShape {
78 readonly provision: (input: {
79 readonly userId: string;
80 readonly environmentId: string;
81 readonly origin: RelayManagedEndpointOrigin;
82 }) => Effect.Effect<ManagedEndpointProvisioningResult, ManagedEndpointProviderError>;
83 readonly deprovision: (input: {
84 readonly userId: string;
85 readonly environmentId: string;
86 }) => Effect.Effect<void, ManagedEndpointDeprovisioningFailed>;
87}
88
89export class ManagedEndpointProvider extends Context.Service<77export class ManagedEndpointProvider extends Context.Service<
90 ManagedEndpointProvider,78 ManagedEndpointProvider,
91 ManagedEndpointProviderShape79 {
80 readonly provision: (input: {
81 readonly userId: string;
82 readonly environmentId: string;
83 readonly origin: RelayManagedEndpointOrigin;
84 }) => Effect.Effect<ManagedEndpointProvisioningResult, ManagedEndpointProviderError>;
85 readonly deprovision: (input: {
86 readonly userId: string;
87 readonly environmentId: string;
88 }) => Effect.Effect<void, ManagedEndpointDeprovisioningFailed>;
89 }
92>()("t3code-relay/environments/ManagedEndpointProvider") {}90>()("t3code-relay/environments/ManagedEndpointProvider") {}
9391
94interface ManagedEndpointTunnel {92interface ManagedEndpointTunnel {
...@@ -105,36 +103,42 @@ export class ManagedEndpointTunnelClientError extends Schema.TaggedErrorClass<Ma...@@ -105,36 +103,42 @@ export class ManagedEndpointTunnelClientError extends Schema.TaggedErrorClass<Ma
105 }103 }
106}104}
107105
108export interface ManagedEndpointTunnelClientShape {
109 readonly list: (request: {
110 readonly name: string;
111 readonly isDeleted: false;
112 }) => Effect.Effect<
113 { readonly result: ReadonlyArray<ManagedEndpointTunnel> },
114 ManagedEndpointTunnelClientError
115 >;
116 readonly create: (request: {
117 readonly name: string;
118 readonly configSrc: "cloudflare";
119 }) => Effect.Effect<ManagedEndpointTunnel, ManagedEndpointTunnelClientError>;
120 readonly putConfiguration: (
121 tunnelId: string,
122 config: {
123 readonly ingress: Array<{
124 readonly hostname?: string;
125 readonly service: string;
126 }>;
127 },
128 ) => Effect.Effect<unknown, ManagedEndpointTunnelClientError>;
129 readonly getToken: (tunnelId: string) => Effect.Effect<string, ManagedEndpointTunnelClientError>;
130 readonly delete: (tunnelId: string) => Effect.Effect<unknown, ManagedEndpointTunnelClientError>;
131}
132
133export class ManagedEndpointTunnelClient extends Context.Service<106export class ManagedEndpointTunnelClient extends Context.Service<
134 ManagedEndpointTunnelClient,107 ManagedEndpointTunnelClient,
135 ManagedEndpointTunnelClientShape108 {
109 readonly list: (request: {
110 readonly name: string;
111 readonly isDeleted: false;
112 }) => Effect.Effect<
113 { readonly result: ReadonlyArray<ManagedEndpointTunnel> },
114 ManagedEndpointTunnelClientError
115 >;
116 readonly create: (request: {
117 readonly name: string;
118 readonly configSrc: "cloudflare";
119 }) => Effect.Effect<ManagedEndpointTunnel, ManagedEndpointTunnelClientError>;
120 readonly putConfiguration: (
121 tunnelId: string,
122 config: {
123 readonly ingress: Array<{
124 readonly hostname?: string;
125 readonly service: string;
126 }>;
127 },
128 ) => Effect.Effect<unknown, ManagedEndpointTunnelClientError>;
129 readonly getToken: (
130 tunnelId: string,
131 ) => Effect.Effect<string, ManagedEndpointTunnelClientError>;
132 readonly delete: (tunnelId: string) => Effect.Effect<unknown, ManagedEndpointTunnelClientError>;
133 }
136>()("t3code-relay/environments/ManagedEndpointProvider/ManagedEndpointTunnelClient") {}134>()("t3code-relay/environments/ManagedEndpointProvider/ManagedEndpointTunnelClient") {}
137135
136export const makeTunnelClient = (client: ManagedEndpointTunnelClient["Service"]) =>
137 ManagedEndpointTunnelClient.of(client);
138
139export const layerTunnelClient = (client: ManagedEndpointTunnelClient["Service"]) =>
140 Layer.succeed(ManagedEndpointTunnelClient, makeTunnelClient(client));
141
138interface ManagedEndpointCnameRecordInput {142interface ManagedEndpointCnameRecordInput {
139 readonly type: "CNAME";143 readonly type: "CNAME";
140 readonly name: string;144 readonly name: string;
...@@ -152,29 +156,33 @@ export class ManagedEndpointDnsClientError extends Schema.TaggedErrorClass<Manag...@@ -152,29 +156,33 @@ export class ManagedEndpointDnsClientError extends Schema.TaggedErrorClass<Manag
152 }156 }
153}157}
154158
155export interface ManagedEndpointDnsClientShape {
156 readonly listRecords: (
157 hostname: string,
158 ) => Effect.Effect<ReadonlyArray<{ readonly id: string }>, ManagedEndpointDnsClientError>;
159 readonly createRecord: (
160 request: ManagedEndpointCnameRecordInput,
161 ) => Effect.Effect<{ readonly id: string }, ManagedEndpointDnsClientError>;
162 readonly updateRecord: (
163 dnsRecordId: string,
164 request: ManagedEndpointCnameRecordInput,
165 ) => Effect.Effect<unknown, ManagedEndpointDnsClientError>;
166 readonly deleteRecord: (
167 dnsRecordId: string,
168 ) => Effect.Effect<unknown, ManagedEndpointDnsClientError>;
169}
170
171export class ManagedEndpointDnsClient extends Context.Service<159export class ManagedEndpointDnsClient extends Context.Service<
172 ManagedEndpointDnsClient,160 ManagedEndpointDnsClient,
173 ManagedEndpointDnsClientShape161 {
162 readonly listRecords: (
163 hostname: string,
164 ) => Effect.Effect<ReadonlyArray<{ readonly id: string }>, ManagedEndpointDnsClientError>;
165 readonly createRecord: (
166 request: ManagedEndpointCnameRecordInput,
167 ) => Effect.Effect<{ readonly id: string }, ManagedEndpointDnsClientError>;
168 readonly updateRecord: (
169 dnsRecordId: string,
170 request: ManagedEndpointCnameRecordInput,
171 ) => Effect.Effect<unknown, ManagedEndpointDnsClientError>;
172 readonly deleteRecord: (
173 dnsRecordId: string,
174 ) => Effect.Effect<unknown, ManagedEndpointDnsClientError>;
175 }
174>()("t3code-relay/environments/ManagedEndpointProvider/ManagedEndpointDnsClient") {}176>()("t3code-relay/environments/ManagedEndpointProvider/ManagedEndpointDnsClient") {}
175177
178export const makeDnsClient = (client: ManagedEndpointDnsClient["Service"]) =>
179 ManagedEndpointDnsClient.of(client);
180
181export const layerDnsClient = (client: ManagedEndpointDnsClient["Service"]) =>
182 Layer.succeed(ManagedEndpointDnsClient, makeDnsClient(client));
183
176const requireCloudflareSettings = Effect.fnUntraced(function* (184const requireCloudflareSettings = Effect.fnUntraced(function* (
177 settings: RelayConfiguration.RelayConfigurationShape,185 settings: RelayConfiguration.RelayConfiguration["Service"],
178) {186) {
179 if (!settings.managedEndpointBaseDomain || !settings.managedEndpointNamespace) {187 if (!settings.managedEndpointBaseDomain || !settings.managedEndpointNamespace) {
180 return yield* new ManagedEndpointProvisioningNotConfigured();188 return yield* new ManagedEndpointProvisioningNotConfigured();
...@@ -234,7 +242,7 @@ const make = Effect.gen(function* () {...@@ -234,7 +242,7 @@ const make = Effect.gen(function* () {
234 const crypto = yield* Crypto.Crypto;242 const crypto = yield* Crypto.Crypto;
235 const tunnels = yield* ManagedEndpointTunnelClient;243 const tunnels = yield* ManagedEndpointTunnelClient;
236 const dns = yield* ManagedEndpointDnsClient;244 const dns = yield* ManagedEndpointDnsClient;
237 const allocations = yield* ManagedEndpointAllocations;245 const allocations = yield* ManagedEndpointAllocations.ManagedEndpointAllocations;
238246
239 const updateExistingDnsRecords = Effect.fnUntraced(function* (247 const updateExistingDnsRecords = Effect.fnUntraced(function* (
240 records: ReadonlyArray<{ readonly id: string }>,248 records: ReadonlyArray<{ readonly id: string }>,
...@@ -452,8 +460,7 @@ export const layerCloudflareBindings = (...@@ -452,8 +460,7 @@ export const layerCloudflareBindings = (
452 layer.pipe(460 layer.pipe(
453 Layer.provide(461 Layer.provide(
454 Layer.mergeAll(462 Layer.mergeAll(
455 Layer.succeed(463 layerTunnelClient(
456 ManagedEndpointTunnelClient,
457 ManagedEndpointTunnelClient.of({464 ManagedEndpointTunnelClient.of({
458 list: (request) =>465 list: (request) =>
459 tunnelClient.list(request).pipe(466 tunnelClient.list(request).pipe(
...@@ -482,8 +489,7 @@ export const layerCloudflareBindings = (...@@ -482,8 +489,7 @@ export const layerCloudflareBindings = (
482 ),489 ),
483 }),490 }),
484 ),491 ),
485 Layer.succeed(492 layerDnsClient(
486 ManagedEndpointDnsClient,
487 ManagedEndpointDnsClient.of({493 ManagedEndpointDnsClient.of({
488 listRecords: (hostname) =>494 listRecords: (hostname) =>
489 dnsClient.listDnsRecords({ search: hostname }).pipe(495 dnsClient.listDnsRecords({ search: hostname }).pipe(
infra/relay/src/http/Api.test.ts+2-2
...@@ -30,7 +30,7 @@ vi.mock("@clerk/backend", () => ({...@@ -30,7 +30,7 @@ vi.mock("@clerk/backend", () => ({
30 verifyToken: vi.fn(),30 verifyToken: vi.fn(),
31}));31}));
3232
33const relaySettings: RelayConfiguration.RelayConfigurationShape = {33const relaySettings: RelayConfiguration.RelayConfiguration["Service"] = {
34 relayIssuer: "https://relay.example.test",34 relayIssuer: "https://relay.example.test",
35 apns: {35 apns: {
36 teamId: "apns-team",36 teamId: "apns-team",
...@@ -110,7 +110,7 @@ describe("relay environment authentication", () => {...@@ -110,7 +110,7 @@ describe("relay environment authentication", () => {
110 const failure = new EnvironmentCredentials.EnvironmentCredentialAuthenticatePersistenceError({110 const failure = new EnvironmentCredentials.EnvironmentCredentialAuthenticatePersistenceError({
111 cause: "database unavailable",111 cause: "database unavailable",
112 });112 });
113 const credentials: EnvironmentCredentials.EnvironmentCredentialsShape = {113 const credentials: EnvironmentCredentials.EnvironmentCredentials["Service"] = {
114 create: () => Effect.die("unused create"),114 create: () => Effect.die("unused create"),
115 authenticate: () => Effect.fail(failure),115 authenticate: () => Effect.fail(failure),
116 revokeForEnvironmentPublicKey: () => Effect.die("unused revoke"),116 revokeForEnvironmentPublicKey: () => Effect.die("unused revoke"),
infra/relay/src/http/Api.ts+8-5
...@@ -66,7 +66,7 @@ import * as ManagedEndpointAllocations from "../environments/ManagedEndpointAllo...@@ -66,7 +66,7 @@ import * as ManagedEndpointAllocations from "../environments/ManagedEndpointAllo
66import * as EnvironmentPublishSignatures from "../environments/EnvironmentPublishSignatures.ts";66import * as EnvironmentPublishSignatures from "../environments/EnvironmentPublishSignatures.ts";
67import * as MobileRegistrations from "../agentActivity/MobileRegistrations.ts";67import * as MobileRegistrations from "../agentActivity/MobileRegistrations.ts";
68import { withSpanAttributes } from "../observability.ts";68import { withSpanAttributes } from "../observability.ts";
69import { RelayDb } from "../db.ts";69import * as RelayDb from "../db.ts";
7070
71const relayCorsAllowedMethods = ["GET", "POST", "DELETE", "OPTIONS"] as const;71const relayCorsAllowedMethods = ["GET", "POST", "DELETE", "OPTIONS"] as const;
72const relayCorsAllowedHeaders = [72const relayCorsAllowedHeaders = [
...@@ -347,7 +347,7 @@ export const healthApi = HttpApiBuilder.group(...@@ -347,7 +347,7 @@ export const healthApi = HttpApiBuilder.group(
347 RelayApi,347 RelayApi,
348 "health",348 "health",
349 Effect.fnUntraced(function* (handlers) {349 Effect.fnUntraced(function* (handlers) {
350 const db = yield* RelayDb;350 const db = yield* RelayDb.RelayDb;
351 return handlers.handle(351 return handlers.handle(
352 "health",352 "health",
353 Effect.fn("relay.api.health")(353 Effect.fn("relay.api.health")(
...@@ -985,7 +985,10 @@ function hasExpectedClerkAudience(audience: unknown, expectedAudience: string):...@@ -985,7 +985,10 @@ function hasExpectedClerkAudience(audience: unknown, expectedAudience: string):
985 audience.some((entry) => typeof entry === "string" && entry === expectedAudience);985 audience.some((entry) => typeof entry === "string" && entry === expectedAudience);
986}986}
987987
988function verifyClerkBearerToken(config: RelayConfiguration.RelayConfigurationShape, token: string) {988function verifyClerkBearerToken(
989 config: RelayConfiguration.RelayConfiguration["Service"],
990 token: string,
991) {
989 return Effect.tryPromise({992 return Effect.tryPromise({
990 try: () =>993 try: () =>
991 verifyToken(token, {994 verifyToken(token, {
...@@ -1001,7 +1004,7 @@ function verifyClerkBearerToken(config: RelayConfiguration.RelayConfigurationSha...@@ -1001,7 +1004,7 @@ function verifyClerkBearerToken(config: RelayConfiguration.RelayConfigurationSha
1001}1004}
10021005
1003function verifyClerkOAuthBearerToken(1006function verifyClerkOAuthBearerToken(
1004 config: RelayConfiguration.RelayConfigurationShape,1007 config: RelayConfiguration.RelayConfiguration["Service"],
1005 token: string,1008 token: string,
1006) {1009) {
1007 return Effect.tryPromise({1010 return Effect.tryPromise({
...@@ -1027,7 +1030,7 @@ function verifyClerkOAuthBearerToken(...@@ -1027,7 +1030,7 @@ function verifyClerkOAuthBearerToken(
1027}1030}
10281031
1029export function verifyRelayClientBearerToken(1032export function verifyRelayClientBearerToken(
1030 config: RelayConfiguration.RelayConfigurationShape,1033 config: RelayConfiguration.RelayConfiguration["Service"],
1031 token: string,1034 token: string,
1032) {1035) {
1033 return verifyClerkBearerToken(config, token).pipe(1036 return verifyClerkBearerToken(config, token).pipe(
infra/relay/src/observability.test.ts+2-2
...@@ -9,7 +9,7 @@ import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";...@@ -9,7 +9,7 @@ import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";
9import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";9import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
10import type { OtlpTracer } from "effect/unstable/observability";10import type { OtlpTracer } from "effect/unstable/observability";
1111
12import { EnvironmentConnectNotAuthorized } from "./environments/EnvironmentConnector.ts";12import * as EnvironmentConnector from "./environments/EnvironmentConnector.ts";
13import { makeRelayTraceLayer } from "./observability.ts";13import { makeRelayTraceLayer } from "./observability.ts";
1414
15interface ExportedRequest {15interface ExportedRequest {
...@@ -43,7 +43,7 @@ it.effect("exports schema error fields as span attributes", () =>...@@ -43,7 +43,7 @@ it.effect("exports schema error fields as span attributes", () =>
43 );43 );
4444
45 yield* Effect.fail(45 yield* Effect.fail(
46 new EnvironmentConnectNotAuthorized({46 new EnvironmentConnector.EnvironmentConnectNotAuthorized({
47 environmentId: "environment-1",47 environmentId: "environment-1",
48 operation: "connect",48 operation: "connect",
49 reason: "managed_endpoint_allocation_not_ready",49 reason: "managed_endpoint_allocation_not_ready",
infra/relay/src/worker.ts+4-9
...@@ -42,7 +42,7 @@ import * as EnvironmentCredentials from "./environments/EnvironmentCredentials.t...@@ -42,7 +42,7 @@ import * as EnvironmentCredentials from "./environments/EnvironmentCredentials.t
42import * as EnvironmentLinks from "./environments/EnvironmentLinks.ts";42import * as EnvironmentLinks from "./environments/EnvironmentLinks.ts";
43import * as ManagedEndpointAllocations from "./environments/ManagedEndpointAllocations.ts";43import * as ManagedEndpointAllocations from "./environments/ManagedEndpointAllocations.ts";
44import * as LiveActivities from "./agentActivity/LiveActivities.ts";44import * as LiveActivities from "./agentActivity/LiveActivities.ts";
45import { RelayDb, RelayHyperdrive } from "./db.ts";45import * as RelayDb from "./db.ts";
46import { RelayApnsDeliveryDeadLetterQueue, RelayApnsDeliveryQueue } from "./queues.ts";46import { RelayApnsDeliveryDeadLetterQueue, RelayApnsDeliveryQueue } from "./queues.ts";
47import * as RelayConfiguration from "./Config.ts";47import * as RelayConfiguration from "./Config.ts";
48import * as AgentActivityPublisher from "./agentActivity/AgentActivityPublisher.ts";48import * as AgentActivityPublisher from "./agentActivity/AgentActivityPublisher.ts";
...@@ -138,7 +138,7 @@ export default class Api extends Cloudflare.Worker<Api>()(...@@ -138,7 +138,7 @@ export default class Api extends Cloudflare.Worker<Api>()(
138138
139 const cloudMintPrivateKey = yield* cloudMintKeyPair.privateKey;139 const cloudMintPrivateKey = yield* cloudMintKeyPair.privateKey;
140 const cloudMintPublicKey = yield* cloudMintKeyPair.publicKey;140 const cloudMintPublicKey = yield* cloudMintKeyPair.publicKey;
141 const hyperdrive = yield* Cloudflare.Hyperdrive.bind(yield* RelayHyperdrive);141 const hyperdrive = yield* Cloudflare.Hyperdrive.bind(yield* RelayDb.RelayHyperdrive);
142 const db = yield* Drizzle.postgres(hyperdrive.connectionString);142 const db = yield* Drizzle.postgres(hyperdrive.connectionString);
143143
144 const managedEndpointTunnelBinding = yield* Cloudflare.TunnelReadWrite.bind();144 const managedEndpointTunnelBinding = yield* Cloudflare.TunnelReadWrite.bind();
...@@ -203,16 +203,11 @@ export default class Api extends Cloudflare.Worker<Api>()(...@@ -203,16 +203,11 @@ export default class Api extends Cloudflare.Worker<Api>()(
203 Layer.provideMerge(AgentActivityRows.layer),203 Layer.provideMerge(AgentActivityRows.layer),
204 Layer.provideMerge(Devices.layer),204 Layer.provideMerge(Devices.layer),
205 Layer.provideMerge(EnvironmentCredentials.layer),205 Layer.provideMerge(EnvironmentCredentials.layer),
206 Layer.provideMerge(206 Layer.provideMerge(Layer.mergeAll(EnvironmentLinks.layer, ManagedEndpointAllocations.layer)),
207 Layer.mergeAll(
208 EnvironmentLinks.layer,
209 ManagedEndpointAllocations.ManagedEndpointAllocations.layer,
210 ),
211 ),
212 Layer.provideMerge(LiveActivities.layer),207 Layer.provideMerge(LiveActivities.layer),
213 Layer.provideMerge(DeliveryAttempts.layer),208 Layer.provideMerge(DeliveryAttempts.layer),
214 Layer.provideMerge(RelayTokens.layer),209 Layer.provideMerge(RelayTokens.layer),
215 Layer.provideMerge(Layer.succeed(RelayDb, db)),210 Layer.provideMerge(Layer.succeed(RelayDb.RelayDb, db)),
216 Layer.provideMerge(Layer.effect(RelayConfiguration.RelayConfiguration, loadSettings)),211 Layer.provideMerge(Layer.effect(RelayConfiguration.RelayConfiguration, loadSettings)),
217 Layer.provideMerge(webcryptoLayer),212 Layer.provideMerge(webcryptoLayer),
218 );213 );