Bring the relay package up to the Grin-style guided-setup standard, matching
GoblinPay's wizard experience:
- install.sh at the repo root: one interactive script that asks a single
topology question (name service alongside the relay / on a separate domain /
relay only / authority standalone), builds what was chosen, installs the
hardened systemd units, and hands off to the authority's setup wizard. Every
prompt has a default; re-runnable; never clobbers an existing config.
- Secret hardening (new house rule): the GoblinPay token and optional webhook
secret are collected over HIDDEN prompts (terminal echo off via libc termios)
and written to a root-only 0600 file, never the (proxy/backup-exposable) env
file. The env file only references GOBLINPAY_TOKEN_FILE /
GOBLINPAY_WEBHOOK_SECRET_FILE. install.sh drops in a systemd credential
(LoadCredential + Environment=%d) so the DynamicUser reads a copy without the
file being broadly readable; the base unit stays valid for a free (no-secret)
authority. Aligns with GoblinPay's 0400 wallet-password + *_FILE pattern
(feat/secret-hardening branch does not exist yet; used root-only 0600 config).
- `floonet-name-authority setup [--reconfigure]` subcommand so install.sh can
drive the wizard explicitly; the auto-run-on-first-launch path stays.
- config.rs reads GOBLINPAY_WEBHOOK_SECRET_FILE (mirrors the token _FILE path).
- README, .env.example, and the systemd unit header document the guided path
and the secret model. Authority tests (incl. a new secret-out-of-env test)
and the 32 plugin tests stay green; fmt/clippy clean.
Ports the Goblin Name Transfer Protocol v1 into the colocated Floonet name
authority: a seller lodges a signed kind-3402 offer to reassign a name to a
buyer for a Grin price, and the buyer claims it with an on-chain payment proof.
Strictly non-custodial: the buyer pays the seller wallet to wallet, the
authority verifies two artifacts and swaps one names row. Keys never move.
New routes, mounted only when FLOONET_TRANSFERS is set (else they 404):
POST /api/v1/transfer/offer NIP-98 (seller), body {offer}
GET /api/v1/transfer/offer/{id} public, read-limited, CORS *
DELETE /api/v1/transfer/offer/{id} NIP-98 (seller)
POST /api/v1/transfer/claim NIP-98 (buyer), body {offer_id, proof}
Claim verification runs in the spec's order: NIP-98 + replay set, offer exists
(consumed -> idempotent 200 or 409), revoked/expired grace + end_height gate,
buyer key == p tag, seller still owner, both proof signatures over the canonical
73-byte message (amount_BE || excess || sender), recipient address and exact
amount match the offer, kernel confirmed on chain >= min_conf, excess never used
(durable UNIQUE), buyer holds no name (exempt from the name-change cooldown).
The reassignment is one atomic SQLite transaction that also records the transfer,
consumes the offer, and arms the seller's cooldown. The lodge ambiguity rule
keys on proof_address alone among live offers (offer_ambiguous).
The transfer path is fully independent of the fork's GoblinPay paid-names
integration: zero calls into src/paid.rs, no GOBLINPAY_URL dependency, pure
in-process crypto plus the read-only Grin node foreign API. Transfers and paid
names toggle in any combination. Config follows the repo's FLOONET_ pattern
(from_env/validate/summary/for_test), fail-fast when transfers are on without
FLOONET_GRIN_NODE_URL, and is documented in .env.example, docker-compose.yml,
and the us-east env template.
Tests: 27 transfer integration tests (offer state machine, revoke, expiry,
late-claim grace and end_height rules, every claim failure mode with real
ed25519 proofs, successful reassign + idempotent retry + cooldown, plus a
transfers-with-pay-mode-active combination) adapted to tests/http.rs helpers,
plus proof, node and config unit tests. cargo test green (32 + 17 + 27).
Public-note kinds are now accepted only from an operator-configured list of
author pubkeys (FLOONET_AUTHORIZED_AUTHORS, hex or npub). Closed by default:
with no authors set, kinds 1 and 30023 are rejected for everyone, so random
notes cannot be spammed to the relay. Every other kind is unaffected and
kind 0 profiles stay open.
- add 30023 (long-form article) to the default kind whitelist
- check_authorized_authors runs right after check_kind; LOCKED_KINDS {1,30023}
- pure-python bech32 npub decoder (no new deps); invalid entries logged/skipped
- load_config also reads a KEY=VALUE floonet.env next to the plugin
(FLOONET_ENV_FILE), env vars win, so config changes need no container
recreate; strfry reloads the plugin on mtime change (no restart)
- extend test_policy.py (32 tests green)
- scripts/purge_public_notes.sh (dry-run default) to clean pre-existing notes
- scripts/smoke_test_lockdown.py post-deploy check
- README + .env.example config docs
The Goblin wallet moved off the Nym mixnet to Tor: it now reaches relays
over a Tor circuit to their clearnet endpoint, so the co-located mixnet
exit this package bundled is retired.
- delete the vendored `mixexit/` crate and the hardened
`deploy/systemd/floonet-mixexit.service` bare-metal unit
- docker-compose: drop the `mixexit` service (COMPOSE_PROFILES=exit) and
its `mixexit-data` volume; fix the top-of-file service list
- .env.example: replace the "Mixnet exit" block (COMPOSE_PROFILES=exit,
FLOONET_EXIT_UPSTREAM) with the Tor onion toggle
- README: replace the "Mixnet exit" section with "Tor onion", fix the
feature table, deploy paths, and the COMPOSE_PROFILES reference row
Add Tor as the first-class replacement deploy option, the same recipe
already proven in production: an optional `tor` compose service
(COMPOSE_PROFILES=tor) plus deploy/tor/torrc, a stock system tor daemon
whose hidden service forwards straight to strfry's websocket listener
(no TLS on that hop, the onion is already encrypted end to end). strfry
core stays stock; this is packaging and docs, no relay patch.
The default stack is unchanged (the onion is opt-in, as the exit was).
name-authority builds green; cargo test and clippy pass. docker-compose
validated (structure + YAML).
Serve the authority's NIP-05 lookup on the relay's own domain so
`name@relay.example` resolves, without giving the authority a second
vhost/cert. Live on us-east: relay.floonet.dev now answers
/.well-known/nostr.json from the co-located authority (127.0.0.1:8193)
while the WebSocket relay and NIP-11 stay untouched.
* deploy/us-east/colocated-authority.conf
The nginx opt-in: an exact-match `location = /.well-known/nostr.json`
proxied to the authority ahead of the relay's WebSocket catch-all.
Only the READ path is exposed; registration and the rest of /api/*
stay on the authority's own domain. Sets X-Real-IP (the per-IP rate
limiter keys off it). Same proxy shape as nm.floonet.dev.conf.
* README.md — "Co-locating names on the relay domain": the Caddy/compose
stack is co-located by default (single FLOONET_DOMAIN); a split nginx
deploy opts in with the snippet. Documents FLOONET_AUTHORITY_COLOCATED.
* deploy/Caddyfile, .env.example — note the single-domain stack is
co-located by default and point split deploys at the snippet.
The box vhost mirrors this snippet exactly; applied with nginx -t + reload
(no restart), firewalld untouched.
Stock strfry + a default-deny write-policy plugin (kinds 0,3,5,13,1059,
10002,10050,27235 only), NIP-42 auth, neutral NIP-11, a bundled name
authority (paid names/uses via GoblinPay), and a config-toggled co-located
mixnet exit. Docker Compose + Caddy + hardened systemd. strfry core stays
stock (plugin + config only). Validated end to end against real strfry.