diff --git a/scripts/seed-communities.sql b/scripts/seed-communities.sql new file mode 100644 index 000000000..03bd56d02 --- /dev/null +++ b/scripts/seed-communities.sql @@ -0,0 +1,11 @@ +-- Upsert local dev community hosts for row-zero host binding. +-- +-- :hosts is a newline-separated list of authorities derived by seed-hosts.py +-- (e.g. "localhost:3000", "localhost", "127.0.0.1", "127.0.0.1:3000"). Each +-- distinct authority becomes a communities row; existing rows are left +-- untouched (ON CONFLICT against the unique lower(host) index). +INSERT INTO communities (host) +SELECT btrim(h) +FROM unnest(string_to_array(:'hosts', E'\n')) AS h +WHERE btrim(h) <> '' +ON CONFLICT (lower(host)) DO NOTHING; diff --git a/scripts/seed-hosts.py b/scripts/seed-hosts.py new file mode 100755 index 000000000..98eea3628 --- /dev/null +++ b/scripts/seed-hosts.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Derive the loopback community hosts to seed for local dev. + +The relay uses row-zero host binding: it resolves a request's community from the +Host header and fails closed (404) when that authority is not a `communities` +row. Local dev tooling hits the relay under several loopback authorities +(``localhost``/``127.0.0.1``, with and without the port), and under host binding +those are *distinct* keys -- so we seed each one to avoid a fail-closed 404 when +one client uses an alternate authority. + +Reads RELAY_URL from the environment (default ``ws://localhost:3000``) and prints +the authorities to seed, one per line, normalized the same way the relay +normalizes a community host. seed-local-community.sh feeds these to +seed-communities.sql. +""" + +import os +from urllib.parse import urlparse + + +def derive_hosts(relay_url): + """Return the ordered, de-duplicated list of authorities to seed.""" + parsed = urlparse(relay_url) + host = (parsed.hostname or "").rstrip(".").lower() + port = parsed.port + scheme = parsed.scheme.lower() + + def authority(h): + if not h: + return "" + display_host = f"[{h}]" if ":" in h and not h.startswith("[") else h + default_port = (scheme == "ws" and port == 80) or (scheme == "wss" and port == 443) + if port and not default_port: + return f"{display_host}:{port}" + return display_host + + hosts = [] + primary = authority(host) + if primary: + hosts.append(primary) + + # Non-loopback deployments seed only RELAY_URL's authority; loopback dev + # additionally seeds both localhost and 127.0.0.1, with and without port. + if host in {"localhost", "127.0.0.1"}: + hosts.extend(["localhost", "127.0.0.1"]) + if port: + hosts.extend([f"localhost:{port}", f"127.0.0.1:{port}"]) + + seen = [] + for h in hosts: + if h and h not in seen: + seen.append(h) + return seen + + +def main(): + relay_url = os.environ.get("RELAY_URL", "ws://localhost:3000") + hosts = derive_hosts(relay_url) + if not hosts: + raise SystemExit("could not derive a host from RELAY_URL") + for h in hosts: + print(h) + + +if __name__ == "__main__": + main() diff --git a/scripts/seed-local-community.sh b/scripts/seed-local-community.sh index 9a79761c2..f477c7b84 100755 --- a/scripts/seed-local-community.sh +++ b/scripts/seed-local-community.sh @@ -4,6 +4,12 @@ # The relay intentionally fails closed when the request Host header is not in # `communities`. Local dev uses loopback hosts, so bootstrap must create those # rows after migrations before desktop/Tauri HTTP bridge calls can succeed. +# +# Host derivation lives in seed-hosts.py and the upsert in seed-communities.sql, +# kept out of this shell script on purpose: inlining the Python as a heredoc +# inside `$(...)` made bash 3.2 (stock macOS /bin/bash) fail to parse the script +# at all. The shell now only wires the two together and never parses their +# source, so it is portable across bash versions. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -24,71 +30,26 @@ export PGPASSWORD="${PGPASSWORD:-buzz_dev}" export PGDATABASE="${PGDATABASE:-buzz}" export RELAY_URL="${RELAY_URL:-ws://localhost:3000}" -hosts_sql=$(python3 - <<'PY' -import os -from urllib.parse import urlparse - -relay_url = os.environ.get("RELAY_URL", "ws://localhost:3000") -parsed = urlparse(relay_url) - -host = (parsed.hostname or "").rstrip(".").lower() -port = parsed.port -scheme = parsed.scheme.lower() - -def authority(host, port, scheme): - if not host: - return "" - display_host = f"[{host}]" if ":" in host and not host.startswith("[") else host - default_port = (scheme == "ws" and port == 80) or (scheme == "wss" and port == 443) - if port and not default_port: - return f"{display_host}:{port}" - return display_host - -primary = authority(host, port, scheme) -hosts = [] -if primary: - hosts.append(primary) - -# Local desktop/dev tooling has historically used both localhost and 127.0.0.1, -# and some HTTP clients can omit the default/non-default port in Host handling. -# Under row-zero host binding these are distinct hosts, so seed loopback aliases -# for local dev to avoid a fail-closed 404 when one side uses an alternate -# authority. Non-loopback deployments seed only RELAY_URL's authority. -if host in {"localhost", "127.0.0.1"}: - hosts.extend(["localhost", "127.0.0.1"]) - if port: - hosts.extend([f"localhost:{port}", f"127.0.0.1:{port}"]) - -seen = [] -for h in hosts: - if h and h not in seen: - seen.append(h) - -if not seen: - raise SystemExit("could not derive a host from RELAY_URL") - -print(",\n".join(f" ('{h.replace("'", "''")}')" for h in seen)) -PY -) +hosts="$(python3 "${SCRIPT_DIR}/seed-hosts.py")" +if [[ -z "${hosts}" ]]; then + echo "error: could not derive any community host from RELAY_URL=${RELAY_URL}" >&2 + exit 1 +fi -sql=" -INSERT INTO communities (host) -SELECT host -FROM (VALUES -${hosts_sql} -) AS v(host) -ON CONFLICT (lower(host)) DO NOTHING; -" +sql_file="${SCRIPT_DIR}/seed-communities.sql" if command -v psql >/dev/null 2>&1; then - PGPASSWORD="${PGPASSWORD}" psql -h "${PGHOST}" -p "${PGPORT}" -U "${PGUSER}" -d "${PGDATABASE}" -v ON_ERROR_STOP=1 -c "${sql}" + PGPASSWORD="${PGPASSWORD}" psql -h "${PGHOST}" -p "${PGPORT}" -U "${PGUSER}" -d "${PGDATABASE}" \ + -v ON_ERROR_STOP=1 -v hosts="${hosts}" -f "${sql_file}" elif docker exec buzz-postgres psql --version >/dev/null 2>&1; then docker exec -i -e PGPASSWORD="${PGPASSWORD}" buzz-postgres \ - psql -U "${PGUSER}" -d "${PGDATABASE}" -v ON_ERROR_STOP=1 -c "${sql}" + psql -U "${PGUSER}" -d "${PGDATABASE}" -v ON_ERROR_STOP=1 -v hosts="${hosts}" < "${sql_file}" else echo "error: neither psql nor buzz-postgres docker psql is available" >&2 exit 1 fi echo "Seeded local dev community host(s):" -echo "${hosts_sql}" | sed -E "s/^ +\('(.+)'\),?$/ - \1/" +while IFS= read -r host; do + [[ -n "${host}" ]] && echo " - ${host}" +done <<< "${hosts}"