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
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,12 +52,13 @@ 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
|
|
|
51
|
-
declare function
|
|
59
|
+
declare function sessionKit(config?: SessionKitConfig): AstroIntegration;
|
|
52
60
|
|
|
53
|
-
declare const version = "0.1.
|
|
61
|
+
declare const version = "0.1.20";
|
|
54
62
|
|
|
55
|
-
export {
|
|
63
|
+
export { sessionKit as default, version };
|
|
56
64
|
export type { AccessHooks, CustomProtectionRule, PermissionProtectionRule, PermissionsProtectionRule, ProtectionRule, RoleProtectionRule, RolesProtectionRule, Session, SessionContext, SessionKitConfig };
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,UAAU,MAAM,eAAe,CAAC;AAEvC,eAAe,UAAU,CAAC;AAM1B,YAAY,EACR,OAAO,EACP,cAAc,EACd,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACxB,yBAAyB,EACzB,oBAAoB,EACpB,gBAAgB,EAChB,WAAW,EACX,cAAc,EACjB,MAAM,cAAc,CAAC;AAMtB,eAAO,MAAM,OAAO,WAAW,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,27 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import sessionKit from './integration.js';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
setConfig(config);
|
|
5
|
-
const resolvedConfig = getConfig();
|
|
6
|
-
return {
|
|
7
|
-
name: "astro-sessionkit",
|
|
8
|
-
hooks: {
|
|
9
|
-
"astro:config:setup": ({ addMiddleware }) => {
|
|
10
|
-
addMiddleware({
|
|
11
|
-
entrypoint: "astro-sessionkit/middleware",
|
|
12
|
-
order: "pre",
|
|
13
|
-
});
|
|
14
|
-
if ((resolvedConfig.protect && resolvedConfig.protect.length > 0) || resolvedConfig.globalProtect) {
|
|
15
|
-
addMiddleware({
|
|
16
|
-
entrypoint: "astro-sessionkit/guard",
|
|
17
|
-
order: "pre",
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
const version = "0.1.0";
|
|
3
|
+
const version = "0.1.20";
|
|
25
4
|
|
|
26
|
-
export {
|
|
5
|
+
export { sessionKit as default, version };
|
|
27
6
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["// ============================================================================\n// Astro SessionKit - Main Integration Entry Point\n// ============================================================================\n\nimport
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["// ============================================================================\n// Astro SessionKit - Main Integration Entry Point\n// ============================================================================\n\nimport sessionkit from \"./integration\";\n\nexport default sessionkit;\n\n// ============================================================================\n// Re-export types for convenience\n// ============================================================================\n\nexport type {\n Session,\n ProtectionRule,\n RoleProtectionRule,\n RolesProtectionRule,\n PermissionProtectionRule,\n PermissionsProtectionRule,\n CustomProtectionRule,\n SessionKitConfig,\n AccessHooks,\n SessionContext\n} from \"./core/types\";\n\n// ============================================================================\n// Version export\n// ============================================================================\n\nexport const version = \"0.1.20\";\n"],"names":[],"mappings":";;AA6BO,MAAM,OAAO,GAAG;;;;"}
|
package/dist/integration.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AstroIntegration } from "astro";
|
|
2
2
|
import type { SessionKitConfig } from "./core/types";
|
|
3
3
|
export default function sessionKit(config?: SessionKitConfig): AstroIntegration;
|
|
4
|
-
export type { Session, ProtectionRule, SessionKitConfig } from "./core/types";
|
|
4
|
+
export type { Session, ProtectionRule, RoleProtectionRule, RolesProtectionRule, PermissionProtectionRule, PermissionsProtectionRule, CustomProtectionRule, SessionKitConfig, AccessHooks, SessionContext } from "./core/types";
|
|
5
5
|
//# sourceMappingURL=integration.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../src/integration.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AAE9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAwBrD,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,MAAM,GAAE,gBAAqB,GAAG,gBAAgB,
|
|
1
|
+
{"version":3,"file":"integration.d.ts","sourceRoot":"","sources":["../src/integration.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AAE9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAwBrD,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,MAAM,GAAE,gBAAqB,GAAG,gBAAgB,CA8BlF;AAED,YAAY,EACV,OAAO,EACP,cAAc,EACd,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACxB,yBAAyB,EACzB,oBAAoB,EACpB,gBAAgB,EAChB,WAAW,EACX,cAAc,EACf,MAAM,cAAc,CAAC"}
|
package/dist/integration.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { setConfig, getConfig } from './core/config.js';
|
|
2
|
+
|
|
3
|
+
function sessionKit(config = {}) {
|
|
3
4
|
setConfig(config);
|
|
4
5
|
const resolvedConfig = getConfig();
|
|
5
6
|
return {
|
|
@@ -10,14 +11,21 @@ export default function sessionKit(config = {}) {
|
|
|
10
11
|
entrypoint: "astro-sessionkit/middleware",
|
|
11
12
|
order: "pre",
|
|
12
13
|
});
|
|
13
|
-
|
|
14
|
+
const hasRules = (resolvedConfig.protect && resolvedConfig.protect.length > 0);
|
|
15
|
+
const isGlobal = !!resolvedConfig.globalProtect;
|
|
16
|
+
if (hasRules || isGlobal) {
|
|
14
17
|
addMiddleware({
|
|
15
18
|
entrypoint: "astro-sessionkit/guard",
|
|
16
19
|
order: "pre",
|
|
17
20
|
});
|
|
18
21
|
}
|
|
22
|
+
else if (resolvedConfig.debug) {
|
|
23
|
+
console.log("[SessionKit] Route guard NOT registered: no rules and globalProtect is false.");
|
|
24
|
+
}
|
|
19
25
|
},
|
|
20
26
|
},
|
|
21
27
|
};
|
|
22
28
|
}
|
|
23
|
-
|
|
29
|
+
|
|
30
|
+
export { sessionKit as default };
|
|
31
|
+
//# sourceMappingURL=integration.js.map
|
package/dist/integration.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"integration.js","
|
|
1
|
+
{"version":3,"file":"integration.js","sources":["../src/integration.ts"],"sourcesContent":["// ============================================================================\n// Astro Integration\n// ============================================================================\n\nimport type { AstroIntegration } from \"astro\";\nimport {getConfig, setConfig} from \"./core/config\";\nimport type { SessionKitConfig } from \"./core/types\";\n\n/**\n * SessionKit - Simple session access and route protection for Astro\n * \n * @example\n * ```ts\n * // astro.config.mjs\n * import sessionkit from 'astro-sessionkit';\n * \n * export default defineConfig({\n * integrations: [\n * sessionkit({\n * loginPath: '/login',\n * protect: [\n * { pattern: '/admin/**', role: 'admin' },\n * { pattern: '/dashboard', roles: ['user', 'admin'] },\n * { pattern: '/settings', permissions: ['settings:write'] }\n * ]\n * })\n * ]\n * });\n * ```\n */\nexport default function sessionKit(config: SessionKitConfig = {}): AstroIntegration {\n // Store configuration\n setConfig(config);\n const resolvedConfig = getConfig();\n\n return {\n name: \"astro-sessionkit\",\n hooks: {\n \"astro:config:setup\": ({ addMiddleware }) => {\n // 1. Always add session context middleware first\n addMiddleware({\n entrypoint: \"astro-sessionkit/middleware\",\n order: \"pre\",\n });\n\n // 2. Add route guard if there are protection rules or global protection is enabled\n const hasRules = (resolvedConfig.protect && resolvedConfig.protect.length > 0);\n const isGlobal = !!resolvedConfig.globalProtect;\n\n if (hasRules || isGlobal) {\n addMiddleware({\n entrypoint: \"astro-sessionkit/guard\",\n order: \"pre\",\n });\n } else if (resolvedConfig.debug) {\n console.log(\"[SessionKit] Route guard NOT registered: no rules and globalProtect is false.\");\n }\n },\n },\n };\n}\n\nexport type {\n Session,\n ProtectionRule,\n RoleProtectionRule,\n RolesProtectionRule,\n PermissionProtectionRule,\n PermissionsProtectionRule,\n CustomProtectionRule,\n SessionKitConfig,\n AccessHooks,\n SessionContext\n} from \"./core/types\";\n"],"names":[],"mappings":";;AA8Bc,SAAU,UAAU,CAAC,SAA2B,EAAE,EAAA;IAE9D,SAAS,CAAC,MAAM,CAAC;AACjB,IAAA,MAAM,cAAc,GAAG,SAAS,EAAE;IAElC,OAAO;AACL,QAAA,IAAI,EAAE,kBAAkB;AACxB,QAAA,KAAK,EAAE;AACL,YAAA,oBAAoB,EAAE,CAAC,EAAE,aAAa,EAAE,KAAI;AAE1C,gBAAA,aAAa,CAAC;AACZ,oBAAA,UAAU,EAAE,6BAA6B;AACzC,oBAAA,KAAK,EAAE,KAAK;AACb,iBAAA,CAAC;AAGF,gBAAA,MAAM,QAAQ,IAAI,cAAc,CAAC,OAAO,IAAI,cAAc,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC;AAC9E,gBAAA,MAAM,QAAQ,GAAG,CAAC,CAAC,cAAc,CAAC,aAAa;AAE/C,gBAAA,IAAI,QAAQ,IAAI,QAAQ,EAAE;AACxB,oBAAA,aAAa,CAAC;AACZ,wBAAA,UAAU,EAAE,wBAAwB;AACpC,wBAAA,KAAK,EAAE,KAAK;AACb,qBAAA,CAAC;gBACJ;AAAO,qBAAA,IAAI,cAAc,CAAC,KAAK,EAAE;AAC/B,oBAAA,OAAO,CAAC,GAAG,CAAC,+EAA+E,CAAC;gBAC9F;YACF,CAAC;AACF,SAAA;KACF;AACH;;;;"}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import {
|
|
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(
|
|
21
|
-
declare function clearSession(context
|
|
22
|
-
declare function
|
|
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 };
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAC,OAAO,
|
|
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(
|
|
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
|
-
|
|
59
|
+
if (store) {
|
|
60
|
+
store.session = session;
|
|
61
|
+
}
|
|
62
|
+
ctx.session?.set('__session__', session);
|
|
54
63
|
}
|
|
55
64
|
function clearSession(context) {
|
|
56
|
-
|
|
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(
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
64
|
-
if (!
|
|
65
|
-
|
|
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
|
-
|
|
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
|
package/dist/server.js.map
CHANGED
|
@@ -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.
|
|
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
|
}
|
package/src/core/config.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|
package/src/core/context.ts
CHANGED
|
@@ -19,12 +19,35 @@ export function runWithContext<T>(
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
* Get
|
|
22
|
+
* Get current Astro context (from middleware binding or explicit)
|
|
23
23
|
*/
|
|
24
|
-
export function getContextStore(): SessionContext
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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 {
|
|
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,70 +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
|
|
77
|
-
const { protect, loginPath, globalProtect, exclude } = getConfig();
|
|
78
|
-
|
|
76
|
+
return async (context, next) => {
|
|
79
77
|
let pathname: string;
|
|
80
78
|
try {
|
|
81
|
-
|
|
79
|
+
pathname = new URL(context.request.url).pathname;
|
|
82
80
|
} catch {
|
|
83
|
-
|
|
84
|
-
pathname = "/";
|
|
81
|
+
pathname = "/";
|
|
85
82
|
}
|
|
86
83
|
|
|
87
|
-
|
|
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
|
-
|
|
93
|
+
if (debug) {
|
|
94
|
+
logger.debug(`[Guard] Skipping ${pathname} because no rules are configured and globalProtect is false`);
|
|
95
|
+
}
|
|
96
|
+
return next();
|
|
92
97
|
}
|
|
93
98
|
|
|
94
99
|
const sessionContext = getContextStore();
|
|
95
100
|
const session = sessionContext?.session ?? null;
|
|
96
101
|
|
|
102
|
+
if (debug) {
|
|
103
|
+
logger.debug(`[Guard] Session retrieved from store: ${session ? 'exists' : 'null'}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
97
106
|
// Find matching rule
|
|
98
107
|
const rule = protect.find((r) => matchesPattern(r.pattern, pathname));
|
|
99
|
-
|
|
100
|
-
if (rule) {
|
|
101
|
-
|
|
108
|
+
|
|
109
|
+
if (rule && debug) {
|
|
110
|
+
logger.debug(`[Guard] Found matching rule for ${pathname}:`, rule);
|
|
102
111
|
}
|
|
103
112
|
|
|
104
113
|
// No matching rule - check global protection
|
|
105
114
|
if (!rule) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
+
}
|
|
117
139
|
}
|
|
118
140
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
logger.debug(`[GlobalProtect] Redirecting to ${loginPath} because session is ${session ? 'invalid' : 'missing'}`);
|
|
122
|
-
return context.redirect(loginPath);
|
|
141
|
+
if (debug) {
|
|
142
|
+
logger.debug(`[GlobalProtect] Allowing ${pathname} because session is valid or globalProtect is false`);
|
|
123
143
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
logger.debug(`[GlobalProtect] Allowing ${pathname} because session is valid or globalProtect is false`);
|
|
127
|
-
return next();
|
|
144
|
+
return next();
|
|
128
145
|
}
|
|
129
146
|
|
|
130
147
|
// Check if access is allowed
|
|
131
148
|
const allowed = await checkRule(rule, session);
|
|
132
149
|
|
|
133
150
|
if (!allowed) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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);
|
|
137
156
|
}
|
|
138
157
|
|
|
139
|
-
|
|
158
|
+
if (debug) {
|
|
159
|
+
logger.debug(`[Guard] Allowing ${pathname} because access was granted by rule:`, rule);
|
|
160
|
+
}
|
|
140
161
|
return next();
|
|
141
162
|
};
|
|
142
163
|
}
|
package/src/core/matcher.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|