Skip to content

feat: credit codes (Premium days), invite bonus and affiliate links (0.6.0) - #301

Merged
ralyodio merged 2 commits into
masterfrom
feat/credit-codes-invites
Oct 26, 2026
Merged

ralyodio merged 2 commits into
masterfrom
feat/credit-codes-invites

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Credit code redemptions and invite/affiliate links for bittorrented.com (fleet rollout batch 1). Anthony 2026-10-26: "mint him a token i can give him to redeem credits ... add credit code redemptions and invite codes/(affiliate) to all properties".

What a code grants: @profullstack/redeem-credits 0.2.0, unit premium_days, codes look like BT-XXXX-XXXX-XXXX-XXXX. A redeemed code goes through grant_premium_days(), which writes user_subscriptions. That is the same row a paid Premium plan already uses (activate_subscription), so every existing Premium check sees it and there is no new flag. Days are added after the end of a running plan or trial, and Family stays Family. It is idempotent per (user, source), so a retried hook never adds days twice.

Invites: bittorrented already has scarce invites: 5 a month, required to sign up, enforced by the a_consume_invite_on_signup trigger. They stay the invite system. A second INV- code would not satisfy the signup trigger, and 3 more invites per account would double the supply. What is new:

  • Bonus: when an invited member confirms their email and signs in, both people get 7 days of Premium (grant_invite_bonus()). The inviter can collect this at most 5 times a month. Only invites accepted from 2026-10-26 count, so nothing is paid out retroactively. Admin-seeded invites have no inviter. Banned or unconfirmed accounts get nothing. The bonus is paid on /api/auth/login, and on /api/auth/me for magic-link and nixamp sign-ins (once per process per member).
  • /i/: a short invite link that redirects to /signup?invite=<code>. The code travels in the URL, not a cookie. Invite links now use it, and old /signup?invite= links still work. It is in all three open lists (site-offline, proxy PUBLIC_PATHS, legal-mode).
  • /invites: the member invites screen (same as /invite-only), now with the bonus line and the member's referral link.

Affiliate links: @profullstack/affiliates 0.2.0. Each member has one stable ?ref= code. First touch is recorded at sign-up through refFromRequest(), which reads the existing referral_code cookie that the proxy's trackReferralCode already sets, so there is no second cookie. Codes from the older @profullstack/stack/referrals table come back external and are left to that system, so nothing is double counted.

Surfaces:

  • "Have a code?" card on /account (Subscription tab) and /settings
  • POST /api/account/redeem
  • GET/POST/DELETE /api/admin/credit-codes (admins only, 404 otherwise)
  • pnpm credits mint --days N [--max N] [--label T] [--expires DATE] | list | revoke <id>

The repo has no MCP server.

Migrations: 20261026130000_redeem_credits.sql and 20261026130100_affiliates.sql are copied verbatim from the packages. 20261026130200_premium_day_grants.sql adds the table, the two functions (service_role only) and a pgrst reload. All three were applied by hand on dev2 before merge (backup ~/backups/bittorrented/credits-invites.bak-001.sql plus schema-public.bak-001.sql). They were first dry-run in a rolled-back transaction with a functional test: unconfirmed → refused, confirmed → both +7, repeat → already_granted, code grant idempotent, authenticated → permission denied.

Tests: new src/lib/rewards.test.ts, plus route tests for redeem, admin credit codes and /i/<code>. The legal-mode and site-offline lists, invites API and invite link tests are updated. Full suite: 3134 pass. tsc is clean and next build is OK.

Not done: there is no affiliate commission payout. bittorrented has no payout ledger, so aff.commission() is not wired into the CoinPay webhook yet.

🤖 Generated with Claude Code

ralyodio and others added 2 commits October 26, 2026 16:51
- @profullstack/redeem-credits 0.2.0: a code grants unit premium_days. Redeeming writes
  user_subscriptions through grant_premium_days(), the row a paid Premium plan already lives
  on, idempotent per (user, source); days go on the end of a running plan or trial, Family
  stays Family.
- Invites stay the existing system (5 a month, required to sign up). New: once an invited
  member confirms their email, both people get 7 Premium days at sign-in
  (grant_invite_bonus(), inviter capped at 5 a month, invites accepted from 2026-10-26).
- @profullstack/affiliates 0.2.0: a stable ?ref= link per member on /invites, first touch
  recorded at sign-up from the existing referral_code cookie (no second cookie; codes from
  @profullstack/stack/referrals come back external and are left to it).
- Pages: "Have a code?" on /account (Subscription) and /settings, /invites, /i/<code> short
  invite links (now what invite links use). API: POST /api/account/redeem,
  /api/admin/credit-codes (mint/list/revoke). CLI: pnpm credits mint|list|revoke.
- Migrations 20261026130000/130100/130200 (applied on dev2 before merge).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​profullstack/​affiliates@​0.2.07610010090100
Added@​profullstack/​redeem-credits@​0.2.07610010090100

View full report

@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 0237873 into master Oct 26, 2026
9 checks passed
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