Skip to content

fix(payments): settle VOD, seedbox and IPTV pass payments from the one CoinPay webhook - #296

Merged
ralyodio merged 1 commit into
masterfrom
one-coinpay-webhook
Oct 21, 2026
Merged

ralyodio merged 1 commit into
masterfrom
one-coinpay-webhook

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

CoinPay only delivers to the business's single webhook_url (/api/webhooks/coinpayportal), so the vod, share and iptv-share routes never received a delivery and paid passes were never granted.

  • Main webhook dispatches on metadata.type (vod / seedbox_share / iptv_share) to the existing grant handlers, after signature verification, ahead of translation/session/subscription handling. Unknown grant: 200 no-op. Handler error: 500 (redelivery).
  • Idempotency: VOD + seedbox markGrantPaid now conditional (pending/expired -> paid) and return whether they changed, like IPTV's; earnings count once across confirmed + forwarded + redeliveries.
  • Old routes: thin forwards (export { POST, GET } from '../route'). Checkouts now name the main webhook.
  • Tests: grant-routing.test.ts with really signed nested payloads per kind through the real verifier. Full vitest 3106 pass; next build ok.

🤖 Generated with Claude Code

… the one CoinPay webhook

CoinPay delivers every event for a business to its single webhook_url
(https://bittorrented.com/api/webhooks/coinpayportal). The VOD, seedbox
rental and IPTV resale pass checkouts sent a per-payment webhook_url
(/vod, /share, /iptv-share) that CoinPay never delivers to, so a paid pass
reached the main route, matched no payment_history row, and was never
granted.

The main route now dispatches on metadata.type (vod, seedbox_share,
iptv_share) to the existing grant handlers, after the same signature check
(HMAC over t.rawBody, 300 s tolerance), before the translation, session and
subscription paths. An unknown grant is a 200 no-op; a handler error is a
500 so CoinPay redelivers.

Idempotency: VOD and seedbox markGrantPaid are now conditional (pending or
expired -> paid, returning whether it changed), as IPTV's already was, so a
redelivery or a confirmed-then-forwarded pair credits provider/share
earnings once.

The three old routes are thin forwards (re-export of the main POST/GET),
and the checkouts now name the main webhook.

Tests: grant-routing.test.ts posts really signed, nested CoinPay payloads per
kind through the real verifier (paid once, forwarded/redelivery no-op,
expired, unknown payment, bad signature 401, old addresses forward,
subscription payments still reach the subscription handler).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

97 finding(s)

HIGH/CRITICAL: 11 | MEDIUM: 30 | LOW: 56

Severity Rule Location
HIGH secret-private-key src/app/settings/seedbox-section.tsx:412
HIGH secret-generic-api-key docs/incidents/2026-05-okshanaby-supply-chain.md:18
HIGH tls-verification-disabled src/app/api/iptv-proxy/route.ts:39
HIGH tls-verification-disabled src/app/api/iptv/channels/route.ts:35
HIGH tls-verification-disabled src/app/api/iptv/playlists/[id]/route.ts:73
HIGH tls-verification-disabled src/app/api/iptv/playlists/route.ts:64
HIGH js-cors-origin-reflected src/app/api/public/shares/[slug]/checkout/route.ts:40
HIGH js-cors-origin-reflected src/app/api/public/vod/[slug]/checkout/route.ts:34
HIGH tls-verification-disabled src/lib/iptv/shares/upstream.ts:37
HIGH tls-verification-disabled workers/iptv-cache/epg-fetcher.ts:25
HIGH tls-verification-disabled workers/iptv-cache/playlist-fetcher.ts:62
MEDIUM secret-jwt .github/workflows/ci.yml:120
MEDIUM secret-jwt .github/workflows/ci.yml:121
MEDIUM secret-jwt .github/workflows/ci.yml:123
MEDIUM secret-jwt .github/workflows/ci.yml:162
MEDIUM secret-jwt .github/workflows/ci.yml:164
MEDIUM secret-jwt docs/tunein (2).py:9
MEDIUM secret-jwt docs/tunein.py:9
MEDIUM sh-remote-script-execution scripts/setup-server.sh:182
MEDIUM sh-remote-script-execution scripts/setup-server.sh:419
MEDIUM sh-remote-script-execution scripts/setup-server.sh:428
MEDIUM sh-unquoted-expansion-destructive scripts/setup-server.sh:1096
MEDIUM sh-unquoted-expansion-destructive scripts/setup-server.sh:1106
MEDIUM js-unescaped-html-sink src/app/api/player/route.ts:127
MEDIUM js-unescaped-html-sink src/app/api/player/route.ts:281
MEDIUM js-unescaped-html-sink src/app/blog/[slug]/page.tsx:40
MEDIUM js-unescaped-html-sink src/app/blog/[slug]/page.tsx:66
MEDIUM js-unescaped-html-sink src/app/email/email-content.tsx:566
MEDIUM js-open-redirect src/app/login/page.tsx:68
MEDIUM js-open-redirect src/app/pricing/page.tsx:148
MEDIUM js-open-redirect src/app/rent/[slug]/rent-client.tsx:170
MEDIUM js-unescaped-html-sink src/app/rss/rss-content.tsx:615
MEDIUM js-open-redirect src/app/vod/[slug]/vod-client.tsx:138
MEDIUM js-open-redirect src/app/watch/[slug]/watch-client.tsx:131
MEDIUM js-unescaped-html-sink src/app/youtube/youtube-content.tsx:546
MEDIUM js-open-redirect src/components/account/iptv-subscription-section.tsx:121
MEDIUM js-open-redirect src/components/account/iptv-subscription-section.tsx:151
MEDIUM js-unescaped-html-sink src/components/layout/pit-footer.tsx:25
MEDIUM js-unescaped-html-sink src/components/news/news-section.tsx:358
MEDIUM js-unescaped-html-sink src/components/news/news-section.tsx:731
MEDIUM redos-nested-quantifier src/lib/metadata-enrichment/metadata-enrichment.ts:317
LOW tls-verification-disabled docs/tunein (2).py:34
LOW tls-verification-disabled docs/tunein (2).py:37
LOW tls-verification-disabled docs/tunein (2).py:47
LOW tls-verification-disabled docs/tunein.py:34
LOW tls-verification-disabled docs/tunein.py:37
LOW tls-verification-disabled docs/tunein.py:47
LOW tls-verification-disabled src/app/api/iptv-proxy/route.test.ts:457
LOW secret-generic-credential src/app/api/iptv/subscription/route.test.ts:66
LOW secret-generic-credential src/lib/argontv/client.test.ts:54

…and 47 more. Full results in the Security tab.

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 3354839 into master Oct 21, 2026
9 checks passed
@ralyodio
ralyodio deleted the one-coinpay-webhook branch October 21, 2026 13:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant