@bongos/core 1.19.656 → 1.19.658

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.
@@ -1,4 +1,31 @@
1
- // src/bongos/stealth-gate.js — the pre-launch stealth gate (task 1002775).
1
+ // src/bongos/prelaunch-gate.js — the instance PRE-LAUNCH gate (task 1002775).
2
+ //
3
+ // ── THE NAME (task 1003152, owner decision 2026-09-09; ADR 0182 Decision 7) ──
4
+ //
5
+ // This was `stealth-gate.js` and it collided head-on with the OTHER live meaning
6
+ // of the word. `platform_identity_projects.visibility = 'stealth'` is a project
7
+ // that IS on the public orbs map — rendered as a black hole, anonymous, refusing
8
+ // applications, but published and visible; only its identity is withheld. This
9
+ // gate is close to the opposite: the whole instance is UNREADABLE without the
10
+ // password. Same class of defect as ADR 0174's "role", which is why the rename
11
+ // lives in the government goal.
12
+ //
13
+ // The PRODUCT meaning keeps the word — ADR 0182 locks it to the orb metaphor via
14
+ // goal 1000046's own criterion, and the ~16 files carrying that sense are
15
+ // deliberately untouched. Renaming those would have been the actual mistake.
16
+ //
17
+ // THREE IDENTIFIERS DID NOT MOVE, and each one is a deliberate choice, not a
18
+ // missed occurrence (the vocabulary check below knows them by name):
19
+ //
20
+ // • the cookie `cb_stealth` and the HMAC label `stealth-v1` — both are keyed
21
+ // INTO live cookies. Renaming either invalidates every outstanding cookie on
22
+ // a running instance and forces every holder to re-enter the password, to
23
+ // buy nothing: neither string is ever read by a human. The cookie name is
24
+ // wire format and the label is an HMAC message, not vocabulary.
25
+ // • `/__stealth` — the submit path. `/__prelaunch` is what the form posts to
26
+ // now; the old path is still accepted and still exempt, so a form already
27
+ // open in somebody's tab when this deploys still works instead of 401ing
28
+ // them back to the start.
2
29
  //
3
30
  // An instance that is built but not announced needs a door that is shut without
4
31
  // being broken. The proxy can do that with basic_auth, and task 1002774 did
@@ -11,8 +38,11 @@
11
38
  // So the gate lives in the app, where it can set a cookie the browser will send
12
39
  // to every subdomain:
13
40
  //
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.
41
+ // - It is INERT unless PRELAUNCH_PASSWORD is set. That is the portable
42
+ // default: a vanilla instance has no gate and needs no config to not have
43
+ // one. STEALTH_PASSWORD is honoured as a deprecated alias — it is SET ON
44
+ // LIVE today, and a rename that did not read the old name would be an
45
+ // instant outage.
16
46
  // - The cookie is a SESSION cookie — no Max-Age, no Expires — because the
17
47
  // owner explicitly did not want it to outlive the browser. That is also the
18
48
  // safer default: nothing is written to disk to be found later.
@@ -35,8 +65,15 @@
35
65
  const crypto = require('node:crypto');
36
66
  const { branding } = require('../branding');
37
67
 
68
+ // Wire format, deliberately unrenamed — see the header. Both are keyed into
69
+ // cookies that are live right now.
38
70
  const COOKIE = 'cb_stealth';
39
- const SUBMIT_PATH = '/__stealth';
71
+ const HMAC_LABEL = 'stealth-v1';
72
+
73
+ const SUBMIT_PATH = '/__prelaunch';
74
+ // Accepted for as long as a form served before the rename might still be open.
75
+ const LEGACY_SUBMIT_PATH = '/__stealth';
76
+ const SUBMIT_PATHS = [SUBMIT_PATH, LEGACY_SUBMIT_PATH];
40
77
 
41
78
  // Paths that must answer even to a stranger, each for its own reason:
42
79
  // /api/* the GitHub OAuth round trip has to complete, and a gated callback
@@ -45,19 +82,37 @@ const SUBMIT_PATH = '/__stealth';
45
82
  // /metrics Prometheus, already access-gated on its own terms (loopback or
46
83
  // bearer token) — double-gating it would break the scrape
