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/index.d.ts CHANGED
@@ -1,5 +1,11 @@
1
- import { AstroIntegration } from 'astro';
1
+ import { AstroCookies, AstroSession, AstroIntegration } from 'astro';
2
2
 
3
+ interface SessionKitContext {
4
+ cookies: AstroCookies;
5
+ session?: AstroSession;
6
+ redirect: (path: string, status?: number) => Response;
7
+ [key: string]: any;
8
+ }
3
9
  interface Session {
4
10
  userId: string;
5
11
  email?: string;
@@ -10,6 +16,7 @@ interface Session {
10
16
  }
11
17
  interface SessionContext {
12
18
  session: Session | null;
19
+ astroContext?: SessionKitContext;
13
20
  }
14
21
  interface BaseProtectionRule {
15
22
  pattern: string;
@@ -45,6 +52,7 @@ interface SessionKitConfig {
45
52
  setContextStore?: (context: SessionContext) => void;
46
53
  globalProtect?: boolean;
47
54
  exclude?: string[];
55
+ context?: any;
48
56
  debug?: boolean;
49
57
  }
50
58
 
package/dist/server.d.ts CHANGED
@@ -1,5 +1,11 @@
1
- import { APIContext } from 'astro';
1
+ import { AstroCookies, AstroSession } from 'astro';
2
2
 
3
+ interface SessionKitContext {
4
+ cookies: AstroCookies;
5
+ session?: AstroSession;
6
+ redirect: (path: string, status?: number) => Response;
7
+ [key: string]: any;
8
+ }
3
9
  interface Session {
4
10
  userId: string;
5
11
  email?: string;
@@ -17,8 +23,9 @@ declare function hasPermission(permission: string): boolean;
17
23
  declare function hasAllPermissions(...permissions: string[]): boolean;
18
24
  declare function hasAnyPermission(...permissions: string[]): boolean;
19
25
  declare function hasRolePermission(role: string, permission: string): boolean;
20
- declare function setSession(context: APIContext, session: Session): void;
21
- declare function clearSession(context: APIContext): void;
22
- declare function updateSession(context: APIContext, updates: Partial<Session>): void;
26
+ declare function setSession(session: Session, context?: SessionKitContext): void;
27
+ declare function clearSession(context?: SessionKitContext): void;
28
+ declare function regenerateSession(context?: SessionKitContext): void;
29
+ declare function updateSession(updates: Partial<Session>, context?: SessionKitContext): void;
23
30
 
24
- export { clearSession, getSession, hasAllPermissions, hasAnyPermission, hasPermission, hasRole, hasRolePermission, isAuthenticated, requireSession, setSession, updateSession };
31
+ export { clearSession, getSession, hasAllPermissions, hasAnyPermission, hasPermission, hasRole, hasRolePermission, isAuthenticated, regenerateSession, requireSession, setSession, updateSession };
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,cAAc,CAAC;AAC1C,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,OAAO,CAAC;AActC,wBAAgB,UAAU,IAAI,OAAO,GAAG,IAAI,CAG3C;AAcD,wBAAgB,cAAc,IAAI,OAAO,CAQxC;AAKD,wBAAgB,eAAe,IAAI,OAAO,CAEzC;AAKD,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAS7C;AAKD,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAKzD;AAKD,wBAAgB,iBAAiB,CAAC,GAAG,WAAW,EAAE,MAAM,EAAE,GAAG,OAAO,CAMnE;AAKD,wBAAgB,gBAAgB,CAAC,GAAG,WAAW,EAAE,MAAM,EAAE,GAAG,OAAO,CAMlE;AAsBD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAE3E;AAqCD,wBAAgB,UAAU,CAAC,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAUtE;AAyBD,wBAAgB,YAAY,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI,CAEtD;AA6BD,wBAAgB,aAAa,CAAC,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAkBlF"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAC,OAAO,EAAE,iBAAiB,EAAC,MAAM,cAAc,CAAC;AAc7D,wBAAgB,UAAU,IAAI,OAAO,GAAG,IAAI,CAG3C;AAcD,wBAAgB,cAAc,IAAI,OAAO,CAQxC;AAKD,wBAAgB,eAAe,IAAI,OAAO,CAEzC;AAKD,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAS7C;AAKD,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAKzD;AAKD,wBAAgB,iBAAiB,CAAC,GAAG,WAAW,EAAE,MAAM,EAAE,GAAG,OAAO,CAMnE;AAKD,wBAAgB,gBAAgB,CAAC,GAAG,WAAW,EAAE,MAAM,EAAE,GAAG,OAAO,CAMlE;AAsBD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAE3E;AAqCD,wBAAgB,UAAU,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAyB9E;AAyBD,wBAAgB,YAAY,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAiB9D;AAyBD,wBAAgB,iBAAiB,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAanE;AA6BD,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAmC1F"}
package/dist/server.js CHANGED
@@ -46,26 +46,61 @@ function hasAnyPermission(...permissions) {
46
46
  function hasRolePermission(role, permission) {
47
47
  return hasRole(role) && hasPermission(permission);
48
48
  }
49
- function setSession(context, session) {
49
+ function setSession(session, context) {
50
+ const store = getContextStore();
51
+ const ctx = context || store?.astroContext;
52
+ if (!ctx) {
53
+ throw new Error('[SessionKit] Cannot set session: Astro context is missing. ' +
54
+ 'Provide it as a second argument or ensure sessionMiddleware is running.');
55
+ }
50
56
  if (!isValidSessionStructure(session)) {
51
57
  throw new Error('[SessionKit] Invalid session structure. Session must have a valid userId and follow the Session interface.');
52
58
  }
53
- context.session?.set('__session__', session);
59
+ if (store) {
60
+ store.session = session;
61
+ }
62
+ ctx.session?.set('__session__', session);
54
63
  }
55
64
  function clearSession(context) {
56
- context.session?.delete('__session__');
65
+ const store = getContextStore();
66
+ const ctx = context || store?.astroContext;
67
+ if (!ctx) {
68
+ throw new Error('[SessionKit] Cannot clear session: Astro context is missing. ' +
69
+ 'Provide it as an argument or ensure sessionMiddleware is running.');
70
+ }
71
+ if (store) {
72
+ store.session = null;
73
+ }
74
+ ctx.session?.delete('__session__');
75
+ }
76
+ function regenerateSession(context) {
77
+ const ctx = context || getContextStore()?.astroContext;
78
+ if (!ctx) {
79
+ throw new Error('[SessionKit] Cannot regenerate session: Astro context is missing. ' +
80
+ 'Provide it as an argument or ensure sessionMiddleware is running.');
81
+ }
82
+ if (ctx.session?.regenerate) {
83
+ ctx.session.regenerate();
84
+ }
57
85
  }
58
- function updateSession(context, updates) {
59
- const currentSession = context.session?.get('__session__');
60
- if (!currentSession) {
61
- throw new Error('[SessionKit] Cannot update session: no session exists');
86
+ function updateSession(updates, context) {
87
+ const store = getContextStore();
88
+ const ctx = context || store?.astroContext;
89
+ if (!ctx) {
90
+ throw new Error('[SessionKit] Cannot update session: Astro context is missing. ' +
91
+ 'Provide it as a second argument or ensure sessionMiddleware is running.');
62
92
  }
63
- const updatedSession = { ...currentSession, ...updates };
64
- if (!isValidSessionStructure(updatedSession)) {
65
- throw new Error('[SessionKit] Invalid session structure after update. Ensure all fields are valid.');
93
+ const currentSession = store?.session || ctx.session?.get('__session__');
94
+ if (!currentSession || (currentSession instanceof Promise)) {
95
+ const session = currentSession instanceof Promise ? null : currentSession;
96
+ if (!session) {
97
+ throw new Error('[SessionKit] Cannot update session: no session exists');
98
+ }
66
99
  }
67
- context.session?.set('__session__', updatedSession);
100
+ const session = currentSession;
101
+ const updatedSession = { ...session, ...updates };
102
+ setSession(updatedSession, ctx);
68
103
  }
69
104
 
70
- export { clearSession, getSession, hasAllPermissions, hasAnyPermission, hasPermission, hasRole, hasRolePermission, isAuthenticated, requireSession, setSession, updateSession };
105
+ export { clearSession, getSession, hasAllPermissions, hasAnyPermission, hasPermission, hasRole, hasRolePermission, isAuthenticated, regenerateSession, requireSession, setSession, updateSession };
71
106
  //# sourceMappingURL=server.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sources":["../src/server.ts"],"sourcesContent":["// ============================================================================\n// Public Server API - Use these in your Astro components/endpoints\n// ============================================================================\n\nimport {getContextStore} from \"./core/context\";\nimport {isValidSessionStructure} from \"./core/validation\";\nimport type {Session} from \"./core/types\";\nimport type {APIContext} from \"astro\";\n\n/**\n * Get the current session (returns null if not authenticated)\n *\n * @example\n * ```ts\n * // In .astro component\n * const session = getSession();\n * if (session) {\n * console.log('User ID:', session.userId);\n * }\n * ```\n */\nexport function getSession(): Session | null {\n const context = getContextStore();\n return context?.session ?? null;\n}\n\n/**\n * Get the current session or throw if not authenticated\n *\n * @throws {Response} 401 Unauthorized if no session\n *\n * @example\n * ```ts\n * // In API endpoint\n * const session = requireSession();\n * // TypeScript knows session is not null here\n * ```\n */\nexport function requireSession(): Session {\n const session = getSession();\n\n if (!session) {\n throw new Response(\"Unauthorized\", {status: 401});\n }\n\n return session;\n}\n\n/**\n * Check if user is authenticated\n */\nexport function isAuthenticated(): boolean {\n return getSession() !== null;\n}\n\n/**\n * Check if user has a specific role\n */\nexport function hasRole(role: string): boolean {\n const session = getSession();\n if (!session) return false;\n\n // Check primary role\n if (session.role === role) return true;\n\n // Check additional roles\n return session.roles?.includes(role) ?? false;\n}\n\n/**\n * Check if user has a specific permission\n */\nexport function hasPermission(permission: string): boolean {\n const session = getSession();\n if (!session) return false;\n\n return session.permissions?.includes(permission) ?? false;\n}\n\n/**\n * Check if user has ALL of the specified permissions\n */\nexport function hasAllPermissions(...permissions: string[]): boolean {\n const session = getSession();\n if (!session) return false;\n\n const userPermissions = session.permissions ?? [];\n return permissions.every((p) => userPermissions.includes(p));\n}\n\n/**\n * Check if user has ANY of the specified permissions\n */\nexport function hasAnyPermission(...permissions: string[]): boolean {\n const session = getSession();\n if (!session) return false;\n\n const userPermissions = session.permissions ?? [];\n return permissions.some((p) => userPermissions.includes(p));\n}\n\n// ============================================================================\n// Session Management\n// ============================================================================\n\n/**\n * Check if a specific role has a specific permission.\n *\n * This checks if the current user has the specified role and if that role\n * is associated with the specified permission.\n *\n * @param role - The role to check\n * @param permission - The permission to check\n *\n * @example\n * ```ts\n * if (hasRolePermission(\"admin\", \"delete users\")) {\n * // ...\n * }\n * ```\n */\nexport function hasRolePermission(role: string, permission: string): boolean {\n return hasRole(role) && hasPermission(permission);\n}\n\n/**\n * Set session data in context.locals.session\n *\n * Use this after successful authentication to register the user's session.\n * This does NOT handle session storage (cookies, Redis, etc.) - you must do that separately.\n *\n * @param context - Astro API context\n * @param session - Session data to set\n *\n * @throws {Error} If session structure is invalid\n *\n * @example\n * ```ts\n * // In API endpoint after verifying credentials\n * export const POST: APIRoute = async (context) => {\n * const { email, password } = await context.request.json();\n * const user = await verifyCredentials(email, password);\n *\n * if (user) {\n * // Register session with SessionKit\n * setSession(context, {\n * userId: user.id,\n * email: user.email,\n * role: user.role,\n * permissions: user.permissions\n * });\n *\n * // YOU must also store the session (cookie, Redis, etc.)\n * context.cookies.set('session_id', sessionId, { httpOnly: true });\n *\n * return new Response(JSON.stringify({ success: true }));\n * }\n * };\n * ```\n */\nexport function setSession(context: APIContext, session: Session): void {\n // Validate session structure\n if (!isValidSessionStructure(session)) {\n throw new Error(\n '[SessionKit] Invalid session structure. Session must have a valid userId and follow the Session interface.'\n );\n }\n\n // Set in context.locals for SessionKit middleware to read\n context.session?.set('__session__', session);\n}\n\n/**\n * Clear session from context.locals.session\n *\n * Use this during logout. This does NOT delete session storage (cookies, Redis, etc.) -\n * you must do that separately.\n *\n * @param context - Astro API context\n *\n * @example\n * ```ts\n * // In logout endpoint\n * export const POST: APIRoute = async (context) => {\n * // Clear from SessionKit\n * clearSession(context);\n *\n * // YOU must also delete the session storage\n * context.cookies.delete('session_id');\n * await db.deleteSession(sessionId);\n *\n * return context.redirect('/');\n * };\n * ```\n */\nexport function clearSession(context: APIContext): void {\n context.session?.delete('__session__');\n}\n\n/**\n * Update specific fields in the current session\n *\n * Useful for updating session data without replacing the entire session.\n * The updated session is validated before being set.\n *\n * @param context - Astro API context\n * @param updates - Partial session data to merge\n *\n * @throws {Error} If no session exists or updated session is invalid\n *\n * @example\n * ```ts\n * // Update user's role after promotion\n * export const POST: APIRoute = async (context) => {\n * updateSession(context, {\n * role: 'admin',\n * permissions: ['admin:read', 'admin:write']\n * });\n *\n * // YOU must also update session storage\n * await db.updateSession(sessionId, updatedData);\n *\n * return new Response(JSON.stringify({ success: true }));\n * };\n * ```\n */\nexport function updateSession(context: APIContext, updates: Partial<Session>): void {\n const currentSession = context.session?.get<Session>('__session__');\n\n if (!currentSession) {\n throw new Error('[SessionKit] Cannot update session: no session exists');\n }\n\n // Merge updates with current session\n const updatedSession = {...currentSession, ...updates};\n\n // Validate merged session\n if (!isValidSessionStructure(updatedSession)) {\n throw new Error(\n '[SessionKit] Invalid session structure after update. Ensure all fields are valid.'\n );\n }\n\n context.session?.set('__session__', updatedSession);\n}"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server.js","sources":["../src/server.ts"],"sourcesContent":["// ============================================================================\n// Public Server API - Use these in your Astro components/endpoints\n// ============================================================================\n\nimport {getContextStore} from \"./core/context\";\nimport {isValidSessionStructure} from \"./core/validation\";\nimport type {Session, SessionKitContext} from \"./core/types\";\n\n/**\n * Get the current session (returns null if not authenticated)\n *\n * @example\n * ```ts\n * // In .astro component\n * const session = getSession();\n * if (session) {\n * console.log('User ID:', session.userId);\n * }\n * ```\n */\nexport function getSession(): Session | null {\n const context = getContextStore();\n return context?.session ?? null;\n}\n\n/**\n * Get the current session or throw if not authenticated\n *\n * @throws {Response} 401 Unauthorized if no session\n *\n * @example\n * ```ts\n * // In API endpoint\n * const session = requireSession();\n * // TypeScript knows session is not null here\n * ```\n */\nexport function requireSession(): Session {\n const session = getSession();\n\n if (!session) {\n throw new Response(\"Unauthorized\", {status: 401});\n }\n\n return session;\n}\n\n/**\n * Check if user is authenticated\n */\nexport function isAuthenticated(): boolean {\n return getSession() !== null;\n}\n\n/**\n * Check if user has a specific role\n */\nexport function hasRole(role: string): boolean {\n const session = getSession();\n if (!session) return false;\n\n // Check primary role\n if (session.role === role) return true;\n\n // Check additional roles\n return session.roles?.includes(role) ?? false;\n}\n\n/**\n * Check if user has a specific permission\n */\nexport function hasPermission(permission: string): boolean {\n const session = getSession();\n if (!session) return false;\n\n return session.permissions?.includes(permission) ?? false;\n}\n\n/**\n * Check if user has ALL of the specified permissions\n */\nexport function hasAllPermissions(...permissions: string[]): boolean {\n const session = getSession();\n if (!session) return false;\n\n const userPermissions = session.permissions ?? [];\n return permissions.every((p) => userPermissions.includes(p));\n}\n\n/**\n * Check if user has ANY of the specified permissions\n */\nexport function hasAnyPermission(...permissions: string[]): boolean {\n const session = getSession();\n if (!session) return false;\n\n const userPermissions = session.permissions ?? [];\n return permissions.some((p) => userPermissions.includes(p));\n}\n\n// ============================================================================\n// Session Management\n// ============================================================================\n\n/**\n * Check if a specific role has a specific permission.\n *\n * This checks if the current user has the specified role and if that role\n * is associated with the specified permission.\n *\n * @param role - The role to check\n * @param permission - The permission to check\n *\n * @example\n * ```ts\n * if (hasRolePermission(\"admin\", \"delete users\")) {\n * // ...\n * }\n * ```\n */\nexport function hasRolePermission(role: string, permission: string): boolean {\n return hasRole(role) && hasPermission(permission);\n}\n\n/**\n * Set session data in context.locals.session\n *\n * Use this after successful authentication to register the user's session.\n * This does NOT handle session storage (cookies, Redis, etc.) - you must do that separately.\n *\n * @param session - Session data to set\n * @param context - Astro API context (optional if called within request context)\n *\n * @throws {Error} If session structure is invalid or context missing\n *\n * @example\n * ```ts\n * // In API endpoint after verifying credentials\n * export const POST: APIRoute = async (context) => {\n * const { email, password } = await context.request.json();\n * const user = await verifyCredentials(email, password);\n *\n * if (user) {\n * // Register session with SessionKit\n * setSession({\n * userId: user.id,\n * email: user.email,\n * role: user.role,\n * permissions: user.permissions\n * });\n *\n * // YOU must also store the session (cookie, Redis, etc.)\n * context.cookies.set('session_id', sessionId, { httpOnly: true });\n *\n * return new Response(JSON.stringify({ success: true }));\n * }\n * };\n * ```\n */\nexport function setSession(session: Session, context?: SessionKitContext): void {\n const store = getContextStore();\n const ctx = context || store?.astroContext;\n\n if (!ctx) {\n throw new Error(\n '[SessionKit] Cannot set session: Astro context is missing. ' +\n 'Provide it as a second argument or ensure sessionMiddleware is running.'\n );\n }\n\n // Validate session structure\n if (!isValidSessionStructure(session)) {\n throw new Error(\n '[SessionKit] Invalid session structure. Session must have a valid userId and follow the Session interface.'\n );\n }\n\n // Update ALS store if available for same-request consistency\n if (store) {\n store.session = session;\n }\n\n // Set in context.session for Astro to persist\n ctx.session?.set('__session__', session);\n}\n\n/**\n * Clear session from context.locals.session\n *\n * Use this during logout. This does NOT delete session storage (cookies, Redis, etc.) -\n * you must do that separately.\n *\n * @param context - Astro API context (optional if called within request context)\n *\n * @example\n * ```ts\n * // In logout endpoint\n * export const POST: APIRoute = async (context) => {\n * // Clear from SessionKit\n * clearSession();\n *\n * // YOU must also delete the session storage\n * context.cookies.delete('session_id');\n * await db.deleteSession(sessionId);\n *\n * return context.redirect('/');\n * };\n * ```\n */\nexport function clearSession(context?: SessionKitContext): void {\n const store = getContextStore();\n const ctx = context || store?.astroContext;\n\n if (!ctx) {\n throw new Error(\n '[SessionKit] Cannot clear session: Astro context is missing. ' +\n 'Provide it as an argument or ensure sessionMiddleware is running.'\n );\n }\n\n // Update ALS store if available for same-request consistency\n if (store) {\n store.session = null;\n }\n\n ctx.session?.delete('__session__');\n}\n\n/**\n * Regenerate the session ID to prevent session fixation attacks\n *\n * Use this after a successful login or privilege change.\n * This is only supported if the underlying Astro session driver supports it.\n *\n * @param context - Astro API context (optional if called within request context)\n *\n * @example\n * ```ts\n * // In login endpoint\n * export const POST: APIRoute = async (context) => {\n * const user = await authenticate(request);\n * if (user) {\n * // 1. Regenerate session ID\n * regenerateSession();\n *\n * // 2. Set new session data\n * setSession({ userId: user.id, role: user.role });\n * }\n * }\n * ```\n */\nexport function regenerateSession(context?: SessionKitContext): void {\n const ctx = context || getContextStore()?.astroContext;\n\n if (!ctx) {\n throw new Error(\n '[SessionKit] Cannot regenerate session: Astro context is missing. ' +\n 'Provide it as an argument or ensure sessionMiddleware is running.'\n );\n }\n\n if (ctx.session?.regenerate) {\n ctx.session.regenerate();\n }\n}\n\n/**\n * Update specific fields in the current session\n *\n * Useful for updating session data without replacing the entire session.\n * The updated session is validated before being set.\n *\n * @param updates - Partial session data to merge\n * @param context - Astro API context (optional if called within request context)\n *\n * @throws {Error} If no session exists or updated session is invalid\n *\n * @example\n * ```ts\n * // Update user's role after promotion\n * export const POST: APIRoute = async (context) => {\n * updateSession({\n * role: 'admin',\n * permissions: ['admin:read', 'admin:write']\n * });\n *\n * // YOU must also update session storage\n * await db.updateSession(sessionId, updatedData);\n *\n * return new Response(JSON.stringify({ success: true }));\n * };\n * ```\n */\nexport function updateSession(updates: Partial<Session>, context?: SessionKitContext): void {\n const store = getContextStore();\n const ctx = context || store?.astroContext;\n\n if (!ctx) {\n throw new Error(\n '[SessionKit] Cannot update session: Astro context is missing. ' +\n 'Provide it as a second argument or ensure sessionMiddleware is running.'\n );\n }\n\n // Get current session from ALS (preferred) or Astro session\n const currentSession = store?.session || ctx.session?.get<Session>('__session__');\n\n // Note: ctx.session.get might return a Promise in some Astro versions/drivers.\n // However, since sessionMiddleware already awaits it, store.session should be populated.\n // If store.session is missing but we are in a middleware-managed request, it means no session exists.\n \n if (!currentSession || (currentSession instanceof Promise)) {\n // If it's a promise, we might have a sync/async mismatch, but usually getSession() handles this.\n // For robustness, we check if we actually have a session object.\n const session = currentSession instanceof Promise ? null : currentSession;\n if (!session) {\n throw new Error('[SessionKit] Cannot update session: no session exists');\n }\n }\n\n // We can safely cast here if it's not a promise\n const session = currentSession as Session;\n\n // Merge updates with current session\n const updatedSession = {...session, ...updates};\n\n // Use setSession to handle validation and both store updates\n setSession(updatedSession, ctx);\n}"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-sessionkit",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
4
4
  "description": "Simple session access and route protection for Astro applications",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -85,6 +85,8 @@
85
85
  "test": "vitest run",
86
86
  "test:watch": "vitest",
87
87
  "test:coverage": "vitest run --coverage",
88
- "bench": "vitest bench run bench/"
88
+ "bench": "vitest bench run bench/",
89
+ "playground:install": "cd playground && npm install",
90
+ "playground:dev": "cd playground && npm run dev"
89
91
  }
90
92
  }
@@ -18,11 +18,15 @@ export interface ResolvedConfig {
18
18
  runWithContext?: <T>(context: SessionContext, fn: () => T) => T | Promise<T>;
19
19
  getContextStore?: () => SessionContext | undefined;
20
20
  setContextStore?: (context: SessionContext) => void;
21
+ context?: any;
21
22
  globalProtect: boolean;
22
23
  exclude: string[];
23
24
  debug: boolean;
24
25
  }
25
26
 
27
+ const CONFIG_KEY = Symbol.for('astro-sessionkit.config');
28
+ const globalStorage = globalThis as any;
29
+
26
30
  const DEFAULT_CONFIG: ResolvedConfig = {
27
31
  loginPath: "/login",
28
32
  protect: [],
@@ -36,7 +40,17 @@ const DEFAULT_CONFIG: ResolvedConfig = {
36
40
  debug: false,
37
41
  };
38
42
 
39
- let config: ResolvedConfig = { ...DEFAULT_CONFIG };
43
+ // Initialize global storage if not present
44
+ if (!globalStorage[CONFIG_KEY]) {
45
+ globalStorage[CONFIG_KEY] = Object.freeze({ ...DEFAULT_CONFIG });
46
+ }
47
+
48
+ /**
49
+ * Reset configuration to defaults (mainly for testing)
50
+ */
51
+ export function resetConfig(): void {
52
+ globalStorage[CONFIG_KEY] = Object.freeze({ ...DEFAULT_CONFIG });
53
+ }
40
54
 
41
55
  /**
42
56
  * Set configuration (called by integration)
@@ -113,7 +127,8 @@ export function setConfig(userConfig: SessionKitConfig): void {
113
127
  newConfig.runWithContext = userConfig.runWithContext;
114
128
  newConfig.getContextStore = userConfig.getContextStore;
115
129
  newConfig.setContextStore = userConfig.setContextStore;
116
-
130
+ newConfig.context = userConfig.context;
131
+
117
132
  if (userConfig.globalProtect !== undefined) {
118
133
  newConfig.globalProtect = userConfig.globalProtect;
119
134
  }
@@ -137,12 +152,23 @@ export function setConfig(userConfig: SessionKitConfig): void {
137
152
  }
138
153
 
139
154
  // Atomic update
140
- config = Object.freeze(newConfig);
155
+ globalStorage[CONFIG_KEY] = Object.freeze(newConfig);
141
156
  }
142
157
 
143
158
  /**
144
159
  * Get current configuration
145
160
  */
146
161
  export function getConfig(): ResolvedConfig {
147
- return config;
162
+ return globalStorage[CONFIG_KEY] || DEFAULT_CONFIG;
163
+ }
164
+
165
+ // Handle injected configuration from Astro integration
166
+ try {
167
+ // @ts-ignore
168
+ const injectedConfig = typeof __SESSIONKIT_CONFIG__ !== 'undefined' ? __SESSIONKIT_CONFIG__ : undefined;
169
+ if (injectedConfig) {
170
+ setConfig(injectedConfig);
171
+ }
172
+ } catch (e) {
173
+ // Ignore errors in environments where __SESSIONKIT_CONFIG__ might be restricted
148
174
  }
@@ -19,12 +19,35 @@ export function runWithContext<T>(
19
19
  }
20
20
 
21
21
  /**
22
- * Get the current session context (only available inside middleware chain)
22
+ * Get current Astro context (from middleware binding or explicit)
23
23
  */
24
- export function getContextStore(): SessionContext | undefined {
25
- const customGetter = getConfig().getContextStore;
26
- if (customGetter) {
27
- return customGetter();
24
+ export function getContextStore(): SessionContext {
25
+ const config = getConfig();
26
+ const getStore = config.getContextStore;
27
+ const context = (config as any).context || als;
28
+
29
+ const store = getStore
30
+ ? getStore()
31
+ : (context as AsyncLocalStorage<SessionContext>).getStore();
32
+
33
+ if (!store) {
34
+ return undefined as any;
28
35
  }
29
- return als.getStore();
36
+
37
+ return store;
30
38
  }
39
+
40
+ /**
41
+ * Check if context is available
42
+ */
43
+ export function hasContext(): boolean {
44
+ const config = getConfig();
45
+ const getStore = config.getContextStore;
46
+ const context = (config as any).context || als;
47
+
48
+ const store = getStore
49
+ ? getStore()
50
+ : (context as AsyncLocalStorage<SessionContext>).getStore();
51
+
52
+ return !!store;
53
+ }
@@ -2,7 +2,7 @@
2
2
  // Route Guard Middleware - Enforces protection rules
3
3
  // ============================================================================
4
4
 
5
- import type {APIContext, MiddlewareHandler} from "astro";
5
+ import type {MiddlewareHandler} from "astro";
6
6
  import { getContextStore } from "./context";
7
7
  import { getConfig } from "./config";
8
8
  import { matchesPattern } from "./matcher";
@@ -73,71 +73,91 @@ async function checkRule(rule: ProtectionRule, session: Session | null): Promise
73
73
  * Create route guard middleware
74
74
  */
75
75
  export function createGuardMiddleware(): MiddlewareHandler {
76
- return async (context : APIContext, next) => {
77
- const { protect, loginPath, globalProtect, exclude } = getConfig();
78
-
76
+ return async (context, next) => {
79
77
  let pathname: string;
80
78
  try {
81
- pathname = new URL(context.request.url).pathname;
79
+ pathname = new URL(context.request.url).pathname;
82
80
  } catch {
83
- // Fallback if URL is invalid (unlikely in Astro)
84
- pathname = "/";
81
+ pathname = "/";
85
82
  }
86
83
 
87
- logger.debug(`[Guard] Pathname: ${pathname}, GlobalProtect: ${globalProtect}, Rules: ${protect.length}`);
84
+ const config = getConfig();
85
+ const {protect, loginPath, globalProtect, exclude, debug} = config;
86
+
87
+ if (debug) {
88
+ logger.debug(`[Guard] Pathname: ${pathname}, GlobalProtect: ${globalProtect}, Rules: ${protect.length}`);
89
+ }
88
90
 
89
91
  // No rules configured and no global protect - skip
90
92
  if (protect.length === 0 && !globalProtect) {
91
- logger.debug(`[Guard] Skipping ${pathname} because no rules are configured and globalProtect is false`);
92
- return next();
93
+ if (debug) {
94
+ logger.debug(`[Guard] Skipping ${pathname} because no rules are configured and globalProtect is false`);
95
+ }
96
+ return next();
93
97
  }
94
98
 
95
99
  const sessionContext = getContextStore();
96
100
  const session = sessionContext?.session ?? null;
97
101
 
102
+ if (debug) {
103
+ logger.debug(`[Guard] Session retrieved from store: ${session ? 'exists' : 'null'}`);
104
+ }
105
+
98
106
  // Find matching rule
99
107
  const rule = protect.find((r) => matchesPattern(r.pattern, pathname));
100
-
101
- if (rule) {
102
- logger.debug(`[Guard] Found matching rule for ${pathname}:`, rule);
108
+
109
+ if (rule && debug) {
110
+ logger.debug(`[Guard] Found matching rule for ${pathname}:`, rule);
103
111
  }
104
112
 
105
113
  // No matching rule - check global protection
106
114
  if (!rule) {
107
- if (globalProtect) {
108
- // Skip if path is in exclude list
109
- if (exclude.some((pattern) => matchesPattern(pattern, pathname))) {
110
- logger.debug(`[GlobalProtect] Skipping ${pathname} because it matches an exclude pattern`);
111
- return next();
112
- }
113
-
114
- // Skip if it's the login page itself (to avoid redirect loops)
115
- if (pathname === loginPath) {
116
- logger.debug(`[GlobalProtect] Skipping ${pathname} because it is the loginPath`);
117
- return next();
115
+ if (globalProtect) {
116
+ // Skip if path is in exclude list
117
+ if (exclude.some((pattern) => matchesPattern(pattern, pathname))) {
118
+ if (debug) {
119
+ logger.debug(`[GlobalProtect] Skipping ${pathname} because it matches an exclude pattern`);
120
+ }
121
+ return next();
122
+ }
123
+
124
+ // Skip if it's the login page itself (to avoid redirect loops)
125
+ if (pathname === loginPath) {
126
+ if (debug) {
127
+ logger.debug(`[GlobalProtect] Skipping ${pathname} because it is the loginPath`);
128
+ }
129
+ return next();
130
+ }
131
+
132
+ // Require valid session
133
+ if (!session || !isValidSessionStructure(session)) {
134
+ if (debug) {
135
+ logger.debug(`[GlobalProtect] Redirecting to ${loginPath} because session is ${session ? 'invalid' : 'missing'}`);
136
+ }
137
+ return context.redirect(loginPath);
138
+ }
118
139
  }
119
140
 
120
- // Require valid session
121
- if (!session || !isValidSessionStructure(session)) {
122
- logger.debug(`[GlobalProtect] Redirecting to ${loginPath} because session is ${session ? 'invalid' : 'missing'}`);
123
- return context.redirect(loginPath);
141
+ if (debug) {
142
+ logger.debug(`[GlobalProtect] Allowing ${pathname} because session is valid or globalProtect is false`);
124
143
  }
125
- }
126
-
127
- logger.debug(`[GlobalProtect] Allowing ${pathname} because session is valid or globalProtect is false`);
128
- return next();
144
+ return next();
129
145
  }
130
146
 
131
147
  // Check if access is allowed
132
148
  const allowed = await checkRule(rule, session);
133
149
 
134
150
  if (!allowed) {
135
- const redirectTo = rule.redirectTo ?? loginPath;
136
- logger.debug(`[Guard] Redirecting to ${redirectTo} because access was denied by rule:`, rule);
137
- return context.redirect(redirectTo);
151
+ const redirectTo = rule.redirectTo ?? loginPath;
152
+ if (debug) {
153
+ logger.debug(`[Guard] Redirecting to ${redirectTo} because access was denied by rule:`, rule);
154
+ }
155
+ return context.redirect(redirectTo);
138
156
  }
139
157
 
140
- logger.debug(`[Guard] Allowing ${pathname} because access was granted by rule:`, rule);
158
+ if (debug) {
159
+ logger.debug(`[Guard] Allowing ${pathname} because access was granted by rule:`, rule);
160
+ }
141
161
  return next();
142
162
  };
143
163
  }
@@ -6,7 +6,12 @@ function escapeRegex(str: string): string {
6
6
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7
7
  }
8
8
 
9
+ const regexCache = new Map<string, RegExp>();
10
+
9
11
  function globToRegex(pattern: string): RegExp {
12
+ const cached = regexCache.get(pattern);
13
+ if (cached) return cached;
14
+
10
15
  let regex = "";
11
16
  let i = 0;
12
17
 
@@ -53,7 +58,9 @@ function globToRegex(pattern: string): RegExp {
53
58
  i += 1;
54
59
  }
55
60
 
56
- return new RegExp(`^${regex}$`);
61
+ const result = new RegExp(`^${regex}$`);
62
+ regexCache.set(pattern, result);
63
+ return result;
57
64
  }
58
65
 
59
66
  export function matchesPattern(pattern: string, path: string): boolean {
@@ -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 config = getConfig();
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 (config.getContextStore && !config.runWithContext) {
75
+ if (getContextStore && !runWithContext) {
51
76
  // Initialize context store if setter is provided
52
- const store = config.getContextStore();
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 (config.setContextStore) {
56
- config.setContextStore({session});
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 next();
91
+ return runLogic();
92
+ }
93
+
94
+ if (debug) {
95
+ logger.debug('[SessionMiddleware] Using' + (runWithContext ? ' custom ' : ' default ') + 'runner');
61
96
  }
62
97
 
63
- const runner = config.runWithContext ?? defaultRunWithContext;
64
- return runner({session}, () => next());
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
- * Enable debug logging
144
- * @default false
145
- */
146
- debug?: boolean;
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
  }