@bongos/core 1.21.181 → 1.22.1

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.
Files changed (138) hide show
  1. package/.bongos-core.json +159 -144
  2. package/docs/copy-inventory.md +165 -165
  3. package/docs/copy-registry.json +184 -184
  4. package/docs/file-map.md +1 -1
  5. package/docs/module-api-changelog.md +12 -0
  6. package/docs/page-inventory.json +35 -26
  7. package/docs/page-readings.json +389 -388
  8. package/modules/hall-ui/public/approval-queue.js +3 -3
  9. package/modules/hall-ui/public/atlas.html +2 -1
  10. package/modules/hall-ui/public/atlas.js +1 -1
  11. package/modules/hall-ui/public/blockers.html +3 -4
  12. package/modules/hall-ui/public/board-room.html +2 -2
  13. package/modules/hall-ui/public/brand-holes.js +3 -3
  14. package/modules/hall-ui/public/city.html +2 -1
  15. package/modules/hall-ui/public/collab.html +2 -2
  16. package/modules/hall-ui/public/commands.html +2 -2
  17. package/modules/hall-ui/public/commands.js +1 -2
  18. package/modules/hall-ui/public/copy-desk.html +2 -2
  19. package/modules/hall-ui/public/core-move-notice.js +1 -5
  20. package/modules/hall-ui/public/craft-mode.js +1 -5
  21. package/modules/hall-ui/public/criterion-uat-ui.js +3 -3
  22. package/modules/hall-ui/public/deploy.html +2 -2
  23. package/modules/hall-ui/public/diagrams.html +2 -1
  24. package/modules/hall-ui/public/dom-utils.js +15 -6
  25. package/modules/hall-ui/public/drachmae.html +2 -1
  26. package/modules/hall-ui/public/fleet-costs.js +3 -2
  27. package/modules/hall-ui/public/fleet.html +2 -2
  28. package/modules/hall-ui/public/founding-mark.js +1 -6
  29. package/modules/hall-ui/public/gate.html +2 -2
  30. package/modules/hall-ui/public/goal-map.html +2 -1
  31. package/modules/hall-ui/public/goals.html +2 -2
  32. package/modules/hall-ui/public/government.html +2 -2
  33. package/modules/hall-ui/public/hall-ask.js +3 -3
  34. package/modules/hall-ui/public/hall-kit.js +3 -5
  35. package/modules/hall-ui/public/hall-visual.js +3 -5
  36. package/modules/hall-ui/public/idea.html +2 -2
  37. package/modules/hall-ui/public/ideas.html +2 -2
  38. package/modules/hall-ui/public/index.html +2 -2
  39. package/modules/hall-ui/public/lightbulb.js +1 -5
  40. package/modules/hall-ui/public/md-reader.js +1 -8
  41. package/modules/hall-ui/public/modules.html +2 -2
  42. package/modules/hall-ui/public/my-agents.html +2 -2
  43. package/modules/hall-ui/public/my-agents.js +3 -1
  44. package/modules/hall-ui/public/page-notes.js +1 -5
  45. package/modules/hall-ui/public/paintbrush.js +1 -5
  46. package/modules/hall-ui/public/palette.js +1 -5
  47. package/modules/hall-ui/public/primer.html +2 -1
  48. package/modules/hall-ui/public/primer.js +1 -1
  49. package/modules/hall-ui/public/profile-crafts.js +3 -5
  50. package/modules/hall-ui/public/profile.html +2 -3
  51. package/modules/hall-ui/public/project-settings.html +2 -2
  52. package/modules/hall-ui/public/ranks.html +2 -3
  53. package/modules/hall-ui/public/roadmap.html +2 -2
  54. package/modules/hall-ui/public/roster.html +3 -4
  55. package/modules/hall-ui/public/sessions.html +2 -3
  56. package/modules/hall-ui/public/settings.html +2 -3
  57. package/modules/hall-ui/public/shell.js +1 -5
  58. package/modules/hall-ui/public/sky-data.js +1 -1
  59. package/modules/hall-ui/public/street.html +2 -1
  60. package/modules/hall-ui/public/studio.html +2 -2
  61. package/modules/hall-ui/public/task-detail.js +1 -6
  62. package/modules/hall-ui/public/task-lib.js +3 -8
  63. package/modules/hall-ui/public/task.html +2 -3
  64. package/modules/hall-ui/public/thinking.html +2 -1
  65. package/modules/hall-ui/public/tweak-editor.html +2 -2
  66. package/modules/hall-ui/public/vote-control.js +1 -1
  67. package/modules/hall-ui/public/watch.html +2 -3
  68. package/modules/hall-ui/public/welcome-boot.js +3 -5
  69. package/modules/hall-ui/public/work.html +2 -3
  70. package/modules/lifecycle/ship-card.js +1 -4
  71. package/modules/npm-release/preview/proxy.js +2 -3
  72. package/modules/npm-release/public/task-widget.js +1 -1
  73. package/modules/platform-identity/routes/sso.js +1 -3
  74. package/modules/provisioning/routes/callback-page.js +2 -6
  75. package/modules/public-landing/public/projects.html +3 -5
  76. package/modules/raise/raise.js +1 -1
  77. package/package-lock.json +2 -2
  78. package/package.json +1 -1
  79. package/release-notes.json +26 -12
  80. package/scripts/bongos/escape-helper-guard.js +147 -0
  81. package/scripts/bongos/fitness.js +3 -2
  82. package/scripts/bongos/key-card.js +1 -4
  83. package/scripts/bongos/review-sheet.js +1 -3
  84. package/scripts/bongos/start.js +1 -1
  85. package/scripts/bongos/status.js +1 -1
  86. package/scripts/gds/escape-helper-guard.js +6 -0
  87. package/scripts/status-mirror-sync.js +2 -3
  88. package/src/bongos/module-scope-map.js +1 -0
  89. package/src/bongos/platform-visibility-gate.js +2 -3
  90. package/src/bongos/prelaunch-gate.js +2 -3
  91. package/src/bongos/routes/auth.js +1 -5
  92. package/src/bongos/serve-internal.js +2 -6
  93. package/src/html-escape.js +26 -0
  94. package/src/module-api.js +9 -1
  95. package/tests/blockers_kit_pager.mjs +2 -2
  96. package/tests/blockers_page_ui.mjs +2 -2
  97. package/tests/core_move_notice.mjs +3 -1
  98. package/tests/core_update_banner.mjs +2 -0
  99. package/tests/core_upgrade_owner_door.mjs +2 -0
  100. package/tests/craft_mode.mjs +2 -0
  101. package/tests/craft_mode_nav.mjs +3 -0
  102. package/tests/craft_review_mode.mjs +3 -2
  103. package/tests/criterion_ref_status.mjs +7 -2
  104. package/tests/curve_kind_overlay.mjs +2 -1
  105. package/tests/founding_mode.mjs +10 -2
  106. package/tests/github_install_nudge.mjs +4 -1
  107. package/tests/goal_map_page.mjs +2 -2
  108. package/tests/government_protected_surfaces.mjs +15 -96
  109. package/tests/hall_approval_queue.mjs +2 -2
  110. package/tests/hall_city.mjs +1 -1
  111. package/tests/hall_fullscreen.mjs +2 -0
  112. package/tests/hall_gov_real_site.mjs +2 -0
  113. package/tests/hall_kit_charts.mjs +2 -1
  114. package/tests/hall_my_agents.mjs +3 -2
  115. package/tests/hall_nav.mjs +2 -0
  116. package/tests/hall_palette.mjs +2 -2
  117. package/tests/hall_reading_world.mjs +3 -1
  118. package/tests/hall_reference_world.mjs +2 -1
  119. package/tests/hall_signout.mjs +2 -0
  120. package/tests/hall_street.mjs +1 -1
  121. package/tests/hall_studio_world.mjs +3 -1
  122. package/tests/hall_tweak_editor.mjs +2 -1
  123. package/tests/helpers.mjs +17 -2
  124. package/tests/html_escape.mjs +149 -0
  125. package/tests/lightbulb.mjs +2 -0
  126. package/tests/module_api.mjs +1 -0
  127. package/tests/module_store_howto.mjs +2 -1
  128. package/tests/nav_permission_atoms.mjs +1 -1
  129. package/tests/npm_release_where.mjs +2 -1
  130. package/tests/page_notes_panel.mjs +4 -3
  131. package/tests/paintbrush.mjs +2 -1
  132. package/tests/profile_connections_ui.mjs +2 -1
  133. package/tests/profile_role_headline.mjs +2 -2
  134. package/tests/profile_role_tabs.mjs +2 -2
  135. package/tests/projects_hub_repo_private.mjs +4 -0
  136. package/tests/terms_acceptance.mjs +1 -0
  137. package/tests/welcome_boot_browser.mjs +1 -1
  138. package/modules/government/protected-surfaces.js +0 -88
