@nsxbet/admin-sdk 0.5.0 → 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 +10 -2
  116. package/dist/vite/config.js +13 -10
  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
@@ -0,0 +1,89 @@
1
+ export class GatewayTimeoutError extends Error {
2
+ constructor(gatewayUrl, timeoutMs) {
3
+ super(`Gateway at ${gatewayUrl} did not respond within ${timeoutMs}ms`);
4
+ Object.defineProperty(this, "gatewayUrl", {
5
+ enumerable: true,
6
+ configurable: true,
7
+ writable: true,
8
+ value: gatewayUrl
9
+ });
10
+ Object.defineProperty(this, "timeoutMs", {
11
+ enumerable: true,
12
+ configurable: true,
13
+ writable: true,
14
+ value: timeoutMs
15
+ });
16
+ this.name = 'GatewayTimeoutError';
17
+ }
18
+ }
19
+ export class GatewayFetchError extends Error {
20
+ constructor(gatewayUrl, statusCode, originalError) {
21
+ const detail = statusCode
22
+ ? `returned HTTP ${statusCode}`
23
+ : 'is not reachable';
24
+ super(`Gateway at ${gatewayUrl} ${detail}`);
25
+ Object.defineProperty(this, "gatewayUrl", {
26
+ enumerable: true,
27
+ configurable: true,
28
+ writable: true,
29
+ value: gatewayUrl
30
+ });
31
+ Object.defineProperty(this, "statusCode", {
32
+ enumerable: true,
33
+ configurable: true,
34
+ writable: true,
35
+ value: statusCode
36
+ });
37
+ Object.defineProperty(this, "originalError", {
38
+ enumerable: true,
39
+ configurable: true,
40
+ writable: true,
41
+ value: originalError
42
+ });
43
+ this.name = 'GatewayFetchError';
44
+ }
45
+ }
46
+ function isValidResponse(data) {
47
+ if (typeof data !== 'object' || data === null)
48
+ return false;
49
+ const obj = data;
50
+ return typeof obj.token === 'string' && (typeof obj.expires === 'string' || typeof obj.expires === 'number');
51
+ }
52
+ export async function fetchGatewayToken(gatewayUrl, user, options) {
53
+ const timeoutMs = options?.timeoutMs ?? 5000;
54
+ const params = new URLSearchParams({
55
+ sub: user.id,
56
+ email: user.email,
57
+ roles: user.roles.join(','),
58
+ scopes: 'openid,profile,email',
59
+ });
60
+ const url = `${gatewayUrl}/auth/token?${params.toString()}`;
61
+ const controller = new AbortController();
62
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
63
+ try {
64
+ const response = await fetch(url, { signal: controller.signal });
65
+ if (!response.ok) {
66
+ throw new GatewayFetchError(gatewayUrl, response.status);
67
+ }
68
+ const data = await response.json();
69
+ if (!isValidResponse(data)) {
70
+ throw new GatewayFetchError(gatewayUrl);
71
+ }
72
+ const expiresAt = typeof data.expires === 'number'
73
+ ? data.expires * 1000
74
+ : new Date(data.expires).getTime();
75
+ return { token: data.token, expiresAt };
76
+ }
77
+ catch (error) {
78
+ if (error instanceof GatewayTimeoutError || error instanceof GatewayFetchError) {
79
+ throw error;
80
+ }
81
+ if (error instanceof DOMException && error.name === 'AbortError') {
82
+ throw new GatewayTimeoutError(gatewayUrl, timeoutMs);
83
+ }
84
+ throw new GatewayFetchError(gatewayUrl, undefined, error);
85
+ }
86
+ finally {
87
+ clearTimeout(timeoutId);
88
+ }
89
+ }
@@ -5,6 +5,7 @@
5
5
  * Users can be selected from a predefined list or created custom.
6
6
  */
7
7
  import type { MockUser, InMemoryAuthClient } from './interface';
8
+ export { GatewayTimeoutError, GatewayFetchError } from './gateway-token';
8
9
  /**
9
10
  * Role configuration for creating mock users
10
11
  */
