@fjall/deploy-core 0.89.5 → 0.89.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/LICENSE +50 -21
  2. package/README.md +25 -0
  3. package/dist/.minified +1 -0
  4. package/dist/src/__test-utils__/awsMockHelpers.d.ts +20 -0
  5. package/dist/src/__test-utils__/awsMockHelpers.js +1 -0
  6. package/dist/src/__test-utils__/index.d.ts +1 -0
  7. package/dist/src/__test-utils__/index.js +1 -0
  8. package/dist/src/aws/AwsProvider.js +0 -1
  9. package/dist/src/aws/SimpleAwsProvider.js +1 -70
  10. package/dist/src/aws/index.d.ts +4 -2
  11. package/dist/src/aws/index.js +1 -3
  12. package/dist/src/aws/organisations/accounts.js +10 -10
  13. package/dist/src/aws/organisations/backup.js +4 -2
  14. package/dist/src/aws/organisations/costAllocation.js +4 -2
  15. package/dist/src/aws/organisations/delegatedAdmin.d.ts +9 -0
  16. package/dist/src/aws/organisations/delegatedAdmin.js +43 -0
  17. package/dist/src/aws/organisations/identityCentre.d.ts +1 -1
  18. package/dist/src/aws/organisations/identityCentre.js +6 -2
  19. package/dist/src/aws/organisations/index.d.ts +4 -3
  20. package/dist/src/aws/organisations/index.js +1 -12
  21. package/dist/src/aws/organisations/ipam.js +4 -2
  22. package/dist/src/aws/organisations/organisation.js +27 -18
  23. package/dist/src/aws/organisations/organisationalUnits.d.ts +26 -6
  24. package/dist/src/aws/organisations/organisationalUnits.js +149 -35
  25. package/dist/src/aws/organisations/policies.js +4 -3
  26. package/dist/src/aws/organisations/ram.js +6 -2
  27. package/dist/src/aws/organisations/serviceAccess.js +12 -6
  28. package/dist/src/aws/organisations/trustedAccess.js +6 -2
  29. package/dist/src/aws/organisations/types.d.ts +23 -1
  30. package/dist/src/aws/organisations/types.js +1 -16
  31. package/dist/src/aws/utils/__tests__/cloudformationTestHelpers.d.ts +6 -0
  32. package/dist/src/aws/utils/__tests__/cloudformationTestHelpers.js +1 -0
  33. package/dist/src/aws/utils/cloudformationEventHelpers.d.ts +48 -0
  34. package/dist/src/aws/utils/cloudformationEventHelpers.js +1 -0
  35. package/dist/src/aws/utils/cloudformationEventTypes.d.ts +45 -0
  36. package/dist/src/aws/utils/cloudformationEventTypes.js +1 -0
  37. package/dist/src/aws/utils/cloudformationEvents.d.ts +8 -54
  38. package/dist/src/aws/utils/cloudformationEvents.js +1 -596
  39. package/dist/src/aws/utils/index.d.ts +5 -0
  40. package/dist/src/aws/utils/index.js +1 -0
  41. package/dist/src/aws/utils/stackStatus.js +1 -90
  42. package/dist/src/events/index.d.ts +13 -0
  43. package/dist/src/events/index.js +1 -0
  44. package/dist/src/index.d.ts +34 -17
  45. package/dist/src/index.js +41 -21
  46. package/dist/src/orchestration/__tests__/cascadeTestHelpers.d.ts +12 -0
  47. package/dist/src/orchestration/__tests__/cascadeTestHelpers.js +78 -0
  48. package/dist/src/orchestration/activeDeploymentGuard.d.ts +10 -0
  49. package/dist/src/orchestration/activeDeploymentGuard.js +39 -0
  50. package/dist/src/orchestration/applicationDeploy.js +46 -229
  51. package/dist/src/orchestration/applicationDeployHelpers.d.ts +39 -0
  52. package/dist/src/orchestration/applicationDeployHelpers.js +223 -0
  53. package/dist/src/orchestration/applicationDestroy.d.ts +14 -0
  54. package/dist/src/orchestration/applicationDestroy.js +131 -0
  55. package/dist/src/orchestration/builders/dockerBuilder.d.ts +17 -0
  56. package/dist/src/orchestration/builders/dockerBuilder.js +98 -0
  57. package/dist/src/orchestration/builders/frameworkRegistry.d.ts +23 -0
  58. package/dist/src/orchestration/builders/frameworkRegistry.js +1 -0
  59. package/dist/src/orchestration/builders/index.d.ts +4 -0
  60. package/dist/src/orchestration/builders/index.js +1 -0
  61. package/dist/src/orchestration/builders/openNextBuilder.d.ts +21 -0
  62. package/dist/src/orchestration/builders/openNextBuilder.js +144 -0
  63. package/dist/src/orchestration/cascadeDestroyHelpers.d.ts +30 -0
  64. package/dist/src/orchestration/cascadeDestroyHelpers.js +1 -0
  65. package/dist/src/orchestration/cascadeHelpers.d.ts +46 -0
  66. package/dist/src/orchestration/cascadeHelpers.js +160 -0
  67. package/dist/src/orchestration/contextHelpers.d.ts +46 -2
  68. package/dist/src/orchestration/contextHelpers.js +93 -1
  69. package/dist/src/orchestration/destroy.d.ts +13 -0
  70. package/dist/src/orchestration/destroy.js +67 -0
  71. package/dist/src/orchestration/detectionPipeline.d.ts +2 -11
  72. package/dist/src/orchestration/detectionPipeline.js +29 -10
  73. package/dist/src/orchestration/dockerBuildHelper.d.ts +10 -0
  74. package/dist/src/orchestration/dockerBuildHelper.js +49 -0
  75. package/dist/src/orchestration/dockerInterface.d.ts +4 -2
  76. package/dist/src/orchestration/index.d.ts +8 -1
  77. package/dist/src/orchestration/index.js +1 -3
  78. package/dist/src/orchestration/manifestSecretParser.d.ts +11 -0
  79. package/dist/src/orchestration/manifestSecretParser.js +1 -0
  80. package/dist/src/orchestration/openNextBuild.d.ts +28 -0
  81. package/dist/src/orchestration/openNextBuild.js +243 -0
  82. package/dist/src/orchestration/organisationDeploy.js +110 -233
  83. package/dist/src/orchestration/organisationDestroy.d.ts +24 -0
  84. package/dist/src/orchestration/organisationDestroy.js +189 -0
  85. package/dist/src/orchestration/organisationSetup.d.ts +6 -4
  86. package/dist/src/orchestration/organisationSetup.js +28 -8
  87. package/dist/src/orchestration/resolveOperation.js +68 -6
  88. package/dist/src/orchestration/serviceFactory.d.ts +4 -0
  89. package/dist/src/orchestration/serviceFactory.js +1 -16
  90. package/dist/src/orchestration/spawnHelpers.d.ts +47 -0
  91. package/dist/src/orchestration/spawnHelpers.js +1 -0
  92. package/dist/src/orchestration/stackCleanup.d.ts +39 -0
  93. package/dist/src/orchestration/stackCleanup.js +1 -0
  94. package/dist/src/orchestration/welcomeImageHelper.d.ts +15 -0
  95. package/dist/src/orchestration/welcomeImageHelper.js +64 -0
  96. package/dist/src/services/application/ApplicationStackService.d.ts +21 -30
  97. package/dist/src/services/application/ApplicationStackService.js +16 -234
  98. package/dist/src/services/application/applicationStackHelpers.d.ts +46 -0
  99. package/dist/src/services/application/applicationStackHelpers.js +248 -0
  100. package/dist/src/services/application/index.d.ts +1 -0
  101. package/dist/src/services/application/index.js +1 -1
  102. package/dist/src/services/index.d.ts +6 -0
  103. package/dist/src/services/index.js +1 -0
  104. package/dist/src/services/infrastructure/CdkArgumentBuilder.js +1 -67
  105. package/dist/src/services/infrastructure/CdkCommandRunner.d.ts +10 -2
  106. package/dist/src/services/infrastructure/CdkCommandRunner.js +18 -15
  107. package/dist/src/services/infrastructure/CdkErrorFormatter.js +16 -194
  108. package/dist/src/services/infrastructure/CdkEventMonitoring.js +1 -41
  109. package/dist/src/services/infrastructure/CdkOutputAnalyser.js +1 -1
  110. package/dist/src/services/infrastructure/CdkOutputParser.js +2 -33
  111. package/dist/src/services/infrastructure/CdkProcessManager.d.ts +5 -0
  112. package/dist/src/services/infrastructure/CdkProcessManager.js +81 -47
  113. package/dist/src/services/infrastructure/CdkService.d.ts +7 -53
  114. package/dist/src/services/infrastructure/CdkService.js +41 -83
  115. package/dist/src/services/infrastructure/CdkServiceTypes.d.ts +50 -0
  116. package/dist/src/services/infrastructure/CdkServiceTypes.js +0 -0
  117. package/dist/src/services/infrastructure/CloudFormationService.js +9 -10
  118. package/dist/src/services/infrastructure/ICdkProcessManager.d.ts +27 -0
  119. package/dist/src/services/infrastructure/ICdkProcessManager.js +1 -0
  120. package/dist/src/services/infrastructure/__tests__/cloudFormationTestHelpers.d.ts +9 -0
  121. package/dist/src/services/infrastructure/__tests__/cloudFormationTestHelpers.js +1 -0
  122. package/dist/src/services/infrastructure/cdkServiceHelpers.d.ts +9 -0
  123. package/dist/src/services/infrastructure/cdkServiceHelpers.js +1 -0
  124. package/dist/src/services/infrastructure/constructMapEnrichment.d.ts +7 -0
  125. package/dist/src/services/infrastructure/constructMapEnrichment.js +1 -0
  126. package/dist/src/services/infrastructure/index.d.ts +3 -1
  127. package/dist/src/services/infrastructure/index.js +1 -7
  128. package/dist/src/services/supporting/TemplateHashService.js +1 -1
  129. package/dist/src/services/supporting/helpers.js +1 -81
  130. package/dist/src/services/supporting/index.js +1 -3
  131. package/dist/src/steps/index.d.ts +1 -0
  132. package/dist/src/steps/index.js +1 -0
  133. package/dist/src/steps/stepRegistry.d.ts +71 -0
  134. package/dist/src/steps/stepRegistry.js +505 -0
  135. package/dist/src/types/FjallState.js +1 -118
  136. package/dist/src/types/ProgressEvent.js +1 -48
  137. package/dist/src/types/application/ApplicationServiceTypes.js +1 -30
  138. package/dist/src/types/application/index.js +1 -1
  139. package/dist/src/types/callbacks.d.ts +76 -4
  140. package/dist/src/types/callbacks.js +0 -1
  141. package/dist/src/types/constants.d.ts +2 -0
  142. package/dist/src/types/constants.js +1 -6
  143. package/dist/src/types/credentials.js +0 -1
  144. package/dist/src/types/deployment/DeploymentServiceTypes.d.ts +5 -2
  145. package/dist/src/types/deployment/DeploymentServiceTypes.js +1 -1
  146. package/dist/src/types/deployment/DeploymentTypes.js +0 -1
  147. package/dist/src/types/deployment/cloudformation.js +0 -1
  148. package/dist/src/types/deployment/index.d.ts +3 -1
  149. package/dist/src/types/deployment/index.js +1 -1
  150. package/dist/src/types/deployment/parallel.js +1 -10
  151. package/dist/src/types/deploymentEventSchema.d.ts +158 -0
  152. package/dist/src/types/deploymentEventSchema.js +1 -0
  153. package/dist/src/types/detection.d.ts +22 -0
  154. package/dist/src/types/detection.js +1 -0
  155. package/dist/src/types/entitlements.d.ts +31 -0
  156. package/dist/src/types/entitlements.js +0 -0
  157. package/dist/src/types/errors/CdkError.js +1 -20
  158. package/dist/src/types/errors/ServiceError.d.ts +2 -1
  159. package/dist/src/types/errors/ServiceError.js +1 -119
  160. package/dist/src/types/errors/index.d.ts +2 -0
  161. package/dist/src/types/errors/index.js +1 -0
  162. package/dist/src/types/events.d.ts +3 -9
  163. package/dist/src/types/events.js +0 -5
  164. package/dist/src/types/frameworkBuilder.d.ts +96 -0
  165. package/dist/src/types/frameworkBuilder.js +8 -0
  166. package/dist/src/types/index.d.ts +19 -4
  167. package/dist/src/types/index.js +1 -9
  168. package/dist/src/types/operations.d.ts +3 -2
  169. package/dist/src/types/operations.js +1 -285
  170. package/dist/src/types/orgConfig.d.ts +2 -10
  171. package/dist/src/types/orgConfig.js +0 -11
  172. package/dist/src/types/params.d.ts +60 -1
  173. package/dist/src/types/patternDetection.d.ts +14 -16
  174. package/dist/src/types/patternDetection.js +14 -18
  175. package/dist/src/types/patternTypes.d.ts +19 -0
  176. package/dist/src/types/patternTypes.js +1 -0
  177. package/dist/src/types/stepDefinitions.d.ts +163 -0
  178. package/dist/src/types/stepDefinitions.js +98 -0
  179. package/dist/src/types/validation.js +0 -1
  180. package/dist/src/util/dockerfileDetection.d.ts +5 -0
  181. package/dist/src/util/dockerfileDetection.js +1 -0
  182. package/dist/src/util/index.d.ts +4 -3
  183. package/dist/src/util/index.js +1 -3
  184. package/dist/src/util/sequencedCallbacks.d.ts +44 -0
  185. package/dist/src/util/sequencedCallbacks.js +1 -0
  186. package/package.json +49 -8
  187. package/dist/src/aws/utils/CloudFormationFailureAnalyser.d.ts +0 -32
  188. package/dist/src/aws/utils/CloudFormationFailureAnalyser.js +0 -228
  189. package/dist/src/aws/utils/errors.d.ts +0 -26
  190. package/dist/src/aws/utils/errors.js +0 -59
  191. package/dist/src/util/fsHelpers.d.ts +0 -4
  192. package/dist/src/util/fsHelpers.js +0 -16
  193. package/dist/src/util/securityHelpers.d.ts +0 -31
  194. package/dist/src/util/securityHelpers.js +0 -124
  195. package/dist/src/util/singleton.d.ts +0 -2
  196. package/dist/src/util/singleton.js +0 -9
  197. package/dist/src/util/sleep.d.ts +0 -4
  198. package/dist/src/util/sleep.js +0 -4
