@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/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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).
|
|
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">
|
|
763
|
-
<li data-step="4"><span class="snum">4</span><span class="slbl">
|
|
764
|
-
<li data-step="5"><span class="snum">5</span><span class="slbl">
|
|
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 ·
|
|
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="
|
|
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"> </div>
|
|
906
947
|
<div class="nav">
|
|
907
|
-
<button type="button" class="btn ghost" data-back="
|
|
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
|
|
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="
|
|
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"> </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="
|
|
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
|
|
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="
|
|
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
|
-
/* -
|
|
2492
|
-
so a draft written under
|
|
2493
|
-
reinterpreted — a stale step 4
|
|
2494
|
-
|
|
2495
|
-
|
|
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
|
-
|
|
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 <=
|
|
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
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
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 ===
|
|
2712
|
-
var cur = 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 <
|
|
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 ===
|
|
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 ===
|
|
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 >
|
|
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 ===
|
|
2760
|
-
if (step ===
|
|
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
|
-
|
|
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 = ' '; $('h-home').className = 'hint';
|
|
3062
3168
|
return true;
|
|
3063
3169
|
}
|
|
3064
|
-
if (step ===
|
|
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(
|
|
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(
|
|
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 ===
|
|
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 = ' ';
|
|
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(
|
|
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 <=
|
|
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();
|
package/scripts/gds/fitness.js
CHANGED
|
@@ -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)
|
package/scripts/gds/onboard.js
CHANGED
|
@@ -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
|
|
79
|
-
//
|
|
80
|
-
|
|
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})`);
|