← back to Dw Signup Fulfillment

arm-webhook-env.sh

166 lines

#!/usr/bin/env bash
# TK-11114 — Option 1: arm the LIVE dw-signup-fulfillment service env so its
# WEBHOOK_URL_TOKEN matches the token the EXISTING customers/create webhook
# already carries. It does NOT register/delete/modify any webhook (no new scope,
# no duplicate-webhook risk — the opposite end from refresh-webhook.sh/Option 2).
#
# WHY IT MATTERS
#   server.js webhookAuth (~L72): if config.WEBHOOK_URL_TOKEN is set and the path
#   token != it  -> 401; if it is UNSET while live -> 503. Either way the signup
#   callback is rejected and no verify letter is sent.
#   config.js resolves WEBHOOK_URL_TOKEN with firstEnv order:
#       process.env  ->  <project>/.env  ->  ~/Projects/secrets-manager/.env
#   The pm2 ecosystem env does NOT set this key, so the highest-priority live
#   source is the PROJECT .env. This script sets it there (and .env is never
#   rewritten by deploy.sh), so the running service resolves the webhook's token.
#
# SAFE BY CONSTRUCTION
#   * DRY-RUN by default — prints the plan and exits. Mutates only with --apply.
#   * Self-verifying + idempotent — if the live service ALREADY resolves the
#     webhook's token it reports NO-OP and exits 0 without changing anything.
#     (This is what resolves the "is the token actually mismatched?" question on
#     prod, where the truth lives — Mac2 is classifier-blocked from prod SSH.)
#   * Reversible — timestamped .env backup before any write; rollback printed.
#   * Never touches DRY_RUN (go-live state) or any other unrelated env key.
#
# GATED — this changes the LIVE customer-facing service (env + pm2 restart). Run
# on the Kamatera prod box, pasted by Steve (Mac2 is blocked from prod SSH):
#   ! ssh root@45.61.58.125 'cd /root/Projects/dw-signup-fulfillment && git pull -q && bash arm-webhook-env.sh'         # dry-run first
#   ! ssh root@45.61.58.125 'cd /root/Projects/dw-signup-fulfillment && bash arm-webhook-env.sh --apply'                # then apply
set -euo pipefail

APPLY=0
for a in "$@"; do [ "$a" = "--apply" ] && APPLY=1; done

# --- locate project + inputs (portable: Mac2 steve / Kamatera root) ---------
SELF="${BASH_SOURCE[0]:-}"
if [ -n "$SELF" ] && [ -f "$SELF" ]; then PROJECT_DIR="$(cd "$(dirname "$SELF")" && pwd)"
elif [ -d /root/Projects/dw-signup-fulfillment ]; then PROJECT_DIR=/root/Projects/dw-signup-fulfillment
else PROJECT_DIR="$PWD"; fi
ENV_FILE="$PROJECT_DIR/.env"
SECRETS="$HOME/Projects/secrets-manager/.env"
SHOP="designer-laboratory-sandbox.myshopify.com"
BASE="https://$SHOP/admin/api/2024-10"
PM2_APP="dw-signup-fulfillment"
PORT="9862"

mask(){ local t="${1:-}"; if [ "${#t}" -gt 6 ]; then printf '…%s' "${t: -6}"; else printf '%s' "$t"; fi; }
# readers: always succeed (empty on miss) so `set -e` doesn't kill an absent-key lookup
sget(){ { grep -E "^$1=" "$SECRETS" 2>/dev/null || true; } | head -1 | cut -d= -f2- | sed -E "s/^[\"']|[\"']\$//g"; }
eget(){ { grep -E "^$1=" "$2" 2>/dev/null || true; } | head -1 | cut -d= -f2- | sed -E "s/^[\"']|[\"']\$//g"; }

# --- required secrets (task item 1: read VERIFY_SECRET + fulfillment token) --
FT="$(sget SHOPIFY_FULFILLMENT_TOKEN)"
VS="$(sget DW_SIGNUP_VERIFY_SECRET)"   # config.js reads VERIFY_SECRET from this key
[ -z "$FT" ] && { echo "ABORT: SHOPIFY_FULFILLMENT_TOKEN not found in $SECRETS"; exit 1; }
[ -z "$VS" ] && { echo "ABORT: DW_SIGNUP_VERIFY_SECRET not found in $SECRETS"; exit 1; }

# --- read the token the EXISTING webhook carries (the source of truth) -------
WH_JSON="$(curl -fsS "$BASE/webhooks.json?topic=customers/create" -H "X-Shopify-Access-Token: $FT")" \
  || { echo "ABORT: could not read webhooks from Shopify (check SHOPIFY_FULFILLMENT_TOKEN scopes)"; exit 1; }

PARSE="$(printf '%s' "$WH_JSON" | python3 -c "
import sys,json
whs=[w for w in json.load(sys.stdin).get('webhooks',[]) if '/webhooks/customers/create/' in (w.get('address') or '')]
if not whs:
    print('NONE NONE 0'); sys.exit(0)
if len(whs) > 1:
    print('MULTI NONE %d' % len(whs)); sys.exit(0)
w=whs[0]; tok=(w.get('address') or '').rstrip('/').split('/')[-1]
print(w.get('id'), tok, 1)
")"
read -r WH_ID WH_TOKEN WH_N <<<"$PARSE"

