@lacspace/otp
v1.1.1
Published
TOTP & HOTP two-factor auth, Google Authenticator compatible — generate secrets, compute/verify codes and build otpauth:// QR URIs. Built on Web Crypto: runs on Node, edge and browser. Zero-dependency, isomorphic.
Maintainers
Readme
@lacspace/otp
TOTP & HOTP two-factor auth — Google Authenticator compatible, runs everywhere.
Generate secrets, compute & verify TOTP (RFC 6238) and HOTP (RFC 4226) codes, and build the
otpauth://URI you turn into a QR code. Built on the Web Crypto API, so the same code runs on Node 18+, edge runtimes and the browser — nocryptopolyfills, no native deps. Verified against the official RFC test vectors.
- 🔐
totp/hotp+verifyTotp/verifyHotp(timing-safe, clock-drift window) - 🔑
generateSecret()(CSPRNG) · base32 encode/decode - 📱
keyuri()→otpauth://for Google Authenticator, Authy, 1Password… - ⏱️
timeRemaining()for countdown UIs - ⚡ Zero dependencies · 🌍 isomorphic (Web Crypto) · 📦 ESM + CJS · fully typed
Install
npm install @lacspace/otp # or pnpm add / yarn add / bun addEnroll a user
import { generateSecret, keyuri } from "@lacspace/otp";
const secret = generateSecret(); // store this (encrypted) against the user
const uri = keyuri({ secret, label: "[email protected]", issuer: "Lacspace" });
// otpauth://totp/Lacspace:[email protected]?secret=…&issuer=Lacspace&algorithm=SHA1&digits=6&period=30
// → render `uri` as a QR code for the user to scanVerify a login code
import { verifyTotp } from "@lacspace/otp";
const offset = await verifyTotp(submittedCode, secret); // tolerates ±1 time step by default
if (offset === null) throw new Error("Invalid or expired code");
// offset: 0 = current window, -1/+1 = adjacent (clock drift)Countdown UI
import { totp, timeRemaining } from "@lacspace/otp";
await totp(secret); // current 6-digit code
timeRemaining(); // seconds until it rolls overHOTP (counter-based)
import { hotp, verifyHotp } from "@lacspace/otp";
await hotp(secret, counter);
const matched = await verifyHotp(code, secret, counter, { window: 5 }); // scan ahead 5Options
All functions accept { digits, algorithm } (and TOTP adds period, timestamp):
await totp(secret, { digits: 8, period: 60, algorithm: "SHA-256" });
await verifyTotp(code, secret, { window: 2 });Compatibility: defaults (
SHA-1, 6 digits, 30s) match Google Authenticator, Authy and most apps.
The Lacspace WebKit
| Package | For |
| --- | --- |
| @lacspace/seo | Metadata & JSON-LD |
| @lacspace/env | Typed env variables |
| @lacspace/rate-limit | Rate limiting |
| @lacspace/otp | TOTP/HOTP 2FA (this package) |
| @lacspace/next | Next.js SDK integration |
New in 1.1 — enrollment, replay guard & backup codes
import { setupTotp, verifyTotpOnce, generateBackupCodes, verifyBackupCode } from "@lacspace/otp";
// One-call enrollment: fresh secret + otpauth URI (render as a QR with any lib)
const { secret, uri } = setupTotp({ account: "[email protected]", issuer: "Lacspace" });
// Replay-safe verify — persist the returned step; a re-used code is rejected
const step = await verifyTotpOnce(code, secret, user.lastTotpStep);
if (step === null) throw new Error("invalid or replayed code");
user.lastTotpStep = step;
// Single-use recovery codes — show `codes` once, store `hashes`
const { codes, hashes } = await generateBackupCodes(10);
const i = await verifyBackupCode(entered, hashes); // -1 = no match; else remove hashes[i]Licensing
This package is free under the Lacspace Free Licence — MIT-equivalent 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.
Part of the Lacspace ecosystem — 35 zero-dependency, isomorphic TypeScript packages.
