@kumori/aurora-backend-handler 1.0.100 → 1.0.112

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