if [ "$WH_ID" = "NONE" ]; then
  echo "ABORT: no customers/create webhook is registered. Option 1 has nothing to match."
  echo "       (This is the 'do NOT register a webhook' case flagged by the peer session;"
  echo "        registering one is Option 2 / a separate gated decision — see refresh-webhook.sh.)"
  exit 1
fi
if [ "$WH_ID" = "MULTI" ]; then
  echo "ABORT: $WH_N customers/create webhooks are registered (duplicate). Option 1 can only match ONE token."
  echo "       Resolve the duplicates first (delete the stale one), then re-run."
  exit 1
fi

# --- what the SERVICE resolves right now -------------------------------------
CUR_ENV="$(eget WEBHOOK_URL_TOKEN "$ENV_FILE")"
CUR_SECR="$(sget WEBHOOK_URL_TOKEN)"
if [ -n "$CUR_ENV" ]; then CUR="$CUR_ENV"; SRC="$ENV_FILE"
elif [ -n "$CUR_SECR" ]; then CUR="$CUR_SECR"; SRC="$SECRETS"
else CUR=""; SRC="(unset)"; fi

echo "project dir           : $PROJECT_DIR"
echo "webhook id            : $WH_ID"
echo "webhook token         : $(mask "$WH_TOKEN")  (len ${#WH_TOKEN})"
echo "service resolves      : $(mask "$CUR")  from $SRC"
echo "secrets-manager holds : $(mask "$CUR_SECR")"

if [ "$CUR" = "$WH_TOKEN" ]; then
  echo
  echo "RESULT: NO-OP — the live service already resolves the webhook's token."
  echo "        There is no token mismatch; TK-11114's signup failure (if any) is NOT the"
  echo "        webhook token — look at the SEND side (service->George). No env change made."
  exit 0
fi

echo
echo "RESULT: MISMATCH — service uses $(mask "$CUR"), webhook sends $(mask "$WH_TOKEN")."
echo "        Option 1 fix = set WEBHOOK_URL_TOKEN=<webhook token> in $ENV_FILE"
if [ "$APPLY" != "1" ]; then
  echo
  echo "(dry-run) re-run with --apply to write the env, restart pm2, and verify. Nothing changed."
  exit 0
fi

# --- APPLY (reversible) ------------------------------------------------------
TS="$(date +%Y%m%dT%H%M%S)"
if [ -f "$ENV_FILE" ]; then
  cp "$ENV_FILE" "$ENV_FILE.tk11114.bak.$TS"; echo "backup: $ENV_FILE.tk11114.bak.$TS"
else
  : > "$ENV_FILE"; echo "created $ENV_FILE"
fi

upsert(){ # upsert KEY VALUE into $ENV_FILE (idempotent, value-safe via python)
  local k="$1" v="$2"
  if grep -qE "^$k=" "$ENV_FILE"; then
    python3 - "$ENV_FILE" "$k" "$v" <<'PY'
import sys,re
f,k,v=sys.argv[1],sys.argv[2],sys.argv[3]
lines=open(f).read().splitlines()
out=[(k+'='+v) if re.match('^'+re.escape(k)+'=',l) else l for l in lines]
open(f,'w').write('\n'.join(out)+'\n')
PY
  else
    printf '%s=%s\n' "$k" "$v" >> "$ENV_FILE"
  fi
}

upsert WEBHOOK_URL_TOKEN "$WH_TOKEN"
# ensure the rest of the verify path is present, but NEVER clobber an existing value
grep -qE '^DW_SIGNUP_VERIFY_SECRET=' "$ENV_FILE"    || upsert DW_SIGNUP_VERIFY_SECRET "$VS"
grep -qE '^SHOPIFY_FULFILLMENT_TOKEN=' "$ENV_FILE"  || upsert SHOPIFY_FULFILLMENT_TOKEN "$FT"
chmod 600 "$ENV_FILE"
echo "wrote WEBHOOK_URL_TOKEN=$(mask "$WH_TOKEN") to $ENV_FILE"

echo "restarting pm2 $PM2_APP (re-reads .env at boot)…"
pm2 restart "$PM2_APP" >/dev/null 2>&1 || echo "WARN: 'pm2 restart $PM2_APP' failed — restart it manually."

sleep 2
echo "--- verify (no handler side effects) ---"
curl -s -o /dev/null -w "  /healthz            -> %{http_code} (expect 200)\n" "http://127.0.0.1:$PORT/healthz" || true
curl -s -o /dev/null -w "  bad-token POST      -> %{http_code} (expect 401 = token now enforced, not 503-unset)\n" \
  -X POST "http://127.0.0.1:$PORT/webhooks/customers/create/deadbeefdeadbeef" -H 'Content-Type: application/json' -d '{"id":0}' || true
NOW_ENV="$(eget WEBHOOK_URL_TOKEN "$ENV_FILE")"
[ "$NOW_ENV" = "$WH_TOKEN" ] && echo "  .env now resolves    -> $(mask "$NOW_ENV") == webhook token ✓" \
                             || echo "  .env now resolves    -> $(mask "$NOW_ENV") (UNEXPECTED — investigate)"

echo
echo "ROLLBACK:"
if [ -f "$ENV_FILE.tk11114.bak.$TS" ]; then
  echo "  cp '$ENV_FILE.tk11114.bak.$TS' '$ENV_FILE' && pm2 restart $PM2_APP"
else
  echo "  (no prior .env) remove the WEBHOOK_URL_TOKEN line from $ENV_FILE && pm2 restart $PM2_APP"
fi
echo "DONE — TK-11114 Option 1 applied."