spectoflow 0.29.0 → 0.30.0

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/README.md CHANGED
@@ -198,6 +198,12 @@ spectoflow dashboard # → http://localhost:4319 (or --port=
198
198
  dashboard already running on the port). `spectoflow status` tells you whether one is up. Zero
199
199
  dependencies, updates live via SSE + file watching.
200
200
 
201
+ **It only answers this machine.** The hub can launch agents and write your project files, so it listens
202
+ on the loopback interfaces only (`127.0.0.1`, `::1`) and refuses any request whose `Host` isn't local or
203
+ that another website sent — a malicious page, a DNS-rebinding trick, or a tunnel/proxy you run on the same
204
+ machine can't drive it. Clicking a link to it from another site still opens it. To reach your projects from
205
+ another device, use the online dashboard ([below](#going-online-optional-local-hub-vs-relay-server)).
206
+
201
207
  ### One hub, every project
202
208
 
203
209
  There is only ever **one dashboard process on your machine**, no matter how many projects you have.
@@ -388,7 +394,7 @@ agents then read and grow your second brain through that server: nothing is copi
388
394
  that output also carries command output and file contents, so a line hidden in a repository can't slip
389
395
  in unseen.
390
396
  - **Private:** the Second brain tab and its API answer this machine only — not the online dashboard
391
- (`server/` refuses them for everyone, the project owner included), and not other machines on your network.
397
+ (`server/` refuses them for everyone, the project owner included), and not other machines or websites.
392
398
  A run started from either of those can't write into it, and a learned fact never goes into a project's chat
393
399
  log.
394
400
 
@@ -29,12 +29,19 @@ const { ROUTES, findRoute } = require('./routes');
29
29
  const LOOPBACK = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
30
30
  const LOCAL_HOSTNAMES = new Set(['localhost', '127.0.0.1', '[::1]']);
31
31
  const isLoopback = (addr) => !!addr && (LOOPBACK.has(addr) || addr.startsWith('127.') || addr.startsWith('::ffff:127.'));
32
- function isLocalRequest(req) {
32
+ // allowNavigation: the hub's own gate lets a top-level GET navigation from another site through (a link to
33
+ // the dashboard clicked elsewhere) — the linking page can't read what it opens. Ops never allow it.
34
+ function isLocalRequest(req, { allowNavigation = false } = {}) {
33
35
  if (!isLoopback(req.socket && req.socket.remoteAddress)) return false;
34
36
  const host = String(req.headers.host || '');
35
37
  let hostname;
36
38
  try { hostname = new URL(`http://${host}`).hostname; } catch { return false; }
37
39
  if (!LOCAL_HOSTNAMES.has(hostname)) return false;
40
+ const site = req.headers['sec-fetch-site'];
41
+ if (site === 'cross-site' || site === 'same-site') {
42
+ const navigation = req.headers['sec-fetch-mode'] === 'navigate' && (req.method === 'GET' || req.method === 'HEAD');
43
+ if (!(allowNavigation && navigation)) return false;
44
+ }
38
45
  const origin = req.headers.origin;
39
46
  if (origin === undefined) return true;
40
47
  try { return new URL(origin).host === host; } catch { return false; }
@@ -283,7 +283,16 @@ function clearLock(){ try{ const l=JSON.parse(fs.readFileSync(LOCK,'utf8')); if(
283
283
  process.on('exit', clearLock);
284
284
  ['SIGINT','SIGTERM'].forEach((s)=> process.on(s, ()=>{ if (connector) connector.stop(); clearLock(); process.exit(0); }));
285
285
 
286
+ const { isLocalRequest } = require('./handlers');
286
287
  const server = http.createServer(async (req, res) => {
288
+ // This machine only (D74). The hub runs agents and writes project files: it listens on the loopback
289
+ // interfaces only, and still refuses a request whose Host isn't local (DNS rebinding, a tunnel or proxy
290
+ // on this machine) or that another site sent (cross-site fetch/POST). Reaching a project from elsewhere
291
+ // is what the online dashboard (server/) is for.
292
+ if (!isLocalRequest(req, { allowNavigation: true })) {
293
+ res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
294
+ return res.end(`spectoflow: this dashboard only answers this machine — open http://localhost:${PORT}`);
295
+ }
287
296
  const u = new URL(req.url, `http://localhost:${PORT}`);
288
297
  const p = u.pathname;
289
298
  try {
@@ -358,4 +367,8 @@ function watchBrain() {
358
367
  } catch (_) {}
359
368
  }
360
369
 
361
- server.listen(PORT, () => { watchBrain(); writeLock(); console.log(`spectoflow · hub → http://localhost:${PORT}${migrated.movedRegistry ? ' (moved your project list into the workspace)' : ''}`); startConnector(); });
370
+ // IPv6 loopback too, best effort: `localhost` may resolve to ::1 first. No ::1 on this machine → IPv4 only.
371
+ const server6 = http.createServer((req, res) => server.emit('request', req, res));
372
+ server6.on('error', () => {});
373
+ server.on('listening', () => server6.listen(PORT, '::1'));
374
+ server.listen(PORT, '127.0.0.1', () => { watchBrain(); writeLock(); console.log(`spectoflow · hub → http://localhost:${PORT}${migrated.movedRegistry ? ' (moved your project list into the workspace)' : ''}`); startConnector(); });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spectoflow",
3
- "version": "0.29.0",
3
+ "version": "0.30.0",
4
4
  "description": "Agent-agnostic spec-driven development framework + real-time local control plane. Markdown artifacts, intent router, workflow-by-scope.",
5
5
  "keywords": [
6
6
  "spec-driven-development",