{"_id":"2fa-kit","_rev":"3-a82ec301b1135ac9e009f822913f6435","name":"2fa-kit","dist-tags":{"latest":"1.1.0"},"versions":{"1.0.0":{"name":"2fa-kit","version":"1.0.0","keywords":["2fa","totp","hotp","otp","authenticator","otpauth","rfc6238","rfc4226","mfa"],"license":"MIT","_id":"2fa-kit@1.0.0","maintainers":[{"name":"amansoomro062","email":"amansoomro062@gmail.com"}],"homepage":"https://github.com/amansoomro062/otpk#readme","bugs":{"url":"https://github.com/amansoomro062/otpk/issues"},"dist":{"shasum":"ad59aca59bfcf356bd7f45bed8abcb97866c6301","tarball":"https://registry.npmjs.org/2fa-kit/-/2fa-kit-1.0.0.tgz","fileCount":7,"integrity":"sha512-S1W8kxP2UXTSbLyUpYsKKp/mgqPKJDJPxHfPpQVGJnrtYG2V7fm0iAkamffvq/DXPmknfl+/QRy77jtD49D3Nw==","signatures":[{"sig":"MEUCIQCj8AvNPW4ot0jjxagF9JphrNQZCvl4y9Jk0til8UuT8gIgGwlYFc5AFCI47tpHvb2b2KomYlXUIXqZ6XdIOaieyRo=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":77277},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"e14499f5c5240f06738c82df07310e6a4b6028db","scripts":{"test":"vitest run","build":"tsup","typecheck":"tsc --noEmit","prepublishOnly":"npm run typecheck && npm test && npm run build"},"_npmUser":{"name":"amansoomro062","email":"amansoomro062@gmail.com"},"repository":{"url":"git+https://github.com/amansoomro062/otpk.git","type":"git"},"_npmVersion":"11.6.2","description":"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.","directories":{},"_nodeVersion":"24.13.0","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.5","vitest":"^3.0.5","typescript":"^5.7.3"},"_npmOperationalInternal":{"tmp":"tmp/2fa-kit_1.0.0_1786664897283_0.6912584122208654","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"2fa-kit","version":"1.0.1","keywords":["2fa","totp","hotp","otp","authenticator","otpauth","rfc6238","rfc4226","mfa"],"license":"MIT","_id":"2fa-kit@1.0.1","maintainers":[{"name":"amansoomro062","email":"amansoomro062@gmail.com"}],"homepage":"https://github.com/amansoomro062/2fa-kit#readme","bugs":{"url":"https://github.com/amansoomro062/2fa-kit/issues"},"dist":{"shasum":"fdfb16cd211d7a5ae17f32a4a3892f92386c6aad","tarball":"https://registry.npmjs.org/2fa-kit/-/2fa-kit-1.0.1.tgz","fileCount":7,"integrity":"sha512-xSfXvslo971g8xtUAXM6ErzkbtUkEfXmYrgRu1Fmi9tmJEg/qF4YpS99Q2Xn6HQqcnCmYbZPwmNJlF5k4G5j6w==","signatures":[{"sig":"MEQCIQDU3WR5VoNi3DlFtIiUkAnRxrbGX3N6PuBtO54oNvjHJAIfRNv0pDodjYxo97sFM6IcsOAXyTXLjyB3yk9GZusxWA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":67720},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"62d2b3e629b677d155fbedeec1b7aebcf1b9f739","scripts":{"test":"vitest run","build":"tsup","typecheck":"tsc --noEmit","prepublishOnly":"npm run typecheck && npm test && npm run build"},"_npmUser":{"name":"amansoomro062","email":"amansoomro062@gmail.com"},"repository":{"url":"git+https://github.com/amansoomro062/2fa-kit.git","type":"git"},"_npmVersion":"11.6.2","description":"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.","directories":{},"_nodeVersion":"24.13.0","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.5","vitest":"^3.0.5","typescript":"^5.7.3"},"_npmOperationalInternal":{"tmp":"tmp/2fa-kit_1.0.1_1786665759004_0.3246795878216555","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"2fa-kit","version":"1.1.0","description":"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.","keywords":["2fa","totp","hotp","otp","authenticator","otpauth","rfc6238","rfc4226","mfa"],"license":"MIT","repository":{"type":"git","url":"git+https://github.com/amansoomro062/2fa-kit.git"},"bugs":{"url":"https://github.com/amansoomro062/2fa-kit/issues"},"homepage":"https://github.com/amansoomro062/2fa-kit#readme","type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"engines":{"node":">=20"},"scripts":{"build":"tsup","test":"vitest run","typecheck":"tsc --noEmit","prepublishOnly":"npm run typecheck && npm test && npm run build"},"devDependencies":{"tsup":"^8.3.5","typescript":"^5.7.3","vitest":"^3.0.5"},"gitHead":"200e5e21695a44e4275907ad533667dc739c0ce2","_id":"2fa-kit@1.1.0","_nodeVersion":"22.23.2","_npmVersion":"12.0.2","dist":{"integrity":"sha512-BuurarPlovOaHpb94v1Ej4vKiSwX6euzf+j4AvxnIWuqdHF/7n3Vfa/iSABW3UTP9FPqrsz1aGbgayaLWV1vhg==","shasum":"3a3ac72361f2afee1e6d3f5498d64c96e34ed482","tarball":"https://registry.npmjs.org/2fa-kit/-/2fa-kit-1.1.0.tgz","fileCount":7,"unpackedSize":81885,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/2fa-kit@1.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCNMdMB3peV2xI7YJ+mcqPTtiA17qcthtqBws8t5We3jwIgYqFmY71gWbY9v4v3vHJE07vuSaoonS6/rOVnN5m/jfc="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:77d33fea-bd21-4da5-9614-aeb733cd4b9e"}},"directories":{},"maintainers":[{"name":"amansoomro062","email":"amansoomro062@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/2fa-kit_1.1.0_1786723121979_0.27698604336338284"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-13T23:48:17.218Z","modified":"2026-08-14T15:58:42.480Z","1.0.0":"2026-08-13T23:48:17.484Z","1.0.1":"2026-08-14T00:02:39.164Z","1.1.0":"2026-08-14T15:58:42.122Z"},"bugs":{"url":"https://github.com/amansoomro062/2fa-kit/issues"},"license":"MIT","homepage":"https://github.com/amansoomro062/2fa-kit#readme","keywords":["2fa","totp","hotp","otp","authenticator","otpauth","rfc6238","rfc4226","mfa"],"repository":{"type":"git","url":"git+https://github.com/amansoomro062/2fa-kit.git"},"description":"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.","maintainers":[{"name":"amansoomro062","email":"amansoomro062@gmail.com"}],"readme":"# 2fa-kit\n\nAdd Google-Authenticator-style 2FA to your app. Zero dependencies, runs on\nNode 20+, Bun, Deno, edge workers, and browsers.\n\n[![CI](https://github.com/amansoomro062/2fa-kit/actions/workflows/ci.yml/badge.svg)](https://github.com/amansoomro062/2fa-kit/actions/workflows/ci.yml)\n[![npm version](https://img.shields.io/npm/v/2fa-kit)](https://www.npmjs.com/package/2fa-kit)\n[![license](https://img.shields.io/badge/license-MIT-blue)](https://github.com/amansoomro062/2fa-kit/blob/main/LICENSE)\n\n## What you need\n\n- Node.js 20 or newer (nothing else to install, no native modules)\n- One environment variable: `MASTER_KEY`, any long random string (used to\n  encrypt stored secrets, same idea as a JWT secret)\n- Three columns on your user record: `totp_secret`, `totp_salt`, and\n  `totp_last_step` (an integer, for replay protection)\n\n## Install\n\n```sh\nnpm install 2fa-kit\n```\n\n## The whole flow in two steps\n\n```\n enrol:   server makes a secret --> user scans QR --> server stores it encrypted\n login:   user types 6-digit code --> server decrypts secret --> verify --> allow/deny\n```\n\n### Step 1: enrol a user\n\n```ts\nimport { createVault, generateSecret, buildUri } from \"2fa-kit\";\n\nconst vault = await createVault(process.env.MASTER_KEY!);\n\nconst secret = await generateSecret();\nconst uri = buildUri({ label: user.email, secret, issuer: \"Acme\" });\n// show `uri` as a QR code (see below) - the user scans it once\n\nconst { encrypted, salt } = await vault.encrypt(secret);\n// save `encrypted` and `salt` on the user record\n```\n\n### Step 2: verify at login\n\n```ts\nimport { verifyTotpWithDelta } from \"2fa-kit\";\n\nconst secret = await vault.decrypt(user.totpSecret, user.totpSalt);\nconst { valid, step } = await verifyTotpWithDelta(secret, codeFromLoginForm);\n\nif (!valid || step! <= user.totpLastStep) deny();\nuser.totpLastStep = step!; // persist, then allow\n```\n\nThe `step` check is replay protection, and it is not optional: codes stay\nvalid for up to 90 seconds of clock drift, so a code that is only checked\nwith a boolean can be intercepted and used again. Storing the last accepted\nstep and requiring each login to beat it closes that door (RFC 6238 requires\nit). `verifyTotp` still exists and returns a plain boolean for cases where\nreplay is handled elsewhere.\n\nThat is the entire integration. Everything below is optional extras.\n\n## Showing the QR code\n\nThe library gives you the URI; any QR library renders it:\n\n```ts\nimport QRCode from \"qrcode\";\nconst dataUrl = await QRCode.toDataURL(uri); // <img src={dataUrl} />\n```\n\n## Extras\n\n**Backup codes** - one-time recovery codes when the user loses their phone.\nStore only the hashes. Pass your master key so a leaked database row cannot\nbe brute-forced offline:\n\n```ts\nconst { codes, hashed } = await generateBackupCodes({ key: masterKey });\n// show `codes` once, store `hashed`\n\nconst { valid, remaining } = await verifyBackupCode(input, user.backupCodes, { key: masterKey });\nif (!valid) deny();\nuser.backupCodes = remaining; // codes are single-use: persist, then allow\n```\n\n`verifyBackupCode` accepts any case, with or without dashes, and compares in\nconstant time.\n\n**Import from Google Authenticator** - decode a \"Transfer accounts\" export QR:\n\n```ts\nconst accounts = await parseMigrationUri(migrationUri); // -> ParsedOtpauth[]\n```\n\n## API\n\n| Function | What it does |\n|---|---|\n| `generateSecret(opts?)` | Random base32 secret (default 32 chars) |\n| `totp(secret, opts?)` | Current code + seconds remaining |\n| `verifyTotp(secret, code, opts?)` | Check a code, tolerates +/-1 time step |\n| `verifyTotpWithDelta(secret, code, opts?)` | Check a code and report the matched step, for replay protection |\n| `hotp(secret, counter, opts?)` | Counter-based code (RFC 4226) |\n| `buildUri(opts)` | `otpauth://` URI for the QR code |\n| `parseUri(uri)` | Parse an `otpauth://` URI back into parts |\n| `createVault(masterKey)` | Encrypt/decrypt secrets (PBKDF2 + AES-256-GCM) |\n| `generateSalt(length?)` | Random hex salt for the user record |\n| `deriveKey(masterKey, salt)` | Derive the AES key directly (advanced) |\n| `encryptSecret` / `decryptSecret` | Low-level encrypt/decrypt (advanced) |\n| `generateBackupCodes(opts?)` | Recovery codes + digests (HMAC when `key` is set) |\n| `verifyBackupCode(input, hashed, opts?)` | Check a backup code and consume it |\n| `parseMigrationUri(uri)` | Decode a Google Authenticator export |\n| `sha256Hex(string)` / `hmacSha256Hex(key, string)` | Hex digest helpers |\n| `base32Encode` / `base32Decode` | RFC 4648 base32 |\n\nFull signatures and options are in the TypeScript types (`dist/index.d.ts`).\n\n## Notes\n\n- Defaults match the authenticator ecosystem: SHA-1, 6 digits, 30 seconds.\n- Secrets are accepted lowercase, unpadded, or with spaces, just like the\n  apps display them.\n- The vault never stores plaintext: wrong master key or tampered data throws\n  instead of decrypting.\n- Losing `MASTER_KEY` makes stored secrets unrecoverable. Back it up.\n\n## License\n\nMIT - see [LICENSE](./LICENSE).\n","readmeFilename":"README.md"}