| 1 | /** |
| 2 | * The grep guard. |
| 3 | * |
| 4 | * `docs/TELEMETRY.md` publishes "Batches are IP-stripped at ingest. No IP is |
| 5 | * stored, logged, or joined to `install_id`." The only thing that makes that |
| 6 | * sentence true is that `src/` never asks for the address. Behavioural tests |
| 7 | * cannot prove a negative here — an IP read that only fires on some code path, |
| 8 | * or that goes to a log rather than to the dataset, passes every functional |
| 9 | * test in the suite. |
| 10 | * |
| 11 | * So this file reads the shipped source as text and fails if the names appear |
| 12 | * at all. It is deliberately blunt: a later edit that adds one "just for |
| 13 | * debugging" cannot land quietly, and the failure names the promise it breaks. |
| 14 | * |
| 15 | * It scans `src/` and `wrangler.jsonc`, and never itself: this file and the |
| 16 | * "Verifying no IP is stored" section of `README.md` are the only places in the |
| 17 | * directory where the forbidden names are written down, and neither one ships. |
| 18 | */ |
| 19 | |
| 20 | import { readFileSync, readdirSync } from "node:fs"; |
| 21 | import { fileURLToPath } from "node:url"; |
| 22 | |
| 23 | import { describe, expect, it } from "vitest"; |
| 24 | |
| 25 | const ROOT = fileURLToPath(new URL("..", import.meta.url)); |
| 26 | |
| 27 | const SHIPPED: Array<[string, string]> = [ |
| 28 | ...readdirSync(`${ROOT}src`) |
| 29 | .filter((name) => name.endsWith(".ts")) |
| 30 | .map( |
| 31 | (name) => |
| 32 | [`src/${name}`, readFileSync(`${ROOT}src/${name}`, "utf8")] as [ |
| 33 | string, |
| 34 | string, |
| 35 | ], |
| 36 | ), |
| 37 | ["wrangler.jsonc", readFileSync(`${ROOT}wrangler.jsonc`, "utf8")], |
| 38 | ]; |
| 39 | |
| 40 | const HANDLER = readFileSync(`${ROOT}src/index.ts`, "utf8"); |
| 41 | |
| 42 | /** Names that would put a client address, or a geo derived from one, in scope. */ |
| 43 | const FORBIDDEN: Array<[string, RegExp]> = [ |
| 44 | ["the connecting-IP header", /cf-connecting-ip/i], |
| 45 | ["the forwarded-for header", /x-forwarded-for/i], |
| 46 | ["the real-IP header", /x-real-ip/i], |
| 47 | ["the true-client-IP header", /true-client-ip/i], |
| 48 | ["the forwarded header", /["']forwarded["']/i], |
| 49 | ["request.cf geo — country", /cf\.country/i], |
| 50 | ["request.cf geo — colo", /cf\.colo/i], |
| 51 | ["request.cf geo — city", /cf\.city/i], |
| 52 | ["request.cf geo — region", /cf\.region/i], |
| 53 | ["request.cf geo — asn", /cf\.asn/i], |
| 54 | ["request.cf geo — coordinates", /cf\.(latitude|longitude)/i], |
| 55 | ["request.cf geo — postal code", /cf\.postalcode/i], |
| 56 | ["request.cf geo — timezone", /cf\.timezone/i], |
| 57 | ["the request cf property", /\brequest\.cf\b/], |
| 58 | ["the cf property, however spelled", /\.cf\s*[.[]/], |
| 59 | ]; |
| 60 | |
| 61 | describe("no client IP, structurally", () => { |
| 62 | it.each(FORBIDDEN)("never references %s", (_label, pattern) => { |
| 63 | for (const [name, source] of SHIPPED) { |
| 64 | expect( |
| 65 | pattern.test(source), |
| 66 | `${name} references ${pattern} — docs/TELEMETRY.md promises no IP is read, stored, logged, or joined to install_id`, |
| 67 | ).toBe(false); |
| 68 | } |
| 69 | }); |
| 70 | |
| 71 | it("reads exactly two headers, ever", () => { |
| 72 | const asked = new Set<string>(); |
| 73 | for (const [, source] of SHIPPED) { |
| 74 | for (const match of source.matchAll( |
| 75 | /headers\s*\.\s*(get|has)\s*\(\s*["'`]([^"'`]+)["'`]/g, |
| 76 | )) { |
| 77 | asked.add(match[2].toLowerCase()); |
| 78 | } |
| 79 | } |
| 80 | expect([...asked].sort()).toEqual(["content-length", "content-type"]); |
| 81 | }); |
| 82 | |
| 83 | it("never iterates the request headers", () => { |
| 84 | for (const [name, source] of SHIPPED) { |
| 85 | expect( |
| 86 | /headers\s*\.\s*(entries|keys|values|forEach)/.test(source), |
| 87 | `${name} enumerates headers`, |
| 88 | ).toBe(false); |
| 89 | expect( |
| 90 | /Object\.fromEntries\s*\(\s*[a-zA-Z.]*headers/.test(source), |
| 91 | `${name} snapshots headers`, |
| 92 | ).toBe(false); |
| 93 | } |
| 94 | }); |
| 95 | |
| 96 | it("logs nothing at all", () => { |
| 97 | for (const [name, source] of SHIPPED) { |
| 98 | expect(/\bconsole\s*\./.test(source), `${name} logs`).toBe(false); |
| 99 | } |
| 100 | }); |
| 101 | |
| 102 | it("turns invocation logs off in the Worker config", () => { |
| 103 | const config = readFileSync(`${ROOT}wrangler.jsonc`, "utf8"); |
| 104 | expect(config).toMatch(/"invocation_logs"\s*:\s*false/); |
| 105 | }); |
| 106 | |
| 107 | it("never constructs a Response with a body", () => { |
| 108 | const constructions = [...HANDLER.matchAll(/new Response\(([^,)]*)/g)]; |
| 109 | expect(constructions.length).toBeGreaterThan(0); |
| 110 | for (const construction of constructions) { |
| 111 | expect(construction[1].trim()).toBe("null"); |
| 112 | } |
| 113 | }); |
| 114 | |
| 115 | it("keeps the request out of the storage path", () => { |
| 116 | // `datapoint.ts` builds every Analytics Engine row. If it can see a |
| 117 | // `Request`, it can see an address; it must only ever see a validated |
| 118 | // batch. |
| 119 | const datapoint = readFileSync(`${ROOT}src/datapoint.ts`, "utf8"); |
| 120 | expect(/\bRequest\b/.test(datapoint)).toBe(false); |
| 121 | expect(/\brequest\b/.test(datapoint)).toBe(false); |
| 122 | }); |
| 123 | |
| 124 | it("declares no binding that could hold per-request state", () => { |
| 125 | const config = readFileSync(`${ROOT}wrangler.jsonc`, "utf8"); |
| 126 | for (const binding of [ |
| 127 | "kv_namespaces", |
| 128 | "d1_databases", |
| 129 | "r2_buckets", |
| 130 | "durable_objects", |
| 131 | "queues", |
| 132 | "hyperdrive", |
| 133 | "vectorize", |
| 134 | ]) { |
| 135 | expect( |
| 136 | new RegExp(`"${binding}"\\s*:`).test(config), |
| 137 | `${binding} is not needed for write-only ingest`, |
| 138 | ).toBe(false); |
| 139 | } |
| 140 | }); |
| 141 | }); |
| 142 |