@@ -1,18 +1,19 @@
1
1
  import { CreateOrganizationalUnitCommand, ListOrganizationalUnitsForParentCommand, ListParentsCommand, MoveAccountCommand } from "@aws-sdk/client-organizations";
2
2
  import { success, failure } from "@fjall/generator";
3
- import { extractErrorName } from "./types.js";
3
+ import { extractErrorName, isOULeaf, SDK_TIMEOUT_MS, AWS_ERROR_NAMES } from "./types.js";
4
+ import { getErrorMessage } from "@fjall/util";
4
5
  /**
5
6
  * List all OUs under a parent, handling pagination.
6
7
  */
7
8
  async function listOUsForParent(client, parentId) {
8
- const response = await client.send(new ListOrganizationalUnitsForParentCommand({ ParentId: parentId }));
9
+ const response = await client.send(new ListOrganizationalUnitsForParentCommand({ ParentId: parentId }), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
9
10
  let ous = response.OrganizationalUnits ?? [];
10
11
  let nextToken = response.NextToken;
11
12
  while (nextToken) {
12
13
  const nextResponse = await client.send(new ListOrganizationalUnitsForParentCommand({
13
14
  ParentId: parentId,
14
15
  NextToken: nextToken
15
- }));
16
+ }), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
16
17
  ous = ous.concat(nextResponse.OrganizationalUnits ?? []);
17
18
  nextToken = nextResponse.NextToken;
18
19
  }
