@bongos/core 1.19.838 → 1.19.840

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 (38) hide show
  1. package/.bongos-core.json +69 -39
  2. package/clients/bongos-client/index.d.ts +1 -1
  3. package/config/branding.neutral.json +2 -1
  4. package/docs/adr/0303-provisioning-verifies-the-base-caddyfile-and-never-edits-it.md +49 -0
  5. package/docs/adr/README.md +1 -0
  6. package/docs/api/openapi.json +2 -0
  7. package/docs/architecture.md +2 -2
  8. package/docs/branding-contract.md +1 -1
  9. package/docs/branding-fill-prompt.md +1 -1
  10. package/docs/copy-inventory.md +3 -3
  11. package/docs/copy-registry.json +5 -5
  12. package/docs/module-api-changelog.md +4 -0
  13. package/modules/lifecycle/github-push.js +2 -2
  14. package/modules/lifecycle/pr-assign.js +77 -0
  15. package/modules/lifecycle/routes/tasks.js +16 -1
  16. package/modules/status-ui/public/index.html +2 -1
  17. package/package-lock.json +2 -2
  18. package/package.json +1 -1
  19. package/scripts/gds/fitness-checks-identity.js +9 -1
  20. package/scripts/gds/fitness.js +34 -1
  21. package/scripts/gds/paste-token.js +65 -14
  22. package/scripts/gds/provision-config.js +3 -0
  23. package/scripts/gds/provision-net.js +32 -0
  24. package/scripts/gds/provision.js +21 -4
  25. package/scripts/gds/ship-land.js +12 -0
  26. package/scripts/status-mirror-sync.js +6 -2
  27. package/src/bongos/routes/auth.js +24 -3
  28. package/src/bongos/serve-internal.js +9 -2
  29. package/src/branding.js +22 -2
  30. package/src/module-api.js +1 -1
  31. package/tests/bongos_cli.mjs +13 -4
  32. package/tests/cli_token_reissue.mjs +54 -7
  33. package/tests/fitness.mjs +47 -0
  34. package/tests/paste_token_api_base.mjs +87 -0
  35. package/tests/pr_assign.mjs +189 -0
  36. package/tests/provision.mjs +12 -0
  37. package/tests/provision_caddy_wiring.mjs +148 -0
  38. package/tests/status_company_attribution.mjs +60 -0
