# Shared container-side primitives. Secret JSON travels through stdin or private files. set -eu set -o pipefail umask 077 STATE="$PWD/.local/dev" TOOL_STATE=$STATE mkdir -p "$STATE" WORK=$(mktemp -d) cleanup() { rm -rf "$WORK" chown -R "${LOCAL_UID:-0}:${LOCAL_GID:-0}" "$TOOL_STATE" } trap cleanup EXIT trap 'exit 130' INT trap 'exit 143' TERM fail() { echo "$*" >&2; exit 1; } compose() { docker compose --project-directory . -f infra/local/compose.yml --env-file "$STATE/images.env" "$@"; } write() { target=$1; mode=${2:-600} mkdir -p "$(dirname "$target")" cat > "$target.tmp" chmod "$mode" "$target.tmp" mv "$target.tmp" "$target" } # Token is never placed in curl's arguments, errors or response logs. vault() ( path=$1; method=${2:-GET}; missing=${3:-no} header=$(mktemp "$WORK/header.XXXXXX") response=$(mktemp "$WORK/response.XXXXXX") trap 'rm -f "$header" "$response"' EXIT printf 'Content-Type: application/json\n' > "$header" if [ -n "${VAULT_TOKEN:-}" ]; then printf 'X-Vault-Token: %s\n' "$VAULT_TOKEN" >> "$header"; fi if [ "$method" = GET ]; then code=$(curl -s --connect-timeout 5 --max-time 30 -H "@$header" -o "$response" -w '%{http_code}' "http://vault:8200/v1/$path") || exit 1 else code=$(curl -s --connect-timeout 5 --max-time 30 -H "@$header" -X "$method" --data-binary @- -o "$response" -w '%{http_code}' "http://vault:8200/v1/$path") || exit 1 fi case "$code" in 200|204) if [ -s "$response" ]; then cat "$response"; else printf '{}'; fi;; 404) if [ "$missing" = yes ]; then printf 'null'; else echo "Vault $path returned HTTP $code" >&2; exit 1; fi;; *) echo "Vault $path returned HTTP $code" >&2; exit 1;; esac ) existing() { [ -f "$STATE/admin/init.json" ] || fail 'Missing .local/dev/admin/init.json; restore matching bootstrap material' VAULT_TOKEN=$(jq -er '.root_token' "$STATE/admin/init.json") export VAULT_TOKEN } kv() { vault "kv/data/$1" GET yes | jq 'if . == null then null else .data.data end'; } put() { jq '{data:.}' | vault "kv/data/$1" POST >/dev/null; } random() { printf '{"bytes":%s,"format":"hex"}' "${1:-32}" | vault sys/tools/random POST | jq -er '.data.random_bytes'; } ensure() ( path=$1 value=$(kv "$path") if [ "$value" = null ]; then put "$path"; fi ) # Never forward mongosh errors: authentication/command errors may include secrets. mongo() { compose exec -T mongo mongosh --quiet --file /dev/stdin > "$WORK/mongo-result" 2> "$WORK/mongo-error" || return 1; cat "$WORK/mongo-result"; } admin_auth() { kv infrastructure/mongo/bootstrap | jq -r '"const admin=db.getSiblingDB(\"admin\"); admin.auth(\"local-admin\","+(.password|tojson)+");"'; } email_valid() { jq -e '(.api_key|type=="string") and (.api_key|test("\\S"))' >/dev/null; } export_ca() { compose exec -T frontend cat /data/caddy/pki/authorities/local/root.crt | write "$STATE/ca.pem" 644 }