flowviant 0.52.0 → 0.54.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 +25 -31
- package/bin/cli.mjs +28 -22
- package/bin/lib/authproxy.mjs +131 -32
- package/bin/lib/config.mjs +15 -20
- package/bin/lib/fleet.mjs +141 -63
- package/bin/lib/git.mjs +1 -1
- package/bin/lib/instance.mjs +296 -17
- package/bin/lib/listeners.mjs +269 -0
- package/bin/lib/login.mjs +5 -1
- package/bin/lib/mcp-cli.mjs +12 -11
- package/bin/lib/preflight.mjs +8 -5
- package/bin/lib/preview.mjs +294 -390
- package/bin/lib/runtimes.mjs +12 -3
- package/bin/lib/stream.mjs +10 -8
- package/bin/lib/update.mjs +3 -2
- package/bin/lib/work.mjs +249 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
# flowviant
|
|
2
2
|
|
|
3
|
-
Run your own
|
|
3
|
+
Run your own coding CLIs as build agents for [Flowviant](https://flowviant.com) — [Claude Code](https://claude.com/claude-code), Codex or Antigravity, on your own credentials. This daemon holds your sessions, keeps a worktree per tab, and ships branches on your word. Flowviant never sees your Claude, Codex or GitHub logins.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npx flowviant login
|
|
7
|
-
npx flowviant # run your fleet
|
|
6
|
+
npx flowviant@latest login # approve the code in Flowviant → connected
|
|
8
7
|
```
|
|
9
8
|
|
|
9
|
+
Login keeps going straight into the daemon — there is no second command to run.
|
|
10
|
+
|
|
10
11
|
## What it does
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
A Flowviant project has ONE machine, and this is it: one box, running one Claude (or Codex, or Antigravity) account, serving the whole team's sessions so nobody has to set up their own. Solo and team are the same architecture at N=1 and N>1.
|
|
14
|
+
|
|
15
|
+
You work in the **Workbench**, where your sessions are TABS — each one a held context plus a persistent git worktree on its own `session/<id>` branch. The browser is a terminal projected onto this machine, so you reach the same session from any device. When you say ship, the tab merges its own branch into base with `--no-ff` — no squash, so the commit shas the work reported still exist on main.
|
|
13
16
|
|
|
14
17
|
Because it drives the CLIs you're already logged into, **the cost is yours** (your Claude subscription, your GitHub) and **the daemon never handles a credential** — it shells out to tools you authenticated yourself.
|
|
15
18
|
|
|
@@ -17,9 +20,9 @@ Because it drives the CLIs you're already logged into, **the cost is yours** (yo
|
|
|
17
20
|
|
|
18
21
|
On the machine that runs the daemon:
|
|
19
22
|
|
|
20
|
-
- **[Claude Code](https://claude.com/claude-code)
|
|
21
|
-
- **[GitHub CLI](https://cli.github.com)** authenticated (`gh auth login`) — for opening PRs
|
|
23
|
+
- **at least one coding CLI** installed and signed in — [Claude Code](https://claude.com/claude-code) (`claude`), Codex (`codex`) or Antigravity (`agy`)
|
|
22
24
|
- **git**, and **Node 20+**
|
|
25
|
+
- **[GitHub CLI](https://cli.github.com)** (`gh`) — optional; the daemon offers to fetch an isolated copy, and `flowviant gh-auth` signs it in
|
|
23
26
|
- run it from inside the git repository you want worked
|
|
24
27
|
|
|
25
28
|
## Connecting
|
|
@@ -27,53 +30,44 @@ On the machine that runs the daemon:
|
|
|
27
30
|
The easy way — device login, like `gh auth login`:
|
|
28
31
|
|
|
29
32
|
```bash
|
|
30
|
-
npx flowviant login
|
|
33
|
+
npx flowviant@latest login
|
|
31
34
|
```
|
|
32
35
|
|
|
33
|
-
It shows a short code
|
|
36
|
+
It shows a short code. Open your project's **Workbench** in Flowviant and enter the code where it offers to connect a machine. The credential is stored at `~/.flowviant/credentials.json`, and from then on `npx flowviant@latest` just runs.
|
|
34
37
|
|
|
35
|
-
Prefer an explicit token? Create a
|
|
38
|
+
Prefer an explicit token? Create a machine credential in the app and pass it directly:
|
|
36
39
|
|
|
37
40
|
```bash
|
|
38
|
-
FLOWVIANT_FLEET=fva_… npx flowviant
|
|
41
|
+
FLOWVIANT_FLEET=fva_… npx flowviant@latest
|
|
39
42
|
```
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
Each task runs a **persistent** Claude session you can talk to mid-task from the app: the agent streams its work into the task's conversation, you `@`-mention it to steer or answer questions, and it resumes in place. Blockers park the session at zero cost until you answer. When it finishes, it posts a delivery card (summary + checklist self-report) in the thread — a human confirms done by merging there.
|
|
44
|
+
Launch with `@latest` so each start pulls the newest published version — a bare `npx flowviant` can reuse a stale cache. A running daemon also self-updates at startup and when idle (`FLOWVIANT_NO_UPDATE=1` makes it nag-only; `flowviant update` updates now).
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
## Sessions
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
FLOWVIANT_POLL=1 npx flowviant
|
|
49
|
-
```
|
|
48
|
+
Each tab in the Workbench is a persistent Claude session with its own worktree, and it stays where you left it — the branch outlives the tab. The daemon runs each turn in event mode and relays what the CLI is printing (thinking, reads, greps, commands) back to the tab, reports the worktree's branch and diffstat after every turn, and fetches a commit's patch when you click a sha in the app.
|
|
50
49
|
|
|
51
|
-
|
|
50
|
+
Nothing starts work except you opening a tab and typing in it.
|
|
52
51
|
|
|
53
|
-
|
|
52
|
+
## Sharing a preview
|
|
54
53
|
|
|
55
|
-
|
|
56
|
-
// .flowviant/preview.json
|
|
57
|
-
{ "ui": { "cmd": "npm run dev", "port": 5173 } }
|
|
58
|
-
```
|
|
54
|
+
You run your dev server yourself, in the session's own worktree, exactly as you would in any terminal. The daemon NOTICES the listening port; ask for a share in the app and it puts a [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) quick tunnel in front of it (auto-fetched if missing, pinned and checksummed) behind a **mandatory password gate**. Flowviant stores only the tunnel URL; your browser talks to it directly.
|
|
59
55
|
|
|
60
|
-
|
|
56
|
+
The daemon never executes anything the repository declares. An earlier version read a `.flowviant/preview.json` from the branch and spawned the command it named — that start path was removed in 0.53.0 and is not coming back; see the header of `bin/lib/preview.mjs` for exactly what it did, so nobody rebuilds it.
|
|
61
57
|
|
|
62
58
|
## Modes
|
|
63
59
|
|
|
64
60
|
| Env | What runs |
|
|
65
61
|
| --- | --- |
|
|
66
|
-
| _(stored login)_ or `FLOWVIANT_FLEET` | **
|
|
67
|
-
| `FLOWVIANT_TOKEN` | a single agent in the current checkout |
|
|
68
|
-
| `FLOWVIANT_TOKENS=a,b,c` | a static fleet, one worktree each |
|
|
62
|
+
| _(stored login)_ or `FLOWVIANT_FLEET` | **the daemon** — the project's machine, serving its sessions |
|
|
69
63
|
| `FLOWVIANT_SAFE=1` | restrict the toolset instead of running unattended |
|
|
70
64
|
|
|
71
65
|
## Security posture
|
|
72
66
|
|
|
73
|
-
Every project member with edit access can run turns on this machine —
|
|
74
|
-
Workbench
|
|
75
|
-
|
|
76
|
-
|
|
67
|
+
Every project member with edit access can run turns on this machine — a
|
|
68
|
+
Workbench tab executes a coding agent with the daemon's own OS permissions.
|
|
69
|
+
Membership is the consent boundary, the same trust plane as the shared
|
|
70
|
+
repository: invite people you would give a shell to.
|
|
77
71
|
|
|
78
72
|
Two knobs bound the blast radius, and both are worth setting on a shared box:
|
|
79
73
|
|
package/bin/cli.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* flowviant — run your own Claude Code
|
|
3
|
+
* flowviant — run your own coding CLI (Claude Code, Codex, Antigravity) as the
|
|
4
|
+
* machine behind a Flowviant project.
|
|
4
5
|
*
|
|
5
6
|
* ONE mode, one credential:
|
|
6
7
|
*
|
|
7
|
-
* FLOWVIANT_FLEET=
|
|
8
|
+
* FLOWVIANT_FLEET=fva_… npx flowviant@latest # the machine daemon
|
|
8
9
|
*
|
|
9
10
|
* `FLOWVIANT_TOKEN` (one worker, current checkout) and `FLOWVIANT_TOKENS` (a
|
|
10
11
|
* comma list, one worktree each) stood beside it until 2026-08-19. Both ran the
|
|
@@ -19,28 +20,32 @@
|
|
|
19
20
|
* on its own — at startup and when idle — so it stays current without restarts
|
|
20
21
|
* (FLOWVIANT_NO_UPDATE=1 makes it nag-only; `flowviant update` updates now).
|
|
21
22
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
23
|
+
* The daemon: install ONCE with a machine credential, then work entirely from
|
|
24
|
+
* Flowviant. It polls GET /api/v2/fleet/agents, and the roster hands it the
|
|
25
|
+
* project's SESSIONS — the Workbench's tabs. Each session gets one persistent
|
|
26
|
+
* git worktree on its own `session/<id>` branch, held across turns (never reset
|
|
27
|
+
* to base: the branch outlives the tab). A turn spawns the session's CLI with a
|
|
28
|
+
* short-lived per-session MCP token, relays what it prints back to the tab,
|
|
29
|
+
* reports the worktree's branch and diffstat when it settles, and answers the
|
|
30
|
+
* odd side job the roster carries — a commit's patch, a preview share, a wiki
|
|
31
|
+
* regen. When you say ship, the daemon merges that branch into base `--no-ff`.
|
|
29
32
|
*
|
|
30
33
|
* Env:
|
|
31
|
-
*
|
|
34
|
+
* FLOWVIANT_FLEET the machine credential (or use `flowviant login`).
|
|
32
35
|
* FLOWVIANT_API_URL default https://api.flowviant.com/api/v2
|
|
33
36
|
* FLOWVIANT_MCP_URL default <API_URL>/mcp
|
|
34
37
|
* FLOWVIANT_FLEET_URL default <API_URL>/fleet/agents
|
|
35
|
-
*
|
|
36
|
-
* IDLE_SECONDS gap between work checks when idle (default 30)
|
|
37
|
-
* RECONCILE_SECONDS fleet roster poll cadence (default 10)
|
|
38
|
+
* RECONCILE_SECONDS roster poll cadence (default 10)
|
|
38
39
|
* FLOWVIANT_SAFE=1 restrict the toolset instead of running unattended.
|
|
39
40
|
*
|
|
40
|
-
* Requires
|
|
41
|
-
* repo you want worked.
|
|
41
|
+
* Requires one of `claude` / `codex` / `agy` on PATH, plus `git`; run from
|
|
42
|
+
* inside the git repo you want worked. `gh` is optional.
|
|
42
43
|
*
|
|
43
|
-
* Implementation lives in ./lib/: config, ui,
|
|
44
|
+
* Implementation lives in ./lib/: config, ui, preflight, install, update,
|
|
45
|
+
* instance, login, mcp-cli; fleet (the roster loop) and work (session turns);
|
|
46
|
+
* claude + runtimes + prompts + stream (spawning a CLI and reading its events);
|
|
47
|
+
* git + worktreeDiff + patch; localSessions, listeners, preview + authproxy;
|
|
48
|
+
* env + env-cli + vault, resources, deploy, shot.
|
|
44
49
|
*/
|
|
45
50
|
import { FLEET_TOKEN } from './lib/config.mjs';
|
|
46
51
|
import { runFleetDaemon } from './lib/fleet.mjs';
|
|
@@ -132,9 +137,9 @@ if (process.argv[2] === 'clean') {
|
|
|
132
137
|
}
|
|
133
138
|
|
|
134
139
|
// `flowviant shot <url>` — capture a headless-browser screenshot of a running
|
|
135
|
-
// page.
|
|
136
|
-
//
|
|
137
|
-
//
|
|
140
|
+
// page. A session's agent shells out to this to SEE the change it just made.
|
|
141
|
+
// Self-contained + graceful (no browser → exit 1, and the agent carries on in
|
|
142
|
+
// text); needs no credential, so it runs before the auth gate.
|
|
138
143
|
if (process.argv[2] === 'shot') {
|
|
139
144
|
const { runShot } = await import('./lib/shot.mjs');
|
|
140
145
|
await runShot(process.argv.slice(3));
|
|
@@ -145,8 +150,9 @@ if (process.argv[2] === 'shot') {
|
|
|
145
150
|
// are sealed to the project pubkey ON THIS MACHINE (same write-only crypto as
|
|
146
151
|
// the browser); `show` decrypts locally — it only works on an ENROLLED machine.
|
|
147
152
|
// `flowviant mcp` — connect YOUR Claude to Flowviant so you can file work from
|
|
148
|
-
// the terminal. Mints a `cli` credential: a separate principal from the
|
|
149
|
-
//
|
|
153
|
+
// the terminal. Mints a `cli` credential: a separate principal from the
|
|
154
|
+
// per-session tokens, with only the management tools and no way to work or ship
|
|
155
|
+
// a card.
|
|
150
156
|
if (process.argv[2] === 'mcp') {
|
|
151
157
|
const { runMcpCommand } = await import('./lib/mcp-cli.mjs');
|
|
152
158
|
await runMcpCommand(process.argv.slice(3));
|
|
@@ -164,7 +170,7 @@ if (!FLEET_TOKEN) {
|
|
|
164
170
|
'error: no credential found. Easiest:\n' +
|
|
165
171
|
' flowviant login (approve in the app — recommended)\n' +
|
|
166
172
|
'Or set:\n' +
|
|
167
|
-
' FLOWVIANT_FLEET=
|
|
173
|
+
' FLOWVIANT_FLEET=fva_… (machine token, from the app)'
|
|
168
174
|
);
|
|
169
175
|
process.exit(1);
|
|
170
176
|
}
|
package/bin/lib/authproxy.mjs
CHANGED
|
@@ -1,44 +1,124 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* it reaches the running app. This gates it: cloudflared → this proxy → dev
|
|
5
|
-
* server, with a generated password the app surfaces to the reviewer.
|
|
2
|
+
* The password gate in front of a shared preview. cloudflared → this proxy →
|
|
3
|
+
* the dev server the DRIVER started in their own worktree.
|
|
6
4
|
*
|
|
7
|
-
*
|
|
5
|
+
* MANDATORY, not opt-in (2026-08-21). It was `.flowviant/preview.json`
|
|
6
|
+
* "auth": true, defaulting OFF, and its caller logged "tunneling WITHOUT a
|
|
7
|
+
* password" to a console nobody reads and opened the tunnel anyway. A tunnel
|
|
8
|
+
* publishes a worktree holding the project's materialized dev secrets; there is
|
|
9
|
+
* no honest default but closed. `startAuthProxy` returning null now means the
|
|
10
|
+
* share is ABORTED and the machine says why.
|
|
11
|
+
*
|
|
12
|
+
* Minimal and dependency-free: forwards HTTP, and pipes WS upgrades (HMR) — the
|
|
8
13
|
* browser re-sends the cached Basic-auth header on same-origin upgrades, so HMR
|
|
9
|
-
* still authenticates.
|
|
10
|
-
*
|
|
14
|
+
* still authenticates.
|
|
15
|
+
*
|
|
16
|
+
* Three things this file gets wrong easily, all of them fixed here and all of
|
|
17
|
+
* them worth keeping fixed:
|
|
18
|
+
* - the credential must NOT reach the origin. `headers: req.headers` forwarded
|
|
19
|
+
* `authorization` verbatim, handing the gate password to whatever code the
|
|
20
|
+
* branch happens to be running. It is stripped now.
|
|
21
|
+
* - the comparison is over a secret, so it is constant-time over a digest
|
|
22
|
+
* rather than `===` over a string.
|
|
23
|
+
* - `stop()` was a bare `server.close()`, which refuses NEW connections and
|
|
24
|
+
* leaves live ones alone — so a held HMR websocket kept the page alive for
|
|
25
|
+
* the one most-engaged viewer after teardown. Live sockets are tracked and
|
|
26
|
+
* destroyed.
|
|
11
27
|
*/
|
|
12
28
|
|
|
13
29
|
import { createServer, request } from 'node:http';
|
|
14
|
-
import { randomBytes } from 'node:crypto';
|
|
30
|
+
import { randomBytes, createHash, timingSafeEqual } from 'node:crypto';
|
|
31
|
+
|
|
32
|
+
/** Failed attempts before the proxy stops answering at all. A quick tunnel's
|
|
33
|
+
* hostname is unguessable, so this is not the primary control — it is what
|
|
34
|
+
* turns a discovered URL from an offline guessing target into a visible,
|
|
35
|
+
* self-closing incident. */
|
|
36
|
+
const MAX_FAILED = 25;
|
|
37
|
+
|
|
38
|
+
const digest = (s) => createHash('sha256').update(String(s)).digest();
|
|
39
|
+
|
|
40
|
+
/** Constant-time over sha256 digests, so length never leaks and a missing
|
|
41
|
+
* header costs the same as a wrong one. */
|
|
42
|
+
function sameSecret(a, b) {
|
|
43
|
+
try {
|
|
44
|
+
return timingSafeEqual(digest(a ?? ''), digest(b ?? ''));
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
15
49
|
|
|
16
|
-
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Start the gate in front of a dev server on `targetPort`. Resolves
|
|
52
|
+
* { port, user, password, stop } — or NULL, which the caller must treat as a
|
|
53
|
+
* hard failure. Binds loopback only; cloudflared connects locally, and the
|
|
54
|
+
* password is what gates the public hostname.
|
|
55
|
+
*
|
|
56
|
+
* `onAbuse` fires once, after MAX_FAILED rejected attempts, so the caller can
|
|
57
|
+
* tear the whole share down rather than leaving a URL under attack.
|
|
58
|
+
*/
|
|
59
|
+
export function startAuthProxy({ targetPort, log, onAbuse }) {
|
|
20
60
|
const user = 'preview';
|
|
21
|
-
|
|
61
|
+
// 24 bytes → 32 url-safe chars. It was 9 bytes, chosen when this was an
|
|
62
|
+
// opt-in convenience; it is the only thing between a public hostname and a
|
|
63
|
+
// worktree now.
|
|
64
|
+
const password = randomBytes(24).toString('base64url');
|
|
22
65
|
const expected = 'Basic ' + Buffer.from(`${user}:${password}`).toString('base64');
|
|
23
|
-
const authed = (req) => req.headers['authorization'] === expected;
|
|
24
66
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
67
|
+
let failed = 0;
|
|
68
|
+
let abused = false;
|
|
69
|
+
const authed = (req) => {
|
|
70
|
+
if (abused) return false;
|
|
71
|
+
if (sameSecret(req.headers['authorization'], expected)) {
|
|
72
|
+
failed = 0;
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
// A bare GET with no header is the browser's first move on every load, and
|
|
76
|
+
// it is answered with a 401 challenge — it is not an attempt.
|
|
77
|
+
if (req.headers['authorization']) failed += 1;
|
|
78
|
+
if (failed >= MAX_FAILED && !abused) {
|
|
79
|
+
abused = true;
|
|
80
|
+
log?.(`preview gate: ${failed} failed attempts — closing the share.`);
|
|
81
|
+
try {
|
|
82
|
+
onAbuse?.();
|
|
83
|
+
} catch {
|
|
84
|
+
/* the caller's teardown is best-effort */
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// The gate credential is OURS and stops here. Everything else is passed
|
|
91
|
+
// through untouched: the origin is the driver's own dev server and rewriting
|
|
92
|
+
// its request would be us editing their app's input.
|
|
93
|
+
const forwardOpts = (req) => {
|
|
94
|
+
const headers = { ...req.headers };
|
|
95
|
+
delete headers.authorization;
|
|
96
|
+
delete headers['proxy-authorization'];
|
|
97
|
+
return {
|
|
98
|
+
host: '127.0.0.1',
|
|
99
|
+
port: targetPort,
|
|
100
|
+
method: req.method,
|
|
101
|
+
path: req.url,
|
|
102
|
+
headers,
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const challenge = (res) => {
|
|
107
|
+
res.writeHead(401, {
|
|
108
|
+
'WWW-Authenticate': 'Basic realm="Flowviant preview"',
|
|
109
|
+
'Content-Type': 'text/plain',
|
|
110
|
+
// A preview is a moving target by definition; nothing about it should sit
|
|
111
|
+
// in a cache the viewer cannot see.
|
|
112
|
+
'Cache-Control': 'no-store',
|
|
113
|
+
});
|
|
114
|
+
res.end('This preview is password-protected. Enter the password shown in Flowviant.');
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Every live socket, so stop() can actually end the ones already talking.
|
|
118
|
+
const sockets = new Set();
|
|
32
119
|
|
|
33
120
|
const server = createServer((req, res) => {
|
|
34
|
-
if (!authed(req))
|
|
35
|
-
res.writeHead(401, {
|
|
36
|
-
'WWW-Authenticate': 'Basic realm="Flowviant preview"',
|
|
37
|
-
'Content-Type': 'text/plain',
|
|
38
|
-
});
|
|
39
|
-
res.end('This preview is password-protected. Enter the password shown in Flowviant.');
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
121
|
+
if (!authed(req)) return challenge(res);
|
|
42
122
|
const proxyReq = request(forwardOpts(req), (proxyRes) => {
|
|
43
123
|
res.writeHead(proxyRes.statusCode || 502, proxyRes.headers);
|
|
44
124
|
proxyRes.pipe(res);
|
|
@@ -50,8 +130,13 @@ export function startAuthProxy({ targetPort, log }) {
|
|
|
50
130
|
req.pipe(proxyReq);
|
|
51
131
|
});
|
|
52
132
|
|
|
133
|
+
server.on('connection', (socket) => {
|
|
134
|
+
sockets.add(socket);
|
|
135
|
+
socket.on('close', () => sockets.delete(socket));
|
|
136
|
+
});
|
|
137
|
+
|
|
53
138
|
// WS upgrade (HMR). The browser resends the Basic-auth header on same-origin
|
|
54
|
-
// upgrades, so we
|
|
139
|
+
// upgrades, so we gate it too, then pipe the two sockets together.
|
|
55
140
|
server.on('upgrade', (req, socket, head) => {
|
|
56
141
|
if (!authed(req)) {
|
|
57
142
|
socket.write('HTTP/1.1 401 Unauthorized\r\nWWW-Authenticate: Basic realm="Flowviant preview"\r\n\r\n');
|
|
@@ -65,6 +150,8 @@ export function startAuthProxy({ targetPort, log }) {
|
|
|
65
150
|
if (proxyHead && proxyHead.length) proxySocket.unshift(proxyHead);
|
|
66
151
|
proxySocket.pipe(socket);
|
|
67
152
|
socket.pipe(proxySocket);
|
|
153
|
+
sockets.add(proxySocket);
|
|
154
|
+
proxySocket.on('close', () => sockets.delete(proxySocket));
|
|
68
155
|
proxySocket.on('error', () => socket.destroy());
|
|
69
156
|
socket.on('error', () => proxySocket.destroy());
|
|
70
157
|
});
|
|
@@ -74,10 +161,12 @@ export function startAuthProxy({ targetPort, log }) {
|
|
|
74
161
|
});
|
|
75
162
|
|
|
76
163
|
return new Promise((resolve) => {
|
|
77
|
-
|
|
164
|
+
// Could not bind → the caller ABORTS the share. There is no no-proxy path
|
|
165
|
+
// to fall back to any more.
|
|
166
|
+
server.on('error', () => resolve(null));
|
|
78
167
|
server.listen(0, '127.0.0.1', () => {
|
|
79
168
|
const port = server.address().port;
|
|
80
|
-
log?.(`
|
|
169
|
+
log?.(`preview gate on :${port}`);
|
|
81
170
|
resolve({
|
|
82
171
|
port,
|
|
83
172
|
user,
|
|
@@ -88,6 +177,16 @@ export function startAuthProxy({ targetPort, log }) {
|
|
|
88
177
|
} catch {
|
|
89
178
|
/* already closed */
|
|
90
179
|
}
|
|
180
|
+
// close() only stops NEW connections. An open HMR socket would keep
|
|
181
|
+
// serving the viewer who is still looking at it.
|
|
182
|
+
for (const s of sockets) {
|
|
183
|
+
try {
|
|
184
|
+
s.destroy();
|
|
185
|
+
} catch {
|
|
186
|
+
/* already gone */
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
sockets.clear();
|
|
91
190
|
},
|
|
92
191
|
});
|
|
93
192
|
});
|
package/bin/lib/config.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Parsed configuration: env vars, CLI flags, and the chosen credentials. */
|
|
2
2
|
|
|
3
|
+
import { randomBytes } from 'node:crypto';
|
|
3
4
|
import { readFileSync } from 'node:fs';
|
|
4
5
|
import { dirname, join } from 'node:path';
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
@@ -43,11 +44,6 @@ function argFlag(name) {
|
|
|
43
44
|
return i >= 0 ? process.argv[i + 1] : undefined;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
/** A bare boolean flag (no value follows it). */
|
|
47
|
-
function hasFlag(name) {
|
|
48
|
-
return process.argv.includes(name);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
47
|
const API_BASE = process.env.FLOWVIANT_API_URL || 'https://api.flowviant.com/api/v2';
|
|
52
48
|
export const MCP_URL = process.env.FLOWVIANT_MCP_URL || `${API_BASE}/mcp`;
|
|
53
49
|
export const FLEET_URL = process.env.FLOWVIANT_FLEET_URL || `${API_BASE}/fleet/agents`;
|
|
@@ -148,25 +144,24 @@ export const AUTO_UPDATE = process.env.FLOWVIANT_NO_UPDATE !== '1';
|
|
|
148
144
|
// path (one-shot `claude -p` turns) survives behind FLOWVIANT_POLL=1 as the
|
|
149
145
|
// escape hatch; FLOWVIANT_LIVE=1 is still honored for old scripts.
|
|
150
146
|
export const LIVE = process.env.FLOWVIANT_POLL !== '1';
|
|
147
|
+
// Sent on the daemon's own HTTP calls so Cloudflare Bot Fight Mode doesn't 403
|
|
148
|
+
// them (Node's default UA is treated as a bot). Claude Code sends its own UA.
|
|
149
|
+
export const USER_AGENT = `flowviant/${VERSION}`;
|
|
150
|
+
|
|
151
151
|
/**
|
|
152
|
-
*
|
|
153
|
-
* working checkout, with no PR and no review?
|
|
152
|
+
* WHICH DAEMON PROCESS this is — not which credential.
|
|
154
153
|
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
154
|
+
* Two daemons legitimately share one fleet token (that is exactly what
|
|
155
|
+
* `machineDaemonsDisagree` reports, and the instance lock added in 0.51.2
|
|
156
|
+
* cannot see a peer OLDER than itself). So the token cannot identify a lease
|
|
157
|
+
* holder, and anything that must be done exactly once needs this instead.
|
|
159
158
|
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
159
|
+
* Regenerated every start, deliberately: a daemon that restarted is a daemon
|
|
160
|
+
* that lost whatever it was holding, and a stale lease should not follow it
|
|
161
|
+
* back. Sent on the poll as `di`; its ABSENCE is what an older daemon looks
|
|
162
|
+
* like, and the server hands preview work to nobody who cannot name themselves.
|
|
164
163
|
*/
|
|
165
|
-
export const
|
|
166
|
-
!hasFlag('--no-patches') && process.env.FLOWVIANT_PATCHES !== '0';
|
|
167
|
-
// Sent on the daemon's own HTTP calls so Cloudflare Bot Fight Mode doesn't 403
|
|
168
|
-
// them (Node's default UA is treated as a bot). Claude Code sends its own UA.
|
|
169
|
-
export const USER_AGENT = `flowviant/${VERSION}`;
|
|
164
|
+
export const DAEMON_INSTANCE = randomBytes(12).toString('hex');
|
|
170
165
|
|
|
171
166
|
// The ONE credential. `tokens` (FLOWVIANT_TOKEN / FLOWVIANT_TOKENS / --token /
|
|
172
167
|
// --tokens) stood beside it and carried WORKER tokens into the pre-daemon loop;
|