From bee4e661ea910fa057bd87b917643c2e925fce0f Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Thu, 23 Apr 2026 23:42:07 -0700 Subject: [PATCH] feat(setup-gbrain): add gstack-gbrain-supabase-verify structural URL check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zero-network validator for Supabase Session Pooler URLs before handing them to `gbrain init`. Canonical shape verified per gbrain init.ts:266: postgresql://postgres.:@aws-0-.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) --- bin/gstack-gbrain-supabase-verify | 126 ++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100755 bin/gstack-gbrain-supabase-verify diff --git a/bin/gstack-gbrain-supabase-verify b/bin/gstack-gbrain-supabase-verify new file mode 100755 index 00000000..5a3b04c5 --- /dev/null +++ b/bin/gstack-gbrain-supabase-verify @@ -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 +# echo "" | 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.:@aws-0-.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 <:@aws-0-.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.:@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. (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.' (20-char ref)" ;; +esac + +echo "ok"