@bongos/core 1.19.129 → 1.19.131
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 +21 -16
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/mirror-lineage-check.js +41 -26
- package/scripts/gds/run-unit-tests.js +1 -1
- package/scripts/gds/ship.js +33 -1
- package/src/module-api.js +5 -1
- package/tests/mirror_lineage_fulltree.mjs +28 -0
- package/tests/mirror_lineage_proof.mjs +56 -2
- package/tests/ship_publish_error.mjs +119 -0
- package/tests/subdomain_auth.mjs +44 -12
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.131",
|
|
6
|
+
"core_contract": "1.19.131",
|
|
7
|
+
"source_commit": "7fc91a6722c91cd40c09ce632c2a765377952e49",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-15T03:23:58.787Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 303,
|
|
13
13
|
"agent_docs_stubbed": 21,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1247,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1571,
|
|
20
|
+
"tree_sha256": "81215c25cbaed99553dba15247df2e2f9daf37245d8144df0a5e32b507b4c0b9",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -4172,12 +4172,12 @@
|
|
|
4172
4172
|
{
|
|
4173
4173
|
"path": "package-lock.json",
|
|
4174
4174
|
"mode": "0000644",
|
|
4175
|
-
"sha256": "
|
|
4175
|
+
"sha256": "ad651e607ce3937ca8522552d21ecfbaa03108734f9c7987c79a2e8ef8a7c7f3"
|
|
4176
4176
|
},
|
|
4177
4177
|
{
|
|
4178
4178
|
"path": "package.json",
|
|
4179
4179
|
"mode": "0000644",
|
|
4180
|
-
"sha256": "
|
|
4180
|
+
"sha256": "178ddf3d5958c428d370e3381cdca505455abe6006b9f920245de0062bc2d5aa"
|
|
4181
4181
|
},
|
|
4182
4182
|
{
|
|
4183
4183
|
"path": "public-docs/index.html",
|
|
@@ -4992,7 +4992,7 @@
|
|
|
4992
4992
|
{
|
|
4993
4993
|
"path": "scripts/gds/mirror-lineage-check.js",
|
|
4994
4994
|
"mode": "0000644",
|
|
4995
|
-
"sha256": "
|
|
4995
|
+
"sha256": "31296eda794e640ce5f7191d166d08710b35290be67878826a2c7209f0216e54"
|
|
4996
4996
|
},
|
|
4997
4997
|
{
|
|
4998
4998
|
"path": "scripts/gds/mirror-redact.js",
|
|
@@ -5157,7 +5157,7 @@
|
|
|
5157
5157
|
{
|
|
5158
5158
|
"path": "scripts/gds/run-unit-tests.js",
|
|
5159
5159
|
"mode": "0000644",
|
|
5160
|
-
"sha256": "
|
|
5160
|
+
"sha256": "4c3bef6907b4807d93426e754ff20c75e56b498e4302c1cd47e4b3953bd8b4d0"
|
|
5161
5161
|
},
|
|
5162
5162
|
{
|
|
5163
5163
|
"path": "scripts/gds/sandbox-stage.js",
|
|
@@ -5322,7 +5322,7 @@
|
|
|
5322
5322
|
{
|
|
5323
5323
|
"path": "scripts/gds/ship.js",
|
|
5324
5324
|
"mode": "0000644",
|
|
5325
|
-
"sha256": "
|
|
5325
|
+
"sha256": "681de343f8491a2f22165246f21ac371a8f76411b395991c784635e8e9646ed7"
|
|
5326
5326
|
},
|
|
5327
5327
|
{
|
|
5328
5328
|
"path": "scripts/gds/smoke-batch-claim.sh",
|
|
@@ -5762,7 +5762,7 @@
|
|
|
5762
5762
|
{
|
|
5763
5763
|
"path": "src/module-api.js",
|
|
5764
5764
|
"mode": "0000644",
|
|
5765
|
-
"sha256": "
|
|
5765
|
+
"sha256": "fd76c822f4e795248c2c010052dab9cce8cc237a5f501fbf90e66c413c58eac6"
|
|
5766
5766
|
},
|
|
5767
5767
|
{
|
|
5768
5768
|
"path": "src/module-loader/catalog.js",
|
|
@@ -7052,12 +7052,12 @@
|
|
|
7052
7052
|
{
|
|
7053
7053
|
"path": "tests/mirror_lineage_fulltree.mjs",
|
|
7054
7054
|
"mode": "0000644",
|
|
7055
|
-
"sha256": "
|
|
7055
|
+
"sha256": "0482cd900a7c45a18ece314deb977eef852060b50d56b814d58ac631bc568426"
|
|
7056
7056
|
},
|
|
7057
7057
|
{
|
|
7058
7058
|
"path": "tests/mirror_lineage_proof.mjs",
|
|
7059
7059
|
"mode": "0000644",
|
|
7060
|
-
"sha256": "
|
|
7060
|
+
"sha256": "74301070c93c5cb5b4f31d32e508336c96ba199064db9fdf810f07f8d431a98d"
|
|
7061
7061
|
},
|
|
7062
7062
|
{
|
|
7063
7063
|
"path": "tests/mirror_redact.mjs",
|
|
@@ -7654,6 +7654,11 @@
|
|
|
7654
7654
|
"mode": "0000644",
|
|
7655
7655
|
"sha256": "23731936e26feb6406333f484c86210be2f2eaee1b4060ba34c9e03f8fd09685"
|
|
7656
7656
|
},
|
|
7657
|
+
{
|
|
7658
|
+
"path": "tests/ship_publish_error.mjs",
|
|
7659
|
+
"mode": "0000644",
|
|
7660
|
+
"sha256": "eb8c52362355479a50c83ba84968d605b93f98e93513cf8436dd096e6514f6e8"
|
|
7661
|
+
},
|
|
7657
7662
|
{
|
|
7658
7663
|
"path": "tests/ship_resume.mjs",
|
|
7659
7664
|
"mode": "0000644",
|
|
@@ -7767,7 +7772,7 @@
|
|
|
7767
7772
|
{
|
|
7768
7773
|
"path": "tests/subdomain_auth.mjs",
|
|
7769
7774
|
"mode": "0000644",
|
|
7770
|
-
"sha256": "
|
|
7775
|
+
"sha256": "24257ca4ad1a7bcefb735ee1b02609469035730e0723645f71e10eb71525bb72"
|
|
7771
7776
|
},
|
|
7772
7777
|
{
|
|
7773
7778
|
"path": "tests/task_classifier_vocab.mjs",
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.131",
|
|
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.131",
|
|
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.131",
|
|
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",
|
|
@@ -13,10 +13,11 @@
|
|
|
13
13
|
// export does (mirror-redact + secret-scrub, via the SHARED redactBlob) and
|
|
14
14
|
// assert ZERO residual instance literals, public IPs, or secrets survive.
|
|
15
15
|
// Same detector the export gates on — "one detector, two consumers."
|
|
16
|
-
// 2. MIGRATION NUMBERING — the set of core
|
|
17
|
-
// reach the mirror is IDENTICAL to the
|
|
18
|
-
// a file), so the claim-time next-number
|
|
19
|
-
// migration on both sides. A dropped
|
|
16
|
+
// 2. MIGRATION NUMBERING — the set of core migration numbers (bare NNN_*.sql and
|
|
17
|
+
// namespaced core_NNN_*.sql alike) that reach the mirror is IDENTICAL to the
|
|
18
|
+
// private set (redaction never renames a file), so the claim-time next-number
|
|
19
|
+
// allocator computes the SAME next migration on both sides. A dropped or
|
|
20
|
+
// renumbered migration = divergence = fail.
|
|
20
21
|
// 3. ENUM VALUES — for every publishable migration, the CHECK (... IN (...))
|
|
21
22
|
// value-lists are byte-identical before and after redaction. Redaction may
|
|
22
23
|
// touch a domain in a comment; it must NEVER alter a schema enum value.
|
|
@@ -46,6 +47,7 @@ const {
|
|
|
46
47
|
const { isBinary } = require(path.join(REPO_ROOT, 'scripts/public-mirror-export.js'));
|
|
47
48
|
const { scrubSecrets, collectEnvSecrets, SECRET_PATTERNS } = require(path.join(REPO_ROOT, 'modules/security/secret-scrub.js'));
|
|
48
49
|
const { nextFree } = require(path.join(REPO_ROOT, 'scripts/gds/next-number.js'));
|
|
50
|
+
const { migrationNumberOf } = require(path.join(REPO_ROOT, 'scripts/gds/migration-namespace.js'));
|
|
49
51
|
|
|
50
52
|
// Known secret SHAPES as NON-GLOBAL clones. We scan each file with .test(), and a
|
|
51
53
|
// `/g` regex carries lastIndex state across .test() calls — reused in a loop over
|
|
@@ -55,30 +57,41 @@ const { nextFree } = require(path.join(REPO_ROOT, 'scripts/gds/next-number.js'))
|
|
|
55
57
|
// deterministic regardless of file order.
|
|
56
58
|
const KNOWN_SECRET_SHAPES = SECRET_PATTERNS.map((re) => new RegExp(re.source, re.flags.replace('g', '')));
|
|
57
59
|
|
|
58
|
-
// Core migrations are migrations/NNN_slug.sql (3+ digit prefix) DIRECTLY under
|
|
59
|
-
// migrations/. Module migrations live under modules/<key>/migrations/ and instance
|
|
60
|
-
// SEEDS under migrations/instance/ (ADR 0105) — both numbered independently and
|
|
61
|
-
// excluded from the publishable mirror; the claim-time allocator only reads the
|
|
62
|
-
// top-level core dir, so only core numbering can diverge.
|
|
63
|
-
const MIGRATION_BASENAME_RE = /^(\d{3,})_.*\.sql$/;
|
|
64
|
-
|
|
65
60
|
// ---- pure helpers (exported for tests) ------------------------------------
|
|
66
61
|
|
|
62
|
+
// The CORE migration number for a repo-relative path, or null if it is not one.
|
|
63
|
+
//
|
|
64
|
+
// Core migrations sit DIRECTLY under migrations/ as either the legacy bare NNN_slug.sql
|
|
65
|
+
// or the namespaced core_NNN_slug.sql every NEW core migration must use (ADR 0108 §5).
|
|
66
|
+
// The prefix namespaces the FILENAME (so a core stem can't collide with an instance/
|
|
67
|
+
// module one across repos), NOT the number line — core_188 continues the bare 001–187
|
|
68
|
+
// sequence as ONE sequence. Hence the shared migrationNumberOf, the SAME parser the
|
|
69
|
+
// claim-time allocator (next-number.js) counts from: this proof and the allocator can
|
|
70
|
+
// never disagree about which numbers are taken. (Task 1002527 — the bare-regex version
|
|
71
|
+
// was blind to core_188+, leaving BOTH halves equally blind and the gate falsely green.)
|
|
72
|
+
//
|
|
73
|
+
// Module migrations (modules/<key>/migrations/) and instance SEEDS
|
|
74
|
+
// (migrations/instance/, ADR 0105) are numbered independently and excluded from the
|
|
75
|
+
// publishable mirror; the allocator only reads the top-level core dir, so only core
|
|
76
|
+
// numbering can diverge.
|
|
77
|
+
function coreMigrationNumber(repoRelPath) {
|
|
78
|
+
const s = String(repoRelPath);
|
|
79
|
+
if (!s.startsWith('migrations/')) return null; // the migrations dir …
|
|
80
|
+
// … and ONLY its top level. Counting migrations/instance/NNN_*.sql put every instance
|
|
81
|
+
// seed in the PRIVATE set but not the MIRROR set (they are unpublishable, task 2140) —
|
|
82
|
+
// a false private⇄mirror divergence that red-lined the unit gate (task 2152). A path
|
|
83
|
+
// with a further slash is a subdir, never a core migration.
|
|
84
|
+
const rest = s.slice('migrations/'.length);
|
|
85
|
+
if (rest.includes('/')) return null;
|
|
86
|
+
return migrationNumberOf(rest);
|
|
87
|
+
}
|
|
88
|
+
|
|
67
89
|
// The set of CORE migration numbers among a list of repo-relative paths.
|
|
68
90
|
function migrationNumbersFromPaths(paths) {
|
|
69
91
|
const out = new Set();
|
|
70
92
|
for (const p of paths || []) {
|
|
71
|
-
const
|
|
72
|
-
if (
|
|
73
|
-
// … and ONLY its top level. migrations/instance/NNN_*.sql are this instance's host
|
|
74
|
-
// seeds (ADR 0105), excluded from the publishable mirror (task 2140) — counting them
|
|
75
|
-
// here put every instance seed in the PRIVATE set but not the MIRROR set, a false
|
|
76
|
-
// private⇄mirror divergence that red-lined the unit gate (task 2152). A path with a
|
|
77
|
-
// further slash is a subdir, never a core migration.
|
|
78
|
-
const rest = s.slice('migrations/'.length);
|
|
79
|
-
if (rest.includes('/')) continue;
|
|
80
|
-
const m = MIGRATION_BASENAME_RE.exec(rest);
|
|
81
|
-
if (m) out.add(parseInt(m[1], 10));
|
|
93
|
+
const n = coreMigrationNumber(p);
|
|
94
|
+
if (n != null) out.add(n);
|
|
82
95
|
}
|
|
83
96
|
return out;
|
|
84
97
|
}
|
|
@@ -178,10 +191,12 @@ function proveLineage({ publishableFiles, privateMigrationNumbers, ctx }) {
|
|
|
178
191
|
for (const f of publishableFiles) {
|
|
179
192
|
const { redactedText, leaks: fileLeaks } = scanFile(f.buf, ctx);
|
|
180
193
|
for (const l of fileLeaks) leaks.push({ path: f.path, label: l.label, sample: l.sample });
|
|
181
|
-
|
|
194
|
+
// Same predicate as the private half (migrationNumbersFromPaths) — the two sets are
|
|
195
|
+
// only comparable if both are built by the identical parser.
|
|
196
|
+
const migNum = coreMigrationNumber(f.path);
|
|
197
|
+
if (migNum != null) {
|
|
182
198
|
migFiles.push({ path: f.path, original: f.buf.toString('utf8'), redacted: redactedText != null ? redactedText : f.buf.toString('utf8') });
|
|
183
|
-
|
|
184
|
-
mirrorNums.add(parseInt(mm[1], 10));
|
|
199
|
+
mirrorNums.add(migNum);
|
|
185
200
|
}
|
|
186
201
|
}
|
|
187
202
|
|
|
@@ -317,7 +332,7 @@ function main() {
|
|
|
317
332
|
if (require.main === module) main();
|
|
318
333
|
|
|
319
334
|
module.exports = {
|
|
320
|
-
|
|
335
|
+
coreMigrationNumber,
|
|
321
336
|
migrationNumbersFromPaths,
|
|
322
337
|
parseEnumChecks,
|
|
323
338
|
numberingDivergence,
|
|
@@ -77,7 +77,7 @@ const INTEGRATION = new Set([
|
|
|
77
77
|
'governance_fold_bench', 'governance_live_proof', 'linkify_refs', 'memory_isolation',
|
|
78
78
|
'memory_pull', 'needs_rebase', 'parallel-load', 'reward_mode_cost_plus_only',
|
|
79
79
|
'session_records', 'setup_smoke',
|
|
80
|
-
'status-probe',
|
|
80
|
+
'status-probe',
|
|
81
81
|
]);
|
|
82
82
|
|
|
83
83
|
// Excluded so this gate is trustworthy in a CLEAN checkout (the CI condition):
|
package/scripts/gds/ship.js
CHANGED
|
@@ -1213,6 +1213,34 @@ async function ciLand(taskId, branch, valueSummary) {
|
|
|
1213
1213
|
return await ciLandLocal(taskId, branch, valueSummary);
|
|
1214
1214
|
}
|
|
1215
1215
|
|
|
1216
|
+
// task 1001916: render a FAILED POST /tasks/:id/publish-branch for the builder.
|
|
1217
|
+
// The server has carried { step, reason } — which git step failed + the
|
|
1218
|
+
// credential-scrubbed stderr — since task 1547, but the bail site printed only
|
|
1219
|
+
// `data.message || data.error`: "[object Object]" on the ADR 0116 envelope, a
|
|
1220
|
+
// bare `publish_failed` on the legacy flat shape. That hid a non-fast-forward
|
|
1221
|
+
// rejection (the server had already freshened the branch itself) across several
|
|
1222
|
+
// blind retry laps, so that case also gets its specific remedy. Pure + exported
|
|
1223
|
+
// for tests/ship_publish_error.mjs (no git, no network).
|
|
1224
|
+
const NON_FAST_FORWARD_RE = /fetch first|non-fast-forward/i;
|
|
1225
|
+
|
|
1226
|
+
function formatPublishFailure(data, status, branch) {
|
|
1227
|
+
const { code, message } = normalizeApiError(data);
|
|
1228
|
+
const envelopeDetails = (data && data.error && typeof data.error === 'object' && data.error.details) || null;
|
|
1229
|
+
const details = envelopeDetails || data || {};
|
|
1230
|
+
const label = message || code || (data && data.message) || `HTTP ${status}`;
|
|
1231
|
+
// git stderr is multi-line; this is embedded mid-sentence in one log line.
|
|
1232
|
+
const reason = details.reason ? String(details.reason).replace(/\s+/g, ' ').trim() : '';
|
|
1233
|
+
const parts = [];
|
|
1234
|
+
if (details.step) parts.push(`step: ${details.step}`);
|
|
1235
|
+
if (reason) parts.push(`reason: ${reason}`);
|
|
1236
|
+
let out = parts.length ? `${label} (${parts.join('; ')})` : label;
|
|
1237
|
+
if (NON_FAST_FORWARD_RE.test(reason)) {
|
|
1238
|
+
out += ` — the server already moved ${branch}: fetch origin, reset onto origin/${branch}, `
|
|
1239
|
+
+ 're-run the generators, and re-ship so the push fast-forwards.';
|
|
1240
|
+
}
|
|
1241
|
+
return out;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1216
1244
|
// ciLandServer — the dev-box path: no GitHub push credential on this machine, so
|
|
1217
1245
|
// hand the branch to the GDS, which pushes it + opens the PR + auto-merges with
|
|
1218
1246
|
// the server-side credential. Requires a RE-AUTHED (non-box) cli session — a
|
|
@@ -1249,7 +1277,7 @@ async function ciLandServer(taskId, branch, valueSummary) {
|
|
|
1249
1277
|
return landBailed('server-mediated push is not configured (operator action)', strandCommand(taskId, 'server'));
|
|
1250
1278
|
}
|
|
1251
1279
|
if (!r.ok) {
|
|
1252
|
-
const e = (r.data
|
|
1280
|
+
const e = formatPublishFailure(r.data, r.status, branch);
|
|
1253
1281
|
console.log(` ci-land: server publish failed — ${e}. Task stays at confirmed. ${strandNextStep(taskId, 'server')}`);
|
|
1254
1282
|
return landBailed(`server publish failed — ${e}`, strandCommand(taskId, 'server'));
|
|
1255
1283
|
}
|
|
@@ -4472,6 +4500,10 @@ module.exports = {
|
|
|
4472
4500
|
// object, so a consumer MUST test `.ok`; the old `if (!merged)` would sail past
|
|
4473
4501
|
// a bail. Also pins pending (a poll timeout) as distinct from bailed.
|
|
4474
4502
|
landed, landPending, landBailed, strandCommand,
|
|
4503
|
+
// task 1001916: the publish-branch failure formatter — exported pure so both
|
|
4504
|
+
// wire shapes (envelope + legacy flat), the step/reason surfacing, and the
|
|
4505
|
+
// non-fast-forward remedy hint are pinned without a server.
|
|
4506
|
+
formatPublishFailure,
|
|
4475
4507
|
// task 1719: pure post-land status reconciler — a task that reached 'shipped'
|
|
4476
4508
|
// in-session shows the SHIPPED banner + writes no breadcrumb; otherwise keep the
|
|
4477
4509
|
// pre-merge status (confirmed → next-turn breadcrumb fallback). Exported for tests.
|
package/src/module-api.js
CHANGED
|
@@ -685,8 +685,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
685
685
|
// landed since 1.19.127 with no explicit bump. run 31857842473. (task 1002620)
|
|
686
686
|
// 1.19.129 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
687
687
|
// landed since 1.19.128 with no explicit bump. run 31858158416. (task 1002620)
|
|
688
|
+
// 1.19.130 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
689
|
+
// landed since 1.19.129 with no explicit bump. run 31861289412. (task 1002620)
|
|
690
|
+
// 1.19.131 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
691
|
+
// landed since 1.19.130 with no explicit bump. run 31861682626. (task 1002620)
|
|
688
692
|
// ---------------------------------------------------------------------------
|
|
689
|
-
const CORE_VERSION = '1.19.
|
|
693
|
+
const CORE_VERSION = '1.19.131'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
690
694
|
|
|
691
695
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
692
696
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -12,11 +12,18 @@
|
|
|
12
12
|
// and asserts zero residual leaks + private⇄public migration-numbering/enum stability.
|
|
13
13
|
// Filesystem + git (git ls-files) are the only dependencies — no DB, no network.
|
|
14
14
|
import assert from 'node:assert/strict';
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
15
18
|
import { test } from 'node:test';
|
|
16
19
|
import { createRequire } from 'node:module';
|
|
17
20
|
|
|
18
21
|
const require = createRequire(import.meta.url);
|
|
19
22
|
const { runSimulation } = require('../scripts/gds/mirror-lineage-check.js');
|
|
23
|
+
const { migrationNumberOf } = require('../scripts/gds/migration-namespace.js');
|
|
24
|
+
const { nextFree } = require('../scripts/gds/next-number.js');
|
|
25
|
+
|
|
26
|
+
const MIGRATIONS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'migrations');
|
|
20
27
|
|
|
21
28
|
test('C9 full-tree simulation: the redacted publishable mirror leaks nothing', () => {
|
|
22
29
|
const report = runSimulation();
|
|
@@ -43,3 +50,24 @@ test('C9 full-tree simulation: the redacted publishable mirror leaks nothing', (
|
|
|
43
50
|
`redaction altered enum value-list(s) in ${report.drift.length} migration(s): ${report.drift.map((d) => d.path).join(', ')}`
|
|
44
51
|
);
|
|
45
52
|
});
|
|
53
|
+
|
|
54
|
+
// The proof is only worth its green tick if it actually SEES every core migration.
|
|
55
|
+
// Before task 1002527 it parsed the bare `NNN_` form only, so every `core_NNN_` file
|
|
56
|
+
// (ADR 0108 §5) was invisible to BOTH halves — equally blind, therefore silently
|
|
57
|
+
// "consistent". This asserts the simulation's coverage against the real tree so the
|
|
58
|
+
// gate can't quietly stop covering new core migrations again.
|
|
59
|
+
test('C9 full-tree simulation covers namespaced core_NNN_ migrations too (task 1002527)', () => {
|
|
60
|
+
const basenames = fs.readdirSync(MIGRATIONS_DIR).filter((n) => migrationNumberOf(n) != null);
|
|
61
|
+
const namespaced = basenames.filter((n) => !/^\d/.test(n));
|
|
62
|
+
assert.ok(namespaced.length > 0, 'expected at least one core_NNN_ migration on disk — update this test if the namespace discipline changed');
|
|
63
|
+
|
|
64
|
+
const report = runSimulation();
|
|
65
|
+
assert.equal(
|
|
66
|
+
report.counts.migrations,
|
|
67
|
+
basenames.length,
|
|
68
|
+
`the proof saw ${report.counts.migrations} core migrations but migrations/ holds ${basenames.length} — a naming form is not being parsed`
|
|
69
|
+
);
|
|
70
|
+
// Bare and namespaced core files share ONE number line, so the proof's next-free must
|
|
71
|
+
// match what next-number.js hands a claim; if they disagree a number gets re-used.
|
|
72
|
+
assert.equal(report.nextPrivate, nextFree(new Set(basenames.map(migrationNumberOf))));
|
|
73
|
+
});
|
|
@@ -8,7 +8,9 @@ import { createRequire } from 'node:module';
|
|
|
8
8
|
const require = createRequire(import.meta.url);
|
|
9
9
|
const {
|
|
10
10
|
migrationNumbersFromPaths, parseEnumChecks, numberingDivergence, enumDrift,
|
|
11
|
+
proveLineage, buildRedactionContext,
|
|
11
12
|
} = require('../scripts/gds/mirror-lineage-check.js');
|
|
13
|
+
const { nextFree } = require('../scripts/gds/next-number.js');
|
|
12
14
|
|
|
13
15
|
test('migrationNumbersFromPaths picks CORE migrations only (not module/ADR numbers)', () => {
|
|
14
16
|
const set = migrationNumbersFromPaths([
|
|
@@ -31,9 +33,61 @@ test('migrationNumbersFromPaths excludes migrations/instance/ seeds (task 2152)'
|
|
|
31
33
|
'migrations/080_core_thing.sql', // bare core → counted
|
|
32
34
|
'migrations/instance/080_gds_v4_planning_seed.sql', // instance seed → NOT core
|
|
33
35
|
'migrations/instance/186_governance_seed.sql', // instance seed → NOT core
|
|
34
|
-
'migrations/core_192_namespaced.sql', //
|
|
36
|
+
'migrations/core_192_namespaced.sql', // namespaced core (ADR 0108 §5) → counted
|
|
35
37
|
]);
|
|
36
|
-
assert.deepEqual([...set].sort((a, b) => a - b), [80]);
|
|
38
|
+
assert.deepEqual([...set].sort((a, b) => a - b), [80, 192]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// ---- namespaced core migrations (ADR 0108 §5 / task 1002527) ---------------
|
|
42
|
+
// Core migrations above LEGACY_BARE_CORE_MAX are `core_NNN_`-prefixed so their stem
|
|
43
|
+
// can't collide with an instance/module migration across repos. The prefix namespaces
|
|
44
|
+
// the FILENAME, not the NUMBER LINE: core_188 continues the bare 001–187 sequence as
|
|
45
|
+
// ONE sequence (migration-namespace.js pins the cutoff so no bare core file can ever
|
|
46
|
+
// exceed it, and next-number.js unions both forms into a single max+1). The C9 proof
|
|
47
|
+
// must parse both forms or it silently stops covering every new core migration.
|
|
48
|
+
|
|
49
|
+
test('migrationNumbersFromPaths parses namespaced core_NNN_ names as ONE sequence with the bare ones (task 1002527)', () => {
|
|
50
|
+
const set = migrationNumbersFromPaths([
|
|
51
|
+
'migrations/003_pms.sql', // legacy bare core
|
|
52
|
+
'migrations/187_goal_sort_order.sql', // the last bare core number
|
|
53
|
+
'migrations/core_188_goal_visibility.sql',
|
|
54
|
+
'migrations/core_219_grade_attempts_idx.sql',
|
|
55
|
+
'modules/game/migrations/game_001_world.sql', // module migration — still excluded
|
|
56
|
+
]);
|
|
57
|
+
assert.deepEqual([...set].sort((a, b) => a - b), [3, 187, 188, 219]);
|
|
58
|
+
// ONE sequence, so the next free core number continues past the namespaced files —
|
|
59
|
+
// the same number next-number.js hands a claim (188 here would re-use core_188).
|
|
60
|
+
assert.equal(nextFree(set), 220);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('proveLineage counts namespaced core_NNN_ migrations in the MIRROR set (task 1002527)', () => {
|
|
64
|
+
const ctx = buildRedactionContext();
|
|
65
|
+
const file = (p, sql) => ({ path: p, buf: Buffer.from(sql, 'utf8') });
|
|
66
|
+
const publishableFiles = [
|
|
67
|
+
file('migrations/003_pms.sql', "CREATE TABLE a (s TEXT CHECK (s IN ('x', 'y')));\n"),
|
|
68
|
+
file('migrations/core_188_goal_visibility.sql', "CREATE TABLE b (s TEXT CHECK (s IN ('p', 'q')));\n"),
|
|
69
|
+
file('src/bongos/db.js', 'module.exports = {};\n'), // not a migration
|
|
70
|
+
];
|
|
71
|
+
const report = proveLineage({ publishableFiles, privateMigrationNumbers: new Set([3, 188]), ctx });
|
|
72
|
+
|
|
73
|
+
// Both halves must agree: the private set (migrationNumbersFromPaths) and the mirror
|
|
74
|
+
// set (proveLineage) parse the SAME names, so a namespaced file is never a false
|
|
75
|
+
// "missing" divergence — the failure mode that would red-line the C9 gate.
|
|
76
|
+
assert.equal(report.counts.migrations, 2);
|
|
77
|
+
assert.deepEqual(report.numbering, { missing: [], extra: [], consistent: true });
|
|
78
|
+
assert.equal(report.nextMirror, 189);
|
|
79
|
+
assert.equal(report.nextPrivate, 189);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('proveLineage still CATCHES a genuinely dropped namespaced core migration (task 1002527)', () => {
|
|
83
|
+
const ctx = buildRedactionContext();
|
|
84
|
+
const publishableFiles = [
|
|
85
|
+
{ path: 'migrations/003_pms.sql', buf: Buffer.from('SELECT 1;\n', 'utf8') },
|
|
86
|
+
];
|
|
87
|
+
// core_188 exists privately but never reached the mirror → a real divergence.
|
|
88
|
+
const report = proveLineage({ publishableFiles, privateMigrationNumbers: new Set([3, 188]), ctx });
|
|
89
|
+
assert.equal(report.numbering.consistent, false);
|
|
90
|
+
assert.deepEqual(report.numbering.missing, [188]);
|
|
37
91
|
});
|
|
38
92
|
|
|
39
93
|
test('parseEnumChecks extracts the quoted value-lists from CHECK (... IN (...))', () => {
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// tests/ship_publish_error.mjs
|
|
2
|
+
//
|
|
3
|
+
// Unit tests for formatPublishFailure (scripts/gds/ship.js, task 1001916) — the
|
|
4
|
+
// display string ciLandServer prints when POST /tasks/:id/publish-branch fails.
|
|
5
|
+
//
|
|
6
|
+
// Why this exists: the server has carried { step, reason } (the credential-
|
|
7
|
+
// scrubbed git stderr) since task 1547, but ciLandServer printed only
|
|
8
|
+
// `r.data.message || r.data.error`. On the ADR 0116 envelope that renders
|
|
9
|
+
// "[object Object]", and on the legacy flat shape a bare "publish_failed" — so a
|
|
10
|
+
// non-fast-forward rejection (the server had already freshened the branch) was
|
|
11
|
+
// invisible and the builder retried blind for several laps. What we prove here:
|
|
12
|
+
// 1. Both wire shapes resolve to the CODE, never "[object Object]".
|
|
13
|
+
// 2. step + reason appear when the server sent them.
|
|
14
|
+
// 3. A non-fast-forward reason carries the specific remedy hint (reset onto
|
|
15
|
+
// origin/<branch>, re-run the generators, re-ship) — and only then.
|
|
16
|
+
// Pure: no network, no git, no fs.
|
|
17
|
+
//
|
|
18
|
+
// Run: node tests/ship_publish_error.mjs
|
|
19
|
+
|
|
20
|
+
import { strict as assert } from 'node:assert';
|
|
21
|
+
import { createRequire } from 'node:module';
|
|
22
|
+
|
|
23
|
+
const require = createRequire(import.meta.url);
|
|
24
|
+
const ship = require('../scripts/gds/ship.js');
|
|
25
|
+
|
|
26
|
+
let passed = 0;
|
|
27
|
+
let failed = 0;
|
|
28
|
+
function test(name, fn) {
|
|
29
|
+
try {
|
|
30
|
+
fn();
|
|
31
|
+
passed++;
|
|
32
|
+
console.log(` ok ${name}`);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
failed++;
|
|
35
|
+
console.log(` FAIL ${name}`);
|
|
36
|
+
console.log(` ${err.message}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const BRANCH = 'claude/task-1001916';
|
|
41
|
+
const fmt = (data, status = 500, branch = BRANCH) => ship.formatPublishFailure(data, status, branch);
|
|
42
|
+
|
|
43
|
+
// ---------- 1. bare error, no step/reason ----------
|
|
44
|
+
|
|
45
|
+
test('legacy flat { error: code } with no details reads as the bare code', () => {
|
|
46
|
+
const out = fmt({ error: 'publish_failed' });
|
|
47
|
+
assert.match(out, /publish_failed/);
|
|
48
|
+
assert.ok(!/step:/.test(out), 'no step was sent, so none is invented');
|
|
49
|
+
assert.ok(!/reason:/.test(out), 'no reason was sent, so none is invented');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('ADR 0116 envelope with no details resolves to the code, never [object Object]', () => {
|
|
53
|
+
const out = fmt({ error: { code: 'publish_failed', message: 'publish_failed' } });
|
|
54
|
+
assert.match(out, /publish_failed/);
|
|
55
|
+
assert.ok(!/\[object Object\]/.test(out), 'the envelope object must never be stringified raw');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('no body at all falls back to the HTTP status', () => {
|
|
59
|
+
assert.match(fmt(null, 502), /HTTP 502/);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ---------- 2. error + step + reason ----------
|
|
63
|
+
|
|
64
|
+
test('envelope details carry step + reason into the message', () => {
|
|
65
|
+
const out = fmt({
|
|
66
|
+
error: {
|
|
67
|
+
code: 'publish_failed',
|
|
68
|
+
message: 'publish_failed',
|
|
69
|
+
details: { step: 'push', reason: 'remote: Permission to org/repo.git denied' },
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
assert.match(out, /publish_failed/);
|
|
73
|
+
assert.match(out, /step: push/);
|
|
74
|
+
assert.match(out, /Permission to org\/repo\.git denied/);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('legacy flat top-level step + reason are surfaced the same way', () => {
|
|
78
|
+
const out = fmt({ error: 'publish_failed', step: 'fetch', reason: 'could not read Username' });
|
|
79
|
+
assert.match(out, /step: fetch/);
|
|
80
|
+
assert.match(out, /could not read Username/);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('a non-git failure reason gets NO fast-forward remedy hint', () => {
|
|
84
|
+
const out = fmt({ error: 'publish_failed', step: 'push', reason: 'Permission denied (publickey)' });
|
|
85
|
+
assert.ok(!/fast-forward/i.test(out), 'the remedy hint is for a stale-branch push only');
|
|
86
|
+
assert.ok(!/reset/i.test(out), 'nor the reset instruction');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ---------- 3. the non-fast-forward remedy hint ----------
|
|
90
|
+
|
|
91
|
+
const NON_FF_STDERR =
|
|
92
|
+
'! [rejected] claude/task-1001916 -> claude/task-1001916 (fetch first)\n' +
|
|
93
|
+
"error: failed to push some refs to 'github.com/org/repo.git'";
|
|
94
|
+
|
|
95
|
+
test('a "fetch first" rejection names the branch and the reset-onto-origin remedy', () => {
|
|
96
|
+
const out = fmt({ error: 'publish_failed', step: 'push', reason: NON_FF_STDERR });
|
|
97
|
+
assert.match(out, /step: push/);
|
|
98
|
+
assert.match(out, new RegExp(`origin/${BRANCH}`), 'points at the ref the server already moved');
|
|
99
|
+
assert.match(out, /reset/i, 'names the reset');
|
|
100
|
+
assert.match(out, /generator/i, 'and the regenerate step before re-shipping');
|
|
101
|
+
assert.match(out, /re-ship|fast-forward/i, 'and why: so the push fast-forwards');
|
|
102
|
+
assert.ok(!/\n/.test(out), 'multi-line git stderr is collapsed — this goes mid-sentence into one log line');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('the "non-fast-forward" wording triggers the same hint through the envelope', () => {
|
|
106
|
+
const out = fmt({
|
|
107
|
+
error: {
|
|
108
|
+
code: 'publish_failed',
|
|
109
|
+
message: 'publish_failed',
|
|
110
|
+
details: { step: 'push', reason: 'Updates were rejected: non-fast-forward' },
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
assert.match(out, new RegExp(`origin/${BRANCH}`));
|
|
114
|
+
assert.match(out, /reset/i);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
console.log('');
|
|
118
|
+
console.log(`ship_publish_error: ${passed} passed, ${failed} failed`);
|
|
119
|
+
process.exit(failed === 0 ? 0 : 1);
|
package/tests/subdomain_auth.mjs
CHANGED
|
@@ -1,14 +1,46 @@
|
|
|
1
1
|
// tests/subdomain_auth.mjs — the builders-subdomain auth helpers (#726).
|
|
2
2
|
//
|
|
3
3
|
// Locks down the two security-sensitive pure helpers added for the dedicated
|
|
4
|
-
// builders
|
|
4
|
+
// builders.<apex> subdomain:
|
|
5
5
|
// - safeReturnTo: the open-redirect guard on the post-sign-in return target.
|
|
6
6
|
// - sessionCookieDomainOpts: which hosts get an apex-scoped session cookie
|
|
7
7
|
// (so one sign-in spans the subdomains) vs a host-only one (localhost,
|
|
8
8
|
// staging — which must NOT bleed a token across environments).
|
|
9
9
|
// Pure functions, no DB — requiring the route module does not open a pool.
|
|
10
|
+
//
|
|
11
|
+
// Both helpers derive their allowed hosts from the instance branding pack's
|
|
12
|
+
// domains.cookieDomain, so the fixtures inject a SYNTHETIC pack (the
|
|
13
|
+
// GDS_BRANDING_FILE seam, as tests/single_host_hall.mjs does) instead of
|
|
14
|
+
// reading whatever config/branding.json happens to be on disk. That keeps the
|
|
15
|
+
// assertions identical on every instance — the neutral core (cookieDomain "")
|
|
16
|
+
// and a branded host alike. envPrefix is overridden too so an ambient
|
|
17
|
+
// <PREFIX>_COOKIE_DOMAIN in the shell cannot reach the resolver.
|
|
18
|
+
//
|
|
19
|
+
// Run: node tests/subdomain_auth.mjs
|
|
10
20
|
|
|
11
21
|
import { createRequire } from 'node:module';
|
|
22
|
+
import { writeFileSync, mkdtempSync, rmSync } from 'node:fs';
|
|
23
|
+
import { tmpdir } from 'node:os';
|
|
24
|
+
import { join } from 'node:path';
|
|
25
|
+
|
|
26
|
+
// The apex every fixture below is built from. RFC 2606 reserved TLD — clearly
|
|
27
|
+
// synthetic, never routable, and no host brand is baked into this file.
|
|
28
|
+
const APEX = 'bongos-test.example';
|
|
29
|
+
|
|
30
|
+
const brandDir = mkdtempSync(join(tmpdir(), 'cb-subdomain-auth-'));
|
|
31
|
+
const brandFile = join(brandDir, 'branding.json');
|
|
32
|
+
writeFileSync(brandFile, JSON.stringify({
|
|
33
|
+
envPrefix: 'SUBDOMAIN_AUTH_FIXTURE',
|
|
34
|
+
// Leading dot mirrors the real packs; configuredCookieApex() strips it.
|
|
35
|
+
domains: { cookieDomain: `.${APEX}`, publicOrigin: `https://${APEX}` },
|
|
36
|
+
}));
|
|
37
|
+
process.env.GDS_BRANDING_FILE = brandFile;
|
|
38
|
+
// The pack is only read once (auth.js resolves COOKIE_APEX at module load);
|
|
39
|
+
// remove the temp dir on every exit path so runs don't accumulate in tmpdir.
|
|
40
|
+
process.on('exit', () => { try { rmSync(brandDir, { recursive: true, force: true }); } catch { /* best-effort */ } });
|
|
41
|
+
|
|
42
|
+
// Required AFTER the pack is in place — auth.js resolves COOKIE_APEX at module
|
|
43
|
+
// load, and branding() memoizes on its first call.
|
|
12
44
|
const require = createRequire(import.meta.url);
|
|
13
45
|
const authRoute = require('../src/bongos/routes/auth.js');
|
|
14
46
|
|
|
@@ -24,15 +56,15 @@ console.log('\nsafeReturnTo — accepts safe same-city targets:');
|
|
|
24
56
|
ok(safeReturnTo('/builders') === '/builders', 'relative /builders');
|
|
25
57
|
ok(safeReturnTo('/watch') === '/watch', 'relative /watch');
|
|
26
58
|
ok(safeReturnTo('/builders/settings?x=1') === '/builders/settings?x=1', 'relative path with query');
|
|
27
|
-
ok(safeReturnTo(
|
|
28
|
-
ok(safeReturnTo(
|
|
29
|
-
ok(safeReturnTo(
|
|
59
|
+
ok(safeReturnTo(`https://builders.${APEX}/watch`) === `https://builders.${APEX}/watch`, 'absolute builders subdomain');
|
|
60
|
+
ok(safeReturnTo(`https://${APEX}/builders`) === `https://${APEX}/builders`, 'absolute apex');
|
|
61
|
+
ok(safeReturnTo(`https://status.${APEX}/`) === `https://status.${APEX}/`, 'absolute status subdomain');
|
|
30
62
|
|
|
31
63
|
console.log('\nsafeReturnTo — rejects everything else (→ null, caller falls back):');
|
|
32
64
|
ok(safeReturnTo('//evil.com') === null, 'protocol-relative //evil.com');
|
|
33
65
|
ok(safeReturnTo('https://evil.com/builders') === null, 'foreign https origin');
|
|
34
|
-
ok(safeReturnTo(
|
|
35
|
-
ok(safeReturnTo(
|
|
66
|
+
ok(safeReturnTo(`https://${APEX}.evil.com/`) === null, 'lookalike suffix host');
|
|
67
|
+
ok(safeReturnTo(`http://${APEX}/builders`) === null, 'http (non-https) apex');
|
|
36
68
|
ok(safeReturnTo('javascript:alert(1)') === null, 'javascript: scheme');
|
|
37
69
|
ok(safeReturnTo('/path\\with\\backslash') === null, 'backslash path');
|
|
38
70
|
ok(safeReturnTo('') === null, 'empty');
|
|
@@ -43,15 +75,15 @@ ok(safeReturnTo('builders') === null, 'bare word (no leading slash, not a URL)')
|
|
|
43
75
|
// ---- sessionCookieDomainOpts: cross-subdomain scoping ----------------------
|
|
44
76
|
const reqWith = (host) => ({ headers: { host } });
|
|
45
77
|
console.log('\nsessionCookieDomainOpts — apex-scoped on production hosts:');
|
|
46
|
-
ok(sessionCookieDomainOpts(reqWith(
|
|
47
|
-
ok(sessionCookieDomainOpts(reqWith(
|
|
48
|
-
ok(sessionCookieDomainOpts(reqWith(
|
|
49
|
-
ok(sessionCookieDomainOpts(reqWith(
|
|
50
|
-
ok(sessionCookieDomainOpts(reqWith(
|
|
78
|
+
ok(sessionCookieDomainOpts(reqWith(APEX)).domain === `.${APEX}`, `apex → .${APEX}`);
|
|
79
|
+
ok(sessionCookieDomainOpts(reqWith(`www.${APEX}`)).domain === `.${APEX}`, `www → .${APEX}`);
|
|
80
|
+
ok(sessionCookieDomainOpts(reqWith(`builders.${APEX}`)).domain === `.${APEX}`, `builders → .${APEX}`);
|
|
81
|
+
ok(sessionCookieDomainOpts(reqWith(`status.${APEX}`)).domain === `.${APEX}`, `status → .${APEX}`);
|
|
82
|
+
ok(sessionCookieDomainOpts(reqWith(`${APEX}:3000`)).domain === `.${APEX}`, 'apex with port');
|
|
51
83
|
|
|
52
84
|
console.log('\nsessionCookieDomainOpts — host-only elsewhere (no domain key):');
|
|
53
85
|
ok(sessionCookieDomainOpts(reqWith('localhost:3000')).domain === undefined, 'localhost → host-only');
|
|
54
|
-
ok(sessionCookieDomainOpts(reqWith(
|
|
86
|
+
ok(sessionCookieDomainOpts(reqWith(`staging.${APEX}`)).domain === undefined, 'staging → host-only (no cross-env bleed)');
|
|
55
87
|
ok(sessionCookieDomainOpts(reqWith('127.0.0.1')).domain === undefined, 'raw IP → host-only');
|
|
56
88
|
|
|
57
89
|
console.log(`\nSubdomain-auth tests: ${pass} passed, ${fail} failed`);
|