Skip to content

feat(pwa): credit codes + scarce invites on app.moshcode.sh - #591

Merged
ralyodio merged 1 commit into
mainfrom
feat/pwa-credit-codes-invites
Oct 26, 2026
Merged

ralyodio merged 1 commit into
mainfrom
feat/pwa-credit-codes-invites

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Fleet credits rollout, batch 10 (app.moshcode.sh). PWA only; the CLI is untouched.

What a code grants

MC-XXXX-XXXX-XXXX-XXXX codes (@profullstack/redeem-credits 0.2) grant moshcode credits straight into the existing credit_ledger, the same balance CoinPay packs top up. One ledger row per redemption, source = code:<id>, unique on (user_id, source) (new column + index, migration 027), so a retried grant can never land twice. The module only keeps its code/redemption tables, prefixed redeem_ so they do not collide with credit_ledger.

  • /settings gets a "Have a code?" card; /redeem?code= prefilled page (sign-in first if needed); dashboard credits card links both.
  • POST /api/credits/redeem {code} with Authorization: Bearer mck_... (API key only: /api/ sits outside the CSRF guard, so a cookie is refused).
  • Mint: docker exec appmoshcodesh-app-1 bun apps/pwa/scripts/credit-codes.mjs mint --credits 1000 --label "for X" (also list, revoke <id>, quota <email> <n>), or POST /api/admin/credit-codes for ADMIN_EMAILS operators.

Invites

@profullstack/affiliates 0.3: 3 invites per account on /invites, single use, 30 days, landing at /i/<code> (email sign-up form carrying the code, plus a signed cookie). Accepted only when the email sign-up creates the account; one accepted invite per account.

The bonus (INVITE_BONUS_CREDITS, default 100 each side, same as the sign-up bonus) is paid only after the invitee confirms their email with a signed link that works in any browser. It is refused for throwaway domains, the same mailbox (+tags, Gmail dots), an inviter younger than 24h, or more than 10 bonuses per inviter in 30 days. Every decision is recorded in invite_bonuses. ?ref= sets the shared referral_code cookie (first touch wins), and the referrer is recorded at sign-up (aff_attributions). No commission is paid.

Not touched

Moshpit registry, name registration, ending sales, offers, /webhooks/coinpay (still not rate-limited, by the app or by nginx).

Tests

  • New test/credit-codes.test.mjs (8 tests) passes on node, on bun, and against Postgres (pg-test-preload).
  • Full PWA suite: 818/818 pass on node. Every file passes under bun.
  • Migration 027 dry-run on prod in a rolled-back transaction. Prod DB backed up first.

🤖 Generated with Claude Code

Codes (@profullstack/redeem-credits, MC-XXXX-XXXX-XXXX-XXXX) grant moshcode
credits INTO the existing credit_ledger: one row per redemption, keyed on
(user_id, source) with a unique index, so a grant can never land twice. The
module keeps only its code/redemption tables (prefix redeem_, so it does not
collide with credit_ledger). Redeem on /settings ("Have a code?"), /redeem?code=,
or POST /api/credits/redeem with an API key (never a cookie: /api is outside CSRF).

Invites (@profullstack/affiliates): 3 per account at /invites, landing at
/i/<code> (sign-up form carrying the code + a signed cookie). Accepted only when
the email sign-up creates the account; one accepted invite per account. The
bonus (INVITE_BONUS_CREDITS, default 100 each side) is paid only after the
invitee confirms their email through a signed link that works in any browser,
and is refused for throwaway domains, the same mailbox (+tags, Gmail dots), an
inviter under 24h old, or more than 10 bonuses per inviter in 30 days. Every
decision is recorded in invite_bonuses. ?ref= links set the shared
referral_code cookie (first touch) and are attributed at sign-up; no commission.

Operators mint with scripts/credit-codes.mjs (docker exec) or
/api/admin/credit-codes (ADMIN_EMAILS). Nothing touches the Moshpit registry,
name sales, ending sales or the CoinPay webhook.

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

Copy link
Copy Markdown
Contributor

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
Updatedpg@​8.23.0 ⏵ 8.23.199 +1100100 +189100

View full report


