@periodic/tungsten 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,814 @@
1
+ # ๐Ÿ”ฉ Periodic Tungsten
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@periodic/tungsten.svg)](https://www.npmjs.com/package/@periodic/tungsten)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.3-blue)](https://www.typescriptlang.org/)
6
+
7
+ **Production-grade, security-auditable authentication primitives for Node.js with TypeScript support**
8
+
9
+ Part of the **Periodic** series of Node.js packages by Uday Thakur.
10
+
11
+ ---
12
+
13
+ ## ๐Ÿ’ก Why Tungsten?
14
+
15
+ **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**.
16
+
17
+ 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.
18
+
19
+ The name represents:
20
+ - **Hardness**: Cryptographic primitives that don't bend under attack
21
+ - **Precision**: Every operation is explicit โ€” no magic, no hidden configuration
22
+ - **Durability**: Secure defaults that remain correct over time and key rotation
23
+ - **Purity**: No framework coupling, no database assumptions, no transport opinions
24
+
25
+ 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.
26
+
27
+ ---
28
+
29
+ ## ๐ŸŽฏ Why Choose Tungsten?
30
+
31
+ Authentication is one of the most common sources of critical security vulnerabilities โ€” and most implementations get the subtle parts wrong:
32
+
33
+ - **DIY JWT libraries** miss `issuer` and `audience` validation, opening the door to token confusion attacks
34
+ - **bcrypt** is showing its age โ€” Argon2id is the current OWASP recommendation and tungsten uses it by default
35
+ - **API key generation** with `Math.random()` or weak entropy is endemic in backend codebases
36
+ - **No refresh token rotation** means stolen tokens are valid forever
37
+ - **Timing-unsafe comparisons** in API key and HMAC verification leak information to attackers
38
+ - **No key rotation support** means rotating credentials requires a deployment instead of a config change
39
+
40
+ **Periodic Tungsten** provides the perfect solution:
41
+
42
+ โœ… **Zero framework dependencies** โ€” works with Express, Fastify, Koa, or no framework at all
43
+ โœ… **JWT Access & Refresh Tokens** โ€” HS256 and RS256 with key rotation built in
44
+ โœ… **Argon2id Password Hashing** โ€” OWASP-recommended defaults, constant-time verification
45
+ โœ… **API Key Generation** โ€” cryptographically secure with prefix support
46
+ โœ… **Opaque Tokens** โ€” session identifier generation
47
+ โœ… **TOTP (RFC 6238)** โ€” time-based one-time passwords for 2FA
48
+ โœ… **HMAC Request Signing** โ€” webhook verification with replay protection
49
+ โœ… **Cookie Utilities** โ€” secure configuration helpers
50
+ โœ… **Multi-Tenant Key Abstraction** โ€” enterprise key management
51
+ โœ… **Key Rotation** โ€” add, retire, and switch signing keys without downtime
52
+ โœ… **Type-safe** โ€” strict TypeScript throughout, zero `any`
53
+ โœ… **Tree-shakeable** โ€” ESM + CJS, import only what you use
54
+ โœ… **No global state** โ€” no side effects on import
55
+ โœ… **Production-ready** โ€” timing-safe, entropy-safe, no secret leakage
56
+
57
+ ---
58
+
59
+ ## ๐Ÿ“ฆ Installation
60
+
61
+ ```bash
62
+ npm install @periodic/tungsten
63
+ ```
64
+
65
+ Or with yarn:
66
+
67
+ ```bash
68
+ yarn add @periodic/tungsten
69
+ ```
70
+
71
+ ---
72
+
73
+ ## ๐Ÿš€ Quick Start
74
+
75
+ ```typescript
76
+ import {
77
+ signAccessToken,
78
+ verifyAccessToken,
79
+ hashPassword,
80
+ verifyPassword,
81
+ generateApiKey,
82
+ SimpleKeyProvider,
83
+ } from '@periodic/tungsten';
84
+
85
+ // JWT
86
+ const keyProvider = new SimpleKeyProvider('your-secret-key-min-32-chars', 'HS256');
87
+ const token = await signAccessToken({ sub: 'user_123', role: 'admin' }, {
88
+ expiresIn: '15m',
89
+ issuer: 'api.example.com',
90
+ audience: 'dashboard',
91
+ keyProvider,
92
+ });
93
+ const payload = await verifyAccessToken(token, { keyProvider, issuer: 'api.example.com', audience: 'dashboard' });
94
+
95
+ // Password
96
+ const hash = await hashPassword('MySecurePassword123!');
97
+ const isValid = await verifyPassword('MySecurePassword123!', hash);
98
+
99
+ // API Key
100
+ const apiKey = generateApiKey({ prefix: 'sk_live_' }); // sk_live_xYz123...
101
+ ```
102
+
103
+ **Example token payload:**
104
+
105
+ ```json
106
+ {
107
+ "sub": "user_123",
108
+ "role": "admin",
109
+ "iss": "api.example.com",
110
+ "aud": "dashboard",
111
+ "iat": 1708000000,
112
+ "exp": 1708000900,
113
+ "jti": "01HQ4K2N..."
114
+ }
115
+ ```
116
+
117
+ ---
118
+
119
+ ## ๐Ÿง  Core Concepts
120
+
121
+ ### Key Providers
122
+
123
+ - **Key providers are the central abstraction** โ€” they decouple signing keys from the functions that use them
124
+ - `SimpleKeyProvider` for single-tenant, single-key setups
125
+ - `RotatingKeyProvider` for production systems that need to retire old keys without downtime
126
+ - **Pass the provider at call time** โ€” no global state, safe for multi-tenant apps
127
+
128
+ ```typescript
129
+ // Single key
130
+ const provider = new SimpleKeyProvider('your-secret-key', 'HS256');
131
+
132
+ // Key rotation โ€” sign with new key, verify with old and new
133
+ const provider = new RotatingKeyProvider({
134
+ kid: process.env.CURRENT_KEY_ID,
135
+ secret: process.env.CURRENT_KEY_SECRET,
136
+ algorithm: 'HS256',
137
+ });
138
+ provider.addKey(process.env.OLD_KEY_ID, process.env.OLD_KEY_SECRET, 'HS256');
139
+ ```
140
+
141
+ ### Security Model
142
+
143
+ **Design principle:**
144
+ > 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.
145
+
146
+ - All comparisons are timing-safe โ€” no early exits that leak information
147
+ - All generation uses `crypto.randomBytes` โ€” no `Math.random()`
148
+ - All hashing uses Argon2id with OWASP-recommended parameters
149
+ - All JWT verification validates `issuer` and `audience` when provided
150
+
151
+ ---
152
+
153
+ ## โœจ Features
154
+
155
+ ### ๐Ÿ”‘ JWT Access & Refresh Tokens
156
+
157
+ Sign and verify tokens with HS256 or RS256, with key rotation and refresh token replay detection:
158
+
159
+ ```typescript
160
+ import { signAccessToken, verifyAccessToken, rotateRefreshToken } from '@periodic/tungsten';
161
+
162
+ // Sign
163
+ const token = await signAccessToken({ sub: 'user_123' }, {
164
+ expiresIn: '15m',
165
+ issuer: 'api.example.com',
166
+ audience: 'dashboard',
167
+ keyProvider,
168
+ });
169
+
170
+ // Verify
171
+ const payload = await verifyAccessToken(token, {
172
+ keyProvider,
173
+ issuer: 'api.example.com',
174
+ audience: 'dashboard',
175
+ clockTolerance: 60, // seconds
176
+ });
177
+
178
+ // Rotate refresh token with replay detection
179
+ const result = await rotateRefreshToken(oldToken, {
180
+ keyProvider,
181
+ onTokenReused: async (jti) => {
182
+ logger.warn('Token reuse detected โ€” revoking all sessions', { jti });
183
+ await revokeAllUserSessions(jti);
184
+ },
185
+ });
186
+ ```
187
+
188
+ ### ๐Ÿ”’ Password Hashing
189
+
190
+ Argon2id with OWASP-recommended defaults and constant-time verification:
191
+
192
+ ```typescript
193
+ import { hashPassword, verifyPassword } from '@periodic/tungsten';
194
+
195
+ const hash = await hashPassword('MySecurePassword123!');
196
+ // Defaults: 64MB memory, 3 iterations, parallelism 4
197
+
198
+ const isValid = await verifyPassword('MySecurePassword123!', hash);
199
+ // Constant-time โ€” safe against timing attacks
200
+ ```
201
+
202
+ ### ๐Ÿ—๏ธ API Key Generation
203
+
204
+ Cryptographically secure generation with timing-safe verification:
205
+
206
+ ```typescript
207
+ import { generateApiKey, hashApiKey, verifyApiKey } from '@periodic/tungsten';
208
+
209
+ const apiKey = generateApiKey({ prefix: 'sk_live_', length: 32 });
210
+ // sk_live_xYz123... (crypto.randomBytes, min 16-byte entropy enforced)
211
+
212
+ const hash = hashApiKey(apiKey); // SHA-256, store this
213
+ const isValid = verifyApiKey(apiKey, hash); // timing-safe comparison
214
+ ```
215
+
216
+ ### ๐Ÿ” TOTP (Two-Factor Authentication)
217
+
218
+ RFC 6238 compliant, compatible with Google Authenticator and Authy:
219
+
220
+ ```typescript
221
+ import { generateTOTPSecret, generateTOTP, verifyTOTP } from '@periodic/tungsten';
222
+
223
+ const secret = generateTOTPSecret(); // show QR code to user
224
+ const code = generateTOTP(secret, { period: 30, digits: 6 });
225
+
226
+ const result = verifyTOTP(userProvidedCode, secret, { window: 1 });
227
+ if (result.valid) {
228
+ console.log('2FA verified');
229
+ }
230
+ ```
231
+
232
+ ### โœ๏ธ HMAC Request Signing
233
+
234
+ Webhook payload signing with replay protection:
235
+
236
+ ```typescript
237
+ import { signPayload, verifySignature } from '@periodic/tungsten';
238
+
239
+ const signature = signPayload({ event: 'user.created', userId: '123' }, 'webhook-secret');
240
+ const isValid = verifySignature(payload, signature, 'webhook-secret');
241
+ // Validates timestamp โ€” rejects requests older than 5 minutes by default
242
+ ```
243
+
244
+ ### ๐Ÿช Cookie Utilities
245
+
246
+ Secure cookie configuration helpers:
247
+
248
+ ```typescript
249
+ import { getSecureCookieOptions } from '@periodic/tungsten';
250
+
251
+ const options = getSecureCookieOptions({
252
+ maxAge: 15 * 60, // 15 minutes
253
+ sameSite: 'strict',
254
+ });
255
+ // httpOnly: true, secure: true, sameSite: 'strict' โ€” safe defaults
256
+ ```
257
+
258
+ ### ๐Ÿข Key Rotation
259
+
260
+ Add, retire, and switch signing keys without downtime:
261
+
262
+ ```typescript
263
+ import { RotatingKeyProvider } from '@periodic/tungsten';
264
+
265
+ const provider = new RotatingKeyProvider({
266
+ kid: 'key-2025-01',
267
+ secret: process.env.KEY_2025_01,
268
+ algorithm: 'HS256',
269
+ });
270
+
271
+ // Add new key โ€” start signing with it
272
+ provider.addKey('key-2025-02', process.env.KEY_2025_02, 'HS256');
273
+ provider.setCurrentKey('key-2025-02');
274
+
275
+ // Old key stays registered for verifying tokens still in circulation
276
+ // Remove it once all old tokens have expired
277
+ ```
278
+
279
+ ---
280
+
281
+ ## ๐Ÿ“š Common Patterns
282
+
283
+ ### 1. Authentication Middleware
284
+
285
+ ```typescript
286
+ import { verifyAccessToken, SimpleKeyProvider } from '@periodic/tungsten';
287
+
288
+ const keyProvider = new SimpleKeyProvider(process.env.JWT_SECRET, 'HS256');
289
+
290
+ export async function authMiddleware(req, res, next) {
291
+ const token = req.headers.authorization?.replace('Bearer ', '');
292
+ if (!token) return res.status(401).json({ error: 'Missing token' });
293
+
294
+ try {
295
+ const payload = await verifyAccessToken(token, {
296
+ keyProvider,
297
+ issuer: process.env.JWT_ISSUER,
298
+ audience: process.env.JWT_AUDIENCE,
299
+ });
300
+ req.user = payload;
301
+ next();
302
+ } catch {
303
+ res.status(401).json({ error: 'Invalid token' });
304
+ }
305
+ }
306
+ ```
307
+
308
+ ### 2. Registration and Login
309
+
310
+ ```typescript
311
+ import { hashPassword, verifyPassword, signAccessToken } from '@periodic/tungsten';
312
+
313
+ async function register(email: string, password: string) {
314
+ const hash = await hashPassword(password);
315
+ await db.users.create({ email, passwordHash: hash });
316
+ }
317
+
318
+ async function login(email: string, password: string) {
319
+ const user = await db.users.findOne({ email });
320
+ const isValid = await verifyPassword(password, user.passwordHash);
321
+ if (!isValid) throw new Error('Invalid credentials');
322
+
323
+ return signAccessToken({ sub: user.id, role: user.role }, {
324
+ expiresIn: '15m',
325
+ keyProvider,
326
+ });
327
+ }
328
+ ```
329
+
330
+ ### 3. Refresh Token Rotation
331
+
332
+ ```typescript
333
+ import { rotateRefreshToken } from '@periodic/tungsten';
334
+
335
+ app.post('/auth/refresh', async (req, res) => {
336
+ try {
337
+ const result = await rotateRefreshToken(req.cookies.refresh_token, {
338
+ keyProvider,
339
+ onTokenReused: async (jti) => {
340
+ logger.warn('Refresh token reuse โ€” possible theft', { jti });
341
+ await db.sessions.revokeAll({ jti });
342
+ },
343
+ });
344
+
345
+ res.cookie('refresh_token', result.newToken, getSecureCookieOptions({ maxAge: 7 * 24 * 60 * 60 }));
346
+ res.json({ accessToken: result.accessToken });
347
+ } catch {
348
+ res.status(401).json({ error: 'Invalid refresh token' });
349
+ }
350
+ });
351
+ ```
352
+
353
+ ### 4. API Key Issuance and Verification
354
+
355
+ ```typescript
356
+ import { generateApiKey, hashApiKey, verifyApiKey } from '@periodic/tungsten';
357
+
358
+ // Issuance โ€” show the plain key once, store only the hash
359
+ async function issueApiKey(userId: string) {
360
+ const apiKey = generateApiKey({ prefix: 'sk_live_' });
361
+ const hash = hashApiKey(apiKey);
362
+ await db.apiKeys.create({ userId, hash, createdAt: new Date() });
363
+ return apiKey; // return to user once โ€” never stored
364
+ }
365
+
366
+ // Verification
367
+ async function verifyApiKeyRequest(providedKey: string) {
368
+ const keys = await db.apiKeys.findAll();
369
+ return keys.find(k => verifyApiKey(providedKey, k.hash));
370
+ }
371
+ ```
372
+
373
+ ### 5. TOTP Enrollment and Verification
374
+
375
+ ```typescript
376
+ import { generateTOTPSecret, verifyTOTP } from '@periodic/tungsten';
377
+ import QRCode from 'qrcode';
378
+
379
+ async function enrollTOTP(userId: string) {
380
+ const secret = generateTOTPSecret();
381
+ await db.users.update({ userId }, { totpSecret: secret, totpEnabled: false });
382
+
383
+ const otpAuthUrl = `otpauth://totp/MyApp:${userId}?secret=${secret}&issuer=MyApp`;
384
+ const qrCode = await QRCode.toDataURL(otpAuthUrl);
385
+ return { secret, qrCode };
386
+ }
387
+
388
+ async function verifyAndActivateTOTP(userId: string, code: string) {
389
+ const user = await db.users.findOne({ userId });
390
+ const result = verifyTOTP(code, user.totpSecret);
391
+ if (!result.valid) throw new Error('Invalid TOTP code');
392
+ await db.users.update({ userId }, { totpEnabled: true });
393
+ }
394
+ ```
395
+
396
+ ### 6. Webhook Signing and Verification
397
+
398
+ ```typescript
399
+ import { signPayload, verifySignature } from '@periodic/tungsten';
400
+
401
+ // Signing outbound webhooks
402
+ async function sendWebhook(url: string, event: object) {
403
+ const signature = signPayload(event, process.env.WEBHOOK_SECRET);
404
+ await fetch(url, {
405
+ method: 'POST',
406
+ headers: { 'X-Signature': signature, 'Content-Type': 'application/json' },
407
+ body: JSON.stringify(event),
408
+ });
409
+ }
410
+
411
+ // Verifying inbound webhooks
412
+ app.post('/webhooks/stripe', (req, res) => {
413
+ const isValid = verifySignature(req.body, req.headers['x-signature'], process.env.WEBHOOK_SECRET);
414
+ if (!isValid) return res.status(401).json({ error: 'Invalid signature' });
415
+ // process event
416
+ res.sendStatus(200);
417
+ });
418
+ ```
419
+
420
+ ### 7. Structured Logging Integration
421
+
422
+ ```typescript
423
+ import { createLogger, ConsoleTransport, JsonFormatter } from '@periodic/iridium';
424
+ import { rotateRefreshToken } from '@periodic/tungsten';
425
+
426
+ const logger = createLogger({
427
+ transports: [new ConsoleTransport({ formatter: new JsonFormatter() })],
428
+ });
429
+
430
+ await rotateRefreshToken(oldToken, {
431
+ keyProvider,
432
+ onTokenReused: async (jti) => {
433
+ logger.warn('tungsten.token_reuse', { jti, severity: 'high' });
434
+ await revokeAllUserSessions(jti);
435
+ },
436
+ });
437
+ ```
438
+
439
+ ### 8. Production Configuration
440
+
441
+ ```typescript
442
+ import {
443
+ RotatingKeyProvider,
444
+ signAccessToken,
445
+ verifyAccessToken,
446
+ hashPassword,
447
+ verifyPassword,
448
+ } from '@periodic/tungsten';
449
+
450
+ const keyProvider = new RotatingKeyProvider({
451
+ kid: process.env.JWT_KEY_ID,
452
+ secret: process.env.JWT_KEY_SECRET,
453
+ algorithm: 'HS256',
454
+ });
455
+
456
+ // Register previous key for tokens still in circulation
457
+ if (process.env.JWT_PREV_KEY_ID) {
458
+ keyProvider.addKey(process.env.JWT_PREV_KEY_ID, process.env.JWT_PREV_KEY_SECRET, 'HS256');
459
+ }
460
+
461
+ export const auth = {
462
+ sign: (payload: object) =>
463
+ signAccessToken(payload, {
464
+ expiresIn: '15m',
465
+ issuer: process.env.JWT_ISSUER,
466
+ audience: process.env.JWT_AUDIENCE,
467
+ keyProvider,
468
+ }),
469
+
470
+ verify: (token: string) =>
471
+ verifyAccessToken(token, {
472
+ keyProvider,
473
+ issuer: process.env.JWT_ISSUER,
474
+ audience: process.env.JWT_AUDIENCE,
475
+ clockTolerance: 60,
476
+ }),
477
+
478
+ hashPassword,
479
+ verifyPassword,
480
+ };
481
+
482
+ export default auth;
483
+ ```
484
+
485
+ ---
486
+
487
+ ## ๐ŸŽ›๏ธ Configuration Options
488
+
489
+ ### `signAccessToken` Options
490
+
491
+ | Option | Type | Default | Description |
492
+ |--------|------|---------|-------------|
493
+ | `keyProvider` | `KeyProvider` | required | Key provider instance |
494
+ | `expiresIn` | `string \| number` | required | Expiration (e.g. `'15m'`, `'1h'`) |
495
+ | `issuer` | `string` | โ€” | Token issuer (`iss` claim) |
496
+ | `audience` | `string \| string[]` | โ€” | Token audience (`aud` claim) |
497
+ | `kid` | `string` | โ€” | Key ID override for rotation |
498
+
499
+ ### `verifyAccessToken` Options
500
+
501
+ | Option | Type | Default | Description |
502
+ |--------|------|---------|-------------|
503
+ | `keyProvider` | `KeyProvider` | required | Key provider instance |
504
+ | `issuer` | `string` | โ€” | Expected issuer (validated if provided) |
505
+ | `audience` | `string \| string[]` | โ€” | Expected audience (validated if provided) |
506
+ | `clockTolerance` | `number` | `60` | Clock skew tolerance in seconds |
507
+
508
+ ### `generateApiKey` Options
509
+
510
+ | Option | Type | Default | Description |
511
+ |--------|------|---------|-------------|
512
+ | `prefix` | `string` | โ€” | Key prefix (e.g. `'sk_live_'`) |
513
+ | `length` | `number` | `32` | Key entropy in bytes (min: 16) |
514
+
515
+ ### `generateTOTP` / `verifyTOTP` Options
516
+
517
+ | Option | Type | Default | Description |
518
+ |--------|------|---------|-------------|
519
+ | `period` | `number` | `30` | Time step in seconds |
520
+ | `digits` | `number` | `6` | Code length |
521
+ | `algorithm` | `'SHA1' \| 'SHA256' \| 'SHA512'` | `'SHA1'` | Hash algorithm |
522
+ | `window` | `number` | `1` | Verification tolerance window |
523
+
524
+ ### `RotatingKeyProvider`
525
+
526
+ | Method | Description |
527
+ |--------|-------------|
528
+ | `addKey(kid, secret, algorithm)` | Register an additional key for verification |
529
+ | `setCurrentKey(kid)` | Switch signing to a different registered key |
530
+
531
+ ---
532
+
533
+ ## ๐Ÿ“‹ API Reference
534
+
535
+ ### JWT
536
+
537
+ ```typescript
538
+ signAccessToken(payload: JWTPayload, options: SignAccessTokenOptions): Promise<string>
539
+ verifyAccessToken(token: string, options: VerifyAccessTokenOptions): Promise<JWTPayload>
540
+ rotateRefreshToken(oldToken: string, options: RotateRefreshTokenOptions): Promise<RefreshTokenRotationResult>
541
+ ```
542
+
543
+ ### Password
544
+
545
+ ```typescript
546
+ hashPassword(password: string): Promise<string>
547
+ verifyPassword(password: string, hash: string): Promise<boolean>
548
+ ```
549
+
550
+ ### API Keys
551
+
552
+ ```typescript
553
+ generateApiKey(options?: GenerateApiKeyOptions): string
554
+ hashApiKey(apiKey: string): string
555
+ verifyApiKey(apiKey: string, hash: string): boolean
556
+ ```
557
+
558
+ ### TOTP
559
+
560
+ ```typescript
561
+ generateTOTPSecret(): string
562
+ generateTOTP(secret: string, options?: TOTPOptions): string
563
+ verifyTOTP(code: string, secret: string, options?: TOTPOptions): TOTPVerificationResult
564
+ ```
565
+
566
+ ### HMAC
567
+
568
+ ```typescript
569
+ signPayload(payload: unknown, secret: string): string
570
+ verifySignature(payload: unknown, signature: string, secret: string): boolean
571
+ ```
572
+
573
+ ### Key Providers
574
+
575
+ ```typescript
576
+ new SimpleKeyProvider(secret: string, algorithm: Algorithm, kid?: string): KeyProvider
577
+ new RotatingKeyProvider(primaryKey: KeyConfig): KeyProvider
578
+ ```
579
+
580
+ ### Types
581
+
582
+ ```typescript
583
+ import type {
584
+ JWTPayload,
585
+ KeyProvider,
586
+ SignAccessTokenOptions,
587
+ VerifyAccessTokenOptions,
588
+ GenerateApiKeyOptions,
589
+ TOTPOptions,
590
+ TOTPVerificationResult,
591
+ RefreshTokenRotationResult,
592
+ } from '@periodic/tungsten';
593
+ ```
594
+
595
+ ---
596
+
597
+ ## ๐Ÿงฉ Architecture
598
+
599
+ ```
600
+ @periodic/tungsten/
601
+ โ”œโ”€โ”€ src/
602
+ โ”‚ โ”œโ”€โ”€ jwt/
603
+ โ”‚ โ”‚ โ”œโ”€โ”€ sign.ts # signAccessToken()
604
+ โ”‚ โ”‚ โ”œโ”€โ”€ verify.ts # verifyAccessToken()
605
+ โ”‚ โ”‚ โ””โ”€โ”€ refresh.ts # rotateRefreshToken() + replay detection
606
+ โ”‚ โ”œโ”€โ”€ password/
607
+ โ”‚ โ”‚ โ””โ”€โ”€ index.ts # hashPassword(), verifyPassword() โ€” Argon2id
608
+ โ”‚ โ”œโ”€โ”€ apikey/
609
+ โ”‚ โ”‚ โ””โ”€โ”€ index.ts # generateApiKey(), hashApiKey(), verifyApiKey()
610
+ โ”‚ โ”œโ”€โ”€ totp/
611
+ โ”‚ โ”‚ โ””โ”€โ”€ index.ts # generateTOTPSecret(), generateTOTP(), verifyTOTP()
612
+ โ”‚ โ”œโ”€โ”€ hmac/
613
+ โ”‚ โ”‚ โ””โ”€โ”€ index.ts # signPayload(), verifySignature()
614
+ โ”‚ โ”œโ”€โ”€ cookies/
615
+ โ”‚ โ”‚ โ””โ”€โ”€ index.ts # getSecureCookieOptions()
616
+ โ”‚ โ”œโ”€โ”€ keys/
617
+ โ”‚ โ”‚ โ”œโ”€โ”€ simple.ts # SimpleKeyProvider
618
+ โ”‚ โ”‚ โ””โ”€โ”€ rotating.ts # RotatingKeyProvider
619
+ โ”‚ โ”œโ”€โ”€ types.ts # All shared TypeScript interfaces
620
+ โ”‚ โ””โ”€โ”€ index.ts # Public API
621
+ ```
622
+
623
+ **Design Philosophy:**
624
+ - **Primitives only** โ€” no framework coupling, no database assumptions, no transport opinions
625
+ - **Explicit over implicit** โ€” every security parameter is required or has a safe default
626
+ - **No global state** โ€” all configuration is passed at call time
627
+ - **Timing-safe throughout** โ€” no early exits in comparisons that could leak information
628
+ - **Key providers** decouple signing keys from the functions that use them โ€” swap without changing call sites
629
+
630
+ ---
631
+
632
+ ## ๐Ÿ“ˆ Performance
633
+
634
+ - **Argon2id** is intentionally slow for password hashing โ€” that's the security property
635
+ - **JWT signing and verification** are async and non-blocking
636
+ - **API key generation** uses `crypto.randomBytes` โ€” synchronous but fast
637
+ - **TOTP** verification is synchronous and sub-millisecond
638
+ - **No global state** โ€” multiple instances in the same process are fully isolated
639
+ - **Tree-shakeable** โ€” only the primitives you use end up in your bundle
640
+
641
+ ---
642
+
643
+ ## ๐Ÿšซ Explicit Non-Goals
644
+
645
+ This package **intentionally does not** include:
646
+
647
+ โŒ Session management or storage (bring your own database)
648
+ โŒ OAuth / OpenID Connect flows (use a dedicated OAuth library)
649
+ โŒ Framework middleware (adapt the primitives yourself)
650
+ โŒ User model or database schema (no opinions on your data layer)
651
+ โŒ Rate limiting (use `@periodic/titanium` for that)
652
+ โŒ HTTP transport (no `req`/`res` coupling)
653
+ โŒ Magic or implicit behavior on import
654
+ โŒ Configuration files (configure in code)
655
+
656
+ Focus on doing one thing well: **cryptographically sound, framework-agnostic authentication primitives**.
657
+
658
+ ---
659
+
660
+ ## ๐ŸŽจ TypeScript Support
661
+
662
+ Full TypeScript support with complete type safety:
663
+
664
+ ```typescript
665
+ import type {
666
+ JWTPayload,
667
+ KeyProvider,
668
+ TOTPVerificationResult,
669
+ RefreshTokenRotationResult,
670
+ } from '@periodic/tungsten';
671
+
672
+ // Generic payload โ€” type flows through automatically
673
+ const payload = await verifyAccessToken<{ sub: string; role: 'admin' | 'user' }>(token, options);
674
+ payload.role; // typed as 'admin' | 'user'
675
+
676
+ // TOTPVerificationResult is discriminated
677
+ const result = verifyTOTP(code, secret);
678
+ if (result.valid) {
679
+ result.delta; // number โ€” present only when valid
680
+ }
681
+ ```
682
+
683
+ ---
684
+
685
+ ## ๐Ÿงช Testing
686
+
687
+ ```bash
688
+ # Run tests
689
+ npm test
690
+
691
+ # Run tests with coverage
692
+ npm run test:coverage
693
+
694
+ # Run tests in watch mode
695
+ npm run test:watch
696
+ ```
697
+
698
+ **Note:** All tests achieve >80% code coverage.
699
+
700
+ ---
701
+
702
+ ## ๐Ÿค Related Packages
703
+
704
+ Part of the **Periodic** series by Uday Thakur:
705
+
706
+ - [**@periodic/iridium**](https://www.npmjs.com/package/@periodic/iridium) - Structured logging
707
+ - [**@periodic/arsenic**](https://www.npmjs.com/package/@periodic/arsenic) - Semantic runtime monitoring
708
+ - [**@periodic/zirconium**](https://www.npmjs.com/package/@periodic/zirconium) - Environment configuration
709
+ - [**@periodic/vanadium**](https://www.npmjs.com/package/@periodic/vanadium) - Idempotency and distributed locks
710
+ - [**@periodic/strontium**](https://www.npmjs.com/package/@periodic/strontium) - Resilient HTTP client
711
+ - [**@periodic/obsidian**](https://www.npmjs.com/package/@periodic/obsidian) - HTTP error handling
712
+ - [**@periodic/titanium**](https://www.npmjs.com/package/@periodic/titanium) - Rate limiting
713
+ - [**@periodic/osmium**](https://www.npmjs.com/package/@periodic/osmium) - Redis caching
714
+
715
+ Build complete, production-ready APIs with the Periodic series!
716
+
717
+ ---
718
+
719
+ ## ๐Ÿ“– Documentation
720
+
721
+ - [Quick Start Guide](QUICKSTART.md)
722
+ - [Contributing Guide](CONTRIBUTING.md)
723
+ - [Changelog](CHANGELOG.md)
724
+
725
+ ---
726
+
727
+ ## ๐Ÿ› ๏ธ Production Recommendations
728
+
729
+ ### Key Management
730
+
731
+ Store signing keys in environment variables or a secrets manager โ€” never hardcode them:
732
+
733
+ ```bash
734
+ JWT_KEY_ID=key-2025-01
735
+ JWT_KEY_SECRET=your-256-bit-secret-here
736
+ JWT_PREV_KEY_ID=key-2024-12 # keep until old tokens expire
737
+ JWT_PREV_KEY_SECRET=previous-secret
738
+ JWT_ISSUER=api.example.com
739
+ JWT_AUDIENCE=dashboard
740
+ ```
741
+
742
+ ### Log Aggregation
743
+
744
+ Pair with `@periodic/iridium` for structured JSON output:
745
+
746
+ ```typescript
747
+ import { createLogger, ConsoleTransport, JsonFormatter } from '@periodic/iridium';
748
+
749
+ const logger = createLogger({
750
+ transports: [new ConsoleTransport({ formatter: new JsonFormatter() })],
751
+ });
752
+
753
+ await rotateRefreshToken(oldToken, {
754
+ keyProvider,
755
+ onTokenReused: async (jti) => {
756
+ logger.warn('tungsten.token_reuse', { jti, severity: 'critical' });
757
+ await revokeAllUserSessions(jti);
758
+ },
759
+ });
760
+
761
+ // Pipe to Elasticsearch, Datadog, CloudWatch, etc.
762
+ ```
763
+
764
+ ### Security Monitoring
765
+
766
+ Capture authentication anomalies in your error tracker:
767
+
768
+ ```typescript
769
+ app.use(async (req, res, next) => {
770
+ try {
771
+ req.user = await verifyAccessToken(token, { keyProvider, issuer, audience });
772
+ next();
773
+ } catch (err) {
774
+ Sentry.captureException(err, { extra: { url: req.url } });
775
+ res.status(401).json({ error: 'Unauthorized' });
776
+ }
777
+ });
778
+ ```
779
+
780
+ ---
781
+
782
+ ## ๐Ÿ“ License
783
+
784
+ MIT ยฉ [Uday Thakur](LICENSE)
785
+
786
+ ---
787
+
788
+ ## ๐Ÿ™ Contributing
789
+
790
+ Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on:
791
+
792
+ - Code of conduct
793
+ - Development setup
794
+ - Pull request process
795
+ - Coding standards
796
+ - Architecture principles
797
+
798
+ ---
799
+
800
+ ## ๐Ÿ“ž Support
801
+
802
+ - ๐Ÿ“ง **Email:** udaythakurwork@gmail.com
803
+ - ๐Ÿ› **Issues:** [GitHub Issues](https://github.com/udaythakur7469/periodic-tungsten/issues)
804
+ - ๐Ÿ’ฌ **Discussions:** [GitHub Discussions](https://github.com/udaythakur7469/periodic-tungsten/discussions)
805
+
806
+ ---
807
+
808
+ ## ๐ŸŒŸ Show Your Support
809
+
810
+ Give a โญ๏ธ if this project helped you build better applications!
811
+
812
+ ---
813
+
814
+ **Built with โค๏ธ by Uday Thakur for production-grade Node.js applications**