@bongos/core 1.19.98 → 1.19.99

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,11 +2,11 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.98",
6
- "core_contract": "1.19.98",
7
- "source_commit": "6616c83d7f0ee7029db21b183e3983fc16568aad",
5
+ "core_version": "1.19.99",
6
+ "core_contract": "1.19.99",
7
+ "source_commit": "7ab95a1bcf1751dbc3f5890c41461dcad6826506",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-13T22:25:33.504Z",
9
+ "built_at": "2026-08-13T22:41:45.519Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 298,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 1549,
20
- "tree_sha256": "729068034b75cf1c5878e8099c36f949384e07228d6337ea7387c7d586abd68e",
20
+ "tree_sha256": "37c2c9f1fb605b8e730ce3562abac5ed38699596c45f2113f0066464fb12b85f",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -4127,12 +4127,12 @@
4127
4127
  {
4128
4128
  "path": "package-lock.json",
4129
4129
  "mode": "0000644",
4130
- "sha256": "1fac3202890f9d9899a8aaa72a473e4f73b6c27dccc28dd7a064a3c5b1367b85"
4130
+ "sha256": "1df1aa5e45f918110c6adebcec118d46207c09bfadd5bf17ddc1b563cb67d8f7"
4131
4131
  },
4132
4132
  {
4133
4133
  "path": "package.json",
4134
4134
  "mode": "0000644",
4135
- "sha256": "b8338943bf583ab9caa3686836ebeafb43be12df7f156f991763278abcca02dc"
4135
+ "sha256": "4cf100a9e30702e1e1bdcb42c3437e2ce4fe61795bca9c305ce08fb17c42438d"
4136
4136
  },
