@bongos/core 1.21.45 → 1.21.47
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 +40 -30
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/api/openapi.json +2 -0
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +763 -761
- package/modules/hall-ui/public/street-card.css +8 -2
- package/modules/hall-ui/public/street-desk.js +78 -9
- package/modules/hall-ui/public/street.css +4 -1
- package/modules/hall-ui/public/street.js +9 -6
- package/modules/hall-ui/records/governor-street.md +14 -0
- package/modules/provisioning/manifest-truth.js +63 -0
- package/modules/provisioning/provisioning.js +3 -11
- package/modules/provisioning/routes/provisioning.js +3 -1
- package/modules/public-landing/public/projects.html +38 -11
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/provision.js +4 -2
- package/src/bongos/routes/auth.js +15 -2
- package/src/module-api.js +1 -1
- package/tests/hall_street_desk.mjs +64 -2
- package/tests/projects_hub_pre_uat.mjs +3 -2
- package/tests/provision.mjs +53 -0
- package/tests/provisioning_github_app_routes.mjs +23 -0
- package/tests/provisioning_settings.mjs +6 -4
- package/tests/wizard_github_feedback.mjs +239 -0
|
@@ -108,8 +108,10 @@ body[data-page="street"] {
|
|
|
108
108
|
|
|
109
109
|
.mcard-btn:focus-visible, .mcard-textbtn:focus-visible, .mcard-form__text:focus-visible, .mcard-form__select:focus-visible, .mcard-chip:focus-visible, .mcard-chips__field:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
|
|
110
110
|
|
|
111
|
-
/* the toast: one short line over the street
|
|
112
|
-
|
|
111
|
+
/* the toast: one short line over the street. Centred with translate, not
|
|
112
|
+
transform: the enter animation ends on transform none, which put the toast
|
|
113
|
+
half its width right of centre (task 1004562) */
|
|
114
|
+
.street-toast { position: absolute; left: 50%; top: 192px; translate: -50% 0; max-width: calc(100% - 32px); box-sizing: border-box; font-size: 15px; line-height: 1.35; color: var(--ink); padding: 11px 20px; border-radius: 999px; background: var(--bg-card); border: 1px solid var(--street-rule-strong); z-index: 10; pointer-events: none; text-align: center; }
|
|
113
115
|
.street-toast[hidden] { display: none; }
|
|
114
116
|
|
|
115
117
|
/* the city answers: a building rises (the prototype's rise) or lights, a pin settles */
|
|
@@ -128,6 +130,10 @@ body[data-page="street"] {
|
|
|
128
130
|
@media (max-width: 560px) {
|
|
129
131
|
.street-toast { top: 96px; border-radius: 16px; }
|
|
130
132
|
}
|
|
133
|
+
/* the desk's toast after an act (Desks.dc.html, task 1004562): an ink pill with
|
|
134
|
+
the orange dot, low over the desk, just above the six readings */
|
|
135
|
+
.street-toast--desk { top: auto; bottom: 150px; display: flex; align-items: center; gap: 10px; width: max-content; font-size: 14px; background: var(--ink); color: var(--bg); border: 0; border-radius: 999px; text-align: left; }
|
|
136
|
+
.street-toast--desk::before { content: ''; flex: none; width: 8px; height: 8px; border-radius: 999px; background: var(--accent); }
|
|
131
137
|
|
|
132
138
|
/* The Kill Switch: under reduced motion nothing moves; the street still
|
|
133
139
|
answers, at rest (the toast, the new pins, the lit windows). */
|
|
@@ -96,6 +96,50 @@
|
|
|
96
96
|
if (d < 14) return d + ' days';
|
|
97
97
|
return Math.round(d / 7) + ' weeks';
|
|
98
98
|
}
|
|
99
|
+
// The record line's age, in the board's short words: 'today', '3d', '2w', '1mo'
|
|
100
|
+
// (task 1004562: the long age was cut off the end of nearly every record item).
|
|
101
|
+
function shortAge(at, now) {
|
|
102
|
+
const t = at ? Date.parse(at) : NaN;
|
|
103
|
+
if (!isFinite(t) || !now) return '';
|
|
104
|
+
const d = Math.floor(Math.max(0, now - t) / 86400000);
|
|
105
|
+
if (d < 1) return 'today';
|
|
106
|
+
if (d < 14) return d + 'd';
|
|
107
|
+
if (d < 60) return Math.round(d / 7) + 'w';
|
|
108
|
+
return Math.round(d / 30) + 'mo';
|
|
109
|
+
}
|
|
110
|
+
// One record item: the title, then what came of it and when, in 30
|
|
111
|
+
// characters. The age always shows; the outcome only when the title fits
|
|
112
|
+
// beside it whole ('Lea → metic · 2w' on the board).
|
|
113
|
+
function recordText(h, now) {
|
|
114
|
+
const title = str(h && h.title).replace(/\s+/g, ' ').trim();
|
|
115
|
+
const age = shortAge(h && h.at, now);
|
|
116
|
+
const out = str(h && h.outcome).trim();
|
|
117
|
+
const tail = (parts) => parts.filter(Boolean).join(' · ');
|
|
118
|
+
const full = tail([title, out, age]);
|
|
119
|
+
if (full.length <= LIMITS.recordLen) return full;
|
|
120
|
+
const room = LIMITS.recordLen - (age ? age.length + 3 : 0);
|
|
121
|
+
return fit(tail([fit(title, room), age]), LIMITS.recordLen);
|
|
122
|
+
}
|
|
123
|
+
// A quiet floor's tile says what its number counts, in two words that read as
|
|
124
|
+
// a label: the floor line's own words when they follow that same number and
|
|
125
|
+
// are short ('5 seats'), else the floor's name ('3 testers', '2 signed off'),
|
|
126
|
+
// else the line's last two words ('1 went live'). Never the first two words of
|
|
127
|
+
// a longer line, which read 'Lu, Bo' or 'piece of' (task 1004562).
|
|
128
|
+
function floorLabel(count, line, name) {
|
|
129
|
+
const first = str(line).split(' · ')[0].replace(/\s+/g, ' ').trim();
|
|
130
|
+
const m = /^([$]?[0-9][0-9,.%]*)\s+(.+)$/.exec(first);
|
|
131
|
+
const same = !!m && m[1] === str(count).trim();
|
|
132
|
+
const w = (s) => s.split(' ').filter(Boolean);
|
|
133
|
+
if (same && w(m[2]).length <= LIMITS.tileWords) return m[2];
|
|
134
|
+
const nm = str(name).replace(/\s+/g, ' ').trim().toLowerCase();
|
|
135
|
+
if (nm && !/^§/.test(nm) && w(nm).length <= LIMITS.tileWords) return nm;
|
|
136
|
+
if (same) return w(m[2]).slice(-LIMITS.tileWords).join(' ');
|
|
137
|
+
return 'here';
|
|
138
|
+
}
|
|
139
|
+
// A count agrees with its noun: '1 rank', '2 open votes' (task 1004562).
|
|
140
|
+
const SINGULAR = Object.freeze({ ranks: 'rank', runners: 'runner', builders: 'builder', invites: 'invite', settings: 'setting', modules: 'module', backups: 'backup', updates: 'update' });
|
|
141
|
+
const PLURAL = Object.freeze({ 'open vote': 'open votes', update: 'updates' });
|
|
142
|
+
function agree(n, noun) { return Number(n) === 1 ? (SINGULAR[noun] || noun) : (PLURAL[noun] || noun); }
|
|
99
143
|
|
|
100
144
|
// The words a floor counts its rows in, and says when it is cleared.
|
|
101
145
|
const NOUNS = Object.freeze({
|
|
@@ -105,6 +149,10 @@
|
|
|
105
149
|
const CLEAR = Object.freeze({
|
|
106
150
|
access_requests: 'Everyone is in', goal_join_requests: 'No one is asking', override_requests: 'No overrides asked',
|
|
107
151
|
gate_approvals: 'Everything ready is live', blockers: 'Nothing blocked on you', board: 'Every vote is in', idea: 'No ideas waiting',
|
|
152
|
+
// every other floor that holds matters says its own line too (task 1004562)
|
|
153
|
+
redteam_reports: 'No reports open', removal_proposals: 'No removals asked', grade_appeals: 'No appeals open',
|
|
154
|
+
module_rechecks: 'Every module checked', criteria_uat: 'Nothing waits for a tester', core_update: 'Core is up to date',
|
|
155
|
+
autobongos_fence: 'The switch is set', standing_rules: 'No rules proposed', treasury_spending: 'No spending asked',
|
|
108
156
|
});
|
|
109
157
|
// The card's acts under the desk's words (the board's button labels).
|
|
110
158
|
const ACT_WORDS = Object.freeze({
|
|
@@ -246,7 +294,12 @@
|
|
|
246
294
|
if (card && card.subject) card.subject.council = councilOf(ctx.docket, card.building) ? card.building : null;
|
|
247
295
|
const actions = [];
|
|
248
296
|
const choices = {};
|
|
297
|
+
// 'open' leaves for an older page: a row that can act here keeps its detail
|
|
298
|
+
// in place (tap the name) and drops the link; only a row with no act of its
|
|
299
|
+
// own may link out (owner standard, task 1004562)
|
|
300
|
+
const inPlace = acts.some((a) => a && a.key !== 'open' && a.key !== 'record');
|
|
249
301
|
acts.forEach((a) => {
|
|
302
|
+
if (a.key === 'open' && inPlace) return;
|
|
250
303
|
// the core's 'take' is its own word ('take it'), not the rank's 'take back'
|
|
251
304
|
const word = a.key === 'take' && card.subject && card.subject.kind === 'core_pin' ? 'take_core' : a.key;
|
|
252
305
|
if (actions.length >= LIMITS.actions || !ACT_WORDS[word]) return;
|
|
@@ -311,7 +364,18 @@
|
|
|
311
364
|
// a desk row that stands in for a source (the core update's check-then-take)
|
|
312
365
|
// replaces that source's matter rows on this floor
|
|
313
366
|
const over = desked.map((r) => r.over).filter(Boolean);
|
|
314
|
-
|
|
367
|
+
const here = floorMatters(key, floor, docket).filter((m) => !done[str(m.id)]);
|
|
368
|
+
// the stand-in keeps what it stands for: a matter that needs you still
|
|
369
|
+
// needs you on the desk (orange status, its age), as on the floor's sign
|
|
370
|
+
// and the street's count (task 1004562)
|
|
371
|
+
desked.forEach((r) => {
|
|
372
|
+
if (!r.over) return;
|
|
373
|
+
const stood = here.filter((m) => m.source === r.over && stateOf(m) === 'needs');
|
|
374
|
+
if (!stood.length) return;
|
|
375
|
+
r.needs = true;
|
|
376
|
+
if (!r.opened) r.opened = stood.reduce((a, m) => (m.opened_at && (!a || Date.parse(m.opened_at) < Date.parse(a)) ? m.opened_at : a), null);
|
|
377
|
+
});
|
|
378
|
+
return here.filter((m) => over.indexOf(m.source) < 0).map((m) => rowOf(m, c))
|
|
315
379
|
.concat(desked)
|
|
316
380
|
.sort((a, b) => (b.needs - a.needs) || ((Date.parse(a.opened || 0) || 0) - (Date.parse(b.opened || 0) || 0)));
|
|
317
381
|
}
|
|
@@ -400,9 +464,11 @@
|
|
|
400
464
|
const quiet = rows.length === 0;
|
|
401
465
|
let status, hot = false;
|
|
402
466
|
if (!docket) status = 'the docket could not be read';
|
|
403
|
-
else if (needs.length) { hot = true; const age = ageWord(oldestOf(needs), o.now); status = needs.length + ' ' + noun + (age ? ' · oldest ' + age : ''); }
|
|
404
|
-
else if (counted.length) { const age = ageWord(oldestOf(counted), o.now); status = counted.length + ' ' + moving + (age ? ' · oldest ' + age : ''); }
|
|
405
|
-
|
|
467
|
+
else if (needs.length) { hot = true; const age = ageWord(oldestOf(needs), o.now); status = needs.length + ' ' + agree(needs.length, noun) + (age ? ' · oldest ' + age : ''); }
|
|
468
|
+
else if (counted.length) { const age = ageWord(oldestOf(counted), o.now); status = counted.length + ' ' + agree(counted.length, moving) + (age ? ' · oldest ' + age : ''); }
|
|
469
|
+
// only tools left (an invite, a setting): the floor's line, unless that line
|
|
470
|
+
// still counts what you just did ('1 needs you' after you let them in)
|
|
471
|
+
else if (rows.length) status = /needs? you/.test(str(fl.line)) ? 'nothing needs you' : fl.line;
|
|
406
472
|
else if (cleared) status = 'floor clear';
|
|
407
473
|
else status = 'nothing needs you · ' + fl.line;
|
|
408
474
|
const history = docket && Array.isArray(docket.history) ? docket.history.filter((h) => h && h.building === o.key) : [];
|
|
@@ -419,13 +485,16 @@
|
|
|
419
485
|
// the order's one number: the share of its goals' promises kept
|
|
420
486
|
const on = ordered.reduce((a, r) => a + (r.pips || []).filter((p) => p.on).length, 0);
|
|
421
487
|
const of = ordered.reduce((a, r) => a + (r.pips || []).length, 0);
|
|
422
|
-
tile(needs.length, noun);
|
|
488
|
+
tile(needs.length, agree(needs.length, noun));
|
|
423
489
|
if (of) tile(Math.round((100 * on) / of) + '%', 'promises kept');
|
|
424
|
-
} else if (rows.length) { tile(needs.length, noun); tile(counted.length - needs.length, moving); }
|
|
425
|
-
|
|
490
|
+
} else if (rows.length) { tile(needs.length, agree(needs.length, noun)); tile(counted.length - needs.length, agree(counted.length - needs.length, moving)); }
|
|
491
|
+
// a quiet floor's own number; never on a floor you just cleared, and never a
|
|
492
|
+
// 'needs you' count on a floor where nothing does (task 1004562)
|
|
493
|
+
else if (fl.count && !cleared && !/needs? you/.test(str(fl.line))) tile(fl.count, floorLabel(fl.count, fl.line, spec[0]));
|
|
426
494
|
tile(history.length, 'on record');
|
|
427
|
-
|
|
428
|
-
|
|
495
|
+
// what you did keeps its ' · now'; a closed item keeps its age (task 1004562)
|
|
496
|
+
const record = (o.mine || []).map((t, i) => ({ key: 'm' + i, text: fit(t, LIMITS.recordLen - 6) + ' · now', mine: true }))
|
|
497
|
+
.concat(history.map((h, i) => ({ key: 'h' + i, text: recordText(h, o.now), mine: false })))
|
|
429
498
|
.slice(0, LIMITS.record);
|
|
430
499
|
return {
|
|
431
500
|
key: o.key + ':' + floor, building: o.key, floor: floor, name: spec[0], sym: o.sym || { d: '', f: '' },
|
|
@@ -344,7 +344,10 @@ html[data-fullscreen="on"] .street-top { padding-right: 184px; }
|
|
|
344
344
|
.desk-quiet--cleared .desk-quiet__tick { stroke: var(--street-accent-text); }
|
|
345
345
|
.desk-quiet__line { font-size: 20px; font-weight: 300; }
|
|
346
346
|
.desk-quiet__sub { font-size: 13.5px; color: var(--ink-faint); }
|
|
347
|
-
|
|
347
|
+
/* the record is ONE line, as on the board: items that do not fit wrap onto a
|
|
348
|
+
second line that is clipped away, so the newest stay and none is cut in half
|
|
349
|
+
(task 1004562) */
|
|
350
|
+
.desk-record { display: flex; align-items: baseline; flex-wrap: wrap; gap: 0 22px; border-top: 1px solid var(--street-rule); margin-top: 18px; padding-top: 14px; box-sizing: border-box; line-height: 20px; max-height: 35px; overflow: hidden; }
|
|
348
351
|
.desk-record__label { font-size: 11.5px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink-faint); }
|
|
349
352
|
.desk-record__item { font-size: 13px; color: var(--ink-faint); white-space: nowrap; }
|
|
350
353
|
.desk-record__item--mine { color: var(--ink); }
|
|
@@ -707,11 +707,11 @@
|
|
|
707
707
|
draw();
|
|
708
708
|
return send(req).then(function (out) {
|
|
709
709
|
desk.busy = false;
|
|
710
|
-
if (!out.ok) { say(C.refusal(out.status, out.body)); draw(); return false; }
|
|
710
|
+
if (!out.ok) { say(C.refusal(out.status, out.body), true); draw(); return false; }
|
|
711
711
|
desk.order[k] = null;
|
|
712
712
|
desk.mine[k] = [dm.save.council ? 'order sent to council' : 'order set'].concat(desk.mine[k] || []);
|
|
713
713
|
bumpToday();
|
|
714
|
-
say(dm.save.said);
|
|
714
|
+
say(dm.save.said, true);
|
|
715
715
|
draw();
|
|
716
716
|
return load().then(function () { return true; });
|
|
717
717
|
});
|
|
@@ -760,7 +760,7 @@
|
|
|
760
760
|
var C = window.GovMatterCard, st = view.inside;
|
|
761
761
|
if (!C || !st || !r.card || desk.busy) return Promise.resolve(false);
|
|
762
762
|
var refused = C.check(r.card, key, input);
|
|
763
|
-
if (refused) { say(refused); return Promise.resolve(false); }
|
|
763
|
+
if (refused) { say(refused, true); return Promise.resolve(false); }
|
|
764
764
|
var k = st.key + ':' + (view.insideModel ? view.insideModel.selFloor : 0);
|
|
765
765
|
var req = C.request(r.card, key, input);
|
|
766
766
|
var settle = function (body) {
|
|
@@ -774,14 +774,14 @@
|
|
|
774
774
|
if (st.floor == null && view.insideModel) st.floor = view.insideModel.selFloor;
|
|
775
775
|
bumpToday();
|
|
776
776
|
if (a.building && a.kind) card.answer = { building: a.building, matter: r.matter, kind: a.kind, at: Date.now() };
|
|
777
|
-
say(a.toast || 'Done.');
|
|
777
|
+
say(a.toast || 'Done.', true);
|
|
778
778
|
};
|
|
779
779
|
if (!req) return new Promise(function (res) { clearRow(r.key, function () { settle({}); draw(); res(true); }); });
|
|
780
780
|
desk.busy = true;
|
|
781
781
|
draw();
|
|
782
782
|
return send(req).then(function (out) {
|
|
783
783
|
desk.busy = false;
|
|
784
|
-
if (!out.ok) { say(C.refusal(out.status, out.body)); draw(); return false; }
|
|
784
|
+
if (!out.ok) { say(C.refusal(out.status, out.body), true); draw(); return false; }
|
|
785
785
|
return new Promise(function (res) {
|
|
786
786
|
clearRow(r.key, function () {
|
|
787
787
|
settle(out.body);
|
|
@@ -1230,9 +1230,12 @@
|
|
|
1230
1230
|
for (var j = 0; j < pins.length; j++) if (String(pins[j].getAttribute('data-matters') || pins[j].getAttribute('data-matter')).split(' ').indexOf(a.matter) >= 0) pins[j].className += ' street-pin--answer';
|
|
1231
1231
|
}
|
|
1232
1232
|
var toastTimer = null;
|
|
1233
|
-
|
|
1233
|
+
// desk: the Desks board's toast after an act on a floor (an ink pill with the
|
|
1234
|
+
// orange dot, low over the desk); the street's own toast otherwise (task 1004562)
|
|
1235
|
+
function say(text, desk) {
|
|
1234
1236
|
var t = $('streetToast');
|
|
1235
1237
|
if (!t || !text) return;
|
|
1238
|
+
t.className = 'street-toast' + (desk ? ' street-toast--desk' : '');
|
|
1236
1239
|
t.textContent = text;
|
|
1237
1240
|
t.hidden = false;
|
|
1238
1241
|
if (typeof setTimeout !== 'function') return;
|
|
@@ -33,3 +33,17 @@ Files: `street-inside.js` (`window.GovInside`, node-require-safe, pure): the pro
|
|
|
33
33
|
- **Narrow.** Under 900 px the board becomes a column: the drawing in its own sideways strip (never scaled under 0.6, so a floor stays a 24 px target), then the panel.
|
|
34
34
|
|
|
35
35
|
Tests: `tests/hall_street_inside.mjs` runs the prototype's own `Component` beside `street-inside.js` on the prototype's sample floors and holds every building's section, panel bindings and every district equal to it in both modes, then the docket rules (no invented numbers) and the page in a vm (the persistent lift, the flip, the drawers, the amendment note, the card from a table row, back to the street, a district's tasks). The states file renders every building, a floor pick, the amendment note and a district.
|
|
36
|
+
|
|
37
|
+
## The floor desks, walked (task 1004562)
|
|
38
|
+
|
|
39
|
+
Every building and every floor desk was walked in light, dark and at phone width (375 px) against `Desks.dc.html` and the line rule of `Ground.dc.html`. The live site is behind its password gate, so the walk ran on the real pages in the hall preview harness with its fixtures. What it settled:
|
|
40
|
+
|
|
41
|
+
- **A quiet floor's tile says what it counts.** Two words that read as a label: the floor line's own noun when it follows the same number ("5 seats"), else the floor's name ("3 testers", "2 signed off"), else the line's last two words ("1 went live"). Never the first two words of a longer line ("3 Lu, Bo", "1 piece of"). `floorLabel()` in `street-desk.js`.
|
|
42
|
+
- **A cleared floor shows no count of the work just done.** The docket's floor count lags the act, so a cleared floor draws no floor-count tile, and a floor left with only a tool (the invite) does not repeat a stale "1 needs you" as its status. Floors that hold matters each say their own clear line ("No removals asked").
|
|
43
|
+
- **Counts agree with their nouns**: "2 open votes", "1 rank".
|
|
44
|
+
- **The record is one line.** Each item keeps its age in the board's short words ("· 3d", "· 2w"); the outcome shows only when the title fits beside it; what you did keeps its "· now". Items that do not fit wrap onto a clipped second line, so none is cut in half.
|
|
45
|
+
- **A stand-in row keeps the need it stands for.** The Release desk's core row replaces the core-update matter; it now carries that matter's need, so the status is orange where the floor sign and the street count already said "needs you".
|
|
46
|
+
- **No link out beside an act.** A row that can act in place drops the server's `open` act; its detail opens in place by its name. A row with no act of its own (a Full Idea that needs its page to finish, Work categories) still links, as the standard allows.
|
|
47
|
+
- **The desk's toast is the board's**: an ink pill with the orange dot, low over the desk. The street's own toast is centred again (its enter animation had undone the centring).
|
|
48
|
+
|
|
49
|
+
Left as found, on purpose: the six readings stay under a building (task 1004526 marks the building's own reading), the old month drawers stay under the desk, and the recording sign-off still goes to its page (an open owner choice).
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/provisioning/manifest-truth.js — what the wizard is told about the GitHub
|
|
4
|
+
// sign-in leg (the GitHub-App-Manifest handshake, ADR 0133), at every stage.
|
|
5
|
+
//
|
|
6
|
+
// WHY (task 1004572, live on 2026-10-04). A founder setting up a project saw
|
|
7
|
+
// "Waiting for your approval on GitHub…" while the platform was already placing
|
|
8
|
+
// credentials, then "GitHub setup didn't start. Nothing was changed on GitHub" after
|
|
9
|
+
// GitHub HAD created an app on their account, and — once a setup link lapsed — no
|
|
10
|
+
// button at all, forever, because nothing ever flips a lapsed 'pending' row.
|
|
11
|
+
// These helpers make the read surface say what is actually true. PURE: no DB, no
|
|
12
|
+
// network; split out because provisioning.js sits at the 1500-line ratchet.
|
|
13
|
+
|
|
14
|
+
// The status a manifest row MEANS right now. Only 'pending' decays: past expires_at
|
|
15
|
+
// the callback refuses its code (attachManifestCode's TTL guard in SQL), so a lapsed
|
|
16
|
+
// 'pending' row is expired in every sense but the column.
|
|
17
|
+
function effectiveManifestStatus(row, now = Date.now()) {
|
|
18
|
+
const status = String((row && row.status) || '');
|
|
19
|
+
if (status === 'pending' && row.expires_at && new Date(row.expires_at).getTime() <= now) return 'expired';
|
|
20
|
+
return status;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// How far a FAILED attempt got, as a closed vocabulary the wizard turns into a
|
|
24
|
+
// sentence. The raw last_error never leaves the server (the status route is public
|
|
25
|
+
// by state token). Read from the row's shape, never from error-message text:
|
|
26
|
+
// 'not_approved' — the runner never picked it up (attempts 0): nothing we
|
|
27
|
+
// know of happened on GitHub's side.
|
|
28
|
+
// 'sign_in_unconfirmed' — GitHub handed over the credentials (the App's slug is
|
|
29
|
+
// recorded) and they were placed, but the project never
|
|
30
|
+
// confirmed sign-in works. An App exists on the founder's
|
|
31
|
+
// account, so "try again" makes a second one.
|
|
32
|
+
// 'finish_failed' — GitHub approved, but turning that into credentials on
|
|
33
|
+
// the project failed before the App was recorded.
|
|
34
|
+
const MANIFEST_FAILURES = Object.freeze(['not_approved', 'sign_in_unconfirmed', 'finish_failed']);
|
|
35
|
+
|
|
36
|
+
function manifestFailure(row) {
|
|
37
|
+
if (!row || !(Number(row.attempts) > 0)) return 'not_approved';
|
|
38
|
+
if (row.app_slug) return 'sign_in_unconfirmed';
|
|
39
|
+
return 'finish_failed';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The GitHub-App manifest leg's truth for an instance, as the instance read carries
|
|
43
|
+
// it (task 1003235): the latest attempt's lifecycle status + the App's public slug,
|
|
44
|
+
// in the same vocabulary as GET /github-app/status ('pending' | 'code_received' |
|
|
45
|
+
// 'converting' | 'done' | 'error' | 'expired'), so the wizard can seed `live.manifest`
|
|
46
|
+
// after a reload, a second tab, or another device. null = no attempt ever made.
|
|
47
|
+
// status — the EFFECTIVE status (above).
|
|
48
|
+
// failure — on 'error' only, how far it got (above); null otherwise.
|
|
49
|
+
// started_at — when the setup link was minted, so a reloaded page can still offer
|
|
50
|
+
// "start again" once the wait has clearly gone wrong.
|
|
51
|
+
// An allow-list projection of the row, never the row itself (it holds GitHub's code).
|
|
52
|
+
function publicGithubApp(manifestRow, now = Date.now()) {
|
|
53
|
+
if (!manifestRow || !manifestRow.status) return null;
|
|
54
|
+
const status = effectiveManifestStatus(manifestRow, now);
|
|
55
|
+
return {
|
|
56
|
+
status,
|
|
57
|
+
app_slug: manifestRow.app_slug || null,
|
|
58
|
+
failure: status === 'error' ? manifestFailure(manifestRow) : null,
|
|
59
|
+
started_at: manifestRow.created_at ? new Date(manifestRow.created_at).toISOString() : null,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { effectiveManifestStatus, manifestFailure, publicGithubApp, MANIFEST_FAILURES };
|
|
@@ -477,17 +477,9 @@ function publicInstance(row) {
|
|
|
477
477
|
};
|
|
478
478
|
}
|
|
479
479
|
|
|
480
|
-
// The GitHub-App manifest leg's truth
|
|
481
|
-
// it
|
|
482
|
-
|
|
483
|
-
// 'converting' | 'done' | 'error' | 'expired'), so the wizard can seed its
|
|
484
|
-
// `live.manifest` from a plain instance read after a reload, a second tab, or
|
|
485
|
-
// another device — no sessionStorage bridge needed. null = no attempt ever made.
|
|
486
|
-
// PURE: an allow-list projection of the manifest row, never the row itself.
|
|
487
|
-
function publicGithubApp(manifestRow) {
|
|
488
|
-
if (!manifestRow || !manifestRow.status) return null;
|
|
489
|
-
return { status: String(manifestRow.status), app_slug: manifestRow.app_slug || null };
|
|
490
|
-
}
|
|
480
|
+
// The GitHub-App manifest leg's truth as the read surface carries it (task 1003235;
|
|
481
|
+
// task 1004572 made it tell the truth at every stage) — moved to manifest-truth.js.
|
|
482
|
+
const { publicGithubApp } = require('./manifest-truth');
|
|
491
483
|
|
|
492
484
|
// The env variables that carry this instance's settings, as the instance reads
|
|
493
485
|
// them: { <PREFIX>_PLATFORM_VISIBILITY, <PREFIX>_JOINABILITY } (task 1003139 /
|
|
@@ -1405,10 +1405,12 @@ module.exports = function provisioningRoutes() {
|
|
|
1405
1405
|
const row = await provisioning.getOAuthManifestByState(pool, state);
|
|
1406
1406
|
if (!row) return res.status(404).json({ ok: false, error: 'unknown_state' });
|
|
1407
1407
|
const inst = await provisioning.getInstanceById(pool, row.instance_id).catch(() => null);
|
|
1408
|
-
|
|
1408
|
+
// EFFECTIVE status + how far a failure got (task 1004572, manifest-truth.js)
|
|
1409
|
+
const { status, failure } = provisioning.publicGithubApp(row);
|
|
1409
1410
|
return res.json({
|
|
1410
1411
|
ok: true,
|
|
1411
1412
|
status,
|
|
1413
|
+
failure,
|
|
1412
1414
|
ready: status === 'done',
|
|
1413
1415
|
terminal: status === 'done' || status === 'error' || status === 'expired',
|
|
1414
1416
|
domain: inst && inst.domain ? inst.domain : null,
|
|
@@ -6533,7 +6533,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6533
6533
|
return '<a href="' + (instanceId ? '?view=manage&id=' + encodeURIComponent(instanceId) : '?view=mine') +
|
|
6534
6534
|
'">' + label + '</a>';
|
|
6535
6535
|
}
|
|
6536
|
-
var live = { instStatus: null, addrReady: false, manifest: null, manifestMsg: '', manifestCode: '', appSlug: null };
|
|
6536
|
+
var live = { instStatus: null, addrReady: false, manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null };
|
|
6537
6537
|
var planPromises = {}; /* mode|domain → Promise<steps[]> */
|
|
6538
6538
|
var lastStepsHtml = '';
|
|
6539
6539
|
var lastProgHtml = ''; /* same change-only contract, for the progress strip */
|
|
@@ -6654,14 +6654,24 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6654
6654
|
details (task 1003244). The one refusal a founder can act on — no
|
|
6655
6655
|
address yet — says where to go instead of what the server said. */
|
|
6656
6656
|
var snag, raw = '';
|
|
6657
|
+
/* a failure AFTER GitHub's approval left an app on the founder's account,
|
|
6658
|
+
so "nothing was changed" would be false — the server says how far it got
|
|
6659
|
+
(live.manifestFailure, task 1004572) and the words follow it */
|
|
6657
6660
|
if (live.manifest === 'expired') snag = 'That setup link expired (they last 30 minutes) — mint a fresh one.';
|
|
6658
6661
|
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.';
|
|
6662
|
+
else if (live.manifestFailure === 'sign_in_unconfirmed') {
|
|
6663
|
+
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';
|
|
6664
|
+
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>' +
|
|
6665
|
+
progressMarkup(1, 1, 'Stopped: your project didn’t confirm sign-in.', 'isErr', 'signin') +
|
|
6666
|
+
'<button type="button" class="linklike" data-action="manifest-start">Try again anyway</button></li>';
|
|
6667
|
+
}
|
|
6668
|
+
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.';
|
|
6659
6669
|
else { snag = 'GitHub setup didn’t start. Nothing was changed on GitHub — try again.'; raw = live.manifestMsg; }
|
|
6660
6670
|
return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>' + snag + '</p>' +
|
|
6661
6671
|
(raw ? '<details><summary>Technical details</summary><p>' + esc(raw) + '</p></details>' : '') +
|
|
6662
6672
|
'<button type="button" class="ghbtn" data-action="manifest-start">' + GH_ICON + 'Set up GitHub sign-in</button>' + manualOauthDetails() + '</li>';
|
|
6663
6673
|
}
|
|
6664
|
-
if (live.manifest === 'code_received') {
|
|
6674
|
+
if (live.manifest === 'code_received' || live.manifest === 'converting') {
|
|
6665
6675
|
/* GitHub's half is done — say so, rather than keep telling the founder
|
|
6666
6676
|
to approve an app they just approved */
|
|
6667
6677
|
autoOpenArmed = true;
|
|
@@ -6670,7 +6680,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6670
6680
|
}
|
|
6671
6681
|
if (live.manifest) { /* pending — GitHub holds the manifest, nobody has approved it yet */
|
|
6672
6682
|
autoOpenArmed = true;
|
|
6673
|
-
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. Once approved, the platform places the credentials and verifies sign-in on its own; this step ticks itself.</p>' +
|
|
6683
|
+
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>' +
|
|
6674
6684
|
progressMarkup(0, 0, 'Waiting for your approval on GitHub…', '', 'signin') + autoOpenTick() +
|
|
6675
6685
|
(manifestStale ? '<button type="button" class="linklike" data-action="manifest-start">Didn’t get to GitHub? Start the setup again</button>' : '') + '</li>';
|
|
6676
6686
|
}
|
|
@@ -6889,7 +6899,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6889
6899
|
inp.type = 'hidden'; inp.name = 'manifest'; inp.value = JSON.stringify(d.manifest);
|
|
6890
6900
|
form.appendChild(inp); document.body.appendChild(form);
|
|
6891
6901
|
form.submit(); form.remove();
|
|
6892
|
-
live.manifest = 'pending'; live.manifestMsg = '';
|
|
6902
|
+
live.manifest = 'pending'; live.manifestMsg = ''; live.manifestFailure = null;
|
|
6893
6903
|
manifestStartedAt = Date.now(); manifestStale = false;
|
|
6894
6904
|
rememberManifest(d.state, 'pending');
|
|
6895
6905
|
renderSteps();
|
|
@@ -6897,12 +6907,21 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6897
6907
|
}).catch(function (err) {
|
|
6898
6908
|
if (String(err && err.message) === 'signing in') return;
|
|
6899
6909
|
if (tab) { try { tab.close(); } catch (e) {} }
|
|
6900
|
-
live.manifest = 'error'; live.manifestMsg = String((err && err.message) || 'could not start');
|
|
6910
|
+
live.manifest = 'error'; live.manifestFailure = null; live.manifestMsg = String((err && err.message) || 'could not start');
|
|
6901
6911
|
live.manifestCode = String((err && err.code) || '');
|
|
6902
6912
|
renderSteps();
|
|
6903
6913
|
});
|
|
6904
6914
|
}
|
|
6905
6915
|
|
|
6916
|
+
/* the "start again" offer once a wait has clearly gone wrong — measured from
|
|
6917
|
+
when the link was MINTED (the server's started_at after a reload), so a
|
|
6918
|
+
reloaded page offers it too instead of never (task 1004572) */
|
|
6919
|
+
function manifestInFlight() {
|
|
6920
|
+
return live.manifest === 'pending' || live.manifest === 'code_received' || live.manifest === 'converting';
|
|
6921
|
+
}
|
|
6922
|
+
function noteManifestWait() {
|
|
6923
|
+
if (live.manifest === 'pending' && !manifestStale && manifestStartedAt && (Date.now() - manifestStartedAt) > 90000) { manifestStale = true; renderSteps(); }
|
|
6924
|
+
}
|
|
6906
6925
|
function pollManifest(stateToken) {
|
|
6907
6926
|
stopManifestPoll();
|
|
6908
6927
|
manifestState = stateToken;
|
|
@@ -6916,8 +6935,9 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6916
6935
|
if (myGen !== manifestGen) return;
|
|
6917
6936
|
if (d.app_slug && d.app_slug !== live.appSlug) { live.appSlug = d.app_slug; rememberManifest(undefined, undefined, d.app_slug); renderSteps(); }
|
|
6918
6937
|
var next = d.ready ? 'done' : (d.status || 'pending');
|
|
6919
|
-
|
|
6920
|
-
|
|
6938
|
+
var failure = d.failure || null;
|
|
6939
|
+
if (next !== live.manifest || failure !== live.manifestFailure) { live.manifest = next; live.manifestFailure = failure; rememberManifest(undefined, next); renderSteps(); }
|
|
6940
|
+
else noteManifestWait();
|
|
6921
6941
|
if (!d.terminal) manifestPollTimer = setTimeout(tick, 4000);
|
|
6922
6942
|
else manifestPollTimer = null; /* a stale fired id would read as "still polling" and resume a finished poll */
|
|
6923
6943
|
})
|
|
@@ -7119,10 +7139,15 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7119
7139
|
var ga = data.github_app;
|
|
7120
7140
|
if (ga && ga.status && !manifestPollTimer) {
|
|
7121
7141
|
var seeded = ga.status === 'done' ? 'done' : ga.status;
|
|
7122
|
-
var
|
|
7142
|
+
var seedFailure = ga.failure || null;
|
|
7143
|
+
var seedChanged = seeded !== live.manifest || seedFailure !== live.manifestFailure || (!!ga.app_slug && ga.app_slug !== live.appSlug);
|
|
7123
7144
|
if (ga.app_slug) live.appSlug = ga.app_slug;
|
|
7124
|
-
live.manifest = seeded;
|
|
7145
|
+
live.manifest = seeded; live.manifestFailure = seedFailure;
|
|
7146
|
+
/* the wait is timed from when the link was MINTED, so a reload still
|
|
7147
|
+
reaches the "start again" offer (task 1004572) */
|
|
7148
|
+
if (!manifestStartedAt && ga.started_at) manifestStartedAt = Date.parse(ga.started_at) || 0;
|
|
7125
7149
|
if (seedChanged) { rememberManifest(undefined, live.manifest, live.appSlug || undefined); renderSteps(); }
|
|
7150
|
+
else noteManifestWait();
|
|
7126
7151
|
}
|
|
7127
7152
|
/* the checklist reflects live state (task 1002694): a status transition —
|
|
7128
7153
|
e.g. the scaffold landing — re-renders the steps (change-only, so the
|
|
@@ -7131,7 +7156,9 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7131
7156
|
live.instStatus = inst.status; live.addrReady = addressReady(inst); renderSteps();
|
|
7132
7157
|
}
|
|
7133
7158
|
var terminal = renderProgress(inst);
|
|
7134
|
-
|
|
7159
|
+
/* a GitHub leg still in flight keeps this read alive even once the
|
|
7160
|
+
project is up — after a reload it is the ONLY thing watching it */
|
|
7161
|
+
if (!terminal || manifestInFlight()) pollTimer = setTimeout(tick, 5000);
|
|
7135
7162
|
else stopPolling();
|
|
7136
7163
|
})
|
|
7137
7164
|
.catch(function (err) {
|
|
@@ -7160,7 +7187,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7160
7187
|
instanceId = null;
|
|
7161
7188
|
watchPending = false; manifestPending = false; manifestState = null;
|
|
7162
7189
|
manifestStartedAt = 0; manifestStale = false;
|
|
7163
|
-
live = { instStatus: null, addrReady: false, manifest: null, manifestMsg: '', manifestCode: '', appSlug: null };
|
|
7190
|
+
live = { instStatus: null, addrReady: false, manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null };
|
|
7164
7191
|
lastStepsHtml = '';
|
|
7165
7192
|
lastProgHtml = '';
|
|
7166
7193
|
try { sessionStorage.removeItem(DRAFT_KEY); } catch (e) {}
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.47",
|
|
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.47",
|
|
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.47",
|
|
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
|
@@ -9034,5 +9034,17 @@
|
|
|
9034
9034
|
"id": "1004497",
|
|
9035
9035
|
"text": "Artists can now edit any hall page's words right where they stand. In Artistry mode, \"Edit words\" offers to take the page; the words get an underline and can be typed over, a review lists each change as was/now, and \"Send for"
|
|
9036
9036
|
}
|
|
9037
|
+
],
|
|
9038
|
+
"1.21.46": [
|
|
9039
|
+
{
|
|
9040
|
+
"id": "1004572",
|
|
9041
|
+
"text": "Setting up GitHub sign-in for a new project now says what is really happening at each step, and always leaves a way forward if it gets stuck."
|
|
9042
|
+
}
|
|
9043
|
+
],
|
|
9044
|
+
"1.21.47": [
|
|
9045
|
+
{
|
|
9046
|
+
"id": "1004562",
|
|
9047
|
+
"text": "Walked every building and floor desk of the Governor City in light, dark and on a phone, and fixed what did not match the design: number tiles now say what they count, a cleared floor no longer shows a stale count, counts read"
|
|
9048
|
+
}
|
|
9037
9049
|
]
|
|
9038
9050
|
}
|
package/scripts/gds/provision.js
CHANGED
|
@@ -1095,7 +1095,9 @@ async function placeManifestCreds(inst, mrow, deps) {
|
|
|
1095
1095
|
await sleep(MANIFEST_VERIFY_INTERVAL_MS);
|
|
1096
1096
|
}
|
|
1097
1097
|
if (!authConfigured) {
|
|
1098
|
-
|
|
1098
|
+
// The App now EXISTS on the owner's account — keep its id + slug on the row so
|
|
1099
|
+
// the wizard can name it and say why "try again" makes a second one (task 1004572).
|
|
1100
|
+
return { ok: false, codeSpent: true, appId: creds.appId, appSlug: creds.appSlug, error: 'the credentials are placed, but the project did not report sign-in ready in time' };
|
|
1099
1101
|
}
|
|
1100
1102
|
log(' [oauth-manifest] ✓ /healthz auth_configured=true — sign-in is live');
|
|
1101
1103
|
} else {
|
|
@@ -1147,7 +1149,7 @@ async function drainOAuthManifests(deps) {
|
|
|
1147
1149
|
// single-use, so resetting the row to 'code_received' guarantees the
|
|
1148
1150
|
// next tick re-converts a burned code and 404s forever. Only failures
|
|
1149
1151
|
// that happened BEFORE the conversion stay retryable.
|
|
1150
|
-
if (result.codeSpent) await provisioning.resolveOAuthManifest(db, mrow.id, 'error', { last_error: result.error, github_code: null });
|
|
1152
|
+
if (result.codeSpent) await provisioning.resolveOAuthManifest(db, mrow.id, 'error', { last_error: result.error, github_code: null, ...(result.appSlug ? { app_id: result.appId, app_slug: result.appSlug } : {}) });
|
|
1151
1153
|
else if (mrow.attempts >= MAX_INTENT_ATTEMPTS) await provisioning.resolveOAuthManifest(db, mrow.id, 'error', { last_error: result.error });
|
|
1152
1154
|
else await provisioning.resolveOAuthManifest(db, mrow.id, 'code_received', { last_error: result.error });
|
|
1153
1155
|
errors++; log(` ✖ oauth-manifest #${mrow.id}: ${result.error}`);
|
|
@@ -127,6 +127,19 @@ function renderAuthErrorPage(heading, detail, opts = {}) {
|
|
|
127
127
|
</body></html>`;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
// The project has no way to sign anyone in yet: neither its own GitHub app nor
|
|
131
|
+
// sign-in through the platform (task 1004572). It used to answer a bare 503
|
|
132
|
+
// "auth not configured", which told a founder mid-setup nothing they could act on.
|
|
133
|
+
// Still a 503 — the service genuinely is not available — but in words, and
|
|
134
|
+
// pointing the one person who can fix it at the step that does.
|
|
135
|
+
const AUTH_NOT_CONFIGURED_DETAIL = "Sign-in isn't set up on this project yet. If it's your project, go back to its setup checklist on the platform where you created it and finish the \"Wire GitHub sign-in\" step. Otherwise, ask the project's owner.";
|
|
136
|
+
function sendAuthNotConfigured(res) {
|
|
137
|
+
return res.status(503).type('text/html').send(renderAuthErrorPage(
|
|
138
|
+
"Sign-in isn't set up yet", AUTH_NOT_CONFIGURED_DETAIL,
|
|
139
|
+
{ subtitle: "This project can't sign anyone in yet.", cta: { href: '/', label: 'Back to the project' }, note: 'Nothing is wrong with your account.' },
|
|
140
|
+
));
|
|
141
|
+
}
|
|
142
|
+
|
|
130
143
|
// renderAccessPendingPage — the calm "hang tight" page shown instead of the red
|
|
131
144
|
// dead-end when admission is plausibly imminent (task 1002097): the configured
|
|
132
145
|
// firstAdmin during the bootstrap window, or a login with a pending request.
|
|
@@ -681,7 +694,7 @@ module.exports = function buildAuthRouter() {
|
|
|
681
694
|
// Either direct-GitHub creds OR platform-IdP federation must be configured
|
|
682
695
|
// (ADR 0141 §1) — a federated instance has no GitHub app of its own.
|
|
683
696
|
if (!auth.authConfigured() && !auth.idpConfigured()) {
|
|
684
|
-
return res
|
|
697
|
+
return sendAuthNotConfigured(res);
|
|
685
698
|
}
|
|
686
699
|
const wantsRepos = req.query.repos === '1' || req.query.repos === 'true';
|
|
687
700
|
// A sign-in begun on a satellite hub host (builders./status.) restarts on
|
|
@@ -842,7 +855,7 @@ module.exports = function buildAuthRouter() {
|
|
|
842
855
|
// rank: public — OAuth callback; the state cookie + code are the bearer.
|
|
843
856
|
router.get('/auth/web/callback', async (req, res) => {
|
|
844
857
|
if (!auth.authConfigured() && !auth.idpConfigured()) {
|
|
845
|
-
return res
|
|
858
|
+
return sendAuthNotConfigured(res);
|
|
846
859
|
}
|
|
847
860
|
const expectedState = parseCookie(req, STATE_COOKIE) || parseCookie(req, STATE_COOKIE_LEGACY);
|
|
848
861
|
const { state, code, error } = req.query;
|
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.47'; // 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');
|