@bongos/core 1.21.182 → 1.22.2

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 (247) hide show
  1. package/.bongos-core.json +273 -248
  2. package/docs/copy-inventory.md +165 -165
  3. package/docs/copy-registry.json +184 -184
  4. package/docs/module-api-changelog.md +12 -0
  5. package/docs/page-inventory.json +35 -26
  6. package/docs/page-readings.json +389 -388
  7. package/modules/agents/lib/answer-hold.js +1 -1
  8. package/modules/agents/lib/fire-budget.js +1 -1
  9. package/modules/autonomy/cadence.js +3 -3
  10. package/modules/autonomy/config-idle.js +1 -1
  11. package/modules/autonomy/fence.js +1 -1
  12. package/modules/autonomy/gauge.js +2 -2
  13. package/modules/autonomy/governor-docket.js +0 -1
  14. package/modules/autonomy/server-schedule.js +1 -1
  15. package/modules/builder-settings/builder-needs.js +1 -3
  16. package/modules/builder-settings/event-sound-prefs.js +0 -1
  17. package/modules/builder-settings/law-locks.js +0 -1
  18. package/modules/builder-settings/model-alloc-prefs.js +0 -5
  19. package/modules/builder-settings/profile-nudge-prefs.js +0 -1
  20. package/modules/builder-settings/render-prefs.js +0 -1
  21. package/modules/builder-settings/skill-prefs.js +0 -2
  22. package/modules/builder-settings/sound-prefs.js +0 -3
  23. package/modules/builder-settings/wandering-prefs.js +0 -2
  24. package/modules/copy-desk/artist-stats.js +1 -1
  25. package/modules/copy-desk/flags.js +1 -1
  26. package/modules/copy-desk/guarded-lines.js +1 -1
  27. package/modules/copy-desk/page-status.js +0 -5
  28. package/modules/copy-desk/proposals.js +0 -2
  29. package/modules/copy-desk/registry.js +1 -1
  30. package/modules/copy-desk/stuck-tweaks.js +1 -1
  31. package/modules/economy/credits-by-craft.js +0 -2
  32. package/modules/economy/streak.js +0 -3
  33. package/modules/government/asks-db.js +1 -1
  34. package/modules/government/asks.js +0 -1
  35. package/modules/government/decision-board.js +0 -1
  36. package/modules/government/docket.js +0 -6
  37. package/modules/government/leads.js +0 -1
  38. package/modules/government/positions.js +1 -1
  39. package/modules/government/recommendations.js +0 -3
  40. package/modules/government/routing.js +1 -1
  41. package/modules/government/treasury-db.js +1 -1
  42. package/modules/government/treasury.js +2 -2
  43. package/modules/grading/grade-engine.js +0 -1
  44. package/modules/hall-ui/public/approval-queue.js +3 -3
  45. package/modules/hall-ui/public/atlas.html +2 -1
  46. package/modules/hall-ui/public/atlas.js +1 -1
  47. package/modules/hall-ui/public/blockers.html +3 -4
  48. package/modules/hall-ui/public/board-room.html +2 -2
  49. package/modules/hall-ui/public/brand-holes.js +3 -3
  50. package/modules/hall-ui/public/city.html +2 -1
  51. package/modules/hall-ui/public/collab.html +2 -2
  52. package/modules/hall-ui/public/commands.html +2 -2
  53. package/modules/hall-ui/public/commands.js +1 -2
  54. package/modules/hall-ui/public/copy-desk.html +2 -2
  55. package/modules/hall-ui/public/core-move-notice.js +1 -5
  56. package/modules/hall-ui/public/craft-mode.js +1 -5
  57. package/modules/hall-ui/public/criterion-uat-ui.js +3 -3
  58. package/modules/hall-ui/public/deploy.html +2 -2
  59. package/modules/hall-ui/public/diagrams.html +2 -1
  60. package/modules/hall-ui/public/dom-utils.js +15 -6
  61. package/modules/hall-ui/public/drachmae.html +2 -1
  62. package/modules/hall-ui/public/fleet-costs.js +3 -2
  63. package/modules/hall-ui/public/fleet.html +2 -2
  64. package/modules/hall-ui/public/founding-mark.js +1 -6
  65. package/modules/hall-ui/public/gate.html +2 -2
  66. package/modules/hall-ui/public/goal-map.html +2 -1
  67. package/modules/hall-ui/public/goals.html +2 -2
  68. package/modules/hall-ui/public/government.html +2 -2
  69. package/modules/hall-ui/public/hall-ask.js +3 -3
  70. package/modules/hall-ui/public/hall-kit.js +3 -5
  71. package/modules/hall-ui/public/hall-visual.js +3 -5
  72. package/modules/hall-ui/public/idea.html +2 -2
  73. package/modules/hall-ui/public/ideas.html +2 -2
  74. package/modules/hall-ui/public/index.html +2 -2
  75. package/modules/hall-ui/public/lightbulb.js +1 -5
  76. package/modules/hall-ui/public/md-reader.js +1 -8
  77. package/modules/hall-ui/public/modules.html +2 -2
  78. package/modules/hall-ui/public/my-agents.html +2 -2
  79. package/modules/hall-ui/public/my-agents.js +3 -1
  80. package/modules/hall-ui/public/page-notes.js +1 -5
  81. package/modules/hall-ui/public/paintbrush.js +1 -5
  82. package/modules/hall-ui/public/palette.js +1 -5
  83. package/modules/hall-ui/public/primer.html +2 -1
  84. package/modules/hall-ui/public/primer.js +1 -1
  85. package/modules/hall-ui/public/profile-crafts.js +3 -5
  86. package/modules/hall-ui/public/profile.html +2 -3
  87. package/modules/hall-ui/public/project-settings.html +2 -2
  88. package/modules/hall-ui/public/ranks.html +2 -3
  89. package/modules/hall-ui/public/roadmap.html +2 -2
  90. package/modules/hall-ui/public/roster.html +3 -4
  91. package/modules/hall-ui/public/sessions.html +2 -3
  92. package/modules/hall-ui/public/settings.html +2 -3
  93. package/modules/hall-ui/public/shell.js +1 -5
  94. package/modules/hall-ui/public/sky-data.js +1 -1
  95. package/modules/hall-ui/public/street.html +2 -1
  96. package/modules/hall-ui/public/studio.html +2 -2
  97. package/modules/hall-ui/public/task-detail.js +1 -6
  98. package/modules/hall-ui/public/task-lib.js +3 -8
  99. package/modules/hall-ui/public/task.html +2 -3
  100. package/modules/hall-ui/public/thinking.html +2 -1
  101. package/modules/hall-ui/public/tweak-editor.html +2 -2
  102. package/modules/hall-ui/public/vote-control.js +1 -1
  103. package/modules/hall-ui/public/watch.html +2 -3
  104. package/modules/hall-ui/public/welcome-boot.js +3 -5
  105. package/modules/hall-ui/public/work.html +2 -3
  106. package/modules/ideas/completeness.js +1 -1
  107. package/modules/ideas/descent.js +0 -1
  108. package/modules/ideas/governor-docket.js +0 -2
  109. package/modules/ideas/idea-state.js +0 -1
  110. package/modules/ideas/ideator-stats.js +1 -1
  111. package/modules/ideas/votes.js +1 -1
  112. package/modules/lifecycle/autopickup-files.js +0 -3
  113. package/modules/lifecycle/conflict-resolve.js +0 -1
  114. package/modules/lifecycle/criterion-uat-db.js +0 -4
  115. package/modules/lifecycle/dead-deps.js +2 -2
  116. package/modules/lifecycle/est-advisory.js +1 -1
  117. package/modules/lifecycle/goal-category.js +0 -3
  118. package/modules/lifecycle/goal-spec-task.js +1 -1
  119. package/modules/lifecycle/governor-docket.js +0 -1
  120. package/modules/lifecycle/kickoff-seed.js +1 -1
  121. package/modules/lifecycle/mingle.js +3 -3
  122. package/modules/lifecycle/page-tweak-claim.js +1 -1
  123. package/modules/lifecycle/rebase-gate.js +1 -1
  124. package/modules/lifecycle/removal-derivation.js +0 -1
  125. package/modules/lifecycle/routes/abandon-handler.js +1 -1
  126. package/modules/lifecycle/routes/version-route-authz.js +0 -1
  127. package/modules/lifecycle/ship-card.js +2 -5
  128. package/modules/lifecycle/ship-preflight.js +0 -1
  129. package/modules/lifecycle/sky.js +1 -1
  130. package/modules/lifecycle/task-classifier.js +0 -1
  131. package/modules/lifecycle/task-visuals.js +0 -2
  132. package/modules/lifecycle/version-close.js +0 -1
  133. package/modules/memory/memory.js +0 -1
  134. package/modules/memory/search-chunker.js +1 -1
  135. package/modules/memory/search.js +0 -3
  136. package/modules/npm-release/preview/divert.js +1 -1
  137. package/modules/npm-release/preview/proxy.js +2 -3
  138. package/modules/npm-release/preview/runtime.js +1 -1
  139. package/modules/npm-release/public/task-widget.js +1 -1
  140. package/modules/onboarding/founding-builders.js +1 -1
  141. package/modules/onboarding/founding-mode.js +1 -1
  142. package/modules/onboarding/howto-guide.js +1 -1
  143. package/modules/onboarding/howto-state.js +1 -1
  144. package/modules/onboarding/onboarding-state.js +0 -4
  145. package/modules/onboarding/onboarding.js +0 -6
  146. package/modules/page-notes/notes.js +0 -1
  147. package/modules/platform-identity/community-leaderboard.js +0 -1
  148. package/modules/platform-identity/hub-keys.js +0 -2
  149. package/modules/platform-identity/instance-feedback.js +0 -1
  150. package/modules/platform-identity/read-rate-limit.js +1 -1
  151. package/modules/platform-identity/routes/sso.js +1 -3
  152. package/modules/platform-identity/visibility-pull.js +0 -9
  153. package/modules/provisioning/app-status.js +1 -1
  154. package/modules/provisioning/catalog-bridge.js +0 -1
  155. package/modules/provisioning/core-upgrade.js +1 -1
  156. package/modules/provisioning/credential-preflight.js +1 -1
  157. package/modules/provisioning/demo.js +2 -2
  158. package/modules/provisioning/look.js +1 -1
  159. package/modules/provisioning/routes/callback-page.js +3 -7
  160. package/modules/public-landing/public/projects.html +3 -5
  161. package/modules/raise/raise.js +2 -2
  162. package/modules/render-deploy/deploys.js +1 -1
  163. package/modules/security/artifact-scan/aggregate.js +0 -4
  164. package/modules/security/artifact-scan/detectors/capabilities.js +1 -1
  165. package/modules/security/artifact-scan/detectors/config-obfuscation.js +1 -1
  166. package/modules/security/artifact-scan/detectors/credentials.js +1 -1
  167. package/modules/security/artifact-scan/detectors/dependencies.js +1 -1
  168. package/modules/security/artifact-scan/detectors/injection.js +1 -1
  169. package/modules/security/artifact-scan/detectors/network.js +1 -1
  170. package/modules/security/artifact-scan/detectors/npm-lifecycle.js +1 -1
  171. package/modules/security/artifact-scan/detectors/permissions.js +1 -1
  172. package/modules/security/artifact-scan/detectors/shared.js +0 -3
  173. package/modules/security/artifact-scan/floor.js +0 -3
  174. package/modules/security/artifact-scan/mirror.js +1 -1
  175. package/modules/security/artifact-scan/quarantine.js +0 -1
  176. package/modules/security/artifact-scan/rebuild-spec.js +0 -2
  177. package/modules/security/artifact-scan/report.js +1 -1
  178. package/modules/security/artifact-scan/sanitize.js +0 -2
  179. package/modules/security/secret-scrub.js +0 -4
  180. package/modules/security/security.js +0 -2
  181. package/modules/sessions/accessibility.js +0 -2
  182. package/modules/sessions/bfg.js +0 -4
  183. package/modules/sessions/upload-health.js +0 -3
  184. package/modules/specialities/db.js +1 -1
  185. package/modules/specialities/specialities.js +1 -1
  186. package/modules/ui-design/kit/lib.js +2 -2
  187. package/package-lock.json +2 -2
  188. package/package.json +1 -1
  189. package/release-notes.json +26 -12
  190. package/scripts/bongos/escape-helper-guard.js +147 -0
  191. package/scripts/bongos/fitness.js +3 -2
  192. package/scripts/bongos/key-card.js +1 -4
  193. package/scripts/bongos/review-sheet.js +1 -3
  194. package/scripts/bongos/start.js +1 -1
  195. package/scripts/bongos/status.js +1 -1
  196. package/scripts/gds/escape-helper-guard.js +6 -0
  197. package/scripts/status-mirror-sync.js +2 -3
  198. package/src/bongos/module-scope-map.js +1 -0
  199. package/src/bongos/platform-visibility-gate.js +2 -3
  200. package/src/bongos/prelaunch-gate.js +2 -3
  201. package/src/bongos/routes/auth.js +1 -5
  202. package/src/bongos/serve-internal.js +2 -6
  203. package/src/html-escape.js +26 -0
  204. package/src/module-api.js +9 -1
  205. package/tests/blockers_kit_pager.mjs +2 -2
  206. package/tests/blockers_page_ui.mjs +2 -2
  207. package/tests/core_move_notice.mjs +3 -1
  208. package/tests/core_update_banner.mjs +2 -0
  209. package/tests/core_upgrade_owner_door.mjs +2 -0
  210. package/tests/craft_mode.mjs +2 -0
  211. package/tests/craft_mode_nav.mjs +3 -0
  212. package/tests/craft_review_mode.mjs +3 -2
  213. package/tests/criterion_ref_status.mjs +7 -2
  214. package/tests/curve_kind_overlay.mjs +2 -1
  215. package/tests/founding_mode.mjs +10 -2
  216. package/tests/github_install_nudge.mjs +4 -1
  217. package/tests/goal_map_page.mjs +2 -2
  218. package/tests/hall_approval_queue.mjs +2 -2
  219. package/tests/hall_city.mjs +1 -1
  220. package/tests/hall_fullscreen.mjs +2 -0
  221. package/tests/hall_gov_real_site.mjs +2 -0
  222. package/tests/hall_kit_charts.mjs +2 -1
  223. package/tests/hall_my_agents.mjs +3 -2
  224. package/tests/hall_nav.mjs +2 -0
  225. package/tests/hall_palette.mjs +2 -2
  226. package/tests/hall_reading_world.mjs +3 -1
  227. package/tests/hall_reference_world.mjs +2 -1
  228. package/tests/hall_signout.mjs +2 -0
  229. package/tests/hall_street.mjs +1 -1
  230. package/tests/hall_studio_world.mjs +3 -1
  231. package/tests/hall_tweak_editor.mjs +2 -1
  232. package/tests/helpers.mjs +17 -2
  233. package/tests/html_escape.mjs +149 -0
  234. package/tests/lightbulb.mjs +2 -0
  235. package/tests/module_api.mjs +1 -0
  236. package/tests/module_store_howto.mjs +2 -1
  237. package/tests/modules_external_export_contract.mjs +48 -0
  238. package/tests/nav_permission_atoms.mjs +1 -1
  239. package/tests/npm_release_where.mjs +2 -1
  240. package/tests/page_notes_panel.mjs +4 -3
  241. package/tests/paintbrush.mjs +2 -1
  242. package/tests/profile_connections_ui.mjs +2 -1
  243. package/tests/profile_role_headline.mjs +2 -2
  244. package/tests/profile_role_tabs.mjs +2 -2
  245. package/tests/projects_hub_repo_private.mjs +4 -0
  246. package/tests/terms_acceptance.mjs +1 -0
  247. package/tests/welcome_boot_browser.mjs +1 -1
