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 | |
| 27 | import http from 'node:http'; |
| 28 | import { readFile, stat } from 'node:fs/promises'; |
| 29 | import { extname, join, normalize } from 'node:path'; |
| 30 | import { fileURLToPath } from 'node:url'; |
| 31 | |
| 32 | const ROOT = normalize(join(fileURLToPath(import.meta.url), '..', '..', 'www')); |
| 33 | const PORT = Number(process.env.DAIMOND_PORT || 8777); |
| 34 | const GATEWAY = { host: '127.0.0.1', port: Number(process.env.DAIMOND_GW_PORT || 9002) }; |
| 35 | |
| 36 | const 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 | |
| 52 | function 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. |
| 79 | function 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 | |
| 111 | const 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 | |
| 152 | server.on('upgrade', proxyUpgrade); |
| 153 | |
| 154 | server.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 | }); |