import dns from 'dns/promises' const raw = (process.env.OFFICE_IP_CHECK || 'disabled').trim() const matchers = raw.split(',').map(s => s.trim()).filter(Boolean) const TTL = 5 * 60 * 1000 const cache = new Map() const PUBLIC_IP_URLS = [ 'https://api.ipify.org', 'https://ifconfig.co/ip', 'https://icanhazip.com', ] function normalizeIP(ip) { return ip?.startsWith('::ffff:') ? ip.slice(7) : ip } function isIPv4(s) { return /^\d{1,3}(\.\d{1,3}){3}$/.test(s) } function ipInCidr(ip, cidr) { const [range, bits] = cidr.split('/') if (!isIPv4(ip) || !isIPv4(range)) return false const mask = ~(2 ** (32 - parseInt(bits)) - 1) >>> 0 const toInt = s => s.split('.').reduce((a, o) => (a << 8) + parseInt(o), 0) >>> 0 return (toInt(ip) & mask) === (toInt(range) & mask) } async function fetchPublicIP() { for (const url of PUBLIC_IP_URLS) { try { const ctrl = new AbortController() const timer = setTimeout(() => ctrl.abort(), 4000) const res = await fetch(url, { signal: ctrl.signal }) clearTimeout(timer) if (!res.ok) continue const ip = (await res.text()).trim() if (isIPv4(ip)) return ip } catch { // try next } } return null } async function resolveDynamic(key, resolver) { const hit = cache.get(key) if (hit && Date.now() < hit.expiry) return hit.ip const ip = await resolver() if (ip) { cache.set(key, { ip, expiry: Date.now() + TTL }) return ip } return hit ? hit.ip : null } export async function isOnsite(requestIP) { if (matchers.length === 0 || matchers.includes('disabled')) return true const ip = normalizeIP(requestIP) if (!ip) return false for (const m of matchers) { if (m === 'auto') { const pub = await resolveDynamic('auto', fetchPublicIP) if (pub && ip === pub) return true } else if (m.includes('/')) { if (ipInCidr(ip, m)) return true } else if (/[a-zA-Z]/.test(m)) { const resolved = await resolveDynamic(m, async () => { try { return (await dns.resolve4(m))[0] } catch { return null } }) if (resolved && ip === resolved) return true } else { if (ip === m) return true } } return false }