@bongos/core 1.25.23 → 1.25.25
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 +32 -27
- package/docs/copy-inventory.md +257 -241
- package/docs/copy-registry.json +426 -272
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +195 -189
- package/modules/hall-ui/public/genesis-home.js +98 -16
- package/modules/onboarding/genesis-home.js +64 -4
- package/modules/onboarding/genesis-work.js +65 -10
- package/modules/onboarding/routes/genesis-work.js +2 -1
- package/modules/public-landing/public/projects.html +6 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/bongos/auth-github.js +17 -0
- package/src/bongos/auth.js +2 -1
- package/src/bongos/routes/auth.js +7 -7
- package/src/module-api.js +1 -1
- package/tests/auth_switch_account.mjs +3 -7
- package/tests/genesis_home.mjs +115 -2
- package/tests/genesis_work.mjs +38 -0
- package/tests/genesis_work_routes.mjs +21 -0
- package/tests/github_signup_handoff.mjs +135 -0
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
stage_tasks_unfinished: 'This stage still has unfinished tasks.',
|
|
31
31
|
not_current_stage: 'That stage is already behind you.',
|
|
32
32
|
legislation_not_passed: 'Legislation has to pass the Board Room first.',
|
|
33
|
-
|
|
33
|
+
stage_closed: 'The Board Room has passed that stage, so its answers are fixed.',
|
|
34
|
+
stage_lock_unknown: 'The Board Room could not be read, so nothing was changed. Try again in a moment.',
|
|
34
35
|
};
|
|
35
36
|
|
|
36
37
|
// What saving the plan did to the government, when it was not settled in the same
|
|
@@ -99,9 +100,17 @@
|
|
|
99
100
|
|
|
100
101
|
// ── decide ────────────────────────────────────────────────────────────────
|
|
101
102
|
|
|
103
|
+
// Task 1005107: the same form REVISES a saved plan — it opens with the founder's own
|
|
104
|
+
// answers, and saving the same plan again changes nothing on the server.
|
|
102
105
|
function decideHtml(g, gov) {
|
|
106
|
+
var p = g.plan || null;
|
|
107
|
+
if (p && p.government && gov) gov = Object.assign({}, gov, { recommended: { key: p.government } });
|
|
108
|
+
if (p && p.expected_builders && gov) gov = Object.assign({}, gov, { headcount: p.expected_builders });
|
|
103
109
|
var rows = g.stages.map(function (s) {
|
|
104
|
-
|
|
110
|
+
// A saved length that is not one of the presets is kept as its own choice, so
|
|
111
|
+
// reopening the plan never quietly changes it (task 1005107).
|
|
112
|
+
var choices = DAY_CHOICES.indexOf(s.days) < 0 ? DAY_CHOICES.concat([s.days]).sort(function (x, y) { return x - y; }) : DAY_CHOICES;
|
|
113
|
+
var opts = choices.map(function (d) {
|
|
105
114
|
return '<option value="' + d + '"' + (d === s.days ? ' selected' : '') + '>' + d + (d === 1 ? ' day' : ' days') + '</option>';
|
|
106
115
|
}).join('');
|
|
107
116
|
return '<li class="genesis__plan-row">'
|
|
@@ -110,22 +119,25 @@
|
|
|
110
119
|
+ '<label class="genesis__plan-len"><span class="sr-only">Length of ' + esc(s.label) + '</span>'
|
|
111
120
|
+ '<select data-days="' + esc(s.key) + '">' + opts + '</select></label></li>';
|
|
112
121
|
}).join('');
|
|
113
|
-
return
|
|
114
|
-
|
|
122
|
+
return (p
|
|
123
|
+
? '<p class="scroll__lede">Change any of your answers. Due dates follow the new lengths; a new government goes to the board like any charter change.</p>'
|
|
124
|
+
: '<p class="scroll__lede">Settle the plan and each stage becomes real tasks. '
|
|
125
|
+
+ 'Everything is preset to our recommendation, so one click is enough.</p>')
|
|
115
126
|
+ '<ol class="genesis__plan">' + rows + '</ol>'
|
|
116
127
|
+ '<div class="genesis__picks">'
|
|
117
128
|
+ governmentHtml(gov)
|
|
118
129
|
+ '<fieldset class="genesis__pick"><legend>How many passes</legend>'
|
|
119
|
-
+ radio('passes', '1', 'One', 'Each stage is decided once.',
|
|
120
|
-
+ radio('passes', '2', 'Two', 'Once now, and again when your next builders join.',
|
|
130
|
+
+ radio('passes', '1', 'One', 'Each stage is decided once.', !p || p.passes !== 2)
|
|
131
|
+
+ radio('passes', '2', 'Two', 'Once now, and again when your next builders join.', !!p && p.passes === 2)
|
|
121
132
|
+ '</fieldset>'
|
|
122
133
|
+ '<fieldset class="genesis__pick"><legend>Advisors</legend>'
|
|
123
|
-
+ radio('advisors', 'none', 'None for now', 'You can add advisors at any stage.',
|
|
124
|
-
+ radio('advisors', 'cbaas', 'Cloud Bongos advisors', 'Our consultants review each stage.',
|
|
134
|
+
+ radio('advisors', 'none', 'None for now', 'You can add advisors at any stage.', !p || p.advisors !== 'cbaas')
|
|
135
|
+
+ radio('advisors', 'cbaas', 'Cloud Bongos advisors', 'Our consultants review each stage.', !!p && p.advisors === 'cbaas')
|
|
125
136
|
+ '</fieldset>'
|
|
126
|
-
+ '<label class="genesis__check"><input type="checkbox" data-existing> It already exists, so gather what it has first</label>'
|
|
137
|
+
+ '<label class="genesis__check"><input type="checkbox" data-existing' + (p && p.existing ? ' checked' : '') + '> It already exists, so gather what it has first</label>'
|
|
127
138
|
+ '</div>'
|
|
128
|
-
+ '<div class="genesis__doors"><button type="button" class="btn-accent" data-genesis-plan>Use this plan</button
|
|
139
|
+
+ '<div class="genesis__doors"><button type="button" class="btn-accent" data-genesis-plan>' + (p ? 'Save the plan' : 'Use this plan') + '</button>'
|
|
140
|
+
+ (p ? '<button type="button" class="btn-ghost" data-genesis-plan-cancel>Keep it as it was</button>' : '') + '</div>';
|
|
129
141
|
}
|
|
130
142
|
|
|
131
143
|
function radio(name, value, label, hint, checked) {
|
|
@@ -157,6 +169,47 @@
|
|
|
157
169
|
+ '</fieldset>';
|
|
158
170
|
}
|
|
159
171
|
|
|
172
|
+
// ── changing a finished stage's answers (task 1005107) ─────────────────────
|
|
173
|
+
// The stage's own record (GET /founding/work/:stage), opened in place with the answers
|
|
174
|
+
// already given. Save sends only what changed; nothing changed sends nothing.
|
|
175
|
+
function stageEditorHtml(w) {
|
|
176
|
+
if (!w) return '';
|
|
177
|
+
var rows = (w.questions || []).map(function (q) {
|
|
178
|
+
var cur = q.answer && q.answer.state === 'answered' ? (q.answer.text || '') : '';
|
|
179
|
+
var field = q.choices
|
|
180
|
+
? '<select data-answer="' + esc(q.id) + '"><option value=""' + (cur ? '' : ' selected') + '>Not answered</option>'
|
|
181
|
+
+ q.choices.map(function (c) { return '<option value="' + esc(c.key) + '"' + (c.key === cur ? ' selected' : '') + '>' + esc(c.label) + '</option>'; }).join('') + '</select>'
|
|
182
|
+
: '<textarea rows="3" data-answer="' + esc(q.id) + '">' + esc(cur) + '</textarea>';
|
|
183
|
+
return '<label class="genesis__edit-q"><b>' + esc(q.ask) + '</b>' + field + '</label>';
|
|
184
|
+
}).join('');
|
|
185
|
+
return '<section class="genesis__edit" aria-label="Change the ' + esc(w.stage) + ' answers">'
|
|
186
|
+
+ '<h3 class="genesis__now-h">Change the ' + esc(w.stage) + ' answers</h3>'
|
|
187
|
+
+ '<p class="genesis__note">Changing them does not reopen the stage.</p>' + rows
|
|
188
|
+
+ '<label class="genesis__edit-q"><b>In your own words</b><textarea rows="5" data-page>' + esc(w.page || '') + '</textarea></label>'
|
|
189
|
+
+ '<div class="genesis__doors"><button type="button" class="btn-accent" data-genesis-stage-save>Save changes</button>'
|
|
190
|
+
+ '<button type="button" class="btn-ghost" data-genesis-stage-cancel>Leave it as it was</button></div></section>';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// stageChanges(w, host) → the writes the edited form needs, only for what changed.
|
|
194
|
+
function stageChanges(w, host) {
|
|
195
|
+
var out = [];
|
|
196
|
+
(w.questions || []).forEach(function (q) {
|
|
197
|
+
var el = host.querySelector('[data-answer="' + q.id + '"]');
|
|
198
|
+
if (!el) return;
|
|
199
|
+
var was = q.answer && q.answer.state === 'answered' ? (q.answer.text || '') : '';
|
|
200
|
+
var now = String(el.value || '').trim();
|
|
201
|
+
if (now === was.trim()) return;
|
|
202
|
+
var path = '/founding/work/' + encodeURIComponent(w.stage) + '/answers/' + encodeURIComponent(q.id);
|
|
203
|
+
if (now) out.push(['PUT', path, { state: 'answered', text: now }]);
|
|
204
|
+
else if (q.answer) out.push(['DELETE', path, null]);
|
|
205
|
+
});
|
|
206
|
+
var page = host.querySelector('[data-page]');
|
|
207
|
+
if (page && String(page.value || '').trim() !== String(w.page || '').trim()) {
|
|
208
|
+
out.push(['PUT', '/founding/work/' + encodeURIComponent(w.stage) + '/page', { page: String(page.value || '') }]);
|
|
209
|
+
}
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
|
|
160
213
|
// ── the stages ────────────────────────────────────────────────────────────
|
|
161
214
|
|
|
162
215
|
function lifeHtml(g) {
|
|
@@ -207,9 +260,15 @@
|
|
|
207
260
|
// buildings it raises, and for a stage that wakes no room, what it earns.
|
|
208
261
|
+ '<span>' + esc(gainsText(s)) + esc(due) + '</span>'
|
|
209
262
|
+ (s.state === 'later' && s.can_close ? '<span><button type="button" class="btn-ghost" data-genesis-close="' + esc(s.key) + '">Put to the board</button></span>' : '')
|
|
263
|
+
// Task 1005107: a finished stage stays editable unless the Board Room passed it.
|
|
264
|
+
+ (s.state === 'done' ? (s.locked
|
|
265
|
+
? '<span class="genesis__stage-fixed">' + esc('Fixed: ' + s.locked.reason) + '</span>'
|
|
266
|
+
: '<span><button type="button" class="btn-ghost" data-genesis-stage-edit="' + esc(s.key) + '">Change its answers</button></span>') : '')
|
|
210
267
|
+ '</span>'
|
|
211
268
|
+ '<span class="genesis__stage-st">' + esc(stateText(s)) + '</span></li>';
|
|
212
|
-
}).join('') + '</ol>'
|
|
269
|
+
}).join('') + '</ol>'
|
|
270
|
+
// Task 1005107: the plan is never a trap — reopen it with the answers already given.
|
|
271
|
+
+ (g.plan_editable && !g.demo ? '<div class="genesis__doors"><button type="button" class="btn-ghost" data-genesis-plan-edit>Change the plan</button></div>' : '');
|
|
213
272
|
}
|
|
214
273
|
|
|
215
274
|
// task 1005109: Copy prompt on each task still to do, so the founder pastes it into
|
|
@@ -382,9 +441,10 @@
|
|
|
382
441
|
function render(host, state) {
|
|
383
442
|
var g = state.genesis;
|
|
384
443
|
if (!g) { host.hidden = true; return; }
|
|
385
|
-
host.innerHTML = headHtml(g) + (g.deciding
|
|
386
|
-
? decideHtml(g, state.gov)
|
|
387
|
-
: demoHtml(g) + lifeHtml(g) + votesHtml(g, state.viewer) + '<div class="genesis__grid">' + stagesHtml(g) + nowHtml(g, state.viewer) + '</div>'
|
|
444
|
+
host.innerHTML = headHtml(g) + (g.deciding || state.editingPlan
|
|
445
|
+
? decideHtml(g.deciding ? Object.assign({}, g, { plan: null }) : g, state.gov)
|
|
446
|
+
: demoHtml(g) + lifeHtml(g) + votesHtml(g, state.viewer) + '<div class="genesis__grid">' + stagesHtml(g) + nowHtml(g, state.viewer) + '</div>'
|
|
447
|
+
+ stageEditorHtml(state.stageWork));
|
|
388
448
|
host.hidden = false;
|
|
389
449
|
}
|
|
390
450
|
|
|
@@ -449,22 +509,44 @@
|
|
|
449
509
|
if (!t || !host.contains(t)) return;
|
|
450
510
|
if (t.hasAttribute('data-genesis-copy')) {
|
|
451
511
|
copyPrompt(t, state);
|
|
512
|
+
} else if (t.hasAttribute('data-genesis-stage-edit')) {
|
|
513
|
+
act(send('GET', '/founding/work/' + encodeURIComponent(t.getAttribute('data-genesis-stage-edit'))).then(function (d) {
|
|
514
|
+
state.stageWork = d && d.work; render(host, state);
|
|
515
|
+
}));
|
|
516
|
+
} else if (t.hasAttribute('data-genesis-stage-cancel')) {
|
|
517
|
+
state.stageWork = null;
|
|
518
|
+
render(host, state);
|
|
519
|
+
} else if (t.hasAttribute('data-genesis-stage-save')) {
|
|
520
|
+
var writes = stageChanges(state.stageWork, host);
|
|
521
|
+
if (!writes.length) { state.stageWork = null; render(host, state); toast('Nothing changed.'); return; }
|
|
522
|
+
act(writes.reduce(function (p, w) { return p.then(function () { return send(w[0], w[1], w[2]); }); }, Promise.resolve())
|
|
523
|
+
.then(function () { state.stageWork = null; render(host, state); toast('Saved.'); }));
|
|
524
|
+
} else if (t.hasAttribute('data-genesis-plan-edit')) {
|
|
525
|
+
var exp = state.genesis && state.genesis.plan && state.genesis.plan.expected_builders;
|
|
526
|
+
act(loadGov(exp || null).then(function (gov) { state.gov = gov; state.editingPlan = true; render(host, state); }));
|
|
527
|
+
} else if (t.hasAttribute('data-genesis-plan-cancel')) {
|
|
528
|
+
state.editingPlan = false;
|
|
529
|
+
render(host, state);
|
|
452
530
|
} else if (t.hasAttribute('data-genesis-plan')) {
|
|
453
531
|
// The government picked here is filed as a charter amendment with the plan
|
|
454
532
|
// (task 1004656). When the founder's one yes decides it, saving the plan is
|
|
455
533
|
// that yes too, the same one-click fast-track the stage closes take.
|
|
456
534
|
act(send('POST', '/founding/plan', planBody(host)).then(function (out) {
|
|
535
|
+
// A revision (task 1005107): say if nothing changed, and every change it could not carry.
|
|
536
|
+
if (out && out.revised === false) { state.editingPlan = false; render(host, state); toast('Nothing changed.'); return 'stay'; }
|
|
537
|
+
var held = !!(out && out.could_not && out.could_not.length);
|
|
538
|
+
if (held) toast(out.could_not.join(' '));
|
|
457
539
|
var c = out && out.charter;
|
|
458
540
|
if (c && c.state === 'sitting' && c.one_vote_decides && c.item_id) {
|
|
459
541
|
return send('POST', '/government/board/items/' + encodeURIComponent(c.item_id) + '/votes', { direction: 'yes' })
|
|
460
542
|
.catch(function () { return null; });
|
|
461
543
|
}
|
|
462
544
|
var said = charterNote(c);
|
|
463
|
-
if (!said) return null;
|
|
545
|
+
if (!said) return held ? window.OTB.sleep(3500) : null;
|
|
464
546
|
toast(said);
|
|
465
547
|
// Give the sentence time to be read before the reload takes it away.
|
|
466
548
|
return window.OTB.sleep(3500);
|
|
467
|
-
}).then(function () { window.location.reload(); }));
|
|
549
|
+
}).then(function (r) { if (r !== 'stay') window.location.reload(); }));
|
|
468
550
|
} else if (t.hasAttribute('data-genesis-close')) {
|
|
469
551
|
// One call (task 1005121): where the founder's one yes decides, the server
|
|
470
552
|
// closes the stage with it; otherwise it answers 202 with what the sitting
|
|
@@ -209,6 +209,8 @@ function shapeGenesis({ mode, plan, tasks = {}, board = {}, stageAt = {}, boardR
|
|
|
209
209
|
// What this stage left behind (task 1005115): null where the store is not read.
|
|
210
210
|
document: documents[key] || null,
|
|
211
211
|
all_finished: allFinished,
|
|
212
|
+
// task 1005107: a stage the Board Room passed is fixed; its answers read-only, with why.
|
|
213
|
+
locked: b && b.state === 'passed' ? { reason: require('./genesis-work').lockReason(key) } : null,
|
|
212
214
|
board: b ? {
|
|
213
215
|
state: b.state, item_id: b.item_id || null, needs: b.needs || null,
|
|
214
216
|
one_vote_decides: !!b.one_vote_decides, waiting_on: b.waiting_on || [],
|
|
@@ -233,6 +235,8 @@ function shapeGenesis({ mode, plan, tasks = {}, board = {}, stageAt = {}, boardR
|
|
|
233
235
|
day: mode.day,
|
|
234
236
|
deciding: mode.stage === 'decide',
|
|
235
237
|
plan: plan || null,
|
|
238
|
+
// The plan stays editable for as long as genesis is on (task 1005107): POST /founding/plan revises it.
|
|
239
|
+
plan_editable: !!plan,
|
|
236
240
|
// The demo's time box when this genesis is a demo's (task 1004506), else null.
|
|
237
241
|
demo: plan && plan.demo ? plan.demo : null,
|
|
238
242
|
stages,
|
|
@@ -257,7 +261,7 @@ function seams(deps = {}) {
|
|
|
257
261
|
government: deps.government !== undefined ? deps.government : api.resolveOptional('government'),
|
|
258
262
|
// Each stage's document summary (task 1005115): the founder's store unless injected.
|
|
259
263
|
// Fails soft in readGenesis, so a hall without the tables still renders.
|
|
260
|
-
documents: deps.documents !== undefined ? deps.documents : () => require('./genesis-work').readDocuments(),
|
|
264
|
+
documents: deps.documents !== undefined ? deps.documents : (extra) => require('./genesis-work').readDocuments(extra),
|
|
261
265
|
now: deps.now,
|
|
262
266
|
};
|
|
263
267
|
}
|
|
@@ -317,7 +321,10 @@ async function readGenesis(deps = {}) {
|
|
|
317
321
|
}
|
|
318
322
|
let documents = {};
|
|
319
323
|
if (typeof s.documents === 'function') {
|
|
320
|
-
|
|
324
|
+
// The board was just read: hand its passed stages over, so the documents need no second read.
|
|
325
|
+
const locks = {};
|
|
326
|
+
for (const [k, row] of Object.entries(board)) if (row && row.state === 'passed') locks[k] = { reason: require('./genesis-work').lockReason(k) };
|
|
327
|
+
try { documents = (await s.documents({ locks })) || {}; } catch (_) { documents = {}; } // the home still renders without them
|
|
321
328
|
}
|
|
322
329
|
return shapeGenesis({ mode, plan, tasks, board, stageAt, boardRoom: hasBoardRoom(s.government), documents });
|
|
323
330
|
}
|
|
@@ -371,12 +378,13 @@ async function setEnabled(enabled, builderId, deps = {}) {
|
|
|
371
378
|
}
|
|
372
379
|
|
|
373
380
|
// savePlan — the decide stage's door: record the plan, file the five stages as tasks,
|
|
374
|
-
// and move to scope.
|
|
381
|
+
// and move to scope. After that, the same door REVISES the plan (task 1005107; owner:
|
|
382
|
+
// "the UI is very restricting to be in the flow and not be able to go back and edit").
|
|
375
383
|
async function savePlan(body, builderId, deps = {}) {
|
|
376
384
|
const s = seams(deps);
|
|
377
385
|
const mode = await fm.foundingModeFor(modeDeps(s));
|
|
378
386
|
if (!mode) return { ok: false, reason: 'not_founding' };
|
|
379
|
-
if (mode.stage !== 'decide') return
|
|
387
|
+
if (mode.stage !== 'decide') return revisePlan(body, builderId, s);
|
|
380
388
|
const parsed = parsePlan(body);
|
|
381
389
|
if (!parsed.ok) return parsed;
|
|
382
390
|
if (!s.lifecycle || typeof s.lifecycle.seedGenesis !== 'function') return { ok: false, reason: 'no_board' };
|
|
@@ -387,6 +395,58 @@ async function savePlan(body, builderId, deps = {}) {
|
|
|
387
395
|
return { ok: true, plan: parsed.plan, seeded: seeded && seeded.seeded, charter };
|
|
388
396
|
}
|
|
389
397
|
|
|
398
|
+
// revisePlan — change the settled plan while genesis is on. The decide stage is never
|
|
399
|
+
// put to a sitting, so it is never fixed. Re-sending the same plan changes NOTHING (no
|
|
400
|
+
// write, no side effect). A changed plan is saved and what was derived from it follows:
|
|
401
|
+
// days each stage's due date is computed from the plan at read time, so it moves at once
|
|
402
|
+
// government a different form is filed as a charter amendment, decided like any (applyGovernmentChoice)
|
|
403
|
+
// existing turning it on files the gather-what-exists step (the seeder dedups); turning it
|
|
404
|
+
// off cannot take back a step already filed, and the answer says so
|
|
405
|
+
// `could_not` lists, in words, every change it could not carry through. A demo's settled
|
|
406
|
+
// time box and minutes are kept: the plan route does not carry them.
|
|
407
|
+
async function revisePlan(body, builderId, s) {
|
|
408
|
+
const prevRow = await s.get(PLAN_KEY);
|
|
409
|
+
const prev = readPlanValue(prevRow && prevRow.value);
|
|
410
|
+
const parsed = parsePlan(body);
|
|
411
|
+
if (!parsed.ok) return parsed;
|
|
412
|
+
const plan = parsed.plan;
|
|
413
|
+
if (prev && prev.demo) { plan.demo = prev.demo; if (prev.minutes) plan.minutes = prev.minutes; }
|
|
414
|
+
const same = prev && JSON.stringify(canonPlan(prev)) === JSON.stringify(canonPlan(plan));
|
|
415
|
+
if (same) return { ok: true, revised: false, changed: [], could_not: [], plan: prev };
|
|
416
|
+
const changed = prev ? Object.keys(plan).filter((k) => JSON.stringify(prev[k]) !== JSON.stringify(plan[k])) : Object.keys(plan);
|
|
417
|
+
const couldNot = [];
|
|
418
|
+
await s.set(PLAN_KEY, JSON.stringify(plan), builderId);
|
|
419
|
+
let charter = null;
|
|
420
|
+
if (changed.includes('government')) {
|
|
421
|
+
charter = plan.government ? await applyGovernmentChoice(plan.government, builderId, s) : { state: 'none' };
|
|
422
|
+
if (charter.state === 'not_filed' || charter.state === 'needs_council') {
|
|
423
|
+
couldNot.push(`The government could not be changed to ${plan.government} here: ${GOV_NOT_FILED[charter.reason || charter.state] || GOV_NOT_FILED.charter_failed}. The one in force stays.`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (changed.includes('existing')) {
|
|
427
|
+
if (plan.existing && s.lifecycle && typeof s.lifecycle.seedGenesis === 'function') {
|
|
428
|
+
await s.lifecycle.seedGenesis({ builderId, existing: true });
|
|
429
|
+
} else if (!plan.existing) {
|
|
430
|
+
couldNot.push('The step for gathering what already exists stays on the board: a filed task is not taken back. Drop it there if it no longer applies.');
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return { ok: true, revised: true, changed, could_not: couldNot, plan, charter };
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Why a government choice could not be filed, as a person would say it.
|
|
437
|
+
const GOV_NOT_FILED = Object.freeze({
|
|
438
|
+
needs_council: 'it needs a council with a rank before it can seat anyone',
|
|
439
|
+
no_government: 'this project has no Board Room to file it with',
|
|
440
|
+
charter_failed: 'the change to the charter could not be filed',
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// canonPlan — PURE. A plan with its keys in one order, so two equal plans compare equal.
|
|
444
|
+
function canonPlan(p) {
|
|
445
|
+
const out = {};
|
|
446
|
+
for (const k of Object.keys(p).sort()) out[k] = p[k] && typeof p[k] === 'object' && !Array.isArray(p[k]) ? canonPlan(p[k]) : p[k];
|
|
447
|
+
return out;
|
|
448
|
+
}
|
|
449
|
+
|
|
390
450
|
// applyGovernmentChoice — the government picked at decide becomes the one in force
|
|
391
451
|
// (task 1004656). It is filed as an ordinary charter amendment under the constitution
|
|
392
452
|
// in force (the government port's proposeCharter), so the board decides it like any
|
|
@@ -297,9 +297,9 @@ function documentOf(s) {
|
|
|
297
297
|
markdown: filled ? lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n' : '',
|
|
298
298
|
counts: s.counts,
|
|
299
299
|
updated_at: latest([s.updated_at, ...said.map((q) => q.answer.updated_at)]),
|
|
300
|
-
// Editing never reopens the stage.
|
|
301
|
-
|
|
302
|
-
|
|
300
|
+
// Editing never reopens the stage. A stage the Board Room passed is fixed (task 1005107).
|
|
301
|
+
editable: !s.locked,
|
|
302
|
+
locked_reason: s.locked ? s.locked.reason : null,
|
|
303
303
|
edit: {
|
|
304
304
|
answer: `PUT /api/bongos/founding/work/${s.stage}/answers/:question`,
|
|
305
305
|
page: `PUT /api/bongos/founding/work/${s.stage}/page`,
|
|
@@ -320,9 +320,11 @@ async function readDocument(stage, deps = {}) {
|
|
|
320
320
|
// genesis home carries per stage, so the page shows each stage's document in place.
|
|
321
321
|
async function readDocuments(deps = {}) {
|
|
322
322
|
const out = {};
|
|
323
|
+
let locks = {};
|
|
324
|
+
try { locks = await locksFor(deps); } catch (_) { locks = {}; }
|
|
323
325
|
for (const s of await readAll(deps)) {
|
|
324
|
-
const d = documentOf(s);
|
|
325
|
-
out[s.stage] = { title: d.title, filled: d.filled, updated_at: d.updated_at, editable: d.editable };
|
|
326
|
+
const d = documentOf({ ...s, locked: locks[s.stage] || null });
|
|
327
|
+
out[s.stage] = { title: d.title, filled: d.filled, updated_at: d.updated_at, editable: d.editable, locked_reason: d.locked_reason };
|
|
326
328
|
}
|
|
327
329
|
return out;
|
|
328
330
|
}
|
|
@@ -441,10 +443,50 @@ async function readAll(deps = {}) {
|
|
|
441
443
|
return shapeAll(await storeOf(deps).loadAll());
|
|
442
444
|
}
|
|
443
445
|
|
|
446
|
+
// ── which stages are fixed (task 1005107) ─────────────────────────────────────
|
|
447
|
+
// The owner's smallest honest answer: any stage not yet closed is editable; a stage the
|
|
448
|
+
// Board Room PASSED is shown read-only with a plain reason, because its answers are what
|
|
449
|
+
// the team voted on. A stage that moved on without a Board Room (a solo founder's
|
|
450
|
+
// finished tasks, task 1005115) stays editable: nobody voted on it. No government module
|
|
451
|
+
// means no sitting, so nothing is fixed. If the Board Room cannot be read, writes are
|
|
452
|
+
// refused rather than risk changing what was voted on.
|
|
453
|
+
const lockReason = (stage) => `The Board Room passed the ${stage} stage, so its answers are fixed: they are what was voted on. To change them, put the change to the Board Room.`;
|
|
454
|
+
|
|
455
|
+
// locksFor(deps) → { stage: { reason } } for every stage the Board Room passed. A caller
|
|
456
|
+
// that has already read the board passes the answer in as deps.locks (one read per request).
|
|
457
|
+
async function locksFor(deps = {}) {
|
|
458
|
+
if (deps.locks) return deps.locks;
|
|
459
|
+
const api = deps.api || require('../../src/module-api');
|
|
460
|
+
const gov = deps.government !== undefined ? deps.government : api.resolveOptional('government');
|
|
461
|
+
if (!gov || typeof gov.genesisStageStatus !== 'function') return {};
|
|
462
|
+
const st = await gov.genesisStageStatus();
|
|
463
|
+
const out = {};
|
|
464
|
+
for (const row of (st && st.stages) || []) {
|
|
465
|
+
if (row && row.state === 'passed' && isStage(row.key)) out[row.key] = { reason: lockReason(row.key) };
|
|
466
|
+
}
|
|
467
|
+
return out;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// guardWrite(stage, deps) → { refused } when the stage may not be written, else
|
|
471
|
+
// { deps } carrying the lock read, so the read that answers the write reuses it. Every
|
|
472
|
+
// write checks this first, before its own content checks, so a fixed stage always
|
|
473
|
+
// answers stage_closed whatever was sent.
|
|
474
|
+
async function guardWrite(stage, deps) {
|
|
475
|
+
let locks;
|
|
476
|
+
try { locks = await locksFor(deps); } catch (_) {
|
|
477
|
+
return { refused: { ok: false, reason: 'stage_lock_unknown', message: 'The Board Room could not be read, so nothing was changed. Try again in a moment.' } };
|
|
478
|
+
}
|
|
479
|
+
if (locks[stage]) return { refused: { ok: false, reason: 'stage_closed', message: locks[stage].reason } };
|
|
480
|
+
return { deps: { ...deps, locks } };
|
|
481
|
+
}
|
|
482
|
+
|
|
444
483
|
async function readStage(stage, deps = {}) {
|
|
445
484
|
if (!isStage(stage)) return { ok: false, reason: 'unknown_stage' };
|
|
446
485
|
const all = await readAll(deps);
|
|
447
486
|
const work = all.find((s) => s.stage === stage);
|
|
487
|
+
let locks = {};
|
|
488
|
+
try { locks = await locksFor(deps); } catch (_) { locks = {}; }
|
|
489
|
+
work.locked = locks[stage] || null;
|
|
448
490
|
if (stage === 'charter') work.advice = await charterAdvice(work, deps);
|
|
449
491
|
return { ok: true, work };
|
|
450
492
|
}
|
|
@@ -490,16 +532,22 @@ async function setDoor(stage, door, by, deps = {}) {
|
|
|
490
532
|
return readStage(stage, deps);
|
|
491
533
|
}
|
|
492
534
|
|
|
493
|
-
async function setPage(stage, page, by,
|
|
535
|
+
async function setPage(stage, page, by, deps0 = {}) {
|
|
494
536
|
if (!isStage(stage)) return { ok: false, reason: 'unknown_stage' };
|
|
537
|
+
const g = await guardWrite(stage, deps0);
|
|
538
|
+
if (g.refused) return g.refused;
|
|
539
|
+
const deps = g.deps;
|
|
495
540
|
const words = text(page);
|
|
496
541
|
if (words.length > LIMITS.page) return { ok: false, reason: 'page_too_long' };
|
|
497
542
|
await storeOf(deps).upsertWork(stage, { page_md: words || null }, by);
|
|
498
543
|
return readStage(stage, deps);
|
|
499
544
|
}
|
|
500
545
|
|
|
501
|
-
async function setLink(stage, url, by,
|
|
546
|
+
async function setLink(stage, url, by, deps0 = {}) {
|
|
502
547
|
if (!isStage(stage)) return { ok: false, reason: 'unknown_stage' };
|
|
548
|
+
const g = await guardWrite(stage, deps0);
|
|
549
|
+
if (g.refused) return g.refused;
|
|
550
|
+
const deps = g.deps;
|
|
503
551
|
const c = checkLink(url);
|
|
504
552
|
if (!c.ok) return c;
|
|
505
553
|
await storeOf(deps).upsertWork(stage, { link_url: c.value }, by);
|
|
@@ -508,7 +556,11 @@ async function setLink(stage, url, by, deps = {}) {
|
|
|
508
556
|
|
|
509
557
|
// setAnswer — store or revise one answer. opts.ifEmpty (for carrying answers over,
|
|
510
558
|
// task 1005274) writes only where the founder has said nothing yet.
|
|
511
|
-
async function setAnswer(stage, questionId, body, by,
|
|
559
|
+
async function setAnswer(stage, questionId, body, by, deps0 = {}, opts = {}) {
|
|
560
|
+
if (!isStage(stage)) return { ok: false, reason: 'unknown_stage' };
|
|
561
|
+
const g = await guardWrite(stage, deps0);
|
|
562
|
+
if (g.refused) return g.refused;
|
|
563
|
+
const deps = g.deps;
|
|
512
564
|
const c = checkAnswer(stage, questionId, body);
|
|
513
565
|
if (!c.ok) return c;
|
|
514
566
|
const { changed } = await storeOf(deps).writeAnswer(stage, questionId, c.value, by, opts);
|
|
@@ -517,8 +569,11 @@ async function setAnswer(stage, questionId, body, by, deps = {}, opts = {}) {
|
|
|
517
569
|
}
|
|
518
570
|
|
|
519
571
|
// clearAnswer — un-answer: the question goes back to empty (not to "skipped").
|
|
520
|
-
async function clearAnswer(stage, questionId, by,
|
|
572
|
+
async function clearAnswer(stage, questionId, by, deps0 = {}) {
|
|
521
573
|
if (!isStage(stage)) return { ok: false, reason: 'unknown_stage' };
|
|
574
|
+
const g = await guardWrite(stage, deps0);
|
|
575
|
+
if (g.refused) return g.refused;
|
|
576
|
+
const deps = g.deps;
|
|
522
577
|
if (!questionFor(stage, questionId)) return { ok: false, reason: 'unknown_question' };
|
|
523
578
|
const { cleared } = await storeOf(deps).clearAnswer(stage, questionId, by);
|
|
524
579
|
const r = await readStage(stage, deps);
|
|
@@ -547,6 +602,6 @@ async function readContext(deps = {}) {
|
|
|
547
602
|
// and are exercised through the operations (tests/genesis_work.mjs).
|
|
548
603
|
module.exports = {
|
|
549
604
|
STAGES, LIMITS, QUESTIONS, assistPrompt,
|
|
550
|
-
readDocument, readDocuments,
|
|
605
|
+
readDocument, readDocuments, lockReason,
|
|
551
606
|
pgStore, readAll, readStage, setDoor, setPage, setLink, setAnswer, clearAnswer, answerHistory, readContext,
|
|
552
607
|
};
|
|
@@ -17,6 +17,7 @@ const handoff = require('../guide-handoff'); // the start-a-project guide's an
|
|
|
17
17
|
|
|
18
18
|
const REFUSALS = {
|
|
19
19
|
unknown_stage: 404, unknown_question: 404, bad_door: 400, bad_state: 400, bad_origin: 400,
|
|
20
|
+
stage_closed: 409, stage_lock_unknown: 503,
|
|
20
21
|
unknown_fact: 400, bad_fact: 400, region_not_in_country: 400,
|
|
21
22
|
empty_answer: 400, not_a_choice: 400, answer_too_long: 400, page_too_long: 400, link_too_long: 400, bad_link: 400,
|
|
22
23
|
};
|
|
@@ -28,7 +29,7 @@ module.exports = function mountGenesisWorkRoutes(router, deps = {}) {
|
|
|
28
29
|
try {
|
|
29
30
|
const r = await fn(req);
|
|
30
31
|
if (r && r.ok === false) {
|
|
31
|
-
return res.status(REFUSALS[r.reason] || 409).json({ error: { code: r.reason, message: r.reason, ...(r.expected ? { expected: r.expected } : {}) } });
|
|
32
|
+
return res.status(REFUSALS[r.reason] || 409).json({ error: { code: r.reason, message: r.message || r.reason, ...(r.expected ? { expected: r.expected } : {}) } });
|
|
32
33
|
}
|
|
33
34
|
res.set('Cache-Control', 'no-store');
|
|
34
35
|
res.json(r);
|
|
@@ -1558,6 +1558,12 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1558
1558
|
<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>
|
|
1559
1559
|
Continue with GitHub
|
|
1560
1560
|
</a>
|
|
1561
|
+
<!-- No GitHub account yet (task 1005283): the same sign-in, opened on GitHub's
|
|
1562
|
+
sign-up page (signup=1), which offers "Continue with Google" and hands the
|
|
1563
|
+
sign-in straight back here once the account exists. Bongos cannot make a
|
|
1564
|
+
GitHub account for anyone; GitHub can, in one step, with Google. -->
|
|
1565
|
+
<p class="finePrint" id="wizWhyGh">Your project’s files live in a GitHub repository, so it needs a GitHub account.</p>
|
|
1566
|
+
<p class="finePrint" id="wizNoGh">No GitHub account? <a id="wizSignup" href="/api/bongos/auth/web/start?via=github&signup=1&return=/projects%3Fview%3Dnew&repos=1">Make one free with Google</a>. On GitHub’s page choose <strong>Continue with Google</strong>, and you come straight back here.</p>
|
|
1561
1567
|
<!-- Informed consent BEFORE GitHub's own wording (ADR 0155): the scope the
|
|
1562
1568
|
button asks for reads as "all your repositories" on GitHub's screen, so
|
|
1563
1569
|
the reason is stated here, on the path every founder actually takes. -->
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.25",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.25.
|
|
9
|
+
"version": "1.25.25",
|
|
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.25.
|
|
3
|
+
"version": "1.25.25",
|
|
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",
|
package/release-notes.json
CHANGED
|
@@ -11906,5 +11906,17 @@
|
|
|
11906
11906
|
"id": "1005285",
|
|
11907
11907
|
"text": "In the builders' hall, a project still being founded no longer lists its closed rooms in the quick-jump search; only the rooms that are open can be found."
|
|
11908
11908
|
}
|
|
11909
|
+
],
|
|
11910
|
+
"1.25.24": [
|
|
11911
|
+
{
|
|
11912
|
+
"id": "1005107",
|
|
11913
|
+
"text": "A founder can now go back and change their Genesis plan after saving it, and change any earlier stage's answers until the Board Room has voted that stage through — a voted stage shows its answers with a plain reason why they a"
|
|
11914
|
+
}
|
|
11915
|
+
],
|
|
11916
|
+
"1.25.25": [
|
|
11917
|
+
{
|
|
11918
|
+
"id": "1005283",
|
|
11919
|
+
"text": "Someone with no GitHub account can now start a project. The creation page explains why GitHub is needed and offers a 'Make one free with Google' link. GitHub's own sign-up page opens, they continue with their Google account, a"
|
|
11920
|
+
}
|
|
11909
11921
|
]
|
|
11910
11922
|
}
|
|
@@ -291,6 +291,22 @@ function buildAuthorizeUrl(state, redirectUri, { repos = false, switchAccount =
|
|
|
291
291
|
return u.toString();
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
// GitHub's own sign-up page, for a person with no GitHub account (task 1005283). It is the page GitHub's sign-in screen links as "Create an account"
|
|
295
|
+
// during an OAuth sign-in: `return_to` carries the authorize step back, so once the
|
|
296
|
+
// account exists (GitHub's sign-up offers "Continue with Google") GitHub resumes the
|
|
297
|
+
// SAME sign-in — same state, same scope — and our callback lands the person where they
|
|
298
|
+
// started. Only github.com's own authorize path is ever handed on; anything else is null
|
|
299
|
+
// and the caller falls back to the plain authorize redirect.
|
|
300
|
+
function githubSignupUrl(authorizeUrl) {
|
|
301
|
+
let u;
|
|
302
|
+
try { u = new URL(String(authorizeUrl)); } catch { return null; }
|
|
303
|
+
if (u.protocol !== 'https:' || u.host !== 'github.com' || u.pathname !== '/login/oauth/authorize') return null;
|
|
304
|
+
const signup = new URL('https://github.com/signup');
|
|
305
|
+
signup.searchParams.set('return_to', u.pathname + u.search);
|
|
306
|
+
signup.searchParams.set('source', 'oauth');
|
|
307
|
+
return signup.toString();
|
|
308
|
+
}
|
|
309
|
+
|
|
294
310
|
async function completeWebFlow({ code, redirectUri, userAgent, captureRepoToken = false }) {
|
|
295
311
|
const { accessToken, scope: grantedScope } = await exchangeCodeForToken(code, redirectUri);
|
|
296
312
|
const ghUser = await fetchGithubUser(accessToken);
|
|
@@ -386,6 +402,7 @@ async function completeWebFlow({ code, redirectUri, userAgent, captureRepoToken
|
|
|
386
402
|
// slashes on origin are trimmed so `https://hub/` and `https://hub` behave alike.
|
|
387
403
|
|
|
388
404
|
module.exports = {
|
|
405
|
+
githubSignupUrl,
|
|
389
406
|
buildAuthorizeUrl,
|
|
390
407
|
completeWebFlow,
|
|
391
408
|
exchangeCodeForToken,
|
package/src/bongos/auth.js
CHANGED
|
@@ -58,7 +58,7 @@ const { branding, userAgent } = require('../branding');
|
|
|
58
58
|
|
|
59
59
|
const { SCOPE, authConfigured, grantedScopeCoversRepos, idpConfigured, idpEnabled, loadIdpConfig, scopeForWebFlow } = require('./auth-config.js');
|
|
60
60
|
const { firstAdminLogin, firstTimeAdmissionBlocked, instanceNeedsFirstAdmin, joinabilityMode, maybeSeatFirstAdmin, membershipKind, notifyHubOfInvite, openEnrollmentEnabled, readAppStatusFromHub, readApplicantProfileFromHub, readApplicantViewFromHub, webAdmissionStatus } = require('./auth-admission.js');
|
|
61
|
-
const { buildAuthorizeUrl, completeWebFlow, pollDeviceFlow, startDeviceFlow } = require('./auth-github.js');
|
|
61
|
+
const { buildAuthorizeUrl, githubSignupUrl, completeWebFlow, pollDeviceFlow, startDeviceFlow } = require('./auth-github.js');
|
|
62
62
|
const { buildIdpAuthorizeUrl, completeDeviceExchange, completeIdpFlow, idpAuthorizeUrl, loadHubPublicKey, logoutTokenJtiSeenAndRecord, readApplicationVouch, verifyHubAssertion, verifyHubLogoutToken } = require('./auth-idp.js');
|
|
63
63
|
const { addDiscordGuildMember, buildDiscordAuthorizeUrl, discordAuthorizeUrl, discordBotConfigured, discordConfigured, exchangeDiscordCode, fetchDiscordUser, isValidDiscordUserId, loadDiscordBotToken, loadDiscordCreds, removeDiscordGuildMember } = require('./auth-discord.js');
|
|
64
64
|
|
|
@@ -854,6 +854,7 @@ module.exports = {
|
|
|
854
854
|
startDeviceFlow,
|
|
855
855
|
pollDeviceFlow,
|
|
856
856
|
buildAuthorizeUrl,
|
|
857
|
+
githubSignupUrl,
|
|
857
858
|
scopeForWebFlow,
|
|
858
859
|
grantedScopeCoversRepos,
|
|
859
860
|
buildIdpAuthorizeUrl,
|
|
@@ -710,6 +710,7 @@ module.exports = function buildAuthRouter() {
|
|
|
710
710
|
// ?switch=1 — sign in as a DIFFERENT GitHub account (task 1004789): show
|
|
711
711
|
// GitHub's account picker instead of re-using the one logged in there.
|
|
712
712
|
const wantsSwitch = req.query.switch === '1' || req.query.switch === 'true';
|
|
713
|
+
const wantsSignup = req.query.signup === '1' || req.query.signup === 'true'; // no GitHub account yet: its sign-up page (task 1005283)
|
|
713
714
|
// A sign-in begun on a satellite hub host (builders./status.) restarts on
|
|
714
715
|
// the canonical host (task 1004357). The handshake cookies are host-only now,
|
|
715
716
|
// and the callback always lands on the canonical host, so a state cookie set
|
|
@@ -720,8 +721,7 @@ module.exports = function buildAuthRouter() {
|
|
|
720
721
|
const raw = safeReturnTo(req.query.return) || safeReturnTo(req.headers.referer) || '/';
|
|
721
722
|
const back = raw.startsWith('/') ? `https://${host}${raw}` : raw;
|
|
722
723
|
const params = new URLSearchParams({ return: back });
|
|
723
|
-
|
|
724
|
-
if (wantsSwitch) params.set('switch', '1');
|
|
724
|
+
for (const [k, on] of [['repos', wantsRepos], ['switch', wantsSwitch], ['signup', wantsSignup]]) if (on) params.set(k, '1');
|
|
725
725
|
if (req.query.via === 'github') params.set('via', 'github');
|
|
726
726
|
return res.redirect(`${WEB_REDIRECT_ORIGIN.replace(/\/+$/, '')}${apiBase(req)}/auth/web/start?${params}`);
|
|
727
727
|
}
|
|
@@ -729,7 +729,7 @@ module.exports = function buildAuthRouter() {
|
|
|
729
729
|
// host (a builder who signed in on the apex, then opened builders.): no
|
|
730
730
|
// GitHub round-trip, just hand that host its copy. A repo-scope sign-in
|
|
731
731
|
// still goes to GitHub — it exists to ask for the broader grant.
|
|
732
|
-
if (!wantsRepos && !wantsSwitch) {
|
|
732
|
+
if (!wantsRepos && !wantsSwitch && !wantsSignup) {
|
|
733
733
|
const target = safeReturnTo(req.query.return);
|
|
734
734
|
if (target && !target.startsWith('/')) {
|
|
735
735
|
try {
|
|
@@ -746,7 +746,7 @@ module.exports = function buildAuthRouter() {
|
|
|
746
746
|
// lands on one chooser — Continue with GitHub or Continue with Google — so no
|
|
747
747
|
// page needs its own pair of buttons. `via=github` is the GitHub button (and
|
|
748
748
|
// any door that needs GitHub itself); "New account" names a GitHub account.
|
|
749
|
-
if (req.query.via !== 'github' && !wantsSwitch && googleConfigured()) {
|
|
749
|
+
if (req.query.via !== 'github' && !wantsSwitch && !wantsSignup && googleConfigured()) {
|
|
750
750
|
const ret = safeReturnTo(req.query.return) || safeReturnTo(req.headers.referer);
|
|
751
751
|
const gh = new URLSearchParams({ via: 'github' });
|
|
752
752
|
if (ret) gh.set('return', ret);
|
|
@@ -803,9 +803,9 @@ module.exports = function buildAuthRouter() {
|
|
|
803
803
|
// Federate through the hub when configured (ADR 0141 §1); otherwise the direct
|
|
804
804
|
// GitHub authorize, carrying the repo-scope opt-in. Same CSRF state cookie either
|
|
805
805
|
// way — the callback validates it identically, so the hub leg is CSRF-protected too.
|
|
806
|
-
res.redirect(auth.
|
|
807
|
-
|
|
808
|
-
|
|
806
|
+
if (auth.idpConfigured()) return res.redirect(auth.buildIdpAuthorizeUrl(state, redirectUri));
|
|
807
|
+
const authorizeUrl = auth.buildAuthorizeUrl(state, redirectUri, { repos: wantsRepos, switchAccount: wantsSwitch });
|
|
808
|
+
res.redirect((wantsSignup && auth.githubSignupUrl(authorizeUrl)) || authorizeUrl);
|
|
809
809
|
});
|
|
810
810
|
|
|
811
811
|
// GET /auth/web/handoff?code=&return= — the far side of the one-time session
|
package/src/module-api.js
CHANGED
|
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
|
|
|
81
81
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
82
82
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
83
83
|
|
|
84
|
-
const CORE_VERSION = '1.25.
|
|
84
|
+
const CORE_VERSION = '1.25.25'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
85
85
|
|
|
86
86
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
87
87
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|