@gottheflag/otp

HOTP · TOTP · otpauth://

Small OTP primitives with explicit inputs.

A focused JavaScript and TypeScript library for generating HOTP/TOTP codes, parsing authenticator URIs, preserving diagnostics, and serializing the current object state.

pnpm add @gottheflag/otp
Fully static and local. This documentation is one self-contained HTML file. It loads no external scripts, fonts, analytics, trackers, or APIs. You can open docs/index.html directly from disk; nothing on this page is sent anywhere.

Quick start

Use raw bytes when you already own the secret material. Direct class API string secrets are Base64.

import { TOTP } from "@gottheflag/otp";

const secret = new TextEncoder().encode("development-secret");

const otp = new TOTP({
    secret,
    account: "alice@example.com",
    issuer: "Example",
});

try {
    const code = otp.generate();
} finally {
    otp.destroy();
    secret.fill(0);
}

HOTP

import { HOTP } from "@gottheflag/otp";

const otp = new HOTP({
    secret,
    account: "alice@example.com",
    counter: 0n,
});

const code = otp.generate();
otp.counter += 1n;

Secret formats and ownership are explicit

Class API · string

Canonical Base64. It is decoded into bytes owned by the OTP instance because JavaScript strings cannot be securely borrowed or wiped.

Class API · Uint8Array

Raw secret bytes are borrowed by reference with no persistent copy. The caller owns and wipes the buffer.

otpauth:// URI

Standard Base32. A valid parsed instance owns the single decoded byte buffer and can wipe it with destroy().

destroy()

Wipes owned secret bytes, releases borrowed references without modifying them, and permanently disables secret-dependent operations.

Parse authenticator URIs

OTP.parse() reports URI problems as diagnostics instead of turning ordinary malformed input into exceptions.

import { OTP } from "@gottheflag/otp";

const parsed = OTP.parse(uri);

if (parsed.valid) {
    try {
        console.log(parsed.generate());
        console.log(String(parsed));
    } finally {
        parsed.destroy();
    }
} else {
    console.log(parsed.diagnostics);
}

Strict mode

Normal parsing keeps recoverable non-standard fields as warnings. Strict mode promotes those conditions to errors.

const parsed = OTP.parse(uri, { strict: true });

console.log(parsed.valid);
console.log(parsed.diagnostics);

Object state is the source of truth

Parsed instances are normal mutable HOTP or TOTP objects. Serialization reflects the current values, not a stale copy of the original URI.

const parsed = OTP.parse(uri);

if (parsed.valid) {
    parsed.account = "bob@example.com";
    parsed.issuer = "New Example";
    parsed.digits = 8;

    const updatedURI = String(parsed);
}

Supported settings

SettingSupport / default
AlgorithmsSHA1, SHA256, SHA512
Digits6, 7, 8 · default 6
TOTP periodPositive integer seconds · default 30
HOTP counterUnsigned 64-bit bigint · required
TOTP timestampMilliseconds since Unix epoch

The implementation is tested against RFC 4226, RFC 6238, RFC 4648 Base32 vectors, and HMAC reference vectors.

Security

  • Treat OTP secrets and complete secret-bearing otpauth:// URIs as credentials.
  • Do not log, paste into bug reports, or commit real secrets.
  • Prefer Uint8Array when your application should control the secret buffer lifetime explicitly.
  • Uint8Array inputs are borrowed without a persistent copy. Call secret.fill(0) yourself when finished.
  • Call destroy() for OTP instances that own decoded secret material, including parsed URIs and Base64 string secrets.
  • JavaScript cannot guarantee perfect secure-memory erasure, but the package avoids unnecessary persistent byte copies and wipes owned buffers explicitly.
  • The parser preserves diagnostics so applications can make their own acceptance decisions.
  • Report vulnerabilities privately through SECURITY.md.