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 +7 -1
- package/lib/dashboard/handlers.js +8 -1
- package/lib/dashboard/hub-server.js +14 -1
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|