(params: {
logger: Logger
})
| 66 | const CREATION_CLUSTER_MIN_SIZE = 4 |
| 67 | |
| 68 | export async function identifyBotSuspects(params: { |
| 69 | logger: Logger |
| 70 | }): Promise<SweepReport> { |
| 71 | const { logger } = params |
| 72 | const now = new Date() |
| 73 | const cutoff = new Date(now.getTime() - WINDOW_HOURS * 3600_000) |
| 74 | // postgres-js can't encode a JS Date as an ad-hoc template parameter |
| 75 | // (it only knows how when the driver recognises the target column's |
| 76 | // type). Embed the ISO string with an explicit cast so the FILTER |
| 77 | // clauses below go through cleanly. |
| 78 | const cutoffIso = cutoff.toISOString() |
| 79 | |
| 80 | const sessions = await db |
| 81 | .select({ |
| 82 | user_id: schema.freeSession.user_id, |
| 83 | status: schema.freeSession.status, |
| 84 | model: schema.freeSession.model, |
| 85 | email: schema.user.email, |
| 86 | name: schema.user.name, |
| 87 | handle: schema.user.handle, |
| 88 | banned: schema.user.banned, |
| 89 | user_created_at: schema.user.created_at, |
| 90 | }) |
| 91 | .from(schema.freeSession) |
| 92 | .leftJoin(schema.user, eq(schema.freeSession.user_id, schema.user.id)) |
| 93 | |
| 94 | if (sessions.length === 0) { |
| 95 | return { |
| 96 | generatedAt: now, |
| 97 | totalSessions: 0, |
| 98 | activeCount: 0, |
| 99 | queuedCount: 0, |
| 100 | suspects: [], |
| 101 | creationClusters: [], |
| 102 | } |
| 103 | } |
| 104 | |
| 105 | const userIds = sessions.map((s) => s.user_id) |
| 106 | |
| 107 | const msgStats = await db |
| 108 | .select({ |
| 109 | user_id: schema.message.user_id, |
| 110 | msgs24h: sql<number>`COUNT(*) FILTER (WHERE ${schema.message.finished_at} >= ${cutoffIso}::timestamptz)`, |
| 111 | distinctHours24h: sql<number>`COUNT(DISTINCT EXTRACT(HOUR FROM ${schema.message.finished_at})) FILTER (WHERE ${schema.message.finished_at} >= ${cutoffIso}::timestamptz)`, |
| 112 | lifetime: sql<number>`COUNT(*)`, |
| 113 | }) |
| 114 | .from(schema.message) |
| 115 | .where( |
| 116 | and( |
| 117 | inArray(schema.message.user_id, userIds), |
| 118 | inArray(schema.message.agent_id, FREEBUFF_ROOT_AGENT_IDS), |
| 119 | ), |
| 120 | ) |
| 121 | .groupBy(schema.message.user_id) |
| 122 | const statsByUser = new Map(msgStats.map((m) => [m.user_id!, m])) |
| 123 | |
| 124 | // Agent diversity is a counter-signal: real users fan out across basher, |
| 125 | // file-picker, code-reviewer, etc.; bot farms stay narrow on the root agent. |
no test coverage detected