@fjall/deploy-core 0.94.1 → 0.95.0

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 (54) hide show
  1. package/dist/.minified +1 -1
  2. package/dist/src/aws/organisations/accounts.js +1 -99
  3. package/dist/src/aws/organisations/backup.js +1 -30
  4. package/dist/src/aws/organisations/costAllocation.js +1 -28
  5. package/dist/src/aws/organisations/delegatedAdmin.js +3 -43
  6. package/dist/src/aws/organisations/identityCentre.js +1 -23
  7. package/dist/src/aws/organisations/ipam.js +1 -20
  8. package/dist/src/aws/organisations/organisation.js +1 -103
  9. package/dist/src/aws/organisations/organisationalUnits.js +1 -239
  10. package/dist/src/aws/organisations/policies.js +1 -37
  11. package/dist/src/aws/organisations/ram.js +1 -19
  12. package/dist/src/aws/organisations/serviceAccess.js +1 -44
  13. package/dist/src/aws/organisations/trustedAccess.js +1 -19
  14. package/dist/src/aws/utils/regions.js +1 -1
  15. package/dist/src/index.js +1 -65
  16. package/dist/src/orchestration/__tests__/cascadeTestHelpers.js +1 -78
  17. package/dist/src/orchestration/activeDeploymentGuard.js +5 -39
  18. package/dist/src/orchestration/applicationDeploy.js +1 -149
  19. package/dist/src/orchestration/applicationDeployHelpers.js +4 -223
  20. package/dist/src/orchestration/applicationDestroy.js +1 -131
  21. package/dist/src/orchestration/builders/dockerBuilder.js +1 -98
  22. package/dist/src/orchestration/builders/openNextBuilder.js +1 -144
  23. package/dist/src/orchestration/cascadeHelpers.js +1 -160
  24. package/dist/src/orchestration/contextHelpers.js +1 -107
  25. package/dist/src/orchestration/deploy.js +1 -42
  26. package/dist/src/orchestration/destroy.js +1 -67
  27. package/dist/src/orchestration/detectionPipeline.js +1 -84
  28. package/dist/src/orchestration/dockerBuildHelper.js +1 -49
  29. package/dist/src/orchestration/dockerInterface.js +0 -1
  30. package/dist/src/orchestration/domainInterface.js +0 -1
  31. package/dist/src/orchestration/openNextBuild.js +3 -243
  32. package/dist/src/orchestration/organisationDeploy.js +3 -284
  33. package/dist/src/orchestration/organisationDestroy.js +3 -189
  34. package/dist/src/orchestration/organisationSetup.js +1 -247
  35. package/dist/src/orchestration/resolveOperation.js +1 -123
  36. package/dist/src/orchestration/welcomeImageHelper.js +1 -64
  37. package/dist/src/services/application/ApplicationStackService.js +1 -218
  38. package/dist/src/services/application/applicationStackHelpers.js +4 -248
  39. package/dist/src/services/infrastructure/CdkCommandRunner.js +2 -244
  40. package/dist/src/services/infrastructure/CdkOutputAnalyser.js +1 -125
  41. package/dist/src/services/infrastructure/CdkProcessManager.js +3 -278
  42. package/dist/src/services/infrastructure/CdkService.js +3 -213
  43. package/dist/src/services/infrastructure/CloudFormationService.js +1 -248
  44. package/dist/src/services/infrastructure/ICdkProcessManager.js +0 -1
  45. package/dist/src/services/supporting/CdkContextBuilder.js +1 -44
  46. package/dist/src/services/supporting/TemplateHashService.js +1 -152
  47. package/dist/src/steps/stepRegistry.js +1 -505
  48. package/dist/src/types/apiClient.js +0 -1
  49. package/dist/src/types/detection.js +0 -1
  50. package/dist/src/types/frameworkBuilder.js +0 -8
  51. package/dist/src/types/params.js +0 -1
  52. package/dist/src/types/patternDetection.js +1 -88
  53. package/dist/src/types/stepDefinitions.js +1 -98
  54. package/package.json +4 -4
