← back to Costa Rica
cycle 30 docs: YOLO_NOTES ledger — WhatsApp inbound cost guard + retry safety
cce97e4b3e485c475b193ed9d0a415c3b60dfb51 · 2026-09-24 07:47:39 -0700 · Steve
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic
Files touched
Diff
commit cce97e4b3e485c475b193ed9d0a415c3b60dfb51
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Sep 24 07:47:39 2026 -0700
cycle 30 docs: YOLO_NOTES ledger — WhatsApp inbound cost guard + retry safety
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic
---
YOLO_NOTES.md | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)
diff --git a/YOLO_NOTES.md b/YOLO_NOTES.md
index 35ae97c..a432291 100644
--- a/YOLO_NOTES.md
+++ b/YOLO_NOTES.md
@@ -637,3 +637,23 @@ Canonical apply order now: `004_marketplace → 005_apple → 006_contacts → 0
**Latent (noted, not fixed):** the refund UPDATE has no clawback link to the payouts row — moot while completion/payout is unwired; tracked with the refund-after-payout deferred item.
**Backlog:** 7 gated memos + 2 SIWA + 1 admin-auth GO-LIVE decisions on Steve's desk. Audited surfaces so far: charge leg (c19), payout leg (c23), ingest (c24), SIWA auth (c27), admin authZ (c28), payment-webhook state machine (c29). Next cycle: lib/whatsapp.js inbound handler (phone-spoof / impersonation / send-loop cost) OR lib/money.js money-math invariants — both still un-audited.
+
+---
+
+## Cycle 30 — cold Cody audit of the WhatsApp inbound handler: cost guard + retry-safety (TK-10346)
+
+**Surface:** lib/whatsapp.js handleInbound + the routes/webhooks.js POST /whatsapp auto-reply. Chose it over the booking/pricing path (already server-computed + heavily guarded: R5 input guards, int4 overflow, max-nights cap; money.js has solid integer/bps/non-negative guards).
+
+**Sound (Cody confirmed, matched my pre-checks):** impersonation — `m.from` is Meta-signature-verified and only ever echoed back to the sender; `whatsapp_contacts.user_id` FK is unused; no inbound `wa_id` flows into a cross-user/app_users lookup. Stored-XSS (profile_name/body stored raw) and SSRF (sendImage/Document link) are LATENT — no admin surface renders WA data, no caller invokes the media sends. Documented all three as traps in migrate_013's header + GO-LIVE.
+
+**2 real fixes:**
+1. **Cost amplification** (exploitable today, zero preconditions) — the keyword auto-reply fired one Meta-BILLED `sendButtons` per inbound message with NO rate limit. Added a **durable per-contact 60s cooldown** via an atomic conditional UPDATE (`SET last_auto_reply_at=NOW() WHERE ... IS NULL OR < NOW()-interval`) on a new `migrate_013` column — both checks+claims the slot (no TOCTOU, survives restarts, unlike an in-process Map in cluster mode). rowCount 0 → skip the billed send.
+2. **Silent inbound-message loss** — `handleInbound` errors were swallowed (warn + 200) AFTER the idempotency marker was claimed, so Meta never retried → permanent loss. Now releases the marker + 500s (handleInbound is idempotent), scoped to the persistence step only.
+
+**Cody's diff-gate earned its keep — caught a REAL regression in my own fix:** the cost-guard UPDATE sat OUTSIDE any try/catch, in the loop whose comment promises "always 200s to Meta." A DB blip on that UPDATE (after the marker is committed) would escape → 500 → Meta's retry dedupes to 200 → the auto-reply silently lost for this AND every remaining event in the batch (same bug class as the #4b I was fixing, and inconsistent — the flakier network send WAS wrapped, the local UPDATE wasn't). Fixed: the whole per-event body is now one try/catch, fail-CLOSED on cost. Tests E (throw → still 200, no send) + F (one event's error doesn't abort the batch) cover it.
+
+**Test:** `test/wa-webhook-cooldown.test.js` — cooldown won/skipped/non-keyword, handleInbound-throw→marker-release+500, cooldown-throw→200, batch isolation. Suite 234→**240**, serial green. Commit `1f25a1f`.
+
+**Cost:** $0 (local + two Cody passes — audit found the 2 bugs, the gate found the regression in the fix).
+
+**Backlog:** 7 gated memos + 3 GO-LIVE decisions on Steve's desk. Audited surfaces: charge leg (c19), payout leg (c23), ingest (c24), SIWA auth (c27), admin authZ (c28), payment-webhook (c29), WhatsApp inbound (c30). Remaining un-audited: lib/money.js money-math invariants (though the booking route wrapping it is well-guarded), lib/plaid.js (host ACH onboarding), lib/reconcile.js edge cases, or routes/build.js / logo-agent.js (non-money admin tools). Next cycle: lib/money.js or lib/plaid.js.
← 1f25a1f costa-rica: WhatsApp inbound hardening — auto-reply cost gua
·
back to Costa Rica
·
costa-rica: fail-closed admin guard when site gate unset (TK 3031607 →