@bongos/core 1.21.74 → 1.21.76
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 +78 -48
- 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/adr/0343-a-module-score-is-a-security-gate-then-an-average-of-visible-parts.md +12 -0
- package/docs/api/openapi.json +81 -3
- package/docs/api-reference.md +5 -3
- package/docs/copy-inventory.md +45 -42
- package/docs/copy-registry.json +76 -49
- package/docs/module-api-changelog.md +4 -0
- package/docs/modules-contract.md +1 -0
- package/docs/page-inventory.json +16 -8
- package/docs/page-readings.json +1542 -1522
- package/modules/hall-ui/public/craft-mode.js +3 -1
- package/modules/hall-ui/public/page-notes.css +150 -0
- package/modules/hall-ui/public/page-notes.js +30 -5
- package/modules/hall-ui/public/studio.css +15 -0
- package/modules/hall-ui/public/studio.html +6 -0
- package/modules/hall-ui/public/studio.js +38 -4
- package/modules/hall-ui/public/style.css +1 -116
- package/modules/page-notes/db.js +19 -0
- package/modules/page-notes/migrations/page_notes_002_surface_index.sql +12 -0
- package/modules/page-notes/notes.js +25 -2
- package/modules/page-notes/routes/page-notes.js +9 -0
- package/modules/public-landing/public/account.html +2 -0
- package/modules/public-landing/public/assets/artist-review.js +107 -0
- package/modules/public-landing/public/contact.html +2 -0
- package/modules/public-landing/public/guild.html +2 -0
- package/modules/public-landing/public/index.html +2 -0
- package/modules/public-landing/public/invitations.html +2 -0
- package/modules/public-landing/public/privacy.html +2 -0
- package/modules/public-landing/public/projects.html +2 -0
- package/modules/public-landing/public/terms.html +2 -0
- package/modules/ui-design/kit/serve.js +32 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/bongos/module-listing.js +85 -0
- package/src/bongos/routes/modules.js +15 -0
- package/src/module-api.js +1 -1
- package/tests/artist_review.mjs +231 -0
- package/tests/hall_studio_home.mjs +39 -1
- package/tests/landing_legal_pages.mjs +2 -1
- package/tests/module_store_listing.mjs +156 -0
- package/tests/page_notes_db.mjs +28 -1
- package/tests/page_notes_panel.mjs +9 -3
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/* assets/artist-review.js — THE ARTIST REVIEW BUTTON on the public site (task
|
|
2
|
+
1004499, goal 1000095 WA6; owner decisions 2026-10-04).
|
|
3
|
+
|
|
4
|
+
A signed-in builder looking at a live public page (the landing, /projects,
|
|
5
|
+
/account and the footer pages) writes UI feedback about it without leaving it.
|
|
6
|
+
The button sits in the hub bar beside the identity chip and opens the hall's
|
|
7
|
+
own feedback panel (modules/hall-ui/public/page-notes.js, the Artistry
|
|
8
|
+
palette), so a review written here is the same doc as one written in the hall:
|
|
9
|
+
split by an AI into ideas filed under the reviewer, and kept as that page's
|
|
10
|
+
design reference. Its page id is site:<page>; the Studio lists them.
|
|
11
|
+
|
|
12
|
+
Signed out there is no button: GET /me answers 401 and nothing is drawn. Any
|
|
13
|
+
signed-in builder sees it, in any craft mode — the button is cosmetic, the
|
|
14
|
+
panel's routes hold the walls (ADR 0016).
|
|
15
|
+
|
|
16
|
+
The page names the hall's origin on this tag (data-hall="{{buildersOrigin}}",
|
|
17
|
+
stamped server-side), so the ideas the panel links to open in the hall, which
|
|
18
|
+
on the hub is another host. A ?note=N link opens that doc in place. */
|
|
19
|
+
(function () {
|
|
20
|
+
'use strict';
|
|
21
|
+
var PANEL = '/modules/hall-ui/public/';
|
|
22
|
+
// The hall palette's own icon (craft-mode.js ICONS.artistry), built as nodes:
|
|
23
|
+
// this surface writes no innerHTML.
|
|
24
|
+
var SVG_NS = 'http://www.w3.org/2000/svg';
|
|
25
|
+
var ICON_PATHS = [
|
|
26
|
+
['M8 2a6 6 0 1 0 0 12c.9 0 1.4-.6 1.4-1.3 0-.8-.6-1-.6-1.7 0-.6.5-1 1.2-1h1.6A2.4 2.4 0 0 0 14 7.6C14 4.5 11.3 2 8 2Z', null],
|
|
27
|
+
['M5 6.2h.01M7.6 4.6h.01M10.4 5.2h.01M4.6 9h.01', '2.2'],
|
|
28
|
+
];
|
|
29
|
+
function icon() {
|
|
30
|
+
var svg = document.createElementNS(SVG_NS, 'svg');
|
|
31
|
+
[['viewBox', '0 0 16 16'], ['fill', 'none'], ['stroke', 'currentColor'], ['stroke-width', '1.4'], ['stroke-linecap', 'round'], ['stroke-linejoin', 'round'], ['aria-hidden', 'true']]
|
|
32
|
+
.forEach(function (a) { svg.setAttribute(a[0], a[1]); });
|
|
33
|
+
ICON_PATHS.forEach(function (p) {
|
|
34
|
+
var path = document.createElementNS(SVG_NS, 'path');
|
|
35
|
+
path.setAttribute('d', p[0]);
|
|
36
|
+
if (p[1]) path.setAttribute('stroke-width', p[1]);
|
|
37
|
+
svg.appendChild(path);
|
|
38
|
+
});
|
|
39
|
+
return svg;
|
|
40
|
+
}
|
|
41
|
+
var tag = document.currentScript;
|
|
42
|
+
var hall = (tag && tag.getAttribute('data-hall')) || '';
|
|
43
|
+
|
|
44
|
+
function linkSheet() {
|
|
45
|
+
if (document.getElementById('arv-sheet')) return;
|
|
46
|
+
var l = document.createElement('link');
|
|
47
|
+
l.id = 'arv-sheet';
|
|
48
|
+
l.rel = 'stylesheet';
|
|
49
|
+
l.href = PANEL + 'page-notes.css';
|
|
50
|
+
document.head.appendChild(l);
|
|
51
|
+
}
|
|
52
|
+
var loading = null;
|
|
53
|
+
// The panel learns it is on the public site, and where the hall is, from here.
|
|
54
|
+
function ready(api) { api.configure({ surface: 'site', hall: hall }); return api; }
|
|
55
|
+
function loadPanel() {
|
|
56
|
+
if (window.OTBPageNotes) return Promise.resolve(ready(window.OTBPageNotes));
|
|
57
|
+
if (loading) return loading;
|
|
58
|
+
loading = new Promise(function (resolve, reject) {
|
|
59
|
+
var s = document.createElement('script');
|
|
60
|
+
s.src = PANEL + 'page-notes.js';
|
|
61
|
+
s.onload = function () { if (window.OTBPageNotes) resolve(ready(window.OTBPageNotes)); else reject(new Error('panel did not load')); };
|
|
62
|
+
s.onerror = function () { loading = null; reject(new Error('panel did not load')); };
|
|
63
|
+
document.head.appendChild(s);
|
|
64
|
+
});
|
|
65
|
+
return loading;
|
|
66
|
+
}
|
|
67
|
+
// The hall's palette (craft-mode.js TOOLS.artistry) wears the same two names;
|
|
68
|
+
// the two pages share no script, so tests/artist_review.mjs holds them equal.
|
|
69
|
+
var LABEL = 'Artist review: write feedback on this page';
|
|
70
|
+
var SHORT = 'Review';
|
|
71
|
+
function drawButton() {
|
|
72
|
+
var bar = document.querySelector('.barRight');
|
|
73
|
+
if (!bar || document.getElementById('arv-btn')) return null;
|
|
74
|
+
var b = document.createElement('button');
|
|
75
|
+
b.type = 'button';
|
|
76
|
+
b.id = 'arv-btn';
|
|
77
|
+
b.className = 'arv-btn';
|
|
78
|
+
b.setAttribute('aria-label', LABEL);
|
|
79
|
+
b.title = LABEL;
|
|
80
|
+
var label = document.createElement('span');
|
|
81
|
+
label.className = 'arv-btn__label';
|
|
82
|
+
label.textContent = SHORT;
|
|
83
|
+
b.appendChild(icon());
|
|
84
|
+
b.appendChild(label);
|
|
85
|
+
b.addEventListener('click', function () {
|
|
86
|
+
loadPanel().then(function (api) { api.open(); }, function () { b.setAttribute('aria-disabled', 'true'); });
|
|
87
|
+
});
|
|
88
|
+
var who = document.getElementById('who');
|
|
89
|
+
if (who && who.parentNode === bar) bar.insertBefore(b, who);
|
|
90
|
+
else bar.appendChild(b);
|
|
91
|
+
return b;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function signedIn(me) {
|
|
95
|
+
if (!me) return;
|
|
96
|
+
linkSheet();
|
|
97
|
+
drawButton();
|
|
98
|
+
var note = /[?&]note=([1-9]\d{0,17})(?:&|$)/.exec(window.location.search || '');
|
|
99
|
+
if (note) loadPanel().then(function (api) { api.openNote(note[1]); }, function () { /* the page still works */ });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fetch('/api/bongos/me', { method: 'GET', credentials: 'same-origin', headers: { Accept: 'application/json' } })
|
|
103
|
+
.then(function (r) { return r.ok ? r.json().catch(function () { return null; }) : null; })
|
|
104
|
+
.then(function (d) { signedIn(d ? (d.builder || d.me || d) : null); }, function () { /* signed out or offline: no button */ });
|
|
105
|
+
|
|
106
|
+
window.OTBArtistReview = { signedIn: signedIn };
|
|
107
|
+
})();
|
|
@@ -120,5 +120,7 @@
|
|
|
120
120
|
|
|
121
121
|
<footer class="foot"><!--apex-only:start--><span>A non-profit, capture-resistant commons. </span><!--apex-only:end-->Built by agents, on Cloud Bongos. <a href="https://www.npmjs.com/package/@bongos/core" target="_blank" rel="noopener">Open source</a> (<a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">AGPL-3.0</a>).<nav class="footNav" aria-label="Site"><a href="/privacy">Privacy</a><a href="/terms">Terms</a><a href="/contact" aria-current="page">Contact</a><a data-link-hall href="{{buildersOrigin}}">Builders' hall</a></nav></footer>
|
|
122
122
|
<script src="/assets/hub-bar.js"></script>
|
|
123
|
+
<!-- the Artist review button: signed-in builders only (task 1004499) -->
|
|
124
|
+
<script src="/assets/artist-review.js" data-hall="{{buildersOrigin}}"></script>
|
|
123
125
|
</body>
|
|
124
126
|
</html>
|
|
@@ -1261,5 +1261,7 @@ composition A; landing composition v3, picked by the owner 2026-08-27. No roll,
|
|
|
1261
1261
|
});
|
|
1262
1262
|
})();
|
|
1263
1263
|
</script>
|
|
1264
|
+
<!-- the Artist review button: signed-in builders only (task 1004499) -->
|
|
1265
|
+
<script src="/assets/artist-review.js" data-hall="{{buildersOrigin}}"></script>
|
|
1264
1266
|
</body>
|
|
1265
1267
|
</html>
|
|
@@ -129,5 +129,7 @@
|
|
|
129
129
|
|
|
130
130
|
<footer class="foot"><!--apex-only:start--><span>A non-profit, capture-resistant commons. </span><!--apex-only:end-->Built by agents, on Cloud Bongos. <a href="https://www.npmjs.com/package/@bongos/core" target="_blank" rel="noopener">Open source</a> (<a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">AGPL-3.0</a>).<nav class="footNav" aria-label="Site"><a href="/privacy" aria-current="page">Privacy</a><a href="/terms">Terms</a><a href="/contact">Contact</a><a data-link-hall href="{{buildersOrigin}}">Builders' hall</a></nav></footer>
|
|
131
131
|
<script src="/assets/hub-bar.js"></script>
|
|
132
|
+
<!-- the Artist review button: signed-in builders only (task 1004499) -->
|
|
133
|
+
<script src="/assets/artist-review.js" data-hall="{{buildersOrigin}}"></script>
|
|
132
134
|
</body>
|
|
133
135
|
</html>
|
|
@@ -11174,5 +11174,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
11174
11174
|
.then(loadSelfShipped, loadSelfShipped);
|
|
11175
11175
|
})();
|
|
11176
11176
|
</script>
|
|
11177
|
+
<!-- the Artist review button: signed-in builders only (task 1004499) -->
|
|
11178
|
+
<script src="/assets/artist-review.js" data-hall="{{buildersOrigin}}"></script>
|
|
11177
11179
|
</body>
|
|
11178
11180
|
</html>
|
|
@@ -134,5 +134,7 @@
|
|
|
134
134
|
|
|
135
135
|
<footer class="foot"><!--apex-only:start--><span>A non-profit, capture-resistant commons. </span><!--apex-only:end-->Built by agents, on Cloud Bongos. <a href="https://www.npmjs.com/package/@bongos/core" target="_blank" rel="noopener">Open source</a> (<a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">AGPL-3.0</a>).<nav class="footNav" aria-label="Site"><a href="/privacy">Privacy</a><a href="/terms" aria-current="page">Terms</a><a href="/contact">Contact</a><a data-link-hall href="{{buildersOrigin}}">Builders' hall</a></nav></footer>
|
|
136
136
|
<script src="/assets/hub-bar.js"></script>
|
|
137
|
+
<!-- the Artist review button: signed-in builders only (task 1004499) -->
|
|
138
|
+
<script src="/assets/artist-review.js" data-hall="{{buildersOrigin}}"></script>
|
|
137
139
|
</body>
|
|
138
140
|
</html>
|
|
@@ -439,6 +439,33 @@ function handlePatch(res, body) {
|
|
|
439
439
|
return json(res, 200, { handle: h, previous_handle: previous || null });
|
|
440
440
|
}
|
|
441
441
|
|
|
442
|
+
// The page feedback panel's routes (task 1004499: the public site's Artist review
|
|
443
|
+
// button opens modules/hall-ui/public/page-notes.js), so a render can picture the
|
|
444
|
+
// panel open. One draft per page for the run; nothing is ever split or filed.
|
|
445
|
+
// SIMPLIFIED: drafts are keyed by page alone, where the real route keys them by
|
|
446
|
+
// (author, page) — the stub has one fixture builder, so the two agree here.
|
|
447
|
+
const pnDrafts = new Map();
|
|
448
|
+
function pageNotesRoute(req, res, rest, u) {
|
|
449
|
+
if (!rest.startsWith('/page-notes')) return false;
|
|
450
|
+
if (!AUTH) { json(res, 401, UNAUTH); return true; }
|
|
451
|
+
if (rest === '/page-notes/site') { json(res, 200, { notes: [] }); return true; }
|
|
452
|
+
if (rest === '/page-notes' && req.method === 'GET') { json(res, 200, { page_id: u.searchParams.get('page_id') || '', notes: [] }); return true; }
|
|
453
|
+
if (rest === '/page-notes' && req.method === 'POST') {
|
|
454
|
+
readBody(req, (body) => {
|
|
455
|
+
const key = String(body.page_id || '');
|
|
456
|
+
if (!pnDrafts.has(key)) pnDrafts.set(key, { id: String(pnDrafts.size + 1), page_id: key, page_path: body.page_path || '/', page_title: body.page_title || '', body_md: '', status: 'draft', idea_ids: [], author_name: 'builder' });
|
|
457
|
+
json(res, 200, { note: pnDrafts.get(key), images: [] });
|
|
458
|
+
});
|
|
459
|
+
return true;
|
|
460
|
+
}
|
|
461
|
+
const one = rest.match(/^\/page-notes\/(\d+)$/);
|
|
462
|
+
if (one && req.method === 'PATCH') {
|
|
463
|
+
readBody(req, (body) => { for (const n of pnDrafts.values()) if (n.id === one[1]) n.body_md = String(body.body_md || ''); json(res, 200, { ok: true }); });
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
|
|
442
469
|
// The hub's guild page (task 1002301): GET /guilds/:slug (public), GET /guilds/:slug/members
|
|
443
470
|
// and PATCH /guilds/:slug/membership (own-scope), answered from ONE set of guilds held for the
|
|
444
471
|
// run (guilds.json, or the file a states file's stub.fixtures names), keyed by slug, each with
|
|
@@ -661,6 +688,7 @@ const server = http.createServer((req, res) => {
|
|
|
661
688
|
starter-bundles.js declaration, so the picture the kit renders and the
|
|
662
689
|
payload the route serves cannot drift apart silently. */
|
|
663
690
|
if (rest === '/provisioning/starter-bundles') return json(res, 200, STARTER_BUNDLES_FIXTURE);
|
|
691
|
+
if (pageNotesRoute(req, res, rest, u)) return;
|
|
664
692
|
if (rest === '/auth/logout') return json(res, 200, { ok: true });
|
|
665
693
|
if (rest.startsWith('/auth/web/start')) { res.writeHead(302, { location: PREFIX }); return res.end(); }
|
|
666
694
|
if (rest === '/instance') { const b = pages.size ? pages.values().next().value.brand : {}; return json(res, 200, { brand: { productName: (b && b.productName) || '', project: {} }, modules: {} }); }
|
|
@@ -672,6 +700,10 @@ const server = http.createServer((req, res) => {
|
|
|
672
700
|
res.writeHead(200, { 'content-type': MIME['.js'], 'cache-control': 'no-store' });
|
|
673
701
|
return res.end(bundle ? fs.readFileSync(bundle, 'utf8') : '/* stub: no client bundle in this checkout */');
|
|
674
702
|
}
|
|
703
|
+
// an enabled module's browser assets, as serve-internal.js serves them on every host
|
|
704
|
+
// (task 1004499: the public site loads the hall's feedback panel from here)
|
|
705
|
+
const modAsset = p.match(/^\/modules\/([a-z][a-z0-9-]*)\/public\/(.+)$/);
|
|
706
|
+
if (modAsset) { const abs = fileUnder(path.join(ROOT, 'modules', modAsset[1], 'public'), modAsset[2]); if (abs) return sendFile(abs, res); }
|
|
675
707
|
// docs/ raw (a mock's relative links resolve) and the surface dir by its checkout path
|
|
676
708
|
if (p.startsWith('/docs/')) { const abs = fileUnder(DOCS, p.slice('/docs/'.length)); if (abs) return sendFile(abs, res); }
|
|
677
709
|
const relSurface = path.relative(ROOT, DIR).split(path.sep).join('/');
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.76",
|
|
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.76",
|
|
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.76",
|
|
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
|
@@ -9254,5 +9254,17 @@
|
|
|
9254
9254
|
"id": "1004628",
|
|
9255
9255
|
"text": "Projects now have a Ship mode setting. Auto, the default, means the AI ships finished work on its own. Manual means it asks first. Owners can switch it on the project settings page."
|
|
9256
9256
|
}
|
|
9257
|
+
],
|
|
9258
|
+
"1.21.75": [
|
|
9259
|
+
{
|
|
9260
|
+
"id": "1003807",
|
|
9261
|
+
"text": "The module store now only shows modules that are safe and good enough: each must pass the security check, and once it has real-world data it needs a score of at least 50 out of 100. Brand-new modules show with a 'New' label un"
|
|
9262
|
+
}
|
|
9263
|
+
],
|
|
9264
|
+
"1.21.76": [
|
|
9265
|
+
{
|
|
9266
|
+
"id": "1004499",
|
|
9267
|
+
"text": "Artists can now review the public website while looking at it. A signed-in builder on cloudbongos.com's public pages (home, projects, account, guilds, invitations, privacy, terms, contact) sees a Review button in the top bar,"
|
|
9268
|
+
}
|
|
9257
9269
|
]
|
|
9258
9270
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// src/bongos/module-listing.js — which store modules the catalog lists, under the
|
|
2
|
+
// quality floor (task 1003807, criterion wa5-curated-listing, ADR 0343 D2/D5).
|
|
3
|
+
//
|
|
4
|
+
// One module appears once, as its NEWEST published version (semver order), judged by
|
|
5
|
+
// that version's CURRENT score: the newest module_assessment_scores row for it
|
|
6
|
+
// (computed_at DESC, id DESC), whether computed or a Metic+ override (core_265).
|
|
7
|
+
// placeVersion is the whole rule and is pure, so tests drive it directly:
|
|
8
|
+
//
|
|
9
|
+
// excluded the Security gate has not passed (false, or not run yet), or the
|
|
10
|
+
// version has no score row at all. The gate is a hard exclusion (D2).
|
|
11
|
+
// new passed the gate but has no real-world data yet (is_new, D5). Listed
|
|
12
|
+
// with a "New" label and never ranked: the floor does not apply until
|
|
13
|
+
// it has a real score (owner decision, 2026-10-01).
|
|
14
|
+
// listed passed the gate and its overall score is at least the floor.
|
|
15
|
+
// excluded otherwise: scored below the floor, or no overall to compare.
|
|
16
|
+
//
|
|
17
|
+
// A delisted module (ADR 0338 D2) is never in the catalog, whatever its score.
|
|
18
|
+
|
|
19
|
+
const { parseVersion, cmp } = require('../module-loader/semver');
|
|
20
|
+
|
|
21
|
+
// The quality floor, 0-100. It lives here and nowhere else: it is part of Bongos
|
|
22
|
+
// itself, not a live setting, so no author, builder or instance can move it.
|
|
23
|
+
// Changing it is an owner-approved, reviewed task (owner decision, 2026-10-01).
|
|
24
|
+
// Authors read the rule in docs/modules-contract.md ("Getting listed").
|
|
25
|
+
const QUALITY_FLOOR = 50;
|
|
26
|
+
|
|
27
|
+
// Where one version stands. `score` is its current score row, or null.
|
|
28
|
+
// Returns { placement: 'listed' | 'new' | 'excluded', reason }.
|
|
29
|
+
function placeVersion(score, floor = QUALITY_FLOOR) {
|
|
30
|
+
if (!score) return { placement: 'excluded', reason: 'not_assessed' };
|
|
31
|
+
if (score.security_passed !== true) return { placement: 'excluded', reason: 'security_not_passed' };
|
|
32
|
+
if (score.is_new) return { placement: 'new', reason: 'new' };
|
|
33
|
+
if (score.overall == null) return { placement: 'excluded', reason: 'no_score' };
|
|
34
|
+
if (score.overall < floor) return { placement: 'excluded', reason: 'below_floor' };
|
|
35
|
+
return { placement: 'listed', reason: 'at_or_above_floor' };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The newest version row per module key, in semver order (a version that does not
|
|
39
|
+
// parse is skipped, as latestVersions does in module-store.js).
|
|
40
|
+
function newestPerModule(rows) {
|
|
41
|
+
const out = new Map();
|
|
42
|
+
for (const r of rows) {
|
|
43
|
+
const next = parseVersion(r.version);
|
|
44
|
+
if (!next) continue;
|
|
45
|
+
const have = out.get(r.module_key);
|
|
46
|
+
if (have && cmp(parseVersion(have.version), next) >= 0) continue;
|
|
47
|
+
out.set(r.module_key, r);
|
|
48
|
+
}
|
|
49
|
+
return [...out.values()];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The catalog: { floor, listed: [...], new: [...] }, each entry
|
|
53
|
+
// { key, title, version, published_at, overall, is_new }. Excluded versions are left
|
|
54
|
+
// out entirely: a module below the bar is not listed at all. Both lists are in key
|
|
55
|
+
// order; ranking is a separate task (1003808) and does not happen here.
|
|
56
|
+
async function listCatalog({ db, floor = QUALITY_FLOOR } = {}) {
|
|
57
|
+
const pool = db || require('./pool').pool;
|
|
58
|
+
const { rows: versions } = await pool.query(
|
|
59
|
+
`SELECT m.module_key, m.title, v.id AS version_id, v.version, v.published_at
|
|
60
|
+
FROM store_modules m JOIN store_module_versions v ON v.module_key = m.module_key
|
|
61
|
+
WHERE m.status = 'listed'`);
|
|
62
|
+
const newest = newestPerModule(versions);
|
|
63
|
+
const scores = new Map();
|
|
64
|
+
if (newest.length) {
|
|
65
|
+
const { rows } = await pool.query(
|
|
66
|
+
`SELECT DISTINCT ON (version_id) version_id, overall, security_passed, is_new
|
|
67
|
+
FROM module_assessment_scores
|
|
68
|
+
WHERE version_id = ANY($1)
|
|
69
|
+
ORDER BY version_id, computed_at DESC, id DESC`, [newest.map((r) => r.version_id)]);
|
|
70
|
+
for (const s of rows) scores.set(String(s.version_id), s);
|
|
71
|
+
}
|
|
72
|
+
const out = { floor, listed: [], new: [] };
|
|
73
|
+
for (const r of newest.sort((a, b) => a.module_key.localeCompare(b.module_key))) {
|
|
74
|
+
const score = scores.get(String(r.version_id)) || null;
|
|
75
|
+
const { placement } = placeVersion(score, floor);
|
|
76
|
+
if (placement === 'excluded') continue;
|
|
77
|
+
out[placement].push({
|
|
78
|
+
key: r.module_key, title: r.title, version: r.version, published_at: r.published_at,
|
|
79
|
+
overall: placement === 'new' ? null : score.overall, is_new: placement === 'new',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { QUALITY_FLOOR, placeVersion, listCatalog };
|
|
@@ -97,6 +97,7 @@ const log = require('../logger').logger.child({ src: 'modules' });
|
|
|
97
97
|
// property access on a CommonJS namespace, so reaching these through one made them read
|
|
98
98
|
// as unused exports and tripped the dead-code ratchet that held this branch out of main.
|
|
99
99
|
const { listOverrides, setOverride } = require('../module-overrides');
|
|
100
|
+
const { listCatalog } = require('../module-listing');
|
|
100
101
|
|
|
101
102
|
// A packaged submission embeds every file's content (base64) — cap the total
|
|
102
103
|
// so a runaway/misbuilt module can't bloat the review-queue table. Generous
|
|
@@ -692,6 +693,20 @@ module.exports = function buildModulesRouter() {
|
|
|
692
693
|
res.json({ latest });
|
|
693
694
|
}));
|
|
694
695
|
|
|
696
|
+
// The store catalog under the quality floor (task 1003807, ADR 0343 D2/D5): each
|
|
697
|
+
// listed module's newest version, if it passed the Security gate and scores at or
|
|
698
|
+
// above the floor, or under "new" if it has no real-world data yet. Below the bar
|
|
699
|
+
// is not listed at all. The floor is fixed in module-listing.js and returned here.
|
|
700
|
+
//
|
|
701
|
+
// rank: any-builder — a read of public store metadata, like /store/modules/latest:
|
|
702
|
+
// no entitlement taken, nothing changed.
|
|
703
|
+
router.get('/store/modules', auth.requireBuilder,
|
|
704
|
+
asyncHandler('GET /store/modules', async (_req, res) => {
|
|
705
|
+
const listing = await listCatalog();
|
|
706
|
+
res.set('Cache-Control', 'no-store');
|
|
707
|
+
res.json(listing);
|
|
708
|
+
}));
|
|
709
|
+
|
|
695
710
|
router.post('/store/modules/:key/acquired', auth.requireBuilder,
|
|
696
711
|
asyncHandler('POST /store/modules/:key/acquired', async (req, res) => {
|
|
697
712
|
const { key } = req.params;
|
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.76'; // 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,231 @@
|
|
|
1
|
+
// tests/artist_review.mjs — the Artist review button on the public site (task
|
|
2
|
+
// 1004499, goal 1000095 WA6). Owner decisions 2026-10-04: the public pages plus
|
|
3
|
+
// the Studio; any signed-in builder sees it; it sits in the hub bar; the
|
|
4
|
+
// reviews are listed in the Studio.
|
|
5
|
+
//
|
|
6
|
+
// Four altitudes: the pure rules (a site doc is read in the Studio), the SQL,
|
|
7
|
+
// the route over a stubbed db through real HTTP, and the browser half — the
|
|
8
|
+
// REAL artist-review.js drawing the button (or nothing) and loading the REAL
|
|
9
|
+
// page-notes.js, whose page id and idea links follow the public surface.
|
|
10
|
+
import { test } from 'node:test';
|
|
11
|
+
import { strict as assert } from 'node:assert';
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
import http from 'node:http';
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import vm from 'node:vm';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
21
|
+
const N = require('../modules/page-notes/notes.js');
|
|
22
|
+
const LOADER = fs.readFileSync(path.join(ROOT, 'modules/public-landing/public/assets/artist-review.js'), 'utf8');
|
|
23
|
+
const NOTES = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/page-notes.js'), 'utf8');
|
|
24
|
+
const realListForSurface = require('../modules/page-notes/db.js').listForSurface;
|
|
25
|
+
const PAGES = ['index', 'projects', 'account', 'privacy', 'terms', 'contact', 'guild', 'invitations'];
|
|
26
|
+
|
|
27
|
+
// ---------- pure ----------
|
|
28
|
+
|
|
29
|
+
test('a site doc is read in the Studio; a hall doc still on its own page', () => {
|
|
30
|
+
assert.equal(N.noteLink({ id: '5', page_id: 'site:projects', page_path: '/projects' }), '/builders/studio?note=5');
|
|
31
|
+
assert.equal(N.noteLink({ id: '6', page_id: 'builders:studio', page_path: '/studio' }), '/studio?note=6');
|
|
32
|
+
assert.equal(N.isSiteNote({ page_id: 'site:home' }), true);
|
|
33
|
+
assert.equal(N.isSiteNote({ page_id: 'builders:site' }), false, 'the surface, not a page named site');
|
|
34
|
+
assert.equal(N.normalizeDraft({ page_id: 'site:home', page_path: '/' }).ok, true, 'the site surface is a valid page id');
|
|
35
|
+
const body = N.ideaBody({ body: 'The hero is too dark.' }, { id: '5', page_id: 'site:home', page_title: 'Home' }, 'Ana');
|
|
36
|
+
assert.match(body, /Full reference doc: \[Home feedback\]\(\/builders\/studio\?note=5\)$/, 'every idea it becomes links into the hall');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('the Studio list row: counts the ideas, carries the link, never the body', () => {
|
|
40
|
+
const r = N.siteListItem({ id: 9, page_id: 'site:terms', page_path: '/terms', page_title: 'Terms', status: 'filed', submitted_at: 't', idea_ids: [1, 2, 3], author_name: 'Bo', excerpt: 'x', body_md: 'SECRET' });
|
|
41
|
+
assert.deepEqual(r, { id: '9', page_id: 'site:terms', page_path: '/terms', page_title: 'Terms', status: 'filed', submitted_at: 't', idea_count: 3, author_name: 'Bo', excerpt: 'x', link: '/builders/studio?note=9' });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// ---------- SQL ----------
|
|
45
|
+
|
|
46
|
+
test('listForSurface reads submitted docs on that surface only, newest first', async () => {
|
|
47
|
+
let seen = null;
|
|
48
|
+
await realListForSurface('site', { limit: 20 }, async (text, params) => { seen = { text, params }; return { rows: [] }; });
|
|
49
|
+
assert.deepEqual(seen.params, ['site:%', 20]);
|
|
50
|
+
assert.match(seen.text, /n\.page_id LIKE \$1 AND n\.status <> 'draft'/);
|
|
51
|
+
assert.match(seen.text, /ORDER BY n\.submitted_at DESC/);
|
|
52
|
+
assert.doesNotMatch(seen.text, /\bn\.body_md\b(?!,)/, 'only an excerpt leaves the table');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// ---------- the route ----------
|
|
56
|
+
|
|
57
|
+
const db = require('../modules/page-notes/db.js');
|
|
58
|
+
const api = require('../src/module-api.js');
|
|
59
|
+
let viewer = { id: 9, rank: 'thetes' };
|
|
60
|
+
let asked = null;
|
|
61
|
+
db.listForSurface = async (surface, opts) => {
|
|
62
|
+
asked = { surface, opts };
|
|
63
|
+
return [{ id: 41, page_id: 'site:projects', page_path: '/projects', page_title: 'Projects', status: 'filed', submitted_at: '2026-10-04', idea_ids: [7], author_name: 'Ana', excerpt: 'Labels overlap.' }];
|
|
64
|
+
};
|
|
65
|
+
db.getNote = async () => null;
|
|
66
|
+
api.requireBuilder = (req, res, next) => { if (!viewer) return res.status(401).json({ error: { code: 'unauthenticated' } }); req.builder = viewer; next(); };
|
|
67
|
+
const express = require('express');
|
|
68
|
+
const app = express();
|
|
69
|
+
app.use((req, res, next) => { res.fail = (code, s) => res.status(typeof s === 'number' ? s : 400).json({ error: { code } }); next(); });
|
|
70
|
+
app.use(express.json());
|
|
71
|
+
app.use('/api/bongos', require('../modules/page-notes/routes/page-notes.js')());
|
|
72
|
+
const server = app.listen(0);
|
|
73
|
+
await new Promise((r) => server.once('listening', r));
|
|
74
|
+
test.after(() => server.close());
|
|
75
|
+
const get = (p) => new Promise((resolve, reject) => {
|
|
76
|
+
http.get(`http://127.0.0.1:${server.address().port}/api/bongos${p}`, (res) => {
|
|
77
|
+
let s = '';
|
|
78
|
+
res.on('data', (c) => { s += c; });
|
|
79
|
+
res.on('end', () => resolve({ status: res.statusCode, body: JSON.parse(s || 'null'), headers: res.headers }));
|
|
80
|
+
}).on('error', reject);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('GET /page-notes/site: any signed-in builder reads the list; a visitor nothing', async () => {
|
|
84
|
+
viewer = { id: 9, rank: 'thetes' };
|
|
85
|
+
const r = await get('/page-notes/site');
|
|
86
|
+
assert.equal(r.status, 200, 'answered by the list, never read as doc id "site"');
|
|
87
|
+
assert.deepEqual(asked, { surface: 'site', opts: { limit: 20 } });
|
|
88
|
+
assert.equal(r.headers['cache-control'], 'no-store');
|
|
89
|
+
assert.deepEqual(r.body.notes.map((n) => [n.id, n.idea_count, n.link]), [['41', 1, '/builders/studio?note=41']]);
|
|
90
|
+
viewer = null;
|
|
91
|
+
assert.equal((await get('/page-notes/site')).status, 401);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ---------- the browser half ----------
|
|
95
|
+
|
|
96
|
+
function el(tag, id, cls) {
|
|
97
|
+
const attrs = {};
|
|
98
|
+
const listeners = {};
|
|
99
|
+
const e = {
|
|
100
|
+
tagName: tag, id: id || '', className: cls || '', children: [], parentNode: null, innerHTML: '', title: '', type: '', src: '', href: '', rel: '',
|
|
101
|
+
dataset: {},
|
|
102
|
+
setAttribute(k, v) { attrs[k] = String(v); if (k.startsWith('data-')) e.dataset[k.slice(5).replace(/-(\w)/g, (_m, c) => c.toUpperCase())] = String(v); },
|
|
103
|
+
getAttribute(k) { return k in attrs ? attrs[k] : null; },
|
|
104
|
+
appendChild(c) { c.parentNode = e; e.children.push(c); if (e.onAppend) e.onAppend(c); return c; },
|
|
105
|
+
insertBefore(c, ref) { c.parentNode = e; const at = e.children.indexOf(ref); if (at === -1) e.children.push(c); else e.children.splice(at, 0, c); return c; },
|
|
106
|
+
addEventListener(t, fn) { (listeners[t] ||= []).push(fn); },
|
|
107
|
+
click() { (listeners.click || []).forEach((fn) => fn({ target: e })); },
|
|
108
|
+
};
|
|
109
|
+
return e;
|
|
110
|
+
}
|
|
111
|
+
const tree = (r) => r.children.flatMap((c) => [c, ...tree(c)]);
|
|
112
|
+
const flush = async () => { for (let i = 0; i < 6; i++) await new Promise((r) => setTimeout(r, 0)); };
|
|
113
|
+
|
|
114
|
+
function bootSite({ me = { builder: { id: 9, github_login: 'ana' } }, search = '', pathname = '/projects', hall = 'https://builders.example.com' } = {}) {
|
|
115
|
+
const body = el('BODY');
|
|
116
|
+
const head = el('HEAD');
|
|
117
|
+
const bar = body.appendChild(el('DIV', '', 'barRight'));
|
|
118
|
+
bar.appendChild(el('BUTTON', 'projTrig', 'trig'));
|
|
119
|
+
const who = bar.appendChild(el('BUTTON', 'who', 'trig who'));
|
|
120
|
+
const script = el('SCRIPT');
|
|
121
|
+
script.setAttribute('data-hall', hall);
|
|
122
|
+
const fetched = [];
|
|
123
|
+
const loaded = [];
|
|
124
|
+
const opened = [];
|
|
125
|
+
const configured = [];
|
|
126
|
+
const all = () => [head, body, ...tree(head), ...tree(body)];
|
|
127
|
+
const document = {
|
|
128
|
+
body, head, currentScript: script, title: 'Projects — Example',
|
|
129
|
+
getElementById: (id) => all().find((n) => n.id === id) || null,
|
|
130
|
+
querySelector: (sel) => (sel === '.barRight' ? bar : null),
|
|
131
|
+
createElement: (t) => el(t.toUpperCase()),
|
|
132
|
+
createElementNS: (_ns, t) => el(t),
|
|
133
|
+
};
|
|
134
|
+
const window = { location: { search, pathname }, document };
|
|
135
|
+
head.onAppend = (tag) => {
|
|
136
|
+
if (tag.tagName !== 'SCRIPT') return;
|
|
137
|
+
loaded.push(tag.src);
|
|
138
|
+
window.OTBPageNotes = { configure: (c) => configured.push(JSON.parse(JSON.stringify(c))), open: () => opened.push('open'), openNote: (id) => opened.push('note ' + id) };
|
|
139
|
+
tag.onload();
|
|
140
|
+
};
|
|
141
|
+
const fetch = async (url, opts) => {
|
|
142
|
+
fetched.push([url, opts.credentials]);
|
|
143
|
+
return me ? { ok: true, json: async () => me } : { ok: false, status: 401, json: async () => ({}) };
|
|
144
|
+
};
|
|
145
|
+
const sandbox = { window, document, fetch, Promise, setTimeout };
|
|
146
|
+
vm.createContext(sandbox);
|
|
147
|
+
vm.runInContext(LOADER, sandbox, { filename: 'artist-review.js' });
|
|
148
|
+
return { window, document, body, head, bar, who, fetched, loaded, opened, configured, sandbox };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
test('SIGNED OUT: no button, no sheet, no panel — the page is untouched', async () => {
|
|
152
|
+
const s = bootSite({ me: null });
|
|
153
|
+
await flush();
|
|
154
|
+
assert.deepEqual(s.fetched, [['/api/bongos/me', 'same-origin']]);
|
|
155
|
+
assert.equal(s.document.getElementById('arv-btn'), null);
|
|
156
|
+
assert.equal(s.head.children.length, 0);
|
|
157
|
+
assert.deepEqual(s.configured, [], 'the panel is never told anything');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('SIGNED IN: the Review button sits in the hub bar right before the identity chip, and opens the hall\'s panel', async () => {
|
|
161
|
+
const s = bootSite();
|
|
162
|
+
await flush();
|
|
163
|
+
const b = s.document.getElementById('arv-btn');
|
|
164
|
+
assert.ok(b, 'drawn');
|
|
165
|
+
assert.equal(b.parentNode, s.bar);
|
|
166
|
+
assert.equal(s.bar.children[s.bar.children.indexOf(b) + 1], s.who, 'beside the identity chip');
|
|
167
|
+
assert.equal(b.getAttribute('aria-label'), 'Artist review: write feedback on this page', 'the same name as the hall\'s palette');
|
|
168
|
+
assert.equal(b.innerHTML, '', 'built as nodes: this surface writes no innerHTML');
|
|
169
|
+
assert.equal(b.children[0].tagName, 'svg');
|
|
170
|
+
assert.deepEqual([b.children[1].className, b.children[1].textContent], ['arv-btn__label', 'Review']);
|
|
171
|
+
assert.doesNotMatch(LOADER, /\.innerHTML\s*=/);
|
|
172
|
+
assert.deepEqual(s.head.children.map((t) => t.href), ['/modules/hall-ui/public/page-notes.css'], 'the panel\'s own sheet, served from the hall module');
|
|
173
|
+
assert.deepEqual(s.loaded, [], 'the panel is not loaded until it is wanted');
|
|
174
|
+
b.click(); await flush();
|
|
175
|
+
b.click(); await flush();
|
|
176
|
+
assert.deepEqual(s.loaded, ['/modules/hall-ui/public/page-notes.js'], 'loaded once');
|
|
177
|
+
assert.deepEqual(s.opened, ['open', 'open']);
|
|
178
|
+
assert.deepEqual(s.configured[0], { surface: 'site', hall: 'https://builders.example.com' }, 'the panel is told, explicitly, it is on the public site and where the hall is');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('a ?note=N link opens that doc in place; a hall origin that is not one is dropped', async () => {
|
|
182
|
+
const s = bootSite({ search: '?note=41', hall: 'javascript:alert(1)' });
|
|
183
|
+
await flush();
|
|
184
|
+
assert.deepEqual(s.opened, ['note 41']);
|
|
185
|
+
assert.equal(s.body.dataset.pnHall, undefined);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('the panel names a public page as site:<first path segment>, and its ideas open in the hall', async () => {
|
|
189
|
+
const run = (pathname, hall = 'https://builders.example.com') => {
|
|
190
|
+
const s = bootSite({ pathname, hall });
|
|
191
|
+
return flush().then(() => {
|
|
192
|
+
vm.runInContext(NOTES, s.sandbox, { filename: 'page-notes.js' });
|
|
193
|
+
s.window.OTBPageNotes.configure({ surface: 'site', hall });
|
|
194
|
+
return s;
|
|
195
|
+
});
|
|
196
|
+
};
|
|
197
|
+
for (const [p, id] of [['/', 'site:home'], ['/projects', 'site:projects'], ['/account', 'site:account'], ['/privacy.html', 'site:privacy']]) {
|
|
198
|
+
const s = await run(p);
|
|
199
|
+
assert.equal(s.window.OTBPageNotes.page().id, id, p);
|
|
200
|
+
assert.equal(N.PAGE_ID_RE.test(id), true, 'the server takes it');
|
|
201
|
+
}
|
|
202
|
+
const s = await run('/projects');
|
|
203
|
+
s.document.title = 'Example — projects';
|
|
204
|
+
assert.equal(s.window.OTBPageNotes.page().title, 'Projects', 'named by its path, not the product name the title leads with');
|
|
205
|
+
assert.equal((await run('/')).window.OTBPageNotes.page().title, 'Home');
|
|
206
|
+
assert.match(NOTES, /esc\(ideaHref\(i\.id\)\)/, 'idea links go through the surface-aware href');
|
|
207
|
+
assert.equal(s.window.OTBPageNotes.ideaHref('7'), 'https://builders.example.com/builders/idea/7', 'an idea opens in the hall');
|
|
208
|
+
const bad = await run('/projects', 'javascript:alert(1)');
|
|
209
|
+
assert.equal(bad.window.OTBPageNotes.ideaHref('7'), '/builders/idea/7', 'a hall origin that is not one is dropped');
|
|
210
|
+
s.window.OTBPageNotes.configure({});
|
|
211
|
+
assert.equal(s.window.OTBPageNotes.page().id, 'builders:home', 'unconfigured, it is the hall panel it always was');
|
|
212
|
+
assert.equal(s.window.OTBPageNotes.ideaHref('7'), '/#/idea/7');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('the public button and the hall palette wear one name', () => {
|
|
216
|
+
const craft = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/craft-mode.js'), 'utf8');
|
|
217
|
+
const tool = /id: 'palette', icon: 'artistry', short: '([^']+)', label: '([^']+)'/.exec(craft);
|
|
218
|
+
assert.ok(tool, 'the palette tool');
|
|
219
|
+
assert.ok(LOADER.includes("var SHORT = '" + tool[1] + "';"), 'the short name');
|
|
220
|
+
assert.ok(LOADER.includes("var LABEL = '" + tool[2] + "';"), 'the full name');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('every public page carries the loader last, with the hall origin stamped server-side', () => {
|
|
224
|
+
for (const f of PAGES) {
|
|
225
|
+
const html = fs.readFileSync(path.join(ROOT, 'modules/public-landing/public', f + '.html'), 'utf8').replace(/\r\n/g, '\n');
|
|
226
|
+
assert.match(html, /<script src="\/assets\/artist-review\.js" data-hall="\{\{buildersOrigin\}\}"><\/script>\n<\/body>/, f);
|
|
227
|
+
}
|
|
228
|
+
const serve = fs.readFileSync(path.join(ROOT, 'src/bongos/serve-internal.js'), 'utf8');
|
|
229
|
+
assert.match(serve, /\.split\('\{\{buildersOrigin\}\}'\)/, 'the token the tag carries is one the server fills');
|
|
230
|
+
assert.match(serve, /const MODULE_ASSET_RE = \/\^\\\/modules\\\/\(\[a-z\]\[a-z0-9-\]\*\)\\\/public\\\/\(\.\+\)\$\//, 'the panel is served at /modules/hall-ui/public/');
|
|
231
|
+
});
|