@fjall/deploy-core 2.11.1 → 2.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/.minified +1 -1
  2. package/dist/src/aws/cloudtrail/orgTrailDelivery.d.ts +44 -0
  3. package/dist/src/aws/cloudtrail/orgTrailDelivery.js +1 -0
  4. package/dist/src/aws/organisations/accounts.d.ts +3 -1
  5. package/dist/src/aws/organisations/accounts.js +1 -1
  6. package/dist/src/aws/organisations/backup.d.ts +3 -2
  7. package/dist/src/aws/organisations/backup.js +2 -2
  8. package/dist/src/aws/organisations/organisationalUnits.d.ts +1 -1
  9. package/dist/src/aws/organisations/organisationalUnits.js +1 -1
  10. package/dist/src/aws/organisations/policies.js +1 -1
  11. package/dist/src/aws/organisations/serviceAccess.js +1 -1
  12. package/dist/src/aws/organisations/types.d.ts +6 -0
  13. package/dist/src/events/index.d.ts +2 -0
  14. package/dist/src/events/index.js +1 -1
  15. package/dist/src/index.d.ts +6 -2
  16. package/dist/src/index.js +1 -1
  17. package/dist/src/orchestration/accountsConfig.d.ts +11 -0
  18. package/dist/src/orchestration/accountsConfig.js +1 -1
  19. package/dist/src/orchestration/cascadeDestroyHelpers.d.ts +1 -1
  20. package/dist/src/orchestration/cascadeDestroyHelpers.js +1 -1
  21. package/dist/src/orchestration/cascadeHelpers.d.ts +16 -0
  22. package/dist/src/orchestration/cascadeHelpers.js +1 -1
  23. package/dist/src/orchestration/contextHelpers.d.ts +3 -0
  24. package/dist/src/orchestration/contextHelpers.js +1 -1
  25. package/dist/src/orchestration/index.d.ts +5 -1
  26. package/dist/src/orchestration/index.js +1 -1
  27. package/dist/src/orchestration/organisationDeploy.js +5 -5
  28. package/dist/src/orchestration/organisationSetup.d.ts +5 -1
  29. package/dist/src/orchestration/organisationSetup.js +1 -1
  30. package/dist/src/orchestration/reconcileProviderAccounts.js +1 -1
  31. package/dist/src/orchestration/stackCleanup.d.ts +7 -0
  32. package/dist/src/orchestration/stackCleanup.js +1 -1
  33. package/dist/src/orchestration/trailMigration/memberTrailCleanup.d.ts +43 -0
  34. package/dist/src/orchestration/trailMigration/memberTrailCleanup.js +1 -0
  35. package/dist/src/orchestration/trailMigration/trailMigration.d.ts +64 -0
  36. package/dist/src/orchestration/trailMigration/trailMigration.js +1 -0
  37. package/dist/src/services/infrastructure/CdkArgumentBuilder.js +1 -1
  38. package/dist/src/services/infrastructure/CdkServiceTypes.d.ts +1 -0
  39. package/dist/src/services/infrastructure/cdkServiceHelpers.js +1 -1
  40. package/dist/src/services/supporting/CdkContextBuilder.d.ts +1 -0
  41. package/dist/src/services/supporting/CdkContextBuilder.js +1 -1
  42. package/dist/src/types/callbackKeys.d.ts +1 -1
  43. package/dist/src/types/callbackKeys.js +1 -1
  44. package/dist/src/types/callbacks.d.ts +15 -1
  45. package/dist/src/types/deployment/DeploymentTypes.d.ts +1 -0
  46. package/dist/src/types/deploymentEventSchema.d.ts +19 -3
  47. package/dist/src/types/deploymentEventSchema.js +1 -1
  48. package/dist/src/types/events.d.ts +12 -0
  49. package/dist/src/types/events.js +1 -0
  50. package/dist/src/types/index.d.ts +2 -1
  51. package/dist/src/types/index.js +1 -1
  52. package/dist/src/types/params.d.ts +6 -0
  53. package/dist/src/util/index.d.ts +1 -0
  54. package/dist/src/util/index.js +1 -1
  55. package/dist/src/util/sleepAbortable.d.ts +8 -0
  56. package/dist/src/util/sleepAbortable.js +1 -0
  57. package/package.json +6 -4