@@ -0,0 +1,148 @@
1
+ // tests/provision_caddy_wiring.mjs — provisioning checks the base Caddyfile carries the
2
+ // on-demand TLS ask and imports the site snippets before it reports a domain wired
3
+ // (task 1002860, ADR 0303). Before it, a box missing either directive provisioned
4
+ // "successfully" and the instance was simply unreachable. Hermetic: exec is a fake that
5
+ // answers `caddy adapt` with a fixture of Caddy's resolved JSON.
6
+ import assert from 'node:assert/strict';
7
+ import { test } from 'node:test';
8
+ import { createRequire } from 'node:module';
9
+
10
+ const require = createRequire(import.meta.url);
11
+ const P = require('../scripts/gds/provision.js');
12
+ const { caddyWiringGap, checkCaddyWiring } = require('../scripts/gds/provision-net.js');
13
+
14
+ const DOMAIN = 'mercury.example.com';
15
+ // What `caddy adapt` prints: 2.7 writes the ask as a string, 2.8+ as a permission module.
16
+ const adapted = ({ onDemand, hosts = [] } = {}) => JSON.stringify({
17
+ apps: {
18
+ http: { servers: { srv0: { routes: hosts.map((h) => ({ match: [{ host: [h] }] })) } } },
19
+ ...(onDemand ? { tls: { automation: { on_demand: onDemand } } } : {}),
20
+ },
21
+ });
22
+ const ASK_27 = { ask: 'http://127.0.0.1:3002/api/bongos/provisioning/tls-check' };
23
+ const ASK_28 = { permission: { module: 'http', endpoint: 'http://127.0.0.1:3002/api/gds/provisioning/tls-check' } };
24
+ const OPTS = { domain: DOMAIN, caddyfile: '/etc/caddy/Caddyfile', snippetDir: '/etc/caddy/sites' };
25
+
26
+ test('a wired config passes in both the 2.7 ask and the 2.8 permission shape', () => {
27
+ for (const onDemand of [ASK_27, ASK_28]) {
28
+ assert.equal(caddyWiringGap(adapted({ onDemand, hosts: [DOMAIN] }), { ...OPTS, requireHost: true }), null);
29
+ }
30
+ });
31
+
32
+ test('no on-demand ask, or one aimed elsewhere, names the directive and the file to add it to', () => {
33
+ for (const onDemand of [undefined, { ask: 'http://127.0.0.1:9000/some-other-check' }]) {
34
+ const gap = caddyWiringGap(adapted({ onDemand, hosts: [DOMAIN] }), OPTS);
35
+ assert.match(gap, /on_demand_tls/);
36
+ assert.match(gap, /\/etc\/caddy\/Caddyfile/);
37
+ assert.match(gap, /\/provisioning\/tls-check/);
38
+ }
39
+ });
40
+
41
+ test('a domain Caddy does not serve after the snippet is written names the missing import', () => {
42
+ const gap = caddyWiringGap(adapted({ onDemand: ASK_27, hosts: ['other.example.com'] }), { ...OPTS, requireHost: true });
43
+ assert.match(gap, /import \/etc\/caddy\/sites\/\*\.caddy/);
44
+ assert.match(gap, /mercury\.example\.com/);
45
+ assert.equal(caddyWiringGap(adapted({ onDemand: ASK_27 }), OPTS), null, 'the preflight asks only for the ask — no snippet exists yet');
46
+ });
47
+
48
+ test('output that is not Caddy JSON, or a failed adapt, is a gap rather than a pass or a crash', () => {
49
+ assert.match(caddyWiringGap('', OPTS), /caddy adapt/);
50
+ assert.match(caddyWiringGap('null', OPTS), /caddy adapt/);
51
+ const boom = () => { const e = new Error('Command failed'); e.stderr = Buffer.from('Error: adapting config: Caddyfile:3 - unrecognized directive'); throw e; };
52
+ assert.match(checkCaddyWiring(boom, OPTS), /unrecognized directive/);
53
+ const cmds = [];
54
+ checkCaddyWiring((c) => { cmds.push(c); return { ok: true, stdout: adapted({ onDemand: ASK_27 }) }; }, OPTS);
55
+ assert.deepEqual(cmds, ['caddy adapt --config /etc/caddy/Caddyfile --adapter caddyfile'], 'unprivileged and read-only');
56
+ });
57
+
58
+ // -- the runner legs --------------------------------------------------------
59
+
60
+ const coTenant = { id: 1, slug: 'mercury', hosting_shape: 'co-tenant', status: 'requested', owner_builder_id: 7, port: 3003, db_name: 'mercury', domain: DOMAIN };
61
+ const activeStandalone = { id: 18, slug: 'spike', hosting_shape: 'standalone', status: 'active', owner_builder_id: 7, port: 3010, db_name: 'spike', domain: 'spike.example.com' };
62
+ const WEB_ENV = 'PORT=3010\nBONGOS_PUBLIC_ORIGIN=https://old.example.com\n';
63
+
64
+ // `served` = the hosts Caddy's resolved config carries once a snippet is written — the
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 } = {}) {
67
+ const cmds = [], writes = [], statuses = [];
68
+ let snippetWritten = null;
69
+ const exec = (cmd, opts = {}) => {
70
+ cmds.push({ cmd, opts });
71
+ if (cmd.startsWith('caddy adapt')) return { ok: true, stdout: adapted({ onDemand, hosts: importsSnippets && snippetWritten ? [snippetWritten] : [] }) };
72
+ if (/cat\b.*web\.env/.test(cmd)) return { ok: true, stdout: WEB_ENV };
73
+ if (/reload caddy/.test(cmd) && reloadFails) throw new Error('Job for caddy.service failed');
74
+ return { ok: true, stdout: '' };
75
+ };
76
+ const deps = {
77
+ apply: true, log: () => {}, sleep: async () => {},
78
+ db: { query: async () => ({ rows: [] }) },
79
+ provisioning: {
80
+ setInstanceStatus: async (_db, _id, s) => { statuses.push(s); }, recordEvent: async () => {},
81
+ recordDnsNote: async () => {}, clearReleasedDomains: async () => {},
82
+ },
83
+ exec,
84
+ writeFile: (p, body) => { writes.push(p); if (p.endsWith('.caddy')) snippetWritten = (/^(\S+) \{/.exec(body) || [])[1] || null; return { ok: true }; },
85
+ doApi: null,
86
+ checkDnsTokenScope: async () => ({ reachable: true, zone: 'example.com' }),
87
+ getGithubToken: async () => null,
88
+ };
89
+ const ran = (re) => cmds.some((c) => re.test(c.cmd));
90
+ return { deps, cmds, writes, statuses, ran };
91
+ }
92
+
93
+ test('provision: a box with no tls-check ask is refused before anything on it changes', async () => {
94
+ const h = harness({ onDemand: null });
95
+ const r = await P.provisionInstance(coTenant, h.deps);
96
+ assert.equal(r.ok, false);
97
+ assert.match(r.error, /^Caddy preflight failed: .*on_demand_tls/);
98
+ assert.equal(h.writes.length, 0, 'no file written');
99
+ assert.ok(!h.ran(/createdb|systemctl/), 'no database or service touched');
100
+ assert.deepEqual(h.statuses, [], 'the instance never flips to provisioning');
101
+ });
102
+
103
+ test('provision: a snippet Caddy never imports fails the run instead of reporting success, and Caddy is not reloaded', async () => {
104
+ const h = harness({ importsSnippets: false });
105
+ const r = await P.provisionInstance(coTenant, h.deps);
106
+ assert.equal(r.ok, false);
107
+ assert.match(r.error, /^Caddy wiring failed: .*import \/etc\/caddy\/sites\/\*\.caddy/);
108
+ assert.ok(h.writes.some((p) => p.endsWith('/mercury.caddy')), 'the snippet was written first — that is what the check proves Caddy reads');
109
+ assert.ok(!h.ran(/reload caddy/));
110
+ assert.ok(!h.ran(/healthz/), 'never reaches the healthz step');
111
+ });
112
+
113
+ test('provision: a wired box reloads Caddy loudly — a rejected reload fails the run', async () => {
114
+ const ok = harness();
115
+ assert.equal((await P.provisionInstance(coTenant, ok.deps)).ok, true);
116
+ const reload = ok.cmds.find((c) => /reload caddy/.test(c.cmd));
117
+ assert.ok(reload, 'reloads');
118
+ assert.notEqual(reload.opts.allowFail, true, 'not allowFail');
119
+ await assert.rejects(P.provisionInstance(coTenant, harness({ reloadFails: true }).deps), /caddy\.service failed/);
120
+ });
121
+
122
+ test('provision: a domainless instance and a dry run never call caddy adapt', async () => {
123
+ const bare = harness({ onDemand: null });
124
+ assert.equal((await P.provisionInstance({ ...coTenant, domain: null }, bare.deps)).ok, true);
125
+ const dry = harness({ onDemand: null });
126
+ dry.deps.apply = false;
127
+ await P.provisionInstance(coTenant, dry.deps);
128
+ assert.ok(!bare.ran(/caddy adapt/) && !dry.ran(/caddy adapt/));
129
+ });
130
+
131
+ test('domain-attach: refused before the env patch without the ask; fails loudly when the snippet is not imported', async () => {
132
+ const noAsk = harness({ onDemand: null });
133
+ const r1 = await P.domainAttachInstance(activeStandalone, noAsk.deps);
134
+ assert.equal(r1.ok, false);
135
+ assert.match(r1.error, /^Caddy preflight failed/);
136
+ assert.equal(noAsk.writes.length, 0, 'web.env untouched');
137
+ assert.ok(!noAsk.ran(/systemctl/));
138
+
139
+ const noImport = harness({ importsSnippets: false });
140
+ const r2 = await P.domainAttachInstance(activeStandalone, noImport.deps);
141
+ assert.equal(r2.ok, false);
142
+ assert.match(r2.error, /^Caddy wiring failed: .*spike\.example\.com/);
143
+ assert.ok(!noImport.ran(/reload caddy/));
144
+
145
+ const wired = harness();
146
+ assert.equal((await P.domainAttachInstance(activeStandalone, wired.deps)).ok, true);
147
+ assert.notEqual(wired.cmds.find((c) => /reload caddy/.test(c.cmd)).opts.allowFail, true);
148
+ });
@@ -0,0 +1,60 @@
1
+ // tests/status_company_attribution.mjs — the status page footer's company clause
2
+ // comes from the branding pack, and disappears whole when the pack names no company
3
+ // (task 1002853). Before it, a vanilla instance's CORE-served status page credited
4
+ // the founder's own logistics company as its maker.
5
+ import assert from 'node:assert/strict';
6
+ import { test } from 'node:test';
7
+ import { readFileSync } from 'node:fs';
8
+ import { createRequire } from 'node:module';
9
+
10
+ const require = createRequire(import.meta.url);
11
+ const { applyBrandTokens } = require('../src/bongos/serve-internal.js');
12
+ const { clientBranding } = require('../src/branding.js');
13
+ const { brandTokens } = require('../scripts/status-mirror-sync.js');
14
+
15
+ const PAGE = readFileSync(new URL('../modules/status-ui/public/index.html', import.meta.url), 'utf8');
16
+ const brand = (company) => ({ identity: { productName: 'Acme Rockets', worldName: 'Acme', company }, currency: { label: 'tokens' } });
17
+ const footer = (html) => html.match(/<footer[\s\S]*?<\/footer>/)[0];
18
+ const stamp = (company) => footer(applyBrandTokens(PAGE, brand(company), 'https://acme.example', '', '', '', ''));
19
+
20
+ test('the source page names no company and carries every company slot as a token', () => {
21
+ assert.doesNotMatch(PAGE, /hermeslines|cleveland/i);
22
+ for (const t of ['companyName', 'companySite', 'companyTagline']) assert.ok(PAGE.includes(`{{${t}}}`), `{{${t}}} is missing`);
23
+ });
24
+
25
+ test('a pack with no company drops the whole attribution sentence, not half of it', () => {
26
+ const out = stamp({ name: '', site: 'https://ignored.example', tagline: 'ignored' });
27
+ assert.doesNotMatch(out, /is the work of|ignored/);
28
+ assert.doesNotMatch(out, /\{\{|brand-if/);
29
+ assert.match(out, /Tokens are wrought/, 'the neighbouring footer lines survive');
30
+ });
31
+
32
+ test('a full company renders the linked name and its tagline', () => {
33
+ const out = stamp({ name: 'Acme Corp', site: 'https://acme-corp.example', tagline: 'a rocket works in Dayton' });
34
+ assert.match(out, /<p>Acme is the work of <a href="https:\/\/acme-corp\.example" target="_blank" rel="noopener"><strong>Acme Corp<\/strong><\/a>, a rocket works in Dayton\.<\/p>/);
35
+ assert.doesNotMatch(out, /\{\{|brand-if/);
36
+ });
37
+
38
+ test('a company with no site or tagline renders the bare name, never an empty href or a dangling comma', () => {
39
+ const out = stamp({ name: 'Acme Corp', site: '', tagline: '' });
40
+ assert.match(out, /<p>Acme is the work of <strong>Acme Corp<\/strong>\.<\/p>/);
41
+ assert.doesNotMatch(out, /href=""|, \./);
42
+ });
43
+
44
+ test('company values are escaped like every other token', () => {
45
+ const out = stamp({ name: 'A<b>', site: 'https://x.example/"onmouseover="x', tagline: '' });
46
+ assert.ok(out.includes('<strong>A&lt;b&gt;</strong>'));
47
+ assert.ok(out.includes('href="https://x.example/&quot;onmouseover=&quot;x"'));
48
+ });
49
+
50
+ test('the off-box mirror stamps the footer exactly as the served page does (KEEP IN SYNC)', () => {
51
+ for (const company of [{ name: '', site: '', tagline: '' }, { name: 'Acme Corp', site: 'https://acme-corp.example', tagline: 'a rocket works' }, { name: 'Acme Corp', site: '', tagline: '' }]) {
52
+ assert.equal(footer(brandTokens(PAGE, brand(company), 'https://acme.example')), stamp(company));
53
+ }
54
+ });
55
+
56
+ test('clientBranding exposes the company site and tagline the footer stamps from', () => {
57
+ const c = clientBranding({ identity: { company: { name: 'Acme Corp', site: 'https://acme-corp.example', tagline: 'a rocket works' } } });
58
+ assert.deepEqual(c.identity.company, { name: 'Acme Corp', site: 'https://acme-corp.example', tagline: 'a rocket works' });
59
+ assert.deepEqual(clientBranding({}).identity.company, { name: '', site: '', tagline: '' });
60
+ });