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.
@@ -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
- res.setHeader('Access-Control-Allow-Origin', '*');
78
- // Expose our debug headers to browsers (CORS hides custom headers by default)
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 !== '127.0.0.1' && HOST !== 'localhost') {
134
- log.info(` ⚠ bound to ${HOST} — reachable beyond this machine. Operators authenticate via invite`);
135
- log.info(` links; put your reverse-proxy auth in front for anything admin-grade.`);
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
- operators sign in via invite links. Only names you
820
- list are answered: GREAT_CTO_ALLOWED_HOSTS=console.example.com
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "3.77.0",
3
+ "version": "3.78.1",
4
4
  "description": "One command install for the great_cto Claude Code plugin. Auto-detects your stack, picks the right archetype, bootstraps PROJECT.md.",
5
5
  "keywords": [
6
6
  "claude-code",