@bongos/core 1.21.17 → 1.21.18

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.
@@ -590,7 +590,6 @@ summary{min-height:24px;padding:3px 0;}
590
590
 
591
591
  /* GitHub repo picker (task 1002099): hairline rows, the chosen one marked on
592
592
  the accent; the private badge in the label face */
593
- #ghConnect .btn{margin-top:4px;}
594
593
  .ghlist{margin-top:4px;max-height:220px;overflow-y:auto;border:0;border-top:1px solid var(--rule);border-radius:0;background:none;}
595
594
  .ghlist:empty{display:none;}
596
595
  .ghrow{display:flex;align-items:center;gap:8px;width:100%;min-height:40px;text-align:left;background:none;border:0;border-bottom:1px solid var(--rule-soft);padding:10px 0;font:inherit;font-size:14px;color:var(--ink);cursor:pointer;}
@@ -1637,11 +1636,11 @@ summary{min-height:24px;padding:3px 0;}
1637
1636
  <!-- GitHub-integrated repo picker (adopt) + auto-create (fresh) — task 1002099.
1638
1637
  Without repo access we fall back to the manual owner/repo inputs
1639
1638
  (#manualHome), which stay authoritative. -->
1640
- <div id="ghConnect" class="reassure">
1641
- <p id="ghConnectMsg">&nbsp;</p>
1642
- <button type="button" class="btn" id="ghConnectBtn">Allow repo access</button>
1643
- <button type="button" class="linklike" id="ghManualToggle">or enter it manually</button>
1644
- </div>
1639
+ <!-- no repo-access ask in the wizard (task 1004555): every hub sign-in asks for repo
1640
+ access (ADR 0364), and a missing token is fetched by one automatic GitHub
1641
+ round-trip. Only when that hop could not help does this line show, above
1642
+ the manual fields. -->
1643
+ <div id="ghNoAccess" class="hint" hidden>We couldn’t read your GitHub repositories just now, so enter the repo by hand below.</div>
1645
1644
  <div id="ghBusy" class="hint" hidden>Reading your GitHub repositories…</div>
1646
1645
  <div id="ghAdopt" class="wizField" hidden>
1647
1646
  <span>Pick one of your repositories</span>
@@ -1667,8 +1666,6 @@ summary{min-height:24px;padding:3px 0;}
1667
1666
  </label>
1668
1667
  <!-- visibility stated BEFORE GitHub's own radio is the first place it appears (task 1002721) -->
1669
1668
  <div class="hint" style="grid-column:1 / -1;">Creating the repo yourself on GitHub? Public or private is your choice there — both work here, and we never change it.</div>
1670
- <!-- the way back from the manual fallback (task 1003683) -->
1671
- <button type="button" class="linklike" id="ghManualBack" style="grid-column:1 / -1;" hidden>Use my GitHub repositories instead</button>
1672
1669
  </div>
1673
1670
  <div class="hint" id="h-home">&nbsp;</div>
1674
1671
  <button type="button" class="btn small rowUse" data-row-use>Use this</button>
@@ -5276,44 +5273,39 @@ summary{min-height:24px;padding:3px 0;}
5276
5273
  403 → offer to grant repo access (a ?repos=1 re-sign-in);
5277
5274
  503 (server can't store the token) or any error → the manual inputs.
5278
5275
  #manualHome (the classic owner/repo fields) is the always-safe fallback. ── */
5279
- var repoAccess = null; /* null=unknown/loading · true · 'need-connect' · 'unavailable' · 'manual' */
5276
+ var repoAccess = null; /* null=unknown/loading · true · 'unavailable' (manual fields) */
5280
5277
  var repoList = []; /* the user's repos (from the probe) — feeds the picker + the fresh-name clash hint */
5281
5278
  var ghProbed = false;
5282
5279
 
5283
- function isManualHome() { return repoAccess === 'manual' || repoAccess === 'unavailable'; }
5280
+ function isManualHome() { return repoAccess === 'unavailable'; }
5284
5281
  function validRepoName(s) { return /^[A-Za-z0-9._-]{1,100}$/.test(String(s || '')); }
5285
5282
  function myLogin() { return (me && me.github_login) || 'you'; }
5286
5283
 
5287
- function connectRepos(e) {
5288
- if (e) e.preventDefault();
5289
- /* Same-tab navigation so the sessionStorage draft round-trips back to the Home step. */
5284
+ /* The one automatic GitHub round-trip (task 1004555). The token is missing (a session
5285
+ from before ADR 0364, or a sign-out elsewhere), and GitHub answers a repo sign-in at
5286
+ once for anyone who already approved it — so the page goes and fetches it instead of
5287
+ asking. Same tab, so the sessionStorage draft comes back with its step (task 1004549).
5288
+ ONCE per tab session: a hop that came back still without access cannot be helped by a
5289
+ second, so the caller falls back to the manual fields instead of looping. A probe that
5290
+ answers 200 clears the mark, so a later expiry may hop again. */
5291
+ var HOP_KEY = 'cb-repo-hop';
5292
+ function repoHopDone() { try { return sessionStorage.getItem(HOP_KEY) === '1'; } catch (e) { return true; } }
5293
+ function hopForRepoAccess() {
5294
+ if (repoHopDone()) return false;
5295
+ try { sessionStorage.setItem(HOP_KEY, '1'); } catch (e) { return false; }
5296
+ saveDraft();
5290
5297
  window.location.assign(API + '/auth/web/start?return=/projects%3Fview%3Dnew&repos=1');
5298
+ return true;
5291
5299
  }
5292
- $('ghConnectBtn').addEventListener('click', connectRepos);
5293
- $('ghManualToggle').addEventListener('click', function () { repoAccess = 'manual'; syncHome(); $('f-owner').focus(); });
5294
- $('ghManualBack').addEventListener('click', function () { repoAccess = 'need-connect'; syncHome(); $('ghConnectBtn').focus(); });
5295
5300
 
5296
5301
  function syncHome() {
5297
- var need = repoAccess === 'need-connect';
5298
5302
  var ok = repoAccess === true;
5299
5303
  var adopt = state.mode === 'adopt';
5300
5304
  $('ghBusy').hidden = !!repoAccess; /* busy only while still probing (null) */
5301
- $('ghConnect').classList.toggle('on', need); /* .reassure box: shown via .on (not hidden — .reassure sets display:none) */
5305
+ $('ghNoAccess').hidden = repoAccess !== 'unavailable';
5302
5306
  $('ghAdopt').hidden = !(ok && adopt);
5303
5307
  $('ghFresh').hidden = !(ok && !adopt);
5304
5308
  $('manualHome').hidden = !isManualHome();
5305
- /* only when the visitor CHOSE manual: with 'unavailable' the server cannot hold a
5306
- token, so a connect round-trip would land back here having changed nothing */
5307
- $('ghManualBack').hidden = repoAccess !== 'manual';
5308
- if (need) {
5309
- /* Informed consent (ADR 0155): this grants `repo`, which GitHub's own screen
5310
- words as full control of ALL your repositories — so say plainly why we ask
5311
- and what we do with it, rather than letting GitHub's wording be the first
5312
- the user hears of it. Private repos are the reason the scope is this wide. */
5313
- $('ghConnectMsg').textContent = adopt
5314
- ? 'You’re signed in — creating your project needs one more GitHub permission. GitHub will ask for repository access (it words this as access to all your repositories — that is the only scope it offers that can write to a private repo). We use it to read the repo you pick and push the setup commit to it. Nothing else is touched.'
5315
- : 'You’re signed in — creating your project needs one more GitHub permission. GitHub will ask for repository access (it words this as access to all your repositories — the narrower option cannot write to private repos). We use it to create your new empty repo and push the setup commit to it. Nothing else is touched.';
5316
- }
5317
5309
  if (ok && adopt) initAdoptPicker();
5318
5310
  if (ok && !adopt) syncFresh();
5319
5311
  }
@@ -5413,13 +5405,16 @@ summary{min-height:24px;padding:3px 0;}
5413
5405
 
5414
5406
  function ensureRepoAccess() {
5415
5407
  if (ghProbed && repoAccess !== null) return; /* already known this session */
5416
- if (isManualHome()) return; /* user chose manual — don't nag */
5417
5408
  repoAccess = null; syncHome(); /* show "reading your repos…" */
5418
5409
  fetch(API + '/github/repos', { credentials: 'same-origin' })
5419
5410
  .then(function (res) {
5420
5411
  ghProbed = true;
5421
- if (res.status === 200) return res.json().then(function (d) { repoList = (d && d.repos) || []; repoAccess = true; });
5422
- if (res.status === 403 || res.status === 401) { repoAccess = 'need-connect'; return; }
5412
+ if (res.status === 200) {
5413
+ try { sessionStorage.removeItem(HOP_KEY); } catch (e) {}
5414
+ return res.json().then(function (d) { repoList = (d && d.repos) || []; repoAccess = true; });
5415
+ }
5416
+ /* no stored token: fetch it (one hop), else the manual fields */
5417
+ if (res.status === 403 || res.status === 401) { if (!hopForRepoAccess()) repoAccess = 'unavailable'; return; }
5423
5418
  repoAccess = 'unavailable'; return; /* 503 or anything else → manual fallback */
5424
5419
  })
5425
5420
  .catch(function () { ghProbed = true; repoAccess = 'unavailable'; })
@@ -5901,7 +5896,6 @@ summary{min-height:24px;padding:3px 0;}
5901
5896
  if (!works.length) return homeErr('Pick what already exists, then your main one.');
5902
5897
  if (works.indexOf(a.main_work) === -1) return homeErr('Mark which of them is your main one.');
5903
5898
  }
5904
- if (repoAccess === 'need-connect') return homeErr('Allow GitHub repo access to pick or create your repository — or choose “enter it manually”.');
5905
5899
  if (repoAccess === true && state.mode !== 'adopt') {
5906
5900
  /* Fresh with access: just the new repo NAME (owner is the signed-in account). */
5907
5901
  if (!validRepoName(state.repo)) return homeErr('Name the new repository (letters, numbers, and . _ -).');
@@ -6346,9 +6340,12 @@ summary{min-height:24px;padding:3px 0;}
6346
6340
  }).then(function (res) {
6347
6341
  return res.json().catch(function () { return {}; }).then(function (data) {
6348
6342
  if (res.status === 401 || res.status === 403) {
6349
- repoAccess = 'need-connect';
6350
- var e0 = new Error('reconnect'); e0.gotoHome = true;
6351
- e0.homeMsg = 'Your GitHub authorization expired — reconnect on the Home step to create the repo.';
6343
+ /* the token went between the probe and Create: the hop fetches it again and
6344
+ brings the founder back to this screen; if it cannot, the manual fields */
6345
+ var hopped = hopForRepoAccess();
6346
+ if (!hopped) { repoAccess = 'unavailable'; syncHome(); }
6347
+ var e0 = new Error('reconnect'); e0.gotoHome = !hopped;
6348
+ e0.homeMsg = 'We couldn’t create the repo on GitHub just now — enter one you made yourself below.';
6352
6349
  throw e0;
6353
6350
  }
6354
6351
  if (!res.ok) {
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.17",
3
+ "version": "1.21.18",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.21.17",
9
+ "version": "1.21.18",
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.17",
3
+ "version": "1.21.18",
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",
@@ -8858,5 +8858,11 @@
8858
8858
  "id": "1004554",
8859
8859
  "text": "Bongos no longer plays any sounds: no ship bell, credits chime, fanfare or end-of-turn chime. Settings → Sound now just says they're off for now."
8860
8860
  }
8861
+ ],
8862
+ "1.21.18": [
8863
+ {
8864
+ "id": "1004555",
8865
+ "text": "Project setup no longer shows an \"Allow repo access\" button — if the site needs GitHub access it fetches it in a quick reload and brings you straight back to where you were."
8866
+ }
8861
8867
  ]
8862
8868
  }
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.17'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.21.18'; // 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');
@@ -101,57 +101,86 @@ test('a draft already at step 1 is unchanged, and an out-of-range step is not re
101
101
  }
102
102
  });
103
103
 
104
- // ── slice 2 · syncHome's control over the way back ────────────────────────────────
104
+ // ── slice 2 · no "Allow repo access" step: the token is fetched, not asked for (task 1004555) ──
105
+ // Owner, 2026-10-02: "I don't want to see the old button at all." Every hub sign-in asks
106
+ // for repo access (ADR 0364) and GitHub answers at once for anyone who approved it, so a
107
+ // missing token is fetched by ONE automatic round-trip. The slices below are syncHome (what
108
+ // shows) and ensureRepoAccess (the probe and the hop), run under a DOM + fetch stub.
105
109
  const SYNC_RE = /function syncHome\(\) \{[\s\S]*?\n {4}\}/;
106
110
  const syncSlice = (HUB.match(SYNC_RE) || [null])[0];
111
+ const HOP_RE = /var HOP_KEY = 'cb-repo-hop';[\s\S]*?function hopForRepoAccess\(\) \{[\s\S]*?\n {4}\}/;
112
+ const hopSlice = (HUB.match(HOP_RE) || [null])[0];
113
+ const PROBE_RE = /function ensureRepoAccess\(\) \{[\s\S]*?\n {4}\}/;
114
+ const probeSlice = (HUB.match(PROBE_RE) || [null])[0];
107
115
 
108
- test('the syncHome slice is still findable', () => {
116
+ test('the syncHome, hop and probe slices are still findable', () => {
109
117
  assert.ok(syncSlice, 'syncHome() was not found in projects.html');
110
- assert.match(syncSlice, /ghManualBack/, 'syncHome governs the way-back control');
118
+ assert.ok(hopSlice, 'hopForRepoAccess() was not found in projects.html');
119
+ assert.ok(probeSlice, 'ensureRepoAccess() was not found in projects.html');
120
+ });
121
+
122
+ test('the page carries no "Allow repo access" control at all', () => {
123
+ assert.doesNotMatch(HUB, /Allow repo access/);
124
+ assert.doesNotMatch(HUB, /id="ghConnect/);
125
+ assert.doesNotMatch(HUB, /need-connect/, 'no state is left that would ask for it');
111
126
  });
112
127
 
113
128
  function runSync(repoAccess) {
114
129
  const els = {};
115
130
  // Elements start HIDDEN so that "shown" is only ever the result of syncHome setting it.
116
- // With a `hidden: false` default, a control syncHome never mentions reads as visible and
117
- // the assertion below would pass against the unfixed page.
118
- const el = (id) => (els[id] ||= {
119
- id, hidden: true, textContent: '', _on: false,
120
- classList: { toggle(_c, v) { els[id]._on = !!v; } },
121
- });
131
+ const el = (id) => (els[id] ||= { id, hidden: true, textContent: '' });
122
132
  const sandbox = {
123
- repoAccess,
124
- state: { mode: 'greenfield' },
125
- $: el,
126
- isManualHome: () => repoAccess === 'manual' || repoAccess === 'unavailable',
133
+ repoAccess, state: { mode: 'greenfield' }, $: el,
134
+ isManualHome: () => repoAccess === 'unavailable',
127
135
  initAdoptPicker() {}, syncFresh() {},
128
136
  };
129
137
  sandbox.globalThis = sandbox;
130
138
  vm.createContext(sandbox);
131
139
  vm.runInContext(syncSlice + '\nsyncHome();', sandbox);
132
- return { connectShown: el('ghConnect')._on, manualShown: !el('manualHome').hidden, backShown: !el('ghManualBack').hidden };
140
+ return { noteShown: !el('ghNoAccess').hidden, manualShown: !el('manualHome').hidden, freshShown: !el('ghFresh').hidden };
141
+ }
142
+
143
+ test('without access the manual fields show with one plain line; with access they do not', () => {
144
+ assert.deepEqual(runSync('unavailable'), { noteShown: true, manualShown: true, freshShown: false });
145
+ assert.deepEqual(runSync(true), { noteShown: false, manualShown: false, freshShown: true });
146
+ });
147
+
148
+ async function runProbe({ status, hopped = false }) {
149
+ const store = new Map(hopped ? [['cb-repo-hop', '1']] : []);
150
+ const assigned = [];
151
+ let saves = 0;
152
+ const sandbox = {
153
+ API: '/api/bongos', ghProbed: false, repoAccess: null, repoList: [],
154
+ sessionStorage: { getItem: (k) => (store.has(k) ? store.get(k) : null), setItem: (k, v) => store.set(k, v), removeItem: (k) => store.delete(k) },
155
+ window: { location: { assign: (u) => assigned.push(u) } },
156
+ saveDraft() { saves += 1; }, syncHome() {},
157
+ fetch: () => Promise.resolve({ status, json: () => Promise.resolve({ repos: [] }) }),
158
+ };
159
+ sandbox.globalThis = sandbox;
160
+ vm.createContext(sandbox);
161
+ vm.runInContext(hopSlice + '\n' + probeSlice + '\nvar done = ensureRepoAccess();', sandbox);
162
+ await new Promise((r) => setTimeout(r, 0));
163
+ await new Promise((r) => setTimeout(r, 0));
164
+ return { assigned, saves, repoAccess: vm.runInContext('repoAccess', sandbox), mark: store.get('cb-repo-hop') || null };
133
165
  }
134
166
 
135
- test('choosing manual leaves a way back to the connect offer', () => {
136
- const manual = runSync('manual');
137
- assert.equal(manual.manualShown, true, 'the manual fields are up');
138
- assert.equal(manual.connectShown, false, 'the connect box is down — that is what made it a trap');
139
- assert.equal(manual.backShown, true, 'and the way back is offered');
167
+ test('a missing token is fetched by one automatic GitHub round-trip, back to the wizard, draft saved first', async () => {
168
+ const r = await runProbe({ status: 403 });
169
+ assert.deepEqual(r.assigned, ['/api/bongos/auth/web/start?return=/projects%3Fview%3Dnew&repos=1']);
170
+ assert.equal(r.saves, 1, 'the draft is saved before the page leaves');
171
+ assert.equal(r.mark, '1', 'the hop is marked for this tab');
140
172
  });
141
173
 
142
- test('a FORCED fallback offers no way back, because connecting cannot help', () => {
143
- // 'unavailable' means the server could not store a token. Offering "use my GitHub
144
- // repositories instead" there would send the visitor through a round-trip that lands
145
- // them right back here — an offer that cannot be honoured is worse than none.
146
- const forced = runSync('unavailable');
147
- assert.equal(forced.manualShown, true);
148
- assert.equal(forced.backShown, false);
174
+ test('a second miss after the hop falls back to the manual fields instead of looping', async () => {
175
+ const r = await runProbe({ status: 403, hopped: true });
176
+ assert.deepEqual(r.assigned, [], 'no second round-trip');
177
+ assert.equal(r.repoAccess, 'unavailable');
149
178
  });
150
179
 
151
- test('while the connect offer is up, the way back is not', () => {
152
- const need = runSync('need-connect');
153
- assert.equal(need.connectShown, true);
154
- assert.equal(need.backShown, false, 'no second, redundant control beside the button itself');
180
+ test('access found clears the mark, so a later expiry may hop again', async () => {
181
+ const r = await runProbe({ status: 200, hopped: true });
182
+ assert.equal(r.repoAccess, true);
183
+ assert.equal(r.mark, null);
155
184
  });
156
185
 
157
186
  // ── slice 3 · why this task's original team-shape claim was wrong ─────────────────
@@ -102,7 +102,7 @@ test('nothing the old steps could do is lost: their controls are moved whole int
102
102
  const r = (p.match(new RegExp(`<div class="setRow" data-row="${row}">[\\s\\S]*?\\n {16}</div>\\n`)) || [''])[0];
103
103
  for (const id of ids) assert.match(r, new RegExp(`id="${id}"`), `${row} row hosts #${id}`);
104
104
  };
105
- has('code', ['physExisting', 'c-green', 'c-adopt', 'adoptNote', 'ghConnect', 'ghAdopt', 'ghFresh', 'manualHome', 'h-home']);
105
+ has('code', ['physExisting', 'c-green', 'c-adopt', 'adoptNote', 'ghNoAccess', 'ghAdopt', 'ghFresh', 'manualHome', 'h-home']);
106
106
  has('address', ['f-slug', 'addrConfirm', 'addrCustom', 'f-domain', 'f-noaddr']);
107
107
  has('app', ['appOpts', 'renderLater', 'renderSetup', 'f-renderKey', 'renderWs', 'renderSvc', 'renderNew']);
108
108
  has('extras', ['modQ', 'modCards', 'modWhy', 'modCoreNote']);