A pass over every file the branch touched, after the iOS animation hunt
left tries and reverts behind it. Removed what nothing reaches: the
view-transition rule for a transition no code starts, an unreachable
guard in the transition hook, a no-op KeepAlive key branch, a sign-in
guard the menu already enforces, usage-dialog wrappers that only
forwarded to the store, a duplicated modifier-click check and card
filter (isPlainClick / listedTools), a test that hand-rolled
listedTools, a drawer-era fixture parameter. auth-flow.js folded into
auth-redirect.js. Comments cut to what the code cannot say itself —
file headers of a few lines, the measured iOS reasons, non-obvious
orderings — and stale ones fixed (Nav under KeepAlive, Home.vue
reusing DEFAULT_META, "from Nav", enabledCards). AGENTS.md sections
tightened to the code as it is now.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GbLTGMZ45MV78qd2tHoco
VITE_FIREBASE_AUTH_DOMAIN stays the shared sign-in domain every browser
uses; the new optional VITE_FIREBASE_PWA_AUTH_DOMAIN is the domain the
installed PWA initialises Firebase with — the site's own host, served
by the /__/auth proxy, so the redirect handler runs inside the PWA
(utils/auth-domain.js). GitHub's OAuth app has one callback, on the
shared domain, so when the PWA domain differs the account menu offers
Google only. .env.example documents both and the console-side steps.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GbLTGMZ45MV78qd2tHoco
The installed iOS PWA opens an out-of-scope authDomain in the system's
in-app browser view, whose storage the PWA's own webview cannot read,
so a redirect sign-in completed there never reached the app. The
frontend server and the Vite dev server now proxy /__/auth/* to
https://<project>.firebaseapp.com (common/firebase-auth-proxy.js),
mounted only when VITE_FIREBASE_PROJECT_ID is set, so with
VITE_FIREBASE_AUTH_DOMAIN set to the site's host the handler runs
in-scope, inside the PWA itself. .env.example documents the three keys
and the console-side requirements (authorized domain, OAuth callback at
https://<host>/__/auth/handler).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GbLTGMZ45MV78qd2tHoco
A new In-depth Checks tool shows which blocklists an IP is on: the
real-time DNSBLs and the threat intelligence lists the private API's
/iphitlist checks. Listings are split by kind, so a VPN or Tor exit
reads as an anonymity mark rather than a bad record; hits stay open with
their reasons and removal links, the remaining lists fold away, and a
legend explains every mark.
/api/ipblocklist proxies the upstream with the caller's headers and a
30s timeout (a cold DNSBL pass outlasts the default 8s), passing sign-in
and quota rejections through. Signed-in users only, original site only,
150 checks a month (ip_hitlist in the usage dialog and the pre-flight
quota gate). Shift+B opens it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
MAC Lookup no longer calls maclookup.app. The five public IEEE registries
(MA-L, MA-M, MA-S, IAB, CID) are a new `oui` dataset row — no key needed,
published as downloaded, refused below per-registry row floors — and
common/oui-db.js answers the longest assignment covering a query, the group
bit masked so a multicast address resolves to the block it derives from.
The country is read off the end of the registered address.
?mac= now takes a vendor prefix as well as a full address (6–12 hex digits,
colon / hyphen / dotted / bare), parsed by common/mac-input.js, which the
form shares. An address no block covers is still answered, with its flags.
The route is gated by requireOfflineData while the registries download at
boot. MAC_LOOKUP_API_KEY is gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A run that died or lost its lock between renames left every file present
(possibly a mixed MaxMind City / ASN pair), and the boot download skipped
the row because the files existed. The in-flux state now carries
`publishing: true`, cleared by the final record, and boot re-fetches a row
whose last publish never finished.
- The boot skip asked whether a snapshot file existed, so an empty or
corrupt one — which the reader refuses — kept the row unrepaired. Rows now
supply isLoaded(), the reader's own readiness (all four), and boot
downloads whenever the reader holds nothing usable; files under the
published names are re-fetched even when the state records the remote's
version (updateDataset's new `force`).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CauPFca6bYL9qpuYEmsU9H
The backend downloaded every missing dataset before listening, so a first
boot (or a newly added dataset) refused every request for a minute or more.
Snapshots already on disk now load before the listener opens and missing
ones download behind it. Meanwhile only routes reading still-missing data
answer 503 (never edge-cached): /api/maxmind, /api/asn-connectivity and
/api/service-status. An ASN Profile section whose data is still loading
answers 'error' on its own, and the connectivity graph client doesn't cache
a 503, so both recover once the data lands.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxMind and CAIDA each had their own updater with its own lock, state file,
publish step, boot download and timer. One engine (common/dataset-updater.js)
now owns all of that, and each dataset is a row in common/datasets.js that
holds only its own logic: find the newest remote version, fetch or distill
it, validate, reload.
- Locks: proper-lockfile, a heartbeat lock shared by every process, so a
crashed holder's lock goes stale on its own; a booting server waits for a
live holder and loads what it published.
- State: one file per dataset, written with write-file-atomic; the old
.maxmind-update-state.json / .caida-update-state.json are read once, so an
upgrade doesn't re-download.
- Schedule: croner at a fixed time (DATASET_UPDATE_CRON, default 04:30 local)
instead of 24 h from process start, plus a catch-up run at boot for a check
a downtime skipped. DATASET_AUTO_UPDATE (default on) replaces
MAXMIND_AUTO_UPDATE / CAIDA_AUTO_UPDATE, which still apply when it is unset.
- Reload: the engine watches every dataset's files, so a publish by another
process or a hand-placed .mmdb is picked up; MaxMind's own watcher goes.
MaxMind's City and ASN editions now publish together under one version.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The panel's IPv4/IPv6 prefix counts came from Radar's routes/stats, an
endpoint that fails often and, as one of the profile's summary segments,
kept complete ASN Profiles out of the edge cache. The ASN Profile already
shows prefix counts (from the pfx2as list) and relationships (the CAIDA
graph), so the panel drops both: no more routes/stats call, and the Radar
asn view no longer reads the local CAIDA snapshot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review findings on the ASN Profile work, mostly one class of bug: a failed
upstream read as "no data", which the 7-day edge cache would then pin.
- AS Rank: a 200 carrying GraphQL errors is a failure, not an unknown AS.
- Radar: an answer built from partly failed segments is still shown but
flagged `incomplete` and kept out of the cache, on the aggregate and on
the standalone asn view. A 404 is Radar's "no such AS" and counts as
empty, as does a segment that simply has no figures.
- Reputation: a malformed or non-200 answer is an error.
- Prefixes: a payload without `prefix_origins` is an error; [] stays valid.
- Radar is `empty` when only local CAIDA counts are present.
- requireValidASN enforces AS1–AS4294967295 and canonicalizes; the range
rule lives once in common/asn-input.js, shared with the frontend.
- The PeeringDB listing lookup goes through fetchUpstream.
- New functions follow the const-arrow convention; test stubs compare
parsed hostnames instead of URL substrings.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A third offline dataset joins the CAIDA updater: the daily PeeringDB dump,
distilled by a streaming pass into a compact per-ASN index (network type,
scope, peering policy, exchange and facility presence). Contact tables are
never parsed and the raw dump is deleted after distilling. It downloads
only when the Cloudflare key is configured, since the tool is hidden
without it.
The profile shows network type, scope and website in the header and a
"Peering & facilities" card whenever the AS lists an exchange or facility.
Aggregate deadlines now sit above the fetch timeouts they wrap, so a
source that degrades internally still delivers its partial answer.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
/api/asn-profile gathers every source for one AS in parallel and answers
once: Radar entity and traffic data, announced prefixes with country
shares, the CAIDA topology and neighbours, the RDAP registration record,
AS Rank, and (when the private API is configured) reputation. Each source
has its own deadline; a slow or failed one is simply absent, flagged in a
`status` map, and only complete answers are edge-cached.
- Upstream / peer / customer counts now come from the local CAIDA
snapshot with the same rule the topology graph applies, for both the
profile and the ASN Info panel; the graph builder itself is unchanged.
- The ASN data family (aggregate, Radar asn and bgp-prefixes views)
shares a 7-day edge cache.
- /api/asrank is folded into the aggregate.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- cfradar `view=asn` keeps the RPKI route counts from routes/stats.
- New cfradar `view=bgp-prefixes`: the AS's announced prefixes (pfx2as)
plus IPv4 country shares resolved against the local MaxMind database.
- New /api/asrank: CAIDA AS Rank lookup (rank, customer cone, degree).
Misses answer 200 with null fields and stay out of the edge cache via
the new `cacheIf` option on `cacheable`.
- /api/asn-connectivity adds `neighbours`: provider / peer / customer
counts and name lists from the local AS-relationship snapshot.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The Whois tool and /api/whois now take `AS<n>` (prefix in any case) next
to IPs and domains, answered from the RIR's RDAP autnum record. A bare
number stays invalid input, so it can never be mistaken for anything else.
The raw blocks no longer start with the template's own indentation.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A local pnpm dev with a DSN in .env reported every half-finished edit
against the production projects. The frontend gate now also checks
import.meta.env.DEV; the backend bootstrap skips init under
SENTRY_ENVIRONMENT=development, mirroring the cron check-in rule.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
/api/cfradar, /api/cfradar-traffic and /api/outages all proxied Cloudflare
Radar with their own route, guard wiring and cache TTL — every new Radar
dataset meant another copy of that boilerplate. All Radar data now rides
the single /api/cfradar route: ?view= dispatches over the RADAR_VIEWS
registry in common/cf-radar.js, where each view declares its guards,
edge-cache TTL and fetch function. cacheable() accepts a per-request TTL
resolver so 30-day profiles and the 1-hour outage feed share the route
without losing their cache behavior.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A weekday × hour heatmap of Cloudflare Radar HTTP traffic for the IP's
country, opened from a button beside the Country field. Country-level
rather than per-ASN because global ASNs have no single diurnal rhythm,
and ~250 cache keys let a 30-day edge cache absorb nearly every request.
Defaults to likely-human traffic with a switch for all traffic (the
other scope prefetches for instant flipping); hours render in the IP's
own timezone.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`?type=` was never validated. The UDP branch fell through to a throw and
returned N/A, but the DoH branch interpolated whatever arrived straight
into the upstream URL, which made the route a query proxy for record
types we never intended to support.
requireValidRecordType checks it against DNS_RECORD_TYPES and uppercases
it in place, the same way requireValidDomain lowercases its param — so
`?type=caa` now works as well.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
common/langs.js is gone. Its list conflated two unrelated things — the
UI's locales and the languages upstream data comes in. The MaxMind set
(verified against the shipped City database) now lives in
maxmind-service.js, where lookupMaxMind family-normalizes any raw tag
itself, so de/es/ja/pt-BR finally serve their real localized names and
zh-TW-class variants resolve into their family instead of English. The
dns-leak proxy forwards ?lang untouched — its upstream owns resolution —
retiring the odd zh-CN default for langless calls. Net effect: no ?lang
allow-list anywhere, and a new UI locale is a front-end-only change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Move the resolver list into api/data/dns-resolvers.js as structured
per-country data so adding a resolver becomes a one-object PR, gated
by a new data-integrity test. The API returns one flat annotated list
and the UI grows badge-style region filter chips (flags + localized
names via Intl.DisplayNames) above the result table.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
POST /api/persona/evaluate — thin proxy to the private evaluator, with
smoke tests and the AGENTS note on its header handling.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every geo route now returns an IANA zone name, added on the way out by the
withTimeZone() middleware rather than by any handler — including the private
API pass-through, which needs no new upstream field.
The zone is derived from the latitude/longitude the response itself carries,
not from a second lookup of the same IP. Asking another database would
eventually place the address elsewhere and print a zone that contradicts the
city next to it. Coordinate accuracy measured against MaxMind's own time_zone
over 30k sampled addresses: 99.80% exact, 99.98% correct offset; the rest are
neighbouring zones across a land border that share an offset anyway.
Only the name ships. Geo routes sit behind a 24h edge cache, so a cached UTC
offset would be an hour wrong for everyone between a DST switch and the
entry's expiry — the frontend renders the offset per view instead, reusing
the formatter the analytics path already had.
The field travels the full report path: event payload, builder, schema (as a
typed timeZone kind, not a free string), the /r/:id table, the Markdown
export and the JSON download.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The seven geo routes let a reserved address through to their upstream,
which could only answer "no data" — a visitor querying their own LAN IP
cost a real API call at every source. requireValidIP tightens into
requirePublicIP and rejects that address before the handler runs, so
geo-handler, maxmind and ipcheck-ing need no check of their own; the
guard was already their sole validation layer.
Malformed input and reserved space keep distinct messages — the two
failures point at different fixes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New /api/outages merges Cloudflare Radar verified outages and traffic
anomalies (30d window, anomaly-vs-outage dedupe, ongoing-first ordering,
1h edge cache); the Pulse Sheet renders them as a bulletin card list
with cause/level translated in all four locales.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bare .pipe() left upstream body errors unobserved: a reset after headers
could crash the process, and client disconnects kept the upstream fetch
running. Await stream/promises pipeline() so errors propagate both ways,
treat premature-close as a routine client abort, and fall back to the
500 JSON response only while headers are still unsent.
Closes#363
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New /api/globalping-probes proxies Globalping's probe list and reduces it
server-side to the country picker's shape (which countries have an online
probe, bucketed into five continents) — a ~1.4KB payload instead of the
multi-hundred-KB raw list on the visitor's connection. Coverage changes
slowly and the pickers fail open, so the route sits behind a week of edge
cache. Aggregation is a pure function in common/ with its own spec.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New /api/ooni-blocking backs the reworked Censorship Check: OONI matches
domains against the exact tested hostname, so the handler merges the apex
and www. variants (probe_cc x blocking_type) and classifies each country
into confirmed / likely / signs tiers calibrated on real 30-day data.
Window is server-computed and day-aligned so the 6h edge cache holds; 5xx
from OONI's occasionally flaky API gets one fast retry, timeouts do not.
Adds the requireValidDomain() guard (lowercases for one canonical cache key).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
POST /api/report validates an untrusted report against the shared schema
whitelist (free text cannot enter storage), caps it at 32KB and writes it
to Workers KV under a 128-bit unguessable id with a user-chosen TTL
(1/7/30 days); GET /api/report/:id serves the read-only report page and
AI agents, behind a short edge cache since reports are immutable. The
whole feature is env-gated on the three CLOUDFLARE_* variables and
surfaced to the frontend as configs.reportSharing. Rate limiting is
deliberately left to the deployment layer (edge rules / the env-gated
global /api limiter), not the handler.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sentry sections cover the gating philosophy (no DSN, nothing ships or
loads), the two frontend rules (never import Sentry in app code;
explicit signals ride the app-events bus) and the backend split
(warn+ to Logs, error+ to Issues, Crons via sentry-cron).
The slim-down removes what code already expresses: tutorial-style
snippets become one-line rules pointing at exemplar components, and
per-file directory trees drop to directory level (file headers are the
per-file source of truth). Also fixes a stale claim — the router has
been on HTML5 history mode, not hash mode. 38 KB → 19 KB total.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
shields.io's stars badge kept failing on GitHub token-pool exhaustion
("Unable to select the next GitHub token from pool"). Serve the count
from our own backend instead and render the badge ourselves.
- api/github-stars.js: fetch stargazers_count unauthenticated (no token),
edge-cached for a day in backend-server.js. Behind Cloudflare the origin
hits GitHub at most once per cache window, well under the 60 req/hour
unauthenticated limit; error responses stay no-store so failures aren't
cached.
- Nav.vue: replace the shields.io <img> with our own GitHub badge on
desktop and add the count to the mobile drawer's "Star on GitHub" row,
both fed by /api/github-stars (fetched on mount). Count is hidden until
it lands / on error, so the link never depends on the fetch.
- format-star-count.js: pure compact formatter (1234 -> 1.2k) with tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a "Service Status" section between Connectivity and WebRTC showing the
live availability of well-known products (Claude, OpenAI, Cursor, GitHub,
Discord, Cloudflare, Reddit, Notion) from their Statuspage / incident.io pages.
Backend
- common/service-status-providers.js: upstream list + id whitelist
- common/service-status-transform.js: pure summary/incidents normalizers
(first-level components only; incident URLs built from page + id)
- common/service-status-store.js: 5-min in-memory poller with last-known-good
so transient upstream blips don't degrade a healthy provider
- api/service-status.js: overview + per-provider components/incidents, served
from the in-memory snapshot (no upstream call per request)
- guards.js: requireValidProviderId; routes edge-cached for 5 min
Frontend
- ServiceStatus.vue: static provider list (cards always render), expandable
cards with Services / Recent Incidents tabs, skeleton placeholders, ri brand
icons, status-icon tones; incidents show title + date / severity (! count) /
lifecycle-colored status
- utils/service-status-tone.js: status-vocab -> tone/level helpers
- wired into sections.js, App.vue, refresh orchestrator; i18n in en/zh/fr/tr
Tests: transform/tone unit tests, guard + handler smokes; changelog entry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New /api/asn-connectivity returns a layered graph of upstream paths from
the origin AS toward Tier 1 ISPs (bgp.tools-style). Rendered via dagre +
SVG with Manhattan routing and an expand-into-dialog action.
Backend is fully offline. Two new local snapshots:
- common/as-org-db CAIDA as2org (ASN → org name)
- common/as-rel-db CAIDA as-rel2 (p2c relationships, Tier 1 set)
common/caida-updater handles bootstrap + periodic refresh for both
datasets in one vendor-scoped file, mirroring maxmind-updater. New
common/decompress unifies gzip / bzip2 stream decompression so future
formats are one switch case rather than a new dependency per updater.
Tier 1 membership is derived from CAIDA (no providers + customer cone
>= 100), replacing the hand-rolled list. Origins that are themselves
Tier 1 are flagged so the frontend can render a combined origin+Tier 1
box.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
/api/asn-history previously took ?ip=, producing a unique CF cache key
per IP — wasteful since BGP routes are announced at /24 (v4) and /48
(v6) at minimum, so every IP in the same prefix has identical routing
history. Frontend now quantizes the user's IP to its DFZ-floor prefix
via toBgpPrefix() and hits /api/asn-history?prefix=...; CF can dedupe
256 v4 addresses (or a /48's worth of v6) onto one edge cache entry.
Backend swaps requireValidIP() for a new requireValidPrefix() guard
that validates any well-formed CIDR. RIPEstat returns identical
results whether queried by IP or its covering prefix (verified
empirically), so the upstream call is unchanged in shape.
Shared via common/bgp-prefix.js with a frontend re-export at
frontend/utils/bgp-prefix.js, matching the valid-ip.js pattern. 50
new tests cover v4/v6 quantization (including various IPv6 shorthand
forms, mixed case, leading-zero normalization) and the prefix guard.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduce a cacheable(maxAge) middleware factory in backend-server.js
that hooks res.json to attach Cache-Control: public, max-age=N — but
only on 2xx responses, so Cloudflare's edge never caches 4xx/5xx.
Wire it on the routes whose upstreams refresh slowly: cfradar /
asn-history / whois / maxmind / macchecker plus the third-party IP
geolocation handlers (ipinfo / ipapicom / ipsb / ipapiis / ip2location).
Auth-context handlers (ipchecking, invisibility, dns-leak-test/session,
getuserinfo, updateuserachievement) remain on the global no-store
default — their caching belongs at the upstream that owns the auth.
Also trim stale "what" comments throughout backend-server.js and
document the new pattern in api/AGENTS.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <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>
Previously reloadMaxMindDatabases ran fire-and-forget at the top level
and only logged an error when the .mmdb files were missing — the server
would start but MaxMind API returned 503 forever until someone noticed.
Now backend-server.js runs an explicit bootBackend() that:
1. bootstrapMaxMindIfMissing — if files are absent, check for
MAXMIND_ACCOUNT_ID / MAXMIND_LICENSE_KEY and either print a clear
"how to fix" warning or run a synchronous download cycle capped at
5 minutes (AbortController threaded through fetch + pipeline so the
abort actually stops in-flight I/O).
2. reloadMaxMindDatabases — load readers.
3. startMaxMindFileWatcher + startMaxMindAutoUpdate — unchanged.
4. app.listen — only after the steps above.
The 5-minute cap is a total timeout only; we deliberately skipped the
"no-bytes-for-N-seconds stall detector" to keep the path simple. On any
failure (bad credentials, blocked outbound, timeout) the server still
starts — just with MaxMind returning 503 and a clear operator warning.
MAXMIND_AUTO_UPDATE is intentionally NOT consulted by bootstrap; that
flag only gates the periodic scheduler. Credentials alone are enough
signal that the operator wants MaxMind working.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New advanced tool at /enhanceddnsleaktest: generates a 32-hex session
token client-side, fires four <img> DNS probes in parallel, then pulls
the captured recursive-resolver rows (IP / ASN / geo / ECS / DNSSEC
DO+CD) from the main IPCheck.ing API. Firebase sign-in + configs.originalSite
gated, matching the InvisibilityTest pattern.
- api/dns-leak-test.js — thin proxy to
GET {IPCHECKING_API_ENDPOINT}/dnsleaktest/session/:token, forwarding
Authorization and attaching the apikey query param; upstream status
and body passed through verbatim so the frontend can surface
"Sign in required" / "Invalid token".
- EnhancedDnsLeakTest.vue — full client flow: parallel probe firing
with sequential UI animation, single-region flow card (status dot +
stage text + percent + shadcn Progress), sortable / dedupable result
Table with ECS-first, country-grouped, numeric-IP ordering, DNSSEC
DO/CD rendered as green check / amber cross, field-reference legend.
- Homepage DNS Leak Test gains a fade-slide banner that surfaces the
deeper tool once the basic test settles (sticky, won't retract on
re-run) and a new D keyboard shortcut.
- Shadcn Table primitive copied in (via shadcn-vue add table).
- i18n: full 4-locale coverage for the enhanced tool, the banner, the
advancedtools card blurb, and the D shortcut description.
- Tests: dns-leak-test smoke coverage added; configs test expectation
aligned with restored handler.
- AGENTS.md: sync api/ and frontend/ docs with the new handler, new
advanced tool, new Table primitive, and the extra private-API
header-passthrough consumer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
whoiser's port-43 WHOIS path returns empty for newer gTLDs (.ing /
.app / .dev / …) that expose RDAP only. Keep whoiser as the primary
path — it still returns richer multi-provider text for legacy gTLDs
like .com / .us — and fall back to a minimal in-house RDAP client
(common/rdap.js) when whoiser produces no __raw for any provider.
The fallback:
- loads IANA's TLD → RDAP endpoint map (data.iana.org/rdap/dns.json)
with a 24h in-memory cache
- issues one RDAP GET via fetchUpstream (5s cap)
- formats the RDAP JSON into a WHOIS-like text block (status, events,
entities-by-role with vCard name / email / address, nameservers,
DNSSEC)
- returns the same { [host]: { __raw, ... } } shape as whoiser.domain()
so the frontend Accordion renders it with zero changes
IP queries stay on whoiser only — regional IP WHOIS servers are
widely reachable and RDAP adds no coverage there.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CLAUDE.md is now a 10-line pointer to AGENTS.md plus a Claude-specific
extension slot; everything contributor-facing lives in AGENTS.md.
Root AGENTS.md is the index — Overview, Stack, Commands, Project layout,
universal Conventions (language / comments / i18n), Testing, a short
Security & Boundaries summary, and Workflow. It references
@frontend/AGENTS.md and @api/AGENTS.md for area-specific details.
frontend/AGENTS.md collects the UI system (primitives, design tokens,
status tones, canonical patterns) plus the shadcn-vue-first rule and
frontend-specific project layout / conventions. This is what a UI
contributor needs — API rules never get in the way.
api/AGENTS.md covers the backend handler shape (fetchUpstream, error
shape, response normalization), the middleware model (requireReferer
globally, requireValidIP per-route, handlers must not repeat those
checks), the private-API header pass-through exception, and the smoke
test layout.
New rules captured across the three files:
- Every new file opens with a header comment stating its purpose
- Large templates / functions carry block comments on each meaningful section
- Big features and modifications to tested features update tests in the
same change; `npm run check` must be green before handing off
- One concern per commit
- Visual changes get an explicit "please test in npm run dev" handoff
- User testing must pass before commit
- shadcn-vue-first for new UI components (local primitives → shadcn-vue
library → hand-rolled Tailwind as last resort)
- Every commit scans AGENTS.md for staleness and updates it in the same
change if the rules shifted
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>