Skip to content

Commit 7210fd3

Browse files
VickyXAI1bcMax
andauthored
chore(brand): vendor the hardened sync-brand-numbers.mjs (assertRenderable/escAttr) (#388)
Verbatim from blockrun-mcp f9480ad2. A brand value fetched from the mirror is now refused, and attribute-escaped, before the unattended brand-sync bot writes it into this repo's markdown. --check output unchanged here. Co-authored-by: 1bcMax <viewitter@gmail.com>
1 parent 05de1e0 commit 7210fd3

1 file changed

Lines changed: 70 additions & 4 deletions

File tree

scripts/sync-brand-numbers.mjs

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,19 @@
1818
* Markers look like: <!-- br:models.chatVisible -->66<!-- /br:models.chatVisible -->
1919
* and wrap the WHOLE token, so a badge URL, its alt text and the prose number
2020
* can all regenerate from one key.
21+
*
22+
* THIS COPY IS AHEAD OF THE SOURCE. blockrun's `brand-script-sync` CI job
23+
* diffs every consumer against brand/sync-brand-numbers.mjs and its printed
24+
* remediation is "copy the source over the consumer" — twice that overwrote a
25+
* fix made here (#84, #128). What this copy carries that the source does not,
26+
* as of 2026-09-13: assertRenderable + escAttr (a value from the mirror is
27+
* refused, and attribute-escaped, before it is written into a README that the
28+
* brand-sync bot then pushes unattended with contents:write), keyOf() on the
29+
* keys-in-use count, and the --check summary that does not say "up to date"
30+
* under a list of stale fenced markers. Resync source <- consumer: land THIS
31+
* file in blockrun/brand and fan it out; do not copy the source over it.
32+
* test/brand-sync-script.test.ts fails on a copy without the guard, so a
33+
* consumer <- source resync cannot pass this repo's required `test` check.
2134
*/
2235
import { execFileSync } from "node:child_process";
2336
import { existsSync, lstatSync, readFileSync, writeFileSync, readdirSync } from "node:fs";
@@ -101,15 +114,58 @@ function flatten(obj, prefix = "") {
101114
* Renderers are registered under the FULL marker name so a badge's label is
102115
* written out rather than guessed from the key.
103116
*/
117+
/**
118+
* What a brand value is allowed to be, checked at the moment it is USED.
119+
*
120+
* These values arrive over the network from blockrun.ai (or the
121+
* awesome-blockrun mirror) and are written verbatim into README.md,
122+
* CONTRIBUTING.md and skills/*\/SKILL.md, which `.github/workflows/brand-sync.yml`
123+
* then commits and pushes to the default branch weekly, unattended, with
124+
* `contents: write`. Rendering was `String(value)` and the badge renderer
125+
* interpolated straight into `src="..."` and `alt="..."`, so a value carrying
126+
* a quote or an angle bracket closed the attribute and injected markup into
127+
* every consuming repo's README. Write access to one mirror repo was enough.
128+
*
129+
* Checked here rather than over the whole artifact on purpose: the payload
130+
* legitimately carries prose fields we never render (`savings.baselineModel`
131+
* is a string), and refusing those would break the sync on an unrelated
132+
* addition upstream.
133+
*/
134+
const SAFE_TEXT = /^[\p{L}\p{N} .,%+/·-]{1,64}$/u;
135+
136+
function assertRenderable(marker, value) {
137+
const what = () => `${marker} = ${JSON.stringify(value)}`;
138+
if (typeof value === "number") {
139+
if (!Number.isFinite(value)) fail(`brand-numbers: refusing to render ${what()} — not a finite number`);
140+
return value;
141+
}
142+
if (typeof value === "string") {
143+
if (!SAFE_TEXT.test(value)) {
144+
fail(
145+
`brand-numbers: refusing to render ${what()} — a rendered value must be ` +
146+
`a number or a short plain label. This value would be written verbatim ` +
147+
`into README/CONTRIBUTING/SKILL.md and pushed by the brand-sync bot.`,
148+
);
149+
}
150+
return value;
151+
}
152+
fail(`brand-numbers: refusing to render ${what()} — expected a number or a string, got ${Array.isArray(value) ? "an array" : typeof value}`);
153+
}
154+
155+
/** Escape for an HTML attribute. Belt to assertRenderable's braces. */
156+
const escAttr = (v) =>
157+
String(v).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
158+
.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
159+
104160
const badge = (label) => (n) =>
105-
`<img src="https://img.shields.io/badge/${label}-${n}-5B9BF6?style=flat-square&labelColor=0B0A0F" alt="${n} ${label}">`;
161+
`<img src="https://img.shields.io/badge/${label}-${escAttr(n)}-5B9BF6?style=flat-square&labelColor=0B0A0F" alt="${escAttr(n)} ${label}">`;
106162

107163
const RENDER = {
108164
"mcp.tools@badge": badge("tools"),
109165
"models.totalVisible@badge": badge("models"),
110166
"models.chatVisible@badge": badge("models"),
111167
};
112-
const render = (marker, value) => (RENDER[marker] ?? String)(value);
168+
const render = (marker, value) => (RENDER[marker] ?? String)(assertRenderable(marker, value));
113169

114170
/** `mcp.tools@badge` looks up `mcp.tools`. Unmodified markers are unaffected. */
115171
const keyOf = (marker) => marker.split("@")[0];
@@ -308,7 +364,8 @@ const everUsed = new Set();
308364

309365
for (const file of walk(ROOT)) {
310366
const { before, after, changed, used } = syncFile(file, numbers, problems, skipped);
311-
used.forEach((k) => everUsed.add(k));
367+
// keyOf: mcp.tools and mcp.tools@badge are ONE key in use, not two.
368+
used.forEach((k) => everUsed.add(keyOf(k)));
312369
if (!changed) continue;
313370
drifted.push({ file: relative(ROOT, file), before, after });
314371
if (!check) writeFileSync(file, after);
@@ -332,7 +389,16 @@ if (problems.length) {
332389

333390
if (check) {
334391
if (drifted.length === 0) {
335-
console.log(`brand-numbers: up to date (${everUsed.size} keys in use)`);
392+
// Do not say "up to date" straight after listing markers known to be
393+
// stale. The skip stays non-fatal for the reason above, but a CI log that
394+
// prints the stale ones and then declares everything current is a log
395+
// nobody reads twice.
396+
console.log(
397+
skipped.length
398+
? `brand-numbers: no drift outside code fences (${everUsed.size} keys in use), ` +
399+
`but ${skipped.length} fenced marker(s) listed above are stale — add @live to sync them`
400+
: `brand-numbers: up to date (${everUsed.size} keys in use)`,
401+
);
336402
process.exit(0);
337403
}
338404
console.error("brand-numbers: these files disagree with brand-numbers.json\n");

0 commit comments

Comments
 (0)