@xyd-js/plugin-access-control 0.0.0-build-8804789-20260430104829

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,167 @@
1
+ import React from 'react';
2
+ import { AccessControl } from '@xyd-js/core';
3
+
4
+ type AccessLevel = "public" | "authenticated" | string;
5
+ type AccessMap = Record<string, AccessLevel>;
6
+ interface AccessEvaluation {
7
+ allowed: boolean;
8
+ reason: string;
9
+ }
10
+ /**
11
+ * Evaluate access for a specific page given its metadata and access control config.
12
+ * Returns the access level string for the access map.
13
+ */
14
+ declare function resolvePageAccess(pagePath: string, metadata: {
15
+ public?: boolean;
16
+ accessGroups?: string[];
17
+ }, config: AccessControl): AccessLevel;
18
+ /**
19
+ * Evaluate whether a user has access to a page.
20
+ */
21
+ declare function evaluateAccess(pagePath: string, accessMap: AccessMap, userGroups: string[]): AccessEvaluation;
22
+ /**
23
+ * Build a complete access map from page path mapping and access control config.
24
+ * Used at build time to pre-compute access levels for all pages.
25
+ */
26
+ declare function buildAccessMap(pagePathMapping: Record<string, string>, metadataMap: Record<string, {
27
+ public?: boolean;
28
+ accessGroups?: string[];
29
+ }>, config: AccessControl): AccessMap;
30
+
31
+ interface AuthState {
32
+ authenticated: boolean;
33
+ groups: string[];
34
+ token: string | null;
35
+ }
36
+ declare global {
37
+ interface Window {
38
+ __xydAuthState?: AuthState;
39
+ __xydAuthDebug?: any;
40
+ }
41
+ }
42
+ interface AuthGuardProps {
43
+ children: React.ReactNode;
44
+ accessMap: Record<string, string>;
45
+ config: {
46
+ provider: {
47
+ type: string;
48
+ loginUrl?: string;
49
+ };
50
+ unauthorizedBehavior?: "redirect" | "404";
51
+ session?: {
52
+ cookieName?: string;
53
+ };
54
+ };
55
+ }
56
+ /**
57
+ * AuthGuard wraps the application and enforces access control.
58
+ * It reads auth state from window.__xydAuthState (set by pre-hydration script)
59
+ * and checks the access map for the current route.
60
+ */
61
+ declare function AuthGuard({ children, accessMap, config }: AuthGuardProps): React.JSX.Element;
62
+ /**
63
+ * Hook to access authentication state.
64
+ */
65
+ declare function useAuth(): AuthState & {
66
+ login: () => void;
67
+ logout: () => void;
68
+ };
69
+
70
+ /**
71
+ * Default login page. Wraps itself with AccessControlProvider
72
+ * so it works regardless of the host wrapper.
73
+ */
74
+ declare function LoginPage(): React.JSX.Element;
75
+
76
+ /**
77
+ * Handles both JWT and OAuth callback flows:
78
+ *
79
+ * JWT: /auth/jwt-callback#eyJ... → extract token from hash
80
+ * OAuth: /auth/callback?code=XXX&state=/page → exchange code for token
81
+ */
82
+ declare function AuthCallbackPage(): React.JSX.Element;
83
+
84
+ interface AccessControlActions {
85
+ /** Current auth config from docs.json */
86
+ config: any | null;
87
+ /** Whether config has loaded */
88
+ ready: boolean;
89
+ /** Last error message */
90
+ error: string;
91
+ /** Clear error */
92
+ clearError: () => void;
93
+ /** Redirect to external OAuth provider */
94
+ signInWithOAuth: () => void;
95
+ /** Redirect to external JWT login URL */
96
+ signInWithRedirect: () => void;
97
+ /** Sign in with test token (dev/edge server) */
98
+ signInAsUser: () => void;
99
+ /** Sign in as admin with test token (dev/edge server) */
100
+ signInAsAdmin: () => void;
101
+ /** Sign in with specific groups */
102
+ signInWithGroups: (groups: string[]) => void;
103
+ /** Provider type: "jwt" | "oauth" */
104
+ providerType: string;
105
+ /** Whether an external auth URL is configured */
106
+ hasExternalAuth: boolean;
107
+ /** Login page title from config */
108
+ title: string;
109
+ /** Login page description from config */
110
+ description: string;
111
+ /** Login page logo URL from config */
112
+ logo: string;
113
+ /** Login page background image from config */
114
+ backgroundImage: string;
115
+ /** The redirect URL (where to go after login) */
116
+ redirectUrl: string;
117
+ }
118
+ /**
119
+ * Hook to access auth actions in custom login pages.
120
+ *
121
+ * @example
122
+ * ```tsx
123
+ * import { useAccessControl } from "@xyd-js/plugin-access-control"
124
+ *
125
+ * function MyLoginPage() {
126
+ * const { signInWithOAuth, title, logo, error } = useAccessControl()
127
+ *
128
+ * return (
129
+ * <div>
130
+ * {logo && <img src={logo} />}
131
+ * <h1>{title}</h1>
132
+ * {error && <p>{error}</p>}
133
+ * <button onClick={signInWithOAuth}>Sign in</button>
134
+ * </div>
135
+ * )
136
+ * }
137
+ * ```
138
+ */
139
+ declare function useAccessControl(): AccessControlActions;
140
+ declare function AccessControlProvider({ children }: {
141
+ children: React.ReactNode;
142
+ }): React.JSX.Element;
143
+
144
+ interface SidebarItem {
145
+ title: string;
146
+ href: string;
147
+ items?: SidebarItem[];
148
+ [key: string]: any;
149
+ }
150
+ interface SidebarGroup {
151
+ group: string;
152
+ groupIndex: number;
153
+ items: SidebarItem[];
154
+ icon?: string;
155
+ [key: string]: any;
156
+ }
157
+ /**
158
+ * Filters sidebar navigation groups to hide pages the user cannot access.
159
+ * This runs at runtime in the browser based on the user's auth state.
160
+ */
161
+ declare function filterSidebarGroups(sidebarGroups: SidebarGroup[], accessMap: AccessMap, userGroups: string[]): SidebarGroup[];
162
+ /**
163
+ * Filters an array of paths (used for sitemap/llms.txt) to exclude protected pages.
164
+ */
165
+ declare function filterProtectedPaths(paths: string[], accessMap: AccessMap): string[];
166
+
167
+ export { type AccessControlActions, AccessControlProvider, type AccessEvaluation, type AccessLevel, type AccessMap, AuthCallbackPage, AuthGuard, LoginPage, buildAccessMap, evaluateAccess, filterProtectedPaths, filterSidebarGroups, resolvePageAccess, useAccessControl, useAuth };
package/dist/client.js ADDED
@@ -0,0 +1,484 @@
1
+ // src/components/AccessControlContext.tsx
2
+ import React, { createContext, useContext, useEffect, useState, useCallback } from "react";
3
+
4
+ // src/devOnly.ts
5
+ function isDevEnvironment() {
6
+ if (typeof window === "undefined") {
7
+ return process.env.NODE_ENV !== "production";
8
+ }
9
+ try {
10
+ return !!import.meta.env?.DEV;
11
+ } catch {
12
+ return false;
13
+ }
14
+ }
15
+
16
+ // src/components/AccessControlContext.tsx
17
+ var AccessControlCtx = createContext(null);
18
+ function useAccessControl() {
19
+ const ctx = useContext(AccessControlCtx);
20
+ if (!ctx) {
21
+ throw new Error("useAccessControl must be used inside <AccessControlProvider>");
22
+ }
23
+ return ctx;
24
+ }
25
+ function AccessControlProvider({ children }) {
26
+ const [config, setConfig] = useState(null);
27
+ const [error, setError] = useState("");
28
+ const [ready, setReady] = useState(false);
29
+ useEffect(() => {
30
+ const globalConfig = window.__xydAccessControlSettings?.accessControlConfig;
31
+ if (globalConfig) {
32
+ setConfig(globalConfig);
33
+ setReady(true);
34
+ return;
35
+ }
36
+ import("virtual:xyd-access-control-settings").then((mod) => {
37
+ setConfig(mod.accessControlConfig);
38
+ setReady(true);
39
+ }).catch(() => setReady(true));
40
+ }, []);
41
+ const providerType = config?.provider?.type || "jwt";
42
+ const loginUrl = config?.provider?.loginUrl || "";
43
+ const authorizationUrl = config?.provider?.authorizationUrl || "";
44
+ const hasExternalAuth = providerType === "oauth" ? !!authorizationUrl : !!loginUrl && loginUrl !== "/login";
45
+ const loginConfig = typeof config?.login === "string" ? {} : config?.login || {};
46
+ const title = loginConfig.title || "Sign in to access documentation";
47
+ const description = loginConfig.description || "";
48
+ const logo = loginConfig.logo || "";
49
+ const backgroundImage = loginConfig.backgroundImage || "";
50
+ const params = typeof window !== "undefined" ? new URLSearchParams(window.location.search) : null;
51
+ const redirectUrl = params?.get("redirect") || "/";
52
+ const storeTokenAndRedirect = useCallback((token) => {
53
+ const cookieName = config?.session?.cookieName || "xyd-auth-token";
54
+ localStorage.setItem(cookieName, token);
55
+ document.cookie = `${cookieName}=${token}; path=/; max-age=86400; SameSite=Lax`;
56
+ window.location.href = redirectUrl;
57
+ }, [config, redirectUrl]);
58
+ const signInWithOAuth = useCallback(() => {
59
+ if (!authorizationUrl) {
60
+ setError("No OAuth authorization URL configured");
61
+ return;
62
+ }
63
+ const callbackPath = config?.provider?.callbackPath || "/auth/callback";
64
+ const redirectUri = window.location.origin + callbackPath;
65
+ const scopes = config?.provider?.scopes || [];
66
+ const clientId = config?.provider?.clientId || "";
67
+ const oauthParams = new URLSearchParams({
68
+ client_id: clientId,
69
+ redirect_uri: redirectUri,
70
+ response_type: "code",
71
+ scope: scopes.join(" "),
72
+ state: redirectUrl
73
+ });
74
+ window.location.href = `${authorizationUrl}?${oauthParams.toString()}`;
75
+ }, [config, authorizationUrl, redirectUrl]);
76
+ const signInWithRedirect = useCallback(() => {
77
+ if (!loginUrl || loginUrl === "/login") {
78
+ setError("No external login URL configured");
79
+ return;
80
+ }
81
+ const sep = loginUrl.includes("?") ? "&" : "?";
82
+ window.location.href = `${loginUrl}${sep}redirect=${encodeURIComponent(redirectUrl)}`;
83
+ }, [loginUrl, redirectUrl]);
84
+ const signInWithGroups = useCallback((groups) => {
85
+ const hasDeploy = !!config?.deploy;
86
+ if (hasDeploy) {
87
+ const groupsParam = groups.length ? `&groups=${groups.join(",")}` : "";
88
+ window.location.href = `/auth/test-login?redirect=${encodeURIComponent(redirectUrl)}${groupsParam}`;
89
+ return;
90
+ }
91
+ if (!isDevEnvironment()) {
92
+ setError("Test login is only available in development mode.");
93
+ return;
94
+ }
95
+ const header = btoa(JSON.stringify({ alg: "HS256", typ: "JWT" }));
96
+ const payload = btoa(JSON.stringify({
97
+ sub: "test-user",
98
+ groups,
99
+ exp: Math.floor(Date.now() / 1e3) + 86400,
100
+ iat: Math.floor(Date.now() / 1e3)
101
+ }));
102
+ storeTokenAndRedirect(`${header}.${payload}.dGVzdA`);
103
+ }, [config, redirectUrl, storeTokenAndRedirect]);
104
+ const signInAsUser = useCallback(() => signInWithGroups([]), [signInWithGroups]);
105
+ const signInAsAdmin = useCallback(() => signInWithGroups(["admin"]), [signInWithGroups]);
106
+ const value = {
107
+ config,
108
+ ready,
109
+ error,
110
+ clearError: () => setError(""),
111
+ signInWithOAuth,
112
+ signInWithRedirect,
113
+ signInAsUser,
114
+ signInAsAdmin,
115
+ signInWithGroups,
116
+ providerType,
117
+ hasExternalAuth,
118
+ title,
119
+ description,
120
+ logo,
121
+ backgroundImage,
122
+ redirectUrl
123
+ };
124
+ return /* @__PURE__ */ React.createElement(AccessControlCtx.Provider, { value }, children);
125
+ }
126
+
127
+ // src/components/AuthGuard.tsx
128
+ import React2, {
129
+ createContext as createContext2,
130
+ useContext as useContext2,
131
+ useEffect as useEffect2,
132
+ useState as useState2,
133
+ useMemo
134
+ } from "react";
135
+ var AuthContext = createContext2({
136
+ authenticated: false,
137
+ groups: [],
138
+ token: null
139
+ });
140
+ function buildLoginUrl(loginUrl, returnPath) {
141
+ const separator = loginUrl.includes("?") ? "&" : "?";
142
+ return `${loginUrl}${separator}redirect=${encodeURIComponent(returnPath)}`;
143
+ }
144
+ function AuthGuard({ children, accessMap, config }) {
145
+ const [authState, setAuthState] = useState2(() => {
146
+ if (typeof window !== "undefined" && window.__xydAuthState) {
147
+ return window.__xydAuthState;
148
+ }
149
+ return { authenticated: false, groups: [], token: null };
150
+ });
151
+ useEffect2(() => {
152
+ const handleStorage = (e) => {
153
+ const cookieName = config.session?.cookieName || "xyd-auth-token";
154
+ if (e.key === cookieName) {
155
+ if (e.newValue) {
156
+ try {
157
+ const payload = JSON.parse(atob(e.newValue.split(".")[1]));
158
+ setAuthState({
159
+ authenticated: true,
160
+ groups: payload.groups || [],
161
+ token: e.newValue
162
+ });
163
+ } catch {
164
+ setAuthState({ authenticated: false, groups: [], token: null });
165
+ }
166
+ } else {
167
+ setAuthState({ authenticated: false, groups: [], token: null });
168
+ }
169
+ }
170
+ };
171
+ window.addEventListener("storage", handleStorage);
172
+ return () => window.removeEventListener("storage", handleStorage);
173
+ }, [config.session?.cookieName]);
174
+ const value = useMemo(() => authState, [authState]);
175
+ return /* @__PURE__ */ React2.createElement(AuthContext.Provider, { value }, /* @__PURE__ */ React2.createElement(AuthEnforcer, { accessMap, config }, children));
176
+ }
177
+ function AuthEnforcer({
178
+ children,
179
+ accessMap,
180
+ config
181
+ }) {
182
+ const authState = useContext2(AuthContext);
183
+ useEffect2(() => {
184
+ if (typeof window === "undefined") return;
185
+ const pathname = window.location.pathname;
186
+ const pageAccess = accessMap[pathname];
187
+ if (!pageAccess || pageAccess === "public") return;
188
+ if (!authState.authenticated) {
189
+ const loginUrl = config.provider.loginUrl;
190
+ if (loginUrl) {
191
+ window.location.href = buildLoginUrl(loginUrl, pathname);
192
+ }
193
+ return;
194
+ }
195
+ if (pageAccess !== "authenticated") {
196
+ const requiredGroups = pageAccess.split(",");
197
+ const hasAccess = requiredGroups.some(
198
+ (g) => authState.groups.includes(g)
199
+ );
200
+ if (!hasAccess) {
201
+ if (config.unauthorizedBehavior === "404") {
202
+ window.location.href = "/404";
203
+ } else {
204
+ const loginUrl = config.provider.loginUrl;
205
+ if (loginUrl) {
206
+ window.location.href = buildLoginUrl(loginUrl, pathname);
207
+ }
208
+ }
209
+ }
210
+ }
211
+ }, [authState, accessMap, config]);
212
+ return /* @__PURE__ */ React2.createElement(React2.Fragment, null, children);
213
+ }
214
+ function useAuth() {
215
+ const state = useContext2(AuthContext);
216
+ return {
217
+ ...state,
218
+ login() {
219
+ console.warn("[xyd:access-control] login() called without config");
220
+ },
221
+ logout() {
222
+ if (typeof window === "undefined") return;
223
+ const cookieName = "xyd-auth-token";
224
+ localStorage.removeItem(cookieName);
225
+ document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
226
+ document.cookie = `${cookieName}-state=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
227
+ window.__xydAuthState = {
228
+ authenticated: false,
229
+ groups: [],
230
+ token: null
231
+ };
232
+ document.documentElement.setAttribute("data-auth", "anonymous");
233
+ window.location.reload();
234
+ }
235
+ };
236
+ }
237
+
238
+ // src/components/LoginPage.tsx
239
+ import React3 from "react";
240
+ function LoginPage() {
241
+ return /* @__PURE__ */ React3.createElement(AccessControlProvider, null, /* @__PURE__ */ React3.createElement(LoginPageUI, null));
242
+ }
243
+ function LoginPageUI() {
244
+ const {
245
+ providerType,
246
+ hasExternalAuth,
247
+ title,
248
+ description,
249
+ logo,
250
+ backgroundImage,
251
+ error,
252
+ signInWithOAuth,
253
+ signInWithRedirect,
254
+ signInAsUser,
255
+ signInAsAdmin
256
+ } = useAccessControl();
257
+ const handleSignIn = () => {
258
+ if (providerType === "oauth") signInWithOAuth();
259
+ else signInWithRedirect();
260
+ };
261
+ const bgOverride = backgroundImage ? { backgroundImage: `url(${backgroundImage})`, backgroundSize: "cover", backgroundPosition: "center", background: "none" } : void 0;
262
+ return /* @__PURE__ */ React3.createElement("div", { className: "xyd-login-page", style: bgOverride }, /* @__PURE__ */ React3.createElement("div", { part: "card" }, logo && /* @__PURE__ */ React3.createElement("img", { part: "logo", src: logo, alt: "" }), /* @__PURE__ */ React3.createElement("h1", { part: "title" }, title), description && /* @__PURE__ */ React3.createElement("p", { part: "description" }, description), error && /* @__PURE__ */ React3.createElement("p", { part: "error" }, error), hasExternalAuth ? /* @__PURE__ */ React3.createElement("button", { part: "button", onClick: handleSignIn }, "Sign in") : /* @__PURE__ */ React3.createElement("div", { part: "actions" }, /* @__PURE__ */ React3.createElement("button", { part: "button", onClick: signInAsUser }, "Sign in as User"), /* @__PURE__ */ React3.createElement("button", { part: "button", "data-kind": "secondary", onClick: signInAsAdmin }, "Sign in as Admin"))));
263
+ }
264
+
265
+ // src/components/AuthCallbackPage.tsx
266
+ import React4, { useEffect as useEffect3, useState as useState3 } from "react";
267
+ function AuthCallbackPage() {
268
+ const [error, setError] = useState3("");
269
+ useEffect3(() => {
270
+ if (typeof window === "undefined") return;
271
+ import("virtual:xyd-access-control-settings").then((mod) => {
272
+ const config = mod.accessControlConfig;
273
+ handleCallback(config);
274
+ }).catch(() => {
275
+ handleCallback(null);
276
+ });
277
+ }, []);
278
+ function storeTokenAndRedirect(token, groups, redirect, cookieName) {
279
+ localStorage.setItem(cookieName, token);
280
+ document.cookie = `${cookieName}=${token}; path=/; max-age=86400; SameSite=Lax`;
281
+ window.__xydAuthState = {
282
+ authenticated: true,
283
+ groups,
284
+ token
285
+ };
286
+ document.documentElement.setAttribute("data-auth", "authenticated");
287
+ window.location.href = redirect;
288
+ }
289
+ async function handleCallback(config) {
290
+ const params = new URLSearchParams(window.location.search);
291
+ const hash = window.location.hash.slice(1);
292
+ const code = params.get("code");
293
+ const state = params.get("state") || "/";
294
+ const cookieName = config?.session?.cookieName || "xyd-auth-token";
295
+ const groupsClaim = config?.provider?.groupsClaim || "groups";
296
+ if (code && config?.provider?.type === "oauth") {
297
+ try {
298
+ await handleOAuthCallback(code, state, config, cookieName, groupsClaim);
299
+ } catch (e) {
300
+ setError(e instanceof Error ? e.message : "OAuth authentication failed");
301
+ }
302
+ return;
303
+ }
304
+ const token = params.get("token") || hash;
305
+ const redirect = params.get("redirect") || state;
306
+ if (token) {
307
+ try {
308
+ handleJWTCallback(token, redirect, cookieName, groupsClaim);
309
+ } catch (e) {
310
+ setError(e instanceof Error ? e.message : "JWT authentication failed");
311
+ }
312
+ return;
313
+ }
314
+ setError("No authentication data found in callback URL.");
315
+ }
316
+ function handleJWTCallback(hash, redirect, cookieName, groupsClaim) {
317
+ const parts = hash.split(".");
318
+ if (parts.length !== 3) throw new Error("Invalid JWT format");
319
+ const payload = JSON.parse(atob(parts[1]));
320
+ if (payload.exp && payload.exp * 1e3 < Date.now()) {
321
+ throw new Error("Token has expired");
322
+ }
323
+ storeTokenAndRedirect(hash, payload[groupsClaim] || [], redirect, cookieName);
324
+ }
325
+ async function handleOAuthCallback(code, redirect, config, cookieName, groupsClaim) {
326
+ const tokenUrl = config.provider.tokenUrl;
327
+ const userInfoUrl = config.provider.userInfoUrl;
328
+ const clientId = config.provider.clientId || "";
329
+ const callbackPath = config.provider.callbackPath || "/auth/callback";
330
+ const redirectUri = window.location.origin + callbackPath;
331
+ if (!tokenUrl) throw new Error("No token URL configured");
332
+ const tokenRes = await fetch(tokenUrl, {
333
+ method: "POST",
334
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
335
+ body: new URLSearchParams({
336
+ grant_type: "authorization_code",
337
+ code,
338
+ client_id: clientId,
339
+ redirect_uri: redirectUri
340
+ }).toString()
341
+ });
342
+ if (!tokenRes.ok) {
343
+ throw new Error(`Token exchange failed: ${tokenRes.status}`);
344
+ }
345
+ const tokenData = await tokenRes.json();
346
+ const accessToken = tokenData.access_token;
347
+ if (!accessToken) throw new Error("No access token in response");
348
+ let groups = [];
349
+ if (userInfoUrl) {
350
+ try {
351
+ const userRes = await fetch(userInfoUrl, {
352
+ headers: { Authorization: `Bearer ${accessToken}` }
353
+ });
354
+ if (userRes.ok) {
355
+ const userInfo = await userRes.json();
356
+ groups = userInfo[groupsClaim] || userInfo.roles || userInfo.groups || [];
357
+ }
358
+ } catch {
359
+ }
360
+ }
361
+ const header = btoa(JSON.stringify({ alg: "HS256", typ: "JWT" }));
362
+ const payload = btoa(JSON.stringify({
363
+ sub: "oauth-user",
364
+ [groupsClaim]: groups,
365
+ exp: Math.floor(Date.now() / 1e3) + 86400,
366
+ iat: Math.floor(Date.now() / 1e3),
367
+ access_token: accessToken
368
+ }));
369
+ const sessionToken = `${header}.${payload}.oauth`;
370
+ storeTokenAndRedirect(sessionToken, groups, redirect, cookieName);
371
+ }
372
+ if (error) {
373
+ return /* @__PURE__ */ React4.createElement("div", { className: "xyd-callback-page" }, /* @__PURE__ */ React4.createElement("div", { part: "card" }, /* @__PURE__ */ React4.createElement("h2", { part: "title", "data-error": "" }, "Authentication Error"), /* @__PURE__ */ React4.createElement("p", { part: "message" }, error), /* @__PURE__ */ React4.createElement("button", { part: "button", onClick: () => window.location.href = "/" }, "Go to homepage")));
374
+ }
375
+ return /* @__PURE__ */ React4.createElement("div", { className: "xyd-callback-page" }, /* @__PURE__ */ React4.createElement("div", { part: "card" }, /* @__PURE__ */ React4.createElement("p", { part: "message" }, "Authenticating...")));
376
+ }
377
+
378
+ // src/access.ts
379
+ function matchPattern(pattern, path) {
380
+ const regexStr = pattern.replace(/\*\*/g, "___GLOBSTAR___").replace(/\*/g, "[^/]*").replace(/___GLOBSTAR___/g, ".*");
381
+ const regex = new RegExp(`^${regexStr}$`);
382
+ return regex.test(path);
383
+ }
384
+ function resolvePageAccess(pagePath, metadata, config) {
385
+ const normalizedPath = pagePath.startsWith("/") ? pagePath : `/${pagePath}`;
386
+ if (metadata.public === true) {
387
+ return "public";
388
+ }
389
+ if (metadata.public === false) {
390
+ if (metadata.accessGroups?.length) {
391
+ return metadata.accessGroups.join(",");
392
+ }
393
+ return "authenticated";
394
+ }
395
+ if (config.rules) {
396
+ for (const rule of config.rules) {
397
+ if (matchPattern(rule.match, normalizedPath)) {
398
+ if (rule.access === "public") {
399
+ return "public";
400
+ }
401
+ if (rule.groups?.length) {
402
+ return rule.groups.join(",");
403
+ }
404
+ return "authenticated";
405
+ }
406
+ }
407
+ }
408
+ return config.defaultAccess === "protected" ? "authenticated" : "public";
409
+ }
410
+ function evaluateAccess(pagePath, accessMap, userGroups) {
411
+ const access = accessMap[pagePath];
412
+ if (!access || access === "public") {
413
+ return { allowed: true, reason: `access:public` };
414
+ }
415
+ if (access === "authenticated") {
416
+ return {
417
+ allowed: userGroups.length > 0 || false,
418
+ reason: "access:authenticated"
419
+ };
420
+ }
421
+ const requiredGroups = access.split(",");
422
+ const hasGroup = requiredGroups.some((g) => userGroups.includes(g));
423
+ return {
424
+ allowed: hasGroup,
425
+ reason: `access:groups:${access}`
426
+ };
427
+ }
428
+ function buildAccessMap(pagePathMapping, metadataMap, config) {
429
+ const accessMap = {};
430
+ for (const pagePath of Object.keys(pagePathMapping)) {
431
+ const metadata = metadataMap[pagePath] || {};
432
+ accessMap[pagePath] = resolvePageAccess(pagePath, metadata, config);
433
+ }
434
+ return accessMap;
435
+ }
436
+
437
+ // src/navigation.ts
438
+ function filterSidebarGroups(sidebarGroups, accessMap, userGroups) {
439
+ return sidebarGroups.map((group) => ({
440
+ ...group,
441
+ items: filterItems(group.items, accessMap, userGroups)
442
+ })).filter((group) => group.items.length > 0);
443
+ }
444
+ function filterItems(items, accessMap, userGroups) {
445
+ return items.filter((item) => {
446
+ const href = item.href;
447
+ if (!href) return true;
448
+ const normalizedHref = href.startsWith("/") ? href : `/${href}`;
449
+ const access = accessMap[normalizedHref] || accessMap[href];
450
+ if (!access || access === "public") return true;
451
+ if (access === "authenticated") return userGroups.length > 0;
452
+ const requiredGroups = access.split(",");
453
+ return requiredGroups.some((g) => userGroups.includes(g));
454
+ }).map((item) => {
455
+ if (item.items?.length) {
456
+ return {
457
+ ...item,
458
+ items: filterItems(item.items, accessMap, userGroups)
459
+ };
460
+ }
461
+ return item;
462
+ });
463
+ }
464
+ function filterProtectedPaths(paths, accessMap) {
465
+ return paths.filter((p) => {
466
+ const normalizedPath = p.startsWith("/") ? p : `/${p}`;
467
+ const access = accessMap[normalizedPath] || accessMap[p];
468
+ return !access || access === "public";
469
+ });
470
+ }
471
+ export {
472
+ AccessControlProvider,
473
+ AuthCallbackPage,
474
+ AuthGuard,
475
+ LoginPage,
476
+ buildAccessMap,
477
+ evaluateAccess,
478
+ filterProtectedPaths,
479
+ filterSidebarGroups,
480
+ resolvePageAccess,
481
+ useAccessControl,
482
+ useAuth
483
+ };
484
+ //# sourceMappingURL=client.js.map