Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 173 additions & 44 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,83 +1,212 @@
# ─────────────────────────────────────────────────────────────────────────────
# IPGeolocation.io – Next.js Middleware · Environment Variables
# ─────────────────────────────────────────────────────────────────────────────
# Get your free API key at https://ipgeolocation.io
# ─────────────────────────────────────────────────────────────────────────────

# ── Required ──────────────────────────────────────────────────────────────────

# =============================================================================
# IPGeolocation.io middleware for Next.js
# Environment variables
#
# Copy the values you need into .env.local for local development, and into
# Project Settings > Environment Variables on Vercel for deployments.
#
# Get an API key at https://app.ipgeolocation.io/signup
# =============================================================================

# -----------------------------------------------------------------------------
# Required
# -----------------------------------------------------------------------------

# Your IPGeolocation.io API key. Without it the middleware does nothing.
IPGEOLOCATION_API_KEY=

# -----------------------------------------------------------------------------
# Country controls
# -----------------------------------------------------------------------------

# ── Country controls ──────────────────────────────────────────────────────────

# Allow-list: ONLY these countries can access your app (others → block page).
# Leave empty to allow all countries.
# Format: comma-separated ISO 3166-1 alpha-2 codes (case-insensitive)
# Allow list. Only these countries can reach the app, everyone else is blocked.
# Leave empty to allow every country.
# Format: comma separated ISO 3166-1 alpha-2 codes, case insensitive.
# Example: US,CA,GB,AU
IPGEO_ALLOWED_COUNTRIES=

# Block-list: these countries are always redirected to the block page.
# Format: comma-separated ISO 3166-1 alpha-2 codes (case-insensitive)
# Block list. These countries are always blocked.
# Example: CN,RU,KP
IPGEO_BLOCKED_COUNTRIES=

# Country-based path redirects (JSON map).
# Users from a matching country are redirected to the given path once.
# Example: {"US":"/us","GB":"/uk","PK":"/pk","DE":"/de"}
# When an allow list is set and the country cannot be determined, the request is
# blocked. Set this to true to let unknown countries through instead.
IPGEO_ALLOW_UNKNOWN_COUNTRY=false

# Country to path redirects, as a JSON object.
# Example: {"US":"/us","GB":"/uk","DE":"/de"}
IPGEO_COUNTRY_REDIRECTS={}

# Keep the requested path and query string when redirecting, so that
# /pricing?ref=ad becomes /uk/pricing?ref=ad.
IPGEO_REDIRECT_PRESERVE_PATH=true

# Leave visitors alone when they are already on one of the mapped paths, so a
# German visitor who opens /uk on purpose stays there.
IPGEO_REDIRECT_RESPECT_EXISTING=true

# ── Security controls ─────────────────────────────────────────────────────────
# HTTP status for country redirects. One of 301, 302, 307 or 308.
IPGEO_REDIRECT_STATUS=307

# Block VPN users
# Name of a cookie that switches country redirects off for that visitor. Set the
# cookie from a "stay on this site" link. Leave empty to disable the feature.
IPGEO_REDIRECT_SKIP_COOKIE=

# -----------------------------------------------------------------------------
# Security controls
#
# Every variable in this block needs the security module, which is available on
# paid plans. A lookup with security costs 3 credits instead of 1. When all of
# them are false and no threat score threshold is set, the middleware does not
# request the security module at all and each lookup costs 1 credit.
# -----------------------------------------------------------------------------

# Block VPN exit nodes.
IPGEO_BLOCK_VPN=false

# Block proxy connections (datacenter / anonymous proxies)
# Block datacenter and anonymous proxies.
IPGEO_BLOCK_PROXY=false

# Block Tor exit nodes (recommended: true)
IPGEO_BLOCK_TOR=true
# Block residential proxy networks.
IPGEO_BLOCK_RESIDENTIAL_PROXY=false

# Block Tor exit nodes.
IPGEO_BLOCK_TOR=false

# Block relay networks such as iCloud Private Relay.
# Be careful: this affects a large number of ordinary Apple users.
IPGEO_BLOCK_RELAY=false

