@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.
- package/.bongos-core.json +159 -144
- package/docs/copy-inventory.md +165 -165
- package/docs/copy-registry.json +184 -184
- package/docs/file-map.md +1 -1
- package/docs/module-api-changelog.md +12 -0
- package/docs/page-inventory.json +35 -26
- package/docs/page-readings.json +389 -388
- package/modules/hall-ui/public/approval-queue.js +3 -3
- package/modules/hall-ui/public/atlas.html +2 -1
- package/modules/hall-ui/public/atlas.js +1 -1
- package/modules/hall-ui/public/blockers.html +3 -4
- package/modules/hall-ui/public/board-room.html +2 -2
- package/modules/hall-ui/public/brand-holes.js +3 -3
- package/modules/hall-ui/public/city.html +2 -1
- package/modules/hall-ui/public/collab.html +2 -2
- package/modules/hall-ui/public/commands.html +2 -2
- package/modules/hall-ui/public/commands.js +1 -2
- package/modules/hall-ui/public/copy-desk.html +2 -2
- package/modules/hall-ui/public/core-move-notice.js +1 -5
- package/modules/hall-ui/public/craft-mode.js +1 -5
- package/modules/hall-ui/public/criterion-uat-ui.js +3 -3
- package/modules/hall-ui/public/deploy.html +2 -2
- package/modules/hall-ui/public/diagrams.html +2 -1
- package/modules/hall-ui/public/dom-utils.js +15 -6
- package/modules/hall-ui/public/drachmae.html +2 -1
- package/modules/hall-ui/public/fleet-costs.js +3 -2
- package/modules/hall-ui/public/fleet.html +2 -2
- package/modules/hall-ui/public/founding-mark.js +1 -6
- package/modules/hall-ui/public/gate.html +2 -2
- package/modules/hall-ui/public/goal-map.html +2 -1
- package/modules/hall-ui/public/goals.html +2 -2
- package/modules/hall-ui/public/government.html +2 -2
- package/modules/hall-ui/public/hall-ask.js +3 -3
- package/modules/hall-ui/public/hall-kit.js +3 -5
- package/modules/hall-ui/public/hall-visual.js +3 -5
- package/modules/hall-ui/public/idea.html +2 -2
- package/modules/hall-ui/public/ideas.html +2 -2
- package/modules/hall-ui/public/index.html +2 -2
- package/modules/hall-ui/public/lightbulb.js +1 -5
- package/modules/hall-ui/public/md-reader.js +1 -8
- package/modules/hall-ui/public/modules.html +2 -2
- package/modules/hall-ui/public/my-agents.html +2 -2
- package/modules/hall-ui/public/my-agents.js +3 -1
- package/modules/hall-ui/public/page-notes.js +1 -5
- package/modules/hall-ui/public/paintbrush.js +1 -5
- package/modules/hall-ui/public/palette.js +1 -5
- package/modules/hall-ui/public/primer.html +2 -1
- package/modules/hall-ui/public/primer.js +1 -1
- package/modules/hall-ui/public/profile-crafts.js +3 -5
- package/modules/hall-ui/public/profile.html +2 -3
- package/modules/hall-ui/public/project-settings.html +2 -2
- package/modules/hall-ui/public/ranks.html +2 -3
- package/modules/hall-ui/public/roadmap.html +2 -2
- package/modules/hall-ui/public/roster.html +3 -4
- package/modules/hall-ui/public/sessions.html +2 -3
- package/modules/hall-ui/public/settings.html +2 -3
- package/modules/hall-ui/public/shell.js +1 -5
- package/modules/hall-ui/public/sky-data.js +1 -1
- package/modules/hall-ui/public/street.html +2 -1
- package/modules/hall-ui/public/studio.html +2 -2
- package/modules/hall-ui/public/task-detail.js +1 -6
- package/modules/hall-ui/public/task-lib.js +3 -8
- package/modules/hall-ui/public/task.html +2 -3
- package/modules/hall-ui/public/thinking.html +2 -1
- package/modules/hall-ui/public/tweak-editor.html +2 -2
- package/modules/hall-ui/public/vote-control.js +1 -1
- package/modules/hall-ui/public/watch.html +2 -3
- package/modules/hall-ui/public/welcome-boot.js +3 -5
- package/modules/hall-ui/public/work.html +2 -3
- package/modules/lifecycle/ship-card.js +1 -4
- package/modules/npm-release/preview/proxy.js +2 -3
- package/modules/npm-release/public/task-widget.js +1 -1
- package/modules/platform-identity/routes/sso.js +1 -3
- package/modules/provisioning/routes/callback-page.js +2 -6
- package/modules/public-landing/public/projects.html +3 -5
- package/modules/raise/raise.js +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +26 -12
- package/scripts/bongos/escape-helper-guard.js +147 -0
- package/scripts/bongos/fitness.js +3 -2
- package/scripts/bongos/key-card.js +1 -4
- package/scripts/bongos/review-sheet.js +1 -3
- package/scripts/bongos/start.js +1 -1
- package/scripts/bongos/status.js +1 -1
- package/scripts/gds/escape-helper-guard.js +6 -0
- package/scripts/status-mirror-sync.js +2 -3
- package/src/bongos/module-scope-map.js +1 -0
- package/src/bongos/platform-visibility-gate.js +2 -3
- package/src/bongos/prelaunch-gate.js +2 -3
- package/src/bongos/routes/auth.js +1 -5
- package/src/bongos/serve-internal.js +2 -6
- package/src/html-escape.js +26 -0
- package/src/module-api.js +9 -1
- package/tests/blockers_kit_pager.mjs +2 -2
- package/tests/blockers_page_ui.mjs +2 -2
- package/tests/core_move_notice.mjs +3 -1
- package/tests/core_update_banner.mjs +2 -0
- package/tests/core_upgrade_owner_door.mjs +2 -0
- package/tests/craft_mode.mjs +2 -0
- package/tests/craft_mode_nav.mjs +3 -0
- package/tests/craft_review_mode.mjs +3 -2
- package/tests/criterion_ref_status.mjs +7 -2
- package/tests/curve_kind_overlay.mjs +2 -1
- package/tests/founding_mode.mjs +10 -2
- package/tests/github_install_nudge.mjs +4 -1
- package/tests/goal_map_page.mjs +2 -2
- package/tests/government_protected_surfaces.mjs +15 -96
- package/tests/hall_approval_queue.mjs +2 -2
- package/tests/hall_city.mjs +1 -1
- package/tests/hall_fullscreen.mjs +2 -0
- package/tests/hall_gov_real_site.mjs +2 -0
- package/tests/hall_kit_charts.mjs +2 -1
- package/tests/hall_my_agents.mjs +3 -2
- package/tests/hall_nav.mjs +2 -0
- package/tests/hall_palette.mjs +2 -2
- package/tests/hall_reading_world.mjs +3 -1
- package/tests/hall_reference_world.mjs +2 -1
- package/tests/hall_signout.mjs +2 -0
- package/tests/hall_street.mjs +1 -1
- package/tests/hall_studio_world.mjs +3 -1
- package/tests/hall_tweak_editor.mjs +2 -1
- package/tests/helpers.mjs +17 -2
- package/tests/html_escape.mjs +149 -0
- package/tests/lightbulb.mjs +2 -0
- package/tests/module_api.mjs +1 -0
- package/tests/module_store_howto.mjs +2 -1
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/npm_release_where.mjs +2 -1
- package/tests/page_notes_panel.mjs +4 -3
- package/tests/paintbrush.mjs +2 -1
- package/tests/profile_connections_ui.mjs +2 -1
- package/tests/profile_role_headline.mjs +2 -2
- package/tests/profile_role_tabs.mjs +2 -2
- package/tests/projects_hub_repo_private.mjs +4 -0
- package/tests/terms_acceptance.mjs +1 -0
- package/tests/welcome_boot_browser.mjs +1 -1
- package/modules/government/protected-surfaces.js +0 -88
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// scripts/bongos/escape-helper-guard.js — fitness check: ONE HTML escaper per side
|
|
2
|
+
// (task 1004876, goal 1000135).
|
|
3
|
+
//
|
|
4
|
+
// WHY. The HTML escaper is what keeps a builder's own text (a task title, an idea,
|
|
5
|
+
// a goal name) from turning into live markup on someone else's screen. The
|
|
6
|
+
// 2026-10-25 audit found ~45 private copies of it, and six of them escaped fewer
|
|
7
|
+
// characters than the rest (no single quote) or printed "null" for a missing
|
|
8
|
+
// value — each one a place a cross-site-scripting hole could hide. They were
|
|
9
|
+
// collapsed onto exactly two definitions:
|
|
10
|
+
//
|
|
11
|
+
// browser modules/hall-ui/public/dom-utils.js → window.OTB.escapeHtml
|
|
12
|
+
// server src/html-escape.js → escapeHtml (modules reach it
|
|
13
|
+
// through src/module-api.js)
|
|
14
|
+
//
|
|
15
|
+
// This check keeps it that way. It does not look for a NAME (a copy called
|
|
16
|
+
// `hesc` or `sesc` hid from a name search) — it looks for the escape TABLE
|
|
17
|
+
// itself: a quoted < string literal is what every hand-rolled escaper has
|
|
18
|
+
// to contain, and nothing else in the code needs one. A file that contains one
|
|
19
|
+
// outside the allow-list below is a new private escaper: delete it and call the
|
|
20
|
+
// shared one (a thin `function esc(s) { return window.OTB.escapeHtml(s); }`
|
|
21
|
+
// delegate is fine — it holds no table).
|
|
22
|
+
//
|
|
23
|
+
// A delegate must really delegate, though. primer.js used to fall back to
|
|
24
|
+
// `(s) => String(s)` — an "escaper" with no table that escaped nothing — so a
|
|
25
|
+
// local HTML-escaper NAME (esc / escapeHtml / escAttr / escHtml / hesc / sesc)
|
|
26
|
+
// must point at a shared escaper on the line that defines it. In browser code
|
|
27
|
+
// (modules/*/public/) every such name is checked; on the server `esc` alone is
|
|
28
|
+
// exempt, because scripts there use it for regex and markdown escaping too.
|
|
29
|
+
//
|
|
30
|
+
// Second half: a hall page must load dom-utils.js as its FIRST hall script and
|
|
31
|
+
// without `defer`/`async`, because shell.js, palette.js and craft-mode.js run
|
|
32
|
+
// while the page parses and now escape through window.OTB.
|
|
33
|
+
|
|
34
|
+
'use strict';
|
|
35
|
+
|
|
36
|
+
const fs = require('node:fs');
|
|
37
|
+
const path = require('node:path');
|
|
38
|
+
|
|
39
|
+
const { REPO_ROOT } = require('./fitness-lib.js');
|
|
40
|
+
|
|
41
|
+
// The two shared escapers, plus the one file whose escaper is NOT an HTML one.
|
|
42
|
+
const ALLOWED = new Map([
|
|
43
|
+
['modules/hall-ui/public/dom-utils.js', 'the shared BROWSER escaper (window.OTB.escapeHtml)'],
|
|
44
|
+
['src/html-escape.js', 'the shared SERVER escaper (module-api escapeHtml)'],
|
|
45
|
+
['modules/copy-desk/docx.js', 'an XML escaper for .docx files (', drops control characters) — a different contract'],
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
// Where code lives. Tests, docs and vendored bundles are out of scope: a test may
|
|
49
|
+
// carry its own decoder/encoder to CHECK the shared one against.
|
|
50
|
+
const SCAN_ROOTS = ['src', 'scripts', 'modules', 'server.js', 'bin'];
|
|
51
|
+
const SCAN_EXT = /\.(c|m)?js$|\.html$/;
|
|
52
|
+
const SKIP_DIR = new Set(['node_modules', '.git', 'tests', 'fixtures']);
|
|
53
|
+
|
|
54
|
+
// A quoted < literal — the entity standing alone as a string in any quote style.
|
|
55
|
+
const ESCAPE_TABLE_RE = /(['"`])<\1/;
|
|
56
|
+
|
|
57
|
+
function walk(abs, out) {
|
|
58
|
+
let st;
|
|
59
|
+
try { st = fs.statSync(abs); } catch { return out; }
|
|
60
|
+
if (st.isFile()) { if (SCAN_EXT.test(abs)) out.push(abs); return out; }
|
|
61
|
+
for (const ent of fs.readdirSync(abs, { withFileTypes: true })) {
|
|
62
|
+
if (SKIP_DIR.has(ent.name)) continue;
|
|
63
|
+
walk(path.join(abs, ent.name), out);
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// findEscapeTables(text) → 1-based line numbers holding a hand-rolled escape table.
|
|
69
|
+
function findEscapeTables(text) {
|
|
70
|
+
const lines = String(text).split('\n');
|
|
71
|
+
const hits = [];
|
|
72
|
+
for (let i = 0; i < lines.length; i++) if (ESCAPE_TABLE_RE.test(lines[i])) hits.push(i + 1);
|
|
73
|
+
return hits;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// findFakeDelegates(text, { browser }) → 1-based lines that define a local HTML
|
|
77
|
+
// escaper name WITHOUT pointing it at a shared escaper (window.OTB.escapeHtml, a
|
|
78
|
+
// kit/reader alias of it, api.escapeHtml, or src/html-escape.js).
|
|
79
|
+
const BROWSER_NAMES = /(?:function\s+|(?:const|let|var)\s+)(esc|escapeHtml|escAttr|escHtml|hesc|sesc)\s*(?:\(|=)/;
|
|
80
|
+
const SERVER_NAMES = /(?:function\s+|(?:const|let|var)\s+)(escapeHtml|escAttr|escHtml|hesc|sesc)\s*(?:\(|=)/;
|
|
81
|
+
const POINTS_AT_SHARED = /\.escapeHtml\b|\.esc\b|\bescapeHtml;|require\([^)]*html-escape/;
|
|
82
|
+
function findFakeDelegates(text, { browser = false } = {}) {
|
|
83
|
+
const lines = String(text).split('\n');
|
|
84
|
+
const hits = [];
|
|
85
|
+
for (let i = 0; i < lines.length; i++) {
|
|
86
|
+
const m = lines[i].match(browser ? BROWSER_NAMES : SERVER_NAMES);
|
|
87
|
+
if (!m) continue;
|
|
88
|
+
if (!POINTS_AT_SHARED.test(lines[i].slice(m.index + m[0].length))) hits.push(i + 1);
|
|
89
|
+
}
|
|
90
|
+
return hits;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// hallPageOrderProblem(html) → null when the page is fine, else a sentence saying
|
|
94
|
+
// why. A page loading no /builders/ script at all has nothing to order.
|
|
95
|
+
function hallPageOrderProblem(html) {
|
|
96
|
+
const tags = [...String(html).matchAll(/<script\b[^>]*\bsrc=["']([^"']+)["'][^>]*>/g)]
|
|
97
|
+
.map((m) => ({ tag: m[0], src: m[1].split('?')[0] }))
|
|
98
|
+
.filter((t) => t.src.startsWith('/builders/'));
|
|
99
|
+
if (!tags.length) return null;
|
|
100
|
+
const at = tags.findIndex((t) => t.src === '/builders/dom-utils.js');
|
|
101
|
+
if (at < 0) return 'loads hall scripts but never /builders/dom-utils.js (window.OTB.escapeHtml would be undefined)';
|
|
102
|
+
if (at > 0) return `loads ${tags[0].src} before /builders/dom-utils.js — dom-utils must be the first hall script`;
|
|
103
|
+
if (/\b(defer|async)\b/.test(tags[at].tag)) return 'loads /builders/dom-utils.js with defer/async — scripts that run while the page parses would find no window.OTB';
|
|
104
|
+
if (tags.filter((t) => t.src === '/builders/dom-utils.js').length > 1) return 'loads /builders/dom-utils.js twice';
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function checkOneHtmlEscaper({ root = REPO_ROOT } = {}) {
|
|
109
|
+
const violations = [];
|
|
110
|
+
let scanned = 0;
|
|
111
|
+
for (const r of SCAN_ROOTS) {
|
|
112
|
+
for (const abs of walk(path.join(root, r), [])) {
|
|
113
|
+
scanned++;
|
|
114
|
+
const relPath = path.relative(root, abs).split(path.sep).join('/');
|
|
115
|
+
if (ALLOWED.has(relPath)) continue;
|
|
116
|
+
const text = fs.readFileSync(abs, 'utf8');
|
|
117
|
+
const hits = findEscapeTables(text);
|
|
118
|
+
if (hits.length) {
|
|
119
|
+
violations.push(`${relPath}:${hits[0]} — a private HTML escaper (a quoted < literal: an escape table). Use the shared one: `
|
|
120
|
+
+ 'window.OTB.escapeHtml in the browser, escapeHtml from src/html-escape.js (or api.escapeHtml in a module) on the server.');
|
|
121
|
+
}
|
|
122
|
+
const fakes = findFakeDelegates(text, { browser: /^modules\/[^/]+\/public\//.test(relPath) });
|
|
123
|
+
if (fakes.length) {
|
|
124
|
+
violations.push(`${relPath}:${fakes[0]} — defines a local HTML escaper name that does not point at a shared escaper `
|
|
125
|
+
+ '(a fallback like `(s) => String(s)` escapes nothing). Delegate to window.OTB.escapeHtml / api.escapeHtml / src/html-escape.js.');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const hallDir = path.join(root, 'modules', 'hall-ui', 'public');
|
|
130
|
+
let pages = 0;
|
|
131
|
+
for (const abs of walk(hallDir, []).filter((f) => f.endsWith('.html'))) {
|
|
132
|
+
pages++;
|
|
133
|
+
const problem = hallPageOrderProblem(fs.readFileSync(abs, 'utf8'));
|
|
134
|
+
if (problem) violations.push(`${path.relative(root, abs).split(path.sep).join('/')} — ${problem}`);
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
name: 'one HTML escaper per side (task 1004876)',
|
|
138
|
+
ok: violations.length === 0,
|
|
139
|
+
hardFail: violations.length > 0,
|
|
140
|
+
violations,
|
|
141
|
+
warnings: [],
|
|
142
|
+
note: `${scanned} code files hold no private escape table (allowed: ${[...ALLOWED.keys()].join(', ')}); `
|
|
143
|
+
+ `${pages} hall pages load dom-utils.js first.`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = { checkOneHtmlEscaper, findEscapeTables, findFakeDelegates, hallPageOrderProblem, ALLOWED };
|
|
@@ -523,7 +523,7 @@ const KERNEL_FILES = [
|
|
|
523
523
|
'src/module-api.js', // the published doorway
|
|
524
524
|
'src/modules.js', // the registry/resolver facade
|
|
525
525
|
'src/module-seams.js', // the seam registry (ports/events/contributions)
|
|
526
|
-
'src/role-responsibilities.js',
|
|
526
|
+
'src/role-responsibilities.js', 'src/html-escape.js', // owner's role statements (task 1003732) + THE server HTML escaper (task 1004876) — both import nothing
|
|
527
527
|
'src/module-loader/loader.js', // the disk loader (discover → validate → mount)
|
|
528
528
|
'src/module-loader/manifest-schema.js', // the manifest validator
|
|
529
529
|
'src/module-loader/semver.js', // coreVersion compatibility
|
|
@@ -1371,6 +1371,7 @@ const CHECKS = [
|
|
|
1371
1371
|
// Task 1003206 carved this file from 2,004 lines to ~1,350, so that toll is paid
|
|
1372
1372
|
// and the check gets its own line back.
|
|
1373
1373
|
require('./version-literal-guard.js').checkNoVersionLiterals,
|
|
1374
|
+
require('./escape-helper-guard.js').checkOneHtmlEscaper, // task 1004876: ONE HTML escaper per side (dom-utils.js / src/html-escape.js), and hall pages load dom-utils first
|
|
1374
1375
|
require('./dev-box-guard.js').checkNoDevBox, // task 1003899 / ADR 0346: the retired dev box stays out (its exemption list lives in that file)
|
|
1375
1376
|
require('./control-manifest.js').checkControlsExist, // Check 21 — task 1003131: goal-1000076 controls assert their own existence (the ADR 0022 assurance-of-assurance)
|
|
1376
1377
|
require('./routine-schedule.js').checkRoutineCadencesScheduled, // Check 25 — task 1003209: a routine that declares a cadence must have a scheduler firing it AT that period (ADR 0188)
|
|
@@ -1436,7 +1437,7 @@ module.exports = {
|
|
|
1436
1437
|
scanForRawNul,
|
|
1437
1438
|
checkVocabularyWall, VOCAB_ALLOW, VOCAB_CITATION, VOCAB_EXTS, vocabFilesToScan,
|
|
1438
1439
|
checkGoalMembershipVocabulary, GOAL_VOCAB_SURFACE, checkGovernmentRenameIdentity, RENAME_IDENTITY_RE, checkPrelaunchVocabulary, PRELAUNCH_SURFACE, // 19c task 1003120, 19d task 1003152
|
|
1439
|
-
runAll,
|
|
1440
|
+
runAll, CHECKS, // CHECKS: so a test can prove a check is registered, not just written (task 1004876)
|
|
1440
1441
|
resolvesToHostGame,
|
|
1441
1442
|
checkCoreHostBoundary,
|
|
1442
1443
|
checkClaudeMdBudget,
|
|
@@ -112,10 +112,7 @@ function renderMonospaceKeyCard(model) {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
// ---- HTML renderer (the in-feed branded card) ---------------------------
|
|
115
|
-
|
|
116
|
-
return String(s == null ? '' : s).replace(/[&<>"']/g, (c) =>
|
|
117
|
-
({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
118
|
-
}
|
|
115
|
+
const { escapeHtml: esc } = require('../../src/html-escape'); // the one server escaper (task 1004876)
|
|
119
116
|
function renderKeyCardHtml(model) {
|
|
120
117
|
const m = model;
|
|
121
118
|
const bg = 'var(--color-background-warning)';
|
|
@@ -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
|
-
|
|
28
|
-
return String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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.
|
package/scripts/bongos/start.js
CHANGED
|
@@ -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 = (
|
|
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) {
|
package/scripts/bongos/status.js
CHANGED
|
@@ -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 = (
|
|
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 =
|
|
68
|
-
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
69
|
-
.replace(/"/g, '"').replace(/'/g, ''');
|
|
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
|
-
|
|
181
|
-
|
|
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
|
-
|
|
184
|
-
|
|
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
|
-
|
|
48
|
-
return String(s ?? '').replace(/[&<>"']/g, (c) => ({
|
|
49
|
-
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
|
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
|
-
//
|
|
268
|
-
|
|
269
|
-
return String(s == null ? '' : s)
|
|
270
|
-
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
271
|
-
.replace(/"/g, '"').replace(/'/g, ''');
|
|
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 = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
|
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.
|
|
82
|
+
const CORE_VERSION = '1.22.1'; // 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: () => ({
|
package/tests/craft_mode.mjs
CHANGED
|
@@ -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() {} }),
|
package/tests/craft_mode_nav.mjs
CHANGED
|
@@ -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
|
-
|
|
198
|
-
|
|
197
|
+
// The attribute is HTML-escaped AFTER the JS escape, so the apostrophe arrives as
|
|
198
|
+
// \' (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\\'hara-rule[^"]*)"/);
|
|
201
|
+
assert.ok(onclick, 'the apostrophe was not escaped for the JS string');
|
|
202
|
+
assert.ok(onclick[1].replace(/'/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');
|
package/tests/founding_mode.mjs
CHANGED
|
@@ -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),
|
|
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({
|