@@ -45,6 +46,10 @@ export interface InMemoryAuthClientOptions {
45
46
  users: MockUser[];
46
47
  /** localStorage key prefix (defaults to '@nsxbet/auth') */
47
48
  storageKey?: string;
49
+ /** Admin gateway URL for fetching real signed JWTs. If not set, falls back to import.meta.env.VITE_ADMIN_GATEWAY_URL. If neither is set, BFF integration is disabled. */
50
+ gatewayUrl?: string | null;
51
+ /** Timeout in milliseconds for gateway token fetch requests (defaults to 5000) */
52
+ tokenTimeout?: number;
48
53
  }
49
54
  /**
50
55
  * Create an in-memory auth client for development/testing
@@ -66,4 +71,3 @@ export declare function createInMemoryAuthClient(options: InMemoryAuthClientOpti
66
71
  * Clear in-memory auth storage (useful for tests)
67
72
  */
68
73
  export declare function clearInMemoryAuth(storageKey?: string): void;
69
- //# sourceMappingURL=in-memory.d.ts.map
@@ -4,6 +4,8 @@
4
4
  * Provides fake authentication for development and testing.
5
5
  * Users can be selected from a predefined list or created custom.
6
6
  */
7
+ import { fetchGatewayToken } from './gateway-token';
8
+ export { GatewayTimeoutError, GatewayFetchError } from './gateway-token';
7
9
  /**
8
10
  * Create mock users from a role configuration
9
11
  *
@@ -67,15 +69,19 @@ const DEFAULT_STORAGE_KEY = '@nsxbet/auth';
67
69
  * ```
68
70
  */