async function signup(base, email, extra = {}) {
const b = browser(base);
const r = await b.post("/auth/register", { email, password: "correct-horse", ...extra });
assert.match(landing.text, /name="invite" value="INV-/);
assert.ok(guest.jar.has("mc_invite"), "the invite rides in a cookie too");

const reg = await guest.post("/auth/register", { email: "invitee@moshtest.dev", password: "correct-horse", invite: inviter.code });
assert.equal(Number(theirs[0].delta), 250);

// The invite is spent.
const late = await browser(base).post("/auth/register", { email: "late@moshtest.dev", password: "correct-horse", invite: inviter.code });
test("invite bonuses are refused for the same mailbox, a too-new inviter and a throwaway domain", { skip }, async () => {
const { base, db } = await app();
const a = await inviterWithInvite(base, db, "dave.smith@gmail.com");
await browser(base).post("/auth/register", { email: "davesmith+alt@gmail.com", password: "correct-horse", invite: a.code });
assert.deepEqual({ ...r1 }, { status: "refused", reason: "same_mailbox" });

const young = await inviterWithInvite(base, db, "young@moshtest.dev", { ageHours: 1 });
await browser(base).post("/auth/register", { email: "friend@moshtest.dev", password: "correct-horse", invite: young.code });
assert.equal(r2.reason, "inviter_too_new");

const c = await inviterWithInvite(base, db, "c@moshtest.dev");
await browser(base).post("/auth/register", { email: "x@mailinator.com", password: "correct-horse", invite: c.code });
const { base, db, codes } = await app();
const a = await inviterWithInvite(base, db, "one@moshtest.dev");
const b = await inviterWithInvite(base, db, "two@moshtest.dev");
await browser(base).post("/auth/register", { email: "target@moshtest.dev", password: "correct-horse", invite: a.code });
await v.get(`/?ref=${ref}`);
await v.get(`/?ref=SOMEONEELSE`);
assert.equal(v.jar.get("referral_code"), ref);
await v.post("/auth/register", { email: "referred@moshtest.dev", password: "correct-horse" });
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

8 finding(s) in the 14 file(s) this pull request changes.

LOW: 8

Severity Rule Location
LOW secret-generic-credential apps/pwa/test/credit-codes.test.mjs:91
LOW secret-generic-credential apps/pwa/test/credit-codes.test.mjs:179
LOW secret-generic-credential apps/pwa/test/credit-codes.test.mjs:202
LOW secret-generic-credential apps/pwa/test/credit-codes.test.mjs:209
LOW secret-generic-credential apps/pwa/test/credit-codes.test.mjs:214
LOW secret-generic-credential apps/pwa/test/credit-codes.test.mjs:219
LOW secret-generic-credential apps/pwa/test/credit-codes.test.mjs:228
LOW secret-generic-credential apps/pwa/test/credit-codes.test.mjs:253
106 pre-existing finding(s) elsewhere in the repository — **HIGH/CRITICAL**: 8 | **MEDIUM**: 87 | **LOW**: 11

Not introduced by this pull request. The full set is in the Security tab.

Severity Rule Location
HIGH js-ssrf-outbound-request apps/pwa/public/sw.js:76
HIGH tls-verification-disabled apps/pwa/src/lib/moshpit-gateway.mjs:299
HIGH sh-remote-script-execution install.sh:86
HIGH sh-remote-script-execution install.sh:90
HIGH sh-remote-script-execution install.sh:258
HIGH sh-remote-script-execution install.sh:269
HIGH sh-remote-script-execution install.sh:275
HIGH tls-verification-disabled src/dns.mjs:823
MEDIUM js-unescaped-html-sink apps/pwa/public/herd/herd.js:117
MEDIUM sql-template-interpolation apps/pwa/src/lib/moshpit-certs.mjs:44
MEDIUM sql-template-interpolation apps/pwa/src/lib/moshpit-certs.mjs:82
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:139
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:153
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:179
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:373
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:377
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:422
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:674
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:870
MEDIUM sql-template-interpolation apps/pwa/src/moshpit.mjs:872

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

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit bfdb9f5 into main Oct 26, 2026
7 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.

2 participants