Replace the per-Vercel-instance in-memory Map rate-limit cache with an
atomic shared store backed by the existing Turso RateLimit table, so limits
hold across all instances/redeploys and cannot be bypassed by distributing
brute-force attempts across instances (audit finding p8-010, MEDIUM).
- checkRateLimit now performs a single atomic round-trip:
INSERT ... ON CONFLICT(identifier) DO UPDATE ... RETURNING count, reset_at
with window-reset semantics (CASE WHEN reset_at < now THEN 1 ELSE count+1).
- The DB is now the primary source of truth (no longer a fire-and-forget
fallback). The per-instance Map is reduced to a short-TTL local cache used
ONLY to fast-fail already-blocked identifiers (cuts DB load during brute-
force storms); it can never let a request bypass the limit.
- ensureRateLimitSchema() creates the table + a UNIQUE identifier index so
ON CONFLICT upserts are well-defined; added RateLimit to db/create.ts.
- resetLoginRateLimits / clearRateLimitStore invalidate the local cache.
- getClientIP now trusts proxy headers in non-development environments
(production + test); local dev stays strict against header spoofing.
- bunfig.toml defines import.meta.env.SSR=true so the server-only env guard
loads under 'bun test'.
- Tests: await clearRateLimitStore in beforeEach (fixes a race where an
un-awaited clear let leftover rows corrupt the next upsert); unique test
identifiers; realistic remote-shared-store perf bounds; new p8-010
distributed-store tests (restart-survival, multi-instance aggregation,
no bypass by alternating instances).
Enforce requireClubMembership on social.getPost, addComment, comments,
like, unlike, challenges.leave, and challenges.submitProgress so private
club content is not readable/actionable by non-members (was IDOR).
Extract the membership helpers (requireClubMembership,
resolveClubIdFromPost, resolveClubIdFromChallenge) into a shared
dependency-free module (nessa-community-authz.ts) so all membership-gated
endpoints use one implementation and the libsql connection surface is
typed uniformly. Each post/challenge endpoint now resolves the owning
clubId first (NOT_FOUND if the resource is missing) then gates on it.
Add regression tests (nessa-community-authz.test.ts) covering: non-member
FORBIDDEN vs member allowed for all 7 endpoints' resolve→require sequences,
NOT_FOUND for missing post/challenge, and a join→allowed→leave→blocked
integration.