@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
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.419",
3
+ "version": "1.19.420",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.419",
9
+ "version": "1.19.420",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.419",
3
+ "version": "1.19.420",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -95,14 +95,17 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
95
95
  .wizField{display:block;margin-bottom:18px;}
96
96
  .wizField>span{display:block;font-size:13.5px;font-weight:700;color:var(--ink-soft);margin-bottom:7px;letter-spacing:0.2px;}
97
97
  .wizField small{font-weight:500;color:var(--ink-faint);}
98
- #view-new input[type=text]{
98
+ /* the About step's description is prose, so it is a textarea — it wears the
99
+ same control skin as every other field (task 1002334), never its own */
100
+ #view-new input[type=text], #view-new textarea{
99
101
  width:100%;font-family:var(--font-body);font-size:17px;font-weight:600;color:var(--ink);
100
102
  background:var(--bg-card);border:2px solid var(--ctl-line);border-radius:14px;padding:13px 16px;outline:none;
101
103
  transition:border-color .15s ease, box-shadow .15s ease;
102
104
  }
105
+ #view-new textarea{font-size:15.5px;line-height:1.5;resize:vertical;min-height:84px;display:block;}
103
106
  /* no fade toward the card — a 70% mix washed placeholders to 2.89:1 (task 1002749) */
104
- #view-new input[type=text]::placeholder{color:var(--ink-faint);font-weight:500;}
105
- #view-new input[type=text]:focus{border-color:var(--accent);box-shadow:0 0 0 4px color-mix(in srgb,var(--accent) 13%,transparent);}
107
+ #view-new input[type=text]::placeholder, #view-new textarea::placeholder{color:var(--ink-faint);font-weight:500;}
108
+ #view-new input[type=text]:focus, #view-new textarea:focus{border-color:var(--accent);box-shadow:0 0 0 4px color-mix(in srgb,var(--accent) 13%,transparent);}
106
109
  #view-new input[type=text].bad{border-color:var(--err);}
107
110
  .hint{font-size:13px;font-weight:600;color:var(--ink-faint);margin-top:7px;min-height:18px;}
108
111
  .hint.ok{color:var(--ok);}
@@ -146,6 +149,9 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
146
149
  .humanSteps .ghbtn{font-size:14px;padding:9px 16px;margin-top:10px;}
147
150
 
148
151
  .nav{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:26px;}
152
+ /* two controls sharing one end of a nav row — the About step's Skip beside its
153
+ Next (task 1002334). Wraps on a narrow card rather than crushing either. */
154
+ .navPair{display:inline-flex;align-items:center;gap:10px;flex-wrap:wrap;justify-content:flex-end;}
149
155
  .btn.big{width:100%;justify-content:center;font-size:18px;padding:17px 30px;}
150
156
 
151
157
  /* review list */
@@ -748,7 +754,7 @@ summary{min-height:24px;padding:3px 0;}
748
754
 
749
755
  <!-- ══════════════════ VIEW · NEW PROJECT ══════════════════════════════════
750
756
  The whole create wizard, absorbed from /create (task 1002763; direction
751
- §4 — that route now 302s here). Seven panels behind the five-beat rail; the
757
+ §4 — that route now 302s here). Eight panels behind the six-beat rail; the
752
758
  wizard block in the script below drives them. Same trust boundary as
753
759
  ever: every button here only ENQUEUES a provisioning request (ADR 0111).
754
760
  The name panel carries a static `on` so a no-script visitor still reads
@@ -759,9 +765,10 @@ summary{min-height:24px;padding:3px 0;}
759
765
  <ol class="steps" aria-label="Creation steps">
760
766
  <li class="now" data-step="1" aria-current="step"><span class="snum">1</span><span class="slbl">Name</span></li>
761
767
  <li data-step="2"><span class="snum">2</span><span class="slbl">Type</span></li>
762
- <li data-step="3"><span class="snum">3</span><span class="slbl">Home</span></li>
763
- <li data-step="4"><span class="snum">4</span><span class="slbl">Address</span></li>
764
- <li data-step="5"><span class="snum">5</span><span class="slbl">Create</span></li>
768
+ <li data-step="3"><span class="snum">3</span><span class="slbl">About</span></li>
769
+ <li data-step="4"><span class="snum">4</span><span class="slbl">Home</span></li>
770
+ <li data-step="5"><span class="snum">5</span><span class="slbl">Address</span></li>
771
+ <li data-step="6"><span class="snum">6</span><span class="slbl">Create</span></li>
765
772
  </ol>
