@nsxbet/admin-sdk 0.5.1 → 0.6.0

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.
Files changed (173) hide show
  1. package/CHECKLIST.md +40 -10
  2. package/README.md +337 -36
  3. package/dist/auth/client/gateway-token.d.ts +19 -0
  4. package/dist/auth/client/gateway-token.js +89 -0
  5. package/dist/auth/client/in-memory.d.ts +5 -1
  6. package/dist/auth/client/in-memory.js +75 -38
  7. package/dist/auth/client/index.d.ts +0 -1
  8. package/dist/auth/client/interface.d.ts +6 -3
  9. package/dist/auth/client/keycloak.d.ts +0 -1
  10. package/dist/auth/client/keycloak.js +6 -3
  11. package/dist/auth/components/UserSelector.d.ts +0 -1
  12. package/dist/auth/components/UserSelector.js +89 -7
  13. package/dist/auth/components/index.d.ts +0 -1
  14. package/dist/auth/index.d.ts +0 -1
  15. package/dist/components/AuthProvider.d.ts +0 -1
  16. package/dist/components/Timestamp.d.ts +7 -0
  17. package/dist/components/Timestamp.js +50 -0
  18. package/dist/hooks/useAuth.d.ts +0 -1
  19. package/dist/hooks/useAuth.js +1 -1
  20. package/dist/hooks/useFetch.d.ts +0 -1
  21. package/dist/hooks/useI18n.d.ts +0 -1
  22. package/dist/hooks/usePlatformAPI.d.ts +0 -1
  23. package/dist/hooks/useTelemetry.d.ts +0 -1
  24. package/dist/hooks/useTimestamp.d.ts +8 -0
  25. package/dist/hooks/useTimestamp.js +122 -0
  26. package/dist/i18n/config.d.ts +20 -2
  27. package/dist/i18n/config.js +48 -0
  28. package/dist/i18n/index.d.ts +2 -3
  29. package/dist/i18n/index.js +1 -1
  30. package/dist/i18n/locales/en-US.json +95 -18
  31. package/dist/i18n/locales/es.json +95 -18
  32. package/dist/i18n/locales/pt-BR.json +95 -18
  33. package/dist/i18n/locales/ro.json +95 -18
  34. package/dist/index.d.ts +11 -7
  35. package/dist/index.js +5 -1
  36. package/dist/registry/AdminShellRegistry.d.ts +1 -2
  37. package/dist/registry/cache/cached-catalog.d.ts +11 -0
  38. package/dist/registry/cache/cached-catalog.js +42 -0
  39. package/dist/registry/cache/catalog-cache.d.ts +10 -0
  40. package/dist/registry/cache/catalog-cache.js +58 -0
  41. package/dist/registry/cache/index.d.ts +5 -0
  42. package/dist/registry/cache/index.js +3 -0
  43. package/dist/registry/cache/types.d.ts +20 -0
  44. package/dist/registry/cache/types.js +3 -0
  45. package/dist/registry/client/http.d.ts +0 -1
  46. package/dist/registry/client/http.js +13 -0
  47. package/dist/registry/client/in-memory.d.ts +0 -1
  48. package/dist/registry/client/in-memory.js +117 -12
  49. package/dist/registry/client/index.d.ts +0 -1
  50. package/dist/registry/client/interface.d.ts +21 -6
  51. package/dist/registry/index.d.ts +5 -2
  52. package/dist/registry/index.js +4 -0
  53. package/dist/registry/types/index.d.ts +2 -3
  54. package/dist/registry/types/manifest.d.ts +20 -24
  55. package/dist/registry/types/manifest.js +17 -18
  56. package/dist/registry/types/module.d.ts +43 -14
  57. package/dist/registry/useRegistryPolling.d.ts +15 -0
  58. package/dist/registry/useRegistryPolling.js +66 -0
  59. package/dist/router/DynamicModule.d.ts +6 -22
  60. package/dist/router/DynamicModule.js +25 -48
  61. package/dist/router/ModuleErrorBoundary.d.ts +39 -0
  62. package/dist/router/ModuleErrorBoundary.js +101 -0
  63. package/dist/router/index.d.ts +1 -1
  64. package/dist/router/url-allowlist.d.ts +22 -0
  65. package/dist/router/url-allowlist.js +65 -0
  66. package/dist/shell/AdminShell.d.ts +0 -1
  67. package/dist/shell/AdminShell.js +178 -43
  68. package/dist/shell/BackofficeShell.d.ts +0 -1
  69. package/dist/shell/BackofficeShell.js +59 -25
  70. package/dist/shell/components/CommandPalette.d.ts +0 -1
  71. package/dist/shell/components/CommandPalette.js +26 -50
  72. package/dist/shell/components/DevtoolsPanel.d.ts +11 -0
  73. package/dist/shell/components/DevtoolsPanel.js +145 -0
  74. package/dist/shell/components/HomePage.d.ts +0 -1
  75. package/dist/shell/components/HomePage.js +9 -4
  76. package/dist/shell/components/LeftNav.d.ts +0 -1
  77. package/dist/shell/components/LeftNav.js +91 -93
  78. package/dist/shell/components/MainContent.d.ts +3 -2
  79. package/dist/shell/components/MainContent.js +8 -23
  80. package/dist/shell/components/ModuleOverview.d.ts +0 -1
  81. package/dist/shell/components/ModuleOverview.js +4 -20
  82. package/dist/shell/components/ProfilePage.d.ts +0 -1
  83. package/dist/shell/components/ProfilePage.js +1 -1
  84. package/dist/shell/components/RegistryPage.d.ts +0 -1
  85. package/dist/shell/components/RegistryPage.js +154 -64
  86. package/dist/shell/components/RegistryStatusBanner.d.ts +6 -0
  87. package/dist/shell/components/RegistryStatusBanner.js +31 -0
  88. package/dist/shell/components/RegistryUnavailable.d.ts +4 -0
  89. package/dist/shell/components/RegistryUnavailable.js +7 -0
  90. package/dist/shell/components/SettingsPage.d.ts +0 -1
  91. package/dist/shell/components/StackedPanel.d.ts +15 -0
  92. package/dist/shell/components/StackedPanel.js +45 -0
  93. package/dist/shell/components/TopBar.d.ts +4 -2
  94. package/dist/shell/components/TopBar.js +9 -3
  95. package/dist/shell/components/UpdateBanner.d.ts +5 -0
  96. package/dist/shell/components/UpdateBanner.js +8 -0
  97. package/dist/shell/components/index.d.ts +4 -1
  98. package/dist/shell/components/index.js +2 -0
  99. package/dist/shell/components/theme-provider.d.ts +0 -1
  100. package/dist/shell/components/theme-provider.js +8 -5
  101. package/dist/shell/hooks/useCspViolations.d.ts +12 -0
  102. package/dist/shell/hooks/useCspViolations.js +34 -0
  103. package/dist/shell/index.d.ts +1 -2
  104. package/dist/shell/polling-config.d.ts +10 -0
  105. package/dist/shell/polling-config.js +26 -0
  106. package/dist/shell/search/fuzzy.d.ts +0 -1
  107. package/dist/shell/search/index.d.ts +0 -1
  108. package/dist/shell/telemetry.d.ts +0 -1
  109. package/dist/shell/types.d.ts +34 -18
  110. package/dist/tailwind/index.d.ts +0 -1
  111. package/dist/types/keycloak.d.ts +0 -1
  112. package/dist/types/platform.d.ts +12 -1
  113. package/dist/vite/AdminShellSharedDeps.d.ts +64 -0
  114. package/dist/vite/AdminShellSharedDeps.js +215 -0
  115. package/dist/vite/config.d.ts +2 -2
  116. package/dist/vite/config.js +5 -7
  117. package/dist/vite/i18n-plugin.d.ts +13 -0
  118. package/dist/vite/i18n-plugin.js +81 -0
  119. package/dist/vite/index.d.ts +2 -1
  120. package/dist/vite/index.js +2 -0
  121. package/dist/vite/plugins.d.ts +0 -1
  122. package/package.json +6 -2
  123. package/dist/auth/client/in-memory.d.ts.map +0 -1
  124. package/dist/auth/client/index.d.ts.map +0 -1
  125. package/dist/auth/client/interface.d.ts.map +0 -1
  126. package/dist/auth/client/keycloak.d.ts.map +0 -1
  127. package/dist/auth/components/UserSelector.d.ts.map +0 -1
  128. package/dist/auth/components/index.d.ts.map +0 -1
  129. package/dist/auth/index.d.ts.map +0 -1
  130. package/dist/components/AuthProvider.d.ts.map +0 -1
  131. package/dist/hooks/useAuth.d.ts.map +0 -1
  132. package/dist/hooks/useFetch.d.ts.map +0 -1
  133. package/dist/hooks/useI18n.d.ts.map +0 -1
  134. package/dist/hooks/usePlatformAPI.d.ts.map +0 -1
  135. package/dist/hooks/useTelemetry.d.ts.map +0 -1
  136. package/dist/i18n/config.d.ts.map +0 -1
  137. package/dist/i18n/index.d.ts.map +0 -1
  138. package/dist/index.d.ts.map +0 -1
  139. package/dist/registry/AdminShellRegistry.d.ts.map +0 -1
  140. package/dist/registry/client/http.d.ts.map +0 -1
  141. package/dist/registry/client/in-memory.d.ts.map +0 -1
  142. package/dist/registry/client/index.d.ts.map +0 -1
  143. package/dist/registry/client/interface.d.ts.map +0 -1
  144. package/dist/registry/index.d.ts.map +0 -1
  145. package/dist/registry/types/index.d.ts.map +0 -1
  146. package/dist/registry/types/manifest.d.ts.map +0 -1
  147. package/dist/registry/types/module.d.ts.map +0 -1
  148. package/dist/router/DynamicModule.d.ts.map +0 -1
  149. package/dist/router/index.d.ts.map +0 -1
  150. package/dist/shell/AdminShell.d.ts.map +0 -1
  151. package/dist/shell/BackofficeShell.d.ts.map +0 -1
  152. package/dist/shell/components/CommandPalette.d.ts.map +0 -1
  153. package/dist/shell/components/HomePage.d.ts.map +0 -1
  154. package/dist/shell/components/LeftNav.d.ts.map +0 -1
  155. package/dist/shell/components/MainContent.d.ts.map +0 -1
  156. package/dist/shell/components/ModuleOverview.d.ts.map +0 -1
  157. package/dist/shell/components/ProfilePage.d.ts.map +0 -1
  158. package/dist/shell/components/RegistryPage.d.ts.map +0 -1
  159. package/dist/shell/components/SettingsPage.d.ts.map +0 -1
  160. package/dist/shell/components/TopBar.d.ts.map +0 -1
  161. package/dist/shell/components/index.d.ts.map +0 -1
  162. package/dist/shell/components/theme-provider.d.ts.map +0 -1
  163. package/dist/shell/index.d.ts.map +0 -1
  164. package/dist/shell/search/fuzzy.d.ts.map +0 -1
  165. package/dist/shell/search/index.d.ts.map +0 -1
  166. package/dist/shell/telemetry.d.ts.map +0 -1
  167. package/dist/shell/types.d.ts.map +0 -1
  168. package/dist/tailwind/index.d.ts.map +0 -1
  169. package/dist/types/keycloak.d.ts.map +0 -1
  170. package/dist/types/platform.d.ts.map +0 -1
  171. package/dist/vite/config.d.ts.map +0 -1
  172. package/dist/vite/index.d.ts.map +0 -1
  173. package/dist/vite/plugins.d.ts.map +0 -1
