openvibers/openvibe.domains README.md
README.md
| 1 | # OpenVibe.Domains |
| 2 | |
| 3 | > Name it, price it, run it. |
| 4 | |
| 5 | **Live site:** https://openvibe.domains · **API docs:** https://openvibe.services · **For AI:** https://openvibe.domains/llms.txt |
| 6 | **Run it:** `npm ci && npm run dev` (Node 22 or newer) · **Tests:** `npm test` |
| 7 | |
| 8 | **Status:** v1 is the service skeleton (see [STATUS.json](STATUS.json)); the pages are plain placeholders. |
| 9 | **Domain:** `openvibe.domains` · **Port:** 5070 · **Service id:** `domains` · **Env prefix:** `DOMAINS` |
| 10 | **License:** AGPL-3.0-only (same as every OpenVibe service). |
| 11 | |
| 12 | OpenVibe.Domains is a stateless read-only front over two OpenVibe services. For a name it shows the registry's |
| 13 | availability and every registrar's prices for that ending, with one-, three- and five-year totals; every ending has |
| 14 | its own page; every domain has a page with its public registration history. |
| 15 | |
| 16 | There is no database, no sign-in, no session and no event stream. Nothing is stored and nothing is written. |
| 17 | |
| 18 | ## Pages (v1) |
| 19 | |
| 20 | | Route | What it shows | |
| 21 | |---|---| |
| 22 | | `/` | the first page of OpenVibe.Host's price catalog (popular endings), plus OpenVibe.Tools' public "most looked up" and "expiring soon" lists when it answers | |
| 23 | | `/search?q=` | registry status for the name, and every registrar's first-year, three-year and five-year totals, sorted by the three-year total; `noindex` because every query is a page | |
| 24 | | `/tld/:tld` | one ending (`.dev`, `co.uk`) with every registrar's prices, the five-year total and the renewal-trap mark | |
| 25 | | `/.<ending>` | 301 to `/tld/<ending>` (for example `/.dev` → `/tld/dev`) | |
| 26 | | `/d/:domain` | a full domain: its public history from OpenVibe.Tools and its ending's prices from OpenVibe.Host | |
| 27 | | anything else | the 404 page | |
| 28 | |
| 29 | All of it is server-rendered and readable without JavaScript. The shell is the OpenVibe Frame (`openvibe-shared/shell`): |
| 30 | theme loader, navbar mounted into `#navbar-mount`, the shared server-rendered footer, service id `domains`. |
| 31 | |
| 32 | ## The control panel (v2) |
| 33 | |
| 34 | `/panel` is a signed-in person's own (sign-in with OpenVibe.Network, client `domains`). |
| 35 | |
| 36 | - **Connect:** paste a Cloudflare API token (Edit zone DNS + Zone Read) or a Porkbun API key pair (create keys at |
| 37 | porkbun.com/account/api, then switch on API access for each domain). Domains verifies it, then stores it as a |
| 38 | secret in your own Network project (`OpenVibe.Domains`, ADR-056), granted to `domains` for calls to that provider |
| 39 | only (`https://api.cloudflare.com` or `https://api.porkbun.com`). It is revealed per call (`network.secret.use`) |
| 40 | and never kept here. Porkbun's pair is one JSON secret, parsed only inside its client. |
| 41 | - **Zones and records:** zones are synced into `domains_zones` (Porkbun's domains are its zones); records are read |
| 42 | live. |
| 43 | - **Proposals:** every change is a `domains.proposal@1`: create, update or delete records, why, what could break, and |
| 44 | 7 days to decide. A registrar change is a nameserver list update (`NS`) or a DNSSEC DS create/delete (`DS`), applied |
| 45 | through the registrar API. Approve on `/panel/p/:id` or in the Action Center. Changes apply in order; a record or |
| 46 | nameserver list that moved since the proposal stops the run (stale). An applied (or part-applied) proposal rolls |
| 47 | back in one step. |
| 48 | - **API:** `GET /api/v1/panel/connections`, `/api/v1/panel/zones`, `/api/v1/panel/zones/:conn/:zone/records`, |
| 49 | `GET|POST /api/v1/proposals`, `POST /api/v1/proposals/:id/decide|rollback` with your Network token; Network's Action |
| 50 | Center reads `GET /api/v1/actions?subject=` (`domains.actions.read`) and decides (`domains.proposal.decide`). |
| 51 | |
| 52 | ## API |
| 53 | |
| 54 | Everything under `/api/v1`, read-only, CORS-open (`Access-Control-Allow-Origin: *`), cached publicly for five |
| 55 | minutes. A bad name is RFC 9457 `application/problem+json`; an ending the catalog does not know is 404. |
| 56 | |
| 57 | | Route | Answer | |
| 58 | |---|---| |
| 59 | | `GET /api/v1/search?q=` | `{ name, status, rows }` — `name` is `{ label, tld, ascii, display }`, `status` is the registry's answer, `rows` are the priced registrar rows | |
| 60 | | `GET /api/v1/tlds?q=&kind=&sort=&page=` | OpenVibe.Host's list as it answers it: `{ tlds, next_cursor }`. Sorts: `popular`, `tld`, `register`, `renew`, `five_year` | |
| 61 | | `GET /api/v1/tlds/:tld` | `{ tld }` with its prices priced like the page | |
| 62 | | `GET /api/v1/domains/:domain` | `{ name, history, prices }` — history from OpenVibe.Tools, prices for that domain's ending only | |
| 63 | |
| 64 | `/api/v1` and `/search` share a per-IP limit of 120 requests a minute (`express-rate-limit`). |
| 65 | |
| 66 | ## What "priced" means |
| 67 | |
| 68 | OpenVibe.Host's check rows carry `register`, `renew`, `five_year`, `registrars`, `currency`, `checked_at`, `tld`, |
| 69 | `display` and `kind`. Each row gains: |
| 70 | |
| 71 | - `y1` — first year: `register`; |
| 72 | - `y3` — three years: `register + 2 × renew`; |
| 73 | - `y5` — five years: the catalog's `five_year` when it has one, else `register + 4 × renew`; |
| 74 | - `trap` — `renew ≥ 2 × register` **and** `renew − register ≥ 5`: the cheap-first-year renewal trap. |
| 75 | |
| 76 | A total that needs a missing price is `null`, never `NaN`. Money is rounded to cents. Only USD rows are compared and |
| 77 | sorted (by `y3`, rows without a `y3` last); other currencies are kept after them and flagged `currency_other: true`. |
| 78 | |
| 79 | ## The data it reads |
| 80 | |
| 81 | | Source | Endpoint | Used for | Cache | |
| 82 | |---|---|---|---| |
| 83 | | OpenVibe.Host | `GET /api/v1/catalog/domains?sort=…&cursor=…` | the catalog's ending list and the sitemap | 10 minutes | |
| 84 | | OpenVibe.Host | `GET /api/v1/catalog/domains/:tld` | one ending's registrar prices and history | 10 minutes | |
| 85 | | OpenVibe.Host | `GET /api/v1/catalog/domains/check?name=` | a name's registry status and priced endings | 2 minutes | |
| 86 | | OpenVibe.Tools | `GET /api/v1/domains/` | the public most-looked-up and expiring-soon lists | 10 minutes | |
| 87 | | OpenVibe.Tools | `GET /api/v1/domains/:domain` | a domain's public snapshots and changes | 10 minutes | |
| 88 | |
| 89 | Both clients take an injectable `fetchImpl`, time out after 8 seconds, and keep a small in-process `Map` cache with a |
| 90 | 2,000-entry cap (oldest insertion out). A failure is `null` and one short log line — never an exception in a page. |
| 91 | The upstream URLs come from `OV_HOST_INTERNAL_URL` / `OV_TOOLS_INTERNAL_URL`, or the internal origins in the pinned |
| 92 | OpenVibe.Contracts manifests, with `https://openvibe.tools` as the Tools fallback. |
| 93 | |
| 94 | OpenVibe.Tools keeps RDAP's public registration facts only (registrar, dates, status codes, name servers, DNSSEC) and |
| 95 | publishes a domain on its lists only after at least five lookups. This service adds no data of its own. |
| 96 | |
| 97 | ## Configuration |
| 98 | |
| 99 | Copy [.env.example](.env.example) to the environment file (`/etc/openvibe/domains.env` in production): |
| 100 | |
| 101 | | Variable | Default | What | |
| 102 | |---|---|---| |
| 103 | | `NODE_ENV` | `development` | `production` selects the production base URL | |
| 104 | | `PORT` | `5070` | listen port | |
| 105 | | `HOST` | `127.0.0.1` | listen address | |
| 106 | | `BASE_URL` | `https://openvibe.domains` in production | public origin used for canonical URLs, the sitemap and llms.txt | |
| 107 | | `TRUST_PROXY` | `2` | how many proxies to trust (nginx and the CDN) | |
| 108 | | `OV_HOST_INTERNAL_URL` | the Host manifest's `internalOrigin` | OpenVibe.Host's internal API | |
| 109 | | `OV_TOOLS_INTERNAL_URL` | the Tools manifest's `internalOrigin` | OpenVibe.Tools' internal API | |
| 110 | | `DOMAINS_RATE_LIMIT_PER_MIN` | `120` | the shared `/api/v1` and `/search` per-IP budget | |
| 111 | | `DOMAINS_UPSTREAM_TIMEOUT_MS` | `8000` | the two data clients' timeout | |
| 112 | |
| 113 | ## Run it |
| 114 | |
| 115 | ```bash |
| 116 | npm install |
| 117 | npm test # every test/*.test.js, each in its own process, with stub upstreams |
| 118 | npm run dev # http://localhost:5070 |
| 119 | npm start # production |
| 120 | ``` |
| 121 | |
| 122 | The tests need no database, no network and no running OpenVibe site. `node --check` every `.js` file is the other |
| 123 | gate. |
| 124 | |
| 125 | ## Machine endpoints |
| 126 | |
| 127 | | Route | What | |
| 128 | |---|---| |
| 129 | | `/api/health` | `{ status: "ok", service: "openvibe-domains", version }` | |
| 130 | | `/api/ready` | truthful readiness (`openvibe-shared/ready`): `ready` while the process serves; a Host or Tools failure makes `status: "degraded"` and names it, never 503 | |
| 131 | | `/release.json`, `/release-metrics` | the release manifest and the tab collector (`openvibe-shared/release`) | |
| 132 | | `/metrics` | Prometheus text format, loopback only (`openvibe-shared/metrics`) | |
| 133 | | `/shared/*` | this service's own pinned OpenVibe Frame files | |
| 134 | | `/robots.txt` | allow everything, name the sitemap | |
| 135 | | `/sitemap.xml` | `/` and one `/tld/<ending>` per ending in every page of the Host catalog; `lastmod` from the release | |
| 136 | | `/llms.txt` | what the site is, its pages and its API | |
| 137 | |
| 138 | ## Deploy |
| 139 | |
| 140 | - `deploy/systemd/openvibe-domains.service` — user `ubuntu`, `WorkingDirectory=/opt/openvibe.domains`, |
| 141 | `EnvironmentFile=/etc/openvibe/domains.env`, port 5070. |
| 142 | - `deploy/nginx/openvibe.domains.conf` — TLS, `www` → apex 301, proxy to `127.0.0.1:5070`, security headers, GET-only |
| 143 | `/api/v1`, loopback-only metrics. |
| 144 | - CI: `.github/workflows/ci.yml` runs the shared syntax/contracts/security jobs; there is no database service. |
| 145 | |
| 146 | ## License |
| 147 | |
| 148 | AGPL-3.0-only. See [LICENSE](LICENSE). |
| 149 | |
| 150 | <!-- versions:start --> |
| 151 | - openvibe-contracts: v0.419.0 |
| 152 | - openvibe-sdk: v0.166.0 |
| 153 | - openvibe-shared: v4.241.0 |
| 154 | <!-- versions:end --> |
| 155 |