4137
4137
  {
4138
4138
  "path": "public-docs/index.html",
@@ -5702,7 +5702,7 @@
5702
5702
  {
5703
5703
  "path": "src/module-api.js",
5704
5704
  "mode": "0000644",
5705
- "sha256": "fb7a891d2f7b815e57271245e93c4e4ede5f78bedb76e4648fcbf56b69ad3de3"
5705
+ "sha256": "896f7912e0981f27a671fb68d5cada8481f84e2cfc1025c3cb3c69eda95f7fe9"
5706
5706
  },
5707
5707
  {
5708
5708
  "path": "src/module-loader/loader.js",
@@ -5737,7 +5737,7 @@
5737
5737
  {
5738
5738
  "path": "src/platform-server.js",
5739
5739
  "mode": "0000644",
5740
- "sha256": "181c9850c374b653136a259d80e276ac285d2ee5f55fadeb9109aef418190d91"
5740
+ "sha256": "6ffe23398bc452b4224e7c4aa49dbd3c4f61a61da0fd51d43bb6c5ce0bd51aa7"
5741
5741
  },
5742
5742
  {
5743
5743
  "path": "src/ui/adapters/claude-design/index.js",
@@ -7247,7 +7247,7 @@
7247
7247
  {
7248
7248
  "path": "tests/public_landing_projects.mjs",
7249
7249
  "mode": "0000644",
7250
- "sha256": "489ad330f7fe118ffd66a22b0a437743a2f3d3610a5324ba764abc4fc88b0c74"
7250
+ "sha256": "e03bbf258cb4b8ffd3e242ab2ddd927d1f52706347d06b539a386ca502caf1e4"
7251
7251
  },
7252
7252
  {
7253
7253
  "path": "tests/public_mirror_export.mjs",
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.98",
3
+ "version": "1.19.99",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.98",
9
+ "version": "1.19.99",
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.98",
3
+ "version": "1.19.99",
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/src/module-api.js CHANGED
@@ -623,8 +623,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
623
623
  // landed since 1.19.96 with no explicit bump. run 31749223878. (task 1002620)
624
624
  // 1.19.98 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
625
625
  // landed since 1.19.97 with no explicit bump. run 31749841394. (task 1002620)
626
+ // 1.19.99 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
627
+ // landed since 1.19.98 with no explicit bump. run 31750941866. (task 1002620)
626
628
  // ---------------------------------------------------------------------------
627
- const CORE_VERSION = '1.19.98'; // CI auto-patch carrier (ADR 0161); see changelog above
629
+ const CORE_VERSION = '1.19.99'; // CI auto-patch carrier (ADR 0161); see changelog above
628
630
 
629
631
  // A namespaced logger so a module's log lines are attributable + consistent.
630
632
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -60,12 +60,58 @@ const HOST = process.env.HOST || '127.0.0.1';
60
60
  // branding pack is injected once at boot (theme + window.__BRANDING__), exactly
61
61
  // as the hall pages are, so the look + identity come from config, not hardcode.
62
62
  const LANDING_DIR = path.join(__dirname, '..', 'public-landing');
63
+ // ── CONTENT-STAMPED STATIC ASSETS (task 1002766) ──
64
+ // The landing HTML is `cf-cache-status: DYNAMIC` and never cached, but the
65
+ // assets it references come back `Cache-Control: public, max-age=14400` — four
66
+ // hours, Cloudflare's default Browser Cache TTL, not a header this app sets. So
67
+ // after a deploy a RETURNING visitor gets brand-new markup styled by a
68
+ // stylesheet up to four hours old: markup whose components that stylesheet has
69
+ // never heard of. Measured live — a cached cosmos.css of 25,693 bytes with no
70
+ // `.shell{` against 42,610 fresh — and again when the project card shipped and
71
+ // rendered as an unstyled block at the foot of the page.
72
+ //
73
+ // A COLD visitor is unaffected, which is exactly what makes it easy to miss:
74
+ // whoever deploys usually reloads in the browser holding the stale copy.
75
+ //
76
+ // Fixed by changing the URL rather than by fighting the header: a content hash
77
+ // means a new core produces a new URL, so there is nothing to invalidate and no
78
+ // Cloudflare purge token to hold (task 1002766 weighed both). Same `?v=<sha8>`
79
+ // shape serve-internal.js already stamps onto the hall's own scripts.
80
+ //
81
+ // Stamped by RESOLUTION, not by a path allow-list: a URL is an asset if it
82
+ // actually names a file under public-landing/. Routes (`/projects`, `/create`)
83
+ // and API paths resolve to nothing and are left alone, so a new asset is
84
+ // stamped the day it is added and nobody has to remember to list it.
85
+ const _assetStamps = new Map();
86
+ function assetStamp(url) {
87
+ if (!_assetStamps.has(url)) {
88
+ let v = '';
89
+ try {
90
+ const abs = path.resolve(LANDING_DIR, '.' + url);
91
+ // never let a template's own text walk out of the landing dir
92
+ if (abs.startsWith(LANDING_DIR + path.sep)) {
93
+ v = require('node:crypto').createHash('sha256')
94
+ .update(fs.readFileSync(abs)).digest('hex').slice(0, 8);
95
+ }
96
+ } catch { v = ''; } // not a file → not an asset → no stamp
97
+ _assetStamps.set(url, v);
98
+ }
99
+ return _assetStamps.get(url);
100
+ }
101
+ const LOCAL_ASSET = /(href|src)="(\/[A-Za-z0-9._~/-]+\.[A-Za-z0-9]+)"/g;
102
+ function stampAssets(html) {
103
+ return String(html).replace(LOCAL_ASSET, (m, attr, url) =>
104
+ assetStamp(url) ? `${attr}="${url}?v=${assetStamp(url)}"` : m);
105
+ }
106
+
63
107
  const _landingPages = new Map(); // memoized per file; '' if absent (instance ships no landing)
64
108
  function landingPage(file) {
65
109
  if (!_landingPages.has(file)) {
66
110
  let html = '';
67
111
  try {
68
- html = injectBrandingGlobal(stripApexOnly(fs.readFileSync(path.join(LANDING_DIR, file), 'utf8')));
112
+ // stamped LAST, so anything a later transform introduces is stamped too
113
+ html = stampAssets(injectBrandingGlobal(stripApexOnly(
114
+ fs.readFileSync(path.join(LANDING_DIR, file), 'utf8'))));
69
115
  } catch { /* absent → '' */ }
70
116
  _landingPages.set(file, html);
71
117
  }
@@ -119,3 +119,62 @@ test('create.html: the poll give-up points at /projects — "the hall has it" is
119
119
  assert.match(body, /\/projects/, 'the give-up path must hand the owner the projects page');
120
120
  assert.doesNotMatch(body, /the hall has it/, 'the false poll-stop copy must be gone');
121
121
  });
122
+
123
+ /* ── Content-stamped assets (task 1002766) ──────────────────────────────────
124
+ The landing HTML is never cached but its assets come back max-age=14400 —
125
+ Cloudflare's default browser TTL, not ours. So a returning visitor gets new
126
+ markup with up to four-hour-old CSS: markup whose components that stylesheet
127
+ has never heard of. Twice now that has shipped a visibly broken page. The URL
128
+ carries the version so there is nothing to invalidate. ── */
129
+
130
+ test('the stylesheet URL carries a content stamp, so new markup cannot meet old CSS', async () => {
131
+ const res = await request('/projects');
132
+ assert.equal(res.status, 200);
133
+ const m = res.body.match(/href="\/assets\/cosmos\.css\?v=([0-9a-f]+)"/);
134
+ assert.ok(m, 'cosmos.css must be served with a ?v= content stamp');
135
+ assert.equal(m[1].length, 8, 'the stamp is the first 8 hex of a sha256');
136
+ assert.doesNotMatch(res.body, /href="\/assets\/cosmos\.css"/,
137
+ 'no unstamped copy may survive alongside the stamped one');
138
+ });
139
+
140
+ test('the stamp is derived from CONTENT, so it changes only when the file does', async () => {
141
+ const fsMod = await import('node:fs');
142
+ const pathMod = await import('node:path');
143
+ const cryptoMod = await import('node:crypto');
144
+ const url = await import('node:url');
145
+ const root = pathMod.dirname(url.fileURLToPath(import.meta.url));
146
+ const css = fsMod.readFileSync(pathMod.join(root, '..', 'public-landing', 'assets', 'cosmos.css'));
147
+ const want = cryptoMod.createHash('sha256').update(css).digest('hex').slice(0, 8);
148
+ const res = await request('/projects');
149
+ assert.ok(res.body.includes(`cosmos.css?v=${want}`),
150
+ 'the stamp must be the hash of the bytes actually on disk');
151
+ });
152
+
153
+ test('every landing surface is stamped, not just the one that broke', async () => {
154
+ for (const p of ['/', '/projects', '/create']) {
155
+ const res = await request(p);
156
+ if (res.status !== 200) continue;
157
+ assert.doesNotMatch(res.body, /(href|src)="\/assets\/[^"?]+"/,
158
+ `${p} still serves an unstamped /assets/ URL`);
159
+ }
160
+ });
161
+
162
+ test('routes and API paths are never mistaken for assets', async () => {
163
+ // stamping resolves the URL against public-landing/ — a route names no file,
164
+ // so it must come back untouched rather than acquiring a meaningless ?v=
165
+ const res = await request('/projects');
166
+ assert.match(res.body, /href="\/create"/, '/create is a route, not an asset');
167
+ assert.doesNotMatch(res.body, /href="\/create\?v=/);
168
+ assert.doesNotMatch(res.body, /href="\/projects\?v=/);
169
+ assert.doesNotMatch(res.body, /\/api\/bongos\/[^"]*\?v=[0-9a-f]{8}/,
170
+ 'API paths must never be stamped');
171
+ });
172
+
173
+ test('a stamped asset still serves — the query string is not part of the filename', async () => {
174
+ const page = await request('/projects');
175
+ const stamped = (page.body.match(/\/assets\/cosmos\.css\?v=[0-9a-f]+/) || [])[0];
176
+ assert.ok(stamped, 'need a stamped URL to fetch');
177
+ const res = await request(stamped);
178
+ assert.equal(res.status, 200, 'express.static must ignore the ?v= when resolving the file');
179
+ assert.match(res.body, /\.shell\{/, 'and it must be the real stylesheet');
180
+ });