← back to Secrets Manager

postbubbe-rotation-reminder.sh

63 lines

#!/usr/bin/env bash
# postbubbe-rotation-reminder.sh
# Durable daily reminder for the OUTSTANDING post-bubbe-RCE Tier-1 secret rotation.
# Compares each Tier-1 key's current registry digest against the 2026-05-31
# baseline; a key is ROTATED when its digest changes (Steve regenerates in the
# provider console, then `cli.js import-paste` rewrites the digest).
#   - any still un-rotated  -> macOS notification + log line, exit 0
#   - ALL rotated           -> success notification, SELF-RETIRE (unload + disable
#                              the LaunchAgent), drop a RESOLVED marker, exit 0
# Self-contained, no args. Driven by com.steve.postbubbe-rotation-reminder.
set -uo pipefail

SM="$HOME/Projects/secrets-manager"
BASELINE="$SM/postbubbe-tier1-baseline.json"
REGISTRY="$SM/registry.json"
LOG="$SM/postbubbe-rotation-reminder.log"
PLIST="$HOME/Library/LaunchAgents/com.steve.postbubbe-rotation-reminder.plist"
LABEL="com.steve.postbubbe-rotation-reminder"
MARKER="$SM/.postbubbe-rotation-RESOLVED"
STAMP="$(date '+%Y-%m-%d %H:%M:%S')"

note(){ printf '[%s] %s\n' "$STAMP" "$*" >> "$LOG"; }
notify(){ # title, message
  /usr/bin/osascript -e "display notification \"$2\" with title \"$1\" sound name \"Glass\"" >/dev/null 2>&1 || true
}

[ -f "$BASELINE" ] || { note "ERROR baseline missing: $BASELINE"; exit 0; }
[ -f "$REGISTRY" ] || { note "ERROR registry missing: $REGISTRY"; exit 0; }

# Compare digests in node (jq may be absent); print "ROTATED a b" / "PENDING x y".
RESULT="$(BASELINE="$BASELINE" REGISTRY="$REGISTRY" /usr/bin/env node -e '
const fs=require("fs");
const base=JSON.parse(fs.readFileSync(process.env.BASELINE,"utf8")).keys||{};
const reg=JSON.parse(fs.readFileSync(process.env.REGISTRY,"utf8")).secrets||{};
const pending=[], rotated=[];
for(const k of Object.keys(base)){
  const was=base[k].digest||null;
  const now=(reg[k]&&reg[k].digest)||null;
  if(now && was && now!==was) rotated.push(k); else pending.push(k);
}
console.log("PENDING\t"+pending.join(","));
console.log("ROTATED\t"+rotated.join(","));
' 2>>"$LOG")"

PENDING="$(printf '%s\n' "$RESULT" | awk -F'\t' '/^PENDING/{print $2}')"
ROTATED="$(printf '%s\n' "$RESULT" | awk -F'\t' '/^ROTATED/{print $2}')"
PCOUNT=0; [ -n "$PENDING" ] && PCOUNT="$(printf '%s' "$PENDING" | tr ',' '\n' | grep -c .)"

if [ "$PCOUNT" -eq 0 ]; then
  note "ALL Tier-1 keys rotated ($ROTATED). Retiring reminder."
  notify "✅ Post-bubbe rotation complete" "All Tier-1 keys rotated. Retiring the daily reminder."
  printf 'RESOLVED %s\nrotated: %s\n' "$STAMP" "$ROTATED" > "$MARKER"
  # Self-retire the LaunchAgent (modern bootout, fall back to legacy unload).
  launchctl bootout "gui/$(id -u)/$LABEL" 2>/dev/null || launchctl unload "$PLIST" 2>/dev/null || true
  # Disable RunAtLoad/relaunch by renaming the plist aside so it won't reload.
  [ -f "$PLIST" ] && mv "$PLIST" "$PLIST.retired-$(date +%Y%m%d)" 2>/dev/null || true
  exit 0
fi

note "STILL OUTSTANDING ($PCOUNT): $PENDING"
notify "🔒 Rotate $PCOUNT post-bubbe keys" "Still live & unrotated: ${PENDING}. Regenerate in console → cli.js import-paste. Order: CF/GoDaddy/Stripe/Shopify → OpenAI/Gemini/ElevenLabs."
exit 0