# pdf-inspector provenance and rebuild

PufferPDF pins Firecrawl's `pdf-inspector` rather than tracking a moving branch.
The distributed browser module was built from upstream commit
`585d36e6a6b4e7a714b88b60a103fe3475bbe968` (core `0.1.7`, WASM package base
`0.1.3`) plus `pufferpdf.patch`. Its runtime version is deliberately distinct:
`0.1.3-pufferpdf.1`.

The patch addresses the Phase 3 fidelity corpus:

- ordered and nested list recovery from retained geometry;
- escaped literal pipes in Markdown tables;
- indentation-preserving, variable-fence code blocks;
- short prose-column detection and left-column-first reading order;
- Arabic glyph joining and RTL baseline normalization;
- separate CJK, Hebrew, and Arabic paragraph runs;
- safe URI hyperlink retention from validated PDF link annotations;
- tolerant repair of recoverable malformed PDF containers.

## Verify the distributed runtime

`provenance.json` lists a SHA-256 digest for the source patch, generated
JavaScript module, WebAssembly binary, TypeScript declaration, and combined
license notice. Compare each distributed file with that digest before using or
redistributing a rebuilt runtime. The normal PufferPDF development checkout
performs the same offline, non-mutating verification with `npm run build`.

## Rebuild from source

The release package deliberately contains no executable build helper. The
following recipe reconstructs the modified source and browser runtime using the
public upstream commit, included patch, and toolchain recorded in
`provenance.json`.

Requirements: Git, Rustup, Cargo, a POSIX shell, and the recorded
`aarch64-apple-darwin` Rust host for byte-for-byte verification. From the
PufferPDF plugin directory, first confirm that no other PufferPDF rebuild is
running and that the fixed build directory does not exist, then run:

```sh
export PUFFERPDF_SOURCE_DIR="/tmp/pufferpdf-pdf-inspector-reproducible"
export PUFFERPDF_PLUGIN_DIR="$(pwd)"
test ! -e "$PUFFERPDF_SOURCE_DIR"
mkdir -m 700 "$PUFFERPDF_SOURCE_DIR"
export PUFFERPDF_SOURCE_DIR="$(cd "$PUFFERPDF_SOURCE_DIR" && pwd -P)"
export CARGO_HOME="$PUFFERPDF_SOURCE_DIR/cargo-home"
export CARGO_TARGET_DIR="$PUFFERPDF_SOURCE_DIR/cargo-target"
export RUSTFLAGS="--remap-path-prefix=$PUFFERPDF_SOURCE_DIR=/pufferpdf-build"
export SOURCE_DATE_EPOCH=1785973391
export TMPDIR="$PUFFERPDF_SOURCE_DIR/process-tmp"
mkdir -m 700 "$TMPDIR"

git clone --filter=blob:none https://github.com/firecrawl/pdf-inspector.git \
  "$PUFFERPDF_SOURCE_DIR/pdf-inspector"
git -C "$PUFFERPDF_SOURCE_DIR/pdf-inspector" checkout --detach \
  585d36e6a6b4e7a714b88b60a103fe3475bbe968
git -C "$PUFFERPDF_SOURCE_DIR/pdf-inspector" apply --check \
  "$PUFFERPDF_PLUGIN_DIR/vendor-src/pdf-inspector/pufferpdf.patch"
git -C "$PUFFERPDF_SOURCE_DIR/pdf-inspector" apply \
  "$PUFFERPDF_PLUGIN_DIR/vendor-src/pdf-inspector/pufferpdf.patch"

rustup toolchain install 1.95.0 --profile minimal
rustup target add wasm32-unknown-unknown --toolchain 1.95.0
test "$(rustup run 1.95.0 rustc -vV | sed -n 's/^host: //p')" = \
  "aarch64-apple-darwin"
rustup run 1.95.0 cargo install wasm-pack --version 0.15.0 --locked \
  --root "$PUFFERPDF_SOURCE_DIR/tools"

PATH="$PUFFERPDF_SOURCE_DIR/tools/bin:$PATH" \
RUSTUP_TOOLCHAIN=1.95.0 \
wasm-pack build "$PUFFERPDF_SOURCE_DIR/pdf-inspector/wasm" \
  --target web \
  --release \
  --out-dir pkg

cp "$PUFFERPDF_SOURCE_DIR/pdf-inspector/wasm/pkg/pdf_inspector_wasm.js" \
  "$PUFFERPDF_PLUGIN_DIR/assets/vendor/pdf-inspector/pdf_inspector_wasm.js"
cp "$PUFFERPDF_SOURCE_DIR/pdf-inspector/wasm/pkg/pdf_inspector_wasm_bg.wasm" \
  "$PUFFERPDF_PLUGIN_DIR/assets/vendor/pdf-inspector/pdf_inspector_wasm_bg.wasm"
cp "$PUFFERPDF_SOURCE_DIR/pdf-inspector/wasm/pkg/pdf_inspector_wasm.d.ts" \
  "$PUFFERPDF_PLUGIN_DIR/assets/vendor/pdf-inspector/pdf_inspector_wasm.d.ts"
```

Then verify every distributed source/runtime file against the included
provenance manifest:

```sh
node --input-type=module <<'NODE'
import { createHash } from 'node:crypto';
import { readFileSync } from 'node:fs';

const manifest = JSON.parse(
  readFileSync('vendor-src/pdf-inspector/provenance.json', 'utf8')
);
for (const [path, expected] of Object.entries(manifest.files)) {
  const actual = createHash('sha256').update(readFileSync(path)).digest('hex');
  if (actual !== expected) {
    throw new Error(`${path}: expected ${expected}, found ${actual}`);
  }
  console.log(`${actual}  ${path}`);
}
NODE
```

The recipe does not delete its temporary working directory. Remove that exact
directory after reviewing the checkout and generated artifacts.

The fixed build root, canonical source prefix, private Cargo home, and pinned
host keep usernames and random temporary paths out of the binary. Two clean
rebuilds on the recorded host produced identical bytes. A build on another
host can still be reviewed for functional equivalence, but it is not accepted
as byte-for-byte provenance. If a future pinned build differs, review the
artifact diff and update `provenance.json` only as an intentional vendor
revision.

The repository rebuild command also regenerates
`assets/vendor/pdf-inspector/THIRD_PARTY_LICENSES.txt` from the exact locked
`wasm32-unknown-unknown` normal/build graph reported by Cargo. The generator
fails when a compiled dependency has no reviewable license text. The published
`include_dir` 0.7.4 crates omit their repository-level MIT file, so the
generator explicitly maps both packages to the license at their recorded
upstream commit instead of silently reporting an empty notice. The reviewed
source text is retained under `license-overrides/`; its raw checksum and the
75-package target-graph digest are pinned in `provenance.json`. The release
package retains that generated dependency inventory and its full license texts
alongside this recipe.

## License

Upstream and the PufferPDF patch use the MIT license. The distributed `LICENSE`
also includes the Adobe CMap third-party notice carried by the official WASM
package because those resources remain embedded in this build. The locked Rust
dependency inventory and license texts are in `THIRD_PARTY_LICENSES.txt` beside
the runtime.
