@tsconfigdotjson/vibe-os 0.0.0-stage → 0.2.0-rc.1

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.
Files changed (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +988 -2
  3. package/THIRD_PARTY_NOTICES.md +2467 -0
  4. package/bin/vibe-os.mjs +13 -0
  5. package/dist/web/apple-touch-icon.png +0 -0
  6. package/dist/web/assets/index-B77Qk8B5.js +35 -0
  7. package/dist/web/assets/index-B77Qk8B5.js.br +0 -0
  8. package/dist/web/assets/index-B77Qk8B5.js.gz +0 -0
  9. package/dist/web/assets/index-CH5Em-Mo.css +1 -0
  10. package/dist/web/assets/index-CH5Em-Mo.css.br +0 -0
  11. package/dist/web/assets/index-CH5Em-Mo.css.gz +0 -0
  12. package/dist/web/icon-192.png +0 -0
  13. package/dist/web/icon-512.png +0 -0
  14. package/dist/web/icon-app.svg +19 -0
  15. package/dist/web/icon-maskable-512.png +0 -0
  16. package/dist/web/icon-maskable.svg +13 -0
  17. package/dist/web/icon.svg +15 -0
  18. package/dist/web/index.html +36 -0
  19. package/dist/web/ssh.wasm +0 -0
  20. package/dist/web/ssh.wasm.br +0 -0
  21. package/dist/web/ssh.wasm.gz +0 -0
  22. package/dist/web/sw.js +29 -0
  23. package/dist/web/wasm_exec.js +575 -0
  24. package/dist/web/wasm_exec.js.br +0 -0
  25. package/dist/web/wasm_exec.js.gz +0 -0
  26. package/package.json +82 -4
  27. package/scripts/compile.ts +62 -0
  28. package/scripts/compress.mjs +54 -0
  29. package/scripts/fetch-wasm.mjs +242 -0
  30. package/scripts/gen-assets.ts +76 -0
  31. package/scripts/lib/walk.ts +18 -0
  32. package/scripts/notices.ts +189 -0
  33. package/server/acme.ts +182 -0
  34. package/server/api.ts +515 -0
  35. package/server/assets.generated.ts +53 -0
  36. package/server/attach.ts +524 -0
  37. package/server/auth.ts +194 -0
  38. package/server/bridge.ts +145 -0
  39. package/server/browser.ts +519 -0
  40. package/server/cli.ts +857 -0
  41. package/server/config.ts +385 -0
  42. package/server/db.ts +262 -0
  43. package/server/doctor.ts +977 -0
  44. package/server/fsx.ts +88 -0
  45. package/server/harness.ts +655 -0
  46. package/server/http.ts +53 -0
  47. package/server/index.ts +469 -0
  48. package/server/log.ts +40 -0
  49. package/server/main.ts +7 -0
  50. package/server/mcp.ts +291 -0
  51. package/server/preflight.ts +87 -0
  52. package/server/profiles.ts +238 -0
  53. package/server/projects.ts +721 -0
  54. package/server/reaper.ts +303 -0
  55. package/server/runtime.ts +64 -0
  56. package/server/session.ts +330 -0
  57. package/server/setup.ts +1151 -0
  58. package/server/ssh-ca.ts +368 -0
  59. package/server/static.ts +147 -0
  60. package/server/wallpapers.ts +248 -0
  61. package/server/windows.ts +237 -0
  62. package/shared/args.ts +91 -0
  63. package/shared/grid.ts +32 -0
  64. package/shared/wire.ts +226 -0
@@ -0,0 +1,368 @@
1
+ // vibe-os is its own short-lived SSH certificate authority.
2
+ //
3
+ // The browser generates an ed25519 keypair inside the WASM sandbox and keeps
4
+ // the private half in IndexedDB — it never leaves the tab. It POSTs only the
5
+ // public half here, and gets back a certificate signed by this CA, valid for a
6
+ // few hours. The VPS trusts exactly one line in ~/.ssh/authorized_keys:
7
+ //
8
+ // cert-authority ssh-ed25519 AAAA... vibe-os-ca
9
+ //
10
+ // That is the whole enrollment story: no key to copy and paste, no private key
11
+ // on the wire, no sshd_config edit, and no root. Revoking access to every
12
+ // browser that ever connected is deleting that one line.
13
+ //
14
+ // Certificates also carry a per-pane `force-command` critical option, which is
15
+ // how each pane lands in its own persistent dtach session (see certificate.ts's
16
+ // caller). sshd allocates the PTY because the client asks for a shell, then
17
+ // runs the forced command inside it — so resize and job control behave.
18
+
19
+ import { execFile } from "node:child_process";
20
+ import { createHash } from "node:crypto";
21
+ import {
22
+ appendFile,
23
+ chmod,
24
+ mkdir,
25
+ mkdtemp,
26
+ readFile,
27
+ rm,
28
+ writeFile,
29
+ } from "node:fs/promises";
30
+ import os from "node:os";
31
+ import path from "node:path";
32
+ import { promisify } from "node:util";
33
+ import { pathExists } from "./fsx.ts";
34
+ import { log } from "./log.ts";
35
+
36
+ const run = promisify(execFile);
37
+
38
+ /** Public key types we are willing to sign. Certificates are rejected. */
39
+ const SIGNABLE_KEY_TYPE =
40
+ /^(ssh-ed25519|ecdsa-sha2-nistp(256|384|521)|ssh-rsa|sk-ssh-ed25519@openssh\.com|sk-ecdsa-sha2-nistp256@openssh\.com)$/;
41
+
42
+ /**
43
+ * Ceiling on a posted public key. Exported because `server/api.ts` pre-checks
44
+ * `content-length` against it: two constants meant one limit rejected at the
45
+ * HTTP edge and a different, smaller one rejected here with the same message.
46
+ */
47
+ export const MAX_PUBKEY_BYTES = 8 * 1024;
48
+
49
+ export interface SignOptions {
50
+ /** Public key in authorized_keys form, as posted by the browser. */
51
+ publicKey: string;
52
+ /** Unix user the certificate is valid for. */
53
+ principal: string;
54
+ /** Human-readable identity; sshd logs this on every login. */
55
+ identity: string;
56
+ /** Optional forced command, e.g. `dtach new-session -A -s vibe-1`. */
57
+ forceCommand?: string;
58
+ /** Certificate lifetime in seconds. */
59
+ ttlSeconds: number;
60
+ }
61
+
62
+ /**
63
+ * Confirms ssh-keygen is on PATH.
64
+ *
65
+ * The probe has to be side-effect free: several of ssh-keygen's flags generate
66
+ * key material as a side effect (`-A` writes host keys), so this asks it to
67
+ * fingerprint a path that cannot exist. That always exits non-zero — the only
68
+ * outcome we care about is whether the binary was found at all.
69
+ */
70
+ export async function requireSshKeygen(): Promise<void> {
71
+ try {
72
+ await run("ssh-keygen", ["-l", "-f", "/nonexistent/vibe-os-probe"], {
73
+ timeout: 5_000,
74
+ });
75
+ } catch (err: unknown) {
76
+ if ((err as NodeJS.ErrnoException)?.code === "ENOENT") {
77
+ throw new Error(
78
+ "ssh-keygen was not found on PATH — install openssh-client",
79
+ );
80
+ }
81
+ }
82
+ }
83
+
84
+ export class SshCa {
85
+ readonly keyPath: string;
86
+ readonly pubPath: string;
87
+ private publicKeyLine = "";
88
+ private serialCounter = 0n;
89
+
90
+ constructor(stateDir: string) {
91
+ this.keyPath = path.join(stateDir, "ca");
92
+ this.pubPath = path.join(stateDir, "ca.pub");
93
+ }
94
+
95
+ /** The CA's public key in authorized_keys form, without the trailing newline. */
96
+ get publicKey(): string {
97
+ if (!this.publicKeyLine)
98
+ throw new Error("CA not initialised — call ensure() first");
99
+ return this.publicKeyLine;
100
+ }
101
+
102
+ /** Creates the CA keypair if it does not exist yet. Idempotent. */
103
+ async ensure(): Promise<void> {
104
+ await mkdir(path.dirname(this.keyPath), { recursive: true, mode: 0o700 });
105
+ await chmod(path.dirname(this.keyPath), 0o700).catch(() => {});
106
+
107
+ if (!(await pathExists(this.keyPath))) {
108
+ // ssh-keygen refuses to overwrite without prompting, so a stale .pub with
109
+ // no private half has to go first.
110
+ await rm(this.pubPath, { force: true });
111
+ await run("ssh-keygen", [
112
+ "-q",
113
+ "-t",
114
+ "ed25519",
115
+ "-f",
116
+ this.keyPath,
117
+ "-N",
118
+ "",
119
+ "-C",
120
+ `vibe-os-ca@${os.hostname()}`,
121
+ ]);
122
+ log.ok(`generated SSH certificate authority at ${this.keyPath}`);
123
+ }
124
+
125
+ // Outside the branch above on purpose. A key restored from a backup, copied
126
+ // from another box, or written by an older version under a loose umask
127
+ // would otherwise keep whatever mode it arrived with — and ssh-keygen signs
128
+ // with a world-readable CA without complaining. This is the one key whose
129
+ // disclosure mints certificates for any principal.
130
+ await chmod(this.keyPath, 0o600).catch(() => {});
131
+
132
+ this.publicKeyLine = (await readFile(this.pubPath, "utf8")).trim();
133
+ if (!this.publicKeyLine) throw new Error(`${this.pubPath} is empty`);
134
+ }
135
+
136
+ /**
137
+ * Adds the `cert-authority` line to authorized_keys so sshd will accept any
138
+ * certificate this CA signs. Idempotent — matches on the key blob, so a
139
+ * hand-edited comment or extra options on the line are left alone.
140
+ */
141
+ async trustInAuthorizedKeys(homeDir: string): Promise<"added" | "present"> {
142
+ const sshDir = path.join(homeDir, ".ssh");
143
+ const file = path.join(sshDir, "authorized_keys");
144
+
145
+ await mkdir(sshDir, { recursive: true, mode: 0o700 });
146
+ await chmod(sshDir, 0o700).catch(() => {});
147
+
148
+ const blob = this.publicKey.split(/\s+/)[1];
149
+ if (!blob) throw new Error("malformed CA public key");
150
+
151
+ let current = "";
152
+ if (await pathExists(file)) current = await readFile(file, "utf8");
153
+ if (current.includes(blob)) return "present";
154
+
155
+ const prefix = current.length === 0 || current.endsWith("\n") ? "" : "\n";
156
+ await appendFile(
157
+ file,
158
+ `${prefix}# added by vibe-os — trusts browser-held keys certified by this host\ncert-authority ${this.publicKey}\n`,
159
+ { mode: 0o600 },
160
+ );
161
+ await chmod(file, 0o600).catch(() => {});
162
+ return "added";
163
+ }
164
+
165
+ /**
166
+ * Signs a browser-supplied public key.
167
+ *
168
+ * Everything reaching ssh-keygen goes through an argv array, never a shell,
169
+ * so a hostile `forceCommand` or public key cannot break out into command
170
+ * execution. The key itself is still validated because ssh-keygen will
171
+ * happily sign anything shaped like a key, and we do not want to mint
172
+ * certificates from certificates.
173
+ */
174
+ async signUserCert(opts: SignOptions): Promise<string> {
175
+ const publicKey = normalisePublicKey(opts.publicKey);
176
+
177
+ // mkdtemp, not a name we compose ourselves: it creates the directory
178
+ // atomically with an unpredictable suffix, so nothing in a world-writable
179
+ // /tmp can pre-create the path (or a symlink at it) and have us write key
180
+ // material somewhere else.
181
+ const tmp = await mkdtemp(path.join(os.tmpdir(), "vibe-os-sign-"));
182
+ await chmod(tmp, 0o700).catch(() => {});
183
+ const keyFile = path.join(tmp, "id.pub");
184
+ const certFile = path.join(tmp, "id-cert.pub");
185
+
186
+ try {
187
+ await writeFile(keyFile, `${publicKey}\n`, { mode: 0o600 });
188
+
189
+ const args = [
190
+ "-q",
191
+ "-s",
192
+ this.keyPath,
193
+ "-I",
194
+ opts.identity,
195
+ "-n",
196
+ opts.principal,
197
+ // A little slack on the lower bound: the browser's clock is not ours.
198
+ "-V",
199
+ `-5m:+${Math.max(1, Math.round(opts.ttlSeconds / 60))}m`,
200
+ "-z",
201
+ String(this.nextSerial()),
202
+ ];
203
+ if (opts.forceCommand)
204
+ args.push("-O", `force-command=${opts.forceCommand}`);
205
+ args.push(keyFile);
206
+
207
+ await run("ssh-keygen", args, { timeout: 15_000 });
208
+ const cert = await readFile(certFile, "utf8");
209
+ if (!cert.includes("-cert-v01@openssh.com")) {
210
+ throw new Error("ssh-keygen did not produce a certificate");
211
+ }
212
+ return cert;
213
+ } finally {
214
+ await rm(tmp, { recursive: true, force: true });
215
+ }
216
+ }
217
+
218
+ private nextSerial(): bigint {
219
+ this.serialCounter += 1n;
220
+ return BigInt(Date.now()) * 1000n + (this.serialCounter % 1000n);
221
+ }
222
+ }
223
+
224
+ /** Validates and trims a public key posted by the browser. Throws on anything suspect. */
225
+ export function normalisePublicKey(raw: string): string {
226
+ if (raw.length > MAX_PUBKEY_BYTES) throw new Error("public key too large");
227
+
228
+ const line = raw.trim();
229
+ if (line.includes("\n") || line.includes("\r"))
230
+ throw new Error("public key must be a single line");
231
+
232
+ const [type, blob] = line.split(/\s+/, 2);
233
+ if (!type || !blob) throw new Error("malformed public key");
234
+ if (type.includes("-cert-v01@openssh.com"))
235
+ throw new Error("refusing to certify a certificate");
236
+ if (!SIGNABLE_KEY_TYPE.test(type))
237
+ throw new Error(`unsupported key type: ${type}`);
238
+ if (!/^[A-Za-z0-9+/]+={0,3}$/.test(blob))
239
+ throw new Error("malformed public key body");
240
+
241
+ // The base64 blob must decode to a key whose embedded type matches the
242
+ // declared one — otherwise a caller could mislabel a key to slip past the
243
+ // type check above.
244
+ const decoded = Buffer.from(blob, "base64");
245
+ if (decoded.length < 4) throw new Error("malformed public key body");
246
+ const nameLength = decoded.readUInt32BE(0);
247
+ if (nameLength > 64 || decoded.length < 4 + nameLength)
248
+ throw new Error("malformed public key body");
249
+ const embedded = decoded.subarray(4, 4 + nameLength).toString("ascii");
250
+ if (embedded !== type)
251
+ throw new Error("public key type does not match its body");
252
+
253
+ // Re-emit without any client-supplied comment.
254
+ return `${type} ${blob}`;
255
+ }
256
+
257
+ /**
258
+ * Finds this host's SSH host key so the browser can pin it and skip the
259
+ * trust-on-first-use prompt.
260
+ *
261
+ * The order matters: it mirrors the order golang.org/x/crypto/ssh offers host
262
+ * key algorithms, so the key we pin is the one sshd will actually present.
263
+ * If none is readable we return null and the browser falls back to prompting.
264
+ */
265
+ /**
266
+ * OpenSSH-style SHA256 fingerprint of a public key line.
267
+ *
268
+ * Computed here rather than in the browser because `crypto.subtle` does not
269
+ * exist on a non-secure origin, and plain HTTP on a bare IP — the default way
270
+ * this thing gets run — is exactly that.
271
+ */
272
+ export function fingerprint(publicKeyLine: string): string | null {
273
+ const blob = publicKeyLine.trim().split(/\s+/)[1];
274
+ if (!blob) return null;
275
+ try {
276
+ const digest = createHash("sha256")
277
+ .update(Buffer.from(blob, "base64"))
278
+ .digest("base64");
279
+ return `SHA256:${digest.replace(/=+$/, "")}`;
280
+ } catch {
281
+ return null;
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Host key types in the order the client will actually pick them.
287
+ *
288
+ * This is golang.org/x/crypto/ssh's `supportedHostKeyAlgos` order, which puts
289
+ * ECDSA ahead of Ed25519 — the opposite of what OpenSSH does, and the opposite
290
+ * of what looks reasonable. Getting it wrong is not harmless: pinning a key the
291
+ * server holds but does not present makes every pane report the host key as
292
+ * *changed*, which reads like an attack rather than a misconfiguration.
293
+ */
294
+ const HOST_KEY_TYPES = ["ecdsa", "rsa", "ed25519"] as const;
295
+
296
+ /**
297
+ * Finds the host key the SSH target will actually present, so the browser can
298
+ * pin it and skip the trust-on-first-use prompt.
299
+ *
300
+ * Asking the target with ssh-keyscan beats reading /etc/ssh/*.pub: it works
301
+ * when the bridge points somewhere other than this machine, it needs no file
302
+ * permissions, and it cannot pin a key that sshd is not actually serving.
303
+ * Reading the local files stays as a fallback for when ssh-keyscan is missing.
304
+ *
305
+ * The type order matters — the pinned key has to be the one that wins
306
+ * negotiation, not merely one the server happens to hold. Returns null if
307
+ * nothing is discoverable, and the browser prompts once instead.
308
+ */
309
+ export async function discoverHostKey(
310
+ host: string,
311
+ port: number,
312
+ ): Promise<string | null> {
313
+ for (const type of HOST_KEY_TYPES) {
314
+ try {
315
+ const { stdout } = await run(
316
+ "ssh-keyscan",
317
+ ["-t", type, "-p", String(port), "-T", "5", host],
318
+ {
319
+ timeout: 10_000,
320
+ },
321
+ );
322
+ // Output is "<host> <type> <base64>". The leading host field is not part
323
+ // of the key, and sshterm keys its known-hosts entry by endpoint name.
324
+ const line = stdout
325
+ .split("\n")
326
+ .find((l) => l.trim() && !l.startsWith("#"));
327
+ const parts = line?.trim().split(/\s+/);
328
+ if (parts && parts.length >= 3) return `${parts[1]} ${parts[2]}`;
329
+ } catch {
330
+ // server does not offer this type, or no ssh-keyscan — try the next
331
+ }
332
+ }
333
+
334
+ // Reading this box's own host keys only answers the question when the bridge
335
+ // points at this box. With --ssh-host aimed elsewhere it would pin the wrong
336
+ // machine's key, and every pane would then report the host key as changed —
337
+ // which reads as an attack rather than the misconfiguration it is.
338
+ if (!isLocalHost(host)) return null;
339
+
340
+ for (const type of HOST_KEY_TYPES) {
341
+ try {
342
+ const line = (await readFile(`/etc/ssh/ssh_host_${type}_key.pub`, "utf8"))
343
+ .trim()
344
+ .split("\n")[0]
345
+ ?.trim();
346
+ if (line) return line;
347
+ } catch {
348
+ // unreadable or absent — try the next
349
+ }
350
+ }
351
+ return null;
352
+ }
353
+
354
+ /** Does this name refer to the machine we are running on? */
355
+ function isLocalHost(host: string): boolean {
356
+ const h = host
357
+ .trim()
358
+ .toLowerCase()
359
+ .replace(/^\[|\]$/g, "");
360
+ return (
361
+ h === "localhost" ||
362
+ h === "127.0.0.1" ||
363
+ h === "::1" ||
364
+ h === "0.0.0.0" ||
365
+ h === "" ||
366
+ h === os.hostname().toLowerCase()
367
+ );
368
+ }
@@ -0,0 +1,147 @@
1
+ import path from "node:path";
2
+ import { BUILD_ID, EMBEDDED } from "./assets.generated.ts";
3
+ import { IMMUTABLE_CACHE_CONTROL } from "./http.ts";
4
+
5
+ /**
6
+ * Serves the built web app.
7
+ *
8
+ * Two things earn their keep here: precompressed variants (ssh.wasm is ~20MB
9
+ * raw and about a fifth of that as brotli, and it is on the critical path of
10
+ * the very first page load), and an SPA fallback so deep links work.
11
+ *
12
+ * Assets resolve from the embedded manifest first, then from disk. In a
13
+ * compiled binary the manifest holds everything and the disk is never touched;
14
+ * under `bun run` the manifest is empty and everything comes off disk. Both
15
+ * paths end at Bun.file, so there is only one code path to get right.
16
+ */
17
+
18
+ /** Vite writes content-hashed filenames into assets/, so those cache forever. */
19
+ const IMMUTABLE = /\/assets\/[^/]+-[A-Za-z0-9_-]{8,}\.[a-z0-9]+$/;
20
+
21
+ const MIME: Record<string, string> = {
22
+ ".html": "text/html; charset=utf-8",
23
+ ".js": "text/javascript; charset=utf-8",
24
+ ".css": "text/css; charset=utf-8",
25
+ ".json": "application/json; charset=utf-8",
26
+ // Required for WebAssembly.instantiateStreaming to take the fast path.
27
+ ".wasm": "application/wasm",
28
+ ".svg": "image/svg+xml",
29
+ ".png": "image/png",
30
+ ".jpg": "image/jpeg",
31
+ ".jpeg": "image/jpeg",
32
+ ".webp": "image/webp",
33
+ ".avif": "image/avif",
34
+ ".gif": "image/gif",
35
+ ".ico": "image/x-icon",
36
+ ".woff2": "font/woff2",
37
+ ".txt": "text/plain; charset=utf-8",
38
+ };
39
+
40
+ function cacheControl(urlPath: string): string {
41
+ if (IMMUTABLE.test(urlPath)) return IMMUTABLE_CACHE_CONTROL;
42
+ if (urlPath.endsWith(".html") || urlPath === "/") return "no-cache";
43
+ // ssh.wasm and wasm_exec.js are big but do change on upgrade: let the client
44
+ // keep them and revalidate cheaply with an ETag.
45
+ return "public, max-age=0, must-revalidate";
46
+ }
47
+
48
+ function encodingsFor(req: Request): ("br" | "gzip")[] {
49
+ const accept = req.headers.get("accept-encoding") ?? "";
50
+ const out: ("br" | "gzip")[] = [];
51
+ if (/\bbr\b/.test(accept)) out.push("br");
52
+ if (/\bgzip\b/.test(accept)) out.push("gzip");
53
+ return out;
54
+ }
55
+
56
+ const SUFFIX = { br: ".br", gzip: ".gz" } as const;
57
+
58
+ export function createStaticServer(webRoot: string) {
59
+ const root = path.resolve(webRoot);
60
+
61
+ /** Resolves a repo-relative asset path to something Bun.file can open. */
62
+ const locate = async (relative: string): Promise<string | null> => {
63
+ const embedded = EMBEDDED[`/${relative}`];
64
+ if (embedded) return embedded;
65
+
66
+ const onDisk = path.resolve(root, relative);
67
+ // path.resolve collapses "..", so one containment check is enough.
68
+ if (onDisk !== root && !onDisk.startsWith(root + path.sep)) return null;
69
+ return (await Bun.file(onDisk).exists()) ? onDisk : null;
70
+ };
71
+
72
+ return async function serveStatic(
73
+ req: Request,
74
+ urlPath: string,
75
+ ): Promise<Response | null> {
76
+ if (req.method !== "GET" && req.method !== "HEAD") return null;
77
+
78
+ let decoded: string;
79
+ try {
80
+ decoded = decodeURIComponent(urlPath);
81
+ } catch {
82
+ return null;
83
+ }
84
+ if (decoded.includes("\0")) return null;
85
+
86
+ let relative = decoded === "/" ? "index.html" : decoded.replace(/^\/+/, "");
87
+ let resolved = await locate(relative);
88
+ // What the cache policy is decided against. It follows the file actually
89
+ // served rather than the path asked for, so an SPA route gets index.html's
90
+ // `no-cache` instead of the extensionless default of `must-revalidate`.
91
+ let served = urlPath;
92
+
93
+ // SPA fallback: anything that is not a real file and does not look like an
94
+ // asset request gets index.html.
95
+ if (!resolved) {
96
+ if (path.extname(relative) !== "") return null;
97
+ relative = "index.html";
98
+ resolved = await locate(relative);
99
+ if (!resolved) return null;
100
+ served = "/index.html";
101
+ }
102
+
103
+ const type =
104
+ MIME[path.extname(relative).toLowerCase()] ?? "application/octet-stream";
105
+
106
+ let encoding: "br" | "gzip" | null = null;
107
+ let body = Bun.file(resolved);
108
+ for (const candidate of encodingsFor(req)) {
109
+ const alt = await locate(`${relative}${SUFFIX[candidate]}`);
110
+ if (alt) {
111
+ encoding = candidate;
112
+ body = Bun.file(alt);
113
+ break;
114
+ }
115
+ }
116
+
117
+ // `.size` works for both real files and files embedded in a compiled
118
+ // binary; `.stat()` returns undefined for the latter. Embedded assets have
119
+ // no mtime either, so BUILD_ID stands in as the thing that changes when the
120
+ // bytes do. The encoding is folded in so switching between br/gzip/identity
121
+ // can never reuse a cached body of the wrong shape.
122
+ const size = body.size;
123
+ let version = BUILD_ID;
124
+ if (!EMBEDDED[`/${relative}`]) {
125
+ const st = await Promise.resolve(body.stat()).catch(() => null);
126
+ if (st) version = Math.floor(st.mtimeMs).toString(16);
127
+ }
128
+ const etag = `W/"${size.toString(16)}-${version}${encoding ? `-${encoding}` : ""}"`;
129
+
130
+ const headers = new Headers({
131
+ "content-type": type,
132
+ "cache-control": cacheControl(served),
133
+ etag,
134
+ vary: "Accept-Encoding",
135
+ });
136
+ if (encoding) headers.set("content-encoding", encoding);
137
+
138
+ if (req.headers.get("if-none-match") === etag) {
139
+ return new Response(null, { status: 304, headers });
140
+ }
141
+
142
+ headers.set("content-length", String(size));
143
+ if (req.method === "HEAD")
144
+ return new Response(null, { status: 200, headers });
145
+ return new Response(body, { status: 200, headers });
146
+ };
147
+ }