[object Object]

← back to Costa Rica

cycle 6 docs: YOLO_NOTES ledger + GO-LIVE runbook — #4/#5/#6/#7 hardening landed, remaining live-only items

9b74e96e287d28f5a12d672a0c11e9f7ba8638bd · 2026-09-23 18:28:43 -0700 · Steve

Records the PRE-FLIGHT #7 body-read timeout + Cody-found fail-OPEN fix, and updates
the go-live runbook to distinguish the provider-agnostic hardening now IN CODE
(#4/#5 fetch timeout, #7 body timeout + fail-closed refund/payout, #6 local charge
idempotency) from the remaining LIVE-ONLY preflight (sig encoding, createCharge
non-timeout error-status body, provider-honored idempotency key, $1 verify).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic

Files touched

Diff

commit 9b74e96e287d28f5a12d672a0c11e9f7ba8638bd
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Sep 23 18:28:43 2026 -0700

    cycle 6 docs: YOLO_NOTES ledger + GO-LIVE runbook — #4/#5/#6/#7 hardening landed, remaining live-only items
    
    Records the PRE-FLIGHT #7 body-read timeout + Cody-found fail-OPEN fix, and updates
    the go-live runbook to distinguish the provider-agnostic hardening now IN CODE
    (#4/#5 fetch timeout, #7 body timeout + fail-closed refund/payout, #6 local charge
    idempotency) from the remaining LIVE-ONLY preflight (sig encoding, createCharge
    non-timeout error-status body, provider-honored idempotency key, $1 verify).
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic
---
 YOLO_NOTES.md   | 24 ++++++++++++++++++++++++
 docs/GO-LIVE.md | 13 ++++++++++---
 2 files changed, 34 insertions(+), 3 deletions(-)

diff --git a/YOLO_NOTES.md b/YOLO_NOTES.md
index e14def5..4327bac 100644
--- a/YOLO_NOTES.md
+++ b/YOLO_NOTES.md
@@ -164,3 +164,27 @@ Canonical apply order now: `004_marketplace → 005_apple → 006_contacts → 0
 **Cost:** $0 (local PG/tests + one Cody pass).
 
 **Backlog:** PRE-FLIGHT #6 (provider-honored idempotency key) + #7 (body-read timeout) stay LIVE-ONLY. Safe local work largely exhausted. Next non-Steve-gated items: minor (docs/GO-LIVE.md runbook refresh); everything else customer-facing/spend/live-credential gated.
+
+## /yoloforever CYCLE 6 (2026-09-23, yf-costa) — PRE-FLIGHT #7 + Cody-found fail-OPEN fix
+
+**Decision:** Close GO-LIVE PRE-FLIGHT #7 (bound the response BODY read, not just connect+headers). Provider-AGNOSTIC + reversible.
+
+**Landed (local/reversible, sandbox-inert):**
+- `lib/payments/http.js` — `fetchT()` deadline now spans the WHOLE request lifecycle (connect + headers AND the body read). One AbortController stays armed across `res.json()`/`res.text()`; the returned object is a Proxy over the real Response whose body-consuming methods run under the armed signal and relabel an abort as `PROVIDER_TIMEOUT`. `timer.unref()` so a metadata-only caller (token() throws on !res.ok, never reads the body) can't leave an active timer.
+
+**Cody gate — CRITICAL fail-OPEN found + FIXED (verified against the code + empirically repro'd by Cody):**
+- Once the body read is bounded, `res.json()` throws `PROVIDER_TIMEOUT` on a stall. But `tilopay.refund()`/`payout()` + `onvo.refund()` used `res.json().catch(() => ({}))`, which **swallowed** that timeout. Since `res.ok` was already `true` (fast headers), they returned a FABRICATED success:
+  - `payout()` → `{providerRef: undefined, status: 'processing', raw: {}}` → `payouts.js` writes a stuck `'processing'` row with a **NULL provider_ref that can never reconcile**, and its catch/mark-failed never fires (nothing threw). Silent, on exactly the header-fast/body-stalled failure #7 exists to catch.
+  - `refund()` → `{status: 'refunded', raw: {}}` on a refund that never confirmed.
+- **Fix:** fail closed on a `PROVIDER_TIMEOUT` specifically — `payout()` THROWS (so payouts.js marks the row `'failed'` + surfaces); `refund()` returns `status:'failed'`. A merely empty/malformed but **fully-received** 200 body is still tolerated (`raw:{}`), since refund success is HTTP-status-driven — the fix does NOT over-correct legitimate empty responses.
+- Cody also: credited the Proxy `Reflect.get(target,prop,target)` as correct (a `receiver` refactor would break real-Response private getters); flagged the dangling-timer on token()'s error path (fixed via `unref`); noted `res.clone()` returns a raw un-proxied Response (latent, no caller — documented, not wired).
+
+**Tests (+4, suite 122 → 126, all green):**
+- `payments-timeout.test.js`: body-stall → `json()` rejects `PROVIDER_TIMEOUT`; fast body clears the deadline; metadata+body read through the Proxy off a **REAL undici Response** (guards `Reflect.get(target,prop,target)` — plain-object mocks would miss a `receiver` regression).
+- `payments-body-timeout-failclosed.test.js` (NEW): tilopay `payout()` throws + `refund()` → `'failed'` on a body-read timeout; an empty/malformed fully-received body stays tolerated (not over-corrected). Forces LIVE mode via env + require-cache reset; faked `global.fetch`, zero network/money.
+
+**FINAL DTD:** SHIP. Provider-agnostic hardening for #4/#5/#6/#7 now COMPLETE. Suite 126/126.
+
+**Cost:** $0 (local PG/tests + one Cody pass).
+
+**Backlog:** Provider-honored idempotency key (double-payout-on-retry guard) stays the ONLY remaining money-path pre-flight, and it's LIVE-ONLY (needs the Tilopay/ONVO account — CR-KYC blocked). `res.clone()` proxy hygiene = latent. Otherwise safe local work is exhausted; remaining is customer-facing/spend/live-credential gated.
diff --git a/docs/GO-LIVE.md b/docs/GO-LIVE.md
index abc5eee..f06ab91 100644
--- a/docs/GO-LIVE.md
+++ b/docs/GO-LIVE.md
@@ -50,10 +50,17 @@ ssh root@45.61.58.125 'pm2 reload costa-rica --update-env'
 curl -s https://costarica.agentabrams.com/api/app/health   # expect ok:true
 ```
 
