feat(seo): account-management verbs (connect/accounts/forget) + connect.sh wrapper

tokenstore remove/clear, fetch.sh forget dispatch, and a connect.sh wrapper
that sources ~/.claude/.env internally and runs from any project. /seo now
routes connect|accounts|forget before the audit flow; Makefile seo-connect
delegates to the wrapper. Labels are guarded to shell-safe ASCII (POSIX case,
whole-string, C-locale) as defense-in-depth; forget output states local
removal is not a Google-side revocation.
This commit is contained in:
Bastien Chanot
2026-07-10 12:38:32 +02:00
parent 61a98d3ae1
commit 8bf7459566
7 changed files with 258 additions and 27 deletions
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# One-time OAuth consent wrapper — runnable from ANY directory:
# bash ~/.claude/lib/seo-data/connect.sh --label <label>
# Sources the env vault internally (never echoed), prefers the engine venv,
# then execs connect.py. Interactive by design: stdout carries the auth URL,
# stderr stays visible (unlike fetch.sh, there is no secret-leak surface to
# suppress — connect.py never prints tokens).
set -uo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
ENV_FILE="${SEO_DATA_ENV_FILE:-${HOME}/.claude/.env}" # canonical; tests override to /dev/null
VENV_PY="${HOME}/.claude/.venv-seo-data/bin/python3"
# Whole-string label guard (shell-safe ASCII: leading alnum then alnum/._-).
# POSIX `case` in a C-locale subshell: no per-line grep pitfall (a newline is
# a non-allowed byte caught by *[!...]*), no locale range surprise, no second
# grammar to differ from. Empty and non-alnum-leading are rejected too.
_label_safe() ( LC_ALL=C; case "$1" in ''|[!A-Za-z0-9]*|*[!A-Za-z0-9._-]*) exit 1;; esac )
# Strict argv grammar (parser-differential defense): accept ONLY the exact
# forms `--label <value>` / `--store <path>` — never `=`-joined or abbreviated
# forms — so the downstream argparse can never resolve a token this guard
# didn't see. Runs BEFORE any secret is loaded.
argv=("$@"); n=${#argv[@]}; i=0
while [ "$i" -lt "$n" ]; do
case "${argv[$i]}" in
--label)
if ! _label_safe "${argv[$((i+1))]:-}"; then
echo "connect.sh: unsafe label — must match ^[A-Za-z0-9][A-Za-z0-9._-]*\$" >&2
exit 2
fi
i=$((i+2)) ;;
--store) i=$((i+2)) ;;
*)
echo "connect.sh: unsupported argument '${argv[$i]}' — usage: connect.sh --label <label> [--store <path>]" >&2
exit 2 ;;
esac
done
# Load secrets quietly (sourced, never echoed).
if [ -f "$ENV_FILE" ]; then
set -a; # shellcheck source=/dev/null
. "$ENV_FILE"; set +a
fi
PY="python3"; [ -x "$VENV_PY" ] && PY="$VENV_PY"
exec "$PY" "$HERE/connect.py" "$@"