astro-sessionkit 0.1.20 → 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.
@@ -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/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
  }