Skip to content

Credit codes + invites; fix domain takeover and email-based account linking - #140

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

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

Conversation

@ralyodio

Copy link
Copy Markdown
Collaborator

What

Credit codes (@profullstack/redeem-credits 0.2.0, unit pro_days, prefix MOSH-). A code adds days of the paid affiliate plan ($1/mo: set your own commission rate instead of the 80% floor). Grants go through one path, grantPaidDays() (lib/plan.ts), into plan_grants (PK account_id + source = idempotent; code:<id>, invite:<id>:inviter|invitee). The plan is paid while affiliates.plan='paid' or granted days remain; days queue after each other and are computed on read, so concurrent grants never overwrite each other. A future $1/mo checkout should call grantPaidDays() with its payment id.

  • Redeem: /invites#redeem ("Have a code?", linked from the dashboard Affiliates tab), POST /api/credits {code} (session or API key), GET /api/credits.
  • Mint on dev2: docker exec moshcodingcom-app-1 bun scripts/credit-codes.ts mint --days 30 --label "for X" [--max N] [--expires-days N] (also list, revoke <id>, quota <email> <n>).

Invites (@profullstack/affiliates 0.3.0): 3 per account, /invites, /i/<code> (cookie mc_invite, 30 days). Accepted at email sign-up, CoinPay sign-in, or by pasting the code on /invites (another device). One accepted invite per account (already_invited; the refused invite stays unused). Only accounts <= 7 days old can accept; senders must be >= 24 h old. Neither sign-in here verifies email, so the bonus (30 days each side, INVITE_BONUS_DAYS) waits in invite_bonuses until the invitee clicks a signed link emailed to them (/i/confirm?t=, any browser, 14 days). No bonus for throwaway domains (mailchecker), the inviter's own mailbox (+tag / Gmail dots), staff; inviter half capped at 5 per 30 days (INVITE_MONTHLY_CAP). The existing ?ref= / mc_ref affiliate program is untouched (no aff.attribute()).

Security fixes (rule 10/11 sweep)

  1. Domain / tenant page takeover. addParkedDomain did ON CONFLICT (domain) DO UPDATE SET account_id = excluded.account_id, and upsertTenant overwrote any row. Any signed-in user (POST /api/tenant {addDomain} then ?dn=), or an anonymous email sign-up with domain=moshscript.com, took over another account's page, including Anthony's moshscript.com / moshcode.sh / chovy.com. Now a domain held by another account (parked, tenant row incl. owner-less staff rows, or account page domain) is refused (409), and upsertTenant only updates a row the same account owns.
  2. CoinPay sign-in matched accounts by email. CoinPay's userinfo always says email_verified=false (no verification flow), so registering a CoinPay account with someone's address opened their moshcoding account. Accounts are now bound by CoinPay sub (accounts.coinpay_sub, unique). The deploy that adds the column binds the 5 existing CoinPay users to their current accounts once. After that, a CoinPay sign-in can only claim an account with no password and no binding.
  3. ADMIN_EMAILS granted admin from an unverified email (e.g. native sign-up with an admin address that had no account, then the public /api/affiliate/join with it). findOrCreateAccountByEmail never grants admin now; existing is_admin rows are unchanged.

Tests

bun test tests/: 272 pass (11 new in tests/credits-invites.test.mjs, also run green against Postgres with TEST_PG_URL). tsc --noEmit clean, bun run build ok. Schema change dry-run on a restored copy of prod: bindings and tenant configs as expected.

🤖 Generated with Claude Code

…ount linking

Credit codes (@profullstack/redeem-credits 0.2.0, unit pro_days) grant days of
the paid affiliate plan ($1/mo) through plan_grants, one row per (account,
source), so every grant is idempotent. Invites (@profullstack/affiliates 0.3.0):
3 per account, /invites, /i/<code>, accepted at sign-up, CoinPay sign-in or on
the page; the bonus (30 days each side) waits for an emailed confirm link.

Security fixes found in the sweep:
- any signed-in user (or an anonymous sign-up) could claim a domain another
  account holds (addParkedDomain reassigned it, upsertTenant overwrote the page),
  e.g. moshscript.com / moshcode.sh;
- CoinPay sign-ins were matched to accounts by email, which CoinPay never
  verifies (email_verified=false): bound by CoinPay sub now;
- ADMIN_EMAILS promoted any account with that (unverified) email to admin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@socket-security

socket-security Bot commented Oct 27, 2026 •

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/​redeem-credits@​0.2.07610010090100
Added@​profullstack/​affiliates@​0.3.07610010092100
Addedmailchecker@​6.0.2110010010088100

View full report

Comment thread lib/credits.ts Fixed
@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

ThreatCrush Security Scan

22 finding(s)

HIGH/CRITICAL: 2 | MEDIUM: 14 | LOW: 6

Severity Rule Location
HIGH sh-remote-script-execution public/install.sh:302
HIGH sh-remote-script-execution public/install.sh:305
MEDIUM js-open-redirect app/dashboard/[[...tab]]/page.tsx:67
MEDIUM js-open-redirect app/signup/page.tsx:46
MEDIUM js-unescaped-html-sink components/PitFooter.tsx:30
MEDIUM js-open-redirect components/PitSearch.tsx:118
MEDIUM js-unescaped-html-sink components/Tenant.tsx:29
MEDIUM js-unescaped-html-sink components/Tenant.tsx:30
MEDIUM js-unescaped-html-sink components/Tenant.tsx:78
MEDIUM js-unescaped-html-sink components/Tenant.tsx:150
MEDIUM sql-template-interpolation lib/db.ts:1227
MEDIUM sql-template-interpolation lib/db.ts:1257
MEDIUM redos-nested-quantifier lib/markdown.ts:109
MEDIUM sh-remote-script-execution public/install.sh:123
MEDIUM sh-remote-script-execution public/install.sh:127
MEDIUM sh-remote-script-execution public/install.sh:146
LOW secret-generic-credential tests/domain-webhook-active.test.mjs:18
LOW secret-generic-credential tests/domain-webhook-active.test.mjs:25
LOW secret-generic-credential tests/domain-webhook-active.test.mjs:30
LOW secret-generic-credential tests/project-webhook-management.test.mjs:53
LOW secret-generic-credential tests/signup-profile.test.mjs:28
LOW secret-generic-credential tests/signup-profile.test.mjs:39

Snippets are redacted; ThreatCrush never prints matched credential material.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ralyodio
ralyodio merged commit 5813aad into master Oct 27, 2026
5 checks passed
@ralyodio
ralyodio deleted the feat/credit-codes-invites branch October 27, 2026 03:05
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.

2 participants