@bongos/core 1.21.118 → 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 +26 -21
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +1 -1
- package/modules/provisioning/github-setup.js +40 -7
- package/modules/provisioning/pollers/liveness-sweep.js +34 -2
- package/modules/provisioning/tests/liveness.mjs +1 -1
- package/modules/public-landing/public/projects.html +7 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/run-unit-tests.js +1 -1
- package/src/module-api.js +1 -1
- package/tests/github_one_trip.mjs +1 -1
- package/tests/github_sign_in_seen.mjs +148 -0
- package/tests/hub_profile_leak.mjs +19 -4
- package/tests/setup_status_facts.mjs +33 -0
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.
|
|
6
|
-
"core_contract": "1.21.
|
|
7
|
-
"source_commit": "
|
|
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-
|
|
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":
|
|
14
|
+
"functional_verbatim": 3117,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
10470
|
+
"sha256": "acf5cb64360f149851f33d37eb868b75ea6a476b6a26fe8c7fba494d0a530202"
|
|
10471
10471
|
},
|
|
10472
10472
|
{
|
|
10473
10473
|
"path": "package.json",
|
|
10474
10474
|
"mode": "0000644",
|
|
10475
|
-
"sha256": "
|
|
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": "
|
|
10495
|
+
"sha256": "6969dc70a1c0541fcd0a79f7b801d7de58b8a84b96c22e8e36b5524432878219"
|
|
10496
10496
|
},
|
|
10497
10497
|
{
|
|
10498
10498
|
"path": "scripts/cron/style_guide_audit.sh",
|
|
@@ -11742,7 +11742,7 @@
|
|
|
11742
11742
|
{
|
|
11743
11743
|
"path": "scripts/gds/run-unit-tests.js",
|
|
11744
11744
|
"mode": "0000644",
|
|
11745
|
-
"sha256": "
|
|
11745
|
+
"sha256": "d6d81c3bb8017122cb6b5a46355b637f7986ec324132bcd583f0c02d2725b6d0"
|
|
11746
11746
|
},
|
|
11747
11747
|
{
|
|
11748
11748
|
"path": "scripts/gds/runner-drift.js",
|
|
@@ -12802,7 +12802,7 @@
|
|
|
12802
12802
|
{
|
|
12803
12803
|
"path": "src/module-api.js",
|
|
12804
12804
|
"mode": "0000644",
|
|
12805
|
-
"sha256": "
|
|
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": "
|
|
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",
|
|
@@ -15632,7 +15637,7 @@
|
|
|
15632
15637
|
{
|
|
15633
15638
|
"path": "tests/hub_profile_leak.mjs",
|
|
15634
15639
|
"mode": "0000644",
|
|
15635
|
-
"sha256": "
|
|
15640
|
+
"sha256": "298afa1c71a485c3c64d90a3976c1a7147f1e888a9362a84502d5a9a94611d56"
|
|
15636
15641
|
},
|
|
15637
15642
|
{
|
|
15638
15643
|
"path": "tests/hub_recent_ships.mjs",
|
|
@@ -17832,7 +17837,7 @@
|
|
|
17832
17837
|
{
|
|
17833
17838
|
"path": "tests/setup_status_facts.mjs",
|
|
17834
17839
|
"mode": "0000644",
|
|
17835
|
-
"sha256": "
|
|
17840
|
+
"sha256": "3ef47e51d12d3cfc49d4da87145d9bb29e213e51cf77e0a0c5ecbf424e3b5a37"
|
|
17836
17841
|
},
|
|
17837
17842
|
{
|
|
17838
17843
|
"path": "tests/shell_stat_portability.mjs",
|
|
@@ -3073,5 +3073,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
3073
3073
|
landed since 1.21.116 with no explicit bump. run 38548762428. (task 1002620)
|
|
3074
3074
|
1.21.118 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3075
3075
|
landed since 1.21.117 with no explicit bump. run 38552002742. (task 1002620)
|
|
3076
|
+
1.21.119 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
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)
|
|
3076
3080
|
---------------------------------------------------------------------------
|
|
3077
3081
|
```
|
package/docs/page-readings.json
CHANGED
|
@@ -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": "
|
|
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
|
|
117
|
-
const
|
|
118
|
-
const
|
|
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
|
-
|
|
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 (
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|
package/release-notes.json
CHANGED
|
@@ -9690,5 +9690,17 @@
|
|
|
9690
9690
|
"id": "1004729",
|
|
9691
9691
|
"text": "Hosted projects now take released core updates on their own, one at a time, by each project's update rule (pinned projects never move), with a platform-wide off switch."
|
|
9692
9692
|
}
|
|
9693
|
+
],
|
|
9694
|
+
"1.21.119": [
|
|
9695
|
+
{
|
|
9696
|
+
"id": "1004664",
|
|
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
|
+
}
|
|
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
|
+
}
|
|
9693
9705
|
]
|
|
9694
9706
|
}
|
|
@@ -465,7 +465,7 @@ const INTEGRATION_PARKED = new Set([
|
|
|
465
465
|
'bfg', 'bfg_write',
|
|
466
466
|
'claim_gate_open_blocker', 'claim_gate_rebase',
|
|
467
467
|
'clawback_confirmed_backlog', 'copy_desk_flags_db',
|
|
468
|
-
'government_board_membership_db', 'handle_machinery',
|
|
468
|
+
'government_board_membership_db', 'handle_machinery',
|
|
469
469
|
'idea_credit_streams_db', 'memory_isolation', 'memory_pull', 'needs_rebase',
|
|
470
470
|
'profile_fields', 'project_visibility_db', 'reward_mode_cost_plus_only',
|
|
471
471
|
'session_earnings_mirror_db', 'setup_smoke', 'status-probe',
|
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.
|
|
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
|
+
});
|
|
@@ -68,7 +68,8 @@ const SECRET_HASH = 'HASH-DO-NOT-LEAK-p05';
|
|
|
68
68
|
const SECRET_PROJECT = 'P05-PRIVATE-DO-NOT-LEAK';
|
|
69
69
|
const SECRET_PREV = 'p05-prev-sentinel';
|
|
70
70
|
const PUB_ORIGIN = 'https://p05-pub.example', PRIV_ORIGIN = 'https://p05-priv.example';
|
|
71
|
-
|
|
71
|
+
// crafts: each craft's own summed numbers (task 1004440), named or not like the totals.
|
|
72
|
+
const CP_KEYS = ['totals', 'disciplines', 'crafts', 'last_active', 'projects', 'more_projects'];
|
|
72
73
|
const ACCOUNT_KEYS = ['handle', 'github_login', 'display_name', 'avatar_url', 'member_since', 'bio', 'links'];
|
|
73
74
|
|
|
74
75
|
const q = (sql, params) => pool.query(sql, params);
|
|
@@ -128,6 +129,16 @@ try {
|
|
|
128
129
|
// 42703 "column does not exist" instead of a 404-vs-200 assertion.
|
|
129
130
|
await q(migrationSql('platform_identity_012_account_privacy.sql'));
|
|
130
131
|
await q(migrationSql('platform_identity_019_terms_acceptance.sql'));
|
|
132
|
+
// getRollupForBuilder reads four more (task 1004664 — this file failed 3 of 7 on a FRESH
|
|
133
|
+
// database: every check that reaches the rollup answered 42703, while an old shared
|
|
134
|
+
// database that already had the columns hid it). It names a project only when it is ON
|
|
135
|
+
// THE MAP (projects-feed.js onMapSql: status, the publish gate's description, a known
|
|
136
|
+
// door) AND its door is public — 011 + 020 — and serializes a.recent_ships (012) and
|
|
137
|
+
// a.crafts (029).
|
|
138
|
+
await q(migrationSql('platform_identity_011_project_visibility.sql'));
|
|
139
|
+
await q(migrationSql('platform_identity_012_recent_ships.sql'));
|
|
140
|
+
await q(migrationSql('platform_identity_020_visibility_pull.sql'));
|
|
141
|
+
await q(migrationSql('platform_identity_029_activity_crafts.sql'));
|
|
131
142
|
await cleanup();
|
|
132
143
|
|
|
133
144
|
// The active account carries the P02 fields + a user-set name source, so the
|
|
@@ -150,9 +161,13 @@ try {
|
|
|
150
161
|
VALUES ($1, $3, 'P05 Pub Proj', $4), ($2, $3, $5, $6)`,
|
|
151
162
|
[SECRET_CLIENT_PUB, SECRET_CLIENT_PRIV, SECRET_HASH, PUB_ORIGIN, SECRET_PROJECT, PRIV_ORIGIN],
|
|
152
163
|
);
|
|
164
|
+
// Both projects are on the map — active, described, door known — so the ONLY thing that
|
|
165
|
+
// keeps the second one unnamed is its private door. A fixture that left out the
|
|
166
|
+
// description would pass the "never named" check for the wrong reason (unpublishable).
|
|
153
167
|
await q(
|
|
154
|
-
`INSERT INTO platform_identity_projects (origin, name, featured, status)
|
|
155
|
-
VALUES ($1, 'P05 Pub Proj', true, 'active'
|
|
168
|
+
`INSERT INTO platform_identity_projects (origin, name, featured, status, description, visibility, visibility_source)
|
|
169
|
+
VALUES ($1, 'P05 Pub Proj', true, 'active', 'A public P05 project.', 'public', 'control-plane'),
|
|
170
|
+
($2, $3, false, 'active', 'A private P05 project.', 'private', 'control-plane')`,
|
|
156
171
|
[PUB_ORIGIN, PRIV_ORIGIN, SECRET_PROJECT],
|
|
157
172
|
);
|
|
158
173
|
await q(
|
|
@@ -180,7 +195,7 @@ try {
|
|
|
180
195
|
assert.equal(r.json.cross_project.totals.projects, 2, 'both projects count');
|
|
181
196
|
assert.equal(r.json.cross_project.projects.length, 1, 'only the publicly listed project is named');
|
|
182
197
|
assert.equal(r.json.cross_project.projects[0].name, 'P05 Pub Proj');
|
|
183
|
-
assert.deepEqual(Object.keys(r.json.cross_project.projects[0]).sort(), ['name', 'origin', 'rank_held'].sort());
|
|
198
|
+
assert.deepEqual(Object.keys(r.json.cross_project.projects[0]).sort(), ['name', 'origin', 'rank_held', 'recent_ships'].sort());
|
|
184
199
|
assert.equal(r.json.cross_project.more_projects, 1, 'the non-public project is counted, never named');
|
|
185
200
|
assert.equal(r.json.account.bio, 'Builds things.', 'bio serializes (P02)');
|
|
186
201
|
assert.deepEqual(r.json.account.links, { website: 'https://p05.example/' }, 'links serialize (P02)');
|
|
@@ -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
|
+
});
|