@bongos/core 1.19.51 → 1.19.52

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.51",
6
- "core_contract": "1.19.51",
7
- "source_commit": "fe0b1449ce7c972b972778e74945261c35565c48",
5
+ "core_version": "1.19.52",
6
+ "core_contract": "1.19.52",
7
+ "source_commit": "ce8e0d15694aed30b1640891c4256c96322a2a6f",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-11T18:53:46.990Z",
9
+ "built_at": "2026-08-11T19:06:37.776Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 291,
13
13
  "agent_docs_stubbed": 21,
14
- "functional_verbatim": 1217,
14
+ "functional_verbatim": 1218,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1529,
20
- "tree_sha256": "c3aec243f4a401c529edd9c426192e56b1264abfa615e29045ac5cf054e33d57",
19
+ "file_count": 1530,
20
+ "tree_sha256": "72953759b072de03029b81979095699aeb2d45b2363ba7047d8503457761956f",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -2502,7 +2502,7 @@
2502
2502
  {
2503
2503
  "path": "modules/dev-box/app/src/gds-client.js",
2504
2504
  "mode": "0000644",
2505
- "sha256": "845f51622803363dbc1229c325c4d6148087c756812e7e8e049ced7aa479f168"
2505
+ "sha256": "323f5e71b7836dd1fb6c7710e1b2159dec6382df89a7c5d001b7e7cccd5aa46d"
2506
2506
  },
