@bongos/core 1.24.91 → 1.24.92
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 +39 -34
- package/.claude/skills/builder-reauth/SKILL.md +23 -25
- package/clients/bongos-client/index.d.ts +2 -1
- package/docs/adr/0396-google-sign-in-a-stand-in-login-and-a-browser-approved-cli-sign-in.md +8 -0
- package/docs/api/openapi.json +22 -2
- package/docs/api-reference.md +1 -1
- package/docs/copy-inventory.md +2 -2
- package/docs/copy-registry.json +2 -2
- package/docs/module-api-changelog.md +2 -2
- package/docs/page-readings.json +6 -6
- package/modules/hall-ui/public/city.css +6 -2
- package/modules/hall-ui/public/city.js +44 -2
- package/modules/hall-ui/public/visor.js +18 -3
- package/modules/hall-ui/records/visor.md +7 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +9 -1
- package/scripts/bongos/cli-lib.js +7 -6
- package/scripts/bongos/login.js +14 -3
- package/scripts/bongos/plain-cards.js +1 -1
- package/scripts/bongos/reauth.js +100 -50
- package/src/bongos/routes/auth-google.js +5 -3
- package/src/bongos/routes/auth.js +3 -3
- package/src/bongos/sign-in-pages.js +1 -2
- package/src/module-api.js +1 -1
- package/tests/cli_reauth_browser.mjs +157 -0
- package/tests/hall_city.mjs +41 -1
- package/tests/hall_city_living.mjs +2 -0
- package/tests/hall_visor.mjs +27 -1
|
@@ -411,6 +411,7 @@
|
|
|
411
411
|
start.setAttribute('href', model.cta.href);
|
|
412
412
|
start.setAttribute('aria-label', model.cta.aria);
|
|
413
413
|
}
|
|
414
|
+
if (model.cta) registerScene(model.cta);
|
|
414
415
|
drawPlaque(model.plaque || null);
|
|
415
416
|
if (openKey) renderCard(openKey);
|
|
416
417
|
return model;
|
|
@@ -759,6 +760,47 @@
|
|
|
759
760
|
window.GovCityPage.ready = boot();
|
|
760
761
|
// task 1005148: the Governor City as the visor's scene. No camera until R13, so no movement
|
|
761
762
|
// actions (ADR 0399 §2.2); the card's own keys are listed for the sheet.
|
|
762
|
-
|
|
763
|
-
|
|
763
|
+
// task 1005209 (ADR 0403): in game mode the city's door and its two archived links are the
|
|
764
|
+
// visor's. The door is the lead action, on the glass as a key hint with D; the archived
|
|
765
|
+
// links are in the sheet's "This place" list. city.css takes them off the scene under
|
|
766
|
+
// html.visor-on; in classic mode they stay where they were and these do nothing new.
|
|
767
|
+
function doorLabel(cta) { return cta && cta.label ? String(cta.label) : 'start governing'; }
|
|
768
|
+
function sceneFor(cta) {
|
|
769
|
+
var label = doorLabel(cta);
|
|
770
|
+
return {
|
|
771
|
+
name: 'city', place: 'Governor city',
|
|
772
|
+
actions: [
|
|
773
|
+
{ label: label.charAt(0).toUpperCase() + label.slice(1), key: 'd', bind: true, lead: true, run: function () {
|
|
774
|
+
if (window.GovCityDecide && window.GovDeck) openDecide(null);
|
|
775
|
+
else if (window.location) window.location.hash = '#decide';
|
|
776
|
+
} },
|
|
777
|
+
].concat(archivedActions()),
|
|
778
|
+
keys: [{ label: 'On a card: answer', key: '1–9' }, { label: 'On a card: next', key: '← →' }],
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
// The archived links, read off the page's own line (city.html), so the sheet says and opens
|
|
782
|
+
// exactly what the line does: "Archived: Street", "Archived: Building Level Views".
|
|
783
|
+
function archivedActions() {
|
|
784
|
+
var line = document.querySelector ? document.querySelector('.city-archived') : null;
|
|
785
|
+
var links = line && line.querySelectorAll ? Array.prototype.slice.call(line.querySelectorAll('a[href]')) : [];
|
|
786
|
+
return links.map(function (a) {
|
|
787
|
+
var href = a.getAttribute('href');
|
|
788
|
+
return { label: 'Archived: ' + a.textContent, run: function () { window.location.assign(href); } };
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
// Registered once per door label: draw() calls this on every poll, and only a new label
|
|
792
|
+
// (a different streak, say) rebuilds the scene and reads the archived links again.
|
|
793
|
+
// Order matters here: boot() ran above and its first draw() has normally registered
|
|
794
|
+
// already, with the real door label. sceneLabel deliberately has no initial value, so the
|
|
795
|
+
// line after this function only registers the fallback when that draw did not register.
|
|
796
|
+
var sceneLabel;
|
|
797
|
+
function registerScene(cta) {
|
|
798
|
+
var label = doorLabel(cta);
|
|
799
|
+
if (label === sceneLabel) return;
|
|
800
|
+
sceneLabel = label;
|
|
801
|
+
var next = sceneFor(cta);
|
|
802
|
+
window.__visorScene = next;
|
|
803
|
+
if (window.OTBVisor) window.OTBVisor.scene(next);
|
|
804
|
+
}
|
|
805
|
+
if (sceneLabel == null) registerScene(null);
|
|
764
806
|
}());
|
|
@@ -133,12 +133,18 @@
|
|
|
133
133
|
var scene = null;
|
|
134
134
|
// A scene says what it is and, if it has a camera, the actions that move it (ADR 0399
|
|
135
135
|
// §2.2). `keys` are lines for the sheet's "this place" list: keys the scene already binds.
|
|
136
|
+
// An action's `key` is a key the scene already binds, shown beside it; with `bind: true`
|
|
137
|
+
// the visor binds it instead (in game mode, after the character's keys), for a scene button
|
|
138
|
+
// that had no key of its own. `lead: true` puts the action on the glass as the first key
|
|
139
|
+
// hint (ADR 0403: in game mode the scene's buttons are the visor's; task 1005209).
|
|
136
140
|
function registerScene(s) {
|
|
137
141
|
if (!s || typeof s !== 'object') return;
|
|
138
142
|
scene = {
|
|
139
143
|
name: String(s.name || ''),
|
|
140
144
|
place: String(s.place || ''),
|
|
141
|
-
actions: (Array.isArray(s.actions) ? s.actions : []).filter(function (a) { return a && a.label && typeof a.run === 'function'; })
|
|
145
|
+
actions: (Array.isArray(s.actions) ? s.actions : []).filter(function (a) { return a && a.label && typeof a.run === 'function'; }).map(function (a) {
|
|
146
|
+
return { label: String(a.label), key: a.key ? String(a.key).toLowerCase() : '', bind: !!a.bind, lead: !!a.lead, run: a.run };
|
|
147
|
+
}),
|
|
142
148
|
keys: (Array.isArray(s.keys) ? s.keys : []).filter(function (k) { return k && k.label && k.key; }),
|
|
143
149
|
};
|
|
144
150
|
render();
|
|
@@ -174,6 +180,10 @@
|
|
|
174
180
|
function hintHtml(a) {
|
|
175
181
|
return '<a class="visor__hint" data-visor-act="' + esc(a.id) + '" ' + actionAttrs(a) + '>' + keyHtml(a.key) + '<span>' + esc(a.label) + '</span></a>';
|
|
176
182
|
}
|
|
183
|
+
// The scene's lead action, as the first quiet hint (task 1005209).
|
|
184
|
+
function placeHintHtml(a, i) {
|
|
185
|
+
return '<button class="visor__hint visor__hint--place" type="button" data-visor-scene="' + i + '">' + (a.key ? keyHtml(a.key) : '') + '<span>' + esc(a.label) + '</span></button>';
|
|
186
|
+
}
|
|
177
187
|
function actionAttrs(a) {
|
|
178
188
|
return a.href ? 'href="' + esc(hallHref(a.href)) + '"' : 'href="#" role="button"';
|
|
179
189
|
}
|
|
@@ -192,7 +202,7 @@
|
|
|
192
202
|
'</div>' +
|
|
193
203
|
'<a class="visor__main" data-visor-act="' + esc(main.id) + '" ' + actionAttrs(main) + '>' +
|
|
194
204
|
'<span>' + esc(main.label) + '</span>' + keyHtml(main.key) + '</a>' +
|
|
195
|
-
'<div class="visor__hints">' + acts.slice(1).map(hintHtml).join('') +
|
|
205
|
+
'<div class="visor__hints">' + scene.actions.map(function (a, i) { return a.lead ? placeHintHtml(a, i) : ''; }).join('') + acts.slice(1).map(hintHtml).join('') +
|
|
196
206
|
'<button class="visor__hint visor__hint--sheet" type="button" data-visor-sheet aria-expanded="false" aria-controls="visor-sheet">' +
|
|
197
207
|
'<span class="visor__dots" aria-hidden="true"></span>' + keyHtml(SHEET_KEY) + '<span>Actions</span></button>' +
|
|
198
208
|
'<a class="visor__hint visor__hint--options" href="' + esc(hallHref('/settings')) + '">' + keyHtml('Esc') + '<span>Options</span></a>' +
|
|
@@ -297,6 +307,7 @@
|
|
|
297
307
|
// ---------- running an action ----------
|
|
298
308
|
function run(a) {
|
|
299
309
|
if (!a) return;
|
|
310
|
+
if (typeof a.run === 'function') { a.run(); return; }
|
|
300
311
|
if (a.tool) {
|
|
301
312
|
var C = craft();
|
|
302
313
|
if (C && typeof C.openTool === 'function') C.openTool(a.tool);
|
|
@@ -342,11 +353,15 @@
|
|
|
342
353
|
if (k === SHEET_KEY) return 'sheet';
|
|
343
354
|
var acts = actionsFor(id);
|
|
344
355
|
for (var i = 0; i < acts.length; i++) if (acts[i].key === k) return acts[i];
|
|
356
|
+
// then the keys the visor binds for the place (task 1005209); the character's win a clash
|
|
357
|
+
var own = scene ? scene.actions : [];
|
|
358
|
+
for (var j = 0; j < own.length; j++) if (own[j].bind && own[j].key === k) return own[j];
|
|
345
359
|
return null;
|
|
346
360
|
}
|
|
347
361
|
// Bubble, on window: a scene or pop-up that used the key has called preventDefault by now.
|
|
348
362
|
function onKey(e) {
|
|
349
|
-
|
|
363
|
+
// The glass is game mode's alone (visor.css); outside it, its keys are not bound either.
|
|
364
|
+
if (!shown() || document.documentElement.dataset.game !== 'on' || inField(e.target) || busy()) return;
|
|
350
365
|
var hit = keyFor(character(), e);
|
|
351
366
|
if (!hit) return;
|
|
352
367
|
e.preventDefault();
|
|
@@ -4,7 +4,13 @@ Covers: `visor.js` + `visor.css`, loaded on Home, `/city`, `/goal-map` and `/thi
|
|
|
4
4
|
|
|
5
5
|
## The rule every game page keeps (ADR 0403, owner 2026-11-11)
|
|
6
6
|
|
|
7
|
-
The visor is the control and the scene is the view: every main call to action in game mode is on the glass (the accent button, the key hints, the Actions sheet), and a scene carries none of its own. A scene that has something to do registers it with `OTBVisor.scene({ actions, keys })`. ADR 0403 §3 lists the scenes that do not keep it yet.
|
|
7
|
+
The visor is the control and the scene is the view: every main call to action in game mode is on the glass (the accent button, the key hints, the Actions sheet), and a scene carries none of its own. A scene that has something to do registers it with `OTBVisor.scene({ actions, keys })`. ADR 0403 §3 lists the scenes that do not keep it yet; the Governor City keeps it since task 1005209 (below).
|
|
8
|
+
|
|
9
|
+
## A place's own buttons on the glass (task 1005209, the Governor City first)
|
|
10
|
+
|
|
11
|
+
- **The scene contract grew two flags.** A scene action's `key` is still a key the scene binds itself, shown beside it in the sheet. With `bind: true` the visor binds that key instead, after the character's keys (a clash goes to the character), for a scene button that had no key of its own. With `lead: true` the action is also the first quiet key hint on the glass (`.visor__hint--place`). Every scene action is still a row under "This place" in the sheet.
|
|
12
|
+
- **The glass binds keys in game mode only.** `onKey` now returns unless `html[data-game="on"]`: the glass was never drawn outside game mode, so its keys no longer act there either (before, a held character's letter keys still ran on a classic page).
|
|
13
|
+
- **The Governor City keeps ADR 0403.** In game mode (`html.visor-on`) the door's button (`#cityStart`) and the archived Street links leave the scene (`city.css`). The door is the lead action, on D, in the door's own words ("Decide 37 in a row", or "Start governing"); it re-registers only when those words change. The archived links are rows in the sheet ("Archived: Street", "Archived: Building Level Views"), read off the page's own archived line so the words and the addresses cannot drift; reached by A and the keyboard. The city line stays on the scene as words, raised to `bottom: 224px` because the extra hint wraps the hints onto a second line at a desk width and the centred accent button rises with it. A building's card is a pop-up, not the scene, and keeps its own buttons and keys. Classic mode is as it was.
|
|
8
14
|
|
|
9
15
|
## The minimal visor (Helm look 3, task 1005195)
|
|
10
16
|
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.92",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.24.
|
|
9
|
+
"version": "1.24.92",
|
|
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.24.
|
|
3
|
+
"version": "1.24.92",
|
|
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
|
@@ -11644,7 +11644,15 @@
|
|
|
11644
11644
|
"text": "In the game view the Artist's feedback tab no longer sits on the screen; the Artist opens the feedback sheet from the visor, and only the Artist can open it."
|
|
11645
11645
|
}
|
|
11646
11646
|
],
|
|
11647
|
-
"1.24.
|
|
11647
|
+
"1.24.92": [
|
|
11648
|
+
{
|
|
11649
|
+
"id": "1005209",
|
|
11650
|
+
"text": "In game mode the Governor City no longer has a button of its own: \"Decide N in a row\" is now on the visor (press D), and the archived Street links are in the visor's Actions list. Classic mode is unchanged."
|
|
11651
|
+
},
|
|
11652
|
+
{
|
|
11653
|
+
"id": "1005118",
|
|
11654
|
+
"text": "Signing the command line back in is now one Approve click in the browser — no copying, no pasting, and the sign-in key never appears in the chat."
|
|
11655
|
+
},
|
|
11648
11656
|
{
|
|
11649
11657
|
"id": "1005206",
|
|
11650
11658
|
"text": "The screen you land on after signing in, where you pick Game or Classic, now looks like the V3 game start menu: your usual choice is the bright salmon bar with its Enter key, the other a quiet row."
|
|
@@ -567,15 +567,16 @@ function printUnauthGuidanceAndExit() {
|
|
|
567
567
|
console.error('');
|
|
568
568
|
if (isTerminalSurface()) {
|
|
569
569
|
// No checkout here, so `node scripts/bongos/*.js` is not a path this reader has (task 1003730).
|
|
570
|
-
console.error(`
|
|
571
|
-
console.error(' (opens your
|
|
572
|
-
console.error('
|
|
570
|
+
console.error(` Sign back in — run ${surfaceAction('reauth')}`);
|
|
571
|
+
console.error(' (a page opens in your browser: click "Approve" and you are back in.');
|
|
572
|
+
console.error(' Nothing to copy or paste.)');
|
|
573
573
|
console.error(' Fallback (browser sign-in): npx bongos login <instance-url>');
|
|
574
574
|
} else {
|
|
575
|
-
|
|
575
|
+
// Task 1005118: one Approve click in the browser. Claude runs it when asked.
|
|
576
|
+
console.error(' Sign back in — ask Claude to "sign me back in" (/builder-reauth), or run:');
|
|
576
577
|
console.error(' node scripts/bongos/reauth.js');
|
|
577
|
-
console.error(' (opens your
|
|
578
|
-
console.error('
|
|
578
|
+
console.error(' (a page opens in your browser: click "Approve" and you are back in.');
|
|
579
|
+
console.error(' Nothing to copy or paste.)');
|
|
579
580
|
console.error(' Fallback (terminal Device Flow): node scripts/bongos/setup.js --force');
|
|
580
581
|
}
|
|
581
582
|
console.error('');
|
package/scripts/bongos/login.js
CHANGED
|
@@ -104,14 +104,19 @@ const { sleep } = require('../../src/sleep'); // the one shared sleep (task 1004
|
|
|
104
104
|
// instance's /auth/device/exchange — a hosted instance has no GitHub app of its own.
|
|
105
105
|
// The instance's OWN admission gate runs at poll (direct) or exchange (federated),
|
|
106
106
|
// so the stranger path is identical for both.
|
|
107
|
-
|
|
107
|
+
// `browser: true` (a returning builder's re-sign-in, task 1005118) asks the
|
|
108
|
+
// instance for its own approval page instead of GitHub's code entry: a browser
|
|
109
|
+
// that is already signed in approves with one click. An older instance ignores
|
|
110
|
+
// the ask and answers GitHub's flow, which this loop runs unchanged.
|
|
111
|
+
async function runDeviceFlow(base, { log, idp = null, browser = false }) {
|
|
108
112
|
const startUrl = idp ? `${idp.origin}/api/bongos/sso/device/start` : `${base}/api/bongos/auth/device/start`;
|
|
109
113
|
const start = await fetchJson(startUrl, {
|
|
110
114
|
method: 'POST',
|
|
111
115
|
// Federated: the hub resolves + binds the client from the USER-TYPED origin
|
|
112
116
|
// (`base`), never a client_id we supply — so this CLI can only ever mint an
|
|
113
117
|
// assertion scoped to the instance the user actually typed (ADR 0141 §1).
|
|
114
|
-
...(idp ? { body: JSON.stringify({ origin: base }) }
|
|
118
|
+
...(idp ? { body: JSON.stringify({ origin: base }) }
|
|
119
|
+
: browser ? { body: JSON.stringify({ approve_in_browser: true }) } : {}),
|
|
115
120
|
});
|
|
116
121
|
// ADR 0116 nests the slug at error.code (task 1003658). This site was half-converted:
|
|
117
122
|
// it read the nested shape for idp_signing_unconfigured but kept a FLAT comparison for
|
|
@@ -142,10 +147,16 @@ async function runDeviceFlow(base, { log, idp = null }) {
|
|
|
142
147
|
const expires = Number(start.data.expires_in) || 900;
|
|
143
148
|
|
|
144
149
|
log('');
|
|
150
|
+
// An approval-page link (it carries the code) is longer than the box: print it
|
|
151
|
+
// whole underneath rather than cut it, so a browser that didn't open can be
|
|
152
|
+
// pointed at it by hand.
|
|
153
|
+
const uri = String(verification_uri);
|
|
154
|
+
const fits = uri.length <= 36;
|
|
145
155
|
log(' ┌────────────────────────────────────────────┐');
|
|
146
|
-
log(` │ Open: ${
|
|
156
|
+
if (fits) log(` │ Open: ${uri.padEnd(36)}│`);
|
|
147
157
|
log(` │ Code: ${String(user_code).slice(0, 36).padEnd(36)}│`);
|
|
148
158
|
log(' └────────────────────────────────────────────┘');
|
|
159
|
+
if (!fits) log(` Open: ${uri}`);
|
|
149
160
|
log('');
|
|
150
161
|
openBrowser(verification_uri);
|
|
151
162
|
log(' Waiting for you to approve in the browser…');
|
|
@@ -324,7 +324,7 @@ const CARDS = Object.freeze({
|
|
|
324
324
|
'You, to sign in.', RULE),
|
|
325
325
|
session_invalid: card(
|
|
326
326
|
'Your sign-in to the project has ended.',
|
|
327
|
-
'I will
|
|
327
|
+
'I will open a page in your browser — you click Approve, and that is all.',
|
|
328
328
|
'You, to sign in again.', RULE),
|
|
329
329
|
instance_mismatch: card(
|
|
330
330
|
'This computer is signed in to a different project than the one this folder belongs to.',
|
package/scripts/bongos/reauth.js
CHANGED
|
@@ -1,45 +1,42 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// scripts/bongos/reauth.js — #504.
|
|
2
|
+
// scripts/bongos/reauth.js — #504; one-click since task 1005118.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// deploy. (It no longer stops working from mere disuse: ADR 0294 removed the idle
|
|
9
|
-
// expiry, because the timer only ever signed out the legitimate holder while
|
|
10
|
-
// refresh-on-use kept a thief's copy alive.) Whatever killed it, the friction of
|
|
11
|
-
// getting back in — the slow Device Flow, terminal codes, Gemini-key prompts — is
|
|
12
|
-
// not something to keep. This script is the fix:
|
|
4
|
+
// Signs the command line back in when its Bongos session has stopped working —
|
|
5
|
+
// revoked from the hall's "Sign out everywhere", swept by a demotion, or
|
|
6
|
+
// invalidated by an auth-changing deploy. (Never from mere disuse: ADR 0294
|
|
7
|
+
// removed the idle expiry.)
|
|
13
8
|
//
|
|
14
9
|
// 1. Detect — is the current session still valid? (GET /api/bongos/me)
|
|
15
|
-
// valid
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// /builder-reauth skill runs it (paste-token.js verifies + writes safely).
|
|
10
|
+
// valid → print who you are + exit 0. Nothing to do.
|
|
11
|
+
// 2. Approve in the browser (task 1005118) — start a sign-in that asks the
|
|
12
|
+
// instance for its OWN approval page (device-approval.js), open it with the
|
|
13
|
+
// code filled in, and wait. The builder's browser is already signed in, so
|
|
14
|
+
// the page is one Approve click; the terminal's poll then receives the new
|
|
15
|
+
// session and this script saves it. Nothing to copy, nothing to paste, and
|
|
16
|
+
// the token never passes through the chat.
|
|
23
17
|
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
18
|
+
// It used to be four steps with a paste: Settings → "Re-issue CLI token" → Copy →
|
|
19
|
+
// paste a `bongos paste-token <token>` line into Claude, which put the token in
|
|
20
|
+
// the chat history. That path stays as the FALLBACK for a machine with no browser
|
|
21
|
+
// to approve from (`--paste` prints it; so does any failure here).
|
|
27
22
|
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
23
|
+
// The session is written only after the server handed back a token for a
|
|
24
|
+
// signed-in person, and through saveSession (the per-instance store, ADR 0269),
|
|
25
|
+
// so a failed or cancelled approval never touches the existing session file.
|
|
31
26
|
//
|
|
32
27
|
// Usage:
|
|
33
|
-
// node scripts/bongos/reauth.js # detect; if invalid,
|
|
34
|
-
// node scripts/bongos/reauth.js --
|
|
28
|
+
// node scripts/bongos/reauth.js # detect; if invalid, approve in the browser
|
|
29
|
+
// node scripts/bongos/reauth.js --paste # detect; if invalid, print the copy-paste path
|
|
30
|
+
// node scripts/bongos/reauth.js --no-open # as --paste, without opening the browser
|
|
35
31
|
//
|
|
36
32
|
// Exit codes:
|
|
37
|
-
// 0 — session
|
|
38
|
-
// 10 —
|
|
39
|
-
// 2 — could not reach the
|
|
33
|
+
// 0 — signed in: the session was already valid, or the approval just landed
|
|
34
|
+
// 10 — still not signed in (cancelled, ran out, or not let in); fallback printed
|
|
35
|
+
// 2 — could not reach the instance (network); the session may be fine
|
|
40
36
|
|
|
41
37
|
const { spawn } = require('node:child_process');
|
|
42
38
|
const lib = require('./cli-lib');
|
|
39
|
+
const { readSessionConfigSync } = require('../../src/instance-config');
|
|
43
40
|
|
|
44
41
|
// lib.API_BASE is session-aware (task 1002427), so both URLs name the instance the
|
|
45
42
|
// builder is actually signed into — not this checkout's branding default, which is
|
|
@@ -85,7 +82,7 @@ function openInBrowser(url) {
|
|
|
85
82
|
|
|
86
83
|
function printGuidance({ opened }) {
|
|
87
84
|
console.log('');
|
|
88
|
-
console.log('
|
|
85
|
+
console.log('Fallback — sign back in with a copied line:');
|
|
89
86
|
console.log('');
|
|
90
87
|
// We can't reliably know if the browser actually launched — spawn() reports
|
|
91
88
|
// a missing-binary error asynchronously, after this prints. So we never claim
|
|
@@ -102,19 +99,71 @@ function printGuidance({ opened }) {
|
|
|
102
99
|
console.log('');
|
|
103
100
|
console.log(' If the page says you are not signed in, sign in at the builders\'');
|
|
104
101
|
console.log(` hall first (${HALL_URL}), then return to Settings and click Re-issue.`);
|
|
105
|
-
console.log(' Fallback (terminal Device Flow): node scripts/bongos/setup.js --force');
|
|
106
102
|
console.log('');
|
|
107
103
|
}
|
|
108
104
|
|
|
105
|
+
// The session to write after an approval: the server's token + builder for THIS
|
|
106
|
+
// instance, with every other field (gemini_api_key et al.) carried over from the
|
|
107
|
+
// existing session when it belongs to the same instance. Pure, so it is tested.
|
|
108
|
+
function nextSession(existing, { token, builder, apiBase }) {
|
|
109
|
+
const sameInstance = existing && existing.api_base
|
|
110
|
+
&& lib.sessionHostKey(existing.api_base) === lib.sessionHostKey(apiBase);
|
|
111
|
+
const carried = sameInstance ? existing
|
|
112
|
+
: (existing && existing.gemini_api_key ? { gemini_api_key: existing.gemini_api_key } : {});
|
|
113
|
+
return { ...carried, token, builder, api_base: apiBase };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Approve in the browser, then save. → true when signed back in.
|
|
117
|
+
async function approveInBrowser() {
|
|
118
|
+
const { runDeviceFlow } = require('./login');
|
|
119
|
+
console.log('');
|
|
120
|
+
console.log('Opening a page in your browser to sign the command line back in.');
|
|
121
|
+
console.log('Check the code matches, then click "Approve" — this carries on by itself.');
|
|
122
|
+
let out;
|
|
123
|
+
try {
|
|
124
|
+
out = await runDeviceFlow(lib.API_BASE, { log: console.log, browser: true });
|
|
125
|
+
} catch (err) {
|
|
126
|
+
console.log('');
|
|
127
|
+
console.log(`The browser sign-in didn't finish: ${err && err.message ? err.message : err}`);
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
console.log('');
|
|
131
|
+
if (!out || out.state !== 'ok' || !out.token) {
|
|
132
|
+
console.log(out && out.state === 'not_approved'
|
|
133
|
+
? 'That account is not let in to this project yet — ask its owner.'
|
|
134
|
+
: 'The sign-in ran out before it was approved. Run this again to get a fresh page.');
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
let existing = null;
|
|
138
|
+
try {
|
|
139
|
+
const raw = readSessionConfigSync();
|
|
140
|
+
existing = raw ? JSON.parse(raw) : null;
|
|
141
|
+
} catch (_) { existing = null; }
|
|
142
|
+
await lib.saveSession(nextSession(existing, { token: out.token, builder: out.builder, apiBase: lib.API_BASE }));
|
|
143
|
+
const b = out.builder || {};
|
|
144
|
+
console.log(`✓ Signed back in as ${b.github_login || '?'}${b.rank ? ` (rank: ${b.rank})` : ''}.`);
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function reSignIn({ paste, noOpen }) {
|
|
149
|
+
if (paste) {
|
|
150
|
+
printGuidance({ opened: noOpen ? false : openInBrowser(SETTINGS_URL) });
|
|
151
|
+
process.exit(10);
|
|
152
|
+
}
|
|
153
|
+
if (await approveInBrowser()) process.exit(0);
|
|
154
|
+
printGuidance({ opened: false });
|
|
155
|
+
process.exit(10);
|
|
156
|
+
}
|
|
157
|
+
|
|
109
158
|
async function main() {
|
|
159
|
+
// --no-open (the old flag) means no browser at all, so it is the paste path too.
|
|
110
160
|
const noOpen = process.argv.includes('--no-open');
|
|
161
|
+
const paste = noOpen || process.argv.includes('--paste');
|
|
111
162
|
|
|
112
163
|
const session = await lib.loadSession();
|
|
113
164
|
if (!session || !session.token) {
|
|
114
|
-
console.log('No
|
|
115
|
-
|
|
116
|
-
printGuidance({ opened });
|
|
117
|
-
process.exit(10);
|
|
165
|
+
console.log('No Bongos sign-in found on this machine.');
|
|
166
|
+
return reSignIn({ paste, noOpen });
|
|
118
167
|
}
|
|
119
168
|
|
|
120
169
|
// Detect: is the existing token still valid? /me is the canonical check
|
|
@@ -126,30 +175,31 @@ async function main() {
|
|
|
126
175
|
res = await api.me.getMe();
|
|
127
176
|
} catch (err) {
|
|
128
177
|
console.error(`Could not reach ${lib.API_BASE} to check your session: ${err.message}`);
|
|
129
|
-
|
|
130
|
-
printGuidance({ opened });
|
|
178
|
+
console.error('Your sign-in may be fine — try again once you are back online.');
|
|
131
179
|
process.exit(2);
|
|
132
180
|
}
|
|
133
181
|
|
|
134
182
|
if (res.ok) {
|
|
135
183
|
const b = (res.data && res.data.builder) || {};
|
|
136
|
-
console.log(
|
|
184
|
+
console.log('Your Bongos sign-in is working — nothing to do.');
|
|
137
185
|
console.log(` Signed in as ${b.github_login || '?'}${b.rank ? ` (rank: ${b.rank})` : ''}.`);
|
|
138
|
-
console.log(' (
|
|
186
|
+
console.log(' (Sign-ins do not expire — they last until revoked. Kill one from');
|
|
139
187
|
console.log(' Settings → Active sessions, or "Sign out everywhere".)');
|
|
140
188
|
process.exit(0);
|
|
141
189
|
}
|
|
142
190
|
|
|
143
|
-
// 401 / 403 / anything not-ok →
|
|
144
|
-
console.log('Your
|
|
145
|
-
|
|
146
|
-
printGuidance({ opened });
|
|
147
|
-
process.exit(10);
|
|
191
|
+
// 401 / 403 / anything not-ok → sign back in.
|
|
192
|
+
console.log('Your Bongos sign-in has stopped working (revoked or replaced).');
|
|
193
|
+
return reSignIn({ paste, noOpen });
|
|
148
194
|
}
|
|
149
195
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
196
|
+
if (require.main === module) {
|
|
197
|
+
main().catch((err) => {
|
|
198
|
+
console.error('reauth failed:', err && err.message ? err.message : err);
|
|
199
|
+
// Still print the manual path so the builder isn't stranded.
|
|
200
|
+
printGuidance({ opened: false });
|
|
201
|
+
process.exit(2);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
module.exports = { nextSession };
|
|
@@ -12,8 +12,10 @@
|
|
|
12
12
|
// GET /auth/device → the page a terminal sends the person to
|
|
13
13
|
// POST /auth/device/approve → the signed-in person approves or cancels it
|
|
14
14
|
//
|
|
15
|
-
//
|
|
16
|
-
// the hub that keeps who signs in with it (auth-google.js).
|
|
15
|
+
// The two /auth/google routes answer only where googleConfigured() — the owner's
|
|
16
|
+
// Google app AND the hub that keeps who signs in with it (auth-google.js). The
|
|
17
|
+
// two /auth/device routes answer everywhere: a CLI re-sign-in asks for them on a
|
|
18
|
+
// GitHub-only site too (task 1005118), where the page offers GitHub alone.
|
|
17
19
|
'use strict';
|
|
18
20
|
|
|
19
21
|
const express = require('express');
|
|
@@ -244,7 +246,7 @@ module.exports = function buildAuthGoogleRouter() {
|
|
|
244
246
|
state: 'signin',
|
|
245
247
|
userCode: pending.userCode,
|
|
246
248
|
githubHref: `${base}/auth/web/start?via=github&return=${encodeURIComponent(here)}`,
|
|
247
|
-
googleHref: `${base}/auth/google/start?return=${encodeURIComponent(here)}
|
|
249
|
+
googleHref: googleConfigured() ? `${base}/auth/google/start?return=${encodeURIComponent(here)}` : null,
|
|
248
250
|
}));
|
|
249
251
|
}
|
|
250
252
|
return res.type('text/html').send(renderDeviceApprovalPage({
|
|
@@ -603,9 +603,9 @@ module.exports = function buildAuthRouter() {
|
|
|
603
603
|
if (!auth.authConfigured()) {
|
|
604
604
|
return res.fail('auth_not_configured', { status: 503, message: 'GitHub OAuth App credentials missing on the server. See blockers/blockers.md#github-oauth-app.' });
|
|
605
605
|
}
|
|
606
|
-
|
|
607
|
-
//
|
|
608
|
-
if (googleConfigured()) {
|
|
606
|
+
if (validateOrRespond(req, res, { approve_in_browser: { type: 'boolean' } }, { strict: true })) return;
|
|
607
|
+
// Google offered, or a re-sign-in asked (task 1005118): this site's own approval page (device-approval.js).
|
|
608
|
+
if (googleConfigured() || req.body?.approve_in_browser === true) {
|
|
609
609
|
return res.json(startBrowserDeviceFlow({
|
|
610
610
|
pageUrl: `${WEB_REDIRECT_ORIGIN.replace(/\/+$/, '')}${apiBase(req)}/auth/device`,
|
|
611
611
|
}));
|
|
@@ -76,8 +76,7 @@ ${bodyHtml}
|
|
|
76
76
|
function choiceButtons({ githubHref, googleHref }) {
|
|
77
77
|
return ` <div class="signinChoices">
|
|
78
78
|
<a class="landing__btn" href="${escapeHtml(githubHref)}">${GITHUB_MARK}Continue with GitHub</a>
|
|
79
|
-
<a class="landing__btn" href="${escapeHtml(googleHref)}">${GOOGLE_MARK}Continue with Google</a
|
|
80
|
-
</div>`;
|
|
79
|
+
${googleHref ? ` <a class="landing__btn" href="${escapeHtml(googleHref)}">${GOOGLE_MARK}Continue with Google</a>\n` : ''} </div>`;
|
|
81
80
|
}
|
|
82
81
|
|
|
83
82
|
// The sign-in chooser /auth/web/start shows when Google sign-in is switched on.
|
package/src/module-api.js
CHANGED
|
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
|
|
|
81
81
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
82
82
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
83
83
|
|
|
84
|
-
const CORE_VERSION = '1.24.
|
|
84
|
+
const CORE_VERSION = '1.24.92'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
85
85
|
|
|
86
86
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
87
87
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|