@linzumi/cli 0.0.5-beta → 0.0.6-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,500 @@
1
+ /*
2
+ - Date: 2026-04-26
3
+ Spec: plans/2026-04-26-local-runner-forwarding-and-editor-plan.md
4
+ Relationship: Implements the local side of bounded HTTP preview forwarding,
5
+ enforcing explicit allowed ports before the runner contacts loopback.
6
+
7
+ - Date: 2026-04-26
8
+ Spec: plans/2026-04-26-local-runner-subdomain-forwarding-epr.md
9
+ Relationship: Implements the runner side of local WebSocket forwarding for
10
+ isolated preview subdomains.
11
+ */
12
+ import { gzipSync } from "node:zlib";
13
+ import { type JsonObject, type JsonValue, isJsonObject } from "./protocol";
14
+ import type { PhoenixClient } from "./phoenix";
15
+
16
+ const maxForwardBodyBytes = 64 * 1024 * 1024;
17
+ const gzipForwardThresholdBytes = 32 * 1024;
18
+
19
+ export type ForwardHttpRequestControl = {
20
+ readonly type: "forward_http_request";
21
+ readonly requestId: string;
22
+ readonly port: number;
23
+ readonly method: string;
24
+ readonly path: string;
25
+ readonly queryString?: string;
26
+ readonly headers?: JsonValue;
27
+ readonly bodyBase64?: string;
28
+ };
29
+
30
+ export type ForwardHttpResponsePayload = JsonObject;
31
+
32
+ type RequestBodyDecision =
33
+ | { readonly ok: true; readonly body: Uint8Array | undefined }
34
+ | { readonly ok: false; readonly error: string };
35
+
36
+ export type ForwardWebSocketOpenControl = {
37
+ readonly type: "forward_websocket_open";
38
+ readonly socketId: string;
39
+ readonly port: number;
40
+ readonly path: string;
41
+ readonly queryString?: string;
42
+ readonly headers?: JsonValue;
43
+ };
44
+
45
+ export type ForwardWebSocketSendControl = {
46
+ readonly type: "forward_websocket_send";
47
+ readonly socketId: string;
48
+ readonly opcode: "text" | "binary" | "ping" | "pong";
49
+ readonly bodyBase64: string;
50
+ };
51
+
52
+ export type ForwardWebSocketCloseControl = {
53
+ readonly type: "forward_websocket_close";
54
+ readonly socketId: string;
55
+ };
56
+
57
+ export type ForwardWebSocketControl =
58
+ | ForwardWebSocketOpenControl
59
+ | ForwardWebSocketSendControl
60
+ | ForwardWebSocketCloseControl;
61
+
62
+ export type ForwardWebSocketManager = {
63
+ readonly handle: (control: ForwardWebSocketControl) => void;
64
+ readonly close: () => void;
65
+ };
66
+
67
+ export async function handleForwardHttpRequest(
68
+ control: ForwardHttpRequestControl,
69
+ allowedPorts: readonly number[],
70
+ ): Promise<ForwardHttpResponsePayload> {
71
+ if (!allowedPorts.includes(control.port)) {
72
+ return forwardError(control.requestId, "forward_port_not_allowed");
73
+ }
74
+
75
+ const bodyDecision = requestBody(control);
76
+
77
+ if (!bodyDecision.ok) {
78
+ return forwardError(control.requestId, bodyDecision.error);
79
+ }
80
+
81
+ try {
82
+ const request = {
83
+ method: control.method,
84
+ headers: requestHeaders(control.headers),
85
+ ...(bodyDecision.body === undefined ? {} : { body: bodyDecision.body }),
86
+ };
87
+ const response = await fetchWithHttpsFallback(
88
+ control.port,
89
+ control.path,
90
+ control.queryString,
91
+ request,
92
+ );
93
+ const upstreamBuffer = Buffer.from(await response.arrayBuffer());
94
+ const patchedBody = patchForwardBody(
95
+ control.path,
96
+ response.headers,
97
+ upstreamBuffer,
98
+ );
99
+ const preparedResponse = prepareResponseBodyForChannel(
100
+ control,
101
+ response.headers,
102
+ patchedBody,
103
+ );
104
+
105
+ if (preparedResponse.body.byteLength > maxForwardBodyBytes) {
106
+ return forwardError(control.requestId, "forward_response_too_large");
107
+ }
108
+
109
+ return {
110
+ requestId: control.requestId,
111
+ ok: true,
112
+ status: response.status,
113
+ headers: preparedResponse.headers,
114
+ bodyBase64: preparedResponse.body.toString("base64"),
115
+ };
116
+ } catch (_error) {
117
+ return forwardError(control.requestId, "forward_target_unavailable");
118
+ }
119
+ }
120
+
121
+ export function isForwardHttpRequestControl(control: {
122
+ readonly type: string;
123
+ }): control is ForwardHttpRequestControl {
124
+ return control.type === "forward_http_request";
125
+ }
126
+
127
+ export function isForwardWebSocketControl(control: {
128
+ readonly type: string;
129
+ }): control is ForwardWebSocketControl {
130
+ return (
131
+ control.type === "forward_websocket_open" ||
132
+ control.type === "forward_websocket_send" ||
133
+ control.type === "forward_websocket_close"
134
+ );
135
+ }
136
+
137
+ export function createForwardWebSocketManager(
138
+ kandan: Pick<PhoenixClient, "push">,
139
+ topic: string,
140
+ allowedPorts: () => readonly number[],
141
+ ): ForwardWebSocketManager {
142
+ const sockets = new Map<string, WebSocket>();
143
+
144
+ const pushEvent = (payload: JsonObject) =>
145
+ kandan
146
+ .push(topic, "forward:websocket_event", payload)
147
+ .catch(() => undefined);
148
+
149
+ const closeSocket = (socketId: string) => {
150
+ const socket = sockets.get(socketId);
151
+ sockets.delete(socketId);
152
+ socket?.close();
153
+ };
154
+
155
+ return {
156
+ handle: (control) => {
157
+ switch (control.type) {
158
+ case "forward_websocket_open": {
159
+ if (!allowedPorts().includes(control.port)) {
160
+ void pushEvent({
161
+ socketId: control.socketId,
162
+ type: "error",
163
+ error: "forward_port_not_allowed",
164
+ });
165
+ return;
166
+ }
167
+
168
+ openLocalWebSocket(control, sockets, pushEvent, "ws");
169
+ return;
170
+ }
171
+ case "forward_websocket_send": {
172
+ const socket = sockets.get(control.socketId);
173
+ if (socket === undefined || socket.readyState !== WebSocket.OPEN) {
174
+ void pushEvent({
175
+ socketId: control.socketId,
176
+ type: "error",
177
+ error: "websocket_not_open",
178
+ });
179
+ return;
180
+ }
181
+
182
+ const body = Buffer.from(control.bodyBase64, "base64");
183
+ switch (control.opcode) {
184
+ case "text":
185
+ socket.send(body.toString());
186
+ return;
187
+ case "binary":
188
+ case "ping":
189
+ case "pong":
190
+ socket.send(body);
191
+ return;
192
+ }
193
+ }
194
+ case "forward_websocket_close":
195
+ closeSocket(control.socketId);
196
+ return;
197
+ }
198
+ },
199
+ close: () => {
200
+ for (const socketId of sockets.keys()) {
201
+ closeSocket(socketId);
202
+ }
203
+ },
204
+ };
205
+ }
206
+
207
+ function openLocalWebSocket(
208
+ control: ForwardWebSocketOpenControl,
209
+ sockets: Map<string, WebSocket>,
210
+ pushEvent: (payload: JsonObject) => Promise<void | JsonValue>,
211
+ scheme: "ws" | "wss",
212
+ ): void {
213
+ let opened = false;
214
+ const websocket = new WebSocket(
215
+ localForwardUrl(
216
+ scheme === "ws" ? "http" : "https",
217
+ control.port,
218
+ control.path,
219
+ control.queryString,
220
+ ).replace(/^http/, scheme),
221
+ );
222
+ sockets.set(control.socketId, websocket);
223
+ websocket.addEventListener("open", () => {
224
+ opened = true;
225
+ void pushEvent({ socketId: control.socketId, type: "open" });
226
+ });
227
+ websocket.addEventListener("message", (event) => {
228
+ const body =
229
+ typeof event.data === "string"
230
+ ? Buffer.from(event.data)
231
+ : Buffer.from(event.data as ArrayBuffer);
232
+ void pushEvent({
233
+ socketId: control.socketId,
234
+ type: "message",
235
+ opcode: typeof event.data === "string" ? "text" : "binary",
236
+ bodyBase64: body.toString("base64"),
237
+ });
238
+ });
239
+ websocket.addEventListener("close", (event) => {
240
+ sockets.delete(control.socketId);
241
+ void pushEvent({
242
+ socketId: control.socketId,
243
+ type: "close",
244
+ code: event.code,
245
+ reason: event.reason,
246
+ });
247
+ });
248
+ websocket.addEventListener("error", () => {
249
+ sockets.delete(control.socketId);
250
+ if (!opened && scheme === "ws") {
251
+ openLocalWebSocket(control, sockets, pushEvent, "wss");
252
+ return;
253
+ }
254
+
255
+ void pushEvent({
256
+ socketId: control.socketId,
257
+ type: "error",
258
+ error: "websocket_error",
259
+ });
260
+ });
261
+ }
262
+
263
+ function localForwardUrl(
264
+ scheme: "http" | "https",
265
+ port: number,
266
+ path: string,
267
+ queryString: string | undefined,
268
+ ): string {
269
+ const normalizedPath = path.startsWith("/") ? path : `/${path}`;
270
+ const url = new URL(`${scheme}://127.0.0.1:${port}${normalizedPath}`);
271
+
272
+ if (queryString !== undefined && queryString.trim() !== "") {
273
+ url.search = queryString;
274
+ }
275
+
276
+ return url.toString();
277
+ }
278
+
279
+ async function fetchWithHttpsFallback(
280
+ port: number,
281
+ path: string,
282
+ queryString: string | undefined,
283
+ request: RequestInit,
284
+ ): Promise<Response> {
285
+ try {
286
+ return await fetch(
287
+ localForwardUrl("http", port, path, queryString),
288
+ request,
289
+ );
290
+ } catch (httpError) {
291
+ try {
292
+ return await fetch(
293
+ localForwardUrl("https", port, path, queryString),
294
+ request,
295
+ );
296
+ } catch (_httpsError) {
297
+ throw httpError;
298
+ }
299
+ }
300
+ }
301
+
302
+ function requestBody(
303
+ control: ForwardHttpRequestControl,
304
+ ): RequestBodyDecision {
305
+ if (control.method === "GET" || control.method === "HEAD") {
306
+ return { ok: true, body: undefined };
307
+ }
308
+
309
+ if (control.bodyBase64 === undefined || control.bodyBase64 === "") {
310
+ return { ok: true, body: undefined };
311
+ }
312
+
313
+ const body = decodeBase64Body(control.bodyBase64);
314
+
315
+ if (body === undefined) {
316
+ return { ok: false, error: "invalid_forward_request" };
317
+ }
318
+
319
+ if (body.byteLength > maxForwardBodyBytes) {
320
+ return { ok: false, error: "forward_body_too_large" };
321
+ }
322
+
323
+ return { ok: true, body };
324
+ }
325
+
326
+ function decodeBase64Body(value: string): Uint8Array | undefined {
327
+ const normalized = value.trim();
328
+
329
+ if (
330
+ normalized.length % 4 === 1 ||
331
+ !/^[A-Za-z0-9+/]*={0,2}$/.test(normalized)
332
+ ) {
333
+ return undefined;
334
+ }
335
+
336
+ const body = Buffer.from(normalized, "base64");
337
+ const canonicalBody = body.toString("base64").replace(/=+$/, "");
338
+ const canonicalInput = normalized.replace(/=+$/, "");
339
+
340
+ return canonicalBody === canonicalInput ? body : undefined;
341
+ }
342
+
343
+ function requestHeaders(headers: JsonValue | undefined): Headers {
344
+ const result = new Headers();
345
+
346
+ if (!Array.isArray(headers)) {
347
+ return result;
348
+ }
349
+
350
+ for (const header of headers) {
351
+ if (isHeader(header)) {
352
+ result.set(header.name, header.value);
353
+ }
354
+ }
355
+
356
+ return result;
357
+ }
358
+
359
+ function responseHeaders(headers: Headers): JsonValue[] {
360
+ return Array.from(headers.entries())
361
+ .filter(([name]) => !blockedForwardHeaderNames.has(name.toLowerCase()))
362
+ .slice(0, 40)
363
+ .map(([name, value]) => ({ name, value }));
364
+ }
365
+
366
+ function patchForwardBody(path: string, headers: Headers, body: Buffer): Buffer {
367
+ if (!codeServerWorkbenchScript(path, headers)) {
368
+ return body;
369
+ }
370
+
371
+ return Buffer.from(
372
+ body
373
+ .toString("utf8")
374
+ .replace(
375
+ "message:h(3783,null,\"code-server\")",
376
+ "message:h(3783,null,\"Kandan\")",
377
+ ),
378
+ );
379
+ }
380
+
381
+ function prepareResponseBodyForChannel(
382
+ control: ForwardHttpRequestControl,
383
+ headers: Headers,
384
+ body: Buffer,
385
+ ): { readonly body: Buffer; readonly headers: JsonValue[] } {
386
+ const forwardedHeaders = responseHeaders(headers);
387
+
388
+ if (!shouldGzipResponse(control, headers, body)) {
389
+ return { body, headers: forwardedHeaders };
390
+ }
391
+
392
+ return {
393
+ body: gzipSync(body),
394
+ headers: [
395
+ ...forwardedHeaders,
396
+ { name: "content-encoding", value: "gzip" },
397
+ { name: "vary", value: "accept-encoding" },
398
+ ],
399
+ };
400
+ }
401
+
402
+ function shouldGzipResponse(
403
+ control: ForwardHttpRequestControl,
404
+ headers: Headers,
405
+ body: Buffer,
406
+ ): boolean {
407
+ if (
408
+ control.method === "HEAD" ||
409
+ body.byteLength < gzipForwardThresholdBytes ||
410
+ !clientAcceptsGzip(control.headers)
411
+ ) {
412
+ return false;
413
+ }
414
+
415
+ return compressibleResponse(control.path, headers);
416
+ }
417
+
418
+ function clientAcceptsGzip(headers: JsonValue | undefined): boolean {
419
+ if (!Array.isArray(headers)) {
420
+ return false;
421
+ }
422
+
423
+ return headers.some(
424
+ header =>
425
+ isWireHeader(header) &&
426
+ header.name.toLowerCase() === "accept-encoding" &&
427
+ header.value
428
+ .toLowerCase()
429
+ .split(",")
430
+ .map(value => value.trim())
431
+ .some(value => value === "gzip" || value.startsWith("gzip;")),
432
+ );
433
+ }
434
+
435
+ function compressibleResponse(path: string, headers: Headers): boolean {
436
+ const contentType = headers.get("content-type")?.toLowerCase() ?? "";
437
+
438
+ if (
439
+ contentType.startsWith("text/") ||
440
+ contentType.includes("javascript") ||
441
+ contentType.includes("json") ||
442
+ contentType.includes("xml")
443
+ ) {
444
+ return true;
445
+ }
446
+
447
+ return /\.(?:css|html|js|json|map|mjs|svg|txt)$/i.test(path);
448
+ }
449
+
450
+ function codeServerWorkbenchScript(path: string, headers: Headers): boolean {
451
+ return (
452
+ path.endsWith("/out/vs/code/browser/workbench/workbench.js") &&
453
+ (headers.get("content-type") ?? "").toLowerCase().includes("javascript")
454
+ );
455
+ }
456
+
457
+ function isHeader(
458
+ value: JsonValue,
459
+ ): value is { readonly name: string; readonly value: string } {
460
+ return (
461
+ isWireHeader(value) &&
462
+ !blockedForwardHeaderNames.has(value.name.toLowerCase())
463
+ );
464
+ }
465
+
466
+ function isWireHeader(
467
+ value: JsonValue,
468
+ ): value is { readonly name: string; readonly value: string } {
469
+ return (
470
+ isJsonObject(value) &&
471
+ typeof value.name === "string" &&
472
+ typeof value.value === "string"
473
+ );
474
+ }
475
+
476
+ function forwardError(
477
+ requestId: string,
478
+ error: string,
479
+ ): ForwardHttpResponsePayload {
480
+ return {
481
+ requestId,
482
+ ok: false,
483
+ error,
484
+ };
485
+ }
486
+
487
+ const blockedForwardHeaderNames = new Set([
488
+ "accept-encoding",
489
+ "connection",
490
+ "content-encoding",
491
+ "content-length",
492
+ "host",
493
+ "keep-alive",
494
+ "proxy-authenticate",
495
+ "proxy-authorization",
496
+ "te",
497
+ "trailer",
498
+ "transfer-encoding",
499
+ "upgrade",
500
+ ]);
package/src/oauth.ts CHANGED
@@ -11,6 +11,7 @@ export type LocalRunnerOAuthOptions = {
11
11
  readonly kandanUrl: string;
12
12
  readonly workspaceSlug?: string | undefined;
13
13
  readonly channelSlug?: string | undefined;
14
+ readonly onboarding?: "start" | undefined;
14
15
  readonly callbackHost?: string | undefined;
15
16
  readonly openAuthorizationUrl?: ((url: string) => Promise<void> | void) | undefined;
16
17
  };