@@ -1,4 +1,6 @@
1
- import { Component, type ReactNode } from "react";
1
+ import { type ReactNode } from "react";
2
+ import { ModuleErrorBoundary } from "./ModuleErrorBoundary";
3
+ import type { ModuleInfo } from "./ModuleErrorBoundary";
2
4
  interface DynamicModuleProps {
3
5
  /** Base URL where the module is hosted (e.g., "http://localhost:5001") */
4
6
  baseUrl: string;
@@ -8,30 +10,13 @@ interface DynamicModuleProps {
8
10
  fallback?: ReactNode;
9
11
  /** Custom error fallback */
10
12
  errorFallback?: ReactNode;
13
+ /** Module metadata for error boundary ownership display */
14
+ moduleInfo: ModuleInfo;
11
15
  }
12
16
  /**
13
17
  * Loading spinner shown while module is being fetched
14
18
  */
15
19
  declare function ModuleLoading(): import("react/jsx-runtime").JSX.Element;
16
- /**
17
- * Error boundary for catching module load errors
18
- */
19
- interface ErrorBoundaryProps {
20
- children: ReactNode;
21
- fallback?: ReactNode;
22
- onRetry?: () => void;
23
- }
24
- interface ErrorBoundaryState {
25
- hasError: boolean;
26
- error?: Error;
27
- }
28
- declare class ModuleErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
29
- constructor(props: ErrorBoundaryProps);
30
- static getDerivedStateFromError(error: Error): ErrorBoundaryState;
31
- componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void;
32
- handleRetry: () => void;
33
- render(): string | number | boolean | Iterable<ReactNode> | import("react/jsx-runtime").JSX.Element | null | undefined;
34
- }
35
20
  /**
36
21
  * DynamicModule - Loads a module from a URL using React.lazy
37
22
  *
@@ -45,6 +30,5 @@ declare class ModuleErrorBoundary extends Component<ErrorBoundaryProps, ErrorBou
45
30
  * <DynamicModule baseUrl="http://localhost:5001" />
46
31
  * ```
47
32
  */
