@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.
@@ -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
- window.__visorScene = { name: 'city', place: 'Governor city', keys: [{ label: 'On a card: answer', key: '1–9' }, { label: 'On a card: next', key: '← →' }] };
763
- if (window.OTBVisor) window.OTBVisor.scene(window.__visorScene);
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
- if (!shown() || inField(e.target) || busy()) return;
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.91",
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.91",
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.91",
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",
@@ -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.91": [
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(` Recover the fast (UI-first) way — run ${surfaceAction('reauth')}`);
571
- console.error(' (opens your Settings page; under "CLI Access" click "Re-issue CLI');
572
- console.error(' token", copy the one-liner, paste it back — no terminal Device Flow.)');
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
- console.error(' Recover the fast (UI-first) way — run /builder-reauth, or:');
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 Settings page; under "CLI Access" click "Re-issue CLI');
578
- console.error(' token", copy the one-liner, paste it back — no terminal Device Flow.)');
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('');
@@ -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
- async function runDeviceFlow(base, { log, idp = null }) {
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: ${String(verification_uri).slice(0, 36).padEnd(36)}│`);
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 walk you through signing in again — Settings, then one copy and paste.',
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.',
@@ -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
- // The fast, UI-first path to refresh an expired/invalid GDS CLI session.
5
- //
6
- // Why this exists: a CLI session can stop working — revoked from the hall's
7
- // "Sign out everywhere", swept by a demotion, or invalidated by an auth-changing
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 → print who you are + exit 0. Nothing to do.
16
- // invalid → exit 10 ("re-auth needed") after opening the browser.
17
- // 2. Guide — open the builder's Settings page in the default browser (where
18
- // the builder is already signed in via the httpOnly cookie) and print the
19
- // exact click-path: Settings → CLI Access → Re-issue CLI token → Copy.
20
- // The page hands back a one-liner: `bongos paste-token
21
- // <token>`. The builder pastes that one line back into Claude; the
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
- // This script never touches the session file itself — paste-token.js owns the
25
- // write (and its verify-before-write safety contract). reauth.js is detect +
26
- // open + instruct only, so a failed re-auth can never corrupt a good session.
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
- // Honors Lars's UI-first rule as far as guided-paste allows: browser clicks +
29
- // a single copy/paste, no Device-Flow code entry. (A true zero-paste loopback
30
- // flow is a deferred follow-up.)
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, open Settings + guide
34
- // node scripts/bongos/reauth.js --no-open # detect + guide, but don't open browser
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 is valid (no re-auth needed)
38
- // 10 — re-auth needed (browser opened, instructions printed)
39
- // 2 — could not reach the API to check (network); guidance still printed
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('Refresh your GDS CLI session (UI-first — no Device Flow):');
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 GDS CLI session found on this machine.');
115
- const opened = noOpen ? false : openInBrowser(SETTINGS_URL);
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
- const opened = noOpen ? false : openInBrowser(SETTINGS_URL);
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(`Your GDS CLI session is valid — no re-auth needed.`);
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(' (CLI sessions do not expire — they last until revoked. Kill one from');
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 → treat as "needs re-auth".
144
- console.log('Your GDS CLI session is no longer valid (expired or revoked).');
145
- const opened = noOpen ? false : openInBrowser(SETTINGS_URL);
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
- main().catch((err) => {
151
- console.error('reauth failed:', err && err.message ? err.message : err);
152
- // Still print the manual path so the builder isn't stranded.
153
- printGuidance({ opened: false });
154
- process.exit(2);
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
- // Every route answers only where googleConfigured() — the owner's Google app AND
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
- // Google offered here: the terminal is sent to this instance's own approval
607
- // page, where the person signs in with GitHub or Google (device-approval.js).
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.91'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
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');