@beesolve/aws-accounts 1.0.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.
@@ -0,0 +1,1365 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { readFile, unlink, writeFile } from "node:fs/promises";
3
+ import { join, resolve } from "node:path";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
+ import { build as esbuildBuild } from "esbuild";
6
+ import * as v from "valibot";
7
+ import {
8
+ assertIamPolicyDocument,
9
+ iamActionCatalog,
10
+ iamPolicyDocumentSchema
11
+ } from "@beesolve/iam-policy-ts";
12
+ import {
13
+ createAccessRoleName,
14
+ readStateFile,
15
+ validateState
16
+ } from "./state.js";
17
+ import { assertUnreachable, toRecordByProperty } from "./helpers.js";
18
+ const nonEmptyString = v.pipe(v.string(), v.nonEmpty());
19
+ const pendingCreationId = "__pending_creation__";
20
+ function resolveAccountStateMatchForConfigEntry(props) {
21
+ const matchedByName = props.accountByName[props.account.name];
22
+ if (matchedByName != null) {
23
+ return matchedByName;
24
+ }
25
+ const emailMatches = props.accounts.filter(
26
+ (candidate) => candidate.email === props.account.email
27
+ );
28
+ if (emailMatches.length > 1) {
29
+ throw new Error(
30
+ `Cannot map config account "${props.account.name}": multiple member accounts use email "${props.account.email}".`
31
+ );
32
+ }
33
+ return emailMatches[0];
34
+ }
35
+ const deploymentSchema = v.strictObject({
36
+ profile: v.string(),
37
+ region: v.string(),
38
+ lambdaArn: v.string(),
39
+ stateBucketName: v.string(),
40
+ stateCacheTtlSeconds: v.number()
41
+ });
42
+ const awsContextSchema = v.strictObject({
43
+ version: nonEmptyString,
44
+ generatedAt: nonEmptyString,
45
+ organization: v.strictObject({
46
+ managementAccountId: nonEmptyString,
47
+ rootId: nonEmptyString,
48
+ graveyardOuId: nonEmptyString
49
+ }),
50
+ identityCenter: v.strictObject({
51
+ instanceArn: nonEmptyString,
52
+ identityStoreId: nonEmptyString
53
+ }),
54
+ deployment: v.optional(deploymentSchema)
55
+ });
56
+ const awsConfigModelSchema = v.strictObject({
57
+ organizationalUnits: v.array(
58
+ v.strictObject({
59
+ name: v.string(),
60
+ parentName: v.nullable(v.string()),
61
+ accounts: v.array(
62
+ v.strictObject({
63
+ name: v.string(),
64
+ email: v.string(),
65
+ tags: v.array(
66
+ v.strictObject({
67
+ key: v.string(),
68
+ value: v.string()
69
+ })
70
+ )
71
+ })
72
+ )
73
+ })
74
+ ),
75
+ users: v.array(
76
+ v.strictObject({
77
+ userName: v.string(),
78
+ displayName: v.string(),
79
+ email: v.string()
80
+ })
81
+ ),
82
+ groups: v.array(
83
+ v.strictObject({
84
+ displayName: v.string(),
85
+ description: v.optional(v.string()),
86
+ members: v.array(v.string())
87
+ })
88
+ ),
89
+ permissionSets: v.array(
90
+ v.strictObject({
91
+ name: v.string(),
92
+ description: v.string(),
93
+ inlinePolicy: v.optional(iamPolicyDocumentSchema),
94
+ awsManagedPolicies: v.array(v.string()),
95
+ customerManagedPolicies: v.array(
96
+ v.strictObject({
97
+ name: v.string(),
98
+ path: v.string()
99
+ })
100
+ )
101
+ })
102
+ ),
103
+ assignments: v.array(
104
+ v.strictObject({
105
+ permissionSet: v.string(),
106
+ group: v.optional(v.string()),
107
+ user: v.optional(v.string()),
108
+ accounts: v.array(v.string())
109
+ })
110
+ )
111
+ });
112
+ const moduleDirectoryPath = resolve(
113
+ fileURLToPath(new URL(".", import.meta.url))
114
+ );
115
+ const projectRootPath = resolve(moduleDirectoryPath, "..");
116
+ async function writeAwsConfigFromState(props) {
117
+ const state = await readStateFile(props.statePath);
118
+ const context = await readAwsContextFile(props.contextPath);
119
+ assertStateMatchesContext({
120
+ state,
121
+ context
122
+ });
123
+ const mappedConfig = mapStateToAwsConfig({
124
+ state
125
+ });
126
+ const sortedConfig = sortAwsConfigModel({
127
+ config: mappedConfig
128
+ });
129
+ const nextConfigContent = renderAwsConfigTs({
130
+ config: sortedConfig
131
+ });
132
+ const nextTypesContent = renderAwsConfigTypesTs({
133
+ config: sortedConfig
134
+ });
135
+ const [currentConfigContent, currentTypesContent] = await Promise.all([
136
+ readIfExists(props.configPath),
137
+ readIfExists(props.typesPath)
138
+ ]);
139
+ const changedFiles = [];
140
+ if (currentConfigContent !== nextConfigContent) {
141
+ changedFiles.push({
142
+ path: props.configPath,
143
+ previousBytes: Buffer.byteLength(currentConfigContent ?? "", "utf8"),
144
+ nextBytes: Buffer.byteLength(nextConfigContent, "utf8"),
145
+ content: nextConfigContent
146
+ });
147
+ }
148
+ if (currentTypesContent !== nextTypesContent) {
149
+ changedFiles.push({
150
+ path: props.typesPath,
151
+ previousBytes: Buffer.byteLength(currentTypesContent ?? "", "utf8"),
152
+ nextBytes: Buffer.byteLength(nextTypesContent, "utf8"),
153
+ content: nextTypesContent
154
+ });
155
+ }
156
+ if (changedFiles.length === 0) {
157
+ props.logger.log("No changes.");
158
+ return {
159
+ configPath: props.configPath,
160
+ typesPath: props.typesPath,
161
+ files: [
162
+ {
163
+ path: props.configPath,
164
+ status: "unchanged"
165
+ },
166
+ {
167
+ path: props.typesPath,
168
+ status: "unchanged"
169
+ }
170
+ ]
171
+ };
172
+ }
173
+ const fileSummaries = changedFiles.map(
174
+ (file) => `${file.path}: ${file.previousBytes} -> ${file.nextBytes} bytes`
175
+ );
176
+ for (const fileSummary of fileSummaries) {
177
+ props.logger.log(fileSummary);
178
+ }
179
+ props.logger.log(
180
+ `Review with: git diff ${props.configPath} ${props.typesPath}`
181
+ );
182
+ const shouldWrite = await props.overwriteConfirmation({
183
+ fileSummaries
184
+ });
185
+ if (!shouldWrite) {
186
+ props.logger.log("Config write cancelled.");
187
+ return {
188
+ configPath: props.configPath,
189
+ typesPath: props.typesPath,
190
+ files: [
191
+ {
192
+ path: props.configPath,
193
+ status: currentConfigContent === nextConfigContent ? "unchanged" : "would-write"
194
+ },
195
+ {
196
+ path: props.typesPath,
197
+ status: currentTypesContent === nextTypesContent ? "unchanged" : "would-write"
198
+ }
199
+ ]
200
+ };
201
+ }
202
+ await Promise.all(
203
+ changedFiles.map((file) => writeFile(file.path, file.content, "utf8"))
204
+ );
205
+ return {
206
+ configPath: props.configPath,
207
+ typesPath: props.typesPath,
208
+ files: [
209
+ {
210
+ path: props.configPath,
211
+ status: currentConfigContent === nextConfigContent ? "unchanged" : "written"
212
+ },
213
+ {
214
+ path: props.typesPath,
215
+ status: currentTypesContent === nextTypesContent ? "unchanged" : "written"
216
+ }
217
+ ]
218
+ };
219
+ }
220
+ async function regenerateAwsConfigTypes(props) {
221
+ const typesModule = await loadAwsConfigTypesModule({
222
+ typesPath: props.typesPath
223
+ });
224
+ const loadedConfig = await loadAwsConfigFromTsFile({
225
+ configPath: props.configPath,
226
+ schema: typesModule.awsConfigSchema
227
+ });
228
+ const sortedConfig = sortAwsConfigModel({
229
+ config: loadedConfig
230
+ });
231
+ const nextTypesContent = renderAwsConfigTypesTs({
232
+ config: sortedConfig
233
+ });
234
+ const currentTypesContent = await readIfExists(props.typesPath);
235
+ if (currentTypesContent === nextTypesContent) {
236
+ props.logger.log("No changes.");
237
+ return {
238
+ typesPath: props.typesPath,
239
+ changed: false,
240
+ files: [
241
+ {
242
+ path: props.typesPath,
243
+ status: "unchanged"
244
+ }
245
+ ]
246
+ };
247
+ }
248
+ const fileSummary = `${props.typesPath}: ${Buffer.byteLength(currentTypesContent ?? "", "utf8")} -> ${Buffer.byteLength(nextTypesContent, "utf8")} bytes`;
249
+ props.logger.log(fileSummary);
250
+ props.logger.log(`Review with: git diff ${props.typesPath}`);
251
+ const shouldWrite = await props.overwriteConfirmation({
252
+ fileSummaries: [fileSummary]
253
+ });
254
+ if (!shouldWrite) {
255
+ props.logger.log("Types write cancelled.");
256
+ return {
257
+ typesPath: props.typesPath,
258
+ changed: false,
259
+ files: [
260
+ {
261
+ path: props.typesPath,
262
+ status: "would-write"
263
+ }
264
+ ]
265
+ };
266
+ }
267
+ await writeFile(props.typesPath, nextTypesContent, "utf8");
268
+ return {
269
+ typesPath: props.typesPath,
270
+ changed: true,
271
+ files: [
272
+ {
273
+ path: props.typesPath,
274
+ status: "written"
275
+ }
276
+ ]
277
+ };
278
+ }
279
+ function mapStateToAwsConfig(props) {
280
+ const organizationalUnits = [
281
+ {
282
+ name: "root",
283
+ parentName: null,
284
+ accounts: []
285
+ }
286
+ ];
287
+ const organizationalUnitById = toRecordByProperty(
288
+ props.state.organization.organizationalUnits,
289
+ "id"
290
+ );
291
+ for (const organizationalUnit of props.state.organization.organizationalUnits) {
292
+ if (organizationalUnit.name === "Graveyard") {
293
+ continue;
294
+ }
295
+ const parentName = organizationalUnit.parentId === props.state.organization.rootId ? "root" : organizationalUnitById[organizationalUnit.parentId]?.name;
296
+ if (parentName == null) {
297
+ throw new Error(
298
+ `Organizational unit "${organizationalUnit.name}" has unknown parentId "${organizationalUnit.parentId}".`
299
+ );
300
+ }
301
+ organizationalUnits.push({
302
+ name: organizationalUnit.name,
303
+ parentName,
304
+ accounts: []
305
+ });
306
+ }
307
+ const organizationalUnitByName = toRecordByProperty(
308
+ organizationalUnits,
309
+ "name"
310
+ );
311
+ const graveyardOrganizationalUnit = props.state.organization.organizationalUnits.find(
312
+ (organizationalUnit) => organizationalUnit.name === "Graveyard"
313
+ );
314
+ const graveyardOrganizationalUnitId = graveyardOrganizationalUnit?.id;
315
+ for (const account of props.state.organization.accounts) {
316
+ const ownerOuName = account.parentId === props.state.organization.rootId ? "root" : organizationalUnitById[account.parentId]?.name;
317
+ if (ownerOuName == null) {
318
+ throw new Error(
319
+ `Account "${account.name}" has unknown parentId "${account.parentId}".`
320
+ );
321
+ }
322
+ if (ownerOuName === "Graveyard") {
323
+ continue;
324
+ }
325
+ const ownerOu = organizationalUnitByName[ownerOuName];
326
+ if (ownerOu == null) {
327
+ throw new Error(
328
+ `Could not map account "${account.name}" to organizational unit "${ownerOuName}".`
329
+ );
330
+ }
331
+ ownerOu.accounts.push({
332
+ name: account.name,
333
+ email: account.email,
334
+ tags: account.tags ?? []
335
+ });
336
+ }
337
+ const permissionSetByArn = toRecordByProperty(
338
+ props.state.identityCenter.permissionSets,
339
+ "permissionSetArn"
340
+ );
341
+ const groupById = toRecordByProperty(
342
+ props.state.identityCenter.groups,
343
+ "groupId"
344
+ );
345
+ const userById = toRecordByProperty(
346
+ props.state.identityCenter.users,
347
+ "userId"
348
+ );
349
+ const accountById = toRecordByProperty(
350
+ props.state.organization.accounts,
351
+ "id"
352
+ );
353
+ const membersByGroupDisplayName = new Map(
354
+ props.state.identityCenter.groups.map((group) => [
355
+ group.displayName,
356
+ []
357
+ ])
358
+ );
359
+ const assignmentsByKey = /* @__PURE__ */ new Map();
360
+ for (const assignment of props.state.identityCenter.accountAssignments) {
361
+ const permissionSetName = permissionSetByArn[assignment.permissionSetArn]?.name;
362
+ if (permissionSetName == null) {
363
+ throw new Error(
364
+ `Could not resolve permission set name for assignment permissionSetArn "${assignment.permissionSetArn}".`
365
+ );
366
+ }
367
+ const accountName = accountById[assignment.accountId]?.name;
368
+ if (accountName == null) {
369
+ throw new Error(
370
+ `Could not resolve account name for assignment accountId "${assignment.accountId}".`
371
+ );
372
+ }
373
+ const accountParentId = accountById[assignment.accountId]?.parentId;
374
+ if (graveyardOrganizationalUnitId != null && accountParentId === graveyardOrganizationalUnitId) {
375
+ continue;
376
+ }
377
+ const principal = mapAssignmentPrincipal({
378
+ assignment,
379
+ groupById,
380
+ userById
381
+ });
382
+ const assignmentKey = `${principal.kind}:${principal.value}|${permissionSetName}`;
383
+ const existingAssignment = assignmentsByKey.get(assignmentKey);
384
+ if (existingAssignment == null) {
385
+ assignmentsByKey.set(assignmentKey, {
386
+ permissionSet: permissionSetName,
387
+ group: principal.kind === "group" ? principal.value : void 0,
388
+ user: principal.kind === "user" ? principal.value : void 0,
389
+ accounts: [accountName]
390
+ });
391
+ continue;
392
+ }
393
+ if (existingAssignment.accounts.includes(accountName) === false) {
394
+ existingAssignment.accounts.push(accountName);
395
+ }
396
+ }
397
+ for (const groupMembership of props.state.identityCenter.groupMemberships) {
398
+ const groupDisplayName = groupById[groupMembership.groupId]?.displayName;
399
+ if (groupDisplayName == null) {
400
+ throw new Error(
401
+ `Could not resolve group display name for membership groupId "${groupMembership.groupId}".`
402
+ );
403
+ }
404
+ const userName = userById[groupMembership.userId]?.userName;
405
+ if (userName == null) {
406
+ throw new Error(
407
+ `Could not resolve user name for membership userId "${groupMembership.userId}".`
408
+ );
409
+ }
410
+ const members = membersByGroupDisplayName.get(groupDisplayName);
411
+ if (members == null) {
412
+ throw new Error(
413
+ `Could not map membership for group "${groupDisplayName}".`
414
+ );
415
+ }
416
+ if (members.includes(userName) === false) {
417
+ members.push(userName);
418
+ }
419
+ }
420
+ const mapped = {
421
+ organizationalUnits,
422
+ users: props.state.identityCenter.users.map((user) => ({
423
+ userName: user.userName,
424
+ displayName: user.displayName,
425
+ email: user.email
426
+ })),
427
+ groups: props.state.identityCenter.groups.map((group) => ({
428
+ displayName: group.displayName,
429
+ description: group.description ?? "",
430
+ members: membersByGroupDisplayName.get(group.displayName) ?? []
431
+ })),
432
+ permissionSets: props.state.identityCenter.permissionSets.map(
433
+ (permissionSet) => ({
434
+ name: permissionSet.name,
435
+ description: permissionSet.description,
436
+ inlinePolicy: permissionSet.inlinePolicy == null ? void 0 : parseInlinePolicyForConfig({
437
+ permissionSetName: permissionSet.name,
438
+ inlinePolicy: permissionSet.inlinePolicy
439
+ }),
440
+ awsManagedPolicies: [...permissionSet.awsManagedPolicies],
441
+ customerManagedPolicies: permissionSet.customerManagedPolicies.map(
442
+ (customerManagedPolicy) => ({
443
+ name: customerManagedPolicy.name,
444
+ path: customerManagedPolicy.path
445
+ })
446
+ )
447
+ })
448
+ ),
449
+ assignments: [...assignmentsByKey.values()]
450
+ };
451
+ assertUniqueNames({
452
+ values: mapped.organizationalUnits.map((ou) => ou.name),
453
+ entityName: "organizational unit"
454
+ });
455
+ assertUniqueNames({
456
+ values: mapped.organizationalUnits.flatMap(
457
+ (ou) => ou.accounts.map((account) => account.name)
458
+ ),
459
+ entityName: "account"
460
+ });
461
+ assertUniqueNames({
462
+ values: mapped.groups.map((group) => group.displayName),
463
+ entityName: "group"
464
+ });
465
+ assertUniqueNames({
466
+ values: mapped.users.map((user) => user.userName),
467
+ entityName: "user"
468
+ });
469
+ assertUniqueNames({
470
+ values: mapped.permissionSets.map((permissionSet) => permissionSet.name),
471
+ entityName: "permission set"
472
+ });
473
+ return v.parse(awsConfigModelSchema, mapped);
474
+ }
475
+ function mapAwsConfigToState(props) {
476
+ const organizationalUnitByName = toRecordByProperty(
477
+ props.currentState.organization.organizationalUnits,
478
+ "name"
479
+ );
480
+ const accountByName = toRecordByProperty(
481
+ props.currentState.organization.accounts,
482
+ "name"
483
+ );
484
+ const userByUserName = toRecordByProperty(
485
+ props.currentState.identityCenter.users,
486
+ "userName"
487
+ );
488
+ const userById = toRecordByProperty(
489
+ props.currentState.identityCenter.users,
490
+ "userId"
491
+ );
492
+ const groupByDisplayName = toRecordByProperty(
493
+ props.currentState.identityCenter.groups,
494
+ "displayName"
495
+ );
496
+ const groupById = toRecordByProperty(
497
+ props.currentState.identityCenter.groups,
498
+ "groupId"
499
+ );
500
+ const groupMembershipByNameKey = toRecordByProperty(
501
+ props.currentState.identityCenter.groupMemberships,
502
+ (groupMembership) => {
503
+ const currentGroup = groupById[groupMembership.groupId];
504
+ if (currentGroup == null) {
505
+ throw new Error(
506
+ `Could not resolve current group for membership groupId "${groupMembership.groupId}".`
507
+ );
508
+ }
509
+ const currentUser = userById[groupMembership.userId];
510
+ if (currentUser == null) {
511
+ throw new Error(
512
+ `Could not resolve current user for membership userId "${groupMembership.userId}".`
513
+ );
514
+ }
515
+ return createGroupMembershipNameKey({
516
+ groupDisplayName: currentGroup.displayName,
517
+ userName: currentUser.userName
518
+ });
519
+ }
520
+ );
521
+ const permissionSetByName = toRecordByProperty(
522
+ props.currentState.identityCenter.permissionSets,
523
+ "name"
524
+ );
525
+ const configOrganizationalUnitNameSet = new Set(
526
+ props.config.organizationalUnits.map(
527
+ (organizationalUnit) => organizationalUnit.name
528
+ )
529
+ );
530
+ const mappedOrganizationalUnitIdByName = /* @__PURE__ */ new Map();
531
+ for (const organizationalUnit of props.config.organizationalUnits) {
532
+ if (organizationalUnit.name !== "root" && organizationalUnit.parentName != null && configOrganizationalUnitNameSet.has(organizationalUnit.parentName) === false) {
533
+ throw new Error(
534
+ `Organizational unit "${organizationalUnit.name}" references unknown parentName "${organizationalUnit.parentName}".`
535
+ );
536
+ }
537
+ const mappedId = resolveOrganizationalUnitId({
538
+ organizationalUnitName: organizationalUnit.name,
539
+ matchedOrganizationalUnit: organizationalUnitByName[organizationalUnit.name],
540
+ context: props.context
541
+ });
542
+ mappedOrganizationalUnitIdByName.set(organizationalUnit.name, mappedId);
543
+ }
544
+ const mappedOrganizationalUnits = [];
545
+ for (const organizationalUnit of props.config.organizationalUnits) {
546
+ if (organizationalUnit.name === "root") {
547
+ continue;
548
+ }
549
+ const mappedId = mappedOrganizationalUnitIdByName.get(
550
+ organizationalUnit.name
551
+ );
552
+ if (mappedId == null) {
553
+ throw new Error(
554
+ `Could not resolve mapped id for organizational unit "${organizationalUnit.name}".`
555
+ );
556
+ }
557
+ const parentId = organizationalUnit.parentName == null ? props.context.organization.rootId : mappedOrganizationalUnitIdByName.get(
558
+ organizationalUnit.parentName
559
+ ) ?? pendingCreationId;
560
+ const matchedOrganizationalUnit = organizationalUnitByName[organizationalUnit.name];
561
+ mappedOrganizationalUnits.push({
562
+ id: mappedId,
563
+ parentId,
564
+ arn: matchedOrganizationalUnit?.arn ?? pendingCreationId,
565
+ name: organizationalUnit.name
566
+ });
567
+ }
568
+ for (const managedOrganizationalUnitName of ["Graveyard"]) {
569
+ const managedOuId = resolveOrganizationalUnitId({
570
+ organizationalUnitName: managedOrganizationalUnitName,
571
+ matchedOrganizationalUnit: organizationalUnitByName[managedOrganizationalUnitName],
572
+ context: props.context
573
+ });
574
+ mappedOrganizationalUnitIdByName.set(
575
+ managedOrganizationalUnitName,
576
+ managedOuId
577
+ );
578
+ if (mappedOrganizationalUnits.some(
579
+ (organizationalUnit) => organizationalUnit.id === managedOuId
580
+ )) {
581
+ continue;
582
+ }
583
+ const matchedManagedOrganizationalUnit = organizationalUnitByName[managedOrganizationalUnitName];
584
+ mappedOrganizationalUnits.push({
585
+ id: managedOuId,
586
+ parentId: props.context.organization.rootId,
587
+ arn: matchedManagedOrganizationalUnit?.arn ?? pendingCreationId,
588
+ name: managedOrganizationalUnitName
589
+ });
590
+ }
591
+ const mappedAccountIdByName = /* @__PURE__ */ new Map();
592
+ const mappedAccounts = [];
593
+ for (const organizationalUnit of props.config.organizationalUnits) {
594
+ const ownerParentId = mappedOrganizationalUnitIdByName.get(
595
+ organizationalUnit.name
596
+ );
597
+ if (ownerParentId == null) {
598
+ throw new Error(
599
+ `Could not resolve mapped parent id for organizational unit "${organizationalUnit.name}".`
600
+ );
601
+ }
602
+ for (const account of organizationalUnit.accounts) {
603
+ const matchedAccount = resolveAccountStateMatchForConfigEntry({
604
+ account,
605
+ accountByName,
606
+ accounts: props.currentState.organization.accounts
607
+ });
608
+ const mappedId = matchedAccount?.id ?? pendingCreationId;
609
+ mappedAccounts.push({
610
+ id: mappedId,
611
+ arn: matchedAccount?.arn ?? pendingCreationId,
612
+ name: account.name,
613
+ email: account.email,
614
+ status: matchedAccount?.status ?? "ACTIVE",
615
+ parentId: ownerParentId,
616
+ tags: account.tags
617
+ });
618
+ mappedAccountIdByName.set(account.name, mappedId);
619
+ }
620
+ }
621
+ const mappedUsers = props.config.users.map((user) => {
622
+ const matchedUser = userByUserName[user.userName];
623
+ return {
624
+ userId: matchedUser?.userId ?? pendingCreationId,
625
+ userName: user.userName,
626
+ displayName: user.displayName,
627
+ email: user.email
628
+ };
629
+ });
630
+ const mappedUserByUserName = toRecordByProperty(
631
+ mappedUsers,
632
+ "userName"
633
+ );
634
+ const mappedGroups = props.config.groups.map((group) => {
635
+ const matchedGroup = groupByDisplayName[group.displayName];
636
+ return {
637
+ groupId: matchedGroup?.groupId ?? pendingCreationId,
638
+ displayName: group.displayName,
639
+ description: group.description ?? ""
640
+ };
641
+ });
642
+ const mappedGroupByDisplayName = toRecordByProperty(
643
+ mappedGroups,
644
+ "displayName"
645
+ );
646
+ const mappedGroupMemberships = [];
647
+ for (const group of props.config.groups) {
648
+ assertUniqueNames({
649
+ values: group.members,
650
+ entityName: `group member for "${group.displayName}"`
651
+ });
652
+ const groupId = mappedGroupByDisplayName[group.displayName]?.groupId ?? pendingCreationId;
653
+ for (const userName of group.members) {
654
+ const currentMembership = groupMembershipByNameKey[createGroupMembershipNameKey({
655
+ groupDisplayName: group.displayName,
656
+ userName
657
+ })];
658
+ mappedGroupMemberships.push({
659
+ membershipId: currentMembership?.membershipId ?? pendingCreationId,
660
+ groupId,
661
+ userId: mappedUserByUserName[userName]?.userId ?? pendingCreationId
662
+ });
663
+ }
664
+ }
665
+ const mappedPermissionSets = props.config.permissionSets.map((permissionSet) => {
666
+ const matchedPermissionSet = permissionSetByName[permissionSet.name];
667
+ return {
668
+ permissionSetArn: matchedPermissionSet?.permissionSetArn ?? pendingCreationId,
669
+ name: permissionSet.name,
670
+ description: permissionSet.description,
671
+ inlinePolicy: stableStringifyInlinePolicy(permissionSet.inlinePolicy),
672
+ awsManagedPolicies: [...permissionSet.awsManagedPolicies],
673
+ customerManagedPolicies: permissionSet.customerManagedPolicies.map(
674
+ (customerManagedPolicy) => ({
675
+ name: customerManagedPolicy.name,
676
+ path: customerManagedPolicy.path
677
+ })
678
+ )
679
+ };
680
+ });
681
+ const mappedPermissionSetByName = toRecordByProperty(
682
+ mappedPermissionSets,
683
+ "name"
684
+ );
685
+ const mappedAccountAssignments = [];
686
+ for (const assignment of props.config.assignments) {
687
+ const hasGroupPrincipal = assignment.group != null;
688
+ const hasUserPrincipal = assignment.user != null;
689
+ if (hasGroupPrincipal === hasUserPrincipal) {
690
+ throw new Error(
691
+ `Assignment for permission set "${assignment.permissionSet}" must include exactly one principal (group or user).`
692
+ );
693
+ }
694
+ const mappedPrincipal = hasGroupPrincipal === true ? {
695
+ principalId: mappedGroupByDisplayName[assignment.group ?? ""]?.groupId ?? pendingCreationId,
696
+ principalType: "GROUP"
697
+ } : {
698
+ principalId: mappedUserByUserName[assignment.user ?? ""]?.userId ?? pendingCreationId,
699
+ principalType: "USER"
700
+ };
701
+ const permissionSetArn = mappedPermissionSetByName[assignment.permissionSet]?.permissionSetArn ?? pendingCreationId;
702
+ for (const accountName of assignment.accounts) {
703
+ mappedAccountAssignments.push({
704
+ accountId: mappedAccountIdByName.get(accountName) ?? pendingCreationId,
705
+ permissionSetArn,
706
+ principalId: mappedPrincipal.principalId,
707
+ principalType: mappedPrincipal.principalType
708
+ });
709
+ }
710
+ }
711
+ const mapped = {
712
+ version: props.currentState.version,
713
+ generatedAt: props.currentState.generatedAt,
714
+ organization: {
715
+ rootId: props.context.organization.rootId,
716
+ organizationalUnits: mappedOrganizationalUnits,
717
+ accounts: mappedAccounts
718
+ },
719
+ identityCenter: {
720
+ instanceArn: props.context.identityCenter.instanceArn,
721
+ identityStoreId: props.context.identityCenter.identityStoreId,
722
+ users: mappedUsers,
723
+ groups: mappedGroups,
724
+ groupMemberships: mappedGroupMemberships,
725
+ permissionSets: mappedPermissionSets,
726
+ accountAssignments: mappedAccountAssignments,
727
+ accessRoles: mappedAccountAssignments.map((assignment) => ({
728
+ accountId: assignment.accountId,
729
+ permissionSetArn: assignment.permissionSetArn,
730
+ principalId: assignment.principalId,
731
+ principalType: assignment.principalType,
732
+ roleName: createAccessRoleName(assignment)
733
+ }))
734
+ }
735
+ };
736
+ assertUniqueNames({
737
+ values: props.config.organizationalUnits.map(
738
+ (organizationalUnit) => organizationalUnit.name
739
+ ),
740
+ entityName: "organizational unit"
741
+ });
742
+ assertUniqueNames({
743
+ values: props.config.organizationalUnits.flatMap(
744
+ (organizationalUnit) => organizationalUnit.accounts.map((account) => account.name)
745
+ ),
746
+ entityName: "account"
747
+ });
748
+ assertUniqueNames({
749
+ values: props.config.groups.map((group) => group.displayName),
750
+ entityName: "group"
751
+ });
752
+ assertUniqueNames({
753
+ values: props.config.users.map((user) => user.userName),
754
+ entityName: "user"
755
+ });
756
+ assertUniqueNames({
757
+ values: props.config.permissionSets.map(
758
+ (permissionSet) => permissionSet.name
759
+ ),
760
+ entityName: "permission set"
761
+ });
762
+ return validateState(mapped);
763
+ }
764
+ function sortAwsConfigModel(props) {
765
+ const childrenByParentName = /* @__PURE__ */ new Map();
766
+ for (const organizationalUnit of props.config.organizationalUnits) {
767
+ const existingChildren = childrenByParentName.get(organizationalUnit.parentName) ?? [];
768
+ existingChildren.push(organizationalUnit);
769
+ childrenByParentName.set(organizationalUnit.parentName, existingChildren);
770
+ }
771
+ const orderedOrganizationalUnits = [];
772
+ const root = props.config.organizationalUnits.find(
773
+ (ou) => ou.name === "root"
774
+ );
775
+ if (root == null || root.parentName !== null) {
776
+ throw new Error(
777
+ "Config model must include a synthetic root organizational unit with parentName set to null."
778
+ );
779
+ }
780
+ orderedOrganizationalUnits.push({
781
+ ...root,
782
+ accounts: [...root.accounts].sort(
783
+ (left, right) => left.name.localeCompare(right.name)
784
+ )
785
+ });
786
+ const queue = [root.name];
787
+ while (queue.length > 0) {
788
+ const currentParentName = queue.shift();
789
+ if (currentParentName == null) {
790
+ continue;
791
+ }
792
+ const children = (childrenByParentName.get(currentParentName) ?? []).filter((ou) => ou.name !== "root").sort((left, right) => left.name.localeCompare(right.name));
793
+ for (const child of children) {
794
+ orderedOrganizationalUnits.push({
795
+ ...child,
796
+ accounts: [...child.accounts].sort(
797
+ (left, right) => left.name.localeCompare(right.name)
798
+ )
799
+ });
800
+ queue.push(child.name);
801
+ }
802
+ }
803
+ return {
804
+ organizationalUnits: orderedOrganizationalUnits,
805
+ users: [...props.config.users].sort(
806
+ (left, right) => left.userName.localeCompare(right.userName)
807
+ ),
808
+ groups: [...props.config.groups].map((group) => ({
809
+ ...group,
810
+ members: [...group.members].sort(
811
+ (left, right) => left.localeCompare(right)
812
+ )
813
+ })).sort((left, right) => left.displayName.localeCompare(right.displayName)),
814
+ permissionSets: [...props.config.permissionSets].map((permissionSet) => ({
815
+ ...permissionSet,
816
+ inlinePolicy: permissionSet.inlinePolicy == null ? void 0 : sortJsonRecord(permissionSet.inlinePolicy),
817
+ awsManagedPolicies: [...permissionSet.awsManagedPolicies].sort(
818
+ (left, right) => left.localeCompare(right)
819
+ ),
820
+ customerManagedPolicies: [...permissionSet.customerManagedPolicies].sort(
821
+ (left, right) => compareStringKeys(left.path, right.path, left.name, right.name)
822
+ )
823
+ })).sort((left, right) => left.name.localeCompare(right.name)),
824
+ assignments: [...props.config.assignments].map((assignment) => ({
825
+ ...assignment,
826
+ accounts: [...assignment.accounts].sort(
827
+ (left, right) => left.localeCompare(right)
828
+ )
829
+ })).sort((left, right) => {
830
+ const leftPrincipal = left.group ?? left.user ?? "";
831
+ const rightPrincipal = right.group ?? right.user ?? "";
832
+ const principalComparison = leftPrincipal.localeCompare(rightPrincipal);
833
+ if (principalComparison !== 0) {
834
+ return principalComparison;
835
+ }
836
+ return left.permissionSet.localeCompare(right.permissionSet);
837
+ })
838
+ };
839
+ }
840
+ function renderAwsConfigTs(props) {
841
+ const serializedConfig = renderTsValue(props.config, {
842
+ indentLevel: 0,
843
+ withinInlinePolicy: false
844
+ });
845
+ return `import * as v from "valibot";
846
+ import { awsConfigSchema, iam, type AwsConfig } from "./aws.config.types.js";
847
+
848
+ /**
849
+ * Human-editable AWS config.
850
+ * Generated by "init"; refresh picklists after edits with "regenerate".
851
+ * Use helpers like iam.s3("GetObject") for IAM action autocomplete in inline policies.
852
+ * Generated inline policies use those helpers automatically when the action is
853
+ * present in the installed @beesolve/iam-policy-ts catalog.
854
+ * The synthetic { name: "root", parentName: null } entry represents organization root.
855
+ * "Graveyard" is bootstrap-managed and used internally as the account-removal sink;
856
+ * it is intentionally omitted from generated organizationalUnits in this file.
857
+ */
858
+ const awsConfig: AwsConfig = v.parse(awsConfigSchema, ${serializedConfig} satisfies AwsConfig);
859
+
860
+ export default awsConfig;
861
+ `;
862
+ }
863
+ function renderTsValue(value, props) {
864
+ if (value === null) {
865
+ return "null";
866
+ }
867
+ if (value === void 0) {
868
+ throw new Error("Undefined values must be handled before TypeScript rendering.");
869
+ }
870
+ if (typeof value === "string") {
871
+ return renderTsStringValue(value, props);
872
+ }
873
+ if (typeof value === "number" || typeof value === "boolean") {
874
+ return JSON.stringify(value);
875
+ }
876
+ if (Array.isArray(value)) {
877
+ return renderTsArray(value, props);
878
+ }
879
+ if (isJsonRecord(value)) {
880
+ return renderTsObject(value, props);
881
+ }
882
+ throw new Error(`Unsupported config value type: ${typeof value}.`);
883
+ }
884
+ function renderTsStringValue(value, props) {
885
+ if (props.withinInlinePolicy && (props.parentPropertyName === "Action" || props.parentPropertyName === "NotAction")) {
886
+ return renderPolicyActionString(value);
887
+ }
888
+ return JSON.stringify(value);
889
+ }
890
+ function renderTsArray(value, props) {
891
+ if (value.length === 0) {
892
+ return "[]";
893
+ }
894
+ const indent = " ".repeat(props.indentLevel);
895
+ const childIndent = " ".repeat(props.indentLevel + 1);
896
+ const renderedItems = value.map(
897
+ (item) => item === void 0 ? "null" : renderTsValue(item, {
898
+ indentLevel: props.indentLevel + 1,
899
+ withinInlinePolicy: props.withinInlinePolicy,
900
+ parentPropertyName: props.parentPropertyName
901
+ })
902
+ );
903
+ return `[
904
+ ${renderedItems.map((item) => `${childIndent}${item}`).join(",\n")}
905
+ ${indent}]`;
906
+ }
907
+ function renderTsObject(value, props) {
908
+ const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== void 0);
909
+ if (entries.length === 0) {
910
+ return "{}";
911
+ }
912
+ const indent = " ".repeat(props.indentLevel);
913
+ const childIndent = " ".repeat(props.indentLevel + 1);
914
+ const renderedEntries = entries.map(([key, entryValue]) => {
915
+ const nextWithinInlinePolicy = props.withinInlinePolicy || key === "inlinePolicy";
916
+ const renderedValue = renderTsValue(entryValue, {
917
+ indentLevel: props.indentLevel + 1,
918
+ withinInlinePolicy: nextWithinInlinePolicy,
919
+ parentPropertyName: key
920
+ });
921
+ return `${childIndent}${renderTsObjectKey(key)}: ${renderedValue}`;
922
+ });
923
+ return `{
924
+ ${renderedEntries.join(",\n")}
925
+ ${indent}}`;
926
+ }
927
+ function renderPolicyActionString(value) {
928
+ const separatorIndex = value.indexOf(":");
929
+ if (separatorIndex <= 0 || separatorIndex === value.length - 1) {
930
+ return JSON.stringify(value);
931
+ }
932
+ const servicePrefix = value.slice(0, separatorIndex);
933
+ const actionName = value.slice(separatorIndex + 1);
934
+ const knownActions = iamActionCatalog[servicePrefix];
935
+ if (knownActions == null || knownActions.includes(actionName) === false) {
936
+ return JSON.stringify(value);
937
+ }
938
+ if (isIdentifierSafeServicePrefix(servicePrefix)) {
939
+ return `iam.${servicePrefix}(${JSON.stringify(actionName)})`;
940
+ }
941
+ return `iam[${JSON.stringify(servicePrefix)}](${JSON.stringify(actionName)})`;
942
+ }
943
+ function isIdentifierSafeServicePrefix(value) {
944
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/u.test(value);
945
+ }
946
+ function renderTsObjectKey(value) {
947
+ return isIdentifierSafeServicePrefix(value) ? value : JSON.stringify(value);
948
+ }
949
+ function renderAwsConfigTypesTs(props) {
950
+ const organizationalUnitNames = props.config.organizationalUnits.map(
951
+ (ou) => ou.name
952
+ );
953
+ const accountNames = props.config.organizationalUnits.flatMap(
954
+ (ou) => ou.accounts.map((account) => account.name)
955
+ );
956
+ const permissionSetNames = props.config.permissionSets.map(
957
+ (permissionSet) => permissionSet.name
958
+ );
959
+ const groupNames = props.config.groups.map((group) => group.displayName);
960
+ const userNames = props.config.users.map((user) => user.userName);
961
+ const organizationalUnitNameSchema = renderPicklistSchema({
962
+ values: organizationalUnitNames
963
+ });
964
+ const accountNameSchema = renderPicklistSchema({
965
+ values: accountNames
966
+ });
967
+ const permissionSetNameSchema = renderPicklistSchema({
968
+ values: permissionSetNames
969
+ });
970
+ const groupNameSchema = renderPicklistSchema({
971
+ values: groupNames
972
+ });
973
+ const userNameSchema = renderPicklistSchema({
974
+ values: userNames
975
+ });
976
+ return `import * as v from "valibot";
977
+ import { iamPolicyDocumentSchema } from "@beesolve/iam-policy-ts";
978
+ export {
979
+ iam,
980
+ iamAction,
981
+ iamActionCatalog,
982
+ iamActionCatalogActionCount,
983
+ iamActionCatalogSourceSha256,
984
+ iamActionCatalogSourceUrl,
985
+ iamPolicyDocumentSchema,
986
+ iamPolicyStatementSchema,
987
+ iamPolicyDocumentStrictSchema,
988
+ iamPolicyStatementStrictSchema,
989
+ isIamPolicyDocument,
990
+ isIamPolicyStatement,
991
+ isIamPolicyDocumentStrict,
992
+ assertIamPolicyDocument,
993
+ assertIamPolicyDocumentStrict,
994
+ } from "@beesolve/iam-policy-ts";
995
+ export type {
996
+ IamActionCatalog,
997
+ IamPolicyServicePrefix,
998
+ IamPolicyActionNameByService,
999
+ IamPolicyActionForService,
1000
+ IamPolicyVersion,
1001
+ IamPolicyScalar,
1002
+ IamPolicyScalarList,
1003
+ IamPolicyStringList,
1004
+ IamPolicyPrincipalMap,
1005
+ IamPolicyPrincipal,
1006
+ IamPolicyConditionBlock,
1007
+ IamPolicyStatement,
1008
+ IamPolicyDocument,
1009
+ IamPolicyStatementStrict,
1010
+ IamPolicyDocumentStrict,
1011
+ } from "@beesolve/iam-policy-ts";
1012
+
1013
+ /**
1014
+ * Generated file. Do not edit by hand.
1015
+ */
1016
+ const organizationalUnitNameSchema = ${organizationalUnitNameSchema};
1017
+ const accountNameSchema = ${accountNameSchema};
1018
+ const permissionSetNameSchema = ${permissionSetNameSchema};
1019
+ const groupNameSchema = ${groupNameSchema};
1020
+ const userNameSchema = ${userNameSchema};
1021
+
1022
+ export const awsConfigSchema = v.strictObject({
1023
+ organizationalUnits: v.array(
1024
+ v.strictObject({
1025
+ name: v.string(),
1026
+ parentName: v.union([organizationalUnitNameSchema, v.null_()]),
1027
+ accounts: v.array(
1028
+ v.strictObject({
1029
+ name: v.string(),
1030
+ email: v.string(),
1031
+ tags: v.array(
1032
+ v.strictObject({
1033
+ key: v.string(),
1034
+ value: v.string(),
1035
+ }),
1036
+ ),
1037
+ }),
1038
+ ),
1039
+ }),
1040
+ ),
1041
+ users: v.array(
1042
+ v.strictObject({
1043
+ userName: v.string(),
1044
+ displayName: v.string(),
1045
+ email: v.string(),
1046
+ }),
1047
+ ),
1048
+ groups: v.array(
1049
+ v.strictObject({
1050
+ displayName: v.string(),
1051
+ description: v.optional(v.string()),
1052
+ members: v.array(userNameSchema),
1053
+ }),
1054
+ ),
1055
+ permissionSets: v.array(
1056
+ v.strictObject({
1057
+ name: v.string(),
1058
+ description: v.string(),
1059
+ inlinePolicy: v.optional(iamPolicyDocumentSchema),
1060
+ awsManagedPolicies: v.array(v.string()),
1061
+ customerManagedPolicies: v.array(
1062
+ v.strictObject({
1063
+ name: v.string(),
1064
+ path: v.string(),
1065
+ }),
1066
+ ),
1067
+ }),
1068
+ ),
1069
+ assignments: v.array(
1070
+ v.strictObject({
1071
+ permissionSet: permissionSetNameSchema,
1072
+ group: v.optional(groupNameSchema),
1073
+ user: v.optional(userNameSchema),
1074
+ accounts: v.array(accountNameSchema),
1075
+ }),
1076
+ ),
1077
+ });
1078
+
1079
+ export type AwsConfig = v.InferOutput<typeof awsConfigSchema>;
1080
+ `;
1081
+ }
1082
+ function assertStateMatchesContext(props) {
1083
+ if (props.state.organization.rootId !== props.context.organization.rootId) {
1084
+ throw new Error(
1085
+ `state/context mismatch for organization.rootId: state has "${props.state.organization.rootId}" but context has "${props.context.organization.rootId}".`
1086
+ );
1087
+ }
1088
+ const graveyardOrganizationalUnit = props.state.organization.organizationalUnits.find(
1089
+ (ou) => ou.name === "Graveyard"
1090
+ );
1091
+ if (graveyardOrganizationalUnit?.id !== props.context.organization.graveyardOuId) {
1092
+ throw new Error(
1093
+ `state/context mismatch for Graveyard OU id: state has "${graveyardOrganizationalUnit?.id ?? "<missing>"}" but context has "${props.context.organization.graveyardOuId}".`
1094
+ );
1095
+ }
1096
+ if (props.state.identityCenter.instanceArn !== props.context.identityCenter.instanceArn || props.state.identityCenter.identityStoreId !== props.context.identityCenter.identityStoreId) {
1097
+ throw new Error(
1098
+ "state/context mismatch for identityCenter.instanceArn or identityCenter.identityStoreId."
1099
+ );
1100
+ }
1101
+ }
1102
+ function assertUniqueNames(props) {
1103
+ const seen = /* @__PURE__ */ new Set();
1104
+ const duplicates = /* @__PURE__ */ new Set();
1105
+ for (const value of props.values) {
1106
+ if (seen.has(value)) {
1107
+ duplicates.add(value);
1108
+ }
1109
+ seen.add(value);
1110
+ }
1111
+ if (duplicates.size > 0) {
1112
+ throw new Error(
1113
+ `Duplicate ${props.entityName} names detected: ${[...duplicates.values()].join(", ")}.`
1114
+ );
1115
+ }
1116
+ }
1117
+ function mapAssignmentPrincipal(props) {
1118
+ const principalType = props.assignment.principalType;
1119
+ if (principalType === "GROUP") {
1120
+ const groupDisplayName = props.groupById[props.assignment.principalId]?.displayName;
1121
+ if (groupDisplayName == null) {
1122
+ throw new Error(
1123
+ `Could not resolve group display name for principalId "${props.assignment.principalId}".`
1124
+ );
1125
+ }
1126
+ return {
1127
+ kind: "group",
1128
+ value: groupDisplayName
1129
+ };
1130
+ }
1131
+ if (principalType === "USER") {
1132
+ const userName = props.userById[props.assignment.principalId]?.userName;
1133
+ if (userName == null) {
1134
+ throw new Error(
1135
+ `Could not resolve user name for principalId "${props.assignment.principalId}".`
1136
+ );
1137
+ }
1138
+ return {
1139
+ kind: "user",
1140
+ value: userName
1141
+ };
1142
+ }
1143
+ assertUnreachable(
1144
+ principalType,
1145
+ `Unsupported principal type "${principalType}" in account assignment.`
1146
+ );
1147
+ }
1148
+ function createGroupMembershipNameKey(props) {
1149
+ return [props.groupDisplayName, props.userName].join("|");
1150
+ }
1151
+ function parseInlinePolicyForConfig(props) {
1152
+ let parsed;
1153
+ try {
1154
+ parsed = JSON.parse(props.inlinePolicy);
1155
+ } catch (error) {
1156
+ const message = error instanceof Error ? error.message : String(error);
1157
+ throw new Error(
1158
+ `Could not parse inline policy for permission set "${props.permissionSetName}": ${message}`
1159
+ );
1160
+ }
1161
+ if (isJsonRecord(parsed) === false) {
1162
+ throw new Error(
1163
+ `Inline policy for permission set "${props.permissionSetName}" must be a JSON object.`
1164
+ );
1165
+ }
1166
+ return sortJsonRecord(assertIamPolicyDocument(parsed));
1167
+ }
1168
+ function stableStringifyInlinePolicy(inlinePolicy) {
1169
+ if (inlinePolicy == null) {
1170
+ return null;
1171
+ }
1172
+ return JSON.stringify(sortJsonRecord(assertIamPolicyDocument(inlinePolicy)));
1173
+ }
1174
+ function sortJsonRecord(input) {
1175
+ return Object.fromEntries(
1176
+ Object.entries(input).sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey)).map(([key, value]) => [key, sortJsonValue(value)])
1177
+ );
1178
+ }
1179
+ function sortJsonValue(value) {
1180
+ if (Array.isArray(value)) {
1181
+ return value.map((entry) => sortJsonValue(entry));
1182
+ }
1183
+ if (isJsonRecord(value)) {
1184
+ return sortJsonRecord(value);
1185
+ }
1186
+ return value;
1187
+ }
1188
+ function isJsonRecord(value) {
1189
+ return value != null && typeof value === "object" && Array.isArray(value) === false;
1190
+ }
1191
+ function compareStringKeys(...values) {
1192
+ for (let index = 0; index < values.length; index += 2) {
1193
+ const left = values[index] ?? "";
1194
+ const right = values[index + 1] ?? "";
1195
+ const compared = left.localeCompare(right);
1196
+ if (compared !== 0) {
1197
+ return compared;
1198
+ }
1199
+ }
1200
+ return 0;
1201
+ }
1202
+ function resolveOrganizationalUnitId(props) {
1203
+ if (props.organizationalUnitName === "root") {
1204
+ return props.context.organization.rootId;
1205
+ }
1206
+ if (props.organizationalUnitName === "Graveyard") {
1207
+ return props.context.organization.graveyardOuId;
1208
+ }
1209
+ return props.matchedOrganizationalUnit?.id ?? pendingCreationId;
1210
+ }
1211
+ function renderPicklistSchema(props) {
1212
+ if (props.values.length === 0) {
1213
+ return 'v.picklist(["__EMPTY_PICKLIST__"])';
1214
+ }
1215
+ const literals = [...props.values].sort((left, right) => left.localeCompare(right)).map((value) => JSON.stringify(value)).join(", ");
1216
+ return `v.picklist([${literals}])`;
1217
+ }
1218
+ async function readAwsContextFile(path) {
1219
+ const rawContent = await readFile(path, "utf8");
1220
+ const parsed = JSON.parse(rawContent);
1221
+ return v.parse(awsContextSchema, parsed);
1222
+ }
1223
+ async function loadAwsConfigModelFromTsFile(props) {
1224
+ const typesModule = await loadAwsConfigTypesModule({
1225
+ typesPath: props.typesPath
1226
+ });
1227
+ return await loadAwsConfigFromTsFile({
1228
+ configPath: props.configPath,
1229
+ schema: typesModule.awsConfigSchema
1230
+ });
1231
+ }
1232
+ async function readAwsContextFromFile(path) {
1233
+ return readAwsContextFile(path);
1234
+ }
1235
+ async function loadAwsConfigTypesModule(props) {
1236
+ const loadedModule = await loadTsModule({
1237
+ modulePath: props.typesPath
1238
+ });
1239
+ if (loadedModule == null || typeof loadedModule !== "object" || "awsConfigSchema" in loadedModule === false) {
1240
+ throw new Error(
1241
+ `Types module "${props.typesPath}" does not export awsConfigSchema.`
1242
+ );
1243
+ }
1244
+ const moduleWithSchema = loadedModule;
1245
+ if (moduleWithSchema.awsConfigSchema == null) {
1246
+ throw new Error(
1247
+ `Types module "${props.typesPath}" does not export awsConfigSchema.`
1248
+ );
1249
+ }
1250
+ return {
1251
+ awsConfigSchema: moduleWithSchema.awsConfigSchema
1252
+ };
1253
+ }
1254
+ async function loadAwsConfigFromTsFile(props) {
1255
+ let loadedModule;
1256
+ try {
1257
+ loadedModule = await loadTsModule({
1258
+ modulePath: props.configPath
1259
+ });
1260
+ } catch (error) {
1261
+ if (isValiErrorLike(error)) {
1262
+ throw new Error(
1263
+ `aws.config.ts validation failed: ${error instanceof Error ? error.message : String(error)}. If you recently edited names/references, re-run regenerate after fixing the config.`
1264
+ );
1265
+ }
1266
+ throw error;
1267
+ }
1268
+ if (loadedModule == null || typeof loadedModule !== "object" || "default" in loadedModule === false) {
1269
+ throw new Error(
1270
+ `Config module "${props.configPath}" must export a default config object.`
1271
+ );
1272
+ }
1273
+ const moduleWithDefault = loadedModule;
1274
+ if (moduleWithDefault.default == null) {
1275
+ throw new Error(
1276
+ `Config module "${props.configPath}" must export a default config object.`
1277
+ );
1278
+ }
1279
+ try {
1280
+ const validatedConfig = v.parse(props.schema, moduleWithDefault.default);
1281
+ return v.parse(awsConfigModelSchema, validatedConfig);
1282
+ } catch (error) {
1283
+ if (isValiErrorLike(error)) {
1284
+ throw new Error(
1285
+ `aws.config.ts validation failed: ${error instanceof Error ? error.message : String(error)}. If you recently edited names/references, re-run regenerate after fixing the config.`
1286
+ );
1287
+ }
1288
+ throw error;
1289
+ }
1290
+ }
1291
+ async function loadTsModule(props) {
1292
+ const resolvedModulePath = resolve(props.modulePath);
1293
+ const temporaryOutputPath = join(`aws-accounts-${randomUUID()}.mjs`);
1294
+ const temporaryOutputAtProjectRoot = join(
1295
+ projectRootPath,
1296
+ temporaryOutputPath
1297
+ );
1298
+ try {
1299
+ await esbuildBuild({
1300
+ entryPoints: [resolvedModulePath],
1301
+ outfile: temporaryOutputAtProjectRoot,
1302
+ bundle: true,
1303
+ platform: "node",
1304
+ format: "esm",
1305
+ target: "node24",
1306
+ absWorkingDir: projectRootPath,
1307
+ nodePaths: [join(projectRootPath, "node_modules")],
1308
+ write: true
1309
+ });
1310
+ const moduleUrl = pathToFileURL(temporaryOutputAtProjectRoot).href;
1311
+ return await import(moduleUrl);
1312
+ } finally {
1313
+ await safeUnlink(temporaryOutputAtProjectRoot);
1314
+ }
1315
+ }
1316
+ async function readIfExists(path) {
1317
+ try {
1318
+ return await readFile(path, "utf8");
1319
+ } catch (error) {
1320
+ const code = error.code;
1321
+ if (code === "ENOENT") {
1322
+ return void 0;
1323
+ }
1324
+ throw error;
1325
+ }
1326
+ }
1327
+ async function safeUnlink(path) {
1328
+ try {
1329
+ await unlink(path);
1330
+ } catch (error) {
1331
+ const code = error.code;
1332
+ if (code === "ENOENT") {
1333
+ return;
1334
+ }
1335
+ throw error;
1336
+ }
1337
+ }
1338
+ function isValiErrorLike(error) {
1339
+ return error instanceof v.ValiError || error instanceof Error && error.name === "ValiError";
1340
+ }
1341
+ async function regenerateTypesFromState(props) {
1342
+ try {
1343
+ const mappedConfig = mapStateToAwsConfig({ state: props.state });
1344
+ const sortedConfig = sortAwsConfigModel({ config: mappedConfig });
1345
+ const nextTypesContent = renderAwsConfigTypesTs({ config: sortedConfig });
1346
+ const currentTypesContent = await readIfExists(props.typesPath);
1347
+ if (currentTypesContent === nextTypesContent) {
1348
+ return;
1349
+ }
1350
+ await writeFile(props.typesPath, nextTypesContent, "utf8");
1351
+ props.logger.log("Updated aws.config.types.ts");
1352
+ } catch (error) {
1353
+ const message = error instanceof Error ? error.message : String(error);
1354
+ props.logger.log(`Warning: Failed to regenerate types: ${message}`);
1355
+ }
1356
+ }
1357
+ export {
1358
+ awsConfigModelSchema,
1359
+ loadAwsConfigModelFromTsFile,
1360
+ mapAwsConfigToState,
1361
+ readAwsContextFromFile,
1362
+ regenerateAwsConfigTypes,
1363
+ regenerateTypesFromState,
1364
+ writeAwsConfigFromState
1365
+ };