# Ephemera — Domain registration (Route53 Domains)

> **Self-executing Markdown.** This file is the source of truth for *intent*; the cloud is the source of
> truth for *state*. An agent reads it top-to-bottom, makes the world match it, and writes back what
> actually happened into **Live State**.
>
> **Provides:** `delegated-zone(DOMAIN_NAME)` — a registered domain whose Route53 hosted zone exists and is
> delegated. Any consumer plan that needs managed DNS (e.g. [`web.aws.md`](./web.aws.md) `DNS_MODE=managed`,
> an API Gateway custom domain, an MX/email plan) **Requires** this and discovers it by observing the
> cloud — never by reading this file. Composition is through the cloud, not a shared state ledger.

---

## 🤖 Director prompt — read this first, every run

You are the **Director**. You execute this plan against AWS. Your operating contract:

1. **Read Intent, then Live State.** Live State is a *cache* of what was realized. The cloud is truth —
   when they disagree, believe the cloud and correct Live State.
2. **Observe before you act.** Every step says how to check "does it already look like this?" Run that
   first; act only on the delta. Re-running this plan must be safe.
3. **Advance one verified step at a time.** Run a step → run its `verify` → only then move on.
4. **Stop at every 🔴 and 💥.** Print what you are about to do and *wait for a human "go"*. The
   `register-domain` step **is the purchase** — never auto-run it.
5. **Write back.** After any mutation, record realized IDs in **Live State**; after any `verify`, record
   PASS/FAIL + the observed value. This file is the audit trail.
6. **⏳ steps are async.** Registration is not instant — poll the operation; don't block a shell on it.
7. **Teardown is asymmetric.** You cannot *un-buy* a domain. Teardown is "stop the bleeding + remove what
   is reversible," not "reconcile to gone." That asymmetry is named, not hidden.

```
Legend
🟢 create      · reversible
🟡 config      · mutating, idempotent on re-run
🔴 GATE        · billable/global or slow-to-reverse — HUMAN GO REQUIRED
💥 destructive · irreversible — HUMAN GO REQUIRED
⏳ wait        · async; completion is not immediate — background it
✔ verify       · read-only assertion: "does it look like this?"
✉ out-of-band  · a human action outside any API (e.g. click an email link)
```

---

## Intent

Register a domain through **Route53 Domains** and end with its **hosted zone live and delegated**, so any
downstream plan can attach DNS records to it. Registration is the one cloud action that is genuinely a
*purchase* — billable, ~non-refundable, an ~annual commitment, and bound to ICANN contact + email-
verification rules. This plan automates everything *around* the purchase and stops cold at the purchase
itself.

The payoff specific to Route53: registering **here** closes the delegation gap — `register-domain`
auto-creates the hosted zone and points the domain's nameservers at it, so the zone is born delegated. (If
the registrar were elsewhere, delegation would be a manual step at that registrar; that case is
[`domain.cloudflare.md`](./domain.cloudflare.md)'s territory and the external-DNS path of consumer plans.)

---

## Provisioning Inputs

> The Director resolves these **once**, up front, before any cloud mutation — walk the rows, confirm each
> with the human (accept the **default** on silence), then write `resolved_inputs` into Live State. Every
> option is a closed enum (bar the free-text domain). Contacts are PII the human supplies in a separate
> `contacts.json` (registrant/admin/tech) — never inlined here.

| # | Question | Options (closed enum) | Default | Sets | Gates |
|---|----------|-----------------------|---------|------|-------|
| 1 | Domain name | text — an apex, e.g. `example.com` | — | `DOMAIN_NAME` | the whole plan |
| 2 | Registration term (years) | `1` … `10` | `1` | `YEARS` | `register-domain` duration |
| 3 | Auto-renew? | `on` / `off` | `on` | `AUTO_RENEW` | renewal + how teardown releases it |
| 4 | WHOIS privacy? | `on` / `off` | `on` | `PRIVACY` | privacy flags on `register-domain` |

