@bongos/core 1.19.528 → 1.19.530
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 +40 -25
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/adr/0243-a-bundle-adjustment-belongs-to-the-bundle-not-the-advice-about-it.md +159 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +5 -3
- package/docs/copy-inventory.md +219 -219
- package/docs/copy-registry.json +282 -282
- package/docs/module-api-changelog.md +4 -0
- package/modules/provisioning/recommendations.js +18 -57
- package/modules/provisioning/routes/callback-page.js +159 -0
- package/modules/provisioning/routes/provisioning.js +10 -138
- package/modules/provisioning/starter-bundles.js +143 -4
- package/modules/public-landing/public/projects.html +55 -4
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/design_tokens_sync.mjs +1 -1
- package/tests/projects_hub_module_picker.mjs +217 -4
- package/tests/provisioning_callback_page.mjs +74 -0
- package/tests/provisioning_module_selection.mjs +64 -4
- package/tests/provisioning_starter_bundles.mjs +90 -1
|
@@ -1493,5 +1493,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1493
1493
|
landed since 1.19.526 with no explicit bump. run 33827382028. (task 1002620)
|
|
1494
1494
|
1.19.528 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1495
1495
|
landed since 1.19.527 with no explicit bump. run 33829339717. (task 1002620)
|
|
1496
|
+
1.19.529 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1497
|
+
landed since 1.19.528 with no explicit bump. run 33877197857. (task 1002620)
|
|
1498
|
+
1.19.530 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1499
|
+
landed since 1.19.529 with no explicit bump. run 33878817005. (task 1002620)
|
|
1496
1500
|
---------------------------------------------------------------------------
|
|
1497
1501
|
```
|
|
@@ -39,42 +39,20 @@
|
|
|
39
39
|
|
|
40
40
|
'use strict';
|
|
41
41
|
|
|
42
|
-
const {
|
|
42
|
+
const { bundleForAnswers, TEAM_SHAPE_RULES, ALWAYS_ON_CORE } = require('./starter-bundles');
|
|
43
43
|
const { PROJECT_DETAIL_VOCAB } = require('./provisioning');
|
|
44
44
|
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
45
|
+
// `TEAM_SHAPE_RULES` MOVED to starter-bundles.js (BV1.R22, task 1002341; ADR 0243)
|
|
46
|
+
// and is re-exported here so this file's public surface is unchanged. It went to
|
|
47
|
+
// the bundle layer because a bundle adjustment is not advice about the bundle —
|
|
48
|
+
// it IS the bundle once the owner has answered, and `effectiveModules` is the
|
|
49
|
+
// function ADR 0240 §2 hangs the whole skip contract on. A rule the creation
|
|
50
|
+
// picker applies but that fallback does not is two lists kept in agreement by
|
|
51
|
+
// hand, which is the failure ADR 0240 §2 was written to make impossible.
|
|
51
52
|
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
const TEAM_SHAPE_RULES = Object.freeze([
|
|
56
|
-
Object.freeze({
|
|
57
|
-
id: 'a-community-needs-somewhere-to-gather',
|
|
58
|
-
teamShape: 'community',
|
|
59
|
-
effect: 'add',
|
|
60
|
-
module: 'discord',
|
|
61
|
-
reason: 'You said an open community is building this, so somewhere for it to gather is switched on.',
|
|
62
|
-
}),
|
|
63
|
-
Object.freeze({
|
|
64
|
-
id: 'a-team-shares-one-environment',
|
|
65
|
-
teamShape: 'small-team',
|
|
66
|
-
effect: 'add',
|
|
67
|
-
module: 'dev-box',
|
|
68
|
-
reason: 'A small team gets one shared environment to build in, so nobody is debugging their own laptop.',
|
|
69
|
-
}),
|
|
70
|
-
Object.freeze({
|
|
71
|
-
id: 'building-alone-needs-no-shared-environment',
|
|
72
|
-
teamShape: 'solo',
|
|
73
|
-
effect: 'remove',
|
|
74
|
-
module: 'dev-box',
|
|
75
|
-
reason: 'You are building this alone, so the shared cloud environment is off — your own machine is the setup.',
|
|
76
|
-
}),
|
|
77
|
-
]);
|
|
53
|
+
// What stays HERE is the advice: a governance preset that is only ever previewed,
|
|
54
|
+
// and an invite SEEK nothing in this module may run. Neither is a thing a project
|
|
55
|
+
// silently ends up with, so neither belongs in the read-time resolution.
|
|
78
56
|
|
|
79
57
|
// The governance presets, by key. A preset is a NAME plus the one sentence that
|
|
80
58
|
// says what living under it feels like — never a set of roles or permissions,
|
|
@@ -167,25 +145,6 @@ function normalizeTeamShape(value) {
|
|
|
167
145
|
return PROJECT_DETAIL_VOCAB.team_shape.includes(value) ? value : null;
|
|
168
146
|
}
|
|
169
147
|
|
|
170
|
-
// Apply the team-shape rules to one type's opt-in bundle. Returns the adjusted
|
|
171
|
-
// list plus the adjustments that ACTUALLY changed it — a rule whose module the
|
|
172
|
-
// bundle already carries (or already lacks) records nothing, because a stated
|
|
173
|
-
// reason for a change that did not happen is a lie the owner cannot check.
|
|
174
|
-
// Never touches the always-on core. PURE.
|
|
175
|
-
function adjustBundle(bundle, teamShape) {
|
|
176
|
-
const modules = [...bundle];
|
|
177
|
-
const adjustments = [];
|
|
178
|
-
for (const rule of TEAM_SHAPE_RULES) {
|
|
179
|
-
if (rule.teamShape !== teamShape) continue;
|
|
180
|
-
const at = modules.indexOf(rule.module);
|
|
181
|
-
if (rule.effect === 'add' && at === -1) modules.push(rule.module);
|
|
182
|
-
else if (rule.effect === 'remove' && at !== -1) modules.splice(at, 1);
|
|
183
|
-
else continue;
|
|
184
|
-
adjustments.push({ rule: rule.id, effect: rule.effect, module: rule.module, reason: rule.reason });
|
|
185
|
-
}
|
|
186
|
-
return { modules, adjustments };
|
|
187
|
-
}
|
|
188
|
-
|
|
189
148
|
// The whole recommendation for one set of creation answers:
|
|
190
149
|
//
|
|
191
150
|
// {
|
|
@@ -211,8 +170,10 @@ function adjustBundle(bundle, teamShape) {
|
|
|
211
170
|
function recommendationsForDetails(answers) {
|
|
212
171
|
const given = answers && typeof answers === 'object' && !Array.isArray(answers) ? answers : {};
|
|
213
172
|
const teamShape = normalizeTeamShape(given.team_shape);
|
|
214
|
-
|
|
215
|
-
|
|
173
|
+
// The SAME resolver the picker preselects from and effectiveModules falls back
|
|
174
|
+
// to (ADR 0243) — so the recommendation, the panel and the project a skipped
|
|
175
|
+
// picker ends up with are one function's answer, not three that agree.
|
|
176
|
+
const preset = bundleForAnswers(given.type, teamShape);
|
|
216
177
|
|
|
217
178
|
const gov = GOVERNANCE_RULES.find((r) => r.teamShape === teamShape) || GOVERNANCE_DEFAULT;
|
|
218
179
|
const invite = INVITE_RULES.find((r) => r.teamShape === teamShape) || INVITE_DEFAULT;
|
|
@@ -226,9 +187,9 @@ function recommendationsForDetails(answers) {
|
|
|
226
187
|
label: preset.label,
|
|
227
188
|
summary: preset.summary,
|
|
228
189
|
core: [...ALWAYS_ON_CORE],
|
|
229
|
-
bundle:
|
|
230
|
-
modules:
|
|
231
|
-
adjustments,
|
|
190
|
+
bundle: preset.bundle,
|
|
191
|
+
modules: preset.modules,
|
|
192
|
+
adjustments: preset.adjustments,
|
|
232
193
|
},
|
|
233
194
|
governance: {
|
|
234
195
|
preset: gov.preset,
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// modules/provisioning/routes/callback-page.js — the standalone HTML the public
|
|
2
|
+
// GitHub-App-Manifest redirect lands on (task 1003570; the page itself is task 2096).
|
|
3
|
+
//
|
|
4
|
+
// WHY. This is pure PRESENTATION for exactly one route — `GET /provisioning/
|
|
5
|
+
// github-app/callback` in routes/provisioning.js — and it is ~135 lines of escaper,
|
|
6
|
+
// palette, stylesheet, client script and template with no request, DB or auth logic
|
|
7
|
+
// in it. Carrying it inside the route file pushed that file onto the 1500-line
|
|
8
|
+
// OVERSIZE_LINES fitness ratchet (scripts/gds/fitness-ratchets.js), where the next
|
|
9
|
+
// single added line failed `unit` and the only obvious escape — raising the baseline
|
|
10
|
+
// — is the one thing the ratchet forbids (ADR 0243 §4/§5 for the argument that put
|
|
11
|
+
// it there). Moved here verbatim so the route file has room to change again.
|
|
12
|
+
//
|
|
13
|
+
// Reaches core ONLY through the doorway src/module-api.js (the one-way rule, ADR
|
|
14
|
+
// 0083); it is required by routes/provisioning.js and by nothing else.
|
|
15
|
+
//
|
|
16
|
+
// Shape: no DB, no network, no request object — given a plain options bag it returns
|
|
17
|
+
// a string. `esc` and `callbackPage` are the whole surface; PAGE_STYLE and
|
|
18
|
+
// CALLBACK_SCRIPT stay private because they are only ever this page's own body.
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const api = require('../../../src/module-api');
|
|
22
|
+
|
|
23
|
+
// Minimal HTML escaper for the callback landing page (no template engine here).
|
|
24
|
+
function esc(s) {
|
|
25
|
+
return String(s == null ? '' : s).replace(/[&<>"']/g, (c) => (
|
|
26
|
+
{ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]
|
|
27
|
+
));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Accent pair resolved from the instance's own branding — the pack is the palette
|
|
31
|
+
// authority (task 1002810); the literal fallbacks are the neutral pack's values, so
|
|
32
|
+
// a branding validation error still paints the current family, never a stale one.
|
|
33
|
+
const ACCENT = (() => {
|
|
34
|
+
try {
|
|
35
|
+
const c = api.branding().theme?.ui?.colors || {};
|
|
36
|
+
return { sun: c.accent || '#fb9569', sun600: c.accentDeep || '#a3481a' };
|
|
37
|
+
} catch { return { sun: '#fb9569', sun600: '#a3481a' }; }
|
|
38
|
+
})();
|
|
39
|
+
|
|
40
|
+
// Brand styling for the standalone onboarding pages — sunrise palette + Baloo 2 /
|
|
41
|
+
// Quicksand (accents from the branding pack, config/branding.neutral.json resolved
|
|
42
|
+
// under the instance's own config/branding.json). Light + dark aware; no external
|
|
43
|
+
// dependency beyond an optional Google-Fonts link (system fallback if it doesn't load).
|
|
44
|
+
const PAGE_STYLE = `
|
|
45
|
+
:root{color-scheme:light dark;--sun:${ACCENT.sun};--sun600:${ACCENT.sun600};--sunInk:#2a1305;--coral:#fb7185;--sky:#38bdf8;--sky600:#0284c7;--ok:#16a34a;--warn:#f59e0b;--ink:#2a1f17;--inkSoft:#6b5d52;--card:#fffdfa;--line:#f0e2cf;--bg1:#fde8c8;--bg2:#fff4e6;--bg3:#fffdfa}
|
|
46
|
+
@media (prefers-color-scheme:dark){:root{--ink:#f5efe6;--inkSoft:#cbbdad;--card:#241b13;--line:#3a2c1e;--bg1:#2a1d10;--bg2:#1b130b;--bg3:#130d07}}
|
|
47
|
+
*{box-sizing:border-box}html,body{height:100%}
|
|
48
|
+
body{margin:0;font-family:"Quicksand",system-ui,-apple-system,"Segoe UI",sans-serif;color:var(--ink);background:linear-gradient(180deg,var(--bg1) 0%,var(--bg2) 45%,var(--bg3) 100%);background-attachment:fixed;display:flex;align-items:center;justify-content:center;padding:1.5rem;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}
|
|
49
|
+
.card{width:100%;max-width:30rem;background:var(--card);border:1px solid var(--line);border-radius:20px;padding:2.4rem 2rem 2rem;text-align:center;box-shadow:0 18px 50px -22px rgba(120,60,10,.4),0 2px 8px rgba(120,60,10,.07)}
|
|
50
|
+
.badge-warn{font-size:2.4rem;line-height:1;margin:0 auto 1rem}
|
|
51
|
+
h1{font-family:"Baloo 2",system-ui,sans-serif;font-weight:700;font-size:1.5rem;line-height:1.15;margin:.2rem 0 .55rem}
|
|
52
|
+
.sub{color:var(--inkSoft);font-size:1.02rem;line-height:1.5;margin:0 auto;max-width:24rem}
|
|
53
|
+
.sub p{margin:.6rem 0}
|
|
54
|
+
code{font-family:"Fira Code",ui-monospace,SFMono-Regular,monospace;background:rgba(56,189,248,.14);color:var(--sky600);padding:.12em .45em;border-radius:6px;font-size:.9em;overflow-wrap:anywhere}
|
|
55
|
+
@media (prefers-color-scheme:dark){code{color:var(--sky)}}
|
|
56
|
+
.steps{display:flex;align-items:flex-start;justify-content:center;margin:1.9rem 0 1.1rem}
|
|
57
|
+
.step{flex:1 1 0;max-width:9rem;position:relative}
|
|
58
|
+
.step::before{content:"";position:absolute;top:16px;left:-50%;width:100%;height:3px;background:var(--line);z-index:0;transition:background .4s ease}
|
|
59
|
+
.step:first-child::before{display:none}
|
|
60
|
+
.dot{width:34px;height:34px;border-radius:50%;margin:0 auto;display:grid;place-items:center;background:var(--bg3);border:2px solid var(--line);color:var(--inkSoft);font-weight:700;font-size:.95rem;position:relative;z-index:1;transition:all .4s ease}
|
|
61
|
+
.lbl{margin-top:.55rem;font-size:.78rem;color:var(--inkSoft);font-weight:600;line-height:1.25;padding:0 .2rem}
|
|
62
|
+
.step.done .dot{background:var(--ok);border-color:var(--ok);color:#fff}
|
|
63
|
+
.step.done::before{background:var(--ok)}
|
|
64
|
+
.step.active .dot{border-color:var(--sun);color:var(--sun600);animation:pulse 1.5s ease-in-out infinite}
|
|
65
|
+
.step.active .lbl{color:var(--ink)}
|
|
66
|
+
.step.active .dot::after{content:"";position:absolute;inset:-3px;border-radius:50%;border:2px solid transparent;border-top-color:var(--sun);animation:spin .8s linear infinite}
|
|
67
|
+
@keyframes spin{to{transform:rotate(360deg)}}
|
|
68
|
+
@keyframes pulse{0%,100%{box-shadow:0 0 0 4px rgba(249,115,22,.15)}50%{box-shadow:0 0 0 9px rgba(249,115,22,.04)}}
|
|
69
|
+
@media (prefers-reduced-motion:reduce){.step.active .dot,.step.active .dot::after{animation:none}}
|
|
70
|
+
.status-line{min-height:1.4em;color:var(--inkSoft);font-size:.94rem;margin:.4rem auto 0;max-width:24rem;line-height:1.45}
|
|
71
|
+
.cta{margin-top:1.4rem}
|
|
72
|
+
.btn{display:inline-block;background:var(--sun);color:var(--sunInk);text-decoration:none;font-weight:700;font-family:"Baloo 2",system-ui,sans-serif;padding:.72rem 1.5rem;border-radius:999px;font-size:1.02rem;box-shadow:0 10px 22px -8px rgba(249,115,22,.7);transition:transform .15s ease,box-shadow .15s ease}
|
|
73
|
+
.btn:hover{transform:translateY(-1px);box-shadow:0 14px 28px -8px rgba(249,115,22,.8)}
|
|
74
|
+
.muted{color:var(--inkSoft);opacity:.9;font-size:.85rem;margin:1.1rem auto 0;max-width:24rem;line-height:1.45}
|
|
75
|
+
.hidden{display:none!important}
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
// Client script for the PROGRESS variant only (task 2096): poll the public status
|
|
79
|
+
// endpoint, advance the stepper, and auto-open the instance the moment its credentials
|
|
80
|
+
// land. Deliberately contains NO backticks and NO ${...} so it survives the outer
|
|
81
|
+
// template literal verbatim; the only server data it reads is the JSON config block
|
|
82
|
+
// written just above it. Fails soft — a poll hiccup just retries; a long wait reveals a
|
|
83
|
+
// manual "open" button rather than ever falsely erroring. The auto-open countdown runs
|
|
84
|
+
// ONLY when this page is the wizard's pop-up; a same-tab return (pop-up blocked) keeps
|
|
85
|
+
// the founder here with the door and a way back to the checklist (task 1003236).
|
|
86
|
+
const CALLBACK_SCRIPT = `
|
|
87
|
+
(function(){
|
|
88
|
+
var cfg=window.__CB_SETUP__||{};
|
|
89
|
+
var state=cfg.state||'',domain=cfg.domain||'';
|
|
90
|
+
if(!state||!domain)return;
|
|
91
|
+
var target='https://'+domain;
|
|
92
|
+
var pollUrl=location.pathname.replace('/callback','/status')+'?state='+encodeURIComponent(state);
|
|
93
|
+
var steps=document.getElementById('steps'),statusEl=document.getElementById('status');
|
|
94
|
+
var cta=document.getElementById('cta'),go=document.getElementById('go'),foot=document.getElementById('foot'),back=document.getElementById('back');
|
|
95
|
+
/* Opened from the wizard as a pop-up (window.opener alive, or the tab still carries the name the wizard gave it):
|
|
96
|
+
the checklist is in the other tab, so this one may auto-open the instance. Same-tab fallback (pop-up blocked,
|
|
97
|
+
form target=_self): this IS the founder's checklist tab — never navigate it away; offer both doors (task 1003236). */
|
|
98
|
+
var popup=!!window.opener||window.name==='ghAppSetup';
|
|
99
|
+
var finished=false,tries=0,manual=false;
|
|
100
|
+
function dot(n){return steps.querySelector('[data-step="'+n+'"]');}
|
|
101
|
+
function setActive(n){for(var i=1;i<=3;i++){var el=dot(i);if(!el)continue;el.classList.remove('active','done');if(i<n){el.classList.add('done');el.querySelector('.dot').textContent='\\u2713';}else if(i===n){el.classList.add('active');}}}
|
|
102
|
+
function allDone(){for(var i=1;i<=3;i++){var el=dot(i);if(!el)continue;el.classList.remove('active');el.classList.add('done');el.querySelector('.dot').textContent='\\u2713';}}
|
|
103
|
+
function ready(){if(finished)return;finished=true;allDone();statusEl.textContent='Your instance is ready \\u2014 sign in with GitHub to claim it as owner.';cta.classList.remove('hidden');
|
|
104
|
+
if(!popup){go.textContent='Open '+domain+' \\u2192';if(back)back.classList.remove('hidden');if(foot)foot.textContent='Your setup checklist is waiting where you left it \\u2014 finish it first, or open your instance now.';return;}
|
|
105
|
+
var n=4;(function tick(){if(n<=0){location.href=target;return;}go.textContent='Opening '+domain+' in '+n+'\\u2026';n--;setTimeout(tick,1000);})();}
|
|
106
|
+
function failed(){if(finished)return;finished=true;statusEl.textContent='Setup didn\\u2019t finish. Open your instance page and start the GitHub sign-in setup again.';cta.classList.remove('hidden');go.textContent='Open '+domain+' \\u2192';}
|
|
107
|
+
function revealManual(){if(manual||finished)return;manual=true;cta.classList.remove('hidden');go.textContent='Open '+domain+' \\u2192';if(foot)foot.textContent='This is taking a little longer than usual \\u2014 you can open your instance now, or keep waiting.';}
|
|
108
|
+
function schedule(){if(!finished)setTimeout(poll,3000);}
|
|
109
|
+
function poll(){tries++;fetch(pollUrl,{headers:{accept:'application/json'},cache:'no-store'}).then(function(r){return r.ok?r.json():null;}).then(function(j){if(!j){schedule();return;}if(j.ready||j.status==='done'){ready();return;}if(j.status==='error'||j.status==='expired'){failed();return;}setActive(2);if(tries===40)revealManual();schedule();}).catch(function(){schedule();});}
|
|
110
|
+
setActive(2);poll();
|
|
111
|
+
})();
|
|
112
|
+
`;
|
|
113
|
+
|
|
114
|
+
// The standalone landing page for the public GitHub redirect (the mothership serves it;
|
|
115
|
+
// it is NOT the hall shell). Two variants share the Cloud Bongos brand shell (task 2096):
|
|
116
|
+
// • progress — ok + state + domain: an animated stepper that polls the status endpoint
|
|
117
|
+
// and auto-opens the instance when its credentials are placed.
|
|
118
|
+
// • message — everything else (errors + the no-domain success fallback): a calm badge
|
|
119
|
+
// + `body` (server-authored, trusted HTML).
|
|
120
|
+
function callbackPage({ ok, title, body, state, domain }) {
|
|
121
|
+
const progress = !!(ok && state && domain);
|
|
122
|
+
const cfgJson = JSON.stringify({ state: state || '', domain: domain || '' }).replace(/</g, '\\u003c');
|
|
123
|
+
const head = `<!doctype html><html lang="en"><head><meta charset="utf-8">
|
|
124
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
125
|
+
<meta name="color-scheme" content="light dark">
|
|
126
|
+
<title>${esc(title)}</title>
|
|
127
|
+
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
128
|
+
<link href="https://fonts.googleapis.com/css2?family=Baloo+2:wght@600;700;800&family=Quicksand:wght@400;500;600;700&family=Fira+Code&display=swap" rel="stylesheet">
|
|
129
|
+
<style>${PAGE_STYLE}</style></head><body>`;
|
|
130
|
+
if (progress) {
|
|
131
|
+
return `${head}
|
|
132
|
+
<main class="card">
|
|
133
|
+
<h1>${esc(title)}</h1>
|
|
134
|
+
<p class="sub">Finishing sign-in setup for <code>${esc(domain)}</code>.</p>
|
|
135
|
+
<div class="steps" id="steps" aria-label="Setup progress">
|
|
136
|
+
<div class="step done" data-step="1"><div class="dot">✓</div><div class="lbl">GitHub app created</div></div>
|
|
137
|
+
<div class="step active" data-step="2"><div class="dot">2</div><div class="lbl">Placing credentials</div></div>
|
|
138
|
+
<div class="step" data-step="3"><div class="dot">3</div><div class="lbl">Ready to sign in</div></div>
|
|
139
|
+
</div>
|
|
140
|
+
<p class="status-line" id="status">Hang tight — keep this tab open while we finish.</p>
|
|
141
|
+
<div class="cta hidden" id="cta"><a class="btn" id="go" href="https://${esc(domain)}">Open ${esc(domain)} →</a></div>
|
|
142
|
+
<p class="muted hidden" id="back"><a href="/projects?view=new">Finish your remaining setup steps →</a></p>
|
|
143
|
+
<p class="muted" id="foot">This page opens <code>${esc(domain)}</code> for you the moment it’s ready.</p>
|
|
144
|
+
<noscript><p class="muted">When setup finishes, open <a href="https://${esc(domain)}">${esc(domain)}</a> and sign in with GitHub — that first sign-in makes you the owner.</p></noscript>
|
|
145
|
+
</main>
|
|
146
|
+
<script>window.__CB_SETUP__=${cfgJson};</script>
|
|
147
|
+
<script>${CALLBACK_SCRIPT}</script>
|
|
148
|
+
</body></html>`;
|
|
149
|
+
}
|
|
150
|
+
return `${head}
|
|
151
|
+
<main class="card">
|
|
152
|
+
<div class="badge-warn">${ok ? '✓' : '⚠️'}</div>
|
|
153
|
+
<h1>${esc(title)}</h1>
|
|
154
|
+
<div class="sub">${body || ''}</div>
|
|
155
|
+
</main>
|
|
156
|
+
</body></html>`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = { esc, callbackPage };
|
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
//
|
|
28
28
|
// Reaches core ONLY through the doorway src/module-api.js (the one-way rule, ADR
|
|
29
29
|
// 0083); it requires this module's own provisioning.js (same-dir), never a sibling.
|
|
30
|
+
// The GitHub-App callback page's presentation (esc / ACCENT / the stylesheet / the
|
|
31
|
+
// client script / callbackPage) lives in ./callback-page.js — moved out under task
|
|
32
|
+
// 1003570 because this file had reached the 1500-line fitness ratchet.
|
|
30
33
|
'use strict';
|
|
31
34
|
|
|
32
35
|
const crypto = require('node:crypto');
|
|
@@ -36,9 +39,10 @@ const auth = api;
|
|
|
36
39
|
const { pool, validateOrRespond, parseId } = api;
|
|
37
40
|
const provisioning = require('../provisioning');
|
|
38
41
|
const { onboardPlan, stepsForShape } = require('../onboard-plan');
|
|
39
|
-
const { ALWAYS_ON_CORE, OPTIONAL_MODULES,
|
|
42
|
+
const { ALWAYS_ON_CORE, OPTIONAL_MODULES, starterBundleAnswerTable, optionalModules, effectiveModules } = require('../starter-bundles');
|
|
40
43
|
const { recommendationsForDetails } = require('../recommendations');
|
|
41
44
|
const { makeAskRateLimiter, clientIp } = require('../rate-limit');
|
|
45
|
+
const { callbackPage } = require('./callback-page');
|
|
42
46
|
// task 1003208: structured logging (pino via the doorway) — was console.*.
|
|
43
47
|
const log = api.logger('provisioning');
|
|
44
48
|
|
|
@@ -139,142 +143,6 @@ function badModuleSelection(modules) {
|
|
|
139
143
|
return null;
|
|
140
144
|
}
|
|
141
145
|
|
|
142
|
-
// Minimal HTML escaper for the callback landing page (no template engine here).
|
|
143
|
-
function esc(s) {
|
|
144
|
-
return String(s == null ? '' : s).replace(/[&<>"']/g, (c) => (
|
|
145
|
-
{ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]
|
|
146
|
-
));
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Accent pair resolved from the instance's own branding — the pack is the palette
|
|
150
|
-
// authority (task 1002810); the literal fallbacks are the neutral pack's values, so
|
|
151
|
-
// a branding validation error still paints the current family, never a stale one.
|
|
152
|
-
const ACCENT = (() => {
|
|
153
|
-
try {
|
|
154
|
-
const c = api.branding().theme?.ui?.colors || {};
|
|
155
|
-
return { sun: c.accent || '#fb9569', sun600: c.accentDeep || '#a3481a' };
|
|
156
|
-
} catch { return { sun: '#fb9569', sun600: '#a3481a' }; }
|
|
157
|
-
})();
|
|
158
|
-
|
|
159
|
-
// Brand styling for the standalone onboarding pages — sunrise palette + Baloo 2 /
|
|
160
|
-
// Quicksand (accents from the branding pack, config/branding.neutral.json resolved
|
|
161
|
-
// under the instance's own config/branding.json). Light + dark aware; no external
|
|
162
|
-
// dependency beyond an optional Google-Fonts link (system fallback if it doesn't load).
|
|
163
|
-
const PAGE_STYLE = `
|
|
164
|
-
:root{color-scheme:light dark;--sun:${ACCENT.sun};--sun600:${ACCENT.sun600};--sunInk:#2a1305;--coral:#fb7185;--sky:#38bdf8;--sky600:#0284c7;--ok:#16a34a;--warn:#f59e0b;--ink:#2a1f17;--inkSoft:#6b5d52;--card:#fffdfa;--line:#f0e2cf;--bg1:#fde8c8;--bg2:#fff4e6;--bg3:#fffdfa}
|
|
165
|
-
@media (prefers-color-scheme:dark){:root{--ink:#f5efe6;--inkSoft:#cbbdad;--card:#241b13;--line:#3a2c1e;--bg1:#2a1d10;--bg2:#1b130b;--bg3:#130d07}}
|
|
166
|
-
*{box-sizing:border-box}html,body{height:100%}
|
|
167
|
-
body{margin:0;font-family:"Quicksand",system-ui,-apple-system,"Segoe UI",sans-serif;color:var(--ink);background:linear-gradient(180deg,var(--bg1) 0%,var(--bg2) 45%,var(--bg3) 100%);background-attachment:fixed;display:flex;align-items:center;justify-content:center;padding:1.5rem;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}
|
|
168
|
-
.card{width:100%;max-width:30rem;background:var(--card);border:1px solid var(--line);border-radius:20px;padding:2.4rem 2rem 2rem;text-align:center;box-shadow:0 18px 50px -22px rgba(120,60,10,.4),0 2px 8px rgba(120,60,10,.07)}
|
|
169
|
-
.badge-warn{font-size:2.4rem;line-height:1;margin:0 auto 1rem}
|
|
170
|
-
h1{font-family:"Baloo 2",system-ui,sans-serif;font-weight:700;font-size:1.5rem;line-height:1.15;margin:.2rem 0 .55rem}
|
|
171
|
-
.sub{color:var(--inkSoft);font-size:1.02rem;line-height:1.5;margin:0 auto;max-width:24rem}
|
|
172
|
-
.sub p{margin:.6rem 0}
|
|
173
|
-
code{font-family:"Fira Code",ui-monospace,SFMono-Regular,monospace;background:rgba(56,189,248,.14);color:var(--sky600);padding:.12em .45em;border-radius:6px;font-size:.9em;overflow-wrap:anywhere}
|
|
174
|
-
@media (prefers-color-scheme:dark){code{color:var(--sky)}}
|
|
175
|
-
.steps{display:flex;align-items:flex-start;justify-content:center;margin:1.9rem 0 1.1rem}
|
|
176
|
-
.step{flex:1 1 0;max-width:9rem;position:relative}
|
|
177
|
-
.step::before{content:"";position:absolute;top:16px;left:-50%;width:100%;height:3px;background:var(--line);z-index:0;transition:background .4s ease}
|
|
178
|
-
.step:first-child::before{display:none}
|
|
179
|
-
.dot{width:34px;height:34px;border-radius:50%;margin:0 auto;display:grid;place-items:center;background:var(--bg3);border:2px solid var(--line);color:var(--inkSoft);font-weight:700;font-size:.95rem;position:relative;z-index:1;transition:all .4s ease}
|
|
180
|
-
.lbl{margin-top:.55rem;font-size:.78rem;color:var(--inkSoft);font-weight:600;line-height:1.25;padding:0 .2rem}
|
|
181
|
-
.step.done .dot{background:var(--ok);border-color:var(--ok);color:#fff}
|
|
182
|
-
.step.done::before{background:var(--ok)}
|
|
183
|
-
.step.active .dot{border-color:var(--sun);color:var(--sun600);animation:pulse 1.5s ease-in-out infinite}
|
|
184
|
-
.step.active .lbl{color:var(--ink)}
|
|
185
|
-
.step.active .dot::after{content:"";position:absolute;inset:-3px;border-radius:50%;border:2px solid transparent;border-top-color:var(--sun);animation:spin .8s linear infinite}
|
|
186
|
-
@keyframes spin{to{transform:rotate(360deg)}}
|
|
187
|
-
@keyframes pulse{0%,100%{box-shadow:0 0 0 4px rgba(249,115,22,.15)}50%{box-shadow:0 0 0 9px rgba(249,115,22,.04)}}
|
|
188
|
-
@media (prefers-reduced-motion:reduce){.step.active .dot,.step.active .dot::after{animation:none}}
|
|
189
|
-
.status-line{min-height:1.4em;color:var(--inkSoft);font-size:.94rem;margin:.4rem auto 0;max-width:24rem;line-height:1.45}
|
|
190
|
-
.cta{margin-top:1.4rem}
|
|
191
|
-
.btn{display:inline-block;background:var(--sun);color:var(--sunInk);text-decoration:none;font-weight:700;font-family:"Baloo 2",system-ui,sans-serif;padding:.72rem 1.5rem;border-radius:999px;font-size:1.02rem;box-shadow:0 10px 22px -8px rgba(249,115,22,.7);transition:transform .15s ease,box-shadow .15s ease}
|
|
192
|
-
.btn:hover{transform:translateY(-1px);box-shadow:0 14px 28px -8px rgba(249,115,22,.8)}
|
|
193
|
-
.muted{color:var(--inkSoft);opacity:.9;font-size:.85rem;margin:1.1rem auto 0;max-width:24rem;line-height:1.45}
|
|
194
|
-
.hidden{display:none!important}
|
|
195
|
-
`;
|
|
196
|
-
|
|
197
|
-
// Client script for the PROGRESS variant only (task 2096): poll the public status
|
|
198
|
-
// endpoint, advance the stepper, and auto-open the instance the moment its credentials
|
|
199
|
-
// land. Deliberately contains NO backticks and NO ${...} so it survives the outer
|
|
200
|
-
// template literal verbatim; the only server data it reads is the JSON config block
|
|
201
|
-
// written just above it. Fails soft — a poll hiccup just retries; a long wait reveals a
|
|
202
|
-
// manual "open" button rather than ever falsely erroring. The auto-open countdown runs
|
|
203
|
-
// ONLY when this page is the wizard's pop-up; a same-tab return (pop-up blocked) keeps
|
|
204
|
-
// the founder here with the door and a way back to the checklist (task 1003236).
|
|
205
|
-
const CALLBACK_SCRIPT = `
|
|
206
|
-
(function(){
|
|
207
|
-
var cfg=window.__CB_SETUP__||{};
|
|
208
|
-
var state=cfg.state||'',domain=cfg.domain||'';
|
|
209
|
-
if(!state||!domain)return;
|
|
210
|
-
var target='https://'+domain;
|
|
211
|
-
var pollUrl=location.pathname.replace('/callback','/status')+'?state='+encodeURIComponent(state);
|
|
212
|
-
var steps=document.getElementById('steps'),statusEl=document.getElementById('status');
|
|
213
|
-
var cta=document.getElementById('cta'),go=document.getElementById('go'),foot=document.getElementById('foot'),back=document.getElementById('back');
|
|
214
|
-
/* Opened from the wizard as a pop-up (window.opener alive, or the tab still carries the name the wizard gave it):
|
|
215
|
-
the checklist is in the other tab, so this one may auto-open the instance. Same-tab fallback (pop-up blocked,
|
|
216
|
-
form target=_self): this IS the founder's checklist tab — never navigate it away; offer both doors (task 1003236). */
|
|
217
|
-
var popup=!!window.opener||window.name==='ghAppSetup';
|
|
218
|
-
var finished=false,tries=0,manual=false;
|
|
219
|
-
function dot(n){return steps.querySelector('[data-step="'+n+'"]');}
|
|
220
|
-
function setActive(n){for(var i=1;i<=3;i++){var el=dot(i);if(!el)continue;el.classList.remove('active','done');if(i<n){el.classList.add('done');el.querySelector('.dot').textContent='\\u2713';}else if(i===n){el.classList.add('active');}}}
|
|
221
|
-
function allDone(){for(var i=1;i<=3;i++){var el=dot(i);if(!el)continue;el.classList.remove('active');el.classList.add('done');el.querySelector('.dot').textContent='\\u2713';}}
|
|
222
|
-
function ready(){if(finished)return;finished=true;allDone();statusEl.textContent='Your instance is ready \\u2014 sign in with GitHub to claim it as owner.';cta.classList.remove('hidden');
|
|
223
|
-
if(!popup){go.textContent='Open '+domain+' \\u2192';if(back)back.classList.remove('hidden');if(foot)foot.textContent='Your setup checklist is waiting where you left it \\u2014 finish it first, or open your instance now.';return;}
|
|
224
|
-
var n=4;(function tick(){if(n<=0){location.href=target;return;}go.textContent='Opening '+domain+' in '+n+'\\u2026';n--;setTimeout(tick,1000);})();}
|
|
225
|
-
function failed(){if(finished)return;finished=true;statusEl.textContent='Setup didn\\u2019t finish. Open your instance page and start the GitHub sign-in setup again.';cta.classList.remove('hidden');go.textContent='Open '+domain+' \\u2192';}
|
|
226
|
-
function revealManual(){if(manual||finished)return;manual=true;cta.classList.remove('hidden');go.textContent='Open '+domain+' \\u2192';if(foot)foot.textContent='This is taking a little longer than usual \\u2014 you can open your instance now, or keep waiting.';}
|
|
227
|
-
function schedule(){if(!finished)setTimeout(poll,3000);}
|
|
228
|
-
function poll(){tries++;fetch(pollUrl,{headers:{accept:'application/json'},cache:'no-store'}).then(function(r){return r.ok?r.json():null;}).then(function(j){if(!j){schedule();return;}if(j.ready||j.status==='done'){ready();return;}if(j.status==='error'||j.status==='expired'){failed();return;}setActive(2);if(tries===40)revealManual();schedule();}).catch(function(){schedule();});}
|
|
229
|
-
setActive(2);poll();
|
|
230
|
-
})();
|
|
231
|
-
`;
|
|
232
|
-
|
|
233
|
-
// The standalone landing page for the public GitHub redirect (the mothership serves it;
|
|
234
|
-
// it is NOT the hall shell). Two variants share the Cloud Bongos brand shell (task 2096):
|
|
235
|
-
// • progress — ok + state + domain: an animated stepper that polls the status endpoint
|
|
236
|
-
// and auto-opens the instance when its credentials are placed.
|
|
237
|
-
// • message — everything else (errors + the no-domain success fallback): a calm badge
|
|
238
|
-
// + `body` (server-authored, trusted HTML).
|
|
239
|
-
function callbackPage({ ok, title, body, state, domain }) {
|
|
240
|
-
const progress = !!(ok && state && domain);
|
|
241
|
-
const cfgJson = JSON.stringify({ state: state || '', domain: domain || '' }).replace(/</g, '\\u003c');
|
|
242
|
-
const head = `<!doctype html><html lang="en"><head><meta charset="utf-8">
|
|
243
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
244
|
-
<meta name="color-scheme" content="light dark">
|
|
245
|
-
<title>${esc(title)}</title>
|
|
246
|
-
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
247
|
-
<link href="https://fonts.googleapis.com/css2?family=Baloo+2:wght@600;700;800&family=Quicksand:wght@400;500;600;700&family=Fira+Code&display=swap" rel="stylesheet">
|
|
248
|
-
<style>${PAGE_STYLE}</style></head><body>`;
|
|
249
|
-
if (progress) {
|
|
250
|
-
return `${head}
|
|
251
|
-
<main class="card">
|
|
252
|
-
<h1>${esc(title)}</h1>
|
|
253
|
-
<p class="sub">Finishing sign-in setup for <code>${esc(domain)}</code>.</p>
|
|
254
|
-
<div class="steps" id="steps" aria-label="Setup progress">
|
|
255
|
-
<div class="step done" data-step="1"><div class="dot">✓</div><div class="lbl">GitHub app created</div></div>
|
|
256
|
-
<div class="step active" data-step="2"><div class="dot">2</div><div class="lbl">Placing credentials</div></div>
|
|
257
|
-
<div class="step" data-step="3"><div class="dot">3</div><div class="lbl">Ready to sign in</div></div>
|
|
258
|
-
</div>
|
|
259
|
-
<p class="status-line" id="status">Hang tight — keep this tab open while we finish.</p>
|
|
260
|
-
<div class="cta hidden" id="cta"><a class="btn" id="go" href="https://${esc(domain)}">Open ${esc(domain)} →</a></div>
|
|
261
|
-
<p class="muted hidden" id="back"><a href="/projects?view=new">Finish your remaining setup steps →</a></p>
|
|
262
|
-
<p class="muted" id="foot">This page opens <code>${esc(domain)}</code> for you the moment it’s ready.</p>
|
|
263
|
-
<noscript><p class="muted">When setup finishes, open <a href="https://${esc(domain)}">${esc(domain)}</a> and sign in with GitHub — that first sign-in makes you the owner.</p></noscript>
|
|
264
|
-
</main>
|
|
265
|
-
<script>window.__CB_SETUP__=${cfgJson};</script>
|
|
266
|
-
<script>${CALLBACK_SCRIPT}</script>
|
|
267
|
-
</body></html>`;
|
|
268
|
-
}
|
|
269
|
-
return `${head}
|
|
270
|
-
<main class="card">
|
|
271
|
-
<div class="badge-warn">${ok ? '✓' : '⚠️'}</div>
|
|
272
|
-
<h1>${esc(title)}</h1>
|
|
273
|
-
<div class="sub">${body || ''}</div>
|
|
274
|
-
</main>
|
|
275
|
-
</body></html>`;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
146
|
// Register this module's kernel seam (ADR 0083 / the registerBoxSeams precedent).
|
|
279
147
|
// Idempotent — the loader calls this factory once at boot, but tests may
|
|
280
148
|
// instantiate the router repeatedly; gating on hasProvider keeps the port
|
|
@@ -840,6 +708,8 @@ module.exports = function provisioningRoutes() {
|
|
|
840
708
|
// project is now on the map, and one who cleared it learns it just left.
|
|
841
709
|
// Re-deriving it from a follow-up GET would let the two answers disagree.
|
|
842
710
|
publish: publishVerdict(updated),
|
|
711
|
+
// `team_shape` MOVES a bundle-sourced project's modules — effect with cause (ADR 0243 §5).
|
|
712
|
+
modules: effectiveModules(updated),
|
|
843
713
|
});
|
|
844
714
|
} catch (err) {
|
|
845
715
|
log.error('[provisioning] PATCH /provisioning/instances/:id/detail', err);
|
|
@@ -1337,8 +1207,10 @@ module.exports = function provisioningRoutes() {
|
|
|
1337
1207
|
// row for. The alternatives were both worse: `GET /modules` needs a session
|
|
1338
1208
|
// (this wizard is read signed-out) and `GET /public/modules` serves booleans,
|
|
1339
1209
|
// not copy. Additive — `core` and `bundles` keep their shape.
|
|
1210
|
+
// `byTeamShape` re-resolves each bundle for every answer the About step can give,
|
|
1211
|
+
// riding THIS payload rather than a second read (task 1002341; ADR 0243 §4).
|
|
1340
1212
|
router.get('/provisioning/starter-bundles', (req, res) => {
|
|
1341
|
-
res.json({ core: [...ALWAYS_ON_CORE], bundles:
|
|
1213
|
+
res.json({ core: [...ALWAYS_ON_CORE], bundles: starterBundleAnswerTable(), optional: optionalModules() });
|
|
1342
1214
|
});
|
|
1343
1215
|
|
|
1344
1216
|
// GET /provisioning/recommendations?type=<project type>&team_shape=<interview answer>
|