@bongos/core 1.19.203 → 1.19.204

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 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.203",
6
- "core_contract": "1.19.203",
7
- "source_commit": "3596d383ae4955438fc6ab7f9599e680ddafb570",
5
+ "core_version": "1.19.204",
6
+ "core_contract": "1.19.204",
7
+ "source_commit": "1b831b99695f793cf8d6e7fdf248e20ea2486c77",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-17T19:20:16.116Z",
9
+ "built_at": "2026-08-17T19:29:56.265Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 310,
13
13
  "agent_docs_stubbed": 21,
14
- "functional_verbatim": 1295,
14
+ "functional_verbatim": 1296,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1626,
20
- "tree_sha256": "d4a2fd8d4540f54177859f2312a5a48c6539b34b12e1e61757d0b242f92be615",
19
+ "file_count": 1627,
20
+ "tree_sha256": "a44dd13817d43a88eb9a226b742a2c039888e0390bf5a5cb428c312e95db4aaf",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -4127,7 +4127,7 @@
4127
4127
  {
4128
4128
  "path": "modules/security/routes/scans.js",
4129
4129
  "mode": "0000644",
4130
- "sha256": "008bee59c78d7e67726f30d770da81b5c16f4fe01905f22a47a5d2ab6c5e68d8"
4130
+ "sha256": "c32d481210539609844d3c9abd8ccb3dd349a512d055e2a9f8bc055105b29c08"
4131
4131
  },
4132
4132
  {
4133
4133
  "path": "modules/security/scan-store.js",
@@ -4169,6 +4169,11 @@
4169
4169
  "mode": "0000644",
4170
4170
  "sha256": "4af88247bed3dcb3c12370e48380625871a9b50a0b430a6da34db0ce6d6c8aca"
4171
4171
  },
