@webority/ensemble 0.5.15 → 0.5.17

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/bin/ensemble.js CHANGED
@@ -9,7 +9,7 @@ const core = require('../lib/core');
9
9
  // Bus subcommands are handled by the internal `ensemble-runtime` binary; the Node CLI forwards argv
10
10
  // through to it (inherited stdio, same exit code) so users type only one command: `ensemble <sub>`.
11
11
  const BUS_SUBCOMMANDS = new Set([
12
- 'send', 'who', 'read', 'reply', 'label', 'hook', 'mcp', 'register', 'detach', 'ask',
12
+ 'send', 'who', 'read', 'watch', 'reply', 'label', 'hook', 'mcp', 'register', 'detach', 'ask',
13
13
  'rooms', 'room-create', 'room-send', 'room-messages', 'room-add', 'room-remove',
14
14
  ]);
15
15
 
package/lib/core.js CHANGED
@@ -4,6 +4,7 @@ const os = require('os');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const https = require('https');
7
+ const crypto = require('crypto');
7
8
  const { spawn, spawnSync } = require('child_process');
8
9
 
9
10
  const HOME = os.homedir();
@@ -69,6 +70,47 @@ function downloadTo(url, dest) {
69
70
  });
70
71
  }
71
72
 
73
+ // SHA-256 of a file on disk (hex). Used to verify CDN / package payloads against manifest.json.
74
+ function sha256File(filePath) {
75
+ const hash = crypto.createHash('sha256');
76
+ hash.update(fs.readFileSync(filePath));
77
+ return hash.digest('hex');
78
+ }
79
+
80
+ // Fetch the release manifest from the same origin as binaries (DL_BASE/manifest.json).
81
+ // Shape: { "version": "x.y.z", "files": { "ensemble-runtime-win-x64.exe": "<sha256hex>", ... } }
82
+ // ENSEMBLE_DL_BASE is treated as trusted-only: CDN installs fail closed without a matching entry.
83
+ async function fetchReleaseManifest() {
84
+ const url = DL_BASE.replace(/\/$/, '') + '/manifest.json';
85
+ try {
86
+ const res = await request('GET', url, {}, null);
87
+ if (res.status !== 200) {
88
+ return { ok: false, reason: 'HTTP ' + res.status + ' for ' + url };
89
+ }
90
+ const body = JSON.parse(res.body);
91
+ if (!body || typeof body !== 'object' || !body.files || typeof body.files !== 'object') {
92
+ return { ok: false, reason: 'manifest.json missing files map' };
93
+ }
94
+ return { ok: true, manifest: body };
95
+ } catch (e) {
96
+ return { ok: false, reason: e && e.message ? e.message : String(e) };
97
+ }
98
+ }
99
+
100
+ // Verify tmp against the expected hex from the manifest. Throws on mismatch / missing entry.
101
+ function verifyBinarySha256(tmpPath, expectedHex, logicalName) {
102
+ if (!expectedHex || typeof expectedHex !== 'string') {
103
+ throw new Error('no SHA-256 in manifest for ' + logicalName +
104
+ ' — refuse to place an unverified binary from ' + DL_BASE);
105
+ }
106
+ const actual = sha256File(tmpPath);
107
+ if (actual.toLowerCase() !== expectedHex.trim().toLowerCase()) {
108
+ throw new Error('SHA-256 mismatch for ' + logicalName +
109
+ ' (expected ' + expectedHex.trim().toLowerCase() + ', got ' + actual +
110
+ '). Refusing to place — CDN/package may be compromised or stale.');
111
+ }
112
+ }
113
+
72
114
  // Throws (not die) so the npm postinstall can catch and not fail the whole install on an
73
115
  // unsupported OS; the CLI dispatcher catches the throw and prints it cleanly.
