@digicroz/jwt
v1.0.1
Published
Production-grade JWT utilities with complete type safety, timing-safe verification, and error handling without throwing. Fully typed, tested, and optimized for modern TypeScript projects.
Maintainers
Readme
@digicroz/jwt
Production-grade JWT utilities with complete type safety, zero thrown errors, and timing-safe verification.
A modern, type-safe JWT library for Node.js and TypeScript. Built with security-first design, comprehensive type inference, and production-ready error handling. Never throws errors—always returns a Result type for predictable error handling.
🌟 Features
- 🔒 Type-Safe: Full TypeScript support with generic payload types
- 🚫 No Throw Errors: All operations return
Result<T>(success | error) - ⏱️ Timing-Safe: Protection against timing attacks on token verification
- 🧪 Fully Tested: 79 tests with 82% coverage
- ⚡ Production-Ready: Error types, detailed diagnostics, and error chaining
- 📦 Single Dependency: Only depends on
jsonwebtoken - 🌐 Universal: Works in Node.js and modern browsers
- 📝 Well Documented: Comprehensive JSDoc and examples
🎯 Why @digicroz/jwt?
Problem: Traditional JWT Libraries
// Old way - Throws errors, poor type safety
try {
const payload = await jwtVerifyAsync(token, secret)
// payload type is unknown!
} catch (err) {
// Handle multiple error types
}Solution: @digicroz/jwt
// New way - Type-safe, no thrown errors
const result = await jwtVerify<CustomPayload>(token, secret)
if (result.success) {
// result.data is typed as CustomPayload!
console.log(result.data.userId)
} else {
// Handle specific error types
console.error(`${result.error.type}: ${result.error.message}`)
}📦 Installation
npm install @digicroz/jwt🚀 Quick Start
Verify JWT Token
import { jwtVerify } from "@digicroz/jwt"
// Define your payload type
interface AuthPayload {
userId: string
email: string
role: "admin" | "user"
}
const result = await jwtVerify<AuthPayload>(token, secret)
if (result.success) {
console.log(`User: ${result.data.userId}`)
} else {
console.error(`Verification failed: ${result.error.type}`)
}Sign JWT Token
import { jwtSign } from "@digicroz/jwt"
const payload = { userId: "123", role: "admin" }
const result = jwtSign(payload, secret, {
expiresIn: "1h",
issuer: "my-app",
})
if (result.success) {
console.log(`Token: ${result.data}`)
} else {
console.error(`Signing failed: ${result.error.message}`)
}Decode JWT Token
import { jwtDecode } from "@digicroz/jwt"
// Decode without verification - inspect token contents
const result = jwtDecode<AuthPayload>(token)
if (result.success) {
console.log(result.data) // Payload without verification
} else {
console.error("Invalid token structure")
}📚 API Reference
jwtVerify<T>(token, secret, options?)
Verify and decode a JWT token asynchronously.
const result = await jwtVerify<PayloadType>(token, secret, {
algorithms: ["HS256"],
issuer: "my-app",
audience: "my-api",
ignoreExpiration: false,
clockTolerance: 0,
})Returns: Promise<Result<T>>
jwtSign<T>(payload, secret, options?)
Sign and create a JWT token synchronously.
const result = jwtSign(payload, secret, {
expiresIn: "24h",
issuer: "my-app",
subject: "user-auth",
audience: "my-api",
algorithm: "HS256",
})Returns: Result<string>
jwtDecode<T>(token, options?)
Decode a JWT token without verification.
const result = jwtDecode<PayloadType>(token, {
complete: false,
})Returns: Result<T>
🛡️ Error Handling
Error Types
import { JwtErrorType } from "@digicroz/jwt"
enum JwtErrorType {
INVALID_TOKEN = "INVALID_TOKEN",
EXPIRED_TOKEN = "EXPIRED_TOKEN",
INVALID_SIGNATURE = "INVALID_SIGNATURE",
MALFORMED_TOKEN = "MALFORMED_TOKEN",
INVALID_ALGORITHM = "INVALID_ALGORITHM",
VERIFICATION_FAILED = "VERIFICATION_FAILED",
SIGNING_FAILED = "SIGNING_FAILED",
INVALID_SECRET = "INVALID_SECRET",
UNKNOWN_ERROR = "UNKNOWN_ERROR",
}Error Handling Patterns
// ✅ BEST DX: Direct equality check
const result = await jwtVerify(token, secret)
if (result.success === false) {
// TypeScript knows result.error exists here
console.error(`Error: ${result.error.message}`)
}
// ✅ ALSO GOOD: Check success === true
if (result.success === true) {
// TypeScript knows result.data exists here
console.log(result.data)
}
// ✅ ALTERNATIVE: Using type guards
import { isSuccess, isError } from "@digicroz/jwt"
if (isError(result)) {
console.error(result.error.type)
} else if (isSuccess(result)) {
console.log(result.data)
}
// ✅ FOR COMPLEX LOGIC: Specific error handling
if (result.success === false) {
switch (result.error.type) {
case JwtErrorType.EXPIRED_TOKEN:
// Handle expired token
break
case JwtErrorType.INVALID_SIGNATURE:
// Handle invalid signature
break
default:
// Handle other errors
}
}
// ❌ DON'T USE: Negation pattern (!result.success)
// TypeScript can't narrow the type properly with negation
// if (!result.success) {
// console.error(result.error) // TS Error!
// }🔐 Security Features
Timing-Safe Comparison
Protects against timing attacks:
import { timingSafeEqual } from "@digicroz/jwt/utils"
const isEqual = timingSafeEqual(secret1, secret2)Token Structure Validation
Quick structural validation before full verification:
import { isValidTokenStructure } from "@digicroz/jwt/utils"
if (!isValidTokenStructure(token)) {
console.error("Invalid token format")
}📋 Usage Examples
Express Middleware
import { jwtVerify, JwtErrorType } from "@digicroz/jwt"
export async function authMiddleware(req, res, next) {
const token = req.headers.authorization?.split(" ")[1]
if (!token) {
return res.status(401).json({ error: "Missing token" })
}
const result = await jwtVerify(token, process.env.JWT_SECRET)
if (!result.success) {
if (result.error.type === JwtErrorType.EXPIRED_TOKEN) {
return res.status(401).json({ error: "Token expired" })
}
return res.status(401).json({ error: "Invalid token" })
}
req.user = result.data
next()
}Refresh Token Flow
const refreshResult = await jwtVerify(refreshToken, process.env.REFRESH_SECRET)
if (refreshResult.success) {
// Issue new access token
const newToken = jwtSign(
{ userId: refreshResult.data.userId },
process.env.JWT_SECRET,
{ expiresIn: "1h" }
)
if (newToken.success) {
return res.json({ accessToken: newToken.data })
}
}
return res.status(401).json({ error: "Token refresh failed" })🧪 Testing
Run the test suite:
# Run all tests
npm run test
# Run with coverage
npm run test:coverage
# Watch mode
npm run test:watch
# UI mode
npm run test:ui📈 Performance
- No thrown errors: Eliminates overhead of exception handling
- Timing-safe verification: Constant-time comparison prevents timing attacks
- Tree-shakeable: Only import what you need
- Lightweight: Single dependency (jsonwebtoken)
🔄 Migration from jwtVerifyAsync
Before (Old approach):
try {
const payload = await jwtVerifyAsync(token, secret)
} catch (err) {
// Handle error
}After (New approach):
const result = await jwtVerify(token, secret)
if (result.success) {
const payload = result.data
}📝 API Types
Result Type
type Result<T> =
| { success: true; data: T }
| { success: false; error: JwtError }JwtPayload Interface
interface JwtPayload {
[key: string]: unknown
iat?: number // Issued at
exp?: number // Expiration time
nbf?: number // Not before
iss?: string // Issuer
sub?: string // Subject
aud?: string | string[] // Audience
jti?: string // JWT ID
}🆘 Troubleshooting
TypeScript Error: "Type does not satisfy constraint 'JwtPayload'"
Your payload type must have an index signature:
// ✅ Correct
interface AuthPayload extends Record<string, unknown> {
userId: string
}
// ❌ Wrong
interface AuthPayload {
userId: string
}"Invalid token" Error Even with Valid Token
Check these common issues:
- Wrong secret: Ensure the secret matches the one used to sign
- Expired token: Check expiration time with
jwtDecode() - Malformed token: Verify token has 3 parts separated by dots
- Clock skew: Use
clockToleranceoption for synchronization issues
📄 License
MIT © Adarsh Hatkar
🤝 Contributing
Contributions welcome! Please open an issue or submit a PR.
