Takazudo Modular Docs

Type to search...

to open search from anywhere

/sub-packages/photo-uploader/CLAUDE.md

CLAUDE.md at /sub-packages/photo-uploader/CLAUDE.md

Path: sub-packages/photo-uploader/CLAUDE.md

photo-uploader — agent notes

Vite + React + TypeScript mobile-first photo uploader for the Photo Uploader epic (#1473 under super-epic #1470). Lives alongside sub-packages/zpreorder and follows the same build conventions.

Port

  • Dev: http://localhost:14189 (between zmdpreview:14188 and addac-order:14190).
  • Strict port — if :14189 is in use, Vite fails fast instead of picking another.

Layout

sub-packages/photo-uploader/
  index.html                  # Vite entry — loads /src/index.tsx
  package.json                # scripts: dev / build / test (vitest)
  tsconfig.json
  vite.config.ts              # host:localhost port:14189; /.netlify/functions proxy
  vitest.config.ts            # node env, tests in tests/**
  postcss.config.js
  tailwind.config.js          # re-uses the design-system tokens
  src/
    index.tsx                 # React bootstrap
    app.tsx                   # Routes: /login, / (uploader)
    components/
      login-route.tsx         # Password gate
      upload-route.tsx        # Picker + preview + per-file progress/error/retry
    styles.css                # design-system import + Tailwind
    utils/
      api-client.ts           # login / sign / commit / presigned PUT
      auth-state.ts           # localStorage "remembered" flag only (NEVER stores token)
      orientation.ts          # deriveOrientation / roundAspectRatio / deriveGeometry
      prepare-upload.ts       # HEIC->JPEG (heic2any) + EXIF (exifr) + decode dimensions
      slug.ts                 # deriveSlug(bytes, takenAt, uploadedAt) + SLUG_REGEX
  tests/
    slug.test.ts
    orientation.test.ts

Backend (Cloudflare Worker)

The backend is a single Cloudflare Worker at sub-packages/photo-uploader-worker/. It owns all four photo-uploader API routes and accesses Cloudflare D1 via a native binding. The frontend stays unchanged from the Netlify-Functions era — it still POSTs to relative /.netlify/functions/photo-uploader-{login,sign,commit} paths, which now resolve to thin proxy Functions (netlify/functions/photo-uploader-{login,sign,commit}.ts, see netlify/functions/shared/photo-uploader-proxy.ts) that forward to the Worker. Same-origin is preserved so the SameSite=Strict; HttpOnly session cookie survives the round-trip. The shim Functions exist instead of [[redirects]] rewrites because Netlify silently drops any rewrite whose from starts with /.netlify/ (reserved namespace).

Route (Worker)Purpose
POST /photo-uploader-loginPOST password, timingSafeEqual against PHOTO_UPLOADER_PASSWORD, mint signed cookie (HMAC-SHA256 with PHOTO_UPLOADER_SESSION_SECRET, 24h TTL).
POST /photo-uploader-signValidate cookie, return presigned S3 PUT URL for photos/originals/{YYYY}/{MM}/{slug}.{ext} with a 5-minute expiry (signed via aws4fetch).
POST /photo-uploader-commitValidate cookie, upsert the photo row in D1 via env.DB, fire-and-forget the Netlify build hook.
GET /photos.jsonBearer-authed read endpoint for the build pipeline (pnpm photos:build).

See sub-packages/photo-uploader-worker/CLAUDE.md for the full secrets runbook, D1 binding setup, and deploy procedure.

Auth flow

  1. Client POSTs password → server verifies via passwordMatches (SHA-256 hashed + timingSafeEqual).
  2. Server issues a body.sig token (base64url JSON payload {iat,exp} + HMAC-SHA256 sig), returns it as:
    • Set-Cookie: photo_uploader_session=...; Max-Age=86400; Path=/; HttpOnly; SameSite=Strict; Secure
  3. Subsequent calls send the cookie; verifySessionToken checks signature + expiry.
  4. Client stores ONLY a boolean photo-uploader:remembered flag in localStorage so the UI opens on the upload route next time. The token itself never touches JS.

Upload flow

client file → prepareUpload:
    - HEIC? transcode to JPEG (heic2any), fall back to raw HEIC on failure
    - read EXIF DateTimeOriginal → takenAt
    - decode dimensions, compute aspectRatio / orientation
  → deriveSlug({ bytes, takenAt, uploadedAt })   // YYYYMMDD-HHMMSS-sha256[:8]
  → POST photo-uploader-sign → { putUrl, objectKey, slug }
  → PUT putUrl (raw body, Content-Type must match signer)
  → POST photo-uploader-commit (server logs for now)

Security notes

  • HTTPS required in deploys. The password is sent over the network in plaintext before being hashed server-side — only the Secure attribute on the session cookie protects the subsequent session. Netlify enforces HTTPS on preview and production, which is the intended deploy target. Do NOT host this app on a non-HTTPS origin.
  • No presigned-PUT size cap. The R2 PutObjectCommand signer does not pin ContentLength, so a client with a valid cookie can upload arbitrarily large files. This is acceptable while access is gated behind the shared password; revisit if the access model loosens.

Known gaps

  • No rate limiting. Login and sign endpoints log client IP + UA on every attempt but don’t throttle. Revisit when abuse is observed.
  • Commit is a stub. The authoritative DB rebuild happens via photos:build (peer topic, under epic #1473 Phase 2). The commit endpoint exists so the client has a clear success signal and so we have a single server-side log line per upload. It is not idempotent — retries from the client will produce duplicate log lines (but re-uploads to R2 are harmless because the slug is content-derived, so the key is stable).
  • HEIC fallback uploads raw bytes. If heic2any fails in the browser, the client uploads the HEIC as-is. photos:build handles HEIC on the server side.
  • Per-file progress only during the PUT step. The HEIC transcode + EXIF + decode phases don’t report sub-progress — they show a single “Preparing…” state.

Testing conventions

  • Vitest unit tests only. Pure helpers (slug, orientation, photo-uploader-auth) are covered. Do not add port-binding tests, Playwright, or browser-level integration here — those happen at review/merge time.
  • Tests run in the Node environment (vitest.config.ts); they never load heic2any or exifr (those are browser-only and are exercised manually).
  • New browser logic that’s heavy on user interaction (drag-drop, picker flows) should be verified via /headless-browser or manual QA rather than tests.

Commit scope

Use the [photo-uploader] prefix for changes inside this sub-package and for its three Netlify functions (netlify/functions/photo-uploader-*). Cross-cutting setup (env vars, setup-local.sh, gitignore) goes under [misc].