@bongos/core 1.20.39 → 1.20.40

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.20.39",
6
- "core_contract": "1.20.39",
7
- "source_commit": "5d6bec248ce87fac41937a1171ac6743ff471488",
5
+ "core_version": "1.20.40",
6
+ "core_contract": "1.20.40",
7
+ "source_commit": "b2b1ab60800736f857d25d84bf91ee5bd53153fb",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-10-01T00:27:49.636Z",
9
+ "built_at": "2026-10-01T00:37:27.302Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 564,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 3274,
20
- "tree_sha256": "ff380e8ab7ab557d485314ddacf18761ea6c419e7ad119f3537bbfb3d793dd58",
20
+ "tree_sha256": "c0d69a1151540d8702aca870e7ac26d1ed5e95c2b60eafed5a232c20fde50f71",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2222,7 +2222,7 @@
2222
2222
  {
2223
2223
  "path": "docs/adr/0349-a-version-preview-is-a-sandboxed-child-the-web-tier-launches.md",
2224
2224
  "mode": "0000644",
2225
- "sha256": "1c25e4c012e5321ffb2169e871fcbb2cea7bc823656959a6872a0af428674f3d"
2225
+ "sha256": "7fe345d475d9540181d12544606c1c2cbfd5510e94c8fbf3599fc48d2790b3cb"
2226
2226
  },