@@ -18,6 +19,15 @@ export type LocalRunnerOAuthOptions = {
18
19
  export type LocalRunnerOAuthToken = {
19
20
  readonly accessToken: string;
20
21
  readonly expiresInSeconds?: number | undefined;
22
+ readonly workspaceSlug?: string | undefined;
23
+ readonly channelSlug?: string | undefined;
24
+ };
25
+
26
+ export type LocalRunnerStartTarget = {
27
+ readonly workspaceSlug: string;
28
+ readonly channelSlug: string;
29
+ readonly workspaceName: string;
30
+ readonly channelName: string;
21
31
  };
22
32
 
23
33
  export async function acquireLocalRunnerToken(
@@ -41,6 +51,7 @@ export async function acquireLocalRunnerTokenDetails(
41
51
  state,
42
52
  workspaceSlug: options.workspaceSlug,
43
53
  channelSlug: options.channelSlug,
54
+ onboarding: options.onboarding,
44
55
  });
45
56
 
46
57
  process.stderr.write(`Authorize the local Codex runner:\n${authorizeUrl}\n`);
@@ -103,6 +114,52 @@ export async function validateLocalRunnerToken(args: {
103
114
  }
104
115
  }
105
116
 
117
+ export async function fetchLocalRunnerStartTarget(args: {
118
+ readonly kandanUrl: string;
119
+ readonly accessToken: string;
120
+ }): Promise<LocalRunnerStartTarget> {
121
+ const response = await fetch(
122
+ new URL(
123
+ "/api/v2/local-codex-runner/onboarding/start",
124
+ kandanHttpBaseUrl(args.kandanUrl),
125
+ ),
126
+ {
127
+ method: "GET",
128
+ headers: { authorization: `Bearer ${args.accessToken}` },
129
+ },
130
+ );
131
+ const body: unknown = await response.json();
132
+
133
+ if (!response.ok || typeof body !== "object" || body === null) {
134
+ throw new Error(`local runner start target failed with HTTP ${response.status}`);
135
+ }
136
+
137
+ const workspace = "workspace" in body ? body.workspace : undefined;
138
+ const channel = "channel" in body ? body.channel : undefined;
139
+ const workspaceName = "workspace_name" in body ? body.workspace_name : undefined;
140
+ const channelName = "channel_name" in body ? body.channel_name : undefined;
141
+
142
+ if (
143
+ typeof workspace !== "string" ||
144
+ workspace.trim() === "" ||
145
+ typeof channel !== "string" ||
146
+ channel.trim() === "" ||
147
+ typeof workspaceName !== "string" ||
148
+ workspaceName.trim() === "" ||
149
+ typeof channelName !== "string" ||
150
+ channelName.trim() === ""
151
+ ) {
152
+ throw new Error("local runner start target response was incomplete");
153
+ }
154
+
155
+ return {
156
+ workspaceSlug: workspace,
157
+ channelSlug: channel,
158
+ workspaceName,
159
+ channelName,
160
+ };
161
+ }
162
+
106
163
  export function kandanHttpBaseUrl(kandanUrl: string): string {
107
164
  const parsed = new URL(kandanUrl);
108
165
 
@@ -173,6 +230,7 @@ function authorizationUrl(args: {
173
230
  readonly state: string;
174
231
  readonly workspaceSlug?: string | undefined;
175
232
  readonly channelSlug?: string | undefined;
233
+ readonly onboarding?: "start" | undefined;
176
234
  }): string {
177
235
  const url = new URL("/api/v2/local-codex-runner/oauth/authorize", args.httpBaseUrl);
178
236
  url.searchParams.set("redirect_uri", args.redirectUri);
@@ -186,6 +244,10 @@ function authorizationUrl(args: {
186
244
  url.searchParams.set("channel", args.channelSlug);
187
245
  }
188
246
 
247
+ if (args.onboarding !== undefined) {
248
+ url.searchParams.set("onboarding", args.onboarding);
249
+ }
250
+
189
251
  return url.toString();
190
252
  }
191
253
 
@@ -222,9 +284,17 @@ async function exchangeCodeForToken(args: {
222
284
  return {
223
285
  accessToken: token,
224
286
  expiresInSeconds: typeof expiresIn === "number" ? expiresIn : undefined,
287
+ workspaceSlug: stringBodyField(body, "workspace"),
288
+ channelSlug: stringBodyField(body, "channel"),
225
289
  };
226
290
  }
227
291
 
292
+ function stringBodyField(body: object, key: string): string | undefined {
293
+ const value = key in body ? body[key as keyof typeof body] : undefined;
294
+
295
+ return typeof value === "string" && value.trim() !== "" ? value : undefined;
296
+ }
297
+
228
298
  function startCallbackServer(args: {
229
299
  readonly host: string;
230
300
  }): Promise<{
@@ -236,9 +306,11 @@ function startCallbackServer(args: {
236
306
  let resolveCallback:
237
307
  | ((value: { readonly code: string; readonly state: string }) => void)
238
308
  | undefined;
309
+ let rejectCallback: ((reason?: unknown) => void) | undefined;
239
310
  const callbackPromise = new Promise<{ readonly code: string; readonly state: string }>(
240
- (callbackResolve) => {
311
+ (callbackResolve, callbackReject) => {
241
312
  resolveCallback = callbackResolve;
313
+ rejectCallback = callbackReject;
242
314
  },
243
315
  );
244
316
 
@@ -249,14 +321,30 @@ function startCallbackServer(args: {
249
321
  const url = new URL(request.url);
250
322
  const code = url.searchParams.get("code");
251
323
  const state = url.searchParams.get("state");
324
+ const error = url.searchParams.get("error");
325
+
326
+ if (error !== null && error.trim() !== "") {
327
+ rejectCallback?.(new Error(`local runner OAuth failed: ${error}`));
328
+ return oauthResultHtml({
329
+ title: "Linzumi CLI was not authorized",
330
+ body: "You denied the request. You can close this tab and rerun linzumi start when you are ready.",
331
+ status: 403,
332
+ });
333
+ }
252
334
 
253
335
  if (code === null || state === null || code.trim() === "" || state.trim() === "") {
254
- return new Response("Missing local runner authorization code.", { status: 400 });
336
+ return oauthResultHtml({
337
+ title: "Authorization callback was incomplete",
338
+ body: "Kandan did not send the local runner authorization code. Return to your terminal and try again.",
339
+ status: 400,
340
+ });
255
341
  }
256
342
 
257
343
  resolveCallback?.({ code, state });
258
- return new Response("Kandan local Codex runner authorized. You can close this tab.", {
259
- headers: { "content-type": "text/plain; charset=utf-8" },
344
+ return oauthResultHtml({
345
+ title: "Linzumi CLI is connected",
346
+ body: "You can close this tab and return to the terminal. Kandan will finish starting the local runner.",
347
+ status: 200,
260
348
  });
261
349
  },
262
350
  });
@@ -271,6 +359,49 @@ function startCallbackServer(args: {
271
359
  });
272
360
  }
273
361
 
362
+ function oauthResultHtml(args: {
363
+ readonly title: string;
364
+ readonly body: string;
365
+ readonly status: number;
366
+ }): Response {
367
+ return new Response(
368
+ `<!doctype html>
369
+ <html>
370
+ <head>
371
+ <meta charset="utf-8" />
372
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
373
+ <title>${escapeHtml(args.title)}</title>
374
+ <style>
375
+ :root { color-scheme: light dark; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
376
+ body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: #f6f7f9; color: #13161c; }
377
+ main { width: min(520px, calc(100vw - 32px)); border: 1px solid #d9dde5; border-radius: 12px; background: #fff; padding: 30px; box-shadow: 0 24px 70px rgba(18, 25, 38, 0.12); }
378
+ h1 { margin: 0 0 10px; font-size: 24px; line-height: 1.2; }
379
+ p { margin: 0; color: #465160; line-height: 1.55; }
380
+ @media (prefers-color-scheme: dark) {
381
+ body { background: #0f131a; color: #f3f5f8; }
382
+ main { background: #171c25; border-color: #2b3442; box-shadow: 0 24px 70px rgba(0, 0, 0, 0.4); }
383
+ p { color: #aab4c2; }
384
+ }
385
+ </style>
386
+ </head>
387
+ <body>
388
+ <main>
389
+ <h1>${escapeHtml(args.title)}</h1>
390
+ <p>${escapeHtml(args.body)}</p>
391
+ </main>
392
+ </body>
393
+ </html>`,
394
+ {
395
+ status: args.status,
396
+ headers: { "content-type": "text/html; charset=utf-8" },
397
+ },
398
+ );
399
+ }
400
+
401
+ function escapeHtml(value: string): string {
402
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
403
+ }
404
+
274
405
  function openBrowser(url: string): Promise<void> {
275
406
  const command =
276
407
  process.platform === "darwin"