🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@arcjet/ip

Package Overview
Dependencies
Maintainers
2
Versions
70
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@arcjet/ip - npm Package Compare versions

Comparing version
1.7.0-rc.1
to
1.7.0
+41
cloudflare.d.ts
import type { ProxyService } from "./index.js";
/**
* Cloudflare IPv4 ranges.
*
* Source: https://www.cloudflare.com/ips-v4/
*/
export declare const cloudflareIpv4Ranges: ReadonlyArray<string>;
/**
* Cloudflare IPv6 ranges.
*
* Source: https://www.cloudflare.com/ips-v6/
*/
export declare const cloudflareIpv6Ranges: ReadonlyArray<string>;
/**
* Configuration for {@linkcode cloudflare}.
*/
export interface CloudflareOptions {
/**
* IP addresses and CIDR ranges that identify Cloudflare
* (optional; defaults to the ranges bundled with this package).
*
* Override this only if the bundled ranges are out of date for your setup.
*/
ranges?: ReadonlyArray<string> | null | undefined;
}
/**
* Describe Cloudflare as a trusted proxy in front of your application.
*
* Pass the result in the `proxies` array. When a request reaches your platform
* from a Cloudflare IP, Arcjet will read the real client IP from the
* `CF-Connecting-IP` / `CF-Connecting-IPv6` header instead of treating the
* Cloudflare edge address as the client. The header is only trusted when the
* connecting address is within Cloudflare's ranges, so it cannot be spoofed by
* clients connecting directly to your platform.
*
* @param options
* Configuration (optional).
* @returns
* Proxy service descriptor to include in the `proxies` array.
*/
export declare function cloudflare(options?: CloudflareOptions | null | undefined): ProxyService;
// Cloudflare publishes its IP ranges at the following URLs. These change
// infrequently (roughly annually). They are bundled here so users don't have to
// fetch and paste them, and verified against the live lists by
// `scripts/verify-ranges.ts` (run on a schedule in CI).
//
// IPv4: https://www.cloudflare.com/ips-v4/
// IPv6: https://www.cloudflare.com/ips-v6/
//
// last verified: 2026-06-08
/**
* Cloudflare IPv4 ranges.
*
* Source: https://www.cloudflare.com/ips-v4/
*/
const cloudflareIpv4Ranges = [
"173.245.48.0/20",
"103.21.244.0/22",
"103.22.200.0/22",
"103.31.4.0/22",
"141.101.64.0/18",
"108.162.192.0/18",
"190.93.240.0/20",
"188.114.96.0/20",
"197.234.240.0/22",
"198.41.128.0/17",
"162.158.0.0/15",
"104.16.0.0/13",
"104.24.0.0/14",
"172.64.0.0/13",
"131.0.72.0/22",
];
/**
* Cloudflare IPv6 ranges.
*
* Source: https://www.cloudflare.com/ips-v6/
*/
const cloudflareIpv6Ranges = [
"2400:cb00::/32",
"2606:4700::/32",
"2803:f800::/32",
"2405:b500::/32",
"2405:8100::/32",
"2a06:98c0::/29",
"2c0f:f248::/32",
];
/**
* Describe Cloudflare as a trusted proxy in front of your application.
*
* Pass the result in the `proxies` array. When a request reaches your platform
* from a Cloudflare IP, Arcjet will read the real client IP from the
* `CF-Connecting-IP` / `CF-Connecting-IPv6` header instead of treating the
* Cloudflare edge address as the client. The header is only trusted when the
* connecting address is within Cloudflare's ranges, so it cannot be spoofed by
* clients connecting directly to your platform.
*
* @param options
* Configuration (optional).
* @returns
* Proxy service descriptor to include in the `proxies` array.
*/
function cloudflare(options) {
// An empty `ranges` array is treated as "not provided" and falls back to the
// bundled defaults, matching how `isTrustedProxy` treats an empty `proxies`
// list. Trusting an empty list would silently disable Cloudflare detection
// (no edge address would ever match), so we never let it through.
const ranges = options && Array.isArray(options.ranges) && options.ranges.length > 0
? [...options.ranges]
: [...cloudflareIpv4Ranges, ...cloudflareIpv6Ranges];
return {
kind: "service",
name: "cloudflare",
// CIDR range strings are parsed by `findIp` (where `parseProxy` lives) so
// this module stays free of a runtime dependency on `index.ts`.
ranges,
// CF-Connecting-IPv6:
// https://developers.cloudflare.com/fundamentals/reference/http-request-headers/#cf-connecting-ipv6
// CF-Connecting-IP:
// https://developers.cloudflare.com/fundamentals/reference/http-request-headers/#cf-connecting-ip
clientIp: [
{ header: "cf-connecting-ipv6", format: "ip" },
{ header: "cf-connecting-ip", format: "ip" },
],
};
}
export { cloudflare, cloudflareIpv4Ranges, cloudflareIpv6Ranges };
type Ipv4Tuple = [number, number, number, number];
type Ipv6Tuple = [
number,
number,
number,
number,
number,
number,
number,
number
];
declare class Ipv4Cidr {
type: "v4";
partSize: 8;
parts: Readonly<Ipv4Tuple>;
bits: number;
constructor(parts: Ipv4Tuple, bits: number);
contains(ip: Array<number>): boolean;
}
declare class Ipv6Cidr {
type: "v6";
partSize: 16;
parts: Readonly<Ipv6Tuple>;
bits: number;
constructor(parts: Ipv6Tuple, bits: number);
contains(ip: Array<number>): boolean;
}
/**
* Parse CIDR addresses and keep non-CIDR IP addresses.
*
* @param value
* Value to parse.
* @returns
* Parsed {@linkcode Cidr} if range or given `value` if IP.
*/
export declare function parseProxy(value: string): string | Cidr;
/**
* Parse a list of trusted proxies.
*
* CIDR range strings are parsed to {@linkcode Cidr} (so they match by range);
* plain IP strings and {@linkcode ProxyService} objects (such as those created
* by {@linkcode cloudflare}) are passed through unchanged. Use this to
* normalize the `proxies` option before handing it to {@linkcode findIp}.
*
* @param proxies
* Trusted proxies to parse.
* @returns
* Parsed proxies.
*/
export declare function parseProxies(proxies: ReadonlyArray<string | ProxyService>): Array<string | Cidr | ProxyService>;
/**
* Socket-like interface.
*/
interface PartialSocket {
remoteAddress?: string | null | undefined;
}
/**
* Interface that looks like info.
*/
interface PartialInfo {
remoteAddress?: string | null | undefined;
}
interface PartialIdentiy {
sourceIp?: string | null | undefined;
}
/**
* Interface that looks like a request context.
*/
interface PartialRequestContext {
identity?: PartialIdentiy | null | undefined;
}
/**
* Interface with `headers`.
*/
export type HeaderLike = {
/**
* Headers.
*/
headers: Headers | Record<string, string[] | string | undefined>;
};
/**
* Interface that looks like a request,
* of which `headers` is required and several other fields may exist.
*/
export type RequestLike = {
/**
* Some platforms pass `info`.
*/
info?: PartialInfo | null | undefined;
/**
* Some platforms such as Cloudflare and Vercel provide `ip` directly on
* `request`.
*/
ip?: unknown;
/**
* Some platforms pass info in `requestContext`.
*/
requestContext?: PartialRequestContext | null | undefined;
/**
* Some platforms pass a `socket`.
*/
socket?: PartialSocket | null | undefined;
} & HeaderLike;
/**
* Platform name.
*/
export type Platform = "cloudflare" | "firebase" | "fly-io" | "render" | "vercel";
/**
* Format of a client IP header set by a proxy service.
*
* - `"ip"` — a single IP address (e.g. `CF-Connecting-IP`).
* - `"ips"` — an `X-Forwarded-For`-style comma separated list, parsed
* tail-to-head.
*/
export type ClientIpFormat = "ip" | "ips";
/**
* A client IP header set by a proxy service.
*/
export interface ClientIpHeader {
/**
* Header name (lower-case).
*/
header: string;
/**
* How to parse the header value.
*/
format: ClientIpFormat;
}
/**
* A trusted proxy service (such as Cloudflare) sitting in front of the
* application.
*
* Identified by IP range, with the header(s) that carry the real client IP.
* Create one with a helper such as {@linkcode cloudflare} and include it in the
* `proxies` array.
*/
export interface ProxyService {
/**
* Discriminant marking this entry as a proxy service.
*/
kind: "service";
/**
* Name of the service (such as `"cloudflare"`).
*/
name: string;
/**
* IP addresses and CIDR ranges that identify this service.
*/
ranges: ReadonlyArray<string | Cidr>;
/**
* Header(s) this service uses to relay the real client IP, in priority order.
*/
clientIp: ReadonlyArray<ClientIpHeader>;
}
/**
* Configuration.
*/
export interface Options {
/**
* Platform the code is running on;
* used to allow only known more trustworthy headers.
*/
platform?: Platform | null | undefined;
/**
* Trusted proxies.
*
* IP addresses and CIDR ranges are treated as trusted load balancers or
* proxies and skipped when finding the client IP. Proxy services created with
* a helper such as {@linkcode cloudflare} additionally declare which header
* carries the real client IP.
*/
proxies?: ReadonlyArray<string | Cidr | ProxyService> | null | undefined;
}
/**
* Find a client IP address on a request-like object.
*
* @param request
* Request-like object.
* @param [options]
* Configuration (optional).
* @returns
* Found IP address; empty string if not found.
*/
export declare function findIp(request: RequestLike, options?: Options | null | undefined): string;
/**
* One of the CIDR ranges.
*/
export type Cidr = Ipv4Cidr | Ipv6Cidr;
export { cloudflare } from "./cloudflare.js";
export type { CloudflareOptions } from "./cloudflare.js";
/**
* Find an IP address.
*
* @deprecated
* Use the named export `findIp` instead.
*/
export default findIp;
export { cloudflare } from './cloudflare.js';
function parseXForwardedFor(value) {
if (typeof value !== "string") {
return [];
}
const forwardedIps = [];
// As per MDN X-Forwarded-For Headers documentation at
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For
// The `x-forwarded-for` header may return one or more IP addresses as
// "client IP, proxy 1 IP, proxy 2 IP", so we want to split by the comma and
// trim each item.
for (const item of value.split(",")) {
forwardedIps.push(item.trim());
}
return forwardedIps;
}
function isIpv4Cidr(cidr) {
return (typeof cidr === "object" &&
cidr !== null &&
"type" in cidr &&
typeof cidr.type === "string" &&
cidr.type === "v4" &&
"contains" in cidr &&
typeof cidr.contains === "function");
}
function isIpv6Cidr(cidr) {
return (typeof cidr === "object" &&
cidr !== null &&
"type" in cidr &&
typeof cidr.type === "string" &&
cidr.type === "v6" &&
"contains" in cidr &&
typeof cidr.contains === "function");
}
function isProxyService(value) {
return (typeof value === "object" &&
value !== null &&
"kind" in value &&
value.kind === "service");
}
// Resolve a candidate IP found in a (trusted) header or platform field.
//
// Returns `undefined` when the candidate is not a usable global IP, so callers
// can continue to the next candidate exactly as before. When services are
// configured and the candidate is the edge address of one of them, the real
// client IP is read from that service's declared header(s) instead.
//
// The service header carries the same trust level as the candidate it is read
// from. For platform- or socket-attested candidates (`request.ip`,
// `socket.remoteAddress`, the platform-specific header branches) that means the
// service header cannot be spoofed by a client connecting directly. For
// candidates pulled from a generic header walk (e.g. the `x-forwarded-for`
// fallback when no `platform` is set) it carries no more trust than the
// surrounding header walk already does — a direct client can forge both.
function resolveCandidate(candidate, services, proxies, headers) {
if (!isGlobalIp(candidate, proxies)) {
return undefined;
}
if (services.length === 0) {
return candidate;
}
// The candidate already passed `isGlobalIp` above, so it is a routable IP.
// It is therefore the edge address of a service exactly when it falls inside
// that service's ranges, i.e. when treating those ranges as proxies makes it
// no longer "global". This reuses the same matching as trusted proxies.
const service = services.find((service) => !isGlobalIp(candidate, service.ranges));
if (!service) {
return candidate;
}
// The candidate is a known service edge but we can't read its headers, so we
// can't recover the real client IP. Skip it rather than returning the proxy.
if (typeof headers !== "object" || headers === null) {
return undefined;
}
for (const { header, format } of service.clientIp) {
const value = getHeader(headers, header);
// `parseXForwardedFor` trims each item; mirror that for the single-IP
// format so a header value padded with whitespace still parses.
const items = format === "ip"
? [typeof value === "string" ? value.trim() : value]
: parseXForwardedFor(value).reverse();
for (const item of items) {
// The client IP from a service header is the real client, so we don't
// resolve services again (avoiding loops); only filter trusted proxies.
if (isGlobalIp(item, proxies)) {
return item;
}
}
}
// The candidate is a known service edge but none of its headers held a usable
// client IP. Skip it rather than returning the proxy address.
return undefined;
}
function isTrustedProxy(ip, segments, proxies) {
if (Array.isArray(proxies) && proxies.length > 0) {
return proxies.some((proxy) => {
if (typeof proxy === "string") {
return proxy === ip;
}
if (isIpv4Tuple(segments) && isIpv4Cidr(proxy)) {
return proxy.contains(segments);
}
if (isIpv6Tuple(segments) && isIpv6Cidr(proxy)) {
return proxy.contains(segments);
}
return false;
});
}
return false;
}
// Based on CIDR matching implementation in `ipaddr.js`
// Source code:
// https://github.com/whitequark/ipaddr.js/blob/08c2cd41e2cb3400683cbd503f60421bfdf66921/lib/ipaddr.js#L107-L130
//
// Licensed: The MIT License (MIT)
// Copyright (C) 2011-2017 whitequark <whitequark@whitequark.org>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
function cidrContains(cidr, ip) {
let part = 0;
let shift;
let cidrBits = cidr.bits;
while (cidrBits > 0) {
shift = cidr.partSize - cidrBits;
if (shift < 0) {
shift = 0;
}
if (ip[part] >> shift !== cidr.parts[part] >> shift) {
return false;
}
cidrBits -= cidr.partSize;
part += 1;
}
return true;
}
class Ipv4Cidr {
type = "v4";
partSize = 8;
parts;
bits;
constructor(parts, bits) {
this.bits = bits;
this.parts = parts;
Object.freeze(this);
}
contains(ip) {
return cidrContains(this, ip);
}
}
class Ipv6Cidr {
type = "v6";
partSize = 16;
parts;
bits;
constructor(parts, bits) {
this.bits = bits;
this.parts = parts;
Object.freeze(this);
}
contains(ip) {
return cidrContains(this, ip);
}
}
function parseCidr(cidr) {
// Pre-condition: `cidr` has be verified to have at least one `/`
const cidrParts = cidr.split("/");
if (cidrParts.length !== 2) {
throw new Error("invalid CIDR address: must be exactly 2 parts");
}
const parser = new Parser(cidrParts[0]);
const maybeIpv4 = parser.readIpv4Address();
if (isIpv4Tuple(maybeIpv4)) {
const bits = parseInt(cidrParts[1], 10);
if (isNaN(bits) || bits < 0 || bits > 32) {
throw new Error("invalid CIDR address: incorrect amount of bits");
}
return new Ipv4Cidr(maybeIpv4, bits);
}
const maybeIpv6 = parser.readIpv6Address();
if (isIpv6Tuple(maybeIpv6)) {
const bits = parseInt(cidrParts[1], 10);
if (isNaN(bits) || bits < 0 || bits > 128) {
throw new Error("invalid CIDR address: incorrect amount of bits");
}
return new Ipv6Cidr(maybeIpv6, bits);
}
throw new Error("invalid CIDR address: could not parse IP address");
}
function isCidr(address) {
return address.includes("/");
}
/**
* Parse CIDR addresses and keep non-CIDR IP addresses.
*
* @param value
* Value to parse.
* @returns
* Parsed {@linkcode Cidr} if range or given `value` if IP.
*/
function parseProxy(value) {
if (isCidr(value)) {
return parseCidr(value);
}
else {
return value;
}
}
/**
* Parse a list of trusted proxies.
*
* CIDR range strings are parsed to {@linkcode Cidr} (so they match by range);
* plain IP strings and {@linkcode ProxyService} objects (such as those created
* by {@linkcode cloudflare}) are passed through unchanged. Use this to
* normalize the `proxies` option before handing it to {@linkcode findIp}.
*
* @param proxies
* Trusted proxies to parse.
* @returns
* Parsed proxies.
*/
function parseProxies(proxies) {
return proxies.map((proxy) => typeof proxy === "string" ? parseProxy(proxy) : proxy);
}
function isIpv4Tuple(segements) {
if (typeof segements === "undefined") {
return false;
}
return segements.length === 4;
}
function isIpv6Tuple(segements) {
if (typeof segements === "undefined") {
return false;
}
return segements.length === 8;
}
function u16FromBytes(bytes) {
const u8 = new Uint8Array(bytes);
return new Uint16Array(u8.buffer)[0];
}
function u32FromBytes(bytes) {
const u8 = new Uint8Array(bytes);
return new Uint32Array(u8.buffer)[0];
}
// This Parser and "is global" comparisons are a TypeScript implementation of
// similar code in the Rust stdlib with only slight deviations as noted.
//
// We want to mirror Rust's logic as close as possible, because we'll be relying
// on its implementation when we add a Wasm library to determine IPs and only
// falling back to JavaScript in non-Wasm environments.
//
// Parser source:
// https://github.com/rust-lang/rust/blob/07921b50ba6dcb5b2984a1dba039a38d85bffba2/library/core/src/net/parser.rs#L34
// Comparison source:
// https://github.com/rust-lang/rust/blob/87e1447aadaa2899ff6ccabe1fa669eb50fb60a1/library/core/src/net/ip_addr.rs#L749
// https://github.com/rust-lang/rust/blob/87e1447aadaa2899ff6ccabe1fa669eb50fb60a1/library/core/src/net/ip_addr.rs#L1453
//
// Licensed: The MIT License (MIT)
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions: The above copyright
// notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
class Parser {
state;
constructor(input) {
this.state = input;
}
readAtomically(inner) {
const state = this.state;
const result = inner(this);
if (typeof result === "undefined") {
this.state = state;
}
return result;
}
peakChar() {
return this.state[0];
}
readChar() {
const b = this.state[0];
this.state = this.state.slice(1);
return b;
}
readGivenChar(target) {
return this.readAtomically((p) => {
const c = p.readChar();
if (c === target) {
return c;
}
});
}
readSeparator(sep, index, inner) {
return this.readAtomically((p) => {
if (index > 0) {
const c = p.readGivenChar(sep);
if (typeof c === "undefined") {
return;
}
}
return inner(p);
});
}
readNumber(radix, maxDigits, allowZeroPrefix = false) {
return this.readAtomically((p) => {
let result = 0;
let digitCount = 0;
const hasLeadingZero = p.peakChar() === "0";
function nextCharAsDigit() {
return p.readAtomically((p) => {
const c = p.readChar();
if (c) {
const n = parseInt(c, radix);
if (!isNaN(n)) {
return n;
}
}
});
}
for (let digit = nextCharAsDigit(); digit !== undefined; digit = nextCharAsDigit()) {
result = result * radix;
result = result + digit;
digitCount += 1;
if (typeof maxDigits !== "undefined") {
if (digitCount > maxDigits) {
return;
}
}
}
if (digitCount === 0) {
return;
}
else if (!allowZeroPrefix && hasLeadingZero && digitCount > 1) {
return;
}
else {
return result;
}
});
}
readIpv4Address() {
return this.readAtomically((p) => {
const groups = [];
for (let idx = 0; idx < 4; idx++) {
const result = p.readSeparator(".", idx, (p) => {
// Disallow octal number in IP string
// https://tools.ietf.org/html/rfc6943#section-3.1.1
return p.readNumber(10, 3, false);
});
if (result === undefined) {
return;
}
else {
groups.push(result);
}
}
return groups;
});
}
readIpv6Address() {
// Read a chunk of an IPv6 address into `groups`. Returns the number of
// groups read, along with a bool indicating if an embedded trailing IPv4
// address was read. Specifically, read a series of colon-separated IPv6
// groups (0x0000 - 0xFFFF), with an optional trailing embedded IPv4 address
const readGroups = (p, groups) => {
const limit = groups.length;
for (const i of groups.keys()) {
// Try to read a trailing embedded IPv4 address. There must be at least
// two groups left
if (i < limit - 1) {
const ipv4 = p.readSeparator(":", i, (p) => p.readIpv4Address());
if (isIpv4Tuple(ipv4)) {
const [one, two, three, four] = ipv4;
groups[i + 0] = u16FromBytes([one, two]);
groups[i + 1] = u16FromBytes([three, four]);
return [i + 2, true];
}
}
const group = p.readSeparator(":", i, (p) => p.readNumber(16, 4, true));
if (typeof group !== "undefined") {
groups[i] = group;
}
else {
return [i, false];
}
}
return [groups.length, false];
};
return this.readAtomically((p) => {
// Read the front part of the address; either the whole thing, or up
// to the first ::
const head = new Uint16Array(8);
const [headSize, headIpv4] = readGroups(p, head);
if (headSize === 8) {
return head;
}
// IPv4 part is not allowed before `::`
if (headIpv4) {
return;
}
// Read `::` if previous code parsed less than 8 groups.
// `::` indicates one or more groups of 16 bits of zeros.
if (typeof p.readGivenChar(":") === "undefined") {
return;
}
if (typeof p.readGivenChar(":") === "undefined") {
return;
}
// Read the back part of the address. The :: must contain at least one
// set of zeroes, so our max length is 7.
const tail = new Uint16Array(7);
const limit = 8 - (headSize + 1);
const [tailSize, _] = readGroups(p, tail.subarray(0, limit));
head.set(tail.slice(0, tailSize), 8 - tailSize);
return head;
});
}
readPort() {
return this.readAtomically((p) => {
if (typeof p.readGivenChar(":") !== "undefined") {
return p.readNumber(10, undefined, true);
}
});
}
readScopeId() {
return this.readAtomically((p) => {
if (typeof p.readGivenChar("%") !== "undefined") {
return p.readNumber(10, undefined, true);
}
});
}
}
const IPV4_BROADCAST = u32FromBytes([255, 255, 255, 255]);
function isGlobalIpv4(s, proxies) {
if (typeof s !== "string") {
return false;
}
const parser = new Parser(s);
const octets = parser.readIpv4Address();
if (!isIpv4Tuple(octets)) {
return false;
}
if (isTrustedProxy(s, octets, proxies)) {
return false;
}
// Rust doesn't check the remaining state when parsing an IPv4. However, we
// want to ensure we have exactly an IP (with optionally a port), so we parse
// it and then check remaining parser state.
parser.readPort();
if (parser.state.length !== 0) {
return false;
}
// "This network"
if (octets[0] === 0) {
return false;
}
// Private IPv4 address ranges
if (octets[0] === 10) {
return false;
}
if (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) {
return false;
}
if (octets[0] === 192 && octets[1] === 168) {
return false;
}
// Loopback address
if (octets[0] === 127) {
return false;
}
// Shared range
if (octets[0] === 100 && (octets[1] & 0b1100_0000) === 0b0100_0000) {
return false;
}
// Link-local range
if (octets[0] === 169 && octets[1] === 254) {
return false;
}
// addresses reserved for future protocols (`192.0.0.0/24`)
if (octets[0] === 192 && octets[1] === 0 && octets[2] === 0) {
return false;
}
// Documentation ranges
if (octets[0] === 192 && octets[1] === 0 && octets[2] === 2) {
return false;
}
if (octets[0] === 198 && octets[1] === 51 && octets[2] === 100) {
return false;
}
if (octets[0] === 203 && octets[1] === 0 && octets[2] === 113) {
return false;
}
// Benchmarking range
if (octets[0] === 198 && (octets[1] & 0xfe) === 18) {
return false;
}
const isBroadcast = u32FromBytes(octets) === IPV4_BROADCAST;
// Reserved range
if ((octets[0] & 240) === 240 && !isBroadcast) {
return false;
}
// Broadcast address
if (isBroadcast) {
return false;
}
for (const octet of octets) {
if (octet < 0 || octet > 255) {
return false;
}
}
return true;
}
function isGlobalIpv6(s, proxies) {
if (typeof s !== "string") {
return false;
}
const parser = new Parser(s);
const segments = parser.readIpv6Address();
if (!isIpv6Tuple(segments)) {
return false;
}
if (isTrustedProxy(s, segments, proxies)) {
return false;
}
// Rust doesn't check the remaining state when parsing an IPv6. However, we
// want to ensure we have exactly an IP (with optionally a scope id), so we
// parse it and then check remaining parser state.
// TODO: We don't support an IPv6 address with a port because that seems to
// require wrapping the address and scope in `[]`, e.g. `[:ffff%1]:8080`
parser.readScopeId();
if (parser.state.length !== 0) {
return false;
}
// Unspecified address
if (segments[0] === 0 &&
segments[1] === 0 &&
segments[2] === 0 &&
segments[3] === 0 &&
segments[4] === 0 &&
segments[5] === 0 &&
segments[6] === 0 &&
segments[7] === 0) {
return false;
}
// Loopback address
if (segments[0] === 0 &&
segments[1] === 0 &&
segments[2] === 0 &&
segments[3] === 0 &&
segments[4] === 0 &&
segments[5] === 0 &&
segments[6] === 0 &&
segments[7] === 0x1) {
return false;
}
// IPv4-mapped Address (`::ffff:0:0/96`)
if (segments[0] === 0 &&
segments[1] === 0 &&
segments[2] === 0 &&
segments[3] === 0 &&
segments[4] === 0 &&
segments[5] === 0xffff) {
return false;
}
// IPv4-IPv6 Translat. (`64:ff9b:1::/48`)
if (segments[0] === 0x64 && segments[1] === 0xff9b && segments[2] === 1) {
return false;
}
// Discard-Only Address Block (`100::/64`)
if (segments[0] === 0x100 &&
segments[1] === 0 &&
segments[2] === 0 &&
segments[3] === 0) {
return false;
}
// IETF Protocol Assignments (`2001::/23`)
if (segments[0] === 0x2001 && segments[1] < 0x200) {
// Port Control Protocol Anycast (`2001:1::1`)
if (segments[0] === 0x2001 &&
segments[1] === 1 &&
segments[2] === 0 &&
segments[3] === 0 &&
segments[4] === 0 &&
segments[5] === 0 &&
segments[6] === 0 &&
segments[7] === 1) {
return true;
}
// Traversal Using Relays around NAT Anycast (`2001:1::2`)
if (segments[0] === 0x2001 &&
segments[1] === 1 &&
segments[2] === 0 &&
segments[3] === 0 &&
segments[4] === 0 &&
segments[5] === 0 &&
segments[6] === 0 &&
segments[7] === 2) {
return true;
}
// AMT (`2001:3::/32`)
if (segments[0] === 0x2001 && segments[1] === 3) {
return true;
}
// AS112-v6 (`2001:4:112::/48`)
if (segments[0] === 0x2001 && segments[1] === 4 && segments[2] === 0x112) {
return true;
}
// ORCHIDv2 (`2001:20::/28`)
if (segments[0] === 0x2001 && segments[1] >= 0x20 && segments[1] <= 0x2f) {
return true;
}
// Benchmarking range (and others)
return false;
}
// Documentation range
if (segments[0] === 0x2001 && segments[1] === 0xdb8) {
return false;
}
// Unique local range
if ((segments[0] & 0xfe00) === 0xfc00) {
return false;
}
// Unicast link local range
if ((segments[0] & 0xffc0) === 0xfe80) {
return false;
}
return true;
}
function isGlobalIp(s, proxies) {
if (isGlobalIpv4(s, proxies)) {
return true;
}
if (isGlobalIpv6(s, proxies)) {
return true;
}
return false;
}
function isHeaders(val) {
return typeof val.get === "function";
}
function getHeader(headers, headerKey) {
if (isHeaders(headers)) {
return headers.get(headerKey);
}
else {
const headerValue = headers[headerKey];
if (Array.isArray(headerValue)) {
return headerValue.join(",");
}
else {
return headerValue;
}
}
}
// Heavily based on https://github.com/pbojinov/request-ip
//
// Licensed: The MIT License (MIT) Copyright (c) 2022 Petar Bojinov -
// petarbojinov+github@gmail.com
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions: The above copyright
// notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
/**
* Find a client IP address on a request-like object.
*
* @param request
* Request-like object.
* @param [options]
* Configuration (optional).
* @returns
* Found IP address; empty string if not found.
*/
function findIp(request, options) {
const { platform, proxies: rawProxies } = options || {};
const proxies = [];
const services = [];
if (Array.isArray(rawProxies)) {
for (const cidrOrIp of rawProxies) {
if (typeof cidrOrIp === "string") {
proxies.push(parseProxy(cidrOrIp));
}
if (isIpv4Cidr(cidrOrIp) || isIpv6Cidr(cidrOrIp)) {
proxies.push(cidrOrIp);
}
if (isProxyService(cidrOrIp)) {
// Parse any CIDR range strings to `Cidr` so they match by range rather
// than exact string equality.
const ranges = [];
for (const range of cidrOrIp.ranges) {
ranges.push(typeof range === "string" ? parseProxy(range) : range);
}
services.push({ ...cidrOrIp, ranges });
}
}
}
// Prefer anything available via the platform over headers since headers can
// be set by users. Only if we don't have an IP available in `request` do we
// search the `headers`.
const ipFromRequest = resolveCandidate(request.ip, services, proxies, request.headers);
if (ipFromRequest) {
return ipFromRequest;
}
const socketRemoteAddress = resolveCandidate(request.socket?.remoteAddress, services, proxies, request.headers);
if (socketRemoteAddress) {
return socketRemoteAddress;
}
const infoRemoteAddress = resolveCandidate(request.info?.remoteAddress, services, proxies, request.headers);
if (infoRemoteAddress) {
return infoRemoteAddress;
}
// AWS Api Gateway + Lambda
const requestContextIdentitySourceIp = resolveCandidate(request.requestContext?.identity?.sourceIp, services, proxies, request.headers);
if (requestContextIdentitySourceIp) {
return requestContextIdentitySourceIp;
}
// Validate we have some object for `request.headers`
if (typeof request.headers !== "object" || request.headers === null) {
return "";
}
// Platform-specific headers should only be accepted when we can determine
// that we are running on that platform. For example, the `CF-Connecting-IP`
// header should only be accepted when running on Cloudflare; otherwise, it
// can be spoofed.
if (platform === "cloudflare") {
// CF-Connecting-IPv6: https://developers.cloudflare.com/fundamentals/reference/http-request-headers/#cf-connecting-ipv6
const cfConnectingIpv6 = getHeader(request.headers, "cf-connecting-ipv6");
if (isGlobalIpv6(cfConnectingIpv6, proxies)) {
return cfConnectingIpv6;
}
// CF-Connecting-IP: https://developers.cloudflare.com/fundamentals/reference/http-request-headers/#cf-connecting-ip
const cfConnectingIp = getHeader(request.headers, "cf-connecting-ip");
if (isGlobalIp(cfConnectingIp, proxies)) {
return cfConnectingIp;
}
// If we are using a platform check and don't have a Global IP, we exit
// early with an empty IP since the more generic headers shouldn't be
// trusted over the platform-specific headers.
return "";
}
// Firebase https://github.com/arcjet/arcjet-js/issues/5383
if (platform === "firebase") {
const fahClientIp = resolveCandidate(getHeader(request.headers, "x-fah-client-ip"), services, proxies, request.headers);
if (fahClientIp) {
return fahClientIp;
}
// https://cloud.google.com/functions/docs/reference/headers#x-forwarded-for
// (and https://github.com/arcjet/arcjet-js/issues/5383).
// The last are probably going to be proxies which have to be filtered with
// `proxies`.
const xForwardedFor = getHeader(request.headers, "x-forwarded-for");
const xForwardedForItems = parseXForwardedFor(xForwardedFor);
for (const item of xForwardedForItems.reverse()) {
const resolved = resolveCandidate(item, services, proxies, request.headers);
if (resolved) {
return resolved;
}
}
// If we are using a platform check and don't have a Global IP, we exit
// early with an empty IP since the more generic headers shouldn't be
// trusted over the platform-specific headers.
return "";
}
// Fly.io: https://fly.io/docs/machines/runtime-environment/#fly_app_name
if (platform === "fly-io") {
// Fly-Client-IP: https://fly.io/docs/networking/request-headers/#fly-client-ip
const flyClientIp = resolveCandidate(getHeader(request.headers, "fly-client-ip"), services, proxies, request.headers);
if (flyClientIp) {
return flyClientIp;
}
// If we are using a platform check and don't have a Global IP, we exit
// early with an empty IP since the more generic headers shouldn't be
// trusted over the platform-specific headers.
return "";
}
if (platform === "vercel") {
// https://vercel.com/docs/edge-network/headers/request-headers#x-real-ip
// Also used by `@vercel/functions`, see:
// https://github.com/vercel/vercel/blob/d7536d52c87712b1b3f83e4b0fd535a1fb7e384c/packages/functions/src/headers.ts#L12
const xRealIp = resolveCandidate(getHeader(request.headers, "x-real-ip"), services, proxies, request.headers);
if (xRealIp) {
return xRealIp;
}
// https://vercel.com/docs/edge-network/headers/request-headers#x-vercel-forwarded-for
// By default, it seems this will be 1 address, but they discuss trusted
// proxy forwarding so we try to parse it like normal. See
// https://vercel.com/docs/edge-network/headers/request-headers#custom-x-forwarded-for-ip
const xVercelForwardedFor = getHeader(request.headers, "x-vercel-forwarded-for");
const xVercelForwardedForItems = parseXForwardedFor(xVercelForwardedFor);
// As per MDN X-Forwarded-For Headers documentation at
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For
// We may find more than one IP in the `x-forwarded-for` header. Since the
// first IP will be closest to the user (and the most likely to be spoofed),
// we want to iterate tail-to-head so we reverse the list.
for (const item of xVercelForwardedForItems.reverse()) {
const resolved = resolveCandidate(item, services, proxies, request.headers);
if (resolved) {
return resolved;
}
}
// https://vercel.com/docs/edge-network/headers/request-headers#x-forwarded-for
// By default, it seems this will be 1 address, but they discuss trusted
// proxy forwarding so we try to parse it like normal. See
// https://vercel.com/docs/edge-network/headers/request-headers#custom-x-forwarded-for-ip
const xForwardedFor = getHeader(request.headers, "x-forwarded-for");
const xForwardedForItems = parseXForwardedFor(xForwardedFor);
// As per MDN X-Forwarded-For Headers documentation at
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For
// We may find more than one IP in the `x-forwarded-for` header. Since the
// first IP will be closest to the user (and the most likely to be spoofed),
// we want to iterate tail-to-head so we reverse the list.
for (const item of xForwardedForItems.reverse()) {
const resolved = resolveCandidate(item, services, proxies, request.headers);
if (resolved) {
return resolved;
}
}
// If we are using a platform check and don't have a Global IP, we exit
// early with an empty IP since the more generic headers shouldn't be
// trusted over the platform-specific headers.
return "";
}
if (platform === "render") {
// True-Client-IP: https://community.render.com/t/what-number-of-proxies-sit-in-front-of-an-express-app-deployed-on-render/35981/2
const trueClientIp = resolveCandidate(getHeader(request.headers, "true-client-ip"), services, proxies, request.headers);
if (trueClientIp) {
return trueClientIp;
}
// If we are using a platform check and don't have a Global IP, we exit
// early with an empty IP since the more generic headers shouldn't be
// trusted over the platform-specific headers.
return "";
}
// Load-balancers (AWS ELB) or proxies.
const xForwardedFor = getHeader(request.headers, "x-forwarded-for");
const xForwardedForItems = parseXForwardedFor(xForwardedFor);
// As per MDN X-Forwarded-For Headers documentation at
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For
// We may find more than one IP in the `x-forwarded-for` header. Since the
// first IP will be closest to the user (and the most likely to be spoofed),
// we want to iterate tail-to-head so we reverse the list.
for (const item of xForwardedForItems.reverse()) {
const resolved = resolveCandidate(item, services, proxies, request.headers);
if (resolved) {
return resolved;
}
}
// Standard headers used by Amazon EC2, Heroku, and others.
const xClientIp = resolveCandidate(getHeader(request.headers, "x-client-ip"), services, proxies, request.headers);
if (xClientIp) {
return xClientIp;
}
// DigitalOcean.
// DO-Connecting-IP: https://www.digitalocean.com/community/questions/app-platform-client-ip
const doConnectingIp = resolveCandidate(getHeader(request.headers, "do-connecting-ip"), services, proxies, request.headers);
if (doConnectingIp) {
return doConnectingIp;
}
// Fastly and Firebase hosting header (When forwared to cloud function)
// Fastly-Client-IP
const fastlyClientIp = resolveCandidate(getHeader(request.headers, "fastly-client-ip"), services, proxies, request.headers);
if (fastlyClientIp) {
return fastlyClientIp;
}
// Akamai
// True-Client-IP
const trueClientIp = resolveCandidate(getHeader(request.headers, "true-client-ip"), services, proxies, request.headers);
if (trueClientIp) {
return trueClientIp;
}
// Default nginx proxy/fcgi; alternative to x-forwarded-for, used by some proxies
// X-Real-IP
const xRealIp = resolveCandidate(getHeader(request.headers, "x-real-ip"), services, proxies, request.headers);
if (xRealIp) {
return xRealIp;
}
// Rackspace LB and Riverbed's Stingray?
const xClusterClientIp = resolveCandidate(getHeader(request.headers, "x-cluster-client-ip"), services, proxies, request.headers);
if (xClusterClientIp) {
return xClusterClientIp;
}
const xForwarded = resolveCandidate(getHeader(request.headers, "x-forwarded"), services, proxies, request.headers);
if (xForwarded) {
return xForwarded;
}
const forwardedFor = resolveCandidate(getHeader(request.headers, "forwarded-for"), services, proxies, request.headers);
if (forwardedFor) {
return forwardedFor;
}
const forwarded = resolveCandidate(getHeader(request.headers, "forwarded"), services, proxies, request.headers);
if (forwarded) {
return forwarded;
}
// Google Cloud App Engine
// X-Appengine-User-IP: https://cloud.google.com/appengine/docs/standard/reference/request-headers?tab=node.js
const xAppEngineUserIp = resolveCandidate(getHeader(request.headers, "x-appengine-user-ip"), services, proxies, request.headers);
if (xAppEngineUserIp) {
return xAppEngineUserIp;
}
return "";
}
export { findIp as default, findIp, parseProxies, parseProxy };
+31
-40
{
"name": "@arcjet/ip",
"version": "1.7.0-rc.1",
"version": "1.7.0",
"description": "Arcjet utilities for finding the originating IP of a request",

@@ -8,6 +8,12 @@ "keywords": [

"ip",
"util",
"utility"
"utility",
"util"
],
"license": "Apache-2.0",
"homepage": "https://arcjet.com",
"repository": {
"type": "git",
"url": "git+https://github.com/arcjet/arcjet-js.git",
"directory": "ip"
},
"bugs": {

@@ -17,3 +23,2 @@ "url": "https://github.com/arcjet/arcjet-js/issues",

},
"license": "Apache-2.0",
"author": {

@@ -24,39 +29,21 @@ "name": "Arcjet",

},
"repository": {
"type": "git",
"url": "git+https://github.com/arcjet/arcjet-js.git",
"directory": "ip"
"engines": {
"node": ">=22.21.0 <23 || >=24.5.0"
},
"type": "module",
"main": "./index.js",
"types": "./index.d.ts",
"files": [
"dist"
"cloudflare.d.ts",
"cloudflare.js",
"index.d.ts",
"index.js"
],
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./cloudflare": {
"types": "./dist/cloudflare.d.ts",
"default": "./dist/cloudflare.js"
},
"./cloudflare.js": {
"types": "./dist/cloudflare.d.ts",
"default": "./dist/cloudflare.js"
},
"./package.json": "./package.json"
},
"publishConfig": {
"access": "public",
"tag": "latest"
},
"scripts": {
"build": "tsdown",
"typecheck": "tsgo --noEmit",
"build": "rollup --config rollup.config.js",
"generate": "npm run build && node scripts/verify-ranges.ts --write",
"test-api": "node --test -- test/*.test.ts",
"test-coverage": "node --experimental-test-coverage --test --test-coverage-branches=100 --test-coverage-exclude \"test/**/*.{js,ts}\" --test-coverage-functions=100 --test-coverage-lines=100 -- test/*.test.ts",
"test": "npm run build && npm run test-coverage",
"lint": "eslint .",
"test-api": "node --test -- test/*.test.js",
"test-coverage": "node --experimental-test-coverage --test --test-coverage-branches=100 --test-coverage-exclude \"test/**/*.{js,ts}\" --test-coverage-functions=100 --test-coverage-lines=100 -- test/*.test.js",
"test": "npm run build && npm run lint && npm run test-coverage",
"verify-ranges": "npm run build && node scripts/verify-ranges.ts"

@@ -66,9 +53,13 @@ },

