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.
- package/dist/core/config.d.ts +2 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +16 -3
- package/dist/core/config.js.map +1 -1
- package/dist/core/context.d.ts +2 -1
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/context.js +9 -4
- package/dist/core/context.js.map +1 -1
- package/dist/core/guardMiddleware.d.ts.map +1 -1
- package/dist/core/guardMiddleware.js +30 -10
- package/dist/core/guardMiddleware.js.map +1 -1
- package/dist/core/matcher.d.ts.map +1 -1
- package/dist/core/matcher.js +7 -1
- package/dist/core/matcher.js.map +1 -1
- package/dist/core/sessionMiddleware.d.ts.map +1 -1
- package/dist/core/sessionMiddleware.js +38 -10
- package/dist/core/sessionMiddleware.js.map +1 -1
- package/dist/core/types.d.ts +9 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/validation.d.ts.map +1 -1
- package/dist/core/validation.js +10 -1
- package/dist/core/validation.js.map +1 -1
- package/dist/index.d.ts +9 -1
- package/dist/server.d.ts +12 -5
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +47 -12
- package/dist/server.js.map +1 -1
- package/package.json +4 -2
- package/src/core/config.ts +30 -4
- package/src/core/context.ts +29 -6
- package/src/core/guardMiddleware.ts +56 -36
- package/src/core/matcher.ts +8 -1
- package/src/core/sessionMiddleware.ts +45 -10
- package/src/core/types.ts +27 -5
- package/src/core/validation.ts +14 -1
- package/src/server.ts +110 -26
package/src/core/validation.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
-
//
|
|
170
|
-
|
|
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(
|
|
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
|
|
197
|
-
|
|
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(
|
|
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(
|
|
228
|
-
const
|
|
294
|
+
export function updateSession(updates: Partial<Session>, context?: SessionKitContext): void {
|
|
295
|
+
const store = getContextStore();
|
|
296
|
+
const ctx = context || store?.astroContext;
|
|
229
297
|
|
|
230
|
-
if (!
|
|
231
|
-
throw new Error(
|
|
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
|
-
//
|
|
235
|
-
const
|
|
305
|
+
// Get current session from ALS (preferred) or Astro session
|
|
306
|
+
const currentSession = store?.session || ctx.session?.get<Session>('__session__');
|
|
236
307
|
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
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
|
}
|