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>
This commit is contained in:
1 parent
5ca71853eb
commit
c6a564ec5d
44 files changed
+1142
-4654
No files matched your search
+8
-56
@@ -3,12 +3,6 @@
|
||||
FROM node:20-bookworm-slim AS base
|
||||
WORKDIR /app
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
# openssl must be present in every stage that touches Prisma: the CLI picks its
|
||||
# query-engine build from the detected libssl version, so a build stage without
|
||||
# it would bake in an engine the runtime stage then tries to re-download.
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends openssl ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
FROM base AS deps
|
||||
COPY package.json package-lock.json* ./
|
||||
@@ -17,75 +11,33 @@ RUN npm ci
|
||||
FROM base AS builder
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
# Generate the Prisma client (output: src/generated/prisma) before building.
|
||||
RUN npx prisma generate
|
||||
# Dummy build-time env only satisfies module-load guards during `next build`;
|
||||
# real values are injected at runtime via compose. No secrets baked in.
|
||||
ENV DATABASE_URL="postgresql://build:build@localhost:5432/build"
|
||||
ENV NEXTAUTH_SECRET="build"
|
||||
ENV NEXTAUTH_URL="http://localhost:3000"
|
||||
ENV GOOGLE_CLIENT_ID="build"
|
||||
ENV GOOGLE_CLIENT_SECRET="build"
|
||||
# `prebuild` stages the pdf.js worker and the Tesseract core/WASM out of
|
||||
# node_modules into public/, so the app serves them from its own origin
|
||||
# instead of a CDN. Nothing here needs env vars: the app has no database, no
|
||||
# auth and no server-side processing, so there is nothing to configure.
|
||||
RUN npm run build
|
||||
|
||||
# The Prisma CLI is only needed to apply migrations at container start. Rather
|
||||
# than drag the whole 600MB dev tree into the runtime image (or hand-pick
|
||||
# packages and miss transitive deps like valibot), install just the CLI at the
|
||||
# exact lockfile version into an isolated prefix, and copy that tree in.
|
||||
FROM base AS migrator
|
||||
COPY package-lock.json ./
|
||||
RUN PRISMA_VERSION="$(node -p "require('./package-lock.json').packages['node_modules/prisma'].version")" \
|
||||
&& rm -f package-lock.json \
|
||||
&& npm init -y > /dev/null \
|
||||
&& npm install --no-audit --no-fund --omit=dev \
|
||||
"prisma@${PRISMA_VERSION}" dotenv
|
||||
# NOTE: the Studio dependency stack (effect, electric-sql, lodash, ...) looks
|
||||
# prunable but is not — the CLI requires @prisma/studio-core/data/bff eagerly
|
||||
# at startup, even for `migrate deploy`. Removing it breaks the entrypoint.
|
||||
|
||||
FROM node:20-bookworm-slim AS runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV PORT=3000
|
||||
ENV STORAGE_DIR=/app/storage
|
||||
# Docker sets HOSTNAME to the container id, and Next's standalone server binds
|
||||
# to it verbatim — which makes the app unreachable on localhost inside the
|
||||
# container (breaking the healthcheck). Pin it to all interfaces.
|
||||
ENV HOSTNAME=0.0.0.0
|
||||
|
||||
# openssl must match the build stages so Prisma resolves the same engine build.
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends openssl \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& groupadd --system --gid 1001 nodejs \
|
||||
RUN groupadd --system --gid 1001 nodejs \
|
||||
&& useradd --system --uid 1001 --gid nodejs nextjs
|
||||
|
||||
# Next standalone server + assets
|
||||
# Next standalone server + assets. public/ carries the OCR model and WASM, so
|
||||
# it is the bulk of the image.
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/.next/standalone ./
|
||||
COPY --from=builder /app/.next/static ./.next/static
|
||||
|
||||
# Local Tesseract traineddata (no runtime CDN fetch)
|
||||
COPY --from=builder /app/tessdata ./tessdata
|
||||
|
||||
# Prisma schema + migrations + CLI so the entrypoint can run `migrate deploy`.
|
||||
# The CLI tree is merged into the standalone server's node_modules so that
|
||||
# prisma.config.ts can resolve "prisma/config" and "dotenv/config" from /app.
|
||||
COPY --from=builder /app/prisma ./prisma
|
||||
COPY --from=builder /app/prisma.config.ts ./prisma.config.ts
|
||||
COPY --from=migrator /app/node_modules ./node_modules
|
||||
|
||||
COPY --from=builder /app/docker-entrypoint.sh ./docker-entrypoint.sh
|
||||
RUN chmod +x ./docker-entrypoint.sh \
|
||||
&& mkdir -p /app/storage \
|
||||
&& chown -R nextjs:nodejs /app/storage /app/node_modules/@prisma
|
||||
|
||||
# Persist uploads + results outside the image layers
|
||||
VOLUME ["/app/storage"]
|
||||
|
||||
USER nextjs
|
||||
EXPOSE 3000
|
||||
|
||||
ENTRYPOINT ["./docker-entrypoint.sh"]
|
||||
# No entrypoint script: there are no migrations to run and no volume to set up.
|
||||
CMD ["node", "server.js"]
|
||||
Reference in new issue
Block a user