4172
+ {
4173
+ "path": "modules/security/tests/scan_route_schema.mjs",
4174
+ "mode": "0000644",
4175
+ "sha256": "434e69a1bfd9da64462f7f063336cc3b31c19c7643bb33692a45b18cef2ce5f6"
4176
+ },
4172
4177
  {
4173
4178
  "path": "modules/security/tests/scan_store.mjs",
4174
4179
  "mode": "0000644",
@@ -4272,12 +4277,12 @@
4272
4277
  {
4273
4278
  "path": "package-lock.json",
4274
4279
  "mode": "0000644",
4275
- "sha256": "30f113e605eea7d404a3393045402c6234a6822724656d0830a5a553a6215fac"
4280
+ "sha256": "45a21ec8d9e8ffae13d85a59beb598f381a10fb85746c42888f0b8c3be098e0f"
4276
4281
  },
4277
4282
  {
4278
4283
  "path": "package.json",
4279
4284
  "mode": "0000644",
4280
- "sha256": "fa8341a0a90d43e710d3f9e04589ce7f055c8f180cf915f28fa074b0b9206977"
4285
+ "sha256": "07c27a2b49959b9022ad8465bc02abe9fbce3bceb9068385a457d01c32423f0f"
4281
4286
  },
4282
4287
  {
4283
4288
  "path": "public-docs/index.html",
@@ -4942,7 +4947,7 @@
4942
4947
  {
4943
4948
  "path": "scripts/gds/gen-api-docs.js",
4944
4949
  "mode": "0000644",
4945
- "sha256": "ccdebd47cc53a4896c766c6a70f43dfb91e3b84264b82bd927c918ac941a6905"
4950
+ "sha256": "03b4263f19c0f7157160011f43a174599e5294f0b62fdaf3a6e943e9bfeec855"
4946
4951
  },
4947
4952
  {
4948
4953
  "path": "scripts/gds/gen-atlas.js",
@@ -5887,7 +5892,7 @@
5887
5892
  {
5888
5893
  "path": "src/module-api.js",
5889
5894
  "mode": "0000644",
5890
- "sha256": "f5b425d9be5b552f40d72f31cd96dbcb901b15478ed7a60a35221aaeb09b4fb7"
5895
+ "sha256": "acfdae5a37ae7fa878a752cb566ec930dec8a7703cb86ef9d16609f6ec8c1bab"
5891
5896
  },
5892
5897
  {
5893
5898
  "path": "src/module-loader/catalog.js",
@@ -23,18 +23,50 @@ const { validateOrRespond } = api;
23
23
  // Interposed BEFORE api.requireRank so a Xenos gets the documented degraded
24
24
  // path instead of the generic rank refusal. Anyone Metic+ falls through to the
25
25
  // real gate (which fitness/route-rank-check reads as this route's rank).
26
+ // try-first: a throw here (e.g. a doorway-version skew dropping
27
+ // rankMeetsThreshold) must answer NAMED, never fall to the global
28
+ // internal_error net — the failure mode of the first real scan (task 1003057).
26
29
  function xenosDegradedPath(req, res, next) {
27
- if (api.rankMeetsThreshold(req.builder?.rank, 'metic')) return next();
28
- return res.fail('scan_row_requires_metic', 403, {
29
- degraded_path:
30
- 'Your scan itself is unaffected — keep the report file in your user directory and the chat summary. ' +
31
- 'What your rank cannot do is write the SHARED scan record other builders query. ' +
32
- 'Ask any Metic+ builder to POST this scan, or re-run the record step after promotion.',
33
- kept: ['report_file', 'chat_summary'],
34
- not_recorded: ['gds_scan_row'],
35
- });
30
+ try {
31
+ if (api.rankMeetsThreshold(req.builder?.rank, 'metic')) return next();
32
+ return res.fail('scan_row_requires_metic', 403, {
33
+ degraded_path:
34
+ 'Your scan itself is unaffected — keep the report file in your user directory and the chat summary. ' +
35
+ 'What your rank cannot do is write the SHARED scan record other builders query. ' +
36
+ 'Ask any Metic+ builder to POST this scan, or re-run the record step after promotion.',
37
+ kept: ['report_file', 'chat_summary'],
38
+ not_recorded: ['gds_scan_row'],
39
+ });
40
+ } catch (err) {
41
+ console.error('[gds] scans gate', err);
42
+ return res.fail('scan_record_failed', 500);
43
+ }
36
44
  }
37
45
 
46
+ // Built ONCE at module scope, not per request: the [...scanStore.X] spreads
47
+ // mean a missing/renamed scan-store export now crashes at BOOT with a named
48
+ // stack in the server log, instead of throwing on every request forever while
49
+ // the route "exists". Deep checks stay in scanStore.normalizeScanPayload.
50
+ const SCAN_ROW_SCHEMA = Object.freeze({
51
+ target_url: { required: true, type: 'string', maxLength: 2048, minLength: 9 },
52
+ artifact_kind: { required: true, type: 'string', enum: [...scanStore.ARTIFACT_KINDS] },
53
+ resolved_sha: { type: 'string', maxLength: 40 },
54
+ content_hash: { type: 'string', maxLength: 64 },
55
+ file_count: { type: 'integer', min: 0 },
56
+ total_bytes: { type: 'integer', min: 0 },
57
+ tier: { required: true, type: 'string', enum: [...scanStore.TIERS] },
58
+ verdict: { type: 'string', enum: [...scanStore.VERDICTS] },
59
+ completion: { required: true, type: 'string', enum: [...scanStore.COMPLETIONS] },
60
+ abort_code: { type: 'string', maxLength: 64 },
61
+ findings_critical: { type: 'integer', min: 0 },
62
+ findings_high: { type: 'integer', min: 0 },
63
+ findings_medium: { type: 'integer', min: 0 },
64
+ findings_low: { type: 'integer', min: 0 },
65
+ findings_summary: { type: 'array' },
66
+ action_taken: { type: 'string', enum: [...scanStore.ACTIONS] },
67
+ engine_version: { required: true, type: 'string', maxLength: 64, minLength: 1 },
68
+ });
69
+
38
70
  module.exports = function buildSecurityScansRouter() {
39
71
  const router = express.Router();
40
72
 
@@ -52,35 +84,20 @@ module.exports = function buildSecurityScansRouter() {
52
84
  xenosDegradedPath,
53
85
  api.requireRank('metic', 'archon'),
54
86
  async (req, res) => {
55
- if (validateOrRespond(req, res, {
56
- target_url: { required: true, type: 'string', maxLength: 2048, minLength: 9 },
57
- artifact_kind: { required: true, type: 'string', enum: [...scanStore.ARTIFACT_KINDS] },
58
- resolved_sha: { type: 'string', maxLength: 40 },
59
- content_hash: { type: 'string', maxLength: 64 },
60
- file_count: { type: 'integer', min: 0 },
61
- total_bytes: { type: 'integer', min: 0 },
62
- tier: { required: true, type: 'string', enum: [...scanStore.TIERS] },
63
- verdict: { type: 'string', enum: [...scanStore.VERDICTS] },
64
- completion: { required: true, type: 'string', enum: [...scanStore.COMPLETIONS] },
65
- abort_code: { type: 'string', maxLength: 64 },
66
- findings_critical: { type: 'integer', min: 0 },
67
- findings_high: { type: 'integer', min: 0 },
68
- findings_medium: { type: 'integer', min: 0 },
69
- findings_low: { type: 'integer', min: 0 },
70
- findings_summary: { type: 'array' },
71
- action_taken: { type: 'string', enum: [...scanStore.ACTIONS] },
72
- engine_version: { required: true, type: 'string', maxLength: 64, minLength: 1 },
73
- })) return;
87
+ try {
88
+ if (validateOrRespond(req, res, SCAN_ROW_SCHEMA)) return;
74
89
 
75
- const norm = scanStore.normalizeScanPayload(req.body);
76
- if (!norm.ok) return res.fail(norm.code, 400, norm.detail);
90
+ const norm = scanStore.normalizeScanPayload(req.body);
91
+ if (!norm.ok) return res.fail(norm.code, 400, norm.detail);
77
92
 
78
- try {
79
93
  const scan = await scanStore.insertScan(norm.value, req.builder.id);
80
94
  res.status(201).json({ ok: true, scan });
81
95
  } catch (err) {
96
+ // NO statement in this handler runs outside the try: any throw —
97
+ // validator, normalizer, or insert — answers this named envelope with
98
+ // the stack in the server log, never the global internal_error net.
82
99
  console.error('[gds] POST /security/scans', err);
83
- res.fail('scan_record_failed', 500);
100
+ if (!res.headersSent) res.fail('scan_record_failed', 500);
84
101
  }
85
102
  }
86
103
  );
@@ -113,3 +130,7 @@ module.exports = function buildSecurityScansRouter() {
113
130
 
114
131
  return router;
115
132
  };
133
+
134
+ // Exposed for the schema-contract test (scan_route_schema.mjs): every enum
135
+ // spread must resolve at require time, and required fields must stay pinned.
136
+ module.exports.SCAN_ROW_SCHEMA = SCAN_ROW_SCHEMA;
@@ -0,0 +1,69 @@
1
+ // routes/scans.js failure-path contract (task 1003057, idea 1000781).
2
+ //
3
+ // The first real scan could not record its row: live answered 500
4
+ // {internal_error} for EVERY POST body — even an invalid tier that should have
5
+ // produced a named 400 — meaning the handler threw before validation could
6
+ // answer and the global error net swallowed the name. Two properties keep that
7
+ // class of failure loud and named from now on:
8
+ //
9
+ // 1. The request schema (with its [...scanStore.X] enum spreads) is built at
10
+ // MODULE SCOPE — a missing/renamed scan-store export now crashes at boot
11
+ // (named, in the server log) instead of throwing per-request forever.
12
+ // 2. Every failure inside the POST handler answers a NAMED envelope
13
+ // (scan_record_failed at worst) — internal_error must never escape it.
14
+ import assert from 'node:assert/strict';
15
+ import { readFileSync } from 'node:fs';
16
+ import { createRequire } from 'node:module';
17
+ import { fileURLToPath } from 'node:url';
18
+ import path from 'node:path';
19
+
20
+ const require = createRequire(import.meta.url);
21
+ const scansRoute = require('../routes/scans.js');
22
+ const scanStore = require('../scan-store.js');
23
+
24
+ const routeSource = readFileSync(
25
+ path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'routes', 'scans.js'),
26
+ 'utf8'
27
+ );
28
+
29
+ let passed = 0;
30
+ let failed = 0;
31
+ async function test(name, fn) {
32
+ try { await fn(); passed++; console.log(` ok ${name}`); }
33
+ catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.stack || err.message}`); }
34
+ }
35
+
36
+ await test('the POST schema exists at module scope with every enum resolved', () => {
37
+ const schema = scansRoute.SCAN_ROW_SCHEMA;
38
+ assert.ok(schema && typeof schema === 'object', 'SCAN_ROW_SCHEMA must be exported');
39
+ assert.deepEqual(schema.tier.enum, [...scanStore.TIERS]);
40
+ assert.deepEqual(schema.artifact_kind.enum, [...scanStore.ARTIFACT_KINDS]);
41
+ assert.deepEqual(schema.verdict.enum, [...scanStore.VERDICTS]);
42
+ assert.deepEqual(schema.completion.enum, [...scanStore.COMPLETIONS]);
43
+ assert.deepEqual(schema.action_taken.enum, [...scanStore.ACTIONS]);
44
+ for (const field of ['target_url', 'artifact_kind', 'tier', 'completion', 'engine_version']) {
45
+ assert.equal(schema[field].required, true, `${field} must stay required`);
46
+ }
47
+ // Every enum really resolved — an undefined spread here is exactly the
48
+ // per-request TypeError class this test exists to prevent.
49
+ for (const [field, rule] of Object.entries(schema)) {
50
+ if (rule.enum) assert.ok(rule.enum.length > 0, `${field}.enum resolved empty`);
51
+ }
52
+ });
53
+
54
+ await test('the POST handler body opens with try and answers scan_record_failed on any throw', () => {
55
+ // Source-shape pin (the route-rank-check idiom): the async handler must not
56
+ // run ANY statement outside its try, and the catch must answer a named code.
57
+ assert.match(routeSource, /async \(req, res\) => \{\s*\n\s*try \{/,
58
+ 'POST handler must enter try before its first statement');
59
+ assert.match(routeSource, /res\.fail\('scan_record_failed', 500\)/,
60
+ 'the catch must answer the named scan_record_failed envelope');
61
+ });
62
+
63
+ await test('the xenos degraded-path gate cannot throw unnamed', () => {
64
+ assert.match(routeSource, /function xenosDegradedPath\(req, res, next\) \{\s*\n\s*try \{/,
65
+ 'xenosDegradedPath must enter try before its first statement');
66
+ });
67
+
68
+ console.log(`\nscan_route_schema: ${passed} passed, ${failed} failed`);
69
+ if (failed > 0) process.exit(1);
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.203",
3
+ "version": "1.19.204",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.203",
9
+ "version": "1.19.204",
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.203",
3
+ "version": "1.19.204",
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",
@@ -412,7 +412,23 @@ function extractBody(lines, i, method, constants = {}) {
412
412
  let strict = false;
413
413
  if (validateCall) {
414
414
  const after = validateCall.index + validateCall[0].length;
415
- const lit = captureBalanced(window, after);
415
+ let lit = captureBalanced(window, after);
416
+ // captureBalanced seeks the NEXT '{' anywhere — when the third argument is
417
+ // an identifier, that grabs some unrelated later block. Only trust the
418
+ // capture when the brace directly follows the call.
419
+ if (lit && !/^\s*\{/.test(window.slice(after))) lit = null;
420
+ if (!lit) {
421
+ // Schema hoisted to a module-scope const (the boot-loud enum-spread
422
+ // pattern, task 1003057): resolve `validateOrRespond(req, res, IDENT)`
423
+ // against `const IDENT = [Object.freeze(] { … }` anywhere in the file,
424
+ // so hoisting a schema never silently drops a route from the API docs.
425
+ const ident = /^\s*([A-Za-z_$][\w$]*)\s*[,)]/.exec(window.slice(after));
426
+ if (ident) {
427
+ const whole = stripComments(lines.join('\n'));
428
+ const decl = new RegExp(`const\\s+${ident[1]}\\s*=\\s*(?:Object\\.freeze\\s*\\(\\s*)?`).exec(whole);
429
+ if (decl) lit = captureBalanced(whole, decl.index + decl[0].length);
430
+ }
431
+ }
416
432
  if (lit) {
417
433
  for (const { key, value } of topLevelEntries(lit)) {
418
434
  if (/^\d/.test(key)) continue; // defensive: no numeric-leading field names
@@ -420,8 +436,11 @@ function extractBody(lines, i, method, constants = {}) {
420
436
  rules[key] = parseRule(value, constants);
421
437
  }
422
438
  if (orRespond) {
423
- // The opts arg follows the schema literal; strict unless it passes strict:false.
424
- const optsWindow = window.slice(window.indexOf(lit, after) + lit.length);
439
+ // The opts arg follows the schema literal; strict unless it passes
440
+ // strict:false. When the schema was resolved from a module-scope const,
441
+ // the literal is not in the window — read the call tail instead.
442
+ const litAt = window.indexOf(lit, after);
443
+ const optsWindow = litAt >= 0 ? window.slice(litAt + lit.length) : window.slice(after);
425
444
  strict = !/\bstrict\s*:\s*false\b/.test(optsWindow);
426
445
  }
427
446
  }
package/src/module-api.js CHANGED
@@ -833,8 +833,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
833
833
  // landed since 1.19.201 with no explicit bump. run 32059276345. (task 1002620)
834
834
  // 1.19.203 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
835
835
  // landed since 1.19.202 with no explicit bump. run 32059705946. (task 1002620)
836
+ // 1.19.204 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
837
+ // landed since 1.19.203 with no explicit bump. run 32060607072. (task 1002620)
836
838
  // ---------------------------------------------------------------------------
837
- const CORE_VERSION = '1.19.203'; // CI auto-patch carrier (ADR 0161); see changelog above
839
+ const CORE_VERSION = '1.19.204'; // CI auto-patch carrier (ADR 0161); see changelog above
838
840
 
839
841
  // A namespaced logger so a module's log lines are attributable + consistent.
840
842
  // Usage: const log = api.logger('dev-box'); log.info('mounted');