--- name: pdf-lib-determinism-moddate-leak description: "Diagnose pdf-lib output that isn't byte-identical across fetches: PDFDocument.load restamps /ModDate; fake-clock determinism test; inflate object streams to diff." --- # pdf-lib byte-identical output: the /ModDate clock leak ## Symptom A "reconstruct-on-read, byte-identical forever" PDF endpoint passes its back-to-back refetch test but returns **different bytes when fetches cross a wall-clock second**. Same-second fetches match; a fetch 1s later differs (often a different stream `/Length`, e.g. object stream 411→420 bytes). ## Root cause `PDFDocument.load()` has its own `updateMetadata` option (**defaults true**) and stamps `/Info /ModDate` from the wall clock **at load time**, not just at save. `doc.save({updateMetadata:false})` on its own is not enough (and in pdf-lib 1.17 `updateMetadata` isn't even in the `SaveOptions` type — only in `PDFOptions` on `load`); passing it to `save` also fails typecheck. ## Fix ```ts const doc = await PDFDocument.load(templateBytes, { updateMetadata: false }); // ... fill, stamp print layer ... return doc.save(); // save never stamps when load didn't ``` Output becomes a pure function of (template bytes, values). ## Determinism test law A determinism test MUST straddle a time boundary. Back-to-back fetches/fills are structurally blind to per-second stamping. - Unit: fake the clock (`@sinonjs/fake-timers` or `vi.useFakeTimers`) → fill → jump +2 months → re-fill → `expect(Buffer.from(second)).toEqual(first)`. No sleeps, no flake window. Faking `Date` outside a test framework requires patching `globalThis.Date` BEFORE the module import that uses it. - Prove it red first against the old code (expect the byte/length drift). ## Byte-diff forensics (when lengths differ) 1. Dump the differing region raw: `Buffer.subarray(idx-60, idx+60)` rendered latin1 with non-printables stripped — locate the exact field (`D:YYYYMMDDHHmmss` = clock leak). 2. If the diff is inside a compressed object stream, inflate and compare streams: ```ts import { inflateSync } from "node:zlib"; // find /Type /ObjStm ... stream chunks, inflateSync each, string-compare ``` 3. pdf-lib escapes text-show operands — inflate + unescape `\\(\\[()\\\\]\\)` before substring-asserting printed values. ## Live proof recipe Seed a submission, then fetch both retrieval doors at t, t+1.5s, t+3s; `sha256sum` all three → one hash. Same-second comparisons prove nothing. And kill any long-running dev service holding the app port first — a live :3000-style stack can hang or false-fail an e2e gate.