@bongos/core 1.19.103 → 1.19.105

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 CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.103",
6
- "core_contract": "1.19.103",
7
- "source_commit": "4c26152f4ffe43100617e89d904852639f51a9dc",
5
+ "core_version": "1.19.105",
6
+ "core_contract": "1.19.105",
7
+ "source_commit": "e17befb90a1b0010369b049bfe92c597f46feaa1",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-13T23:41:03.246Z",
9
+ "built_at": "2026-08-14T00:47:52.457Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 301,
13
13
  "agent_docs_stubbed": 21,
14
- "functional_verbatim": 1234,
14
+ "functional_verbatim": 1236,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1556,
20
- "tree_sha256": "b559bea5c153dfcb5b4007dee090a97532674449005645ee1953761712bcf02a",
19
+ "file_count": 1558,
20
+ "tree_sha256": "0bd2c06a8df30c32d74aae5a437dc8e71707e2201560e7275d066136aa1f0a63",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -4147,12 +4147,12 @@
4147
4147
  {
4148
4148
  "path": "package-lock.json",
4149
4149
  "mode": "0000644",
4150
- "sha256": "b3854f65fe8ad4aead3d08a87f9f2200d87e2b049aed77f7849d51f9aa6f6e5b"
4150
+ "sha256": "1180a81b8034f5e25bb1da6370b91e69575b653dc6f459e16d20dc6764c38e71"
4151
4151
  },
4152
4152
  {
4153
4153
  "path": "package.json",
4154
4154
  "mode": "0000644",
4155
- "sha256": "7467e0451b4fd162c287c7a5eceee8da746785a3da989bda154830ead5728f3d"
4155
+ "sha256": "ebcbf2f1c4b4479c82525349cb2f2aacf659c80eab6876112947bff44600b718"
4156
4156
  },
