@bongos/core 1.24.49 → 1.24.50

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/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.24.49",
3
+ "version": "1.24.50",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.24.49",
9
+ "version": "1.24.50",
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.24.49",
3
+ "version": "1.24.50",
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",
@@ -11265,5 +11265,11 @@
11265
11265
  "id": "1005088",
11266
11266
  "text": "The Cloud Bongos logo and the browser-tab icon now come from one file, so every page, the hall, the welcome screen, the gates and the fundraising page show exactly the same mark, and phones get a proper home-screen icon."
11267
11267
  }
11268
+ ],
11269
+ "1.24.50": [
11270
+ {
11271
+ "id": "1005089",
11272
+ "text": "The public status page now looks like the rest of Cloud Bongos: same colours, fonts, logo and panels, with a dark mode, and its off-site backup copy is built from the same source so the two always match."
11273
+ }
11268
11274
  ]
11269
11275
  }
@@ -23,15 +23,19 @@
23
23
  // mirror, and the publish REFUSES rather than shipping a
24
24
  // page whose banner points nowhere.
25
25
  //
26
- // DESIGN: status.js + style.css + cursors/ are byte-identical between the
27
- // droplet copy and the mirror (#737 parameterized the one differing constant via
28
- // window.STATUS_API_BASE), so they are PURE COPIES. Only index.html carries the
29
- // three off-box additions, injected here at stable anchors:
26
+ // DESIGN: status.js + style.css + the shared sheets and brand files the page
27
+ // links (SHARED_ASSETS, at the same paths) are byte-identical between the server
28
+ // copy and the mirror (#737 parameterized the one differing constant via
29
+ // window.STATUS_API_BASE), so they are PURE COPIES. index.html is built from the
30
+ // SAME functions the server serves it with (task 1005089): the server's brand-token
31
+ // stamp (applyBrandTokens) and its theme splice (themeMarkup — the pack's palette,
32
+ // faces, font links and favicon), so the mirror cannot drift from the live page.
33
+ // On top of that it carries the three off-box additions, injected at stable anchors:
30
34
  // 1. <script>window.STATUS_API_BASE = <domains.publicOrigin></script> (head)
31
35
  // 2. <div id="offbox-status">…</div> (after <body>)
32
36
  // 3. the availability-banner driver <script> (reads domains.statusDataUrl) (before status.js)
33
37
  // If any anchor is missing the script THROWS — a silent mis-sync is worse than
34
- // a failed run.
38
+ // a failed run. tests/status_mirror_sync.mjs holds the mirror equal to the live page.
35
39
  //
36
40
  // USAGE:
37
41
  // node scripts/status-mirror-sync.js --repo-dir <path-to-mirror-checkout> [--push]
@@ -48,35 +52,25 @@ const { execFileSync } = require('node:child_process');
48
52
 
49
53
  const REPO_ROOT = path.resolve(__dirname, '..');
50
54
  const SRC = path.join(REPO_ROOT, 'modules', 'status-ui', 'public');
