@bongos/core 1.20.64 → 1.20.65

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.64",
6
- "core_contract": "1.20.64",
7
- "source_commit": "575676341f5c84eab982a6e546119e8b477740a3",
5
+ "core_version": "1.20.65",
6
+ "core_contract": "1.20.65",
7
+ "source_commit": "d83ddd9aee9444dee8d60cf40c8dcb9b3d32e296",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-10-01T10:53:37.791Z",
9
+ "built_at": "2026-10-01T11:03:38.854Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 572,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 3390,
20
- "tree_sha256": "52d2e7e9c9429a83086dcfccfa49a001131cd68200e4e4c6713eb3cd7f6326c3",
20
+ "tree_sha256": "ede972bceb550b8da7d974492895636275f2f66415178c7632b00d1d85e3dbe0",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2207,7 +2207,7 @@
2207
2207
  {
2208
2208
  "path": "docs/adr/0349-a-version-preview-is-a-sandboxed-child-the-web-tier-launches.md",
2209
2209
  "mode": "0000644",
2210
- "sha256": "7fe345d475d9540181d12544606c1c2cbfd5510e94c8fbf3599fc48d2790b3cb"
2210
+ "sha256": "b609d870b217440a12839ce675617973aac721e9f017e831ff208e06567c9939"
2211
2211
  },