4157
4157
  {
4158
4158
  "path": "public-docs/index.html",
@@ -4412,7 +4412,7 @@
4412
4412
  {
4413
4413
  "path": "public-landing/projects.html",
4414
4414
  "mode": "0000644",
4415
- "sha256": "f70211d0d2bfe21fa7000ab82d0adcaa97df1f28763e366965da2b92847b31b6"
4415
+ "sha256": "9e6d99f0a54a6189f7295b799c3061c4b23b69569d15e0dd1e1d2e6e8c8671fc"
4416
4416
  },
4417
4417
  {
4418
4418
  "path": "public-landing/world/assets/clouds/cloud-island-large.png",
@@ -5692,7 +5692,12 @@
5692
5692
  {
5693
5693
  "path": "src/bongos/serve-internal.js",
5694
5694
  "mode": "0000644",
5695
- "sha256": "693ef4dfd3619a2d999e6223de0e2d51b697cf4004c482b74f60b8cfb362a566"
5695
+ "sha256": "c791369591627f89ff1755ea1d343159716b512e64f92286d27547209da88df7"
5696
+ },
5697
+ {
5698
+ "path": "src/bongos/stealth-gate.js",
5699
+ "mode": "0000644",
5700
+ "sha256": "6ea72c90380663fac7ece7d97cd628792f3d68a2969a502c59addfcb83c2bcb4"
5696
5701
  },
5697
5702
  {
5698
5703
  "path": "src/bongos/uptime-poller.js",
@@ -5722,7 +5727,7 @@
5722
5727
  {
5723
5728
  "path": "src/module-api.js",
5724
5729
  "mode": "0000644",
5725
- "sha256": "18be8d4886ea03a65a0cd5d817c811ca0496ea509df5716f416d985a5d682b84"
5730
+ "sha256": "452383073e87173fa0a4f56df5a3a126ab888807dd416df4939ba951a0a0cf43"
5726
5731
  },
5727
5732
  {
5728
5733
  "path": "src/module-loader/catalog.js",
@@ -7247,7 +7252,7 @@
7247
7252
  {
7248
7253
  "path": "tests/projects_hub.mjs",
7249
7254
  "mode": "0000644",
7250
- "sha256": "d8fd9759b99b2136f8b438727b74b15346db7cbd8f0ef8b67aec3049f059df2b"
7255
+ "sha256": "d57dbc4d5b06d2302211d73d1d41c7edf025b38fca433b30df37cf8afc898da9"
7251
7256
  },
7252
7257
  {
7253
7258
  "path": "tests/provision.mjs",
@@ -7689,6 +7694,11 @@
7689
7694
  "mode": "0000644",
7690
7695
  "sha256": "aa1d79bb3f900097a911c8c6b3055debe4bb20bfe2c3d6c8c64cfd8d7688824e"
7691
7696
  },
7697
+ {
7698
+ "path": "tests/stealth_gate.mjs",
7699
+ "mode": "0000644",
7700
+ "sha256": "c4deafe658457e9f3c17a99f6ab2435e1b1c4d42d74d77304ba792458562e771"
7701
+ },
7692
7702
  {
7693
7703
  "path": "tests/stop_refcheck.mjs",
7694
7704
  "mode": "0000644",
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.103",
3
+ "version": "1.19.105",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.103",
9
+ "version": "1.19.105",
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.19.103",
3
+ "version": "1.19.105",
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",
@@ -159,22 +159,6 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
159
159
  </div>
160
160
  <div class="hintbar"><span><kbd>click a project</kbd> open it</span></div>
161
161
  </div>
162
-
163
- <!-- The project card. An OVERLAY inside the map, not a fourth view: opening
164
- a project must not cost you the sky you were reading (direction §9).
165
- Real markup, never JS-built (C7) — the same reason every other view
166
- ships as markup — so its copy is in the document and its ids are stable.
167
- Filled by openCard(); it carries no project identity of its own. -->
168
- <div class="cardScrim" id="cardScrim" hidden>
169
- <div class="projCard" id="projCard" role="dialog" aria-modal="true" aria-labelledby="cardName">
170
- <button type="button" class="cardX" id="cardClose" aria-label="Close">✕</button>
171
- <span class="cardDot" id="cardDot"></span>
172
- <h3 id="cardName"></h3>
173
- <p class="cardTag" id="cardTag"></p>
174
- <p class="cardRel" id="cardRel" hidden></p>
175
- <div class="cardActions" id="cardActions"></div>
176
- </div>
177
- </div>
178
162
  </div>
179
163
 
180
164
  <div class="wrap viewWrap" id="sub-table-body" hidden>
@@ -208,6 +192,30 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
208
192
  </table>
209
193
  <p class="finePrint" style="text-align:center">Click a column to sort.</p>
210
194
  </div>
195
+
196
+ <!-- The project card. An OVERLAY inside the map, not a fourth view: opening
197
+ a project must not cost you the sky you were reading (direction §9).
198
+ Real markup, never JS-built (C7) — the same reason every other view
199
+ ships as markup — so its copy is in the document and its ids are stable.
200
+ Filled by openCard(); it carries no project identity of its own.
201
+
202
+ IT MUST STAY A SIBLING OF THE TWO SUB-VIEWS, never a child of either.
203
+ It shipped inside #sub-sky-body, which the table view sets `hidden` on —
204
+ and `[hidden]{display:none !important}` means every descendant stops
205
+ generating a box, so the card opened at 0x0 in table view and was simply
206
+ never seen. position:fixed does not rescue an element inside a
207
+ display:none ancestor. It worked in sky view, which is the default and
208
+ where it was built, so nothing noticed (task 1002780). -->
209
+ <div class="cardScrim" id="cardScrim" hidden>
210
+ <div class="projCard" id="projCard" role="dialog" aria-modal="true" aria-labelledby="cardName">
211
+ <button type="button" class="cardX" id="cardClose" aria-label="Close">✕</button>
212
+ <span class="cardDot" id="cardDot"></span>
213
+ <h3 id="cardName"></h3>
214
+ <p class="cardTag" id="cardTag"></p>
215
+ <p class="cardRel" id="cardRel" hidden></p>
216
+ <div class="cardActions" id="cardActions"></div>
217
+ </div>
218
+ </div>
211
219
  </section>
212
220
 
213
221
  <!-- ══════════════════ VIEW · YOUR PROJECTS ════════════════════════════════ -->
@@ -45,6 +45,7 @@ const { buildInfo } = require('../build-info');
45
45
  const { buildDevboxDownloads } = require('../devbox-downloads');
46
46
  const { buildBongosDownloads } = require('../bongos-downloads');
47
47
  const { clientBranding, branding } = require('../branding');
48
+ const { stealthGate } = require('./stealth-gate');
48
49
  const { clientModules, isModuleEnabled } = require('../modules');
49
50
  const { hallWidgetScripts, moduleWebSurfaces } = require('../module-loader/loader');
50
51
  const { resolveCoreRoot, resolveDocsRoot, resolveInstanceRoot } = require('../instance-config');
@@ -441,6 +442,16 @@ function mountInternalSurfaces(app) {
441
442
  // game routes too) — it records on res 'finish' regardless of which handler
442
443
  // answers. The /metrics scrape endpoint is access-gated (loopback Prometheus
443
444
  // or Bearer METRICS_TOKEN; see metrics.authorizeScrape).
445
+ // THE STEALTH GATE (task 1002775) mounts before anything it is meant to
446
+ // cover — a surface registered earlier would answer first and the gate would
447
+ // never run. Inert unless STEALTH_PASSWORD is set, so a vanilla instance is
448
+ // unaffected and pays only a closure call per request.
449
+ //
450
+ // It sits ABOVE the metrics middleware deliberately: a refused knock is still
451
+ // a request this instance served, and hiding gate 401s would make the traffic
452
+ // graph lie about how much attention an unannounced instance is getting.
453
+ app.use(stealthGate());
454
+
444
455
  app.use(metrics.httpMiddleware());
445
456
  app.get('/metrics', metrics.metricsHandler());
446
457
 
@@ -0,0 +1,203 @@
1
+ // src/bongos/stealth-gate.js — the pre-launch stealth gate (task 1002775).
2
+ //
3
+ // An instance that is built but not announced needs a door that is shut without
4
+ // being broken. The proxy can do that with basic_auth, and task 1002774 did
5
+ // exactly that to close an exposure quickly — but HTTP basic auth is per-ORIGIN,
6
+ // so the apex, the hall and the status dashboard each prompt separately. Three
7
+ // native browser dialogs, none of them able to say what the site is or why it is
8
+ // asking. The owner asked for one password, entered once, good across the whole
9
+ // domain for the session.
10
+ //
11
+ // So the gate lives in the app, where it can set a cookie the browser will send
12
+ // to every subdomain:
13
+ //
14
+ // - It is INERT unless STEALTH_PASSWORD is set. That is the portable default:
15
+ // a vanilla instance has no gate and needs no config to not have one.
16
+ // - The cookie is a SESSION cookie — no Max-Age, no Expires — because the
17
+ // owner explicitly did not want it to outlive the browser. That is also the
18
+ // safer default: nothing is written to disk to be found later.
19
+ // - It is scoped to the cookie apex, so ONE entry unlocks apex + www +
20
+ // builders + status. The apex and its host set come from the same branding
21
+ // key auth.js reads (domains.cookieDomain), so the gate and the session
22
+ // cookie cannot disagree about what "this domain" means.
23
+ // - The cookie carries an HMAC KEYED BY THE PASSWORD, never the password. No
24
+ // second secret to manage, and rotating the password invalidates every
25
+ // outstanding cookie for free.
26
+ //
27
+ // This is deliberately not an authorization system. It has no accounts, no
28
+ // ranks, and it grants nothing: everything behind it still enforces its own
29
+ // rank checks against the database (ADR 0016). It only decides whether a
30
+ // stranger may SEE the instance at all, which is why one shared password is the
31
+ // right primitive and why it must never be mistaken for sign-in.
32
+
33
+ 'use strict';
34
+
35
+ const crypto = require('node:crypto');
36
+ const { branding } = require('../branding');
37
+
38
+ const COOKIE = 'cb_stealth';
39
+ const SUBMIT_PATH = '/__stealth';
40
+
41
+ // Paths that must answer even to a stranger, each for its own reason:
42
+ // /api/* the GitHub OAuth round trip has to complete, and a gated callback
43
+ // would break sign-in for the very people holding the password
44
+ // /healthz external probes + the upgrade's post-restart health check
45
+ // /metrics Prometheus, already access-gated on its own terms (loopback or
46
+ // bearer token) — double-gating it would break the scrape
47
+ // /version the deploy pin check, a public commit sha
48
+ const EXEMPT = [/^\/api\//, /^\/healthz$/, /^\/metrics$/, /^\/version$/, /^\/__stealth$/];
49
+
50
+ function password() {
51
+ const p = process.env.STEALTH_PASSWORD;
52
+ return typeof p === 'string' && p.length ? p : null;
53
+ }
54
+
55
+ function enabled() { return password() !== null; }
56
+
57
+ // The token a valid cookie must carry. Keyed by the password, so the password
58
+ // itself never leaves the server and a rotation invalidates every cookie.
59
+ function expectedToken() {
60
+ return crypto.createHmac('sha256', password()).update('stealth-v1').digest('hex');
61
+ }
62
+
63
+ function tokenMatches(raw) {
64
+ if (typeof raw !== 'string') return false;
65
+ const want = Buffer.from(expectedToken(), 'utf8');
66
+ const got = Buffer.from(raw, 'utf8');
67
+ // timingSafeEqual throws on a length mismatch, which is itself a comparison —
68
+ // check the length first so both branches cost the same shape
69
+ if (got.length !== want.length) return false;
70
+ return crypto.timingSafeEqual(got, want);
71
+ }
72
+
73
+ function cookieApex() {
74
+ try {
75
+ return String(branding().domains?.cookieDomain || '').replace(/^\./, '').toLowerCase();
76
+ } catch {
77
+ return '';
78
+ }
79
+ }
80
+
81
+ function reqHost(req) {
82
+ const raw = req.headers['x-forwarded-host'] || req.headers['host'] || '';
83
+ return String(raw).toLowerCase().split(',')[0].trim().split(':')[0];
84
+ }
85
+
86
+ // Scope to the apex only on the hosts that actually share it — the same set
87
+ // auth.js uses for the session cookie. Anywhere else (localhost, a preview host)
88
+ // the cookie stays host-only, because a Domain= attribute for a domain you are
89
+ // not on is simply rejected by the browser and would lock the operator out.
90
+ function domainOpts(req) {
91
+ const apex = cookieApex();
92
+ if (!apex) return {};
93
+ const host = reqHost(req);
94
+ const shared = new Set([apex, 'www.' + apex, 'builders.' + apex, 'status.' + apex]);
95
+ return shared.has(host) ? { domain: '.' + apex } : {};
96
+ }
97
+
98
+ function readCookie(req, name) {
99
+ for (const part of String(req.headers.cookie || '').split(';')) {
100
+ const [k, ...v] = part.trim().split('=');
101
+ if (k === name && v.length) return decodeURIComponent(v.join('='));
102
+ }
103
+ return null;
104
+ }
105
+
106
+ function isExempt(path) {
107
+ return EXEMPT.some((re) => re.test(path));
108
+ }
109
+
110
+ // The form. Self-contained and token-free on purpose: it is served BEFORE the
111
+ // visitor is allowed to see anything, so it must not pull the branding bundle,
112
+ // the stylesheet, or any other asset that would leak the design of an
113
+ // unannounced product to whoever knocks.
114
+ function formHtml({ productName, wrong }) {
115
+ const name = String(productName || 'This instance').replace(/[<>&"]/g, '');
116
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8">
117
+ <meta name="viewport" content="width=device-width,initial-scale=1">
118
+ <meta name="robots" content="noindex,nofollow">
119
+ <title>${name}</title><style>
120
+ :root{color-scheme:light dark}
121
+ *{box-sizing:border-box}
122
+ body{margin:0;min-height:100vh;display:grid;place-items:center;padding:24px;
123
+ font:500 15px/1.5 ui-rounded,"Quicksand",system-ui,-apple-system,"Segoe UI",sans-serif;
124
+ background:#f9d9a8;color:#2b170e}
125
+ @media(prefers-color-scheme:dark){body{background:#150c06;color:#f4e6cd}}
126
+ .box{width:min(380px,100%);text-align:center}
127
+ .dots{display:flex;gap:10px;justify-content:center;margin-bottom:22px}
128
+ .dots i{display:block;border-radius:50%;background:#ce4806}
129
+ .dots i:first-child{width:38px;height:38px}
130
+ .dots i:last-child{width:28px;height:28px;background:#855b3d;align-self:flex-end}
131
+ h1{font-size:20px;font-weight:700;margin:0 0 6px}
132
+ p{margin:0 0 20px;opacity:.72;font-size:14px}
133
+ form{display:flex;gap:8px}
134
+ input{flex:1;min-width:0;padding:11px 14px;border-radius:999px;border:1.5px solid rgba(206,72,6,.35);
135
+ background:rgba(255,255,255,.72);color:inherit;font:inherit}
136
+ @media(prefers-color-scheme:dark){input{background:rgba(255,255,255,.08)}}
137
+ input:focus{outline:2px solid #ce4806;outline-offset:1px}
138
+ button{padding:11px 20px;border:0;border-radius:999px;background:#ce4806;color:#fff;
139
+ font:700 14px/1 inherit;cursor:pointer}
140
+ .err{margin:14px 0 0;color:#b3261e;font-size:13.5px}
141
+ @media(prefers-color-scheme:dark){.err{color:#ffb4a8}}
142
+ </style></head><body><div class="box">
143
+ <div class="dots"><i></i><i></i></div>
144
+ <h1>${name}</h1>
145
+ <p>Not open to the world yet. Enter the password to have a look.</p>
146
+ <form method="POST" action="${SUBMIT_PATH}">
147
+ <input type="password" name="password" placeholder="Password" aria-label="Password" autofocus required>
148
+ <button type="submit">Enter</button>
149
+ </form>
150
+ ${wrong ? '<p class="err">That password didn’t work.</p>' : ''}
151
+ </div></body></html>`;
152
+ }
153
+
154
+ function productName() {
155
+ try { return branding().identity?.productName || 'This instance'; } catch { return 'This instance'; }
156
+ }
157
+
158
+ // Mounted FIRST, ahead of every other surface — a route registered earlier would
159
+ // answer before the gate ever ran.
160
+ function stealthGate() {
161
+ return function stealth(req, res, next) {
162
+ if (!enabled()) return next();
163
+
164
+ if (req.method === 'POST' && req.path === SUBMIT_PATH) {
165
+ // The form is urlencoded; body parsers mount later (and only on some
166
+ // routers), so read it here rather than depending on middleware order.
167
+ let body = '';
168
+ req.on('data', (c) => {
169
+ body += c;
170
+ if (body.length > 4096) req.destroy(); // a password is not a payload
171
+ });
172
+ req.on('end', () => {
173
+ const params = new URLSearchParams(body);
174
+ const given = params.get('password') || '';
175
+ const want = password();
176
+ const ok = given.length === want.length
177
+ && crypto.timingSafeEqual(Buffer.from(given), Buffer.from(want));
178
+ if (!ok) {
179
+ return res.status(401).type('html').send(formHtml({ productName: productName(), wrong: true }));
180
+ }
181
+ res.cookie(COOKIE, expectedToken(), {
182
+ httpOnly: true,
183
+ secure: req.secure || req.headers['x-forwarded-proto'] === 'https',
184
+ sameSite: 'lax',
185
+ path: '/',
186
+ ...domainOpts(req),
187
+ // no maxAge/expires — a SESSION cookie, by the owner's choice
188
+ });
189
+ res.redirect(303, '/');
190
+ });
191
+ return undefined;
192
+ }
193
+
194
+ if (isExempt(req.path)) return next();
195
+ if (tokenMatches(readCookie(req, COOKIE))) return next();
196
+
197
+ // 401 rather than 200: this is an unauthenticated request being refused, and
198
+ // a crawler that ignores the noindex should still be told so.
199
+ return res.status(401).type('html').send(formHtml({ productName: productName(), wrong: false }));
200
+ };
201
+ }
202
+
203
+ module.exports = { stealthGate, enabled, isExempt, tokenMatches, expectedToken, COOKIE, SUBMIT_PATH };
package/src/module-api.js CHANGED
@@ -633,8 +633,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
633
633
  // landed since 1.19.101 with no explicit bump. run 31753496152. (task 1002620)
634
634
  // 1.19.103 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
635
635
  // landed since 1.19.102 with no explicit bump. run 31754659965. (task 1002620)
636
+ // 1.19.104 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
637
+ // landed since 1.19.103 with no explicit bump. run 31756625919. (task 1002620)
638
+ // 1.19.105 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
639
+ // landed since 1.19.104 with no explicit bump. run 31758526405. (task 1002620)
636
640
  // ---------------------------------------------------------------------------
637
- const CORE_VERSION = '1.19.103'; // CI auto-patch carrier (ADR 0161); see changelog above
641
+ const CORE_VERSION = '1.19.105'; // CI auto-patch carrier (ADR 0161); see changelog above
638
642
 
639
643
  // A namespaced logger so a module's log lines are attributable + consistent.
640
644
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -453,3 +453,67 @@ test('the platform answers for itself by the same rule it applies to others', ()
453
453
  assert.match(SCRIPT, /function loadSelfStats/);
454
454
  assert.match(SCRIPT, /-system\$/, 'machine accounts are excluded here too, or the platform inflates itself');
455
455
  });
456
+
457
+
458
+ /* ── The card must RENDER in both sub-views (task 1002780) ──────────────────
459
+ It shipped nested inside #sub-sky-body. The table view sets `hidden` on that
460
+ container, and [hidden]{display:none !important} stops every descendant
461
+ generating a box — so the card "opened" at 0x0 and was never seen. Every
462
+ property said open; only the geometry disagreed.
463
+
464
+ The first attempt at this fix deleted #sub-sky-body's closing tag, which
465
+ re-parented everything after it and left the bug in place while LOOKING
466
+ fixed — regex-proximity checks passed on the broken file. These tests walk
467
+ the real tag depth instead. ── */
468
+
469
+ function mapSection(html) {
470
+ const open = html.indexOf('<section id="view-map">');
471
+ assert.ok(open !== -1, 'view-map section must exist');
472
+ return html.slice(open, html.indexOf('<section id="view-mine"'));
473
+ }
474
+
475
+ test('the map section closes every div it opens', () => {
476
+ // an unclosed div is invisible to a regex check and to `hidden`-flag
477
+ // assertions alike, and silently re-parents everything after it
478
+ const map = mapSection(HUB);
479
+ const opens = (map.match(/<div\b/g) || []).length;
480
+ const closes = (map.match(/<\/div>/g) || []).length;
481
+ assert.equal(opens, closes, 'unbalanced divs re-parent the nodes that follow');
482
+ });
483
+
484
+ // Walk the real tag structure and return the ids of every element still OPEN
485
+ // where `needle` appears. Depth-tracking, NOT regex proximity: the first
486
+ // version of this test took the next `</div>` as proof the outer div had
487
+ // closed, accepted an inner one, and passed on the still-broken file.
488
+ function openAncestorsAt(html, needle) {
489
+ const at = html.indexOf(needle);
490
+ assert.ok(at !== -1, needle + ' not found');
491
+ const stack = [];
492
+ const tag = /<(\/?)(div|section)\b([^>]*)>/g;
493
+ let m;
494
+ while ((m = tag.exec(html)) !== null && m.index < at) {
495
+ if (m[1] === '/') stack.pop();
496
+ else if (!m[3].trim().endsWith('/')) {
497
+ const id = (m[3].match(/id="([^"]+)"/) || [, null])[1];
498
+ stack.push(id);
499
+ }
500
+ }
501
+ return stack.filter(Boolean);
502
+ }
503
+
504
+ test('no container the view router hides may be an ancestor of the card', () => {
505
+ const ancestors = openAncestorsAt(HUB, 'id="cardScrim"');
506
+ for (const hidden of ['sub-sky-body', 'sub-table-body']) {
507
+ assert.ok(!ancestors.includes(hidden),
508
+ '#' + hidden + ' is toggled hidden — the card cannot live inside it. open: ' + ancestors.join(' > '));
509
+ }
510
+ assert.ok(ancestors.includes('view-map'), 'it is still the overlay of the map view');
511
+ });
512
+
513
+ test('the table body is a sibling of the sky wrapper, not nested inside it', () => {
514
+ // the same defect seen from the other side — a missing close tag re-parents
515
+ // the table itself, which is what the first fix attempt actually did
516
+ const ancestors = openAncestorsAt(HUB, 'id="sub-table-body"');
517
+ assert.ok(!ancestors.includes('sub-sky-body'),
518
+ 'the table must not sit inside the sky wrapper. open: ' + ancestors.join(' > '));
519
+ });
@@ -0,0 +1,169 @@
1
+ // tests/stealth_gate.mjs — the pre-launch stealth gate (task 1002775).
2
+ //
3
+ // A gate that is wrong in either direction is worse than no gate: too open and
4
+ // the unannounced instance is readable; too closed and it locks out sign-in,
5
+ // health probes, or the operator. These drive the REAL booted app, because the
6
+ // gate's whole job is to sit in front of surfaces registered elsewhere — a unit
7
+ // test of the middleware alone could not prove it was mounted early enough.
8
+ //
9
+ // Run: node --test tests/stealth_gate.mjs
10
+
11
+ import assert from 'node:assert/strict';
12
+ import { test, before, after } from 'node:test';
13
+ import http from 'node:http';
14
+ import { createRequire } from 'node:module';
15
+
16
+ const require = createRequire(import.meta.url);
17
+
18
+ const PASSWORD = 'a-test-password';
19
+ process.env.STEALTH_PASSWORD = PASSWORD; // set BEFORE the app is built
20
+
21
+ const gate = require('../src/bongos/stealth-gate.js');
22
+ const platformMod = require('../src/platform-server.js');
23
+
24
+ let server, port;
25
+
26
+ before(async () => {
27
+ server = http.createServer(platformMod.buildPlatformApp());
28
+ await new Promise((r) => server.listen(0, '127.0.0.1', r));
29
+ port = server.address().port;
30
+ });
31
+ after(async () => {
32
+ await new Promise((r) => server.close(r));
33
+ delete process.env.STEALTH_PASSWORD;
34
+ });
35
+
36
+ function request(pathname, { method = 'GET', cookie, body } = {}) {
37
+ return new Promise((resolve, reject) => {
38
+ const req = http.request({
39
+ host: '127.0.0.1', port, path: pathname, method,
40
+ headers: {
41
+ host: '127.0.0.1',
42
+ ...(cookie ? { cookie } : {}),
43
+ ...(body ? { 'content-type': 'application/x-www-form-urlencoded', 'content-length': Buffer.byteLength(body) } : {}),
44
+ },
45
+ }, (res) => {
46
+ let data = '';
47
+ res.on('data', (c) => { data += c; });
48
+ res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: data }));
49
+ });
50
+ req.on('error', reject);
51
+ if (body) req.write(body);
52
+ req.end();
53
+ });
54
+ }
55
+
56
+ const validCookie = () => `${gate.COOKIE}=${gate.expectedToken()}`;
57
+
58
+ test('a stranger gets the form, not the product', async () => {
59
+ const res = await request('/');
60
+ assert.equal(res.status, 401, 'a refused knock is a 401, not a 200');
61
+ assert.match(res.body, /type="password"/, 'the form must be usable');
62
+ assert.match(res.body, /noindex/, 'an unannounced instance must not be indexed');
63
+ });
64
+
65
+ test('the form leaks nothing about the product it is hiding', async () => {
66
+ const res = await request('/');
67
+ // it is served BEFORE the visitor may see anything, so it must not pull the
68
+ // stylesheet, the branding bundle, or any other asset that would show the
69
+ // design of an unannounced product to whoever knocks
70
+ assert.doesNotMatch(res.body, /cosmos\.css/);
71
+ assert.doesNotMatch(res.body, /__BRANDING__/);
72
+ assert.doesNotMatch(res.body, /<script/, 'no script at all on the gate page');
73
+ });
74
+
75
+ test('every human surface is gated, not just the front page', async () => {
76
+ // the old proxy lock covered '/' but not '/projects', so the hub leaked for
77
+ // the whole stealth period — that regression must be impossible here
78
+ for (const p of ['/', '/projects', '/create', '/builders', '/docs']) {
79
+ const res = await request(p);
80
+ assert.equal(res.status, 401, `${p} must be gated`);
81
+ }
82
+ });
83
+
84
+ test('the OAuth round trip and the probes stay open', async () => {
85
+ // gating these would lock out the very people holding the password, and blind
86
+ // the deploy's own health check
87
+ for (const p of ['/healthz', '/version']) {
88
+ const res = await request(p);
89
+ assert.notEqual(res.status, 401, `${p} must answer a stranger`);
90
+ }
91
+ // a DB-free API path: 404 from the router proves the request REACHED the API
92
+ // (i.e. the gate let it through) without needing a database in this suite
93
+ const api = await request('/api/bongos/__not-a-route');
94
+ assert.notEqual(api.status, 401, 'the OAuth/public API surface must not be gated');
95
+ assert.equal(api.status, 404, 'and it should reach the API router itself');
96
+ });
97
+
98
+ test('the right password opens it and sets a SESSION cookie', async () => {
99
+ const res = await request(gate.SUBMIT_PATH, {
100
+ method: 'POST',
101
+ body: new URLSearchParams({ password: PASSWORD }).toString(),
102
+ });
103
+ assert.equal(res.status, 303, 'a successful POST redirects rather than rendering');
104
+ const setCookie = String(res.headers['set-cookie'] || '');
105
+ assert.match(setCookie, new RegExp(gate.COOKIE));
106
+ assert.match(setCookie, /HttpOnly/i, 'script must not be able to read it');
107
+ assert.match(setCookie, /SameSite=Lax/i);
108
+ // the owner asked for session-only — no persistence to disk
109
+ assert.doesNotMatch(setCookie, /Max-Age/i, 'must not outlive the browser session');
110
+ assert.doesNotMatch(setCookie, /Expires/i);
111
+ });
112
+
113
+ test('the cookie never carries the password itself', async () => {
114
+ const res = await request(gate.SUBMIT_PATH, {
115
+ method: 'POST',
116
+ body: new URLSearchParams({ password: PASSWORD }).toString(),
117
+ });
118
+ const setCookie = String(res.headers['set-cookie'] || '');
119
+ assert.ok(!setCookie.includes(PASSWORD), 'the password must never reach the client');
120
+ });
121
+
122
+ test('a valid cookie gets the real page', async () => {
123
+ const res = await request('/', { cookie: validCookie() });
124
+ assert.equal(res.status, 200);
125
+ assert.doesNotMatch(res.body, /type="password"/, 'the gate must be out of the way now');
126
+ });
127
+
128
+ test('a wrong password is refused and says so', async () => {
129
+ const res = await request(gate.SUBMIT_PATH, {
130
+ method: 'POST',
131
+ body: new URLSearchParams({ password: 'not-it' }).toString(),
132
+ });
133
+ assert.equal(res.status, 401);
134
+ assert.match(res.body, /didn/, 'the visitor must be told it failed');
135
+ assert.ok(!String(res.headers['set-cookie'] || '').includes(gate.COOKIE), 'no cookie on failure');
136
+ });
137
+
138
+ test('a forged or stale cookie does not open it', async () => {
139
+ for (const bad of ['x', 'deadbeef', gate.expectedToken().slice(0, -1) + '0']) {
140
+ const res = await request('/', { cookie: `${gate.COOKIE}=${bad}` });
141
+ assert.equal(res.status, 401, `forged cookie ${bad.slice(0, 12)} must be refused`);
142
+ }
143
+ });
144
+
145
+ test('rotating the password invalidates outstanding cookies', async () => {
146
+ const old = validCookie();
147
+ process.env.STEALTH_PASSWORD = 'a-different-password';
148
+ const res = await request('/', { cookie: old });
149
+ process.env.STEALTH_PASSWORD = PASSWORD;
150
+ assert.equal(res.status, 401, 'the token is keyed by the password, so a rotation revokes');
151
+ });
152
+
153
+ test('an oversized POST body is dropped rather than buffered', async () => {
154
+ // a password is not a payload — the handler reads the body itself, so it must
155
+ // bound it or it is a trivial memory sink on an unauthenticated endpoint
156
+ const src = require('node:fs').readFileSync(
157
+ new URL('../src/bongos/stealth-gate.js', import.meta.url), 'utf8');
158
+ assert.match(src, /body\.length > \d+/, 'the body read must be bounded');
159
+ assert.match(src, /req\.destroy\(\)/);
160
+ });
161
+
162
+ test('the gate is inert when no password is configured', () => {
163
+ // the portable default: a vanilla instance has no gate and needs no config
164
+ // to not have one
165
+ delete process.env.STEALTH_PASSWORD;
166
+ assert.equal(gate.enabled(), false);
167
+ process.env.STEALTH_PASSWORD = PASSWORD;
168
+ assert.equal(gate.enabled(), true);
169
+ });