@bongos/core 1.21.94 → 1.21.95
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 +34 -29
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +5 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/api/openapi.json +75 -3
- package/docs/api-reference.md +4 -2
- package/docs/design/project-startup-direction.md +9 -1
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-readings.json +1 -1
- package/modules/provisioning/routes/callback-page.js +20 -10
- package/modules/provisioning/routes/github-setup.js +110 -2
- package/modules/public-landing/public/projects.html +103 -10
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/module-api.js +1 -1
- package/tests/github_handback_any_device.mjs +340 -0
- package/tests/projects_hub.mjs +6 -2
- package/tests/provisioning_callback_page.mjs +3 -2
- package/tests/provisioning_github_app_routes.mjs +15 -10
- package/tests/wizard_draft_resume.mjs +4 -0
- package/tests/wizard_github_feedback.mjs +2 -1
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
// single-use `state` minted for the signed-in owner is the only key, and neither route
|
|
10
10
|
// reveals a secret or changes authority. The checklist they report is derived in
|
|
11
11
|
// ../github-setup.js from what the control-plane runner has VERIFIED.
|
|
12
|
+
//
|
|
13
|
+
// Two signed-in routes join them (task 1004650 — the hand-back from any tab or device):
|
|
14
|
+
// the RETURN route the callback and setup pages send the founder to, which lands the state's
|
|
15
|
+
// own project for its owner and nobody else, and the RESUME read the project page asks when
|
|
16
|
+
// its tab remembers nothing — so a phone, a second window or a fresh tab picks up part 03 of
|
|
17
|
+
// the right project from what the server knows, never from sessionStorage.
|
|
12
18
|
'use strict';
|
|
13
19
|
|
|
14
20
|
const express = require('express');
|
|
@@ -17,10 +23,38 @@ const { pool } = api;
|
|
|
17
23
|
const provisioning = require('../provisioning');
|
|
18
24
|
const githubSetup = require('../github-setup');
|
|
19
25
|
const { makeAskRateLimiter } = require('../rate-limit');
|
|
20
|
-
const { callbackPage } = require('./callback-page');
|
|
26
|
+
const { callbackPage, returnHref } = require('./callback-page');
|
|
21
27
|
|
|
22
28
|
const log = api.logger('provisioning');
|
|
23
29
|
|
|
30
|
+
// The state tokens this leg mints are hex (provisioning.js); anything else is not one of ours.
|
|
31
|
+
const STATE_RE = /^[0-9a-f]{16,128}$/i;
|
|
32
|
+
|
|
33
|
+
// An attempt or a project older than this is not "in progress" for the resume read: a
|
|
34
|
+
// founder who walked away a week ago and opens the create page gets a fresh page.
|
|
35
|
+
const RESUME_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
|
|
36
|
+
|
|
37
|
+
// Is this instance one the hand-back may land on for this builder? Strictly the OWNER —
|
|
38
|
+
// no Archon override: the return route names a project to whoever holds the state, and a
|
|
39
|
+
// project page is where its owner, and only its owner, connects GitHub. A gone project is
|
|
40
|
+
// no landing either. PURE.
|
|
41
|
+
function ownsLanding(inst, builderId) {
|
|
42
|
+
return !!(inst && builderId != null && String(inst.owner_builder_id) === String(builderId)
|
|
43
|
+
&& !inst.torn_down_at && inst.status !== 'torn_down');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// The project page's resume record: just what part 03 needs to name and watch the project
|
|
47
|
+
// (the page reads the rest from the owner's instance read). Never the state token. PURE.
|
|
48
|
+
function resumeRecord(inst) {
|
|
49
|
+
return { id: inst.id, slug: inst.slug, domain: inst.domain || null, onboard_mode: inst.onboard_mode || 'greenfield', repo: githubSetup.targetRepo(inst) };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The project page to land on: part 03 of that project, by id. The state rides the
|
|
53
|
+
// FRAGMENT (never sent to a server) so the page can keep watching that attempt. PURE.
|
|
54
|
+
function landingHref(instanceId, state) {
|
|
55
|
+
return `/projects?view=new&id=${encodeURIComponent(String(instanceId))}${state ? `#ghstate=${encodeURIComponent(state)}` : ''}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
24
58
|
// GitHub's installation id is digits; anything else is dropped before it reaches a log line.
|
|
25
59
|
function installationIdOf(q) {
|
|
26
60
|
const s = String((q && q.installation_id) || '');
|
|
@@ -32,6 +66,7 @@ module.exports = function buildGithubSetupRouter() {
|
|
|
32
66
|
// the page polls every few seconds, the callback page every two — per-IP, in-module
|
|
33
67
|
const statusRateLimit = makeAskRateLimiter({ max: 120 });
|
|
34
68
|
const setupRateLimit = makeAskRateLimiter({ max: 30 });
|
|
69
|
+
const returnRateLimit = makeAskRateLimiter({ max: 30 });
|
|
35
70
|
|
|
36
71
|
// GET /provisioning/github-app/status?state=<> — the PUBLIC progress read (task 1004548;
|
|
37
72
|
// the checklist since task 1004649). Reveals the manifest lifecycle status, the
|
|
@@ -85,7 +120,7 @@ module.exports = function buildGithubSetupRouter() {
|
|
|
85
120
|
const sendPage = (status, page) => res.status(status).type('html').send(page);
|
|
86
121
|
try {
|
|
87
122
|
const state = String(req.query.state || '');
|
|
88
|
-
const row =
|
|
123
|
+
const row = STATE_RE.test(state) ? await provisioning.getOAuthManifestByState(pool, state) : null;
|
|
89
124
|
if (!row) {
|
|
90
125
|
return sendPage(200, callbackPage({ ok: true, title: 'Installed on GitHub',
|
|
91
126
|
body: '<p>GitHub has installed the app. Go back to your project’s page: it checks the install itself.</p><p><a class="btn" href="/projects?view=mine">Back to my projects →</a></p>' }));
|
|
@@ -109,5 +144,78 @@ module.exports = function buildGithubSetupRouter() {
|
|
|
109
144
|
}
|
|
110
145
|
});
|
|
111
146
|
|
|
147
|
+
// GET /provisioning/github-app/return?state=<> — where the callback and setup pages send
|
|
148
|
+
// the founder back (task 1004650). The SERVER resolves the one-time state to the project
|
|
149
|
+
// it was minted for and 302s that project's signed-in OWNER to its part 03
|
|
150
|
+
// (/projects?view=new&id=<id>), so the hand-back works from any tab, window or device —
|
|
151
|
+
// nothing in browser storage is read. Not signed in → through the web sign-in and back
|
|
152
|
+
// here. Signed in as anyone else, an unknown or malformed state, or a gone project → one
|
|
153
|
+
// refusal page that names no project (no oracle about which). The state stays single-use
|
|
154
|
+
// where it matters: only the manifest callback's code exchange consumes it, and this route
|
|
155
|
+
// changes nothing — it reveals a project's page only to the person who already owns it.
|
|
156
|
+
// rank: public — as a GATE: it is a browser redirect, so no middleware 401s a signed-out
|
|
157
|
+
// visitor (they are sent through sign-in instead). WHO lands is decided in-handler from
|
|
158
|
+
// the live session: the project's own owner, no Archon override, and nobody else ever
|
|
159
|
+
// learns which project the state names. It reveals no secret and writes nothing.
|
|
160
|
+
router.get('/provisioning/github-app/return', returnRateLimit, async (req, res) => {
|
|
161
|
+
res.set('Cache-Control', 'no-store');
|
|
162
|
+
res.set('Referrer-Policy', 'no-referrer');
|
|
163
|
+
const refuse = () => res.status(404).type('html').send(callbackPage({ ok: false, title: 'This isn’t your project',
|
|
164
|
+
body: '<p>This link is for a project on another account, or it has expired — sign in with the account that started the project, or open your own projects.</p><p><a class="btn" href="/projects?view=mine">My projects →</a></p>' }));
|
|
165
|
+
try {
|
|
166
|
+
const state = String(req.query.state || '');
|
|
167
|
+
if (!STATE_RE.test(state)) return refuse();
|
|
168
|
+
const session = await api.resolveSession(req);
|
|
169
|
+
if (!session) {
|
|
170
|
+
// the relative return passes the web flow's open-redirect guard (safeReturnTo);
|
|
171
|
+
// the repo scope rides every hub sign-in door (ADR 0364)
|
|
172
|
+
return res.redirect(302, `/api/bongos/auth/web/start?return=${encodeURIComponent(returnHref(state))}&repos=1`);
|
|
173
|
+
}
|
|
174
|
+
if (session.status === 'inactive') return refuse();
|
|
175
|
+
const row = await provisioning.getOAuthManifestByState(pool, state);
|
|
176
|
+
const inst = row ? await provisioning.getInstanceById(pool, row.instance_id).catch(() => null) : null;
|
|
177
|
+
if (!ownsLanding(inst, session.builder_id)) return refuse();
|
|
178
|
+
return res.redirect(302, landingHref(inst.id, state));
|
|
179
|
+
} catch (err) {
|
|
180
|
+
log.error('[provisioning] GET /provisioning/github-app/return', err);
|
|
181
|
+
return res.status(500).type('html').send(callbackPage({ ok: false, title: 'Something went wrong',
|
|
182
|
+
body: '<p>We couldn’t find your project just now — it is on your projects page.</p><p><a class="btn" href="/projects?view=mine">My projects →</a></p>' }));
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// GET /provisioning/github-app/resume[?id=<n>] — the project page's "which project am I
|
|
187
|
+
// connecting?" read when its tab remembers nothing (task 1004650): a hand-back on another
|
|
188
|
+
// device, a fresh tab, a cleared browser. With an id: that project, if the caller owns it.
|
|
189
|
+
// Without: the caller's newest project whose GitHub checklist is not connected yet, made or
|
|
190
|
+
// last tried within a week — so opening the create page later resumes part 03 from the
|
|
191
|
+
// server's view. Either way { project: null } when there is none; never the state token.
|
|
192
|
+
// rank: any authenticated builder (requireBuilder), own projects only — no Archon override.
|
|
193
|
+
router.get('/provisioning/github-app/resume', api.requireBuilder, async (req, res) => {
|
|
194
|
+
try {
|
|
195
|
+
const me = req.builder && req.builder.id;
|
|
196
|
+
const asked = String(req.query.id || '');
|
|
197
|
+
if (asked) {
|
|
198
|
+
if (!/^\d{1,12}$/.test(asked)) return res.json({ ok: true, project: null });
|
|
199
|
+
const inst = await provisioning.getInstanceById(pool, Number(asked)).catch(() => null);
|
|
200
|
+
return res.json({ ok: true, project: ownsLanding(inst, me) ? resumeRecord(inst) : null });
|
|
201
|
+
}
|
|
202
|
+
const now = Date.now();
|
|
203
|
+
const owned = (await provisioning.listInstancesForOwner(pool, me)).filter((i) => ownsLanding(i, me)).slice(0, 5);
|
|
204
|
+
// the (at most five) candidates are read together; the newest unfinished one wins
|
|
205
|
+
const open = await Promise.all(owned.map(async (inst) => {
|
|
206
|
+
const row = await provisioning.getLatestOAuthManifestForInstance(pool, inst.id);
|
|
207
|
+
const touched = Math.max(Date.parse(inst.created_at) || 0, row ? Date.parse(row.created_at) || 0 : 0);
|
|
208
|
+
if (now - touched > RESUME_WINDOW_MS) return false;
|
|
209
|
+
const setup = await githubSetup.githubSetupRead(pool, inst, row, row ? provisioning.publicGithubApp(row).status : null, { withState: false });
|
|
210
|
+
return !setup.checklist.connected;
|
|
211
|
+
}));
|
|
212
|
+
const found = owned.find((_inst, i) => open[i]);
|
|
213
|
+
return res.json({ ok: true, project: found ? resumeRecord(found) : null });
|
|
214
|
+
} catch (err) {
|
|
215
|
+
log.error('[provisioning] GET /provisioning/github-app/resume', err);
|
|
216
|
+
return res.status(500).json({ ok: false, error: 'resume_read_failed' });
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
112
220
|
return router;
|
|
113
221
|
};
|
|
@@ -4583,6 +4583,9 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4583
4583
|
survives the GitHub round-trip; the server, not this page, names the terms version. */
|
|
4584
4584
|
var DRAFT_KEY = 'cb-create-draft-v10';
|
|
4585
4585
|
var CREATED_KEY = 'cb-created-instance';
|
|
4586
|
+
/* the project "Start another project" set down (task 1004650): the server's resume
|
|
4587
|
+
read does not pull this tab back to it */
|
|
4588
|
+
var DECLINED_KEY = 'cb-resume-declined';
|
|
4586
4589
|
/* `description` + `teamShape` are the optional About step (task 1002334) —
|
|
4587
4590
|
both legitimately stay empty, and empty is what the create request omits. */
|
|
4588
4591
|
/* `modules` is the picker's answer (task 1002339) and is THREE-VALUED like
|
|
@@ -6926,18 +6929,36 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6926
6929
|
try { localStorage.setItem(AUTO_OPEN_KEY, box.checked ? '1' : '0'); } catch (e) {}
|
|
6927
6930
|
});
|
|
6928
6931
|
|
|
6929
|
-
/* ── C8: the wizard's polls run only on stage,
|
|
6930
|
-
Off-view
|
|
6931
|
-
|
|
6932
|
+
/* ── C8: the wizard's polls run only while the create view is on stage, like the
|
|
6933
|
+
sky. Off-view a due tick parks itself instead of fetching; enter() resumes it.
|
|
6934
|
+
A HIDDEN tab is different (task 1004650): that is exactly when the founder is on
|
|
6935
|
+
GitHub, in another tab or on their phone, so the polls keep checking there at a
|
|
6936
|
+
slower pace (HIDDEN_POLL_MS), and the moment the tab is seen again a waiting tick
|
|
6937
|
+
runs at once — the laptop's checklist ticks on its own. ── */
|
|
6938
|
+
var HIDDEN_POLL_MS = 20000;
|
|
6932
6939
|
function wizPollActive() {
|
|
6933
|
-
return
|
|
6940
|
+
return currentView === 'new';
|
|
6941
|
+
}
|
|
6942
|
+
/* the next tick's wait: the poll's own pace while the tab is seen, the slow one while hidden */
|
|
6943
|
+
function wizPollDelay(ms) {
|
|
6944
|
+
return document.hidden ? Math.max(ms, HIDDEN_POLL_MS) : ms;
|
|
6934
6945
|
}
|
|
6935
6946
|
function resumePolls() {
|
|
6936
6947
|
if (watchPending && instanceId) { watchPending = false; startProgress(instanceId); }
|
|
6937
6948
|
if (manifestPending && manifestState) { manifestPending = false; pollManifest(manifestState); }
|
|
6938
6949
|
}
|
|
6950
|
+
/* each poll's waiting tick, so a return to the tab can run it now instead of later.
|
|
6951
|
+
Only a WAITING tick is run (its timer is set): one mid-fetch re-arms itself, and
|
|
6952
|
+
running it again would start a second chain. */
|
|
6953
|
+
var progressKick = null, manifestKick = null;
|
|
6954
|
+
function kickPolls() {
|
|
6955
|
+
if (pollTimer && progressKick) { clearTimeout(pollTimer); pollTimer = null; progressKick(); }
|
|
6956
|
+
if (manifestPollTimer && manifestKick) { clearTimeout(manifestPollTimer); manifestPollTimer = null; manifestKick(); }
|
|
6957
|
+
}
|
|
6939
6958
|
document.addEventListener('visibilitychange', function () {
|
|
6940
|
-
if (wizPollActive())
|
|
6959
|
+
if (document.hidden || !wizPollActive()) return;
|
|
6960
|
+
resumePolls();
|
|
6961
|
+
kickPolls();
|
|
6941
6962
|
});
|
|
6942
6963
|
|
|
6943
6964
|
/* ── GitHub-App manifest flow (ADR 0133): mint a one-time state for OUR instance,
|
|
@@ -6957,6 +6978,17 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6957
6978
|
var handbackState = (function () {
|
|
6958
6979
|
try { var m = /[#&]ghstate=([0-9a-f]{16,128})/i.exec(location.hash || ''); return m ? m[1] : null; } catch (e) { return null; }
|
|
6959
6980
|
})();
|
|
6981
|
+
/* …and the PROJECT it is for (task 1004650): the return route resolves the state on
|
|
6982
|
+
the server and lands its owner on ?view=new&id=<project>, so a phone, another
|
|
6983
|
+
window or a fresh tab reaches the right project's part 03 with nothing in this
|
|
6984
|
+
tab's storage. Read once at load, for the same reason as the state above. */
|
|
6985
|
+
var handbackProject = (function () {
|
|
6986
|
+
try {
|
|
6987
|
+
var q = new URLSearchParams(location.search || '');
|
|
6988
|
+
var id = q.get('view') === 'new' ? q.get('id') : null;
|
|
6989
|
+
return id && /^\d{1,12}$/.test(id) ? id : null;
|
|
6990
|
+
} catch (e) { return null; }
|
|
6991
|
+
})();
|
|
6960
6992
|
/* the manifest leg survives a reload: token + last status ride the CREATED
|
|
6961
6993
|
record, so enter() re-seeds the step instead of re-offering "Set up
|
|
6962
6994
|
GitHub sign-in" for an app the founder already approved (task 1003232) */
|
|
@@ -7045,6 +7077,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7045
7077
|
function tick() {
|
|
7046
7078
|
if (myGen !== manifestGen) return;
|
|
7047
7079
|
if (!wizPollActive()) { manifestPending = true; stopManifestPoll(); return; }
|
|
7080
|
+
manifestKick = tick;
|
|
7048
7081
|
fetch(API + '/provisioning/github-app/status?state=' + encodeURIComponent(stateToken))
|
|
7049
7082
|
.then(function (res) { if (!res.ok) throw new Error('soft'); return res.json(); })
|
|
7050
7083
|
.then(function (d) {
|
|
@@ -7055,10 +7088,10 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7055
7088
|
var failure = d.failure || null;
|
|
7056
7089
|
if (next !== live.manifest || failure !== live.manifestFailure) { live.manifest = next; live.manifestFailure = failure; rememberManifest(undefined, next); renderSteps(); }
|
|
7057
7090
|
else noteManifestWait();
|
|
7058
|
-
if (!d.terminal) manifestPollTimer = setTimeout(tick, 4000);
|
|
7091
|
+
if (!d.terminal) manifestPollTimer = setTimeout(tick, wizPollDelay(4000));
|
|
7059
7092
|
else manifestPollTimer = null; /* a stale fired id would read as "still polling" and resume a finished poll */
|
|
7060
7093
|
})
|
|
7061
|
-
.catch(function () { if (myGen === manifestGen) manifestPollTimer = setTimeout(tick, 8000); });
|
|
7094
|
+
.catch(function () { if (myGen === manifestGen) manifestPollTimer = setTimeout(tick, wizPollDelay(8000)); });
|
|
7062
7095
|
}
|
|
7063
7096
|
tick();
|
|
7064
7097
|
}
|
|
@@ -7249,6 +7282,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7249
7282
|
function tick() {
|
|
7250
7283
|
if (myGen !== watchGen) return;
|
|
7251
7284
|
if (!wizPollActive()) { watchPending = true; stopPolling(); return; }
|
|
7285
|
+
progressKick = tick;
|
|
7252
7286
|
ticks += 1;
|
|
7253
7287
|
if (ticks > 400) {
|
|
7254
7288
|
/* ~33 min: this tab stops watching, but the standup keeps going — hand
|
|
@@ -7305,7 +7339,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7305
7339
|
var terminal = renderProgress(inst);
|
|
7306
7340
|
/* a GitHub leg still in flight keeps this read alive even once the
|
|
7307
7341
|
project is up — after a reload it is the ONLY thing watching it */
|
|
7308
|
-
if (!terminal || manifestInFlight() || setupOpen()) pollTimer = setTimeout(tick, 5000);
|
|
7342
|
+
if (!terminal || manifestInFlight() || setupOpen()) pollTimer = setTimeout(tick, wizPollDelay(5000));
|
|
7309
7343
|
else stopPolling();
|
|
7310
7344
|
})
|
|
7311
7345
|
.catch(function (err) {
|
|
@@ -7318,7 +7352,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7318
7352
|
return;
|
|
7319
7353
|
}
|
|
7320
7354
|
/* transient (network / 5xx): keep the last view, try again a bit later */
|
|
7321
|
-
pollTimer = setTimeout(tick, 10000);
|
|
7355
|
+
pollTimer = setTimeout(tick, wizPollDelay(10000));
|
|
7322
7356
|
});
|
|
7323
7357
|
}
|
|
7324
7358
|
tick();
|
|
@@ -7367,6 +7401,11 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7367
7401
|
resetForNewProject();
|
|
7368
7402
|
/* the deliberate goodbye: the record goes too, so nothing offers to resume
|
|
7369
7403
|
the project the founder has just said they are finished watching */
|
|
7404
|
+
try {
|
|
7405
|
+
var gone = createdRecord();
|
|
7406
|
+
if (gone && gone.id) sessionStorage.setItem(DECLINED_KEY, String(gone.id));
|
|
7407
|
+
} catch (e) {}
|
|
7408
|
+
serverRecord = null;
|
|
7370
7409
|
try { sessionStorage.removeItem(CREATED_KEY); } catch (e) {}
|
|
7371
7410
|
paintResumeOffer(null);
|
|
7372
7411
|
show(1, true);
|
|
@@ -7810,8 +7849,11 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7810
7849
|
})();
|
|
7811
7850
|
|
|
7812
7851
|
/* ── the hub's three doors ── */
|
|
7852
|
+
/* the server's resume answer (task 1004650), kept in memory too: a browser whose
|
|
7853
|
+
storage throws still has the project it was handed */
|
|
7854
|
+
var serverRecord = null;
|
|
7813
7855
|
function createdRecord() {
|
|
7814
|
-
try { return JSON.parse(sessionStorage.getItem(CREATED_KEY) || 'null'); } catch (e) { return
|
|
7856
|
+
try { return JSON.parse(sessionStorage.getItem(CREATED_KEY) || 'null') || serverRecord; } catch (e) { return serverRecord; }
|
|
7815
7857
|
}
|
|
7816
7858
|
/* The "still watching …" offer above step 1 (task 1003503). Its markup is baked
|
|
7817
7859
|
into the panel, so this only names the project and shows or hides the line —
|
|
@@ -7876,6 +7918,12 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7876
7918
|
/* restore the opt-out too — without it resolvedDomain() re-derives the
|
|
7877
7919
|
assigned address and the done panel promises one that doesn't exist */
|
|
7878
7920
|
state.noAddress = !!created.noaddr;
|
|
7921
|
+
/* a record the server handed over (another device, a fresh tab) carries the repo
|
|
7922
|
+
the project is on, so the install step names it (task 1004650) */
|
|
7923
|
+
if (created.repo_ref && !state.repo) {
|
|
7924
|
+
var rr = String(created.repo_ref).split('/');
|
|
7925
|
+
if (rr.length === 2) { state.owner = rr[0]; state.repo = rr[1]; }
|
|
7926
|
+
}
|
|
7879
7927
|
/* a hand-back from the callback page names the attempt GitHub just approved */
|
|
7880
7928
|
if (handbackState) {
|
|
7881
7929
|
created.manifest_state = handbackState;
|
|
@@ -7898,9 +7946,54 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7898
7946
|
identity settles it shows the draft step optimistically (the page stays
|
|
7899
7947
|
usable while /me is in flight — the boot's own rule); onAuth() re-enters
|
|
7900
7948
|
once the answer lands. Focus only moves on a deliberate nav (the flag). */
|
|
7949
|
+
/* askServerResume(focus) — which project is this founder connecting, by the SERVER's
|
|
7950
|
+
view, when this tab cannot say (task 1004650)? Asked once per page load. A hand-back
|
|
7951
|
+
names its project (handbackProject, from the return route): it always lands on that
|
|
7952
|
+
project's part 03, and only its owner gets an answer. With no name, the server picks
|
|
7953
|
+
the founder's newest project whose GitHub is not connected yet: an arrival lands on
|
|
7954
|
+
it unless something is being typed, and a deliberate "New project" only offers it on
|
|
7955
|
+
the resume line. Nothing found → the page as it was. */
|
|
7956
|
+
var resumeAsked = false;
|
|
7957
|
+
function recordFromServer(p) {
|
|
7958
|
+
return { id: p.id, slug: p.slug || '', name: p.slug || '', domain: p.domain || '', noaddr: !p.domain,
|
|
7959
|
+
mode: p.onboard_mode === 'adopt' ? 'adopt' : 'greenfield', repo_ref: p.repo || null, at: Date.now(), from_server: true };
|
|
7960
|
+
}
|
|
7961
|
+
function askServerResume(focus) {
|
|
7962
|
+
resumeAsked = true;
|
|
7963
|
+
var named = handbackProject;
|
|
7964
|
+
handbackProject = null;
|
|
7965
|
+
return fetch(API + '/provisioning/github-app/resume' + (named ? '?id=' + encodeURIComponent(named) : ''), { credentials: 'same-origin' })
|
|
7966
|
+
.then(function (res) { return res.ok ? res.json() : null; })
|
|
7967
|
+
.catch(function () { return null; })
|
|
7968
|
+
.then(function (d) {
|
|
7969
|
+
var p = d && d.project;
|
|
7970
|
+
var had = createdRecord();
|
|
7971
|
+
if (!p || !p.id || (!named && had && had.id)) { if (named) enter(focus); return; }
|
|
7972
|
+
var declined = null;
|
|
7973
|
+
try { declined = sessionStorage.getItem(DECLINED_KEY); } catch (e) {}
|
|
7974
|
+
if (!named && declined === String(p.id)) return;
|
|
7975
|
+
var rec = recordFromServer(p);
|
|
7976
|
+
/* the same project this tab already knew keeps what it knew (a demo's teammates) */
|
|
7977
|
+
if (had && String(had.id) === String(p.id)) rec = Object.assign({}, had, rec, { name: had.name || rec.name });
|
|
7978
|
+
else if (had && had.id) resetForNewProject(); /* a hand-back for a different project */
|
|
7979
|
+
serverRecord = rec;
|
|
7980
|
+
try { sessionStorage.setItem(CREATED_KEY, JSON.stringify(rec)); } catch (e) {}
|
|
7981
|
+
if (currentView !== 'new') return;
|
|
7982
|
+
if (named || (!focus && !String(state.name || '').trim())) { resumeIntent = true; enterCreated(rec, false); }
|
|
7983
|
+
else paintResumeOffer(rec);
|
|
7984
|
+
});
|
|
7985
|
+
}
|
|
7986
|
+
|
|
7901
7987
|
function enter(focus) {
|
|
7902
7988
|
if (authKnown && !me) { show(0, !!focus); return; }
|
|
7903
7989
|
var created = authKnown ? createdRecord() : null;
|
|
7990
|
+
/* this tab cannot say which project — ask the server (task 1004650). A named
|
|
7991
|
+
hand-back waits for that answer instead of painting another project first. */
|
|
7992
|
+
if (authKnown && !resumeAsked && (handbackProject || !(created && created.id))) {
|
|
7993
|
+
var waitForIt = !!handbackProject;
|
|
7994
|
+
askServerResume(!!focus);
|
|
7995
|
+
if (waitForIt) return;
|
|
7996
|
+
}
|
|
7904
7997
|
/* A draft on the page (step 1) beside a stored create is a SECOND project being
|
|
7905
7998
|
typed. A reload or a GitHub round-trip (repo access) is that same visit
|
|
7906
7999
|
continuing, so the draft keeps its place and the earlier project stays one
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.95",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.95",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.95",
|
|
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
|
@@ -9394,5 +9394,11 @@
|
|
|
9394
9394
|
"id": "1004649",
|
|
9395
9395
|
"text": "Connecting GitHub to a new project is now one trip. The founder clicks \"Connect GitHub\", approves the project's app on GitHub, and GitHub carries straight on to installing it on their repo (already picked when the repo is publ"
|
|
9396
9396
|
}
|
|
9397
|
+
],
|
|
9398
|
+
"1.21.95": [
|
|
9399
|
+
{
|
|
9400
|
+
"id": "1004650",
|
|
9401
|
+
"text": "Connecting GitHub no longer depends on staying in one browser tab. After you approve on GitHub, even on your phone or in another window, you land back on your own project's GitHub checklist. Our server remembers which project"
|
|
9402
|
+
}
|
|
9397
9403
|
]
|
|
9398
9404
|
}
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.95'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|