@beesolve/aws-accounts 1.0.7 → 1.2.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.
package/README.md CHANGED
@@ -11,7 +11,12 @@ Config-driven management for AWS Organizations and IAM Identity Center. Define y
11
11
  npm install @beesolve/aws-accounts
12
12
  ```
13
13
 
14
- Requires Node.js 24+ and valid AWS credentials (via environment, profile, or SSO).
14
+ ## Prerequisites
15
+
16
+ - **Node.js 24+**
17
+ - **AWS Organization** with all features enabled
18
+ - **IAM Identity Center** enabled in the organization's management account (or delegated admin account)
19
+ - **AWS credentials** with access to the management account (via environment, profile, or SSO)
15
20
 
16
21
  ## Quick Start
17
22
 
@@ -20,7 +25,7 @@ Requires Node.js 24+ and valid AWS credentials (via environment, profile, or SSO
20
25
  mkdir my-org && cd my-org
21
26
  npm init -y
22
27
  npm pkg set type=module
23
- npm install @beesolve/aws-accounts
28
+ npm install @beesolve/aws-accounts typescript
24
29
 
25
30
  # 2. Initialize git and add a .gitignore
26
31
  git init
@@ -54,7 +59,9 @@ After `init`, `aws.config.ts` is your source of truth. Edit it to add accounts,
54
59
  | `apply` | Executes planned operations via Lambda |
55
60
  | `upgrade` | Updates the deployed Lambda function code |
56
61
  | `scan` | Refreshes remote state in S3 (advanced/recovery use) |
62
+ | `validate` | Validates `aws.config.ts` locally without hitting AWS |
57
63
  | `graveyard` | Lists accounts parked in the Graveyard OU |
64
+ | `profile` | Generates an AWS CLI SSO profile block from local state |
58
65
 
59
66
  ## Workflow
60
67
 
@@ -72,6 +79,20 @@ After `init`, your project contains:
72
79
  - **`aws.config.ts`** — your desired state: OUs, accounts, users, groups, permission sets, assignments
73
80
  - **`aws.config.types.ts`** — generated types and helpers for IDE autocomplete
74
81
 
82
+ ### Permission Sets
83
+
84
+ ```ts
85
+ permissionSets: [
86
+ {
87
+ name: "AdminAccess",
88
+ description: "Full administrator access",
89
+ sessionDuration: "PT8H", // ISO-8601 duration; omit to use the AWS default of 1h (max 12h)
90
+ awsManagedPolicies: ["arn:aws:iam::aws:policy/AdministratorAccess"],
91
+ customerManagedPolicies: [],
92
+ },
93
+ ],
94
+ ```
95
+
75
96
  ### IAM Policy Helpers
76
97
 
77
98
  `aws.config.types.ts` exports `iam` helpers with service-scoped action autocomplete:
@@ -102,10 +123,32 @@ When `init` generates your config, recognized IAM actions in inline policies are
102
123
  - Update group descriptions
103
124
  - Manage group memberships
104
125
  - Create, update, and delete permission sets
126
+ - Set permission set session duration (ISO-8601, e.g. `"PT8H"` — default 1h, max 12h)
105
127
  - Manage inline policies, AWS managed policies, and customer-managed policy references
106
128
  - Grant and revoke account assignments
107
129
  - Reprovision changed permission sets
108
130
 
131
+ ## Validating your config
132
+
133
+ Run `validate` before `plan` to catch mistakes locally without making any AWS API calls:
134
+
135
+ ```bash
136
+ npx aws-accounts validate
137
+ ```
138
+
139
+ It checks two layers:
140
+
141
+ **Schema and reference errors** — caught by compiling `aws.config.ts` against the generated types in `aws.config.types.ts`:
142
+ - Type mismatches and missing required fields
143
+ - References to unknown OUs, accounts, groups, users, or permission sets (enforced by the generated picklist types)
144
+
145
+ **Semantic errors** — additional checks run after the schema passes:
146
+ - Circular OU parent references (e.g. OU A has `parentName: "B"` and B has `parentName: "A"`)
147
+ - Assignments with no principal or with both `group` and `user` set
148
+ - Permission set inline policies exceeding the 10,240 character limit
149
+
150
+ Exits with code 1 if any errors are found, making it safe to use in CI before running `plan`.
151
+
109
152
  ## Plan/Apply Safety
110
153
 
111
154
  - `plan` fetches current remote state from S3 before computing the diff.
@@ -140,20 +183,46 @@ npx aws-accounts plan # review remaining diff
140
183
  npx aws-accounts apply # re-apply (add --allow-destructive if needed)
141
184
  ```
