@bongos/core 1.21.93 → 1.21.94

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 (33) hide show
  1. package/.bongos-core.json +55 -35
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +2 -0
  4. package/clients/bongos-client/index.cjs +2 -0
  5. package/clients/bongos-client/index.d.ts +4 -2
  6. package/clients/bongos-client/index.mjs +2 -0
  7. package/docs/api/openapi.json +38 -7
  8. package/docs/api-reference.md +4 -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 +7 -0
  12. package/docs/module-api-changelog.md +2 -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 +66 -1
  18. package/modules/provisioning/routes/github-setup.js +113 -0
  19. package/modules/provisioning/routes/provisioning.js +11 -45
  20. package/modules/public-landing/public/projects.html +105 -57
  21. package/package-lock.json +2 -2
  22. package/package.json +1 -1
  23. package/release-notes.json +6 -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_one_trip.mjs +297 -0
  29. package/tests/projects_hub_dns_ready.mjs +2 -1
  30. package/tests/projects_hub_pre_uat.mjs +17 -9
  31. package/tests/provision.mjs +51 -9
  32. package/tests/provisioning_github_app_routes.mjs +94 -8
  33. package/tests/wizard_github_feedback.mjs +8 -5
@@ -601,6 +601,8 @@ body:has(#view-new:not([hidden])) footer.foot{position:relative;z-index:1;backgr
601
601
  display:inline-flex;align-items:center;justify-content:center;font-family:var(--font-body);font-size:13px;letter-spacing:0;}
602
602
  #panel-done .humanSteps>li.sdone::before{content:"✓";text-transform:none;color:var(--ok);background:color-mix(in srgb,var(--ok) 12%,transparent);border:1px solid var(--ok);}
603
603
  #panel-done .humanSteps>li.slocked::before{border:1px solid var(--rule-soft);}
604
+ /* the optional step (command-line sign-in, task 1004649): a dashed ring until it ticks */
605
+ #panel-done .humanSteps>li.soptional:not(.sdone)::before{border-style:dashed;}
604
606
  #panel-done .humanSteps b{font-size:15px;line-height:24px;}
605
607
  .alive{margin-top:18px;padding-top:18px;border-top:1px solid var(--rule);}
606
608
  #view-new .aliveH{margin:0;font-family:var(--font-display);font-size:28px;font-weight:200;letter-spacing:-.02em;line-height:1.2;text-transform:none;color:var(--ink);}
@@ -6603,7 +6605,7 @@ summary{min-height:24px;padding:3px 0;}
6603
6605
  return '<a href="' + (instanceId ? '?view=manage&id=' + encodeURIComponent(instanceId) : '?view=mine') +
6604
6606
  '">' + label + '</a>';
6605
6607
  }
6606
- var live = { instStatus: null, addrReady: false, manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null };
6608
+ var live = { instStatus: null, addrReady: false, manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null, setup: null, installUrl: null };
6607
6609
  var planPromises = {}; /* mode|domain → Promise<steps[]> */
6608
6610
  var lastStepsHtml = '';
6609
6611
  var lastProgHtml = ''; /* same change-only contract, for the progress strip */
@@ -6632,6 +6634,9 @@ summary{min-height:24px;padding:3px 0;}
6632
6634
  (task 1002562) — the owner no longer runs `bongos init --adopt` here. */
