@bongos/core 1.19.936 → 1.19.938

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 (43) hide show
  1. package/.bongos-core.json +69 -44
  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/0281-an-instance-identity-is-its-own-unix-account-and-pg-role.md +2 -0
  8. package/docs/api/openapi.json +91 -2
  9. package/docs/api-reference.md +3 -2
  10. package/docs/module-api-changelog.md +4 -0
  11. package/docs/recipes/self-host.md +1 -1
  12. package/modules/government/board.js +31 -0
  13. package/modules/government/db.js +33 -0
  14. package/modules/government/migrations/government_018_passed_items_index.sql +31 -0
  15. package/modules/government/routes/government.js +7 -0
  16. package/modules/hall-ui/public/deploy.js +1 -1
  17. package/modules/ideas/inbox.js +11 -0
  18. package/modules/ideas/migrations/ideas_010_goal_ratification.sql +103 -0
  19. package/modules/ideas/module.json +1 -0
  20. package/modules/ideas/ratify.js +288 -1
  21. package/modules/ideas/routes/ratify-goal.js +163 -0
  22. package/modules/ideas/templates.js +16 -4
  23. package/modules/lifecycle/goal-from-idea.js +131 -0
  24. package/modules/lifecycle/lifecycle.js +14 -0
  25. package/modules/provisioning/core-upgrade.js +1 -1
  26. package/modules/provisioning/routes/core-upgrade.js +1 -1
  27. package/package-lock.json +2 -2
  28. package/package.json +1 -1
  29. package/scripts/gds/audit-authorship.sh +33 -0
  30. package/scripts/gds/db-backup-nightly.sh +1 -1
  31. package/scripts/gds/instance-apex.js +11 -1
  32. package/scripts/gds/provision-config.js +3 -2
  33. package/scripts/gds/provision-core-upgrade.js +7 -7
  34. package/scripts/gds/provision-process.js +2 -2
  35. package/scripts/gds/provision-repo.js +35 -10
  36. package/scripts/gds/provision-units.js +3 -3
  37. package/scripts/gds/provision.js +6 -3
  38. package/src/module-api.js +1 -1
  39. package/tests/core_upgrade_runner.mjs +10 -7
  40. package/tests/government_seed.mjs +1 -0
  41. package/tests/idea_goal_ratification.mjs +553 -0
  42. package/tests/provision.mjs +44 -7
  43. package/tests/provision_account_identity.mjs +16 -0
@@ -18,6 +18,8 @@ import { join } from 'node:path';
18
18
 
19
19
  const require = createRequire(import.meta.url);
20
20
  const P = require('../scripts/gds/provision.js');
21
+ const AP = require('../scripts/gds/instance-apex.js');
22
+ const RP = require('../scripts/gds/provision-repo.js');
21
23
 
22
24
  // The env-var prefix a provisioned instance resolves = the envPrefix an instance INHERITS from
23
25
  // the neutral pack (task 1003740: webEnvBody no longer pins a branding pack anywhere — the
@@ -40,6 +42,11 @@ process.env.PROVISION_GIT_SSH_ALIAS = 'github-deploy';
40
42
  // The probe itself is covered directly, with an injected fs.
41
43
  process.env.PROVISION_PROCESS_MANAGER = 'systemd';
42
44
 
45
+ // And the app user (task 1002719): unpinned it is whoever runs the suite. Pinned to a name
46
+ // that is nobody's real account, so an assertion passes only if the value came through the
47
+ // resolver — a leftover literal cannot produce it.
48
+ process.env.PROVISION_APP_USER = 'appsvc';
49
+
43
50
  let passed = 0, failed = 0;