142
185
 
186
+ ## Generating AWS CLI profiles
187
+
188
+ The `profile` command reads your local state cache and presents an interactive picker of every account/permission-set combination you have access to, then prints a ready-to-paste `~/.aws/config` block:
189
+
190
+ ```bash
191
+ npx aws-accounts profile --sso-start-url https://d-xxxxxxxxxx.awsapps.com/start
192
+ ```
193
+
194
+ ```ini
195
+ [profile my-account-admin-access]
196
+ sso_session = sso
197
+ sso_account_id = 123456789012
198
+ sso_role_name = AdminAccess
199
+
200
+ [sso-session sso]
201
+ sso_start_url = https://d-xxxxxxxxxx.awsapps.com/start
202
+ sso_region = eu-central-1
203
+ sso_registration_scopes = sso:account:access
204
+ ```
205
+
206
+ The SSO start URL is not returned by the AWS API — set it via the flag or the `AWS_SSO_START_URL` environment variable to avoid typing it every time. Use `--sso-session <name>` to customise the session name (default: `sso`).
207
+
208
+ Requires a populated local state cache — run `plan` or `scan` first if the cache is empty.
209
+
143
210
  ## CLI Options
144
211
 
145
212
  ```
146
213
  npx aws-accounts <command> [options]
147
214
 
148
215
  Options:
149
- --profile <name> AWS profile (fallback: AWS_PROFILE)
150
- --region <region> AWS region (fallback: AWS_REGION, AWS_DEFAULT_REGION)
151
- --yes Skip interactive confirmations
152
- --json Output plan as JSON (plan command)
153
- --allow-destructive Allow destructive operations (apply command)
154
- --ignore-unsupported Proceed with non-destructive unsupported diffs (apply command)
155
- --refresh Force state refresh before planning (plan command)
156
- --help Show help
216
+ --profile <name> AWS profile (fallback: AWS_PROFILE)
217
+ --region <region> AWS region (fallback: AWS_REGION, AWS_DEFAULT_REGION)
218
+ --yes Skip interactive confirmations
219
+ --json Output plan as JSON (plan command)
220
+ --allow-destructive Allow destructive operations (apply command)
221
+ --ignore-unsupported Proceed with non-destructive unsupported diffs (apply command)
222
+ --refresh Force state refresh before planning (plan command)
223
+ --sso-start-url <url> IAM Identity Center access portal URL (fallback: AWS_SSO_START_URL)
224
+ --sso-session <name> SSO session name for profile output (default: sso)
225
+ --help Show help
157
226
  ```
158
227
 
159
228
  ## IAM Permissions
@@ -173,7 +242,7 @@ The CLI delegates all AWS operations to a deployed Lambda. Day-to-day usage requ
173
242
 
174
243
  `bootstrap` and `upgrade` require broader permissions for deploying infrastructure (S3, IAM, Lambda, SSO). See the full policy in the [docs](./docs/adr/002-architecture-and-technology-choices.md).
175
244
 
176
- Commands that need no AWS permissions: `regenerate` (local codegen only), `graveyard` (reads local cache only).
245
+ Commands that need no AWS permissions: `regenerate` (local codegen only), `validate` (local config checks only), `graveyard` (reads local cache only).
177
246
 
178
247
  ## FAQ
179
248
 
@@ -1,15 +1,22 @@
1
1
  import {
2
- PutAccountNameCommand
2
+ DeleteAlternateContactCommand,
3
+ PutAccountNameCommand,
4
+ PutAlternateContactCommand
3
5
  } from "@aws-sdk/client-account";
4
6
  import {
7
+ AttachPolicyCommand,
5
8
  CreateOrganizationalUnitCommand,
9
+ CreatePolicyCommand,
6
10
  DeleteOrganizationalUnitCommand,
11
+ DeletePolicyCommand,
12
+ DetachPolicyCommand,
7
13
  ListAccountsForParentCommand,
8
14
  ListOrganizationalUnitsForParentCommand,
9
15
  MoveAccountCommand,
10
16
  TagResourceCommand,
11
17
  UntagResourceCommand,
12
- UpdateOrganizationalUnitCommand
18
+ UpdateOrganizationalUnitCommand,
19
+ UpdatePolicyCommand
13
20
  } from "@aws-sdk/client-organizations";