# Block cloud / datacenter IP ranges (AWS, GCP, Azure, etc.)
# Block cloud and hosting ranges such as AWS, GCP and Azure.
IPGEO_BLOCK_CLOUD_PROVIDER=false

# Block known bots / scrapers
# Block IPs with bot activity.
IPGEO_BLOCK_BOT=false

# Block IPs flagged as spam sources
# Keep bots the API marks as known good, such as search engine crawlers, even
# when IPGEO_BLOCK_BOT is on. Turning this off can remove your site from search
# results, so leave it on unless you know you need the opposite.
IPGEO_ALLOW_KNOWN_GOOD_BOTS=true

# Block IPs flagged as spam sources.
IPGEO_BLOCK_SPAM=false

# Block IPs with a known attack history
# Block IPs with a known attack history.
IPGEO_BLOCK_KNOWN_ATTACKER=false

# Block if threat score ≥ this value. Leave empty to disable.
# Range: 0–100. Recommended starting value: 75
# Example: 75
# Block any IP the API marks as anonymous, which covers VPN, proxy, Tor and
# relay in one flag.
IPGEO_BLOCK_ANONYMOUS=false

# Block at or above this threat score, from 0 to 100. Leave empty to disable.
# A useful starting point is 75.
IPGEO_THREAT_SCORE_BLOCK_THRESHOLD=

# Block the request when a security rule is active but the API returned no
# security data, which happens on a free plan. The default is to let the request
# through and log the problem.
IPGEO_REQUIRE_SECURITY=false

# ── Behavior ──────────────────────────────────────────────────────────────────
# -----------------------------------------------------------------------------
# Blocking behaviour
# -----------------------------------------------------------------------------

# Path to redirect blocked requests to
# How to handle a blocked request.
# redirect send the visitor to IPGEO_BLOCK_PATH (default)
# rewrite render IPGEO_BLOCK_PATH at the original URL
# deny answer immediately with IPGEO_BLOCK_STATUS and IPGEO_BLOCK_MESSAGE
IPGEO_BLOCK_MODE=redirect

# The page used by redirect and rewrite mode.
IPGEO_BLOCK_PATH=/blocked

# Prefix for geo headers forwarded to your app
# Your app can read e.g. request.headers.get('x-ipgeo-country')
# Status and body used by deny mode.
IPGEO_BLOCK_STATUS=403
IPGEO_BLOCK_MESSAGE=Access to this site is restricted.

# Add ?reason=... to the block URL. The reason is always sent as the
# x-ipgeo-block-reason response header.
IPGEO_BLOCK_INCLUDE_REASON=true

# Block traffic when a lookup fails, instead of letting it through.
# false keeps the site available during an API incident, which is the default.
IPGEO_FAIL_CLOSED=false

# -----------------------------------------------------------------------------
# Bypasses
# -----------------------------------------------------------------------------

# Turn the middleware off without removing it from the project.
IPGEO_ENABLED=true

# Paths that skip every check. Useful for health checks and inbound webhooks.
# Example: /health,/api/webhooks
IPGEO_BYPASS_PATHS=

# IP addresses that skip every check, comma separated.
# Example: 203.0.113.10,198.51.100.4
IPGEO_BYPASS_IPS=

# A shared secret. A request carrying x-ipgeo-bypass-token with this value skips
# every check, which lets an uptime monitor through. Leave empty to disable.
IPGEO_BYPASS_TOKEN=

# -----------------------------------------------------------------------------
# Headers
# -----------------------------------------------------------------------------

# Prefix for the geo headers the middleware adds to the request.
# Inbound headers that use this prefix are always removed first, so a visitor
# cannot forge them.
IPGEO_HEADER_PREFIX=x-ipgeo

# API request timeout in milliseconds (default: 3000)
# Also copy the geo headers onto the response. Off by default, because the
# values describe the visitor and should not be cached by a shared cache.
IPGEO_SET_RESPONSE_HEADERS=false

# -----------------------------------------------------------------------------
# Network behaviour
# -----------------------------------------------------------------------------

# API timeout per attempt, in milliseconds.
IPGEO_TIMEOUT_MS=3000