2227
2227
  {
2228
2228
  "path": "docs/adr/0350-the-hub-holds-a-per-project-write-deploy-key-so-a-hosted-upgrade-reaches-github.md",
@@ -2812,7 +2812,7 @@
2812
2812
  {
2813
2813
  "path": "docs/module-api-changelog.md",
2814
2814
  "mode": "0000644",
2815
- "sha256": "086a0fac7811e72373874b93837f2920a3a8b1c0505dd58d301005cebd238569"
2815
+ "sha256": "151589d412762bf79fe8fae3f48ae9ceb0a0052c0159860a1ed02a670101690c"
2816
2816
  },
2817
2817
  {
2818
2818
  "path": "docs/modules-contract.md",
@@ -6777,7 +6777,7 @@
6777
6777
  {
6778
6778
  "path": "modules/npm-release/preview/divert.js",
6779
6779
  "mode": "0000644",
6780
- "sha256": "f8b716868ff00ae1f14e38c4d2e8ddd32c660615b1c2b52a06d3b9b186c01825"
6780
+ "sha256": "d7dd379f30685df8874102c40c4e68f1e7547ed85f2ec7bbaf3d98e0a7528bed"
6781
6781
  },
6782
6782
  {
6783
6783
  "path": "modules/npm-release/preview/env.js",
@@ -6787,7 +6787,7 @@
6787
6787
  {
6788
6788
  "path": "modules/npm-release/preview/proxy.js",
6789
6789
  "mode": "0000644",
6790
- "sha256": "6c8a352990ffc241ab83075b4cb8f216a8d03cdcd39d19e99cb1a4eb25b55778"
6790
+ "sha256": "8b68575faf04ee2905b31a9b9e2e584723666d313612ab80be3a189ea10c6f0d"
6791
6791
  },
6792
6792
  {
6793
6793
  "path": "modules/npm-release/preview/runtime.js",
@@ -6817,7 +6817,7 @@
6817
6817
  {
6818
6818
  "path": "modules/npm-release/routes/preview.js",
6819
6819
  "mode": "0000644",
6820
- "sha256": "4bdc69fff014efe3c0a9535c2dbd25fc800fca723a097ee9992370a47c6bedfc"
6820
+ "sha256": "bb2d1b5db4a13949d69177ad10bdd9d4a3e6ad77710317317e764358bc150551"
6821
6821
  },
6822
6822
  {
6823
6823
  "path": "modules/npm-release/routes/task-where.js",
@@ -9022,12 +9022,12 @@
9022
9022
  {
9023
9023
  "path": "package-lock.json",
9024
9024
  "mode": "0000644",
9025
- "sha256": "e95ad9a27eb5f55ee344edbb3c5fc6280d6f58f1e6624f1a296a869439aa1cbf"
9025
+ "sha256": "9f46a8f738d89eb715a930ddb554bf6b1bd3d027a0851e356aed3c4545354f43"
9026
9026
  },
9027
9027
  {
9028
9028
  "path": "package.json",
9029
9029
  "mode": "0000644",
9030
- "sha256": "994effd15c335b602139f038ec2a0daabe8fa2d196660a4d187ff513a0369065"
9030
+ "sha256": "b504322a4ccdf13821f15b0c09de9c9c44383bdfa54457925772af80d80445c7"
9031
9031
  },
9032
9032
  {
9033
9033
  "path": "public-docs/index.html",
@@ -9047,7 +9047,7 @@
9047
9047
  {
9048
9048
  "path": "release-notes.json",
9049
9049
  "mode": "0000644",
9050
- "sha256": "ac05eeee55a8e591437c2b14ce073b89b4717077b0eb3a9fb73b017c732b3a29"
9050
+ "sha256": "88bbf251ccd70e10dff2d818ed34eb8047ee80075856cde2da929f473ea03e89"
9051
9051
  },
9052
9052
  {
9053
9053
  "path": "scripts/bongos-mcp.js",
@@ -11202,7 +11202,7 @@
11202
11202
  {
11203
11203
  "path": "src/module-api.js",
11204
11204
  "mode": "0000644",
11205
- "sha256": "04dc5e05b911d167797ae2aeff9f250c978866d515fd95c74dc68569fa6309ce"
11205
+ "sha256": "e3bc53481c5c7f502e3fb43c06d8ceadae02c0c57edb504aab696fff1e50af7d"
11206
11206
  },
11207
11207
  {
11208
11208
  "path": "src/module-loader/catalog.js",
@@ -14357,7 +14357,7 @@
14357
14357
  {
14358
14358
  "path": "tests/npm_release_preview_divert.mjs",
14359
14359
  "mode": "0000644",
14360
- "sha256": "be847731e116e9f5f15392668111596cbcbab74cae4f3da4ab92d780647cc9af"
14360
+ "sha256": "154a7246124d65e9c0c487b9fd24b23d6786d2e9879cb9454ecdde50c618700d"
14361
14361
  },
14362
14362
  {
14363
14363
  "path": "tests/npm_release_preview_env.mjs",
@@ -14367,12 +14367,12 @@
14367
14367
  {
14368
14368
  "path": "tests/npm_release_preview_proxy.mjs",
14369
14369
  "mode": "0000644",
14370
- "sha256": "406a1183b9a799a79176191d510aa676558d569bcc6057f6164cb8857c56f4a3"
14370
+ "sha256": "5aeb98a740acf5dc96ad456f72dfe7616a53a5a4c0ba306fa24764b5df299881"
14371
14371
  },
14372
14372
  {
14373
14373
  "path": "tests/npm_release_preview_routes.mjs",
14374
14374
  "mode": "0000644",
14375
- "sha256": "6e1e70a9917711a76e50f3b89d1f265fd0a169e5d0b3158f591e835cb74c2851"
14375
+ "sha256": "187d0c93077dce3321c8063ccd04ea6033e46ee868e9530c9d6eaf5092b926a4"
14376
14376
  },
14377
14377
  {
14378
14378
  "path": "tests/npm_release_preview_supervisor.mjs",
@@ -96,6 +96,7 @@ What keeps that exception narrow:
96
96
  IPAddressDeny` is the follow-up and needs the droplet.
97
97
  - A preview started by the live process is stopped when that process exits, but the copy is
98
98
  dropped only by the next start or stop. It is one fixed name, so it cannot pile up.
99
+ - **Correction, 2026-09-30 (task 1004466): the proxy strips every upstream `Set-Cookie`.** Same origin means a cookie the child set replaced the live hall's own (`cb_stealth`, the session) and locked the owner out of `/deploy`. The child's cookies now stay with the child; the steering cookie is the only one this module writes, and its clear repeats the set's Path, SameSite and Secure.
99
100
 
100
101
  ## Consequences
101
102
 
@@ -2735,5 +2735,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
2735
2735
  landed since 1.20.37 with no explicit bump. run 36794921587. (task 1002620)
2736
2736
  1.20.39 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2737
2737
  landed since 1.20.38 with no explicit bump. run 36796243918. (task 1002620)
2738
+ 1.20.40 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2739
+ landed since 1.20.39 with no explicit bump. run 36797045221. (task 1002620)
2738
2740
  ---------------------------------------------------------------------------
2739
2741
  ```
@@ -44,9 +44,19 @@ function pathOf(req) {
44
44
  return q === -1 ? url : url.slice(0, q);
45
45
  }
46
46
 
47
- /** The Set-Cookie value that removes the steering cookie. */
48
- function clearCookie() {
49
- return `${COOKIE_NAME}=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax`;
47
+ /** Path/HttpOnly/SameSite/Secure the steering cookie is set with; the clear repeats them. */
48
+ function steerFlags(req) {
49
+ return `HttpOnly; SameSite=Lax${req && req.secure ? '; Secure' : ''}`;
50
+ }
51
+
52
+ /** The Set-Cookie value that sets the steering cookie. */
53
+ function setCookie(version, req) {
54
+ return `${COOKIE_NAME}=${version}; Path=/; ${steerFlags(req)}`;
55
+ }
56
+
57
+ /** The Set-Cookie value that removes it: same Path, SameSite and Secure as the set (task 1004466). */
58
+ function clearCookie(req) {
59
+ return `${COOKIE_NAME}=; Path=/; Max-Age=0; ${steerFlags(req)}`;
50
60
  }
51
61
 
52
62
  // Run an express-style gate to its verdict without letting it write a response of its own:
@@ -77,14 +87,14 @@ function createDivert({ supervisor, forward, requireBuilder, requirePermission }
77
87
  const st = supervisor.status();
78
88
  if (st.state !== 'running' || st.version !== wanted) {
79
89
  // The preview this cookie pointed at is gone: drop the cookie and serve the live hall.
80
- res.append('Set-Cookie', clearCookie());
90
+ res.append('Set-Cookie', clearCookie(req));
81
91
  return next();
82
92
  }
83
93
  const p = pathOf(req);
84
94
  if (AUTH_PATH.test(p) || PREVIEW_PATH.test(p)) return next();
85
95
  if (req.headers.authorization) return next();
86
96
  if (!(await passes(requireBuilder, req)) || !(await passes(mayPreview, req))) {
87
- res.append('Set-Cookie', clearCookie());
97
+ res.append('Set-Cookie', clearCookie(req));
88
98
  return next();
89
99
  }
90
100
  supervisor.touch();
@@ -92,4 +102,4 @@ function createDivert({ supervisor, forward, requireBuilder, requirePermission }
92
102
  };
93
103
  }
94
104
 
95
- module.exports = { createDivert, cookieVersion, clearCookie, AUTH_PATH, PREVIEW_PATH };
105
+ module.exports = { createDivert, cookieVersion, clearCookie, setCookie, AUTH_PATH, PREVIEW_PATH };
@@ -74,7 +74,10 @@ function createProxy({ port, banner, exitHref, httpImpl = http } = {}) {
74
74
  { host: '127.0.0.1', port: port(), method: req.method, path: req.originalUrl || req.url, headers },
75
75
  (upRes) => {
76
76
  const out = {};
77
- for (const [k, v] of Object.entries(upRes.headers)) if (!HOP.has(k)) out[k] = v;
77
+ // SET-COOKIE NEVER CROSSES (task 1004466). The preview shares the live hall's origin,
78
+ // so a cookie it set would overwrite the live hall's own (cb_stealth, the session,
79
+ // the steering cookie) and lock its owner out. Its cookies stay with the child.
80
+ for (const [k, v] of Object.entries(upRes.headers)) if (!HOP.has(k) && k.toLowerCase() !== 'set-cookie') out[k] = v;
78
81
  const isPage = /^text\/html\b/i.test(String(upRes.headers['content-type'] || ''));
79
82
  if (!isPage) {
80
83
  res.writeHead(upRes.statusCode, out);
@@ -20,8 +20,8 @@
20
20
 
21
21
  const express = require('express');
22
22
  const api = require('../../../src/module-api');
23
- const { createDivert, clearCookie } = require('../preview/divert');
24
- const { createProxy, COOKIE_NAME } = require('../preview/proxy');
23
+ const { createDivert, clearCookie, setCookie } = require('../preview/divert');
24
+ const { createProxy } = require('../preview/proxy');
25
25
  const { previewable } = require('../preview/supervisor');
26
26
  const { VERSION_RE, PACKAGE } = require('../preview/commands');
27
27
 
@@ -119,14 +119,15 @@ module.exports = function buildNpmReleasePreviewRouter({
119
119
  if (!VERSION_RE.test(v) || st.state !== 'running' || st.version !== v) {
120
120
  return failure(res, 409, 'preview_not_running', 'That preview is not running.');
121
121
  }
122
- const secure = req.secure ? '; Secure' : '';
123
- res.append('Set-Cookie', `${COOKIE_NAME}=${v}; Path=/; HttpOnly; SameSite=Lax${secure}`);
122
+ res.append('Set-Cookie', setCookie(v, req));
123
+ res.set('Cache-Control', 'no-store');
124
124
  sup.touch();
125
125
  return res.redirect(302, '/');
126
126
  });
127
127
 
128
128
  router.get('/npm-release/preview/exit', api.requireBuilder, mayPreview, (req, res) => {
129
- res.append('Set-Cookie', clearCookie());
129
+ res.append('Set-Cookie', clearCookie(req));
130
+ res.set('Cache-Control', 'no-store');
130
131
  res.redirect(302, '/deploy');
131
132
  });
132
133
 
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.20.39",
3
+ "version": "1.20.40",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.20.39",
9
+ "version": "1.20.40",
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.20.39",
3
+ "version": "1.20.40",
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",
@@ -8343,5 +8343,11 @@
8343
8343
  "id": "1004464",
8344
8344
  "text": "Setting up an app on Render from the project wizard no longer fails when the owner leaves the build command empty. The box now comes filled in with Render's usual build command for the chosen language, which the owner can chan"
8345
8345
  }
8346
+ ],
8347
+ "1.20.40": [
8348
+ {
8349
+ "id": "1004466",
8350
+ "text": "Stop the version preview from overwriting the live hall's cookies, and make leaving a preview reliably clear it."
8351
+ }
8346
8352
  ]
8347
8353
  }
package/src/module-api.js CHANGED
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
75
75
  // MAJOR (see allowBoxScope below): passes the request through untouched.
76
76
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
77
77
 
78
- const CORE_VERSION = '1.20.39'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.20.40'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
79
79
 
80
80
  // A namespaced logger so a module's log lines are attributable + consistent.
81
81
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -159,6 +159,16 @@ test('a cookie for a preview that is not running is dropped and the live hall an
159
159
  }
160
160
  });
161
161
 
162
+ test('the clearing cookie repeats the set attributes, Secure included (task 1004466)', () => {
163
+ const { setCookie } = require('../modules/npm-release/preview/divert.js');
164
+ for (const secure of [false, true]) {
165
+ const req = { secure };
166
+ const attrs = (c) => c.split('; ').slice(1).filter((a) => !/^Max-Age=/.test(a)).join('; ');
167
+ assert.equal(attrs(clearCookie(req)), attrs(setCookie('1.19.1000', req)), 'secure=' + secure);
168
+ assert.equal(/; Secure$/.test(clearCookie(req)), secure);
169
+ }
170
+ });
171
+
162
172
  test('the clearing cookie expires the steering cookie for the whole site', () => {
163
173
  assert.match(clearCookie(), /^bongos_preview=; Path=\/; Max-Age=0/);
164
174
  });
@@ -137,6 +137,32 @@ test('JSON and other non-page answers pass through untouched, headers included',
137
137
  assert.doesNotMatch(res.text, /banner/);
138
138
  });
139
139
 
140
+ test('a Set-Cookie from the preview never reaches the browser (it would overwrite the live hall cookies), task 1004466', async () => {
141
+ const cookies = ['cb_stealth=child; Path=/', '__Host-sid=child; Path=/; Secure', 'bongos_preview=; Max-Age=0'];
142
+ for (const [type, body] of [['application/json', '{}'], ['text/html; charset=utf-8', '<html><body>x</body></html>']]) {
143
+ const http = fakeHttp(() => ({ headers: { 'content-type': type, 'set-cookie': cookies, 'x-keep': 'yes' }, body }));
144
+ const res = new FakeRes();
145
+ make(http)(request(), res);
146
+ await finish(res);
147
+ assert.equal(Object.keys(res.headers).filter((k) => k.toLowerCase() === 'set-cookie').length, 0, type);
148
+ assert.equal(res.headers['x-keep'], 'yes', 'other headers still pass');
149
+ }
150
+ });
151
+
152
+ test('a streamed answer with a Set-Cookie is still streamed, cookie dropped', async () => {
153
+ const stream = new PassThrough();
154
+ const http = fakeHttp(() => ({ headers: { 'content-type': 'text/event-stream', 'set-cookie': 'cb_stealth=child' }, stream }));
155
+ const res = new FakeRes();
156
+ make(http)(request(), res);
157
+ await tick();
158
+ stream.write('data: one\n\n');
159
+ await tick();
160
+ assert.equal(res.headers['set-cookie'], undefined);
161
+ assert.equal(res.text, 'data: one\n\n');
162
+ stream.end();
163
+ await finish(res);
164
+ });
165
+
140
166
  test('an event stream is streamed, not held: the first chunk arrives before the stream ends', async () => {
141
167
  const stream = new PassThrough();
142
168
  const http = fakeHttp(() => ({ headers: { 'content-type': 'text/event-stream' }, stream }));
@@ -52,6 +52,7 @@ let base;
52
52
  before(async () => {
53
53
  state = { state: 'idle', version: null };
54
54
  const app = express();
55
+ app.set('trust proxy', true);
55
56
  app.use(express.json());
56
57
  app.use('/api/bongos', buildRouter({ supervisor, listPublished: async () => registry, runningVersion: () => '1.19.1040' }));
57
58
  server = http.createServer(app);
@@ -60,10 +61,10 @@ before(async () => {
60
61
  });
61
62
  after(() => new Promise((r) => server.close(r)));
62
63
 
63
- function call(method, url, { cookie = 'sid=good', body } = {}) {
64
+ function call(method, url, { cookie = 'sid=good', body, extra } = {}) {
64
65
  return new Promise((resolve, reject) => {
65
66
  const data = body === undefined ? null : JSON.stringify(body);
66
- const headers = { host: 'builders.example.test' };
67
+ const headers = { host: 'builders.example.test', ...(extra || {}) };
67
68
  if (cookie) headers.cookie = cookie;
68
69
  if (data) { headers['content-type'] = 'application/json'; headers['content-length'] = Buffer.byteLength(data); }
69
70
  const r = http.request({ host: '127.0.0.1', port: base, path: '/api/bongos' + url, method, headers }, (res) => {
@@ -178,6 +179,18 @@ test('enter sets the steering cookie for the running version and goes to the hal
178
179
  assert.match(c, /^bongos_preview=1\.19\.1030; Path=\/; HttpOnly; SameSite=Lax/);
179
180
  });
180
181
 
182
+ test('exit clears with the same attributes enter set, Secure included, and is not cached (task 1004466)', async () => {
183
+ state = { state: 'running', version: '1.19.1030' };
184
+ for (const extra of [undefined, { 'x-forwarded-proto': 'https' }]) {
185
+ const e = await call('GET', '/npm-release/preview/enter?v=1.19.1030', { extra });
186
+ const x = await call('GET', '/npm-release/preview/exit', { extra });
187
+ const attrs = (r) => [].concat(r.headers['set-cookie'])[0].split('; ').slice(1).filter((a) => !/^Max-Age=/.test(a)).join('; ');
188
+ assert.equal(attrs(x), attrs(e));
189
+ assert.equal(/Secure/.test(attrs(x)), Boolean(extra));
190
+ assert.equal(x.headers['cache-control'], 'no-store');
191
+ }
192
+ });
193
+
181
194
  test('enter refuses a preview that is not the one running, and sets no cookie', async () => {
182
195
  for (const [st, v] of [[{ state: 'idle', version: null }, '1.19.1030'], [{ state: 'starting', version: '1.19.1030' }, '1.19.1030'], [{ state: 'running', version: '1.19.1031' }, '1.19.1030'], [{ state: 'running', version: '1.19.1030' }, 'zzz']]) {
183
196
  state = st;