astro-sessionkit 0.1.19 → 0.1.21

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 (46) hide show
  1. package/dist/core/config.d.ts +2 -0
  2. package/dist/core/config.d.ts.map +1 -1
  3. package/dist/core/config.js +16 -3
  4. package/dist/core/config.js.map +1 -1
  5. package/dist/core/context.d.ts +2 -1
  6. package/dist/core/context.d.ts.map +1 -1
  7. package/dist/core/context.js +9 -4
  8. package/dist/core/context.js.map +1 -1
  9. package/dist/core/guardMiddleware.d.ts.map +1 -1
  10. package/dist/core/guardMiddleware.js +30 -9
  11. package/dist/core/guardMiddleware.js.map +1 -1
  12. package/dist/core/matcher.d.ts.map +1 -1
  13. package/dist/core/matcher.js +7 -1
  14. package/dist/core/matcher.js.map +1 -1
  15. package/dist/core/sessionMiddleware.d.ts.map +1 -1
  16. package/dist/core/sessionMiddleware.js +38 -10
  17. package/dist/core/sessionMiddleware.js.map +1 -1
  18. package/dist/core/types.d.ts +9 -0
  19. package/dist/core/types.d.ts.map +1 -1
  20. package/dist/core/types.js.map +1 -1
  21. package/dist/core/validation.d.ts.map +1 -1
  22. package/dist/core/validation.js +10 -1
  23. package/dist/core/validation.js.map +1 -1
  24. package/dist/index.d.ts +12 -4
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +3 -24
  27. package/dist/index.js.map +1 -1
  28. package/dist/integration.d.ts +1 -1
  29. package/dist/integration.d.ts.map +1 -1
  30. package/dist/integration.js +12 -4
  31. package/dist/integration.js.map +1 -1
  32. package/dist/server.d.ts +12 -5
  33. package/dist/server.d.ts.map +1 -1
  34. package/dist/server.js +47 -12
  35. package/dist/server.js.map +1 -1
  36. package/package.json +4 -2
  37. package/src/core/config.ts +30 -4
  38. package/src/core/context.ts +29 -6
  39. package/src/core/guardMiddleware.ts +56 -35
  40. package/src/core/matcher.ts +8 -1
  41. package/src/core/sessionMiddleware.ts +45 -10
  42. package/src/core/types.ts +27 -5
  43. package/src/core/validation.ts +14 -1
  44. package/src/index.ts +3 -52
  45. package/src/integration.ts +18 -3
  46. package/src/server.ts +110 -26
@@ -5,7 +5,7 @@
5
5
  import type {MiddlewareHandler} from "astro";
6
6
  import {runWithContext as defaultRunWithContext} from "./context";
7
7
  import {isValidSessionStructure} from "./validation";
8
- import type {Session} from "./types";
8
+ import type {Session, SessionContext, SessionKitContext} from "./types";
9
9
  import {getConfig} from "./config";
10
10
  import * as logger from "./logger";
11
11
 
@@ -14,6 +14,11 @@ import * as logger from "./logger";
14
14
  */
15
15
  const SESSION_KEY = "__session__";
16
16
 
17
+ /**
18
+ * Redundant logging prevention key
19
+ */
20
+ const LOGGED_KEY = Symbol.for('astro-sessionkit.middleware.logged');
21
+
17
22
  /**
18
23
  * Main session middleware
19
24
  *
@@ -21,8 +26,11 @@ const SESSION_KEY = "__session__";
21
26
  * throughout the request via AsyncLocalStorage
22
27
  */
