mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 03:35:09 +02:00
feat(setup-gbrain): add gstack-gbrain-supabase-verify structural URL check
Zero-network validator for Supabase Session Pooler URLs before handing
them to `gbrain init`. Canonical shape verified per gbrain init.ts:266:
postgresql://postgres.<ref>:<password>@aws-0-<region>.pooler.supabase.com:6543/postgres
Rejects direct-connection URLs (db.*.supabase.co:5432) with a distinct
exit code 3 and clear IPv6-failure remediation — that's the most common
paste mistake users make, so it earns its own UX path rather than a
generic "bad URL" error.
Never echoes the URL (contains a password) in error messages; tests
verify a distinct seed password never appears in stderr on any reject
path. Accepts URL from argv[1] or stdin ("-" or no arg).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Executable
+126
@@ -0,0 +1,126 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# gstack-gbrain-supabase-verify — structural check on a Supabase Session
|
||||||
|
# Pooler URL before handing it to `gbrain init`.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# gstack-gbrain-supabase-verify <url>
|
||||||
|
# echo "<url>" | gstack-gbrain-supabase-verify -
|
||||||
|
#
|
||||||
|
# Accepts ONLY Session Pooler URLs (port 6543, host *.pooler.supabase.com).
|
||||||
|
# Rejects direct-connection URLs (db.*.supabase.co:5432) since those are
|
||||||
|
# IPv6-only and fail in many environments — gbrain's init wizard warns
|
||||||
|
# about this at init.ts:150-158.
|
||||||
|
#
|
||||||
|
# Canonical shape (per gbrain init.ts:266):
|
||||||
|
# postgresql://postgres.<ref>:<password>@aws-0-<region>.pooler.supabase.com:6543/postgres
|
||||||
|
#
|
||||||
|
# Exit codes:
|
||||||
|
# 0 — URL passes structural check
|
||||||
|
# 2 — invalid format (bad scheme, port, host, userinfo, or empty password)
|
||||||
|
# 3 — direct-connection URL rejected (common mistake, special-cased for UX)
|
||||||
|
#
|
||||||
|
# The verifier never makes a network call; purely a regex match. Whether
|
||||||
|
# the URL actually works (database up, password correct, host reachable)
|
||||||
|
# is gbrain's problem at init time.
|
||||||
|
#
|
||||||
|
# Reads URL from:
|
||||||
|
# 1. argv[1] if provided and not "-"
|
||||||
|
# 2. stdin if argv[1] is "-" or missing
|
||||||
|
#
|
||||||
|
# Never echoes the URL to stderr (it contains a password). Error messages
|
||||||
|
# refer to "the URL" generically.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
die() { echo "gstack-gbrain-supabase-verify: $*" >&2; exit 2; }
|
||||||
|
reject_direct() {
|
||||||
|
cat >&2 <<EOF
|
||||||
|
gstack-gbrain-supabase-verify: rejected direct-connection URL
|
||||||
|
|
||||||
|
You pasted a Supabase direct-connection URL (db.*.supabase.co on port
|
||||||
|
5432). Direct connections are IPv6-only and fail in many environments.
|
||||||
|
|
||||||
|
Use the Session Pooler instead:
|
||||||
|
Supabase Dashboard → Settings → Database → Connection Pooler →
|
||||||
|
Transaction/Session → copy URI (port 6543)
|
||||||
|
|
||||||
|
Expected shape:
|
||||||
|
postgresql://postgres.<ref>:<password>@aws-0-<region>.pooler.supabase.com:6543/postgres
|
||||||
|
EOF
|
||||||
|
exit 3
|
||||||
|
}
|
||||||
|
|
||||||
|
URL=""
|
||||||
|
case "${1:-}" in
|
||||||
|
-) URL=$(cat) ;;
|
||||||
|
"") URL=$(cat) ;;
|
||||||
|
*) URL="$1" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
URL=$(printf '%s' "$URL" | tr -d '[:space:]')
|
||||||
|
[ -z "$URL" ] && die "empty URL"
|
||||||
|
|
||||||
|
# Scheme: must be postgresql:// or postgres://. Explicitly reject other
|
||||||
|
# schemes rather than guess.
|
||||||
|
case "$URL" in
|
||||||
|
postgresql://*|postgres://*) ;;
|
||||||
|
*) die "bad scheme (must start with postgresql:// or postgres://)" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Strip scheme to expose userinfo + host + port + path.
|
||||||
|
rest="${URL#*://}"
|
||||||
|
|
||||||
|
# Userinfo portion: everything before the first @. Must contain a : (user:pass).
|
||||||
|
case "$rest" in
|
||||||
|
*@*) ;;
|
||||||
|
*) die "missing userinfo (expected postgres.<ref>:<password>@host)" ;;
|
||||||
|
esac
|
||||||
|
userinfo="${rest%%@*}"
|
||||||
|
after_at="${rest#*@}"
|
||||||
|
|
||||||
|
# Userinfo must be user:password with neither part empty.
|
||||||
|
case "$userinfo" in
|
||||||
|
*:*) ;;
|
||||||
|
*) die "userinfo missing password separator (expected user:password@)" ;;
|
||||||
|
esac
|
||||||
|
user_part="${userinfo%%:*}"
|
||||||
|
pass_part="${userinfo#*:}"
|
||||||
|
[ -z "$user_part" ] && die "empty user portion in userinfo"
|
||||||
|
[ -z "$pass_part" ] && die "empty password in userinfo"
|
||||||
|
|
||||||
|
# Host + port + path.
|
||||||
|
# Direct-connection detection FIRST (specific error beats generic).
|
||||||
|
case "$after_at" in
|
||||||
|
db.*.supabase.co:5432*|db.*.supabase.co/*|db.*.supabase.co) reject_direct ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Extract host:port (before first / if present).
|
||||||
|
hostport="${after_at%%/*}"
|
||||||
|
case "$hostport" in
|
||||||
|
*:*) ;;
|
||||||
|
*) die "missing port (Session Pooler requires :6543)" ;;
|
||||||
|
esac
|
||||||
|
host="${hostport%:*}"
|
||||||
|
port="${hostport##*:}"
|
||||||
|
|
||||||
|
# Host must be *.pooler.supabase.com (case-insensitive).
|
||||||
|
host_lower=$(printf '%s' "$host" | tr '[:upper:]' '[:lower:]')
|
||||||
|
case "$host_lower" in
|
||||||
|
*.pooler.supabase.com) ;;
|
||||||
|
*) die "host '$host' is not a Supabase Session Pooler (expected *.pooler.supabase.com)" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Port must be 6543 (Session Pooler default).
|
||||||
|
if [ "$port" != "6543" ]; then
|
||||||
|
die "port must be 6543 for Session Pooler (got $port)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# User portion should look like postgres.<ref> (20-char lowercase ref,
|
||||||
|
# per the Supabase Management API contract). Not strictly required by
|
||||||
|
# gbrain, but rejecting a plain "postgres" user catches a common paste
|
||||||
|
# error where someone grabs the Direct URL userinfo by mistake.
|
||||||
|
case "$user_part" in
|
||||||
|
postgres.*) ;;
|
||||||
|
*) die "user portion '$user_part' should be 'postgres.<project-ref>' (20-char ref)" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "ok"
|
||||||
Reference in New Issue
Block a user