@periodic/tungsten
v1.0.0
Published
Production-grade, security-auditable authentication primitives for Node.js with TypeScript support
Maintainers
Readme
🔩 Periodic Tungsten
Production-grade, security-auditable authentication primitives for Node.js with TypeScript support
Part of the Periodic series of Node.js packages by Uday Thakur.
💡 Why Tungsten?
Tungsten gets its name from the chemical element renowned for having the highest melting point of all metals — it holds its structure under conditions that destroy everything else. In engineering, tungsten is the material you reach for when the environment is too extreme for anything less. Just like tungsten performs where other materials fail, this library handles authentication under conditions where a mistake means a breach.
In chemistry, tungsten forms exceptionally strong carbide compounds used in cutting tools and armour-piercing projectiles — its strength comes not from softness or flexibility, but from density and resistance to deformation. Similarly, @periodic/tungsten is uncompromising: constant-time comparisons, memory-hard hashing, cryptographically secure generation, and no shortcuts.
The name represents:
- Hardness: Cryptographic primitives that don't bend under attack
- Precision: Every operation is explicit — no magic, no hidden configuration
- Durability: Secure defaults that remain correct over time and key rotation
- Purity: No framework coupling, no database assumptions, no transport opinions
Just as tungsten is the material engineers trust in the most demanding environments, @periodic/tungsten is the authentication layer you trust when the cost of failure is highest.
🎯 Why Choose Tungsten?
Authentication is one of the most common sources of critical security vulnerabilities — and most implementations get the subtle parts wrong:
- DIY JWT libraries miss
issuerandaudiencevalidation, opening the door to token confusion attacks - bcrypt is showing its age — Argon2id is the current OWASP recommendation and tungsten uses it by default
- API key generation with
Math.random()or weak entropy is endemic in backend codebases - No refresh token rotation means stolen tokens are valid forever
- Timing-unsafe comparisons in API key and HMAC verification leak information to attackers
- No key rotation support means rotating credentials requires a deployment instead of a config change
Periodic Tungsten provides the perfect solution:
✅ Zero framework dependencies — works with Express, Fastify, Koa, or no framework at all
✅ JWT Access & Refresh Tokens — HS256 and RS256 with key rotation built in
✅ Argon2id Password Hashing — OWASP-recommended defaults, constant-time verification
✅ API Key Generation — cryptographically secure with prefix support
✅ Opaque Tokens — session identifier generation
✅ TOTP (RFC 6238) — time-based one-time passwords for 2FA
✅ HMAC Request Signing — webhook verification with replay protection
✅ Cookie Utilities — secure configuration helpers
✅ Multi-Tenant Key Abstraction — enterprise key management
✅ Key Rotation — add, retire, and switch signing keys without downtime
✅ Type-safe — strict TypeScript throughout, zero any
✅ Tree-shakeable — ESM + CJS, import only what you use
✅ No global state — no side effects on import
✅ Production-ready — timing-safe, entropy-safe, no secret leakage
📦 Installation
npm install @periodic/tungstenOr with yarn:
yarn add @periodic/tungsten🚀 Quick Start
import {
signAccessToken,
verifyAccessToken,
hashPassword,
verifyPassword,
generateApiKey,
SimpleKeyProvider,
} from '@periodic/tungsten';
// JWT
const keyProvider = new SimpleKeyProvider('your-secret-key-min-32-chars', 'HS256');
const token = await signAccessToken({ sub: 'user_123', role: 'admin' }, {
expiresIn: '15m',
issuer: 'api.example.com',
audience: 'dashboard',
keyProvider,
});
const payload = await verifyAccessToken(token, { keyProvider, issuer: 'api.example.com', audience: 'dashboard' });
// Password
const hash = await hashPassword('MySecurePassword123!');
const isValid = await verifyPassword('MySecurePassword123!', hash);
// API Key
const apiKey = generateApiKey({ prefix: 'sk_live_' }); // sk_live_xYz123...Example token payload:
{
"sub": "user_123",
"role": "admin",
"iss": "api.example.com",
"aud": "dashboard",
"iat": 1708000000,
"exp": 1708000900,
"jti": "01HQ4K2N..."
}🧠 Core Concepts
Key Providers
- Key providers are the central abstraction — they decouple signing keys from the functions that use them
SimpleKeyProviderfor single-tenant, single-key setupsRotatingKeyProviderfor production systems that need to retire old keys without downtime- Pass the provider at call time — no global state, safe for multi-tenant apps
// Single key
const provider = new SimpleKeyProvider('your-secret-key', 'HS256');
// Key rotation — sign with new key, verify with old and new
const provider = new RotatingKeyProvider({
kid: process.env.CURRENT_KEY_ID,
secret: process.env.CURRENT_KEY_SECRET,
algorithm: 'HS256',
});
provider.addKey(process.env.OLD_KEY_ID, process.env.OLD_KEY_SECRET, 'HS256');Security Model
Design principle:
Every function is explicit. Nothing reads from
process.env, nothing has global configuration, nothing silently falls back to insecure defaults. If a parameter is required for security, it is required by the type system.
- All comparisons are timing-safe — no early exits that leak information
- All generation uses
crypto.randomBytes— noMath.random() - All hashing uses Argon2id with OWASP-recommended parameters
- All JWT verification validates
issuerandaudiencewhen provided
✨ Features
🔑 JWT Access & Refresh Tokens
Sign and verify tokens with HS256 or RS256, with key rotation and refresh token replay detection:
import { signAccessToken, verifyAccessToken, rotateRefreshToken } from '@periodic/tungsten';
// Sign
const token = await signAccessToken({ sub: 'user_123' }, {
expiresIn: '15m',
issuer: 'api.example.com',
audience: 'dashboard',
keyProvider,
});
// Verify
const payload = await verifyAccessToken(token, {
keyProvider,
issuer: 'api.example.com',
audience: 'dashboard',
clockTolerance: 60, // seconds
});
// Rotate refresh token with replay detection
const result = await rotateRefreshToken(oldToken, {
keyProvider,
onTokenReused: async (jti) => {
logger.warn('Token reuse detected — revoking all sessions', { jti });
await revokeAllUserSessions(jti);
},
});🔒 Password Hashing
Argon2id with OWASP-recommended defaults and constant-time verification:
import { hashPassword, verifyPassword } from '@periodic/tungsten';
const hash = await hashPassword('MySecurePassword123!');
// Defaults: 64MB memory, 3 iterations, parallelism 4
const isValid = await verifyPassword('MySecurePassword123!', hash);
// Constant-time — safe against timing attacks🗝️ API Key Generation
Cryptographically secure generation with timing-safe verification:
import { generateApiKey, hashApiKey, verifyApiKey } from '@periodic/tungsten';
const apiKey = generateApiKey({ prefix: 'sk_live_', length: 32 });
// sk_live_xYz123... (crypto.randomBytes, min 16-byte entropy enforced)
const hash = hashApiKey(apiKey); // SHA-256, store this
const isValid = verifyApiKey(apiKey, hash); // timing-safe comparison🔐 TOTP (Two-Factor Authentication)
RFC 6238 compliant, compatible with Google Authenticator and Authy:
import { generateTOTPSecret, generateTOTP, verifyTOTP } from '@periodic/tungsten';
const secret = generateTOTPSecret(); // show QR code to user
const code = generateTOTP(secret, { period: 30, digits: 6 });
const result = verifyTOTP(userProvidedCode, secret, { window: 1 });
if (result.valid) {
console.log('2FA verified');
}✍️ HMAC Request Signing
Webhook payload signing with replay protection:
import { signPayload, verifySignature } from '@periodic/tungsten';
const signature = signPayload({ event: 'user.created', userId: '123' }, 'webhook-secret');
const isValid = verifySignature(payload, signature, 'webhook-secret');
// Validates timestamp — rejects requests older than 5 minutes by default🍪 Cookie Utilities
Secure cookie configuration helpers:
import { getSecureCookieOptions } from '@periodic/tungsten';
const options = getSecureCookieOptions({
maxAge: 15 * 60, // 15 minutes
sameSite: 'strict',
});
// httpOnly: true, secure: true, sameSite: 'strict' — safe defaults🏢 Key Rotation
Add, retire, and switch signing keys without downtime:
import { RotatingKeyProvider } from '@periodic/tungsten';
const provider = new RotatingKeyProvider({
kid: 'key-2025-01',
secret: process.env.KEY_2025_01,
algorithm: 'HS256',
});
// Add new key — start signing with it
provider.addKey('key-2025-02', process.env.KEY_2025_02, 'HS256');
provider.setCurrentKey('key-2025-02');
// Old key stays registered for verifying tokens still in circulation
// Remove it once all old tokens have expired📚 Common Patterns
1. Authentication Middleware
import { verifyAccessToken, SimpleKeyProvider } from '@periodic/tungsten';
const keyProvider = new SimpleKeyProvider(process.env.JWT_SECRET, 'HS256');
export async function authMiddleware(req, res, next) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) return res.status(401).json({ error: 'Missing token' });
try {
const payload = await verifyAccessToken(token, {
keyProvider,
issuer: process.env.JWT_ISSUER,
audience: process.env.JWT_AUDIENCE,
});
req.user = payload;
next();
} catch {
res.status(401).json({ error: 'Invalid token' });
}
}2. Registration and Login
import { hashPassword, verifyPassword, signAccessToken } from '@periodic/tungsten';
async function register(email: string, password: string) {
const hash = await hashPassword(password);
await db.users.create({ email, passwordHash: hash });
}
async function login(email: string, password: string) {
const user = await db.users.findOne({ email });
const isValid = await verifyPassword(password, user.passwordHash);
if (!isValid) throw new Error('Invalid credentials');
return signAccessToken({ sub: user.id, role: user.role }, {
expiresIn: '15m',
keyProvider,
});
}3. Refresh Token Rotation
import { rotateRefreshToken } from '@periodic/tungsten';
app.post('/auth/refresh', async (req, res) => {
try {
const result = await rotateRefreshToken(req.cookies.refresh_token, {
keyProvider,
onTokenReused: async (jti) => {
logger.warn('Refresh token reuse — possible theft', { jti });
await db.sessions.revokeAll({ jti });
},
});
res.cookie('refresh_token', result.newToken, getSecureCookieOptions({ maxAge: 7 * 24 * 60 * 60 }));
res.json({ accessToken: result.accessToken });
} catch {
res.status(401).json({ error: 'Invalid refresh token' });
}
});4. API Key Issuance and Verification
import { generateApiKey, hashApiKey, verifyApiKey } from '@periodic/tungsten';
// Issuance — show the plain key once, store only the hash
async function issueApiKey(userId: string) {
const apiKey = generateApiKey({ prefix: 'sk_live_' });
const hash = hashApiKey(apiKey);
await db.apiKeys.create({ userId, hash, createdAt: new Date() });
return apiKey; // return to user once — never stored
}
// Verification
async function verifyApiKeyRequest(providedKey: string) {
const keys = await db.apiKeys.findAll();
return keys.find(k => verifyApiKey(providedKey, k.hash));
}5. TOTP Enrollment and Verification
import { generateTOTPSecret, verifyTOTP } from '@periodic/tungsten';
import QRCode from 'qrcode';
async function enrollTOTP(userId: string) {
const secret = generateTOTPSecret();
await db.users.update({ userId }, { totpSecret: secret, totpEnabled: false });
const otpAuthUrl = `otpauth://totp/MyApp:${userId}?secret=${secret}&issuer=MyApp`;
const qrCode = await QRCode.toDataURL(otpAuthUrl);
return { secret, qrCode };
}
async function verifyAndActivateTOTP(userId: string, code: string) {
const user = await db.users.findOne({ userId });
const result = verifyTOTP(code, user.totpSecret);
if (!result.valid) throw new Error('Invalid TOTP code');
await db.users.update({ userId }, { totpEnabled: true });
}6. Webhook Signing and Verification
import { signPayload, verifySignature } from '@periodic/tungsten';
// Signing outbound webhooks
async function sendWebhook(url: string, event: object) {
const signature = signPayload(event, process.env.WEBHOOK_SECRET);
await fetch(url, {
method: 'POST',
headers: { 'X-Signature': signature, 'Content-Type': 'application/json' },
body: JSON.stringify(event),
});
}
// Verifying inbound webhooks
app.post('/webhooks/stripe', (req, res) => {
const isValid = verifySignature(req.body, req.headers['x-signature'], process.env.WEBHOOK_SECRET);
if (!isValid) return res.status(401).json({ error: 'Invalid signature' });
// process event
res.sendStatus(200);
});7. Structured Logging Integration
import { createLogger, ConsoleTransport, JsonFormatter } from '@periodic/iridium';
import { rotateRefreshToken } from '@periodic/tungsten';
const logger = createLogger({
transports: [new ConsoleTransport({ formatter: new JsonFormatter() })],
});
await rotateRefreshToken(oldToken, {
keyProvider,
onTokenReused: async (jti) => {
logger.warn('tungsten.token_reuse', { jti, severity: 'high' });
await revokeAllUserSessions(jti);
},
});8. Production Configuration
import {
RotatingKeyProvider,
signAccessToken,
verifyAccessToken,
hashPassword,
verifyPassword,
} from '@periodic/tungsten';
const keyProvider = new RotatingKeyProvider({
kid: process.env.JWT_KEY_ID,
secret: process.env.JWT_KEY_SECRET,
algorithm: 'HS256',
});
// Register previous key for tokens still in circulation
if (process.env.JWT_PREV_KEY_ID) {
keyProvider.addKey(process.env.JWT_PREV_KEY_ID, process.env.JWT_PREV_KEY_SECRET, 'HS256');
}
export const auth = {
sign: (payload: object) =>
signAccessToken(payload, {
expiresIn: '15m',
issuer: process.env.JWT_ISSUER,
audience: process.env.JWT_AUDIENCE,
keyProvider,
}),
verify: (token: string) =>
verifyAccessToken(token, {
keyProvider,
issuer: process.env.JWT_ISSUER,
audience: process.env.JWT_AUDIENCE,
clockTolerance: 60,
}),
hashPassword,
verifyPassword,
};
export default auth;🎛️ Configuration Options
signAccessToken Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| keyProvider | KeyProvider | required | Key provider instance |
| expiresIn | string \| number | required | Expiration (e.g. '15m', '1h') |
| issuer | string | — | Token issuer (iss claim) |
| audience | string \| string[] | — | Token audience (aud claim) |
| kid | string | — | Key ID override for rotation |
verifyAccessToken Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| keyProvider | KeyProvider | required | Key provider instance |
| issuer | string | — | Expected issuer (validated if provided) |
| audience | string \| string[] | — | Expected audience (validated if provided) |
| clockTolerance | number | 60 | Clock skew tolerance in seconds |
generateApiKey Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| prefix | string | — | Key prefix (e.g. 'sk_live_') |
| length | number | 32 | Key entropy in bytes (min: 16) |
generateTOTP / verifyTOTP Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| period | number | 30 | Time step in seconds |
| digits | number | 6 | Code length |
| algorithm | 'SHA1' \| 'SHA256' \| 'SHA512' | 'SHA1' | Hash algorithm |
| window | number | 1 | Verification tolerance window |
RotatingKeyProvider
| Method | Description |
|--------|-------------|
| addKey(kid, secret, algorithm) | Register an additional key for verification |
| setCurrentKey(kid) | Switch signing to a different registered key |
📋 API Reference
JWT
signAccessToken(payload: JWTPayload, options: SignAccessTokenOptions): Promise<string>
verifyAccessToken(token: string, options: VerifyAccessTokenOptions): Promise<JWTPayload>
rotateRefreshToken(oldToken: string, options: RotateRefreshTokenOptions): Promise<RefreshTokenRotationResult>Password
hashPassword(password: string): Promise<string>
verifyPassword(password: string, hash: string): Promise<boolean>API Keys
generateApiKey(options?: GenerateApiKeyOptions): string
hashApiKey(apiKey: string): string
verifyApiKey(apiKey: string, hash: string): booleanTOTP
generateTOTPSecret(): string
generateTOTP(secret: string, options?: TOTPOptions): string
verifyTOTP(code: string, secret: string, options?: TOTPOptions): TOTPVerificationResultHMAC
signPayload(payload: unknown, secret: string): string
verifySignature(payload: unknown, signature: string, secret: string): booleanKey Providers
new SimpleKeyProvider(secret: string, algorithm: Algorithm, kid?: string): KeyProvider
new RotatingKeyProvider(primaryKey: KeyConfig): KeyProviderTypes
import type {
JWTPayload,
KeyProvider,
SignAccessTokenOptions,
VerifyAccessTokenOptions,
GenerateApiKeyOptions,
TOTPOptions,
TOTPVerificationResult,
RefreshTokenRotationResult,
} from '@periodic/tungsten';🧩 Architecture
@periodic/tungsten/
├── src/
│ ├── jwt/
│ │ ├── sign.ts # signAccessToken()
│ │ ├── verify.ts # verifyAccessToken()
│ │ └── refresh.ts # rotateRefreshToken() + replay detection
│ ├── password/
│ │ └── index.ts # hashPassword(), verifyPassword() — Argon2id
│ ├── apikey/
│ │ └── index.ts # generateApiKey(), hashApiKey(), verifyApiKey()
│ ├── totp/
│ │ └── index.ts # generateTOTPSecret(), generateTOTP(), verifyTOTP()
│ ├── hmac/
│ │ └── index.ts # signPayload(), verifySignature()
│ ├── cookies/
│ │ └── index.ts # getSecureCookieOptions()
│ ├── keys/
│ │ ├── simple.ts # SimpleKeyProvider
│ │ └── rotating.ts # RotatingKeyProvider
│ ├── types.ts # All shared TypeScript interfaces
│ └── index.ts # Public APIDesign Philosophy:
- Primitives only — no framework coupling, no database assumptions, no transport opinions
- Explicit over implicit — every security parameter is required or has a safe default
- No global state — all configuration is passed at call time
- Timing-safe throughout — no early exits in comparisons that could leak information
- Key providers decouple signing keys from the functions that use them — swap without changing call sites
📈 Performance
- Argon2id is intentionally slow for password hashing — that's the security property
- JWT signing and verification are async and non-blocking
- API key generation uses
crypto.randomBytes— synchronous but fast - TOTP verification is synchronous and sub-millisecond
- No global state — multiple instances in the same process are fully isolated
- Tree-shakeable — only the primitives you use end up in your bundle
🚫 Explicit Non-Goals
This package intentionally does not include:
❌ Session management or storage (bring your own database)
❌ OAuth / OpenID Connect flows (use a dedicated OAuth library)
❌ Framework middleware (adapt the primitives yourself)
❌ User model or database schema (no opinions on your data layer)
❌ Rate limiting (use @periodic/titanium for that)
❌ HTTP transport (no req/res coupling)
❌ Magic or implicit behavior on import
❌ Configuration files (configure in code)
Focus on doing one thing well: cryptographically sound, framework-agnostic authentication primitives.
🎨 TypeScript Support
Full TypeScript support with complete type safety:
import type {
JWTPayload,
KeyProvider,
TOTPVerificationResult,
RefreshTokenRotationResult,
} from '@periodic/tungsten';
// Generic payload — type flows through automatically
const payload = await verifyAccessToken<{ sub: string; role: 'admin' | 'user' }>(token, options);
payload.role; // typed as 'admin' | 'user'
// TOTPVerificationResult is discriminated
const result = verifyTOTP(code, secret);
if (result.valid) {
result.delta; // number — present only when valid
}🧪 Testing
# Run tests
npm test
# Run tests with coverage
npm run test:coverage
# Run tests in watch mode
npm run test:watchNote: All tests achieve >80% code coverage.
🤝 Related Packages
Part of the Periodic series by Uday Thakur:
- @periodic/iridium - Structured logging
- @periodic/arsenic - Semantic runtime monitoring
- @periodic/zirconium - Environment configuration
- @periodic/vanadium - Idempotency and distributed locks
- @periodic/strontium - Resilient HTTP client
- @periodic/obsidian - HTTP error handling
- @periodic/titanium - Rate limiting
- @periodic/osmium - Redis caching
Build complete, production-ready APIs with the Periodic series!
📖 Documentation
🛠️ Production Recommendations
Key Management
Store signing keys in environment variables or a secrets manager — never hardcode them:
JWT_KEY_ID=key-2025-01
JWT_KEY_SECRET=your-256-bit-secret-here
JWT_PREV_KEY_ID=key-2024-12 # keep until old tokens expire
JWT_PREV_KEY_SECRET=previous-secret
JWT_ISSUER=api.example.com
JWT_AUDIENCE=dashboardLog Aggregation
Pair with @periodic/iridium for structured JSON output:
import { createLogger, ConsoleTransport, JsonFormatter } from '@periodic/iridium';
const logger = createLogger({
transports: [new ConsoleTransport({ formatter: new JsonFormatter() })],
});
await rotateRefreshToken(oldToken, {
keyProvider,
onTokenReused: async (jti) => {
logger.warn('tungsten.token_reuse', { jti, severity: 'critical' });
await revokeAllUserSessions(jti);
},
});
// Pipe to Elasticsearch, Datadog, CloudWatch, etc.Security Monitoring
Capture authentication anomalies in your error tracker:
app.use(async (req, res, next) => {
try {
req.user = await verifyAccessToken(token, { keyProvider, issuer, audience });
next();
} catch (err) {
Sentry.captureException(err, { extra: { url: req.url } });
res.status(401).json({ error: 'Unauthorized' });
}
});📝 License
MIT © Uday Thakur
🙏 Contributing
Contributions are welcome! Please read CONTRIBUTING.md for details on:
- Code of conduct
- Development setup
- Pull request process
- Coding standards
- Architecture principles
📞 Support
- 📧 Email: [email protected]
- 🐛 Issues: GitHub Issues
- 💬 Discussions: GitHub Discussions
🌟 Show Your Support
Give a ⭐️ if this project helped you build better applications!
Built with ❤️ by Uday Thakur for production-grade Node.js applications
