@beesolve/aws-accounts 1.0.7 → 1.1.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
 
@@ -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,6 +1,4 @@
1
- import {
2
- PutAccountNameCommand
3
- } from "@aws-sdk/client-account";
1
+ import { PutAccountNameCommand } from "@aws-sdk/client-account";
4
2
  import {
5
3
  CreateOrganizationalUnitCommand,
6
4
  DeleteOrganizationalUnitCommand,
@@ -204,7 +202,10 @@ async function executeOperation(props) {
204
202
  workingState: props.state,
205
203
  account: {
206
204
  ...account,
207
- tags: Object.entries(operation.tags).map(([key, value]) => ({ key, value }))
205
+ tags: Object.entries(operation.tags).map(([key, value]) => ({
206
+ key,
207
+ value
208
+ }))
208
209
  }
209
210
  });
210
211
  }
@@ -246,7 +247,9 @@ async function executeOperation(props) {
246
247
  DestinationParentId: operation.toOuId
247
248
  })
248
249
  );
249
- props.logger.log(`Done: "${operation.accountName}" -> ${operation.toOuName}`);
250
+ props.logger.log(
251
+ `Done: "${operation.accountName}" -> ${operation.toOuName}`
252
+ );
250
253
  return moveAccountInWorkingState({
251
254
  workingState: props.state,
252
255
  accountId: operation.accountId,
@@ -473,7 +476,8 @@ async function executeOperation(props) {
473
476
  new CreatePermissionSetCommand({
474
477
  InstanceArn: props.state.identityCenter.instanceArn,
475
478
  Name: operation.permissionSetName,
476
- Description: operation.description.length > 0 ? operation.description : void 0
479
+ Description: operation.description.length > 0 ? operation.description : void 0,
480
+ SessionDuration: operation.sessionDuration ?? void 0
477
481
  })
478
482
  );
479
483
  const permissionSetArn = response.PermissionSet?.PermissionSetArn;
@@ -489,6 +493,7 @@ async function executeOperation(props) {
489
493
  permissionSetArn,
490
494
  name: operation.permissionSetName,
491
495
  description: operation.description,
496
+ sessionDuration: operation.sessionDuration,
492
497
  inlinePolicy: null,
493
498
  awsManagedPolicies: [],
494
499
  customerManagedPolicies: []
@@ -519,6 +524,30 @@ async function executeOperation(props) {
519
524
  }
520
525
  });
521
526
  }