766
773
 
767
774
  <div class="createSplit">
@@ -841,11 +848,45 @@ summary{min-height:24px;padding:3px 0;}
841
848
  </form>
842
849
  </section>
843
850
 
844
- <!-- step 3 · home (repo + mode) -->
851
+ <!-- step 3 · about (task 1002334) — the OPTIONAL project-detail step.
852
+ The one panel in the wizard that can be walked past without an
853
+ answer, so the skip is a real button of its own, sitting beside
854
+ Next rather than hidden as "leave it blank and press Next" — an
855
+ optional step whose escape hatch has to be inferred is not
856
+ optional in practice. What skipping costs is stated ON the panel
857
+ (ADR 0182 D4: no description ⇒ the project is off the public map
858
+ until it has one), because a consequence discovered afterwards is
859
+ not a choice the owner made. -->
860
+ <section class="panel" id="panel-detail" aria-labelledby="detail-h">
861
+ <h1 id="detail-h">Tell us about it</h1>
862
+ <p class="lede">Optional — skip it and nothing about your project changes. Answering shapes what we set up for you, and a description is what puts the project on the public map.</p>
863
+ <form data-next="4">
864
+ <label class="wizField"><span>What are you building? <small>(a sentence or two)</small></span>
865
+ <textarea id="f-desc" rows="3" maxlength="600" placeholder="A co-op farming game where the seasons never repeat."></textarea>
866
+ </label>
867
+ <div class="wizField"><span id="teamLbl">Who’s building it?</span>
868
+ <div class="typeCards" id="teamCards" role="group" aria-labelledby="teamLbl">
869
+ <button type="button" class="tpill" data-team="solo" aria-pressed="false">Just me</button>
870
+ <button type="button" class="tpill" data-team="small-team" aria-pressed="false">A small team</button>
871
+ <button type="button" class="tpill" data-team="community" aria-pressed="false">An open community</button>
872
+ </div>
873
+ </div>
874
+ <div class="reassure" id="detailSkipNote"></div>
875
+ <div class="nav">
876
+ <button type="button" class="btn ghost" data-back="2">← Back</button>
877
+ <span class="navPair">
878
+ <button type="button" class="btn ghost" id="skipDetail">Skip for now</button>
879
+ <button type="submit" class="btn">Next →</button>
880
+ </span>
881
+ </div>
882
+ </form>
883
+ </section>
884
+
885
+ <!-- step 4 · home (repo + mode) -->
845
886
  <section class="panel" id="panel-home" aria-labelledby="home-h">
846
887
  <h1 id="home-h">Where the code lives</h1>
847
888
  <p class="lede">Every project is a real GitHub repository that you own. Start from a clean slate, or bring a repo you already have.</p>
848
- <form data-next="4">
889
+ <form data-next="5">
849
890
  <div class="choices" role="group" aria-label="Starting point">
850
891
  <button type="button" class="choice" id="c-green" aria-pressed="true">
851
892
  <!-- token-built, not platform orange — public-landing/ ships wholesale to
@@ -904,13 +945,13 @@ summary{min-height:24px;padding:3px 0;}
904
945
  </div>
905
946
  <div class="hint" id="h-home">&nbsp;</div>
906
947
  <div class="nav">
907
- <button type="button" class="btn ghost" data-back="2">← Back</button>
948
+ <button type="button" class="btn ghost" data-back="3">← Back</button>
908
949
  <button type="submit" class="btn">Next →</button>
909
950
  </div>
910
951
  </form>
911
952
  </section>
912
953
 
