Repository navigation
feat: close the remaining audit gaps — escrow arbitration, durable webhooks, installer pinning - #282
Merged
Merged
Conversation
ESC-NEW-01: dispute_resolution and dispute_status existed in the production schema with no writer anywhere. disputeEscrow set status='disputed' and a reason, and stopped — so a dispute recorded a grievance and changed nothing. The sharper half was the exits. A disputed escrow could only be *released*, and only by the depositor; refund required 'funded', so raising a dispute REMOVED the refund path. Whoever raised one made their own position strictly worse and the beneficiary had no path at all, so a disputed escrow sat until someone gave up. 'disputed' is now refundable — a beneficiary can concede — and resolveDispute() adds the arbiter exit, admin-gated because the two parties disagree by definition and neither can be the one who decides. REC-D-07: deliverWebhook retried three times in-process, spending its whole budget inside one request over roughly three seconds. An endpoint down for four — a deploy, a restart, a brief network fault — lost the event permanently, as did anything in flight when our own process was recycled. Merchants reconcile against these, so a lost payment.confirmed is a payment the merchant never hears about. Adds webhook_deliveries (migration 20260819210000, applied and verified): a durable queue with 1m→12h backoff and a dead-letter state after 8 attempts, so an abandoned event becomes a row an operator can find rather than something that evaporated. Rows are claimed before delivery, conditioned on the attempt count read, so two overlapping cron runs cannot both send the same webhook. Payloads are re-signed on every attempt rather than replaying a stored signature: a signature is bound to its timestamp, so a stored one is either rejected by a merchant enforcing freshness or — worse, if they are not — accepted indefinitely. The secret is re-read too, so a rotation mid-queue is honoured and a removed webhook stops being retried. W-01: unattended auto-upgrade from a mutable ref is now opt-in. Unpinned, the installer follows master and polls every five minutes, so any merge executed on every operator host within five minutes with no human in between. Pinned installs keep auto-upgrade, since a tag can only re-install the same code. Verified the gate's truth table across all five pinned/opt-in/disabled combinations. Suite green at 334 files / 4690 tests (up from 327/4600), tsc --noEmit clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ThreatCrush Security Scan319 finding(s) HIGH/CRITICAL: 32 | MEDIUM: 37 | LOW: 250
…and 269 more. Full results in the Security tab. Snippets are redacted; ThreatCrush never prints matched credential material. |
ralyodio
added a commit
that referenced
this pull request
Aug 19, 2026
…bhooks, installer pinning (#282) Closes the three product gaps left open after #279. ESC-NEW-01: dispute_resolution/dispute_status had no writer, and refund required 'funded' — so raising a dispute removed the refund path entirely. 'disputed' is now refundable and resolveDispute() adds an admin-gated arbiter exit. REC-D-07: webhook retry spent its whole budget in ~3 seconds inside one request. Adds a durable queue with 1m-12h backoff and a dead-letter after 8 attempts; rows are claimed before delivery and payloads re-signed per attempt. W-01: unattended auto-upgrade from a mutable ref is now opt-in. Previously any merge to master executed on every operator host within five minutes. Also deactivated 5 bogus reputation issuers in production (reversible), after verifying only ugig.net and d0rz.com have ever produced a receipt. 334 files / 4690 tests green, all 8 CI checks pass.
ralyodio
added a commit
that referenced
this pull request
Aug 19, 2026
…bhooks, installer pinning (#282) Closes the three product gaps left open after #279. ESC-NEW-01: dispute_resolution/dispute_status had no writer, and refund required 'funded' — so raising a dispute removed the refund path entirely. 'disputed' is now refundable and resolveDispute() adds an admin-gated arbiter exit. REC-D-07: webhook retry spent its whole budget in ~3 seconds inside one request. Adds a durable queue with 1m-12h backoff and a dead-letter after 8 attempts; rows are claimed before delivery and payloads re-signed per attempt. W-01: unattended auto-upgrade from a mutable ref is now opt-in. Previously any merge to master executed on every operator host within five minutes. Also deactivated 5 bogus reputation issuers in production (reversible), after verifying only ugig.net and d0rz.com have ever produced a receipt. 334 files / 4690 tests green, all 8 CI checks pass.
ralyodio
added a commit
that referenced
this pull request
Aug 19, 2026
…bhooks, installer pinning (#282) Closes the three product gaps left open after #279. ESC-NEW-01: dispute_resolution/dispute_status had no writer, and refund required 'funded' — so raising a dispute removed the refund path entirely. 'disputed' is now refundable and resolveDispute() adds an admin-gated arbiter exit. REC-D-07: webhook retry spent its whole budget in ~3 seconds inside one request. Adds a durable queue with 1m-12h backoff and a dead-letter after 8 attempts; rows are claimed before delivery and payloads re-signed per attempt. W-01: unattended auto-upgrade from a mutable ref is now opt-in. Previously any merge to master executed on every operator host within five minutes. Also deactivated 5 bogus reputation issuers in production (reversible), after verifying only ugig.net and d0rz.com have ever produced a receipt. 334 files / 4690 tests green, all 8 CI checks pass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The three product gaps left open after #279, plus a production issuer cleanup.
ESC-NEW-01— disputed escrows had no exitdispute_resolutionanddispute_statusexisted in the production schema with no writer anywhere.disputeEscrowsetstatus='disputed'and a reason, and stopped.The sharper half was the exits. A disputed escrow could only be released, and only by the depositor — the party who had just been disputed against, or who had just disputed. Refund required
funded, so raising a dispute removed the refund path. Whoever raised one made their own position strictly worse, the beneficiary had no path at all, and the escrow sat until somebody gave up.disputedis now refundable, so a beneficiary can concederesolveDispute()+ admin-gatedPOST /api/admin/escrows/[id]/resolveis the arbiter exitAdmin-gated deliberately: the two parties disagree by definition, so neither can be the one who decides. A resolution note of ≥10 chars is required — it is the record of why someone else's money moved.
REC-D-07— three seconds of retry, then the event is gonedeliverWebhookretried 3× in-process with backoff, spending the entire budget inside one request over ~3 seconds. An endpoint down for four — a deploy, a restart, a brief network fault — lost the event permanently, as did anything in flight when our own process was recycled. Merchants reconcile against these, so a lostpayment.confirmedis a payment the merchant never hears about.New
webhook_deliveriesqueue (migration20260819210000, applied and verified):payment.confirmedis a real problem for a merchantPayloads are re-signed per attempt rather than replaying a stored signature: a signature is bound to its timestamp, so a stored one is either rejected by a merchant enforcing freshness or — worse, if they are not — accepted indefinitely. The secret is re-read too, so a rotation mid-queue is honoured and a removed webhook stops being retried.
W-01— a merge reached every host in five minutesUnpinned, the installer follows
masterand the timer polls every 300s, so anything merged executed on every operator host within five minutes with no human between the merge and the execution.Unattended auto-upgrade from a mutable ref is now opt-in (
COINPAY_AUTO_UPGRADE_UNPINNED=1). Pinned installs keep it, since a tag can only ever re-install the same code it already has. Verified the gate's truth table across all five pinned/opt-in/disabled combinations.Production issuer cleanup (done, reversible)
Deactivated 5 of 18 reputation issuers. An issuer key provisions merchant accounts and issues invoices on other people's behalf:
evilpoc,poc2,OutHunt— all on.example, which is RFC 2606 reserved and cannot resolveX/ domainX— not a domainTounes/Coinpayportal.com— a third party registered an issuer claiming the platform's own domain, verified by nothingChecked first that only
ugig.net(14,333 receipts) andd0rz.com(5) have ever produced anything, so none of the deactivated rows could break live traffic.active = truerestores any of them.Still open and deliberately untouched: 6
*.trycloudflare.comissuers (ephemeral tunnel hostnames are a weak identity by construction) and the cleartextapi_keycolumn on 17 rows — rotating those needs the integrators told.Tests
334 files / 4690 tests, up from 327/4600.
tsc --noEmitclean.🤖 Generated with Claude Code