Skip to content

Launch-readiness: reframe scope, security hardening, consent docs - #1

Open
dedmonwalkin wants to merge 1 commit into
mainfrom
claude/launch-readiness
Open

Launch-readiness: reframe scope, security hardening, consent docs#1
dedmonwalkin wants to merge 1 commit into
mainfrom
claude/launch-readiness

Conversation

@dedmonwalkin

Copy link
Copy Markdown
Owner

Summary

Four parallel work streams to take this from prototype to something we can honestly put in front of users.

1. Product reframe

  • README now leads with captions + AD Draft Assist, not sign language
  • New LIMITATIONS.md: honest WER numbers, human-review requirement for AD, explicit removal of the sign-language module citing the WFD/WASLI 2018 position against signing avatars for machine-generated content
  • Marketing claims about WCAG/EAA conformance removed pending a proper ACR/VPAT

2. Security hardening

  • Path traversal (C1): JOB_ID_RE = /^job_[0-9a-f-]{36}$/ gate on every job endpoint; resolved upload paths asserted inside UPLOADS_DIR
  • Auth (C2): crypto.timingSafeEqual key compare; dropped ?api_key= query-string fallback
  • XFF spoofing (C3): X-Forwarded-For ignored unless TRUSTED_PROXY=true
  • SSRF (C4): new src/utils/safeFetch.js refuses private/loopback/ULA/multicast/link-local after DNS resolution; streaming response cap; allowHttpLocalhost auto-enabled for local inference
  • Silent fallback (C5): MODEL_FALLBACK_ON_ERROR=false by default; fallback outputs tagged _provider: 'mock-fallback'
  • Magic-byte sniff (H2): first 12 bytes must match declared MIME (ftyp, EBML, RIFF/WAVE, OggS, ID3/MPEG sync); stream cleanup on failure
  • Orphan uploads (H3): sweepOrphanDirs deletes failed-upload directories older than TTL
  • Body cap (H5): 1MB default with 413 + drain-don't-destroy so the response flushes
  • Snapshot retention: default 20, pruned in the same transaction
  • Dockerfile: non-root app user, tini as PID 1
  • Loud warn on empty API_KEYS when AUTH_DISABLED is not set

3. Architecture

  • Config validation with parseNumber/parseBool, fail-fast + cached
  • structuredClone in state serializer to decouple snapshots from live Map mutation
  • Pipeline cancellation check per chunk (JobCancelledError)
  • SSE: progress replay on attach + 15s heartbeat + cleanup on close
  • .unref() on snapshot/cleanup intervals so the process exits cleanly

4. Cookies & consent

  • COOKIES.md — 4 categories (strictly-necessary / functional / analytics opt-in / advertising never)
  • PRIVACY.md — GDPR table, controller-vs-operator distinction, CCPA note
  • SECURITY.md — disclosure via GH Security tab, 3/7/90-day SLAs, operator hardening checklist
  • /cookies and /privacy routes render shared-layout markdown

Test plan

  • npm test passes — 75 tests, 6 suites, 0 failures
  • New tests: cookie/privacy routes, security headers, path-traversal rejection, job-id regex, unsupported file type, magic-byte mismatch, 413 on oversize body
  • Manual smoke on Docker build with non-root user
  • Review LIMITATIONS.md wording before any public launch post
  • Decide SaaS stance on LibreTranslate AGPL before enabling translation in hosted tier

🤖 Generated with Claude Code

Product reframe
- README leads with captions + AD Draft Assist; removed sign-language claim
- New LIMITATIONS.md: honest about mock vs production, WER, AD human review,
  WFD/WASLI 2018 rationale for dropping the sign-language module

Security
- Path-traversal gate: JOB_ID_RE validation before every lookup; resolved
  upload paths asserted inside UPLOADS_DIR
- Timing-safe API key compare; dropped ?api_key= query-string auth
- TRUSTED_PROXY gate for X-Forwarded-For (no spoofed client IPs by default)
- SSRF guard (safeFetch) refuses private/loopback/ULA; streaming response
  cap; allowHttpLocalhost for intentional local inference
- MODEL_FALLBACK_ON_ERROR=false by default; fallback outputs tagged
  _provider: 'mock-fallback' so silent degradation is visible
- Magic-byte sniff on uploads (mp4/webm/wav/ogg/mp3) — declared MIME must
  match bytes; stream cleanup on failure + orphan-dir sweep
- 1MB cap on JSON bodies with 413 response (drain, don't destroy)
- Loud warn on empty API_KEYS when AUTH_DISABLED not set
- Snapshot retention (default 20) to bound postgres growth
- Dockerfile: non-root 'app' user, tini as PID 1

Architecture
- Config validation with parseNumber/parseBool, fail-fast + cached
- structuredClone in state serializer to decouple snapshots from live Map
- Pipeline cancellation check per chunk (JobCancelledError)
- SSE progress replay on attach + 15s heartbeat + cleanup on close
- .unref() on snapshot/cleanup intervals so process exits cleanly

Cookies & consent
- COOKIES.md with 4 categories (strictly-necessary / functional /
  analytics opt-in / advertising never)
- PRIVACY.md: GDPR table, controller vs operator, CCPA note
- SECURITY.md: disclosure via GH Security tab, SLAs, operator hardening
- /cookies and /privacy routes render shared-layout markdown

Tests
- test/jobService.test.js rewritten with dynamic import inside before()
  so AUTH_DISABLED/PORT reach runtimeConfig before it caches
- New tests: cookie/privacy routes, security headers, path-traversal
  rejection, job-id regex, unsupported file type, magic-byte mismatch,
  413 on oversize body

All 75 tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2b835e2bee

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/utils/safeFetch.js
Comment on lines +62 to +63
if (parsed.protocol === 'http:' && !(allowHttpLocalhost && isLocalhostName)) {
throw new SsrfError(`Refusing plain http to non-localhost host: ${host}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Permit loopback IPs for local HTTP model endpoints

createHttpModelProvider and createLibreTranslateProvider set allowHttpLocalhost for 127.0.0.1 and ::1, but assertSafeUrl only treats localhost/localhost.localdomain as eligible for plain HTTP. As a result, configs like MODEL_HTTP_BASE_URL=http://127.0.0.1:11434 (or IPv6 loopback) are rejected with Refusing plain http to non-localhost host, breaking common local/offline deployments that previously worked with loopback IPs.

Useful? React with 👍 / 👎.

Comment thread src/server.js
if (err instanceof PayloadTooLargeError) {
return sendJson(res, 413, { error: 'Request body too large', limit_bytes: err.limit });
}
const status = err.message.includes('Missing required') || err.message.includes('Unsupported file') || err.message.includes('do not match declared type') ? 400 : 500;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Return 400 for short media uploads failing magic-byte checks

parseUpload now throws File too short to validate for allowed MIME uploads under 12 bytes, but the top-level error mapper does not include that message in its client-error list, so these malformed uploads are reported as 500. This misclassifies a user input validation failure as a server fault and makes client handling/retries incorrect.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant