@nsxbet/admin-sdk 0.7.1 → 0.8.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 (65) hide show
  1. package/CHECKLIST.md +48 -13
  2. package/README.md +24 -74
  3. package/dist/auth/client/bff.d.ts +38 -0
  4. package/dist/auth/client/bff.js +270 -0
  5. package/dist/auth/client/in-memory.d.ts +1 -1
  6. package/dist/auth/client/in-memory.js +2 -2
  7. package/dist/auth/client/index.d.ts +1 -1
  8. package/dist/auth/client/index.js +2 -2
  9. package/dist/auth/client/interface.d.ts +4 -4
  10. package/dist/auth/client/interface.js +1 -1
  11. package/dist/auth/components/LoginPage.d.ts +8 -0
  12. package/dist/auth/components/LoginPage.js +32 -0
  13. package/dist/auth/components/UserSelector.js +2 -2
  14. package/dist/auth/components/index.d.ts +2 -0
  15. package/dist/auth/components/index.js +1 -0
  16. package/dist/auth/index.d.ts +3 -2
  17. package/dist/auth/index.js +2 -2
  18. package/dist/components/AuthProvider.d.ts +3 -3
  19. package/dist/components/AuthProvider.js +25 -10
  20. package/dist/env.d.ts +17 -0
  21. package/dist/env.js +50 -0
  22. package/dist/hooks/useAuth.d.ts +3 -3
  23. package/dist/hooks/useAuth.js +1 -1
  24. package/dist/hooks/useFetch.js +6 -1
  25. package/dist/hooks/useI18n.js +2 -2
  26. package/dist/i18n/config.d.ts +2 -1
  27. package/dist/i18n/config.js +4 -3
  28. package/dist/i18n/index.d.ts +1 -1
  29. package/dist/i18n/index.js +1 -1
  30. package/dist/i18n/locales/en-US.json +7 -0
  31. package/dist/i18n/locales/es.json +7 -0
  32. package/dist/i18n/locales/pt-BR.json +7 -0
  33. package/dist/i18n/locales/ro.json +7 -0
  34. package/dist/index.d.ts +6 -5
  35. package/dist/index.js +5 -2
  36. package/dist/registry/client/http.js +6 -1
  37. package/dist/registry/client/in-memory.js +20 -5
  38. package/dist/registry/types/manifest.d.ts +5 -0
  39. package/dist/registry/types/manifest.js +4 -1
  40. package/dist/registry/types/module.d.ts +6 -2
  41. package/dist/sdk-version.d.ts +5 -0
  42. package/dist/sdk-version.js +5 -0
  43. package/dist/shell/AdminShell.d.ts +12 -9
  44. package/dist/shell/AdminShell.js +56 -70
  45. package/dist/shell/components/ModuleOverview.js +1 -5
  46. package/dist/shell/components/RegistryPage.js +1 -1
  47. package/dist/shell/components/TopBar.js +2 -2
  48. package/dist/shell/index.d.ts +1 -1
  49. package/dist/shell/polling-config.d.ts +4 -3
  50. package/dist/shell/polling-config.js +11 -9
  51. package/dist/shell/types.d.ts +3 -1
  52. package/dist/types/platform.d.ts +2 -11
  53. package/dist/vite/config.d.ts +4 -9
  54. package/dist/vite/config.js +85 -27
  55. package/dist/vite/index.d.ts +1 -1
  56. package/dist/vite/index.js +1 -1
  57. package/dist/vite/plugins.js +6 -1
  58. package/package.json +11 -5
  59. package/scripts/write-sdk-version.mjs +21 -0
  60. package/dist/auth/client/keycloak.d.ts +0 -18
  61. package/dist/auth/client/keycloak.js +0 -129
  62. package/dist/shell/BackofficeShell.d.ts +0 -37
  63. package/dist/shell/BackofficeShell.js +0 -339
  64. package/dist/types/keycloak.d.ts +0 -25
  65. package/dist/types/keycloak.js +0 -1
@@ -5,6 +5,17 @@
5
5
  * Used for local development and testing without a backend.
6
6
  */
7
7
  import { parseManifest } from '../types';
8
+ import { SDK_PACKAGE_VERSION } from '../../sdk-version.js';
9
+ function parseManifestWithSdkDefault(value) {
10
+ const raw = value;
11
+ const merged = {
12
+ ...raw,
13
+ sdkVersion: typeof raw.sdkVersion === 'string' && raw.sdkVersion.trim() !== ''
14
+ ? raw.sdkVersion
15
+ : SDK_PACKAGE_VERSION,
16
+ };
17
+ return parseManifest(merged);
18
+ }
8
19
  const DEFAULT_STORAGE_KEY = '@nsxbet/registry';
9
20
  /**
10
21
  * Create an in-memory registry client
@@ -47,12 +58,13 @@ export function createInMemoryRegistryClient(options = {}) {
47
58
  if (options.seed && data.modules.length === 0) {
48
59
  const now = new Date().toISOString();
49
60
  for (const manifest of options.seed) {
50
- const validated = parseManifest(manifest);
61
+ const validated = parseManifestWithSdkDefault(manifest);
51
62
  data.modules.push({
52
63
  id: ++data.autoIncrement,
53
64
  moduleId: validated.id,
54
65
  baseUrl: '',
55
66
  manifest: validated,
67
+ sdkVersion: validated.sdkVersion,
56
68
  navigationOrder: validated.navigationOrder ?? data.autoIncrement * 10,
57
69
  enabled: true,
58
70
  registeredAt: now,
@@ -66,12 +78,13 @@ export function createInMemoryRegistryClient(options = {}) {
66
78
  const now = new Date().toISOString();
67
79
  for (const moduleConfig of options.initialModules) {
68
80
  const { baseUrl, ...manifestData } = moduleConfig;
69
- const validated = parseManifest(manifestData);
81
+ const validated = parseManifestWithSdkDefault(manifestData);
70
82
  data.modules.push({
71
83
  id: ++data.autoIncrement,
72
84
  moduleId: validated.id,
73
85
  baseUrl,
74
86
  manifest: validated,
87
+ sdkVersion: validated.sdkVersion,
75
88
  navigationOrder: validated.navigationOrder ?? data.autoIncrement * 10,
76
89
  enabled: true,
77
90
  registeredAt: now,
@@ -92,7 +105,7 @@ export function createInMemoryRegistryClient(options = {}) {
92
105
  result = result.filter((m) => m.manifest.category === filters.category);
93
106
  }
94
107
  if (filters?.status) {
95
- result = result.filter(() => filters.status === 'active');
108
+ result = result.filter((m) => filters.status === 'active' ? m.enabled : !m.enabled);
96
109
  }
97
110
  // Sort by navigationOrder
98
111
  result.sort((a, b) => a.navigationOrder - b.navigationOrder);
@@ -119,13 +132,13 @@ export function createInMemoryRegistryClient(options = {}) {
119
132
  const manifestData = await response.json();
120
133
  // Extract manifest without entry (entry is fetched by DynamicModule at runtime)
121
134
  const { entry: _entry, ...manifest } = manifestData;
122
- const validated = parseManifest(manifest);
135
+ const validated = parseManifestWithSdkDefault(manifest);
123
136
  return this.registerFromManifest(validated, baseUrl);
124
137
  },
125
138
  async registerFromManifest(manifest, baseUrl = '') {
126
139
  data = loadStorage(storageKey);
127
140
  // Validate manifest
128
- const validated = parseManifest(manifest);
141
+ const validated = parseManifestWithSdkDefault(manifest);
129
142
  // Check for duplicate moduleId
130
143
  if (data.modules.some((m) => m.moduleId === validated.id)) {
131
144
  throw new Error(`Module with id ${validated.id} already exists`);
@@ -136,6 +149,7 @@ export function createInMemoryRegistryClient(options = {}) {
136
149
  moduleId: validated.id,
137
150
  baseUrl,
138
151
  manifest: validated,
152
+ sdkVersion: validated.sdkVersion,
139
153
  navigationOrder: validated.navigationOrder ?? data.autoIncrement * 10,
140
154
  enabled: true,
141
155
  registeredAt: now,
@@ -265,6 +279,7 @@ export function createInMemoryRegistryClient(options = {}) {
265
279
  supportChannel: m.manifest.owners?.supportChannel ?? '#general',
266
280
  },
267
281
  status: 'active',
282
+ sdkVersion: m.manifest.sdkVersion ?? SDK_PACKAGE_VERSION,
268
283
  navigationOrder: m.navigationOrder,
269
284
  icon: m.manifest.icon,
270
285
  navigation: m.manifest.navigation ? {
@@ -82,6 +82,11 @@ export interface AdminModuleManifest {
82
82
  permissions?: ModulePermissions;
83
83
  /** Module ownership info */
84
84
  owners?: ModuleOwners;
85
+ /**
86
+ * Semver of @nsxbet/admin-sdk the module was built against.
87
+ * Injected at build time by `defineModuleConfig` — omit in source `admin.module.json`.
88
+ */
89
+ sdkVersion?: string;
85
90
  }
86
91
  /**
87
92
  * Validates that an object is a valid AdminModuleManifest
@@ -36,6 +36,9 @@ export function validateManifest(value) {
36
36
  if (!isLocalizedField(obj.description)) {
37
37
  return false;
38
38
  }
39
+ if (typeof obj.sdkVersion !== "string" || obj.sdkVersion.trim() === "") {
40
+ return false;
41
+ }
39
42
  // Optional field validations
40
43
  if (obj.category !== undefined && typeof obj.category !== 'string') {
41
44
  return false;
@@ -74,7 +77,7 @@ export function validateManifest(value) {
74
77
  */
75
78
  export function parseManifest(value) {
76
79
  if (!validateManifest(value)) {
77
- throw new Error('Invalid manifest: must have id, title (localized), description (localized), and routeBase (starting with /). Title and description must be objects with all 4 locales: en-US, pt-BR, es, ro.');
80
+ throw new Error("Invalid manifest: must include id, title (all locales), description (all locales), routeBase (starting with /), and sdkVersion (non-empty string, injected at build time by defineModuleConfig). Title and description must include en-US, pt-BR, es, ro.");
78
81
  }
79
82
  return value;
80
83
  }
