@bongos/core 1.19.207 → 1.19.208

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,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.207",
6
- "core_contract": "1.19.207",
7
- "source_commit": "f67179175e23c7d9144c3ede28fd365783eed34f",
5
+ "core_version": "1.19.208",
6
+ "core_contract": "1.19.208",
7
+ "source_commit": "6dabd2e84af8785ac779f8a6801b951d80ecad3b",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-17T20:29:20.492Z",
9
+ "built_at": "2026-08-17T20:55:34.065Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 310,
13
13
  "agent_docs_stubbed": 21,
14
- "functional_verbatim": 1298,
14
+ "functional_verbatim": 1299,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1629,
20
- "tree_sha256": "fb6abbc6818ae6e399e4154a08816d3aeeaf4d9cd65ab89ff1bb3ce3f33428b5",
19
+ "file_count": 1630,
20
+ "tree_sha256": "63df68d9f034ba54b42011a4bfc310a811d2c53cb550a586bf40d5d35228a258",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -187,7 +187,7 @@
187
187
  {
188
188
  "path": ".claude/skills/new-project/SKILL.md",
189
189
  "mode": "0000644",
190
- "sha256": "9b36d441850c335fa634d892088e4c15a2a87acaa9e52808b74a8b2e6e9af39f"
190
+ "sha256": "1d6f60e8fa3cbf06cc5f8901ccf21fd8cac8050f81b3296039b659d700eba242"
191
191
  },
