| 467 | } |
| 468 | |
| 469 | export async function getFreeModeCountryAccess( |
| 470 | req: NextRequest, |
| 471 | options: FreeModeCountryAccessOptions, |
| 472 | ): Promise<FreeModeCountryAccess> { |
| 473 | const cfCountry = req.headers.get('cf-ipcountry')?.toUpperCase() ?? null |
| 474 | const clientIp = extractClientIp(req) |
| 475 | const clientIpHash = hashClientIp(clientIp, options.ipHashSecret) |
| 476 | |
| 477 | // Dev-only bypass: when no Cloudflare country header is set and the request |
| 478 | // is from loopback (or has no client IP at all), treat it as US-allowed so |
| 479 | // local development doesn't require ipinfo or geoip resolution. In |
| 480 | // production behind Cloudflare, cf-ipcountry is always set, so this branch |
| 481 | // is unreachable. |
| 482 | if ( |
| 483 | options.allowLocalhost && |
| 484 | !cfCountry && |
| 485 | (!clientIp || isLocalhostIp(clientIp)) |
| 486 | ) { |
| 487 | if (options.forceLimited) { |
| 488 | return { |
| 489 | allowed: false, |
| 490 | countryCode: 'US', |
| 491 | blockReason: 'country_not_allowed', |
| 492 | cfCountry: null, |
| 493 | geoipCountry: null, |
| 494 | ipPrivacy: { signals: [] }, |
| 495 | ...NOT_CHECKED_SPUR_CONTEXT, |
| 496 | hasClientIp: Boolean(clientIp), |
| 497 | // Null hash skips the country-access cache so toggling the env var |
| 498 | // takes effect immediately without evicting prior allowed=true rows. |
| 499 | clientIpHash: null, |
| 500 | } |
| 501 | } |
| 502 | return { |
| 503 | allowed: true, |
| 504 | countryCode: 'US', |
| 505 | blockReason: null, |
| 506 | cfCountry: null, |
| 507 | geoipCountry: null, |
| 508 | ipPrivacy: { signals: [] }, |
| 509 | ...NOT_CHECKED_SPUR_CONTEXT, |
| 510 | hasClientIp: Boolean(clientIp), |
| 511 | clientIpHash, |
| 512 | } |
| 513 | } |
| 514 | |
| 515 | if (cfCountry && CLOUDFLARE_ANONYMIZED_OR_UNKNOWN_COUNTRIES.has(cfCountry)) { |
| 516 | return { |
| 517 | allowed: false, |
| 518 | countryCode: null, |
| 519 | blockReason: 'anonymized_or_unknown_country', |
| 520 | cfCountry, |
| 521 | geoipCountry: null, |
| 522 | ipPrivacy: |
| 523 | cfCountry === CLOUDFLARE_TOR_COUNTRY ? { signals: ['tor'] } : null, |
| 524 | ...NOT_CHECKED_SPUR_CONTEXT, |
| 525 | hasClientIp: Boolean(clientIp), |
| 526 | clientIpHash, |