@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
package/LICENSE CHANGED
@@ -1,21 +1,50 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Fjall
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ Fjall Proprietary Software Licence
2
+
3
+ Copyright (c) 2026 Fjall. All rights reserved.
4
+
5
+ This software, including all source, object, bundled, and minified forms
6
+ ("the Software"), is the proprietary and confidential property of Fjall.
7
+
8
+ 1. Permitted Use. Subject to the terms of this Licence, Fjall grants you
9
+ a non-exclusive, non-transferable, revocable licence to install the
10
+ Software via the npm registry and to execute it solely for the purpose
11
+ of deploying, operating, and managing your own applications and
12
+ infrastructure on cloud providers.
13
+
14
+ 2. Restrictions. You may NOT, and may not permit any third party to:
15
+ (a) copy, redistribute, sublicense, sell, rent, lease, or otherwise
16
+ transfer the Software;
17
+ (b) modify, adapt, translate, or create derivative works of the Software;
18
+ (c) reverse engineer, decompile, disassemble, deminify, or otherwise
19
+ attempt to derive the source code, structure, or organisation of
20
+ the Software, except to the minimum extent expressly permitted by
21
+ applicable mandatory law;
22
+ (d) use the Software, or any portion of it, to develop, train, or
23
+ improve any product or service that competes with Fjall;
24
+ (e) remove, alter, or obscure any proprietary notices contained in
25
+ the Software;
26
+ (f) publish, share, or otherwise disclose the Software or its contents
27
+ to any third party.
28
+
29
+ 3. Ownership. All right, title, and interest in and to the Software,
30
+ including all intellectual property rights, remain with Fjall. No
31
+ rights are granted except as expressly set out in this Licence.
32
+
33
+ 4. Termination. This Licence terminates automatically if you breach any
34
+ of its terms. Upon termination you must cease all use of the Software
35
+ and destroy all copies in your possession.
36
+
37
+ 5. Disclaimer of Warranty. THE SOFTWARE IS PROVIDED "AS IS" WITHOUT
38
+ WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
39
+ THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
40
+ AND NON-INFRINGEMENT.
41
+
42
+ 6. Limitation of Liability. IN NO EVENT SHALL FJALL BE LIABLE FOR ANY
43
+ INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES
44
+ ARISING OUT OF OR RELATED TO THE SOFTWARE, EVEN IF ADVISED OF THE
45
+ POSSIBILITY OF SUCH DAMAGES.
46
+
47
+ 7. Governing Law. This Licence is governed by the laws of England and
48
+ Wales, without regard to conflict of laws principles.
49
+
50
+ For commercial licensing enquiries, contact: contact@fjall.io
package/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # @fjall/deploy-core
2
+
3
+ Shared deployment engine for Fjall. Used by both `@fjall/cli` and the Fjall webapp deployment worker to provide a single source of truth for build, synth, and deploy orchestration across CDK stacks.
4
+
5
+ Provides `AwsProvider`, `FrameworkBuilder`, `StepRegistry`, and the `DeployCallbacks` contract. Pure orchestration — environment-agnostic and free of UI concerns.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @fjall/deploy-core
11
+ ```
12
+
13
+ ## Subpath exports
14
+
15
+ ```typescript
16
+ import { AwsProvider } from "@fjall/deploy-core/aws";
17
+ import { FrameworkBuilder } from "@fjall/deploy-core/builders";
18
+ import { StepRegistry } from "@fjall/deploy-core/steps";
19
+ ```
20
+
21
+ See [docs.fjall.io](https://docs.fjall.io) for full usage.
22
+
23
+ ## Licence
24
+
25
+ Proprietary — see [LICENSE](./LICENSE).
package/dist/.minified ADDED
@@ -0,0 +1 @@
1
+ 114 files minified at 2026-04-20T23:47:40.999Z
@@ -0,0 +1,20 @@
1
+ import { vi } from "vitest";
2
+ /**
3
+ * Create an AWS error with the correct name property for SDK v3 error matching.
4
+ * AWS SDK v3 checks error.name for typed error handling (e.g. "AccountAlreadyRegisteredException").
5
+ * Object.defineProperty is required because Error.name is non-writable by default.
6
+ */
7
+ export declare function createAwsError(name: string, message: string): Error;
8
+ /**
9
+ * Create a mock AWS SDK v3 client with a spied `send` method.
10
+ * The returned object is typed as the specified client type for test convenience.
11
+ *
12
+ * Usage:
13
+ * ```ts
14
+ * const mockClient = createMockAwsClient<OrganizationsClient>();
15
+ * vi.mocked(mockClient.send).mockResolvedValue({ ... });
16
+ * ```
17
+ */
18
+ export declare function createMockAwsClient<T>(): T & {
19
+ send: ReturnType<typeof vi.fn>;
20
+ };
@@ -0,0 +1 @@
1
+ import{vi as n}from"vitest";function c(e,t){const r=new Error(t);return Object.defineProperty(r,"name",{value:e,writable:!0}),r}function i(){return{send:n.fn()}}export{c as createAwsError,i as createMockAwsClient};
@@ -0,0 +1 @@
1
+ export { createAwsError, createMockAwsClient } from "./awsMockHelpers.js";
@@ -0,0 +1 @@
1
+ import{createAwsError as o,createMockAwsClient as t}from"./awsMockHelpers.js";export{o as createAwsError,t as createMockAwsClient};
@@ -1 +0,0 @@
1
- export {};
@@ -1,70 +1 @@
1
- import { STSClient, AssumeRoleCommand } from "@aws-sdk/client-sts";
2
- /**
3
- * Minimal AWS provider for pre-assumed credentials.
4
- *
5
- * Used by the webapp worker, which assumes an OIDC role before
6
- * calling deploy-core. No SSO, no credential discovery, no caching.
7
- */
8
- export class SimpleAwsProvider {
9
- credentials;
10
- region;
11
- accountId;
12
- clientCache = new Map();
13
- constructor(awsCredentials) {
14
- this.credentials = {
15
- accessKeyId: awsCredentials.accessKeyId,
16
- secretAccessKey: awsCredentials.secretAccessKey,
17
- sessionToken: awsCredentials.sessionToken
18
- };
19
- this.region = awsCredentials.region;
20
- this.accountId = awsCredentials.accountId;
21
- }
22
- getClient(ClientClass) {
23
- const key = ClientClass.name;
24
- const cached = this.clientCache.get(key);
25
- if (cached !== undefined)
26
- return cached;
27
- const client = new ClientClass({
28
- region: this.region,
29
- credentials: this.credentials
30
- });
31
- this.clientCache.set(key, client);
32
- return client;
33
- }
34
- getRegion() {
35
- return this.region;
36
- }
37
- getAccountId() {
38
- return this.accountId;
39
- }
40
- getCredentials() {
41
- return this.credentials;
42
- }
43
- exportToEnv() {
44
- process.env.AWS_ACCESS_KEY_ID = this.credentials.accessKeyId;
45
- process.env.AWS_SECRET_ACCESS_KEY = this.credentials.secretAccessKey;
46
- process.env.AWS_REGION = this.region;
47
- if (this.credentials.sessionToken) {
48
- process.env.AWS_SESSION_TOKEN = this.credentials.sessionToken;
49
- }
50
- }
51
- async assumeRole(roleArn, sessionName) {
52
- const stsClient = new STSClient({
53
- region: this.region,
54
- credentials: this.credentials
55
- });
56
- const response = await stsClient.send(new AssumeRoleCommand({
57
- RoleArn: roleArn,
58
- RoleSessionName: sessionName
59
- }), { abortSignal: AbortSignal.timeout(30_000) });
60
- const assumed = response.Credentials;
61
- if (!assumed?.AccessKeyId || !assumed?.SecretAccessKey) {
62
- throw new Error(`AssumeRole for ${roleArn} returned incomplete credentials`);
63
- }
64
- return {
65
- accessKeyId: assumed.AccessKeyId,
66
- secretAccessKey: assumed.SecretAccessKey,
67
- sessionToken: assumed.SessionToken
68
- };
69
- }
70
- }
1
+ import{STSClient as i,AssumeRoleCommand as r}from"@aws-sdk/client-sts";import{NodeHttpHandler as d}from"@smithy/node-http-handler";const o=3e4;class u{credentials;region;accountId;clientCache=new Map;constructor(e){this.credentials={accessKeyId:e.accessKeyId,secretAccessKey:e.secretAccessKey,sessionToken:e.sessionToken},this.region=e.region,this.accountId=e.accountId}getClient(e){const t=e.name,n=this.clientCache.get(t);if(n!==void 0)return n;const c={region:this.region,credentials:this.credentials,requestHandler:new d({requestTimeout:o})},s=new e(c);return this.clientCache.set(t,s),s}getRegion(){return this.region}getAccountId(){return this.accountId}getCredentials(){return this.credentials}exportToEnv(){process.env.AWS_ACCESS_KEY_ID=this.credentials.accessKeyId,process.env.AWS_SECRET_ACCESS_KEY=this.credentials.secretAccessKey,process.env.AWS_REGION=this.region,this.credentials.sessionToken?process.env.AWS_SESSION_TOKEN=this.credentials.sessionToken:delete process.env.AWS_SESSION_TOKEN}async assumeRole(e,t){const s=(await this.getClient(i).send(new r({RoleArn:e,RoleSessionName:t}),{abortSignal:AbortSignal.timeout(o)})).Credentials;if(!s?.AccessKeyId||!s?.SecretAccessKey)throw new Error(`AssumeRole for ${e} returned incomplete credentials`);return{accessKeyId:s.AccessKeyId,secretAccessKey:s.SecretAccessKey,sessionToken:s.SessionToken}}}export{u as SimpleAwsProvider};
@@ -1,4 +1,6 @@
1
1
  export type { AwsProvider, AwsProviderCredentials, AwsSdkClientConstructor } from "./AwsProvider.js";
2
2
  export { SimpleAwsProvider } from "./SimpleAwsProvider.js";
3
- export { ensureOrganisationExists, describeOrganisation, enablePolicyTypes, enableServiceAccess, enableRamSharing, activateTrustedAccess, enableIpamDelegatedAdmin, updateBackupGlobalSettings, listAccounts, findAccount, createAccount, ensureOrganisationalUnitsExist, placeAccountsInOUs, activateCostAllocationTags, checkIdentityCentreStatus } from "./organisations/index.js";
4
- export type { OrgDetails, OUMap, AccountPlacementResult, AccountInfo, IdentityCentreStatus, BackupGlobalSettings, CostAllocationTag, CreateAccountResult } from "./organisations/index.js";
3
+ export { ensureOrganisationExists, describeOrganisation, enablePolicyTypes, enableServiceAccess, enableRamSharing, activateTrustedAccess, enableIpamDelegatedAdmin, updateBackupGlobalSettings, listAccounts, findAccount, createAccount, ensureOrganisationalUnitsExist, placeAccountsInOUs, buildAccountToOUMap, activateCostAllocationTags, checkIdentityCentreStatus, extractErrorName, isOULeaf, registerSecurityDelegates, SECURITY_SERVICE_PRINCIPALS } from "./organisations/index.js";
4
+ export type { OrgDetails, OUMap, OUTree, AccountPlacementResult, AccountInfo, IdentityCentreStatus, BackupGlobalSettings, CostAllocationTag, CreateAccountResult } from "./organisations/index.js";
5
+ export { CloudFormationEventMonitor } from "./utils/cloudformationEvents.js";
6
+ export type { AwsClientLike, EventLogWriter, EventFailureAnalyser, EventLogWriterFactory, EventMonitorDeps } from "./utils/cloudformationEvents.js";
@@ -1,3 +1 @@
1
- export { SimpleAwsProvider } from "./SimpleAwsProvider.js";
2
- // Organisation setup primitives
3
- export { ensureOrganisationExists, describeOrganisation, enablePolicyTypes, enableServiceAccess, enableRamSharing, activateTrustedAccess, enableIpamDelegatedAdmin, updateBackupGlobalSettings, listAccounts, findAccount, createAccount, ensureOrganisationalUnitsExist, placeAccountsInOUs, activateCostAllocationTags, checkIdentityCentreStatus } from "./organisations/index.js";
1
+ import{SimpleAwsProvider as a}from"./SimpleAwsProvider.js";import{ensureOrganisationExists as n,describeOrganisation as r,enablePolicyTypes as s,enableServiceAccess as c,enableRamSharing as o,activateTrustedAccess as l,enableIpamDelegatedAdmin as u,updateBackupGlobalSettings as A,listAccounts as d,findAccount as p,createAccount as g,ensureOrganisationalUnitsExist as m,placeAccountsInOUs as S,buildAccountToOUMap as b,activateCostAllocationTags as E,checkIdentityCentreStatus as I,extractErrorName as x,isOULeaf as C,registerSecurityDelegates as O,SECURITY_SERVICE_PRINCIPALS as f}from"./organisations/index.js";import{CloudFormationEventMonitor as T}from"./utils/cloudformationEvents.js";export{T as CloudFormationEventMonitor,f as SECURITY_SERVICE_PRINCIPALS,a as SimpleAwsProvider,E as activateCostAllocationTags,l as activateTrustedAccess,b as buildAccountToOUMap,I as checkIdentityCentreStatus,g as createAccount,r as describeOrganisation,u as enableIpamDelegatedAdmin,s as enablePolicyTypes,o as enableRamSharing,c as enableServiceAccess,n as ensureOrganisationExists,m as ensureOrganisationalUnitsExist,x as extractErrorName,p as findAccount,C as isOULeaf,d as listAccounts,S as placeAccountsInOUs,O as registerSecurityDelegates,A as updateBackupGlobalSettings};
@@ -1,20 +1,20 @@
1
1
  import { ListAccountsCommand, CreateAccountCommand, CreateAccountState, DescribeCreateAccountStatusCommand } from "@aws-sdk/client-organizations";
2
2
  import { success, failure } from "@fjall/generator";
3
- import { sleep } from "../../util/sleep.js";
4
- import { extractErrorName } from "./types.js";
3
+ import { sleep, getErrorMessage } from "@fjall/util";
4
+ import { extractErrorName, SDK_TIMEOUT_MS, AWS_ERROR_NAMES } from "./types.js";
5
5
  /**
6
6
  * List all accounts in the organisation, handling pagination.
7
7
  */
8
8
  export async function listAccounts(client) {
9
9
  try {
10
- const response = await client.send(new ListAccountsCommand({ MaxResults: 20 }));
10
+ const response = await client.send(new ListAccountsCommand({ MaxResults: 20 }), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
11
11
  let accounts = response.Accounts ?? [];
12
12
  let nextToken = response.NextToken;
13
13
  while (nextToken) {
14
14
  const nextResponse = await client.send(new ListAccountsCommand({
15
15
  MaxResults: 20,
16
16
  NextToken: nextToken
17
- }));
17
+ }), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
18
18
  accounts = accounts.concat(nextResponse.Accounts ?? []);
19
19
  nextToken = nextResponse.NextToken;
20
20
  }
@@ -22,10 +22,10 @@ export async function listAccounts(client) {
22
22
  }
23
23
  catch (error) {
24
24
  const errorName = extractErrorName(error);
25
- if (errorName === "AWSOrganizationsNotInUseException") {
25
+ if (errorName === AWS_ERROR_NAMES.ORGS_NOT_IN_USE) {
26
26
  return failure(new Error("AWS Organisations is not enabled for this account"));
27
27
  }
28
- return failure(new Error(`Failed to list accounts: ${error instanceof Error ? error.message : String(error)}`));
28
+ return failure(new Error(`Failed to list accounts: ${getErrorMessage(error)}`));
29
29
  }
30
30
  }
31
31
  /**
@@ -51,11 +51,11 @@ export async function createAccount(client, accountName, email, maxAttempts = 18
51
51
  response = await client.send(new CreateAccountCommand({
52
52
  AccountName: accountName,
53
53
  Email: email
54
- }));
54
+ }), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
55
55
  }
56
56
  catch (error) {
57
57
  const errorName = extractErrorName(error);
58
- if (errorName === "AccessDeniedException") {
58
+ if (errorName === AWS_ERROR_NAMES.ACCESS_DENIED) {
59
59
  return failure(new Error(`Access denied when creating account "${accountName}". ` +
60
60
  "Ensure your credentials have organizations:CreateAccount permission."));
61
61
  }
@@ -74,7 +74,7 @@ export async function createAccount(client, accountName, email, maxAttempts = 18
74
74
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
75
75
  const statusResponse = await client.send(new DescribeCreateAccountStatusCommand({
76
76
  CreateAccountRequestId: requestId
77
- }));
77
+ }), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
78
78
  const status = statusResponse.CreateAccountStatus;
79
79
  if (!status) {
80
80
  return failure(new Error(`No status returned for CreateAccount request ${requestId}`));
@@ -94,6 +94,6 @@ export async function createAccount(client, accountName, email, maxAttempts = 18
94
94
  return failure(new Error(`Account creation for "${accountName}" timed out after ${(maxAttempts * delayMs) / 1000} seconds`));
95
95
  }
96
96
  catch (error) {
97
- return failure(new Error(`Failed to create account "${accountName}": ${error instanceof Error ? error.message : String(error)}`));
97
+ return failure(new Error(`Failed to create account "${accountName}": ${getErrorMessage(error)}`));
98
98
  }
99
99
  }
@@ -1,5 +1,7 @@
1
1
  import { UpdateGlobalSettingsCommand } from "@aws-sdk/client-backup";
2
2
  import { success, failure } from "@fjall/generator";
3
+ import { getErrorMessage } from "@fjall/util";
4
+ import { SDK_TIMEOUT_MS } from "./types.js";
3
5
  const DEFAULT_BACKUP_SETTINGS = {
4
6
  enableCrossAccountBackup: true,
5
7
  enableDelegatedAdministrator: true,
@@ -19,10 +21,10 @@ export async function updateBackupGlobalSettings(client, settings) {
19
21
  };
20
22
  await client.send(new UpdateGlobalSettingsCommand({
21
23
  GlobalSettings: globalSettings
22
- }));
24
+ }), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
23
25
  return success(undefined);
24
26
  }
25
27
  catch (error) {
26
- return failure(new Error(`Failed to update backup global settings: ${error instanceof Error ? error.message : String(error)}`));
28
+ return failure(new Error(`Failed to update backup global settings: ${getErrorMessage(error)}`));
27
29
  }
28
30
  }
@@ -1,5 +1,7 @@
1
1
  import { UpdateCostAllocationTagsStatusCommand } from "@aws-sdk/client-cost-explorer";
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 cost allocation tags for billing tracking.
5
7
  * Idempotent — activating already-active tags is a no-op.
@@ -17,10 +19,10 @@ export async function activateCostAllocationTags(client, tags) {
17
19
  }));
18
20
  await client.send(new UpdateCostAllocationTagsStatusCommand({
19
21
  CostAllocationTagsStatus: costAllocationTagsStatus
20
- }));
22
+ }), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
21
23
  return success(undefined);
22
24
  }
23
25
  catch (error) {
24
- return failure(new Error(`Failed to activate cost allocation tags: ${error instanceof Error ? error.message : String(error)}`));
26
+ return failure(new Error(`Failed to activate cost allocation tags: ${getErrorMessage(error)}`));
25
27
  }
26
28
  }
@@ -0,0 +1,9 @@
1
+ import { type OrganizationsClient } from "@aws-sdk/client-organizations";
2
+ import { type Result } from "@fjall/generator";
3
+ declare const SECURITY_SERVICE_PRINCIPALS: readonly ["guardduty.amazonaws.com", "securityhub.amazonaws.com", "config.amazonaws.com", "inspector2.amazonaws.com", "access-analyzer.amazonaws.com"];
4
+ /**
5
+ * Register delegated administrators for security services.
6
+ * Idempotent — already-registered delegates are silently skipped.
7
+ */
8
+ export declare function registerSecurityDelegates(client: OrganizationsClient, delegateAccountId: string, services?: readonly string[]): Promise<Result<void>>;
9
+ export { SECURITY_SERVICE_PRINCIPALS };
@@ -0,0 +1,43 @@
1
+ import { RegisterDelegatedAdministratorCommand } from "@aws-sdk/client-organizations";
2
+ import { success, failure } from "@fjall/generator";
3
+ import { extractErrorName, SDK_TIMEOUT_MS, AWS_ERROR_NAMES } from "./types.js";
4
+ import { getErrorMessage } from "@fjall/util";
5
+ const SECURITY_SERVICE_PRINCIPALS = [
6
+ "guardduty.amazonaws.com",
7
+ "securityhub.amazonaws.com",
8
+ "config.amazonaws.com",
9
+ "inspector2.amazonaws.com",
10
+ "access-analyzer.amazonaws.com"
11
+ ];
12
+ /**
13
+ * Register delegated administrators for security services.
14
+ * Idempotent — already-registered delegates are silently skipped.
15
+ */
16
+ export async function registerSecurityDelegates(client, delegateAccountId, services = SECURITY_SERVICE_PRINCIPALS) {
17
+ const errors = [];
18
+ for (const service of services) {
19
+ try {
20
+ await client.send(new RegisterDelegatedAdministratorCommand({
21
+ AccountId: delegateAccountId,
22
+ ServicePrincipal: service
23
+ }), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
24
+ }
25
+ catch (error) {
26
+ const errorName = extractErrorName(error);
27
+ if (errorName === AWS_ERROR_NAMES.ACCOUNT_ALREADY_REGISTERED) {
28
+ continue;
29
+ }
30
+ if (errorName === AWS_ERROR_NAMES.ACCESS_DENIED) {
31
+ const prefix = errors.length > 0 ? `${errors.join("; ")}; ` : "";
32
+ return failure(new Error(`${prefix}Access denied when registering delegated admin for ${service}. ` +
33
+ "Ensure your credentials have organizations:RegisterDelegatedAdministrator permission."));
34
+ }
35
+ errors.push(`${service}: ${getErrorMessage(error)}`);
36
+ }
37
+ }
38
+ if (errors.length > 0) {
39
+ return failure(new Error(`Failed to register security delegates for account ${delegateAccountId}:\n ${errors.join("\n ")}`));
40
+ }
41
+ return success(undefined);
42
+ }
43
+ export { SECURITY_SERVICE_PRINCIPALS };
@@ -1,6 +1,6 @@
1
1
  import { type SSOAdminClient } from "@aws-sdk/client-sso-admin";
2
2
  import { type Result } from "@fjall/generator";
3
- import type { IdentityCentreStatus } from "./types.js";
3
+ import { type IdentityCentreStatus } from "./types.js";
4
4
  /**
5
5
  * Check if AWS Identity Centre (SSO) is enabled.
6
6
  * Returns the number of SSO instances found.
@@ -1,12 +1,16 @@
1
1
  import { ListInstancesCommand } from "@aws-sdk/client-sso-admin";
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
  * Check if AWS Identity Centre (SSO) is enabled.
5
7
  * Returns the number of SSO instances found.
6
8
  */
7
9
  export async function checkIdentityCentreStatus(client) {
8
10
  try {
9
- const response = await client.send(new ListInstancesCommand({}));
11
+ const response = await client.send(new ListInstancesCommand({}), {
12
+ abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS)
13
+ });
10
14
  const instances = response.Instances ?? [];
11
15
  return success({
12
16
  enabled: instances.length > 0,
@@ -14,6 +18,6 @@ export async function checkIdentityCentreStatus(client) {
14
18
  });
15
19
  }
16
20
  catch (error) {
17
- return failure(new Error(`Failed to check Identity Centre status: ${error instanceof Error ? error.message : String(error)}`));
21
+ return failure(new Error(`Failed to check Identity Centre status: ${getErrorMessage(error)}`));
18
22
  }
19
23
  }
@@ -1,5 +1,5 @@
1
- export type { OrgDetails, OUMap, AccountPlacementResult, AccountInfo, IdentityCentreStatus } from "./types.js";
2
- export { extractErrorName } from "./types.js";
1
+ export type { OrgDetails, OUMap, OUTree, AccountPlacementResult, AccountInfo, IdentityCentreStatus } from "./types.js";
2
+ export { extractErrorName, isOULeaf } from "./types.js";
3
3
  export type { BackupGlobalSettings } from "./backup.js";
4
4
  export type { CostAllocationTag } from "./costAllocation.js";
5
5
  export type { CreateAccountResult } from "./accounts.js";
@@ -11,6 +11,7 @@ export { activateTrustedAccess } from "./trustedAccess.js";
11
11
  export { enableIpamDelegatedAdmin } from "./ipam.js";
12
12
  export { updateBackupGlobalSettings } from "./backup.js";
13
13
  export { listAccounts, findAccount, createAccount } from "./accounts.js";
14
- export { ensureOrganisationalUnitsExist, placeAccountsInOUs } from "./organisationalUnits.js";
14
+ export { ensureOrganisationalUnitsExist, placeAccountsInOUs, buildAccountToOUMap } from "./organisationalUnits.js";
15
15
  export { activateCostAllocationTags } from "./costAllocation.js";
16
16
  export { checkIdentityCentreStatus } from "./identityCentre.js";
17
+ export { registerSecurityDelegates, SECURITY_SERVICE_PRINCIPALS } from "./delegatedAdmin.js";
@@ -1,12 +1 @@
1
- export { extractErrorName } from "./types.js";
2
- export { ensureOrganisationExists, describeOrganisation } from "./organisation.js";
3
- export { enablePolicyTypes } from "./policies.js";
4
- export { enableServiceAccess } from "./serviceAccess.js";
5
- export { enableRamSharing } from "./ram.js";
6
- export { activateTrustedAccess } from "./trustedAccess.js";
7
- export { enableIpamDelegatedAdmin } from "./ipam.js";
8
- export { updateBackupGlobalSettings } from "./backup.js";
9
- export { listAccounts, findAccount, createAccount } from "./accounts.js";
10
- export { ensureOrganisationalUnitsExist, placeAccountsInOUs } from "./organisationalUnits.js";
11
- export { activateCostAllocationTags } from "./costAllocation.js";
12
- export { checkIdentityCentreStatus } from "./identityCentre.js";
1
+ import{extractErrorName as r,isOULeaf as o}from"./types.js";import{ensureOrganisationExists as c,describeOrganisation as s}from"./organisation.js";import{enablePolicyTypes as i}from"./policies.js";import{enableServiceAccess as m}from"./serviceAccess.js";import{enableRamSharing as f}from"./ram.js";import{activateTrustedAccess as u}from"./trustedAccess.js";import{enableIpamDelegatedAdmin as g}from"./ipam.js";import{updateBackupGlobalSettings as S}from"./backup.js";import{listAccounts as I,findAccount as E,createAccount as O}from"./accounts.js";import{ensureOrganisationalUnitsExist as T,placeAccountsInOUs as U,buildAccountToOUMap as y}from"./organisationalUnits.js";import{activateCostAllocationTags as v}from"./costAllocation.js";import{checkIdentityCentreStatus as h}from"./identityCentre.js";import{registerSecurityDelegates as D,SECURITY_SERVICE_PRINCIPALS as L}from"./delegatedAdmin.js";export{L as SECURITY_SERVICE_PRINCIPALS,v as activateCostAllocationTags,u as activateTrustedAccess,y as buildAccountToOUMap,h as checkIdentityCentreStatus,O as createAccount,s as describeOrganisation,g as enableIpamDelegatedAdmin,i as enablePolicyTypes,f as enableRamSharing,m as enableServiceAccess,c as ensureOrganisationExists,T as ensureOrganisationalUnitsExist,r as extractErrorName,E as findAccount,o as isOULeaf,I as listAccounts,U as placeAccountsInOUs,D as registerSecurityDelegates,S as updateBackupGlobalSettings};
@@ -1,5 +1,7 @@
1
1
  import { EnableIpamOrganizationAdminAccountCommand } from "@aws-sdk/client-ec2";
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 IPAM delegated administrator for the given account.
5
7
  * Idempotent — calling when already delegated is a no-op.
@@ -9,10 +11,10 @@ export async function enableIpamDelegatedAdmin(client, delegatedAdminAccountId)
9
11
  await client.send(new EnableIpamOrganizationAdminAccountCommand({
10
12
  DryRun: false,
11
13
  DelegatedAdminAccountId: delegatedAdminAccountId
12
- }));
14
+ }), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
13
15
  return success(undefined);
14
16
  }
15
17
  catch (error) {
16
- return failure(new Error(`Failed to enable IPAM delegation for account ${delegatedAdminAccountId}: ${error instanceof Error ? error.message : String(error)}`));
18
+ return failure(new Error(`Failed to enable IPAM delegation for account ${delegatedAdminAccountId}: ${getErrorMessage(error)}`));
17
19
  }
18
20
  }
@@ -1,6 +1,7 @@
1
1
  import { DescribeOrganizationCommand, CreateOrganizationCommand, ListRootsCommand } 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, AWS_ERROR_NAMES } from "./types.js";
4
5
  /**
5
6
  * Ensure an AWS Organisation exists, creating one if necessary.
6
7
  * Idempotent — safe to call when the organisation already exists.
@@ -13,7 +14,7 @@ export async function ensureOrganisationExists(client) {
13
14
  if (!org) {
14
15
  // Create new organisation with all features
15
16
  try {
16
- const createResponse = await client.send(new CreateOrganizationCommand({ FeatureSet: "ALL" }));
17
+ const createResponse = await client.send(new CreateOrganizationCommand({ FeatureSet: "ALL" }), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
17
18
  org = createResponse.Organization ?? null;
18
19
  created = true;
19
20
  }
@@ -35,20 +36,18 @@ export async function ensureOrganisationExists(client) {
35
36
  return failure(new Error("Organisation is missing required fields (Id or MasterAccountId)"));
36
37
  }
37
38
  // Get root ID
38
- const rootsResponse = await client.send(new ListRootsCommand({}));
39
- const roots = rootsResponse.Roots ?? [];
40
- if (roots.length === 0 || !roots[0]?.Id) {
41
- return failure(new Error("No organisation root found"));
42
- }
39
+ const rootResult = await getOrganisationRootId(client);
40
+ if (!rootResult.success)
41
+ return rootResult;
43
42
  return success({
44
43
  orgId: org.Id,
45
- rootId: roots[0].Id,
44
+ rootId: rootResult.data,
46
45
  managementAccountId: org.MasterAccountId,
47
46
  created
48
47
  });
49
48
  }
50
49
  catch (error) {
51
- return failure(new Error(`Failed to ensure organisation exists: ${error instanceof Error ? error.message : String(error)}`));
50
+ return failure(new Error(`Failed to ensure organisation exists: ${getErrorMessage(error)}`));
52
51
  }
53
52
  }
54
53
  /**
@@ -63,30 +62,40 @@ export async function describeOrganisation(client) {
63
62
  if (!org.Id || !org.MasterAccountId) {
64
63
  return failure(new Error("Organisation is missing required fields (Id or MasterAccountId)"));
65
64
  }
66
- const rootsResponse = await client.send(new ListRootsCommand({}));
67
- const roots = rootsResponse.Roots ?? [];
68
- if (roots.length === 0 || !roots[0]?.Id) {
69
- return failure(new Error("No organisation root found"));
70
- }
65
+ const rootResult = await getOrganisationRootId(client);
66
+ if (!rootResult.success)
67
+ return rootResult;
71
68
  return success({
72
69
  orgId: org.Id,
73
- rootId: roots[0].Id,
70
+ rootId: rootResult.data,
74
71
  managementAccountId: org.MasterAccountId,
75
72
  created: false
76
73
  });
77
74
  }
78
75
  catch (error) {
79
- return failure(new Error(`Failed to describe organisation: ${error instanceof Error ? error.message : String(error)}`));
76
+ return failure(new Error(`Failed to describe organisation: ${getErrorMessage(error)}`));
80
77
  }
81
78
  }
79
+ async function getOrganisationRootId(client) {
80
+ const rootsResponse = await client.send(new ListRootsCommand({}), {
81
+ abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS)
82
+ });
83
+ const roots = rootsResponse.Roots ?? [];
84
+ if (roots.length === 0 || !roots[0]?.Id) {
85
+ return failure(new Error("No organisation root found"));
86
+ }
87
+ return success(roots[0].Id);
88
+ }
82
89
  async function describeOrganisationRaw(client) {
83
90
  try {
84
- const response = await client.send(new DescribeOrganizationCommand({}));
91
+ const response = await client.send(new DescribeOrganizationCommand({}), {
92
+ abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS)
93
+ });
85
94
  return response.Organization ?? null;
86
95
  }
87
96
  catch (error) {
88
97
  const errorName = extractErrorName(error);
89
- if (errorName === "AWSOrganizationsNotInUseException") {
98
+ if (errorName === AWS_ERROR_NAMES.ORGS_NOT_IN_USE) {
90
99
  return null;
91
100
  }
92
101
  throw error;
@@ -1,19 +1,39 @@
1
1
  import { type OrganizationsClient } from "@aws-sdk/client-organizations";
2
2
  import { type Result } from "@fjall/generator";
3
- import { type OUMap, type AccountInfo, type AccountPlacementResult } from "./types.js";
3
+ import { type OUMap, type OUTree, type AccountInfo, type AccountPlacementResult } from "./types.js";
4
4
  /**
5
- * Ensure the specified organisational units exist under the root.
6
- * Creates any missing OUs. Returns a map of OU name (lowercase) → OU ID.
5
+ * Ensure the specified organisational units exist. Accepts either:
6
+ * - `string[]` (flat): creates OUs directly under root (original behaviour)
7
+ * - `OUTree` (nested): creates a recursive OU hierarchy
8
+ *
9
+ * Returns a map of OU key (lowercase) to OU ID. For nested trees, keys
10
+ * use dot notation (e.g., "workloads.production") with shorthand aliases
11
+ * for leaf names that don't collide with top-level keys.
7
12
  *
8
13
  * Idempotent — existing OUs are adopted, not duplicated.
9
14
  *
10
- * @param ouNames Array of OU names to ensure exist (will be capitalised)
15
+ * AWS cannot move OUs between parents. Migrating from flat to nested
16
+ * creates new OUs under new parents; old empty OUs remain at root.
17
+ */
18
+ export declare function ensureOrganisationalUnitsExist(client: OrganizationsClient, rootId: string, ouConfig: string[] | OUTree): Promise<Result<OUMap>>;
19
+ /**
20
+ * Build a flat map of accountName (lowercase) to OU ID from an OUTree
21
+ * and its resolved OUMap.
22
+ *
23
+ * Walks the tree recursively. For each leaf (string[]), maps each account
24
+ * name to the parent OU's ID using the dotted OUMap key.
25
+ *
26
+ * Callers must pass account names that match the tree leaf values.
11
27
  */
12
- export declare function ensureOrganisationalUnitsExist(client: OrganizationsClient, rootId: string, ouNames: string[]): Promise<Result<OUMap>>;
28
+ export declare function buildAccountToOUMap(tree: OUTree, ouMap: OUMap, prefix?: string): Record<string, string>;
13
29
  /**
14
30
  * Place accounts into the correct organisational units.
15
31
  * Skips accounts with environment "root" and accounts already in the target OU.
16
32
  *
33
+ * When `accountToOU` is provided, looks up the target OU by account name
34
+ * (lowercase) instead of by environment. This supports tree-based placement
35
+ * where the OU is determined by which leaf the account appears in.
36
+ *
17
37
  * Idempotent — accounts already in the correct OU are counted but not moved.
18
38
  */
19
- export declare function placeAccountsInOUs(client: OrganizationsClient, ouMap: OUMap, accounts: AccountInfo[]): Promise<Result<AccountPlacementResult>>;
39
+ export declare function placeAccountsInOUs(client: OrganizationsClient, ouMap: OUMap, accounts: AccountInfo[], accountToOU?: Record<string, string>): Promise<Result<AccountPlacementResult>>;