---

## Live State

> The Director writes here. `—` means "not yet realized / unknown." Treat as a cache of the cloud.

```yaml
status:        not-created      # published template - run it to realize state
last_action:   —
last_verified: —

resolved_inputs:
  domain_name: —
  years:       1
  auto_renew:  on            # on | off
  privacy:     on            # on | off
```

| key            | value |
|----------------|-------|
| DOMAINS_REGION | `us-east-1`  *(the Route53 Domains API endpoint — global service, us-east-1 only)* |
| DOMAIN_NAME    | — |
| OP_ID          | — *(register-domain async operation id)* |
| HOSTED_ZONE_ID | — *(auto-created + delegated by register-domain)* |
| NAMESERVERS    | — |
| EXPIRES_ON     | — |

**Verify ledger** (most recent run):

| ✔ check                       | expected                         | observed | result |
|-------------------------------|----------------------------------|----------|--------|
| availability (pre-purchase)   | `AVAILABLE`                      | —        | —      |
| registration operation        | `SUCCESSFUL`                     | —        | —      |
| registrant email reachability | `DONE`                           | —        | —      |
| zone delegated                | domain NS == hosted-zone NS      | —        | —      |

---

## 0. Variables

```bash
export DOMAINS_REGION="us-east-1"      # route53domains API endpoint is us-east-1 ONLY
# Resolved from Provisioning Inputs:
export DOMAIN_NAME=""                   # e.g. example.com (an apex/registrable name)
export YEARS="1"                        # 1..10
export AUTO_RENEW="on"                  # on | off
export PRIVACY="on"                     # on | off
export CONTACTS_FILE="./contacts.json"  # PII the human supplies: {AdminContact,RegistrantContact,TechContact}
# Realized later — the Director rehydrates from Live State or re-derives from the cloud:
export OP_ID="" HOSTED_ZONE_ID=""
```

`contacts.json` is the ICANN-required registrant/admin/tech contact set (name, address, email, phone,
contact type). Copy [`contacts.example.json`](./contacts.example.json) → `contacts.json` and fill in real
values. It's PII, not infra — already listed in `.arcignore` so the daemon never snapshots it. The
ignore entry exists *before* the real file, on purpose: Arc captures on every edit, so an un-ignored
`contacts.json` would journal your address into the substrate.

---

## Dependency frontier (why order matters)

```
check-availability ─> list-prices ─> contacts.json ready ─┐
                                                           ▼
                                          🔴 register-domain  (THE purchase)
                                                           │
                                       ⏳ operation SUCCESSFUL
                                                           │
                              ✉ ICANN registrant-email verify (human, out-of-band)
                                                           │
                         zone auto-created + delegated ─> ✔ Provides delegated-zone(DOMAIN_NAME)
```

Non-negotiable edges: **nothing bills before the gate**; the **email verification is a human action no API
can perform**; the zone is **Provided only once its NS match the registered domain's NS** (proof of
delegation), which is what a consumer plan relies on.

---

## 1. Availability  ✔ (safe)

```bash
# observe: is the name even buyable? (read-only, free)
aws route53domains check-domain-availability --region "$DOMAINS_REGION" \
  --domain-name "$DOMAIN_NAME" --query 'Availability' --output text
# expect: AVAILABLE   (UNAVAILABLE / RESERVED / etc. → stop, pick another name)
```
> **→ write Live State:** verify row "availability".

---

## 2. Price  ✔ (safe)

```bash
# what a year of this TLD costs, so the human goes into the gate with eyes open
aws route53domains list-prices --region "$DOMAINS_REGION" --tld "${DOMAIN_NAME##*.}" \
  --query 'Prices[0].{Register:RegistrationPrice,Renew:RenewalPrice}' --output json
```

---

## 3. Register the domain  🔴 GATE — *this is the purchase*

