yinzerflow 0.5.6 → 0.6.7

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.
@@ -0,0 +1,982 @@
1
+ # 📖 Overview
2
+
3
+ YinzerFlow provides a built-in cookie parser that handles parsing incoming cookies, setting outgoing cookies, and optionally signing/validating cookies using HMAC-SHA256. The cookie parser is <span style="color: #95a5a6">**disabled by default**</span> and must be explicitly enabled in the YinzerFlow constructor when needed.
4
+
5
+ <span style="color: #3498db">**💡 Tip:**</span> The cookie parser makes it easy to work with session management and user preferences.
6
+
7
+ **When to use:**
8
+
9
+ - 🍪 Session management (store session IDs)
10
+ - 👤 User authentication tokens (JWT refresh tokens)
11
+ - 🎨 User preferences (theme, language)
12
+ - 📊 Analytics and tracking
13
+
14
+ **Expected outcomes:**
15
+
16
+ - ✅ Automatic parsing of incoming `Cookie` headers into Maps
17
+ - ✅ Helper methods for setting cookies with attributes
18
+ - 🛡️ HMAC signature validation to detect tampering
19
+ - 📝 Type-safe cookie access on request and context objects
20
+
21
+ # ⚙️ Usage
22
+
23
+ ## 🎛️ Settings
24
+
25
+ ### enabled — @default <span style="color: #95a5a6">`false`</span>
26
+
27
+ Enable or disable the cookie parser. Must be explicitly enabled to use cookie parsing.
28
+
29
+ ```typescript
30
+ import { YinzerFlow } from 'yinzerflow';
31
+
32
+ // Enable cookie parser
33
+ const app = new YinzerFlow({
34
+ port: 3000,
35
+ cookieParser: {
36
+ enabled: true,
37
+ secret: process.env.COOKIE_SECRET
38
+ }
39
+ });
40
+
41
+ // Disabled by default (no configuration needed)
42
+ const app = new YinzerFlow({ port: 3000 });
43
+ ```
44
+
45
+ ### secret — @default <span style="color: #95a5a6">`undefined`</span>
46
+
47
+ Secret key for signing cookies using HMAC-SHA256. When provided, cookies can be signed to detect tampering.
48
+
49
+ <span style="color: #e74c3c">**⚠️ Warning:**</span> In production, always use a strong secret (at least 32 characters) stored in environment variables.
50
+
51
+ #### What Makes a Strong Secret?
52
+
53
+ A strong secret should be:
54
+ - **Random**: Generated using cryptographically secure random methods
55
+ - **Long**: Minimum 32 characters (256 bits of entropy)
56
+ - **Unique**: Different for each application/environment
57
+ - **Secret**: Never committed to version control
58
+
59
+ ```typescript
60
+ const app = new YinzerFlow({
61
+ port: 3000,
62
+ cookieParser: {
63
+ secret: process.env.COOKIE_SECRET
64
+ }
65
+ });
66
+ ```
67
+
68
+ #### How to Generate a Strong Secret
69
+
70
+ **Option 1: Using Node.js crypto (recommended)**
71
+ ```bash
72
+ node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
73
+ # Output: 4a8f2c9d1e6b5a3c7f8e2d9b1a4c6e8f2d7b9a3c5e8f1d6b4a2
74
+
75
+ # Use the output in your .env file:
76
+ # COOKIE_SECRET=4a8f2c9d1e6b5a3c7f8e2d9b1a4c6e8f2d7b9a3c5e8f1d6b4a2
77
+ ```
78
+
79
+ **Option 2: Using OpenSSL**
80
+ ```bash
81
+ openssl rand -hex 32
82
+ ```
83
+
84
+ **Option 3: Online generators**
85
+ - Use trusted services like 1Password Secret Key Generator (for reference, not production)
86
+ - Generate locally and store securely
87
+
88
+ #### ❌ What NOT to Use
89
+
90
+ ```typescript
91
+ // ❌ Weak - predictable
92
+ secret: 'my-secret-key'
93
+
94
+ // ❌ Weak - too short
95
+ secret: '12345678'
96
+
97
+ // ❌ Weak - based on company name
98
+ secret: 'acmecorp2024'
99
+
100
+ // ✅ Strong - random and long
101
+ secret: '4a8f2c9d1e6b5a3c7f8e2d9b1a4c6e8f2d7b9a3c5e8f1d6b4a2'
102
+ ```
103
+
104
+ ### signed — @default <span style="color: #95a5a6">`undefined`</span> (sign all)
105
+
106
+ Array of cookie names to sign. If undefined or empty, all cookies are signed when a secret is provided.
107
+
108
+ <span style="color: #3498db">**💡 Tip:**</span> Only sign cookies that need integrity protection (session IDs, user tokens).
109
+
110
+ ```typescript
111
+ // Sign all cookies (default)
112
+ const app = new YinzerFlow({
113
+ port: 3000,
114
+ cookieParser: {
115
+ secret: process.env.COOKIE_SECRET
116
+ }
117
+ });
118
+
119
+ // Sign specific cookies only
120
+ const app = new YinzerFlow({
121
+ port: 3000,
122
+ cookieParser: {
123
+ secret: process.env.COOKIE_SECRET,
124
+ signed: ['sessionId', 'userId']
125
+ }
126
+ });
127
+ ```
128
+
129
+ ### defaults — @default <span style="color: #95a5a6">`undefined`</span>
130
+
131
+ Default cookie options applied to all cookies set via `ctx.cookies.set()`. Can be overridden per-cookie.
132
+
133
+ ```typescript
134
+ const app = new YinzerFlow({
135
+ port: 3000,
136
+ cookieParser: {
137
+ secret: process.env.COOKIE_SECRET,
138
+ defaults: {
139
+ httpOnly: true, // Prevent JavaScript access
140
+ secure: true, // HTTPS only
141
+ sameSite: 'strict', // CSRF protection
142
+ maxAge: '1h' // 1 hour using TimeString (or 3600 as seconds)
143
+ }
144
+ }
145
+ });
146
+ ```
147
+
148
+ <aside>
149
+
150
+ Cookie Options:
151
+
152
+ - **httpOnly**: Prevent JavaScript access (recommended for auth cookies)
153
+ - **secure**: Only send over HTTPS (required in production)
154
+ - **sameSite**: CSRF protection - 'strict' (best), 'lax', or 'none' (requires secure)
155
+ - **maxAge**: Cookie lifetime - accepts `'30s'`, `'15m'`, `'2h'`, `'1d'` or seconds as number
156
+ - **expires**: Expiration date (alternative to maxAge)
157
+ - **domain**: Cookie domain
158
+ - **path**: Cookie path (must start with '/')
159
+
160
+ </aside>
161
+
162
+ # 🔧 Basic Usage
163
+
164
+ ## Parsing Cookies
165
+
166
+ ```typescript
167
+ app.get('/api/user', async (ctx) => {
168
+ // Access unsigned cookies
169
+ const theme = ctx.request.cookies.get('theme');
170
+
171
+ // Access signed cookies (validated)
172
+ const sessionId = ctx.request.signedCookies.get('sessionId');
173
+
174
+ return { theme, sessionId };
175
+ });
176
+ ```
177
+
178
+ ## Setting Cookies
179
+
180
+ ```typescript
181
+ app.post('/api/login', async (ctx) => {
182
+ const user = await validateUser(ctx.request.body);
183
+
184
+ // Set session cookie
185
+ ctx.cookies.set('sessionId', user.sessionId, {
186
+ httpOnly: true,
187
+ secure: true,
188
+ sameSite: 'strict',
189
+ maxAge: '1h' // or 3600 for 1 hour in seconds
190
+ });
191
+
192
+ return { success: true };
193
+ });
194
+ ```
195
+
196
+ ### Setting Multiple Cookies
197
+
198
+ You can set multiple cookies in a single handler. Each call to `ctx.cookies.set()` will add a separate `Set-Cookie` header to the response:
199
+
200
+ ```typescript
201
+ app.post('/api/set-preferences', async (ctx) => {
202
+ // Set multiple cookies - each creates its own Set-Cookie header
203
+ ctx.cookies.set('theme', 'dark', {
204
+ httpOnly: false, // Allow JavaScript access
205
+ maxAge: 31536000 // 1 year
206
+ });
207
+
208
+ ctx.cookies.set('language', 'en', {
209
+ httpOnly: false,
210
+ maxAge: 31536000
211
+ });
212
+
213
+ ctx.cookies.set('sessionId', user.sessionId, {
214
+ httpOnly: true, // Secure session cookie
215
+ secure: true,
216
+ sameSite: 'strict',
217
+ maxAge: 3600
218
+ });
219
+
220
+ return { success: true };
221
+ });
222
+ ```
223
+
224
+ ## Signed Cookies
225
+
226
+ ```typescript
227
+ // Sign and set a cookie
228
+ const signedValue = ctx.cookies.sign('sessionId', 'abc123');
229
+ ctx.cookies.set('sessionId', signedValue, {
230
+ httpOnly: true,
231
+ secure: true
232
+ });
233
+
234
+ // Validate signed cookies
235
+ const sessionId = ctx.request.signedCookies.get('sessionId');
236
+ if (!sessionId) {
237
+ throw new Error('Session cookie missing or tampered');
238
+ }
239
+ ```
240
+
241
+ # 💻 Examples
242
+
243
+ ## JWT Refresh Token Flow
244
+
245
+ **Use Case:** Secure authentication with JWT access tokens and refresh tokens
246
+
247
+ **Description:** Production-ready JWT authentication where short-lived access tokens are used for API calls, and long-lived refresh tokens are stored in httpOnly cookies.
248
+
249
+ ```typescript
250
+ import { YinzerFlow } from 'yinzerflow';
251
+ import jwt from 'jsonwebtoken';
252
+
253
+ const app = new YinzerFlow({
254
+ port: 3000,
255
+ cookieParser: {
256
+ enabled: true,
257
+ secret: process.env.COOKIE_SECRET,
258
+ defaults: {
259
+ httpOnly: true,
260
+ secure: true,
261
+ sameSite: 'strict'
262
+ }
263
+ }
264
+ });
265
+
266
+ // Constants for token lifetime
267
+ const ACCESS_TOKEN_EXPIRY = 15 * 60; // 15 minutes
268
+ const REFRESH_TOKEN_EXPIRY = 7 * 24 * 60 * 60; // 7 days (in seconds)
269
+
270
+ // Login endpoint
271
+ app.post('/api/auth/login', async (ctx) => {
272
+ const { username, password } = ctx.request.body;
273
+
274
+ // Authenticate user
275
+ const user = await validateUser(username, password);
276
+ if (!user) {
277
+ ctx.response.setStatusCode(401);
278
+ return { error: 'Invalid credentials' };
279
+ }
280
+
281
+ // Generate short-lived access token
282
+ const accessToken = jwt.sign(
283
+ { userId: user.id, type: 'access' },
284
+ process.env.JWT_SECRET,
285
+ { expiresIn: `${ACCESS_TOKEN_EXPIRY}s` }
286
+ );
287
+
288
+ // Generate long-lived refresh token
289
+ const refreshToken = jwt.sign(
290
+ { userId: user.id, type: 'refresh' },
291
+ process.env.REFRESH_SECRET,
292
+ { expiresIn: `${REFRESH_TOKEN_EXPIRY}s` }
293
+ );
294
+
295
+ // Store refresh token in cookie with matching maxAge
296
+ ctx.cookies.set('refreshToken', refreshToken, {
297
+ maxAge: REFRESH_TOKEN_EXPIRY // Same as JWT expiration!
298
+ });
299
+
300
+ // Return access token in response body
301
+ return { accessToken };
302
+ });
303
+
304
+ // Token refresh endpoint
305
+ app.post('/api/auth/refresh', async (ctx) => {
306
+ const refreshToken = ctx.request.signedCookies.get('refreshToken');
307
+
308
+ if (!refreshToken) {
309
+ ctx.response.setStatusCode(401);
310
+ return { error: 'Refresh token missing' };
311
+ }
312
+
313
+ try {
314
+ // Verify refresh token
315
+ const decoded = jwt.verify(refreshToken, process.env.REFRESH_SECRET);
316
+
317
+ // Generate new access token
318
+ const accessToken = jwt.sign(
319
+ { userId: decoded.userId, type: 'access' },
320
+ process.env.JWT_SECRET,
321
+ { expiresIn: `${ACCESS_TOKEN_EXPIRY}s` }
322
+ );
323
+
324
+ return { accessToken };
325
+ } catch (error) {
326
+ ctx.response.setStatusCode(401);
327
+ return { error: 'Invalid refresh token' };
328
+ }
329
+ });
330
+
331
+ // Protected endpoint using access token
332
+ app.get('/api/user/profile', async (ctx) => {
333
+ const authHeader = ctx.request.headers.authorization;
334
+ const accessToken = authHeader?.replace('Bearer ', '');
335
+
336
+ if (!accessToken) {
337
+ ctx.response.setStatusCode(401);
338
+ return { error: 'Access token required' };
339
+ }
340
+
341
+ try {
342
+ const decoded = jwt.verify(accessToken, process.env.JWT_SECRET);
343
+ const user = await getUserById(decoded.userId);
344
+ return { user };
345
+ } catch (error) {
346
+ ctx.response.setStatusCode(401);
347
+ return { error: 'Invalid access token' };
348
+ }
349
+ });
350
+
351
+ await app.listen();
352
+ ```
353
+
354
+ ## Session Management
355
+
356
+ **Use Case:** Secure session management with signed cookies
357
+
358
+ ```typescript
359
+ import { YinzerFlow } from 'yinzerflow';
360
+
361
+ const app = new YinzerFlow({
362
+ port: 3000,
363
+ cookieParser: {
364
+ enabled: true,
365
+ secret: process.env.COOKIE_SECRET,
366
+ defaults: {
367
+ httpOnly: true,
368
+ secure: true,
369
+ sameSite: 'strict',
370
+ maxAge: 3600 // 1 hour
371
+ },
372
+ signed: ['sessionId', 'userId']
373
+ }
374
+ });
375
+
376
+ app.post('/api/auth/login', async (ctx) => {
377
+ const user = await authenticate(ctx.request.body);
378
+
379
+ const sessionId = generateSessionId();
380
+ await saveSession(sessionId, user.id);
381
+
382
+ const signedSessionId = ctx.cookies.sign('sessionId', sessionId);
383
+ ctx.cookies.set('sessionId', signedSessionId);
384
+
385
+ return { success: true };
386
+ });
387
+
388
+ app.get('/api/protected', async (ctx) => {
389
+ const sessionId = ctx.request.signedCookies.get('sessionId');
390
+ if (!sessionId) {
391
+ ctx.response.setStatusCode(401);
392
+ return { error: 'Not authenticated' };
393
+ }
394
+
395
+ const userId = await getUserIdFromSession(sessionId);
396
+ const user = await getUserById(userId);
397
+
398
+ return { user };
399
+ });
400
+
401
+ await app.listen();
402
+ ```
403
+
404
+ ## Development Setup
405
+
406
+ **Use Case:** Development server with relaxed cookie settings
407
+
408
+ ```typescript
409
+ import { YinzerFlow } from 'yinzerflow';
410
+
411
+ const app = new YinzerFlow({
412
+ port: 3000,
413
+ networkLogs: true,
414
+ cookieParser: {
415
+ enabled: true,
416
+ defaults: {
417
+ httpOnly: false, // Allow JavaScript access
418
+ secure: false, // Allow HTTP
419
+ sameSite: 'lax'
420
+ }
421
+ }
422
+ });
423
+
424
+ app.get('/api/test', async (ctx) => {
425
+ ctx.cookies.set('theme', 'dark');
426
+ return { theme: ctx.request.cookies.get('theme') };
427
+ });
428
+
429
+ await app.listen();
430
+ ```
431
+
432
+ # ✨ Best Practices
433
+
434
+ - 🛡️ **Always use `secret`**: Sign authentication cookies to detect tampering
435
+ - 🔒 **Use `httpOnly: true`**: Prevent JavaScript access to sensitive cookies
436
+ - 🔐 **Use `secure: true`**: Only send cookies over HTTPS in production
437
+ - 🎯 **Use `sameSite: 'strict'`**: Best CSRF protection
438
+ - ⏱️ **Match `maxAge` to JWT expiration**: For refresh tokens, set cookie `maxAge` to match JWT `expiresIn`
439
+ - 🔑 **Store secrets in environment variables**: Never commit secrets
440
+ - 📊 **Use signed cookies for sensitive data**: Session IDs, user tokens
441
+ - 💡 **Use unsigned cookies for preferences**: Theme, language
442
+
443
+ ## 🚀 Performance Notes
444
+
445
+ - ⚡ **Parsing overhead**: Minimal - cookies are parsed once per request
446
+ - 💾 **Memory usage**: Maps are garbage collected after request
447
+ - 🔐 **HMAC performance**: Fast - HMAC-SHA256 is efficient (~1ms per cookie)
448
+ - 📊 **Signature size**: ~43 characters per signed cookie (base64url)
449
+
450
+ ## 🔒 Security Notes
451
+
452
+ YinzerFlow implements several security measures to protect against common cookie vulnerabilities:
453
+
454
+ ### 🛡️ HMAC Signature Validation
455
+ - **Problem**: Cookies can be tampered with by clients
456
+ - **YinzerFlow Solution**: HMAC-SHA256 signatures detect tampering
457
+
458
+ ### 🛡️ XSS Protection
459
+ - **Problem**: JavaScript can access cookies via `document.cookie`
460
+ - **YinzerFlow Solution**: `httpOnly: true` prevents JavaScript access
461
+
462
+ ### 🛡️ Man-in-the-Middle Attacks
463
+ - **Problem**: Cookies sent over HTTP can be intercepted
464
+ - **YinzerFlow Solution**: `secure: true` ensures HTTPS-only transmission
465
+
466
+ ### 🛡️ CSRF Protection
467
+ - **Problem**: Cross-site requests can include cookies
468
+ - **YinzerFlow Solution**: `sameSite: 'strict'` blocks cross-site cookie sends
469
+
470
+ ## 🔧 Troubleshooting
471
+
472
+ ### Cookies are not being set
473
+ **Symptom**: Cookies don't appear in browser
474
+
475
+ **Solution:**
476
+ - Enable cookie parser in configuration (`enabled: true`)
477
+ - Check `secure: true` only for HTTPS connections
478
+ - Verify domain/path match
479
+
480
+ ### Signed cookies return `false` when unsigned
481
+ **Symptom**: `ctx.request.signedCookies` is empty or returns false
482
+
483
+ **Solution:**
484
+ - Verify secret is correct
485
+ - Ensure cookie was properly signed using `ctx.cookies.sign()`
486
+ - Check if cookie was tampered with# 📖 Overview
487
+
488
+ YinzerFlow provides a built-in cookie parser that handles parsing incoming cookies, setting outgoing cookies, and optionally signing/validating cookies using HMAC-SHA256. The cookie parser is <span style="color: #95a5a6">**disabled by default**</span> and must be explicitly enabled in the YinzerFlow constructor when needed.
489
+
490
+ <span style="color: #3498db">**💡 Tip:**</span> The cookie parser makes it easy to work with session management and user preferences.
491
+
492
+ **When to use:**
493
+
494
+ - 🍪 Session management (store session IDs)
495
+ - 👤 User authentication tokens (JWT refresh tokens)
496
+ - 🎨 User preferences (theme, language)
497
+ - 📊 Analytics and tracking
498
+
499
+ **Expected outcomes:**
500
+
501
+ - ✅ Automatic parsing of incoming `Cookie` headers into Maps
502
+ - ✅ Helper methods for setting cookies with attributes
503
+ - 🛡️ HMAC signature validation to detect tampering
504
+ - 📝 Type-safe cookie access on request and context objects
505
+
506
+ # ⚙️ Usage
507
+
508
+ ## 🎛️ Settings
509
+
510
+ ### enabled — @default <span style="color: #95a5a6">`false`</span>
511
+
512
+ Enable or disable the cookie parser. Must be explicitly enabled to use cookie parsing.
513
+
514
+ ```typescript
515
+ import { YinzerFlow } from 'yinzerflow';
516
+
517
+ // Enable cookie parser
518
+ const app = new YinzerFlow({
519
+ port: 3000,
520
+ cookieParser: {
521
+ enabled: true,
522
+ secret: process.env.COOKIE_SECRET
523
+ }
524
+ });
525
+
526
+ // Disabled by default (no configuration needed)
527
+ const app = new YinzerFlow({ port: 3000 });
528
+ ```
529
+
530
+ ### secret — @default <span style="color: #95a5a6">`undefined`</span>
531
+
532
+ Secret key for signing cookies using HMAC-SHA256. When provided, cookies can be signed to detect tampering.
533
+
534
+ <span style="color: #e74c3c">**⚠️ Warning:**</span> In production, always use a strong secret (at least 32 characters) stored in environment variables.
535
+
536
+ #### What Makes a Strong Secret?
537
+
538
+ A strong secret should be:
539
+ - **Random**: Generated using cryptographically secure random methods
540
+ - **Long**: Minimum 32 characters (256 bits of entropy)
541
+ - **Unique**: Different for each application/environment
542
+ - **Secret**: Never committed to version control
543
+
544
+ ```typescript
545
+ const app = new YinzerFlow({
546
+ port: 3000,
547
+ cookieParser: {
548
+ secret: process.env.COOKIE_SECRET
549
+ }
550
+ });
551
+ ```
552
+
553
+ #### How to Generate a Strong Secret
554
+
555
+ **Option 1: Using Node.js crypto (recommended)**
556
+ ```bash
557
+ node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
558
+ # Output: 4a8f2c9d1e6b5a3c7f8e2d9b1a4c6e8f2d7b9a3c5e8f1d6b4a2
559
+
560
+ # Use the output in your .env file:
561
+ # COOKIE_SECRET=4a8f2c9d1e6b5a3c7f8e2d9b1a4c6e8f2d7b9a3c5e8f1d6b4a2
562
+ ```
563
+
564
+ **Option 2: Using OpenSSL**
565
+ ```bash
566
+ openssl rand -hex 32
567
+ ```
568
+
569
+ **Option 3: Online generators**
570
+ - Use trusted services like 1Password Secret Key Generator (for reference, not production)
571
+ - Generate locally and store securely
572
+
573
+ #### ❌ What NOT to Use
574
+
575
+ ```typescript
576
+ // ❌ Weak - predictable
577
+ secret: 'my-secret-key'
578
+
579
+ // ❌ Weak - too short
580
+ secret: '12345678'
581
+
582
+ // ❌ Weak - based on company name
583
+ secret: 'acmecorp2024'
584
+
585
+ // ✅ Strong - random and long
586
+ secret: '4a8f2c9d1e6b5a3c7f8e2d9b1a4c6e8f2d7b9a3c5e8f1d6b4a2'
587
+ ```
588
+
589
+ ### signed — @default <span style="color: #95a5a6">`undefined`</span> (sign all)
590
+
591
+ Array of cookie names to sign. If undefined or empty, all cookies are signed when a secret is provided.
592
+
593
+ <span style="color: #3498db">**💡 Tip:**</span> Only sign cookies that need integrity protection (session IDs, user tokens).
594
+
595
+ ```typescript
596
+ // Sign all cookies (default)
597
+ const app = new YinzerFlow({
598
+ port: 3000,
599
+ cookieParser: {
600
+ secret: process.env.COOKIE_SECRET
601
+ }
602
+ });
603
+
604
+ // Sign specific cookies only
605
+ const app = new YinzerFlow({
606
+ port: 3000,
607
+ cookieParser: {
608
+ secret: process.env.COOKIE_SECRET,
609
+ signed: ['sessionId', 'userId']
610
+ }
611
+ });
612
+ ```
613
+
614
+ ### defaults — @default <span style="color: #95a5a6">`undefined`</span>
615
+
616
+ Default cookie options applied to all cookies set via `ctx.cookies.set()`. Can be overridden per-cookie.
617
+
618
+ ```typescript
619
+ const app = new YinzerFlow({
620
+ port: 3000,
621
+ cookieParser: {
622
+ secret: process.env.COOKIE_SECRET,
623
+ defaults: {
624
+ httpOnly: true, // Prevent JavaScript access
625
+ secure: true, // HTTPS only
626
+ sameSite: 'strict', // CSRF protection
627
+ maxAge: '1h' // 1 hour using TimeString (or 3600 as seconds)
628
+ }
629
+ }
630
+ });
631
+ ```
632
+
633
+ <aside>
634
+
635
+ Cookie Options:
636
+
637
+ - **httpOnly**: Prevent JavaScript access (recommended for auth cookies)
638
+ - **secure**: Only send over HTTPS (required in production)
639
+ - **sameSite**: CSRF protection - 'strict' (best), 'lax', or 'none' (requires secure)
640
+ - **maxAge**: Cookie lifetime - accepts `'30s'`, `'15m'`, `'2h'`, `'1d'` or seconds as number
641
+ - **expires**: Expiration date (alternative to maxAge)
642
+ - **domain**: Cookie domain
643
+ - **path**: Cookie path (must start with '/')
644
+
645
+ </aside>
646
+
647
+ # 🔧 Basic Usage
648
+
649
+ ## Parsing Cookies
650
+
651
+ ```typescript
652
+ app.get('/api/user', async (ctx) => {
653
+ // Access unsigned cookies
654
+ const theme = ctx.request.cookies.get('theme');
655
+
656
+ // Access signed cookies (validated)
657
+ const sessionId = ctx.request.signedCookies.get('sessionId');
658
+
659
+ return { theme, sessionId };
660
+ });
661
+ ```
662
+
663
+ ## Setting Cookies
664
+
665
+ ```typescript
666
+ app.post('/api/login', async (ctx) => {
667
+ const user = await validateUser(ctx.request.body);
668
+
669
+ // Set session cookie
670
+ ctx.cookies.set('sessionId', user.sessionId, {
671
+ httpOnly: true,
672
+ secure: true,
673
+ sameSite: 'strict',
674
+ maxAge: '1h' // or 3600 for 1 hour in seconds
675
+ });
676
+
677
+ return { success: true };
678
+ });
679
+ ```
680
+
681
+ ### Setting Multiple Cookies
682
+
683
+ You can set multiple cookies in a single handler. Each call to `ctx.cookies.set()` will add a separate `Set-Cookie` header to the response:
684
+
685
+ ```typescript
686
+ app.post('/api/set-preferences', async (ctx) => {
687
+ // Set multiple cookies - each creates its own Set-Cookie header
688
+ ctx.cookies.set('theme', 'dark', {
689
+ httpOnly: false, // Allow JavaScript access
690
+ maxAge: 31536000 // 1 year
691
+ });
692
+
693
+ ctx.cookies.set('language', 'en', {
694
+ httpOnly: false,
695
+ maxAge: 31536000
696
+ });
697
+
698
+ ctx.cookies.set('sessionId', user.sessionId, {
699
+ httpOnly: true, // Secure session cookie
700
+ secure: true,
701
+ sameSite: 'strict',
702
+ maxAge: 3600
703
+ });
704
+
705
+ return { success: true };
706
+ });
707
+ ```
708
+
709
+ ## Signed Cookies
710
+
711
+ ```typescript
712
+ // Sign and set a cookie
713
+ const signedValue = ctx.cookies.sign('sessionId', 'abc123');
714
+ ctx.cookies.set('sessionId', signedValue, {
715
+ httpOnly: true,
716
+ secure: true
717
+ });
718
+
719
+ // Validate signed cookies
720
+ const sessionId = ctx.request.signedCookies.get('sessionId');
721
+ if (!sessionId) {
722
+ throw new Error('Session cookie missing or tampered');
723
+ }
724
+ ```
725
+
726
+ # 💻 Examples
727
+
728
+ ## JWT Refresh Token Flow
729
+
730
+ **Use Case:** Secure authentication with JWT access tokens and refresh tokens
731
+
732
+ **Description:** Production-ready JWT authentication where short-lived access tokens are used for API calls, and long-lived refresh tokens are stored in httpOnly cookies.
733
+
734
+ ```typescript
735
+ import { YinzerFlow } from 'yinzerflow';
736
+ import jwt from 'jsonwebtoken';
737
+
738
+ const app = new YinzerFlow({
739
+ port: 3000,
740
+ cookieParser: {
741
+ enabled: true,
742
+ secret: process.env.COOKIE_SECRET,
743
+ defaults: {
744
+ httpOnly: true,
745
+ secure: true,
746
+ sameSite: 'strict'
747
+ }
748
+ }
749
+ });
750
+
751
+ // Constants for token lifetime
752
+ const ACCESS_TOKEN_EXPIRY = 15 * 60; // 15 minutes
753
+ const REFRESH_TOKEN_EXPIRY = 7 * 24 * 60 * 60; // 7 days (in seconds)
754
+
755
+ // Login endpoint
756
+ app.post('/api/auth/login', async (ctx) => {
757
+ const { username, password } = ctx.request.body;
758
+
759
+ // Authenticate user
760
+ const user = await validateUser(username, password);
761
+ if (!user) {
762
+ ctx.response.setStatusCode(401);
763
+ return { error: 'Invalid credentials' };
764
+ }
765
+
766
+ // Generate short-lived access token
767
+ const accessToken = jwt.sign(
768
+ { userId: user.id, type: 'access' },
769
+ process.env.JWT_SECRET,
770
+ { expiresIn: `${ACCESS_TOKEN_EXPIRY}s` }
771
+ );
772
+
773
+ // Generate long-lived refresh token
774
+ const refreshToken = jwt.sign(
775
+ { userId: user.id, type: 'refresh' },
776
+ process.env.REFRESH_SECRET,
777
+ { expiresIn: `${REFRESH_TOKEN_EXPIRY}s` }
778
+ );
779
+
780
+ // Store refresh token in cookie with matching maxAge
781
+ ctx.cookies.set('refreshToken', refreshToken, {
782
+ maxAge: REFRESH_TOKEN_EXPIRY // Same as JWT expiration!
783
+ });
784
+
785
+ // Return access token in response body
786
+ return { accessToken };
787
+ });
788
+
789
+ // Token refresh endpoint
790
+ app.post('/api/auth/refresh', async (ctx) => {
791
+ const refreshToken = ctx.request.signedCookies.get('refreshToken');
792
+
793
+ if (!refreshToken) {
794
+ ctx.response.setStatusCode(401);
795
+ return { error: 'Refresh token missing' };
796
+ }
797
+
798
+ try {
799
+ // Verify refresh token
800
+ const decoded = jwt.verify(refreshToken, process.env.REFRESH_SECRET);
801
+
802
+ // Generate new access token
803
+ const accessToken = jwt.sign(
804
+ { userId: decoded.userId, type: 'access' },
805
+ process.env.JWT_SECRET,
806
+ { expiresIn: `${ACCESS_TOKEN_EXPIRY}s` }
807
+ );
808
+
809
+ return { accessToken };
810
+ } catch (error) {
811
+ ctx.response.setStatusCode(401);
812
+ return { error: 'Invalid refresh token' };
813
+ }
814
+ });
815
+
816
+ // Protected endpoint using access token
817
+ app.get('/api/user/profile', async (ctx) => {
818
+ const authHeader = ctx.request.headers.authorization;
819
+ const accessToken = authHeader?.replace('Bearer ', '');
820
+
821
+ if (!accessToken) {
822
+ ctx.response.setStatusCode(401);
823
+ return { error: 'Access token required' };
824
+ }
825
+
826
+ try {
827
+ const decoded = jwt.verify(accessToken, process.env.JWT_SECRET);
828
+ const user = await getUserById(decoded.userId);
829
+ return { user };
830
+ } catch (error) {
831
+ ctx.response.setStatusCode(401);
832
+ return { error: 'Invalid access token' };
833
+ }
834
+ });
835
+
836
+ await app.listen();
837
+ ```
838
+
839
+ ## Session Management
840
+
841
+ **Use Case:** Secure session management with signed cookies
842
+
843
+ ```typescript
844
+ import { YinzerFlow } from 'yinzerflow';
845
+
846
+ const app = new YinzerFlow({
847
+ port: 3000,
848
+ cookieParser: {
849
+ enabled: true,
850
+ secret: process.env.COOKIE_SECRET,
851
+ defaults: {
852
+ httpOnly: true,
853
+ secure: true,
854
+ sameSite: 'strict',
855
+ maxAge: 3600 // 1 hour
856
+ },
857
+ signed: ['sessionId', 'userId']
858
+ }
859
+ });
860
+
861
+ app.post('/api/auth/login', async (ctx) => {
862
+ const user = await authenticate(ctx.request.body);
863
+
864
+ const sessionId = generateSessionId();
865
+ await saveSession(sessionId, user.id);
866
+
867
+ const signedSessionId = ctx.cookies.sign('sessionId', sessionId);
868
+ ctx.cookies.set('sessionId', signedSessionId);
869
+
870
+ return { success: true };
871
+ });
872
+
873
+ app.get('/api/protected', async (ctx) => {
874
+ const sessionId = ctx.request.signedCookies.get('sessionId');
875
+ if (!sessionId) {
876
+ ctx.response.setStatusCode(401);
877
+ return { error: 'Not authenticated' };
878
+ }
879
+
880
+ const userId = await getUserIdFromSession(sessionId);
881
+ const user = await getUserById(userId);
882
+
883
+ return { user };
884
+ });
885
+
886
+ await app.listen();
887
+ ```
888
+
889
+ ## Development Setup
890
+
891
+ **Use Case:** Development server with relaxed cookie settings
892
+
893
+ ```typescript
894
+ import { YinzerFlow } from 'yinzerflow';
895
+
896
+ const app = new YinzerFlow({
897
+ port: 3000,
898
+ networkLogs: true,
899
+ cookieParser: {
900
+ enabled: true,
901
+ defaults: {
902
+ httpOnly: false, // Allow JavaScript access
903
+ secure: false, // Allow HTTP
904
+ sameSite: 'lax'
905
+ }
906
+ }
907
+ });
908
+
909
+ app.get('/api/test', async (ctx) => {
910
+ ctx.cookies.set('theme', 'dark');
911
+ return { theme: ctx.request.cookies.get('theme') };
912
+ });
913
+
914
+ await app.listen();
915
+ ```
916
+
917
+ # ✨ Best Practices
918
+
919
+ - 🛡️ **Always use `secret`**: Sign authentication cookies to detect tampering
920
+ - 🔒 **Use `httpOnly: true`**: Prevent JavaScript access to sensitive cookies
921
+ - 🔐 **Use `secure: true`**: Only send cookies over HTTPS in production
922
+ - 🎯 **Use `sameSite: 'strict'`**: Best CSRF protection
923
+ - ⏱️ **Match `maxAge` to JWT expiration**: For refresh tokens, set cookie `maxAge` to match JWT `expiresIn`
924
+ - 🔑 **Store secrets in environment variables**: Never commit secrets
925
+ - 📊 **Use signed cookies for sensitive data**: Session IDs, user tokens
926
+ - 💡 **Use unsigned cookies for preferences**: Theme, language
927
+
928
+ ## 🚀 Performance Notes
929
+
930
+ - ⚡ **Parsing overhead**: Minimal - cookies are parsed once per request
931
+ - 💾 **Memory usage**: Maps are garbage collected after request
932
+ - 🔐 **HMAC performance**: Fast - HMAC-SHA256 is efficient (~1ms per cookie)
933
+ - 📊 **Signature size**: ~43 characters per signed cookie (base64url)
934
+
935
+ ## 🔒 Security Notes
936
+
937
+ YinzerFlow implements several security measures to protect against common cookie vulnerabilities:
938
+
939
+ ### 🛡️ HMAC Signature Validation
940
+ - **Problem**: Cookies can be tampered with by clients
941
+ - **YinzerFlow Solution**: HMAC-SHA256 signatures detect tampering
942
+
943
+ ### 🛡️ XSS Protection
944
+ - **Problem**: JavaScript can access cookies via `document.cookie`
945
+ - **YinzerFlow Solution**: `httpOnly: true` prevents JavaScript access
946
+
947
+ ### 🛡️ Man-in-the-Middle Attacks
948
+ - **Problem**: Cookies sent over HTTP can be intercepted
949
+ - **YinzerFlow Solution**: `secure: true` ensures HTTPS-only transmission
950
+
951
+ ### 🛡️ CSRF Protection
952
+ - **Problem**: Cross-site requests can include cookies
953
+ - **YinzerFlow Solution**: `sameSite: 'strict'` blocks cross-site cookie sends
954
+
955
+ ## 🔧 Troubleshooting
956
+
957
+ ### Cookies are not being set
958
+ **Symptom**: Cookies don't appear in browser
959
+
960
+ **Solution:**
961
+ - Enable cookie parser in configuration (`enabled: true`)
962
+ - Check `secure: true` only for HTTPS connections
963
+ - Verify domain/path match
964
+
965
+ ### Signed cookies return `false` when unsigned
966
+ **Symptom**: `ctx.request.signedCookies` is empty or returns false
967
+
968
+ **Solution:**
969
+ - Verify secret is correct
970
+ - Ensure cookie was properly signed using `ctx.cookies.sign()`
971
+ - Check if cookie was tampered with
972
+
973
+ ### Cookies accessible to JavaScript when they shouldn't be
974
+ **Symptom**: JavaScript can read sensitive cookies
975
+
976
+ **Solution:** Set `httpOnly: true` in defaults or per-cookie
977
+
978
+
979
+ ### Cookies accessible to JavaScript when they shouldn't be
980
+ **Symptom**: JavaScript can read sensitive cookies
981
+
982
+ **Solution:** Set `httpOnly: true` in defaults or per-cookie