Skip to content

Data-layer hardening: Ably token scoping, input guards, +500 tests#87

Merged
zwrose merged 5 commits into
mainfrom
chunk-0-hardening
May 28, 2026
Merged

Data-layer hardening: Ably token scoping, input guards, +500 tests#87
zwrose merged 5 commits into
mainfrom
chunk-0-hardening

Conversation

@zwrose
Copy link
Copy Markdown
Owner

@zwrose zwrose commented May 28, 2026

Pre-redesign data-layer hardening, extracted from the claude-design-redesign branch (Chunk 0) so it can land on main independently of the visual redesign. Purely additive and behavior-preserving — no UI changes.

Why a separate PR

The redesign is a long-lived branch; this hardening is valuable on its own and unblocks parallel work that benefits from the new test coverage and the security fixes. The redesign branch will absorb this via a routine git merge main.

Security fixes (surfaced by an /audit-debt sweep)

  • 🔴 Critical — Ably realtime token leak. /api/ably/token had no auth check and minted a wildcard-capability token, letting any caller subscribe to any store's shopping-store:<id> channel and read other families' shopping data. Now requires a session and scopes the token capability to the user's owned + accepted-invitation stores (subscribe/presence only; the server still publishes via the API key).
  • ObjectId.isValid guards on meal-plans/[id] (GET/PUT/DELETE), food-items/[id] (PUT/DELETE), and admin approve/toggle-admin — malformed ids now return 400 instead of 500.
  • Mass-assignment allowlists: recipes/[id] PUT no longer spreads the raw body into $set (blocks createdBy/createdAt/_id injection; isGlobal stays client-settable as the recipe-sharing mechanism); user/settings POST writes only themeMode + defaultMealPlanOwner via dot-notation, so a crafted body can't forge sharing-invitation grants.

Test hardening (the untouched data layer)

  • Golden-master for meal-plan-utils overlap/next-date (incl. the skip-advance loop), via vi.setSystemTime.
  • New util coverage: shopping-list-position-utils, recipe-user-data-utils, recipe-utils, pantry-utils; MSW path coverage for the sharing/shopping-list fetch wrappers; auth.ts redirect/session/jwt callbacks.
  • 14 previously-untested sharing/store API routes now have 401/400/ownership/500 coverage.
  • Regression tests for each security fix above.

Full suite: 1306 tests / 125 files green, lint clean, build OK.

Also included

🤖 Generated with Claude Code

zwrose and others added 5 commits May 28, 2026 14:20
Design-bundle reference snapshots under docs/design/ (HTML/JSX/TSX) are
not app source and were breaking `next build` typecheck. Excluding docs/
from tsconfig restores a fully green `npm run check` baseline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Full-repo /audit-debt sweep of the pre-redesign codebase. 58 findings
(1 Critical, 27 Important, 27 Minor, 3 Nit). Disposition recorded:
data-layer + security items fixed in Chunk 0c; the rest filed as GitHub
issues #76-#86 for separate scheduling.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t allowlists (Chunk 0c)

Pre-redesign security hardening surfaced by the audit sweep, behavior-
preserving for legitimate clients:

- Critical: /api/ably/token had no auth check and minted a wildcard-
  capability token, letting any caller subscribe to any store's
  shopping-store:<id> channel (cross-user data leak). Now requires a
  session and scopes capability to the user's owned + accepted-invitation
  stores (subscribe+presence only; server still publishes via the API key).
- ObjectId.isValid guards on meal-plans/[id] (GET/PUT/DELETE),
  food-items/[id] (PUT/DELETE), and admin approve/toggle-admin body userId
  — malformed ids now return 400 instead of 500.
- Mass-assignment allowlists: recipes/[id] PUT no longer spreads the raw
  body into $set (blocks createdBy/createdAt/_id injection; isGlobal stays
  client-settable as the recipe-sharing mechanism); user/settings POST
  writes only themeMode + defaultMealPlanOwner via dot-notation, so a
  crafted body can't forge sharing-invitation grants.

Adds regression tests for each, including a new ably/token test file.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Backfill behavior-preserving tests for the untouched data layer, per the
audit sweep — golden-master for pure transforms, MSW path coverage for
fetch wrappers, and 401/400/ownership/500 coverage for the sharing/store
API routes.

Utils (lib):
- meal-plan-utils: golden-master for findNextAvailableMealPlanStartDate
  skip-advance loop + checkMealPlanOverlap edge cases (vi.setSystemTime)
