@bongos/core 1.19.195 → 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 +22 -17
- package/docs/adr/0133-github-app-manifest-onboarding-signin.md +3 -1
- package/docs/recipes/ops-gotchas.md +7 -0
- package/modules/government/catalog.js +16 -0
- package/modules/government/migrations/government_004_board_permissions.sql +87 -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 +5 -1
- package/tests/provision.mjs +66 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
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,
|
|
13
13
|
"agent_docs_stubbed": 21,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1285,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1615,
|
|
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",
|
|
@@ -2892,7 +2892,7 @@
|
|
|
2892
2892
|
{
|
|
2893
2893
|
"path": "modules/government/catalog.js",
|
|
2894
2894
|
"mode": "0000644",
|
|
2895
|
-
"sha256": "
|
|
2895
|
+
"sha256": "ed853e83a9765e0a5de90195a15dea6b0bf1f6d07be0b6ad740d54a7c79c2c7d"
|
|
2896
2896
|
},
|
|
2897
2897
|
{
|
|
2898
2898
|
"path": "modules/government/config.js",
|
|
@@ -2954,6 +2954,11 @@
|
|
|
2954
2954
|
"mode": "0000644",
|
|
2955
2955
|
"sha256": "e4802709d31031f2a7b66e1c3a07fd71def94b2eaf4eff7ad8a0f22bf6eeaf5e"
|
|
2956
2956
|
},
|
|
2957
|
+
{
|
|
2958
|
+
"path": "modules/government/migrations/government_004_board_permissions.sql",
|
|
2959
|
+
"mode": "0000644",
|
|
2960
|
+
"sha256": "75913af702d6529c9a5cf2e2c42d9c329eea92ae99274fb71f2524520de4cfbf"
|
|
2961
|
+
},
|
|
2957
2962
|
{
|
|
2958
2963
|
"path": "modules/government/module.json",
|
|
2959
2964
|
"mode": "0000644",
|
|
@@ -3962,7 +3967,7 @@
|
|
|
3962
3967
|
{
|
|
3963
3968
|
"path": "modules/provisioning/provisioning.js",
|
|
3964
3969
|
"mode": "0000644",
|
|
3965
|
-
"sha256": "
|
|
3970
|
+
"sha256": "eebcba4b333030f17c06bc6e1423450ae4af124e4075af52cbd512e80d5ba95e"
|
|
3966
3971
|
},
|
|
3967
3972
|
{
|
|
3968
3973
|
"path": "modules/provisioning/rate-limit.js",
|
|
@@ -3982,7 +3987,7 @@
|
|
|
3982
3987
|
{
|
|
3983
3988
|
"path": "modules/provisioning/tests/provisioning.mjs",
|
|
3984
3989
|
"mode": "0000644",
|
|
3985
|
-
"sha256": "
|
|
3990
|
+
"sha256": "98787fad5df787a8ebdbf90fb8bf21b68f1529387b15d65724fdafc8ac0b91d0"
|
|
3986
3991
|
},
|
|
3987
3992
|
{
|
|
3988
3993
|
"path": "modules/provisioning/ui/.gitkeep",
|
|
@@ -4267,12 +4272,12 @@
|
|
|
4267
4272
|
{
|
|
4268
4273
|
"path": "package-lock.json",
|
|
4269
4274
|
"mode": "0000644",
|
|
4270
|
-
"sha256": "
|
|
4275
|
+
"sha256": "69be9b66c6af5ccad87644d9a2460f8a6c59908e42613104b612654c41fa67ba"
|
|
4271
4276
|
},
|
|
4272
4277
|
{
|
|
4273
4278
|
"path": "package.json",
|
|
4274
4279
|
"mode": "0000644",
|
|
4275
|
-
"sha256": "
|
|
4280
|
+
"sha256": "0b113ba37f114ec82e9f2dafa7e9d5cca35886e157934d7475850eb4f9dd66e1"
|
|
4276
4281
|
},
|
|
4277
4282
|
{
|
|
4278
4283
|
"path": "public-docs/index.html",
|
|
@@ -5187,7 +5192,7 @@
|
|
|
5187
5192
|
{
|
|
5188
5193
|
"path": "scripts/gds/provision.js",
|
|
5189
5194
|
"mode": "0000644",
|
|
5190
|
-
"sha256": "
|
|
5195
|
+
"sha256": "1c348e8859004c69e66b8eede56da0bab68ec4d473790f1c1f9bf1769934211f"
|
|
5191
5196
|
},
|
|
5192
5197
|
{
|
|
5193
5198
|
"path": "scripts/gds/publish-credential-check.js",
|
|
@@ -5882,7 +5887,7 @@
|
|
|
5882
5887
|
{
|
|
5883
5888
|
"path": "src/module-api.js",
|
|
5884
5889
|
"mode": "0000644",
|
|
5885
|
-
"sha256": "
|
|
5890
|
+
"sha256": "bdfd06f58403f6cb568ed65718a92450c7e41470a6eae1c2583101e9e47e6656"
|
|
5886
5891
|
},
|
|
5887
5892
|
{
|
|
5888
5893
|
"path": "src/module-loader/catalog.js",
|
|
@@ -7502,7 +7507,7 @@
|
|
|
7502
7507
|
{
|
|
7503
7508
|
"path": "tests/provision.mjs",
|
|
7504
7509
|
"mode": "0000644",
|
|
7505
|
-
"sha256": "
|
|
7510
|
+
"sha256": "9e8da984a2a9378809f03cc3f5f1c2cda34f8589ad954db1acd5b12bf424f322"
|
|
7506
7511
|
},
|
|
7507
7512
|
{
|
|
7508
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
|
|
@@ -198,6 +198,22 @@ const PERMISSIONS = [
|
|
|
198
198
|
{ key: 'override_request.decide', system: false, floor: 'metic', guards: 'GET /override-requests, POST .../:id/decide (ADR 0157, was archon)' },
|
|
199
199
|
{ key: 'claim.release.any', system: false, floor: 'metic', scope: 'any', resource: 'claim', guards: 'POST /claims/:id/release-on-behalf (ADR 0157, was archon)' },
|
|
200
200
|
{ key: 'idea.ratify', system: false, floor: 'metic', guards: 'POST /inbox/:id/ratify (ADR 0157, was archon)' },
|
|
201
|
+
// ── the Board Room (ADR 0175, goal 1000069) ──────────────────────────────────
|
|
202
|
+
// Declared BEFORE their routes exist (R07/R08 add them), and deliberately so:
|
|
203
|
+
// for a NEW key the unsafe order is code-first. A grant row naming a key no
|
|
204
|
+
// route reads is INERT; a route reading a key with no grant rows 403s everyone.
|
|
205
|
+
// ADR 0174's Consequences record that failure landing on the Government tab
|
|
206
|
+
// itself. So grants lead, routes follow — inside this goal, not across releases.
|
|
207
|
+
{ key: 'board.item.open', system: false, floor: 'metic', guards: 'POST /board/items — put a subject to the board (ADR 0175; a Full Idea window opens automatically on grade pass, so in practice this guards an amendment)' },
|
|
208
|
+
// floor 'xenos' is NOT a mistake and must not be "tightened". Who may vote is
|
|
209
|
+
// the CONSTITUTION's membership predicate, resolved per request from config
|
|
210
|
+
// (ADR 0175 §6) — today `rank:archon`. Encoding that rank here too would fork
|
|
211
|
+
// authority truth into a second place and, worse, would make widening the
|
|
212
|
+
// franchise a MIGRATION rather than the config change ADR 0175 §6 promises it
|
|
213
|
+
// is. So the atom says only "this route exists for authenticated builders" and
|
|
214
|
+
// the predicate does the real gating — the `criterion.create` pattern above,
|
|
215
|
+
// where the floor is real but explicitly not the whole gate.
|
|
216
|
+
{ key: 'board.vote.cast', system: false, floor: 'xenos', guards: 'POST /board/items/:id/vote — floor is the coarse gate; board membership is the configured predicate (ADR 0175 §6), checked in-handler' },
|
|
201
217
|
{ key: 'task.newcomer_restock', system: false, floor: 'metic', guards: 'POST /tasks/newcomer-restock (ADR 0157, was archon)' },
|
|
202
218
|
{ key: 'task.peer_votes.tally', system: false, floor: 'metic', guards: 'POST /tasks/peer-votes/tally (ADR 0157, was archon)' },
|
|
203
219
|
{ key: 'goal.reorder', system: false, floor: 'metic', guards: 'POST /goals/reorder (ADR 0157, was archon)' },
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
-- government_004_board_permissions.sql — grant the two Board Room atoms
|
|
2
|
+
-- (task 1003017 / R04 of goal 1000069). Decision of record: ADR 0175.
|
|
3
|
+
--
|
|
4
|
+
-- catalog.js now carries `board.item.open` (floor 'metic') and `board.vote.cast`
|
|
5
|
+
-- (floor 'xenos'). Both are operational (system:false) — creating an item or
|
|
6
|
+
-- casting a vote confers authority on nobody, which is the same argument the
|
|
7
|
+
-- `version.create` row makes against a symmetry-with-`version.close` reading.
|
|
8
|
+
--
|
|
9
|
+
-- ── THIS MIGRATION IS LOAD-BEARING, not bookkeeping ─────────────────────────
|
|
10
|
+
-- The compat seed (governance_002) is run-once, so a database that has already
|
|
11
|
+
-- migrated would never learn a key added only in code: no rank would hold these,
|
|
12
|
+
-- and the routes R07/R08 add would 403 every builder including the Archon. The
|
|
13
|
+
-- grant has to arrive as its own migration for the change to be behaviour-
|
|
14
|
+
-- identical in practice and not just on a fresh install. `governance_006` is the
|
|
15
|
+
-- worked example this copies.
|
|
16
|
+
--
|
|
17
|
+
-- ── WHY THE GRANTS LAND BEFORE THE ROUTES ───────────────────────────────────
|
|
18
|
+
-- The consuming routes do not exist yet (R07 opens a window, R08 casts a vote).
|
|
19
|
+
-- That order is deliberate, and it is the safe one: a grant row naming a key no
|
|
20
|
+
-- route reads is INERT, while a route reading a key with no grant rows fails
|
|
21
|
+
-- closed for everyone. ADR 0174's Consequences record what the inverse costs —
|
|
22
|
+
-- the key that gates the Government tab failing closed on the very surface an
|
|
23
|
+
-- owner would use to investigate. So grants lead and routes follow, inside this
|
|
24
|
+
-- goal rather than across releases.
|
|
25
|
+
--
|
|
26
|
+
-- ── WHY `board.vote.cast` GOES TO EVERY RANK, INCLUDING xenos ───────────────
|
|
27
|
+
-- This is the row most likely to be "corrected" by a later reader, so: it is
|
|
28
|
+
-- correct. Who may vote is the CONSTITUTION's membership predicate, resolved per
|
|
29
|
+
-- request from config (ADR 0175 §6) — today `rank:archon`. Encoding that rank
|
|
30
|
+
-- here as well would fork authority truth into a second place, and would make
|
|
31
|
+
-- widening the franchise a MIGRATION rather than the config change ADR 0175 §6
|
|
32
|
+
-- promises. The atom therefore says only "this route exists for authenticated
|
|
33
|
+
-- builders"; the predicate does the real gating, in-handler. This is exactly the
|
|
34
|
+
-- shape `criterion.create` already uses, where the floor is real but explicitly
|
|
35
|
+
-- not the whole gate.
|
|
36
|
+
--
|
|
37
|
+
-- ── NOT GRANTED: there is no `board.config.manage` ──────────────────────────
|
|
38
|
+
-- The planning outline named a third key for changing the constitution. It is
|
|
39
|
+
-- deliberately absent, because the constitution is FILE config: config.js reads
|
|
40
|
+
-- config/government.json and config/government.neutral.json with readFileSync
|
|
41
|
+
-- and there is no write route — every write route in this module edits DB rows
|
|
42
|
+
-- (ranks, grants, assignments), never config. A key no route consumes does not
|
|
43
|
+
-- exist (catalog.js's first rule), so minting one would have made the catalog
|
|
44
|
+
-- describe authority nothing can exercise. Amending the constitution stays what
|
|
45
|
+
-- ADR 0175 §7 describes: the board RATIFIES, and the file edit + merge executes
|
|
46
|
+
-- — which is also ADR 0162's "advisory, never self-executing" holding.
|
|
47
|
+
--
|
|
48
|
+
-- ── ONE BLOCK PER RANK, and the form is load-bearing ────────────────────────
|
|
49
|
+
-- The drift guard (tests/government_seed.mjs) scans for the literal
|
|
50
|
+
-- `SELECT '<rank>', unnest(ARRAY[…])`, so a combined
|
|
51
|
+
-- `unnest(ARRAY['metic','archon'])` form parses as ZERO grants and reds the
|
|
52
|
+
-- build. Four blocks because the seed is a cumulative superset: a 'xenos'-floored
|
|
53
|
+
-- key is held by every rank, a 'metic'-floored key by metic and archon.
|
|
54
|
+
--
|
|
55
|
+
-- Additive only: two new permission keys granted; nothing is revoked (the union
|
|
56
|
+
-- model in tests/government_seed.mjs cannot express a revoke). Idempotent
|
|
57
|
+
-- (ON CONFLICT DO NOTHING); forward-safe (INSERT only).
|
|
58
|
+
|
|
59
|
+
BEGIN;
|
|
60
|
+
|
|
61
|
+
INSERT INTO government_rank_permissions (rank_key, permission_key)
|
|
62
|
+
SELECT 'xenos', unnest(ARRAY[
|
|
63
|
+
'board.vote.cast'
|
|
64
|
+
])
|
|
65
|
+
ON CONFLICT (rank_key, permission_key) DO NOTHING;
|
|
66
|
+
|
|
67
|
+
INSERT INTO government_rank_permissions (rank_key, permission_key)
|
|
68
|
+
SELECT 'thetes', unnest(ARRAY[
|
|
69
|
+
'board.vote.cast'
|
|
70
|
+
])
|
|
71
|
+
ON CONFLICT (rank_key, permission_key) DO NOTHING;
|
|
72
|
+
|
|
73
|
+
INSERT INTO government_rank_permissions (rank_key, permission_key)
|
|
74
|
+
SELECT 'metic', unnest(ARRAY[
|
|
75
|
+
'board.vote.cast',
|
|
76
|
+
'board.item.open'
|
|
77
|
+
])
|
|
78
|
+
ON CONFLICT (rank_key, permission_key) DO NOTHING;
|
|
79
|
+
|
|
80
|
+
INSERT INTO government_rank_permissions (rank_key, permission_key)
|
|
81
|
+
SELECT 'archon', unnest(ARRAY[
|
|
82
|
+
'board.vote.cast',
|
|
83
|
+
'board.item.open'
|
|
84
|
+
])
|
|
85
|
+
ON CONFLICT (rank_key, permission_key) DO NOTHING;
|
|
86
|
+
|
|
87
|
+
COMMIT;
|
|
@@ -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
|
@@ -817,8 +817,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
817
817
|
// landed since 1.19.193 with no explicit bump. run 31987857242. (task 1002620)
|
|
818
818
|
// 1.19.195 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
819
819
|
// landed since 1.19.194 with no explicit bump. run 31988381869. (task 1002620)
|
|
820
|
+
// 1.19.196 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
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)
|
|
820
824
|
// ---------------------------------------------------------------------------
|
|
821
|
-
const CORE_VERSION = '1.19.
|
|
825
|
+
const CORE_VERSION = '1.19.197'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
822
826
|
|
|
823
827
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
824
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');
|