From 75c16ac32d67fd3acac5c590ca694e4b004f86bf Mon Sep 17 00:00:00 2001 From: doxa-maps-88655c70 <88627970+doxa-maps-88655c70@users.noreply.github.com> Date: Thu, 24 Sep 2026 02:23:47 +0000 Subject: [PATCH] Hygiene gate: process-vocabulary patterns + JWT claim decoder (public-exposure audit) Two widenings from the 2026-09-24 anonymous-clone audit: 1. LEAK_STRICT: role nouns (the Driver/Driver), private repo/org names, forge/platform names, operational-context vocabulary, CI workflow paths. Every one was found live in a public mirror before this rule existed. WARNINGS in the normal build (the source tree still carries them; failing build:unlocked overnight gets a gate disabled), FINDINGS under --strict -- which the publish/export flow must run: publishing is a gated action, not a habit. 2. A credential is a DOCUMENT, not a string: every JWT-shaped value's payload is decoded and name-like claims flagged. The pk token accepted by the 2026-09-21 ruling carries a u claim naming a real account -- the ruling covered the token's scope, not the identity inside it. Both modes negative-tested: strict exits 1 against the current tree (110 findings), normal exits 0 (118 warnings). --- internal/public-hygiene-check.mjs | 34 +++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/internal/public-hygiene-check.mjs b/internal/public-hygiene-check.mjs index a4684cc86..1b573a804 100644 --- a/internal/public-hygiene-check.mjs +++ b/internal/public-hygiene-check.mjs @@ -63,6 +63,22 @@ const LEAK = [ [/\beco-dx\b/, 'internal folder name (eco-dx)'], [/qa-cycle/, 'internal folder name (qa-cycle)'], [/\b_archive\b/, 'internal folder name (_archive)'], [/localhost:(5174|3001|3033)\b/, 'dev-server URL'], ] +// ★ ADDED 2026-09-24 (public-exposure audit, policies P3/P4/P7). A public tree carries +// ARTIFACTS, never PROCESS or operational context — each of these was found live in a public +// mirror by anonymous clone before this rule existed. They are WARNINGS in the normal build +// (the source tree's own public/ still carries them, and failing build:unlocked for every +// writer overnight would get this gate disabled) and FINDINGS under --strict, which the +// publish/export flow MUST run (policy P5: publishing is a gated action, not a habit). +const LEAK_STRICT = [ + [/\bthe Driver\b/, 'internal role noun (the Driver) — process vocabulary'], + [/\bDriver\b/, 'internal role noun (Driver) — process vocabulary'], + [/doxa-life\/doxa-maps/, 'private repo name'], + [/\bdoxa-life\b/, 'private org name'], + [/\bforgejo\b/i, 'self-hosted forge name'], + [/\bnuxtbase\b/, 'internal platform name'], + [/\bpersecution\b/, 'operational-context vocabulary (allowed only inside data CSVs)'], + [/\.forgejo\/workflows/, 'CI workflow path — process, never ships in a public tree'], +] // ★ WIDENED 2026-09-21. This held ONE pattern (the Mapbox JWT), on the reasoning that public/ // is a tree we GENERATE, so the only realistic leak is the token the build itself writes. // THAT REASONING WAS WRONG: 855 files under public/ are TRACKED, 95 of them text — boundaries, @@ -106,6 +122,24 @@ for (const f of files) { if (!isText || f.size > 40 * 1024 * 1024) continue const t = readFileSync(f.abs, 'utf8') for (const [re, why] of LEAK) if (re.test(t)) hit(findings, 'leaks', f.rel, why) + for (const [re, why] of LEAK_STRICT) if (re.test(t) && !/\.csv$/.test(f.rel)) hit(STRICT ? findings : warnings, 'leaks', f.rel, why) + // ★ P2 (2026-09-24): a credential is a DOCUMENT, not a string. Every JWT-shaped value gets + // its payload DECODED and its claims reviewed: 'it is a public token' is not sufficient — + // the pk. accepted by the 2026-09-21 ruling turned out to carry a `u` claim naming a real, + // searchable ACCOUNT in base64 anyone can decode. The ruling covered the token's scope, + // not the identity inside it. Flag any claim value that looks like a meaningful name. + for (const m of t.matchAll(/\b(?:pk|sk|tk)\.(eyJ[A-Za-z0-9_-]{8,})/g)) { + try { + const pad = m[1] + '='.repeat((4 - (m[1].length % 4)) % 4) + const claims = JSON.parse(Buffer.from(pad, 'base64').toString('utf8')) + for (const [k, v] of Object.entries(claims)) { + if (typeof v === 'string' && /^[a-z][a-z0-9_-]{3,}$/i.test(v) && !/^[0-9a-f]+$/i.test(v)) { + hit(STRICT ? findings : warnings, 'secrets', f.rel, `JWT claim "${k}" carries a name-like value (${v.length} chars) — decode + review before publishing`) + break + } + } + } catch { /* not decodable — not a JWT payload */ } + } for (const [re, why] of SECRET) if (re.test(t)) hit(findings, 'secrets', f.rel, why) if (/sourceMappingURL=/.test(t)) hit(warnings, 'debug', f.rel, 'sourceMappingURL comment (map file absent → harmless, but points at a missing map)') if (ext === '.js' && /console\.(log|debug)\(/.test(t)) hit(warnings, 'debug', f.rel, 'console.log/debug in a shipped bundle')