74
116
  function assertSupported() {
@@ -300,24 +342,62 @@ async function ensureBinaries(opts) {
300
342
  { src: pkgDir && path.join(pkgDir, 'ensemble-runtime' + pf.exe), cdn: 'ensemble-runtime-' + pf.key + pf.exe, dest: runtimePath },
301
343
  { src: pkgDir && path.join(pkgDir, 'Ensemble.Runner' + pf.exe), cdn: 'ensemble-runner-' + pf.key + pf.exe, dest: runnerPath },
302
344
  ];
345
+
346
+ // CDN path requires a SHA-256 manifest (fail closed). npm package copies also verify when the
347
+ // manifest lists the file (npm integrity is not a substitute for a published release hash).
348
+ // ENSEMBLE_DL_BASE is trusted-only as the origin of both binaries and manifest.json.
349
+ let manifestFiles = null;
350
+ const needsCdn = targets.some((t) => shouldRefreshBinary(t.dest, t.src, force) && !(t.src && isUsableBinary(t.src)));
351
+ const needsAny = targets.some((t) => shouldRefreshBinary(t.dest, t.src, force));
352
+ if (needsAny) {
353
+ const fetched = await fetchReleaseManifest();
354
+ if (fetched.ok) {
355
+ manifestFiles = fetched.manifest.files;
356
+ } else if (needsCdn) {
357
+ throw new Error('CDN install refused: could not load SHA-256 manifest from ' +
358
+ DL_BASE.replace(/\/$/, '') + '/manifest.json (' + fetched.reason +
359
+ '). Publish manifest.json next to the binaries, or install via npm platform packages.');
360
+ } else {
361
+ warn('manifest.json unavailable (' + fetched.reason +
362
+ ') — placing npm package binaries without SHA-256 verify. Publish a release manifest for full integrity.');
363
+ }
364
+ }
365
+
303
366
  let runnerRefreshed = false;
304
367
  for (const t of targets) {
305
368
  if (!shouldRefreshBinary(t.dest, t.src, force)) continue;
306
369
  // Write to a temp then atomically place onto dest — placeBinary tolerates a Windows lock on the
307
370
  // running runner (rename-aside), so an upgrade never fails just because the daemon is up.
308
371
  const tmp = t.dest + '.new';
309
- if (t.src && isUsableBinary(t.src)) {
372
+ const fromNpm = !!(t.src && isUsableBinary(t.src));
373
+ if (fromNpm) {
310
374
  log(' installing ' + path.basename(t.dest) + ' from npm package …');
311
375
  fs.copyFileSync(t.src, tmp);
312
376
  } else {
313
377
  log(' downloading ' + t.cdn + ' …');
314
- await downloadTo(DL_BASE + '/' + t.cdn, tmp);
378
+ await downloadTo(DL_BASE.replace(/\/$/, '') + '/' + t.cdn, tmp);
315
379
  }
316
380
  if (!isUsableBinary(tmp)) {
317
381
  try { fs.unlinkSync(tmp); } catch (_) { /* ignore */ }
318
382
  throw new Error('installed binary looks corrupt or too small: ' + t.dest +
319
383
  ' (expected ≥ ' + MIN_BINARY_BYTES + ' bytes). Check CDN / platform package.');
320
384
  }
385
+ // Integrity: CDN always requires a matching hash. npm package verifies when the release
386
+ // manifest lists this cdn key (same bytes as the published artifact).
387
+ if (manifestFiles) {
388
+ const expected = manifestFiles[t.cdn];
389
+ if (!fromNpm || expected) {
390
+ try {
391
+ verifyBinarySha256(tmp, expected, t.cdn);
392
+ } catch (e) {
393
+ try { fs.unlinkSync(tmp); } catch (_) { /* ignore */ }
394
+ throw e;
395
+ }
396
+ }
397
+ } else if (!fromNpm) {
398
+ try { fs.unlinkSync(tmp); } catch (_) { /* ignore */ }
399
+ throw new Error('CDN install refused: no SHA-256 for ' + t.cdn);
400
+ }
321
401
  placeBinary(tmp, t.dest);
322
402
  if (t.dest === runnerPath) runnerRefreshed = true;
323
403
  if (pf.os !== 'windows') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webority/ensemble",
3
- "version": "0.5.15",
3
+ "version": "0.5.17",
4
4
  "description": "Connect this machine to Ensemble — runs the local agent runner and wires the ensemble session bus so your coding sessions talk (per-org, isolated).",
5
5
  "bin": {
6
6
  "ensemble": "bin/ensemble.js"
@@ -12,11 +12,11 @@
12
12
  "node": ">=18"
13
13
  },
14
14
  "optionalDependencies": {
15
- "@webority/ensemble-darwin-arm64": "0.5.15",
16
- "@webority/ensemble-darwin-x64": "0.5.15",
17
- "@webority/ensemble-linux-arm64": "0.5.15",
18
- "@webority/ensemble-linux-x64": "0.5.15",
19
- "@webority/ensemble-win-x64": "0.5.15"
15
+ "@webority/ensemble-darwin-arm64": "0.5.17",
16
+ "@webority/ensemble-darwin-x64": "0.5.17",
17
+ "@webority/ensemble-linux-arm64": "0.5.17",
18
+ "@webority/ensemble-linux-x64": "0.5.17",
19
+ "@webority/ensemble-win-x64": "0.5.17"
20
20
  },
21
21
  "files": [
22
22
  "bin",