astro-sessionkit 0.1.15 → 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.
@@ -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";
@@ -0,0 +1,5 @@
1
+ // ============================================================================
2
+ // Middleware Entry Point
3
+ // ============================================================================
4
+
5
+ export { sessionMiddleware as onRequest } from "./core/sessionMiddleware";
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
+ }