@fynixorg/ui 1.0.13 → 1.0.14

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.
@@ -19,10 +19,71 @@ function escapeHTML(str) {
19
19
  .replace(/"/g, """)
20
20
  .replace(/'/g, "'")
21
21
  .replace(/`/g, "`")
22
- .replace(/\//g, "/");
22
+ .replace(/\//g, "/")
23
+ .replace(/=/g, "=")
24
+ .replace(/\(/g, "(")
25
+ .replace(/\)/g, ")")
26
+ .replace(/\{/g, "{")
27
+ .replace(/\}/g, "}")
28
+ .replace(/\[/g, "[")
29
+ .replace(/\]/g, "]");
30
+ }
31
+ function sanitizeContent(content) {
32
+ return content
33
+ .replace(/<script[^>]*>.*?<\/script>/gis, "")
34
+ .replace(/<iframe[^>]*>.*?<\/iframe>/gis, "")
35
+ .replace(/<object[^>]*>.*?<\/object>/gis, "")
36
+ .replace(/<embed[^>]*>/gi, "")
37
+ .replace(/<link[^>]*>/gi, "")
38
+ .replace(/on\w+\s*=/gi, "")
39
+ .replace(/javascript:/gi, "")
40
+ .replace(/vbscript:/gi, "")
41
+ .replace(/data:/gi, "")
42
+ .replace(/expression\s*\(/gi, "");
43
+ }
44
+ function sanitizeProps(props) {
45
+ const sanitized = {};
46
+ for (const [key, value] of Object.entries(props)) {
47
+ if (typeof key !== "string" ||
48
+ key.startsWith("__") ||
49
+ key.includes("javascript") ||
50
+ key.includes("on")) {
51
+ continue;
52
+ }
53
+ if (typeof value === "string") {
54
+ const cleanContent = sanitizeContent(value);
55
+ sanitized[key] = escapeHTML(cleanContent);
56
+ }
57
+ else if (typeof value === "object" && value !== null) {
58
+ if (Object.keys(value).length < 50) {
59
+ sanitized[key] = sanitizeProps(value);
60
+ }
61
+ }
62
+ else if (typeof value === "number" || typeof value === "boolean") {
63
+ sanitized[key] = value;
64
+ }
65
+ }
66
+ return sanitized;
23
67
  }
24
68
  function isValidURL(url) {
25
69
  try {
70
+ const suspiciousPatterns = [
71
+ /javascript:/i,
72
+ /vbscript:/i,
73
+ /data:/i,
74
+ /mailto:/i,
75
+ /tel:/i,
76
+ /ftp:/i,
77
+ /file:/i,
78
+ /%2f%2f/i,
79
+ /%5c%5c/i,
80
+ /\\\\/,
81
+ /@/,
82
+ ];
83
+ if (suspiciousPatterns.some((pattern) => pattern.test(url))) {
84
+ console.warn("[Router] Security: Suspicious URL pattern blocked");
85
+ return false;
86
+ }
26
87
  const parsed = new URL(url, window.location.origin);
27
88
  if (parsed.origin !== window.location.origin) {
28
89
  console.warn("[Router] Security: Cross-origin navigation blocked");
@@ -32,6 +93,15 @@ function isValidURL(url) {
32
93
  console.warn("[Router] Security: Dangerous protocol blocked:", parsed.protocol);
33
94
  return false;
34
95
  }
96
+ const decodedPath = decodeURIComponent(parsed.pathname);
97
+ if (decodedPath !== parsed.pathname && /[<>"'`]/.test(decodedPath)) {
98
+ console.warn("[Router] Security: Encoded XSS attempt blocked");
99
+ return false;
100
+ }
101
+ if (url.length > 2048) {
102
+ console.warn("[Router] Security: Excessively long URL blocked");
103
+ return false;
104
+ }
35
105
  return true;
36
106
  }
37
107
  catch (e) {
@@ -69,16 +139,12 @@ function tryGlobPaths() {
69
139
  let modules = import.meta.glob("/src/**/*.{fnx,tsx,jsx,ts,js}", {
70
140
  eager: true,
71
141
  });
72
- console.log("[Router] Glob attempt 1 (/src/**):", Object.keys(modules));
73
142
  if (Object.keys(modules).length === 0) {
74
143
  modules = import.meta.glob(["./**/*.fnx", "./**/*.tsx", "./**/*.jsx", "./**/*.ts", "./**/*.js"], { eager: true });
75
- console.log("[Router] Glob attempt 2 (./**):", Object.keys(modules));
76
144
  }
77
145
  if (Object.keys(modules).length === 0) {
78
146
  modules = import.meta.glob(["../**/*.fnx", "../**/*.tsx", "../**/*.jsx"], { eager: true });
79
- console.log("[Router] Glob attempt 3 (../**):", Object.keys(modules));
80
147
  }
81
- console.log("[Router] Final modules loaded:", Object.keys(modules).length);
82
148
  return modules || {};
83
149
  }
84
150
  catch (error) {
@@ -167,13 +233,14 @@ function updateMetaTags(meta = {}) {
167
233
  if (!meta || typeof meta !== "object")
168
234
  return;
169
235
  if (meta.title && typeof meta.title === "string") {
170
- document.title = escapeHTML(meta.title);
236
+ const sanitizedTitle = escapeHTML(meta.title).substring(0, 60);
237
+ document.title = sanitizedTitle;
171
238
  }
172
239
  MANAGED_META.forEach((def) => {
173
240
  const value = meta[def.key];
174
241
  const selector = def.name
175
- ? `meta[name="${def.name}"]`
176
- : `meta[property="${def.property}"]`;
242
+ ? `meta[name="${CSS.escape(def.name)}"]`
243
+ : `meta[property="${CSS.escape(def.property || "")}"]`;
177
244
  let el = document.querySelector(selector);
178
245
  if (value == null) {
179
246
  if (el)
@@ -182,6 +249,12 @@ function updateMetaTags(meta = {}) {
182
249
  }
183
250
  if (typeof value !== "string")
184
251
  return;
252
+ const cleanValue = sanitizeContent(value);
253
+ const sanitizedValue = escapeHTML(cleanValue).substring(0, 300);
254
+ if (/javascript:|vbscript:|data:|<|>/i.test(sanitizedValue)) {
255
+ console.warn(`[Router] Security: Blocked suspicious meta content for ${def.key}`);
256
+ return;
257
+ }
185
258
  if (!el) {
186
259
  el = document.createElement("meta");
187
260
  if (def.name)
@@ -190,9 +263,177 @@ function updateMetaTags(meta = {}) {
190
263
  el.setAttribute("property", def.property);
191
264
  document.head.appendChild(el);
192
265
  }
193
- el.setAttribute("content", escapeHTML(value));
266
+ el.setAttribute("content", sanitizedValue);
194
267
  });
195
268
  }
269
+ class EnterpriseRouter {
270
+ constructor() {
271
+ this.routeCache = new Map();
272
+ this.preloadQueue = new Set();
273
+ this.routeMatchCache = new Map();
274
+ this.routes = {};
275
+ }
276
+ setRoutes(routes) {
277
+ if (!routes || typeof routes !== "object") {
278
+ console.warn("[EnterpriseRouter] Invalid routes configuration");
279
+ return;
280
+ }
281
+ this.routes = routes;
282
+ }
283
+ async preloadRoute(path) {
284
+ if (this.routeCache.has(path))
285
+ return;
286
+ const route = this.routes[path];
287
+ if (route?.component) {
288
+ const loadRoute = async () => {
289
+ try {
290
+ const component = await route.component();
291
+ this.routeCache.set(path, component);
292
+ route.prefetch?.forEach((prefetchPath) => {
293
+ this.preloadQueue.add(prefetchPath);
294
+ });
295
+ console.log(`[EnterpriseRouter] Preloaded route: ${path}`);
296
+ }
297
+ catch (error) {
298
+ console.warn(`[EnterpriseRouter] Failed to preload route ${path}:`, error);
299
+ }
300
+ };
301
+ if ("requestIdleCallback" in window) {
302
+ requestIdleCallback(loadRoute);
303
+ }
304
+ else {
305
+ setTimeout(loadRoute, 0);
306
+ }
307
+ }
308
+ }
309
+ matchRoute(path) {
310
+ const cached = this.routeMatchCache.get(path);
311
+ if (cached !== undefined)
312
+ return cached;
313
+ const match = this.computeRouteMatch(path);
314
+ if (this.routeMatchCache.size > 100) {
315
+ const firstKey = this.routeMatchCache.keys().next().value;
316
+ if (firstKey !== undefined) {
317
+ this.routeMatchCache.delete(firstKey);
318
+ }
319
+ }
320
+ this.routeMatchCache.set(path, match);
321
+ return match;
322
+ }
323
+ computeRouteMatch(path) {
324
+ const segments = path.split("/").filter(Boolean);
325
+ for (const [routePath, routeConfig] of Object.entries(this.routes)) {
326
+ const routeSegments = routePath.split("/").filter(Boolean);
327
+ if (segments.length !== routeSegments.length)
328
+ continue;
329
+ const params = {};
330
+ let isMatch = true;
331
+ for (let i = 0; i < segments.length; i++) {
332
+ const segment = segments[i];
333
+ const routeSegment = routeSegments[i];
334
+ if (routeSegment && segment) {
335
+ if (routeSegment.startsWith(":")) {
336
+ params[routeSegment.slice(1)] = segment;
337
+ }
338
+ else if (segment !== routeSegment) {
339
+ isMatch = false;
340
+ break;
341
+ }
342
+ }
343
+ else {
344
+ isMatch = false;
345
+ break;
346
+ }
347
+ }
348
+ if (isMatch) {
349
+ return {
350
+ component: routeConfig.component,
351
+ params,
352
+ meta: typeof routeConfig.meta === "function"
353
+ ? routeConfig.meta(params)
354
+ : routeConfig.meta,
355
+ };
356
+ }
357
+ }
358
+ return null;
359
+ }
360
+ async checkRouteGuard(route, path, params) {
361
+ if (!route.guard)
362
+ return true;
363
+ if (route.guard.canActivate) {
364
+ const canActivate = await route.guard.canActivate(path, params);
365
+ return canActivate;
366
+ }
367
+ return true;
368
+ }
369
+ getPreloadedComponent(path) {
370
+ return this.routeCache.get(path);
371
+ }
372
+ clearCache() {
373
+ this.routeCache.clear();
374
+ this.routeMatchCache.clear();
375
+ this.preloadQueue.clear();
376
+ }
377
+ }
378
+ class LayoutRouter {
379
+ constructor() {
380
+ this.layoutCache = new Map();
381
+ this.keepAliveComponents = new Map();
382
+ }
383
+ renderNestedRoutes(routes, segments) {
384
+ if (segments.length === 0)
385
+ return null;
386
+ const [currentSegment, ...remainingSegments] = segments;
387
+ const currentRoute = routes.find((r) => r.path === currentSegment);
388
+ if (!currentRoute)
389
+ return null;
390
+ let content;
391
+ if (remainingSegments.length > 0 && currentRoute.children) {
392
+ content = this.renderNestedRoutes(currentRoute.children, remainingSegments);
393
+ }
394
+ else {
395
+ if (currentRoute.keepAlive && currentSegment) {
396
+ content = this.renderKeepAlive(currentRoute.component, currentSegment);
397
+ }
398
+ else {
399
+ content = this.renderComponent(currentRoute.component);
400
+ }
401
+ }
402
+ if (currentRoute.layout) {
403
+ const layoutKey = `${currentSegment}_layout`;
404
+ let layoutComponent = this.layoutCache.get(layoutKey);
405
+ if (!layoutComponent) {
406
+ layoutComponent = this.renderComponent(currentRoute.layout);
407
+ this.layoutCache.set(layoutKey, layoutComponent);
408
+ }
409
+ return this.renderComponent(currentRoute.layout, { children: content });
410
+ }
411
+ return content;
412
+ }
413
+ renderKeepAlive(component, key) {
414
+ if (this.keepAliveComponents.has(key)) {
415
+ return this.keepAliveComponents.get(key);
416
+ }
417
+ const rendered = this.renderComponent(component);
418
+ this.keepAliveComponents.set(key, rendered);
419
+ return rendered;
420
+ }
421
+ renderComponent(component, props = {}) {
422
+ try {
423
+ return component(props);
424
+ }
425
+ catch (error) {
426
+ console.error("[LayoutRouter] Component render error:", error);
427
+ return null;
428
+ }
429
+ }
430
+ cleanup() {
431
+ this.layoutCache.clear();
432
+ this.keepAliveComponents.clear();
433
+ }
434
+ }
435
+ const enterpriseRouter = new EnterpriseRouter();
436
+ const layoutRouter = new LayoutRouter();
196
437
  function createFynix() {
197
438
  const isDevMode = import.meta.hot !== undefined;
198
439
  if (routerInstance && isRouterInitialized && !isDevMode) {
@@ -210,6 +451,8 @@ function createFynix() {
210
451
  let isDestroyed = false;
211
452
  let listenerCount = 0;
212
453
  let renderTimeout = null;
454
+ let lastNavigationTime = 0;
455
+ const NAVIGATION_RATE_LIMIT = 100;
213
456
  const listeners = [];
214
457
  if (!window[PROPS_NAMESPACE]) {
215
458
  window[PROPS_NAMESPACE] = {};
@@ -262,13 +505,24 @@ function createFynix() {
262
505
  routes[routePath] = component;
263
506
  }
264
507
  }
265
- function renderRouteImmediate() {
508
+ async function renderRouteImmediate() {
266
509
  if (isDestroyed)
267
510
  return;
268
511
  const path = normalizePath(window.location.pathname);
269
512
  let Page = routes[path];
270
513
  let params = {};
271
514
  let routeProps = {};
515
+ const enterpriseMatch = enterpriseRouter.matchRoute(path);
516
+ if (enterpriseMatch) {
517
+ const preloadedComponent = enterpriseRouter.getPreloadedComponent(path);
518
+ if (preloadedComponent) {
519
+ Page = preloadedComponent;
520
+ }
521
+ else {
522
+ Page = enterpriseMatch.component;
523
+ }
524
+ params = enterpriseMatch.params;
525
+ }
272
526
  if (!Page) {
273
527
  const match = matchDynamicRoute(path, dynamicRoutes);
274
528
  if (match) {
@@ -282,8 +536,30 @@ function createFynix() {
282
536
  return;
283
537
  }
284
538
  if (!Page) {
285
- root.innerHTML = `<h2>404 Not Found</h2><p>Path: ${escapeHTML(path)}</p>`;
539
+ root.innerHTML = "";
540
+ const container = document.createElement("div");
541
+ container.style.cssText =
542
+ "padding: 2rem; text-align: center; font-family: system-ui, sans-serif;";
543
+ const heading = document.createElement("h2");
544
+ heading.textContent = "404 Not Found";
545
+ heading.style.cssText = "color: #dc2626; margin-bottom: 1rem;";
546
+ const pathInfo = document.createElement("p");
547
+ const safePath = escapeHTML(sanitizeContent(path));
548
+ pathInfo.textContent = `Path: ${safePath}`;
549
+ pathInfo.style.cssText = "color: #6b7280; margin-bottom: 2rem;";
550
+ const backButton = document.createElement("button");
551
+ backButton.textContent = "Go Back";
552
+ backButton.style.cssText =
553
+ "padding: 0.5rem 1rem; background: #3b82f6; color: white; border: none; border-radius: 0.25rem; cursor: pointer;";
554
+ backButton.onclick = () => window.history.back();
555
+ container.appendChild(heading);
556
+ container.appendChild(pathInfo);
557
+ container.appendChild(backButton);
558
+ root.appendChild(container);
286
559
  updateMetaTags({ title: "404 - Page Not Found" });
560
+ ["/", "/home", "/about"].forEach((commonPath) => {
561
+ enterpriseRouter.preloadRoute(commonPath).catch(console.warn);
562
+ });
287
563
  return;
288
564
  }
289
565
  const state = (window.history.state || {});
@@ -301,17 +577,23 @@ function createFynix() {
301
577
  const meta = typeof Page.meta === "function" ? Page.meta(params) : Page.meta;
302
578
  updateMetaTags(meta);
303
579
  }
304
- window.__lastRouteProps = {
580
+ const unsafeProps = {
305
581
  ...routeProps,
306
582
  ...passedProps,
307
583
  params,
308
584
  };
585
+ const safeProps = sanitizeProps(unsafeProps);
586
+ window.__lastRouteProps = safeProps;
309
587
  try {
310
- mount(Page, rootSelector, window.__lastRouteProps);
588
+ mount(Page, rootSelector, safeProps);
311
589
  }
312
590
  catch (err) {
313
591
  console.error("[Router] Mount failed:", err);
314
- root.innerHTML = `<pre style="color:red;">Mount Error occurred</pre>`;
592
+ root.innerHTML = "";
593
+ const errorDiv = document.createElement("pre");
594
+ errorDiv.style.color = "red";
595
+ errorDiv.textContent = "Mount Error occurred";
596
+ root.appendChild(errorDiv);
315
597
  }
316
598
  currentPath = path;
317
599
  }
@@ -329,6 +611,12 @@ function createFynix() {
329
611
  function navigate(path, props = {}) {
330
612
  if (isDestroyed)
331
613
  return;
614
+ const now = Date.now();
615
+ if (now - lastNavigationTime < NAVIGATION_RATE_LIMIT) {
616
+ console.warn("[Router] Security: Navigation rate limited");
617
+ return;
618
+ }
619
+ lastNavigationTime = now;
332
620
  const normalizedPath = normalizePath(path);
333
621
  if (!isValidURL(window.location.origin + normalizedPath)) {
334
622
  console.error("[Router] Invalid navigation URL");
@@ -336,8 +624,10 @@ function createFynix() {
336
624
  }
337
625
  if (normalizedPath === currentPath)
338
626
  return;
627
+ enterpriseRouter.preloadRoute(normalizedPath).catch(console.warn);
628
+ const sanitizedProps = sanitizeProps(props);
339
629
  const cacheKey = generateCacheKey();
340
- addToCache(propsCache, cacheKey, props);
630
+ addToCache(propsCache, cacheKey, sanitizedProps);
341
631
  try {
342
632
  window.history.pushState({ __fynixCacheKey: cacheKey }, "", normalizedPath);
343
633
  renderRoute();
@@ -349,13 +639,20 @@ function createFynix() {
349
639
  function replace(path, props = {}) {
350
640
  if (isDestroyed)
351
641
  return;
642
+ const now = Date.now();
643
+ if (now - lastNavigationTime < NAVIGATION_RATE_LIMIT) {
644
+ console.warn("[Router] Security: Replace rate limited");
645
+ return;
646
+ }
647
+ lastNavigationTime = now;
352
648
  const normalizedPath = normalizePath(path);
353
649
  if (!isValidURL(window.location.origin + normalizedPath)) {
354
650
  console.error("[Router] Invalid replace URL");
355
651
  return;
356
652
  }
653
+ const sanitizedProps = sanitizeProps(props);
357
654
  const cacheKey = generateCacheKey();
358
- addToCache(propsCache, cacheKey, props);
655
+ addToCache(propsCache, cacheKey, sanitizedProps);
359
656
  try {
360
657
  window.history.replaceState({ __fynixCacheKey: cacheKey }, "", normalizedPath);
361
658
  renderRoute();
@@ -459,6 +756,8 @@ function createFynix() {
459
756
  renderTimeout = null;
460
757
  }
461
758
  isDestroyed = true;
759
+ enterpriseRouter.clearCache();
760
+ layoutRouter.cleanup();
462
761
  listeners.forEach(({ element, event, handler }) => {
463
762
  try {
464
763
  element.removeEventListener(event, handler);
@@ -519,6 +818,15 @@ function createFynix() {
519
818
  cleanup,
520
819
  routes,
521
820
  dynamicRoutes,
821
+ preloadRoute: enterpriseRouter.preloadRoute.bind(enterpriseRouter),
822
+ clearCache: () => {
823
+ enterpriseRouter.clearCache();
824
+ layoutRouter.cleanup();
825
+ },
826
+ enableNestedRouting: (nestedRoutes) => {
827
+ router.nestedRoutes = nestedRoutes;
828
+ console.log("[Router] Nested routing enabled with", nestedRoutes.length, "routes");
829
+ },
522
830
  };
523
831
  routerInstance = router;
524
832
  return router;