Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/serve.mjs

7.3 KiB, 1 run

created by r2519314175:129, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1// dev/serve.mjs — a local launcher for Daimond.
2//
3// Serves www/ on http://localhost:8777 (a secure context, so OPFS works) and
4// reverse-proxies /api and /webhook to THIS WORLD'S gateway — exactly what Steel
5// does in production, so the session cookie is same-origin and every
6// gateway-backed feature (read, credits, email) works locally. If this world has
7// no gateway, those calls are REFUSED and the browser-only tiers carry on.
8//
9// node dev/serve.mjs # from the app root
10//
11// The port is `DAIMOND_PORT`, default 8777, and the gateway it proxies to is
12// `DAIMOND_GW_PORT`. Both are settable so that several agents can
13// each hold a whole world -- server, mock provider and browser profile -- at once;
14// with one fixed port only one browser harness could run at a time, and no agent
15// could see its own work. `dev/world.sh N` prints a matching set, and its
16// gateway row is 9700 + N: a port of the world's own that nothing binds unless
17// the world asked for a gateway.
18//
19// THE DEFAULT IS ONLY FOR A SERVER STARTED BY HAND. Under a world the variable is
20// always set, and it is set to something that is not 9002 -- because 9002 is where
21// a stray `devgw.sh` lands, and until 2026-08-25 every world proxied there, so
22// which lane's account state a verifier read was whichever lane had a gateway up.
23// `dev/world.sh` carries the measurement.
24//
25// No dependencies; plain Node http.
26
27import http from 'node:http';
28import { readFile, stat } from 'node:fs/promises';
29import { extname, join, normalize } from 'node:path';
30import { fileURLToPath } from 'node:url';
31
32const ROOT = normalize(join(fileURLToPath(import.meta.url), '..', '..', 'www'));
33const PORT = Number(process.env.DAIMOND_PORT || 8777);
34const GATEWAY = { host: '127.0.0.1', port: Number(process.env.DAIMOND_GW_PORT || 9002) };
35
36const TYPES = {
37 '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8',
38 // The vendored Typst compiler's glue is `.mjs`, and a dynamic import() refuses anything that
39 // is not served as JavaScript. Without this the whole feature failed locally with "Failed to
40 // fetch dynamically imported module" -- a MIME type, reported as a broken compiler.
41 '.mjs': 'text/javascript; charset=utf-8',
42 '.css': 'text/css; charset=utf-8', '.json': 'application/json',
43 // The web app manifest. Steel already serves this extension correctly
44 // (fe2o3_net::file), and without it here a locally served Daimond is not
45 // installable -- which is exactly the thing that has to be testable locally.
46 '.webmanifest': 'application/manifest+json',
47 '.wasm': 'application/wasm', '.svg': 'image/svg+xml', '.png': 'image/png',
48 '.woff2': 'font/woff2', '.map': 'application/json', '.pdf': 'application/pdf',
49 '.ico': 'image/x-icon', '.txt': 'text/plain; charset=utf-8',
50};
51
52function proxy(req, res) {
53 const opts = {
54 host: GATEWAY.host, port: GATEWAY.port,
55 method: req.method, path: req.url, headers: req.headers,
56 };
57 const up = http.request(opts, u => {
58 res.writeHead(u.statusCode || 502, u.headers);
59 u.pipe(res);
60 });
61 // THE REFUSAL NAMES THE PORT AND SAYS WHOSE IT IS. A caller that reads "the
62 // gateway is not running" and knows only the phrase cannot tell an absent
63 // gateway from one it was never entitled to reach, and the whole point of the
64 // world's own port is that the second case no longer exists.
65 up.on('error', () => {
66 res.writeHead(502, { 'content-type': 'application/json' });
67 res.end(JSON.stringify({ error: `No gateway is running on 127.0.0.1:${GATEWAY.port}, `
68 + `which is this world's own. No other world's gateway is reachable from here: `
69 + `start one on this port (DAIMOND_GW_PORT), or use the browser-only features.` }));
70 });
71 req.pipe(up);
72}
73
74// A WebSocket upgrade on an /api path is relayed to the gateway and then left
75// alone, which is what Steel's own proxy does (fe2o3_steel::srv::wsproxy): the
76// handshake is forwarded verbatim so the gateway computes the accept value the
77// browser checks, and after the 101 this hop copies bytes and parses nothing.
78// Without it the sync wake channel could only be tested in production.
79function proxyUpgrade(req, socket, head) {
80 if (!(req.url || '').startsWith('/api/')) { socket.destroy(); return; }
81 const up = http.request({
82 host: GATEWAY.host, port: GATEWAY.port,
83 method: req.method, path: req.url, headers: req.headers,
84 });
85 up.on('upgrade', (ures, usocket, uhead) => {
86 const lines = [`HTTP/1.1 ${ures.statusCode} ${ures.statusMessage}`];
87 for (let i = 0; i < ures.rawHeaders.length; i += 2) {
88 lines.push(`${ures.rawHeaders[i]}: ${ures.rawHeaders[i + 1]}`);
89 }
90 socket.write(lines.join('\r\n') + '\r\n\r\n');
91 if (uhead && uhead.length) socket.write(uhead);
92 if (head && head.length) usocket.write(head);
93 usocket.pipe(socket);
94 socket.pipe(usocket);
95 const shut = () => { usocket.destroy(); socket.destroy(); };
96 usocket.on('error', shut); socket.on('error', shut);
97 usocket.on('close', shut); socket.on('close', shut);
98 });
99 // The gateway answered without upgrading (a 401, say). Pass that on whole, so
100 // the browser reports a refusal rather than a connection that vanished.
101 up.on('response', ures => {
102 socket.write(`HTTP/1.1 ${ures.statusCode} ${ures.statusMessage}\r\n`
103 + 'Content-Length: 0\r\nConnection: close\r\n\r\n');
104 ures.resume();
105 socket.end();
106 });
107 up.on('error', () => socket.destroy());
108 up.end();
109}
110
111const server = http.createServer(async (req, res) => {
112 const url = decodeURIComponent((req.url || '/').split('?')[0]);
113 if (url.startsWith('/api/') || url.startsWith('/webhook/')) return proxy(req, res);
114
115 // WHICH TREE THIS SERVES, so a caller can identify the process holding the
116 // port instead of trusting that it is the one it started.
117 //
118 // `dev/world.sh --up` leaves an already-bound port alone, which is right when
119 // a person is sharing a world and was ruinous on 2026-08-17: a gate of
120 // 99c838e found world 9 still held by an interrupted gate of b536d60, said
121 // "already serving, left alone", and spent two hours driving a browser at
122 // ANOTHER COMMIT'S FILES while reporting the result against 99c838e. Nothing
123 // in the run could have noticed, because nothing could ask. Now it can.
124 //
125 // `__` so it can never collide with a served file, and dev-only: this server
126 // is not what runs in production.
127 if (url === '/__world') {
128 const body = JSON.stringify({ root: ROOT, port: PORT, gateway: GATEWAY.port, pid: process.pid });
129 res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' });
130 return res.end(body);
131 }
132
133 let path = normalize(join(ROOT, url === '/' ? '/index.html' : url));
134 if (!path.startsWith(ROOT)) { res.writeHead(403); return res.end('no'); }
135 try {
136 const s = await stat(path);
137 if (s.isDirectory()) path = join(path, 'index.html');
138 const body = await readFile(path);
139 res.writeHead(200, {
140 'content-type': TYPES[extname(path)] || 'application/octet-stream',
141 // Cross-origin isolation is not needed, but no-cache keeps you on the
142 // latest build while you test.
143 'cache-control': 'no-cache',
144 });
145 res.end(body);
146 } catch (e) {
147 res.writeHead(404, { 'content-type': 'text/plain' });
148 res.end('Not found: ' + url);
149 }
150});
151
152server.on('upgrade', proxyUpgrade);
153
154server.listen(PORT, 'localhost', () => {
155 console.log(`Daimond dev server → http://localhost:${PORT}`);
156 console.log(` /api and /webhook proxy to the gateway on :${GATEWAY.port} (start it separately for read/email/credits)`);
157});