14
21
  import {
15
22
  CreateGroupMembershipCommand,
@@ -37,6 +44,7 @@ import {
37
44
  DetachManagedPolicyFromPermissionSetCommand,
38
45
  ProvisionPermissionSetCommand,
39
46
  PutInlinePolicyToPermissionSetCommand,
47
+ UpdateInstanceAccessControlAttributeConfigurationCommand,
40
48
  UpdatePermissionSetCommand
41
49
  } from "@aws-sdk/client-sso-admin";
42
50
  import { createAccountAndMoveToOu } from "./accountCreation.js";
@@ -44,6 +52,7 @@ import { assertUnreachable, delay } from "./helpers.js";
44
52
  import {
45
53
  addGroupMembershipToWorkingState,
46
54
  addAccountAssignmentToWorkingState,
55
+ addOrgPolicyAttachmentToWorkingState,
47
56
  createGroupMembershipKey,
48
57
  moveAccountInWorkingState,
49
58
  removeAccountAssignmentFromWorkingState,
@@ -52,12 +61,15 @@ import {
52
61
  removeIdcPermissionSetFromWorkingState,
53
62
  removeIdcUserFromWorkingState,
54
63
  removeOrganizationalUnitFromWorkingState,
64
+ removeOrgPolicyAttachmentFromWorkingState,
65
+ removeOrgPolicyFromWorkingState,
55
66
  renameOrganizationalUnitInWorkingState,
56
67
  upsertIdcGroupInWorkingState,
57
68
  upsertIdcPermissionSetInWorkingState,
58
69
  upsertIdcUserInWorkingState,
59
70
  upsertAccountInWorkingState,
60
- upsertOrganizationalUnitInWorkingState
71
+ upsertOrganizationalUnitInWorkingState,
72
+ upsertOrgPolicyInWorkingState
61
73
  } from "./state.js";
62
74
  async function executeOperation(props) {
63
75
  const operation = props.operation;
@@ -204,7 +216,10 @@ async function executeOperation(props) {
204
216
  workingState: props.state,
205
217
  account: {
206
218
  ...account,
207
- tags: Object.entries(operation.tags).map(([key, value]) => ({ key, value }))
219
+ tags: Object.entries(operation.tags).map(([key, value]) => ({
220
+ key,
221
+ value
222
+ }))
208
223
  }
209
224
  });
210
225
  }
@@ -246,7 +261,9 @@ async function executeOperation(props) {
246
261
  DestinationParentId: operation.toOuId
247
262
  })
248
263
  );
249
- props.logger.log(`Done: "${operation.accountName}" -> ${operation.toOuName}`);
264
+ props.logger.log(
265
+ `Done: "${operation.accountName}" -> ${operation.toOuName}`
266
+ );
250
267
  return moveAccountInWorkingState({
251
268
  workingState: props.state,
252
269
  accountId: operation.accountId,
@@ -473,7 +490,8 @@ async function executeOperation(props) {
473
490
  new CreatePermissionSetCommand({
474
491
  InstanceArn: props.state.identityCenter.instanceArn,
475
492
  Name: operation.permissionSetName,
476
- Description: operation.description.length > 0 ? operation.description : void 0
493
+ Description: operation.description.length > 0 ? operation.description : void 0,
494
+ SessionDuration: operation.sessionDuration ?? void 0
477
495
  })
478
496
  );
479
497
  const permissionSetArn = response.PermissionSet?.PermissionSetArn;
@@ -489,6 +507,7 @@ async function executeOperation(props) {
489
507
  permissionSetArn,
490
508
  name: operation.permissionSetName,
491
509
  description: operation.description,
510
+ sessionDuration: operation.sessionDuration,
492
511
  inlinePolicy: null,
493
512
  awsManagedPolicies: [],
494
513
  customerManagedPolicies: []
@@ -519,6 +538,30 @@ async function executeOperation(props) {
519
538
  }
520
539
  });
521
540
  }