-## 5. Live-only preflight (verify against the REAL provider before opening bookings — do NOT guess)
+## 5. Money-path hardening — LANDED (provider-agnostic, sandbox-inert, tested)
+These closed in the /yoloforever cycles; they need no creds and are already live in code:
+- **#4/#5 fetch timeout** — every live provider call is bounded by `fetchT` (`lib/payments/http.js`, env `PROVIDER_HTTP_TIMEOUT_MS`, default 15s) so a hung TCP can't strand a payment/payout in `processing` forever.
+- **#7 body-read timeout** — the deadline spans the **whole** request (connect + headers AND the `res.json()` body read), so a header-fast/body-stalled provider can't hang the money path. `refund()`/`payout()` **fail closed** on a body-read timeout (payout throws → `payouts.js` marks the row `failed`; refund → `failed`) instead of fabricating a success.
+- **#6 charge idempotency (local half)** — `POST /bookings/:code/pay` writes a `processing` payments row **before** `createCharge()` and reuses an in-flight payment on retry (no double-charge); a timeout leaves a reconcilable row.
+
+## 5b. Live-only preflight (verify against the REAL provider before opening bookings — do NOT guess)
 1. **Webhook signature encoding** — adapters compare hex; if the provider signs base64, set `Buffer.from(sig,'base64')` explicitly once the real delivery is observable.
-2. **Live createCharge error path** — guard `res.ok` + explicit `failed` mapping before extracting `providerRef` (a bad live response can write `providerRef=undefined,status=processing` → stuck booking).
-3. **Verify with a $1 real charge + refund** before opening bookings.
+2. **Live createCharge error path** — guard `res.ok` + explicit `failed` mapping before extracting `providerRef` (a bad live 4xx/5xx body can otherwise write `providerRef=undefined,status=processing` → stuck booking). `createCharge` reads the body unwrapped, so a body **timeout** already throws → 502; this item is specifically the **non-timeout** error-status body.
+3. **Provider-honored idempotency key (#6 live half)** — pass the provider's real idempotency header/field (ONVO `Idempotency-Key`-style; Tilopay per its live account) on `createCharge` **and** on the SINPE `payout` transfer, so a retry after a timeout can't double-charge a traveler or double-pay a host. The local pre-charge-row half is already in; this is the provider-side guarantee.
+4. **Verify with a $1 real charge + refund** before opening bookings.
 
 ## 6. Verify money math
 `total = platform_fee + host_payout` (10% platform fee; `fees` is display-only). Enforced by DB CHECK `bookings_total_reconciles` + `test/money.test.js`.

← a00cd46 costa-rica: bound provider body-read + fix fail-OPEN on refu  ·  back to Costa Rica  ·  costa-rica: createCharge fail-closed on live error status + b7da97c →