package/dist/.minified CHANGED
@@ -1 +1 @@
1
- 114 files minified at 2026-04-21T02:31:24.067Z
1
+ 114 files minified at 2026-04-21T03:05:05.153Z
@@ -1,99 +1 @@
1
- import { ListAccountsCommand, CreateAccountCommand, CreateAccountState, DescribeCreateAccountStatusCommand } from "@aws-sdk/client-organizations";
2
- import { success, failure } from "@fjall/generator";
3
- import { sleep, getErrorMessage } from "@fjall/util";
4
- import { extractErrorName, SDK_TIMEOUT_MS, AWS_ERROR_NAMES } from "./types.js";
5
- /**
6
- * List all accounts in the organisation, handling pagination.
7
- */
8
- export async function listAccounts(client) {
9
- try {
10
- const response = await client.send(new ListAccountsCommand({ MaxResults: 20 }), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
11
- let accounts = response.Accounts ?? [];
12
- let nextToken = response.NextToken;
13
- while (nextToken) {
14
- const nextResponse = await client.send(new ListAccountsCommand({
15
- MaxResults: 20,
16
- NextToken: nextToken
17
- }), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
18
- accounts = accounts.concat(nextResponse.Accounts ?? []);
19
- nextToken = nextResponse.NextToken;
20
- }
21
- return success(accounts);
22
- }
23
- catch (error) {
24
- const errorName = extractErrorName(error);
25
- if (errorName === AWS_ERROR_NAMES.ORGS_NOT_IN_USE) {
26
- return failure(new Error("AWS Organisations is not enabled for this account"));
27
- }
28
- return failure(new Error(`Failed to list accounts: ${getErrorMessage(error)}`));
29
- }
30
- }
31
- /**
32
- * Find an account by ID in the organisation.
33
- */
34
- export async function findAccount(client, accountId) {
35
- const listResult = await listAccounts(client);
36
- if (!listResult.success) {
37
- return listResult;
38
- }
39
- return success(listResult.data.find((acc) => acc.Id === accountId));
40
- }
41
- /**
42
- * Create an AWS account and poll until creation completes.
43
- *
44
- * @param maxAttempts Maximum polling attempts (default: 180 = ~15 minutes at 5s intervals)
45
- * @param delayMs Delay between polling attempts in milliseconds (default: 5000)
46
- */
47
- export async function createAccount(client, accountName, email, maxAttempts = 180, delayMs = 5000) {
48
- try {
49
- let response;
50
- try {
51
- response = await client.send(new CreateAccountCommand({
52
- AccountName: accountName,
53
- Email: email
54
- }), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
55
- }
56
- catch (error) {
57
- const errorName = extractErrorName(error);
58
- if (errorName === AWS_ERROR_NAMES.ACCESS_DENIED) {
59
- return failure(new Error(`Access denied when creating account "${accountName}". ` +
60
- "Ensure your credentials have organizations:CreateAccount permission."));
61
- }
62
- if (errorName === "DuplicateAccountException") {
63
- return failure(new Error(`An account with the email "${email}" already exists in the organisation.`));
64
- }
65
- if (errorName === "FinalizingOrganizationException") {
66
- return failure(new Error("The organisation is still being initialised. Please wait and try again."));
67
- }
68
- throw error;
69
- }
70
- const requestId = response.CreateAccountStatus?.Id;
71
- if (!requestId) {
72
- return failure(new Error(`CreateAccount request for "${accountName}" did not return a status ID`));
73
- }
74
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
75
- const statusResponse = await client.send(new DescribeCreateAccountStatusCommand({
76
- CreateAccountRequestId: requestId
77
- }), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
78
- const status = statusResponse.CreateAccountStatus;
79
- if (!status) {
80
- return failure(new Error(`No status returned for CreateAccount request ${requestId}`));
81
- }
82
- if (status.State === CreateAccountState.SUCCEEDED) {
83
- if (!status.AccountId) {
84
- return failure(new Error(`Account creation succeeded but no account ID returned for "${accountName}"`));
85
- }
86
- return success({ accountId: status.AccountId, accountName });
87
- }
88
- if (status.State === CreateAccountState.FAILED) {
89
- return failure(new Error(`Account creation failed for "${accountName}": ${status.FailureReason}`));
90
- }
91
- // Still IN_PROGRESS
92
- await sleep(delayMs);
93
- }
94
- return failure(new Error(`Account creation for "${accountName}" timed out after ${(maxAttempts * delayMs) / 1000} seconds`));
95
- }
96
- catch (error) {
97
- return failure(new Error(`Failed to create account "${accountName}": ${getErrorMessage(error)}`));
98
- }
99
- }
1
+ import{ListAccountsCommand as f,CreateAccountCommand as m,CreateAccountState as E,DescribeCreateAccountStatusCommand as p}from"@aws-sdk/client-organizations";import{success as A,failure as e}from"@fjall/generator";import{sleep as C,getErrorMessage as w}from"@fjall/util";import{extractErrorName as S,SDK_TIMEOUT_MS as d,AWS_ERROR_NAMES as g}from"./types.js";async function x(o){try{const t=await o.send(new f({MaxResults:20}),{abortSignal:AbortSignal.timeout(d)});let r=t.Accounts??[],n=t.NextToken;for(;n;){const s=await o.send(new f({MaxResults:20,NextToken:n}),{abortSignal:AbortSignal.timeout(d)});r=r.concat(s.Accounts??[]),n=s.NextToken}return A(r)}catch(t){return S(t)===g.ORGS_NOT_IN_USE?e(new Error("AWS Organisations is not enabled for this account")):e(new Error(`Failed to list accounts: ${w(t)}`))}}async function $(o,t){const r=await x(o);return r.success?A(r.data.find(n=>n.Id===t)):r}async function D(o,t,r,n=180,s=5e3){try{let i;try{i=await o.send(new m({AccountName:t,Email:r}),{abortSignal:AbortSignal.timeout(d)})}catch(c){const u=S(c);if(u===g.ACCESS_DENIED)return e(new Error(`Access denied when creating account "${t}". Ensure your credentials have organizations:CreateAccount permission.`));if(u==="DuplicateAccountException")return e(new Error(`An account with the email "${r}" already exists in the organisation.`));if(u==="FinalizingOrganizationException")return e(new Error("The organisation is still being initialised. Please wait and try again."));throw c}const l=i.CreateAccountStatus?.Id;if(!l)return e(new Error(`CreateAccount request for "${t}" did not return a status ID`));for(let c=0;c<n;c++){const a=(await o.send(new p({CreateAccountRequestId:l}),{abortSignal:AbortSignal.timeout(d)})).CreateAccountStatus;if(!a)return e(new Error(`No status returned for CreateAccount request ${l}`));if(a.State===E.SUCCEEDED)return a.AccountId?A({accountId:a.AccountId,accountName:t}):e(new Error(`Account creation succeeded but no account ID returned for "${t}"`));if(a.State===E.FAILED)return e(new Error(`Account creation failed for "${t}": ${a.FailureReason}`));await C(s)}return e(new Error(`Account creation for "${t}" timed out after ${n*s/1e3} seconds`))}catch(i){return e(new Error(`Failed to create account "${t}": ${w(i)}`))}}export{D as createAccount,$ as findAccount,x as listAccounts};
@@ -1,30 +1 @@
1
- import { UpdateGlobalSettingsCommand } from "@aws-sdk/client-backup";
2
- import { success, failure } from "@fjall/generator";
3
- import { getErrorMessage } from "@fjall/util";
4
- import { SDK_TIMEOUT_MS } from "./types.js";
5
- const DEFAULT_BACKUP_SETTINGS = {
6
- enableCrossAccountBackup: true,
7
- enableDelegatedAdministrator: true,
8
- enableMpa: false
9
- };
10
- /**
11
- * Update AWS Backup global settings for the organisation.
12
- * Idempotent — safe to call repeatedly with the same settings.
13
- */
14
- export async function updateBackupGlobalSettings(client, settings) {
15
- try {
16
- const finalSettings = { ...DEFAULT_BACKUP_SETTINGS, ...settings };
17
- const globalSettings = {
18
- isCrossAccountBackupEnabled: finalSettings.enableCrossAccountBackup.toString(),
19
- isDelegatedAdministratorEnabled: finalSettings.enableDelegatedAdministrator.toString(),
20
- isMpaEnabled: finalSettings.enableMpa.toString()
21
- };
22
- await client.send(new UpdateGlobalSettingsCommand({
23
- GlobalSettings: globalSettings
24
- }), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
25
- return success(undefined);
26
- }
27
- catch (error) {
28
- return failure(new Error(`Failed to update backup global settings: ${getErrorMessage(error)}`));
29
- }
30
- }
1
+ import{UpdateGlobalSettingsCommand as r}from"@aws-sdk/client-backup";import{success as o,failure as i}from"@fjall/generator";import{getErrorMessage as s}from"@fjall/util";import{SDK_TIMEOUT_MS as l}from"./types.js";const c={enableCrossAccountBackup:!0,enableDelegatedAdministrator:!0,enableMpa:!1};async function p(e,a){try{const t={...c,...a},n={isCrossAccountBackupEnabled:t.enableCrossAccountBackup.toString(),isDelegatedAdministratorEnabled:t.enableDelegatedAdministrator.toString(),isMpaEnabled:t.enableMpa.toString()};return await e.send(new r({GlobalSettings:n}),{abortSignal:AbortSignal.timeout(l)}),o(void 0)}catch(t){return i(new Error(`Failed to update backup global settings: ${s(t)}`))}}export{p as updateBackupGlobalSettings};
@@ -1,28 +1 @@
1
- import { UpdateCostAllocationTagsStatusCommand } from "@aws-sdk/client-cost-explorer";
2
- import { success, failure } from "@fjall/generator";
3
- import { getErrorMessage } from "@fjall/util";
4
- import { SDK_TIMEOUT_MS } from "./types.js";
5
- /**
6
- * Activate cost allocation tags for billing tracking.
7
- * Idempotent — activating already-active tags is a no-op.
8
- *
9
- * Returns success with no action if the tags array is empty.
10
- */
11
- export async function activateCostAllocationTags(client, tags) {
12
- if (tags.length === 0) {
13
- return success(undefined);
14
- }
15
- try {
16
- const costAllocationTagsStatus = tags.map((tag) => ({
17
- TagKey: tag.TagKey,
18
- Status: "Active"
19
- }));
20
- await client.send(new UpdateCostAllocationTagsStatusCommand({
21
- CostAllocationTagsStatus: costAllocationTagsStatus
22
- }), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
23
- return success(undefined);
24
- }
25
- catch (error) {
26
- return failure(new Error(`Failed to activate cost allocation tags: ${getErrorMessage(error)}`));
27
- }
28
- }
1
+ import{UpdateCostAllocationTagsStatusCommand as n}from"@aws-sdk/client-cost-explorer";import{success as a,failure as i}from"@fjall/generator";import{getErrorMessage as s}from"@fjall/util";import{SDK_TIMEOUT_MS as c}from"./types.js";async function f(r,o){if(o.length===0)return a(void 0);try{const t=o.map(e=>({TagKey:e.TagKey,Status:"Active"}));return await r.send(new n({CostAllocationTagsStatus:t}),{abortSignal:AbortSignal.timeout(c)}),a(void 0)}catch(t){return i(new Error(`Failed to activate cost allocation tags: ${s(t)}`))}}export{f as activateCostAllocationTags};
@@ -1,43 +1,3 @@
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
+ import{RegisterDelegatedAdministratorCommand as u}from"@aws-sdk/client-organizations";import{success as E,failure as a}from"@fjall/generator";import{extractErrorName as d,SDK_TIMEOUT_MS as f,AWS_ERROR_NAMES as i}from"./types.js";import{getErrorMessage as S}from"@fjall/util";const s=["guardduty.amazonaws.com","securityhub.amazonaws.com","config.amazonaws.com","inspector2.amazonaws.com","access-analyzer.amazonaws.com"];async function w(c,o,m=s){const r=[];for(const e of m)try{await c.send(new u({AccountId:o,ServicePrincipal:e}),{abortSignal:AbortSignal.timeout(f)})}catch(t){const n=d(t);if(n===i.ACCOUNT_ALREADY_REGISTERED)continue;if(n===i.ACCESS_DENIED){const g=r.length>0?`${r.join("; ")}; `:"";return a(new Error(`${g}Access denied when registering delegated admin for ${e}. Ensure your credentials have organizations:RegisterDelegatedAdministrator permission.`))}r.push(`${e}: ${S(t)}`)}return r.length>0?a(new Error(`Failed to register security delegates for account ${o}:
2
+ ${r.join(`
3
+ `)}`)):E(void 0)}export{s as SECURITY_SERVICE_PRINCIPALS,w as registerSecurityDelegates};
@@ -1,23 +1 @@
1
- import { ListInstancesCommand } from "@aws-sdk/client-sso-admin";
2
- import { success, failure } from "@fjall/generator";
3
- import { getErrorMessage } from "@fjall/util";
4
- import { SDK_TIMEOUT_MS } from "./types.js";
5
- /**
6
- * Check if AWS Identity Centre (SSO) is enabled.
7
- * Returns the number of SSO instances found.
8
- */
9
- export async function checkIdentityCentreStatus(client) {
10
- try {
11
- const response = await client.send(new ListInstancesCommand({}), {
12
- abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS)
13
- });
14
- const instances = response.Instances ?? [];
15
- return success({
16
- enabled: instances.length > 0,
17
- instanceCount: instances.length
18
- });
19
- }
20
- catch (error) {
21
- return failure(new Error(`Failed to check Identity Centre status: ${getErrorMessage(error)}`));
22
- }
23
- }
1
+ import{ListInstancesCommand as r}from"@aws-sdk/client-sso-admin";import{success as s,failure as o}from"@fjall/generator";import{getErrorMessage as a}from"@fjall/util";import{SDK_TIMEOUT_MS as c}from"./types.js";async function p(n){try{const e=(await n.send(new r({}),{abortSignal:AbortSignal.timeout(c)})).Instances??[];return s({enabled:e.length>0,instanceCount:e.length})}catch(t){return o(new Error(`Failed to check Identity Centre status: ${a(t)}`))}}export{p as checkIdentityCentreStatus};
@@ -1,20 +1 @@
1
- import { EnableIpamOrganizationAdminAccountCommand } from "@aws-sdk/client-ec2";
2
- import { success, failure } from "@fjall/generator";
3
- import { getErrorMessage } from "@fjall/util";
4
- import { SDK_TIMEOUT_MS } from "./types.js";
5
- /**
6
- * Enable IPAM delegated administrator for the given account.
7
- * Idempotent — calling when already delegated is a no-op.
8
- */
9
- export async function enableIpamDelegatedAdmin(client, delegatedAdminAccountId) {
10
- try {
11
- await client.send(new EnableIpamOrganizationAdminAccountCommand({
12
- DryRun: false,
13
- DelegatedAdminAccountId: delegatedAdminAccountId
14
- }), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
15
- return success(undefined);
16
- }
17
- catch (error) {
18
- return failure(new Error(`Failed to enable IPAM delegation for account ${delegatedAdminAccountId}: ${getErrorMessage(error)}`));
19
- }
20
- }
1
+ import{EnableIpamOrganizationAdminAccountCommand as o}from"@aws-sdk/client-ec2";import{success as a,failure as t}from"@fjall/generator";import{getErrorMessage as i}from"@fjall/util";import{SDK_TIMEOUT_MS as m}from"./types.js";async function d(e,r){try{return await e.send(new o({DryRun:!1,DelegatedAdminAccountId:r}),{abortSignal:AbortSignal.timeout(m)}),a(void 0)}catch(n){return t(new Error(`Failed to enable IPAM delegation for account ${r}: ${i(n)}`))}}export{d as enableIpamDelegatedAdmin};
@@ -1,103 +1 @@
1
- import { DescribeOrganizationCommand, CreateOrganizationCommand, ListRootsCommand } from "@aws-sdk/client-organizations";
2
- import { success, failure } from "@fjall/generator";
3
- import { getErrorMessage } from "@fjall/util";
4
- import { extractErrorName, SDK_TIMEOUT_MS, AWS_ERROR_NAMES } from "./types.js";
5
- /**
6
- * Ensure an AWS Organisation exists, creating one if necessary.
7
- * Idempotent — safe to call when the organisation already exists.
8
- */
9
- export async function ensureOrganisationExists(client) {
10
- try {
11
- let created = false;
12
- // Try to describe existing organisation
13
- let org = await describeOrganisationRaw(client);
14
- if (!org) {
15
- // Create new organisation with all features
16
- try {
17
- const createResponse = await client.send(new CreateOrganizationCommand({ FeatureSet: "ALL" }), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
18
- org = createResponse.Organization ?? null;
19
- created = true;
20
- }
21
- catch (createError) {
22
- const errorName = extractErrorName(createError);
23
- if (errorName === "AlreadyInOrganizationException") {
24
- // Race condition — another process created it
25
- org = await describeOrganisationRaw(client);
26
- if (!org) {
27
- return failure(new Error("Account is already in an organisation but DescribeOrganization returned nothing"));
28
- }
29
- }
30
- else {
31
- throw createError;
32
- }
33
- }
34
- }
35
- if (!org?.Id || !org.MasterAccountId) {
36
- return failure(new Error("Organisation is missing required fields (Id or MasterAccountId)"));
37
- }
38
- // Get root ID
39
- const rootResult = await getOrganisationRootId(client);
40
- if (!rootResult.success)
41
- return rootResult;
42
- return success({
43
- orgId: org.Id,
44
- rootId: rootResult.data,
45
- managementAccountId: org.MasterAccountId,
46
- created
47
- });
48
- }
49
- catch (error) {
50
- return failure(new Error(`Failed to ensure organisation exists: ${getErrorMessage(error)}`));
51
- }
52
- }
53
- /**
54
- * Describe the current AWS Organisation, returning null if none exists.
55
- */
56
- export async function describeOrganisation(client) {
57
- try {
58
- const org = await describeOrganisationRaw(client);
59
- if (!org) {
60
- return success(null);
61
- }
62
- if (!org.Id || !org.MasterAccountId) {
63
- return failure(new Error("Organisation is missing required fields (Id or MasterAccountId)"));
64
- }
65
- const rootResult = await getOrganisationRootId(client);
66
- if (!rootResult.success)
67
- return rootResult;
68
- return success({
69
- orgId: org.Id,
70
- rootId: rootResult.data,
71
- managementAccountId: org.MasterAccountId,
72
- created: false
73
- });
74
- }
75
- catch (error) {
76
- return failure(new Error(`Failed to describe organisation: ${getErrorMessage(error)}`));
77
- }
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
- }
89
- async function describeOrganisationRaw(client) {
90
- try {
91
- const response = await client.send(new DescribeOrganizationCommand({}), {
92
- abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS)
93
- });
94
- return response.Organization ?? null;
95
- }
96
- catch (error) {
97
- const errorName = extractErrorName(error);
98
- if (errorName === AWS_ERROR_NAMES.ORGS_NOT_IN_USE) {
99
- return null;
100
- }
101
- throw error;
102
- }
103
- }
1
+ import{DescribeOrganizationCommand as m,CreateOrganizationCommand as l,ListRootsCommand as f}from"@aws-sdk/client-organizations";import{success as n,failure as o}from"@fjall/generator";import{getErrorMessage as u}from"@fjall/util";import{extractErrorName as d,SDK_TIMEOUT_MS as s,AWS_ERROR_NAMES as I}from"./types.js";async function p(e){try{let r=!1,t=await c(e);if(!t)try{t=(await e.send(new l({FeatureSet:"ALL"}),{abortSignal:AbortSignal.timeout(s)})).Organization??null,r=!0}catch(i){if(d(i)==="AlreadyInOrganizationException"){if(t=await c(e),!t)return o(new Error("Account is already in an organisation but DescribeOrganization returned nothing"))}else throw i}if(!t?.Id||!t.MasterAccountId)return o(new Error("Organisation is missing required fields (Id or MasterAccountId)"));const a=await g(e);return a.success?n({orgId:t.Id,rootId:a.data,managementAccountId:t.MasterAccountId,created:r}):a}catch(r){return o(new Error(`Failed to ensure organisation exists: ${u(r)}`))}}async function S(e){try{const r=await c(e);if(!r)return n(null);if(!r.Id||!r.MasterAccountId)return o(new Error("Organisation is missing required fields (Id or MasterAccountId)"));const t=await g(e);return t.success?n({orgId:r.Id,rootId:t.data,managementAccountId:r.MasterAccountId,created:!1}):t}catch(r){return o(new Error(`Failed to describe organisation: ${u(r)}`))}}async function g(e){const t=(await e.send(new f({}),{abortSignal:AbortSignal.timeout(s)})).Roots??[];return t.length===0||!t[0]?.Id?o(new Error("No organisation root found")):n(t[0].Id)}async function c(e){try{return(await e.send(new m({}),{abortSignal:AbortSignal.timeout(s)})).Organization??null}catch(r){if(d(r)===I.ORGS_NOT_IN_USE)return null;throw r}}export{S as describeOrganisation,p as ensureOrganisationExists};
@@ -1,239 +1 @@
1
- import { CreateOrganizationalUnitCommand, ListOrganizationalUnitsForParentCommand, ListParentsCommand, MoveAccountCommand } from "@aws-sdk/client-organizations";
2
- import { success, failure } from "@fjall/generator";
3
- import { extractErrorName, isOULeaf, SDK_TIMEOUT_MS, AWS_ERROR_NAMES } from "./types.js";
4
- import { getErrorMessage } from "@fjall/util";
5
- /**
6
- * List all OUs under a parent, handling pagination.
7
- */
8
- async function listOUsForParent(client, parentId) {
9
- const response = await client.send(new ListOrganizationalUnitsForParentCommand({ ParentId: parentId }), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
10
- let ous = response.OrganizationalUnits ?? [];
11
- let nextToken = response.NextToken;
12
- while (nextToken) {
13
- const nextResponse = await client.send(new ListOrganizationalUnitsForParentCommand({
14
- ParentId: parentId,
15
- NextToken: nextToken
16
- }), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
17
- ous = ous.concat(nextResponse.OrganizationalUnits ?? []);
18
- nextToken = nextResponse.NextToken;
19
- }
20
- return ous;
21
- }
22
- /**
23
- * Get the parent ID for a child (account or OU).
24
- */
25
- async function getParentId(client, childId) {
26
- try {
27
- const response = await client.send(new ListParentsCommand({ ChildId: childId }), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
28
- return response.Parents?.[0]?.Id;
29
- }
30
- catch (error) {
31
- const errorName = extractErrorName(error);
32
- if (errorName === AWS_ERROR_NAMES.CHILD_NOT_FOUND) {
33
- return undefined;
34
- }
35
- throw error;
36
- }
37
- }
38
- /**
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.
131
- *
132
- * Idempotent — existing OUs are adopted, not duplicated.
133
- *
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.
136
- */
137
- export async function ensureOrganisationalUnitsExist(client, rootId, ouConfig) {
138
- try {
139
- if (Array.isArray(ouConfig)) {
140
- return await ensureFlatOUs(client, rootId, ouConfig);
141
- }
142
- const ouMap = {};
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);
147
- }
148
- return success(ouMap);
149
- }
150
- catch (error) {
151
- return failure(new Error(`Failed to ensure OUs exist: ${getErrorMessage(error)}`));
152
- }
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
- }
183
- /**
184
- * Place accounts into the correct organisational units.
185
- * Skips accounts with environment "root" and accounts already in the target OU.
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
- *
191
- * Idempotent — accounts already in the correct OU are counted but not moved.
192
- */
193
- export async function placeAccountsInOUs(client, ouMap, accounts, accountToOU) {
194
- try {
195
- if (accounts.length === 0) {
196
- return success({ moved: 0, alreadyPlaced: 0 });
197
- }
198
- let moved = 0;
199
- let alreadyPlaced = 0;
200
- for (const account of accounts) {
201
- if (account.environment === "root") {
202
- continue;
203
- }
204
- const targetOuId = accountToOU
205
- ? accountToOU[account.name.toLowerCase()]
206
- : ouMap[account.environment.toLowerCase()];
207
- if (!targetOuId) {
208
- continue;
209
- }
210
- const currentParentId = await getParentId(client, account.id);
211
- if (!currentParentId) {
212
- continue;
213
- }
214
- if (currentParentId === targetOuId) {
215
- alreadyPlaced++;
216
- continue;
217
- }
218
- try {
219
- await client.send(new MoveAccountCommand({
220
- AccountId: account.id,
221
- SourceParentId: currentParentId,
222
- DestinationParentId: targetOuId
223
- }), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
224
- moved++;
225
- }
226
- catch (error) {
227
- const errorName = extractErrorName(error);
228
- if (errorName === AWS_ERROR_NAMES.ACCOUNT_NOT_FOUND) {
229
- continue;
230
- }
231
- throw error;
232
- }
233
- }
234
- return success({ moved, alreadyPlaced });
235
- }
236
- catch (error) {
237
- return failure(new Error(`Failed to place accounts in OUs: ${getErrorMessage(error)}`));
238
- }
239
- }
1
+ import{CreateOrganizationalUnitCommand as p,ListOrganizationalUnitsForParentCommand as h,ListParentsCommand as S,MoveAccountCommand as A}from"@aws-sdk/client-organizations";import{success as u,failure as d}from"@fjall/generator";import{extractErrorName as U,isOULeaf as y,SDK_TIMEOUT_MS as w,AWS_ERROR_NAMES as I}from"./types.js";import{getErrorMessage as O}from"@fjall/util";async function N(o,a){const t=await o.send(new h({ParentId:a}),{abortSignal:AbortSignal.timeout(w)});let e=t.OrganizationalUnits??[],n=t.NextToken;for(;n;){const r=await o.send(new h({ParentId:a,NextToken:n}),{abortSignal:AbortSignal.timeout(w)});e=e.concat(r.OrganizationalUnits??[]),n=r.NextToken}return e}async function L(o,a){try{return(await o.send(new S({ChildId:a}),{abortSignal:AbortSignal.timeout(w)})).Parents?.[0]?.Id}catch(t){if(U(t)===I.CHILD_NOT_FOUND)return;throw t}}async function C(o,a,t,e){const n=e.find(r=>r.Name===t);if(n?.Id)return u(n.Id);try{const s=(await o.send(new p({Name:t,ParentId:a}),{abortSignal:AbortSignal.timeout(w)})).OrganizationalUnit;return s?.Id?u(s.Id):d(new Error(`OU "${t}" was created but has no ID`))}catch(r){return d(new Error(`Failed to create OU "${t}": ${O(r)}`))}}async function P(o,a,t){const e={};if(t.length===0)return u(e);const n=await N(o,a);for(const r of t){const s=r.charAt(0).toUpperCase()+r.slice(1),i=await C(o,a,s,n);if(!i.success)return d(i.error);e[r.toLowerCase()]=i.data,n.push({Id:i.data,Name:s})}return u(e)}async function b(o,a,t,e,n,r){const s=await N(o,a);for(const[i,c]of Object.entries(t)){const f=await C(o,a,i,s);if(!f.success)return d(f.error);const l=f.data,g=n?`${n}.${i.toLowerCase()}`:i.toLowerCase();if(e[g]=l,n){const m=i.toLowerCase();r.has(m)||(e[m]=l)}if(s.push({Id:l,Name:i}),!y(c)){const m=await b(o,l,c,e,g,r);if(!m.success)return m}}return u(void 0)}async function _(o,a,t){try{if(Array.isArray(t))return await P(o,a,t);const e={},n=new Set(Object.keys(t).map(s=>s.toLowerCase())),r=await b(o,a,t,e,"",n);return r.success?u(e):d(r.error)}catch(e){return d(new Error(`Failed to ensure OUs exist: ${O(e)}`))}}function x(o,a,t=""){const e={};for(const[n,r]of Object.entries(o)){const s=t?`${t}.${n.toLowerCase()}`:n.toLowerCase(),i=a[s];if(y(r)){if(i)for(const c of r)e[c.toLowerCase()]=i}else Object.assign(e,x(r,a,s))}return e}async function D(o,a,t,e){try{if(t.length===0)return u({moved:0,alreadyPlaced:0});let n=0,r=0;for(const s of t){if(s.environment==="root")continue;const i=e?e[s.name.toLowerCase()]:a[s.environment.toLowerCase()];if(!i)continue;const c=await L(o,s.id);if(c){if(c===i){r++;continue}try{await o.send(new A({AccountId:s.id,SourceParentId:c,DestinationParentId:i}),{abortSignal:AbortSignal.timeout(w)}),n++}catch(f){if(U(f)===I.ACCOUNT_NOT_FOUND)continue;throw f}}}return u({moved:n,alreadyPlaced:r})}catch(n){return d(new Error(`Failed to place accounts in OUs: ${O(n)}`))}}export{x as buildAccountToOUMap,_ as ensureOrganisationalUnitsExist,D as placeAccountsInOUs};
@@ -1,37 +1 @@
1
- import { EnablePolicyTypeCommand, PolicyType } from "@aws-sdk/client-organizations";
2
- import { success, failure } from "@fjall/generator";
3
- import { getErrorMessage } from "@fjall/util";
4
- import { extractErrorName, SDK_TIMEOUT_MS } from "./types.js";
5
- const POLICY_TYPES = [
6
- PolicyType.SERVICE_CONTROL_POLICY,
7
- PolicyType.TAG_POLICY,
8
- PolicyType.BACKUP_POLICY,
9
- PolicyType.AISERVICES_OPT_OUT_POLICY
10
- ];
11
- /**
12
- * Enable all required policy types on the organisation root.
13
- * Idempotent — skips policy types that are already enabled.
14
- */
15
- export async function enablePolicyTypes(client, rootId) {
16
- try {
17
- for (const policyType of POLICY_TYPES) {
18
- try {
19
- await client.send(new EnablePolicyTypeCommand({
20
- RootId: rootId,
21
- PolicyType: policyType
22
- }), { abortSignal: AbortSignal.timeout(SDK_TIMEOUT_MS) });
23
- }
24
- catch (error) {
25
- const errorName = extractErrorName(error);
26
- if (errorName === "PolicyTypeAlreadyEnabledException") {
27
- continue;
28
- }
29
- throw error;
30
- }
31
- }
32
- return success(undefined);
33
- }
34
- catch (error) {
35
- return failure(new Error(`Failed to enable policy types: ${getErrorMessage(error)}`));
36
- }
37
- }
1
+ import{EnablePolicyTypeCommand as i,PolicyType as r}from"@aws-sdk/client-organizations";import{success as a,failure as c}from"@fjall/generator";import{getErrorMessage as y}from"@fjall/util";import{extractErrorName as l,SDK_TIMEOUT_MS as p}from"./types.js";const m=[r.SERVICE_CONTROL_POLICY,r.TAG_POLICY,r.BACKUP_POLICY,r.AISERVICES_OPT_OUT_POLICY];async function C(t,n){try{for(const e of m)try{await t.send(new i({RootId:n,PolicyType:e}),{abortSignal:AbortSignal.timeout(p)})}catch(o){if(l(o)==="PolicyTypeAlreadyEnabledException")continue;throw o}return a(void 0)}catch(e){return c(new Error(`Failed to enable policy types: ${y(e)}`))}}export{C as enablePolicyTypes};