great-cto 3.77.0 → 3.78.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.
- package/board/.claude-plugin/plugin.json +1 -1
- package/board/packages/board/lib/config.mjs +27 -6
- package/board/packages/board/lib/util.mjs +19 -1
- package/board/packages/board/public/assets/admin-shell.js +15 -8
- package/board/packages/board/public/index.html +693 -364
- package/board/packages/board/server.mjs +29 -8
- package/dist/main.js +5 -3
- package/package.json +1 -1
|
@@ -12,9 +12,9 @@ import http from 'http';
|
|
|
12
12
|
import fs from 'fs';
|
|
13
13
|
import path from 'path';
|
|
14
14
|
import { spawnSync } from 'child_process';
|
|
15
|
-
import { PORT, PUBLIC, HOST } from './lib/config.mjs';
|
|
15
|
+
import { PORT, PUBLIC, HOST, bindRefusal } from './lib/config.mjs';
|
|
16
16
|
import { warmTasksAsync } from './lib/beads.mjs';
|
|
17
|
-
import { originAllowed, hostAllowed, parseAllowedHosts, isInsideDir } from './lib/util.mjs';
|
|
17
|
+
import { originAllowed, corsOriginFor, crossSiteRead, hostAllowed, parseAllowedHosts, isInsideDir } from './lib/util.mjs';
|
|
18
18
|
import { discoverProjects, resolveProjectInfo } from './lib/projects.mjs';
|
|
19
19
|
import { startAlertCron } from './lib/alerts.mjs';
|
|
20
20
|
import { watchBeads, watchVerdicts, watchAgentEvents } from './lib/watchers.mjs';
|
|
@@ -73,10 +73,26 @@ const server = http.createServer(async (req, res) => {
|
|
|
73
73
|
res.setHeader('X-Project-Resolved', projInfo.resolved);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
// CORS
|
|
77
|
-
|
|
78
|
-
//
|
|
76
|
+
// CORS: only the board's own origins. This was `*`, so any page open in the same
|
|
77
|
+
// browser could read the API. Vary keeps a cache from serving one origin's answer
|
|
78
|
+
// to another.
|
|
79
|
+
res.setHeader('Vary', 'Origin');
|
|
80
|
+
// Expose our debug headers to browsers (CORS hides custom headers by default).
|
|
81
|
+
// Sent on every answer: without Access-Control-Allow-Origin it grants nothing,
|
|
82
|
+
// and a client that checks the list must not find it missing.
|
|
79
83
|
res.setHeader('Access-Control-Expose-Headers', 'X-Project-Fallback, X-Project-Resolved, X-Project-Serving');
|
|
84
|
+
const corsOrigin = corsOriginFor(req);
|
|
85
|
+
if (corsOrigin) res.setHeader('Access-Control-Allow-Origin', corsOrigin);
|
|
86
|
+
|
|
87
|
+
// ── Cross-site reads ────────────────────────────────────────────────────────
|
|
88
|
+
// Withholding the CORS header stops a page reading the answer; the request still
|
|
89
|
+
// arrives and still runs. The API and the event stream are refused outright.
|
|
90
|
+
// The page itself (`/`) stays reachable: a link in an alert email is cross-site.
|
|
91
|
+
if (pathname.startsWith('/api/') && crossSiteRead(req)) {
|
|
92
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
93
|
+
res.end(JSON.stringify({ error: 'cross-origin request blocked — the board API answers only its own page and local tools' }));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
80
96
|
|
|
81
97
|
// ── CSRF guard (BH-31) ──────────────────────────────────────────────────────
|
|
82
98
|
// The board binds 127.0.0.1, but a page the user visits in their browser can still
|
|
@@ -128,11 +144,16 @@ const server = http.createServer(async (req, res) => {
|
|
|
128
144
|
});
|
|
129
145
|
|
|
130
146
|
// ── Start ──────────────────────────────────────────────────────────────────────
|
|
147
|
+
const refusal = bindRefusal();
|
|
148
|
+
if (refusal) {
|
|
149
|
+
log.error(refusal);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
131
152
|
server.listen(PORT, HOST, () => {
|
|
132
153
|
log.info(`great_cto board → http://${HOST === '0.0.0.0' ? 'localhost' : HOST}:${PORT}`);
|
|
133
|
-
if (HOST
|
|
134
|
-
log.info(` ⚠ bound to ${HOST} — reachable beyond this machine.
|
|
135
|
-
log.info(`
|
|
154
|
+
if (bindRefusal(HOST, {}, []) !== null) {
|
|
155
|
+
log.info(` ⚠ bound to ${HOST} — reachable beyond this machine. The board has no login of its own:`);
|
|
156
|
+
log.info(` you stated that authentication sits in front of it (GREAT_CTO_BOARD_BEHIND_AUTH / --behind-auth).`);
|
|
136
157
|
}
|
|
137
158
|
if (['0.0.0.0', '::'].includes(HOST) && parseAllowedHosts().length === 0) {
|
|
138
159
|
log.info(` ⚠ bound to ${HOST} with no GREAT_CTO_ALLOWED_HOSTS — only localhost / 127.0.0.1 names are answered.`);
|
package/dist/main.js
CHANGED
|
@@ -815,9 +815,11 @@ ${bold("Board:")}
|
|
|
815
815
|
${bold("Operator console (the second surface — invite-only, hostable):")}
|
|
816
816
|
great-cto console Serve ONLY the operator console (no dev board)
|
|
817
817
|
great-cto console --port 8788 Different port
|
|
818
|
-
great-cto console --bind 0.0.0.0 Reachable beyond this machine (tunnel/hosting)
|
|
819
|
-
|
|
820
|
-
|
|
818
|
+
great-cto console --bind 0.0.0.0 Reachable beyond this machine (tunnel/hosting).
|
|
819
|
+
There is no login: put authentication in front and set
|
|
820
|
+
GREAT_CTO_BOARD_BEHIND_AUTH=1, or the server refuses to start.
|
|
821
|
+
Only names you list are answered:
|
|
822
|
+
GREAT_CTO_ALLOWED_HOSTS=console.example.com
|
|
821
823
|
|
|
822
824
|
${bold("Telemetry (anonymous, opt-IN — OFF by default):")}
|
|
823
825
|
great-cto telemetry status Show state + endpoint + your anon_id
|
package/package.json
CHANGED