541
+ if (operation.kind === "updateIdcPermissionSetSessionDuration") {
542
+ const permissionSet = resolvePermissionSetByName({
543
+ state: props.state,
544
+ permissionSetName: operation.permissionSetName
545
+ });
546
+ props.logger.log(
547
+ `Updating IdC permission set session duration for "${operation.permissionSetName}"...`
548
+ );
549
+ await props.ssoAdminClient.send(
550
+ new UpdatePermissionSetCommand({
551
+ InstanceArn: props.state.identityCenter.instanceArn,
552
+ PermissionSetArn: permissionSet.permissionSetArn,
553
+ SessionDuration: operation.sessionDuration ?? void 0
554
+ })
555
+ );
556
+ props.logger.log(`Done: "${operation.permissionSetName}"`);
557
+ return upsertIdcPermissionSetInWorkingState({
558
+ workingState: props.state,
559
+ permissionSet: {
560
+ ...permissionSet,
561
+ sessionDuration: operation.sessionDuration
562
+ }
563
+ });
564
+ }
522
565
  if (operation.kind === "deleteIdcPermissionSet") {
523
566
  const permissionSet = resolvePermissionSetByName({
524
567
  state: props.state,
@@ -880,6 +923,218 @@ async function executeOperation(props) {
880
923
  }
881
924
  });
882
925
  }
