From ace9750e669cb928a035c3a16203ef30f6666103 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Sat, 1 Aug 2026 20:15:07 +0000 Subject: [PATCH] Publish the pin from the script, so serving a name is one command setup-origin.sh did everything except the step that makes a name usable. It generated the key, wrote nginx, verified the certificate -- then printed a base64 string and told a human to go paste it in a browser. Until they did, every client refused the name, so the automated part produced nothing that worked on its own. It turns out no browser is needed. The registry's own router accepts a bearer token alongside a cookie session: moshpitRouter.use("/api/moshpit", async (req, _res, next) => { if (!req.user) { const user = await userForApiKey(bearer(req)); ... So with MOSHPIT_API_KEY set, the script now publishes the pin and optionally sets the name's target, and "serve this name" is genuinely one command. Without a key nothing changes -- it prints the pin exactly as before. It reads the pin back afterwards rather than trusting the 201. A write being accepted and the endpoint clients query actually serving it are different claims, and a pin that is accepted but not served leaves the name refused with nothing to show for it. --target refuses an IPv4 literal up front, because the registry refuses one by design (an A record on a small host is leased, NATed or shared, so a name pointed at one goes stale silently). The error names the fix -- use a hostname -- which the API's own message does not. Also fixes --help, which was consumed as the name and answered "'--help' does not look like a Moshpit name". Pre-existing. Request shapes verified against a mock registry, and they match the deployed routes' setNameTarget({tld,label,target}) and addPin({tld,label,pin,kind}). sh -n clean; guards for missing --api-key, IPv4 literals and URLs exercised. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 28 +++++++++ scripts/setup-origin.sh | 124 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 140 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 497355b..a994227 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,34 @@ broken, when what is broken is one missing `server_name`. The script connects ba and checks, rather than trusting that a clean `nginx -t` means the name resolves to the right block. +### One command, including the registry + +`setup-origin.sh` stops at printing the pin because publishing it needs an +account. With an API key it does not stop: + +```sh +MOSHPIT_API_KEY=... sh scripts/setup-origin.sh chovy.hacker \ + --target dev.profullstack.com +``` + +Key, certificate, nginx block, reload, verify, **publish the pin, set the +target, and read it back from the registry** — the whole of "serve this name". +Get a key at [app.moshcode.sh/settings](https://app.moshcode.sh/settings). + +This works because `/api/moshpit` accepts a bearer token as well as a cookie +session, so nothing here needs a browser. Without a key the script behaves +exactly as before and prints the pin for you to paste. + +The read-back at the end is the point. A `201` means the write was accepted; +only a read proves the endpoint clients actually query returns it, and a pin +that was accepted but is not served leaves the name refused with nothing to +show for it. + +**`--target` will not take an IPv4 literal**, because the registry refuses one +by design — an A record on a small host is leased, NATed or shared, and a name +pointed at one goes stale silently. Use a hostname: the address behind it is +resolved normally, which is also how a name reaches IPv4-only clients. + Nothing is proxied on the server side. `moshpit-proxy` runs on the *visitor's* machine — it is the thing that checks the pin on behalf of a browser that cannot. diff --git a/scripts/setup-origin.sh b/scripts/setup-origin.sh index 0c903a3..56288ed 100755 --- a/scripts/setup-origin.sh +++ b/scripts/setup-origin.sh @@ -13,6 +13,15 @@ # for the name and clients checking the key they were handed against it. Which # means the last step is not optional: until the pin is published, every client # refuses the name rather than trusting it on sight. +# +# With MOSHPIT_API_KEY set, that last step stops being manual: +# +# MOSHPIT_API_KEY=... sh setup-origin.sh chovy.hacker --target dev.profullstack.com +# +# publishes the pin and sets the target over the registry API, so the whole of +# "serve this name" is one command. Get a key at app.moshcode.sh/settings. +# Without the key nothing changes -- it prints the pin and tells you where to +# paste it, exactly as before. set -eu NAME="${1:-}" @@ -22,6 +31,9 @@ ENABLEDIR="${MOSHPIT_ENABLEDIR:-/etc/nginx/sites-enabled}" WEBROOT="${MOSHPIT_WEBROOT:-/var/www/$NAME}" DAYS="${MOSHPIT_DAYS:-825}" TEMPLATE="${MOSHPIT_TEMPLATE:-$(dirname "$0")/../nginx/moshpit-origin.conf}" +API_KEY="${MOSHPIT_API_KEY:-}" +REGISTRY="${MOSHPIT_REGISTRY:-https://app.moshcode.sh}" +TARGET="" DRY_RUN=0 RED=''; BOLD=''; DIM=''; OFF='' @@ -32,23 +44,40 @@ warn() { printf '%swarning:%s %s\n' "$RED" "$OFF" "$*" >&2; } die() { printf '%serror:%s %s\n' "$RED" "$OFF" "$*" >&2; exit 1; } have() { command -v "$1" >/dev/null 2>&1; } -shift 2>/dev/null || true -while [ $# -gt 0 ]; do - case "$1" in - --dry-run) DRY_RUN=1 ;; - --days) DAYS="${2:?--days needs a number}"; shift ;; - --webroot) WEBROOT="${2:?--webroot needs a path}"; shift ;; - -h|--help) - cat >&2 <&2 < [options] --dry-run write nothing, print what would happen --days certificate lifetime (default: $DAYS) --webroot site files (default: /var/www/) + --api-key publish the pin instead of printing it for a human + --target also set the name's "points at" (needs --api-key) + --registry registry base (default: $REGISTRY) + +An IPv4 literal is refused by the registry by design -- point a name at an +IPv6 address or a hostname. A hostname is how a name reaches IPv4 clients, +since the address behind it is resolved normally. -environment: MOSHPIT_CERTDIR, MOSHPIT_SITEDIR, MOSHPIT_ENABLEDIR, MOSHPIT_WEBROOT +environment: MOSHPIT_CERTDIR, MOSHPIT_SITEDIR, MOSHPIT_ENABLEDIR, MOSHPIT_WEBROOT, + MOSHPIT_API_KEY, MOSHPIT_REGISTRY EOF - exit 0 ;; +} + +# Before the name is consumed, or `--help` is read as the name and the script +# dies telling you that `--help` is not a Moshpit name. +case "$NAME" in -h|--help) usage; exit 0 ;; esac + +shift 2>/dev/null || true +while [ $# -gt 0 ]; do + case "$1" in + --dry-run) DRY_RUN=1 ;; + --days) DAYS="${2:?--days needs a number}"; shift ;; + --webroot) WEBROOT="${2:?--webroot needs a path}"; shift ;; + --api-key) API_KEY="${2:?--api-key needs a token}"; shift ;; + --target) TARGET="${2:?--target needs an address or hostname}"; shift ;; + --registry) REGISTRY="${2:?--registry needs a base URL}"; shift ;; + -h|--help) usage; exit 0 ;; *) die "unknown option: $1 (try --help)" ;; esac shift @@ -60,6 +89,19 @@ case "$NAME" in *) die "'$NAME' does not look like a Moshpit name" ;; esac have openssl || die "openssl is required" + +# Caught here rather than after the certificate exists, so a typo does not leave +# a half-configured name behind. +if [ -n "$TARGET" ] && [ -z "$API_KEY" ]; then + die "--target needs --api-key (or MOSHPIT_API_KEY) — the target is set through the registry API" +fi +case "$TARGET" in + *://*) die "--target takes a bare address or hostname, not a URL" ;; + # An IPv4 literal is refused by the registry by design; saying so here saves a + # round trip and explains the fix, which the API's own error does not. + [0-9]*.[0-9]*.[0-9]*.[0-9]*) + die "--target will not accept an IPv4 literal — use a hostname that resolves to it (the registry stores IPv6 or hostnames)" ;; +esac [ -f "$TEMPLATE" ] || die "template not found: $TEMPLATE" if [ "$DRY_RUN" = "0" ] && [ "$(id -u)" != "0" ]; then @@ -194,13 +236,70 @@ else PIN="(dry run — no key was generated)" fi -cat >&2 <&1 + } + ok_status() { case "$1" in 2*) return 0 ;; *) return 1 ;; esac; } + report() { + _what="$1"; _out="$2" + _code=$(printf '%s' "$_out" | tail -n1) + _body=$(printf '%s' "$_out" | sed '$d') + if ok_status "$_code"; then + say " ${DIM}$_what — ok ($_code)${OFF}" + return 0 + fi + warn "$_what failed ($_code): $_body" + return 1 + } + + if [ -n "$TARGET" ]; then + step "pointing $NAME at $TARGET" + report "target" "$(api PUT "/api/moshpit/tlds/$TLD/names" \ + "{\"label\":\"$LABEL\",\"target\":\"$TARGET\"}")" || true + fi + + step "publishing the pin" + # 409 means this exact pin is already published under a different kind, which + # is a real mistake worth surfacing rather than a retry. + report "pin" "$(api POST "/api/moshpit/tlds/$TLD/pins" \ + "{\"label\":\"$LABEL\",\"pin\":\"$PIN\",\"kind\":\"tls\",\"note\":\"setup-origin.sh\"}")" || true + + # Read it back. A 201 means the write was accepted; only a read proves the + # thing clients actually query now returns it. + step "confirming the registry serves it" + published=$(curl -sS "$REGISTRY/api/moshpit/tlds/$TLD/pins?label=$LABEL" 2>/dev/null || true) + case "$published" in + *"$PIN"*) say " ${DIM}$NAME is published and verifiable${OFF}" ;; + *) warn "the registry does not list this pin yet: $published" + warn "clients will keep refusing $NAME until it does" ;; + esac +else + cat >&2 <&2 <