Files
Nexumi/apps/bot/src/modules/verification/alt-detection.ts
smueller 04e9ffad28 Enhance verification system with multi-provider support and alt-account detection
- Added support for multiple captcha providers including Google reCAPTCHA (v2 and v3), hCaptcha, and Cloudflare Turnstile.
- Introduced new fields in the verification configuration for selecting captcha providers and enabling alt-account detection.
- Implemented logic to handle verification attempts and flag potential alt accounts based on IP and invite code analysis.
- Updated environment configuration to include necessary keys for captcha providers.
- Enhanced the user interface to allow selection of captcha providers in the verification setup.
- Improved backend handling of verification records to store additional data related to captcha provider usage.
2026-07-23 11:28:58 +02:00

63 lines
1.9 KiB
TypeScript

export type AltSignalMatch = {
reason: 'ip' | 'invite_cluster';
matchedUserId: string | null;
};
export type AltDetectionInput = {
userId: string;
accountAgeDays: number;
inviteCode: string | null;
ipHash: string | null;
/** Prior verification records in the same guild (excluding current user). */
priorRecords: Array<{
userId: string;
inviteCode: string | null;
ipHash: string | null;
createdAt: Date;
flaggedAsAlt: boolean;
}>;
altIpWindowDays: number;
altInviteWindowHours: number;
altMaxAccountsPerInvite: number;
/** Only flag invite clusters when the current account is this young or younger. */
youngAccountMaxDays?: number;
};
/**
* Detects likely alt accounts from Discord + captcha signals.
* Does not use browser fingerprinting beyond salted IP / UA hashes from the captcha page.
*/
export function detectAltAccount(input: AltDetectionInput): AltSignalMatch | null {
const youngMax = input.youngAccountMaxDays ?? 7;
const now = Date.now();
if (input.ipHash) {
const ipWindowMs = input.altIpWindowDays * 24 * 60 * 60 * 1000;
const ipMatch = input.priorRecords.find(
(record) =>
record.ipHash === input.ipHash &&
now - record.createdAt.getTime() <= ipWindowMs
);
if (ipMatch) {
return { reason: 'ip', matchedUserId: ipMatch.userId };
}
}
if (input.inviteCode && input.accountAgeDays <= youngMax) {
const inviteWindowMs = input.altInviteWindowHours * 60 * 60 * 1000;
const recentSameInvite = input.priorRecords.filter(
(record) =>
record.inviteCode === input.inviteCode &&
now - record.createdAt.getTime() <= inviteWindowMs
);
if (recentSameInvite.length >= input.altMaxAccountsPerInvite) {
return {
reason: 'invite_cluster',
matchedUserId: recentSameInvite[0]?.userId ?? null
};
}
}
return null;
}