23
28
  export const sessionMiddleware: MiddlewareHandler = async (context, next) => {
29
+ const config = getConfig();
30
+ const {runWithContext, getContextStore, setContextStore, context: externalContext, debug} = config;
31
+
24
32
  // Get session from context.session store
25
- const rawSession = context.session?.get<Session>(SESSION_KEY) ?? null;
33
+ const rawSession = await context.session?.get<Session>(SESSION_KEY) ?? null;
26
34
 
27
35
  // Validate session structure if present
28
36
  let session: Session | null = null;
@@ -42,24 +50,51 @@ export const sessionMiddleware: MiddlewareHandler = async (context, next) => {
42
50
  }
43
51
 
44
52
  // Run the rest of the request chain with session context
45
- const config = getConfig();
53
+ const globalStorage = globalThis as any;
54
+ if (!globalStorage[LOGGED_KEY]) {
55
+ let contextStrategy = 'default';
56
+
57
+ if (runWithContext) {
58
+ contextStrategy = 'custom (runWithContext)';
59
+ } else if (getContextStore) {
60
+ contextStrategy = 'custom (getter/setter)';
61
+ } else if (externalContext) {
62
+ contextStrategy = 'custom (external AsyncLocalStorage)';
63
+ }
64
+
65
+ logger.debug(`[SessionKit] Middleware initialized (context: ${contextStrategy})`);
66
+ globalStorage[LOGGED_KEY] = true;
67
+ }
68
+
69
+ const runLogic = () => next();
70
+ const sessionContext: SessionContext = { session, astroContext: context as SessionKitContext };
46
71
 
47
72
  // If getContextStore is provided, but runWithContext is NOT,
48
73
  // we assume the user is managing the context at a superior level
49
74
  // and we should NOT wrap the call in our default runner.
50
- if (config.getContextStore && !config.runWithContext) {
75
+ if (getContextStore && !runWithContext) {
51
76
  // Initialize context store if setter is provided
52
- const store = config.getContextStore();
77
+ const store = getContextStore();
78
+ if (debug) {
79
+ logger.debug('[SessionMiddleware] Custom getContextStore returned:', !!store);
80
+ }
53
81
  if (store) {
54
82
  store.session = session;
55
- } else if (config.setContextStore) {
56
- config.setContextStore({session});
83
+ } else if (setContextStore) {
84
+ if (debug) {
85
+ logger.debug('[SessionMiddleware] Calling custom setContextStore');
86
+ }
87
+ setContextStore(sessionContext);
57
88
  } else {
58
89
  logger.error('getContextStore returned undefined, cannot set session');
59
90
  }
60
- return next();
91
+ return runLogic();
92
+ }
93
+
94
+ if (debug) {
95
+ logger.debug('[SessionMiddleware] Using' + (runWithContext ? ' custom ' : ' default ') + 'runner');
61
96
  }
62
97
 
63
- const runner = config.runWithContext ?? defaultRunWithContext;
64
- return runner({session}, () => next());
98
+ const runner = runWithContext ?? defaultRunWithContext;
99
+ return runner(sessionContext, runLogic);
65
100
  };
package/src/core/types.ts CHANGED
@@ -2,6 +2,18 @@
2
2
  // Core Session Types
3
3
  // ============================================================================
4
4
 
5
+ import type {AstroCookies, AstroSession} from "astro";
6
+
7
+ /**
8
+ * Minimal context required by SessionKit
9
+ */
10
+ export interface SessionKitContext {
11
+ cookies: AstroCookies;
12
+ session?: AstroSession;
13
+ redirect: (path: string, status?: number) => Response;
14
+ [key: string]: any;
15
+ }
16
+
5
17
  /**
6
18
  * The session object stored in context.locals.session
7
19
  * This is what your Astro app provides - we just read it.
@@ -31,6 +43,10 @@ export interface Session {
31
43
  */
32
44
  export interface SessionContext {
33
45
  session: Session | null;
46
+ /**
47
+ * Original Astro context (cookies, session, redirect, etc.)
48
+ */
49
+ astroContext?: SessionKitContext;
34
50
  }
35
51
 
36
52
  // ============================================================================
@@ -139,9 +155,15 @@ export interface SessionKitConfig {
139
155
  */
140
156
  exclude?: string[];
141
157
 
142
- /**
143
- * Enable debug logging
144
- * @default false
145
- */
146
- debug?: boolean;
158
+ /**
159
+ * Optional external AsyncLocalStorage instance to use for session context.
160
+ * If provided, SessionKit will use this instead of its internal instance.
161
+ */
162
+ context?: any;
163
+
164
+ /**
165
+ * Enable debug logging
166
+ * @default false
167
+ */
168
+ debug?: boolean;
147
169
  }
@@ -21,13 +21,18 @@ export function isValidSessionStructure(input: unknown): input is Session {
21
21
  return false;
22
22
  }
23
23
 
24
+ // Security: Check for unexpected null bytes or control characters in userId
25
+ if (/[\x00-\x1F\x7F]/.test(session.userId)) {
26
+ return false;
27
+ }
28
+
24
29
  // DoS protection: Limit userId length
25
30
  if (session.userId.length > 255) {
26
31
  return false;
27
32
  }
28
33
 
29
34
  // Optional fields validation (if present)
30
- if (session.email !== undefined) {
35
+ if (session.email !== undefined && session.email !== null) {
31
36
  if (typeof session.email !== 'string') {
32
37
  return false;
33
38
  }
@@ -35,6 +40,10 @@ export function isValidSessionStructure(input: unknown): input is Session {
35
40
  if (session.email.length > 320) {
36
41
  return false;
37
42
  }
43
+ // Basic email format sanity check (not full validation, just security)
44
+ if (session.email.length > 0 && !session.email.includes('@')) {
45
+ return false;
46
+ }
38
47
  }
39
48
 
40
49
  if (session.role !== undefined && session.role !== null) {
@@ -45,6 +54,10 @@ export function isValidSessionStructure(input: unknown): input is Session {
45
54
  if (session.role.length > 100) {
46
55
  return false;
47
56
  }
57
+ // Security: No control characters in role
58
+ if (/[\x00-\x1F\x7F]/.test(session.role)) {
59
+ return false;
60
+ }
48
61
  }
49
62
 
50
63
  if (session.roles !== undefined && session.roles !== null) {
package/src/index.ts CHANGED
@@ -2,58 +2,9 @@
2
2
  // Astro SessionKit - Main Integration Entry Point
3
3
  // ============================================================================
4
4
 
5
- import type {AstroIntegration } from "astro";
6
- import {getConfig, setConfig} from "./core/config";
7
- import type { SessionKitConfig } from "./core/types";
5
+ import sessionkit from "./integration";
8
6
 
9
- /**
10
- * SessionKit - Simple session access and route protection for Astro
11
- *
12
- * @example
13
- * ```ts
14
- * // astro.config.mjs
15
- * import sessionkit from 'astro-sessionkit';
16
- *
17
- * export default defineConfig({
18
- * integrations: [
19
- * sessionkit({
20
- * loginPath: '/login',
21
- * protect: [
22
- * { pattern: '/admin/**', role: 'admin' },
23
- * { pattern: '/dashboard', roles: ['user', 'admin'] },
24
- * { pattern: '/settings', permissions: ['settings:write'] }
25
- * ]
26
- * })
27
- * ]
28
- * });
29
- * ```
30
- */
31
- export default function sessionkit(config: SessionKitConfig = {}): AstroIntegration {
32
- // Store configuration
33
- setConfig(config);
34
- const resolvedConfig = getConfig();
35
-
36
- return {
37
- name: "astro-sessionkit",
38
- hooks: {
39
- "astro:config:setup": ({ addMiddleware }) => {
40
- // 1. Always add session context middleware first
41
- addMiddleware({
42
- entrypoint: "astro-sessionkit/middleware",
43
- order: "pre",
44
- });
45
-
46
- // 2. Add route guard if there are protection rules or global protection is enabled
47
- if ((resolvedConfig.protect && resolvedConfig.protect.length > 0) || resolvedConfig.globalProtect) {
48
- addMiddleware({
49
- entrypoint: "astro-sessionkit/guard",
50
- order: "pre",
51
- });
52
- }
53
- },
54
- },
55
- };
56
- }
7
+ export default sessionkit;
57
8
 
58
9
  // ============================================================================
59
10
  // Re-export types for convenience
@@ -76,4 +27,4 @@ export type {
76
27
  // Version export
77
28
  // ============================================================================
78
29
 
79
- export const version = "0.1.0";
30
+ export const version = "0.1.20";
@@ -44,16 +44,31 @@ export default function sessionKit(config: SessionKitConfig = {}): AstroIntegrat
44
44
  });
45
45
 
46
46
  // 2. Add route guard if there are protection rules or global protection is enabled
47
- if ((resolvedConfig.protect && resolvedConfig.protect.length > 0) || resolvedConfig.globalProtect) {
47
+ const hasRules = (resolvedConfig.protect && resolvedConfig.protect.length > 0);
48
+ const isGlobal = !!resolvedConfig.globalProtect;
49
+
50
+ if (hasRules || isGlobal) {
48
51
  addMiddleware({
49
52
  entrypoint: "astro-sessionkit/guard",
50
53
  order: "pre",
51
54
  });
55
+ } else if (resolvedConfig.debug) {
56
+ console.log("[SessionKit] Route guard NOT registered: no rules and globalProtect is false.");
52
57
  }
53
58
  },
54
59
  },
55
60
  };
56
61
  }
57
62
 
58
- // Re-export types for convenience
59
- export type { Session, ProtectionRule, SessionKitConfig } from "./core/types";
63
+ export type {
64
+ Session,
65
+ ProtectionRule,
66
+ RoleProtectionRule,
67
+ RolesProtectionRule,
68
+ PermissionProtectionRule,
69
+ PermissionsProtectionRule,
70
+ CustomProtectionRule,
71
+ SessionKitConfig,
72
+ AccessHooks,
73
+ SessionContext
74
+ } from "./core/types";
package/src/server.ts CHANGED
@@ -4,8 +4,7 @@
4
4
 
5
5
  import {getContextStore} from "./core/context";
6
6
  import {isValidSessionStructure} from "./core/validation";
7
- import type {Session} from "./core/types";
8
- import type {APIContext} from "astro";
7
+ import type {Session, SessionKitContext} from "./core/types";
9
8
 
10
9
  /**
11
10
  * Get the current session (returns null if not authenticated)
@@ -129,10 +128,10 @@ export function hasRolePermission(role: string, permission: string): boolean {
129
128
  * Use this after successful authentication to register the user's session.
130
129
  * This does NOT handle session storage (cookies, Redis, etc.) - you must do that separately.
131
130
  *
132
- * @param context - Astro API context
133
131
  * @param session - Session data to set
132
+ * @param context - Astro API context (optional if called within request context)
134
133
  *
135
- * @throws {Error} If session structure is invalid
134
+ * @throws {Error} If session structure is invalid or context missing
136
135
  *
137
136
  * @example
138
137
  * ```ts
@@ -143,7 +142,7 @@ export function hasRolePermission(role: string, permission: string): boolean {
143
142
  *
144
143
  * if (user) {
145
144
  * // Register session with SessionKit
146
- * setSession(context, {
145
+ * setSession({
147
146
  * userId: user.id,
148
147
  * email: user.email,
149
148
  * role: user.role,
@@ -158,7 +157,17 @@ export function hasRolePermission(role: string, permission: string): boolean {
158
157
  * };
159
158
  * ```
160
159
  */
161
- export function setSession(context: APIContext, session: Session): void {
160
+ export function setSession(session: Session, context?: SessionKitContext): void {
161
+ const store = getContextStore();
162
+ const ctx = context || store?.astroContext;
163
+
164
+ if (!ctx) {
165
+ throw new Error(
166
+ '[SessionKit] Cannot set session: Astro context is missing. ' +
167
+ 'Provide it as a second argument or ensure sessionMiddleware is running.'
168
+ );
169
+ }
170
+
162
171
  // Validate session structure
163
172
  if (!isValidSessionStructure(session)) {
164
173
  throw new Error(
@@ -166,8 +175,13 @@ export function setSession(context: APIContext, session: Session): void {
166
175
  );
167
176
  }
168
177
 
169
- // Set in context.locals for SessionKit middleware to read
170
- context.session?.set('__session__', session);
178
+ // Update ALS store if available for same-request consistency
179
+ if (store) {
180
+ store.session = session;
181
+ }
182
+
183
+ // Set in context.session for Astro to persist
184
+ ctx.session?.set('__session__', session);
171
185
  }
172
186
 
173
187
  /**
@@ -176,14 +190,14 @@ export function setSession(context: APIContext, session: Session): void {
176
190
  * Use this during logout. This does NOT delete session storage (cookies, Redis, etc.) -
177
191
  * you must do that separately.
178
192
  *
179
- * @param context - Astro API context
193
+ * @param context - Astro API context (optional if called within request context)
180
194
  *
181
195
  * @example
182
196
  * ```ts
183
197
  * // In logout endpoint
184
198
  * export const POST: APIRoute = async (context) => {
185
199
  * // Clear from SessionKit
186
- * clearSession(context);
200
+ * clearSession();
187
201
  *
188
202
  * // YOU must also delete the session storage
189
203
  * context.cookies.delete('session_id');
@@ -193,8 +207,61 @@ export function setSession(context: APIContext, session: Session): void {
193
207
  * };
194
208
  * ```
195
209
  */
196
- export function clearSession(context: APIContext): void {
197
- context.session?.delete('__session__');
210
+ export function clearSession(context?: SessionKitContext): void {
211
+ const store = getContextStore();
212
+ const ctx = context || store?.astroContext;
213
+
214
+ if (!ctx) {
215
+ throw new Error(
216
+ '[SessionKit] Cannot clear session: Astro context is missing. ' +
217
+ 'Provide it as an argument or ensure sessionMiddleware is running.'
218
+ );
219
+ }
220
+
221
+ // Update ALS store if available for same-request consistency
222
+ if (store) {
223
+ store.session = null;
224
+ }
225
+
226
+ ctx.session?.delete('__session__');
227
+ }
228
+
229
+ /**
230
+ * Regenerate the session ID to prevent session fixation attacks
231
+ *
232
+ * Use this after a successful login or privilege change.
233
+ * This is only supported if the underlying Astro session driver supports it.
234
+ *
235
+ * @param context - Astro API context (optional if called within request context)
236
+ *
237
+ * @example
238
+ * ```ts
239
+ * // In login endpoint
240
+ * export const POST: APIRoute = async (context) => {
241
+ * const user = await authenticate(request);
242
+ * if (user) {
243
+ * // 1. Regenerate session ID
244
+ * regenerateSession();
245
+ *
246
+ * // 2. Set new session data
247
+ * setSession({ userId: user.id, role: user.role });
248
+ * }
249
+ * }
250
+ * ```
251
+ */
252
+ export function regenerateSession(context?: SessionKitContext): void {
253
+ const ctx = context || getContextStore()?.astroContext;
254
+
255
+ if (!ctx) {
256
+ throw new Error(
257
+ '[SessionKit] Cannot regenerate session: Astro context is missing. ' +
258
+ 'Provide it as an argument or ensure sessionMiddleware is running.'
259
+ );
260
+ }
261
+
262
+ if (ctx.session?.regenerate) {
263
+ ctx.session.regenerate();
264
+ }
198
265
  }
199
266
 
200
267
  /**
@@ -203,8 +270,8 @@ export function clearSession(context: APIContext): void {
203
270
  * Useful for updating session data without replacing the entire session.
204
271
  * The updated session is validated before being set.
205
272
  *
206
- * @param context - Astro API context
207
273
  * @param updates - Partial session data to merge
274
+ * @param context - Astro API context (optional if called within request context)
208
275
  *
209
276
  * @throws {Error} If no session exists or updated session is invalid
210
277
  *
@@ -212,7 +279,7 @@ export function clearSession(context: APIContext): void {
212
279
  * ```ts
213
280
  * // Update user's role after promotion
214
281
  * export const POST: APIRoute = async (context) => {
215
- * updateSession(context, {
282
+ * updateSession({
216
283
  * role: 'admin',
217
284
  * permissions: ['admin:read', 'admin:write']
218
285
  * });
@@ -224,22 +291,39 @@ export function clearSession(context: APIContext): void {
224
291
  * };
225
292
  * ```
226
293
  */
227
- export function updateSession(context: APIContext, updates: Partial<Session>): void {
228
- const currentSession = context.session?.get<Session>('__session__');
294
+ export function updateSession(updates: Partial<Session>, context?: SessionKitContext): void {
295
+ const store = getContextStore();
296
+ const ctx = context || store?.astroContext;
229
297
 
230
- if (!currentSession) {
231
- throw new Error('[SessionKit] Cannot update session: no session exists');
298
+ if (!ctx) {
299
+ throw new Error(
300
+ '[SessionKit] Cannot update session: Astro context is missing. ' +
301
+ 'Provide it as a second argument or ensure sessionMiddleware is running.'
302
+ );
232
303
  }
233
304
 
234
- // Merge updates with current session
235
- const updatedSession = {...currentSession, ...updates};
305
+ // Get current session from ALS (preferred) or Astro session
306
+ const currentSession = store?.session || ctx.session?.get<Session>('__session__');
236
307
 
237
- // Validate merged session
238
- if (!isValidSessionStructure(updatedSession)) {
239
- throw new Error(
240
- '[SessionKit] Invalid session structure after update. Ensure all fields are valid.'
241
- );
308
+ // Note: ctx.session.get might return a Promise in some Astro versions/drivers.
309
+ // However, since sessionMiddleware already awaits it, store.session should be populated.
310
+ // If store.session is missing but we are in a middleware-managed request, it means no session exists.
311
+
312
+ if (!currentSession || (currentSession instanceof Promise)) {
313
+ // If it's a promise, we might have a sync/async mismatch, but usually getSession() handles this.
314
+ // For robustness, we check if we actually have a session object.
315
+ const session = currentSession instanceof Promise ? null : currentSession;
316
+ if (!session) {
317
+ throw new Error('[SessionKit] Cannot update session: no session exists');
318
+ }
242
319
  }
243
320
 
244
- context.session?.set('__session__', updatedSession);
321
+ // We can safely cast here if it's not a promise
322
+ const session = currentSession as Session;
323
+
324
+ // Merge updates with current session
325
+ const updatedSession = {...session, ...updates};
326
+
327
+ // Use setSession to handle validation and both store updates
328
+ setSession(updatedSession, ctx);
245
329
  }