51
- const { branding, clientBranding, applyBrandConditionals } = require('../src/branding');
52
- const { escapeHtml } = require('../src/html-escape');
53
-
54
- // Stamp instance-identity tokens into the mirror HTML. The off-box mirror is
55
- // served by a static Pages host, NOT the branding-injecting Node server — so this
56
- // script must reproduce serve-internal.applyBrandTokens (task 1705, ADR 0062 §3), else
57
- // the mirror would render literal {{worldName}}/{{currency}} placeholders. KEEP IN SYNC
58
- // with src/bongos/serve-internal.js applyBrandTokens. `brand` is the client-safe pack
59
- // and `publicOrigin` the resolved origin — both injected, never read ambiently, so the
60
- // transform stays pure and testable.
55
+ const { branding, clientBranding } = require('../src/branding');
56
+ const { stampHtml } = require('../src/asset-version');
57
+ // The server's own serving transforms: the mirror is built with them, not with a
58
+ // copy of them, so the off-box page and the live page are one page (task 1005089).
59
+ const { applyBrandTokens, themeMarkup } = require('../src/bongos/serve-internal');
60
+
61
+ // The shared files the page links from other modules, published at the SAME paths
62
+ // so the page's root-absolute hrefs resolve on the mirror's host as on the server.
63
+ const SHARED_ASSETS = [
64
+ 'modules/ui-design/public/tokens.css',
65
+ 'modules/hall-ui/public/controls.css',
66
+ 'modules/ui-design/public/brand',
67
+ ];
68
+
69
+ // Stamp instance-identity tokens into the mirror HTML: the server's applyBrandTokens
70
+ // (task 1705, ADR 0062 §3), with every origin INJECTED ('' when absent) so nothing is
71
+ // read ambiently and the transform stays pure and testable.
61
72
  function brandTokens(html, brand = {}, publicOrigin = '', buildersOrigin = '') {
62
- const world = (brand.identity && brand.identity.worldName) || '';
63
- const product = (brand.identity && brand.identity.productName) || '';
64
- const tagline = (brand.copy && brand.copy.tagline) || '';
65
- const cur = (brand.currency && brand.currency.label) || 'credits';
66
- const Cur = cur ? cur.charAt(0).toUpperCase() + cur.slice(1) : cur;
67
- const company = (brand.identity && brand.identity.company) || {};
68
- const esc = escapeHtml; // the one server escaper (task 1004876)
69
- return applyBrandConditionals(html, { companyName: company.name, companySite: company.site, companyTagline: company.tagline })
70
- .split('{{worldName}}').join(esc(world))
71
- .split('{{productName}}').join(esc(product))
72
- .split('{{Currency}}').join(esc(Cur))
73
- .split('{{currency}}').join(esc(cur))
74
- .split('{{tagline}}').join(esc(tagline))
75
- .split('{{companyName}}').join(esc(company.name))
76
- .split('{{companySite}}').join(esc(company.site))
77
- .split('{{companyTagline}}').join(esc(company.tagline))
78
- .split('{{publicOrigin}}').join(esc(publicOrigin || ''))
79
- .split('{{buildersOrigin}}').join(esc(buildersOrigin || publicOrigin || ''));
73
+ return applyBrandTokens(html, brand || {}, publicOrigin || '', buildersOrigin || publicOrigin || '', '', '', '');
80
74
  }
81
75
 
82
76
  // ---- the three off-box index.html injections -------------------------------