@@ -0,0 +1,149 @@
1
+ // tests/html_escape.mjs — the ONE HTML escaper per side (task 1004876, goal 1000135).
2
+ //
3
+ // The escaper is what keeps a builder's own words (a task title, an idea) from
4
+ // becoming live markup on another builder's screen. ~45 private copies were
5
+ // collapsed onto two: window.OTB.escapeHtml (modules/hall-ui/public/dom-utils.js)
6
+ // in the browser and escapeHtml (src/html-escape.js, re-exported by the module
7
+ // doorway) on the server. These tests pin:
8
+ // 1. both shared escapers escape all five characters, map null/undefined to '',
9
+ // and agree with each other on every input;
10
+ // 2. the module doorway hands modules the very same function;
11
+ // 3. pages that used to carry a private copy now render user text through the
12
+ // shared one (a title with <, & and a quote stays text);
13
+ // 4. the fitness check goes red on a third definition, and on a hall page that
14
+ // loads dom-utils.js after (or deferred behind) the scripts that need it.
15
+
16
+ import { test } from 'node:test';
17
+ import assert from 'node:assert/strict';
18
+ import fs from 'node:fs';
19
+ import os from 'node:os';
20
+ import path from 'node:path';
21
+ import vm from 'node:vm';
22
+ import { createRequire } from 'node:module';
23
+ import { fileURLToPath } from 'node:url';
24
+
25
+ const require = createRequire(import.meta.url);
26
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
27
+ const PUB = path.join(ROOT, 'modules', 'hall-ui', 'public');
28
+
29
+ const { escapeHtml: serverEscape } = require('../src/html-escape.js');
30
+ const guard = require('../scripts/bongos/escape-helper-guard.js');
31
+
32
+ // Load the real dom-utils.js into a sandbox and hand back its window.OTB.
33
+ function browserOTB() {
34
+ const sandbox = { window: {}, document: { querySelector: () => null } };
35
+ vm.createContext(sandbox);
36
+ vm.runInContext(fs.readFileSync(path.join(PUB, 'dom-utils.js'), 'utf8'), sandbox, { filename: 'dom-utils.js' });
37
+ return sandbox.window.OTB;
38
+ }
39
+
40
+ const HOSTILE = `<img src=x onerror="alert('x')"> & "quoted" 'single' a&amp;b`;
41
+ const CASES = [HOSTILE, '', 'plain', 0, 42, -1.5, true, false, null, undefined, '&lt;', '<<>>', "it's", '"'];
42
+
43
+ test('server escaper escapes all five HTML characters', () => {
44
+ assert.equal(serverEscape(`<a href="x" title='y'>&</a>`),
45
+ '&lt;a href=&quot;x&quot; title=&#39;y&#39;&gt;&amp;&lt;/a&gt;');
46
+ });
47
+
48
+ test('server escaper: null/undefined render as empty text, numbers and booleans as their string', () => {
49
+ assert.equal(serverEscape(null), '');
50
+ assert.equal(serverEscape(undefined), '');
51
+ assert.equal(serverEscape(0), '0');
52
+ assert.equal(serverEscape(42), '42');
53
+ assert.equal(serverEscape(false), 'false');
54
+ });
55
+
56
+ test('server escaper escapes an ampersand first, so an existing entity is shown literally', () => {
57
+ assert.equal(serverEscape('&lt;'), '&amp;lt;');
58
+ });
59
+
60
+ test('browser and server escapers agree on every input', () => {
61
+ const OTB = browserOTB();
62
+ for (const c of CASES) assert.equal(OTB.escapeHtml(c), serverEscape(c), `input ${JSON.stringify(c)}`);
63
+ });
64
+
65
+ test('the module doorway exports the shared server escaper', () => {
66
+ const api = require('../src/module-api.js');
67
+ assert.equal(api.escapeHtml, serverEscape);
68
+ });
69
+
70
+ test('a hall helper that dropped its private copy renders user text through window.OTB', () => {
71
+ // hall-kit.js used to carry its own esc() (exported as OTBKit.esc); it must now delegate, at call time.
72
+ const sandbox = { window: {}, document: { querySelector: () => null } };
73
+ vm.createContext(sandbox);
74
+ vm.runInContext(fs.readFileSync(path.join(PUB, 'dom-utils.js'), 'utf8'), sandbox);
75
+ vm.runInContext(fs.readFileSync(path.join(PUB, 'hall-kit.js'), 'utf8'), sandbox);
76
+ const lib = sandbox.window.OTBKit;
77
+ assert.ok(lib && typeof lib.esc === 'function', 'hall-kit exposes esc');
78
+ assert.equal(lib.esc('Fix <b> & "q" \'s\''), 'Fix &lt;b&gt; &amp; &quot;q&quot; &#39;s&#39;');
79
+ assert.equal(lib.esc(null), '');
80
+ });
81
+
82
+ test('fitness: a third escape table anywhere in the code is a hard fail', () => {
83
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'esc-guard-'));
84
+ try {
85
+ fs.mkdirSync(path.join(tmp, 'src'), { recursive: true });
86
+ fs.mkdirSync(path.join(tmp, 'modules', 'x', 'public'), { recursive: true });
87
+ fs.mkdirSync(path.join(tmp, 'modules', 'x', 'tests'), { recursive: true });
88
+ fs.writeFileSync(path.join(tmp, 'src', 'html-escape.js'), "const T = { '<': '&lt;' };\n");
89
+ fs.writeFileSync(path.join(tmp, 'modules', 'x', 'tests', 't.mjs'), "const T = { '<': '&lt;' };\n");
90
+ let r = guard.checkOneHtmlEscaper({ root: tmp });
91
+ assert.equal(r.ok, true, r.violations.join('\n'));
92
+ // A renamed copy is still caught: the detector reads the table, not the name.
93
+ fs.writeFileSync(path.join(tmp, 'modules', 'x', 'public', 'page.js'),
94
+ "const hesc = (s) => String(s).replace(/</g, '&lt;');\n");
95
+ r = guard.checkOneHtmlEscaper({ root: tmp });
96
+ assert.equal(r.ok, false);
97
+ assert.equal(r.hardFail, true);
98
+ assert.match(r.violations[0], /modules\/x\/public\/page\.js:1/);
99
+ } finally { fs.rmSync(tmp, { recursive: true, force: true }); }
100
+ });
101
+
102
+ test('fitness: a delegate to the shared escaper is not a copy', () => {
103
+ assert.deepEqual(guard.findEscapeTables('function esc(s) { return window.OTB.escapeHtml(s); }'), []);
104
+ assert.deepEqual(guard.findEscapeTables("s.replace(/&lt;/g, '<')"), [], 'a DECODER is not an escaper');
105
+ assert.deepEqual(guard.findEscapeTables("x\n{ '<': \"&lt;\" }"), [2]);
106
+ });
107
+
108
+ test('fitness: a local escaper name that does not delegate is a hard fail (the old primer fallback)', () => {
109
+ const browser = { browser: true };
110
+ assert.deepEqual(guard.findFakeDelegates(" const escapeHtml = (MD && MD.escapeHtml) || ((s) => String(s));", browser), [],
111
+ 'names the shared one — but the line-level rule cannot see the fallback, so primer.js no longer has one');
112
+ assert.deepEqual(guard.findFakeDelegates(' const escapeHtml = (s) => String(s);', browser), [1]);
113
+ assert.deepEqual(guard.findFakeDelegates(' function esc(s) { return s; }', browser), [1]);
114
+ assert.deepEqual(guard.findFakeDelegates(' function esc(s) { return window.OTB.escapeHtml(s); }', browser), []);
115
+ assert.deepEqual(guard.findFakeDelegates(' var esc = kit.esc;', browser), []);
116
+ // server: `esc` is also a regex/markdown escaper name there, so only the HTML names are held
117
+ assert.deepEqual(guard.findFakeDelegates("const esc = (k) => k.replace(/[.*]/g, '\\\\$&');"), []);
118
+ assert.deepEqual(guard.findFakeDelegates('const hesc = (s) => String(s);'), [1]);
119
+ assert.deepEqual(guard.findFakeDelegates("const { escapeHtml: hesc } = require('../../src/html-escape');"), []);
120
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'esc-guard-'));
121
+ try {
122
+ fs.mkdirSync(path.join(tmp, 'modules', 'x', 'public'), { recursive: true });
123
+ fs.writeFileSync(path.join(tmp, 'modules', 'x', 'public', 'p.js'), 'const esc = (s) => String(s);\n');
124
+ const r = guard.checkOneHtmlEscaper({ root: tmp });
125
+ assert.equal(r.ok, false);
126
+ assert.match(r.violations[0], /p\.js:1 — defines a local HTML escaper name/);
127
+ } finally { fs.rmSync(tmp, { recursive: true, force: true }); }
128
+ });
129
+
130
+ test('fitness: the guard is registered in fitness.js CHECKS, so CI runs it', () => {
131
+ const fitness = require('../scripts/bongos/fitness.js');
132
+ const checks = fitness.CHECKS || fitness.checks;
133
+ assert.ok(Array.isArray(checks), 'fitness.js exports its CHECKS list');
134
+ assert.ok(checks.includes(guard.checkOneHtmlEscaper), 'checkOneHtmlEscaper is one of the fitness checks');
135
+ });
136
+
137
+ test('fitness: a hall page must load dom-utils.js first and synchronously', () => {
138
+ const ok = '<script src="/builders/dom-utils.js?v=1"></script><script src="/builders/shell.js"></script>';
139
+ assert.equal(guard.hallPageOrderProblem(ok), null);
140
+ assert.equal(guard.hallPageOrderProblem('<p>no scripts</p>'), null);
141
+ assert.match(guard.hallPageOrderProblem('<script src="/builders/shell.js"></script><script src="/builders/dom-utils.js"></script>'), /before/);
142
+ assert.match(guard.hallPageOrderProblem('<script src="/builders/dom-utils.js" defer></script><script src="/builders/shell.js"></script>'), /defer/);
143
+ assert.match(guard.hallPageOrderProblem('<script src="/builders/shell.js"></script>'), /never/);
144
+ });
145
+
146
+ test('fitness: the real tree holds exactly the two shared escapers', () => {
147
+ const r = guard.checkOneHtmlEscaper();
148
+ assert.equal(r.ok, true, r.violations.join('\n'));
149
+ });
@@ -17,6 +17,7 @@ import path from 'node:path';
17
17
  import vm from 'node:vm';