@@ -23,61 +24,173 @@ async function listOUsForParent(client, parentId) {
23
24
  */
24
25
  async function getParentId(client, childId) {
25
26
  try {
26
- const response = await client.send(new ListParentsCommand({ ChildId: childId }));
27
+ const response = await client.send(new ListParentsCommand({ ChildId: childId }), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
27
28
  return response.Parents?.[0]?.Id;
28
29
  }
29
30
  catch (error) {
30
31
  const errorName = extractErrorName(error);
31
- if (errorName === "ChildNotFoundException") {
32
+ if (errorName === AWS_ERROR_NAMES.CHILD_NOT_FOUND) {
32
33
  return undefined;
33
34
  }
34
35
  throw error;
35
36
  }
36
37
  }
37
38
  /**
38
- * Ensure the specified organisational units exist under the root.
39
- * Creates any missing OUs. Returns a map of OU name (lowercase) → OU ID.
39
+ * Find or create an OU by name under a parent. Searches the provided
40
+ * `existingOUs` list first to avoid duplicate creation.
41
+ */
42
+ async function findOrCreateOU(client, parentId, ouName, existingOUs) {
43
+ const match = existingOUs.find((ou) => ou.Name === ouName);
44
+ if (match?.Id) {
45
+ return success(match.Id);
46
+ }
47
+ try {
48
+ const response = await client.send(new CreateOrganizationalUnitCommand({
49
+ Name: ouName,
50
+ ParentId: parentId
51
+ }), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
52
+ const ou = response.OrganizationalUnit;
53
+ if (!ou?.Id) {
54
+ return failure(new Error(`OU "${ouName}" was created but has no ID`));
55
+ }
56
+ return success(ou.Id);
57
+ }
58
+ catch (error) {
59
+ return failure(new Error(`Failed to create OU "${ouName}": ${getErrorMessage(error)}`));
60
+ }
61
+ }
62
+ /**
63
+ * Ensure flat OUs exist directly under root. Original logic, extracted.
64
+ */
65
+ async function ensureFlatOUs(client, rootId, ouNames) {
66
+ const ouMap = {};
67
+ if (ouNames.length === 0) {
68
+ return success(ouMap);
69
+ }
70
+ // Fetch existing OUs once for the root level
71
+ const existing = await listOUsForParent(client, rootId);
72
+ for (const ouName of ouNames) {
73
+ const capitalised = ouName.charAt(0).toUpperCase() + ouName.slice(1);
74
+ const result = await findOrCreateOU(client, rootId, capitalised, existing);
75
+ if (!result.success) {
76
+ return failure(result.error);
77
+ }
78
+ ouMap[ouName.toLowerCase()] = result.data;
79
+ // Add to existing list so subsequent iterations can see it
80
+ existing.push({ Id: result.data, Name: capitalised });
81
+ }
82
+ return success(ouMap);
83
+ }
84
+ /**
85
+ * Recursively ensure nested OUs exist. Populates `ouMap` with:
86
+ * - Dotted keys for nested OUs (e.g., "workloads.production")
87
+ * - Shorthand leaf-name keys when they don't collide with top-level keys
88
+ *
89
+ * When a shorthand key collides with a top-level key, only the dotted
90
+ * key is stored for the nested OU.
91
+ */
92
+ async function ensureNestedOUs(client, parentId, tree, ouMap, prefix, topLevelKeys) {
93
+ // Fetch existing OUs once per parent level
94
+ const existing = await listOUsForParent(client, parentId);
95
+ for (const [ouName, children] of Object.entries(tree)) {
96
+ const result = await findOrCreateOU(client, parentId, ouName, existing);
97
+ if (!result.success) {
98
+ return failure(result.error);
99
+ }
100
+ const ouId = result.data;
101
+ const dottedKey = prefix
102
+ ? `${prefix}.${ouName.toLowerCase()}`
103
+ : ouName.toLowerCase();
104
+ ouMap[dottedKey] = ouId;
105
+ // Add shorthand key if not colliding with a top-level key
106
+ if (prefix) {
107
+ const shorthand = ouName.toLowerCase();
108
+ if (!topLevelKeys.has(shorthand)) {
109
+ ouMap[shorthand] = ouId;
110
+ }
111
+ }
112
+ // Add to existing list so subsequent iterations can see it
113
+ existing.push({ Id: ouId, Name: ouName });
114
+ if (!isOULeaf(children)) {
115
+ const recurseResult = await ensureNestedOUs(client, ouId, children, ouMap, dottedKey, topLevelKeys);
116
+ if (!recurseResult.success) {
117
+ return recurseResult;
118
+ }
119
+ }
120
+ }
121
+ return success(undefined);
122
+ }
123
+ /**
124
+ * Ensure the specified organisational units exist. Accepts either:
125
+ * - `string[]` (flat): creates OUs directly under root (original behaviour)
126
+ * - `OUTree` (nested): creates a recursive OU hierarchy
127
+ *
128
+ * Returns a map of OU key (lowercase) to OU ID. For nested trees, keys
129
+ * use dot notation (e.g., "workloads.production") with shorthand aliases
130
+ * for leaf names that don't collide with top-level keys.
40
131
  *
41
132
  * Idempotent — existing OUs are adopted, not duplicated.
42
133
  *
43
- * @param ouNames Array of OU names to ensure exist (will be capitalised)
134
+ * AWS cannot move OUs between parents. Migrating from flat to nested
135
+ * creates new OUs under new parents; old empty OUs remain at root.
44
136
  */
45
- export async function ensureOrganisationalUnitsExist(client, rootId, ouNames) {
137
+ export async function ensureOrganisationalUnitsExist(client, rootId, ouConfig) {
46
138
  try {
139
+ if (Array.isArray(ouConfig)) {
140
+ return await ensureFlatOUs(client, rootId, ouConfig);
141
+ }
47
142
  const ouMap = {};
48
- for (const ouName of ouNames) {
49
- const capitalised = ouName.charAt(0).toUpperCase() + ouName.slice(1);
50
- // Check if OU already exists
51
- const existing = await listOUsForParent(client, rootId);
52
- const match = existing.find((ou) => ou.Name === capitalised);
53
- if (match?.Id) {
54
- ouMap[ouName.toLowerCase()] = match.Id;
55
- continue;
56
- }
57
- // Create the OU
58
- const response = await client.send(new CreateOrganizationalUnitCommand({
59
- Name: capitalised,
60
- ParentId: rootId
61
- }));
62
- const ou = response.OrganizationalUnit;
63
- if (!ou?.Id) {
64
- return failure(new Error(`OU "${capitalised}" was created but has no ID`));
65
- }
66
- ouMap[ouName.toLowerCase()] = ou.Id;
143
+ const topLevelKeys = new Set(Object.keys(ouConfig).map((k) => k.toLowerCase()));
144
+ const result = await ensureNestedOUs(client, rootId, ouConfig, ouMap, "", topLevelKeys);
145
+ if (!result.success) {
146
+ return failure(result.error);
67
147
  }
68
148
  return success(ouMap);
69
149
  }
70
150
  catch (error) {
71
- return failure(new Error(`Failed to ensure OUs exist: ${error instanceof Error ? error.message : String(error)}`));
151
+ return failure(new Error(`Failed to ensure OUs exist: ${getErrorMessage(error)}`));
72
152
  }
73
153
  }
154
+ /**
155
+ * Build a flat map of accountName (lowercase) to OU ID from an OUTree
156
+ * and its resolved OUMap.
157
+ *
158
+ * Walks the tree recursively. For each leaf (string[]), maps each account
159
+ * name to the parent OU's ID using the dotted OUMap key.
160
+ *
161
+ * Callers must pass account names that match the tree leaf values.
162
+ */
163
+ export function buildAccountToOUMap(tree, ouMap, prefix = "") {
164
+ const result = {};
165
+ for (const [ouName, children] of Object.entries(tree)) {
166
+ const dottedKey = prefix
167
+ ? `${prefix}.${ouName.toLowerCase()}`
168
+ : ouName.toLowerCase();
169
+ const ouId = ouMap[dottedKey];
170
+ if (isOULeaf(children)) {
171
+ if (ouId) {
172
+ for (const accountName of children) {
173
+ result[accountName.toLowerCase()] = ouId;
174
+ }
175
+ }
176
+ }
177
+ else {
178
+ Object.assign(result, buildAccountToOUMap(children, ouMap, dottedKey));
179
+ }
180
+ }
181
+ return result;
182
+ }
74
183
  /**
75
184
  * Place accounts into the correct organisational units.
76
185
  * Skips accounts with environment "root" and accounts already in the target OU.
77
186
  *
187
+ * When `accountToOU` is provided, looks up the target OU by account name
188
+ * (lowercase) instead of by environment. This supports tree-based placement
189
+ * where the OU is determined by which leaf the account appears in.
190
+ *
78
191
  * Idempotent — accounts already in the correct OU are counted but not moved.
79
192
  */
80
- export async function placeAccountsInOUs(client, ouMap, accounts) {
193
+ export async function placeAccountsInOUs(client, ouMap, accounts, accountToOU) {
81
194
  try {
82
195
  if (accounts.length === 0) {
83
196
  return success({ moved: 0, alreadyPlaced: 0 });
@@ -88,13 +201,14 @@ export async function placeAccountsInOUs(client, ouMap, accounts) {
88
201
  if (account.environment === "root") {
89
202
  continue;
90
203
  }
91
- const targetOuId = ouMap[account.environment.toLowerCase()];
204
+ const targetOuId = accountToOU
205
+ ? accountToOU[account.name.toLowerCase()]
206
+ : ouMap[account.environment.toLowerCase()];
92
207
  if (!targetOuId) {
93
208
  continue;
94
209
  }
95
210
  const currentParentId = await getParentId(client, account.id);
96
211
  if (!currentParentId) {
97
- // Account not found in organisation
98
212
  continue;
99
213
  }
100
214
  if (currentParentId === targetOuId) {
@@ -106,12 +220,12 @@ export async function placeAccountsInOUs(client, ouMap, accounts) {
106
220
  AccountId: account.id,
107
221
  SourceParentId: currentParentId,
108
222
  DestinationParentId: targetOuId
109
- }));
223
+ }), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
110
224
  moved++;
111
225
  }
112
226
  catch (error) {
113
227
  const errorName = extractErrorName(error);
114
- if (errorName === "AccountNotFoundException") {
228
+ if (errorName === AWS_ERROR_NAMES.ACCOUNT_NOT_FOUND) {
115
229
  continue;
116
230
  }
117
231
  throw error;
@@ -120,6 +234,6 @@ export async function placeAccountsInOUs(client, ouMap, accounts) {
120
234
  return success({ moved, alreadyPlaced });
121
235
  }
122
236
  catch (error) {
123
- return failure(new Error(`Failed to place accounts in OUs: ${error instanceof Error ? error.message : String(error)}`));
237
+ return failure(new Error(`Failed to place accounts in OUs: ${getErrorMessage(error)}`));
124
238
  }
125
239
  }
@@ -1,6 +1,7 @@
1
1
  import { EnablePolicyTypeCommand, PolicyType } from "@aws-sdk/client-organizations";
2
2
  import { success, failure } from "@fjall/generator";
3
- import { extractErrorName } from "./types.js";
3
+ import { getErrorMessage } from "@fjall/util";
4
+ import { extractErrorName, SDK_TIMEOUT_MS } from "./types.js";
4
5
  const POLICY_TYPES = [
5
6
  PolicyType.SERVICE_CONTROL_POLICY,
6
7
  PolicyType.TAG_POLICY,
@@ -18,7 +19,7 @@ export async function enablePolicyTypes(client, rootId) {
18
19
  await client.send(new EnablePolicyTypeCommand({
19
20
  RootId: rootId,
20
21
  PolicyType: policyType
21
- }));
22
+ }), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
22
23
  }
23
24
  catch (error) {
24
25
  const errorName = extractErrorName(error);
@@ -31,6 +32,6 @@ export async function enablePolicyTypes(client, rootId) {
31
32
  return success(undefined);
32
33
  }
33
34
  catch (error) {
34
- return failure(new Error(`Failed to enable policy types: ${error instanceof Error ? error.message : String(error)}`));
35
+ return failure(new Error(`Failed to enable policy types: ${getErrorMessage(error)}`));
35
36
  }
36
37
  }
@@ -1,15 +1,19 @@
1
1
  import { EnableSharingWithAwsOrganizationCommand } from "@aws-sdk/client-ram";
2
2
  import { success, failure } from "@fjall/generator";
3
+ import { getErrorMessage } from "@fjall/util";
4
+ import { SDK_TIMEOUT_MS } from "./types.js";
3
5
  /**
4
6
  * Enable RAM sharing with the AWS Organisation.
5
7
  * Idempotent — calling when already enabled is a no-op.
6
8
  */
7
9
  export async function enableRamSharing(client) {
8
10
  try {
9
- await client.send(new EnableSharingWithAwsOrganizationCommand({}));
11
+ await client.send(new EnableSharingWithAwsOrganizationCommand({}), {
12
+ abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS)
13
+ });
10
14
  return success(undefined);
11
15
  }
12
16
  catch (error) {
13
- return failure(new Error(`Failed to enable RAM sharing: ${error instanceof Error ? error.message : String(error)}`));
17
+ return failure(new Error(`Failed to enable RAM sharing: ${getErrorMessage(error)}`));
14
18
  }
15
19
  }
@@ -1,13 +1,19 @@
1
1
  import { EnableAWSServiceAccessCommand } from "@aws-sdk/client-organizations";
2
2
  import { success, failure } from "@fjall/generator";
3
- import { extractErrorName } from "./types.js";
3
+ import { extractErrorName, SDK_TIMEOUT_MS, AWS_ERROR_NAMES } from "./types.js";
4
+ import { getErrorMessage } from "@fjall/util";
4
5
  const SERVICE_PRINCIPALS = [
5
6
  "account.amazonaws.com",
6
7
  "sso.amazonaws.com",
7
8
  "ipam.amazonaws.com",
8
9
  "ram.amazonaws.com",
9
10
  "backup.amazonaws.com",
10
- "member.org.stacksets.cloudformation.amazonaws.com"
11
+ "member.org.stacksets.cloudformation.amazonaws.com",
12
+ "guardduty.amazonaws.com",
13
+ "securityhub.amazonaws.com",
14
+ "config.amazonaws.com",
15
+ "inspector2.amazonaws.com",
16
+ "access-analyzer.amazonaws.com"
11
17
  ];
12
18
  /**
13
19
  * Enable AWS service access for all required service principals.
@@ -19,20 +25,20 @@ export async function enableServiceAccess(client) {
19
25
  try {
20
26
  await client.send(new EnableAWSServiceAccessCommand({
21
27
  ServicePrincipal: principal
22
- }));
28
+ }), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
23
29
  }
24
30
  catch (error) {
25
31
  const errorName = extractErrorName(error);
26
- if (errorName === "AccessDeniedException") {
32
+ if (errorName === AWS_ERROR_NAMES.ACCESS_DENIED) {
27
33
  return failure(new Error(`Access denied when enabling service access for ${principal}. ` +
28
34
  "Ensure your credentials have organizations:EnableAWSServiceAccess permission."));
29
35
  }
30
- throw error;
36
+ throw new Error(`Service principal ${principal}: ${getErrorMessage(error)}`);
31
37
  }
32
38
  }
33
39
  return success(undefined);
34
40
  }
35
41
  catch (error) {
36
- return failure(new Error(`Failed to enable service access: ${error instanceof Error ? error.message : String(error)}`));
42
+ return failure(new Error(`Failed to enable service access: ${getErrorMessage(error)}`));
37
43
  }
38
44
  }
@@ -1,15 +1,19 @@
1
1
  import { ActivateOrganizationsAccessCommand } from "@aws-sdk/client-cloudformation";
2
2
  import { success, failure } from "@fjall/generator";
3
+ import { getErrorMessage } from "@fjall/util";
4
+ import { SDK_TIMEOUT_MS } from "./types.js";
3
5
  /**
4
6
  * Activate trusted access for CloudFormation StackSets.
5
7
  * Idempotent — calling when already activated is a no-op.
6
8
  */
7
9
  export async function activateTrustedAccess(client) {
8
10
  try {
9
- await client.send(new ActivateOrganizationsAccessCommand({}));
11
+ await client.send(new ActivateOrganizationsAccessCommand({}), {
12
+ abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS)
13
+ });
10
14
  return success(undefined);
11
15
  }
12
16
  catch (error) {
13
- return failure(new Error(`Failed to activate trusted access: ${error instanceof Error ? error.message : String(error)}`));
17
+ return failure(new Error(`Failed to activate trusted access: ${getErrorMessage(error)}`));
14
18
  }
15
19
  }
@@ -1,6 +1,14 @@
1
1
  /**
2
- * Shared types for AWS Organisation setup primitives.
2
+ * Shared types and constants for AWS Organisation setup primitives.
3
3
  */
4
+ export declare const SDK_TIMEOUT_MS = 30000;
5
+ export declare const AWS_ERROR_NAMES: {
6
+ readonly ACCESS_DENIED: "AccessDeniedException";
7
+ readonly ACCOUNT_ALREADY_REGISTERED: "AccountAlreadyRegisteredException";
8
+ readonly ACCOUNT_NOT_FOUND: "AccountNotFoundException";
9
+ readonly CHILD_NOT_FOUND: "ChildNotFoundException";
10
+ readonly ORGS_NOT_IN_USE: "AWSOrganizationsNotInUseException";
11
+ };
4
12
  export interface OrgDetails {
5
13
  orgId: string;
6
14
  rootId: string;
@@ -22,6 +30,20 @@ export interface IdentityCentreStatus {
22
30
  enabled: boolean;
23
31
  instanceCount: number;
24
32
  }
33
+ /**
34
+ * Recursive OU hierarchy. Each key is an OU name.
35
+ * - string[] value = account names to place in that OU (leaf node)
36
+ * - OUTree value = child OUs (branch node, no direct accounts)
37
+ *
38
+ * AWS cannot move OUs between parents. Migrating from a flat string[]
39
+ * to a nested OUTree creates new OUs under new parents; old empty OUs
40
+ * at the root level remain and must be removed manually.
41
+ */
42
+ export type OUTree = {
43
+ [ouName: string]: string[] | OUTree;
44
+ };
45
+ /** Returns true if the value is a list of account names (leaf node). */
46
+ export declare function isOULeaf(value: string[] | OUTree): value is string[];
25
47
  /**
26
48
  * Extract the error name from an AWS SDK error.
27
49
  * AWS SDK v3 errors have a `name` property that identifies the error type.
@@ -1,16 +1 @@
1
- /**
2
- * Shared types for AWS Organisation setup primitives.
3
- */
4
- /**
5
- * Extract the error name from an AWS SDK error.
6
- * AWS SDK v3 errors have a `name` property that identifies the error type.
7
- */
8
- export function extractErrorName(error) {
9
- if (typeof error === "object" &&
10
- error !== null &&
11
- "name" in error &&
12
- typeof error.name === "string") {
13
- return error.name;
14
- }
15
- return "UnknownError";
16
- }
1
+ const t=3e4,e={ACCESS_DENIED:"AccessDeniedException",ACCOUNT_ALREADY_REGISTERED:"AccountAlreadyRegisteredException",ACCOUNT_NOT_FOUND:"AccountNotFoundException",CHILD_NOT_FOUND:"ChildNotFoundException",ORGS_NOT_IN_USE:"AWSOrganizationsNotInUseException"};function o(n){return Array.isArray(n)}function E(n){return typeof n=="object"&&n!==null&&"name"in n&&typeof n.name=="string"?n.name:"UnknownError"}export{e as AWS_ERROR_NAMES,t as SDK_TIMEOUT_MS,E as extractErrorName,o as isOULeaf};
@@ -0,0 +1,6 @@
1
+ import { vi } from "vitest";
2
+ import type { EventLogWriter } from "../cloudformationEventTypes.js";
3
+ export declare function createMockEventLogger(): EventLogWriter;
4
+ export declare function createMockAwsClient(sendFn: ReturnType<typeof vi.fn>): {
5
+ getClient: import("vitest").Mock<(...args: any[]) => any>;
6
+ };
@@ -0,0 +1 @@
1
+ import{vi as e}from"vitest";function r(){return{writeEvent:e.fn(),writeDeploymentEnd:e.fn(),writeFailureSummary:e.fn(),writeFailureAnalysis:e.fn(),writeCdkOutput:e.fn(),getLogPath:e.fn(),getLogSummary:e.fn()}}function i(t){return{getClient:e.fn().mockReturnValue({send:t})}}export{i as createMockAwsClient,r as createMockEventLogger};
@@ -0,0 +1,48 @@
1
+ import type { ResourceEvent } from "@fjall/util/aws";
2
+ import { type EventLogWriter, type EventFailureAnalyser, type AwsClientLike } from "./cloudformationEventTypes.js";
3
+ import type { FailureAnalysis } from "@fjall/util/aws";
4
+ export declare function isTerminalState(status: string): boolean;
5
+ export declare function isSuccessState(status: string): boolean;
6
+ export declare function isIpamResource(resourceType: string): boolean;
7
+ /** Tracks in-progress IPAM resources for concurrency diagnostics */
8
+ export declare class IpamConcurrencyTracker {
9
+ private inProgress;
10
+ get size(): number;
11
+ clear(): void;
12
+ /**
13
+ * Process an IPAM resource event — logs concurrency info and writes
14
+ * diagnostic metadata to the event logger when provided.
15
+ */
16
+ trackEvent(event: ResourceEvent, eventLogger: EventLogWriter | null): void;
17
+ }
18
+ /** Mutable state passed into pollStackEvents from the monitor class */
19
+ export interface PollContext {
20
+ aws: AwsClientLike;
21
+ seenEventIds: Set<string>;
22
+ activeNestedStacks: Map<string, string>;
23
+ failureReasons: Map<string, string>;
24
+ eventHistory: Map<string, ResourceEvent[]>;
25
+ maxHistorySize: number;
26
+ ipamTracker: IpamConcurrencyTracker;
27
+ eventLogger: EventLogWriter | null;
28
+ }
29
+ /** Poll CloudFormation for new stack events and dispatch them to the callback */
30
+ export declare function pollStackEvents(ctx: PollContext, stackName: string, onResourceUpdate: (event: ResourceEvent) => void): Promise<boolean | "throttled">;
31
+ /** Result of handling stack completion */
32
+ export interface StackCompletionResult {
33
+ success: boolean;
34
+ failureMessage?: string;
35
+ analysis: FailureAnalysis | null;
36
+ }
37
+ /**
38
+ * Handle stack completion — determines success/failure, logs results,
39
+ * and runs failure analysis. Called after pollEvents detects a terminal state.
40
+ */
41
+ export declare function handleStackCompletion(aws: AwsClientLike, stackName: string, failureReasons: Map<string, string>, eventLogger: EventLogWriter | null, failureAnalyser: EventFailureAnalyser | null, eventHistory: Map<string, ResourceEvent[]>): Promise<StackCompletionResult>;
42
+ /** Fetch the current status of a CloudFormation stack */
43
+ export declare function fetchStackStatus(aws: AwsClientLike, stackName: string): Promise<{
44
+ status: string;
45
+ statusReason?: string;
46
+ } | null>;
47
+ /** Fetch the latest event for each resource in a stack */
48
+ export declare function fetchCurrentResources(aws: AwsClientLike, stackName: string): Promise<ResourceEvent[]>;
@@ -0,0 +1 @@
1
+ import{CloudFormationClient as g,DescribeStackEventsCommand as y,DescribeStacksCommand as A}from"@aws-sdk/client-cloudformation";import{logger as m}from"@fjall/util/logger";import{getErrorMessage as S,maskSensitiveOutput as C}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as f}from"@fjall/util/aws";import{CF_STACK_RESOURCE_TYPE as I}from"./cloudformationEventTypes.js";import{extractErrorName as P}from"../organisations/types.js";const L=["CREATE_COMPLETE","CREATE_FAILED","DELETE_COMPLETE","DELETE_FAILED","UPDATE_COMPLETE","UPDATE_FAILED","UPDATE_ROLLBACK_COMPLETE","UPDATE_ROLLBACK_FAILED","ROLLBACK_COMPLETE","ROLLBACK_FAILED","IMPORT_COMPLETE","IMPORT_ROLLBACK_COMPLETE","IMPORT_ROLLBACK_FAILED"],h=["CREATE_COMPLETE","UPDATE_COMPLETE","DELETE_COMPLETE","IMPORT_COMPLETE"];function k(s){return L.includes(s)}function K(s){return h.includes(s)}const O=new Set(["AWS::EC2::IPAMPool","AWS::EC2::IPAMPoolCidr","AWS::EC2::IPAM"]);function M(s){return O.has(s)}class b{inProgress=new Map;get size(){return this.inProgress.size}clear(){this.inProgress.clear()}trackEvent(e,i){if(e.status.includes("IN_PROGRESS")){this.inProgress.set(e.logicalId,{resourceType:e.resourceType,startTime:e.timestamp});const o=Array.from(this.inProgress.entries()).map(([a,r])=>`${a} (${r.resourceType.split("::").pop()}, started ${r.startTime.toISOString()})`);m.debug("CloudFormation","IPAM resource started",{logicalId:e.logicalId,resourceType:e.resourceType,concurrentIpamCount:this.inProgress.size,concurrentIpamResources:o}),i&&i.writeEvent(e,{phase:"ipam_concurrency",isMainStack:!0,parentStack:`concurrent=${this.inProgress.size}: ${o.join(", ")}`})}else if(e.status.includes("COMPLETE")||e.status.includes("FAILED")){const o=this.inProgress.has(e.logicalId),a=this.inProgress.get(e.logicalId);this.inProgress.delete(e.logicalId);const r=a?e.timestamp.getTime()-a.startTime.getTime():void 0;if(m.debug("CloudFormation",`IPAM resource ${e.status}`,{logicalId:e.logicalId,resourceType:e.resourceType,status:e.status,statusReason:e.statusReason,durationMs:r,wasTracked:o,remainingIpamCount:this.inProgress.size,remainingIpamResources:Array.from(this.inProgress.keys())}),e.status.includes("FAILED")&&i){const l=Array.from(this.inProgress.entries()).map(([n,c])=>({logicalId:n,resourceType:c.resourceType,startTime:c.startTime.toISOString(),inFlightDurationMs:e.timestamp.getTime()-c.startTime.getTime()}));i.writeEvent(e,{phase:"ipam_failure_snapshot",isMainStack:!0,parentStack:JSON.stringify({failedResource:e.logicalId,reason:e.statusReason,durationMs:r,concurrentIpamAtFailure:l})})}}}}async function v(s,e,i){try{const o=s.aws.getClient(g);let a,r=!1,l=!1;do{const n=new y({StackName:e,...a?{NextToken:a}:{}}),c=await o.send(n);if(!c.StackEvents)break;for(const t of c.StackEvents){const d=`${t.EventId}`;if(s.seenEventIds.has(d)){l=!0;continue}if(s.seenEventIds.add(d),t.ResourceType===I&&t.LogicalResourceId!==e){const E=t.ResourceStatus||"",T=t.LogicalResourceId||"";E.includes("IN_PROGRESS")?s.activeNestedStacks.set(T,E):(E.includes("COMPLETE")||E.includes("FAILED"))&&s.activeNestedStacks.delete(T)}if(t.LogicalResourceId===e&&t.ResourceType===I){const E=t.ResourceStatus||"";k(E)&&(r=!0)}t.ResourceStatus&&t.ResourceStatus.includes("FAILED")&&t.ResourceStatusReason&&(s.failureReasons.set(t.LogicalResourceId||"unknown",t.ResourceStatusReason),m.debug("CloudFormation",`Captured failure: ${t.LogicalResourceId} - ${t.ResourceStatusReason}`));const u={logicalId:t.LogicalResourceId||"",physicalId:t.PhysicalResourceId,resourceType:t.ResourceType||"",status:t.ResourceStatus||"",statusReason:t.ResourceStatusReason,timestamp:t.Timestamp||new Date},p=u.logicalId,R=s.eventHistory.get(p)??[];if(R.push(u),s.eventHistory.set(p,R),s.eventHistory.size>s.maxHistorySize){const E=[...s.eventHistory.keys()].slice(0,s.eventHistory.size-s.maxHistorySize);for(const T of E)s.eventHistory.delete(T)}M(u.resourceType)&&s.ipamTracker.trackEvent(u,s.eventLogger),s.eventLogger&&s.eventLogger.writeEvent(u),m.debug("CloudFormation","pollEvents delivering event",{stackName:e,logicalId:u.logicalId,status:u.status,resourceType:u.resourceType}),i(u)}a=l?void 0:c.NextToken}while(a);return r&&s.activeNestedStacks.size>0?!1:r}catch(o){const a=P(o),r=S(o);return a==="ValidationError"&&r.includes(f)?!1:a==="Throttling"||a==="TooManyRequestsException"||a==="ThrottlingException"?"throttled":(r.includes(f)||m.error("CloudFormation","Unexpected polling error",{error:C(r)}),!1)}}async function $(s,e,i,o,a,r){const l=await _(s,e),n=l?.status||"UNKNOWN",c=n.endsWith("_COMPLETE")&&!n.includes("ROLLBACK")&&!n.includes("FAILED");let t=l?.statusReason;!c&&i.size>0&&(t=(Array.from(i.values())[0]??t)||t);let d=null;return o&&(o.writeDeploymentEnd(c,n,t),!c&&i.size>0&&o.writeFailureSummary(Array.from(i.entries()).map(([u,p])=>({logicalId:u,reason:p}))),!c&&a&&(d=a.analyseFailure(r),d&&o.writeFailureAnalysis({rootCause:d.rootCause.reason,failedResources:d.affectedResources.map(u=>({logicalId:u.logicalId,resourceType:u.resourceType,reason:u.statusReason||"Unknown reason"})),dependencyChain:d.dependencyChain,remediation:d.remediation}))),{success:c,failureMessage:t,analysis:d}}async function _(s,e){try{const i=new A({StackName:e}),r=(await s.getClient(g).send(i)).Stacks?.[0];return r?{status:r.StackStatus||"UNKNOWN",statusReason:r.StackStatusReason}:null}catch(i){return m.debug("CloudFormation","getStackStatus failed",{error:S(i)}),null}}async function H(s,e){const i=[];try{const o=new y({StackName:e}),r=await s.getClient(g).send(o);if(!r.StackEvents)return i;const l=new Map;for(const n of r.StackEvents){const c=n.LogicalResourceId||"";if(c===e&&n.ResourceType===I)continue;const t=l.get(c);(!t||n.Timestamp&&t.timestamp<n.Timestamp)&&l.set(c,{logicalId:c,physicalId:n.PhysicalResourceId,resourceType:n.ResourceType||"",status:n.ResourceStatus||"",statusReason:n.ResourceStatusReason,timestamp:n.Timestamp||new Date})}return Array.from(l.values())}catch(o){const a=P(o),r=S(o);return a==="ValidationError"&&r.includes(f)||r.includes(f)||m.error("CloudFormation","Error getting current resources",{error:C(S(o))}),i}}export{b as IpamConcurrencyTracker,H as fetchCurrentResources,_ as fetchStackStatus,$ as handleStackCompletion,M as isIpamResource,K as isSuccessState,k as isTerminalState,v as pollStackEvents};
@@ -0,0 +1,45 @@
1
+ import type { AwsSdkClientConstructor } from "../AwsProvider.js";
2
+ import type { ResourceEvent, FailureAnalysis } from "@fjall/util/aws";
3
+ /** Minimal AWS interface — only getClient() is needed for event monitoring */
4
+ export interface AwsClientLike {
5
+ getClient<T>(ClientClass: AwsSdkClientConstructor<T>): T;
6
+ }
7
+ /** Interface for event logging — decouples from concrete CloudFormationEventLogger */
8
+ export interface EventLogWriter {
9
+ writeEvent(event: ResourceEvent, metadata?: {
10
+ phase?: string;
11
+ isMainStack?: boolean;
12
+ parentStack?: string;
13
+ }): void;
14
+ writeDeploymentEnd(success: boolean, finalStatus: string, failureMessage?: string): void;
15
+ writeFailureSummary(failures: Array<{
16
+ logicalId: string;
17
+ reason: string;
18
+ }>): void;
19
+ writeFailureAnalysis(analysis: {
20
+ rootCause: string;
21
+ failedResources: Array<{
22
+ logicalId: string;
23
+ resourceType: string;
24
+ reason: string;
25
+ }>;
26
+ dependencyChain?: string[];
27
+ remediation?: string[];
28
+ }): void;
29
+ writeCdkOutput(stream: "stdout" | "stderr", chunk: string): void;
30
+ getLogPath(): string;
31
+ getLogSummary(): string;
32
+ }
33
+ /** Interface for failure analysis — decouples from concrete CloudFormationFailureAnalyser */
34
+ export interface EventFailureAnalyser {
35
+ analyseFailure(eventHistory: Map<string, ResourceEvent[]>): FailureAnalysis | null;
36
+ }
37
+ /** Factory to create an EventLogWriter for a specific deployment */
38
+ export type EventLogWriterFactory = (deploymentId: string, stackName: string, region: string, deploymentName?: string) => EventLogWriter;
39
+ /** Dependencies injected into CloudFormationEventMonitor */
40
+ export interface EventMonitorDeps {
41
+ failureAnalyser?: EventFailureAnalyser;
42
+ eventLogWriterFactory?: EventLogWriterFactory;
43
+ }
44
+ /** CloudFormation resource type for nested stacks */
45
+ export declare const CF_STACK_RESOURCE_TYPE = "AWS::CloudFormation::Stack";
@@ -0,0 +1 @@
1
+ const o="AWS::CloudFormation::Stack";export{o as CF_STACK_RESOURCE_TYPE};
@@ -1,53 +1,7 @@
1
- import type { AwsProvider } from "../AwsProvider.js";
2
- import type { FailureAnalysis } from "./CloudFormationFailureAnalyser.js";
3
- export declare const STACK_NOT_FOUND_PATTERN = "does not exist";
4
- export declare const CDK_NO_STACKS_MATCH = "No stacks match the name(s)";
5
- export interface ResourceEvent {
6
- logicalId: string;
7
- physicalId?: string;
8
- resourceType: string;
9
- status: string;
10
- statusReason?: string;
11
- timestamp: Date;
12
- }
13
- export declare function isResourceEvent(event: unknown): event is ResourceEvent;
14
- /** Interface for event logging — decouples from concrete CloudFormationEventLogger */
15
- export interface EventLogWriter {
16
- writeEvent(event: ResourceEvent, metadata?: {
17
- phase?: string;
18
- isMainStack?: boolean;
19
- parentStack?: string;
20
- }): void;
21
- writeDeploymentEnd(success: boolean, finalStatus: string, failureMessage?: string): void;
22
- writeFailureSummary(failures: Array<{
23
- logicalId: string;
24
- reason: string;
25
- }>): void;
26
- writeFailureAnalysis(analysis: {
27
- rootCause: string;
28
- failedResources: Array<{
29
- logicalId: string;
30
- resourceType: string;
31
- reason: string;
32
- }>;
33
- dependencyChain?: string[];
34
- remediation?: string[];
35
- }): void;
36
- writeCdkOutput(stream: "stdout" | "stderr", chunk: string): void;
37
- getLogPath(): string;
38
- getLogSummary(): string;
39
- }
40
- /** Interface for failure analysis — decouples from concrete CloudFormationFailureAnalyser */
41
- export interface EventFailureAnalyser {
42
- analyseFailure(eventHistory: Map<string, ResourceEvent[]>): FailureAnalysis | null;
43
- }
44
- /** Factory to create an EventLogWriter for a specific deployment */
45
- export type EventLogWriterFactory = (deploymentId: string, stackName: string, region: string, deploymentName?: string) => EventLogWriter;
46
- /** Dependencies injected into CloudFormationEventMonitor */
47
- export interface EventMonitorDeps {
48
- failureAnalyser?: EventFailureAnalyser;
49
- eventLogWriterFactory?: EventLogWriterFactory;
50
- }
1
+ import { type ResourceEvent, type FailureAnalysis } from "@fjall/util/aws";
2
+ export { type ResourceEvent, isResourceEvent, STACK_NOT_FOUND_PATTERN, CDK_NO_STACKS_MATCH, type FailureAnalysis } from "@fjall/util/aws";
3
+ export type { AwsClientLike, EventLogWriter, EventFailureAnalyser, EventLogWriterFactory, EventMonitorDeps } from "./cloudformationEventTypes.js";
4
+ import { type AwsClientLike, type EventLogWriter, type EventMonitorDeps } from "./cloudformationEventTypes.js";
51
5
  export declare class CloudFormationEventMonitor {
52
6
  private aws;
53
7
  private seenEventIds;
@@ -63,12 +17,13 @@ export declare class CloudFormationEventMonitor {
63
17
  private deploymentStartTime;
64
18
  private maxHistorySize;
65
19
  private maxSeenEventIds;
66
- private ipamInProgress;
67
- constructor(aws: AwsProvider, deps?: EventMonitorDeps);
20
+ private ipamTracker;
21
+ constructor(aws: AwsClientLike, deps?: EventMonitorDeps);
68
22
  enableLogging(deploymentId: string, stackName: string, region: string, deploymentName?: string): void;
69
23
  startMonitoring(stackName: string, onResourceUpdate: (event: ResourceEvent) => void, onStackComplete?: (success: boolean, message?: string) => void): Promise<void>;
70
24
  stopMonitoring(): void;
71
25
  private cleanup;
26
+ private handleStackComplete;
72
27
  getResourceHistory(logicalId: string): ResourceEvent[];
73
28
  getEventHistory(): Map<string, ResourceEvent[]>;
74
29
  getFailureAnalysis(): FailureAnalysis | null;
@@ -87,8 +42,7 @@ export declare class CloudFormationEventMonitor {
87
42
  failureReason?: string;
88
43
  logPath?: string;
89
44
  }>;
90
- private isTerminalState;
91
- private isSuccessState;
45
+ private pollContext;
92
46
  private pollEvents;
93
47
  getStackStatus(stackName: string): Promise<{
94
48
  status: string;