@@ -24,9 +24,7 @@ const path = require('node:path');
24
24
  const { pathToFileURL } = require('node:url');
25
25
  const { spawn } = require('node:child_process');
26
26
 
27
- function escapeHtml(s) {
28
- return String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
29
- }
27
+ const { escapeHtml } = require('../../src/html-escape'); // the one server escaper (task 1004876)
30
28
 
31
29
  // parseArgs — the flags above; `shots` keep their order. A --shot with no "="
32
30
  // is an error, not a guess: the caption and the path cannot be told apart.
@@ -268,7 +268,7 @@ function claimableTable(rows, rewardFromServer, goals) {
268
268
  // the same card every time, app-native. Falls back to the markdown card when
269
269
  // the client can't draw widgets. Design system: flat, 0.5px borders, CSS
270
270
  // theme vars (light/dark), Tabler icons, no emoji.
271
- const hesc = (s) => String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
271
+ const { escapeHtml: hesc } = require('../../src/html-escape'); // the one server escaper (task 1004876); it also escapes '
272
272
  const jsq = (s) => String(s == null ? '' : s).replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, ' ');
273
273
 
274
274
  function miniBar(pct) {
@@ -223,7 +223,7 @@ function clabel(c) {
223
223
  // builder-start card (flat, 0.5px borders, CSS theme vars, Tabler icons, no
224
224
  // emoji); classes are `.st-*` so they can't collide with builder-start's `.bs-*`
225
225
  // or the ship card's `.cc-*`.
226
- const sesc = (s) => String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
226
+ const { escapeHtml: sesc } = require('../../src/html-escape'); // the one server escaper (task 1004876); it also escapes '
227
227
 
228
228
  // A value going inside a single-quoted JS string inside an HTML attribute needs
229
229
  // BOTH escapes, in that order. Task ids are digits so the existing handlers get
@@ -0,0 +1,6 @@
1
+ // scripts/gds/escape-helper-guard.js — @bongos-forwarder (task 1004848). Generated by
2
+ // scripts/bongos/codemod-rename-scripts-gds.js; do not edit.
3
+ // The real script is scripts/bongos/escape-helper-guard.js. This forwarder keeps callers that still name
4
+ // the old path working, and warns once so they get repointed. Task 1003706 removes it.
5
+ 'use strict';
6
+ require('../bongos/forwarder.js').forward(module, 'escape-helper-guard.js');
@@ -49,6 +49,7 @@ const { execFileSync } = require('node:child_process');
49
49
  const REPO_ROOT = path.resolve(__dirname, '..');
50
50
  const SRC = path.join(REPO_ROOT, 'modules', 'status-ui', 'public');
51
51
  const { branding, clientBranding, applyBrandConditionals } = require('../src/branding');
52
+ const { escapeHtml } = require('../src/html-escape');
52
53
 
53
54
  // Stamp instance-identity tokens into the mirror HTML. The off-box mirror is
54
55
  // served by a static Pages host, NOT the branding-injecting Node server — so this
@@ -64,9 +65,7 @@ function brandTokens(html, brand = {}, publicOrigin = '', buildersOrigin = '') {
64
65
  const cur = (brand.currency && brand.currency.label) || 'credits';
65
66
  const Cur = cur ? cur.charAt(0).toUpperCase() + cur.slice(1) : cur;
66
67
  const company = (brand.identity && brand.identity.company) || {};
67
- const esc = (s) => String(s == null ? '' : s)
68
- .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
69
- .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
68
+ const esc = escapeHtml; // the one server escaper (task 1004876)
70
69
  return applyBrandConditionals(html, { companyName: company.name, companySite: company.site, companyTagline: company.tagline })
71
70
  .split('{{worldName}}').join(esc(world))
72
71
  .split('{{productName}}').join(esc(product))
@@ -464,6 +464,7 @@ const MODULE_GLOBS = {
464
464
  'scripts/bongos/route-shadow-guard.js', 'scripts/bongos/routine-schedule.js',
465
465
  'scripts/bongos/seam-wiring-guard.js', 'scripts/bongos/skill-preflight.js',
466
466
  'scripts/bongos/test-path-guard.js', 'scripts/bongos/version-literal-guard.js', 'scripts/bongos/dev-box-guard.js',
467
+ 'scripts/bongos/escape-helper-guard.js', // task 1004876
467
468
 
468
469
  // ADOPTED (task 1004111): the enforcement surfaces this module already IS.
469
470
  // install-git-hooks.js + scripts/bongos/hooks/ are the pre-push hook beside the
@@ -177,9 +177,8 @@ function relativeReturn(raw) {
177
177
  return raw === '/' ? null : raw;
178
178
  }
179
179
 
180
- function escAttr(s) {
181
- return String(s).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
182
- }
180
+ // The one server escaper (task 1004876); it also escapes ' , which this copy did not.
181
+ const { escapeHtml: escAttr } = require('../html-escape');
183
182
 
184
183
  // The door. Self-contained on purpose (the pre-launch gate's shape): it is
185
184
  // served to someone who may not see the project, so it pulls no stylesheet, no
@@ -180,9 +180,8 @@ function relativeReturn(raw) {
180
180
  return raw;
181
181
  }
182
182
 
183
- function escAttr(s) {
184
- return String(s).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
185
- }
183
+ // The one server escaper (task 1004876); it also escapes ' , which this copy did not.
184
+ const { escapeHtml: escAttr } = require('../html-escape');
186
185
 
187
186
  // The form. Self-contained and token-free on purpose: it is served BEFORE the
188
187
  // visitor is allowed to see anything, so it must not pull the branding bundle,
@@ -44,11 +44,7 @@ const seams = require('../../module-seams');
44
44
  // the kernel logger directly; only MODULES go via api.logger (the doorway).
45
45
  const log = require('../logger').logger.child({ src: 'auth' });
46
46
 
47
- function escapeHtml(s) {
48
- return String(s ?? '').replace(/[&<>"']/g, (c) => ({
49
- '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
50
- }[c]));
51
- }
47
+ const { escapeHtml } = require('../../html-escape'); // the one server escaper (task 1004876)
52
48
 
53
49
  // Render a small standalone HTML page in the builders' hall palette. Used for
54
50
  // OAuth error branches so the visitor sees something coherent rather than a
@@ -264,12 +264,8 @@ function dropEmptyHrefLinks(html, values) {
264
264
  // No token is ever emitted into a <script> or a CSS context (checked across
265
265
  // public-landing/, hall-ui/, status-ui/ — all under modules/ since task 1003322)
266
266
  // — that would need different escaping.
267
- // Ampersand first, or the other replacements get double-escaped.
268
- function esc(s) {
269
- return String(s == null ? '' : s)
270
- .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
271
- .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
272
- }
267
+ // The one server escaper (src/html-escape.js, task 1004876).
268
+ const { escapeHtml: esc } = require('../html-escape');
273
269
  // The instance apex origin from the full branding pack (domains is server-only,
274
270
  // so it never rides clientBranding). Best-effort so a branding hiccup can't crash
275
271
  // serving.
@@ -0,0 +1,26 @@
1
+ // src/html-escape.js — THE server-side HTML escaper (task 1004876, goal 1000135).
2
+ //
3
+ // Escapes the five characters that let text become markup — & < > " ' — so a
4
+ // builder's own words (a task title, an error detail, a project name) stay text
5
+ // when they are written into a page, an attribute or an email. null and
6
+ // undefined render as empty text; anything else is String()-ed first.
7
+ //
8
+ // ONE copy per side, on purpose. The browser twin is window.OTB.escapeHtml in
9
+ // modules/hall-ui/public/dom-utils.js and the two must agree on every input
10
+ // (tests/html_escape.mjs runs both). Modules reach this one through the doorway
11
+ // (`api.escapeHtml`, src/module-api.js — ADR 0083); core files and scripts
12
+ // require it directly. scripts/bongos/escape-helper-guard.js fails fitness on a
13
+ // third hand-rolled copy anywhere in the code.
14
+ //
15
+ // Zero dependencies by design: the CLI requires it, so it must cost nothing to load.
16
+
17
+ 'use strict';
18
+
19
+ const TABLE = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
20
+
21
+ /** HTML-escape `s` for text or a quoted attribute value. null/undefined → ''. */
22
+ function escapeHtml(s) {
23
+ return String(s ?? '').replace(/[&<>"']/g, (c) => TABLE[c]);
24
+ }
25
+
26
+ module.exports = { escapeHtml };
package/src/module-api.js CHANGED
@@ -57,6 +57,10 @@ const { buildInfo } = require('./build-info');
57
57
  // (`responsibilitiesFor` via src/modules.js, `ROLE_RESPONSIBILITIES` via
58
58
  // scripts/bongos/gen-role-responsibilities.js) were never flagged and this one was.
59
59
  const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibilities');
60
+ // Eager and destructured for the same reasons: html-escape.js requires nothing.
61
+ // escapeHtml is THE server-side HTML escaper (task 1004876) — a module renders
62
+ // user text into a page through this, never through a private copy.
63
+ const { escapeHtml } = require('./html-escape');
60
64
 
61
65
  // ---------------------------------------------------------------------------
62
66
  // CORE_VERSION — the version of THIS published surface (semver).
@@ -75,7 +79,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
75
79
  // MAJOR (see allowBoxScope below): passes the request through untouched.
76
80
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
77
81
 
78
- const CORE_VERSION = '1.21.182'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
82
+ const CORE_VERSION = '1.22.2'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
79
83
 
80
84
  // A namespaced logger so a module's log lines are attributable + consistent.
81
85
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -122,6 +126,10 @@ function logger(moduleKey) {
122
126
  module.exports = {
123
127
  CORE_VERSION,
124
128
 
129
+ // escapeHtml(s) — the one server-side HTML escaper (& < > " '; null → ''). The
130
+ // browser twin is window.OTB.escapeHtml; fitness fails on a third copy (task 1004876).
131
+ escapeHtml,
132
+
125
133
  // --- auth & rank (ADR 0016) — the gates the loader composes around a module's
126
134
  // routes; a module declares the rank it needs and never wires auth itself.
127
135
  get requireBuilder() { return require('./bongos/auth').requireBuilder; },
@@ -22,7 +22,7 @@ import fs from 'node:fs';
22
22
  import path from 'node:path';
23
23
  import vm from 'node:vm';
24
24
  import { fileURLToPath } from 'node:url';
25
- import { makeRunner } from './helpers.mjs';
25
+ import { makeRunner, realOTB } from './helpers.mjs';
26
26
 
27
27
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
28
28
  const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
@@ -97,7 +97,7 @@ function harness({ total = 57, failAfter = null, perPagePref = null } = {}) {
97
97
  getItem: (k) => (store.has(k) ? store.get(k) : null),
98
98
  setItem: (k, v) => store.set(k, String(v)),
99
99
  },
100
- OTB: { toast: (msg) => toasts.push(msg) },
100
+ OTB: { ...realOTB(), toast: (msg) => toasts.push(msg) },
101
101
  addEventListener() {},
102
102
  // loadViewer() calls this for GET /me; signed-out (401) keeps isReviewer()
103
103
  // false, which is fine for these paging tests — they don't inspect the
@@ -26,7 +26,7 @@ import path from 'node:path';
26
26
  import vm from 'node:vm';
27
27
  import { fileURLToPath } from 'node:url';
28
28
  import { createRequire } from 'node:module';
29
- import { makeRunner, makeEl, walk } from './helpers.mjs';
29
+ import { makeRunner, makeEl, walk, realOTB } from './helpers.mjs';
30
30
 
31
31
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
32
32
  const HALL = path.join(ROOT, 'modules/hall-ui/public');
@@ -114,7 +114,7 @@ async function boot(opts = {}) {
114
114
  getElementById(id) { return walk(root, (el) => (el.id === id ? el : null)); },
115
115
  };
116
116
  const windowObj = {
117
- OTB: { toast: (m) => toasts.push(m) },
117
+ OTB: { ...realOTB(), toast: (m) => toasts.push(m) },
118
118
  OTBBlockersLib,
119
119
  OTBTaskDetail: { href: (id) => '/task/' + id },
120
120
  BongosClient: { createClient: () => api },
@@ -15,6 +15,7 @@ import path from 'node:path';
15
15
  import vm from 'node:vm';
16
16
  import { fileURLToPath } from 'node:url';
17
17
  import { createRequire } from 'node:module';
18
+ import { realOTB } from './helpers.mjs';
18
19
 
19
20
  const require = createRequire(import.meta.url);
20
21
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
@@ -233,6 +234,7 @@ function boot({ data, permissions = { 'core.pin.move': true }, stored = {} } = {
233
234
  };
234
235
  const location = { hostname: 'h', origin: 'https://h', pathname: '/', hash: '', assign() {}, replace() {} };
235
236
  const windowObj = {
237
+ OTB: realOTB(), // the page loads dom-utils.js first (task 1004876)
236
238
  __BRANDING__: {}, __MODULES__: { enabled: { provisioning: false } },
237
239
  BongosClient: { createClient: () => ({ request: (method, p, opts) => {
238
240
  requested.push({ method, p, body: opts && opts.body });
@@ -313,7 +315,7 @@ async function panel(data) {
313
315
  const el = () => ({ hidden: true, textContent: '', _html: '', dataset: {}, set innerHTML(v) { this._html = String(v); }, get innerHTML() { return this._html; }, querySelectorAll: () => [] });
314
316
  const els = new Map([['software-update-scroll', el()], ['su-body', el()], ['su-note', el()], ['settings-groups', el()]]);
315
317
  els.get('settings-groups').dataset.active = 'update';
316
- const windowObj = { BongosClient: { createClient: () => ({ request: async () => ({ ok: true, status: 200, data }) }) }, OTB: { escapeHtml }, location: { assign() {} } };
318
+ const windowObj = { BongosClient: { createClient: () => ({ request: async () => ({ ok: true, status: 200, data }) }) }, OTB: { ...realOTB(), escapeHtml }, location: { assign() {} } };
317
319
  const sandbox = { window: windowObj, document: { getElementById: (id) => els.get(id) || null, addEventListener() {} }, console, setTimeout, Date, Math, URL, Promise, String, Number };
318
320
  vm.createContext(sandbox);
319
321
  vm.runInContext(PANEL_SRC, sandbox, { filename: 'settings-software-update.js' });
@@ -12,6 +12,7 @@ import path from 'node:path';
12
12
  import vm from 'node:vm';
13
13
  import { createRequire } from 'node:module';
14
14
  import { fileURLToPath } from 'node:url';
15
+ import { realOTB } from './helpers.mjs';
15
16
 
16
17
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
17
18
  const require_ = createRequire(import.meta.url);
@@ -265,6 +266,7 @@ function boot({ permissions, update, status = 200, dismissed = null, provisionin
265
266
  };
266
267
  const location = { hostname: 'h', origin: 'https://h', pathname, hash: '', assign() {}, replace() {} };
267
268
  const windowObj = {
269
+ OTB: realOTB(), // the page loads dom-utils.js first (task 1004876)
268
270
  __BRANDING__: {}, __MODULES__: { enabled: { provisioning } },
269
271
  BongosClient: {
270
272
  createClient: () => ({
@@ -23,6 +23,7 @@ import fs from 'node:fs';
23
23
  import path from 'node:path';
24
24
  import vm from 'node:vm';
25
25
  import { fileURLToPath } from 'node:url';
26
+ import { realOTB } from './helpers.mjs';
26
27
 
27
28
  const require = createRequire(import.meta.url);
28
29
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
@@ -409,6 +410,7 @@ function boot({ provisioning, deployUrl, pathname = '/settings', hash = '', held
409
410
  const location = { hostname: 'h', origin: 'https://h', pathname, hash, assign() {}, replace() {} };
410
411
  const update = { running: '1.19.871', latest: '1.19.1033', behind: true, reading: 'ok' };
411
412
  const windowObj = {
413
+ OTB: realOTB(), // the page loads dom-utils.js first (task 1004876)
412
414
  __BRANDING__: {}, __MODULES__: { enabled: { provisioning } },
413
415
  BongosClient: {
414
416
  createClient: () => ({
@@ -14,6 +14,7 @@ import fs from 'node:fs';
14
14
  import path from 'node:path';
15
15
  import vm from 'node:vm';
16
16
  import { fileURLToPath } from 'node:url';
17
+ import { realOTB } from './helpers.mjs';
17
18
 
18
19
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
19
20
  const HALL = path.join(ROOT, 'modules/hall-ui/public');
@@ -115,6 +116,7 @@ function boot({ craft = true, session = {}, store = {}, throws = false, optOut =
115
116
  removeItem: (k) => { if (throws) throw new Error('SecurityError'); delete obj[k]; },
116
117
  });
117
118
  const windowObj = {
119
+ OTB: realOTB(), // the page loads dom-utils.js first (task 1004876)
118
120
  __BRANDING__: {}, __MODULES__: { enabled: {} },
119
121
  BongosClient: { createClient: () => ({ request: () => new Promise(() => {}) }) },
120
122
  matchMedia: (q) => ({ matches: narrow && /max-width: 640px/.test(q), addEventListener() {} }),
@@ -14,6 +14,7 @@ import fs from 'node:fs';
14
14
  import path from 'node:path';
15
15
  import vm from 'node:vm';
16
16
  import { fileURLToPath } from 'node:url';
17
+ import { realOTB } from './helpers.mjs';
17
18
 
18
19
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
19
20
  const HALL = path.join(ROOT, 'modules/hall-ui/public');
@@ -90,6 +91,7 @@ function boot({ session = {}, perms = { 'board.vote.cast': true } } = {}) {
90
91
  const location = { hostname: 'h', origin: 'https://h', href: 'https://h/work', pathname: '/work', hash: '', search: '', assign() {}, replace() {}, reload() {} };
91
92
  const mk = (obj) => ({ getItem: (k) => (k in obj ? obj[k] : null), setItem: (k, v) => { obj[k] = String(v); }, removeItem: (k) => { delete obj[k]; } });
92
93
  const windowObj = {
94
+ OTB: realOTB(), // the page loads dom-utils.js first (task 1004876)
93
95
  __BRANDING__: {}, __MODULES__: { enabled: { provisioning: true } },
94
96
  BongosClient: { createClient: () => ({ request: () => new Promise(() => {}) }) },
95
97
  matchMedia: () => ({ matches: false, addEventListener() {} }),
@@ -274,6 +276,7 @@ function bootTree({ mode = null, activeId = null } = {}) {
274
276
  addEventListener() {}, removeEventListener() {},
275
277
  };
276
278
  const windowObj = {
279
+ OTB: realOTB(), // the page loads dom-utils.js first (task 1004876)
277
280
  location: { search: '' }, matchMedia: () => ({ matches: false }),
278
281
  localStorage: mk({}), sessionStorage: mk(mode ? { 'hall-craft': mode } : {}),
279
282
  };
@@ -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');
@@ -77,7 +78,7 @@ function boot({ session = {}, store = {}, narrow = false } = {}) {
77
78
  dispatchEvent() { return true; },
78
79
  };
79
80
  const mk = (obj) => ({ getItem: (k) => (k in obj ? obj[k] : null), setItem: (k, v) => { obj[k] = String(v); }, removeItem: (k) => { delete obj[k]; } });
80
- const window = { localStorage: mk(store), sessionStorage: mk(session), matchMedia: (q) => ({ matches: narrow && /max-width: 640px/.test(q) }), location: { search: '' } };
81
+ const window = { OTB: realOTB(), localStorage: mk(store), sessionStorage: mk(session), matchMedia: (q) => ({ matches: narrow && /max-width: 640px/.test(q) }), location: { search: '' } };
81
82
  const sandbox = { window, document, Promise, setTimeout, clearTimeout, CustomEvent: class { constructor(t, i) { this.type = t; Object.assign(this, i); } } };
82
83
  vm.createContext(sandbox);
83
84
  // "Loading" a script: the wall runs the REAL craft-review.js; the palette is a fake panel.
@@ -208,7 +209,7 @@ test('THE WALL is scribbled in CSS only, in ink alone, and stands still under re
208
209
  });
209
210
 
210
211
  test('craft-review.js draws six small sky doodles, each with its pen, the wide ones foldable', () => {
211
- const sandbox = { window: {} };
212
+ const sandbox = { window: { OTB: realOTB() } };
212
213
  vm.createContext(sandbox);
213
214
  vm.runInContext(REVIEW, sandbox);
214
215
  const api = sandbox.window.OTBCraftReview;
@@ -194,8 +194,13 @@ test('a slug carrying an apostrophe cannot break the click handler', () => {
194
194
  // single-quoted JS string inside an HTML attribute.
195
195
  const odd = { ...crit(2, "o'hara-rule", '1000999') };
196
196
  const html = status.renderStatusWidget({ ...DATA, criteria: [odd] }, null);
197
- assert.ok(html.includes("sendPrompt('/status o\\'hara-rule')"),
198
- 'the apostrophe was not escaped for the JS string');
197
+ // The attribute is HTML-escaped AFTER the JS escape, so the apostrophe arrives as
198
+ // \&#39; (task 1004876: the shared escaper escapes ' too) — the browser decodes the
199
+ // attribute first, and the JS it then parses is the \' asserted below.
200
+ const onclick = html.match(/onclick="([^"]*o\\&#39;hara-rule[^"]*)"/);
201
+ assert.ok(onclick, 'the apostrophe was not escaped for the JS string');
202
+ assert.ok(onclick[1].replace(/&#39;/g, "'").includes("sendPrompt('/status o\\'hara-rule')"),
203
+ 'the decoded handler carries the JS-escaped apostrophe');
199
204
  });
200
205
 
201
206
  console.log(`\ncriterion ref — read side (task 1004069): ${passed} passed, ${failed} failed`);
@@ -26,12 +26,13 @@ import fs from 'node:fs';
26
26
  import path from 'node:path';
27
27
  import vm from 'node:vm';
28
28
  import { fileURLToPath } from 'node:url';
29
+ import { realOTB } from './helpers.mjs';
29
30
 
30
31
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
31
32
 
32
33
  function loadKit() {
33
34
  const src = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/hall-kit.js'), 'utf8');
34
- const win = {};
35
+ const win = { OTB: realOTB() }; // the page loads dom-utils.js first (task 1004876)
35
36
  const ctx = vm.createContext({ window: win, document: { createElement: () => ({}) }, console });
36
37
  vm.runInContext(src, ctx);
37
38
  assert.ok(win.OTBKit, 'hall-kit.js did not publish window.OTBKit');
@@ -13,6 +13,7 @@ import { createRequire } from 'node:module';
13
13
  import fs from 'node:fs';
14
14
  import path from 'node:path';
15
15
  import { fileURLToPath } from 'node:url';
16
+ import { realOTB } from './helpers.mjs';
16
17
 
17
18
  const require = createRequire(import.meta.url);
18
19
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
@@ -135,6 +136,13 @@ function makeHall() {
135
136
 
136
137
  // The REAL escaper and the real wake map are sliced out of shell.js with the function,
137
138
  // so the band's escaping is the shell's own, not a stand-in that agrees with the test.
139
+ // shell.js escapes through window.OTB (dom-utils.js, task 1004876), so the window it is
140
+ // handed carries the real one.
141
+ function sliceEsc() {
142
+ const line = SHELL.split("\n").find((l) => l.startsWith(" function esc("));
143
+ assert.ok(line, "esc was located in shell.js");
144
+ return line;
145
+ }
138
146
  function sliceFn(name) {
139
147
  const start = SHELL.indexOf(' function ' + name + '(');
140
148
  const end = SHELL.indexOf('\n }\n', start);
@@ -145,8 +153,8 @@ function loadApplyFounding(document, access, brand = { identity: { productName:
145
153
  const wakeStart = SHELL.indexOf(' var WAKE_TEXT = {');
146
154
  const wakeEnd = SHELL.indexOf('};', wakeStart) + 2;
147
155
  assert.ok(wakeStart > -1, 'WAKE_TEXT was located in shell.js');
148
- const src = [SHELL.slice(wakeStart, wakeEnd), sliceFn('esc'), sliceFn('applyFounding')].join('\n');
149
- return new Function('document', 'brand', 'access', `${src}\nreturn applyFounding;`)(document, brand, access);
156
+ const src = [SHELL.slice(wakeStart, wakeEnd), sliceEsc(), sliceFn('applyFounding')].join('\n');
157
+ return new Function('document', 'brand', 'access', 'window', `${src}\nreturn applyFounding;`)(document, brand, access, { OTB: realOTB() });
150
158
  }
151
159
 
152
160
  const snapshot = (h) => JSON.stringify({
@@ -22,6 +22,7 @@ import path from 'node:path';
22
22
  import vm from 'node:vm';
23
23
  import { createRequire } from 'node:module';
24
24
  import { fileURLToPath } from 'node:url';
25
+ import { realOTB } from './helpers.mjs';
25
26
 
26
27
  const require = createRequire(import.meta.url);
27
28
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
@@ -159,12 +160,14 @@ test('GET /provisioning/instances and /provisioning/instances/:id carry github_i
159
160
  function grab(head) {
160
161
  const at = HUB.indexOf(`\n ${head}`);
161
162
  assert.ok(at >= 0, `${head} must exist`);
163
+ const line = HUB.slice(at, HUB.indexOf('\n', at + 1));
164
+ if (/\}\s*$/.test(line)) return line; // a one-line declaration (esc, task 1004876)
162
165
  const end = HUB.indexOf('\n }', at + 1);
163
166
  const close = HUB.slice(end, end + 5) === '\n };' ? end + 5 : end + 4;
164
167
  return HUB.slice(at, close);
165
168
  }
166
169
  function page() {
167
- const ctx = { STATE_GROUP: { active: 'live', error: 'attention', torn_down: 'gone' } };
170
+ const ctx = { window: { OTB: realOTB() }, STATE_GROUP: { active: 'live', error: 'attention', torn_down: 'gone' } };
168
171
  vm.createContext(ctx);
169
172
  vm.runInContext(['function esc(', 'function ghInstallRow(', 'function groupFor('].map(grab).join('\n'), ctx);
170
173
  return ctx;
@@ -431,9 +431,9 @@ await test('the page is the sky\'s own surface — the same sheet and the same s
431
431
  assert.match(html, /<main id="hall-main" class="sky-stage">/, 'the sky is drawn in the stage, the skip link\'s target');
432
432
  const scripts = (h) => [...h.matchAll(/<script src="\/builders\/([^"?]+)\?v=([^"]+)"/g)].map((m) => [m[1], m[2]]);
433
433
  // criterion-uat-ui.js (task 1004402): the rings' UAT state words, shared with the goal page.
434
- assert.deepEqual(scripts(html).filter((s) => !['shell-trail.js', 'craft-mode.js', 'shell.js'].includes(s[0])).map((s) => s[0]), ['idea-objects.js', 'criterion-uat-ui.js', 'sky-data.js', 'sky-layout.js', 'sky-draw.js', 'sky-interaction.js', 'sky-panel.js', 'sky-composer.js', 'sky-tour.js']);
434
+ assert.deepEqual(scripts(html).filter((s) => !['dom-utils.js', 'shell-trail.js', 'craft-mode.js', 'shell.js'].includes(s[0])).map((s) => s[0]), ['idea-objects.js', 'criterion-uat-ui.js', 'sky-data.js', 'sky-layout.js', 'sky-draw.js', 'sky-interaction.js', 'sky-panel.js', 'sky-composer.js', 'sky-tour.js']);
435
435
  // Both doors load the shell (tasks 1004327, 1004430); the SKY's files are the same.
436
- const skyScripts = (h) => scripts(h).filter((s) => !['shell-trail.js', 'craft-mode.js', 'shell.js'].includes(s[0]));
436
+ const skyScripts = (h) => scripts(h).filter((s) => !['dom-utils.js', 'shell-trail.js', 'craft-mode.js', 'shell.js'].includes(s[0]));
437
437
  assert.deepEqual(skyScripts(html), skyScripts(thinking), 'the two doors load the same sky files at the same stamp');
438
438
  assert.equal(html.match(/thinking\.css\?v=([^"]+)/)[1], thinking.match(/thinking\.css\?v=([^"]+)/)[1]);
439
439
  assert.match(read('thinking.css'), /body\[data-page\] \.sky-stage \{/, 'the sheet dresses the one stage both doors draw in inside the hall');
@@ -18,7 +18,7 @@ import fs from 'node:fs';
18
18
  import path from 'node:path';
19
19
  import vm from 'node:vm';
20
20
  import { fileURLToPath } from 'node:url';
21
- import { makeRunner, makeEl, walk, settle } from './helpers.mjs';
21
+ import { makeRunner, makeEl, walk, settle, 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)), '..');
@@ -105,7 +105,7 @@ function bootPanel({ answers = {}, writes = null, mode = 'light' } = {}) {
105
105
  return val.__status ? { ok: false, status: val.__status, data: val.data } : { ok: true, status: 200, data: val };
106
106
  },
107
107
  };
108
- const win = { matchMedia: () => ({ matches: false }), __BRANDING__: { identity: { productName: 'Cloud Bongos', worldName: 'the studio' } } };
108
+ const win = { OTB: realOTB(), matchMedia: () => ({ matches: false }), __BRANDING__: { identity: { productName: 'Cloud Bongos', worldName: 'the studio' } } };
109
109
  const ctx = vm.createContext({ window: win, document: documentObj, console, setTimeout, clearTimeout, encodeURIComponent });
110
110
  win.window = win;
111
111
  vm.runInContext(read('brand-holes.js'), ctx, { filename: 'brand-holes.js' });
@@ -288,7 +288,7 @@ test('the city sits in the hall chrome, behind the shell\'s full-screen switch',
288
288
  // task 1004685 (the living city): gov-living.js, the model under the drawing, and
289
289
  // gov-feeds.js, its one reader per page (shared with the shell's tally), load before it
290
290
  // task 1004702: the card answers its first ask in place, so the deck's card and sender (and the desk rows under them) load before the page
291
- assert.deepEqual(scripts, ['shell-trail', 'craft-mode', 'shell', 'palette', 'gov-living', 'gov-feeds', 'city-draw', 'hall-kit', 'street-draw', 'street-card', 'street-inside', 'street-desk', 'street-deck', 'city-decide', 'city-switchboard', 'city'], 'the shell first, as on every hall page');
291
+ assert.deepEqual(scripts, ['dom-utils', 'shell-trail', 'craft-mode', 'shell', 'palette', 'gov-living', 'gov-feeds', 'city-draw', 'hall-kit', 'street-draw', 'street-card', 'street-inside', 'street-desk', 'street-deck', 'city-decide', 'city-switchboard', 'city'], 'the shell first, as on every hall page');
292
292
  const css = read('city.css').replace(/\/\*[\s\S]*?\*\//g, '');
293
293
  assert.match(css, /\.city-main \{ position: fixed; top: var\(--topbar-h, 56px\); left: var\(--sidebar-w-rail, 64px\);[^}]*contain: layout paint;/);
294
294
  assert.match(css, /html\[data-fullscreen="on"\] \.city-main \{ top: 0; left: 0; \}/);
@@ -18,6 +18,7 @@ import fs from 'node:fs';
18
18
  import path from 'node:path';
19
19
  import vm from 'node:vm';
20
20
  import { fileURLToPath } from 'node:url';
21
+ import { realOTB } from './helpers.mjs';
21
22
 
22
23
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
23
24
  const HALL = path.join(ROOT, 'modules/hall-ui/public');
@@ -104,6 +105,7 @@ function boot({ page = 'thinking', optIn = true, store = {}, storageThrows = fal
104
105
  };
105
106
  let resizes = 0;
106
107
  const windowObj = {
108
+ OTB: realOTB(), // the page loads dom-utils.js first (task 1004876)
107
109
  __BRANDING__: {}, __MODULES__: { enabled: {} },
108
110
  // `meId` answers /me once with that builder; otherwise the read never settles.
109
111
  BongosClient: { createClient: () => ({ request: () => (meId == null ? new Promise(() => {})
@@ -31,6 +31,7 @@ import path from 'node:path';
31
31
  import vm from 'node:vm';
32
32
  import { createRequire } from 'node:module';
33
33
  import { fileURLToPath } from 'node:url';
34
+ import { realOTB } from './helpers.mjs';
34
35
 
35
36
  const require = createRequire(import.meta.url);
36
37
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
@@ -300,6 +301,7 @@ function baseWindow(hash = '') {
300
301
  const store = new Map();
301
302
  const ls = { getItem: (k) => (store.has(k) ? store.get(k) : null), setItem: (k, v) => store.set(k, String(v)), removeItem: (k) => store.delete(k) };
302
303
  return {
304
+ OTB: realOTB(), // the page loads dom-utils.js first (task 1004876)
303
305
  innerWidth: 1440, innerHeight: 900, location: { hash, href: '', search: '', pathname: '/' },
304
306
  localStorage: ls, sessionStorage: ls,
305
307
  matchMedia: () => ({ matches: true, addEventListener() {} }), addEventListener() {}, removeEventListener() {},
@@ -20,6 +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 { realOTB } from './helpers.mjs';
23
24
 
24
25
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
25
26
  const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
@@ -299,7 +300,7 @@ function runPage(file, extraWindow = {}) {
299
300
  { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
300
301
  const sandbox = {
301
302
  window: {
302
- OTB: { escapeHtml: esc, $: () => null, fmtNum: (n) => String(n ?? 0), fmtDate: (s) => String(s ?? '') },
303
+ OTB: { ...realOTB(), escapeHtml: esc, $: () => null, fmtNum: (n) => String(n ?? 0), fmtDate: (s) => String(s ?? '') },
303
304
  __BRANDING__: { currency: { label: 'drachmae' } },
304
305
  ...extraWindow,
305
306
  },
@@ -26,6 +26,7 @@ import path from 'node:path';
26
26
  import vm from 'node:vm';
27
27
  import { createRequire } from 'node:module';
28
28
  import { fileURLToPath } from 'node:url';
29
+ import { realOTB } from './helpers.mjs';
29
30
 
30
31
  const require = createRequire(import.meta.url);
31
32
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
@@ -378,7 +379,7 @@ function bootCraft({ session = {} } = {}) {
378
379
  getElementById: () => null, querySelector: () => null, querySelectorAll: () => [],
379
380
  createElement: () => el(), addEventListener() {}, removeEventListener() {}, dispatchEvent() { return true; },
380
381
  };
381
- const window = { location: { search: '' }, matchMedia: () => ({ matches: false }), sessionStorage: mk(session), localStorage: mk({}) };
382
+ const window = { OTB: realOTB(), location: { search: '' }, matchMedia: () => ({ matches: false }), sessionStorage: mk(session), localStorage: mk({}) };
382
383
  const sandbox = { window, document, console, CustomEvent: class { constructor(t, i) { this.type = t; Object.assign(this, i); } } };
383
384
  sandbox.globalThis = sandbox;
384
385
  vm.createContext(sandbox);
@@ -441,7 +442,7 @@ test('PAGE: in the hall family, with no prototype key', () => {
441
442
  const sheets = [...html.matchAll(/href="\/builders\/([a-z-]+\.css)/g)].map((m) => m[1]);
442
443
  assert.deepEqual(sheets, ['style.css', 'oversight.css', 'my-agents.css'], 'rides the oversight archetype, linked before its own sheet');
443
444
  const scripts = [...html.matchAll(/src="\/builders\/([a-z-]+)\.js/g)].map((m) => m[1]);
444
- assert.deepEqual(scripts.slice(0, 3), ['shell-trail', 'craft-mode', 'shell']);
445
+ assert.deepEqual(scripts.slice(0, 4), ['dom-utils', 'shell-trail', 'craft-mode', 'shell']); // dom-utils first: the shell escapes through it (task 1004876)
445
446
  for (const s of ['gov-living', 'gov-positions', 'my-agents']) assert.ok(scripts.includes(s), `loads ${s}.js`);
446
447
  assert.doesNotMatch(html, /ma-key|data-proto|Prototype data/, 'nothing on the page is prototype data, so there is no key');
447
448
  assert.doesNotMatch(read('my-agents.js'), /governor-overlay/, 'and it reads no overlay');
@@ -20,6 +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 { realOTB } from './helpers.mjs';
23
24
 
24
25
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
25
26
  const HALL = path.join(ROOT, 'modules/hall-ui/public');
@@ -136,6 +137,7 @@ function boot({ page = 'settings', hash = '', pathname = '/settings', search = '
136
137
  };
137
138
 
138
139
  const windowObj = {
140
+ OTB: realOTB(), // the page loads dom-utils.js first (task 1004876)
139
141
  __BRANDING__: {}, __MODULES__: { enabled: {} },
140
142
  // Index pages never reach probeMe (builders.js owns /me there); a non-index
141
143
  // boot would, so hand back a never-settling promise rather than fake auth —
@@ -18,7 +18,7 @@ import fs from 'node:fs';
18
18
  import path from 'node:path';
19
19
  import vm from 'node:vm';
20
20
  import { fileURLToPath } from 'node:url';
21
- import { hallSource } from './helpers.mjs';
21
+ import { hallSource, realOTB } from './helpers.mjs';
22
22
 
23
23
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
24
24
  const HALL = path.join(ROOT, 'modules/hall-ui/public');
@@ -79,7 +79,7 @@ function bootPalette({ navItems = [], visibleIds = ['nav-standing', 'nav-work',
79
79
  addEventListener() {},
80
80
  activeElement: null,
81
81
  };
82
- const windowObj = { location: { hash: '', assign() {} }, addEventListener() {}, open() {} };
82
+ const windowObj = { OTB: realOTB(), location: { hash: '', assign() {} }, addEventListener() {}, open() {} };
83
83
  const sandbox = {
84
84
  window: windowObj, document: documentObj, navigator: { platform: 'Win32', userAgent: 'test' },
85
85
  console, setTimeout, clearTimeout, Promise, JSON, Number, String, Array, Object,