fix(07-03): fold the follow-up fixes described in 07-03-SUMMARY
- asset-header probe reads HTTP status BEFORE interpreting headers — a non-2xx is deployment drift (the notRun exit-2 class), never a cache violation; the violations.length count bug fixed - XHTML namespace URI joins the SVG spec-identifier adjudication - probeUnavailable carries the reason only (the CLI prints NOT RUN) - CP2/AU1 suite vectors updated to the corrected contracts Residue note: these edits were on the tree during 07-03/07-04 execution but escaped those plans' commits (07-04-SUMMARY residue caution).
This commit is contained in:
@@ -37,9 +37,10 @@ const WRANGLER_JSONC = path.join(REPO_ROOT, "bot", "wrangler.jsonc");
|
|||||||
* family is REPORTED, never assumed absent (07-RESEARCH A5). */
|
* family is REPORTED, never assumed absent (07-RESEARCH A5). */
|
||||||
export const KEY_FAMILIES = ["session:", "chat:", "loc:", "guidance:"];
|
export const KEY_FAMILIES = ["session:", "chat:", "loc:", "guidance:"];
|
||||||
|
|
||||||
/** A missing environment is the exit-2 class: pass:false + notRun:true. */
|
/** A missing environment is the exit-2 class: pass:false + notRun:true. The
|
||||||
|
* evidence carries the REASON only — the CLI prints the NOT RUN status. */
|
||||||
export function probeUnavailable(reason) {
|
export function probeUnavailable(reason) {
|
||||||
return { id: "probe", pass: false, notRun: true, evidence: `NOT RUN — ${reason}` };
|
return { id: "probe", pass: false, notRun: true, evidence: reason };
|
||||||
}
|
}
|
||||||
|
|
||||||
function notRun(id, reason) {
|
function notRun(id, reason) {
|
||||||
@@ -178,6 +179,12 @@ function categoryOf(err) {
|
|||||||
* `cf-cache-status` headers VERBATIM. CF-Cache-Status is probabilistic per
|
* `cf-cache-status` headers VERBATIM. CF-Cache-Status is probabilistic per
|
||||||
* Cloudflare's own footnote — the evidence prints it, it does not interpret
|
* Cloudflare's own footnote — the evidence prints it, it does not interpret
|
||||||
* it as a guarantee (07-RESEARCH Pitfall 10).
|
* it as a guarantee (07-RESEARCH Pitfall 10).
|
||||||
|
*
|
||||||
|
* Honesty laws: every leg's HTTP status is read BEFORE any header is
|
||||||
|
* interpreted — a non-2xx is deployment/environment drift (the notRun
|
||||||
|
* class), never a cache-header violation. The immutable `/assets/*` policy
|
||||||
|
* itself is `web/public/_headers`' claim (07-04) — this probe quotes, it
|
||||||
|
* does not assert the policy.
|
||||||
*/
|
*/
|
||||||
export function runAssetHeaderProbe(opts = {}) {
|
export function runAssetHeaderProbe(opts = {}) {
|
||||||
const id = "asset-headers";
|
const id = "asset-headers";
|
||||||
@@ -193,7 +200,7 @@ export function runAssetHeaderProbe(opts = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const lines = [];
|
const lines = [];
|
||||||
let violations = 0;
|
const observed = [];
|
||||||
const targets = [["HTML entry /", `${baseUrl}/`]];
|
const targets = [["HTML entry /", `${baseUrl}/`]];
|
||||||
if (assetPath !== null) targets.push([`hashed asset ${assetPath}`, `${baseUrl}${assetPath}`]);
|
if (assetPath !== null) targets.push([`hashed asset ${assetPath}`, `${baseUrl}${assetPath}`]);
|
||||||
try {
|
try {
|
||||||
@@ -203,23 +210,31 @@ export function runAssetHeaderProbe(opts = {}) {
|
|||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
});
|
});
|
||||||
|
const status = Number(out.match(/^HTTP\/[\d.]+\s+(\d+)/m)?.[1] ?? 0);
|
||||||
const cacheControl = out.match(/^cache-control:.*$/im)?.[0]?.trim() ?? null;
|
const cacheControl = out.match(/^cache-control:.*$/im)?.[0]?.trim() ?? null;
|
||||||
const cfStatus = out.match(/^cf-cache-status:.*$/im)?.[0]?.trim() ?? null;
|
const cfStatus = out.match(/^cf-cache-status:.*$/im)?.[0]?.trim() ?? null;
|
||||||
if (cacheControl === null) {
|
observed.push(
|
||||||
violations += 1;
|
`${label}: HTTP ${status || "unknown"}${cacheControl !== null ? ` | ${cacheControl}` : " | no cache-control"}${cfStatus !== null ? ` | ${cfStatus}` : ""}`,
|
||||||
lines.push(`${label}: no cache-control header observed`);
|
);
|
||||||
} else {
|
if (!Number.isInteger(status) || status < 200 || status >= 400) {
|
||||||
lines.push(`${label}: ${cacheControl}${cfStatus !== null ? ` | ${cfStatus}` : " | cf-cache-status: not reported by this response"}`);
|
return notRun(
|
||||||
|
id,
|
||||||
|
`deployment drift — ${label} answered HTTP ${status || "unknown"} ` +
|
||||||
|
`(the deployed origin does not match web/dist); observed: ${observed.join(" || ")}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
if (cacheControl === null) lines.push(`${label}: NO cache-control header observed`);
|
||||||
|
else lines.push(`${label}: ${cacheControl} | ${cfStatus ?? "cf-cache-status not reported"}`);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return notRun(id, `origin unreachable (${categoryOf(err)})`);
|
return notRun(id, `origin unreachable (${categoryOf(err)})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const violations = lines.filter((l) => l.includes("NO cache-control")).length;
|
||||||
const pass = violations === 0;
|
const pass = violations === 0;
|
||||||
const evidence = pass
|
const evidence = pass
|
||||||
? `${targets.length} response header(s), VERBATIM (cf-cache-status is probabilistic — reported, not interpreted as a guarantee): ${lines.join(" || ")}`
|
? `${targets.length} response header(s), VERBATIM (cf-cache-status is probabilistic — quoted, never interpreted as a guarantee): ${lines.join(" || ")}`
|
||||||
: `${violations.length} violation(s): ${lines.join(" || ")}`;
|
: `${violations} violation(s): ${lines.join(" || ")}`;
|
||||||
return { id, pass, evidence, violations };
|
return { id, pass, evidence, violations };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -231,9 +231,10 @@ export function runLogVocabularyGate(opts = {}) {
|
|||||||
const ALLOWED_FETCH_HOSTS = ["tiles.openfreemap.org", "telegram.org"];
|
const ALLOWED_FETCH_HOSTS = ["tiles.openfreemap.org", "telegram.org"];
|
||||||
|
|
||||||
/** Documented, deliberate scope adjudication (the theme-audit precedent):
|
/** Documented, deliberate scope adjudication (the theme-audit precedent):
|
||||||
* the SVG namespace URIs (`http://www.w3.org/2000/svg`) are spec
|
* the SVG and XHTML namespace URIs are W3C spec IDENTIFIERS
|
||||||
* IDENTIFIERS, never fetched. Any other http:// host fails prefer-fail. */
|
* (createElementNS / innerHTML parsing), never fetched. Any other http://
|
||||||
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
* host fails prefer-fail. */
|
||||||
|
const SPEC_NAMESPACE_IDENTIFIERS = ["http://www.w3.org/2000/svg", "http://www.w3.org/1999/xhtml"];
|
||||||
|
|
||||||
/** Extract the hostname of an absolute URL string, or null. */
|
/** Extract the hostname of an absolute URL string, or null. */
|
||||||
function hostOf(urlText) {
|
function hostOf(urlText) {
|
||||||
@@ -266,7 +267,7 @@ export function runRuntimeHostGate(opts = {}) {
|
|||||||
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 (urlText === SVG_NAMESPACE) 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;
|
||||||
violations.push(`${rel} — absolute URL to an unapproved host (${where}): ${host}`);
|
violations.push(`${rel} — absolute URL to an unapproved host (${where}): ${host}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -228,11 +228,14 @@ await test("CP2 both live probes degrade to notRun (never throw) without Cloudfl
|
|||||||
const saved = process.env;
|
const saved = process.env;
|
||||||
process.env = strippedEnv;
|
process.env = strippedEnv;
|
||||||
try {
|
try {
|
||||||
for (const probe of ["runKvExpiryProbe", "runAssetHeaderProbe"]) {
|
const kv = await m.runKvExpiryProbe();
|
||||||
const r = await m[probe]();
|
assert.equal(kv.pass, false, "kv-expiry without credentials is pass:false");
|
||||||
assert.equal(r.pass, false, `${probe} without credentials is pass:false`);
|
assert.equal(kv.notRun, true, "kv-expiry without credentials carries notRun:true");
|
||||||
assert.equal(r.notRun, true, `${probe} without credentials carries notRun:true`);
|
// asset-headers' unavailable driver is the ORIGIN (not CF creds) — the
|
||||||
}
|
// empty baseUrl forces the no-origin branch with NO network call.
|
||||||
|
const headers = await m.runAssetHeaderProbe({ baseUrl: "" });
|
||||||
|
assert.equal(headers.pass, false, "asset-headers without an origin is pass:false");
|
||||||
|
assert.equal(headers.notRun, true, "asset-headers without an origin carries notRun:true");
|
||||||
} finally {
|
} finally {
|
||||||
process.env = saved;
|
process.env = saved;
|
||||||
}
|
}
|
||||||
@@ -249,7 +252,7 @@ await test("AU1 the CLI prints per-gate evidence and exits 0 with PRIVACY AUDIT
|
|||||||
for (const gate of ["storage-api", "kv-ttl", "log-vocabulary", "runtime-hosts", "bundle-telemetry"]) {
|
for (const gate of ["storage-api", "kv-ttl", "log-vocabulary", "runtime-hosts", "bundle-telemetry"]) {
|
||||||
assert.match(res.stdout, new RegExp(`^${gate}: PASS —`, "m"), `per-gate evidence line for ${gate}`);
|
assert.match(res.stdout, new RegExp(`^${gate}: PASS —`, "m"), `per-gate evidence line for ${gate}`);
|
||||||
}
|
}
|
||||||
assert.match(res.stdout, /kv-expiry: NOT RUN \(pass --live\)/, "a skipped live probe is never silently green");
|
assert.match(res.stdout, /^kv-expiry: NOT RUN — \(pass --live\)$/m, "a skipped live probe is never silently green");
|
||||||
assert.match(res.stdout, /PRIVACY AUDIT CLEAN/, "the clean summary line");
|
assert.match(res.stdout, /PRIVACY AUDIT CLEAN/, "the clean summary line");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user