refactor(dns-updater): rename prospector.ct -> sales.ct (public name)

The always-on region-mobile surface is publicly "sales" (the node is still the
Prospector PWA internally). DNS host becomes sales.ct.uvlava.com; the joker.com
CNAME is sales.transquinnftw.com -> sales.ct.uvlava.com. Updated terraform
record, env grant, client examples, README, and tests (8 pass).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Natalie 2026-06-29 14:11:13 -04:00
parent e89cca3dc9
commit 98cc0fa21d
8 changed files with 23 additions and 21 deletions

View file

@ -9,7 +9,7 @@ when they move regions — no registrar GUI, no manual `dig`/edit:
| FQDN (moving) | Node | Lifecycle |
| ---------------------------- | ----------------------------- | ---------------------------------------- |
| `live.ct.uvlava.com` | broadcast relay droplet | **ephemeral** — per show, nearest the broadcaster, torn down after |
| `prospector.ct.uvlava.com` | Prospector PWA node | **always-on** — follows the operator across regions, stays up |
| `sales.ct.uvlava.com` | "sales" surface = Prospector PWA node | **always-on** — follows the operator across regions, stays up |
The pretty `*.transquinnftw.com` names are **static CNAMEs** onto these (set once,
below), so the only records that ever move are inside the zone we control via the
@ -76,8 +76,8 @@ exist (`terraform apply` of the records below), add two **static CNAMEs** there
these never change again:
```
live CNAME live.ct.uvlava.com.
prospector CNAME prospector.ct.uvlava.com.
live CNAME live.ct.uvlava.com.
sales CNAME sales.ct.uvlava.com.
```
> `transquinnftw.com` is NOT delegated to DO, so these CNAMEs are added in the
@ -87,7 +87,7 @@ prospector CNAME prospector.ct.uvlava.com.
## terraform records
`terraform/do/dns.tf` creates `dns.ct` (→ forge) and seeds `live.ct` /
`prospector.ct` with `lifecycle { ignore_changes = [value] }` — TF makes them
`sales.ct` with `lifecycle { ignore_changes = [value] }` — TF makes them
exist; this service owns their value at runtime.
## Node-side usage
@ -101,13 +101,14 @@ curl -fsS -H "Authorization: Bearer $DNS_UPDATER_TOKEN" \
"https://dns.ct.uvlava.com/nic/update?hostname=live.ct.uvlava.com&myip=$DROPLET_IP"
```
**Prospector node (always-on, on boot + every 5 min)** — install the reusable
client (`client/`), which self-reports the node's public IP (`myip` omitted →
caller IP) via a systemd timer so the record tracks the node across moves:
**Sales node (always-on, on boot + every 5 min)** — the public "sales" surface
is the Prospector PWA node. Install the reusable client (`client/`), which
self-reports the node's public IP (`myip` omitted → caller IP) via a systemd
timer so the record tracks the node across moves:
```bash
# on the node, as root:
DNS_HOSTNAME=prospector.ct.uvlava.com NODE_TOKEN=<token> \
DNS_HOSTNAME=sales.ct.uvlava.com NODE_TOKEN=<token> \
./client/install-client.sh
```
@ -116,7 +117,7 @@ It installs `dyndns-update.sh` + a `dyndns-updater.timer` (OnBootSec + every
```bash
curl -fsS -H "Authorization: Bearer $TOKEN" \
"https://dns.ct.uvlava.com/nic/update?hostname=prospector.ct.uvlava.com"
"https://dns.ct.uvlava.com/nic/update?hostname=sales.ct.uvlava.com"
```
## Local dev

View file

@ -5,7 +5,7 @@
#
# Config: /etc/dyndns-updater/dyndns.conf
# DNS_UPDATER_URL=https://dns.ct.uvlava.com
# DNS_HOSTNAME=prospector.ct.uvlava.com
# DNS_HOSTNAME=sales.ct.uvlava.com
# DNS_TOKEN_FILE=/etc/dyndns-updater/token # file containing the node token
#
# myip is omitted, so the updater uses the caller's observed public IP.

View file

@ -3,13 +3,13 @@
# (e.g. the Prospector PWA node on lime). Run as root ON THE NODE.
#
# Usage:
# DNS_HOSTNAME=prospector.ct.uvlava.com NODE_TOKEN=<token> ./install-client.sh
# DNS_HOSTNAME=sales.ct.uvlava.com NODE_TOKEN=<token> ./install-client.sh
#
# After install the node refreshes its A record on boot and every 5 minutes.
set -euo pipefail
DNS_UPDATER_URL="${DNS_UPDATER_URL:-https://dns.ct.uvlava.com}"
DNS_HOSTNAME="${DNS_HOSTNAME:?set DNS_HOSTNAME, e.g. prospector.ct.uvlava.com}"
DNS_HOSTNAME="${DNS_HOSTNAME:?set DNS_HOSTNAME, e.g. sales.ct.uvlava.com}"
NODE_TOKEN="${NODE_TOKEN:?set NODE_TOKEN to this node dyndns token}"
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

View file

@ -9,4 +9,4 @@ DNS_DOMAIN=uvlava.com
# Token -> allowed-hostname grants. One grant per node; a token may update ONLY
# its listed hosts. Generate strong random tokens (e.g. `openssl rand -hex 24`).
DNS_UPDATER_TOKENS=[{"token":"REPLACE_live_node_token","hosts":["live.ct.uvlava.com"]},{"token":"REPLACE_prospector_node_token","hosts":["prospector.ct.uvlava.com"]}]
DNS_UPDATER_TOKENS=[{"token":"REPLACE_live_node_token","hosts":["live.ct.uvlava.com"]},{"token":"REPLACE_sales_node_token","hosts":["sales.ct.uvlava.com"]}]

View file

@ -2,7 +2,7 @@
"name": "@ct/dns-updater",
"version": "0.1.0",
"private": true,
"description": "dyndns2-compatible DNS updater for *.ct.uvlava.com, backed by the DigitalOcean DNS API. Lets region-mobile nodes (live broadcast relay, prospector PWA) self-update their A record on relocation.",
"description": "dyndns2-compatible DNS updater for *.ct.uvlava.com, backed by the DigitalOcean DNS API. Lets region-mobile nodes (live broadcast relay, sales/prospector PWA) self-update their A record on relocation.",
"type": "module",
"scripts": {
"dev": "bun run --watch src/index.ts",

View file

@ -2,7 +2,7 @@
* dns.ct.uvlava.com dyndns2-compatible DNS updater.
*
* Region-mobile nodes (the per-show broadcast relay = live.ct.uvlava.com, the
* always-on prospector PWA = prospector.ct.uvlava.com) call /nic/update on
* always-on sales surface = sales.ct.uvlava.com) call /nic/update on
* (re)location to repoint their own A record at their current public IP, via
* the DigitalOcean DNS API. The transquinnftw.com pretty-names are static
* CNAMEs onto these, so only the records in this zone ever move.

View file

@ -40,7 +40,7 @@ beforeAll(async () => {
process.env.PORT = "8099";
process.env.DNS_UPDATER_TOKENS = JSON.stringify([
{ token: "live-secret", hosts: ["live.ct.uvlava.com"] },
{ token: "prospector-secret", hosts: ["prospector.ct.uvlava.com"] },
{ token: "sales-secret", hosts: ["sales.ct.uvlava.com"] },
]);
await import("../src/index.ts");
@ -82,7 +82,7 @@ test("wrong token is rejected", async () => {
});
test("token cannot update a host outside its grant", async () => {
const r = await update("hostname=prospector.ct.uvlava.com&myip=1.2.3.4", {
const r = await update("hostname=sales.ct.uvlava.com&myip=1.2.3.4", {
authorization: "Bearer live-secret",
});
expect(r.status).toBe(403);

View file

@ -119,12 +119,13 @@ resource "digitalocean_record" "ct_live" {
}
}
# prospector.ct.uvlava.com -> always-on Prospector PWA node (region-mobile;
# follows the operator). Seeded to the current node (lime / lilith-store-backend).
resource "digitalocean_record" "ct_prospector" {
# sales.ct.uvlava.com -> always-on "sales" surface = the public Prospector PWA
# node (region-mobile; follows the operator). "sales" is the public name; the
# node is the Prospector PWA. Seeded to the current node (lime / lilith-store-backend).
resource "digitalocean_record" "ct_sales" {
domain = digitalocean_domain.uvlava.name
type = "A"
name = "prospector.ct"
name = "sales.ct"
value = "165.227.96.183"
ttl = 60