Skip to main content

API Reference

@authlock/core

The engine

ExportKindPurpose
LockoutManagerclasscheck() (read-only pre-auth gate), recordFailure(), recordSuccess(), reset() (unlock one identity), resetAll() (unlock everyone), pruneExpired(); multi-key evaluation + onLockout hook
InMemoryLockoutStoreclasssingle-instance store, ships in the box
deriveKeysfunctionresolve an identity to its storage keys (sha256 of the dimension/value pairs — raw values never reach a store)
cooloffFor / effectiveWindowMs / evaluateRecordfunctionsthe pure policy maths, exported for custom stores and tooling
VERSIONconstthe package version

Types

TypePurpose
Identifiersthe identity dimensions (username, ip, userAgent, custom…)
LockoutParameterone dimension combination that can trip a lock, e.g. ['ip', 'userAgent']
FailureRecorda stored counter: {key, failures, firstFailureAt, lastFailureAt} — lock state is derived, never stored. The window is measured from firstFailureAt; the cooloff from lastFailureAt.
CooloffTier{atFailures, cooloffMs} — escalating cooloff by failure count
FailMode'open' (allow + log on store error, default) or 'closed' (deny)
LockoutPolicylimit, cooloff, window, tiers, parameters, whitelist, resetOnSuccess, failMode
LockoutDecision{locked, retryAfterMs, trippedParameter}
LockoutManagerOptionsthe policy plus store, onLockout, logger, and an injectable now clock
LockoutStorethe persistence seam: increment / get / clear / clearExpired (each sync or async)

Policy options

OptionMeaning
limitfailures allowed before a key locks (locks at exactly limit)
cooloffMsbase lock duration once locked
windowMs?failure-counting window; defaults to the effective cooloff
tiers?escalating cooloff by failure count, e.g. [{atFailures: 10, cooloffMs: 3_600_000}]
parametersdimension combinations to evaluate; a lock trips if any trips
normalize?Record<string, (value) => string> — per-dimension normalizers applied before hashing (case/whitespace-bypass defence)
whitelist?(id) => boolean | Promise<boolean> — identities never counted or locked
resetOnSuccess?clear failures on success (default true)
failMode?'open' (default) or 'closed'

Store subpaths

ImportContents
@authlock/core/drizzleall three stores + table factories (needs drizzle-orm, no driver)
@authlock/core/postgresPostgresLockoutStore + pgLockoutTable()
@authlock/core/sqliteSqliteLockoutStore + sqliteLockoutTable()
@authlock/core/mysqlMysqlLockoutStore + mysqlLockoutTable()

Every dialect uses the same table shape — key (primary key), failures, and first_failure_at (a numeric epoch in ms) — and the same semantics: the increment is a single atomic create-or-increment-with-window-reset statement, so concurrent attempts across app instances count exactly once each. Postgres and SQLite read the result back with RETURNING; MySQL (which has no RETURNING) re-selects the row, which can only ever report an equal-or-higher count — never an undercount an attacker could slip through.

drizzle-orm is an optional peer: the root @authlock/core import pulls no Drizzle at all and stays zero-dependency.

@nest-native/lockout

ExportKindPurpose
LockoutModuledynamic moduleforRoot(options) / forRootAsync({useFactory, inject, imports}); global by default
LockoutGuardCanActivatereject-if-locked, applied before authentication — HTTP 429 + Retry-After (Express and Fastify responses)
LockoutServiceprovidercheck() / reportFailure() / reportSuccess() / reset() / resetAll() — the explicit call site for your login handler and admin unlock
defaultExtractorfunctionusername from the body, ip from req.ip, userAgent from the header — deliberately no X-Forwarded-For trust
LOCKOUT_MANAGER / LOCKOUT_OPTIONStokensSymbol.for DI tokens (the manager is injectable directly)
LockoutModuleOptionstypeeverything LockoutManagerOptions takes, plus extractor and isGlobal
IdentifierExtractortype(context: ExecutionContext) => Identifiers
VERSIONconstthe adapter version