@@ -18,6 +18,8 @@ export interface RegisteredModule {
18
18
  version?: string;
19
19
  /** Full parsed module.manifest.json (without entry - fetched at runtime) */
20
20
  manifest: AdminModuleManifest;
21
+ /** Semver of @nsxbet/admin-sdk (DB column; mirrors manifest.sdkVersion) */
22
+ sdkVersion?: string;
21
23
  /** Admin-controlled navigation order */
22
24
  navigationOrder: number;
23
25
  /** Whether the module is enabled */
@@ -59,7 +61,7 @@ export interface ModuleFilters {
59
61
  /** Filter by category */
60
62
  category?: string;
61
63
  /** Filter by status */
62
- status?: 'active' | 'deprecated' | 'disabled';
64
+ status?: 'active' | 'disabled';
63
65
  }
64
66
  /**
65
67
  * Catalog structure for shell consumption (denormalized)
@@ -96,7 +98,9 @@ export interface CatalogModule {
96
98
  team: string;
97
99
  supportChannel: string;
98
100
  };
99
- status: 'active' | 'deprecated' | 'disabled';
101
+ status: 'active' | 'disabled';
102
+ /** Semver of @nsxbet/admin-sdk the module was built against */
103
+ sdkVersion: string;
100
104
  navigationOrder?: number;
101
105
  icon?: string;
102
106
  navigation?: {
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Semver of @nsxbet/admin-sdk (synced from package.json by scripts/write-sdk-version.mjs).
3
+ * Do not edit manually — run `node scripts/write-sdk-version.mjs` after version bumps.
4
+ */
5
+ export declare const SDK_PACKAGE_VERSION: string;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Semver of @nsxbet/admin-sdk (synced from package.json by scripts/write-sdk-version.mjs).
3
+ * Do not edit manually — run `node scripts/write-sdk-version.mjs` after version bumps.
4
+ */
5
+ export const SDK_PACKAGE_VERSION = "0.8.0";
@@ -1,5 +1,9 @@
1
1
  import type { AuthClient } from "../auth/client/interface";
2
2
  import type { AdminModuleManifest, RegistryClient } from "../registry";
3
+ /** Use admin-bff cookie / Okta auth (`createBffAuthClient`). `true` uses current origin. */
4
+ export type AdminShellBffConfig = true | {
5
+ baseUrl?: string;
6
+ };
3
7
  export interface AdminShellProps {
4
8
  /**
5
9
  * Module manifests to load (used for standalone mode when no registryClient)
@@ -12,17 +16,12 @@ export interface AdminShellProps {
12
16
  */
13
17
  children?: React.ReactNode;
14
18
  /**
15
- * Keycloak configuration
16
- * If not provided, uses in-memory (mock) authentication
19
+ * BFF / Okta cookie authentication (recommended for production).
17
20
  */
18
- keycloak?: {
19
- url: string;
20
- realm: string;
21
- clientId: string;
22
- };
21
+ bff?: AdminShellBffConfig;
23
22
  /**
24
23
  * Auth client to use for authentication.
25
- * If not provided, creates one based on keycloak prop or environment.
24
+ * If not provided, creates one from `bff` or in-memory (mock) authentication depending on environment.
26
25
  */
27
26
  authClient?: AuthClient;
28
27
  /** Registry client for fetching modules from API */
@@ -33,5 +32,9 @@ export interface AdminShellProps {
33
32
  inMemoryRegistry?: boolean;
34
33
  /** Environment (default: "local") */
35
34
  environment?: string;
35
+ /** BFF base URL (passed from shell's env config). Only used when `bff` is `true`. */
36
+ bffBaseUrl?: string;
37
+ /** Registry polling interval in ms (passed from shell's env config). */
38
+ registryPollInterval?: number;
36
39
  }
37
- export declare function AdminShell({ modules: manifests, children, keycloak, authClient: providedAuthClient, registryClient, apiUrl, inMemoryRegistry, environment, }: AdminShellProps): import("react/jsx-runtime").JSX.Element;
40
+ export declare function AdminShell({ modules: manifests, children, bff, authClient: providedAuthClient, registryClient, apiUrl, inMemoryRegistry, environment, bffBaseUrl: bffBaseUrlProp, registryPollInterval: registryPollIntervalProp, }: AdminShellProps): import("react/jsx-runtime").JSX.Element;
@@ -15,7 +15,7 @@ import { RegistryPage } from "./components/RegistryPage";
15
15
  import { HomePage } from "./components/HomePage";
16
16
  import { AuthProvider, useAuthContext } from "../components/AuthProvider";
17
17
  import { createInMemoryAuthClient, createMockUsersFromRoles, } from "../auth/client/in-memory";
18
- import { createKeycloakAuthClient } from "../auth/client/keycloak";
18
+ import { createBffAuthClient } from "../auth/client/bff";
19
19
  import { createInMemoryRegistryClient } from "../registry/client/in-memory";
20
20
  import { createCachedCatalog } from "../registry/cache/cached-catalog";
21
21
  import { DevtoolsPanel } from "./components/DevtoolsPanel";
@@ -25,8 +25,9 @@ import { useRegistryPolling } from "../registry/useRegistryPolling";
25
25
  import { DynamicModule } from "../router/DynamicModule";
26
26
  import { SidebarProvider, SidebarInset } from "@nsxbet/admin-ui";
27
27
  import { initTelemetry, track, trackError } from "./telemetry";
28
- import { initI18n, saveLocale, isSupportedLocale, i18n } from "../i18n";
28
+ import { initI18n, saveLocale, isSupportedLocale, i18n, DEFAULT_LOCALE } from "../i18n";
29
29
  import { resolvePollingInterval } from "./polling-config";
30
+ import { SDK_PACKAGE_VERSION } from "../sdk-version.js";
30
31
  const TIMEZONE_STORAGE_KEY = "admin-timezone-mode";
31
32
  function getStoredTimezoneMode() {
32
33
  if (typeof window === "undefined")
@@ -62,6 +63,7 @@ function manifestToModule(manifest, baseUrl) {
62
63
  supportChannel: manifest.owners?.supportChannel || "",
63
64
  },
64
65
  status: "active",
66
+ sdkVersion: manifest.sdkVersion ?? SDK_PACKAGE_VERSION,
65
67
  navigationOrder: manifest.navigationOrder,
66
68
  icon: manifest.icon,
67
69
  navigation: manifest.navigation ? {
@@ -96,6 +98,7 @@ function catalogModuleToModule(m) {
96
98
  permissions: m.permissions,
97
99
  owners: m.owners,
98
100
  status: m.status,
101
+ sdkVersion: m.sdkVersion,
99
102
  navigationOrder: m.navigationOrder,
100
103
  icon: m.icon,
101
104
  navigation: m.navigation ? {
@@ -176,6 +179,9 @@ function ShellContent({ modules, children, environment, locale, onLocaleChange,
176
179
  fetch: async (input, init) => {
177
180
  const token = await auth.getAccessToken();
178
181
  const headers = new Headers(init?.headers);
182
+ if (token === null) {
183
+ return fetch(input, { ...init, headers, credentials: "include" });
184
+ }
179
185
  headers.set("Authorization", `Bearer ${token}`);
180
186
  return fetch(input, { ...init, headers });
181
187
  },
@@ -220,12 +226,12 @@ function ShellContent({ modules, children, environment, locale, onLocaleChange,
220
226
  owners: module.owners,
221
227
  } })) : (_jsxs("div", { className: "text-muted-foreground", children: ["Module ", module.id, " has no baseUrl configured"] })) }) }, module.id))), _jsx(Route, { path: "*", element: _jsx(MainContent, { modules: modules, moduleBreadcrumbs: moduleBreadcrumbs, children: isStandaloneMode ? children : null }) })] })] })] }), _jsx(CommandPalette, { open: commandPaletteOpen, onOpenChange: onCommandPaletteChange, catalog: catalog }), _jsx(DevtoolsPanel, { environment: environment, modules: modules, catalogVersion: catalog.version, catalogGeneratedAt: catalog.generatedAt, registryMode: registryClient ? "api" : "in-memory", cacheStatus: cacheStatus })] }));
222
228
  }
