5 Commits

Author SHA1 Message Date
martbost 39572c0ff5 Sign-up deny list beside the allow list
fraud-allow.json says "never block this person". There was nothing for the opposite case.
fraud-block.json: an address on it cannot open an account through /api/signup or the
email-code door, and the admin fraud report carries the list with a matching
/api/admin/fraud/block route. Existing accounts are untouched; Suspend covers those.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-19 05:46:04 -05:00
martbost e95f9df13a Approved exceptions: people you have okayed to hold more than one account
Marty approves specific people for multiple accounts (partners, staff, a spouse on a
shared machine) and needed a way to say so without the guard fighting him.

Admin > Members > Duplicate signals now carries an "Approved exceptions" list: add an
email with a note, see who is on it and when, remove one. It sits directly under the
signals so the two are read together.

An exception can be added against the address they ALREADY have, not just the new one.
That matters because the usual case is approving a person before their second address
exists, and at sign-up time the new address is unknown to us. checkSignup now tracks
every account the sign-up collided with, and clears the block if either side is approved.

Clearing the HARD flags matters as much as clearing the block. Those flags are what
silently drop an account off the leaderboard and bar it from adopting out of the holding
tank, so an approved person would have been "allowed" in name only. They now keep both.
The account is tagged 'allowlisted' instead, so the admin sees why it went through, and
the server logs the exception by name.

Suspension still wins. An exception is permission to hold several accounts, not immunity
from being suspended for something else.

qa/fraud-allow.mjs (12 assertions) boots its own server and walks the real flow: first
account created, second blocked with the Qualified Start redirect, exception added,
second account now created, no hard flag left on it, exception visible in the admin
report, removing it blocks again, malformed address refused, endpoint admin-only.
qa/sponsor-note.mjs 5, qa/run.sh member 0 bugs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 10:42:31 -05:00
martbost 2dfccf0039 Sign-in no longer tells an existing member they are joining somebody's line
Marty signed in and was told "You're joining the line of @bliss". He is member #1.

Cause: the last-touch sponsor cookie lives 30 days, and /api/sponsor set invited
purely from that cookie. So any member who had ever clicked a teammate's invite link
was greeted on the sign-in screen as though logging in would place them under that
person. Untrue, and alarming in exactly the wrong place: their sponsor locked at their
first purchase and nothing on that screen can move it. Anyone seeing that would
reasonably worry their line was about to change.

The greeting now shows when someone actually arrived through a link (?ref= in the URL),
or when the cookie is present AND this browser has never had an account, which is the
genuine "came back later to finish joining" case. A browser that already has an account,
or a signed-in session, never sees it.

Attribution is deliberately untouched: the cookie still resolves, the sponsor id is
still returned, and placement still works exactly as before. Only the greeting changed.

fraud.hasAccountOnDevice(req) is the new signal, reusing the device cookie the
one-account-per-person checks already set.

qa/sponsor-note.mjs (5 assertions) boots its own throwaway server and creates a REAL
account so the case is proven rather than assumed: still greeted with ?ref=, still
greeted from the cookie on a browser with no account, NOT greeted on the browser that
has one, and attribution still resolving. qa/run.sh member: 0 bugs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 08:41:46 -05:00
martbost a2cea3a2be Anti-fraud refusals redirect to Qualified Start (the sanctioned way to hold extra positions); chatbot says the same
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 14:52:46 -05:00
martbost 34c62b9d3a Anti-fraud: one account per person enforced at sign-up (device cookie + IP), flags, admin duplicate signals, suspend switch
Marty, 2026-09-16, after @megamol created megamol2/megamol3 under his own link and bought $20 on each
to fake his two qualifying buyers. fraud.js records sign-up IP/UA/browser id (iap.dev cookie set with
the code request) and last-seen on sign-in. New accounts: dup-device (browser already has an account)
and sponsor-device are refused, ip-burst (> fraudMaxSignupsPerIpDay, default 2, per 24h) is refused;
sponsor-ip and shared-ip are flagged only. Flagged/suspended accounts never count on the leaderboard
and cannot adopt from the tank; suspended accounts are signed out everywhere (auth.fromRequest
wrapper) and refused at sign-in. Admin > Members: Duplicate signals card (shared browser / IP,
flagged, suspended), flags badge, Suspend/Unsuspend; GET /api/admin/fraud; PATCH members {suspend,
reason, flags}. Telegram admin alert on every block/flag. Privacy page + chatbot prompt updated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 14:39:13 -05:00