@bongos/core 1.21.119 → 1.21.120

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 CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.21.119",
6
- "core_contract": "1.21.119",
7
- "source_commit": "e45da330efcf6e68ae4775165b5025cbb4903372",
5
+ "core_version": "1.21.120",
6
+ "core_contract": "1.21.120",
7
+ "source_commit": "a0abf3fc6c913d358ffd525f1592603c5ba771b0",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-10-14T13:43:35.449Z",
9
+ "built_at": "2026-10-14T16:25:27.328Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 605,
13
13
  "agent_docs_stubbed": 28,
14
- "functional_verbatim": 3116,
14
+ "functional_verbatim": 3117,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 3750,
20
- "tree_sha256": "ab4ca3dcbd21d9cc55a93db106532caca2363b955e5c205e9f69d3992547742e",
19
+ "file_count": 3751,
20
+ "tree_sha256": "9223a47c6d78a1edb87bbb0565101cac2b5d3c655b3cc9a703bee7c6116db5be",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2922,7 +2922,7 @@
2922
2922
  {
2923
2923
  "path": "docs/module-api-changelog.md",
2924
2924
  "mode": "0000644",
2925
- "sha256": "9273e8ea9033856fb3df24625de22515aa4129046a17c41c65bc5d3f71ec8e24"
2925
+ "sha256": "72ce941af880f14ccd6a7b1df337934e527a8553d85cbe8b986076bbcf1358cc"
2926
2926
  },
