Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,12 @@ The house rule: everything ships a TUI, a CLI, an MCP and an API.
| MCP | `pit mcp`: tools `search`, `moshpit_resolve`, `moshpit_network`, `listing`, `pros`, `ring`, `edit_listing`, `vote_tallies`, `vote_top`. In the official MCP Registry as `world.pit/pit` (`server.json`) |
| PWA | installable from any browser (`/manifest.webmanifest`, share target into `/search`); the worker in `public/sw.js` caches the static shell and an offline page, never a search |

## Art Auctions House

`auctions.pit.world` is the canonical Art Auctions House site. `art.auctions.pit.world` redirects there. Artists can submit a title, name, description and photo; submissions appear publicly in the gallery. Photos up to 8 MB are stored in the local `auctions.db` database, or the path in `PIT_AUCTIONS_DB`.

The gallery and photo submissions are live; bidding and payment are not active. Those terms need to be decided before transactions are added.

### Newsletter (one list for every Profullstack property)

`GET /subscribe` shows the form; `POST /subscribe` (form: `email`, optional `site`) and `POST /api/subscribe` (JSON, CORS `*`) start a double opt-in. The @profullstack/footer subscribe form posts here with `site=<its hostname>`, so every signup keeps the property it came from; pit.world, `*.pit.world` and Moshpit names get pit.world copy, every other site gets Profullstack copy and a link back. An address already on the list still gets an email (its click records the new property).
Expand Down
76 changes: 76 additions & 0 deletions bin/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,81 @@ write_nginx() {
fi
}

# pit.world's relay config is shipped with the app because first-visit TLS and
# nested Moshpit host routing are part of the site's production path.
write_pit_proxy() {
local source_conf="$SCRIPT_DIR/../deploy/pit-proxy.nginx.conf"
local source_cert="$SCRIPT_DIR/../deploy/pit-proxy-cert"
[ -f "$source_conf" ] && [ -f "$source_cert" ] || return 0
need_root "pit.world relay"
command -v nginx >/dev/null || apt_install nginx

local conf link cert_script backup_dir stamp conf_changed=0 link_changed=0 conf_existed=0 conf_backed_up=0
if [ -d /etc/nginx/sites-available ]; then
conf=/etc/nginx/sites-available/pit-proxy
link=/etc/nginx/sites-enabled/pit-proxy
else
conf=/etc/nginx/conf.d/pit-proxy.conf
link=""
fi
cert_script=/usr/local/bin/pit-proxy-cert
backup_dir=/var/backups/pit-world
stamp="$(date -u +%Y%m%dT%H%M%SZ)"

# Accept files installed by older pit.world deploys, as well as ones carrying
# the installer marker. Refuse to overwrite unrelated server configuration.
if $SUDO test -f "$conf" && ! $SUDO grep -qF "$MARKER" "$conf"; then
$SUDO grep -qF "# <name>.pit.world: pit.world's relay" "$conf" || die "$conf exists and is not pit.world's relay config"
fi
if $SUDO test -f "$cert_script" && ! $SUDO grep -qF "$MARKER" "$cert_script"; then
$SUDO grep -qF "# Make (or keep) the Let's Encrypt certificate for one Moshpit name on pit.world." "$cert_script" || die "$cert_script exists and is not pit.world's certificate helper"
fi
if [ -n "$link" ] && $SUDO test -e "$link" && ! $SUDO test -L "$link"; then
die "$link exists and is not a symlink; move it aside before installing the relay config"
fi
if [ -n "$link" ] && $SUDO test -L "$link" && [ "$( $SUDO readlink "$link" )" != "$conf" ]; then
die "$link points somewhere else; move it aside before installing the relay config"
fi

$SUDO install -d -m 0700 "$backup_dir"
if $SUDO test -f "$conf"; then
conf_existed=1
if ! $SUDO cmp -s "$source_conf" "$conf"; then
$SUDO cp -a "$conf" "$backup_dir/pit-proxy.nginx.conf.$stamp"
conf_backed_up=1
fi
fi
if $SUDO test -f "$cert_script" && ! $SUDO cmp -s "$source_cert" "$cert_script"; then
$SUDO cp -a "$cert_script" "$backup_dir/pit-proxy-cert.$stamp"
fi

AS="$SUDO"
CHANGED=0
put_root_file "$conf" 0644 < "$source_conf"
conf_changed="$CHANGED"
if [ -n "$link" ] && ! $SUDO test -L "$link"; then
$SUDO ln -s "$conf" "$link"
link_changed=1
fi
CHANGED=0
put_root_file "$cert_script" 0755 < "$source_cert"

if [ "$conf_changed" = 1 ] || [ "$link_changed" = 1 ]; then
if ! $SUDO nginx -t >/dev/null 2>&1; then
if [ "$conf_backed_up" = 1 ]; then
$SUDO cp -a "$backup_dir/pit-proxy.nginx.conf.$stamp" "$conf"
elif [ "$conf_existed" = 0 ]; then
$SUDO rm -f "$conf"
fi
[ "$link_changed" = 0 ] || $SUDO rm -f "$link"
$SUDO nginx -t || true
die "nginx -t failed for the pit.world relay; restored its previous config"
fi
$SUDO systemctl reload nginx
log "nginx: pit.world relay config reloaded"
fi
}

