@kumori/aurora-backend-handler 1.0.100 → 1.0.111

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.
@@ -1,13 +1,14 @@
1
1
  import { v7 as uuidv7 } from "uuid";
2
2
  import { environment } from "./environment";
3
+ import { AccountStatus } from "@kumori/aurora-interfaces/enums/accountStatusEnum";
3
4
  import {
4
5
  Account,
5
6
  Channel,
6
7
  ClusterToken,
7
8
  Environment,
9
+ errors,
8
10
  Instance,
9
- Link,
10
- MarketplaceService,
11
+ MarketplaceItem,
11
12
  Notification,
12
13
  Organization,
13
14
  Plan,
@@ -17,11 +18,10 @@ import {
17
18
  Resource,
18
19
  Service,
19
20
  Tenant,
20
- tenantRole,
21
21
  Token,
22
- Usage,
23
22
  User,
24
23
  UserData,
24
+ UserStatus
25
25
  } from "@kumori/aurora-interfaces";
26
26
  import { loadMarketplaceItemsForTenant } from "./api/marketplace-api-service";
27
27
  import { getReporting } from "./api/reporting-api-service";
@@ -31,8 +31,8 @@ import {
31
31
  handleTenantOperationSuccess,
32
32
  } from "./helpers/tenant-helper";
33
33
  import {
34
+ handleUserDelete,
34
35
  handleUserEvent,
35
- handleUserOperationError,
36
36
  } from "./helpers/user-helper";
37
37
  import {
38
38
  handleEnvironmentEvent,
@@ -63,6 +63,7 @@ import {
63
63
  handleSecretEvent,
64
64
  handleVolumeEvent,
65
65
  processResourceResult,
66
+ setResourceDeleting,
66
67
  } from "./helpers/resource-helper";
67
68
  import {
68
69
  handlePlanInstanceEvent,
@@ -70,6 +71,7 @@ import {
70
71
  } from "./helpers/plan-helper";
71
72
  import { handleRegistryEvent } from "./helpers/registry-helper";
72
73
  import {
74
+ handleTokenDeletion,
73
75
  handleTokenEvent,
74
76
  handleTokenOperationSuccess,
75
77
  } from "./helpers/token-helper";
@@ -77,6 +79,7 @@ import { handleLinkEvent } from "./helpers/link-helper";
77
79
  import { eventHelper } from "./backend-handler";
78
80
  import { Revision } from "@kumori/aurora-interfaces/interfaces/revision-interface";
79
81
  import { requestRevisionData } from "./api/service-api-service";
82
+ import { processMarketplaceSchemaResponse } from "./helpers/marketplace-helper";
80
83
 
81
84
  let WebSocketClass: any;
82
85
 
@@ -145,10 +148,12 @@ interface Role {
145
148
  */
146
149
  let wsConnection: WebSocket | null = null;
147
150
  let pendingRequests = new Map<string, PendingOperation>();
151
+ let purgingEnvironments = new Set<string>();
148
152
  let currentToken: string | null = null;
149
153
  let connectionPromise: Promise<WebSocket> | null = null;
150
154
  let userData: UserData = {
151
155
  id: "",
156
+ created: "",
152
157
  name: "",
153
158
  surname: "",
154
159
  provider: [],
@@ -171,6 +176,7 @@ let accountsMap = new Map<string, Account>();
171
176
  let revisionsMap = new Map<string, Revision>();
172
177
  let roleMap = new Map<string, Role[]>();
173
178
  let tokenMap = new Map<string, Token>();
179
+ let deletedTokenIds = new Set<string>();
174
180
  let platformInfo: Platform | null = null;
175
181
  let isPlatformInfoLoading = false;
176
182
  let isPlatformInfoLoaded = false;
@@ -192,6 +198,19 @@ const REPORTING_ITERATIONS = 5;
192
198
  const REPORTING_INTERVAL = 1000;
193
199
  let hasLoadedReportingOnce = false;
194
200
  let recentlyUpdatedServices = new Map<string, Service>();
201
+ let pendingTenantStatus = new Map<string, string>();
202
+ let reconnectAttempts = 0;
203
+ let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
204
+ let isManualClose = false;
205
+ let lastConnectionParams: {
206
+ token: string;
207
+ type?: string;
208
+ name?: string;
209
+ previousData?: UserData;
210
+ } | null = null;
211
+ const MAX_RECONNECT_ATTEMPTS = 5;
212
+ const RECONNECT_BASE_DELAY = 1000;
213
+ const RECONNECT_MAX_DELAY = 30000;
195
214
  /**
196
215
  * Helper function to safely stringify error objects
197
216
  */
@@ -234,6 +253,7 @@ export const initializeGlobalWebSocketClient = async (
234
253
  ): Promise<WebSocket> => {
235
254
  userData.notifications = previousData?.notifications;
236
255
  globalEntityName = name || null;
256
+ lastConnectionParams = { token, type, name, previousData };
237
257
  if (
238
258
  wsConnection &&
239
259
  wsConnection.readyState === WebSocket.OPEN &&
@@ -265,8 +285,9 @@ export const initializeGlobalWebSocketClient = async (
265
285
  if (typeof window === "undefined") {
266
286
  wsConnection = new WebSocketClass(wsUrl, {
267
287
  headers: {
268
- authorization: `Bearer ${token}`,
288
+ // authorization: `Bearer ${token}`,
269
289
  },
290
+ credentials: "include",
270
291
  });
271
292
  } else {
272
293
  wsConnection = new WebSocketClass(wsUrl);
@@ -275,6 +296,11 @@ export const initializeGlobalWebSocketClient = async (
275
296
  wsConnection?.addEventListener("open", async () => {
276
297
  currentToken = token;
277
298
  connectionPromise = null;
299
+ reconnectAttempts = 0;
300
+ if (reconnectTimer) {
301
+ clearTimeout(reconnectTimer);
302
+ reconnectTimer = null;
303
+ }
278
304
 
279
305
  resolve(wsConnection!);
280
306
  });
@@ -346,7 +372,7 @@ export const initializeGlobalWebSocketClient = async (
346
372
  message.error?.code === "_unknown_user_" &&
347
373
  message.error?.content !== "Unknown proposed user."
348
374
  ) {
349
- userData.status = "unknown";
375
+ userData.status = UserStatus.UNKNOWN;
350
376
  user = new User(userData);
351
377
  eventHelper.user.publish.loaded(user);
352
378
  }
@@ -368,13 +394,17 @@ export const initializeGlobalWebSocketClient = async (
368
394
  pending.reject(new Error("WebSocket connection closed"));
369
395
  pendingRequests.delete(messageId);
370
396
  });
397
+
398
+ if (isManualClose) {
399
+ isManualClose = false;
400
+ return;
401
+ }
402
+
403
+ scheduleReconnect();
371
404
  });
372
405
 
373
406
  wsConnection?.addEventListener("error", (error) => {
374
407
  console.error("Global WebSocket error:", error);
375
- userData.status = "notlogged";
376
- user = new User(userData);
377
- eventHelper.user.publish.loaded(user);
378
408
  connectionPromise = null;
379
409
  reject(error);
380
410
  });
@@ -387,6 +417,38 @@ export const initializeGlobalWebSocketClient = async (
387
417
  return connectionPromise;
388
418
  };
389
419
 
420
+ /**
421
+ * Schedule a reconnection attempt after an unexpected connection drop
422
+ * (e.g. a transient network blip while the tab was backgrounded).
423
+ * Uses exponential backoff and gives up after MAX_RECONNECT_ATTEMPTS.
424
+ */
425
+ const scheduleReconnect = () => {
426
+ if (reconnectTimer || !lastConnectionParams) {
427
+ return;
428
+ }
429
+
430
+ if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
431
+ console.error("WebSocket reconnection attempts exhausted");
432
+ return;
433
+ }
434
+
435
+ const delay = Math.min(
436
+ RECONNECT_BASE_DELAY * Math.pow(2, reconnectAttempts),
437
+ RECONNECT_MAX_DELAY,
438
+ );
439
+ reconnectAttempts++;
440
+
441
+ reconnectTimer = setTimeout(() => {
442
+ reconnectTimer = null;
443
+ const { token, type, name, previousData } = lastConnectionParams!;
444
+ initializeGlobalWebSocketClient(token, type, name, previousData).catch(
445
+ (error) => {
446
+ console.error("WebSocket reconnect attempt failed:", error);
447
+ },
448
+ );
449
+ }, delay);
450
+ };
451
+
390
452
  /**
391
453
  * Function to make WebSocket requests using the global connection
392
454
  * @param topic Request topic (group:method)
@@ -426,6 +488,14 @@ export const makeGlobalWebSocketRequest = async (
426
488
  payload,
427
489
  };
428
490
 
491
+ let purgeEnvKey: string | null = null;
492
+ if (petitionAction === "CLEAN" && entityType === "environment" && originalData) {
493
+ purgeEnvKey = originalData.tenant && originalData.account
494
+ ? `${originalData.tenant}/${originalData.account}/${petitionInfo}`
495
+ : petitionInfo;
496
+ purgingEnvironments.add(purgeEnvKey);
497
+ }
498
+
429
499
  return new Promise((resolve, reject) => {
430
500
  const timeoutId = setTimeout(() => {
431
501
  const pending = pendingRequests.get(messageId);
@@ -441,16 +511,23 @@ export const makeGlobalWebSocketRequest = async (
441
511
  new Error(`Request timeout for ${topic}`),
442
512
  );
443
513
  }
444
- reject(new Error(`Request timeout for ${topic}`));
514
+ if (purgeEnvKey) purgingEnvironments.delete(purgeEnvKey);
515
+ const timeoutError = Object.assign(
516
+ new Error(`Request timeout for ${topic}`),
517
+ { isTimeout: true },
518
+ );
519
+ reject(timeoutError);
445
520
  }, timeout);
446
521
 
447
522
  const operation: PendingOperation = {
448
523
  resolve: (response: any) => {
449
524
  clearTimeout(timeoutId);
525
+ if (purgeEnvKey) purgingEnvironments.delete(purgeEnvKey);
450
526
  resolve(response.payload || response);
451
527
  },
452
528
  reject: (error: any) => {
453
529
  clearTimeout(timeoutId);
530
+ if (purgeEnvKey) purgingEnvironments.delete(purgeEnvKey);
454
531
  const errorMessage = safeStringifyError(error);
455
532
  console.error(`WebSocket request failed for ${topic}:`, {
456
533
  originalError: error,
@@ -527,6 +604,23 @@ export const makeGlobalWebSocketRequestWithRetry = async (
527
604
  );
528
605
  };
529
606
 
607
+ export const markEnvironmentAsPurging = (envKey: string) => {
608
+ purgingEnvironments.add(envKey);
609
+ const env = environmentsMap.get(envKey);
610
+ if (env) {
611
+ const updatedEnv: Environment = {
612
+ ...env,
613
+ status: {
614
+ code: "PURGING",
615
+ message: `Environment ${env.name} is being purged.`,
616
+ timestamp: new Date().toISOString(),
617
+ },
618
+ };
619
+ environmentsMap.set(envKey, updatedEnv);
620
+ eventHelper.environment.publish.updated(updatedEnv);
621
+ }
622
+ };
623
+
530
624
  /**
531
625
  * Get the current WebSocket connection status
532
626
  * @returns Connection status and token info
@@ -545,6 +639,14 @@ export const getWebSocketStatus = () => {
545
639
  * Close the global WebSocket connection
546
640
  */
547
641
  export const closeGlobalWebSocketConnection = () => {
642
+ isManualClose = true;
643
+ lastConnectionParams = null;
644
+ reconnectAttempts = 0;
645
+ if (reconnectTimer) {
646
+ clearTimeout(reconnectTimer);
647
+ reconnectTimer = null;
648
+ }
649
+
548
650
  if (wsConnection) {
549
651
  wsConnection.close();
550
652
  wsConnection = null;
@@ -562,13 +664,24 @@ export const closeGlobalWebSocketConnection = () => {
562
664
  });
563
665
  };
564
666
 
667
+ const computePromotionInfo = (
668
+ tenants: Tenant[],
669
+ ): { completedSteps: number; remainingDays: number } => {
670
+ const hasValidCredentialsAccount = tenants.some((tenant) =>
671
+ tenant.accounts?.some((account) => account.status === AccountStatus.ACTIVE),
672
+ );
673
+ const hasDeployedService = tenants.some((tenant) => tenant.services?.length > 0);
674
+ const completedSteps = (hasValidCredentialsAccount ? 1 : 0) + (hasDeployedService ? 1 : 0);
675
+ return { completedSteps, remainingDays: 7 };
676
+ };
677
+
565
678
  const rebuildHierarchy = () => {
566
679
  const preservedUserData = { ...userData };
567
680
 
568
681
  tenantsMap.forEach((tenant) => {
569
682
  tenant.accounts = Array.from(accountsMap.values()).filter((account) => {
570
683
  account.environments = Array.from(environmentsMap.values())
571
- .filter((env) => env.account === account.id)
684
+ .filter((env) => env.account === account.id && env.tenant === account.tenant)
572
685
  .map((env) => env.name);
573
686
  return account.tenant === tenant.id;
574
687
  });
@@ -594,16 +707,20 @@ const rebuildHierarchy = () => {
594
707
  }
595
708
  }
596
709
  });
597
-
598
710
  userData = {
599
711
  id: preservedUserData.id,
712
+ created: preservedUserData.created,
600
713
  name: preservedUserData.name,
601
714
  surname: preservedUserData.surname,
602
715
  provider: preservedUserData.provider.map((p) => ({ ...p })),
603
716
  notificationsEnabled: preservedUserData.notificationsEnabled,
604
717
  organizations: Array.from(organizationsMap.values()),
605
718
  clusterTokens: Array.from(clusterTokensMap.values()),
606
- tokens: preservedUserData.tokens || [],
719
+ tokens: (
720
+ tokenMap.size > 0
721
+ ? (preservedUserData.tokens || []).filter(t => tokenMap.has(t.name))
722
+ : (preservedUserData.tokens || [])
723
+ ).filter(t => !deletedTokenIds.has(t.name)),
607
724
  tenants: Array.from(tenantsMap.values()),
608
725
  axebowPlan: preservedUserData.axebowPlan,
609
726
  companyName: preservedUserData.companyName,
@@ -613,6 +730,7 @@ const rebuildHierarchy = () => {
613
730
  notifications: preservedUserData.notifications || [],
614
731
  plans: preservedUserData.plans || [],
615
732
  discoveryMethod: preservedUserData.discoveryMethod,
733
+ promotionInfo: computePromotionInfo(Array.from(tenantsMap.values())),
616
734
  };
617
735
  };
618
736
  const handleEvent = async (message: WSMessage) => {
@@ -630,6 +748,7 @@ const handleEvent = async (message: WSMessage) => {
630
748
  : {};
631
749
  switch (kind) {
632
750
  case "user":
751
+ const previousTokens = userData.tokens || [];
633
752
  const userEventResult = handleUserEvent({
634
753
  eventData,
635
754
  tenantsMap,
@@ -639,7 +758,22 @@ const handleEvent = async (message: WSMessage) => {
639
758
  parseKeyPath,
640
759
  eventHelper,
641
760
  });
761
+ Object.entries(eventData.status.tenants).forEach(
762
+ ([tenantId, tenantInfo]: [string, any]) => {
763
+ if (tenantInfo.status === "rejected") {
764
+ const tenant = tenantsMap.get(tenantId);
765
+ if (tenant) {
766
+ tenantsMap.set(tenantId, { ...tenant, status: "leaving" });
767
+ } else {
768
+ pendingTenantStatus.set(tenantId, "leaving");
769
+ }
770
+ }
771
+ },
772
+ );
642
773
  userData = userEventResult.userData;
774
+ if (!userData.tokens?.length && previousTokens.length) {
775
+ userData.tokens = previousTokens;
776
+ }
643
777
  tempProviders = userEventResult.tempProviders;
644
778
  userEventResult.deletedTenantKeys.forEach((key) => {
645
779
  tenantsMap.delete(key);
@@ -660,7 +794,19 @@ const handleEvent = async (message: WSMessage) => {
660
794
  envEventResult.pendingCloudProviderUpdate,
661
795
  );
662
796
  }
663
- environmentsMap.set(entityId, envEventResult.environment);
797
+ const envSetKey = parentParts.tenant && parentParts.account ? `${parentParts.tenant}/${parentParts.account}/${entityId}` : entityId;
798
+ let envToStore = envEventResult.environment;
799
+ if (envToStore.status?.code?.startsWith("DELETING") && purgingEnvironments.has(envSetKey)) {
800
+ envToStore = {
801
+ ...envToStore,
802
+ status: {
803
+ code: "PURGING",
804
+ message: `Environment ${entityId} is being purged.`,
805
+ timestamp: new Date().toISOString(),
806
+ },
807
+ };
808
+ }
809
+ environmentsMap.set(envSetKey, envToStore);
664
810
  const envTenant = tenantsMap.get(envEventResult.tenantId);
665
811
  if (!envTenant) {
666
812
  pendingEnvironments.push({
@@ -694,12 +840,16 @@ const handleEvent = async (message: WSMessage) => {
694
840
  }
695
841
  if (revisionResult.updatedEnvironment) {
696
842
  const envId = revisionResult.updatedService?.environment;
697
- if (envId)
698
- environmentsMap.set(envId, revisionResult.updatedEnvironment);
843
+ const envRevTenant = revisionResult.updatedEnvironment.tenant;
844
+ const envRevAccount = revisionResult.updatedEnvironment.account;
845
+ const envRevKey = envRevTenant && envRevAccount && envId ? `${envRevTenant}/${envRevAccount}/${envId}` : envId;
846
+ if (envRevKey) environmentsMap.set(envRevKey, revisionResult.updatedEnvironment);
699
847
  }
700
848
  if (revisionResult.updatedAccount) {
701
849
  const accId = revisionResult.updatedService?.account;
702
- if (accId) accountsMap.set(accId, revisionResult.updatedAccount);
850
+ const accRevTenant = revisionResult.updatedAccount.tenant;
851
+ const accRevKey = accRevTenant && accId ? `${accRevTenant}/${accId}` : accId;
852
+ if (accRevKey) accountsMap.set(accRevKey, revisionResult.updatedAccount);
703
853
  }
704
854
  if (revisionResult.pendingRevisionError) {
705
855
  pendingRevisionErrors.push(revisionResult.pendingRevisionError);
@@ -792,8 +942,9 @@ const handleEvent = async (message: WSMessage) => {
792
942
  });
793
943
 
794
944
  if (!message.payload.deleted) {
795
- accountsMap.set(entityId, accountResult.account);
796
- updateEnvironmentsCloudProvider(entityId, eventData.spec.api);
945
+ const accountKey = parentParts.tenant ? `${parentParts.tenant}/${entityId}` : entityId;
946
+ accountsMap.set(accountKey, accountResult.account);
947
+ updateEnvironmentsCloudProvider(entityId, eventData.spec.api, parentParts.tenant);
797
948
  }
798
949
  break;
799
950
 
@@ -804,7 +955,6 @@ const handleEvent = async (message: WSMessage) => {
804
955
  eventData,
805
956
  userData,
806
957
  );
807
-
808
958
  if (
809
959
  existingTenant &&
810
960
  existingTenant.role &&
@@ -813,7 +963,7 @@ const handleEvent = async (message: WSMessage) => {
813
963
  ) {
814
964
  const roleNotification: Notification = {
815
965
  type: "info",
816
- subtype: "tenant-role-changed",
966
+ subtype: errors.tenant.roleChanged.subtype,
817
967
  date: Date.now().toString(),
818
968
  status: "unread",
819
969
  callToAction: false,
@@ -825,6 +975,25 @@ const handleEvent = async (message: WSMessage) => {
825
975
  };
826
976
  eventHelper.notification.publish.creation(roleNotification);
827
977
  }
978
+ existingTenant?.users
979
+ ?.filter((u) => u.status === "pending" && u.id !== userData.id)
980
+ .forEach((previousUser) => {
981
+ const currentUser = newTenant.users?.find((u) => u.id === previousUser.id);
982
+ if (currentUser?.status === "pending") return;
983
+ const invitationNotification: Notification = {
984
+ type: "info",
985
+ subtype: currentUser ? "tenant-invitation-accepted" : "tenant-invitation-declined",
986
+ date: Date.now().toString(),
987
+ status: "unread",
988
+ callToAction: false,
989
+ data: {
990
+ tenant: newTenant.name,
991
+ user: previousUser.id,
992
+ role: previousUser.role,
993
+ },
994
+ };
995
+ eventHelper.notification.publish.creation(invitationNotification);
996
+ });
828
997
  if (existingTenant) {
829
998
  if (
830
999
  !newTenant.resources?.length &&
@@ -850,6 +1019,12 @@ const handleEvent = async (message: WSMessage) => {
850
1019
  }
851
1020
 
852
1021
  tenantsMap.set(entityId, newTenant);
1022
+ if (pendingTenantStatus.has(entityId)) {
1023
+ const pendingStatus = pendingTenantStatus.get(entityId)!;
1024
+ tenantsMap.set(entityId, { ...newTenant, status: pendingStatus });
1025
+ pendingTenantStatus.delete(entityId);
1026
+ }
1027
+
853
1028
  loadMarketplaceItemsForTenant(entityId, "") //TODO Move to helper
854
1029
  .then((items) => {
855
1030
  const tenant = tenantsMap.get(entityId);
@@ -952,13 +1127,17 @@ const handleEvent = async (message: WSMessage) => {
952
1127
  break;
953
1128
 
954
1129
  case "ca":
955
- const caResult = handleCAEvent(eventData, parentParts, tenantsMap, hasRequestingServices,);
1130
+ const caResult = handleCAEvent(
1131
+ eventData,
1132
+ parentParts,
1133
+ tenantsMap,
1134
+ hasRequestingServices,
1135
+ );
956
1136
  processResourceResult(
957
1137
  caResult,
958
1138
  tenantsMap,
959
1139
  pendingDomains,
960
1140
  eventHelper,
961
-
962
1141
  );
963
1142
  break;
964
1143
 
@@ -1051,6 +1230,7 @@ const handleEvent = async (message: WSMessage) => {
1051
1230
  eventData,
1052
1231
  tokenMap,
1053
1232
  userData,
1233
+ deletedTokenIds,
1054
1234
  });
1055
1235
 
1056
1236
  if (tokenResult.isUserToken) {
@@ -1257,6 +1437,7 @@ const handleOperationSuccess = (operation: PendingOperation, response: any) => {
1257
1437
  const channelsInfo = mapChannelsFromApiData(
1258
1438
  response.payload.data,
1259
1439
  serviceName,
1440
+ service.resources,
1260
1441
  );
1261
1442
  service.serverChannels = channelsInfo.serverChannels;
1262
1443
  service.clientChannels = channelsInfo.clientChannels;
@@ -1273,7 +1454,7 @@ const handleOperationSuccess = (operation: PendingOperation, response: any) => {
1273
1454
  svcSuccessResult.revisionData
1274
1455
  ) {
1275
1456
  try {
1276
- const updatedService = processRevisionData(
1457
+ let updatedService = processRevisionData(
1277
1458
  svcSuccessResult.updatedService!,
1278
1459
  svcSuccessResult.revisionData,
1279
1460
  revisionsMap,
@@ -1293,6 +1474,28 @@ const handleOperationSuccess = (operation: PendingOperation, response: any) => {
1293
1474
  };
1294
1475
  }
1295
1476
 
1477
+ const resources = updatedService.resources || [];
1478
+ if (resources.length > 0) {
1479
+ const applyIsPublic = (channels: Channel[]): Channel[] =>
1480
+ channels.map((ch) => ({
1481
+ ...ch,
1482
+ isPublic:
1483
+ ch.isPublic ||
1484
+ resources.some(
1485
+ (r) =>
1486
+ (r.type === "port" || r.type === "domain") &&
1487
+ (r.name === `${ch.name}_port` || r.name === `${ch.name}_domain`),
1488
+ ),
1489
+ }));
1490
+
1491
+ updatedService = {
1492
+ ...updatedService,
1493
+ serverChannels: applyIsPublic(updatedService.serverChannels || []),
1494
+ clientChannels: applyIsPublic(updatedService.clientChannels || []),
1495
+ duplexChannels: applyIsPublic(updatedService.duplexChannels || []),
1496
+ };
1497
+ }
1498
+
1296
1499
  servicesMap.set(svcSuccessResult.serviceId, updatedService);
1297
1500
 
1298
1501
  if (updatedService.role && updatedService.role.length > 0) {
@@ -1347,16 +1550,18 @@ const handleOperationSuccess = (operation: PendingOperation, response: any) => {
1347
1550
  originalData: originalData as Account,
1348
1551
  });
1349
1552
 
1553
+ const accTenant = (originalData as Account)?.tenant;
1554
+ const accSuccessKey = accTenant ? `${accTenant}/${entityName}` : entityName;
1350
1555
  if (accSuccessResult.shouldDelete) {
1351
- const acc = accountsMap.get(entityName);
1556
+ const acc = accountsMap.get(accSuccessKey);
1352
1557
  if (acc) {
1353
- accountsMap.delete(entityName);
1558
+ accountsMap.delete(accSuccessKey);
1354
1559
  eventHelper.notification.publish.creation(
1355
1560
  accSuccessResult.notification,
1356
1561
  );
1357
1562
  }
1358
1563
  } else if (accSuccessResult.updatedAccount) {
1359
- accountsMap.set(entityName, accSuccessResult.updatedAccount);
1564
+ accountsMap.set(accSuccessKey, accSuccessResult.updatedAccount);
1360
1565
  if (accSuccessResult.eventType === "created") {
1361
1566
  eventHelper.account.publish.created(accSuccessResult.updatedAccount);
1362
1567
  } else if (accSuccessResult.eventType === "updated") {
@@ -1375,10 +1580,12 @@ const handleOperationSuccess = (operation: PendingOperation, response: any) => {
1375
1580
  originalData: originalData as Environment,
1376
1581
  });
1377
1582
 
1583
+ const envOrigData = originalData as Environment;
1584
+ const envSuccessKey = envOrigData?.tenant && envOrigData?.account ? `${envOrigData.tenant}/${envOrigData.account}/${entityName}` : entityName;
1378
1585
  if (envSuccessResult.shouldDelete) {
1379
- environmentsMap.delete(entityName);
1586
+ environmentsMap.delete(envSuccessKey);
1380
1587
  } else if (envSuccessResult.updatedEnvironment) {
1381
- environmentsMap.set(entityName, envSuccessResult.updatedEnvironment);
1588
+ environmentsMap.set(envSuccessKey, envSuccessResult.updatedEnvironment);
1382
1589
  if (envSuccessResult.notification) {
1383
1590
  eventHelper.notification.publish.creation(
1384
1591
  envSuccessResult.notification,
@@ -1406,15 +1613,44 @@ const handleOperationSuccess = (operation: PendingOperation, response: any) => {
1406
1613
  }
1407
1614
  break;
1408
1615
  case "token":
1409
- const tokenSuccessResult = handleTokenOperationSuccess({
1410
- responsePayload: response.payload,
1411
- });
1412
- tokenMap.set(tokenSuccessResult.token.name, tokenSuccessResult.token);
1616
+ if (action === "DELETE") {
1617
+ const deletedTokenId = entityName.split("/").pop();
1618
+ if (deletedTokenId) {
1619
+ const tokenDeletionResult = handleTokenDeletion({
1620
+ tokenId: deletedTokenId,
1621
+ tokenMap,
1622
+ userData,
1623
+ deletedTokenIds,
1624
+ });
1625
+ if (tokenDeletionResult.removed) {
1626
+ userData.tokens = tokenDeletionResult.updatedUserTokens;
1627
+ eventHelper.notification.publish.creation(
1628
+ tokenDeletionResult.notification!,
1629
+ );
1630
+ }
1631
+ }
1632
+ } else {
1633
+ const tokenSuccessResult = handleTokenOperationSuccess({
1634
+ responsePayload: response.payload,
1635
+ });
1636
+ tokenMap.set(tokenSuccessResult.token.name, tokenSuccessResult.token);
1637
+ }
1413
1638
  break;
1414
1639
  case "user":
1415
1640
  if (action === "DELETE") {
1416
- userData = handleUserOperationError();
1417
- user = new User(userData);
1641
+ const newUser = handleUserDelete();
1642
+ userData = newUser;
1643
+ user = new User(newUser);
1644
+ }
1645
+ break;
1646
+ case "resource":
1647
+ if (action === "DELETE") {
1648
+ const tenant = originalData?.payload?.tenant;
1649
+ const resourceName = originalData?.payload?.resource;
1650
+ const resourceType = originalData?.payload?.kind;
1651
+ if (tenant && resourceName && resourceType) {
1652
+ setResourceDeleting(tenantsMap, tenant, resourceName, resourceType);
1653
+ }
1418
1654
  }
1419
1655
  break;
1420
1656
  }
@@ -1479,10 +1715,12 @@ const handleOperationError = (operation: PendingOperation, error: any) => {
1479
1715
  error,
1480
1716
  });
1481
1717
 
1718
+ const accErrorTenant = (originalData as Account)?.tenant;
1719
+ const accErrorKey = accErrorTenant ? `${accErrorTenant}/${entityName}` : entityName;
1482
1720
  if (accErrorResult.shouldDelete) {
1483
- accountsMap.delete(entityName);
1721
+ accountsMap.delete(accErrorKey);
1484
1722
  } else if (accErrorResult.updatedAccount) {
1485
- accountsMap.set(entityName, accErrorResult.updatedAccount);
1723
+ accountsMap.set(accErrorKey, accErrorResult.updatedAccount);
1486
1724
  }
1487
1725
 
1488
1726
  if (accErrorResult.eventType === "creationError") {
@@ -1492,7 +1730,6 @@ const handleOperationError = (operation: PendingOperation, error: any) => {
1492
1730
  } else if (accErrorResult.eventType === "deletionError") {
1493
1731
  eventHelper.account.publish.deletionError(originalData);
1494
1732
  }
1495
- eventHelper.notification.publish.creation(accErrorResult.notification);
1496
1733
  break;
1497
1734
  case "environment":
1498
1735
  const envErrorResult = handleEnvironmentOperationError({
@@ -1509,7 +1746,9 @@ const handleOperationError = (operation: PendingOperation, error: any) => {
1509
1746
  eventHelper.environment.publish.deletionError(originalData);
1510
1747
  }
1511
1748
  eventHelper.notification.publish.creation(envErrorResult.notification);
1512
- environmentsMap.set(entityName, envErrorResult.updatedEnvironment);
1749
+ const envErrOrigData = originalData as Environment;
1750
+ const envErrorKey = envErrOrigData?.tenant && envErrOrigData?.account ? `${envErrOrigData.tenant}/${envErrOrigData.account}/${entityName}` : entityName;
1751
+ environmentsMap.set(envErrorKey, envErrorResult.updatedEnvironment);
1513
1752
  break;
1514
1753
  }
1515
1754
  setTimeout(async () => {
@@ -1540,11 +1779,12 @@ const handleDeleteEvent = async (message: WSMessage) => {
1540
1779
  }
1541
1780
  if (keyParts.environment && !keyParts.service) {
1542
1781
  const environmentName = keyParts.environment;
1543
- const deletedEnv = environmentsMap.get(environmentName)!;
1782
+ const envDeleteKey = tenantName && keyParts.account ? `${tenantName}/${keyParts.account}/${environmentName}` : environmentName;
1783
+ const deletedEnv = environmentsMap.get(envDeleteKey)!;
1544
1784
  eventHelper.environment.publish.deleted(deletedEnv);
1545
1785
  const envNotification: Notification = {
1546
1786
  type: "success",
1547
- subtype: "environment-deleted",
1787
+ subtype: errors.environment.deleted.subtype,
1548
1788
  date: Date.now().toString(),
1549
1789
  callToAction: false,
1550
1790
  status: "unread",
@@ -1555,15 +1795,16 @@ const handleDeleteEvent = async (message: WSMessage) => {
1555
1795
  },
1556
1796
  };
1557
1797
  eventHelper.notification.publish.creation(envNotification);
1558
- environmentsMap.delete(environmentName);
1798
+ environmentsMap.delete(envDeleteKey);
1559
1799
  }
1560
1800
  if (keyParts.account && !keyParts.environment) {
1561
1801
  const accountName = keyParts.account;
1562
- const deletedAccount = accountsMap.get(accountName)!;
1802
+ const accountDeleteKey = tenantName ? `${tenantName}/${accountName}` : accountName;
1803
+ const deletedAccount = accountsMap.get(accountDeleteKey)!;
1563
1804
  eventHelper.account.publish.deleted(deletedAccount);
1564
1805
  const accountNotification: Notification = {
1565
1806
  type: "success",
1566
- subtype: "account-deleted",
1807
+ subtype: errors.account.deleted.subtype,
1567
1808
  date: Date.now().toString(),
1568
1809
  callToAction: false,
1569
1810
  status: "unread",
@@ -1573,7 +1814,7 @@ const handleDeleteEvent = async (message: WSMessage) => {
1573
1814
  },
1574
1815
  };
1575
1816
  eventHelper.notification.publish.creation(accountNotification);
1576
- accountsMap.delete(accountName);
1817
+ accountsMap.delete(accountDeleteKey);
1577
1818
  }
1578
1819
  if (keyParts.domain) {
1579
1820
  const domainName = keyParts.domain;
@@ -1701,7 +1942,7 @@ const handleDeleteEvent = async (message: WSMessage) => {
1701
1942
  tenantsMap.set(tenantName, registryTenant);
1702
1943
  const registryNotification: Notification = {
1703
1944
  type: "success",
1704
- subtype: "registry-deleted",
1945
+ subtype: errors.registry.deleted.subtype,
1705
1946
  date: Date.now().toString(),
1706
1947
  status: "unread",
1707
1948
  callToAction: false,
@@ -1757,7 +1998,7 @@ const handleDeleteEvent = async (message: WSMessage) => {
1757
1998
  if (deletedLinkInfo) {
1758
1999
  const unlinkNotification: Notification = {
1759
2000
  type: "success",
1760
- subtype: "service-unlinked",
2001
+ subtype: errors.service.unlinked.subtype,
1761
2002
  date: Date.now().toString(),
1762
2003
  status: "unread",
1763
2004
  callToAction: false,
@@ -1774,24 +2015,17 @@ const handleDeleteEvent = async (message: WSMessage) => {
1774
2015
  }
1775
2016
  }
1776
2017
  if (keyParts.token && keyParts.user) {
1777
- const tokenId = keyParts.token;
1778
- const tokenIndex = userData.tokens.findIndex((t) => t.name === tokenId);
1779
- if (tokenIndex !== -1) {
1780
- const deletedToken = userData.tokens[tokenIndex];
1781
- userData.tokens.splice(tokenIndex, 1);
1782
-
1783
- const tokenNotification: Notification = {
1784
- type: "success",
1785
- subtype: "token-deleted",
1786
- date: Date.now().toString(),
1787
- status: "unread",
1788
- callToAction: false,
1789
- data: {
1790
- tokenName: deletedToken.name,
1791
- tenant: deletedToken.tenant,
1792
- },
1793
- };
1794
- eventHelper.notification.publish.creation(tokenNotification);
2018
+ const tokenDeletionResult = handleTokenDeletion({
2019
+ tokenId: keyParts.token,
2020
+ tokenMap,
2021
+ userData,
2022
+ deletedTokenIds,
2023
+ });
2024
+ if (tokenDeletionResult.removed) {
2025
+ userData.tokens = tokenDeletionResult.updatedUserTokens;
2026
+ eventHelper.notification.publish.creation(
2027
+ tokenDeletionResult.notification!,
2028
+ );
1795
2029
  }
1796
2030
  }
1797
2031
  rebuildHierarchy();
@@ -1833,9 +2067,10 @@ const getChannelsInfo = async (
1833
2067
  const updateEnvironmentsCloudProvider = (
1834
2068
  accountId: string,
1835
2069
  cloudProviderName: string,
2070
+ tenantId?: string,
1836
2071
  ) => {
1837
2072
  environmentsMap.forEach((environment, envId) => {
1838
- if (environment.account === accountId) {
2073
+ if (environment.account === accountId && (!tenantId || environment.tenant === tenantId)) {
1839
2074
  environment.cloudProvider = cloudProviderName;
1840
2075
  environmentsMap.set(envId, environment);
1841
2076
  }
@@ -1853,7 +2088,8 @@ const loadReportingForEnvironment = async (environment: Environment) => {
1853
2088
  reportingData !== null &&
1854
2089
  Array.isArray(reportingData)
1855
2090
  ) {
1856
- const updatedEnvironment = environmentsMap.get(environment.id);
2091
+ const envReportKey = environment.tenant && environment.account ? `${environment.tenant}/${environment.account}/${environment.id}` : environment.id;
2092
+ const updatedEnvironment = environmentsMap.get(envReportKey);
1857
2093
 
1858
2094
  if (!updatedEnvironment) {
1859
2095
  console.warn(`Environment ${environment.id} not found in map`);
@@ -1884,7 +2120,7 @@ const loadReportingForEnvironment = async (environment: Environment) => {
1884
2120
  );
1885
2121
  }
1886
2122
 
1887
- environmentsMap.set(environment.id, updatedEnvironment);
2123
+ environmentsMap.set(envReportKey, updatedEnvironment);
1888
2124
  }
1889
2125
  } catch (error) {
1890
2126
  console.error(
@@ -1916,8 +2152,20 @@ export const updateUserComplete = (updatedUserData: UserData): User => {
1916
2152
  try {
1917
2153
  const preservedNotifications =
1918
2154
  userData.notifications || updatedUserData.notifications || [];
2155
+ const incomingTokenNames = new Set(
2156
+ (updatedUserData.tokens || []).map((t) => t.name)
2157
+ );
2158
+ const localOnlyTokens = (userData.tokens || []).filter(
2159
+ (t) => !incomingTokenNames.has(t.name)
2160
+ );
2161
+ const mergedTokens = [...(updatedUserData.tokens || []), ...localOnlyTokens]
2162
+ .filter((t, index, arr) => arr.findIndex(x => x.name === t.name) === index)
2163
+ .filter(t => tokenMap.size === 0 || tokenMap.has(t.name))
2164
+ .filter(t => !deletedTokenIds.has(t.name));
2165
+
1919
2166
  userData = {
1920
2167
  id: updatedUserData.id || userData.id,
2168
+ created: updatedUserData.created || userData.created,
1921
2169
  name: updatedUserData.name || userData.name,
1922
2170
  surname: updatedUserData.surname || userData.surname,
1923
2171
  provider: updatedUserData.provider || userData.provider || [],
@@ -1927,7 +2175,7 @@ export const updateUserComplete = (updatedUserData: UserData): User => {
1927
2175
  "",
1928
2176
  organizations: updatedUserData.organizations || [],
1929
2177
  clusterTokens: updatedUserData.clusterTokens || [],
1930
- tokens: updatedUserData.tokens || [],
2178
+ tokens: mergedTokens,
1931
2179
  tenants: updatedUserData.tenants || [],
1932
2180
  axebowPlan:
1933
2181
  updatedUserData.axebowPlan || userData.axebowPlan || "freemium",
@@ -1938,6 +2186,7 @@ export const updateUserComplete = (updatedUserData: UserData): User => {
1938
2186
  status: updatedUserData.status,
1939
2187
  discoveryMethod:
1940
2188
  updatedUserData.discoveryMethod || userData.discoveryMethod,
2189
+ promotionInfo: computePromotionInfo(updatedUserData.tenants || userData.tenants || []),
1941
2190
  };
1942
2191
  user = new User(
1943
2192
  userData,
@@ -1956,7 +2205,7 @@ const resolveServiceStatus = (service: Service): Service => {
1956
2205
  return service;
1957
2206
  }
1958
2207
 
1959
- const revisionKey = `${service.tenant}/${service.name}-${service.currentRevision}`;
2208
+ const revisionKey = `/tenant/${service.tenant}/service/${service.name}/revision/${service.currentRevision}`;
1960
2209
  const currentRevision = revisionsMap.get(revisionKey);
1961
2210
 
1962
2211
  if (!currentRevision) {
@@ -1984,3 +2233,58 @@ const resolveServiceStatus = (service: Service): Service => {
1984
2233
  status: currentRevision.status,
1985
2234
  };
1986
2235
  };
2236
+ export const fetchAndStoreMarketplaceSchema = async (
2237
+ item: MarketplaceItem,
2238
+ ): Promise<void> => {
2239
+ const tenantsWithItem: string[] = [];
2240
+ tenantsMap.forEach((tenantEntry, tenantId) => {
2241
+ const hasItem = (tenantEntry.marketplaceItems || []).some(
2242
+ (i) => i.name === item.name && i.domain === item.domain && i.artifact === item.artifact,
2243
+ );
2244
+ if (hasItem) tenantsWithItem.push(tenantId);
2245
+ });
2246
+
2247
+ if (tenantsWithItem.length === 0) return;
2248
+
2249
+ await Promise.all(
2250
+ tenantsWithItem.map(async (tenantId) => {
2251
+ try {
2252
+ const response = await makeGlobalWebSocketRequest(
2253
+ "marketplace:artifact_schema",
2254
+ {
2255
+ tenant: tenantId,
2256
+ module: item.name,
2257
+ version: item.version,
2258
+ domain: item.domain,
2259
+ artifact: item.artifact,
2260
+ },
2261
+ 30000,
2262
+ "GET",
2263
+ tenantId,
2264
+ );
2265
+
2266
+ const data = response?.data;
2267
+ if (!data) return;
2268
+
2269
+ const nameKeys = Object.keys(data);
2270
+ const schema = processMarketplaceSchemaResponse(data[nameKeys[0]], nameKeys[0]);
2271
+
2272
+ const tenantEntry = tenantsMap.get(tenantId);
2273
+ if (tenantEntry) {
2274
+ tenantEntry.marketplaceItems = tenantEntry.marketplaceItems.map((i) =>
2275
+ i.name === item.name && i.domain === item.domain && i.artifact === item.artifact
2276
+ ? { ...i, schema }
2277
+ : i,
2278
+ );
2279
+ tenantsMap.set(tenantId, tenantEntry);
2280
+ }
2281
+ } catch (error) {
2282
+ console.error(`Error loading schema for tenant ${tenantId}:`, error);
2283
+ }
2284
+ }),
2285
+ );
2286
+
2287
+ rebuildHierarchy();
2288
+ await updateUserWithPlatformInfo();
2289
+ };
2290
+