@bongos/core 1.19.209 → 1.19.211
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/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 +7 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/api/openapi.json +146 -4
- package/docs/api-reference.md +5 -3
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +4 -0
- package/modules/provisioning/routes/provisioning.js +18 -14
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +5 -2
- package/public-landing/projects.html +330 -32
- package/src/bongos/github-repos.js +61 -1
- package/src/bongos/routes/github.js +86 -1
- package/src/module-api.js +5 -1
- package/tests/github_repo_visibility.mjs +168 -0
- package/tests/github_repos_helpers.mjs +49 -0
- package/tests/projects_hub.mjs +158 -9
- package/tests/provisioning_settings.mjs +30 -1
- package/tests/public_landing_projects.mjs +24 -0
|
@@ -43,6 +43,12 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
43
43
|
.viewWrap{padding-bottom:40px;}
|
|
44
44
|
.card{margin-bottom:26px;}
|
|
45
45
|
|
|
46
|
+
/* ── the manage view (task 1003041): container rhythm only — every component
|
|
47
|
+
it draws (card, chip, addr, empty) is cosmos vocabulary ── */
|
|
48
|
+
.backRow{margin:14px 0;font-weight:700;}
|
|
49
|
+
#view-manage .empty{text-align:left;}
|
|
50
|
+
#view-manage .empty p{margin-bottom:0;}
|
|
51
|
+
|
|
46
52
|
/* The bongo pair on the New-project view. Component-scoped custom properties,
|
|
47
53
|
never :root (C1). The drum head is SKIN — using --bg-card would turn it black
|
|
48
54
|
in dark mode — and --drum-shadow is ink in light but PLAIN BLACK in dark,
|
|
@@ -557,6 +563,79 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
557
563
|
</div>
|
|
558
564
|
</section>
|
|
559
565
|
|
|
566
|
+
<!-- ══════════════════ VIEW · MANAGE ═══════════════════════════════════════
|
|
567
|
+
One owned project's management home (task 1003041, goal-1000058 wave).
|
|
568
|
+
Deep link: ?view=manage&id=N — goToManage() routes the map's owned
|
|
569
|
+
orbs/rows and the mine cards here. The API is the wall: the per-id read
|
|
570
|
+
is own-scoped (owner, or an archon for fleet support) and answers 404
|
|
571
|
+
for anyone else, so a stranger's deep link renders the same clean
|
|
572
|
+
"no project here" as a wrong id — never a permission hint. The settings
|
|
573
|
+
sections are the homes the sibling manage-wave tasks fill in; until each
|
|
574
|
+
ships, its section says so honestly instead of faking a control. -->
|
|
575
|
+
<section id="view-manage" hidden>
|
|
576
|
+
<div class="wrap viewWrap">
|
|
577
|
+
<p class="backRow"><a href="?view=mine" data-nav-to="mine">← Your projects</a></p>
|
|
578
|
+
|
|
579
|
+
<!-- signed out: the same identity-only door the mine view uses (ADR 0143) -->
|
|
580
|
+
<section id="mGate" class="card" hidden>
|
|
581
|
+
<p class="lede">This is a project’s management page — it answers only to the person who owns it. Sign in with GitHub to open yours.</p>
|
|
582
|
+
<a class="btn" id="mSignin" href="/api/bongos/auth/web/start?return=/projects%3Fview%3Dmine">
|
|
583
|
+
<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>
|
|
584
|
+
Sign in with GitHub
|
|
585
|
+
</a>
|
|
586
|
+
</section>
|
|
587
|
+
|
|
588
|
+
<!-- 404: one clean state for "not yours" and "doesn't exist" — the API
|
|
589
|
+
answers both identically on purpose, and so does this copy -->
|
|
590
|
+
<section id="mMissing" class="card" hidden>
|
|
591
|
+
<p class="lede">No project here. It may have been removed, or the link may be wrong.</p>
|
|
592
|
+
<a class="btn small ghost" href="?view=mine" data-nav-to="mine">Back to your projects</a>
|
|
593
|
+
</section>
|
|
594
|
+
|
|
595
|
+
<div class="alert" id="mAlert" role="alert"></div>
|
|
596
|
+
<div class="loading" id="mLoading">Reading the project…</div>
|
|
597
|
+
|
|
598
|
+
<section id="mMain" hidden>
|
|
599
|
+
<div class="hero" style="padding-left:0;padding-right:0">
|
|
600
|
+
<h1>Manage</h1>
|
|
601
|
+
<p class="lede">One project, up close — its address, its repository, and what the platform last <b>verified</b> about it, with a timestamp. The controls land here piece by piece; each section says what it can do today.</p>
|
|
602
|
+
</div>
|
|
603
|
+
|
|
604
|
+
<div class="card">
|
|
605
|
+
<div class="projHead"><span class="projName" id="mName">…</span><span id="mChip"></span></div>
|
|
606
|
+
<div class="projMeta" id="mMeta" hidden></div>
|
|
607
|
+
<div class="addr" id="mAddr" hidden></div>
|
|
608
|
+
<p class="finePrint" id="mCreated" hidden></p>
|
|
609
|
+
</div>
|
|
610
|
+
|
|
611
|
+
<div class="card" id="mVisibility">
|
|
612
|
+
<h2 style="font-family:var(--font-display);font-size:19px;margin-bottom:12px;">Who can see it</h2>
|
|
613
|
+
<div class="empty"><p>Choosing who can open this project’s pages — everyone, or only people signed in to it — isn’t wired up here yet. It lands in one of the next updates; until then every project keeps the behavior it already has.</p></div>
|
|
614
|
+
</div>
|
|
615
|
+
|
|
616
|
+
<div class="card" id="mJoinability">
|
|
617
|
+
<h2 style="font-family:var(--font-display);font-size:19px;margin-bottom:12px;">How people join</h2>
|
|
618
|
+
<div class="empty"><p>Choosing how new people join — walk right in, apply first, or invite only — isn’t wired up here yet. It lands in one of the next updates.</p></div>
|
|
619
|
+
</div>
|
|
620
|
+
|
|
621
|
+
<!-- live since task 1003045: reads the repo's real visibility from GitHub
|
|
622
|
+
with the owner's own repository credential. Flip-to-PUBLIC only — the
|
|
623
|
+
private direction is refused (server + copy) until the private-repo
|
|
624
|
+
deploy fix lands, because the deploy pull cannot read a private repo
|
|
625
|
+
(walk finding N4). -->
|
|
626
|
+
<div class="card" id="mRepoVisibility">
|
|
627
|
+
<h2 style="font-family:var(--font-display);font-size:19px;margin-bottom:12px;">Repository visibility</h2>
|
|
628
|
+
<div id="mRepoVis"><p class="finePrint">Checking the repository on GitHub…</p></div>
|
|
629
|
+
</div>
|
|
630
|
+
|
|
631
|
+
<div class="card" id="mDelete">
|
|
632
|
+
<h2 style="font-family:var(--font-display);font-size:19px;margin-bottom:12px;">Delete the project</h2>
|
|
633
|
+
<div class="empty"><p>Deleting a project from this page isn’t wired up yet. A running project can still be torn down from its card under <a href="?view=mine" data-nav-to="mine">Your projects</a> — that stops the server and keeps your repository.</p></div>
|
|
634
|
+
</div>
|
|
635
|
+
</section>
|
|
636
|
+
</div>
|
|
637
|
+
</section>
|
|
638
|
+
|
|
560
639
|
<!-- ══════════════════ VIEW · NEW PROJECT ══════════════════════════════════
|
|
561
640
|
The whole create wizard, absorbed from /create (task 1002763; direction
|
|
562
641
|
§4 — that route now 302s here). Seven panels behind the five-beat rail; the
|
|
@@ -912,12 +991,23 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
912
991
|
pointed at /projects?view=new) actually arrive somewhere.
|
|
913
992
|
══════════════════════════════════════════════════════════════════════════ */
|
|
914
993
|
|
|
915
|
-
var VIEWS = { map: 1, mine: 1, new: 1 };
|
|
994
|
+
var VIEWS = { map: 1, mine: 1, new: 1, manage: 1 };
|
|
916
995
|
var currentView = 'map';
|
|
996
|
+
/* the manage view's target project (task 1003041) — set by goToManage() and
|
|
997
|
+
the ?view=manage&id=N deep link, read by loadManage() */
|
|
998
|
+
var manageId = null;
|
|
999
|
+
var manageInst = null;
|
|
917
1000
|
|
|
918
1001
|
function viewFromUrl() {
|
|
919
1002
|
var v;
|
|
920
1003
|
try { v = new URLSearchParams(location.search).get('view'); } catch (e) { v = null; }
|
|
1004
|
+
if (v === 'manage') {
|
|
1005
|
+
/* per-project: no id names no project — the list is the honest landing */
|
|
1006
|
+
var id = null;
|
|
1007
|
+
try { id = new URLSearchParams(location.search).get('id'); } catch (e) {}
|
|
1008
|
+
if (id) { manageId = String(id); return 'manage'; }
|
|
1009
|
+
return 'mine';
|
|
1010
|
+
}
|
|
921
1011
|
return VIEWS[v] ? v : 'map';
|
|
922
1012
|
}
|
|
923
1013
|
|
|
@@ -930,14 +1020,20 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
930
1020
|
if (a.dataset.nav !== 'home' && a.dataset.nav === v) a.setAttribute('aria-current', 'page');
|
|
931
1021
|
else a.removeAttribute('aria-current');
|
|
932
1022
|
});
|
|
933
|
-
|
|
1023
|
+
/* the manage view's URL names its project — the deep-link contract grew
|
|
1024
|
+
an id in task 1003041, so a manage tab is shareable and reload-safe */
|
|
1025
|
+
var idPart = (v === 'manage' && manageId) ? '&id=' + encodeURIComponent(manageId) : '';
|
|
1026
|
+
try { history.replaceState(null, '', '?view=' + v + idPart); } catch (e) {}
|
|
934
1027
|
/* the shell's sign-in door returns the visitor to the view they left, and
|
|
935
1028
|
carries the repo scope when that view is the wizard (ADR 0143: the
|
|
936
1029
|
create path asks once up front instead of a second consent at Home) */
|
|
937
|
-
$('signinTop').href = '/api/bongos/auth/web/start?return=/projects%3Fview%3D' + v +
|
|
1030
|
+
$('signinTop').href = '/api/bongos/auth/web/start?return=/projects%3Fview%3D' + v +
|
|
1031
|
+
idPart.replace('&', '%26').replace('=', '%3D') + (v === 'new' ? '&repos=1' : '');
|
|
938
1032
|
/* the mine view is the only one that polls; it re-reads on entry so a
|
|
939
1033
|
card that changed while you were on the map is not stale */
|
|
940
1034
|
if (v === 'mine' && me) refreshList();
|
|
1035
|
+
/* manage re-reads its project on entry — no timer lives here (C8) */
|
|
1036
|
+
if (v === 'manage') loadManage();
|
|
941
1037
|
/* owned chips refresh on re-entry, never on a timer — C8 keeps the map quiet */
|
|
942
1038
|
if (v === 'map' && me) loadOwnedInstances().then(applyMapFilter);
|
|
943
1039
|
/* the living sky runs only on stage (C8) — start and stop with the view;
|
|
@@ -1169,11 +1265,11 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
1169
1265
|
var href = safeHttpUrl(p.origin);
|
|
1170
1266
|
if (inst) {
|
|
1171
1267
|
/* an OWNED entry — catalog-annotated or owned-only, one code path.
|
|
1172
|
-
Its job is MANAGE (direction §5):
|
|
1173
|
-
project
|
|
1174
|
-
middle-click lands a fresh tab on the
|
|
1175
|
-
|
|
1176
|
-
a.href = '?view=
|
|
1268
|
+
Its job is MANAGE (direction §5): a plain click opens that
|
|
1269
|
+
project's manage view (task 1003041), and the href IS that view's
|
|
1270
|
+
deep link, so a middle/ctrl-click lands a fresh tab on the same
|
|
1271
|
+
honest destination. The public-site door stays on the manage page. */
|
|
1272
|
+
a.href = '?view=manage&id=' + encodeURIComponent(String(inst.id));
|
|
1177
1273
|
a.setAttribute('data-manage', String(inst.id));
|
|
1178
1274
|
} else {
|
|
1179
1275
|
/* The href STAYS, and stays honest, even though a plain click now opens
|
|
@@ -1441,16 +1537,14 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
1441
1537
|
if (p) openCard(p, relOf(p), a);
|
|
1442
1538
|
});
|
|
1443
1539
|
|
|
1444
|
-
/* ── Click-to-manage (task 1002744): an owned orb
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
var ANCHOR_GLOW_MS = 2200;
|
|
1449
|
-
var pendingAnchor = null;
|
|
1540
|
+
/* ── Click-to-manage (task 1002744 → 1003041): an owned orb, row or card
|
|
1541
|
+
opens that project's own manage view — the per-project page the
|
|
1542
|
+
manage-wave tasks fill in. It replaced the scroll-anchor-into-the-list
|
|
1543
|
+
this function used to do. ── */
|
|
1450
1544
|
function goToManage(instId) {
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1545
|
+
manageId = String(instId || '');
|
|
1546
|
+
manageInst = null;
|
|
1547
|
+
setView('manage');
|
|
1454
1548
|
}
|
|
1455
1549
|
/* Card-shell lookup by instance id. CSS.escape, not esc(): the data-inst
|
|
1456
1550
|
attribute holds the DECODED value, so an HTML-escaped id would never match
|
|
@@ -1459,18 +1553,6 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
1459
1553
|
function liFor(id) {
|
|
1460
1554
|
return document.querySelector('li[data-inst="' + CSS.escape(String(id == null ? '' : id)) + '"]');
|
|
1461
1555
|
}
|
|
1462
|
-
function tryAnchor() {
|
|
1463
|
-
if (!pendingAnchor) return;
|
|
1464
|
-
var li = liFor(pendingAnchor);
|
|
1465
|
-
if (!li) return;
|
|
1466
|
-
pendingAnchor = null;
|
|
1467
|
-
/* a search or state filter that hides the target would turn the anchor
|
|
1468
|
-
into a silent no-op — clear them the same way the visitor would */
|
|
1469
|
-
if (li.hidden) $('mineClear').click();
|
|
1470
|
-
li.scrollIntoView({ block: 'center' });
|
|
1471
|
-
li.classList.add('anchored');
|
|
1472
|
-
setTimeout(function () { li.classList.remove('anchored'); }, ANCHOR_GLOW_MS);
|
|
1473
|
-
}
|
|
1474
1556
|
|
|
1475
1557
|
$('mapRows').addEventListener('click', function (ev) {
|
|
1476
1558
|
/* the row's own "Open →" keeps its job — the card is for the rest of the row */
|
|
@@ -3630,7 +3712,9 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3630
3712
|
}
|
|
3631
3713
|
var lm = lastMsg[inst.id];
|
|
3632
3714
|
body += '<div class="projMsg' + (lm && lm.cls ? ' ' + lm.cls : '') + '" id="msg-' + iid + '">' + (lm ? lm.text : '') + '</div>';
|
|
3633
|
-
|
|
3715
|
+
/* every owned card opens its manage view (task 1003041) — a real link, so
|
|
3716
|
+
middle/ctrl-click works; the delegated handler upgrades a plain click */
|
|
3717
|
+
var acts = ['<a class="btn small ghost" data-manage="' + iid + '" href="?view=manage&id=' + iid + '">Manage</a>'];
|
|
3634
3718
|
if (canRetry(inst)) acts.push('<button type="button" class="btn small" data-retry="' + iid + '">Try again</button>');
|
|
3635
3719
|
if (canRevive(inst)) acts.push('<button type="button" class="btn small" data-revive="' + iid + '">Bring it back</button>');
|
|
3636
3720
|
if (canTeardown(inst)) acts.push('<button type="button" class="btn small danger" data-teardown="' + iid + '">Tear it down</button>');
|
|
@@ -3663,7 +3747,6 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3663
3747
|
});
|
|
3664
3748
|
recountMine();
|
|
3665
3749
|
applyMine();
|
|
3666
|
-
tryAnchor(); /* a click-to-manage jump may be waiting on this render */
|
|
3667
3750
|
}
|
|
3668
3751
|
|
|
3669
3752
|
function msg(id, text, cls) {
|
|
@@ -3753,7 +3836,6 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3753
3836
|
var sameSet = instances.length === known && instances.every(function (i) { return byId[i.id]; });
|
|
3754
3837
|
if (!sameSet) { render(instances); return; }
|
|
3755
3838
|
instances.forEach(function (inst) { takeUpdate(inst.id, inst); });
|
|
3756
|
-
tryAnchor(); /* same-set refresh renders no shells — retry here too */
|
|
3757
3839
|
})
|
|
3758
3840
|
.catch(function () { /* transient — next tick will try again */ });
|
|
3759
3841
|
}
|
|
@@ -3767,6 +3849,8 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3767
3849
|
/* returning to a MAP tab re-reads the owned overlay once — event-driven,
|
|
3768
3850
|
like the mine view's entry refresh, not a timer */
|
|
3769
3851
|
if (!document.hidden && me && currentView === 'map') loadOwnedInstances().then(applyMapFilter);
|
|
3852
|
+
/* returning to a MANAGE tab re-reads its one project the same way */
|
|
3853
|
+
if (!document.hidden && currentView === 'manage' && manageId) loadManage(true);
|
|
3770
3854
|
});
|
|
3771
3855
|
|
|
3772
3856
|
/* C6 freshness without C8's cost: once a minute the visible MAP refreshes
|
|
@@ -3793,6 +3877,212 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3793
3877
|
}
|
|
3794
3878
|
setInterval(mapChipTick, REFRESH_MS);
|
|
3795
3879
|
|
|
3880
|
+
/* ── the manage view (task 1003041): one owned project's management home.
|
|
3881
|
+
The API is the wall — the per-id read is own-scoped with an archon
|
|
3882
|
+
override and 404s for anyone else — so this code only renders what it
|
|
3883
|
+
answers. The data re-reads on entry and tab return, never on a fetch
|
|
3884
|
+
timer (C8); the once-a-minute tick below re-renders from data already
|
|
3885
|
+
in hand so "checked Xm ago" keeps telling the truth (C6). ── */
|
|
3886
|
+
|
|
3887
|
+
function manageSignin() {
|
|
3888
|
+
return API + '/auth/web/start?return=' +
|
|
3889
|
+
encodeURIComponent('/projects?view=manage&id=' + manageId);
|
|
3890
|
+
}
|
|
3891
|
+
|
|
3892
|
+
function renderManage(inst) {
|
|
3893
|
+
manageInst = inst;
|
|
3894
|
+
$('mLoading').hidden = true;
|
|
3895
|
+
$('mGate').hidden = true;
|
|
3896
|
+
$('mMissing').hidden = true;
|
|
3897
|
+
$('mMain').hidden = false;
|
|
3898
|
+
$('mName').textContent = inst.slug || '';
|
|
3899
|
+
/* the ONE chip site of this view — chipFor() or nothing, so the manage
|
|
3900
|
+
page can never tell a different liveness story than the card (C6) */
|
|
3901
|
+
$('mChip').innerHTML = chipFor(inst);
|
|
3902
|
+
var meta = [];
|
|
3903
|
+
if (inst.target_ref) {
|
|
3904
|
+
meta.push('repo <a href="https://github.com/' + esc(inst.target_ref) +
|
|
3905
|
+
'" target="_blank" rel="noopener"><code>' + esc(inst.target_ref) + '</code></a>');
|
|
3906
|
+
}
|
|
3907
|
+
if (inst.hosting_shape) meta.push(esc(SHAPE_LABEL[inst.hosting_shape] || inst.hosting_shape));
|
|
3908
|
+
if (inst.onboard_mode === 'adopt') meta.push('adopted repo');
|
|
3909
|
+
$('mMeta').innerHTML = meta.join('<span aria-hidden="true">·</span>');
|
|
3910
|
+
$('mMeta').hidden = !meta.length;
|
|
3911
|
+
var addr = addressRow(inst);
|
|
3912
|
+
$('mAddr').innerHTML = addr;
|
|
3913
|
+
$('mAddr').hidden = !addr;
|
|
3914
|
+
/* a fact with no answer hides — never a dash (the card rule) */
|
|
3915
|
+
$('mCreated').textContent = inst.created_at ? 'Created ' + listedDate(inst.created_at) : '';
|
|
3916
|
+
$('mCreated').hidden = !inst.created_at;
|
|
3917
|
+
}
|
|
3918
|
+
|
|
3919
|
+
function loadManage(quiet) {
|
|
3920
|
+
if (!manageId) return;
|
|
3921
|
+
$('mAlert').classList.remove('on');
|
|
3922
|
+
$('mSignin').href = manageSignin();
|
|
3923
|
+
if (!quiet) {
|
|
3924
|
+
$('mMain').hidden = true;
|
|
3925
|
+
$('mGate').hidden = true;
|
|
3926
|
+
$('mMissing').hidden = true;
|
|
3927
|
+
$('mLoading').hidden = false;
|
|
3928
|
+
}
|
|
3929
|
+
var asked = manageId; /* a stale answer must never paint a newer project */
|
|
3930
|
+
fetch(API + '/provisioning/instances/' + encodeURIComponent(manageId), { credentials: 'same-origin' })
|
|
3931
|
+
.then(function (res) {
|
|
3932
|
+
if (asked !== manageId) throw new Error('stale');
|
|
3933
|
+
if (res.status === 401) {
|
|
3934
|
+
$('mLoading').hidden = true; $('mMain').hidden = true; $('mGate').hidden = false;
|
|
3935
|
+
throw new Error('handled');
|
|
3936
|
+
}
|
|
3937
|
+
/* 404 is ONE clean answer for "not yours" and "doesn't exist" — the
|
|
3938
|
+
API refuses both identically, so this page cannot leak existence */
|
|
3939
|
+
if (res.status === 404) {
|
|
3940
|
+
$('mLoading').hidden = true; $('mMain').hidden = true; $('mMissing').hidden = false;
|
|
3941
|
+
throw new Error('handled');
|
|
3942
|
+
}
|
|
3943
|
+
if (!res.ok) throw new Error('soft');
|
|
3944
|
+
return res.json();
|
|
3945
|
+
})
|
|
3946
|
+
.then(function (data) {
|
|
3947
|
+
var inst = data.instance || {};
|
|
3948
|
+
inst.open_intent = data.open_intent || null;
|
|
3949
|
+
renderManage(inst);
|
|
3950
|
+
/* the SECOND, GitHub-backed read — fired only here (entry + tab
|
|
3951
|
+
return), never from renderManage, which the fetch-free tick calls */
|
|
3952
|
+
loadRepoVisibility(inst);
|
|
3953
|
+
})
|
|
3954
|
+
.catch(function (err) {
|
|
3955
|
+
var m = String(err && err.message);
|
|
3956
|
+
if (m === 'handled' || m === 'stale') return;
|
|
3957
|
+
$('mLoading').hidden = true;
|
|
3958
|
+
var a = $('mAlert');
|
|
3959
|
+
a.textContent = 'Couldn’t read this project right now — refresh in a moment.';
|
|
3960
|
+
a.classList.add('on');
|
|
3961
|
+
});
|
|
3962
|
+
}
|
|
3963
|
+
|
|
3964
|
+
function manageChipTick() {
|
|
3965
|
+
if (document.hidden || currentView !== 'manage' || !manageInst) return;
|
|
3966
|
+
renderManage(manageInst); /* zero requests — re-dates "checked Xm ago" */
|
|
3967
|
+
}
|
|
3968
|
+
setInterval(manageChipTick, REFRESH_MS);
|
|
3969
|
+
|
|
3970
|
+
/* ── repo visibility (task 1003045): read live from GitHub with the owner's
|
|
3971
|
+
own repository credential; flip-to-PUBLIC only. The private direction is
|
|
3972
|
+
refused by the server — and unbuilt here — until the private-repo deploy
|
|
3973
|
+
fix lands: the deploy pull runs as the shared deploy key, which cannot
|
|
3974
|
+
read a private repo (walk finding N4), so flipping private would break
|
|
3975
|
+
the project's next deploy. Every claim below is what GitHub answered for
|
|
3976
|
+
THIS account — never a cached or guessed state. ── */
|
|
3977
|
+
|
|
3978
|
+
var manageRepo = null; /* GitHub's last answer for the manage view's repo */
|
|
3979
|
+
|
|
3980
|
+
function repoVisReconnect() {
|
|
3981
|
+
return API + '/auth/web/start?repos=1&return=' +
|
|
3982
|
+
encodeURIComponent('/projects?view=manage&id=' + manageId);
|
|
3983
|
+
}
|
|
3984
|
+
|
|
3985
|
+
function repoVisMsg(html) { $('mRepoVis').innerHTML = html; }
|
|
3986
|
+
|
|
3987
|
+
function renderRepoVis(repo) {
|
|
3988
|
+
if (repo.private) {
|
|
3989
|
+
repoVisMsg(
|
|
3990
|
+
'<div class="addr">This repository is <b>private</b> — only accounts it’s shared with can read its code.</div>' +
|
|
3991
|
+
'<div class="projMsg err">The platform can’t deploy private repositories yet, so deploys for this project will fail while it stays private.</div>' +
|
|
3992
|
+
(repo.admin
|
|
3993
|
+
? '<div class="actions"><button type="button" class="btn small" data-repo-public>Make it public</button></div>'
|
|
3994
|
+
: '<p class="finePrint">Your GitHub account can’t change this repository’s settings.</p>'));
|
|
3995
|
+
} else {
|
|
3996
|
+
repoVisMsg(
|
|
3997
|
+
'<div class="addr">This repository is <b>public</b> — anyone can read its code.</div>' +
|
|
3998
|
+
'<p class="finePrint">Making it private isn’t possible from here yet: the platform can’t deploy private repositories. The switch lands with that fix.</p>');
|
|
3999
|
+
}
|
|
4000
|
+
}
|
|
4001
|
+
|
|
4002
|
+
function loadRepoVisibility(inst) {
|
|
4003
|
+
manageRepo = null;
|
|
4004
|
+
if (!inst.target_ref) {
|
|
4005
|
+
repoVisMsg('<p class="finePrint">No repository is linked to this project.</p>');
|
|
4006
|
+
return;
|
|
4007
|
+
}
|
|
4008
|
+
repoVisMsg('<p class="finePrint">Checking the repository on GitHub…</p>');
|
|
4009
|
+
var asked = manageId; /* a stale answer must never describe a newer project */
|
|
4010
|
+
fetch(API + '/github/repo?repo=' + encodeURIComponent(inst.target_ref), { credentials: 'same-origin' })
|
|
4011
|
+
.then(function (res) {
|
|
4012
|
+
if (asked !== manageId) throw new Error('stale');
|
|
4013
|
+
return jsonOrEmpty(res).then(function (data) {
|
|
4014
|
+
if (res.ok) { manageRepo = data.repo || {}; renderRepoVis(manageRepo); return; }
|
|
4015
|
+
var e = apiError(data, res.status);
|
|
4016
|
+
if (e.code === 'repo_scope_required') {
|
|
4017
|
+
/* the stored repository credential lives an hour (ADR 0155) — the
|
|
4018
|
+
owner reconnects through the same door the create wizard uses */
|
|
4019
|
+
repoVisMsg('<p class="finePrint">Seeing and changing this here needs repository access. ' +
|
|
4020
|
+
'<a href="' + repoVisReconnect() + '">Reconnect GitHub with repository access</a>.</p>');
|
|
4021
|
+
} else if (e.code === 'github_repos_unavailable') {
|
|
4022
|
+
repoVisMsg('<p class="finePrint">Repository access isn’t configured on this server, so visibility can’t be read from here.</p>');
|
|
4023
|
+
} else if (e.code === 'repo_not_found') {
|
|
4024
|
+
repoVisMsg('<p class="finePrint">GitHub didn’t show this repository to your account — it may have been deleted, renamed, or belong to a different account.</p>');
|
|
4025
|
+
} else if (res.status === 401) {
|
|
4026
|
+
repoVisMsg('<p class="finePrint">' + signInAgain() + '</p>');
|
|
4027
|
+
} else {
|
|
4028
|
+
repoVisMsg('<p class="finePrint">GitHub couldn’t be reached right now — refresh in a moment.</p>');
|
|
4029
|
+
}
|
|
4030
|
+
});
|
|
4031
|
+
})
|
|
4032
|
+
.catch(function (err) {
|
|
4033
|
+
if (String(err && err.message) === 'stale') return;
|
|
4034
|
+
repoVisMsg('<p class="finePrint">GitHub couldn’t be reached right now — refresh in a moment.</p>');
|
|
4035
|
+
});
|
|
4036
|
+
}
|
|
4037
|
+
|
|
4038
|
+
/* flipping public exposes the code to everyone — never one accidental click */
|
|
4039
|
+
function askRepoPublic() {
|
|
4040
|
+
var act = document.querySelector('#mRepoVis .actions');
|
|
4041
|
+
if (!act) return;
|
|
4042
|
+
act.innerHTML = '<div class="confirmRow">' +
|
|
4043
|
+
'<span class="plain">Make it public? Anyone on the internet will be able to read the code.</span>' +
|
|
4044
|
+
'<button type="button" class="btn small" data-repo-public-yes>Yes, make it public</button>' +
|
|
4045
|
+
'<button type="button" class="linklike" data-repo-public-no>Keep it private</button></div>';
|
|
4046
|
+
}
|
|
4047
|
+
|
|
4048
|
+
function doRepoPublic() {
|
|
4049
|
+
if (!manageInst || !manageInst.target_ref) return;
|
|
4050
|
+
repoVisMsg('<p class="finePrint">Asking GitHub…</p>');
|
|
4051
|
+
fetch(API + '/github/repo/visibility', {
|
|
4052
|
+
method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' },
|
|
4053
|
+
body: JSON.stringify({ repo: manageInst.target_ref, visibility: 'public' })
|
|
4054
|
+
}).then(function (res) {
|
|
4055
|
+
return jsonOrEmpty(res).then(function (data) {
|
|
4056
|
+
if (res.ok) {
|
|
4057
|
+
/* GitHub's own answer paints the new state — never an assumption */
|
|
4058
|
+
manageRepo = data.repo || {};
|
|
4059
|
+
renderRepoVis(manageRepo);
|
|
4060
|
+
return;
|
|
4061
|
+
}
|
|
4062
|
+
var e = apiError(data, res.status);
|
|
4063
|
+
/* say what the server said, on top of the still-true state */
|
|
4064
|
+
if (manageRepo) renderRepoVis(manageRepo);
|
|
4065
|
+
else repoVisMsg('');
|
|
4066
|
+
$('mRepoVis').insertAdjacentHTML('beforeend',
|
|
4067
|
+
res.status === 401
|
|
4068
|
+
? '<div class="projMsg err">' + signInAgain() + '</div>'
|
|
4069
|
+
: '<div class="projMsg err">' + esc(e.message || 'The change could not be made — try again in a moment.') + '</div>');
|
|
4070
|
+
});
|
|
4071
|
+
}).catch(function () {
|
|
4072
|
+
if (manageRepo) renderRepoVis(manageRepo);
|
|
4073
|
+
$('mRepoVis').insertAdjacentHTML('beforeend',
|
|
4074
|
+
'<div class="projMsg err">Couldn’t reach the platform — check your connection and try again.</div>');
|
|
4075
|
+
});
|
|
4076
|
+
}
|
|
4077
|
+
|
|
4078
|
+
$('mRepoVis').addEventListener('click', function (ev) {
|
|
4079
|
+
var t = ev.target.closest && ev.target.closest('button');
|
|
4080
|
+
if (!t) return;
|
|
4081
|
+
if (t.hasAttribute('data-repo-public')) return askRepoPublic();
|
|
4082
|
+
if (t.hasAttribute('data-repo-public-yes')) return doRepoPublic();
|
|
4083
|
+
if (t.hasAttribute('data-repo-public-no')) { if (manageRepo) renderRepoVis(manageRepo); }
|
|
4084
|
+
});
|
|
4085
|
+
|
|
3796
4086
|
/* ── teardown: ask, confirm (the truth, itemized — direction §5.4), then
|
|
3797
4087
|
SAY exactly what the server said ── */
|
|
3798
4088
|
|
|
@@ -3923,6 +4213,14 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3923
4213
|
|
|
3924
4214
|
/* one delegated listener for all card actions */
|
|
3925
4215
|
$('projectsList').addEventListener('click', function (ev) {
|
|
4216
|
+
/* the Manage link is a real anchor — a modified click keeps its browser
|
|
4217
|
+
meaning, a plain one stays in-page (the orb-click contract) */
|
|
4218
|
+
var m = ev.target.closest && ev.target.closest('a[data-manage]');
|
|
4219
|
+
if (m) {
|
|
4220
|
+
if (!isPlainClick(ev)) return;
|
|
4221
|
+
ev.preventDefault();
|
|
4222
|
+
return goToManage(m.getAttribute('data-manage'));
|
|
4223
|
+
}
|
|
3926
4224
|
var t = ev.target.closest('button');
|
|
3927
4225
|
if (!t) return;
|
|
3928
4226
|
if (t.hasAttribute('data-teardown')) return askTeardown(t.getAttribute('data-teardown'));
|
|
@@ -102,4 +102,64 @@ async function createUserRepo(token, { name, fetchImpl = fetch } = {}) {
|
|
|
102
102
|
throw new GithubApiError(`GitHub create-repo failed (${res.status})`, { status: res.status, code: 'github_error' });
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
// Read one repo as GitHub answers it FOR THIS TOKEN — the manage page's
|
|
106
|
+
// visibility read (task 1003045). GitHub's own 404 covers both "no such repo"
|
|
107
|
+
// and "no access", which is exactly the no-leak shape the caller wants to
|
|
108
|
+
// forward. `permissions.admin` says whether this token could change settings.
|
|
109
|
+
// Returns { full_name, html_url, private, admin }.
|
|
110
|
+
async function getUserRepo(token, { owner, name, fetchImpl = fetch } = {}) {
|
|
111
|
+
const res = await fetchImpl(`${GH_API}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`, {
|
|
112
|
+
headers: ghHeaders(token),
|
|
113
|
+
});
|
|
114
|
+
if (res.status === 401) {
|
|
115
|
+
throw new GithubApiError('GitHub rejected the stored token', { status: 401, code: 'github_unauthorized' });
|
|
116
|
+
}
|
|
117
|
+
if (res.status === 404) {
|
|
118
|
+
throw new GithubApiError('GitHub answered 404 for that repository', { status: 404, code: 'repo_not_found' });
|
|
119
|
+
}
|
|
120
|
+
if (!res.ok) {
|
|
121
|
+
throw new GithubApiError(`GitHub /repos read failed (${res.status})`, { status: res.status, code: 'github_error' });
|
|
122
|
+
}
|
|
123
|
+
const r = await res.json();
|
|
124
|
+
return {
|
|
125
|
+
full_name: r.full_name,
|
|
126
|
+
html_url: r.html_url,
|
|
127
|
+
private: !!r.private,
|
|
128
|
+
admin: !!(r.permissions && r.permissions.admin),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Flip one repo PUBLIC — and only public. Like createUserRepo's private:false,
|
|
133
|
+
// the direction is deliberately not a parameter: the deploy pull still runs as
|
|
134
|
+
// the shared deploy key that cannot read a private repo (walk finding N4), so
|
|
135
|
+
// nothing in this codebase may flip a repo private until that fix lands — the
|
|
136
|
+
// route refuses the request; this helper makes the refusal structural
|
|
137
|
+
// (task 1003045). Returns the same shape as getUserRepo.
|
|
138
|
+
async function makeRepoPublic(token, { owner, name, fetchImpl = fetch } = {}) {
|
|
139
|
+
const res = await fetchImpl(`${GH_API}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`, {
|
|
140
|
+
method: 'PATCH',
|
|
141
|
+
headers: ghHeaders(token, { 'Content-Type': 'application/json' }),
|
|
142
|
+
body: JSON.stringify({ private: false }),
|
|
143
|
+
});
|
|
144
|
+
if (res.status === 401) {
|
|
145
|
+
throw new GithubApiError('GitHub rejected the stored token', { status: 401, code: 'github_unauthorized' });
|
|
146
|
+
}
|
|
147
|
+
if (res.status === 403) {
|
|
148
|
+
throw new GithubApiError('GitHub refused the change for this account', { status: 403, code: 'not_repo_admin' });
|
|
149
|
+
}
|
|
150
|
+
if (res.status === 404) {
|
|
151
|
+
throw new GithubApiError('GitHub answered 404 for that repository', { status: 404, code: 'repo_not_found' });
|
|
152
|
+
}
|
|
153
|
+
if (!res.ok) {
|
|
154
|
+
throw new GithubApiError(`GitHub repo-visibility change failed (${res.status})`, { status: res.status, code: 'github_error' });
|
|
155
|
+
}
|
|
156
|
+
const r = await res.json();
|
|
157
|
+
return {
|
|
158
|
+
full_name: r.full_name,
|
|
159
|
+
html_url: r.html_url,
|
|
160
|
+
private: !!r.private,
|
|
161
|
+
admin: !!(r.permissions && r.permissions.admin),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = { listUserRepos, createUserRepo, getUserRepo, makeRepoPublic, GithubApiError, MAX_PAGES };
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// src/bongos/routes/github.js — GitHub repo list/create for the signed-in user, the
|
|
2
|
-
// server half of the onboarding wizard's repo picker (adopt) + auto-create (fresh)
|
|
2
|
+
// server half of the onboarding wizard's repo picker (adopt) + auto-create (fresh),
|
|
3
|
+
// plus the manage page's repo-visibility read/flip (task 1003045 — public only; the
|
|
4
|
+
// private direction is refused until the private-repo deploy fix lands).
|
|
3
5
|
// Task 1002099 / ADR 0143.
|
|
4
6
|
//
|
|
5
7
|
// These act on the user's OWN GitHub identity, so they live in core auth alongside
|
|
@@ -18,11 +20,29 @@ const auth = require('../auth');
|
|
|
18
20
|
const db = require('../db');
|
|
19
21
|
const secretBox = require('../secret-box');
|
|
20
22
|
const { listUserRepos, createUserRepo } = require('../github-repos');
|
|
23
|
+
// Namespace require for the visibility helpers (task 1003045): the route tests
|
|
24
|
+
// stub these on the cached module object, which a load-time destructure would
|
|
25
|
+
// pin to the real implementations.
|
|
26
|
+
const githubRepos = require('../github-repos');
|
|
21
27
|
const { validateOrRespond } = require('./_helpers');
|
|
22
28
|
|
|
23
29
|
// GitHub repo names: 1–100 chars of [A-Za-z0-9._-]. We validate strictly for a clean
|
|
24
30
|
// 400; anything GitHub still rejects surfaces as the 422→409 name_taken path.
|
|
25
31
|
const REPO_NAME_RE = /^[A-Za-z0-9._-]{1,100}$/;
|
|
32
|
+
// owner/name for the visibility routes: a GitHub login (1–39 chars, alnum + inner
|
|
33
|
+
// hyphens) over a repo name. Strict for a clean 400; GitHub still owns the truth.
|
|
34
|
+
const REPO_REF_RE = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})?\/[A-Za-z0-9._-]{1,100}$/;
|
|
35
|
+
|
|
36
|
+
// Parse + validate the owner/name ref, or send a 400 and return null.
|
|
37
|
+
function parseRepoRef(raw, res) {
|
|
38
|
+
const ref = String(raw || '').trim();
|
|
39
|
+
if (!REPO_REF_RE.test(ref)) {
|
|
40
|
+
res.fail('bad_repo_ref', { status: 400, message: 'Name the repository as owner/name.' });
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const [owner, name] = ref.split('/');
|
|
44
|
+
return { owner, name };
|
|
45
|
+
}
|
|
26
46
|
|
|
27
47
|
// Resolve the caller's stored repo token, or send the right failure and return null.
|
|
28
48
|
// Centralizes the 503-vs-403 decision both endpoints share.
|
|
@@ -97,5 +117,70 @@ module.exports = function buildGithubRouter() {
|
|
|
97
117
|
}
|
|
98
118
|
});
|
|
99
119
|
|
|
120
|
+
// rank: any-builder — reads one repo AS the caller's own GitHub credential sees
|
|
121
|
+
// it (the manage page's repo-visibility read, task 1003045). GitHub's 404 covers
|
|
122
|
+
// both "no such repo" and "no access" — forwarded as-is, no existence leak.
|
|
123
|
+
router.get('/github/repo', auth.requireBuilder, async (req, res) => {
|
|
124
|
+
const ref = parseRepoRef(req.query.repo, res);
|
|
125
|
+
if (!ref) return;
|
|
126
|
+
const token = await resolveRepoToken(req, res);
|
|
127
|
+
if (!token) return;
|
|
128
|
+
try {
|
|
129
|
+
const repo = await githubRepos.getUserRepo(token, ref);
|
|
130
|
+
// private_deployable is the platform truth the UI copy leans on: the deploy
|
|
131
|
+
// pull runs as the shared deploy key, which cannot read a private repo (N4).
|
|
132
|
+
res.json({ ok: true, repo, private_deployable: false });
|
|
133
|
+
} catch (err) {
|
|
134
|
+
if (err && err.status === 401) {
|
|
135
|
+
await db.deleteGithubToken({ builderId: req.builder.id }).catch(() => {});
|
|
136
|
+
return res.fail('repo_scope_required', { status: 403, message: 'Your GitHub authorization expired. Reconnect to read the repository.' });
|
|
137
|
+
}
|
|
138
|
+
if (err && err.code === 'repo_not_found') {
|
|
139
|
+
return res.fail('repo_not_found', { status: 404, message: 'GitHub didn’t show that repository to your account.' });
|
|
140
|
+
}
|
|
141
|
+
console.error('[gds] GET /github/repo', err && err.message);
|
|
142
|
+
res.fail('github_error', { status: 502, message: 'GitHub could not be reached. Try again in a moment.' });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// rank: any-builder — flips one of the caller's repos PUBLIC, with their own
|
|
147
|
+
// credential. The private direction is REFUSED here, before GitHub is ever
|
|
148
|
+
// called: the deploy pull still runs as the shared deploy key that cannot read
|
|
149
|
+
// a private repo (walk finding N4), so flipping private would break the
|
|
150
|
+
// project's next deploy — the refusal is the honest leg task 1003045 ships.
|
|
151
|
+
router.post('/github/repo/visibility', auth.requireBuilder, async (req, res) => {
|
|
152
|
+
if (validateOrRespond(req, res, {
|
|
153
|
+
repo: { required: true, type: 'string', maxLength: 140 },
|
|
154
|
+
visibility: { required: true, type: 'string', enum: ['public', 'private'] },
|
|
155
|
+
})) return;
|
|
156
|
+
const ref = parseRepoRef(req.body.repo, res);
|
|
157
|
+
if (!ref) return;
|
|
158
|
+
if (req.body.visibility !== 'public') {
|
|
159
|
+
return res.fail('private_not_deployable', {
|
|
160
|
+
status: 409,
|
|
161
|
+
message: 'Private repositories can’t be deployed yet — the platform’s deploy pull can’t read them, so making this repository private would break the project’s next deploy. This lands with the private-repo deploy fix.',
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
const token = await resolveRepoToken(req, res);
|
|
165
|
+
if (!token) return;
|
|
166
|
+
try {
|
|
167
|
+
const repo = await githubRepos.makeRepoPublic(token, ref);
|
|
168
|
+
res.json({ ok: true, repo, private_deployable: false });
|
|
169
|
+
} catch (err) {
|
|
170
|
+
if (err && err.status === 401) {
|
|
171
|
+
await db.deleteGithubToken({ builderId: req.builder.id }).catch(() => {});
|
|
172
|
+
return res.fail('repo_scope_required', { status: 403, message: 'Your GitHub authorization expired. Reconnect to change the repository.' });
|
|
173
|
+
}
|
|
174
|
+
if (err && err.code === 'not_repo_admin') {
|
|
175
|
+
return res.fail('not_repo_admin', { status: 403, message: 'GitHub refused: your account can’t change this repository’s settings.' });
|
|
176
|
+
}
|
|
177
|
+
if (err && err.code === 'repo_not_found') {
|
|
178
|
+
return res.fail('repo_not_found', { status: 404, message: 'GitHub didn’t show that repository to your account.' });
|
|
179
|
+
}
|
|
180
|
+
console.error('[gds] POST /github/repo/visibility', err && err.message);
|
|
181
|
+
res.fail('github_error', { status: 502, message: 'GitHub could not make the change. Try again in a moment.' });
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
100
185
|
return router;
|
|
101
186
|
};
|
package/src/module-api.js
CHANGED
|
@@ -845,8 +845,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
845
845
|
// landed since 1.19.207 with no explicit bump. run 32068332007. (task 1002620)
|
|
846
846
|
// 1.19.209 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
847
847
|
// landed since 1.19.208 with no explicit bump. run 32072227661. (task 1002620)
|
|
848
|
+
// 1.19.210 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
849
|
+
// landed since 1.19.209 with no explicit bump. run 32078135414. (task 1002620)
|
|
850
|
+
// 1.19.211 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
851
|
+
// landed since 1.19.210 with no explicit bump. run 32079512489. (task 1002620)
|
|
848
852
|
// ---------------------------------------------------------------------------
|
|
849
|
-
const CORE_VERSION = '1.19.
|
|
853
|
+
const CORE_VERSION = '1.19.211'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
850
854
|
|
|
851
855
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
852
856
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|