common/ had become the backend's library: 30 of its 41 modules were
imported by nothing in the frontend, so the name no longer said what
belonged there. It now holds only what the frontend or the build imports
too; backend-only code moved to server/, flat except for datasets/ (the
dataset engine and its readers). The *-db/ dataset directories stay in
common/: self-hosting docs and Docker volumes mount those paths.
frontend/utils/ keeps its app-wide modules at the root and gains ip/
(IP / ASN shaping and validation), report/ (the report pipeline) and
features/ (the pure layer of a single feature). Paths only — no logic
changes. The Dockerfile copies server/, and the AGENTS.md files say where
a new module goes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace all backend console.* with a shared pino singleton at
common/logger.js. Startup-style lines get emoji prefixes (🚀 listening,
📦 maxmind ready, 📥 downloading, 🗓️ schedule, ⚠️ warning, ❌ failure);
per-handler / per-request logs stay plain.
Three .env-driven knobs, all without NODE_ENV:
- LOG_LEVEL (default info) — pino level filter
- LOG_FORMAT (default pretty) — "json" for log shippers
- LOG_HTTP (default off) — pino-http on /api/* gated behind this flag
so pm2 logs don't bloat with 2xx/3xx request lines on every deploy;
when enabled, 4xx log as warn and 5xx as error
logger.js calls dotenv.config({ quiet: true }) itself because ES module
imports are hoisted above backend-server.js's dotenv call — without that
LOG_LEVEL from .env was silently ignored.
Fixes in common/rdap.js:
- err: res.status → status: res.status (pino's err serializer expects
an Error object, not a number)
- broken single-quoted template literal in "No RDAP endpoint" message
- demote "No RDAP endpoint" and "Domain not found" from error to warn
(they're expected outcomes for user input, not server failures)
README (en/zh/fr/tr) + .env.example + AGENTS.md (root and api/) all
updated to document the three new vars and the "no console.* in backend"
convention.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The two wrappers (frontend/utils/fetch-with-timeout.js and
common/fetch-upstream.js) were ~95% identical — same AbortController +
setTimeout pattern, same caller-signal chaining, differing only in
default timeout (5s vs 8s) and which half of the app imported them.
Their tests were word-for-word duplicates too.
Consolidate to common/fetch-with-timeout.js with two named exports:
- fetchWithTimeout(url, init) — 5s default (front-end use)
- fetchUpstream(url, init) — 8s preset around fetchWithTimeout (back-end)
- frontend/utils/fetch-with-timeout.js becomes a thin re-export of the
common module (same pattern we use for valid-ip.js). Front-end imports
don't change.
- All 12 back-end handler imports move from './common/fetch-upstream.js'
to './common/fetch-with-timeout.js'.
- tests/fetch-upstream.test.js is deleted; tests/fetch-with-timeout.test.js
now covers both exports (10 cases total).
- common/fetch-upstream.js deleted.
Net: -1 file, -130 lines of duplicate code & tests; single source of truth
for the timeout logic.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
All 16 handlers previously mixed two HTTP client styles:
- 7 used Node's built-in https.get()/http.get() with callback events
(data/end/error) and 20+ lines of JSON accumulation
- 4 used modern fetch() but without any timeout — upstream hangs would
pin the Express connection indefinitely
Every upstream request now goes through common/fetch-upstream.js, which
wraps fetch() with an 8s AbortController timeout. Handlers shrink from
~20 lines of request boilerplate to a 5-line try/catch:
try {
const apiRes = await fetchUpstream(url);
const json = await apiRes.json();
res.json(modifyJson(json));
} catch (e) {
res.status(500).json({ error: e.message });
}
Converted handlers:
- ipinfo-io / ipapi-com / ipapi-is / ip2location-io / ip-sb / mac-checker
(dropped `import { get } from 'https'/'http'`, rewrote the request)
- ipcheck-ing / invisibility-test / update-user-achievement / get-user-info
(already on fetch, swapped to fetchUpstream to gain the timeout)
- cf-radar (inline fetchFromCloudflare helper swapped to fetchUpstream)
Special case: google-map.js streams a binary JPEG to the client rather
than parsing JSON. Uses Readable.fromWeb(apiRes.body).pipe(res) to
bridge the fetch ReadableStream to the Express response.
Net -150 lines of handler code; upstream hangs now cap at 8s instead
of 30-120s; consistent error handling across all handlers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7 handlers that take ?ip (ipinfo, ipapicom, ipchecking, ipsb, ipapiis,
ip2location, maxmind) each opened with the same 8-line block:
const ipAddress = req.query.ip;
if (!ipAddress) return res.status(400).json({ error: 'No IP...' });
if (!isValidIP(ipAddress)) return res.status(400).json({ ... });
Replace by attaching requireValidIP() to the matching routes in
backend-server.js. The handler bodies now assume ?ip has been validated
upstream and just read it:
const ipAddress = req.query.ip;
Handlers that accept other param shapes (ASN, MAC, domain, user ID) or
no params are untouched — they retain whatever handler-specific
validation they need.
Tests updated:
- tests/api-handlers.test.js: drop the 2×3 = 6 "rejects missing/malformed
ip" cases for ip-sb / ipapi-com / ipinfo-io, the 2×2 = 4 for
ipapi-is / ip2location-io, and the 2 for ipcheck-ing. Each set was
testing what the middleware now owns; tests/guards.test.js covers it.
The "reports missing API key after IP passes validation" case for
ipcheck-ing stays — that exercises handler logic beyond the middleware.
- tests/api-smoke.test.js: drop the maxmind "rejects missing and invalid
IP parameters" case (moved to middleware).
Net -13 tests vs pre-commit; 195 still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every handler under api/ used to open with the same 4-line referer gate:
const referer = req.headers.referer;
if (!refererCheck(referer)) {
return res.status(403).json({...});
}
Replace that with `app.use('/api', requireReferer)` in backend-server.js
(from common/guards.js, landed in the previous commit). The middleware
fires before any handler runs, so by the time execution reaches a
handler, the referer is guaranteed to be on the allow-list. 16 handlers
each lose ~4 lines + one import.
api/configs.js is a special case — it still READS req.headers.referer
to classify whether the caller is the canonical ipcheck.ing deployment
("originalSite") or a self-hosted fork. No longer rejects, just reads.
Tests updated:
- tests/api-handlers.test.js: drop the 24-case "handlers reject missing
referer" parametrized suite. Its concern moved to middleware; covered
by tests/guards.test.js.
- tests/api-smoke.test.js: drop the 4 "rejects without referer" cases
for configs / google-map / maxmind / dns-resolver for the same
reason. Non-referer assertions (method gating, invalid params,
successful config read) stay.
Net -208 lines vs pre-commit; 208 tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>