Files
zeekayandHanzo Dev 8b3607cd0d geo: read the visitor's location from our own edge
Every view was being recorded as 127.0.0.1 in Munich. The IP and geo came from
@vercel/functions behind `process.env.VERCEL === "1"`, and this app runs on
Kubernetes — so the branch was always false and every call took the
LOCALHOST_GEO_DATA placeholder. Analytics has been recording a fixed German
address for every visitor.

Cloudflare fronts dataroom.hanzo.ai (and every hanzo.ai host), so the real values
already arrive as request headers: cf-connecting-ip, cf-ipcountry, and — when the
zone's visitor-location transform is on — cf-ipcity, cf-region, cf-iplatitude,
cf-iplongitude, cf-ipcontinent. lib/utils/geo.ts now reads those. No SDK, no
platform binding, and each optional field degrades on its own so a missing city
never costs us the country.

`cf-connecting-ip` is the one a client cannot forge — Cloudflare rewrites it at
the edge. x-forwarded-for is only the fallback for a request that did not come
through it, and only its FIRST entry: the rest are proxies, and anything a client
sent is appended rather than prepended.

This also repairs rate limiting. `send-otp:`, `verify-otp:` and `verify-email:`
key their limiter on the caller's address, and off Vercel that address was
undefined — so every caller shared one bucket and the limit was effectively
global.

Verified with npx tsc --noEmit: 100 errors before and after, compared by
file+code+message rather than line, since inserting imports shifts line numbers
and would otherwise report unchanged errors as new.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-09 10:27:33 -07:00

73 lines
2.6 KiB
TypeScript

import { Geo } from "../types";
/**
* Where a request came from, read from OUR edge.
*
* Cloudflare fronts every hanzo.ai host, so the client address and location
* arrive as request headers — no SDK, no platform binding. This used to read
* `x-vercel-ip-*` and call @vercel/functions, which meant that off Vercel — i.e.
* in production, on Kubernetes — every view was recorded as 127.0.0.1 in Munich.
*
* `cf-connecting-ip` and `cf-ipcountry` are always present. The finer fields
* (city, region, coordinates, continent) arrive only when Cloudflare's "Add
* visitor location headers" transform is on for the zone, so each is optional
* and simply absent otherwise — a missing city must never cost us the country.
*/
const first = (v: string | string[] | undefined): string | undefined =>
Array.isArray(v) ? v[0] : v;
type HeaderBag = { [key: string]: string | string[] | undefined };
/** Normalise either shape of header container to a plain lookup. */
function bag(headers: HeaderBag | Headers): (name: string) => string | undefined {
if (typeof (headers as Headers).get === "function") {
return (name) => (headers as Headers).get(name) ?? undefined;
}
const h = headers as HeaderBag;
return (name) => first(h[name]) ?? first(h[name.toLowerCase()]);
}
export function getGeoData(headers: HeaderBag | Headers): Geo {
const get = bag(headers);
return {
city: get("cf-ipcity"),
region: get("cf-region-code") ?? get("cf-region"),
country: get("cf-ipcountry"),
latitude: get("cf-iplatitude"),
longitude: get("cf-iplongitude"),
};
}
/** Continent code (EU, NA, …) — a separate header, and separately optional. */
export function getContinent(headers: HeaderBag | Headers): string | undefined {
return bag(headers)("cf-ipcontinent");
}
/**
* The client's address.
*
* `cf-connecting-ip` is the one Cloudflare sets and a client cannot forge —
* it is rewritten at the edge. `x-forwarded-for` is only a fallback for
* requests that did not come through Cloudflare, and only its FIRST entry: the
* rest are proxies, and anything a client sent is appended, not prepended.
*/
export function getClientIp(headers: HeaderBag | Headers): string | undefined {
const get = bag(headers);
const direct = get("cf-connecting-ip") ?? get("x-real-ip");
if (direct) return direct;
const forwarded = get("x-forwarded-for");
return forwarded ? forwarded.split(",")[0]?.trim() || undefined : undefined;
}
export const LOCALHOST_GEO_DATA = {
continent: "Europe",
city: "Munich",
region: "BY",
country: "DE",
latitude: "48.1371",
longitude: "11.5761",
};
export const LOCALHOST_IP = "127.0.0.1";