2507
2507
  {
2508
2508
  "path": "modules/dev-box/app/src/main.js",
@@ -4037,12 +4037,12 @@
4037
4037
  {
4038
4038
  "path": "package-lock.json",
4039
4039
  "mode": "0000644",
4040
- "sha256": "43bd308850fdf28f3fa803c778af55a4a86e72edb62ccd8dc2d3297da95efbe2"
4040
+ "sha256": "37bb9183f916ffafd19c5c88408f5f776bdb091afc46b1bfbad64669d642102f"
4041
4041
  },
4042
4042
  {
4043
4043
  "path": "package.json",
4044
4044
  "mode": "0000644",
4045
- "sha256": "a402f396d0bfd5b10f24685a15ac36f9022c182db018787fbdd96b296e26c5db"
4045
+ "sha256": "80465cb4506affe5fc47c835df7a0f4f1634a0743c2de6d14f843f342d487c45"
4046
4046
  },
4047
4047
  {
4048
4048
  "path": "public-docs/index.html",
@@ -5632,7 +5632,7 @@
5632
5632
  {
5633
5633
  "path": "src/bongos/serve-internal.js",
5634
5634
  "mode": "0000644",
5635
- "sha256": "25c52c06c03af400c94f04f2cda1aaab0c5e5486a084005b9a4e224f345a4871"
5635
+ "sha256": "2f88796346b1c3a6dc49e25766b223fbf2d99e34b38b9a0f1e64a6fb9a59856d"
5636
5636
  },
5637
5637
  {
5638
5638
  "path": "src/bongos/uptime-poller.js",
@@ -5662,7 +5662,7 @@
5662
5662
  {
5663
5663
  "path": "src/module-api.js",
5664
5664
  "mode": "0000644",
5665
- "sha256": "3c2f62e446506de3fa855989e8606aad486566e37eed47b61c3a18b835a404f4"
5665
+ "sha256": "fc779475caf64dd09a7a3f952b4dadfcf2f123ca1a3d0ea9af20762b3de5e6c8"
5666
5666
  },
5667
5667
  {
5668
5668
  "path": "src/module-loader/loader.js",
@@ -6247,7 +6247,7 @@
6247
6247
  {
6248
6248
  "path": "tests/devbox_downloads.mjs",
6249
6249
  "mode": "0000644",
6250
- "sha256": "91950e7477f9fff30e361bc32def211e1cb3792fa098e9c839d5db1e1ba21d6a"
6250
+ "sha256": "897fe6b114c612e132e6fa0b3c68f43b865c684c7653e73cf42db894a7e4dc94"
6251
6251
  },
6252
6252
  {
6253
6253
  "path": "tests/devbox_icons.mjs",
@@ -6264,6 +6264,11 @@
6264
6264
  "mode": "0000644",
6265
6265
  "sha256": "ac0a024d96ef40af52af8b94570447ad5f1556467b68380c1b45fd3419139ad6"
6266
6266
  },
6267
+ {
6268
+ "path": "tests/devbox_update_path.mjs",
6269
+ "mode": "0000644",
6270
+ "sha256": "b58e87eeca17053709a8e2bdcefd032060358cc85314d28bf816a92d465fdae8"
6271
+ },
6267
6272
  {
6268
6273
  "path": "tests/device_flow_start.mjs",
6269
6274
  "mode": "0000644",
@@ -82,8 +82,12 @@ function createClient({ getToken, getApiBase, fetchImpl } = {}) {
82
82
  claimableTasks: () => unwrap(gen('').request('GET', '/api/gds/tasks/claimable')),
83
83
  claimTask: (taskId) => unwrap(gen('').request('POST', '/api/gds/claims', { body: { task_id: taskId } })),
84
84
  boxScope: () => unwrap(gen('').request('GET', '/api/gds/box/scope')),
85
- // update check (public static asset at the origin root, not under /api/gds)
86
- latestRelease: () => unwrap(gen('').request('GET', '/downloads/devbox/latest.json')),
85
+ // update check (public route at the origin root, not under /api/gds).
86
+ // task 1002649: polls the EXTENSIONLESS /version twin, NOT latest.json — a
87
+ // front proxy with a file-matcher can eat *.json paths before Node (the
88
+ // live instance's pre-ADR-0152 vhost 404'd latest.json empty while this
89
+ // endpoint worked), and the updater only needs { version, assets } anyway.
90
+ latestRelease: () => unwrap(gen('').request('GET', '/downloads/devbox/version')),
87
91
  };
88
92
  }
89
93
 
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.51",
3
+ "version": "1.19.52",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.51",
9
+ "version": "1.19.52",
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.51",
3
+ "version": "1.19.52",
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",
@@ -359,6 +359,38 @@ function isBuildersHost(req) {
359
359
  // Call this ONCE, right after app setup (trust proxy etc.) and BEFORE any
360
360
  // entrypoint-specific tail (the game static + themed 404 in server.js; the plain
361
361
  // 404 in platform-server.js).
362
+ // The Dev Box download/manifest mini-router (#904, ADR 0045), extracted so the
363
+ // ACTUAL route handlers are hermetically testable (tests/devbox_update_path.mjs
364
+ // mounts them on a throwaway app — the docs_openapi_route pattern) instead of
365
+ // only their pure builder. task 1002649: the manifest is served under BOTH
366
+ // /latest.json and an extensionless /latest twin — a front proxy with a
367
+ // file-matcher can eat *.json paths before Node (the live pre-ADR-0152 vhost
368
+ // 404'd latest.json empty while extensionless siblings worked), so every
369
+ // manifest consumer has a path that survives that class. Registration order
370
+ // matters: the literal routes must precede the `:target` catch-all.
371
+ function mountDevboxDownloads(app, devboxDownloads) {
372
+ const manifest = (_req, res) => {
373
+ // Short TTL: a release bump (config change + deploy) is visible within a
374
+ // minute, same posture as the hall's HTML.
375
+ res.set('Cache-Control', 'public, max-age=60');
376
+ res.json(devboxDownloads.latestJson());
377
+ };
378
+ app.get('/downloads/devbox/latest.json', manifest);
379
+ app.get('/downloads/devbox/latest', manifest);
380
+ // #924: the lightweight update-check endpoint the Dev Box app polls + the hall
381
+ // /settings courtesy line reads.
382
+ app.get('/downloads/devbox/version', (_req, res) => {
383
+ res.set('Cache-Control', 'public, max-age=60');
384
+ res.json(devboxDownloads.versionJson());
385
+ });
386
+ app.get('/downloads/devbox/:target', (req, res) => {
387
+ const url = devboxDownloads.redirectFor(req.params.target);
388
+ if (!url) return res.fail('unknown_target', 404, { targets: Object.keys(devboxDownloads.latestJson().assets) });
389
+ res.set('Cache-Control', 'public, max-age=60');
390
+ res.redirect(302, url);
391
+ });
392
+ }
393
+
362
394
  function mountInternalSurfaces(app) {
363
395
  // Prometheus instrumentation (task #1680, V4.R28a). The counting middleware
364
396
  // mounts FIRST so it wraps every surface registered below (and, since
@@ -477,26 +509,7 @@ function mountInternalSurfaces(app) {
477
509
  // /builders/downloads/* and 404).
478
510
  const devboxCfg = tryLoadInstanceConfig('devbox-app.json');
479
511
  if (devboxCfg) {
480
- const devboxDownloads = buildDevboxDownloads(devboxCfg);
481
- app.get('/downloads/devbox/latest.json', (_req, res) => {
482
- // Short TTL: a release bump (config change + deploy) is visible within a
483
- // minute, same posture as the hall's HTML.
484
- res.set('Cache-Control', 'public, max-age=60');
485
- res.json(devboxDownloads.latestJson());
486
- });
487
- // #924: the lightweight update-check endpoint the Dev Box app polls + the hall
488
- // /settings courtesy line reads. MUST be registered before the `:target` route
489
- // below, or Express would treat "version" as a download target and 404 it.
490
- app.get('/downloads/devbox/version', (_req, res) => {
491
- res.set('Cache-Control', 'public, max-age=60');
492
- res.json(devboxDownloads.versionJson());
493
- });
494
- app.get('/downloads/devbox/:target', (req, res) => {
495
- const url = devboxDownloads.redirectFor(req.params.target);
496
- if (!url) return res.fail('unknown_target', 404, { targets: Object.keys(devboxDownloads.latestJson().assets) });
497
- res.set('Cache-Control', 'public, max-age=60');
498
- res.redirect(302, url);
499
- });
512
+ mountDevboxDownloads(app, buildDevboxDownloads(devboxCfg));
500
513
  }
501
514
 
502
515
  // Standalone `bongos` CLI downloads + the one-line installer (task 1248,
@@ -979,4 +992,4 @@ function mountInternalSurfaces(app) {
979
992
  return app;
980
993
  }
981
994
 
982
- module.exports = { mountInternalSurfaces, isStatusHost, isBuildersHost, injectBrandingGlobal, injectBrandingInto, injectModuleHallWidgets, themeMarkup, applyBrandTokens, resolveOpenapiJson, openapiJson, tryLoadInstanceConfig };
995
+ module.exports = { mountInternalSurfaces, mountDevboxDownloads, isStatusHost, isBuildersHost, injectBrandingGlobal, injectBrandingInto, injectModuleHallWidgets, themeMarkup, applyBrandTokens, resolveOpenapiJson, openapiJson, tryLoadInstanceConfig };
package/src/module-api.js CHANGED
@@ -529,8 +529,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
529
529
  // landed since 1.19.49 with no explicit bump. run 31524280584. (task 1002620)
530
530
  // 1.19.51 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
531
531
  // landed since 1.19.50 with no explicit bump. run 31525032273. (task 1002620)
532
+ // 1.19.52 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
533
+ // landed since 1.19.51 with no explicit bump. run 31526131183. (task 1002620)
532
534
  // ---------------------------------------------------------------------------
533
- const CORE_VERSION = '1.19.51'; // CI auto-patch carrier (ADR 0161); see changelog above
535
+ const CORE_VERSION = '1.19.52'; // CI auto-patch carrier (ADR 0161); see changelog above
534
536
 
535
537
  // A namespaced logger so a module's log lines are attributable + consistent.
536
538
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -119,5 +119,22 @@ t('config/devbox-app.json parses, resolves every target, and aliases point at re
119
119
  }
120
120
  });
121
121
 
122
+ console.log('update manifest (task 1002649 — the payload the devbox-app updater reads):');
123
+
124
+ t('committed config yields a NON-EMPTY manifest — version + at least one asset in both shapes', () => {
125
+ // The live symptom behind task 1002649 was latest.json arriving as an empty
126
+ // body. The route is a pure function of this committed config, so pin here
127
+ // that the manifest can never be empty at the source.
128
+ const d = buildDevboxDownloads(committedConfig);
129
+ for (const shape of [d.latestJson(), d.versionJson()]) {
130
+ assert.match(shape.version, /^\d+\.\d+\.\d+$/, 'manifest carries a semver version');
131
+ assert.ok(Object.keys(shape.assets).length >= 1, 'manifest carries at least one asset');
132
+ }
133
+ });
134
+
135
+ // The client-path pin (extensionless /version, never .json) lives in
136
+ // tests/devbox_update_path.mjs — NOT here — so it still runs on the neutral
137
+ // core checkout where this file topology-skips (task 1002649).
138
+
122
139
  console.log(`\n${passed} passed, ${failed} failed`);
123
140
  process.exit(failed === 0 ? 0 : 1);
@@ -0,0 +1,84 @@
1
+ // tests/devbox_update_path.mjs — the devbox-app update-check path pin (task 1002649).
2
+ //
3
+ // Deliberately SEPARATE from tests/devbox_downloads.mjs: that file topology-
4
+ // skips wherever config/devbox-app.json is absent (this neutral core checkout),
5
+ // but this pin reads only the app client's SOURCE, so it must run everywhere —
6
+ // including this repo's `unit` lane.
7
+ //
8
+ // Why the pin exists: a front proxy with a file-matcher can eat *.json paths
9
+ // before they reach Node — the live pre-ADR-0152 vhost 404'd
10
+ // /downloads/devbox/latest.json with an empty body while the extensionless
11
+ // /downloads/devbox/version twin worked, silently breaking the app's update
12
+ // check. The updater only needs { version, assets }, which /version carries.
13
+ //
14
+ // Run: node --test tests/devbox_update_path.mjs
15
+
16
+ import { strict as assert } from 'node:assert';
17
+ import { test } from 'node:test';
18
+ import { readFileSync } from 'node:fs';
19
+ import { createRequire } from 'node:module';
20
+
21
+ const require = createRequire(import.meta.url);
22
+ const src = readFileSync(new URL('../modules/dev-box/app/src/gds-client.js', import.meta.url), 'utf8');
23
+
24
+ test('latestRelease polls the extensionless /downloads/devbox/version, never a .json path', () => {
25
+ const line = src.split('\n').find((l) => l.includes('latestRelease:'));
26
+ assert.ok(line, 'gds-client exposes latestRelease');
27
+ assert.ok(line.includes("'/downloads/devbox/version'"), 'latestRelease polls /downloads/devbox/version');
28
+ assert.ok(!line.includes('latest.json'), 'latestRelease must not poll a .json-suffixed path');
29
+ });
30
+
31
+ // ---- The ACTUAL route handlers, over loopback (the docs_openapi_route ----
32
+ // pattern). This is the layer the repo owns: the Express routes must serve a
33
+ // NON-EMPTY manifest on all three paths and redirect targets. (The infra layer
34
+ // above Node — the host's front proxy — cannot be pinned from a unit test;
35
+ // that is blocker 1000105. These pins guarantee that when a request reaches
36
+ // Node, the manifest is never empty.)
37
+ const CFG = {
38
+ releases_repo: 'owner/devbox-releases',
39
+ version: '9.9.9',
40
+ assets: { 'win-x64': 'Setup-9.9.9-x64.exe', 'mac-arm64': 'App-9.9.9-arm64.dmg' },
41
+ aliases: { windows: 'win-x64', mac: 'mac-arm64' },
42
+ };
43
+
44
+ async function withDevboxServer(fn) {
45
+ const express = require('express');
46
+ const { mountDevboxDownloads } = require('../src/bongos/serve-internal.js');
47
+ const { buildDevboxDownloads } = require('../src/devbox-downloads.js');
48
+ const app = express();
49
+ mountDevboxDownloads(app, buildDevboxDownloads(CFG));
50
+ const server = await new Promise((resolve) => { const s = app.listen(0, () => resolve(s)); });
51
+ try {
52
+ await fn(`http://127.0.0.1:${server.address().port}`);
53
+ } finally {
54
+ await new Promise((r) => server.close(r));
55
+ }
56
+ }
57
+
58
+ test('latest.json and the extensionless /latest twin both serve the full non-empty manifest', async () => {
59
+ await withDevboxServer(async (base) => {
60
+ for (const p of ['/downloads/devbox/latest.json', '/downloads/devbox/latest']) {
61
+ const r = await fetch(base + p);
62
+ assert.equal(r.status, 200, `${p} must 200`);
63
+ assert.match(r.headers.get('content-type') || '', /application\/json/, `${p} content-type`);
64
+ const j = await r.json();
65
+ assert.equal(j.version, '9.9.9');
66
+ assert.ok(Object.keys(j.assets).length >= 1, `${p} manifest must carry assets — the task-1002649 empty-body class`);
67
+ assert.equal(j.assets['win-x64'].file, 'Setup-9.9.9-x64.exe');
68
+ assert.match(j.assets['win-x64'].url, /\/releases\/download\/v9\.9\.9\//);
69
+ }
70
+ });
71
+ });
72
+
73
+ test('/version serves the flat update-check payload; a target redirects 302', async () => {
74
+ await withDevboxServer(async (base) => {
75
+ const v = await fetch(base + '/downloads/devbox/version');
76
+ assert.equal(v.status, 200);
77
+ const j = await v.json();
78
+ assert.equal(j.version, '9.9.9');
79
+ assert.equal(typeof j.assets['win-x64'], 'string', 'flat {target: url} shape');
80
+ const r = await fetch(base + '/downloads/devbox/windows', { redirect: 'manual' });
81
+ assert.equal(r.status, 302);
82
+ assert.match(r.headers.get('location') || '', /Setup-9\.9\.9-x64\.exe$/);
83
+ });
84
+ });