History

Add bootstrap fd for desktop auth token

- Pass one-shot startup config over inherited fd instead of env - Wire server bootstrap parsing and desktop IPC for websocket URL - Add tests for bootstrap envelope precedence and timeout handling

Julius Marminge committed 7 months ago

Commit 20c05641 parent a542a3b7 files · +488 −59Browse files

Unified · Split

7 files
  1. REMOTE.md +12 −9
  2. apps/desktop/src/main.ts +31 −17
  3. apps/desktop/src/preload.ts +5 −2
  4. apps/server/src/bootstrap.test.ts +88 −0
  5. apps/server/src/bootstrap.ts +121 −0
  6. apps/server/src/main.test.ts +94 −0
  7. apps/server/src/main.ts +137 −31
REMOTE.md+12−9
@@ -6,21 +6,24 @@ Use this when you want to open T3 Code from another device (phone, tablet, anoth · 5 unmodified lines
66
77 The T3 Code CLI accepts the following configuration options, available either as CLI flags or environment variables:
88
9−| CLI flag | Env var | Notes |
10−| ----------------------- | --------------------- | ---------------------------------- |
11−| `--mode <web\|desktop>` | `T3CODE_MODE` | Runtime mode. |
12−| `--port <number>` | `T3CODE_PORT` | HTTP/WebSocket port. |
13−| `--host <address>` | `T3CODE_HOST` | Bind interface/address. |
14−| `--base-dir <path>` | `T3CODE_HOME` | Base directory. |
15−| `--dev-url <url>` | `VITE_DEV_SERVER_URL` | Dev web URL redirect/proxy target. |
16−| `--no-browser` | `T3CODE_NO_BROWSER` | Disable auto-open browser. |
17−| `--auth-token <token>` | `T3CODE_AUTH_TOKEN` | WebSocket auth token. |
9+| CLI flag | Env var | Notes |
10+| ----------------------- | --------------------- | ------------------------------------------------------------------------------------ |
11+| `--mode <web\|desktop>` | `T3CODE_MODE` | Runtime mode. |
12+| `--port <number>` | `T3CODE_PORT` | HTTP/WebSocket port. |
13+| `--host <address>` | `T3CODE_HOST` | Bind interface/address. |
14+| `--base-dir <path>` | `T3CODE_HOME` | Base directory. |
15+| `--dev-url <url>` | `VITE_DEV_SERVER_URL` | Dev web URL redirect/proxy target. |
16+| `--no-browser` | `T3CODE_NO_BROWSER` | Disable auto-open browser. |
17+| `--auth-token <token>` | `T3CODE_AUTH_TOKEN` | WebSocket auth token. Use this for standard CLI and remote-server flows. |
18+| `--bootstrap-fd <fd>` | `T3CODE_BOOTSTRAP_FD` | Read a one-shot bootstrap envelope from an inherited file descriptor during startup. |
1819
1920 > TIP: Use the `--help` flag to see all available options and their descriptions.
2021
2122 ## Security First
2223
2324 - Always set `--auth-token` before exposing the server outside localhost.
25+ - When you control the process launcher, prefer sending the auth token in a JSON envelope via `--bootstrap-fd <fd>`.
26+ With `--bootstrap-fd <fd>`, the launcher starts the server first, then sends a one-shot JSON envelope over the inherited file descriptor. This allows the auth token to be delivered without putting it in process environment or command line arguments.
2427 - Treat the token like a password.
2528 - Prefer binding to trusted interfaces (LAN IP or Tailnet IP) instead of opening all interfaces unless needed.
2629
apps/desktop/src/main.ts+31−17
@@ -56,6 +56,7 @@ const UPDATE_STATE_CHANNEL = "desktop:update-state"; · 55 unmodified lines
5656 const UPDATE_GET_STATE_CHANNEL = "desktop:update-get-state";
5757 const UPDATE_DOWNLOAD_CHANNEL = "desktop:update-download";
5858 const UPDATE_INSTALL_CHANNEL = "desktop:update-install";
59+const GET_WS_URL_CHANNEL = "desktop:get-ws-url";
5960 const BASE_DIR = process.env.T3CODE_HOME?.trim() || Path.join(OS.homedir(), ".t3");
6061 const STATE_DIR = Path.join(BASE_DIR, "userdata");
6162 const DESKTOP_SCHEME = "t3";
@@ -918,17 +919,6 @@ function configureAutoUpdater(): void { · 856 unmodified lines
918919 }, AUTO_UPDATE_POLL_INTERVAL_MS);
919920 updatePollTimer.unref();
920921 }
921−function backendEnv(): NodeJS.ProcessEnv {
922− return {
923− ...process.env,
924− T3CODE_MODE: "desktop",
925− T3CODE_NO_BROWSER: "1",
926− T3CODE_PORT: String(backendPort),
927− T3CODE_HOME: BASE_DIR,
928− T3CODE_AUTH_TOKEN: backendAuthToken,
929− };
930−}
931−
932922 function scheduleBackendRestart(reason: string): void {
933923 if (isQuitting || restartTimer) return;
934924
@@ -952,16 +942,35 @@ function startBackend(): void { · 17 unmodified lines
952942 }
953943
954944 const captureBackendLogs = app.isPackaged && backendLogSink !== null;
955− const child = ChildProcess.spawn(process.execPath, [backendEntry], {
945+ const child = ChildProcess.spawn(process.execPath, [backendEntry, "--bootstrap-fd", "3"], {
956946 cwd: resolveBackendCwd(),
957947 // In Electron main, process.execPath points to the Electron binary.
958948 // Run the child in Node mode so this backend process does not become a GUI app instance.
959949 env: {
960− ...backendEnv(),
950+ ...process.env,
961951 ELECTRON_RUN_AS_NODE: "1",
962952 },
963− stdio: captureBackendLogs ? ["ignore", "pipe", "pipe"] : "inherit",
953+ stdio: captureBackendLogs
954+ ? ["ignore", "pipe", "pipe", "pipe"]
955+ : ["ignore", "inherit", "inherit", "pipe"],
964956 });
957+ const bootstrapStream = child.stdio[3];
958+ if (bootstrapStream && "write" in bootstrapStream) {
959+ bootstrapStream.write(
960+ `${JSON.stringify({
961+ mode: "desktop",
962+ noBrowser: true,
963+ port: backendPort,
964+ t3Home: BASE_DIR,
965+ authToken: backendAuthToken,
966+ })}\n`,
967+ );
968+ bootstrapStream.end();
969+ } else {
970+ child.kill("SIGTERM");
971+ scheduleBackendRestart("missing desktop bootstrap pipe");
972+ return;
973+ }
965974 backendProcess = child;
966975 let backendSessionClosed = false;
967976 const closeBackendSession = (details: string) => {
@@ -1072,6 +1081,11 @@ async function stopBackendAndWaitForExit(timeoutMs = 5_000): Promise<void> { · 104 unmodified lines
10721081 }
10731082
10741083 function registerIpcHandlers(): void {
1084+ ipcMain.removeAllListeners(GET_WS_URL_CHANNEL);
1085+ ipcMain.on(GET_WS_URL_CHANNEL, (event) => {
1086+ event.returnValue = backendWsUrl;
1087+ });
1088+
10751089 ipcMain.removeHandler(PICK_FOLDER_CHANNEL);
10761090 ipcMain.handle(PICK_FOLDER_CHANNEL, async () => {
10771091 const owner = BrowserWindow.getFocusedWindow() ?? mainWindow;
@@ -1320,9 +1334,9 @@ async function bootstrap(): Promise<void> { · 242 unmodified lines
13201334 );
13211335 writeDesktopLogHeader(`reserved backend port via NetService port=${backendPort}`);
13221336 backendAuthToken = Crypto.randomBytes(24).toString("hex");
1323− backendWsUrl = `ws://127.0.0.1:${backendPort}/?token=${encodeURIComponent(backendAuthToken)}`;
1324− process.env.T3CODE_DESKTOP_WS_URL = backendWsUrl;
1325− writeDesktopLogHeader(`bootstrap resolved websocket url=${backendWsUrl}`);
1337+ const baseUrl = `ws://127.0.0.1:${backendPort}`;
1338+ backendWsUrl = `${baseUrl}/?token=${encodeURIComponent(backendAuthToken)}`;
1339+ writeDesktopLogHeader(`bootstrap resolved websocket endpoint baseUrl=${baseUrl}`);
13261340
13271341 registerIpcHandlers();
13281342 writeDesktopLogHeader("bootstrap ipc handlers registered");
apps/desktop/src/preload.ts+5−2
@@ -11,10 +11,13 @@ const UPDATE_STATE_CHANNEL = "desktop:update-state"; · 10 unmodified lines
1111 const UPDATE_GET_STATE_CHANNEL = "desktop:update-get-state";
1212 const UPDATE_DOWNLOAD_CHANNEL = "desktop:update-download";
1313 const UPDATE_INSTALL_CHANNEL = "desktop:update-install";
14−const wsUrl = process.env.T3CODE_DESKTOP_WS_URL ?? null;
14+const GET_WS_URL_CHANNEL = "desktop:get-ws-url";
1515
1616 contextBridge.exposeInMainWorld("desktopBridge", {
17− getWsUrl: () => wsUrl,
17+ getWsUrl: () => {
18+ const result = ipcRenderer.sendSync(GET_WS_URL_CHANNEL);
19+ return typeof result === "string" ? result : null;
20+ },
1821 pickFolder: () => ipcRenderer.invoke(PICK_FOLDER_CHANNEL),
1922 confirm: (message) => ipcRenderer.invoke(CONFIRM_CHANNEL, message),
2023 setTheme: (theme) => ipcRenderer.invoke(SET_THEME_CHANNEL, theme),
apps/server/src/bootstrap.test.ts+88−0
@@ -0,0 +1,88 @@
1+import * as NFS from "node:fs";
2+import * as path from "node:path";
3+import { execFileSync, spawn } from "node:child_process";
4+import * as NodeServices from "@effect/platform-node/NodeServices";
5+import { it } from "@effect/vitest";
6+import { FileSystem, Schema } from "effect";
7+import * as Duration from "effect/Duration";
8+import * as Effect from "effect/Effect";
9+import * as Fiber from "effect/Fiber";
10+import { TestClock } from "effect/testing";
11+
12+import { readBootstrapEnvelope } from "./bootstrap";
13+import { assertNone, assertSome } from "@effect/vitest/utils";
14+
15+const TestEnvelopeSchema = Schema.Struct({ mode: Schema.String });
16+
17+it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {
18+ it.effect("reads a bootstrap envelope from a provided fd", () =>
19+ Effect.gen(function* () {
20+ const fs = yield* FileSystem.FileSystem;
21+ const filePath = yield* fs.makeTempFileScoped({ prefix: "t3-bootstrap-", suffix: ".ndjson" });
22+
23+ yield* fs.writeFileString(
24+ filePath,
25+ `${yield* Schema.encodeEffect(Schema.fromJsonString(TestEnvelopeSchema))({
26+ mode: "desktop",
27+ })}\n`,
28+ );
29+
30+ const fd = yield* Effect.acquireRelease(
31+ Effect.sync(() => NFS.openSync(filePath, "r")),
32+ (fd) => Effect.sync(() => NFS.closeSync(fd)),
33+ );
34+
35+ const payload = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, { timeoutMs: 100 });
36+ assertSome(payload, {
37+ mode: "desktop",
38+ });
39+ }),
40+ );
41+
42+ it.effect("returns none when the fd is unavailable", () =>
43+ Effect.gen(function* () {
44+ const fd = NFS.openSync("/dev/null", "r");
45+ NFS.closeSync(fd);
46+
47+ const payload = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, { timeoutMs: 100 });
48+ assertNone(payload);
49+ }),
50+ );
51+
52+ it.effect("returns none when the bootstrap read times out before any value arrives", () =>
53+ Effect.gen(function* () {
54+ const fs = yield* FileSystem.FileSystem;
55+ const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-bootstrap-" });
56+ const fifoPath = path.join(tempDir, "bootstrap.pipe");
57+
58+ yield* Effect.sync(() => execFileSync("mkfifo", [fifoPath]));
59+
60+ const _writer = yield* Effect.acquireRelease(
61+ Effect.sync(() =>
62+ spawn("sh", ["-c", 'exec 3>"$1"; sleep 60', "sh", fifoPath], {
63+ stdio: ["ignore", "ignore", "ignore"],
64+ }),
65+ ),
66+ (writer) =>
67+ Effect.sync(() => {
68+ writer.kill("SIGKILL");
69+ }),
70+ );
71+
72+ const fd = yield* Effect.acquireRelease(
73+ Effect.sync(() => NFS.openSync(fifoPath, "r")),
74+ (fd) => Effect.sync(() => NFS.closeSync(fd)),
75+ );
76+
77+ const fiber = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, {
78+ timeoutMs: 100,
79+ }).pipe(Effect.forkScoped);
80+
81+ yield* Effect.yieldNow;
82+ yield* TestClock.adjust(Duration.millis(100));
83+
84+ const payload = yield* Fiber.join(fiber);
85+ assertNone(payload);
86+ }).pipe(Effect.provide(TestClock.layer())),
87+ );
88+});
apps/server/src/bootstrap.ts+121−0
@@ -0,0 +1,121 @@
1+import * as NFS from "node:fs";
2+import * as readline from "node:readline";
3+
4+import { Data, Effect, Option, Result, Schema } from "effect";
5+import { decodeJsonResult } from "@t3tools/shared/schemaJson";
6+
7+class BootstrapError extends Data.TaggedError("BootstrapError")<{
8+ readonly message: string;
9+ readonly cause?: unknown;
10+}> {}
11+
12+export const readBootstrapEnvelope = Effect.fn("readBootstrapEnvelope")(function* <
13+ S extends Schema.Codec<unknown, unknown, never, never>,
14+>(
15+ schema: S,
16+ fd: number,
17+ options?: {
18+ timeoutMs?: number;
19+ },
20+): Effect.fn.Return<Option.Option<Schema.Schema.Type<S>>, BootstrapError> {
21+ const fdReady = yield* Effect.try({
22+ try: () => NFS.fstatSync(fd),
23+ catch: (error) =>
24+ new BootstrapError({
25+ message: "Failed to stat bootstrap fd.",
26+ cause: error,
27+ }),
28+ }).pipe(
29+ Effect.as(true),
30+ Effect.catchIf(
31+ (error) => isUnavailableBootstrapFdError(error.cause),
32+ () => Effect.succeed(false),
33+ ),
34+ );
35+ if (!fdReady) return Option.none();
36+
37+ const streamFd = yield* Effect.try({
38+ try: () => NFS.openSync(resolveFdPath(fd), "r"),
39+ catch: (error) =>
40+ new BootstrapError({
41+ message: "Failed to duplicate bootstrap fd.",
42+ cause: error,
43+ }),
44+ });
45+
46+ const stream = NFS.createReadStream("", {
47+ fd: streamFd,
48+ encoding: "utf8",
49+ autoClose: true,
50+ });
51+
52+ const timeoutMs = options?.timeoutMs ?? 1000;
53+
54+ return yield* Effect.callback<Option.Option<Schema.Schema.Type<S>>, BootstrapError>((resume) => {
55+ const input = readline.createInterface({
56+ input: stream,
57+ crlfDelay: Infinity,
58+ });
59+
60+ const cleanup = () => {
61+ stream.removeListener("error", handleError);
62+ input.removeListener("line", handleLine);
63+ input.removeListener("close", handleClose);
64+ input.close();
65+ };
66+
67+ const handleError = (error: Error) => {
68+ if (isUnavailableBootstrapFdError(error)) {
69+ resume(Effect.succeed(Option.none<Schema.Schema.Type<S>>()));
70+ return;
71+ }
72+ resume(
73+ Effect.fail(
74+ new BootstrapError({
75+ message: "Failed to read bootstrap envelope.",
76+ cause: error,
77+ }),
78+ ),
79+ );
80+ };
81+
82+ const handleLine = (line: string) => {
83+ const parsed = decodeJsonResult(schema)(line);
84+ if (Result.isSuccess(parsed)) {
85+ resume(Effect.succeed(Option.some(parsed.success as Schema.Schema.Type<S>)));
86+ return;
87+ }
88+ resume(
89+ Effect.fail(
90+ new BootstrapError({
91+ message: "Failed to decode bootstrap envelope.",
92+ cause: parsed.failure,
93+ }),
94+ ),
95+ );
96+ };
97+
98+ const handleClose = () => {
99+ resume(Effect.succeed(Option.none<Schema.Schema.Type<S>>()));
100+ };
101+
102+ stream.once("error", handleError);
103+ input.once("line", handleLine);
104+ input.once("close", handleClose);
105+
106+ return Effect.sync(cleanup);
107+ }).pipe(Effect.timeoutOption(timeoutMs), Effect.map(Option.flatten));
108+});
109+
110+function isUnavailableBootstrapFdError(error: unknown): boolean {
111+ return (
112+ error instanceof Error && "code" in error && (error.code === "EBADF" || error.code === "ENOENT")
113+ );
114+}
115+
116+function resolveFdPath(fd: number): string {
117+ if (process.platform === "linux") {
118+ return `/proc/self/fd/${fd}`;
119+ }
120+ return `/dev/fd/${fd}`;
121+}
apps/server/src/main.test.ts+94−0
@@ -4,6 +4,7 @@ import { assert, it, vi } from "@effect/vitest"; · 3 unmodified lines
44 import type { OrchestrationReadModel } from "@t3tools/contracts";
55 import * as ConfigProvider from "effect/ConfigProvider";
66 import * as Effect from "effect/Effect";
7+import * as FileSystem from "effect/FileSystem";
78 import * as Layer from "effect/Layer";
89 import * as Command from "effect/unstable/cli/Command";
910 import { FetchHttpClient } from "effect/unstable/http";
@@ -151,6 +152,99 @@ it.layer(testLayer)("server CLI command", (it) => { · 141 unmodified lines
151152 }),
152153 );
153154
155+ const openBootstrapFd = Effect.fn(function* (payload: Record<string, unknown>) {
156+ const fs = yield* FileSystem.FileSystem;
157+ const filePath = yield* fs.makeTempFileScoped({ prefix: "t3-bootstrap-", suffix: ".ndjson" });
158+ yield* fs.writeFileString(filePath, `${JSON.stringify(payload)}\n`);
159+ const { fd } = yield* fs.open(filePath, { flag: "r" });
160+ return fd;
161+ });
162+
163+ it.effect("recognizes bootstrap fd from environment config", () =>
164+ Effect.gen(function* () {
165+ const fd = yield* openBootstrapFd({ authToken: "bootstrap-token" });
166+
167+ yield* runCli([], {
168+ T3CODE_MODE: "web",
169+ T3CODE_BOOTSTRAP_FD: String(fd),
170+ T3CODE_AUTH_TOKEN: "env-token",
171+ T3CODE_NO_BROWSER: "true",
172+ });
173+
174+ assert.equal(start.mock.calls.length, 1);
175+ assert.equal(resolvedConfig?.mode, "web");
176+ assert.equal(resolvedConfig?.authToken, "env-token");
177+ }),
178+ );
179+
180+ it.effect("uses bootstrap envelope values as fallbacks when CLI and env are absent", () =>
181+ Effect.gen(function* () {
182+ const fd = yield* openBootstrapFd({
183+ mode: "desktop",
184+ port: 4888,
185+ host: "127.0.0.2",
186+ t3Home: "/tmp/t3-bootstrap-home",
187+ devUrl: "http://127.0.0.1:5173",
188+ noBrowser: true,
189+ authToken: "bootstrap-token",
190+ autoBootstrapProjectFromCwd: false,
191+ logWebSocketEvents: true,
192+ });
193+
194+ yield* runCli([], {
195+ T3CODE_BOOTSTRAP_FD: String(fd),
196+ });
197+
198+ assert.equal(start.mock.calls.length, 1);
199+ assert.equal(resolvedConfig?.mode, "desktop");
200+ assert.equal(resolvedConfig?.port, 4888);
201+ assert.equal(resolvedConfig?.host, "127.0.0.2");
202+ assert.equal(resolvedConfig?.baseDir, "/tmp/t3-bootstrap-home");
203+ assert.equal(resolvedConfig?.stateDir, "/tmp/t3-bootstrap-home/dev");
204+ assert.equal(resolvedConfig?.devUrl?.toString(), "http://127.0.0.1:5173/");
205+ assert.equal(resolvedConfig?.noBrowser, true);
206+ assert.equal(resolvedConfig?.authToken, "bootstrap-token");
207+ assert.equal(resolvedConfig?.autoBootstrapProjectFromCwd, false);
208+ assert.equal(resolvedConfig?.logWebSocketEvents, true);
209+ }),
210+ );
211+
212+ it.effect("applies CLI then env precedence over bootstrap envelope values", () =>
213+ Effect.gen(function* () {
214+ const fd = yield* openBootstrapFd({
215+ mode: "desktop",
216+ port: 4888,
217+ host: "127.0.0.2",
218+ t3Home: "/tmp/t3-bootstrap-home",
219+ devUrl: "http://127.0.0.1:5173",
220+ noBrowser: false,
221+ authToken: "bootstrap-token",
222+ autoBootstrapProjectFromCwd: false,
223+ logWebSocketEvents: false,
224+ });
225+
226+ yield* runCli(["--port", "4999", "--host", "0.0.0.0", "--auth-token", "cli-token"], {
227+ T3CODE_MODE: "web",
228+ T3CODE_BOOTSTRAP_FD: String(fd),
229+ T3CODE_HOME: "/tmp/t3-env-home",
230+ T3CODE_NO_BROWSER: "true",
231+ T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD: "true",
232+ T3CODE_LOG_WS_EVENTS: "true",
233+ });
234+
235+ assert.equal(start.mock.calls.length, 1);
236+ assert.equal(resolvedConfig?.mode, "web");
237+ assert.equal(resolvedConfig?.port, 4999);
238+ assert.equal(resolvedConfig?.host, "0.0.0.0");
239+ assert.equal(resolvedConfig?.baseDir, "/tmp/t3-env-home");
240+ assert.equal(resolvedConfig?.devUrl?.toString(), "http://127.0.0.1:5173/");
241+ assert.equal(resolvedConfig?.noBrowser, true);
242+ assert.equal(resolvedConfig?.authToken, "cli-token");
243+ assert.equal(resolvedConfig?.autoBootstrapProjectFromCwd, true);
244+ assert.equal(resolvedConfig?.logWebSocketEvents, true);
245+ }),
246+ );
247+
154248 it.effect("prefers --mode over T3CODE_MODE", () =>
155249 Effect.gen(function* () {
156250 findAvailablePort.mockImplementation((_preferred: number) => Effect.succeed(4666));
apps/server/src/main.ts+137−31
@@ -27,12 +27,27 @@ import { Server } from "./wsServer"; · 26 unmodified lines
2727 import { ServerLoggerLive } from "./serverLogger";
2828 import { AnalyticsServiceLayerLive } from "./telemetry/Layers/AnalyticsService";
2929 import { AnalyticsService } from "./telemetry/Services/AnalyticsService";
30+import { readBootstrapEnvelope } from "./bootstrap";
3031
3132 export class StartupError extends Data.TaggedError("StartupError")<{
3233 readonly message: string;
3334 readonly cause?: unknown;
3435 }> {}
3536
37+const PortSchema = Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65535 }));
38+
39+const BootstrapEnvelopeSchema = Schema.Struct({
40+ mode: Schema.optional(Schema.String),
41+ port: Schema.optional(PortSchema),
42+ host: Schema.optional(Schema.String),
43+ t3Home: Schema.optional(Schema.String),
44+ devUrl: Schema.optional(Schema.URLFromString),
45+ noBrowser: Schema.optional(Schema.Boolean),
46+ authToken: Schema.optional(Schema.String),
47+ autoBootstrapProjectFromCwd: Schema.optional(Schema.Boolean),
48+ logWebSocketEvents: Schema.optional(Schema.Boolean),
49+});
50+
3651 interface CliInput {
3752 readonly mode: Option.Option<RuntimeMode>;
3853 readonly port: Option.Option<number>;
@@ -41,6 +56,7 @@ interface CliInput { · 2 unmodified lines
4156 readonly devUrl: Option.Option<URL>;
4257 readonly noBrowser: Option.Option<boolean>;
4358 readonly authToken: Option.Option<string>;
59+ readonly bootstrapFd: Option.Option<number>;
4460 readonly autoBootstrapProjectFromCwd: Option.Option<boolean>;
4561 readonly logWebSocketEvents: Option.Option<boolean>;
4662 }
@@ -91,12 +107,8 @@ export class CliConfig extends ServiceMap.Service<CliConfig, CliConfigShape>()( · 44 unmodified lines
91107 const CliEnvConfig = Config.all({
92108 mode: Config.string("T3CODE_MODE").pipe(
93109 Config.option,
94− Config.map(
95− Option.match<RuntimeMode, string>({
96− onNone: () => "web",
97− onSome: (value) => (value === "desktop" ? "desktop" : "web"),
98− }),
99− ),
110+ Config.map(Option.map((value) => (value === "desktop" ? "desktop" : "web"))),
111+ Config.map(Option.getOrUndefined),
100112 ),
101113 port: Config.port("T3CODE_PORT").pipe(Config.option, Config.map(Option.getOrUndefined)),
102114 host: Config.string("T3CODE_HOST").pipe(Config.option, Config.map(Option.getOrUndefined)),
@@ -110,6 +122,10 @@ const CliEnvConfig = Config.all({ · 7 unmodified lines
110122 Config.option,
111123 Config.map(Option.getOrUndefined),
112124 ),
125+ bootstrapFd: Config.int("T3CODE_BOOTSTRAP_FD").pipe(
126+ Config.option,
127+ Config.map(Option.getOrUndefined),
128+ ),
113129 autoBootstrapProjectFromCwd: Config.boolean("T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD").pipe(
114130 Config.option,
115131 Config.map(Option.getOrUndefined),
@@ -123,6 +139,14 @@ const CliEnvConfig = Config.all({ · 7 unmodified lines
123139 const resolveBooleanFlag = (flag: Option.Option<boolean>, envValue: boolean) =>
124140 Option.getOrElse(Option.filter(flag, Boolean), () => envValue);
125141
142+const resolveOptionPrecedence = <Value>(
143+ ...values: ReadonlyArray<Option.Option<Value>>
144+): Option.Option<Value> => Option.firstSomeOf(values);
145+
146+const isValidPort = (value: number): boolean => value >= 1 && value <= 65_535;
147+const isRuntimeMode = (value: string): value is RuntimeMode =>
148+ value === "web" || value === "desktop";
149+
126150 const ServerConfigLive = (input: CliInput) =>
127151 Layer.effect(
128152 ServerConfig,
@@ -136,39 +160,115 @@ const ServerConfigLive = (input: CliInput) => · 7 unmodified lines
136160 ),
137161 );
138162
139− const mode = Option.getOrElse(input.mode, () => env.mode);
140−
141− const port = yield* Option.match(input.port, {
142− onSome: (value) => Effect.succeed(value),
143− onNone: () => {
144− if (env.port) {
145− return Effect.succeed(env.port);
146− }
147− if (mode === "desktop") {
148− return Effect.succeed(DEFAULT_PORT);
149− }
150− return findAvailablePort(DEFAULT_PORT);
163+ const bootstrapFd = Option.getOrUndefined(input.bootstrapFd) ?? env.bootstrapFd;
164+ const bootstrapEnvelope =
165+ bootstrapFd !== undefined
166+ ? yield* readBootstrapEnvelope(BootstrapEnvelopeSchema, bootstrapFd)
167+ : Option.none();
168+
169+ const mode: RuntimeMode = Option.getOrElse(
170+ resolveOptionPrecedence(
171+ input.mode,
172+ Option.fromUndefinedOr(env.mode),
173+ Option.flatMap(bootstrapEnvelope, (bootstrap) =>
174+ Option.filter(Option.fromUndefinedOr(bootstrap.mode), isRuntimeMode),
175+ ),
176+ ),
177+ () => "web",
178+ );
179+ const port = yield* Option.match(
180+ resolveOptionPrecedence(
181+ input.port,
182+ Option.fromUndefinedOr(env.port),
183+ Option.flatMap(bootstrapEnvelope, (bootstrap) =>
184+ Option.filter(Option.fromUndefinedOr(bootstrap.port), isValidPort),
185+ ),
186+ ),
187+ {
188+ onSome: (value) => Effect.succeed(value),
189+ onNone: () => {
190+ if (mode === "desktop") {
191+ return Effect.succeed(DEFAULT_PORT);
192+ }
193+ return findAvailablePort(DEFAULT_PORT);
194+ },
151195 },
152− });
196+ );
153197
154− const devUrl = Option.getOrElse(input.devUrl, () => env.devUrl);
155− const baseDir = yield* resolveBaseDir(Option.getOrUndefined(input.t3Home) ?? env.t3Home);
198+ const devUrl = Option.getOrElse(
199+ resolveOptionPrecedence(
200+ input.devUrl,
201+ Option.fromUndefinedOr(env.devUrl),
202+ Option.flatMap(bootstrapEnvelope, (bootstrap) =>
203+ Option.fromUndefinedOr(bootstrap.devUrl),
204+ ),
205+ ),
206+ () => undefined,
207+ );
208+ const baseDir = yield* resolveBaseDir(
209+ Option.getOrUndefined(
210+ resolveOptionPrecedence(
211+ input.t3Home,
212+ Option.fromUndefinedOr(env.t3Home),
213+ Option.flatMap(bootstrapEnvelope, (bootstrap) =>
214+ Option.fromUndefinedOr(bootstrap.t3Home),
215+ ),
216+ ),
217+ ),
218+ );
156219 const derivedPaths = yield* deriveServerPaths(baseDir, devUrl);
157− const noBrowser = resolveBooleanFlag(input.noBrowser, env.noBrowser ?? mode === "desktop");
158− const authToken = Option.getOrUndefined(input.authToken) ?? env.authToken;
220+ const noBrowser = resolveBooleanFlag(
221+ input.noBrowser,
222+ Option.getOrElse(
223+ resolveOptionPrecedence(
224+ Option.fromUndefinedOr(env.noBrowser),
225+ Option.flatMap(bootstrapEnvelope, (bootstrap) =>
226+ Option.fromUndefinedOr(bootstrap.noBrowser),
227+ ),
228+ ),
229+ () => mode === "desktop",
230+ ),
231+ );
232+ const authToken = resolveOptionPrecedence(
233+ input.authToken,
234+ Option.fromUndefinedOr(env.authToken),
235+ Option.flatMap(bootstrapEnvelope, (bootstrap) =>
236+ Option.fromUndefinedOr(bootstrap.authToken),
237+ ),
238+ );
159239 const autoBootstrapProjectFromCwd = resolveBooleanFlag(
160240 input.autoBootstrapProjectFromCwd,
161− env.autoBootstrapProjectFromCwd ?? mode === "web",
241+ Option.getOrElse(
242+ resolveOptionPrecedence(
243+ Option.fromUndefinedOr(env.autoBootstrapProjectFromCwd),
244+ Option.flatMap(bootstrapEnvelope, (bootstrap) =>
245+ Option.fromUndefinedOr(bootstrap.autoBootstrapProjectFromCwd),
246+ ),
247+ ),
248+ () => mode === "web",
249+ ),
162250 );
163251 const logWebSocketEvents = resolveBooleanFlag(
164252 input.logWebSocketEvents,
165− env.logWebSocketEvents ?? Boolean(devUrl),
253+ Option.getOrElse(
254+ resolveOptionPrecedence(
255+ Option.fromUndefinedOr(env.logWebSocketEvents),
256+ Option.flatMap(bootstrapEnvelope, (bootstrap) =>
257+ Option.fromUndefinedOr(bootstrap.logWebSocketEvents),
258+ ),
259+ ),
260+ () => Boolean(devUrl),
261+ ),
166262 );
167263 const staticDir = devUrl ? undefined : yield* cliConfig.resolveStaticDir;
168− const host =
169− Option.getOrUndefined(input.host) ??
170− env.host ??
171− (mode === "desktop" ? "127.0.0.1" : undefined);
264+ const host = Option.getOrElse(
265+ resolveOptionPrecedence(
266+ input.host,
267+ Option.fromUndefinedOr(env.host),
268+ Option.flatMap(bootstrapEnvelope, (bootstrap) => Option.fromUndefinedOr(bootstrap.host)),
269+ ),
270+ () => (mode === "desktop" ? "127.0.0.1" : undefined),
271+ );
172272
173273 const config: ServerConfigShape = {
174274 mode,
@@ -180,7 +280,7 @@ const ServerConfigLive = (input: CliInput) => · 5 unmodified lines
180280 staticDir,
181281 devUrl,
182282 noBrowser,
183− authToken,
283+ authToken: Option.getOrUndefined(authToken),
184284 autoBootstrapProjectFromCwd,
185285 logWebSocketEvents,
186286 } satisfies ServerConfigShape;
@@ -287,7 +387,7 @@ const modeFlag = Flag.choice("mode", ["web", "desktop"]).pipe( · 100 unmodified lines
287387 Flag.optional,
288388 );
289389 const portFlag = Flag.integer("port").pipe(
290− Flag.withSchema(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65535 }))),
390+ Flag.withSchema(PortSchema),
291391 Flag.withDescription("Port for the HTTP/WebSocket server."),
292392 Flag.optional,
293393 );
@@ -313,6 +413,11 @@ const authTokenFlag = Flag.string("auth-token").pipe( · 19 unmodified lines
313413 Flag.withAlias("token"),
314414 Flag.optional,
315415 );
416+const bootstrapFdFlag = Flag.integer("bootstrap-fd").pipe(
417+ Flag.withSchema(Schema.Int),
418+ Flag.withDescription("Read one-time bootstrap secrets from the given file descriptor."),
419+ Flag.optional,
420+);
316421 const autoBootstrapProjectFromCwdFlag = Flag.boolean("auto-bootstrap-project-from-cwd").pipe(
317422 Flag.withDescription(
318423 "Create a project for the current working directory on startup when missing.",
@@ -335,6 +440,7 @@ export const t3Cli = Command.make("t3", { · 16 unmodified lines
335440 devUrl: devUrlFlag,
336441 noBrowser: noBrowserFlag,
337442 authToken: authTokenFlag,
443+ bootstrapFd: bootstrapFdFlag,
338444 autoBootstrapProjectFromCwd: autoBootstrapProjectFromCwdFlag,
339445 logWebSocketEvents: logWebSocketEventsFlag,
340446 }).pipe(