chore: sync in-progress work across marketing, admin, API and tests
Snapshot of uncommitted work that had accumulated in the tree alongside the Turnstile changes: - marketing pages, SEO helpers (lib/seo.ts, lib/marketing/) and structured data - admin billing actions and a per-user portfolio view, plus an admin error boundary - rate limiting (lib/rate-limit.ts) applied across the /api/v1 surface - CSP and proxy adjustments, accounting/webhook lib updates - Playwright config and an e2e/unit test suite - next bumped to ^16.3.4 with the lockfile regenerated - generated AGENTS.md / CLAUDE.md Authored by other sessions working in this tree; committed here so the Turnstile work could be pushed without leaving the tree dirty. Typecheck passes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8f90347659
commit
1d02598786
+65
-11
@@ -39,18 +39,72 @@ function isPrivateIPv4(ip: string): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
/** True for IPv6 loopback, unspecified, ULA, link-local, multicast, or mapped-v4. */
|
||||
/**
|
||||
* Expand an IPv6 literal to its eight numeric hextets, or null if unparseable.
|
||||
* Handles "::" compression and a trailing dotted-quad (::ffff:1.2.3.4).
|
||||
*/
|
||||
function ipv6Hextets(input: string): number[] | null {
|
||||
let s = input.toLowerCase().split("%")[0].replace(/^\[|\]$/g, "")
|
||||
|
||||
// Fold a trailing dotted-quad into two hextets so one code path handles both
|
||||
// spellings of an IPv4-mapped address.
|
||||
const dotted = s.match(/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/)
|
||||
if (dotted && dotted.index !== undefined) {
|
||||
const parts = dotted[1].split(".").map((n) => parseInt(n, 10))
|
||||
if (parts.some((n) => Number.isNaN(n) || n < 0 || n > 255)) return null
|
||||
s =
|
||||
s.slice(0, dotted.index) +
|
||||
(((parts[0] << 8) | parts[1]) >>> 0).toString(16) +
|
||||
":" +
|
||||
(((parts[2] << 8) | parts[3]) >>> 0).toString(16)
|
||||
}
|
||||
|
||||
const halves = s.split("::")
|
||||
if (halves.length > 2) return null
|
||||
const split = (chunk: string) => (chunk ? chunk.split(":").filter(Boolean) : [])
|
||||
const head = split(halves[0])
|
||||
const tail = halves.length === 2 ? split(halves[1]) : []
|
||||
const groups =
|
||||
halves.length === 2
|
||||
? [...head, ...Array(Math.max(0, 8 - head.length - tail.length)).fill("0"), ...tail]
|
||||
: head
|
||||
if (groups.length !== 8) return null
|
||||
|
||||
const out: number[] = []
|
||||
for (const g of groups) {
|
||||
if (!/^[0-9a-f]{1,4}$/.test(g)) return null
|
||||
out.push(parseInt(g, 16))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* True for IPv6 loopback, unspecified, ULA, link-local, multicast, or any
|
||||
* address embedding a private IPv4 address.
|
||||
*
|
||||
* The embedded-IPv4 check works on the NUMERIC hextets, not on the text. Node's
|
||||
* URL parser rewrites `::ffff:169.254.169.254` to `::ffff:a9fe:a9fe`, so a
|
||||
* previous version that only matched the dotted-quad spelling let the cloud
|
||||
* metadata endpoint — and every private range — straight through.
|
||||
*/
|
||||
function isPrivateIPv6(ip: string): boolean {
|
||||
const addr = ip.toLowerCase().split("%")[0] // strip zone id
|
||||
if (addr === "::1" || addr === "::") return true
|
||||
// IPv4-mapped / -compatible (e.g. ::ffff:169.254.169.254) — check the v4 part.
|
||||
const mapped = addr.match(/(?:^::ffff:|^::)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/)
|
||||
if (mapped) return isPrivateIPv4(mapped[1])
|
||||
const head = addr.replace(/^\[|\]$/g, "")
|
||||
if (head.startsWith("fe8") || head.startsWith("fe9") || head.startsWith("fea") || head.startsWith("feb"))
|
||||
return true // fe80::/10 link-local
|
||||
if (head.startsWith("fc") || head.startsWith("fd")) return true // fc00::/7 unique-local
|
||||
if (head.startsWith("ff")) return true // ff00::/8 multicast
|
||||
const h = ipv6Hextets(ip)
|
||||
if (!h) return true // unparseable → treat as unsafe
|
||||
|
||||
// ::/96 (covers :: and ::1) and ::ffff:0:0/96 both carry an IPv4 address in
|
||||
// the low 32 bits. Decode it and reuse the IPv4 rules.
|
||||
const embedsIPv4 =
|
||||
h[0] === 0 && h[1] === 0 && h[2] === 0 && h[3] === 0 && h[4] === 0 &&
|
||||
(h[5] === 0 || h[5] === 0xffff)
|
||||
if (embedsIPv4) {
|
||||
const v4 = [h[6] >> 8, h[6] & 0xff, h[7] >> 8, h[7] & 0xff].join(".")
|
||||
return isPrivateIPv4(v4)
|
||||
}
|
||||
|
||||
const first = h[0]
|
||||
if ((first & 0xffc0) === 0xfe80) return true // fe80::/10 link-local
|
||||
if ((first & 0xfe00) === 0xfc00) return true // fc00::/7 unique-local
|
||||
if ((first & 0xff00) === 0xff00) return true // ff00::/8 multicast
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user