package/dist/.minified CHANGED
@@ -1 +1 @@
1
- 121 files minified at 2026-06-07T01:02:15.647Z
1
+ 125 files minified at 2026-06-10T21:04:38.185Z
@@ -0,0 +1,44 @@
1
+ import { type S3Client } from "@aws-sdk/client-s3";
2
+ import { type CloudTrailClient } from "@aws-sdk/client-cloudtrail";
3
+ import { type Result } from "@fjall/generator";
4
+ export interface OrgTrailAccountDelivery {
5
+ accountId: string;
6
+ /** At least one log object observed in the recency window's date partitions. */
7
+ delivered: boolean;
8
+ /**
9
+ * An observed object falls inside the recency window. `undefined` when the
10
+ * facts could not be determined: region discovery was truncated or over the
11
+ * cap, or a window partition exhausted its page cap before recency was
12
+ * proven. The reconciler treats `undefined` as "do not advance", never as
13
+ * verified.
14
+ */
15
+ recentDelivery: boolean | undefined;
16
+ latestObjectAt?: Date;
17
+ }
18
+ export interface OrgTrailDeliveryReport {
19
+ bucketName: string;
20
+ perAccount: OrgTrailAccountDelivery[];
21
+ }
22
+ /**
23
+ * Verify per-member delivery into the organisation trail bucket under the
24
+ * management account. Region prefixes are discovered with one delimiter
25
+ * listing of AWSLogs/<orgId>/<accountId>/CloudTrail/, then only the date
26
+ * partitions covering the recency window are scanned. Read-only; a
27
+ * NoSuchBucket failure means the org trail has not deployed yet.
28
+ */
29
+ export declare function verifyOrgTrailDelivery(s3Client: S3Client, options: {
30
+ bucketName: string;
31
+ orgId: string;
32
+ accountIds: string[];
33
+ recencyWindowMs?: number;
34
+ abortSignal?: AbortSignal;
35
+ }): Promise<Result<OrgTrailDeliveryReport>>;
36
+ export interface TrailLoggingStatus {
37
+ isLogging: boolean;
38
+ latestDeliveryTime?: Date;
39
+ }
40
+ /**
41
+ * Reverse-path primitive: before leaving the organisation trail (org →
42
+ * account), the recreated per-account trail must be confirmed logging.
43
+ */
44
+ export declare function getTrailLoggingStatus(cloudTrailClient: CloudTrailClient, trailName: string, abortSignal?: AbortSignal): Promise<Result<TrailLoggingStatus>>;
@@ -0,0 +1 @@
1
+ import{ListObjectsV2Command as M}from"@aws-sdk/client-s3";import{GetTrailStatusCommand as D}from"@aws-sdk/client-cloudtrail";import{success as h,failure as S}from"@fjall/generator";import{getErrorMessage as E,maskSensitiveOutput as P}from"@fjall/util";import{SDK_TIMEOUT_MS as k,extractErrorName as w}from"../organisations/types.js";const f=1440*60*1e3,x=f,v=50,A=2;function b(n){const e=AbortSignal.timeout(k);return n!==void 0?AbortSignal.any([n,e]):e}function N(n,e){const r=[],t=Math.floor((e-n)/f)*f;for(let c=t;c<=e;c+=f){const i=new Date(c),s=String(i.getUTCMonth()+1).padStart(2,"0"),o=String(i.getUTCDate()).padStart(2,"0");r.push(`${i.getUTCFullYear()}/${s}/${o}`)}return r.reverse()}async function I(n,e){let r=!1,t,c=!1,i=!1;const s=a=>{for(const u of a.Contents??[])r=!0,u.LastModified!==void 0&&((t===void 0||u.LastModified>t)&&(t=u.LastModified),u.LastModified.getTime()>=e.recentSinceMs&&(c=!0))},o=a=>({delivered:r,recentDelivery:a,...t!==void 0?{latestObjectAt:t}:{}}),d=await n.send(new M({Bucket:e.bucketName,Prefix:e.accountPrefix,Delimiter:"/"}),{abortSignal:b(e.abortSignal)});s(d);const y=(d.CommonPrefixes??[]).map(a=>a.Prefix).filter(a=>a!==void 0);if(d.IsTruncated===!0||y.length>v)return o(void 0);for(const a of y)for(const u of e.datePaths){let l,g=!1;for(let T=0;T<A&&!g;T++){const m=await n.send(new M({Bucket:e.bucketName,Prefix:`${a}${u}/`,...l!==void 0?{ContinuationToken:l}:{}}),{abortSignal:b(e.abortSignal)});if(s(m),c)return o(!0);m.IsTruncated===!0?l=m.NextContinuationToken:g=!0}g||(i=!0)}return o(i?void 0:!1)}async function O(n,e){const r=e.recencyWindowMs??x,t=Date.now(),c=N(r,t),i=[];for(const s of e.accountIds)try{const o=await I(n,{bucketName:e.bucketName,accountPrefix:`AWSLogs/${e.orgId}/${s}/CloudTrail/`,datePaths:c,recentSinceMs:t-r,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}});i.push({accountId:s,...o})}catch(o){return w(o)==="NoSuchBucket"?S(new Error(`Organisation trail bucket ${e.bucketName} does not exist \u2014 the org trail has not been deployed yet.`)):S(new Error(`Failed to verify org-trail delivery for account ${s}: ${P(E(o))}`))}return h({bucketName:e.bucketName,perAccount:i})}async function F(n,e,r){try{const t=await n.send(new D({Name:e}),{abortSignal:b(r)});return h({isLogging:t.IsLogging===!0,...t.LatestDeliveryTime!==void 0?{latestDeliveryTime:t.LatestDeliveryTime}:{}})}catch(t){return S(new Error(`Failed to read trail status for ${e}: ${P(E(t))}`))}}export{F as getTrailLoggingStatus,O as verifyOrgTrailDelivery};
@@ -17,5 +17,7 @@ export declare function findAccount(client: OrganizationsClient, accountId: stri
17
17
  *
18
18
  * @param maxAttempts Maximum polling attempts (default: 180 = ~15 minutes at 5s intervals)
19
19
  * @param delayMs Delay between polling attempts in milliseconds (default: 5000)
20
+ * @param abortSignal Optional shutdown signal — short-circuits the poll loop
21
+ * and inter-poll sleeps so SIGTERM does not stall for up to ~15 minutes
20
22
  */
21
- export declare function createAccount(client: OrganizationsClient, accountName: string, email: string, maxAttempts?: number, delayMs?: number): Promise<Result<CreateAccountResult>>;
23
+ export declare function createAccount(client: OrganizationsClient, accountName: string, email: string, maxAttempts?: number, delayMs?: number, abortSignal?: AbortSignal): Promise<Result<CreateAccountResult>>;
@@ -1 +1 @@
1
- import{ListAccountsCommand as f,CreateAccountCommand as m,CreateAccountState as E,DescribeCreateAccountStatusCommand as p}from"@aws-sdk/client-organizations";import{success as A,failure as e}from"@fjall/generator";import{sleep as C,getErrorMessage as w}from"@fjall/util";import{extractErrorName as S,SDK_TIMEOUT_MS as d,AWS_ERROR_NAMES as g}from"./types.js";async function x(o){try{const t=await o.send(new f({MaxResults:20}),{abortSignal:AbortSignal.timeout(d)});let r=t.Accounts??[],n=t.NextToken;for(;n;){const s=await o.send(new f({MaxResults:20,NextToken:n}),{abortSignal:AbortSignal.timeout(d)});r=r.concat(s.Accounts??[]),n=s.NextToken}return A(r)}catch(t){return S(t)===g.ORGS_NOT_IN_USE?e(new Error("AWS Organisations is not enabled for this account")):e(new Error(`Failed to list accounts: ${w(t)}`))}}async function $(o,t){const r=await x(o);return r.success?A(r.data.find(n=>n.Id===t)):r}async function D(o,t,r,n=180,s=5e3){try{let i;try{i=await o.send(new m({AccountName:t,Email:r}),{abortSignal:AbortSignal.timeout(d)})}catch(c){const u=S(c);if(u===g.ACCESS_DENIED)return e(new Error(`Access denied when creating account "${t}". Ensure your credentials have organizations:CreateAccount permission.`));if(u==="DuplicateAccountException")return e(new Error(`An account with the email "${r}" already exists in the organisation.`));if(u==="FinalizingOrganizationException")return e(new Error("The organisation is still being initialised. Please wait and try again."));throw c}const l=i.CreateAccountStatus?.Id;if(!l)return e(new Error(`CreateAccount request for "${t}" did not return a status ID`));for(let c=0;c<n;c++){const a=(await o.send(new p({CreateAccountRequestId:l}),{abortSignal:AbortSignal.timeout(d)})).CreateAccountStatus;if(!a)return e(new Error(`No status returned for CreateAccount request ${l}`));if(a.State===E.SUCCEEDED)return a.AccountId?A({accountId:a.AccountId,accountName:t}):e(new Error(`Account creation succeeded but no account ID returned for "${t}"`));if(a.State===E.FAILED)return e(new Error(`Account creation failed for "${t}": ${a.FailureReason}`));await C(s)}return e(new Error(`Account creation for "${t}" timed out after ${n*s/1e3} seconds`))}catch(i){return e(new Error(`Failed to create account "${t}": ${w(i)}`))}}export{D as createAccount,$ as findAccount,x as listAccounts};
1
+ import{ListAccountsCommand as w,CreateAccountCommand as C,CreateAccountState as E,DescribeCreateAccountStatusCommand as $}from"@aws-sdk/client-organizations";import{success as f,failure as r}from"@fjall/generator";import{getErrorMessage as S}from"@fjall/util";import{extractErrorName as m,SDK_TIMEOUT_MS as A,AWS_ERROR_NAMES as p}from"./types.js";import{sleepAbortable as h}from"../../util/sleepAbortable.js";async function x(e){try{const t=await e.send(new w({MaxResults:20}),{abortSignal:AbortSignal.timeout(A)});let n=t.Accounts??[],o=t.NextToken;for(;o;){const c=await e.send(new w({MaxResults:20,NextToken:o}),{abortSignal:AbortSignal.timeout(A)});n=n.concat(c.Accounts??[]),o=c.NextToken}return f(n)}catch(t){return m(t)===p.ORGS_NOT_IN_USE?r(new Error("AWS Organisations is not enabled for this account")):r(new Error(`Failed to list accounts: ${S(t)}`))}}async function _(e,t){const n=await x(e);return n.success?f(n.data.find(o=>o.Id===t)):n}function g(e){const t=AbortSignal.timeout(A);return e!==void 0?AbortSignal.any([e,t]):t}function b(e){return e?.aborted===!0}async function O(e,t,n,o=180,c=5e3,i){if(b(i))return r(new Error(`Aborted: account creation for "${t}" cancelled by shutdown signal before starting`));try{let u;try{u=await e.send(new C({AccountName:t,Email:n}),{abortSignal:g(i)})}catch(a){const l=m(a);if(l===p.ACCESS_DENIED)return r(new Error(`Access denied when creating account "${t}". Ensure your credentials have organizations:CreateAccount permission.`));if(l==="DuplicateAccountException")return r(new Error(`An account with the email "${n}" already exists in the organisation.`));if(l==="FinalizingOrganizationException")return r(new Error("The organisation is still being initialised. Please wait and try again."));throw a}const d=u.CreateAccountStatus?.Id;if(!d)return r(new Error(`CreateAccount request for "${t}" did not return a status ID`));for(let a=0;a<o;a++){if(b(i))return r(new Error(`Aborted: account creation polling for "${t}" cancelled by shutdown signal \u2014 AWS request ${d} may still complete server-side`));const s=(await e.send(new $({CreateAccountRequestId:d}),{abortSignal:g(i)})).CreateAccountStatus;if(!s)return r(new Error(`No status returned for CreateAccount request ${d}`));if(s.State===E.SUCCEEDED)return s.AccountId?f({accountId:s.AccountId,accountName:t}):r(new Error(`Account creation succeeded but no account ID returned for "${t}"`));if(s.State===E.FAILED)return r(new Error(`Account creation failed for "${t}": ${s.FailureReason}`));await h(c,i)}return r(new Error(`Account creation for "${t}" timed out after ${o*c/1e3} seconds`))}catch(u){return r(new Error(`Failed to create account "${t}": ${S(u)}`))}}export{O as createAccount,_ as findAccount,x as listAccounts};
@@ -1,6 +1,7 @@
1
1
  import { type BackupClient } from "@aws-sdk/client-backup";
2
2
  import { type Result } from "@fjall/generator";
3
- export declare const BACKUP_VAULT_NAME = "backupVault";
3
+ import { BACKUP_VAULT_NAME } from "@fjall/util";
4
+ export { BACKUP_VAULT_NAME };
4
5
  export interface BackupGlobalSettings {
5
6
  enableCrossAccountBackup?: boolean;
6
7
  enableDelegatedAdministrator?: boolean;
@@ -22,7 +23,7 @@ export declare function updateBackupGlobalSettings(client: BackupClient, setting
22
23
  * dead work that fails on a `backup:DescribeBackupVault` permission those
23
24
  * accounts never grant. Keep this predicate in lockstep with account.ts:122-125.
24
25
  */
25
- export declare function accountHasDisasterRecovery(environment: string | undefined, disasterRecoveryRegion: string | undefined): boolean;
26
+ export declare function accountHasDisasterRecovery(environment: string | null | undefined, disasterRecoveryRegion: string | undefined): boolean;
26
27
  /**
27
28
  * Probe whether the disaster-recovery backup vault already exists in the target
28
29
  * account/region, to decide adopt-vs-create. A retained, vault-locked DR vault
@@ -1,2 +1,2 @@
1
- import{DescribeBackupVaultCommand as s,UpdateGlobalSettingsCommand as p}from"@aws-sdk/client-backup";import{success as o,failure as i}from"@fjall/generator";import{getErrorMessage as c,maskSensitiveOutput as l}from"@fjall/util";import{SDK_TIMEOUT_MS as u}from"./types.js";const a="backupVault",m={enableCrossAccountBackup:!0,enableDelegatedAdministrator:!0,enableMpa:!1};async function y(e,t){try{const n={...m,...t},r={isCrossAccountBackupEnabled:n.enableCrossAccountBackup.toString(),isDelegatedAdministratorEnabled:n.enableDelegatedAdministrator.toString(),isMpaEnabled:n.enableMpa.toString()};return await e.send(new p({GlobalSettings:r}),{abortSignal:AbortSignal.timeout(u)}),o(void 0)}catch(n){return i(new Error(`Failed to update backup global settings: ${c(n)}`))}}function S(e,t){return t===void 0||t===""?!1:e==="production"||e==="compliance"}async function A(e){try{return await e.send(new s({BackupVaultName:a}),{abortSignal:AbortSignal.timeout(u)}),o(!0)}catch(t){return t instanceof Error&&t.name==="ResourceNotFoundException"?o(!1):i(new Error(`Failed to describe backup vault "${a}": ${l(c(t))}`))}}async function E(e,t){try{const n=await e.send(new s({BackupVaultName:a}),{abortSignal:AbortSignal.timeout(u)}),r=n.LockDate,d=r!==void 0&&r.getTime()<=Date.now();return o({vaultName:n.BackupVaultName??a,region:t,locked:n.Locked??!1,...r!==void 0&&{lockDate:r},lockPermanent:d,recoveryPointCount:n.NumberOfRecoveryPoints??0,...n.EncryptionKeyArn!==void 0&&{encryptionKeyArn:n.EncryptionKeyArn}})}catch(n){return n instanceof Error&&n.name==="ResourceNotFoundException"?o(null):i(new Error(`Failed to describe surviving backup vault "${a}": ${l(c(n))}`))}}function v(e){const t=e.locked?e.lockPermanent?"compliance-locked, PERMANENT \u2014 cannot be removed by anyone, including AWS or the root user":e.lockDate!==void 0?`compliance-locked, immutable from ${e.lockDate.toISOString().slice(0,10)}`:"governance-locked \u2014 removable by privileged IAM":"unlocked";return[`Backup vault "${e.vaultName}" in ${e.region} SURVIVES this destroy.`,` \u2022 Lock: ${t}`,` \u2022 Recovery points retained: ${e.recoveryPointCount}`,...e.encryptionKeyArn!==void 0?[` \u2022 Retained KMS key (also orphaned): ${e.encryptionKeyArn}`]:[]," The vault and its KMS key are RemovalPolicy.RETAIN \u2014 destroy cannot delete them."].join(`
2
- `)}export{a as BACKUP_VAULT_NAME,S as accountHasDisasterRecovery,A as describeBackupVaultExists,E as describeSurvivingBackupVault,v as formatSurvivingVaultWarning,y as updateBackupGlobalSettings};
1
+ import{DescribeBackupVaultCommand as s,UpdateGlobalSettingsCommand as p}from"@aws-sdk/client-backup";import{success as a,failure as i}from"@fjall/generator";import{BACKUP_VAULT_NAME as o,getErrorMessage as c,maskSensitiveOutput as l}from"@fjall/util";import{SDK_TIMEOUT_MS as u}from"./types.js";const m={enableCrossAccountBackup:!0,enableDelegatedAdministrator:!0,enableMpa:!1};async function y(e,t){try{const n={...m,...t},r={isCrossAccountBackupEnabled:n.enableCrossAccountBackup.toString(),isDelegatedAdministratorEnabled:n.enableDelegatedAdministrator.toString(),isMpaEnabled:n.enableMpa.toString()};return await e.send(new p({GlobalSettings:r}),{abortSignal:AbortSignal.timeout(u)}),a(void 0)}catch(n){return i(new Error(`Failed to update backup global settings: ${c(n)}`))}}function S(e,t){return t===void 0||t===""?!1:e==="production"||e==="compliance"}async function A(e){try{return await e.send(new s({BackupVaultName:o}),{abortSignal:AbortSignal.timeout(u)}),a(!0)}catch(t){return t instanceof Error&&t.name==="ResourceNotFoundException"?a(!1):i(new Error(`Failed to describe backup vault "${o}": ${l(c(t))}`))}}async function E(e,t){try{const n=await e.send(new s({BackupVaultName:o}),{abortSignal:AbortSignal.timeout(u)}),r=n.LockDate,d=r!==void 0&&r.getTime()<=Date.now();return a({vaultName:n.BackupVaultName??o,region:t,locked:n.Locked??!1,...r!==void 0&&{lockDate:r},lockPermanent:d,recoveryPointCount:n.NumberOfRecoveryPoints??0,...n.EncryptionKeyArn!==void 0&&{encryptionKeyArn:n.EncryptionKeyArn}})}catch(n){return n instanceof Error&&n.name==="ResourceNotFoundException"?a(null):i(new Error(`Failed to describe surviving backup vault "${o}": ${l(c(n))}`))}}function v(e){const t=e.locked?e.lockPermanent?"compliance-locked, PERMANENT \u2014 cannot be removed by anyone, including AWS or the root user":e.lockDate!==void 0?`compliance-locked, immutable from ${e.lockDate.toISOString().slice(0,10)}`:"governance-locked \u2014 removable by privileged IAM":"unlocked";return[`Backup vault "${e.vaultName}" in ${e.region} SURVIVES this destroy.`,` \u2022 Lock: ${t}`,` \u2022 Recovery points retained: ${e.recoveryPointCount}`,...e.encryptionKeyArn!==void 0?[` \u2022 Retained KMS key (also orphaned): ${e.encryptionKeyArn}`]:[]," The vault and its KMS key are RemovalPolicy.RETAIN \u2014 destroy cannot delete them."].join(`
2
+ `)}export{o as BACKUP_VAULT_NAME,S as accountHasDisasterRecovery,A as describeBackupVaultExists,E as describeSurvivingBackupVault,v as formatSurvivingVaultWarning,y as updateBackupGlobalSettings};
@@ -28,7 +28,7 @@ export declare function ensureOrganisationalUnitsExist(client: OrganizationsClie
28
28
  export declare function buildAccountToOUMap(tree: OUTree, ouMap: OUMap, prefix?: string): Record<string, string>;
29
29
  /**
30
30
  * Place accounts into the correct organisational units.
31
- * Skips accounts with environment "root" and accounts already in the target OU.
31
+ * Skips organisation-tier accounts (via accountTier) and accounts already in the target OU.
32
32
  *
33
33
  * When `accountToOU` is provided, looks up the target OU by account name
34
34
  * (lowercase) instead of by environment. This supports tree-based placement
@@ -1 +1 @@
1
- import{CreateOrganizationalUnitCommand as A,ListOrganizationalUnitsForParentCommand as U,ListParentsCommand as L,MoveAccountCommand as P}from"@aws-sdk/client-organizations";import{success as u,failure as d}from"@fjall/generator";import{extractErrorName as y,isOULeaf as I,SDK_TIMEOUT_MS as m,AWS_ERROR_NAMES as N}from"./types.js";import{getErrorMessage as g}from"@fjall/util";async function C(r,a){const t=await r.send(new U({ParentId:a}),{abortSignal:AbortSignal.timeout(m)});let e=t.OrganizationalUnits??[],n=t.NextToken;for(;n;){const o=await r.send(new U({ParentId:a,NextToken:n}),{abortSignal:AbortSignal.timeout(m)});e=e.concat(o.OrganizationalUnits??[]),n=o.NextToken}return e}async function x(r,a){try{return(await r.send(new L({ChildId:a}),{abortSignal:AbortSignal.timeout(m)})).Parents?.[0]?.Id}catch(t){if(y(t)===N.CHILD_NOT_FOUND)return;throw t}}async function b(r,a,t,e){const n=e.find(o=>o.Name===t);if(n?.Id)return u(n.Id);try{const s=(await r.send(new A({Name:t,ParentId:a}),{abortSignal:AbortSignal.timeout(m)})).OrganizationalUnit;return s?.Id?u(s.Id):d(new Error(`OU "${t}" was created but has no ID`))}catch(o){return d(new Error(`Failed to create OU "${t}": ${g(o)}`))}}function S(r){return r.charAt(0).toUpperCase()+r.slice(1)}async function E(r,a,t){const e={};if(t.length===0)return u(e);const n=await C(r,a);for(const o of t){const s=S(o),i=await b(r,a,s,n);if(!i.success)return d(i.error);e[o.toLowerCase()]=i.data,n.push({Id:i.data,Name:s})}return u(e)}async function p(r,a,t,e,n,o){const s=await C(r,a);for(const[i,c]of Object.entries(t)){const f=S(i),w=await b(r,a,f,s);if(!w.success)return d(w.error);const O=w.data,h=n?`${n}.${i.toLowerCase()}`:i.toLowerCase();if(e[h]=O,n){const l=i.toLowerCase();o.has(l)||(e[l]=O)}if(s.push({Id:O,Name:f}),!I(c)){const l=await p(r,O,c,e,h,o);if(!l.success)return l}}return u(void 0)}async function k(r,a,t){try{if(Array.isArray(t))return await E(r,a,t);const e={},n=new Set(Object.keys(t).map(s=>s.toLowerCase())),o=await p(r,a,t,e,"",n);return o.success?u(e):d(o.error)}catch(e){return d(new Error(`Failed to ensure OUs exist: ${g(e)}`))}}function T(r,a,t=""){const e={};for(const[n,o]of Object.entries(r)){const s=t?`${t}.${n.toLowerCase()}`:n.toLowerCase(),i=a[s];if(I(o)){if(i)for(const c of o)e[c.toLowerCase()]=i}else Object.assign(e,T(o,a,s))}return e}async function v(r,a,t,e){try{if(t.length===0)return u({moved:0,alreadyPlaced:0});let n=0,o=0;for(const s of t){if(s.environment==="root")continue;const i=e?e[s.name.toLowerCase()]:a[s.environment.toLowerCase()];if(!i)continue;const c=await x(r,s.id);if(c){if(c===i){o++;continue}try{await r.send(new P({AccountId:s.id,SourceParentId:c,DestinationParentId:i}),{abortSignal:AbortSignal.timeout(m)}),n++}catch(f){if(y(f)===N.ACCOUNT_NOT_FOUND)continue;throw f}}}return u({moved:n,alreadyPlaced:o})}catch(n){return d(new Error(`Failed to place accounts in OUs: ${g(n)}`))}}export{T as buildAccountToOUMap,k as ensureOrganisationalUnitsExist,v as placeAccountsInOUs};
1
+ import{CreateOrganizationalUnitCommand as A,ListOrganizationalUnitsForParentCommand as U,ListParentsCommand as L,MoveAccountCommand as P}from"@aws-sdk/client-organizations";import{success as u,failure as d}from"@fjall/generator";import{extractErrorName as y,isOULeaf as I,SDK_TIMEOUT_MS as w,AWS_ERROR_NAMES as N}from"./types.js";import{accountTier as x,getErrorMessage as g}from"@fjall/util";async function C(r,a){const t=await r.send(new U({ParentId:a}),{abortSignal:AbortSignal.timeout(w)});let e=t.OrganizationalUnits??[],n=t.NextToken;for(;n;){const o=await r.send(new U({ParentId:a,NextToken:n}),{abortSignal:AbortSignal.timeout(w)});e=e.concat(o.OrganizationalUnits??[]),n=o.NextToken}return e}async function E(r,a){try{return(await r.send(new L({ChildId:a}),{abortSignal:AbortSignal.timeout(w)})).Parents?.[0]?.Id}catch(t){if(y(t)===N.CHILD_NOT_FOUND)return;throw t}}async function b(r,a,t,e){const n=e.find(o=>o.Name===t);if(n?.Id)return u(n.Id);try{const s=(await r.send(new A({Name:t,ParentId:a}),{abortSignal:AbortSignal.timeout(w)})).OrganizationalUnit;return s?.Id?u(s.Id):d(new Error(`OU "${t}" was created but has no ID`))}catch(o){return d(new Error(`Failed to create OU "${t}": ${g(o)}`))}}function S(r){return r.charAt(0).toUpperCase()+r.slice(1)}async function T(r,a,t){const e={};if(t.length===0)return u(e);const n=await C(r,a);for(const o of t){const s=S(o),i=await b(r,a,s,n);if(!i.success)return d(i.error);e[o.toLowerCase()]=i.data,n.push({Id:i.data,Name:s})}return u(e)}async function p(r,a,t,e,n,o){const s=await C(r,a);for(const[i,c]of Object.entries(t)){const f=S(i),m=await b(r,a,f,s);if(!m.success)return d(m.error);const O=m.data,h=n?`${n}.${i.toLowerCase()}`:i.toLowerCase();if(e[h]=O,n){const l=i.toLowerCase();o.has(l)||(e[l]=O)}if(s.push({Id:O,Name:f}),!I(c)){const l=await p(r,O,c,e,h,o);if(!l.success)return l}}return u(void 0)}async function z(r,a,t){try{if(Array.isArray(t))return await T(r,a,t);const e={},n=new Set(Object.keys(t).map(s=>s.toLowerCase())),o=await p(r,a,t,e,"",n);return o.success?u(e):d(o.error)}catch(e){return d(new Error(`Failed to ensure OUs exist: ${g(e)}`))}}function $(r,a,t=""){const e={};for(const[n,o]of Object.entries(r)){const s=t?`${t}.${n.toLowerCase()}`:n.toLowerCase(),i=a[s];if(I(o)){if(i)for(const c of o)e[c.toLowerCase()]=i}else Object.assign(e,$(o,a,s))}return e}async function M(r,a,t,e){try{if(t.length===0)return u({moved:0,alreadyPlaced:0});let n=0,o=0;for(const s of t){if(x(s)==="organisation")continue;const i=e?e[s.name.toLowerCase()]:a[s.environment.toLowerCase()];if(!i)continue;const c=await E(r,s.id);if(c){if(c===i){o++;continue}try{await r.send(new P({AccountId:s.id,SourceParentId:c,DestinationParentId:i}),{abortSignal:AbortSignal.timeout(w)}),n++}catch(f){if(y(f)===N.ACCOUNT_NOT_FOUND)continue;throw f}}}return u({moved:n,alreadyPlaced:o})}catch(n){return d(new Error(`Failed to place accounts in OUs: ${g(n)}`))}}export{$ as buildAccountToOUMap,z as ensureOrganisationalUnitsExist,M as placeAccountsInOUs};
@@ -1 +1 @@
1
- import{EnablePolicyTypeCommand as f,ListRootsCommand as u,PolicyType as a}from"@aws-sdk/client-organizations";import{success as d,failure as P}from"@fjall/generator";import{getErrorMessage as b}from"@fjall/util";import{extractErrorName as w,SDK_TIMEOUT_MS as y}from"./types.js";const E=[a.SERVICE_CONTROL_POLICY,a.TAG_POLICY,a.BACKUP_POLICY,a.AISERVICES_OPT_OUT_POLICY],_=2e3,L=6e4;async function g(i,t,e={}){try{for(const o of E)try{await i.send(new f({RootId:t,PolicyType:o}),{abortSignal:AbortSignal.timeout(y)})}catch(r){if(w(r)==="PolicyTypeAlreadyEnabledException")continue;throw r}return await I(i,t,e),d(void 0)}catch(o){return P(new Error(`Failed to enable policy types: ${b(o)}`))}}async function I(i,t,e){const o=e.pollIntervalMs??_,r=e.pollTimeoutMs??L,s=new Set(E),m=Date.now();for(;;){const T=(await i.send(new u({}),{abortSignal:AbortSignal.timeout(y)})).Roots?.find(n=>n.Id===t),l=new Set;for(const n of T?.PolicyTypes??[])n.Status==="ENABLED"&&n.Type&&l.add(n.Type);const c=[...s].filter(n=>!l.has(n));if(c.length===0)return;const p=Date.now()-m;if(p>=r)throw new Error(`Policy types still PENDING_ENABLE after ${p}ms: ${c.join(", ")}`);if(o>0&&(await S(o,e.abortSignal),e.abortSignal?.aborted===!0))throw new Error("Aborted while waiting for policy types to enable")}}function S(i,t){return t?.aborted===!0?Promise.resolve():new Promise(e=>{const o=setTimeout(()=>{t?.removeEventListener("abort",r),e()},i),r=()=>{clearTimeout(o),e()};t?.addEventListener("abort",r,{once:!0})})}export{g as enablePolicyTypes};
1
+ import{EnablePolicyTypeCommand as m,ListRootsCommand as P,PolicyType as i}from"@aws-sdk/client-organizations";import{success as _,failure as d}from"@fjall/generator";import{getErrorMessage as w}from"@fjall/util";import{extractErrorName as L,SDK_TIMEOUT_MS as y}from"./types.js";import{sleepAbortable as b}from"../../util/sleepAbortable.js";const E=[i.SERVICE_CONTROL_POLICY,i.TAG_POLICY,i.BACKUP_POLICY,i.AISERVICES_OPT_OUT_POLICY],u=2e3,I=6e4;async function h(r,n,o={}){try{for(const t of E)try{await r.send(new m({RootId:n,PolicyType:t}),{abortSignal:AbortSignal.timeout(y)})}catch(a){if(L(a)==="PolicyTypeAlreadyEnabledException")continue;throw a}return await S(r,n,o),_(void 0)}catch(t){return d(new Error(`Failed to enable policy types: ${w(t)}`))}}async function S(r,n,o){const t=o.pollIntervalMs??u,a=o.pollTimeoutMs??I,s=new Set(E),T=Date.now();for(;;){const f=(await r.send(new P({}),{abortSignal:AbortSignal.timeout(y)})).Roots?.find(e=>e.Id===n),l=new Set;for(const e of f?.PolicyTypes??[])e.Status==="ENABLED"&&e.Type&&l.add(e.Type);const c=[...s].filter(e=>!l.has(e));if(c.length===0)return;const p=Date.now()-T;if(p>=a)throw new Error(`Policy types still PENDING_ENABLE after ${p}ms: ${c.join(", ")}`);if(t>0&&(await b(t,o.abortSignal),o.abortSignal?.aborted===!0))throw new Error("Aborted while waiting for policy types to enable")}}export{h as enablePolicyTypes};
@@ -1 +1 @@
1
- import{EnableAWSServiceAccessCommand as n}from"@aws-sdk/client-organizations";import{success as s,failure as e}from"@fjall/generator";import{extractErrorName as m,SDK_TIMEOUT_MS as i,AWS_ERROR_NAMES as t}from"./types.js";import{getErrorMessage as o}from"@fjall/util";const u=["account.amazonaws.com","sso.amazonaws.com","ipam.amazonaws.com","ram.amazonaws.com","backup.amazonaws.com","member.org.stacksets.cloudformation.amazonaws.com","guardduty.amazonaws.com","securityhub.amazonaws.com","config.amazonaws.com","inspector2.amazonaws.com","access-analyzer.amazonaws.com"];async function f(c){try{for(const a of u)try{await c.send(new n({ServicePrincipal:a}),{abortSignal:AbortSignal.timeout(i)})}catch(r){if(m(r)===t.ACCESS_DENIED)return e(new Error(`Access denied when enabling service access for ${a}. Ensure your credentials have organizations:EnableAWSServiceAccess permission.`));throw new Error(`Service principal ${a}: ${o(r)}`,{cause:r})}return s(void 0)}catch(a){return e(new Error(`Failed to enable service access: ${o(a)}`))}}export{f as enableServiceAccess};
1
+ import{EnableAWSServiceAccessCommand as n}from"@aws-sdk/client-organizations";import{success as s,failure as e}from"@fjall/generator";import{extractErrorName as m,SDK_TIMEOUT_MS as i,AWS_ERROR_NAMES as t}from"./types.js";import{getErrorMessage as o}from"@fjall/util";const u=["account.amazonaws.com","sso.amazonaws.com","ipam.amazonaws.com","ram.amazonaws.com","backup.amazonaws.com","member.org.stacksets.cloudformation.amazonaws.com","guardduty.amazonaws.com","securityhub.amazonaws.com","config.amazonaws.com","inspector2.amazonaws.com","access-analyzer.amazonaws.com","cloudtrail.amazonaws.com"];async function z(c){try{for(const a of u)try{await c.send(new n({ServicePrincipal:a}),{abortSignal:AbortSignal.timeout(i)})}catch(r){if(m(r)===t.ACCESS_DENIED)return e(new Error(`Access denied when enabling service access for ${a}. Ensure your credentials have organizations:EnableAWSServiceAccess permission.`));throw new Error(`Service principal ${a}: ${o(r)}`,{cause:r})}return s(void 0)}catch(a){return e(new Error(`Failed to enable service access: ${o(a)}`))}}export{z as enableServiceAccess};
@@ -24,6 +24,12 @@ export interface AccountPlacementResult {
24
24
  export interface AccountInfo {
25
25
  id: string;
26
26
  name: string;
27
+ /**
28
+ * OU placement key (the bucket name in the ACCOUNTS config — `root`,
29
+ * `platform`, or a workload stage), NOT the nullable account STAGE axis.
30
+ * Callers coalesce stage-less accounts to their tier before building this
31
+ * (see OrganisationSetupService), so it is always present here.
32
+ */
27
33
  environment: string;
28
34
  }
29
35
  export interface IdentityCentreStatus {
@@ -11,3 +11,5 @@
11
11
  export { DeploymentEventSchema, DEPLOYMENT_EVENT_TYPES, DEPLOYMENT_EVENT_RESOURCE_CATEGORIES, CASCADE_PHASES, CASCADE_ACCOUNT_STATUSES } from "../types/deploymentEventSchema.js";
12
12
  export type { DeploymentEvent, DeploymentEventType, DeploymentEventResourceCategory, DeploymentEventCascadePhase, DeploymentEventCascadeAccountStatus } from "../types/deploymentEventSchema.js";
13
13
  export { toCascadePhase } from "../types/deploymentEventSchema.js";
14
+ export { TRAIL_MIGRATION_PHASES, TRAIL_MIGRATION_STATUSES } from "../types/events.js";
15
+ export type { TrailMigrationPhase, TrailMigrationStatus, TrailMigrationPhaseEvent } from "../types/events.js";
@@ -1 +1 @@
1
- import{DeploymentEventSchema as C,DEPLOYMENT_EVENT_TYPES as T,DEPLOYMENT_EVENT_RESOURCE_CATEGORIES as e,CASCADE_PHASES as A,CASCADE_ACCOUNT_STATUSES as _}from"../types/deploymentEventSchema.js";import{toCascadePhase as t}from"../types/deploymentEventSchema.js";export{_ as CASCADE_ACCOUNT_STATUSES,A as CASCADE_PHASES,e as DEPLOYMENT_EVENT_RESOURCE_CATEGORIES,T as DEPLOYMENT_EVENT_TYPES,C as DeploymentEventSchema,t as toCascadePhase};
1
+ import{DeploymentEventSchema as T,DEPLOYMENT_EVENT_TYPES as A,DEPLOYMENT_EVENT_RESOURCE_CATEGORIES as _,CASCADE_PHASES as e,CASCADE_ACCOUNT_STATUSES as C}from"../types/deploymentEventSchema.js";import{toCascadePhase as I}from"../types/deploymentEventSchema.js";import{TRAIL_MIGRATION_PHASES as O,TRAIL_MIGRATION_STATUSES as R}from"../types/events.js";export{C as CASCADE_ACCOUNT_STATUSES,e as CASCADE_PHASES,_ as DEPLOYMENT_EVENT_RESOURCE_CATEGORIES,A as DEPLOYMENT_EVENT_TYPES,T as DeploymentEventSchema,O as TRAIL_MIGRATION_PHASES,R as TRAIL_MIGRATION_STATUSES,I as toCascadePhase};
@@ -13,7 +13,7 @@
13
13
  */
14
14
  export { DeploymentEventSchema, DEPLOYMENT_EVENT_TYPES, DEPLOYMENT_EVENT_RESOURCE_CATEGORIES, CASCADE_PHASES, CASCADE_ACCOUNT_STATUSES } from "./types/index.js";
15
15
  export type { DeploymentEvent, DeploymentEventType, DeploymentEventResourceCategory, DeploymentEventCascadePhase, DeploymentEventCascadeAccountStatus } from "./types/index.js";
16
- export type { AwsCredentials, DeployIdentity, DeployCallbacks, StepCompleteStatus, ProgressEvent, ProgressEventType, ResourceEvent, AwsAuthResult, CascadeDeploymentResult, CascadePhase, BuildPushStartEvent, BuildPushProgressEvent, BuildPushCompleteEvent, TaskDefRegisteredEvent, ECSCompleteEvent, MigrationsStartEvent, MigrationsCompleteEvent, ApiClientInterface, EntitlementsData, DeployParams, DeployOptions, DeploymentType, DeployResult, DestroyParams, DestroyOptions, DestroyResult, OrgConfig, ProviderAccount, SSOSession, Entitlements } from "./types/index.js";
16
+ export type { AwsCredentials, DeployIdentity, DeployCallbacks, StepCompleteStatus, ProgressEvent, ProgressEventType, ResourceEvent, AwsAuthResult, CascadeDeploymentResult, CascadePhase, BuildPushStartEvent, BuildPushProgressEvent, BuildPushCompleteEvent, TaskDefRegisteredEvent, ECSCompleteEvent, MigrationsStartEvent, MigrationsCompleteEvent, TrailMigrationPhase, TrailMigrationStatus, TrailMigrationPhaseEvent, ApiClientInterface, EntitlementsData, DeployParams, DeployOptions, DeploymentType, DeployResult, DestroyParams, DestroyOptions, DestroyResult, OrgConfig, ProviderAccount, SSOSession, Entitlements } from "./types/index.js";
17
17
  export type { AwsProvider, AwsProviderCredentials, AwsSdkClientConstructor } from "./aws/index.js";
18
18
  export { SimpleAwsProvider } from "./aws/index.js";
19
19
  export { ensureOrganisationExists, describeOrganisation, enablePolicyTypes, enableServiceAccess, enableRamSharing, activateTrustedAccess, enableIpamDelegatedAdmin, updateBackupGlobalSettings, listAccounts, findAccount, createAccount, ensureOrganisationalUnitsExist, placeAccountsInOUs, buildAccountToOUMap, activateCostAllocationTags, checkIdentityCentreStatus, extractErrorName, isOULeaf, registerSecurityDelegates, SECURITY_SERVICE_PRINCIPALS } from "./aws/index.js";
@@ -49,7 +49,11 @@ export { projectScalarSummary, projectAccountRows } from "./orchestration/index.
49
49
  export type { CascadeOutcomeResult, CascadeMemberOutcome, CascadePlatformOutcome, CascadeLedger, CascadeAccountRow, CascadePlatformRow, CascadeAccountProjection } from "./orchestration/index.js";
50
50
  export { reconcileProviderAccounts, mergeReconciledProviderAccounts } from "./orchestration/index.js";
51
51
  export type { ReconcileResult } from "./orchestration/index.js";
52
- export { parseAccountsConfiguration, flattenAccountsToEnvironments, extractAllAccountNames, accountsConfigToOUTree, isStringArray, isAccountsConfig } from "./orchestration/index.js";
52
+ export { decideNextTransition, reconcileTrailMigration, decommissionMemberTrailStorage, ORG_TRAIL_BUCKET_OUTPUT_KEY, TRAIL_BUCKET_OUTPUT_KEY, TRAIL_KEY_ARN_OUTPUT_KEY } from "./orchestration/index.js";
53
+ export type { MemberTrailFacts, TrailMigrationTransition, TrailMigrationOutcome, DecommissionClients, DecommissionInput, DecommissionOutcome } from "./orchestration/index.js";
54
+ export { verifyOrgTrailDelivery } from "./aws/cloudtrail/orgTrailDelivery.js";
55
+ export type { OrgTrailDeliveryReport, OrgTrailAccountDelivery } from "./aws/cloudtrail/orgTrailDelivery.js";
56
+ export { parseAccountsConfiguration, flattenAccountsToEnvironments, extractAllAccountNames, accountsConfigToOUTree, isStringArray, isAccountsConfig, isOuOnlyAccountBucket, OU_ONLY_ACCOUNT_BUCKETS } from "./orchestration/index.js";
53
57
  export type { AccountsConfig } from "./orchestration/index.js";
54
58
  export { runOpenNextBuild } from "./orchestration/index.js";
55
59
  export { runOrganisationSetup } from "./orchestration/index.js";
package/dist/src/index.js CHANGED
@@ -1 +1 @@
1
- import{DeploymentEventSchema as t,DEPLOYMENT_EVENT_TYPES as o,DEPLOYMENT_EVENT_RESOURCE_CATEGORIES as a,CASCADE_PHASES as i,CASCADE_ACCOUNT_STATUSES as s}from"./types/index.js";import{SimpleAwsProvider as c}from"./aws/index.js";import{ensureOrganisationExists as p,describeOrganisation as S,enablePolicyTypes as l,enableServiceAccess as A,enableRamSharing as u,activateTrustedAccess as m,enableIpamDelegatedAdmin as T,updateBackupGlobalSettings as P,listAccounts as O,findAccount as d,createAccount as R,ensureOrganisationalUnitsExist as _,placeAccountsInOUs as f,buildAccountToOUMap as C,activateCostAllocationTags as g,checkIdentityCentreStatus as N,extractErrorName as x,isOULeaf as D,registerSecurityDelegates as I,SECURITY_SERVICE_PRINCIPALS as L}from"./aws/index.js";import{STEP_IDS as k,STEP_NAMES as y,INFRASTRUCTURE_STEP_NAMES as F,INFRA_STEP_NAME as U}from"./types/index.js";import{ProgressReporter as b,APPLICATION_STACKS as h,ORGANISATION_TYPES as Y,APPLICATION_DEPLOY_ORDER as G,APPLICATION_DESTROY_ORDER as B,OPENNEXT_DEPLOY_ORDER as w,OPENNEXT_DESTROY_ORDER as H,PARALLEL_DEPLOY_GROUPS as K,PARALLEL_DESTROY_GROUPS as V,OPENNEXT_PARALLEL_GROUPS as X,PARALLEL_OPERATION_TYPES as j,isApplicationOperation as q,isOrganisationOperation as z,getParallelDeployGroups as J,getParallelDestroyGroups as Q,getApplicationDeployOrder as W,getApplicationDestroyOrder as Z,getApplicationStackName as $,getOrganisationStackName as ee,isApplicationStack as re,getApplicationStepName as te,getApplicationStepId as oe,toPascalCase as ae,isOpenNextPattern as ie,OPENNEXT_PATTERNS as se,deriveResourcesFromManifestStacks as ne,STACK_NOT_FOUND_PATTERN as ce,STACK_FAILED_STATE_PATTERN as Ee,CDK_NO_STACKS_MATCH as pe,INFRASTRUCTURE_FILENAME as Se,ApplicationError as le,wrapApplicationError as Ae,FjallStateFileSchema as ue,readStateFile as me,writeStateFile as Te,createEmptyState as Pe,deleteStateFile as Oe,updateTemplateHash as de,getStateFilePath as Re,stubCallerIdentity as _e}from"./types/index.js";import{detectPattern as Ce}from"./types/index.js";import{detectPayloadPattern as Ne}from"./types/index.js";import{detectDatabase as De}from"./types/index.js";import{CloudFormationEventMonitor as Le}from"./aws/index.js";import{CdkService as ke,CdkArgumentBuilder as ye,CdkProcessManager as Fe,CdkEventMonitor as Ue,startStackMonitoring as Me,DEFAULT_DEPLOY_TIMEOUT_MS as be,isCdkError as he,formatInfrastructureError as Ye,getStructuralHint as Ge,getSourceContext as Be,hasCdkDifferences as we,parseDiffOutput as He,CloudFormationService as Ke,CloudFormationError as Ve,EcsService as Xe,EcsError as je,EcsServiceResolver as qe,TemplateHashService as ze,TemplateHashError as Je,CdkContextBuilder as Qe,emitProgress as We,PROGRESS_MESSAGES as Ze,parseBuildPhase as $e,buildStepContextBuildConfig as er,convertCloudFormationOutputsToRecord as rr,ApplicationStackService as tr}from"./services/index.js";import{CdkError as ar}from"./types/errors/index.js";import{BaseServiceError as sr,ValidationError as nr,AuthError as cr,AwsError as Er,DeploymentError as pr,NetworkError as Sr,FileSystemError as lr,ConfigError as Ar,toServiceError as ur}from"./types/errors/index.js";import{filterDangerousEnvVars as Tr,maskSensitiveOutput as Pr,parseShellArgs as Or,sleep as dr}from"@fjall/util";import{hasDockerfile as _r}from"./util/dockerfileDetection.js";import{createSequencedCallbacks as Cr}from"./util/sequencedCallbacks.js";import{fileExists as Nr}from"@fjall/util/fsHelpers";import{success as Dr,failure as Ir,isSuccess as Lr,isFailure as vr}from"@fjall/generator";import{deploy as yr}from"./orchestration/index.js";import{destroy as Ur}from"./orchestration/index.js";import{partitionAccounts as br}from"./orchestration/index.js";import{buildRegionList as Yr,buildAccountRegionPairs as Gr}from"./orchestration/index.js";import{projectScalarSummary as wr,projectAccountRows as Hr}from"./orchestration/index.js";import{reconcileProviderAccounts as Vr,mergeReconciledProviderAccounts as Xr}from"./orchestration/index.js";import{parseAccountsConfiguration as qr,flattenAccountsToEnvironments as zr,extractAllAccountNames as Jr,accountsConfigToOUTree as Qr,isStringArray as Wr,isAccountsConfig as Zr}from"./orchestration/index.js";import{runOpenNextBuild as et}from"./orchestration/index.js";import{runOrganisationSetup as tt}from"./orchestration/index.js";import{FrameworkRegistry as at}from"./orchestration/index.js";import{openNextBuilder as st,dockerBuilder as nt}from"./orchestration/index.js";import{StepRegistry as Et,getDestroyStepId as pt}from"./steps/index.js";export{G as APPLICATION_DEPLOY_ORDER,B as APPLICATION_DESTROY_ORDER,h as APPLICATION_STACKS,le as ApplicationError,tr as ApplicationStackService,cr as AuthError,Er as AwsError,sr as BaseServiceError,s as CASCADE_ACCOUNT_STATUSES,i as CASCADE_PHASES,pe as CDK_NO_STACKS_MATCH,ye as CdkArgumentBuilder,Qe as CdkContextBuilder,ar as CdkError,Ue as CdkEventMonitor,Fe as CdkProcessManager,ke as CdkService,Ve as CloudFormationError,Le as CloudFormationEventMonitor,Ke as CloudFormationService,Ar as ConfigError,be as DEFAULT_DEPLOY_TIMEOUT_MS,a as DEPLOYMENT_EVENT_RESOURCE_CATEGORIES,o as DEPLOYMENT_EVENT_TYPES,pr as DeploymentError,t as DeploymentEventSchema,je as EcsError,Xe as EcsService,qe as EcsServiceResolver,lr as FileSystemError,ue as FjallStateFileSchema,at as FrameworkRegistry,Se as INFRASTRUCTURE_FILENAME,F as INFRASTRUCTURE_STEP_NAMES,U as INFRA_STEP_NAME,Sr as NetworkError,w as OPENNEXT_DEPLOY_ORDER,H as OPENNEXT_DESTROY_ORDER,X as OPENNEXT_PARALLEL_GROUPS,se as OPENNEXT_PATTERNS,Y as ORGANISATION_TYPES,K as PARALLEL_DEPLOY_GROUPS,V as PARALLEL_DESTROY_GROUPS,j as PARALLEL_OPERATION_TYPES,Ze as PROGRESS_MESSAGES,b as ProgressReporter,L as SECURITY_SERVICE_PRINCIPALS,Ee as STACK_FAILED_STATE_PATTERN,ce as STACK_NOT_FOUND_PATTERN,k as STEP_IDS,y as STEP_NAMES,c as SimpleAwsProvider,Et as StepRegistry,Je as TemplateHashError,ze as TemplateHashService,nr as ValidationError,Qr as accountsConfigToOUTree,g as activateCostAllocationTags,m as activateTrustedAccess,Gr as buildAccountRegionPairs,C as buildAccountToOUMap,Yr as buildRegionList,er as buildStepContextBuildConfig,N as checkIdentityCentreStatus,rr as convertCloudFormationOutputsToRecord,R as createAccount,Pe as createEmptyState,Cr as createSequencedCallbacks,Oe as deleteStateFile,yr as deploy,ne as deriveResourcesFromManifestStacks,S as describeOrganisation,Ur as destroy,De as detectDatabase,Ce as detectPattern,Ne as detectPayloadPattern,nt as dockerBuilder,We as emitProgress,T as enableIpamDelegatedAdmin,l as enablePolicyTypes,u as enableRamSharing,A as enableServiceAccess,p as ensureOrganisationExists,_ as ensureOrganisationalUnitsExist,Jr as extractAllAccountNames,x as extractErrorName,Ir as failure,Nr as fileExists,Tr as filterDangerousEnvVars,d as findAccount,zr as flattenAccountsToEnvironments,Ye as formatInfrastructureError,W as getApplicationDeployOrder,Z as getApplicationDestroyOrder,$ as getApplicationStackName,oe as getApplicationStepId,te as getApplicationStepName,pt as getDestroyStepId,ee as getOrganisationStackName,J as getParallelDeployGroups,Q as getParallelDestroyGroups,Be as getSourceContext,Re as getStateFilePath,Ge as getStructuralHint,we as hasCdkDifferences,_r as hasDockerfile,Zr as isAccountsConfig,q as isApplicationOperation,re as isApplicationStack,he as isCdkError,vr as isFailure,D as isOULeaf,ie as isOpenNextPattern,z as isOrganisationOperation,Wr as isStringArray,Lr as isSuccess,O as listAccounts,Pr as maskSensitiveOutput,Xr as mergeReconciledProviderAccounts,st as openNextBuilder,qr as parseAccountsConfiguration,$e as parseBuildPhase,He as parseDiffOutput,Or as parseShellArgs,br as partitionAccounts,f as placeAccountsInOUs,Hr as projectAccountRows,wr as projectScalarSummary,me as readStateFile,Vr as reconcileProviderAccounts,I as registerSecurityDelegates,et as runOpenNextBuild,tt as runOrganisationSetup,dr as sleep,Me as startStackMonitoring,_e as stubCallerIdentity,Dr as success,ae as toPascalCase,ur as toServiceError,P as updateBackupGlobalSettings,de as updateTemplateHash,Ae as wrapApplicationError,Te as writeStateFile};
1
+ import{DeploymentEventSchema as t,DEPLOYMENT_EVENT_TYPES as o,DEPLOYMENT_EVENT_RESOURCE_CATEGORIES as i,CASCADE_PHASES as a,CASCADE_ACCOUNT_STATUSES as n}from"./types/index.js";import{SimpleAwsProvider as s}from"./aws/index.js";import{ensureOrganisationExists as p,describeOrganisation as l,enablePolicyTypes as S,enableServiceAccess as A,enableRamSharing as T,activateTrustedAccess as u,enableIpamDelegatedAdmin as m,updateBackupGlobalSettings as O,listAccounts as _,findAccount as P,createAccount as d,ensureOrganisationalUnitsExist as R,placeAccountsInOUs as C,buildAccountToOUMap as f,activateCostAllocationTags as N,checkIdentityCentreStatus as g,extractErrorName as x,isOULeaf as D,registerSecurityDelegates as I,SECURITY_SERVICE_PRINCIPALS as L}from"./aws/index.js";import{STEP_IDS as v,STEP_NAMES as y,INFRASTRUCTURE_STEP_NAMES as k,INFRA_STEP_NAME as F}from"./types/index.js";import{ProgressReporter as Y,APPLICATION_STACKS as b,ORGANISATION_TYPES as h,APPLICATION_DEPLOY_ORDER as B,APPLICATION_DESTROY_ORDER as K,OPENNEXT_DEPLOY_ORDER as G,OPENNEXT_DESTROY_ORDER as w,PARALLEL_DEPLOY_GROUPS as H,PARALLEL_DESTROY_GROUPS as V,OPENNEXT_PARALLEL_GROUPS as X,PARALLEL_OPERATION_TYPES as j,isApplicationOperation as q,isOrganisationOperation as z,getParallelDeployGroups as J,getParallelDestroyGroups as Q,getApplicationDeployOrder as W,getApplicationDestroyOrder as Z,getApplicationStackName as $,getOrganisationStackName as ee,isApplicationStack as re,getApplicationStepName as te,getApplicationStepId as oe,toPascalCase as ie,isOpenNextPattern as ae,OPENNEXT_PATTERNS as ne,deriveResourcesFromManifestStacks as ce,STACK_NOT_FOUND_PATTERN as se,STACK_FAILED_STATE_PATTERN as Ee,CDK_NO_STACKS_MATCH as pe,INFRASTRUCTURE_FILENAME as le,ApplicationError as Se,wrapApplicationError as Ae,FjallStateFileSchema as Te,readStateFile as ue,writeStateFile as me,createEmptyState as Oe,deleteStateFile as _e,updateTemplateHash as Pe,getStateFilePath as de,stubCallerIdentity as Re}from"./types/index.js";import{detectPattern as fe}from"./types/index.js";import{detectPayloadPattern as ge}from"./types/index.js";import{detectDatabase as De}from"./types/index.js";import{CloudFormationEventMonitor as Le}from"./aws/index.js";import{CdkService as ve,CdkArgumentBuilder as ye,CdkProcessManager as ke,CdkEventMonitor as Fe,startStackMonitoring as Me,DEFAULT_DEPLOY_TIMEOUT_MS as Ye,isCdkError as be,formatInfrastructureError as he,getStructuralHint as Be,getSourceContext as Ke,hasCdkDifferences as Ge,parseDiffOutput as we,CloudFormationService as He,CloudFormationError as Ve,EcsService as Xe,EcsError as je,EcsServiceResolver as qe,TemplateHashService as ze,TemplateHashError as Je,CdkContextBuilder as Qe,emitProgress as We,PROGRESS_MESSAGES as Ze,parseBuildPhase as $e,buildStepContextBuildConfig as er,convertCloudFormationOutputsToRecord as rr,ApplicationStackService as tr}from"./services/index.js";import{CdkError as ir}from"./types/errors/index.js";import{BaseServiceError as nr,ValidationError as cr,AuthError as sr,AwsError as Er,DeploymentError as pr,NetworkError as lr,FileSystemError as Sr,ConfigError as Ar,toServiceError as Tr}from"./types/errors/index.js";import{filterDangerousEnvVars as mr,maskSensitiveOutput as Or,parseShellArgs as _r,sleep as Pr}from"@fjall/util";import{hasDockerfile as Rr}from"./util/dockerfileDetection.js";import{createSequencedCallbacks as fr}from"./util/sequencedCallbacks.js";import{fileExists as gr}from"@fjall/util/fsHelpers";import{success as Dr,failure as Ir,isSuccess as Lr,isFailure as Ur}from"@fjall/generator";import{deploy as yr}from"./orchestration/index.js";import{destroy as Fr}from"./orchestration/index.js";import{partitionAccounts as Yr}from"./orchestration/index.js";import{buildRegionList as hr,buildAccountRegionPairs as Br}from"./orchestration/index.js";import{projectScalarSummary as Gr,projectAccountRows as wr}from"./orchestration/index.js";import{reconcileProviderAccounts as Vr,mergeReconciledProviderAccounts as Xr}from"./orchestration/index.js";import{decideNextTransition as qr,reconcileTrailMigration as zr,decommissionMemberTrailStorage as Jr,ORG_TRAIL_BUCKET_OUTPUT_KEY as Qr,TRAIL_BUCKET_OUTPUT_KEY as Wr,TRAIL_KEY_ARN_OUTPUT_KEY as Zr}from"./orchestration/index.js";import{verifyOrgTrailDelivery as et}from"./aws/cloudtrail/orgTrailDelivery.js";import{parseAccountsConfiguration as tt,flattenAccountsToEnvironments as ot,extractAllAccountNames as it,accountsConfigToOUTree as at,isStringArray as nt,isAccountsConfig as ct,isOuOnlyAccountBucket as st,OU_ONLY_ACCOUNT_BUCKETS as Et}from"./orchestration/index.js";import{runOpenNextBuild as lt}from"./orchestration/index.js";import{runOrganisationSetup as At}from"./orchestration/index.js";import{FrameworkRegistry as ut}from"./orchestration/index.js";import{openNextBuilder as Ot,dockerBuilder as _t}from"./orchestration/index.js";import{StepRegistry as dt,getDestroyStepId as Rt}from"./steps/index.js";export{B as APPLICATION_DEPLOY_ORDER,K as APPLICATION_DESTROY_ORDER,b as APPLICATION_STACKS,Se as ApplicationError,tr as ApplicationStackService,sr as AuthError,Er as AwsError,nr as BaseServiceError,n as CASCADE_ACCOUNT_STATUSES,a as CASCADE_PHASES,pe as CDK_NO_STACKS_MATCH,ye as CdkArgumentBuilder,Qe as CdkContextBuilder,ir as CdkError,Fe as CdkEventMonitor,ke as CdkProcessManager,ve as CdkService,Ve as CloudFormationError,Le as CloudFormationEventMonitor,He as CloudFormationService,Ar as ConfigError,Ye as DEFAULT_DEPLOY_TIMEOUT_MS,i as DEPLOYMENT_EVENT_RESOURCE_CATEGORIES,o as DEPLOYMENT_EVENT_TYPES,pr as DeploymentError,t as DeploymentEventSchema,je as EcsError,Xe as EcsService,qe as EcsServiceResolver,Sr as FileSystemError,Te as FjallStateFileSchema,ut as FrameworkRegistry,le as INFRASTRUCTURE_FILENAME,k as INFRASTRUCTURE_STEP_NAMES,F as INFRA_STEP_NAME,lr as NetworkError,G as OPENNEXT_DEPLOY_ORDER,w as OPENNEXT_DESTROY_ORDER,X as OPENNEXT_PARALLEL_GROUPS,ne as OPENNEXT_PATTERNS,h as ORGANISATION_TYPES,Qr as ORG_TRAIL_BUCKET_OUTPUT_KEY,Et as OU_ONLY_ACCOUNT_BUCKETS,H as PARALLEL_DEPLOY_GROUPS,V as PARALLEL_DESTROY_GROUPS,j as PARALLEL_OPERATION_TYPES,Ze as PROGRESS_MESSAGES,Y as ProgressReporter,L as SECURITY_SERVICE_PRINCIPALS,Ee as STACK_FAILED_STATE_PATTERN,se as STACK_NOT_FOUND_PATTERN,v as STEP_IDS,y as STEP_NAMES,s as SimpleAwsProvider,dt as StepRegistry,Wr as TRAIL_BUCKET_OUTPUT_KEY,Zr as TRAIL_KEY_ARN_OUTPUT_KEY,Je as TemplateHashError,ze as TemplateHashService,cr as ValidationError,at as accountsConfigToOUTree,N as activateCostAllocationTags,u as activateTrustedAccess,Br as buildAccountRegionPairs,f as buildAccountToOUMap,hr as buildRegionList,er as buildStepContextBuildConfig,g as checkIdentityCentreStatus,rr as convertCloudFormationOutputsToRecord,d as createAccount,Oe as createEmptyState,fr as createSequencedCallbacks,qr as decideNextTransition,Jr as decommissionMemberTrailStorage,_e as deleteStateFile,yr as deploy,ce as deriveResourcesFromManifestStacks,l as describeOrganisation,Fr as destroy,De as detectDatabase,fe as detectPattern,ge as detectPayloadPattern,_t as dockerBuilder,We as emitProgress,m as enableIpamDelegatedAdmin,S as enablePolicyTypes,T as enableRamSharing,A as enableServiceAccess,p as ensureOrganisationExists,R as ensureOrganisationalUnitsExist,it as extractAllAccountNames,x as extractErrorName,Ir as failure,gr as fileExists,mr as filterDangerousEnvVars,P as findAccount,ot as flattenAccountsToEnvironments,he as formatInfrastructureError,W as getApplicationDeployOrder,Z as getApplicationDestroyOrder,$ as getApplicationStackName,oe as getApplicationStepId,te as getApplicationStepName,Rt as getDestroyStepId,ee as getOrganisationStackName,J as getParallelDeployGroups,Q as getParallelDestroyGroups,Ke as getSourceContext,de as getStateFilePath,Be as getStructuralHint,Ge as hasCdkDifferences,Rr as hasDockerfile,ct as isAccountsConfig,q as isApplicationOperation,re as isApplicationStack,be as isCdkError,Ur as isFailure,D as isOULeaf,ae as isOpenNextPattern,z as isOrganisationOperation,st as isOuOnlyAccountBucket,nt as isStringArray,Lr as isSuccess,_ as listAccounts,Or as maskSensitiveOutput,Xr as mergeReconciledProviderAccounts,Ot as openNextBuilder,tt as parseAccountsConfiguration,$e as parseBuildPhase,we as parseDiffOutput,_r as parseShellArgs,Yr as partitionAccounts,C as placeAccountsInOUs,wr as projectAccountRows,Gr as projectScalarSummary,ue as readStateFile,Vr as reconcileProviderAccounts,zr as reconcileTrailMigration,I as registerSecurityDelegates,lt as runOpenNextBuild,At as runOrganisationSetup,Pr as sleep,Me as startStackMonitoring,Re as stubCallerIdentity,Dr as success,ie as toPascalCase,Tr as toServiceError,O as updateBackupGlobalSettings,Pe as updateTemplateHash,et as verifyOrgTrailDelivery,Ae as wrapApplicationError,me as writeStateFile};
@@ -4,6 +4,17 @@ import type { OUTree } from "../aws/organisations/types.js";
4
4
  export type AccountsConfig = {
5
5
  readonly [key: string]: string | readonly string[] | AccountsConfig;
6
6
  };
7
+ /**
8
+ * Scaffold buckets that are OU structure ONLY — accounts placed here get an
9
+ * organisational unit but no workload stage (ADR
10
+ * 2026-04-07-nested-ou-accounts-structure: environments resolve solely from
11
+ * the five stage leaf keys). Producers mapping leaf keys to wire
12
+ * `environment` values must decode these buckets to a null stage rather than
13
+ * forwarding the key verbatim — the org-config ingress enum rejects them.
14
+ */
15
+ export declare const OU_ONLY_ACCOUNT_BUCKETS: readonly ["security", "suspended"];
16
+ /** Type guard: checks whether an ACCOUNTS leaf key is an OU-only bucket. */
17
+ export declare function isOuOnlyAccountBucket(key: string): boolean;
7
18
  export declare function isStringArray(value: unknown): value is string[];
8
19
  export declare function isAccountsConfig(value: unknown): value is AccountsConfig;
9
20
  /**
@@ -1 +1 @@
1
- import{success as u,failure as a}from"@fjall/generator";import{maskSensitiveOutput as A,getErrorMessage as C}from"@fjall/util";import{ConfigError as f}from"../types/errors/ServiceError.js";const c="infrastructure.ts";function g(n){return Array.isArray(n)&&n.every(t=>typeof t=="string")}function p(n){return typeof n!="object"||n===null?!1:Object.entries(n).every(([t,r])=>typeof t=="string"&&(typeof r=="string"||g(r)||p(r)))}function O(n){const t=[];for(const[r,e]of Object.entries(n))if(typeof e=="string")t.push({accountName:e,environment:r});else if(Array.isArray(e))for(const o of e)t.push({accountName:o,environment:r});else typeof e=="object"&&e!==null&&t.push(...O(e));return t}function d(n){const t=[];for(const r of Object.values(n))typeof r=="string"?t.push(r):Array.isArray(r)?t.push(...r):typeof r=="object"&&r!==null&&t.push(...d(r));return t}function T(n){const t={};for(const[r,e]of Object.entries(n))typeof e=="string"?t[r]=[e]:Array.isArray(e)?t[r]=[...e]:typeof e=="object"&&e!==null&&(t[r]=T(e));return t}async function S(n){try{const t=await import("path"),r=await import("fs"),{pathToFileURL:e}=await import("url"),o=t.join(n,"fjall","organisation",c);if(!r.existsSync(o))return u(null);const{tsImport:l}=await import("tsx/esm/api"),y=e(o).href,s=await l(o,y),m=s.default,i=s.ACCOUNTS||m?.ACCOUNTS;return i?p(i)?u(i):a(new f("ACCOUNTS configuration has invalid structure","ACCOUNTS",o,{providedValue:i})):a(new f(`ACCOUNTS constant not found in organisation/${c}`,"ACCOUNTS",o,{moduleKeys:Object.keys(s),hasDefault:s.default!==void 0}))}catch(t){const r=A(C(t));return a(new f(`Failed to import ACCOUNTS from organisation/${c}: ${r}`,"ACCOUNTS",void 0,{originalError:t}))}}export{T as accountsConfigToOUTree,d as extractAllAccountNames,O as flattenAccountsToEnvironments,p as isAccountsConfig,g as isStringArray,S as parseAccountsConfiguration};
1
+ import{success as f,failure as c}from"@fjall/generator";import{maskSensitiveOutput as C,getErrorMessage as m}from"@fjall/util";import{ConfigError as u}from"../types/errors/ServiceError.js";const a="infrastructure.ts",O=["security","suspended"],g=new Set(O);function x(n){return g.has(n)}function T(n){return Array.isArray(n)&&n.every(t=>typeof t=="string")}function p(n){return typeof n!="object"||n===null?!1:Object.entries(n).every(([t,e])=>typeof t=="string"&&(typeof e=="string"||T(e)||p(e)))}function U(n){const t=[];for(const[e,r]of Object.entries(n))if(typeof r=="string")t.push({accountName:r,environment:e});else if(Array.isArray(r))for(const o of r)t.push({accountName:o,environment:e});else typeof r=="object"&&r!==null&&t.push(...U(r));return t}function N(n){const t=[];for(const e of Object.values(n))typeof e=="string"?t.push(e):Array.isArray(e)?t.push(...e):typeof e=="object"&&e!==null&&t.push(...N(e));return t}function d(n){const t={};for(const[e,r]of Object.entries(n))typeof r=="string"?t[e]=[r]:Array.isArray(r)?t[e]=[...r]:typeof r=="object"&&r!==null&&(t[e]=d(r));return t}async function E(n){try{const t=await import("path"),e=await import("fs"),{pathToFileURL:r}=await import("url"),o=t.join(n,"fjall","organisation",a);if(!e.existsSync(o))return f(null);const{tsImport:l}=await import("tsx/esm/api"),y=r(o).href,s=await l(o,y),A=s.default,i=s.ACCOUNTS||A?.ACCOUNTS;return i?p(i)?f(i):c(new u("ACCOUNTS configuration has invalid structure","ACCOUNTS",o,{providedValue:i})):c(new u(`ACCOUNTS constant not found in organisation/${a}`,"ACCOUNTS",o,{moduleKeys:Object.keys(s),hasDefault:s.default!==void 0}))}catch(t){const e=C(m(t));return c(new u(`Failed to import ACCOUNTS from organisation/${a}: ${e}`,"ACCOUNTS",void 0,{originalError:t}))}}export{O as OU_ONLY_ACCOUNT_BUCKETS,d as accountsConfigToOUTree,N as extractAllAccountNames,U as flattenAccountsToEnvironments,p as isAccountsConfig,x as isOuOnlyAccountBucket,T as isStringArray,E as parseAccountsConfiguration};
@@ -26,5 +26,5 @@ export interface CascadeDestroyAccountResult {
26
26
  export declare function destroyCascadeAccount(params: DestroyParams, services: DeployServices, operation: OrganisationOperation, account: {
27
27
  id: string;
28
28
  name: string;
29
- environment: string;
29
+ environment: string | null;
30
30
  }, deployType: "platform" | "account", region: string, callbacks: DeployCallbacks): Promise<CascadeDestroyAccountResult>;
@@ -1 +1 @@
1
- import{logger as f}from"@fjall/util/logger";import{maskSensitiveOutput as s,getErrorMessage as y}from"@fjall/util";import{stubCallerIdentity as E}from"../types/deployment/index.js";import{CloudFormationClient as I,DescribeStacksCommand as R}from"@aws-sdk/client-cloudformation";import{BackupClient as $}from"@aws-sdk/client-backup";import{accountHasDisasterRecovery as g,describeSurvivingBackupVault as O,formatSurvivingVaultWarning as _}from"../aws/organisations/backup.js";import{NodeHttpHandler as B}from"@smithy/node-http-handler";import{ORGANISATION_TYPES as k,getOrganisationStackName as F}from"../types/operations.js";import{CdkContextBuilder as x}from"../services/supporting/CdkContextBuilder.js";import{buildParamsContext as L,assumeCascadeRole as M,forwardOutput as T}from"./contextHelpers.js";import{cleanupFailedStack as H}from"./stackCleanup.js";import{STACK_NOT_FOUND_PATTERN as K,STACK_FAILED_STATE_PATTERN as V}from"../types/constants.js";async function re(n,o,i,e,c,t,r){const d=Date.now(),a=`${e.name} (${t})`;r.onCascadeAccountStart?.(a,e.id,t,c);const l=await M(o.awsProvider,e.id,t,`fjall-cascade-destroy-${e.name}`);if(!l.success)return r.onCascadeAccountComplete?.(a,!1,s(l.error.message),t),{accountName:e.name,accountId:e.id,region:t,success:!1,duration:Date.now()-d,error:`AssumeRole failed: ${s(l.error.message)}`};const{provider:w,credentials:p}=l.data,S=x.buildDeploymentContext({deployType:c,target:i.target,path:i.path,region:t,accountName:e.name,callerIdentity:E(e.id),...L({orgConfig:n.orgConfig,identity:n.identity})},{verbose:n.options?.verbose},n.orgConfig);r.onCascadeAccountPhaseChange?.(a,"synth",t);const A=await o.cdkService.runCdkSynth(S,T(r));if(!A.success){const u=s(`Synth failed: ${A.error}`);return r.onCascadeAccountComplete?.(a,!1,u,t),{accountName:e.name,accountId:e.id,region:t,success:!1,duration:Date.now()-d,error:u}}r.onCascadeAccountPhaseChange?.(a,"destroy",t);const m=F(c==="platform"?k.PLATFORM:k.ACCOUNT);g(e.environment,n.orgConfig?.disasterRecoveryRegion)&&await q(w.getClient($),t,r);const D=await o.cdkService.runCdkDestroy(S,m,T(r),u=>r.onCascadeAccountResourceProgress?.(a,u,t),w,!0,p);if(!D.success){const u=D.error;if(u.includes(V)){f.warn("cascadeDestroy",`CDK destroy failed on ${m} in failed state, retrying via CloudFormation API`,{region:t,account:e.name});try{await H(m,t,p,void 0,r)}catch(C){const h=`cleanupFailedStack threw for ${m}: ${s(y(C))}`;f.warn("cascadeDestroy",h),r.onLog?.(h,"warn")}const v=await U(m,t,p);if(v.deleted)return r.onCascadeAccountComplete?.(a,!0,void 0,t),{accountName:e.name,accountId:e.id,region:t,success:!0,duration:Date.now()-d};if(v.error){const C=s(v.error);return r.onCascadeAccountComplete?.(a,!1,C,t),{accountName:e.name,accountId:e.id,region:t,success:!1,duration:Date.now()-d,error:C}}const P=s(`Stack ${m} cleanup attempted but stack still exists in ${t}`);return r.onCascadeAccountComplete?.(a,!1,P,t),{accountName:e.name,accountId:e.id,region:t,success:!1,duration:Date.now()-d,error:P}}const N=s(u);return r.onCascadeAccountComplete?.(a,!1,N,t),{accountName:e.name,accountId:e.id,region:t,success:!1,duration:Date.now()-d,error:N}}return r.onCascadeAccountComplete?.(a,!0,void 0,t),{accountName:e.name,accountId:e.id,region:t,success:!0,duration:Date.now()-d}}async function q(n,o,i){try{const e=await O(n,o);if(!e.success){f.debug("cascadeDestroy","Backup-vault survival probe failed",{region:o,error:s(e.error.message)});return}if(e.data===null)return;i.onProgress?.({type:"warning",message:_(e.data),metadata:{source:"backup-vault-survival"}}),f.warn("cascadeDestroy","Backup vault survives destroy",{region:o,vaultName:e.data.vaultName,recoveryPointCount:e.data.recoveryPointCount,lockPermanent:e.data.lockPermanent})}catch(e){f.debug("cascadeDestroy","Backup-vault survival probe threw",{region:o,error:s(y(e))})}}async function U(n,o,i){try{const t=(await new I({region:o,credentials:i,requestHandler:new B({requestTimeout:15e3})}).send(new R({StackName:n}))).Stacks?.[0]?.StackStatus;return!t||t==="DELETE_COMPLETE"?{deleted:!0}:{deleted:!1,error:`Stack still in ${t} after cleanup attempt`}}catch(e){if(e instanceof Error&&e.message?.includes(K))return{deleted:!0};const c=s(y(e));return f.debug("cascadeDestroy","Stack verification failed",{error:c}),{deleted:!1,error:`Stack verification failed: ${c}`}}}export{re as destroyCascadeAccount};
1
+ import{join as R}from"path";import{logger as d}from"@fjall/util/logger";import{maskSensitiveOutput as i,getErrorMessage as y}from"@fjall/util";import{stubCallerIdentity as T}from"../types/deployment/index.js";import{CloudFormationClient as $,DescribeStacksCommand as I}from"@aws-sdk/client-cloudformation";import{BackupClient as O}from"@aws-sdk/client-backup";import{accountHasDisasterRecovery as _,describeSurvivingBackupVault as b,formatSurvivingVaultWarning as x}from"../aws/organisations/backup.js";import{ORGANISATION_TYPES as D,getOrganisationStackName as B}from"../types/operations.js";import{CdkContextBuilder as F}from"../services/supporting/CdkContextBuilder.js";import{buildParamsContext as M,assumeCascadeRole as L,forwardOutput as P}from"./contextHelpers.js";import{cleanupFailedStack as K}from"./stackCleanup.js";import{STACK_NOT_FOUND_PATTERN as V,STACK_FAILED_STATE_PATTERN as j}from"../types/constants.js";async function ae(n,a,o,e,l,t,r){const C=Date.now(),c=`${e.name} (${t})`,m=(s,f=s)=>(r.onCascadeAccountComplete?.(c,!1,f,t),{accountName:e.name,accountId:e.id,region:t,success:!1,duration:Date.now()-C,error:s});r.onCascadeAccountStart?.(c,e.id,t,l);const p=await L(a.awsProvider,e.id,t,`fjall-cascade-destroy-${e.name}`);if(!p.success){const s=i(p.error.message);return m(`AssumeRole failed: ${s}`,s)}const{provider:v,credentials:S}=p.data,h=t.replace(/-/g,""),N=R(o.path,`cdk.out.${e.id}.${h}`),g=F.buildDeploymentContext({deployType:l,target:o.target,path:o.path,assemblyDir:N,region:t,accountName:e.name,callerIdentity:T(e.id),...M({orgConfig:n.orgConfig,identity:n.identity})},{verbose:n.options?.verbose},n.orgConfig);r.onCascadeAccountPhaseChange?.(c,"synth",t);const w=await a.cdkService.runCdkSynth(g,P(r));if(!w.success)return m(i(`Synth failed: ${w.error}`));r.onCascadeAccountPhaseChange?.(c,"destroy",t);const u=B(l==="platform"?D.PLATFORM:D.ACCOUNT);_(e.environment,n.orgConfig?.disasterRecoveryRegion)&&await U(v.getClient(O),t,r);const k=await a.cdkService.runCdkDestroy(g,u,P(r),s=>r.onCascadeAccountResourceProgress?.(c,s,t),v,!0,S);if(!k.success){const s=k.error;if(s.includes(j)){d.warn("cascadeDestroy",`CDK destroy failed on ${u} in failed state, retrying via CloudFormation API`,{region:t,account:e.name});try{await K(u,t,S,void 0,r)}catch(E){const A=`cleanupFailedStack threw for ${u}: ${i(y(E))}`;d.warn("cascadeDestroy",A),r.onLog?.(A,"warn")}const f=await G(u,v.getClient($));return f.deleted?(r.onCascadeAccountComplete?.(c,!0,void 0,t),{accountName:e.name,accountId:e.id,region:t,success:!0,duration:Date.now()-C}):f.error?m(i(f.error)):m(i(`Stack ${u} cleanup attempted but stack still exists in ${t}`))}return m(i(s))}return r.onCascadeAccountComplete?.(c,!0,void 0,t),{accountName:e.name,accountId:e.id,region:t,success:!0,duration:Date.now()-C}}async function U(n,a,o){try{const e=await b(n,a);if(!e.success){d.debug("cascadeDestroy","Backup-vault survival probe failed",{region:a,error:i(e.error.message)});return}if(e.data===null)return;o.onProgress?.({type:"warning",message:x(e.data),metadata:{source:"backup-vault-survival"}}),d.warn("cascadeDestroy","Backup vault survives destroy",{region:a,vaultName:e.data.vaultName,recoveryPointCount:e.data.recoveryPointCount,lockPermanent:e.data.lockPermanent})}catch(e){d.debug("cascadeDestroy","Backup-vault survival probe threw",{region:a,error:i(y(e))})}}async function G(n,a){try{const e=(await a.send(new I({StackName:n}),{abortSignal:AbortSignal.timeout(15e3)})).Stacks?.[0]?.StackStatus;return!e||e==="DELETE_COMPLETE"?{deleted:!0}:{deleted:!1,error:`Stack still in ${e} after cleanup attempt`}}catch(o){if(o instanceof Error&&o.message?.includes(V))return{deleted:!0};const e=i(y(o));return d.debug("cascadeDestroy","Stack verification failed",{error:e}),{deleted:!1,error:`Stack verification failed: ${e}`}}}export{ae as destroyCascadeAccount};
@@ -39,6 +39,22 @@ export declare function buildRegionList(orgConfig: OrgConfig | undefined): strin
39
39
  */
40
40
  export declare function buildAccountRegionPairs(accounts: ProviderAccount[], regions: string[]): AccountRegionPair[];
41
41
  export { buildCascadeRoleArn } from "./contextHelpers.js";
42
+ /** A single pre-flight probe failure: the cascade role could not be assumed. */
43
+ export interface CascadeRoleProbeFailure {
44
+ accountId: string;
45
+ accountName: string;
46
+ error: string;
47
+ }
48
+ /**
49
+ * Pre-flight check that OrganizationAccountAccessRole is assumable in every
50
+ * cascade target, run BEFORE any synth/bootstrap/deploy work. An imported
51
+ * (invite-joined) member never gets the role auto-created, so without this
52
+ * probe a broken role only surfaces mid-cascade — after the org root has
53
+ * already deployed — one account at a time. Read-only: the assumed
54
+ * credentials are discarded. Inherits assumeCascadeRole's retry budget, so
55
+ * role propagation on freshly created accounts does not false-fail.
56
+ */
57
+ export declare function probeCascadeRoles(services: DeployServices, accounts: ProviderAccount[], callbacks: DeployCallbacks): Promise<CascadeRoleProbeFailure[]>;
42
58
  /**
43
59
  * Deploy a single cascade account (platform or member).
44
60
  * Assumes the target account's role, sets env credentials, and deploys.
@@ -1 +1 @@
1
- import{join as T}from"path";import{success as L,failure as C}from"@fjall/generator";import{logger as R}from"@fjall/util/logger";import{maskSensitiveOutput as m}from"@fjall/util";import{ORGANISATION_TYPES as h,getOrganisationStackName as M}from"../types/operations.js";import{CdkContextBuilder as V}from"../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as _}from"../types/deployment/index.js";import{CloudFormationService as B}from"../services/infrastructure/CloudFormationService.js";import{getCascadeStateFilePath as G}from"../types/FjallState.js";import{BackupClient as z}from"@aws-sdk/client-backup";import{accountHasDisasterRecovery as K,describeBackupVaultExists as Y}from"../aws/organisations/backup.js";import{buildParamsContext as X,collectStackOutputs as U,assumeCascadeRole as b,forwardOutput as k}from"./contextHelpers.js";import{STRUCTURAL_ENVIRONMENTS as v}from"@fjall/util";import{DEFAULT_REGION as q}from"../aws/utils/regions.js";const de=4;function pe(s){const n=s.find(e=>e.environment===v.PLATFORM),i=s.filter(e=>e.environment!==v.ROOT&&e.environment!==v.PLATFORM);return{platformAccount:n,memberAccounts:i}}function me(s){const n=s?.primaryRegion??q,i=s?.secondaryRegions??[],e=[n,...i],a=s?.disasterRecoveryRegion;return a&&!e.includes(a)&&e.push(a),e}function fe(s,n){const i=[];for(const e of n)for(const a of s)i.push({account:a,region:e});return i}import{buildCascadeRoleArn as Ae}from"./contextHelpers.js";async function le(s,n,i,e,a,r,u){const t=u?.region??e.region??n.awsProvider.getRegion(),o=`${e.name} (${t})`,c=u?.orgConfig??s.orgConfig,f=u?.ipamPoolId;r.onCascadeAccountStart?.(o,e.id,t,a);const d=await b(n.awsProvider,e.id,t,`fjall-cascade-${e.name}`);if(!d.success)return r.onCascadeAccountComplete?.(o,!1,m(d.error.message),t),C(new Error(`Failed to assume role for ${e.name}: ${m(d.error.message)}`));const{provider:g,credentials:y}=d.data,P=T(s.workingDirectory,"fjall",a==="platform"?h.PLATFORM:h.ACCOUNT),j=t.replace(/-/g,""),D=T(P,`cdk.out.${e.id}.${j}`),A=V.buildDeploymentContext({deployType:a,target:i.target,path:P,assemblyDir:D,environment:e.environment,region:t,accountName:e.name,callerIdentity:_(e.id),ipamPoolId:f,...X({orgConfig:c,identity:s.identity,skipOidc:s.options?.skipOidc,skipAccountGlobals:u?.skipAccountGlobals})},{verbose:s.options?.verbose},c);if(K(e.environment,c?.disasterRecoveryRegion)){const p=await Y(g.getClient(z));if(!p.success)return r.onCascadeAccountComplete?.(o,!1,m(p.error.message),t),C(new Error(`Backup vault probe failed for ${e.name}: ${m(p.error.message)}`));A.fjallAdoptBackupVault=p.data}r.onCascadeAccountPhaseChange?.(o,"synth",t);const w=await n.cdkService.runCdkSynth(A,k(r),y);if(!w.success)return r.onCascadeAccountComplete?.(o,!1,m(`Synth failed: ${w.error}`),t),C(new Error(`Synth failed for ${e.name}: ${m(w.error)}`));const l=M(a==="platform"?h.PLATFORM:h.ACCOUNT),x=G(P,e.id,t),O=new B(g),{changed:H,currentHash:E}=await n.hashService.compareCascadeStack(D,l,x);if(!H&&s.options?.force!==!0&&await O.stackExists(l)){const p=await O.getStackOutputs(l);p.success||R.debug("cascadeHelpers","Failed to read outputs for skipped cascade account (non-critical)",{stackName:l,account:e.name});const N=U(p);return r.onLog?.(`${e.name}: no infrastructure changes \u2014 skipping deploy`,"info"),r.onCascadeAccountComplete?.(o,!0,void 0,t,N,!0),L({outputs:N,skipped:!0})}r.onCascadeAccountPhaseChange?.(o,"bootstrap",t);const S=await n.cdkService.runCdkBootstrap(A,k(r),y);if(!S.success)return r.onCascadeAccountComplete?.(o,!1,m(`Bootstrap failed: ${S.error}`),t),C(new Error(`Bootstrap failed for ${e.name}: ${m(S.error)}`));r.onCascadeAccountPhaseChange?.(o,"deploy",t);const $=await n.cdkService.runCdkDeploy(A,l,k(r),p=>r.onCascadeAccountResourceProgress?.(o,p,t),g,y);if(!$.success)return r.onCascadeAccountComplete?.(o,!1,m($.error),t),C(new Error(m($.error)));const I=await O.getStackOutputs(l);I.success||R.debug("cascadeHelpers","Failed to read cascade account stack outputs (non-critical)",{stackName:l,account:e.name});const F=U(I);return E!==void 0&&((await n.hashService.persistCascadeStack(x,l,E)).success||R.debug("cascadeHelpers","Failed to persist cascade hash state (non-critical)",{stackName:l,account:e.name})),r.onCascadeAccountComplete?.(o,!0,void 0,t,F,!1),L({outputs:F,skipped:!1})}async function ge(s,n,i){const e=new Map,a=s.awsProvider.getRegion(),r=await b(s.awsProvider,n.id,a,`fjall-ipam-read-${n.name}`);if(!r.success)return R.debug("organisationDeploy",`Cannot read Platform outputs: ${r.error.message}`),e;const u=new B(r.data.provider),t=M(h.PLATFORM),o=await u.getStackOutputs(t);if(!o.success)return R.debug("organisationDeploy",`Failed to read Platform stack outputs: ${o.error.message}`),e;const c=/^IpamPoolId(\d{12})(\w+)$/;for(const f of o.data){const d=f.OutputKey?.match(c);if(d&&f.OutputValue){const g=`${d[1]}-${d[2]}`;e.set(g,f.OutputValue)}}return e.size>0&&i.onLog?.(`Read ${e.size} IPAM pool ID(s) from Platform stack`,"info"),e}async function Ce(s,n){const i=s.getDomains();if(i.length===0)return{domainsDeployed:0,errors:[]};n.onCascadePhaseStart?.("domains");const e=i.filter(t=>t.type==="apex"),a=i.filter(t=>t.type==="delegated");let r=0;const u=[];for(const t of e){const o=await s.deployDomain(t.name,n);o.success?r++:u.push(`${t.name}: ${o.error.message}`)}if(a.length>0){const t=await Promise.allSettled(a.map(o=>s.deployDomain(o.name,n)));for(let o=0;o<t.length;o++){const c=t[o],f=a[o];if(!(!c||!f))if(c.status==="fulfilled")c.value.success?r++:u.push(`${f.name}: ${c.value.error.message}`);else{const d=c.reason instanceof Error?c.reason.message:String(c.reason);u.push(`${f.name}: ${d}`)}}}return n.onCascadePhaseComplete?.("domains"),{domainsDeployed:r,errors:u}}export{de as CASCADE_MAX_CONCURRENCY,fe as buildAccountRegionPairs,Ae as buildCascadeRoleArn,me as buildRegionList,le as deployCascadeAccount,Ce as deployDomains,pe as partitionAccounts,ge as readPlatformIpamPoolIds};
1
+ import{join as L}from"path";import{success as B,failure as C}from"@fjall/generator";import{logger as h}from"@fjall/util/logger";import{maskSensitiveOutput as p,mapSettledWithConcurrency as G}from"@fjall/util";import{ORGANISATION_TYPES as y,getOrganisationStackName as j}from"../types/operations.js";import{CdkContextBuilder as U}from"../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as _}from"../types/deployment/index.js";import{CloudFormationService as M}from"../services/infrastructure/CloudFormationService.js";import{getCascadeStateFilePath as z}from"../types/FjallState.js";import{BackupClient as K}from"@aws-sdk/client-backup";import{accountHasDisasterRecovery as Y,describeBackupVaultExists as W}from"../aws/organisations/backup.js";import{buildParamsContext as X,collectStackOutputs as T,assumeCascadeRole as v,forwardOutput as k}from"./contextHelpers.js";import{accountTier as b}from"@fjall/util";import{DEFAULT_REGION as q}from"../aws/utils/regions.js";const J=4;function pe(n){const r=n.find(e=>b(e)==="platform"),i=n.filter(e=>b(e)==="account");return{platformAccount:r,memberAccounts:i}}function le(n){const r=n?.primaryRegion??q,i=n?.secondaryRegions??[],e=new Set([r,...i]),a=n?.disasterRecoveryRegion;return a&&e.add(a),[...e]}function me(n,r){const i=[];for(const e of r)for(const a of n)i.push({account:a,region:e});return i}import{buildCascadeRoleArn as Pe}from"./contextHelpers.js";async function ge(n,r,i){i.onLog?.(`Verifying cascade role access for ${r.length} account(s)\u2026`,"info");const e=await G(r,J,async t=>v(n.awsProvider,t.id,t.region??n.awsProvider.getRegion(),`fjall-preflight-${t.name}`)),a=[];return e.forEach((t,c)=>{const o=r[c];if(o){if(t.status==="rejected"){a.push({accountId:o.id,accountName:o.name,error:t.reason instanceof Error?t.reason.message:String(t.reason)});return}t.value.success||a.push({accountId:o.id,accountName:o.name,error:t.value.error.message})}}),a.length===0&&i.onLog?.(`Cascade role access verified for ${r.length} account(s)`,"info"),a}async function Ce(n,r,i,e,a,t,c){const o=c?.region??e.region??r.awsProvider.getRegion(),s=`${e.name} (${o})`,u=c?.orgConfig??n.orgConfig,l=c?.ipamPoolId;t.onCascadeAccountStart?.(s,e.id,o,a);const d=await v(r.awsProvider,e.id,o,`fjall-cascade-${e.name}`);if(!d.success)return t.onCascadeAccountComplete?.(s,!1,p(d.error.message),o),C(new Error(`Failed to assume role for ${e.name}: ${p(d.error.message)}`));const{provider:g,credentials:w}=d.data,P=L(n.workingDirectory,"fjall",a==="platform"?y.PLATFORM:y.ACCOUNT),H=o.replace(/-/g,""),D=L(P,`cdk.out.${e.id}.${H}`),R=U.buildDeploymentContext({deployType:a,target:i.target,path:P,assemblyDir:D,environment:e.environment??void 0,region:o,accountName:e.name,callerIdentity:_(e.id),ipamPoolId:l,...X({orgConfig:u,identity:n.identity,skipOidc:n.options?.skipOidc,skipAccountGlobals:c?.skipAccountGlobals,trailLifecycle:e.trailLifecycle})},{verbose:n.options?.verbose},u);if(Y(e.environment,u?.disasterRecoveryRegion)){const f=await W(g.getClient(K));if(!f.success)return t.onCascadeAccountComplete?.(s,!1,p(f.error.message),o),C(new Error(`Backup vault probe failed for ${e.name}: ${p(f.error.message)}`));R.fjallAdoptBackupVault=f.data}t.onCascadeAccountPhaseChange?.(s,"synth",o);const $=await r.cdkService.runCdkSynth(R,k(t),w);if(!$.success)return t.onCascadeAccountComplete?.(s,!1,p(`Synth failed: ${$.error}`),o),C(new Error(`Synth failed for ${e.name}: ${p($.error)}`));const m=j(a==="platform"?y.PLATFORM:y.ACCOUNT),x=z(P,e.id,o),A=new M(g),{changed:V,currentHash:I}=await r.hashService.compareCascadeStack(D,m,x);if(!V&&n.options?.force!==!0&&await A.stackExists(m)){const f=await A.getStackOutputs(m);f.success||h.debug("cascadeHelpers","Failed to read outputs for skipped cascade account (non-critical)",{stackName:m,account:e.name});const F=T(f);return t.onLog?.(`${e.name}: no infrastructure changes \u2014 skipping deploy`,"info"),t.onCascadeAccountComplete?.(s,!0,void 0,o,F,!0),B({outputs:F,skipped:!0})}t.onCascadeAccountPhaseChange?.(s,"bootstrap",o);const S=await r.cdkService.runCdkBootstrap(R,k(t),w);if(!S.success)return t.onCascadeAccountComplete?.(s,!1,p(`Bootstrap failed: ${S.error}`),o),C(new Error(`Bootstrap failed for ${e.name}: ${p(S.error)}`));t.onCascadeAccountPhaseChange?.(s,"deploy",o);const O=await r.cdkService.runCdkDeploy(R,m,k(t),f=>t.onCascadeAccountResourceProgress?.(s,f,o),g,w);if(!O.success)return t.onCascadeAccountComplete?.(s,!1,p(O.error),o),C(new Error(p(O.error)));const E=await A.getStackOutputs(m);E.success||h.debug("cascadeHelpers","Failed to read cascade account stack outputs (non-critical)",{stackName:m,account:e.name});const N=T(E);return I!==void 0&&((await r.hashService.persistCascadeStack(x,m,I)).success||h.debug("cascadeHelpers","Failed to persist cascade hash state (non-critical)",{stackName:m,account:e.name})),t.onCascadeAccountComplete?.(s,!0,void 0,o,N,!1),B({outputs:N,skipped:!1})}async function he(n,r,i){const e=new Map,a=n.awsProvider.getRegion(),t=await v(n.awsProvider,r.id,a,`fjall-ipam-read-${r.name}`);if(!t.success)return h.debug("organisationDeploy",`Cannot read Platform outputs: ${t.error.message}`),e;const c=new M(t.data.provider),o=j(y.PLATFORM),s=await c.getStackOutputs(o);if(!s.success)return h.debug("organisationDeploy",`Failed to read Platform stack outputs: ${s.error.message}`),e;const u=/^IpamPoolId(\d{12})(\w+)$/;for(const l of s.data){const d=l.OutputKey?.match(u);if(d&&l.OutputValue){const g=`${d[1]}-${d[2]}`;e.set(g,l.OutputValue)}}return e.size>0&&i.onLog?.(`Read ${e.size} IPAM pool ID(s) from Platform stack`,"info"),e}async function ye(n,r){const i=n.getDomains();if(i.length===0)return{domainsDeployed:0,errors:[]};r.onCascadePhaseStart?.("domains");const e=i.filter(o=>o.type==="apex"),a=i.filter(o=>o.type==="delegated");let t=0;const c=[];for(const o of e){const s=await n.deployDomain(o.name,r);s.success?t++:c.push(`${o.name}: ${s.error.message}`)}if(a.length>0){const o=await Promise.allSettled(a.map(s=>n.deployDomain(s.name,r)));for(let s=0;s<o.length;s++){const u=o[s],l=a[s];if(!(!u||!l))if(u.status==="fulfilled")u.value.success?t++:c.push(`${l.name}: ${u.value.error.message}`);else{const d=u.reason instanceof Error?u.reason.message:String(u.reason);c.push(`${l.name}: ${d}`)}}}return r.onCascadePhaseComplete?.("domains"),{domainsDeployed:t,errors:c}}export{J as CASCADE_MAX_CONCURRENCY,me as buildAccountRegionPairs,Pe as buildCascadeRoleArn,le as buildRegionList,Ce as deployCascadeAccount,ye as deployDomains,pe as partitionAccounts,ge as probeCascadeRoles,he as readPlatformIpamPoolIds};
@@ -1,4 +1,5 @@
1
1
  import { type Result } from "@fjall/generator";
2
+ import type { TrailLifecycleState } from "@fjall/util/config";
2
3
  import type { ResourceEvent } from "@fjall/util/aws";
3
4
  import type { OrgConfig } from "../types/orgConfig.js";
4
5
  import type { DeployIdentity } from "../types/credentials.js";
@@ -16,11 +17,13 @@ export declare function buildParamsContext(params: {
16
17
  identity?: DeployIdentity;
17
18
  skipOidc?: boolean;
18
19
  skipAccountGlobals?: boolean;
20
+ trailLifecycle?: TrailLifecycleState;
19
21
  }): {
20
22
  orgConfig?: string;
21
23
  fjallOrgId?: string;
22
24
  fjallOidcConfigured?: boolean;
23
25
  fjallAccountGlobalsConfigured?: boolean;
26
+ fjallAccountTrailState?: string;
24
27
  };
25
28
  /** Forward onOutput callback — reduces lambda repetition across orchestration files. */
26
29
  export declare function forwardOutput(callbacks: DeployCallbacks): (chunk: string) => void;
@@ -1 +1 @@
1
- import{success as f,failure as u}from"@fjall/generator";import{getErrorMessage as y,maskSensitiveOutput as d,sleep as E}from"@fjall/util";import{logger as O}from"@fjall/util/logger";import{SimpleAwsProvider as R}from"../aws/SimpleAwsProvider.js";function k(e){return{...e.orgConfig!==void 0?{orgConfig:JSON.stringify(e.orgConfig)}:{},...e.identity!==void 0?{fjallOrgId:e.identity.fjallOrgId}:{},...e.skipOidc?{fjallOidcConfigured:!0}:{},...e.skipAccountGlobals?{fjallAccountGlobalsConfigured:!0}:{}}}function C(e){return r=>e.onOutput?.(r)}function M(e){return r=>e.onResourceProgress?.(r)}function T(e){if(!e.success||e.data.length===0)return;const r={};for(const t of e.data)t.OutputKey&&t.OutputValue!==void 0&&(r[t.OutputKey]=t.OutputValue);return Object.keys(r).length>0?r:void 0}const g="OrganizationAccountAccessRole",l=5,S=5e3,w=3e4;function $(e){return`arn:aws:iam::${e}:role/${g}`}async function D(e,r,t,s){if(!e.assumeRole)return u(new Error("AwsProvider does not support assumeRole"));const n=$(r),i=e.assumeRole.bind(e);let o;for(let c=0;c<=l;c++)try{o=await i(n,s);break}catch(a){const p=a instanceof Error?a.name:void 0;if(p==="AccessDenied"||p==="AccessDeniedException")return u(new Error(`Access denied assuming ${g} in account ${r}. The role may not exist or may not trust the management account.`));if(c<l){const m=Math.min(S*2**c,w);O.debug("assumeCascadeRole",`Attempt ${c+1} failed for account ${r}, retrying in ${Math.round(m/1e3)}s`,{error:d(y(a))}),await E(m);continue}return u(new Error(`Failed to assume role in account ${r} after ${l+1} attempts: ${d(y(a))}`))}if(!o)return u(new Error(`Failed to assume role in account ${r}`));const A=new R({accessKeyId:o.accessKeyId,secretAccessKey:o.secretAccessKey,sessionToken:o.sessionToken,region:t,accountId:r});return f({provider:A,credentials:{accessKeyId:o.accessKeyId,secretAccessKey:o.secretAccessKey,sessionToken:o.sessionToken}})}async function b(e,r,t,s){const n=await e.cdkService.runCdkSynth(r,i=>t.onCdkOutput?.(i,"synth"));if(!n.success){const i=new Error(d(`${s}: ${n.error}`));return t.onError?.(i),u(i)}return f(void 0)}async function B(e,r,t){t.onCDKBootstrap?.("bootstrapping");const s=await e.cdkService.runCdkBootstrap(r,C(t));if(!s.success){t.onCDKBootstrap?.("failed");const n=new Error(d(`Bootstrap failed: ${s.error}`));return t.onError?.(n),u(n)}return t.onCDKBootstrap?.("complete"),f(void 0)}export{D as assumeCascadeRole,B as bootstrapOrFail,$ as buildCascadeRoleArn,k as buildParamsContext,T as collectStackOutputs,C as forwardOutput,M as forwardResourceProgress,b as synthOrFail};
1
+ import{success as f,failure as c}from"@fjall/generator";import{getErrorMessage as y,maskSensitiveOutput as d,sleep as E}from"@fjall/util";import{logger as C}from"@fjall/util/logger";import{SimpleAwsProvider as O}from"../aws/SimpleAwsProvider.js";const R={account:"active",draining:"draining",org:"removed"};function k(e){return{...e.orgConfig!==void 0?{orgConfig:JSON.stringify(e.orgConfig)}:{},...e.identity!==void 0?{fjallOrgId:e.identity.fjallOrgId}:{},...e.skipOidc?{fjallOidcConfigured:!0}:{},...e.skipAccountGlobals?{fjallAccountGlobalsConfigured:!0}:{},...e.trailLifecycle!==void 0?{fjallAccountTrailState:R[e.trailLifecycle]}:{}}}function S(e){return t=>e.onOutput?.(t)}function M(e){return t=>e.onResourceProgress?.(t)}function D(e){if(!e.success||e.data.length===0)return;const t={};for(const r of e.data)r.OutputKey&&r.OutputValue!==void 0&&(t[r.OutputKey]=r.OutputValue);return Object.keys(t).length>0?t:void 0}const m="OrganizationAccountAccessRole",l=5,_=5e3,w=3e4;function K(e){return`arn:aws:iam::${e}:role/${m}`}async function L(e,t,r,s){if(!e.assumeRole)return c(new Error("AwsProvider does not support assumeRole"));const o=K(t),i=e.assumeRole.bind(e);let n;for(let u=0;u<=l;u++)try{n=await i(o,s);break}catch(a){const p=a instanceof Error?a.name:void 0;if(p==="AccessDenied"||p==="AccessDeniedException")return c(new Error(`Access denied assuming ${m} in account ${t}. The role may not exist or may not trust the management account.`));if(u<l){const g=Math.min(_*2**u,w);C.debug("assumeCascadeRole",`Attempt ${u+1} failed for account ${t}, retrying in ${Math.round(g/1e3)}s`,{error:d(y(a))}),await E(g);continue}return c(new Error(`Failed to assume role in account ${t} after ${l+1} attempts: ${d(y(a))}`))}if(!n)return c(new Error(`Failed to assume role in account ${t}`));const A=new O({accessKeyId:n.accessKeyId,secretAccessKey:n.secretAccessKey,sessionToken:n.sessionToken,region:r,accountId:t});return f({provider:A,credentials:{accessKeyId:n.accessKeyId,secretAccessKey:n.secretAccessKey,sessionToken:n.sessionToken}})}async function b(e,t,r,s){const o=await e.cdkService.runCdkSynth(t,i=>r.onCdkOutput?.(i,"synth"));if(!o.success){const i=new Error(d(`${s}: ${o.error}`));return r.onError?.(i),c(i)}return f(void 0)}async function j(e,t,r){r.onCDKBootstrap?.("bootstrapping");const s=await e.cdkService.runCdkBootstrap(t,S(r));if(!s.success){r.onCDKBootstrap?.("failed");const o=new Error(d(`Bootstrap failed: ${s.error}`));return r.onError?.(o),c(o)}return r.onCDKBootstrap?.("complete"),f(void 0)}export{L as assumeCascadeRole,j as bootstrapOrFail,K as buildCascadeRoleArn,k as buildParamsContext,D as collectStackOutputs,S as forwardOutput,M as forwardResourceProgress,b as synthOrFail};
@@ -10,7 +10,11 @@ export { projectScalarSummary, projectAccountRows } from "./cascadeSummary.js";
10
10
  export type { CascadeOutcomeResult, CascadeMemberOutcome, CascadePlatformOutcome, CascadeLedger, CascadeAccountRow, CascadePlatformRow, CascadeAccountProjection } from "./cascadeSummary.js";
11
11
  export { reconcileProviderAccounts, mergeReconciledProviderAccounts } from "./reconcileProviderAccounts.js";
12
12
  export type { ReconcileResult } from "./reconcileProviderAccounts.js";
13
- export { parseAccountsConfiguration, flattenAccountsToEnvironments, extractAllAccountNames, accountsConfigToOUTree, isStringArray, isAccountsConfig } from "./accountsConfig.js";
13
+ export { decideNextTransition, reconcileTrailMigration, ORG_TRAIL_BUCKET_OUTPUT_KEY, TRAIL_BUCKET_OUTPUT_KEY, TRAIL_KEY_ARN_OUTPUT_KEY } from "./trailMigration/trailMigration.js";
14
+ export type { MemberTrailFacts, TrailMigrationTransition, TrailMigrationOutcome } from "./trailMigration/trailMigration.js";
15
+ export { decommissionMemberTrailStorage } from "./trailMigration/memberTrailCleanup.js";
16
+ export type { DecommissionClients, DecommissionInput, DecommissionOutcome } from "./trailMigration/memberTrailCleanup.js";
17
+ export { parseAccountsConfiguration, flattenAccountsToEnvironments, extractAllAccountNames, accountsConfigToOUTree, isStringArray, isAccountsConfig, isOuOnlyAccountBucket, OU_ONLY_ACCOUNT_BUCKETS } from "./accountsConfig.js";
14
18
  export type { AccountsConfig } from "./accountsConfig.js";
15
19
  export type { DockerProvider, DockerProgressCallback, DockerServiceConfig, DockerBuildParams, DockerBuildResult, ECRInitParams, ECRInitResult, TagImagesParams, TagImagesResult, TagByDigestParams } from "./dockerInterface.js";
16
20
  export type { DomainDeployProvider, DomainConfig, DomainDeployResult } from "./domainInterface.js";
@@ -1 +1 @@
1
- import{deploy as t}from"./deploy.js";import{destroy as n}from"./destroy.js";import{deployOrganisation as i}from"./organisationDeploy.js";import{destroyOrganisation as p}from"./organisationDestroy.js";import{cleanupFailedStack as u,isCleanableState as m,SAFE_CLEANUP_STATES as f}from"./stackCleanup.js";import{partitionAccounts as x,buildRegionList as A,buildAccountRegionPairs as d}from"./cascadeHelpers.js";import{projectScalarSummary as S,projectAccountRows as y}from"./cascadeSummary.js";import{reconcileProviderAccounts as O,mergeReconciledProviderAccounts as T}from"./reconcileProviderAccounts.js";import{parseAccountsConfiguration as P,flattenAccountsToEnvironments as R,extractAllAccountNames as b,accountsConfigToOUTree as v,isStringArray as N,isAccountsConfig as j}from"./accountsConfig.js";import{runOpenNextBuild as L}from"./openNextBuild.js";import{runOrganisationSetup as _}from"./organisationSetup.js";export*from"./builders/index.js";export{f as SAFE_CLEANUP_STATES,v as accountsConfigToOUTree,d as buildAccountRegionPairs,A as buildRegionList,u as cleanupFailedStack,t as deploy,i as deployOrganisation,n as destroy,p as destroyOrganisation,b as extractAllAccountNames,R as flattenAccountsToEnvironments,j as isAccountsConfig,m as isCleanableState,N as isStringArray,T as mergeReconciledProviderAccounts,P as parseAccountsConfiguration,x as partitionAccounts,y as projectAccountRows,S as projectScalarSummary,O as reconcileProviderAccounts,L as runOpenNextBuild,_ as runOrganisationSetup};
1
+ import{deploy as e}from"./deploy.js";import{destroy as n}from"./destroy.js";import{deployOrganisation as c}from"./organisationDeploy.js";import{destroyOrganisation as s}from"./organisationDestroy.js";import{cleanupFailedStack as m,isCleanableState as u,SAFE_CLEANUP_STATES as T}from"./stackCleanup.js";import{partitionAccounts as l,buildRegionList as f,buildAccountRegionPairs as x}from"./cascadeHelpers.js";import{projectScalarSummary as d,projectAccountRows as O}from"./cascadeSummary.js";import{reconcileProviderAccounts as g,mergeReconciledProviderAccounts as E}from"./reconcileProviderAccounts.js";import{decideNextTransition as C,reconcileTrailMigration as R,ORG_TRAIL_BUCKET_OUTPUT_KEY as y,TRAIL_BUCKET_OUTPUT_KEY as K,TRAIL_KEY_ARN_OUTPUT_KEY as N}from"./trailMigration/trailMigration.js";import{decommissionMemberTrailStorage as L}from"./trailMigration/memberTrailCleanup.js";import{parseAccountsConfiguration as Y,flattenAccountsToEnvironments as b,extractAllAccountNames as v,accountsConfigToOUTree as I,isStringArray as j,isAccountsConfig as k,isOuOnlyAccountBucket as F,OU_ONLY_ACCOUNT_BUCKETS as M}from"./accountsConfig.js";import{runOpenNextBuild as G}from"./openNextBuild.js";import{runOrganisationSetup as q}from"./organisationSetup.js";export*from"./builders/index.js";export{y as ORG_TRAIL_BUCKET_OUTPUT_KEY,M as OU_ONLY_ACCOUNT_BUCKETS,T as SAFE_CLEANUP_STATES,K as TRAIL_BUCKET_OUTPUT_KEY,N as TRAIL_KEY_ARN_OUTPUT_KEY,I as accountsConfigToOUTree,x as buildAccountRegionPairs,f as buildRegionList,m as cleanupFailedStack,C as decideNextTransition,L as decommissionMemberTrailStorage,e as deploy,c as deployOrganisation,n as destroy,s as destroyOrganisation,v as extractAllAccountNames,b as flattenAccountsToEnvironments,k as isAccountsConfig,u as isCleanableState,F as isOuOnlyAccountBucket,j as isStringArray,E as mergeReconciledProviderAccounts,Y as parseAccountsConfiguration,l as partitionAccounts,O as projectAccountRows,d as projectScalarSummary,g as reconcileProviderAccounts,R as reconcileTrailMigration,G as runOpenNextBuild,q as runOrganisationSetup};
@@ -1,5 +1,5 @@
1
- import{join as Ie}from"node:path";import{success as re,failure as h}from"@fjall/generator";import{OrganizationsClient as we}from"@aws-sdk/client-organizations";import{BackupClient as Ne}from"@aws-sdk/client-backup";import{ORGANISATION_TYPES as K,getOrganisationStackName as me}from"../types/operations.js";import{CdkContextBuilder as ye}from"../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as De}from"../types/deployment/index.js";import{ensureOrganisationExists as ke}from"../aws/organisations/organisation.js";import{accountHasDisasterRecovery as Te,describeBackupVaultExists as be}from"../aws/organisations/backup.js";import{buildParamsContext as Le,collectStackOutputs as ae,synthOrFail as ge,bootstrapOrFail as fe,forwardOutput as Ce,forwardResourceProgress as Se}from"./contextHelpers.js";import{partitionAccounts as Ae,deployCascadeAccount as Ee,readPlatformIpamPoolIds as Me,deployDomains as _e,buildRegionList as $e,buildAccountRegionPairs as Fe,CASCADE_MAX_CONCURRENCY as Ge}from"./cascadeHelpers.js";import{projectScalarSummary as Oe}from"./cascadeSummary.js";import{reconcileProviderAccounts as Ye,mergeReconciledProviderAccounts as ve}from"./reconcileProviderAccounts.js";import{maskSensitiveOutput as i,STRUCTURAL_ENVIRONMENTS as xe,mapSettledWithConcurrency as Ue}from"@fjall/util";import{INFRA_STEP_NAME as X,STEP_IDS as b,STEP_NAMES as se}from"../types/stepDefinitions.js";async function at(o,t,s){const g=Date.now();switch(s.type){case K.ORGANISATION:return We(o,t,s,g);case K.PLATFORM:return he(o,t,s,"platform",g);case K.ACCOUNT:return he(o,t,s,"account",g);default:{const e=s.type;return h(new Error(`Unsupported organisation type: ${String(e)}`))}}}function Re(o,t,s,g,e,r){return ye.buildDeploymentContext({deployType:g,target:s.target,path:s.path,region:t.awsProvider.getRegion(),accountName:r,callerIdentity:De(t.awsProvider.getAccountId()),orgId:e.orgId,rootId:e.rootId,managementAccountId:e.managementAccountId,...Le({orgConfig:o.orgConfig,identity:o.identity,skipOidc:o.options?.skipOidc})},{verbose:o.options?.verbose,infraOnly:o.options?.infraOnly},o.orgConfig)}async function Pe(o){const t=o.awsProvider.getClient(we),s=await ke(t);return s.success?re({orgId:s.data.orgId,rootId:s.data.rootId,managementAccountId:s.data.managementAccountId}):h(s.error)}const n={CONNECT:{id:b.CONNECT,name:X.CONNECT},PREPARE:{id:b.PREPARE_ENVIRONMENT,name:X.PREPARE},DEPLOY:{id:b.DEPLOY,name:X.DEPLOY},MONITORING:{id:b.MONITORING,name:X.MONITORING},ORG_DEPLOY:{id:b.ORG_DEPLOY,name:se.ORG_DEPLOY},CASCADE_PLATFORM:{id:b.CASCADE_PLATFORM,name:se.CASCADE_PLATFORM},CASCADE_ACCOUNTS:{id:b.CASCADE_ACCOUNTS,name:se.CASCADE_ACCOUNTS}},S=4;async function he(o,t,s,g,e){const{callbacks:r}=o;r.onStepComplete?.(n.CONNECT.id,n.CONNECT.name,"completed",0,S),r.onStepStart?.(n.PREPARE.id,n.PREPARE.name,1,S);const p=await Pe(t);if(!p.success){r.onStepComplete?.(n.PREPARE.id,n.PREPARE.name,"error",1,S);const l=new Error(i(p.error.message));return r.onError?.(l),h(l)}const $=Re(o,t,s,g,p.data,g==="account"?s.target:void 0),L=o.orgConfig?.disasterRecoveryRegion,W=L!==void 0&&L!=="",M=g==="account"?o.orgConfig?.providerAccounts.find(l=>l.name===s.target):void 0;if(g==="account"&&W&&(M===void 0||Te(M.environment,L))){const l=await be(t.awsProvider.getClient(Ne));if(!l.success){r.onStepComplete?.(n.PREPARE.id,n.PREPARE.name,"error",1,S);const y=new Error(i(l.error.message));return r.onError?.(y),h(y)}$.fjallAdoptBackupVault=l.data}r.onLog?.(`Synthesising ${g} infrastructure\u2026`,"info");const _=await ge(t,$,r,"CDK synthesis failed");if(!_.success)return r.onStepComplete?.(n.PREPARE.id,n.PREPARE.name,"error",1,S),_;const j=await fe(t,$,r);if(!j.success)return r.onStepComplete?.(n.PREPARE.id,n.PREPARE.name,"error",1,S),j;r.onStepComplete?.(n.PREPARE.id,n.PREPARE.name,"completed",1,S);const V=me(s.type);r.onStepStart?.(n.DEPLOY.id,n.DEPLOY.name,2,S);const F=await t.cdkService.runCdkDeploy($,V,Ce(r),Se(r),t.awsProvider);if(!F.success){r.onStepComplete?.(n.DEPLOY.id,n.DEPLOY.name,"error",2,S);const l=new Error(i(F.error));return r.onError?.(l),h(l)}r.onStepComplete?.(n.DEPLOY.id,n.DEPLOY.name,"completed",2,S);const G=await t.cfnService.getStackOutputs(V);G.success||r.onLog?.("Failed to read stack outputs (non-critical)","debug");const d=ae(G);return r.onStepStart?.(n.MONITORING.id,n.MONITORING.name,3,S),r.onStepComplete?.(n.MONITORING.id,n.MONITORING.name,"completed",3,S),re({target:s.target,deploymentType:"organisation",outputs:d,durationMs:Date.now()-e})}async function We(o,t,s,g){const{callbacks:e,options:r}=o;let p=o.orgConfig?.providerAccounts??[];if(p.length===0||p.every(a=>a.environment===xe.ROOT)){const a=await Ye(t,o.workingDirectory);if(a.success){const{providerAccounts:c,missingAccountNames:C}=a.data;c.length>0&&(p=ve(o.orgConfig,c).providerAccounts,e.onLog?.(`Reconciled ${c.length} account(s) from AWS Organizations`,"info")),C.length>0&&(e.onCascadeMissingAccounts?.(C),e.onProgress?.({type:"warning",message:i(`Accounts declared in ACCOUNTS but not yet in AWS Organizations (cascade will skip): ${C.join(", ")}`)}))}else e.onProgress?.({type:"warning",message:i(`Could not reconcile accounts from AWS Organizations \u2014 cascade may skip accounts: ${a.error.message}`)})}const L=o.orgConfig?.primaryRegion??t.awsProvider.getRegion();p=p.map(a=>a.region!==void 0?a:{...a,region:L});const W=o.orgConfig!==void 0?{...o.orgConfig,providerAccounts:p}:p.length>0?{providerAccounts:p}:void 0,M=await Pe(t);if(!M.success){const a=new Error(i(M.error.message));return e.onError?.(a),h(a)}const N=Re(o,t,s,"organisation",M.data),_=r?.cascade!==!1,{platformAccount:j,memberAccounts:V}=Ae(p),F=_&&j!==void 0?1:0,G=_&&V.length>0?1:0,d=2+F+G;e.onCascadeAccountsReconciled?.({hasPlatformAccount:F>0,hasMemberAccounts:G>0});const{id:l,name:y}=n.PREPARE;e.onStepStart?.(l,y,0,d),e.onLog?.("Synthesising organisation infrastructure\u2026","info");const ce=await ge(t,N,e,"CDK synthesis failed");if(!ce.success)return e.onStepComplete?.(l,y,"error",0,d),ce;const ie=await fe(t,N,e);if(!ie.success)return e.onStepComplete?.(l,y,"error",0,d),ie;e.onStepComplete?.(l,y,"completed",0,d);const{id:J,name:Q}=n.ORG_DEPLOY,D=me(K.ORGANISATION);let de=!0;const Y=await t.hashService.getTemplateHashes(Ie(N.path,"cdk.out"));if(Y.success){const a=await t.hashService.compareWithState(Y.data,N.path);a.success?de=a.data.stackChanges.get(D)??!0:e.onLog?.(i(`Org root change detection failed \u2014 deploying to be safe: ${a.error.message}`),"warn")}else e.onLog?.(i(`Org root template hashing failed \u2014 deploying to be safe: ${Y.error.message}`),"warn");const Z=de||r?.force===!0||!await t.cfnService.stackExists(D);e.onOrgChangesDetected?.({hasOrgChanges:Z});let ee;if(Z){e.onStepStart?.(J,Q,1,d);const a=await t.cdkService.runCdkDeploy(N,D,Ce(e),Se(e),t.awsProvider);if(!a.success){e.onStepComplete?.(J,Q,"error",1,d);const R=new Error(i(a.error));return e.onError?.(R),h(R)}const c=await t.cfnService.getStackOutputs(D);c.success||e.onLog?.("Failed to read org stack outputs (non-critical)","debug"),ee=ae(c);const C=Y.success?Y.data.get(D):void 0;if(C!==void 0){const R=await t.hashService.updateStateAfterDeploy(N.path,new Map([[D,C]]));R.success||e.onLog?.(`Warning: failed to update state file \u2014 next deploy may re-deploy the org root: ${i(R.error.message)}`,"warn")}e.onStepComplete?.(J,Q,"completed",1,d)}else{e.onLog?.("Organisation root: no infrastructure changes \u2014 skipping deploy","info");const a=await t.cfnService.getStackOutputs(D);a.success||e.onLog?.("Failed to read org stack outputs (non-critical)","debug"),ee=ae(a)}const f=[],H=[];let z=Z;if(_&&p.length>0){e.onCascadeStart?.();const a=Date.now();let c=2,C=!1,R,te=!1;const B=[],ue=u=>({members:B,...R!==void 0?{platform:R}:{},domainsDeployed:te,errors:f,totalDurationMs:u}),{platformAccount:A,memberAccounts:q}=Ae(p);if(A){const{id:u,name:m}=n.CASCADE_PLATFORM;e.onStepStart?.(u,m,c,d),e.onCascadePhaseStart?.("platform");let E;const oe=Date.now();try{E=await Ee(o,t,s,A,"platform",e,{orgConfig:W})}catch(P){const x=i(P instanceof Error?P.message:String(P));f.push({accountId:A.id,error:x}),e.onCascadePhaseComplete?.("platform"),e.onStepComplete?.(u,m,"error",c,d),E=h(new Error(x))}const v=Date.now()-oe;if(E.success){C=!0;const P=E.data.skipped===!0;P||(z=!0),E.data.outputs&&H.push({accountId:A.id,outputs:E.data.outputs}),e.onCascadePhaseComplete?.("platform"),e.onStepComplete?.(u,m,P?"skipped":"completed",c,d),R={accountId:A.id,result:P?"skipped":"succeeded",durationMs:v}}else f.some(P=>P.accountId===A.id)||(f.push({accountId:A.id,error:i(E.error.message)}),e.onCascadePhaseComplete?.("platform"),e.onStepComplete?.(u,m,"error",c,d)),R={accountId:A.id,result:"failed",durationMs:v,error:i(E.error.message)};c++}let le=new Map;if(C&&A&&(le=await Me(t,A,e)),o.domainProvider){const u=await _e(o.domainProvider,e);te=u.domainsDeployed>0,te&&(z=!0);for(const m of u.errors)f.push({accountId:"domains",error:i(m)})}if(A!==void 0&&!C&&q.length>0){const{id:u,name:m}=n.CASCADE_ACCOUNTS;e.onStepStart?.(u,m,c,d),e.onStepComplete?.(u,m,"skipped",c,d),e.onLog?.("Skipping account cascade \u2014 platform deployment failed; platform is a prerequisite for member accounts.","warn")}else if(q.length>0){const{id:u,name:m}=n.CASCADE_ACCOUNTS;e.onStepStart?.(u,m,c,d),e.onCascadePhaseStart?.("accounts");const E=$e(o.orgConfig),oe=E[0]??L,v=Fe(q,E);(await Ue(v,Ge,async({account:I,region:U})=>{const k=U.replace(/-/g,""),O=le.get(`${I.id}-${k}`),T=Date.now();return{result:await Ee(o,t,s,I,"account",e,{ipamPoolId:O,orgConfig:W,region:U,skipAccountGlobals:U!==oe}),durationMs:Date.now()-T}})).forEach((I,U)=>{const k=v[U];if(!k)return;const O=k.account;if(I.status==="rejected"){const w=i(I.reason instanceof Error?I.reason.message:String(I.reason));B.push({accountId:O.id,accountName:O.name,region:k.region,result:"failed",durationMs:0,error:w}),f.push({accountId:O.id,error:w});return}const{result:T,durationMs:ne}=I.value;if(T.success){const w=T.data.skipped===!0;w||(z=!0),T.data.outputs&&H.push({accountId:O.id,outputs:T.data.outputs}),B.push({accountId:O.id,accountName:O.name,region:k.region,result:w?"skipped":"succeeded",durationMs:ne})}else{const w=i(T.error.message);B.push({accountId:O.id,accountName:O.name,region:k.region,result:"failed",durationMs:ne,error:w}),f.push({accountId:O.id,error:w})}});const x=Oe(ue(0));e.onCascadePhaseComplete?.("accounts"),e.onStepComplete?.(u,m,x.accountsFailed>0?"error":x.accountsSkipped===q.length?"skipped":"completed",c,d)}const pe=ue(Date.now()-a);if(e.onCascadeComplete?.(Oe(pe)),e.onCascadeLedger?.(pe),f.length>0){const u=f.map(m=>` ${m.accountId}: ${m.error}`).join(`
2
- `);e.onLog?.(i(`Cascade failed for ${f.length} target(s):
3
- ${u}`),"warn")}}if(f.length>0){const a=f.map(C=>i(`${C.accountId}: ${C.error}`)).join(`
4
- `),c=new Error(`Organisation root deployed, but the cascade failed for ${f.length} target(s):
5
- ${a}`);return e.onError?.(c),h(c)}return re({target:s.target,deploymentType:"organisation",outputs:ee,...H.length>0?{cascadeOutputs:H}:{},...z?{}:{noChanges:!0},durationMs:Date.now()-g})}export{at as deployOrganisation};
1
+ import{join as _t}from"node:path";import{success as W,failure as x}from"@fjall/generator";import{OrganizationsClient as $t}from"@aws-sdk/client-organizations";import{BackupClient as Ft}from"@aws-sdk/client-backup";import{S3Client as st}from"@aws-sdk/client-s3";import{CloudTrailClient as Pt}from"@aws-sdk/client-cloudtrail";import{KMSClient as Rt}from"@aws-sdk/client-kms";import{ORGANISATION_TYPES as Z,getOrganisationStackName as ht}from"../types/operations.js";import{CdkContextBuilder as xt}from"../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as Gt}from"../types/deployment/index.js";import{ensureOrganisationExists as Yt}from"../aws/organisations/organisation.js";import{DEFAULT_REGION as Ut}from"../aws/utils/regions.js";import{accountHasDisasterRecovery as vt,describeBackupVaultExists as Bt}from"../aws/organisations/backup.js";import{assumeCascadeRole as jt,buildParamsContext as Wt,collectStackOutputs as Kt,synthOrFail as Et,bootstrapOrFail as It,forwardOutput as wt,forwardResourceProgress as yt}from"./contextHelpers.js";import{reconcileTrailMigration as Ht,ORG_TRAIL_BUCKET_OUTPUT_KEY as Vt,TRAIL_BUCKET_OUTPUT_KEY as zt}from"./trailMigration/trailMigration.js";import{partitionAccounts as Nt,deployCascadeAccount as Dt,probeCascadeRoles as qt,readPlatformIpamPoolIds as Xt,deployDomains as Jt,buildRegionList as Qt,buildAccountRegionPairs as Zt,CASCADE_MAX_CONCURRENCY as te}from"./cascadeHelpers.js";import{projectScalarSummary as Tt}from"./cascadeSummary.js";import{reconcileProviderAccounts as ee,mergeReconciledProviderAccounts as oe}from"./reconcileProviderAccounts.js";import{accountTier as dt,getErrorMessage as kt,maskSensitiveOutput as g,mapSettledWithConcurrency as ne}from"@fjall/util";import{INFRA_STEP_NAME as tt,STEP_IDS as M,STEP_NAMES as ut}from"../types/stepDefinitions.js";async function ye(o,e,n){const f=Date.now();switch(n.type){case Z.ORGANISATION:return re(o,e,n,f);case Z.PLATFORM:return Mt(o,e,n,"platform",f);case Z.ACCOUNT:return Mt(o,e,n,"account",f);default:{const t=n.type;return x(new Error(`Unsupported organisation type: ${String(t)}`))}}}function bt(o,e,n,f,t,r,u){return xt.buildDeploymentContext({deployType:f,target:n.target,path:n.path,region:e.awsProvider.getRegion(),accountName:r,callerIdentity:Gt(e.awsProvider.getAccountId()),orgId:t.orgId,rootId:t.rootId,managementAccountId:t.managementAccountId,...Wt({orgConfig:o.orgConfig,identity:o.identity,skipOidc:o.options?.skipOidc,trailLifecycle:u})},{verbose:o.options?.verbose,infraOnly:o.options?.infraOnly},o.orgConfig)}async function Lt(o){const e=o.awsProvider.getClient($t),n=await Yt(e);return n.success?W({orgId:n.data.orgId,rootId:n.data.rootId,managementAccountId:n.data.managementAccountId}):x(n.error)}const c={CONNECT:{id:M.CONNECT,name:tt.CONNECT},PREPARE:{id:M.PREPARE_ENVIRONMENT,name:tt.PREPARE},DEPLOY:{id:M.DEPLOY,name:tt.DEPLOY},MONITORING:{id:M.MONITORING,name:tt.MONITORING},ORG_DEPLOY:{id:M.ORG_DEPLOY,name:ut.ORG_DEPLOY},CASCADE_PLATFORM:{id:M.CASCADE_PLATFORM,name:ut.CASCADE_PLATFORM},CASCADE_ACCOUNTS:{id:M.CASCADE_ACCOUNTS,name:ut.CASCADE_ACCOUNTS}},h=4;function et(o){o.onStepComplete?.(c.PREPARE.id,c.PREPARE.name,"error",1,h)}function G(o,e){const n=new Error(g(e));return o.onError?.(n),x(n)}async function lt(o,e,n,f){const t=await o.cfnService.getStackOutputs(n);return t.success||e.onLog?.(f,"debug"),Kt(t)}async function Mt(o,e,n,f,t){const{callbacks:r}=o;r.onStepComplete?.(c.CONNECT.id,c.CONNECT.name,"completed",0,h),r.onStepStart?.(c.PREPARE.id,c.PREPARE.name,1,h);const u=await Lt(e);if(!u.success)return et(r),G(r,u.error.message);const K=f==="account"?o.orgConfig?.providerAccounts.find(P=>P.name===n.target):o.orgConfig?.providerAccounts.find(P=>dt(P)==="platform"),E=bt(o,e,n,f,u.data,f==="account"?n.target:void 0,K?.trailLifecycle),_=o.orgConfig?.disasterRecoveryRegion;if(f==="account"&&(_!==void 0&&_!=="")&&(K===void 0||vt(K.environment,_))){const P=await Bt(e.awsProvider.getClient(Ft));if(!P.success)return et(r),G(r,P.error.message);E.fjallAdoptBackupVault=P.data}r.onLog?.(`Synthesising ${f} infrastructure\u2026`,"info");const R=await Et(e,E,r,"CDK synthesis failed");if(!R.success)return et(r),R;const T=await It(e,E,r);if(!T.success)return et(r),T;r.onStepComplete?.(c.PREPARE.id,c.PREPARE.name,"completed",1,h);const I=ht(n.type);r.onStepStart?.(c.DEPLOY.id,c.DEPLOY.name,2,h);const Y=await e.cdkService.runCdkDeploy(E,I,wt(r),yt(r),e.awsProvider);if(!Y.success)return r.onStepComplete?.(c.DEPLOY.id,c.DEPLOY.name,"error",2,h),G(r,Y.error);r.onStepComplete?.(c.DEPLOY.id,c.DEPLOY.name,"completed",2,h);const H=await lt(e,r,I,"Failed to read stack outputs (non-critical)");return r.onStepStart?.(c.MONITORING.id,c.MONITORING.name,3,h),r.onStepComplete?.(c.MONITORING.id,c.MONITORING.name,"completed",3,h),W({target:n.target,deploymentType:"organisation",outputs:H,durationMs:Date.now()-t})}async function re(o,e,n,f){const{callbacks:t,options:r}=o;let u=o.orgConfig?.providerAccounts??[];if(u.length===0||u.every(a=>dt(a)==="organisation")){const a=await ee(e,o.workingDirectory);if(a.success){const{providerAccounts:i,missingAccountNames:l}=a.data;i.length>0&&(u=oe(o.orgConfig,i).providerAccounts,t.onLog?.(`Reconciled ${i.length} account(s) from AWS Organizations`,"info")),l.length>0&&(t.onCascadeMissingAccounts?.(l),t.onProgress?.({type:"warning",message:g(`Accounts declared in ACCOUNTS but not yet in AWS Organizations (cascade will skip): ${l.join(", ")}`)}))}else t.onProgress?.({type:"warning",message:g(`Could not reconcile accounts from AWS Organizations \u2014 cascade may skip accounts: ${a.error.message}`)})}const E=o.orgConfig?.primaryRegion??Ut;u=u.map(a=>a.region!==void 0?a:{...a,region:E});const _=o.orgConfig!==void 0?{...o.orgConfig,providerAccounts:u}:u.length>0?{providerAccounts:u}:void 0,D=await Lt(e);if(!D.success)return G(t,D.error.message);const pt=u.find(a=>dt(a)==="organisation"),R=bt(o,e,n,"organisation",D.data,void 0,pt?.trailLifecycle),T=r?.cascade!==!1,{platformAccount:I,memberAccounts:Y}=Nt(u),H=T&&I!==void 0?1:0,P=T&&Y.length>0?1:0,m=2+H+P,gt=T?[...I!==void 0?[I]:[],...Y]:[];if(gt.length>0){const a=await qt(e,gt,t),i=I!==void 0?a.find(l=>l.accountId===I.id):void 0;if(i!==void 0)return G(t,`Pre-flight cascade role check failed for the platform account ${i.accountName} (${i.accountId}): ${i.error}`);for(const l of a)t.onProgress?.({type:"warning",message:g(`Pre-flight cascade role check failed for ${l.accountName} (${l.accountId}) \u2014 its cascade deploy is expected to fail: ${l.error}`)})}t.onCascadeAccountsReconciled?.({hasPlatformAccount:H>0,hasMemberAccounts:P>0});const{id:V,name:z}=c.PREPARE;t.onStepStart?.(V,z,0,m),t.onLog?.("Synthesising organisation infrastructure\u2026","info");const ft=await Et(e,R,t,"CDK synthesis failed");if(!ft.success)return t.onStepComplete?.(V,z,"error",0,m),ft;const mt=await It(e,R,t);if(!mt.success)return t.onStepComplete?.(V,z,"error",0,m),mt;t.onStepComplete?.(V,z,"completed",0,m);const{id:ot,name:nt}=c.ORG_DEPLOY,k=ht(Z.ORGANISATION);let Ct=!0;const U=await e.hashService.getTemplateHashes(_t(R.path,"cdk.out"));if(U.success){const a=await e.hashService.compareWithState(U.data,R.path);a.success?Ct=a.data.stackChanges.get(k)??!0:t.onLog?.(g(`Org root change detection failed \u2014 deploying to be safe: ${a.error.message}`),"warn")}else t.onLog?.(g(`Org root template hashing failed \u2014 deploying to be safe: ${U.error.message}`),"warn");const rt=Ct||r?.force===!0||!await e.cfnService.stackExists(k);t.onOrgChangesDetected?.({hasOrgChanges:rt});let $;if(rt){t.onStepStart?.(ot,nt,1,m);const a=await e.cdkService.runCdkDeploy(R,k,wt(t),yt(t),e.awsProvider);if(!a.success)return t.onStepComplete?.(ot,nt,"error",1,m),G(t,a.error);$=await lt(e,t,k,"Failed to read org stack outputs (non-critical)");const i=U.success?U.data.get(k):void 0;if(i!==void 0){const l=await e.hashService.updateStateAfterDeploy(R.path,new Map([[k,i]]));l.success||t.onLog?.(`Warning: failed to update state file \u2014 next deploy may re-deploy the org root: ${g(l.error.message)}`,"warn")}t.onStepComplete?.(ot,nt,"completed",1,m)}else t.onLog?.("Organisation root: no infrastructure changes \u2014 skipping deploy","info"),$=await lt(e,t,k,"Failed to read org stack outputs (non-critical)");const C=[],v=[];let q=rt;if(T&&u.length>0){t.onCascadeStart?.();const a=Date.now();let i=2,l=!1,X,at=!1;const J=[],St=s=>({members:J,...X!==void 0?{platform:X}:{},domainsDeployed:at,errors:C,totalDurationMs:s}),{platformAccount:S,memberAccounts:Q}=Nt(u);if(S){const{id:s,name:p}=c.CASCADE_PLATFORM;t.onStepStart?.(s,p,i,m),t.onCascadePhaseStart?.("platform");let d;const F=Date.now();try{d=await Dt(o,e,n,S,"platform",t,{orgConfig:_})}catch(w){const B=g(kt(w));C.push({accountId:S.id,error:B}),t.onCascadePhaseComplete?.("platform"),t.onStepComplete?.(s,p,"error",i,m),d=x(new Error(B))}const A=Date.now()-F;if(d.success){l=!0;const w=d.data.skipped===!0;w||(q=!0),d.data.outputs&&v.push({accountId:S.id,outputs:d.data.outputs}),t.onCascadePhaseComplete?.("platform"),t.onStepComplete?.(s,p,w?"skipped":"completed",i,m),X={accountId:S.id,result:w?"skipped":"succeeded",durationMs:A}}else C.some(w=>w.accountId===S.id)||(C.push({accountId:S.id,error:g(d.error.message)}),t.onCascadePhaseComplete?.("platform"),t.onStepComplete?.(s,p,"error",i,m)),X={accountId:S.id,result:"failed",durationMs:A,error:g(d.error.message)};i++}let At=new Map;if(l&&S&&(At=await Xt(e,S,t)),o.domainProvider){const s=await Jt(o.domainProvider,t);at=s.domainsDeployed>0,at&&(q=!0);for(const p of s.errors)C.push({accountId:"domains",error:g(p)})}if(S!==void 0&&!l&&Q.length>0){const{id:s,name:p}=c.CASCADE_ACCOUNTS;t.onStepStart?.(s,p,i,m),t.onStepComplete?.(s,p,"skipped",i,m),t.onLog?.("Skipping account cascade \u2014 platform deployment failed; platform is a prerequisite for member accounts.","warn")}else if(Q.length>0){const{id:s,name:p}=c.CASCADE_ACCOUNTS;t.onStepStart?.(s,p,i,m),t.onCascadePhaseStart?.("accounts");const d=Qt(o.orgConfig),F=d[0]??E,A=Zt(Q,d);(await ne(A,te,async({account:y,region:j})=>{const b=j.replace(/-/g,""),O=At.get(`${y.id}-${b}`),L=Date.now();return{result:await Dt(o,e,n,y,"account",t,{ipamPoolId:O,orgConfig:_,region:j,skipAccountGlobals:j!==F}),durationMs:Date.now()-L}})).forEach((y,j)=>{const b=A[j];if(!b)return;const O=b.account;if(y.status==="rejected"){const N=g(y.reason instanceof Error?y.reason.message:String(y.reason));J.push({accountId:O.id,accountName:O.name,region:b.region,result:"failed",durationMs:0,error:N}),C.push({accountId:O.id,error:N});return}const{result:L,durationMs:ct}=y.value;if(L.success){const N=L.data.skipped===!0;N||(q=!0),L.data.outputs&&v.push({accountId:O.id,outputs:L.data.outputs}),J.push({accountId:O.id,accountName:O.name,region:b.region,result:N?"skipped":"succeeded",durationMs:ct})}else{const N=g(L.error.message);J.push({accountId:O.id,accountName:O.name,region:b.region,result:"failed",durationMs:ct,error:N}),C.push({accountId:O.id,error:N})}});const B=Tt(St(0));t.onCascadePhaseComplete?.("accounts"),t.onStepComplete?.(s,p,B.accountsFailed>0?"error":B.accountsSkipped===Q.length?"skipped":"completed",i,m)}const Ot=St(Date.now()-a);t.onCascadeComplete?.(Tt(Ot)),t.onCascadeLedger?.(Ot);const it=$?.[Vt];if(it!==void 0&&it!==""){const s=new Map;for(const d of v)(s.get(d.accountId)===void 0||zt in d.outputs)&&s.set(d.accountId,d.outputs);$!==void 0&&s.set(D.data.managementAccountId,$);const p=async(d,F)=>{if(d.id===D.data.managementAccountId)return W({cloudTrailClient:e.awsProvider.getClient(Pt),s3Client:e.awsProvider.getClient(st),kmsClient:e.awsProvider.getClient(Rt)});const A=await jt(e.awsProvider,d.id,F,`fjall-trail-migration-${d.name}`);return A.success?W({cloudTrailClient:A.data.provider.getClient(Pt),s3Client:A.data.provider.getClient(st),kmsClient:A.data.provider.getClient(Rt)}):x(A.error)};try{await Ht({managementS3Client:e.awsProvider.getClient(st),getMemberClients:p},{orgId:D.data.orgId,orgTrailBucketName:it,accounts:u,cascadeOutputs:s,defaultRegion:E,...o.abortSignal!==void 0?{abortSignal:o.abortSignal}:{}},t)}catch(d){t.onLog?.(g(`Org-trail migration reconciliation failed (non-fatal): ${kt(d)}`),"warn")}}if(C.length>0){const s=C.map(p=>` ${p.accountId}: ${p.error}`).join(`
2
+ `);t.onLog?.(g(`Cascade failed for ${C.length} target(s):
3
+ ${s}`),"warn")}}if(C.length>0){const a=C.map(l=>g(`${l.accountId}: ${l.error}`)).join(`
4
+ `),i=new Error(`Organisation root deployed, but the cascade failed for ${C.length} target(s):
5
+ ${a}`);return t.onError?.(i),x(i)}return W({target:n.target,deploymentType:"organisation",outputs:$,...v.length>0?{cascadeOutputs:v}:{},...q?{}:{noChanges:!0},durationMs:Date.now()-f})}export{ye as deployOrganisation};