@mrmelphin/otp-lib
v0.1.0
Published
OTP (One-Time Password) library with pluggable storage and delivery
Maintainers
Readme
otp-lib-ts
Lightweight OTP (One-Time Password) library for TypeScript/Node.js with pluggable storage and delivery.
Features
- Cryptographically secure numeric OTP generation (
crypto.randomInt) - SHA-256 hashed record storage (no plaintext codes persisted)
- One active OTP per login (upsert semantics)
- Configurable TTL and code length
- Pluggable
RepositoryandSenderinterfaces — bring your own storage and delivery - Built-in repository implementations: In-Memory, PostgreSQL, SQLite, MongoDB
Install
npm install @mrmelphin/otp-libQuick Start
import { OtpService, DEFAULT_CONFIG } from "@mrmelphin/otp-lib";
import type { Repository, Sender } from "@mrmelphin/otp-lib";
import { createMemoryRepository } from "@mrmelphin/otp-lib";
const repo = createMemoryRepository();
const sender: Sender = /* your implementation */;
const svc = new OtpService(repo, sender, DEFAULT_CONFIG);
// Generate and send OTP
const result = await svc.generate("[email protected]");
if (result.sendError) {
console.warn("OTP generated but send failed:", result.sendError);
}
// Verify OTP
const valid = await svc.verify("[email protected]", result.code);
if (!valid) {
throw new Error("Invalid or expired OTP");
}
console.log("OTP verified!");
// Clean up expired records (call periodically)
await svc.cleanExpired();Configuration
import type { OtpConfig } from "@mrmelphin/otp-lib";
const config: OtpConfig = {
ttl: 10 * 60 * 1000, // 10 minutes (default: 300000 = 5 min)
otpLength: 8, // default: 6, minimum: 4
};Interfaces
Repository
interface Repository {
upsert(record: OtpRecord): Promise<void>;
findByOtp(otpHash: string): Promise<OtpRecord | null>;
deleteByOtpId(otpId: string): Promise<void>;
deleteExpired(threshold: Date): Promise<void>;
}Sender
interface Sender {
send(login: string, code: string): Promise<void>;
}Data Model
interface OtpRecord {
otpId: string; // SHA-256(login)
otp: string; // SHA-256(login + code)
createdAt: Date;
}Repositories
The library ships with four ready-to-use repository implementations. Each can be imported from the main package or directly from @mrmelphin/otp-lib/repo/*. Driver dependencies are optional peer deps — install only what you need.
In-Memory
Zero dependencies. Useful for tests and prototyping — data is lost on process restart.
import { createMemoryRepository } from "@mrmelphin/otp-lib";
const repo = createMemoryRepository();PostgreSQL
Uses any pg-compatible client or pool.
npm install pgimport { Pool } from "pg";
import { createPostgresRepository, createPostgresSchema } from "@mrmelphin/otp-lib";
const pool = new Pool({ connectionString: "postgres://localhost:5432/mydb" });
// Create table and indexes (call once, e.g. on app startup)
await createPostgresSchema(pool);
const repo = createPostgresRepository(pool);
// Optional: createPostgresRepository(pool, { tableName: "custom_otp" })SQLite
Uses better-sqlite3 (synchronous driver, wrapped in async interface).
npm install better-sqlite3import Database from "better-sqlite3";
import { createSqliteRepository, createSqliteSchema } from "@mrmelphin/otp-lib";
const db = new Database("otp.db");
// Create table and indexes (call once)
createSqliteSchema(db);
const repo = createSqliteRepository(db);
// Optional: createSqliteRepository(db, { tableName: "custom_otp" })MongoDB
Uses the official mongodb driver — pass a Collection.
npm install mongodbimport { MongoClient } from "mongodb";
import { createMongoRepository, createMongoSchema } from "@mrmelphin/otp-lib";
const client = new MongoClient("mongodb://localhost:27017");
const coll = client.db("mydb").collection("otp");
// Create indexes (call once)
await createMongoSchema(coll);
const repo = createMongoRepository(coll);Testing
npm testDesign Decisions
- SHA-256 without salt — OTP codes are short-lived and single-use; the threat model differs from password storage.
- Lookup by OTP hash — verification uses
findByOtp(hash)rather than looking up by login, preventing information leaks about active OTPs. - No rollback on send failure — if sending fails, the record stays; the caller can retry via
generate(upsert overwrites), and TTL handles expiration.
License
MIT
