astro-sessionkit 0.1.14 → 0.1.16
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 +3 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +40 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/context.js.map +1 -1
- package/dist/core/guardMiddleware.d.ts.map +1 -1
- package/dist/core/guardMiddleware.js +15 -3
- package/dist/core/guardMiddleware.js.map +1 -1
- package/dist/core/logger.d.ts +4 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +11 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/matcher.js.map +1 -1
- package/dist/core/sessionMiddleware.d.ts.map +1 -1
- package/dist/core/sessionMiddleware.js +4 -4
- package/dist/core/sessionMiddleware.js.map +1 -1
- package/dist/core/types.d.ts +3 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/validation.js.map +1 -1
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/integration.js +1 -1
- package/dist/integration.js.map +1 -1
- package/dist/server.js.map +1 -1
- package/package.json +2 -1
- package/readme.md +40 -1
- package/src/core/config.ts +153 -0
- package/src/core/context.ts +30 -0
- package/src/core/guardMiddleware.ts +131 -0
- package/src/core/logger.ts +31 -0
- package/src/core/matcher.ts +61 -0
- package/src/core/sessionMiddleware.ts +65 -0
- package/src/core/types.ts +147 -0
- package/src/core/validation.ts +137 -0
- package/src/guard.ts +7 -0
- package/src/index.ts +78 -0
- package/src/integration.ts +58 -0
- package/src/middleware.ts +5 -0
- package/src/server.ts +245 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Security Validation Utilities
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import type {Session} from "./types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Validate that session has the expected structure
|
|
9
|
+
* Prevents crashes from malformed data and DoS attacks
|
|
10
|
+
*/
|
|
11
|
+
export function isValidSessionStructure(input: unknown): input is Session {
|
|
12
|
+
// Must be an object
|
|
13
|
+
if (!input || typeof input !== 'object') {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const session = input as any;
|
|
18
|
+
|
|
19
|
+
// Required: userId must be a non-empty string
|
|
20
|
+
if (typeof session.userId !== 'string' || !session.userId.trim()) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// DoS protection: Limit userId length
|
|
25
|
+
if (session.userId.length > 255) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Optional fields validation (if present)
|
|
30
|
+
if (session.email !== undefined) {
|
|
31
|
+
if (typeof session.email !== 'string') {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
// Reasonable email length limit
|
|
35
|
+
if (session.email.length > 320) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (session.role !== undefined && session.role !== null) {
|
|
41
|
+
if (typeof session.role !== 'string') {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
// Reasonable role length limit
|
|
45
|
+
if (session.role.length > 100) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (session.roles !== undefined && session.roles !== null) {
|
|
51
|
+
if (!Array.isArray(session.roles)) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
// DoS protection: Limit array size
|
|
55
|
+
if (session.roles.length > 100) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
// All items must be strings with reasonable length
|
|
59
|
+
if (!session.roles.every((r: any) => typeof r === 'string' && r.length <= 100)) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (session.permissions !== undefined && session.permissions !== null) {
|
|
65
|
+
if (!Array.isArray(session.permissions)) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
// DoS protection: Limit array size
|
|
69
|
+
if (session.permissions.length > 500) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
// All items must be strings with reasonable length
|
|
73
|
+
if (!session.permissions.every((p: any) => typeof p === 'string' && p.length <= 200)) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Validate route patterns to prevent ReDoS attacks
|
|
83
|
+
*/
|
|
84
|
+
export function isValidPattern(pattern: string): boolean {
|
|
85
|
+
|
|
86
|
+
if (typeof pattern !== 'string' || pattern.length === 0) return false;
|
|
87
|
+
|
|
88
|
+
// Length limit
|
|
89
|
+
if (pattern.length > 1000) return false;
|
|
90
|
+
|
|
91
|
+
// Must start with /
|
|
92
|
+
if (!pattern.startsWith("/")) return false;
|
|
93
|
+
|
|
94
|
+
// ReDoS protection: reject 4+ consecutive asterisks anywhere
|
|
95
|
+
if (/\*{4,}/.test(pattern)) return false;
|
|
96
|
+
|
|
97
|
+
// Additional sanity: ensure any '*' run is 1..3
|
|
98
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
99
|
+
if (pattern[i] !== "*") continue;
|
|
100
|
+
|
|
101
|
+
let j = i;
|
|
102
|
+
while (j < pattern.length && pattern[j] === "*") j++;
|
|
103
|
+
|
|
104
|
+
const run = j - i; // consecutive '*'
|
|
105
|
+
if (run < 1 || run > 3) return false; // (4+ already blocked above)
|
|
106
|
+
|
|
107
|
+
// Optional: keep your patterns segment-oriented.
|
|
108
|
+
// Wildcards should not be glued to letters (e.g., "/**abc").
|
|
109
|
+
const next = pattern[j];
|
|
110
|
+
if (next !== undefined && next !== "/") return false;
|
|
111
|
+
|
|
112
|
+
i = j - 1;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Validate redirect path (open redirect protection)
|
|
120
|
+
*/
|
|
121
|
+
export function isValidRedirectPath(path: string): boolean {
|
|
122
|
+
|
|
123
|
+
if (typeof path !== 'string') return false;
|
|
124
|
+
|
|
125
|
+
// Reasonable length limit
|
|
126
|
+
if (path.length === 0 || path.length > 500) return false;
|
|
127
|
+
|
|
128
|
+
// Must be a site-relative path (exactly one leading slash)
|
|
129
|
+
// Reject protocol-relative URLs like "//example.com"
|
|
130
|
+
if (!path.startsWith("/") || path.startsWith("//")) return false;
|
|
131
|
+
|
|
132
|
+
// Extra hardening: reject anything that looks like a URL scheme
|
|
133
|
+
// (e.g. "http://", "https://", "javascript:", "data:", etc.)
|
|
134
|
+
return !/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(path);
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
}
|
package/src/guard.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Guard Middleware Entry Point
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import { createGuardMiddleware } from "./core/guardMiddleware";
|
|
6
|
+
|
|
7
|
+
export const onRequest = createGuardMiddleware();
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Astro SessionKit - Main Integration Entry Point
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import type {AstroIntegration } from "astro";
|
|
6
|
+
import { setConfig } from "./core/config";
|
|
7
|
+
import type { SessionKitConfig } from "./core/types";
|
|
8
|
+
|
|
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
|
+
|
|
35
|
+
return {
|
|
36
|
+
name: "astro-sessionkit",
|
|
37
|
+
hooks: {
|
|
38
|
+
"astro:config:setup": ({ addMiddleware }) => {
|
|
39
|
+
// 1. Always add session context middleware first
|
|
40
|
+
addMiddleware({
|
|
41
|
+
entrypoint: "astro-sessionkit/middleware",
|
|
42
|
+
order: "pre",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// 2. Add route guard if there are protection rules or global protection is enabled
|
|
46
|
+
if ((config.protect && config.protect.length > 0) || config.globalProtect) {
|
|
47
|
+
addMiddleware({
|
|
48
|
+
entrypoint: "astro-sessionkit/guard",
|
|
49
|
+
order: "pre",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// Re-export types for convenience
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
export type {
|
|
62
|
+
Session,
|
|
63
|
+
ProtectionRule,
|
|
64
|
+
RoleProtectionRule,
|
|
65
|
+
RolesProtectionRule,
|
|
66
|
+
PermissionProtectionRule,
|
|
67
|
+
PermissionsProtectionRule,
|
|
68
|
+
CustomProtectionRule,
|
|
69
|
+
SessionKitConfig,
|
|
70
|
+
AccessHooks,
|
|
71
|
+
SessionContext
|
|
72
|
+
} from "./core/types";
|
|
73
|
+
|
|
74
|
+
// ============================================================================
|
|
75
|
+
// Version export
|
|
76
|
+
// ============================================================================
|
|
77
|
+
|
|
78
|
+
export const version = "0.1.0";
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Astro Integration
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import type { AstroIntegration } from "astro";
|
|
6
|
+
import { setConfig } from "./core/config";
|
|
7
|
+
import type { SessionKitConfig } from "./core/types";
|
|
8
|
+
|
|
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
|
+
|
|
35
|
+
return {
|
|
36
|
+
name: "astro-sessionkit",
|
|
37
|
+
hooks: {
|
|
38
|
+
"astro:config:setup": ({ addMiddleware }) => {
|
|
39
|
+
// 1. Always add session context middleware first
|
|
40
|
+
addMiddleware({
|
|
41
|
+
entrypoint: "astro-sessionkit/middleware",
|
|
42
|
+
order: "pre",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// 2. Add route guard if there are protection rules or global protection is enabled
|
|
46
|
+
if ((config.protect && config.protect.length > 0) || config.globalProtect) {
|
|
47
|
+
addMiddleware({
|
|
48
|
+
entrypoint: "astro-sessionkit/guard",
|
|
49
|
+
order: "pre",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Re-export types for convenience
|
|
58
|
+
export type { Session, ProtectionRule, SessionKitConfig } from "./core/types";
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Public Server API - Use these in your Astro components/endpoints
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import {getContextStore} from "./core/context";
|
|
6
|
+
import {isValidSessionStructure} from "./core/validation";
|
|
7
|
+
import type {Session} from "./core/types";
|
|
8
|
+
import type {APIContext} from "astro";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get the current session (returns null if not authenticated)
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* // In .astro component
|
|
16
|
+
* const session = getSession();
|
|
17
|
+
* if (session) {
|
|
18
|
+
* console.log('User ID:', session.userId);
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function getSession(): Session | null {
|
|
23
|
+
const context = getContextStore();
|
|
24
|
+
return context?.session ?? null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get the current session or throw if not authenticated
|
|
29
|
+
*
|
|
30
|
+
* @throws {Response} 401 Unauthorized if no session
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* // In API endpoint
|
|
35
|
+
* const session = requireSession();
|
|
36
|
+
* // TypeScript knows session is not null here
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function requireSession(): Session {
|
|
40
|
+
const session = getSession();
|
|
41
|
+
|
|
42
|
+
if (!session) {
|
|
43
|
+
throw new Response("Unauthorized", {status: 401});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return session;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Check if user is authenticated
|
|
51
|
+
*/
|
|
52
|
+
export function isAuthenticated(): boolean {
|
|
53
|
+
return getSession() !== null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if user has a specific role
|
|
58
|
+
*/
|
|
59
|
+
export function hasRole(role: string): boolean {
|
|
60
|
+
const session = getSession();
|
|
61
|
+
if (!session) return false;
|
|
62
|
+
|
|
63
|
+
// Check primary role
|
|
64
|
+
if (session.role === role) return true;
|
|
65
|
+
|
|
66
|
+
// Check additional roles
|
|
67
|
+
return session.roles?.includes(role) ?? false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check if user has a specific permission
|
|
72
|
+
*/
|
|
73
|
+
export function hasPermission(permission: string): boolean {
|
|
74
|
+
const session = getSession();
|
|
75
|
+
if (!session) return false;
|
|
76
|
+
|
|
77
|
+
return session.permissions?.includes(permission) ?? false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if user has ALL of the specified permissions
|
|
82
|
+
*/
|
|
83
|
+
export function hasAllPermissions(...permissions: string[]): boolean {
|
|
84
|
+
const session = getSession();
|
|
85
|
+
if (!session) return false;
|
|
86
|
+
|
|
87
|
+
const userPermissions = session.permissions ?? [];
|
|
88
|
+
return permissions.every((p) => userPermissions.includes(p));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Check if user has ANY of the specified permissions
|
|
93
|
+
*/
|
|
94
|
+
export function hasAnyPermission(...permissions: string[]): boolean {
|
|
95
|
+
const session = getSession();
|
|
96
|
+
if (!session) return false;
|
|
97
|
+
|
|
98
|
+
const userPermissions = session.permissions ?? [];
|
|
99
|
+
return permissions.some((p) => userPermissions.includes(p));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ============================================================================
|
|
103
|
+
// Session Management
|
|
104
|
+
// ============================================================================
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check if a specific role has a specific permission.
|
|
108
|
+
*
|
|
109
|
+
* This checks if the current user has the specified role and if that role
|
|
110
|
+
* is associated with the specified permission.
|
|
111
|
+
*
|
|
112
|
+
* @param role - The role to check
|
|
113
|
+
* @param permission - The permission to check
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```ts
|
|
117
|
+
* if (hasRolePermission("admin", "delete users")) {
|
|
118
|
+
* // ...
|
|
119
|
+
* }
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
export function hasRolePermission(role: string, permission: string): boolean {
|
|
123
|
+
return hasRole(role) && hasPermission(permission);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Set session data in context.locals.session
|
|
128
|
+
*
|
|
129
|
+
* Use this after successful authentication to register the user's session.
|
|
130
|
+
* This does NOT handle session storage (cookies, Redis, etc.) - you must do that separately.
|
|
131
|
+
*
|
|
132
|
+
* @param context - Astro API context
|
|
133
|
+
* @param session - Session data to set
|
|
134
|
+
*
|
|
135
|
+
* @throws {Error} If session structure is invalid
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```ts
|
|
139
|
+
* // In API endpoint after verifying credentials
|
|
140
|
+
* export const POST: APIRoute = async (context) => {
|
|
141
|
+
* const { email, password } = await context.request.json();
|
|
142
|
+
* const user = await verifyCredentials(email, password);
|
|
143
|
+
*
|
|
144
|
+
* if (user) {
|
|
145
|
+
* // Register session with SessionKit
|
|
146
|
+
* setSession(context, {
|
|
147
|
+
* userId: user.id,
|
|
148
|
+
* email: user.email,
|
|
149
|
+
* role: user.role,
|
|
150
|
+
* permissions: user.permissions
|
|
151
|
+
* });
|
|
152
|
+
*
|
|
153
|
+
* // YOU must also store the session (cookie, Redis, etc.)
|
|
154
|
+
* context.cookies.set('session_id', sessionId, { httpOnly: true });
|
|
155
|
+
*
|
|
156
|
+
* return new Response(JSON.stringify({ success: true }));
|
|
157
|
+
* }
|
|
158
|
+
* };
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
export function setSession(context: APIContext, session: Session): void {
|
|
162
|
+
// Validate session structure
|
|
163
|
+
if (!isValidSessionStructure(session)) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
'[SessionKit] Invalid session structure. Session must have a valid userId and follow the Session interface.'
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Set in context.locals for SessionKit middleware to read
|
|
170
|
+
context.session?.set('__session__', session);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Clear session from context.locals.session
|
|
175
|
+
*
|
|
176
|
+
* Use this during logout. This does NOT delete session storage (cookies, Redis, etc.) -
|
|
177
|
+
* you must do that separately.
|
|
178
|
+
*
|
|
179
|
+
* @param context - Astro API context
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* ```ts
|
|
183
|
+
* // In logout endpoint
|
|
184
|
+
* export const POST: APIRoute = async (context) => {
|
|
185
|
+
* // Clear from SessionKit
|
|
186
|
+
* clearSession(context);
|
|
187
|
+
*
|
|
188
|
+
* // YOU must also delete the session storage
|
|
189
|
+
* context.cookies.delete('session_id');
|
|
190
|
+
* await db.deleteSession(sessionId);
|
|
191
|
+
*
|
|
192
|
+
* return context.redirect('/');
|
|
193
|
+
* };
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
196
|
+
export function clearSession(context: APIContext): void {
|
|
197
|
+
context.session?.delete('__session__');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Update specific fields in the current session
|
|
202
|
+
*
|
|
203
|
+
* Useful for updating session data without replacing the entire session.
|
|
204
|
+
* The updated session is validated before being set.
|
|
205
|
+
*
|
|
206
|
+
* @param context - Astro API context
|
|
207
|
+
* @param updates - Partial session data to merge
|
|
208
|
+
*
|
|
209
|
+
* @throws {Error} If no session exists or updated session is invalid
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* ```ts
|
|
213
|
+
* // Update user's role after promotion
|
|
214
|
+
* export const POST: APIRoute = async (context) => {
|
|
215
|
+
* updateSession(context, {
|
|
216
|
+
* role: 'admin',
|
|
217
|
+
* permissions: ['admin:read', 'admin:write']
|
|
218
|
+
* });
|
|
219
|
+
*
|
|
220
|
+
* // YOU must also update session storage
|
|
221
|
+
* await db.updateSession(sessionId, updatedData);
|
|
222
|
+
*
|
|
223
|
+
* return new Response(JSON.stringify({ success: true }));
|
|
224
|
+
* };
|
|
225
|
+
* ```
|
|
226
|
+
*/
|
|
227
|
+
export function updateSession(context: APIContext, updates: Partial<Session>): void {
|
|
228
|
+
const currentSession = context.session?.get<Session>('__session__');
|
|
229
|
+
|
|
230
|
+
if (!currentSession) {
|
|
231
|
+
throw new Error('[SessionKit] Cannot update session: no session exists');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Merge updates with current session
|
|
235
|
+
const updatedSession = {...currentSession, ...updates};
|
|
236
|
+
|
|
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
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
context.session?.set('__session__', updatedSession);
|
|
245
|
+
}
|