From f608931779b92ad51299a156c97ef458e19a5825 Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 21 Sep 2026 01:58:06 +0200 Subject: [PATCH] feat(http): best-effort client IP from headers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 8a Task 3. Reads x-forwarded-for (first hop), then x-real-ip, falls back to 'unknown'. Not used for auth or logging — pure rate- limit keying so concurrent clients do not share one bucket. --- src/lib/http/client-ip.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/lib/http/client-ip.ts diff --git a/src/lib/http/client-ip.ts b/src/lib/http/client-ip.ts new file mode 100644 index 0000000..ed51791 --- /dev/null +++ b/src/lib/http/client-ip.ts @@ -0,0 +1,23 @@ +/** + * Best-effort client IP for rate-limit keying. + * + * This is NOT used for authentication, logging, or any decision that needs to + * be accurate. It is a tie-breaker so two clients hammering the same token + * from different networks each get their own budget. The spec deliberately + * does not persist it. + * + * Reads `x-forwarded-for` first (the only header a load balancer or reverse + * proxy should be trusted to set), then `x-real-ip`, and falls back to + * "unknown" so all unrecognised callers share one bucket rather than each + * getting their own unlimited budget. + */ +export function clientIpFromHeaders(headers: Headers): string { + const fwd = headers.get("x-forwarded-for"); + if (fwd) { + const first = fwd.split(",")[0]?.trim(); + if (first) return first; + } + const real = headers.get("x-real-ip"); + if (real) return real.trim(); + return "unknown"; +}