@lacspace/lock
v1.2.0
Published
Account lockout & brute-force protection (server lock) — N-strikes, exponential + progressive/tiered backoff, per-account/per-IP composite locking, allow/deny lists, CAPTCHA step-up threshold, onLock hook, self-resetting window, pluggable store. Zero-depe
Maintainers
Readme
@lacspace/lock
Account lockout & brute-force protection — "server lock".
1.2.0 — the lock engages on the maxAttempts-th failure
lockout({ maxAttempts: 3 }) used to lock on the fourth record(). The
third returned remaining: 0 beside locked: false — zero attempts left, yet
not locked — and every attacker got one free extra guess. Three strikes now
means out, which is how this README always read, how @lacspace/mfa counts in
the same kit, and what the status object was already claiming. Backoff is
unchanged: base delay on the first lock, doubling after, capped at maxDelayMs.
This is a security-tightening behaviour change, hence the minor bump.
Stop credential-stuffing and brute force at the door. Track failed attempts per key (user, email, IP), lock after N strikes with exponential backoff, auto-expire the window, and reset on success. In-memory store built in; implement
LockStorefor Redis/Mongo.
- 🔒
record/check/resetwith a clearLockStatus - 📈 Exponential backoff up to a cap · self-resetting window
- 🔌 Pluggable
LockStore(memory included) - ⚡ Zero dependencies · 🌍 isomorphic · fully typed
New in 1.1.0 — all additive, the core
lockout()path is unchanged: composite-key & multi-dimension locking (throttle per-account and per-IP so account-spraying from one IP is caught), progressive/tiered delay schedules with an optional hard lock, allow/deny lists, a CAPTCHA / step-up threshold (requiresChallenge), anonLocknotify hook, anddescribe()for UI. Clock is injectable for tests.
Install
npm install @lacspace/lockUsage
import { lockout } from "@lacspace/lock";
const guard = lockout({ maxAttempts: 5, baseDelayMs: 60_000, maxDelayMs: 3_600_000 });
// before checking the password
const status = await guard.check(email);
if (status.locked) throw new Error(`Too many attempts. Try again in ${Math.ceil(status.retryAfterMs / 1000)}s`);
if (await verifyPassword(input, stored)) {
await guard.reset(email); // success — clear strikes
} else {
const s = await guard.record(email); // failure — may lock
throw new Error(s.locked ? "Account temporarily locked." : `${s.remaining} attempts left`);
}API
| Export | Description |
| --- | --- |
| lockout(opts?) | maxAttempts, baseDelayMs, maxDelayMs, windowMs, store |
| .check(key) | status without recording |
| .record(key) | record a failure → new status |
| .reset(key) | clear on success |
| MemoryLockStore / LockStore | storage (bring your own for Redis) |
LockStatus → { locked, attempts, remaining, retryAfterMs }.
New in 1.1.0
Composite key & multi-dimension locking
Lock on a composite key, or track two dimensions at once so spraying many accounts from one IP is still throttled by the IP dimension:
import { compositeKey, multiLockout } from "@lacspace/lock";
// (a) one composite key (account + ip)
const guard = lockout({ maxAttempts: 5 });
await guard.record(compositeKey(email, ip));
// (b) per-account AND per-IP at once — blocked if EITHER is locked
const multi = multiLockout({
account: { maxAttempts: 5 },
ip: { maxAttempts: 50 }, // looser, but catches spray from one source
});
const s = await multi.record({ account: email, ip });
if (s.locked) throw new Error(`Locked by ${s.lockedBy.join(", ")}`);Progressive / tiered delays + hard lock + step-up
import { progressiveLockout } from "@lacspace/lock";
const guard = progressiveLockout({
schedule: [0, 0, 0, 1_000, 5_000, 30_000, 300_000], // 3 free, then 1s/5s/30s/5m
challengeAfter: 4, // require a CAPTCHA before the hard lock
hardLockAfter: 8, // terminal lock
hardLockMs: 86_400_000,
onLock: (e) => audit.log("locked", e),
now: () => Date.now(), // injectable clock for tests
});
const st = await guard.record(email);
if (st.requiresChallenge) return showCaptcha();
if (st.locked) return retryAfter(st.retryAfterMs, st.nextAttemptAt);
const ui = await guard.describe(email); // { locked, attemptsRemaining, retryAfter, level }Allow / deny lists (pure checks)
import { keyLists } from "@lacspace/lock";
const lists = keyLists({
allow: ["10.0.0.1"], // never locked
deny: ["1.2.3.4"], // always treated as locked
denyMatch: (k) => k.endsWith(".evil"),
});
lists.decide(ip); // "allow" | "deny" | "none"| Export | Description |
| --- | --- |
| compositeKey(...parts) | collision-safe composite key from parts |
| multiLockout(dims) / MultiLockout | per-dimension guards; check/record/reset a { account, ip } map; strictest combined MultiLockStatus (+ dimensions, lockedBy) |
| progressiveLockout(opts) / ProgressiveLockout | tiered schedule, hardLockAfter/hardLockMs, challengeAfter, onLock, injectable now; check/record/reset/describe/requiresChallenge |
| describe(status) | { locked, attemptsRemaining, retryAfter, level } for UI |
| requiresChallenge(status) | pure step-up check |
| keyLists(opts) / KeyLists | isAllowed/isDenied/decide allow/deny checks |
ProgressiveStatus → { locked, hardLocked, attempts, remaining, retryAfterMs, nextAttemptAt, requiresChallenge, level }, where level ∈ none | soft | challenge | locked | hard.
The Lacspace Security Kit
| Package | For |
| --- | --- |
| @lacspace/crypto | AES encryption & hashing |
| @lacspace/password | Password hashing |
| @lacspace/jwt | JWTs & tokens |
| @lacspace/apikey | API keys |
| @lacspace/otp | TOTP/HOTP 2FA |
| @lacspace/webauthn | Passkeys / biometric |
| @lacspace/mfa | 2FA/3FA orchestration |
| @lacspace/lock | Account lockout (this package) |
| @lacspace/headers | Secure headers / CSP |
| @lacspace/redact | Log redaction |
Licensing
This package is free under the Lacspace Free Licence — permissive freedoms. Use it in personal and commercial projects at no cost; just keep the notice.
Not every Lacspace package is free. We also offer Commercial (paid), Client-specific, and Private (proprietary) packages under separate terms. See the full Lacspace Licence Centre.
The Lacspace Developer Platform
@lacspace/lock is part of 80+ zero-dependency, isomorphic TypeScript packages — one standard library for the modern web. Explore the ecosystem:
- 📦 This package, documented — https://developer.lacspace.com/packages/lock
- 🗂️ All 80+ packages — https://developer.lacspace.com/packages
- 🧭 Developer handbook — guides & runnable recipes — https://developer.lacspace.com/handbook
- 🧪 Live playground — run any package in your browser — https://developer.lacspace.com/playground
- 🖥️ Finished app templates — https://templates.lacspace.com
- 🚀 Scaffold a full app —
npm create lacspace-app@latest
Free under the Lacspace Free Licence — a permissive, free-to-use licence.