2927
2927
  {
2928
2928
  "path": "docs/modules-contract.md",
@@ -3012,7 +3012,7 @@
3012
3012
  {
3013
3013
  "path": "docs/page-readings.json",
3014
3014
  "mode": "0000644",
3015
- "sha256": "03476cabc2dae58cf07f00b17125cfaca03a5de921359c8a6e14e540cb40d2ce"
3015
+ "sha256": "cda2d7936c5a440c9bf7a535d553c316ee41271a7081b283d52d1599f605d052"
3016
3016
  },
3017
3017
  {
3018
3018
  "path": "docs/project-context.template.md",
@@ -8537,7 +8537,7 @@
8537
8537
  {
8538
8538
  "path": "modules/provisioning/github-setup.js",
8539
8539
  "mode": "0000644",
8540
- "sha256": "78d6aa74315507f0de80b676858fd2c952cdf1e2038b197326bded06b5708170"
8540
+ "sha256": "3de4b170e869c21236d5e5c60f4a0774788dc3022a9879c20dab09a50b210f04"
8541
8541
  },
8542
8542
  {
8543
8543
  "path": "modules/provisioning/look.js",
@@ -8772,7 +8772,7 @@
8772
8772
  {
8773
8773
  "path": "modules/provisioning/pollers/liveness-sweep.js",
8774
8774
  "mode": "0000644",
8775
- "sha256": "57cf7145af0f4236f6fb6a3a9e9ac7efd39de04f604e4b7498c811df8cf3ec93"
8775
+ "sha256": "2403b4c3da7a401e65f9d97815d6582e17c28dcb5fdc78fc3956906163acb712"
8776
8776
  },
8777
8777
  {
8778
8778
  "path": "modules/provisioning/pollers/runner-drift-notify.js",
@@ -8907,7 +8907,7 @@
8907
8907
  {
8908
8908
  "path": "modules/provisioning/tests/liveness.mjs",
8909
8909
  "mode": "0000644",
8910
- "sha256": "a0783526eac5ad6394d85f93e9ff709eff7ab588969390303f70763b5c6f9a23"
8910
+ "sha256": "a0889e57bf8f620f678dbd5d39799ae261e6a813586af9206dd87e453b34edba"
8911
8911
  },
8912
8912
  {
8913
8913
  "path": "modules/provisioning/tests/provisioning.mjs",
@@ -9667,7 +9667,7 @@
9667
9667
  {
9668
9668
  "path": "modules/public-landing/public/projects.html",
9669
9669
  "mode": "0000644",
9670
- "sha256": "97b606c1d45295be87adb7165ceb5ab36aca28f89b86a91302ffc212c1053b27"
9670
+ "sha256": "060c9904c6fefb2848819609a61bbb76822c273c601d670500a9e031380120e1"
9671
9671
  },
9672
9672
  {
9673
9673
  "path": "modules/public-landing/public/projects.probes.json",
@@ -10467,12 +10467,12 @@
10467
10467
  {
10468
10468
  "path": "package-lock.json",
10469
10469
  "mode": "0000644",
10470
- "sha256": "6469337696b64177fd52f7466f9ac81a7944f2095f16b3d0f7689569038efd02"
10470
+ "sha256": "acf5cb64360f149851f33d37eb868b75ea6a476b6a26fe8c7fba494d0a530202"
10471
10471
  },
10472
10472
  {
10473
10473
  "path": "package.json",
10474
10474
  "mode": "0000644",
10475
- "sha256": "90f1c32901b1026f763dc05ee48aa2e1a6aa09727fbe360f49fbb4d21e5126a6"
10475
+ "sha256": "0e819e620052f3e27e6088f87839bfa0ac850b3b1992024fd13c5540930cafce"
10476
10476
  },
10477
10477
  {
10478
10478
  "path": "public-docs/index.html",
@@ -10492,7 +10492,7 @@
10492
10492
  {
10493
10493
  "path": "release-notes.json",
10494
10494
  "mode": "0000644",
10495
- "sha256": "ef8c6938bb9e62be346bd1a44b2a590a7ff503bbbc0b44fc7ad2a4d112e96910"
10495
+ "sha256": "6969dc70a1c0541fcd0a79f7b801d7de58b8a84b96c22e8e36b5524432878219"
10496
10496
  },
10497
10497
  {
10498
10498
  "path": "scripts/cron/style_guide_audit.sh",
@@ -12802,7 +12802,7 @@
12802
12802
  {
12803
12803
  "path": "src/module-api.js",
12804
12804
  "mode": "0000644",
12805
- "sha256": "9d6d6c4c7cbec83c8655f8299d3700b0810cb2d6e0cf5b7a06757e33cd756263"
12805
+ "sha256": "ea34607161a0fbc072ecd805d5e150692df9557f574b6c74ebd53f96ea8ba341"
12806
12806
  },
12807
12807
  {
12808
12808
  "path": "src/module-loader/catalog.js",
@@ -14482,7 +14482,7 @@
14482
14482
  {
14483
14483
  "path": "tests/github_one_trip.mjs",
14484
14484
  "mode": "0000644",
14485
- "sha256": "fe4267e980206aa0dfe2180889af1139eb77c7ce0e3f7bfdb60e2a0565314152"
14485
+ "sha256": "55c30dc99188ef3db4a69f61b048e5d9f65b0a62bfe2236e17b61fd1076137ee"
14486
14486
  },
14487
14487
  {
14488
14488
  "path": "tests/github_org_account.mjs",
@@ -14524,6 +14524,11 @@
14524
14524
  "mode": "0000644",
14525
14525
  "sha256": "d0c12aab13423b92d3b1d1199ac75950da769f56d361853f4ab2f3eda98e6620"
14526
14526
  },
14527
+ {
14528
+ "path": "tests/github_sign_in_seen.mjs",
14529
+ "mode": "0000644",
14530
+ "sha256": "7a337ab8268947ad705872d341e1d1d95df70fed342e4ecb1df82ff3ef0044c2"
14531
+ },
14527
14532
  {
14528
14533
  "path": "tests/github_token_session_life.mjs",
14529
14534
  "mode": "0000644",
@@ -17832,7 +17837,7 @@
17832
17837
  {
17833
17838
  "path": "tests/setup_status_facts.mjs",
17834
17839
  "mode": "0000644",
17835
- "sha256": "8a82be51e797e64e1525fa703b88a2fd0af00125565027d54085a45b2a279cd0"
17840
+ "sha256": "3ef47e51d12d3cfc49d4da87145d9bb29e213e51cf77e0a0c5ecbf424e3b5a37"
17836
17841
  },
17837
17842
  {
17838
17843
  "path": "tests/shell_stat_portability.mjs",
@@ -3075,5 +3075,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
3075
3075
  landed since 1.21.117 with no explicit bump. run 38552002742. (task 1002620)
3076
3076
  1.21.119 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3077
3077
  landed since 1.21.118 with no explicit bump. run 38574092141. (task 1002620)
3078
+ 1.21.120 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3079
+ landed since 1.21.119 with no explicit bump. run 38597828843. (task 1002620)
3078
3080
  ---------------------------------------------------------------------------
3079
3081
  ```
@@ -5129,7 +5129,7 @@
5129
5129
  "surface": "landing",
5130
5130
  "title": "{{productName}} — projects",
5131
5131
  "states_read": ["out-map","in-map","in-map-projects","in-map-profile","in-map-kbd","in-map-search","in-map-mine-on","in-table","in-table-by-builders","in-mine","in-manage","in-manage-address","in-manage-planet","in-manage-card","in-manage-look","in-manage-kind","in-manage-invite","in-manage-invite-handoff","in-manage-invite-guild","in-manage-app-live","in-manage-app-building","in-manage-app-failed","in-manage-app-retry","in-manage-app-offer","in-manage-app-connect","in-manage-snag","in-manage-gone","in-manage-missing","out-manage","in-new","in-new-named","in-new-ready","in-new-name-prefilled","in-new-demo","in-new-demo-used","in-new-checks-filled","in-new-checks-entity","in-new-row-code","in-new-row-address","in-new-row-team","in-new-row-extras","in-new-row-rewards","in-new-row-app","in-new-row-run","in-new-row-ship","in-new-look","in-new-look-custom","in-new-create-blocked","in-new-done","in-new-resume-offer","out-new","out-map-projects","card","card-noart","card-self","in-manage-demo","in-mine-dns-pending","in-manage-dns-pending","in-mine-empty","card-floor","card-backdrop","in-new-done-live","setup-just-created","setup-waiting-on-github","setup-installing","setup-stopped","setup-all-done","setup-cloud-hop-open"],
5132
- "files_hash": "7cf86c7f93b39749",
5132
+ "files_hash": "82d94c8932993dbd",
5133
5133
  "reading_hash": "302909900601b80c",
5134
5134
  "counts": {"lines":825,"placed":246,"shared":21,"unplaced":558},
5135
5135
  "omitted": {"data":150},
@@ -19,7 +19,11 @@
19
19
  // docs warn it can be spoofed, so that redirect only records that the founder
20
20
  // SAYS they installed it ('github-app-install-reported'), which makes the
21
21
  // runner look again.
22
- // sign_in — the runner saw the project report auth_configured (the manifest row 'done').
22
+ // sign_in — the runner saw the project report auth_configured (the manifest row 'done'),
23
+ // OR the liveness sweep did ('github-sign-in-seen', task 1004410). The second
24
+ // is what keeps the step honest when sign-in was wired some other way (a manual
25
+ // OAuth app, or a GitHub tab that never came back): the project's own answer
26
+ // is the fact the step names, so it ticks whatever the attempt row says.
23
27
  // device — the runner started a device-flow sign-in with the app's client id and GitHub
24
28
  // did not refuse it with device_flow_disabled ('github-app-device-flow').
25
29
  // GitHub offers no manifest field and no API to switch device flow on (the
@@ -43,6 +47,9 @@ const SETUP_EVENTS = Object.freeze({
43
47
  // the founder pressed "Pick up where it stopped" or "Check again" (task 1004652). Like an
44
48
  // install report it re-opens the runner's checking window; it claims nothing itself.
45
49
  checkRequested: 'github-app-check-requested',
50
+ // the liveness sweep saw the project's own /healthz answer auth_configured: true (task
51
+ // 1004410). Recorded once per attempt (recordSignInSeen); no detail.
52
+ signInSeen: 'github-sign-in-seen',
46
53
  });
47
54
 
48
55
  const { effectiveManifestStatus } = require('./manifest-truth');
@@ -99,7 +106,7 @@ async function githubSetupFacts(db, instanceId, since) {
99
106
  const org = find(SETUP_EVENTS.appOrg);
100
107
  return {
101
108
  installReportedAt: at(SETUP_EVENTS.installReported), installedAt: at(SETUP_EVENTS.installed), deviceFlowAt: at(SETUP_EVENTS.deviceFlow),
102
- checkRequestedAt: at(SETUP_EVENTS.checkRequested),
109
+ checkRequestedAt: at(SETUP_EVENTS.checkRequested), signInSeenAt: at(SETUP_EVENTS.signInSeen),
103
110
  appOrg: org && validGithubLogin(org.detail) ? org.detail : null,
104
111
  };
105
112
  }
@@ -108,22 +115,42 @@ async function githubSetupFacts(db, instanceId, since) {
108
115
  // vocabulary) + the facts above. A closed vocabulary the page turns into words:
109
116
  // 'done' · 'now' (waiting on GitHub, the founder or the platform) · 'todo' (not reached)
110
117
  // · 'stopped' (this attempt failed at this step) · 'later' (device flow only: optional, off).
118
+ // A project that answers sign-in works with no app of ours recorded was wired some other
119
+ // way: approve and install name steps it never needed, so they read done and
120
+ // `wired_elsewhere` says why (task 1004410). With an app recorded, they keep their own facts.
111
121
  // PURE.
112
122
  function githubChecklist(status, appSlug, facts = {}) {
113
123
  const s = String(status || '');
114
124
  const inFlight = s === 'pending' || s === 'code_received' || s === 'converting';
115
125
  const failed = s === 'error' || s === 'expired';
116
- const approve = appSlug ? 'done' : inFlight ? 'now' : failed ? 'stopped' : 'todo';
117
- const install = facts.installedAt ? 'done' : appSlug ? 'now' : 'todo';
118
- const signIn = s === 'done' ? 'done' : appSlug && failed ? 'stopped' : appSlug ? 'now' : 'todo';
126
+ const seen = !!facts.signInSeenAt;
127
+ const elsewhere = seen && !appSlug;
128
+ const approve = appSlug || elsewhere ? 'done' : inFlight ? 'now' : failed ? 'stopped' : 'todo';
129
+ const install = facts.installedAt || elsewhere ? 'done' : appSlug ? 'now' : 'todo';
130
+ const signIn = s === 'done' || seen ? 'done' : appSlug && failed ? 'stopped' : appSlug ? 'now' : 'todo';
119
131
  const device = facts.deviceFlowAt ? 'done' : s === 'done' ? 'later' : 'todo';
120
132
  return {
121
133
  approve, install, sign_in: signIn, device_flow: device,
122
134
  // what "is alive" waits for — device flow is optional by design
123
135
  connected: approve === 'done' && install === 'done' && signIn === 'done',
136
+ wired_elsewhere: elsewhere,
124
137
  };
125
138
  }
126
139
 
140
+ // The liveness sweep saw this project answer auth_configured: true (task 1004410). One row
141
+ // per attempt: nothing is written when one already stands since the latest attempt began
142
+ // (or ever, for a project with no attempt), so a five-minute sweep never grows the ledger.
143
+ async function recordSignInSeen(db, instanceId) {
144
+ await db.query(
145
+ `INSERT INTO provisioning_events (instance_id, event, actor)
146
+ SELECT $1, $2, 'liveness'
147
+ WHERE NOT EXISTS (
148
+ SELECT 1 FROM provisioning_events e
149
+ WHERE e.instance_id = $1 AND e.event = $2
150
+ AND e.created_at >= COALESCE((SELECT max(m.created_at) FROM provisioning_oauth_manifests m WHERE m.instance_id = $1), 'epoch'::timestamptz))`,
151
+ [instanceId, SETUP_EVENTS.signInSeen]);
152
+ }
153
+
127
154
  // Is the runner still checking this attempt? While it is, an install or a 'later'
128
155
  // device-flow step can still tick itself, so the page keeps polling. PURE.
129
156
  function stillVerifying(row, facts = {}, now = Date.now()) {
@@ -147,6 +174,8 @@ function stillVerifying(row, facts = {}, now = Date.now()) {
147
174
  // PURE.
148
175
  function pickUpFor(row, status, checklist, verifying, now = Date.now()) {
149
176
  if (!row) return null;
177
+ // sign-in works without our app (task 1004410): there is nothing left to pick up
178
+ if (checklist && checklist.wired_elsewhere) return null;
150
179
  if (status === 'pending') return 'reopen';
151
180
  if (status === 'expired') return 'fresh';
152
181
  if (row.app_slug || row.app_id) {
@@ -195,8 +224,12 @@ async function requeueConversion(db, id) {
195
224
  // lands on the setup page, which sends the founder back to their projects.
196
225
  // A failed facts read is "nothing verified yet" — the honest 'now' states, never a 500.
197
226
  async function githubSetupRead(db, inst, row, status, { now = Date.now(), withState = true } = {}) {
198
- if (!row) return { checklist: githubChecklist(null, null), install_url: null, repo: targetRepo(inst), verifying: false, app_org: null, pick_up: null };
199
227
  let facts = {};
228
+ if (!row) {
229
+ // no attempt: only the project's own answer can tick anything (task 1004410)
230
+ try { facts = (inst && inst.id != null) ? await module.exports.githubSetupFacts(db, inst.id, null) : {}; } catch (_) { facts = {}; }
231
+ return { checklist: githubChecklist(null, null, facts), install_url: null, repo: targetRepo(inst), verifying: false, app_org: null, pick_up: null };
232
+ }
200
233
  // through module.exports so a route test can stub the one DB read on the cached module
201
234
  try { facts = await module.exports.githubSetupFacts(db, row.instance_id, row.created_at); } catch (_) { facts = {}; }
202
235
  const checklist = githubChecklist(status, row.app_slug, facts);
@@ -216,5 +249,5 @@ async function githubSetupRead(db, inst, row, status, { now = Date.now(), withSt
216
249
 
217
250
  module.exports = {
218
251
  SETUP_EVENTS, VERIFY_WINDOW_MS, installUrlFor, targetRepo, githubSetupFacts, githubChecklist, stillVerifying, githubSetupRead,
219
- validGithubLogin, pickUpFor, mintGuard, requeueConversion,
252
+ validGithubLogin, pickUpFor, mintGuard, requeueConversion, recordSignInSeen,
220
253
  };
@@ -27,6 +27,7 @@ const dns = require('node:dns').promises;
27
27
  const { Agent: UndiciAgent } = require('undici');
28
28
  const api = require('../../../src/module-api');
29
29
  const provisioning = require('../provisioning');
30
+ const githubSetup = require('../github-setup');
30
31
  const { planNotices, postNotice } = require('./liveness-notify');
31
32
  // task 1004000: the runner-drift check rides this tick rather than installing a timer of
32
33
  // its own — the sweep already runs every ~5 min on the one host that can see the systemd
@@ -163,8 +164,35 @@ function probeReason(err) {
163
164
  return err.message ? String(err.message).slice(0, 40) : 'error';
164
165
  }
165
166
 
167
+ // The /healthz answer is tiny; read no more than this of a body a builder's host chose.
168
+ const BODY_CAP_BYTES = 4096;
169
+
170
+ // Did the answer say auth_configured: true (task 1004410)? Reads at most BODY_CAP_BYTES
171
+ // and answers false for anything else — a big, slow or non-JSON body is never "configured".
172
+ async function readAuthConfigured(res) {
173
+ try {
174
+ if (!res.body || typeof res.body.getReader !== 'function') {
175
+ return typeof res.json === 'function' ? (await res.json()).auth_configured === true : false;
176
+ }
177
+ const reader = res.body.getReader();
178
+ const chunks = [];
179
+ let size = 0;
180
+ for (;;) {
181
+ const { done, value } = await reader.read();
182
+ if (done) break;
183
+ size += value.length;
184
+ if (size > BODY_CAP_BYTES) { reader.cancel().catch(() => {}); return false; }
185
+ chunks.push(Buffer.from(value));
186
+ }
187
+ return JSON.parse(Buffer.concat(chunks).toString('utf8')).auth_configured === true;
188
+ } catch (_) {
189
+ return false;
190
+ }
191
+ }
192
+
166
193
  // Knock once. Any 2xx = up; everything else (non-2xx, refused, DNS, TLS, timeout)
167
- // = down with a reason. Never throws — a probe result is the whole point.
194
+ // = down with a reason. Never throws — a probe result is the whole point. An up answer
195
+ // also carries whether the project says its sign-in is configured (task 1004410).
168
196
  async function probeHealthz(url, { timeoutMs = PROBE_TIMEOUT_MS, fetchImpl, pin } = {}) {
169
197
  const doFetch = fetchImpl || globalThis.fetch;
170
198
  const dispatcher = pin && !fetchImpl ? pinnedDispatcher(pin, timeoutMs) : null;
@@ -178,7 +206,7 @@ async function probeHealthz(url, { timeoutMs = PROBE_TIMEOUT_MS, fetchImpl, pin
178
206
  headers: { accept: 'application/json, text/plain;q=0.9, */*;q=0.8' },
179
207
  ...(dispatcher ? { dispatcher } : {}),
180
208
  });
181
- if (res.status >= 200 && res.status < 300) return { up: true };
209
+ if (res.status >= 200 && res.status < 300) return { up: true, authConfigured: await readAuthConfigured(res) };
182
210
  return { up: false, reason: `http ${res.status}` };
183
211
  } catch (e) {
184
212
  return { up: false, reason: probeReason(e) };
@@ -229,6 +257,10 @@ async function sweepOnce(deps = {}) {
229
257
  if (row.domain && !row.dns_resolved_at && (pin || probe.up)) {
230
258
  await provisioning.recordDnsResolved(db, row.id).catch((e) => log(`[liveness] dns record failed (non-blocking): ${e && e.message ? e.message : e}`));
231
259
  }
260
+ // The project's own word that sign-in works ticks its setup checklist (task 1004410).
261
+ if (probe.up && probe.authConfigured) {
262
+ await githubSetup.recordSignInSeen(db, row.id).catch((e) => log(`[liveness] sign-in record failed (non-blocking): ${e && e.message ? e.message : e}`));
263
+ }
232
264
  return { row, probed: true, prevUp: rec ? rec.prev_up : null, ...probe };
233
265
  }));
234
266
 
@@ -330,7 +330,7 @@ await ta('the pin is load-bearing on the REAL client: same URL, the pinned addre
330
330
  try {
331
331
  // 'pinned.invalid' resolves nowhere; only the pin can make this connect.
332
332
  const hit = await liveness.probeHealthz(url, { timeoutMs: 3000, pin: { address: '127.0.0.1', family: 4 } });
333
- assert.deepEqual(hit, { up: true }, 'the pinned address is what gets connected');
333
+ assert.deepEqual(hit, { up: true, authConfigured: false }, 'the pinned address is what gets connected');
334
334
  const miss = await liveness.probeHealthz(url, { timeoutMs: 3000, pin: { address: '127.0.0.2', family: 4 } });
335
335
  assert.equal(miss.up, false, 'a different pin cannot reach the same server — the pin, not DNS, decides');
336
336
  } finally {
@@ -6987,7 +6987,11 @@ summary{min-height:24px;padding:3px 0;}
6987
6987
  including when a LATER step stopped (sign-in unconfirmed) */
6988
6988
  var appMade = (ghStep('approve') === 'done' || live.appSlug) && live.manifest !== 'expired' &&
6989
6989
  (live.manifest !== 'error' || live.manifestFailure === 'sign_in_unconfirmed');
6990
- if (appMade) { approve.state = 'done'; approve.sub = name + ' app on ' + (appOrgLogin() || 'your account'); }
6990
+ /* sign-in works with no app of ours: it was wired another way, and the project's own
6991
+ answer is the fact (task 1004410) — never "waiting on GitHub" for a step it skipped */
6992
+ var elsewhere = !!ghStep('wired_elsewhere');
6993
+ if (elsewhere) { approve.state = 'done'; approve.sub = 'Not needed. Sign-in was set up another way.'; }
6994
+ else if (appMade) { approve.state = 'done'; approve.sub = name + ' app on ' + (appOrgLogin() || 'your account'); }
6991
6995
  else if (!dom) { approve.state = 'todo'; approve.sub = 'Opens once it has an address.'; }
6992
6996
  /* GitHub refuses app manifests whose hook URL it cannot reach (owner walk, 2026-08-12) */
6993
6997
  else if (!apexBase()) { approve.state = 'todo'; approve.sub = 'GitHub can’t reach a platform on a local address, so this works only on a public one.'; }
@@ -7010,7 +7014,8 @@ summary{min-height:24px;padding:3px 0;}
7010
7014
 
7011
7015
  var install = { key: 'install', title: 'Install it on your repo', sub: repo || '' };
7012
7016
  var installUrl = live.installUrl || (live.appSlug ? 'https://github.com/apps/' + encodeURIComponent(live.appSlug) + '/installations/new' : null);
7013
- if (ghStep('install') === 'done') install.state = 'done';
7017
+ if (elsewhere) { install.state = 'done'; install.sub = 'Not needed. Sign-in was set up another way.'; }
7018
+ else if (ghStep('install') === 'done') install.state = 'done';
7014
7019
  else if (!live.appSlug) install.state = 'todo';
7015
7020
  /* the runner stopped checking and GitHub never confirmed the install: the link opens the
7016
7021
  SAME app's install page, and the click asks the runner to look again */
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.119",
3
+ "version": "1.21.120",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.21.119",
9
+ "version": "1.21.120",
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.119",
3
+ "version": "1.21.120",
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",
@@ -9696,5 +9696,11 @@
9696
9696
  "id": "1004664",
9697
9697
  "text": "A profile privacy test that had been switched off because it failed on a fresh database is fixed and switched back on, so it guards the public profile page again."
9698
9698
  }
9699
+ ],
9700
+ "1.21.120": [
9701
+ {
9702
+ "id": "1004410",
9703
+ "text": "The new-project page no longer says GitHub sign-in is unfinished when it already works: the platform now checks the project itself and ticks the step."
9704
+ }
9699
9705
  ]
9700
9706
  }
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.119'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.21.120'; // 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');
@@ -35,7 +35,7 @@ const SCRIPT = HUB.slice(HUB.lastIndexOf('<script>'), HUB.lastIndexOf('</script>
35
35
 
36
36
  test('each step reads only what the server has seen — the honest "not yet" states included', () => {
37
37
  const none = GS.githubChecklist(null, null);
38
- assert.deepEqual(none, { approve: 'todo', install: 'todo', sign_in: 'todo', device_flow: 'todo', connected: false }, 'never started');
38
+ assert.deepEqual(none, { approve: 'todo', install: 'todo', sign_in: 'todo', device_flow: 'todo', connected: false, wired_elsewhere: false }, 'never started');
39
39
  assert.equal(GS.githubChecklist('pending', null).approve, 'now', 'waiting for the approval on GitHub');
40
40
  for (const s of ['code_received', 'converting']) {
41
41
  const c = GS.githubChecklist(s, null);
@@ -0,0 +1,148 @@
1
+ // tests/github_sign_in_seen.mjs — the project's own "sign-in works" ticks its setup
2
+ // checklist (task 1004410, from a Discord #bugs report: "says that github wiring is not done.
3
+ // When in reality i did make it and was able to sign in via github to my account").
4
+ //
5
+ // Sign-in used to tick only from the one-click GitHub attempt reaching 'done', so a project
6
+ // whose sign-in was wired some other way — a hand-made OAuth app, or a GitHub tab that never
7
+ // came back — sat at "Waiting on GitHub" for good. Now the liveness sweep reads the project's
8
+ // /healthz answer, records 'github-sign-in-seen' once per attempt, and githubChecklist reads it.
9
+ // The page half (what the founder sees) is tests/setup_status_facts.mjs §6.
10
+ //
11
+ // Pinned here, DB-free over recording fakes and real loopback servers:
12
+ // * githubChecklist: the fact ticks sign-in; with no app of ours it is wired_elsewhere and
13
+ // connected; with one, the install keeps its own fact;
14
+ // * pickUpFor offers nothing to pick up once sign-in works elsewhere;
15
+ // * githubSetupRead reads the fact even when no attempt exists;
16
+ // * recordSignInSeen writes at most one row per attempt;
17
+ // * probeHealthz reports auth_configured only from a small JSON answer saying true;
18
+ // * sweepOnce records the fact only for an up project that says so.
19
+ //
20
+ // Run: node --test tests/github_sign_in_seen.mjs
21
+
22
+ import { test } from 'node:test';
23
+ import assert from 'node:assert/strict';
24
+ import http from 'node:http';
25
+ import path from 'node:path';
26
+ import { createRequire } from 'node:module';
27
+ import { fileURLToPath } from 'node:url';
28
+
29
+ const require = createRequire(import.meta.url);
30
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
31
+ const gs = require(path.join(ROOT, 'modules', 'provisioning', 'github-setup.js'));
32
+ const liveness = require(path.join(ROOT, 'modules', 'provisioning', 'pollers', 'liveness-sweep.js'));
33
+
34
+ const SEEN = { signInSeenAt: '2026-10-14T10:00:00.000Z' };
35
+
36
+ // ── the checklist ──────────────────────────────────────────────────────────────────────
37
+ test('the project saying sign-in works ticks sign-in, whatever the attempt row says', () => {
38
+ for (const status of [null, 'pending', 'expired', 'error', 'code_received']) {
39
+ const c = gs.githubChecklist(status, null, SEEN);
40
+ assert.equal(c.sign_in, 'done', `${status}: sign-in is the project's own answer`);
41
+ assert.equal(c.wired_elsewhere, true, `${status}: no app of ours, so it was wired another way`);
42
+ assert.equal(c.approve, 'done');
43
+ assert.equal(c.install, 'done');
44
+ assert.equal(c.connected, true, `${status}: the project is not held back`);
45
+ }
46
+ });
47
+
48
+ test('without that answer, nothing changes: a pending attempt still waits on GitHub', () => {
49
+ const c = gs.githubChecklist('pending', null, {});
50
+ assert.deepEqual(c, { approve: 'now', install: 'todo', sign_in: 'todo', device_flow: 'todo', connected: false, wired_elsewhere: false });
51
+ });
52
+
53
+ test('with our app recorded, the answer ticks sign-in and the install keeps its own fact', () => {
54
+ const c = gs.githubChecklist('converting', 'kestrel-cloudbongos-com', SEEN);
55
+ assert.equal(c.sign_in, 'done');
56
+ assert.equal(c.install, 'now', 'the install is still GitHub\'s to confirm');
57
+ assert.equal(c.wired_elsewhere, false);
58
+ assert.equal(c.connected, false);
59
+ assert.equal(gs.githubChecklist('done', 'k', { ...SEEN, installedAt: SEEN.signInSeenAt }).connected, true);
60
+ });
61
+
62
+ test('nothing to pick up once sign-in works another way', () => {
63
+ const now = Date.now();
64
+ for (const [status, row] of [['pending', { state: 'S', attempts: 0 }], ['expired', { attempts: 0 }], ['error', { attempts: 2, created_at: new Date(now).toISOString(), github_code: 'c' }]]) {
65
+ const checklist = gs.githubChecklist(status, null, SEEN);
66
+ assert.equal(gs.pickUpFor(row, status, checklist, false, now), null, `${status}: no Reopen, Start fresh or Convert`);
67
+ }
68
+ // and the old answers stand when it does not
69
+ assert.equal(gs.pickUpFor({ state: 'S' }, 'pending', gs.githubChecklist('pending', null, {}), false, now), 'reopen');
70
+ });
71
+
72
+ // ── the read ───────────────────────────────────────────────────────────────────────────
73
+ test('githubSetupRead reads the fact for a project with no attempt at all', async () => {
74
+ const seen = [];
75
+ const pool = {
76
+ query: async (sql, params) => {
77
+ seen.push(params);
78
+ assert.match(sql, /FROM provisioning_events/);
79
+ return { rows: params[1].includes('github-sign-in-seen') ? [{ event: 'github-sign-in-seen', at: SEEN.signInSeenAt, detail: null }] : [] };
80
+ },
81
+ };
82
+ const r = await gs.githubSetupRead(pool, { id: 42, target_ref: 'you/kestrel' }, null, null);
83
+ assert.equal(seen[0][0], 42, 'read by the project\'s own id');
84
+ assert.equal(r.checklist.connected, true);
85
+ assert.equal(r.checklist.wired_elsewhere, true);
86
+ assert.equal(r.pick_up, null);
87
+ // a failed read is "nothing verified yet", never a 500
88
+ const broken = await gs.githubSetupRead({ query: async () => { throw new Error('db down'); } }, { id: 42 }, null, null);
89
+ assert.equal(broken.checklist.sign_in, 'todo');
90
+ });
91
+
92
+ test('recordSignInSeen writes at most one row per attempt, from the liveness sweep', async () => {
93
+ let sql = '', params = null;
94
+ await gs.recordSignInSeen({ query: async (s, p) => { sql = s; params = p; return { rows: [] }; } }, 42);
95
+ assert.deepEqual(params, [42, 'github-sign-in-seen']);
96
+ assert.match(sql, /INSERT INTO provisioning_events/);
97
+ assert.match(sql, /WHERE NOT EXISTS/, 'a five-minute sweep never grows the ledger');
98
+ assert.match(sql, /max\(m\.created_at\) FROM provisioning_oauth_manifests m WHERE m\.instance_id = \$1/, 'once per attempt, so a fresh attempt is seen afresh');
99
+ assert.match(sql, /'liveness'/);
100
+ });
101
+
102
+ // ── the probe and the sweep ────────────────────────────────────────────────────────────
103
+ function serve(body, type = 'application/json') {
104
+ const server = http.createServer((req, res) => { res.writeHead(200, { 'content-type': type }); res.end(body); });
105
+ return new Promise((r) => server.listen(0, '127.0.0.1', () => r(server)));
106
+ }
107
+ function close(server) { if (server.closeAllConnections) server.closeAllConnections(); server.close(); }
108
+
109
+ test('probeHealthz says auth_configured only from a small JSON answer saying true', async () => {
110
+ const cases = [
111
+ ['{"ok":true,"auth_configured":true}', true],
112
+ ['{"ok":true,"auth_configured":false}', false],
113
+ ['{"ok":true}', false],
114
+ ['{"ok":true,"auth_configured":"true"}', false],
115
+ ['ok', false],
116
+ [JSON.stringify({ ok: true, auth_configured: true, pad: 'x'.repeat(8000) }), false],
117
+ ];
118
+ for (const [body, want] of cases) {
119
+ const s = await serve(body);
120
+ try {
121
+ const r = await liveness.probeHealthz(`http://127.0.0.1:${s.address().port}/healthz`, { timeoutMs: 3000 });
122
+ assert.equal(r.up, true, 'any 2xx is still up');
123
+ assert.equal(r.authConfigured, want, body.slice(0, 50));
124
+ } finally { close(s); }
125
+ }
126
+ });
127
+
128
+ test('the sweep records the fact for an up project that says so, and for no other', async () => {
129
+ const yes = await serve('{"ok":true,"auth_configured":true}');
130
+ const no = await serve('{"ok":true,"auth_configured":false}');
131
+ const inserts = [];
132
+ const db = {
133
+ query: async (sql, params) => {
134
+ if (/FROM provisioning_instances/.test(sql) && /WHERE status = 'active'/.test(sql)) {
135
+ return { rows: [{ id: 1, slug: 'kestrel', domain: null, port: yes.address().port }, { id: 2, slug: 'tern', domain: null, port: no.address().port }] };
136
+ }
137
+ if (/^UPDATE provisioning_instances/.test(sql)) return { rows: [{ id: params[0], prev_up: true }] };
138
+ if (/INSERT INTO provisioning_events/.test(sql)) { inserts.push(params); return { rows: [] }; }
139
+ throw new Error(`unexpected SQL: ${sql}`);
140
+ },
141
+ };
142
+ try {
143
+ liveness.resetNoticeState();
144
+ const out = await liveness.sweepOnce({ pool: db, log: () => {}, timeoutMs: 3000, notify: () => {}, collectRunnerDrift: async () => [] });
145
+ assert.equal(out.up, 2);
146
+ assert.deepEqual(inserts, [[1, 'github-sign-in-seen']]);
147
+ } finally { close(yes); close(no); }
148
+ });
@@ -398,3 +398,36 @@ test('part 03 names its own classes: the map table already owns .facts / .fact',
398
398
  assert.match(PANEL, /<div class="setFacts" id="homeFacts"/);
399
399
  assert.match(FACTS, /textNode\('div', 'setFact ' \+ r\.state\)/);
400
400
  });
401
+
402
+ // ── 6. sign-in set up another way (task 1004410) ───────────────────────────────────────
403
+ // A founder whose project already answers "sign-in works" was told "Waiting on GitHub" for
404
+ // good: the step ticked only from the one-click attempt. The liveness sweep now records the
405
+ // project's own answer ('github-sign-in-seen'), and that fact is what the step names.
406
+ const seen = { event: 'github-sign-in-seen', at: ago(2), detail: null };
407
+ const elsewhereCases = {
408
+ 'a GitHub tab that never came back': attempt({ status: 'pending', attempts: 0, created_at: ago(1), expires_at: soon(29) }),
409
+ 'a GitHub link that lapsed': attempt({ status: 'pending', attempts: 0, created_at: ago(90), expires_at: ago(30) }),
410
+ 'no attempt at all (a hand-made OAuth app)': null,
411
+ };
412
+ for (const [label, manifest] of Object.entries(elsewhereCases)) {
413
+ test(`sign-in that works is never "waiting on GitHub": ${label}`, async () => {
414
+ db = JSON.parse(JSON.stringify({ inst: inst(), intent: null, manifest, events: [seen] }));
415
+ const v = (await page()).read();
416
+ assert.deepEqual(v.byKey, { address: 'done', server: 'done', answers: 'done', approve: 'done', install: 'done', sign_in: 'done', device_flow: 'todo' });
417
+ assert.equal(v.count, 'ready ✓');
418
+ assert.equal(v.said, 'Kestrel is ready. Command-line sign-in is optional; it ticks itself whenever you turn it on.');
419
+ const approve = v.facts.find((f) => f.key === 'approve');
420
+ assert.equal(approve.sub, 'Not needed. Sign-in was set up another way.', 'says why a step it skipped is done');
421
+ assert.deepEqual(approve.acts, [], 'no Connect, Reopen or Pick up for a step that is not needed');
422
+ assert.equal(v.aliveHidden, false, 'the project is alive');
423
+ });
424
+ }
425
+
426
+ test('with our app recorded, the project\'s answer ticks sign-in and leaves the install its own fact', async () => {
427
+ db = JSON.parse(JSON.stringify({ inst: inst(), intent: null, manifest: attempt({ status: 'converting', app_id: 5, app_slug: 'kestrel-cloudbongos-com', updated_at: ago(1) }), events: [seen] }));
428
+ const v = (await page()).read();
429
+ assert.equal(v.byKey.approve, 'done');
430
+ assert.equal(v.byKey.sign_in, 'done');
431
+ assert.equal(v.byKey.install, 'you', 'the install is still GitHub\'s to confirm');
432
+ assert.notEqual(v.facts.find((f) => f.key === 'approve').sub, 'Not needed. Sign-in was set up another way.');
433
+ });