astro-sessionkit 0.1.2

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.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/dist/core/config.d.ts +9 -0
  3. package/dist/core/config.d.ts.map +1 -0
  4. package/dist/core/config.js +43 -0
  5. package/dist/core/config.js.map +1 -0
  6. package/dist/core/context.d.ts +7 -0
  7. package/dist/core/context.d.ts.map +1 -0
  8. package/dist/core/context.js +11 -0
  9. package/dist/core/context.js.map +1 -0
  10. package/dist/core/guardMiddleware.d.ts +3 -0
  11. package/dist/core/guardMiddleware.d.ts.map +1 -0
  12. package/dist/core/guardMiddleware.js +56 -0
  13. package/dist/core/guardMiddleware.js.map +1 -0
  14. package/dist/core/matcher.d.ts +2 -0
  15. package/dist/core/matcher.d.ts.map +1 -0
  16. package/dist/core/matcher.js +38 -0
  17. package/dist/core/matcher.js.map +1 -0
  18. package/dist/core/sessionMiddleware.d.ts +3 -0
  19. package/dist/core/sessionMiddleware.d.ts.map +1 -0
  20. package/dist/core/sessionMiddleware.js +23 -0
  21. package/dist/core/sessionMiddleware.js.map +1 -0
  22. package/dist/core/types.d.ts +40 -0
  23. package/dist/core/types.d.ts.map +1 -0
  24. package/dist/core/types.js +2 -0
  25. package/dist/core/types.js.map +1 -0
  26. package/dist/core/validation.d.ts +5 -0
  27. package/dist/core/validation.d.ts.map +1 -0
  28. package/dist/core/validation.js +91 -0
  29. package/dist/core/validation.js.map +1 -0
  30. package/dist/guard.d.ts +2 -0
  31. package/dist/guard.d.ts.map +1 -0
  32. package/dist/guard.js +5 -0
  33. package/dist/guard.js.map +1 -0
  34. package/dist/index.d.ts +6 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +25 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/integration.d.ts +5 -0
  39. package/dist/integration.d.ts.map +1 -0
  40. package/dist/integration.js +22 -0
  41. package/dist/integration.js.map +1 -0
  42. package/dist/middleware.d.ts +2 -0
  43. package/dist/middleware.d.ts.map +1 -0
  44. package/dist/middleware.js +1 -0
  45. package/dist/middleware.js.map +1 -0
  46. package/dist/server.d.ts +13 -0
  47. package/dist/server.d.ts.map +1 -0
  48. package/dist/server.js +67 -0
  49. package/dist/server.js.map +1 -0
  50. package/package.json +89 -0
  51. package/readme.md +601 -0
  52. package/security.md +349 -0
