diff --git a/.gitignore b/.gitignore index 228e446e..3c6db2ed 100644 --- a/.gitignore +++ b/.gitignore @@ -33,7 +33,7 @@ localtest .VSCodeCounter -# Offline datasets (common/datasets.js): every common/-db/ directory +# Offline datasets (server/datasets/datasets.js): every common/-db/ directory # holds only downloaded data, its state and lock — created at runtime, never # committed (CAIDA's data is under a non-commercial license). common/*-db/ diff --git a/AGENTS.md b/AGENTS.md index 42dea25b..ec867db5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,7 +33,7 @@ Express 5 back-end API. | Build | Vite + `@vitejs/plugin-vue`; Tailwind CSS v4 + `tw-animate-css` | | UI | shadcn-vue copy-in primitives (reka-ui) · lucide icons · circle-flags via `@iconify/vue` · vaul-vue drawer · vue-sonner toast | | Backend | Express 5 | -| Logger | `pino` singleton at `common/logger.js` (+ `pino-http`, opt-in) | +| Logger | `pino` singleton at `server/logger.js` (+ `pino-http`, opt-in) | | Auth | Firebase Auth (optional, env-gated) | | Error monitoring | Sentry — optional & env-gated on both halves: `@sentry/vue` (no `VITE_SENTRY_DSN_FRONTEND`, no Sentry in the build — see frontend/AGENTS.md) + `@sentry/node` (no `SENTRY_DSN_BACKEND`, never loaded — see api/AGENTS.md) | | PWA | `manifest.webmanifest` only — installable but online-only, no service worker | @@ -62,8 +62,11 @@ use npm / yarn — they'd produce a competing lockfile. ├── AGENTS.md / CLAUDE.md ← this file + Claude pointer to it ├── frontend/ ← Vue 3 SPA (see frontend/AGENTS.md) ├── api/ ← Express handlers (see api/AGENTS.md) -├── common/ ← code shared by both halves (valid-ip / -│ fetch-with-timeout / guards / logger / …) +├── server/ ← backend-only code (logger / guards / …; +│ datasets/ = the offline dataset engine) +├── common/ ← code the frontend or build imports too +│ (valid-ip / fetch-with-timeout / …) + the +│ dataset dirs (`*-db/`, a self-host mount point) ├── tests/ ← Node test runner specs ├── backend-server.js ← Express app (default port 11966) ├── sentry-instrument.js ← backend Sentry bootstrap via `node --import`; @@ -112,7 +115,7 @@ use npm / yarn — they'd produce a competing lockfile. ### Logging (backend) -- **Always the shared logger** (`common/logger.js`) in backend files; bare +- **Always the shared logger** (`server/logger.js`) in backend files; bare `console.*` is banned there (frontend keeps using `console.*`). - Pino first-arg-is-context: `logger.error({ err, ip }, 'short message')`. - Env knobs: `LOG_LEVEL` (default info), `LOG_FORMAT=json` for shippers, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b65cf2f4..ad117e0c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,7 +36,8 @@ from building something that can't be merged. ``` frontend/ Vue 3 SPA (Pinia, vue-router, vue-i18n, Tailwind v4 + shadcn-vue) api/ Express 5 handlers, one file per route (wired in backend-server.js) -common/ Code shared by both halves (validators, fetch helper, logger, …) +server/ Backend-only code (logger, guards, upstream clients, dataset engine, …) +common/ Code both halves import (validators, fetch helper, report schema, …) tests/ Node test runner specs (node --test) ``` @@ -60,8 +61,8 @@ Details and rationale live in the AGENTS.md files; the headlines: - **The `full` locales land together** — user-visible copy ships in `en` / `zh` / `zh-TW` / `fr` / `ru` in the same PR (`frontend/locales/`; tests enforce it). `beta` locales are exempt — they fall back to English. -- **Backend logging goes through the shared pino logger** (`common/logger.js`) — - no `console.*` in `api/` or `common/`. +- **Backend logging goes through the shared pino logger** (`server/logger.js`) — + no `console.*` in `api/`, `server/` or `common/`. ## Good places to start diff --git a/Dockerfile b/Dockerfile index 64aa8358..e62d683a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,6 +29,7 @@ COPY --from=build-stage /app/frontend-server.js ./ # npm start boots the backend with `--import ./sentry-instrument.js` COPY --from=build-stage /app/sentry-instrument.js ./ COPY --from=build-stage /app/api ./api +COPY --from=build-stage /app/server ./server COPY --from=build-stage /app/common ./common EXPOSE 18966 diff --git a/SECURITY.md b/SECURITY.md index 6cb9aabb..f13b575e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -18,8 +18,8 @@ Helpful things to include: - What an attacker can do with it, and what they'd need to start (a session? just a URL?). - Steps to reproduce — a request, a payload, or a short script. -- Where the problem lives: front-end (`frontend/`), API handler (`api/`), or shared - code (`common/`). +- Where the problem lives: front-end (`frontend/`), API handler (`api/`), backend + code (`server/`), or shared code (`common/`). - Whether you hit it on [ipcheck.ing](https://ipcheck.ing) or your own deployment, and the version if self-hosted. diff --git a/api/AGENTS.md b/api/AGENTS.md index 3334a1c7..c15c5e28 100644 --- a/api/AGENTS.md +++ b/api/AGENTS.md @@ -1,15 +1,21 @@ # api/AGENTS.md -Conventions for Express 5 handlers under `api/` and shared back-end code -under `common/`. Universal rules live in ../AGENTS.md. +Conventions for Express 5 handlers under `api/` and the back-end code under +`server/` and `common/`. Universal rules live in ../AGENTS.md. ## Overview The Express app lives in `backend-server.js` at the repo root — every route -is wired there and delegated to one handler module under `api/`. `common/` -holds shared back-end code (guards, logger, fetch helper, MaxMind / CAIDA -services, service-status poller), parts of which the frontend also imports -(`valid-ip.js`, `fetch-with-timeout.js`). +is wired there and delegated to one handler module under `api/`. Code the +handlers share is split by who imports it: + +- **`server/`** — backend-only (guards, logger, upstream clients, service-status + poller, …), flat; `server/datasets/` is the offline dataset engine and its + readers (see "Offline datasets"). +- **`common/`** — only what the frontend or the build imports too + (`valid-ip.js`, `fetch-with-timeout.js`, `report-schema.js`, …); it never + imports from `server/`. It also holds the dataset directories (`*-db/`), + which self-hosting docs and Docker volumes mount — they stay put. Roughly one handler file per route: IP-geolocation sources (`ipinfo-io` / `ipapi-com` / `ipapi-is` / `ip2location-io` / `ip-sb` / `ipcheck-ing` / @@ -23,7 +29,7 @@ and purpose — read those for specifics. The exception to one-file-per-route is Cloudflare Radar: all Radar data rides `/api/cfradar`, dispatched by `?view=` over `RADAR_VIEWS` in -`common/cf-radar.js`, where each view declares its guards, edge-cache TTL and +`server/cf-radar.js`, where each view declares its guards, edge-cache TTL and fetch function — new Radar data is a view function plus a registry row, never a new route. `api/data/` holds contributor-editable static config consumed by handlers — currently `dns-resolvers.js`, the country-annotated resolver list @@ -31,20 +37,20 @@ behind `dns-resolver` (gated by `tests/dns-resolvers-data.test.js`). ### ASN data -- **`/api/asn-profile`** (`common/asn-profile.js`) runs each ASN source's core +- **`/api/asn-profile`** (`server/asn-profile.js`) runs each ASN source's core under a deadline above the upstream timeouts it wraps (`INNER_TIMEOUTS`); a failed or late one is just absent. Always 200 `{ asn, status, incomplete, …sections }` (`ok` / `empty` / `error` / `disabled`; an upstream fault is `error`, never `empty`; `incomplete` = `ok` but partial); 502 if all fail. - **Relationship counts follow the graph:** `asn-connectivity` neighbour - lists come from local CAIDA (`common/as-relationships.js`), dropping the + lists come from local CAIDA (`server/as-relationships.js`), dropping the providers the graph walk distrusts. The Radar `asn` view (ASN Info) carries no prefix or relationship counts — the ASN Profile has both. - **`/api/whois` `?q=`:** IPs (RDAP, whoiser fallback), domains (whoiser, RDAP fallback), `AS` (RDAP autnum); all carry a `__raw` block. - **Offline datasets** (MaxMind, CAIDA, PeeringDB, IEEE MAC registries) are rows in - `common/datasets.js`, run by one engine, - `common/dataset-updater.js` (lock, atomic state, publish, boot download, + `server/datasets/datasets.js`, run by one engine, + `server/datasets/dataset-updater.js` (lock, atomic state, publish, boot download, daily schedule). A row holds only its own logic — find the newest remote version, fetch / distill it, validate, reload; a new dataset is a new row, never a new updater. Fetched at boot when its reader has nothing usable @@ -56,9 +62,9 @@ behind `dns-resolver` (gated by `tests/dns-resolvers-data.test.js`). reloaded by the engine's watcher. MaxMind only with its credentials (City + ASN publish together); PeeringDB only with a Cloudflare key; its 116 MB dump is stream-distilled (never parsed whole) to a ~6 MB per-ASN index without - contact data (`common/peeringdb-distill.js`). The IEEE row (MAC Lookup) + contact data (`server/datasets/peeringdb-distill.js`). The IEEE row (MAC Lookup) needs no key: five registry CSVs published as downloaded, answered by the - longest covering assignment (`common/oui-db.js`). + longest covering assignment (`server/datasets/oui-db.js`). ## Conventions @@ -67,7 +73,7 @@ behind `dns-resolver` (gated by `tests/dns-resolvers-data.test.js`). - **Every upstream call uses `fetchUpstream`** (`common/fetch-with-timeout.js`, 8s timeout), never a bare `fetch()` / `https.get()` — a hanging provider must time out. It injects a default `User-Agent` `MyIP/v/` - (`common/upstream-ua.js`; some WAFs block undici's `node` UA); caller-supplied + (`server/upstream-ua.js`; some WAFs block undici's `node` UA); caller-supplied ones, incl. the private-API `{ ...req.headers }` pass-through, win. - **Error shape.** `res.status(500).json({ error: error.message })` on upstream failure, `400` on bad input. Terse — the frontend doesn't show them verbatim. @@ -76,7 +82,7 @@ behind `dns-resolver` (gated by `tests/dns-resolvers-data.test.js`). sources match it. `timezone` is the exception (see "Response enrichment"). - **`?lang` is never validated in a handler.** Pass the raw tag through: `lookupMaxMind` normalizes it onto the languages its data carries - (`SUPPORTED_LANGS` in `common/maxmind-service.js`); the private-API proxies + (`SUPPORTED_LANGS` in `server/datasets/maxmind-service.js`); the private-API proxies forward it for the upstream's own resolution. No allow-lists. - **Logging.** Shared logger only, `logger.error({ err, ...ctx }, 'msg')`; no `console.*`, no "received request" lines (`pino-http` covers those). @@ -87,14 +93,14 @@ behind `dns-resolver` (gated by `tests/dns-resolvers-data.test.js`). loaded; `SENTRY_ENVIRONMENT=development` skips the init. Handlers never import Sentry: throws and 5xx traces are automatic; caught failures stay on the logger (its hook mirrors warn+ to Sentry Logs, error+ to Issues). Periodic - jobs wrap their tick in `common/sentry-cron.js`; `key` / `token` query params - are redacted (`common/sentry-scrub.js`). + jobs wrap their tick in `server/sentry-cron.js`; `key` / `token` query params + are redacted (`server/sentry-scrub.js`). ## Security & Boundaries ### Guards live in middleware, not handlers -`common/guards.js`, attached in `backend-server.js`; handlers never repeat them: +`server/guards.js`, attached in `backend-server.js`; handlers never repeat them: - `requireReferer` — global on `/api/*` (ALLOWED_DOMAINS + localhost). - `requirePublicIP()` — per-route `?ip=`: well-formed and publicly routable; @@ -120,7 +126,7 @@ New param shape → new guard there, attached in `backend-server.js`. ### Response enrichment lives in middleware too -`withTimeZone()` (`common/ip-timezone.js`), on all seven geo routes, derives +`withTimeZone()` (`server/ip-timezone.js`), on all seven geo routes, derives `timezone` (IANA name) from the `latitude` / `longitude` the handler returned (2xx only, the `cacheable` hook); no handler computes or forwards one. Own coordinates keep the zone consistent with the city beside it; only the name @@ -132,7 +138,7 @@ DST switch. A new geo source adds the middleware. The backend listens before any missing offline dataset or the service-status snapshot is fetched; snapshots already on disk load first, so only a first boot waits on downloads. During that window `requireOfflineData([...probes])` -(`common/offline-data.js`) answers 503 on a route whose data is still missing +(`server/offline-data.js`) answers 503 on a route whose data is still missing — never edge-cached — and is a no-op after boot, when a missing dataset means a failed download served degraded. A route reading local data declares its probes (`isMaxMindReady`, `isAsRelLoaded`, …) in `backend-server.js`. Answers @@ -144,7 +150,7 @@ loading (`isStillLoading`) answers `error`, leaving the others to answer. Handlers proxying our private IPCheck.ing API (`ipcheck-ing`, `invisibility-test`, `update-user-achievement`, `get-user-info`, `dns-leak-test`, `persona`, and `asn-profile`'s reputation via -`common/asn-reputation.js`) forward the caller's headers upstream (`headers: { +`server/asn-reputation.js`) forward the caller's headers upstream (`headers: { ...req.headers }`) — the upstream needs caller context (Accept-Language, auth tokens). `persona` strips the framing headers (`host` / `content-length` / …) first, as it re-serializes the body. Do **not** replicate for third-party @@ -153,7 +159,7 @@ upstreams; those get only what's explicitly needed. ## Edge caching Every `/api/*` response defaults to `Cache-Control: no-store`; slowly-changing -public routes opt in via `cacheable(maxAge)` (`common/cache-control.js`) in +public routes opt in via `cacheable(maxAge)` (`server/cache-control.js`) in `backend-server.js`, e.g. `cacheable(24 * 60 * 60)` (TTLs written that way, not raw seconds). `maxAge` may be a `(req) => seconds` resolver (cfradar: its view's TTL; falsy = no-store). `{ cacheIf }` vetoes the full TTL for a 2xx body, diff --git a/api/asn-connectivity.js b/api/asn-connectivity.js index 5affe933..995cfb9d 100644 --- a/api/asn-connectivity.js +++ b/api/asn-connectivity.js @@ -8,21 +8,21 @@ // backbone connectivity is settlement-free peering with the Tier 1 clique, // invisible to a p2c-only walk. // -// Data is fully local (common/as-rel-db.js + common/as-org-db.js), so the +// Data is fully local (server/datasets/as-rel-db.js + server/datasets/as-org-db.js), so the // whole BFS is synchronous. We only hit RIPEstat for as-overview as a // rare fallback when as2org doesn't have an ASN's org name. // // Alongside the graph, `neighbours` lists the origin's direct providers, // peers and customers from the same snapshot, counted by the walk's own -// misinference rule (common/as-relationships.js) so the counts never show an +// misinference rule (server/as-relationships.js) so the counts never show an // upstream the graph drops. List names come from as2org, gaps filled from the // graph's resolved names. -import { resolveAsnOrgName } from '../common/ripestat.js'; -import { providersOf, peersOf, customersOf, customerCountOf, isTier1 } from '../common/as-rel-db.js'; -import { lookupAsOrgName } from '../common/as-org-db.js'; -import { TIER1_PEERING_TRUSTED, TIER1_ADJACENCY_TRUSTED, countedRelationships } from '../common/as-relationships.js'; -import logger from '../common/logger.js'; +import { resolveAsnOrgName } from '../server/ripestat.js'; +import { providersOf, peersOf, customersOf, customerCountOf, isTier1 } from '../server/datasets/as-rel-db.js'; +import { lookupAsOrgName } from '../server/datasets/as-org-db.js'; +import { TIER1_PEERING_TRUSTED, TIER1_ADJACENCY_TRUSTED, countedRelationships } from '../server/as-relationships.js'; +import logger from '../server/logger.js'; // How deep to recurse from the origin. 3 covers regional networks reaching // Tier 1s through 1-2 intermediates; deeper just adds noise at the periphery. @@ -33,10 +33,10 @@ const MAX_DEPTH = 3; const MAX_INTERMEDIATE_BRANCH = 3; // Past either bar (TIER1_PEERING_TRUSTED / TIER1_ADJACENCY_TRUSTED, defined -// in common/as-relationships.js so the neighbour counts use the same ones), a +// in server/as-relationships.js so the neighbour counts use the same ones), a // node's non-Tier-1 "providers" are treated as CAIDA misinference and dropped. -// Two-tier org name resolver lives in common/ripestat.js. No onError hook +// Two-tier org name resolver lives in server/ripestat.js. No onError hook // here — connectivity stays silent on as-overview fallback failures (a node // just keeps name=null); asn-history is the one that warns. const resolveOrgName = (asn) => resolveAsnOrgName(asn); diff --git a/api/asn-history.js b/api/asn-history.js index c3ab8a16..c081e34e 100644 --- a/api/asn-history.js +++ b/api/asn-history.js @@ -13,8 +13,8 @@ import { fetchRoutingHistory, resolveAsnOrgName, -} from '../common/ripestat.js'; -import logger from '../common/logger.js'; +} from '../server/ripestat.js'; +import logger from '../server/logger.js'; const prefixLength = (prefix) => parseInt((prefix || '').split('/')[1], 10); @@ -56,7 +56,7 @@ function summarizeOrigin(entry, minLen) { }; } -// Two-tier resolver lives in common/ripestat.js. Here we pass a warn hook so +// Two-tier resolver lives in server/ripestat.js. Here we pass a warn hook so // a failed as-overview fallback stays observable (asn-connectivity omits it // and stays silent — keep that difference). const resolveOrgName = (asn) => diff --git a/api/asn-profile.js b/api/asn-profile.js index 59d677d9..84617860 100644 --- a/api/asn-profile.js +++ b/api/asn-profile.js @@ -1,23 +1,23 @@ // /api/asn-profile?asn= — everything the ASN Profile page shows, in one -// answer: Radar ASN summary + prefix list (common/cf-radar.js), CAIDA +// answer: Radar ASN summary + prefix list (server/cf-radar.js), CAIDA // topology and neighbours (./asn-connectivity.js), RDAP autnum -// (common/rdap.js), ASRank (common/asrank.js), the local PeeringDB index -// (common/peeringdb-db.js) and, when the private API is configured, -// reputation (common/asn-reputation.js). Composition, deadlines and statuses -// live in common/asn-profile.js. `asn` arrives as a canonical number string +// (server/rdap.js), ASRank (server/asrank.js), the local PeeringDB index +// (server/datasets/peeringdb-db.js) and, when the private API is configured, +// reputation (server/asn-reputation.js). Composition, deadlines and statuses +// live in server/asn-profile.js. `asn` arrives as a canonical number string // (requireValidASN). 200 unless every configured source failed (502). -import { RADAR_VIEWS, hasRadarApiKey, loadAsnSummary, isCompleteRadarAnswer } from '../common/cf-radar.js'; -import { rdapAutnum, isAutnumMissing } from '../common/rdap.js'; -import { queryAsRank } from '../common/asrank.js'; -import { requestAsnReputation } from '../common/asn-reputation.js'; -import { isPeeringdbLoaded, lookupPeeringdb } from '../common/peeringdb-db.js'; -import { isAsRelLoaded } from '../common/as-rel-db.js'; -import { isAsOrgLoaded } from '../common/as-org-db.js'; -import { isStillLoading } from '../common/offline-data.js'; +import { RADAR_VIEWS, hasRadarApiKey, loadAsnSummary, isCompleteRadarAnswer } from '../server/cf-radar.js'; +import { rdapAutnum, isAutnumMissing } from '../server/rdap.js'; +import { queryAsRank } from '../server/asrank.js'; +import { requestAsnReputation } from '../server/asn-reputation.js'; +import { isPeeringdbLoaded, lookupPeeringdb } from '../server/datasets/peeringdb-db.js'; +import { isAsRelLoaded } from '../server/datasets/as-rel-db.js'; +import { isAsOrgLoaded } from '../server/datasets/as-org-db.js'; +import { isStillLoading } from '../server/offline-data.js'; import { SOURCE_TIMEOUTS, buildSectionLoaders, composeAsnProfile, allSourcesFailed, -} from '../common/asn-profile.js'; +} from '../server/asn-profile.js'; import { getAsnConnectivity } from './asn-connectivity.js'; // A section whose local data isn't there fails (status 'error'): left out diff --git a/api/cf-radar.js b/api/cf-radar.js index bcb9eae6..c66f4aca 100644 --- a/api/cf-radar.js +++ b/api/cf-radar.js @@ -1,12 +1,12 @@ // /api/cfradar — single dispatch route for all Cloudflare Radar data. -// `?view=` selects an entry in RADAR_VIEWS (common/cf-radar.js): the view's +// `?view=` selects an entry in RADAR_VIEWS (server/cf-radar.js): the view's // guards validate and normalize its params, its fetch builds the payload, // and the route's cache middleware in backend-server.js reads the view's // per-view TTL. Guards run before the API-key check so param errors stay // 400s on keyless deployments (and tests never reach an upstream call). -import { RADAR_VIEWS, hasRadarApiKey } from '../common/cf-radar.js'; -import logger from '../common/logger.js'; +import { RADAR_VIEWS, hasRadarApiKey } from '../server/cf-radar.js'; +import logger from '../server/logger.js'; // Run a view's guard middlewares outside an Express chain. Each guard either // calls next() or writes its own 4xx response — so "didn't pass" means the diff --git a/api/dns-leak-test.js b/api/dns-leak-test.js index c42df789..90606577 100644 --- a/api/dns-leak-test.js +++ b/api/dns-leak-test.js @@ -9,7 +9,7 @@ // GET /api/dnsleaktest/session/:token import { fetchUpstream } from '../common/fetch-with-timeout.js'; -import logger from '../common/logger.js'; +import logger from '../server/logger.js'; const TOKEN_RE = /^[0-9a-f]{32}$/; diff --git a/api/dns-resolver.js b/api/dns-resolver.js index 3eadb93b..3baf0b54 100644 --- a/api/dns-resolver.js +++ b/api/dns-resolver.js @@ -3,7 +3,7 @@ // one flat, country-annotated result list the frontend groups by country. import { Resolver } from 'node:dns/promises'; import { fetchUpstream } from '../common/fetch-with-timeout.js'; -import logger from '../common/logger.js'; +import logger from '../server/logger.js'; import { DNS_RESOLVERS } from './data/dns-resolvers.js'; import { NAME_VALUED_TYPES } from '../common/dns-record-types.js'; diff --git a/api/get-user-info.js b/api/get-user-info.js index e158cd08..0977ba87 100644 --- a/api/get-user-info.js +++ b/api/get-user-info.js @@ -1,5 +1,5 @@ import { fetchUpstream } from '../common/fetch-with-timeout.js'; -import logger from '../common/logger.js'; +import logger from '../server/logger.js'; export default async (req, res) => { const key = process.env.IPCHECKING_API_KEY; diff --git a/api/get-whois.js b/api/get-whois.js index a53312f4..2c5f2ae9 100644 --- a/api/get-whois.js +++ b/api/get-whois.js @@ -4,8 +4,8 @@ import whoiser from 'whoiser'; import { isValidIP, isUsablePublicIP } from '../common/valid-ip.js'; -import { rdapDomain, rdapIp, rdapAutnum, isAutnumMissing } from '../common/rdap.js'; -import logger from '../common/logger.js'; +import { rdapDomain, rdapIp, rdapAutnum, isAutnumMissing } from '../server/rdap.js'; +import logger from '../server/logger.js'; function isValidDomain(domain) { const domainPattern = /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/i; diff --git a/api/github-stars.js b/api/github-stars.js index 4a2e5aca..6100534d 100644 --- a/api/github-stars.js +++ b/api/github-stars.js @@ -4,7 +4,7 @@ // Cloudflare the origin hits GitHub at most once per cache window — well under // the 60 req/hour unauthenticated limit. import { fetchUpstream } from '../common/fetch-with-timeout.js'; -import logger from '../common/logger.js'; +import logger from '../server/logger.js'; // The project's own repository. GitHub requires a User-Agent on every // request; fetchUpstream's default project UA satisfies that. diff --git a/api/globalping-probes.js b/api/globalping-probes.js index 5968bb25..f001d7de 100644 --- a/api/globalping-probes.js +++ b/api/globalping-probes.js @@ -5,8 +5,8 @@ // bytes, probe-country coverage changes slowly. import { fetchUpstream } from '../common/fetch-with-timeout.js'; -import { buildProbeInventory } from '../common/globalping-inventory.js'; -import logger from '../common/logger.js'; +import { buildProbeInventory } from '../server/globalping-inventory.js'; +import logger from '../server/logger.js'; const GLOBALPING_PROBES_URL = 'https://api.globalping.io/v1/probes'; diff --git a/api/google-map.js b/api/google-map.js index fd458aad..5fcfd03d 100644 --- a/api/google-map.js +++ b/api/google-map.js @@ -1,7 +1,7 @@ import { Readable } from 'node:stream'; import { pipeline } from 'node:stream/promises'; import { fetchUpstream } from '../common/fetch-with-timeout.js'; -import logger from '../common/logger.js'; +import logger from '../server/logger.js'; // Validate request legitimacy function isValidRequest(req) { diff --git a/api/invisibility-test.js b/api/invisibility-test.js index 08b74026..f91c1d3a 100644 --- a/api/invisibility-test.js +++ b/api/invisibility-test.js @@ -1,5 +1,5 @@ import { fetchUpstream } from '../common/fetch-with-timeout.js'; -import logger from '../common/logger.js'; +import logger from '../server/logger.js'; // If length is not 28 and is not a combination of letters and numbers, return false function isValidUserID(userID) { diff --git a/api/ip-sb.js b/api/ip-sb.js index 2cfeb994..7cc44ca0 100644 --- a/api/ip-sb.js +++ b/api/ip-sb.js @@ -2,7 +2,7 @@ // Token-free upstream; normalizes the response into the canonical geo // shape via the shared makeGeoHandler factory. -import { makeGeoHandler } from '../common/geo-handler.js'; +import { makeGeoHandler } from '../server/geo-handler.js'; function buildUrl(req) { const ipAddress = req.query.ip; diff --git a/api/ip2location-io.js b/api/ip2location-io.js index 36ff5417..e2b25ad6 100644 --- a/api/ip2location-io.js +++ b/api/ip2location-io.js @@ -2,7 +2,7 @@ // Picks a random API key and normalizes the response into the canonical // geo shape via the shared makeGeoHandler factory. -import { makeGeoHandler } from '../common/geo-handler.js'; +import { makeGeoHandler } from '../server/geo-handler.js'; function buildUrl(req) { const ipAddress = req.query.ip; diff --git a/api/ipapi-com.js b/api/ipapi-com.js index 2760b2a6..b6689246 100644 --- a/api/ipapi-com.js +++ b/api/ipapi-com.js @@ -2,7 +2,7 @@ // Forwards the optional ?lang and normalizes the response into the // canonical geo shape via the shared makeGeoHandler factory. -import { makeGeoHandler } from '../common/geo-handler.js'; +import { makeGeoHandler } from '../server/geo-handler.js'; function buildUrl(req) { const ipAddress = req.query.ip; diff --git a/api/ipapi-is.js b/api/ipapi-is.js index 4defa0c4..a9d81947 100644 --- a/api/ipapi-is.js +++ b/api/ipapi-is.js @@ -2,7 +2,7 @@ // Picks a random API key and normalizes the response into the canonical // geo shape (plus isHosting / isProxy) via the shared makeGeoHandler factory. -import { makeGeoHandler } from '../common/geo-handler.js'; +import { makeGeoHandler } from '../server/geo-handler.js'; function buildUrl(req) { const ipAddress = req.query.ip; diff --git a/api/ipcheck-ing.js b/api/ipcheck-ing.js index 152b3624..03a361fd 100644 --- a/api/ipcheck-ing.js +++ b/api/ipcheck-ing.js @@ -1,5 +1,5 @@ import { fetchUpstream } from '../common/fetch-with-timeout.js'; -import logger from '../common/logger.js'; +import logger from '../server/logger.js'; export default async (req, res) => { // Presence, validity and public routability guaranteed by the diff --git a/api/ipinfo-io.js b/api/ipinfo-io.js index 4e25bb63..d044f079 100644 --- a/api/ipinfo-io.js +++ b/api/ipinfo-io.js @@ -3,7 +3,7 @@ // into the canonical geo shape via the shared makeGeoHandler factory. import getCountryName from '../common/country-name.js'; -import { makeGeoHandler } from '../common/geo-handler.js'; +import { makeGeoHandler } from '../server/geo-handler.js'; function buildUrl(req) { const ipAddress = req.query.ip; diff --git a/api/mac-checker.js b/api/mac-checker.js index 057e11ca..1f9e84aa 100644 --- a/api/mac-checker.js +++ b/api/mac-checker.js @@ -1,11 +1,11 @@ // /api/macchecker?mac= — MAC Lookup against the local IEEE registries -// (common/oui-db.js). `mac` is a full address or a vendor prefix of 6–12 +// (server/datasets/oui-db.js). `mac` is a full address or a vendor prefix of 6–12 // hex digits (common/mac-input.js). The answer names the block that covers // it, its range and the first-octet flags; an address no block covers is // still a 200 with `found: false`. import { normalizeMacQuery } from '../common/mac-input.js'; -import { lookupMac } from '../common/oui-db.js'; +import { lookupMac } from '../server/datasets/oui-db.js'; // `lookup` is injectable so tests can run the handler over fixture registries. export const createMacChecker = (lookup) => async (req, res) => { diff --git a/api/maxmind.js b/api/maxmind.js index 8c1d68bb..5990d3d9 100644 --- a/api/maxmind.js +++ b/api/maxmind.js @@ -1,5 +1,5 @@ -import { lookupMaxMind } from '../common/maxmind-service.js'; -import logger from '../common/logger.js'; +import { lookupMaxMind } from '../server/datasets/maxmind-service.js'; +import logger from '../server/logger.js'; export default (req, res) => { // Presence, validity and public routability guaranteed by the diff --git a/api/ooni-blocking.js b/api/ooni-blocking.js index 25f53fbd..125d7c5c 100644 --- a/api/ooni-blocking.js +++ b/api/ooni-blocking.js @@ -1,7 +1,7 @@ // /api/ooni-blocking — per-country blocking view for a domain over the last // 30 days, built from OONI's open aggregation API (web_connectivity test, // probe_cc × blocking_type axes). Powers the default view of the frontend -// CensorshipCheck tool; classification lives in common/ooni-blocking.js. +// CensorshipCheck tool; classification lives in server/ooni-blocking.js. // // OONI matches `domain` against the exact tested hostname, so a single query // misses most of the dataset (e.g. `bbc.com` has ~6 rows while `www.bbc.com` @@ -13,8 +13,8 @@ // `cacheable()` and CF absorbs repeat lookups of popular domains. import { fetchUpstream } from '../common/fetch-with-timeout.js'; -import { classifyOoniCountries, OONI_WINDOW_DAYS } from '../common/ooni-blocking.js'; -import logger from '../common/logger.js'; +import { classifyOoniCountries, OONI_WINDOW_DAYS } from '../server/ooni-blocking.js'; +import logger from '../server/logger.js'; const OONI_AGGREGATION_URL = 'https://api.ooni.io/api/v1/aggregation'; diff --git a/api/persona.js b/api/persona.js index 69e82f71..8e02331a 100644 --- a/api/persona.js +++ b/api/persona.js @@ -7,7 +7,7 @@ // ids and enums, and the front end renders its own four-language copy. import { fetchUpstream } from '../common/fetch-with-timeout.js'; -import logger from '../common/logger.js'; +import logger from '../server/logger.js'; // Headers that describe *this* hop rather than the caller, dropped before the // request is rebuilt. diff --git a/api/sentry-tunnel.js b/api/sentry-tunnel.js index 70133a2c..8e1efa32 100644 --- a/api/sentry-tunnel.js +++ b/api/sentry-tunnel.js @@ -7,7 +7,7 @@ // only when VITE_SENTRY_DSN_FRONTEND is set. import { fetchUpstream } from '../common/fetch-with-timeout.js'; import { isValidIP } from '../common/valid-ip.js'; -import logger from '../common/logger.js'; +import logger from '../server/logger.js'; // The envelope header is the first newline-delimited JSON line and carries // the DSN the SDK was configured with. Returns a URL or null. Exported for diff --git a/api/service-status.js b/api/service-status.js index 0bc56929..e4df5073 100644 --- a/api/service-status.js +++ b/api/service-status.js @@ -1,7 +1,7 @@ // /api/service-status — the "Service Status" section's read endpoints. // // Both handlers serve slices of the in-memory snapshot maintained by -// common/service-status-store.js (a background timer refreshes every provider +// server/service-status-store.js (a background timer refreshes every provider // on a fixed 5-minute schedule). Neither touches an upstream — request volume // has no effect on upstream load. Splitting overview vs per-provider detail // keeps the initial page load light: the detail (sub-services + incidents) is @@ -13,7 +13,7 @@ // The detail handler's `id` query param is whitelisted by // requireValidProviderId() in backend-server.js. -import { getServiceStatusOverview, getProviderDetail } from '../common/service-status-store.js'; +import { getServiceStatusOverview, getProviderDetail } from '../server/service-status-store.js'; // Overview: every provider's status light, no heavy detail arrays. export default async (req, res) => { diff --git a/api/share-report.js b/api/share-report.js index a801c1f3..370cde0a 100644 --- a/api/share-report.js +++ b/api/share-report.js @@ -17,7 +17,7 @@ import crypto from 'node:crypto'; import { fetchUpstream } from '../common/fetch-with-timeout.js'; import { validateReport, REPORT_TTL_DAYS, REPORT_MAX_BYTES } from '../common/report-schema.js'; -import logger from '../common/logger.js'; +import logger from '../server/logger.js'; // --- env / KV plumbing ------------------------------------------------------- diff --git a/api/update-user-achievement.js b/api/update-user-achievement.js index e8fb7a24..4ea5fae5 100644 --- a/api/update-user-achievement.js +++ b/api/update-user-achievement.js @@ -1,5 +1,5 @@ import { fetchUpstream } from '../common/fetch-with-timeout.js'; -import logger from '../common/logger.js'; +import logger from '../server/logger.js'; export default async (req, res) => { // defensive; app.put() in backend-server.js already gates method diff --git a/backend-server.js b/backend-server.js index 011ae227..a9691294 100644 --- a/backend-server.js +++ b/backend-server.js @@ -6,10 +6,10 @@ import { fileURLToPath } from 'url'; import { slowDown } from 'express-slow-down' import rateLimit from 'express-rate-limit'; import pinoHttp from 'pino-http'; -import logger from './common/logger.js'; +import logger from './server/logger.js'; import { requireReferer, requirePublicIP, requireValidPrefix, requireValidASN, requireValidDomain, requireValidProviderId, - requireValidRecordType, requireValidReportId, normalizeAsnQuery } from './common/guards.js'; -import { withTimeZone } from './common/ip-timezone.js'; + requireValidRecordType, requireValidReportId, normalizeAsnQuery } from './server/guards.js'; +import { withTimeZone } from './server/ip-timezone.js'; // Backend APIs import mapHandler from './api/google-map.js'; @@ -23,7 +23,7 @@ import ipsbHandler from './api/ip-sb.js'; import maxmindHandler from './api/maxmind.js'; // Others import cfRadarHandler from './api/cf-radar.js'; -import { RADAR_VIEWS, isCompleteRadarAnswer } from './common/cf-radar.js'; +import { RADAR_VIEWS, isCompleteRadarAnswer } from './server/cf-radar.js'; import asnHistoryHandler from './api/asn-history.js'; import asnConnectivityHandler from './api/asn-connectivity.js'; import ooniBlockingHandler from './api/ooni-blocking.js'; @@ -41,23 +41,23 @@ import macChecker from './api/mac-checker.js'; import githubStarsHandler from './api/github-stars.js'; import personaEvaluateHandler from './api/persona.js'; import asnProfileHandler from './api/asn-profile.js'; -import { isCompleteProfile } from './common/asn-profile.js'; +import { isCompleteProfile } from './server/asn-profile.js'; // User import validateConfigs from './api/configs.js'; import getUserinfo from './api/get-user-info.js'; import updateUserAchievement from './api/update-user-achievement.js'; -import { reloadMaxMindDatabases, isMaxMindReady, getMaxMindDbPaths } from './common/maxmind-service.js'; -import { bootstrapDatasets, startDatasetScheduler, watchDatasets } from './common/dataset-updater.js'; -import { datasets } from './common/datasets.js'; +import { reloadMaxMindDatabases, isMaxMindReady, getMaxMindDbPaths } from './server/datasets/maxmind-service.js'; +import { bootstrapDatasets, startDatasetScheduler, watchDatasets } from './server/datasets/dataset-updater.js'; +import { datasets } from './server/datasets/datasets.js'; import { bootstrapServiceStatus, startServiceStatusPolling, isServiceStatusPrimed, -} from './common/service-status-store.js'; -import { isAsRelLoaded } from './common/as-rel-db.js'; -import { isAsOrgLoaded } from './common/as-org-db.js'; -import { isOuiLoaded } from './common/oui-db.js'; -import { runOfflineBootstrap, requireOfflineData, isOfflineBootstrapping } from './common/offline-data.js'; -import { cacheable } from './common/cache-control.js'; -import { initUpstreamUserAgent } from './common/upstream-ua.js'; +} from './server/service-status-store.js'; +import { isAsRelLoaded } from './server/datasets/as-rel-db.js'; +import { isAsOrgLoaded } from './server/datasets/as-org-db.js'; +import { isOuiLoaded } from './server/datasets/oui-db.js'; +import { runOfflineBootstrap, requireOfflineData, isOfflineBootstrapping } from './server/offline-data.js'; +import { cacheable } from './server/cache-control.js'; +import { initUpstreamUserAgent } from './server/upstream-ua.js'; dotenv.config({ quiet: true }); initUpstreamUserAgent(); @@ -216,17 +216,17 @@ if (speedLimitSet !== 0) { app.use(express.json({ limit: '500kb' })); // Default every /api/* response to no-store. Routes that want edge caching -// declare it explicitly via `cacheable(maxAge)` (common/cache-control.js). +// declare it explicitly via `cacheable(maxAge)` (server/cache-control.js). app.use('/api', (req, res, next) => { res.setHeader('Cache-Control', 'no-store'); next(); }); // Global referer gate for all /api/* routes. Handlers no longer repeat this -// check individually — see common/guards.js. +// check individually — see server/guards.js. app.use('/api', requireReferer); -// Readiness gates for routes reading local data (common/offline-data.js): +// Readiness gates for routes reading local data (server/offline-data.js): // 503 while that data is still downloading at boot, a no-op afterwards. const needsMaxMind = requireOfflineData([isMaxMindReady]); const needsAsGraph = requireOfflineData([isAsRelLoaded, isAsOrgLoaded]); @@ -275,7 +275,7 @@ app.get('/api/asn-profile', requireValidASN(), cacheable(SEVEN_DAYS_CACHE, { degradedMaxAge: () => (isOfflineBootstrapping() ? 0 : ONE_DAY_CACHE), }), asnProfileHandler); // All Cloudflare Radar data rides one route; `?view=` picks the dataset and -// the TTL comes from that view's registry entry (common/cf-radar.js) — 7d +// the TTL comes from that view's registry entry (server/cf-radar.js) — 7d // for the ASN summary and prefix list (same as /api/asn-profile), 30d for // country traffic, 1h for the outage feed. A partial answer (some upstream // calls failed, or prefix countries MaxMind couldn't fully work out) is diff --git a/common/asn-input.js b/common/asn-input.js index 9e9a1790..e1c6b7b3 100644 --- a/common/asn-input.js +++ b/common/asn-input.js @@ -1,4 +1,4 @@ -// The AS-number grammar shared by the ASN guards (common/guards.js) and the +// The AS-number grammar shared by the ASN guards (server/guards.js) and the // tools that take a typed AS number (Whois, ASN Profile — via the // frontend/utils/asn-input.js bridge): up to ten digits, AS1 … AS4294967295, // `AS` prefix case-insensitive. diff --git a/common/bgp-prefix.js b/common/bgp-prefix.js index 54de1ed7..76e62bec 100644 --- a/common/bgp-prefix.js +++ b/common/bgp-prefix.js @@ -10,7 +10,7 @@ const HEX_GROUP = /^[0-9a-fA-F]{1,4}$/; // Expand an IPv6 address (with optional `::` compression) into 8 hextet // strings. Returns null on any structural failure. Also consumed by -// common/rdap.js for CIDR containment math. +// server/rdap.js for CIDR containment math. export function expandIPv6(ip) { const halves = ip.split('::'); if (halves.length > 2) return null; diff --git a/common/fetch-with-timeout.js b/common/fetch-with-timeout.js index bdf146a5..444939ab 100644 --- a/common/fetch-with-timeout.js +++ b/common/fetch-with-timeout.js @@ -36,7 +36,7 @@ export async function fetchWithTimeout(url, init = {}) { } // Optional User-Agent for fetchUpstream calls. Backend boot injects a -// project-identifying UA (see common/upstream-ua.js) because some upstream +// project-identifying UA (see server/upstream-ua.js) because some upstream // WAFs hard-block undici's default `User-Agent: node`. Injection keeps this // file browser-safe: the frontend bundle never touches fs / process. let upstreamUserAgent = null; diff --git a/common/locale-registry.js b/common/locale-registry.js index 634e8f50..5a711045 100644 --- a/common/locale-registry.js +++ b/common/locale-registry.js @@ -3,7 +3,7 @@ // // The back end takes no part in that: which languages upstream *data* comes in // is a separate set owned by the source itself (SUPPORTED_LANGS in -// common/maxmind-service.js), and every `?lang` consumer resolves an unfamiliar +// server/datasets/maxmind-service.js), and every `?lang` consumer resolves an unfamiliar // tag onto its own family instead of rejecting it. A new UI locale is a // front-end-only change. diff --git a/common/report-schema.js b/common/report-schema.js index 00c3fd60..75ccbff2 100644 --- a/common/report-schema.js +++ b/common/report-schema.js @@ -25,7 +25,7 @@ export const REPORT_TTL_DAYS = [1, 3, 7]; export const REPORT_MAX_BYTES = 256 * 1024; // Anonymity verdict codes for ipinfo cards — made by -// utils/transform-ip-data.js, shared by the builder whitelist and the schema +// utils/ip/transform-ip-data.js, shared by the builder whitelist and the schema // enum below. export const ANONYMITY_CODES = [ 'tor', 'relay', 'residential', 'suspected_residential', @@ -33,7 +33,7 @@ export const ANONYMITY_CODES = [ ]; // Connectivity status codes — single source shared by the recording component -// (ConnectivityTest.vue), the builder whitelist (utils/report-builders.js) +// (ConnectivityTest.vue), the builder whitelist (utils/report/report-builders.js) // and the schema enum below, so a rename can't drift them apart. export const CONNECTIVITY_STATUS = { OK: 'ok', @@ -165,7 +165,7 @@ const SECTION_SPECS = { connectivity: obj({ testedAt: isoDate(), // 72 = 7 built-in targets + the 60-target custom/import cap, plus - // headroom; matches the builder's slice in utils/report-builders.js. + // headroom; matches the builder's slice in utils/report/report-builders.js. targets: arr(72, obj({ id: str(48), name: str(64), diff --git a/frontend-server.js b/frontend-server.js index 3a5a7c7c..8ca6902a 100644 --- a/frontend-server.js +++ b/frontend-server.js @@ -4,7 +4,7 @@ import express from 'express'; import path from 'path'; import { fileURLToPath } from 'url'; import { createProxyMiddleware } from 'http-proxy-middleware'; -import logger from './common/logger.js'; +import logger from './server/logger.js'; dotenv.config({ quiet: true }); diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index bfa48f58..c26adcc3 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -26,10 +26,14 @@ Every file opens with a header comment stating its purpose — read those. - **Composition API** everywhere; no Options API. Alias `@` → `frontend/`. - **Shared-with-backend helpers live in `common/`**, re-exported through a thin - `utils/` bridge so consumers keep `@/utils/...` imports (`utils/valid-ip.js`). + `utils/` bridge so consumers keep `@/utils/...` imports (`utils/ip/valid-ip.js`). - **Helper placement:** Vue reactivity / lifecycle → `composables/` (`useXxx`); otherwise `utils/` (never `use-` prefixed). `lib/` stays shadcn-only. A pure function next to a composable exports from that composable's file. +- **Inside `utils/`:** app-wide plumbing at its root; IP / ASN data shaping + and validation → `ip/`; the report pipeline → `report/`; the pure layer of + one feature (a tool, a section, Pulse) → `features/`; per-source probe + modules keep their own dirs (`getips/`, `dnsleaks/`, `persona/`). ### Achievements are event-driven @@ -39,7 +43,7 @@ events → slugs, `composables/use-achievement-engine.js` owns all guards (rules wait for the remote snapshot; pre-sync hits parked). New achievement = entry + rule + (only if no suitable event exists) a new event. The shareable report rides the same bus: `:finished` → `use-report-collector.js` → -`utils/report-builders.js` → sections whitelisted by `common/report-schema.js`. +`utils/report/report-builders.js` → sections whitelisted by `common/report-schema.js`. New reportable test = event + builder + schema entry in one change; new result semantics = builder whitelist + schema enum too (builders fail soft, fixtures are frozen: drift = quietly missing fields). Report links are public: the diff --git a/frontend/App.vue b/frontend/App.vue index bf4502b4..4dbb5f8a 100644 --- a/frontend/App.vue +++ b/frontend/App.vue @@ -17,7 +17,7 @@ import { TooltipProvider } from '@/components/ui/tooltip'; import Alert from '@/components/widgets/Toast.vue'; import DocsAssistant from '@/components/widgets/DocsAssistant.vue'; import { shouldOfferPwaInstall } from '@/utils/pwa.js'; -import { sendVisitBeacon } from '@/utils/pulse-beacon.js'; +import { sendVisitBeacon } from '@/utils/features/pulse-beacon.js'; import { useTheme } from '@/composables/use-theme.js'; // PWA install prompt — async and eligibility-gated: ineligible visits (too @@ -31,7 +31,7 @@ onMounted(() => { setTimeout(() => { offerPwaInstall.value = true; }, 30 * 1000); } // Earth Online visit tick — app-level so every route counts, once per - // page load; the backend dedups per IP (utils/pulse-beacon.js). + // page load; the backend dedups per IP (utils/features/pulse-beacon.js). sendVisitBeacon(); }); import { useAchievementEngine } from '@/composables/use-achievement-engine.js'; diff --git a/frontend/components/ConnectivityTest.vue b/frontend/components/ConnectivityTest.vue index 008d36fb..f6ca1a26 100644 --- a/frontend/components/ConnectivityTest.vue +++ b/frontend/components/ConnectivityTest.vue @@ -160,9 +160,9 @@ import { useI18n } from 'vue-i18n'; import { trackEvent } from '@/utils/analytics'; import { emitAppEvent, waitForAppEvent } from '@/utils/app-events'; import { useAppCommand } from '@/composables/use-app-command.js'; -import { CONNECTIVITY_STATUS } from '@/utils/report-schema.js'; +import { CONNECTIVITY_STATUS } from '@/utils/report/report-schema.js'; import { TILE_PREVIEW, faviconPath, MINE_LIST_ID } from '@/data/connectivity-import-lists.js'; -import { siteUrlOf, removeMember } from '@/utils/connectivity-lists.js'; +import { siteUrlOf, removeMember } from '@/utils/features/connectivity-lists.js'; import { JnTooltip } from '@/components/ui/tooltip'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; @@ -386,7 +386,7 @@ const checkConnectivityHandler = async (test, onTestComplete = () => { }, isManu const testTime = Math.round(performance.now() - beginTime); test.status = t('connectivity.StatusAvailable'); // Locale-free twin of the localized status label, consumed by the report - // builder (utils/report-builders.js) — the label itself can't be mapped + // builder (utils/report/report-builders.js) — the label itself can't be mapped // back to an enum without string comparison against t() output. test.statusCode = CONNECTIVITY_STATUS.OK; test.mintime = test.mintime === 0 ? testTime : Math.min(test.mintime, testTime); diff --git a/frontend/components/IpInfos.vue b/frontend/components/IpInfos.vue index 855009c0..c74a9633 100644 --- a/frontend/components/IpInfos.vue +++ b/frontend/components/IpInfos.vue @@ -35,9 +35,9 @@ import { ref, computed, onMounted, reactive, watch } from 'vue'; import { useMainStore } from '@/store'; import { useI18n } from 'vue-i18n'; import { trackEvent } from '@/utils/analytics'; -import { isUsablePublicIP } from '@/utils/valid-ip.js'; +import { isUsablePublicIP } from '@/utils/ip/valid-ip.js'; import { toApiTag } from '@/utils/locale-registry.js'; -import { transformDataFromIPapi } from '@/utils/transform-ip-data.js'; +import { transformDataFromIPapi } from '@/utils/ip/transform-ip-data.js'; import { getIPFromIPIP, getIPFromCloudflare_V4, getIPFromCloudflare_V6, getIPFromIPChecking64, getIPFromIPChecking4, getIPFromIPChecking6 } from '@/utils/getips'; import { emitAppEvent, waitForAppEvent } from '@/utils/app-events'; import { useAppCommand } from '@/composables/use-app-command.js'; diff --git a/frontend/components/SpeedTest.vue b/frontend/components/SpeedTest.vue index 382e947e..229b4a37 100644 --- a/frontend/components/SpeedTest.vue +++ b/frontend/components/SpeedTest.vue @@ -184,11 +184,11 @@ import { trackEvent } from '@/utils/analytics'; import { emitAppEvent } from '@/utils/app-events.js'; import { useAppCommand } from '@/composables/use-app-command.js'; import { fetchWithTimeout } from '@/utils/fetch-with-timeout.js'; -import { isValidIP } from '@/utils/valid-ip.js'; -import { parseTrace } from '@/utils/parse-trace.js'; +import { isValidIP } from '@/utils/ip/valid-ip.js'; +import { parseTrace } from '@/utils/ip/parse-trace.js'; import getCountryName from '@/data/country-name.js'; import SpeedTestEngine from '@cloudflare/speedtest'; -import { createSpeedTestSession, getSpeedTestLiveValues } from '@/utils/speedtest-session.js'; +import { createSpeedTestSession, getSpeedTestLiveValues } from '@/utils/features/speedtest-session.js'; import useSpeedTestCharts from '@/composables/use-speedtest-charts.js'; import { JnTooltip } from '@/components/ui/tooltip'; import { Button } from '@/components/ui/button'; diff --git a/frontend/components/advanced-tools/AsnProfile.vue b/frontend/components/advanced-tools/AsnProfile.vue index 57ebf3fd..e9373245 100644 --- a/frontend/components/advanced-tools/AsnProfile.vue +++ b/frontend/components/advanced-tools/AsnProfile.vue @@ -1,6 +1,6 @@ + written back on every run). Shaping lives in utils/ip/asn-profile-view.js. -->