@bongos/core 1.19.737 → 1.19.739

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.
@@ -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.739'; // 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
- }