@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.
package/dist/index.js ADDED
@@ -0,0 +1,1200 @@
1
+ // src/access.ts
2
+ function matchPattern(pattern, path) {
3
+ const regexStr = pattern.replace(/\*\*/g, "___GLOBSTAR___").replace(/\*/g, "[^/]*").replace(/___GLOBSTAR___/g, ".*");
4
+ const regex = new RegExp(`^${regexStr}$`);
5
+ return regex.test(path);
6
+ }
7
+ function resolvePageAccess(pagePath, metadata, config) {
8
+ const normalizedPath = pagePath.startsWith("/") ? pagePath : `/${pagePath}`;
9
+ if (metadata.public === true) {
10
+ return "public";
11
+ }
12
+ if (metadata.public === false) {
13
+ if (metadata.accessGroups?.length) {
14
+ return metadata.accessGroups.join(",");
15
+ }
16
+ return "authenticated";
17
+ }
18
+ if (config.rules) {
19
+ for (const rule of config.rules) {
20
+ if (matchPattern(rule.match, normalizedPath)) {
21
+ if (rule.access === "public") {
22
+ return "public";
23
+ }
24
+ if (rule.groups?.length) {
25
+ return rule.groups.join(",");
26
+ }
27
+ return "authenticated";
28
+ }
29
+ }
30
+ }
31
+ return config.defaultAccess === "protected" ? "authenticated" : "public";
32
+ }
33
+ function evaluateAccess(pagePath, accessMap, userGroups) {
34
+ const access = accessMap[pagePath];
35
+ if (!access || access === "public") {
36
+ return { allowed: true, reason: `access:public` };
37
+ }
38
+ if (access === "authenticated") {
39
+ return {
40
+ allowed: userGroups.length > 0 || false,
41
+ reason: "access:authenticated"
42
+ };
43
+ }
44
+ const requiredGroups = access.split(",");
45
+ const hasGroup = requiredGroups.some((g) => userGroups.includes(g));
46
+ return {
47
+ allowed: hasGroup,
48
+ reason: `access:groups:${access}`
49
+ };
50
+ }
51
+ function buildAccessMap(pagePathMapping, metadataMap, config) {
52
+ const accessMap = {};
53
+ for (const pagePath of Object.keys(pagePathMapping)) {
54
+ const metadata = metadataMap[pagePath] || {};
55
+ accessMap[pagePath] = resolvePageAccess(pagePath, metadata, config);
56
+ }
57
+ return accessMap;
58
+ }
59
+
60
+ // src/devOnly.ts
61
+ function isDevEnvironment() {
62
+ if (typeof window === "undefined") {
63
+ return process.env.NODE_ENV !== "production";
64
+ }
65
+ try {
66
+ return !!import.meta.env?.DEV;
67
+ } catch {
68
+ return false;
69
+ }
70
+ }
71
+ function isAuthBypassed() {
72
+ if (!isDevEnvironment()) return false;
73
+ return process.env.XYD_AUTH_BYPASS === "1" || process.env.XYD_AUTH_BYPASS === "true";
74
+ }
75
+
76
+ // src/virtual.ts
77
+ var VIRTUAL_SETTINGS_ID = "virtual:xyd-access-control-settings";
78
+ var RESOLVED_SETTINGS_ID = "\0" + VIRTUAL_SETTINGS_ID;
79
+ var VIRTUAL_GUARD_ID = "virtual:xyd-access-control-guard";
80
+ var RESOLVED_GUARD_ID = "\0" + VIRTUAL_GUARD_ID;
81
+ var VIRTUAL_PAGES_ID = "virtual:xyd-plugin-pages";
82
+ var RESOLVED_PAGES_ID = "\0" + VIRTUAL_PAGES_ID;
83
+ function virtualAccessControlSettingsPlugin(config, buildAccessMap2) {
84
+ let cachedAccessMap = null;
85
+ function getAccessMap() {
86
+ if (!cachedAccessMap) {
87
+ cachedAccessMap = buildAccessMap2(config);
88
+ }
89
+ return cachedAccessMap;
90
+ }
91
+ return {
92
+ name: "xyd-plugin-access-control-virtual-settings",
93
+ resolveId(id) {
94
+ if (id === VIRTUAL_SETTINGS_ID) return RESOLVED_SETTINGS_ID;
95
+ if (id === VIRTUAL_GUARD_ID) return RESOLVED_GUARD_ID;
96
+ if (id === VIRTUAL_PAGES_ID) return RESOLVED_PAGES_ID;
97
+ return null;
98
+ },
99
+ load(id) {
100
+ if (id === RESOLVED_SETTINGS_ID) {
101
+ const isBypassed = isAuthBypassed();
102
+ const safeConfig = sanitizeConfig(config);
103
+ const accessMap = isBypassed ? {} : getAccessMap();
104
+ if (isBypassed) {
105
+ console.log("[xyd:access-control] Auth bypass enabled (XYD_AUTH_BYPASS). All pages are public.");
106
+ }
107
+ return `
108
+ export const accessControlConfig = ${JSON.stringify(safeConfig)};
109
+ export const accessMap = ${JSON.stringify(accessMap)};
110
+ `;
111
+ }
112
+ if (id === RESOLVED_GUARD_ID) {
113
+ return [
114
+ `export { default as AuthGuard, useAuth } from "@xyd-js/plugin-access-control/AuthGuard";`,
115
+ `export { AccessControlProvider, useAccessControl } from "@xyd-js/plugin-access-control/AccessControlContext";`
116
+ ].join("\n");
117
+ }
118
+ if (id === RESOLVED_PAGES_ID) {
119
+ const pluginPages = globalThis.__xydPluginPages || [];
120
+ const imports = [];
121
+ const entries = [];
122
+ pluginPages.forEach((page, i) => {
123
+ const dist = page.dist || page._pluginPkg;
124
+ if (dist) {
125
+ imports.push(`import Page${i}, * as PageMod${i} from "${dist}";`);
126
+ entries.push(` "${page.route}": { component: Page${i}, seoTags: PageMod${i}.seoTags, shadowCss: PageMod${i}.shadowCss }`);
127
+ }
128
+ });
129
+ return [
130
+ ...imports,
131
+ `export { AccessControlProvider } from "@xyd-js/plugin-access-control/AccessControlContext";`,
132
+ `export const pluginPages = {`,
133
+ entries.join(",\n"),
134
+ `};`
135
+ ].join("\n");
136
+ }
137
+ return null;
138
+ },
139
+ // Invalidate cached access map when settings change (dev HMR)
140
+ handleHotUpdate() {
141
+ cachedAccessMap = null;
142
+ }
143
+ };
144
+ }
145
+ function sanitizeConfig(config) {
146
+ const safe = { ...config };
147
+ if (safe.provider) {
148
+ const provider = { ...safe.provider };
149
+ if ("secret" in provider) {
150
+ provider.secret = void 0;
151
+ }
152
+ safe.provider = provider;
153
+ }
154
+ return safe;
155
+ }
156
+
157
+ // src/scripts/authPrehydration.ts
158
+ function generateAuthPrehydrationScript(cookieName, groupsClaim) {
159
+ const script = `(function(){var n="%%COOKIE_NAME%%";var t=null;try{t=localStorage.getItem(n)}catch(e){}if(!t){var c=document.cookie.split(";");for(var i=0;i<c.length;i++){var p=c[i].trim().split("=");if(p[0]===n||p[0]===n+"-state"){t=decodeURIComponent(p.slice(1).join("="));break}}}var a=false;var g=[];if(t){try{var d=JSON.parse(atob(t.split(".")[1]));if(d.exp&&d.exp*1000>Date.now()){a=true;g=d["%%GROUPS_CLAIM%%"]||[]}}catch(e){}}window.__xydAuthState={authenticated:a,groups:g,token:a?t:null};document.documentElement.setAttribute("data-auth",a?"authenticated":"anonymous")})();`;
160
+ return script.replace(/%%COOKIE_NAME%%/g, cookieName).replace(/%%GROUPS_CLAIM%%/g, groupsClaim);
161
+ }
162
+
163
+ // src/middleware/shared.ts
164
+ function generateMiddlewareCore(config, accessMap) {
165
+ const loginUrl = "loginUrl" in config.provider ? config.provider.loginUrl : "";
166
+ const cookieName = config.session?.cookieName || "xyd-auth-token";
167
+ const groupsClaim = "groupsClaim" in config.provider ? config.provider.groupsClaim || "groups" : "groups";
168
+ const defaultAccess = config.defaultAccess || "public";
169
+ return `
170
+ const ACCESS_MAP = ${JSON.stringify(accessMap)};
171
+ const LOGIN_URL = ${JSON.stringify(loginUrl)};
172
+ const COOKIE_NAME = ${JSON.stringify(cookieName)};
173
+ const GROUPS_CLAIM = ${JSON.stringify(groupsClaim)};
174
+ const DEFAULT_ACCESS = ${JSON.stringify(defaultAccess)};
175
+
176
+ function parseCookies(cookieHeader) {
177
+ const cookies = {};
178
+ if (!cookieHeader) return cookies;
179
+ cookieHeader.split(";").forEach(function(c) {
180
+ const parts = c.trim().split("=");
181
+ if (parts.length >= 2) {
182
+ cookies[parts[0]] = decodeURIComponent(parts.slice(1).join("="));
183
+ }
184
+ });
185
+ return cookies;
186
+ }
187
+
188
+ function decodeJWTPayload(token) {
189
+ try {
190
+ const parts = token.split(".");
191
+ if (parts.length !== 3) return null;
192
+ return JSON.parse(atob(parts[1]));
193
+ } catch {
194
+ return null;
195
+ }
196
+ }
197
+
198
+ function handleAuthRequest(request) {
199
+ const url = new URL(request.url);
200
+ const path = url.pathname;
201
+
202
+ // Skip asset requests
203
+ if (path.startsWith("/assets/") || path.match(/\\.[a-z0-9]+$/i)) {
204
+ return null; // pass through
205
+ }
206
+
207
+ // Check access requirement
208
+ const access = ACCESS_MAP[path] || DEFAULT_ACCESS;
209
+ if (access === "public") return null; // pass through
210
+
211
+ // Check auth cookie
212
+ const cookies = parseCookies(request.headers.get("cookie"));
213
+ const token = cookies[COOKIE_NAME];
214
+
215
+ if (!token) {
216
+ const redirectUrl = LOGIN_URL
217
+ ? LOGIN_URL + (LOGIN_URL.includes("?") ? "&" : "?") + "redirect=" + encodeURIComponent(url.href)
218
+ : null;
219
+ return { status: 302, redirect: redirectUrl };
220
+ }
221
+
222
+ // Decode and validate token
223
+ const payload = decodeJWTPayload(token);
224
+ if (!payload || (payload.exp && payload.exp * 1000 < Date.now())) {
225
+ const redirectUrl = LOGIN_URL
226
+ ? LOGIN_URL + (LOGIN_URL.includes("?") ? "&" : "?") + "redirect=" + encodeURIComponent(url.href)
227
+ : null;
228
+ return { status: 302, redirect: redirectUrl };
229
+ }
230
+
231
+ // Check group access
232
+ if (access !== "authenticated") {
233
+ const requiredGroups = access.split(",");
234
+ const userGroups = payload[GROUPS_CLAIM] || [];
235
+ const hasAccess = requiredGroups.some(function(g) { return userGroups.includes(g); });
236
+ if (!hasAccess) {
237
+ return { status: 404 };
238
+ }
239
+ }
240
+
241
+ return null; // pass through - authorized
242
+ }
243
+ `;
244
+ }
245
+
246
+ // src/middleware/netlify.ts
247
+ async function generateNetlifyEdge(config, outputDir) {
248
+ const { writeFileSync, mkdirSync, readFileSync, existsSync } = await import("fs");
249
+ const { join } = await import("path");
250
+ const projectRoot = join(outputDir, "../..");
251
+ const edgeFnDir = join(projectRoot, "netlify/edge-functions");
252
+ mkdirSync(edgeFnDir, { recursive: true });
253
+ const accessMap = globalThis.__xydAccessMap || {};
254
+ const middlewareCore = generateMiddlewareCore(config, accessMap);
255
+ const edgeFunction = `
256
+ ${middlewareCore}
257
+
258
+ export default async function handler(request) {
259
+ const result = handleAuthRequest(request);
260
+
261
+ if (!result) return; // pass through to static files
262
+
263
+ if (result.redirect) {
264
+ return Response.redirect(result.redirect, 302);
265
+ }
266
+
267
+ if (result.status === 404) {
268
+ return new Response("Not Found", { status: 404 });
269
+ }
270
+ }
271
+
272
+ export const config = { path: "/*" };
273
+ `;
274
+ writeFileSync(join(edgeFnDir, "access-control.js"), edgeFunction, "utf-8");
275
+ const tomlPath = join(projectRoot, "netlify.toml");
276
+ const declaration = `
277
+ [[edge_functions]]
278
+ function = "access-control"
279
+ path = "/*"
280
+ `;
281
+ if (existsSync(tomlPath)) {
282
+ const existing = readFileSync(tomlPath, "utf-8");
283
+ if (!existing.includes('function = "access-control"')) {
284
+ writeFileSync(tomlPath, existing + "\n" + declaration, "utf-8");
285
+ }
286
+ } else {
287
+ writeFileSync(tomlPath, declaration, "utf-8");
288
+ }
289
+ }
290
+
291
+ // src/middleware/vercel.ts
292
+ async function generateVercelMiddleware(config, outputDir) {
293
+ const { writeFileSync } = await import("fs");
294
+ const { join } = await import("path");
295
+ const projectRoot = join(outputDir, "../..");
296
+ const accessMap = globalThis.__xydAccessMap || {};
297
+ const middlewareCore = generateMiddlewareCore(config, accessMap);
298
+ const middleware = `
299
+ ${middlewareCore}
300
+
301
+ export default function middleware(request) {
302
+ const result = handleAuthRequest(request);
303
+
304
+ if (!result) return; // pass through
305
+
306
+ if (result.redirect) {
307
+ return new Response(null, {
308
+ status: 302,
309
+ headers: { Location: result.redirect },
310
+ });
311
+ }
312
+
313
+ if (result.status === 404) {
314
+ return new Response("Not Found", { status: 404 });
315
+ }
316
+ }
317
+ `;
318
+ writeFileSync(join(projectRoot, "middleware.js"), middleware, "utf-8");
319
+ }
320
+
321
+ // src/middleware/cloudflare.ts
322
+ async function generateCloudflareMiddleware(config, outputDir) {
323
+ const { writeFileSync, mkdirSync } = await import("fs");
324
+ const { join } = await import("path");
325
+ const functionsDir = join(outputDir, "functions");
326
+ mkdirSync(functionsDir, { recursive: true });
327
+ const accessMap = globalThis.__xydAccessMap || {};
328
+ const middlewareCore = generateMiddlewareCore(config, accessMap);
329
+ const middleware = `
330
+ ${middlewareCore}
331
+
332
+ export async function onRequest(context) {
333
+ const result = handleAuthRequest(context.request);
334
+
335
+ if (!result) return context.next(); // pass through to static files
336
+
337
+ if (result.redirect) {
338
+ return Response.redirect(result.redirect, 302);
339
+ }
340
+
341
+ if (result.status === 404) {
342
+ return new Response("Not Found", { status: 404 });
343
+ }
344
+ }
345
+ `;
346
+ writeFileSync(join(functionsDir, "_middleware.js"), middleware, "utf-8");
347
+ }
348
+
349
+ // src/middleware/node.ts
350
+ async function generateNodeServer(config, outputDir) {
351
+ const { writeFileSync } = await import("fs");
352
+ const { join } = await import("path");
353
+ const accessMap = globalThis.__xydAccessMap || {};
354
+ const cookieName = config.session?.cookieName || "xyd-auth-token";
355
+ const maxAge = config.session?.maxAge || 86400;
356
+ const groupsClaim = "groupsClaim" in config.provider ? config.provider.groupsClaim || "groups" : "groups";
357
+ const loginUrl = "loginUrl" in config.provider ? config.provider.loginUrl || "/login" : "/login";
358
+ const secret = "secret" in config.provider ? config.provider.secret || "" : "";
359
+ const imp = "import";
360
+ const server = `#!/usr/bin/env node
361
+ /**
362
+ * Auto-generated by xyd build with deploy.platform: "node-edge"
363
+ * Standalone access control server with JWT signature verification.
364
+ *
365
+ * Usage:
366
+ * node server.mjs
367
+ * # or: PORT=8080 node server.mjs
368
+ */
369
+ ${imp} { createServer } from "node:http";
370
+ ${imp} { createHmac } from "node:crypto";
371
+ ${imp} { readFileSync, existsSync } from "node:fs";
372
+ ${imp} { join, extname } from "node:path";
373
+ ${imp} { fileURLToPath } from "node:url";
374
+
375
+ const __dirname = fileURLToPath(new URL(".", import.meta.url));
376
+ const PORT = parseInt(process.env.PORT || "3000", 10);
377
+ const STATIC_DIR = __dirname;
378
+ const COOKIE_NAME = ${JSON.stringify(cookieName)};
379
+ const MAX_AGE = ${maxAge};
380
+ const GROUPS_CLAIM = ${JSON.stringify(groupsClaim)};
381
+ const LOGIN_URL = ${JSON.stringify(loginUrl)};
382
+ const JWT_SECRET = process.env.AUTH_SECRET || ${JSON.stringify(secret.startsWith("$") ? "" : secret)};
383
+ const ACCESS_MAP = ${JSON.stringify(accessMap)};
384
+
385
+ const MIME = {
386
+ ".html":"text/html",".js":"application/javascript",".css":"text/css",
387
+ ".json":"application/json",".svg":"image/svg+xml",".png":"image/png",
388
+ ".jpg":"image/jpeg",".ico":"image/x-icon",".woff":"font/woff",
389
+ ".woff2":"font/woff2",".ttf":"font/ttf",".map":"application/json",
390
+ };
391
+
392
+ function parseCookies(h) {
393
+ const c = {};
394
+ if (!h) return c;
395
+ h.split(";").forEach(s => { const [k,...v] = s.trim().split("="); c[k] = decodeURIComponent(v.join("=")); });
396
+ return c;
397
+ }
398
+
399
+ function verifyJWT(token) {
400
+ try {
401
+ const [header, payload, signature] = token.split(".");
402
+ if (!header || !payload || !signature) return null;
403
+ if (JWT_SECRET) {
404
+ const expected = createHmac("sha256", JWT_SECRET).update(header + "." + payload).digest("base64url");
405
+ if (signature !== expected) return null;
406
+ }
407
+ const decoded = JSON.parse(Buffer.from(payload, "base64url").toString());
408
+ if (decoded.exp && decoded.exp * 1000 < Date.now()) return null;
409
+ return decoded;
410
+ } catch { return null; }
411
+ }
412
+
413
+ function checkAccess(pathname, cookies) {
414
+ const access = ACCESS_MAP[pathname] || ACCESS_MAP[pathname.replace(/\\/$/, "")] || null;
415
+ if (!access || access === "public") return { ok: true };
416
+ const token = cookies[COOKIE_NAME];
417
+ if (!token) return { ok: false, reason: "no-token" };
418
+ const payload = verifyJWT(token);
419
+ if (!payload) return { ok: false, reason: "invalid-token" };
420
+ if (access === "authenticated") return { ok: true, user: payload };
421
+ const userGroups = payload[GROUPS_CLAIM] || [];
422
+ if (access.split(",").some(g => userGroups.includes(g))) return { ok: true, user: payload };
423
+ return { ok: false, reason: "insufficient-groups" };
424
+ }
425
+
426
+ function serve(res, filePath) {
427
+ try {
428
+ if (!existsSync(filePath)) { res.writeHead(404); res.end("Not Found"); return; }
429
+ const ext = extname(filePath);
430
+ res.writeHead(200, { "Content-Type": MIME[ext] || "application/octet-stream" });
431
+ res.end(readFileSync(filePath));
432
+ } catch {
433
+ if (!res.headersSent) { res.writeHead(500); }
434
+ res.end("Error");
435
+ }
436
+ }
437
+
438
+ const server = createServer((req, res) => {
439
+ const url = new URL(req.url, "http://localhost:" + PORT);
440
+ const path = url.pathname;
441
+ const cookies = parseCookies(req.headers.cookie);
442
+
443
+ // Static assets
444
+ if (path.startsWith("/assets/") || path.match(/\\.\\w+$/)) {
445
+ return serve(res, join(STATIC_DIR, path));
446
+ }
447
+
448
+ // Test login \u2014 DEVELOPMENT ONLY. Disabled in production.
449
+ if (path === "/auth/test-login") {
450
+ if (process.env.NODE_ENV === "production" && !process.env.XYD_TEST_LOGIN) {
451
+ res.writeHead(404); res.end("Not Found"); return;
452
+ }
453
+ const groups = (url.searchParams.get("groups") || "").split(",").filter(Boolean);
454
+ const redirect = url.searchParams.get("redirect") || "/";
455
+ if (!JWT_SECRET) { res.writeHead(500); res.end("AUTH_SECRET not set"); return; }
456
+ const header = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" })).toString("base64url");
457
+ const payload = Buffer.from(JSON.stringify({
458
+ sub: groups.includes("admin") ? "admin" : "user",
459
+ [GROUPS_CLAIM]: groups,
460
+ exp: Math.floor(Date.now() / 1000) + MAX_AGE,
461
+ iat: Math.floor(Date.now() / 1000),
462
+ })).toString("base64url");
463
+ const sig = createHmac("sha256", JWT_SECRET).update(header + "." + payload).digest("base64url");
464
+ res.writeHead(302, {
465
+ Location: redirect,
466
+ "Set-Cookie": COOKIE_NAME + "=" + header + "." + payload + "." + sig + "; Path=/; Max-Age=" + MAX_AGE + "; SameSite=Lax",
467
+ });
468
+ res.end(); return;
469
+ }
470
+
471
+ // JWT callback: external auth service POSTs or GETs a token
472
+ // Accepts: ?token=JWT&redirect=/page or POST body with token field
473
+ if (path === "/auth/jwt-callback" || path === ${JSON.stringify("callbackPath" in config.provider ? config.provider.callbackPath || "/auth/jwt-callback" : "/auth/jwt-callback")}) {
474
+ let token = url.searchParams.get("token") || url.searchParams.get("fern_token") || "";
475
+ const redirect = url.searchParams.get("redirect") || url.searchParams.get("state") || "/";
476
+
477
+ if (token) {
478
+ const verified = verifyJWT(token);
479
+ if (!verified) {
480
+ res.writeHead(401); res.end("Invalid or expired token"); return;
481
+ }
482
+ res.writeHead(302, {
483
+ Location: redirect,
484
+ "Set-Cookie": COOKIE_NAME + "=" + token + "; Path=/; Max-Age=" + MAX_AGE + "; SameSite=Lax",
485
+ });
486
+ res.end(); return;
487
+ }
488
+
489
+ // No token in query \u2014 serve the callback HTML page (handles hash fragment client-side)
490
+ let fp = join(STATIC_DIR, path, "index.html");
491
+ if (!existsSync(fp)) fp = join(STATIC_DIR, "index.html");
492
+ return serve(res, fp);
493
+ }
494
+
495
+ // Logout
496
+ if (path === "/auth/logout") {
497
+ res.writeHead(302, {
498
+ Location: "/",
499
+ "Set-Cookie": COOKIE_NAME + "=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT",
500
+ });
501
+ res.end(); return;
502
+ }
503
+
504
+ // Protected content chunks
505
+ if (path.startsWith("/__xyd_protected_content/")) {
506
+ const r = checkAccess("/protected", cookies);
507
+ if (!r.ok) { res.writeHead(401); res.end("Unauthorized"); return; }
508
+ return serve(res, join(STATIC_DIR, path));
509
+ }
510
+
511
+ // Access check
512
+ const result = checkAccess(path, cookies);
513
+ if (!result.ok) {
514
+ // Always redirect to /login (xyd login page), not directly to the external auth URL.
515
+ // The login page UI handles redirecting to the external provider.
516
+ const redir = "/login?redirect=" + encodeURIComponent(path);
517
+ console.log("[access-control] " + path + " \u2192 302 (" + result.reason + ")");
518
+ res.writeHead(302, { Location: redir }); res.end(); return;
519
+ }
520
+
521
+ if (result.user) console.log("[access-control] " + path + " \u2192 200 (user: " + result.user.sub + ")");
522
+ else console.log("[access-control] " + path + " \u2192 200 (public)");
523
+
524
+ // Serve page
525
+ let fp = join(STATIC_DIR, path, "index.html");
526
+ if (!existsSync(fp)) fp = join(STATIC_DIR, path + ".html");
527
+ if (!existsSync(fp)) fp = join(STATIC_DIR, "index.html");
528
+ serve(res, fp);
529
+ });
530
+
531
+ server.listen(PORT, () => {
532
+ console.log("\\n xyd access-control server running at http://localhost:" + PORT);
533
+ console.log(" Callback: /auth/jwt-callback?token=JWT&redirect=/");
534
+ console.log(" Logout: /auth/logout");
535
+ if (process.env.NODE_ENV !== "production" || process.env.XYD_TEST_LOGIN) {
536
+ console.log(" Test: /auth/test-login?groups=admin&redirect=/ (dev only)");
537
+ }
538
+ console.log("");
539
+ });
540
+ `;
541
+ writeFileSync(join(outputDir, "server.mjs"), server, "utf-8");
542
+ }
543
+
544
+ // src/content.ts
545
+ function protectedContentPlugin(config, buildAccessMap2) {
546
+ let cachedAccessMap = null;
547
+ function getAccessMap() {
548
+ if (!cachedAccessMap) {
549
+ cachedAccessMap = buildAccessMap2(config);
550
+ }
551
+ return cachedAccessMap;
552
+ }
553
+ return {
554
+ name: "xyd-plugin-access-control-protected-content",
555
+ configureServer(server) {
556
+ server.middlewares.use((req, res, next) => {
557
+ const url = req.url?.split("?")[0] || "";
558
+ if (url.startsWith("/assets/") || url.startsWith("/@") || url.startsWith("/__") || url.startsWith("/node_modules/") || url.includes(".") || url === "/login" || url.startsWith("/auth/")) {
559
+ return next();
560
+ }
561
+ const am = getAccessMap();
562
+ const slug = url.replace(/^\//, "");
563
+ const access = am["/" + slug] || am[slug];
564
+ if (!access || access === "public") {
565
+ return next();
566
+ }
567
+ const cookieHeader = req.headers.cookie || "";
568
+ const cookieName = config.session?.cookieName || "xyd-auth-token";
569
+ const hasToken = cookieHeader.includes(`${cookieName}=`);
570
+ const isBypassed = isAuthBypassed();
571
+ if (!hasToken && !isBypassed) {
572
+ const redirect = "/login?redirect=" + encodeURIComponent(req.url || "/");
573
+ res.writeHead(302, { Location: redirect });
574
+ res.end();
575
+ return;
576
+ }
577
+ next();
578
+ });
579
+ server.middlewares.use(async (req, res, next) => {
580
+ if (!req.url?.startsWith("/__xyd_protected_content/")) {
581
+ return next();
582
+ }
583
+ const slug = decodeURIComponent(
584
+ req.url.replace("/__xyd_protected_content/", "").replace(/\.js$/, "")
585
+ );
586
+ const am = getAccessMap();
587
+ const pageAccess = am["/" + slug] || am[slug];
588
+ if (!pageAccess || pageAccess === "public") {
589
+ res.statusCode = 404;
590
+ res.end("Not found");
591
+ return;
592
+ }
593
+ const cookieHeader = req.headers.cookie || "";
594
+ const hasAuthToken = cookieHeader.includes("xyd-auth-token=");
595
+ const isBypassed = isAuthBypassed();
596
+ if (!hasAuthToken && !isBypassed) {
597
+ res.statusCode = 401;
598
+ res.end("Unauthorized");
599
+ return;
600
+ }
601
+ const pagePath = globalThis.__xydPagePathMapping?.[slug];
602
+ if (!pagePath) {
603
+ res.statusCode = 404;
604
+ res.end("Page not found");
605
+ return;
606
+ }
607
+ try {
608
+ const { ContentFS } = await import("@xyd-js/content");
609
+ const { markdownPlugins } = await import("@xyd-js/content/md");
610
+ const settings = globalThis.__xydSettings;
611
+ const mdPlugins = await markdownPlugins({ maxDepth: 2 }, settings);
612
+ const remarkPlugins = [...mdPlugins.remarkPlugins];
613
+ const rehypePlugins = [...mdPlugins.rehypePlugins];
614
+ if (globalThis.__xydUserMarkdownPlugins?.remark?.length) {
615
+ remarkPlugins.push(globalThis.__xydUserMarkdownPlugins.remark);
616
+ }
617
+ if (globalThis.__xydUserMarkdownPlugins?.rehype?.length) {
618
+ rehypePlugins.push(globalThis.__xydUserMarkdownPlugins.rehype);
619
+ }
620
+ const contentFs = new ContentFS(
621
+ settings,
622
+ remarkPlugins,
623
+ rehypePlugins,
624
+ mdPlugins.recmaPlugins,
625
+ globalThis.__xydUserMarkdownPlugins?.remarkRehypeHandlers || {}
626
+ );
627
+ const code = await contentFs.compile(pagePath);
628
+ res.setHeader("Content-Type", "application/javascript");
629
+ res.setHeader("Cache-Control", "no-store");
630
+ res.end(code);
631
+ } catch (e) {
632
+ console.error(
633
+ "[xyd:access-control] Failed to compile protected content:",
634
+ e
635
+ );
636
+ res.statusCode = 500;
637
+ res.end("Internal error");
638
+ }
639
+ });
640
+ },
641
+ async closeBundle() {
642
+ const { writeFileSync, mkdirSync } = await import("fs");
643
+ const { join } = await import("path");
644
+ const outputDir = join(
645
+ process.cwd(),
646
+ ".xyd/build/client/__xyd_protected_content"
647
+ );
648
+ mkdirSync(outputDir, { recursive: true });
649
+ const pagePathMapping = globalThis.__xydPagePathMapping || {};
650
+ for (const [slug, access] of Object.entries(getAccessMap())) {
651
+ if (access === "public") continue;
652
+ const normalizedSlug = slug.startsWith("/") ? slug.slice(1) : slug;
653
+ const pagePath = pagePathMapping[normalizedSlug] || pagePathMapping[slug];
654
+ if (!pagePath) continue;
655
+ try {
656
+ const { ContentFS } = await import("@xyd-js/content");
657
+ const { markdownPlugins } = await import("@xyd-js/content/md");
658
+ const settings = globalThis.__xydSettings;
659
+ const mdPlugins = await markdownPlugins({ maxDepth: 2 }, settings);
660
+ const remarkPlugins = [...mdPlugins.remarkPlugins];
661
+ const rehypePlugins = [...mdPlugins.rehypePlugins];
662
+ if (globalThis.__xydUserMarkdownPlugins?.remark?.length) {
663
+ remarkPlugins.push(globalThis.__xydUserMarkdownPlugins.remark);
664
+ }
665
+ if (globalThis.__xydUserMarkdownPlugins?.rehype?.length) {
666
+ rehypePlugins.push(globalThis.__xydUserMarkdownPlugins.rehype);
667
+ }
668
+ const contentFs = new ContentFS(
669
+ settings,
670
+ remarkPlugins,
671
+ rehypePlugins,
672
+ mdPlugins.recmaPlugins,
673
+ globalThis.__xydUserMarkdownPlugins?.remarkRehypeHandlers || {}
674
+ );
675
+ const code = await contentFs.compile(pagePath);
676
+ const fileName = `${encodeURIComponent(normalizedSlug)}.js`;
677
+ writeFileSync(join(outputDir, fileName), code, "utf-8");
678
+ } catch (e) {
679
+ console.error(
680
+ `[xyd:access-control] Failed to compile protected page ${slug}:`,
681
+ e
682
+ );
683
+ }
684
+ }
685
+ }
686
+ };
687
+ }
688
+
689
+ // src/components/LoginPage.tsx
690
+ import React2 from "react";
691
+
692
+ // src/components/AccessControlContext.tsx
693
+ import React, { createContext, useContext, useEffect, useState, useCallback } from "react";
694
+ var AccessControlCtx = createContext(null);
695
+ function useAccessControl() {
696
+ const ctx = useContext(AccessControlCtx);
697
+ if (!ctx) {
698
+ throw new Error("useAccessControl must be used inside <AccessControlProvider>");
699
+ }
700
+ return ctx;
701
+ }
702
+ function AccessControlProvider({ children }) {
703
+ const [config, setConfig] = useState(null);
704
+ const [error, setError] = useState("");
705
+ const [ready, setReady] = useState(false);
706
+ useEffect(() => {
707
+ const globalConfig = window.__xydAccessControlSettings?.accessControlConfig;
708
+ if (globalConfig) {
709
+ setConfig(globalConfig);
710
+ setReady(true);
711
+ return;
712
+ }
713
+ import("virtual:xyd-access-control-settings").then((mod) => {
714
+ setConfig(mod.accessControlConfig);
715
+ setReady(true);
716
+ }).catch(() => setReady(true));
717
+ }, []);
718
+ const providerType = config?.provider?.type || "jwt";
719
+ const loginUrl = config?.provider?.loginUrl || "";
720
+ const authorizationUrl = config?.provider?.authorizationUrl || "";
721
+ const hasExternalAuth = providerType === "oauth" ? !!authorizationUrl : !!loginUrl && loginUrl !== "/login";
722
+ const loginConfig = typeof config?.login === "string" ? {} : config?.login || {};
723
+ const title = loginConfig.title || "Sign in to access documentation";
724
+ const description = loginConfig.description || "";
725
+ const logo = loginConfig.logo || "";
726
+ const backgroundImage = loginConfig.backgroundImage || "";
727
+ const params = typeof window !== "undefined" ? new URLSearchParams(window.location.search) : null;
728
+ const redirectUrl = params?.get("redirect") || "/";
729
+ const storeTokenAndRedirect = useCallback((token) => {
730
+ const cookieName = config?.session?.cookieName || "xyd-auth-token";
731
+ localStorage.setItem(cookieName, token);
732
+ document.cookie = `${cookieName}=${token}; path=/; max-age=86400; SameSite=Lax`;
733
+ window.location.href = redirectUrl;
734
+ }, [config, redirectUrl]);
735
+ const signInWithOAuth = useCallback(() => {
736
+ if (!authorizationUrl) {
737
+ setError("No OAuth authorization URL configured");
738
+ return;
739
+ }
740
+ const callbackPath = config?.provider?.callbackPath || "/auth/callback";
741
+ const redirectUri = window.location.origin + callbackPath;
742
+ const scopes = config?.provider?.scopes || [];
743
+ const clientId = config?.provider?.clientId || "";
744
+ const oauthParams = new URLSearchParams({
745
+ client_id: clientId,
746
+ redirect_uri: redirectUri,
747
+ response_type: "code",
748
+ scope: scopes.join(" "),
749
+ state: redirectUrl
750
+ });
751
+ window.location.href = `${authorizationUrl}?${oauthParams.toString()}`;
752
+ }, [config, authorizationUrl, redirectUrl]);
753
+ const signInWithRedirect = useCallback(() => {
754
+ if (!loginUrl || loginUrl === "/login") {
755
+ setError("No external login URL configured");
756
+ return;
757
+ }
758
+ const sep = loginUrl.includes("?") ? "&" : "?";
759
+ window.location.href = `${loginUrl}${sep}redirect=${encodeURIComponent(redirectUrl)}`;
760
+ }, [loginUrl, redirectUrl]);
761
+ const signInWithGroups = useCallback((groups) => {
762
+ const hasDeploy = !!config?.deploy;
763
+ if (hasDeploy) {
764
+ const groupsParam = groups.length ? `&groups=${groups.join(",")}` : "";
765
+ window.location.href = `/auth/test-login?redirect=${encodeURIComponent(redirectUrl)}${groupsParam}`;
766
+ return;
767
+ }
768
+ if (!isDevEnvironment()) {
769
+ setError("Test login is only available in development mode.");
770
+ return;
771
+ }
772
+ const header = btoa(JSON.stringify({ alg: "HS256", typ: "JWT" }));
773
+ const payload = btoa(JSON.stringify({
774
+ sub: "test-user",
775
+ groups,
776
+ exp: Math.floor(Date.now() / 1e3) + 86400,
777
+ iat: Math.floor(Date.now() / 1e3)
778
+ }));
779
+ storeTokenAndRedirect(`${header}.${payload}.dGVzdA`);
780
+ }, [config, redirectUrl, storeTokenAndRedirect]);
781
+ const signInAsUser = useCallback(() => signInWithGroups([]), [signInWithGroups]);
782
+ const signInAsAdmin = useCallback(() => signInWithGroups(["admin"]), [signInWithGroups]);
783
+ const value = {
784
+ config,
785
+ ready,
786
+ error,
787
+ clearError: () => setError(""),
788
+ signInWithOAuth,
789
+ signInWithRedirect,
790
+ signInAsUser,
791
+ signInAsAdmin,
792
+ signInWithGroups,
793
+ providerType,
794
+ hasExternalAuth,
795
+ title,
796
+ description,
797
+ logo,
798
+ backgroundImage,
799
+ redirectUrl
800
+ };
801
+ return /* @__PURE__ */ React.createElement(AccessControlCtx.Provider, { value }, children);
802
+ }
803
+
804
+ // src/components/LoginPage.tsx
805
+ function LoginPage() {
806
+ return /* @__PURE__ */ React2.createElement(AccessControlProvider, null, /* @__PURE__ */ React2.createElement(LoginPageUI, null));
807
+ }
808
+ function LoginPageUI() {
809
+ const {
810
+ providerType,
811
+ hasExternalAuth,
812
+ title,
813
+ description,
814
+ logo,
815
+ backgroundImage,
816
+ error,
817
+ signInWithOAuth,
818
+ signInWithRedirect,
819
+ signInAsUser,
820
+ signInAsAdmin
821
+ } = useAccessControl();
822
+ const handleSignIn = () => {
823
+ if (providerType === "oauth") signInWithOAuth();
824
+ else signInWithRedirect();
825
+ };
826
+ const bgOverride = backgroundImage ? { backgroundImage: `url(${backgroundImage})`, backgroundSize: "cover", backgroundPosition: "center", background: "none" } : void 0;
827
+ return /* @__PURE__ */ React2.createElement("div", { className: "xyd-login-page", style: bgOverride }, /* @__PURE__ */ React2.createElement("div", { part: "card" }, logo && /* @__PURE__ */ React2.createElement("img", { part: "logo", src: logo, alt: "" }), /* @__PURE__ */ React2.createElement("h1", { part: "title" }, title), description && /* @__PURE__ */ React2.createElement("p", { part: "description" }, description), error && /* @__PURE__ */ React2.createElement("p", { part: "error" }, error), hasExternalAuth ? /* @__PURE__ */ React2.createElement("button", { part: "button", onClick: handleSignIn }, "Sign in") : /* @__PURE__ */ React2.createElement("div", { part: "actions" }, /* @__PURE__ */ React2.createElement("button", { part: "button", onClick: signInAsUser }, "Sign in as User"), /* @__PURE__ */ React2.createElement("button", { part: "button", "data-kind": "secondary", onClick: signInAsAdmin }, "Sign in as Admin"))));
828
+ }
829
+
830
+ // src/components/AuthCallbackPage.tsx
831
+ import React3, { useEffect as useEffect2, useState as useState2 } from "react";
832
+ function AuthCallbackPage() {
833
+ const [error, setError] = useState2("");
834
+ useEffect2(() => {
835
+ if (typeof window === "undefined") return;
836
+ import("virtual:xyd-access-control-settings").then((mod) => {
837
+ const config = mod.accessControlConfig;
838
+ handleCallback(config);
839
+ }).catch(() => {
840
+ handleCallback(null);
841
+ });
842
+ }, []);
843
+ function storeTokenAndRedirect(token, groups, redirect, cookieName) {
844
+ localStorage.setItem(cookieName, token);
845
+ document.cookie = `${cookieName}=${token}; path=/; max-age=86400; SameSite=Lax`;
846
+ window.__xydAuthState = {
847
+ authenticated: true,
848
+ groups,
849
+ token
850
+ };
851
+ document.documentElement.setAttribute("data-auth", "authenticated");
852
+ window.location.href = redirect;
853
+ }
854
+ async function handleCallback(config) {
855
+ const params = new URLSearchParams(window.location.search);
856
+ const hash = window.location.hash.slice(1);
857
+ const code = params.get("code");
858
+ const state = params.get("state") || "/";
859
+ const cookieName = config?.session?.cookieName || "xyd-auth-token";
860
+ const groupsClaim = config?.provider?.groupsClaim || "groups";
861
+ if (code && config?.provider?.type === "oauth") {
862
+ try {
863
+ await handleOAuthCallback(code, state, config, cookieName, groupsClaim);
864
+ } catch (e) {
865
+ setError(e instanceof Error ? e.message : "OAuth authentication failed");
866
+ }
867
+ return;
868
+ }
869
+ const token = params.get("token") || hash;
870
+ const redirect = params.get("redirect") || state;
871
+ if (token) {
872
+ try {
873
+ handleJWTCallback(token, redirect, cookieName, groupsClaim);
874
+ } catch (e) {
875
+ setError(e instanceof Error ? e.message : "JWT authentication failed");
876
+ }
877
+ return;
878
+ }
879
+ setError("No authentication data found in callback URL.");
880
+ }
881
+ function handleJWTCallback(hash, redirect, cookieName, groupsClaim) {
882
+ const parts = hash.split(".");
883
+ if (parts.length !== 3) throw new Error("Invalid JWT format");
884
+ const payload = JSON.parse(atob(parts[1]));
885
+ if (payload.exp && payload.exp * 1e3 < Date.now()) {
886
+ throw new Error("Token has expired");
887
+ }
888
+ storeTokenAndRedirect(hash, payload[groupsClaim] || [], redirect, cookieName);
889
+ }
890
+ async function handleOAuthCallback(code, redirect, config, cookieName, groupsClaim) {
891
+ const tokenUrl = config.provider.tokenUrl;
892
+ const userInfoUrl = config.provider.userInfoUrl;
893
+ const clientId = config.provider.clientId || "";
894
+ const callbackPath = config.provider.callbackPath || "/auth/callback";
895
+ const redirectUri = window.location.origin + callbackPath;
896
+ if (!tokenUrl) throw new Error("No token URL configured");
897
+ const tokenRes = await fetch(tokenUrl, {
898
+ method: "POST",
899
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
900
+ body: new URLSearchParams({
901
+ grant_type: "authorization_code",
902
+ code,
903
+ client_id: clientId,
904
+ redirect_uri: redirectUri
905
+ }).toString()
906
+ });
907
+ if (!tokenRes.ok) {
908
+ throw new Error(`Token exchange failed: ${tokenRes.status}`);
909
+ }
910
+ const tokenData = await tokenRes.json();
911
+ const accessToken = tokenData.access_token;
912
+ if (!accessToken) throw new Error("No access token in response");
913
+ let groups = [];
914
+ if (userInfoUrl) {
915
+ try {
916
+ const userRes = await fetch(userInfoUrl, {
917
+ headers: { Authorization: `Bearer ${accessToken}` }
918
+ });
919
+ if (userRes.ok) {
920
+ const userInfo = await userRes.json();
921
+ groups = userInfo[groupsClaim] || userInfo.roles || userInfo.groups || [];
922
+ }
923
+ } catch {
924
+ }
925
+ }
926
+ const header = btoa(JSON.stringify({ alg: "HS256", typ: "JWT" }));
927
+ const payload = btoa(JSON.stringify({
928
+ sub: "oauth-user",
929
+ [groupsClaim]: groups,
930
+ exp: Math.floor(Date.now() / 1e3) + 86400,
931
+ iat: Math.floor(Date.now() / 1e3),
932
+ access_token: accessToken
933
+ }));
934
+ const sessionToken = `${header}.${payload}.oauth`;
935
+ storeTokenAndRedirect(sessionToken, groups, redirect, cookieName);
936
+ }
937
+ if (error) {
938
+ return /* @__PURE__ */ React3.createElement("div", { className: "xyd-callback-page" }, /* @__PURE__ */ React3.createElement("div", { part: "card" }, /* @__PURE__ */ React3.createElement("h2", { part: "title", "data-error": "" }, "Authentication Error"), /* @__PURE__ */ React3.createElement("p", { part: "message" }, error), /* @__PURE__ */ React3.createElement("button", { part: "button", onClick: () => window.location.href = "/" }, "Go to homepage")));
939
+ }
940
+ return /* @__PURE__ */ React3.createElement("div", { className: "xyd-callback-page" }, /* @__PURE__ */ React3.createElement("div", { part: "card" }, /* @__PURE__ */ React3.createElement("p", { part: "message" }, "Authenticating...")));
941
+ }
942
+
943
+ // src/styles/login.css
944
+ var login_default = '@layer defaults {\n .xyd-login-page {\n display: flex;\n align-items: center;\n justify-content: center;\n position: fixed;\n inset: 0;\n overflow: hidden;\n font-family: var(--font-body-family, system-ui, sans-serif);\n background: radial-gradient(ellipse at 50% 50%, #f9b8d2 0%, #c6dff7 40%, #7ec8f0 100%);\n }\n\n .xyd-login-page [part="card"] {\n display: flex;\n flex-direction: column;\n align-items: center;\n width: 100%;\n max-width: 350px;\n padding: var(--xyd-padding-xxlarge, 44px);\n background: rgba(255, 255, 255, 0.92);\n border-radius: var(--xyd-border-radius-large, 16px);\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);\n backdrop-filter: blur(12px);\n }\n\n .xyd-login-page [part="logo"] {\n max-height: 30px;\n width: auto;\n margin-bottom: var(--xyd-padding-xlarge, 30px);\n }\n\n .xyd-login-page [part="title"] {\n font-size: var(--xyd-font-size-2xl, 32px);\n font-weight: var(--xyd-font-weight-bold, 700);\n line-height: var(--xyd-line-height-2xl, 48px);\n margin: 0 0 var(--xyd-padding-small, 6px);\n text-align: center;\n color: var(--dark100, #000);\n }\n\n .xyd-login-page [part="description"] {\n color: var(--dark48, #6e6e80);\n font-size: var(--xyd-font-size-small, 14px);\n line-height: var(--xyd-line-height-small, 20px);\n margin: 0 0 var(--xyd-padding-xxlarge, 44px);\n text-align: center;\n }\n\n .xyd-login-page [part="error"] {\n color: var(--xyd-text-color--error, #ef4444);\n font-size: var(--xyd-font-size-xsmall, 12px);\n line-height: var(--xyd-line-height-xsmall, 16px);\n margin: 0 0 var(--xyd-padding-medium, 10px);\n }\n\n .xyd-login-page [part="form"] {\n width: 100%;\n }\n\n .xyd-login-page [part="input"] {\n width: 100%;\n padding: var(--xyd-padding-medium, 10px) var(--xyd-padding-small, 6px);\n font-size: var(--xyd-font-size-small, 14px);\n line-height: var(--xyd-line-height-small, 20px);\n font-family: inherit;\n border: 1px solid var(--dark32, #ececf1);\n border-radius: var(--xyd-border-radius-small, 4px);\n margin-bottom: var(--xyd-padding-medium, 10px);\n box-sizing: border-box;\n outline: none;\n background: var(--white, #fff);\n color: var(--dark80, #111827);\n }\n\n .xyd-login-page [part="input"]:focus {\n border-color: var(--dark48, #6e6e80);\n }\n\n .xyd-login-page [part="actions"] {\n width: 100%;\n display: flex;\n flex-direction: column;\n gap: var(--xyd-padding-small, 6px);\n }\n\n .xyd-login-page [part="button"] {\n width: 100%;\n padding: var(--xyd-padding-medium, 10px) var(--xyd-padding-large, 18px);\n font-size: var(--xyd-font-size-small, 14px);\n font-weight: var(--xyd-font-weight-medium, 500);\n font-family: inherit;\n background-color: var(--xyd-button-primary-bg, var(--color-primary, #111));\n color: var(--xyd-button-primary-color, #fff);\n border: 1px solid var(--xyd-button-primary-border, transparent);\n border-radius: var(--xyd-button-border-radius, 6px);\n cursor: pointer;\n transition: background-color 0.15s linear;\n }\n\n .xyd-login-page [part="button"]:hover {\n background-color: var(--xyd-button-primary-bg-hover, var(--color-primary--active, #333));\n }\n\n .xyd-login-page [part="button"][data-kind="secondary"] {\n background-color: var(--xyd-button-secondary-bg, #f5f5f1);\n color: var(--xyd-button-secondary-color, #111);\n border: 1px solid var(--xyd-button-secondary-border, #ececf1);\n }\n\n .xyd-login-page [part="button"][data-kind="secondary"]:hover {\n background-color: var(--xyd-button-secondary-bg-hover, #e5e5e1);\n }\n}\n';
945
+
946
+ // src/styles/callback.css
947
+ var callback_default = '@layer defaults {\n .xyd-callback-page {\n display: flex;\n align-items: center;\n justify-content: center;\n position: fixed;\n inset: 0;\n overflow: hidden;\n font-family: var(--font-body-family, system-ui, sans-serif);\n background: radial-gradient(ellipse at 50% 50%, #f9b8d2 0%, #c6dff7 40%, #7ec8f0 100%);\n }\n\n .xyd-callback-page [part="card"] {\n display: flex;\n flex-direction: column;\n align-items: center;\n text-align: center;\n max-width: 350px;\n padding: var(--xyd-padding-xxlarge, 44px);\n background: rgba(255, 255, 255, 0.92);\n border-radius: var(--xyd-border-radius-large, 16px);\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);\n backdrop-filter: blur(12px);\n }\n\n .xyd-callback-page [part="title"] {\n font-size: var(--xyd-font-size-xlarge, 22px);\n font-weight: var(--xyd-font-weight-semibold, 600);\n line-height: var(--xyd-line-height-xlarge, 36px);\n margin: 0 0 var(--xyd-padding-small, 6px);\n color: var(--dark100, #000);\n }\n\n .xyd-callback-page [part="title"][data-error] {\n color: var(--xyd-text-color--error, #ef4444);\n }\n\n .xyd-callback-page [part="message"] {\n color: var(--dark48, #6e6e80);\n font-size: var(--xyd-font-size-small, 14px);\n line-height: var(--xyd-line-height-small, 20px);\n }\n\n .xyd-callback-page [part="button"] {\n margin-top: var(--xyd-padding-large, 18px);\n padding: var(--xyd-padding-medium, 10px) var(--xyd-padding-large, 18px);\n font-size: var(--xyd-font-size-small, 14px);\n font-weight: var(--xyd-font-weight-medium, 500);\n font-family: inherit;\n background-color: var(--xyd-button-primary-bg, var(--color-primary, #111));\n color: var(--xyd-button-primary-color, #fff);\n border: 1px solid var(--xyd-button-primary-border, transparent);\n border-radius: var(--xyd-button-border-radius, 6px);\n cursor: pointer;\n }\n\n .xyd-callback-page [part="button"]:hover {\n background-color: var(--xyd-button-primary-bg-hover, var(--color-primary--active, #333));\n }\n}';
948
+
949
+ // src/components/AuthGuard.tsx
950
+ import React4, {
951
+ createContext as createContext2,
952
+ useContext as useContext2,
953
+ useEffect as useEffect3,
954
+ useState as useState3,
955
+ useMemo
956
+ } from "react";
957
+ var AuthContext = createContext2({
958
+ authenticated: false,
959
+ groups: [],
960
+ token: null
961
+ });
962
+ function buildLoginUrl(loginUrl, returnPath) {
963
+ const separator = loginUrl.includes("?") ? "&" : "?";
964
+ return `${loginUrl}${separator}redirect=${encodeURIComponent(returnPath)}`;
965
+ }
966
+ function AuthGuard({ children, accessMap, config }) {
967
+ const [authState, setAuthState] = useState3(() => {
968
+ if (typeof window !== "undefined" && window.__xydAuthState) {
969
+ return window.__xydAuthState;
970
+ }
971
+ return { authenticated: false, groups: [], token: null };
972
+ });
973
+ useEffect3(() => {
974
+ const handleStorage = (e) => {
975
+ const cookieName = config.session?.cookieName || "xyd-auth-token";
976
+ if (e.key === cookieName) {
977
+ if (e.newValue) {
978
+ try {
979
+ const payload = JSON.parse(atob(e.newValue.split(".")[1]));
980
+ setAuthState({
981
+ authenticated: true,
982
+ groups: payload.groups || [],
983
+ token: e.newValue
984
+ });
985
+ } catch {
986
+ setAuthState({ authenticated: false, groups: [], token: null });
987
+ }
988
+ } else {
989
+ setAuthState({ authenticated: false, groups: [], token: null });
990
+ }
991
+ }
992
+ };
993
+ window.addEventListener("storage", handleStorage);
994
+ return () => window.removeEventListener("storage", handleStorage);
995
+ }, [config.session?.cookieName]);
996
+ const value = useMemo(() => authState, [authState]);
997
+ return /* @__PURE__ */ React4.createElement(AuthContext.Provider, { value }, /* @__PURE__ */ React4.createElement(AuthEnforcer, { accessMap, config }, children));
998
+ }
999
+ function AuthEnforcer({
1000
+ children,
1001
+ accessMap,
1002
+ config
1003
+ }) {
1004
+ const authState = useContext2(AuthContext);
1005
+ useEffect3(() => {
1006
+ if (typeof window === "undefined") return;
1007
+ const pathname = window.location.pathname;
1008
+ const pageAccess = accessMap[pathname];
1009
+ if (!pageAccess || pageAccess === "public") return;
1010
+ if (!authState.authenticated) {
1011
+ const loginUrl = config.provider.loginUrl;
1012
+ if (loginUrl) {
1013
+ window.location.href = buildLoginUrl(loginUrl, pathname);
1014
+ }
1015
+ return;
1016
+ }
1017
+ if (pageAccess !== "authenticated") {
1018
+ const requiredGroups = pageAccess.split(",");
1019
+ const hasAccess = requiredGroups.some(
1020
+ (g) => authState.groups.includes(g)
1021
+ );
1022
+ if (!hasAccess) {
1023
+ if (config.unauthorizedBehavior === "404") {
1024
+ window.location.href = "/404";
1025
+ } else {
1026
+ const loginUrl = config.provider.loginUrl;
1027
+ if (loginUrl) {
1028
+ window.location.href = buildLoginUrl(loginUrl, pathname);
1029
+ }
1030
+ }
1031
+ }
1032
+ }
1033
+ }, [authState, accessMap, config]);
1034
+ return /* @__PURE__ */ React4.createElement(React4.Fragment, null, children);
1035
+ }
1036
+ function useAuth() {
1037
+ const state = useContext2(AuthContext);
1038
+ return {
1039
+ ...state,
1040
+ login() {
1041
+ console.warn("[xyd:access-control] login() called without config");
1042
+ },
1043
+ logout() {
1044
+ if (typeof window === "undefined") return;
1045
+ const cookieName = "xyd-auth-token";
1046
+ localStorage.removeItem(cookieName);
1047
+ document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
1048
+ document.cookie = `${cookieName}-state=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
1049
+ window.__xydAuthState = {
1050
+ authenticated: false,
1051
+ groups: [],
1052
+ token: null
1053
+ };
1054
+ document.documentElement.setAttribute("data-auth", "anonymous");
1055
+ window.location.reload();
1056
+ }
1057
+ };
1058
+ }
1059
+
1060
+ // src/navigation.ts
1061
+ function filterSidebarGroups(sidebarGroups, accessMap, userGroups) {
1062
+ return sidebarGroups.map((group) => ({
1063
+ ...group,
1064
+ items: filterItems(group.items, accessMap, userGroups)
1065
+ })).filter((group) => group.items.length > 0);
1066
+ }
1067
+ function filterItems(items, accessMap, userGroups) {
1068
+ return items.filter((item) => {
1069
+ const href = item.href;
1070
+ if (!href) return true;
1071
+ const normalizedHref = href.startsWith("/") ? href : `/${href}`;
1072
+ const access = accessMap[normalizedHref] || accessMap[href];
1073
+ if (!access || access === "public") return true;
1074
+ if (access === "authenticated") return userGroups.length > 0;
1075
+ const requiredGroups = access.split(",");
1076
+ return requiredGroups.some((g) => userGroups.includes(g));
1077
+ }).map((item) => {
1078
+ if (item.items?.length) {
1079
+ return {
1080
+ ...item,
1081
+ items: filterItems(item.items, accessMap, userGroups)
1082
+ };
1083
+ }
1084
+ return item;
1085
+ });
1086
+ }
1087
+ function filterProtectedPaths(paths, accessMap) {
1088
+ return paths.filter((p) => {
1089
+ const normalizedPath = p.startsWith("/") ? p : `/${p}`;
1090
+ const access = accessMap[normalizedPath] || accessMap[p];
1091
+ return !access || access === "public";
1092
+ });
1093
+ }
1094
+
1095
+ // src/index.ts
1096
+ function edgeMiddlewarePlugin(config) {
1097
+ return {
1098
+ name: "xyd-plugin-access-control-deploy",
1099
+ apply: "build",
1100
+ async closeBundle() {
1101
+ if (!config.deploy) return;
1102
+ const outputDir = globalThis.__xydBuildOutputDir || process.cwd() + "/.xyd/build/client";
1103
+ switch (config.deploy.platform) {
1104
+ case "netlify-edge":
1105
+ await generateNetlifyEdge(config, outputDir);
1106
+ break;
1107
+ case "vercel-edge":
1108
+ await generateVercelMiddleware(config, outputDir);
1109
+ break;
1110
+ case "cloudflare-edge":
1111
+ await generateCloudflareMiddleware(config, outputDir);
1112
+ break;
1113
+ case "node-edge":
1114
+ await generateNodeServer(config, outputDir);
1115
+ break;
1116
+ }
1117
+ }
1118
+ };
1119
+ }
1120
+ function buildAccessMapFromGlobals(config) {
1121
+ const pagePathMapping = globalThis.__xydPagePathMapping || {};
1122
+ const accessMap = {};
1123
+ for (const pagePath of Object.keys(pagePathMapping)) {
1124
+ const access = resolvePageAccess(pagePath, {}, config);
1125
+ accessMap[pagePath] = access;
1126
+ const withSlash = pagePath.startsWith("/") ? pagePath : `/${pagePath}`;
1127
+ accessMap[withSlash] = access;
1128
+ }
1129
+ globalThis.__xydAccessMap = accessMap;
1130
+ return accessMap;
1131
+ }
1132
+ function AccessControlPlugin(pluginOptions) {
1133
+ return (settings) => {
1134
+ const config = pluginOptions;
1135
+ const cookieName = config.session?.cookieName || "xyd-auth-token";
1136
+ const groupsClaim = "groupsClaim" in config.provider ? config.provider.groupsClaim || "groups" : "groups";
1137
+ const authCss = `[data-auth="anonymous"] [data-auth-protected]{display:none!important}
1138
+ ${login_default}
1139
+ ${callback_default}`;
1140
+ const prehydrationScript = generateAuthPrehydrationScript(
1141
+ cookieName,
1142
+ groupsClaim
1143
+ );
1144
+ const head = [
1145
+ ["style", {}, authCss],
1146
+ ["script", {}, prehydrationScript]
1147
+ ];
1148
+ const vitePlugins = [
1149
+ virtualAccessControlSettingsPlugin(config, buildAccessMapFromGlobals),
1150
+ protectedContentPlugin(config, buildAccessMapFromGlobals)
1151
+ ];
1152
+ if (config.deploy) {
1153
+ vitePlugins.push(edgeMiddlewarePlugin(config));
1154
+ }
1155
+ const callbackRoute = "callbackPath" in config.provider ? config.provider.callbackPath || "/auth/jwt-callback" : "/auth/jwt-callback";
1156
+ const isCustomLogin = typeof config.login === "string";
1157
+ const loginConfig = isCustomLogin ? {} : config.login || {};
1158
+ return {
1159
+ name: "plugin-access-control",
1160
+ vite: vitePlugins,
1161
+ head,
1162
+ // Custom pages rendered inside the xyd theme layout
1163
+ pages: [
1164
+ {
1165
+ route: "/login",
1166
+ component: isCustomLogin ? LoginPage : LoginPage,
1167
+ // component is resolved at runtime
1168
+ dist: isCustomLogin ? config.login : "@xyd-js/plugin-access-control/LoginPage",
1169
+ metadata: {
1170
+ title: loginConfig.title || "Sign in",
1171
+ description: loginConfig.description
1172
+ },
1173
+ public: true
1174
+ },
1175
+ {
1176
+ route: callbackRoute,
1177
+ component: AuthCallbackPage,
1178
+ dist: "@xyd-js/plugin-access-control/AuthCallbackPage",
1179
+ metadata: { title: "Authenticating" },
1180
+ public: true
1181
+ }
1182
+ ]
1183
+ };
1184
+ };
1185
+ }
1186
+ export {
1187
+ AccessControlProvider,
1188
+ AuthCallbackPage,
1189
+ AuthGuard,
1190
+ LoginPage,
1191
+ buildAccessMap,
1192
+ AccessControlPlugin as default,
1193
+ evaluateAccess,
1194
+ filterProtectedPaths,
1195
+ filterSidebarGroups,
1196
+ resolvePageAccess,
1197
+ useAccessControl,
1198
+ useAuth
1199
+ };
1200
+ //# sourceMappingURL=index.js.map