Skip to content

fix(critical): programScope enrollment attachment skipped on double invocation - #263

Merged
jgupta05072003-code merged 2 commits into
mainfrom
debug/db-based-diagnostics
Sep 25, 2026
Merged

jgupta05072003-code merged 2 commits into
mainfrom
debug/db-based-diagnostics

Conversation

@jgupta05072003-code

Copy link
Copy Markdown
Collaborator

Summary

Root cause found for the Lakshya Aran "not enrolled" incident (and likely a meaningful share of today's other "not enrolled" reports), via the DB-based debug instrumentation added over the last several PRs (now removed).

programScope() is mounted both globally (bootstrap/middleware.ts:155, before any route-specific auth middleware runs) and again per-route after optionalAuth/protect. The global pass runs with no req.user yet, so it could resolve req.programContext but never had a user to attach req.programEnrollment for. The old if (req.programContext) return next() early return then made the per-route pass — the only one that ever has a real user — skip re-running entirely, so req.programEnrollment silently never got attached for any signed-in user on any route using this middleware. enforceProgramMembership() then fell through to the fail-open check, found the user had some enrollment (just not attached to the request), and blocked with "You are not enrolled in this program" — for demonstrably, correctly enrolled users.

Fix: batch-context resolution is still cached once resolved (the batch itself doesn't change mid-request), but enrollment attachment is now independent of that cache and always attempted whenever a signed-in, non-admin user doesn't have req.programEnrollment set yet — regardless of which pass is running.

This means the cross-cohort fix from earlier today (#246/#247) was less effective than believed — the authorization logic itself was correct, but this pre-existing structural bug prevented it from reliably attaching real enrollments at all.

Also in this PR:

  • programScope-double-invocation.test.ts — reproduces the exact global-then-per-route sequence and asserts enrollment ends up attached.
  • Removed all debug/journalctl-workaround scaffolding added while diagnosing this (tail-csfaq-logs.yml, read-debug-temp.yml+.ts).
  • Added a one-off cleanup script+workflow to drop the throwaway debug_temp_2026_09_25 collection they wrote to.

Test plan

  • tsc --noEmit clean
  • Full backend suite: 747 tests passing (1 new, reproducing the exact bug)

🤖 Generated with Claude Code

jgupta05072003-code and others added 2 commits September 25, 2026 17:38
…le invocation

Root cause of the Lakshya Aran "not enrolled" incident, found via the
DB-based debug instrumentation added over the last several commits
(now removed).

programScope() is mounted BOTH globally (bootstrap/middleware.ts, line
155, before any route-specific auth middleware runs) AND again
per-route after optionalAuth/protect. The global pass runs with no
req.user yet, so it could resolve req.programContext but never had a
user to attach req.programEnrollment for. The old "if (req.programContext)
return next()" early return then made the per-route pass — the only
one that ever has a real user — skip re-running entirely, so
req.programEnrollment silently never got attached for ANY signed-in
user on ANY route using this middleware (which is every route this
whole incident has been about). enforceProgramMembership then fell
through to the fail-open check, found the user had SOME enrollment
(just not attached to the request), and blocked with "You are not
enrolled in this program" — for demonstrably, correctly enrolled
users.

Fix: batch-context resolution is still cached once resolved (the
batch itself doesn't change mid-request), but enrollment attachment is
now independent of that cache and always attempted whenever a
signed-in, non-admin user doesn't have req.programEnrollment set yet —
regardless of which pass (global or per-route) is running.

This means the entire cross-cohort fix from earlier today (#246, #247)
was less effective than believed: the authorization logic was correct,
but this pre-existing structural bug prevented it from ever correctly
attaching a real enrollment on the first (global) pass, and skipped
retrying on the second (per-route, authenticated) pass. It's likely
this bug — not missing enrollment data — explains a meaningful share of
the "not enrolled" reports today, on top of the genuinely-missing-data
cases already fixed.

Added programScope-double-invocation.test.ts, which reproduces the
exact global-then-per-route sequence and asserts req.programEnrollment
ends up attached. Removed all debug/journalctl-workaround scaffolding
(tail-csfaq-logs.yml, read-debug-temp.yml + .ts) added while diagnosing
this; added a one-off cleanup script+workflow to drop the throwaway
debug_temp_2026_09_25 collection they wrote to.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CodeQL flagged 3 findings on this PR:
- bootstrap/middleware.ts:155 — the global programScope() mount does a
  DB lookup (Batch.findById) on every request carrying a batchId, on
  every route in the app, with zero rate limiting. Added a generous
  (600/min) cap ahead of it — high enough that normal shared-IP/
  multi-tab browsing never trips it, low enough to bound a scripted
  flood.
- search.routes.ts /trending and POST / — both had programScope()
  running BEFORE their rate limiter, so a flood could still hit the DB
  via programScope's own lookup regardless of the limiter downstream.
  Reordered so the limiter runs first on both routes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@jgupta05072003-code
jgupta05072003-code merged commit 527de2d into main Sep 25, 2026
5 checks passed
@jgupta05072003-code
jgupta05072003-code deleted the debug/db-based-diagnostics branch September 25, 2026 12:22
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