@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 +20 -15
- package/.claude/skills/new-project/SKILL.md +1 -1
- package/modules/provisioning/onboard-plan.js +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/projects.html +17 -0
- package/scripts/gds/setup.js +160 -58
- package/src/module-api.js +3 -1
- package/tests/onboard.mjs +5 -1
- package/tests/setup_device_flow_fallback.mjs +179 -0
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.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
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:
|
|
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":
|
|
14
|
+
"functional_verbatim": 1299,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
4285
|
+
"sha256": "df481ba4f5057938d85fadf4e5551602cb50c68f78358dc110d03ebfb99ff278"
|
|
4286
4286
|
},
|
|
4287
4287
|
{
|
|
4288
4288
|
"path": "package.json",
|
|
4289
4289
|
"mode": "0000644",
|
|
4290
|
-
"sha256": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
package/scripts/gds/setup.js
CHANGED
|
@@ -273,7 +273,129 @@ async function promptOnboardingConsent() {
|
|
|
273
273
|
}
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
-
(
|
|
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
|
-
|
|
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
|
-
}
|
|
571
|
-
|
|
572
|
-
|
|
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.
|
|
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);
|