{"_id":"@altf4-auth/core","_rev":"2-9bb1d99e4e3837bb6c3d348f6d5a7449","name":"@altf4-auth/core","dist-tags":{"latest":"1.0.1"},"versions":{"1.0.0":{"name":"@altf4-auth/core","version":"1.0.0","_id":"@altf4-auth/core@1.0.0","maintainers":[{"name":"pradyum_m","email":"pradyum.g@somaiya.edu"}],"dist":{"shasum":"650f99d34325cb6bee2953ff300a754b7fdbf191","tarball":"https://registry.npmjs.org/@altf4-auth/core/-/core-1.0.0.tgz","fileCount":8,"integrity":"sha512-k2jq3VSiTDQmid3/4dJpo1at8dilergdrokaHT3ma8rGbDNE7gGkLKYHQYqKFGOc8TtqeEIlfebHB7fCXgoM3w==","signatures":[{"sig":"MEQCIANYsAEr1OQaB2wVmPQJxzb/XCZ0e2b5qxtsY1bWLbZJAiBwFuusY/4xsqLAWNvJDwncpB+ma5EjlJmEjbS05hq3Bg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":151664},"main":"./dist/index.js","_from":"file:altf4-auth-core-1.0.0.tgz","types":"./dist/index.d.ts","module":"./dist/index.mjs","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"}},"scripts":{"lint":"eslint src","test":"vitest run --coverage","build":"tsup","test:watch":"vitest"},"_npmUser":{"name":"pradyum_m","email":"pradyum.g@somaiya.edu"},"_resolved":"C:\\Users\\prady\\AppData\\Local\\Temp\\f83b86a6f3490a858a612951f57bd521\\altf4-auth-core-1.0.0.tgz","_integrity":"sha512-k2jq3VSiTDQmid3/4dJpo1at8dilergdrokaHT3ma8rGbDNE7gGkLKYHQYqKFGOc8TtqeEIlfebHB7fCXgoM3w==","_npmVersion":"11.2.0","description":"Email OTP-based authentication with optional TOTP 2FA","directories":{},"_nodeVersion":"22.14.0","dependencies":{"bcryptjs":"^2.4.3","nodemailer":"^6.9.13","jsonwebtoken":"^9.0.2"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","@types/bcryptjs":"^2.4.6","@types/nodemailer":"^6.4.14","@types/jsonwebtoken":"^9.0.6","@vitest/coverage-v8":"^4.1.2"},"_npmOperationalInternal":{"tmp":"tmp/core_1.0.0_1774694857810_0.7533264203509","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@altf4-auth/core","version":"1.0.1","description":"Email OTP-based authentication with optional TOTP 2FA","main":"./dist/index.js","module":"./dist/index.mjs","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","require":"./dist/index.js","import":"./dist/index.mjs"}},"dependencies":{"bcryptjs":"^2.4.3","jsonwebtoken":"^9.0.2","nodemailer":"^6.9.13"},"devDependencies":{"@types/bcryptjs":"^2.4.6","@types/jsonwebtoken":"^9.0.6","@types/nodemailer":"^6.4.14","@vitest/coverage-v8":"^4.1.2","tsup":"^8.5.1"},"scripts":{"build":"tsup","test":"vitest run --coverage","test:watch":"vitest","lint":"eslint src"},"_id":"@altf4-auth/core@1.0.1","_integrity":"sha512-xAvD9HZzi5lFInWRfxN/wbjOce9Kuk2PTstoj7gnjho0iQZLe/RAjNSo7g9AaeHsQelRx1+sLDQfLetNdAcOEA==","_resolved":"C:\\Users\\prady\\AppData\\Local\\Temp\\97fa32a1e29d9226bd5743b954680b04\\altf4-auth-core-1.0.1.tgz","_from":"file:altf4-auth-core-1.0.1.tgz","_nodeVersion":"22.14.0","_npmVersion":"11.12.1","dist":{"integrity":"sha512-xAvD9HZzi5lFInWRfxN/wbjOce9Kuk2PTstoj7gnjho0iQZLe/RAjNSo7g9AaeHsQelRx1+sLDQfLetNdAcOEA==","shasum":"92c4b7a881d261afb3cd1665b57deeec951597c3","tarball":"https://registry.npmjs.org/@altf4-auth/core/-/core-1.0.1.tgz","fileCount":8,"unpackedSize":176126,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIFd268VdJmBFvLHE5zu1rqknsSxKJGiqIQIxTDu2gEzdAiEAqyLAbxT/4Jz/gsPdQ82AvYREBKjXHXZgLWSC9ng8teY="}]},"_npmUser":{"name":"pradyum_m","email":"pradyum.g@somaiya.edu"},"directories":{},"maintainers":[{"name":"pradyum_m","email":"pradyum.g@somaiya.edu"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/core_1.0.1_1778244576258_0.11558603786243138"},"_hasShrinkwrap":false}},"time":{"created":"2026-03-28T10:47:37.696Z","modified":"2026-05-08T12:49:36.552Z","1.0.0":"2026-03-28T10:47:37.987Z","1.0.1":"2026-05-08T12:49:36.425Z"},"description":"Email OTP-based authentication with optional TOTP 2FA","maintainers":[{"name":"pradyum_m","email":"pradyum.g@somaiya.edu"}],"readme":"# easy-auth\n\nEmail OTP authentication with optional TOTP 2FA — drop it into any Node.js backend and have a working auth system in under 2 minutes.\n\n---\n\n## Why this exists\n\n- **Clerk** is expensive at scale and locks you into a third-party service.\n- **Passport.js** requires significant setup and strategy wrangling before you get anything working.\n- **easy-auth** gives you email OTP login and optional 2FA in a single `createAuth()` call, running entirely on your own infrastructure.\n\nNo vendor lock-in. No paid tiers. No separate auth server. Your users' emails never leave your server.\n\n---\n\n## Getting started\n\n```bash\nnpm install easy-auth easy-auth-sqlite\n```\n\n```ts\n// lib/auth.ts — create once, import everywhere\nimport { createAuth } from \"@altf4-auth/core\";\nimport { sqliteAdapter } from \"@altf4-auth/sqlite\";\n\nexport const auth = createAuth({\n  smtp: {\n    host: \"smtp.example.com\",\n    port: 587,\n    secure: false,\n    auth: { user: \"you@example.com\", pass: process.env.SMTP_PASS },\n    from: \"My App <noreply@example.com>\",\n  },\n  jwt: {\n    secret: process.env.JWT_SECRET, // min 32 characters\n  },\n  store: sqliteAdapter(\"./auth.db\"),\n});\n```\n\n```ts\n// Express route handlers\nimport express from \"express\";\nimport { auth } from \"./lib/auth.js\";\nimport { AuthError } from \"@altf4-auth/core\";\n\nconst app = express();\napp.use(express.json());\n\n// 1. Request an OTP\napp.post(\"/api/auth/send-otp\", async (req, res) => {\n  try {\n    await auth.sendOTP(req.body.email);\n    res.json({ ok: true });\n  } catch (err) {\n    if (err instanceof AuthError) {\n      res.status(err.code === \"INVALID_EMAIL\" ? 400 : 500).json({ error: err.code });\n    } else {\n      res.status(500).json({ error: \"Internal server error\" });\n    }\n  }\n});\n\n// 2. Verify the OTP and receive a session token\napp.post(\"/api/auth/verify-otp\", async (req, res) => {\n  try {\n    const { token, user, isNewUser } = await auth.verifyOTP(req.body.email, req.body.code);\n    res.json({ token, user, isNewUser });\n  } catch (err) {\n    if (err instanceof AuthError) {\n      const status = { OTP_INVALID: 401, OTP_EXPIRED: 401, ACCOUNT_LOCKED: 429, OTP_MAX_ATTEMPTS: 429 }[err.code] ?? 500;\n      res.status(status).json({ error: err.code });\n    } else {\n      res.status(500).json({ error: \"Internal server error\" });\n    }\n  }\n});\n\n// 3. Protected route\napp.get(\"/api/me\", async (req, res) => {\n  const token = req.headers.authorization?.replace(\"Bearer \", \"\");\n  if (!token) return res.status(401).json({ error: \"No token\" });\n  try {\n    const user = await auth.verifyToken(token);\n    res.json({ user });\n  } catch {\n    res.status(401).json({ error: \"Unauthorized\" });\n  }\n});\n```\n\nThat's it. Copy-paste ready. No extra config needed to get started.\n\n---\n\n## Storage adapters\n\n| Package | Best for | Setup |\n|---|---|---|\n| `@altf4-auth/sqlite` | Dev, small apps, single-process | `sqliteAdapter(\"./auth.db\")` |\n| `@altf4-auth/redis` | Multi-process / multi-server | `redisAdapter({ url: \"redis://localhost:6379\" })` |\n| `@altf4-auth/mongo` | Teams already using MongoDB | `mongoAdapter({ uri: \"...\", dbName: \"@altf4-auth/core\" })` |\n\nIf you pass no `store`, an in-memory adapter is used automatically. **It logs a warning on startup and loses all state on restart. Do not use it in production.**\n\nSee [ADAPTERS.md](../../docs/ADAPTERS.md) for full details.\n\n---\n\n## Frontend\n\n- **React / Next.js** — `npm install easy-auth-react` — drop in `<AuthModal />` and pass your API endpoints as props.\n- **Flutter** — `easy_auth_flutter` on pub.dev — use `EasyAuthModal` as a bottom sheet.\n\n---\n\n## 2FA (TOTP)\n\n```ts\n// Enrollment — show this QR code or secret to the user\nconst { secret, qrDataUrl, backupCodes } = await auth.enroll2FA(email);\n\n// Confirmation — after the user scans and enters their first code\nawait auth.confirm2FA(email, totpCodeFromUser);\n\n// On each subsequent login, after verifyOTP succeeds:\nawait auth.verify2FA(email, totpCodeFromUser);\n```\n\nSee [2FA.md](../../docs/2FA.md) for the full flow and backup code usage.\n\n---\n\n## Configuration reference\n\n```ts\ninterface AuthConfig {\n  smtp: {\n    host: string;          // SMTP server hostname\n    port: number;          // Usually 587 (TLS) or 465 (SSL)\n    secure: boolean;       // true for port 465\n    auth: {\n      user: string;        // SMTP username / email\n      pass: string;        // SMTP password or API key\n    };\n    from: string;          // Display name + address: \"App <noreply@app.com>\"\n  };\n  jwt: {\n    secret: string;        // Min 32 chars. Use a random value from a secret manager.\n    expiresIn?: string;    // Default \"7d\". Accepts ms/zeit-style notation.\n  };\n  otp?: {\n    length?: number;       // Default 6\n    ttlSeconds?: number;   // Default 600 (10 minutes)\n    maxAttempts?: number;  // Default 5\n    lockoutSeconds?: number; // Default 900 (15 minutes)\n  };\n  store?: StorageAdapter;  // Default: in-memory (dev only)\n  email?: {\n    subject?: string;      // Default \"Your login code\"\n    templateFn?: (code: string) => { text: string; html: string };\n  };\n}\n```\n\n---\n\n## Error handling\n\nAll errors thrown by easy-auth are `AuthError` instances with a `code` field:\n\n| Code | HTTP status | Meaning |\n|---|---|---|\n| `INVALID_EMAIL` | 400 | Email format rejected before any store access |\n| `OTP_EXPIRED` | 401 | No valid OTP found or it has expired |\n| `OTP_INVALID` | 401 | Wrong code entered |\n| `OTP_MAX_ATTEMPTS` | 429 | Too many wrong attempts — account locked |\n| `ACCOUNT_LOCKED` | 429 | Lockout active from a previous flood of attempts |\n| `TOKEN_INVALID` | 401 | JWT is malformed, tampered, or signed with a different secret |\n| `TOKEN_EXPIRED` | 401 | JWT has expired |\n| `2FA_NOT_ENROLLED` | 400 | TOTP called but user has not enrolled |\n| `2FA_INVALID` | 401 | Wrong TOTP code or backup code |\n| `2FA_ALREADY_ENROLLED` | 400 | enroll2FA called when TOTP is already active |\n| `CONFIG_INVALID` | — | Thrown synchronously at createAuth() time if config is wrong |\n\n---\n\n## Security model\n\n**What easy-auth does:**\n\n- OTPs are generated with `crypto.randomInt` and hashed with bcrypt (cost 10) before storage\n- OTP comparison uses bcrypt.compare — timing-safe by design\n- JWTs are HS256 signed with a minimum 32-char secret, include a `jti` per token\n- TOTP secrets are encrypted at rest with AES-256-GCM, key derived via HKDF from your JWT secret\n- Backup codes are bcrypt-hashed and single-use\n- Failed OTP attempts are counted and result in a configurable lockout\n- OTPs are deleted from the store immediately after a successful verify\n\n**What easy-auth does NOT do (your app's responsibility):**\n\n- HTTP-level rate limiting — use [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit), [@fastify/rate-limit](https://github.com/fastify/fastify-rate-limit), or your CDN/proxy\n- HTTPS — always terminate TLS before your Node.js process\n- CAPTCHA integration\n- IP blocking\n\n---\n\n## Self-hosting\n\neasy-auth runs entirely on your own infrastructure. No telemetry, no callbacks to third-party services. The only external network calls are the SMTP emails you configure and any connections to your chosen database adapter.\n\n---\n\n## Roadmap\n\nFuture versions may add: OAuth / social login, SMS OTP (Twilio / Vonage), per-token revocation via a denylist.\n\n---\n\n## License\n\nMIT\n","readmeFilename":"README.md"}