2212
2212
  {
2213
2213
  "path": "docs/adr/0350-the-hub-holds-a-per-project-write-deploy-key-so-a-hosted-upgrade-reaches-github.md",
@@ -2802,7 +2802,7 @@
2802
2802
  {
2803
2803
  "path": "docs/module-api-changelog.md",
2804
2804
  "mode": "0000644",
2805
- "sha256": "7a7ee07e1f53d9a35fb896d8b88c0b03ee3437d7b60dcd85f94c49fe2317bd05"
2805
+ "sha256": "9754d3deec186136216507d8dfc84c40eaf82c4ca40c63f48ad60884527452bf"
2806
2806
  },
2807
2807
  {
2808
2808
  "path": "docs/modules-contract.md",
@@ -7142,7 +7142,7 @@
7142
7142
  {
7143
7143
  "path": "modules/npm-release/preview/env.js",
7144
7144
  "mode": "0000644",
7145
- "sha256": "d9716eea3c3c233eb0397fa0976de9dfcb8a87a8e3b802c5bbea4e5568e691f9"
7145
+ "sha256": "018bdef460587e17cb6bf1fd622a2b14f671a777a1e36e4aeb00e798da5f30c3"
7146
7146
  },
7147
7147
  {
7148
7148
  "path": "modules/npm-release/preview/proxy.js",
@@ -7157,7 +7157,7 @@
7157
7157
  {
7158
7158
  "path": "modules/npm-release/preview/supervisor.js",
7159
7159
  "mode": "0000644",
7160
- "sha256": "fd27648c4d75e03ec541f1295a05ffd05aae957bbf0748215497b624106cf12e"
7160
+ "sha256": "cd708c56ae7bba361c139aba9cfccbe8501843db1c4f54bcd0c0dcf93e76f83c"
7161
7161
  },
7162
7162
  {
7163
7163
  "path": "modules/npm-release/public/task-widget.js",
@@ -9447,12 +9447,12 @@
9447
9447
  {
9448
9448
  "path": "package-lock.json",
9449
9449
  "mode": "0000644",
9450
- "sha256": "e76e52c17163b9b22e508e70795ddc77484f81fd47a2ccc156fd39ea4123c39f"
9450
+ "sha256": "9c2fa6e82319388a9b4b7c68acb6aabf12cf447cc9df068c9351540e39e41f6e"
9451
9451
  },
9452
9452
  {
9453
9453
  "path": "package.json",
9454
9454
  "mode": "0000644",
9455
- "sha256": "456223fa87e0f237ec34208c0019990f0c76a4fadfd6812797812c1d36113125"
9455
+ "sha256": "253fd4502f205668e930fb1863b1dbc47f376462139e3a33809d84bb62157110"
9456
9456
  },
9457
9457
  {
9458
9458
  "path": "public-docs/index.html",
@@ -9472,7 +9472,7 @@
9472
9472
  {
9473
9473
  "path": "release-notes.json",
9474
9474
  "mode": "0000644",
9475
- "sha256": "412a73973976c63d7bf974f72354cdbeae90720c79b923c97f8b1f7d8cb81e9a"
9475
+ "sha256": "e041a1335bad43a910cce4057eaf312ec70054008fef566a5b07f373fbddd8aa"
9476
9476
  },
9477
9477
  {
9478
9478
  "path": "scripts/bongos-mcp.js",
@@ -11652,7 +11652,7 @@
11652
11652
  {
11653
11653
  "path": "src/module-api.js",
11654
11654
  "mode": "0000644",
11655
- "sha256": "e63e10c38215257f0811ee1300826615a5922a000872faf02d6aa835043ca281"
11655
+ "sha256": "b8165107a23910ad35f528a86cd39ddff17a185fe7081f7cb8fb03f0c072a813"
11656
11656
  },
11657
11657
  {
11658
11658
  "path": "src/module-loader/catalog.js",
@@ -14877,7 +14877,7 @@
14877
14877
  {
14878
14878
  "path": "tests/npm_release_preview_diagnosis.mjs",
14879
14879
  "mode": "0000644",
14880
- "sha256": "9a246f9a29da48eaed7d0f19e156f1f63a48afb0277ab8ce02f7d6dc07c6d92b"
14880
+ "sha256": "1d0fe23920e58d8838d6753750097d55c0c36d32d30d8f14ddd916a8bea908d6"
14881
14881
  },
14882
14882
  {
14883
14883
  "path": "tests/npm_release_preview_divert.mjs",
@@ -14887,7 +14887,7 @@
14887
14887
  {
14888
14888
  "path": "tests/npm_release_preview_env.mjs",
14889
14889
  "mode": "0000644",
14890
- "sha256": "f25208db9870e67ac3b53456a97fbc02d2ab56a01974c82d1e264222ea063639"
14890
+ "sha256": "9febec41c155f29a2e22a7fa7dc8059be1fe6a36ff1845dceb85f87e695f42c3"
14891
14891
  },
14892
14892
  {
14893
14893
  "path": "tests/npm_release_preview_proxy.mjs",
@@ -14902,7 +14902,7 @@
14902
14902
  {
14903
14903
  "path": "tests/npm_release_preview_supervisor.mjs",
14904
14904
  "mode": "0000644",
14905
- "sha256": "d699b5b2a6b36169cfcdd925bf6b580c0238a6125174469a0129ca7945b1e69b"
14905
+ "sha256": "f326bc26e3b137dc8e17a22beeb08861181d35dc881ec1762e05377b9d9430e7"
14906
14906
  },
14907
14907
  {
14908
14908
  "path": "tests/npm_release_where.mjs",
@@ -97,6 +97,7 @@ What keeps that exception narrow:
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
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.
100
+ - **Correction, 2026-10-01 (task 1004486): the child is handed `PGUSER`.** The droplet's hall connects by socket peer auth with no `PGUSER`, so the driver names its login from `USER`, which an environment built from nothing does not have: every query in the copy failed. When no `PGUSER` is carried, the child gets the OS user it runs as (the identity peer auth checks anyway). And the dump file, a full copy of the live database, is now deleted as soon as the restore has read it; a service restart had left one in `/tmp` for eleven hours.
100
101
 
101
102
  ## Consequences
102
103
 
@@ -2785,5 +2785,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
2785
2785
  landed since 1.20.62 with no explicit bump. run 36850426143. (task 1002620)
2786
2786
  1.20.64 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2787
2787
  landed since 1.20.63 with no explicit bump. run 36851876428. (task 1002620)
2788
+ 1.20.65 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2789
+ landed since 1.20.64 with no explicit bump. run 36852902728. (task 1002620)
2788
2790
  ---------------------------------------------------------------------------
2789
2791
  ```
@@ -13,6 +13,8 @@
13
13
  // is pointed at an empty scratch directory so a config file in the real home (the
14
14
  // Discord webhook file among them) is not found.
15
15
 
16
+ const os = require('node:os');
17
+
16
18
  // The only names carried over from the live process.
17
19
  // PATH so node, and the child's own tooling, resolve
18
20
  // NODE_ENV production behaviour, as the live hall runs it
@@ -61,9 +63,10 @@ const MODULES_OFF = Object.freeze(PREFIXES.flatMap((pfx) => MODULE_KEYS.map((k)
61
63
  * @param {string} [o.envPrefix] the env prefix of the copied instance's branding pack, if any
62
64
  * @param {string} [o.instanceRoot] the scratch directory shaped like an instance root (config/,
63
65
  * migrations/instance/); the child reads its host content from it
66
+ * @param {Function} [o.osUser] () => the OS login name; PGUSER when none is carried
64
67
  * @returns {Record<string,string>} a fresh object, never `env` itself
65
68
  */
66
- function buildPreviewEnv({ env = process.env, port, scratchHome, instanceRoot, envPrefix } = {}) {
69
+ function buildPreviewEnv({ env = process.env, port, scratchHome, instanceRoot, envPrefix, osUser = () => os.userInfo().username } = {}) {
67
70
  if (!port) throw new TypeError('buildPreviewEnv: port is required');
68
71
  if (!scratchHome) throw new TypeError('buildPreviewEnv: scratchHome is required');
69
72
  const out = {};
@@ -71,6 +74,12 @@ function buildPreviewEnv({ env = process.env, port, scratchHome, instanceRoot, e
71
74
  if (env[name] !== undefined && env[name] !== '') out[name] = String(env[name]);
72
75
  }
73
76
  if (!out.NODE_ENV) out.NODE_ENV = 'production';
77
+ // THE LOGIN NAME IS STATED (task 1004388). With no PGUSER the database driver takes it from
78
+ // USER, which an environment built from nothing does not have, and every query fails with
79
+ // "no PostgreSQL user name specified". The OS user is exactly who peer auth checks.
80
+ if (!out.PGUSER) {
81
+ try { const u = osUser(); if (u) out.PGUSER = String(u); } catch { /* left unset: the driver's own default */ }
82
+ }
74
83
  out.PORT = String(port);
75
84
  out.HOST = '127.0.0.1';
76
85
  out.HOME = scratchHome;
@@ -217,6 +217,10 @@ function createSupervisor({
217
217
  await step('create-copy', cmds.createDbCommand());
218
218
  await step('dump', cmds.dumpCommand({ liveDb, dumpFile }));
219
219
  await step('restore', cmds.restoreCommand({ dumpFile }));
220
+ // The dump is a full copy of the live database, sessions included, and is not needed
221
+ // once restored. A restart of the live service cannot finish the stop that would have
222
+ // removed it, so it goes now (task 1004388).
223
+ try { fsImpl.rmSync(dumpFile, { force: true }); } catch { /* the stop's cleanup still removes workDir */ }
220
224
  await step('migrate', cmds.migrateCommand({ cacheRoot, version, env: childEnv, instanceDir }));
221
225
 
222
226
  set({ stage: 'launch' });
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.20.64",
3
+ "version": "1.20.65",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.20.64",
9
+ "version": "1.20.65",
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.64",
3
+ "version": "1.20.65",
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",
@@ -8513,5 +8513,11 @@
8513
8513
  "id": "1004390",
8514
8514
  "text": "When someone adds a prerequisite to a job that was already waiting in the \"available work\" list, the job used to stay on that list — a builder would pick it up, and only then be told they couldn't start it yet. The listing was"
8515
8515
  }
8516
+ ],
8517
+ "1.20.65": [
8518
+ {
8519
+ "id": "1004486",
8520
+ "text": "Previewing an older version from the deploy page now works on the real server: the preview could not log in to its copy of the database, so every page in it failed. It also no longer leaves a full copy of the live database lyi"
8521
+ }
8516
8522
  ]
8517
8523
  }
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.64'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.20.65'; // 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');
@@ -149,7 +149,7 @@ test('a probe that sees a database failure fails the preview, says why, and clea
149
149
  assert.match(s.error, /schemaPending null/);
150
150
  assert.ok(events.includes('kill:SIGTERM'), 'the child was stopped');
151
151
  assert.deepEqual(calls[calls.length - 1], cmds.dropDbCommand(), 'the copy was dropped');
152
- assert.equal(fake.rm.length, 1, 'the scratch directory was removed');
152
+ assert.equal(fake.rm.filter((p) => !String(p).replace(/\\/g, '/').endsWith('/live.dump')).length, 1, 'the scratch directory was removed');
153
153
  });
154
154
 
155
155
  test('a preview is running only once the probe says it can serve data', async () => {
@@ -98,3 +98,26 @@ test('a missing port or scratch home is refused rather than guessed', () => {
98
98
  assert.throws(() => buildPreviewEnv({ env: LIVE, scratchHome: '/h' }), /port/);
99
99
  assert.throws(() => buildPreviewEnv({ env: LIVE, port: 1 }), /scratchHome/);
100
100
  });
101
+
102
+ // THE FIRST REAL RUN ON THE DROPLET (task 1004388). The live hall connects by Unix-socket peer
103
+ // auth with no PGUSER set, so the database driver takes the login name from USER — and an
104
+ // environment built from nothing has no USER. Every query in the preview then failed with
105
+ // "no PostgreSQL user name specified", while the migration (psql, which asks the OS) passed.
106
+ // The login name is therefore stated: the carried PGUSER when there is one, else the OS user
107
+ // this process runs as — the same identity peer auth would have checked.
108
+ test('with no PGUSER to carry, the preview logs in as the OS user it runs as', () => {
109
+ const { PGUSER, ...noUser } = LIVE;
110
+ const out = buildPreviewEnv({ env: noUser, port: 3190, scratchHome: '/h', osUser: () => 'lars' });
111
+ assert.equal(out.PGUSER, 'lars');
112
+ });
113
+
114
+ test('a carried PGUSER wins over the OS user', () => {
115
+ const out = buildPreviewEnv({ env: LIVE, port: 3190, scratchHome: '/h', osUser: () => 'lars' });
116
+ assert.equal(out.PGUSER, 'hall');
117
+ });
118
+
119
+ test('an OS user that cannot be read leaves PGUSER unset rather than failing the start', () => {
120
+ const { PGUSER, ...noUser } = LIVE;
121
+ const out = buildPreviewEnv({ env: noUser, port: 3190, scratchHome: '/h', osUser: () => { throw new Error('no passwd entry'); } });
122
+ assert.equal('PGUSER' in out, false);
123
+ });
@@ -46,6 +46,8 @@ function rig({ failOn, probeResults, env } = {}) {
46
46
  }
47
47
 
48
48
  const fwd = (x) => String(x).split(String.fromCharCode(92)).join('/');
49
+ // The scratch directory's own removals; the dump file is removed separately, right after the restore.
50
+ const scratchRms = (r) => r.filter((p) => !fwd(p).endsWith('/live.dump'));
49
51
  const DROP = { cmd: 'dropdb', args: ['--if-exists', '--force', PREVIEW_DB] };
50
52
 
51
53
  test('a start runs install, copy, restore and migrate, in that order, then launches and waits for health', async () => {
@@ -92,7 +94,7 @@ for (const failOn of ['npm', 'dropdb', 'createdb', 'dump', 'pg_restore', 'bash']
92
94
  assert.match(s.error, failOn === 'dropdb' ? /could not be dropped/ : /failed [(]exit 3[)]/);
93
95
  assert.equal(fake.spawned.length, 0, 'no process was started');
94
96
  if (failOn !== 'dropdb') assert.deepEqual(calls[calls.length - 1], DROP, 'the copy is dropped last');
95
- assert.equal(rmCalls.length, 1, 'the scratch directory is removed');
97
+ assert.equal(scratchRms(rmCalls).length, 1, 'the scratch directory is removed');
96
98
  });
97
99
  }
98
100
 
@@ -102,7 +104,7 @@ test('a preview that never answers is stopped and cleaned up', async () => {
102
104
  assert.equal(sup.status().state, 'failed');
103
105
  assert.match(sup.status().error, /did not answer/);
104
106
  assert.equal(fake.killed, 1);
105
- assert.equal(rmCalls.length, 1);
107
+ assert.equal(scratchRms(rmCalls).length, 1);
106
108
  });
107
109
 
108
110
  test('only one preview at a time: a second version is refused, the same version is not restarted', async () => {
@@ -139,7 +141,7 @@ test('stop kills the child, drops the copy, removes scratch and returns to idle'
139
141
  await sup.stop();
140
142
  assert.equal(fake.killed, 1);
141
143
  assert.deepEqual(calls[calls.length - 1], DROP);
142
- assert.equal(rmCalls.length, 1);
144
+ assert.equal(scratchRms(rmCalls).length, 1);
143
145
  assert.equal(sup.status().state, 'idle');
144
146
  assert.equal(sup.status().version, null);
145
147
  assert.equal(sup.start('1.19.1001').ok, true, 'and a new one may start');
@@ -201,3 +203,16 @@ test('when the running version is not in the list the window counts from the new
201
203
  assert.equal(previewable('1.19.1029', { published, running: '0.0.1' }).ok, true);
202
204
  assert.equal(previewable('1.19.1028', { published, running: '0.0.1' }).ok, false);
203
205
  });
206
+
207
+ // THE DUMP IS A FULL COPY OF THE LIVE DATABASE, sessions included (task 1004388). It is needed
208
+ // only until the restore has read it; leaving it to the stop's cleanup meant a restart of the
209
+ // live service (which cannot finish an async stop) left it in /tmp — found there 11 hours on.
210
+ test('the dump file is deleted as soon as the restore has read it, before the child launches', async () => {
211
+ const { sup, rmCalls, fake } = rig();
212
+ const r = sup.start('1.19.1000');
213
+ await r.done;
214
+ const dumpRm = rmCalls.findIndex((p) => fwd(p).endsWith('/live.dump'));
215
+ assert.ok(dumpRm >= 0, 'the dump was never removed: ' + JSON.stringify(rmCalls));
216
+ assert.equal(sup.status().state, 'running');
217
+ assert.equal(fake.spawned.length, 1);
218
+ });