44
51
  function t(name, fn) {
45
52
  try { fn(); passed++; console.log(` PASS ${name}`); }
@@ -167,11 +174,16 @@ t('buildCorePinRefreshCommit (the commit step): git sequence, token-URL push, an
167
174
  // must not attribute its commits to the hub. Assert against the resolver so
168
175
  // this pins the behaviour, not one instance's literal.
169
176
  assert.deepEqual(withTok.config, [
170
- 'git -C /srv/bongos/spike-harbor config user.name "Cloud Bongos"',
177
+ `git -C /srv/bongos/spike-harbor config user.name "${AP.provisionerBotName()}"`,
171
178
  `git -C /srv/bongos/spike-harbor config user.email "${P.provisionerBotEmail()}"`,
172
179
  ]);
173
180
  assert.equal(P.provisionerBotEmail({ PROVISION_BOT_EMAIL: 'ops@acme.test' }), 'ops@acme.test', 'env override wins');
174
181
  assert.doesNotMatch(P.provisionerBotEmail({}), /example/i, 'never a founder-host address');
182
+ // task 1002719: the NAME comes from the pack too, and one the shell cannot quote falls back.
183
+ assert.equal(AP.provisionerBotName('Acme Forge'), 'Acme Forge', 'the pack\'s product name');
184
+ for (const unsafe of ['Acme "Forge"', 'a$(id)', 'a`id`', 'a\\b', 'a\nb', '']) {
185
+ assert.equal(AP.provisionerBotName(unsafe), 'Cloud Bongos', `falls back on ${JSON.stringify(unsafe)}`);
186
+ }
175
187
  assert.equal(withTok.add, 'git -C /srv/bongos/spike-harbor add -A');
176
188
  assert.match(withTok.commit, /@bongos\/core 1\.19\.2 -> 1\.19\.196 \(task 1003050\)/);
177
189
  assert.match(withTok.push, /store --file=\/tmp\/cred.*x-access-token@github\.com\/jaxri\/spike-harbor\.git HEAD:main/);
@@ -199,7 +211,7 @@ t('backup units: per-instance names + the DB set on the unit (fixes the task-221
199
211
  // the DB is pinned on the unit → no fallback to the nonexistent example default
200
212
  assert.match(svc, /Environment=GDS_BACKUP_DB=mercury/);
201
213
  assert.match(svc, /Environment=GDS_BACKUP_DIR=\/var\/backups\/gds/);
202
- assert.match(svc, new RegExp(`User=${reEsc('lars')}`), 'runs as the app user (mirrors serviceUnit)');
214
+ assert.match(svc, new RegExp(`User=${reEsc('appsvc')}`), 'runs as the app user (mirrors serviceUnit)');
203
215
  assert.match(svc, /ReadWritePaths=\/var\/backups\/gds/, 'the only writable path is the backup dir');
204
216
  const timer = P.backupTimer(coTenant);
205
217
  assert.match(timer, /OnCalendar=\*-\*-\* 04:00:00 UTC/);
@@ -466,7 +478,7 @@ await ta('provisionInstance(co-tenant) composes db → migrate → service → h
466
478
  assert.ok(writes.some((w) => /\/mercury\.service$/.test(w)), 'wrote systemd unit');
467
479
  assert.ok(writes.some((w) => /\/mercury\.caddy$/.test(w)), 'wrote per-instance Caddy snippet');
468
480
  // task 2213: EVERY instance is born with backups — dir + per-instance timer/service
469
- assert.match(joined, /install -d -o lars -g lars -m 0750 \/var\/backups\/gds/, 'creates the backup dir');
481
+ assert.match(joined, /install -d -o appsvc -g \$\(id -gn appsvc\) -m 0750 \/var\/backups\/gds/, 'creates the backup dir');
470
482
  assert.match(joined, /systemctl enable --now mercury-db-backup\.timer/, 'enables the per-instance backup timer');
471
483
  assert.ok(writes.some((w) => /\/mercury-db-backup\.service$/.test(w)), 'wrote backup service unit');
472
484
  assert.ok(writes.some((w) => /\/mercury-db-backup\.timer$/.test(w)), 'wrote backup timer unit');
@@ -626,7 +638,8 @@ t('systemd manager: emits byte-identical commands + unit paths to the inline str
626
638
  PM.systemd.installBackup(INST, io2);
627
639
  // The backup DIR moved in here with the schedule it serves: a manager that installs no
628
640
  // schedule must not create it either. Order is unchanged — dir, units, reload, enable.
629
- assert.match(io2.cmds[0], /^sudo install -d -o \S+ -g \S+ -m 0750 /);
641
+ // The group is the app user's PRIMARY group, looked up on the box (task 1002719).
642
+ assert.match(io2.cmds[0], /^sudo install -d -o appsvc -g \$\(id -gn appsvc\) -m 0750 /);
630
643
  assert.deepEqual(io2.cmds.slice(1), ['sudo systemctl daemon-reload', 'sudo systemctl enable --now demo-db-backup.timer']);
631
644
  assert.deepEqual(io2.files.map((f) => f.path),
632
645
  ['/etc/systemd/system/demo-db-backup.service', '/etc/systemd/system/demo-db-backup.timer']);
@@ -1329,7 +1342,7 @@ await ta('dedicatedUserData bakes the nightly backup: unit files (b64) + dir + t
1329
1342
  const b64 = cloudInit.match(/content: ([A-Za-z0-9+/=]+)/)[1];
1330
1343
  assert.match(Buffer.from(b64, 'base64').toString('utf8'), /Environment=GDS_BACKUP_DB=mercury/);
1331
1344
  // and the dir is created + the timer enabled on first boot
1332
- assert.match(cloudInit, /install -d -o lars -g lars -m 0750 \/var\/backups\/gds/);
1345
+ assert.match(cloudInit, /install -d -o appsvc -g \$\(id -gn appsvc\) -m 0750 \/var\/backups\/gds/);
1333
1346
  assert.match(cloudInit, /enable --now mercury mercury-db-backup\.timer/);
1334
1347
  });
1335
1348
 
@@ -2932,7 +2945,31 @@ t('ensureInstanceUserCmd: idempotent SYSTEM account, nologin, state dir re-asser
2932
2945
  });
2933
2946
 
2934
2947
  t('grantInstanceRepoReadCmd: adds the instance account to the app-user group so the unit can exec its WorkingDirectory', () => {
2935
- assert.equal(P.grantInstanceRepoReadCmd(coTenant, { privileged: true }), 'sudo usermod -aG lars bongos-mercury');
2948
+ assert.equal(P.grantInstanceRepoReadCmd(coTenant, { privileged: true }), 'sudo usermod -aG $(id -gn appsvc) bongos-mercury');
2949
+ });
2950
+
2951
+ t('appUser: PROVISION_APP_USER, else the account the runner runs as — never a named founder (task 1002719)', () => {
2952
+ const runner = (username) => () => ({ username });
2953
+ assert.equal(RP.appUser({ env: { PROVISION_APP_USER: 'ops' }, userInfo: runner('deploy') }), 'ops', 'the pin wins');
2954
+ assert.equal(RP.appUser({ env: {}, userInfo: runner('deploy') }), 'deploy', "unpinned: the runner's own account");
2955
+ assert.equal(RP.appUser(), 'appsvc', 'the suite pin is what every generator above rendered');
2956
+ });
2957
+
2958
+ t('appUser: refuses root and any name a shell command or unit file could not carry', () => {
2959
+ const runner = (username) => () => ({ username });
2960
+ assert.throws(() => RP.appUser({ env: {}, userInfo: runner('root') }), /is root — set PROVISION_APP_USER/);
2961
+ assert.throws(() => RP.appUser({ env: {}, userInfo: () => { throw new Error('ENOENT'); } }),
2962
+ /"" is not a usable unix account name/, 'a uid with no passwd entry names the knob');
2963
+ for (const bad of ['a b', 'x;rm -rf /', '$(id)', 'Admin', '-o', 'a'.repeat(33)]) {
2964
+ assert.throws(() => RP.appUser({ env: { PROVISION_APP_USER: bad }, userInfo: runner('deploy') }),
2965
+ /not a usable unix account name/, `refuses ${JSON.stringify(bad)}`);
2966
+ }
2967
+ // …and the generators go through it rather than rendering the bad value.
2968
+ process.env.PROVISION_APP_USER = 'root';
2969
+ try {
2970
+ assert.throws(() => P.backupService(coTenant), /is root/);
2971
+ assert.throws(() => P.grantInstanceRepoReadCmd(coTenant), /is root/);
2972
+ } finally { process.env.PROVISION_APP_USER = 'appsvc'; }
2936
2973
  });
2937
2974
 
2938
2975
  t('dbRoleCmd: LOGIN + password, upserted — CREATE on a fresh box, ALTER on a re-provision (the rotation path)', () => {
@@ -3052,7 +3089,7 @@ t('dedicatedUserData: the droplet runs the SAME account + role steps, in the sam
3052
3089
  const ud = P.dedicatedUserData(coTenant);
3053
3090
  const at = (needle) => ud.indexOf(needle);
3054
3091
  assert.ok(at('useradd --system') > -1, 'the droplet creates the account too');
3055
- assert.ok(at('usermod -aG lars') > -1);
3092
+ assert.ok(at('usermod -aG $(id -gn appsvc)') > -1);
3056
3093
  assert.ok(at('useradd --system') < at('createdb mercury'), 'account before the DB');
3057
3094
  assert.ok(at('migrate.sh') < at('CREATE ROLE %I'), 'the role grants must follow the migrate that created the tables');
3058
3095
  assert.ok(at('CREATE ROLE %I') < at('GRANT ALL ON ALL TABLES'), 'role before its grants');
@@ -12,6 +12,9 @@ const require = createRequire(import.meta.url);
12
12
  const P = require('../scripts/gds/provision.js');
13
13
  const { accountPlaneGap, accountPreflight } = require('../scripts/gds/provision-repo.js');
14
14
 
15
+ // The app user is derived from whoever runs the suite unless pinned (task 1002719).
16
+ process.env.PROVISION_APP_USER = 'appsvc';
17
+
15
18
  const PROBE = `sh -c 'test "$(id -u)" = 0'`;
16
19
  const isRoot = () => ({ ok: true, stdout: '' });
17
20
  // What a shell that RAN reports: exit 1 from `test`, or 127 when sudo is not installed.
@@ -133,3 +136,16 @@ test('provision: a box where the runner reaches root is unchanged — the live s
133
136
  const idx = (re) => h.cmds.findIndex((c) => re.test(c));
134
137
  assert.ok(idx(/id -u.*= 0/) < idx(/useradd --system/), 'the probe precedes the account it guards');
135
138
  });
139
+
140
+ test('provision: an app user that resolves to root is refused before a single write (task 1002719)', async () => {
141
+ const h = harness();
142
+ process.env.PROVISION_APP_USER = 'root'; // what an unpinned runner running as root resolves
143
+ try {
144
+ const r = await P.provisionInstance(coTenant, h.deps);
145
+ assert.equal(r.ok, false);
146
+ assert.match(r.error, /is root — set PROVISION_APP_USER/);
147
+ assert.ok(!h.ran(/useradd/), 'refused before step 1c makes an account the run could not finish');
148
+ assert.equal(h.writes.length, 0);
149
+ assert.deepEqual(h.statuses, [], 'the instance never flips to provisioning');
150
+ } finally { process.env.PROVISION_APP_USER = 'appsvc'; }
151
+ });