69
71
  export function createInMemoryAuthClient(options) {
70
- const { users, storageKey = DEFAULT_STORAGE_KEY } = options;
71
- // Use provided users
72
+ const { users, storageKey = DEFAULT_STORAGE_KEY, tokenTimeout = 5000 } = options;
72
73
  const predefinedUsers = users;
74
+ // Resolve gatewayUrl: explicit option → env var → null (disabled)
75
+ const resolvedGatewayUrl = options.gatewayUrl !== undefined
76
+ ? options.gatewayUrl ?? null
77
+ : (typeof import.meta !== 'undefined' && import.meta.env?.VITE_ADMIN_GATEWAY_URL) || null;
73
78
  // State
74
79
  let selectedUser = null;
80
+ let tokenCache = null;
81
+ let useMockFallback = false;
82
+ let backgroundRefreshInFlight = false;
75
83
  const subscribers = new Set();
76
- /**
77
- * Load state from localStorage
78
- */
84
+ const REFRESH_BUFFER_MS = 60000;
79
85
  function loadStorage() {
80
86
  try {
81
87
  const data = localStorage.getItem(storageKey);
@@ -88,28 +94,16 @@ export function createInMemoryAuthClient(options) {
88
94
  }
89
95
  return { selectedUserId: null, customUsers: [] };
90
96
  }
91
- /**
92
- * Save state to localStorage
93
- */
94
97
  function saveStorage(data) {
95
98
  localStorage.setItem(storageKey, JSON.stringify(data));
96
99
  }
97
- /**
98
- * Get all available users (predefined + custom)
99
- */
100
100
  function getAllUsers() {
101
101
  const storage = loadStorage();
102
102
  return [...predefinedUsers, ...storage.customUsers];
103
103
  }
104
- /**
105
- * Find user by ID
106
- */
107
104
  function findUser(userId) {
108
105
  return getAllUsers().find((u) => u.id === userId);
109
106
  }
110
- /**
111
- * Notify subscribers of state change
112
- */
113
107
  function notifySubscribers() {
114
108
  const state = {
115
109
  isAuthenticated: selectedUser !== null,
@@ -117,23 +111,36 @@ export function createInMemoryAuthClient(options) {
117
111
  id: selectedUser.id,
118
112
  email: selectedUser.email,
119
113
  displayName: selectedUser.displayName,
114
+ roles: selectedUser.roles,
120
115
  } : null,
121
116
  };
122
117
  subscribers.forEach((callback) => callback(state));
123
118
  }
124
- /**
125
- * Convert MockUser to User
126
- */
127
119
  function toUser(mockUser) {
128
120
  return {
129
121
  id: mockUser.id,
130
122
  email: mockUser.email,
131
123
  displayName: mockUser.displayName,
124
+ roles: mockUser.roles,
132
125
  };
133
126
  }
134
- // Public API
127
+ function mockToken() {
128
+ return `mock-token-${selectedUser.id}-${Date.now()}`;
129
+ }
130
+ async function refreshToken() {
131
+ return fetchGatewayToken(resolvedGatewayUrl, selectedUser, { timeoutMs: tokenTimeout });
132
+ }
133
+ function selectUser(user, userId) {
134
+ selectedUser = user;
135
+ const storage = loadStorage();
136
+ storage.selectedUserId = userId;
137
+ saveStorage(storage);
138
+ }
135
139
  const client = {
136
140
  type: 'in-memory',
141
+ get gatewayUrl() {
142
+ return resolvedGatewayUrl;
143
+ },
137
144
  async initialize() {
138
145
  const storage = loadStorage();
139
146
  if (storage.selectedUserId) {
@@ -155,21 +162,44 @@ export function createInMemoryAuthClient(options) {
155
162
  if (!selectedUser) {
156
163
  throw new Error('Not authenticated');
157
164
  }
158
- return `mock-token-${selectedUser.id}-${Date.now()}`;
165
+ if (useMockFallback || !resolvedGatewayUrl) {
166
+ return mockToken();
167
+ }
168
+ if (tokenCache) {
169
+ const now = Date.now();
170
+ if (now < tokenCache.expiresAt) {
171
+ // Token still valid — trigger background refresh if nearing expiry
172
+ if (now >= tokenCache.expiresAt - REFRESH_BUFFER_MS && !backgroundRefreshInFlight) {
173
+ backgroundRefreshInFlight = true;
174
+ refreshToken()
175
+ .then((result) => { tokenCache = result; })
176
+ .catch(() => { console.warn('[InMemoryAuth] Background token refresh failed, will retry on next access'); })
177
+ .finally(() => { backgroundRefreshInFlight = false; });
178
+ }
179
+ return tokenCache.token;
180
+ }
181
+ // Token fully expired — try foreground fetch
182
+ try {
183
+ tokenCache = await refreshToken();
184
+ return tokenCache.token;
185
+ }
186
+ catch {
187
+ console.warn('[InMemoryAuth] Token expired and refresh failed, falling back to mock token');
188
+ return mockToken();
189
+ }
190
+ }
191
+ return mockToken();
159
192
  },
160
193
  hasPermission(permission) {
161
194
  if (!selectedUser) {
162
195
  return false;
163
196
  }
164
- // '*' means all permissions
165
197
  if (selectedUser.roles.includes('*')) {
166
198
  return true;
167
199
  }
168
- // Check exact match or wildcard
169
200
  return selectedUser.roles.some((role) => {
170
201
  if (role === permission)
171
202
  return true;
172
- // Support wildcard like 'admin.*'
173
203
  if (role.endsWith('.*')) {
174
204
  const prefix = role.slice(0, -2);
175
205
  return permission.startsWith(prefix);
@@ -179,6 +209,8 @@ export function createInMemoryAuthClient(options) {
179
209
  },
180
210
  logout() {
181
211
  selectedUser = null;
212
+ tokenCache = null;
213
+ useMockFallback = false;
182
214
  const storage = loadStorage();
183
215
  storage.selectedUserId = null;
184
216
  saveStorage(storage);
@@ -193,34 +225,39 @@ export function createInMemoryAuthClient(options) {
193
225
  getAvailableUsers() {
194
226
  return getAllUsers();
195
227
  },
196
- login(userId) {
228
+ async login(userId, loginOptions) {
197
229
  const user = findUser(userId);
198
230
  if (!user) {
199
231
  throw new Error(`User not found: ${userId}`);
200
232
  }
201
- selectedUser = user;
202
- const storage = loadStorage();
203
- storage.selectedUserId = userId;
204
- saveStorage(storage);
233
+ tokenCache = null;
234
+ if (loginOptions?.fallbackToMock || !resolvedGatewayUrl) {
235
+ useMockFallback = true;
236
+ if (resolvedGatewayUrl && loginOptions?.fallbackToMock) {
237
+ console.warn('[InMemoryAuth] Continuing with mock token — gateway token fetch was skipped');
238
+ }
239
+ selectUser(user, userId);
240
+ notifySubscribers();
241
+ return;
242
+ }
243
+ useMockFallback = false;
244
+ tokenCache = await fetchGatewayToken(resolvedGatewayUrl, user, { timeoutMs: tokenTimeout });
245
+ selectUser(user, userId);
205
246
  notifySubscribers();
206
247
  },
207
- createCustomUser(userData) {
248
+ async createCustomUser(userData) {
208
249
  const id = `custom-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
209
250
  const newUser = { ...userData, id };
210
- // Save to storage
211
251
  const storage = loadStorage();
212
252
  storage.customUsers.push(newUser);
213
253
  saveStorage(storage);
214
- // Login as the new user
215
- client.login(id);
254
+ await client.login(id);
216
255
  return newUser;
217
256
  },
218
257
  isCustomUser(userId) {
219
- // Custom users have IDs starting with 'custom-'
220
258
  return userId.startsWith('custom-');
221
259
  },
222
260
  deleteCustomUser(userId) {
223
- // Cannot delete predefined users
224
261
  if (!client.isCustomUser(userId)) {
225
262
  return false;
226
263
  }
@@ -229,12 +266,12 @@ export function createInMemoryAuthClient(options) {
229
266
  if (index === -1) {
230
267
  return false;
231
268
  }
232
- // Remove from storage
233
269
  storage.customUsers.splice(index, 1);
234
- // If deleted user was logged in, logout
235
270
  if (storage.selectedUserId === userId) {
236
271
  storage.selectedUserId = null;
237
272
  selectedUser = null;
273
+ tokenCache = null;
274
+ useMockFallback = false;
238
275
  }
239
276
  saveStorage(storage);
240
277
  notifySubscribers();
@@ -4,4 +4,3 @@
4
4
  export type { AuthClient, InMemoryAuthClient, MockUser, AuthState, AuthStateCallback, } from './interface';
5
5
  export { createInMemoryAuthClient, clearInMemoryAuth, createMockUsersFromRoles, type InMemoryAuthClientOptions, type MockUserRoles, } from './in-memory';
6
6
  export { createKeycloakAuthClient, type KeycloakAuthClientOptions, } from './keycloak';
7
- //# sourceMappingURL=index.d.ts.map
@@ -91,6 +91,8 @@ export interface AuthClient {
91
91
  */
92
92
  export interface InMemoryAuthClient extends AuthClient {
93
93
  readonly type: 'in-memory';
94
+ /** Resolved gateway URL, or null if BFF integration is disabled */
95
+ readonly gatewayUrl: string | null;
94
96
  /**
95
97
  * Get all available mock users
96
98
  */
@@ -102,14 +104,15 @@ export interface InMemoryAuthClient extends AuthClient {
102
104
  /**
103
105
  * Login as a specific user
104
106
  */
105
- login(userId: string): void;
107
+ login(userId: string, options?: {
108
+ fallbackToMock?: boolean;
109
+ }): Promise<void>;
106
110
  /**
107
111
  * Create and login as a custom user
108
112
  */
109
- createCustomUser(user: Omit<MockUser, 'id'>): MockUser;
113
+ createCustomUser(user: Omit<MockUser, 'id'>): Promise<MockUser>;
110
114
  /**
111
115
  * Delete a custom user (predefined users cannot be deleted)
112
116
  */
113
117
  deleteCustomUser(userId: string): boolean;
114
118
  }
115
- //# sourceMappingURL=interface.d.ts.map
@@ -16,4 +16,3 @@ export interface KeycloakAuthClientOptions {
16
16
  * Create a Keycloak auth client for production
17
17
  */
18
18
  export declare function createKeycloakAuthClient(options: KeycloakAuthClientOptions): AuthClient;
19
- //# sourceMappingURL=keycloak.d.ts.map
@@ -21,6 +21,7 @@ export function createKeycloakAuthClient(options) {
21
21
  id: tokenParsed.sub || '',
22
22
  email: tokenParsed.email || '',
23
23
  displayName: tokenParsed.name || tokenParsed.preferred_username || '',
24
+ roles: tokenParsed.realm_access?.roles ?? [],
24
25
  };
25
26
  }
26
27
  /**
@@ -101,10 +102,12 @@ export function createKeycloakAuthClient(options) {
101
102
  }
102
103
  },
103
104
  hasPermission(permission) {
104
- // If not initialized yet, return true to avoid hiding modules during init
105
- // The UI will re-render after authentication completes
105
+ // Deny-by-default: return false until Keycloak is fully initialized.
106
+ // This prevents a window where all actions appear permitted during auth init.
107
+ // AuthProvider gates rendering behind isAuthenticated, so modules won't
108
+ // render permission-gated UI until auth completes.
106
109
  if (!isInitialized || !keycloak) {
107
- return true;
110
+ return false;
108
111
  }
109
112
  return keycloak.hasRealmRole(permission);
110
113
  },
@@ -16,4 +16,3 @@ interface UserSelectorProps {
16
16
  */
17
17
  export declare function UserSelector({ authClient, onUserSelected }: UserSelectorProps): import("react/jsx-runtime").JSX.Element;
18
18
  export {};
19
- //# sourceMappingURL=UserSelector.d.ts.map
@@ -6,7 +6,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
6
6
  * Uses Brasa Design System tokens for consistent styling.
7
7
  */
8
8
  import { useState } from 'react';
9
- import { Crown, Ban, Eye, User, Users, Sparkles, Wrench, Trash2, Plus, ChevronRight } from '@nsxbet/admin-ui';
9
+ import { Crown, Ban, Eye, User, Users, Sparkles, Wrench, Trash2, Plus, ChevronRight, Loader2, AlertCircle, Clock, ArrowLeft, RefreshCw, ShieldAlert, } from '@nsxbet/admin-ui';
10
+ import { GatewayTimeoutError } from '../client/in-memory';
10
11
  /**
11
12
  * Get user icon based on roles
12
13
  */
@@ -84,26 +85,107 @@ function CustomUserForm({ onSubmit, onCancel, }) {
84
85
  const isValid = displayName.trim() && email.trim();
85
86
  return (_jsxs("form", { onSubmit: handleSubmit, className: "p-4 rounded-xl border border-border bg-card", children: [_jsxs("h3", { className: "font-semibold text-foreground mb-4 flex items-center gap-2", children: [_jsx(Sparkles, { className: "h-5 w-5 text-amber-500" }), "Create Custom User"] }), _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { children: [_jsx("label", { className: "block text-sm font-medium text-muted-foreground mb-1", children: "Display Name" }), _jsx("input", { type: "text", value: displayName, onChange: (e) => setDisplayName(e.target.value), placeholder: "John Doe", className: "w-full px-3 py-2 rounded-lg border border-border bg-background text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent" })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-sm font-medium text-muted-foreground mb-1", children: "Email" }), _jsx("input", { type: "email", value: email, onChange: (e) => setEmail(e.target.value), placeholder: "john@example.com", className: "w-full px-3 py-2 rounded-lg border border-border bg-background text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent" })] }), _jsxs("div", { children: [_jsxs("label", { className: "block text-sm font-medium text-muted-foreground mb-1", children: ["Roles ", _jsx("span", { className: "text-muted-foreground/60", children: "(comma-separated)" })] }), _jsx("input", { type: "text", value: rolesInput, onChange: (e) => setRolesInput(e.target.value), placeholder: "admin, admin.users.view, admin.tasks.edit", className: "w-full px-3 py-2 rounded-lg border border-border bg-background text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent" }), _jsxs("p", { className: "text-xs text-muted-foreground mt-1", children: ["Use ", _jsx("code", { className: "px-1 py-0.5 rounded bg-muted", children: "*" }), " for all permissions"] })] })] }), _jsxs("div", { className: "flex gap-2 mt-4", children: [_jsx("button", { type: "button", onClick: onCancel, className: "flex-1 px-4 py-2 rounded-lg border border-border text-foreground hover:bg-muted/50 transition-colors", children: "Cancel" }), _jsx("button", { type: "submit", disabled: !isValid, className: "flex-1 px-4 py-2 rounded-lg bg-primary text-primary-foreground font-medium hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors", children: "Create & Login" })] })] }));
86
87
  }
88
+ /**
89
+ * Loading screen shown while fetching a BFF token.
90
+ */
91
+ function LoginLoadingScreen({ userName }) {
92
+ return (_jsx("div", { className: "min-h-screen bg-background flex items-center justify-center p-4", children: _jsxs("div", { className: "w-full max-w-md text-center", children: [_jsx("div", { className: "inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary/10 mb-6", children: _jsx(Loader2, { className: "h-8 w-8 text-primary animate-spin" }) }), _jsx("h2", { className: "text-xl font-semibold text-foreground mb-2", children: "Authenticating\u2026" }), _jsxs("p", { className: "text-muted-foreground", children: ["Fetching token for ", _jsx("span", { className: "font-medium text-foreground", children: userName })] })] }) }));
93
+ }
94
+ /**
95
+ * Shared action bar for error / timeout screens.
96
+ */
97
+ function LoginRecoveryActions({ onRetry, onFallback, onBack, retrying, }) {
98
+ return (_jsxs("div", { className: "space-y-3 mt-6 w-full", children: [_jsxs("button", { onClick: onRetry, disabled: retrying, className: "w-full px-4 py-2.5 rounded-lg bg-primary text-primary-foreground font-medium hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center justify-center gap-2", children: [retrying ? _jsx(Loader2, { className: "h-4 w-4 animate-spin" }) : _jsx(RefreshCw, { className: "h-4 w-4" }), "Retry"] }), _jsxs("button", { onClick: onFallback, disabled: retrying, className: "w-full px-4 py-2.5 rounded-lg border border-border text-foreground hover:bg-muted/50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center justify-center gap-2", children: [_jsx(ShieldAlert, { className: "h-4 w-4" }), "Continue with mock token"] }), _jsxs("button", { onClick: onBack, disabled: retrying, className: "w-full px-4 py-2 text-sm text-muted-foreground hover:text-foreground disabled:opacity-50 transition-colors flex items-center justify-center gap-1", children: [_jsx(ArrowLeft, { className: "h-3.5 w-3.5" }), "Back to user selection"] })] }));
99
+ }
100
+ /**
101
+ * Error screen shown when the gateway returns an error or is unreachable.
102
+ */
103
+ function LoginErrorScreen({ errorMessage, gatewayUrl, onRetry, onFallback, onBack, retrying, }) {
104
+ return (_jsx("div", { className: "min-h-screen bg-background flex items-center justify-center p-4", children: _jsxs("div", { className: "w-full max-w-md flex flex-col items-center text-center", children: [_jsx("div", { className: "inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-destructive/10 mb-6", children: _jsx(AlertCircle, { className: "h-8 w-8 text-destructive" }) }), _jsx("h2", { className: "text-xl font-semibold text-foreground mb-2", children: "Authentication Failed" }), _jsx("p", { className: "text-muted-foreground mb-1", children: errorMessage }), gatewayUrl && (_jsx("p", { className: "text-xs text-muted-foreground/70 font-mono break-all", children: gatewayUrl })), _jsx(LoginRecoveryActions, { onRetry: onRetry, onFallback: onFallback, onBack: onBack, retrying: retrying })] }) }));
105
+ }
106
+ /**
107
+ * Timeout screen shown when the gateway doesn't respond in time.
108
+ */
109
+ function LoginTimeoutScreen({ gatewayUrl, onRetry, onFallback, onBack, retrying, }) {
110
+ return (_jsx("div", { className: "min-h-screen bg-background flex items-center justify-center p-4", children: _jsxs("div", { className: "w-full max-w-md flex flex-col items-center text-center", children: [_jsx("div", { className: "inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-amber-500/10 mb-6", children: _jsx(Clock, { className: "h-8 w-8 text-amber-500" }) }), _jsx("h2", { className: "text-xl font-semibold text-foreground mb-2", children: "Gateway Timeout" }), _jsx("p", { className: "text-muted-foreground mb-1", children: "The gateway did not respond in time." }), gatewayUrl && (_jsx("p", { className: "text-xs text-muted-foreground/70 font-mono break-all", children: gatewayUrl })), _jsx(LoginRecoveryActions, { onRetry: onRetry, onFallback: onFallback, onBack: onBack, retrying: retrying })] }) }));
111
+ }
87
112
  /**
88
113
  * Main User Selector Component
89
114
  */
90
115
  export function UserSelector({ authClient, onUserSelected }) {
91
116
  const [showCustomForm, setShowCustomForm] = useState(false);
117
+ const [loginState, setLoginState] = useState({ status: 'idle' });
92
118
  const [, forceUpdate] = useState(0);
93
119
  const users = authClient.getAvailableUsers();
94
- const handleSelectUser = (userId) => {
95
- authClient.login(userId);
96
- onUserSelected?.();
120
+ const findUserName = (userId) => users.find((u) => u.id === userId)?.displayName ?? userId;
121
+ const handleSelectUser = async (userId) => {
122
+ if (!authClient.gatewayUrl) {
123
+ await authClient.login(userId);
124
+ onUserSelected?.();
125
+ return;
126
+ }
127
+ setLoginState({ status: 'loading', userId });
128
+ try {
129
+ await authClient.login(userId);
130
+ onUserSelected?.();
131
+ }
132
+ catch (error) {
133
+ if (error instanceof GatewayTimeoutError) {
134
+ setLoginState({ status: 'timeout', userId });
135
+ }
136
+ else {
137
+ setLoginState({
138
+ status: 'error',
139
+ userId,
140
+ error: error instanceof Error ? error.message : 'An unexpected error occurred',
141
+ canRetry: true,
142
+ });
143
+ }
144
+ }
145
+ };
146
+ const handleRetry = () => {
147
+ if (loginState.status === 'error' || loginState.status === 'timeout') {
148
+ handleSelectUser(loginState.userId);
149
+ }
97
150
  };
98
- const handleCreateCustomUser = (userData) => {
99
- authClient.createCustomUser(userData);
151
+ const handleFallbackToMock = async () => {
152
+ if (loginState.status === 'error' || loginState.status === 'timeout') {
153
+ const { userId } = loginState;
154
+ setLoginState({ status: 'loading', userId });
155
+ try {
156
+ await authClient.login(userId, { fallbackToMock: true });
157
+ onUserSelected?.();
158
+ }
159
+ catch (error) {
160
+ setLoginState({
161
+ status: 'error',
162
+ userId,
163
+ error: error instanceof Error ? error.message : 'An unexpected error occurred',
164
+ canRetry: true,
165
+ });
166
+ }
167
+ }
168
+ };
169
+ const handleBack = () => {
170
+ setLoginState({ status: 'idle' });
171
+ };
172
+ const handleCreateCustomUser = async (userData) => {
173
+ await authClient.createCustomUser(userData);
100
174
  onUserSelected?.();
101
175
  };
102
176
  const handleDeleteUser = (userId) => {
103
177
  authClient.deleteCustomUser(userId);
104
- // Force re-render to update the list
105
178
  forceUpdate((n) => n + 1);
106
179
  };
180
+ if (loginState.status === 'loading') {
181
+ return _jsx(LoginLoadingScreen, { userName: findUserName(loginState.userId) });
182
+ }
183
+ if (loginState.status === 'error') {
184
+ return (_jsx(LoginErrorScreen, { errorMessage: loginState.error, gatewayUrl: authClient.gatewayUrl, onRetry: handleRetry, onFallback: handleFallbackToMock, onBack: handleBack, retrying: false }));
185
+ }
186
+ if (loginState.status === 'timeout') {
187
+ return (_jsx(LoginTimeoutScreen, { gatewayUrl: authClient.gatewayUrl, onRetry: handleRetry, onFallback: handleFallbackToMock, onBack: handleBack, retrying: false }));
188
+ }
107
189
  return (_jsx("div", { className: "min-h-screen bg-background flex items-center justify-center p-4", children: _jsxs("div", { className: "w-full max-w-md", children: [_jsxs("div", { className: "text-center mb-8", children: [_jsx("div", { className: "inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary mb-4 shadow-lg shadow-primary/25", children: _jsx(Users, { className: "h-8 w-8 text-primary-foreground" }) }), _jsx("h1", { className: "text-2xl font-bold text-foreground mb-2", children: "Select User" }), _jsx("p", { className: "text-muted-foreground", children: "Choose a mock user to continue in development mode" })] }), _jsx("div", { className: "space-y-3 mb-6", children: users.map((user) => {
108
190
  const isCustom = authClient.isCustomUser(user.id);
109
191
  return (_jsx(UserCard, { user: user, isCustom: isCustom, onClick: () => handleSelectUser(user.id), onDelete: isCustom ? () => handleDeleteUser(user.id) : undefined }, user.id));
@@ -2,4 +2,3 @@
2
2
  * Auth components exports
3
3
  */
4
4
  export { UserSelector } from './UserSelector';
5
- //# sourceMappingURL=index.d.ts.map
@@ -4,4 +4,3 @@
4
4
  export type { AuthClient, InMemoryAuthClient, MockUser, AuthState, AuthStateCallback, } from './client';
5
5
  export { createInMemoryAuthClient, clearInMemoryAuth, createMockUsersFromRoles, createKeycloakAuthClient, type InMemoryAuthClientOptions, type MockUserRoles, type KeycloakAuthClientOptions, } from './client';
6
6
  export { UserSelector } from './components';
7
- //# sourceMappingURL=index.d.ts.map
@@ -45,4 +45,3 @@ export declare function useAuthContext(): AuthContextValue;
45
45
  */
46
46
  export declare function useOptionalAuthContext(): AuthContextValue | null;
47
47
  export {};
48
- //# sourceMappingURL=AuthProvider.d.ts.map
@@ -0,0 +1,7 @@
1
+ import type { TimestampFormat } from '../types/platform';
2
+ export interface TimestampProps {
3
+ value: Date | string;
4
+ format?: TimestampFormat;
5
+ className?: string;
6
+ }
7
+ export declare function Timestamp({ value, format, className }: TimestampProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,50 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo } from 'react';
3
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@nsxbet/admin-ui';
4
+ import { useTimestamp } from '../hooks/useTimestamp';
5
+ export function Timestamp({ value, format = 'datetime', className }) {
6
+ const { mode, formatDate } = useTimestamp();
7
+ const date = useMemo(() => {
8
+ if (value instanceof Date)
9
+ return value;
10
+ const parsed = new Date(value);
11
+ return parsed;
12
+ }, [value]);
13
+ const isInvalid = isNaN(date.getTime());
14
+ const formatted = useMemo(() => {
15
+ if (isInvalid)
16
+ return 'Invalid date';
17
+ return formatDate(date, format);
18
+ }, [date, format, formatDate, isInvalid]);
19
+ const tooltipText = useMemo(() => {
20
+ if (isInvalid)
21
+ return '';
22
+ const oppositeMode = mode === 'utc' ? 'local' : 'utc';
23
+ const oppositeTz = oppositeMode === 'utc' ? 'UTC' : undefined;
24
+ const suffix = oppositeMode === 'utc' ? ' UTC' : '';
25
+ if (format === 'relative') {
26
+ const options = {
27
+ year: 'numeric',
28
+ month: 'short',
29
+ day: 'numeric',
30
+ hour: 'numeric',
31
+ minute: '2-digit',
32
+ second: '2-digit',
33
+ timeZone: oppositeTz,
34
+ };
35
+ return new Intl.DateTimeFormat('en-US', options).format(date) + suffix;
36
+ }
37
+ const options = {
38
+ year: 'numeric',
39
+ month: 'short',
40
+ day: 'numeric',
41
+ hour: 'numeric',
42
+ minute: '2-digit',
43
+ second: '2-digit',
44
+ timeZone: oppositeTz,
45
+ };
46
+ return new Intl.DateTimeFormat('en-US', options).format(date) + suffix;
47
+ }, [date, mode, format, isInvalid]);
48
+ const isoString = isInvalid ? '' : date.toISOString();
49
+ return (_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("time", { dateTime: isoString, className: className, children: formatted }) }), !isInvalid && (_jsx(TooltipContent, { children: _jsx("p", { children: tooltipText }) }))] }) }));
50
+ }
@@ -18,4 +18,3 @@ export interface UseAuthResult {
18
18
  * - Standalone mode with AuthClient (Keycloak or in-memory)
19
19
  */
20
20
  export declare function useAuth(): UseAuthResult;
21
- //# sourceMappingURL=useAuth.d.ts.map
@@ -25,7 +25,7 @@ export function useAuth() {
25
25
  return {
26
26
  getAccessToken: authContext.getAccessToken,
27
27
  hasPermission: authContext.hasPermission,
28
- getUser: () => authContext.user || { id: '', email: '', displayName: '' },
28
+ getUser: () => authContext.user || { id: '', email: '', displayName: '', roles: [] },
29
29
  logout: authContext.logout,
30
30
  };
31
31
  }
@@ -5,4 +5,3 @@
5
5
  * In standalone mode: Manually adds Authorization header with token
6
6
  */
7
7
  export declare function useFetch(): (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
8
- //# sourceMappingURL=useFetch.d.ts.map
@@ -43,4 +43,3 @@ export interface UseI18nResult {
43
43
  * ```
44
44
  */
45
45
  export declare function useI18n(namespace?: string): UseI18nResult;
46
- //# sourceMappingURL=useI18n.d.ts.map
@@ -9,4 +9,3 @@ export declare function usePlatformAPI(): {
9
9
  isShellMode: boolean;
10
10
  api: PlatformAPI | undefined;
11
11
  };
12
- //# sourceMappingURL=usePlatformAPI.d.ts.map