> 🔴 **STOP. Human go required.** `register-domain` **bills the AWS account immediately**, is generally
> **non-refundable**, and commits you for `$YEARS` year(s). There is no separate "pay" step to skip — this
> call *is* the payment. Print the exact request (domain, years, auto-renew, privacy) and wait for "go".

```bash
# observe: already registered to this account? then skip the purchase entirely.
if aws route53domains get-domain-detail --region "$DOMAINS_REGION" --domain-name "$DOMAIN_NAME" >/dev/null 2>&1; then
  echo "already registered to this account — skip purchase"
else
  # assemble the register request: resolved inputs + the human's contacts.json (PII stays in that file)
  jq -n --arg d "$DOMAIN_NAME" --argjson y "$YEARS" \
    --argjson ar "$( [ "$AUTO_RENEW" = on ] && echo true || echo false )" \
    --argjson pp "$( [ "$PRIVACY" = on ] && echo true || echo false )" \
    --slurpfile c "$CONTACTS_FILE" \
    '{DomainName:$d, DurationInYears:$y, AutoRenew:$ar,
      PrivacyProtectAdminContact:$pp, PrivacyProtectRegistrantContact:$pp, PrivacyProtectTechContact:$pp,
      AdminContact:$c[0].AdminContact, RegistrantContact:$c[0].RegistrantContact, TechContact:$c[0].TechContact}' \
    > /tmp/pf-register.json

  # 🔴 THE PURCHASE — never auto-run.
  OP_ID="$(aws route53domains register-domain --region "$DOMAINS_REGION" \
    --cli-input-json file:///tmp/pf-register.json --query OperationId --output text)"
  echo "OP_ID=$OP_ID"
fi
```
> **→ write Live State:** `OP_ID`; `status: registering`.

```bash
# ⏳ WAIT — registration is async (minutes; some TLDs longer). Poll until terminal.
while :; do
  S="$(aws route53domains get-operation-detail --region "$DOMAINS_REGION" \
        --operation-id "$OP_ID" --query 'Status' --output text)"
  echo "op=$S"; case "$S" in SUCCESSFUL) break;; ERROR|FAILED) echo "registration failed"; break;; esac
  sleep 30
done
```

```bash
# ✔ verify — operation succeeded
aws route53domains get-operation-detail --region "$DOMAINS_REGION" --operation-id "$OP_ID" \
  --query 'Status' --output text   # expect: SUCCESSFUL
```
> **→ write Live State:** verify row "registration operation"; `status: pending-verify`.

---

## 4. ICANN registrant-email verification  ✉ 🔴 (human, out-of-band)

> ICANN requires the **registrant email be verified** — typically within ~15 days, or the domain is
> *suspended*. Cloudflare-style automation can't click the link for you; this is the one irreducibly-human
> step. Emit the address, point the human at the email, and pause.

```bash
aws route53domains get-contact-reachability-status --region "$DOMAINS_REGION" \
  --domain-name "$DOMAIN_NAME" --query 'status' --output text
# PENDING → tell the human to click the verification email (resend with the command below if lost)
# DONE    → verified, continue
# EXPIRED → resend, then have them click

# (re)send the verification email if needed:
# aws route53domains resend-contact-reachability-email --region "$DOMAINS_REGION" --domain-name "$DOMAIN_NAME"
```
> **→ 🔴 PAUSE** until reachability reads `DONE`. **→ write Live State:** verify row "registrant email".

---

## 5. Zone delegation — the Provides contract  ✔