913
- <!-- step 4 · address — a CONFIRMATION, not a form (task 1002704: every project
954
+ <!-- step 5 · address — a CONFIRMATION, not a form (task 1002704: every project
914
955
  gets its slug subdomain assigned up front). The domainless default was a
915
956
  proven dead-end (QA walk, task 1002702).
916
957
  Three choices, all legible without opening anything (task 1002898 N8):
@@ -922,7 +963,7 @@ summary{min-height:24px;padding:3px 0;}
922
963
  <section class="panel" id="panel-address" aria-labelledby="addr-h">
923
964
  <h1 id="addr-h">Its address in the sky</h1>
924
965
  <p class="lede">The handle names your project on the platform — and its address comes with it. Once it stands up, a few GitHub steps (the next screen walks you through them) switch sign-in on.</p>
925
- <form data-next="5">
966
+ <form data-next="6">
926
967
  <label class="wizField"><span>Project handle</span>
927
968
  <input type="text" id="f-slug" placeholder="mercury" autocomplete="off" maxlength="40" spellcheck="false">
928
969
  <div class="hint" id="h-slug">&nbsp;</div>
@@ -944,13 +985,13 @@ summary{min-height:24px;padding:3px 0;}
944
985
  </label>
945
986
  </div>
946
987
  <div class="nav">
947
- <button type="button" class="btn ghost" data-back="3">← Back</button>
988
+ <button type="button" class="btn ghost" data-back="4">← Back</button>
948
989
  <button type="submit" class="btn">Next →</button>
949
990
  </div>
950
991
  </form>
951
992
  </section>
952
993
 
953
- <!-- step 5 · review + create -->
994
+ <!-- step 6 · review + create -->
954
995
  <section class="panel" id="panel-review" aria-labelledby="rev-h">
955
996
  <h1 id="rev-h">Ready to roll</h1>
956
997
  <p class="lede">One tap files the request and the platform starts standing your project up — server, database, address. Here's the beat you're about to drop:</p>
@@ -958,7 +999,7 @@ summary{min-height:24px;padding:3px 0;}
958
999
  <div class="alert" id="createAlert" role="alert"></div>
959
1000
  <div class="nav" style="flex-direction:column;align-items:stretch;">
960
1001
  <button type="button" class="btn big" id="doCreate">Create my project</button>
961
- <button type="button" class="btn ghost" data-back="4" style="align-self:flex-start;margin-top:10px;">← Back</button>
1002
+ <button type="button" class="btn ghost" data-back="5" style="align-self:flex-start;margin-top:10px;">← Back</button>
962
1003
  </div>
963
1004
  <p class="finePrint">This files the request; the control plane does the real standup and nothing touches servers from this page. You'll get the few steps only you can do right after.</p>
964
1005
  </section>
@@ -2488,13 +2529,16 @@ summary{min-height:24px;padding:3px 0;}
2488
2529
  var root = $('view-new');
2489
2530
 
2490
2531
  /* ── state ── */
2491
- /* -v2 since task 1002772: the step indices renumbered (Type joined as 2),
2492
- so a draft written under the five-step numbering must be ignored, not
2493
- reinterpreted — a stale step 4 would land on Address and skip the Type
2494
- gate with the rail already marking it done. */
2495
- var DRAFT_KEY = 'cb-create-draft-v2';
2532
+ /* -v3 since task 1002334: the indices renumbered AGAIN (About joined as 3),
2533
+ so a draft written under either older numbering must be ignored, not
2534
+ reinterpreted — a stale step 4 meant Address under v1 and Home under v3,
2535
+ and replaying it would land on the wrong panel with the rail already
2536
+ marking the skipped ones done. Bump this key on EVERY renumbering. */
2537
+ var DRAFT_KEY = 'cb-create-draft-v3';
2496
2538
  var CREATED_KEY = 'cb-created-instance';
2497
- var state = { step: 0, name: '', projType: null, mode: 'greenfield', owner: '', ownerTouched: false, repo: '', repoTouched: false, slug: '', slugTouched: false, domain: '', noAddress: false };
2539
+ /* `description` + `teamShape` are the optional About step (task 1002334) —
2540
+ both legitimately stay empty, and empty is what the create request omits. */
2541
+ var state = { step: 0, name: '', projType: null, description: '', teamShape: null, mode: 'greenfield', owner: '', ownerTouched: false, repo: '', repoTouched: false, slug: '', slugTouched: false, domain: '', noAddress: false };
2498
2542
  try { Object.assign(state, JSON.parse(sessionStorage.getItem(DRAFT_KEY) || '{}')); } catch (e) {}
2499
2543
  var authKnown = false; /* flips true when the boot's /me settles, either way */
2500
2544
 
@@ -2508,7 +2552,7 @@ summary{min-height:24px;padding:3px 0;}
2508
2552
  state.name = handedName.trim().slice(0, 80);
2509
2553
  state.slug = deriveSlug(state.name); state.slugTouched = false;
2510
2554
  state.repo = deriveRepo(state.name); state.repoTouched = false;
2511
- state.step = state.step >= 1 && state.step <= 5 ? state.step : 1;
2555
+ state.step = state.step >= 1 && state.step <= 6 ? state.step : 1;
2512
2556
  try { sessionStorage.setItem(DRAFT_KEY, JSON.stringify(state)); } catch (e) {}
2513
2557
  }
