@kumori/aurora-backend-handler 1.0.99 → 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,6 +955,45 @@ const handleEvent = async (message: WSMessage) => {
804
955
  eventData,
805
956
  userData,
806
957
  );
958
+ if (
959
+ existingTenant &&
960
+ existingTenant.role &&
961
+ newTenant.role &&
962
+ existingTenant.role !== newTenant.role
963
+ ) {
964
+ const roleNotification: Notification = {
965
+ type: "info",
966
+ subtype: errors.tenant.roleChanged.subtype,
967
+ date: Date.now().toString(),
968
+ status: "unread",
969
+ callToAction: false,
970
+ data: {
971
+ tenant: newTenant.name,
972
+ oldRole: existingTenant.role,
973
+ newRole: newTenant.role,
974
+ },
975
+ };
976
+ eventHelper.notification.publish.creation(roleNotification);
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
+ });
807
997
  if (existingTenant) {
808
998
  if (
809
999
  !newTenant.resources?.length &&
@@ -829,6 +1019,12 @@ const handleEvent = async (message: WSMessage) => {
829
1019
  }
830
1020
 
831
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
+
832
1028
  loadMarketplaceItemsForTenant(entityId, "") //TODO Move to helper
833
1029
  .then((items) => {
834
1030
  const tenant = tenantsMap.get(entityId);
@@ -931,13 +1127,17 @@ const handleEvent = async (message: WSMessage) => {
931
1127
  break;
932
1128
 
933
1129
  case "ca":
934
- const caResult = handleCAEvent(eventData, parentParts, tenantsMap, hasRequestingServices,);
1130
+ const caResult = handleCAEvent(
1131
+ eventData,
1132
+ parentParts,
1133
+ tenantsMap,
1134
+ hasRequestingServices,
1135
+ );
935
1136
  processResourceResult(
936
1137
  caResult,
937
1138
  tenantsMap,
938
1139
  pendingDomains,
939
1140
  eventHelper,
940
-
941
1141
  );
942
1142
  break;
943
1143
 
@@ -1030,6 +1230,7 @@ const handleEvent = async (message: WSMessage) => {
1030
1230
  eventData,
1031
1231
  tokenMap,
1032
1232
  userData,
1233
+ deletedTokenIds,
1033
1234
  });
1034
1235
 
1035
1236
  if (tokenResult.isUserToken) {
@@ -1236,6 +1437,7 @@ const handleOperationSuccess = (operation: PendingOperation, response: any) => {
1236
1437
  const channelsInfo = mapChannelsFromApiData(
1237
1438
  response.payload.data,
1238
1439
  serviceName,
1440
+ service.resources,
1239
1441
  );
1240
1442
  service.serverChannels = channelsInfo.serverChannels;
1241
1443
  service.clientChannels = channelsInfo.clientChannels;
@@ -1252,7 +1454,7 @@ const handleOperationSuccess = (operation: PendingOperation, response: any) => {
1252
1454
  svcSuccessResult.revisionData
1253
1455
  ) {
1254
1456
  try {
1255
- const updatedService = processRevisionData(
1457
+ let updatedService = processRevisionData(
1256
1458
  svcSuccessResult.updatedService!,
1257
1459
  svcSuccessResult.revisionData,
1258
1460
  revisionsMap,
@@ -1272,6 +1474,28 @@ const handleOperationSuccess = (operation: PendingOperation, response: any) => {
1272
1474
  };
1273
1475
  }
1274
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
+
1275
1499
  servicesMap.set(svcSuccessResult.serviceId, updatedService);
1276
1500
 
1277
1501
  if (updatedService.role && updatedService.role.length > 0) {
@@ -1326,16 +1550,18 @@ const handleOperationSuccess = (operation: PendingOperation, response: any) => {
1326
1550
  originalData: originalData as Account,
1327
1551
  });
1328
1552
 
1553
+ const accTenant = (originalData as Account)?.tenant;
1554
+ const accSuccessKey = accTenant ? `${accTenant}/${entityName}` : entityName;
1329
1555
  if (accSuccessResult.shouldDelete) {
1330
- const acc = accountsMap.get(entityName);
1556
+ const acc = accountsMap.get(accSuccessKey);
1331
1557
  if (acc) {
1332
- accountsMap.delete(entityName);
1558
+ accountsMap.delete(accSuccessKey);
1333
1559
  eventHelper.notification.publish.creation(
1334
1560
  accSuccessResult.notification,
1335
1561
  );
1336
1562
  }
1337
1563
  } else if (accSuccessResult.updatedAccount) {
1338
- accountsMap.set(entityName, accSuccessResult.updatedAccount);
1564
+ accountsMap.set(accSuccessKey, accSuccessResult.updatedAccount);
1339
1565
  if (accSuccessResult.eventType === "created") {
1340
1566
  eventHelper.account.publish.created(accSuccessResult.updatedAccount);
1341
1567
  } else if (accSuccessResult.eventType === "updated") {
@@ -1354,10 +1580,12 @@ const handleOperationSuccess = (operation: PendingOperation, response: any) => {
1354
1580
  originalData: originalData as Environment,
1355
1581
  });
1356
1582
 
1583
+ const envOrigData = originalData as Environment;
1584
+ const envSuccessKey = envOrigData?.tenant && envOrigData?.account ? `${envOrigData.tenant}/${envOrigData.account}/${entityName}` : entityName;
1357
1585
  if (envSuccessResult.shouldDelete) {
1358
- environmentsMap.delete(entityName);
1586
+ environmentsMap.delete(envSuccessKey);
1359
1587
  } else if (envSuccessResult.updatedEnvironment) {
1360
- environmentsMap.set(entityName, envSuccessResult.updatedEnvironment);
1588
+ environmentsMap.set(envSuccessKey, envSuccessResult.updatedEnvironment);
1361
1589
  if (envSuccessResult.notification) {
1362
1590
  eventHelper.notification.publish.creation(
1363
1591
  envSuccessResult.notification,
@@ -1385,15 +1613,44 @@ const handleOperationSuccess = (operation: PendingOperation, response: any) => {
1385
1613
  }
1386
1614
  break;
1387
1615
  case "token":
1388
- const tokenSuccessResult = handleTokenOperationSuccess({
1389
- responsePayload: response.payload,
1390
- });
1391
- 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
+ }
1392
1638
  break;
1393
1639
  case "user":
1394
1640
  if (action === "DELETE") {
1395
- userData = handleUserOperationError();
1396
- 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
+ }
1397
1654
  }
1398
1655
  break;
1399
1656
  }
@@ -1458,10 +1715,12 @@ const handleOperationError = (operation: PendingOperation, error: any) => {
1458
1715
  error,
1459
1716
  });
1460
1717
 
1718
+ const accErrorTenant = (originalData as Account)?.tenant;
1719
+ const accErrorKey = accErrorTenant ? `${accErrorTenant}/${entityName}` : entityName;
1461
1720
  if (accErrorResult.shouldDelete) {
1462
- accountsMap.delete(entityName);
1721
+ accountsMap.delete(accErrorKey);
1463
1722
  } else if (accErrorResult.updatedAccount) {
1464
- accountsMap.set(entityName, accErrorResult.updatedAccount);
1723
+ accountsMap.set(accErrorKey, accErrorResult.updatedAccount);
1465
1724
  }
1466
1725
 
1467
1726
  if (accErrorResult.eventType === "creationError") {
@@ -1471,7 +1730,6 @@ const handleOperationError = (operation: PendingOperation, error: any) => {
1471
1730
  } else if (accErrorResult.eventType === "deletionError") {
1472
1731
  eventHelper.account.publish.deletionError(originalData);
1473
1732
  }
1474
- eventHelper.notification.publish.creation(accErrorResult.notification);
1475
1733
  break;
1476
1734
  case "environment":
1477
1735
  const envErrorResult = handleEnvironmentOperationError({
@@ -1488,7 +1746,9 @@ const handleOperationError = (operation: PendingOperation, error: any) => {
1488
1746
  eventHelper.environment.publish.deletionError(originalData);
1489
1747
  }
1490
1748
  eventHelper.notification.publish.creation(envErrorResult.notification);
1491
- 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);
1492
1752
  break;
1493
1753
  }
1494
1754
  setTimeout(async () => {
@@ -1519,11 +1779,12 @@ const handleDeleteEvent = async (message: WSMessage) => {
1519
1779
  }
1520
1780
  if (keyParts.environment && !keyParts.service) {
1521
1781
  const environmentName = keyParts.environment;
1522
- const deletedEnv = environmentsMap.get(environmentName)!;
1782
+ const envDeleteKey = tenantName && keyParts.account ? `${tenantName}/${keyParts.account}/${environmentName}` : environmentName;
1783
+ const deletedEnv = environmentsMap.get(envDeleteKey)!;
1523
1784
  eventHelper.environment.publish.deleted(deletedEnv);
1524
1785
  const envNotification: Notification = {
1525
1786
  type: "success",
1526
- subtype: "environment-deleted",
1787
+ subtype: errors.environment.deleted.subtype,
1527
1788
  date: Date.now().toString(),
1528
1789
  callToAction: false,
1529
1790
  status: "unread",
@@ -1534,15 +1795,16 @@ const handleDeleteEvent = async (message: WSMessage) => {
1534
1795
  },
1535
1796
  };
1536
1797
  eventHelper.notification.publish.creation(envNotification);
1537
- environmentsMap.delete(environmentName);
1798
+ environmentsMap.delete(envDeleteKey);
1538
1799
  }
1539
1800
  if (keyParts.account && !keyParts.environment) {
1540
1801
  const accountName = keyParts.account;
1541
- const deletedAccount = accountsMap.get(accountName)!;
1802
+ const accountDeleteKey = tenantName ? `${tenantName}/${accountName}` : accountName;
1803
+ const deletedAccount = accountsMap.get(accountDeleteKey)!;
1542
1804
  eventHelper.account.publish.deleted(deletedAccount);
1543
1805
  const accountNotification: Notification = {
1544
1806
  type: "success",
1545
- subtype: "account-deleted",
1807
+ subtype: errors.account.deleted.subtype,
1546
1808
  date: Date.now().toString(),
1547
1809
  callToAction: false,
1548
1810
  status: "unread",
@@ -1552,7 +1814,7 @@ const handleDeleteEvent = async (message: WSMessage) => {
1552
1814
  },
1553
1815
  };
1554
1816
  eventHelper.notification.publish.creation(accountNotification);
1555
- accountsMap.delete(accountName);
1817
+ accountsMap.delete(accountDeleteKey);
1556
1818
  }
1557
1819
  if (keyParts.domain) {
1558
1820
  const domainName = keyParts.domain;
@@ -1680,7 +1942,7 @@ const handleDeleteEvent = async (message: WSMessage) => {
1680
1942
  tenantsMap.set(tenantName, registryTenant);
1681
1943
  const registryNotification: Notification = {
1682
1944
  type: "success",
1683
- subtype: "registry-deleted",
1945
+ subtype: errors.registry.deleted.subtype,
1684
1946
  date: Date.now().toString(),
1685
1947
  status: "unread",
1686
1948
  callToAction: false,
@@ -1736,7 +1998,7 @@ const handleDeleteEvent = async (message: WSMessage) => {
1736
1998
  if (deletedLinkInfo) {
1737
1999
  const unlinkNotification: Notification = {
1738
2000
  type: "success",
1739
- subtype: "service-unlinked",
2001
+ subtype: errors.service.unlinked.subtype,
1740
2002
  date: Date.now().toString(),
1741
2003
  status: "unread",
1742
2004
  callToAction: false,
@@ -1753,24 +2015,17 @@ const handleDeleteEvent = async (message: WSMessage) => {
1753
2015
  }
1754
2016
  }
1755
2017
  if (keyParts.token && keyParts.user) {
1756
- const tokenId = keyParts.token;
1757
- const tokenIndex = userData.tokens.findIndex((t) => t.name === tokenId);
1758
- if (tokenIndex !== -1) {
1759
- const deletedToken = userData.tokens[tokenIndex];
1760
- userData.tokens.splice(tokenIndex, 1);
1761
-
1762
- const tokenNotification: Notification = {
1763
- type: "success",
1764
- subtype: "token-deleted",
1765
- date: Date.now().toString(),
1766
- status: "unread",
1767
- callToAction: false,
1768
- data: {
1769
- tokenName: deletedToken.name,
1770
- tenant: deletedToken.tenant,
1771
- },
1772
- };
1773
- 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
+ );
1774
2029
  }
1775
2030
  }
1776
2031
  rebuildHierarchy();
@@ -1812,9 +2067,10 @@ const getChannelsInfo = async (
1812
2067
  const updateEnvironmentsCloudProvider = (
1813
2068
  accountId: string,
1814
2069
  cloudProviderName: string,
2070
+ tenantId?: string,
1815
2071
  ) => {
1816
2072
  environmentsMap.forEach((environment, envId) => {
1817
- if (environment.account === accountId) {
2073
+ if (environment.account === accountId && (!tenantId || environment.tenant === tenantId)) {
1818
2074
  environment.cloudProvider = cloudProviderName;
1819
2075
  environmentsMap.set(envId, environment);
1820
2076
  }
@@ -1832,7 +2088,8 @@ const loadReportingForEnvironment = async (environment: Environment) => {
1832
2088
  reportingData !== null &&
1833
2089
  Array.isArray(reportingData)
1834
2090
  ) {
1835
- 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);
1836
2093
 
1837
2094
  if (!updatedEnvironment) {
1838
2095
  console.warn(`Environment ${environment.id} not found in map`);
@@ -1863,7 +2120,7 @@ const loadReportingForEnvironment = async (environment: Environment) => {
1863
2120
  );
1864
2121
  }
1865
2122
 
1866
- environmentsMap.set(environment.id, updatedEnvironment);
2123
+ environmentsMap.set(envReportKey, updatedEnvironment);
1867
2124
  }
1868
2125
  } catch (error) {
1869
2126
  console.error(
@@ -1895,8 +2152,20 @@ export const updateUserComplete = (updatedUserData: UserData): User => {
1895
2152
  try {
1896
2153
  const preservedNotifications =
1897
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
+
1898
2166
  userData = {
1899
2167
  id: updatedUserData.id || userData.id,
2168
+ created: updatedUserData.created || userData.created,
1900
2169
  name: updatedUserData.name || userData.name,
1901
2170
  surname: updatedUserData.surname || userData.surname,
1902
2171
  provider: updatedUserData.provider || userData.provider || [],
@@ -1906,7 +2175,7 @@ export const updateUserComplete = (updatedUserData: UserData): User => {
1906
2175
  "",
1907
2176
  organizations: updatedUserData.organizations || [],
1908
2177
  clusterTokens: updatedUserData.clusterTokens || [],
1909
- tokens: updatedUserData.tokens || [],
2178
+ tokens: mergedTokens,
1910
2179
  tenants: updatedUserData.tenants || [],
1911
2180
  axebowPlan:
1912
2181
  updatedUserData.axebowPlan || userData.axebowPlan || "freemium",
@@ -1917,6 +2186,7 @@ export const updateUserComplete = (updatedUserData: UserData): User => {
1917
2186
  status: updatedUserData.status,
1918
2187
  discoveryMethod:
1919
2188
  updatedUserData.discoveryMethod || userData.discoveryMethod,
2189
+ promotionInfo: computePromotionInfo(updatedUserData.tenants || userData.tenants || []),
1920
2190
  };
1921
2191
  user = new User(
1922
2192
  userData,
@@ -1935,7 +2205,7 @@ const resolveServiceStatus = (service: Service): Service => {
1935
2205
  return service;
1936
2206
  }
1937
2207
 
1938
- const revisionKey = `${service.tenant}/${service.name}-${service.currentRevision}`;
2208
+ const revisionKey = `/tenant/${service.tenant}/service/${service.name}/revision/${service.currentRevision}`;
1939
2209
  const currentRevision = revisionsMap.get(revisionKey);
1940
2210
 
1941
2211
  if (!currentRevision) {
@@ -1963,3 +2233,58 @@ const resolveServiceStatus = (service: Service): Service => {
1963
2233
  status: currentRevision.status,
1964
2234
  };
1965
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
+