Address review: PGRST205 degrade, per-user rate limit, header hardening

↗ view on GitHub · sloth-ninja · 2026-08-07 · 20288bac

Five should-fixes and six nits from the independent review of #81.

PGRST205 was a real defect. A missing TABLE on Supabase is PostgREST's
PGRST205 (404, answered from its schema cache), never Postgres's 42P01 -
verified empirically against PostgREST 14.16 in a local container (no
credentials involved): missing table -> PGRST205, missing column in a
filter -> 42703, missing column in a write payload -> PGRST204. The
42P01-only degrade would have thrown in production, and would have failed
account deletion / SAR export outright on an unmigrated database. The
code table now lives in a dependency-free leaf module lib/postgrestCodes.ts.

- /clio-matters gets its own limiter keyed on res.locals.userId (IP only
  as the pre-auth fallback), inside the router because an app-level
  limiter runs before auth. The pilot firm NATs one office IP, so the
  shared IP-keyed research bucket would have let a few solicitors take
  /companies and /legislation down for everyone. New vars
  RATE_LIMIT_CLIO_MATTERS_MAX / _WINDOW_MINUTES, documented in
  .env.example and CLAUDE.md. Auth moves to router level so no future
  route can omit it.
- headersFor drops any caller header colliding case-insensitively with a
  fixed one: object spread is case-sensitive and new Headers() then joins
  case-differing duplicates into "Bearer attacker, Bearer real".
- linkWorkspace gains the matter-side pre-check; link candidates are
  ordered by created_at with the caller's own preferred; the workspace
  side of the conflict gets its own 409 copy.
- Nits: cross-user cache isolation test, etag input guard, created_by
  predicate on the unlink delete, uuid guard on getLinkForProject,
  lifecycle fixtures corrected, amountsHidden renamed amountsUnavailable
  (a null price can mean a rate-less entry, so only quantityRedacted may
  drive "Hidden by your Clio permissions" copy).

The first attempt imported the predicate from the feature seam into
userDataCleanup, forming an import cycle through lib/access.ts that broke
an unrelated tombstone test in routes/tabular.test.ts; hence the leaf
module. Lifecycle paths must never import a feature seam.

Verified: tsc clean; full vitest 871 passed / 49 files (baseline 757);
prettier clean on all new and newly-touched files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Repository Sloth-ninja/JessicaOSS
Author sloth-ninja <ezanahaddis@gmail.com>
Authored
Parents 7945ad49
Stats 17 files changed , +742 , -146
Part of Practice Management: Clio-backed Matters and workspace links

Capture this commit into my fork

Download a Markdown prompt that tells Claude how to port this exact commit into your working tree. Run it via claude -p < capture-commit-20288bac.md from inside the repo you want the change in.

⬇ Download capture-commit-20288bac.md