@bongos/core 1.21.60 → 1.21.61
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 +38 -28
- 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 +58 -3
- package/docs/api-reference.md +7 -1
- package/docs/copy-inventory.md +10 -3
- package/docs/copy-registry.json +67 -4
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-readings.json +222 -215
- package/modules/hall-ui/public/project-settings.css +5 -0
- package/modules/hall-ui/public/project-settings.html +12 -0
- package/modules/hall-ui/public/project-settings.js +20 -0
- package/modules/hall-ui/records/project-danger-zone.md +10 -0
- package/modules/public-landing/public/projects.html +30 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/bongos/routes/core-update.js +33 -0
- package/src/module-api.js +1 -1
- package/tests/project_manage_link.mjs +111 -0
|
@@ -214,3 +214,8 @@
|
|
|
214
214
|
the 24px floor: the prose-link recipe buys the height with padding and hands it back
|
|
215
215
|
with an equal negative margin, so the line does not re-space. */
|
|
216
216
|
.dp-settled .ov-row__sub a { display: inline-block; min-height: 24px; padding: 3px 0; margin: -3px 0; }
|
|
217
|
+
|
|
218
|
+
/* The danger zone (task 1004570) — two links out to the hosting platform, each with
|
|
219
|
+
one sentence saying what it does. Plain list, the lede's measure. */
|
|
220
|
+
.ps-danger__list { margin: 10px 0 0; padding-left: 18px; line-height: 1.6; max-width: var(--measure); }
|
|
221
|
+
.ps-danger__list li + li { margin-top: 8px; }
|
|
@@ -49,6 +49,18 @@
|
|
|
49
49
|
thing this page exists to end. -->
|
|
50
50
|
<div id="ps-rows"><span class="kit-skel"></span></div>
|
|
51
51
|
</section>
|
|
52
|
+
<!-- Danger zone (task 1004570): where this project's OWNER takes it offline or
|
|
53
|
+
disconnects it. Both controls live on the platform that hosts the project,
|
|
54
|
+
so this only links there. Hidden unless GET /project/manage-link answers
|
|
55
|
+
with a link — an Archon on a hosted project; never on a self-hosted one. -->
|
|
56
|
+
<section class="scroll" id="ps-danger" aria-labelledby="ps-danger-h" hidden>
|
|
57
|
+
<h2 class="scroll__h" id="ps-danger-h">Danger zone</h2>
|
|
58
|
+
<p class="scroll__lede"><span id="ps-danger-lede"></span> <a id="ps-danger-manage" href="#"></a></p>
|
|
59
|
+
<ul class="ps-danger__list">
|
|
60
|
+
<li><a id="ps-danger-offline" href="#">Take it offline</a> — stops the project and its nightly backups but keeps everything: its database, its backups and your repository on GitHub. You can bring it back.</li>
|
|
61
|
+
<li><a id="ps-danger-disconnect" href="#">Disconnect from Bongos entirely</a> — erases the project’s server, its database and every backup, and releases its name and web address. Your repository on GitHub is kept. This cannot be undone.</li>
|
|
62
|
+
</ul>
|
|
63
|
+
</section>
|
|
52
64
|
</main>
|
|
53
65
|
|
|
54
66
|
<footer class="app-foot" id="app-foot"></footer>
|
|
@@ -245,5 +245,25 @@
|
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
+
// The danger zone (task 1004570). The server answers only an Archon — on a hosted
|
|
249
|
+
// project, its owner — and answers { link: null } where there is no hosting platform
|
|
250
|
+
// to link to, so every other outcome (refused, failed, self-hosted) leaves the section
|
|
251
|
+
// hidden: a missing link, never a dead one. The act itself stays on the platform.
|
|
252
|
+
async function loadDanger() {
|
|
253
|
+
let link = null;
|
|
254
|
+
try {
|
|
255
|
+
const res = await api.request('GET', `${API}/project/manage-link`);
|
|
256
|
+
link = res.ok && res.data ? res.data.link : null;
|
|
257
|
+
} catch (_) { link = null; }
|
|
258
|
+
if (!link || !link.offline_url || !link.disconnect_url) return;
|
|
259
|
+
$('ps-danger-lede').textContent = `This project is hosted on ${link.hub_host}, and that is where it is taken offline or removed. Both links below open this project’s page there.`;
|
|
260
|
+
$('ps-danger-manage').textContent = `Manage this project on ${link.hub_host}`;
|
|
261
|
+
$('ps-danger-manage').href = link.manage_url;
|
|
262
|
+
$('ps-danger-offline').href = link.offline_url;
|
|
263
|
+
$('ps-danger-disconnect').href = link.disconnect_url;
|
|
264
|
+
$('ps-danger').hidden = false;
|
|
265
|
+
}
|
|
266
|
+
|
|
248
267
|
load();
|
|
268
|
+
loadDanger();
|
|
249
269
|
})();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# The danger zone on /project-settings — a hosted project's owner reaches Take-offline and Delete (task 1004570)
|
|
2
|
+
|
|
3
|
+
The owner of a hosted project could not find how to delete it: both controls live on the hub's Projects page, and nothing in the project's own hall pointed there. What this settled:
|
|
4
|
+
|
|
5
|
+
- **Link-out only.** The section links to the hub's page for this project, scrolled to the `#mDelete` / `#mDisconnect` cards. The typed-name confirm and the intent queue stay on the hub (ADR 0290); the hall gains no second delete path.
|
|
6
|
+
- **The server decides who sees it.** `GET /project/manage-link` is Archon-only (on a hosted project the owner is its Archon), and answers `{ link: null }` when the instance has no hub to link to — a self-hosted instance, or the hub itself. The section is `hidden` in the markup and shown only for a real link, so every other outcome is a missing section, never a dead link.
|
|
7
|
+
- **The link names the project by slug.** The hall knows its hub origin and client id (which is its slug, ADR 0246) from its sign-in configuration, never a hardcoded domain. The hub's Projects page resolves `?view=manage&project=<slug>` against the visitor's OWN projects; a slug that is not theirs opens their list, not someone else's project. The slug survives a sign-in detour.
|
|
8
|
+
- **The two sentences track the hub's folds.** `tests/project_manage_link.mjs` holds the hall's claims (repository kept, cannot be undone, can bring it back) against the hub's own fold copy.
|
|
9
|
+
|
|
10
|
+
Tests: `tests/project_manage_link.mjs` (C1 owner link, C2 non-owner refused by the real gate, C3 self-hosted answers none). Harness fixture: `project__manage-link.json`.
|
|
@@ -2477,6 +2477,13 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
2477
2477
|
the ?view=manage&id=N deep link, read by loadManage() */
|
|
2478
2478
|
var manageId = null;
|
|
2479
2479
|
var manageInst = null;
|
|
2480
|
+
/* a project's own hall links here by SLUG (task 1004570): it knows its slug, not
|
|
2481
|
+
this page's instance id. Held until the visitor's own projects load, then
|
|
2482
|
+
resolved against THOSE only — a slug that is not yours opens nothing. The
|
|
2483
|
+
section (#mDelete / #mDisconnect) is held too, because setView rewrites the
|
|
2484
|
+
URL and drops the hash before the manage view has painted. */
|
|
2485
|
+
var handedProject = null;
|
|
2486
|
+
var handedSection = null;
|
|
2480
2487
|
|
|
2481
2488
|
function viewFromUrl() {
|
|
2482
2489
|
var v;
|
|
@@ -2486,6 +2493,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
2486
2493
|
var id = null;
|
|
2487
2494
|
try { id = new URLSearchParams(location.search).get('id'); } catch (e) {}
|
|
2488
2495
|
if (id) { manageId = String(id); return 'manage'; }
|
|
2496
|
+
try { handedProject = new URLSearchParams(location.search).get('project') || null; } catch (e) {}
|
|
2497
|
+
if (handedProject && /^#m(Delete|Disconnect)$/.test(location.hash || '')) handedSection = location.hash.slice(1);
|
|
2489
2498
|
return 'mine';
|
|
2490
2499
|
}
|
|
2491
2500
|
return VIEWS[v] ? v : 'map';
|
|
@@ -2526,8 +2535,10 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
2526
2535
|
sign-in door on the hub's own pages carries the repo scope (task 1004549,
|
|
2527
2536
|
ADR 0364): it is asked once, at sign-in, and held for the whole session, so
|
|
2528
2537
|
no flow ever stops halfway to ask for it */
|
|
2529
|
-
|
|
2530
|
-
|
|
2538
|
+
/* a hand-off that is still waiting on sign-in keeps its slug through the door */
|
|
2539
|
+
var backPart = handedProject ? encodeURIComponent('?view=manage&project=' + handedProject)
|
|
2540
|
+
: '%3Fview%3D' + v + idPart.replace('&', '%26').replace('=', '%3D');
|
|
2541
|
+
$('signinTop').href = '/api/bongos/auth/web/start?return=/projects' + backPart + '&repos=1';
|
|
2531
2542
|
/* the mine view is the only one that polls; it re-reads on entry so a
|
|
2532
2543
|
card that changed while you were on the map is not stale — including
|
|
2533
2544
|
the membership + invitations lists (task 1003774), the same re-read
|
|
@@ -3324,6 +3335,16 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
3324
3335
|
opens that project's own manage view — the per-project page the
|
|
3325
3336
|
manage-wave tasks fill in. It replaced the scroll-anchor-into-the-list
|
|
3326
3337
|
this function used to do. ── */
|
|
3338
|
+
/* the hall's hand-off (task 1004570): the slug becomes one of YOUR projects or
|
|
3339
|
+
nothing — this page stays on your list rather than guessing */
|
|
3340
|
+
function openHandedProject() {
|
|
3341
|
+
if (!handedProject) return;
|
|
3342
|
+
var slug = handedProject;
|
|
3343
|
+
handedProject = null;
|
|
3344
|
+
var inst = ownedInstances.filter(function (i) { return i && i.slug === slug; })[0];
|
|
3345
|
+
if (inst) goToManage(inst.id);
|
|
3346
|
+
else handedSection = null;
|
|
3347
|
+
}
|
|
3327
3348
|
function goToManage(instId) {
|
|
3328
3349
|
manageId = String(instId || '');
|
|
3329
3350
|
manageInst = null;
|
|
@@ -8609,6 +8630,12 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
8609
8630
|
var inst = data.instance || {};
|
|
8610
8631
|
inst.open_intent = data.open_intent || null;
|
|
8611
8632
|
renderManage(inst);
|
|
8633
|
+
/* the hall's hand-off named a card — land on it once the page has its content */
|
|
8634
|
+
if (handedSection) {
|
|
8635
|
+
var target = $(handedSection);
|
|
8636
|
+
handedSection = null;
|
|
8637
|
+
if (target && target.scrollIntoView) target.scrollIntoView({ block: 'start' });
|
|
8638
|
+
}
|
|
8612
8639
|
/* the SECOND, GitHub-backed read — fired only here (entry + tab
|
|
8613
8640
|
return), never from renderManage, which the fetch-free tick calls */
|
|
8614
8641
|
if (!keepRepo) loadRepoVisibility(inst);
|
|
@@ -10841,7 +10868,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
10841
10868
|
$('gate').hidden = true;
|
|
10842
10869
|
$('mine').hidden = false;
|
|
10843
10870
|
load();
|
|
10844
|
-
return Promise.all([loadMemberships(), loadOwnedInstances(), loadInvites()]);
|
|
10871
|
+
return Promise.all([loadMemberships(), loadOwnedInstances(), loadInvites()]).then(openHandedProject);
|
|
10845
10872
|
})
|
|
10846
10873
|
.catch(function (err) {
|
|
10847
10874
|
me = null;
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.61",
|
|
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.61",
|
|
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.61",
|
|
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
|
@@ -9132,5 +9132,11 @@
|
|
|
9132
9132
|
"id": "1004553",
|
|
9133
9133
|
"text": "Builders can now save their filter settings on the Goals and Tasks pages as named views, pick one to open the page with, and find them again on any device. Each builder's views are private to them."
|
|
9134
9134
|
}
|
|
9135
|
+
],
|
|
9136
|
+
"1.21.61": [
|
|
9137
|
+
{
|
|
9138
|
+
"id": "1004570",
|
|
9139
|
+
"text": "A project owner now sees a Danger zone in their project's settings, with links straight to the place where they can take the project offline or delete it. Nobody else sees it, and self-hosted projects don't show it."
|
|
9140
|
+
}
|
|
9135
9141
|
]
|
|
9136
9142
|
}
|
|
@@ -50,6 +50,23 @@ function hubDeployUrl(config) {
|
|
|
50
50
|
return `${config.origin.replace(/\/+$/, '')}/deploy?project=${encodeURIComponent(String(config.clientId))}`;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
// WHERE THIS PROJECT'S OWNER TAKES IT OFFLINE OR DISCONNECTS IT (task 1004570). Both
|
|
54
|
+
// controls live on the hub's Projects page for this project — the typed-name confirm and
|
|
55
|
+
// the intent queue are there (ADR 0290) — so the hall only links to them and gains no
|
|
56
|
+
// second delete path. Same two inputs as hubDeployUrl (the hub origin and this project's
|
|
57
|
+
// client id, which is its slug); null on a self-hosted instance or the hub itself, so the
|
|
58
|
+
// hall shows no section rather than a dead link. The page resolves ?project=<slug>
|
|
59
|
+
// against the visitor's OWN projects, so the link opens nothing for anyone else.
|
|
60
|
+
function hubManageLinks(config) {
|
|
61
|
+
if (!config || typeof config.origin !== 'string' || !config.clientId) return null;
|
|
62
|
+
if (!/^https?:\/\/[^\s"'<>]+$/i.test(config.origin)) return null;
|
|
63
|
+
const origin = config.origin.replace(/\/+$/, '');
|
|
64
|
+
const page = `${origin}/projects?view=manage&project=${encodeURIComponent(String(config.clientId))}`;
|
|
65
|
+
let host = origin;
|
|
66
|
+
try { host = new URL(origin).host; } catch { /* keep the origin as written */ }
|
|
67
|
+
return { hub_host: host, manage_url: page, offline_url: `${page}#mDelete`, disconnect_url: `${page}#mDisconnect` };
|
|
68
|
+
}
|
|
69
|
+
|
|
53
70
|
// WHERE THIS PROJECT'S UPDATE BUTTON GOES (task 1004296). The act itself never happens in
|
|
54
71
|
// Settings: it is the deploy door's two-step confirm and auto-rollback (ADR 0293), and that
|
|
55
72
|
// door is the provisioning module's — so only a hall that runs provisioning has one.
|
|
@@ -216,9 +233,25 @@ function buildCoreUpdateRouter({
|
|
|
216
233
|
}, { errorCode: 'software_update_failed', message: false }),
|
|
217
234
|
);
|
|
218
235
|
|
|
236
|
+
// GET /project/manage-link — the hub's page for THIS project, where its owner takes it
|
|
237
|
+
// offline or disconnects it (task 1004570). On a hosted project the owner is its Archon,
|
|
238
|
+
// so the read is Archon-only: a builder below that is refused and the hall draws no
|
|
239
|
+
// section. { link: null } on a self-hosted instance (no hub to link to).
|
|
240
|
+
// rank: archon — the project's owner; it names where the project can be ended.
|
|
241
|
+
router.get(
|
|
242
|
+
'/project/manage-link',
|
|
243
|
+
auth.requireBuilder,
|
|
244
|
+
auth.requireRank('archon'),
|
|
245
|
+
asyncHandler('GET /project/manage-link', async (_req, res) => {
|
|
246
|
+
const { idp } = doorInputs();
|
|
247
|
+
res.json({ ok: true, link: hubManageLinks(idp) });
|
|
248
|
+
}, { errorCode: 'manage_link_failed', message: false }),
|
|
249
|
+
);
|
|
250
|
+
|
|
219
251
|
return router;
|
|
220
252
|
}
|
|
221
253
|
|
|
222
254
|
module.exports = buildCoreUpdateRouter;
|
|
255
|
+
module.exports.hubManageLinks = hubManageLinks;
|
|
223
256
|
module.exports.hubDeployUrl = hubDeployUrl;
|
|
224
257
|
module.exports.coreUpdateDocketMatters = coreUpdateDocketMatters;
|
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.61'; // 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');
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// tests/project_manage_link.mjs — a hosted project's owner reaches Take-offline and
|
|
2
|
+
// Delete from their own hall (task 1004570). These EXECUTE: the link builder against
|
|
3
|
+
// real inputs, and real HTTP requests through the assembled router with only
|
|
4
|
+
// requireBuilder replaced, so the Archon gate that refuses a non-owner is the real one.
|
|
5
|
+
//
|
|
6
|
+
// Run: node --test tests/project_manage_link.mjs
|
|
7
|
+
|
|
8
|
+
import assert from 'node:assert/strict';
|
|
9
|
+
import { test } from 'node:test';
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
|
|
15
|
+
process.env.NODE_ENV = 'test';
|
|
16
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
17
|
+
const require_ = createRequire(import.meta.url);
|
|
18
|
+
const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
|
|
19
|
+
const { hubManageLinks } = require_(path.join(ROOT, 'src/bongos/routes/core-update.js'));
|
|
20
|
+
|
|
21
|
+
const HUB = { origin: 'https://hub.example.com/', clientId: 'mercury' };
|
|
22
|
+
|
|
23
|
+
test('the link opens THIS project on the hub, at the two cards', () => {
|
|
24
|
+
assert.deepEqual(hubManageLinks(HUB), {
|
|
25
|
+
hub_host: 'hub.example.com',
|
|
26
|
+
manage_url: 'https://hub.example.com/projects?view=manage&project=mercury',
|
|
27
|
+
offline_url: 'https://hub.example.com/projects?view=manage&project=mercury#mDelete',
|
|
28
|
+
disconnect_url: 'https://hub.example.com/projects?view=manage&project=mercury#mDisconnect',
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('no hub (self-hosted, or the hub itself) means no link, never a wrong one', () => {
|
|
33
|
+
assert.equal(hubManageLinks(null), null);
|
|
34
|
+
assert.equal(hubManageLinks({ origin: 'https://hub.example.com' }), null, 'no client id');
|
|
35
|
+
assert.equal(hubManageLinks({ origin: 'javascript:alert(1)', clientId: 'x' }), null, 'not an http origin');
|
|
36
|
+
assert.equal(hubManageLinks({ origin: 'https://hub.example.com/"><script>', clientId: 'x' }), null);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// One server per test: requireBuilder is swapped for a stub that seats the rank the
|
|
40
|
+
// request names, and the route module is re-required so it captures the stub.
|
|
41
|
+
async function withServer(idp, fn) {
|
|
42
|
+
const r2 = createRequire(import.meta.url);
|
|
43
|
+
const authPath = r2.resolve(path.join(ROOT, 'src/bongos/auth.js'));
|
|
44
|
+
const routePath = r2.resolve(path.join(ROOT, 'src/bongos/routes/core-update.js'));
|
|
45
|
+
const realAuth = r2(authPath);
|
|
46
|
+
const saved = realAuth.requireBuilder;
|
|
47
|
+
realAuth.requireBuilder = (req, _res, next) => { req.builder = { id: 7, rank: req.get('x-rank') }; next(); };
|
|
48
|
+
delete r2.cache[routePath];
|
|
49
|
+
const express = r2('express');
|
|
50
|
+
const { attachFail } = r2(path.join(ROOT, 'src/bongos/middleware/error-envelope.js'));
|
|
51
|
+
const app = express();
|
|
52
|
+
app.use(attachFail);
|
|
53
|
+
app.use(r2(routePath)({ doorInputs: () => ({ provisioning: false, idp }) }));
|
|
54
|
+
const server = app.listen(0);
|
|
55
|
+
await new Promise((res) => server.once('listening', res));
|
|
56
|
+
const get = (rank) => fetch(`http://127.0.0.1:${server.address().port}/project/manage-link`, { headers: { 'x-rank': rank } });
|
|
57
|
+
try { await fn(get); } finally {
|
|
58
|
+
server.close();
|
|
59
|
+
realAuth.requireBuilder = saved;
|
|
60
|
+
delete r2.cache[routePath];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
test('C1: the owner (the hosted project\'s Archon) gets the link', async () => {
|
|
65
|
+
await withServer(HUB, async (get) => {
|
|
66
|
+
const res = await get('archon');
|
|
67
|
+
assert.equal(res.status, 200);
|
|
68
|
+
assert.equal((await res.json()).link.offline_url, 'https://hub.example.com/projects?view=manage&project=mercury#mDelete');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('C2: a builder of the same project below Archon is refused by the server', async () => {
|
|
73
|
+
await withServer(HUB, async (get) => {
|
|
74
|
+
for (const rank of ['metic', 'thetes', 'xenos']) {
|
|
75
|
+
const res = await get(rank);
|
|
76
|
+
assert.equal(res.status, 403, `${rank} is refused`);
|
|
77
|
+
assert.equal(JSON.stringify(await res.json()).includes('hub.example.com'), false, `${rank} learns nothing`);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('C3: a self-hosted instance answers no link', async () => {
|
|
83
|
+
await withServer(null, async (get) => {
|
|
84
|
+
const res = await get('archon');
|
|
85
|
+
assert.equal(res.status, 200);
|
|
86
|
+
assert.equal((await res.json()).link, null);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('the hall section is hidden in the markup and shown only for a real link', () => {
|
|
91
|
+
const html = read('modules/hall-ui/public/project-settings.html');
|
|
92
|
+
assert.match(html, /<section class="scroll" id="ps-danger"[^>]*\bhidden>/);
|
|
93
|
+
const js = read('modules/hall-ui/public/project-settings.js');
|
|
94
|
+
assert.match(js, /if \(!link \|\| !link\.offline_url \|\| !link\.disconnect_url\) return;/);
|
|
95
|
+
assert.match(js, /\/project\/manage-link/);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('the hub resolves the slug against the visitor\'s OWN projects only', () => {
|
|
99
|
+
const page = read('modules/public-landing/public/projects.html');
|
|
100
|
+
assert.match(page, /ownedInstances\.filter\(function \(i\) \{ return i && i\.slug === slug; \}\)\[0\]/);
|
|
101
|
+
assert.match(page, /\.then\(openHandedProject\)/);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('the hall\'s two sentences say what the hub\'s own folds promise', () => {
|
|
105
|
+
const hall = read('modules/hall-ui/public/project-settings.html');
|
|
106
|
+
const hub = read('modules/public-landing/public/projects.html');
|
|
107
|
+
for (const claim of ['repository on GitHub', 'cannot be undone', 'bring it back', 'Take it offline', 'Disconnect from Bongos entirely']) {
|
|
108
|
+
assert.ok(hall.includes(claim), `the hall says "${claim}"`);
|
|
109
|
+
assert.ok(hub.includes(claim), `the hub says "${claim}" too`);
|
|
110
|
+
}
|
|
111
|
+
});
|