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:
+39
-1
@@ -56,7 +56,11 @@ only as `navigator.sendBeacon`; the MapLibre library's own static-text URLs (`ma
|
|||||||
`wiki.openstreetmap.org`, `github.com` diagnostics, `maplibre.invalid` protocol-test placeholder)
|
`wiki.openstreetmap.org`, `github.com` diagnostics, `maplibre.invalid` protocol-test placeholder)
|
||||||
and our own dataset's ODbL license strings (`opendatacommons.org`) are printed in the inventory as
|
and our own dataset's ODbL license strings (`opendatacommons.org`) are printed in the inventory as
|
||||||
a documented static-text class — never fetched; `http://www.w3.org/{2000/svg,1999/xhtml}` are W3C
|
a documented static-text class — never fetched; `http://www.w3.org/{2000/svg,1999/xhtml}` are W3C
|
||||||
spec identifiers.
|
spec identifiers. The SOURCE-side `runtime-hosts` gate carries the same kind of adjudication for
|
||||||
|
the `/help` Known-limits doc pointer (see the 2026-10-25 addendum below): the published repo
|
||||||
|
origin appears ONLY as a bare URL literal in the bot strings — user-clicked navigation, never
|
||||||
|
fetched — and the class is NARROW (a `fetch(` target naming the same host is still a violation;
|
||||||
|
pinned by the SA6 vector).
|
||||||
|
|
||||||
## Storage inventory
|
## Storage inventory
|
||||||
|
|
||||||
@@ -102,6 +106,7 @@ assumed, A4):
|
|||||||
| `maplibre.org`, `wiki.openstreetmap.org`, `github.com`, `maplibre.invalid` | documented static-text class | MapLibre's own attribution/diagnostic constants inside the bundled library — displayed text, never fetched |
|
| `maplibre.org`, `wiki.openstreetmap.org`, `github.com`, `maplibre.invalid` | documented static-text class | MapLibre's own attribution/diagnostic constants inside the bundled library — displayed text, never fetched |
|
||||||
| `opendatacommons.org` | documented static-text class | our own gazetteer/fixture ODbL license metadata strings |
|
| `opendatacommons.org` | documented static-text class | our own gazetteer/fixture ODbL license metadata strings |
|
||||||
| `www.w3.org` | documented spec identifiers | SVG/XHTML namespace constants — never fetched |
|
| `www.w3.org` | documented spec identifiers | SVG/XHTML namespace constants — never fetched |
|
||||||
|
| `gitmirah` (the published repo origin) | documented static-text class (SOURCE-side, URL literals only) | the `/help` Known-limits doc pointer in `bot/src/i18n/strings.ts` (D-08/D-09) — user-facing copy in a chat card, never fetched; a `fetch(` to the same host is still a violation (SA6) |
|
||||||
|
|
||||||
**ZERO telemetry (D-02):** no analytics, no beacons, no tracking pixels, no third-party CDN, no
|
**ZERO telemetry (D-02):** no analytics, no beacons, no tracking pixels, no third-party CDN, no
|
||||||
error-reporting host, no social pixels — the shipped bundle contains no analytics or beacon token
|
error-reporting host, no social pixels — the shipped bundle contains no analytics or beacon token
|
||||||
@@ -125,6 +130,39 @@ expiry metadata, deployed cache headers) were observed NOT RUN in this environme
|
|||||||
re-run with `npm run privacy-audit -- --live` at the launch gate — exit 0 only when they PASS,
|
re-run with `npm run privacy-audit -- --live` at the launch gate — exit 0 only when they PASS,
|
||||||
never when they are skipped.
|
never when they are skipped.
|
||||||
|
|
||||||
|
## 2026-10-25 — Phase 7 gate-prep re-run: the /help doc pointer adjudication
|
||||||
|
|
||||||
|
**Why this addendum exists:** plan 07-05's committed `/help` Known-limits pointer (commit
|
||||||
|
`78482ab`) put the repo's actual published-origin URL (`http://gitmirah:3000/c-fu/arah2/…`,
|
||||||
|
the maintainer's Gitea remote per `git remote -v`) into the bot strings (en + ms), and the
|
||||||
|
`runtime-hosts` gate — a positive allowlist with no static-text escape — flagged it as 2
|
||||||
|
violations (`PRIVACY AUDIT FAILED`). Logged in the phase's `deferred-items.md`; fixed by the
|
||||||
|
07-06 gate prep, following the audit's OWN laws (the MapLibre static-text adjudication
|
||||||
|
precedent), not by weakening them.
|
||||||
|
|
||||||
|
**The fix (source-side, `scripts/lib/source-gates.mjs`):**
|
||||||
|
- A documented `DOC_POINTER_STATIC_TEXT_HOSTS` class on the SOURCE-side `runtime-hosts` gate:
|
||||||
|
the published repo origin may appear ONLY as a bare URL literal in bot copy (the /help
|
||||||
|
Known-limits line — user-clicked navigation, never fetched). Each entry names its reason;
|
||||||
|
forks replace it with their own published origin.
|
||||||
|
- The class is **NARROW by construction and pinned by the SA6 vector**: it is consulted ONLY
|
||||||
|
for bare URL literals. A `fetch(` target naming the same host is STILL a violation. This
|
||||||
|
required fixing a latent gate bug (SA6's RED proved it): the `fetch(` call-site handler's
|
||||||
|
declaration heuristic (`argText.includes(":")`) misclassified any QUOTED absolute URL as a
|
||||||
|
TS method declaration and skipped it — a quoted string is always a target, so quoted fetch
|
||||||
|
targets now reach the host check.
|
||||||
|
- **Result:** `npm run privacy-audit` → `runtime-hosts: PASS — host inventory (printed, not
|
||||||
|
assumed): www.w3.org×3, gitmirah×2, telegram.org×1; 4 relative fetch target(s) to the app's
|
||||||
|
own Worker, 1 fetch declaration(s) skipped; every absolute URL host is in the positive
|
||||||
|
allowlist [tiles.openfreemap.org, telegram.org] or the documented doc-pointer static-text
|
||||||
|
class (URL literals only — never fetched)` → **PRIVACY AUDIT CLEAN — 5 gate(s) passed,
|
||||||
|
0 violations**; the suite is 13/13 (SA1–6, BG1–2, CP1–2, AU1–3). The Findings table above
|
||||||
|
remains the 07-03 run's historical record; this section is the gate-time re-run record.
|
||||||
|
|
||||||
|
**Driver-resolution caveat (unchanged from 07-05's decision):** the URL resolves for a driver
|
||||||
|
only once the D-09 launch checklist's "repo pushed public" step lands main on the remote —
|
||||||
|
the launch gate verifies the pointer line resolves before signing.
|
||||||
|
|
||||||
## Signature
|
## Signature
|
||||||
|
|
||||||
The audit record is SIGNED BY THE HUMAN at the launch gate (the tool produces the evidence; the
|
The audit record is SIGNED BY THE HUMAN at the launch gate (the tool produces the evidence; the
|
||||||
|
|||||||
@@ -230,6 +230,17 @@ export function runLogVocabularyGate(opts = {}) {
|
|||||||
* allowed. */
|
* allowed. */
|
||||||
const ALLOWED_FETCH_HOSTS = ["tiles.openfreemap.org", "telegram.org"];
|
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):
|
/** Documented, deliberate scope adjudication (the theme-audit precedent):
|
||||||
* the SVG and XHTML namespace URIs are W3C spec IDENTIFIERS
|
* the SVG and XHTML namespace URIs are W3C spec IDENTIFIERS
|
||||||
* (createElementNS / innerHTML parsing), never fetched. Any other http://
|
* (createElementNS / innerHTML parsing), never fetched. Any other http://
|
||||||
@@ -263,12 +274,15 @@ export function runRuntimeHostGate(opts = {}) {
|
|||||||
hostInventory.set(host, (hostInventory.get(host) ?? 0) + 1);
|
hostInventory.set(host, (hostInventory.get(host) ?? 0) + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkUrl(rel, urlText, where) {
|
function checkUrl(rel, urlText, where, fromFetch = false) {
|
||||||
noteHost(urlText);
|
noteHost(urlText);
|
||||||
const host = hostOf(urlText);
|
const host = hostOf(urlText);
|
||||||
if (host === null) return; // not a parseable URL — leave it to the bundle scan / live probe
|
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 (SPEC_NAMESPACE_IDENTIFIERS.includes(urlText)) return; // documented adjudication — a spec identifier, never fetched
|
||||||
if (ALLOWED_FETCH_HOSTS.includes(host)) return;
|
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}`);
|
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)) {
|
for (const m of code.matchAll(/\bfetch\s*\(/g)) {
|
||||||
const rest = code.slice(m.index + m[0].length, code.indexOf("\n", m.index));
|
const rest = code.slice(m.index + m[0].length, code.indexOf("\n", m.index));
|
||||||
const argText = rest.split(/(?<![^,(]\()[,)]/)[0]?.trim() ?? rest.trim();
|
const argText = rest.split(/(?<![^,(]\()[,)]/)[0]?.trim() ?? rest.trim();
|
||||||
if (argText.includes(":") || argText === "") {
|
if (argText === "") {
|
||||||
fetchDeclarations += 1;
|
fetchDeclarations += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const literal = argText.match(/^["'`]([^"'`]*)/);
|
const literal = argText.match(/^["'`]([^"'`]*)/);
|
||||||
if (literal === null) {
|
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}`);
|
violations.push(`${rel} — fetch( first argument is dynamic, not statically verifiable: ${argText}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -299,7 +322,7 @@ export function runRuntimeHostGate(opts = {}) {
|
|||||||
fetchTargets.push(`${rel} — ${target}`);
|
fetchTargets.push(`${rel} — ${target}`);
|
||||||
if (target.startsWith("/")) continue; // relative — the app's own Worker origin
|
if (target.startsWith("/")) continue; // relative — the app's own Worker origin
|
||||||
if (/^https?:\/\//.test(target)) {
|
if (/^https?:\/\//.test(target)) {
|
||||||
checkUrl(rel, target, "fetch target");
|
checkUrl(rel, target, "fetch target", true);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
violations.push(`${rel} — fetch( target is neither relative nor an approved absolute URL: ${target}`);
|
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 inventory = [...hostInventory.entries()].map(([h, n]) => `${h}×${n}`).join(", ") || "none";
|
||||||
const skippedNote = skipped.length > 0 ? ` (skipped, absent: ${skipped.join(", ")})` : "";
|
const skippedNote = skipped.length > 0 ? ` (skipped, absent: ${skipped.join(", ")})` : "";
|
||||||
const evidence = pass
|
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("; ")}`;
|
: `host inventory: ${inventory}; ${violations.length} violation(s): ${violations.join("; ")}`;
|
||||||
return { id, pass, evidence, violations: violations.length };
|
return { id, pass, evidence, violations: violations.length };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user