Files
GearBox/.planning/phases/24-public-access-infrastructure/24-01-PLAN.md

9.5 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
24-public-access-infrastructure 01 execute 1
src/server/middleware/rateLimit.ts
src/server/index.ts
tests/middleware/rateLimit.test.ts
true
INFR-01
truths artifacts key_links
Public GET endpoints return 429 after exceeding the configured rate limit
Different endpoint tiers have different rate limit thresholds
Existing OAuth rate limiting (5 req/15 min) continues to work unchanged
path provides exports
src/server/middleware/rateLimit.ts createRateLimit factory function
createRateLimit
rateLimit
_resetForTesting
path provides contains
src/server/index.ts Rate limit middleware applied to public GET endpoints createRateLimit
path provides contains
tests/middleware/rateLimit.test.ts Tests for configurable rate limit tiers createRateLimit
from to via pattern
src/server/index.ts src/server/middleware/rateLimit.ts import createRateLimit createRateLimit(\d+,
Refactor the rate limiter into a configurable factory and apply tiered rate limits to all public GET API endpoints.

Purpose: Protect public endpoints from abuse (INFR-01) while allowing normal browsing patterns. The existing single-tier (5 req/15 min) rate limiter is only appropriate for OAuth/auth endpoints. Output: createRateLimit(max, windowMs) factory, tiered limits on public GET routes, extended tests.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/24-public-access-infrastructure/24-CONTEXT.md @.planning/phases/24-public-access-infrastructure/24-RESEARCH.md

@src/server/middleware/rateLimit.ts @src/server/index.ts @tests/middleware/rateLimit.test.ts

Task 1: Refactor rateLimit.ts to factory pattern and extend tests src/server/middleware/rateLimit.ts, tests/middleware/rateLimit.test.ts - src/server/middleware/rateLimit.ts (current single-tier implementation) - tests/middleware/rateLimit.test.ts (existing tests to preserve) - Test: createRateLimit(3, 60000) allows exactly 3 requests then returns 429 - Test: createRateLimit(10, 60000) allows exactly 10 requests then returns 429 - Test: Two different createRateLimit instances with different limits operate independently (share store but different keys) - Test: Original `rateLimit` export still blocks after 5 requests (backward compat) - Test: 429 response includes Retry-After header - Test: Different IPs tracked independently with createRateLimit Refactor `src/server/middleware/rateLimit.ts` per D-07:
1. Keep the existing module-level `store` Map and `cleanup()`, `getClientIp()` helper functions unchanged.
2. Add a new exported factory function:
   ```typescript
   export function createRateLimit(maxAttempts: number, windowMs: number) {
     return async function rateLimitMiddleware(c: Context, next: Next) {
       cleanup();
       const ip = getClientIp(c);
       const key = `${ip}:${c.req.path}`;
       const now = Date.now();
       const entry = store.get(key);
       if (!entry || now >= entry.resetAt) {
         store.set(key, { count: 1, resetAt: now + windowMs });
         return next();
       }
       if (entry.count >= maxAttempts) {
         const retryAfter = Math.ceil((entry.resetAt - now) / 1000);
         c.header("Retry-After", String(retryAfter));
         return c.json({ error: "Too many requests. Try again later." }, 429);
       }
       entry.count++;
       return next();
     };
   }
   ```
3. Rewrite the original `rateLimit` export to delegate to the factory:
   ```typescript
   export const rateLimit = createRateLimit(5, 15 * 60 * 1000);
   ```
   Note: Change from `async function` to `const` assignment. The `rateLimit` export must remain a middleware function (not a wrapper that creates one on each call).
4. Keep `_resetForTesting()` unchanged — it clears the shared store, which is correct for all tiers.

In `tests/middleware/rateLimit.test.ts`:
5. Add import for `createRateLimit` alongside existing imports.
6. Add a new `describe("createRateLimit factory")` block with tests for:
   - Custom limit (3 req) blocks on 4th request
   - Custom limit (10 req) allows 10 then blocks
   - Different IPs tracked independently
   - Retry-After header present on 429
7. Keep all existing tests in the `"rateLimit middleware"` describe block unchanged — they validate backward compatibility.
cd /home/jean-luc-makiola/Development/projects/GearBox && bun test tests/middleware/rateLimit.test.ts - rateLimit.ts contains `export function createRateLimit(maxAttempts: number, windowMs: number)` - rateLimit.ts contains `export const rateLimit = createRateLimit(5,` (backward-compatible export) - rateLimit.ts contains `export function _resetForTesting()` - rateLimit.test.ts contains `describe("createRateLimit factory"` with at least 4 test cases - All existing tests in "rateLimit middleware" describe block still pass - `bun test tests/middleware/rateLimit.test.ts` exits 0 createRateLimit factory exported, backward-compatible rateLimit still works, all tests pass including new factory tests Task 2: Apply tiered rate limits to public GET endpoints in index.ts src/server/index.ts - src/server/index.ts (current route registration and auth skip logic, lines 100-167) - src/server/middleware/rateLimit.ts (after Task 1 — confirm createRateLimit export exists) Apply rate limit tiers to public GET endpoints per D-07 and D-08 (same limits for auth and anon).
1. Add import at top of `src/server/index.ts`:
   ```typescript
   import { createRateLimit } from "./middleware/rateLimit";
   ```

2. After the `app.use("/api/*", async (c, next) => { c.set("db", prodDb); ... })` block (around line 118) and BEFORE the auth middleware block (line 121), add rate limit middleware:
   ```typescript
   // Rate limiting for public endpoints (per D-07, D-08)
   const browseTier = createRateLimit(120, 60_000);
   const detailTier = createRateLimit(60, 60_000);

   // Browse endpoints — higher limit for list/search
   app.use("/api/global-items", async (c, next) => {
     if (c.req.method === "GET" && !c.req.path.match(/^\/api\/global-items\/\d+$/))
       return browseTier(c, next);
     return next();
   });
   app.use("/api/tags", async (c, next) => {
     if (c.req.method === "GET") return browseTier(c, next);
     return next();
   });

   // Detail endpoints — moderate limit for individual resources
   app.use("/api/global-items/:id", async (c, next) => {
     if (c.req.method === "GET") return detailTier(c, next);
     return next();
   });
   app.use("/api/setups/:id/public", async (c, next) => {
     if (c.req.method === "GET") return detailTier(c, next);
     return next();
   });
   app.use("/api/users/:id/profile", async (c, next) => {
     if (c.req.method === "GET") return detailTier(c, next);
     return next();
   });
   ```

3. Do NOT modify the existing auth skip logic (lines 121-140) — it already correctly skips auth for these GET endpoints per D-01.
4. Do NOT apply rate limits to `/api/auth/*` or OAuth endpoints — those already have the original `rateLimit` (5/15min) applied where needed.
cd /home/jean-luc-makiola/Development/projects/GearBox && bun test tests/middleware/rateLimit.test.ts && bun run lint - index.ts contains `import { createRateLimit } from "./middleware/rateLimit"` - index.ts contains `const browseTier = createRateLimit(120, 60_000)` - index.ts contains `const detailTier = createRateLimit(60, 60_000)` - index.ts contains rate limit middleware for `/api/global-items`, `/api/tags`, `/api/global-items/:id`, `/api/setups/:id/public`, `/api/users/:id/profile` - Rate limit middleware is placed BEFORE the auth middleware block - `bun run lint` exits 0 All public GET endpoints have tiered rate limits applied. Browse endpoints (global-items list, tags) at 120/min, detail endpoints (global-item detail, public setup, profile) at 60/min. - `bun test tests/middleware/rateLimit.test.ts` — all rate limit tests pass - `bun run lint` — no lint errors - `bun test` — full suite passes (no regressions)

<success_criteria>

  • createRateLimit factory is exported and tested with configurable limits
  • Original rateLimit export unchanged in behavior (backward compatible)
  • All 5 public GET endpoint groups have rate limits applied in index.ts
  • Rate limits are applied before auth middleware
  • No new dependencies added </success_criteria>
After completion, create `.planning/phases/24-public-access-infrastructure/24-01-SUMMARY.md`