Skip to content
Source
Sign inSign up

config.ts

291 lines11.3 KB

1import * as Context from "effect/Context";
2import * as Clock from "effect/Clock";
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5import * as Layer from "effect/Layer";
6import * as LogLevel from "effect/LogLevel";
7import * as Path from "effect/Path";
8import type * as Redacted from "effect/Redacted";
9import * as Schema from "effect/Schema";
10​
11import type { OtlpEndpointSource } from "@t3tools/contracts";
12import { sweepStalePendingAttachments } from "./attachmentStore.ts";
13import {
14 DEFAULT_SIGNAL_EXPORT,
15 nodeProcessRuntimeAttributes,
16 processServiceInstanceId,
17 type SignalExport,
18} from "@t3tools/shared/observability";
19import * as OtelEnvironment from "@t3tools/shared/otelEnvironment";
20import { type ReleaseChannel, releaseResourceAttributes } from "@t3tools/shared/releaseResource";
21import packageJson from "../package.json" with { type: "json" };
22​
23// Official release workflows set this telemetry channel; other builds report dev.
24// Unlike __T3CODE_BUILD_CHANNEL__, this is not the sign-in page's latest/nightly channel.
25declare const __T3CODE_TELEMETRY_CHANNEL__: ReleaseChannel | undefined;
26export const telemetryChannel: ReleaseChannel =
27 typeof __T3CODE_TELEMETRY_CHANNEL__ === "undefined" ? "dev" : __T3CODE_TELEMETRY_CHANNEL__;
28​
29export const DEFAULT_PORT = 3773;
30​
31export const RuntimeMode = Schema.Literals(["web", "desktop"]);
32export type RuntimeMode = typeof RuntimeMode.Type;
33​
34export const StartupPresentation = Schema.Literals(["browser", "headless"]);
35export type StartupPresentation = typeof StartupPresentation.Type;
36​
37export interface ServerDerivedPaths {
38 readonly stateDir: string;
39 readonly dbPath: string;
40 readonly keybindingsConfigPath: string;
41 readonly settingsPath: string;
42 readonly environmentThemesDir: string;
43 readonly providerStatusCacheDir: string;
44 readonly worktreesDir: string;
45 readonly worktreeCheckoutCacheDir: string;
46 readonly attachmentsDir: string;
47 readonly browserArtifactsDir: string;
48 readonly logsDir: string;
49 readonly serverTracePath: string;
50 readonly providerLogsDir: string;
51 readonly providerEventLogPath: string;
52 readonly terminalLogsDir: string;
53 readonly anonymousIdPath: string;
54 readonly environmentIdPath: string;
55 readonly serverRuntimeStatePath: string;
56 readonly secretsDir: string;
58​
59export interface DeriveServerPathsOptions {
60 readonly baseDirIsExplicit?: boolean;
62​
63export class ServerConfig extends Context.Service<
64 ServerConfig,
65 ServerDerivedPaths & {
66 readonly logLevel: LogLevel.LogLevel;
67 readonly traceMinLevel: LogLevel.LogLevel;
68 readonly traceTimingEnabled: boolean;
69 readonly traceBatchWindowMs: number;
70 readonly traceMaxBytes: number;
71 readonly traceMaxFiles: number;
72 readonly otlpTracesUrl: string | undefined;
73 readonly otlpMetricsUrl: string | undefined;
74 readonly otlpLogsUrl: string | undefined;
75 readonly otlpTracesExport: SignalExport;
76 readonly otlpMetricsExport: SignalExport;
77 readonly otlpLogsExport: SignalExport;
78 readonly otlpTracesSource?: OtlpEndpointSource;
79 readonly otlpTracesVariable?: string;
80 readonly otlpMetricsSource?: OtlpEndpointSource;
81 readonly otlpMetricsVariable?: string;
82 readonly otlpLogsSource?: OtlpEndpointSource;
83 readonly otlpLogsVariable?: string;
84 readonly otelEnvironment: OtelEnvironment.OtelEnvironment;
85 readonly mode: RuntimeMode;
86 readonly port: number;
87 readonly host: string | undefined;
88 /** Set when the host was defaulted to a private-network listener for the service. */
89 readonly privateNetworkOnly?: boolean | undefined;
90 readonly cwd: string;
91 readonly baseDir: string;
92 readonly staticDir: string | undefined;
93 readonly devUrl: URL | undefined;
94 readonly devAuthToken?: Redacted.Redacted<string> | undefined;
95 readonly devAllowedOrigins: ReadonlyArray<string>;
96 readonly noBrowser: boolean;
97 readonly startupPresentation: StartupPresentation;
98 readonly desktopBootstrapToken: string | undefined;
99 readonly desktopBootstrapSecret?: string | undefined;
100 readonly shellEnvironmentPrepared?: boolean | undefined;
101 readonly desktopTelemetryFd?: number | undefined;
102 readonly desktopTelemetryControlFd?: number | undefined;
103 readonly desktopAppUpdate?: boolean | undefined;
104 readonly desktopBrowserFd?: number | undefined;
105 readonly desktopBrowserControlFd?: number | undefined;
106 readonly resourceMonitorPath?: string | undefined;
107 readonly autoBootstrapProjectFromCwd: boolean;
108 readonly logWebSocketEvents: boolean;
109 readonly tailscaleServeEnabled: boolean;
110 readonly tailscaleServePort: number;
111 }
112>()("t3/config/ServerConfig") {
113 static readonly layerTest = (
114 cwd: string,
115 baseDirOrPrefix: string | { readonly prefix: string },
116 ) => layerTest(cwd, baseDirOrPrefix);
118​
119export const make = (config: ServerConfig["Service"]) => ServerConfig.of(config);
120​
121export const otlpResource = (
122 config: Pick<ServerConfig["Service"], "mode" | "otelEnvironment">,
123 hostName: string,
124) => ({
125 serviceName: "t3code-server",
126 serviceVersion: packageJson.version,
127 attributes: {
128 "service.namespace": "t3code",
129 "service.runtime": "t3-server",
130 "t3code.server.managed_by": config.mode === "desktop" ? "desktop" : "standalone",
131 "service.instance.id": processServiceInstanceId(config.otelEnvironment.resourceAttributes),
132 ...releaseResourceAttributes(telemetryChannel, config.otelEnvironment.resourceAttributes),
133 "host.name": config.otelEnvironment.resourceAttributes["host.name"] || hostName,
134 ...nodeProcessRuntimeAttributes(),
135 },
136});
137​
138export const layer = (config: ServerConfig["Service"]) => Layer.succeed(ServerConfig, make(config));
139​
140export const deriveServerPaths = Effect.fn(function* (
141 baseDir: ServerConfig["Service"]["baseDir"],
142 devUrl: ServerConfig["Service"]["devUrl"],
143 options: DeriveServerPathsOptions = {},
144): Effect.fn.Return<ServerDerivedPaths, never, Path.Path> {
145 const { join } = yield* Path.Path;
146 const stateDir = join(
147 baseDir,
148 devUrl !== undefined && !options.baseDirIsExplicit ? "dev" : "userdata",
149 );
150 const dbPath = join(stateDir, "statev2.sqlite");
151 const attachmentsDir = join(stateDir, "attachments");
152 const logsDir = join(stateDir, "logs");
153 const providerLogsDir = join(logsDir, "provider");
154 const providerStatusCacheDir = join(baseDir, "caches");
155 return {
156 stateDir,
157 dbPath,
158 keybindingsConfigPath: join(stateDir, "keybindings.json"),
159 settingsPath: join(stateDir, "settings.json"),
160 environmentThemesDir: join(stateDir, "themes"),
161 providerStatusCacheDir,
162 worktreesDir: join(baseDir, "worktrees"),
163 worktreeCheckoutCacheDir: join(providerStatusCacheDir, "worktree-checkouts"),
164 attachmentsDir,
165 browserArtifactsDir: join(stateDir, "browser-artifacts"),
166 logsDir,
167 serverTracePath: join(logsDir, "server.trace.ndjson"),
168 providerLogsDir,
169 providerEventLogPath: join(providerLogsDir, "events.log"),
170 terminalLogsDir: join(logsDir, "terminals"),
171 anonymousIdPath: join(stateDir, "anonymous-id"),
172 environmentIdPath: join(stateDir, "environment-id"),
173 serverRuntimeStatePath: join(stateDir, "server-runtime.json"),
174 secretsDir: join(stateDir, "secrets"),
175 };
176});
177​
178export const ensureServerDirectories = Effect.fn(function* (derivedPaths: ServerDerivedPaths) {
179 const fs = yield* FileSystem.FileSystem;
180 const path = yield* Path.Path;
181​
182 yield* Effect.all(
183 [
184 fs.makeDirectory(derivedPaths.stateDir, { recursive: true }),
185 fs.makeDirectory(derivedPaths.logsDir, { recursive: true }),
186 fs.makeDirectory(derivedPaths.providerLogsDir, { recursive: true }),
187 fs.makeDirectory(derivedPaths.terminalLogsDir, { recursive: true }),
188 fs.makeDirectory(derivedPaths.attachmentsDir, { recursive: true }),
189 fs.makeDirectory(derivedPaths.worktreesDir, { recursive: true }),
190 fs.makeDirectory(path.dirname(derivedPaths.keybindingsConfigPath), { recursive: true }),
191 fs.makeDirectory(path.dirname(derivedPaths.settingsPath), { recursive: true }),
192 fs.makeDirectory(derivedPaths.providerStatusCacheDir, { recursive: true }),
193 fs.makeDirectory(path.dirname(derivedPaths.anonymousIdPath), { recursive: true }),
194 fs.makeDirectory(path.dirname(derivedPaths.serverRuntimeStatePath), { recursive: true }),
195 ],
196 { concurrency: "unbounded" },
197 );
198 // Logs, traces and the database are written with the process umask; keep them owner-only.
199 yield* fs
200 .chmod(derivedPaths.stateDir, 0o700)
201 .pipe(
202 Effect.catch((cause) =>
203 Effect.logWarning("Could not restrict state directory permissions.", { cause }),
204 ),
205 );
206​
207 const swept = sweepStalePendingAttachments({
208 attachmentsDir: derivedPaths.attachmentsDir,
209 nowMs: yield* Clock.currentTimeMillis,
210 });
211 if (swept.deleted > 0) {
212 yield* Effect.logInfo("Removed expired attachment uploads.", { deleted: swept.deleted });
213 }
214});
215​
216const makeTest = Effect.fn("ServerConfig.makeTest")(function* (
217 cwd: string,
218 baseDirOrPrefix: string | { readonly prefix: string },
219) {
220 const devUrl = undefined;
221 const fs = yield* FileSystem.FileSystem;
222 const baseDir =
223 typeof baseDirOrPrefix === "string"
224 ? baseDirOrPrefix
225 : // macOS temp dirs live under the /var -> /private/var symlink; storage cleanup's
226 // symlink guards compare resolved paths, so generated test dirs must be real paths.
227 yield* fs.realPath(yield* fs.makeTempDirectoryScoped({ prefix: baseDirOrPrefix.prefix }));
228 const derivedPaths = yield* deriveServerPaths(baseDir, devUrl);
229 yield* ensureServerDirectories(derivedPaths);
230​
231 return ServerConfig.of({
232 logLevel: "Error",
233 traceMinLevel: "Info",
234 traceTimingEnabled: true,
235 traceBatchWindowMs: 200,
236 traceMaxBytes: 10 * 1024 * 1024,
237 traceMaxFiles: 10,
238 otlpTracesUrl: undefined,
239 otlpMetricsUrl: undefined,
240 otlpLogsUrl: undefined,
241 otlpTracesExport: DEFAULT_SIGNAL_EXPORT,
242 otlpMetricsExport: DEFAULT_SIGNAL_EXPORT,
243 otlpLogsExport: DEFAULT_SIGNAL_EXPORT,
244 otelEnvironment: OtelEnvironment.none,
245 cwd,
246 baseDir,
247 ...derivedPaths,
248 mode: "web",
249 autoBootstrapProjectFromCwd: false,
250 logWebSocketEvents: false,
251 tailscaleServeEnabled: false,
252 tailscaleServePort: 443,
253 port: 0,
254 host: undefined,
255 desktopBootstrapToken: undefined,
256 desktopTelemetryFd: undefined,
257 desktopTelemetryControlFd: undefined,
258 desktopBrowserFd: undefined,
259 desktopBrowserControlFd: undefined,
260 resourceMonitorPath: undefined,
261 staticDir: undefined,
262 devUrl,
263 devAllowedOrigins: [],
264 noBrowser: false,
265 startupPresentation: "browser",
266 });
267});
268​
269export const layerTest = (cwd: string, baseDirOrPrefix: string | { readonly prefix: string }) =>
270 Layer.effect(ServerConfig, makeTest(cwd, baseDirOrPrefix));
271​
272export const resolveStaticDir = Effect.fn(function* () {
273 const { join, resolve } = yield* Path.Path;
274 const { exists } = yield* FileSystem.FileSystem;
275 const bundledClient = resolve(join(import.meta.dirname, "client"));
276 const bundledStat = yield* exists(join(bundledClient, "index.html")).pipe(
277 Effect.orElseSucceed(() => false),
278 );
279 if (bundledStat) {
280 return bundledClient;
281 }
282​
283 const monorepoClient = resolve(join(import.meta.dirname, "../../web/dist"));
284 const monorepoStat = yield* exists(join(monorepoClient, "index.html")).pipe(
285 Effect.orElseSucceed(() => false),
286 );
287 if (monorepoStat) {
288 return monorepoClient;
289 }
290 return undefined;
291});