@kumori/aurora-backend-handler 1.0.87 → 1.0.89

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,5 +1,28 @@
1
1
  import { Account, Notification } from "@kumori/aurora-interfaces";
2
+ const CREDENTIAL_ERROR_CODES = new Set([
3
+ "_error_retrieving_credentials_",
4
+ "_invalid_account_credentials_",
5
+ "_unsupported_region_",
6
+ ]);
2
7
 
8
+ /**
9
+ * All statuses that mean the account has reached a final error state.
10
+ * Pollers and status-strategy consumers should treat these as terminal.
11
+ */
12
+ export const ACCOUNT_ERROR_STATUSES = new Set([
13
+ "error",
14
+ "invalid_credentials",
15
+ "failed",
16
+ ]);
17
+
18
+ /**
19
+ * Map a raw backend error code to the normalized account status
20
+ * we write to accountsMap / the Account object.
21
+ */
22
+ export const resolveErrorStatus = (
23
+ code: string,
24
+ ): "invalid_credentials" | "error" =>
25
+ CREDENTIAL_ERROR_CODES.has(code) ? "invalid_credentials" : "error";
3
26
 
4
27
  interface HandleAccountEventParams {
5
28
  entityId: string;
@@ -39,10 +62,10 @@ interface HandleAccountOperationErrorResult {
39
62
  eventType: "creationError" | "updateError" | "deletionError";
40
63
  }
41
64
  /**
42
- * Extract cloud provider credentials from event data
65
+ * Extract cloud provider credentials from event data.
43
66
  */
44
67
  const extractCloudProviderCredentials = (
45
- eventData: any
68
+ eventData: any,
46
69
  ): { providerType: string; credentials: any } => {
47
70
  let providerType = "";
48
71
  let credentials = {};
@@ -52,18 +75,24 @@ const extractCloudProviderCredentials = (
52
75
  credentials = {
53
76
  region: eventData.spec.credentials.openstack?.region_name || "",
54
77
  interface: eventData.spec.credentials.openstack?.interface || "",
55
- apiVersion: eventData.spec.credentials.openstack?.identity_api_version || "",
78
+ apiVersion:
79
+ eventData.spec.credentials.openstack?.identity_api_version || "",
56
80
  authType: eventData.spec.credentials.openstack?.auth_type || "",
57
81
  authUrl: eventData.spec.credentials.openstack?.auth?.auth_url || "",
58
- credentialId: eventData.spec.credentials.openstack?.auth?.application_credential_id || "",
59
- credentialSecret: eventData.spec.credentials.openstack?.auth?.application_credential_secret || "",
82
+ credentialId:
83
+ eventData.spec.credentials.openstack?.auth?.application_credential_id ||
84
+ "",
85
+ credentialSecret:
86
+ eventData.spec.credentials.openstack?.auth
87
+ ?.application_credential_secret || "",
60
88
  };
61
89
  } else if (eventData.spec.credentials.aws) {
62
90
  providerType = "aws";
63
91
  credentials = {
64
92
  region: eventData.spec.credentials.aws?.region || "",
65
93
  credentialId: eventData.spec.credentials.aws?.aws_access_key_id || "",
66
- credentialSecret: eventData.spec.credentials.aws?.aws_secret_access_key || "",
94
+ credentialSecret:
95
+ eventData.spec.credentials.aws?.aws_secret_access_key || "",
67
96
  };
68
97
  } else if (eventData.spec.credentials.azure) {
69
98
  providerType = "azure";
@@ -87,28 +116,72 @@ const extractCloudProviderCredentials = (
87
116
  };
88
117
 
89
118
  /**
90
- * Determine account status from event data
119
+ * Determine the canonical account status from a WebSocket event.
120
+ *
121
+ * Priority order:
122
+ * 1. Soft-deleted accounts → 'deleting'
123
+ * 2. validCredentials status reported by the backend (covers both
124
+ * success and the async credential-validation path)
125
+ * 3. Keep the existing status so we never regress a terminal state
126
+ * (e.g. don't overwrite 'invalid_credentials' with 'pending')
127
+ * 4. Default to 'pending' for brand-new accounts
91
128
  */
92
129
  const determineAccountStatus = (
93
130
  eventData: any,
94
- existingAccount: Account | undefined
131
+ existingAccount: Account | undefined,
95
132
  ): string => {
96
133
  if (eventData.meta?.deleted) {
97
134
  return "deleting";
98
135
  }
99
- if (eventData.status.validCredentials?.status) {
100
- return eventData.status.validCredentials.status;
136
+ const rawStatus: string | undefined =
137
+ eventData.status?.validCredentials?.status;
138
+ if (rawStatus) {
139
+ if (CREDENTIAL_ERROR_CODES.has(rawStatus)) {
140
+ return "invalid_credentials";
141
+ }
142
+ return rawStatus;
101
143
  }
102
- if (existingAccount?.status) {
144
+ if (existingAccount?.status && existingAccount.status !== "pending") {
103
145
  return existingAccount.status;
104
146
  }
105
147
  return "pending";
106
148
  };
107
149
 
150
+ /**
151
+ * Translates account error notifications → account.status on accountsMap
152
+ * so that waitForAccountStatus only needs to poll account.status, not notifications.
153
+ *
154
+ * Called from the websocket-manager's `user` event case immediately after
155
+ * handleUserEvent so the status is always written in the same event-loop tick.
156
+ */
157
+ export const syncAccountStatusFromNotifications = (
158
+ notifications: any[] = [],
159
+ accountsMap: Map<string, Account>,
160
+ ): void => {
161
+ for (const notification of notifications) {
162
+ if (notification.type !== "error") continue;
163
+ if (
164
+ notification.subtype !== "account-creation-error" &&
165
+ notification.subtype !== "account-update-error"
166
+ )
167
+ continue;
168
+
169
+ const accountName: string = notification.data?.account;
170
+ if (!accountName) continue;
171
+
172
+ const account = accountsMap.get(accountName);
173
+ if (!account || ACCOUNT_ERROR_STATUSES.has(account.status)) continue;
174
+
175
+ const code: string = notification.info_content?.code ?? "";
176
+ accountsMap.set(accountName, {
177
+ ...account,
178
+ status: resolveErrorStatus(code),
179
+ });
180
+ }
181
+ };
108
182
 
109
183
  /**
110
- * Handles the "account" event from WebSocket messages
111
- * Processes account data updates and cloud provider credentials
184
+ * Handles the "account" kind event from WebSocket messages.
112
185
  */
113
186
  export const handleAccountEvent = ({
114
187
  entityId,
@@ -117,11 +190,13 @@ export const handleAccountEvent = ({
117
190
  accountsMap,
118
191
  }: HandleAccountEventParams): HandleAccountEventResult => {
119
192
  const accountTenantId = parentParts.tenant;
120
- const { providerType, credentials } = extractCloudProviderCredentials(eventData);
193
+ const { providerType, credentials } =
194
+ extractCloudProviderCredentials(eventData);
121
195
  const accountLabels: Record<string, string> = eventData.meta.labels;
122
196
  const hasCredentials = "__axebow::managedCredentials" in accountLabels;
123
197
  const existingAccount = accountsMap.get(entityId);
124
198
  const accountStatus = determineAccountStatus(eventData, existingAccount);
199
+
125
200
  const newAccount: Account = {
126
201
  id: entityId,
127
202
  name: entityId,
@@ -196,9 +271,8 @@ export const handleAccountEvent = ({
196
271
  };
197
272
  };
198
273
 
199
-
200
274
  /**
201
- * Handles successful account operations (CREATE, UPDATE, DELETE)
275
+ * Handles successful account operations (CREATE, UPDATE, DELETE).
202
276
  */
203
277
  export const handleAccountOperationSuccess = ({
204
278
  action,
@@ -206,57 +280,37 @@ export const handleAccountOperationSuccess = ({
206
280
  originalData,
207
281
  }: HandleAccountOperationSuccessParams): HandleAccountOperationSuccessResult => {
208
282
  if (action === "DELETE") {
209
- const accountNotification: Notification = {
210
- type: "success",
211
- subtype: "account-deleted",
212
- date: Date.now().toString(),
213
- status: "unread",
214
- callToAction: false,
215
- data: {
216
- account: originalData.name,
217
- tenant: originalData.tenant,
218
- },
219
- };
220
-
221
283
  return {
222
284
  updatedAccount: null,
223
285
  shouldDelete: true,
224
- notification: accountNotification,
286
+ notification: {
287
+ type: "success",
288
+ subtype: "account-deleted",
289
+ date: Date.now().toString(),
290
+ status: "unread",
291
+ callToAction: false,
292
+ data: { account: originalData.name, tenant: originalData.tenant },
293
+ },
225
294
  eventType: "deleted",
226
295
  };
227
296
  }
228
297
 
229
298
  if (originalData) {
230
- const updatedAccount = { ...originalData, status: "active" };
231
-
232
- let subtype: string;
233
- let eventType: "created" | "updated" | null = null;
234
-
235
- if (action === "CREATE") {
236
- subtype = "account-created";
237
- eventType = "created";
238
- } else {
239
- subtype = "account-updated";
240
- eventType = "updated";
241
- }
242
-
243
- const accountNotification: Notification = {
244
- type: "success",
245
- subtype,
246
- date: Date.now().toString(),
247
- status: "unread",
248
- callToAction: false,
249
- data: {
250
- account: updatedAccount.name,
251
- tenant: updatedAccount.tenant,
252
- },
253
- };
299
+ const updatedAccount: Account = { ...originalData, status: "active" };
300
+ const isCreate = action === "CREATE";
254
301
 
255
302
  return {
256
303
  updatedAccount,
257
304
  shouldDelete: false,
258
- notification: accountNotification,
259
- eventType,
305
+ notification: {
306
+ type: "success",
307
+ subtype: isCreate ? "account-created" : "account-updated",
308
+ date: Date.now().toString(),
309
+ status: "unread",
310
+ callToAction: false,
311
+ data: { account: updatedAccount.name, tenant: updatedAccount.tenant },
312
+ },
313
+ eventType: isCreate ? "created" : "updated",
260
314
  };
261
315
  }
262
316
 
@@ -275,9 +329,12 @@ export const handleAccountOperationSuccess = ({
275
329
  };
276
330
  };
277
331
 
278
-
279
332
  /**
280
- * Handles failed account operations (CREATE, UPDATE, DELETE)
333
+ * Handles failed account operations (CREATE, UPDATE, DELETE).
334
+ *
335
+ * For CREATE failures the account is removed from the map (shouldDelete = true).
336
+ * For UPDATE failures the existing account is kept but stamped with the
337
+ * resolved error status so waitForAccountStatus can terminate cleanly.
281
338
  */
282
339
  export const handleAccountOperationError = ({
283
340
  action,
@@ -285,52 +342,52 @@ export const handleAccountOperationError = ({
285
342
  originalData,
286
343
  error,
287
344
  }: HandleAccountOperationErrorParams): HandleAccountOperationErrorResult => {
288
- let subtype: string;
289
- let eventType: "creationError" | "updateError" | "deletionError";
290
- let shouldDelete = false;
345
+ const isCreate = action === "CREATE";
346
+ const isUpdate = action === "UPDATE";
291
347
 
292
- if (action === "CREATE") {
293
- subtype = "account-creation-error";
294
- eventType = "creationError";
295
- shouldDelete = true;
296
- } else if (action === "UPDATE") {
297
- subtype = "account-update-error";
298
- eventType = "updateError";
299
- } else {
300
- subtype = "account-deletion-error";
301
- eventType = "deletionError";
302
- }
348
+ const subtype = isCreate
349
+ ? "account-creation-error"
350
+ : isUpdate
351
+ ? "account-update-error"
352
+ : "account-deletion-error";
353
+
354
+ const eventType: "creationError" | "updateError" | "deletionError" = isCreate
355
+ ? "creationError"
356
+ : isUpdate
357
+ ? "updateError"
358
+ : "deletionError";
359
+ const errorCode: string = error?.error?.code ?? error?.code ?? "";
360
+ const errorStatus = resolveErrorStatus(errorCode);
303
361
 
304
- const accountErrorNotification: Notification = {
362
+ const notification: Notification = {
305
363
  type: "error",
306
364
  subtype,
307
365
  date: Date.now().toString(),
308
366
  status: "unread",
309
367
  info_content: {
310
- code: error?.error?.code || "UNKNOWN_ERROR",
311
- message: error?.error?.content || error?.error?.message || "Unknown error",
312
- timestamp: error?.error?.timestamp || Date.now().toString(),
368
+ code: errorCode || "UNKNOWN_ERROR",
369
+ message:
370
+ error?.error?.content ?? error?.error?.message ?? "Unknown error",
371
+ timestamp: error?.error?.timestamp ?? Date.now().toString(),
313
372
  },
314
373
  callToAction: false,
315
374
  data: {
316
- account: entityName || originalData?.name || originalData?.account || "unknown",
317
- tenant: originalData?.tenant || "unknown",
375
+ account:
376
+ entityName || originalData?.name || originalData?.account || "unknown",
377
+ tenant: originalData?.tenant ?? "unknown",
318
378
  },
319
379
  };
320
-
321
- let updatedAccount: Account | null = null;
322
-
323
- if (!shouldDelete && originalData) {
324
- updatedAccount = {
325
- ...originalData,
326
- status: "error",
380
+ if (isCreate) {
381
+ return {
382
+ updatedAccount: null,
383
+ shouldDelete: true,
384
+ notification,
385
+ eventType,
327
386
  };
328
387
  }
388
+ const updatedAccount: Account | null = originalData
389
+ ? { ...originalData, status: errorStatus }
390
+ : null;
329
391
 
330
- return {
331
- updatedAccount,
332
- shouldDelete,
333
- notification: accountErrorNotification,
334
- eventType,
335
- };
336
- };
392
+ return { updatedAccount, shouldDelete: false, notification, eventType };
393
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kumori/aurora-backend-handler",
3
- "version": "1.0.87",
3
+ "version": "1.0.89",
4
4
  "description": "backend handler",
5
5
  "main": "backend-handler.ts",
6
6
  "scripts": {
@@ -53,6 +53,7 @@ import {
53
53
  handleAccountEvent,
54
54
  handleAccountOperationError,
55
55
  handleAccountOperationSuccess,
56
+ syncAccountStatusFromNotifications,
56
57
  } from "./helpers/account-helper";
57
58
  import {
58
59
  handleCAEvent,
@@ -191,7 +192,6 @@ const REPORTING_ITERATIONS = 5;
191
192
  const REPORTING_INTERVAL = 1000;
192
193
  let hasLoadedReportingOnce = false;
193
194
  let recentlyUpdatedServices = new Map<string, Service>();
194
-
195
195
  /**
196
196
  * Helper function to safely stringify error objects
197
197
  */
@@ -644,6 +644,7 @@ const handleEvent = async (message: WSMessage) => {
644
644
  userEventResult.deletedTenantKeys.forEach((key) => {
645
645
  tenantsMap.delete(key);
646
646
  });
647
+ syncAccountStatusFromNotifications(userData.notifications, accountsMap);
647
648
  break;
648
649
  case "environment":
649
650
  const envEventResult = handleEnvironmentEvent({
@@ -1453,6 +1454,7 @@ const handleOperationError = (operation: PendingOperation, error: any) => {
1453
1454
  } else if (accErrorResult.updatedAccount) {
1454
1455
  accountsMap.set(entityName, accErrorResult.updatedAccount);
1455
1456
  }
1457
+
1456
1458
  if (accErrorResult.eventType === "creationError") {
1457
1459
  eventHelper.account.publish.creationError(originalData);
1458
1460
  } else if (accErrorResult.eventType === "updateError") {
@@ -1480,6 +1482,10 @@ const handleOperationError = (operation: PendingOperation, error: any) => {
1480
1482
  environmentsMap.set(entityName, envErrorResult.updatedEnvironment);
1481
1483
  break;
1482
1484
  }
1485
+ setTimeout(async () => {
1486
+ rebuildHierarchy();
1487
+ await updateUserWithPlatformInfo();
1488
+ }, 100);
1483
1489
  };
1484
1490
  const handleDeleteEvent = async (message: WSMessage) => {
1485
1491
  const keyParts = parseKeyPath(message.payload.key);