18
18
  import { createRequire } from 'node:module';
19
19
  import { fileURLToPath } from 'node:url';
20
+ import { realOTB } from './helpers.mjs';
20
21
 
21
22
  const require = createRequire(import.meta.url);
22
23
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
@@ -265,6 +266,7 @@ function boot({ storage = {}, throwing = false, respond } = {}) {
265
266
  ? { ok: true, status: 200, json: { ideas: [{ id: '5', title: '<b>Bold</b> idea', author_name: 'Bo', captured_at: '2026-10-01T00:00:00Z' }] } }
266
267
  : { ok: true, status: 201, json: { ideas: [{ id: '801', title: 'Sort drafts' }, { id: '802', title: 'Name writers' }], split_by: 'ai' } }));
267
268
  const win = {
269
+ OTB: realOTB(), // the page loads dom-utils.js first (task 1004876)
268
270
  location: { pathname: '/studio' },
269
271
  localStorage: store,
270
272
  fetch: (url, opts) => {
@@ -20,6 +20,7 @@ const seams = require(path.join(ROOT, 'src', 'module-seams.js'));
20
20
  // `withTx` was added; no enabled module at its pinned coreVersion lost a capability.
21
21
  const PUBLISHED_SURFACE = [
22
22
  'CORE_VERSION',
23
+ 'escapeHtml', // task 1004876: THE server-side HTML escaper (1.22.0) — modules render user text through it, never a private copy
23
24
  'requireBuilder', 'requireRank', 'rankMeetsThreshold', 'requireBfgPrincipal', 'allowBoxScope', 'resolveSession',
24
25
  'requirePermission', // BV1.R105: the authority-atom gate module routes migrate onto (1.19.0; ADR 0151)
25
26
  'requireActAlone', // added by task 1004657 (ADR 0363 D5): a building's own page asks its decision-rules row before one builder acts alone. On the doorway because eight buildings' pages live in six modules and core, and each must refuse the same way People does
@@ -16,6 +16,7 @@ import os from 'node:os';
16
16
  import path from 'node:path';
17
17
  import vm from 'node:vm';
18
18
  import { fileURLToPath } from 'node:url';
19
+ import { realOTB } from './helpers.mjs';
19
20
 
20
21
  const require = createRequire(import.meta.url);
21
22
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
@@ -173,7 +174,7 @@ test('a delisted module\'s how-to still reads for people who hold it, and only f
173
174
  // ---- the page: the author's markdown is untrusted input ----
174
175
 
175
176
  function loadReader() {
176
- const ctx = { window: {}, location: { href: 'https://builders.example.com/builders/modules', hostname: 'builders.example.com' }, URL };
177
+ const ctx = { window: { OTB: realOTB() }, location: { href: 'https://builders.example.com/builders/modules', hostname: 'builders.example.com' }, URL };
177
178
  vm.createContext(ctx);
178
179
  vm.runInContext(fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/md-reader.js'), 'utf8'), ctx);
179
180
  return ctx.window.OTBMdReader;
@@ -93,7 +93,7 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
93
93
  // says a held core version was tried and rolled back; task 1004731: the one-time
94
94
  // "Upgraded to core X" notice after a move; task 1004826: Standing is Accomplishments;
95
95
  // task 1004846: the new logo (its bump landed late, in task 1004852).
96
- const SHELL_SHA = '670b2f71085c13b6';
96
+ const SHELL_SHA = 'cea38eeab6b234cc';
97
97
 
98
98
  test('shell.js has not changed without its cache-bust being bumped', () => {
99
99
  const actual = crypto.createHash('sha256').update(SHELL).digest('hex').slice(0, 16);
@@ -18,7 +18,7 @@ import path from 'node:path';
18
18
  import vm from 'node:vm';
19
19
  import { fileURLToPath } from 'node:url';
20
20
  import { createRequire } from 'node:module';
21
- import { npmTarball, npmPackument, fakeHttpsGet } from './helpers.mjs';
21
+ import { npmTarball, npmPackument, fakeHttpsGet, realOTB } from './helpers.mjs';
22
22
 
23
23
  const require = createRequire(import.meta.url);
24
24
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
@@ -193,6 +193,7 @@ function bootWidget(answer, { shown = null } = {}) {
193
193
  const listeners = {};
194
194
  const asked = [];
195
195
  const windowObj = {
196
+ OTB: realOTB(), // the page loads dom-utils.js first (task 1004876)
196
197
  OTBTaskShown: shown,
197
198
  BongosClient: { createClient: () => ({ request: async (m, url) => { asked.push(url); return answer(url); } }) },
198
199
  };
@@ -13,6 +13,7 @@ import fs from 'node:fs';
13
13
  import path from 'node:path';
14
14
  import vm from 'node:vm';
15
15
  import { fileURLToPath } from 'node:url';
16
+ import { realOTB } from './helpers.mjs';
16
17
 
17
18
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
18
19
  const HALL = path.join(ROOT, 'modules/hall-ui/public');
@@ -59,7 +60,7 @@ function boot({ session = {}, search = '' } = {}) {
59
60
  };
60
61
  const mk = (obj) => ({ getItem: (k) => (k in obj ? obj[k] : null), setItem: (k, v) => { obj[k] = String(v); }, removeItem: (k) => { delete obj[k]; } });
61
62
  const opened = [];
62
- const window = { localStorage: mk({}), sessionStorage: mk(session), matchMedia: () => ({ matches: false }), location: { search } };
63
+ const window = { OTB: realOTB(), localStorage: mk({}), sessionStorage: mk(session), matchMedia: () => ({ matches: false }), location: { search } };
63
64
  // "Loading" page-notes.js: record the tag, then define the API it would define.
64
65
  head.onAppend = (tag) => {
65
66
  loaded.push(tag.src);
@@ -115,7 +116,7 @@ test('a ?note= link opens that doc on arrival, in any mode', async () => {
115
116
 
116
117
  // ---------- the reader ----------
117
118
  function reader() {
118
- const window = { location: { pathname: '/studio' } };
119
+ const window = { OTB: realOTB(), location: { pathname: '/studio' } };
119
120
  const document = { body: { dataset: { page: 'studio', pageTitle: 'Studio' } }, title: 'Studio — hall' };
120
121
  const sandbox = { window, document };
121
122
  vm.createContext(sandbox);
@@ -206,7 +207,7 @@ function dockedPanel({ reviewing = null, opened = false } = {}) {
206
207
  experiences: () => [{ id: 'ideating', label: 'Ideator experience', short: 'Ideating' }, { id: 'building', label: 'Building experience', short: 'Building' }],
207
208
  iconHtml: () => '<svg></svg>',
208
209
  };
209
- const window = { location: { pathname: '/studio' }, OTBCraftMode: craft };
210
+ const window = { OTB: realOTB(), location: { pathname: '/studio' }, OTBCraftMode: craft };
210
211
  const sandbox = { window, document, fetch, setTimeout, clearTimeout, Promise };
211
212
  vm.createContext(sandbox);
212
213
  vm.runInContext(NOTES, sandbox, { filename: 'page-notes.js' });
@@ -17,6 +17,7 @@ import fs from 'node:fs';
17
17
  import path from 'node:path';
18
18
  import vm from 'node:vm';
19
19
  import { fileURLToPath } from 'node:url';
20
+ import { realOTB } from './helpers.mjs';
20
21
 
21
22
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
22
23
  const HALL = path.join(ROOT, 'modules/hall-ui/public');
@@ -157,7 +158,7 @@ function boot({ round = null, draft = null, claim = { status: 200, body: { ok: t
157
158
  return respond(404, {});
158
159
  };
159
160
  const winListeners = {};
160
- const window = { location: { pathname: '/roadmap' }, addEventListener: (t, fn) => { (winListeners[t] ||= []).push(fn); }, removeEventListener: (t, fn) => { winListeners[t] = (winListeners[t] || []).filter((f) => f !== fn); } };
161
+ const window = { OTB: realOTB(), location: { pathname: '/roadmap' }, addEventListener: (t, fn) => { (winListeners[t] ||= []).push(fn); }, removeEventListener: (t, fn) => { winListeners[t] = (winListeners[t] || []).filter((f) => f !== fn); } };
161
162
  const sandbox = { window, document, fetch, Promise, setTimeout, clearTimeout, JSON, Math, String, Array, Object };
162
163
  window.window = window;
163
164
  vm.createContext(sandbox);
@@ -58,6 +58,7 @@ import vm from 'node:vm';
58
58
  import { readFileSync } from 'node:fs';
59
59
  import { fileURLToPath } from 'node:url';
60
60
  import { dirname, join } from 'node:path';
61
+ import { realOTB } from './helpers.mjs';
61
62
  // The REAL predicates, not a copy. accountFloor() below used to spell these
62
63
  // itself, which made it a hand-maintained mirror of account-visibility.js —
63
64
  // and it drifted the moment ADR 0213 made consent two columns. Imported, the
@@ -498,7 +499,7 @@ function boot({ transport, hub = HUB } = {}) {
498
499
  window: {
499
500
  OTBAsk: require('../modules/hall-ui/public/hall-ask.js'),
500
501
  BongosClient: { createClient: () => api },
501
- OTB: { $, escapeHtml, fmtDate: (d) => String(d).slice(0, 10), toast: (m) => toasts.push(m), hubOrigin: () => hub },
502
+ OTB: { ...realOTB(), $, escapeHtml, fmtDate: (d) => String(d).slice(0, 10), toast: (m) => toasts.push(m), hubOrigin: () => hub },
502
503
  OTBKit: {
503
504
  makePager: (prefix, key) => ({ prefix, key, page: 1, perPage: 25 }),
504
505
  makeLedger: ({ mount, rowHtml, emptyMsg }) => ({
@@ -20,7 +20,7 @@ import fs from 'node:fs';
20
20
  import path from 'node:path';
21
21
  import vm from 'node:vm';
22
22
  import { fileURLToPath } from 'node:url';
23
- import { makeRunner } from './helpers.mjs';
23
+ import { makeRunner, realOTB } from './helpers.mjs';
24
24
 
25
25
  process.env.NODE_ENV = 'test';
26
26
  const require = createRequire(import.meta.url);
@@ -129,7 +129,7 @@ async function boot(profileData) {
129
129
  // The real kit draws the surface bars; the section rail needs a real DOM and
130
130
  // is not what this suite is about, so it is left out (profile.js skips it).
131
131
  OTBKit: { ...OTBKit, makeSectionRail: undefined },
132
- OTB: {
132
+ OTB: { ...realOTB(),
133
133
  $, escapeHtml,
134
134
  fmtNum: (n) => Number(n || 0).toLocaleString('en-US'),
135
135
  fmtDate: (d) => String(d).slice(0, 10),
@@ -22,7 +22,7 @@ import os from 'node:os';
22
22
  import path from 'node:path';
23
23
  import vm from 'node:vm';
24
24
  import { fileURLToPath } from 'node:url';
25
- import { makeRunner, bootProfilePage, settle } from './helpers.mjs';
25
+ import { makeRunner, bootProfilePage, settle, realOTB } from './helpers.mjs';
26
26
 
27
27
  process.env.NODE_ENV = 'test';
28
28
  const require = createRequire(import.meta.url);
@@ -95,7 +95,7 @@ await test('the route: signed in only, a bad id refused, paged, and it asks for
95
95
  // ---- 2. the ideator's trail -------------------------------------------------
96
96
 
97
97
  function loadCrafts() {
98
- const sandbox = { window: {}, console };
98
+ const sandbox = { window: { OTB: realOTB() }, console };
99
99
  sandbox.globalThis = sandbox;
100
100
  vm.createContext(sandbox);
101
101
  for (const f of ['idea-objects.js', 'profile-crafts.js']) {
@@ -17,6 +17,7 @@
17
17
  import { strict as assert } from 'node:assert';
18
18
  import { readFileSync } from 'node:fs';
19
19
  import vm from 'node:vm';
20
+ import { realOTB } from './helpers.mjs';
20
21
 
21
22
  const HUB = readFileSync(new URL('../modules/public-landing/public/projects.html', import.meta.url), 'utf8').replace(/\r\n/g, '\n');
22
23
 
@@ -30,6 +31,8 @@ async function ta(name, fn) {
30
31
  function grab(head) {
31
32
  const at = HUB.indexOf(`\n ${head}`);
32
33
  assert.ok(at >= 0, `${head} must exist`);
34
+ const line = HUB.slice(at, HUB.indexOf('\n', at + 1));
35
+ if (/\}\s*$/.test(line)) return line; // a one-line declaration (esc, task 1004876)
33
36
  const end = HUB.indexOf('\n }', at + 1);
34
37
  const close = HUB.slice(end, end + 5) === '\n };' ? end + 5 : end + 4;
35
38
  return HUB.slice(at, close);
@@ -50,6 +53,7 @@ function world({ reply = RECEIPT } = {}) {
50
53
  API: '/api/bongos', manageId: '18', manageInst: null, manageRepo: null,
51
54
  mRepoPrivSeen: false, mRepoPrivState: '',
52
55
  $: (id) => els[id],
56
+ window: { OTB: realOTB() }, // the page loads dom-utils.js first (task 1004876)
53
57
  document: { querySelector: () => null },
54
58
  jsonOrEmpty: (res) => res.json(),
55
59
  apiError: (data) => (data && data.error) || {},
@@ -400,6 +400,7 @@ require.cache[moduleApiPath] = {
400
400
  resolveEnv: () => null,
401
401
  userAgent: () => 'cloud-bongos-tests',
402
402
  LIVE_RANK_LADDER: ['xenos', 'thetes', 'metic', 'archon'],
403
+ escapeHtml: require('../src/html-escape.js').escapeHtml, // the doorway's one escaper (task 1004876)
403
404
  },
404
405
  };
405
406
 
@@ -33,7 +33,7 @@ const INDEX = fs.readFileSync(path.join(HALL, 'index.html'), 'utf8')
33
33
  .replace(/<script\b[^>]*>[\s\S]*?<\/script>/g, '')
34
34
  .replace(/\{\{[^}]*\}\}/g, '');
35
35
  const STUB = '<script>window.__BRANDING__ = { identity: { worldName: "Harbor" } }; window.BongosClient = { createClient: () => ({ request: async () => ({ ok: false, status: 404, data: null }) }) };</script>';
36
- const SCRIPTS = ['shell-trail.js', 'craft-mode.js', 'welcome-boot.js', 'shell.js']
36
+ const SCRIPTS = ['dom-utils.js', 'shell-trail.js', 'craft-mode.js', 'welcome-boot.js', 'shell.js'] // dom-utils first, as on the real page (task 1004876)
37
37
  .map((f) => `<script src="/builders/${f}"></script>`).join('');
38
38
  const PAGE = INDEX.replace('</body>', STUB + SCRIPTS + '</body>');
39
39
 
@@ -1,88 +0,0 @@
1
- 'use strict';
2
-
3
- // modules/government/protected-surfaces.js — the protected-surface registry's
4
- // GOVERNMENT-side API (ADR 0151 §4, BV1.R101).
5
- //
6
- // POLICY OWNS / SUBSTRATE ENFORCES — the whole point of this slice.
7
- //
8
- // GOVERNMENT OWNS THE DATA. protected-surfaces.json (next to this file) says
9
- // WHICH repo paths are protected, at WHAT rank floor, mapped to WHICH
10
- // `system: true` catalog permission. That file is the source of truth; the
11
- // Archon Government tab (R110/R111) edits it as policy. Before R101 the same
12
- // information was a hardcoded PROTECTED_GLOBS constant inside the kernel.
13
- //
14
- // THE OUT-OF-BAND SUBSTRATE STAYS THE ENFORCER. Nothing here enforces
15
- // anything. The enforcers are the git pre-push hook, the grader pre-pass, the
16
- // post-push main audit and the goal-scope check — they read
17
- // the registry with no DB, no server and no HTTP, and they are what actually
18
- // blocks a sub-floor change (ADR 0043). Re-mapping a floor here changes what
19
- // the substrate protects; it can never make the request path the authority.
20
- // Moving enforcement into the app is the regression R101 exists to prevent
21
- // (ADR 0151 §"Alternatives considered").
22
- //
23
- // NO DB DEPENDENCY. A DB-backed override is a government-tab concern layered
24
- // on top and must never become a dependency of the hook — the hook runs on a
25
- // laptop with no database and no network. This module is therefore
26
- // synchronous, DB-free and requires nothing but the catalog and the doorway.
27
- //
28
- // DEPENDENCY DIRECTION (ADR 0083's one-way rule). Code flows exactly one way:
29
- // this module → src/module-api.js (the published doorway) → the kernel matcher
30
- // in src/bongos/permission-path-check.js. Core never requires this module. The
31
- // kernel loads the JSON data file directly (a static data read, the same way the
32
- // sibling trust primitive route-rank-check.js reads modules/*/routes/*.js off
33
- // disk to rank-gate them), and we read the parsed result back through the
34
- // doorway so there is exactly ONE loader and ONE matcher — no second list to
35
- // drift (the SEC #863 failure mode).
36
-
37
- const api = require('../../src/module-api');
38
- const catalog = require('./catalog');
39
-
40
- const ppc = api.permissionPathCheck;
41
-
42
- // The registry, as parsed by the single kernel loader.
43
- const SURFACES = ppc.PROTECTED_SURFACES;
44
-
45
- // ── invariants asserted at module load (fail loud on an authoring typo) ───────
46
- // The government module is where the registry meets the permission catalog, so
47
- // this is where "every protected surface maps to a real, non-grantable
48
- // permission" is checked. A surface mapping to an unknown key, or to a
49
- // `system: false` (grantable) key, would mean an Archon could hand a custom rank
50
- // the power the substrate is supposed to reserve — exactly the wall ADR 0151 §4
51
- // builds. Fail loud rather than silently protecting nothing.
52
- for (const s of SURFACES) {
53
- const perm = catalog.byKey(s.permission);
54
- if (!perm) {
55
- throw new Error(`protected-surface registry: "${s.glob}" maps to "${s.permission}", which the permission catalog does not define`);
56
- }
57
- if (perm.system !== true) {
58
- throw new Error(`protected-surface registry: "${s.glob}" maps to "${s.permission}", which is not system:true — a protected surface must map to a NON-GRANTABLE permission (ADR 0151 §4)`);
59
- }
60
- if (!catalog.RANK_ORDER.includes(s.floor)) {
61
- throw new Error(`protected-surface registry: "${s.glob}" declares floor "${s.floor}", which is not a rank`);
62
- }
63
- }
64
-
65
- // The lowest floor any surface declares — 'metic' today (ADR 0043's Metic floor,
66
- // now DERIVED from registry data rather than hardcoded).
67
- const FLOOR = catalog.RANK_ORDER[Math.min(...SURFACES.map((s) => catalog.RANK_ORDER.indexOf(s.floor)))];
68
-
69
- // The distinct system permissions the registry maps onto.
70
- const SYSTEM_PERMISSIONS = Object.freeze([...new Set(SURFACES.map((s) => s.permission))].sort());
71
-
72
- // Per-path lookups — the single matcher, reached through the doorway.
73
- const surfaceFor = (file) => ppc.surfaceFor(file);
74
- const floorFor = (file) => ppc.floorFor(file);
75
- const permissionFor = (file) => ppc.permissionFor(file);
76
- const isProtected = (file) => ppc.surfaceFor(file) !== null;
77
-
78
- module.exports = {
79
- SURFACES,
80
- PROTECTED_GLOBS: ppc.PROTECTED_GLOBS,
81
- REGISTRY_PATH: ppc.REGISTRY_PATH,
82
- FLOOR,
83
- SYSTEM_PERMISSIONS,
84
- surfaceFor,
85
- floorFor,
86
- permissionFor,
87
- isProtected,
88
- };