@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.
- package/.bongos-core.json +27 -37
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/private-npm-distribution.md +11 -11
- package/docs/recipes/upgrading-the-core.md +3 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/go-live.js +18 -44
- package/scripts/gds/init.js +21 -29
- package/scripts/gds/package-core.js +7 -6
- package/scripts/gds/provision-config.js +0 -8
- package/scripts/gds/provision-repo.js +26 -30
- package/scripts/gds/provision.js +3 -5
- package/scripts/gds/start.js +1 -1
- package/scripts/gds/update-channel.js +3 -3
- package/scripts/gds/upgrade.js +17 -29
- package/src/module-api.js +1 -1
- package/tests/go_live.mjs +20 -19
- package/tests/init.mjs +11 -6
- package/tests/provision.mjs +33 -10
- package/tests/start_env_guard.mjs +8 -3
- package/tests/upgrade.mjs +6 -3
- package/scripts/gds/npmrc.js +0 -124
- package/tests/npmrc.mjs +0 -154
|
@@ -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,
|
|
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
|
|
606
|
-
//
|
|
607
|
-
//
|
|
608
|
-
//
|
|
609
|
-
//
|
|
610
|
-
//
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
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
|
-
//
|
|
631
|
-
//
|
|
632
|
-
//
|
|
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
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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
|
//
|
package/scripts/gds/provision.js
CHANGED
|
@@ -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
|
-
//
|
|
364
|
-
// .
|
|
365
|
-
//
|
|
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 });
|
package/scripts/gds/start.js
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
127
|
-
//
|
|
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 } = {}) {
|
package/scripts/gds/upgrade.js
CHANGED
|
@@ -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
|
|
102
|
-
//
|
|
103
|
-
//
|
|
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
|
|
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
|
-
//
|
|
485
|
-
//
|
|
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.
|
|
987
|
-
|
|
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
|
|
1098
|
-
// REGISTRY (--registry): pin @bongos/core@<version>
|
|
1099
|
-
// the
|
|
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) {
|
|
1114
|
-
log(` ${dryRun ? '[dry-run] would pin' : '✓ pinned'} ${CORE_PKG} → ${targetVersion} (registry)${
|
|
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
|
|
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 (
|
|
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.
|
|
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
|
|
446
|
-
const cmd = G.asOwner({ runAs: 'lars', env: { PGDATABASE: 'cb' } }, 'npm install'
|
|
447
|
-
//
|
|
448
|
-
// sudo's COMMAND= field
|
|
449
|
-
// a different field of the SAME log line, which
|
|
450
|
-
//
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
//
|
|
455
|
-
assert.
|
|
456
|
-
assert.
|
|
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('
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
assert.
|
|
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
|
|
474
|
-
const cmd = G.asOwner({ env: { A: 'b' } }, 'npm x'
|
|
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.
|
|
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
|
|
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
|
|
774
|
-
//
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
package/tests/provision.mjs
CHANGED
|
@@ -599,18 +599,41 @@ async function scaffoldWithEnv(env) {
|
|
|
599
599
|
}
|
|
600
600
|
}
|
|
601
601
|
|
|
602
|
-
|
|
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, /
|
|
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), '
|
|
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
|
|
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(/
|
|
113
|
-
assert.ok(/
|
|
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
|
|
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
|
-
|
|
464
|
-
|
|
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
|
});
|
package/scripts/gds/npmrc.js
DELETED
|
@@ -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
|
-
}
|