223
- export function AdminShell({ modules: manifests = [], children, keycloak, authClient: providedAuthClient, registryClient, apiUrl, inMemoryRegistry = true, environment = "local", }) {
229
+ export function AdminShell({ modules: manifests = [], children, bff, authClient: providedAuthClient, registryClient, apiUrl, inMemoryRegistry = true, environment = "local", bffBaseUrl: bffBaseUrlProp, registryPollInterval: registryPollIntervalProp, }) {
224
230
  const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
225
231
  const [locale, setLocale] = useState(() => {
226
232
  // Initialize i18n and get the current language
227
233
  initI18n();
228
- return i18n.language || "pt-BR";
234
+ return i18n.language || DEFAULT_LOCALE;
229
235
  });
230
236
  const [apiModules, setApiModules] = useState([]);
231
237
  const [initialCatalogVersion, setInitialCatalogVersion] = useState("");
@@ -270,70 +276,47 @@ export function AdminShell({ modules: manifests = [], children, keycloak, authCl
270
276
  if (providedAuthClient) {
271
277
  return providedAuthClient;
272
278
  }
273
- const isProd = import.meta.env.PROD === true;
274
- const mockAuthExplicit = import.meta.env.VITE_MOCK_AUTH === 'true';
275
- const noAuthConfig = !keycloak;
276
- // Production guard: require auth config unless explicitly opted in
277
- if (typeof window !== 'undefined' &&
278
- isProd &&
279
- noAuthConfig &&
280
- !mockAuthExplicit) {
281
- throw new Error('[AdminShell] Authentication configuration is required in production. ' +
282
- 'Provide authClient or keycloak prop, or set VITE_MOCK_AUTH=true to explicitly opt in to mock auth.');
283
- }
284
- // Check if we should use mock auth
285
- const useMockAuth = typeof window !== 'undefined' &&
286
- (mockAuthExplicit || noAuthConfig);
287
- if (useMockAuth) {
288
- if (isProd && mockAuthExplicit) {
289
- console.warn('[AdminShell] Mock auth is active in production build (VITE_MOCK_AUTH=true). ' +
290
- 'Use real authentication (Keycloak) for production deployments.');
291
- }
292
- // Default mock users with explicit roles for tasks and users modules
293
- const defaultMockUsers = createMockUsersFromRoles({
294
- admin: [
295
- 'admin.tasks.view',
296
- 'admin.tasks.edit',
297
- 'admin.tasks.delete',
298
- 'admin.users.view',
299
- 'admin.users.edit',
300
- 'admin.users.delete',
301
- 'admin.platform.view',
302
- 'admin.platform.edit',
303
- 'admin.platform.delete',
304
- ],
305
- editor: [
306
- 'admin.tasks.view',
307
- 'admin.tasks.edit',
308
- 'admin.users.view',
309
- 'admin.users.edit',
310
- 'admin.platform.view',
311
- 'admin.platform.edit',
312
- ],
313
- viewer: ['admin.tasks.view', 'admin.users.view', 'admin.platform.view'],
314
- noAccess: [],
315
- });
316
- return createInMemoryAuthClient({ users: defaultMockUsers, gatewayUrl: import.meta.env.VITE_ADMIN_GATEWAY_URL });
279
+ if (bff) {
280
+ const baseUrl = bff === true
281
+ ? bffBaseUrlProp
282
+ ? bffBaseUrlProp.replace(/\/$/, '')
283
+ : window.location.origin
284
+ : bff.baseUrl
285
+ ? bff.baseUrl.replace(/\/$/, '')
286
+ : bffBaseUrlProp
287
+ ? bffBaseUrlProp.replace(/\/$/, '')
288
+ : window.location.origin;
289
+ return createBffAuthClient({ bffBaseUrl: baseUrl });
317
290
  }
318
- // Use Keycloak
319
- return createKeycloakAuthClient({
320
- config: keycloak,
291
+ const defaultMockUsers = createMockUsersFromRoles({
292
+ admin: [
293
+ 'admin.tasks.view',
294
+ 'admin.tasks.edit',
295
+ 'admin.tasks.delete',
296
+ 'admin.users.view',
297
+ 'admin.users.edit',
298
+ 'admin.users.delete',
299
+ 'admin.platform.view',
300
+ 'admin.platform.edit',
301
+ 'admin.platform.delete',
302
+ ],
303
+ editor: [
304
+ 'admin.tasks.view',
305
+ 'admin.tasks.edit',
306
+ 'admin.users.view',
307
+ 'admin.users.edit',
308
+ 'admin.platform.view',
309
+ 'admin.platform.edit',
310
+ ],
311
+ viewer: ['admin.tasks.view', 'admin.users.view', 'admin.platform.view'],
312
+ noAccess: [],
321
313
  });
322
- }, [providedAuthClient, keycloak]);
314
+ return createInMemoryAuthClient({ users: defaultMockUsers });
315
+ }, [providedAuthClient, bff, bffBaseUrlProp]);
323
316
  // Initialize telemetry
324
317
  useEffect(() => {
325
318
  initTelemetry(environment);
326
319
  }, [environment]);
327
- // Initialize Keycloak configuration (for legacy support)
328
- useEffect(() => {
329
- if (keycloak) {
330
- window.__KEYCLOAK_CONFIG__ = {
331
- url: keycloak.url,
332
- realm: keycloak.realm,
333
- clientId: keycloak.clientId,
334
- };
335
- }
336
- }, [keycloak]);
337
320
  // Initialize in-memory registry if enabled and no registryClient
338
321
  useEffect(() => {
339
322
  if (!registryClient && inMemoryRegistry && manifests.length > 0) {
@@ -385,8 +368,8 @@ export function AdminShell({ modules: manifests = [], children, keycloak, authCl
385
368
  const pollingInterval = useMemo(() => {
386
369
  if (!registryClient || !initialCatalogVersion)
387
370
  return 0;
388
- return resolvePollingInterval(environment);
389
- }, [registryClient, initialCatalogVersion, environment]);
371
+ return resolvePollingInterval(environment, registryPollIntervalProp);
372
+ }, [registryClient, initialCatalogVersion, environment, registryPollIntervalProp]);
390
373
  // Poll for catalog version changes
391
374
  const { hasUpdates, dismiss: dismissUpdate } = useRegistryPolling({
392
375
  registryClient: registryClient,
@@ -428,13 +411,16 @@ export function AdminShell({ modules: manifests = [], children, keycloak, authCl
428
411
  setCacheStatus(cachedCatalogOps.getStatus());
429
412
  }
430
413
  }, [cachedCatalogOps]);
431
- // Show loading state while fetching from API
414
+ // Resolve shell content loading, unavailable, or ready
415
+ let shellContent;
432
416
  if (isLoading) {
433
- return (_jsx("div", { className: "flex h-screen items-center justify-center", children: _jsx("div", { className: "text-muted-foreground", children: "Loading modules..." }) }));
417
+ shellContent = (_jsx("div", { className: "flex h-screen items-center justify-center", children: _jsx("div", { className: "text-muted-foreground", children: "Loading modules..." }) }));
418
+ }
419
+ else if (cacheStatus.state === "unavailable") {
420
+ shellContent = _jsx(RegistryUnavailable, { onRetry: handleRetry });
434
421
  }
435
- // Registry completely unavailable (no cache, no API)
436
- if (cacheStatus.state === "unavailable") {
437
- return (_jsx(BrowserRouter, { children: _jsx(I18nextProvider, { i18n: i18n, children: _jsx(ThemeProvider, { children: _jsx(RegistryUnavailable, { onRetry: handleRetry }) }) }) }));
422
+ else {
423
+ shellContent = (_jsx(ShellContent, { modules: modules, environment: environment, locale: locale, onLocaleChange: handleLocaleChange, onSearchClick: handleSearchClick, catalog: catalog, commandPaletteOpen: commandPaletteOpen, onCommandPaletteChange: setCommandPaletteOpen, apiUrl: apiUrl, registryClient: registryClient, isStandaloneMode: isStandaloneMode, cacheStatus: cacheStatus, onRetry: handleRetry, hasUpdates: hasUpdates, onDismissUpdate: dismissUpdate, localeCallbacksRef: localeCallbacksRef, timezoneMode: timezoneMode, onTimezoneToggle: handleTimezoneToggle, timezoneCallbacksRef: timezoneCallbacksRef, children: children }));
438
424
  }
439
- return (_jsx(BrowserRouter, { children: _jsx(I18nextProvider, { i18n: i18n, children: _jsx(ThemeProvider, { children: _jsx(AuthProvider, { authClient: authClient, children: _jsx(ShellContent, { modules: modules, environment: environment, locale: locale, onLocaleChange: handleLocaleChange, onSearchClick: handleSearchClick, catalog: catalog, commandPaletteOpen: commandPaletteOpen, onCommandPaletteChange: setCommandPaletteOpen, apiUrl: apiUrl, registryClient: registryClient, isStandaloneMode: isStandaloneMode, cacheStatus: cacheStatus, onRetry: handleRetry, hasUpdates: hasUpdates, onDismissUpdate: dismissUpdate, localeCallbacksRef: localeCallbacksRef, timezoneMode: timezoneMode, onTimezoneToggle: handleTimezoneToggle, timezoneCallbacksRef: timezoneCallbacksRef, children: children }) }) }) }) }));
425
+ return (_jsx(BrowserRouter, { children: _jsx(I18nextProvider, { i18n: i18n, children: _jsx(ThemeProvider, { children: _jsx(AuthProvider, { authClient: authClient, children: shellContent }) }) }) }));
440
426
  }
@@ -13,11 +13,7 @@ export function ModuleOverview({ modules }) {
13
13
  if (!module) {
14
14
  return (_jsx("div", { className: "p-6 max-w-3xl mx-auto space-y-6", children: _jsx(Card, { className: "border-destructive", children: _jsx(CardContent, { className: "pt-6", children: _jsxs("div", { className: "flex items-center gap-3 text-destructive", children: [_jsx(Icon, { name: "alert-circle", className: "h-6 w-6" }), _jsxs("div", { children: [_jsx("h1", { className: "text-xl font-bold", children: t('errors.moduleNotFound') }), _jsx("p", { className: "text-muted-foreground mt-1", children: t('errors.moduleNotFoundDescription', { moduleId }) })] })] }) }) }) }));
15
15
  }
16
- const statusVariant = module.status === "active"
17
- ? "success"
18
- : module.status === "deprecated"
19
- ? "warning"
20
- : "secondary";
16
+ const statusVariant = module.status === "active" ? "success" : "secondary";
21
17
  const moduleTitle = resolveLocalizedString(module.title, i18n.language);
22
18
  const moduleDescription = resolveLocalizedString(module.description, i18n.language);
23
19
  return (_jsxs("div", { className: "p-6 max-w-3xl mx-auto space-y-6", children: [_jsxs("div", { className: "flex items-start gap-4", children: [_jsx("div", { className: "flex-shrink-0 p-3 rounded-lg bg-muted", children: _jsx(Icon, { name: module.icon || "package", className: "h-8 w-8" }) }), _jsxs("div", { className: "flex-1", children: [_jsxs("div", { className: "flex items-center gap-3 mb-1", children: [_jsx("h1", { className: "text-3xl font-bold", children: moduleTitle }), _jsx(Badge, { variant: statusVariant, className: "capitalize", children: module.status })] }), moduleDescription && (_jsx("p", { className: "text-muted-foreground", children: moduleDescription }))] })] }), module.commands && module.commands.length > 0 && (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "pb-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Icon, { name: "zap", className: "h-5 w-5 text-muted-foreground" }), _jsx(CardTitle, { className: "text-lg", children: t('moduleOverview.actions') })] }), _jsx(CardDescription, { children: t('moduleOverview.actionsDescription') })] }), _jsx(CardContent, { children: _jsx("div", { className: "grid gap-3 sm:grid-cols-2 lg:grid-cols-3", children: module.commands.map((command) => (_jsxs("button", { onClick: () => navigate(command.route), className: "flex items-center gap-3 rounded-lg border border-border bg-card p-4 text-left transition-colors hover:bg-accent hover:border-accent", children: [_jsx("div", { className: "flex-shrink-0 p-2 rounded-md bg-muted", children: _jsx(Icon, { name: command.icon || "file-text", className: "h-5 w-5" }) }), _jsx("span", { className: "font-medium", children: resolveLocalizedString(command.title, i18n.language) })] }, command.id))) }) })] })), _jsxs(Card, { children: [_jsx(CardHeader, { className: "pb-3", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Icon, { name: "info", className: "h-5 w-5 text-muted-foreground" }), _jsx(CardTitle, { className: "text-lg", children: t('moduleOverview.information') })] }) }), _jsx(CardContent, { children: _jsxs("dl", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between py-2 border-b border-border", children: [_jsxs("dt", { className: "text-sm font-medium text-muted-foreground flex items-center gap-2", children: [_jsx(Icon, { name: "folder", className: "h-4 w-4" }), t('moduleOverview.category')] }), _jsx("dd", { className: "text-sm font-medium", children: module.category })] }), _jsxs("div", { className: "flex items-center justify-between py-2 border-b border-border", children: [_jsxs("dt", { className: "text-sm font-medium text-muted-foreground flex items-center gap-2", children: [_jsx(Icon, { name: "activity", className: "h-4 w-4" }), t('moduleOverview.status')] }), _jsx("dd", { children: _jsx(Badge, { variant: statusVariant, className: "capitalize", children: module.status }) })] }), module.owners?.team && (_jsxs("div", { className: "flex items-center justify-between py-2 border-b border-border", children: [_jsxs("dt", { className: "text-sm font-medium text-muted-foreground flex items-center gap-2", children: [_jsx(Icon, { name: "users", className: "h-4 w-4" }), t('moduleOverview.owner')] }), _jsx("dd", { className: "text-sm font-medium", children: module.owners.team })] })), module.owners?.supportChannel && (_jsxs("div", { className: "flex items-center justify-between py-2", children: [_jsxs("dt", { className: "text-sm font-medium text-muted-foreground flex items-center gap-2", children: [_jsx(Icon, { name: "message-circle", className: "h-4 w-4" }), t('moduleOverview.support')] }), _jsx("dd", { className: "text-sm font-medium", children: module.owners.supportChannel })] }))] }) })] })] }));
