log every auth attempt that names an identity the platform does not know

This commit is contained in:
2026-08-06 05:31:10 +00:00
parent b6b9b01aa6
commit 1eb2140f95
6 changed files with 245 additions and 2 deletions
+57
View File
@@ -0,0 +1,57 @@
import { getUsers, getUserByEmail } from 'officerdb';
// Who this platform knows about, loaded once at launch.
//
// This is a CLASSIFIER, never a gate. Nothing here decides whether a request is allowed — the signin
// handler still does its own database lookup and is the only thing that authenticates. The snapshot
// exists so the audit middleware can answer one question cheaply, on the hot path of an unauthenticated
// endpoint: "is the name in this request even one of ours?" A wrong answer costs a spurious log line or
// a missing one, never access.
//
// It holds emails AND usernames because both are identities a caller can present, and both are unique
// columns on `users`. Matching is case-insensitive: an attempt on OWNER@example.com is the owner's own
// address typed differently, not a stranger, and logging it as an intrusion would train the owner to
// ignore the file.
//
// Single-user is a hard invariant here, so this set has two entries in practice. It is a Set rather
// than a string because that invariant is the platform's, not this file's, and a Set costs nothing.
let snapshot: Set<string> | null = null;
const key = (identity: string) => identity.trim().toLowerCase();
const identitiesOf = (user: { email: string; username: string | null }): string[] =>
[user.email, user.username].filter((value): value is string => typeof value === 'string' && !!value.trim());
/** Read every account into memory. Called once at launch; safe to call again to refresh. */
export async function loadKnownUsers(): Promise<number> {
const users = await getUsers();
snapshot = new Set(users.flatMap(identitiesOf).map(key));
return snapshot.size;
}
/**
* Fold a newly created account in. Bootstrap is the only path that mints one, and without this the
* owner's very first sign-in after creating the account would be logged as a stranger — the snapshot
* having been taken while the user table was still empty.
*/
export function rememberUser(user: { email: string; username: string | null }): void {
if (!snapshot) return;
for (const identity of identitiesOf(user)) snapshot.add(key(identity));
}
/** How many identities are loaded, or null if the launch-time load has not succeeded. */
export const knownIdentityCount = (): number | null => snapshot?.size ?? null;
/**
* Whether an identity belongs to an account. Falls back to the database when the launch-time load
* never ran or failed — a Postgres blip at boot must not turn every later login into an alert.
*/
export async function isKnownIdentity(identity: string): Promise<boolean> {
const wanted = key(identity);
if (!wanted) return false;
if (snapshot) return snapshot.has(wanted);
const user = await getUserByEmail(identity).catch(() => undefined);
return !!user;
}