926
+ if (operation.kind === "createOrgPolicy") {
927
+ props.logger.log(
928
+ `Creating org policy "${operation.policyName}" (${operation.policyType})...`
929
+ );
930
+ const response = await props.organizationsClient.send(
931
+ new CreatePolicyCommand({
932
+ Name: operation.policyName,
933
+ Description: operation.description.length > 0 ? operation.description : void 0,
934
+ Content: operation.content,
935
+ Type: operation.policyType
936
+ })
937
+ );
938
+ const policy = response.Policy?.PolicySummary;
939
+ if (policy?.Id == null || policy.Arn == null) {
940
+ throw new Error(
941
+ `CreatePolicy for "${operation.policyName}" returned incomplete data.`
942
+ );
943
+ }
944
+ props.logger.log(`Done: "${operation.policyName}"`);
945
+ return upsertOrgPolicyInWorkingState({
946
+ workingState: props.state,
947
+ policy: {
948
+ id: policy.Id,
949
+ arn: policy.Arn,
950
+ name: operation.policyName,
951
+ description: operation.description,
952
+ type: operation.policyType,
953
+ content: operation.content
954
+ }
955
+ });
956
+ }
957
+ if (operation.kind === "updateOrgPolicyContent") {
958
+ props.logger.log(`Updating org policy content "${operation.policyName}"...`);
959
+ await props.organizationsClient.send(
960
+ new UpdatePolicyCommand({
961
+ PolicyId: operation.policyId,
962
+ Content: operation.content
963
+ })
964
+ );
965
+ props.logger.log(`Done: "${operation.policyName}"`);
966
+ const currentPolicy = props.state.organization.policiesById[operation.policyId];
967
+ if (currentPolicy == null) {
968
+ return props.state;
969
+ }
970
+ return upsertOrgPolicyInWorkingState({
971
+ workingState: props.state,
972
+ policy: { ...currentPolicy, content: operation.content }
973
+ });
974
+ }
975
+ if (operation.kind === "updateOrgPolicyDescription") {
976
+ props.logger.log(
977
+ `Updating org policy description "${operation.policyName}"...`
978
+ );
979
+ await props.organizationsClient.send(
980
+ new UpdatePolicyCommand({
981
+ PolicyId: operation.policyId,
982
+ Description: operation.description
983
+ })
984
+ );
985
+ props.logger.log(`Done: "${operation.policyName}"`);
986
+ const currentPolicy = props.state.organization.policiesById[operation.policyId];
987
+ if (currentPolicy == null) {
988
+ return props.state;
989
+ }
990
+ return upsertOrgPolicyInWorkingState({
991
+ workingState: props.state,
992
+ policy: { ...currentPolicy, description: operation.description }
993
+ });
994
+ }
995
+ if (operation.kind === "attachOrgPolicy") {
996
+ props.logger.log(
997
+ `Attaching org policy "${operation.policyName}" to "${operation.targetName}"...`
998
+ );
999
+ const resolvedPolicyId = resolvePolicyId({
1000
+ state: props.state,
1001
+ policyId: operation.policyId,
1002
+ policyName: operation.policyName
1003
+ });
1004
+ await props.organizationsClient.send(
1005
+ new AttachPolicyCommand({
1006
+ PolicyId: resolvedPolicyId,
1007
+ TargetId: operation.targetId
1008
+ })
1009
+ );
1010
+ props.logger.log(`Done: "${operation.policyName}" -> "${operation.targetName}"`);
1011
+ const targetType = operation.targetId === props.context.organization.rootId ? "ROOT" : props.state.organization.organizationalUnitsById[operation.targetId] != null ? "ORGANIZATIONAL_UNIT" : "ACCOUNT";
1012
+ return addOrgPolicyAttachmentToWorkingState({
1013
+ workingState: props.state,
1014
+ attachment: {
1015
+ policyId: resolvedPolicyId,
1016
+ targetId: operation.targetId,
1017
+ targetType
1018
+ }
1019
+ });
1020
+ }
1021
+ if (operation.kind === "detachOrgPolicy") {
1022
+ props.logger.log(
1023
+ `Detaching org policy "${operation.policyName}" from "${operation.targetName}"...`
1024
+ );
1025
+ await props.organizationsClient.send(
1026
+ new DetachPolicyCommand({
1027
+ PolicyId: operation.policyId,
1028
+ TargetId: operation.targetId
1029
+ })
1030
+ );
1031
+ props.logger.log(`Done: "${operation.policyName}" x "${operation.targetName}"`);
1032
+ return removeOrgPolicyAttachmentFromWorkingState({
1033
+ workingState: props.state,
1034
+ policyId: operation.policyId,
1035
+ targetId: operation.targetId
1036
+ });
1037
+ }
1038
+ if (operation.kind === "deleteOrgPolicy") {
1039
+ props.logger.log(`Deleting org policy "${operation.policyName}"...`);
1040
+ await props.organizationsClient.send(
1041
+ new DeletePolicyCommand({ PolicyId: operation.policyId })
1042
+ );
1043
+ props.logger.log(`Done: "${operation.policyName}"`);
1044
+ return removeOrgPolicyFromWorkingState({
1045
+ workingState: props.state,
1046
+ policyId: operation.policyId
1047
+ });
1048
+ }
1049
+ if (operation.kind === "putAlternateContact") {
1050
+ props.logger.log(
1051
+ `Setting ${operation.contactType} alternate contact for "${operation.accountName}" (${operation.accountId})...`
1052
+ );
1053
+ await props.accountClient.send(
1054
+ new PutAlternateContactCommand({
1055
+ AccountId: operation.accountId,
1056
+ AlternateContactType: operation.contactType,
1057
+ Name: operation.name,
1058
+ EmailAddress: operation.email,
1059
+ PhoneNumber: operation.phone,
1060
+ Title: operation.title
1061
+ })
1062
+ );
1063
+ props.logger.log(`Done: ${operation.contactType} contact for "${operation.accountName}"`);
1064
+ const account = props.state.organization.accountsById[operation.accountId];
1065
+ if (account == null) {
1066
+ throw new Error(
1067
+ `Could not resolve account (${operation.accountId}) in working state.`
1068
+ );
1069
+ }
1070
+ const updatedContacts = [
1071
+ ...(account.alternateContacts ?? []).filter(
1072
+ (c) => c.contactType !== operation.contactType
1073
+ ),
1074
+ {
1075
+ contactType: operation.contactType,
1076
+ name: operation.name,
1077
+ email: operation.email,
1078
+ phone: operation.phone,
1079
+ title: operation.title
1080
+ }
1081
+ ];
1082
+ return upsertAccountInWorkingState({
1083
+ workingState: props.state,
1084
+ account: { ...account, alternateContacts: updatedContacts }
1085
+ });
1086
+ }
1087
+ if (operation.kind === "deleteAlternateContact") {
1088
+ props.logger.log(
1089
+ `Deleting ${operation.contactType} alternate contact for "${operation.accountName}" (${operation.accountId})...`
1090
+ );
1091
+ await props.accountClient.send(
1092
+ new DeleteAlternateContactCommand({
1093
+ AccountId: operation.accountId,
1094
+ AlternateContactType: operation.contactType
1095
+ })
1096
+ );
1097
+ props.logger.log(`Done: removed ${operation.contactType} contact for "${operation.accountName}"`);
1098
+ const account = props.state.organization.accountsById[operation.accountId];
1099
+ if (account == null) {
1100
+ throw new Error(
1101
+ `Could not resolve account (${operation.accountId}) in working state.`
1102
+ );
1103
+ }
1104
+ return upsertAccountInWorkingState({
1105
+ workingState: props.state,
1106
+ account: {
1107
+ ...account,
1108
+ alternateContacts: (account.alternateContacts ?? []).filter(
1109
+ (c) => c.contactType !== operation.contactType
1110
+ )
1111
+ }
1112
+ });
1113
+ }
1114
+ if (operation.kind === "setIdcAccessControlAttributes") {
1115
+ props.logger.log(
1116
+ `Setting IdC access control attributes (${operation.attributes.length} attribute(s))...`
1117
+ );
1118
+ await props.ssoAdminClient.send(
1119
+ new UpdateInstanceAccessControlAttributeConfigurationCommand({
1120
+ InstanceArn: props.state.identityCenter.instanceArn,
1121
+ InstanceAccessControlAttributeConfiguration: {
1122
+ AccessControlAttributes: operation.attributes.map((attr) => ({
1123
+ Key: attr.key,
1124
+ Value: { Source: attr.source }
1125
+ }))
1126
+ }
1127
+ })
1128
+ );
1129
+ props.logger.log(`Done: access control attributes updated`);
1130
+ return {
1131
+ ...props.state,
1132
+ identityCenter: {
1133
+ ...props.state.identityCenter,
1134
+ accessControlAttributes: operation.attributes
1135
+ }
1136
+ };
1137
+ }
883
1138
  assertUnreachable(operation, "Unsupported operation kind in apply.");
