@bongos/core 1.19.737 → 1.19.738

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 CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.737",
6
- "core_contract": "1.19.737",
7
- "source_commit": "1ceb8f6a2ad0321079098910ad8a721dffa67454",
5
+ "core_version": "1.19.738",
6
+ "core_contract": "1.19.738",
7
+ "source_commit": "37a35be93e9879af0144594f30a8a894a84502f5",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-14T01:10:42.991Z",
9
+ "built_at": "2026-09-14T01:52:45.616Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 479,
13
13
  "agent_docs_stubbed": 24,
14
- "functional_verbatim": 2109,
14
+ "functional_verbatim": 2107,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2612,
20
- "tree_sha256": "c34cdc2f87b36f12f71f276e47ce740238e94841fab60b4245613189e0c527d2",
19
+ "file_count": 2610,
20
+ "tree_sha256": "476bbdb5c1700b8ecf374a05e95a28bc6c61df721e5eb815c65439e08126feab",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2492,7 +2492,7 @@
2492
2492
  {
2493
2493
  "path": "docs/module-api-changelog.md",
2494
2494
  "mode": "0000644",
2495
- "sha256": "d1b64255f150e8c8a1b88cffa7326633b7e5bc22e22744f7b1d9ee47410f9e13"
2495
+ "sha256": "63fe92dfdaaacde0ba4dfb7882c97c6b875cd90d9b5f3dac0dacc404dbb1531f"
2496
2496
  },
