@bongos/core 1.21.93 → 1.21.95
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 +63 -38
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +6 -0
- package/clients/bongos-client/index.cjs +6 -0
- package/clients/bongos-client/index.d.ts +9 -2
- package/clients/bongos-client/index.mjs +6 -0
- package/docs/api/openapi.json +112 -9
- package/docs/api-reference.md +6 -3
- package/docs/copy-inventory.md +354 -354
- package/docs/copy-registry.json +420 -420
- package/docs/design/project-startup-direction.md +15 -0
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +963 -992
- package/modules/provisioning/github-setup.js +129 -0
- package/modules/provisioning/module.json +2 -1
- package/modules/provisioning/provisioning.js +3 -0
- package/modules/provisioning/routes/callback-page.js +83 -8
- package/modules/provisioning/routes/github-setup.js +221 -0
- package/modules/provisioning/routes/provisioning.js +11 -45
- package/modules/public-landing/public/projects.html +207 -66
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/oauth-secret.js +4 -2
- package/scripts/gds/provision-github-setup.js +148 -0
- package/scripts/gds/provision.js +19 -5
- package/src/module-api.js +1 -1
- package/tests/github_handback_any_device.mjs +340 -0
- package/tests/github_one_trip.mjs +297 -0
- package/tests/projects_hub.mjs +6 -2
- package/tests/projects_hub_dns_ready.mjs +2 -1
- package/tests/projects_hub_pre_uat.mjs +17 -9
- package/tests/provision.mjs +51 -9
- package/tests/provisioning_callback_page.mjs +3 -2
- package/tests/provisioning_github_app_routes.mjs +103 -12
- package/tests/wizard_draft_resume.mjs +4 -0
- package/tests/wizard_github_feedback.mjs +9 -5
|
@@ -39,8 +39,18 @@ branding.branding = () => ({ ...realBranding(), identity: { ...(realBranding().i
|
|
|
39
39
|
const api = require(`${ROOT}/src/module-api.js`);
|
|
40
40
|
api.requireBuilder = (req, _res, next) => { req.builder = { id: 7, github_login: 'owner' }; next(); };
|
|
41
41
|
|
|
42
|
+
// The verified-facts read (task 1004649) is the one DB query the github-setup routes make;
|
|
43
|
+
// stub it on the cached module like the data-access fns above.
|
|
44
|
+
const githubSetup = require(`${ROOT}/modules/provisioning/github-setup.js`);
|
|
45
|
+
let facts = {};
|
|
46
|
+
const reported = [];
|
|
47
|
+
githubSetup.githubSetupFacts = async () => facts;
|
|
48
|
+
const prevRecord = provisioning.recordEvent;
|
|
49
|
+
provisioning.recordEvent = async (_db, e) => { reported.push(e); return prevRecord(); };
|
|
50
|
+
|
|
42
51
|
const express = require('express');
|
|
43
52
|
const provisioningRoutes = require(`${ROOT}/modules/provisioning/routes/provisioning.js`);
|
|
53
|
+
const githubSetupRoutes = require(`${ROOT}/modules/provisioning/routes/github-setup.js`);
|
|
44
54
|
const app = express();
|
|
45
55
|
app.use(express.json());
|
|
46
56
|
// res.fail is mounted globally by the core error middleware in production; add a minimal
|
|
@@ -53,6 +63,7 @@ app.use((_req, res, next) => {
|
|
|
53
63
|
next();
|
|
54
64
|
});
|
|
55
65
|
app.use('/api/bongos', provisioningRoutes());
|
|
66
|
+
app.use('/api/bongos', githubSetupRoutes());
|
|
56
67
|
|
|
57
68
|
const server = app.listen(0);
|
|
58
69
|
await new Promise((r) => server.once('listening', r));
|
|
@@ -70,6 +81,7 @@ console.log('POST /provisioning/instances/:id/github-app (mint the one-click lin
|
|
|
70
81
|
t('manifest.redirect_url is the MOTHERSHIP callback', j.manifest && j.manifest.redirect_url === 'https://cloudbongos.com/api/bongos/provisioning/github-app/callback', j.manifest && j.manifest.redirect_url);
|
|
71
82
|
t('manifest.callback_urls are the INSTANCE sign-in (both prefixes)', j.manifest && j.manifest.callback_urls[0] === 'https://mercury.example.com/api/gds/auth/web/callback' && j.manifest.callback_urls[1] === 'https://mercury.example.com/api/bongos/auth/web/callback');
|
|
72
83
|
t('manifest.name is the instance domain (unique + matches the consent-screen redirect) within 34 chars', j.manifest && j.manifest.name === 'mercury.example.com' && j.manifest.name.length <= 34, j.manifest && j.manifest.name);
|
|
84
|
+
t('manifest.setup_url sends an install back to the MOTHERSHIP setup route (task 1004649)', j.manifest && j.manifest.setup_url === 'https://cloudbongos.com/api/bongos/provisioning/github-app/setup' && j.manifest.setup_on_update === true, j.manifest && j.manifest.setup_url);
|
|
73
85
|
t('org_url derived from the target_ref owner (acme)', j.org_url === `https://github.com/organizations/acme/settings/apps/new?state=${j.state}`, j.org_url);
|
|
74
86
|
t('the attempt row was persisted with the minted state', !!(stored && stored.instanceId === 42 && stored.state === j.state && stored.requestedBy === 7));
|
|
75
87
|
}
|
|
@@ -93,24 +105,97 @@ console.log('\nGET /provisioning/github-app/callback (the public GitHub redirect
|
|
|
93
105
|
{
|
|
94
106
|
const res = await fetch(`${base}/provisioning/github-app/callback?code=GHCODE123&state=GOODSTATE`);
|
|
95
107
|
const html = await res.text();
|
|
96
|
-
t('good state → 200 text/html
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
//
|
|
100
|
-
t('carries a plain
|
|
108
|
+
t('good state → 200 text/html one-trip page', res.status === 200 && /text\/html/.test(res.headers.get('content-type') || '') && /GitHub app created/.test(html) && /__CB_SETUP__/.test(html) && html.includes('GOODSTATE'), `status=${res.status}`);
|
|
109
|
+
// task 1004649: GitHub's half of the approval is done — the SAME tab goes on to the install
|
|
110
|
+
t('says the next step is installing it on the repo', /Next, install it on your repo/.test(html));
|
|
111
|
+
// task 1004650: the way back is the return route, which names the project on the server
|
|
112
|
+
t('carries a plain way back to the project (works without script)', html.includes('<a class="btn" id="back" href="/api/bongos/provisioning/github-app/return?state=GOODSTATE">Back to my project'));
|
|
113
|
+
// the script's config is the one the page SERVED, not a copy (task 1004650)
|
|
114
|
+
const servedCfg = JSON.parse((html.match(/window\.__CB_SETUP__=(\{[^<]*?\});/) || [])[1] || '{}');
|
|
115
|
+
t('the served config sends the founder to the return route, never to a bare create page', servedCfg.back === '/api/bongos/provisioning/github-app/return?state=GOODSTATE', JSON.stringify(servedCfg));
|
|
101
116
|
t('no progress stepper or countdown left on the page', !/id="steps"/.test(html) && !/Placing credentials/.test(html) && !/location\.href=target/.test(html));
|
|
102
117
|
|
|
103
|
-
// Run the served script
|
|
104
|
-
|
|
118
|
+
// Run the served INSTALL script (not a copy): it polls the real status shape and goes on.
|
|
119
|
+
const installScript = (html.match(/<script>\s*(\(function\(\)\{[\s\S]*?)<\/script>\s*<\/body>/) || [])[1];
|
|
120
|
+
t('the one-trip page ships its client script', typeof installScript === 'string' && /installations|install_url/.test(installScript));
|
|
121
|
+
async function runInstall(statuses, repoAnswer) {
|
|
122
|
+
const vm = await import('node:vm');
|
|
123
|
+
const back = { href: '' }, note = { textContent: '' };
|
|
124
|
+
const timers = [], fetched = [];
|
|
125
|
+
const calls = { replaced: null };
|
|
126
|
+
const sandbox = {
|
|
127
|
+
__CB_SETUP__: servedCfg,
|
|
128
|
+
document: { getElementById: (id) => (id === 'back' ? back : id === 'note' ? note : null) },
|
|
129
|
+
location: { replace: (u) => { calls.replaced = u; } },
|
|
130
|
+
setTimeout: (fn) => { timers.push(fn); return timers.length; },
|
|
131
|
+
encodeURIComponent, String,
|
|
132
|
+
fetch: async (url) => {
|
|
133
|
+
fetched.push(url);
|
|
134
|
+
if (/api\.github\.com\/repos\//.test(url)) return repoAnswer ? { ok: true, json: async () => repoAnswer } : { ok: false, json: async () => ({}) };
|
|
135
|
+
const body = statuses.length > 1 ? statuses.shift() : statuses[0];
|
|
136
|
+
return { ok: true, json: async () => body };
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
sandbox.window = sandbox;
|
|
140
|
+
vm.runInNewContext(installScript, sandbox);
|
|
141
|
+
for (let i = 0; i < 20 && !calls.replaced; i++) {
|
|
142
|
+
await new Promise((r) => setImmediate(r));
|
|
143
|
+
if (!calls.replaced && timers.length) timers.shift()();
|
|
144
|
+
}
|
|
145
|
+
return { ...calls, fetched, note: note.textContent };
|
|
146
|
+
}
|
|
147
|
+
// the REAL status route's answer for this attempt, before and after the runner named the app
|
|
148
|
+
const statusOf = async () => (await fetch(`${base}/provisioning/github-app/status?state=GOODSTATE`)).json();
|
|
149
|
+
const prevGet = provisioning.getOAuthManifestByState;
|
|
150
|
+
provisioning.getOAuthManifestByState = async () => ({ id: 99, instance_id: 42, state: 'GOODSTATE', status: 'converting', attempts: 1, expires_at: new Date(Date.now() + 600000).toISOString() });
|
|
151
|
+
const waiting = await statusOf();
|
|
152
|
+
provisioning.getOAuthManifestByState = async () => ({ id: 99, instance_id: 42, state: 'GOODSTATE', status: 'converting', attempts: 1, app_id: 5, app_slug: 'mercury-app', expires_at: new Date(Date.now() + 600000).toISOString() });
|
|
153
|
+
const named = await statusOf();
|
|
154
|
+
provisioning.getOAuthManifestByState = prevGet;
|
|
155
|
+
t('the real status: no install link until the app is named, then the one-trip link with the state', waiting.install_url === null && named.install_url === 'https://github.com/apps/mercury-app/installations/new?state=GOODSTATE' && named.repo === 'acme/mercury', JSON.stringify({ waiting: waiting.install_url, named: named.install_url }));
|
|
156
|
+
const narrowed = await runInstall([waiting, named], { id: 777, owner: { id: 42424 } });
|
|
157
|
+
t('waits for the app, then sends the founder to install it on THEIR repo, preselected', narrowed.replaced === 'https://github.com/apps/mercury-app/installations/new/permissions?state=GOODSTATE&suggested_target_id=42424&repository_ids[]=777', JSON.stringify(narrowed));
|
|
158
|
+
const plain = await runInstall([named], null);
|
|
159
|
+
t('a repo GitHub will not describe (private) falls back to the plain install page', plain.replaced === 'https://github.com/apps/mercury-app/installations/new?state=GOODSTATE', JSON.stringify(plain));
|
|
160
|
+
const doneAlready = await runInstall([{ ...named, checklist: { ...named.checklist, install: 'done' } }], null);
|
|
161
|
+
t('already installed → straight back to the checklist', doneAlready.replaced === '/api/bongos/provisioning/github-app/return?state=GOODSTATE', JSON.stringify(doneAlready));
|
|
162
|
+
const stopped = await runInstall([{ ...named, status: 'error' }], null);
|
|
163
|
+
t('a stopped attempt → back to the checklist, which says what stopped', stopped.replaced === '/api/bongos/provisioning/github-app/return?state=GOODSTATE', JSON.stringify(stopped));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
console.log('\nGET /provisioning/github-app/setup (the App\'s setup_url — task 1004649):');
|
|
167
|
+
{
|
|
168
|
+
reported.length = 0;
|
|
169
|
+
const prevGet = provisioning.getOAuthManifestByState;
|
|
170
|
+
provisioning.getOAuthManifestByState = async (_db, s) => (s === 'abcdef0123456789abcdef' ? { id: 99, instance_id: 42, requested_by: 7, state: s, status: 'done', app_slug: 'mercury-app' } : null);
|
|
171
|
+
const res = await fetch(`${base}/provisioning/github-app/setup?installation_id=123456&setup_action=install&state=abcdef0123456789abcdef`);
|
|
172
|
+
const html = await res.text();
|
|
173
|
+
t('a known state → 200 hand-back page to the checklist, carrying the attempt', res.status === 200 && /Installed on GitHub/.test(html) && html.includes('abcdef0123456789abcdef') && /Back to my checklist/.test(html), `status=${res.status}`);
|
|
174
|
+
t('records only that an install was REPORTED (the id is unverified), for the runner to check', reported.length === 1 && reported[0].event === 'github-app-install-reported' && /installation 123456, unverified/.test(reported[0].detail), JSON.stringify(reported));
|
|
175
|
+
reported.length = 0;
|
|
176
|
+
const forged = await fetch(`${base}/provisioning/github-app/setup?installation_id=1;rm%20-rf&state=abcdef0123456789abcdef`);
|
|
177
|
+
t('a non-numeric installation id never reaches the record', forged.status === 200 && reported.length === 1 && !/rm/.test(reported[0].detail), JSON.stringify(reported));
|
|
178
|
+
reported.length = 0;
|
|
179
|
+
const unknown = await fetch(`${base}/provisioning/github-app/setup?installation_id=1&state=NOPE`);
|
|
180
|
+
const uh = await unknown.text();
|
|
181
|
+
t('an unknown or missing state → the same way back, nothing recorded, no oracle', unknown.status === 200 && /Back to my projects/.test(uh) && reported.length === 0, `status=${unknown.status}`);
|
|
182
|
+
provisioning.getOAuthManifestByState = prevGet;
|
|
183
|
+
}
|
|
184
|
+
{
|
|
185
|
+
// the plain hand-back page (now the setup_url's) still behaves as before (task 1004548)
|
|
186
|
+
const callbackPage = require(`${ROOT}/modules/provisioning/routes/callback-page.js`).callbackPage;
|
|
187
|
+
const html = callbackPage({ ok: true, title: 'Installed on GitHub', state: 'GOODSTATE', domain: 'mercury.example.com' });
|
|
188
|
+
t('carries a plain "back to my checklist" link (works without script)', html.includes('<a class="btn" id="back" href="/api/bongos/provisioning/github-app/return?state=GOODSTATE">Back to my checklist'));
|
|
189
|
+
const servedCfg = JSON.parse((html.match(/window\.__CB_SETUP__=(\{[^<]*?\});/) || [])[1] || '{}');
|
|
105
190
|
const script = (html.match(/<script>\s*(\(function\(\)\{[\s\S]*?)<\/script>\s*<\/body>/) || [])[1];
|
|
106
191
|
t('the hand-back page ships the client script', typeof script === 'string' && script.length > 100);
|
|
107
192
|
async function runCallback({ opener }) {
|
|
108
193
|
const vm = await import('node:vm');
|
|
109
|
-
const back = { href: '
|
|
194
|
+
const back = { href: '' };
|
|
110
195
|
const timers = [];
|
|
111
196
|
const calls = { close: 0, focusOpener: 0, replaced: null };
|
|
112
197
|
const sandbox = {
|
|
113
|
-
__CB_SETUP__:
|
|
198
|
+
__CB_SETUP__: servedCfg,
|
|
114
199
|
opener: opener ? { closed: false, focus: () => { calls.focusOpener++; } } : null,
|
|
115
200
|
close: () => { calls.close++; },
|
|
116
201
|
document: { getElementById: (id) => (id === 'back' ? back : null) },
|
|
@@ -124,12 +209,12 @@ console.log('\nGET /provisioning/github-app/callback (the public GitHub redirect
|
|
|
124
209
|
while (timers.length) timers.shift()();
|
|
125
210
|
return { ...calls, replacedBeforeTimer, backHref: back.href };
|
|
126
211
|
}
|
|
127
|
-
const want = '/
|
|
212
|
+
const want = '/api/bongos/provisioning/github-app/return?state=GOODSTATE';
|
|
128
213
|
const popup = await runCallback({ opener: true });
|
|
129
214
|
t('as the wizard’s pop-up it focuses the checklist tab and closes itself', popup.focusOpener === 1 && popup.close === 1 && popup.replacedBeforeTimer === null, JSON.stringify(popup));
|
|
130
215
|
t('…and if the browser refuses the close, the pop-up becomes the checklist', popup.replaced === want, JSON.stringify(popup));
|
|
131
216
|
const sameTab = await runCallback({ opener: false });
|
|
132
|
-
t('as a same-tab return it goes straight back
|
|
217
|
+
t('as a same-tab return it goes straight back through the return route, carrying the attempt', sameTab.close === 0 && sameTab.replaced === want && sameTab.backHref === want, JSON.stringify(sameTab));
|
|
133
218
|
}
|
|
134
219
|
{
|
|
135
220
|
const res = await fetch(`${base}/provisioning/github-app/callback?code=X&state=NOPE`);
|
|
@@ -155,7 +240,13 @@ console.log('\nGET /provisioning/github-app/status (the page poll → auto-open
|
|
|
155
240
|
: null);
|
|
156
241
|
const res = await fetch(`${base}/provisioning/github-app/status?state=GOODSTATE`);
|
|
157
242
|
const j = await res.json();
|
|
158
|
-
|
|
243
|
+
// task 1004649: sign-in verified is not the end of the trip — the install still is ahead
|
|
244
|
+
t("status 'done', install not verified → ready:true, NOT terminal, the install still ahead", res.status === 200 && j.ready === true && j.terminal === false && j.checklist && j.checklist.sign_in === 'done' && j.checklist.install === 'todo', JSON.stringify(j));
|
|
245
|
+
facts = { installedAt: new Date().toISOString() };
|
|
246
|
+
provisioning.getOAuthManifestByState = async () => ({ id: 99, instance_id: 42, state: 'GOODSTATE', status: 'done', app_slug: 'mercury-app', resolved_at: new Date(Date.now() - 3 * 3600 * 1000).toISOString(), expires_at: new Date().toISOString() });
|
|
247
|
+
const j2 = await (await fetch(`${base}/provisioning/github-app/status?state=GOODSTATE`)).json();
|
|
248
|
+
t('installed + sign-in verified, the runner done checking → connected, terminal, no install link', j2.checklist.connected === true && j2.checklist.device_flow === 'later' && j2.terminal === true && j2.install_url === null, JSON.stringify(j2));
|
|
249
|
+
facts = {};
|
|
159
250
|
provisioning.getOAuthManifestByState = prev;
|
|
160
251
|
}
|
|
161
252
|
{
|
|
@@ -247,6 +247,10 @@ function runEnter({ draftStep, focus = false, resumeIntent = false, created = {
|
|
|
247
247
|
const log = { shown: [], created: [], offer: undefined };
|
|
248
248
|
const sandbox = {
|
|
249
249
|
authKnown: true, me: { github_login: 'probe' }, resumeIntent,
|
|
250
|
+
// the server's resume read (task 1004650) already answered for this page load —
|
|
251
|
+
// this slice is about the record the tab stored (tests/github_handback_any_device.mjs
|
|
252
|
+
// holds the server's half)
|
|
253
|
+
resumeAsked: true, handbackProject: null, askServerResume() { throw new Error('not asked twice'); },
|
|
250
254
|
state: { step: draftStep },
|
|
251
255
|
createdRecord: () => created,
|
|
252
256
|
enterCreated: (rec, f) => log.created.push({ id: rec.id, focus: f }),
|
|
@@ -69,7 +69,8 @@ test('publicGithubApp: effective status, failure only on error, started_at, and
|
|
|
69
69
|
|
|
70
70
|
// ── 2. the words for each stage ─────────────────────────────────────────────
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
// from the shared helpers of the four GitHub steps (task 1004649) to the next step
|
|
73
|
+
const RS = SCRIPT.indexOf(' /* The four GitHub steps of part 03');
|
|
73
74
|
const RE = SCRIPT.indexOf(' /* device-flow', RS);
|
|
74
75
|
const RENDER = SCRIPT.slice(RS, RE);
|
|
75
76
|
|
|
@@ -80,7 +81,8 @@ test('the GitHub step region is still findable (a rename must fail loudly)', ()
|
|
|
80
81
|
|
|
81
82
|
function stage(live, { stale = false } = {}) {
|
|
82
83
|
const sandbox = {
|
|
83
|
-
live: { manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null, instStatus: 'active', addrReady: true, ...live },
|
|
84
|
+
live: { manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null, setup: null, installUrl: null, instStatus: 'active', addrReady: true, ...live },
|
|
85
|
+
state: { name: 'Mercury', slug: 'mercury' },
|
|
84
86
|
manifestStale: stale,
|
|
85
87
|
autoOpenArmed: false,
|
|
86
88
|
resolvedDomain: () => 'mercury.example.com',
|
|
@@ -109,7 +111,8 @@ test('pending: waiting for approval, and says where a missing GitHub tab went',
|
|
|
109
111
|
test('converting is the SAME stage as code_received — approved, placing credentials — never "waiting for approval"', () => {
|
|
110
112
|
for (const m of ['code_received', 'converting']) {
|
|
111
113
|
const html = stage({ manifest: m });
|
|
112
|
-
assert.match(html, /
|
|
114
|
+
assert.match(html, /Approved on GitHub/, m);
|
|
115
|
+
assert.match(html, /goes on to installing it on your repo/, `${m}: the trip carries on to the install (task 1004649)`);
|
|
113
116
|
assert.match(html, /<prog now=1 done=1/, `${m}: the strip is past the approval stage`);
|
|
114
117
|
assert.doesNotMatch(html, /Waiting for your approval/, m);
|
|
115
118
|
}
|
|
@@ -161,8 +164,9 @@ const SEED = SCRIPT.slice(SS, SE);
|
|
|
161
164
|
test('the reload regions are still findable (a rename must fail loudly)', () => {
|
|
162
165
|
assert.ok(WS > 0 && WE > WS, 'wait-timer boundaries found');
|
|
163
166
|
assert.ok(SS > 0 && SE > SS, 'instance-read seed boundaries found');
|
|
164
|
-
|
|
165
|
-
|
|
167
|
+
// the pace slows while the tab is hidden, never stops (task 1004650)
|
|
168
|
+
assert.match(SCRIPT, /if \(!terminal \|\| manifestInFlight\(\) \|\| setupOpen\(\)\) pollTimer = setTimeout\(tick, wizPollDelay\(5000\)\);/,
|
|
169
|
+
'a GitHub leg in flight — or an install not yet verified (task 1004649) — keeps the instance read alive once the project is up');
|
|
166
170
|
});
|
|
167
171
|
|
|
168
172
|
// A page that RELOADED: no click in this tab, no manifest poll, just the instance read.
|