flowviant 0.91.2 → 0.92.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/bin/cli.mjs +10 -10
- package/bin/lib/childEnv.mjs +128 -9
- package/bin/lib/config.mjs +28 -0
- package/bin/lib/deploy.mjs +92 -45
- package/bin/lib/env.mjs +595 -852
- package/bin/lib/fleet.mjs +319 -77
- package/bin/lib/git.mjs +63 -0
- package/bin/lib/machines.mjs +39 -13
- package/bin/lib/mcp-cli.mjs +8 -7
- package/bin/lib/vaultArtefacts.mjs +170 -0
- package/bin/lib/work.mjs +34 -43
- package/package.json +1 -1
- package/bin/lib/env-cli.mjs +0 -216
package/bin/cli.mjs
CHANGED
|
@@ -60,7 +60,8 @@
|
|
|
60
60
|
* instance, login, mcp-cli; fleet (the roster loop) and work (session turns);
|
|
61
61
|
* claude + runtimes + prompts + stream (spawning a CLI and reading its events);
|
|
62
62
|
* git + worktreeDiff + patch; localSessions, listeners, preview + authproxy;
|
|
63
|
-
* env
|
|
63
|
+
* env (the box keypair, the uplink scrubber, the env-comparison scan) + vault
|
|
64
|
+
* (the knowledge wiki, not secrets), resources, deploy, shot.
|
|
64
65
|
*/
|
|
65
66
|
import { FLEET_TOKEN, CREDENTIAL, adoptStoredCredential } from './lib/config.mjs';
|
|
66
67
|
import { runFleetDaemon } from './lib/fleet.mjs';
|
|
@@ -311,9 +312,14 @@ if (process.argv[2] === 'machines') {
|
|
|
311
312
|
process.exit(0);
|
|
312
313
|
}
|
|
313
314
|
|
|
314
|
-
// `flowviant env <import|set|show>`
|
|
315
|
-
//
|
|
316
|
-
//
|
|
315
|
+
// `flowviant env <import|set|show>` IS DELETED (2026-09-21), with the
|
|
316
|
+
// end-to-end-encrypted secrets vault it was the terminal half of. The owner:
|
|
317
|
+
// "no i dont want it. unless its needed where i want to show the env of each
|
|
318
|
+
// of the machines (for comparison)." So the terminal surface is back to the
|
|
319
|
+
// three commands it is allowed: `npx flowviant`, `npx flowviant login`, and
|
|
320
|
+
// the view-only `npx flowviant machines`. The comparison readout that ruling
|
|
321
|
+
// carves out is a daemon REPORT, not a command — see `scanEnvFiles` in env.mjs.
|
|
322
|
+
//
|
|
317
323
|
// `flowviant mcp` — connect YOUR Claude to Flowviant so you can file work from
|
|
318
324
|
// the terminal. Mints a `cli` credential: a separate principal from the
|
|
319
325
|
// per-session tokens, with only the management tools and no way to work or ship
|
|
@@ -324,12 +330,6 @@ if (process.argv[2] === 'mcp') {
|
|
|
324
330
|
process.exit(0);
|
|
325
331
|
}
|
|
326
332
|
|
|
327
|
-
if (process.argv[2] === 'env') {
|
|
328
|
-
const { runEnvCommand } = await import('./lib/env-cli.mjs');
|
|
329
|
-
await runEnvCommand(process.argv.slice(3));
|
|
330
|
-
process.exit(0);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
333
|
// ── WHICH PROJECT THIS START SERVES — said, asked, or refused; never guessed.
|
|
334
334
|
//
|
|
335
335
|
// The store holds many projects since 0.55.0 and resolution is BY REPO
|
package/bin/lib/childEnv.mjs
CHANGED
|
@@ -61,20 +61,77 @@ const KEEP = [
|
|
|
61
61
|
'XDG_DATA_HOME',
|
|
62
62
|
];
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* THE SECOND GROUP: INFRA CREDENTIALS, AND ONLY BEHIND `deploy: true`.
|
|
66
|
+
*
|
|
67
|
+
* WHY IT EXISTS (2026-09-21). Deploy used to get its credentials from the
|
|
68
|
+
* secrets vault — `childEnv({ extra: deployCreds() })`, where `deployCreds()`
|
|
69
|
+
* was the deploy-scope half of a bundle Flowviant had decrypted onto this box.
|
|
70
|
+
* The vault is deleted (the owner: "no i dont want it"), so Flowviant holds no
|
|
71
|
+
* cloud credential for anybody, and the source of a deploy's secrets is now the
|
|
72
|
+
* MACHINE'S OWN ENVIRONMENT — the operator's `CLOUDFLARE_API_TOKEN`, exported
|
|
73
|
+
* in the shell they started the daemon in, exactly as it would be if they ran
|
|
74
|
+
* `wrangler deploy` themselves.
|
|
75
|
+
*
|
|
76
|
+
* But this file exists precisely because a spawned command is built from `{}`,
|
|
77
|
+
* so an operator's own token is absent by construction and `wrangler deploy`
|
|
78
|
+
* fails with an auth error that looks like a broken deploy. Hence an OPT-IN
|
|
79
|
+
* WIDENING for the one command that genuinely needs infra credentials.
|
|
80
|
+
*
|
|
81
|
+
* IT IS STILL AN ALLOWLIST. `deploy: true` does not mean `{...process.env}`; it
|
|
82
|
+
* means these names and nothing else. The whole argument of this module is that
|
|
83
|
+
* a denylist is a claim about a set you cannot see and rots the moment somebody
|
|
84
|
+
* upstream adds a secret — that argument does not weaken because the caller is
|
|
85
|
+
* a deploy.
|
|
86
|
+
*
|
|
87
|
+
* THE BLAST RADIUS IS EXACTLY THIS LIST, and it is worth naming out loud: a
|
|
88
|
+
* deploy target's `build` string is REPO-CONTROLLED (it is read from
|
|
89
|
+
* `.flowviant/deploy.json` on the base branch, so authoring it requires landing
|
|
90
|
+
* a commit — but it is still a string this daemon runs). Everything below is
|
|
91
|
+
* therefore reachable by a reviewed commit on main, and nothing else is.
|
|
92
|
+
*
|
|
93
|
+
* `FLOWVIANT_*` IS NEVER IN IT, at any opt-in. The machine credential is the
|
|
94
|
+
* one secret whose leak costs the project itself, and the bug this module was
|
|
95
|
+
* written for is that it rode into `target.build` under a comment saying it did
|
|
96
|
+
* not.
|
|
97
|
+
*/
|
|
98
|
+
const DEPLOY_KEEP = [
|
|
99
|
+
'CLOUDFLARE_API_TOKEN',
|
|
100
|
+
'CLOUDFLARE_ACCOUNT_ID',
|
|
101
|
+
'VERCEL_TOKEN',
|
|
102
|
+
'NETLIFY_AUTH_TOKEN',
|
|
103
|
+
'FLY_API_TOKEN',
|
|
104
|
+
'AWS_ACCESS_KEY_ID',
|
|
105
|
+
'AWS_SECRET_ACCESS_KEY',
|
|
106
|
+
'AWS_REGION',
|
|
107
|
+
'AWS_SESSION_TOKEN',
|
|
108
|
+
'RAILWAY_TOKEN',
|
|
109
|
+
'RENDER_API_KEY',
|
|
110
|
+
'NPM_TOKEN',
|
|
111
|
+
];
|
|
112
|
+
|
|
64
113
|
/**
|
|
65
114
|
* A child environment for a command Flowviant runs on the operator's behalf.
|
|
66
115
|
*
|
|
67
|
-
* `
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
116
|
+
* `deploy: true` additionally keeps `DEPLOY_KEEP` from `process.env` — the
|
|
117
|
+
* opt-in widening for the one command that needs infra credentials. Nothing
|
|
118
|
+
* else passes it, and passing it does not widen anything beyond that list.
|
|
119
|
+
*
|
|
120
|
+
* `extra` is layered LAST and is the caller's own material. It is never
|
|
121
|
+
* repo-supplied: the deleted preview feature let `.flowviant/preview.json`
|
|
122
|
+
* contribute an `env` map that was layered last and therefore won every
|
|
123
|
+
* collision, which is how a branch got to set `PATH`.
|
|
72
124
|
*/
|
|
73
|
-
export function childEnv({ cwd, extra } = {}) {
|
|
125
|
+
export function childEnv({ cwd, extra, deploy = false } = {}) {
|
|
74
126
|
const env = {};
|
|
75
127
|
for (const k of KEEP) {
|
|
76
128
|
if (typeof process.env[k] === 'string') env[k] = process.env[k];
|
|
77
129
|
}
|
|
130
|
+
if (deploy) {
|
|
131
|
+
for (const k of DEPLOY_KEEP) {
|
|
132
|
+
if (typeof process.env[k] === 'string') env[k] = process.env[k];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
78
135
|
// Set by us rather than inherited.
|
|
79
136
|
//
|
|
80
137
|
// TERM=dumb: a process that believes it owns a TTY draws progress bars and
|
|
@@ -93,9 +150,15 @@ export function childEnv({ cwd, extra } = {}) {
|
|
|
93
150
|
return extra ? { ...env, ...extra } : env;
|
|
94
151
|
}
|
|
95
152
|
|
|
96
|
-
/** The names this deliberately drops, for the test to
|
|
97
|
-
* mechanism — the mechanism is the allowlist above,
|
|
98
|
-
* a sample of what it excludes. Adding a name here
|
|
153
|
+
/** The names this deliberately drops on the ORDINARY path, for the test to
|
|
154
|
+
* assert against. NOT the mechanism — the mechanism is the allowlist above,
|
|
155
|
+
* and this list is only ever a sample of what it excludes. Adding a name here
|
|
156
|
+
* changes nothing.
|
|
157
|
+
*
|
|
158
|
+
* Two of these (`CLOUDFLARE_API_TOKEN`, `AWS_SECRET_ACCESS_KEY`) are in
|
|
159
|
+
* `DEPLOY_KEEP` and survive `deploy: true`. They stay in this sample on
|
|
160
|
+
* purpose: the default is what almost every caller gets, and the sample is
|
|
161
|
+
* about the default. */
|
|
99
162
|
export const DROPPED_SAMPLE = [
|
|
100
163
|
'FLOWVIANT_FLEET',
|
|
101
164
|
'FLOWVIANT_FLEET_URL',
|
|
@@ -111,3 +174,59 @@ export const DROPPED_SAMPLE = [
|
|
|
111
174
|
'AWS_SECRET_ACCESS_KEY',
|
|
112
175
|
'npm_config__authToken',
|
|
113
176
|
];
|
|
177
|
+
|
|
178
|
+
/** The opt-in group, exported for the test: `deploy: true` keeps exactly these
|
|
179
|
+
* and nothing else, and a name that is NOT here must still be absent. */
|
|
180
|
+
export const DEPLOY_KEEP_NAMES = [...DEPLOY_KEEP];
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* THE SAME INFRA CREDENTIALS, HANDED TO THE REDACTOR — never to the wire
|
|
184
|
+
* (2026-09-21, the review).
|
|
185
|
+
*
|
|
186
|
+
* THE REGRESSION THIS CLOSES. Until the vault was deleted, `scrub()` was fed
|
|
187
|
+
* the decrypted bundle, and the deploy-scope half of that bundle WAS
|
|
188
|
+
* `CLOUDFLARE_API_TOKEN` and friends — so every line of `wrangler` output that
|
|
189
|
+
* echoed the operator's token was redacted on its way to the server. The
|
|
190
|
+
* replacement scrubber reads the CHECKOUT'S `.env*` files, and an operator's
|
|
191
|
+
* deploy credential is almost never in one: it is exported in the shell they
|
|
192
|
+
* started the daemon in, which is exactly why `DEPLOY_KEEP` above exists. So
|
|
193
|
+
* the one lane that PASSES a secret into a command it then streams to the
|
|
194
|
+
* server had stopped redacting it. `deploy.mjs`'s own header was meanwhile
|
|
195
|
+
* still asserting the opposite — "strictly more of what a deploy log can
|
|
196
|
+
* contain than the vault ever delivered" — which is the defended-local-decision
|
|
197
|
+
* shape: a confident comment over a guarantee that had quietly lapsed.
|
|
198
|
+
*
|
|
199
|
+
* WHY IT LIVES HERE AND NOT IN `env.mjs`. This file is the single definition of
|
|
200
|
+
* what a spawned command may be handed; the redaction list must be the same set
|
|
201
|
+
* or it rots the moment somebody adds a name to `DEPLOY_KEEP` and forgets the
|
|
202
|
+
* other half. Deriving it FROM that array is what makes forgetting impossible,
|
|
203
|
+
* and it is why the merge happens inside `scanEnvForScrub` rather than at a
|
|
204
|
+
* call site somebody has to remember. The dependency direction is the only one
|
|
205
|
+
* that is acyclic besides: this module imports nothing at all.
|
|
206
|
+
*
|
|
207
|
+
* TWO NAMES ARE HERE THAT ARE NOT IN `DEPLOY_KEEP`, and the asymmetry is the
|
|
208
|
+
* point. `CF_API_TOKEN` and `CF_ACCOUNT_ID` are wrangler's older spellings — it
|
|
209
|
+
* still reads them — so a box may well hold the token under that name while the
|
|
210
|
+
* deploy command is handed nothing. REDACTING a value we do not pass costs
|
|
211
|
+
* nothing and covers the operator who is still on the old spelling; PASSING one
|
|
212
|
+
* would widen the blast radius of a repo-controlled `build` string, which is
|
|
213
|
+
* the thing this module exists to bound. Redaction and admission are different
|
|
214
|
+
* questions and this is the one place they are allowed to differ.
|
|
215
|
+
*
|
|
216
|
+
* IT NEVER REACHES THE REPORT. `/fleet/env-report` is a statement about the
|
|
217
|
+
* CHECKOUT'S FILES — that is the comparison the owner asked for, "show the env
|
|
218
|
+
* of each of the machines (for comparison)" — and this process's environment is
|
|
219
|
+
* not a fact about the checkout. Relaying it would put a box's shell
|
|
220
|
+
* configuration on the wire under a heading that claims to be about a directory,
|
|
221
|
+
* which is both a lie and a wider disclosure than anybody asked for. Values
|
|
222
|
+
* never leave the box either way; names would, and a name is enough to tell
|
|
223
|
+
* somebody which credential to go looking for.
|
|
224
|
+
*/
|
|
225
|
+
export function processEnvSecrets() {
|
|
226
|
+
const out = [];
|
|
227
|
+
for (const name of [...DEPLOY_KEEP, 'CF_API_TOKEN', 'CF_ACCOUNT_ID']) {
|
|
228
|
+
const value = process.env[name];
|
|
229
|
+
if (typeof value === 'string' && value) out.push({ name, value });
|
|
230
|
+
}
|
|
231
|
+
return out;
|
|
232
|
+
}
|
package/bin/lib/config.mjs
CHANGED
|
@@ -293,8 +293,36 @@ export const PROCESS_STARTED_AT = new Date(
|
|
|
293
293
|
export let FLEET_TOKEN =
|
|
294
294
|
argFlag('--fleet') || process.env.FLOWVIANT_FLEET || CREDENTIAL.entry?.fleetToken || '';
|
|
295
295
|
|
|
296
|
+
/**
|
|
297
|
+
* WHICH PROJECT THIS DAEMON SERVES, or null when nothing on this box names one.
|
|
298
|
+
*
|
|
299
|
+
* ADDED 2026-09-21 for one consumer: it SALTS the env report's value
|
|
300
|
+
* fingerprints (`env.mjs`), so an eight-character hash over `production` or
|
|
301
|
+
* `3000` stops being a dictionary lookup anybody with the stored report can
|
|
302
|
+
* perform. The whole comparison is between the boxes of ONE project, so a
|
|
303
|
+
* per-project salt costs the feature nothing.
|
|
304
|
+
*
|
|
305
|
+
* NULL IS A REAL ANSWER AND IS NOT PAPERED OVER. A credential handed in through
|
|
306
|
+
* `FLOWVIANT_FLEET` or `--fleet` names no project until the roster does, and the
|
|
307
|
+
* consumer's rule is that a box which cannot name its project reports NO env
|
|
308
|
+
* files rather than fingerprints salted with something else — incomparable
|
|
309
|
+
* hashes would render as a confident "these boxes differ" beside a box holding
|
|
310
|
+
* the identical value. Ignorance renders nothing.
|
|
311
|
+
*
|
|
312
|
+
* `let`, for the same reason `FLEET_TOKEN` is: when cli.mjs answers an
|
|
313
|
+
* ambiguous store with a picker, `adoptStoredCredential` is what settles both,
|
|
314
|
+
* and ES named imports are LIVE bindings so every importer sees the answer.
|
|
315
|
+
* A frozen copy read at import would salt a picked project's report with null
|
|
316
|
+
* and report nothing at all, for the whole life of the process.
|
|
317
|
+
*/
|
|
318
|
+
export let PROJECT_ID = CREDENTIAL.entry?.projectId ?? null;
|
|
319
|
+
|
|
296
320
|
/** cli.mjs's picker chose. Must run BEFORE runFleetDaemon — nothing here
|
|
297
321
|
* re-authenticates a connection already made. */
|
|
298
322
|
export function adoptStoredCredential(entry) {
|
|
299
323
|
if (entry?.fleetToken) FLEET_TOKEN = entry.fleetToken;
|
|
324
|
+
// The project id travels WITH the token, always. They are one answer, and a
|
|
325
|
+
// picker that updated only the credential left the fingerprint salt null on
|
|
326
|
+
// exactly the boxes that had to be asked which project they serve.
|
|
327
|
+
if (entry?.projectId) PROJECT_ID = entry.projectId;
|
|
300
328
|
}
|
package/bin/lib/deploy.mjs
CHANGED
|
@@ -1,13 +1,65 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cloudflare DevOps — the daemon runs the user's own `wrangler`. Broker-not-
|
|
3
|
-
* host: no cloud credential ever reaches Flowviant. A
|
|
4
|
-
* claims deploy jobs off the roster, runs build →
|
|
3
|
+
* host: no cloud credential ever reaches Flowviant. A daemon on a project with
|
|
4
|
+
* deploy allowed claims deploy jobs off the roster, runs build → deploy →
|
|
5
5
|
* verify, and reports the outcome. It also reports its parsed
|
|
6
6
|
* .flowviant/deploy.json so the app can list targets, and (basic) observes
|
|
7
7
|
* out-of-band deployments.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* ── THE MACHINE'S OWN ENVIRONMENT IS THE SOURCE OF A DEPLOY'S SECRETS
|
|
10
|
+
* (2026-09-21), BECAUSE FLOWVIANT NO LONGER HOLDS ANY ──
|
|
11
|
+
*
|
|
12
|
+
* This lane used to get both halves of its secrets from the end-to-end
|
|
13
|
+
* encrypted vault: `deployCreds()` put the deploy-scope credentials in the
|
|
14
|
+
* command's environment, and `appSecretsFor('prod')` fed `wrangler secret put`
|
|
15
|
+
* on stdin so a prod deploy pushed the project's app secrets to the provider's
|
|
16
|
+
* own store. The vault is DELETED — the owner: "no i dont want it. unless its
|
|
17
|
+
* needed where i want to show the env of each of the machines (for
|
|
18
|
+
* comparison)", and on the recovery passphrase that protected it, "no, its fine
|
|
19
|
+
* to repaste from providers".
|
|
20
|
+
*
|
|
21
|
+
* So both halves are gone, and what replaces them is the operator:
|
|
22
|
+
*
|
|
23
|
+
* - the deploy COMMAND runs with the infra credentials that are in this
|
|
24
|
+
* daemon's own environment, passed through `childEnv`'s opt-in `deploy: true`
|
|
25
|
+
* allowlist. If `wrangler` authenticates when the operator runs it in that
|
|
26
|
+
* shell, it authenticates here.
|
|
27
|
+
* - the `pushSecrets` step is DELETED outright rather than reimplemented
|
|
28
|
+
* against `process.env`. It existed to move values Flowviant was custodian
|
|
29
|
+
* of; with no custody there is nothing here that the operator does not
|
|
30
|
+
* already have in front of them, and `wrangler secret put` is a command they
|
|
31
|
+
* can run. Flowviant automating a push of secrets it does not hold, from an
|
|
32
|
+
* environment it does not own, into a provider store it cannot read back, is
|
|
33
|
+
* the shape of a feature that fails silently and invisibly. `target
|
|
34
|
+
* .pushSecrets` is still PARSED and reported (a dormant key is the standing
|
|
35
|
+
* call) — it simply drives nothing.
|
|
36
|
+
*
|
|
37
|
+
* ── WHAT A DEPLOY LOG IS ACTUALLY SCRUBBED AGAINST, corrected 2026-09-21 by
|
|
38
|
+
* the review that caught this paragraph asserting a guarantee it had lost ──
|
|
39
|
+
*
|
|
40
|
+
* Every log line that leaves the machine still passes through `scrub`. What
|
|
41
|
+
* this paragraph claimed for a few hours was that being fed from the checkout's
|
|
42
|
+
* `.env*` files was "strictly more of what a deploy log can contain than the
|
|
43
|
+
* vault ever delivered", and for THIS lane that was exactly backwards. The
|
|
44
|
+
* vault's deploy-scope half WAS `CLOUDFLARE_API_TOKEN` and friends, so those
|
|
45
|
+
* were redacted; an operator's own token lives in the shell they started the
|
|
46
|
+
* daemon in — which is the entire reason `childEnv`'s `deploy: true` widening
|
|
47
|
+
* exists — and almost never in a checkout file. So the one lane that hands a
|
|
48
|
+
* credential to a command and then streams that command's stdout to the server
|
|
49
|
+
* was the one lane no longer redacting it.
|
|
50
|
+
*
|
|
51
|
+
* It is fed from BOTH now, and the join is made in `scanEnvForScrub` rather
|
|
52
|
+
* than here so no call site has to remember: the checkout's `.env*` files, plus
|
|
53
|
+
* the PRESENT values of `childEnv`'s own `DEPLOY_KEEP` names out of
|
|
54
|
+
* `process.env` (see `processEnvSecrets`, which also explains why `CF_API_TOKEN`
|
|
55
|
+
* is redacted although it is never passed, and why none of this reaches the
|
|
56
|
+
* `/fleet/env-report` wire — that report is a statement about the checkout's
|
|
57
|
+
* files, and this box's shell is not one). Deriving the redaction list from the
|
|
58
|
+
* admission list is what stops the two drifting the next time a name is added.
|
|
59
|
+
*
|
|
60
|
+
* The honest residue, stated: a credential that is neither in `DEPLOY_KEEP` nor
|
|
61
|
+
* in a checkout `.env*` — one a `build` script fetches for itself, say — is not
|
|
62
|
+
* redacted, because this daemon has never seen it.
|
|
11
63
|
*/
|
|
12
64
|
|
|
13
65
|
|
|
@@ -15,7 +67,7 @@ import { spawn } from 'node:child_process';
|
|
|
15
67
|
import { join } from 'node:path';
|
|
16
68
|
import { FLEET_URL, FLEET_TOKEN, USER_AGENT, DAEMON_INSTANCE } from './config.mjs';
|
|
17
69
|
import { c, note, ok, warn } from './ui.mjs';
|
|
18
|
-
import {
|
|
70
|
+
import { scrub, myPubB64 } from './env.mjs';
|
|
19
71
|
import { childEnv } from './childEnv.mjs';
|
|
20
72
|
import { git } from './git.mjs';
|
|
21
73
|
|
|
@@ -131,7 +183,7 @@ export async function reportDeployConfig(repoRoot, baseRef) {
|
|
|
131
183
|
/** Run a shell command ASYNC (never blocks the daemon's event loop — the
|
|
132
184
|
* reconcile poll + the deploy heartbeat must keep firing during a long
|
|
133
185
|
* deploy). Captures combined + scrubbed output; resolves {ok,out,code}. */
|
|
134
|
-
function run(command, { cwd, env
|
|
186
|
+
function run(command, { cwd, env }) {
|
|
135
187
|
return new Promise((resolve) => {
|
|
136
188
|
const child = spawn(command, { cwd, env, shell: true, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
137
189
|
let buf = '';
|
|
@@ -148,21 +200,17 @@ function run(command, { cwd, env, input }) {
|
|
|
148
200
|
/* already gone */
|
|
149
201
|
}
|
|
150
202
|
}, 30 * 60_000);
|
|
151
|
-
// A broken pipe (child exits before
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
203
|
+
// A broken pipe (a child that exits before its stdin is closed) surfaces as
|
|
204
|
+
// an ASYNC 'error' on the stdin stream, which the try/catch around this
|
|
205
|
+
// cannot catch. Without a listener it is an uncaught exception that kills
|
|
206
|
+
// the whole daemon. Swallow it.
|
|
207
|
+
//
|
|
208
|
+
// The `input` parameter this used to take went with the prod-secret push —
|
|
209
|
+
// it existed so a value could reach `wrangler secret put` on stdin rather
|
|
210
|
+
// than argv, and there is no value left to send. Closing stdin immediately
|
|
211
|
+
// is now the only case.
|
|
155
212
|
child.stdin.on('error', () => {});
|
|
156
|
-
|
|
157
|
-
try {
|
|
158
|
-
child.stdin.write(input);
|
|
159
|
-
child.stdin.end();
|
|
160
|
-
} catch {
|
|
161
|
-
/* stdin closed */
|
|
162
|
-
}
|
|
163
|
-
} else {
|
|
164
|
-
child.stdin.end();
|
|
165
|
-
}
|
|
213
|
+
child.stdin.end();
|
|
166
214
|
child.on('close', (code) => {
|
|
167
215
|
clearTimeout(timer);
|
|
168
216
|
resolve({ ok: code === 0, code: code ?? -1, out: scrub(buf) });
|
|
@@ -198,9 +246,9 @@ const claiming = new Set(); // in-flight guard (single-flight per daemon process
|
|
|
198
246
|
* A deploy is IRREVERSIBLE and the report is not: a transient 5xx, a DNS blip
|
|
199
247
|
* or the 30s timeout meant the outcome never landed, the heartbeat stopped,
|
|
200
248
|
* and three minutes later the server requeued the job and this same daemon ran
|
|
201
|
-
* `wrangler rollback` — or a full prod deploy
|
|
202
|
-
*
|
|
203
|
-
*
|
|
249
|
+
* `wrangler rollback` — or a full prod deploy — a SECOND time, leaving
|
|
250
|
+
* production two versions behind the intended one with nothing recording that
|
|
251
|
+
* it happened twice.
|
|
204
252
|
*
|
|
205
253
|
* So the process remembers. Not a substitute for the report (see the retry
|
|
206
254
|
* below, which is the real fix); a floor under it, for the case where the
|
|
@@ -212,8 +260,8 @@ const ran = new Set();
|
|
|
212
260
|
|
|
213
261
|
/**
|
|
214
262
|
* Process queued deploy jobs from the roster. `ctx` = { repoRoot, baseRef,
|
|
215
|
-
* myPubB64 }. Each job: claim → build →
|
|
216
|
-
*
|
|
263
|
+
* myPubB64 }. Each job: claim → build → deploy → verify → report. Runs
|
|
264
|
+
* concurrently but one-per-jobId.
|
|
217
265
|
*/
|
|
218
266
|
export function processDeployJobs(jobs, ctx) {
|
|
219
267
|
if (!Array.isArray(jobs) || !jobs.length) return;
|
|
@@ -315,7 +363,13 @@ async function runDeploy(job, target, ctx) {
|
|
|
315
363
|
// command — and of `target.build`, which is a string the REPO controls —
|
|
316
364
|
// under a comment asserting the opposite. A denylist is a claim about a set
|
|
317
365
|
// you cannot see; this is built from {} instead.
|
|
318
|
-
|
|
366
|
+
//
|
|
367
|
+
// `deploy: true` is the OPT-IN SECOND GROUP (childEnv.mjs): a named set of
|
|
368
|
+
// infra credential variables kept out of this daemon's own environment, and
|
|
369
|
+
// nothing else. It replaced `extra: deployCreds()` when the vault was deleted
|
|
370
|
+
// — the credentials are the OPERATOR's now, in the shell they started the
|
|
371
|
+
// daemon in, rather than values Flowviant decrypted onto the box.
|
|
372
|
+
const env = childEnv({ cwd: ctx.repoRoot, deploy: true }); // infra creds; never a file
|
|
319
373
|
const logs = [];
|
|
320
374
|
// Rollback is a single wrangler command; deploy is build → secrets → deploy.
|
|
321
375
|
if (job.kind === 'rollback') {
|
|
@@ -331,24 +385,17 @@ async function runDeploy(job, target, ctx) {
|
|
|
331
385
|
if (!b.ok) return { ok: false, message: `build failed:\n${logs.slice(-6).join('\n')}`, logs };
|
|
332
386
|
}
|
|
333
387
|
|
|
334
|
-
//
|
|
335
|
-
// `
|
|
336
|
-
//
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
? target.commands['secretPut'].replace('{name}', name)
|
|
346
|
-
: `npx wrangler secret put ${name} --env production`;
|
|
347
|
-
const s = await run(putCmd, { cwd: ctx.repoRoot, env, input: secrets[name] });
|
|
348
|
-
if (!s.ok) return { ok: false, message: `pushing secret ${name} failed`, logs };
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
388
|
+
// THE PROD-SECRET PUSH IS DELETED (2026-09-21). What stood here read
|
|
389
|
+
// `appSecretsFor('prod')` out of the vault and piped each value into
|
|
390
|
+
// `wrangler secret put` on stdin — never argv, so no prod plaintext in `ps`.
|
|
391
|
+
// That care was right and it is moot: the vault is gone, Flowviant holds no
|
|
392
|
+
// app secret for anybody, and there is nothing left to push. It is not
|
|
393
|
+
// reimplemented against `process.env`, because a deploy that quietly pushes
|
|
394
|
+
// whatever happens to be exported in the daemon's shell into a provider's
|
|
395
|
+
// secret store is a worse feature than no feature — the operator can see and
|
|
396
|
+
// run `wrangler secret put`, and they are the only one who can tell which
|
|
397
|
+
// values belong there. `target.pushSecrets` stays parsed and reported, which
|
|
398
|
+
// is what every retired key in this product does.
|
|
352
399
|
const cmd = target.commands?.[job.env] || target.command;
|
|
353
400
|
const d = await run(cmd, { cwd: ctx.repoRoot, env });
|
|
354
401
|
logs.push(...tailLines(d.out));
|
|
@@ -379,7 +426,7 @@ async function runDeploy(job, target, ctx) {
|
|
|
379
426
|
* heartbeat, and the server — which requeues a running job after three minutes
|
|
380
427
|
* without one — handed the SAME job back to the SAME daemon, which ran it
|
|
381
428
|
* again. For a rollback that is production two versions behind the intended
|
|
382
|
-
* one; for a prod deploy it is
|
|
429
|
+
* one; for a prod deploy it is the whole deploy run twice. Nothing recorded
|
|
383
430
|
* that it had happened at all.
|
|
384
431
|
*
|
|
385
432
|
* The heartbeat keeps running throughout (the caller's `finally` is what stops
|