| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816 |
- #!/usr/bin/env bash
- # fetch-tlds.sh — Scrape purchasable TLD lists from registrar APIs
- # Outputs clean sorted lists for use in Lists.toml
- #
- # Usage:
- # ./scripts/fetch-tlds.sh # fetch all, show summary
- # ./scripts/fetch-tlds.sh porkbun # porkbun only
- # ./scripts/fetch-tlds.sh inwx # inwx only
- # ./scripts/fetch-tlds.sh --raw # output raw TLD lists (one per line)
- # ./scripts/fetch-tlds.sh --toml # output TOML-ready arrays
- # ./scripts/fetch-tlds.sh --diff # compare against current Lists.toml
- # ./scripts/fetch-tlds.sh --template # generate full Lists.toml with whois overrides if necessary
- #
- # Notes : yea this is ai slop, didnt make it myself oooo scary, but most of the rust i did myself just didnt feel like doing this at 4am and it somewhat works
- set -euo pipefail
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
- PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
- LISTS_TOML="$PROJECT_DIR/Lists.toml"
- CACHE_DIR="$PROJECT_DIR/.tld-cache"
- mkdir -p "$CACHE_DIR"
- RED='\033[0;31m'
- GREEN='\033[0;32m'
- YELLOW='\033[1;33m'
- CYAN='\033[0;36m'
- BOLD='\033[1m'
- NC='\033[0m'
- # ─── Porkbun ────────────────────────────────────────────────────────────────
- fetch_porkbun() {
- local cache="$CACHE_DIR/porkbun.json"
- local max_age=86400 # 24h cache
- if [[ -f "$cache" ]]; then
- local age=$(( $(date +%s) - $(stat -f%m "$cache" 2>/dev/null || stat -c%Y "$cache" 2>/dev/null || echo 0) ))
- if (( age < max_age )); then
- echo "$cache"
- return 0
- fi
- fi
- echo -e "${CYAN}Fetching Porkbun pricing API...${NC}" >&2
- # abusing porkbun public no money needed ah endpoint is no API key is even needed
- if curl -sf -X POST "https://api.porkbun.com/api/json/v3/pricing/get" \
- -H "Content-Type: application/json" \
- -d '{}' \
- -o "$cache" 2>/dev/null; then
- echo "$cache"
- else
- echo -e "${RED}Failed to fetch Porkbun data${NC}" >&2
- return 1
- fi
- }
- parse_porkbun() {
- local json_file="$1"
- # Extract TLD keys from the pricing response
- # Response format: {"status":"SUCCESS","pricing":{"com":{...},"net":{...},...}}
- if command -v jq &>/dev/null; then
- jq -r '.pricing // {} | keys[]' "$json_file" 2>/dev/null | sort -u
- else
- # Fallback: grep for TLD keys (less reliable but works)
- grep -o '"[a-z][a-z0-9.-]*":{' "$json_file" | sed 's/"//g; s/:{//' | sort -u
- fi
- }
- # ─── INWX ───────────────────────────────────────────────────────────────────
- fetch_inwx() {
- local cache="$CACHE_DIR/inwx.html"
- local max_age=86400
- if [[ -f "$cache" ]]; then
- local age=$(( $(date +%s) - $(stat -f%m "$cache" 2>/dev/null || stat -c%Y "$cache" 2>/dev/null || echo 0) ))
- if (( age < max_age )); then
- echo "$cache"
- return 0
- fi
- fi
- echo -e "${CYAN}Fetching INWX domain list...${NC}" >&2
- # INWX domain check page has TLD list embedded as JSON
- if curl -sfL "https://www.inwx.de/en/domain/check" \
- -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)" \
- -o "$cache" 2>/dev/null; then
- echo "$cache"
- else
- echo -e "${YELLOW}Could not fetch INWX${NC}" >&2
- return 1
- fi
- }
- parse_inwx() {
- local html_file="$1"
- # TLDs are embedded as JSON objects with "tld":"xxx" in the page
- grep -oE '"tld":"[a-z]{2,20}"' "$html_file" | sed 's/"tld":"//;s/"//' | sort -u
- }
- # ─── OVH ────────────────────────────────────────────────────────────────────
- fetch_ovh() {
- local cache="$CACHE_DIR/ovh.json"
- local max_age=86400
- if [[ -f "$cache" ]]; then
- local age=$(( $(date +%s) - $(stat -f%m "$cache" 2>/dev/null || stat -c%Y "$cache" 2>/dev/null || echo 0) ))
- if (( age < max_age )); then
- echo "$cache"
- return 0
- fi
- fi
- echo -e "${CYAN}Fetching OVH domain extensions...${NC}" >&2
- if curl -sf "https://www.ovh.com/engine/apiv6/domain/extensions" \
- -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)" \
- -o "$cache" 2>/dev/null; then
- echo "$cache"
- else
- echo -e "${YELLOW}Could not fetch OVH extensions${NC}" >&2
- return 1
- fi
- }
- parse_ovh() {
- local json_file="$1"
- if command -v jq &>/dev/null; then
- # Only top-level TLDs (no dots = not sub-TLDs like com.au)
- jq -r '.[]' "$json_file" 2>/dev/null | grep -vE '\.' | sort -u
- else
- grep -oE '"[a-z]{2,20}"' "$json_file" | tr -d '"' | grep -vE '\.' | sort -u
- fi
- }
- # ─── tld-list.com (comprehensive registry, free basic list) ────────────────
- fetch_tldlist() {
- local cache="$CACHE_DIR/tldlist-basic.txt"
- local max_age=86400
- if [[ -f "$cache" ]]; then
- local age=$(( $(date +%s) - $(stat -f%m "$cache" 2>/dev/null || stat -c%Y "$cache" 2>/dev/null || echo 0) ))
- if (( age < max_age )); then
- echo "$cache"
- return 0
- fi
- fi
- echo -e "${CYAN}Fetching tld-list.com basic list...${NC}" >&2
- if curl -sf "https://tld-list.com/df/tld-list-basic.csv" \
- -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)" \
- -o "$cache" 2>/dev/null; then
- echo "$cache"
- else
- echo -e "${YELLOW}Could not fetch tld-list.com${NC}" >&2
- return 1
- fi
- }
- parse_tldlist() {
- local file="$1"
- # One TLD per line, CR/LF endings, includes IDN entries — filter to ASCII only
- tr -d '\r' < "$file" | grep -E '^[a-z][a-z0-9]*$' | sort -u
- }
- # ─── IANA root zone (fallback) ──────────────────────────────────────────────
- fetch_iana() {
- local cache="$CACHE_DIR/iana-tlds.txt"
- local max_age=604800 # 7 days
- if [[ -f "$cache" ]]; then
- local age=$(( $(date +%s) - $(stat -f%m "$cache" 2>/dev/null || stat -c%Y "$cache" 2>/dev/null || echo 0) ))
- if (( age < max_age )); then
- echo "$cache"
- return 0
- fi
- fi
- echo -e "${CYAN}Fetching IANA TLD list...${NC}" >&2
- if curl -sf "https://data.iana.org/TLD/tlds-alpha-by-domain.txt" -o "$cache" 2>/dev/null; then
- echo "$cache"
- else
- echo -e "${RED}Failed to fetch IANA list${NC}" >&2
- return 1
- fi
- }
- parse_iana() {
- local file="$1"
- # Skip header line, lowercase everything, filter to 2-3 char ccTLDs
- tail -n +2 "$file" | tr '[:upper:]' '[:lower:]' | sort -u
- }
- parse_iana_cctlds() {
- local file="$1"
- tail -n +2 "$file" | tr '[:upper:]' '[:lower:]' | grep -E '^[a-z]{2}$' | sort -u
- }
- # ─── RDAP bootstrap (what actually has lookup servers) ──────────────────────
- fetch_rdap() {
- local cache="$CACHE_DIR/rdap-dns.json"
- local max_age=86400
- if [[ -f "$cache" ]]; then
- local age=$(( $(date +%s) - $(stat -f%m "$cache" 2>/dev/null || stat -c%Y "$cache" 2>/dev/null || echo 0) ))
- if (( age < max_age )); then
- echo "$cache"
- return 0
- fi
- fi
- echo -e "${CYAN}Fetching RDAP bootstrap...${NC}" >&2
- if curl -sf "https://data.iana.org/rdap/dns.json" -o "$cache" 2>/dev/null; then
- echo "$cache"
- else
- echo -e "${RED}Failed to fetch RDAP bootstrap${NC}" >&2
- return 1
- fi
- }
- parse_rdap_tlds() {
- local json_file="$1"
- if command -v jq &>/dev/null; then
- jq -r '.services[][] | .[]' "$json_file" 2>/dev/null | grep -v '^http' | tr '[:upper:]' '[:lower:]' | sort -u
- else
- grep -oE '"[a-z]{2,20}"' "$json_file" | tr -d '"' | sort -u
- fi
- }
- # ─── WHOIS server list (rfc1036/whois project) ─────────────────────────────
- fetch_whois_servers() {
- local cache="$CACHE_DIR/tld_serv_list.txt"
- local max_age=604800 # 7 days
- if [[ -f "$cache" ]]; then
- local age=$(( $(date +%s) - $(stat -f%m "$cache" 2>/dev/null || stat -c%Y "$cache" 2>/dev/null || echo 0) ))
- if (( age < max_age )); then
- echo "$cache"
- return 0
- fi
- fi
- echo -e "${CYAN}Fetching WHOIS server list...${NC}" >&2
- if curl -sf "https://raw.githubusercontent.com/rfc1036/whois/next/tld_serv_list" -o "$cache" 2>/dev/null; then
- echo "$cache"
- else
- echo -e "${YELLOW}Could not fetch WHOIS server list${NC}" >&2
- return 1
- fi
- }
- # Get the WHOIS server for a given TLD from the cached server list
- # Returns empty string if no server found or server is NONE/ARPA/etc
- get_whois_server() {
- local tld="$1"
- local serv_file="$2"
- # Format: .tld [optional-tag] server
- # Some entries have VERISIGN or similar tag before the server
- local line
- line=$(grep -E "^\\.${tld}[[:space:]]" "$serv_file" 2>/dev/null | head -1)
- if [[ -z "$line" ]]; then
- echo ""
- return
- fi
- # Extract server: last word on the line that looks like a hostname
- local server
- server=$(echo "$line" | awk '{
- for (i=NF; i>=2; i--) {
- if ($i ~ /^[a-z0-9].*\.[a-z]/) { print $i; exit }
- }
- }')
- # Filter out unusable entries
- if [[ "$server" == "NONE" || "$server" == "ARPA" || -z "$server" || "$server" == http* ]]; then
- echo ""
- else
- echo "$server"
- fi
- }
- # Get WHOIS server from IANA directly (slower, single TLD at a time)
- get_iana_whois_server() {
- local tld="$1"
- curl -s "https://www.iana.org/domains/root/db/${tld}.html" 2>/dev/null \
- | sed -n 's/.*WHOIS Server:<\/b> *\([^ <]*\).*/\1/p' \
- | head -1
- }
- # ─── Extract current Lists.toml entries ─────────────────────────────────────
- parse_current_lists() {
- local list_name="${1:-all}"
- if [[ ! -f "$LISTS_TOML" ]]; then
- echo -e "${RED}No Lists.toml found at $LISTS_TOML${NC}" >&2
- return 1
- fi
- # Extract TLDs from a named list, stripping quotes, colons (whois overrides), commas
- awk -v list="$list_name" '
- $0 ~ "^"list" *= *\\[" { found=1; next }
- found && /^\]/ { exit }
- found && /^[[:space:]]*\[/ { exit }
- found {
- gsub(/["\t,]/, " ")
- n = split($0, parts, " ")
- for (i=1; i<=n; i++) {
- if (parts[i] != "") {
- # Strip whois override suffix
- sub(/:.*/, "", parts[i])
- print parts[i]
- }
- }
- }
- ' "$LISTS_TOML" | sort -u
- }
- # ─── Helpers ────────────────────────────────────────────────────────────────
- to_toml_array() {
- # Reads TLDs from stdin, outputs TOML array format (wrapped at ~80 chars)
- local tlds=()
- while IFS= read -r tld; do
- [[ -z "$tld" ]] && continue
- tlds+=("$tld")
- done
- local line='\t'
- local first=true
- for tld in "${tlds[@]}"; do
- local entry="\"$tld\""
- if $first; then
- line+="$entry"
- first=false
- else
- local test_line="$line, $entry"
- if (( ${#test_line} > 78 )); then
- echo -e "$line,"
- line="\t$entry"
- else
- line+=", $entry"
- fi
- fi
- done
- [[ -n "$line" ]] && echo -e "$line,"
- }
- filter_cctlds() {
- grep -E '^[a-z]{2}$'
- }
- filter_short_tlds() {
- # 2-6 char TLDs that are useful for domain hacking
- grep -E '^[a-z]{2,6}$'
- }
- # ─── Known broken/unregistrable TLDs ────────────────────────────────────────
- SKIP_TLDS="bl bq eh mf gb bv sj kp hm"
- filter_skip() {
- local skip_pattern
- skip_pattern=$(echo "$SKIP_TLDS" | tr ' ' '|')
- grep -vE "^($skip_pattern)$"
- }
- # ─── Template generation ────────────────────────────────────────────────────
- # Generates a full Lists.toml with:
- # - "tld" for TLDs with RDAP support (direct lookup works)
- # - "tld:whois.server" for TLDs needing WHOIS fallback
- # - skip TLDs omitted entirely (no Patch.toml needed)
- #
- # Uses: Porkbun + OVH + INWX (purchasable), RDAP bootstrap (has server?), WHOIS server list
- # With --all-sources: also cross-references tld-list.com
- generate_template() {
- local all_registrar_tlds="$1"
- local rdap_tlds="$2"
- local source_summary="$3"
- # Fetch WHOIS server list for fallback
- local whois_serv_file=""
- if whois_serv_file=$(fetch_whois_servers 2>/dev/null); then
- true # got it
- fi
- # The input is already merged + filtered from all registrar sources
- local buyable_tlds
- buyable_tlds=$(echo "$all_registrar_tlds" | filter_skip | sort -u)
- local buyable_count
- buyable_count=$(echo "$buyable_tlds" | grep -c . || echo 0)
- # Build annotated TLD list: "tld" or "tld:whois_server"
- # A TLD needs a whois override if it's NOT in the RDAP bootstrap
- local annotated_all=()
- local annotated_cc=()
- local rdap_hit=0 whois_hit=0 bare_hit=0
- while IFS= read -r tld; do
- [[ -z "$tld" ]] && continue
- local entry=""
- if echo "$rdap_tlds" | grep -qx "$tld" 2>/dev/null; then
- # Has RDAP server — no override needed
- entry="$tld"
- ((rdap_hit++)) || true
- else
- # No RDAP — try to find WHOIS server
- local server=""
- if [[ -n "$whois_serv_file" ]]; then
- server=$(get_whois_server "$tld" "$whois_serv_file")
- fi
- if [[ -n "$server" ]]; then
- entry="${tld}:${server}"
- ((whois_hit++)) || true
- else
- # No known server — include bare, hoardom will try common patterns
- entry="$tld"
- ((bare_hit++)) || true
- fi
- fi
- annotated_all+=("$entry")
- # Also track ccTLDs (2-letter entries)
- local base_tld="${tld%%:*}"
- if [[ "$base_tld" =~ ^[a-z]{2}$ ]]; then
- annotated_cc+=("$entry")
- fi
- done <<< "$buyable_tlds"
- echo -e "${CYAN}Building template...${NC}" >&2
- echo -e " ${GREEN}${rdap_hit}${NC} TLDs with RDAP (direct lookup)" >&2
- echo -e " ${YELLOW}${whois_hit}${NC} TLDs with WHOIS override" >&2
- echo -e " ${RED}${bare_hit}${NC} TLDs with no known server (will probe)" >&2
- echo "" >&2
- # ── Curated lists (bare TLD names, annotated automatically) ─────────
- # Standard: com, net, org + generally desirable / well-known TLDs
- local standard_tlds=(
- "com" "net" "org" "io" "co" "dev" "app" "me" "info"
- "biz" "one" "xyz" "online" "site" "tech" "pro" "tv"
- "cc" "to" "sh" "li" "fm" "am" "gg" "ws" "la"
- "ms" "nu" "cx" "mn" "st" "tel" "ai" "id" "in"
- "it" "is" "at" "be" "de" "eu" "fr" "nl" "se"
- "uk" "us" "ca" "au" "nz" "club" "blog" "art" "fun"
- "lol" "wtf" "page" "link" "space" "store" "shop"
- )
- # Decent: the best of the best — com, net, org, io + short desirable ones
- # that work great for domain hacking and are punchy
- local decent_tlds=(
- "com" "net" "org" "io" "dev" "app" "co" "me"
- "ai" "sh" "to" "fm" "tv" "gg" "cc" "li" "am"
- "la" "nu" "id" "in" "it" "is" "at" "ws"
- "one" "pro" "bio" "art" "ink" "run" "win" "new"
- "lol" "pub" "fun" "vet" "fit" "rip" "wtf" "zip"
- )
- # Swiss: standard-like but with Swiss / Central European related TLDs up front
- local swiss_tlds=(
- "com" "net" "org" "ch" "li" "swiss" "zuerich"
- "io" "co" "dev" "app" "me" "info" "one" "pro"
- "de" "at" "fr" "it" "eu"
- "tech" "online" "site" "shop" "store"
- "biz" "xyz" "tv" "cc" "to" "sh" "fm" "am" "gg"
- )
- # Annotate curated lists with whois overrides where needed
- annotate_list() {
- local -n input_list=$1
- local result=()
- for bare_tld in "${input_list[@]}"; do
- local found=false
- for ann in "${annotated_all[@]}"; do
- local ann_base="${ann%%:*}"
- if [[ "$ann_base" == "$bare_tld" ]]; then
- result+=("$ann")
- found=true
- break
- fi
- done
- if ! $found; then
- result+=("$bare_tld")
- fi
- done
- printf '%s\n' "${result[@]}"
- }
- # Length-based filtered lists from annotated_all
- filter_annotated_by_length() {
- local min="$1"
- local max="$2"
- for ann in "${annotated_all[@]}"; do
- local base="${ann%%:*}"
- local len=${#base}
- if (( len >= min && len <= max )); then
- echo "$ann"
- fi
- done
- }
- # ─── Output ─────────────────────────────────────────────────────────
- local date_str
- date_str=$(date +%Y-%m-%d)
- cat <<HEADER
- # Lists.toml — Built-in TLD lists for hoardom
- # Auto-generated on ${date_str} from ${source_summary}
- #
- # Format:
- # "tld" — TLD has RDAP support, lookup works directly
- # "tld:whois.server" — No RDAP: use this WHOIS server for fallback
- #
- # ${buyable_count} purchasable TLDs (handshake/sub-TLDs excluded)
- # ${rdap_hit} have RDAP, ${whois_hit} need WHOIS override, ${bare_hit} will auto-probe
- #
- # Lists:
- # standard — common desirable TLDs (com, net, org, io, dev, ...)
- # decent — very best short punchy TLDs for domain hacking
- # swiss — standard-like but with Swiss/Central European TLDs prioritized
- # country — all 2-letter country-code TLDs
- # two — all 2-letter TLDs
- # three — all TLDs with 3 or fewer letters
- # four — all TLDs with exactly 4 letters
- # long — all TLDs with 5+ letters
- # all — everything
- HEADER
- echo "standard = ["
- annotate_list standard_tlds | to_toml_array
- echo "]"
- echo ""
- echo "decent = ["
- annotate_list decent_tlds | to_toml_array
- echo "]"
- echo ""
- echo "swiss = ["
- annotate_list swiss_tlds | to_toml_array
- echo "]"
- echo ""
- echo "country = ["
- printf '%s\n' "${annotated_cc[@]}" | to_toml_array
- echo "]"
- echo ""
- echo "two = ["
- filter_annotated_by_length 2 2 | to_toml_array
- echo "]"
- echo ""
- echo "three = ["
- filter_annotated_by_length 2 3 | to_toml_array
- echo "]"
- echo ""
- echo "four = ["
- filter_annotated_by_length 4 4 | to_toml_array
- echo "]"
- echo ""
- echo "long = ["
- filter_annotated_by_length 5 99 | to_toml_array
- echo "]"
- echo ""
- echo "all = ["
- printf '%s\n' "${annotated_all[@]}" | to_toml_array
- echo "]"
- }
- # ─── Main ───────────────────────────────────────────────────────────────────
- main() {
- local mode="summary"
- local source="all"
- local all_sources=false
- for arg in "$@"; do
- case "$arg" in
- --raw) mode="raw" ;;
- --toml) mode="toml" ;;
- --diff) mode="diff" ;;
- --template) mode="template" ;;
- --all-sources) all_sources=true ;;
- porkbun) source="porkbun" ;;
- inwx) source="inwx" ;;
- ovh) source="ovh" ;;
- iana) source="iana" ;;
- rdap) source="rdap" ;;
- tldlist) source="tldlist" ;;
- --help|-h)
- echo "Usage: $0 [source] [--raw|--toml|--diff|--template] [--all-sources]"
- echo ""
- echo "Sources: porkbun, ovh, inwx, iana, rdap, tldlist"
- echo ""
- echo "Flags:"
- echo " --raw Output raw TLD list (one per line)"
- echo " --toml Output TOML-ready arrays"
- echo " --diff Compare against current Lists.toml"
- echo " --template Generate full Lists.toml with whois overrides"
- echo " --all-sources Include tld-list.com for extra coverage (used as"
- echo " a filter: only TLDs also in a registrar are kept)"
- exit 0 ;;
- esac
- done
- local porkbun_tlds="" inwx_tlds="" ovh_tlds="" iana_tlds="" rdap_tlds="" tldlist_tlds=""
- local porkbun_count=0 inwx_count=0 ovh_count=0 iana_count=0 rdap_count=0 tldlist_count=0
- # Template mode needs all registrar sources + rdap regardless of source filter
- if [[ "$mode" == "template" ]]; then
- source="all"
- fi
- # ── Fetch from selected sources ──
- if [[ "$source" == "all" || "$source" == "porkbun" ]]; then
- if porkbun_file=$(fetch_porkbun); then
- porkbun_tlds=$(parse_porkbun "$porkbun_file")
- porkbun_count=$(echo "$porkbun_tlds" | grep -c . || true)
- fi
- fi
- if [[ "$source" == "all" || "$source" == "ovh" ]]; then
- if ovh_file=$(fetch_ovh); then
- ovh_tlds=$(parse_ovh "$ovh_file")
- ovh_count=$(echo "$ovh_tlds" | grep -c . || true)
- fi
- fi
- if [[ "$source" == "all" || "$source" == "inwx" ]]; then
- if inwx_file=$(fetch_inwx 2>/dev/null); then
- inwx_tlds=$(parse_inwx "$inwx_file")
- inwx_count=$(echo "$inwx_tlds" | grep -c . || true)
- fi
- fi
- if [[ "$source" == "all" || "$source" == "iana" ]]; then
- if iana_file=$(fetch_iana); then
- iana_tlds=$(parse_iana "$iana_file")
- iana_count=$(echo "$iana_tlds" | grep -c . || true)
- fi
- fi
- if [[ "$source" == "all" || "$source" == "rdap" ]]; then
- if rdap_file=$(fetch_rdap); then
- rdap_tlds=$(parse_rdap_tlds "$rdap_file")
- rdap_count=$(echo "$rdap_tlds" | grep -c . || true)
- fi
- fi
- if [[ "$all_sources" == true || "$source" == "tldlist" ]]; then
- if tldlist_file=$(fetch_tldlist); then
- tldlist_tlds=$(parse_tldlist "$tldlist_file")
- tldlist_count=$(echo "$tldlist_tlds" | grep -c . || true)
- fi
- fi
- # ── Filter porkbun: no handshake, no sub-TLDs ──
- local porkbun_filtered=""
- if [[ -n "$porkbun_tlds" ]]; then
- local porkbun_file="$CACHE_DIR/porkbun.json"
- if command -v jq &>/dev/null && [[ -f "$porkbun_file" ]]; then
- porkbun_filtered=$(jq -r '
- .pricing // {} | to_entries[] |
- select(.key | contains(".") | not) |
- select(.value.specialType // "" | test("handshake") | not) |
- .key
- ' "$porkbun_file" 2>/dev/null | sort -u)
- else
- porkbun_filtered=$(echo "$porkbun_tlds" | grep -v '\.' | sort -u)
- fi
- fi
- # ── Merge all registrar-confirmed purchasable TLDs ──
- # Only TLDs that have pricing at a real registrar are included
- local registrar_tlds
- registrar_tlds=$(echo -e "${porkbun_filtered}\n${ovh_tlds}\n${inwx_tlds}" | grep -E '^[a-z]' | sort -u | filter_skip)
- # If --all-sources, also include tld-list.com TLDs that appear in at least
- # one registrar (cross-reference = purchasable + known to community list)
- if [[ "$all_sources" == true && -n "$tldlist_tlds" ]]; then
- # tld-list.com entries that are ALSO in a registrar = confirmed purchasable
- local tldlist_confirmed
- tldlist_confirmed=$(comm -12 <(echo "$tldlist_tlds") <(echo "$registrar_tlds") 2>/dev/null || true)
- # They're already in registrar_tlds, so this just validates.
- # More useful: tld-list entries NOT in any registrar = brand/reserved (skip them)
- local tldlist_extra
- tldlist_extra=$(comm -23 <(echo "$tldlist_tlds") <(echo "$registrar_tlds") 2>/dev/null || true)
- local extra_count
- extra_count=$(echo "$tldlist_extra" | grep -c . || echo 0)
- echo -e " ${YELLOW}tld-list.com:${NC} $extra_count TLDs with no registrar pricing (brand/reserved, excluded)" >&2
- fi
- local all_tlds="$registrar_tlds"
- local all_cctlds
- all_cctlds=$(echo "$all_tlds" | filter_cctlds)
- # Build source summary string for template header
- local sources_used=()
- [[ $porkbun_count -gt 0 ]] && sources_used+=("Porkbun")
- [[ $ovh_count -gt 0 ]] && sources_used+=("OVH")
- [[ $inwx_count -gt 0 ]] && sources_used+=("INWX")
- local source_summary
- local joined
- joined=$(printf " + %s" "${sources_used[@]}")
- joined="${joined:3}" # strip leading " + "
- source_summary="${joined} + RDAP bootstrap + WHOIS server list"
- case "$mode" in
- raw)
- echo "$all_tlds"
- ;;
- toml)
- echo -e "${BOLD}# Purchasable TLDs from all registrars ($(echo "$all_tlds" | wc -l | tr -d ' ') total)${NC}"
- echo "all_registrars = ["
- echo "$all_tlds" | to_toml_array
- echo "]"
- echo ""
- echo "# Country-code TLDs (purchasable)"
- echo "cctlds = ["
- echo "$all_cctlds" | to_toml_array
- echo "]"
- ;;
- diff)
- echo -e "${BOLD}Comparing registrar data vs current Lists.toml${NC}"
- echo ""
- local current_all current_country
- current_all=$(parse_current_lists "all")
- current_country=$(parse_current_lists "country")
- # TLDs in registrars but NOT in our 'all' list
- if [[ -n "$all_tlds" ]]; then
- local missing_from_all
- missing_from_all=$(comm -23 <(echo "$all_tlds" | filter_short_tlds | sort) <(echo "$current_all" | sort) 2>/dev/null || true)
- if [[ -n "$missing_from_all" ]]; then
- local mc
- mc=$(echo "$missing_from_all" | wc -l | tr -d ' ')
- echo -e "${YELLOW}TLDs at registrars but NOT in our 'all' list ($mc):${NC}"
- echo "$missing_from_all" | tr '\n' ' '
- echo ""
- echo ""
- fi
- # ccTLDs at registrars but NOT in our 'country' list
- local missing_cc
- missing_cc=$(comm -23 <(echo "$all_cctlds" | sort) <(echo "$current_country" | sort) 2>/dev/null || true)
- if [[ -n "$missing_cc" ]]; then
- local mcc
- mcc=$(echo "$missing_cc" | wc -l | tr -d ' ')
- echo -e "${YELLOW}ccTLDs at registrars but NOT in 'country' list ($mcc):${NC}"
- echo "$missing_cc" | tr '\n' ' '
- echo ""
- echo ""
- fi
- # TLDs in our 'all' list but NOT at any registrar
- local extra
- extra=$(comm -13 <(echo "$all_tlds" | sort) <(echo "$current_all" | sort) 2>/dev/null || true)
- if [[ -n "$extra" ]]; then
- local ec
- ec=$(echo "$extra" | wc -l | tr -d ' ')
- echo -e "${CYAN}TLDs in our 'all' list but NOT at any registrar ($ec):${NC}"
- echo "$extra" | tr '\n' ' '
- echo ""
- echo ""
- fi
- fi
- # Check which of our TLDs have RDAP servers
- if [[ -n "$rdap_tlds" && -n "$current_all" ]]; then
- local no_rdap
- no_rdap=$(comm -23 <(echo "$current_all" | sort) <(echo "$rdap_tlds" | sort) 2>/dev/null || true)
- if [[ -n "$no_rdap" ]]; then
- local nrc
- nrc=$(echo "$no_rdap" | wc -l | tr -d ' ')
- echo -e "${RED}TLDs in our lists with NO RDAP server ($nrc) — need WHOIS fallback:${NC}"
- echo "$no_rdap" | tr '\n' ' '
- echo ""
- fi
- fi
- ;;
- template)
- generate_template "$registrar_tlds" "$rdap_tlds" "$source_summary"
- ;;
- summary)
- echo -e "${BOLD}═══ TLD Source Summary ═══${NC}"
- echo ""
- [[ $porkbun_count -gt 0 ]] && echo -e " ${GREEN}Porkbun${NC} $(echo "$porkbun_filtered" | grep -c . || echo 0) TLDs ($(echo "$porkbun_filtered" | filter_cctlds | wc -l | tr -d ' ') ccTLDs)"
- [[ $ovh_count -gt 0 ]] && echo -e " ${GREEN}OVH${NC} $ovh_count TLDs ($(echo "$ovh_tlds" | filter_cctlds | wc -l | tr -d ' ') ccTLDs)"
- [[ $inwx_count -gt 0 ]] && echo -e " ${GREEN}INWX${NC} $inwx_count TLDs ($(echo "$inwx_tlds" | filter_cctlds | wc -l | tr -d ' ') ccTLDs)"
- [[ $tldlist_count -gt 0 ]] && echo -e " ${GREEN}tld-list.com${NC} $tldlist_count TLDs (community registry, no pricing)"
- [[ $iana_count -gt 0 ]] && echo -e " ${GREEN}IANA${NC} $iana_count TLDs"
- [[ $rdap_count -gt 0 ]] && echo -e " ${GREEN}RDAP${NC} $rdap_count TLDs with lookup servers"
- echo ""
- # Show what each registrar uniquely contributes
- if [[ $porkbun_count -gt 0 && $ovh_count -gt 0 ]]; then
- local ovh_unique inwx_unique
- ovh_unique=$(comm -23 <(echo "$ovh_tlds" | sort) <(echo "$porkbun_filtered" | sort) | wc -l | tr -d ' ')
- echo -e " ${CYAN}OVH adds${NC} $ovh_unique TLDs not on Porkbun"
- if [[ $inwx_count -gt 0 ]]; then
- inwx_unique=$(comm -23 <(echo "$inwx_tlds" | sort) <(echo -e "${porkbun_filtered}\n${ovh_tlds}" | sort -u) | wc -l | tr -d ' ')
- echo -e " ${CYAN}INWX adds${NC} $inwx_unique TLDs not on Porkbun/OVH"
- fi
- echo ""
- fi
- echo -e " ${BOLD}Merged purchasable:${NC} $(echo "$all_tlds" | wc -l | tr -d ' ') TLDs"
- echo -e " ${BOLD}Merged ccTLDs:${NC} $(echo "$all_cctlds" | wc -l | tr -d ' ')"
- echo ""
- echo -e " Cached data in: ${CYAN}$CACHE_DIR${NC}"
- echo -e " Use ${BOLD}--diff${NC} to compare against Lists.toml"
- echo -e " Use ${BOLD}--toml${NC} to output TOML-ready arrays"
- echo -e " Use ${BOLD}--template${NC} to generate template Lists.toml"
- echo -e " Use ${BOLD}--all-sources${NC} to also fetch tld-list.com"
- echo -e " Use ${BOLD}--raw${NC} for raw TLD list (one per line)"
- ;;
- esac
- }
- main "$@"
|