@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.
- package/.bongos-core.json +58 -43
- package/.claude/skills/new-project/SKILL.md +22 -11
- 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 +5 -1
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0209-one-budget-for-the-account-existence-oracle.md +79 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +103 -5
- package/docs/api-reference.md +4 -3
- package/docs/copy-inventory.md +186 -174
- package/docs/copy-registry.json +348 -238
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +2 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
- package/modules/onboarding/routes/access-requests.js +16 -3
- package/modules/platform-identity/platform-identity.js +5 -5
- package/modules/platform-identity/tests/platform-identity.mjs +20 -0
- package/modules/provisioning/migrations/provisioning_013_project_detail.sql +54 -0
- package/modules/provisioning/onboard-plan.js +13 -0
- package/modules/provisioning/provisioning.js +113 -5
- package/modules/provisioning/routes/provisioning.js +105 -0
- package/modules/provisioning/tests/provisioning.mjs +84 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/projects.html +161 -42
- package/scripts/gds/fitness.js +9 -0
- package/scripts/gds/onboard.js +41 -1
- package/scripts/gds/provision-net.js +7 -3
- package/src/bongos/middleware/rate-limit.js +68 -2
- package/src/bongos/routes/auth.js +19 -12
- package/src/module-api.js +13 -1
- package/tests/account_existence_rate_limit.mjs +375 -0
- package/tests/module_api.mjs +1 -0
- package/tests/onboard.mjs +56 -0
- package/tests/projects_hub.mjs +69 -0
- package/tests/projects_hub_pre_uat.mjs +27 -5
- package/tests/provision.mjs +20 -0
package/tests/projects_hub.mjs
CHANGED
|
@@ -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.
|
|
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.
|
|
451
|
-
|
|
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.
|
|
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.
|
|
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');
|
package/tests/provision.mjs
CHANGED
|
@@ -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();
|