@bongos/core 1.19.910 → 1.19.912

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.
Files changed (35) hide show
  1. package/.bongos-core.json +62 -37
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +2 -0
  4. package/clients/bongos-client/index.cjs +2 -0
  5. package/clients/bongos-client/index.d.ts +3 -0
  6. package/clients/bongos-client/index.mjs +2 -0
  7. package/docs/adr/0314-module-protection-is-per-path-not-per-module.md +7 -2
  8. package/docs/adr/0315-a-plane-with-no-proxy-is-refused-not-given-a-no-op-ingress.md +96 -0
  9. package/docs/adr/README.md +1 -0
  10. package/docs/api/openapi.json +62 -2
  11. package/docs/api-reference.md +3 -2
  12. package/docs/copy-inventory.md +25 -25
  13. package/docs/copy-registry.json +29 -29
  14. package/docs/module-api-changelog.md +5 -1
  15. package/docs/recipes/self-host.md +1 -1
  16. package/modules/hall-ui/public/ideas.css +25 -0
  17. package/modules/hall-ui/public/ideas.html +12 -0
  18. package/modules/hall-ui/public/ideas.js +115 -2
  19. package/modules/ideas/landing-preview.js +180 -0
  20. package/modules/ideas/routes/inbox.js +47 -0
  21. package/package-lock.json +2 -2
  22. package/package.json +1 -1
  23. package/scripts/gds/api-alias-caller-scan.js +213 -0
  24. package/scripts/gds/fitness-ratchets.js +31 -0
  25. package/scripts/gds/provision-net.js +41 -0
  26. package/scripts/gds/provision.js +11 -3
  27. package/scripts/hall-preview/server.js +14 -1
  28. package/src/bongos/module-scope-map.js +43 -5
  29. package/src/module-api.js +1 -1
  30. package/tests/api_alias_caller_ratchet.mjs +130 -0
  31. package/tests/idea_landing_preview.mjs +337 -0
  32. package/tests/idea_spark_hall.mjs +183 -0
  33. package/tests/module-scope-map.mjs +87 -3
  34. package/tests/provision_caddy_wiring.mjs +76 -2
  35. package/tests/quarantine_canonical.mjs +32 -4
@@ -9,7 +9,7 @@ import { createRequire } from 'node:module';
9
9
 
10
10
  const require = createRequire(import.meta.url);
11
11
  const P = require('../scripts/gds/provision.js');
12
- const { caddyWiringGap, checkCaddyWiring } = require('../scripts/gds/provision-net.js');
12
+ const { caddyWiringGap, checkCaddyWiring, ingressPlaneGap } = require('../scripts/gds/provision-net.js');
13
13
 
14
14
  const DOMAIN = 'mercury.example.com';
15
15
  // What `caddy adapt` prints: 2.7 writes the ask as a string, 2.8+ as a permission module.
@@ -63,11 +63,13 @@ const WEB_ENV = 'PORT=3010\nBONGOS_PUBLIC_ORIGIN=https://old.example.com\n';
63
63
 
64
64
  // `served` = the hosts Caddy's resolved config carries once a snippet is written — the
65
65
  // fake flips to them after the first .caddy write, like the real import would.
