@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.
Files changed (37) hide show
  1. package/.bongos-core.json +63 -38
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +6 -0
  4. package/clients/bongos-client/index.cjs +6 -0
  5. package/clients/bongos-client/index.d.ts +9 -2
  6. package/clients/bongos-client/index.mjs +6 -0
  7. package/docs/api/openapi.json +112 -9
  8. package/docs/api-reference.md +6 -3
  9. package/docs/copy-inventory.md +354 -354
  10. package/docs/copy-registry.json +420 -420
  11. package/docs/design/project-startup-direction.md +15 -0
  12. package/docs/module-api-changelog.md +4 -0
  13. package/docs/page-readings.json +963 -992
  14. package/modules/provisioning/github-setup.js +129 -0
  15. package/modules/provisioning/module.json +2 -1
  16. package/modules/provisioning/provisioning.js +3 -0
  17. package/modules/provisioning/routes/callback-page.js +83 -8
  18. package/modules/provisioning/routes/github-setup.js +221 -0
  19. package/modules/provisioning/routes/provisioning.js +11 -45
  20. package/modules/public-landing/public/projects.html +207 -66
  21. package/package-lock.json +2 -2
  22. package/package.json +1 -1
  23. package/release-notes.json +12 -0
  24. package/scripts/gds/oauth-secret.js +4 -2
  25. package/scripts/gds/provision-github-setup.js +148 -0
  26. package/scripts/gds/provision.js +19 -5
  27. package/src/module-api.js +1 -1
  28. package/tests/github_handback_any_device.mjs +340 -0
  29. package/tests/github_one_trip.mjs +297 -0
  30. package/tests/projects_hub.mjs +6 -2
  31. package/tests/projects_hub_dns_ready.mjs +2 -1
  32. package/tests/projects_hub_pre_uat.mjs +17 -9
  33. package/tests/provision.mjs +51 -9
  34. package/tests/provisioning_callback_page.mjs +3 -2
  35. package/tests/provisioning_github_app_routes.mjs +103 -12
  36. package/tests/wizard_draft_resume.mjs +4 -0
  37. 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 hand-back 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}`);
97
- t('the page names the instance host', /mercury\.example\.com/.test(html));
98
- // task 1004548: the founder no longer waits here — the page hands them back to the
99
- // checklist, which shows the wait. A manual way back is always on the page.
100
- t('carries a plain "back to my checklist" link (works without script)', /<a class="btn" id="back" href="\/projects\?view=new">Back to my checklist/.test(html));
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 itself (not a copy) against a minimal DOM, once as the
104
- // wizard's pop-up and once as a same-tab return, and watch what it does.
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: '/projects?view=new' };
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__: { state: 'GOODSTATE', domain: 'mercury.example.com', back: '/projects?view=new' },
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 = '/projects?view=new#ghstate=GOODSTATE';
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 to the checklist, carrying the attempt in the fragment', sameTab.close === 0 && sameTab.replaced === want && sameTab.backHref === want, JSON.stringify(sameTab));
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
- t("status 'done' → ready:true + terminal:true", res.status === 200 && j.ready === true && j.terminal === true, JSON.stringify(j));
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
- const RS = SCRIPT.indexOf(' /* oauth-app — the ADR 0133 one-click GitHub-App manifest flow');
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, /GitHub’s part is done/, m);
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
- assert.match(SCRIPT, /if \(!terminal \|\| manifestInFlight\(\)\) pollTimer = setTimeout\(tick, 5000\);/,
165
- 'a GitHub leg in flight keeps the instance read alive once the project is up — after a reload it is the only watcher');
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.