527
+ if (operation.kind === "updateIdcPermissionSetSessionDuration") {
528
+ const permissionSet = resolvePermissionSetByName({
529
+ state: props.state,
530
+ permissionSetName: operation.permissionSetName
531
+ });
532
+ props.logger.log(
533
+ `Updating IdC permission set session duration for "${operation.permissionSetName}"...`
534
+ );
535
+ await props.ssoAdminClient.send(
536
+ new UpdatePermissionSetCommand({
537
+ InstanceArn: props.state.identityCenter.instanceArn,
538
+ PermissionSetArn: permissionSet.permissionSetArn,
539
+ SessionDuration: operation.sessionDuration ?? void 0
540
+ })
541
+ );
542
+ props.logger.log(`Done: "${operation.permissionSetName}"`);
543
+ return upsertIdcPermissionSetInWorkingState({
544
+ workingState: props.state,
545
+ permissionSet: {
546
+ ...permissionSet,
547
+ sessionDuration: operation.sessionDuration
548
+ }
549
+ });
550
+ }
522
551
  if (operation.kind === "deleteIdcPermissionSet") {
523
552
  const permissionSet = resolvePermissionSetByName({
524
553
  state: props.state,
@@ -959,9 +988,9 @@ function upsertPermissionSetPolicyState(props) {
959
988
  workingState: props.state,
960
989
  permissionSet: {
961
990
  ...nextPermissionSet,
962
- awsManagedPolicies: [...new Set(nextPermissionSet.awsManagedPolicies)].sort(
963
- (left, right) => left.localeCompare(right)
964
- ),
991
+ awsManagedPolicies: [
992
+ ...new Set(nextPermissionSet.awsManagedPolicies)
993
+ ].sort((left, right) => left.localeCompare(right)),
965
994
  customerManagedPolicies: [
966
995
  ...nextPermissionSet.customerManagedPolicies
967
996
  ].sort((left, right) => {
@@ -992,11 +1021,13 @@ async function assertOrganizationalUnitIsEmpty(props) {
992
1021
  });
993
1022
  if (childOrganizationalUnit != null) {
994
1023
  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.`
1024
+ `Refusing to delete OU "${props.organizationalUnitName}": live AWS preflight failed [child-ou-present]: ${formatLivePreflightResource(
1025
+ {
1026
+ resourceType: "child OU",
1027
+ name: childOrganizationalUnit.Name,
1028
+ id: childOrganizationalUnit.Id
1029
+ }
1030
+ )} is still attached.`
1000
1031
  );
1001
1032
  }
1002
1033
  const account = await listFirstAccountForParent({
@@ -1005,11 +1036,13 @@ async function assertOrganizationalUnitIsEmpty(props) {
1005
1036
  });
1006
1037
  if (account != null) {
1007
1038
  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.`
1039
+ `Refusing to delete OU "${props.organizationalUnitName}": live AWS preflight failed [account-present]: ${formatLivePreflightResource(
1040
+ {
1041
+ resourceType: "account",
1042
+ name: account.Name,
1043
+ id: account.Id
1044
+ }
1045
+ )} is still attached.`
1013
1046
  );
1014
1047
  }
1015
1048
  }
package/dist/awsConfig.js CHANGED
@@ -89,6 +89,7 @@ const awsConfigModelSchema = v.strictObject({
89
89
  v.strictObject({
90
90
  name: v.string(),
91
91
  description: v.string(),
92
+ sessionDuration: v.optional(v.string()),
92
93
  inlinePolicy: v.optional(iamPolicyDocumentSchema),
93
94
  awsManagedPolicies: v.array(v.string()),
94
95
  customerManagedPolicies: v.array(
@@ -432,6 +433,7 @@ function mapStateToAwsConfig(props) {
432
433
  (permissionSet) => ({
433
434
  name: permissionSet.name,
434
435
  description: permissionSet.description,
436
+ sessionDuration: permissionSet.sessionDuration ?? void 0,
435
437
  inlinePolicy: permissionSet.inlinePolicy == null ? void 0 : parseInlinePolicyForConfig({
436
438
  permissionSetName: permissionSet.name,
437
439
  inlinePolicy: permissionSet.inlinePolicy
@@ -667,6 +669,7 @@ function mapAwsConfigToState(props) {
667
669
  permissionSetArn: matchedPermissionSet?.permissionSetArn ?? pendingCreationId,
668
670
  name: permissionSet.name,
669
671
  description: permissionSet.description,
672
+ sessionDuration: permissionSet.sessionDuration ?? null,
670
673
  inlinePolicy: stableStringifyInlinePolicy(permissionSet.inlinePolicy),
671
674
  awsManagedPolicies: [...permissionSet.awsManagedPolicies],
672
675
  customerManagedPolicies: permissionSet.customerManagedPolicies.map(
@@ -934,10 +937,14 @@ function renderPolicyActionString(value) {
934
937
  if (knownActions == null || knownActions.includes(actionName) === false) {
935
938
  return JSON.stringify(value);
936
939
  }
937
- if (isIdentifierSafeServicePrefix(servicePrefix)) {
938
- return `iam.${servicePrefix}(${JSON.stringify(actionName)})`;
940
+ const fnName = servicePrefixToCamelCase(servicePrefix);
941
+ if (isIdentifierSafeServicePrefix(fnName)) {
942
+ return `iam.${fnName}(${JSON.stringify(actionName)})`;
939
943
  }
940
- return `iam[${JSON.stringify(servicePrefix)}](${JSON.stringify(actionName)})`;
944
+ return `iam[${JSON.stringify(fnName)}](${JSON.stringify(actionName)})`;
945
+ }
946
+ function servicePrefixToCamelCase(value) {
947
+ return value.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
941
948
  }
942
949
  function isIdentifierSafeServicePrefix(value) {
943
950
  return /^[A-Za-z_$][A-Za-z0-9_$]*$/u.test(value);
@@ -974,9 +981,8 @@ function renderAwsConfigTypesTs(props) {
974
981
  });
975
982
  return `import * as v from "valibot";
976
983
  import { iamPolicyDocumentSchema } from "@beesolve/iam-policy-ts";
984
+ export * as iam from "@beesolve/iam-policy-ts";
977
985
  export {
978
- iam,
979
- iamAction,
980
986
  iamActionCatalog,
981
987
  iamActionCatalogActionCount,
982
988
  iamActionCatalogSourceSha256,
@@ -992,10 +998,6 @@ export {
992
998
  assertIamPolicyDocumentStrict,
993
999
  } from "@beesolve/iam-policy-ts";
994
1000
  export type {
995
- IamActionCatalog,
996
- IamPolicyServicePrefix,
997
- IamPolicyActionNameByService,
998
- IamPolicyActionForService,
999
1001
  IamPolicyVersion,
1000
1002
  IamPolicyScalar,
1001
1003
  IamPolicyScalarList,
@@ -1055,6 +1057,7 @@ export const awsConfigSchema = v.strictObject({
1055
1057
  v.strictObject({
1056
1058
  name: v.string(),
1057
1059
  description: v.string(),
1060
+ sessionDuration: v.optional(v.string()),
1058
1061
  inlinePolicy: v.optional(iamPolicyDocumentSchema),
1059
1062
  awsManagedPolicies: v.array(v.string()),
1060
1063
  customerManagedPolicies: v.array(
package/dist/cli.js CHANGED
@@ -12,7 +12,9 @@ import {
12
12
  } from "./awsClientConfig.js";
13
13
  import { consoleLogger } from "./logger.js";
14
14
  import { runGraveyardCommand } from "./commands/graveyard.js";
15
+ import { runProfileCommand } from "./commands/profile.js";
15
16
  import { runRegenerateCommand } from "./commands/regenerate.js";
17
+ import { runValidateCommand } from "./commands/validate.js";
16
18
  import {
17
19
  runRemoteBootstrap,
18
20
  runRemoteScan,
@@ -31,7 +33,9 @@ const commands = [
31
33
  "scan",
32
34
  "init",
33
35
  "regenerate",
36
+ "validate",
34
37
  "graveyard",
38
+ "profile",
35
39
  "plan",
36
40
  "apply",
37
41
  "upgrade"
@@ -51,6 +55,8 @@ async function main() {
51
55
  "ignore-unsupported": { type: "boolean", default: false },
52
56
  "allow-destructive": { type: "boolean", default: false },
53
57
  refresh: { type: "boolean", default: false },
58
+ "sso-start-url": { type: "string" },
59
+ "sso-session": { type: "string", default: "sso" },
54
60
  help: { type: "boolean", default: false }
55
61
  },
56
62
  allowPositionals: true
@@ -87,6 +93,13 @@ async function main() {
87
93
  }
88
94
  return;
89
95
  }
96
+ if (command === "validate") {
97
+ const valid = await runValidateCommand({ logger });
98
+ if (!valid) {
99
+ process.exitCode = 1;
100
+ }
101
+ return;
102
+ }
90
103
  if (command === "graveyard") {
91
104
  await runGraveyardCommand({
92
105
  logger,
@@ -95,6 +108,22 @@ async function main() {
95
108
  });
96
109
  return;
97
110
  }
111
+ if (command === "profile") {
112
+ const ssoStartUrl = args.values["sso-start-url"] ?? process.env.AWS_SSO_START_URL;
113
+ if (ssoStartUrl == null) {
114
+ printHelp(logger);
115
+ throw toUsageError("--sso-start-url is required for the profile command (or set AWS_SSO_START_URL).");
116
+ }
117
+ await runProfileCommand({
118
+ logger,
119
+ cachePath: ".remote-state-cache.json",
120
+ contextPath,
121
+ ssoStartUrl,
122
+ ssoSession: args.values["sso-session"] ?? "sso",
123
+ isTty: process.stdin.isTTY
124
+ });
125
+ return;
126
+ }
98
127
  const overwriteConfirmation = buildOverwriteConfirmation({
99
128
  yes: args.values.yes ?? false,
100
129
  isTty: process.stdin.isTTY
@@ -152,7 +181,11 @@ function printHelp(logger) {
152
181
  " npm run cli -- init [--profile <name>] [--region <region>] [--yes]"
153
182
  );
154
183
  logger.log(" npm run cli -- regenerate [--yes]");
184
+ logger.log(" npm run cli -- validate");
155
185
  logger.log(" npm run cli -- graveyard");
186
+ logger.log(
187
+ " npm run cli -- profile --sso-start-url <url> [--sso-session <name>] (env: AWS_SSO_START_URL)"
188
+ );
156
189
  logger.log(
157
190
  " npm run cli -- plan [--profile <name>] [--region <region>] [--refresh]"
158
191
  );
@@ -0,0 +1,116 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { readAwsContextFromFile } from "../awsConfig.js";
3
+ import { readStateCache } from "../remoteStateCache.js";
4
+ async function runProfileCommand(input) {
5
+ const cache = await readStateCache(input.cachePath);
6
+ if (cache == null) {
7
+ throw new Error(
8
+ `No remote state cache found at "${input.cachePath}". Run scan or plan first.`
9
+ );
10
+ }
11
+ const context = await readAwsContextFromFile(input.contextPath);
12
+ const region = context.deployment?.region || process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || "us-east-1";
13
+ const entries = buildProfileEntries(cache.state);
14
+ if (entries.length === 0) {
15
+ input.logger.log("No account assignments found in state cache.");
16
+ return;
17
+ }
18
+ const selected = await selectEntry({ entries, logger: input.logger, isTty: input.isTty });
19
+ if (selected == null) {
20
+ return;
21
+ }
22
+ const profileName = buildProfileName(selected);
23
+ const block = renderProfileBlock({
24
+ profileName,
25
+ ssoSession: input.ssoSession,
26
+ accountId: selected.accountId,
27
+ roleName: selected.permissionSetName,
28
+ ssoStartUrl: input.ssoStartUrl,
29
+ region,
30
+ ssoRegistrationScopes: "sso:account:access"
31
+ });
32
+ input.logger.log("");
33
+ input.logger.log(block);
34
+ }
35
+ async function selectEntry(props) {
36
+ if (props.isTty !== true) {
37
+ throw new Error(
38
+ "Profile command requires an interactive terminal. Use --account and --permission-set in non-interactive mode (not yet supported)."
39
+ );
40
+ }
41
+ props.logger.log("Select an account/permission-set combination:");
42
+ props.logger.log("");
43
+ for (const [index, entry] of props.entries.entries()) {
44
+ props.logger.log(
45
+ ` ${index + 1}. ${entry.accountName} / ${entry.permissionSetName} (${entry.accountId})`
46
+ );
47
+ }
48
+ props.logger.log("");
49
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
50
+ try {
51
+ let choice;
52
+ while (choice == null) {
53
+ const answer = await rl.question(`Enter number (1-${props.entries.length}): `);
54
+ const parsed = parseInt(answer.trim(), 10);
55
+ if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= props.entries.length) {
56
+ choice = parsed;
57
+ } else {
58
+ props.logger.log(`Please enter a number between 1 and ${props.entries.length}.`);
59
+ }
60
+ }
61
+ return props.entries[choice - 1] ?? null;
62
+ } finally {
63
+ rl.close();
64
+ }
65
+ }
66
+ function buildProfileEntries(state) {
67
+ const accountById = Object.fromEntries(state.organization.accounts.map((a) => [a.id, a]));
68
+ const permissionSetByArn = Object.fromEntries(
69
+ state.identityCenter.permissionSets.map((ps) => [ps.permissionSetArn, ps])
70
+ );
71
+ const seen = /* @__PURE__ */ new Set();
72
+ const entries = [];
73
+ for (const assignment of state.identityCenter.accountAssignments) {
74
+ const key = `${assignment.accountId}|${assignment.permissionSetArn}`;
75
+ if (seen.has(key)) {
76
+ continue;
77
+ }
78
+ seen.add(key);
79
+ const account = accountById[assignment.accountId];
80
+ const permissionSet = permissionSetByArn[assignment.permissionSetArn];
81
+ if (account == null || permissionSet == null) {
82
+ continue;
83
+ }
84
+ entries.push({
85
+ accountId: account.id,
86
+ accountName: account.name,
87
+ permissionSetName: permissionSet.name
88
+ });
89
+ }
90
+ return entries.sort((a, b) => {
91
+ const accountCmp = a.accountName.localeCompare(b.accountName);
92
+ return accountCmp !== 0 ? accountCmp : a.permissionSetName.localeCompare(b.permissionSetName);
93
+ });
94
+ }
95
+ function buildProfileName(entry) {
96
+ return `${toKebabCase(entry.accountName)}-${toKebabCase(entry.permissionSetName)}`;
97
+ }
98
+ function toKebabCase(value) {
99
+ return value.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase().replace(/[^a-z0-9-]/g, "");
100
+ }
101
+ function renderProfileBlock(props) {
102
+ return [
103
+ `[profile ${props.profileName}]`,
104
+ `sso_session = ${props.ssoSession}`,
105
+ `sso_account_id = ${props.accountId}`,
106
+ `sso_role_name = ${props.roleName}`,
107
+ ``,
108
+ `[sso-session ${props.ssoSession}]`,
109
+ `sso_start_url = ${props.ssoStartUrl}`,
110
+ `sso_region = ${props.region}`,
111
+ `sso_registration_scopes = ${props.ssoRegistrationScopes}`
112
+ ].join("\n");
113
+ }
114
+ export {
115
+ runProfileCommand
116
+ };
@@ -51,7 +51,7 @@ import {
51
51
  import { applyReservedOuDeletionGuard } from "../reservedOuDeletion.js";
52
52
  import { validateState } from "../state.js";
53
53
  import { assertUnreachable, delay } from "../helpers.js";
54
- import { iam } from "@beesolve/iam-policy-ts";
54
+ import { sts, organizations, sso, identitystore, s3, logs, account, iam, lambda } from "@beesolve/iam-policy-ts";
55
55
  const remoteCommandSchema = v.object({
56
56
  subcommand: v.picklist(["bootstrap", "scan", "init", "plan", "apply", "upgrade"]),
57
57
  profile: v.optional(v.string()),
@@ -179,7 +179,7 @@ async function ensureIamRole(props) {
179
179
  {
180
180
  Effect: "Allow",
181
181
  Principal: { Service: "lambda.amazonaws.com" },
182
- Action: iam.sts("AssumeRole")
182
+ Action: sts("AssumeRole")
183
183
  }
184
184
  ]
185
185
  });
@@ -193,17 +193,17 @@ async function ensureIamRole(props) {
193
193
  Statement: [
194
194
  {
195
195
  Effect: "Allow",
196
- Action: iam.organizations("*"),
196
+ Action: organizations("*"),
197
197
  Resource: "*"
198
198
  },
199
199
  {
200
200
  Effect: "Allow",
201
- Action: [iam.sso("*"), iam.identitystore("*")],
201
+ Action: [sso("*"), identitystore("*")],
202
202
  Resource: "*"
203
203
  },
204
204
  {
205
205
  Effect: "Allow",
206
- Action: [iam.s3("GetObject"), iam.s3("PutObject"), iam.s3("ListBucket")],
206
+ Action: [s3("GetObject"), s3("PutObject"), s3("ListBucket")],
207
207
  Resource: [
208
208
  `arn:aws:s3:::${props.bucketName}`,
209
209
  `arn:aws:s3:::${props.bucketName}/*`
@@ -212,15 +212,15 @@ async function ensureIamRole(props) {
212
212
  {
213
213
  Effect: "Allow",
214
214
  Action: [
215
- iam.logs("CreateLogGroup"),
216
- iam.logs("CreateLogStream"),
217
- iam.logs("PutLogEvents")
215
+ logs("CreateLogGroup"),
216
+ logs("CreateLogStream"),
217
+ logs("PutLogEvents")
218
218
  ],
219
219
  Resource: "arn:aws:logs:*:*:*"
220
220
  },
221
221
  {
222
222
  Effect: "Allow",
223
- Action: [iam.account("PutAccountName")],
223
+ Action: [account("PutAccountName")],
224
224
  Resource: "*"
225
225
  }
226
226
  ]
@@ -761,6 +761,10 @@ function formatOperationLine(operation) {
761
761
  if (operation.kind === "revokeIdcAccountAssignment") {
762
762
  return ` revoke IdC assignment "${operation.permissionSetName}" from ${formatPrincipalLabel(operation.principalType, operation.principalName)} on "${operation.accountName}"`;
763
763
  }
764
+ if (operation.kind === "updateIdcPermissionSetSessionDuration") {
765
+ const duration = operation.sessionDuration ?? "default";
766
+ return ` update IdC permission set session duration "${operation.permissionSetName}" -> ${duration}`;
767
+ }
764
768
  assertUnreachable(operation, "Unsupported operation kind in formatOperationLine.");
765
769
  }
766
770
  function formatPrincipalLabel(principalType, principalName) {
@@ -817,7 +821,7 @@ async function ensureOrganizationManagementPermissionSet(props) {
817
821
  Version: "2012-10-17",
818
822
  Statement: [{
819
823
  Effect: "Allow",
820
- Action: [iam.organizations("*"), iam.sso("*"), iam.identitystore("*"), iam.account("*"), iam.iam("*")],
824
+ Action: [organizations("*"), sso("*"), identitystore("*"), account("*"), iam("*")],
821
825
  Resource: "*"
822
826
  }]
823
827
  });
@@ -867,7 +871,7 @@ async function ensureOrganizationRemoteManagementPermissionSet(props) {
867
871
  Version: "2012-10-17",
868
872
  Statement: [{
869
873
  Effect: "Allow",
870
- Action: [iam.lambda("InvokeFunction")],
874
+ Action: [lambda("InvokeFunction")],
871
875
  Resource: props.lambdaArn
872
876
  }]
873
877
  });
@@ -0,0 +1,80 @@
1
+ import { loadAwsConfigModelFromTsFile } from "../awsConfig.js";
2
+ const INLINE_POLICY_MAX_CHARS = 10240;
3
+ async function runValidateCommand(input) {
4
+ const configPath = input.configPath ?? "aws.config.ts";
5
+ const typesPath = input.typesPath ?? "aws.config.types.ts";
6
+ let config;
7
+ try {
8
+ config = await loadAwsConfigModelFromTsFile({ configPath, typesPath });
9
+ } catch (error) {
10
+ input.logger.log(`Config error: ${error instanceof Error ? error.message : String(error)}`);
11
+ return false;
12
+ }
13
+ const errors = [];
14
+ checkCircularOuReferences(config, errors);
15
+ checkAssignmentPrincipals(config, errors);
16
+ checkInlinePolicySizes(config, errors);
17
+ if (errors.length > 0) {
18
+ for (const error of errors) {
19
+ input.logger.log(`Error: ${error}`);
20
+ }
21
+ input.logger.log(`
22
+ Validation failed with ${errors.length} error(s).`);
23
+ return false;
24
+ }
25
+ input.logger.log("Config is valid.");
26
+ return true;
27
+ }
28
+ function checkCircularOuReferences(config, errors) {
29
+ const parentByName = new Map(
30
+ config.organizationalUnits.map((ou) => [ou.name, ou.parentName])
31
+ );
32
+ const confirmed = /* @__PURE__ */ new Set();
33
+ for (const ou of config.organizationalUnits) {
34
+ if (ou.name === "root" || confirmed.has(ou.name)) {
35
+ continue;
36
+ }
37
+ const visited = /* @__PURE__ */ new Set();
38
+ let current = ou.name;
39
+ while (current != null) {
40
+ if (visited.has(current)) {
41
+ errors.push(`Circular OU reference detected: "${current}" is its own ancestor.`);
42
+ confirmed.add(current);
43
+ break;
44
+ }
45
+ visited.add(current);
46
+ current = parentByName.get(current) ?? null;
47
+ }
48
+ }
49
+ }
50
+ function checkAssignmentPrincipals(config, errors) {
51
+ for (const assignment of config.assignments) {
52
+ const hasGroup = assignment.group != null;
53
+ const hasUser = assignment.user != null;
54
+ if (hasGroup && hasUser) {
55
+ errors.push(
56
+ `Assignment for permission set "${assignment.permissionSet}" specifies both "group" and "user" \u2014 only one is allowed.`
57
+ );
58
+ } else if (!hasGroup && !hasUser) {
59
+ errors.push(
60
+ `Assignment for permission set "${assignment.permissionSet}" has no principal \u2014 "group" or "user" is required.`
61
+ );
62
+ }
63
+ }
64
+ }
65
+ function checkInlinePolicySizes(config, errors) {
66
+ for (const ps of config.permissionSets) {
67
+ if (ps.inlinePolicy == null) {
68
+ continue;
69
+ }
70
+ const length = JSON.stringify(ps.inlinePolicy).length;
71
+ if (length > INLINE_POLICY_MAX_CHARS) {
72
+ errors.push(
73
+ `Permission set "${ps.name}" inline policy is ${length} characters (limit: ${INLINE_POLICY_MAX_CHARS}).`
74
+ );
75
+ }
76
+ }
77
+ }
78
+ export {
79
+ runValidateCommand
80
+ };
package/dist/diff.js CHANGED
@@ -18,20 +18,21 @@ const operationExecutionPriority = {
18
18
  addIdcGroupMembership: 12,
19
19
  createIdcPermissionSet: 13,
20
20
  updateIdcPermissionSetDescription: 14,
21
- putIdcPermissionSetInlinePolicy: 15,
22
- deleteIdcPermissionSetInlinePolicy: 16,
23
- attachIdcManagedPolicyToPermissionSet: 17,
24
- detachIdcManagedPolicyFromPermissionSet: 18,
25
- attachIdcCustomerManagedPolicyReferenceToPermissionSet: 19,
26
- detachIdcCustomerManagedPolicyReferenceFromPermissionSet: 20,
27
- provisionIdcPermissionSet: 21,
28
- grantIdcAccountAssignment: 22,
29
- removeIdcGroupMembership: 23,
30
- revokeIdcAccountAssignment: 24,
31
- deleteIdcUser: 25,
32
- deleteIdcGroup: 26,
33
- deleteIdcPermissionSet: 27,
34
- deleteOu: 28
21
+ updateIdcPermissionSetSessionDuration: 15,
22
+ putIdcPermissionSetInlinePolicy: 16,
23
+ deleteIdcPermissionSetInlinePolicy: 17,
24
+ attachIdcManagedPolicyToPermissionSet: 18,
25
+ detachIdcManagedPolicyFromPermissionSet: 19,
26
+ attachIdcCustomerManagedPolicyReferenceToPermissionSet: 20,
27
+ detachIdcCustomerManagedPolicyReferenceFromPermissionSet: 21,
28
+ provisionIdcPermissionSet: 22,
29
+ grantIdcAccountAssignment: 23,
30
+ removeIdcGroupMembership: 24,
31
+ revokeIdcAccountAssignment: 25,
32
+ deleteIdcUser: 26,
33
+ deleteIdcGroup: 27,
34
+ deleteIdcPermissionSet: 28,
35
+ deleteOu: 29
35
36
  };
36
37
  function diffStates(props) {
37
38
  const operations = [];
@@ -440,7 +441,8 @@ function diffStates(props) {
440
441
  operations.push({
441
442
  kind: "createIdcPermissionSet",
442
443
  permissionSetName: nextPermissionSet.name,
443
- description: nextPermissionSet.description
444
+ description: nextPermissionSet.description,
445
+ sessionDuration: nextPermissionSet.sessionDuration
444
446
  });
445
447
  }
446
448
  const permissionSetMutationStartIndex = operations.length;
@@ -452,6 +454,13 @@ function diffStates(props) {
452
454
  description: nextPermissionSet.description
453
455
  });
454
456
  }
457
+ if (currentPermissionSet.sessionDuration !== nextPermissionSet.sessionDuration) {
458
+ operations.push({
459
+ kind: "updateIdcPermissionSetSessionDuration",
460
+ permissionSetName: nextPermissionSet.name,
461
+ sessionDuration: nextPermissionSet.sessionDuration
462
+ });
463
+ }
455
464
  }
456
465
  const currentInlinePolicy = normalizeInlinePolicyString(
457
466
  currentPermissionSet?.inlinePolicy ?? null
@@ -100,13 +100,19 @@ const removeIdcGroupMembershipOperationSchema = v.strictObject({
100
100
  const createIdcPermissionSetOperationSchema = v.strictObject({
101
101
  kind: v.literal("createIdcPermissionSet"),
102
102
  permissionSetName: v.string(),
103
- description: v.string()
103
+ description: v.string(),
104
+ sessionDuration: v.nullable(v.string())
104
105
  });
105
106
  const updateIdcPermissionSetDescriptionOperationSchema = v.strictObject({
106
107
  kind: v.literal("updateIdcPermissionSetDescription"),
107
108
  permissionSetName: v.string(),
108
109
  description: v.string()
109
110
  });
111
+ const updateIdcPermissionSetSessionDurationOperationSchema = v.strictObject({
112
+ kind: v.literal("updateIdcPermissionSetSessionDuration"),
113
+ permissionSetName: v.string(),
114
+ sessionDuration: v.nullable(v.string())
115
+ });
110
116
  const deleteIdcPermissionSetOperationSchema = v.strictObject({
111
117
  kind: v.literal("deleteIdcPermissionSet"),
112
118
  permissionSetName: v.string()
@@ -180,6 +186,7 @@ const operationSchema = v.variant("kind", [
180
186
  removeIdcGroupMembershipOperationSchema,
181
187
  createIdcPermissionSetOperationSchema,
182
188
  updateIdcPermissionSetDescriptionOperationSchema,
189
+ updateIdcPermissionSetSessionDurationOperationSchema,
183
190
  deleteIdcPermissionSetOperationSchema,
184
191
  putIdcPermissionSetInlinePolicyOperationSchema,
185
192
  deleteIdcPermissionSetInlinePolicyOperationSchema,
package/dist/scanLogic.js CHANGED
@@ -333,6 +333,7 @@ async function listPermissionSets(props) {
333
333
  permissionSetArn: permissionSet.PermissionSetArn,
334
334
  name: permissionSet.Name,
335
335
  description: permissionSet.Description ?? "",
336
+ sessionDuration: permissionSet.SessionDuration ?? null,
336
337
  inlinePolicy,
337
338
  awsManagedPolicies,
338
339
  customerManagedPolicies
package/dist/state.js CHANGED
@@ -46,6 +46,7 @@ const permissionSetSchema = v.strictObject({
46
46
  permissionSetArn: nonEmptyString,
47
47
  name: nonEmptyString,
48
48
  description: v.string(),
49
+ sessionDuration: v.nullable(v.string()),
49
50
  inlinePolicy: v.nullable(nonEmptyString),
50
51
  awsManagedPolicies: v.array(nonEmptyString),
51
52
  customerManagedPolicies: v.array(customerManagedPolicyReferenceSchema)
@@ -345,7 +346,7 @@ function removeIdcGroupFromWorkingState(props) {
345
346
  }
346
347
  function upsertIdcPermissionSetInWorkingState(props) {
347
348
  const currentPermissionSet = props.workingState.identityCenter.permissionSetsByName[props.permissionSet.name];
348
- if (currentPermissionSet != null && currentPermissionSet.permissionSetArn === props.permissionSet.permissionSetArn && currentPermissionSet.name === props.permissionSet.name && currentPermissionSet.description === props.permissionSet.description && currentPermissionSet.inlinePolicy === props.permissionSet.inlinePolicy && JSON.stringify(currentPermissionSet.awsManagedPolicies) === JSON.stringify(props.permissionSet.awsManagedPolicies) && JSON.stringify(currentPermissionSet.customerManagedPolicies) === JSON.stringify(props.permissionSet.customerManagedPolicies)) {
349
+ if (currentPermissionSet != null && currentPermissionSet.permissionSetArn === props.permissionSet.permissionSetArn && currentPermissionSet.name === props.permissionSet.name && currentPermissionSet.description === props.permissionSet.description && currentPermissionSet.sessionDuration === props.permissionSet.sessionDuration && currentPermissionSet.inlinePolicy === props.permissionSet.inlinePolicy && JSON.stringify(currentPermissionSet.awsManagedPolicies) === JSON.stringify(props.permissionSet.awsManagedPolicies) && JSON.stringify(currentPermissionSet.customerManagedPolicies) === JSON.stringify(props.permissionSet.customerManagedPolicies)) {
349
350
  return props.workingState;
350
351
  }
351
352
  const remainingPermissionSets = props.workingState.identityCenter.permissionSets.filter(
@@ -739,13 +739,19 @@ var removeIdcGroupMembershipOperationSchema = strictObject({
739
739
  var createIdcPermissionSetOperationSchema = strictObject({
740
740
  kind: literal("createIdcPermissionSet"),
741
741
  permissionSetName: string(),
742
- description: string()
742
+ description: string(),
743
+ sessionDuration: nullable(string())
743
744
  });
744
745
  var updateIdcPermissionSetDescriptionOperationSchema = strictObject({
745
746
  kind: literal("updateIdcPermissionSetDescription"),
746
747
  permissionSetName: string(),
747
748
  description: string()
748
749
  });
750
+ var updateIdcPermissionSetSessionDurationOperationSchema = strictObject({
751
+ kind: literal("updateIdcPermissionSetSessionDuration"),
752
+ permissionSetName: string(),
753
+ sessionDuration: nullable(string())
754
+ });
749
755
  var deleteIdcPermissionSetOperationSchema = strictObject({
750
756
  kind: literal("deleteIdcPermissionSet"),
751
757
  permissionSetName: string()
@@ -819,6 +825,7 @@ var operationSchema = variant("kind", [
819
825
  removeIdcGroupMembershipOperationSchema,
820
826
  createIdcPermissionSetOperationSchema,
821
827
  updateIdcPermissionSetDescriptionOperationSchema,
828
+ updateIdcPermissionSetSessionDurationOperationSchema,
822
829
  deleteIdcPermissionSetOperationSchema,
823
830
  putIdcPermissionSetInlinePolicyOperationSchema,
824
831
  deleteIdcPermissionSetInlinePolicyOperationSchema,
@@ -915,6 +922,7 @@ var permissionSetSchema = strictObject({
915
922
  permissionSetArn: nonEmptyString,
916
923
  name: nonEmptyString,
917
924
  description: string(),
925
+ sessionDuration: nullable(string()),
918
926
  inlinePolicy: nullable(nonEmptyString),
919
927
  awsManagedPolicies: array(nonEmptyString),
920
928
  customerManagedPolicies: array(customerManagedPolicyReferenceSchema)
@@ -1211,7 +1219,7 @@ function removeIdcGroupFromWorkingState(props) {
1211
1219
  }
1212
1220
  function upsertIdcPermissionSetInWorkingState(props) {
1213
1221
  const currentPermissionSet = props.workingState.identityCenter.permissionSetsByName[props.permissionSet.name];
1214
- if (currentPermissionSet != null && currentPermissionSet.permissionSetArn === props.permissionSet.permissionSetArn && currentPermissionSet.name === props.permissionSet.name && currentPermissionSet.description === props.permissionSet.description && currentPermissionSet.inlinePolicy === props.permissionSet.inlinePolicy && JSON.stringify(currentPermissionSet.awsManagedPolicies) === JSON.stringify(props.permissionSet.awsManagedPolicies) && JSON.stringify(currentPermissionSet.customerManagedPolicies) === JSON.stringify(props.permissionSet.customerManagedPolicies)) {
1222
+ if (currentPermissionSet != null && currentPermissionSet.permissionSetArn === props.permissionSet.permissionSetArn && currentPermissionSet.name === props.permissionSet.name && currentPermissionSet.description === props.permissionSet.description && currentPermissionSet.sessionDuration === props.permissionSet.sessionDuration && currentPermissionSet.inlinePolicy === props.permissionSet.inlinePolicy && JSON.stringify(currentPermissionSet.awsManagedPolicies) === JSON.stringify(props.permissionSet.awsManagedPolicies) && JSON.stringify(currentPermissionSet.customerManagedPolicies) === JSON.stringify(props.permissionSet.customerManagedPolicies)) {
1215
1223
  return props.workingState;
1216
1224
  }
1217
1225
  const remainingPermissionSets = props.workingState.identityCenter.permissionSets.filter(
@@ -1766,6 +1774,7 @@ async function listPermissionSets(props) {
1766
1774
  permissionSetArn: permissionSet.PermissionSetArn,
1767
1775
  name: permissionSet.Name,
1768
1776
  description: permissionSet.Description ?? "",
1777
+ sessionDuration: permissionSet.SessionDuration ?? null,
1769
1778
  inlinePolicy,
1770
1779
  awsManagedPolicies,
1771
1780
  customerManagedPolicies
@@ -1885,9 +1894,7 @@ async function listAccountsForPermissionSet(props) {
1885
1894
  }
1886
1895
 
1887
1896
  // src/applyLogic.ts
1888
- import {
1889
- PutAccountNameCommand
1890
- } from "@aws-sdk/client-account";
1897
+ import { PutAccountNameCommand } from "@aws-sdk/client-account";
1891
1898
  import {
1892
1899
  CreateOrganizationalUnitCommand,
1893
1900
  DeleteOrganizationalUnitCommand,
@@ -2206,7 +2213,10 @@ async function executeOperation(props) {
2206
2213
  workingState: props.state,
2207
2214
  account: {
2208
2215
  ...account,
2209
- tags: Object.entries(operation.tags).map(([key, value]) => ({ key, value }))
2216
+ tags: Object.entries(operation.tags).map(([key, value]) => ({
2217
+ key,
2218
+ value
2219
+ }))
2210
2220
  }
2211
2221
  });
2212
2222
  }
@@ -2248,7 +2258,9 @@ async function executeOperation(props) {
2248
2258
  DestinationParentId: operation.toOuId
2249
2259
  })
2250
2260
  );
2251
- props.logger.log(`Done: "${operation.accountName}" -> ${operation.toOuName}`);
2261
+ props.logger.log(
2262
+ `Done: "${operation.accountName}" -> ${operation.toOuName}`
2263
+ );
2252
2264
  return moveAccountInWorkingState({
2253
2265
  workingState: props.state,
2254
2266
  accountId: operation.accountId,
@@ -2475,7 +2487,8 @@ async function executeOperation(props) {
2475
2487
  new CreatePermissionSetCommand({
2476
2488
  InstanceArn: props.state.identityCenter.instanceArn,
2477
2489
  Name: operation.permissionSetName,
2478
- Description: operation.description.length > 0 ? operation.description : void 0
2490
+ Description: operation.description.length > 0 ? operation.description : void 0,
2491
+ SessionDuration: operation.sessionDuration ?? void 0
2479
2492
  })
2480
2493
  );
2481
2494
  const permissionSetArn = response.PermissionSet?.PermissionSetArn;
@@ -2491,6 +2504,7 @@ async function executeOperation(props) {
2491
2504
  permissionSetArn,
2492
2505
  name: operation.permissionSetName,
2493
2506
  description: operation.description,
2507
+ sessionDuration: operation.sessionDuration,
2494
2508
  inlinePolicy: null,
2495
2509
  awsManagedPolicies: [],
2496
2510
  customerManagedPolicies: []
@@ -2521,6 +2535,30 @@ async function executeOperation(props) {
2521
2535
  }
2522
2536
  });
2523
2537
  }
2538
+ if (operation.kind === "updateIdcPermissionSetSessionDuration") {
2539
+ const permissionSet = resolvePermissionSetByName({
2540
+ state: props.state,
2541
+ permissionSetName: operation.permissionSetName
2542
+ });
2543
+ props.logger.log(
2544
+ `Updating IdC permission set session duration for "${operation.permissionSetName}"...`
2545
+ );
2546
+ await props.ssoAdminClient.send(
2547
+ new UpdatePermissionSetCommand({
2548
+ InstanceArn: props.state.identityCenter.instanceArn,
2549
+ PermissionSetArn: permissionSet.permissionSetArn,
2550
+ SessionDuration: operation.sessionDuration ?? void 0
2551
+ })
2552
+ );
2553
+ props.logger.log(`Done: "${operation.permissionSetName}"`);
2554
+ return upsertIdcPermissionSetInWorkingState({
2555
+ workingState: props.state,
2556
+ permissionSet: {
2557
+ ...permissionSet,
2558
+ sessionDuration: operation.sessionDuration
2559
+ }
2560
+ });
2561
+ }
2524
2562
  if (operation.kind === "deleteIdcPermissionSet") {
2525
2563
  const permissionSet = resolvePermissionSetByName({
2526
2564
  state: props.state,
@@ -2961,9 +2999,9 @@ function upsertPermissionSetPolicyState(props) {
2961
2999
  workingState: props.state,
2962
3000
  permissionSet: {
2963
3001
  ...nextPermissionSet,
2964
- awsManagedPolicies: [...new Set(nextPermissionSet.awsManagedPolicies)].sort(
2965
- (left, right) => left.localeCompare(right)
2966
- ),
3002
+ awsManagedPolicies: [
3003
+ ...new Set(nextPermissionSet.awsManagedPolicies)
3004
+ ].sort((left, right) => left.localeCompare(right)),
2967
3005
  customerManagedPolicies: [
2968
3006
  ...nextPermissionSet.customerManagedPolicies
2969
3007
  ].sort((left, right) => {
@@ -2994,11 +3032,13 @@ async function assertOrganizationalUnitIsEmpty(props) {
2994
3032
  });
2995
3033
  if (childOrganizationalUnit != null) {
2996
3034
  throw new Error(
2997
- `Refusing to delete OU "${props.organizationalUnitName}": live AWS preflight failed [child-ou-present]: ${formatLivePreflightResource({
2998
- resourceType: "child OU",
2999
- name: childOrganizationalUnit.Name,
3000
- id: childOrganizationalUnit.Id
3001
- })} is still attached.`
3035
+ `Refusing to delete OU "${props.organizationalUnitName}": live AWS preflight failed [child-ou-present]: ${formatLivePreflightResource(
3036
+ {
3037
+ resourceType: "child OU",
3038
+ name: childOrganizationalUnit.Name,
3039
+ id: childOrganizationalUnit.Id
3040
+ }
3041
+ )} is still attached.`
3002
3042
  );
3003
3043
  }
3004
3044
  const account = await listFirstAccountForParent({
@@ -3007,11 +3047,13 @@ async function assertOrganizationalUnitIsEmpty(props) {
3007
3047
  });
3008
3048
  if (account != null) {
3009
3049
  throw new Error(
3010
- `Refusing to delete OU "${props.organizationalUnitName}": live AWS preflight failed [account-present]: ${formatLivePreflightResource({
3011
- resourceType: "account",
3012
- name: account.Name,
3013
- id: account.Id
3014
- })} is still attached.`
3050
+ `Refusing to delete OU "${props.organizationalUnitName}": live AWS preflight failed [account-present]: ${formatLivePreflightResource(
3051
+ {
3052
+ resourceType: "account",
3053
+ name: account.Name,
3054
+ id: account.Id
3055
+ }
3056
+ )} is still attached.`
3015
3057
  );
3016
3058
  }
3017
3059
  }
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beesolve/aws-accounts",
3
- "version": "1.0.7",
3
+ "version": "1.1.0",
4
4
  "description": "AWS Organizations and IAM Identity Center management CLI",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {