@nsxbet/admin-sdk 0.5.1 → 0.7.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 +52 -10
  2. package/README.md +371 -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 +17 -3
  116. package/dist/vite/config.js +34 -8
  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 +3 -2
  120. package/dist/vite/index.js +3 -1
  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
package/README.md CHANGED
@@ -41,7 +41,7 @@ npx @nsxbet/admin-cli check
41
41
  npx @nsxbet/admin-cli check --fix
42
42
  ```
43
43
 
44
- The CLI scaffolds all 13 required files with correct configuration, dependency versions, and SDK integration. It also validates existing modules against the canonical reference.
44
+ The CLI scaffolds all 17 required files (including 4 locale files) with correct configuration, dependency versions, and SDK integration. It also validates existing modules against the canonical reference.
45
45
 
46
46
  > **Required for LLMs:** After creating or modifying a module, run `npx @nsxbet/admin-cli checklist --output CHECKLIST.md` and verify every item in the generated file before considering the setup complete. The checklist is a self-contained verification guide—no CLI installation needed to consume it.
47
47
 
@@ -74,7 +74,7 @@ This SDK enables you to build admin modules that integrate with the NSX Admin sh
74
74
  | File | Purpose | When used |
75
75
  |------|---------|-----------|
76
76
  | `src/spa.tsx` | Default export of App | Shell loads this via React.lazy |
77
- | `src/standalone.tsx` | Full app with AdminShell wrapper | Local development (`npm run dev`) |
77
+ | `src/main.tsx` | Full app with AdminShell wrapper | Local development (`npm run dev`) |
78
78
 
79
79
  ## Complete Module Example
80
80
 
@@ -94,26 +94,53 @@ export default defineModuleConfig({
94
94
 
95
95
  ### File: `admin.module.json`
96
96
 
97
+ Title, description, and command titles must be **localized objects** with all 4 locales (`en-US`, `pt-BR`, `es`, `ro`):
98
+
97
99
  ```json
98
100
  {
99
101
  "id": "@admin/my-module",
100
- "title": "My Module",
101
- "description": "Description of what this module does",
102
- "version": "1.0.0",
102
+ "title": {
103
+ "en-US": "My Module",
104
+ "pt-BR": "Meu Módulo",
105
+ "es": "Mi Módulo",
106
+ "ro": "Modulul Meu"
107
+ },
108
+ "description": {
109
+ "en-US": "Description of what this module does",
110
+ "pt-BR": "Descrição do que este módulo faz",
111
+ "es": "Descripción de lo que hace este módulo",
112
+ "ro": "Descrierea a ceea ce face acest modul"
113
+ },
103
114
  "category": "Tools",
104
115
  "icon": "clipboard-list",
105
116
  "routeBase": "/my-module",
106
117
  "keywords": ["my", "module", "example"],
118
+ "navigation": {
119
+ "style": "stacked",
120
+ "sections": [
121
+ { "id": "general", "label": { "en-US": "General", "pt-BR": "Geral", "es": "General", "ro": "General" } }
122
+ ]
123
+ },
107
124
  "commands": [
108
125
  {
109
126
  "id": "list",
110
- "title": "List Items",
127
+ "title": {
128
+ "en-US": "List Items",
129
+ "pt-BR": "Listar Itens",
130
+ "es": "Listar Elementos",
131
+ "ro": "Lista Elemente"
132
+ },
111
133
  "route": "/my-module/list",
112
134
  "icon": "file-text"
113
135
  },
114
136
  {
115
137
  "id": "new",
116
- "title": "New Item",
138
+ "title": {
139
+ "en-US": "New Item",
140
+ "pt-BR": "Novo Item",
141
+ "es": "Nuevo Elemento",
142
+ "ro": "Element Nou"
143
+ },
117
144
  "route": "/my-module/new",
118
145
  "icon": "plus"
119
146
  }
@@ -142,7 +169,7 @@ import { App } from "./App";
142
169
  export default App;
143
170
  ```
144
171
 
145
- ### File: `src/standalone.tsx`
172
+ ### File: `src/main.tsx`
146
173
 
147
174
  ```tsx
148
175
  import React from "react";
@@ -150,7 +177,6 @@ import ReactDOM from "react-dom/client";
150
177
  import {
151
178
  AdminShell,
152
179
  initI18n,
153
- i18n,
154
180
  createInMemoryAuthClient,
155
181
  createMockUsersFromRoles,
156
182
  } from "@nsxbet/admin-sdk";
@@ -160,19 +186,10 @@ import manifest from "../admin.module.json";
160
186
 
161
187
  import "./index.css";
162
188
 
163
- // Import module translations (optional - for i18n support)
164
- import enUS from "./i18n/locales/en-US.json";
165
- import ptBR from "./i18n/locales/pt-BR.json";
166
-
167
- // Initialize i18n BEFORE shell renders
189
+ // Initialize i18n BEFORE shell renders.
190
+ // The Vite plugin (admin-module-i18n) auto-injects module translation registration into spa.tsx and main.tsx.
168
191
  initI18n();
169
192
 
170
- // Register module translations with namespace matching your module
171
- const NAMESPACE = "mymodule";
172
- i18n.addResourceBundle("en-US", NAMESPACE, enUS, true, true);
173
- i18n.addResourceBundle("pt-BR", NAMESPACE, ptBR, true, true);
174
-
175
- // Type assertion for JSON import
176
193
  const moduleManifest = manifest as AdminModuleManifest;
177
194
 
178
195
  // Check environment variable to toggle between mock auth and Keycloak
@@ -288,25 +305,27 @@ export function ItemList() {
288
305
  @tailwind utilities;
289
306
  ```
290
307
 
291
- ### Directory: `src/i18n/` (optional - for translations)
308
+ ### Directory: `src/i18n/` (required for translations)
309
+
310
+ All 4 locale files are **required** (`en-US`, `pt-BR`, `es`, `ro`). The Vite plugin validates this at build time.
292
311
 
293
312
  Structure:
294
313
 
295
314
  ```
296
315
  src/i18n/
297
316
  locales/
298
- en-US.json # English (required)
299
- pt-BR.json # Portuguese
317
+ en-US.json
318
+ pt-BR.json
319
+ es.json
320
+ ro.json
300
321
  ```
301
322
 
302
- Example `src/i18n/locales/en-US.json`:
323
+ **No manual registration needed.** The `admin-module-i18n` Vite plugin (included in `defineModuleConfig`) auto-injects `registerModuleTranslations` into `spa.tsx` and `main.tsx` at build/serve time. Just create the locale files.
324
+
325
+ Example `src/i18n/locales/en-US.json` (content translations only — nav titles live in `admin.module.json`):
303
326
 
304
327
  ```json
305
328
  {
306
- "module": {
307
- "title": "My Module",
308
- "description": "Description of my module"
309
- },
310
329
  "list": {
311
330
  "title": "All Items",
312
331
  "noItems": "No items found.",
@@ -327,13 +346,14 @@ Use translations in components:
327
346
  import { useI18n } from "@nsxbet/admin-sdk";
328
347
 
329
348
  function MyComponent() {
330
- const { t } = useI18n();
349
+ const { t } = useI18n("my-module");
331
350
 
332
- // Use with namespace prefix (set in standalone.tsx)
333
- return <h1>{t("mymodule:list.title")}</h1>;
351
+ return <h1>{t("list.title")}</h1>;
334
352
  }
335
353
  ```
336
354
 
355
+ The namespace is derived from your module id (e.g. `@admin/my-module` → `my-module`).
356
+
337
357
  ### File: `tailwind.config.js`
338
358
 
339
359
  Use `withAdminSdk` which automatically includes the UI preset and SDK/UI content paths:
@@ -423,7 +443,7 @@ export default withAdminSdk({
423
443
  </head>
424
444
  <body>
425
445
  <div id="root"></div>
426
- <script type="module" src="/src/standalone.tsx"></script>
446
+ <script type="module" src="/src/main.tsx"></script>
427
447
  </body>
428
448
  </html>
429
449
  ```
@@ -449,6 +469,7 @@ TypeScript declarations for environment variables and platform API:
449
469
  declare global {
450
470
  interface ImportMetaEnv {
451
471
  readonly VITE_MOCK_AUTH?: string;
472
+ readonly VITE_ALLOWED_MODULE_ORIGINS?: string;
452
473
  }
453
474
 
454
475
  interface ImportMeta {
@@ -497,6 +518,11 @@ VITE_MOCK_AUTH=true
497
518
 
498
519
  # Set to "false" to use Keycloak authentication
499
520
  # VITE_MOCK_AUTH=false
521
+
522
+ # Module URL allowlist (shell mode only). Comma-separated patterns.
523
+ # Supports *.domain wildcard (e.g. *.nsx.dev matches modules.nsx.dev, nsx.dev).
524
+ # In dev mode, localhost and 127.0.0.1 are always allowed.
525
+ # VITE_ALLOWED_MODULE_ORIGINS=*.nsx.dev,*.nsx.services
500
526
  ```
501
527
 
502
528
  ## Manifest Schema (`admin.module.json`)
@@ -507,10 +533,10 @@ VITE_MOCK_AUTH=true
507
533
  | `title` | string | ✅ | Human-readable title |
508
534
  | `routeBase` | string | ✅ | Base route path (must start with `/`) |
509
535
  | `description` | string | | What the module does |
510
- | `version` | string | | Semantic version |
511
536
  | `category` | string | | Navigation grouping |
512
537
  | `icon` | string | | Lucide icon name in kebab-case |
513
538
  | `keywords` | string[] | | Search keywords |
539
+ | `navigation` | object | | Navigation config (`style`, `sections`) |
514
540
  | `commands` | Command[] | | Available actions |
515
541
  | `permissions` | object | | Permission configuration |
516
542
  | `owners` | object | | Team ownership info |
@@ -524,6 +550,41 @@ VITE_MOCK_AUTH=true
524
550
  | `route` | string | ✅ | Full route path |
525
551
  | `icon` | string | | Lucide icon name |
526
552
  | `keywords` | string[] | | Search keywords |
553
+ | `section` | string | | Section ID for stacked navigation grouping |
554
+
555
+ ### Stacked Navigation
556
+
557
+ Modules with many commands can use stacked navigation for a dedicated sidebar panel with sectioned command grouping.
558
+
559
+ **Enable stacked navigation** by adding a `navigation` field to `admin.module.json`:
560
+
561
+ ```json
562
+ {
563
+ "navigation": {
564
+ "style": "stacked",
565
+ "sections": [
566
+ {
567
+ "id": "general",
568
+ "label": { "en-US": "General", "pt-BR": "Geral", "es": "General", "ro": "General" }
569
+ },
570
+ {
571
+ "id": "advanced",
572
+ "label": { "en-US": "Advanced", "pt-BR": "Avançado", "es": "Avanzado", "ro": "Avansat" }
573
+ }
574
+ ]
575
+ },
576
+ "commands": [
577
+ { "id": "list", "title": {...}, "route": "/mod/list", "section": "general" },
578
+ { "id": "settings", "title": {...}, "route": "/mod/settings", "section": "advanced" }
579
+ ]
580
+ }
581
+ ```
582
+
583
+ - `style`: `"stacked"` enables the dedicated panel; `"collapsible"` (default) keeps the inline expand behavior
584
+ - `sections`: Array of section definitions with `id` and localized `label`
585
+ - Commands reference sections via the `section` field matching a section `id`
586
+ - Commands without a `section` appear in an implicit top-level group
587
+ - The stacked panel is URL-driven: navigating to the module's `routeBase` activates it
527
588
 
528
589
  ### Icon Names
529
590
 
@@ -615,6 +676,38 @@ export default defineConfig(({ mode }) => ({
615
676
  | `entry` | string | `"./src/spa.tsx"` | Entry file path |
616
677
  | `outDir` | string | `"dist"` | Output directory |
617
678
  | `additionalExternals` | string[] | `[]` | Extra externals |
679
+ | `gatewayUrl` | `string \| null` | `undefined` | Admin gateway URL for env injection. `undefined` = auto-inject staging URL, `string` = inject custom URL, `null` = disable injection. Explicit env vars always take precedence. |
680
+
681
+ ### Automatic Environment Injection
682
+
683
+ `adminModule()` includes an env injection plugin that automatically sets `VITE_ADMIN_GATEWAY_URL` at build time. This ensures Lovable projects get real JWT tokens from the staging gateway without any manual `.env` configuration.
684
+
685
+ **Precedence order** (highest to lowest):
686
+
687
+ 1. Explicit env var (`.env`, `.env.local`, shell environment)
688
+ 2. `gatewayUrl` option passed to `adminModule()` or `defineModuleConfig()`
689
+ 3. Default: `https://admin-bff-stg.nsx.dev` (staging)
690
+
691
+ **Known gateway URLs:**
692
+
693
+ | Environment | URL |
694
+ |---|---|
695
+ | Staging | `https://admin-bff-stg.nsx.dev` |
696
+ | Homol | `https://admin-bff-homol.nsx.dev` |
697
+ | Local | `http://localhost:8080` |
698
+
699
+ **Examples:**
700
+
701
+ ```ts
702
+ // Default: staging URL auto-injected
703
+ ...adminModule()
704
+
705
+ // Custom URL
706
+ ...adminModule({ gatewayUrl: "http://localhost:8080" })
707
+
708
+ // Disable injection entirely
709
+ ...adminModule({ gatewayUrl: null })
710
+ ```
618
711
 
619
712
  ### Shared Externals
620
713
 
@@ -694,7 +787,10 @@ import adminPlugin from "@nsxbet/eslint-plugin-admin";
694
787
  export default [adminPlugin.configs.recommended];
695
788
  ```
696
789
 
697
- This enables all recommended rules including `@nsxbet/no-raw-fetch` which flags direct `fetch()`/`window.fetch()` calls that bypass authentication. Use `useFetch()` from the SDK instead.
790
+ This enables all recommended rules:
791
+
792
+ - **`@nsxbet/no-raw-fetch`** (error) — flags direct `fetch()`/`window.fetch()` calls that bypass authentication. Use `useFetch()` instead.
793
+ - **`@nsxbet/no-raw-date-format`** (warn) — flags direct date formatting (`toLocaleDateString`, `toLocaleTimeString`, `Intl.DateTimeFormat`, dayjs/moment `.format()`, date-fns `format`) that bypasses the platform timezone preference. Use `<Timestamp />` or `useTimestamp()` instead.
698
794
 
699
795
  ## SDK Hooks
700
796
 
@@ -719,7 +815,7 @@ function MyComponent() {
719
815
 
720
816
  | Method | Returns | Description |
721
817
  |--------|---------|-------------|
722
- | `hasPermission(perm)` | boolean | Check if user has permission |
818
+ | `hasPermission(perm)` | boolean | Check if user has permission. Returns `false` during Keycloak initialization until auth completes. |
723
819
  | `getUser()` | User | Get current user info |
724
820
  | `getAccessToken()` | Promise\<string\> | Get JWT token |
725
821
  | `logout()` | void | Log out user |
@@ -799,6 +895,132 @@ function MyComponent() {
799
895
  }
800
896
  ```
801
897
 
898
+ ### `useTimestamp()`
899
+
900
+ Timezone-aware date formatting that respects the shell's UTC/Local preference.
901
+
902
+ ```typescript
903
+ import { useTimestamp, Timestamp } from "@nsxbet/admin-sdk";
904
+
905
+ function MyComponent() {
906
+ const { mode, setMode, formatDate, timezone } = useTimestamp();
907
+
908
+ return (
909
+ <div>
910
+ <p>Timezone: {timezone} ({mode})</p>
911
+ <p>Formatted: {formatDate(new Date(), "datetime")}</p>
912
+ <Timestamp value={new Date()} format="date" />
913
+ </div>
914
+ );
915
+ }
916
+ ```
917
+
918
+ | Property | Type | Description |
919
+ |----------|------|-------------|
920
+ | `mode` | `"utc" \| "local"` | Current timezone mode |
921
+ | `setMode` | `(mode) => void` | Change the timezone mode |
922
+ | `formatDate` | `(date, format?) => string` | Format a date with current mode and locale |
923
+ | `timezone` | `string` | Resolved IANA timezone string (`"UTC"` or browser local) |
924
+
925
+ **Format presets:**
926
+
927
+ | Preset | Example (UTC, en-US) | Description |
928
+ |--------|----------------------|-------------|
929
+ | `"datetime"` (default) | "Mar 16, 2026, 2:30:05 PM UTC" | Full date and time |
930
+ | `"date"` | "Mar 16, 2026" | Date only |
931
+ | `"time"` | "2:30:05 PM UTC" | Time only |
932
+ | `"relative"` | "5 minutes ago" | Relative to now (timezone-independent) |
933
+
934
+ In shell mode, reads from `window.__ADMIN_PLATFORM_API__.timestamp`. In standalone mode, falls back to `localStorage` key `admin-timezone-mode` (default: `"local"`).
935
+
936
+ ### `<Timestamp />` Component
937
+
938
+ Renders a formatted date that automatically respects the shell's timezone preference.
939
+
940
+ ```tsx
941
+ import { Timestamp } from "@nsxbet/admin-sdk";
942
+
943
+ // Basic usage (datetime format)
944
+ <Timestamp value={new Date("2026-03-16T14:30:05Z")} />
945
+
946
+ // Date only
947
+ <Timestamp value={createdAt} format="date" />
948
+
949
+ // Relative time
950
+ <Timestamp value={updatedAt} format="relative" />
951
+
952
+ // String input (auto-parsed)
953
+ <Timestamp value="2026-03-16T14:30:05Z" format="time" />
954
+ ```
955
+
956
+ | Prop | Type | Default | Description |
957
+ |------|------|---------|-------------|
958
+ | `value` | `Date \| string` | required | The date to display |
959
+ | `format` | `TimestampFormat` | `"datetime"` | Format preset |
960
+ | `className` | `string` | | CSS class for the `<time>` element |
961
+
962
+ Renders a semantic `<time>` element with a `dateTime` attribute. Hovering shows a tooltip with the date in the opposite timezone mode.
963
+
964
+ **Migration guide:**
965
+
966
+ | Before | After |
967
+ |--------|-------|
968
+ | `date.toLocaleDateString()` | `<Timestamp value={date} format="date" />` |
969
+ | `date.toLocaleString()` | `<Timestamp value={date} />` |
970
+ | `date.toLocaleTimeString()` | `<Timestamp value={date} format="time" />` |
971
+ | `new Intl.DateTimeFormat(...).format(date)` | `<Timestamp value={date} />` |
972
+
973
+ ### `useRegistryPolling()`
974
+
975
+ Detect catalog changes via lightweight version polling. Used by the shell to show an "Updates available" banner without forcing a reload.
976
+
977
+ ```typescript
978
+ import { useRegistryPolling } from "@nsxbet/admin-sdk";
979
+
980
+ const { hasUpdates, dismiss } = useRegistryPolling({
981
+ registryClient,
982
+ initialVersion: catalog.version,
983
+ interval: 60000, // poll every 60s, 0 to disable
984
+ });
985
+ ```
986
+
987
+ | Property | Type | Description |
988
+ |----------|------|-------------|
989
+ | `hasUpdates` | boolean | `true` when the server version differs from the loaded version |
990
+ | `dismiss` | () => void | Hide the banner for the current version; re-shows on newer versions |
991
+
992
+ **Options:**
993
+
994
+ | Option | Type | Description |
995
+ |--------|------|-------------|
996
+ | `registryClient` | RegistryClient | The registry client instance |
997
+ | `initialVersion` | string | Version string from the initial `catalog.get()` response |
998
+ | `interval` | number \| undefined | Polling interval in ms. `0` or `undefined` disables polling |
999
+
1000
+ The hook integrates the Page Visibility API — polling pauses when the tab is hidden and resumes with an immediate check when the tab becomes visible again. Network errors are silently ignored.
1001
+
1002
+ #### `catalog.version()`
1003
+
1004
+ Both HTTP and in-memory registry clients expose a `catalog.version()` method:
1005
+
1006
+ ```typescript
1007
+ const { version, generatedAt } = await registryClient.catalog.version();
1008
+ ```
1009
+
1010
+ The HTTP client calls `GET /api/catalog/version`. The in-memory client derives a version from the local mutation state.
1011
+
1012
+ #### Environment configuration
1013
+
1014
+ The polling interval is resolved from `REGISTRY_POLL_INTERVAL` (milliseconds):
1015
+
1016
+ | Source | Example |
1017
+ |--------|---------|
1018
+ | `window.__ENV__.REGISTRY_POLL_INTERVAL` | Docker runtime injection |
1019
+ | `import.meta.env.VITE_REGISTRY_POLL_INTERVAL` | Vite env var |
1020
+ | Environment default | `local`: 60s, `staging`: 5min, `production`: 15min |
1021
+
1022
+ Set `REGISTRY_POLL_INTERVAL=0` to disable polling entirely.
1023
+
802
1024
  ### Navigation
803
1025
 
804
1026
  **Use `useNavigate` from `react-router-dom` directly:**
@@ -847,7 +1069,7 @@ const mockUsers = createMockUsersFromRoles({
847
1069
  // Create auth client with custom users
848
1070
  const authClient = createInMemoryAuthClient({ users: mockUsers });
849
1071
 
850
- // Use in standalone.tsx
1072
+ // Use in main.tsx
851
1073
  ReactDOM.createRoot(document.getElementById("root")!).render(
852
1074
  <AdminShell authClient={authClient} modules={[manifest]}>
853
1075
  <App />
@@ -892,6 +1114,50 @@ const authClient = createInMemoryAuthClient({ users: customUsers });
892
1114
  |--------|------|---------|-------------|
893
1115
  | `users` | MockUser[] | **required** | Mock users available for selection |
894
1116
  | `storageKey` | string | `"@nsxbet/auth"` | localStorage key for persistence |
1117
+ | `gatewayUrl` | string \| null | `VITE_ADMIN_GATEWAY_URL` env var | Admin gateway URL for real JWT tokens |
1118
+ | `tokenTimeout` | number | `5000` | Timeout in ms for gateway token fetch |
1119
+
1120
+ ### Gateway Token Integration (BFF)
1121
+
1122
+ When `VITE_ADMIN_GATEWAY_URL` is set (or `gatewayUrl` is passed explicitly), the InMemory auth client fetches real signed JWTs from the admin gateway instead of returning mock token strings. This enables end-to-end testing against backends that validate tokens.
1123
+
1124
+ **How it works:**
1125
+
1126
+ 1. Developer clicks a user card in the UserSelector
1127
+ 2. The client calls `GET {gatewayUrl}/auth/token?sub=...&email=...&roles=...&scopes=...`
1128
+ 3. The gateway returns a signed JWT which is cached in memory
1129
+ 4. `getAccessToken()` returns the cached JWT for all subsequent API calls
1130
+ 5. When the token nears expiry (within 60s), a background refresh is triggered
1131
+
1132
+ **Gateway URL resolution order:**
1133
+
1134
+ 1. Explicit `gatewayUrl` option passed to `createInMemoryAuthClient()`
1135
+ 2. `import.meta.env.VITE_ADMIN_GATEWAY_URL` environment variable
1136
+ 3. If neither is set, BFF integration is disabled (mock tokens, current behavior)
1137
+
1138
+ > **Vite plugin auto-injection:** When using `adminModule()` or `defineModuleConfig()` from `@nsxbet/admin-sdk/vite`, the `VITE_ADMIN_GATEWAY_URL` environment variable is automatically set to the staging gateway URL. No `.env.staging` file or `--mode staging` script is needed. See the [Vite Configuration](#vite-configuration) section for override options.
1139
+
1140
+ **Error handling:**
1141
+
1142
+ The UserSelector shows loading, error, and timeout states during the token fetch. If the gateway is unreachable, the developer can:
1143
+ - **Retry** the token fetch
1144
+ - **Continue with mock token** to fall back to legacy behavior
1145
+ - **Go back** to the user selection list
1146
+
1147
+ ```bash
1148
+ # Add to your .env file to enable gateway token integration
1149
+ VITE_ADMIN_GATEWAY_URL=https://admin-bff-stg.nsx.dev
1150
+ ```
1151
+
1152
+ ## Module URL Allowlist (Shell Mode)
1153
+
1154
+ When the shell loads modules dynamically from URLs, it validates each URL against `VITE_ALLOWED_MODULE_ORIGINS` before `import()` or `loadScript()`.
1155
+
1156
+ **Format:** Comma-separated patterns supporting `*.domain` wildcard (e.g. `*.nsx.dev` matches `modules.nsx.dev`, `cdn.nsx.dev`, and apex `nsx.dev`).
1157
+
1158
+ **Dev mode:** `localhost` and `127.0.0.1` are always allowed regardless of the allowlist, so local dev servers work without configuration.
1159
+
1160
+ **Production:** Set `VITE_ALLOWED_MODULE_ORIGINS` in your build environment. If unset or empty, all module loads fail with a clear error.
895
1161
 
896
1162
  ## Keycloak Configuration (Production Auth)
897
1163
 
@@ -929,6 +1195,10 @@ Or pass the `keycloak` prop directly without `authClient`:
929
1195
  </AdminShell>
930
1196
  ```
931
1197
 
1198
+ ### Mock Auth Production Guard
1199
+
1200
+ In production builds, AdminShell requires authentication configuration. If you omit both `authClient` and `keycloak` props, the shell throws an error unless you explicitly set `VITE_MOCK_AUTH=true`. When mock auth is used in production with `VITE_MOCK_AUTH=true`, a console warning is logged. Always use Keycloak (or another real auth client) for production deployments.
1201
+
932
1202
  ### Keycloak Configuration Options
933
1203
 
934
1204
  | Option | Type | Description |
@@ -1026,6 +1296,29 @@ const fetch = useFetch();
1026
1296
  const data = await fetch("/api/internal-endpoint");
1027
1297
  ```
1028
1298
 
1299
+ ### ❌ DO NOT format dates directly
1300
+
1301
+ ```typescript
1302
+ // WRONG - bypasses platform timezone preference
1303
+ date.toLocaleDateString(); // ❌
1304
+ date.toLocaleTimeString(); // ❌
1305
+ new Intl.DateTimeFormat("en-US").format(date); // ❌
1306
+ dayjs(date).format("YYYY-MM-DD"); // ❌
1307
+ import { format } from "date-fns"; format(date, "PP"); // ❌
1308
+ ```
1309
+
1310
+ ```tsx
1311
+ // CORRECT - use <Timestamp /> or useTimestamp()
1312
+ import { Timestamp, useTimestamp } from "@nsxbet/admin-sdk";
1313
+
1314
+ <Timestamp value={date} format="date" /> // ✅
1315
+
1316
+ const { formatDate } = useTimestamp();
1317
+ formatDate(date, "datetime"); // ✅
1318
+ ```
1319
+
1320
+ Direct formatting bypasses the platform timezone preference, causing inconsistent timestamp display across modules. The `@nsxbet/no-raw-date-format` ESLint rule enforces this.
1321
+
1029
1322
  ### ❌ DO NOT import useNavigate from SDK
1030
1323
 
1031
1324
  ```typescript
@@ -1091,6 +1384,43 @@ bun run build
1091
1384
  bun run preview
1092
1385
  ```
1093
1386
 
1387
+ ## Error Boundary with Module Ownership
1388
+
1389
+ When a module crashes at runtime, the error boundary displays ownership information to help with incident triage. The `DynamicModule` component accepts an optional `moduleInfo` prop that provides module metadata to the error boundary.
1390
+
1391
+ ```tsx
1392
+ <DynamicModule
1393
+ baseUrl="http://localhost:5001"
1394
+ moduleInfo={{
1395
+ id: "@admin/payments",
1396
+ title: { "en-US": "Payments", "pt-BR": "Pagamentos", "es": "Pagos", "ro": "Plăți" },
1397
+ owners: { team: "Payments", supportChannel: "#payments-support" },
1398
+ }}
1399
+ />
1400
+ ```
1401
+
1402
+ When a module error occurs, the error boundary will:
1403
+ - Display the module name (localized for the current locale)
1404
+ - Show the owner team and support channel (if provided)
1405
+ - Render the support channel as a clickable link if it starts with `http`/`https`
1406
+ - Provide both "Try Again" (re-render) and "Reload Page" (full reload) buttons
1407
+ - Report the error to telemetry with module attribution (`moduleId`, `ownerTeam`, `errorType`)
1408
+ - Capture unhandled promise rejections while the module is mounted and report them via telemetry
1409
+
1410
+ If `owners.team` is empty, the ownership section is omitted and the error boundary shows a standard error UI.
1411
+
1412
+ ## Platform API — Timestamp Namespace
1413
+
1414
+ The shell exposes `window.__ADMIN_PLATFORM_API__.timestamp` for timezone preference management:
1415
+
1416
+ | Property | Type | Description |
1417
+ |----------|------|-------------|
1418
+ | `mode` | `"utc" \| "local"` | Current timezone mode |
1419
+ | `setMode(mode)` | `(TimezoneMode) => void` | Change the timezone mode |
1420
+ | `onModeChange(cb)` | `(callback) => () => void` | Subscribe to changes; returns unsubscribe |
1421
+
1422
+ > **Note:** Modules should use `useTimestamp()` or `<Timestamp />` rather than accessing the Platform API directly.
1423
+
1094
1424
  ## Types
1095
1425
 
1096
1426
  ```typescript
@@ -1100,6 +1430,11 @@ import type {
1100
1430
  PlatformAPI,
1101
1431
  User,
1102
1432
  Breadcrumb,
1433
+ ModuleInfo,
1434
+ ErrorBoundaryProps,
1435
+ TimezoneMode,
1436
+ TimestampFormat,
1437
+ UseTimestampResult,
1103
1438
  } from "@nsxbet/admin-sdk";
1104
1439
  ```
1105
1440
 
@@ -0,0 +1,19 @@
1
+ import type { MockUser } from './interface';
2
+ export declare class GatewayTimeoutError extends Error {
3
+ readonly gatewayUrl: string;
4
+ readonly timeoutMs: number;
5
+ constructor(gatewayUrl: string, timeoutMs: number);
6
+ }
7
+ export declare class GatewayFetchError extends Error {
8
+ readonly gatewayUrl: string;
9
+ readonly statusCode?: number | undefined;
10
+ readonly originalError?: unknown | undefined;
11
+ constructor(gatewayUrl: string, statusCode?: number | undefined, originalError?: unknown | undefined);
12
+ }
13
+ export interface GatewayTokenResult {
14
+ token: string;
15
+ expiresAt: number;
16
+ }
17
+ export declare function fetchGatewayToken(gatewayUrl: string, user: MockUser, options?: {
18
+ timeoutMs?: number;
19
+ }): Promise<GatewayTokenResult>;