@bongos/core 1.19.330 → 1.19.332

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 CHANGED
@@ -2,11 +2,11 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.330",
6
- "core_contract": "1.19.330",
7
- "source_commit": "acf9d9dd29679f6c527824b2803950999664958e",
5
+ "core_version": "1.19.332",
6
+ "core_contract": "1.19.332",
7
+ "source_commit": "e2808f7a5ce4556d6265f326004a0cc87f463841",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-24T23:30:13.047Z",
9
+ "built_at": "2026-08-25T00:18:07.252Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 324,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 1811,
20
- "tree_sha256": "40c5457e809c4588d34d3be61fccf5c49606fc002cbf64170aaf50c56ffa7652",
20
+ "tree_sha256": "e1c90c92977490868bc70a33b665c39305e4a5ec7c2b8af2f042df1ad8151de5",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -382,7 +382,7 @@
382
382
  {
383
383
  "path": "config/branding.neutral.json",
384
384
  "mode": "0000644",
385
- "sha256": "d1d2622e002edaed8a98deb8b69ff5a77c6eeab7993e3b0e7f1513f32ff027c6"
385
+ "sha256": "7b204f9621ced68ef54b9f74aa8fbec53ab2fa81b0380133c762950ac4de1479"
386
386
  },
387
387
  {
388
388
  "path": "config/design-tokens.neutral.json",
@@ -4667,12 +4667,12 @@
4667
4667
  {
4668
4668
  "path": "package-lock.json",
4669
4669
  "mode": "0000644",
4670
- "sha256": "9bb6d40a874f3fa2ac02ff97883933dd86765c7c0d0f12aee6914961584b9702"
4670
+ "sha256": "bc62624584d42e3c04dda858b69f3fdbb1c09db67d54c46b1e7601f004c2eca9"
4671
4671
  },
4672
4672
  {
4673
4673
  "path": "package.json",
4674
4674
  "mode": "0000644",
4675
- "sha256": "2b5ad8d6ae7d079ecc2d53f39c32f076521b843514fe80b71851f80440463ab6"
4675
+ "sha256": "be5885b562847832c030b3dcbed240eb204a1c648eb6835d0b3bddb107aec2f6"
4676
4676
  },
4677
4677
  {
4678
4678
  "path": "public-docs/index.html",
@@ -4912,7 +4912,7 @@
4912
4912
  {
4913
4913
  "path": "public-landing/assets/cosmos.css",
4914
4914
  "mode": "0000644",
4915
- "sha256": "a7f21729b00719b22cfd13c14b33d87f4fdd8aaa2890a174bfc36d1b53c1e5e4"
4915
+ "sha256": "2a258be2790a6fadd1c2d9e190675c62e2b68dddece65f497b4e983c6d9b9146"
4916
4916
  },
4917
4917
  {
4918
4918
  "path": "public-landing/assets/verro-medallion.png",
@@ -4932,7 +4932,7 @@
4932
4932
  {
4933
4933
  "path": "public-landing/projects.html",
4934
4934
  "mode": "0000644",
4935
- "sha256": "203b726527c02ed0383945c431df062ae6f02ed64cadc7942f9fef3e587a9da6"
4935
+ "sha256": "44474703a87cecfa8f80ecbca6ecfb03d4fc3ccf166fe26bc4c310f404f5f76a"
4936
4936
  },
4937
4937
  {
4938
4938
  "path": "public-landing/world/assets/clouds/cloud-island-large.png",
@@ -6342,7 +6342,7 @@
6342
6342
  {
6343
6343
  "path": "src/bongos/routes/auth.js",
6344
6344
  "mode": "0000644",
6345
- "sha256": "c5688194f21600a64ced6b27aa5c12b4f580db786ffcf85a151422635269aa40"
6345
+ "sha256": "bb62110c21a07745a8bcfd3e2cf94f29deec1de5baea9d62fa903febcca54f2b"
6346
6346
  },
6347
6347
  {
6348
6348
  "path": "src/bongos/routes/backup.js",
@@ -6452,7 +6452,7 @@
6452
6452
  {
6453
6453
  "path": "src/module-api.js",
6454
6454
  "mode": "0000644",
6455
- "sha256": "da4209544a6cd0b389cccc563cd45f6c04fc22816f7b132772024f77fa2900f8"
6455
+ "sha256": "34ffbdddcf9e14262f980f9badc4f19eee6235321572aeb71809810eb09b32b5"
6456
6456
  },
6457
6457
  {
6458
6458
  "path": "src/module-loader/catalog.js",
@@ -8277,7 +8277,7 @@
8277
8277
  {
8278
8278
  "path": "tests/post_login_landing.mjs",
8279
8279
  "mode": "0000644",
8280
- "sha256": "942b2a9b0d5e2d2dd337b3ff738b7193ce0936d196a936f61bef51b74ed11ad0"
8280
+ "sha256": "08a7496090f4617cef7e270afddcef8ee3a0b58bff6920b56bc2f2bbb49ed4ca"
8281
8281
  },
8282
8282
  {
8283
8283
  "path": "tests/pre_compact.mjs",
@@ -8332,7 +8332,7 @@
8332
8332
  {
8333
8333
  "path": "tests/projects_hub_pre_uat.mjs",
8334
8334
  "mode": "0000644",
8335
- "sha256": "a3cea371bb80b2dc05762a11bc43de84667f06db8723e384b34073e48fdfda6a"
8335
+ "sha256": "c30221f0cb0b23b7f18b2edffd26cb895038543b07ee87c7a401508131edfc1a"
8336
8336
  },
8337
8337
  {
8338
8338
  "path": "tests/promote_goal_id.mjs",
@@ -103,7 +103,7 @@
103
103
  }
104
104
  },
105
105
  "landing": {
106
- "_note": "Where a successful sign-in lands when no explicit ?return= / same-apex Referer was carried (auth.js postLoginLanding). Portable default '/builders' — the hall always exists. A platform-home instance points this at its own front door (cloudbongos.com → '/gate/', the orbs). Validated through the open-redirect allowlist; env override <PREFIX>_POST_LOGIN_LANDING. Task 1002240.",
106
+ "_note": "Where a successful sign-in lands when no explicit ?return= / same-apex Referer was carried (auth.js postLoginLanding). Portable default '/builders' — the hall always exists. A platform-home instance points this at its own front door (cloudbongos.com → '/', its landing page; the retired '/gate/' orb sky folds to '/' — task 1003233). Validated through the open-redirect allowlist; env override <PREFIX>_POST_LOGIN_LANDING. Task 1002240.",
107
107
  "postLogin": "/builders"
108
108
  },
109
109
  "envPrefix": "CLOUDBONGOS"
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.330",
3
+ "version": "1.19.332",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.330",
9
+ "version": "1.19.332",
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.19.330",
3
+ "version": "1.19.332",
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",
@@ -414,6 +414,10 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
414
414
  /* ── page-level bits ── */
415
415
  .alert{display:none;background:var(--err-soft);border:2px solid color-mix(in srgb,var(--err) 25%,transparent);color:var(--err);font-weight:600;font-size:14px;border-radius:14px;padding:12px 16px;margin-bottom:18px;}
416
416
  .alert.on{display:block;}
417
+ /* the raw server text folds under the alert's plain sentence (task 1003244) */
418
+ .alert details{margin-top:8px;}
419
+ .alert summary{cursor:pointer;font-size:12.5px;font-weight:700;}
420
+ .alert details p{margin-top:6px;font-family:var(--font-mono);font-size:12px;font-weight:500;overflow-wrap:anywhere;}
417
421
  .loading{font-size:14.5px;font-weight:600;color:var(--ink-faint);padding:14px 0;}
418
422
  .empty{border:2px dashed var(--line);border-radius:18px;padding:22px;text-align:center;}
419
423
  .empty p{font-size:14.5px;font-weight:600;color:var(--ink-soft);margin-bottom:14px;line-height:1.55;}
@@ -614,7 +614,7 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
614
614
  stripFor/errMarkup/canRetry/canRevive the list card reads -->
615
615
  <div id="mStrip"></div>
616
616
  <div class="projMsg err" id="mErr" hidden></div>
617
- <div class="projMsg" id="mMsg"></div>
617
+ <div class="projMsg" id="mMsg" role="status" aria-live="polite"></div>
618
618
  <div class="actions" id="mActs" hidden></div>
619
619
  </div>
620
620
 
@@ -683,7 +683,7 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
683
683
  <div class="wrap viewWrap">
684
684
  <div class="card">
685
685
  <ol class="steps" aria-label="Creation steps">
686
- <li class="now" data-step="1"><span class="snum">1</span><span class="slbl">Name</span></li>
686
+ <li class="now" data-step="1" aria-current="step"><span class="snum">1</span><span class="slbl">Name</span></li>
687
687
  <li data-step="2"><span class="snum">2</span><span class="slbl">Type</span></li>
688
688
  <li data-step="3"><span class="snum">3</span><span class="slbl">Home</span></li>
689
689
  <li data-step="4"><span class="snum">4</span><span class="slbl">Address</span></li>
@@ -2400,16 +2400,27 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
2400
2400
  Six steps since task 1002772: gate 0 · name 1 · type 2 · home 3 ·
2401
2401
  address 4 · review 5 · done 6 (the rail shows 1-5; done marks it all). */
2402
2402
  var panels = { 0: 'panel-gate', 1: 'panel-name', 2: 'panel-type', 3: 'panel-home', 4: 'panel-address', 5: 'panel-review', 6: 'panel-done' };
2403
+ /* the rail at the done panel (task 1003244): "done" is the Create step
2404
+ still running — the marker stays on 5 until the project actually
2405
+ answers, and only then does the whole rail read finished. aria-current
2406
+ tells a screen reader the same thing. */
2407
+ function paintRail(step) {
2408
+ var landed = step === 6 && !!(live && live.instStatus === 'active');
2409
+ var cur = step === 6 ? 5 : step;
2410
+ root.querySelectorAll('.steps li').forEach(function (li) {
2411
+ var n = Number(li.getAttribute('data-step'));
2412
+ var now = !landed && n === cur;
2413
+ li.classList.toggle('now', now);
2414
+ li.classList.toggle('done', landed || n < cur);
2415
+ if (now) li.setAttribute('aria-current', 'step'); else li.removeAttribute('aria-current');
2416
+ });
2417
+ }
2403
2418
  function show(step, moveFocus) {
2404
2419
  state.step = step;
2405
2420
  Object.keys(panels).forEach(function (k) {
2406
2421
  $(panels[k]).classList.toggle('on', Number(k) === step);
2407
2422
  });
2408
- root.querySelectorAll('.steps li').forEach(function (li) {
2409
- var n = Number(li.getAttribute('data-step'));
2410
- li.classList.toggle('now', n === step);
2411
- li.classList.toggle('done', n < step || step === 6);
2412
- });
2423
+ paintRail(step);
2413
2424
  saveDraft();
2414
2425
  /* the done step takes the whole card — the bongo pair steps aside */
2415
2426
  root.querySelector('.createSplit').classList.toggle('wizDone', step === 6);
@@ -2782,14 +2793,24 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
2782
2793
  }
2783
2794
 
2784
2795
  /* ── create ── */
2796
+ /* the mapped codes are headline-only; anything else gets a fixed owner
2797
+ sentence with the raw text folded under Technical details (direction
2798
+ §5.1, task 1003244) — operator text is never the headline */
2785
2799
  function friendlyCreateError(err) {
2786
2800
  var code = err && err.data && err.data.error && (err.data.error.code || '');
2787
2801
  if (code === 'slug_taken') return 'That handle is already taken by another project. Pick a different one.';
2788
2802
  if (code === 'bad_slug') return 'The handle didn’t pass the server’s checks — 2–40 lowercase letters, numbers, or dashes.';
2789
- if (code === 'domain_reserved') return 'That address is reserved for the platform itself. Pick a different handle, or bring a domain you own.';
2803
+ /* both server branches: a reserved hostname, and any name under the platform's own apex */
2804
+ if (code === 'domain_reserved') return 'That address belongs to the platform’s own domain — your project already gets its own address there from its handle. Use a domain you own instead.';
2790
2805
  if (code === 'domain_taken') return 'That address is already held by another project. Pick a different handle or domain.';
2791
2806
  if (code === 'bad_domain') return 'The domain didn’t parse as a real hostname — use something like myproject.example.com.';
2792
- return 'Couldn’t file the request: ' + (err && err.message ? err.message : 'something went sideways. Try again.');
2807
+ if (code === 'repo_scope_required') return 'GitHub hasn’t granted repository access for this session. Sign in again — it asks for repository access — and try once more.';
2808
+ if (code === 'request_failed') return 'The platform couldn’t file the request. Nothing was created — try again in a moment.';
2809
+ return '';
2810
+ }
2811
+ function createFailureMarkup(err) {
2812
+ return 'We couldn’t file the request just now. Nothing was created — you can try again.' +
2813
+ '<details><summary>Technical details</summary><p>' + esc(err && err.message ? err.message : 'no detail came back') + '</p></details>';
2793
2814
  }
2794
2815
 
2795
2816
  /* Fresh + repo access: auto-create the empty GitHub repo NOW (the commit moment),
@@ -2901,7 +2922,9 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
2901
2922
  $('h-home').className = 'hint err';
2902
2923
  return;
2903
2924
  }
2904
- alertBox.textContent = friendlyCreateError(err);
2925
+ var friendly = friendlyCreateError(err);
2926
+ if (friendly) alertBox.textContent = friendly;
2927
+ else alertBox.innerHTML = createFailureMarkup(err);
2905
2928
  alertBox.classList.add('on');
2906
2929
  });
2907
2930
  });
@@ -2918,7 +2941,7 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
2918
2941
  var GH_ICON = '<svg class="gh" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>';
2919
2942
 
2920
2943
  var instanceId = null; /* set when a create lands / a resumed watch starts */
2921
- var live = { instStatus: null, manifest: null, manifestMsg: '', appSlug: null };
2944
+ var live = { instStatus: null, manifest: null, manifestMsg: '', manifestCode: '', appSlug: null };
2922
2945
  var planPromises = {}; /* mode|domain → Promise<steps[]> */
2923
2946
  var lastStepsHtml = '';
2924
2947
  var FALLBACK_HUMAN_KEYS = ['github-repo', 'oauth-app', 'device-flow', 'app-install', 'first-sign-in', 'enter-hall'];
@@ -3000,8 +3023,15 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
3000
3023
  return '<li class="sdone" data-onboard-step="oauth-app"><b>GitHub sign-in is wired</b><p>Credentials placed and verified — sign-in on <code style="font-family:var(--font-mono)">' + esc(domain) + '</code> is live.</p></li>';
3001
3024
  }
3002
3025
  if (live.manifest === 'error' || live.manifest === 'expired') {
3003
- return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>' +
3004
- (live.manifest === 'expired' ? 'That setup link expired (they last 30 minutes) — mint a fresh one.' : 'GitHub setup hit a snag' + (live.manifestMsg ? ': ' + esc(live.manifestMsg) : '') + '. Try again.') + '</p>' +
3026
+ /* a plain sentence first; the raw route text folds under Technical
3027
+ details (task 1003244). The one refusal a founder can act on — no
3028
+ address yet — says where to go instead of what the server said. */
3029
+ var snag, raw = '';
3030
+ if (live.manifest === 'expired') snag = 'That setup link expired (they last 30 minutes) — mint a fresh one.';
3031
+ else if (live.manifestCode === 'domain_required') snag = 'This project needs an address first — attach one from <a href="?view=mine">your projects page</a>, then come back to this step.';
3032
+ else { snag = 'GitHub setup didn’t start. Nothing was changed on GitHub — try again.'; raw = live.manifestMsg; }
3033
+ return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>' + snag + '</p>' +
3034
+ (raw ? '<details><summary>Technical details</summary><p>' + esc(raw) + '</p></details>' : '') +
3005
3035
  '<button type="button" class="ghbtn" data-action="manifest-start">' + GH_ICON + 'Set up GitHub sign-in</button>' + manualOauthDetails() + '</li>';
3006
3036
  }
3007
3037
  if (live.manifest === 'code_received') {
@@ -3186,6 +3216,7 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
3186
3216
  return res.json().catch(function () { return {}; }).then(function (d) {
3187
3217
  if (!res.ok) {
3188
3218
  var e = new Error((d.error && (d.error.message || d.error.code)) || ('HTTP ' + res.status));
3219
+ e.code = (d.error && d.error.code) || '';
3189
3220
  throw e;
3190
3221
  }
3191
3222
  return d;
@@ -3206,6 +3237,7 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
3206
3237
  if (String(err && err.message) === 'signing in') return;
3207
3238
  if (tab) { try { tab.close(); } catch (e) {} }
3208
3239
  live.manifest = 'error'; live.manifestMsg = String((err && err.message) || 'could not start');
3240
+ live.manifestCode = String((err && err.code) || '');
3209
3241
  renderSteps();
3210
3242
  });
3211
3243
  }
@@ -3380,7 +3412,10 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
3380
3412
  /* the checklist reflects live state (task 1002694): a status transition —
3381
3413
  e.g. the scaffold landing — re-renders the steps (change-only, so the
3382
3414
  5s tick never clobbers an open dropdown or mid-flight button) */
3383
- if (inst && inst.status !== live.instStatus) { live.instStatus = inst.status; renderSteps(); }
3415
+ if (inst && inst.status !== live.instStatus) {
3416
+ live.instStatus = inst.status; renderSteps();
3417
+ if (state.step === 6) paintRail(6); /* the rail finishes only when the project answers */
3418
+ }
3384
3419
  var terminal = renderProgress(inst);
3385
3420
  if (!terminal) pollTimer = setTimeout(tick, 5000);
3386
3421
  else stopPolling();
@@ -3412,7 +3447,7 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
3412
3447
  instanceId = null;
3413
3448
  watchPending = false; manifestPending = false; manifestState = null;
3414
3449
  manifestStartedAt = 0; manifestStale = false;
3415
- live = { instStatus: null, manifest: null, manifestMsg: '', appSlug: null };
3450
+ live = { instStatus: null, manifest: null, manifestMsg: '', manifestCode: '', appSlug: null };
3416
3451
  lastStepsHtml = '';
3417
3452
  try { sessionStorage.removeItem(CREATED_KEY); sessionStorage.removeItem(DRAFT_KEY); } catch (e) {}
3418
3453
  state = { step: 1, name: '', projType: null, mode: 'greenfield', owner: '', ownerTouched: false, repo: '', repoTouched: false, slug: '', slugTouched: false, domain: '', noAddress: false };
@@ -3882,7 +3917,9 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
3882
3917
  : '. The repository is still yours, and its database is kept — bringing it back takes one click. Or <a href="?view=new">start another</a>.</div>');
3883
3918
  }
3884
3919
  var lm = lastMsg[inst.id];
3885
- body += '<div class="projMsg' + (lm && lm.cls ? ' ' + lm.cls : '') + '" id="msg-' + iid + '">' + (lm ? lm.text : '') + '</div>';
3920
+ /* a live region: an action's result is announced where the pointer is,
3921
+ not only painted (task 1003244) */
3922
+ body += '<div class="projMsg' + (lm && lm.cls ? ' ' + lm.cls : '') + '" id="msg-' + iid + '" role="status" aria-live="polite">' + (lm ? lm.text : '') + '</div>';
3886
3923
  /* every owned card opens its manage view (task 1003041) — a real link, so
3887
3924
  middle/ctrl-click works; the delegated handler upgrades a plain click */
3888
3925
  var acts = ['<a class="btn small ghost" data-manage="' + iid + '" href="?view=manage&id=' + iid + '">Manage</a>'];
@@ -3903,12 +3940,51 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
3903
3940
  var keepOpen = !!(oldFold && oldFold.open);
3904
3941
  var typed = oldInput && oldInput.value !== oldInput.defaultValue ? oldInput.value : '';
3905
3942
  var hadFocus = !!oldInput && document.activeElement === oldInput;
3943
+ /* focus anywhere else on the card (a door, a confirm, the fold's summary)
3944
+ comes back to the same control — or the nearest one — after the swap;
3945
+ quietActions() leaves a note when a disabled button already let go */
3946
+ var wasInside = li.contains(document.activeElement) && document.activeElement !== li;
3947
+ var prefer = wasInside ? focusKeyOf(document.activeElement) : null;
3948
+ var focusBack = wasInside || li.hasAttribute('data-focus-back');
3949
+ li.removeAttribute('data-focus-back');
3906
3950
  li.innerHTML = orbFor(inst) + '<div class="projBody">' + body + '</div>';
3907
3951
  var fold = li.querySelector('details.attachFold');
3908
3952
  var input = fold ? fold.querySelector('input') : null;
3909
3953
  if (fold && keepOpen) fold.open = true;
3910
3954
  if (input && typed) input.value = typed;
3911
3955
  if (input && hadFocus) input.focus();
3956
+ else if (focusBack) focusCardControl(inst.id, prefer);
3957
+ }
3958
+
3959
+ /* ── keeping focus on the card through a re-render (task 1003244) ── */
3960
+
3961
+ /* which control had focus, as a selector the rebuilt card can answer;
3962
+ a confirm's Yes/Keep lands back on the door that opened it */
3963
+ function focusKeyOf(el) {
3964
+ if (!el || !el.getAttribute) return null;
3965
+ if (el.tagName === 'SUMMARY') return 'details.attachFold summary';
3966
+ if (el.hasAttribute('data-teardown-yes') || el.hasAttribute('data-teardown-no')) return '[data-teardown]';
3967
+ var keys = ['data-manage', 'data-attach', 'data-retry', 'data-revive', 'data-teardown'];
3968
+ for (var i = 0; i < keys.length; i++) if (el.hasAttribute(keys[i])) return '[' + keys[i] + ']';
3969
+ return null;
3970
+ }
3971
+ function focusCardControl(id, prefer) {
3972
+ var li = liFor(id);
3973
+ if (!li) return;
3974
+ var t = (prefer && li.querySelector(prefer)) ||
3975
+ li.querySelector('.actions a, .actions button') ||
3976
+ li.querySelector('details.attachFold summary');
3977
+ if (t) t.focus();
3978
+ }
3979
+ /* an action in flight: the row stays put and its buttons go quiet, so
3980
+ nothing under the pointer is deleted mid-click; a button that held
3981
+ focus lets go when disabled, so the card remembers to take it back */
3982
+ function quietActions(id) {
3983
+ var act = $('act-' + id);
3984
+ if (!act) return;
3985
+ var li = liFor(id);
3986
+ if (li && act.contains(document.activeElement)) li.setAttribute('data-focus-back', '');
3987
+ act.querySelectorAll('button').forEach(function (b) { b.disabled = true; });
3912
3988
  }
3913
3989
 
3914
3990
  function render(instances) {
@@ -4465,16 +4541,20 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
4465
4541
  var act = $('act-' + id);
4466
4542
  if (!act) return;
4467
4543
  cardAsking = String(id);
4468
- act.innerHTML = '<div class="confirmRow">' +
4544
+ act.innerHTML = '<div class="confirmRow" tabindex="-1">' +
4469
4545
  '<span class="plain">Tear it down? ' + teardownTruth(byId[id]) + '</span>' +
4470
4546
  '<button type="button" class="btn small danger" data-teardown-yes="' + esc(id) + '">Yes, tear it down</button>' +
4471
4547
  '<button type="button" class="linklike" data-teardown-no="' + esc(id) + '">Keep it running</button></div>';
4548
+ /* the pressed button is gone — focus lands on the question itself, so
4549
+ the truth is read before Tab reaches Yes (task 1003244) */
4550
+ var row = act.firstChild;
4551
+ if (row && row.focus) row.focus();
4472
4552
  }
4473
4553
  function unAskTeardown(id) { cardAsking = null; var inst = byId[id]; if (inst) renderCard(inst); }
4474
4554
  function doTeardown(id) {
4475
4555
  cardAsking = null;
4476
- var act = $('act-' + id);
4477
- if (act) act.innerHTML = '<span class="projMsg">Asking…</span>';
4556
+ quietActions(id);
4557
+ msg(id, 'Asking…');
4478
4558
  fetch(API + '/provisioning/instances/' + encodeURIComponent(id) + '/teardown', {
4479
4559
  method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: '{}'
4480
4560
  }).then(function (res) {
@@ -4501,8 +4581,8 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
4501
4581
  }).catch(function () {
4502
4582
  msg(id, 'Couldn’t reach the platform — check your connection and try again.', 'err');
4503
4583
  }).then(function () {
4504
- /* restore the actions row ("Asking…" replaced it) even when the state
4505
- didn't change, then pick up whatever DID change */
4584
+ /* wake the quieted row even when the state didn't change (renderCard
4585
+ also hands focus back), then pick up whatever DID change */
4506
4586
  if (byId[id]) renderCard(byId[id]);
4507
4587
  watchOnce(id);
4508
4588
  });
@@ -4515,8 +4595,8 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
4515
4595
  function reProvision(id, verb) {
4516
4596
  var inst = byId[id];
4517
4597
  if (!inst) return;
4518
- var act = $('act-' + id);
4519
- if (act) act.innerHTML = '<span class="projMsg">Asking…</span>';
4598
+ quietActions(id);
4599
+ msg(id, 'Asking…');
4520
4600
  var body = { slug: inst.slug, hosting_shape: inst.hosting_shape || 'standalone' };
4521
4601
  if (inst.onboard_mode) body.onboard_mode = inst.onboard_mode;
4522
4602
  if (inst.target_ref) body.target_ref = inst.target_ref;
@@ -273,18 +273,27 @@ function configuredOAuthOrigin() {
273
273
  // was stashed for this sign-in. Configurable per instance via
274
274
  // branding.landing.postLogin (env override <PREFIX>_POST_LOGIN_LANDING) so a
275
275
  // platform-home instance can land visitors on its OWN front door —
276
- // cloudbongos.com sets it to /gate/ (the orbs) so a visitor who signs in from
277
- // the orbs returns to the orbs, never auto-redirected into the builders' hall
278
- // (task 1002240). Every other instance keeps the portable default '/builders':
279
- // the hall always exists, a /gate/ orbs site does not. The configured value is
280
- // run through the SAME open-redirect allowlist as ?return= (safeReturnTo), so a
281
- // misconfigured landing can never become an open redirect; anything unsafe or
282
- // absent falls back to '/builders'. Takes an optional resolved pack so the pure
283
- // path is unit-testable without disk I/O (mirrors clientBranding()).
276
+ // cloudbongos.com points it at '/' (its landing page) so a visitor who signs in
277
+ // from the front door returns to it, never auto-redirected into the builders'
278
+ // hall (task 1002240). Every other instance keeps the portable default
279
+ // '/builders': the hall always exists, a front door of its own may not. The
280
+ // configured value is run through the SAME open-redirect allowlist as ?return=
281
+ // (safeReturnTo), so a misconfigured landing can never become an open redirect;
282
+ // anything unsafe or absent falls back to '/builders'. Takes an optional
283
+ // resolved pack so the pure path is unit-testable without disk I/O (mirrors
284
+ // clientBranding()).
285
+ //
286
+ // /gate/ — the orb sky this knob was first written for — was retired with the
287
+ // landing-page front door (task 1002743) and the apex answers a bare 404 there,
288
+ // so a pack still pointing at it would land every cookie-less sign-in on a
289
+ // 404. It folds to '/' instead (task 1003233).
290
+ const RETIRED_LANDINGS = new Set(['/gate']);
284
291
  function postLoginLanding(brand) {
285
292
  try {
286
293
  const b = brand || branding();
287
- return safeReturnTo(b.landing?.postLogin) || '/builders';
294
+ const configured = safeReturnTo(b.landing?.postLogin) || '/builders';
295
+ const bare = configured.replace(/\/+$/, '').split('?')[0] || '/';
296
+ return RETIRED_LANDINGS.has(bare) ? '/' : configured;
288
297
  } catch {
289
298
  return '/builders';
290
299
  }
@@ -568,7 +577,7 @@ module.exports = function buildAuthRouter() {
568
577
  // Land back where sign-in began if a validated return target was stashed
569
578
  // (e.g. the builders subdomain bounced through here); else the instance's
570
579
  // configured post-login landing (default '/builders'; cloudbongos.com →
571
- // /gate/, the orbs — task 1002240).
580
+ // '/', its landing page — task 1002240).
572
581
  const returnTo = safeReturnTo(parseCookie(req, RETURN_COOKIE));
573
582
  res.clearCookie(RETURN_COOKIE, domainOpts);
574
583
  res.redirect(returnTo || postLoginLanding());
package/src/module-api.js CHANGED
@@ -1087,8 +1087,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
1087
1087
  // landed since 1.19.328 with no explicit bump. run 32788827293. (task 1002620)
1088
1088
  // 1.19.330 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1089
1089
  // landed since 1.19.329 with no explicit bump. run 32789596635. (task 1002620)
1090
+ // 1.19.331 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1091
+ // landed since 1.19.330 with no explicit bump. run 32791877167. (task 1002620)
1092
+ // 1.19.332 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1093
+ // landed since 1.19.331 with no explicit bump. run 32793033453. (task 1002620)
1090
1094
  // ---------------------------------------------------------------------------
1091
- const CORE_VERSION = '1.19.330'; // CI auto-patch carrier (ADR 0161); see changelog above
1095
+ const CORE_VERSION = '1.19.332'; // CI auto-patch carrier (ADR 0161); see changelog above
1092
1096
 
1093
1097
  // A namespaced logger so a module's log lines are attributable + consistent.
1094
1098
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -1,42 +1,42 @@
1
- // tests/post_login_landing.mjs — the configurable post-sign-in landing (task 1002240).
1
+ // tests/post_login_landing.mjs — where a cookie-less sign-in lands (task 1003233,
2
+ // the pre-walk ops check for goal 1000058).
2
3
  //
3
- // postLoginLanding(brand) resolves the redirect target used after a successful
4
- // web sign-in when NO explicit ?return= / same-apex Referer was carried. It is
5
- // the ONE seam that lets a platform-home instance (cloudbongos.com) land a
6
- // visitor back on its orbs (/gate/) instead of the builders' hall, while every
7
- // other instance keeps the portable '/builders' default (the hall always
8
- // exists; a /gate/ orbs site does not). Pure over an injected brand pack, and
9
- // hardened through the SAME open-redirect allowlist as ?return= (safeReturnTo),
10
- // so a misconfigured landing can never become an open redirect.
11
- //
12
- // Pure function, no DB — requiring the route module does not open a pool.
4
+ // The walk found that config/branding.neutral.json still documented the
5
+ // platform-home landing as '/gate/' — the orb sky retired with the landing-page
6
+ // front door (task 1002743), a bare 404 on the apex since. The live instance
7
+ // pack already points at '/', so nothing was broken on the box; this pins the
8
+ // fold that keeps an old pack from ever landing a founder on that 404, and the
9
+ // note that no longer sends an operator there.
13
10
 
11
+ import { strict as assert } from 'node:assert';
12
+ import { test } from 'node:test';
14
13
  import { createRequire } from 'node:module';
15
- const require = createRequire(import.meta.url);
16
- const { postLoginLanding } = require('../src/bongos/routes/auth.js');
14
+ import fs from 'node:fs';
15
+ import path from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
17
17
 
18
- let pass = 0, fail = 0;
19
- function ok(cond, msg) {
20
- if (cond) { pass++; console.log(` ok ${msg}`); }
21
- else { fail++; console.log(` FAIL ${msg}`); }
22
- }
23
-
24
- console.log('\npostLoginLanding — portable default is the hall:');
25
- ok(postLoginLanding({}) === '/builders', 'no landing config → /builders');
26
- ok(postLoginLanding({ landing: {} }) === '/builders', 'empty landing block → /builders');
27
- // No-arg path reads the real committed neutral pack, which ships postLogin:'/builders'.
28
- ok(postLoginLanding() === '/builders', 'committed neutral pack → /builders');
18
+ const require = createRequire(import.meta.url);
19
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
20
+ const { postLoginLanding } = require(path.join(ROOT, 'src/bongos/routes/auth.js'));
29
21
 
30
- console.log('\npostLoginLanding — an instance lands visitors on its own front door:');
31
- ok(postLoginLanding({ landing: { postLogin: '/gate/' } }) === '/gate/', 'cloudbongos orbs → /gate/');
32
- ok(postLoginLanding({ landing: { postLogin: '/gate/?entered=1' } }) === '/gate/?entered=1', 'relative path with query preserved');
22
+ test('a pack that still names the retired /gate/ lands on the front door, not a 404', () => {
23
+ assert.equal(postLoginLanding({ landing: { postLogin: '/gate/' } }), '/');
24
+ assert.equal(postLoginLanding({ landing: { postLogin: '/gate' } }), '/');
25
+ });
33
26
 
34
- console.log('\npostLoginLanding — a misconfigured landing can never become an open redirect:');
35
- ok(postLoginLanding({ landing: { postLogin: 'https://evil.com/' } }) === '/builders', 'foreign https origin → fallback');
36
- ok(postLoginLanding({ landing: { postLogin: '//evil.com' } }) === '/builders', 'protocol-relative → fallback');
37
- ok(postLoginLanding({ landing: { postLogin: 'javascript:alert(1)' } }) === '/builders', 'javascript: scheme → fallback');
38
- ok(postLoginLanding({ landing: { postLogin: '/a\\b' } }) === '/builders', 'backslash path → fallback');
39
- ok(postLoginLanding({ landing: { postLogin: 42 } }) === '/builders', 'non-string value → fallback');
27
+ test('every live landing passes through untouched; absent or unsafe falls back to the hall', () => {
28
+ assert.equal(postLoginLanding({ landing: { postLogin: '/' } }), '/', 'cloudbongos.com today');
29
+ assert.equal(postLoginLanding({ landing: { postLogin: '/projects' } }), '/projects');
30
+ assert.equal(postLoginLanding({ landing: { postLogin: '/builders' } }), '/builders');
31
+ assert.equal(postLoginLanding({ landing: {} }), '/builders');
32
+ assert.equal(postLoginLanding({}), '/builders');
33
+ assert.equal(postLoginLanding({ landing: { postLogin: 'https://evil.example/x' } }), '/builders', 'the open-redirect allowlist still governs');
34
+ assert.equal(postLoginLanding({ landing: { postLogin: '//evil.example' } }), '/builders');
35
+ });
40
36
 
41
- console.log(`\nPost-login-landing tests: ${pass} passed, ${fail} failed`);
42
- if (fail) process.exit(1);
37
+ test('the neutral pack no longer documents the retired landing as live', () => {
38
+ const pack = JSON.parse(fs.readFileSync(path.join(ROOT, 'config/branding.neutral.json'), 'utf8'));
39
+ assert.equal(pack.landing.postLogin, '/builders', 'the portable default is unchanged');
40
+ assert.doesNotMatch(pack.landing._note, /'\/gate\/', the orbs/, 'the note stops calling /gate/ a front door');
41
+ assert.match(pack.landing._note, /retired '\/gate\/'/, 'and says what became of it');
42
+ });
@@ -309,3 +309,56 @@ test('decision 10 — both footers carry the open-source (AGPL-3.0) link, in lin
309
309
  assert.ok(indexFoot.indexOf('<!--apex-only:end-->') < indexFoot.search(AGPL), 'index.html: after the apex-only block');
310
310
  assert.match(COSMOS, /\.foot a\{color:var\(--link-ink\);font-weight:700;text-decoration:none;\}/, 'never the browser’s blue');
311
311
  });
312
+
313
+ /* ── the should-fix remainder (task 1003244) ── */
314
+ test('an unmapped create failure gets a plain sentence with the raw text folded under Technical details', () => {
315
+ const friendly = fn('friendlyCreateError');
316
+ assert.match(friendly, /return '';\n\s*\}/, 'unmapped codes return nothing — the caller folds them');
317
+ assert.doesNotMatch(friendly, /Couldn’t file the request: ' \+/, 'the raw message is no longer the headline');
318
+ for (const code of ['slug_taken', 'bad_slug', 'domain_reserved', 'domain_taken', 'bad_domain', 'repo_scope_required', 'request_failed']) {
319
+ assert.match(friendly, new RegExp(`code === '${code}'`), `${code} stays headline-only`);
320
+ }
321
+ assert.match(friendly, /belongs to the platform’s own domain/, 'domain_reserved covers both server branches');
322
+ assert.match(fn('createFailureMarkup'), /Nothing was created — you can try again\.' \+\n\s*'<details><summary>Technical details<\/summary><p>' \+ esc\(err && err\.message/);
323
+ assert.match(SCRIPT, /if \(friendly\) alertBox\.textContent = friendly;\n\s*else alertBox\.innerHTML = createFailureMarkup\(err\);/);
324
+ assert.match(COSMOS, /\.alert details p\{[^}]*font-family:var\(--font-mono\)/, 'the raw text reads as code, inside the alert');
325
+ });
326
+
327
+ test('a manifest error says a plain sentence, folds the route text, and sends a domainless project to its address', () => {
328
+ const steps = fn('renderStepOauthApp');
329
+ assert.doesNotMatch(steps, /'GitHub setup hit a snag' \+ \(live\.manifestMsg \? ': ' \+ esc\(live\.manifestMsg\)/, 'the raw route text is no longer the headline');
330
+ assert.match(steps, /else if \(live\.manifestCode === 'domain_required'\) snag = 'This project needs an address first — attach one from <a href="\?view=mine">your projects page<\/a>/);
331
+ assert.match(steps, /\(raw \? '<details><summary>Technical details<\/summary><p>' \+ esc\(raw\) \+ '<\/p><\/details>' : ''\)/);
332
+ assert.match(SCRIPT, /e\.code = \(d\.error && d\.error\.code\) \|\| '';/, 'the route code travels with the error');
333
+ assert.match(SCRIPT, /live\.manifestCode = String\(\(err && err\.code\) \|\| ''\);/);
334
+ assert.equal((SCRIPT.match(/manifestMsg: '', manifestCode: '', appSlug: null/g) || []).length, 2, 'both resets carry the code');
335
+ });
336
+
337
+ test('the rail at the done panel keeps its marker on Create until the project answers', () => {
338
+ const rail = fn('paintRail');
339
+ assert.match(rail, /var landed = step === 6 && !!\(live && live\.instStatus === 'active'\);/);
340
+ assert.match(rail, /var cur = step === 6 \? 5 : step;/);
341
+ assert.match(rail, /li\.classList\.toggle\('done', landed \|\| n < cur\);/);
342
+ assert.match(rail, /if \(now\) li\.setAttribute\('aria-current', 'step'\); else li\.removeAttribute\('aria-current'\);/);
343
+ assert.match(fn('show'), /paintRail\(step\);/, 'show() paints through it');
344
+ assert.doesNotMatch(fn('show'), /classList\.toggle\('done'/, 'and no longer marks the rail on its own');
345
+ assert.match(SCRIPT, /live\.instStatus = inst\.status; renderSteps\(\);\n\s*if \(state\.step === 6\) paintRail\(6\);/, 'the done-panel poll repaints when the status moves');
346
+ assert.match(HUB, /<li class="now" data-step="1" aria-current="step">/, 'the static rail starts with the same marker');
347
+ });
348
+
349
+ test('card action results announce, nothing under the pointer is deleted, and focus stays on the card', () => {
350
+ assert.match(fn('renderCard'), /id="msg-' \+ iid \+ '" role="status" aria-live="polite">/, 'the card slot is a live region');
351
+ assert.match(HUB, /<div class="projMsg" id="mMsg" role="status" aria-live="polite"><\/div>/, 'so is the manage page’s');
352
+ assert.doesNotMatch(SCRIPT, /act\.innerHTML = '<span class="projMsg">Asking…<\/span>'/, 'no action replaces the row it was pressed in');
353
+ assert.match(fn('doTeardown'), /quietActions\(id\);\n\s*msg\(id, 'Asking…'\);/);
354
+ assert.match(fn('reProvision'), /quietActions\(id\);\n\s*msg\(id, 'Asking…'\);/);
355
+ assert.match(fn('quietActions'), /act\.querySelectorAll\('button'\)\.forEach\(function \(b\) \{ b\.disabled = true; \}\);/);
356
+ assert.match(fn('quietActions'), /li\.setAttribute\('data-focus-back', ''\)/, 'a disabled button that held focus leaves a note');
357
+ assert.match(fn('askTeardown'), /<div class="confirmRow" tabindex="-1">/);
358
+ assert.match(fn('askTeardown'), /if \(row && row\.focus\) row\.focus\(\);/, 'the question takes focus when the button goes');
359
+ const card = fn('renderCard');
360
+ assert.match(card, /var wasInside = li\.contains\(document\.activeElement\) && document\.activeElement !== li;/);
361
+ assert.match(card, /else if \(focusBack\) focusCardControl\(inst\.id, prefer\);/);
362
+ assert.match(fn('focusKeyOf'), /data-teardown-yes'\) \|\| el\.hasAttribute\('data-teardown-no'\)\) return '\[data-teardown\]'/, 'Yes / Keep land back on the door that opened them');
363
+ assert.match(fn('focusCardControl'), /li\.querySelector\('\.actions a, \.actions button'\) \|\|\n\s*li\.querySelector\('details\.attachFold summary'\)/, 'nearest control as the fallback');
364
+ });