- shopping-list-position-utils: new (pure position math + MSW)
- shopping-list-utils: cover all remaining fetch wrappers (MSW)
- recipe-sharing-utils, meal-plan-sharing-utils, recipe-user-data-utils: new MSW
- recipe-utils, pantry-utils: new (array/paginated normalization)
- user-utils: getCurrentUserAdminStatus; date-utils: day/next-day arms;
  auth.ts: redirect/session/jwt callbacks

API routes (14 sharing/store routes, previously untested):
- stores/[id], stores/[id]/invite, stores/[id]/invitations/[userId],
  stores/invitations, shopping-lists/[storeId]/positions
- user/meal-plan-sharing/* and user/recipe-sharing/* (invitations,
  invitations/[userId], invite, owners, shared-users)

Full suite: 1304 tests across 125 files, green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
review-code (base main) returned READY FOR PR (0 Critical/Important).
Applied the informational Minor/Nit fixes that harden Chunk 0's own work:

- meal-plans/[id] invalid-id guard returns API_ERRORS.BAD_REQUEST (was a
  "not found" message with a 400 status) — consistent with food-items/[id]
- auth.test: add jwt happy-path + missing-user coverage (was catch-path only)
- meal-plan-sharing invitations/[userId]: assert the $pull filter/shape on
  owner-remove and self-leave (was bare toHaveBeenCalled)
- recipe-sharing tests: drop no-op collection name-branching

npm run check green: 1306 tests, lint clean, build OK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 28, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
weekly-eats Ready Ready Preview, Comment May 28, 2026 6:24pm

@zwrose zwrose merged commit b86f652 into main May 28, 2026
3 checks passed
@zwrose zwrose deleted the chunk-0-hardening branch May 28, 2026 18:26
zwrose added a commit that referenced this pull request May 29, 2026
* docs: design spec for server-side approval enforcement (#83)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: revise approval-enforcement spec per /review-plan findings (#83)

Correct two false premises (existing useApprovalStatus hook; auth.ts does
not handle the update trigger), make the auth.ts fix a required change with
DB re-read (no self-approval), expand the test plan (middleware harness,
~18-route fixture migration), and pin the middleware gate ordering.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: update approval-enforcement spec test plan for #87 merge (#83)

Route-test migration scope grew from ~18 to 34 user-data route test files
(~270 session-mock literals) after #87; point the jwt update-trigger test at
the now-existing src/lib/__tests__/auth.test.ts harness; note ably/token test
joins the migration set. Design sections unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: fold pass-2 review refinements into approval-enforcement spec (#83)

Exempt /api/avatar in middleware (correct the 'no avatar' claim; verified
AuthenticatedLayout->Header->CachedAvatar->/api/avatar), specify the
useApprovalStatus hook test's fake-timer requirement + mock harness ref,
pin the shared approvedSession() fixture shape/location, and soften the
coverage claim in Rollout. Verdict: PLAN READY.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: implementation plan for server-side approval enforcement (#83)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: fold plan-review findings into approval-enforcement plan + spec (#83)

Admin bypass: gate and helper now exempt admins (isAdmin/isApproved are
independent; unapproved admins must keep admin access) + tests. Clarify the
user/settings migration (email-only mocks -> approvedSession({email}); it is a
real auth tightening). Fix the worked-example request construction
(routes.GET(makeReq)), enumerate explicitly-excluded routes, justify the hook
fetch stub under MSW, and note the x-middleware-next assertion check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: handle test-less routes in approval-enforcement plan (#83)

Pass-3 review: 3 of 37 migrated routes (recipes/tags, recipes/[id]/user-data,
shopping-lists/[storeId]) have no colocated test — Task 6 now instructs
scaffolding a minimal route.test.ts (unapproved->403 + approved passthrough)
rather than stalling. Verdict: PLAN READY.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: add requireApprovedSession helper (#83)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: add approved/unapproved session fixtures (#83)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: refresh isApproved on jwt update trigger (#83)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: enforce approval in middleware (#83)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: cover useApprovalStatus approve/demote transitions (#83)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: enforce approval on meal-plans routes (#83)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: enforce approval on recipes routes (#83)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: enforce approval on food-items and pantry routes (#83)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: enforce approval on stores routes (#83)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: enforce approval on shopping-lists routes (#83)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: enforce approval on meal-plan-sharing routes (#83)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: enforce approval on recipe-sharing routes (#83)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: enforce approval on ably-token and settings routes (#83)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: narrow settings email + drop unused test imports (#83)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Auto-fix round 1: 1 finding (Test)

Add steady-state test cases to use-approval-status.test.tsx asserting
that when the polled status matches the current session value, neither
update() nor router.push() is called.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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