@bongos/core 1.19.419 → 1.19.420

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 (39) hide show
  1. package/.bongos-core.json +58 -43
  2. package/.claude/skills/new-project/SKILL.md +22 -11
  3. package/clients/bongos-client/README.md +1 -1
  4. package/clients/bongos-client/bongos-client.global.js +2 -0
  5. package/clients/bongos-client/index.cjs +2 -0
  6. package/clients/bongos-client/index.d.ts +5 -1
  7. package/clients/bongos-client/index.mjs +2 -0
  8. package/docs/adr/0209-one-budget-for-the-account-existence-oracle.md +79 -0
  9. package/docs/adr/README.md +1 -0
  10. package/docs/api/openapi.json +103 -5
  11. package/docs/api-reference.md +4 -3
  12. package/docs/copy-inventory.md +186 -174
  13. package/docs/copy-registry.json +348 -238
  14. package/docs/file-map.md +1 -0
  15. package/docs/module-api-changelog.md +2 -0
  16. package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
  17. package/modules/onboarding/routes/access-requests.js +16 -3
  18. package/modules/platform-identity/platform-identity.js +5 -5
  19. package/modules/platform-identity/tests/platform-identity.mjs +20 -0
  20. package/modules/provisioning/migrations/provisioning_013_project_detail.sql +54 -0
  21. package/modules/provisioning/onboard-plan.js +13 -0
  22. package/modules/provisioning/provisioning.js +113 -5
  23. package/modules/provisioning/routes/provisioning.js +105 -0
  24. package/modules/provisioning/tests/provisioning.mjs +84 -0
  25. package/package-lock.json +2 -2
  26. package/package.json +1 -1
  27. package/public-landing/projects.html +161 -42
  28. package/scripts/gds/fitness.js +9 -0
  29. package/scripts/gds/onboard.js +41 -1
  30. package/scripts/gds/provision-net.js +7 -3
  31. package/src/bongos/middleware/rate-limit.js +68 -2
  32. package/src/bongos/routes/auth.js +19 -12
  33. package/src/module-api.js +13 -1
  34. package/tests/account_existence_rate_limit.mjs +375 -0
  35. package/tests/module_api.mjs +1 -0
  36. package/tests/onboard.mjs +56 -0
  37. package/tests/projects_hub.mjs +69 -0
  38. package/tests/projects_hub_pre_uat.mjs +27 -5
  39. package/tests/provision.mjs +20 -0
@@ -613,6 +613,75 @@ test('every table column has a source — Type is declared, never guessed', () =
613
613
  assert.match(SCRIPT, /type: state\.projType/, 'the create request carries the declared type');
614
614
  });
615
615
 
