fix(cli): resume pending settlement safely#73
Conversation
GPT review — changes requestedReviewed head
The normal retry path otherwise preserves the exact signature/request, bounds retries and replacement signing, and reapplies |
- carry the do-not-repay guidance across network and 5xx failures that follow an observed pending settlement, so a lost retry can no longer read as a plain error inviting a fresh key - require the x-h402-previous-idempotency-key binding (presence and match) before signing a replacement challenge Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review follow-upAddressed both GPT findings in commit
Verification: |
…tlement-resume Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Final reviewVerdict: REJECT — blocking money-safety gap Reviewed head Blocker
I added a disposable regression with this sequence:
It failed as follows:
A signed request is settlement-risk-bearing as soon as it is sent, not only after the backend manages to return the explicit pending code. Please preserve the risk guidance across every non-definitive exit after any signed send; optionally retry the byte-identical signed request/key in-process on bounded network failures as well. Add regressions for the first signed request returning a network error and a generic 5xx. Verification performed
No paid, wallet-signing, production, or provider call was made; signing and HTTP were mocked. |
Review follow-upAddressed the final first-signed-request money-safety blocker in commit
Verification:
No real wallet signing, provider call, payment, or production request was made; HTTP and signing remained mocked. |
GPT ReviewerVerdict: REJECT Reviewed head An initial replacement-marked 402 bypasses the pending-settlement gate and can cause a fresh authorization to be signed under the original idempotency key. All shared deterministic checks passed. Findings
Shared exact-snapshot validation
|
Opus ReviewerVerdict: PASS Reviewed head This PR makes the CLI resume a I traced the full
stdout/stderr contract preserved (only final success to stdout; all failures thrown to the error envelope). The api.ts money-sensitive code-set expansion is mirrored in api.test.ts; docs edits match implementation and the new docs.test assertions. The refactor does not worsen the pre-existing (unchanged) absence of the auth header on the paid retry. Shared validation passes: typecheck, lint, build, and 172 CLI + 14 core tests. Items considered and dismissed as non-actionable: the deliberate double FindingsNone. Shared exact-snapshot validation
|
Review follow-upAddressed the initial replacement-challenge signing blocker in commit
Verification:
No real wallet signing, provider call, payment, or production request was made; HTTP and signing remained mocked. |
GPT ReviewerVerdict: PASS Reviewed head The pending-settlement state machine safely reuses the original signed request, bounds retries and replacement signing, validates replacement-key binding, reapplies the payment cap, rejects initial replacement challenges before signing, and preserves settlement-risk guidance across ambiguous failures. Documentation and tests match the implementation, and all shared validation passed. FindingsNone. Shared exact-snapshot validation
|
Opus ReviewerVerdict: PASS Reviewed head This PR makes the CLI resume a I independently traced the full I traced every new test (13 settlement-retry cases) and confirmed they match the implementation, including the initial-replacement rejection (0 signer calls, 1 fetch) and the repeated-replacement stop (signs 2, not 3). The api.ts money-sensitive code-set expansion (5 codes) is mirrored in api.test.ts; the docs.test.ts regexes match the reworded SKILL.md/cli README (line-wrap tolerant), and the removed 'reuse the same key after a lost response' phrasing is confirmed absent. stdout/stderr contract preserved (only success via printJson; all non-2xx routed through assertOk to the error envelope). Argument validation (positionals, route id, --max-usd, query-on-POST) precedes all side effects, and --max-usd is re-enforced before any replacement signing. Items considered and dismissed as non-actionable: the deliberate double assertUnderMaxUsd fail-fast-before-wallet check (documented by comment), the conservative over-warning naming the server-issued replacement key on a max-usd replacement refusal (explicitly tested/intentional and money-safe since no fresh key is fabricated), the unchanged absence of the auth header on the paid retry (pre-existing, not worsened), and the root README pending-settlement text being outside the diff (identical in head/base, now matched by the code). Shared validation passes: typecheck, lint, 173 CLI + 14 core tests, build. Zero actionable findings. FindingsNone. Shared exact-snapshot validation
|
|
Closing in favor of #80 and Steemhunt/h402-web#609. PR #73 could turn settlement uncertainty into a second purchase by automatically signing a replacement authorization with a new idempotency key. The replacement CLI change performs only bounded, same-authorization reconciliation and never signs a replacement payment. The backend companion removes replacement 402 responses, keeps indeterminate attempts pending with on-chain-only reconciliation, and returns This pivot intentionally does not persist or replay upstream response data. |
|
Superseded by #80 and Steemhunt/h402-web#609; follow-up review continues on those split PRs. |
Summary
payment_settlement_pendingresponses in-process with the byte-identicalPAYMENT-SIGNATURE, idempotency key, method, path, body, and provider path--max-usdbefore signing a replacement challenge and extend money-sensitive guidance to pending, reconciled, and authorization-conflict responsesSafety properties
x-h402-replacement-idempotency-keyand a valid replacement challengeVerification
npm run test -w @h402/cli -- tests/docs.test.ts tests/api.test.ts tests/call-settlement-retry.test.ts tests/call-max-usd.test.tsgit diff --checknpm run typechecknpm run lintnpm testnpm run buildCloses #43