@bongos/core 1.19.400 → 1.19.402
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 +48 -38
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/adr/0200-re-point-a-bring-your-own-domain.md +105 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +3 -2
- package/docs/copy-inventory.md +233 -232
- package/docs/copy-registry.json +293 -284
- package/docs/design/projects-hub-direction-v2.md +38 -0
- package/docs/recipes/ui-look-before-you-ship.md +1 -1
- package/modules/provisioning/migrations/provisioning_012_domain_repoint.sql +35 -0
- package/modules/provisioning/provisioning.js +68 -8
- package/modules/provisioning/routes/provisioning.js +50 -23
- package/modules/ui-design/kit/lib.js +6 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +90 -35
- package/public-landing/index.html +13 -15
- package/public-landing/index.probes.json +8 -5
- package/public-landing/projects.html +49 -20
- package/public-landing/projects.probes.json +17 -10
- package/public-landing/projects.states.json +1 -1
- package/scripts/gds/provision-net.js +33 -0
- package/scripts/gds/provision-units.js +18 -0
- package/scripts/gds/provision.js +97 -5
- package/src/module-api.js +5 -1
- package/tests/landing_page.mjs +8 -4
- package/tests/projects_hub.mjs +28 -1
- package/tests/projects_hub_pre_uat.mjs +28 -2
- package/tests/provision.mjs +185 -12
- package/tests/provisioning_default_domain.mjs +5 -3
- package/tests/provisioning_domain_attach.mjs +166 -17
- package/tests/public_landing_projects.mjs +5 -1
- package/tests/ui_design_kit.mjs +5 -4
|
@@ -191,10 +191,11 @@ main:focus{outline:none;box-shadow:none}
|
|
|
191
191
|
@media (prefers-color-scheme:dark){:root:not([data-mode="light"]) .sun{display:block}
|
|
192
192
|
:root:not([data-mode="light"]) .moon{display:none}}
|
|
193
193
|
|
|
194
|
-
/* the menu bands (.menuBand — .band is this page's content sections): not floating cards. Full width on the card ground, closed by one hairline,
|
|
195
|
-
the page
|
|
196
|
-
|
|
197
|
-
.
|
|
194
|
+
/* the menu bands (.menuBand — .band is this page's content sections): not floating cards. Full width on the card ground, closed by one hairline, a LAYER
|
|
195
|
+
over the page under the bar — the page does not reflow (owner, 2026-08-28; task 1003332). Cells in a
|
|
196
|
+
hairline-separated row, a lowercase display title over one clause; the whole cell is the target.
|
|
197
|
+
One authored move, dead under world.css's kill switch. */
|
|
198
|
+
.menuBand{position:absolute;left:0;right:0;top:var(--top-h);z-index:49;background:var(--bg-card);border-bottom:1px solid var(--rule);
|
|
198
199
|
animation:bandIn .22s cubic-bezier(.16,1,.3,1) both}
|
|
199
200
|
@keyframes bandIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
|
|
200
201
|
.bandIn{padding:20px 26px 28px}
|
|
@@ -207,8 +208,6 @@ main:focus{outline:none;box-shadow:none}
|
|
|
207
208
|
.bandCell:hover .bandT{color:var(--ink)}
|
|
208
209
|
.bandCell:hover{border-top-color:var(--ink-faint)}
|
|
209
210
|
.bandC{display:block;margin-top:5px;font-size:13.5px;line-height:1.45;color:var(--ink-soft)}
|
|
210
|
-
.bandMore{margin-top:18px}
|
|
211
|
-
.bandMore .bandT{font-size:17px;font-weight:400;min-height:24px;display:inline-flex;align-items:center}
|
|
212
211
|
.bandProfile{display:flex;align-items:flex-end;justify-content:space-between;gap:34px;flex-wrap:wrap}
|
|
213
212
|
.bandWho{display:flex;align-items:center;gap:10px;padding-top:15px;border-top:1px solid var(--rule-soft);
|
|
214
213
|
flex:0 1 auto;font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);
|
|
@@ -843,9 +842,10 @@ composition A; landing composition v3, picked by the owner 2026-08-27. No roll,
|
|
|
843
842
|
</header>
|
|
844
843
|
|
|
845
844
|
<!-- the two bands: full-width, on the card ground, closed by one hairline; they unfold under the
|
|
846
|
-
bar's edge
|
|
847
|
-
foot of the page): Escape closes and returns focus
|
|
848
|
-
Tab out closes, a click outside closes, opening one
|
|
845
|
+
bar's edge as a layer over the page, which does not reflow (task 1003332). One disclosure
|
|
846
|
+
component, two instances (the script at the foot of the page): Escape closes and returns focus
|
|
847
|
+
to the trigger, the arrows walk the items, Tab out closes, a click outside closes, opening one
|
|
848
|
+
closes the other. -->
|
|
849
849
|
<div class="menuBand" id="menuProjects" data-menu="projects" hidden>
|
|
850
850
|
<div class="bandIn">
|
|
851
851
|
<ul class="bandRow">
|
|
@@ -854,12 +854,8 @@ composition A; landing composition v3, picked by the owner 2026-08-27. No roll,
|
|
|
854
854
|
<li class="bandCell" id="mineCell" hidden><a class="bandT" href="/projects?view=mine">Your projects</a><span class="bandC">the ones you own or build on</span></li>
|
|
855
855
|
<li class="bandCell"><a class="bandT" href="/projects?view=new">New project</a><span class="bandC">five questions and a home</span></li>
|
|
856
856
|
</ul>
|
|
857
|
-
<!--
|
|
858
|
-
|
|
859
|
-
<ul class="bandRow bandMore">
|
|
860
|
-
<li class="bandCell"><a class="bandT" href="#about">About</a></li>
|
|
861
|
-
<li class="bandCell"><a class="bandT" href="/docs">Docs</a></li>
|
|
862
|
-
</ul>
|
|
857
|
+
<!-- About and Docs are site pages, not project views: they live in the footer row with
|
|
858
|
+
Privacy / Terms / Contact (owner, 2026-08-28; task 1003332). -->
|
|
863
859
|
</div>
|
|
864
860
|
</div>
|
|
865
861
|
<div class="menuBand" id="menuProfile" data-menu="profile" hidden>
|
|
@@ -1096,6 +1092,8 @@ composition A; landing composition v3, picked by the owner 2026-08-27. No roll,
|
|
|
1096
1092
|
<footer>
|
|
1097
1093
|
<div class="footrow">
|
|
1098
1094
|
<p><!--apex-only:start-->A non-profit, capture-resistant commons. <!--apex-only:end-->Built by agents, on Cloud Bongos. <a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">Open source (AGPL-3.0)</a>.</p>
|
|
1095
|
+
<a href="#about">About</a>
|
|
1096
|
+
<a href="/docs">Docs</a>
|
|
1099
1097
|
<a href="/privacy">Privacy</a>
|
|
1100
1098
|
<a href="/terms">Terms</a>
|
|
1101
1099
|
<a href="/contact">Contact</a>
|
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
"page": "/",
|
|
4
4
|
"surface": "public-landing",
|
|
5
5
|
"probes": [
|
|
6
|
-
{ "name": "landing:
|
|
6
|
+
{ "name": "landing closed: the main content starts under the bar", "auth": true, "width": 1440,
|
|
7
|
+
"steps": [],
|
|
8
|
+
"expect": { "expanded": false, "top": [["#main", 72]] } },
|
|
9
|
+
{ "name": "landing: Enter opens Projects on the first item; the band overlays and the hero does not move (task 1003332)", "continue": true,
|
|
7
10
|
"steps": [["focus", "#projTrig"], ["press", "Enter"], ["wait", 150]],
|
|
8
|
-
"expect": { "open": "#menuProjects", "activeText": "Sky" } },
|
|
11
|
+
"expect": { "open": "#menuProjects", "activeText": "Sky", "top": [["#main", 72]] } },
|
|
9
12
|
{ "name": "landing: arrows walk the visible items (Your projects shows signed in)", "continue": true,
|
|
10
13
|
"steps": [["press", "ArrowDown"], ["press", "ArrowDown"]],
|
|
11
14
|
"expect": { "activeText": "Your projects" } },
|
|
@@ -14,16 +17,16 @@
|
|
|
14
17
|
"expect": { "closed": "#menuProjects", "activeId": "projTrig" } },
|
|
15
18
|
{ "name": "landing: the profile band closes the Projects band", "continue": true,
|
|
16
19
|
"steps": [["click", "#projTrig"], ["wait", 120], ["click", "#who .trig"], ["wait", 120]],
|
|
17
|
-
"expect": { "open": "#menuProfile", "closed": "#menuProjects" } },
|
|
20
|
+
"expect": { "open": "#menuProfile", "closed": "#menuProjects", "top": [["#main", 72]] } },
|
|
18
21
|
{ "name": "landing: the profile band names the person; sign out keeps the honest href", "continue": true,
|
|
19
22
|
"steps": [],
|
|
20
23
|
"expect": { "href": [["#menuProfile a.signout", "/api/bongos/auth/logout"]], "text": [["#bandWho", "builder"]], "count": [["#bandWho img", 1]] } },
|
|
21
24
|
{ "name": "landing: a click outside closes", "continue": true,
|
|
22
25
|
"steps": [["clickAt", 700, 700], ["wait", 120]],
|
|
23
26
|
"expect": { "closed": "#menuProfile", "expanded": false } },
|
|
24
|
-
{ "name": "landing: the band deep-links the hub views
|
|
27
|
+
{ "name": "landing: the band deep-links the hub views; About and Docs are footer links (task 1003332)", "continue": true,
|
|
25
28
|
"steps": [["click", "#projTrig"], ["wait", 120]],
|
|
26
|
-
"expect": { "count": [["#menuProjects a",
|
|
29
|
+
"expect": { "count": [["#menuProjects a", 4], ["#menuProjects a[href=\"/docs\"]", 0]], "href": [["#menuProjects .bandCell:nth-child(1) a", "/projects?view=map"], ["#menuProjects .bandCell:nth-child(2) a", "/projects?view=map&sub=table"], ["footer a[href=\"#about\"]", "#about"], ["footer a[href=\"/docs\"]", "/docs"]] } },
|
|
27
30
|
{ "name": "landing: #about exists", "continue": true,
|
|
28
31
|
"steps": [["press", "Escape"]],
|
|
29
32
|
"expect": { "count": [["#about", 1]] } },
|
|
@@ -418,9 +418,10 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
418
418
|
</header>
|
|
419
419
|
|
|
420
420
|
<!-- ══ THE BANDS ══ The menus are not floating cards. Each is a full-width band
|
|
421
|
-
that unfolds under the bar's edge
|
|
422
|
-
|
|
423
|
-
|
|
421
|
+
that unfolds under the bar's edge as a layer OVER the page — the page does
|
|
422
|
+
not reflow (owner, 2026-08-28; task 1003332) — so a destination is a place
|
|
423
|
+
that opens under the bar rather than a list that hovers beside it. One
|
|
424
|
+
component, two instances (see the bands block in the script):
|
|
424
425
|
Escape closes and returns focus to the trigger, the arrows walk the items,
|
|
425
426
|
a click outside closes, opening one closes the other. Plain links in a
|
|
426
427
|
disclosure (aria-expanded + aria-controls, no menu role), so what is
|
|
@@ -435,12 +436,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
435
436
|
<li class="bandCell"><a class="bandT" href="?view=mine" data-nav="mine">Your projects</a><span class="bandC">the ones you own or build on</span></li>
|
|
436
437
|
<li class="bandCell"><a class="bandT" href="?view=new" data-nav="new">New project</a><span class="bandC">five questions and a home</span></li>
|
|
437
438
|
</ul>
|
|
438
|
-
<!--
|
|
439
|
-
|
|
440
|
-
<ul class="bandRow bandMore">
|
|
441
|
-
<li class="bandCell"><a class="bandT" href="/#about">About</a></li>
|
|
442
|
-
<li class="bandCell"><a class="bandT" href="/docs">Docs</a></li>
|
|
443
|
-
</ul>
|
|
439
|
+
<!-- About and Docs are site pages, not project views: they live in the
|
|
440
|
+
footer row with Privacy / Terms / Contact (owner, 2026-08-28; task 1003332). -->
|
|
444
441
|
</div>
|
|
445
442
|
</div>
|
|
446
443
|
<div class="menuBand" id="menuProfile" data-menu="profile" hidden>
|
|
@@ -508,6 +505,15 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
508
505
|
</div>
|
|
509
506
|
|
|
510
507
|
<div class="wrap viewWrap" id="sub-table-body" hidden>
|
|
508
|
+
<!-- ══ THE TABLE IN THE WORLD (task 1003332) ══ The table is a view of the same
|
|
509
|
+
world as the sky: a masthead in the world's material — the view title in
|
|
510
|
+
the display voice and one chrome object — then ledger rows. The owner's
|
|
511
|
+
pick from three rendered treatments (docs/design/projects-hub-direction-v2.md
|
|
512
|
+
§9): "the ledger", with the title cut to two words. -->
|
|
513
|
+
<div class="tblMast">
|
|
514
|
+
<h2 class="tblTitle">every project</h2>
|
|
515
|
+
<span class="mastObj" aria-hidden="true"></span>
|
|
516
|
+
</div>
|
|
511
517
|
<table class="tbl">
|
|
512
518
|
<thead>
|
|
513
519
|
<tr>
|
|
@@ -533,7 +539,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
533
539
|
<td class="right num" id="selfBuilders">—</td>
|
|
534
540
|
<td class="colTags" id="selfTags">—</td>
|
|
535
541
|
<td class="right colStatus"><span class="chip calm"><span class="cdot"></span>—</span></td>
|
|
536
|
-
<td class="colGo"><a class="
|
|
542
|
+
<td class="colGo"><a class="rowGo" href="/">Open →</a></td>
|
|
537
543
|
</tr>
|
|
538
544
|
<!--apex-only:end-->
|
|
539
545
|
</tbody>
|
|
@@ -1070,7 +1076,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1070
1076
|
|
|
1071
1077
|
</main>
|
|
1072
1078
|
|
|
1073
|
-
<footer class="foot"><!--apex-only:start--><span>A non-profit, capture-resistant commons. </span><!--apex-only:end-->Built by agents, on <span data-brand-product-inline>Cloud Bongos</span>. <a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">Open source (AGPL-3.0)</a>.<nav class="footNav" aria-label="Site"><a href="/privacy">Privacy</a><a href="/terms">Terms</a><a href="/contact">Contact</a><!--apex-only:start--><a data-link-hall href="{{buildersOrigin}}">Builders' hall</a><!--apex-only:end--></nav></footer>
|
|
1079
|
+
<footer class="foot"><!--apex-only:start--><span>A non-profit, capture-resistant commons. </span><!--apex-only:end-->Built by agents, on <span data-brand-product-inline>Cloud Bongos</span>. <a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">Open source (AGPL-3.0)</a>.<nav class="footNav" aria-label="Site"><a href="/#about">About</a><a href="/docs">Docs</a><a href="/privacy">Privacy</a><a href="/terms">Terms</a><a href="/contact">Contact</a><!--apex-only:start--><a data-link-hall href="{{buildersOrigin}}">Builders' hall</a><!--apex-only:end--></nav></footer>
|
|
1074
1080
|
|
|
1075
1081
|
<script>
|
|
1076
1082
|
/* Brand-pack fill (R57): the server injects window.__BRANDING__ ahead of this
|
|
@@ -1485,7 +1491,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1485
1491
|
'<td class="right colStatus">' + (p._inst ? chipFor(p._inst)
|
|
1486
1492
|
: '<span class="chip calm"><span class="cdot"></span>—</span>') + '</td>' +
|
|
1487
1493
|
'<td class="colGo">' + (safeHttpUrl(p.origin)
|
|
1488
|
-
? '<a class="
|
|
1494
|
+
? '<a class="rowGo" href="' + esc(safeHttpUrl(p.origin)) + '" target="_blank" rel="noopener">Open →</a>'
|
|
1489
1495
|
: '') + '</td>' +
|
|
1490
1496
|
'</tr>';
|
|
1491
1497
|
}).join(''));
|
|
@@ -4016,8 +4022,12 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4016
4022
|
(error_note, no intent in flight) can re-send its own domain — the route
|
|
4017
4023
|
re-enqueues the attach for exactly this shape */
|
|
4018
4024
|
if (inst.status === 'active' && inst.domain && inst.error_note && !inst.open_intent) return true;
|
|
4019
|
-
|
|
4020
|
-
|
|
4025
|
+
/* every address here is now correctable (task 1003237): none (give it one),
|
|
4026
|
+
the platform-assigned <slug>.<apex> (upgrade away from it), and — since the
|
|
4027
|
+
owner decision of 2026-08-28 — a bring-your-own address that was mistyped.
|
|
4028
|
+
The server's guards (reserved, in-apex squat, already held by another
|
|
4029
|
+
project) are unchanged and stay the authority; this is only the door. */
|
|
4030
|
+
return inst.status === 'requested' || inst.status === 'error' || inst.status === 'active';
|
|
4021
4031
|
}
|
|
4022
4032
|
/* re-enqueue doors: Try again on snagged, Bring it back on torn down —
|
|
4023
4033
|
both re-POST the slug (the server's PROVISION_ENQUEUE_FROM gate) */
|
|
@@ -4231,13 +4241,30 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4231
4241
|
be refused (intent_conflict), so the fold waits for the run to land */
|
|
4232
4242
|
if (inst.open_intent && inst.open_intent.action === 'domain-attach') return '';
|
|
4233
4243
|
var rekick = inst.status === 'active' && inst.domain && inst.error_note;
|
|
4244
|
+
/* the CORRECTION door (task 1003237): the project already answers on an
|
|
4245
|
+
address the founder brought, and it can be changed. The field carries the
|
|
4246
|
+
current one so a typo is fixed by editing it, not retyped from scratch —
|
|
4247
|
+
the re-kick door's shape, for the same reason. The platform-assigned
|
|
4248
|
+
<slug>.<apex> is NOT a correction: moving off it is the upgrade the fold
|
|
4249
|
+
already offered, and the field stays empty for the domain being brought. */
|
|
4250
|
+
var byo = inst.domain && inst.domain !== assignedDefaultFor(inst);
|
|
4251
|
+
var repoint = !rekick && byo;
|
|
4252
|
+
var prefill = rekick || repoint;
|
|
4253
|
+
var label = rekick ? 'Try the address again'
|
|
4254
|
+
: repoint ? 'Use a different domain'
|
|
4255
|
+
: inst.domain ? 'Use a domain I own' : 'Give it an address';
|
|
4234
4256
|
return '<details class="attachFold"' + (rekick ? ' open' : '') + '>' +
|
|
4235
|
-
'<summary>' +
|
|
4257
|
+
'<summary>' + label + '</summary>' +
|
|
4236
4258
|
'<div class="attach">' +
|
|
4237
4259
|
'<input type="text" id="dom-' + iid + '" placeholder="myproject.example.com" autocomplete="off" spellcheck="false" maxlength="253" aria-label="A domain you own, for ' + esc(inst.slug) + '"' +
|
|
4238
|
-
(
|
|
4260
|
+
(prefill ? ' value="' + esc(inst.domain) + '"' : '') + '>' +
|
|
4239
4261
|
'<button type="button" class="btn small" data-attach="' + iid + '">' +
|
|
4240
|
-
(rekick ? 'Try the address again' : 'Use this domain') + '</button></div
|
|
4262
|
+
(rekick ? 'Try the address again' : repoint ? 'Change the address' : 'Use this domain') + '</button></div>' +
|
|
4263
|
+
/* true in every state the fold renders in: a REQUESTED project never
|
|
4264
|
+
answered anywhere yet, so "stops answering" would be a claim about a
|
|
4265
|
+
thing that never happened (the F16 rule) */
|
|
4266
|
+
(repoint ? '<p class="attachNote">Changing it lets <code>' + esc(inst.domain) + '</code> go — this project won’t use that address any more.</p>' : '') +
|
|
4267
|
+
'</details>';
|
|
4241
4268
|
}
|
|
4242
4269
|
|
|
4243
4270
|
function renderCard(inst) {
|
|
@@ -5421,8 +5448,6 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5421
5448
|
msg(id, 'That doesn’t look like a usable address — ' + esc(e.details.reason || 'use a real hostname like myproject.example.com') + '.', 'err');
|
|
5422
5449
|
} else if (e.code === 'domain_taken') {
|
|
5423
5450
|
msg(id, esc(e.message || 'That address is already in use by another project.'), 'err');
|
|
5424
|
-
} else if (e.code === 'domain_already_set') {
|
|
5425
|
-
msg(id, esc(e.message || 'This project already has an address — re-pointing isn’t supported here.'), 'err');
|
|
5426
5451
|
} else if (e.code === 'domain_not_attachable') {
|
|
5427
5452
|
msg(id, esc(e.message || 'An address can be attached once the instance settles.'), 'err');
|
|
5428
5453
|
} else if (e.code === 'intent_conflict') {
|
|
@@ -5543,10 +5568,14 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5543
5568
|
/* Boot. The view is set FIRST, from the URL, so the page is navigable before
|
|
5544
5569
|
any request resolves — /projects?view=new must land on the create view even
|
|
5545
5570
|
if the identity call is slow or fails. */
|
|
5571
|
+
/* the boot query is read ONCE, before the view is set: setView rewrites the
|
|
5572
|
+
URL to ?view=<v> and would drop everything else (the Table deep link from
|
|
5573
|
+
the landing landed on the sky until task 1003332) */
|
|
5574
|
+
var bootQ = new URLSearchParams(location.search);
|
|
5546
5575
|
setView(viewFromUrl());
|
|
5547
5576
|
/* the Table item's deep link (and the landing's) names the sub-view; a view change
|
|
5548
5577
|
never writes it back, so a reload from the band returns to the sky as before */
|
|
5549
|
-
if (
|
|
5578
|
+
if (bootQ.get('sub') === 'table') setSub('table');
|
|
5550
5579
|
|
|
5551
5580
|
fetch(API + '/me', { credentials: 'same-origin' })
|
|
5552
5581
|
.then(function (res) { if (!res.ok) throw new Error(String(res.status)); return res.json(); })
|
|
@@ -3,26 +3,29 @@
|
|
|
3
3
|
"page": "/projects",
|
|
4
4
|
"surface": "public-landing",
|
|
5
5
|
"probes": [
|
|
6
|
-
{ "name": "
|
|
6
|
+
{ "name": "closed: the main content starts under the bar", "auth": true, "width": 1440,
|
|
7
|
+
"steps": [],
|
|
8
|
+
"expect": { "expanded": false, "top": [["#hubMain", 64]] } },
|
|
9
|
+
{ "name": "Enter opens Projects and lands on the first item; the band overlays and the content does not move (task 1003332)", "continue": true,
|
|
7
10
|
"steps": [["focus", "#projTrig"], ["press", "Enter"], ["wait", 150]],
|
|
8
|
-
"expect": { "open": "#menuProjects", "activeText": "Sky" } },
|
|
11
|
+
"expect": { "open": "#menuProjects", "activeText": "Sky", "top": [["#hubMain", 64]] } },
|
|
9
12
|
{ "name": "ArrowDown walks to Table", "continue": true,
|
|
10
13
|
"steps": [["press", "ArrowDown"]],
|
|
11
14
|
"expect": { "activeText": "Table" } },
|
|
12
|
-
{ "name": "ArrowUp wraps to the last item (
|
|
15
|
+
{ "name": "ArrowUp wraps to the last item (New project; About and Docs are footer links, task 1003332)", "continue": true,
|
|
13
16
|
"steps": [["press", "ArrowUp"], ["press", "ArrowUp"]],
|
|
14
|
-
"expect": { "activeText": "
|
|
17
|
+
"expect": { "activeText": "New project" } },
|
|
15
18
|
{ "name": "Escape closes and returns focus to the trigger", "continue": true,
|
|
16
19
|
"steps": [["press", "Escape"], ["wait", 100]],
|
|
17
20
|
"expect": { "closed": "#menuProjects", "activeId": "projTrig", "expanded": false } },
|
|
18
21
|
{ "name": "opening the profile band closes the Projects band", "continue": true,
|
|
19
22
|
"steps": [["click", "#projTrig"], ["wait", 120], ["click", "#who"], ["wait", 120]],
|
|
20
|
-
"expect": { "open": "#menuProfile", "closed": "#menuProjects", "expandedIds": ["who"] } },
|
|
23
|
+
"expect": { "open": "#menuProfile", "closed": "#menuProjects", "expandedIds": ["who"], "top": [["#hubMain", 64]] } },
|
|
21
24
|
{ "name": "a click outside closes", "continue": true,
|
|
22
25
|
"steps": [["clickAt", 700, 600], ["wait", 120]],
|
|
23
26
|
"expect": { "closed": "#menuProfile", "expanded": false } },
|
|
24
27
|
{ "name": "Tab out of the band closes it", "continue": true,
|
|
25
|
-
"steps": [["click", "#projTrig"], ["wait", 120], ["focus", "#menuProjects a[
|
|
28
|
+
"steps": [["click", "#projTrig"], ["wait", 120], ["focus", "#menuProjects a[data-nav=\"new\"]"], ["press", "Tab"], ["wait", 120]],
|
|
26
29
|
"expect": { "closed": "#menuProjects" } },
|
|
27
30
|
{ "name": "Table from the band: the table on stage, Table current, band closed", "continue": true,
|
|
28
31
|
"steps": [["click", "#projTrig"], ["wait", 120], ["click", "a[data-sub=\"table\"]"], ["wait", 300]],
|
|
@@ -54,12 +57,16 @@
|
|
|
54
57
|
{ "name": "the mode toggle: the label says what the press does, and the press does it", "continue": true,
|
|
55
58
|
"steps": [["clickAt", 700, 600], ["click", "#modeToggle"], ["wait", 100]],
|
|
56
59
|
"expect": { "attr": [["#modeToggle", "aria-label", "Switch to light mode"], ["html", "data-mode", "dark"]] } },
|
|
60
|
+
{ "name": "the Table deep link (the landing's band item) lands on the table, Table current (task 1003332)", "auth": true, "width": 1440, "url": "/projects?view=map&sub=table",
|
|
61
|
+
"steps": [["wait", 300]],
|
|
62
|
+
"expect": { "visible": ["#sub-table-body", ".tblMast"], "hidden": ["#sub-sky-body"], "text": [["[aria-current=\"page\"]", "Table"], [".tblTitle", "every project"]] } },
|
|
57
63
|
{ "name": "signed out: no chip, the door shows, no switch, the search stays", "auth": false, "width": 1440,
|
|
58
64
|
"steps": [],
|
|
59
65
|
"expect": { "hidden": ["#who", ".switch"], "visible": ["#signinTop", "#qWrap"] } },
|
|
60
|
-
{ "name": "signed out the Projects band is Sky / Table / New project
|
|
66
|
+
{ "name": "signed out the Projects band is Sky / Table / New project (no Your projects); About and Docs are in the footer row", "continue": true,
|
|
61
67
|
"steps": [["click", "#projTrig"], ["wait", 120]],
|
|
62
|
-
"expect": { "count": [["#menuProjects .bandCell:not([hidden])",
|
|
68
|
+
"expect": { "count": [["#menuProjects .bandCell:not([hidden])", 3], ["#menuProjects a[href=\"/docs\"]", 0]], "hidden": ["#menuProjects [data-nav=\"mine\"]"],
|
|
69
|
+
"href": [[".footNav a[href=\"/#about\"]", "/#about"], [".footNav a[href=\"/docs\"]", "/docs"]] } },
|
|
63
70
|
{ "name": "signed out: the card still closes on Escape (nothing throws with no session)", "continue": true,
|
|
64
71
|
"steps": [["press", "Escape"], ["wait", 100], ["dispatchClick", "#orbField a.worldOrb:not(#selfOrb)"], ["wait", 400], ["press", "Escape"], ["wait", 150]],
|
|
65
72
|
"expect": { "dialog": false, "closed": "#menuProjects" } },
|
|
@@ -69,9 +76,9 @@
|
|
|
69
76
|
{ "name": "390: Escape folds the row and returns focus to the magnifier", "continue": true,
|
|
70
77
|
"steps": [["press", "Escape"], ["wait", 100]],
|
|
71
78
|
"expect": { "activeId": "qToggle", "hasClass": [["#qWrap", "is-open", false]] } },
|
|
72
|
-
{ "name": "390: an opening band folds the search", "continue": true,
|
|
79
|
+
{ "name": "390: an opening band folds the search and overlays the sky (the content does not move)", "continue": true,
|
|
73
80
|
"steps": [["click", "#qToggle"], ["wait", 100], ["click", "#projTrig"], ["wait", 150]],
|
|
74
|
-
"expect": { "hasClass": [["#qWrap", "is-open", false]], "open": "#menuProjects" } },
|
|
81
|
+
"expect": { "hasClass": [["#qWrap", "is-open", false]], "open": "#menuProjects", "top": [["#hubMain", 60]] } },
|
|
75
82
|
{ "name": "390: a solo Shift tap opens the fold and focuses the field, without overflow", "continue": true,
|
|
76
83
|
"steps": [["press", "Escape"], ["wait", 100], ["press", "Shift"], ["wait", 150]],
|
|
77
84
|
"expect": { "hasClass": [["#qWrap", "is-open", true]], "activeId": "mapQ", "noOverflow": true } }
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"in-map-kbd": { "auth": true, "actions": [["focus", "#projTrig"], ["press", "Enter"], ["wait", 350], ["press", "ArrowDown"], ["wait", 120]], "expect": { "expanded": true, "activeText": "Table" } },
|
|
15
15
|
"in-map-search": { "auth": true, "actions": [["clickIfVisible", "#qToggle"], ["wait", 200], ["fill", "#mapQ", "emer"], ["wait", 300]], "expect": { "value": [["#mapQ", "emer"]] } },
|
|
16
16
|
"in-map-mine-on": { "auth": true, "actions": [["check", "#onlyMine"], ["wait", 300]], "expect": { "checked": "#onlyMine" } },
|
|
17
|
-
"in-table": { "auth": true, "actions": [["click", "#projTrig"], ["wait", 250], ["click", "a[data-sub=\"table\"]"], ["wait", 400]], "expect": { "visible": ["#sub-table-body"], "closed": "#menuProjects" } },
|
|
17
|
+
"in-table": { "auth": true, "actions": [["click", "#projTrig"], ["wait", 250], ["click", "a[data-sub=\"table\"]"], ["wait", 400]], "expect": { "visible": ["#sub-table-body", ".tblMast", ".mastObj"], "closed": "#menuProjects" } },
|
|
18
18
|
"in-mine": { "auth": true, "url": "/projects?view=mine", "expect": { "hidden": ["#qWrap"] } },
|
|
19
19
|
"in-new": { "auth": true, "url": "/projects?view=new", "expect": { "hidden": ["#qWrap"] } },
|
|
20
20
|
"out-new": { "auth": false, "url": "/projects?view=new", "expect": { "hidden": ["#qWrap"] } },
|
|
@@ -118,6 +118,37 @@ function dnsUpsertEnv(inst, { publicIp, zone, proxied = false } = {}) {
|
|
|
118
118
|
};
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
// The RFC 5737 TEST-NET-1 sentinel a released hostname is parked at (task
|
|
122
|
+
// 1003237) — the shape scripts/gds/box-infra.js proved for a torn-down box
|
|
123
|
+
// (idea 309 / task 1234). 192.0.2.1 is reserved for documentation and is
|
|
124
|
+
// GUARANTEED never routable, so a name parked here can never dial platform
|
|
125
|
+
// infra or a recycled address.
|
|
126
|
+
const DNS_BLACKHOLE_IP = '192.0.2.1';
|
|
127
|
+
|
|
128
|
+
// The env a RELEASE invocation of the DNS hook needs: the abandoned hostname,
|
|
129
|
+
// parked at the blackhole sentinel. A re-point (task 1003237) leaves the old
|
|
130
|
+
// address UPSERTed at the platform's IP for a project that no longer answers
|
|
131
|
+
// there — the ADR 0181 §2 leak — and this is how the runner lets it go.
|
|
132
|
+
//
|
|
133
|
+
// REPOINT, not delete, for the reason box-infra.js records: deleting a name
|
|
134
|
+
// opens an NXDOMAIN window that resolvers negative-cache for the zone's SOA
|
|
135
|
+
// neg-TTL (~30 min, not editable), so a client that queried mid-gap stays
|
|
136
|
+
// broken long after. A name that always resolves — to a guaranteed-dead host —
|
|
137
|
+
// has nothing to negative-cache. It also needs only the UPSERT contract every
|
|
138
|
+
// BOX_DNS_HOOK implements, so no hook grows a new mode for this.
|
|
139
|
+
//
|
|
140
|
+
// proxied=0: a parked record must never ride the Cloudflare edge, which would
|
|
141
|
+
// answer for it. Takes a bare `hostname` rather than an instance — the whole
|
|
142
|
+
// point is that the row has already moved on. PURE.
|
|
143
|
+
function dnsReleaseEnv(hostname, { zone } = {}) {
|
|
144
|
+
return {
|
|
145
|
+
BOX_HOSTNAME: hostname,
|
|
146
|
+
BOX_IP: DNS_BLACKHOLE_IP,
|
|
147
|
+
CLOUDFLARE_ZONE: zone || zoneForDomain(hostname),
|
|
148
|
+
BOX_PROXIED: '0',
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
121
152
|
// ── DNS token-scope PREFLIGHT (task 1980) ────────────────────────────────────
|
|
122
153
|
// Onboarding a NEW project on a domain the control-plane's Cloudflare token does NOT
|
|
123
154
|
// cover fails LATE: the DNS UPSERT (step 5) errors mid-provision (the Emersonian
|
|
@@ -208,10 +239,12 @@ function healthzCmd(inst) {
|
|
|
208
239
|
|
|
209
240
|
module.exports = {
|
|
210
241
|
CF_API_BASE,
|
|
242
|
+
DNS_BLACKHOLE_IP,
|
|
211
243
|
caddyBlock,
|
|
212
244
|
caddySnippetPath,
|
|
213
245
|
checkDnsTokenScope,
|
|
214
246
|
classifyZoneScope,
|
|
247
|
+
dnsReleaseEnv,
|
|
215
248
|
dnsUpsertEnv,
|
|
216
249
|
federateInstance,
|
|
217
250
|
federationHubOrigin,
|
|
@@ -240,6 +240,23 @@ function webEnvPath(inst, { etcBase = CONFIG.etcBase } = {}) {
|
|
|
240
240
|
// The settings env vars the runner pushes to a RUNNING instance (task 1003140):
|
|
241
241
|
// the module's settingsEnvVars under the prefix the instance actually resolves —
|
|
242
242
|
// the same pair webEnvBody writes at standup, so standup and push cannot drift.
|
|
243
|
+
// The four origin overrides web.env carries for an addressed instance — the
|
|
244
|
+
// domain-dependent half of webEnvBody, on its own so domain-attach can patch
|
|
245
|
+
// exactly those lines IN PLACE (task 1003237). A single-host instance has no
|
|
246
|
+
// dedicated builders./status. subdomain, so all four point at the one host.
|
|
247
|
+
// Empty for a domainless instance: there is no origin to assert, and writing
|
|
248
|
+
// `=` over a real one would take the hall down. PURE.
|
|
249
|
+
function originEnvVarsFor(inst) {
|
|
250
|
+
if (!inst || !inst.domain) return {};
|
|
251
|
+
const prefix = provisionedEnvPrefix(), origin = `https://${inst.domain}`;
|
|
252
|
+
return {
|
|
253
|
+
[`${prefix}_PUBLIC_ORIGIN`]: origin,
|
|
254
|
+
[`${prefix}_BUILDERS_ORIGIN`]: origin,
|
|
255
|
+
[`${prefix}_STATUS_ORIGIN`]: origin,
|
|
256
|
+
[`${prefix}_OAUTH_ORIGIN`]: origin,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
243
260
|
function settingsEnvVarsFor(inst) {
|
|
244
261
|
const { settingsEnvVars } = require(path.join(REPO_ROOT, 'modules/provisioning/provisioning')); // eslint-disable-line global-require
|
|
245
262
|
return settingsEnvVars(inst, provisionedEnvPrefix());
|
|
@@ -309,6 +326,7 @@ module.exports = {
|
|
|
309
326
|
backupTimerPath,
|
|
310
327
|
backupUnitName,
|
|
311
328
|
instanceManifestCmd,
|
|
329
|
+
originEnvVarsFor,
|
|
312
330
|
provisionedEnvPrefix,
|
|
313
331
|
serviceUnit,
|
|
314
332
|
serviceUnitPath,
|
package/scripts/gds/provision.js
CHANGED
|
@@ -51,8 +51,8 @@ const crypto = require('node:crypto');
|
|
|
51
51
|
|
|
52
52
|
const { CONFIG, MANIFEST_UA, MANIFEST_VERIFY_INTERVAL_MS, MANIFEST_VERIFY_TRIES, MAX_INTENT_ATTEMPTS, REPO_ROOT, coreVersionSafe, hasFlag, loadDeps, oauthSecret, provisionerBotEmail } = require('./provision-config.js');
|
|
53
53
|
const { APP_USER, alreadyScaffolded, buildCorePinRefreshCommit, dbCreateCmd, dbName, deployKeyPath, deployKeyTitle, ensurePrivateRepoAccess, installedCorePackDir, installedCoreTarball, instanceInitSpec, instanceRepoRemote, migrateCmd, onboardMode, ownerLoginOf, parseTargetRef, planCorePinRefresh, refreshStandaloneCorePin, resolveOwnerGithubToken, resolveVendorableCoreTarball, safeVersionLabel, scaffoldStandaloneRepo, seedFirstVersionCmd, standaloneInstallCmd, standaloneMigrateCmd, standalonePullCmd, standaloneRegenDocsCmd, standaloneRoot } = require('./provision-repo.js');
|
|
54
|
-
const { backupScriptPath, backupService, backupServicePath, backupTimer, backupTimerPath, backupUnitName, instanceManifestCmd, serviceUnit, serviceUnitPath, settingsConsumed, settingsEnvVarsFor, upsertEnvVars, webEnvBody, webEnvPath } = require('./provision-units.js');
|
|
55
|
-
const { caddyBlock, caddySnippetPath, checkDnsTokenScope, classifyZoneScope, dnsUpsertEnv, federateInstance, federationHubOrigin, healthzCmd, shouldFederate, zoneForDomain } = require('./provision-net.js');
|
|
54
|
+
const { backupScriptPath, backupService, backupServicePath, backupTimer, backupTimerPath, backupUnitName, instanceManifestCmd, originEnvVarsFor, serviceUnit, serviceUnitPath, settingsConsumed, settingsEnvVarsFor, upsertEnvVars, webEnvBody, webEnvPath } = require('./provision-units.js');
|
|
55
|
+
const { caddyBlock, caddySnippetPath, checkDnsTokenScope, classifyZoneScope, dnsReleaseEnv, dnsUpsertEnv, federateInstance, federationHubOrigin, healthzCmd, shouldFederate, zoneForDomain } = require('./provision-net.js');
|
|
56
56
|
|
|
57
57
|
function makeExec({ apply, log = console.log, remoteHost = null, remoteCwd = null, sshOpts = '' }) {
|
|
58
58
|
// `shown` (task 1003140): what to LOG in place of the command text — for a command
|
|
@@ -464,6 +464,12 @@ async function provisionInstance(inst, deps) {
|
|
|
464
464
|
}
|
|
465
465
|
}
|
|
466
466
|
|
|
467
|
+
// A re-point on a PRE-ACTIVE instance records its release here and never
|
|
468
|
+
// reaches the attach leg (that one no-ops for anything but 'active'), so this
|
|
469
|
+
// run owns the drain — and an `error` row is frequently a partial standup that
|
|
470
|
+
// already UPSERTed the address it is now leaving (ADR 0181 §1, task 1003237).
|
|
471
|
+
await releaseAbandonedDomains(inst, { controlExec, log, apply, db, provisioning });
|
|
472
|
+
|
|
467
473
|
// ── step 6: Caddy per-instance site snippet + reload ──────────────────────
|
|
468
474
|
// Write the instance's OWN <slug>.caddy snippet (idempotent overwrite via the
|
|
469
475
|
// injected writeFile — no read-modify-append to a shared Caddyfile), then reload.
|
|
@@ -557,6 +563,57 @@ runcmd:
|
|
|
557
563
|
`;
|
|
558
564
|
}
|
|
559
565
|
|
|
566
|
+
// The addresses a re-point left behind and this run still owes a release on
|
|
567
|
+
// (task 1003237). The one guard this tier can make on its own, and the one that
|
|
568
|
+
// matters: the address the instance is CURRENTLY on is never released, so a row
|
|
569
|
+
// that somehow queued its own domain can never blackhole a live project.
|
|
570
|
+
// Keeping a platform-assigned <slug>.<apex> off the queue is the ENQUEUE site's
|
|
571
|
+
// job — the web tier knows the apex, this runner does not (ADR 0181 §2). PURE.
|
|
572
|
+
function releasableDomains(inst) {
|
|
573
|
+
const here = String((inst && inst.domain) || '').toLowerCase();
|
|
574
|
+
return (Array.isArray(inst && inst.released_domains) ? inst.released_domains : [])
|
|
575
|
+
.map((d) => String(d || '').toLowerCase())
|
|
576
|
+
.filter((d) => d && d !== here);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Park every address a re-point moved off at the blackhole sentinel, then empty
|
|
580
|
+
// the queue (task 1003237, ADR 0200). Called by BOTH legs that wire a domain —
|
|
581
|
+
// domain-attach for an ACTIVE instance, and the provision run that owns
|
|
582
|
+
// PRE-ACTIVE ones, since an `error` row is frequently a partial standup that
|
|
583
|
+
// already UPSERTed the address it is now leaving (ADR 0181 §1).
|
|
584
|
+
//
|
|
585
|
+
// BEST-EFFORT in the teardown sense: a hook that fails leaves the queue intact
|
|
586
|
+
// so the next run retries, and never fails the wiring that already landed. EVERY
|
|
587
|
+
// queued address is attempted — one host whose zone the token cannot reach must
|
|
588
|
+
// not strand the others behind it — and the queue is emptied only when all of
|
|
589
|
+
// them landed; a re-park of an address already at the sentinel is an idempotent
|
|
590
|
+
// UPSERT, so retrying the whole list costs nothing but the call. The list is
|
|
591
|
+
// capped at the enqueue site (MAX_RELEASED_DOMAINS), which is what bounds this
|
|
592
|
+
// loop. Returns the first host that failed, or null. Runs on the CONTROL PLANE —
|
|
593
|
+
// the hook needs the Cloudflare token, which never leaves it (ADR 0111 §2).
|
|
594
|
+
async function releaseAbandonedDomains(inst, { controlExec, log, apply, db, provisioning }) {
|
|
595
|
+
const hosts = releasableDomains(inst);
|
|
596
|
+
if (!hosts.length) return null;
|
|
597
|
+
if (!CONFIG.dnsHook) {
|
|
598
|
+
log(` [dns] cannot release ${hosts.join(', ')} (BOX_DNS_HOOK unset) — remove their A records manually`);
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
let firstFailed = null;
|
|
602
|
+
for (const host of hosts) {
|
|
603
|
+
const relEnv = dnsReleaseEnv(host);
|
|
604
|
+
log(` [dns] release ${host} -> ${relEnv.BOX_IP} (blackhole, zone ${relEnv.CLOUDFLARE_ZONE}) via ${CONFIG.dnsHook}`);
|
|
605
|
+
if (!apply) continue;
|
|
606
|
+
try { controlExec(CONFIG.dnsHook, { env: relEnv }); }
|
|
607
|
+
catch (e) {
|
|
608
|
+
log(` ⚠ could not release ${host}: ${(e && e.message) || e} — its A record still points here`);
|
|
609
|
+
if (!firstFailed) firstFailed = host;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
if (firstFailed) return firstFailed;
|
|
613
|
+
if (apply && provisioning.clearReleasedDomains) await provisioning.clearReleasedDomains(db, inst.id).catch(() => {});
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
|
|
560
617
|
// ---------------------------------------------------------------------------
|
|
561
618
|
// Attach a late-arriving domain to an ALREADY-ACTIVE instance (task 1002692):
|
|
562
619
|
// re-assert exactly the domain-dependent runbook legs — web.env origins (+ a
|
|
@@ -565,6 +622,18 @@ runcmd:
|
|
|
565
622
|
// RETURN { ok:false } instead of throwing: the drain loop's catch flips the
|
|
566
623
|
// instance to 'error', which must never happen to a healthy ACTIVE instance
|
|
567
624
|
// over a domain hiccup.
|
|
625
|
+
//
|
|
626
|
+
// This leg ALSO carries a RE-POINT (task 1003237, ADR 0200) — the same intent,
|
|
627
|
+
// because the wiring is the same wiring. Two things follow from that:
|
|
628
|
+
// • The origin lines are patched IN PLACE (upsertEnvVars), never regenerated
|
|
629
|
+
// from the row. A template re-write via webEnvBody would blank the OAuth
|
|
630
|
+
// creds placeManifestCreds placed, the App private key and the federation
|
|
631
|
+
// block — none of which the row knows (ADR 0190 §2). A first attach was
|
|
632
|
+
// MASKED by wizard order (address first, GitHub App second); a re-point runs
|
|
633
|
+
// on a project the founder is already signed in to, so the same move would
|
|
634
|
+
// break sign-in on the exact project they just corrected (idea 1000842).
|
|
635
|
+
// • Every address on the row's release queue is parked at the blackhole
|
|
636
|
+
// sentinel AFTER the new one is wired — best-effort, never fatal.
|
|
568
637
|
// ---------------------------------------------------------------------------
|
|
569
638
|
async function domainAttachInstance(inst, deps) {
|
|
570
639
|
const { db, provisioning, exec, writeFile, log, apply, privileged } = deps;
|
|
@@ -603,8 +672,22 @@ async function domainAttachInstance(inst, deps) {
|
|
|
603
672
|
? (CONFIG.controlPlaneIp || (CONFIG.remoteHost ? null : CONFIG.publicIp))
|
|
604
673
|
: CONFIG.publicIp;
|
|
605
674
|
|
|
606
|
-
|
|
607
|
-
|
|
675
|
+
// Patch ONLY the origin lines (the settingsApplyInstance shape, ADR 0190 §2).
|
|
676
|
+
// Dry-run has no file to read, so it PLANS the patch rather than writing a
|
|
677
|
+
// synthetic body. An unreadable or empty file is refused, never "patched":
|
|
678
|
+
// writing four origins over a missing PORT/PGDATABASE would take the instance
|
|
679
|
+
// down — and this leg runs against instances that are ALREADY serving.
|
|
680
|
+
const origins = originEnvVarsFor(inst);
|
|
681
|
+
const read = boxExec(`${sudoP}cat ${webEnvPath(inst)}`, { allowFail: true });
|
|
682
|
+
const currentEnv = String((read && read.stdout) || '');
|
|
683
|
+
if (!apply) {
|
|
684
|
+
log(` [config] would patch ${Object.keys(origins).join(', ')} in ${webEnvPath(inst)} in place (creds + federation untouched)`);
|
|
685
|
+
} else if (!read || read.softFailed || !currentEnv.trim()) {
|
|
686
|
+
return { ok: false, error: `cannot read ${webEnvPath(inst)} — nothing to patch (was this instance provisioned by the runner?)` };
|
|
687
|
+
} else {
|
|
688
|
+
log(' [config] patch the origin lines of web.env in place (creds + federation untouched)');
|
|
689
|
+
boxWriteFile(webEnvPath(inst), upsertEnvVars(currentEnv, origins), { mode: 0o600, sudo: true });
|
|
690
|
+
}
|
|
608
691
|
log(' [service] restart to pick up the new origins');
|
|
609
692
|
boxExec(`${sudoP}systemctl restart ${inst.slug}`);
|
|
610
693
|
|
|
@@ -629,10 +712,19 @@ async function domainAttachInstance(inst, deps) {
|
|
|
629
712
|
log(` [dns] skipped (BOX_DNS_HOOK unset) — point ${inst.domain}'s A record at ${recordIp || 'this server'} manually`);
|
|
630
713
|
}
|
|
631
714
|
|
|
715
|
+
// The snippet is per-SLUG and always carries the CURRENT domain, so a re-point
|
|
716
|
+
// takes the old vhost away in the same write — Caddy stops serving the old host
|
|
717
|
+
// (and tls-check stops vouching for it) with no separate un-wire step.
|
|
632
718
|
log(' [caddy] write site snippet + reload');
|
|
633
719
|
boxWriteFile(caddySnippetPath(inst), caddyBlock(inst), { mode: 0o644, sudo: true });
|
|
634
720
|
boxExec(`${sudoP}systemctl reload caddy`, { allowFail: true });
|
|
635
721
|
|
|
722
|
+
// Release what the re-point moved off, AFTER the new address is wired: the DNS
|
|
723
|
+
// record is the only half of an abandoned address the platform still holds, and
|
|
724
|
+
// it points at platform infra.
|
|
725
|
+
const releaseFailed = await releaseAbandonedDomains(inst, { controlExec, log, apply, db, provisioning });
|
|
726
|
+
if (releaseFailed) return { ok: true, releaseFailed };
|
|
727
|
+
|
|
636
728
|
log(' [healthz] verify the instance answers');
|
|
637
729
|
boxExec(healthzCmd(inst), { allowFail: true });
|
|
638
730
|
|
|
@@ -1337,7 +1429,7 @@ module.exports = {
|
|
|
1337
1429
|
caddyBlock, caddySnippetPath, healthzCmd, dedicatedUserData, nextFreePort,
|
|
1338
1430
|
// executors + orchestration (injectable deps for tests)
|
|
1339
1431
|
makeExec, makeWriteFile, allocatePort, boundLocalPorts, provisionInstance, teardownInstance,
|
|
1340
|
-
domainAttachInstance, cmdRunIntents, cmdReconcile, cmdStatus,
|
|
1432
|
+
domainAttachInstance, releasableDomains, cmdRunIntents, cmdReconcile, cmdStatus,
|
|
1341
1433
|
// live settings push (task 1003140) + the in-place env patch + read-back it rides on
|
|
1342
1434
|
settingsApplyInstance, upsertEnvVars, settingsEnvVarsFor, settingsConsumed, instanceManifestCmd,
|
|
1343
1435
|
// restart an unreachable ACTIVE instance (task 1003238) — the settings leg minus the env patch
|
package/src/module-api.js
CHANGED
|
@@ -1240,8 +1240,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
1240
1240
|
// landed since 1.19.398 with no explicit bump. run 33201243958. (task 1002620)
|
|
1241
1241
|
// 1.19.400 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1242
1242
|
// landed since 1.19.399 with no explicit bump. run 33201979306. (task 1002620)
|
|
1243
|
+
// 1.19.401 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1244
|
+
// landed since 1.19.400 with no explicit bump. run 33205015717. (task 1002620)
|
|
1245
|
+
// 1.19.402 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1246
|
+
// landed since 1.19.401 with no explicit bump. run 33206226800. (task 1002620)
|
|
1243
1247
|
// ---------------------------------------------------------------------------
|
|
1244
|
-
const CORE_VERSION = '1.19.
|
|
1248
|
+
const CORE_VERSION = '1.19.402'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
1245
1249
|
|
|
1246
1250
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
1247
1251
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|