@bongos/core 1.19.196 → 1.19.197
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 +14 -14
- package/docs/adr/0133-github-app-manifest-onboarding-signin.md +3 -1
- package/docs/recipes/ops-gotchas.md +7 -0
- package/modules/provisioning/provisioning.js +8 -2
- package/modules/provisioning/tests/provisioning.mjs +5 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/provision.js +184 -29
- package/src/module-api.js +3 -1
- package/tests/provision.mjs +66 -0
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.197",
|
|
6
|
+
"core_contract": "1.19.197",
|
|
7
|
+
"source_commit": "daaea01c453e9eae6e2748316e8e7dcfc24ae76b",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-17T14:35:32.236Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 309,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 1615,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "4788b5bff5cc099e1dff83c6a3bee9765222848137afe419fd54e869a6c4c39e",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -1137,7 +1137,7 @@
|
|
|
1137
1137
|
{
|
|
1138
1138
|
"path": "docs/adr/0133-github-app-manifest-onboarding-signin.md",
|
|
1139
1139
|
"mode": "0000644",
|
|
1140
|
-
"sha256": "
|
|
1140
|
+
"sha256": "94e1f79e0a32a53b51e5bbf6d10d37d477d676cad33b267dc3ddc0891ae70704"
|
|
1141
1141
|
},
|
|
1142
1142
|
{
|
|
1143
1143
|
"path": "docs/adr/0134-private-first-npm-distribution.md",
|
|
@@ -1672,7 +1672,7 @@
|
|
|
1672
1672
|
{
|
|
1673
1673
|
"path": "docs/recipes/ops-gotchas.md",
|
|
1674
1674
|
"mode": "0000644",
|
|
1675
|
-
"sha256": "
|
|
1675
|
+
"sha256": "52a209553565405dca461b1c4c0cef9fb0ec6d98b95ed420c82eaf1fa3eb15f6"
|
|
1676
1676
|
},
|
|
1677
1677
|
{
|
|
1678
1678
|
"path": "docs/recipes/overnight-builder.md",
|
|
@@ -3967,7 +3967,7 @@
|
|
|
3967
3967
|
{
|
|
3968
3968
|
"path": "modules/provisioning/provisioning.js",
|
|
3969
3969
|
"mode": "0000644",
|
|
3970
|
-
"sha256": "
|
|
3970
|
+
"sha256": "eebcba4b333030f17c06bc6e1423450ae4af124e4075af52cbd512e80d5ba95e"
|
|
3971
3971
|
},
|
|
3972
3972
|
{
|
|
3973
3973
|
"path": "modules/provisioning/rate-limit.js",
|
|
@@ -3987,7 +3987,7 @@
|
|
|
3987
3987
|
{
|
|
3988
3988
|
"path": "modules/provisioning/tests/provisioning.mjs",
|
|
3989
3989
|
"mode": "0000644",
|
|
3990
|
-
"sha256": "
|
|
3990
|
+
"sha256": "98787fad5df787a8ebdbf90fb8bf21b68f1529387b15d65724fdafc8ac0b91d0"
|
|
3991
3991
|
},
|
|
3992
3992
|
{
|
|
3993
3993
|
"path": "modules/provisioning/ui/.gitkeep",
|
|
@@ -4272,12 +4272,12 @@
|
|
|
4272
4272
|
{
|
|
4273
4273
|
"path": "package-lock.json",
|
|
4274
4274
|
"mode": "0000644",
|
|
4275
|
-
"sha256": "
|
|
4275
|
+
"sha256": "69be9b66c6af5ccad87644d9a2460f8a6c59908e42613104b612654c41fa67ba"
|
|
4276
4276
|
},
|
|
4277
4277
|
{
|
|
4278
4278
|
"path": "package.json",
|
|
4279
4279
|
"mode": "0000644",
|
|
4280
|
-
"sha256": "
|
|
4280
|
+
"sha256": "0b113ba37f114ec82e9f2dafa7e9d5cca35886e157934d7475850eb4f9dd66e1"
|
|
4281
4281
|
},
|
|
4282
4282
|
{
|
|
4283
4283
|
"path": "public-docs/index.html",
|
|
@@ -5192,7 +5192,7 @@
|
|
|
5192
5192
|
{
|
|
5193
5193
|
"path": "scripts/gds/provision.js",
|
|
5194
5194
|
"mode": "0000644",
|
|
5195
|
-
"sha256": "
|
|
5195
|
+
"sha256": "1c348e8859004c69e66b8eede56da0bab68ec4d473790f1c1f9bf1769934211f"
|
|
5196
5196
|
},
|
|
5197
5197
|
{
|
|
5198
5198
|
"path": "scripts/gds/publish-credential-check.js",
|
|
@@ -5887,7 +5887,7 @@
|
|
|
5887
5887
|
{
|
|
5888
5888
|
"path": "src/module-api.js",
|
|
5889
5889
|
"mode": "0000644",
|
|
5890
|
-
"sha256": "
|
|
5890
|
+
"sha256": "bdfd06f58403f6cb568ed65718a92450c7e41470a6eae1c2583101e9e47e6656"
|
|
5891
5891
|
},
|
|
5892
5892
|
{
|
|
5893
5893
|
"path": "src/module-loader/catalog.js",
|
|
@@ -7507,7 +7507,7 @@
|
|
|
7507
7507
|
{
|
|
7508
7508
|
"path": "tests/provision.mjs",
|
|
7509
7509
|
"mode": "0000644",
|
|
7510
|
-
"sha256": "
|
|
7510
|
+
"sha256": "9e8da984a2a9378809f03cc3f5f1c2cda34f8589ad954db1acd5b12bf424f322"
|
|
7511
7511
|
},
|
|
7512
7512
|
{
|
|
7513
7513
|
"path": "tests/provisioning_default_domain.mjs",
|
|
@@ -31,7 +31,9 @@ The conversion endpoint is unauthenticated (the code is the auth), so the web ti
|
|
|
31
31
|
|
|
32
32
|
### Manifest shape
|
|
33
33
|
|
|
34
|
-
`redirect_url` = the **mothership** callback (the instance can't do the exchange). `callback_urls` = the **instance's** own `/api/gds/auth/web/callback` (what `auth.js` actually builds) **and** the `/api/bongos/...` alias, so sign-in works regardless of which prefix a build emits. `default_permissions: {}` (sign-in needs none), `hook_attributes.active:false`, `public:
|
|
34
|
+
`redirect_url` = the **mothership** callback (the instance can't do the exchange). `callback_urls` = the **instance's** own `/api/gds/auth/web/callback` (what `auth.js` actually builds) **and** the `/api/bongos/...` alias, so sign-in works regardless of which prefix a build emits. `default_permissions: {}` (sign-in needs none), `hook_attributes.active:false`, `public:true`. Personal vs org differs only in the POST URL (derived from the repo owner); the App works for sign-in either way.
|
|
35
|
+
|
|
36
|
+
> **Amended 2026-08-17 (task 1003048, gap-list W6):** the manifest originally set `public:false`. That made every wizard-born project structurally single-builder on the web — GitHub answers a private App's authorize URL for any non-owner account with a bare 404, before the instance is ever involved (reproduced live on Spike Harbor). The App requests zero permissions, so public costs nothing; the manifest now sets `public:true`. Apps minted before the fix must be flipped by hand (GitHub App settings → Advanced → Make public) — see the ops-gotchas entry.
|
|
35
37
|
|
|
36
38
|
**App name (tasks 2093 → 2095).** GitHub App names are globally unique across all of GitHub, so a bare project slug (e.g. `demo`) is almost always already taken. The auto name is the **instance domain** (e.g. `demo.cloudbongos.com`): a domain is inherently unique (so it never collides) and it reads perfectly on GitHub's sign-in consent screen, where the name is shown as *"`<name>` by `<owner>` … will redirect to `https://<domain>`"* — the name IS the destination. GitHub keeps the dots in the display name (only the internal slug is transformed) and caps it at 34 chars. **Fallback** (no domain, or a domain over 34 chars): brand-qualify the project — `<mothership brand> - <project>`, e.g. `Cloud Bongos - demo`, from the mothership's own `branding().identity.productName` (portable, not hardcoded) — still far less collision-prone than a bare slug. Either way it is only a default the owner can rename on GitHub's confirmation screen.
|
|
37
39
|
|
|
@@ -127,6 +127,13 @@ Organized by topic, not chronology. New entries go in the matching section; crea
|
|
|
127
127
|
- **Where this lives in the codebase:** [`src/bongos/routes/auth.js`](../../src/bongos/routes/auth.js) — `/auth/web/start` computes `redirectUri` from `publicOrigin(req)` and the `/auth/web/callback` route reads back to it. As of [#371](https://example.com/builders#/task/371) the error branches render a styled HTML page (with a retry link) instead of `text/plain`, so the visitor at least sees something coherent.
|
|
128
128
|
- **Why it bites:** GitHub's warning page is intentionally scary and doesn't tell you *which* URL it expected; the only diagnostic is the `error=redirect_uri_mismatch` query param on the redirect back, which our server now surfaces in the styled error page.
|
|
129
129
|
|
|
130
|
+
### Manifest-born GitHub Apps created before the task-1003048 fix are PRIVATE — non-owners get a bare 404 at sign-in
|
|
131
|
+
|
|
132
|
+
- **Symptom:** On a wizard-created instance, any GitHub account other than the App's owner clicks "Sign in with GitHub" and gets a bare GitHub 404 — before the instance is ever involved. The project is effectively single-builder on the web. (Device Flow sign-in for the same non-owner works, which makes the failure look like anything but an App setting.)
|
|
133
|
+
- **Root cause:** the ADR 0133 manifest flow used to mint the GitHub App with `public: false`, and GitHub 404s a private App's authorize URL for every non-owner. Fixed for newly minted Apps (`buildGithubAppManifest` now sets `public: true` — gap-list W6, task 1003048), but a manifest can't be re-applied to an existing App.
|
|
134
|
+
- **Fix for pre-existing instances:** the App owner flips it by hand — github.com → Settings → Developer settings → GitHub Apps → (the instance's App) → **Advanced** → **Make public**. One click, no permission changes (the App requests none).
|
|
135
|
+
- **Live evidence:** reproduced twice against Spike Harbor (instance 18) with a fresh verified-email account, 2026-08-16 — see `docs/audits/project-creation-gap-list.md` W6.
|
|
136
|
+
|
|
130
137
|
---
|
|
131
138
|
|
|
132
139
|
## Mac / local dev
|
|
@@ -265,7 +265,7 @@ function githubAppName({ domain, brand, project } = {}) {
|
|
|
265
265
|
// • callback_urls — where "Login with GitHub" on the INSTANCE redirects during real
|
|
266
266
|
// sign-in. Both /api/gds/... (what auth.js actually builds) and the /api/bongos/...
|
|
267
267
|
// alias are registered so sign-in works regardless of which prefix a build emits.
|
|
268
|
-
// Minimal for sign-in: no repo permissions, no webhook (active:false)
|
|
268
|
+
// Minimal for sign-in: no repo permissions, no webhook (active:false). PURE.
|
|
269
269
|
function buildGithubAppManifest({ instanceName, slug, instanceDomain, mothershipOrigin, brand }) {
|
|
270
270
|
const instOrigin = `https://${instanceDomain}`;
|
|
271
271
|
const redirectUrl = `${String(mothershipOrigin || '').replace(/\/+$/, '')}/api/bongos/provisioning/github-app/callback`;
|
|
@@ -284,7 +284,13 @@ function buildGithubAppManifest({ instanceName, slug, instanceDomain, mothership
|
|
|
284
284
|
`${instOrigin}/api/gds/auth/web/callback`,
|
|
285
285
|
`${instOrigin}/api/bongos/auth/web/callback`,
|
|
286
286
|
],
|
|
287
|
-
public:
|
|
287
|
+
// public: GitHub answers a PRIVATE App's web authorize URL for any non-owner
|
|
288
|
+
// account with a bare 404 (before the instance is ever involved), so a private
|
|
289
|
+
// App makes the project structurally single-builder on the web. The App requests
|
|
290
|
+
// zero permissions, so public costs nothing (gap-list W6, task 1003048). Apps
|
|
291
|
+
// minted before this fix stay private until flipped by hand — see
|
|
292
|
+
// docs/recipes/ops-gotchas.md.
|
|
293
|
+
public: true,
|
|
288
294
|
};
|
|
289
295
|
}
|
|
290
296
|
|
|
@@ -165,7 +165,7 @@ t('githubAppName falls back to brand-qualified project when there is no usable d
|
|
|
165
165
|
assert.ok(long.length <= 34 && long.startsWith('Cloud Bongos - '), `capped + brand kept: ${long}`);
|
|
166
166
|
});
|
|
167
167
|
|
|
168
|
-
t('buildGithubAppManifest points redirect at the MOTHERSHIP, callbacks at the INSTANCE, minimal perms, domain-named', () => {
|
|
168
|
+
t('buildGithubAppManifest points redirect at the MOTHERSHIP, callbacks at the INSTANCE, minimal perms, public, domain-named', () => {
|
|
169
169
|
const m = P.buildGithubAppManifest({
|
|
170
170
|
instanceName: 'mercury', slug: 'mercury', brand: 'Cloud Bongos',
|
|
171
171
|
instanceDomain: 'mercury.example.com', mothershipOrigin: 'https://cloudbongos.com',
|
|
@@ -179,10 +179,12 @@ t('buildGithubAppManifest points redirect at the MOTHERSHIP, callbacks at the IN
|
|
|
179
179
|
'https://mercury.example.com/api/gds/auth/web/callback',
|
|
180
180
|
'https://mercury.example.com/api/bongos/auth/web/callback',
|
|
181
181
|
]);
|
|
182
|
-
// sign-in needs no repo/account permissions; no active webhook
|
|
182
|
+
// sign-in needs no repo/account permissions; no active webhook
|
|
183
183
|
assert.deepEqual(m.default_permissions, {});
|
|
184
184
|
assert.equal(m.hook_attributes.active, false);
|
|
185
|
-
|
|
185
|
+
// public: a PRIVATE App answers the web authorize URL for any non-owner with a bare
|
|
186
|
+
// GitHub 404, making the project single-builder on the web (gap-list W6, task 1003048)
|
|
187
|
+
assert.equal(m.public, true);
|
|
186
188
|
});
|
|
187
189
|
|
|
188
190
|
t('buildGithubAppManifest tolerates a trailing slash on the mothership origin', () => {
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.197",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.197",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.197",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/scripts/gds/provision.js
CHANGED
|
@@ -440,6 +440,42 @@ function installedCorePackDir({ instanceRoot, fsImpl = fs } = {}) {
|
|
|
440
440
|
return null;
|
|
441
441
|
}
|
|
442
442
|
|
|
443
|
+
// Resolve a vendorable @bongos/core tarball — the three-rung ladder, freshest-correct
|
|
444
|
+
// first, shared by the fresh scaffold and the re-provision pin refresh (task 1003050):
|
|
445
|
+
// 1. a `file:`-pinned control plane reuses its own pinned tarball (task 1002275 —
|
|
446
|
+
// rebuilding via package-core.js aborts with an EMPTY core set when this runner
|
|
447
|
+
// IS a consumer instance, leaving new repos empty);
|
|
448
|
+
// 2. a REGISTRY-pinned consumer (the post-1002621 mothership shape) packs the core
|
|
449
|
+
// it has INSTALLED — the child pins the SAME, CURRENT core the hub runs, ending
|
|
450
|
+
// the frozen-old-tarball births (task 1002706, gap 13) without requiring the
|
|
451
|
+
// child's owner to hold a token for the still-private registry package;
|
|
452
|
+
// 3. a genuine core source checkout rebuilds fresh via package-core.js.
|
|
453
|
+
// Returns { tgz, packDest } — packDest (set only on rung 2) is a temp dir the CALLER
|
|
454
|
+
// must `rm -rf` once the tarball has been vendored.
|
|
455
|
+
function resolveVendorableCoreTarball({ instanceRoot, exec, log, coreV, tag = '[core]' }) {
|
|
456
|
+
let tgz = installedCoreTarball({ instanceRoot });
|
|
457
|
+
let packDest = null;
|
|
458
|
+
if (tgz) {
|
|
459
|
+
log(` ${tag} reusing the pinned core tarball this control plane runs (${path.basename(tgz)}) — no source rebuild (task 1002275)`);
|
|
460
|
+
} else {
|
|
461
|
+
const packSrc = installedCorePackDir({ instanceRoot });
|
|
462
|
+
if (packSrc) {
|
|
463
|
+
// Keyed by VERSION, not slug: no request-derived value enters this command, and
|
|
464
|
+
// two concurrent scaffolds of the same core pack identical bytes — collision-safe.
|
|
465
|
+
packDest = path.join(os.tmpdir(), `bongos-core-pack-${packSrc.version}`);
|
|
466
|
+
log(` ${tag} packing the registry-installed core this control plane runs (v${packSrc.version}) — no source rebuild (task 1002706)`);
|
|
467
|
+
exec(`mkdir -p ${packDest}`);
|
|
468
|
+
exec(`npm pack ${packSrc.dir} --pack-destination ${packDest}`);
|
|
469
|
+
tgz = path.join(packDest, packSrc.tarballName);
|
|
470
|
+
} else {
|
|
471
|
+
log(` ${tag} building a fresh vendored tarball from source (package-core.js)`);
|
|
472
|
+
exec(`node scripts/gds/package-core.js`); // cwd = REPO_ROOT → dist/bongos-core-<v>.tgz
|
|
473
|
+
tgz = path.join(REPO_ROOT, 'dist', `bongos-core-${coreV}.tgz`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return { tgz, packDest };
|
|
477
|
+
}
|
|
478
|
+
|
|
443
479
|
// The onboard mode a provisioning row carries (migration provisioning_004, task 1002562).
|
|
444
480
|
// 'adopt' means target_ref is a repo the owner ALREADY has, so the scaffold must LAYER
|
|
445
481
|
// onto its history. Anything else — including a row written before the column existed, or
|
|
@@ -647,35 +683,11 @@ async function scaffoldStandaloneRepo(inst, deps) {
|
|
|
647
683
|
log(` [scaffold] core install: registry — PRIVATE @bongos/core@${coreV} via env-fed NPM_TOKEN (ADR 0134)`);
|
|
648
684
|
exec(`node scripts/gds/init.js --from ${specPath} --dir ${root} --no-seed ${modeFlags} --registry-core`);
|
|
649
685
|
} else {
|
|
650
|
-
// Vendored tarball —
|
|
651
|
-
//
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
// it has INSTALLED — the child pins the SAME, CURRENT core the hub runs, ending
|
|
656
|
-
// the frozen-old-tarball births (task 1002706, gap 13) without requiring the
|
|
657
|
-
// child's owner to hold a token for the still-private registry package;
|
|
658
|
-
// 3. a genuine core source checkout rebuilds fresh via package-core.js.
|
|
659
|
-
let tgz = installedCoreTarball({ instanceRoot: deps.instanceRoot });
|
|
660
|
-
let packDest = null; // set on the pack rung — cleaned up once init has vendored the tgz
|
|
661
|
-
if (tgz) {
|
|
662
|
-
log(` [scaffold] core install: reusing the pinned core tarball this control plane runs (${path.basename(tgz)}) — no source rebuild (task 1002275)`);
|
|
663
|
-
} else {
|
|
664
|
-
const packSrc = installedCorePackDir({ instanceRoot: deps.instanceRoot });
|
|
665
|
-
if (packSrc) {
|
|
666
|
-
// Keyed by VERSION, not slug: no request-derived value enters this command, and
|
|
667
|
-
// two concurrent scaffolds of the same core pack identical bytes — collision-safe.
|
|
668
|
-
packDest = path.join(os.tmpdir(), `bongos-core-pack-${packSrc.version}`);
|
|
669
|
-
log(` [scaffold] core install: packing the registry-installed core this control plane runs (v${packSrc.version}) — no source rebuild (task 1002706)`);
|
|
670
|
-
exec(`mkdir -p ${packDest}`);
|
|
671
|
-
exec(`npm pack ${packSrc.dir} --pack-destination ${packDest}`);
|
|
672
|
-
tgz = path.join(packDest, packSrc.tarballName);
|
|
673
|
-
} else {
|
|
674
|
-
log(` [scaffold] core install: building a fresh vendored tarball from source (package-core.js)`);
|
|
675
|
-
exec(`node scripts/gds/package-core.js`); // cwd = REPO_ROOT → dist/bongos-core-<v>.tgz
|
|
676
|
-
tgz = path.join(REPO_ROOT, 'dist', `bongos-core-${coreV}.tgz`);
|
|
677
|
-
}
|
|
678
|
-
}
|
|
686
|
+
// Vendored tarball — resolveVendorableCoreTarball's three-rung ladder (shared
|
|
687
|
+
// with the re-provision pin refresh, task 1003050).
|
|
688
|
+
const { tgz, packDest } = resolveVendorableCoreTarball({
|
|
689
|
+
instanceRoot: deps.instanceRoot, exec, log, coreV, tag: '[scaffold] core install:',
|
|
690
|
+
});
|
|
679
691
|
exec(`node scripts/gds/init.js --from ${specPath} --dir ${root} --no-seed ${modeFlags} --vendor-core ${tgz}`);
|
|
680
692
|
// init --vendor-core COPIED the tarball into the child's vendor/ — the temp pack dir is spent.
|
|
681
693
|
if (packDest) exec(`rm -rf ${packDest}`);
|
|
@@ -730,6 +742,140 @@ async function scaffoldStandaloneRepo(inst, deps) {
|
|
|
730
742
|
}
|
|
731
743
|
}
|
|
732
744
|
|
|
745
|
+
// ── re-provision core-pin refresh (gap-list W3, task 1003050) ────────────────────
|
|
746
|
+
//
|
|
747
|
+
// The hazard: a standalone instance's RUNNING core can move ahead of its repo's
|
|
748
|
+
// committed `file:` pin (an out-of-band `bongos upgrade`, a pre-fix re-provision that
|
|
749
|
+
// refreshed the box without committing) — seen live on Spike Harbor, running a modern
|
|
750
|
+
// core while the repo still pinned 1.19.2. The deploy path is repo-driven
|
|
751
|
+
// (`git reset --hard origin/main` + `npm ci`), so the NEXT deploy silently rolls the
|
|
752
|
+
// instance back against a DB the newer core has already migrated. ADR 0168 fixed
|
|
753
|
+
// fresh scaffolds only; this closes the re-provision leg: every provision of an
|
|
754
|
+
// already-scaffolded instance heals the committed pin to the core this control plane
|
|
755
|
+
// runs, as a commit pushed to the instance repo.
|
|
756
|
+
|
|
757
|
+
// The PURE decision half (tested in tests/provision.mjs). Registry pins are npm's to
|
|
758
|
+
// resolve; an absent dep is not ours to invent; only `file:` pins are healed.
|
|
759
|
+
function planCorePinRefresh({ pinnedDep, coreVersion }) {
|
|
760
|
+
const dep = String(pinnedDep || '').trim();
|
|
761
|
+
if (!dep) return { action: 'skip', reason: 'no @bongos/core dependency in the instance package.json' };
|
|
762
|
+
if (!/^file:/.test(dep)) return { action: 'skip', reason: `registry pin (${dep}) — npm ci resolves it; nothing to vendor` };
|
|
763
|
+
const upgrade = require('./upgrade');
|
|
764
|
+
const pinned = upgrade.versionFromTgzPath(dep);
|
|
765
|
+
if (pinned && pinned === coreVersion) return { action: 'current', pinned };
|
|
766
|
+
return { action: 'refresh', pinned: pinned || null };
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Coerce a version label to a semver-safe charset BEFORE it is interpolated into any
|
|
770
|
+
// shell string. The "from" label ultimately derives from the INSTANCE's own repo
|
|
771
|
+
// package.json `file:` pin (lower trust than the shared control plane) via
|
|
772
|
+
// versionFromTgzPath, whose regex admits any char but `/` — including `"`, backtick,
|
|
773
|
+
// `$()`, `;`. A crafted pin (e.g. `file:vendor/bongos-core-1.0.0";touch x;echo".tgz`)
|
|
774
|
+
// would otherwise break out of a `git commit -m "…"` argument and run as the
|
|
775
|
+
// control-plane user. This chokepoint rejects anything outside semver's own alphabet
|
|
776
|
+
// (task 1003050 grade — command-injection block). PURE.
|
|
777
|
+
function safeVersionLabel(v) {
|
|
778
|
+
const s = String(v == null ? '' : v).trim();
|
|
779
|
+
return /^[0-9A-Za-z.+-]{1,64}$/.test(s) ? s : '(unversioned)';
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// The commit + push command sequence for a pin refresh — PURE, so tests/provision.mjs
|
|
783
|
+
// covers "the runner's commit step" (task 1003050 AC) deterministically. Version
|
|
784
|
+
// labels pass through safeVersionLabel (the one interpolation-safety chokepoint); the
|
|
785
|
+
// push prefers the owner's OAuth token URL (the shared deploy key can't write a
|
|
786
|
+
// consumer repo — task 1002317) and falls back to the SSH origin.
|
|
787
|
+
function buildCorePinRefreshCommit({ root, fromVersion, toVersion, repo, ownerToken, credFile }) {
|
|
788
|
+
const from = safeVersionLabel(fromVersion);
|
|
789
|
+
const to = safeVersionLabel(toVersion);
|
|
790
|
+
const msg = `chore: re-provision refreshes the pinned @bongos/core ${from} -> ${to} (task 1003050)`;
|
|
791
|
+
const push = (ownerToken && repo && credFile)
|
|
792
|
+
? `git -C ${root} -c credential.helper='store --file=${credFile}' push https://x-access-token@github.com/${repo.owner}/${repo.name}.git HEAD:main`
|
|
793
|
+
: `git -C ${root} push origin main`;
|
|
794
|
+
return {
|
|
795
|
+
config: [
|
|
796
|
+
`git -C ${root} config user.name "Cloud Bongos"`,
|
|
797
|
+
`git -C ${root} config user.email "bot@cloudbongos.com"`,
|
|
798
|
+
],
|
|
799
|
+
add: `git -C ${root} add -A`,
|
|
800
|
+
commit: `git -C ${root} commit -m "${msg}"`,
|
|
801
|
+
push,
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
async function refreshStandaloneCorePin(inst, deps) {
|
|
806
|
+
const { log } = deps;
|
|
807
|
+
const exec = deps.controlExec || deps.exec;
|
|
808
|
+
const writeFile = deps.controlWriteFile || deps.writeFile;
|
|
809
|
+
const upgrade = require('./upgrade');
|
|
810
|
+
const root = standaloneRoot(inst);
|
|
811
|
+
const coreV = coreVersionSafe();
|
|
812
|
+
if (!coreV) { log(' [core-pin] cannot resolve the running core version — leaving the committed pin alone'); return { action: 'skip', reason: 'no core version' }; }
|
|
813
|
+
let pkg = null;
|
|
814
|
+
try { pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')); } catch { /* unreadable → skip below */ }
|
|
815
|
+
const pinnedDep = pkg && ((pkg.dependencies || {})['@bongos/core'] || (pkg.devDependencies || {})['@bongos/core']);
|
|
816
|
+
const plan = planCorePinRefresh({ pinnedDep, coreVersion: coreV });
|
|
817
|
+
if (plan.action === 'skip') { log(` [core-pin] ${plan.reason}`); return plan; }
|
|
818
|
+
if (plan.action === 'current') { log(` [core-pin] committed pin already matches the running core (${coreV})`); return plan; }
|
|
819
|
+
log(` [core-pin] committed pin ${safeVersionLabel(plan.pinned)} trails this control plane's core ${coreV} — refreshing so the next repo-driven deploy cannot roll the instance back (task 1003050)`);
|
|
820
|
+
|
|
821
|
+
const { tgz, packDest } = resolveVendorableCoreTarball({
|
|
822
|
+
instanceRoot: deps.instanceRoot, exec, log, coreV, tag: '[core-pin]',
|
|
823
|
+
});
|
|
824
|
+
try {
|
|
825
|
+
const targetVersion = upgrade.versionFromTgzPath(path.basename(tgz)) || coreV;
|
|
826
|
+
const vendorRel = upgrade.vendorTarball({ instanceDir: root, fromTgz: tgz, targetVersion });
|
|
827
|
+
// Prune superseded vendored tarballs (+ manifests) so the repo carries exactly ONE
|
|
828
|
+
// core. fs.rmSync, NOT exec — a filename comes off the instance's own (lower-trust)
|
|
829
|
+
// vendor/ dir, so shelling `rm -f ${f}` would be the same injection vector as the
|
|
830
|
+
// commit label (a committed file named `bongos-core-x";touch pwn;".tgz`). A direct
|
|
831
|
+
// fs call takes the name as data, never as shell (task 1003050 grade).
|
|
832
|
+
const vendorDir = path.join(root, 'vendor');
|
|
833
|
+
const keep = new Set([`${upgrade.ARTIFACT}-${targetVersion}.tgz`, `${upgrade.ARTIFACT}-${targetVersion}.manifest.json`]);
|
|
834
|
+
// vendorTarball just mkdirSync'd this, so it exists on the happy path; guard anyway
|
|
835
|
+
// so an unexpectedly-absent dir prunes nothing instead of crashing the whole provision.
|
|
836
|
+
if (fs.existsSync(vendorDir)) {
|
|
837
|
+
for (const f of fs.readdirSync(vendorDir)) {
|
|
838
|
+
if (f.startsWith(`${upgrade.ARTIFACT}-`) && !keep.has(f)) fs.rmSync(path.join(vendorDir, f), { force: true });
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
upgrade.writePin(root, vendorRel);
|
|
842
|
+
// `npm install` (NOT ci): the pin just changed, the lockfile must be regenerated.
|
|
843
|
+
const instRes = upgrade.npmInstall(root);
|
|
844
|
+
if (!instRes.ok) {
|
|
845
|
+
throw new Error(`npm install after the core-pin refresh failed (${instRes.reason || `exit ${instRes.code}`}) — lockfile not regenerated; the pin refresh cannot be committed half-done.`);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Commit + push the refresh to the instance repo — the shared deploy key cannot
|
|
849
|
+
// write a consumer's repo (task 1002317), so prefer the owner's OAuth token,
|
|
850
|
+
// exactly like the scaffold's push.
|
|
851
|
+
let ownerToken = null;
|
|
852
|
+
if (inst.owner_builder_id) {
|
|
853
|
+
try {
|
|
854
|
+
const fetchTok = deps.getGithubToken
|
|
855
|
+
|| ((bid) => require(path.join(REPO_ROOT, 'src/bongos/db')).getGithubToken({ builderId: bid, p: deps.db }));
|
|
856
|
+
const r = await fetchTok(inst.owner_builder_id);
|
|
857
|
+
ownerToken = r && r.token ? r.token : null;
|
|
858
|
+
} catch (e) { log(` [core-pin] owner OAuth token lookup failed (${e && e.message}) — pushing via the SSH origin`); }
|
|
859
|
+
}
|
|
860
|
+
const repo = parseTargetRef(inst.target_ref);
|
|
861
|
+
const credFile = ownerToken && repo ? path.join(os.tmpdir(), `bongos-gitcred-pin-${inst.slug}-${process.pid}`) : null;
|
|
862
|
+
const cmd = buildCorePinRefreshCommit({ root, fromVersion: plan.pinned, toVersion: targetVersion, repo, ownerToken, credFile });
|
|
863
|
+
try {
|
|
864
|
+
cmd.config.forEach((c) => exec(c));
|
|
865
|
+
exec(cmd.add);
|
|
866
|
+
exec(cmd.commit, { allowFail: true });
|
|
867
|
+
if (credFile) writeFile(credFile, `https://x-access-token:${ownerToken}@github.com\n`, { mode: 0o600 });
|
|
868
|
+
exec(cmd.push);
|
|
869
|
+
} finally {
|
|
870
|
+
if (credFile) exec(`rm -f ${credFile}`, { allowFail: true });
|
|
871
|
+
}
|
|
872
|
+
log(` [core-pin] refreshed + pushed: @bongos/core ${safeVersionLabel(plan.pinned)} → ${targetVersion}`);
|
|
873
|
+
return { ...plan, refreshed: true, toVersion: targetVersion };
|
|
874
|
+
} finally {
|
|
875
|
+
if (packDest) exec(`rm -rf ${packDest}`);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
733
879
|
// The per-instance systemd unit (the infra/cloudbongos.service shape): one Node process
|
|
734
880
|
// bound to the instance's port + DB + brand, secrets from its own env file. SHAPE-AWARE:
|
|
735
881
|
// a STANDALONE instance runs the CORE PACKAGE's platform-server from its OWN repo dir
|
|
@@ -1436,6 +1582,13 @@ async function provisionInstance(inst, deps) {
|
|
|
1436
1582
|
const root = standaloneRoot(inst);
|
|
1437
1583
|
log(' [code] standalone — pull instance repo + npm ci (pinned @bongos/core)');
|
|
1438
1584
|
boxExec(standalonePullCmd(), { cwd: root });
|
|
1585
|
+
// W3 (task 1003050): heal a committed `file:` pin that trails this control plane's
|
|
1586
|
+
// running core BEFORE npm ci, so a re-provision can never leave running != pinned
|
|
1587
|
+
// (the silent-rollback hazard: the next `git reset --hard` + `npm ci` deploy would
|
|
1588
|
+
// otherwise revert the instance against an already-migrated DB). Gated on apply —
|
|
1589
|
+
// the refresh writes through upgrade.js fs/npm calls, not the dry-run-safe exec.
|
|
1590
|
+
if (apply) await refreshStandaloneCorePin(inst, deps);
|
|
1591
|
+
else log(' [code] would refresh the committed @bongos/core pin if it trails the running core (task 1003050)');
|
|
1439
1592
|
boxExec(standaloneInstallCmd(), { cwd: root });
|
|
1440
1593
|
}
|
|
1441
1594
|
log(' [db] create + GDS-only migrate');
|
|
@@ -2138,6 +2291,8 @@ module.exports = {
|
|
|
2138
2291
|
standaloneRoot, standalonePullCmd, standaloneInstallCmd, standaloneMigrateCmd,
|
|
2139
2292
|
// standalone scaffold (ADR 0108 / 0125, task 2055; consumer-core fix task 1002275)
|
|
2140
2293
|
parseTargetRef, instanceRepoRemote, instanceInitSpec, installedCoreTarball, installedCorePackDir, scaffoldStandaloneRepo, coreVersionSafe,
|
|
2294
|
+
// re-provision core-pin refresh (gap-list W3, task 1003050)
|
|
2295
|
+
resolveVendorableCoreTarball, planCorePinRefresh, safeVersionLabel, buildCorePinRefreshCommit, refreshStandaloneCorePin,
|
|
2141
2296
|
// adopt vs greenfield onboarding (ADR 0121, task 1002562)
|
|
2142
2297
|
onboardMode, alreadyScaffolded,
|
|
2143
2298
|
zoneForDomain, dnsUpsertEnv, classifyZoneScope, checkDnsTokenScope,
|
package/src/module-api.js
CHANGED
|
@@ -819,8 +819,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
819
819
|
// landed since 1.19.194 with no explicit bump. run 31988381869. (task 1002620)
|
|
820
820
|
// 1.19.196 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
821
821
|
// landed since 1.19.195 with no explicit bump. run 31988890305. (task 1002620)
|
|
822
|
+
// 1.19.197 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
823
|
+
// landed since 1.19.196 with no explicit bump. run 32039532169. (task 1002620)
|
|
822
824
|
// ---------------------------------------------------------------------------
|
|
823
|
-
const CORE_VERSION = '1.19.
|
|
825
|
+
const CORE_VERSION = '1.19.197'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
824
826
|
|
|
825
827
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
826
828
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/provision.mjs
CHANGED
|
@@ -100,6 +100,72 @@ t('standalone step generators: root under standaloneBase + pull / npm-ci / insta
|
|
|
100
100
|
assert.match(P.standaloneMigrateCmd(inst), /PGDATABASE=mercury MEDUSA_GDS_ONLY=1 bash node_modules\/@bongos\/core\/scripts\/migrate\.sh/);
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
+
// ── re-provision core-pin refresh (gap-list W3, task 1003050) ─────────────────
|
|
104
|
+
t('planCorePinRefresh: a trailing file: pin → refresh; a current one → no-op', () => {
|
|
105
|
+
// The W3 shape: repo pins 1.19.2, control plane runs 1.19.196 → refresh (heals
|
|
106
|
+
// the silent-rollback hazard before the next repo-driven deploy).
|
|
107
|
+
assert.deepEqual(
|
|
108
|
+
P.planCorePinRefresh({ pinnedDep: 'file:vendor/bongos-core-1.19.2.tgz', coreVersion: '1.19.196' }),
|
|
109
|
+
{ action: 'refresh', pinned: '1.19.2' });
|
|
110
|
+
// Pin already matches the running core → nothing to commit.
|
|
111
|
+
assert.deepEqual(
|
|
112
|
+
P.planCorePinRefresh({ pinnedDep: 'file:vendor/bongos-core-1.19.196.tgz', coreVersion: '1.19.196' }),
|
|
113
|
+
{ action: 'current', pinned: '1.19.196' });
|
|
114
|
+
// A non-canonical file: pin can't be compared → refresh heals it to canonical.
|
|
115
|
+
assert.equal(
|
|
116
|
+
P.planCorePinRefresh({ pinnedDep: 'file:vendor/core.tgz', coreVersion: '1.19.196' }).action,
|
|
117
|
+
'refresh');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
t('planCorePinRefresh: registry pins and absent deps are skipped, never rewritten', () => {
|
|
121
|
+
// Registry pin (ADR 0134): npm resolves it within its range — not ours to vendor.
|
|
122
|
+
assert.equal(P.planCorePinRefresh({ pinnedDep: '^1.19.100', coreVersion: '1.19.196' }).action, 'skip');
|
|
123
|
+
assert.equal(P.planCorePinRefresh({ pinnedDep: '1.19.196', coreVersion: '1.19.196' }).action, 'skip');
|
|
124
|
+
// No @bongos/core dependency at all → not ours to invent.
|
|
125
|
+
assert.equal(P.planCorePinRefresh({ pinnedDep: undefined, coreVersion: '1.19.196' }).action, 'skip');
|
|
126
|
+
assert.equal(P.planCorePinRefresh({ pinnedDep: '', coreVersion: '1.19.196' }).action, 'skip');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
t('safeVersionLabel: passes clean semver, rejects any shell metacharacter', () => {
|
|
130
|
+
assert.equal(P.safeVersionLabel('1.19.196'), '1.19.196');
|
|
131
|
+
assert.equal(P.safeVersionLabel('1.19.2-rc.1+build'), '1.19.2-rc.1+build');
|
|
132
|
+
// The injection payloads the grader named — every one coerced away.
|
|
133
|
+
assert.equal(P.safeVersionLabel('1.0.0";touch /tmp/pwn;echo"'), '(unversioned)');
|
|
134
|
+
assert.equal(P.safeVersionLabel('$(whoami)'), '(unversioned)');
|
|
135
|
+
assert.equal(P.safeVersionLabel('1.0.0`id`'), '(unversioned)');
|
|
136
|
+
assert.equal(P.safeVersionLabel(''), '(unversioned)');
|
|
137
|
+
assert.equal(P.safeVersionLabel(null), '(unversioned)');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
t('buildCorePinRefreshCommit (the commit step): git sequence, token-URL push, and the injection block', () => {
|
|
141
|
+
const repo = { owner: 'jaxri', name: 'spike-harbor' };
|
|
142
|
+
// Token present → push over the owner's OAuth credential URL (task 1002317).
|
|
143
|
+
const withTok = P.buildCorePinRefreshCommit({
|
|
144
|
+
root: '/srv/bongos/spike-harbor', fromVersion: '1.19.2', toVersion: '1.19.196',
|
|
145
|
+
repo, ownerToken: 'ghs_xxx', credFile: '/tmp/cred',
|
|
146
|
+
});
|
|
147
|
+
assert.deepEqual(withTok.config, [
|
|
148
|
+
'git -C /srv/bongos/spike-harbor config user.name "Cloud Bongos"',
|
|
149
|
+
'git -C /srv/bongos/spike-harbor config user.email "bot@cloudbongos.com"',
|
|
150
|
+
]);
|
|
151
|
+
assert.equal(withTok.add, 'git -C /srv/bongos/spike-harbor add -A');
|
|
152
|
+
assert.match(withTok.commit, /@bongos\/core 1\.19\.2 -> 1\.19\.196 \(task 1003050\)/);
|
|
153
|
+
assert.match(withTok.push, /store --file=\/tmp\/cred.*x-access-token@github\.com\/jaxri\/spike-harbor\.git HEAD:main/);
|
|
154
|
+
|
|
155
|
+
// No token → SSH origin fallback (never a token URL).
|
|
156
|
+
const noTok = P.buildCorePinRefreshCommit({ root: '/srv/bongos/spike-harbor', fromVersion: '1.19.2', toVersion: '1.19.196', repo });
|
|
157
|
+
assert.equal(noTok.push, 'git -C /srv/bongos/spike-harbor push origin main');
|
|
158
|
+
|
|
159
|
+
// A malicious `file:` pin in the instance repo cannot break out of the -m argument:
|
|
160
|
+
// the coerced label carries no quote/`;`/`$()` — the commit line stays one argument.
|
|
161
|
+
const evil = P.buildCorePinRefreshCommit({
|
|
162
|
+
root: '/srv/bongos/spike-harbor', fromVersion: '1.0.0";touch /tmp/pwn;echo"', toVersion: '1.19.196',
|
|
163
|
+
repo, ownerToken: 'ghs_xxx', credFile: '/tmp/cred',
|
|
164
|
+
});
|
|
165
|
+
assert.ok(!evil.commit.includes('touch /tmp/pwn'), 'injection payload must not survive into the commit command');
|
|
166
|
+
assert.match(evil.commit, /@bongos\/core \(unversioned\) -> 1\.19\.196/);
|
|
167
|
+
});
|
|
168
|
+
|
|
103
169
|
// ── nightly DB backup units (task 2213 — productizes the manual 2210 fix) ──────
|
|
104
170
|
t('backup units: per-instance names + the DB set on the unit (fixes the task-2210 unset GDS_BACKUP_DB)', () => {
|
|
105
171
|
assert.equal(P.backupUnitName(coTenant), 'mercury-db-backup');
|