@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.
- package/.bongos-core.json +62 -37
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +3 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0314-module-protection-is-per-path-not-per-module.md +7 -2
- package/docs/adr/0315-a-plane-with-no-proxy-is-refused-not-given-a-no-op-ingress.md +96 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +62 -2
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +25 -25
- package/docs/copy-registry.json +29 -29
- package/docs/module-api-changelog.md +5 -1
- package/docs/recipes/self-host.md +1 -1
- package/modules/hall-ui/public/ideas.css +25 -0
- package/modules/hall-ui/public/ideas.html +12 -0
- package/modules/hall-ui/public/ideas.js +115 -2
- package/modules/ideas/landing-preview.js +180 -0
- package/modules/ideas/routes/inbox.js +47 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/api-alias-caller-scan.js +213 -0
- package/scripts/gds/fitness-ratchets.js +31 -0
- package/scripts/gds/provision-net.js +41 -0
- package/scripts/gds/provision.js +11 -3
- package/scripts/hall-preview/server.js +14 -1
- package/src/bongos/module-scope-map.js +43 -5
- package/src/module-api.js +1 -1
- package/tests/api_alias_caller_ratchet.mjs +130 -0
- package/tests/idea_landing_preview.mjs +337 -0
- package/tests/idea_spark_hall.mjs +183 -0
- package/tests/module-scope-map.mjs +87 -3
- package/tests/provision_caddy_wiring.mjs +76 -2
- 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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|