@devalok/auth-kit
v0.1.0
Published
Devalok's shared auth: the canonical @devalok.in sign-in gate, Google + magic-link providers, an Auth.js v5 config factory, and OTP primitives. Server/edge logic only — no UI (the login screens live in @devalok/login-patterns).
Downloads
96
Readme
@devalok/auth-kit
Devalok's shared authentication logic — the one canonical @devalok.in sign-in
gate, pre-wired Google + magic-link providers, an Auth.js v5 config factory, and
OTP primitives.
This is the logic layer only — no UI. The login screens live in
@devalok/login-patterns.
The two compose by matching shapes, not by importing each other:
@devalok/shilp-sutra (core) ─┐
@devalok/shilp-sutra-brand ─┴─→ @devalok/login-patterns ← screens (prop-driven)
▲ handler props
@devalok/auth-kit ───────────── produces those handlers ← logic (this pkg)
│
the app wires the two togetherWhy it exists: the @devalok.in gate was copy-pasted across ~5 apps with
security-relevant drift (payroll admitted + aliases and skipped
email_verified; karm/share didn't). This builds the door once, correctly.
Install
pnpm add @devalok/auth-kit
# peers: next-auth v5 (>=beta.25). nodemailer only if you use magic-link.The gate
import { devalokEmailGate } from '@devalok/auth-kit/edge'
// In a single-audience app's signIn callback:
const verdict = await devalokEmailGate(
{ email: user.email, profile, provider: account?.provider },
{
allowlist: parseAllowlist(process.env.APP_ALLOWLIST), // per-app, optional
requireVerified: true, // default
onRejectRedirect: '/login?error=not-devalok',
},
)
// verdict: true | false | redirect-path — return it straight from signIn.Dual-audience apps (staff + clients) compose the primitives instead —
isDevalokEmail, emailVerified, matchesAllowlist — and defer accept/reject
to their own DB lookup.
Import the gate from /edge in Edge middleware; the main entry pulls
next-auth provider modules (node-capable).
Providers + config factory
import NextAuth from 'next-auth'
import { PrismaAdapter } from '@auth/prisma-adapter'
import { googleDevalok, makeDevalokAuthConfig } from '@devalok/auth-kit'
import { devalokMagicLink } from '@devalok/auth-kit/email' // subpath — pulls nodemailer
import { prisma } from '@/lib/db'
import { sendEmail } from '@/lib/email' // your ZeptoMail/SMTP transport
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: PrismaAdapter(prisma),
...makeDevalokAuthConfig({
providers: [
googleDevalok(), // per-app client id/secret from env — NEVER shared (nirman-niyam §2)
devalokMagicLink({ sendEmail, from: '[email protected]' }),
],
gate: { allowlist: parseAllowlist(process.env.APP_ALLOWLIST) },
// app-specific DB lookup/create, runs after the gate admits the email:
provisioning: async ({ email, user }) => {
// ...find or create your user row; return true / false / redirect
return true
},
}),
})Magic-link action (wires to login-patterns)
import { createMagicLinkAction } from '@devalok/auth-kit'
import { signIn } from '@/auth'
// Returns a handler shaped exactly like LoginContent.onApplicantLookup.
export const sendMagicLink = createMagicLinkAction({
signIn,
rateLimit: myRedisLimiter, // inject one in production; in-memory fallback warns
})Enumeration-safe: returns { ok: true } whether or not the account exists.
OTP primitives
generateOtp, generateSalt, hashOtp (HMAC-SHA256 + per-record salt),
verifyOtp, timingSafeEqualHex — crypto only (Web Crypto, edge + node).
Storage and the Credentials provider that consumes them stay in the app; store
the salt + hash, never the code.
⚠️ Credentials providers bypass the gate.
makeDevalokAuthConfigtrustscredentialsby default (trustedProviders), so the@devalok.indomain check does NOT run for them. Any customauthorize()(OTP, break-glass password) MUST re-assert it — callassertDevalokEmailOrThrow(email, { allowlist })insideauthorize(), or the domain restriction is silently void for that path.
Scripts
pnpm test # vitest
pnpm typecheck # tsc --noEmit
pnpm build # vite (esm+cjs) + dtsPart of the Devalok estate. Server/edge logic only — keep UI out of this package (that's what makes it importable in Edge middleware).