The adapter builds only on stable Nest primitives (CanActivate, DynamicModule, HttpException, ExecutionContext) and supports NestJS 10, 11, and 12.

:::tip Identity extraction is your trust decision The default extractor reads req.ip — the connection address. Behind a proxy, configure your platform's trust proxy (or supply your own extractor) so the IP dimension reflects a source you actually trust; the library never reads proxy headers for you. :::

Security & operations

  • Do not trust X-Forwarded-For. This is the vulnerability class that hit django-axes and other tools: if the IP you key on comes from a spoofable header, an attacker can rotate it to bypass IP lockout, or forge a victim's IP to lock them out. The default extractor uses req.ip, not X-Forwarded-For — behind a proxy, configure trust proxy so req.ip reflects a source you trust. Because a lock trips if any parameter trips, a ['username'] parameter still catches a single-target brute force even when the IP is spoofable. And never whitelist on a dimension an attacker can forge.
  • Normalize identity dimensions. Alice, alice, and alice hash to three different counters — on a case-insensitive login an attacker bypasses the limit by varying case or whitespace. Set a per-dimension normalize map (e.g. { username: (v) => v.trim().toLowerCase() }); it is applied before hashing on every path (check / record / reset). Or normalize in your extractor.
  • Bound store growth. Every distinct identity creates a record. Schedule pruneExpired() (it drops records past their window, capping the store to identities active within windowMs), put a rate limiter (@nestjs/throttler) in front, and use a Drizzle store (not in-memory) for hostile or multi-instance deployments.
  • fail-open vs. availability. With the default failMode: 'open', a store error allows the attempt and logs — so brute-force protection is off while the store is down. Use failMode: 'closed' to deny during an outage instead.
  • Bulk unlock (incident response). resetAll() clears every counter — the "unlock everyone" button for a false-positive lockout wave. To unlock a single identity use reset(id); note that with single-dimension parameters ([['username'], ['ip']]) reset({ username }) already clears that username's lock across every IP (the ['ip'] parameter is skipped when no IP is given). Unlocking one identity across a combination parameter ([['username', 'ip']]) is not supported — the store is keyed by a one-way hash, so there is no way to enumerate "every IP this username is locked on" without keeping a reverse index of raw dimensions, which would defeat the property that credentials never reach the store. Use resetAll() for that case.

Audit logging

Some deployments need a record of failed logins and lockouts — for forensics, compliance, or alerting. authlock stores only the counters it needs for the lock decision (hashed keys, no raw identities), so it is not an audit log. But you don't need it to be one: an audit trail is built from what you already have.

Log each attempt at your own call site. You already call reportFailure / reportSuccess (or the core recordFailure / recordSuccess) from your login handler — that is the natural, complete place to record every attempt, with the outcome you already know. There is deliberately no per-failure hook in the engine: it would only tell you what your handler already knows, one layer removed.

async login(input: LoginInput, ip: string | undefined) {
const identity = { username: input.username, ip };
const gate = await this.lockout.check(identity);
if (gate.locked) {
this.audit.record({ event: 'login.locked', ...identity }); // dimensions only
throw new TooManyRequests();
}
const user = await this.verify(input);
if (!user) {
await this.lockout.reportFailure(identity);
this.audit.record({ event: 'login.failed', ...identity });
throw new Unauthorized();
}
await this.lockout.reportSuccess(identity);
this.audit.record({ event: 'login.ok', username: user.username, ip });
}

Alert on lock transitions with onLockout. The hook fires once when an attempt first trips a key over the limit (and on each tier escalation) — the right signal for "an account is now locked" alerting, without logging every failure yourself:

new LockoutManager({
// ...
onLockout: (id, decision) =>
this.audit.record({ event: 'lockout', ...id, retryAfterMs: decision.retryAfterMs }),
});

Surface store failures with logger. It is called (error, context) whenever a store operation throws — wire it to your logger so a degraded store (which, under the default failMode: 'open', silently disables protection) is visible.

Never log the credential. Record only the identity dimensions used for the lock decision (username, IP, user-agent) and the outcome — never the password or token. The engine already keeps credentials out of the store by hashing; your audit log must uphold the same rule.