- 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.
63 lines
1.9 KiB
TypeScript
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;
|
|
}
|