# In-memory cache TTL in milliseconds (default: 60000 = 1 minute)
# Set to 0 to disable caching entirely.
# Retries for a timeout, a network error or a 5xx response. 0 to 3.
IPGEO_RETRIES=1

# In memory cache lifetime in milliseconds. 0 disables caching.
IPGEO_CACHE_TTL_MS=60000

# fail-closed: if the geo lookup fails (timeout / API error), block the request
# instead of passing it through. Default: false (fail-open = safer for uptime)
IPGEO_FAIL_CLOSED=false
# Largest number of cached IPs per edge isolate. 0 disables caching.
IPGEO_CACHE_MAX_ENTRIES=1000

# Pause lookups after this many consecutive failures. 0 disables the breaker.
IPGEO_CIRCUIT_FAILURE_THRESHOLD=5

# How long the pause lasts, in milliseconds.
IPGEO_CIRCUIT_COOLDOWN_MS=30000

# Restrict the API response to these fields to shrink the payload.
# Leave empty for the full response.
# Example: location,asn,time_zone,security
IPGEO_FIELDS=

# -----------------------------------------------------------------------------
# Client IP resolution
# -----------------------------------------------------------------------------

# Read the leftmost entry of x-forwarded-for instead of the rightmost.
# Leave this off on Vercel.
IPGEO_TRUST_FIRST_XFF=false

# Number of proxies you operate in front of the app. The client IP is read that
# many positions to the left of the rightmost entry.
IPGEO_TRUSTED_PROXY_COUNT=0

# -----------------------------------------------------------------------------
# Logging
# -----------------------------------------------------------------------------

# Trust the FIRST IP in x-forwarded-for instead of the last.
# On Vercel, the LAST IP is injected by Vercel's edge and is most trustworthy.
# Only set this to true if you're behind a custom reverse proxy that you control.
IPGEO_TRUST_FIRST_XFF=false
# One of silent, error, warn, info or debug. Use debug while setting things up,
# then go back to warn.
IPGEO_LOG_LEVEL=warn
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
test:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [18, 20, 22]

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: npm

- run: npm ci
- run: npm run typecheck
- run: npm test
- run: npm run build
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
node_modules/
dist/
coverage/
.env
.env.local
*.env
*.env.local
.DS_Store
.npmrc
.npmrc
.vscode
87 changes: 83 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,91 @@
# Changelog

