fix(07-06): the /help doc pointer is a documented static-text class — privacy audit green again
- source-side runtime-hosts gate gains DOC_POINTER_STATIC_TEXT_HOSTS (the MapLibre-attribution precedent): the published repo origin may appear ONLY as a bare URL literal in bot copy (D-08/D-09), never fetched - class is NARROW and pinned by SA6: a fetch( target naming the same host is still a violation - Rule 1 (found by SA6's RED): the fetch( handler's declaration heuristic misclassified any quoted absolute URL as a TS declaration — quoted strings are targets; they now reach the host check - docs/PRIVACY-AUDIT.md: adjudication documented + dated re-run addendum - deferred-items.md #1 closed: npm run privacy-audit exits 0 again (PRIVACY AUDIT CLEAN — 5 gate(s) passed, 0 violations; suite 13/13)
This commit is contained in:
@@ -230,6 +230,17 @@ export function runLogVocabularyGate(opts = {}) {
|
||||
* allowed. */
|
||||
const ALLOWED_FETCH_HOSTS = ["tiles.openfreemap.org", "telegram.org"];
|
||||
|
||||
/** Documented static-text hosts (adjudication, the MapLibre-attribution
|
||||
* precedent in bundle-gates.mjs) — user-facing DOC-POINTER copy in the bot
|
||||
* strings (the /help Known-limits line, D-08), never fetched at runtime.
|
||||
* Scoped NARROW by construction: this class is consulted ONLY for bare URL
|
||||
* literals — a `fetch(` target naming one of these hosts is still a
|
||||
* violation. Forks: replace with your own published repo origin. */
|
||||
const DOC_POINTER_STATIC_TEXT_HOSTS = {
|
||||
gitmirah:
|
||||
"the maintainer's published Gitea origin — the /help Known-limits doc pointer (D-08/D-09's 'repo pushed public' remote), user-clicked navigation, never fetched",
|
||||
};
|
||||
|
||||
/** Documented, deliberate scope adjudication (the theme-audit precedent):
|
||||
* the SVG and XHTML namespace URIs are W3C spec IDENTIFIERS
|
||||
* (createElementNS / innerHTML parsing), never fetched. Any other http://
|
||||
@@ -263,12 +274,15 @@ export function runRuntimeHostGate(opts = {}) {
|
||||
hostInventory.set(host, (hostInventory.get(host) ?? 0) + 1);
|
||||
}
|
||||
|
||||
function checkUrl(rel, urlText, where) {
|
||||
function checkUrl(rel, urlText, where, fromFetch = false) {
|
||||
noteHost(urlText);
|
||||
const host = hostOf(urlText);
|
||||
if (host === null) return; // not a parseable URL — leave it to the bundle scan / live probe
|
||||
if (SPEC_NAMESPACE_IDENTIFIERS.includes(urlText)) return; // documented adjudication — a spec identifier, never fetched
|
||||
if (ALLOWED_FETCH_HOSTS.includes(host)) return;
|
||||
// the doc-pointer static-text class applies ONLY to bare URL literals —
|
||||
// a fetch target naming one of these hosts is still a violation
|
||||
if (!fromFetch && DOC_POINTER_STATIC_TEXT_HOSTS[host] !== undefined) return;
|
||||
violations.push(`${rel} — absolute URL to an unapproved host (${where}): ${host}`);
|
||||
}
|
||||
|
||||
@@ -286,12 +300,21 @@ export function runRuntimeHostGate(opts = {}) {
|
||||
for (const m of code.matchAll(/\bfetch\s*\(/g)) {
|
||||
const rest = code.slice(m.index + m[0].length, code.indexOf("\n", m.index));
|
||||
const argText = rest.split(/(?<![^,(]\()[,)]/)[0]?.trim() ?? rest.trim();
|
||||
if (argText.includes(":") || argText === "") {
|
||||
if (argText === "") {
|
||||
fetchDeclarations += 1;
|
||||
continue;
|
||||
}
|
||||
const literal = argText.match(/^["'`]([^"'`]*)/);
|
||||
if (literal === null) {
|
||||
// TS method DECLARATIONS (`fetch(req: Request, …)`) are skipped — the
|
||||
// annotation colon identifies them. A quoted string is always a
|
||||
// TARGET (its URL colon must not masquerade as an annotation) —
|
||||
// fixed for SA6: the doc-pointer class is URL literals only, so a
|
||||
// quoted absolute fetch target must still reach checkUrl.
|
||||
if (argText.includes(":")) {
|
||||
fetchDeclarations += 1;
|
||||
continue;
|
||||
}
|
||||
violations.push(`${rel} — fetch( first argument is dynamic, not statically verifiable: ${argText}`);
|
||||
continue;
|
||||
}
|
||||
@@ -299,7 +322,7 @@ export function runRuntimeHostGate(opts = {}) {
|
||||
fetchTargets.push(`${rel} — ${target}`);
|
||||
if (target.startsWith("/")) continue; // relative — the app's own Worker origin
|
||||
if (/^https?:\/\//.test(target)) {
|
||||
checkUrl(rel, target, "fetch target");
|
||||
checkUrl(rel, target, "fetch target", true);
|
||||
continue;
|
||||
}
|
||||
violations.push(`${rel} — fetch( target is neither relative nor an approved absolute URL: ${target}`);
|
||||
@@ -318,7 +341,7 @@ export function runRuntimeHostGate(opts = {}) {
|
||||
const inventory = [...hostInventory.entries()].map(([h, n]) => `${h}×${n}`).join(", ") || "none";
|
||||
const skippedNote = skipped.length > 0 ? ` (skipped, absent: ${skipped.join(", ")})` : "";
|
||||
const evidence = pass
|
||||
? `host inventory (printed, not assumed): ${inventory}; ${fetchTargets.length} relative fetch target(s) to the app's own Worker, ${fetchDeclarations} fetch declaration(s) skipped; every absolute URL host is in the positive allowlist [${ALLOWED_FETCH_HOSTS.join(", ")}]${skippedNote}`
|
||||
? `host inventory (printed, not assumed): ${inventory}; ${fetchTargets.length} relative fetch target(s) to the app's own Worker, ${fetchDeclarations} fetch declaration(s) skipped; every absolute URL host is in the positive allowlist [${ALLOWED_FETCH_HOSTS.join(", ")}] or the documented doc-pointer static-text class (URL literals only — never fetched)${skippedNote}`
|
||||
: `host inventory: ${inventory}; ${violations.length} violation(s): ${violations.join("; ")}`;
|
||||
return { id, pass, evidence, violations: violations.length };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user