package/readme.md ADDED
@@ -0,0 +1,601 @@
1
+ # 🔐 Astro SessionKit
2
+
3
+ Simple session access and route protection for Astro applications.
4
+
5
+ ## Features
6
+
7
+ - ✅ **Simple API** - Access session data anywhere in your app
8
+ - 🛡️ **Route Protection** - Declarative route guards with roles/permissions
9
+ - 🚀 **Type Safe** - Full TypeScript support
10
+ - 🎯 **Flexible** - Works with any session storage (cookies, Redis, DB, etc.)
11
+ - ⚡ **Fast** - Uses AsyncLocalStorage for zero-overhead access
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install astro-sessionkit
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ ### 1. Configure the Integration
22
+
23
+ ```ts
24
+ // astro.config.mjs
25
+ import { defineConfig } from 'astro/config';
26
+ import sessionkit from 'astro-sessionkit';
27
+
28
+ export default defineConfig({
29
+ integrations: [
30
+ sessionkit({
31
+ loginPath: '/login',
32
+ protect: [
33
+ // Protect admin routes
34
+ { pattern: '/admin/**', role: 'admin' },
35
+
36
+ // Protect dashboard for authenticated users
37
+ { pattern: '/dashboard', roles: ['user', 'admin'] },
38
+
39
+ // Protect by permission
40
+ { pattern: '/settings', permission: 'settings:write' },
41
+
42
+ // Custom logic
43
+ {
44
+ pattern: '/premium/**',
45
+ allow: (session) => session?.subscription === 'premium'
46
+ }
47
+ ]
48
+ })
49
+ ]
50
+ });
51
+ ```
52
+
53
+ ### 2. Set Up Your Session
54
+
55
+ SessionKit reads from `context.session.get('__session__')`. You set it up in your middleware:
56
+
57
+ ```ts
58
+ // src/middleware.ts
59
+ import { defineMiddleware } from 'astro:middleware';
60
+
61
+ export const onRequest = defineMiddleware(async (context, next) => {
62
+ // Get session from wherever you store it
63
+ // (cookies, Redis, database, etc.)
64
+ const sessionId = context.cookies.get('session_id')?.value;
65
+
66
+ if (sessionId) {
67
+ const user = await db.getUserBySessionId(sessionId);
68
+
69
+ // Set session for SessionKit to read
70
+ context.session.set('__session__', {
71
+ userId: user.id,
72
+ email: user.email,
73
+ role: user.role,
74
+ permissions: user.permissions
75
+ });
76
+ }
77
+
78
+ return next();
79
+ });
80
+ ```
81
+
82
+ ### 3. Register Sessions After Authentication
83
+
84
+ Use the provided helpers to register sessions after successful authentication:
85
+
86
+ ```ts
87
+ // src/pages/api/login.ts
88
+ import type { APIRoute } from 'astro';
89
+ import { setSession } from 'astro-sessionkit/server';
90
+
91
+ export const POST: APIRoute = async (context) => {
92
+ const { email, password } = await context.request.json();
93
+
94
+ // Verify credentials (YOUR authentication logic)
95
+ const user = await verifyCredentials(email, password);
96
+
97
+ if (user) {
98
+ // Register session with SessionKit
99
+ setSession(context, {
100
+ userId: user.id,
101
+ email: user.email,
102
+ role: user.role,
103
+ permissions: user.permissions
104
+ });
105
+
106
+ // Store session ID (YOUR storage logic)
107
+ const sessionId = await createSessionInDatabase(user.id);
108
+ context.cookies.set('session_id', sessionId, {
109
+ httpOnly: true,
110
+ secure: true,
111
+ sameSite: 'lax',
112
+ maxAge: 60 * 60 * 24 * 7 // 7 days
113
+ });
114
+
115
+ return new Response(JSON.stringify({ success: true }));
116
+ }
117
+
118
+ return new Response(JSON.stringify({ error: 'Invalid credentials' }), {
119
+ status: 401
120
+ });
121
+ };
122
+ ```
123
+
124
+ ### 4. Use Session in Your Pages
125
+
126
+ ```astro
127
+ ---
128
+ // src/pages/dashboard.astro
129
+ import { getSession, requireSession } from 'astro-sessionkit/server';
130
+
131
+ // Get session (returns null if not authenticated)
132
+ const session = getSession();
133
+
134
+ // Or require authentication (throws 401 if not authenticated)
135
+ const session = requireSession();
136
+ ---
137
+
138
+ <h1>Welcome, {session.email}</h1>
139
+ ```
140
+
141
+ ## API Reference
142
+
143
+ ### Session Management Functions
144
+
145
+ #### `setSession(context, session)`
146
+
147
+ Register a session after successful authentication.
148
+
149
+ **Parameters:**
150
+ - `context: APIContext` - Astro API context
151
+ - `session: Session` - Session data to register
152
+
153
+ **Throws:** Error if session structure is invalid
154
+
155
+ ```ts
156
+ import { setSession } from 'astro-sessionkit/server';
157
+
158
+ export const POST: APIRoute = async (context) => {
159
+ const user = await authenticateUser(credentials);
160
+
161
+ setSession(context, {
162
+ userId: user.id,
163
+ email: user.email,
164
+ role: user.role,
165
+ permissions: user.permissions
166
+ });
167
+
168
+ // Also store in cookies/database
169
+ context.cookies.set('session_id', sessionId);
170
+ };
171
+ ```
172
+
173
+ #### `clearSession(context)`
174
+
175
+ Clear the session during logout.
176
+
177
+ ```ts
178
+ import { clearSession } from 'astro-sessionkit/server';
179
+
180
+ export const POST: APIRoute = async (context) => {
181
+ clearSession(context);
182
+
183
+ // Also delete from cookies/database
184
+ context.cookies.delete('session_id');
185
+ await db.deleteSession(sessionId);
186
+
187
+ return context.redirect('/');
188
+ };
189
+ ```
190
+
191
+ #### `updateSession(context, updates)`
192
+
193
+ Update specific fields in the current session.
194
+
195
+ **Parameters:**
196
+ - `context: APIContext` - Astro API context
197
+ - `updates: Partial<Session>` - Fields to update
198
+
199
+ **Throws:** Error if no session exists or updated session is invalid
200
+
201
+ ```ts
202
+ import { updateSession } from 'astro-sessionkit/server';
203
+
204
+ export const POST: APIRoute = async (context) => {
205
+ // Update user's role
206
+ updateSession(context, {
207
+ role: 'admin',
208
+ permissions: ['admin:read', 'admin:write']
209
+ });
210
+
211
+ // Also update in your storage
212
+ await db.updateSession(sessionId, updates);
213
+ };
214
+ ```
215
+
216
+ ### Session Access Functions
217
+
218
+ All functions are imported from `astro-sessionkit/server`:
219
+
220
+ #### `getSession()`
221
+
222
+ Get the current session (returns `null` if not authenticated).
223
+
224
+ ```ts
225
+ import { getSession } from 'astro-sessionkit/server';
226
+
227
+ const session = getSession();
228
+ if (session) {
229
+ console.log('User ID:', session.userId);
230
+ }
231
+ ```
232
+
233
+ #### `requireSession()`
234
+
235
+ Get the current session or throw 401 if not authenticated.
236
+
237
+ ```ts
238
+ import { requireSession } from 'astro-sessionkit/server';
239
+
240
+ const session = requireSession();
241
+ // TypeScript knows session is not null here
242
+ ```
243
+
244
+ #### `isAuthenticated()`
245
+
246
+ Check if the user is authenticated.
247
+
248
+ ```ts
249
+ import { isAuthenticated } from 'astro-sessionkit/server';
250
+
251
+ if (isAuthenticated()) {
252
+ // User is logged in
253
+ }
254
+ ```
255
+
256
+ #### `hasRole(role: string)`
257
+
258
+ Check if user has a specific role.
259
+
260
+ ```ts
261
+ import { hasRole } from 'astro-sessionkit/server';
262
+
263
+ if (hasRole('admin')) {
264
+ // User is an admin
265
+ }
266
+ ```
267
+
268
+ #### `hasPermission(permission: string)`
269
+
270
+ Check if user has a specific permission.
271
+
272
+ ```ts
273
+ import { hasPermission } from 'astro-sessionkit/server';
274
+
275
+ if (hasPermission('posts:delete')) {
276
+ // User can delete posts
277
+ }
278
+ ```
279
+
280
+ #### `hasAllPermissions(...permissions: string[])`
281
+
282
+ Check if user has ALL specified permissions.
283
+
284
+ ```ts
285
+ import { hasAllPermissions } from 'astro-sessionkit/server';
286
+
287
+ if (hasAllPermissions('posts:read', 'posts:write')) {
288
+ // User has both permissions
289
+ }
290
+ ```
291
+
292
+ #### `hasAnyPermission(...permissions: string[])`
293
+
294
+ Check if user has ANY of the specified permissions.
295
+
296
+ ```ts
297
+ import { hasAnyPermission } from 'astro-sessionkit/server';
298
+
299
+ if (hasAnyPermission('posts:delete', 'admin:panel')) {
300
+ // User has at least one permission
301
+ }
302
+ ```
303
+
304
+ ## Route Protection
305
+
306
+ ### Protection Rules
307
+
308
+ #### By Role
309
+
310
+ Require a specific role:
311
+
312
+ ```ts
313
+ { pattern: '/admin/**', role: 'admin' }
314
+ ```
315
+
316
+ #### By Multiple Roles
317
+
318
+ User must have ONE of these roles:
319
+
320
+ ```ts
321
+ { pattern: '/dashboard', roles: ['user', 'admin', 'moderator'] }
322
+ ```
323
+
324
+ #### By Permission
325
+
326
+ Require a specific permission:
327
+
328
+ ```ts
329
+ { pattern: '/settings', permission: 'settings:write' }
330
+ ```
331
+
332
+ #### By Multiple Permissions
333
+
334
+ User must have ALL of these permissions:
335
+
336
+ ```ts
337
+ { pattern: '/admin/users', permissions: ['users:read', 'users:write'] }
338
+ ```
339
+
340
+ #### Custom Logic
341
+
342
+ Use a custom function for complex logic:
343
+
344
+ ```ts
345
+ {
346
+ pattern: '/premium/**',
347
+ allow: (session) => {
348
+ return session?.subscription === 'premium' && !session?.banned;
349
+ }
350
+ }
351
+ ```
352
+
353
+ #### Custom Redirect
354
+
355
+ Override the default login path per rule:
356
+
357
+ ```ts
358
+ {
359
+ pattern: '/admin/**',
360
+ role: 'admin',
361
+ redirectTo: '/unauthorized'
362
+ }
363
+ ```
364
+
365
+ ### Pattern Matching
366
+
367
+ Patterns support glob syntax:
368
+
369
+ - `/admin` - Exact match
370
+ - `/admin/*` - One or more segments (`/admin/users`, `/admin/users/123`)
371
+ - `/admin/**` - Any path under admin (`/admin`, `/admin/users`, `/admin/x/y/z`)
372
+
373
+ ## Session Type
374
+
375
+ The session object must have this shape:
376
+
377
+ ```ts
378
+ interface Session {
379
+ userId: string; // Required
380
+ email?: string;
381
+ role?: string;
382
+ roles?: string[];
383
+ permissions?: string[];
384
+ [key: string]: unknown; // Add any custom fields
385
+ }
386
+ ```
387
+
388
+ You control what goes in the session - SessionKit just reads it.
389
+
390
+ ## Advanced Configuration
391
+
392
+ ### Custom Access Hooks
393
+
394
+ Override how roles/permissions are extracted:
395
+
396
+ ```ts
397
+ sessionkit({
398
+ access: {
399
+ // Custom role extraction
400
+ getRole: (session) => session?.primaryRole ?? null,
401
+
402
+ // Custom permissions extraction
403
+ getPermissions: (session) => {
404
+ return [...session?.permissions ?? [], ...session?.dynamicPerms ?? []];
405
+ },
406
+
407
+ // Override all built-in checks
408
+ check: (rule, session) => {
409
+ // Your custom logic
410
+ return session?.customField === 'allowed';
411
+ }
412
+ }
413
+ })
414
+ ```
415
+
416
+ ### Logout Flow
417
+
418
+ ```ts
419
+ // src/pages/api/logout.ts
420
+ import type { APIRoute } from 'astro';
421
+ import { clearSession } from 'astro-sessionkit/server';
422
+
423
+ export const POST: APIRoute = async (context) => {
424
+ const sessionId = context.cookies.get('session_id')?.value;
425
+
426
+ // Clear from SessionKit
427
+ clearSession(context);
428
+
429
+ // Delete from storage
430
+ if (sessionId) {
431
+ await db.deleteSession(sessionId);
432
+ }
433
+ context.cookies.delete('session_id');
434
+
435
+ return context.redirect('/login');
436
+ };
437
+ ```
438
+
439
+ ### Update Session Data
440
+
441
+ ```ts
442
+ // src/pages/api/update-role.ts
443
+ import type { APIRoute } from 'astro';
444
+ import { updateSession } from 'astro-sessionkit/server';
445
+
446
+ export const POST: APIRoute = async (context) => {
447
+ const { newRole } = await context.request.json();
448
+
449
+ // Update in SessionKit
450
+ updateSession(context, { role: newRole });
451
+
452
+ // Also update in your database
453
+ const sessionId = context.cookies.get('session_id')?.value;
454
+ await db.updateUserRole(sessionId, newRole);
455
+
456
+ return new Response(JSON.stringify({ success: true }));
457
+ };
458
+ ```
459
+
460
+ ## Examples
461
+
462
+ ### Complete Authentication Flow
463
+
464
+ ```ts
465
+ // src/pages/api/auth/login.ts
466
+ import type { APIRoute } from 'astro';
467
+ import { setSession } from 'astro-sessionkit/server';
468
+ import { hashPassword, generateSessionId } from './utils';
469
+
470
+ export const POST: APIRoute = async (context) => {
471
+ const { email, password } = await context.request.json();
472
+
473
+ // 1. Verify credentials
474
+ const user = await db.findUserByEmail(email);
475
+ if (!user || !await hashPassword.verify(password, user.hashedPassword)) {
476
+ return new Response(JSON.stringify({ error: 'Invalid credentials' }), {
477
+ status: 401
478
+ });
479
+ }
480
+
481
+ // 2. Create session ID
482
+ const sessionId = generateSessionId();
483
+ await db.createSession({
484
+ id: sessionId,
485
+ userId: user.id,
486
+ expiresAt: Date.now() + (7 * 24 * 60 * 60 * 1000) // 7 days
487
+ });
488
+
489
+ // 3. Register with SessionKit
490
+ setSession(context, {
491
+ userId: user.id,
492
+ email: user.email,
493
+ role: user.role,
494
+ permissions: user.permissions
495
+ });
496
+
497
+ // 4. Set secure cookie
498
+ context.cookies.set('session_id', sessionId, {
499
+ httpOnly: true,
500
+ secure: import.meta.env.PROD,
501
+ sameSite: 'lax',
502
+ maxAge: 60 * 60 * 24 * 7, // 7 days
503
+ path: '/'
504
+ });
505
+
506
+ return new Response(JSON.stringify({
507
+ success: true,
508
+ user: { email: user.email, role: user.role }
509
+ }));
510
+ };
511
+ ```
512
+
513
+ ## Examples
514
+
515
+ ### Protect Multiple Route Patterns
516
+
517
+ ```ts
518
+ sessionkit({
519
+ protect: [
520
+ // Admin section
521
+ { pattern: '/admin/**', role: 'admin', redirectTo: '/unauthorized' },
522
+
523
+ // User dashboard
524
+ { pattern: '/dashboard', roles: ['user', 'admin'] },
525
+
526
+ // Settings require specific permission
527
+ { pattern: '/settings/**', permission: 'settings:access' },
528
+
529
+ // Premium content
530
+ {
531
+ pattern: '/premium/**',
532
+ allow: (session) => session?.tier === 'premium'
533
+ }
534
+ ]
535
+ })
536
+ ```
537
+
538
+ ### Conditional Rendering
539
+
540
+ ```astro
541
+ ---
542
+ import { hasRole, hasPermission } from 'astro-sessionkit/server';
543
+ ---
544
+
545
+ {hasRole('admin') && (
546
+ <a href="/admin">Admin Panel</a>
547
+ )}
548
+
549
+ {hasPermission('posts:create') && (
550
+ <button>Create Post</button>
551
+ )}
552
+ ```
553
+
554
+ ### API Route Protection
555
+
556
+ ```ts
557
+ // src/pages/api/admin.ts
558
+ import type { APIRoute } from 'astro';
559
+ import { requireSession, hasRole } from 'astro-sessionkit/server';
560
+
561
+ export const GET: APIRoute = async () => {
562
+ const session = requireSession();
563
+
564
+ if (!hasRole('admin')) {
565
+ return new Response('Forbidden', { status: 403 });
566
+ }
567
+
568
+ // Admin logic here
569
+ return new Response(JSON.stringify({ data: 'secret' }));
570
+ };
571
+ ```
572
+
573
+ ## How It Works
574
+
575
+ 1. **You set the session** in `context.locals.session` via your own middleware
576
+ 2. **SessionKit reads it** and makes it available via AsyncLocalStorage
577
+ 3. **Route guards** automatically protect paths based on your rules
578
+ 4. **Helper functions** provide easy access throughout your app
579
+
580
+ ## Security
581
+
582
+ ⚠️ **Important**: SessionKit handles session access and route protection, but **does NOT handle**:
583
+ - Session creation/storage
584
+ - Authentication
585
+ - Session expiration
586
+ - CSRF protection
587
+
588
+ These are your responsibility. See [SECURITY.md](./SECURITY.md) for a complete security guide.
589
+
590
+ ### Quick Security Checklist
591
+
592
+ Before production:
593
+ - ✅ Encrypt/sign your sessions (use lucia-auth, @auth/astro, or iron-session)
594
+ - ✅ Set secure cookie flags (HttpOnly, Secure, SameSite)
595
+ - ✅ Implement session expiration
596
+ - ✅ Add CSRF protection for state-changing operations
597
+ - ✅ Use HTTPS in production
598
+
599
+ ## License
600
+
601
+ MIT License © Alex Mora