48
- export declare function DynamicModule({ baseUrl, globalName, fallback, errorFallback, }: DynamicModuleProps): import("react/jsx-runtime").JSX.Element;
33
+ export declare function DynamicModule({ baseUrl, globalName, fallback, errorFallback, moduleInfo, }: DynamicModuleProps): import("react/jsx-runtime").JSX.Element;
49
34
  export { ModuleLoading, ModuleErrorBoundary };
50
- //# sourceMappingURL=DynamicModule.d.ts.map
@@ -1,48 +1,16 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Suspense, lazy, useMemo, Component } from "react";
3
- import { Icon, Card, CardContent, Button } from "@nsxbet/admin-ui";
2
+ import { Suspense, lazy, useMemo, useCallback, useState } from "react";
3
+ import { Icon } from "@nsxbet/admin-ui";
4
+ import { useTranslation } from "react-i18next";
5
+ import { validateModuleUrl } from "./url-allowlist";
6
+ import { ModuleErrorBoundary } from "./ModuleErrorBoundary";
7
+ import { trackError } from "../shell/telemetry";
4
8
  /**
5
9
  * Loading spinner shown while module is being fetched
6
10
  */
7
11
  function ModuleLoading() {
8
12
  return (_jsxs("div", { "data-testid": "module-loading", className: "flex flex-col items-center justify-center h-full min-h-[300px] gap-4", children: [_jsx("div", { className: "inline-flex p-4 rounded-full bg-primary/10", children: _jsx(Icon, { name: "loader-2", className: "h-8 w-8 text-primary animate-spin" }) }), _jsxs("div", { className: "text-center", children: [_jsx("p", { className: "text-lg font-medium", children: "Loading module..." }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Please wait while the module is being loaded" })] })] }));
9
13
  }
10
- class ModuleErrorBoundary extends Component {
11
- constructor(props) {
12
- super(props);
13
- Object.defineProperty(this, "handleRetry", {
14
- enumerable: true,
15
- configurable: true,
16
- writable: true,
17
- value: () => {
18
- this.setState({ hasError: false, error: undefined });
19
- this.props.onRetry?.();
20
- }
21
- });
22
- this.state = { hasError: false };
23
- }
24
- static getDerivedStateFromError(error) {
25
- return { hasError: true, error };
26
- }
27
- componentDidCatch(error, errorInfo) {
28
- console.error("[DynamicModule] Failed to load module:", error, errorInfo);
29
- }
30
- render() {
31
- if (this.state.hasError) {
32
- if (this.props.fallback) {
33
- return this.props.fallback;
34
- }
35
- const errorMessage = this.state.error?.message || "An unexpected error occurred";
36
- const isNetworkError = errorMessage.includes("Failed to fetch") ||
37
- errorMessage.includes("NetworkError") ||
38
- errorMessage.includes("Failed to load script");
39
- return (_jsx("div", { "data-testid": "module-error", className: "flex items-center justify-center h-full min-h-[300px] p-6", children: _jsx(Card, { className: "max-w-md w-full", children: _jsx(CardContent, { className: "pt-6", children: _jsxs("div", { className: "flex flex-col items-center text-center gap-4", children: [_jsx("div", { className: "inline-flex p-4 rounded-full bg-destructive/10", children: _jsx(Icon, { name: isNetworkError ? "wifi-off" : "alert-triangle", className: "h-8 w-8 text-destructive" }) }), _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-1", children: isNetworkError ? "Module Unavailable" : "Failed to Load Module" }), _jsx("p", { className: "text-sm text-muted-foreground mb-2", children: isNetworkError
40
- ? "The module server is not responding. Please ensure the module is running."
41
- : "An error occurred while loading the module." }), _jsx("p", { className: "text-xs font-mono text-muted-foreground bg-muted p-2 rounded break-all", children: errorMessage })] }), _jsx("div", { className: "flex gap-2", children: _jsxs(Button, { "data-testid": "module-retry-button", onClick: this.handleRetry, variant: "default", children: [_jsx(Icon, { name: "refresh-cw", className: "h-4 w-4 mr-2" }), "Try Again"] }) }), isNetworkError && (_jsx("p", { className: "text-xs text-muted-foreground", children: "Tip: Run the module dev server or check your network connection." }))] }) }) }) }));
42
- }
43
- return this.props.children;
44
- }
45
- }
46
14
  /**
47
15
  * Load a script and return the global it exposes
48
16
  */
@@ -102,28 +70,37 @@ async function fetchModuleEntry(baseUrl) {
102
70
  * <DynamicModule baseUrl="http://localhost:5001" />
103
71
  * ```
104
72
  */
105
- export function DynamicModule({ baseUrl, globalName, fallback, errorFallback, }) {
106
- // Normalize baseUrl (remove trailing slash)
73
+ export function DynamicModule({ baseUrl, globalName, fallback, errorFallback, moduleInfo, }) {
74
+ const { t, i18n } = useTranslation("shell");
107
75
  const normalizedUrl = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
108
- // Create lazy component - memoized to avoid re-creating on every render
76
+ const [retryCount, setRetryCount] = useState(0);
77
+ const handleError = useCallback((error, meta) => {
78
+ trackError(error, {
79
+ moduleId: meta.moduleId,
80
+ ownerTeam: meta.ownerTeam,
81
+ errorType: meta.errorType,
82
+ });
83
+ }, []);
84
+ const handleRetry = useCallback(() => {
85
+ setRetryCount((c) => c + 1);
86
+ }, []);
87
+ // retryCount in deps forces a fresh lazy() instance, bypassing React.lazy's cached rejection
109
88
  const ModuleComponent = useMemo(() => lazy(async () => {
110
89
  try {
111
- // Step 1: Fetch manifest to get entry path
90
+ const manifestUrl = `${normalizedUrl}/module.manifest.json`;
91
+ validateModuleUrl(manifestUrl);
112
92
  const entry = await fetchModuleEntry(normalizedUrl);
113
- // Step 2: Load the module
114
93
  const moduleUrl = `${normalizedUrl}/${entry}`;
94
+ validateModuleUrl(moduleUrl);
115
95
  console.log(`[DynamicModule] Loading module from ${moduleUrl}`);
116
96
  let moduleExport;
117
97
  if (globalName) {
118
- // IIFE format: load script and get global
119
98
  moduleExport = await loadScript(moduleUrl, globalName);
120
99
  }
121
100
  else {
122
- // ES module format: use dynamic import
123
101
  moduleExport = await import(/* @vite-ignore */ moduleUrl);
124
102
  }
125
103
  console.log(`[DynamicModule] Module loaded successfully`);
126
- // Handle both default export and direct component
127
104
  const mod = moduleExport;
128
105
  const Component = (mod.default || mod.App || mod);
129
106
  if (typeof Component !== "function") {
@@ -135,7 +112,7 @@ export function DynamicModule({ baseUrl, globalName, fallback, errorFallback, })
135
112
  console.error(`[DynamicModule] Failed to load module:`, error);
136
113
  throw error;
137
114
  }
138
- }), [normalizedUrl, globalName]);
139
- return (_jsx(ModuleErrorBoundary, { fallback: errorFallback, children: _jsx(Suspense, { fallback: fallback ?? _jsx(ModuleLoading, {}), children: _jsx(ModuleComponent, {}) }) }));
115
+ }), [normalizedUrl, globalName, retryCount]);
116
+ return (_jsx(ModuleErrorBoundary, { fallback: errorFallback, moduleInfo: moduleInfo, onError: handleError, onRetry: handleRetry, locale: i18n.language, t: (key) => t(key), children: _jsx(Suspense, { fallback: fallback ?? _jsx(ModuleLoading, {}), children: _jsx(ModuleComponent, {}) }) }));
140
117
  }
141
118
  export { ModuleLoading, ModuleErrorBoundary };
@@ -0,0 +1,39 @@
1
+ import { Component, type ReactNode } from "react";
2
+ import type { LocalizedField } from "../i18n/config.js";
3
+ export interface ModuleInfo {
4
+ id: string;
5
+ title: LocalizedField;
6
+ owners: {
7
+ team: string;
8
+ supportChannel: string;
9
+ };
10
+ }
11
+ export interface ErrorBoundaryProps {
12
+ children: ReactNode;
13
+ fallback?: ReactNode;
14
+ onRetry?: () => void;
15
+ moduleInfo?: ModuleInfo;
16
+ onError?: (error: unknown, meta: {
17
+ errorType: "render" | "unhandledRejection";
18
+ moduleId?: string;
19
+ ownerTeam?: string;
20
+ }) => void;
21
+ locale?: string;
22
+ t?: (key: string) => string;
23
+ }
24
+ interface ErrorBoundaryState {
25
+ hasError: boolean;
26
+ error?: Error;
27
+ }
28
+ declare class ModuleErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
29
+ private handleUnhandledRejection;
30
+ constructor(props: ErrorBoundaryProps);
31
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState;
32
+ componentDidMount(): void;
33
+ componentWillUnmount(): void;
34
+ componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void;
35
+ handleRetry: () => void;
36
+ handleReload: () => void;
37
+ render(): ReactNode;
38
+ }
39
+ export { ModuleErrorBoundary };
@@ -0,0 +1,101 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Component } from "react";
3
+ import { Icon, Card, CardContent, Button } from "@nsxbet/admin-ui";
4
+ function resolveTitle(title, locale) {
5
+ if (!title || typeof title !== "object")
6
+ return "";
7
+ const resolved = title[locale];
8
+ if (typeof resolved === "string")
9
+ return resolved;
10
+ return title["en-US"] ?? Object.values(title)[0] ?? "";
11
+ }
12
+ function isUrl(value) {
13
+ return value.startsWith("http://") || value.startsWith("https://");
14
+ }
15
+ function SupportChannelDisplay({ channel }) {
16
+ if (isUrl(channel)) {
17
+ return (_jsx("a", { href: channel, target: "_blank", rel: "noopener noreferrer", className: "text-primary underline underline-offset-2 hover:text-primary/80", children: channel }));
18
+ }
19
+ if (channel.startsWith("#")) {
20
+ return (_jsx("span", { className: "inline-flex items-center gap-1 rounded bg-muted px-1.5 py-0.5 font-mono text-xs", children: channel }));
21
+ }
22
+ return _jsx("span", { children: channel });
23
+ }
24
+ class ModuleErrorBoundary extends Component {
25
+ constructor(props) {
26
+ super(props);
27
+ Object.defineProperty(this, "handleUnhandledRejection", {
28
+ enumerable: true,
29
+ configurable: true,
30
+ writable: true,
31
+ value: null
32
+ });
33
+ Object.defineProperty(this, "handleRetry", {
34
+ enumerable: true,
35
+ configurable: true,
36
+ writable: true,
37
+ value: () => {
38
+ this.setState({ hasError: false, error: undefined });
39
+ this.props.onRetry?.();
40
+ }
41
+ });
42
+ Object.defineProperty(this, "handleReload", {
43
+ enumerable: true,
44
+ configurable: true,
45
+ writable: true,
46
+ value: () => {
47
+ window.location.reload();
48
+ }
49
+ });
50
+ this.state = { hasError: false };
51
+ }
52
+ static getDerivedStateFromError(error) {
53
+ return { hasError: true, error };
54
+ }
55
+ componentDidMount() {
56
+ this.handleUnhandledRejection = (event) => {
57
+ this.props.onError?.(event.reason, {
58
+ errorType: "unhandledRejection",
59
+ moduleId: this.props.moduleInfo?.id,
60
+ ownerTeam: this.props.moduleInfo?.owners.team,
61
+ });
62
+ };
63
+ window.addEventListener("unhandledrejection", this.handleUnhandledRejection);
64
+ }
65
+ componentWillUnmount() {
66
+ if (this.handleUnhandledRejection) {
67
+ window.removeEventListener("unhandledrejection", this.handleUnhandledRejection);
68
+ this.handleUnhandledRejection = null;
69
+ }
70
+ }
71
+ componentDidCatch(error, errorInfo) {
72
+ console.error("[DynamicModule] Failed to load module:", error, errorInfo);
73
+ this.props.onError?.(error, {
74
+ errorType: "render",
75
+ moduleId: this.props.moduleInfo?.id,
76
+ ownerTeam: this.props.moduleInfo?.owners.team,
77
+ });
78
+ }
79
+ render() {
80
+ if (this.state.hasError) {
81
+ if (this.props.fallback) {
82
+ return this.props.fallback;
83
+ }
84
+ const { moduleInfo, locale = "en-US", t } = this.props;
85
+ const errorMessage = this.state.error?.message || "An unexpected error occurred";
86
+ const isNetworkError = errorMessage.includes("Failed to fetch") ||
87
+ errorMessage.includes("NetworkError") ||
88
+ errorMessage.includes("Failed to load script");
89
+ const moduleTitle = moduleInfo ? resolveTitle(moduleInfo.title, locale) : undefined;
90
+ const showOwnership = moduleInfo && moduleInfo.owners.team;
91
+ const label = (key, fallback) => (t ? t(key) : fallback);
92
+ return (_jsx("div", { "data-testid": "module-error", className: "flex items-center justify-center h-full min-h-[300px] p-6", children: _jsxs("div", { className: "max-w-lg w-full space-y-6", children: [_jsxs("div", { className: "flex items-start gap-4", children: [_jsx("div", { className: "shrink-0 mt-0.5 inline-flex p-3 rounded-xl bg-destructive/10", children: _jsx(Icon, { name: isNetworkError ? "wifi-off" : "alert-triangle", className: "h-6 w-6 text-destructive" }) }), _jsxs("div", { className: "space-y-1 min-w-0", children: [_jsx("h3", { className: "text-base font-semibold leading-tight", children: isNetworkError
93
+ ? label("moduleError.networkTitle", "Module Unavailable")
94
+ : label("moduleError.title", "Failed to Load Module") }), _jsx("p", { className: "text-sm text-muted-foreground leading-relaxed", children: isNetworkError
95
+ ? label("moduleError.networkDescription", "The module server is not responding. Please ensure the module is running.")
96
+ : label("moduleError.description", "An error occurred while loading the module.") })] })] }), showOwnership && (_jsx(Card, { className: "bg-muted/40 border-border/50", children: _jsxs(CardContent, { className: "p-4 space-y-3", children: [_jsx("p", { className: "text-xs font-medium uppercase tracking-wider text-muted-foreground", children: label("moduleError.contactHeading", "Report this issue") }), _jsxs("div", { className: "space-y-2", children: [moduleTitle && (_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx(Icon, { name: "box", className: "h-3.5 w-3.5 text-muted-foreground shrink-0" }), _jsx("span", { className: "font-medium truncate", children: moduleTitle })] })), _jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx(Icon, { name: "users", className: "h-3.5 w-3.5 text-muted-foreground shrink-0" }), _jsxs("span", { className: "text-muted-foreground", children: [label("moduleError.ownerTeam", "Owner"), ":"] }), _jsx("span", { children: moduleInfo.owners.team })] }), moduleInfo.owners.supportChannel && (_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx(Icon, { name: "message-circle", className: "h-3.5 w-3.5 text-muted-foreground shrink-0" }), _jsxs("span", { className: "text-muted-foreground", children: [label("moduleError.supportChannel", "Support"), ":"] }), _jsx(SupportChannelDisplay, { channel: moduleInfo.owners.supportChannel })] }))] })] }) })), _jsx("div", { className: "rounded-lg border border-border/50 bg-muted/30 px-3 py-2", children: _jsx("p", { className: "text-xs font-mono text-muted-foreground break-all", children: errorMessage }) }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsxs(Button, { "data-testid": "module-retry-button", onClick: this.handleRetry, variant: "default", size: "sm", children: [_jsx(Icon, { name: "refresh-cw", className: "h-3.5 w-3.5 mr-1.5" }), label("moduleError.retry", "Try Again")] }), _jsxs(Button, { "data-testid": "module-reload-button", onClick: this.handleReload, variant: "ghost", size: "sm", children: [_jsx(Icon, { name: "rotate-cw", className: "h-3.5 w-3.5 mr-1.5" }), label("moduleError.reload", "Reload Page")] })] })] }) }));
97
+ }
98
+ return this.props.children;
99
+ }
100
+ }
101
+ export { ModuleErrorBoundary };
@@ -1,2 +1,2 @@
1
1
  export { DynamicModule, ModuleLoading, ModuleErrorBoundary } from "./DynamicModule";
2
- //# sourceMappingURL=index.d.ts.map
2
+ export type { ModuleInfo, ErrorBoundaryProps } from "./ModuleErrorBoundary";
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Module URL allowlist validation
3
+ *
4
+ * Validates module URLs against VITE_ALLOWED_MODULE_ORIGINS before
5
+ * import() or loadScript() to prevent loading code from untrusted origins.
6
+ */
7
+ /** Options for validateModuleUrl (used by tests to override env) */
8
+ export interface ValidateModuleUrlOptions {
9
+ /** Override allowlist (comma-separated patterns). Default: VITE_ALLOWED_MODULE_ORIGINS */
10
+ allowlist?: string;
11
+ /** Override dev mode. Default: import.meta.env.DEV */
12
+ isDev?: boolean;
13
+ }
14
+ /**
15
+ * Validate that a module URL is allowed before loading.
16
+ * Throws with a descriptive error if validation fails.
17
+ *
18
+ * @param url - Full module URL (e.g. https://cdn.example.com/spa.js)
19
+ * @param options - Optional overrides for testing (uses env vars when omitted)
20
+ * @throws Error if URL is malformed, non-HTTP, or hostname not in allowlist
21
+ */
22
+ export declare function validateModuleUrl(url: string, options?: ValidateModuleUrlOptions): void;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Module URL allowlist validation
3
+ *
4
+ * Validates module URLs against VITE_ALLOWED_MODULE_ORIGINS before
5
+ * import() or loadScript() to prevent loading code from untrusted origins.
6
+ */
7
+ /**
8
+ * Parse allowlist patterns from comma-separated string.
9
+ * Format: comma-separated patterns supporting *.domain wildcard.
10
+ */
11
+ function getAllowlistPatterns(raw) {
12
+ if (typeof raw !== 'string' || raw.trim() === '') {
13
+ return [];
14
+ }
15
+ return raw.split(',').map((p) => p.trim()).filter(Boolean);
16
+ }
17
+ /**
18
+ * Check if hostname matches a pattern (exact or *.domain wildcard).
19
+ * For *.domain: matches apex (domain) and any subdomain (*.domain).
20
+ */
21
+ function hostnameMatchesPattern(hostname, pattern) {
22
+ if (pattern.startsWith('*.')) {
23
+ const suffix = pattern.slice(1); // e.g. ".nsx.dev"
24
+ const apex = pattern.slice(2); // e.g. "nsx.dev"
25
+ return hostname === apex || hostname.endsWith(suffix);
26
+ }
27
+ return hostname === pattern;
28
+ }
29
+ /**
30
+ * Validate that a module URL is allowed before loading.
31
+ * Throws with a descriptive error if validation fails.
32
+ *
33
+ * @param url - Full module URL (e.g. https://cdn.example.com/spa.js)
34
+ * @param options - Optional overrides for testing (uses env vars when omitted)
35
+ * @throws Error if URL is malformed, non-HTTP, or hostname not in allowlist
36
+ */
37
+ export function validateModuleUrl(url, options) {
38
+ let parsed;
39
+ try {
40
+ parsed = new URL(url);
41
+ }
42
+ catch {
43
+ throw new Error(`[DynamicModule] Invalid module URL: "${url}" is not a valid URL`);
44
+ }
45
+ const protocol = parsed.protocol;
46
+ if (protocol !== 'http:' && protocol !== 'https:') {
47
+ throw new Error(`[DynamicModule] Module URL must use http: or https: protocol. Got: ${protocol}`);
48
+ }
49
+ const hostname = parsed.hostname;
50
+ const isDev = options?.isDev ?? import.meta.env.DEV === true;
51
+ const rawAllowlist = options?.allowlist ?? import.meta.env.VITE_ALLOWED_MODULE_ORIGINS;
52
+ // Dev mode: always allow localhost and 127.0.0.1
53
+ if (isDev && (hostname === 'localhost' || hostname === '127.0.0.1')) {
54
+ return;
55
+ }
56
+ const patterns = getAllowlistPatterns(rawAllowlist);
57
+ if (patterns.length === 0) {
58
+ throw new Error(`[DynamicModule] Module URL "${url}" rejected: VITE_ALLOWED_MODULE_ORIGINS is not set or empty. ` +
59
+ 'Configure trusted origins (e.g. *.nsx.dev,*.nsx.services) in your environment.');
60
+ }
61
+ const allowed = patterns.some((p) => hostnameMatchesPattern(hostname, p));
62
+ if (!allowed) {
63
+ throw new Error(`[DynamicModule] Module URL "${url}" rejected: hostname "${hostname}" is not in allowlist [${patterns.join(', ')}]`);
64
+ }
65
+ }
@@ -35,4 +35,3 @@ export interface AdminShellProps {
35
35
  environment?: string;
36
36
  }
37
37
  export declare function AdminShell({ modules: manifests, children, keycloak, authClient: providedAuthClient, registryClient, apiUrl, inMemoryRegistry, environment, }: AdminShellProps): import("react/jsx-runtime").JSX.Element;
38
- //# sourceMappingURL=AdminShell.d.ts.map