Commit Graph
2 Commits
  • Move the whole pipeline into the browser, drop the server
    Every stage now runs client-side: pdf.js renders, a per-pixel diff finds
    the hidden ink, tesseract.js transcribes it on request, and pdf-lib
    rebuilds the flattened export. The PDF goes from the file picker into
    memory and back out as a download, and is never sent anywhere.
    
    That makes the privacy claim structural rather than a promise. The
    server is left with one route — a health check — so there is no code
    path that can receive a document, nothing is persisted, and no account
    exists. The OCR model and WASM are staged out of node_modules into
    public/ at build time so the page never reaches a CDN either. Verified
    in the browser: a full scan with OCR contacts exactly one origin, its
    own, and POST /api/upload returns 404 in the built image.
    
    Contrast stretching turned out to be the wrong method entirely. Text
    painted in pure #FFFFFF rasterizes to bytes identical to blank paper, so
    there is no difference in the image to amplify — and a page with black
    body text already spans 0-255, so the old server-side normalize() was a
    no-op. It could never have found the case it advertised.
    
    So each page is rendered twice, on white and on black. White ink vanishes
    into the first and blazes in the second; inverting the black-backed
    render and taking the darker pixel of each pair puts every mark on the
    page in view whatever colour it was drawn in. Confirmed against a PDF
    carrying a genuine white-on-white payload: invisible in "as delivered",
    flagged in vermilion in "revealed", and transcribed verbatim by OCR.
    Pages that paint their own opaque background defeat the technique, so
    that case is detected and reported as unreliable instead of showing a
    page-sized false positive.
    
    OCR is opt-in, as asked: it costs an ~8MB model download and runs at a
    few seconds a page, so nothing is fetched until the box is ticked.
    
    Consequences:
    - Removed auth, Prisma, Postgres, the upload/render/pixel/ocr/export
      routes, the storage layer and the signed-in dashboard.
    - Runtime dependencies drop from 13 to 6, all browser-capable; sharp,
      @napi-rs/canvas, next-auth, pg and the Prisma stack are all gone.
    - compose is a single stateless service with no volumes and no config,
      the entrypoint and its migration step are gone, and the image drops
      from 868MB to 455MB.
    - The landing page no longer describes the contrast method it used to
      claim, since that method provably does not work.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
  • Make the container actually deployable, publish to GHCR
    The Docker setup built but never came up. Three separate blockers, each
    found by running the full compose stack against a real Postgres:
    
    - The runner stage hand-copied prisma/@prisma/dotenv, which misses the
      CLI's transitive deps (valibot) and crash-looped the entrypoint. A
      dedicated migrator stage now installs the CLI at the exact lockfile
      version with its real dependency tree.
    - Docker sets HOSTNAME to the container id and Next's standalone server
      binds to it verbatim, so the app was unreachable on localhost inside
      the container and the healthcheck could never pass. Pinned to 0.0.0.0.
    - openssl was missing from the build stages but present at runtime, so
      Prisma detected a different platform and tried to download a matching
      query engine at boot, as an unprivileged user, into a root-owned
      directory. Aligned openssl across every stage that touches Prisma.
    
    Verified from a clean volume: migrations apply, both containers report
    healthy, /api/health returns 200, and the app serves from the image.
    
    Also:
    - Publish multi-arch images to GHCR on push to main and on v* tags, with
      an SBOM and a signed provenance attestation.
    - Default compose to the published image; add a build override for
      working on the app itself. Host port and image are configurable.
    - Drop two unused dependencies: canvas (19MB of native node-canvas; the
      pipeline uses @napi-rs/canvas) and multer (routes use formData()).
    - Add a root .gitignore. There wasn't one, so a real .env was one
      `git add .` away from being committed. Untrack .DS_Store.
    - Rewrite the README around what the tool does and how to run it.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>