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

2fa-kit

v1.1.0

Published

Zero-dependency 2FA toolkit: HOTP/TOTP, otpauth URIs, encrypted secret storage, Google Authenticator migration import, and backup codes. Runs on Node 20+, Bun, Deno, edge workers, and browsers.

Readme

2fa-kit

Add Google-Authenticator-style 2FA to your app. Zero dependencies, runs on Node 20+, Bun, Deno, edge workers, and browsers.

CI npm version license

What you need

  • Node.js 20 or newer (nothing else to install, no native modules)
  • One environment variable: MASTER_KEY, any long random string (used to encrypt stored secrets, same idea as a JWT secret)
  • Three columns on your user record: totp_secret, totp_salt, and totp_last_step (an integer, for replay protection)

Install

npm install 2fa-kit

The whole flow in two steps

 enrol:   server makes a secret --> user scans QR --> server stores it encrypted
 login:   user types 6-digit code --> server decrypts secret --> verify --> allow/deny

Step 1: enrol a user

import { createVault, generateSecret, buildUri } from "2fa-kit";

const vault = await createVault(process.env.MASTER_KEY!);

const secret = await generateSecret();
const uri = buildUri({ label: user.email, secret, issuer: "Acme" });
// show `uri` as a QR code (see below) - the user scans it once

const { encrypted, salt } = await vault.encrypt(secret);
// save `encrypted` and `salt` on the user record

Step 2: verify at login

import { verifyTotpWithDelta } from "2fa-kit";

const secret = await vault.decrypt(user.totpSecret, user.totpSalt);
const { valid, step } = await verifyTotpWithDelta(secret, codeFromLoginForm);

if (!valid || step! <= user.totpLastStep) deny();
user.totpLastStep = step!; // persist, then allow

The step check is replay protection, and it is not optional: codes stay valid for up to 90 seconds of clock drift, so a code that is only checked with a boolean can be intercepted and used again. Storing the last accepted step and requiring each login to beat it closes that door (RFC 6238 requires it). verifyTotp still exists and returns a plain boolean for cases where replay is handled elsewhere.

That is the entire integration. Everything below is optional extras.

Showing the QR code

The library gives you the URI; any QR library renders it:

import QRCode from "qrcode";
const dataUrl = await QRCode.toDataURL(uri); // <img src={dataUrl} />

Extras

Backup codes - one-time recovery codes when the user loses their phone. Store only the hashes. Pass your master key so a leaked database row cannot be brute-forced offline:

const { codes, hashed } = await generateBackupCodes({ key: masterKey });
// show `codes` once, store `hashed`

const { valid, remaining } = await verifyBackupCode(input, user.backupCodes, { key: masterKey });
if (!valid) deny();
user.backupCodes = remaining; // codes are single-use: persist, then allow

verifyBackupCode accepts any case, with or without dashes, and compares in constant time.

Import from Google Authenticator - decode a "Transfer accounts" export QR:

const accounts = await parseMigrationUri(migrationUri); // -> ParsedOtpauth[]

API

| Function | What it does | |---|---| | generateSecret(opts?) | Random base32 secret (default 32 chars) | | totp(secret, opts?) | Current code + seconds remaining | | verifyTotp(secret, code, opts?) | Check a code, tolerates +/-1 time step | | verifyTotpWithDelta(secret, code, opts?) | Check a code and report the matched step, for replay protection | | hotp(secret, counter, opts?) | Counter-based code (RFC 4226) | | buildUri(opts) | otpauth:// URI for the QR code | | parseUri(uri) | Parse an otpauth:// URI back into parts | | createVault(masterKey) | Encrypt/decrypt secrets (PBKDF2 + AES-256-GCM) | | generateSalt(length?) | Random hex salt for the user record | | deriveKey(masterKey, salt) | Derive the AES key directly (advanced) | | encryptSecret / decryptSecret | Low-level encrypt/decrypt (advanced) | | generateBackupCodes(opts?) | Recovery codes + digests (HMAC when key is set) | | verifyBackupCode(input, hashed, opts?) | Check a backup code and consume it | | parseMigrationUri(uri) | Decode a Google Authenticator export | | sha256Hex(string) / hmacSha256Hex(key, string) | Hex digest helpers | | base32Encode / base32Decode | RFC 4648 base32 |

Full signatures and options are in the TypeScript types (dist/index.d.ts).

Notes

  • Defaults match the authenticator ecosystem: SHA-1, 6 digits, 30 seconds.
  • Secrets are accepted lowercase, unpadded, or with spaces, just like the apps display them.
  • The vault never stores plaintext: wrong master key or tampered data throws instead of decrypting.
  • Losing MASTER_KEY makes stored secrets unrecoverable. Back it up.

License

MIT - see LICENSE.