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.
- 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 -9
- 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 +12 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -24
- package/dist/index.js.map +1 -1
- package/dist/integration.d.ts +1 -1
- package/dist/integration.d.ts.map +1 -1
- package/dist/integration.js +12 -4
- package/dist/integration.js.map +1 -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 -35
- 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/index.ts +3 -52
- package/src/integration.ts +18 -3
- 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
|
|
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 (
|
|
75
|
+
if (getContextStore && !runWithContext) {
|
|
51
76
|
// Initialize context store if setter is provided
|
|
52
|
-
const store =
|
|
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 (
|
|
56
|
-
|
|
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
|
|
91
|
+
return runLogic();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (debug) {
|
|
95
|
+
logger.debug('[SessionMiddleware] Using' + (runWithContext ? ' custom ' : ' default ') + 'runner');
|
|
61
96
|
}
|
|
62
97
|
|
|
63
|
-
const runner =
|
|
64
|
-
return runner(
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
}
|
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/index.ts
CHANGED
|
@@ -2,58 +2,9 @@
|
|
|
2
2
|
// Astro SessionKit - Main Integration Entry Point
|
|
3
3
|
// ============================================================================
|
|
4
4
|
|
|
5
|
-
import
|
|
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.
|
|
30
|
+
export const version = "0.1.20";
|
package/src/integration.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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(
|
|
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
|
}
|