6633
6635
  function renderStepGithubRepo() {
6634
6636
  var landed = live.instStatus === 'active';
6637
+ /* the one-page checklist (task 1004649) shows only the GitHub steps the founder
6638
+ takes; a repo the platform made or adopted needs nothing, so it is not a row */
6639
+ if (state.mode === 'adopt' || state.repoAutoCreated || landed) return '';
6635
6640
  var repoLabel = state.owner && state.repo ? '<strong>' + esc(state.owner + '/' + state.repo) + '</strong>' : 'your new repository';
6636
6641
  /* Naming a repo without linking it stranded the owner on exactly the path where the
6637
6642
  platform did the work (task 1002855). Adopt links the repo the owner picked;
@@ -6697,27 +6702,34 @@ summary{min-height:24px;padding:3px 0;}
6697
6702
  else window.location.assign(url);
6698
6703
  }
6699
6704
 
6705
+ /* The four GitHub steps of part 03, in the mockup's words and order (task 1004649,
6706
+ docs/design/mocks/day-night-rooms/): Approve the <name> app on GitHub · Install it
6707
+ on your repo · Builders can sign in · Command-line sign-in is on. One click starts
6708
+ ONE trip — approve, straight on to installing, back here — and every tick is what
6709
+ the server verified (live.setup, the status poll's `checklist`). Every value that
6710
+ reaches the markup goes through esc(). */
6711
+ function ghStep(key) { return live.setup && live.setup[key]; }
6712
+ function appTitle() { return 'Approve the ' + esc(state.name || state.slug || 'project') + ' app on GitHub'; }
6713
+
6700
6714
  /* oauth-app — the ADR 0133 one-click GitHub-App manifest flow is the primary path
6701
6715
  (gap 3 / idea 1000676); locked until a domain exists (the App needs the real
6702
6716
  callback host — the server refuses domainless with domain_required). */
6703
6717
  function renderStepOauthApp() {
6704
6718
  var domain = resolvedDomain();
6705
6719
  if (!domain) {
6706
- return '<li class="slocked" data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>Unlocks once your project has an address — sign-in needs a real domain. Attach one any time from ' + projectPageLink('this project’s own page') + '.</p></li>';
6720
+ return '<li class="slocked" data-onboard-step="oauth-app"><b>' + appTitle() + '</b><p>Unlocks once your project has an address — sign-in needs a real domain. Attach one any time from ' + projectPageLink('this project’s own page') + '.</p></li>';
6721
+ }
6722
+ /* GitHub made the app and the platform placed its keys: the slug is recorded only
6723
+ after the placement (the runner), so a known slug outside a failure is the tick —
6724
+ a fact, whatever the venue says about starting a NEW trip */
6725
+ if (live.manifest !== 'error' && live.manifest !== 'expired' && (ghStep('approve') === 'done' || live.appSlug)) {
6726
+ return '<li class="sdone" data-onboard-step="oauth-app"><b>' + appTitle() + '</b><p>GitHub made the app, and its keys are on your project.</p></li>';
6707
6727
  }
6708
6728
  /* honesty about the venue (owner walk, 2026-08-12): GitHub rejects app
6709
6729
  manifests whose hook URL isn't publicly reachable, so a locally-hosted
6710
6730
  platform can't complete the one-click flow — say it BEFORE the bounce. */
6711
6731
  if (!apexBase()) {
6712
- return '<li class="slocked" data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>This platform is running on a local address, and GitHub refuses app setups it can’t reach over the public internet — so the one-click flow can’t complete here. On a publicly-addressed platform this is one click.</p>' + manualOauthDetails() + '</li>';
6713
- }
6714
- if (live.manifest === 'done') {
6715
- /* the wait ends HERE now, not on the callback page (task 1004548): once the
6716
- project answers too, the step carries the door itself */
6717
- var open = live.instStatus === 'active' && live.addrReady;
6718
- return '<li class="sdone" data-onboard-step="oauth-app"><b>GitHub sign-in is wired</b><p>Credentials placed and verified — sign-in on <code style="font-family:var(--font-mono)">' + esc(domain) + '</code> is live.' +
6719
- (open ? '' : ' The door below opens once your project is answering.') + '</p>' +
6720
- (open ? '<a class="ghbtn" href="https://' + esc(domain) + '" target="_blank" rel="noopener">Open ' + esc(domain) + ' →</a>' : '') + '</li>';
6732
+ return '<li class="slocked" data-onboard-step="oauth-app"><b>' + appTitle() + '</b><p>This platform is running on a local address, and GitHub refuses app setups it can’t reach over the public internet — so the one-click flow can’t complete here. On a publicly-addressed platform this is one click.</p>' + manualOauthDetails() + '</li>';
6721
6733
  }
6722
6734
  if (live.manifest === 'error' || live.manifest === 'expired') {
6723
6735
  /* a plain sentence first; the raw route text folds under Technical
@@ -6731,86 +6743,98 @@ summary{min-height:24px;padding:3px 0;}
6731
6743
  else if (live.manifestCode === 'domain_required') snag = 'This project needs an address first — attach one from ' + projectPageLink('its own page') + ', then come back to this step.';
6732
6744
  else if (live.manifestFailure === 'sign_in_unconfirmed') {
6733
6745
  var appLink = live.appSlug ? '<a href="https://github.com/settings/apps/' + esc(live.appSlug) + '" target="_blank" rel="noopener">your app on GitHub</a>' : 'the app on your GitHub account';
6734
- return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>GitHub approved the app and the platform placed its credentials on your project, but your project didn’t confirm that sign-in works. Trying again would make a second app on your GitHub account and is unlikely to fix this — it needs a look from the platform team. <a href="/contact">Contact us</a> and mention ' + appLink + '.</p>' +
6746
+ return '<li data-onboard-step="oauth-app"><b>' + appTitle() + '</b><p>GitHub approved the app and the platform placed its credentials on your project, but your project didn’t confirm that sign-in works. Trying again would make a second app on your GitHub account and is unlikely to fix this — it needs a look from the platform team. <a href="/contact">Contact us</a> and mention ' + appLink + '.</p>' +
6735
6747
  progressMarkup(1, 1, 'Stopped: your project didn’t confirm sign-in.', 'isErr', 'signin') +
6736
6748
  '<button type="button" class="linklike" data-action="manifest-start">Try again anyway</button></li>';
6737
6749
  }
6738
6750
  else if (live.manifestFailure === 'finish_failed') snag = 'GitHub approved the app, but the platform couldn’t finish setting up sign-in on your project. The app GitHub made is still on your account — delete it first (GitHub → Settings → Developer settings → GitHub Apps) so you don’t end up with two, then try again.';
6739
6751
  else { snag = 'GitHub setup didn’t start. Nothing was changed on GitHub — try again.'; raw = live.manifestMsg; }
6740
- return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>' + snag + '</p>' +
6752
+ return '<li data-onboard-step="oauth-app"><b>' + appTitle() + '</b><p>' + snag + '</p>' +
6741
6753
  (raw ? '<details><summary>Technical details</summary><p>' + esc(raw) + '</p></details>' : '') +
6742
- '<button type="button" class="ghbtn" data-action="manifest-start">' + GH_ICON + 'Set up GitHub sign-in</button>' + manualOauthDetails() + '</li>';
6754
+ '<button type="button" class="ghbtn" data-action="manifest-start">' + GH_ICON + 'Connect GitHub ↗</button>' + manualOauthDetails() + '</li>';
6743
6755
  }
6744
6756
  if (live.manifest === 'code_received' || live.manifest === 'converting') {
6745
- /* GitHub's half is done — say so, rather than keep telling the founder
6746
- to approve an app they just approved */
6757
+ /* GitHub's half of the approval is done — say so, rather than keep telling the
6758
+ founder to approve an app they just approved */
6747
6759
  autoOpenArmed = true;
6748
- return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>GitHub’s part is done. The platform is placing the credentials on your project now — this usually takes a few minutes, and this step ticks itself.</p>' +
6749
- progressMarkup(1, 1, 'Placing credentials and checking sign-in works…', '', 'signin') + autoOpenTick() + '</li>';
6760
+ return '<li data-onboard-step="oauth-app"><b>' + appTitle() + '</b><p>Approved on GitHub. The platform is placing the app’s keys on your project, and the GitHub tab goes on to installing it on your repo. This step ticks itself.</p>' +
6761
+ progressMarkup(1, 1, 'Placing the app’s keys on your project…', '', 'signin') + autoOpenTick() + '</li>';
6750
6762
  }
6751
6763
  if (live.manifest) { /* pending — GitHub holds the manifest, nobody has approved it yet */
6752
6764
  autoOpenArmed = true;
6753
- return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>Waiting on GitHub — approve the app in the tab that opened. No GitHub tab? It may have opened behind this window or in another one. Once approved, the platform places the credentials and verifies sign-in on its own; this step ticks itself.</p>' +
6765
+ return '<li data-onboard-step="oauth-app"><b>' + appTitle() + '</b><p>Waiting on GitHub — approve the app in the tab that opened. No GitHub tab? It may have opened behind this window or in another one. Once approved, GitHub takes you straight on to installing it on your repo, then back here; every step ticks itself.</p>' +
6754
6766
  progressMarkup(0, 0, 'Waiting for your approval on GitHub…', '', 'signin') + autoOpenTick() +
6755
6767
  (manifestStale ? '<button type="button" class="linklike" data-action="manifest-start">Didn’t get to GitHub? Start the setup again</button>' : '') + '</li>';
6756
6768
  }
6757
- return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in — one click</b><p>Approve a pre-filled GitHub App and the platform wires sign-in by itself: no keys to copy, and the secret never leaves the server side.</p>' +
6758
- '<button type="button" class="ghbtn" data-action="manifest-start">' + GH_ICON + 'Set up GitHub sign-in</button>' + manualOauthDetails() + '</li>';
6769
+ return '<li data-onboard-step="oauth-app"><b>' + appTitle() + '</b><p>One trip to GitHub: approve the app, install it on your repo, and you land back here. No keys to copy, and the secret never leaves the server side.</p>' +
6770
+ '<button type="button" class="ghbtn" data-action="manifest-start">' + GH_ICON + 'Connect GitHub ↗</button>' + manualOauthDetails() + '</li>';
6759
6771
  }
6760
6772
 
6761
- /* device-flow — the one-checkbox repair for terminal sign-in (task 1003051):
6762
- GitHub creates a manifest-born App with Device Flow OFF and offers no API to
6763
- enable it, so `bongos setup` in a terminal is refused until the owner ticks
6764
- the box. Web sign-in works either way; the CLI also has a web-token fallback. */
6773
+ /* device-flow — "Command-line sign-in is on", the OPTIONAL fourth step (task 1004649).
6774
+ GitHub creates a manifest-born App with Device Flow OFF and offers no manifest
6775
+ field and no API to switch it on (the spike in task 1004649), so only the founder's
6776
+ own tick in the app's settings can — and the runner ticks this step when GitHub
6777
+ starts a device sign-in for the app. It never holds the project back: web sign-in
6778
+ works without it, and the CLI falls back to a web token meanwhile. */
6765
6779
  function renderStepDeviceFlow() {
6780
+ var title = '<b>Command-line sign-in is on</b>';
6781
+ if (ghStep('device_flow') === 'done') {
6782
+ return '<li class="sdone soptional" data-onboard-step="device-flow">' + title + '<p>Builders can sign in from a terminal too — the <code style="font-family:var(--font-mono)">bongos setup</code> command builders run on their own machine.</p></li>';
6783
+ }
6766
6784
  if (live.manifest !== 'done') {
6767
- return '<li class="slocked" data-onboard-step="device-flow"><b>Let builders sign in from a command line — one checkbox</b><p>Unlocks once GitHub sign-in above is wired. It lets builders sign in from a terminal — the <code style="font-family:var(--font-mono)">bongos setup</code> command builders run on their own machine; web sign-in works without it.</p></li>';
6785
+ return '<li class="slocked soptional" data-onboard-step="device-flow">' + title + '<p>Optional — turn it on later. It lets builders sign in from a terminal, with the <code style="font-family:var(--font-mono)">bongos setup</code> command they run on their own machine; web sign-in works without it.</p></li>';
6768
6786
  }
6769
6787
  var appUrl = live.appSlug ? 'https://github.com/settings/apps/' + esc(live.appSlug) : null;
6770
- return '<li data-onboard-step="device-flow"><b>Let builders sign in from a command line — one checkbox</b><p>' +
6788
+ return '<li class="soptional" data-onboard-step="device-flow">' + title + '<p>Optional — turn it on later. GitHub leaves it off on new apps and lets only you switch it on: ' +
6771
6789
  (appUrl
6772
- ? 'Open <a href="' + appUrl + '" target="_blank" rel="noopener">your app’s settings</a>, check <strong>Enable Device Flow</strong>, and save. '
6790
+ ? 'open <a href="' + appUrl + '" target="_blank" rel="noopener">your app’s settings</a>, check <strong>Enable Device Flow</strong>, and save. '
6773
6791
  : 'GitHub → Settings → Developer settings → GitHub Apps → your app → check <strong>Enable Device Flow</strong> → save. ') +
6774
- 'GitHub leaves it off on new apps and only offers it as this checkbox — until it’s ticked, terminal sign-in falls back to a web token from the hall’s Settings page.</p></li>';
6792
+ 'We tick this when GitHub has it on. Until then builders sign in on the web, and terminal sign-in falls back to a web token from the hall’s Settings page.</p></li>';
6775
6793
  }
6776
6794
 
6777
- /* app-install — the ADR 0055 publish credential (task 1003049): the sign-in App
6778
- doubles as it, but only an INSTALLED App can mint push tokens; without this
6779
- one click every finished task waits at confirmed for a manual merge. */
6795
+ /* app-install — "Install it on your repo": the ADR 0055 publish credential (task
6796
+ 1003049). The same trip carries the founder here straight from the approval (task
6797
+ 1004649); this step ticks only once the runner has asked GitHub, with the app's own
6798
+ key, and GitHub said the app is on this repo. The button is the way back for a
6799
+ founder who returned without installing. */
6780
6800
  function renderStepAppInstall() {
6781
- if (live.manifest !== 'done') {
6782
- return '<li class="slocked" data-onboard-step="app-install"><b>Install the GitHub App on your repo</b><p>Unlocks once GitHub sign-in above is wired — the same app, installed on your repo, is what lets finished work merge and ship itself.</p></li>';
6801
+ var title = '<b>Install it on your repo</b>';
6802
+ if (ghStep('install') === 'done') {
6803
+ return '<li class="sdone" data-onboard-step="app-install">' + title + '<p>The app is on ' + (state.owner && state.repo ? esc(state.owner + '/' + state.repo) : 'your repo') + ', so finished work can merge and ship itself.</p></li>';
6783
6804
  }
6784
- var installUrl = live.appSlug ? 'https://github.com/apps/' + esc(live.appSlug) + '/installations/new' : null;
6785
- return '<li data-onboard-step="app-install"><b>Install the GitHub App on your repo</b><p>One click: install the app you just approved on this project’s repo. Without it, finished work waits for a manual merge instead of shipping itself.</p>' +
6786
- (installUrl
6787
- ? '<a class="ghbtn" href="' + installUrl + '" target="_blank" rel="noopener">' + GH_ICON + 'Install the app on your repo</a>'
6788
- : '<p>GitHub → Settings → Developer settings → GitHub Apps → your app → <strong>Install App</strong> → pick this project’s repo.</p>') +
6789
- '</li>';
6805
+ if (!live.appSlug) {
6806
+ return '<li class="slocked" data-onboard-step="app-install">' + title + '<p>Comes straight after the approval, on the same trip: pick your repo and you land back here.</p></li>';
6807
+ }
6808
+ var installUrl = live.installUrl || ('https://github.com/apps/' + encodeURIComponent(live.appSlug) + '/installations/new');
6809
+ return '<li data-onboard-step="app-install">' + title + '<p>Your GitHub tab goes here next. We check with GitHub that the app is on your repo, and this step ticks itself. Came back without installing it?</p>' +
6810
+ '<a class="ghbtn" href="' + esc(installUrl) + '" target="_blank" rel="noopener">' + GH_ICON + 'Install it on your repo ↗</a></li>';
6790
6811
  }
6791
6812
 
6813
+ /* first-sign-in — "Builders can sign in": ticks when the platform has seen sign-in
6814
+ work on the project (the runner's auth_configured check, the manifest 'done'). The
6815
+ door opens only when the project ANSWERS too: a link to an address that does not
6816
+ resolve yet is the worst first impression a founder can get (task 1003232). */
6792
6817
  function renderStepFirstSignIn() {
6793
6818
  var domain = resolvedDomain();
6819
+ var title = '<b>Builders can sign in</b>';
6794
6820
  if (!domain) {
6795
- return '<li class="slocked" data-onboard-step="first-sign-in"><b>First sign-in</b><p>Unlocks with sign-in above — you’ll open your project’s address, sign in with GitHub once, and be seated as the owner (the founding Archon).</p></li>';
6821
+ return '<li class="slocked" data-onboard-step="first-sign-in">' + title + '<p>Unlocks with an address — then you open it, sign in with GitHub once, and take the owner’s seat (the founding Archon).</p></li>';
6796
6822
  }
6797
- /* the door opens only when the project ANSWERS and sign-in is wired: a
6798
- link to an address that does not resolve yet is the worst first
6799
- impression a founder can get of their own project (task 1003232) */
6800
- if (live.instStatus !== 'active' || !live.addrReady || live.manifest !== 'done') {
6801
- return '<li class="slocked" data-onboard-step="first-sign-in"><b>First sign-in</b><p>Unlocks once your project is answering and sign-in above is wired. Then you’ll open <code style="font-family:var(--font-mono)">' + esc(domain) + '</code>, sign in with GitHub once, and be seated as the owner — the founding Archon.</p></li>';
6823
+ if (live.manifest !== 'done') {
6824
+ return '<li class="slocked" data-onboard-step="first-sign-in">' + title + '<p>Ticks once the platform has seen sign-in work on <code style="font-family:var(--font-mono)">' + esc(domain) + '</code>.</p></li>';
6802
6825
  }
6803
- return '<li data-onboard-step="first-sign-in"><b>First sign-in</b><p>Open <a href="https://' + esc(domain) + '" target="_blank" rel="noopener"><code style="font-family:var(--font-mono)">' + esc(domain) + '</code></a> and sign in with GitHub once. That seats you as the owner — the founding Archon.</p></li>';
6826
+ var open = live.instStatus === 'active' && live.addrReady;
6827
+ return '<li class="sdone" data-onboard-step="first-sign-in">' + title + '<p>Sign-in works on <code style="font-family:var(--font-mono)">' + esc(domain) + '</code>.' +
6828
+ (open ? ' Open it and sign in with GitHub once to take the owner’s seat — the founding Archon.' : ' The door opens here once your project is answering.') + '</p>' +
6829
+ (open ? '<a class="ghbtn" href="https://' + esc(domain) + '" target="_blank" rel="noopener">Open ' + esc(domain) + ' →</a>' : '') + '</li>';
6804
6830
  }
6805
6831
 
6832
+ /* enter-hall — not a row of the GitHub checklist (the "is alive" block below carries
6833
+ the way in); kept only for a project with no address, whose hall is local */
6806
6834
  function renderStepEnterHall() {
6807
6835
  var domain = resolvedDomain();
6808
- if (domain && (live.instStatus !== 'active' || !live.addrReady || live.manifest !== 'done')) {
6809
- return '<li class="slocked" data-onboard-step="enter-hall"><b>Enter your builders hall</b><p>Unlocks with the first sign-in — your project’s home will be <code style="font-family:var(--font-mono)">' + esc(domain) + '/builders</code>.</p></li>';
6810
- }
6811
- return domain
6812
- ? '<li data-onboard-step="enter-hall"><b>Enter your builders hall</b><p>Your project’s home — open <a href="https://' + esc(domain) + '/builders" target="_blank" rel="noopener"><code style="font-family:var(--font-mono)">' + esc(domain) + '/builders</code></a> and start building.</p></li>'
6813
- : '<li data-onboard-step="enter-hall"><b>Enter your builders hall</b><p>No address yet — run <code style="font-family:var(--font-mono)">bongos dev</code> in your repo checkout to start the local hall on your machine.</p></li>';
6836
+ if (domain) return '';
6837
+ return '<li data-onboard-step="enter-hall"><b>Enter your builders hall</b><p>No address yet — run <code style="font-family:var(--font-mono)">bongos dev</code> in your repo checkout to start the local hall on your machine (on an address it lives at /builders).</p></li>';
6814
6838
  }
6815
6839
 
6816
6840
  var STEP_RENDERERS = {
@@ -6828,9 +6852,13 @@ summary{min-height:24px;padding:3px 0;}
6828
6852
  control-plane wizard cannot reach. Spun out per the direction's §9
6829
6853
  delivery-split as task 1002739. */
6830
6854
 
6855
+ /* the mockup's order: the repo (only when the founder must make it), approve,
6856
+ install, sign-in, command line; a plan step this page has no row for goes last */
6857
+ var STEP_ORDER = ['github-repo', 'oauth-app', 'app-install', 'first-sign-in', 'device-flow', 'enter-hall'];
6858
+ function stepRank(s) { var i = STEP_ORDER.indexOf(s.key); return i < 0 ? STEP_ORDER.length : i; }
6831
6859
  function renderSteps() {
6832
6860
  fetchPlan().then(function (steps) {
6833
- var html = steps.filter(function (s) { return s && s.kind === 'human'; }).map(function (s) {
6861
+ var html = steps.filter(function (s) { return s && s.kind === 'human'; }).slice().sort(function (a, b) { return stepRank(a) - stepRank(b); }).map(function (s) {
6834
6862
  var r = STEP_RENDERERS[s.key];
6835
6863
  return r ? r(s) : '<li data-onboard-step="' + esc(s.key) + '"><b>' + esc(s.label || s.key) + '</b></li>';
6836
6864
  }).join('');
@@ -6991,6 +7019,22 @@ summary{min-height:24px;padding:3px 0;}
6991
7019
  function manifestInFlight() {
6992
7020
  return live.manifest === 'pending' || live.manifest === 'code_received' || live.manifest === 'converting';
6993
7021
  }
7022
+ /* the checklist the server derived (task 1004649) — change-only; true when it moved.
7023
+ The install link rides only on a state-keyed read; the owner's read keeps the last. */
7024
+ function seedSetup(checklist, installUrl) {
7025
+ if (!checklist || typeof checklist !== 'object') return false;
7026
+ var next = JSON.stringify(checklist), was = live.setup ? JSON.stringify(live.setup) : '';
7027
+ var url = typeof installUrl === 'string' && /^https:\/\/github\.com\/apps\//.test(installUrl) ? installUrl : null;
7028
+ var urlMoved = !!url && url !== live.installUrl && (!live.installUrl || /[?&]state=/.test(url) || !/[?&]state=/.test(live.installUrl));
7029
+ if (next === was && !urlMoved) return false;
7030
+ live.setup = checklist;
7031
+ if (urlMoved) live.installUrl = url;
7032
+ return true;
7033
+ }
7034
+ /* still worth watching after sign-in: the install not verified yet, while the runner checks */
7035
+ function setupOpen() {
7036
+ return !!(live.setup && !live.setup.connected && live.manifest === 'done');
7037
+ }
6994
7038
  function noteManifestWait() {
6995
7039
  if (live.manifest === 'pending' && !manifestStale && manifestStartedAt && (Date.now() - manifestStartedAt) > 90000) { manifestStale = true; renderSteps(); }
6996
7040
  }
@@ -7006,6 +7050,7 @@ summary{min-height:24px;padding:3px 0;}
7006
7050
  .then(function (d) {
7007
7051
  if (myGen !== manifestGen) return;
7008
7052
  if (d.app_slug && d.app_slug !== live.appSlug) { live.appSlug = d.app_slug; rememberManifest(undefined, undefined, d.app_slug); renderSteps(); }
7053
+ if (seedSetup(d.checklist, d.install_url)) renderSteps(); /* the server-verified ticks (task 1004649) */
7009
7054
  var next = d.ready ? 'done' : (d.status || 'pending');
7010
7055
  var failure = d.failure || null;
7011
7056
  if (next !== live.manifest || failure !== live.manifestFailure) { live.manifest = next; live.manifestFailure = failure; rememberManifest(undefined, next); renderSteps(); }
@@ -7249,6 +7294,8 @@ summary{min-height:24px;padding:3px 0;}
7249
7294
  if (seedChanged) { rememberManifest(undefined, live.manifest, live.appSlug || undefined); renderSteps(); }
7250
7295
  else noteManifestWait();
7251
7296
  }
7297
+ /* the server-verified checklist rides the same read (task 1004649) */
7298
+ if (data.github_setup && !manifestPollTimer && seedSetup(data.github_setup.checklist, data.github_setup.install_url)) renderSteps();
7252
7299
  /* the checklist reflects live state (task 1002694): a status transition —
7253
7300
  e.g. the scaffold landing — re-renders the steps (change-only, so the
7254
7301
  5s tick never clobbers an open dropdown or mid-flight button) */
@@ -7258,7 +7305,7 @@ summary{min-height:24px;padding:3px 0;}
7258
7305
  var terminal = renderProgress(inst);
7259
7306
  /* a GitHub leg still in flight keeps this read alive even once the
7260
7307
  project is up — after a reload it is the ONLY thing watching it */
7261
- if (!terminal || manifestInFlight()) pollTimer = setTimeout(tick, 5000);
7308
+ if (!terminal || manifestInFlight() || setupOpen()) pollTimer = setTimeout(tick, 5000);
7262
7309
  else stopPolling();
7263
7310
  })
7264
7311
  .catch(function (err) {
@@ -7288,7 +7335,7 @@ summary{min-height:24px;padding:3px 0;}
7288
7335
  instanceId = null;
7289
7336
  watchPending = false; manifestPending = false; manifestState = null;
7290
7337
  manifestStartedAt = 0; manifestStale = false;
7291
- live = { instStatus: null, addrReady: false, manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null };
7338
+ live = { instStatus: null, addrReady: false, manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null, setup: null, installUrl: null };
7292
7339
  lastStepsHtml = '';
7293
7340
  lastProgHtml = '';
7294
7341
  hopReset();
@@ -7722,7 +7769,8 @@ summary{min-height:24px;padding:3px 0;}
7722
7769
  var list = $('humanSteps'), st = $('ghState');
7723
7770
  if (!list || !st) return;
7724
7771
  if (state.step !== 2) { st.textContent = 'opens once your project exists'; return; }
7725
- var items = Array.prototype.slice.call(list.children);
7772
+ /* the optional step (command-line sign-in) never holds "connected" back */
7773
+ var items = Array.prototype.slice.call(list.children).filter(function (li) { return !li.classList.contains('soptional'); });
7726
7774
  var done = items.filter(function (li) { return li.classList.contains('sdone'); }).length;
7727
7775
  st.textContent = items.length && done === items.length ? 'connected ✓'
7728
7776
  : manifestInFlight() ? 'waiting on GitHub…'
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.93",
3
+ "version": "1.21.94",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.21.93",
9
+ "version": "1.21.94",
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.21.93",
3
+ "version": "1.21.94",
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",
@@ -9388,5 +9388,11 @@
9388
9388
  "id": "1004657",
9389
9389
  "text": "When a project votes that a part of its government is decided by a group, that vote now holds on every page, not only the People page. Before, someone with the right permission could still switch unattended building on, sign o"
9390
9390
  }
9391
+ ],
9392
+ "1.21.94": [
9393
+ {
9394
+ "id": "1004649",
9395
+ "text": "Connecting GitHub to a new project is now one trip. The founder clicks \"Connect GitHub\", approves the project's app on GitHub, and GitHub carries straight on to installing it on their repo (already picked when the repo is publ"
9396
+ }
9391
9397
  ]
9392
9398
  }
@@ -172,12 +172,14 @@ function main() {
172
172
  cp.execSync(`systemctl restart ${slug}`, { stdio: 'inherit' });
173
173
  console.log(`✓ restarted ${slug}`);
174
174
 
175
- // (3) VERIFY — probe /healthz for auth_configured (retry briefly for restart).
175
+ // (3) VERIFY — probe the JSON health answer for auth_configured (retry briefly for
176
+ // restart). /api/bongos/healthz, not the root /healthz, which answers a plain "ok"
177
+ // that never parses (task 1004577).
176
178
  if (!port) { console.error('✖ no PORT in web.env and no --port given — cannot probe /healthz'); process.exit(1); }
177
179
  let probe = { authConfigured: false };
178
180
  for (let i = 0; i < 10; i++) {
179
181
  try {
180
- const out = cp.execSync(`curl -sS --max-time 5 http://127.0.0.1:${port}/healthz`, { stdio: ['ignore', 'pipe', 'pipe'] }).toString();
182
+ const out = cp.execSync(`curl -sS --max-time 5 http://127.0.0.1:${port}/api/bongos/healthz`, { stdio: ['ignore', 'pipe', 'pipe'] }).toString();
181
183
  probe = parseHealthzAuth(out);
182
184
  if (probe.ok || probe.authConfigured) break;
183
185
  } catch { /* not up yet */ }
@@ -0,0 +1,148 @@
1
+ 'use strict';
2
+ // scripts/gds/provision-github-setup.js — the control-plane runner's check of the two
3
+ // GitHub steps a founder finishes ON GITHUB after the app exists: "Install it on your repo"
4
+ // and "Command-line sign-in is on" (task 1004649; the checklist is derived in
5
+ // modules/provisioning/github-setup.js).
6
+ //
7
+ // WHY THE RUNNER. Only the runner holds the app's credentials (it placed them in the
8
+ // project's chmod-600 web.env; ADR 0111 §2 / ADR 0133 — the web tier never sees a secret).
9
+ // The setup_url redirect GitHub sends after an install carries an installation_id, but
10
+ // GitHub's own docs warn it can be spoofed, so a tick needs GitHub's answer to a question
11
+ // asked with the app's own key:
12
+ // install — GET /repos/{owner}/{repo}/installation, authenticated as the app (a JWT
13
+ // signed with its private key). 200 with an id = installed on THIS repo.
14
+ // device — POST github.com/login/device/code with the app's client id. GitHub answers a
15
+ // device code when device flow is on and `device_flow_disabled` when it is off.
16
+ // The unused code simply expires; nothing is signed in. GitHub has NO manifest
17
+ // field and NO API that turns device flow on (spike, task 1004649), so this
18
+ // only ever OBSERVES the founder's own tick on GitHub.
19
+ // A verified fact is recorded once as a provisioning_events row; the checklist reads it.
20
+ //
21
+ // BOUNDED. Only attempts that settled within VERIFY_WINDOW_MS, or whose install was
22
+ // reported within it, are checked, at most SWEEP_LIMIT per tick — so a finished or
23
+ // abandoned project costs nothing, and a founder who ticks device flow later re-opens the
24
+ // window by reinstalling or from the hall (task 1004652 owns "check again").
25
+ // Secrets: web.env is read on the box, parsed in memory, and only NAMES ever reach a log.
26
+
27
+ const crypto = require('node:crypto');
28
+ const { webEnvPath } = require('./provision-units.js');
29
+ const { SETUP_EVENTS, VERIFY_WINDOW_MS, githubSetupFacts, targetRepo } = require('../../modules/provisioning/github-setup.js');
30
+
31
+ const SWEEP_LIMIT = 20;
32
+ const UA = 'cloud-bongos-provisioner';
33
+
34
+ function b64url(input) {
35
+ return Buffer.from(input).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
36
+ }
37
+
38
+ // An app JWT (RS256, 9 minutes, iat backdated for skew) — GitHub's app authentication.
39
+ function appJwt({ appId, pem, nowSec = Math.floor(Date.now() / 1000) }) {
40
+ const head = b64url(JSON.stringify({ alg: 'RS256', typ: 'JWT' }));
41
+ const body = b64url(JSON.stringify({ iat: nowSec - 60, exp: nowSec + 9 * 60, iss: String(appId) }));
42
+ const signer = crypto.createSign('RSA-SHA256');
43
+ signer.update(`${head}.${body}`);
44
+ signer.end();
45
+ return `${head}.${body}.${b64url(signer.sign(pem))}`;
46
+ }
47
+
48
+ // The app's id, private key and client id out of a web.env body. The pem is stored as one
49
+ // line with literal \n (provision-units.js webEnvBody); the client id under the pack's
50
+ // prefix, which this does not need to know. Missing pieces are null. PURE.
51
+ function appCredsFromEnv(body) {
52
+ const val = (re) => { const m = String(body || '').match(re); return m ? m[1].trim() : null; };
53
+ const pemLine = val(/^GITHUB_APP_PRIVATE_KEY=(.+)$/m);
54
+ return {
55
+ appId: val(/^GITHUB_APP_ID=(\d+)\s*$/m),
56
+ pem: pemLine ? pemLine.replace(/\\n/g, '\n') : null,
57
+ clientId: val(/^[A-Z0-9_]+_GITHUB_CLIENT_ID=(\S+)\s*$/m),
58
+ };
59
+ }
60
+
61
+ // Is the app installed on owner/repo? true · false (404: the app is real, not installed
62
+ // there) · null (cannot tell — any other answer, or no network).
63
+ async function installedOnRepo({ appId, pem, repo, fetchImpl = globalThis.fetch }) {
64
+ try {
65
+ const res = await fetchImpl(`https://api.github.com/repos/${repo}/installation`, {
66
+ headers: { Authorization: `Bearer ${appJwt({ appId, pem })}`, Accept: 'application/vnd.github+json', 'X-GitHub-Api-Version': '2022-11-28', 'User-Agent': UA },
67
+ });
68
+ if (res.status === 404) return false;
69
+ if (!res.ok) return null;
70
+ const j = await res.json().catch(() => null);
71
+ return !!(j && j.id != null);
72
+ } catch (_) { return null; }
73
+ }
74
+
75
+ // Does GitHub accept a device-flow start for this client id? true · false
76
+ // (device_flow_disabled) · null (any other answer — never read as "off").
77
+ async function deviceFlowOn({ clientId, fetchImpl = globalThis.fetch }) {
78
+ try {
79
+ const res = await fetchImpl('https://github.com/login/device/code', {
80
+ method: 'POST',
81
+ headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': UA },
82
+ body: `client_id=${encodeURIComponent(clientId)}`,
83
+ });
84
+ const j = await res.json().catch(() => null);
85
+ if (j && j.device_code) return true;
86
+ if (j && /device_flow_disabled/.test(String(j.error || ''))) return false;
87
+ return null;
88
+ } catch (_) { return null; }
89
+ }
90
+
91
+ // The attempts worth checking this tick: each instance's LATEST attempt, past the
92
+ // credential placement (an app id recorded), settled or reported within the window.
93
+ async function attemptsToVerify(db) {
94
+ const windowSec = Math.round(VERIFY_WINDOW_MS / 1000);
95
+ const { rows } = await db.query(
96
+ `WITH latest AS (
97
+ SELECT DISTINCT ON (instance_id) * FROM provisioning_oauth_manifests ORDER BY instance_id, created_at DESC)
98
+ SELECT l.id, l.instance_id, l.created_at, l.requested_by, i.slug, i.target_ref, i.hosting_shape, i.status AS instance_status
99
+ FROM latest l JOIN provisioning_instances i ON i.id = l.instance_id
100
+ WHERE l.app_id IS NOT NULL AND l.status IN ('done', 'error') AND i.status = 'active'
101
+ AND (l.updated_at > now() - ($1 * interval '1 second')
102
+ OR EXISTS (SELECT 1 FROM provisioning_events e WHERE e.instance_id = l.instance_id
103
+ AND e.event = $2 AND e.created_at > now() - ($1 * interval '1 second')))
104
+ ORDER BY l.updated_at DESC LIMIT ${SWEEP_LIMIT}`,
105
+ [windowSec, SETUP_EVENTS.installReported]);
106
+ return rows;
107
+ }
108
+
109
+ // One tick: for every attempt in the window, check whatever is not verified yet and record
110
+ // what GitHub confirmed. Returns { checked, installed, deviceFlow }.
111
+ async function verifyGithubAppSetups(deps) {
112
+ const { db, provisioning, log, apply } = deps;
113
+ const out = { checked: 0, installed: 0, deviceFlow: 0 };
114
+ const rows = await attemptsToVerify(db);
115
+ if (!apply) {
116
+ for (const r of rows) log(` would check the GitHub install + device flow for ${r.slug} (manifest #${r.id})`);
117
+ return out;
118
+ }
119
+ for (const r of rows) {
120
+ const facts = await githubSetupFacts(db, r.instance_id, r.created_at);
121
+ if (facts.installedAt && facts.deviceFlowAt) continue;
122
+ const onControlBox = r.hosting_shape === 'cloud-host' || r.hosting_shape === 'control-plane';
123
+ const boxExec = onControlBox ? (deps.controlExec || deps.exec) : deps.exec;
124
+ const read = boxExec(`${deps.privileged ? 'sudo ' : ''}cat ${webEnvPath(r)}`, { allowFail: true });
125
+ const creds = appCredsFromEnv(read && read.stdout);
126
+ out.checked++;
127
+ const repo = targetRepo(r);
128
+ const fetchImpl = deps.fetchImpl;
129
+ if (!facts.installedAt && creds.appId && creds.pem && repo) {
130
+ if (await installedOnRepo({ appId: creds.appId, pem: creds.pem, repo, fetchImpl })) {
131
+ await provisioning.recordEvent(db, { instanceId: r.instance_id, ownerBuilderId: r.requested_by, event: SETUP_EVENTS.installed,
132
+ detail: `the GitHub app is installed on ${repo} (checked with the app's own key)`, actor: 'runner' });
133
+ out.installed++; log(` ✓ ${r.slug}: GitHub app installed on ${repo}`);
134
+ }
135
+ }
136
+ if (!facts.deviceFlowAt && creds.clientId) {
137
+ if (await deviceFlowOn({ clientId: creds.clientId, fetchImpl })) {
138
+ await provisioning.recordEvent(db, { instanceId: r.instance_id, ownerBuilderId: r.requested_by, event: SETUP_EVENTS.deviceFlow,
139
+ detail: 'command-line sign-in (device flow) is on for the GitHub app', actor: 'runner' });
140
+ out.deviceFlow++; log(` ✓ ${r.slug}: command-line sign-in is on`);
141
+ }
142
+ }
143
+ }
144
+ if (out.checked) log(`github-setup — checked=${out.checked} installed=${out.installed} device-flow=${out.deviceFlow}`);
145
+ return out;
146
+ }
147
+
148
+ module.exports = { appJwt, appCredsFromEnv, installedOnRepo, deviceFlowOn, attemptsToVerify, verifyGithubAppSetups, SWEEP_LIMIT };
@@ -1039,6 +1039,13 @@ async function convertManifestCode(code, { fetchImpl = globalThis.fetch, userAge
1039
1039
  };
1040
1040
  }
1041
1041
 
1042
+ // The sign-in probe: the project's JSON health answer, which carries auth_configured
1043
+ // (src/bongos/routes/healthz.js). NOT healthzCmd — the root /healthz answers a plain "ok",
1044
+ // which never parses, so every setup that worked was recorded FAILED (task 1004577).
1045
+ function authHealthzCmd(inst) {
1046
+ return `curl -sS --max-time 5 http://127.0.0.1:${inst.port}/api/bongos/healthz`;
1047
+ }
1048
+
1042
1049
  // Convert one manifest row's code → creds and PLACE them (rewrite web.env + restart +
1043
1050
  // verify /healthz). Uses the SAME executors provisionInstance does, so co-tenant (local
1044
1051
  // or remote-cohost) and standalone (control-plane-local) are handled identically. Returns { ok, appId, appSlug } | { ok:false, error }.
@@ -1082,15 +1089,19 @@ async function placeManifestCreds(inst, mrow, deps) {
1082
1089
  appId: creds.appId, appPem: creds.appPem,
1083
1090
  }), { mode: 0o600, sudo: true });
1084
1091
  pmFor(deps).restartService(inst, { exec: boxExec, sudoP });
1092
+ // The app now exists AND its credentials are placed — the checklist's first tick, and the
1093
+ // slug the callback page needs to send the founder on to installing it (task 1004649).
1094
+ if (apply && deps.onCredsPlaced) await Promise.resolve().then(() => deps.onCredsPlaced({ appId: creds.appId, appSlug: creds.appSlug })).catch(() => {});
1085
1095
 
1086
- // 4) verify /healthz reports auth_configured=true (only meaningful under --apply).
1096
+ // 4) verify the project reports auth_configured=true (only meaningful under --apply).
1087
1097
  // PATIENT (F23): the child must boot before it can answer; losing this race
1088
- // used to record a failure for a setup that had succeeded.
1098
+ // used to record a failure for a setup that had succeeded. The JSON answer lives
1099
+ // at /api/bongos/healthz — the root /healthz says a plain "ok" (task 1004577).
1089
1100
  if (apply) {
1090
1101
  const sleep = deps.sleep || ((ms) => new Promise((r) => setTimeout(r, ms)));
1091
1102
  let authConfigured = false;
1092
1103
  for (let i = 0; i < MANIFEST_VERIFY_TRIES; i++) {
1093
- const out = boxExec(healthzCmd(inst), { allowFail: true });
1104
+ const out = boxExec(authHealthzCmd(inst), { allowFail: true });
1094
1105
  if (oauthSecret.parseHealthzAuth(out && out.stdout).authConfigured) { authConfigured = true; break; }
1095
1106
  await sleep(MANIFEST_VERIFY_INTERVAL_MS);
1096
1107
  }
@@ -1149,7 +1160,9 @@ async function drainOAuthManifests(deps) {
1149
1160
  try {
1150
1161
  const inst = await provisioning.getInstanceById(db, mrow.instance_id);
1151
1162
  if (!inst) { await provisioning.resolveOAuthManifest(db, mrow.id, 'error', { last_error: 'instance row vanished' }); errors++; continue; }
1152
- const result = await placeManifestCreds(inst, mrow, deps);
1163
+ // the slug lands on the row the moment the credentials are placed (task 1004649)
1164
+ const onCredsPlaced = (c) => provisioning.resolveOAuthManifest(db, mrow.id, 'converting', { app_id: c.appId, app_slug: c.appSlug });
1165
+ const result = await placeManifestCreds(inst, mrow, { ...deps, onCredsPlaced });
1153
1166
  if (result && result.ok === false) {
1154
1167
  // A SPENT code is terminal, always (F23): GitHub's manifest code is
1155
1168
  // single-use, so resetting the row to 'code_received' guarantees the
@@ -1257,6 +1270,7 @@ async function cmdRunIntents(deps) {
1257
1270
  }
1258
1271
  await snapshotAllCoreVersions(deps); // refresh what the deploy control reads, AFTER the drain (ADR 0293 D3/D6)
1259
1272
  const m = await drainOAuthManifests(deps); // same tick: convert + place any GitHub-App-Manifest creds awaiting the control plane
1273
+ await require('./provision-github-setup.js').verifyGithubAppSetups(deps).catch((e) => log(` github-setup check failed (non-blocking): ${e.message}`)); // install + device flow, asked of GitHub with the app's key (task 1004649)
1260
1274
  log(`done — ran=${ran} ok=${ok} errors=${errors}; oauth-manifest converted=${m.converted} errors=${m.errors}`);
1261
1275
  return { ran, ok, errors, manifests: m };
1262
1276
  }
@@ -1438,7 +1452,7 @@ module.exports = {
1438
1452
  // restart an unreachable ACTIVE instance (task 1003238) — the settings leg minus the env patch
1439
1453
  restartInstance,
1440
1454
  // GitHub App Manifest flow (task 2080, ADR 0133)
1441
- convertManifestCode, placeManifestCreds, drainOAuthManifests, MANIFEST_UA,
1455
+ convertManifestCode, placeManifestCreds, drainOAuthManifests, authHealthzCmd, MANIFEST_UA,
1442
1456
  // cross-site identity federation (ADR 0141 §4, task #1002109)
1443
1457
  federationHubOrigin, shouldFederate, federateInstance,
1444
1458
  };
package/src/module-api.js CHANGED
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
75
75
  // MAJOR (see allowBoxScope below): passes the request through untouched.
76
76
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
77
77
 
78
- const CORE_VERSION = '1.21.93'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.21.94'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
79
79
 
80
80
  // A namespaced logger so a module's log lines are attributable + consistent.
81
81
  // Usage: const log = api.logger('discord'); log.info('mounted');