192
192
  {
193
193
  "path": ".claude/skills/otb-character-review/SKILL.md",
@@ -3962,7 +3962,7 @@
3962
3962
  {
3963
3963
  "path": "modules/provisioning/onboard-plan.js",
3964
3964
  "mode": "0000644",
3965
- "sha256": "de7927d2a3c1aac0d40ec5b1ac6c2e98e87cb593c4d3d2bd6ddfe3696371bff6"
3965
+ "sha256": "4aae27c3c56de480e9cc4f19405572a96c6bf0246fdd859adceae629c7a71842"
3966
3966
  },
3967
3967
  {
3968
3968
  "path": "modules/provisioning/pollers/liveness-sweep.js",
@@ -4282,12 +4282,12 @@
4282
4282
  {
4283
4283
  "path": "package-lock.json",
4284
4284
  "mode": "0000644",
4285
- "sha256": "fc64c06226801735546c101af64db40d30492a4ce855303e538bac1111a18930"
4285
+ "sha256": "df481ba4f5057938d85fadf4e5551602cb50c68f78358dc110d03ebfb99ff278"
4286
4286
  },
4287
4287
  {
4288
4288
  "path": "package.json",
4289
4289
  "mode": "0000644",
4290
- "sha256": "d3457496b5c76bab949eb1693518d88745db75eb7bd3afb4299d68e545cdb0bd"
4290
+ "sha256": "5b6943de459044d4545437bd4941abadbdefcedc0063e291f72205e46956beb3"
4291
4291
  },
4292
4292
  {
4293
4293
  "path": "public-docs/index.html",
@@ -4547,7 +4547,7 @@
4547
4547
  {
4548
4548
  "path": "public-landing/projects.html",
4549
4549
  "mode": "0000644",
4550
- "sha256": "494d1aa1a6ff006a8e8f239c29d90c052f735919937c9dc6fb6b8eeff4902e91"
4550
+ "sha256": "20dd4fcb471c8d5a4dced2788ca6fffecaba241207bc0087d918d15b60bcc1a1"
4551
4551
  },
4552
4552
  {
4553
4553
  "path": "public-landing/world/assets/clouds/cloud-island-large.png",
@@ -5447,7 +5447,7 @@
5447
5447
  {
5448
5448
  "path": "scripts/gds/setup.js",
5449
5449
  "mode": "0000644",
5450
- "sha256": "8e512a07ed885666722a216951dfad7f57fbfd92923bdf5079ce1c6f7b5c0abc"
5450
+ "sha256": "98671f5666ae6bdafa7691bd519367345772c3041680b59c24ec83c56b99f419"
5451
5451
  },
5452
5452
  {
5453
5453
  "path": "scripts/gds/shell.js",
@@ -5897,7 +5897,7 @@
5897
5897
  {
5898
5898
  "path": "src/module-api.js",
5899
5899
  "mode": "0000644",
5900
- "sha256": "167ead50611054c2a0d46ebb14510c993e7c05e9c86428cad2d15d78aae0bad4"
5900
+ "sha256": "d20ec520ccfe8d1ac72910455f97f728feaf179b265f79ebf6f9a2ca21f1e424"
5901
5901
  },
5902
5902
  {
5903
5903
  "path": "src/module-loader/catalog.js",
@@ -7382,7 +7382,7 @@
7382
7382
  {
7383
7383
  "path": "tests/onboard.mjs",
7384
7384
  "mode": "0000644",
7385
- "sha256": "c6823a6f0f81bcb4f01ccbd2364d6dcf76eb21e3ff10972738f792438939dee0"
7385
+ "sha256": "71eeb42b47ad2ba11b8379cce263ceac2da42b00b9fe55aab64546027b193364"
7386
7386
  },
7387
7387
  {
7388
7388
  "path": "tests/onboarding_gate.mjs",
@@ -7829,6 +7829,11 @@
7829
7829
  "mode": "0000644",
7830
7830
  "sha256": "a630ac72981f230e8aa6609152765c13e8928ac329cb0ff3fe615371f31208d2"
7831
7831
  },
7832
+ {
7833
+ "path": "tests/setup_device_flow_fallback.mjs",
7834
+ "mode": "0000644",
7835
+ "sha256": "9a9df92b732b4f13d222329877ab17f8f972ef01370c9a918b00635716c76b5e"
7836
+ },
7832
7837
  {
7833
7838
  "path": "tests/setup_smoke.mjs",
7834
7839
  "mode": "0000644",
@@ -9,7 +9,7 @@ You are guiding a new owner from **zero to a live, owned Cloud Bongos instance**
9
9
 
10
10
  A new instance is **standalone** (ADR 0108): it runs from its **own git repo** with `@cloudbongos/core` installed as a **pinned npm dependency**, not a fork of this monorepo. Central updates are a `bongos upgrade` version-pin bump, not a merge. Every step below assumes this shape (`hosting_shape=standalone`).
11
11
 
12
- Five steps are **irreducibly human** and must be rendered as **precise, UI-first, copy-paste-safe prompts** (a non-technical owner does them with browser clicks, no terminal guessing): (1) create the GitHub repo, (2) approve the pre-filled **GitHub App** for sign-in (ONE click — the App Manifest flow, task 2080; no secret to paste), (3) install that App on the repo (ONE click — the publish credential, task 1003049), (4) own a domain, (5) the first sign-in. Everything else is automated. Never ask the owner to do in a terminal what they can do with a click.
12
+ Six steps are **irreducibly human** and must be rendered as **precise, UI-first, copy-paste-safe prompts** (a non-technical owner does them with browser clicks, no terminal guessing): (1) create the GitHub repo, (2) approve the pre-filled **GitHub App** for sign-in (ONE click — the App Manifest flow, task 2080; no secret to paste), (3) tick **Enable Device Flow** in that App's settings (ONE checkbox — terminal sign-in, task 1003051; until then `bongos setup` falls back to a web token), (4) install that App on the repo (ONE click — the publish credential, task 1003049), (5) own a domain, (6) the first sign-in. Everything else is automated. Never ask the owner to do in a terminal what they can do with a click.
13
13
 
14
14
  ## The canonical sequence
15
15
 
@@ -44,6 +44,7 @@ function onboardPlan(mode = 'greenfield', opts = {}) {
44
44
  { key: 'seed-version', kind: 'auto', label: 'Seed the first version (V1) so the instance can take tasks immediately (task 2119)' },
45
45
  { key: 'oauth-app', kind: 'human', label: 'Wire GitHub sign-in — one-click GitHub App (task 2080), or a manual OAuth app' },
46
46
  { key: 'oauth-place', kind: 'auto', shapes: CONTROL_PLANE_SHAPES, label: 'Place the OAuth secret — auto via the Manifest flow, or verified manual (task 1979)' },
47
+ { key: 'device-flow', kind: 'human', label: 'Enable Device Flow in the GitHub App settings — one checkbox, so terminal sign-in (bongos login) works (task 1003051)' },
47
48
  { key: 'app-install', kind: 'human', label: 'Install the GitHub App on the repo — the publish credential that lets shipped work land itself (task 1003049, ADR 0055)' },
48
49
  { key: 'first-sign-in', kind: 'human', label: 'First sign-in (seats the founding Archon)' },
49
50
  { key: 'verify', kind: 'auto', label: 'Verify /healthz auth_configured' },
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.207",
3
+ "version": "1.19.208",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.207",
9
+ "version": "1.19.208",
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.207",
3
+ "version": "1.19.208",
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",
@@ -2861,6 +2861,22 @@ h1[tabindex="-1"]:focus{outline:none;}
2861
2861
  '<button type="button" class="ghbtn" data-action="manifest-start">' + GH_ICON + 'Set up GitHub sign-in</button>' + manualOauthDetails() + '</li>';
2862
2862
  }
2863
2863
 
2864
+ /* device-flow — the one-checkbox repair for terminal sign-in (task 1003051):
2865
+ GitHub creates a manifest-born App with Device Flow OFF and offers no API to
2866
+ enable it, so `bongos setup` in a terminal is refused until the owner ticks
2867
+ the box. Web sign-in works either way; the CLI also has a web-token fallback. */
2868
+ function renderStepDeviceFlow() {
2869
+ if (live.manifest !== 'done') {
2870
+ return '<li class="slocked" data-onboard-step="device-flow"><b>Enable Device Flow — one checkbox</b><p>Unlocks once GitHub sign-in above is wired. It lets builders sign in from a terminal (<code style="font-family:var(--font-mono)">bongos setup</code>); web sign-in works without it.</p></li>';
2871
+ }
2872
+ var appUrl = live.appSlug ? 'https://github.com/settings/apps/' + esc(live.appSlug) : null;
2873
+ return '<li data-onboard-step="device-flow"><b>Enable Device Flow — one checkbox</b><p>' +
2874
+ (appUrl
2875
+ ? 'Open <a href="' + appUrl + '" target="_blank" rel="noopener">your app’s settings</a>, check <strong>Enable Device Flow</strong>, and save. '
2876
+ : 'GitHub → Settings → Developer settings → GitHub Apps → your app → check <strong>Enable Device Flow</strong> → save. ') +
2877
+ 'GitHub leaves it off on new apps and only offers it as this checkbox — until it’s ticked, terminal sign-in falls back to a web token from the hall’s Settings page.</p></li>';
2878
+ }
2879
+
2864
2880
  /* app-install — the ADR 0055 publish credential (task 1003049): the sign-in App
2865
2881
  doubles as it, but only an INSTALLED App can mint push tokens; without this
2866
2882
  one click every finished task waits at confirmed for a manual merge. */
@@ -2894,6 +2910,7 @@ h1[tabindex="-1"]:focus{outline:none;}
2894
2910
  var STEP_RENDERERS = {
2895
2911
  'github-repo': renderStepGithubRepo,
2896
2912
  'oauth-app': renderStepOauthApp,
2913
+ 'device-flow': renderStepDeviceFlow,
2897
2914
  'app-install': renderStepAppInstall,
2898
2915
  'first-sign-in': renderStepFirstSignIn,
2899
2916
  'enter-hall': renderStepEnterHall
@@ -273,7 +273,129 @@ async function promptOnboardingConsent() {
273
273
  }
274
274
  }
275
275
 
276
- (async () => {
276
+ // ── W1: the manifest-born App has Device Flow OFF (task 1003051) ─────────────
277
+ // GitHub creates an App from a manifest with Device Flow unchecked and offers
278
+ // no API to enable it (only a settings checkbox), so on a fresh wizard-born
279
+ // instance the documented CLI sign-in is hard-broken until the owner ticks the
280
+ // box. Detect the proxied refusal (POST /auth/device/start surfaces GitHub's
281
+ // device_flow_disabled — task 2126's envelope) and fall back to the hall's
282
+ // CLI-token path so setup still completes. Pure; exported for tests.
283
+ function isDeviceFlowDisabled(start) {
284
+ if (!start || start.ok) return false;
285
+ const err = start.data && start.data.error;
286
+ const text = [
287
+ typeof err === 'string' ? err : '',
288
+ err && typeof err === 'object' ? `${err.code || ''} ${err.message || ''}` : '',
289
+ start.data && start.data.message ? String(start.data.message) : '',
290
+ ].join(' ');
291
+ return /device_flow_disabled/i.test(text);
292
+ }
293
+
294
+ // The plain-language fallback guidance (task 1003051): what happened, the web
295
+ // path that works right now, and the one-checkbox owner fix. Pure; exported.
296
+ function deviceFlowFallbackLines(base) {
297
+ const settings = `${base}/builders/settings`;
298
+ return [
299
+ '',
300
+ "This project's GitHub App has Device Flow switched off, so terminal sign-in",
301
+ 'cannot start. Apps created by the one-click setup begin this way — GitHub',
302
+ 'only offers the switch as a checkbox in the App settings; no setup flow can',
303
+ 'tick it automatically.',
304
+ '',
305
+ 'Sign in through the web instead (works right now):',
306
+ ` 1. Open ${settings} and sign in with GitHub.`,
307
+ ' 2. Under "CLI Access", click "Re-issue CLI token".',
308
+ ' 3. Copy the token it shows and paste it below (or run the printed',
309
+ ' one-liner in another shell, then re-run setup).',
310
+ '',
311
+ 'To repair terminal sign-in for everyone (the project owner, once):',
312
+ ' GitHub → Settings → Developer settings → GitHub Apps → your app →',
313
+ ' check "Enable Device Flow" → Save. Web sign-in is unaffected either way.',
314
+ ];
315
+ }
316
+
317
+ // Verify a hall-minted CLI token against /me and save the session (the
318
+ // paste-token.js contract: nothing is written unless the token verifies).
319
+ // Returns { ok:true, builder } or { ok:false, status }. Exported for tests.
320
+ async function redeemCliToken(token) {
321
+ let res;
322
+ try {
323
+ res = await fetch(`${API_BASE}/api/gds/me`, { headers: { Authorization: `Bearer ${token}` } });
324
+ } catch (err) {
325
+ console.error(`Could not reach ${API_BASE}: ${err.message}`);
326
+ return { ok: false, status: 0 };
327
+ }
328
+ if (!res.ok) {
329
+ console.error(res.status === 401
330
+ ? `Token rejected by ${API_BASE} (HTTP 401) — copy the full token from the Settings page and try again.`
331
+ : `Verification call failed (HTTP ${res.status}).`);
332
+ return { ok: false, status: res.status };
333
+ }
334
+ const me = await res.json();
335
+ await saveSession({ token, builder: me.builder, api_base: API_BASE });
336
+ return { ok: true, builder: me.builder };
337
+ }
338
+
339
+ // Everything setup does AFTER a session is saved — shared verbatim by the
340
+ // Device Flow success path and the W1 CLI-token fallback, so the fallback
341
+ // completes the SAME onboarding end to end (task 1003051).
342
+ async function finishSetup(builder) {
343
+ console.log('');
344
+ console.log(`Authenticated as ${builder.github_login} (#${builder.id})`);
345
+ if (builder.display_name != null) console.log(`Display name: ${builder.display_name}`);
346
+ if (builder.total_credits != null) console.log(`Total credits: ${builder.total_credits}`);
347
+ console.log(`Session saved to ${SESSION_PATH}`);
348
+ // SEC #863: activate the project git hooks (core.hooksPath → .husky) so
349
+ // the tracked hooks run — here that's the pre-push permission gate
350
+ // (task 1002608); an instance may track more. Best effort — an activation
351
+ // hiccup must never fail an otherwise-good setup; the post-push main
352
+ // audit is the fail-closed net regardless.
353
+ try {
354
+ const { activate } = require('./install-git-hooks');
355
+ const r = activate();
356
+ const live = (r.hooks && r.hooks.length) ? r.hooks.join(', ') : 'none tracked';
357
+ if (r.status === 'activated') console.log(`Git hooks activated (live hooks: ${live})`);
358
+ else if (r.status === 'already-active') console.log(`Git hooks already active (live hooks: ${live})`);
359
+ else console.log(`Note: ${r.message}`);
360
+ } catch (e) {
361
+ console.log(`Note: could not activate git hooks (${e.message}); run: git config core.hooksPath .husky`);
362
+ }
363
+ await promptGeminiKey();
364
+ // #1281: deliver the GDS shared art key to entitled newcomers right now, so
365
+ // a brand-new Xenos's first art task works without waiting for the next
366
+ // session start. Best-effort, bounded — never block setup. (Phase 2 will
367
+ // make the promptGeminiKey copy above rank-aware — "we cover your first art
368
+ // tasks; add your own key when you reach Thetes".)
369
+ try {
370
+ const { spawnSync } = require('node:child_process');
371
+ const path = require('node:path');
372
+ spawnSync(process.execPath, [path.join(__dirname, 'fetch-art-key.js')],
373
+ { stdio: 'ignore', timeout: 8000 });
374
+ } catch (_) { /* non-fatal — the session-start sync will deliver it later */ }
375
+ await promptAnthropicLink();
376
+ await promptDisciplines();
377
+ await promptOnboardingConsent();
378
+ // #766: explicit "what's next?" footer. The Xenos path is action-only —
379
+ // no primer to read first — so a newcomer goes straight from setup to
380
+ // surveying the works. Ship three and the city makes you a Thetes.
381
+ try {
382
+ const meRes = await (await cliClient()).me.getMe();
383
+ if (meRes.ok) {
384
+ console.log('');
385
+ printOnboardingNudge(meRes.data);
386
+ console.log('');
387
+ console.log('Newcomer path:');
388
+ console.log(` 1. Choose thy craft: open ${hallBase()} (creative / engineering)`);
389
+ console.log(' 2. Survey work: /builder-start');
390
+ console.log(' 3. Claim + ship: /builder-claim N → /builder-ship');
391
+ console.log(' 4. Ship three works and you rise to Thetes — the whole queue opens.');
392
+ }
393
+ } catch (_) {
394
+ // never let a UX footer break a successful setup
395
+ }
396
+ }
397
+
398
+ async function main() {
277
399
  const forceFlag = process.argv.includes('--force');
278
400
 
279
401
  // Environment preflight (task 1002401, onboarding-audit area 4). Before auth,
@@ -310,6 +432,7 @@ async function promptOnboardingConsent() {
310
432
  dryRunStep(`No existing session at ${SESSION_PATH}; would proceed straight to Device Flow.`);
311
433
  }
312
434
  dryRunStep('Would POST /api/gds/auth/device/start to get a user_code + verification_uri from GitHub.');
435
+ dryRunStep('If GitHub refuses with device_flow_disabled (a one-click App whose checkbox is unticked, task 1003051): would print the web sign-in guidance (hall Settings → CLI Access) and prompt for a pasted CLI token instead of dead-ending.');
313
436
  dryRunStep('Would print the verification_uri and user_code, then poll /api/gds/auth/device/poll every few seconds until the user approves or the code expires.');
314
437
  dryRunStep(`On approval, would write the bearer token + builder profile to ${SESSION_PATH} (chmod 600).`);
315
438
  if (SKIP_KEYS) {
@@ -448,6 +571,31 @@ async function promptOnboardingConsent() {
448
571
  console.log('Starting GitHub Device Flow...');
449
572
  const start = await (await cliClient()).auth.postAuthDeviceStart();
450
573
  if (!start.ok) {
574
+ // W1 (task 1003051): a manifest-born App refuses Device Flow until its owner
575
+ // ticks the checkbox — fall back to the hall's CLI-token path instead of
576
+ // dead-ending the documented setup command.
577
+ if (isDeviceFlowDisabled(start)) {
578
+ for (const line of deviceFlowFallbackLines(API_BASE)) console.log(line);
579
+ console.log('');
580
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
581
+ // Resolve '' on stdin EOF too, so a non-interactive run exits with the
582
+ // guidance instead of hanging on a question nobody can answer.
583
+ const token = (await new Promise((resolve) => {
584
+ rl.question('Paste the CLI token here (or press Enter to quit): ', resolve);
585
+ rl.once('close', () => resolve(''));
586
+ })).trim();
587
+ rl.close();
588
+ if (!token) {
589
+ console.error('No token pasted — run the Settings page one-liner, then re-run setup.');
590
+ process.exit(1);
591
+ }
592
+ const redeemed = await redeemCliToken(token);
593
+ if (!redeemed.ok) process.exit(1);
594
+ console.log('');
595
+ console.log('Signed in via the web token — terminal Device Flow stays broken until the owner ticks the checkbox.');
596
+ await finishSetup(redeemed.builder);
597
+ process.exit(0);
598
+ }
451
599
  console.error(`device/start failed (${start.status}):`, start.data);
452
600
  if (start.data?.error === 'auth_not_configured') {
453
601
  console.error('');
@@ -509,65 +657,19 @@ async function promptOnboardingConsent() {
509
657
  const { token, builder } = poll.data;
510
658
  await saveSession({ token, builder, api_base: API_BASE });
511
659
  console.log('');
512
- console.log('');
513
- console.log(`Authenticated as ${builder.github_login} (#${builder.id})`);
514
- console.log(`Display name: ${builder.display_name}`);
515
- console.log(`Total credits: ${builder.total_credits}`);
516
- console.log(`Session saved to ${SESSION_PATH}`);
517
- // SEC #863: activate the project git hooks (core.hooksPath → .husky) so
518
- // the tracked hooks run — here that's the pre-push permission gate
519
- // (task 1002608); an instance may track more. Best effort — an activation
520
- // hiccup must never fail an otherwise-good setup; the post-push main
521
- // audit is the fail-closed net regardless.
522
- try {
523
- const { activate } = require('./install-git-hooks');
524
- const r = activate();
525
- const live = (r.hooks && r.hooks.length) ? r.hooks.join(', ') : 'none tracked';
526
- if (r.status === 'activated') console.log(`Git hooks activated (live hooks: ${live})`);
527
- else if (r.status === 'already-active') console.log(`Git hooks already active (live hooks: ${live})`);
528
- else console.log(`Note: ${r.message}`);
529
- } catch (e) {
530
- console.log(`Note: could not activate git hooks (${e.message}); run: git config core.hooksPath .husky`);
531
- }
532
- await promptGeminiKey();
533
- // #1281: deliver the GDS shared art key to entitled newcomers right now, so
534
- // a brand-new Xenos's first art task works without waiting for the next
535
- // session start. Best-effort, bounded — never block setup. (Phase 2 will
536
- // make the promptGeminiKey copy above rank-aware — "we cover your first art
537
- // tasks; add your own key when you reach Thetes".)
538
- try {
539
- const { spawnSync } = require('node:child_process');
540
- const path = require('node:path');
541
- spawnSync(process.execPath, [path.join(__dirname, 'fetch-art-key.js')],
542
- { stdio: 'ignore', timeout: 8000 });
543
- } catch (_) { /* non-fatal — the session-start sync will deliver it later */ }
544
- await promptAnthropicLink();
545
- await promptDisciplines();
546
- await promptOnboardingConsent();
547
- // #766: explicit "what's next?" footer. The Xenos path is action-only —
548
- // no primer to read first — so a newcomer goes straight from setup to
549
- // surveying the works. Ship three and the city makes you a Thetes.
550
- try {
551
- const meRes = await (await cliClient()).me.getMe();
552
- if (meRes.ok) {
553
- console.log('');
554
- printOnboardingNudge(meRes.data);
555
- console.log('');
556
- console.log('Newcomer path:');
557
- console.log(` 1. Choose thy craft: open ${hallBase()} (creative / engineering)`);
558
- console.log(' 2. Survey work: /builder-start');
559
- console.log(' 3. Claim + ship: /builder-claim N → /builder-ship');
560
- console.log(' 4. Ship three works and you rise to Thetes — the whole queue opens.');
561
- }
562
- } catch (_) {
563
- // never let a UX footer break a successful setup
564
- }
660
+ await finishSetup(builder);
565
661
  process.exit(0);
566
662
  }
567
663
  }
568
664
  console.error('\nTimed out waiting for approval.');
569
665
  process.exit(1);
570
- })().catch((err) => {
571
- console.error('fatal:', err);
572
- process.exit(1);
573
- });
666
+ }
667
+
668
+ if (require.main === module) {
669
+ main().catch((err) => {
670
+ console.error('fatal:', err);
671
+ process.exit(1);
672
+ });
673
+ }
674
+
675
+ module.exports = { isDeviceFlowDisabled, deviceFlowFallbackLines, redeemCliToken };
package/src/module-api.js CHANGED
@@ -841,8 +841,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
841
841
  // landed since 1.19.205 with no explicit bump. run 32064728346. (task 1002620)
842
842
  // 1.19.207 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
843
843
  // landed since 1.19.206 with no explicit bump. run 32065995444. (task 1002620)
844
+ // 1.19.208 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
845
+ // landed since 1.19.207 with no explicit bump. run 32068332007. (task 1002620)
844
846
  // ---------------------------------------------------------------------------
845
- const CORE_VERSION = '1.19.207'; // CI auto-patch carrier (ADR 0161); see changelog above
847
+ const CORE_VERSION = '1.19.208'; // CI auto-patch carrier (ADR 0161); see changelog above
846
848
 
847
849
  // A namespaced logger so a module's log lines are attributable + consistent.
848
850
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
package/tests/onboard.mjs CHANGED
@@ -15,7 +15,7 @@ test('onboardPlan is the canonical sequence: name+address first, the builders ha
15
15
  assert.equal(keys[keys.length - 2], 'brand');
16
16
  // the irreducibly-human steps (must be UI-first prompts)
17
17
  const human = plan.filter((s) => s.kind === 'human').map((s) => s.key);
18
- assert.deepEqual(human, ['github-repo', 'oauth-app', 'app-install', 'first-sign-in', 'enter-hall']);
18
+ assert.deepEqual(human, ['github-repo', 'oauth-app', 'device-flow', 'app-install', 'first-sign-in', 'enter-hall']);
19
19
  // the DNS preflight comes BEFORE provision (task 1980: before the address step)
20
20
  assert.ok(keys.indexOf('dns-preflight') < keys.indexOf('provision'), 'dns preflight precedes provision');
21
21
  // OAuth secret placement comes after the OAuth app + before first sign-in
@@ -25,6 +25,10 @@ test('onboardPlan is the canonical sequence: name+address first, the builders ha
25
25
  // must be prompted before the owner walks away into the hall.
26
26
  assert.ok(keys.indexOf('oauth-place') < keys.indexOf('app-install'), 'the App exists before it can be installed');
27
27
  assert.ok(keys.indexOf('app-install') < keys.indexOf('first-sign-in'), 'install prompted before sign-in');
28
+ // The Device Flow checkbox (task 1003051 / gap W1) also needs the App to exist,
29
+ // and belongs with the sign-in wiring it repairs.
30
+ assert.ok(keys.indexOf('oauth-place') < keys.indexOf('device-flow'), 'the App exists before its checkbox can be ticked');
31
+ assert.ok(keys.indexOf('device-flow') < keys.indexOf('first-sign-in'), 'checkbox prompted before sign-in');
28
32
  });
29
33
 
30
34
  test('onboardPlan(adopt): same sequence + human steps, but the scaffold-leg labels branch (task 2046)', () => {
@@ -0,0 +1,179 @@
1
+ // tests/setup_device_flow_fallback.mjs
2
+ //
3
+ // W1 (task 1003051): on a wizard-born instance the GitHub App has Device Flow
4
+ // OFF (the manifest can't enable it, GitHub offers only a settings checkbox),
5
+ // so `bongos setup` — the documented CLI sign-in — dead-ended on a proxied
6
+ // GitHub refusal. setup.js now detects that exact refusal and falls back to the
7
+ // hall's CLI-token path.
8
+ //
9
+ // Covers the fallback branch's machinery, each leg against the stubbed refusal
10
+ // or a stubbed instance API:
11
+ // • isDeviceFlowDisabled — the detector against the REAL server envelope
12
+ // (device_start_failed 502 carrying GitHub's device_flow_disabled), a legacy
13
+ // flat shape, and the shapes it must NOT match (auth_not_configured, other
14
+ // upstream failures, success)
15
+ // • deviceFlowFallbackLines — the guidance names the hall Settings page,
16
+ // CLI Access, and the owner's one-checkbox repair
17
+ // • redeemCliToken — driven in a CHILD process (hermetic HOME/USERPROFILE +
18
+ // GDS_API_BASE at a local stub): a good token verifies via /me and writes
19
+ // the session; a bad token is refused and writes NOTHING
20
+ //
21
+ // The interactive TTY walk (prompt → paste) is deliberately not spawned here:
22
+ // the spawn-the-whole-CLI harness (setup_smoke.mjs) is already environment-
23
+ // sensitive, and every seam the prompt wires together is covered above.
24
+ //
25
+ // Run: node tests/setup_device_flow_fallback.mjs
26
+
27
+ import { strict as assert } from 'node:assert';
28
+ import { createRequire } from 'node:module';
29
+ import { spawn } from 'node:child_process';
30
+ import http from 'node:http';
31
+ import fs from 'node:fs';
32
+ import os from 'node:os';
33
+ import path from 'node:path';
34
+ import { fileURLToPath } from 'node:url';
35
+
36
+ const require = createRequire(import.meta.url);
37
+ const setup = require('../scripts/gds/setup.js');
38
+
39
+ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
40
+ const SETUP_JS = path.join(REPO_ROOT, 'scripts', 'gds', 'setup.js');
41
+
42
+ let passed = 0, failed = 0;
43
+ const t = (name, fn) => {
44
+ try { fn(); passed++; console.log(` PASS ${name}`); }
45
+ catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
46
+ };
47
+ const ta = async (name, fn) => {
48
+ try { await fn(); passed++; console.log(` PASS ${name}`); }
49
+ catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
50
+ };
51
+
52
+ console.log('isDeviceFlowDisabled — the refusal detector:');
53
+
54
+ // The REAL shape: routes/auth.js res.fail('device_start_failed', {status:502,
55
+ // message: 'GitHub rejected device-flow start: device_flow_disabled — …'}).
56
+ const REAL_REFUSAL = {
57
+ ok: false, status: 502,
58
+ data: { error: { code: 'device_start_failed', message: 'GitHub rejected device-flow start: device_flow_disabled — terminal sign-in needs Device Flow enabled on the app: open https://github.com/settings/apps, open your app, check "Enable Device Flow", and save. Web sign-in is unaffected.' } },
59
+ };
60
+
61
+ t('matches the real device_start_failed envelope carrying device_flow_disabled', () => {
62
+ assert.equal(setup.isDeviceFlowDisabled(REAL_REFUSAL), true);
63
+ });
64
+
65
+ t('matches a legacy flat error shape too', () => {
66
+ assert.equal(setup.isDeviceFlowDisabled({ ok: false, status: 502, data: { error: 'device_start_failed', message: 'x device_flow_disabled y' } }), true);
67
+ });
68
+
69
+ t('does NOT match auth_not_configured, other upstream failures, or success', () => {
70
+ assert.equal(setup.isDeviceFlowDisabled({ ok: false, status: 503, data: { error: { code: 'auth_not_configured', message: 'GitHub OAuth App credentials missing on the server.' } } }), false);
71
+ assert.equal(setup.isDeviceFlowDisabled({ ok: false, status: 502, data: { error: { code: 'device_start_failed', message: 'GitHub rejected device-flow start: incorrect_client_credentials' } } }), false);
72
+ assert.equal(setup.isDeviceFlowDisabled({ ok: true, status: 200, data: { device_code: 'x' } }), false);
73
+ assert.equal(setup.isDeviceFlowDisabled(null), false);
74
+ });
75
+
76
+ console.log('\ndeviceFlowFallbackLines — the guidance:');
77
+
78
+ t('names the Settings page, CLI Access, and the owner checkbox repair', () => {
79
+ const text = setup.deviceFlowFallbackLines('https://spike.cloudbongos.com').join('\n');
80
+ assert.match(text, /https:\/\/spike\.cloudbongos\.com\/builders\/settings/, 'points at the hall Settings page');
81
+ assert.match(text, /CLI Access/, 'names the section');
82
+ assert.match(text, /Re-issue CLI token/, 'names the button');
83
+ assert.match(text, /Enable Device Flow/, 'names the owner checkbox');
84
+ assert.match(text, /Web sign-in is unaffected/i, 'says what still works');
85
+ });
86
+
87
+ // ── redeemCliToken in a hermetic child against a stubbed instance API ────────
88
+ // A child process because API_BASE + the session path resolve at module load:
89
+ // the child gets GDS_API_BASE → the stub and HOME/USERPROFILE → a throwaway
90
+ // dir, so the real session can never be touched. The driver prints the JSON
91
+ // result and lets node exit naturally (no process.exit — the Windows async.c
92
+ // abort gotcha), so assertions read stdout, not the exit code.
93
+
94
+ const GOOD_TOKEN = 'a'.repeat(64);
95
+ const server = http.createServer((req, res) => {
96
+ const send = (status, body) => {
97
+ res.writeHead(status, { 'Content-Type': 'application/json' });
98
+ res.end(JSON.stringify(body));
99
+ };
100
+ if (req.method === 'GET' && req.url.endsWith('/api/gds/me')) {
101
+ if ((req.headers.authorization || '') === `Bearer ${GOOD_TOKEN}`) {
102
+ return send(200, { builder: { id: 6, github_login: 'newbuilder', display_name: 'New Builder' } });
103
+ }
104
+ return send(401, { error: { code: 'unauthenticated' } });
105
+ }
106
+ return send(404, { error: { code: 'not_found' } });
107
+ });
108
+ server.listen(0, '127.0.0.1');
109
+ await new Promise((r) => server.once('listening', r));
110
+ const BASE = `http://127.0.0.1:${server.address().port}`;
111
+
112
+ const driverPath = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'w1-driver-')), 'driver.cjs');
113
+ fs.writeFileSync(driverPath, [
114
+ `const s = require(${JSON.stringify(SETUP_JS)});`,
115
+ `s.redeemCliToken(process.argv[2]).then((r) => { console.log('RESULT ' + JSON.stringify(r)); });`,
116
+ ].join('\n'));
117
+
118
+ // ASYNC spawn, not spawnSync: the stub server lives in THIS process, and a sync
119
+ // spawn would block the event loop — the child's fetch would deadlock waiting
120
+ // for a server that can never accept.
121
+ function redeemInChild(token, home) {
122
+ return new Promise((resolve) => {
123
+ const child = spawn(process.execPath, [driverPath, token], {
124
+ env: { ...process.env, HOME: home, USERPROFILE: home, GDS_API_BASE: BASE, OTB_API_BASE: BASE },
125
+ });
126
+ let stdout = '', stderr = '';
127
+ child.stdout.on('data', (c) => { stdout += c; });
128
+ child.stderr.on('data', (c) => { stderr += c; });
129
+ const timer = setTimeout(() => child.kill(), 30_000);
130
+ child.on('close', (code, signal) => {
131
+ clearTimeout(timer);
132
+ const line = stdout.split('\n').find((l) => l.startsWith('RESULT '));
133
+ resolve({ result: line ? JSON.parse(line.slice(7)) : null, raw: { stdout, stderr, code, signal } });
134
+ });
135
+ });
136
+ }
137
+
138
+ function findSessions(home) {
139
+ const hits = [];
140
+ (function walk(dir) {
141
+ let entries = [];
142
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
143
+ for (const f of entries) {
144
+ const p = path.join(dir, f.name);
145
+ if (f.isDirectory()) walk(p);
146
+ else if (f.name === 'gds-session.json') hits.push(p);
147
+ }
148
+ })(home);
149
+ return hits;
150
+ }
151
+
152
+ console.log('\nredeemCliToken — verify-then-save against the stub:');
153
+
154
+ await ta('a good token verifies via /me and writes the session under the hermetic HOME', async () => {
155
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'w1-home-'));
156
+ const { result, raw } = await redeemInChild(GOOD_TOKEN, home);
157
+ assert.ok(result, `driver printed no RESULT\nstdout:\n${raw.stdout}\nstderr:\n${raw.stderr}`);
158
+ assert.equal(result.ok, true);
159
+ assert.equal(result.builder.github_login, 'newbuilder');
160
+ const sessions = findSessions(home);
161
+ assert.equal(sessions.length, 1, `exactly one session file expected under ${home}`);
162
+ const session = JSON.parse(fs.readFileSync(sessions[0], 'utf8'));
163
+ assert.equal(session.token, GOOD_TOKEN);
164
+ assert.equal(session.api_base, BASE, 'the session points at the instance that minted the token');
165
+ });
166
+
167
+ await ta('a bad token is refused (401) and NOTHING is written', async () => {
168
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'w1-home-'));
169
+ const { result, raw } = await redeemInChild('b'.repeat(64), home);
170
+ assert.ok(result, `driver printed no RESULT\nstdout:\n${raw.stdout}\nstderr:\n${raw.stderr}`);
171
+ assert.equal(result.ok, false);
172
+ assert.equal(result.status, 401);
173
+ assert.match(raw.stderr, /Token rejected/, 'says the token was refused');
174
+ assert.equal(findSessions(home).length, 0, 'no session written for an unverified token');
175
+ });
176
+
177
+ server.close();
178
+ console.log(`\nsetup_device_flow_fallback: ${passed} passed, ${failed} failed`);
179
+ process.exit(failed ? 1 : 0);