884
1139
  }
885
1140
  function resolveAssignmentDependencies(props) {
@@ -940,6 +1195,16 @@ function resolveGroupByDisplayName(props) {
940
1195
  }
941
1196
  return group;
942
1197
  }
1198
+ function resolvePolicyId(props) {
1199
+ if (props.policyId !== "__pending_creation__") return props.policyId;
1200
+ const policy = props.state.organization.policiesByName[props.policyName];
1201
+ if (policy == null) {
1202
+ throw new Error(
1203
+ `Could not resolve policy "${props.policyName}" in working state.`
1204
+ );
1205
+ }
1206
+ return policy.id;
1207
+ }
943
1208
  function resolvePermissionSetByName(props) {
944
1209
  const permissionSet = props.state.identityCenter.permissionSetsByName[props.permissionSetName];
945
1210
  if (permissionSet == null) {
@@ -959,9 +1224,9 @@ function upsertPermissionSetPolicyState(props) {
959
1224
  workingState: props.state,
960
1225
  permissionSet: {
961
1226
  ...nextPermissionSet,
962
- awsManagedPolicies: [...new Set(nextPermissionSet.awsManagedPolicies)].sort(
963
- (left, right) => left.localeCompare(right)
964
- ),
1227
+ awsManagedPolicies: [
1228
+ ...new Set(nextPermissionSet.awsManagedPolicies)
1229
+ ].sort((left, right) => left.localeCompare(right)),
965
1230
  customerManagedPolicies: [
966
1231
  ...nextPermissionSet.customerManagedPolicies
967
1232
  ].sort((left, right) => {
@@ -992,11 +1257,13 @@ async function assertOrganizationalUnitIsEmpty(props) {
992
1257
  });
993
1258
  if (childOrganizationalUnit != null) {
994
1259
  throw new Error(
995
- `Refusing to delete OU "${props.organizationalUnitName}": live AWS preflight failed [child-ou-present]: ${formatLivePreflightResource({
996
- resourceType: "child OU",
997
- name: childOrganizationalUnit.Name,
998
- id: childOrganizationalUnit.Id
999
- })} is still attached.`
1260
+ `Refusing to delete OU "${props.organizationalUnitName}": live AWS preflight failed [child-ou-present]: ${formatLivePreflightResource(
1261
+ {
1262
+ resourceType: "child OU",
1263
+ name: childOrganizationalUnit.Name,
1264
+ id: childOrganizationalUnit.Id
1265
+ }
1266
+ )} is still attached.`
1000
1267
  );
1001
1268
  }
1002
1269
  const account = await listFirstAccountForParent({
@@ -1005,11 +1272,13 @@ async function assertOrganizationalUnitIsEmpty(props) {
1005
1272
  });
1006
1273
  if (account != null) {
1007
1274
  throw new Error(
1008
- `Refusing to delete OU "${props.organizationalUnitName}": live AWS preflight failed [account-present]: ${formatLivePreflightResource({
1009
- resourceType: "account",
1010
- name: account.Name,
1011
- id: account.Id
1012
- })} is still attached.`
1275
+ `Refusing to delete OU "${props.organizationalUnitName}": live AWS preflight failed [account-present]: ${formatLivePreflightResource(
1276
+ {
1277
+ resourceType: "account",
1278
+ name: account.Name,
1279
+ id: account.Id
1280
+ }
1281
+ )} is still attached.`
1013
1282
  );
1014
1283
  }
1015
1284
  }