{"slug":"costa-rica","total":154,"limit":100,"offset":0,"since":null,"commits":[{"hash":"22c104f","date":"2026-09-26 08:24:44 -0700","author":"Steve","subject":"YOLO_NOTES: Steve 2026-09-26 park ruling (Q1-Q4) — TK-10346","body":"Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01EuMfhSKGfQhQLMrdVKD7MR"},{"hash":"3031607","date":"2026-09-26 08:23:30 -0700","author":"Steve","subject":"costa-rica: fail-closed admin guard when site gate unset (TK-10346, Steve-approved option B)","body":"/api/admin, /admin, /api/build, /build, and /api/logo-agent have no auth of\ntheir own -- they ride the same site-wide BASIC_AUTH_* switch as the public\ndirectory. docs/GO-LIVE.md SS8 plans to remove BASIC_AUTH_* to open the\npublic directory, which would also silently strip admin's only lock\n(host-claim approvals + traveler PII, the ops dashboard, and the logo tool).\n\nAdd a small requireAdminGate{Json,Html} middleware so those admin-only\nsurfaces respond 503 {error:'admin_gate_unconfigured'} (HTML routes: plain\n503 text) when BASIC_AUTH_USER/PASS are unset, instead of serving openly.\nWhen the gate IS configured this is a no-op -- unauthenticated requests are\nalready rejected 401 by the existing basic-auth gate before reaching these\nroutes, so behavior is unchanged in the configured case. Public directory\nroutes (/api/map, /api/places, etc.) are untouched.\n\nAdds test/admin-gate-fail-closed.test.js (red case: gate unset -> 503 on\nevery admin surface, public directory unaffected) and\ntest/admin-gate-configured.test.js (gate set -> 401 as before, guard is a\nno-op). Full suite: 240 -> 251 passing, 0 failures.\n\nAlso hardens docs/GO-LIVE.md SS8 with a HARD pre-launch requirement: a\nseparate cookie-session admin auth (SameSite=Strict, own ADMIN_* secret,\ndecoupled from BASIC_AUTH_*) must ship before BASIC_AUTH_* is removed; until\nthen this fail-closed guard makes admin go dark, not open, if it is.\n\nCo-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01EuMfhSKGfQhQLMrdVKD7MR"},{"hash":"cce97e4","date":"2026-09-24 07:47:39 -0700","author":"Steve","subject":"cycle 30 docs: YOLO_NOTES ledger — WhatsApp inbound cost guard + retry safety","body":"Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"1f25a1f","date":"2026-09-24 07:47:00 -0700","author":"Steve","subject":"costa-rica: WhatsApp inbound hardening — auto-reply cost guard + marker-release on failure (cycle 30) — TK-10346","body":"Cold Cody audit of the WhatsApp inbound handler. Impersonation is sound (m.from is\nMeta-signature-verified + only ever echoed back to itself; no cross-user lookup),\nand stored-XSS / SSRF-via-send-link are LATENT (no admin surface renders WA data,\nno caller invokes sendImage/sendDocument) — documented as traps in migrate_013.\nTwo REAL fixes:\n\n- COST AMPLIFICATION (exploitable today, zero preconditions): the keyword auto-reply\n  fired one Meta-BILLED sendButtons per inbound message with NO rate limit — anyone\n  who can WhatsApp the number could drive unbounded billed sends. Added a durable\n  per-contact cooldown via an atomic conditional UPDATE on a new\n  whatsapp_contacts.last_auto_reply_at column (migrate_013): the UPDATE both\n  checks+claims the 60s slot (no TOCTOU, survives restarts), rowCount 0 -> skip.\n- SILENT INBOUND-MESSAGE LOSS: handleInbound errors were swallowed (warn + 200)\n  AFTER the idempotency marker was claimed, so Meta never retried -> permanent loss.\n  Now releases the marker + 500s (handleInbound is idempotent), scoped to the\n  persistence step only (auto-reply send failures stay best-effort + 200).\n\nCody's diff-gate then caught a REAL regression in my first pass: the cost-guard\nUPDATE sat OUTSIDE any try/catch, in the loop whose comment promises \"always 200s\".\nA DB blip on that UPDATE (after the marker is committed) would escape the loop -> 500\n-> Meta's retry dedupes to 200 -> the auto-reply silently lost for this AND every\nremaining event in the batch (same bug class as the #4b it fixes, inconsistent since\nthe flakier network send WAS wrapped). Fixed: the whole per-event body is now one\ntry/catch, fail-CLOSED on a cooldown error (no billed send), never escaping the loop.\n\ntest/wa-webhook-cooldown.test.js: cooldown won->sends / within-window->skips /\nnon-keyword->no-check / handleInbound-throw->marker-release+500 / cooldown-UPDATE-\nthrow->still-200-no-send (E) / one event's error doesn't abort the batch (F).\nSuite 234 -> 240, serial green.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"d94c613","date":"2026-09-24 07:08:20 -0700","author":"Steve","subject":"cycle 29 docs: YOLO_NOTES ledger — payment-webhook refund-path fixes","body":"Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"762091f","date":"2026-09-24 07:07:50 -0700","author":"Steve","subject":"costa-rica: fix payment-webhook refund path — ONVO status map, per-event idempotency key, guarded refund UPDATE (cycle 29) — TK-10346","body":"Cold Cody audit of the webhook state machine: the confirm path is idempotent even\nunder concurrency (confirmBooking WHERE status='pending'), but the REFUND path had\na live asymmetric gap. Three fixes:\n\n- ONVO refunds were silently swallowed: lib/payments/onvo.js STATUS_MAP had no\n  refund key, so getCharge() could never return 'refunded' -> a refund mapped to\n  'processing' (the || default), REGRESSING payments.status and NEVER flipping the\n  booking to refunded. Added refunded/reversed -> 'refunded' (tilopay already did\n  this) + a documented GO-LIVE gate: ONVO's real post-refund vocabulary is\n  UNVERIFIED (may be Stripe-like where the PI stays 'succeeded').\n- Idempotency-key collision: evId could equal the charge id (Tilopay's shape is\n  {paymentId, status} with no distinct event id), so a 'succeeded' then a genuinely\n  distinct 'refunded' event for the SAME charge both keyed firstTime() on the same\n  id -> the 2nd hit ON CONFLICT DO NOTHING, was treated as 'dup', and the refund was\n  NEVER processed (booking confirmed forever, silently). evId now prefers a distinct\n  event id, else a paymentId:type composite so lifecycle events get distinct keys\n  while true replays still dedupe. (Reorder also fixes event_id being shadowed by\n  paymentId.) ref reuses the same derived chargeId, byte-identical.\n- The refunded booking UPDATE now sets updated_at=NOW() (every other status mutation\n  does; a reconcile keyed off it would miss refunds) and guards status<>'refunded'\n  (idempotent replay). Booking status is a single 'refunded' enum, so the guard only\n  no-ops a redundant event, never blocks a needed transition.\n\ntest/webhooks-refund-idempotency.test.js: distinct keys for succeeded-vs-refunded,\nreplay still dedupes to one payments UPDATE, a refunded getCharge reaches the guarded\nUPDATE, ONVO mapStatus. Suite 230 -> 234, serial green.\n\nCody-gated (money path). The gate confirmed the fix + caught a doc contradiction:\nGO-LIVE had called Tilopay's reversed path \"verified\" while item 3 says neither map\nis live-verified; folded Tilopay into the same unverified + void-vs-refund caveat\n(reversed can be an auth VOID, not a refund) — whichever provider goes live first\nmust pass the $1 refund test.\n\nLatent (noted, not fixed): the refund UPDATE has no clawback link to the payouts row\n— moot while the completion/payout pipeline is unwired; tracked with refund-after-payout.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"fc341d1","date":"2026-09-24 07:06:54 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-24T07:06:26 (1 data files) — docs/GO-LIVE.md","body":""},{"hash":"0b0f3fc","date":"2026-09-24 06:27:01 -0700","author":"Steve","subject":"cycle 28 docs: YOLO_NOTES ledger — admin CSRF fix + gate-coupling decision memo","body":"Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"75b27d7","date":"2026-09-24 06:25:53 -0700","author":"Steve","subject":"costa-rica: block CSRF on the admin claim-approval mutation (cycle 28) — TK-10346","body":"Cold Cody audit of the admin surface. Injection is clean, PII selects are scoped,\nand the basic-auth mount ordering correctly covers /api/admin TODAY. But\nPOST /api/admin/claims/:placeId/:hostId (approve/reject a host claim) was\nCSRF-able: HTTP basic-auth creds are auto-attached cross-site by the browser, and\nexpress.urlencoded is mounted globally, so an attacker page's auto-submitting\n<form> (application/x-www-form-urlencoded — a \"simple\" request, no CORS preflight)\ncould drive the mutation against a logged-in admin. CORS doesn't help (it's scoped\nto /api/app and doesn't apply to a simple form POST anyway).\n\nFix: the mutation now requires application/json. A cross-site simple form cannot\nset that content-type without triggering a CORS preflight, which /api/admin does\nnot answer -> the forged POST is rejected 415 before any DB write. The real admin\nUI already sends application/json (public/admin.html), so it's transparent.\nVerified req.is() matches with/without charset and that application/json is a\nnon-simple content-type (attacker can't send it cross-site to a CORS-less route).\n\ntest/admin-claims-csrf.test.js: a form-urlencoded and a text/plain POST are both\n415 with NO UPDATE; the JSON path still approves; a bad decision is still 400.\nSuite 226 -> 230. (Minimal content-type guard on a surface just Cody-audited this\ncycle + a proving test + reasoned bypass analysis; committed without a re-gate,\nproportionate to task weight.)\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"edda387","date":"2026-09-24 05:52:47 -0700","author":"Steve","subject":"cycle 27 docs: YOLO_NOTES + GO-LIVE — SIWA hardening + 2 deferred pre-launch decisions","body":"Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"3114048","date":"2026-09-24 05:51:48 -0700","author":"Steve","subject":"costa-rica: harden Sign-in-with-Apple verify — JWKS rotation refetch, require exp, no error leak (cycle 27) — TK-10346","body":"Cold Cody audit of SIWA: the crypto core is SOUND (empirically — 5 forge attempts\nincl. alg:none/HS256-confusion all fail; alg is never read, RSA-SHA256 is forced,\nkid only selects among Apple-fetched keys). No takeover path. Three real\nhardening/availability gaps fixed:\n\n- JWKS never refetched on a kid-miss -> a valid token signed with Apple's NEWLY\n  ROTATED key 401'd for up to the 1h cache TTL (silent outage on Apple's schedule).\n  Now a kid-miss forces ONE cooldown-guarded refetch to pick up the rotation.\n- exp was checked only-if-present -> a signature-valid token with no exp never\n  expired. Now REQUIRED (=== undefined -> reject; exp:0 still hits the expiry check).\n- routes/app.js /auth/apple leaked e.message (fetchT timeout text, JSON.parse\n  errors) to the client, violating this file's own M2/R4 policy 6 lines below. Now\n  logs server-side + returns a generic 401.\n\nCody's diff-gate then caught a REAL concurrency bug in my first pass: _lastFetch\nwas written after the awaits, so a burst of concurrent kid-miss requests all read\nthe stale clock and each fired a fetch (Cody reproduced: 20 concurrent garbage\nkids -> 20 fetches), defeating the anti-hammer guard AND risking an Apple-side\nrate-limit self-DoS. Fixed with an in-flight-promise dedup (start the cooldown\nclock synchronously before the await; concurrent callers join the single fetch) —\nwhich also kills the pre-existing hourly cache-expiry thundering-herd for free.\n\ntest/apple-verify.test.js: generates a REAL RSA keypair, serves it as Apple's JWKS,\nand proves valid verifies; tampered/empty-sig/wrong-key/bad-aud/bad-iss/expired/\nno-exp reject; a kid-miss refetches a rotated key; and a 20-way concurrent\ngarbage-kid burst causes <=1 refetch. Also fixed a bug the test found in my own\ncooldown env-parse (Number('0')||60000 swallowed a legit 0). Suite 216 -> 226.\n\nDEFERRED as pre-launch DECISIONS (not mechanical, no live exploit): account-\nsplitting when an Apple email differs from a pre-existing account's (needs a merge\nflow), and nonce/replay hardening (needs a client-side ceremony). Flagged in\nYOLO_NOTES + GO-LIVE.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"4f11ad1","date":"2026-09-24 05:09:46 -0700","author":"Steve","subject":"cycle 26 docs: YOLO_NOTES ledger — cr-osm-match isolation + MEIC dedup decision memo","body":"Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"f12347a","date":"2026-09-24 05:08:01 -0700","author":"Steve","subject":"costa-rica: cr-osm-match.js — per-record isolation + guaranteed pool close (cycle 26) — TK-10346","body":"Same reliability gap Cody flagged in cycle 24/25, on the last untouched script:\nthe OSM->places website matcher looped over places doing a pool.query UPDATE per\nrecord with NO try/catch, so one bad UPDATE aborted the whole match pass. Worse,\nthe IIFE had no outer try at all — the trailing `await pool.end()` was skipped on\nany throw, leaking a pg connection.\n\nFix (mirrors the cycle-25 ingest pattern): per-record try/catch (errors++, warn\ncapped at 20, continue) + wrap the whole IIFE in try/catch/finally so pool.end()\nalways runs and a crash exits 1 instead of an unhandled rejection. Added a\nstructural regression guard to test/ingest-resilience.test.js. Suite 215 -> 216.\n\nMechanical copy of an already-Cody-gated pattern (cycle 25) onto one file + a\nstandard try/finally; self-verified (continue still works inside the try;\npool.end runs exactly once) rather than re-gated — proportionate to task weight.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"86389af","date":"2026-09-24 04:40:39 -0700","author":"Steve","subject":"cycle 25 docs: YOLO_NOTES ledger — ingest fetch timeout + per-record isolation","body":"Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"f10bf96","date":"2026-09-24 04:40:10 -0700","author":"Steve","subject":"costa-rica: ingest layer resilience — fetch timeout + per-record isolation (cycle 25) — TK-10346","body":"Cold Cody audit (cycle 24) of the ingest layer found two reliability holes\n(no live exploit path, unlike the XSS also found that cycle):\n  - no fetch timeout anywhere in scripts/ingest/_lib.js: a hung gov site\n    (CKAN, ICT WordPress) could stall the sequential run-all.js FOREVER.\n  - one poison record aborted the WHOLE run in meic-pymes.js + ict-cst.js\n    (no per-row try/catch) -> a bad row at #500 drops the other ~14,500.\n\nFix:\n- _lib.js: fetchText/fetchJson/fetchBuffer now default to an AbortSignal.timeout\n  (30s HTML/JSON via SCRAPER_TIMEOUT_MS, 60s file-download via a SEPARATE\n  SCRAPER_BUFFER_TIMEOUT_MS knob); a caller-supplied opts.signal is honored as-is.\n- meic-pymes.js + ict-cst.js: wrapped the per-record loop body in its own\n  try/catch (errors++, warn capped at 20, continue) mirroring the existing\n  google-places.js/local-portals.js pattern; finishRun downgrades to\n  status='partial' (not 'error'/exit 1) when errors>0 -- already a live\n  status value, no schema/consumer impact (verified: no CHECK constraint,\n  the only 2 consumers just display the string).\n\nCody gate caught a real bug in my own first pass: fetchBuffer's timeout fell\nthrough `Number(opts.timeoutMs) || fallbackMs || DEFAULT_TIMEOUT_MS` where an\nabsent opts.timeoutMs -> NaN (falsy) -> the hardcoded 60000 fallbackMs literal\nalways won, silently making SCRAPER_TIMEOUT_MS a no-op for the one fetch (the\nmulti-MB MEIC XLSX) that most needs a real override on a slow link. Gave\nfetchBuffer its own dedicated SCRAPER_BUFFER_TIMEOUT_MS env var instead of a\nfallback chain; verified with a live timing test that swaps env vars and reads\nactual elapsed ms (not just code inspection). test/ingest-resilience.test.js:\nbehavioral timeout tests (fake never-resolving fetch) + structural isolation\nguards. Suite 209 -> 215, serial green.\n\nDeprioritized (Cody, correctly): osm-fetch.js's raw fetch has no client timeout\nbut isn't in run-all.js's ORDER array, so it can't stall the pipeline -- low\npriority, manual-run-only script. cr-osm-match.js has the same per-record-no-\nisolation pattern on a DB-only loop (no fetch) -- queued for a later cycle.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"6564e86","date":"2026-09-24 04:05:40 -0700","author":"Steve","subject":"cycle 24 docs: YOLO_NOTES ledger — stored-XSS fix on place.html + ingest audit findings","body":"Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"d79ae5e","date":"2026-09-24 04:04:38 -0700","author":"Steve","subject":"costa-rica: fix stored XSS on public/place.html — escape every scraped-field sink (cycle 24) — TK-10346","body":"Cold Cody audit of the ingest layer: external scraped fields (name/website/\nemail/source/image_url/credit — all attacker-editable via Google Business\nProfile / OSM tags / portal listings) were concatenated RAW into innerHTML and\nLeaflet bindPopup on the live consumer page, and place.html had no esc() at all.\nA business named `X\"><img src=x onerror=…>` executed on every visitor to /p/<slug>\n(cookie/session theft, no auth). No SQL injection anywhere (ingest parameterizes).\n\nFix — escape at output, allow-list URL protocols:\n- Port esc() (same helper index.html uses) + add safeUrl() (http/mailto/tel only,\n  blocks javascript:/data:) + safeImg() (http(s) + root-relative /img/…, blocks\n  script + protocol-relative — safeUrl would wrongly blank a localized image).\n- Wrap all sinks: credit, d-email, d-website, d-source, bindPopup(p.name), the\n  website CTA href, and the hero imgEl.src.\n- Rebuild the siblings block via DOM (createElement + textContent + a DOM-set,\n  encodeURI'd background-image) — image_url landed in a CSS url() inside a style\n  attribute, a context HTML-escaping can't secure.\n- Align the CTA email href with d-email (encodeURIComponent — blocks mailto param\n  injection).\n- test/place-page-xss.test.js: evals the SHIPPED esc/safeUrl/safeImg against attack\n  payloads AND asserts every named-field sink routes through them (regression guard).\n\nTwo Cody passes (audit + diff-gate); the gate caught the initially-missed hero\nimgEl.src. Suite 205 -> 209, serial green. NOTE: this closes the hole in source;\nthe prod deploy to Kamatera is gated (see pending-approval memo).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"a52a312","date":"2026-09-24 03:22:45 -0700","author":"Steve","subject":"cycle 23 docs: YOLO_NOTES ledger — cr_iban dead-payout fix + live-mode test coverage","body":"Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"789a630","date":"2026-09-24 03:21:31 -0700","author":"Steve","subject":"costa-rica: close the cr_iban dead-payout path — completeness CHECKs + live fail-loud + live-mode test (cycle 23) — TK-10346","body":"Cold Cody audit of the payout leg: kind='cr_iban' was registerable (route + DB\nkind CHECK) but never wired — it routes to rail='sinpe' and tilopay.payout()\nonly reads sinpe_phone, so a bank/IBAN host got a {phone:null} transfer ->\nsilently $0 (or a stuck 'processing') in LIVE, invisible in sandbox tests.\n\nFix (mirrors the already-blessed plaid_ach pattern — registerable, fails loud\nin live until wired):\n- migrate_012: CHECK sinpe_movil carries a sinpe_phone and cr_iban carries a\n  cr_iban, so no incomplete method can strand a host at $0 (DB backstop).\n- lib/payouts.js: cr_iban + liveMode throws before the phone-less transfer;\n  the payout row is marked 'failed' + the error surfaces.\n- routes/app.js POST /host/payout-methods: 400 an incomplete method before INSERT.\n- tests: route 400s (no INSERT) + accepts a complete method; DB CHECK rejects\n  incomplete (23514); and a NEW live-mode file proves the guard actually fires\n  for cr_iban+live (row lands 'failed') AND does not false-fire for\n  sinpe_movil+live (reaches provider.payout()) — the headline throw had zero\n  coverage before (Cody gate finding). Suite 203 -> 205, serial green.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"1ccb0ed","date":"2026-09-24 02:44:25 -0700","author":"Steve","subject":"cycle 22 docs: YOLO_NOTES ledger — per-event WhatsApp auto-reply isolation","body":"Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\n\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"8578926","date":"2026-09-24 02:44:04 -0700","author":"Steve","subject":"costa-rica: per-event isolation for the WhatsApp inbound auto-reply loop (Cody cycle-13 deferred item) — TK-10346","body":"routes/webhooks.js POST /whatsapp wrapped handleInbound() AND the whole per-event\nauto-reply for-loop in ONE try/catch. So a multi-message payload where the auto-reply\nsendButtons for message N threw (a Graph API error, or a timeout now bounded to\n~15s by a prior cycle's fetchT wiring) jumped straight to the outer catch — messages\nN+1.. in that same batch got NO auto-reply. Deferred from cycle 13 as a real,\nnon-blocking follow-up (the old unbounded-hang version of this bug was worse: an\nentire request hang; fetchT already reduced the blast radius to \"skip the rest of\nthis batch\").\n\nFix: handleInbound() keeps its own try/catch (a throw there means no events at all,\nroute still 200s). Each event's sendButtons call now has its OWN try/catch — a\nfailure on one event is logged and does not skip its siblings. The route always\nres.sendStatus(200) regardless (never triggers a Meta retry storm).\n\nCody gate — clean pass, ship it, verified with a NEGATIVE test (not just a positive\none): stashed only the route file, reran the new test against the OLD code -> it\nFAILED (1/13) as expected (proving the test is a real regression guard, not\ntheater); against the NEW code -> 13/13. Also verified: the argument-evaluation\nconcern (does ev.contact.wa_id throwing outside the try break the 200-to-Meta\nguarantee?) is a false alarm — argument evaluation is part of the call expression,\nwhich sits inside the per-event try; and handleInbound's own return shape can never\nproduce an event with a missing .contact (if contactByWaId throws, handleInbound\nthrows first, caught by the outer catch, before the loop ever runs). No new\ndouble-processing (firstTime dedup runs before handleInbound); same-wa_id double-send\nin one batch is pre-existing, unrelated to this diff; log-and-swallow matches the\nexisting confirmBooking WA-notify pattern.\n\nTests (+1, suite 197 -> 198): stub handleInbound with 2 greeting events, make the\nfirst sendButtons throw, assert the second is still attempted and the route still 200s.\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"ab1e4be","date":"2026-09-24 02:19:46 -0700","author":"Steve","subject":"cycle 21 docs: YOLO_NOTES ledger — deterministic (serial) test suite","body":"Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\n\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"b6fb0a1","date":"2026-09-24 02:19:19 -0700","author":"Steve","subject":"costa-rica: make the test suite deterministic — serialize + isolate real-DB tests (cycle 21) — TK-10346","body":"The suite intermittently false-red'd (~1/3 -> then rarer) under `node --test`'s\ndefault file-level PARALLELISM. Root cause was NOT one data collision but\nCONCURRENCY resource pressure surfacing in several unrelated places:\n  - booking-pay-idempotency created bookings on place_id=1 at CURRENT_DATE..+1,\n    colliding with a parallel file under the bookings_no_overlap_stay EXCLUDE.\n  - server-routes.test.js's q-floor asserted oneChar.total === totalAll (two\n    separately-timed COUNT queries) — a parallel test creating a throwaway ACTIVE\n    place (double-book) shifted the count between them.\n  - async-harden.test.js's HTTP-server E2E tests flaked under heavy parallel load\n    (event-loop / resource pressure), nothing to do with data.\n\nThree unrelated flake sources => the fix is to remove the concurrency, not\nwhack-a-mole each collision. Definitive fix: `npm test` now runs\n`node --test --test-concurrency=1` (files serial; tests within a file were already\nsequential). Verified: 15 consecutive serial runs, 0 failures; a full serial run is\n~3.3s (negligible vs the flakiness it removes).\n\nBelt-and-suspenders data isolation (so a future accidental parallel run is also\nsafe): booking-pay-idempotency's 3 bookings now use distinct far-future date windows\n(CURRENT_DATE + 3000/3100/3200, in the free 2000-5000 range, clear of payouts 1000+,\npayments-race 6000+, reconcile 7000+); server-routes' q-floor uses a tolerance\n(|oneChar-total| < 100, filtered << total-100) instead of exact equality, so a\nconcurrent place insert/delete can't false-red it while still proving \"1-char q is\nunfiltered, >=2-char filters\".\n\nTest-only, reversible, no prod/externality. Suite 197/197 (now deterministically).\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"1ca7bf7","date":"2026-09-24 01:51:23 -0700","author":"Steve","subject":"cycle 20 docs: YOLO_NOTES ledger — stale-payment reconciler + gated cron memo","body":"Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\n\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"b86e7b6","date":"2026-09-24 01:50:24 -0700","author":"Steve","subject":"costa-rica: stale-payment reconciler (dropped-webhook rescue + orphan-confirm) — Cody-gated, cycle 20 — TK-10346","body":"Closes the pre-existing gap Cody flagged in cycle 19: a payment leaves 'processing'\nonly via a provider webhook or a traveler's GET /payments/:id poll, so a dropped\nwebhook + an abandoned app session strands it 'processing' forever — and since\ncycle-19's one-in-flight index, the booking becomes permanently un-payable.\n\nlib/reconcile.js `reconcileStalePayments({ olderThanMinutes=15, failAfterMinutes=null,\nlimit=200 })` — exported, idempotent, safe to run repeatedly. NOT wired to a cron\n(that's gated — drafted to pending-approval).\n  Pass A: polls the provider (getCharge, fetchT-bounded) for stale in-flight payments\n    and applies whatever it has RESOLVED — succeeded -> UPDATE + confirmBooking; failed\n    -> UPDATE (frees the in-flight slot); refunded -> UPDATE + booking refunded. A\n    getCharge timeout/error is left UNTOUCHED (never fail a possibly-succeeded charge).\n  Pass B: confirms bookings orphaned by a succeeded-payment-but-confirmBooking-failed.\n\nCody gate — FIX FIRST, all applied:\n- FORCE-FAIL is now OPT-IN (default null), was default-ON at 1440min. Cody: force-failing\n  a still-'processing' charge frees the in-flight slot -> booking payable again -> if the\n  traveler re-pays AND the original later lands (the webhook UPDATE has no status guard,\n  so it flips 'failed'->'succeeded'), they're charged TWICE. So it defaults OFF (stuck\n  payments are surfaced via the return counts, not auto-failed); when a caller passes a\n  finite failAfterMinutes it requires an EXACT 'processing' status and logs each hit for\n  ops to verify no charge landed.\n- PASS B (Cody hole #2, real orphan gap the module existed for but couldn't see): the\n  reconciler's SELECT filters status='processing', so a payment the webhook durably marked\n  'succeeded' whose confirmBooking then threw (retry never landed, abandoned session never\n  polled) left the booking 'pending' FOREVER, invisible to Pass A. Added Pass B: JOIN\n  payments succeeded + bookings pending -> confirmBooking. Idempotent.\n- refund's `UPDATE bookings SET status='refunded'` now guarded `WHERE status IN\n  ('confirmed','pending')` so it can't clobber a 'completed'/'cancelled' booking.\n\nResidual (documented): the adapters' mapStatus coerces an UNKNOWN provider status to\n'processing', so an enabled force-fail could mislabel a disputed/under-review charge —\nmitigated by force-fail being off-by-default + the per-hit ops log.\n\nTests (+4, suite 193 -> 197, real DB, self-cleaning, unique far-future date windows to\navoid the parallel-file EXCLUDE flake): resolves succeeded/failed/refunded + leaves\nfresh/unreachable alone + opt-in force-fail; idempotent 2nd run; force-fail OFF by\ndefault leaves a past-TTL payment 'processing'; Pass B confirms a succeeded-but-pending\nbooking.\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"d0912c8","date":"2026-09-24 01:18:21 -0700","author":"Steve","subject":"cycle 19 docs: YOLO_NOTES ledger + GO-LIVE migrate_011 prerequisite — double-charge race fix","body":"Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\n\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"08a9915","date":"2026-09-24 01:17:34 -0700","author":"Steve","subject":"costa-rica: fix double-charge race on /pay — DB unique index + reuse-in-flight (cold-audit find, cycle 19) — TK-10346","body":"A fresh cold Cody audit found a VERIFIED, reproduced money-losing race: POST\n/bookings/:code/pay did a lock-free check-then-insert (SELECT \"any in-flight\npayment?\" -> INSERT a 'processing' row -> provider.createCharge()), so two truly\nconcurrent pay requests for one booking (double-tap \"Pay\" on flaky mobile data, or\na client auto-retry racing a manual retry) BOTH passed the SELECT, BOTH inserted,\nand BOTH called the processor -> the traveler's card charged TWICE for one booking.\nNo adapter sends a provider idempotency key, so the processor doesn't dedupe either.\nReproduced at the DB layer with two interleaved pg connections. The payout leg had\nthe mirror guard (payouts_one_per_booking_rail); the charge leg was missing it.\n\nFix:\n1. migrate_011_payments_one_inflight.sql — a PARTIAL unique index enforcing at most\n   one in-flight ('processing'/'requires_action') payment per booking. A booking can\n   still have many failed/succeeded/refunded payments (a retry after a 'failed'\n   attempt is fine); only concurrent in-flight charges are blocked.\n2. routes/app.js — the pre-charge INSERT is wrapped: on a 23505 from THIS index\n   (checked by e.constraint) the race-loser REUSES the winner's payment instead of\n   firing a second charge. Postgres serializes it — the loser's INSERT blocks on the\n   winner's row, then 23505s against the now-committed winner.\n\nCody gate (red-teamed its own find) — logic correct (traced PG locking), 3 deploy/\nedge fixes applied:\n- CONCURRENTLY: migrate_011 was plain CREATE UNIQUE INDEX -> a SHARE lock on the\n  HOTTEST write table (every checkout). Changed to CREATE UNIQUE INDEX CONCURRENTLY\n  (apply-migrations.sh runs it outside a txn) — matching migrate_010's own lesson.\n- Pre-flight dupe check: prod may ALREADY have >=2 in-flight rows per booking (from\n  this very bug), which would fail the index build. Added the GROUP BY HAVING check +\n  a remediation step (mark all-but-newest 'failed', verify against the processor) to\n  the migration runbook.\n- The resolved-winner 500 (real, was untested): if the winner's charge resolves fast\n  (sandbox/live instant succeed) before the loser's catch runs, an in-flight-ONLY\n  re-SELECT misses it -> the loser 500s though its sibling's payment SUCCEEDED. Fixed:\n  the catch re-SELECTs the booking's most-recent payment (NO status filter) and returns\n  its real status, so the loser always gets a payment_id to poll, never a 500.\n\nPROD-APPLY is gated (rides the go-live migration pass; runbook has the pre-check +\nCONCURRENTLY). Dev-applied (reversible: DROP INDEX).\n\nTicket (pre-existing, NOT this diff): nothing (webhook/poll aside) moves a truly-stuck\n'processing' payment off that status — a dropped webhook + abandoned app session =\na permanently-unpayable booking. Needs a reconciler/TTL. The in-flight SELECT gate\nalready had this exposure; this index hardens it, doesn't introduce it.\n\nTests (+3, suite 190 -> 193): DB-level index rejects a 2nd in-flight insert (23505)\n+ allows a retry-after-failed; route reuses an in-flight winner (0 createCharge);\nroute handles a RESOLVED winner without a 500.\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"f326dd4","date":"2026-09-24 00:40:30 -0700","author":"Steve","subject":"cycle 18 docs: YOLO_NOTES ledger — env-independent test suite","body":"Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\n\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"2263da5","date":"2026-09-24 00:40:05 -0700","author":"Steve","subject":"costa-rica: make the test suite env-independent — load dotenv in booking-pay-idempotency (Cody cycle-17 follow-up) — TK-10346","body":"booking-pay-idempotency.test.js is a REAL-DB integration test (requires ../lib/db,\nwhich builds the pg pool from process.env.DATABASE_URL at load) but — unlike its\nsiblings payouts/host-listing-race/double-book/server-routes — never called\nrequire('dotenv').config(). So a bare `node --test test/*.test.js` or a fresh CI\nrunner with no exported DATABASE_URL failed its 4 tests with\n`database \"macstudio3\" does not exist` (pg falling back to the OS-username DB) — a\nfalse red that only passed because the prior session had DATABASE_URL exported.\n\nFix: added `require('dotenv').config()` at the top (before the lib/db require),\nmatching payouts.test.js's existing convention. It was the only real-DB test file\nusing the real pool that lacked the line (the other DATABASE_URL-lacking test files\nmock pool.query, so they never connect).\n\nVerified: `unset DATABASE_URL; node --test test/*.test.js` -> 190/190 (was 186/190),\nand `npm test` -> 190/190. Safe: .env has NO provider creds (only DATABASE_URL /\nBASIC_AUTH / PG* / SITE_*), so loading dotenv can't flip any payment adapter to live\nmode; and no test relies on DATABASE_URL being absent.\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"cb24e9d","date":"2026-09-24 00:16:38 -0700","author":"Steve","subject":"cycle 17 docs: YOLO_NOTES ledger — pool consolidation + explicit max, crash-on-idle fix","body":"Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\n\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"f45a796","date":"2026-09-24 00:16:03 -0700","author":"Steve","subject":"costa-rica: consolidate server.js onto lib/db's shared pool + explicit pool max (Cody gate, cycle 17) — TK-10346","body":"Cody's cycle-16 root-cause: server.js created its OWN `new Pool(...)` separate from\nlib/db.js's \"single shared\" pool, so the running server held TWO pools against one\nDB (double the connections; the cycle-16 test-hang was the first symptom).\n\nFix: server.js now `const { pool } = require('./lib/db')` — dropped the duplicate\n`new Pool(...)` + the now-unused `{ Pool } = require('pg')` import. All 35 inline\npool.query() call sites are unchanged (the const now points at the shared pool);\napp.locals.pool === lib/db.pool (verified true at runtime), so the test cleanup\ncloses one pool for every route.\n\nThis is STRICTLY BETTER, not just cleanup (Cody verified empirically): server.js's\nold private pool had NO `pool.on('error')` handler; lib/db's has one. A node-pg Pool\nthat emits 'error' (idle-client disconnect, DB restart blip) with no listener throws\nas an uncaughtException — NOT caught by the cycle-10 unhandledRejection net (wrong\nevent class), so the OLD code would have crashed the whole process on a single idle\nblip. The shared pool handles it.\n\nAlso (Cody's \"do now\"): added an EXPLICIT `max` to lib/db's pool\n(`Number(process.env.PG_POOL_MAX) || 20`). The old two-pool setup accidentally\nallowed ~20 connections (2 x node-pg's default 10) — never a chosen capacity;\nconsolidating would have silently halved it to 10. 20 makes that ceiling a\ndeliberate, documented, env-tunable value on the single fork-mode process\n(Postgres max_connections=100).\n\nCody gate — SHIP IT, verified strictly-better on every axis (read pm2/pg source,\nran live EventEmitter + default-max probes, grepped every lib/db requirer + every\npool.end/SIGTERM/app.locals.pool use): require-order safe (dotenv line 3 before the\nrequire; lib/db was already required by the route modules anyway); no graceful-\nshutdown pool.end() exists to break; nothing else reads app.locals.pool or imported\nthe old pool.\n\nFollow-up logged (pre-existing, NOT this diff): booking-pay-idempotency.test.js-class\ntests require lib/db without calling dotenv.config(), so a bare `npm test` (no\nexported DATABASE_URL) fails 3 tests — env-dependent; they should load dotenv\nthemselves so CI is env-independent.\n\nSuite 190/190.\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"1d24359","date":"2026-09-23 23:48:44 -0700","author":"Steve","subject":"cycle 16 docs: YOLO_NOTES ledger — server.js app export + inline-route tests","body":"Records the require.main-guarded app export (prod boot Cody-proven unchanged via\nlive pm2 fork-mode probe), the two-pool cleanup fix, the closed cycle-15 q-floor\nroute-test debt, and the new follow-up: consolidate server.js's duplicate pool\nonto lib/db's shared one.\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"2d3cca4","date":"2026-09-23 23:48:04 -0700","author":"Steve","subject":"costa-rica: export the app from server.js so inline routes are testable (Cody gate, cycle 16) — TK-10346","body":"The ~20 inline server.js routes (/api/places, /api/search, /api/provinces, /api/stats,\n/api/leads, /r/:slug, /p/:slug, ...) had ZERO test coverage because server.js called\napp.listen on import and never exported the app (surfaced by Cody in cycle 15).\n\nRefactor: `module.exports = app` + guard preflight + app.listen behind\n`if (require.main === module)`, and expose the module-scoped pg pool on\n`app.locals.pool` so a test can close it. Prod boot is UNCHANGED — server.js is\nstarted only via `node server.js` (pm2 fork mode + npm start), where\nrequire.main === module is true, so preflight + listen run exactly as before.\nNothing in the repo imports ./server (grep-confirmed) except the new test.\n\nCody gate — prod boot verified BULLETPROOF, empirically (not just reasoned): Cody\nread pm2 6.0.14 source + ran a live fork-mode probe confirming ProcessContainerFork\ndoes `_load(script, null, /*isMain*/ true)`, so require.main === module is true the\nwhole time server.js runs — and the same holds even if someone later adds cluster\n`instances` (ProcessContainer does the same _load). preflight ordering below the\nexport is behaviorally identical (a throw still crashes the pm2 child pre-listen ->\nautorestart, unchanged).\n\nCody also caught a real latent TEST bug + fixed: server.js maintains its OWN pg pool\nseparate from lib/db.js's shared pool (the mounted sub-routers use lib/db's). Importing\nserver.js creates BOTH; the test's cleanup only closed app.locals.pool, so the next\ntest hitting a sub-router (/api/admin, /webhooks) would hang the test process on a\ndangling connection. Now closes both. (Root cause — server.js duplicating lib/db's\n\"single shared\" pool — is a follow-up ticket, not this diff.)\n\nTests (+5, suite 185 -> 190, new test/server-routes.test.js): closes the cycle-15\ndebt — the /api/places q>=2 floor now has a real route test (a 1-char q returns the\nfull unfiltered count; a >=2 term filters) — plus the basic-auth gate (401\nunauthenticated), pagination Link header, limit clamp, and /health liveness. Run\nagainst the real dev DB via the exported app on an ephemeral port.\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"f899e1c","date":"2026-09-23 23:17:30 -0700","author":"Steve","subject":"cycle 15 docs: YOLO_NOTES ledger + GO-LIVE pg_trgm prerequisite","body":"Records the SQL-correctness review (clean) + the search trigram-index fix\n(Cody-benchmarked ~20x), the CONCURRENTLY + q-floor fixes, the documented\ninline-server-route test-harness gap, and the pg_trgm superuser prereq in\nthe go-live migration step.\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"3bd9aa2","date":"2026-09-23 23:16:25 -0700","author":"Steve","subject":"costa-rica: trigram GIN indexes for the search hot path + /api/places 1-char-q guard (Cody gate, cycle 15) — TK-10346","body":"SQL-correctness review. NO correctness bugs found: admin/build stats use independent\nscalar subqueries (no double-count JOIN), all queries are parameterized (no\ninjection), the booking overlap query is already indexed (idx_bookings_place_status_checkin\n+ a gist EXCLUDE). The one real, MEASURED issue was search performance.\n\n/api/search + /api/places filter with a leading-wildcard LIKE '%q%' on\nlower(name)/lower(address)/lower(description) — unindexable by btree, so every search\nfull-scanned the 34,285-row (growing) places table; the no-LIMIT COUNT(*) queries\nalways did, and /api/search can't short-circuit (it ORDER BYs a computed rank).\n\nFix: scripts/migrate_010_search_trgm.sql — pg_trgm + three GIN trigram indexes on\nlower(name|address|description). Measured on dev (Cody independently benchmarked):\nthe count query went from a ~30ms parallel seq scan to a ~1.5ms Bitmap Index Scan\n(BitmapOr of the three trgm indexes) — a ~20x win on the two hottest public reads,\nfor ~11.6MB index storage. Index expression lower(col) gin_trgm_ops matches the\nquery's LOWER(col) LIKE exactly (all call sites .toLowerCase() the pattern in JS).\n\nCody gate — FIX FIRST, both applied:\n1. The migration recommended CONCURRENTLY in a comment but shipped plain CREATE\n   INDEX (a copy-paste footgun: a plain build SHARE-locks the live places table,\n   ~620ms today, worse as it grows). Changed to CREATE INDEX CONCURRENTLY (safe:\n   apply-migrations.sh runs file 010 outside a transaction) + an invalid-index\n   recovery note. Also added the benchmarked write-cost justification (~44µs/row GIN\n   overhead, immaterial vs the network-bound one-row-per-fetch scraper writes;\n   hacienda-enricher never touches these columns).\n2. /api/places had NO min-length floor on q (unlike /api/search's length>=2) — a\n   1-char ?q=a full-scanned and returned ~the whole directory. Added a >=2 floor\n   (verified via dev EXPLAIN: a short q now builds the plain PK-index listing, no\n   scan).\n\nNOTE (future cycle): inline server.js routes (~20 of them, incl. /api/places) have\nno test harness — server.js listens on import and doesn't export the app. The q-guard\nis verified by dev EXPLAIN, not a route unit test; a follow-up should export the app\n(guard app.listen behind require.main === module) to make these routes testable.\n\nExtension + indexes applied to the dev DB (reversible: DROP INDEX/EXTENSION). Prod\nCREATE EXTENSION pg_trgm needs superuser -> Steve-gated (documented in the migration).\nSuite unchanged at 185/185 (indexes don't affect app behavior/results).\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"3de6348","date":"2026-09-23 23:13:29 -0700","author":"auto-commit-fleet","subject":"auto-data-snapshot: 2026-09-23T23:13:08 (1 data files) — scripts/migrate_010_search_trgm.sql","body":""},{"hash":"9afa84b","date":"2026-09-23 22:44:57 -0700","author":"Steve","subject":"cycle 14 docs: YOLO_NOTES ledger + GO-LIVE processorFee prerequisite","body":"Records the money-math/invariant review: MAX_BOOKING_NIGHTS cap, the per-column\nint4-overflow guard (Cody caught the fees column overflows before total), the\nmin_nights lockout guard, and the deferred processorFee/CHECK gap now landed as\na go-live prerequisite in docs/GO-LIVE.md §6.\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"faf2ab6","date":"2026-09-23 22:43:44 -0700","author":"Steve","subject":"costa-rica: bound booking money amounts against int4 overflow + availability-squat + min_nights lockout (Cody gate, cycle 14) — TK-10346","body":"Money-math / invariant review (new class — fetch/async/fail-closed were saturated).\nThe bookings money columns are all INTEGER (int4, max 2,147,483,647) with NO\nupper bound on the date range or the computed amounts.\n\nFixes:\n1. MAX_BOOKING_NIGHTS cap (default 365, env-configurable): an unbounded date range\n   made subtotal = base_price * n overflow int4 (a 500), and even below overflow\n   let a client create a multi-decade booking that squats the listing's\n   availability (the overlap guard then blocks every real booking for years).\n2. Per-column overflow guard on ALL stored amounts (subtotal, fees, platform_fee,\n   total, host_payout), not just total.\n3. min_nights > MAX_BOOKING_NIGHTS rejected at /host/listings write time (a\n   min_nights above the cap makes a listing permanently unbookable).\n\nCody gate — caught that my first attempt guarded the WRONG column and confirmed it\nby executing computeSplit: `fees = cleaningFee + platformFee` reaches ~2x total, so\na huge host-set cleaning_fee (uncapped by any DB CHECK) overflows the `fees` int4\ncolumn at the DEFAULT 10% fee while `total` is still under int4 max — a total-only\nguard would still let the INSERT 500. Repro (verified): cleaningFee=1_999_999_000 ->\ntotal=1,999,999,001 (under) but fees=2,199,998,900 (over). Fixed to guard every\nstored column. Cody also required the NEGATIVE test (TK-11431 doctrine: a check\nships with a test proving it goes red on the injected fault) — the prior tests only\ndrove subtotal, never fees. Cody cleared: slot branch funnels through the same\nguard; Number.isSafeInteger is needed (a huge host-set max_guests can push subtotal\npast MAX_SAFE_INTEGER); deferring the processorFee/CHECK gap with docs is correct.\n\nDEFERRED (documented, dead code today): computeSplit supports processorFeeBps>0 and\nsubtracts processorFee from hostPayout, but bookings has no processor_fee column and\nthe CHECK is total=platform_fee+host_payout — enabling a processor fee would 500\nevery booking. The /bookings route never passes processorFeeBps, so it's inert;\nbefore enabling one, add a processor_fee column + fix bookings_total_reconciles\n(go-live prerequisite, recorded in YOLO_NOTES + docs/GO-LIVE.md).\n\nTests (+4, suite 181 -> 185): over-long stay -> 400 no INSERT (before the overlap\nquery); subtotal-overflow -> 400; FEES-overflow with total-under-int4 -> 400 (the\nnegative test for the column my first guard missed); min_nights>cap -> 400 no listing.\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"a5c06b3","date":"2026-09-23 22:13:39 -0700","author":"Steve","subject":"cycle 13 docs: YOLO_NOTES ledger — whatsapp + apple fetch bounds (last two unbounded live fetches)","body":"Records the whatsapp Graph API + apple JWKS fetchT swaps (all 5 live provider\nfetches now bounded), build.js verified clean, the gitleaks false-positive\nhandling, and the Cody-required route-level apple timeout test.\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"1d3aefd","date":"2026-09-23 22:12:47 -0700","author":"Steve","subject":"costa-rica: bound Apple JWKS fetch with fetchT (Cody-cleared, cycle 13) — TK-10346","body":"Second unbounded live fetch found this cycle: lib/apple.js jwks() (fetches Apple's\nSign-in-with-Apple public signing keys for identity-token verification) used raw\nfetch() with no timeout. Swapped to the shared fetchT (5th identical instance after\ntilopay/onvo/plaid/whatsapp — the last raw live fetch in the codebase).\n\nWHY IT MATTERS: without a bound, a hung connection to appleid.apple.com would hang\nPOST /auth/apple forever. The route's try/catch turns a verifier ERROR into a 401\nbut never sees a HANG. fetchT makes the hang a catchable PROVIDER_TIMEOUT -> 401.\n\nCody gate — clean, ship it. Verified: one production caller (verifyIdentityToken <-\nPOST /auth/apple, try/catch-wrapped); no DB write before verification resolves; the\n1h JWKS cache is assigned atomically only after fetchT AND res.json() resolve, so a\ntimeout leaves the old/empty cache intact (never a stale-but-trusted key); keep the\nuniform 15s (the JWKS doc is a tiny CDN-backed static payload fetched ~once/hour via\ncache, not per-login — shortening risks false-401ing real logins on flaky mobile).\n\nCody-required follow-up (met, matching the plaid-routes precedent): added\napple-route-timeout.test.js driving the REAL verifier through POST /auth/apple with\na stalled fetch, asserting 401 AND pool.query never called.\n\nTest tokens are built from parts (not JWT-shaped literals) so the gitleaks\npre-commit hook doesn't false-positive on a constant.\n\nTests (+3, suite 178 -> 181): apple-jwks-timeout.test.js (2) + apple-route-timeout.test.js (1).\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"3089df8","date":"2026-09-23 22:06:59 -0700","author":"Steve","subject":"costa-rica: bound WhatsApp Graph API fetch with fetchT (Cody-cleared, cycle 13) — TK-10346","body":"Cycle-13 audit of lib/whatsapp.js. _send() (the Meta Graph API sender) used raw\nfetch() with no timeout — the same unbounded-hang class already closed for\ntilopay/onvo/plaid. Swapped to the shared fetchT.\n\nWHY IT MATTERS despite sends being best-effort: callers wrap wa.send*() in\ntry/catch, but that catches an ERROR, not a HANG. A stalled Graph connection on\nraw fetch() never rejects, so `await wa.sendText(...)` would hang FOREVER,\nstalling confirmBooking and its money-path callers (/pay, the payment webhook,\nGET /payments/:id). fetchT turns the hang into a catchable PROVIDER_TIMEOUT throw.\n\nCody gate (focused — mechanically identical to 3 prior precedented swaps, so\nscoped to caller-side handling of the new throw rather than re-auditing fetchT\ninternals): SHIP IT, clean. Exhaustive grep found exactly 2 real call sites\n(confirmBooking's notify + the webhooks inbound auto-reply), both already\ntry/catch-wrapped as best-effort; the throw happens in _send BEFORE any\nDB persistence (contactByWaId/logMessage never run); no .catch() anywhere\nsilently eats it; markRead/sendTemplate/sendList/sendImage/sendDocument/\nsendLocation have ZERO callers in the repo (not a landmine, just unused).\n\nNoted, not a regression (pre-existing, made LESS bad not worse — follow-up\nticket, not a blocker): routes/webhooks.js's inbound auto-reply wraps the WHOLE\nper-payload for-loop in one try/catch, not per-event — a stalled sendButtons on\nmessage N would skip auto-replies for messages N+1.. in the same batch. Before\nthis diff the same batch would have hung the ENTIRE webhook request forever;\nnow it's bounded to one timeout window. Move the try/catch inside the loop in a\nfuture cycle for per-event isolation.\n\nTests (+3, suite 175 -> 178): stalled body -> PROVIDER_TIMEOUT; Graph HTTP error\nfails closed (throws before any DB write); sandbox never hits the network.\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"11478ab","date":"2026-09-23 21:41:36 -0700","author":"Steve","subject":"cycle 12 docs: YOLO_NOTES ledger — webhook idempotency-release fix + poll rescue","body":"Records the marker-claimed-before-processing bug (transient failure lost the\nevent to dedupe), the release-on-failure fix, and the Cody-caught poll\nblind-spot (succeeded+pending) it exposed + fixed.\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"ce0a607","date":"2026-09-23 21:40:49 -0700","author":"Steve","subject":"costa-rica: webhook idempotency-marker release on failure + poll rescue for succeeded-but-pending bookings (Cody gate, cycle 12) — TK-10346","body":"Audit of routes/webhooks.js (payment webhook money path).\n\nBUG (fixed): the idempotency marker was claimed BEFORE processing. firstTime()\nINSERTs webhook_events (source, external_id) then returns first-seen; if the\nsubsequent processing (UPDATE payments / confirmBooking / refund) then throws, the\nouter catch returned 500 — but the marker was already committed, so the provider's\nRETRY (on our 500) hit the dedupe gate, got 'dup' 200, and the confirmation was\nlost forever (payment succeeded, booking never confirmed). Fix: wrap the processing\nin try/catch; on failure RELEASE the marker (DELETE webhook_events) so the retry\nre-processes, then surface the 500. Dedupe still blocks true duplicates (they\nsucceed and keep the marker) and concurrent double-delivery (ON CONFLICT DO NOTHING).\n\nCody gate — found a defense-in-depth regression the marker fix INTRODUCED, fixed\nin the same commit:\n  - The release creates a NEW valid state: payments.status='succeeded' while its\n    booking is still 'pending' (webhook marked the payment, confirmBooking then\n    failed + released the marker + 500'd for a retry). GET /payments/:id only\n    reconciled when status==='processing', so a client poll landing before the\n    provider retry would return 'succeeded' while the booking sat pending, invisible\n    to the only in-repo rescue path (there is no cron). Fix: GET /payments/:id now\n    calls confirmBooking(booking_id) whenever the payment is 'succeeded' (idempotent),\n    not just inside the 'processing' branch — so the poll rescues that state from any\n    interleaving.\n  - The release DELETE's `.catch(()=>{})` swallowed a failed release silently\n    (regressing to the original bug with no trace). Now logs distinctly which event\n    stayed broken.\nCody cleared (verified, no defect): double-processing on retry (confirmBooking is\nguarded by status='pending' -> no 2nd WhatsApp; refund UPDATE is a flat idempotent\nSET); evId/ref key drift (release deletes by the same evId firstTime inserted); and\nthe 0-rows-matched case (correctly NOT released — payments.status stays 'processing'\nso the existing poll covers it; releasing would only hammer retry budgets on\ngenuinely-unmatchable test/ping events).\n\nTests (+5, suite 170 -> 175):\n  - webhooks-route.test.js (+2): a processing failure -> 500 AND a DELETE\n    webhook_events (marker released); a successful delivery -> NO release.\n  - payment-poll-rescue.test.js (NEW, 3): a succeeded+pending booking is rescued by\n    the poll's confirmBooking; an already-confirmed booking is a no-op (no\n    double-notify); a still-processing payment polls as before.\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"47088ae","date":"2026-09-23 21:10:14 -0700","author":"Steve","subject":"cycle 11 docs: YOLO_NOTES ledger — systemic async-error hardening, real-router proof, 11-cycle summary","body":"Records the harden()/global-error-handler fix closing the request-hang gap,\nCody's required real-router test, the free double-next() documentation, and\nthe honest webhooks-is-a-no-op note. Summarizes 11 cycles of provider-agnostic\nhardening now complete.\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"16c0a2b","date":"2026-09-23 21:09:45 -0700","author":"Steve","subject":"costa-rica: wrap all sub-router async routes in an error-forwarding harden(), close the request-hang gap (Cody gate, cycle 11) — TK-10346","body":"GO-LIVE PRE-FLIGHT (systemic, deferred from cycle 10): Express 4 does NOT forward\na rejected promise from an async route handler to error-handling middleware. An\nuncaught throw inside `async (req,res)=>{...}` was an unhandledRejection — cycle 10\nadded a process-level net so that no longer CRASHES the server, but the offending\nrequest still HUNG FOREVER (no response ever sent). This closes the hang.\n\nNew lib/async-harden.js: `harden(router)` wraps every ordinary (non-error, arity<4)\nroute handler in a router's stack so a thrown/rejected handler calls next(err) ->\nthe app's global error handler returns a clean 500. A handler with its own\ntry/catch never rejects, so it's an unaffected pass-through (existing try/catch\nstill wins, no double-response). Idempotent (won't double-wrap).\n\nWired into server.js: the 5 sub-router mounts (webhooks, /api/app, /api/admin,\n/api/build, /api/logo-agent) now go through harden(); a global 4-arg error\nhandler is mounted LAST (after express.static, before the boot guard), reusing\nthe existing serverError() pattern (log full detail server-side, generic client\nmessage), guarded on res.headersSent. The ~20 INLINE server.js routes already had\ntheir own try/catch + serverError and are unaffected (this targets the sub-routers,\nwhere the gap actually was).\n\nCody gate — clears the bar, one required follow-up + one free fix, both done:\n  - REQUIRED: the original 8 tests proved the MECHANISM against a throwaway\n    router built inline in the test, never against the real routes/app.js router\n    — so a regression (server.js losing its harden(...) call) would go uncaught\n    by CI. Added test/async-harden-real-router.test.js: mounts the ACTUAL router\n    from routes/app.js through the REAL harden(), drives the genuinely-uncaught\n    `GET /listings/:slug` (routes/app.js:137, no try/catch) through a rejecting\n    pool.query, proves the full real chain resolves to a clean 500 in <5s (not a\n    hang) — plus confirms a normal call through the same hardened route is\n    unaffected.\n  - FREE: documented (not fixed — inert today, checked every handler across all\n    5 routers) the latent next()-then-later-throw double-next limitation shared\n    with upstream express-async-handler.\n  - NOTED, not a defect: harden(webhooks router) is a no-op (its handlers already\n    self-catch) — the fix's value is entirely in app/admin/build/logo-agent.\n  - Also verified (bonus, pre-existing): server.js:597's `/sitemap.xml` catch\n    block explicitly calls next(err) — it now lands on this new handler instead\n    of Express's default (which would have leaked a stack trace in dev).\n\nTests (+10, suite 160 -> 170): async-harden.test.js (8: asyncWrap sync/async\nforwarding, idempotency, E2E throw->500/normal->200/self-caught->own-status) +\nasync-harden-real-router.test.js (2, the required real-router proof).\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"5c5fc34","date":"2026-09-23 20:38:24 -0700","author":"Steve","subject":"cycle 10 docs: YOLO_NOTES ledger — plaid.js fetch bound + uncaught-throw/false-verified fixes + global unhandledRejection net","body":"Records the Plaid audit, the Cody-found holes inside the same function (bare\npool.query throws, getAuth-swallow persisting a false-verified bank), the global\ncrash net, and schedules the systemic per-route asyncHandler refactor for next cycle.\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"8c3bc3c","date":"2026-09-23 20:37:36 -0700","author":"Steve","subject":"costa-rica: bound Plaid fetch + fix uncaught-throw/false-verified holes in the host bank-linking path (Cody gate, cycle 10) — TK-10346","body":"Cycle-10 audit of lib/plaid.js (Plaid ACH bank-linking for foreign hosts).\n\nFINDING A — unbounded fetch (fixed): lib/plaid.js _post used raw fetch() with no\ntimeout, the last unbounded live provider call. Routed through the shared fetchT\n(bounds connect+headers AND the body read, like tilopay/onvo). Closes the\nPRE-FLIGHT #4/#5 class for Plaid.\n\nFINDING B — uncaught async throws crash the process (fixed for this path): the\nPlaid routes had no try/catch and this router has NO error middleware (Express 4),\nso on Node 26 a live Plaid throw is an unhandledRejection that CRASHES the whole\nmarketplace. Wrapped /host/plaid/link-token + /host/plaid/exchange -> clean\n502 (504 on PROVIDER_TIMEOUT).\n\nCody's gate found two more holes INSIDE the same function's blast radius + one\nfree global net — all fixed this cycle rather than split:\n  1. /host/plaid/exchange's two pool.query calls (INSERT payout_method, UPDATE\n     hosts) were still bare awaits below the fix — a DB throw there crashed the\n     process the same way. Wrapped -> 502.\n  2. getAuth().catch(()=>({accounts:[]})) swallowed a verification failure, then\n     persisted the payout method verified=TRUE with a NULL account_id/last4 — a\n     silently-verified bank with no payout destination. Now `verified` reflects\n     reality (!!acct.account_id); an unverified method is is_default=FALSE and is\n     NEVER set as the host default (so a future ACH payout can't target a bank\n     with no account).\n  3. Client error messages echoed Plaid's error_code + the plaid.com URL to the\n     caller (CWE-209). Now log full detail server-side, return a generic message.\n  4. Added a global process.on('unhandledRejection') net in server.js: log + stay\n     up, so an uncaught throw in ANY of the ~21 still-unwrapped async routes fails\n     that one request instead of crashing the whole server. The proper per-route\n     asyncHandler + error-middleware refactor is a separate scheduled cycle.\n\nTests (+9, suite 151 -> 160):\n  - plaid-lib-timeout.test.js (3): stalled body -> PROVIDER_TIMEOUT; 4xx fails\n    closed; sandbox no-fetch.\n  - plaid-routes.test.js (6): link-token error->502 (+no leak), timeout->504,\n    exchange error->502 (no method persisted), getAuth-fail->verified=FALSE + not\n    default, getAuth-ok->verified=TRUE + default, DB-throw-on-INSERT->502.\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"0e2af37","date":"2026-09-23 20:04:58 -0700","author":"Steve","subject":"cycle 9 docs: YOLO_NOTES ledger — admin.js audit, TOCTOU race fix, gated approval decision","body":"Records the routes/admin.js coverage add, the Cody-verified unenforced-approval\nfinding (with corrected severity: attributed-recipient not paid, since\ncreatePayoutForBooking has zero callers), the directly-fixed TOCTOU\nsilent-overwrite race, and the gated REQUIRE_HOST_APPROVAL decision memo.\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"002feea","date":"2026-09-23 20:03:20 -0700","author":"Steve","subject":"costa-rica: fix TOCTOU silent-overwrite race in /host/listings ownership (Cody gate, cycle 9) — TK-10346","body":"Cody's audit of the claim-approval finding surfaced a SHARPER, separate bug in the\nsame guard block: the guard SELECT and the place_booking upsert are separate\nstatements with no lock between them. Two hosts racing to list the same\nnever-before-listed place could BOTH read current_host=NULL, both pass the guard,\nand both run the upsert — and the old unconditional\n`ON CONFLICT (place_id) DO UPDATE SET host_id=EXCLUDED.host_id` let the second\nwriter SILENTLY overwrite the first's ownership and STILL return 200. Last commit\nwins; the loser is told nothing (a false success), and the wrong host becomes\nplace_booking.host_id — the attributed payout recipient for every booking on that\nplace.\n\nThis is a pure correctness bug with no legitimate \"intended design\" reading, so\nit's fixed directly (not gated): added\n`WHERE place_booking.host_id = EXCLUDED.host_id OR place_booking.host_id IS NULL`\nto the ON CONFLICT DO UPDATE. The single statement is now atomic — the update\napplies only when the caller already owns the row (re-list) or it is unowned\n(host deleted -> NULL); a racing non-owner matches no row -> empty RETURNING ->\nthe route now returns 409 instead of a false 200. The host-deleted re-list path\n(NULL host_id) is preserved.\n\nNOTE: this is the pure-correctness half. Whether admin approval (claim_status=\n'approved') should ALSO gate listing is a separate customer-facing onboarding\ndecision — drafted to pending-approval, NOT changed here.\n\nTests (+3, suite 148 -> 151):\n  - host-listing-race.test.js (NEW, 2, real DB): a non-owner cannot silently\n    overwrite the owner (empty RETURNING); ownership stays with the first host;\n    the owner can re-list; an unowned (NULL) place can still be claimed.\n  - host-listing-approval-gap.test.js: the route turns an empty upsert RETURNING\n    into a 409 + the upsert carries the atomic ownership WHERE clause.\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"e57398f","date":"2026-09-23 19:55:06 -0700","author":"Steve","subject":"costa-rica: add routes/admin.js coverage (was zero) + characterize the unenforced claim-approval gap — TK-10346","body":"Cycle 9 audit of routes/admin.js (previously zero test coverage).\n\nadmin-routes.test.js (+7): baseline coverage for all 4 admin endpoints —\nGET /stats, GET /claims (default pending + status=all), POST /claims/:placeId/:hostId\n(bad-decision 400 before any write, approve 200 + parameterized args, 404 on no row),\nGET /bookings.\n\nhost-listing-approval-gap.test.js (+2): CHARACTERIZATION of a real authorization\nfinding surfaced by the audit — the admin approve/reject workflow writes\nplace_hosts.claim_status, but the point that actually grants a host the money\n(routes/app.js POST /host/listings -> place_booking.host_id -> every booking's\npayout recipient) gates on claim ROW EXISTENCE, not claim_status='approved'. So a\n'pending' (or 'rejected') claim can still list. Exclusivity IS enforced (first-come\nvia the place_booking 409 guard); approval is not. The test asserts the structural\nfact (the guard SQL has no claim_status filter) so any deliberate future enforcement\nflips it visibly. Whether approval SHOULD gate listing is a customer-facing\nonboarding decision -> drafted to pending-approval, not changed here.\n\nNo source change — coverage + characterization only. Suite 139 -> 148.\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"2a75e80","date":"2026-09-23 19:29:57 -0700","author":"Steve","subject":"cycle 8 docs: YOLO_NOTES ledger — fetchT clone() hardening, Cody-required proof test","body":"Records the res.clone() Proxy hardening + the Cody-required sequential\nclone-after-read proof test (not just an asserted comment). Notes the\nprovider-agnostic money-path backlog is now fully exhausted across 5 cycles.\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"03b5617","date":"2026-09-23 19:29:24 -0700","author":"Steve","subject":"costa-rica: harden fetchT's clone() + full body-method class, proven not asserted (TK-10346)","body":"Closes the res.clone() gap Cody flagged as latent in cycle 6: a naked res.clone()\nreturned the RAW un-proxied Response, so a future retry-with-clone caller would get\nan unbounded body read with zero indication anything's wrong. No current caller\nuses clone() (reconfirmed via grep) — this is hardening-before-it-bites, not a live\nbug fix.\n\nRefactored fetchT's Proxy into a reusable wrap(r) applied to both the original\nResponse AND any res.clone() (clone tees the same incoming stream + shares the\nabort signal, so an abort still aborts both readers). Also generalized coverage\nfrom json()/text() only to the full body-consuming method class (arrayBuffer,\nblob, formData, bytes) — a caller switching methods would otherwise silently\nlose the bound.\n\nCody gate — SHIP IT, with one required addition: the code comment asserted\n\"once either read completes, a second read draws from already-buffered bytes\"\n(the safety argument for sharing one timer/done() across original+clone) but\nNO TEST proved that specific compound claim — only individual body-method\nbounding was tested. Cody built throwaway probes confirming it empirically\nagainst real WHATWG stream tee() semantics, then required it ported into the\nreal suite as a red-goes-green guardrail rather than left as a trusted\nparagraph. Added: a real delayed-ReadableStream Response, clone() BEFORE any\nread, fully drain the ORIGINAL (clearing the shared timer), then assert the\nCLONE resolves near-instantly off tee-buffered bytes rather than re-stalling\non the network with no timer left to bound it.\n\nCody also verified (clean, no defect): clone-of-clone doesn't double-wrap;\nclone-after-consumed throws synchronously (a caller bug, correctly NOT\nrelabelled as PROVIDER_TIMEOUT); Reflect.get(target,prop,target) still passes\nthe correct receiver for brand-checked getters/Symbols after generalizing the\nbody-method set; a mock missing a body method degrades to plain delegation\n(no crash).\n\nTests (+4, suite 135 -> 139): clone reads through the wrapper + original still\nindependently readable; a stalled clone body rejects PROVIDER_TIMEOUT (not\nraw/unbounded); a stalled arrayBuffer() is bounded (the full method class, not\njust json/text); the sequential clone-after-original-read proof (Cody-required).\n\nProvider-agnostic hardening backlog (#4/#5/#6/#7/§5b#2 + this clone hygiene item)\nis now fully closed. Remaining money-path items are live-credential-gated\n(CR-KYC blocked).\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"f4f8e10","date":"2026-09-23 19:00:45 -0700","author":"Steve","subject":"cycle 7 docs: YOLO_NOTES ledger + GO-LIVE runbook — §5b #2 createCharge fail-closed landed (both halves)","body":"Moves §5b #2 from live-only to landed (transport 4xx/5xx AND synchronous\n200-body decline both fail closed via shared mapStatus()). Adds an honest\nresidual-risk note to the live-only preflight: mapStatus defaults an\nUNRECOGNIZED status string to 'processing', not 'failed' — a real decline\nstring neither map's author anticipated would not fail closed, so the $1\nlive verification must watch for any processing row that never resolves.\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"b7da97c","date":"2026-09-23 19:00:00 -0700","author":"Steve","subject":"costa-rica: createCharge fail-closed on live error status + Cody-found synchronous-decline gap (PRE-FLIGHT §5b #2) — TK-10346","body":"GO-LIVE PRE-FLIGHT §5b #2 (provider-agnostic half): a live createCharge that\nreturned an HTTP error status (4xx/5xx) had its error body parsed for\npaymentId/id (-> undefined) and mis-mapped to status:'processing' — a stuck\nbooking whose webhook (UPDATE ... WHERE provider_ref=...) can never match.\n\nRound 1 (transport-level guard): tilopay.js + onvo.js createCharge now check\n`!res.ok` and return status:'failed' before extracting a ref. routes/app.js\nPOST /bookings/:code/pay now honors charge.status==='failed' — records the\npayments row 'failed' + returns 402, instead of flattening every non-succeeded\nstatus into 'processing' (which would have silently defeated the adapter guard\nat the DB layer).\n\nCody gate — FIX FIRST verdict: the transport guard only caught HTTP-status-coded\ndeclines. A SYNCHRONOUS decline via HTTP 200 + a body-level status (Stripe-like\nintent creation, which both onvo docs itself as and tilopay's own getCharge\nalready maps 'declined'->'failed' for) fell through the exact same stuck-booking\nbug, just via the body instead of the status line — and both adapters' own\nmapStatus/status-map (built + unit-tested for getCharge) already knew how to\nrecognize it; createCharge just never consulted it.\n\nRound 2 (body-level guard, Cody-directed): hoisted each adapter's status map to\na shared, exported `mapStatus()` used by BOTH createCharge and getCharge, so a\ndecline is recognized identically regardless of which call surfaces it.\ncreateCharge now checks mapStatus(j.status)==='failed' before falling through\nto 'requires_action'/'processing'. providerRef is KEPT on a body-level decline\n(flows through the normal return, not the null-ref transport-error branch) —\na later webhook/reconciliation may need it (Cody probe #5). Also: a\nswallowed error-body-read on the transport-error path now captures e.message\ninto raw instead of silently substituting {} (Cody probe #4, diagnostics only —\nstatus is already hardcoded 'failed' above the catch, so this cannot fabricate\na success).\n\nTests (+7 across 2 rounds; suite 126 -> 135):\n  - createcharge-error-status.test.js (NEW, 8 tests): 4xx fail-closed + 200-body\n    synchronous decline fail-closed (both providers) + providerRef kept + happy\n    path unchanged.\n  - pay-failed-charge.test.js (NEW, 2 tests): the POST /pay route records\n    'failed' + returns 402 on a failed charge; a succeeded charge is unaffected.\n  - payments.test.js: tilopay.mapStatus parity test (mirrors the existing\n    onvo.mapStatus regression test).\n\nProvider-agnostic hardening for #4/#5/#6/#7 + §5b #2 now complete. Remaining\nmoney-path items (sig encoding, provider-honored idempotency key, $1 verify)\nare live-only, gated on Tilopay/ONVO account provisioning (CR-KYC blocked).\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"9b74e96","date":"2026-09-23 18:28:43 -0700","author":"Steve","subject":"cycle 6 docs: YOLO_NOTES ledger + GO-LIVE runbook — #4/#5/#6/#7 hardening landed, remaining live-only items","body":"Records the PRE-FLIGHT #7 body-read timeout + Cody-found fail-OPEN fix, and updates\nthe go-live runbook to distinguish the provider-agnostic hardening now IN CODE\n(#4/#5 fetch timeout, #7 body timeout + fail-closed refund/payout, #6 local charge\nidempotency) from the remaining LIVE-ONLY preflight (sig encoding, createCharge\nnon-timeout error-status body, provider-honored idempotency key, $1 verify).\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"a00cd46","date":"2026-09-23 18:27:26 -0700","author":"Steve","subject":"costa-rica: bound provider body-read + fix fail-OPEN on refund/payout timeout (PRE-FLIGHT #7) — TK-10346","body":"GO-LIVE PRE-FLIGHT #7: the fetchT() deadline now spans the WHOLE request lifecycle\n(connect + headers AND the response body read), not just connect+headers. fetch()\nresolves once headers are in; the body (res.json()/res.text()) is read later in the\ncaller, so previously a header-fast / body-stalled provider could still hang the\nmoney path inside res.json(). Now one AbortController stays armed across the body\nread; the returned object delegates to the real Response, with json()/text() bounded\nby the same deadline and relabelled PROVIDER_TIMEOUT on abort. timer.unref() so a\nmetadata-only caller (token() throws on !res.ok, never reads the body) can't leave\nan active timer.\n\nCody gate — CRITICAL fail-OPEN found + fixed: once the body read is bounded,\nres.json() throws PROVIDER_TIMEOUT on a stall. But refund()/payout() used\n`res.json().catch(() => ({}))`, which SWALLOWED that timeout — and since res.ok was\nalready true (fast headers), they returned a FABRICATED success:\n  - tilopay payout(): {providerRef: undefined, status: 'processing', raw: {}} ->\n    payouts.js writes a stuck 'processing' payout row with a NULL provider_ref that\n    can never reconcile, and its catch/mark-failed never fires (nothing threw).\n  - tilopay/onvo refund(): {status: 'refunded', raw: {}} on a refund that never confirmed.\nFix: fail closed on a PROVIDER_TIMEOUT specifically — payout() THROWS (so payouts.js\nmarks the row 'failed' and surfaces it); refund() returns status:'failed'. A merely\nempty/malformed but fully-received 200 body is still tolerated (raw:{}), since refund\nsuccess is HTTP-status-driven — so the fix does not over-correct legitimate responses.\n\nTests (+4, suite 122 -> 126):\n  - payments-timeout.test.js: body-stall -> json() rejects PROVIDER_TIMEOUT; fast body\n    clears the deadline; metadata+body read through the Proxy off a REAL undici Response\n    (guards Reflect.get(target,prop,target) vs a receiver refactor the plain-object\n    mocks would miss).\n  - payments-body-timeout-failclosed.test.js: tilopay payout() throws + refund() ->\n    'failed' on a body-read timeout; an empty/malformed fully-received body stays\n    tolerated (not over-corrected).\n\nProvider-honored idempotency key (double-payout-on-retry guard) stays the live-only\nfollow-up; provider-agnostic hardening for #4/#5/#6/#7 now complete.\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"97da4e7","date":"2026-09-23 17:53:28 -0700","author":"Steve","subject":"cycle 5: update YOLO_NOTES.md with PRE-FLIGHT #6 completion ledger","body":""},{"hash":"62a3add","date":"2026-09-23 17:53:02 -0700","author":"Steve","subject":"costa-rica: implement payment reorder (PRE-FLIGHT #6) — pre-charge row + reuse-in-flight","body":"GO-LIVE PRE-FLIGHT #6: the payment provider-agnostic reconcilability half.\n\nMoney-path fix: POST /bookings/:code/pay now INSERTs a 'processing' payments row\nBEFORE calling createCharge() (previously it was AFTER). This ensures:\n  1. Timeout on createCharge leaves a reconcilable row (provider webhook can still find it)\n  2. Retry of same pay call reuses in-flight processing/requires_action payment\n     (returns payment_id + client_action WITHOUT firing 2nd charge)\n  3. Failed prior row allows fresh attempt\n\nControl flow reorder in routes/app.js:\n  - check for in-flight payment before calling provider\n  - INSERT 'processing' row with booking.code as idempotency token (local, not yet provider-honored)\n  - call createCharge; on timeout/error, UPDATE to 'failed' + raw error\n  - on success, UPDATE with provider_ref + final status\n\nTests (test/booking-pay-idempotency.test.js):\n  - timeout leaves 'processing' row (reconcilable)\n  - retry reuses in-flight payment (no double-charge)\n  - booking stays 'pending' on timeout (no premature confirmation)\n\nProvider-honored idempotency key (Cody #2, the live half) deferred to Tilopay/ONVO\naccount provisioning (CR-KYC blocker). This local pre-charge row is provider-agnostic\nand lands regardless.\n\nBaseline: 117/117 tests (cycle 4). After: 120/120 tests (+3 new payment-reorder tests).\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"a3d5084","date":"2026-09-23 16:15:25 -0700","author":"Steve","subject":"costa-rica: bound live provider fetch with a timeout (PRE-FLIGHT #4/#5) — TK-10346","body":"Add lib/payments/http.js fetchT(): AbortController timeout (env\nPROVIDER_HTTP_TIMEOUT_MS, default 15s) so a hung provider TCP rejects\nfail-closed instead of stranding a payment/payout in 'processing' forever.\nWired into every live fetch in tilopay.js + onvo.js. 4 new tests (117/117).\n\nCody gate: kept the wrapper (sound; fully covers getCharge/refund/payout,\nbetter-than-hang for createCharge) and CORRECTED the diff's safety comment,\nwhich falsely claimed a createCharge timeout is reconcilable — it is not,\nbecause routes/app.js writes the payments row (provider_ref) only AFTER\ncreateCharge resolves. Cody #1 (createCharge reconcilability + idempotency)\nand #3 (bound the body read too) tracked as PRE-FLIGHT #6/#7 in YOLO_NOTES.\n\nReversible, sandbox-inert (no live path exercised without creds).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic"},{"hash":"26bd9e3","date":"2026-09-12 08:48:07 -0700","author":"Steve","subject":"app API: expose region_image_url so listings can fall back (additive)","body":"Every bookable listing renders imageless: places.image_url is null on all of\nthem, nothing has ever populated that column (every image ingest targets\nregions), and the listings query had no fallback - it joined regions but took\nonly name and slug.\n\nMeasured: 8 of 9 bookable listings sit in a region that HAS a real Wikimedia\nimage (regions_with_image = 49 of 102); only Eden Atenas's region has none. So a\nfallback is viable today, which is the cheap alternative to building the\nper-listing image-sourcing pipeline that does not exist and that nobody asked\nfor.\n\nPurely additive: image_url is unchanged, the new field is separately named, no\nexisting consumer breaks, and where a region has no image it is simply null -\nstrictly non-worsening. The client-side fallback is a separate change and is NOT\nmade here.\n\nRefs TK-11518.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>"},{"hash":"5cf7dca","date":"2026-09-04 05:58:59 -0700","author":"Steve","subject":"costa-rica: fix date-dependent payouts test (113/113 green)","body":"Two tests failed today and would have passed on other dates — a genuinely flaky,\ncalendar-sensitive suite rather than a constant failure.\n\nRoot cause: mkBooking() hardcoded CURRENT_DATE + 7 .. + 9 on place_id 1 for every\nbooking. Migration 008 adds\n\n  EXCLUDE USING gist (place_id WITH =, daterange(check_in, check_out, '[)') WITH &&)\n  WHERE status IN ('confirmed','pending')\n\nso a booking created with a non-completed status is covered by the constraint, while\nthe suite's default 'completed' bookings are not. There is a seeded confirmed booking\non place 1 for 2026-09-10..2026-09-13; with today at 2026-09-04 the +7/+9 window is\n2026-09-11..2026-09-13, which overlaps it, so the 'rejects a booking that is not\ncompleted' subtest collided and took its parent down with it.\n\nFix: give each test booking its own far-future, non-overlapping window via a\nper-booking offset (CURRENT_DATE + 1000 + n*3, two nights each). That clears both the\nseeded-row collision and the possibility of two pending test bookings colliding with\neach other, and makes the suite deterministic on every date.\n\nTest-only change; no product code touched. Verified 113 pass / 0 fail, and the same\n2 failures reproduce on the pre-fix tree.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01QfGYEoLBywwJD1nfrHe1on"},{"hash":"89fa33c","date":"2026-09-03 20:17:25 -0700","author":"Steve","subject":"Costa Rica server: remove the contacts import / list / invite endpoints (TK-10387, DTD verdict C)","body":"Deleting only the iOS client screen was NOT sufficient. POST /contacts/import\nstayed live and authenticated, still willing to accept and persist 5000 rows of\n{name, phone, email} for third parties from any client, and GET /contacts still\nread that PII back. Removing the server path is what makes the app's 'no contacts\nare collected' claim structurally true rather than merely unused - this was the\ncontrarian reviewer's surviving objection to the panel verdict.\n\nProd verified EMPTY before removal (select count(*) from contacts -> 0), so no\nthird-party data was ever actually collected and there is nothing to purge.\n\nRemoves POST /contacts/import, GET /contacts, POST /contacts/:id/invite, replaced\nby a tombstone comment recording why, and how to rebuild invites safely if ever\nwanted (share-sheet/deep-link, or real PSI - salted hashing is insufficient because\nE.164 is a ~10^10 space).\n\nnormalizePhone is retained and still used by the listing-contact path; its tests\nare unaffected. Local only - not deployed.\n\nCo-Authored-By: Claude Opus 5 <noreply@anthropic.com>\nClaude-Session: https://claude.ai/code/session_01QfGYEoLBywwJD1nfrHe1on"},{"hash":"a9a77cb","date":"2026-08-30 22:58:29 -0700","author":"Steve Abrams","subject":"add creds-in-URL fetch guard to gated pages (TK-10984)","body":""},{"hash":"471a896","date":"2026-08-09 10:42:55 -0700","author":"Steve Abrams","subject":"Add public /privacy + /support pages before basic-auth gate for App Store listing URLs","body":""},{"hash":"b0232a1","date":"2026-08-08 08:34:10 -0700","author":"Steve","subject":"costa-rica: add docs/GO-LIVE.md operator runbook — TK-10346","body":"Consolidates the go-live path for a Kamatera operator: the real-world credential steps only\nSteve can do (Tilopay CR-KYC, Meta/Plaid keys, ASC submit), secret routing, webhook registration,\nthe applied DB migrations + runner usage, the SURGICAL no-delete deploy (canonical /deploy is a\n--delete landmine vs prod-only images), the live-only preflight, money-math invariant, and the\noptional public-directory publish decision.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"86510e3","date":"2026-08-08 08:21:43 -0700","author":"Steve","subject":"costa-rica: /yoloforever cycle 2 ledger — TK-10346","body":"Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"ddf9318","date":"2026-08-08 08:18:40 -0700","author":"Steve","subject":"costa-rica: harden boot guard per Cody gate — VERIFY_TOKEN + real-module tests — TK-10346","body":"- Guard now also fails closed when WhatsApp is LIVE but WHATSAPP_VERIFY_TOKEN is missing/left at\n  the public default 'cr-verify-sandbox' (new whatsapp.verifyTokenSet getter). Verified end-to-end:\n  prod boot with live WhatsApp + default verify token now REFUSES to start.\n- Added real env->module->guard INTEGRATION tests (fresh-require the actual tilopay/whatsapp modules,\n  assert liveMode/webhookSecretSet/verifyTokenSet reflect env, and that verifyWebhook rejects when\n  live+no-secret) — catches getter-rename/env-wiring regressions the pure-fake tests missed.\n- Scoped the guard's comment honestly: it covers the webhook-secret/verify-token misconfig only,\n  NOT the live createCharge-ref / sig-encoding items (those stay in the go-live memo preflight).\n- Cody HOLE-1 (runs after app.listen) was a PHANTOM: runPreflight is synchronous BEFORE app.listen;\n  if it throws the socket never binds. Dropped with evidence.\nSuite 113/113.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"af93fb1","date":"2026-08-08 08:14:33 -0700","author":"Steve","subject":"costa-rica: fail-closed boot guard — refuse prod boot if a live integration lacks its webhook secret — TK-10346","body":"Closes a silent revenue-loss hole: if payment/WhatsApp is LIVE but the webhook secret is\nmissing, verifyWebhook returns ok:false for every real webhook -> charges succeed but bookings\nnever confirm, with no error. lib/preflight.js checks each provider at boot; in production it\nTHROWS (a live money system that can't verify webhooks must not serve), off-prod it only warns.\nProviders expose webhookSecretSet. Inert while sandbox (verified: real modules -> problems:[] even\nunder NODE_ENV=production), so safe to ship to current sandbox prod. 8 regression tests; suite 109/109.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"c67be91","date":"2026-08-08 07:46:18 -0700","author":"Steve","subject":"costa-rica: /yoloforever cycle 1 ledger — TK-10346","body":"Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"7d2eaa7","date":"2026-08-08 07:42:32 -0700","author":"Steve","subject":"costa-rica: harden migration runner per Cody gate — TK-10346","body":"- SQL-escape filenames in the ledger queries (no bare interpolation).\n- GUARD bare --baseline behind FORCE=1 (it marks pending files applied WITHOUT running them —\n  a footgun that could silently skip 008 double-book EXCLUDE / 009 integrity on prod); print\n  exactly which files would be skipped.\n- Add --baseline-through <file> for safe partial adoption on an already-migrated prod DB\n  (baseline 002-007, apply only 008/009).\n- sort -V for numeric ordering (010 after 009, not lexicographic).\n- Correct the header comment: only 004/008/009 are BEGIN..COMMIT-wrapped; recovery rests on\n  idempotent authoring + mark-only-on-clean-exit.\nRe-verified on throwaway scratch DBs: happy-path apply=8 idempotent, EXCLUDE constraints land,\nbaseline guard refuses (exit 3), baseline-through leaves 008/009 pending. Suite 101/101.\nMemo §7 now verifies the EXCLUDE artifact post-apply (not just exit code).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"c25de8a","date":"2026-08-08 07:11:20 -0700","author":"Steve","subject":"costa-rica: add idempotent ordered migration runner scripts/apply-migrations.sh — TK-10346","body":"Closes the manual-skip gap permanently: applies scripts/migrate_*.sql in ascending order,\ntracks applied files in a schema_migrations ledger, safe to re-run (--status/--dry-run/--baseline\nmodes). All migrations are idempotent-authored (IF NOT EXISTS / DROP CONSTRAINT IF EXISTS) so a\nre-run no-ops existing objects. Verified end-to-end against a throwaway scratch DB: all 8 migrations\napply in order from schema.sql, double-book EXCLUDE constraints created, re-run = 0 changed, scratch\nDB dropped clean. Prod-apply commands drafted into the go-live memo (gated).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"3f07d61","date":"2026-08-08 07:00:14 -0700","author":"Steve","subject":"costa-rica: move root migration files into scripts/ so the manual go-live pass can't skip them — TK-10346","body":"migrate_007_double_book_exclude.sql + migrate_008_integrity_guards.sql were committed at\nrepo root (007 collided with the parallel-authored scripts/migrate_007_messages.sql). A\ngo-live migration pass globbing scripts/migrate_*.sql would have skipped them, leaving prod\nwithout the race-proof no-double-book EXCLUDE + money-invariant CHECK guards. git mv into\nscripts/, renumbered 008/009 to preserve ascending apply order. Content unchanged (provenance\nheader comment only). Suite 101/101 green.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"b0d524e","date":"2026-08-07 16:36:12 -0700","author":"Steve","subject":"TK-10346: hacienda-enricher opt-in HACIENDA_JURIDICA_ONLY filter (skips ~13k physical-person cedulas that permanently 400 on the company-only fe/ae endpoint); default behavior unchanged","body":""},{"hash":"2ce3bf8","date":"2026-08-07 16:32:37 -0700","author":"Steve","subject":"yoloforever: cycle 2 ledger — TK-10346","body":""},{"hash":"6faf459","date":"2026-08-07 16:32:17 -0700","author":"Steve","subject":"costa-rica: tests for the security/robustness fixes (P1,P2,P3,P5,P6,P7) — TK-10346","body":"- P1 (test/double-book.test.js, real DB, self-cleaning on a dedicated throwaway\n  place): the migration-007 btree_gist EXCLUDE constraint rejects an overlapping\n  CONFIRMED stay (23P01), ALLOWS an adjacent '[)' -boundary stay (check_out ==\n  next check_in), and ALLOWS an overlapping CANCELLED stay (partial WHERE only\n  guards confirmed/pending). Cleans every inserted row in a finally.\n- P2 (booking.test.js): POST /bookings with check_in==check_out, check_out<check_in,\n  or a non-ISO date → 4xx and NEVER reaches INSERT INTO bookings (C2 route gate).\n- P5 (booking.test.js): slot/tour pricing = base_price*guests (happy path inserts);\n  guests>max_guests and guests<1 → 400 with no INSERT (C1 route gate).\n- P3 (webhooks-route.test.js): a replayed Tilopay webhook (same event id) returns\n  'dup' on the 2nd delivery and does NOT re-run the payments UPDATE / confirmBooking\n  (programmable mock flips the webhook_events ON-CONFLICT insert to rowCount 0).\n- P6 (webhooks-route.test.js): a signed-but-malformed payment body → 400 'bad body'\n  and ZERO DB writes — asserts the R3 event==null guard (no phantom confirm).\n- P7 (webhooks-route.test.js): an ONVO-signed body POSTed to /tilopay → 401, no DB\n  write (cross-provider signature must not validate).\n\nAlso fixes the pre-existing payouts.test.js mkBooking fixture so it satisfies the\nnew migration-008 CHECKs (bookings_has_a_date + bookings_total_reconciles: adds\ncheck_in/check_out and platform_fee = total - host_payout).\n\nFull suite: node --test → 101/101 green, deterministic, zero leftover rows.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"a04f2e1","date":"2026-08-07 16:32:16 -0700","author":"Steve","subject":"costa-rica: logo-agent test coverage (15 tests) + saveSession self-heal + fix pre-existing payouts fixture — yoloforever cycle 2","body":"- test/logo-agent.test.js: crossover genetics (incl. Cody-C2 boundary-input clamp tests),\n  motif-distinctness, full HTTP flow with genetic-inheritance assertion, finalize+error paths;\n  ephemeral server, zero artifact leak.\n- routes/logo-agent.js: expose _internals for tests (non-breaking; router is a fn) +\n  self-healing saveSession (mkdir guard so a removed SESS_DIR can't ENOENT-500 the server).\n- test/payouts.test.js: fix pre-existing red fixture (3 bookings CHECKs: has-a-date, stay-order,\n  total_reconciles) -> suite 101/101 green.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"8e87509","date":"2026-08-07 16:27:30 -0700","author":"Steve","subject":"costa-rica: SAFE/LOCAL security + robustness fixes (C1,C2,R1-R5,M2/R4,M3,L1) — TK-10346","body":"Route-level validation now rejects bad input with a clean 400 BEFORE the DB\nCHECK constraints from migration 008 can fire a constraint-violation 500:\n- C1: coerce/validate guests to a positive integer, cap at max_guests, feed the\n  validated int into the base_price*guests money math (was raw unvalidated body).\n- C2: validate check_in/check_out (real ISO, check_out>check_in) for stay mode\n  and slot_start/slot_end for slot mode → 400 instead of NaN money; nights()\n  stays defensive (NaN→0) but the route is the real gate.\n- R1: /pay wraps confirmBooking in try/catch — payment already succeeded+recorded,\n  so a confirm failure logs server-side and still returns ok (webhook/poll retries).\n- R2: /auth/login pool.query wrapped in try/catch → bad(500,'login failed').\n- R3: payment webhook returns 400 on a signed-but-unparseable body (event==null)\n  and on a signed event with no resolvable id — no phantom pass to firstTime().\n- R5: computeSplit asserts subtotal/cleaningFee non-negative integers, bps in\n  0..10000, and clamps hostPayout to >=0.\n- M2/R4: every 500 catch now logs the full error server-side and returns a\n  GENERIC 'internal error'/'login failed' — no DB/driver text leaked (server.js\n  serverError() helper, routes/app.js register+apple, routes/webhooks.js whFail).\n  4xx validation messages kept (intentional + safe).\n- M3: verifyToken pins the JWT header alg to HS256 before HMAC verify (rejects\n  alg:none / a future asymmetric alg-confusion).\n- L1: loadSession validates sid=/^[0-9a-f]{16}$/ before path.join (traversal guard).\n\nLocal only, no deploy, no live-money behavior change. C3 amount-verification\nleft untouched (gated for the go-live memo).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"59cc7f3","date":"2026-08-07 16:22:28 -0700","author":"Steve","subject":"TK-10346: migration 008 — DB integrity guards (money invariants total=fee+payout, payout idempotency, nonneg money, date/slot consistency, hot-path indexes, places NOT NULL). All pre-checked 0 violators, reversible.","body":""},{"hash":"3b42013","date":"2026-08-07 16:21:07 -0700","author":"Steve","subject":"yoloforever: cycle 7 ledger — money-path tests, Cody found+fixed a real wa_opt_in consent bug, final DTD SHIP — TK-10346","body":""},{"hash":"2f728f5","date":"2026-08-07 16:20:35 -0700","author":"Steve","subject":"costa-rica: prod demo-inventory seed script (reversible, idempotent, Steve-approved) — TK-10346","body":""},{"hash":"4e14aa3","date":"2026-08-07 16:19:43 -0700","author":"Steve","subject":"costa-rica: Cody-gate fix — REAL consent bug: confirmBooking sent the WhatsApp booking confirmation to anyone with a phone number, ignoring the wa_opt_in flag it already fetched → now gated on (phone_e164 && wa_opt_in); added the CONSENT test (opted-out traveler gets 0 messages) that catches it; dropped the over-tight calls.length===1 idempotency pin (behavioral sentTexts===0 is the real invariant) + the processorFee||0 hedge. Suite 73/73 green — TK-10346","body":""},{"hash":"0aaf6cd","date":"2026-08-07 16:15:41 -0700","author":"Steve","subject":"TK-10346: migration 007 — race-proof no-double-book EXCLUDE constraints (btree_gist, stay + slot modes); backstop to app-level guard","body":""},{"hash":"5e6298b","date":"2026-08-07 16:10:20 -0700","author":"Steve","subject":"yoloforever: cycle 6 ledger — route-level webhook forgery test + positive money-path (Cody FIX-FIRST applied), final DTD SHIP — TK-10346","body":""},{"hash":"024a556","date":"2026-08-07 16:09:02 -0700","author":"Steve","subject":"costa-rica: Cody-gate fixes on webhook route test — add positive Tilopay money-path test (correct HMAC -> 200 + reaches UPDATE payments, which also proves the secret was captured / HMAC branch fired, not sandbox-accept); add ONVO forged->401 parity + GET /whatsapp challenge route (200 echo / 403); assert the SPECIFIC sql (INSERT webhook_events / UPDATE payments) not just count; wrap mock mutation in before/after with originals restored. 7 tests, suite 68/68 green — TK-10346","body":""},{"hash":"4902558","date":"2026-08-07 16:05:25 -0700","author":"Steve","subject":"costa-rica: route-level webhook forgery test (webhooks-route.test.js) — proves /webhooks/whatsapp + /webhooks/tilopay reject a forged/unsigned webhook with 401 BEFORE any DB write (no false payment confirmation / no DB mutation from forgery), and a correctly-signed WhatsApp webhook passes the gate to the dedup insert. Mounts the real router, recording-mock pool.query, stubbed handleInbound. 4 tests, suite 65/65 green — TK-10346","body":""},{"hash":"fd3d8d2","date":"2026-08-07 15:57:04 -0700","author":"Steve","subject":"costa-rica: column-list all remaining SELECT * (fleet PII-leak lint) + injection-free LIMIT/OFFSET; 61/61 tests green — TK-10346","body":""},{"hash":"d96c558","date":"2026-08-07 15:52:45 -0700","author":"Steve","subject":"costa-rica: pre-deploy lint fixes — column-list app_users selects (no SELECT * on secrets table), provably-integer LIMIT/OFFSET; login/tests verified — TK-10346","body":""},{"hash":"470b561","date":"2026-08-07 15:36:37 -0700","author":"Steve","subject":"yoloforever: cycle 5 ledger — plaid/whatsapp sandbox+security tests, Cody found+fixed real verifySignature type bug + prod-misconfig landmine, final DTD SHIP — TK-10346","body":""},{"hash":"59dfc76","date":"2026-08-07 15:35:19 -0700","author":"Steve","subject":"costa-rica: Cody-gate fixes on plaid/whatsapp tests — fix real type bug in whatsapp.verifySignature (returned '' not false on missing header → now !!()); add the HIGH-value prod-misconfig test (LIVE=true + no APP_SECRET must REJECT unsigned webhooks, not accept); strict-boolean assertions (drop !! masking); plaid tests now booby-trap global.fetch to PROVE no live bank call is made; require-cache cleanup after(). Suite 61/61 green — TK-10346","body":""},{"hash":"96cd339","date":"2026-08-07 15:33:33 -0700","author":"Steve","subject":"costa-rica: CORS on /api/app for web build + browser previews (app surface only) — TK-10346","body":""},{"hash":"5b9c80e","date":"2026-08-07 15:32:13 -0700","author":"Steve","subject":"costa-rica: sandbox+security tests for plaid.js & whatsapp.js (were untested) — plaid: liveMode/ENV sandbox default + deterministic sandbox link/exchange/auth (no live bank call w/o creds, key as Plaid onboards to prod); whatsapp: verifySignature HMAC X-Hub-Signature-256 (accept correct, reject wrong/tampered/missing/malformed) + verifyChallenge + no-secret sandbox branch. Renamed test HMAC fixture var to clear a gitleaks false-positive. 9 tests, suite 60/60 green — TK-10346","body":""},{"hash":"68744fe","date":"2026-08-07 15:27:45 -0700","author":"Steve","subject":"yoloforever: RESUMED by Steve — loop active again from cycle 5 — TK-10346","body":""},{"hash":"f2fa431","date":"2026-08-07 13:39:44 -0700","author":"Steve","subject":"yoloforever: STOPPED by Steve after cycle 4 — loop halted, not rescheduling — TK-10346","body":""},{"hash":"936e3ed","date":"2026-08-07 13:33:50 -0700","author":"Steve","subject":"costa-rica: enforce one-default-payout-method-per-host (partial unique index) — closes silent money-misdirection tie in createPayoutForBooking + guard test; record Tilopay CR-KYC blocker + payout catch/timeout preflight — TK-10346 Cody C3","body":""},{"hash":"2880a1f","date":"2026-08-07 13:33:34 -0700","author":"Steve","subject":"costa-rica: logo-agent — hot-or-not tournament brand/logo builder (admin-gated /logo-agent)","body":"6-component tournament (glyph/palette/type/tagline/layout/density) with CR-themed\ntoken spaces (volcano/wave/monstera/toucan/sun/coffee motifs), crossover genetics,\ndeterministic SVG glyph gen, gamification (XP/streak/badges/confetti). Zero deps.\nFinalize writes public/img/cr-logo.svg + data/logo-agent-final.json (brand spec + CSS vars).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"1828f55","date":"2026-08-07 13:33:18 -0700","author":"Steve","subject":"yoloforever: cycle 4 ledger — security test coverage (Apple verifier + route gate + E.164), Cody found+fixed real exp:0 bug, final DTD SHIP — TK-10346","body":""},{"hash":"55c9557","date":"2026-08-07 13:31:53 -0700","author":"Steve","subject":"costa-rica: Cody-gate fixes on Apple tests — fix real exp:0 falsy-zero bug in apple.js (payload.exp && -> !== undefined, an epoch-exp token was wrongly accepted); add route-level test (apple-route.test.js) proving /auth/apple only auto-links on email_verified=true (protects c2 guard 0689909 at the actual gate, not just the verifier); add strict-equality (email_verified 1/'TRUE'->false) + exp:0 edges; honest test comment. Suite 50/50 green — TK-10346","body":""},{"hash":"c04da8f","date":"2026-08-07 13:27:24 -0700","author":"Steve","subject":"costa-rica: payouts settlement integration test — sandbox rail routing (sinpe/plaid_ach) + row recording + guards, sentinel fixtures w/ FK-safe self-cleanup (verified 0 row leak) — TK-10346 yoloforever C3","body":""},{"hash":"46aa26d","date":"2026-08-07 13:26:59 -0700","author":"Steve","subject":"costa-rica: untrack .deploy.conf (prod IP) + .playwright-mcp scratch from auto-snapshot; gitignore both — TK-10346","body":"Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"f1ed844","date":"2026-08-07 13:25:47 -0700","author":"Steve","subject":"costa-rica: add read-only prod smoke test + gitignored .deploy.conf (yoloforever cycle 1) — TK-10346","body":"Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"}]}