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>
This commit is contained in:
martbost
2026-09-17 10:42:31 -05:00
parent 2dfccf0039
commit e95f9df13a
5 changed files with 4364 additions and 4188 deletions
+88
View File
@@ -0,0 +1,88 @@
// Approved exceptions: people Marty has okayed to hold more than one account.
//
// The guard blocks a second sign-up from a browser that already has an account. For an
// approved person it must let them through, must NOT leave a hard flag (that is what
// silently drops an account off the leaderboard and out of the holding tank), and must
// still respect suspension.
//
// node qa/fraud-allow.mjs
import { spawn } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
const PORT = 8801;
const B = 'http://127.0.0.1:' + PORT;
const DATA = path.join(os.tmpdir(), 'iap-fraud-allow-' + Date.now());
fs.mkdirSync(DATA, { recursive: true });
const ok = [], bad = [];
const t = (n, c, extra) => { (c ? ok : bad).push(n + (c || !extra ? '' : ' -> ' + extra)); };
const srv = spawn(process.execPath, ['server.js'], {
env: { ...process.env, PORT: String(PORT), DATA_DIR: DATA, ADMIN_PASSWORD: 'localtest', ADMIN_EMAIL: 'admin@example.com', NODE_ENV: 'test' },
stdio: ['ignore', 'pipe', 'pipe']
});
const bye = c => { try { srv.kill(); } catch (e) {} try { fs.rmSync(DATA, { recursive: true, force: true }); } catch (e) {} process.exit(c); };
for (let i = 0; i < 60; i++) { try { if ((await fetch(B + '/api/stats')).ok) break; } catch (e) {} await new Promise(r => setTimeout(r, 500)); }
const DEV = 'dededededededededededededededede';
const post = (p, body, hdrs) => fetch(B + p, { method: 'POST',
headers: Object.assign({ 'Content-Type': 'application/json' }, hdrs || {}), body: JSON.stringify(body) })
.then(async r => ({ status: r.status, body: await r.json().catch(() => ({})) }));
// isAdmin() accepts a bearer token, which is far simpler here than a portal session
const ADMIN = { Authorization: 'Bearer localtest' };
// create the first account on this browser
// NOTE: the duplicate checks run at VERIFY, not at start. Starting a code is harmless;
// creating the account is the act that gets blocked.
const signUp = async (email) => {
const s = await post('/api/auth/email/start', { email, fts: Date.now() - 20000 }, { Cookie: 'iap.dev=' + DEV });
if (s.status !== 200 || !s.body.devCode) return s;
return await post('/api/auth/email/verify', { email, code: s.body.devCode }, { Cookie: 'iap.dev=' + DEV });
};
const first = await signUp('primary@example.com');
t('the first account on a browser is created', first.status === 200 && !first.body.error, JSON.stringify(first.body).slice(0, 140));
// a SECOND account on the same browser is blocked, as designed
const blocked = await signUp('second@example.com');
t('a second account on the same browser is blocked by default', blocked.status === 403, blocked.status + ' ' + JSON.stringify(blocked.body).slice(0, 120));
t('and the refusal points at Qualified Start', /Qualified Start/i.test(String(blocked.body.error || '')), String(blocked.body.error || '').slice(0, 90));
// admin approves the exception on the EXISTING account, which is the usual case
const added = await post('/api/admin/fraud/allow', { email: 'primary@example.com', note: 'business partner, approved' }, ADMIN);
t('the exception is accepted', added.status === 200 && Array.isArray(added.body.list), JSON.stringify(added.body).slice(0, 140));
t('and it is listed with its note', (added.body.list || []).some(a => a.email === 'primary@example.com' && /partner/.test(a.note)), JSON.stringify(added.body.list));
// now the same second sign-up goes through
const allowed = await signUp('second@example.com');
t('the approved person can now create the second account', allowed.status === 200 && !allowed.body.error,
allowed.status + ' ' + JSON.stringify(allowed.body).slice(0, 140));
// and it must not be left carrying a hard flag
const rep = await fetch(B + '/api/admin/fraud', { headers: ADMIN }).then(r => r.json());
// Only the HARD flags matter: those are what drop an account off the leaderboard and out
// of the holding tank. 'shared-ip' is informational and carries no penalty, so an approved
// person may legitimately still show it.
const HARD = ['dup-device', 'ip-burst', 'sponsor-device', 'sponsor-ip', 'multi-account'];
const row = (rep.flagged || []).find(f => f.email === 'second@example.com');
const hard = ((row && row.flags) || []).filter(f => HARD.includes(f));
t('the new account carries NO hard flag', hard.length === 0, JSON.stringify(row || {}));
t('and it is marked as allowlisted so the admin sees why', !row || (row.flags || []).includes('allowlisted'), JSON.stringify(row || {}));
t('the exception shows in the admin report', (rep.allow || []).some(a => a.email === 'primary@example.com'), JSON.stringify(rep.allow));
// removing the exception restores the normal rule
await post('/api/admin/fraud/allow', { email: 'primary@example.com', remove: true }, ADMIN);
const reBlocked = await signUp('third@example.com');
t('removing the exception blocks again', reBlocked.status === 403, reBlocked.status + ' ' + JSON.stringify(reBlocked.body).slice(0, 110));
// a bad address is rejected rather than stored
const badAdd = await post('/api/admin/fraud/allow', { email: 'not-an-email', note: '' }, ADMIN);
t('a malformed address is refused', badAdd.status === 400, JSON.stringify(badAdd.body).slice(0, 100));
// and the endpoint is admin-only
const noAuth = await post('/api/admin/fraud/allow', { email: 'x@example.com' });
t('the endpoint requires admin', noAuth.status === 401, String(noAuth.status));
console.log('PASS ' + ok.length);
for (const b of bad) console.log('FAIL ' + b);
bye(bad.length ? 1 : 0);