All notable changes to `ipgeolocation-vercel-middleware` will be documented here.
All notable changes to `ipgeolocation-vercel-middleware` are recorded here.
This project follows [Semantic Versioning](https://semver.org/).

This project adheres to [Semantic Versioning](https://semver.org/).
## 2.0.0

---
A correctness and hardening release. Read the upgrade notes before deploying,
because several defaults changed.

## [1.0.0] — Initial release
### Breaking changes

- The security module is now requested only when a security rule is switched on.
Version 1.0.0 always sent `include=security`, which made every lookup cost 3
credits and returned HTTP 401 on free plans. With no security rules active, a
lookup now costs 1 credit and works on a free key.
- `lookupIpGeolocation` no longer defaults `includeSecurity` to `true`. Pass
`includeSecurity: true` or `include: ['security']` when you want it.
- `lookupIpGeolocation` rejects private, loopback and bogon addresses before
calling the API, because the API answers HTTP 423 for them. Pass
`allowPrivateIp: true` to restore the old behaviour.
- `getClientIp` validates the address and ignores private ranges by default.
It returns `null` where 1.0.0 returned `127.0.0.1` during local development.
- Country redirects keep the requested path and query string by default. Set
`IPGEO_REDIRECT_PRESERVE_PATH=false` for the 1.0.0 behaviour.
- When `IPGEO_ALLOWED_COUNTRIES` is set and the country cannot be determined,
the request is blocked. Set `IPGEO_ALLOW_UNKNOWN_COUNTRY=true` to allow it.
- Country blocks now include `?reason=country` in the block URL.

### Added

- `evaluateIpGeolocation`, which returns the decision without sending a
response, so the geo headers survive when you compose your own middleware.
- `withIpGeolocation`, a wrapper for the same pattern.
- `createIpGeoMiddleware` and `getMiddlewareConfig` for configuration in code.
- `proxy`, an alias of `middleware` for the Next.js 16 `proxy.ts` convention.
- `lookupIpGeolocationResult`, which reports the failure reason, the HTTP
status, whether the answer came from cache and how many credits were charged.
- Block modes: `redirect`, `rewrite` and `deny` through `IPGEO_BLOCK_MODE`.
- Bypasses: `IPGEO_ENABLED`, `IPGEO_BYPASS_PATHS`, `IPGEO_BYPASS_IPS` and
`IPGEO_BYPASS_TOKEN`.
- Retries for timeouts, network errors and 5xx responses, with `IPGEO_RETRIES`.
- A circuit breaker, `IPGEO_CIRCUIT_FAILURE_THRESHOLD` and
`IPGEO_CIRCUIT_COOLDOWN_MS`, so an API incident does not add the full timeout
to every request.
- Request coalescing, so concurrent lookups for one IP share a single call.
- A bounded cache, `IPGEO_CACHE_MAX_ENTRIES`, with eviction of the coldest keys.
- Automatic retry without the security module when a free plan key rejects it,
plus a five minute latch so the mistake is not repeated on every request.
- New rules: `IPGEO_BLOCK_RESIDENTIAL_PROXY`, `IPGEO_BLOCK_RELAY`,
`IPGEO_BLOCK_ANONYMOUS`, `IPGEO_ALLOW_KNOWN_GOOD_BOTS` and
`IPGEO_REQUIRE_SECURITY`.
- Redirect controls: `IPGEO_REDIRECT_STATUS`, `IPGEO_REDIRECT_PRESERVE_PATH`,
`IPGEO_REDIRECT_RESPECT_EXISTING` and `IPGEO_REDIRECT_SKIP_COOKIE`.
- `IPGEO_TRUSTED_PROXY_COUNT` for deployments behind your own reverse proxy.
- `IPGEO_LOG_LEVEL` with a `warn` default, replacing a log line per lookup.
- `IPGEO_SET_RESPONSE_HEADERS` for copying the geo headers onto the response.
- New headers: `x-ipgeo-continent`, `x-ipgeo-state-code`, `x-ipgeo-zipcode`,
`x-ipgeo-is-eu`, `x-ipgeo-currency`, `x-ipgeo-company`,
`x-ipgeo-is-residential-proxy`, `x-ipgeo-is-relay`, `x-ipgeo-is-anonymous`
and `x-ipgeo-is-known-good-bot`.
- `resetIpGeoRuntimeState` and `getIpGeoRuntimeState` for tests and health
checks.
- 177 unit tests covering the library and the middleware.

### Fixed

- Inbound `x-ipgeo-*` headers are removed on every path. In 1.0.0 a visitor
could send `x-ipgeo-country` and the application had no way to tell.
- A redirect target with a trailing slash, such as `{"GB":"/uk/"}`, caused an
endless redirect loop. Targets are normalised and loops are detected.
- Redirect targets pointing at another origin are rejected, so a mistyped
variable cannot turn the middleware into an open redirect.
- The default matcher skipped every path starting with `blocked`, including
`/blockedlist`, and ran on static files. Both are fixed.
- Country blocks sent no reason, so the block page could not explain itself.
- Country redirects fired on POST requests, which turned a form submission into
a GET.
- An unexpected error inside the middleware could return HTTP 500 for the whole
site. Errors are caught and follow the fail open or fail closed setting.
- Blocks and redirects now send `cache-control: no-store`, so a shared cache
cannot serve one visitor's geo decision to another.
- `x-forwarded-for` values with a port, IPv6 in brackets, IPv4 mapped IPv6 and
zone indexes are parsed correctly instead of being sent to the API as is.
- `IPGEO_COUNTRY_REDIRECTS` is parsed once per value rather than on every
request.

## 1.0.0

### Added
- `lookupIpGeolocation` — fetch geo + security data from IPGeolocation.io v3 API
Expand Down
Loading
Loading