health() {
local deadline=$(( $(date +%s) + HEALTH_TIMEOUT )) url="http://127.0.0.1:$PORT$HEALTH_PATH"
log "health: waiting for $url (up to ${HEALTH_TIMEOUT}s)"
Expand All @@ -474,6 +549,7 @@ phase_activate() {
return 0
fi
systemd_mode
write_pit_proxy
if [ "$SYSTEMD_MODE" = user ] && [ "$(loginctl show-user "$(id -un)" -p Linger --value 2>/dev/null)" != yes ]; then
loginctl enable-linger "$(id -un)" 2>/dev/null \
|| warn "could not enable linger: $UNIT stops when $(id -un) logs out (run: sudo loginctl enable-linger $(id -un))"
Expand Down
25 changes: 18 additions & 7 deletions deploy/pit-proxy-cert
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
#!/bin/sh
# managed by bin/install.sh
# Make (or keep) the Let's Encrypt certificate for one Moshpit name on pit.world.
# Installed as /usr/local/bin/pit-proxy-cert; pit.world runs it on a host's first
# plain-http visit (src/proxy.ts, src/services.ts), and it is safe to run by hand:
# pit-proxy-cert family.ancestry.pit.world
# pit-proxy-cert bbs.family.ancestry.pit.world (same certificate)
#
# One certificate per name covers all three of its hosts:
# For a nested Moshpit name, one certificate covers all three of its hosts:
# <name>.pit.world social.<name>.pit.world bbs.<name>.pit.world
# so a name costs one of Let's Encrypt's 50 certificates a week for pit.world, not three.
# A one-label pit.world app host only needs a certificate for itself.
set -eu

# nginx workers (www-data) read these at handshake time; /etc/letsencrypt/live is root only.
Expand All @@ -21,14 +22,14 @@ install_for_nginx() {
# As the certbot deploy hook: copy whatever was just renewed, under every host it covers.
if [ "${1:-}" = "--renewed" ]; then
for d in ${RENEWED_DOMAINS:-}; do
case "$d" in *.*.pit.world) install_for_nginx "${RENEWED_LINEAGE:-/etc/letsencrypt/live/$d}" "$d" ;; esac
case "$d" in *.pit.world) install_for_nginx "${RENEWED_LINEAGE:-/etc/letsencrypt/live/$d}" "$d" ;; esac
done
exit 0
fi

host="${1:-}"
case "$host" in
*.*.pit.world) ;;
*.pit.world) ;;
*) echo "usage: pit-proxy-cert <name>.pit.world" >&2; exit 2 ;;
esac
printf '%s' "$host" | grep -Eq '^[a-z0-9.-]+$' || { echo "bad host: $host" >&2; exit 2; }
Expand All @@ -40,15 +41,25 @@ case "$host" in
bbs.*.*.pit.world) base="${host#bbs.}" ;;
esac

# A nested Moshpit name gets one certificate for its three site hosts. A one-label
# pit.world app host (such as auctions.pit.world) needs only its own certificate.
# This box's root umask is 007; nginx (www-data) must read the challenge files.
umask 022
mkdir -p /var/www/pit-acme/.well-known/acme-challenge
chmod -R a+rX /var/www/pit-acme
exec 9>/run/lock/pit-proxy-cert.lock
flock 9
certbot certonly --webroot -w /var/www/pit-acme -d "$base" -d "social.$base" -d "bbs.$base" --cert-name "$base" \
--non-interactive --agree-tos --keep-until-expiring --expand --quiet
for h in "$base" "social.$base" "bbs.$base"; do
case "$base" in
*.*.pit.world)
certbot certonly --webroot -w /var/www/pit-acme -d "$base" -d "social.$base" -d "bbs.$base" --cert-name "$base" \
--non-interactive --agree-tos --keep-until-expiring --expand --quiet
hosts="$base social.$base bbs.$base" ;;
*.pit.world)
certbot certonly --webroot -w /var/www/pit-acme -d "$base" --cert-name "$base" \
--non-interactive --agree-tos --keep-until-expiring --expand --quiet
hosts="$base" ;;
esac
for h in $hosts; do
install_for_nginx "/etc/letsencrypt/live/$base" "$h"
done
nginx -t -q && systemctl reload nginx
10 changes: 6 additions & 4 deletions deploy/pit-proxy.nginx.conf
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# managed by bin/install.sh
# <name>.pit.world: pit.world's relay of every Moshpit name to the old internet (src/proxy.ts).
# Installed as /etc/nginx/sites-available/pit-proxy (symlinked into sites-enabled).
#
# DNS: one wildcard, *.pit.world -> dev2. TLS: a wildcard certificate covers a single
# label and these hosts have two or more (family.ancestry.pit.world), so every name has
# its own Let's Encrypt certificate, chosen here by SNI. A certificate named by a variable
# label; nested names and pit.world-owned app hosts have an exact certificate chosen by SNI.
# The relay handles one or more labels before .pit.world, including auctions.pit.world.
# A certificate named by a variable
# is read by the worker (www-data) at handshake time, and /etc/letsencrypt/live is root
# only, so pit-proxy-cert copies each one to /etc/nginx/pit-proxy-certs (root:www-data). /usr/local/bin/pit-proxy-cert makes one; the app asks for it on a name's first
# plain-http visit. pit.world and www.pit.world have their own exact server blocks,
Expand All @@ -12,7 +14,7 @@
server {
listen 80;
listen [::]:80;
server_name ~^[a-z0-9-]+(\.[a-z0-9-]+)+\.pit\.world$;
server_name ~^[a-z0-9-]+(\.[a-z0-9-]+)*\.pit\.world$;
access_log off;
error_log /var/log/nginx/pit-proxy.error.log crit;

Expand All @@ -32,7 +34,7 @@ server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name ~^[a-z0-9-]+(\.[a-z0-9-]+)+\.pit\.world$;
server_name ~^[a-z0-9-]+(\.[a-z0-9-]+)*\.pit\.world$;

ssl_certificate /etc/nginx/pit-proxy-certs/$ssl_server_name.crt;
ssl_certificate_key /etc/nginx/pit-proxy-certs/$ssl_server_name.key;
Expand Down
Loading
Loading