authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-19 21:21:15-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-19 21:21:15-07:00
log4ee719a094d4c4f30fab88c1f6969483890e223e
tree4b9dfc31b8333e631e544b39eeee7f0c8b22a3b2
parent01492ebd3b624e544e6375c12540b741c65256b9
signature Signed by PGP key B5690EEEBB952194

[codex] refactor server cloud Effect services (#3183)

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

5 files changed, 180 insertions(+), 140 deletions(-)

apps/server/src/cloud/CliTokenManager.ts+71-31
...@@ -6,7 +6,6 @@ import * as Clock from "effect/Clock";...@@ -6,7 +6,6 @@ import * as Clock from "effect/Clock";
6import * as Console from "effect/Console";6import * as Console from "effect/Console";
7import * as Context from "effect/Context";7import * as Context from "effect/Context";
8import * as Crypto from "effect/Crypto";8import * as Crypto from "effect/Crypto";
9import * as Data from "effect/Data";
10import * as Deferred from "effect/Deferred";9import * as Deferred from "effect/Deferred";
11import * as Duration from "effect/Duration";10import * as Duration from "effect/Duration";
12import * as Effect from "effect/Effect";11import * as Effect from "effect/Effect";
...@@ -15,10 +14,12 @@ import * as Layer from "effect/Layer";...@@ -15,10 +14,12 @@ import * as Layer from "effect/Layer";
15import * as Option from "effect/Option";14import * as Option from "effect/Option";
16import * as Schema from "effect/Schema";15import * as Schema from "effect/Schema";
17import * as Semaphore from "effect/Semaphore";16import * as Semaphore from "effect/Semaphore";
17import * as HttpClient from "effect/unstable/http/HttpClient";
18import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
19import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
18import * as HttpRouter from "effect/unstable/http/HttpRouter";20import * as HttpRouter from "effect/unstable/http/HttpRouter";
19import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";21import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";
20import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";22import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
21import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
2223
23import * as ServerSecretStore from "../auth/ServerSecretStore.ts";24import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
24import { cloudCliOAuthConfig, type CloudCliOAuthConfig } from "./publicConfig.ts";25import { cloudCliOAuthConfig, type CloudCliOAuthConfig } from "./publicConfig.ts";
...@@ -45,35 +46,74 @@ const OAuthTokenResponse = Schema.Struct({...@@ -45,35 +46,74 @@ const OAuthTokenResponse = Schema.Struct({
45 token_type: Schema.String,46 token_type: Schema.String,
46});47});
4748
48export class CloudCliTokenManagerError extends Data.TaggedError("CloudCliTokenManagerError")<{49export class CloudCliCredentialRemovalError extends Schema.TaggedErrorClass<CloudCliCredentialRemovalError>()(
49 readonly message: string;50 "CloudCliCredentialRemovalError",
50 readonly cause?: unknown;51 { cause: Schema.Defect() },
51}> {}52) {
53 override get message(): string {
54 return "Could not remove the stored T3 Connect CLI credential.";
55 }
56}
57
58export class CloudCliCredentialRefreshError extends Schema.TaggedErrorClass<CloudCliCredentialRefreshError>()(
59 "CloudCliCredentialRefreshError",
60 { cause: Schema.Defect() },
61) {
62 override get message(): string {
63 return "Could not refresh the T3 Connect CLI credential.";
64 }
65}
66
67export class CloudCliCredentialReadError extends Schema.TaggedErrorClass<CloudCliCredentialReadError>()(
68 "CloudCliCredentialReadError",
69 { cause: Schema.Defect() },
70) {
71 override get message(): string {
72 return "Could not read the stored T3 Connect CLI credential.";
73 }
74}
75
76export class CloudCliAuthorizationError extends Schema.TaggedErrorClass<CloudCliAuthorizationError>()(
77 "CloudCliAuthorizationError",
78 { cause: Schema.Defect() },
79) {
80 override get message(): string {
81 return "Could not authorize the T3 Connect CLI.";
82 }
83}
5284
53export interface CloudCliTokenManagerShape {85export class CloudCliAuthorizationTimeoutError extends Schema.TaggedErrorClass<CloudCliAuthorizationTimeoutError>()(
54 readonly get: Effect.Effect<PersistedToken, CloudCliTokenManagerError>;86 "CloudCliAuthorizationTimeoutError",
55 readonly getExisting: Effect.Effect<Option.Option<PersistedToken>, CloudCliTokenManagerError>;87 { cause: Schema.Defect() },
56 readonly hasCredential: Effect.Effect<boolean, CloudCliTokenManagerError>;88) {
57 readonly clear: Effect.Effect<void, CloudCliTokenManagerError>;89 override get message(): string {
90 return "Timed out waiting for T3 Connect authorization.";
91 }
58}92}
5993
94export const CloudCliTokenManagerError = Schema.Union([
95 CloudCliCredentialRemovalError,
96 CloudCliCredentialRefreshError,
97 CloudCliCredentialReadError,
98 CloudCliAuthorizationError,
99 CloudCliAuthorizationTimeoutError,
100]);
101export type CloudCliTokenManagerError = typeof CloudCliTokenManagerError.Type;
102
60export class CloudCliTokenManager extends Context.Service<103export class CloudCliTokenManager extends Context.Service<
61 CloudCliTokenManager,104 CloudCliTokenManager,
62 CloudCliTokenManagerShape105 {
106 readonly get: Effect.Effect<PersistedToken, CloudCliTokenManagerError>;
107 readonly getExisting: Effect.Effect<Option.Option<PersistedToken>, CloudCliTokenManagerError>;
108 readonly hasCredential: Effect.Effect<boolean, CloudCliTokenManagerError>;
109 readonly clear: Effect.Effect<void, CloudCliTokenManagerError>;
110 }
63>()("t3/cloud/CliTokenManager/CloudCliTokenManager") {}111>()("t3/cloud/CliTokenManager/CloudCliTokenManager") {}
64112
65const wrapError =113const wrapError =
66 (message: string) =>114 <WrappedError extends CloudCliTokenManagerError>(makeError: (cause: unknown) => WrappedError) =>
67 <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, CloudCliTokenManagerError, R> =>115 <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, WrappedError, R> =>
68 effect.pipe(116 effect.pipe(Effect.mapError(makeError));
69 Effect.mapError(
70 (cause) =>
71 new CloudCliTokenManagerError({
72 message,
73 cause,
74 }),
75 ),
76 );
77117
78function stringToBytes(value: string): Uint8Array {118function stringToBytes(value: string): Uint8Array {
79 return new TextEncoder().encode(value);119 return new TextEncoder().encode(value);
...@@ -83,7 +123,7 @@ function bytesToString(value: Uint8Array): string {...@@ -83,7 +123,7 @@ function bytesToString(value: Uint8Array): string {
83 return new TextDecoder().decode(value);123 return new TextDecoder().decode(value);
84}124}
85125
86const make = Effect.gen(function* () {126export const make = Effect.gen(function* () {
87 const crypto = yield* Crypto.Crypto;127 const crypto = yield* Crypto.Crypto;
88 const httpClient = (yield* HttpClient.HttpClient).pipe(HttpClient.filterStatusOk);128 const httpClient = (yield* HttpClient.HttpClient).pipe(HttpClient.filterStatusOk);
89 const secrets = yield* ServerSecretStore.ServerSecretStore;129 const secrets = yield* ServerSecretStore.ServerSecretStore;
...@@ -96,7 +136,7 @@ const make = Effect.gen(function* () {...@@ -96,7 +136,7 @@ const make = Effect.gen(function* () {
96136
97 const clear = secrets137 const clear = secrets
98 .remove(CLOUD_CLI_OAUTH_TOKEN_SECRET)138 .remove(CLOUD_CLI_OAUTH_TOKEN_SECRET)
99 .pipe(wrapError("Could not remove the stored T3 Connect CLI credential."));139 .pipe(wrapError((cause) => new CloudCliCredentialRemovalError({ cause })));
100140
101 const read = Effect.fn("cloud.cli_token.read")(function* () {141 const read = Effect.fn("cloud.cli_token.read")(function* () {
102 const encoded = yield* secrets.get(CLOUD_CLI_OAUTH_TOKEN_SECRET);142 const encoded = yield* secrets.get(CLOUD_CLI_OAUTH_TOKEN_SECRET);
...@@ -185,10 +225,10 @@ const make = Effect.gen(function* () {...@@ -185,10 +225,10 @@ const make = Effect.gen(function* () {
185 yield* Console.log(`Open this URL to authorize T3 Connect:\n${authorizationUrl.toString()}\n`);225 yield* Console.log(`Open this URL to authorize T3 Connect:\n${authorizationUrl.toString()}\n`);
186 const code = yield* Deferred.await(callback).pipe(226 const code = yield* Deferred.await(callback).pipe(
187 Effect.timeout(CLOUD_CLI_OAUTH_CALLBACK_TIMEOUT),227 Effect.timeout(CLOUD_CLI_OAUTH_CALLBACK_TIMEOUT),
188 Effect.catchTag("TimeoutError", () =>228 Effect.catchTag("TimeoutError", (cause) =>
189 Effect.fail(229 Effect.fail(
190 new CloudCliTokenManagerError({230 new CloudCliAuthorizationTimeoutError({
191 message: "Timed out waiting for T3 Connect authorization.",231 cause,
192 }),232 }),
193 ),233 ),
194 ),234 ),
...@@ -213,12 +253,12 @@ const make = Effect.gen(function* () {...@@ -213,12 +253,12 @@ const make = Effect.gen(function* () {
213 });253 });
214254
215 const getExisting = semaphore.withPermits(1)(255 const getExisting = semaphore.withPermits(1)(
216 getExistingNoLock().pipe(wrapError("Could not refresh the T3 Connect CLI credential.")),256 getExistingNoLock().pipe(wrapError((cause) => new CloudCliCredentialRefreshError({ cause }))),
217 );257 );
218 const hasCredential = semaphore.withPermits(1)(258 const hasCredential = semaphore.withPermits(1)(
219 read().pipe(259 read().pipe(
220 Effect.map(Option.isSome),260 Effect.map(Option.isSome),
221 wrapError("Could not read the stored T3 Connect CLI credential."),261 wrapError((cause) => new CloudCliCredentialReadError({ cause })),
222 ),262 ),
223 );263 );
224 const get = semaphore.withPermits(1)(264 const get = semaphore.withPermits(1)(
...@@ -227,7 +267,7 @@ const make = Effect.gen(function* () {...@@ -227,7 +267,7 @@ const make = Effect.gen(function* () {
227 return Option.isSome(token)267 return Option.isSome(token)
228 ? token.value268 ? token.value
229 : yield* Effect.scoped(login()).pipe(Effect.flatMap(persist));269 : yield* Effect.scoped(login()).pipe(Effect.flatMap(persist));
230 }).pipe(wrapError("Could not authorize the T3 Connect CLI.")),270 }).pipe(wrapError((cause) => new CloudCliAuthorizationError({ cause }))),
231 );271 );
232272
233 return CloudCliTokenManager.of({ get, getExisting, hasCredential, clear });273 return CloudCliTokenManager.of({ get, getExisting, hasCredential, clear });
apps/server/src/cloud/ManagedEndpointRuntime.test.ts+53-45
...@@ -4,16 +4,15 @@ import * as Deferred from "effect/Deferred";...@@ -4,16 +4,15 @@ import * as Deferred from "effect/Deferred";
4import * as Effect from "effect/Effect";4import * as Effect from "effect/Effect";
5import * as Fiber from "effect/Fiber";5import * as Fiber from "effect/Fiber";
6import * as Layer from "effect/Layer";6import * as Layer from "effect/Layer";
7import * as Option from "effect/Option";
7import * as PlatformError from "effect/PlatformError";8import * as PlatformError from "effect/PlatformError";
8import * as Sink from "effect/Sink";9import * as Sink from "effect/Sink";
9import * as Stream from "effect/Stream";10import * as Stream from "effect/Stream";
10import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";11import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
11import * as RelayClient from "@t3tools/shared/relayClient";12import * as RelayClient from "@t3tools/shared/relayClient";
1213
13import {14import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
14 classifyRelayClientOutput,15import * as ManagedEndpointRuntime from "./ManagedEndpointRuntime.ts";
15 makeCloudManagedEndpointRuntime,
16} from "./ManagedEndpointRuntime.ts";
1716
18const relayClientAvailableLayer = Layer.succeed(17const relayClientAvailableLayer = Layer.succeed(
19 RelayClient.RelayClient,18 RelayClient.RelayClient,
...@@ -29,12 +28,33 @@ const relayClientAvailableLayer = Layer.succeed(...@@ -29,12 +28,33 @@ const relayClientAvailableLayer = Layer.succeed(
29 }),28 }),
30);29);
3130
32const runtimeDependencies = (spawner: ReturnType<typeof ChildProcessSpawner.make>) =>31const runtimeDependencies = (
32 spawner: ReturnType<typeof ChildProcessSpawner.make>,
33 relayClientLayer = relayClientAvailableLayer,
34) =>
33 Layer.mergeAll(35 Layer.mergeAll(
34 Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner),36 Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner),
35 relayClientAvailableLayer,37 relayClientLayer,
38 Layer.mock(ServerSecretStore.ServerSecretStore)({
39 get: () => Effect.succeed(Option.none()),
40 }),
36 );41 );
3742
43const buildCloudManagedEndpointRuntime = (
44 spawner: ReturnType<typeof ChildProcessSpawner.make>,
45 relayClientLayer = relayClientAvailableLayer,
46) =>
47 Effect.gen(function* () {
48 const context = yield* Layer.build(
49 ManagedEndpointRuntime.layer.pipe(
50 Layer.provide(runtimeDependencies(spawner, relayClientLayer)),
51 ),
52 );
53 return yield* Effect.service(ManagedEndpointRuntime.CloudManagedEndpointRuntime).pipe(
54 Effect.provide(context),
55 );
56 });
57
38function makeHandle(input: {58function makeHandle(input: {
39 readonly pid: number;59 readonly pid: number;
40 readonly onKill: () => void;60 readonly onKill: () => void;
...@@ -62,16 +82,20 @@ function makeHandle(input: {...@@ -62,16 +82,20 @@ function makeHandle(input: {
62describe("CloudManagedEndpointRuntime", () => {82describe("CloudManagedEndpointRuntime", () => {
63 it("classifies Cloudflare connection and warning output", () => {83 it("classifies Cloudflare connection and warning output", () => {
64 expect(84 expect(
65 classifyRelayClientOutput(85 ManagedEndpointRuntime.classifyRelayClientOutput(
66 "2026-06-17T02:00:00Z INF Registered tunnel connection connIndex=0",86 "2026-06-17T02:00:00Z INF Registered tunnel connection connIndex=0",
67 ),87 ),
68 ).toBe("connected");88 ).toBe("connected");
69 expect(89 expect(
70 classifyRelayClientOutput("2026-06-17T02:00:00Z ERR Failed to serve tunnel connection"),90 ManagedEndpointRuntime.classifyRelayClientOutput(
91 "2026-06-17T02:00:00Z ERR Failed to serve tunnel connection",
92 ),
71 ).toBe("warning");93 ).toBe("warning");
72 expect(classifyRelayClientOutput("2026-06-17T02:00:00Z INF Starting metrics server")).toBe(94 expect(
73 "debug",95 ManagedEndpointRuntime.classifyRelayClientOutput(
74 );96 "2026-06-17T02:00:00Z INF Starting metrics server",
97 ),
98 ).toBe("debug");
75 });99 });
76100
77 it.effect("starts, deduplicates, rotates, and stops the Cloudflare connector", () =>101 it.effect("starts, deduplicates, rotates, and stops the Cloudflare connector", () =>
...@@ -97,9 +121,7 @@ describe("CloudManagedEndpointRuntime", () => {...@@ -97,9 +121,7 @@ describe("CloudManagedEndpointRuntime", () => {
97 return handle;121 return handle;
98 }),122 }),
99 );123 );
100 const runtime = yield* makeCloudManagedEndpointRuntime.pipe(124 const runtime = yield* buildCloudManagedEndpointRuntime(spawner);
101 Effect.provide(runtimeDependencies(spawner)),
102 );
103125
104 yield* runtime.applyConfig({126 yield* runtime.applyConfig({
105 providerKind: "cloudflare_tunnel",127 providerKind: "cloudflare_tunnel",
...@@ -154,9 +176,7 @@ describe("CloudManagedEndpointRuntime", () => {...@@ -154,9 +176,7 @@ describe("CloudManagedEndpointRuntime", () => {
154 return handle;176 return handle;
155 }),177 }),
156 );178 );
157 const runtime = yield* makeCloudManagedEndpointRuntime.pipe(179 const runtime = yield* buildCloudManagedEndpointRuntime(spawner);
158 Effect.provide(runtimeDependencies(spawner)),
159 );
160180
161 const started = yield* runtime.applyConfig({181 const started = yield* runtime.applyConfig({
162 providerKind: "cloudflare_tunnel",182 providerKind: "cloudflare_tunnel",
...@@ -193,9 +213,7 @@ describe("CloudManagedEndpointRuntime", () => {...@@ -193,9 +213,7 @@ describe("CloudManagedEndpointRuntime", () => {
193 return handle;213 return handle;
194 }),214 }),
195 );215 );
196 const runtime = yield* makeCloudManagedEndpointRuntime.pipe(216 const runtime = yield* buildCloudManagedEndpointRuntime(spawner);
197 Effect.provide(runtimeDependencies(spawner)),
198 );
199 const config = {217 const config = {
200 providerKind: "cloudflare_tunnel" as const,218 providerKind: "cloudflare_tunnel" as const,
201 connectorToken: "token",219 connectorToken: "token",
...@@ -240,9 +258,7 @@ describe("CloudManagedEndpointRuntime", () => {...@@ -240,9 +258,7 @@ describe("CloudManagedEndpointRuntime", () => {
240 return handle;258 return handle;
241 }),259 }),
242 );260 );
243 const runtime = yield* makeCloudManagedEndpointRuntime.pipe(261 const runtime = yield* buildCloudManagedEndpointRuntime(spawner);
244 Effect.provide(runtimeDependencies(spawner)),
245 );
246262
247 const started = yield* runtime.applyConfig({263 const started = yield* runtime.applyConfig({
248 providerKind: "cloudflare_tunnel",264 providerKind: "cloudflare_tunnel",
...@@ -282,9 +298,7 @@ describe("CloudManagedEndpointRuntime", () => {...@@ -282,9 +298,7 @@ describe("CloudManagedEndpointRuntime", () => {
282 return handle;298 return handle;
283 }),299 }),
284 );300 );
285 const runtime = yield* makeCloudManagedEndpointRuntime.pipe(301 const runtime = yield* buildCloudManagedEndpointRuntime(spawner);
286 Effect.provide(runtimeDependencies(spawner)),
287 );
288302
289 const first = yield* runtime303 const first = yield* runtime
290 .applyConfig({304 .applyConfig({
...@@ -322,9 +336,7 @@ describe("CloudManagedEndpointRuntime", () => {...@@ -322,9 +336,7 @@ describe("CloudManagedEndpointRuntime", () => {
322 }),336 }),
323 ),337 ),
324 );338 );
325 const runtime = yield* makeCloudManagedEndpointRuntime.pipe(339 const runtime = yield* buildCloudManagedEndpointRuntime(spawner);
326 Effect.provide(runtimeDependencies(spawner)),
327 );
328340
329 const status = yield* runtime.applyConfig({341 const status = yield* runtime.applyConfig({
330 providerKind: "cloudflare_tunnel",342 providerKind: "cloudflare_tunnel",
...@@ -344,22 +356,18 @@ describe("CloudManagedEndpointRuntime", () => {...@@ -344,22 +356,18 @@ describe("CloudManagedEndpointRuntime", () => {
344 Effect.gen(function* () {356 Effect.gen(function* () {
345 const spawn = vi.fn();357 const spawn = vi.fn();
346 const spawner = ChildProcessSpawner.make(spawn);358 const spawner = ChildProcessSpawner.make(spawn);
347 const runtime = yield* makeCloudManagedEndpointRuntime.pipe(359 const runtime = yield* buildCloudManagedEndpointRuntime(
348 Effect.provide(360 spawner,
349 Layer.mergeAll(361 Layer.succeed(
350 Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner),362 RelayClient.RelayClient,
351 Layer.succeed(363 RelayClient.RelayClient.of({
352 RelayClient.RelayClient,364 resolve: Effect.succeed({
353 RelayClient.RelayClient.of({365 status: "missing",
354 resolve: Effect.succeed({366 version: RelayClient.CLOUDFLARED_VERSION,
355 status: "missing",367 }),
356 version: RelayClient.CLOUDFLARED_VERSION,368 install: Effect.die("unused"),
357 }),369 installWithProgress: () => Effect.die("unused"),
358 install: Effect.die("unused"),370 }),
359 installWithProgress: () => Effect.die("unused"),
360 }),
361 ),
362 ),
363 ),371 ),
364 );372 );
365373
apps/server/src/cloud/ManagedEndpointRuntime.ts+26-31
...@@ -10,7 +10,8 @@ import * as Result from "effect/Result";...@@ -10,7 +10,8 @@ import * as Result from "effect/Result";
10import * as Semaphore from "effect/Semaphore";10import * as Semaphore from "effect/Semaphore";
11import * as Scope from "effect/Scope";11import * as Scope from "effect/Scope";
12import * as Stream from "effect/Stream";12import * as Stream from "effect/Stream";
13import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";13import * as ChildProcess from "effect/unstable/process/ChildProcess";
14import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
1415
15import * as ServerSecretStore from "../auth/ServerSecretStore.ts";16import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
16import { CLOUD_ENDPOINT_RUNTIME_CONFIG, decodeRuntimeConfig } from "./config.ts";17import { CLOUD_ENDPOINT_RUNTIME_CONFIG, decodeRuntimeConfig } from "./config.ts";
...@@ -28,17 +29,6 @@ const readRuntimeConfig = Effect.gen(function* () {...@@ -28,17 +29,6 @@ const readRuntimeConfig = Effect.gen(function* () {
28 return Option.getOrNull(decodeRuntimeConfig(bytesToString(bytes.value)));29 return Option.getOrNull(decodeRuntimeConfig(bytesToString(bytes.value)));
29});30});
3031
31export interface CloudManagedEndpointRuntimeShape {
32 readonly applyConfig: (
33 config: RelayManagedEndpointRuntimeConfig | null,
34 ) => Effect.Effect<CloudManagedEndpointRuntimeStatus>;
35}
36
37export class CloudManagedEndpointRuntime extends Context.Service<
38 CloudManagedEndpointRuntime,
39 CloudManagedEndpointRuntimeShape
40>()("t3/cloud/ManagedEndpointRuntime/CloudManagedEndpointRuntime") {}
41
42export type CloudManagedEndpointRuntimeStatus =32export type CloudManagedEndpointRuntimeStatus =
43 | {33 | {
44 readonly status: "disabled";34 readonly status: "disabled";
...@@ -62,6 +52,15 @@ export type CloudManagedEndpointRuntimeStatus =...@@ -62,6 +52,15 @@ export type CloudManagedEndpointRuntimeStatus =
62 readonly providerKind: RelayManagedEndpointRuntimeConfig["providerKind"];52 readonly providerKind: RelayManagedEndpointRuntimeConfig["providerKind"];
63 };53 };
6454
55export class CloudManagedEndpointRuntime extends Context.Service<
56 CloudManagedEndpointRuntime,
57 {
58 readonly applyConfig: (
59 config: RelayManagedEndpointRuntimeConfig | null,
60 ) => Effect.Effect<CloudManagedEndpointRuntimeStatus>;
61 }
62>()("t3/cloud/ManagedEndpointRuntime/CloudManagedEndpointRuntime") {}
63
65interface ActiveConnector {64interface ActiveConnector {
66 readonly child: ChildProcessSpawner.ChildProcessHandle;65 readonly child: ChildProcessSpawner.ChildProcessHandle;
67 readonly scope: Scope.Closeable;66 readonly scope: Scope.Closeable;
...@@ -97,13 +96,13 @@ const stopConnector = (connector: ActiveConnector | null) =>...@@ -97,13 +96,13 @@ const stopConnector = (connector: ActiveConnector | null) =>
97 )96 )
98 : Effect.void;97 : Effect.void;
9998
100export const makeCloudManagedEndpointRuntime = Effect.gen(function* () {99export const make = Effect.gen(function* () {
101 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;100 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
102 const relayClient = yield* RelayClient.RelayClient;101 const relayClient = yield* RelayClient.RelayClient;
103 const activeRef = yield* Ref.make<ActiveConnector | null>(null);102 const activeRef = yield* Ref.make<ActiveConnector | null>(null);
104 const desiredConfigRef = yield* Ref.make<RelayManagedEndpointRuntimeConfig | null>(null);103 const desiredConfigRef = yield* Ref.make<RelayManagedEndpointRuntimeConfig | null>(null);
105 const reconcileSemaphore = yield* Semaphore.make(1);104 const reconcileSemaphore = yield* Semaphore.make(1);
106 let reconcileConfig: CloudManagedEndpointRuntimeShape["applyConfig"];105 let reconcileConfig: CloudManagedEndpointRuntime["Service"]["applyConfig"];
107106
108 const stopActive = Effect.gen(function* () {107 const stopActive = Effect.gen(function* () {
109 const active = yield* Ref.getAndSet(activeRef, null);108 const active = yield* Ref.getAndSet(activeRef, null);
...@@ -301,24 +300,20 @@ export const makeCloudManagedEndpointRuntime = Effect.gen(function* () {...@@ -301,24 +300,20 @@ export const makeCloudManagedEndpointRuntime = Effect.gen(function* () {
301 ),300 ),
302 );301 );
303302
304 return CloudManagedEndpointRuntime.of({303 const runtime = CloudManagedEndpointRuntime.of({
305 applyConfig,304 applyConfig,
306 });305 });
307});
308306
309export const layer = Layer.effect(307 const initialConfig = yield* readRuntimeConfig.pipe(
310 CloudManagedEndpointRuntime,308 Effect.catch((cause) =>
311 Effect.gen(function* () {309 Effect.logWarning("Failed to read managed endpoint runtime config", { cause }).pipe(
312 const runtime = yield* makeCloudManagedEndpointRuntime;310 Effect.as(null),
313 const initialConfig = yield* readRuntimeConfig.pipe(
314 Effect.catch((cause) =>
315 Effect.logWarning("Failed to read managed endpoint runtime config", { cause }).pipe(
316 Effect.as(null),
317 ),
318 ),311 ),
319 );312 ),
320 yield* runtime.applyConfig(initialConfig);313 );
321 yield* Effect.addFinalizer(() => runtime.applyConfig(null));314 yield* runtime.applyConfig(initialConfig);
322 return runtime;315 yield* Effect.addFinalizer(() => runtime.applyConfig(null));
323 }),316 return runtime;
324);317});
318
319export const layer = Layer.effect(CloudManagedEndpointRuntime, make);
apps/server/src/cloud/http.test.ts+10-13
...@@ -9,13 +9,10 @@ import { HttpClient, HttpServerRequest } from "effect/unstable/http";...@@ -9,13 +9,10 @@ import { HttpClient, HttpServerRequest } from "effect/unstable/http";
9import { RelayClientTracer } from "@t3tools/shared/relayTracing";9import { RelayClientTracer } from "@t3tools/shared/relayTracing";
10import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts";10import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts";
11import * as ServerSecretStore from "../auth/ServerSecretStore.ts";11import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
12import { ServerEnvironment } from "../environment/Services/ServerEnvironment.ts";12import * as ServerEnvironment from "../environment/Services/ServerEnvironment.ts";
13import * as CliTokenManager from "./CliTokenManager.ts";13import * as CliTokenManager from "./CliTokenManager.ts";
14import { consumeCloudReplayGuards, reconcileDesiredCloudLink } from "./http.ts";14import { consumeCloudReplayGuards, reconcileDesiredCloudLink } from "./http.ts";
15import {15import * as ManagedEndpointRuntime from "./ManagedEndpointRuntime.ts";
16 CloudManagedEndpointRuntime,
17 type CloudManagedEndpointRuntimeShape,
18} from "./ManagedEndpointRuntime.ts";
19import { traceAuthenticatedRelayRequest, traceRelayRequest } from "./traceRelayRequest.ts";16import { traceAuthenticatedRelayRequest, traceRelayRequest } from "./traceRelayRequest.ts";
2017
21const storeFailure = (tag: "AlreadyExists" | "PermissionDenied") =>18const storeFailure = (tag: "AlreadyExists" | "PermissionDenied") =>
...@@ -32,8 +29,8 @@ const storeFailure = (tag: "AlreadyExists" | "PermissionDenied") =>...@@ -32,8 +29,8 @@ const storeFailure = (tag: "AlreadyExists" | "PermissionDenied") =>
32const unusedSecretStoreOperation = () => Effect.die("unused secret-store operation");29const unusedSecretStoreOperation = () => Effect.die("unused secret-store operation");
3330
34function makeSecretStore(31function makeSecretStore(
35 create: ServerSecretStore.ServerSecretStoreShape["create"],32 create: ServerSecretStore.ServerSecretStore["Service"]["create"],
36): ServerSecretStore.ServerSecretStoreShape {33): ServerSecretStore.ServerSecretStore["Service"] {
37 return {34 return {
38 get: unusedSecretStoreOperation,35 get: unusedSecretStoreOperation,
39 set: unusedSecretStoreOperation,36 set: unusedSecretStoreOperation,
...@@ -151,21 +148,21 @@ describe("reconcileDesiredCloudLink", () => {...@@ -151,21 +148,21 @@ describe("reconcileDesiredCloudLink", () => {
151 makeSecretStore(unusedSecretStoreOperation),148 makeSecretStore(unusedSecretStoreOperation),
152 ),149 ),
153 Effect.provideService(150 Effect.provideService(
154 ServerEnvironment,151 ServerEnvironment.ServerEnvironment,
155 ServerEnvironment.of({152 ServerEnvironment.ServerEnvironment.of({
156 getEnvironmentId: unusedSecretStoreOperation(),153 getEnvironmentId: unusedSecretStoreOperation(),
157 getDescriptor: unusedSecretStoreOperation(),154 getDescriptor: unusedSecretStoreOperation(),
158 }),155 }),
159 ),156 ),
160 Effect.provideService(157 Effect.provideService(
161 CloudManagedEndpointRuntime,158 ManagedEndpointRuntime.CloudManagedEndpointRuntime,
162 CloudManagedEndpointRuntime.of({159 ManagedEndpointRuntime.CloudManagedEndpointRuntime.of({
163 applyConfig: unusedSecretStoreOperation,160 applyConfig: unusedSecretStoreOperation,
164 } satisfies CloudManagedEndpointRuntimeShape),161 } satisfies ManagedEndpointRuntime.CloudManagedEndpointRuntime["Service"]),
165 ),162 ),
166 Effect.provideService(163 Effect.provideService(
167 EnvironmentAuth.EnvironmentAuth,164 EnvironmentAuth.EnvironmentAuth,
168 EnvironmentAuth.EnvironmentAuth.of({} as EnvironmentAuth.EnvironmentAuthShape),165 EnvironmentAuth.EnvironmentAuth.of({} as EnvironmentAuth.EnvironmentAuth["Service"]),
169 ),166 ),
170 Effect.provideService(167 Effect.provideService(
171 CliTokenManager.CloudCliTokenManager,168 CliTokenManager.CloudCliTokenManager,
apps/server/src/cloud/http.ts+20-20
...@@ -55,14 +55,8 @@ import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder";...@@ -55,14 +55,8 @@ import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder";
55import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts";55import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts";
56import * as ServerSecretStore from "../auth/ServerSecretStore.ts";56import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
57import { requireEnvironmentScope } from "../auth/http.ts";57import { requireEnvironmentScope } from "../auth/http.ts";
58import {58import * as ServerEnvironment from "../environment/Services/ServerEnvironment.ts";
59 ServerEnvironment,59import * as ManagedEndpointRuntime from "./ManagedEndpointRuntime.ts";
60 type ServerEnvironmentShape,
61} from "../environment/Services/ServerEnvironment.ts";
62import {
63 CloudManagedEndpointRuntime,
64 type CloudManagedEndpointRuntimeShape,
65} from "./ManagedEndpointRuntime.ts";
66import {60import {
67 CLOUD_ENDPOINT_RUNTIME_CONFIG,61 CLOUD_ENDPOINT_RUNTIME_CONFIG,
68 CLOUD_LINKED_USER_ID,62 CLOUD_LINKED_USER_ID,
...@@ -103,6 +97,9 @@ const failEnvironmentCloudInternalError =...@@ -103,6 +97,9 @@ const failEnvironmentCloudInternalError =
103 Effect.flatMap(() => Effect.fail(new EnvironmentHttpInternalServerError({ message }))),97 Effect.flatMap(() => Effect.fail(new EnvironmentHttpInternalServerError({ message }))),
104 );98 );
10599
100const failCloudCliTokenManagerError = (error: CliTokenManager.CloudCliTokenManagerError) =>
101 failEnvironmentCloudInternalError(error.message)(error.cause);
102
106const requireRelayUrl = relayUrlConfig.pipe(103const requireRelayUrl = relayUrlConfig.pipe(
107 Effect.mapError(104 Effect.mapError(
108 () =>105 () =>
...@@ -121,7 +118,7 @@ function stringToBytes(value: string): Uint8Array {...@@ -121,7 +118,7 @@ function stringToBytes(value: string): Uint8Array {
121}118}
122119
123export function consumeCloudReplayGuards(input: {120export function consumeCloudReplayGuards(input: {
124 readonly secrets: ServerSecretStore.ServerSecretStoreShape;121 readonly secrets: ServerSecretStore.ServerSecretStore["Service"];
125 readonly names: ReadonlyArray<string>;122 readonly names: ReadonlyArray<string>;
126 readonly value: Uint8Array;123 readonly value: Uint8Array;
127}) {124}) {
...@@ -208,7 +205,7 @@ function validateRelayConfigPayload(...@@ -208,7 +205,7 @@ function validateRelayConfigPayload(
208}205}
209206
210function validateLinkedCloudUser(input: {207function validateLinkedCloudUser(input: {
211 readonly secrets: ServerSecretStore.ServerSecretStoreShape;208 readonly secrets: ServerSecretStore.ServerSecretStore["Service"];
212 readonly cloudUserId: string;209 readonly cloudUserId: string;
213}): Effect.Effect<void, EnvironmentAuth.ServerAuthInternalError | EnvironmentHttpConflictError> {210}): Effect.Effect<void, EnvironmentAuth.ServerAuthInternalError | EnvironmentHttpConflictError> {
214 return input.secrets.get(CLOUD_LINKED_USER_ID).pipe(211 return input.secrets.get(CLOUD_LINKED_USER_ID).pipe(
...@@ -237,7 +234,7 @@ function validateLinkedCloudUser(input: {...@@ -237,7 +234,7 @@ function validateLinkedCloudUser(input: {
237}234}
238235
239function readInstalledCloudUserId(236function readInstalledCloudUserId(
240 secrets: ServerSecretStore.ServerSecretStoreShape,237 secrets: ServerSecretStore.ServerSecretStore["Service"],
241): Effect.Effect<string, EnvironmentAuth.ServerAuthInternalError> {238): Effect.Effect<string, EnvironmentAuth.ServerAuthInternalError> {
242 return secrets.get(CLOUD_LINKED_USER_ID).pipe(239 return secrets.get(CLOUD_LINKED_USER_ID).pipe(
243 Effect.mapError(240 Effect.mapError(
...@@ -335,19 +332,19 @@ const decodeCloudHealthProof = Schema.decodeUnknownEffect(RelayCloudEnvironmentH...@@ -335,19 +332,19 @@ const decodeCloudHealthProof = Schema.decodeUnknownEffect(RelayCloudEnvironmentH
335const decodeCloudMintProof = Schema.decodeUnknownEffect(RelayCloudMintCredentialProofPayload);332const decodeCloudMintProof = Schema.decodeUnknownEffect(RelayCloudMintCredentialProofPayload);
336333
337interface CloudHttpDependencies {334interface CloudHttpDependencies {
338 readonly secrets: ServerSecretStore.ServerSecretStoreShape;335 readonly secrets: ServerSecretStore.ServerSecretStore["Service"];
339 readonly environment: ServerEnvironmentShape;336 readonly environment: ServerEnvironment.ServerEnvironment["Service"];
340 readonly endpointRuntime: CloudManagedEndpointRuntimeShape;337 readonly endpointRuntime: ManagedEndpointRuntime.CloudManagedEndpointRuntime["Service"];
341 readonly environmentAuth: EnvironmentAuth.EnvironmentAuthShape;338 readonly environmentAuth: EnvironmentAuth.EnvironmentAuth["Service"];
342 readonly cliTokenManager: CliTokenManager.CloudCliTokenManagerShape;339 readonly cliTokenManager: CliTokenManager.CloudCliTokenManager["Service"];
343 readonly httpClient: HttpClient.HttpClient;340 readonly httpClient: HttpClient.HttpClient;
344}341}
345342
346const cloudHttpDependencies = Effect.gen(function* () {343const cloudHttpDependencies = Effect.gen(function* () {
347 return {344 return {
348 secrets: yield* ServerSecretStore.ServerSecretStore,345 secrets: yield* ServerSecretStore.ServerSecretStore,
349 environment: yield* ServerEnvironment,346 environment: yield* ServerEnvironment.ServerEnvironment,
350 endpointRuntime: yield* CloudManagedEndpointRuntime,347 endpointRuntime: yield* ManagedEndpointRuntime.CloudManagedEndpointRuntime,
351 environmentAuth: yield* EnvironmentAuth.EnvironmentAuth,348 environmentAuth: yield* EnvironmentAuth.EnvironmentAuth,
352 cliTokenManager: yield* CliTokenManager.CloudCliTokenManager,349 cliTokenManager: yield* CliTokenManager.CloudCliTokenManager,
353 httpClient: yield* HttpClient.HttpClient,350 httpClient: yield* HttpClient.HttpClient,
...@@ -595,8 +592,11 @@ const reconcileDesiredCloudLinkWith = Effect.fn("environment.cloud.reconcileDesi...@@ -595,8 +592,11 @@ const reconcileDesiredCloudLinkWith = Effect.fn("environment.cloud.reconcileDesi
595 });592 });
596 },593 },
597 Effect.catchTags({594 Effect.catchTags({
598 CloudCliTokenManagerError: (error) =>595 CloudCliCredentialRemovalError: failCloudCliTokenManagerError,
599 failEnvironmentCloudInternalError(error.message)(error.cause),596 CloudCliCredentialRefreshError: failCloudCliTokenManagerError,
597 CloudCliCredentialReadError: failCloudCliTokenManagerError,
598 CloudCliAuthorizationError: failCloudCliTokenManagerError,
599 CloudCliAuthorizationTimeoutError: failCloudCliTokenManagerError,
600 SecretStoreError: failEnvironmentCloudInternalError(600 SecretStoreError: failEnvironmentCloudInternalError(
601 "Could not persist desired T3 Connect link state.",601 "Could not persist desired T3 Connect link state.",
602 ),602 ),