serve takes --public-host NAME --auth-header HEADER --allow-user ID, all three or none. With them, a request whose Host is NAME must carry HEADER equal to ID or gets 403; a request that carries HEADER with any other Host gets 403, so a proxy that rewrites Host can't reach the loopback path with a signed-in request; every POST is refused read-only. A repeated Host gets 400 in both modes. Host and header names compare case-insensitively (Traefik sends X-Authentik-Uid). The README states the proxy contract: the edge forwards Host unchanged and strips client copies of HEADER. Author: Dewey. Round 2 candidate manifest 17b6d7f62d106a23d99441eefa46e34ab2e9bd1442a1d02e219f126a82a8668d (base17ea0dbc, no drift on the 4 paths through60c8dcf9). Approved by Filbert (comment 27485) and Darkwing (comment 27486) on #1568. Their wording notes on the README (first signed-in request; the tunnel sentence) go to row 76, which owns the README next. Sage gate rerun on a worktree ofa55915f0with the patch: manifest OK; webui 61/61, control-board 124/124, cli 113/113; test-auth 15, conductor 17, config 24, extension-package 18, foundation 44, queue 27, release 14, task 98, all 0 failed. test-discord failed one timing test (engine: a timed-out run outlives the grace, 280 ms at load 30) on the sequential run and passed 66/66 on a solo rerun of the same worktree; the patch touches no discord file. Co-Authored-By: Claude Opus 5.5 <[email protected]>
53 lines
3.2 KiB
JavaScript
53 lines
3.2 KiB
JavaScript
#!/usr/bin/env node
|
|
import { startServer, previewOptions, DEFAULT_BOARD } from './serve.mjs';
|
|
import { busReader } from './bus.mjs';
|
|
import { loadSystem, socketPath, REPO, rolesDir } from '../../cli/src/config.mjs';
|
|
import { configDir } from '../../business/src/business.mjs';
|
|
import { consoleReads, redactor } from './reads.mjs';
|
|
import { ID_PATTERN } from '../../business/src/vocabulary.mjs';
|
|
const args = process.argv.slice(2);
|
|
const usage = 'node packages/webui/src/cli.mjs serve [--port N] [--board http://127.0.0.1:7331] [--business ID] [--public-host NAME --auth-header HEADER --allow-user ID]';
|
|
try {
|
|
if (args.length === 1 && ['--help', '-h'].includes(args[0])) { console.log(usage); process.exit(0); }
|
|
if (args.shift() !== 'serve') throw new Error(usage);
|
|
const options = { board: DEFAULT_BOARD, port: 7330 }, preview = {};
|
|
while (args.length) {
|
|
const flag = args.shift(), value = args.shift();
|
|
if (!value) throw new Error(`missing value for ${flag}`);
|
|
if (flag === '--port') {
|
|
if (!/^\d+$/.test(value) || Number(value) > 65535) throw new Error('port must be 0..65535');
|
|
options.port = Number(value);
|
|
} else if (flag === '--board') options.board = value;
|
|
else if (flag === '--business') {
|
|
if (!ID_PATTERN.test(value)) throw new Error('business must be a business id');
|
|
options.business = value;
|
|
}
|
|
// Row 74 (#1568): the preview mode, all three or none, checked before any config is read.
|
|
else if (flag === '--public-host') preview.publicHost = value;
|
|
else if (flag === '--auth-header') preview.authHeader = value;
|
|
else if (flag === '--allow-user') preview.allowUser = value;
|
|
else throw new Error(`unknown option: ${flag}`);
|
|
}
|
|
options.preview = previewOptions(preview);
|
|
// The bus views (slice 1 S5) need the system config. Without it the
|
|
// board still serves and the bus routes answer not-configured.
|
|
// The Queue, Business and Settings reads (row 54) need no bus; without
|
|
// the system config, Business can't resolve authority and Settings has
|
|
// no notifier. A refusal printed here names no path (redactor).
|
|
let bus = null, busLine, system = null;
|
|
try {
|
|
system = loadSystem();
|
|
const { dataRoot } = system;
|
|
bus = busReader({ dataRoot, socket: socketPath(dataRoot), business: options.business ?? null });
|
|
busLine = `Bus: ${options.business ?? 'the running bus host\'s business'}`;
|
|
} catch (err) { busLine = `Bus: not configured (${redactor({ configDir: configDir() })(err.message)})`; }
|
|
const reads = consoleReads({ root: REPO, system, configDir: configDir(), rolesDir: rolesDir(), business: options.business ?? null });
|
|
const server = await startServer({ board: options.board, port: options.port, bus, reads, preview: options.preview });
|
|
const previewLine = options.preview ? `\nPreview: https://${options.preview.host}/ through the proxy, signed-in user from ${options.preview.header}, read-only` : '';
|
|
console.log(`Mosaic Console: http://127.0.0.1:${server.address().port}/\nBoard: ${options.board}\n${busLine}${previewLine}\nCtrl-C stops this server.`);
|
|
for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => server.close());
|
|
} catch (err) {
|
|
console.error(`refused: ${err.message}`);
|
|
process.exitCode = 2;
|
|
}
|