2514
2558
  } catch (e) {}
@@ -2700,16 +2744,17 @@ summary{min-height:24px;padding:3px 0;}
2700
2744
  }
2701
2745
 
2702
2746
  /* ── step navigation ──
2703
- Six steps since task 1002772: gate 0 · name 1 · type 2 · home 3 ·
2704
- address 4 · review 5 · done 6 (the rail shows 1-5; done marks it all). */
2705
- var panels = { 0: 'panel-gate', 1: 'panel-name', 2: 'panel-type', 3: 'panel-home', 4: 'panel-address', 5: 'panel-review', 6: 'panel-done' };
2747
+ Seven steps since task 1002334: gate 0 · name 1 · type 2 · about 3 ·
2748
+ home 4 · address 5 · review 6 · done 7 (the rail shows 1-6; done marks it
2749
+ all). About is the one step Next can pass with nothing filled in. */
2750
+ var panels = { 0: 'panel-gate', 1: 'panel-name', 2: 'panel-type', 3: 'panel-detail', 4: 'panel-home', 5: 'panel-address', 6: 'panel-review', 7: 'panel-done' };
2706
2751
  /* the rail at the done panel (task 1003244): "done" is the Create step
2707
2752
  still running — the marker stays on 5 until the project actually
2708
2753
  answers, and only then does the whole rail read finished. aria-current
2709
2754
  tells a screen reader the same thing. */
