@bongos/core 1.19.313 → 1.19.315
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/.bongos-core.json +15 -15
- package/docs/recipes/core-release-pipeline.md +5 -0
- package/docs/recipes/upgrading-the-core.md +2 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/go-live.js +27 -13
- package/scripts/gds/npmrc.js +32 -1
- package/scripts/gds/upgrade.js +22 -7
- package/src/module-api.js +5 -1
- package/tests/go_live.mjs +24 -6
- package/tests/npmrc.mjs +49 -0
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.315",
|
|
6
|
+
"core_contract": "1.19.315",
|
|
7
|
+
"source_commit": "fa29b8da2f1429be8b46153157348ad0893bb702",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-23T21:
|
|
9
|
+
"built_at": "2026-08-23T21:55:22.014Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 323,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 1766,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "194e6d23ae1dc848c9e95e48b8383a33a401686fe98169758b9084884818966b",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -1672,7 +1672,7 @@
|
|
|
1672
1672
|
{
|
|
1673
1673
|
"path": "docs/recipes/core-release-pipeline.md",
|
|
1674
1674
|
"mode": "0000644",
|
|
1675
|
-
"sha256": "
|
|
1675
|
+
"sha256": "15517dbf462ce85e7d161b4e173c90376a19ecad3d0cf57a728f066533697bb2"
|
|
1676
1676
|
},
|
|
1677
1677
|
{
|
|
1678
1678
|
"path": "docs/recipes/devbox-app-release.md",
|
|
@@ -1797,7 +1797,7 @@
|
|
|
1797
1797
|
{
|
|
1798
1798
|
"path": "docs/recipes/upgrading-the-core.md",
|
|
1799
1799
|
"mode": "0000644",
|
|
1800
|
-
"sha256": "
|
|
1800
|
+
"sha256": "82a8a5b17d465b9048ce645e2bc13acaa492f766726cc79981fff1352a92d1b0"
|
|
1801
1801
|
},
|
|
1802
1802
|
{
|
|
1803
1803
|
"path": "docs/recipes/windows-builders.md",
|
|
@@ -4582,12 +4582,12 @@
|
|
|
4582
4582
|
{
|
|
4583
4583
|
"path": "package-lock.json",
|
|
4584
4584
|
"mode": "0000644",
|
|
4585
|
-
"sha256": "
|
|
4585
|
+
"sha256": "501134bb188ffffca60077b5b43b4e068e5713ff2c35002380cddad4a86076b4"
|
|
4586
4586
|
},
|
|
4587
4587
|
{
|
|
4588
4588
|
"path": "package.json",
|
|
4589
4589
|
"mode": "0000644",
|
|
4590
|
-
"sha256": "
|
|
4590
|
+
"sha256": "4e8358d26d8d6cdd6c5378b0860ebc58671a75d40ad790bb896000e91f9bd0c7"
|
|
4591
4591
|
},
|
|
4592
4592
|
{
|
|
4593
4593
|
"path": "public-docs/index.html",
|
|
@@ -5317,7 +5317,7 @@
|
|
|
5317
5317
|
{
|
|
5318
5318
|
"path": "scripts/gds/go-live.js",
|
|
5319
5319
|
"mode": "0000644",
|
|
5320
|
-
"sha256": "
|
|
5320
|
+
"sha256": "fa7803b178d64eda47d4f05032353582f7000359b13caea7d018112f03c9906a"
|
|
5321
5321
|
},
|
|
5322
5322
|
{
|
|
5323
5323
|
"path": "scripts/gds/goal.js",
|
|
@@ -5477,7 +5477,7 @@
|
|
|
5477
5477
|
{
|
|
5478
5478
|
"path": "scripts/gds/npmrc.js",
|
|
5479
5479
|
"mode": "0000644",
|
|
5480
|
-
"sha256": "
|
|
5480
|
+
"sha256": "1beffeed111e8528cfa0a629d5ef81c766023de5c6820a81659a390070868228"
|
|
5481
5481
|
},
|
|
5482
5482
|
{
|
|
5483
5483
|
"path": "scripts/gds/oauth-secret.js",
|
|
@@ -5877,7 +5877,7 @@
|
|
|
5877
5877
|
{
|
|
5878
5878
|
"path": "scripts/gds/upgrade.js",
|
|
5879
5879
|
"mode": "0000644",
|
|
5880
|
-
"sha256": "
|
|
5880
|
+
"sha256": "80856db9974df9b3451decf55385eeb78bcba61e523ca09a2ea070deb48991ab"
|
|
5881
5881
|
},
|
|
5882
5882
|
{
|
|
5883
5883
|
"path": "scripts/gds/validate-design.js",
|
|
@@ -6257,7 +6257,7 @@
|
|
|
6257
6257
|
{
|
|
6258
6258
|
"path": "src/module-api.js",
|
|
6259
6259
|
"mode": "0000644",
|
|
6260
|
-
"sha256": "
|
|
6260
|
+
"sha256": "ce9693c479994dbf2f113c783048cd9b3dca8c3cdbc14c655ffdc0f893227eb3"
|
|
6261
6261
|
},
|
|
6262
6262
|
{
|
|
6263
6263
|
"path": "src/module-loader/catalog.js",
|
|
@@ -7182,7 +7182,7 @@
|
|
|
7182
7182
|
{
|
|
7183
7183
|
"path": "tests/go_live.mjs",
|
|
7184
7184
|
"mode": "0000644",
|
|
7185
|
-
"sha256": "
|
|
7185
|
+
"sha256": "360712fee43b00b410e4b3b8ca27e801ae41966ac0c5ce0a4679fa7943aa554b"
|
|
7186
7186
|
},
|
|
7187
7187
|
{
|
|
7188
7188
|
"path": "tests/goal_abuse_matrix.mjs",
|
|
@@ -7977,7 +7977,7 @@
|
|
|
7977
7977
|
{
|
|
7978
7978
|
"path": "tests/npmrc.mjs",
|
|
7979
7979
|
"mode": "0000644",
|
|
7980
|
-
"sha256": "
|
|
7980
|
+
"sha256": "1aaa68ee1a4c4a58563cef86aac44cfa0e046ad7376d3a2108bf6dd455580942"
|
|
7981
7981
|
},
|
|
7982
7982
|
{
|
|
7983
7983
|
"path": "tests/oauth_secret.mjs",
|
|
@@ -188,6 +188,11 @@ The permission classifier blocks the agent from running a mutating `sudo -u lars
|
|
|
188
188
|
What each piece is for — each guards a failure that actually happened:
|
|
189
189
|
|
|
190
190
|
- **Token extraction runs as lars** (the whole command is inside `sudo -u lars bash -c`). Reading `/home/lars/.npmrc` as `jaxri` is `Permission denied`, which yields an **empty** `NPM_TOKEN`, which sends npm to the **public** registry, which 404s on the private `@bongos/core` — and the upgrade rolls back cleanly, having taught you nothing. The `[ -n "$NPM_TOKEN" ] || exit` guard aborts instead of half-running.
|
|
191
|
+
- **Never put the token in the command line — `sudo` logs the full argv (task 1003218).** The shape above is safe *because* the token is read INSIDE the quoted script, so only the `grep` reaches argv. The tempting variant is not:
|
|
192
|
+
```
|
|
193
|
+
sudo -u lars -H env NPM_TOKEN="<redacted>" … bongos upgrade … # ← LEAKS
|
|
194
|
+
```
|
|
195
|
+
The shell expands `$NPM_TOKEN` before `sudo` ever runs, so the real token lands in the system journal — which is persistent on this box. That variant put the live token into 26 journal lines between 2026-08-17 and 2026-08-23. If you need to hand a secret to a privileged child, use `sudo --preserve-env=NPM_TOKEN …`, which passes it through the environment where nothing logs it. Passing "by reference" in the script text is NOT enough; what matters is what argv looks like after expansion.
|
|
191
196
|
- **`--to <VER>` is required** — `bongos upgrade` refuses to guess a target.
|
|
192
197
|
- **`--service` + `--health-url` buy the automatic rollback.** Point the health URL at `http://127.0.0.1:3002/healthz`, **not the public URL** — a pre-launch `basic_auth` on the apex makes a healthy boot look failed and triggers a needless rollback.
|
|
193
198
|
- **`--commit-pin` commits AND pushes the pin.** The instance is a pull-deploy repo: the next sync runs `git reset --hard origin/main`, which reverts an uncommitted pin, and `npm ci` then reinstalls the old core. Nothing errors. This is exactly how live sat 13 releases behind (task 1002712). The same applies to any tracked instance-config edit (`config/branding.json`): commit + push or the next deploy tick undoes it. A commit that fails to *push* saves nothing — deploy.sh resets to the remote.
|
|
@@ -116,6 +116,8 @@ Each subscribed instance: read its installed core → ask the registry (`npm vie
|
|
|
116
116
|
|
|
117
117
|
## Gotchas
|
|
118
118
|
|
|
119
|
+
- **`--registry` no longer needs a hand-exported `NPM_TOKEN`** (task 1002754). The instance `.npmrc` reads `_authToken=<redacted>, and npm gives that per-project key precedence — so with the env var unset it expanded EMPTY and **masked a perfectly valid literal token in `~/.npmrc`** (npm went anonymous; the private core 404'd; the error blamed a "missing" credential the box actually held). `bongos upgrade --registry` now resolves the credential itself: env `NPM_TOKEN` wins, else the **user-level `~/.npmrc`'s literal `//registry.npmjs.org/:_authToken=…`** is injected into the install's environment (forward install and rollback reinstall both). A pin bump on a box whose `~/.npmrc` authenticates therefore succeeds with **no exported env var**. Resolver: `resolveNpmToken()` in [`scripts/gds/npmrc.js`](../../scripts/gds/npmrc.js).
|
|
120
|
+
- **A green `publish` run is not evidence of a publish.** The publish-on-merge lane succeeds on its no-op paths too (cascade terminator, armed-gate). Read the run's **job summary**, which now states the verdict explicitly: `PUBLISHED @bongos/core@<v>` vs `NO-OP — nothing was published this run`.
|
|
119
121
|
- **`PGDATABASE` is load-bearing on a co-tenant droplet.** `migrate.sh` defaults `PGDATABASE` to `example` — **production**. Always set `PGDATABASE=<instance-db>` explicitly, or you migrate the wrong database.
|
|
120
122
|
- **The health check polls the restart** (fixed in task [#1984](https://example.com/builders#/task/1984)). The service binds its port ~1s after `systemctl restart`, so `--health-url` now polls up to ~30s for a 2xx before reporting a warning — an immediate `fetch failed` no longer means a failed upgrade. (Pre-1984 cores fetched once and spuriously reported `fetch failed`; there, confirm with `is-active` + a manual `curl`.)
|
|
121
123
|
- **The ledger auto-inserts via `PGDATABASE` too** (fixed in task [#1984](https://example.com/builders#/task/1984)). A consumer on `PGDATABASE` + peer auth now gets its `core_upgrades` row written automatically — the channel delegates the insert to `psql -d "$PGDATABASE"`, exactly like `migrate.sh`, so no manual `psql INSERT` is needed. (`DATABASE_URL` still works, via the `pg` driver.) Pre-1984 cores only auto-wrote with `DATABASE_URL`; on those, record the row manually.
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.315",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.315",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.315",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/scripts/gds/go-live.js
CHANGED
|
@@ -177,22 +177,36 @@ function remote(target, script, { run = spawnSync, timeoutMs = 900000 } = {}) {
|
|
|
177
177
|
// `sudo -u <user> -H env K=V … <cmd>` — the shape every runbook entry uses. Returns
|
|
178
178
|
// a string safe to embed because every component was allowlist-validated.
|
|
179
179
|
//
|
|
180
|
-
// `forward` names variables
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
180
|
+
// `forward` names variables that exist in the calling root shell and must reach the
|
|
181
|
+
// child — NPM_TOKEN above all. sudo's env_reset drops it, and an explicit `env K=V`
|
|
182
|
+
// list that omits it drops it too, so `npm install` 404s the PRIVATE package and the
|
|
183
|
+
// upgrade auto-rolls-back, which reads as "the release is broken" when it is only
|
|
184
|
+
// "the token never arrived". Sourcing the env file in the OUTER shell is necessary
|
|
185
|
+
// and NOT sufficient.
|
|
186
|
+
//
|
|
187
|
+
// A FORWARDED VARIABLE IS A SECRET, SO IT MUST NOT ENTER argv (task 1003218).
|
|
188
|
+
// `env K="$K"` keeps the value out of the script TEXT, but the shell EXPANDS it
|
|
189
|
+
// before sudo runs — and sudo logs the full argv. That is how 26 lines of
|
|
190
|
+
// `COMMAND=... NPM_TOKEN=npm_<real token> ...` ended up in the droplet's PERSISTENT
|
|
191
|
+
// journal between 2026-08-17 and 2026-08-23. Passing by reference is NOT the same as
|
|
192
|
+
// keeping a secret out of argv; only `--preserve-env` does that, handing the variable
|
|
193
|
+
// over through the environment where nothing logs it. Verified side by side on the
|
|
194
|
+
// box: both forms reach the child, only this one leaves no value in the journal.
|
|
195
|
+
//
|
|
196
|
+
// The plain `env K=V` list is still used for NON-secret values (PGDATABASE, HOME) —
|
|
197
|
+
// those are configuration, and seeing them in a log is useful rather than harmful.
|
|
187
198
|
function asOwner(target, cmd, { forward = [] } = {}) {
|
|
188
199
|
const envPairs = Object.entries(target.env || {}).map(([k, v]) => `${k}=${v}`);
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
200
|
+
const preserve = forward.filter((k) => SAFE_ENV_KEY_RE.test(k)); // never interpolate an unvetted name
|
|
201
|
+
if (!target.runAs) {
|
|
202
|
+
// No privilege drop: the child already inherits our environment, so a forwarded
|
|
203
|
+
// secret needs no mention at all.
|
|
204
|
+
const joined = envPairs.join(' ');
|
|
205
|
+
return `${joined ? `env ${joined} ` : ''}${cmd}`.trim();
|
|
192
206
|
}
|
|
193
|
-
const
|
|
194
|
-
const
|
|
195
|
-
return
|
|
207
|
+
const keep = preserve.length ? `--preserve-env=${preserve.join(',')} ` : '';
|
|
208
|
+
const joined = [...envPairs, `HOME=/home/${target.runAs}`].join(' ');
|
|
209
|
+
return `sudo ${keep}-u ${target.runAs} -H env ${joined} ${cmd}`.trim();
|
|
196
210
|
}
|
|
197
211
|
|
|
198
212
|
// POSIX single-quote a value for embedding in a shell command.
|
package/scripts/gds/npmrc.js
CHANGED
|
@@ -48,6 +48,37 @@ function hasNpmToken(env = process.env) {
|
|
|
48
48
|
return !!String(env[TOKEN_ENV] || '').trim();
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
// resolveNpmToken (task 1002754) — the install-time credential resolver that removes
|
|
52
|
+
// the hand-exported-NPM_TOKEN step from deploys.
|
|
53
|
+
//
|
|
54
|
+
// THE TRAP. The committed instance `.npmrc` reads `_authToken=${NPM_TOKEN}`; npm gives
|
|
55
|
+
// a per-project key precedence over the user-level one, so with NPM_TOKEN unset the
|
|
56
|
+
// expansion is EMPTY and it MASKS a perfectly valid literal token in ~/.npmrc — npm
|
|
57
|
+
// goes anonymous and the private core 404s. Every deploy then silently depended on the
|
|
58
|
+
// operator remembering `export NPM_TOKEN=…` (the ADR-0162 shape: a human-gated step in
|
|
59
|
+
// the ship path). npm's config syntax cannot express "env var, else fall through", so
|
|
60
|
+
// the fix is to RESOLVE the credential here and feed it to the child install's env —
|
|
61
|
+
// the committed file stays secret-free and unchanged.
|
|
62
|
+
//
|
|
63
|
+
// Order: env NPM_TOKEN wins (explicit operator intent), else the user-level ~/.npmrc's
|
|
64
|
+
// literal `//<host>/:_authToken=<value>` (ignoring env-fed `${…}` values — those are
|
|
65
|
+
// the trap, not a credential). Returns { token, source: 'env'|'user-npmrc'|null }.
|
|
66
|
+
function resolveNpmToken({ env = process.env, homedir, fsImpl = fs, host = REGISTRY_HOST } = {}) {
|
|
67
|
+
const fromEnv = String(env[TOKEN_ENV] || '').trim();
|
|
68
|
+
if (fromEnv) return { token: fromEnv, source: 'env' };
|
|
69
|
+
const home = homedir || (env.HOME || env.USERPROFILE || '');
|
|
70
|
+
if (home) {
|
|
71
|
+
try {
|
|
72
|
+
const raw = fsImpl.readFileSync(path.join(home, '.npmrc'), 'utf8');
|
|
73
|
+
const re = new RegExp(`^\\s*//${host.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/:_authToken\\s*=\\s*(.+?)\\s*$`, 'm');
|
|
74
|
+
const m = re.exec(raw);
|
|
75
|
+
const val = m && m[1].replace(/^["']|["']$/g, '');
|
|
76
|
+
if (val && !val.includes('${')) return { token: val, source: 'user-npmrc' };
|
|
77
|
+
} catch { /* no user npmrc, unreadable — fall through to null */ }
|
|
78
|
+
}
|
|
79
|
+
return { token: null, source: null };
|
|
80
|
+
}
|
|
81
|
+
|
|
51
82
|
// Has the operator opted the standalone scaffold into the registry path? OFF by
|
|
52
83
|
// default — the vendored tarball stays the default until the registry path is proven
|
|
53
84
|
// end-to-end (ADR 0134). '1'/'true'/'yes'/'on' (case-insensitive) → true.
|
|
@@ -76,7 +107,7 @@ function writeNpmrc(dir, { fsImpl = fs, dryRun = false, overwrite = true, log =
|
|
|
76
107
|
|
|
77
108
|
module.exports = {
|
|
78
109
|
REGISTRY_HOST, CORE_SCOPE, TOKEN_ENV, REQUEST_ENV,
|
|
79
|
-
npmrcContent, hasNpmToken, registryCoreRequested, writeNpmrc,
|
|
110
|
+
npmrcContent, hasNpmToken, resolveNpmToken, registryCoreRequested, writeNpmrc,
|
|
80
111
|
};
|
|
81
112
|
|
|
82
113
|
// Tiny CLI: `node scripts/gds/npmrc.js [--print | --write <dir>]` — print the `.npmrc`
|
package/scripts/gds/upgrade.js
CHANGED
|
@@ -30,7 +30,7 @@ const fs = require('node:fs');
|
|
|
30
30
|
const path = require('node:path');
|
|
31
31
|
const { spawnSync } = require('node:child_process');
|
|
32
32
|
const { arg, hasFlag } = require('./cli-lib');
|
|
33
|
-
const { writeNpmrc, hasNpmToken } = require('./npmrc'); // ADR 0134 (task 2090): the --registry channel's env-fed .npmrc
|
|
33
|
+
const { writeNpmrc, hasNpmToken, resolveNpmToken } = require('./npmrc'); // ADR 0134 (task 2090): the --registry channel's env-fed .npmrc; resolveNpmToken task 1002754
|
|
34
34
|
|
|
35
35
|
const CORE_PKG = '@bongos/core';
|
|
36
36
|
const ARTIFACT = 'bongos-core';
|
|
@@ -284,10 +284,14 @@ function reportModulePreflight(mods, targetVersion, { log = console.log, err = c
|
|
|
284
284
|
return lines;
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
-
function npmInstall(instanceDir, run = spawnSync) {
|
|
287
|
+
function npmInstall(instanceDir, run = spawnSync, extraEnv = null) {
|
|
288
288
|
// `npm install` (NOT `npm ci`): the pin just changed, so the lockfile must be regenerated from the
|
|
289
289
|
// new tarball; `npm ci` would refuse on the package.json↔lock divergence.
|
|
290
|
-
|
|
290
|
+
// extraEnv (task 1002754): the registry path injects a resolved NPM_TOKEN here so the
|
|
291
|
+
// instance `.npmrc`'s `${NPM_TOKEN}` expansion never shadows a valid user-level token.
|
|
292
|
+
const opts = { cwd: instanceDir, stdio: 'inherit' };
|
|
293
|
+
if (extraEnv) opts.env = { ...process.env, ...extraEnv };
|
|
294
|
+
const r = run('npm', ['install', '--no-audit', '--no-fund'], opts);
|
|
291
295
|
return !r || r.error ? { ok: false, reason: r && r.error ? r.error.message : 'spawn failed' } : { ok: r.status === 0, code: r.status };
|
|
292
296
|
}
|
|
293
297
|
|
|
@@ -703,8 +707,11 @@ async function rollback({ instanceDir, snapshot, fromVersion, toVersion, opts },
|
|
|
703
707
|
try { restore(instanceDir, snapshot.prevPin, fsImpl); result.restoreOk = true; log(` ✓ restored pin → ${snapshot.prevPin}`); }
|
|
704
708
|
catch (e) { result.restoreOk = false; err(` ! could not restore pin (${e.message}) — package.json may need manual repair`); }
|
|
705
709
|
|
|
706
|
-
// 2. reinstall the previous core.
|
|
707
|
-
|
|
710
|
+
// 2. reinstall the previous core. Same credential resolution as the forward path
|
|
711
|
+
// (task 1002754): the previous pin may be a registry pin too, and a rollback
|
|
712
|
+
// must never fail on the env-shadowing trap the forward install just avoided.
|
|
713
|
+
const rbCred = resolveNpmToken();
|
|
714
|
+
const inst = npmInstall(instanceDir, run, rbCred.source === 'user-npmrc' ? { NPM_TOKEN: rbCred.token } : null);
|
|
708
715
|
result.reinstallOk = !!inst.ok;
|
|
709
716
|
if (inst.ok) log(' ✓ reinstalled previous core');
|
|
710
717
|
else err(` ! reinstall of previous core failed (${inst.reason || 'exit ' + inst.code}) — the instance may need manual repair`);
|
|
@@ -813,9 +820,17 @@ async function runUpgrade(opts, deps = {}) {
|
|
|
813
820
|
// REGISTRY (--registry): pin @bongos/core@<version> + ensure the env-fed .npmrc, so
|
|
814
821
|
// the `npm install` below pulls the PRIVATE package from the registry (needs NPM_TOKEN).
|
|
815
822
|
// VENDORED (default, --from <tgz>): copy the .tgz into vendor/ + rewrite the file: dep.
|
|
823
|
+
let installEnv = null;
|
|
816
824
|
if (opts.registry) {
|
|
817
825
|
if (opts.from) return { ok: false, error: '--registry and --from are mutually exclusive (registry pull vs vendored tarball) — pick one core source' };
|
|
818
|
-
|
|
826
|
+
// task 1002754 — resolve the credential HERE instead of demanding a hand-exported
|
|
827
|
+
// env var: env NPM_TOKEN wins, else the user-level ~/.npmrc's literal token is
|
|
828
|
+
// injected into the install's env so the instance `.npmrc`'s `${NPM_TOKEN}`
|
|
829
|
+
// expansion (which OUTRANKS the user-level key and expands EMPTY when unset)
|
|
830
|
+
// can never shadow a working credential again.
|
|
831
|
+
const cred = resolveNpmToken();
|
|
832
|
+
if (!cred.token) log(' ! no NPM_TOKEN in env and no literal token in ~/.npmrc — `npm install` will 404 on the PRIVATE @bongos/core until a read-only automation token is provided (ADR 0134).');
|
|
833
|
+
else if (cred.source === 'user-npmrc') { installEnv = { NPM_TOKEN: cred.token }; log(' ✓ registry credential: inherited from the user-level ~/.npmrc (no NPM_TOKEN export needed — task 1002754)'); }
|
|
819
834
|
const prevPin = dryRun ? null : writeRegistryPin(instanceDir, targetVersion, fsImpl);
|
|
820
835
|
if (!dryRun) { writeNpmrc(instanceDir, { fsImpl, overwrite: false, log }); snapshot = { prevPin, prevVersion: fromVersion }; }
|
|
821
836
|
log(` ${dryRun ? '[dry-run] would pin' : '✓ pinned'} ${CORE_PKG} → ${targetVersion} (registry)${dryRun ? ' + write an env-fed .npmrc' : ''}${prevPin ? ` (was ${prevPin})` : ''}`);
|
|
@@ -837,7 +852,7 @@ async function runUpgrade(opts, deps = {}) {
|
|
|
837
852
|
if (dryRun) { log(' [dry-run] would: npm install → npm run migrate → materialize .claude → regen API docs+client → restart → verify'); return { ok: true, dryRun: true, fromVersion, toVersion: targetVersion }; }
|
|
838
853
|
|
|
839
854
|
// 3. npm install.
|
|
840
|
-
const inst = npmInstall(instanceDir, run);
|
|
855
|
+
const inst = npmInstall(instanceDir, run, installEnv);
|
|
841
856
|
if (!inst.ok) {
|
|
842
857
|
const error = `npm install failed (${inst.reason || 'exit ' + inst.code})`;
|
|
843
858
|
if (rollbackOnFailure && snapshot) {
|
package/src/module-api.js
CHANGED
|
@@ -1053,8 +1053,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
1053
1053
|
// landed since 1.19.311 with no explicit bump. run 32665664699. (task 1002620)
|
|
1054
1054
|
// 1.19.313 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1055
1055
|
// landed since 1.19.312 with no explicit bump. run 32668457836. (task 1002620)
|
|
1056
|
+
// 1.19.314 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1057
|
+
// landed since 1.19.313 with no explicit bump. run 32668599111. (task 1002620)
|
|
1058
|
+
// 1.19.315 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1059
|
+
// landed since 1.19.314 with no explicit bump. run 32668969037. (task 1002620)
|
|
1056
1060
|
// ---------------------------------------------------------------------------
|
|
1057
|
-
const CORE_VERSION = '1.19.
|
|
1061
|
+
const CORE_VERSION = '1.19.315'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
1058
1062
|
|
|
1059
1063
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
1060
1064
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/go_live.mjs
CHANGED
|
@@ -296,11 +296,17 @@ test("'could not check' is a distinct halt from 'absent'", () => {
|
|
|
296
296
|
// `env K=V` list that omits it drops it too. This cost a real failed go-live.
|
|
297
297
|
// ---------------------------------------------------------------------------
|
|
298
298
|
|
|
299
|
-
test('a forwarded
|
|
299
|
+
test('a forwarded secret NEVER enters argv — it is handed over via --preserve-env', () => {
|
|
300
300
|
const cmd = G.asOwner({ runAs: 'lars', env: { PGDATABASE: 'cb' } }, 'npm install', { forward: ['NPM_TOKEN'] });
|
|
301
|
-
assert.match(cmd,
|
|
302
|
-
//
|
|
303
|
-
|
|
301
|
+
assert.match(cmd, /--preserve-env=NPM_TOKEN/);
|
|
302
|
+
// THE REGRESSION (task 1003218). `env K="$K"` kept the value out of the script TEXT,
|
|
303
|
+
// but the shell expands it before sudo runs and sudo LOGS THE FULL argv — which put
|
|
304
|
+
// the real token into the droplet's persistent journal 26 times. So the command must
|
|
305
|
+
// not mention the variable as an assignment at all, by reference or otherwise.
|
|
306
|
+
assert.ok(!/NPM_TOKEN=/.test(cmd.replace('--preserve-env=NPM_TOKEN', '')),
|
|
307
|
+
'a secret must never appear as an argv assignment — sudo logs argv');
|
|
308
|
+
// Non-secret config is still passed plainly; seeing PGDATABASE in a log is useful.
|
|
309
|
+
assert.match(cmd, /PGDATABASE=cb/);
|
|
304
310
|
});
|
|
305
311
|
|
|
306
312
|
test('forwarding is only wired when the target names an env file to source from', () => {
|
|
@@ -310,12 +316,24 @@ test('forwarding is only wired when the target names an env file to source from'
|
|
|
310
316
|
assert.deepEqual(G.npmForward({}), []);
|
|
311
317
|
});
|
|
312
318
|
|
|
313
|
-
test('an unvetted variable name
|
|
319
|
+
test('an unvetted variable name never reaches the command', () => {
|
|
314
320
|
const cmd = G.asOwner({ runAs: 'me' }, 'x', { forward: ['GOOD', 'bad name', '$(id)', '1BAD'] });
|
|
315
|
-
assert.match(cmd,
|
|
321
|
+
assert.match(cmd, /--preserve-env=GOOD/);
|
|
316
322
|
assert.ok(!cmd.includes('bad name') && !cmd.includes('$(id)') && !cmd.includes('1BAD'));
|
|
317
323
|
});
|
|
318
324
|
|
|
325
|
+
test('with no privilege drop a secret needs no mention at all', () => {
|
|
326
|
+
// The child already inherits our environment, so naming it would be pure exposure.
|
|
327
|
+
const cmd = G.asOwner({ env: { A: 'b' } }, 'x', { forward: ['NPM_TOKEN'] });
|
|
328
|
+
assert.equal(cmd, 'env A=b x');
|
|
329
|
+
assert.ok(!cmd.includes('NPM_TOKEN'));
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test('no forwarding leaves the documented sudo shape untouched', () => {
|
|
333
|
+
assert.equal(G.asOwner({ runAs: 'me', env: { PGDATABASE: 'mydb' } }, 'echo hi'),
|
|
334
|
+
'sudo -u me -H env PGDATABASE=mydb HOME=/home/me echo hi');
|
|
335
|
+
});
|
|
336
|
+
|
|
319
337
|
// ---------------------------------------------------------------------------
|
|
320
338
|
// A failed run must say what the tool said
|
|
321
339
|
// ---------------------------------------------------------------------------
|
package/tests/npmrc.mjs
CHANGED
|
@@ -100,6 +100,55 @@ test('writeNpmrc: overwrite:true (default) replaces an existing .npmrc', () => {
|
|
|
100
100
|
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
+
// ---- resolveNpmToken (task 1002754) ----------------------------------------
|
|
104
|
+
// The install-time credential resolver that removes the hand-exported-NPM_TOKEN
|
|
105
|
+
// deploy step: env wins, else the user-level ~/.npmrc's LITERAL token; env-fed
|
|
106
|
+
// `${…}` values in the user file are the trap, never a credential.
|
|
107
|
+
test('resolveNpmToken: env NPM_TOKEN wins over everything', () => {
|
|
108
|
+
const home = mkdtempSync(join(tmpdir(), 'npmrc-home-'));
|
|
109
|
+
try {
|
|
110
|
+
writeFileSync(join(home, '.npmrc'), '//registry.npmjs.org/:_authToken=npm_userlevel\n');
|
|
111
|
+
const r = n.resolveNpmToken({ env: { NPM_TOKEN: ' npm_fromenv ' }, homedir: home });
|
|
112
|
+
assert.deepEqual(r, { token: 'npm_fromenv', source: 'env' });
|
|
113
|
+
} finally { rmSync(home, { recursive: true, force: true }); }
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('resolveNpmToken: falls back to the user-level ~/.npmrc literal token', () => {
|
|
117
|
+
const home = mkdtempSync(join(tmpdir(), 'npmrc-home-'));
|
|
118
|
+
try {
|
|
119
|
+
writeFileSync(join(home, '.npmrc'),
|
|
120
|
+
'registry=https://registry.npmjs.org/\n//registry.npmjs.org/:_authToken=npm_ABC123\nfund=false\n');
|
|
121
|
+
const r = n.resolveNpmToken({ env: {}, homedir: home });
|
|
122
|
+
assert.deepEqual(r, { token: 'npm_ABC123', source: 'user-npmrc' });
|
|
123
|
+
} finally { rmSync(home, { recursive: true, force: true }); }
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('resolveNpmToken: an env-fed ${…} value in the user file is the trap, not a credential', () => {
|
|
127
|
+
const home = mkdtempSync(join(tmpdir(), 'npmrc-home-'));
|
|
128
|
+
try {
|
|
129
|
+
writeFileSync(join(home, '.npmrc'), '//registry.npmjs.org/:_authToken=${NPM_TOKEN}\n');
|
|
130
|
+
const r = n.resolveNpmToken({ env: {}, homedir: home });
|
|
131
|
+
assert.deepEqual(r, { token: null, source: null });
|
|
132
|
+
} finally { rmSync(home, { recursive: true, force: true }); }
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('resolveNpmToken: a token for a DIFFERENT host is not borrowed', () => {
|
|
136
|
+
const home = mkdtempSync(join(tmpdir(), 'npmrc-home-'));
|
|
137
|
+
try {
|
|
138
|
+
writeFileSync(join(home, '.npmrc'), '//npm.pkg.github.com/:_authToken=ghp_other\n');
|
|
139
|
+
const r = n.resolveNpmToken({ env: {}, homedir: home });
|
|
140
|
+
assert.deepEqual(r, { token: null, source: null });
|
|
141
|
+
} finally { rmSync(home, { recursive: true, force: true }); }
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('resolveNpmToken: no user npmrc at all → null, never a throw', () => {
|
|
145
|
+
const home = mkdtempSync(join(tmpdir(), 'npmrc-home-'));
|
|
146
|
+
try {
|
|
147
|
+
const r = n.resolveNpmToken({ env: {}, homedir: home });
|
|
148
|
+
assert.deepEqual(r, { token: null, source: null });
|
|
149
|
+
} finally { rmSync(home, { recursive: true, force: true }); }
|
|
150
|
+
});
|
|
151
|
+
|
|
103
152
|
console.log('');
|
|
104
153
|
console.log(`npmrc tests: ${passed} passed, ${failed} failed`);
|
|
105
154
|
process.exit(failed ? 1 : 0);
|