```bash
# register-domain auto-creates a hosted zone and points the domain's NS at it. Confirm both, and that they MATCH.
HOSTED_ZONE_ID="$(aws route53 list-hosted-zones-by-name --dns-name "$DOMAIN_NAME" \
  --query "HostedZones[?Name=='${DOMAIN_NAME}.'].Id | [0]" --output text | sed 's#/hostedzone/##')"
echo "HOSTED_ZONE_ID=$HOSTED_ZONE_ID"

DOMAIN_NS="$(aws route53domains get-domain-detail --region "$DOMAINS_REGION" --domain-name "$DOMAIN_NAME" \
  --query 'Nameservers[].Name' --output json | jq -S 'map(ascii_downcase)')"
ZONE_NS="$(aws route53 get-hosted-zone --id "$HOSTED_ZONE_ID" \
  --query 'DelegationSet.NameServers' --output json | jq -S 'map(ascii_downcase)')"

[ "$DOMAIN_NS" = "$ZONE_NS" ] && echo "delegated: domain NS == zone NS" || echo "NOT delegated yet — NS mismatch"
```
> **→ write Live State:** `HOSTED_ZONE_ID`, `NAMESERVERS`, `EXPIRES_ON`; verify row "zone delegated";
> `status: live`. **Provides `delegated-zone(DOMAIN_NAME)`** — consumer plans may now discover and use it.

---

## ✔ Reconcile / drift check (run anytime after go-live)

"Does it still look like this?" Re-run §4 (reachability `DONE`) and §5 (zone exists + NS still match).
Also confirm the registration hasn't lapsed:

```bash
aws route53domains get-domain-detail --region "$DOMAINS_REGION" --domain-name "$DOMAIN_NAME" \
  --query '{Expiry:ExpirationDate,AutoRenew:AutoRenew,Status:StatusList}' --output json
```

---

## Teardown — the asymmetry  💥

> 💥 **Human go required — and read this first.** You **cannot un-buy a domain.** There is no "reconcile to
> gone" for a registration: the money is spent and the name is yours until it expires. Teardown here means
> the *reversible* parts only. Be explicit with the human about what will and won't happen.

```bash
# 1. Stop future spend: disable auto-renew so the registration LAPSES at end of term (the normal "release").
[ "$AUTO_RENEW" = "off" ] || aws route53domains disable-domain-auto-renew \
  --region "$DOMAINS_REGION" --domain-name "$DOMAIN_NAME"
echo "auto-renew disabled — domain will lapse at ${EXPIRES_ON:-its expiry}"

# 2. (optional) delete the hosted zone — ONLY if no consumer plan still needs it AND it holds nothing but
#    the default NS/SOA (delete-hosted-zone fails otherwise). Deleting the zone breaks delegation immediately.
#    aws route53 delete-hosted-zone --id "$HOSTED_ZONE_ID"

# 3. (where supported) delete-domain removes the registration for some TLDs — NO refund, not all TLDs.
#    This is the closest thing to "gone," and it is still a commitment you paid for. 💥
#    aws route53domains delete-domain --region "$DOMAINS_REGION" --domain-name "$DOMAIN_NAME"
```
> **→ write Live State:** `status: lapsing` (auto-renew off; registration persists until expiry) or `gone`
> only if a `delete-domain` actually succeeded. Never claim `gone` while the registration still exists.

```bash
# ✔ verify teardown — auto-renew is off (the honest, reversible outcome)
aws route53domains get-domain-detail --region "$DOMAINS_REGION" --domain-name "$DOMAIN_NAME" \
  --query 'AutoRenew' --output text   # expect: False
```

---

## Deliberately *not* in this plan (named, so the omission is a decision)

- **The purchase is the floor.** Everything up to §3 is safe; §3 is the irreversible commitment. The plan
  will not, and structurally cannot, "do it minus payment" — declining the gate *is* not paying.
- **Domain transfer in/out** — moving an existing registration between registrars (`transfer-domain` /
  `transfer-domain-to-another-aws-account`). Separate intent.
- **DNSSEC** — `associate-delegation-signer-to-domain` + zone signing. Add for real production.
- **Glue / vanity nameservers**, multi-year auto-renew strategy, registrar locks beyond defaults.
- **Refund/cancellation** — does not exist for registrations; see Teardown's asymmetry.
