npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@authlock/core

v0.1.0

Published

Framework-agnostic, django-axes-style login lockout engine — persistent failed-attempt tracking, failure limits, tiered cooloff, and a pluggable store (in-memory + Drizzle)

Readme

@authlock/core

[!NOTE] Unreleased (0.0.0). The engine and all stores are implemented and fully tested (100% coverage, plus real Postgres/MySQL round-trips), but the package has not been published to npm yet. The first release will be 0.1.0.

What it is

@authlock/core tracks failed login attempts in a store you already run and locks an identity out once it trips a failure limit — the classic django-axes behaviour, ported to TypeScript and framework-agnostic:

  • Configurable identity dimensions — key lockout on username, IP, a combination, or any custom dimension. A lock trips if any configured parameter key reaches the limit within the cooloff window.
  • Failure limit → lockout, with cooloff — including tiered cooloff (escalate the wait as failures pile up), reset-on-success, a whitelist predicate, and a Retry-After hint.
  • Pluggable LockoutStore — an in-memory store (single-instance) ships in the box; Drizzle-backed Postgres / SQLite / MySQL stores (with an atomic cross-instance increment) ship from subpaths.
  • Zero runtime dependencies — no framework, no DI, no decorators. Use it from Express, inversify, tsyringe, a bare script, or via the thin @nest-native/lockout NestJS adapter.

Quick start

import { LockoutManager, InMemoryLockoutStore } from '@authlock/core';

const lockout = new LockoutManager({
  store: new InMemoryLockoutStore(),
  limit: 5, // lock after 5 failures…
  cooloffMs: 15 * 60_000, // …for 15 minutes
  parameters: [['username'], ['ip']], // lock by username OR by IP
});

// In your login handler — identity extraction is YOUR trust decision:
const identity = { username, ip: req.ip };

// 1. Gate before checking the credential.
const gate = await lockout.check(identity);
if (gate.locked) {
  res.setHeader('Retry-After', Math.ceil(gate.retryAfterMs! / 1000));
  return res.status(429).json({ error: 'too many attempts' });
}

// 2. Verify the credential however you like, then report the outcome.
if (await verifyPassword(username, password)) {
  await lockout.recordSuccess(identity); // clears the failure counters
  // …issue a session
} else {
  const decision = await lockout.recordFailure(identity);
  return res.status(decision.locked ? 429 : 401).json({ error: 'invalid' });
}

check never mutates state, so it is safe to call on every request; recordFailure and recordSuccess are the two writes, called with the authentication outcome. There is no ambient magic — you wire these three calls into your own handler.

A shared, durable store

The in-memory store is single-process. For multiple instances (or to survive a restart) use a Drizzle store — the increment is a single atomic statement, so concurrent failed attempts across nodes count exactly once each:

import { drizzle } from 'drizzle-orm/node-postgres';
import { LockoutManager } from '@authlock/core';
import { PostgresLockoutStore, pgLockoutTable } from '@authlock/core/postgres';

// Add this table to your Drizzle schema + migration:
export const lockoutAttempts = pgLockoutTable(); // 'lockout_attempts' by default

const lockout = new LockoutManager({
  store: new PostgresLockoutStore(drizzle(pool), lockoutAttempts),
  limit: 5,
  cooloffMs: 15 * 60_000,
  parameters: [['username'], ['ip']],
});

drizzle-orm is an optional peer — you only need it (and a driver) if you use a Drizzle store. Bring your own driver: the store never opens a connection.

Entry points

| Import | Contents | | --- | --- | | @authlock/core | the engine — LockoutManager, InMemoryLockoutStore, types, deriveKeys, policy helpers | | @authlock/core/drizzle | all three Drizzle stores + table factories (needs drizzle-orm, no driver) | | @authlock/core/postgres | PostgresLockoutStore + pgLockoutTable | | @authlock/core/sqlite | SqliteLockoutStore + sqliteLockoutTable | | @authlock/core/mysql | MysqlLockoutStore + mysqlLockoutTable |

Policy options

| Option | Meaning | | --- | --- | | limit | failures allowed before a key locks (locks at exactly limit) | | cooloffMs | base 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 }] | | parameters | dimension combinations to evaluate; a lock trips if any trips | | whitelist? | (id) => boolean — identities that are never counted or locked | | resetOnSuccess? | clear failures on success (default true) | | failMode? | 'open' (default: allow + log on store error) or 'closed' (deny) |

Design principles

  • The core is the cross-framework story. It stays zero-dependency and framework-neutral on purpose — no bespoke inversify/tsyringe adapter packages.
  • Correctness is the product. This is security-critical code: the store's increment is atomic across instances, every configured parameter key is checked, and every change ships with a security pass.
  • fail-open by default. If the store errors, the engine allows the attempt and logs — a database blip must not lock every user out. failMode: 'closed' is available for high-security deployments. Both paths are tested.
  • Identity extraction is the application's trust decision. The engine never reads X-Forwarded-For or any proxy header for you.
  • Not a rate limiter. For request-rate throttling use @nestjs/throttler; this library is about failed-authentication lockout.

MIT licensed. Part of the nest-native family. Not affiliated with the NestJS core team or the django-axes project.