2497
2497
  {
2498
2498
  "path": "docs/modules-contract.md",
@@ -2752,7 +2752,7 @@
2752
2752
  {
2753
2753
  "path": "docs/recipes/upgrading-the-core.md",
2754
2754
  "mode": "0000644",
2755
- "sha256": "36e3c53a8bbe0b987e19333f12cac4dc0951822686759623e3273843e313188e"
2755
+ "sha256": "3691b017562bde760fbe3ff29a5efb80d886dd770f1187dc4673459871cec513"
2756
2756
  },
2757
2757
  {
2758
2758
  "path": "docs/recipes/windows-builders.md",
@@ -7537,12 +7537,12 @@
7537
7537
  {
7538
7538
  "path": "package-lock.json",
7539
7539
  "mode": "0000644",
7540
- "sha256": "fc4587a7fbaaa14aad1e0296015adb626e89da62f4a90a95647f1bbb55a61f1e"
7540
+ "sha256": "35ace7cf502387465f75cea83b1159bb82349d019555503f51769a264fd19c3e"
7541
7541
  },
7542
7542
  {
7543
7543
  "path": "package.json",
7544
7544
  "mode": "0000644",
7545
- "sha256": "d7595c39c515821f76d0b5045c3a9cbb400f2c36a663046d19dfc7cab098cf69"
7545
+ "sha256": "4fbaebd587863651e105d72ed373cef166d16eebd5f86a5d51395d98c33c5f60"
7546
7546
  },
7547
7547
  {
7548
7548
  "path": "public-docs/index.html",
@@ -8122,7 +8122,7 @@
8122
8122
  {
8123
8123
  "path": "scripts/gds/go-live.js",
8124
8124
  "mode": "0000644",
8125
- "sha256": "95c5109be4ab748c5ee59f22489ba5e3de45bafccf1c543d8cf174fc89d75dab"
8125
+ "sha256": "f246947d973eec84ea174736bc87bf52b4716c939fc86bff4037b330f6790f9a"
8126
8126
  },
8127
8127
  {
8128
8128
  "path": "scripts/gds/goal-names.js",
@@ -8157,7 +8157,7 @@
8157
8157
  {
8158
8158
  "path": "scripts/gds/init.js",
8159
8159
  "mode": "0000644",
8160
- "sha256": "e81ab71bcd7d2f06b5aaf462dd099776c966f9840a109846a9753f12482b5281"
8160
+ "sha256": "3879084ce7246d6fdcf3a053006e86e7d533016b6dbb9b5aa160d65a3e9336f5"
8161
8161
  },
8162
8162
  {
8163
8163
  "path": "scripts/gds/install-git-hooks.js",
@@ -8299,11 +8299,6 @@
8299
8299
  "mode": "0000644",
8300
8300
  "sha256": "d34cbccec1fa3053f960c1ac0950ce7e6b4b05f9d32a23304c4c3abc41c22ef7"
8301
8301
  },
8302
- {
8303
- "path": "scripts/gds/npmrc.js",
8304
- "mode": "0000644",
8305
- "sha256": "1beffeed111e8528cfa0a629d5ef81c766023de5c6820a81659a390070868228"
8306
- },
8307
8302
  {
8308
8303
  "path": "scripts/gds/oauth-secret.js",
8309
8304
  "mode": "0000644",
@@ -8362,7 +8357,7 @@
8362
8357
  {
8363
8358
  "path": "scripts/gds/provision-config.js",
8364
8359
  "mode": "0000644",
8365
- "sha256": "359674dd4ba6c7c4dd446882ec88f7d9feffd474300e17736d3129c2526082c1"
8360
+ "sha256": "48d13ecda786629148713e0bd89f0d020ee47de107fa5a2327d44989783907ac"
8366
8361
  },
8367
8362
  {
8368
8363
  "path": "scripts/gds/provision-net.js",
@@ -8377,7 +8372,7 @@
8377
8372
  {
8378
8373
  "path": "scripts/gds/provision-repo.js",
8379
8374
  "mode": "0000644",
8380
- "sha256": "77ca585d99f4058427f1abbe629d66ce5ec9f87b6c974aebc114d1c0e734d8e2"
8375
+ "sha256": "c5822f3b29f90998c831639df2b0925033f17758cc329609de507f5e6e1ad7c8"
8381
8376
  },
8382
8377
  {
8383
8378
  "path": "scripts/gds/provision-units.js",
@@ -8387,7 +8382,7 @@
8387
8382
  {
8388
8383
  "path": "scripts/gds/provision.js",
8389
8384
  "mode": "0000644",
8390
- "sha256": "007c5cfa3b42567df0016038fc7ec5c7dbdab60ec6ec8ecd9d06386933e5641d"
8385
+ "sha256": "614aa63d02f3ae97f39e492cb0948a92b40364e25f4780d87546b64c01e5c69c"
8391
8386
  },
8392
8387
  {
8393
8388
  "path": "scripts/gds/publish-credential-check.js",
@@ -8857,7 +8852,7 @@
8857
8852
  {
8858
8853
  "path": "scripts/gds/start.js",
8859
8854
  "mode": "0000644",
8860
- "sha256": "1bfa9d27bc91836f636f57466876d40c56e13bdb1038451e8298c8a32cb674d6"
8855
+ "sha256": "7e4b7cd97e9a414cf55e1118c902b4ab17239d3810fe26e5b90aa18d6a948432"
8861
8856
  },
8862
8857
  {
8863
8858
  "path": "scripts/gds/status.js",
@@ -8902,12 +8897,12 @@
8902
8897
  {
8903
8898
  "path": "scripts/gds/update-channel.js",
8904
8899
  "mode": "0000644",
8905
- "sha256": "db54fba6065c8b662f574aa9443e686df4e5a152865342999b1f20d50ece01ed"
8900
+ "sha256": "1aeeb6171244e1d42ee7189a3aa2ae5d83fd175bfe1ce89300d6ca7511819f0b"
8906
8901
  },
8907
8902
  {
8908
8903
  "path": "scripts/gds/upgrade.js",
8909
8904
  "mode": "0000644",
8910
- "sha256": "64b2571770a5be9dc046f6c64adf45a09642c6961a6036c9405e1c933be6c3b7"
8905
+ "sha256": "7a82fb0cf919e14700ceba718e28da1324df2eab669a847348271c85b2f7bd66"
8911
8906
  },
8912
8907
  {
8913
8908
  "path": "scripts/gds/validate-design.js",
@@ -9347,7 +9342,7 @@
9347
9342
  {
9348
9343
  "path": "src/module-api.js",
9349
9344
  "mode": "0000644",
9350
- "sha256": "9ad95d6241cc8f3dfda5d02445246c1f7fd62b75b5014a15a9254c15a620aef7"
9345
+ "sha256": "4c5bdcc7f30c8ae47bdb1ecb596b692bd6405a227fc895fa5bfcd33cd8f92340"
9351
9346
  },
9352
9347
  {
9353
9348
  "path": "src/module-loader/catalog.js",
@@ -10567,7 +10562,7 @@
10567
10562
  {
10568
10563
  "path": "tests/go_live.mjs",
10569
10564
  "mode": "0000644",
10570
- "sha256": "2fc62d14a1c7e4fbd63fe4a18a4d1be85dfc481bc12a913cdd0d3812984bd79f"
10565
+ "sha256": "fb937d66f8efa9fdcfb58715785a0c9d94e52034d04175aec78310c70bd53a7f"
10571
10566
  },
10572
10567
  {
10573
10568
  "path": "tests/goal_abuse_matrix.mjs",
@@ -11287,7 +11282,7 @@
11287
11282
  {
11288
11283
  "path": "tests/init.mjs",
11289
11284
  "mode": "0000644",
11290
- "sha256": "3026220335d63e157a1732d03ee6e66f0509bf965cb8676a651cbb3e1780be34"
11285
+ "sha256": "945aaac204e9e60772d3e7b361c97c099d236e6be6b4b794cce36ecb391d8a1a"
11291
11286
  },
11292
11287
  {
11293
11288
  "path": "tests/instance_config.mjs",
@@ -11679,11 +11674,6 @@
11679
11674
  "mode": "0000644",
11680
11675
  "sha256": "c57c0211709ab041ee895bbb4ade9f4b146b43147570bf282e6d6a808560db76"
11681
11676
  },
11682
- {
11683
- "path": "tests/npmrc.mjs",
11684
- "mode": "0000644",
11685
- "sha256": "1aaa68ee1a4c4a58563cef86aac44cfa0e046ad7376d3a2108bf6dd455580942"
11686
- },
11687
11677
  {
11688
11678
  "path": "tests/oauth_secret.mjs",
11689
11679
  "mode": "0000644",
@@ -11982,7 +11972,7 @@
11982
11972
  {
11983
11973
  "path": "tests/provision.mjs",
11984
11974
  "mode": "0000644",
11985
- "sha256": "0e177a4817094985e4aeec0c74883502762304d2174c1e13e77a87404b739088"
11975
+ "sha256": "25fbd2adccf79e921c642f8f57b082f5f19fa01cd981cd5b813dc1070511aadb"
11986
11976
  },
11987
11977
  {
11988
11978
  "path": "tests/provision_restart.mjs",
@@ -12702,7 +12692,7 @@
12702
12692
  {
12703
12693
  "path": "tests/start_env_guard.mjs",
12704
12694
  "mode": "0000644",
12705
- "sha256": "9b0a7d7aeb761cb6fb0c5e1ae650e0848af12e92887ebd630814b35b434c1f41"
12695
+ "sha256": "dd7171dbc59e024de99a063763219237814cf4c943fb3d873cc324f3bfde7f25"
12706
12696
  },
12707
12697
  {
12708
12698
  "path": "tests/start_rebase_warning.mjs",
@@ -12922,7 +12912,7 @@
12922
12912
  {
12923
12913
  "path": "tests/upgrade.mjs",
12924
12914
  "mode": "0000644",
12925
- "sha256": "010c9ac8b05e25c10b9d43c7efd02454facf0d5663df11df555ea913697e23e8"
12915
+ "sha256": "def73ab0f532c04df167d77917981eaf7e0dc7e1d2a7e7c85f3f962d7d65ce2a"
12926
12916
  },
12927
12917
  {
12928
12918
  "path": "tests/upgrade_persist_pin.mjs",
@@ -1948,5 +1948,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1948
1948
  landed since 1.19.735 with no explicit bump. run 34771773838. (task 1002620)
1949
1949
  1.19.737 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1950
1950
  landed since 1.19.736 with no explicit bump. run 34795026282. (task 1002620)
1951
+ 1.19.738 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1952
+ landed since 1.19.737 with no explicit bump. run 34797297122. (task 1002620)
1951
1953
  ---------------------------------------------------------------------------
1952
1954
  ```
@@ -108,7 +108,7 @@ node .claude/scheduled-tasks/core-update-subscription/subscribe.js #
108
108
  node .claude/scheduled-tasks/core-update-subscription/subscribe.js --apply # upgrade
109
109
  ```
110
110
 
111
- Each subscribed instance: read its installed core → ask the registry (`npm view`, needs `NPM_TOKEN`) → resolve the newest version its channel permits → if newer, `bongos upgrade --to <v> --registry` with **auto-rollback ON**. It prints one line per instance plus a tally; a non-zero exit means an upgrade failed (already auto-reverted).
111
+ Each subscribed instance: read its installed core → ask the registry (`npm view`, no credential needed — the package is public) → resolve the newest version its channel permits → if newer, `bongos upgrade --to <v> --registry` with **auto-rollback ON**. It prints one line per instance plus a tally; a non-zero exit means an upgrade failed (already auto-reverted).
112
112
 
113
113
  **Triple-gated, so nothing moves by accident:** the `core-update-subscription` routine is `requiresAutonomy: true` + **default OFF** in [`config/scheduled-routines.json`](../../config/scheduled-routines.json), the roster ships empty, and every upgrade is health-gated. Arm it by enabling the routine's timer on the control plane once autonomy is on.
114
114
 
@@ -116,7 +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).
119
+ - **`--registry` needs no credential at all** (task 1003878). `@bongos/core` is **public** on npm (task 1003872), so `bongos upgrade --registry` resolves it from the default registry like any other dependency — no token, no login, and no instance `.npmrc`. An install failure on this path is therefore a *real* failure (network, registry outage, or a version that was never published), never a missing credential.
120
+ *History, so the symptom is recognisable on an old instance:* the core used to publish private, and the instance `.npmrc` read `_authToken=<redacted> 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, and the error blamed a credential the box actually held. Task 1002754 worked around it by resolving the token in-process; task 1003878 deleted the whole mechanism along with the `.npmrc` that caused it. An instance still carrying a committed `.npmrc` from a pre-1003878 scaffold can safely delete it.
120
121
  - **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`.
121
122
  - **`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.
122
123
  - **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`.)
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.737",
3
+ "version": "1.19.738",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.737",
9
+ "version": "1.19.738",
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.737",
3
+ "version": "1.19.738",
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",
@@ -84,7 +84,7 @@ function isSafeToken(v) {
84
84
  // unit name that the script would then try to `systemctl stop`. Absent, the stop and
85
85
  // the restart are simply skipped: there is no `reset --hard` to race.
86
86
  const REQUIRED_FIELDS = ['instanceDir', 'service', 'healthUrl', 'versionUrl'];
87
- const OPTIONAL_FIELDS = ['ssh', 'local', 'deployTimer', 'deployService', 'npmEnvFile', 'runAs', 'backupDb', 'backupDir', 'env', 'registryPackage', 'pinMode'];
87
+ const OPTIONAL_FIELDS = ['ssh', 'local', 'deployTimer', 'deployService', 'runAs', 'backupDb', 'backupDir', 'env', 'registryPackage', 'pinMode'];
88
88
 
89
89
  // How the bump is made durable — the other half of co-tenant mode (task 1003521).
90
90
  //
@@ -153,7 +153,6 @@ const CONFIG_TEMPLATE = `{
153
153
  "versionUrl": "http://127.0.0.1:3002/version", // box-side truth read back after
154
154
  "deployTimer": "my-deploy.timer", // optional: pull-deploy timer (its reset --hard is the hazard)
155
155
  "deployService": "my-deploy.service", // optional: forced tick proves the pin stuck
156
- "npmEnvFile": "/etc/my/npm.env", // optional: root-only NPM_TOKEN for a PRIVATE package
157
156
  "runAs": "me", // optional: unprivileged owner of the instance
158
157
  "backupDb": "mydb", // optional: pg_dump before the bump
159
158
  "backupDir": "~/backups",
@@ -247,40 +246,19 @@ function remote(target, script, { run = spawnSync, timeoutMs = 900000 } = {}) {
247
246
  // `sudo -u <user> -H env K=V … <cmd>` — the shape every runbook entry uses. Returns
248
247
  // a string safe to embed because every component was allowlist-validated.
249
248
  //
250
- // A privileged child needs npm auth, and there is NO WAY to hand it a secret through
251
- // sudo without logging it (task 1003218, corrected).
252
- //
253
- // `env NPM_TOKEN="$NPM_TOKEN" …` → sudo logs it in COMMAND=
254
- // `sudo --preserve-env=NPM_TOKEN …` → sudo logs it in ENV=
255
- //
256
- // The first was the original bug; the second was my "fix", which only MOVED the value
257
- // to a different field of the same log line. Both put a live credential in a
258
- // persistent journal. What matters is that sudo logs its whole invocation, so the
259
- // secret must never be part of it.
260
- //
261
- // So the child DERIVES the token itself, from a file it can already read. Only the
262
- // `sed` appears in argv; the value never does. This is the shape docs/recipes/
263
- // core-release-pipeline.md already documents as the safe one. The guard matters as
264
- // much as the derivation: an EMPTY token silently sends npm to the public registry,
265
- // which 404s the private package, which auto-rolls-back "cleanly" having taught you
266
- // nothing — so an empty read aborts loudly instead of half-running.
267
- //
268
- // (The nightly lane never takes this path: it runs under systemd as the owner with an
269
- // EnvironmentFile, no sudo, and leaks nothing. This is the manual/agent path only.)
270
- const NPM_AUTH_PREAMBLE = [
271
- 'export NPM_TOKEN="${NPM_TOKEN:-$(sed -n \'s/.*_authToken=//p\' "$HOME/.npmrc" 2>/dev/null | head -1)}"',
272
- '[ -n "$NPM_TOKEN" ] || { echo "go-live: EMPTY NPM_TOKEN — npm would 404 the private core and roll back; aborting" >&2; exit 12; }',
273
- ].join('; ');
274
-
275
249
  // `sudo -u <user> -H env K=V … <cmd>` — the shape every runbook entry uses. Returns
276
250
  // a string safe to embed because every component was allowlist-validated.
277
251
  //
278
- // `needsNpm` wraps the command so it derives its own npm auth (see above) instead of
279
- // having a secret handed across the privilege boundary. Only NON-secret configuration
280
- // (PGDATABASE, HOME) travels in the plain `env K=V` list — seeing those in a log is
281
- // useful rather than harmful.
282
- function asOwner(target, cmd, { needsNpm = false } = {}) {
283
- const inner = needsNpm ? `sh -c ${shq(`${NPM_AUTH_PREAMBLE}; ${cmd}`)}` : cmd;
252
+ // Only NON-secret configuration (PGDATABASE, HOME) travels in the plain `env K=V` list —
253
+ // seeing those in a log is useful rather than harmful.
254
+ //
255
+ // This used to take a `needsNpm` option wrapping the command in a preamble that DERIVED an
256
+ // npm credential on the far side, because sudo logs its whole invocation and there was no way
257
+ // to hand a privileged child a secret without journalling it (task 1003218). @bongos/core is
258
+ // public (task 1003872), so the deploy path needs no npm credential at all and the whole
259
+ // hazard is gone rather than mitigated (task 1003878).
260
+ function asOwner(target, cmd) {
261
+ const inner = cmd;
284
262
  const envPairs = Object.entries(target.env || {}).map(([k, v]) => `${k}=${v}`);
285
263
  if (!target.runAs) {
286
264
  const joined = envPairs.join(' ');
@@ -622,23 +600,21 @@ function renderReport(r) {
622
600
  // Phases
623
601
  // ---------------------------------------------------------------------------
624
602
 
625
- // Registry proof + optional content proof, in ONE round-trip. The registry is
626
- // consulted ON THE BOX because a PRIVATE package 404s without the root-only
627
- // NPM_TOKEN, and because the box's answer is the one that matters.
603
+ // Registry proof + optional content proof, in ONE round-trip. The registry is still
604
+ // consulted ON THE BOX — not because auth lives there (the package is public now), but
605
+ // because the box's answer is the one that matters: its network, its resolver, its cache.
628
606
  function probeRemote(target, targetVersion, marker, opts = {}) {
629
607
  const pkg = target.registryPackage || '@bongos/core';
630
- const src = target.npmEnvFile ? `set -a; . ${target.npmEnvFile}; set +a` : 'true';
631
608
  const script = `
632
609
  set -uo pipefail
633
- ${src}
634
610
  echo "##REGISTRY##"
635
- ${asOwner(target, `npm view ${pkg}@${targetVersion} version`, { needsNpm: true })} 2>&1 | tail -1
611
+ ${asOwner(target, `npm view ${pkg}@${targetVersion} version`)} 2>&1 | tail -1
636
612
  echo "##TREE##"
637
613
  ( cd ${target.instanceDir} && ${target.runAs ? `sudo -u ${target.runAs} -H ` : ''}git status --porcelain | head -20 )
638
614
  echo "##INSTALLED##"
639
615
  ${asOwner(target, `node -e "console.log(require('${target.instanceDir}/node_modules/${pkg}/package.json').version)"`)} 2>&1 | tail -1
640
616
  ${marker ? `echo "##MARKER##"
641
- ${asOwner(target, `sh -c ${shq(markerProbe(pkg, targetVersion, marker))}`, { needsNpm: true })}` : ''}
617
+ ${asOwner(target, `sh -c ${shq(markerProbe(pkg, targetVersion, marker))}`)}` : ''}
642
618
  echo "##END##"
643
619
  `;
644
620
  const res = remote(target, script, opts);
@@ -709,7 +685,6 @@ function applyGoLive(target, targetName, targetVersion, from, opts = {}) {
709
685
  const pkg = target.registryPackage || '@bongos/core';
710
686
  const pinMode = pinModeOf(target);
711
687
  const steps = [];
712
- const src = target.npmEnvFile ? `set -a; . ${target.npmEnvFile}; set +a` : 'true';
713
688
 
714
689
  // Phase A — stop the pull-deploy timer, then back up. The timer's `reset --hard`
715
690
  // is the hazard: it would revert an uncommitted pin mid-flight. A co-tenant has no
@@ -738,8 +713,7 @@ echo "##END##"
738
713
  const upgrade = upgradeCommand(target, targetVersion);
739
714
  const phaseB = remote(target, `
740
715
  set -uo pipefail
741
- ${src}
742
- ${asOwner(target, `bash -c "cd ${target.instanceDir} && ${upgrade}"`, { needsNpm: true })}
716
+ ${asOwner(target, `bash -c "cd ${target.instanceDir} && ${upgrade}"`)}
743
717
  `, opts);
744
718
  const rolledBack = /rolled back/i.test(phaseB.stdout + phaseB.stderr);
745
719
  steps.push({
@@ -995,7 +969,7 @@ if (require.main === module) {
995
969
  module.exports = {
996
970
  loadConfig, selectTarget, validateTarget, planGoLive, artistGateEntries, readArtistGate, ARTIST_GATE_PATH, ARTIST_GATE_LEVELS,
997
971
  parseMarker, parseMarkerHits,
998
- renderPlan, renderReport, failureExcerpt, asOwner, isSafeToken, NPM_AUTH_PREAMBLE, refreshMain, isLocalTarget, gitRefExists, SUDO_PREAMBLE,
972
+ renderPlan, renderReport, failureExcerpt, asOwner, isSafeToken, refreshMain, isLocalTarget, gitRefExists, SUDO_PREAMBLE,
999
973
  applyGoLive, upgradeCommand, pinStep, pinModeOf,
1000
974
  CONFIG_TEMPLATE, HALTS, SEMVER_RE, PIN_MODES, DEFAULT_PIN_MODE,
1001
975
  };
@@ -59,13 +59,13 @@
59
59
  // bongos init --vendor-core <tgz> greenfield only: vendor the @bongos/core .tgz + pin
60
60
  // package.json to file:vendor/… + generate package-lock.json,
61
61
  // so the scaffold is `npm ci`-installable (ADR 0108 gap 1/4).
62
- // Without it the scaffold pins the caret range (which now
63
- // resolves from the PRIVATE registry — see --registry-core).
64
- // bongos init --registry-core greenfield only: keep the caret pin + write an env-fed
65
- // .npmrc (`_authToken=${NPM_TOKEN}`) so `npm ci` pulls the
66
- // PRIVATE @bongos/core from the npm registry (ADR 0134).
67
- // With NPM_TOKEN set it also generates package-lock.json;
68
- // without a token it 404s (the expected tokenless outcome).
62
+ // Without it the scaffold pins the caret range, which
63
+ // resolves from the public npm registry (--registry-core).
64
+ // bongos init --registry-core greenfield only: keep the caret pin and generate
65
+ // package-lock.json from the registry. @bongos/core is
66
+ // PUBLIC (task 1003872), so this needs no token, no login
67
+ // and no .npmrc — it resolves from the default registry
68
+ // like any other dependency.
69
69
  // Mutually exclusive with --vendor-core (registry vs tarball).
70
70
 
71
71
  const fs = require('node:fs');
@@ -78,7 +78,6 @@ const { materializeClaude } = require('./claude-materialize'); // ADR 0108 §3 (
78
78
  const { detectStack, formatStackSummary } = require('./adopt-detect'); // ADR 0121 §Decision 4 (task 2043): inspect an existing repo before layering
79
79
  const { resolveBacklogTasks, DEFAULT_MAX: BACKLOG_MAX } = require('./adopt-import'); // ADR 0121 §Decision 3 (task 2044): import an existing backlog
80
80
  const { preflightAdopt, formatPreflight } = require('./adopt-preflight'); // ADR 0121 §Decision 5 (task 2045): the accept-gated conflict pre-flight
81
- const { writeNpmrc, hasNpmToken } = require('./npmrc'); // ADR 0134 (task 2090): the registry-install path's env-fed .npmrc
82
81
 
83
82
  const KNOWN_MODULES = ['game', 'art-pipeline', 'discord', 'dev-box'];
84
83
 
@@ -946,7 +945,7 @@ async function main(argv) {
946
945
  const adopt = hasFlag('--adopt', args); // brownfield: LAYER onto an existing repo (ADR 0121)
947
946
  const accept = hasFlag('--accept', args) || hasFlag('--yes', args) || hasFlag('-y', args); // adopt pre-flight accept-gate (§Decision 5)
948
947
  const vendorCore = arg('--vendor-core', args); // greenfield: vendor + file:-pin a core .tgz, then generate the lockfile (ADR 0108 gap 1/4, task 2053)
949
- const registryCore = hasFlag('--registry-core', args); // greenfield: caret pin + env-fed .npmrc → `npm ci` pulls the PRIVATE core from the registry (ADR 0134, task 2090)
948
+ const registryCore = hasFlag('--registry-core', args); // greenfield: keep the caret pin and generate the lockfile from the public registry (task 1003878)
950
949
 
951
950
  if (adopt && force) {
952
951
  console.error('init: --force is a greenfield-only overwrite; --adopt never overwrites, so --force is ignored.');
@@ -1030,30 +1029,23 @@ async function main(argv) {
1030
1029
  }
1031
1030
  }
1032
1031
 
1033
- // Registry-install path (ADR 0134, task 2090): keep the caret pin writeConfigs wrote
1034
- // and add an env-fed .npmrc so `npm ci` pulls the PRIVATE @bongos/core from the
1035
- // registry. With NPM_TOKEN present we also generate package-lock.json (a reproducible
1036
- // `npm ci` later); without a token we still write the caret pin + .npmrc so the
1037
- // scaffold is registry-shaped, and say plainly that the private package 404s until a
1038
- // token is provided — the expected tokenless outcome the ADR calls to verify.
1032
+ // Registry-install path: keep the caret pin writeConfigs wrote and generate
1033
+ // package-lock.json from the registry, so a later `npm ci` is reproducible.
1034
+ //
1035
+ // This used to write an env-fed npm auth file and skip the lockfile when no credential was
1036
+ // present, because @bongos/core published PRIVATE. It is public now (task 1003872), so it
1037
+ // resolves from the default registry like any other dependency: no auth file, no credential,
1038
+ // and no tokenless-404 branch to explain (task 1003878).
1039
1039
  if (registryCore) {
1040
1040
  if (dryRun) {
1041
- console.log(`\n[dry-run] would write .npmrc (env-fed @bongos auth) and ${hasNpmToken()
1042
- ? 'run `npm install` to generate package-lock.json from the registry'
1043
- : 'SKIP lockfile generation (no NPM_TOKEN — the private core 404s until one is provided)'}`);
1041
+ console.log('\n[dry-run] would run `npm install` to generate package-lock.json from the registry');
1044
1042
  } else {
1045
- console.log('\nWiring the registry-install path (ADR 0134):');
1046
- writeNpmrc(dir, { log: console.log });
1047
- if (hasNpmToken()) {
1048
- const inst = upgrade.npmInstall(dir);
1049
- if (!inst.ok) {
1050
- console.error(` ! npm install failed (${inst.reason || 'exit ' + inst.code}) — confirm NPM_TOKEN reaches the private @bongos org; package-lock.json not generated.`);
1051
- } else {
1052
- console.log(' ✓ npm install → package-lock.json generated (pulled @bongos/core from the registry)');
1053
- }
1043
+ console.log('\nWiring the registry-install path:');
1044
+ const inst = upgrade.npmInstall(dir);
1045
+ if (!inst.ok) {
1046
+ console.error(` ! npm install failed (${inst.reason || 'exit ' + inst.code}) — package-lock.json not generated. @bongos/core is public, so this is a real failure (network, registry outage, or an unpublished version), not a missing credential.`);
1054
1047
  } else {
1055
- console.log(' • no NPM_TOKEN in the environment — wrote the caret pin + .npmrc but skipped lockfile generation.');
1056
- console.log(' Provide a read-only automation token (paid `bongos` org) as NPM_TOKEN before `npm ci`; without it the private package 404s cleanly.');
1048
+ console.log(' ✓ npm install → package-lock.json generated (pulled @bongos/core from the registry)');
1057
1049
  }
1058
1050
  }
1059
1051
  }
@@ -19,12 +19,6 @@ const REPO_ROOT = path.resolve(__dirname, '..', '..');
19
19
  // plain require (its require.main guard means importing it runs no CLI).
20
20
  const oauthSecret = require('./oauth-secret');
21
21
 
22
- // The registry-install gates + env-fed .npmrc (ADR 0134, task 2090). Lets the
23
- // standalone scaffold OPT IN to pulling the PRIVATE @bongos/core from the npm
24
- // registry (PROVISION_CORE_FROM_REGISTRY=1) instead of vendoring a tarball, with a
25
- // fail-soft fallback to the vendored path when no NPM_TOKEN is present.
26
- const { registryCoreRequested, hasNpmToken } = require('./npmrc');
27
-
28
22
  // The HTTP User-Agent the manifest conversion stamps its GitHub call with (GitHub
29
23
  // requires a UA). Overridable so a fork can identify itself; not secret.
30
24
  const MANIFEST_UA = process.env.PROVISION_GITHUB_UA || 'cloudbongos-provisioner';
@@ -195,10 +189,8 @@ module.exports = {
195
189
  REPO_ROOT,
196
190
  coreVersionSafe,
197
191
  hasFlag,
198
- hasNpmToken,
199
192
  loadDeps,
200
193
  numEnv,
201
194
  oauthSecret,
202
195
  provisionerBotEmail,
203
- registryCoreRequested,
204
196
  };
@@ -14,7 +14,7 @@ const crypto = require('node:crypto');
14
14
  const fs = require('node:fs');
15
15
  const os = require('node:os');
16
16
  const path = require('node:path');
17
- const { CONFIG, REPO_ROOT, coreVersionSafe, hasNpmToken, provisionerBotEmail, registryCoreRequested } = require('./provision-config.js');
17
+ const { CONFIG, REPO_ROOT, coreVersionSafe, provisionerBotEmail } = require('./provision-config.js');
18
18
  // The repo-identity + owner-credential leaf (task 1003081). Carved OUT of this file so
19
19
  // upgrade.js can reach these three without pulling provision*.js into the merge
20
20
  // resolver's executed closure; re-exported below so every existing caller is unchanged.
@@ -602,18 +602,20 @@ async function scaffoldStandaloneRepo(inst, deps) {
602
602
  exec(`git -C ${root} checkout -B main`);
603
603
  }
604
604
 
605
- // Core-install path (ADR 0134, task 2090). DEFAULT = vendor a tarball built from THIS
606
- // control-plane checkout (offline, proven — task 2053). OPT-IN = pull the PRIVATE
607
- // @bongos/core from the npm registry (PROVISION_CORE_FROM_REGISTRY=1). The registry
608
- // path needs NPM_TOKEN at `npm ci` time; if the flag is set but no token is present we
609
- // FAIL SOFT to the vendored path so a tokenless standup still works (never a broken
610
- // instance). The env is injectable for tests; the runner falls back to process.env.
611
- const env = deps.env || process.env;
612
- const wantRegistry = registryCoreRequested(env);
613
- const useRegistry = wantRegistry && hasNpmToken(env);
614
- if (wantRegistry && !useRegistry) {
615
- log(' [scaffold] PROVISION_CORE_FROM_REGISTRY set but no NPM_TOKEN in env — falling back to the vendored-tarball core (ADR 0134 fail-soft).');
616
- }
605
+ // Core-install path: vendor a tarball built from THIS control-plane checkout
606
+ // (offline, proven — task 2053).
607
+ //
608
+ // This used to be a fork: an opt-in env var selected the registry, and a missing npm
609
+ // credential silently failed it soft back to vendored. Both halves were auth-era and both
610
+ // are gone (task 1003878) — @bongos/core is public, so nothing needs a credential, and an
611
+ // env var that silently changed which install path ran is the divergence behind task
612
+ // 1003520. tests/provision.mjs pins both retired names and proves they are inert.
613
+ //
614
+ // Vendored stays the default here for a reason that has nothing to do with auth and
615
+ // survives the package going public: THIS CHECKOUT IS ROUTINELY AHEAD OF THE REGISTRY.
616
+ // A control plane on an unreleased CORE_VERSION has no published version to install, so
617
+ // the registry path would 404 on exactly the commits an operator most wants to stand up.
618
+ // Whether the vendored path still earns its keep at all is gate 6's call (task 1003879).
617
619
 
618
620
  // materialize config/ + .claude/ + a pinned package.json + lockfile via `bongos init`.
619
621
  // --no-seed: the instance's own DB is seeded by migrate, not here (and on the adopt path
@@ -627,27 +629,21 @@ async function scaffoldStandaloneRepo(inst, deps) {
627
629
  // BLOCKING pre-flight finding still hard-stops — it cannot be waved through). init refuses
628
630
  // --adopt together with --force, so the two flag sets are mutually exclusive by design.
629
631
  //
630
- // Two mutually exclusive core-resolution shapes, orthogonal to the mode:
631
- // registry (--registry-core): caret pin + env-fed .npmrc; `npm ci` pulls from the
632
- // registry. init generates the lockfile (NPM_TOKEN is present — we gated on it).
633
- // vendored (--vendor-core, default): a tarball built from this checkout, file:-pinned.
632
+ // Core resolution is vendored (--vendor-core): a tarball built from this checkout,
633
+ // file:-pinned. init still accepts --registry-core for an operator who explicitly wants
634
+ // a published version, and that flag no longer needs any credential.
634
635
  const modeFlags = mode === 'adopt' ? '--adopt --accept' : '--force';
635
636
  const spec = instanceInitSpec(inst, { ownerLogin });
636
637
  const specPath = path.join(os.tmpdir(), `bongos-init-${inst.slug}.json`);
637
638
  writeFile(specPath, JSON.stringify(spec, null, 2));
638
- if (useRegistry) {
639
- log(` [scaffold] core install: registry — PRIVATE @bongos/core@${coreV} via env-fed NPM_TOKEN (ADR 0134)`);
640
- exec(`node scripts/gds/init.js --from ${specPath} --dir ${root} --no-seed ${modeFlags} --registry-core`);
641
- } else {
642
- // Vendored tarball — resolveVendorableCoreTarball's three-rung ladder (shared
643
- // with the re-provision pin refresh, task 1003050).
644
- const { tgz, packDest } = resolveVendorableCoreTarball({
645
- instanceRoot: deps.instanceRoot, exec, log, coreV, tag: '[scaffold] core install:',
646
- });
647
- exec(`node scripts/gds/init.js --from ${specPath} --dir ${root} --no-seed ${modeFlags} --vendor-core ${tgz}`);
648
- // init --vendor-core COPIED the tarball into the child's vendor/ — the temp pack dir is spent.
649
- if (packDest) exec(`rm -rf ${packDest}`);
650
- }
639
+ // Vendored tarball — resolveVendorableCoreTarball's three-rung ladder (shared
640
+ // with the re-provision pin refresh, task 1003050).
641
+ const { tgz, packDest } = resolveVendorableCoreTarball({
642
+ instanceRoot: deps.instanceRoot, exec, log, coreV, tag: '[scaffold] core install:',
643
+ });
644
+ exec(`node scripts/gds/init.js --from ${specPath} --dir ${root} --no-seed ${modeFlags} --vendor-core ${tgz}`);
645
+ // init --vendor-core COPIED the tarball into the child's vendor/ — the temp pack dir is spent.
646
+ if (packDest) exec(`rm -rf ${packDest}`);
651
647
 
652
648
  // 4. commit + push to the instance's own main (keep node_modules out).
653
649
  //
@@ -360,11 +360,9 @@ async function provisionInstance(inst, deps) {
360
360
  // repo + install the pinned @bongos/core, so the migrate + service below run
361
361
  // from the INSTANCE root, not this control-plane checkout. cwd = standaloneRoot.
362
362
  // boxExec = the control plane (where scaffoldStandaloneRepo just created the dir).
363
- // REGISTRY CORE (ADR 0134): when the scaffold pinned the registry core, the committed
364
- // .npmrc reads `_authToken=${NPM_TOKEN}` — so `npm ci` resolves the PRIVATE package
365
- // only if NPM_TOKEN is in this runner's env (makeExec inherits process.env). The
366
- // scaffold already gated on the token, so it is present; the VENDORED default needs
367
- // no token (the tarball is committed). A tokenless registry `npm ci` 404s cleanly.
363
+ // CORE RESOLUTION: the vendored default needs no network at all (the tarball is
364
+ // committed). A registry pin resolves from the public registry — @bongos/core is public
365
+ // (task 1003872), so `npm ci` needs no credential either way (task 1003878).
368
366
  const root = standaloneRoot(inst);
369
367
  log(' [code] standalone — pull instance repo + npm ci (pinned @bongos/core)');
370
368
  boxExec(standalonePullCmd(), { cwd: root });
@@ -587,7 +587,7 @@ function instanceNotRunningCard({ origin, depsMissing } = {}) {
587
587
  out.push(`${surface.strong(`Don't run ${surface.code('bongos dev')} for this.`)} It stands up a separate local instance with its own empty database — it cannot show you this board, and it would hide the outage behind an empty one.`);
588
588
  if (depsMissing) {
589
589
  out.push('');
590
- out.push(`(The ${surface.code('bongos')} CLI isn't installed here. It's optional — the website above is the same board. ${surface.code('@bongos/core')} is a ${surface.strong('private')} package, so installing it needs an ${surface.code('NPM_TOKEN')}; a 404 without one is expected, not a missing version.)`);
590
+ out.push(`(The ${surface.code('bongos')} CLI isn't installed here. It's optional — the website above is the same board. ${surface.code('@bongos/core')} is ${surface.strong('public')} on npm, so ${surface.code('npm install @bongos/core')} needs no token or login; a failure here is a real error, not the expected tokenless 404 it used to be.)`);
591
591
  }
592
592
  return out.join('\n');
593
593
  }
@@ -122,9 +122,9 @@ function resolveChannelTarget({ installed, channel, available } = {}) {
122
122
 
123
123
  // ---- registry: what versions of the core are published? ---------------------
124
124
 
125
- // List published versions of @bongos/core from the registry via `npm view … versions --json`
126
- // (ADR 0134 — the private scoped package resolves through the instance's env-fed .npmrc, so run this
127
- // with cwd inside an instance that has one + NPM_TOKEN in env). Fail-SOFT: any error returns
125
+ // List published versions of @bongos/core from the registry via `npm view … versions --json`.
126
+ // The package is public (task 1003872), so this needs no credential and no instance-local
127
+ // `.npmrc` — any cwd resolves it. Fail-SOFT: any error returns
128
128
  // { ok:false } so the routine skips that instance rather than crashing the whole sweep. `run` +
129
129
  // `pkg` are injectable for tests.
130
130
  function listAvailableVersions({ cwd = process.cwd(), pkg = CORE_PKG, run = spawnSync, env = process.env } = {}) {
@@ -32,7 +32,6 @@ const path = require('node:path');
32
32
  const crypto = require('node:crypto');
33
33
  const { spawnSync } = require('node:child_process');
34
34
  const { arg, hasFlag } = require('./cli-lib');
35
- const { writeNpmrc, hasNpmToken, resolveNpmToken } = require('./npmrc'); // ADR 0134 (task 2090): the --registry channel's env-fed .npmrc; resolveNpmToken task 1002754
36
35
 
37
36
  const CORE_PKG = '@bongos/core';
38
37
  const ARTIFACT = 'bongos-core';
@@ -98,9 +97,9 @@ function writePin(instanceDir, vendorRel, fsImpl = fs) {
98
97
  return prev;
99
98
  }
100
99
 
101
- // Rewrite the consumer package.json to pin an EXACT registry version (ADR 0134, task
102
- // 2090): the registry sibling of writePin. `npm install` then resolves @bongos/core@version
103
- // from the registry (auth via the env-fed .npmrc). Returns the previous dep string.
100
+ // Rewrite the consumer package.json to pin an EXACT registry version: the registry sibling
101
+ // of writePin. `npm install` then resolves @bongos/core@version from the public registry —
102
+ // no credential, since the package is public (task 1003872). Returns the previous dep string.
104
103
  function writeRegistryPin(instanceDir, version, fsImpl = fs) {
105
104
  const pkgPath = path.join(instanceDir, 'package.json');
106
105
  const pkg = readJson(pkgPath, fsImpl);
@@ -478,13 +477,13 @@ function reportModulePreflight(mods, targetVersion, { log = console.log, err = c
478
477
  return lines;
479
478
  }
480
479
 
481
- function npmInstall(instanceDir, run = spawnSync, extraEnv = null) {
480
+ function npmInstall(instanceDir, run = spawnSync) {
482
481
  // `npm install` (NOT `npm ci`): the pin just changed, so the lockfile must be regenerated from the
483
482
  // new tarball; `npm ci` would refuse on the package.json↔lock divergence.
484
- // extraEnv (task 1002754): the registry path injects a resolved NPM_TOKEN here so the
485
- // instance `.npmrc`'s `${NPM_TOKEN}` expansion never shadows a valid user-level token.
483
+ // This used to take an extraEnv carrying a resolved npm credential, because the instance
484
+ // `.npmrc`'s env expansion could shadow a valid user-level one (task 1002754). The core is
485
+ // public and writes no `.npmrc`, so there is no credential to resolve (task 1003878).
486
486
  const opts = { cwd: instanceDir, stdio: 'inherit' };
487
- if (extraEnv) opts.env = { ...process.env, ...extraEnv };
488
487
  const r = run('npm', ['install', '--no-audit', '--no-fund'], opts);
489
488
  return !r || r.error ? { ok: false, reason: r && r.error ? r.error.message : 'spawn failed' } : { ok: r.status === 0, code: r.status };
490
489
  }
@@ -983,11 +982,8 @@ async function rollback({ instanceDir, snapshot, fromVersion, toVersion, opts },
983
982
  try { restore(instanceDir, snapshot.prevPin, fsImpl); result.restoreOk = true; log(` ✓ restored pin → ${snapshot.prevPin}`); }
984
983
  catch (e) { result.restoreOk = false; err(` ! could not restore pin (${e.message}) — package.json may need manual repair`); }
985
984
 
986
- // 2. reinstall the previous core. Same credential resolution as the forward path
987
- // (task 1002754): the previous pin may be a registry pin too, and a rollback
988
- // must never fail on the env-shadowing trap the forward install just avoided.
989
- const rbCred = resolveNpmToken();
990
- const inst = npmInstall(instanceDir, run, rbCred.source === 'user-npmrc' ? { NPM_TOKEN: rbCred.token } : null);
985
+ // 2. reinstall the previous core.
986
+ const inst = npmInstall(instanceDir, run);
991
987
  result.reinstallOk = !!inst.ok;
992
988
  if (inst.ok) log(' ✓ reinstalled previous core');
993
989
  else err(` ! reinstall of previous core failed (${inst.reason || 'exit ' + inst.code}) — the instance may need manual repair`);
@@ -1094,24 +1090,16 @@ async function runUpgrade(opts, deps = {}) {
1094
1090
  return { ok: false, error: 'post-restart health check is mandatory for a bump that restarts the service, but no --health-url was given.\n Pass --health-url <url> to confirm the new core serves after restart, or --no-health-check to explicitly skip the confirmation.' };
1095
1091
  }
1096
1092
 
1097
- // 2. bump the pin. Two mutually exclusive core sources (ADR 0134, task 2090):
1098
- // REGISTRY (--registry): pin @bongos/core@<version> + ensure the env-fed .npmrc, so
1099
- // the `npm install` below pulls the PRIVATE package from the registry (needs NPM_TOKEN).
1093
+ // 2. bump the pin. Two mutually exclusive core sources:
1094
+ // REGISTRY (--registry): pin @bongos/core@<version> and let `npm install` pull it from
1095
+ // the public registry. No credential — the package is public (task 1003872), which
1096
+ // also retired the whole resolve-a-token-and-inject-it dance (task 1002754/1003878).
1100
1097
  // VENDORED (default, --from <tgz>): copy the .tgz into vendor/ + rewrite the file: dep.
1101
- let installEnv = null;
1102
1098
  if (opts.registry) {
1103
1099
  if (opts.from) return { ok: false, error: '--registry and --from are mutually exclusive (registry pull vs vendored tarball) — pick one core source' };
1104
- // task 1002754 — resolve the credential HERE instead of demanding a hand-exported
1105
- // env var: env NPM_TOKEN wins, else the user-level ~/.npmrc's literal token is
1106
- // injected into the install's env so the instance `.npmrc`'s `${NPM_TOKEN}`
1107
- // expansion (which OUTRANKS the user-level key and expands EMPTY when unset)
1108
- // can never shadow a working credential again.
1109
- const cred = resolveNpmToken();
1110
- 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).');
1111
- 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)'); }
1112
1100
  const prevPin = dryRun ? null : writeRegistryPin(instanceDir, targetVersion, fsImpl);
1113
- if (!dryRun) { writeNpmrc(instanceDir, { fsImpl, overwrite: false, log }); snapshot = { prevPin, prevVersion: fromVersion }; }
1114
- log(` ${dryRun ? '[dry-run] would pin' : '✓ pinned'} ${CORE_PKG} → ${targetVersion} (registry)${dryRun ? ' + write an env-fed .npmrc' : ''}${prevPin ? ` (was ${prevPin})` : ''}`);
1101
+ if (!dryRun) { snapshot = { prevPin, prevVersion: fromVersion }; }
1102
+ log(` ${dryRun ? '[dry-run] would pin' : '✓ pinned'} ${CORE_PKG} → ${targetVersion} (registry)${prevPin ? ` (was ${prevPin})` : ''}`);
1115
1103
  } else {
1116
1104
  let vendorRel = opts.pinPath || (opts.from ? null : vendorRelFor(targetVersion));
1117
1105
  if (opts.from) {
@@ -1130,7 +1118,7 @@ async function runUpgrade(opts, deps = {}) {
1130
1118
  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 }; }
1131
1119
 
1132
1120
  // 3. npm install.
1133
- const inst = npmInstall(instanceDir, run, installEnv);
1121
+ const inst = npmInstall(instanceDir, run);
1134
1122
  if (!inst.ok) {
1135
1123
  const error = `npm install failed (${inst.reason || 'exit ' + inst.code})`;
1136
1124
  if (rollbackOnFailure && snapshot) {
@@ -1370,7 +1358,7 @@ async function main(argv = process.argv.slice(2)) {
1370
1358
  '',
1371
1359
  ' --to <version> target core version — presence switches from pre-check to a real bump',
1372
1360
  ' --from <tgz> source tarball to vendor into vendor/ (else it must already be there)',
1373
- ' --registry pull @bongos/core@<--to> from the npm registry (env-fed .npmrc + NPM_TOKEN) instead of vendoring a tarball (ADR 0134); mutually exclusive with --from',
1361
+ ' --registry pull @bongos/core@<--to> from the public npm registry (no token needed) instead of vendoring a tarball; mutually exclusive with --from',
1374
1362
  ' --instance <dir> consumer instance root (default: cwd)',
1375
1363
  ' --service <unit> systemd unit to restart after applying',
1376
1364
  ' --restart-cmd <cmd> explicit restart command (overrides --service)',
package/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.737'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.738'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
package/tests/go_live.mjs CHANGED
@@ -442,26 +442,27 @@ test("'could not check' is a distinct halt from 'absent'", () => {
442
442
  // `env K=V` list that omits it drops it too. This cost a real failed go-live.
443
443
  // ---------------------------------------------------------------------------
444
444
 
445
- test('a privileged npm command DERIVES its token — no secret in argv at all', () => {
446
- const cmd = G.asOwner({ runAs: 'lars', env: { PGDATABASE: 'cb' } }, 'npm install', { needsNpm: true });
447
- // THE REGRESSION, TWICE (task 1003218). `env NPM_TOKEN="$NPM_TOKEN"` put the value in
448
- // sudo's COMMAND= field. `--preserve-env=NPM_TOKEN` moved it to sudo's ENV= field —
449
- // a different field of the SAME log line, which is not a fix. sudo logs its whole
450
- // invocation, so the only safe answer is that the secret is never part of it.
451
- assert.ok(!/NPM_TOKEN=npm_/.test(cmd), 'no token VALUE may appear');
452
- assert.ok(!/--preserve-env/.test(cmd), '--preserve-env logs the value in ENV=');
453
- assert.ok(!/env [^;]*NPM_TOKEN="\$NPM_TOKEN"/.test(cmd), 'no by-reference assignment in argv either');
454
- // What SHOULD be there: the child deriving it, and refusing to run on an empty read.
455
- assert.match(cmd, /_authToken/);
456
- assert.match(cmd, /EMPTY NPM_TOKEN/);
445
+ test('a privileged npm command carries NO credential machinery at all', () => {
446
+ const cmd = G.asOwner({ runAs: 'lars', env: { PGDATABASE: 'cb' } }, 'npm install');
447
+ // HISTORY, so nobody reintroduces it (task 1003218, twice). `env NPM_TOKEN="$NPM_TOKEN"`
448
+ // put the value in sudo's COMMAND= field; `--preserve-env=NPM_TOKEN` moved it to sudo's
449
+ // ENV= field — a different field of the SAME log line, which was never a fix. The third
450
+ // shape had the child DERIVE the token from a file so only a `sed` appeared in argv.
451
+ //
452
+ // All three are now moot rather than mitigated: @bongos/core is public (task 1003872), so
453
+ // the deploy path needs no npm credential and the hazard cannot recur (task 1003878). This
454
+ // asserts the ABSENCE, which is a stronger guarantee than any of the three mitigations.
455
+ assert.doesNotMatch(cmd, /NPM_TOKEN/, 'no token by value, by reference, or by derivation');
456
+ assert.doesNotMatch(cmd, /--preserve-env/, '--preserve-env would log the value in ENV=');
457
+ assert.doesNotMatch(cmd, /_authToken/, 'nothing reads a credential out of an .npmrc');
457
458
  // Non-secret config still travels plainly.
458
459
  assert.match(cmd, /PGDATABASE=cb/);
459
460
  });
460
461
 
461
- test('an empty token ABORTS rather than half-running', () => {
462
- // An empty token sends npm to the PUBLIC registry, which 404s the private core, which
463
- // auto-rolls-back "cleanly" — a failure that teaches you nothing. Fail loudly instead.
464
- assert.match(G.NPM_AUTH_PREAMBLE, /exit 12/);
462
+ test('the retired auth preamble is really gone from the module surface', () => {
463
+ // It was exported and embedded into every privileged command. A re-export would mean the
464
+ // mitigation came back, which now means a credential came back with it.
465
+ assert.equal(G.NPM_AUTH_PREAMBLE, undefined, 'NPM_AUTH_PREAMBLE must not be exported');
465
466
  });
466
467
 
467
468
  test('a command that does not need npm is left alone', () => {
@@ -470,10 +471,10 @@ test('a command that does not need npm is left alone', () => {
470
471
  assert.ok(!cmd.includes('NPM_TOKEN'));
471
472
  });
472
473
 
473
- test('with no privilege drop the command still derives its own auth', () => {
474
- const cmd = G.asOwner({ env: { A: 'b' } }, 'npm x', { needsNpm: true });
474
+ test('with no privilege drop the command is still credential-free', () => {
475
+ const cmd = G.asOwner({ env: { A: 'b' } }, 'npm x');
475
476
  assert.ok(!cmd.startsWith('sudo'));
476
- assert.match(cmd, /_authToken/);
477
+ assert.doesNotMatch(cmd, /NPM_TOKEN|_authToken/);
477
478
  });
478
479
 
479
480
  test('the report surfaces the failing tool’s own words', () => {
package/tests/init.mjs CHANGED
@@ -768,10 +768,10 @@ async function runSeedTests() {
768
768
  });
769
769
  }
770
770
 
771
- // ---- greenfield registry-install path (ADR 0134, task 2090) ----------------
771
+ // ---- greenfield registry-install path ---------------------------------------
772
772
  // main() with --registry-core --dry-run only LOGS (no fs, no network), so we capture
773
- // console output and assert the plan. The real write+install path is proven by the
774
- // CLI scratch run in the session log; the pure gates live in tests/npmrc.mjs.
773
+ // console output and assert the plan. The package is public (task 1003872), so this path
774
+ // writes no `.npmrc` and needs no token; tests/npmrc.mjs went with the module (task 1003878).
775
775
  async function runRegistryCoreTests() {
776
776
  const at = async (name, fn) => {
777
777
  try { await fn(); passed++; console.log(` ok ${name}`); }
@@ -799,14 +799,19 @@ async function runRegistryCoreTests() {
799
799
  return { code, out: lines.join('\n') };
800
800
  };
801
801
 
802
- await at('main --registry-core --dry-run: plans the env-fed .npmrc; does NOT vendor a tarball', async () => {
802
+ await at('main --registry-core --dry-run: plans a plain lockfile generation, no .npmrc, no token', async () => {
803
803
  const { dir, p } = specFile();
804
804
  try {
805
805
  const { code, out } = await capture(['--from', p, '--dir', join(dir, 'out'), '--no-seed', '--registry-core', '--dry-run']);
806
806
  assert.equal(code, 0);
807
- assert.match(out, /would write \.npmrc \(env-fed @bongos auth\)/);
807
+ assert.match(out, /would run `npm install` to generate package-lock\.json/);
808
808
  assert.ok(!/vendor/i.test(out), 'no vendored-tarball step on the registry path');
809
- assert.equal(existsSync(join(dir, 'out', '.npmrc')), false, 'dry-run writes nothing');
809
+ // The load-bearing half (task 1003878): a public package resolves from the default
810
+ // registry, so planning an `.npmrc` or mentioning a token would mean the retired
811
+ // auth path came back.
812
+ assert.doesNotMatch(out, /\.npmrc/, 'must not plan an .npmrc');
813
+ assert.doesNotMatch(out, /NPM_TOKEN/, 'must not mention a token');
814
+ assert.equal(existsSync(join(dir, 'out', '.npmrc')), false, 'writes no .npmrc at all');
810
815
  } finally { rmSync(dir, { recursive: true, force: true }); }
811
816
  });
812
817
 
@@ -599,18 +599,41 @@ async function scaffoldWithEnv(env) {
599
599
  }
600
600
  }
601
601
 
602
- await ta('scaffoldStandaloneRepo: PROVISION_CORE_FROM_REGISTRY=1 + NPM_TOKEN → registry path (init --registry-core, no tarball build)', async () => {
602
+ // Provisioning used to FORK on two env vars: PROVISION_CORE_FROM_REGISTRY chose the registry
603
+ // path, and a missing NPM_TOKEN silently failed it soft back to the vendored tarball. Both are
604
+ // gone (task 1003878) — the package is public, so nothing needs a token, and an env var that
605
+ // silently changed which install path ran is the divergence behind task 1003520.
606
+ //
607
+ // These two assert the ABSENCE of that fork, which is why they set the retired vars explicitly:
608
+ // a test that merely stopped mentioning them would pass whether or not the fork came back.
609
+ await ta('scaffoldStandaloneRepo: the retired PROVISION_CORE_FROM_REGISTRY env no longer diverts to the registry', async () => {
603
610
  const joined = await scaffoldWithEnv({ PROVISION_CORE_FROM_REGISTRY: '1', NPM_TOKEN: 'npm_readonly' });
604
- assert.match(joined, /init\.js --from .* --dir .* --no-seed --force --registry-core/, 'materializes via --registry-core');
605
- assert.ok(!/package-core\.js/.test(joined), 'does NOT build a core tarball on the registry path');
606
- assert.ok(!/--vendor-core/.test(joined), 'does NOT vendor a tarball on the registry path');
607
- });
608
-
609
- await ta('scaffoldStandaloneRepo: registry requested but NO NPM_TOKEN → FAILS SOFT to the vendored tarball', async () => {
610
- const joined = await scaffoldWithEnv({ PROVISION_CORE_FROM_REGISTRY: '1' });
611
- assert.match(joined, /node scripts\/gds\/package-core\.js/, 'builds the core tarball (vendored fallback)');
611
+ assert.match(joined, /node scripts\/gds\/package-core\.js/, 'still vendors — the env var is inert');
612
612
  assert.match(joined, /init\.js --from .* --dir .* --no-seed --force --vendor-core /, 'file:-pins the vendored tarball');
613
- assert.ok(!/--registry-core/.test(joined), 'does NOT take the registry path without a token');
613
+ assert.ok(!/--registry-core/.test(joined), 'the retired env var must not resurrect the registry path');
614
+ });
615
+
616
+ await ta('scaffoldStandaloneRepo: a token in env changes NOTHING about the install path', async () => {
617
+ // BOTH runs set the retired request flag and vary ONLY the token — that is precisely the
618
+ // old divergence (flag + token → registry; flag, no token → vendored fail-soft). Comparing
619
+ // two runs that DON'T set the flag would pass whether or not the fork existed, which an
620
+ // adversarial re-add of the fork proved while this test was written that way.
621
+ const withToken = await scaffoldWithEnv({ PROVISION_CORE_FROM_REGISTRY: '1', NPM_TOKEN: 'npm_readonly' });
622
+ const without = await scaffoldWithEnv({ PROVISION_CORE_FROM_REGISTRY: '1' });
623
+ // THE LOAD-BEARING ASSERTION. The old fail-soft made a tokenless standup take a DIFFERENT
624
+ // install path than a tokenized one — two modes diverging on an env var (task 1003520).
625
+ // One public path means the presence of a credential cannot change the outcome at all.
626
+ // Normalize the per-run random temp names (scratch dir, spec file, pack dir) — they differ
627
+ // between any two runs and say nothing about which install path was taken.
628
+ const norm = (t) => t
629
+ .replace(/scaffold-[A-Za-z0-9]+/g, 'SCRATCH')
630
+ .replace(/bongos-init-[^\s.]+/g, 'SPEC')
631
+ .replace(/core-pack-[^\s/]+/g, 'PACK');
632
+ assert.equal(
633
+ norm(withToken),
634
+ norm(without),
635
+ 'the command sequence must be identical with and without a token',
636
+ );
614
637
  });
615
638
 
616
639
  await ta('scaffoldStandaloneRepo: default (no registry env) stays on the vendored tarball path', async () => {
@@ -107,10 +107,15 @@ t('a hosted origin leads with the board being a website that needs no install',
107
107
  assert.ok(/healthz/.test(card), 'offers the health probe instead of a bring-up');
108
108
  });
109
109
 
110
- t('a hosted origin explains the private-package 404 rather than implying a lost version', () => {
110
+ t('a hosted origin says the core installs with no token, and never re-offers the retired NPM_TOKEN advice', () => {
111
111
  const card = instanceNotRunningCard({ origin: 'https://example-instance.cloudbongos.com', depsMissing: true });
112
- assert.ok(/private/i.test(card) && /NPM_TOKEN/.test(card), 'names the real cause of a tokenless install failure');
113
- assert.ok(/not a missing version/.test(card), 'kills the "go find a version that still exists" wild goose chase');
112
+ assert.ok(/public/i.test(card), 'names the package as public — it is (task 1003872)');
113
+ assert.ok(/no token|needs no token/i.test(card), 'tells the reader nothing needs authenticating');
114
+ // The load-bearing half: this card spent months telling people to go find an NPM_TOKEN for
115
+ // a package that is now public. Advising that again would send a newcomer hunting a
116
+ // credential that does not exist and cannot be issued to them.
117
+ assert.doesNotMatch(card, /NPM_TOKEN/, 'must not resurrect the token advice');
118
+ assert.doesNotMatch(card, /\bprivate\b/i, 'must not call the package private');
114
119
  });
115
120
 
116
121
  t('isLocalOrigin: loopback is local, everything else is remote', () => {
package/tests/upgrade.mjs CHANGED
@@ -444,7 +444,7 @@ t('runUpgrade --registry + --from: refuses (mutually exclusive core sources)', a
444
444
  assert.match(res.error, /mutually exclusive/);
445
445
  });
446
446
 
447
- t('runUpgrade --registry (full): pins the exact version + writes the env-fed .npmrc, then install→migrate→verify', async () => {
447
+ t('runUpgrade --registry (full): pins the exact version, writes NO .npmrc, then install→migrate→verify', async () => {
448
448
  const dir = scratchConsumer({ pinned: '1.16.0', installed: '1.16.0' });
449
449
  const calls = [];
450
450
  const res = await u.runUpgrade(
@@ -460,8 +460,11 @@ t('runUpgrade --registry (full): pins the exact version + writes the env-fed .np
460
460
  assert.equal(res.ok, true, 'registry upgrade succeeds');
461
461
  assert.equal(res.toVersion, '1.17.0');
462
462
  assert.equal(JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).dependencies['@bongos/core'], '1.17.0', 'exact registry pin (no file:)');
463
- const npmrc = readFileSync(join(dir, '.npmrc'), 'utf8');
464
- assert.match(npmrc, /_authToken=\$\{NPM_TOKEN\}/, 'wrote the env-fed .npmrc');
463
+ // @bongos/core is public (task 1003872), so the registry path writes no `.npmrc` and
464
+ // resolves from the default registry. An `.npmrc` reappearing here would mean the retired
465
+ // auth plumbing came back — and it is the file whose `${NPM_TOKEN}` expansion used to
466
+ // SHADOW a working user-level credential (task 1002754), so its absence is load-bearing.
467
+ assert.equal(existsSync(join(dir, '.npmrc')), false, 'writes no .npmrc on the registry path');
465
468
  assert.ok(calls.some((c) => /^npm install/.test(c)), 'ran npm install (resolves from the registry)');
466
469
  assert.ok(!calls.some((c) => /vendor/.test(c)), 'did not vendor a tarball');
467
470
  });
@@ -1,124 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
- //
4
- // scripts/gds/npmrc.js — env-fed `.npmrc` for installing the PRIVATE @bongos/* core
5
- // from the npm registry (ADR 0134, task 2090). The single place that knows the
6
- // registry host + scope + the `.npmrc` shape + the opt-in/token gates, shared by:
7
- // • init.js — the greenfield `--registry-core` scaffold
8
- // • provision.js — the standalone scaffold's registry path (opt-in, fail-soft)
9
- // • upgrade.js — the `--registry <version>` update channel
10
- //
11
- // WHY private + env-fed. `@bongos/core` publishes as a SCOPED, PRIVATE package (ADR
12
- // 0134): a private scoped package can only be installed with auth. Consumers
13
- // authenticate with an `.npmrc` whose token EXPANDS FROM THE ENVIRONMENT at install
14
- // time (`_authToken=${NPM_TOKEN}`) — so the committed file carries NO secret, and a
15
- // tokenless install 404s cleanly (the expected pre-launch outcome).
16
- //
17
- // The vendored-tarball path (task 2053) stays the DEFAULT/fallback until this registry
18
- // path is proven end-to-end on a real greenfield standup (ADR 0134 §Consequences).
19
-
20
- const fs = require('node:fs');
21
- const path = require('node:path');
22
-
23
- const REGISTRY_HOST = 'registry.npmjs.org';
24
- const CORE_SCOPE = '@bongos'; // the paid, canonical npm org (ADR 0134 scope correction, task 2092)
25
- const TOKEN_ENV = 'NPM_TOKEN';
26
- const REQUEST_ENV = 'PROVISION_CORE_FROM_REGISTRY';
27
-
28
- // The `.npmrc` a scaffolded instance commits. The token is the LITERAL `${NPM_TOKEN}`
29
- // (npm expands env vars in `.npmrc` values at read time), so the file is safe to
30
- // commit — it holds no secret. Scoping @bongos to the registry keeps every unrelated
31
- // install on the default public registry.
32
- function npmrcContent({ scope = CORE_SCOPE, host = REGISTRY_HOST, tokenEnv = TOKEN_ENV } = {}) {
33
- return [
34
- `; Cloud Bongos — env-fed auth for the PRIVATE ${scope}/* packages (ADR 0134, task 2090).`,
35
- `; The token EXPANDS FROM THE ENVIRONMENT at install time; nothing secret is committed here.`,
36
- `; \`npm ci\` needs ${tokenEnv} set (a read-only npm access token from the paid \`bongos\` org);`,
37
- `; without it the private package 404s — that is the expected tokenless outcome.`,
38
- `${scope}:registry=https://${host}/`,
39
- `//${host}/:_authToken=\${${tokenEnv}}`,
40
- '',
41
- ].join('\n');
42
- }
43
-
44
- // Is a registry token present in the environment? The fail-soft gate: provisioning
45
- // falls back to the vendored tarball when this is false, so a tokenless standup still
46
- // works (never a broken instance).
47
- function hasNpmToken(env = process.env) {
48
- return !!String(env[TOKEN_ENV] || '').trim();
49
- }
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
-
82
- // Has the operator opted the standalone scaffold into the registry path? OFF by
83
- // default — the vendored tarball stays the default until the registry path is proven
84
- // end-to-end (ADR 0134). '1'/'true'/'yes'/'on' (case-insensitive) → true.
85
- function registryCoreRequested(env = process.env) {
86
- return /^(1|true|yes|on)$/i.test(String(env[REQUEST_ENV] || '').trim());
87
- }
88
-
89
- // Write the env-fed `.npmrc` into an instance dir. Default overwrites (a greenfield
90
- // scaffold owns the file); pass overwrite:false to leave a pre-existing `.npmrc`
91
- // untouched (an upgrade must not clobber a consumer's other registries). Returns the
92
- // path (or null when skipped). Dry-run writes nothing.
93
- function writeNpmrc(dir, { fsImpl = fs, dryRun = false, overwrite = true, log = () => {} } = {}) {
94
- const p = path.join(dir, '.npmrc');
95
- if (!overwrite && fsImpl.existsSync(p)) {
96
- log(` • .npmrc already present — left untouched (add \`${CORE_SCOPE}:registry\` + \`_authToken=\${${TOKEN_ENV}}\` if the private core doesn't resolve).`);
97
- return null;
98
- }
99
- if (dryRun) {
100
- log(` [dry-run] would write ${p} (env-fed ${CORE_SCOPE} auth; token via $${TOKEN_ENV}, never committed)`);
101
- return p;
102
- }
103
- fsImpl.writeFileSync(p, npmrcContent());
104
- log(` ✓ wrote .npmrc (env-fed ${CORE_SCOPE} auth; token via $${TOKEN_ENV}, never committed)`);
105
- return p;
106
- }
107
-
108
- module.exports = {
109
- REGISTRY_HOST, CORE_SCOPE, TOKEN_ENV, REQUEST_ENV,
110
- npmrcContent, hasNpmToken, resolveNpmToken, registryCoreRequested, writeNpmrc,
111
- };
112
-
113
- // Tiny CLI: `node scripts/gds/npmrc.js [--print | --write <dir>]` — print the `.npmrc`
114
- // body or materialize it into <dir>. Handy for a manual registry-standup rehearsal.
115
- if (require.main === module) {
116
- const args = process.argv.slice(2);
117
- const writeIdx = args.indexOf('--write');
118
- if (writeIdx !== -1 && args[writeIdx + 1]) {
119
- const p = writeNpmrc(path.resolve(args[writeIdx + 1]), { log: console.log });
120
- process.stdout.write(`${p}\n`);
121
- } else {
122
- process.stdout.write(npmrcContent());
123
- }
124
- }
package/tests/npmrc.mjs DELETED
@@ -1,154 +0,0 @@
1
- // tests/npmrc.mjs
2
- //
3
- // Unit tests for scripts/gds/npmrc.js (ADR 0134, task 2090) — the single source of the
4
- // env-fed `.npmrc` used to install the PRIVATE @bongos/core from the npm registry, plus
5
- // the opt-in / token gates that keep the vendored-tarball path the default/fallback.
6
- //
7
- // Run: node tests/npmrc.mjs
8
-
9
- import { strict as assert } from 'node:assert';
10
- import { createRequire } from 'node:module';
11
- import { mkdtempSync, writeFileSync, readFileSync, existsSync, rmSync } from 'node:fs';
12
- import { tmpdir } from 'node:os';
13
- import { join } from 'node:path';
14
-
15
- const require = createRequire(import.meta.url);
16
- const n = require('../scripts/gds/npmrc.js');
17
-
18
- let passed = 0, failed = 0;
19
- function test(name, fn) {
20
- try { fn(); passed++; console.log(` ok ${name}`); }
21
- catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.message}`); }
22
- }
23
-
24
- // ---- npmrcContent ----------------------------------------------------------
25
- test('npmrcContent: scopes @bongos to the registry + auth token EXPANDS from the env (literal ${NPM_TOKEN})', () => {
26
- const body = n.npmrcContent();
27
- assert.match(body, /@bongos:registry=https:\/\/registry\.npmjs\.org\//);
28
- assert.match(body, /\/\/registry\.npmjs\.org\/:_authToken=\$\{NPM_TOKEN\}/);
29
- });
30
-
31
- test('npmrcContent: carries NO real secret — only the ${NPM_TOKEN} placeholder', () => {
32
- const body = n.npmrcContent();
33
- // the only _authToken= assignment must be the env placeholder, never a concrete value
34
- const tokenLines = body.split('\n').filter((l) => /_authToken=/.test(l));
35
- assert.equal(tokenLines.length, 1);
36
- assert.equal(tokenLines[0].trim(), '//registry.npmjs.org/:_authToken=${NPM_TOKEN}');
37
- });
38
-
39
- test('npmrcContent: comment lines use the ini `;` comment char', () => {
40
- const comments = n.npmrcContent().split('\n').filter((l) => l.startsWith(';'));
41
- assert.ok(comments.length >= 3, 'has explanatory comments');
42
- });
43
-
44
- test('npmrcContent: honors custom scope/host/tokenEnv', () => {
45
- const body = n.npmrcContent({ scope: '@acme', host: 'npm.acme.dev', tokenEnv: 'ACME_TOKEN' });
46
- assert.match(body, /@acme:registry=https:\/\/npm\.acme\.dev\//);
47
- assert.match(body, /\/\/npm\.acme\.dev\/:_authToken=\$\{ACME_TOKEN\}/);
48
- });
49
-
50
- // ---- gates -----------------------------------------------------------------
51
- test('hasNpmToken: true only when NPM_TOKEN is a non-empty string', () => {
52
- assert.equal(n.hasNpmToken({ NPM_TOKEN: 'npm_abc' }), true);
53
- assert.equal(n.hasNpmToken({ NPM_TOKEN: '' }), false);
54
- assert.equal(n.hasNpmToken({ NPM_TOKEN: ' ' }), false);
55
- assert.equal(n.hasNpmToken({}), false);
56
- });
57
-
58
- test('registryCoreRequested: 1/true/yes/on → true; everything else → false', () => {
59
- for (const v of ['1', 'true', 'TRUE', 'yes', 'on']) assert.equal(n.registryCoreRequested({ PROVISION_CORE_FROM_REGISTRY: v }), true, v);
60
- for (const v of ['0', 'false', 'no', '', 'off', 'garbage']) assert.equal(n.registryCoreRequested({ PROVISION_CORE_FROM_REGISTRY: v }), false, v);
61
- assert.equal(n.registryCoreRequested({}), false, 'unset');
62
- });
63
-
64
- // ---- writeNpmrc ------------------------------------------------------------
65
- test('writeNpmrc: writes the env-fed .npmrc + returns its path', () => {
66
- const dir = mkdtempSync(join(tmpdir(), 'npmrc-'));
67
- try {
68
- const p = n.writeNpmrc(dir, { log: () => {} });
69
- assert.equal(p, join(dir, '.npmrc'));
70
- assert.equal(readFileSync(p, 'utf8'), n.npmrcContent());
71
- } finally { rmSync(dir, { recursive: true, force: true }); }
72
- });
73
-
74
- test('writeNpmrc: dry-run writes nothing but returns the intended path', () => {
75
- const dir = mkdtempSync(join(tmpdir(), 'npmrc-'));
76
- try {
77
- const p = n.writeNpmrc(dir, { dryRun: true, log: () => {} });
78
- assert.equal(p, join(dir, '.npmrc'));
79
- assert.equal(existsSync(join(dir, '.npmrc')), false, 'dry-run must not write');
80
- } finally { rmSync(dir, { recursive: true, force: true }); }
81
- });
82
-
83
- test('writeNpmrc: overwrite:false leaves a pre-existing .npmrc untouched (returns null)', () => {
84
- const dir = mkdtempSync(join(tmpdir(), 'npmrc-'));
85
- try {
86
- const existing = '@other:registry=https://npm.other.dev/\n';
87
- writeFileSync(join(dir, '.npmrc'), existing);
88
- const p = n.writeNpmrc(dir, { overwrite: false, log: () => {} });
89
- assert.equal(p, null, 'skipped');
90
- assert.equal(readFileSync(join(dir, '.npmrc'), 'utf8'), existing, 'existing content preserved');
91
- } finally { rmSync(dir, { recursive: true, force: true }); }
92
- });
93
-
94
- test('writeNpmrc: overwrite:true (default) replaces an existing .npmrc', () => {
95
- const dir = mkdtempSync(join(tmpdir(), 'npmrc-'));
96
- try {
97
- writeFileSync(join(dir, '.npmrc'), 'stale\n');
98
- n.writeNpmrc(dir, { log: () => {} });
99
- assert.equal(readFileSync(join(dir, '.npmrc'), 'utf8'), n.npmrcContent());
100
- } finally { rmSync(dir, { recursive: true, force: true }); }
101
- });
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
-
152
- console.log('');
153
- console.log(`npmrc tests: ${passed} passed, ${failed} failed`);
154
- process.exit(failed ? 1 : 0);