47
84
  // /version the deploy pin check, a public commit sha
48
- const EXEMPT = [/^\/api\//, /^\/healthz$/, /^\/metrics$/, /^\/version$/, /^\/__stealth$/];
85
+ const EXEMPT = [/^\/api\//, /^\/healthz$/, /^\/metrics$/, /^\/version$/, /^\/__prelaunch$/, /^\/__stealth$/];
49
86
 
87
+ // PRELAUNCH_PASSWORD is primary; STEALTH_PASSWORD is the deprecated alias. This
88
+ // is the ONE read site for either, which is what makes honouring both a single
89
+ // expression rather than a migration — and why the rename could be taken at all
90
+ // without an outage window on a live instance already running the old name.
50
91
  function password() {
51
- const p = process.env.STEALTH_PASSWORD;
92
+ const p = process.env.PRELAUNCH_PASSWORD || process.env.STEALTH_PASSWORD;
52
93
  return typeof p === 'string' && p.length ? p : null;
53
94
  }
54
95
 
96
+ // Named once at mount, never per request: a warning on every gated request would
97
+ // be a log flood on exactly the instance that is hardest to look at. Dropping the
98
+ // alias is a separate, later task — this only makes the deprecation audible.
99
+ function warnIfLegacyEnv(log = console.warn) {
100
+ const hasNew = typeof process.env.PRELAUNCH_PASSWORD === 'string' && process.env.PRELAUNCH_PASSWORD.length;
101
+ const hasOld = typeof process.env.STEALTH_PASSWORD === 'string' && process.env.STEALTH_PASSWORD.length;
102
+ if (hasNew || !hasOld) return false;
103
+ log('[prelaunch-gate] STEALTH_PASSWORD is DEPRECATED and still the only password set. '
104
+ + 'Set PRELAUNCH_PASSWORD to the same value (task 1003152); the old name is read for now '
105
+ + 'and will be dropped in a later task.');
106
+ return true;
107
+ }
108
+
55
109
  function enabled() { return password() !== null; }
56
110
 
57
111
  // The token a valid cookie must carry. Keyed by the password, so the password
58
112
  // itself never leaves the server and a rotation invalidates every cookie.
113
+ // HMAC_LABEL is deliberately still 'stealth-v1' — see the header.
59
114
  function expectedToken() {
60
- return crypto.createHmac('sha256', password()).update('stealth-v1').digest('hex');
115
+ return crypto.createHmac('sha256', password()).update(HMAC_LABEL).digest('hex');
61
116
  }
62
117
 
63
118
  function tokenMatches(raw) {
@@ -118,7 +173,7 @@ function relativeReturn(raw) {
118
173
  if (typeof raw !== 'string' || !raw.length || raw.length > 512) return null;
119
174
  if (!raw.startsWith('/') || raw.startsWith('//') || raw.includes('\\')) return null;
120
175
  if (/[\x00-\x1f\x7f]/.test(raw)) return null;
121
- if (raw === '/' || raw.startsWith(SUBMIT_PATH)) return null;
176
+ if (raw === '/' || SUBMIT_PATHS.some((p) => raw.startsWith(p))) return null;
122
177
  return raw;
123
178
  }
124
179
 
@@ -178,11 +233,12 @@ function productName() {
178
233
 
179
234
  // Mounted FIRST, ahead of every other surface — a route registered earlier would
180
235
  // answer before the gate ever ran.
181
- function stealthGate() {
182
- return function stealth(req, res, next) {
236
+ function prelaunchGate() {
237
+ warnIfLegacyEnv();
238
+ return function prelaunch(req, res, next) {
183
239
  if (!enabled()) return next();
184
240
 
185
- if (req.method === 'POST' && req.path === SUBMIT_PATH) {
241
+ if (req.method === 'POST' && SUBMIT_PATHS.includes(req.path)) {
186
242
  // The form is urlencoded; body parsers mount later (and only on some
187
243
  // routers), so read it here rather than depending on middleware order.
188
244
  let body = '';
@@ -226,4 +282,7 @@ function stealthGate() {
226
282
  };
227
283
  }
228
284
 
229
- module.exports = { stealthGate, enabled, isExempt, tokenMatches, expectedToken, COOKIE, SUBMIT_PATH };
285
+ module.exports = {
286
+ prelaunchGate, enabled, isExempt, tokenMatches, expectedToken, warnIfLegacyEnv,
287
+ COOKIE, HMAC_LABEL, SUBMIT_PATH, LEGACY_SUBMIT_PATH, SUBMIT_PATHS,
288
+ };
@@ -47,7 +47,7 @@ const { buildInfo } = require('../build-info');
47
47
  const { buildDevboxDownloads } = require('../devbox-downloads');
48
48
  const { buildBongosDownloads } = require('../bongos-downloads');
49
49
  const { clientBranding, branding } = require('../branding');
50
- const { stealthGate } = require('./stealth-gate');
50
+ const { prelaunchGate } = require('./prelaunch-gate');
51
51
  const { platformVisibilityGate } = require('./platform-visibility-gate');
52
52
  const { clientModules, isModuleEnabled } = require('../modules');
53
53
  const { hallWidgetScripts, moduleWebSurfaces } = require('../module-loader/loader');
@@ -494,15 +494,15 @@ function mountInternalSurfaces(app) {
494
494
  // game routes too) — it records on res 'finish' regardless of which handler
495
495
  // answers. The /metrics scrape endpoint is access-gated (loopback Prometheus
496
496
  // or Bearer METRICS_TOKEN; see metrics.authorizeScrape).
497
- // THE STEALTH GATE (task 1002775) mounts before anything it is meant to
497
+ // THE PRE-LAUNCH GATE (task 1002775) mounts before anything it is meant to
498
498
  // cover — a surface registered earlier would answer first and the gate would
499
- // never run. Inert unless STEALTH_PASSWORD is set, so a vanilla instance is
499
+ // never run. Inert unless PRELAUNCH_PASSWORD is set, so a vanilla instance is
500
500
  // unaffected and pays only a closure call per request.
501
501
  //
502
502
  // It sits ABOVE the metrics middleware deliberately: a refused knock is still
503
503
  // a request this instance served, and hiding gate 401s would make the traffic
504
504
  // graph lie about how much attention an unannounced instance is getting.
505
- app.use(stealthGate());
505
+ app.use(prelaunchGate());
506
506
 
507
507
  app.use(metrics.httpMiddleware());
508
508
  app.get('/metrics', metrics.metricsHandler());
package/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.656'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.658'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -36,6 +36,10 @@ const CLIENT = fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', '
36
36
  const HTML = fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', 'government.html'), 'utf8');
37
37
  const BOARD = fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', 'board-room.js'), 'utf8');
38
38
  const BOARD_HTML = fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', 'board-room.html'), 'utf8');
39
+ // The vocabulary both pages speak (task 1003761). The pass-rule maps and
40
+ // rankNames were one copy per page until the hoist; the assertions that read
41
+ // them come here now, and there is one place to read rather than two.
42
+ const LIB = fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', 'board-lib.js'), 'utf8');
39
43
  const { PASS_RULES } = require(path.join(ROOT, 'modules', 'government', 'config.js'));
40
44
 
41
45
  const card = () => BOARD.slice(BOARD.indexOf('function itemCard'), BOARD.indexOf('async function loadBoard'));
@@ -47,7 +51,13 @@ const card = () => BOARD.slice(BOARD.indexOf('function itemCard'), BOARD.indexOf
47
51
  const CODE = BOARD.split(/\r?\n/).filter((l) => !/^\s*\/\//.test(l)).join('\n');
48
52
 
49
53
  test('every pass rule the server can hold has a plain-language sentence', () => {
50
- const block = BOARD.slice(BOARD.indexOf('const PASS_RULE_PLAIN'), BOARD.indexOf('function decidedByHtml'));
54
+ // In board-lib.js since task 1003761. Both landmarks are asserted before the
55
+ // slice for the reason the next test spells out: an indexOf that misses
56
+ // returns -1, and slice(start, -1) would hand this most of the file.
57
+ const start = LIB.indexOf('const PASS_RULE_PLAIN');
58
+ const end = LIB.indexOf('shared row pieces');
59
+ assert.ok(start !== -1 && end > start, 'PASS_RULE_PLAIN sits above the row pieces in board-lib.js');
60
+ const block = LIB.slice(start, end);
51
61
  assert.ok(PASS_RULES.length >= 3, `sanity: found the real rule set (${PASS_RULES})`);
52
62
  for (const rule of PASS_RULES) {
53
63
  assert.ok(block.includes(`${rule}:`), `PASS_RULE_PLAIN must explain '${rule}' — a rule with no copy renders its enum key`);
@@ -60,24 +70,24 @@ test('every pass rule the server can hold has a plain-language sentence', () =>
60
70
  });
61
71
 
62
72
  test('the short name map covers the same rule set (the history line)', () => {
63
- // Its reader (historyRow, the amendment change-history) stayed on the
64
- // government page when ADR 0266 carved the room out, so this map stayed with
65
- // it. The slice therefore ends at a landmark that is still IN government.js —
66
- // it used to end at `const PASS_RULE_PLAIN`, which has moved to board-room.js,
67
- // and an indexOf that misses returns -1: `slice(start, -1)` would have handed
68
- // this assertion most of the file and passed for the wrong reason.
69
- const start = CLIENT.indexOf('const PASS_RULE_NAME');
70
- const end = CLIENT.indexOf('function dialRow');
71
- assert.ok(start !== -1 && end > start, 'PASS_RULE_NAME sits above dialRow in government.js');
72
- const block = CLIENT.slice(start, end);
73
+ // Its reader (historyRow, the amendment change-history) is on the government
74
+ // page; the map itself joined its sentence-length twin in board-lib.js in task
75
+ // 1003761, so the two renderings of one pass rule sit adjacent and cannot
76
+ // describe the same rule differently. The landmark check stays and matters:
77
+ // an indexOf that misses returns -1, and `slice(start, -1)` would hand this
78
+ // assertion most of the file and pass for the wrong reason.
79
+ const start = LIB.indexOf('const PASS_RULE_NAME');
80
+ const end = LIB.indexOf('const PASS_RULE_PLAIN');
81
+ assert.ok(start !== -1 && end > start, 'PASS_RULE_NAME sits above PASS_RULE_PLAIN in board-lib.js');
82
+ const block = LIB.slice(start, end);
73
83
  for (const rule of PASS_RULES) {
74
84
  assert.ok(block.includes(`${rule}:`), `PASS_RULE_NAME must name '${rule}'`);
75
85
  }
76
86
  });
77
87
 
78
88
  test('a rank reaches the reader as a LABEL, never as the enum key', () => {
79
- assert.match(BOARD, /function rankNames\(keys\)[\s\S]*?window\.OTB\.rankLabel\(k\)/,
80
- 'the board card resolves rank keys through the branding pack');
89
+ assert.match(LIB, /function rankNames\(keys\)[\s\S]*?window\.OTB\.rankLabel\(k\)/,
90
+ 'the shared vocabulary resolves rank keys through the branding pack');
81
91
  const membership = CLIENT.slice(CLIENT.indexOf('function membershipSentence'), CLIENT.indexOf('function passRuleSentence'));
82
92
  assert.match(membership, /window\.OTB\.rankLabel\(k\)/,
83
93
  'so does the Constitution sentence — `archon` is an enum, "Owner" is what a person reads');
@@ -101,7 +111,13 @@ test('the card order is title → action → the idea → the apparatus', () =>
101
111
  });
102
112
 
103
113
  test('the idea renders OPEN — never behind a summary', () => {
104
- const sections = BOARD.slice(BOARD.indexOf('function ideaSectionsHtml'), BOARD.indexOf('function rankNames'));
114
+ // Ends at decidedByHtml, the declaration that now follows: rankNames was the
115
+ // old landmark and moved to board-lib.js in task 1003761. Asserted, not
116
+ // assumed — a missing landmark is -1 and would silently invert this test.
117
+ const from = BOARD.indexOf('function ideaSectionsHtml');
118
+ const to = BOARD.indexOf('function decidedByHtml');
119
+ assert.ok(from !== -1 && to > from, 'ideaSectionsHtml sits above decidedByHtml in board-room.js');
120
+ const sections = BOARD.slice(from, to);
105
121
  assert.ok(!/<details/.test(sections),
106
122
  'the substance of the vote is not a disclosure — that was the reported bug');
107
123
  assert.match(sections, /gov-idea__body/, 'and it is set as prose, not as a config row');
@@ -95,12 +95,17 @@ test('the read rides the vote atom, and the client renders the SERVER value (sou
95
95
  // of a grammar is exactly the drift that produced the bug this task fixes, so
96
96
  // they are held equal here over every form.
97
97
  //
98
+ // The mirror lives in modules/hall-ui/public/board-lib.js since task 1003761.
99
+ // It was in government.js, and ADR 0266's carve left a SECOND copy in
100
+ // board-room.js — so this test lifted one of two implementations and proved
101
+ // nothing about the other. One copy, one lift.
102
+ //
98
103
  // This matters more than a normal mirror: the Constitution panel is where an
99
104
  // owner READS who governs before deciding whether to change it. A panel that
100
105
  // misdescribes the franchise is the same failure as a resolver that mis-seats
101
106
  // it, one layer up.
102
107
  test('the hall renders every predicate form the server actually resolves', () => {
103
- const src = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/government.js'), 'utf8');
108
+ const src = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/board-lib.js'), 'utf8');
104
109
 
105
110
  // Lift the browser mirror out of the IIFE and run it.
106
111
  const ladder = /const STANDARD_LADDER = (\[[^\]]*\]);/.exec(src);
@@ -131,7 +136,7 @@ test('the hall renders every predicate form the server actually resolves', () =>
131
136
  });
132
137
 
133
138
  test('the ladder the hall expands against IS the government ladder', () => {
134
- const src = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/government.js'), 'utf8');
139
+ const src = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/board-lib.js'), 'utf8');
135
140
  const { RANK_ORDER } = require(path.join(ROOT, 'modules/government/catalog.js'));
136
141
  const m = /const STANDARD_LADDER = \[([^\]]*)\]/.exec(src);
137
142
  assert.ok(m);
@@ -1,4 +1,4 @@
1
- // tests/stealth_gate.mjs — the pre-launch stealth gate (task 1002775).
1
+ // tests/prelaunch_gate.mjs — the instance pre-launch gate (task 1002775).
2
2
  //
3
3
  // A gate that is wrong in either direction is worse than no gate: too open and
4
4
  // the unannounced instance is readable; too closed and it locks out sign-in,
@@ -6,7 +6,7 @@
6
6
  // gate's whole job is to sit in front of surfaces registered elsewhere — a unit
7
7
  // test of the middleware alone could not prove it was mounted early enough.
8
8
  //
9
- // Run: node --test tests/stealth_gate.mjs
9
+ // Run: node --test tests/prelaunch_gate.mjs
10
10
 
11
11
  import assert from 'node:assert/strict';
12
12
  import { test, before, after } from 'node:test';
@@ -16,9 +16,9 @@ import { createRequire } from 'node:module';
16
16
  const require = createRequire(import.meta.url);
17
17
 
18
18
  const PASSWORD = 'a-test-password';
19
- process.env.STEALTH_PASSWORD = PASSWORD; // set BEFORE the app is built
19
+ process.env.PRELAUNCH_PASSWORD = PASSWORD; // set BEFORE the app is built
20
20
 
21
- const gate = require('../src/bongos/stealth-gate.js');
21
+ const gate = require('../src/bongos/prelaunch-gate.js');
22
22
  const platformMod = require('../src/platform-server.js');
23
23
 
24
24
  let server, port;
@@ -74,7 +74,7 @@ test('the form leaks nothing about the product it is hiding', async () => {
74
74
 
75
75
  test('every human surface is gated, not just the front page', async () => {
76
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
77
+ // the whole pre-launch period — that regression must be impossible here
78
78
  for (const p of ['/', '/projects', '/create', '/builders', '/docs']) {
79
79
  const res = await request(p);
80
80
  assert.equal(res.status, 401, `${p} must be gated`);
@@ -209,9 +209,9 @@ test('a forged or stale cookie does not open it', async () => {
209
209
 
210
210
  test('rotating the password invalidates outstanding cookies', async () => {
211
211
  const old = validCookie();
212
- process.env.STEALTH_PASSWORD = 'a-different-password';
212
+ process.env.PRELAUNCH_PASSWORD = 'a-different-password';
213
213
  const res = await request('/', { cookie: old });
214
- process.env.STEALTH_PASSWORD = PASSWORD;
214
+ process.env.PRELAUNCH_PASSWORD = PASSWORD;
215
215
  assert.equal(res.status, 401, 'the token is keyed by the password, so a rotation revokes');
216
216
  });
217
217
 
@@ -219,16 +219,145 @@ test('an oversized POST body is dropped rather than buffered', async () => {
219
219
  // a password is not a payload — the handler reads the body itself, so it must
220
220
  // bound it or it is a trivial memory sink on an unauthenticated endpoint
221
221
  const src = require('node:fs').readFileSync(
222
- new URL('../src/bongos/stealth-gate.js', import.meta.url), 'utf8');
222
+ new URL('../src/bongos/prelaunch-gate.js', import.meta.url), 'utf8');
223
223
  assert.match(src, /body\.length > \d+/, 'the body read must be bounded');
224
224
  assert.match(src, /req\.destroy\(\)/);
225
225
  });
226
226
 
227
227
  test('the gate is inert when no password is configured', () => {
228
228
  // the portable default: a vanilla instance has no gate and needs no config
229
- // to not have one
229
+ // to not have one. BOTH names must be clear — an alias that kept the gate up
230
+ // after the primary was unset would be the same bug in reverse.
231
+ delete process.env.PRELAUNCH_PASSWORD;
230
232
  delete process.env.STEALTH_PASSWORD;
231
233
  assert.equal(gate.enabled(), false);
232
- process.env.STEALTH_PASSWORD = PASSWORD;
234
+ process.env.PRELAUNCH_PASSWORD = PASSWORD;
233
235
  assert.equal(gate.enabled(), true);
234
236
  });
237
+
238
+ // ── the rename's compatibility surface (task 1003152) ───────────────────────
239
+ //
240
+ // STEALTH_PASSWORD is SET ON LIVE cloudbongos.com right now. A rename that did
241
+ // not read the old name would be an instant, total outage of the instance — so
242
+ // these are the tests that make the rename safe to deploy, not nice-to-haves.
243
+
244
+ test('the DEPRECATED STEALTH_PASSWORD alone still opens the gate', async () => {
245
+ delete process.env.PRELAUNCH_PASSWORD;
246
+ process.env.STEALTH_PASSWORD = PASSWORD;
247
+ try {
248
+ assert.equal(gate.enabled(), true, 'an instance running only the old name must not go dark');
249
+ // and the cookie it mints is the same one, because the HMAC label did not move
250
+ const res = await request('/', { cookie: `${gate.COOKIE}=${gate.expectedToken()}` });
251
+ assert.equal(res.status, 200, 'a cookie held across the rename must still be honoured');
252
+ } finally {
253
+ delete process.env.STEALTH_PASSWORD;
254
+ process.env.PRELAUNCH_PASSWORD = PASSWORD;
255
+ }
256
+ });
257
+
258
+ test('PRELAUNCH_PASSWORD wins when both are set', () => {
259
+ const fromNewNameAlone = gate.expectedToken(); // PRELAUNCH_PASSWORD only
260
+ process.env.STEALTH_PASSWORD = 'the-old-one';
261
+ try {
262
+ assert.equal(gate.expectedToken(), fromNewNameAlone,
263
+ 'the primary name is primary — an operator who sets the new one and forgets '
264
+ + 'to clear the old must get the value they just wrote, not the stale one');
265
+ } finally {
266
+ delete process.env.STEALTH_PASSWORD;
267
+ }
268
+ });
269
+
270
+ test('running on the old name alone warns once, at mount', () => {
271
+ const lines = [];
272
+ const log = (m) => lines.push(String(m));
273
+ delete process.env.PRELAUNCH_PASSWORD;
274
+ process.env.STEALTH_PASSWORD = PASSWORD;
275
+ try {
276
+ assert.equal(gate.warnIfLegacyEnv(log), true, 'the deprecation must be audible');
277
+ assert.match(lines.join('\n'), /PRELAUNCH_PASSWORD/, 'the warning must name the replacement');
278
+ } finally {
279
+ delete process.env.STEALTH_PASSWORD;
280
+ process.env.PRELAUNCH_PASSWORD = PASSWORD;
281
+ }
282
+ // silent once the new name is set — nobody needs a warning they have acted on
283
+ assert.equal(gate.warnIfLegacyEnv(log), false);
284
+ });
285
+
286
+ test('both submit paths are accepted, and both are exempt', async () => {
287
+ // A form served before the rename may still be open in somebody's tab. The old
288
+ // path keeps working so their password lands them in, not on a 401.
289
+ assert.equal(gate.SUBMIT_PATH, '/__prelaunch');
290
+ assert.equal(gate.LEGACY_SUBMIT_PATH, '/__stealth');
291
+ for (const p of gate.SUBMIT_PATHS) {
292
+ assert.equal(gate.isExempt(p), true, `${p} must be exempt or the submit is gated behind itself`);
293
+ const res = await request(p, { method: 'POST', body: `password=${encodeURIComponent(PASSWORD)}` });
294
+ assert.equal(res.status, 303, `POST ${p} must accept the password`);
295
+ assert.match(String(res.headers['set-cookie'] || ''), new RegExp(gate.COOKIE),
296
+ `POST ${p} must mint the cookie`);
297
+ }
298
+ });
299
+
300
+ test('the wire format did NOT move — renaming it would log everyone out', () => {
301
+ // Deliberate, and the reason it is pinned: the cookie name and the HMAC label
302
+ // are baked into cookies that are live right now. A later "finish the rename"
303
+ // pass that changes either forces every holder to re-enter the password, to
304
+ // buy nothing — neither string is ever read by a human.
305
+ assert.equal(gate.COOKIE, 'cb_stealth');
306
+ assert.equal(gate.HMAC_LABEL, 'stealth-v1');
307
+ });
308
+
309
+ // ── the vocabulary wall around the rename (check 19d, task 1003152) ─────────
310
+ //
311
+ // ADR 0174's lesson, applied: a naming rule is the easiest kind of check to ship
312
+ // broken — allowlist enough and it passes forever while proving nothing. So the
313
+ // load-bearing tests are the MUTATION tests. A rule that cannot be made to fail
314
+ // is not a rule.
315
+
316
+ const fitness = require('../scripts/gds/fitness.js');
317
+
318
+ test('the pre-launch surface is clean today', () => {
319
+ const r = fitness.checkPrelaunchVocabulary();
320
+ assert.equal(r.ok, true, `check 19d is failing on the real tree:\n${r.violations.join('\n')}`);
321
+ assert.ok(r.note.includes('file(s) scanned'), 'the check must report what it scanned');
322
+ });
323
+
324
+ test('a planted "stealth" on the pre-launch surface REDS the build', () => {
325
+ assert.ok(plantInGate('// the stealth gate mounts here').some((v) => /"stealth"/.test(v)),
326
+ 'the retired word must be caught, or the rename un-does itself one comment at a time');
327
+ });
328
+
329
+ test('a documented COMPATIBILITY identifier is not a violation', () => {
330
+ // The four that deliberately stayed. Catching these would force an allowlist,
331
+ // which is what the citation/compat split exists to avoid.
332
+ for (const line of ["const c = 'cb_stealth';", "hmac('stealth-v1');",
333
+ "path === '/__stealth'", 'process.env.STEALTH_PASSWORD']) {
334
+ assert.equal(plantInGate(line).length, 0, `the compat identifier tripped the wall: ${line}`);
335
+ }
336
+ });
337
+
338
+ test('a line CITING the rename or the other meaning may say the word', () => {
339
+ for (const line of ['// renamed from stealth-gate.js (task 1003152)',
340
+ "// platform_identity_projects.visibility = 'stealth' is the OTHER meaning"]) {
341
+ assert.equal(plantInGate(line).length, 0, `a citation tripped the wall: ${line}`);
342
+ }
343
+ });
344
+
345
+ test('the citation does NOT excuse the rest of the file', () => {
346
+ // The header cites task 1003152 on its own line. If the exemption were
347
+ // file-scoped rather than line-scoped, every later line would ride it.
348
+ assert.ok(plantInGate('// bring back the stealth wording').length > 0,
349
+ 'an uncited line must still red — a file-wide exemption is an allowlist wearing a regex');
350
+ });
351
+
352
+ // Plant a line into the real gate file, run the real check, restore.
353
+ function plantInGate(line) {
354
+ const fs = require('node:fs');
355
+ const target = new URL('../src/bongos/prelaunch-gate.js', import.meta.url);
356
+ const original = fs.readFileSync(target, 'utf8');
357
+ try {
358
+ fs.writeFileSync(target, `${original}\n${line}\n`);
359
+ return fitness.checkPrelaunchVocabulary().violations;
360
+ } finally {
361
+ fs.writeFileSync(target, original);
362
+ }
363
+ }
@@ -107,23 +107,35 @@ const STATIC_LADDER_FILES = [
107
107
  'modules/hall-ui/public/ranks.js',
108
108
  'modules/hall-ui/public/profile.js',
109
109
  'modules/hall-ui/public/watch.js',
110
- // task 1003094: the Constitution panel expands the `rank:<key>+` membership
111
- // form ("that rank and above") to render who governs in plain language, so it
112
- // needs the ladder to expand against. Browser code cannot require the server
113
- // module the established mirror pattern hereand listing it in this array
114
- // is what makes the copy provable rather than merely allowed. A second,
115
- // government-specific pin lives in tests/government_constitution_view.mjs,
116
- // which holds it against catalog.RANK_ORDER and holds the whole browser parser
117
- // equal to the server one.
110
+ // task 1003094: the Constitution panel and the Board Room's sitting card both
111
+ // expand the `rank:<key>+` membership form ("that rank and above") to render
112
+ // who governs in plain language, so both need the ladder to expand against.
113
+ // Browser code cannot require the server modulethe established mirror
114
+ // pattern here — and listing it in this array is what makes the copy provable
115
+ // rather than merely allowed.
116
+ //
117
+ // ONE ROW, not two, since task 1003761. ADR 0266 carved the Board Room onto
118
+ // its own page and the carve COPIED the ladder and the parser into both page
119
+ // files, so this array had to allowlist BOTH — which permitted a drift between
120
+ // them by construction: the exact-literal pin below proved each matched the
121
+ // canonical ladder, but nothing held the two parsers equal to EACH OTHER, and
122
+ // government_constitution_view.mjs lifted only one of them. board-lib.js is
123
+ // the single copy now. A second, government-specific pin lives in
124
+ // tests/government_constitution_view.mjs, which holds it against
125
+ // catalog.RANK_ORDER and runs the whole browser parser against the server one.
126
+ //
127
+ // This row used to claim tests/government_board_vote_ui.mjs pinned the
128
+ // board-room copy. It does not — that file asserts the sitting card, the
129
+ // ballot and the vote form, and has no parser assertion at all, so the second
130
+ // copy was guarded only by the exact-literal ladder match.
131
+ 'modules/hall-ui/public/board-lib.js',
132
+ // government.js keeps a ladder literal of its own — the charter library's
133
+ // `seatsRanks` worked example for `rank:xenos+` (the full ladder, spelled out
134
+ // so a reader sees what the predicate resolves to). That is documentation of
135
+ // the grammar rather than a second implementation of it, and it must still
136
+ // match the canonical order or the example teaches a ladder that does not
137
+ // exist.
118
138
  'modules/hall-ui/public/government.js',
119
- // ADR 0266: the Board Room moved off the government page onto its own, and the
120
- // sitting card's `decidedByHtml` expands the same `rank:<key>+` membership form
121
- // to say who may vote on THAT sitting — so the carve took a copy of the ladder
122
- // with it. Same reason as the row above (browser code cannot require the server
123
- // module), and the same pinning: `tests/government_board_vote_ui.mjs` reads
124
- // board-room.js and holds its predicate parser equal to the server's, and the
125
- // pin below in this file holds the array itself against LIVE_RANK_LADDER.
126
- 'modules/hall-ui/public/board-room.js',
127
139
  // task 1002487: the pure agent-definition validator applies the ADR 0043
128
140
  // sub-Metic floor to an agent's declared scope, so it needs the ladder to
129
141
  // compare against. It is one of the "dependency-free" cases this array exists