docs(24): create phase plan
This commit is contained in:
216
.planning/phases/24-public-access-infrastructure/24-01-PLAN.md
Normal file
216
.planning/phases/24-public-access-infrastructure/24-01-PLAN.md
Normal file
@@ -0,0 +1,216 @@
|
||||
---
|
||||
phase: 24-public-access-infrastructure
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- src/server/middleware/rateLimit.ts
|
||||
- src/server/index.ts
|
||||
- tests/middleware/rateLimit.test.ts
|
||||
autonomous: true
|
||||
requirements: [INFR-01]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "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"
|
||||
artifacts:
|
||||
- path: "src/server/middleware/rateLimit.ts"
|
||||
provides: "createRateLimit factory function"
|
||||
exports: ["createRateLimit", "rateLimit", "_resetForTesting"]
|
||||
- path: "src/server/index.ts"
|
||||
provides: "Rate limit middleware applied to public GET endpoints"
|
||||
contains: "createRateLimit"
|
||||
- path: "tests/middleware/rateLimit.test.ts"
|
||||
provides: "Tests for configurable rate limit tiers"
|
||||
contains: "createRateLimit"
|
||||
key_links:
|
||||
- from: "src/server/index.ts"
|
||||
to: "src/server/middleware/rateLimit.ts"
|
||||
via: "import createRateLimit"
|
||||
pattern: "createRateLimit\\(\\d+,"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<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
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Refactor rateLimit.ts to factory pattern and extend tests</name>
|
||||
<files>src/server/middleware/rateLimit.ts, tests/middleware/rateLimit.test.ts</files>
|
||||
<read_first>
|
||||
- src/server/middleware/rateLimit.ts (current single-tier implementation)
|
||||
- tests/middleware/rateLimit.test.ts (existing tests to preserve)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- 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
|
||||
</behavior>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/jean-luc-makiola/Development/projects/GearBox && bun test tests/middleware/rateLimit.test.ts</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 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
|
||||
</acceptance_criteria>
|
||||
<done>createRateLimit factory exported, backward-compatible rateLimit still works, all tests pass including new factory tests</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Apply tiered rate limits to public GET endpoints in index.ts</name>
|
||||
<files>src/server/index.ts</files>
|
||||
<read_first>
|
||||
- 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)
|
||||
</read_first>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/jean-luc-makiola/Development/projects/GearBox && bun test tests/middleware/rateLimit.test.ts && bun run lint</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 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
|
||||
</acceptance_criteria>
|
||||
<done>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.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `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)
|
||||
</verification>
|
||||
|
||||
<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>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/24-public-access-infrastructure/24-01-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user