2710
2755
  function paintRail(step) {
2711
- var landed = step === 6 && !!(live && live.instStatus === 'active');
2712
- var cur = step === 6 ? 5 : step;
2756
+ var landed = step === 7 && !!(live && live.instStatus === 'active');
2757
+ var cur = step === 7 ? 6 : step;
2713
2758
  root.querySelectorAll('.steps li').forEach(function (li) {
2714
2759
  var n = Number(li.getAttribute('data-step'));
2715
2760
  var now = !landed && n === cur;
@@ -2720,7 +2765,7 @@ summary{min-height:24px;padding:3px 0;}
2720
2765
  the button role, the tab stop and the data-goto the delegated handler
2721
2766
  below reads. The done panel offers none — the project is filed, and
2722
2767
  there is nothing behind it to change. */
2723
- if (step < 6 && n < cur) {
2768
+ if (step < 7 && n < cur) {
2724
2769
  li.setAttribute('role', 'button');
2725
2770
  li.setAttribute('tabindex', '0');
2726
2771
  li.setAttribute('data-goto', String(n));
@@ -2739,7 +2784,7 @@ summary{min-height:24px;padding:3px 0;}
2739
2784
  paintRail(step);
2740
2785
  saveDraft();
2741
2786
  /* the done step takes the whole card — the bongo pair steps aside */
2742
- root.querySelector('.createSplit').classList.toggle('wizDone', step === 6);
2787
+ root.querySelector('.createSplit').classList.toggle('wizDone', step === 7);
2743
2788
  /* focus moves ONLY on a deliberate step (the hub rule setView already
2744
2789
  applies to this view: never on boot, never on a background settle) */
2745
2790
  if (moveFocus) {
@@ -2749,15 +2794,15 @@ summary{min-height:24px;padding:3px 0;}
2749
2794
  /* the repo probe is authenticated — never fire it before /me settles
2750
2795
  (a pre-auth probe 401s for strangers and, worse, could seed the
2751
2796
  draft's owner with myLogin()'s placeholder) */
2752
- if (step === 3) { syncHome(); if (authKnown) ensureRepoAccess(); }
2797
+ if (step === 4) { syncHome(); if (authKnown) ensureRepoAccess(); }
2753
2798
  /* a resumed draft can land PAST the Home step with the probe never run —
2754
2799
  Create would then file a repo it promised to make but never did */
2755
- else if (step > 3 && step < 6 && authKnown && repoAccess === null) ensureRepoAccess();
2800
+ else if (step > 4 && step < 7 && authKnown && repoAccess === null) ensureRepoAccess();
2756
2801
  /* re-check on every entry, not just on typing: a resumed draft arrives here with
2757
2802
  a handle nobody has asked the server about, and a handle free ten minutes ago
2758
2803
  may not be now (task 1002898 N10) */
2759
- if (step === 4) { checkSlugAvailable(); renderAddrPanel(); }
2760
- if (step === 5) renderReview();
2804
+ if (step === 5) { checkSlugAvailable(); renderAddrPanel(); }
2805
+ if (step === 6) renderReview();
2761
2806
  }
2762
2807
 
2763
2808
  /* One delegated pair on the rail, so paintRail can add and drop data-goto
@@ -2850,6 +2895,65 @@ summary{min-height:24px;padding:3px 0;}
2850
2895
  });
2851
2896
  paintType(state.projType || null);
2852
2897
 
2898
+ /* ── the OPTIONAL About step (task 1002334) ──
2899
+ Two answers, both skippable. The description is half the publish gate
2900
+ (ADR 0182 D4) and the team shape is what the setup recommendations read;
2901
+ neither gates the standup, and neither is ever guessed on the owner's
2902
+ behalf. paintDetail keeps the consequence visible while they are still on
2903
+ the panel — it flips between "here's what you'll get" and "here's what
2904
+ skipping costs", so pressing Skip is an informed choice rather than a
2905
+ discovery made three screens later. */
2906
+ var TEAM_META = {
2907
+ 'solo': 'Just you. We keep the setup light and skip the joining machinery.',
2908
+ 'small-team': 'A handful of people you know. We suggest a setup built around invites.',
2909
+ 'community': 'Open to whoever turns up. We suggest a setup built for people applying to join.'
2910
+ };
2911
+ function teamLabel(t) {
2912
+ var b = t && root.querySelector('#teamCards .tpill[data-team="' + t + '"]');
2913
+ return b ? b.textContent : '';
2914
+ }
2915
+ function paintTeam(t) {
2916
+ root.querySelectorAll('#teamCards .tpill').forEach(function (b) {
2917
+ b.setAttribute('aria-pressed', String(b.getAttribute('data-team') === t));
2918
+ });
2919
+ }
2920
+ function paintDetail() {
2921
+ var note = $('detailSkipNote');
2922
+ var told = String(state.description || '').trim();
2923
+ var parts = [];
2924
+ if (!told) {
2925
+ parts.push('No description yet — your project still stands up, it just stays off the public map until it has one. You can add it any time from its page.');
2926
+ } else {
2927
+ parts.push('That description is what puts your project on the public map.');
2928
+ }
2929
+ if (state.teamShape && TEAM_META[state.teamShape]) parts.push(TEAM_META[state.teamShape]);
2930
+ note.textContent = parts.join(' ');
2931
+ note.classList.add('on');
2932
+ }
2933
+ $('f-desc').value = state.description || '';
2934
+ $('f-desc').addEventListener('input', function () {
2935
+ state.description = this.value.slice(0, 600);
2936
+ paintDetail();
2937
+ saveDraft();
2938
+ });
2939
+ root.querySelectorAll('#teamCards .tpill').forEach(function (b) {
2940
+ b.addEventListener('click', function () {
2941
+ /* clicking the chosen pill again UNSAYS it — the only way back to "I did
2942
+ not answer", which an optional question has to keep offering */
2943
+ var pick = b.getAttribute('data-team');
2944
+ state.teamShape = state.teamShape === pick ? null : pick;
2945
+ paintTeam(state.teamShape);
2946
+ paintDetail();
2947
+ saveDraft();
2948
+ });
2949
+ });
2950
+ paintTeam(state.teamShape || null);
2951
+ paintDetail();
2952
+ /* Skip = go on with whatever is (or isn't) filled in. It deliberately does
2953
+ NOT erase an answer already given: a visitor who typed a description and
2954
+ then pressed Skip meant "I'm done here", not "throw that away". */
2955
+ $('skipDetail').addEventListener('click', function () { show(4, true); });
2956
+
2853
2957
  /* ── GitHub-integrated repo picker (adopt) + auto-create (fresh) — task 1002099 / ADR 0143.
2854
2958
  Progressive enhancement: probe GET /github/repos once we reach the Home step.
2855
2959
  200 → we hold repo access → show the picker (adopt) / name field (fresh);
@@ -3046,7 +3150,9 @@ summary{min-height:24px;padding:3px 0;}
3046
3150
  }
3047
3151
  return true;
3048
3152
  }
3049
- if (step === 3) {
3153
+ /* step 3 (About) has no clause: it is the optional one, and Next past an
3154
+ empty answer is the whole point (task 1002334). */
3155
+ if (step === 4) {
3050
3156
  var homeErr = function (msg) { $('h-home').textContent = msg; $('h-home').className = 'hint err'; return false; };
3051
3157
  if (repoAccess === 'need-connect') return homeErr('Allow GitHub repo access to pick or create your repository — or choose “enter it manually”.');
3052
3158
  if (repoAccess === true && state.mode !== 'adopt') {
@@ -3061,7 +3167,7 @@ summary{min-height:24px;padding:3px 0;}
3061
3167
  $('h-home').innerHTML = '&nbsp;'; $('h-home').className = 'hint';
3062
3168
  return true;
3063
3169
  }
3064
- if (step === 4) {
3170
+ if (step === 5) {
3065
3171
  if (!validSlug(state.slug)) {
3066
3172
  $('h-slug').textContent = '2–40 lowercase letters, numbers, or dashes; no leading/trailing/double dash.';
3067
3173
  $('h-slug').className = 'hint err'; $('f-slug').classList.add('bad');
@@ -3127,6 +3233,13 @@ summary{min-height:24px;padding:3px 0;}
3127
3233
  var rows = [
3128
3234
  ['Project', esc(state.name)],
3129
3235
  ['Type', typeLabel(state.projType) || 'Not declared'],
3236
+ /* the About step is optional, so its row states the CONSEQUENCE of the
3237
+ skip rather than a bare em-dash — the review is the last screen before
3238
+ Create, and "off the map" should never be a surprise (task 1002334) */
3239
+ ['About', String(state.description || '').trim()
3240
+ ? esc(String(state.description).trim())
3241
+ : '<em style="font-weight:600;color:var(--warn)">not said — off the public map until you add it</em>'],
3242
+ ['Who’s building it', teamLabel(state.teamShape) || 'Not said'],
3130
3243
  ['Starting from', state.mode === 'adopt' ? 'A repo you already have' : 'A fresh, empty repo'],
3131
3244
  ['Repository', '<code>' + esc(state.owner + '/' + state.repo) + '</code>'],
3132
3245
  ['Handle', '<code>' + esc(state.slug) + '</code>'],
@@ -3213,6 +3326,11 @@ summary{min-height:24px;padding:3px 0;}
3213
3326
  body: JSON.stringify({
3214
3327
  slug: state.slug,
3215
3328
  type: state.projType || undefined,
3329
+ /* the optional About answers (task 1002334) — omitted when skipped,
3330
+ because absent means SKIPPED and an empty string would read as an
3331
+ answered-but-blank description to the publish gate */
3332
+ description: String(state.description || '').trim() || undefined,
3333
+ detail: state.teamShape ? { team_shape: state.teamShape } : undefined,
3216
3334
  hosting_shape: 'standalone',
3217
3335
  /* ONLY a bring-your-own domain travels (task 1002898 N2). The server owns
3218
3336
  the assigned <slug>.<apex> default (task 1002704) and applies it to an
@@ -3257,7 +3375,7 @@ summary{min-height:24px;padding:3px 0;}
3257
3375
  sessionStorage.removeItem(DRAFT_KEY);
3258
3376
  } catch (e) {}
3259
3377
  renderDone();
3260
- show(6, true);
3378
+ show(7, true);
3261
3379
  startProgress(data.instance && data.instance.id);
3262
3380
  }).catch(function (err) {
3263
3381
  if (String(err && err.message) === 'signing in') return;
@@ -3265,7 +3383,7 @@ summary{min-height:24px;padding:3px 0;}
3265
3383
  if (err && err.gotoHome) {
3266
3384
  /* A repo-creation problem (name taken / reconnect) — bounce back to the Home
3267
3385
  step with the reason, so the user fixes it where the repo is chosen. */
3268
- show(3, true);
3386
+ show(4, true);
3269
3387
  $('h-home').textContent = err.homeMsg || 'Please revisit the repository step.';
3270
3388
  $('h-home').className = 'hint err';
3271
3389
  return;
@@ -3799,7 +3917,7 @@ summary{min-height:24px;padding:3px 0;}
3799
3917
  5s tick never clobbers an open dropdown or mid-flight button) */
3800
3918
  if (inst && inst.status !== live.instStatus) {
3801
3919
  live.instStatus = inst.status; renderSteps();
3802
- if (state.step === 6) paintRail(6); /* the rail finishes only when the project answers */
3920
+ if (state.step === 7) paintRail(7); /* the rail finishes only when the project answers */
3803
3921
  }
3804
3922
  var terminal = renderProgress(inst);
3805
3923
  if (!terminal) pollTimer = setTimeout(tick, 5000);
@@ -3836,13 +3954,14 @@ summary{min-height:24px;padding:3px 0;}
3836
3954
  lastStepsHtml = '';
3837
3955
  lastProgHtml = '';
3838
3956
  try { sessionStorage.removeItem(CREATED_KEY); sessionStorage.removeItem(DRAFT_KEY); } catch (e) {}
3839
- state = { step: 1, name: '', projType: null, mode: 'greenfield', owner: '', ownerTouched: false, repo: '', repoTouched: false, slug: '', slugTouched: false, domain: '', noAddress: false };
3957
+ state = { step: 1, name: '', projType: null, description: '', teamShape: null, mode: 'greenfield', owner: '', ownerTouched: false, repo: '', repoTouched: false, slug: '', slugTouched: false, domain: '', noAddress: false };
3840
3958
  $('newName').value = ''; $('f-owner').value = ''; $('f-repo').value = ''; $('f-slug').value = ''; $('f-domain').value = '';
3841
- $('f-noaddr').checked = false;
3959
+ $('f-noaddr').checked = false; $('f-desc').value = '';
3842
3960
  /* Re-seed the owner from the signed-in account for the next project. */
3843
3961
  if (me && me.github_login) { state.owner = me.github_login; $('f-owner').value = state.owner; }
3844
3962
  $('h-name').innerHTML = '&nbsp;';
3845
3963
  paintType(null);
3964
+ paintTeam(null);
3846
3965
  setMode('greenfield');
3847
3966
  show(1, true);
3848
3967
  });
@@ -3875,7 +3994,7 @@ summary{min-height:24px;padding:3px 0;}
3875
3994
  manifestState = created.manifest_state; manifestPending = true;
3876
3995
  }
3877
3996
  renderDone();
3878
- show(6, !!focus);
3997
+ show(7, !!focus);
3879
3998
  startProgress(created.id);
3880
3999
  resumePolls();
3881
4000
  return;
@@ -3883,7 +4002,7 @@ summary{min-height:24px;padding:3px 0;}
3883
4002
  /* before identity settles, never render past the name step: deeper steps
3884
4003
  carry authenticated side effects (the Home-step repo probe), and onAuth()
3885
4004
  re-enters to the real draft step the moment /me answers */
3886
- var step = authKnown && state.step >= 1 && state.step <= 5 ? state.step : 1;
4005
+ var step = authKnown && state.step >= 1 && state.step <= 6 ? state.step : 1;
3887
4006
  if (step === 1) {
3888
4007
  show(1, false);
3889
4008
  if (focus) $('newName').focus();
@@ -494,6 +494,15 @@ const KERNEL_FILES = [
494
494
  // and pino, so joining the roster costs nothing and buys the import-direction
495
495
  // guarantee on the file every module now logs through.
496
496
  'src/bongos/logger.js', // pino + request correlation + ADR 0022 redaction
497
+ // task 1003339 (ADR 0209): the abuse ceiling is a trust primitive in the same
498
+ // sense the authz gates are — authz says WHO may ask, this says HOW OFTEN, and
499
+ // an unauthenticated read route has nothing else holding it. src/module-api.js
500
+ // now hands modules the shared account-existence limiter (the onboarding
501
+ // module's account-existence probe spends the same budget as core's), so it
502
+ // sits BELOW the module boundary exactly as logger.js does. It requires NOTHING
503
+ // — no relative imports at all — so joining the roster costs nothing and buys
504
+ // the import-direction guarantee on a file two surfaces now trust.
505
+ 'src/bongos/middleware/rate-limit.js', // the sliding-window abuse ceilings (write + per-surface read)
497
506
  'src/bongos/db-kernel.js', // the kernel db slice — identity/audit/rank/withTx (BV1.R69)
498
507
  'src/bongos/routes/_helpers.js', // shared request validators the doorway re-exports
499
508
  // config / identity (host context a module reads, never hardcodes)
@@ -60,6 +60,23 @@ function buildInitSpec(a = {}) {
60
60
 
61
61
  function healthzUrl(domain) { return `https://${domain}/healthz`; }
62
62
 
63
+ // The optional project-detail step's answers, as the CLI collects them (task
64
+ // 1002334) — the same shape POST /provisioning/instances takes, so the terminal
65
+ // form and the web wizard file identical requests. A blank answer is a SKIP, and a
66
+ // skip is a supported default: the project stands up, it just stays off the map
67
+ // until a description exists (ADR 0182 D4). Off-vocabulary team answers are dropped
68
+ // rather than guessed — the server's normalizeProjectDetail applies the same rule.
69
+ // PURE.
70
+ const TEAM_SHAPES = ['solo', 'small-team', 'community'];
71
+ function buildProjectDetail(a = {}) {
72
+ const out = {};
73
+ const told = typeof a.description === 'string' ? a.description.replace(/\s+/g, ' ').trim() : '';
74
+ if (told) out.description = told;
75
+ const team = String(a.teamShape || '').trim().toLowerCase();
76
+ if (TEAM_SHAPES.includes(team)) out.detail = { team_shape: team };
77
+ return out;
78
+ }
79
+
63
80
  // ── Interactive shell ────────────────────────────────────────────────────────
64
81
 
65
82
  function ask(rl, q, dflt) {
@@ -90,6 +107,29 @@ async function run() {
90
107
  a.slug = await ask(rl, 'Instance slug? (DNS/db-safe, e.g. mercury)');
91
108
  a.domain = await ask(rl, 'Domain you own (bring-your-own; blank = the platform assigns <slug>.<its apex> when configured)?');
92
109
 
110
+ // Step 1b — the OPTIONAL project detail (task 1002334). Every form in the
111
+ // sequence offers it and every owner may skip it: Enter past both questions and
112
+ // the standup is unaffected. Say what skipping costs BEFORE asking, so the skip
113
+ // is an informed choice rather than a shrug — a project with no description
114
+ // stays off the map (ADR 0182 D4) and `bongos` can recommend nothing.
115
+ log('\nTell us about your project — optional. Press Enter to skip either question.');
116
+ log(' A description is what puts your project on the map (you can add it later);');
117
+ log(` who's building it is what the platform's setup recommendations read.`);
118
+ a.description = await ask(rl, 'What are you building, in a sentence or two?');
119
+ a.teamShape = await ask(rl, `Who's building it — ${TEAM_SHAPES.join(' / ')}?`);
120
+ const detail = buildProjectDetail(a);
121
+ if (Object.keys(detail).length) {
122
+ // This CLI prints commands rather than calling the API (the trust boundary),
123
+ // so it shows the answers as the request fields they are — the SAME body the
124
+ // web wizard files, which is what keeps the two forms in lockstep.
125
+ log(' These ride the project\'s provisioning request (POST /provisioning/instances):');
126
+ log(' ' + JSON.stringify(detail));
127
+ }
128
+ if (!detail.description) {
129
+ log(' → no description. Your project stands up either way; it stays off the public map until it');
130
+ log(' has one, and you can add it any time (PATCH /provisioning/instances/:id/detail).');
131
+ }
132
+
93
133
  // Step 2 — human: the GitHub repo (create empty for greenfield; use existing for adopt)
94
134
  if (adopt) {
95
135
  log('\n① Use your EXISTING GitHub repository:');
@@ -224,4 +264,4 @@ async function run() {
224
264
 
225
265
  if (require.main === module) run().catch((e) => { console.error('onboard failed:', e && e.message ? e.message : e); process.exit(1); });
226
266
 
227
- module.exports = { onboardPlan, oauthAppInstructions, buildInitSpec, healthzUrl };
267
+ module.exports = { onboardPlan, oauthAppInstructions, buildInitSpec, healthzUrl, buildProjectDetail, TEAM_SHAPES };
@@ -75,9 +75,13 @@ async function federateInstance(inst, deps) {
75
75
  ownerGithubId = rows[0] ? rows[0].github_id : null;
76
76
  } catch { /* non-fatal */ }
77
77
  await platformIdentity.registerClient(
78
- // type: the owner's declaration rides the instance row from the create
79
- // wizard (task 1002772) and lands on the catalog row here — never guessed.
80
- { clientId, secret: clientSecret, name: inst.slug, redirectUris, origin, ownerGithubId, type: inst.type },
78
+ // type + description: the owner's answers ride the instance row from the
79
+ // create form (tasks 1002772 / 1002334) and land on the catalog row here —
80
+ // never guessed. description is half the publish gate (ADR 0182 D4), so a
81
+ // project that described itself at creation is publishable the moment it
82
+ // federates; one that skipped the optional step passes null and stays dark
83
+ // matter until it fills the answer in.
84
+ { clientId, secret: clientSecret, name: inst.slug, redirectUris, origin, ownerGithubId, type: inst.type, description: inst.description },
81
85
  { pool: db },
82
86
  );
83
87
  log(` [federate] registered client '${clientId}' on the hub (origin ${origin})`);