@@ -152,7 +146,9 @@ function buildMirrorIndex({ srcHtml, apiBase, statusDataUrl, brand }) {
152
146
  if (html.includes('offbox-status')) {
153
147
  throw new Error('modules/status-ui/public/index.html already contains offbox markup — the source should be the droplet (no-banner) copy. Aborting.');
154
148
  }
155
- html = injectOnce(html, '</head>', headInject(apiBase) + brandingInject(brand), 'before');
149
+ // The off-box globals, then the pack's theme exactly as the server splices it —
150
+ // last in <head>, after the page's own sheets, so the pack wins here as it does live.
151
+ html = injectOnce(html, '</head>', headInject(apiBase) + brandingInject(brand) + themeMarkup((brand || {}).theme), 'before');
156
152
  html = injectOnce(html, '<body>', BANNER_DIV, 'after');
157
153
  // Insert the driver right before the main status.js script tag.
158
154
  const scriptAnchor = '<script src="/status.js';
@@ -188,17 +184,11 @@ if (require.main === module) {
188
184
  fs.copyFileSync(path.join(SRC, rel), path.join(repoDir, rel));
189
185
  console.error(` copied ${rel}`);
190
186
  };
191
- const copyDir = (rel) => {
192
- const from = path.join(SRC, rel), to = path.join(repoDir, rel);
193
- if (!fs.existsSync(from)) return;
194
- fs.rmSync(to, { recursive: true, force: true });
195
- fs.cpSync(from, to, { recursive: true });
196
- console.error(` copied ${rel}/`);
197
- };
198
187
 
199
188
  console.error('[status-mirror-sync] building mirror from modules/status-ui/public/ …');
200
189
  const indexHtml = buildMirrorIndex({
201
- srcHtml: fs.readFileSync(path.join(SRC, 'index.html'), 'utf8'),
190
+ // the content-hash ?v= stamp the server serves the index with (#521)
191
+ srcHtml: stampHtml(fs.readFileSync(path.join(SRC, 'index.html'), 'utf8'), { assetDir: SRC, urlPrefix: '/' }),
202
192
  apiBase: b.domains.publicOrigin,
203
193
  statusDataUrl: b.domains.statusDataUrl,
204
194
  brand: clientBranding(),
@@ -207,12 +197,21 @@ if (require.main === module) {
207
197
  console.error(' wrote index.html (with off-box injections)');
208
198
  copyFile('status.js');
209
199
  copyFile('style.css');
210
- copyDir('cursors');
200
+ // the retired bronze cursors (task 1005089) leave the mirror too
201
+ fs.rmSync(path.join(repoDir, 'cursors'), { recursive: true, force: true });
202
+ for (const rel of SHARED_ASSETS) {
203
+ const from = path.join(REPO_ROOT, rel), to = path.join(repoDir, rel);
204
+ fs.rmSync(to, { recursive: true, force: true });
205
+ fs.mkdirSync(path.dirname(to), { recursive: true });
206
+ fs.cpSync(from, to, { recursive: true });
207
+ console.error(` copied ${rel}`);
208
+ }
211
209
 
212
210
  // ---- commit + (optionally) push -------------------------------------------
213
211
 
214
212
  const git = (args) => execFileSync('git', ['-C', repoDir, ...args], { encoding: 'utf8' });
215
- git(['add', 'index.html', 'status.js', 'style.css', 'cursors']);
213
+ git(['add', '-A', '--', 'index.html', 'status.js', 'style.css', 'modules']);
214
+ git(['rm', '-r', '-q', '--cached', '--ignore-unmatch', 'cursors']);
216
215
  const status = git(['status', '--porcelain']);
217
216
  if (!status.trim()) {
218
217
  console.error('[status-mirror-sync] mirror already up to date — nothing to publish.');
@@ -228,4 +227,4 @@ if (require.main === module) {
228
227
  console.error(`[status-mirror-sync] published. The Pages host rebuilds ${b.domains.statusOrigin || 'the mirror'} in ~1 min.`);
229
228
  }
230
229
 
231
- module.exports = { buildMirrorIndex, injectOnce, brandTokens };
230
+ module.exports = { buildMirrorIndex, injectOnce, brandTokens, SHARED_ASSETS };
@@ -835,15 +835,16 @@ function mountInternalSurfaces(app) {
835
835
  const surface = surfaceOf(req);
836
836
  if (!surface) return next();
837
837
  // /api/* already handled above; everything else on a surface host serves from
838
- // the module's static dir. The index page comes from the stamped in-memory copy
839
- // (asset ?v= = content hash); everything else falls through to the on-disk
840
- // static handler.
841
- if ((req.method === 'GET' || req.method === 'HEAD') &&
842
- (req.path === '/' || req.path === '/index.html') &&
843
- surface.html['index.html']) {
838
+ // the module's static dir. Each page ("/" is the index, "/explore[.html]" its
839
+ // sibling; task 1005089) comes from the stamped + branded in-memory copy (asset
840
+ // ?v= = content hash); everything else falls through to the on-disk static handler.
841
+ // The map's keys are fixed at boot (the dir's own .html files), so a slug can only name a shipped page.
842
+ const pm = /^\/(?:([a-z0-9-]+)(?:\.html)?)?$/i.exec(req.path);
843
+ const page = pm && `${pm[1] || 'index'}.html`;
844
+ if ((req.method === 'GET' || req.method === 'HEAD') && page && surface.html[page]) {
844
845
  res.set('Cache-Control', 'public, max-age=60');
845
846
  res.type('html');
846
- return res.send(surface.html['index.html']);
847
+ return res.send(surface.html[page]);
847
848
  }
848
849
  return surface.static(req, res, next);
849
850
  });
package/src/module-api.js CHANGED
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
81
81
  // MAJOR (see allowBoxScope below): passes the request through untouched.
82
82
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
83
83
 
84
- const CORE_VERSION = '1.24.49'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
84
+ const CORE_VERSION = '1.24.50'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
85
85
 
86
86
  // A namespaced logger so a module's log lines are attributable + consistent.
87
87
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -0,0 +1,195 @@
1
+ // tests/status_page_chrome.mjs — the status page on the chrome world (task 1005089,
2
+ // goal 1000138; DESIGN.md "Naming": the owner moved the status page onto this world,
3
+ // keeping its fixed status vocabulary as the hall does).
4
+ //
5
+ // What this pins, DB-free and browser-free:
6
+ // * nothing of the retired Greek ledger look survives in the surface: no parchment
7
+ // colours, no Cinzel / EB Garamond, no bronze cursors, no amphora or laurel
8
+ // sprite, no double-ruled frame;
9
+ // * every page title carries the product name from the branding pack;
10
+ // * the sheet declares no page :root, writes its dark tier twice byte-identical,
11
+ // and carries the hall's status vocabulary value for value;
12
+ // * explore is served through the branding injection, so it loads the pack's faces;
13
+ // * the off-box mirror IS the live page: the server's response for "/" on the
14
+ // status host, and the mirror's index built from the same source, are equal once
15
+ // each side's own injections are taken out — and every file the page links is one
16
+ // the mirror publishes.
17
+ //
18
+ // Run: node --test tests/status_page_chrome.mjs
19
+
20
+ import { test } from 'node:test';
21
+ import assert from 'node:assert/strict';
22
+ import fs from 'node:fs';
23
+ import http from 'node:http';
24
+ import path from 'node:path';
25
+ import { createRequire } from 'node:module';
26
+ import { fileURLToPath } from 'node:url';
27
+
28
+ const require = createRequire(import.meta.url);
29
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
30
+ const DIR = path.join(ROOT, 'modules', 'status-ui', 'public');
31
+ const read = (...p) => fs.readFileSync(path.join(ROOT, ...p), 'utf8').replace(/\r\n/g, '\n');
32
+ const decomment = (s) => s.replace(/\/\*[\s\S]*?\*\//g, '');
33
+ const CSS = read('modules', 'status-ui', 'public', 'style.css');
34
+ const CODE = decomment(CSS);
35
+
36
+ function surfaceFiles(dir = DIR, out = []) {
37
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
38
+ const p = path.join(dir, e.name);
39
+ if (e.isDirectory()) surfaceFiles(p, out);
40
+ else if (/\.(html|css|js)$/.test(e.name)) out.push(p);
41
+ }
42
+ return out;
43
+ }
44
+
45
+ // The retired look, as literals. The parchment hexes are the old sheet's :root.
46
+ const RETIRED = [
47
+ /Cinzel/i, /Garamond/i, /cursors\//, /hand-pointer\.png/, /sigil-amphora/, /orn-(laurel|column|coin|scroll|map|diamond)/,
48
+ /#ece0c2|#f6ecd2|#ddc99c|#e3d0a4|#2a2110|#6e5b3a|#735f3d|#8b6f2c|#b89a55|#4a7d5c|#2f5a3f|#2d6e7e|#a85530/i,
49
+ /rgba\(\s*(139, 111, 44|60, 40, 10|42, 33, 16|243, 230, 198)/,
50
+ /upper-roman/,
51
+ ];
52
+
53
+ test('nothing of the Greek ledger look is left in the status surface', () => {
54
+ const hits = [];
55
+ for (const f of surfaceFiles()) {
56
+ const text = fs.readFileSync(f, 'utf8');
57
+ for (const re of RETIRED) if (re.test(text)) hits.push(`${path.relative(ROOT, f)}: ${re}`);
58
+ }
59
+ assert.deepEqual(hits, []);
60
+ assert.equal(fs.existsSync(path.join(DIR, 'cursors')), false, 'the bronze cursors are gone');
61
+ // no double-ruled frame: a panel is one hairline, no inset rule, no shadow
62
+ const panel = CODE.match(/\n\.tablet \{([^}]*)\}/);
63
+ assert.ok(panel, 'the panel rule exists');
64
+ assert.match(panel[1], /border: var\(--line\) solid var\(--rule-soft\);/);
65
+ assert.match(panel[1], /border-radius: var\(--ds-radius-panel\);/);
66
+ assert.doesNotMatch(panel[1], /box-shadow/);
67
+ assert.doesNotMatch(CODE, /\.tablet::(before|after)/, 'no carved corner ornaments');
68
+ });
69
+
70
+ test('every page title carries the product name from the pack', () => {
71
+ const pages = fs.readdirSync(DIR).filter((f) => f.endsWith('.html'));
72
+ assert.ok(pages.length >= 2);
73
+ for (const p of pages) {
74
+ const title = fs.readFileSync(path.join(DIR, p), 'utf8').match(/<title>([^<]*)<\/title>/);
75
+ assert.ok(title, `${p} has a title`);
76
+ assert.match(title[1], /\{\{productName\}\}/, `${p}: the title names the product`);
77
+ }
78
+ });
79
+
80
+ test('the page draws the shared mark and favicon, and names no face of its own', () => {
81
+ const html = read('modules', 'status-ui', 'public', 'index.html');
82
+ assert.ok(html.includes('<use href="/modules/ui-design/public/brand/mark.svg#mark"/>'), 'the top bar draws the one mark by reference');
83
+ assert.ok(html.includes('<link rel="icon" href="/modules/ui-design/public/brand/favicon.svg"'));
84
+ assert.doesNotMatch(html, /fonts\.googleapis\.com/, 'the faces come from the pack splice, not the page');
85
+ assert.ok(html.indexOf('/modules/hall-ui/public/controls.css') > html.indexOf('/modules/ui-design/public/tokens.css'), 'the shared controls follow the scales');
86
+ assert.ok(html.indexOf('/modules/hall-ui/public/controls.css') < html.indexOf('href="/style.css'), 'and precede the page sheet');
87
+ // exactly one header (the top bar), one main, one h1
88
+ assert.equal((html.match(/<header\b/g) || []).length, 1);
89
+ assert.equal((html.match(/<main\b/g) || []).length, 1);
90
+ assert.equal((html.match(/<h1\b/g) || []).length, 1);
91
+ });
92
+
93
+ test('the sheet declares no page :root, and its dark tier is written twice, byte-identical', () => {
94
+ assert.doesNotMatch(CODE, /(^|[\s,}]):root\b/, 'no :root — the pack splices the only one');
95
+ assert.match(CODE, /:where\(html\) \{/, 'the fallback contract tier sits at zero specificity');
96
+ const switchBody = CODE.match(/\nhtml\[data-theme="dark"\] \{([^}]*)\}/);
97
+ const mediaBody = CODE.match(/@media \(prefers-color-scheme: dark\) \{\s*html:not\(\[data-theme\]\) \{([^}]*)\}/);
98
+ assert.ok(switchBody && mediaBody, 'both dark twins exist');
99
+ const norm = (s) => s.split('\n').map((l) => l.trim()).filter(Boolean).join('\n');
100
+ assert.equal(norm(mediaBody[1]), norm(switchBody[1]));
101
+ // the dark tier re-pins no accent (the Unpinned Accent Rule)
102
+ assert.doesNotMatch(switchBody[1], /--(accent|accent-deep|sea|terracotta):/);
103
+ });
104
+
105
+ test('the status vocabulary is the hall\'s, value for value, in both modes', () => {
106
+ const hall = decomment(read('modules', 'hall-ui', 'public', 'style.css'));
107
+ const NAMES = ['--ok', '--ok-weak', '--warn', '--warn-weak', '--danger', '--danger-weak', '--info', '--info-weak'];
108
+ const valuesIn = (body) => Object.fromEntries(NAMES.map((n) => {
109
+ const m = body.match(new RegExp(`\\n\\s*${n}:\\s*([^;]+);`));
110
+ assert.ok(m, `${n} is declared`);
111
+ return [n, m[1].trim()];
112
+ }));
113
+ const lightOf = (src, open) => { const at = src.indexOf(open); assert.ok(at > -1, open); return src.slice(at, src.indexOf('}', at)); };
114
+ assert.deepEqual(valuesIn(lightOf(CODE, '\nhtml {')), valuesIn(lightOf(hall, '\n:root {')));
115
+ assert.deepEqual(valuesIn(lightOf(CODE, '\nhtml[data-theme="dark"] {')), valuesIn(lightOf(hall, '\nhtml[data-theme="dark"] {')));
116
+ });
117
+
118
+ // ── served ────────────────────────────────────────────────────────────────────
119
+ // node:http, not fetch: fetch silently drops a Host override (tests/module_asset_subdomain.mjs).
120
+ function get(port, p, host) {
121
+ return new Promise((resolve, reject) => {
122
+ const req = http.request({ host: '127.0.0.1', port, path: p, headers: { Host: host } }, (res) => {
123
+ let body = '';
124
+ res.setEncoding('utf8');
125
+ res.on('data', (c) => { body += c; });
126
+ res.on('end', () => resolve({ status: res.statusCode, body }));
127
+ });
128
+ req.on('error', reject);
129
+ req.end();
130
+ });
131
+ }
132
+ async function withServer(fn) {
133
+ const express = require('express');
134
+ const { mountInternalSurfaces } = require('../src/bongos/serve-internal.js');
135
+ const app = express();
136
+ mountInternalSurfaces(app);
137
+ const server = await new Promise((resolve) => { const s = app.listen(0, () => resolve(s)); });
138
+ try { return await fn(server.address().port); } finally { await new Promise((r) => server.close(r)); }
139
+ }
140
+
141
+ test('explore is served through the branding injection: the pack\'s faces, the product name', async () => {
142
+ const { clientBranding } = require('../src/branding.js');
143
+ const brand = clientBranding();
144
+ await withServer(async (port) => {
145
+ for (const p of ['/explore', '/explore.html']) {
146
+ const r = await get(port, p, 'status.example.com');
147
+ assert.equal(r.status, 200, p);
148
+ // the href is attribute-escaped on the way in (themeMarkup), so & arrives as &amp;
149
+ for (const href of brand.theme.fonts.links) assert.ok(r.body.includes(`<link rel="stylesheet" href="${href.replace(/&/g, '&amp;')}">`), `${p} loads the pack's faces`);
150
+ assert.ok(r.body.includes(`<title>Explore — projects on the map · ${brand.identity.productName}</title>`), `${p} names the product`);
151
+ assert.doesNotMatch(r.body, /\{\{productName\}\}/);
152
+ }
153
+ // an asset still comes off disk
154
+ assert.equal((await get(port, '/explore.js', 'status.example.com')).status, 200);
155
+ });
156
+ });
157
+
158
+ test('the off-box mirror is the live page, and publishes every file the page links', async () => {
159
+ const mirror = require('../scripts/status-mirror-sync.js');
160
+ const { stampHtml } = require('../src/asset-version.js');
161
+ const { clientBranding, branding } = require('../src/branding.js');
162
+ const brand = clientBranding();
163
+ const src = stampHtml(read('modules', 'status-ui', 'public', 'index.html'), { assetDir: DIR, urlPrefix: '/' });
164
+ const apiBase = branding().domains.publicOrigin || 'https://origin.example.test';
165
+ const built = mirror.buildMirrorIndex({ srcHtml: src, apiBase, statusDataUrl: 'https://data.example.test/status.json', brand });
166
+
167
+ const live = await withServer(async (port) => (await get(port, '/', 'status.example.com')).body);
168
+ assert.ok(live.includes(brand.theme.fonts.links[0].replace(/&/g, '&amp;')), 'the live page splices the pack');
169
+
170
+ // each side minus its own injections: the server's client bundle + globals, and the
171
+ // mirror's three off-box additions + its globals
172
+ const liveCore = live
173
+ .replace(/<script src="\/lib\/bongos-client\.js[^"]*"><\/script>/, '')
174
+ .replace(/<script>window\.__BRANDING__=[\s\S]*?<\/script>/, '');
175
+ const mirrorCore = built
176
+ .replace(/ {2}<!-- #737: off-box mirror runs cross-origin against the droplet\. -->\n {2}<script>window\.STATUS_API_BASE = [^\n]*<\/script>\n {2}<script>window\.__BRANDING__ = [^\n]*<\/script>\n/, '')
177
+ .replace(/\n<!-- Off-box availability banner \(ADR 0029\)[\s\S]*?<div id="offbox-status" role="status" aria-live="polite"><\/div>\n/, '')
178
+ .replace(/<!-- Off-box availability banner driver \(ADR 0029\)[\s\S]*?<\/script>\n/, '');
179
+ // the origin token is the one value the two resolve from different places on a
180
+ // checkout with no domains; normalise it before comparing
181
+ const norm = (s) => s.split(apiBase).join('{{origin}}');
182
+ assert.equal(norm(mirrorCore), norm(liveCore));
183
+
184
+ // every root-absolute file the page links is published: its own files are copied
185
+ // from the surface dir, the shared ones at the same path
186
+ const refs = [...src.matchAll(/\b(?:href|src)="(\/[^"?#]+)/g)].map((m) => m[1]).filter((u) => !u.startsWith('//'));
187
+ assert.ok(refs.length >= 5);
188
+ for (const u of refs) {
189
+ const rel = u.slice(1).replace(/#.*$/, '');
190
+ const own = ['status.js', 'style.css'].includes(rel);
191
+ const shared = mirror.SHARED_ASSETS.some((a) => rel === a || rel.startsWith(`${a}/`));
192
+ assert.ok(own || shared, `the mirror publishes ${u}`);
193
+ assert.ok(fs.existsSync(path.join(own ? DIR : ROOT, rel)), `${u} exists to publish`);
194
+ }
195
+ });