616
+ test('every per-step hook in show() still points at the panel it belongs to (renumbering guard)', () => {
617
+ // The renumbering risk is NOT the data-next attributes (guarded above) — it is
618
+ // show()'s side-effect hooks, which key off bare integers. Point one at the
619
+ // wrong panel and the wizard still navigates perfectly while silently never
620
+ // probing repo access, never re-checking the handle, or rendering a stale
621
+ // review. Derived from the panels map so this survives the next renumbering.
622
+ const map = SCRIPT.match(/var panels = \{([^}]*)\}/)[1];
623
+ const STEP = {};
624
+ for (const m of map.matchAll(/(\d+):\s*'([a-z-]+)'/g)) STEP[m[2]] = Number(m[1]);
625
+ const show = SCRIPT.slice(SCRIPT.indexOf('function show(step, moveFocus)'));
626
+ const body = show.slice(0, show.search(/\n\s*\/\* One delegated pair on the rail/));
627
+
628
+ assert.ok(body.includes(`if (step === ${STEP['panel-home']}) { syncHome();`),
629
+ 'the repo probe fires on the Home panel');
630
+ assert.ok(body.includes(`if (step === ${STEP['panel-address']}) { checkSlugAvailable(); renderAddrPanel(); }`),
631
+ 'the handle re-check fires on the Address panel');
632
+ assert.ok(body.includes(`if (step === ${STEP['panel-review']}) renderReview();`),
633
+ 'the review is rendered on the Review panel');
634
+ assert.ok(body.includes(`'wizDone', step === ${STEP['panel-done']}`),
635
+ 'the done panel is the one that takes the whole card');
636
+ // and the About panel claims no hook of its own — it has nothing to fetch
637
+ assert.ok(!body.includes(`if (step === ${STEP['panel-detail']})`),
638
+ 'the optional step needs no side effect; adding one would give it a way to fail');
639
+ });
640
+
641
+ test('the About step is OFFERED by every form and can be walked past — the skip is a real control (task 1002334)', () => {
642
+ const about = (HUB.match(/<section class="panel" id="panel-detail"[\s\S]*?<\/section>/) || [''])[0];
643
+ assert.ok(about, '#panel-detail ships as real markup');
644
+ assert.match(about, /id="f-desc"/, 'it asks what the owner is building — the publish gate half');
645
+ assert.match(about, /id="teamCards"/, 'and who is building it — what the recommendations read');
646
+ // the skip must be its OWN button: "leave it blank and press Next" is an
647
+ // escape hatch the visitor has to infer, which is not an optional step
648
+ assert.match(about, /id="skipDetail"/, 'the skip is a control, not an inference');
649
+ assert.match(about, /Optional/, 'and the panel says so before it asks');
650
+ // the consequence of skipping is stated where the choice is made
651
+ assert.match(SCRIPT, /stays off the public map until it has one/,
652
+ 'skipping names its cost (ADR 0182 D4), never leaves it to be discovered later');
653
+ // nothing in validateStep may gate the About step
654
+ const validate = SCRIPT.slice(SCRIPT.indexOf('function validateStep('));
655
+ const map = SCRIPT.match(/var panels = \{([^}]*)\}/)[1];
656
+ const aboutStep = Number((map.match(/(\d+):\s*'panel-detail'/) || [, 0])[1]);
657
+ assert.ok(aboutStep > 0, 'the About panel is in the map');
658
+ assert.ok(!validate.slice(0, validate.indexOf('function ', 1)).includes(`if (step === ${aboutStep}) {`),
659
+ 'the optional step has no validation clause — Next past an empty answer is the point');
660
+ });
661
+
662
+ test('the About answers travel on the create request, and an empty one is OMITTED not blanked', () => {
663
+ assert.match(SCRIPT, /description: String\(state\.description \|\| ''\)\.trim\(\) \|\| undefined/,
664
+ 'a skipped description is absent from the body — an empty string would read as answered');
665
+ assert.match(SCRIPT, /detail: state\.teamShape \? \{ team_shape: state\.teamShape \} : undefined/,
666
+ 'and an unanswered team shape is never guessed into the request');
667
+ // the review is the last screen before Create, so the skip's cost is restated there
668
+ const review = (SCRIPT.match(/function renderReview\(\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
669
+ assert.match(review, /off the public map until you add it/,
670
+ 'the review row states the consequence rather than a bare dash');
671
+ assert.match(review, /\['About',/, 'and the About answer has a row of its own');
672
+ });
673
+
674
+ test('a renumbering of the wizard invalidates old drafts — the key carries the version', () => {
675
+ // task 1002772 renumbered once and bumped to -v2; task 1002334 renumbered again.
676
+ // A draft replayed under a different numbering lands on the wrong panel with the
677
+ // rail already marking the skipped steps done.
678
+ const key = (SCRIPT.match(/var DRAFT_KEY = '([^']+)'/) || [, ''])[1];
679
+ assert.match(key, /-v\d+$/, 'the draft key is versioned');
680
+ const panels = (SCRIPT.match(/var panels = \{([^}]*)\}/) || [, ''])[1];
681
+ const count = [...panels.matchAll(/\d+:\s*'panel-/g)].length;
682
+ assert.equal(count, 8, 'gate + six numbered steps + done — bump DRAFT_KEY when this changes');
683
+ });
684
+
616
685
  test('header and body agree on the column count', () => {
617
686
  // a stray <td> shifts every cell after it — silent, and only visible on screen
618
687
  const n = ((HUB.match(/<thead>([\s\S]*?)<\/thead>/) || [, ''])[1].match(/<th/g) || []).length;
@@ -28,6 +28,20 @@ const fn = (name) => {
28
28
  return next < 0 ? rest : rest.slice(0, next);
29
29
  };
30
30
 
31
+ // The step indices are DERIVED from the panels map, not typed (task 1002334
32
+ // renumbered them a second time and every literal below broke). What these
33
+ // blocks actually claim is "the DONE panel" and "the step before it" — say
34
+ // that, and the next renumbering costs nothing.
35
+ const PANELS = (() => {
36
+ const m = SCRIPT.match(/var panels = \{([^}]*)\}/);
37
+ assert.ok(m, 'the panels map must exist');
38
+ const map = {};
39
+ for (const p of m[1].matchAll(/(\d+):\s*'([a-z-]+)'/g)) map[Number(p[1])] = p[2];
40
+ return map;
41
+ })();
42
+ const STEP = Object.fromEntries(Object.entries(PANELS).map(([n, id]) => [id, Number(n)]));
43
+ const DONE = STEP['panel-done'];
44
+
31
45
  /* ── the gate says why GitHub will ask for repository access ── */
32
46
  test('the gate explains the repo scope BEFORE GitHub does — on the path every founder takes', () => {
33
47
  const gate = (HUB.match(/<section class="panel" id="panel-gate"[\s\S]*?<\/section>/) || [''])[0];
@@ -203,7 +217,9 @@ test('the wizard’s 401 recoveries keep the repo scope they already granted', (
203
217
  });
204
218
 
205
219
  test('a resumed draft past Home still probes repo access before Create', () => {
206
- assert.match(SCRIPT, /else if \(step > 3 && step < 6 && authKnown && repoAccess === null\) ensureRepoAccess\(\);/);
220
+ assert.ok(SCRIPT.includes(
221
+ `else if (step > ${STEP['panel-home']} && step < ${DONE} && authKnown && repoAccess === null) ensureRepoAccess();`),
222
+ 'every step between Home and the done panel re-probes');
207
223
  });
208
224
 
209
225
  test('a half-typed custom domain says why Next is dead; a taken handle silences the format cheer', () => {
@@ -447,13 +463,16 @@ test('a manifest error says a plain sentence, folds the route text, and sends a
447
463
 
448
464
  test('the rail at the done panel keeps its marker on Create until the project answers', () => {
449
465
  const rail = fn('paintRail');
450
- assert.match(rail, /var landed = step === 6 && !!\(live && live\.instStatus === 'active'\);/);
451
- assert.match(rail, /var cur = step === 6 \? 5 : step;/);
466
+ assert.ok(rail.includes(`var landed = step === ${DONE} && !!(live && live.instStatus === 'active');`),
467
+ 'the rail only finishes on the done panel');
468
+ assert.ok(rail.includes(`var cur = step === ${DONE} ? ${DONE - 1} : step;`),
469
+ 'the done panel keeps the marker on the last numbered beat');
452
470
  assert.match(rail, /li\.classList\.toggle\('done', landed \|\| n < cur\);/);
453
471
  assert.match(rail, /if \(now\) li\.setAttribute\('aria-current', 'step'\); else li\.removeAttribute\('aria-current'\);/);
454
472
  assert.match(fn('show'), /paintRail\(step\);/, 'show() paints through it');
455
473
  assert.doesNotMatch(fn('show'), /classList\.toggle\('done'/, 'and no longer marks the rail on its own');
456
- assert.match(SCRIPT, /live\.instStatus = inst\.status; renderSteps\(\);\n\s*if \(state\.step === 6\) paintRail\(6\);/, 'the done-panel poll repaints when the status moves');
474
+ assert.ok(SCRIPT.includes(`if (state.step === ${DONE}) paintRail(${DONE});`),
475
+ 'the done-panel poll repaints when the status moves');
457
476
  assert.match(HUB, /<li class="now" data-step="1" aria-current="step">/, 'the static rail starts with the same marker');
458
477
  });
459
478
 
@@ -560,7 +579,10 @@ test('the hub has one main landmark, a heading for the map, a footer, and a skip
560
579
  });
561
580
 
562
581
  test('completed rail steps are a way back, the repo picker is honest ARIA, the card inerts the page', () => {
563
- assert.match(fn('paintRail'), /if \(step < 6 && n < cur\) \{\n\s*li\.setAttribute\('role', 'button'\);\n\s*li\.setAttribute\('tabindex', '0'\);\n\s*li\.setAttribute\('data-goto', String\(n\)\);/, 'only answered steps become buttons, and none once the project is filed');
582
+ assert.ok(fn('paintRail').includes(`if (step < ${DONE} && n < cur) {`),
583
+ 'only answered steps become buttons, and none once the project is filed');
584
+ assert.ok(fn('paintRail').includes("li.setAttribute('data-goto', String(n));"),
585
+ 'and an answered step carries the goto the delegated handler reads');
564
586
  assert.match(SCRIPT, /railEl\.addEventListener\('click'/);
565
587
  assert.match(SCRIPT, /railEl\.addEventListener\('keydown'/);
566
588
  assert.match(SCRIPT, /show\(Number\(li\.getAttribute\('data-goto'\)\), true\)/, 'a deliberate step: focus moves');
@@ -1673,6 +1673,26 @@ await ta('federateInstance(--apply): registers the client on the hub bound to th
1673
1673
  });
1674
1674
  });
1675
1675
 
1676
+ await ta('federateInstance: the project-detail answers ride to the catalog row — and a SKIP stays skipped (task 1002334)', async () => {
1677
+ await withEnv(HUB_ENV, async () => {
1678
+ // The instance row holds what the owner said at creation; the catalog row is
1679
+ // where the publish gate reads it (ADR 0182 D4). Dropping the answer on this
1680
+ // one hop would leave a project that DID describe itself in dark matter,
1681
+ // which would make the whole optional step decorative.
1682
+ const told = { ...coTenantWithDomain, type: 'game', description: 'A co-op farming game.' };
1683
+ const pi = fakePI();
1684
+ const deps = { apply: true, log: () => {}, platformIdentity: pi, hubKeys: hubKeysOn, db: { query: async () => ({ rows: [{ github_id: 'gh-owner-1' }] }) } };
1685
+ await P.federateInstance(told, deps);
1686
+ assert.equal(pi.calls[0].description, 'A co-op farming game.', 'the description reaches the hub registration');
1687
+ assert.equal(pi.calls[0].type, 'game', 'alongside the declared type it follows');
1688
+
1689
+ // the skipped case: absent, never an empty string invented on the way
1690
+ const skipped = fakePI();
1691
+ await P.federateInstance(coTenantWithDomain, { ...deps, platformIdentity: skipped });
1692
+ assert.equal(skipped.calls[0].description, undefined, 'a skipped interview passes nothing — upsertProject COALESCEs, so it nulls nothing either');
1693
+ });
1694
+ });
1695
+
1676
1696
  await ta('federateInstance(dry-run): registers NOTHING, returns a placeholder block (so webEnvBody can still compose)', async () => {
1677
1697
  await withEnv(HUB_ENV, async () => {
1678
1698
  const pi = fakePI();