openvibers/openvibe.domains AGENTS.md
AGENTS.md
| 1 | # AGENTS.md — OpenVibe.Domains |
| 2 | |
| 3 | ## Project overview |
| 4 | |
| 5 | "Name it, price it, run it." Domains is two things: a **stateless read-only front** over OpenVibe.Host's |
| 6 | price catalog and OpenVibe.Tools' public registry data (availability, registrar prices with 1/3/5-year |
| 7 | totals, per-ending pages, a domain's public registration history), and a **v2 control panel** where a |
| 8 | person connects a registrar/DNS account and every change is a reviewable proposal. Node.js/Express, |
| 9 | PostgreSQL for the panel, server-rendered, CommonJS, **no build step**. |
| 10 | |
| 11 | - Service id `domains`, port `5070`, origin `https://openvibe.domains`, unit `openvibe-domains.service`, |
| 12 | release root `/opt/openvibe.domains`. |
| 13 | - v1 has **no database, no sign-in, no session, no event stream**; it stores nothing and writes nothing. |
| 14 | v2 adds the panel's own `ov_domains` state and Network project secrets for credentials. |
| 15 | |
| 16 | ## What it owns |
| 17 | |
| 18 | - Pages `/`, `/search?q=`, `/tld/:tld`, `/.<ending>` (301 to `/tld/<ending>`), `/d/:domain`, 404 — all |
| 19 | server-rendered, readable without JavaScript, in the OpenVibe Frame. |
| 20 | - The price arithmetic: `y1 = register`, `y3 = register + 2 × renew`, `y5 = five_year` or |
| 21 | `register + 4 × renew`; `trap` when `renew ≥ 2 × register` **and** `renew − register ≥ 5`; a total |
| 22 | needing a missing price is `null`, never `NaN`; only USD rows are compared/sorted, other currencies |
| 23 | follow with `currency_other: true`. |
| 24 | - The panel (`/panel`): connections (Cloudflare API token with Edit zone DNS + Zone Read, or a Porkbun |
| 25 | key pair), zones synced into `domains_zones`, records read live, proposals and their application. |
| 26 | - `domains.proposal@1`: create/update/delete records, why, what could break, a 7-day decision window; a |
| 27 | registrar change is an `NS` list update or a `DNSSEC DS` create/delete. Approve on `/panel/p/:id` or in |
| 28 | the Action Center; changes apply in order; a record change applies through the provider API. |
| 29 | - The JSON API under `/api/v1` (read-only in v1; the panel's routes in v2), discovery files, health/ready. |
| 30 | - `ov_domains` migrations (`0001_panel.sql`, `0002_config.sql`). |
| 31 | |
| 32 | Not owned: the prices and registry data (Host/Tools), the domains themselves and their DNS truth (the |
| 33 | registrar), credentials (a person's Network project secret, ADR-056), identity (Network). |
| 34 | |
| 35 | ## Who calls it, and whom it calls |
| 36 | |
| 37 | - Callers: people in the browser; the Action Center (proposal approvals); services with a Network service |
| 38 | token audience `openvibe.domains` and the route's capability. |
| 39 | - Depends on: |
| 40 | - **OpenVibe.Host** — its internal API (`OV_HOST_INTERNAL_URL`) for the price catalog/check rows. |
| 41 | - **OpenVibe.Tools** — its internal API (`OV_TOOLS_INTERNAL_URL`) for "most looked up", "expiring soon" |
| 42 | and a domain's public history. |
| 43 | - **OpenVibe.Network** — SSO client `domains`, JWKS, and the person's project secret store; a provider |
| 44 | credential is stored as a **secret in the person's own Network project** (`OpenVibe.Domains`, |
| 45 | ADR-056), granted to `domains` for calls to that provider only |
| 46 | (`https://api.cloudflare.com` or `https://api.porkbun.com`), revealed per call (`network.secret.use`) |
| 47 | and never kept here. Porkbun's key pair is one JSON secret, parsed only inside its client. |
| 48 | - **Events** — account export/deletion; the Action Center's proposal flow. |
| 49 | - PostgreSQL (`DATABASE_URL`, `DATABASE_DIRECT_URL` for migrations) and the pinned SDK/Shared. |
| 50 | |
| 51 | The two data clients are cached and bounded (`DOMAINS_UPSTREAM_TIMEOUT_MS`, default 8 s); a Host/Tools |
| 52 | failure shows what is available, never a fabricated price. |
| 53 | |
| 54 | ## Repository map |
| 55 | |
| 56 | - **Entry:** [server/index.js](server/index.js) — `createApp()`, listen, graceful stop. |
| 57 | - **App:** [server/app.js](server/app.js) — `/`/`/search`/`/tld/:tld`/`/d/:domain`, `/panel/*`, `/auth`, |
| 58 | `/api/v1`, `/internal/events`, `/internal/config`, `/shared`, `/css`, discovery, health/ready/release. |
| 59 | - **Config/db:** [server/config.js](server/config.js), [server/db.js](server/db.js), |
| 60 | [server/settings.js](server/settings.js). |
| 61 | - **Data/pricing:** [server/data/host.js](server/data/host.js), [server/data/tools.js](server/data/tools.js), |
| 62 | [server/data/cache.js](server/data/cache.js), [server/pricing.js](server/pricing.js), |
| 63 | [server/names.js](server/names.js). |
| 64 | - **Panel:** [server/panel/panel.js](server/panel/panel.js), |
| 65 | [server/panel/vault.js](server/panel/vault.js), [server/panel/cloudflare.js](server/panel/cloudflare.js), |
| 66 | [server/panel/porkbun.js](server/panel/porkbun.js). |
| 67 | - **HTTP/views:** [server/http/api.js](server/http/api.js), `pages.js`, `panel.js`, `principal.js`, |
| 68 | `discovery.js`, `router.js`; [server/views/](server/views/) (`index.js`, `panel.js`, `short.js`). |
| 69 | - **Auth:** [server/auth/sso.js](server/auth/sso.js), [server/auth/keys.js](server/auth/keys.js). |
| 70 | - **Deploy:** [deploy/systemd/openvibe-domains.service](deploy/systemd/openvibe-domains.service), |
| 71 | [deploy/nginx/openvibe.domains.conf](deploy/nginx/openvibe.domains.conf). |
| 72 | |
| 73 | ## Commands |
| 74 | |
| 75 | ```bash |
| 76 | npm install |
| 77 | npm run dev # http://localhost:5070 |
| 78 | npm start # production |
| 79 | npm test # test/run.js, each file in its own process, stub upstreams |
| 80 | npm test -- data # filter by file name; --strict makes a skip fail |
| 81 | ov test # the harness's sharded run of what the change reaches |
| 82 | node --check <file.js> |
| 83 | ``` |
| 84 | |
| 85 | The tests need no database, no network and no running OpenVibe site. |
| 86 | |
| 87 | ## Database and migrations |
| 88 | |
| 89 | The v1 pages need no database. The v2 panel uses `ov_domains` on the host's data role (ADR-035): |
| 90 | `domains_zones` and the proposal/config state from [migrations/](migrations/). Async through |
| 91 | `openvibe-sdk/db`; never edit an applied migration, add a new number. **A provider credential is never a |
| 92 | row here** — only its Network project/secret name; the value is revealed per call. |
| 93 | |
| 94 | ## Auth and panel security |
| 95 | |
| 96 | - v1 pages are public and anonymous; a read is the same for every caller and no session is created. |
| 97 | - The panel signs a person in through Network SSO (client `domains`, PKCE); the person's Network token is |
| 98 | used for their project secrets. |
| 99 | - A provider token/key is verified before storage, stored in the **person's own Network project** with a |
| 100 | grant limited to that provider's origin, and revealed only when a call is made. Nothing is logged or |
| 101 | echoed; Porkbun's JSON pair is parsed only in its client. |
| 102 | - Every DNS change is a proposal with a reason, a risk note and a 7-day window; apply in order, and record |
| 103 | the outcome. A registrar change (`NS`/`DS`) uses the registrar API. |
| 104 | - Service callers need the route's capability (`domains.*`); account export/deletion goes through |
| 105 | `/internal/events` only. |
| 106 | |
| 107 | ## Frontend loading model |
| 108 | |
| 109 | All pages are server-rendered and readable without JavaScript; the Frame comes from `openvibe-shared/shell` |
| 110 | (theme loader, navbar into `#navbar-mount`, shared footer), and `/shared/*` is served from the pinned |
| 111 | node_modules. There is no SPA, bundler or transpiler. `/css/domains.css` is the only page CSS served here. |
| 112 | |
| 113 | ## Deploy |
| 114 | |
| 115 | Unit [deploy/systemd/openvibe-domains.service](deploy/systemd/openvibe-domains.service) runs |
| 116 | `/opt/openvibe.domains/server/index.js` as `ubuntu` on port `5070`, env `/etc/openvibe/domains.env`; vhost |
| 117 | [deploy/nginx/openvibe.domains.conf](deploy/nginx/openvibe.domains.conf) (TLS, `www`→apex 301, GET-only |
| 118 | `/api/v1`, loopback-only metrics). The repository does not document an `ovhost deploy domains` command — |
| 119 | use the host's own service installer. CI runs syntax/contracts/security and has no database service. |
| 120 | |
| 121 | ## Tests |
| 122 | |
| 123 | `npm test` runs [test/run.js](test/run.js) with stub upstreams: [test/data.test.js](test/data.test.js) |
| 124 | (pricing arithmetic, missing price → `null`, currency handling, renew-trap), `test/home-short.test.js`, |
| 125 | `test/agent-layer.test.js`, `test/account-data.test.js` and helpers. No network or database is needed. |
| 126 | `ov test` runs the same. |
| 127 | |
| 128 | ## Conventions |
| 129 | |
| 130 | - CommonJS, 4-space indent, single quotes, semicolons; data clients and panel providers are injectable |
| 131 | factories so tests pass stubs. |
| 132 | - Money is integer minor units / rounded cents with a currency; a total that needs a missing input is |
| 133 | `null`, not `0` or `NaN`. |
| 134 | - Prices are labelled with their check date (`checked_at`) and source; "as of" is part of the page. |
| 135 | - Errors are small JSON problems or the 404 page; a provider/upstream failure is a clear notice, not a 500. |
| 136 | |
| 137 | ## Pitfalls |
| 138 | |
| 139 | - **Never fabricate a price or a total.** Missing input → `null`; only USD rows are compared; other |
| 140 | currencies are kept and flagged, never silently converted. |
| 141 | - **The renew trap is a warning, not an exclusion**: `renew ≥ 2 × register` and `renew − register ≥ 5`; |
| 142 | do not reorder the list by it. |
| 143 | - **Credentials belong to the person's Network project**, never to `ov_domains`, an env file or a log; a |
| 144 | provider grant is limited to that provider's origin and revealed per call. |
| 145 | - **Every DNS change is a proposal** with a reason, risk, 7-day window and ordered application; do not add |
| 146 | a direct "apply now" admin path. |
| 147 | - **`/.<ending>` must 301** to `/tld/<ending>`; a new ending route keeps that alias. |
| 148 | - **v1 stays read-only**; do not add a write API to the price pages. |
| 149 | - **The `www` host canonicalises to the apex**; keep the 301. |
| 150 | - **Loopback-only metrics and GET-only `/api/v1`** in the vhost; do not widen it for a new route without |
| 151 | the nginx test. |
| 152 | |
| 153 | ## Documentation |
| 154 | |
| 155 | - [README.md](README.md) — the pages, panel, API, what "priced" means, the data it reads, configuration, |
| 156 | run, machine endpoints, deploy. |
| 157 | - [STATUS.json](STATUS.json) — the v1/v2 state. |
| 158 | - OpenVibe.Contracts holds `domains.*`/`domains.proposal@1`; Host/Tools/Network own their upstream shapes. |
| 159 |