@@ -201,7 +201,7 @@ export function RegistryPage({ apiUrl, registryClient }) {
201
201
  }
202
202
  return (_jsxs("div", { className: "p-6 max-w-4xl mx-auto space-y-6", "data-testid": "registry-page", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-3xl font-bold mb-1", "data-testid": "registry-heading", children: t("registryPage.title") }), _jsx("p", { className: "text-muted-foreground", children: t("registryPage.description") })] }), _jsxs("div", { className: "flex items-center gap-2", children: [canEdit && !isReorderMode && (_jsxs(Button, { variant: "outline", size: "sm", onClick: enterReorderMode, children: [_jsx(Icon, { name: "arrow-up-down", className: "h-4 w-4 mr-2" }), t("registryPage.manageOrder")] })), _jsx(Badge, { variant: apiUrl ? "default" : "secondary", children: apiUrl ? t("registryPage.apiMode") : t("registryPage.inMemoryMode") })] })] }), error && (_jsx(Card, { className: "border-destructive bg-destructive/10", children: _jsx(CardContent, { className: "pt-6", children: _jsxs("div", { className: "flex items-center gap-3 text-destructive", children: [_jsx(Icon, { name: "alert-circle", className: "h-5 w-5" }), _jsx("p", { children: error })] }) }) })), isReorderMode && (_jsx(Card, { children: _jsxs(CardContent, { className: "pt-6", children: [_jsxs("div", { className: "flex items-center justify-between mb-4", children: [_jsx("h2", { className: "text-lg font-semibold", children: t("registryPage.manageOrder") }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { variant: "outline", size: "sm", onClick: cancelReorder, disabled: isSavingOrder, children: t("common.cancel") }), _jsx(Button, { size: "sm", onClick: saveOrder, disabled: isSavingOrder || !hasOrderChanged, children: isSavingOrder ? (_jsxs(_Fragment, { children: [_jsx(Icon, { name: "loader-2", className: "h-4 w-4 mr-2 animate-spin" }), t("registryPage.savingOrder")] })) : (t("registryPage.saveOrder")) })] })] }), _jsx("div", { className: "space-y-1", children: reorderList.map((module, index) => (_jsxs("div", { className: "flex items-center gap-3 p-2 rounded-lg border bg-background", children: [_jsx(Badge, { variant: "outline", className: "font-mono text-xs shrink-0", children: t("registryPage.position", { position: index + 1 }) }), _jsx("div", { className: "flex-shrink-0 p-1.5 rounded bg-muted", children: _jsx(Icon, { name: module.manifest.icon || "package", className: "h-4 w-4" }) }), _jsx("span", { className: "flex-1 font-medium truncate", children: resolveLocalizedString(module.manifest.title, i18n.language) }), _jsxs("div", { className: "flex gap-1 shrink-0", children: [_jsx(Button, { variant: "ghost", size: "icon", className: "h-7 w-7", onClick: () => moveUp(index), disabled: index === 0, children: _jsx(Icon, { name: "chevron-up", className: "h-4 w-4" }) }), _jsx(Button, { variant: "ghost", size: "icon", className: "h-7 w-7", onClick: () => moveDown(index), disabled: index === reorderList.length - 1, children: _jsx(Icon, { name: "chevron-down", className: "h-4 w-4" }) })] })] }, module.id))) })] }) })), !isReorderMode && (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Icon, { name: "package", className: "h-5 w-5 text-muted-foreground" }), _jsxs("h2", { className: "text-lg font-semibold", children: [t("registryPage.registeredModules"), " ", !isLoading && (_jsxs("span", { className: "text-muted-foreground font-normal", children: ["(", modules.length, ")"] }))] })] }), isLoading ? (_jsx(Card, { children: _jsxs(CardContent, { className: "py-8 text-center", children: [_jsx(Icon, { name: "loader-2", className: "h-8 w-8 mx-auto mb-3 animate-spin text-muted-foreground" }), _jsx("p", { className: "text-muted-foreground", children: t("registryPage.loadingModules") })] }) })) : modules.length === 0 ? (_jsx(Card, { children: _jsxs(CardContent, { className: "py-8 text-center", children: [_jsx(Icon, { name: "package", className: "h-8 w-8 mx-auto mb-3 text-muted-foreground" }), _jsx("p", { className: "text-muted-foreground", children: t("registryPage.noModules") }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: t("registryPage.noModulesHint") })] }) })) : (_jsx("div", { className: "space-y-3", children: modules.map((module, index) => (_jsx(Card, { className: !module.enabled ? "opacity-60 border-dashed" : undefined, "data-testid": `module-card-${module.moduleId}`, children: _jsx(CardContent, { className: "pt-6", children: _jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { className: "flex items-start gap-4 flex-1 min-w-0", children: [_jsx("div", { className: "flex-shrink-0 p-2 rounded-lg bg-muted", children: _jsx(Icon, { name: module.manifest.icon || "package", className: "h-6 w-6" }) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsxs("div", { className: "flex items-center gap-2 mb-1 flex-wrap", children: [_jsx("h3", { className: "font-semibold truncate", children: resolveLocalizedString(module.manifest.title, i18n.language) }), canEdit && (_jsx(Badge, { variant: module.enabled ? "success" : "secondary", children: module.enabled
203
203
  ? t("registryPage.enabled")
204
- : t("registryPage.disabled") })), module.version ? (_jsxs(Badge, { variant: "outline", "data-testid": `version-badge-${module.moduleId}`, children: ["v", module.version] })) : (_jsx(Badge, { variant: "outline", className: "text-muted-foreground", children: t("registryPage.unversioned") }))] }), _jsx("p", { className: "text-sm text-muted-foreground mb-3", children: resolveLocalizedString(module.manifest.description, i18n.language) ||
204
+ : t("registryPage.disabled") })), module.version ? (_jsxs(Badge, { variant: "outline", "data-testid": `version-badge-${module.moduleId}`, children: ["v", module.version] })) : (_jsx(Badge, { variant: "outline", className: "text-muted-foreground", children: t("registryPage.unversioned") })), (module.sdkVersion ?? module.manifest.sdkVersion) && (_jsxs(Badge, { variant: "outline", className: "font-mono text-xs", children: ["SDK ", module.sdkVersion ?? module.manifest.sdkVersion] }))] }), _jsx("p", { className: "text-sm text-muted-foreground mb-3", children: resolveLocalizedString(module.manifest.description, i18n.language) ||
205
205
  t("registryPage.noDescription") }), _jsxs("div", { className: "flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground", children: [_jsxs("span", { className: "flex items-center gap-1", children: [_jsx(Icon, { name: "hash", className: "h-3 w-3" }), module.moduleId] }), _jsxs("span", { className: "flex items-center gap-1", children: [_jsx(Icon, { name: "folder", className: "h-3 w-3" }), module.manifest.category || "General"] }), _jsxs("span", { className: "flex items-center gap-1", children: [_jsx(Icon, { name: "navigation", className: "h-3 w-3" }), module.manifest.routeBase] }), module.baseUrl && (_jsxs("span", { className: "flex items-center gap-1", title: module.baseUrl, children: [_jsx(Icon, { name: "link", className: "h-3 w-3" }), _jsx("span", { className: "truncate max-w-[200px]", children: module.baseUrl })] }))] })] })] }), _jsxs("div", { className: "flex items-center gap-2 flex-shrink-0", children: [_jsx(Badge, { variant: "outline", className: "font-mono text-xs", children: t("registryPage.position", { position: index + 1 }) }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => openVersionHistory(module), title: t("registryPage.versionHistory"), "data-testid": `version-history-btn-${module.moduleId}`, children: _jsx(Icon, { name: "history", className: "h-4 w-4" }) }), canEdit && (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Label, { className: "text-sm text-muted-foreground", children: t("registryPage.enabled") }), _jsx(Switch, { checked: module.enabled, onCheckedChange: () => handleToggleEnabled(module), "data-testid": `enable-switch-${module.moduleId}` })] })), canDelete && (_jsx(Button, { variant: "destructive", size: "sm", onClick: () => handleDeleteModule(module), children: _jsx(Icon, { name: "trash-2", className: "h-4 w-4" }) }))] })] }) }) }, module.id))) }))] })), _jsx(Dialog, { open: !!disableTarget, onOpenChange: (open) => !open && setDisableTarget(null), children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: t("registryPage.disableTitle", {
206
206
  name: disableTarget
207
207
  ? resolveLocalizedString(disableTarget.manifest.title, i18n.language)
@@ -3,7 +3,7 @@ import { useState, useEffect } from "react";
3
3
  import { Search, Moon, Sun, Check, Clock, Globe } from "lucide-react";
4
4
  import { useTheme } from "./theme-provider";
5
5
  import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Badge, Button, } from "@nsxbet/admin-ui";
6
- import { SUPPORTED_LOCALES, LOCALE_FLAGS, LOCALE_NAMES } from "../../i18n";
6
+ import { SUPPORTED_LOCALES, DEFAULT_LOCALE, LOCALE_FLAGS, LOCALE_NAMES } from "../../i18n";
7
7
  import { useI18n } from "../../hooks/useI18n";
8
8
  const ENV_COLORS = {
9
9
  local: "bg-gray-500",
@@ -30,7 +30,7 @@ const ENV_HEADER_COLORS_DARK = {
30
30
  export function TopBar({ onSearchClick, environment = "local", locale: externalLocale, onLocaleChange, timezoneMode = "local", onTimezoneToggle, }) {
31
31
  const { theme, setTheme } = useTheme();
32
32
  const { t } = useI18n();
33
- const [locale, setLocale] = useState(externalLocale || "pt-BR");
33
+ const [locale, setLocale] = useState(externalLocale || DEFAULT_LOCALE);
34
34
  useEffect(() => {
35
35
  if (externalLocale && SUPPORTED_LOCALES.includes(externalLocale)) {
36
36
  setLocale(externalLocale);
@@ -1,5 +1,5 @@
1
1
  export { AdminShell } from "./AdminShell";
2
- export type { AdminShellProps } from "./AdminShell";
2
+ export type { AdminShellProps, AdminShellBffConfig } from "./AdminShell";
3
3
  export { TopBar, LeftNav, MainContent, CommandPalette, ThemeProvider, useTheme, } from "./components";
4
4
  export type { TopBarProps, CommandPaletteProps } from "./components";
5
5
  export type { Module, ModuleCommand, ModuleStatus, NavigationSection, ModuleNavigation, Catalog, ImportMap, SearchableItem, SearchResult, } from "./types";
@@ -2,9 +2,10 @@
2
2
  * Resolve the registry polling interval in milliseconds.
3
3
  *
4
4
  * Priority:
5
- * 1. Explicit `REGISTRY_POLL_INTERVAL` from `window.__ENV__` or `import.meta.env`
6
- * 2. Environment-based default (local: 60s, staging: 5min, production: 15min)
5
+ * 1. Explicit `explicitInterval` parameter (passed from shell's env config)
6
+ * 2. `REGISTRY_POLL_INTERVAL` from SDK `env` (parsed from `window.__ENV__` via `/env.js`)
7
+ * 3. Environment-based default (local: 60s, staging: 5min, production: 15min)
7
8
  *
8
9
  * Returns 0 (disabled) when the explicit value is "0".
9
10
  */
10
- export declare function resolvePollingInterval(environment: string): number;
11
+ export declare function resolvePollingInterval(environment: string, explicitInterval?: number): number;
@@ -1,3 +1,4 @@
1
+ import { env } from "../env.js";
1
2
  const ENVIRONMENT_DEFAULTS = {
2
3
  local: 60000,
3
4
  development: 60000,
@@ -9,18 +10,19 @@ const ENVIRONMENT_DEFAULTS = {
9
10
  * Resolve the registry polling interval in milliseconds.
10
11
  *
11
12
  * Priority:
12
- * 1. Explicit `REGISTRY_POLL_INTERVAL` from `window.__ENV__` or `import.meta.env`
13
- * 2. Environment-based default (local: 60s, staging: 5min, production: 15min)
13
+ * 1. Explicit `explicitInterval` parameter (passed from shell's env config)
14
+ * 2. `REGISTRY_POLL_INTERVAL` from SDK `env` (parsed from `window.__ENV__` via `/env.js`)
15
+ * 3. Environment-based default (local: 60s, staging: 5min, production: 15min)
14
16
  *
15
17
  * Returns 0 (disabled) when the explicit value is "0".
16
18
  */
17
- export function resolvePollingInterval(environment) {
18
- const raw = (typeof window !== 'undefined' && window.__ENV__?.REGISTRY_POLL_INTERVAL) ||
19
- (typeof import.meta !== 'undefined' && import.meta.env?.VITE_REGISTRY_POLL_INTERVAL);
20
- if (raw !== undefined && raw !== null && raw !== '') {
21
- const parsed = Number(raw);
22
- if (!Number.isNaN(parsed))
23
- return parsed;
19
+ export function resolvePollingInterval(environment, explicitInterval) {
20
+ if (explicitInterval !== undefined) {
21
+ return explicitInterval;
22
+ }
23
+ const fromEnv = env.registryPollInterval;
24
+ if (fromEnv !== undefined) {
25
+ return fromEnv;
24
26
  }
25
27
  return ENVIRONMENT_DEFAULTS[environment] ?? ENVIRONMENT_DEFAULTS.local;
26
28
  }
@@ -5,7 +5,7 @@ import type { LocalizedField } from "../i18n/config.js";
5
5
  /**
6
6
  * Module status in the registry
7
7
  */
8
- export type ModuleStatus = "active" | "deprecated" | "disabled";
8
+ export type ModuleStatus = "active" | "disabled";
9
9
  /**
10
10
  * Command within a module that can be searched and invoked
11
11
  */
@@ -68,6 +68,8 @@ export interface Module {
68
68
  };
69
69
  /** Module status */
70
70
  status: ModuleStatus;
71
+ /** Semver of @nsxbet/admin-sdk the module was built against */
72
+ sdkVersion: string;
71
73
  /** Optional navigation order */
72
74
  navigationOrder?: number;
73
75
  /** Optional icon name */
@@ -30,8 +30,8 @@ export interface PlatformAPI {
30
30
  locale: string;
31
31
  /** Authentication methods */
32
32
  auth: {
33
- /** Get the current access token (auto-refreshed by shell) */
34
- getAccessToken: () => Promise<string>;
33
+ /** Get the current access token (auto-refreshed by shell). Null when using BFF HttpOnly cookie auth. */
34
+ getAccessToken: () => Promise<string | null>;
35
35
  /** Check if user has a specific permission (realm role) */
36
36
  hasPermission: (permission: string) => boolean;
37
37
  /** Get current user information */
@@ -74,21 +74,12 @@ export interface PlatformAPI {
74
74
  /** Authenticated fetch wrapper */
75
75
  fetch: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
76
76
  }
77
- /**
78
- * Keycloak configuration stored in window
79
- */
80
- export interface KeycloakWindowConfig {
81
- url: string;
82
- realm: string;
83
- clientId: string;
84
- }
85
77
  /**
86
78
  * Extend Window interface to include platform API
87
79
  */
88
80
  declare global {
89
81
  interface Window {
90
82
  __ADMIN_PLATFORM_API__?: PlatformAPI;
91
- __KEYCLOAK_CONFIG__?: KeycloakWindowConfig;
92
83
  }
93
84
  }
94
85
  export {};
@@ -1,9 +1,4 @@
1
1
  import { type Plugin } from "vite";
2
- /**
3
- * Default admin gateway URL (staging environment).
4
- * Used by the env injection plugin when no explicit value is configured.
5
- */
6
- export declare const ADMIN_GATEWAY_STAGING_URL = "https://admin-bff-stg.nsx.dev";
7
2
  /**
8
3
  * Shared dependencies that are externalized in module builds.
9
4
  * These are provided by the shell via import maps.
@@ -35,11 +30,11 @@ export interface AdminModuleOptions {
35
30
  buildMode?: "auto" | "always";
36
31
  /**
37
32
  * Admin gateway URL for BFF token integration (InMemory auth).
38
- * - `undefined` (default): auto-inject staging URL (`https://admin-bff-stg.nsx.dev`)
39
- * - `string`: inject the provided URL
40
- * - `null`: disable automatic injection entirely
33
+ * - `undefined` (default): use `ADMIN_GATEWAY_URL` from env, or {@link DEFAULT_GATEWAY_URL}
34
+ * - `string`: use this URL in `/env.js`
35
+ * - `null`: disable `/env.js` entirely (no runtime env injection)
41
36
  *
42
- * Explicit env vars (`.env` files, shell env) always take precedence over this option.
37
+ * Explicit env vars (`.env` files, shell env) take precedence over the string option.
43
38
  */
44
39
  gatewayUrl?: string | null;
45
40
  }