"devDependencies": {
"@arcjet/eslint-config": "1.7.0",
"@arcjet/rollup-config": "1.7.0",
"@rollup/wasm-node": "4.62.2",
"@types/node": "22.19.21",
"tsdown": "0.22.3",
"typescript": "6.0.3"
"eslint": "9.39.4",
"typescript": "5.9.3"
},
"engines": {
"node": ">=22.21.0 <23 || >=24.5.0"
"publishConfig": {
"access": "public",
"tag": "latest"
}
}
import { ProxyService } from "./index.js";
//#region src/cloudflare.d.ts
/**
* Cloudflare IPv4 ranges.
*
* Source: https://www.cloudflare.com/ips-v4/
*/
declare const cloudflareIpv4Ranges: ReadonlyArray<string>;
/**
* Cloudflare IPv6 ranges.
*
* Source: https://www.cloudflare.com/ips-v6/
*/
declare const cloudflareIpv6Ranges: ReadonlyArray<string>;
/**
* Configuration for {@linkcode cloudflare}.
*/
interface CloudflareOptions {
/**
* IP addresses and CIDR ranges that identify Cloudflare
* (optional; defaults to the ranges bundled with this package).
*
* Override this only if the bundled ranges are out of date for your setup.
*/
ranges?: ReadonlyArray<string> | null | undefined;
}
/**
* Describe Cloudflare as a trusted proxy in front of your application.
*
* Pass the result in the `proxies` array. When a request reaches your platform
* from a Cloudflare IP, Arcjet will read the real client IP from the
* `CF-Connecting-IP` / `CF-Connecting-IPv6` header instead of treating the
* Cloudflare edge address as the client. The header is only trusted when the
* connecting address is within Cloudflare's ranges, so it cannot be spoofed by
* clients connecting directly to your platform.
*
* @param options
* Configuration (optional).
* @returns
* Proxy service descriptor to include in the `proxies` array.
*/
declare function cloudflare(options?: CloudflareOptions | null | undefined): ProxyService;
//#endregion
export { CloudflareOptions, cloudflare, cloudflareIpv4Ranges, cloudflareIpv6Ranges };
//#region src/cloudflare.ts
/**
* Cloudflare IPv4 ranges.
*
* Source: https://www.cloudflare.com/ips-v4/
*/
const cloudflareIpv4Ranges = [
"173.245.48.0/20",
"103.21.244.0/22",
"103.22.200.0/22",
"103.31.4.0/22",
"141.101.64.0/18",
"108.162.192.0/18",
"190.93.240.0/20",
"188.114.96.0/20",
"197.234.240.0/22",
"198.41.128.0/17",
"162.158.0.0/15",
"104.16.0.0/13",
"104.24.0.0/14",
"172.64.0.0/13",
"131.0.72.0/22"
];
/**
* Cloudflare IPv6 ranges.
*
* Source: https://www.cloudflare.com/ips-v6/
*/
const cloudflareIpv6Ranges = [
"2400:cb00::/32",
"2606:4700::/32",
"2803:f800::/32",
"2405:b500::/32",
"2405:8100::/32",
"2a06:98c0::/29",
"2c0f:f248::/32"
];
/**
* Describe Cloudflare as a trusted proxy in front of your application.
*
* Pass the result in the `proxies` array. When a request reaches your platform
* from a Cloudflare IP, Arcjet will read the real client IP from the
* `CF-Connecting-IP` / `CF-Connecting-IPv6` header instead of treating the
* Cloudflare edge address as the client. The header is only trusted when the
* connecting address is within Cloudflare's ranges, so it cannot be spoofed by
* clients connecting directly to your platform.
*
* @param options
* Configuration (optional).
* @returns
* Proxy service descriptor to include in the `proxies` array.
*/
function cloudflare(options) {
return {
kind: "service",
name: "cloudflare",
ranges: options && Array.isArray(options.ranges) && options.ranges.length > 0 ? [...options.ranges] : [...cloudflareIpv4Ranges, ...cloudflareIpv6Ranges],
clientIp: [{
header: "cf-connecting-ipv6",
format: "ip"
}, {
header: "cf-connecting-ip",
format: "ip"
}]
};
}
//#endregion
export { cloudflare, cloudflareIpv4Ranges, cloudflareIpv6Ranges };
import { CloudflareOptions, cloudflare } from "./cloudflare.js";
//#region src/index.d.ts
type Ipv4Tuple = [number, number, number, number];
type Ipv6Tuple = [number, number, number, number, number, number, number, number];
declare class Ipv4Cidr {
type: "v4";
partSize: 8;
parts: Readonly<Ipv4Tuple>;
bits: number;
constructor(parts: Ipv4Tuple, bits: number);
contains(ip: Array<number>): boolean;
}
declare class Ipv6Cidr {
type: "v6";
partSize: 16;
parts: Readonly<Ipv6Tuple>;
bits: number;
constructor(parts: Ipv6Tuple, bits: number);
contains(ip: Array<number>): boolean;
}
/**
* Parse CIDR addresses and keep non-CIDR IP addresses.
*
* @param value
* Value to parse.
* @returns
* Parsed {@linkcode Cidr} if range or given `value` if IP.
*/
declare function parseProxy(value: string): string | Cidr;
/**
* Parse a list of trusted proxies.
*
* CIDR range strings are parsed to {@linkcode Cidr} (so they match by range);
* plain IP strings and {@linkcode ProxyService} objects (such as those created
* by {@linkcode cloudflare}) are passed through unchanged. Use this to
* normalize the `proxies` option before handing it to {@linkcode findIp}.
*
* @param proxies
* Trusted proxies to parse.
* @returns
* Parsed proxies.
*/
declare function parseProxies(proxies: ReadonlyArray<string | ProxyService>): Array<string | Cidr | ProxyService>;
/**
* Socket-like interface.
*/
interface PartialSocket {
remoteAddress?: string | null | undefined;
}
/**
* Interface that looks like info.
*/
interface PartialInfo {
remoteAddress?: string | null | undefined;
}
interface PartialIdentiy {
sourceIp?: string | null | undefined;
}
/**
* Interface that looks like a request context.
*/
interface PartialRequestContext {
identity?: PartialIdentiy | null | undefined;
}
/**
* Interface with `headers`.
*/
type HeaderLike = {
/**
* Headers.
*/
headers: Headers | Record<string, string[] | string | undefined>;
};
/**
* Interface that looks like a request,
* of which `headers` is required and several other fields may exist.
*/
type RequestLike = {
/**
* Some platforms pass `info`.
*/
info?: PartialInfo | null | undefined;
/**
* Some platforms such as Cloudflare and Vercel provide `ip` directly on
* `request`.
*/
ip?: unknown;
/**
* Some platforms pass info in `requestContext`.
*/
requestContext?: PartialRequestContext | null | undefined;
/**
* Some platforms pass a `socket`.
*/
socket?: PartialSocket | null | undefined;
} & HeaderLike;
/**
* Platform name.
*/
type Platform = "cloudflare" | "firebase" | "fly-io" | "render" | "vercel";
/**
* Format of a client IP header set by a proxy service.
*
* - `"ip"` — a single IP address (e.g. `CF-Connecting-IP`).
* - `"ips"` — an `X-Forwarded-For`-style comma separated list, parsed
* tail-to-head.
*/
type ClientIpFormat = "ip" | "ips";
/**
* A client IP header set by a proxy service.
*/
interface ClientIpHeader {
/**
* Header name (lower-case).
*/
header: string;
/**
* How to parse the header value.
*/
format: ClientIpFormat;
}
/**
* A trusted proxy service (such as Cloudflare) sitting in front of the
* application.
*
* Identified by IP range, with the header(s) that carry the real client IP.
* Create one with a helper such as {@linkcode cloudflare} and include it in the
* `proxies` array.
*/
interface ProxyService {
/**
* Discriminant marking this entry as a proxy service.
*/
kind: "service";
/**
* Name of the service (such as `"cloudflare"`).
*/
name: string;
/**
* IP addresses and CIDR ranges that identify this service.
*/
ranges: ReadonlyArray<string | Cidr>;
/**
* Header(s) this service uses to relay the real client IP, in priority order.
*/
clientIp: ReadonlyArray<ClientIpHeader>;
}
/**
* Configuration.
*/
interface Options {
/**
* Platform the code is running on;
* used to allow only known more trustworthy headers.
*/
platform?: Platform | null | undefined;
/**
* Trusted proxies.
*
* IP addresses and CIDR ranges are treated as trusted load balancers or
* proxies and skipped when finding the client IP. Proxy services created with
* a helper such as {@linkcode cloudflare} additionally declare which header
* carries the real client IP.
*/
proxies?: ReadonlyArray<string | Cidr | ProxyService> | null | undefined;
}
/**
* Find a client IP address on a request-like object.
*
* @param request
* Request-like object.
* @param [options]
* Configuration (optional).
* @returns
* Found IP address; empty string if not found.
*/
declare function findIp(request: RequestLike, options?: Options | null | undefined): string;
/**
* One of the CIDR ranges.
*/
type Cidr = Ipv4Cidr | Ipv6Cidr;
//#endregion
export { Cidr, ClientIpFormat, ClientIpHeader, type CloudflareOptions, HeaderLike, Options, Platform, ProxyService, RequestLike, cloudflare, findIp as default, findIp, parseProxies, parseProxy };
import { cloudflare } from "./cloudflare.js";
//#region src/index.ts
function parseXForwardedFor(value) {
if (typeof value !== "string") return [];
const forwardedIps = [];
for (const item of value.split(",")) forwardedIps.push(item.trim());
return forwardedIps;
}
function isIpv4Cidr(cidr) {
return typeof cidr === "object" && cidr !== null && "type" in cidr && typeof cidr.type === "string" && cidr.type === "v4" && "contains" in cidr && typeof cidr.contains === "function";
}
function isIpv6Cidr(cidr) {
return typeof cidr === "object" && cidr !== null && "type" in cidr && typeof cidr.type === "string" && cidr.type === "v6" && "contains" in cidr && typeof cidr.contains === "function";
}
function isProxyService(value) {
return typeof value === "object" && value !== null && "kind" in value && value.kind === "service";
}
function resolveCandidate(candidate, services, proxies, headers) {
if (!isGlobalIp(candidate, proxies)) return;
if (services.length === 0) return candidate;
const service = services.find((service) => !isGlobalIp(candidate, service.ranges));
if (!service) return candidate;
if (typeof headers !== "object" || headers === null) return;
for (const { header, format } of service.clientIp) {
const value = getHeader(headers, header);
const items = format === "ip" ? [typeof value === "string" ? value.trim() : value] : parseXForwardedFor(value).reverse();
for (const item of items) if (isGlobalIp(item, proxies)) return item;
}
}
function isTrustedProxy(ip, segments, proxies) {
if (Array.isArray(proxies) && proxies.length > 0) return proxies.some((proxy) => {
if (typeof proxy === "string") return proxy === ip;
if (isIpv4Tuple(segments) && isIpv4Cidr(proxy)) return proxy.contains(segments);
if (isIpv6Tuple(segments) && isIpv6Cidr(proxy)) return proxy.contains(segments);
return false;
});
return false;
}
function cidrContains(cidr, ip) {
let part = 0;
let shift;
let cidrBits = cidr.bits;
while (cidrBits > 0) {
shift = cidr.partSize - cidrBits;
if (shift < 0) shift = 0;
if (ip[part] >> shift !== cidr.parts[part] >> shift) return false;
cidrBits -= cidr.partSize;
part += 1;
}
return true;
}
var Ipv4Cidr = class {
type = "v4";
partSize = 8;
parts;
bits;
constructor(parts, bits) {
this.bits = bits;
this.parts = parts;
Object.freeze(this);
}
contains(ip) {
return cidrContains(this, ip);
}
};
var Ipv6Cidr = class {
type = "v6";
partSize = 16;
parts;
bits;
constructor(parts, bits) {
this.bits = bits;
this.parts = parts;
Object.freeze(this);
}
contains(ip) {
return cidrContains(this, ip);
}
};
function parseCidr(cidr) {
const cidrParts = cidr.split("/");
if (cidrParts.length !== 2) throw new Error("invalid CIDR address: must be exactly 2 parts");
const parser = new Parser(cidrParts[0]);
const maybeIpv4 = parser.readIpv4Address();
if (isIpv4Tuple(maybeIpv4)) {
const bits = parseInt(cidrParts[1], 10);
if (isNaN(bits) || bits < 0 || bits > 32) throw new Error("invalid CIDR address: incorrect amount of bits");
return new Ipv4Cidr(maybeIpv4, bits);
}
const maybeIpv6 = parser.readIpv6Address();
if (isIpv6Tuple(maybeIpv6)) {
const bits = parseInt(cidrParts[1], 10);
if (isNaN(bits) || bits < 0 || bits > 128) throw new Error("invalid CIDR address: incorrect amount of bits");
return new Ipv6Cidr(maybeIpv6, bits);
}
throw new Error("invalid CIDR address: could not parse IP address");
}
function isCidr(address) {
return address.includes("/");
}
/**
* Parse CIDR addresses and keep non-CIDR IP addresses.
*
* @param value
* Value to parse.
* @returns
* Parsed {@linkcode Cidr} if range or given `value` if IP.
*/
function parseProxy(value) {
if (isCidr(value)) return parseCidr(value);
else return value;
}
/**
* Parse a list of trusted proxies.
*
* CIDR range strings are parsed to {@linkcode Cidr} (so they match by range);
* plain IP strings and {@linkcode ProxyService} objects (such as those created
* by {@linkcode cloudflare}) are passed through unchanged. Use this to
* normalize the `proxies` option before handing it to {@linkcode findIp}.
*
* @param proxies
* Trusted proxies to parse.
* @returns
* Parsed proxies.
*/
function parseProxies(proxies) {
return proxies.map((proxy) => typeof proxy === "string" ? parseProxy(proxy) : proxy);
}
function isIpv4Tuple(segements) {
if (typeof segements === "undefined") return false;
return segements.length === 4;
}
function isIpv6Tuple(segements) {
if (typeof segements === "undefined") return false;
return segements.length === 8;
}
function u16FromBytes(bytes) {
const u8 = new Uint8Array(bytes);
return new Uint16Array(u8.buffer)[0];
}
function u32FromBytes(bytes) {
const u8 = new Uint8Array(bytes);
return new Uint32Array(u8.buffer)[0];
}
var Parser = class {
state;
constructor(input) {
this.state = input;
}
readAtomically(inner) {
const state = this.state;
const result = inner(this);
if (typeof result === "undefined") this.state = state;
return result;
}
peakChar() {
return this.state[0];
}
readChar() {
const b = this.state[0];
this.state = this.state.slice(1);
return b;
}
readGivenChar(target) {
return this.readAtomically((p) => {
const c = p.readChar();
if (c === target) return c;
});
}
readSeparator(sep, index, inner) {
return this.readAtomically((p) => {
if (index > 0) {
if (typeof p.readGivenChar(sep) === "undefined") return;
}
return inner(p);
});
}
readNumber(radix, maxDigits, allowZeroPrefix = false) {
return this.readAtomically((p) => {
let result = 0;
let digitCount = 0;
const hasLeadingZero = p.peakChar() === "0";
function nextCharAsDigit() {
return p.readAtomically((p) => {
const c = p.readChar();
if (c) {
const n = parseInt(c, radix);
if (!isNaN(n)) return n;
}
});
}
for (let digit = nextCharAsDigit(); digit !== void 0; digit = nextCharAsDigit()) {
result = result * radix;
result = result + digit;
digitCount += 1;
if (typeof maxDigits !== "undefined") {
if (digitCount > maxDigits) return;
}
}
if (digitCount === 0) return;
else if (!allowZeroPrefix && hasLeadingZero && digitCount > 1) return;
else return result;
});
}
readIpv4Address() {
return this.readAtomically((p) => {
const groups = [];
for (let idx = 0; idx < 4; idx++) {
const result = p.readSeparator(".", idx, (p) => {
return p.readNumber(10, 3, false);
});
if (result === void 0) return;
else groups.push(result);
}
return groups;
});
}
readIpv6Address() {
const readGroups = (p, groups) => {
const limit = groups.length;
for (const i of groups.keys()) {
if (i < limit - 1) {
const ipv4 = p.readSeparator(":", i, (p) => p.readIpv4Address());
if (isIpv4Tuple(ipv4)) {
const [one, two, three, four] = ipv4;
groups[i + 0] = u16FromBytes([one, two]);
groups[i + 1] = u16FromBytes([three, four]);
return [i + 2, true];
}
}
const group = p.readSeparator(":", i, (p) => p.readNumber(16, 4, true));
if (typeof group !== "undefined") groups[i] = group;
else return [i, false];
}
return [groups.length, false];
};
return this.readAtomically((p) => {
const head = /* @__PURE__ */ new Uint16Array(8);
const [headSize, headIpv4] = readGroups(p, head);
if (headSize === 8) return head;
if (headIpv4) return;
if (typeof p.readGivenChar(":") === "undefined") return;
if (typeof p.readGivenChar(":") === "undefined") return;
const tail = /* @__PURE__ */ new Uint16Array(7);
const limit = 8 - (headSize + 1);
const [tailSize, _] = readGroups(p, tail.subarray(0, limit));
head.set(tail.slice(0, tailSize), 8 - tailSize);
return head;
});
}
readPort() {
return this.readAtomically((p) => {
if (typeof p.readGivenChar(":") !== "undefined") return p.readNumber(10, void 0, true);
});
}
readScopeId() {
return this.readAtomically((p) => {
if (typeof p.readGivenChar("%") !== "undefined") return p.readNumber(10, void 0, true);
});
}
};
const IPV4_BROADCAST = u32FromBytes([
255,
255,
255,
255
]);
function isGlobalIpv4(s, proxies) {
if (typeof s !== "string") return false;
const parser = new Parser(s);
const octets = parser.readIpv4Address();
if (!isIpv4Tuple(octets)) return false;
if (isTrustedProxy(s, octets, proxies)) return false;
parser.readPort();
if (parser.state.length !== 0) return false;
if (octets[0] === 0) return false;
if (octets[0] === 10) return false;
if (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) return false;
if (octets[0] === 192 && octets[1] === 168) return false;
if (octets[0] === 127) return false;
if (octets[0] === 100 && (octets[1] & 192) === 64) return false;
if (octets[0] === 169 && octets[1] === 254) return false;
if (octets[0] === 192 && octets[1] === 0 && octets[2] === 0) return false;
if (octets[0] === 192 && octets[1] === 0 && octets[2] === 2) return false;
if (octets[0] === 198 && octets[1] === 51 && octets[2] === 100) return false;
if (octets[0] === 203 && octets[1] === 0 && octets[2] === 113) return false;
if (octets[0] === 198 && (octets[1] & 254) === 18) return false;
const isBroadcast = u32FromBytes(octets) === IPV4_BROADCAST;
if ((octets[0] & 240) === 240 && !isBroadcast) return false;
if (isBroadcast) return false;
for (const octet of octets) if (octet < 0 || octet > 255) return false;
return true;
}
function isGlobalIpv6(s, proxies) {
if (typeof s !== "string") return false;
const parser = new Parser(s);
const segments = parser.readIpv6Address();
if (!isIpv6Tuple(segments)) return false;
if (isTrustedProxy(s, segments, proxies)) return false;
parser.readScopeId();
if (parser.state.length !== 0) return false;
if (segments[0] === 0 && segments[1] === 0 && segments[2] === 0 && segments[3] === 0 && segments[4] === 0 && segments[5] === 0 && segments[6] === 0 && segments[7] === 0) return false;
if (segments[0] === 0 && segments[1] === 0 && segments[2] === 0 && segments[3] === 0 && segments[4] === 0 && segments[5] === 0 && segments[6] === 0 && segments[7] === 1) return false;
if (segments[0] === 0 && segments[1] === 0 && segments[2] === 0 && segments[3] === 0 && segments[4] === 0 && segments[5] === 65535) return false;
if (segments[0] === 100 && segments[1] === 65435 && segments[2] === 1) return false;
if (segments[0] === 256 && segments[1] === 0 && segments[2] === 0 && segments[3] === 0) return false;
if (segments[0] === 8193 && segments[1] < 512) {
if (segments[0] === 8193 && segments[1] === 1 && segments[2] === 0 && segments[3] === 0 && segments[4] === 0 && segments[5] === 0 && segments[6] === 0 && segments[7] === 1) return true;
if (segments[0] === 8193 && segments[1] === 1 && segments[2] === 0 && segments[3] === 0 && segments[4] === 0 && segments[5] === 0 && segments[6] === 0 && segments[7] === 2) return true;
if (segments[0] === 8193 && segments[1] === 3) return true;
if (segments[0] === 8193 && segments[1] === 4 && segments[2] === 274) return true;
if (segments[0] === 8193 && segments[1] >= 32 && segments[1] <= 47) return true;
return false;
}
if (segments[0] === 8193 && segments[1] === 3512) return false;
if ((segments[0] & 65024) === 64512) return false;
if ((segments[0] & 65472) === 65152) return false;
return true;
}
function isGlobalIp(s, proxies) {
if (isGlobalIpv4(s, proxies)) return true;
if (isGlobalIpv6(s, proxies)) return true;
return false;
}
function isHeaders(val) {
return typeof val.get === "function";
}
function getHeader(headers, headerKey) {
if (isHeaders(headers)) return headers.get(headerKey);
else {
const headerValue = headers[headerKey];
if (Array.isArray(headerValue)) return headerValue.join(",");
else return headerValue;
}
}
/**
* Find a client IP address on a request-like object.
*
* @param request
* Request-like object.
* @param [options]
* Configuration (optional).
* @returns
* Found IP address; empty string if not found.
*/
function findIp(request, options) {
const { platform, proxies: rawProxies } = options || {};
const proxies = [];
const services = [];
if (Array.isArray(rawProxies)) for (const cidrOrIp of rawProxies) {
if (typeof cidrOrIp === "string") proxies.push(parseProxy(cidrOrIp));
if (isIpv4Cidr(cidrOrIp) || isIpv6Cidr(cidrOrIp)) proxies.push(cidrOrIp);
if (isProxyService(cidrOrIp)) {
const ranges = [];
for (const range of cidrOrIp.ranges) ranges.push(typeof range === "string" ? parseProxy(range) : range);
services.push({
...cidrOrIp,
ranges
});
}
}
const ipFromRequest = resolveCandidate(request.ip, services, proxies, request.headers);
if (ipFromRequest) return ipFromRequest;
const socketRemoteAddress = resolveCandidate(request.socket?.remoteAddress, services, proxies, request.headers);
if (socketRemoteAddress) return socketRemoteAddress;
const infoRemoteAddress = resolveCandidate(request.info?.remoteAddress, services, proxies, request.headers);
if (infoRemoteAddress) return infoRemoteAddress;
const requestContextIdentitySourceIp = resolveCandidate(request.requestContext?.identity?.sourceIp, services, proxies, request.headers);
if (requestContextIdentitySourceIp) return requestContextIdentitySourceIp;
if (typeof request.headers !== "object" || request.headers === null) return "";
if (platform === "cloudflare") {
const cfConnectingIpv6 = getHeader(request.headers, "cf-connecting-ipv6");
if (isGlobalIpv6(cfConnectingIpv6, proxies)) return cfConnectingIpv6;
const cfConnectingIp = getHeader(request.headers, "cf-connecting-ip");
if (isGlobalIp(cfConnectingIp, proxies)) return cfConnectingIp;
return "";
}
if (platform === "firebase") {
const fahClientIp = resolveCandidate(getHeader(request.headers, "x-fah-client-ip"), services, proxies, request.headers);
if (fahClientIp) return fahClientIp;
const xForwardedForItems = parseXForwardedFor(getHeader(request.headers, "x-forwarded-for"));
for (const item of xForwardedForItems.reverse()) {
const resolved = resolveCandidate(item, services, proxies, request.headers);
if (resolved) return resolved;
}
return "";
}
if (platform === "fly-io") {
const flyClientIp = resolveCandidate(getHeader(request.headers, "fly-client-ip"), services, proxies, request.headers);
if (flyClientIp) return flyClientIp;
return "";
}
if (platform === "vercel") {
const xRealIp = resolveCandidate(getHeader(request.headers, "x-real-ip"), services, proxies, request.headers);
if (xRealIp) return xRealIp;
const xVercelForwardedForItems = parseXForwardedFor(getHeader(request.headers, "x-vercel-forwarded-for"));
for (const item of xVercelForwardedForItems.reverse()) {
const resolved = resolveCandidate(item, services, proxies, request.headers);
if (resolved) return resolved;
}
const xForwardedForItems = parseXForwardedFor(getHeader(request.headers, "x-forwarded-for"));
for (const item of xForwardedForItems.reverse()) {
const resolved = resolveCandidate(item, services, proxies, request.headers);
if (resolved) return resolved;
}
return "";
}
if (platform === "render") {
const trueClientIp = resolveCandidate(getHeader(request.headers, "true-client-ip"), services, proxies, request.headers);
if (trueClientIp) return trueClientIp;
return "";
}
const xForwardedForItems = parseXForwardedFor(getHeader(request.headers, "x-forwarded-for"));
for (const item of xForwardedForItems.reverse()) {
const resolved = resolveCandidate(item, services, proxies, request.headers);
if (resolved) return resolved;
}
const xClientIp = resolveCandidate(getHeader(request.headers, "x-client-ip"), services, proxies, request.headers);
if (xClientIp) return xClientIp;
const doConnectingIp = resolveCandidate(getHeader(request.headers, "do-connecting-ip"), services, proxies, request.headers);
if (doConnectingIp) return doConnectingIp;
const fastlyClientIp = resolveCandidate(getHeader(request.headers, "fastly-client-ip"), services, proxies, request.headers);
if (fastlyClientIp) return fastlyClientIp;
const trueClientIp = resolveCandidate(getHeader(request.headers, "true-client-ip"), services, proxies, request.headers);
if (trueClientIp) return trueClientIp;
const xRealIp = resolveCandidate(getHeader(request.headers, "x-real-ip"), services, proxies, request.headers);
if (xRealIp) return xRealIp;
const xClusterClientIp = resolveCandidate(getHeader(request.headers, "x-cluster-client-ip"), services, proxies, request.headers);
if (xClusterClientIp) return xClusterClientIp;
const xForwarded = resolveCandidate(getHeader(request.headers, "x-forwarded"), services, proxies, request.headers);
if (xForwarded) return xForwarded;
const forwardedFor = resolveCandidate(getHeader(request.headers, "forwarded-for"), services, proxies, request.headers);
if (forwardedFor) return forwardedFor;
const forwarded = resolveCandidate(getHeader(request.headers, "forwarded"), services, proxies, request.headers);
if (forwarded) return forwarded;
const xAppEngineUserIp = resolveCandidate(getHeader(request.headers, "x-appengine-user-ip"), services, proxies, request.headers);
if (xAppEngineUserIp) return xAppEngineUserIp;
return "";
}
//#endregion
export { cloudflare, findIp as default, findIp, parseProxies, parseProxy };