66
- function harness({ onDemand = ASK_27, importsSnippets = true, reloadFails = false } = {}) {
66
+ function harness({ onDemand = ASK_27, importsSnippets = true, reloadFails = false, noProxy = false } = {}) {
67
67
  const cmds = [], writes = [], statuses = [];
68
68
  let snippetWritten = null;
69
69
  const exec = (cmd, opts = {}) => {
70
70
  cmds.push({ cmd, opts });
71
+ // A plane with no proxy at all: the self-host image installs no caddy binary.
72
+ if (noProxy && /^caddy /.test(cmd)) { const e = new Error('Command failed'); e.stderr = Buffer.from('bash: caddy: command not found'); throw e; }
71
73
  if (cmd.startsWith('caddy adapt')) return { ok: true, stdout: adapted({ onDemand, hosts: importsSnippets && snippetWritten ? [snippetWritten] : [] }) };
72
74
  if (/cat\b.*web\.env/.test(cmd)) return { ok: true, stdout: WEB_ENV };
73
75
  if (/reload caddy/.test(cmd) && reloadFails) throw new Error('Job for caddy.service failed');
@@ -146,3 +148,75 @@ test('domain-attach: refused before the env patch without the ask; fails loudly
146
148
  assert.equal((await P.domainAttachInstance(activeStandalone, wired.deps)).ok, true);
147
149
  assert.notEqual(wired.cmds.find((c) => /reload caddy/.test(c.cmd)).opts.allowFail, true);
148
150
  });
151
+
152
+ // -- the plane has no reverse proxy AT ALL (task 1004017) -------------------
153
+ // ADR 0303 asks whether a box's Caddy is wired right. This is the prior question:
154
+ // the self-host image ships no proxy, so a child instance provisioned inside it gets
155
+ // a snippet write and a `systemctl reload caddy` that cannot work.
156
+
157
+ test('ingressPlaneGap: a plane that answers `caddy version` is wired; one that cannot is named as lacking a proxy', () => {
158
+ const cmds = [];
159
+ assert.equal(ingressPlaneGap((c) => { cmds.push(c); return { ok: true, stdout: 'v2.8.4' }; }), null);
160
+ assert.deepEqual(cmds, ['caddy version'], 'one cheap, unprivileged probe — no config read, no running server');
161
+ const boom = () => { const e = new Error('Command failed'); e.stderr = Buffer.from('bash: caddy: command not found'); throw e; };
162
+ const gap = ingressPlaneGap(boom);
163
+ assert.match(gap, /no reverse proxy/, 'names what the plane lacks');
164
+ assert.match(gap, /command not found/, 'carries the real reason');
165
+ });
166
+
167
+ test('ingressPlaneGap: only a MISSING BINARY proves there is no proxy — a transport failure says it could not confirm', () => {
168
+ // boxExec is `ssh <host> bash -s` for a co-tenant, so an unreachable box throws here too.
169
+ // Diagnosing that as "this platform has no proxy, provision elsewhere" would be a confident
170
+ // wrong answer, and the intent would burn its retry budget on it.
171
+ const unreachable = () => { const e = new Error('Command failed'); e.status = 255; e.stderr = Buffer.from('ssh: connect to host box.example.com port 22: Connection timed out'); throw e; };
172
+ const unsure = ingressPlaneGap(unreachable);
173
+ assert.match(unsure, /could not confirm a reverse proxy/);
174
+ assert.match(unsure, /Connection timed out/, 'the real cause leads, not a parenthetical');
175
+ assert.doesNotMatch(unsure, /has no reverse proxy/, 'never asserts a diagnosis it cannot support');
176
+ // Exit 127 still means "no caddy" across the ssh hop — ssh reports its OWN failures as 255
177
+ // and otherwise propagates the remote command's status.
178
+ const remoteMissing = () => { const e = new Error('Command failed'); e.status = 127; e.stderr = Buffer.from(''); throw e; };
179
+ assert.match(ingressPlaneGap(remoteMissing), /has no reverse proxy/);
180
+ const cmdExe = () => { const e = new Error("'caddy' is not recognized as an internal or external command"); throw e; };
181
+ assert.match(ingressPlaneGap(cmdExe), /has no reverse proxy/, 'cmd.exe words it differently');
182
+ });
183
+
184
+ test('provision: a DOMAINLESS child on a plane with no reverse proxy is refused before a single write', async () => {
185
+ // The hole ADR 0303 leaves open: its preflight is gated on inst.domain, so a domainless
186
+ // child walked all the way to step 6 and died on the snippet write instead of being refused.
187
+ const h = harness({ noProxy: true });
188
+ const r = await P.provisionInstance({ ...coTenant, domain: null }, h.deps);
189
+ assert.equal(r.ok, false);
190
+ assert.match(r.error, /no reverse proxy/);
191
+ assert.equal(h.writes.length, 0, 'nothing written — not the snippet, not web.env');
192
+ assert.ok(!h.ran(/reload caddy/), 'the reload that cannot work is never attempted');
193
+ assert.deepEqual(h.statuses, [], 'the instance never flips to provisioning');
194
+ });
195
+
196
+ test('provision: a domained child on a proxyless plane names the missing proxy, not a Caddyfile to edit', async () => {
197
+ const h = harness({ noProxy: true });
198
+ const r = await P.provisionInstance(coTenant, h.deps);
199
+ assert.equal(r.ok, false);
200
+ assert.match(r.error, /no reverse proxy/);
201
+ assert.doesNotMatch(r.error, /add 'import|add 'on_demand_tls/, 'a plane that will never have Caddy is not told to edit a Caddyfile it does not have');
202
+ assert.equal(h.writes.length, 0);
203
+ });
204
+
205
+ test('domain-attach: a plane with no reverse proxy is refused before the env patch', async () => {
206
+ const h = harness({ noProxy: true });
207
+ const r = await P.domainAttachInstance(activeStandalone, h.deps);
208
+ assert.equal(r.ok, false);
209
+ assert.match(r.error, /no reverse proxy/);
210
+ assert.equal(h.writes.length, 0, 'web.env untouched');
211
+ assert.ok(!h.ran(/reload caddy/));
212
+ });
213
+
214
+ test('a dry run never probes the plane, and a wired box still reloads Caddy loudly', async () => {
215
+ const dry = harness({ noProxy: true });
216
+ dry.deps.apply = false;
217
+ await P.provisionInstance(coTenant, dry.deps);
218
+ assert.ok(!dry.ran(/caddy version/), 'a dry run changes nothing and asks nothing of the box');
219
+ const wired = harness();
220
+ assert.equal((await P.provisionInstance(coTenant, wired.deps)).ok, true, 'ADR 0303 behaviour on a real box is unchanged');
221
+ assert.ok(wired.ran(/reload caddy/));
222
+ });
@@ -22,11 +22,39 @@ const q = require('../modules/security/artifact-scan/quarantine.js');
22
22
  const SHORT = os.tmpdir();
23
23
  const LONG = fs.realpathSync.native(SHORT);
24
24
 
25
- test('canonical expands an 8.3 short name — plain realpathSync does not', () => {
25
+ // eightDotThreeReason — why the 8.3 case cannot be evaluated on THIS box, or null (task 1004051).
26
+ //
27
+ // The assertion below it is a statement about one specific divergence: a Windows 8.3 short name,
28
+ // which plain fs.realpathSync does NOT expand. Asking "do the two spellings differ?" is a
29
+ // different question, and macOS is where the two come apart — there os.tmpdir() is /var/folders/…
30
+ // against a real /private/var/folders/…, a SYMLINK divergence that plain realpathSync resolves
31
+ // perfectly well, so the notEqual fails and the suite goes red over a fact that was never claimed
32
+ // about that platform. So ask for the 8.3 signature itself (a `~N` component) rather than
33
+ // inferring it from inequality.
34
+ //
35
+ // It must not read as a PASS either — the task 1003993 contract. This returns a REASON for
36
+ // node:test's own skip, never a bare early `return`, which prints as ✔ over a case that never
37
+ // ran. Same shape as shallowCloneReason() in tests/helpers.mjs (task 1004049); kept local
38
+ // because the 8.3 question belongs to this file alone.
39
+ function eightDotThreeReason() {
40
+ if (SHORT === LONG) {
41
+ return `${process.platform}: os.tmpdir() is already its canonical spelling, so there is no `
42
+ + 'second spelling to expand here';
43
+ }
44
+ if (!/~\d/.test(SHORT)) {
45
+ return `${process.platform}: os.tmpdir() (${SHORT}) diverges from its real path (${LONG}) for `
46
+ + 'a reason that is not an 8.3 short name — plain realpathSync resolves that kind, so the '
47
+ + '8.3 fact cannot be evaluated here';
48
+ }
49
+ return null;
50
+ }
51
+ const NO_8_3 = eightDotThreeReason();
52
+
53
+ test('canonical reports where os.tmpdir() really points, whatever its spelling', () => {
26
54
  assert.equal(q.canonical(SHORT), LONG);
27
- // The premise, stated so this test explains itself on a machine where it does not apply:
28
- // if the two spellings are identical here, the rest is trivially true rather than wrong.
29
- if (SHORT === LONG) return;
55
+ });
56
+
57
+ test('plain realpathSync does NOT expand an 8.3 short name', { skip: NO_8_3 ?? false }, () => {
30
58
  assert.notEqual(fs.realpathSync(SHORT), LONG, 'this is the fact the obvious fix gets wrong');
31
59
  });
32
60