@fjall/deploy-core 2.14.0 → 2.16.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 (59) hide show
  1. package/dist/.minified +1 -1
  2. package/dist/src/aws/SimpleAwsProvider.js +1 -1
  3. package/dist/src/aws/cloudtrail/orgTrailDelivery.js +1 -1
  4. package/dist/src/aws/organisations/accounts.d.ts +1 -1
  5. package/dist/src/aws/organisations/accounts.js +1 -1
  6. package/dist/src/aws/organisations/backup.d.ts +3 -3
  7. package/dist/src/aws/organisations/backup.js +2 -2
  8. package/dist/src/aws/organisations/costAllocation.d.ts +1 -1
  9. package/dist/src/aws/organisations/costAllocation.js +1 -1
  10. package/dist/src/aws/organisations/delegatedAdmin.d.ts +1 -1
  11. package/dist/src/aws/organisations/delegatedAdmin.js +3 -3
  12. package/dist/src/aws/organisations/identityCentre.d.ts +1 -1
  13. package/dist/src/aws/organisations/identityCentre.js +1 -1
  14. package/dist/src/aws/organisations/ipam.d.ts +1 -1
  15. package/dist/src/aws/organisations/ipam.js +1 -1
  16. package/dist/src/aws/organisations/organisation.d.ts +2 -2
  17. package/dist/src/aws/organisations/organisation.js +1 -1
  18. package/dist/src/aws/organisations/organisationalUnits.d.ts +2 -2
  19. package/dist/src/aws/organisations/organisationalUnits.js +1 -1
  20. package/dist/src/aws/organisations/policies.js +1 -1
  21. package/dist/src/aws/organisations/ram.d.ts +1 -1
  22. package/dist/src/aws/organisations/ram.js +1 -1
  23. package/dist/src/aws/organisations/rootAccess.js +3 -3
  24. package/dist/src/aws/organisations/serviceAccess.d.ts +1 -1
  25. package/dist/src/aws/organisations/serviceAccess.js +1 -1
  26. package/dist/src/aws/organisations/trustedAccess.d.ts +1 -1
  27. package/dist/src/aws/organisations/trustedAccess.js +1 -1
  28. package/dist/src/aws/organisations/types.d.ts +6 -1
  29. package/dist/src/aws/organisations/types.js +1 -1
  30. package/dist/src/index.d.ts +2 -0
  31. package/dist/src/index.js +1 -1
  32. package/dist/src/orchestration/applicationDeploy.js +1 -1
  33. package/dist/src/orchestration/cascadeDestroyHelpers.js +1 -1
  34. package/dist/src/orchestration/cascadeHelpers.js +1 -1
  35. package/dist/src/orchestration/dockerBuildHelper.js +1 -1
  36. package/dist/src/orchestration/index.d.ts +8 -0
  37. package/dist/src/orchestration/index.js +1 -1
  38. package/dist/src/orchestration/organisationDeploy/orgCascadeDeploy.js +4 -4
  39. package/dist/src/orchestration/organisationDeploy/orgContext.d.ts +1 -1
  40. package/dist/src/orchestration/organisationDeploy/orgContext.js +1 -1
  41. package/dist/src/orchestration/organisationDeploy/singleComponentDeploy.js +1 -1
  42. package/dist/src/orchestration/organisationSetup.js +1 -1
  43. package/dist/src/orchestration/secretArnResolver.js +1 -1
  44. package/dist/src/orchestration/stackCleanup/bucketOps.js +1 -1
  45. package/dist/src/orchestration/unlock/bucketPolicyTriage.d.ts +82 -0
  46. package/dist/src/orchestration/unlock/bucketPolicyTriage.js +1 -0
  47. package/dist/src/orchestration/unlock/restoreAndReconcileQuarantinedBucket.d.ts +67 -0
  48. package/dist/src/orchestration/unlock/restoreAndReconcileQuarantinedBucket.js +1 -0
  49. package/dist/src/orchestration/unlock/restoreBucketPolicy.d.ts +43 -0
  50. package/dist/src/orchestration/unlock/restoreBucketPolicy.js +1 -0
  51. package/dist/src/orchestration/unlock/toResourcePolicyStatements.d.ts +20 -0
  52. package/dist/src/orchestration/unlock/toResourcePolicyStatements.js +1 -0
  53. package/dist/src/orchestration/unlock/unlockBucket.d.ts +12 -0
  54. package/dist/src/orchestration/unlock/unlockBucket.js +1 -1
  55. package/dist/src/services/infrastructure/CdkArgumentBuilder.d.ts +15 -4
  56. package/dist/src/services/infrastructure/CdkArgumentBuilder.js +1 -1
  57. package/dist/src/services/infrastructure/CdkCommandRunner.js +2 -2
  58. package/dist/src/services/infrastructure/CdkProcessManager.js +3 -3
  59. package/package.json +4 -4
@@ -19,6 +19,14 @@ export { unlockBucket } from "./unlock/unlockBucket.js";
19
19
  export type { UnlockBucketInput, UnlockBucketReport } from "./unlock/unlockBucket.js";
20
20
  export { unlockQueue } from "./unlock/unlockQueue.js";
21
21
  export type { UnlockQueueInput, UnlockQueueReport } from "./unlock/unlockQueue.js";
22
+ export { triageBucketPolicy, isEnforceSslStatement } from "./unlock/bucketPolicyTriage.js";
23
+ export type { BucketPolicyTriage, ClassifiedStatement, StatementClassification, RawPolicyStatement, RawPolicyDocument } from "./unlock/bucketPolicyTriage.js";
24
+ export { toResourcePolicyStatements } from "./unlock/toResourcePolicyStatements.js";
25
+ export type { ConvertedResourcePolicy } from "./unlock/toResourcePolicyStatements.js";
26
+ export { restoreBucketPolicy, synthesiseEnforceSslDocument, ensureEnforceSsl } from "./unlock/restoreBucketPolicy.js";
27
+ export type { RestoreBucketPolicyInput, RestoreBucketPolicySuccess, RestoreBucketPolicyError } from "./unlock/restoreBucketPolicy.js";
28
+ export { restoreAndReconcileQuarantinedBucket } from "./unlock/restoreAndReconcileQuarantinedBucket.js";
29
+ export type { RestoreAndReconcileInput, RestoreAndReconcileReport, ReconcileOutcome } from "./unlock/restoreAndReconcileQuarantinedBucket.js";
22
30
  export { parseAccountsConfiguration, flattenAccountsToEnvironments, extractAllAccountNames, accountsConfigToOUTree, isStringArray, isAccountsConfig, isOuOnlyAccountBucket, OU_ONLY_ACCOUNT_BUCKETS } from "./accountsConfig.js";
23
31
  export type { AccountsConfig } from "./accountsConfig.js";
24
32
  export type { DockerProvider, DockerProgressCallback, DockerServiceConfig, DockerBuildParams, DockerBuildResult, ECRInitParams, ECRInitResult, TagImagesParams, TagImagesResult, TagByDigestParams } from "./dockerInterface.js";
@@ -1 +1 @@
1
- import{deploy as r}from"./deploy.js";import{destroy as n}from"./destroy.js";import{deployOrganisation as i}from"./organisationDeploy.js";import{destroyOrganisation as s}from"./organisationDestroy.js";import{cleanupFailedStack as p,emptyS3Bucket as m,preEmptyStackBuckets as T,formatQuarantineSuspectedMessage as l,formatRetainedBucketsMessage as f,isQuarantineDetail as A,isRetainedBucketsDetail as _,PRE_EMPTY_TAG_KEYS as d,isCleanableState as x,SAFE_CLEANUP_STATES as S}from"./stackCleanup.js";import{partitionAccounts as O,buildRegionList as g,buildAccountRegionPairs as R,cascadeHomeRegion as U,cascadeOperationKey as P}from"./cascadeHelpers.js";import{projectScalarSummary as y,projectAccountRows as B}from"./cascadeSummary.js";import{reconcileProviderAccounts as K,mergeReconciledProviderAccounts as N}from"./reconcileProviderAccounts.js";import{decideNextTransition as L,reconcileTrailMigration as M,ORG_TRAIL_BUCKET_OUTPUT_KEY as b,TRAIL_BUCKET_OUTPUT_KEY as v,TRAIL_KEY_ARN_OUTPUT_KEY as G}from"./trailMigration/trailMigration.js";import{decommissionMemberTrailStorage as Q}from"./trailMigration/memberTrailCleanup.js";import{unlockBucket as D}from"./unlock/unlockBucket.js";import{unlockQueue as H}from"./unlock/unlockQueue.js";import{parseAccountsConfiguration as h,flattenAccountsToEnvironments as q,extractAllAccountNames as z,accountsConfigToOUTree as J,isStringArray as V,isAccountsConfig as W,isOuOnlyAccountBucket as X,OU_ONLY_ACCOUNT_BUCKETS as Z}from"./accountsConfig.js";import{runOpenNextBuild as ee}from"./openNextBuild.js";import{runOrganisationSetup as re,ORG_SETUP_PHASES as te}from"./organisationSetup.js";export*from"./builders/index.js";export{te as ORG_SETUP_PHASES,b as ORG_TRAIL_BUCKET_OUTPUT_KEY,Z as OU_ONLY_ACCOUNT_BUCKETS,d as PRE_EMPTY_TAG_KEYS,S as SAFE_CLEANUP_STATES,v as TRAIL_BUCKET_OUTPUT_KEY,G as TRAIL_KEY_ARN_OUTPUT_KEY,J as accountsConfigToOUTree,R as buildAccountRegionPairs,g as buildRegionList,U as cascadeHomeRegion,P as cascadeOperationKey,p as cleanupFailedStack,L as decideNextTransition,Q as decommissionMemberTrailStorage,r as deploy,i as deployOrganisation,n as destroy,s as destroyOrganisation,m as emptyS3Bucket,z as extractAllAccountNames,q as flattenAccountsToEnvironments,l as formatQuarantineSuspectedMessage,f as formatRetainedBucketsMessage,W as isAccountsConfig,x as isCleanableState,X as isOuOnlyAccountBucket,A as isQuarantineDetail,_ as isRetainedBucketsDetail,V as isStringArray,N as mergeReconciledProviderAccounts,h as parseAccountsConfiguration,O as partitionAccounts,T as preEmptyStackBuckets,B as projectAccountRows,y as projectScalarSummary,K as reconcileProviderAccounts,M as reconcileTrailMigration,ee as runOpenNextBuild,re as runOrganisationSetup,D as unlockBucket,H as unlockQueue};
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 s}from"./organisationDestroy.js";import{cleanupFailedStack as m,emptyS3Bucket as p,preEmptyStackBuckets as l,formatQuarantineSuspectedMessage as f,formatRetainedBucketsMessage as T,isQuarantineDetail as x,isRetainedBucketsDetail as A,PRE_EMPTY_TAG_KEYS as S,isCleanableState as d,SAFE_CLEANUP_STATES as _}from"./stackCleanup.js";import{partitionAccounts as g,buildRegionList as O,buildAccountRegionPairs as R,cascadeHomeRegion as y,cascadeOperationKey as P}from"./cascadeHelpers.js";import{projectScalarSummary as k,projectAccountRows as B}from"./cascadeSummary.js";import{reconcileProviderAccounts as K,mergeReconciledProviderAccounts as N}from"./reconcileProviderAccounts.js";import{decideNextTransition as L,reconcileTrailMigration as M,ORG_TRAIL_BUCKET_OUTPUT_KEY as b,TRAIL_BUCKET_OUTPUT_KEY as Q,TRAIL_KEY_ARN_OUTPUT_KEY as v}from"./trailMigration/trailMigration.js";import{decommissionMemberTrailStorage as G}from"./trailMigration/memberTrailCleanup.js";import{unlockBucket as j}from"./unlock/unlockBucket.js";import{unlockQueue as H}from"./unlock/unlockQueue.js";import{triageBucketPolicy as w,isEnforceSslStatement as q}from"./unlock/bucketPolicyTriage.js";import{toResourcePolicyStatements as J}from"./unlock/toResourcePolicyStatements.js";import{restoreBucketPolicy as W,synthesiseEnforceSslDocument as X,ensureEnforceSsl as Z}from"./unlock/restoreBucketPolicy.js";import{restoreAndReconcileQuarantinedBucket as ee}from"./unlock/restoreAndReconcileQuarantinedBucket.js";import{parseAccountsConfiguration as te,flattenAccountsToEnvironments as re,extractAllAccountNames as ne,accountsConfigToOUTree as ce,isStringArray as ie,isAccountsConfig as ae,isOuOnlyAccountBucket as se,OU_ONLY_ACCOUNT_BUCKETS as ue}from"./accountsConfig.js";import{runOpenNextBuild as pe}from"./openNextBuild.js";import{runOrganisationSetup as fe,ORG_SETUP_PHASES as Te}from"./organisationSetup.js";export*from"./builders/index.js";export{Te as ORG_SETUP_PHASES,b as ORG_TRAIL_BUCKET_OUTPUT_KEY,ue as OU_ONLY_ACCOUNT_BUCKETS,S as PRE_EMPTY_TAG_KEYS,_ as SAFE_CLEANUP_STATES,Q as TRAIL_BUCKET_OUTPUT_KEY,v as TRAIL_KEY_ARN_OUTPUT_KEY,ce as accountsConfigToOUTree,R as buildAccountRegionPairs,O as buildRegionList,y as cascadeHomeRegion,P as cascadeOperationKey,m as cleanupFailedStack,L as decideNextTransition,G as decommissionMemberTrailStorage,t as deploy,i as deployOrganisation,n as destroy,s as destroyOrganisation,p as emptyS3Bucket,Z as ensureEnforceSsl,ne as extractAllAccountNames,re as flattenAccountsToEnvironments,f as formatQuarantineSuspectedMessage,T as formatRetainedBucketsMessage,ae as isAccountsConfig,d as isCleanableState,q as isEnforceSslStatement,se as isOuOnlyAccountBucket,x as isQuarantineDetail,A as isRetainedBucketsDetail,ie as isStringArray,N as mergeReconciledProviderAccounts,te as parseAccountsConfiguration,g as partitionAccounts,l as preEmptyStackBuckets,B as projectAccountRows,k as projectScalarSummary,K as reconcileProviderAccounts,M as reconcileTrailMigration,ee as restoreAndReconcileQuarantinedBucket,W as restoreBucketPolicy,pe as runOpenNextBuild,fe as runOrganisationSetup,X as synthesiseEnforceSslDocument,J as toResourcePolicyStatements,w as triageBucketPolicy,j as unlockBucket,H as unlockQueue};
@@ -1,5 +1,5 @@
1
- import{join as W}from"node:path";import{success as _,failure as Y}from"@fjall/generator";import{ORGANISATION_TYPES as B,getOrganisationStackName as K}from"../../types/operations.js";import{synthOrFail as q,bootstrapOrFail as z,forwardOutput as J,forwardResourceProgress as Q}from"../contextHelpers.js";import{partitionAccounts as U,probeCascadeRoles as V}from"../cascadeHelpers.js";import{accountTier as X,maskSensitiveOutput as l}from"@fjall/util";import{buildOrgContext as Z,resolveOrgDetails as v}from"./orgContext.js";import{INFRA_STEPS as L,maskAndFail as R,readStackOutputsBestEffort as x}from"./infraSteps.js";import{resolveCascadeAccounts as ee}from"./resolveCascadeAccounts.js";import{executeCascade as te}from"./cascadeExecution.js";import{reconcileOrgTrailOutputs as oe}from"./trailReconciliation.js";async function ge(i,t,O,H){const{callbacks:e,options:$}=i,{providerAccounts:u,effectiveOrgConfig:M,defaultRegion:j}=await ee(i,t),g=await v(t);if(!g.success)return R(e,g.error.message);const G=u.find(o=>X(o)==="organisation"),d=Z(i,t,O,"organisation",g.data,void 0,G?.trailLifecycle),m=$?.cascade!==!1,{platformAccount:p,memberAccounts:k}=U(u),P=m&&p!==void 0?1:0,D=m&&k.length>0?1:0,n=2+P+D,I=m?[...p!==void 0?[p]:[],...k]:[];if(I.length>0){const o=await V(t,I,e,i.abortSignal),r=p!==void 0?o.find(a=>a.accountId===p.id):void 0;if(r!==void 0)return R(e,`Pre-flight cascade role check failed for the platform account ${r.accountName} (${r.accountId}): ${r.error}`);for(const a of o)e.onProgress?.({type:"warning",message:l(`Pre-flight cascade role check failed for ${a.accountName} (${a.accountId}) \u2014 its cascade deploy is expected to fail: ${a.error}`)})}e.onCascadeAccountsReconciled?.({hasPlatformAccount:P>0,hasMemberAccounts:D>0});const{id:h,name:S}=L.PREPARE;e.onStepStart?.(h,S,0,n),e.onLog?.("Synthesising organisation infrastructure\u2026","info");const E=await q(t,d,e,"CDK synthesis failed");if(!E.success)return e.onStepComplete?.(h,S,"error",0,n),E;const N=await z(t,d,e);if(!N.success)return e.onStepComplete?.(h,S,"error",0,n),N;e.onStepComplete?.(h,S,"completed",0,n);const{id:C,name:A}=L.ORG_DEPLOY,s=K(B.ORGANISATION);let F=!0;const f=await t.hashService.getTemplateHashes(W(d.path,"cdk.out"));if(f.success){const o=await t.hashService.compareWithState(f.data,d.path);o.success?F=o.data.stackChanges.get(s)??!0:e.onLog?.(l(`Org root change detection failed \u2014 deploying to be safe: ${o.error.message}`),"warn")}else e.onLog?.(l(`Org root template hashing failed \u2014 deploying to be safe: ${f.error.message}`),"warn");const b=F||$?.force===!0||!await t.cfnService.stackExists(s);e.onOrgChangesDetected?.({hasOrgChanges:b});let w;if(b){e.onStepStart?.(C,A,1,n);const o=await t.cdkService.runCdkDeploy(d,s,J(e),Q(e),t.awsProvider);if(!o.success)return e.onStepComplete?.(C,A,"error",1,n),R(e,o.error);w=await x(t,e,s,"Failed to read org stack outputs (non-critical)");const r=f.success?f.data.get(s):void 0;if(r!==void 0){const a=await t.hashService.updateStateAfterDeploy(d.path,new Map([[s,r]]));a.success||e.onLog?.(`Warning: failed to update state file \u2014 next deploy may re-deploy the org root: ${l(a.error.message)}`,"warn")}e.onStepComplete?.(C,A,"completed",1,n)}else e.onLog?.("Organisation root: no infrastructure changes \u2014 skipping deploy","info"),w=await x(t,e,s,"Failed to read org stack outputs (non-critical)");const c=[],y=[];let T=b;if(m&&u.length>0){const{anyCascadeDeployHappened:o}=await te(i,t,O,{providerAccounts:u,effectiveOrgConfig:M,totalSteps:n,cascadeErrors:c,allCascadeOutputs:y});if(o&&(T=!0),await oe(i,t,{orgOutputs:w,allCascadeOutputs:y,orgDetails:g.data,providerAccounts:u,defaultRegion:j}),c.length>0){const r=c.map(a=>` ${a.accountId}: ${a.error}`).join(`
2
- `);e.onLog?.(l(`Cascade failed for ${c.length} target(s):
3
- ${r}`),"warn")}}if(c.length>0){const o=c.map(a=>l(`${a.accountId}: ${a.error}`)).join(`
4
- `),r=new Error(`Organisation root deployed, but the cascade failed for ${c.length} target(s):
1
+ import{join as W}from"node:path";import{success as _,failure as Y}from"@fjall/generator";import{ORGANISATION_TYPES as B,getOrganisationStackName as K}from"../../types/operations.js";import{synthOrFail as q,bootstrapOrFail as z,forwardOutput as J,forwardResourceProgress as Q}from"../contextHelpers.js";import{partitionAccounts as U,probeCascadeRoles as V}from"../cascadeHelpers.js";import{accountTier as X,maskSensitiveOutput as l}from"@fjall/util";import{buildOrgContext as Z,resolveOrgDetails as v}from"./orgContext.js";import{INFRA_STEPS as L,maskAndFail as R,readStackOutputsBestEffort as x}from"./infraSteps.js";import{resolveCascadeAccounts as ee}from"./resolveCascadeAccounts.js";import{executeCascade as te}from"./cascadeExecution.js";import{reconcileOrgTrailOutputs as oe}from"./trailReconciliation.js";async function ge(s,t,O,H){const{callbacks:e,options:$}=s,{providerAccounts:u,effectiveOrgConfig:M,defaultRegion:j}=await ee(s,t),g=await v(t,s.abortSignal);if(!g.success)return R(e,g.error.message);const G=u.find(o=>X(o)==="organisation"),d=Z(s,t,O,"organisation",g.data,void 0,G?.trailLifecycle),m=$?.cascade!==!1,{platformAccount:p,memberAccounts:k}=U(u),P=m&&p!==void 0?1:0,D=m&&k.length>0?1:0,n=2+P+D,I=m?[...p!==void 0?[p]:[],...k]:[];if(I.length>0){const o=await V(t,I,e,s.abortSignal),r=p!==void 0?o.find(a=>a.accountId===p.id):void 0;if(r!==void 0)return R(e,`Pre-flight cascade role check failed for the platform account ${r.accountName} (${r.accountId}): ${r.error}`);for(const a of o)e.onProgress?.({type:"warning",message:l(`Pre-flight cascade role check failed for ${a.accountName} (${a.accountId}) \u2014 its cascade deploy is expected to fail: ${a.error}`)})}e.onCascadeAccountsReconciled?.({hasPlatformAccount:P>0,hasMemberAccounts:D>0});const{id:h,name:S}=L.PREPARE;e.onStepStart?.(h,S,0,n),e.onLog?.("Synthesising organisation infrastructure\u2026","info");const E=await q(t,d,e,"CDK synthesis failed");if(!E.success)return e.onStepComplete?.(h,S,"error",0,n),E;const N=await z(t,d,e);if(!N.success)return e.onStepComplete?.(h,S,"error",0,n),N;e.onStepComplete?.(h,S,"completed",0,n);const{id:C,name:b}=L.ORG_DEPLOY,c=K(B.ORGANISATION);let F=!0;const f=await t.hashService.getTemplateHashes(W(d.path,"cdk.out"));if(f.success){const o=await t.hashService.compareWithState(f.data,d.path);o.success?F=o.data.stackChanges.get(c)??!0:e.onLog?.(l(`Org root change detection failed \u2014 deploying to be safe: ${o.error.message}`),"warn")}else e.onLog?.(l(`Org root template hashing failed \u2014 deploying to be safe: ${f.error.message}`),"warn");const A=F||$?.force===!0||!await t.cfnService.stackExists(c);e.onOrgChangesDetected?.({hasOrgChanges:A});let w;if(A){e.onStepStart?.(C,b,1,n);const o=await t.cdkService.runCdkDeploy(d,c,J(e),Q(e),t.awsProvider);if(!o.success)return e.onStepComplete?.(C,b,"error",1,n),R(e,o.error);w=await x(t,e,c,"Failed to read org stack outputs (non-critical)");const r=f.success?f.data.get(c):void 0;if(r!==void 0){const a=await t.hashService.updateStateAfterDeploy(d.path,new Map([[c,r]]));a.success||e.onLog?.(`Warning: failed to update state file \u2014 next deploy may re-deploy the org root: ${l(a.error.message)}`,"warn")}e.onStepComplete?.(C,b,"completed",1,n)}else e.onLog?.("Organisation root: no infrastructure changes \u2014 skipping deploy","info"),w=await x(t,e,c,"Failed to read org stack outputs (non-critical)");const i=[],y=[];let T=A;if(m&&u.length>0){const{anyCascadeDeployHappened:o}=await te(s,t,O,{providerAccounts:u,effectiveOrgConfig:M,totalSteps:n,cascadeErrors:i,allCascadeOutputs:y});if(o&&(T=!0),await oe(s,t,{orgOutputs:w,allCascadeOutputs:y,orgDetails:g.data,providerAccounts:u,defaultRegion:j}),i.length>0){const r=i.map(a=>` ${a.accountId}: ${a.error}`).join(`
2
+ `);e.onLog?.(l(`Cascade failed for ${i.length} target(s):
3
+ ${r}`),"warn")}}if(i.length>0){const o=i.map(a=>l(`${a.accountId}: ${a.error}`)).join(`
4
+ `),r=new Error(`Organisation root deployed, but the cascade failed for ${i.length} target(s):
5
5
  ${o}`);return e.onError?.(r),Y(r)}return _({target:O.target,deploymentType:"organisation",outputs:w,...y.length>0?{cascadeOutputs:y}:{},...T?{}:{noChanges:!0},durationMs:Date.now()-H})}export{ge as deployOrgWithCascade};
@@ -9,4 +9,4 @@ export interface OrgDetailsForSynth {
9
9
  managementAccountId: string;
10
10
  }
11
11
  export declare function buildOrgContext(params: DeployParams, services: DeployServices, operation: OrganisationOperation, deployType: "organisation" | "platform" | "account", orgDetails: OrgDetailsForSynth, accountName?: string, trailLifecycle?: ProviderAccount["trailLifecycle"]): import("../../types/deployment/DeploymentTypes.js").DeploymentContext;
12
- export declare function resolveOrgDetails(services: DeployServices): Promise<Result<OrgDetailsForSynth>>;
12
+ export declare function resolveOrgDetails(services: DeployServices, abortSignal?: AbortSignal): Promise<Result<OrgDetailsForSynth>>;
@@ -1 +1 @@
1
- import{success as a,failure as c}from"@fjall/generator";import{OrganizationsClient as s}from"@aws-sdk/client-organizations";import{CdkContextBuilder as u}from"../../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as m}from"../../types/deployment/index.js";import{ensureOrganisationExists as f}from"../../aws/organisations/organisation.js";import{buildParamsContext as l}from"../contextHelpers.js";function b(t,n,o,i,r,d,g){const e=n.awsProvider.getRegion();return u.buildDeploymentContext({deployType:i,target:o.target,path:o.path,region:e,accountName:d,callerIdentity:m(n.awsProvider.getAccountId()),orgId:r.orgId,rootId:r.rootId,managementAccountId:r.managementAccountId,...l({orgConfig:t.orgConfig,identity:t.identity,skipOidc:t.options?.skipOidc,...i!=="organisation"?{region:e,primaryRegion:t.orgConfig?.primaryRegion}:{},trailLifecycle:g})},{verbose:t.options?.verbose,infraOnly:t.options?.infraOnly},t.orgConfig)}async function v(t){const n=t.awsProvider.getClient(s),o=await f(n);return o.success?a({orgId:o.data.orgId,rootId:o.data.rootId,managementAccountId:o.data.managementAccountId}):c(o.error)}export{b as buildOrgContext,v as resolveOrgDetails};
1
+ import{success as a,failure as c}from"@fjall/generator";import{OrganizationsClient as s}from"@aws-sdk/client-organizations";import{CdkContextBuilder as u}from"../../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as m}from"../../types/deployment/index.js";import{ensureOrganisationExists as f}from"../../aws/organisations/organisation.js";import{buildParamsContext as l}from"../contextHelpers.js";function b(t,n,r,o,i,d,g){const e=n.awsProvider.getRegion();return u.buildDeploymentContext({deployType:o,target:r.target,path:r.path,region:e,accountName:d,callerIdentity:m(n.awsProvider.getAccountId()),orgId:i.orgId,rootId:i.rootId,managementAccountId:i.managementAccountId,...l({orgConfig:t.orgConfig,identity:t.identity,skipOidc:t.options?.skipOidc,...o!=="organisation"?{region:e,primaryRegion:t.orgConfig?.primaryRegion}:{},trailLifecycle:g})},{verbose:t.options?.verbose,infraOnly:t.options?.infraOnly},t.orgConfig)}async function v(t,n){const r=t.awsProvider.getClient(s),o=await f(r,n);return o.success?a({orgId:o.data.orgId,rootId:o.data.rootId,managementAccountId:o.data.managementAccountId}):c(o.error)}export{b as buildOrgContext,v as resolveOrgDetails};
@@ -1 +1 @@
1
- import{success as A}from"@fjall/generator";import{BackupClient as N}from"@aws-sdk/client-backup";import{IAMClient as b}from"@aws-sdk/client-iam";import{getOrganisationStackName as w}from"../../types/operations.js";import{accountHasDisasterRecovery as D,describeBackupVaultExists as k}from"../../aws/organisations/backup.js";import{describeAccountGlobalsExist as y,buildMissingAccountGlobalsAdvisory as I}from"../../aws/organisations/accountGlobals.js";import{targetsNonPrimaryRegion as L,synthOrFail as T,bootstrapOrFail as h,forwardOutput as M,forwardResourceProgress as v}from"../contextHelpers.js";import{accountTier as F}from"@fjall/util";import{buildOrgContext as G,resolveOrgDetails as Y}from"./orgContext.js";import{INFRA_STEPS as o,INFRA_STEP_TOTAL as i,failPrepareStep as s,maskAndFail as u,readStackOutputsBestEffort as x}from"./infraSteps.js";async function W(n,r,a,c,E){const{callbacks:t}=n;t.onStepComplete?.(o.CONNECT.id,o.CONNECT.name,"completed",0,i),t.onStepStart?.(o.PREPARE.id,o.PREPARE.name,1,i);const p=r.awsProvider.getRegion(),l=n.orgConfig?.primaryRegion;if(l!==void 0&&L(p,l)){const e=await y(r.awsProvider.getClient(b),n.abortSignal);if(!e.success)return s(t),u(t,e.error.message);if(!e.data)return s(t),u(t,I({target:a.target,deployRegion:p,primaryRegion:l,...n.orgConfig!==void 0?{providerAccounts:n.orgConfig.providerAccounts}:{}}))}const g=await Y(r);if(!g.success)return s(t),u(t,g.error.message);const f=c==="account"?n.orgConfig?.providerAccounts.find(e=>e.name===a.target):n.orgConfig?.providerAccounts.find(e=>F(e)==="platform"),d=G(n,r,a,c,g.data,c==="account"?a.target:void 0,f?.trailLifecycle),m=n.orgConfig?.disasterRecoveryRegion;if(c==="account"&&(m!==void 0&&m!=="")&&(f===void 0||D(f.environment,m))){const e=await k(r.awsProvider.getClient(N));if(!e.success)return s(t),u(t,e.error.message);d.fjallAdoptBackupVault=e.data}t.onLog?.(`Synthesising ${c} infrastructure\u2026`,"info");const R=await T(r,d,t,"CDK synthesis failed");if(!R.success)return s(t),R;const P=await h(r,d,t);if(!P.success)return s(t),P;t.onStepComplete?.(o.PREPARE.id,o.PREPARE.name,"completed",1,i);const C=w(a.type);t.onStepStart?.(o.DEPLOY.id,o.DEPLOY.name,2,i);const O=await r.cdkService.runCdkDeploy(d,C,M(t),v(t),r.awsProvider);if(!O.success)return t.onStepComplete?.(o.DEPLOY.id,o.DEPLOY.name,"error",2,i),u(t,O.error);t.onStepComplete?.(o.DEPLOY.id,o.DEPLOY.name,"completed",2,i);const S=await x(r,t,C,"Failed to read stack outputs (non-critical)");return t.onStepStart?.(o.MONITORING.id,o.MONITORING.name,3,i),t.onStepComplete?.(o.MONITORING.id,o.MONITORING.name,"completed",3,i),A({target:a.target,deploymentType:"organisation",outputs:S,durationMs:Date.now()-E})}export{W as deploySingleComponent};
1
+ import{success as A}from"@fjall/generator";import{BackupClient as b}from"@aws-sdk/client-backup";import{IAMClient as N}from"@aws-sdk/client-iam";import{getOrganisationStackName as w}from"../../types/operations.js";import{accountHasDisasterRecovery as D,describeBackupVaultExists as k}from"../../aws/organisations/backup.js";import{describeAccountGlobalsExist as y,buildMissingAccountGlobalsAdvisory as I}from"../../aws/organisations/accountGlobals.js";import{targetsNonPrimaryRegion as L,synthOrFail as T,bootstrapOrFail as h,forwardOutput as M,forwardResourceProgress as v}from"../contextHelpers.js";import{accountTier as F}from"@fjall/util";import{buildOrgContext as G,resolveOrgDetails as Y}from"./orgContext.js";import{INFRA_STEPS as o,INFRA_STEP_TOTAL as i,failPrepareStep as c,maskAndFail as u,readStackOutputsBestEffort as x}from"./infraSteps.js";async function W(e,n,a,s,S){const{callbacks:t}=e;t.onStepComplete?.(o.CONNECT.id,o.CONNECT.name,"completed",0,i),t.onStepStart?.(o.PREPARE.id,o.PREPARE.name,1,i);const p=n.awsProvider.getRegion(),l=e.orgConfig?.primaryRegion;if(l!==void 0&&L(p,l)){const r=await y(n.awsProvider.getClient(N),e.abortSignal);if(!r.success)return c(t),u(t,r.error.message);if(!r.data)return c(t),u(t,I({target:a.target,deployRegion:p,primaryRegion:l,...e.orgConfig!==void 0?{providerAccounts:e.orgConfig.providerAccounts}:{}}))}const g=await Y(n,e.abortSignal);if(!g.success)return c(t),u(t,g.error.message);const f=s==="account"?e.orgConfig?.providerAccounts.find(r=>r.name===a.target):e.orgConfig?.providerAccounts.find(r=>F(r)==="platform"),d=G(e,n,a,s,g.data,s==="account"?a.target:void 0,f?.trailLifecycle),m=e.orgConfig?.disasterRecoveryRegion;if(s==="account"&&(m!==void 0&&m!=="")&&(f===void 0||D(f.environment,m))){const r=await k(n.awsProvider.getClient(b),e.abortSignal);if(!r.success)return c(t),u(t,r.error.message);d.fjallAdoptBackupVault=r.data}t.onLog?.(`Synthesising ${s} infrastructure\u2026`,"info");const R=await T(n,d,t,"CDK synthesis failed");if(!R.success)return c(t),R;const P=await h(n,d,t);if(!P.success)return c(t),P;t.onStepComplete?.(o.PREPARE.id,o.PREPARE.name,"completed",1,i);const C=w(a.type);t.onStepStart?.(o.DEPLOY.id,o.DEPLOY.name,2,i);const O=await n.cdkService.runCdkDeploy(d,C,M(t),v(t),n.awsProvider);if(!O.success)return t.onStepComplete?.(o.DEPLOY.id,o.DEPLOY.name,"error",2,i),u(t,O.error);t.onStepComplete?.(o.DEPLOY.id,o.DEPLOY.name,"completed",2,i);const E=await x(n,t,C,"Failed to read stack outputs (non-critical)");return t.onStepStart?.(o.MONITORING.id,o.MONITORING.name,3,i),t.onStepComplete?.(o.MONITORING.id,o.MONITORING.name,"completed",3,i),A({target:a.target,deploymentType:"organisation",outputs:E,durationMs:Date.now()-S})}export{W as deploySingleComponent};
@@ -1 +1 @@
1
- import{success as y,failure as h}from"@fjall/generator";import{OrganizationsClient as K}from"@aws-sdk/client-organizations";import{RAMClient as $}from"@aws-sdk/client-ram";import{CloudFormationClient as B}from"@aws-sdk/client-cloudformation";import{EC2Client as G}from"@aws-sdk/client-ec2";import{IAMClient as _}from"@aws-sdk/client-iam";import{BackupClient as z}from"@aws-sdk/client-backup";import{CostExplorerClient as H}from"@aws-sdk/client-cost-explorer";import{SSOAdminClient as q}from"@aws-sdk/client-sso-admin";import{ensureOrganisationExists as J}from"../aws/organisations/organisation.js";import{enablePolicyTypes as Q}from"../aws/organisations/policies.js";import{enableServiceAccess as V}from"../aws/organisations/serviceAccess.js";import{enableRamSharing as X}from"../aws/organisations/ram.js";import{activateTrustedAccess as Y}from"../aws/organisations/trustedAccess.js";import{enableIpamDelegatedAdmin as Z}from"../aws/organisations/ipam.js";import{updateBackupGlobalSettings as b}from"../aws/organisations/backup.js";import{listAccounts as w,createAccount as k}from"../aws/organisations/accounts.js";import{ensureOrganisationalUnitsExist as ee,placeAccountsInOUs as te,buildAccountToOUMap as ne}from"../aws/organisations/organisationalUnits.js";import{activateCostAllocationTags as oe}from"../aws/organisations/costAllocation.js";import{checkIdentityCentreStatus as re}from"../aws/organisations/identityCentre.js";import{registerSecurityDelegates as se}from"../aws/organisations/delegatedAdmin.js";import{enableCentralisedRootAccess as ae}from"../aws/organisations/rootAccess.js";import{selectImportedMemberAccounts as ie,formatImportedAccountsWarning as ce}from"../aws/organisations/importedAccounts.js";import{isOULeaf as ue}from"../aws/organisations/types.js";const Ke=["create-organisation","enable-policies","enable-service-access","enable-root-access","enable-ram-sharing","activate-trusted-access","enable-ipam","configure-backup","create-accounts","create-organisational-units","place-accounts","activate-cost-tags","check-identity-centre","register-security-delegates"];async function $e(t,n,e,a){const o=[],s=[],r=[],m=[];let i;const u=t.getClient(K),M=t.getClient(_),x=t.getClient($),N=t.getClient(B),L=t.getClient(G),W=t.getClient(z),F=t.getClient(H),j=t.getClient(q);e?.onPhaseStart?.("create-organisation"),e?.onProgress?.("Ensuring AWS Organisation exists");const l=await J(u);if(!l.success)return e?.onError?.("create-organisation",l.error),e?.onPhaseComplete?.("create-organisation","error"),h(l.error);const{orgId:D,rootId:I,managementAccountId:S}=l.data;if(e?.onPhaseComplete?.("create-organisation","completed"),o.push("create-organisation"),await p("enable-policies",()=>(e?.onProgress?.("Enabling organisation policy types"),Q(u,I,{abortSignal:a})),o,r,e),await p("enable-service-access",()=>(e?.onProgress?.("Enabling AWS service access"),V(u)),o,r,e),n.skipRootAccessManagement?d("enable-root-access",s,e):await p("enable-root-access",async()=>{e?.onProgress?.("Enabling centralised root access management");const c=await ae(M,a);return(c.success?c.data.enabled:c.error.partialSummary.enabled).length>0&&await me(u,S,e),c},o,r,e),await p("enable-ram-sharing",()=>(e?.onProgress?.("Enabling RAM sharing"),X(x)),o,r,e),await p("activate-trusted-access",()=>(e?.onProgress?.("Activating CloudFormation trusted access"),Y(N)),o,r,e),n.platformAccountId){const c=n.platformAccountId;await p("enable-ipam",()=>(e?.onProgress?.("Enabling IPAM delegated administrator"),Z(L,c)),o,r,e)}else d("enable-ipam",s,e);await p("configure-backup",()=>(e?.onProgress?.("Updating backup global settings"),b(W)),o,r,e),e?.onPhaseStart?.("create-accounts"),e?.onProgress?.("Checking for missing accounts");const P=await pe(u,n.accounts,m,e,a);let E=[];P.success?(E=P.data,o.push("create-accounts"),e?.onPhaseComplete?.("create-accounts","completed")):C("create-accounts",P.error,r,e);let f={};e?.onPhaseStart?.("create-organisational-units"),e?.onProgress?.("Ensuring organisational units exist");const A=await ee(u,I,n.organisationalUnits);A.success?(f=A.data,o.push("create-organisational-units"),e?.onPhaseComplete?.("create-organisational-units","completed")):C("create-organisational-units",A.error,r,e);const g=Array.isArray(n.organisationalUnits)?void 0:n.organisationalUnits,R=n.accountPlacements??[],O=R.length===0&&g!==void 0?de(g,E,S):R;if(Object.keys(f).length===0)d("place-accounts",s,e);else if(n.accountPlacements===void 0&&g===void 0){const c=new Error("Account placements not provided despite OUs being created. Caller must populate accountPlacements (flat-list mode cannot derive placements internally).");r.push({phase:"place-accounts",error:c.message}),e?.onPhaseStart?.("place-accounts"),e?.onError?.("place-accounts",c),e?.onPhaseComplete?.("place-accounts","error")}else if(O.length===0)d("place-accounts",s,e);else{const c=g?ne(g,f):void 0;await p("place-accounts",()=>(e?.onProgress?.("Placing accounts in organisational units"),te(u,f,O,c)),o,r,e)}const v=n.costAllocationTags??[];if(v.length>0?await p("activate-cost-tags",()=>(e?.onProgress?.("Activating cost allocation tags"),oe(F,v.map(c=>({TagKey:c})))),o,r,e):d("activate-cost-tags",s,e),n.skipIdentityCentre)d("check-identity-centre",s,e);else{e?.onPhaseStart?.("check-identity-centre"),e?.onProgress?.("Checking Identity Centre status");const c=await re(j);c.success?(i=c.data.enabled?"enabled":"not-enabled",o.push("check-identity-centre"),e?.onPhaseComplete?.("check-identity-centre","completed")):C("check-identity-centre",c.error,r,e)}const T=n.securityDelegateAccountId;return T?await p("register-security-delegates",()=>(e?.onProgress?.("Registering security service delegated administrators"),se(u,T)),o,r,e):d("register-security-delegates",s,e),y({organisationId:D,createdAccounts:m,identityCentreStatus:i,phasesCompleted:o,phasesSkipped:s,errors:r})}function C(t,n,e,a){e.push({phase:t,error:n.message}),a?.onError?.(t,n),a?.onPhaseComplete?.(t,"error")}function d(t,n,e){n.push(t),e?.onPhaseStart?.(t),e?.onPhaseComplete?.(t,"skipped")}async function me(t,n,e){const a=await w(t);if(!a.success){e?.onProgress?.(`Could not check for imported member accounts: ${a.error.message}`);return}const o=ie(a.data,n);if(o.length===0)return;const s=ce(o);e?.onWarning?e.onWarning("enable-root-access",s):e?.onProgress?.(s)}async function p(t,n,e,a,o){o?.onPhaseStart?.(t);const s=await n();s.success?(e.push(t),o?.onPhaseComplete?.(t,"completed")):C(t,s.error,a,o)}async function pe(t,n,e,a,o){const s=await w(t);if(!s.success)return h(s.error);const r=new Set(s.data.map(i=>i.Name?.toLowerCase()).filter(i=>i!==void 0));for(const i of n){if(r.has(i.name.toLowerCase()))continue;a?.onProgress?.(`Account "${i.name}" is not yet a member of this AWS Organisation. Fjall will create a new account (with a new account ID). If "${i.name}" already exists as a standalone Fjall-connected account, abort now and import it into the organisation instead, because creating here produces a duplicate account. See the account-import runbook.`);const u=await k(t,i.name,i.email,void 0,void 0,o);if(!u.success)return h(u.error);e.push({name:u.data.accountName,accountId:u.data.accountId})}if(e.length===0)return y(s.data);const m=await w(t);return m.success?y(m.data):h(m.error)}function U(t){const n=[];for(const[e,a]of Object.entries(t))if(ue(a))for(const o of a)n.push({accountName:o,placementKey:e});else n.push(...U(a));return n}function de(t,n,e){const a=U(t),o=new Map;for(const r of n){const m=r.Name?.toLowerCase();m&&r.Id&&o.set(m,r)}const s=[];for(const{accountName:r,placementKey:m}of a){const i=o.get(r.toLowerCase());!i?.Id||!i.Name||e!==void 0&&i.Id===e||s.push({id:i.Id,name:i.Name,environment:m})}return s}export{Ke as ORG_SETUP_PHASES,$e as runOrganisationSetup};
1
+ import{success as y,failure as h}from"@fjall/generator";import{OrganizationsClient as K}from"@aws-sdk/client-organizations";import{RAMClient as $}from"@aws-sdk/client-ram";import{CloudFormationClient as B}from"@aws-sdk/client-cloudformation";import{EC2Client as G}from"@aws-sdk/client-ec2";import{IAMClient as _}from"@aws-sdk/client-iam";import{BackupClient as z}from"@aws-sdk/client-backup";import{CostExplorerClient as H}from"@aws-sdk/client-cost-explorer";import{SSOAdminClient as q}from"@aws-sdk/client-sso-admin";import{ensureOrganisationExists as J}from"../aws/organisations/organisation.js";import{enablePolicyTypes as Q}from"../aws/organisations/policies.js";import{enableServiceAccess as V}from"../aws/organisations/serviceAccess.js";import{enableRamSharing as X}from"../aws/organisations/ram.js";import{activateTrustedAccess as Y}from"../aws/organisations/trustedAccess.js";import{enableIpamDelegatedAdmin as Z}from"../aws/organisations/ipam.js";import{updateBackupGlobalSettings as b}from"../aws/organisations/backup.js";import{listAccounts as w,createAccount as k}from"../aws/organisations/accounts.js";import{ensureOrganisationalUnitsExist as ee,placeAccountsInOUs as te,buildAccountToOUMap as ne}from"../aws/organisations/organisationalUnits.js";import{activateCostAllocationTags as oe}from"../aws/organisations/costAllocation.js";import{checkIdentityCentreStatus as re}from"../aws/organisations/identityCentre.js";import{registerSecurityDelegates as se}from"../aws/organisations/delegatedAdmin.js";import{enableCentralisedRootAccess as ie}from"../aws/organisations/rootAccess.js";import{selectImportedMemberAccounts as ae,formatImportedAccountsWarning as ce}from"../aws/organisations/importedAccounts.js";import{isOULeaf as ue}from"../aws/organisations/types.js";const Ke=["create-organisation","enable-policies","enable-service-access","enable-root-access","enable-ram-sharing","activate-trusted-access","enable-ipam","configure-backup","create-accounts","create-organisational-units","place-accounts","activate-cost-tags","check-identity-centre","register-security-delegates"];async function $e(n,r,e,o){const t=[],i=[],s=[],m=[];let a;const u=n.getClient(K),x=n.getClient(_),N=n.getClient($),S=n.getClient(B),L=n.getClient(G),W=n.getClient(z),F=n.getClient(H),j=n.getClient(q);e?.onPhaseStart?.("create-organisation"),e?.onProgress?.("Ensuring AWS Organisation exists");const f=await J(u,o);if(!f.success)return e?.onError?.("create-organisation",f.error),e?.onPhaseComplete?.("create-organisation","error"),h(f.error);const{orgId:D,rootId:I,managementAccountId:E}=f.data;if(e?.onPhaseComplete?.("create-organisation","completed"),t.push("create-organisation"),await d("enable-policies",()=>(e?.onProgress?.("Enabling organisation policy types"),Q(u,I,{abortSignal:o})),t,s,e),await d("enable-service-access",()=>(e?.onProgress?.("Enabling AWS service access"),V(u,o)),t,s,e),r.skipRootAccessManagement?p("enable-root-access",i,e):await d("enable-root-access",async()=>{e?.onProgress?.("Enabling centralised root access management");const c=await ie(x,o);return(c.success?c.data.enabled:c.error.partialSummary.enabled).length>0&&await me(u,E,e,o),c},t,s,e),await d("enable-ram-sharing",()=>(e?.onProgress?.("Enabling RAM sharing"),X(N,o)),t,s,e),await d("activate-trusted-access",()=>(e?.onProgress?.("Activating CloudFormation trusted access"),Y(S,o)),t,s,e),r.platformAccountId){const c=r.platformAccountId;await d("enable-ipam",()=>(e?.onProgress?.("Enabling IPAM delegated administrator"),Z(L,c,o)),t,s,e)}else p("enable-ipam",i,e);await d("configure-backup",()=>(e?.onProgress?.("Updating backup global settings"),b(W,void 0,o)),t,s,e),e?.onPhaseStart?.("create-accounts"),e?.onProgress?.("Checking for missing accounts");const P=await de(u,r.accounts,m,e,o);let R=[];P.success?(R=P.data,t.push("create-accounts"),e?.onPhaseComplete?.("create-accounts","completed")):C("create-accounts",P.error,s,e);let l={};e?.onPhaseStart?.("create-organisational-units"),e?.onProgress?.("Ensuring organisational units exist");const A=await ee(u,I,r.organisationalUnits,o);A.success?(l=A.data,t.push("create-organisational-units"),e?.onPhaseComplete?.("create-organisational-units","completed")):C("create-organisational-units",A.error,s,e);const g=Array.isArray(r.organisationalUnits)?void 0:r.organisationalUnits,O=r.accountPlacements??[],v=O.length===0&&g!==void 0?pe(g,R,E):O;if(Object.keys(l).length===0)p("place-accounts",i,e);else if(r.accountPlacements===void 0&&g===void 0){const c=new Error("Account placements not provided despite OUs being created. Caller must populate accountPlacements (flat-list mode cannot derive placements internally).");s.push({phase:"place-accounts",error:c.message}),e?.onPhaseStart?.("place-accounts"),e?.onError?.("place-accounts",c),e?.onPhaseComplete?.("place-accounts","error")}else if(v.length===0)p("place-accounts",i,e);else{const c=g?ne(g,l):void 0;await d("place-accounts",()=>(e?.onProgress?.("Placing accounts in organisational units"),te(u,l,v,c,o)),t,s,e)}const T=r.costAllocationTags??[];if(T.length>0?await d("activate-cost-tags",()=>(e?.onProgress?.("Activating cost allocation tags"),oe(F,T.map(c=>({TagKey:c})),o)),t,s,e):p("activate-cost-tags",i,e),r.skipIdentityCentre)p("check-identity-centre",i,e);else{e?.onPhaseStart?.("check-identity-centre"),e?.onProgress?.("Checking Identity Centre status");const c=await re(j,o);c.success?(a=c.data.enabled?"enabled":"not-enabled",t.push("check-identity-centre"),e?.onPhaseComplete?.("check-identity-centre","completed")):C("check-identity-centre",c.error,s,e)}const U=r.securityDelegateAccountId;return U?await d("register-security-delegates",()=>(e?.onProgress?.("Registering security service delegated administrators"),se(u,U,void 0,o)),t,s,e):p("register-security-delegates",i,e),y({organisationId:D,createdAccounts:m,identityCentreStatus:a,phasesCompleted:t,phasesSkipped:i,errors:s})}function C(n,r,e,o){e.push({phase:n,error:r.message}),o?.onError?.(n,r),o?.onPhaseComplete?.(n,"error")}function p(n,r,e){r.push(n),e?.onPhaseStart?.(n),e?.onPhaseComplete?.(n,"skipped")}async function me(n,r,e,o){const t=await w(n,o);if(!t.success){e?.onProgress?.(`Could not check for imported member accounts: ${t.error.message}`);return}const i=ae(t.data,r);if(i.length===0)return;const s=ce(i);e?.onWarning?e.onWarning("enable-root-access",s):e?.onProgress?.(s)}async function d(n,r,e,o,t){t?.onPhaseStart?.(n);const i=await r();i.success?(e.push(n),t?.onPhaseComplete?.(n,"completed")):C(n,i.error,o,t)}async function de(n,r,e,o,t){const i=await w(n,t);if(!i.success)return h(i.error);const s=new Set(i.data.map(a=>a.Name?.toLowerCase()).filter(a=>a!==void 0));for(const a of r){if(s.has(a.name.toLowerCase()))continue;o?.onProgress?.(`Account "${a.name}" is not yet a member of this AWS Organisation. Fjall will create a new account (with a new account ID). If "${a.name}" already exists as a standalone Fjall-connected account, abort now and import it into the organisation instead, because creating here produces a duplicate account. See the account-import runbook.`);const u=await k(n,a.name,a.email,void 0,void 0,t);if(!u.success)return h(u.error);e.push({name:u.data.accountName,accountId:u.data.accountId})}if(e.length===0)return y(i.data);const m=await w(n,t);return m.success?y(m.data):h(m.error)}function M(n){const r=[];for(const[e,o]of Object.entries(n))if(ue(o))for(const t of o)r.push({accountName:t,placementKey:e});else r.push(...M(o));return r}function pe(n,r,e){const o=M(n),t=new Map;for(const s of r){const m=s.Name?.toLowerCase();m&&s.Id&&t.set(m,s)}const i=[];for(const{accountName:s,placementKey:m}of o){const a=t.get(s.toLowerCase());!a?.Id||!a.Name||e!==void 0&&a.Id===e||i.push({id:a.Id,name:a.Name,environment:m})}return i}export{Ke as ORG_SETUP_PHASES,$e as runOrganisationSetup};
@@ -1 +1 @@
1
- import{SecretsManagerClient as a,DescribeSecretCommand as m}from"@aws-sdk/client-secrets-manager";import{success as d,failure as n}from"@fjall/generator";import{getErrorMessage as l,maskSensitiveOutput as p}from"@fjall/util";const u=3e4;async function g(s,c){const i=s.getClient(a),o={};for(const r of c)try{const e=(await i.send(new m({SecretId:r}),{abortSignal:AbortSignal.timeout(u)})).ARN;if(e===void 0||e==="")return n(new Error(`DescribeSecret for "${r}" returned no ARN \u2014 cannot resolve a complete ARN.`));o[r]=e}catch(t){const e=t instanceof Error?t.name:"";return e==="ResourceNotFoundException"?n(new Error(`Imported secret "${r}" does not exist in this account/region. Create it, or reference it by complete ARN via the secretsImport { arn } escape hatch.`)):e==="AccessDeniedException"||e==="AccessDenied"?n(new Error(`The deploy identity lacks secretsmanager:DescribeSecret on "${r}" \u2014 grant DescribeSecret on this secret so its complete ARN can be resolved.`)):n(new Error(`Failed to resolve imported secret "${r}": ${p(l(t))}`))}return d(o)}export{g as resolveSecretArns};
1
+ import{SecretsManagerClient as a,DescribeSecretCommand as m}from"@aws-sdk/client-secrets-manager";import{success as d,failure as n}from"@fjall/generator";import{getErrorMessage as p,maskSensitiveOutput as l}from"@fjall/util";import{composeSdkAbortSignal as f}from"../aws/organisations/types.js";const u=3e4;async function b(s,c){const i=s.getClient(a),o={};for(const r of c)try{const e=(await i.send(new m({SecretId:r}),{abortSignal:f(void 0,u)})).ARN;if(e===void 0||e==="")return n(new Error(`DescribeSecret for "${r}" returned no ARN \u2014 cannot resolve a complete ARN.`));o[r]=e}catch(t){const e=t instanceof Error?t.name:"";return e==="ResourceNotFoundException"?n(new Error(`Imported secret "${r}" does not exist in this account/region. Create it, or reference it by complete ARN via the secretsImport { arn } escape hatch.`)):e==="AccessDeniedException"||e==="AccessDenied"?n(new Error(`The deploy identity lacks secretsmanager:DescribeSecret on "${r}" \u2014 grant DescribeSecret on this secret so its complete ARN can be resolved.`)):n(new Error(`Failed to resolve imported secret "${r}": ${l(p(t))}`))}return d(o)}export{b as resolveSecretArns};
@@ -1 +1 @@
1
- import{ListObjectVersionsCommand as w,DeleteObjectsCommand as K,GetBucketTaggingCommand as x}from"@aws-sdk/client-s3";import{logger as f}from"@fjall/util/logger";import{getErrorMessage as y,maskSensitiveOutput as p}from"@fjall/util";import{SDK_PRE_EMPTY_TAG_KEY as C}from"@fjall/util/aws";import{composeSdkAbortSignal as B,extractErrorName as T,isAborted as b,isAccessDenied as D}from"../../aws/organisations/types.js";import{STACK_CLEANUP_LOG as E,warnViaCallbacks as g}from"./logging.js";const _=1e3,P=["aws-cdk:auto-delete-objects",C],V=new Set(["NoSuchBucket","NotFound","404"]);function $(t){return V.has(T(t))?!0:(t instanceof Error?t.message??"":"").includes("NoSuchBucket")}function A(t){const e=t instanceof Error?t.message??"":"";return D(T(t))||e.includes("AccessDenied")}async function L(t,e,i,u){let s,a,d=!1;i?.onStackCleanupProgress?.(e,"emptying-bucket");const r=1e3;let l=0;for(;l++<r;){if(b(u))return f.debug(E,`Aborted while emptying bucket ${e}`),"failed";let n;try{n=await t.send(new w({Bucket:e,KeyMarker:s,VersionIdMarker:a}),{abortSignal:B(u)})}catch(o){if($(o))return f.debug(E,`Bucket ${e} no longer exists, skipping`),"missing";const h=`Unexpected error emptying bucket ${e}: ${p(y(o))}`;return g(h,i),A(o)?"access-denied":"failed"}const S=[...n.Versions??[],...n.DeleteMarkers??[]];if(S.length===0)break;for(let o=0;o<S.length;o+=_){const h=S.slice(o,o+_);try{const c=(await t.send(new K({Bucket:e,Delete:{Objects:h.map(m=>({Key:m.Key,VersionId:m.VersionId})),Quiet:!0}}),{abortSignal:B(u)})).Errors??[];if(c.length>0){const m=`Failed to delete ${c.length} object(s) from ${e}: ${p(c[0]?.Message??c[0]?.Code??"unknown error")}`;if(g(m,i),c.some(M=>M.Code?.includes("AccessDenied")))return"access-denied";d=!0}}catch(k){const c=`Failed to delete batch from ${e}: ${p(y(k))}`;if(g(c,i),A(k))return"access-denied";d=!0}}if(!n.IsTruncated)break;s=n.NextKeyMarker,a=n.NextVersionIdMarker}if(l>r){const n=`Bucket ${e} reached ${r} page limit \u2014 some objects may remain`;return g(n,i),"failed"}return f.debug(E,`Emptied bucket ${e}`),d?"failed":"emptied"}async function Y(t,e,i,u){try{const s=await t.send(new x({Bucket:e}),{abortSignal:B(u)}),a=P;return(s.TagSet??[]).some(r=>r.Key!==void 0&&a.includes(r.Key)&&r.Value==="true")?"matched":"retained"}catch(s){const a=T(s),d=s instanceof Error?s.message??"":"",r=$(s),l=a==="NoSuchTagSet"||d.includes("NoSuchTagSet");if(r||l)return f.debug(E,`Bucket ${e} ${r?"no longer exists":"has no tags"} \u2014 skipping`),r?"gone":"retained";const n=`Could not read tags for bucket ${e}, leaving it untouched: ${p(y(s))}`;return g(n,i),"unreadable"}}export{V as BUCKET_GONE_ERROR_NAMES,P as PRE_EMPTY_TAG_KEYS,Y as classifyBucketForPreEmpty,L as emptyS3Bucket,$ as isBucketGoneError};
1
+ import{ListObjectVersionsCommand as K,DeleteObjectsCommand as b,GetBucketTaggingCommand as x}from"@aws-sdk/client-s3";import{logger as g}from"@fjall/util/logger";import{getErrorMessage as y,maskSensitiveOutput as E}from"@fjall/util";import{SDK_PRE_EMPTY_TAG_KEY as C}from"@fjall/util/aws";import{composeSdkAbortSignal as $,extractErrorName as A,isAborted as B,isAccessDenied as D}from"../../aws/organisations/types.js";import{STACK_CLEANUP_LOG as l,warnViaCallbacks as m}from"./logging.js";const T=1e3,P=["aws-cdk:auto-delete-objects",C],V=new Set(["NoSuchBucket","NotFound","404"]);function _(t){return V.has(A(t))?!0:(t instanceof Error?t.message??"":"").includes("NoSuchBucket")}function M(t){const e=t instanceof Error?t.message??"":"";return D(A(t))||e.includes("AccessDenied")}async function L(t,e,i,a){let s,d,u=!1;i?.onStackCleanupProgress?.(e,"emptying-bucket");const r=1e3;let f=0;for(;f++<r;){if(B(a))return g.debug(l,`Aborted while emptying bucket ${e}`),"failed";let n;try{n=await t.send(new K({Bucket:e,KeyMarker:s,VersionIdMarker:d}),{abortSignal:$(a)})}catch(o){if(_(o))return g.debug(l,`Bucket ${e} no longer exists, skipping`),"missing";const S=`Unexpected error emptying bucket ${e}: ${E(y(o))}`;return m(S,i),M(o)?"access-denied":"failed"}const h=[...n.Versions??[],...n.DeleteMarkers??[]];if(h.length===0)break;for(let o=0;o<h.length;o+=T){if(B(a))return g.debug(l,`Aborted while emptying bucket ${e}`),"failed";const S=h.slice(o,o+T);try{const c=(await t.send(new b({Bucket:e,Delete:{Objects:S.map(p=>({Key:p.Key,VersionId:p.VersionId})),Quiet:!0}}),{abortSignal:$(a)})).Errors??[];if(c.length>0){const p=`Failed to delete ${c.length} object(s) from ${e}: ${E(c[0]?.Message??c[0]?.Code??"unknown error")}`;if(m(p,i),c.some(w=>w.Code?.includes("AccessDenied")))return"access-denied";u=!0}}catch(k){const c=`Failed to delete batch from ${e}: ${E(y(k))}`;if(m(c,i),M(k))return"access-denied";u=!0}}if(!n.IsTruncated)break;s=n.NextKeyMarker,d=n.NextVersionIdMarker}if(f>r){const n=`Bucket ${e} reached ${r} page limit \u2014 some objects may remain`;return m(n,i),"failed"}return g.debug(l,`Emptied bucket ${e}`),u?"failed":"emptied"}async function Y(t,e,i,a){try{const s=await t.send(new x({Bucket:e}),{abortSignal:$(a)}),d=P;return(s.TagSet??[]).some(r=>r.Key!==void 0&&d.includes(r.Key)&&r.Value==="true")?"matched":"retained"}catch(s){const d=A(s),u=s instanceof Error?s.message??"":"",r=_(s),f=d==="NoSuchTagSet"||u.includes("NoSuchTagSet");if(r||f)return g.debug(l,`Bucket ${e} ${r?"no longer exists":"has no tags"} \u2014 skipping`),r?"gone":"retained";const n=`Could not read tags for bucket ${e}, leaving it untouched: ${E(y(s))}`;return m(n,i),"unreadable"}}export{V as BUCKET_GONE_ERROR_NAMES,P as PRE_EMPTY_TAG_KEYS,Y as classifyBucketForPreEmpty,L as emptyS3Bucket,_ as isBucketGoneError};
@@ -0,0 +1,82 @@
1
+ import { type Result } from "@fjall/generator";
2
+ /**
3
+ * A raw AWS IAM policy statement, preserved verbatim from the captured policy.
4
+ * Triage never reshapes a kept statement — survivors are re-put byte-for-byte
5
+ * (minus the stripped quarantine deny), so any field we do not model
6
+ * (NotPrincipal, NotAction, NotResource, …) survives intact. Reshaping a kept
7
+ * statement through a typed schema would silently widen or narrow access.
8
+ */
9
+ export type RawPolicyStatement = Record<string, unknown>;
10
+ /**
11
+ * The captured policy envelope. Only `Statement` is load-bearing for triage;
12
+ * every other top-level field (Version, Id, …) is preserved through the index
13
+ * signature so the re-put keeps the document's original shape.
14
+ */
15
+ export interface RawPolicyDocument {
16
+ Statement: RawPolicyStatement[];
17
+ [key: string]: unknown;
18
+ }
19
+ export type StatementClassification = "keep" | "strip" | "keep-flagged";
20
+ export interface ClassifiedStatement {
21
+ statement: RawPolicyStatement;
22
+ classification: StatementClassification;
23
+ /** British-English reason the statement was kept, stripped, or flagged. */
24
+ reason: string;
25
+ }
26
+ export interface BucketPolicyTriage {
27
+ /**
28
+ * The document to re-put: the captured envelope (Version/Id preserved) with
29
+ * Statement narrowed to the survivors. `null` when nothing survives the strip
30
+ * — the caller must then synthesise an enforceSSL-only policy (or leave the
31
+ * bucket policy-less); re-putting an empty Statement array is invalid in AWS.
32
+ */
33
+ sanePolicyDocument: RawPolicyDocument | null;
34
+ /** Survivors (Allow grants, conditioned denies, enforceSSL) — re-put verbatim. */
35
+ keptStatements: RawPolicyStatement[];
36
+ /** Stranded quarantine denies removed by the triage. */
37
+ strippedStatements: RawPolicyStatement[];
38
+ /** Broad denies kept but surfaced for review (could mask a quarantine). */
39
+ flaggedStatements: RawPolicyStatement[];
40
+ /** Whether the captured policy carried an enforceSSL TLS-only deny. */
41
+ hadEnforceSsl: boolean;
42
+ classifications: ClassifiedStatement[];
43
+ /** British-English triage notes for the operator / PR reviewer. */
44
+ notes: string[];
45
+ }
46
+ /**
47
+ * True when the statement is the enforceSSL TLS-only deny — a `Deny` carrying
48
+ * the `aws:SecureTransport: false` condition. This is the exact predicate behind
49
+ * {@link BucketPolicyTriage.hadEnforceSsl}, exported so `toResourcePolicyStatements`
50
+ * can exclude enforceSSL from the typed `resourcePolicyStatements` array using
51
+ * the same rule (the S3Bucket construct always re-adds it via `enforceSSL: true`,
52
+ * so listing it would double-render). Triage and converter share one source.
53
+ */
54
+ export declare function isEnforceSslStatement(statement: RawPolicyStatement): boolean;
55
+ /**
56
+ * Structurally classify a captured S3 bucket policy and separate the stranded
57
+ * quarantine deny from statements worth restoring.
58
+ *
59
+ * The captured policy is the raw `GetBucketPolicy` JSON string taken by
60
+ * {@link unlockBucket} BEFORE it deleted the live policy — detection of the
61
+ * quarantine is behavioural (an AccessDenied proxy), so this is the first code
62
+ * to classify the policy structurally. The result feeds both the SDK re-put (a
63
+ * live policy document) and, later, the code-first reconciliation.
64
+ *
65
+ * Strips conservatively: only a high-confidence TOTAL lockout (denies all S3,
66
+ * every principal, the whole bucket, no condition) is removed. Any broad deny
67
+ * scoped by principal, resource, or condition is KEPT and flagged for review —
68
+ * stripping it would silently widen access, whereas keeping a real lockout fails
69
+ * loudly on re-put and is recoverable. Known limit: a quarantine spelled as an
70
+ * enumerated concrete-action list (rather than `s3:*`/`NotAction`) is flagged,
71
+ * not stripped — the canonical quarantine uses `s3:*`.
72
+ *
73
+ * Fails closed: malformed JSON, a non-object envelope, a non-object statement,
74
+ * or an unrecognised Effect returns a failure rather than guessing — the
75
+ * operator still holds the captured forensic JSON to re-put by hand.
76
+ *
77
+ * Every failure `Error.message` is pre-masked at construction: the JSON-parse
78
+ * detail and the unrecognised-Effect value can embed captured-policy credential
79
+ * fragments, so callers and downstream sinks (the webapp audit ledger / UI) must
80
+ * NOT re-mask. See `.claude/rules/security-standards.md § "Pre-masked producers"`.
81
+ */
82
+ export declare function triageBucketPolicy(capturedPolicyJson: string): Result<BucketPolicyTriage, Error>;
@@ -0,0 +1 @@
1
+ import{success as A,failure as l}from"@fjall/generator";import{maskSensitiveOutput as y}from"@fjall/util";function f(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function u(e){return typeof e=="string"?[e]:Array.isArray(e)?e.filter(n=>typeof n=="string"):[]}function S(e){const{Condition:n}=e;if(!f(n))return!1;for(const[t,s]of Object.entries(n)){const c=t.toLowerCase();if(!(c!=="bool"&&c!=="boolifexists")&&f(s))for(const[o,i]of Object.entries(s)){if(o.toLowerCase()!=="aws:securetransport")continue;if((Array.isArray(i)?i:[i]).some(a=>a===!1||typeof a=="string"&&a.toLowerCase()==="false"))return!0}}return!1}function b(e){return e.Effect==="Deny"&&S(e)}function w(e){const{Condition:n}=e;return f(n)&&Object.keys(n).length>0}function h(e){return e.Action!==void 0?u(e.Action).some(n=>n==="*"||n.toLowerCase()==="s3:*"):e.NotAction!==void 0?!u(e.NotAction).map(t=>t.toLowerCase()).some(t=>t==="*"||t==="s3:*"):!1}function E(e){return h(e)?!0:u(e.Action).some(n=>n.includes("*"))}function C(e){if(e.NotPrincipal!==void 0)return!1;const{Principal:n}=e;return n===void 0||n==="*"?!0:f(n)?u(n.AWS).includes("*"):!1}function L(e){const n=u(e.Resource);return n.length===0?!0:n.some(t=>t==="*"||/^arn:[^:]*:s3:::[^/]+$/.test(t)||/^arn:[^:]*:s3:::[^/]+\/\*$/.test(t))}function v(e){return e.Effect==="Allow"?{statement:e,classification:"keep",reason:"Allow grant preserved."}:S(e)?{statement:e,classification:"keep",reason:"enforceSSL TLS-only deny preserved."}:E(e)?h(e)&&C(e)&&L(e)&&!w(e)?{statement:e,classification:"strip",reason:"Stranded quarantine deny (denies all S3, every principal, whole bucket, no condition) removed."}:{statement:e,classification:"keep-flagged",reason:"Broad deny kept; review that its principal/resource/condition scoping is intended, not a disguised lockout."}:{statement:e,classification:"keep",reason:"Scoped deny preserved."}}function g(e){return e.map(n=>typeof n.Sid=="string"&&n.Sid!==""?n.Sid:"(unnamed)").join(", ")}function N(e){const{strippedStatements:n,flaggedStatements:t,hadEnforceSsl:s,keptCount:c}=e,o=[];return o.push(n.length>0?`Stripped ${n.length} stranded quarantine deny statement(s): ${g(n)}.`:"No stranded quarantine deny statement was found in the captured policy."),s||o.push("The captured policy carried no enforceSSL TLS-only deny \u2014 the re-put must add one to keep the bucket TLS-enforced."),t.length>0&&o.push(`Flagged ${t.length} broad conditioned deny statement(s) for review: ${g(t)}.`),c===0&&o.push("No statements survived the triage \u2014 the re-put must synthesise an enforceSSL-only policy or the bucket will be left without a policy."),o}function $(e){let n;try{n=JSON.parse(e)}catch(r){const m=y(r instanceof Error?r.message:String(r));return l(new Error(`Captured bucket policy is not valid JSON: ${m}`))}if(!f(n))return l(new Error("Captured bucket policy is not a JSON object."));const t=n.Statement,s=Array.isArray(t)?t:t===void 0?[]:[t];for(const r of s){if(!f(r))return l(new Error("Captured bucket policy contains a non-object statement."));if(r.Effect!=="Allow"&&r.Effect!=="Deny")return l(new Error(`Captured bucket policy statement has an unrecognised Effect: ${y(String(r.Effect))}.`))}const c=s,o=c.map(v),i=[],d=[],a=[];for(const r of o){if(r.classification==="strip"){d.push(r.statement);continue}i.push(r.statement),r.classification==="keep-flagged"&&a.push(r.statement)}const p=c.some(b),k=i.length===0?null:{...n,Statement:i};return A({sanePolicyDocument:k,keptStatements:i,strippedStatements:d,flaggedStatements:a,hadEnforceSsl:p,classifications:o,notes:N({strippedStatements:d,flaggedStatements:a,hadEnforceSsl:p,keptCount:i.length})})}export{b as isEnforceSslStatement,$ as triageBucketPolicy};
@@ -0,0 +1,67 @@
1
+ import { type STSClient } from "@aws-sdk/client-sts";
2
+ import { type Result } from "@fjall/generator";
3
+ import { type BucketPolicyTriage } from "./bucketPolicyTriage.js";
4
+ export interface RestoreAndReconcileInput {
5
+ /** Member account holding the quarantined bucket. */
6
+ accountId: string;
7
+ bucketName: string;
8
+ /** Bucket region — also pins the regional STS endpoint requirement. */
9
+ region: string;
10
+ /**
11
+ * The infrastructure file contents, supplied when the bucket is known to map
12
+ * to one. Absent → the reconcile is a no-op (the re-put still heals liveness).
13
+ */
14
+ infrastructureContent?: string;
15
+ /** AST diagnostic path; defaults to the codemod's default when omitted. */
16
+ infrastructureFilePath?: string;
17
+ /**
18
+ * Persist hook for the captured live policy — fired before the delete, after
19
+ * capture. A rejection aborts the unlock (no delete). The callback receives
20
+ * the captured policy UNMASKED (it can carry credential VALUES); the caller
21
+ * MUST mask before persisting or displaying.
22
+ */
23
+ onPolicyCaptured?: (capturedPolicy: string) => Promise<void>;
24
+ abortSignal?: AbortSignal;
25
+ }
26
+ export type ReconcileOutcome = {
27
+ outcome: "nothing-to-change";
28
+ reason: "no-infrastructure-content" | "no-statements-to-reconcile";
29
+ } | {
30
+ outcome: "edit-ready";
31
+ constructId: string;
32
+ modifiedContent: string;
33
+ addedStatements: number;
34
+ } | {
35
+ outcome: "report-only";
36
+ reason: "unresolved-construct" | "has-unconvertible" | "edit-failed";
37
+ /** Whether the bucket maps to a managed `StorageFactory.build` construct. */
38
+ managed: boolean;
39
+ candidates?: string[];
40
+ unconvertibleCount?: number;
41
+ detail?: string;
42
+ };
43
+ export interface RestoreAndReconcileReport {
44
+ unlock: "unlocked" | "no-policy";
45
+ /**
46
+ * The captured live policy, UNMASKED. The caller MUST mask it before
47
+ * persisting or displaying — it can carry credential VALUES (e.g. a presigned
48
+ * URL or connection string embedded in a Condition).
49
+ */
50
+ capturedPolicy: string | null;
51
+ triage: BucketPolicyTriage | null;
52
+ /** True when the re-put added an enforceSSL deny the captured policy lacked. */
53
+ enforceSslSynthesised: boolean;
54
+ restore: {
55
+ outcome: "restored" | "skipped-empty";
56
+ via?: "root" | "cascade";
57
+ };
58
+ reconcile: ReconcileOutcome;
59
+ }
60
+ /**
61
+ * Unlock a quarantined bucket, restore a sane live policy, and reconcile the
62
+ * durable IaC. Returns a structured report; never throws. The reconcile half
63
+ * degrades to report-only rather than emitting a partial edit whenever the
64
+ * construct cannot be located or a kept statement is unconvertible (a partial
65
+ * typed array would be clobbered on the next deploy).
66
+ */
67
+ export declare function restoreAndReconcileQuarantinedBucket(stsClient: STSClient, input: RestoreAndReconcileInput): Promise<Result<RestoreAndReconcileReport, Error>>;
@@ -0,0 +1 @@
1
+ import{S3Client as y}from"@aws-sdk/client-s3";import{AssumeRoleCommand as k}from"@aws-sdk/client-sts";import{success as f,failure as l}from"@fjall/generator";import{modifyResource as h,resolveConstructByLiteralProperty as A}from"@fjall/generator/codemod";import{getErrorMessage as C,maskSensitiveOutput as P}from"@fjall/util";import{composeSdkAbortSignal as b}from"../../aws/organisations/types.js";import{assumeRootForTask as w,MAX_ROOT_SESSION_SECONDS as S}from"../../aws/sts/assumeRoot.js";import{buildCascadeRoleArn as E}from"../contextHelpers.js";import{triageBucketPolicy as R}from"./bucketPolicyTriage.js";import{ensureEnforceSsl as v,restoreBucketPolicy as g,synthesiseEnforceSslDocument as D}from"./restoreBucketPolicy.js";import{formatScpSuspectedFailure as I}from"./scpRemediation.js";import{toResourcePolicyStatements as N}from"./toResourcePolicyStatements.js";import{S3_UNLOCK_POLICY_ARN as O,unlockBucket as _}from"./unlockBucket.js";const K="fjall-bucket-policy-restore";async function G(i,a){const{accountId:o,bucketName:t,region:e,abortSignal:n,onPolicyCaptured:c}=a,r=await _(i,{accountId:o,bucketName:t,region:e,abortSignal:n,...c!==void 0&&{onPolicyCaptured:c}});if(!r.success)return l(r.error);if(r.data.outcome==="no-policy")return f({unlock:"no-policy",capturedPolicy:null,triage:null,enforceSslSynthesised:!1,restore:{outcome:"skipped-empty"},reconcile:{outcome:"nothing-to-change",reason:"no-statements-to-reconcile"}});const u=r.data.capturedPolicy,s=R(u);if(!s.success)return l(s.error);const d=s.data,m=d.sanePolicyDocument===null?D(t):JSON.stringify(v(d.sanePolicyDocument,t)),p=await B(i,{accountId:o,bucketName:t,region:e,policyDocument:m,abortSignal:n});return p.success?f({unlock:"unlocked",capturedPolicy:u,triage:d,enforceSslSynthesised:!d.hadEnforceSsl,restore:{outcome:"restored",via:p.data},reconcile:F(a,d.keptStatements)}):l(p.error)}async function B(i,a){const{accountId:o,bucketName:t,region:e,policyDocument:n,abortSignal:c}=a,r=await w(i,{targetAccountId:o,taskPolicyArn:O,durationSeconds:S,abortSignal:c});if(r.success){const d=new y({region:e,credentials:r.data.credentials}),m=await g(d,{bucketName:t,policyDocument:n,abortSignal:c});if(m.success)return f("root");if(!m.error.isAccessDenied)return l(new Error(m.error.message))}const u=await T(i,o,e,c);if(!u.success)return l(u.error);const s=await g(u.data,{bucketName:t,policyDocument:n,abortSignal:c});return s.success?f("cascade"):s.error.isAccessDenied?l(new Error(I("PutBucketPolicy",{kind:"bucket",name:t},o))):l(new Error(s.error.message))}async function T(i,a,o,t){let e;try{e=(await i.send(new k({RoleArn:E(a),RoleSessionName:K,DurationSeconds:S}),{abortSignal:b(t)})).Credentials}catch(n){return l(new Error(`Failed to assume the admin role in account ${a} for the policy re-put: ${P(C(n))}`))}return e?.AccessKeyId===void 0||e.SecretAccessKey===void 0||e.SessionToken===void 0?l(new Error(`Assuming the admin role in account ${a} returned no credentials.`)):f(new y({region:o,credentials:{accessKeyId:e.AccessKeyId,secretAccessKey:e.SecretAccessKey,sessionToken:e.SessionToken}}))}function F(i,a){const{infrastructureContent:o,infrastructureFilePath:t,bucketName:e}=i;if(o===void 0)return{outcome:"nothing-to-change",reason:"no-infrastructure-content"};const{statements:n,unconvertible:c}=N(a);if(n.length===0&&c.length===0)return{outcome:"nothing-to-change",reason:"no-statements-to-reconcile"};const r=A(o,{type:"storage",property:"bucketName",value:e},t);if(!r.success)return{outcome:"report-only",reason:"unresolved-construct",managed:!1};if(!r.data.resolved)return{outcome:"report-only",reason:"unresolved-construct",managed:r.data.candidates.length>0,candidates:r.data.candidates.map(d=>d.constructId)};if(c.length>0)return{outcome:"report-only",reason:"has-unconvertible",managed:!0,unconvertibleCount:c.length};const u=r.data.construct.constructId,s=h(o,{type:"storage",name:u,properties:{resourcePolicyStatements:n},...t!==void 0&&{filePath:t}});return s.success?{outcome:"edit-ready",constructId:u,modifiedContent:s.data.content,addedStatements:n.length}:{outcome:"report-only",reason:"edit-failed",managed:!0,detail:s.error.kind}}export{G as restoreAndReconcileQuarantinedBucket};
@@ -0,0 +1,43 @@
1
+ import { type S3Client } from "@aws-sdk/client-s3";
2
+ import { type Result } from "@fjall/generator";
3
+ import { type RawPolicyDocument } from "./bucketPolicyTriage.js";
4
+ export interface RestoreBucketPolicyInput {
5
+ bucketName: string;
6
+ /** The sane policy document to re-put, already serialised to JSON. */
7
+ policyDocument: string;
8
+ abortSignal?: AbortSignal;
9
+ }
10
+ export interface RestoreBucketPolicySuccess {
11
+ outcome: "restored";
12
+ }
13
+ export interface RestoreBucketPolicyError {
14
+ message: string;
15
+ /**
16
+ * True when the PutBucketPolicy was AccessDenied. The orchestration uses this
17
+ * to fall back: the S3UnlockBucketPolicy root-task session is Get+Delete only,
18
+ * so the first re-put attempt on it is expected to deny — the member-account
19
+ * admin role regains S3 write once the lockout deny is gone and is retried.
20
+ */
21
+ isAccessDenied: boolean;
22
+ }
23
+ /**
24
+ * Put `policyDocument` verbatim onto `bucketName` via the supplied S3 client.
25
+ * The caller owns credential resolution (root session, then admin-role fallback);
26
+ * this primitive only issues the single `PutBucketPolicy`. Errors are masked; an
27
+ * AccessDenied is reported structurally so the caller can retry with a broader
28
+ * role rather than re-parsing the message.
29
+ */
30
+ export declare function restoreBucketPolicy(s3Client: S3Client, input: RestoreBucketPolicyInput): Promise<Result<RestoreBucketPolicySuccess, RestoreBucketPolicyError>>;
31
+ /**
32
+ * A standalone enforceSSL-only policy document. Re-put when the triage's sane
33
+ * document is null (the captured policy carried only the stranded lockout) so a
34
+ * just-unlocked managed bucket is not left without TLS enforcement.
35
+ */
36
+ export declare function synthesiseEnforceSslDocument(bucketName: string): string;
37
+ /**
38
+ * Return `saneDoc` unchanged if it already carries an enforceSSL deny; otherwise
39
+ * append one. A captured policy can keep legitimate grants yet have lost its
40
+ * enforceSSL deny to out-of-band drift — re-putting without it would leave the
41
+ * managed bucket TLS-unenforced.
42
+ */
43
+ export declare function ensureEnforceSsl(saneDoc: RawPolicyDocument, bucketName: string): RawPolicyDocument;
@@ -0,0 +1 @@
1
+ import{PutBucketPolicyCommand as u}from"@aws-sdk/client-s3";import{success as a,failure as o}from"@fjall/generator";import{getErrorMessage as m,maskSensitiveOutput as f}from"@fjall/util";import{composeSdkAbortSignal as l,extractErrorName as S,isAccessDenied as p}from"../../aws/organisations/types.js";import{isEnforceSslStatement as d}from"./bucketPolicyTriage.js";const y="2012-10-17";async function A(e,t){const{bucketName:r,policyDocument:i,abortSignal:c}=t;try{await e.send(new u({Bucket:r,Policy:i}),{abortSignal:l(c)})}catch(n){return p(S(n))?o({message:`PutBucketPolicy was denied on ${r}.`,isAccessDenied:!0}):o({message:`Failed to restore the bucket policy on ${r}: ${f(m(n))}`,isAccessDenied:!1})}return a({outcome:"restored"})}function s(e){const t=`arn:aws:s3:::${e}`;return{Sid:"DenyInsecureTransport",Effect:"Deny",Principal:{AWS:"*"},Action:"s3:*",Resource:[t,`${t}/*`],Condition:{Bool:{"aws:SecureTransport":"false"}}}}function B(e){return JSON.stringify({Version:y,Statement:[s(e)]})}function w(e,t){return e.Statement.some(d)?e:{...e,Statement:[...e.Statement,s(t)]}}export{w as ensureEnforceSsl,A as restoreBucketPolicy,B as synthesiseEnforceSslDocument};
@@ -0,0 +1,20 @@
1
+ import { type ResourcePolicyStatement } from "@fjall/generator";
2
+ import { type RawPolicyStatement } from "./bucketPolicyTriage.js";
3
+ export interface ConvertedResourcePolicy {
4
+ /** Kept statements that the typed `ResourcePolicyStatement` carries faithfully. */
5
+ statements: ResourcePolicyStatement[];
6
+ /**
7
+ * Kept statements outside the typed shape (NotPrincipal/NotAction/NotResource,
8
+ * a Service/Federated/CanonicalUser principal, a non-string condition operand,
9
+ * an absent Resource). The caller MUST degrade the IaC reconcile to report-only
10
+ * when this is non-empty — a partial typed array would clobber these on deploy.
11
+ */
12
+ unconvertible: RawPolicyStatement[];
13
+ }
14
+ /**
15
+ * Split the triage's kept statements into those representable as typed
16
+ * `ResourcePolicyStatement`s and those that are not. The enforceSSL TLS-only
17
+ * deny is excluded from BOTH — the construct always re-adds it via
18
+ * `enforceSSL: true`, so listing it would double-render.
19
+ */
20
+ export declare function toResourcePolicyStatements(keptStatements: RawPolicyStatement[]): ConvertedResourcePolicy;
@@ -0,0 +1 @@
1
+ import{ResourcePolicyStatementSchema as u}from"@fjall/generator";import{isEnforceSslStatement as c}from"./bucketPolicyTriage.js";function l(n){const e=[],t=[];for(const i of n){if(c(i))continue;const r=s(i);r===void 0?t.push(i):e.push(r)}return{statements:e,unconvertible:t}}function s(n){if(n.NotPrincipal!==void 0||n.NotAction!==void 0||n.NotResource!==void 0)return;const e=n.Effect;if(e!=="Allow"&&e!=="Deny")return;const t=y(n.Principal);if(t===void 0)return;const i=o(n.Action);if(i===void 0||i.length===0)return;const r=o(n.Resource);if(r===void 0||r.length===0)return;const d={effect:e,principals:t,actions:i,resources:r,...typeof n.Sid=="string"&&{sid:n.Sid},...n.Condition!==void 0&&{conditions:n.Condition}},f=u.safeParse(d);if(f.success)return f.data}function y(n){const e=n==="*"?["*"]:S(n)&&Object.keys(n).length===1&&"AWS"in n?o(n.AWS):void 0;if(!(e===void 0||e.length===0))return e}function o(n){if(typeof n=="string")return[n];if(Array.isArray(n))return n.every(e=>typeof e=="string")?n:void 0}function S(n){return typeof n=="object"&&n!==null&&!Array.isArray(n)}export{l as toResourcePolicyStatements};
@@ -1,11 +1,23 @@
1
1
  import type { STSClient } from "@aws-sdk/client-sts";
2
2
  import { type Result } from "@fjall/generator";
3
+ import { type RootTaskPolicyArn } from "../../aws/sts/assumeRoot.js";
4
+ export declare const S3_UNLOCK_POLICY_ARN: RootTaskPolicyArn;
3
5
  export interface UnlockBucketInput {
4
6
  /** Member account holding the quarantined bucket. */
5
7
  accountId: string;
6
8
  bucketName: string;
7
9
  /** Bucket region — also pins the regional STS endpoint requirement. */
8
10
  region: string;
11
+ /**
12
+ * Fired AFTER the policy is captured (guaranteed non-empty) and BEFORE
13
+ * DeleteBucketPolicy runs. A rejection ABORTS the unlock — the live policy is
14
+ * left intact rather than deleted with no durable record of what it carried.
15
+ * DeleteBucketPolicy does not log the document body to CloudTrail, so the
16
+ * delete→re-put window is the only point a crash could lose the policy; this
17
+ * barrier closes it. The callback receives the captured policy UNMASKED (it
18
+ * can carry credential VALUES) — the consumer MUST mask before persisting.
19
+ */
20
+ onPolicyCaptured?: (capturedPolicy: string) => Promise<void>;
9
21
  abortSignal?: AbortSignal;
10
22
  }
11
23
  /**
@@ -1 +1 @@
1
- import{S3Client as w,GetBucketPolicyCommand as b,DeleteBucketPolicyCommand as h}from"@aws-sdk/client-s3";import{success as l,failure as t}from"@fjall/generator";import{getErrorMessage as k,maskSensitiveOutput as m}from"@fjall/util";import{composeSdkAbortSignal as y,extractErrorName as p,isAccessDenied as f}from"../../aws/organisations/types.js";import{assumeRootForTask as E,MAX_ROOT_SESSION_SECONDS as B}from"../../aws/sts/assumeRoot.js";import{formatScpSuspectedFailure as S}from"./scpRemediation.js";const P="arn:aws:iam::aws:policy/root-task/S3UnlockBucketPolicy";function N(n){return`The unlock deleted the ENTIRE bucket policy on ${n} \u2014 including any enforceSSL deny statements and legitimate access grants it carried, not just the stranded deny. Review the captured policy and re-put sane statements (TLS-only deny, intended access grants) before relying on the bucket.`}async function L(n,g){const{accountId:c,bucketName:e,region:i,abortSignal:a}=g;if(i==="")return t(new Error("A bucket region is required to unlock a bucket."));const u=await E(n,{targetAccountId:c,taskPolicyArn:P,durationSeconds:B,abortSignal:a});if(!u.success)return t(u.error);const d=new w({region:i,credentials:u.data.credentials});let o;try{o=(await d.send(new b({Bucket:e}),{abortSignal:y(a)})).Policy}catch(r){const s=p(r);return s==="NoSuchBucketPolicy"?l({outcome:"no-policy"}):s==="NoSuchBucket"?t(new Error(`Bucket ${e} was not found in region ${i} \u2014 check the bucket name and region.`)):f(s)?t(new Error(S("GetBucketPolicy",{kind:"bucket",name:e},c))):t(new Error(`Failed to capture the bucket policy on ${e}: ${m(k(r))}`))}if(o===void 0||o==="")return l({outcome:"no-policy"});try{await d.send(new h({Bucket:e}),{abortSignal:y(a)})}catch(r){return f(p(r))?t(new Error(S("DeleteBucketPolicy",{kind:"bucket",name:e},c))):t(new Error(`Captured the bucket policy on ${e} but failed to delete it: ${m(k(r))}`))}return l({outcome:"unlocked",capturedPolicy:o,warning:N(e)})}export{L as unlockBucket};
1
+ import{S3Client as g,GetBucketPolicyCommand as h,DeleteBucketPolicyCommand as E}from"@aws-sdk/client-s3";import{success as l,failure as r}from"@fjall/generator";import{getErrorMessage as d,maskSensitiveOutput as k}from"@fjall/util";import{composeSdkAbortSignal as y,extractErrorName as f,isAccessDenied as S}from"../../aws/organisations/types.js";import{assumeRootForTask as P,MAX_ROOT_SESSION_SECONDS as B}from"../../aws/sts/assumeRoot.js";import{formatScpSuspectedFailure as b}from"./scpRemediation.js";const C="arn:aws:iam::aws:policy/root-task/S3UnlockBucketPolicy";function N(n){return`The unlock deleted the ENTIRE bucket policy on ${n} \u2014 including any enforceSSL deny statements and legitimate access grants it carried, not just the stranded deny. Review the captured policy and re-put sane statements (TLS-only deny, intended access grants) before relying on the bucket.`}async function R(n,w){const{accountId:c,bucketName:e,region:i,abortSignal:a,onPolicyCaptured:m}=w;if(i==="")return r(new Error("A bucket region is required to unlock a bucket."));const u=await P(n,{targetAccountId:c,taskPolicyArn:C,durationSeconds:B,abortSignal:a});if(!u.success)return r(u.error);const p=new g({region:i,credentials:u.data.credentials});let o;try{o=(await p.send(new h({Bucket:e}),{abortSignal:y(a)})).Policy}catch(t){const s=f(t);return s==="NoSuchBucketPolicy"?l({outcome:"no-policy"}):s==="NoSuchBucket"?r(new Error(`Bucket ${e} was not found in region ${i} \u2014 check the bucket name and region.`)):S(s)?r(new Error(b("GetBucketPolicy",{kind:"bucket",name:e},c))):r(new Error(`Failed to capture the bucket policy on ${e}: ${k(d(t))}`))}if(o===void 0||o==="")return l({outcome:"no-policy"});if(m!==void 0)try{await m(o)}catch(t){return r(new Error(`Captured the bucket policy on ${e} but failed to persist it before deletion; left the live policy intact: ${k(d(t))}`))}try{await p.send(new E({Bucket:e}),{abortSignal:y(a)})}catch(t){return S(f(t))?r(new Error(b("DeleteBucketPolicy",{kind:"bucket",name:e},c))):r(new Error(`Captured the bucket policy on ${e} but failed to delete it: ${k(d(t))}`))}return l({outcome:"unlocked",capturedPolicy:o,warning:N(e)})}export{C as S3_UNLOCK_POLICY_ARN,R as unlockBucket};
@@ -2,15 +2,26 @@ import type { CdkContext, CdkOptions } from "./CdkService.js";
2
2
  export declare class CdkArgumentBuilder {
3
3
  buildContextArgs(context?: CdkContext): string[];
4
4
  /**
5
- * Emit `--parameters key=value` pairs for every entry in the supplied
6
- * record. Returns an empty array when the record is undefined or empty so
7
- * the caller can unconditionally spread the result into argv.
5
+ * Emit `--parameters [stackName:]key=value` pairs for every entry in the
6
+ * supplied record. Returns an empty array when the record is undefined or
7
+ * empty so the caller can unconditionally spread the result into argv.
8
+ *
9
+ * When `stackName` is supplied, each parameter is scoped to that stack
10
+ * (`Stack:Key=Value`). This is REQUIRED for single-stack deploys: the cdk
11
+ * CLI applies an unqualified `--parameters Key=Value` to EVERY stack in the
12
+ * deploy set — including dependency stacks it pulls in upstream — so a
13
+ * parameter declared only on the compute stack makes CloudFormation reject
14
+ * "Parameters: [...] do not exist in the template" on the network/database
15
+ * stacks. Omit `stackName` only for `--all` deploys, where scoping to one
16
+ * stack is impossible and the unqualified form is the only valid one.
8
17
  *
9
18
  * Keys must match CloudFormation's parameter-name shape
10
19
  * (`^[A-Za-z][A-Za-z0-9]*$`). Values must be non-empty and free of `,`
11
20
  * (cdk's parameter-list separator) and newlines (which would break argv).
21
+ * The `stackName:` qualifier is folded into the emitted value AFTER key
22
+ * validation, so the colon never trips the key regex.
12
23
  */
13
- buildParameterArgs(parameters?: Record<string, string>): string[];
24
+ buildParameterArgs(parameters?: Record<string, string>, stackName?: string): string[];
14
25
  injectCascadeCredentials(env: NodeJS.ProcessEnv, credentials?: CdkOptions["credentials"]): void;
15
26
  buildCdkEnv(options?: {
16
27
  context?: {
@@ -1 +1 @@
1
- import{filterDangerousEnvVars as u}from"@fjall/util";class s{buildContextArgs(r){const a=[];return r?.accountId&&a.push("-c",`accountId=${r.accountId}`),r?.environment&&a.push("-c",`environment=${r.environment}`),r?.managedAccount&&a.push("-c","managedAccount=true"),r?.accountName&&a.push("-c",`accountName=${r.accountName}`),r?.orgId&&a.push("-c",`orgId=${r.orgId}`),r?.rootId&&a.push("-c",`rootId=${r.rootId}`),r?.managementAccountId&&a.push("-c",`managementAccountId=${r.managementAccountId}`),r?.ipamPoolId&&a.push("-c",`ipamPoolId=${r.ipamPoolId}`),r?.fjallOrgId&&a.push("-c",`fjallOrgId=${r.fjallOrgId}`),r?.fjallOidcConfigured&&a.push("-c",`fjallOidcConfigured=${r.fjallOidcConfigured}`),r?.fjallAccountGlobalsConfigured&&a.push("-c",`fjallAccountGlobalsConfigured=${r.fjallAccountGlobalsConfigured}`),r?.fjallAccountTrailState&&a.push("-c",`fjallAccountTrailState=${r.fjallAccountTrailState}`),r?.orgConfig&&a.push("-c",`orgConfig=${r.orgConfig}`),r?.fjallAdoptBackupVault&&a.push("-c","fjallAdoptBackupVault=true"),r?.resolvedSecretArns&&a.push("-c",`fjallResolvedSecretArns=${r.resolvedSecretArns}`),a}buildParameterArgs(r){if(r===void 0)return[];const a=Object.entries(r);if(a.length===0)return[];const n=[];for(const[e,l]of a){if(!/^[A-Za-z][A-Za-z0-9]*$/.test(e))throw new Error(`Invalid CloudFormation parameter name "${e}": must match /^[A-Za-z][A-Za-z0-9]*$/ (alphanumeric, leading letter, no separators).`);if(l==="")throw new Error(`CloudFormation parameter "${e}" has an empty value.`);if(/[,\n\r]/.test(l))throw new Error(`CloudFormation parameter "${e}" value contains "," or newline \u2014 cdk's --parameters splits on "," so the deploy would silently fragment.`);n.push("--parameters",`${e}=${l}`)}return n}injectCascadeCredentials(r,a){a&&(r.AWS_ACCESS_KEY_ID=a.accessKeyId,r.AWS_SECRET_ACCESS_KEY=a.secretAccessKey,delete r.AWS_SESSION_TOKEN,a.sessionToken&&(r.AWS_SESSION_TOKEN=a.sessionToken))}buildCdkEnv(r){const a={...u(process.env),CI:"true",FORCE_COLOR:"0",CDK_DISABLE_VERSION_CHECK:"1"};return r?.context?.region&&(a.AWS_REGION=r.context.region,a.AWS_DEFAULT_REGION=r.context.region,a.CDK_DEFAULT_REGION=r.context.region),r?.context?.accountId&&(a.CDK_DEFAULT_ACCOUNT=r.context.accountId),this.injectCascadeCredentials(a,r?.credentials),a}}export{s as CdkArgumentBuilder};
1
+ import{filterDangerousEnvVars as i}from"@fjall/util";class d{buildContextArgs(r){const e=[];return r?.accountId&&e.push("-c",`accountId=${r.accountId}`),r?.environment&&e.push("-c",`environment=${r.environment}`),r?.managedAccount&&e.push("-c","managedAccount=true"),r?.accountName&&e.push("-c",`accountName=${r.accountName}`),r?.orgId&&e.push("-c",`orgId=${r.orgId}`),r?.rootId&&e.push("-c",`rootId=${r.rootId}`),r?.managementAccountId&&e.push("-c",`managementAccountId=${r.managementAccountId}`),r?.ipamPoolId&&e.push("-c",`ipamPoolId=${r.ipamPoolId}`),r?.fjallOrgId&&e.push("-c",`fjallOrgId=${r.fjallOrgId}`),r?.fjallOidcConfigured&&e.push("-c",`fjallOidcConfigured=${r.fjallOidcConfigured}`),r?.fjallAccountGlobalsConfigured&&e.push("-c",`fjallAccountGlobalsConfigured=${r.fjallAccountGlobalsConfigured}`),r?.fjallAccountTrailState&&e.push("-c",`fjallAccountTrailState=${r.fjallAccountTrailState}`),r?.orgConfig&&e.push("-c",`orgConfig=${r.orgConfig}`),r?.fjallAdoptBackupVault&&e.push("-c","fjallAdoptBackupVault=true"),r?.resolvedSecretArns&&e.push("-c",`fjallResolvedSecretArns=${r.resolvedSecretArns}`),e}buildParameterArgs(r,e){if(r===void 0)return[];const l=Object.entries(r);if(l.length===0)return[];const o=e!==void 0&&e!==""?`${e}:`:"",u=[];for(const[a,n]of l){if(!/^[A-Za-z][A-Za-z0-9]*$/.test(a))throw new Error(`Invalid CloudFormation parameter name "${a}": must match /^[A-Za-z][A-Za-z0-9]*$/ (alphanumeric, leading letter, no separators).`);if(n==="")throw new Error(`CloudFormation parameter "${a}" has an empty value.`);if(/[,\n\r]/.test(n))throw new Error(`CloudFormation parameter "${a}" value contains "," or newline \u2014 cdk's --parameters splits on "," so the deploy would silently fragment.`);u.push("--parameters",`${o}${a}=${n}`)}return u}injectCascadeCredentials(r,e){e&&(r.AWS_ACCESS_KEY_ID=e.accessKeyId,r.AWS_SECRET_ACCESS_KEY=e.secretAccessKey,delete r.AWS_SESSION_TOKEN,e.sessionToken&&(r.AWS_SESSION_TOKEN=e.sessionToken))}buildCdkEnv(r){const e={...i(process.env),CI:"true",FORCE_COLOR:"0",CDK_DISABLE_VERSION_CHECK:"1",NODE_NO_WARNINGS:"1"};return r?.context?.region&&(e.AWS_REGION=r.context.region,e.AWS_DEFAULT_REGION=r.context.region,e.CDK_DEFAULT_REGION=r.context.region),r?.context?.accountId&&(e.CDK_DEFAULT_ACCOUNT=r.context.accountId),this.injectCascadeCredentials(e,r?.credentials),e}}export{d as CdkArgumentBuilder};
@@ -1,2 +1,2 @@
1
- import{tmpdir as l}from"os";import{join as C}from"path";import{logger as m}from"@fjall/util/logger";import{success as c,failure as i}from"@fjall/generator";import{CdkError as f}from"../../types/errors/CdkError.js";import{DEFAULT_REGION as O}from"../../aws/utils/regions.js";import{CdkArgumentBuilder as E}from"./CdkArgumentBuilder.js";import{isCdkError as A,formatInfrastructureError as d}from"./CdkErrorFormatter.js";import{hasCdkDifferences as P,parseDiffOutput as _}from"./CdkOutputParser.js";import{DEFAULT_DEPLOY_TIMEOUT_MS as h}from"./CdkEventMonitoring.js";import{analyseBootstrapError as g}from"./CdkOutputAnalyser.js";const R=3e5,k=18e4,L=12e5,y="cdk.out",u=Object.freeze({APP:"--app",CI:"--ci",REQUIRE_APPROVAL:"--require-approval",APPROVAL_NEVER:"never",VERBOSE:"--verbose",NO_LOOKUPS:"--no-lookups",ALL:"--all"});class K{processManager;argumentBuilder=new E;constructor(o){this.processManager=o}dispose(){this.processManager.dispose()}async checkDifferences(o,t,r){const e=["diff"];t?e.push(t):e.push(u.ALL),e.push("--no-color"),r?.noLookups&&e.push(u.NO_LOOKUPS);const s=await this.processManager.runCdkCommand(o,e,{...r,ignoreExitCode:!0,combineOutput:!0});if(!s.success)return i(new f(d(s.error,o),"diff_failed",void 0,void 0,s.error,void 0,!1));const a=s.data.output||"";if(A(a))return i(new f(d(a,o),"diff_failed",void 0,void 0,a,void 0,!1));const n=P(a),p=_(a);return c({hasDifferences:n,output:a,details:p})}async deploy(o,t,r){const e=["deploy"];r?.appDir?e.push(u.APP,r.appDir):r?.useCdkOut&&e.push(u.APP,"cdk.out"),t?e.push(t):e.push(u.ALL),e.push(u.REQUIRE_APPROVAL,u.APPROVAL_NEVER),e.push(u.CI),e.push("--no-version-reporting"),e.push("--no-path-metadata"),e.push("--no-asset-metadata"),r?.verbose?e.push(u.VERBOSE):e.push("--progress","events"),e.push(...this.argumentBuilder.buildParameterArgs(r?.parameters));const s={...r,timeout:r?.timeout||h};return r?.passThroughCDK?this.processManager.runCdkCommandPassthrough(o,e,s):this.processManager.runCdkCommand(o,e,s)}async destroy(o,t,r){const e=["destroy"];r?.appDir?e.push(u.APP,r.appDir):r?.useCdkOut&&e.push(u.APP,"cdk.out"),t?e.push(t):e.push(u.ALL),e.push("--force"),r?.verbose&&e.push(u.VERBOSE);const s={...r,timeout:r?.timeout||h};return this.processManager.runCdkCommand(o,e,s)}async runImport(o,t,r){const e=["import"];r?.stacks&&r.stacks.length>0&&e.push(...r.stacks),t&&e.push("--resource-mapping",t),e.push(u.REQUIRE_APPROVAL,u.APPROVAL_NEVER),e.push(u.CI),r?.noLookups&&e.push(u.NO_LOOKUPS),r?.verbose&&(e.push(u.VERBOSE),e.push("--trace"),e.push("--debug"));const s={...r,timeout:r?.timeout||L};return r?.passThroughCDK?this.processManager.runCdkCommandPassthrough(o,e,s):this.processManager.runCdkCommand(o,e,s)}async synth(o,t){t?.outputCallback?.(`Synthesising CloudFormation template...
2
- `);const r=t?.noLookups?[u.NO_LOOKUPS]:[],s=["--output",t?.outputDir??C(o,y)],a=t?.context?.region||O,n=await this.processManager.runCdkCommand(o,["synth",...r,...s,u.CI,"--quiet"],{...t,context:{...t?.context,region:a},timeout:t?.timeout||R});if(!n.success){const p=n.error?d(n.error,o):"Failed to synthesise CloudFormation template";return i(p)}return n}async bootstrap(o,t,r){const e=l();m.debug("CdkService","Starting CDK bootstrap",{accountId:o,region:t,bootstrapPath:e,target:`aws://${o}/${t}`});const s=await this.processManager.runCdkCommand(e,["bootstrap",`aws://${o}/${t}`,"--cloudformation-execution-policies","arn:aws:iam::aws:policy/AdministratorAccess","-c",`accountId=${o}`,u.REQUIRE_APPROVAL,u.APPROVAL_NEVER,u.CI,"--quiet","--force"],{...r,timeout:r?.timeout||k,context:{region:t,accountId:o},credentials:r?.credentials,skipProjectCheck:!0,extraEnv:{TERM:"dumb",CDK_DISABLE_NOTICES:"true",CDK_DISABLE_PROGRESS_BAR:"true"}});return m.debug("CdkService",s.success?"Bootstrap completed successfully":"Bootstrap exited with non-zero code",{accountId:o,region:t,exitCode:s.success?s.data.exitCode:void 0,output:s.success&&s.data.output?.trim()||"(empty)",error:s.success?"(empty)":s.error.trim()||"(empty)"}),s.success?c({output:"AWS environment bootstrapped",exitCode:0}):s.error.includes("already bootstrapped")?c({output:"Environment is already bootstrapped",exitCode:0}):i(g(s.error))}}export{k as BOOTSTRAP_TIMEOUT_MS,K as CdkCommandRunner,L as IMPORT_TIMEOUT_MS,R as SYNTH_TIMEOUT_MS};
1
+ import{tmpdir as l}from"os";import{join as C}from"path";import{logger as m}from"@fjall/util/logger";import{success as d,failure as i}from"@fjall/generator";import{CdkError as f}from"../../types/errors/CdkError.js";import{DEFAULT_REGION as O}from"../../aws/utils/regions.js";import{CdkArgumentBuilder as E}from"./CdkArgumentBuilder.js";import{isCdkError as A,formatInfrastructureError as c}from"./CdkErrorFormatter.js";import{hasCdkDifferences as P,parseDiffOutput as _}from"./CdkOutputParser.js";import{DEFAULT_DEPLOY_TIMEOUT_MS as h}from"./CdkEventMonitoring.js";import{analyseBootstrapError as g}from"./CdkOutputAnalyser.js";const R=3e5,L=18e4,k=12e5,y="cdk.out",u=Object.freeze({APP:"--app",CI:"--ci",REQUIRE_APPROVAL:"--require-approval",APPROVAL_NEVER:"never",VERBOSE:"--verbose",NO_LOOKUPS:"--no-lookups",ALL:"--all"});class K{processManager;argumentBuilder=new E;constructor(o){this.processManager=o}dispose(){this.processManager.dispose()}async checkDifferences(o,t,r){const e=["diff"];t?e.push(t):e.push(u.ALL),e.push("--no-color"),r?.noLookups&&e.push(u.NO_LOOKUPS);const s=await this.processManager.runCdkCommand(o,e,{...r,ignoreExitCode:!0,combineOutput:!0});if(!s.success)return i(new f(c(s.error,o),"diff_failed",void 0,void 0,s.error,void 0,!1));const a=s.data.output||"";if(A(a))return i(new f(c(a,o),"diff_failed",void 0,void 0,a,void 0,!1));const n=P(a),p=_(a);return d({hasDifferences:n,output:a,details:p})}async deploy(o,t,r){const e=["deploy"];r?.appDir?e.push(u.APP,r.appDir):r?.useCdkOut&&e.push(u.APP,"cdk.out"),t?e.push(t):e.push(u.ALL),e.push(u.REQUIRE_APPROVAL,u.APPROVAL_NEVER),e.push(u.CI),e.push("--no-version-reporting"),e.push("--no-path-metadata"),e.push("--no-asset-metadata"),r?.verbose?e.push(u.VERBOSE):e.push("--progress","events"),e.push(...this.argumentBuilder.buildParameterArgs(r?.parameters,t));const s={...r,timeout:r?.timeout||h};return r?.passThroughCDK?this.processManager.runCdkCommandPassthrough(o,e,s):this.processManager.runCdkCommand(o,e,s)}async destroy(o,t,r){const e=["destroy"];r?.appDir?e.push(u.APP,r.appDir):r?.useCdkOut&&e.push(u.APP,"cdk.out"),t?e.push(t):e.push(u.ALL),e.push("--force"),r?.verbose&&e.push(u.VERBOSE);const s={...r,timeout:r?.timeout||h};return this.processManager.runCdkCommand(o,e,s)}async runImport(o,t,r){const e=["import"];r?.stacks&&r.stacks.length>0&&e.push(...r.stacks),t&&e.push("--resource-mapping",t),e.push(u.REQUIRE_APPROVAL,u.APPROVAL_NEVER),e.push(u.CI),r?.noLookups&&e.push(u.NO_LOOKUPS),r?.verbose&&(e.push(u.VERBOSE),e.push("--trace"),e.push("--debug"));const s={...r,timeout:r?.timeout||k};return r?.passThroughCDK?this.processManager.runCdkCommandPassthrough(o,e,s):this.processManager.runCdkCommand(o,e,s)}async synth(o,t){t?.outputCallback?.(`Synthesising CloudFormation template...
2
+ `);const r=t?.noLookups?[u.NO_LOOKUPS]:[],s=["--output",t?.outputDir??C(o,y)],a=t?.context?.region||O,n=await this.processManager.runCdkCommand(o,["synth",...r,...s,u.CI,"--quiet"],{...t,context:{...t?.context,region:a},timeout:t?.timeout||R});if(!n.success){const p=n.error?c(n.error,o):"Failed to synthesise CloudFormation template";return i(p)}return n}async bootstrap(o,t,r){const e=l();m.debug("CdkService","Starting CDK bootstrap",{accountId:o,region:t,bootstrapPath:e,target:`aws://${o}/${t}`});const s=await this.processManager.runCdkCommand(e,["bootstrap",`aws://${o}/${t}`,"--cloudformation-execution-policies","arn:aws:iam::aws:policy/AdministratorAccess","-c",`accountId=${o}`,u.REQUIRE_APPROVAL,u.APPROVAL_NEVER,u.CI,"--quiet","--force"],{...r,timeout:r?.timeout||L,context:{region:t,accountId:o},credentials:r?.credentials,skipProjectCheck:!0,extraEnv:{TERM:"dumb",CDK_DISABLE_NOTICES:"true",CDK_DISABLE_PROGRESS_BAR:"true"}});return m.debug("CdkService",s.success?"Bootstrap completed successfully":"Bootstrap exited with non-zero code",{accountId:o,region:t,exitCode:s.success?s.data.exitCode:void 0,output:s.success&&s.data.output?.trim()||"(empty)",error:s.success?"(empty)":s.error.trim()||"(empty)"}),s.success?d({output:"AWS environment bootstrapped",exitCode:0}):s.error.includes("already bootstrapped")?d({output:"Environment is already bootstrapped",exitCode:0}):i(g(s.error))}}export{L as BOOTSTRAP_TIMEOUT_MS,K as CdkCommandRunner,k as IMPORT_TIMEOUT_MS,R as SYNTH_TIMEOUT_MS};
@@ -1,3 +1,3 @@
1
- import{spawn as K}from"child_process";import{existsSync as E}from"fs";import{join as b}from"path";import{createRequire as A}from"module";import{logger as T}from"@fjall/util/logger";import{filterDangerousEnvVars as L,maskSensitiveOutput as g}from"@fjall/util";import{success as O,failure as i}from"@fjall/generator";import{getErrorMessage as w}from"@fjall/util";function j(){try{const e=A(import.meta.url).resolve("aws-cdk/bin/cdk");return{command:process.execPath,prefixArgs:[e]}}catch(h){return T.debug("CdkService","Failed to resolve aws-cdk binary, falling back to npx",{error:h instanceof Error?h.message:String(h)}),{command:"npx",prefixArgs:["cdk"]}}}const x=j(),G=5e3,H=1800*1e3;class J{runningProcesses=new Map;processCounter=0;argBuilder;exitHandler;sigintHandler;sigtermHandler;constructor(e){this.argBuilder=e,this.exitHandler=()=>this.cleanup(),this.sigintHandler=()=>{this.cleanup(),process.exit(130)},this.sigtermHandler=()=>{this.cleanup(),process.exit(143)},process.on("exit",this.exitHandler),process.on("SIGINT",this.sigintHandler),process.on("SIGTERM",this.sigtermHandler)}forceKillProcess(e){e.stdout?.destroy(),e.stderr?.destroy(),e.kill("SIGTERM");const c=setTimeout(()=>{e.exitCode===null&&e.kill("SIGKILL")},G);c.unref(),e.once("exit",()=>clearTimeout(c))}cleanup(){for(const[e,c]of this.runningProcesses)c.killed||(c.stdout?.destroy(),c.stderr?.destroy(),c.kill("SIGTERM"));this.runningProcesses.clear()}dispose(){this.cleanup(),process.removeListener("exit",this.exitHandler),process.removeListener("SIGINT",this.sigintHandler),process.removeListener("SIGTERM",this.sigtermHandler)}async runCdkCommandPassthrough(e,c,r){return new Promise(t=>{if(!E(e)){t(i(`Directory not found: ${e}`));return}const a=b(e,"cdk.json");if(!E(a)){t(i(`No CDK project found in ${e}`));return}const m=this.argBuilder.buildContextArgs(r?.context),f=[...x.prefixArgs,...c,...m],S=this.argBuilder.buildCdkEnv(r),d=K(x.command,f,{cwd:e,env:S,stdio:"inherit",shell:!1,detached:!1});if(!d.pid){d.on("error",u=>{T.debug("CdkProcess","Spawn error on failed child process",{error:u.message})}),t(i(`Failed to spawn CDK process - no PID. cwd=${e}, args=${f.join(" ")}`));return}const C=`cdk-passthrough-${++this.processCounter}`;this.runningProcesses.set(C,d);let o=!1,l=!1;const p=r?.timeout??H,k=setTimeout(()=>{d.killed||(o=!0,this.forceKillProcess(d))},p);d.on("close",u=>{if(clearTimeout(k),this.runningProcesses.delete(C),!l){if(l=!0,o){t(i("CDK command timed out"));return}u===0||r?.ignoreExitCode?t(O({exitCode:u||0})):t(i(`CDK command failed with exit code ${u}`))}}),d.on("error",u=>{clearTimeout(k),this.runningProcesses.delete(C),!(l||o)&&(l=!0,t(i(`Failed to run CDK command: ${u.message}`)))})})}async runCdkCommand(e,c,r){return new Promise(t=>{if(!E(e)){t(i(`Directory not found: ${e}`));return}if(!r?.skipProjectCheck){const s=b(e,"cdk.json");if(!E(s)){t(i(`No CDK project found in ${e}`));return}}let a="",m="",f=!1;const S=this.argBuilder.buildContextArgs(r?.context),d=[...x.prefixArgs,...c,...S],C={...this.argBuilder.buildCdkEnv(r),NO_COLOR:"1",...r?.extraEnv?L(r.extraEnv):{}};T.debug("CdkService","Spawning CDK process",{command:`${x.command} ${d.join(" ")}`,workingDir:e});const o=K(x.command,d,{cwd:e,env:C,stdio:["ignore","pipe","pipe"],shell:!1,detached:!1});if(!o.pid){const s=`Failed to spawn CDK process - no PID. cwd=${e}, args=${d.join(" ")}`;o.on("error",n=>{T.debug("CdkProcess","Deferred spawn error on failed child process",{error:n.message})}),t(i(s));return}const l=`cdk-${++this.processCounter}`;this.runningProcesses.set(l,o);let p=!1;const k=r?.timeout??H,u=setTimeout(()=>{o.killed||(f=!0,this.forceKillProcess(o))},k);o.stdout?.on("data",s=>{const n=s.toString();a+=n,r?.outputCallback&&r.outputCallback(g(n)),r?.cdkOutputLogger?.writeCdkOutput("stdout",g(n))}),o.stderr?.on("data",s=>{const n=s.toString();r?.cdkOutputLogger?.writeCdkOutput("stderr",g(n)),!n.includes("deprecated")&&!n.includes("npm WARN")&&!n.includes("ENOENT")&&(m+=n),r?.errorCallback&&r.errorCallback(g(n))}),o.on("error",s=>{clearTimeout(u),this.runningProcesses.delete(l),!(p||f)&&(p=!0,t(i(w(s))))}),o.on("close",s=>{if(clearTimeout(u),this.runningProcesses.delete(l),p)return;if(p=!0,f){t(i("CDK command timed out"));return}const n=s===0||r?.ignoreExitCode===!0&&s===1,M=a+(m?`
2
- ${m}`:"");if(n){const P=r?.combineOutput?M:a;t(O({output:g(P),exitCode:s||0}));return}let $=m;if(a){const P=a.match(/❌.*?Error:(.*)$/m);P&&($=P[1]?.trim()??"")}const I=$||`CDK command failed with exit code ${s}`,y=a?`${I}
3
- ${a}`:I;t(i(g(y)))})})}}export{J as CdkProcessManager};
1
+ import{spawn as K}from"child_process";import{existsSync as E}from"fs";import{join as b}from"path";import{createRequire as A}from"module";import{logger as T}from"@fjall/util/logger";import{filterDangerousEnvVars as w,maskSensitiveOutput as g}from"@fjall/util";import{success as O,failure as i}from"@fjall/generator";import{getErrorMessage as L}from"@fjall/util";function j(){try{const e=A(import.meta.url).resolve("aws-cdk/bin/cdk");return{command:process.execPath,prefixArgs:[e]}}catch(h){return T.debug("CdkService","Failed to resolve aws-cdk binary, falling back to npx",{error:h instanceof Error?h.message:String(h)}),{command:"npx",prefixArgs:["cdk"]}}}const x=j(),G=5e3,H=1800*1e3;class J{runningProcesses=new Map;processCounter=0;argBuilder;exitHandler;sigintHandler;sigtermHandler;constructor(e){this.argBuilder=e,this.exitHandler=()=>this.cleanup(),this.sigintHandler=()=>{this.cleanup(),process.exit(130)},this.sigtermHandler=()=>{this.cleanup(),process.exit(143)},process.on("exit",this.exitHandler),process.on("SIGINT",this.sigintHandler),process.on("SIGTERM",this.sigtermHandler)}forceKillProcess(e){e.stdout?.destroy(),e.stderr?.destroy(),e.kill("SIGTERM");const c=setTimeout(()=>{e.exitCode===null&&e.kill("SIGKILL")},G);c.unref(),e.once("exit",()=>clearTimeout(c))}cleanup(){for(const[e,c]of this.runningProcesses)c.killed||(c.stdout?.destroy(),c.stderr?.destroy(),c.kill("SIGTERM"));this.runningProcesses.clear()}dispose(){this.cleanup(),process.removeListener("exit",this.exitHandler),process.removeListener("SIGINT",this.sigintHandler),process.removeListener("SIGTERM",this.sigtermHandler)}async runCdkCommandPassthrough(e,c,r){return new Promise(s=>{if(!E(e)){s(i(`Directory not found: ${e}`));return}const a=b(e,"cdk.json");if(!E(a)){s(i(`No CDK project found in ${e}`));return}const m=this.argBuilder.buildContextArgs(r?.context),f=[...x.prefixArgs,...c,...m],S=this.argBuilder.buildCdkEnv(r),d=K(x.command,f,{cwd:e,env:S,stdio:"inherit",shell:!1,detached:!1});if(!d.pid){d.on("error",u=>{T.debug("CdkProcess","Spawn error on failed child process",{error:u.message})}),s(i(`Failed to spawn CDK process - no PID. cwd=${e}, args=${f.join(" ")}`));return}const C=`cdk-passthrough-${++this.processCounter}`;this.runningProcesses.set(C,d);let o=!1,l=!1;const p=r?.timeout??H,k=setTimeout(()=>{d.killed||(o=!0,this.forceKillProcess(d))},p);d.on("close",u=>{if(clearTimeout(k),this.runningProcesses.delete(C),!l){if(l=!0,o){s(i("CDK command timed out"));return}u===0||r?.ignoreExitCode?s(O({exitCode:u||0})):s(i(`CDK command failed with exit code ${u}`))}}),d.on("error",u=>{clearTimeout(k),this.runningProcesses.delete(C),!(l||o)&&(l=!0,s(i(`Failed to run CDK command: ${u.message}`)))})})}async runCdkCommand(e,c,r){return new Promise(s=>{if(!E(e)){s(i(`Directory not found: ${e}`));return}if(!r?.skipProjectCheck){const n=b(e,"cdk.json");if(!E(n)){s(i(`No CDK project found in ${e}`));return}}let a="",m="",f=!1;const S=this.argBuilder.buildContextArgs(r?.context),d=[...x.prefixArgs,...c,...S],C={...this.argBuilder.buildCdkEnv(r),NO_COLOR:"1",...r?.extraEnv?w(r.extraEnv):{}};T.debug("CdkService","Spawning CDK process",{command:`${x.command} ${d.join(" ")}`,workingDir:e});const o=K(x.command,d,{cwd:e,env:C,stdio:["ignore","pipe","pipe"],shell:!1,detached:!1});if(!o.pid){const n=`Failed to spawn CDK process - no PID. cwd=${e}, args=${d.join(" ")}`;o.on("error",t=>{T.debug("CdkProcess","Deferred spawn error on failed child process",{error:t.message})}),s(i(n));return}const l=`cdk-${++this.processCounter}`;this.runningProcesses.set(l,o);let p=!1;const k=r?.timeout??H,u=setTimeout(()=>{o.killed||(f=!0,this.forceKillProcess(o))},k);o.stdout?.on("data",n=>{const t=n.toString();a+=t,r?.outputCallback&&r.outputCallback(g(t)),r?.cdkOutputLogger?.writeCdkOutput("stdout",g(t))}),o.stderr?.on("data",n=>{const t=n.toString();r?.cdkOutputLogger?.writeCdkOutput("stderr",g(t)),!t.includes("deprecated")&&!t.includes("npm WARN")&&!t.includes("ENOENT")&&!/\(node:\d+\)/.test(t)&&!t.includes("ExperimentalWarning")&&!t.includes("will no longer support")&&(m+=t),r?.errorCallback&&r.errorCallback(g(t))}),o.on("error",n=>{clearTimeout(u),this.runningProcesses.delete(l),!(p||f)&&(p=!0,s(i(L(n))))}),o.on("close",n=>{if(clearTimeout(u),this.runningProcesses.delete(l),p)return;if(p=!0,f){s(i("CDK command timed out"));return}const t=n===0||r?.ignoreExitCode===!0&&n===1,M=a+(m?`
2
+ ${m}`:"");if(t){const P=r?.combineOutput?M:a;s(O({output:g(P),exitCode:n||0}));return}let $=m;if(a){const P=a.match(/❌.*?Error:(.*)$/m);P&&($=P[1]?.trim()??"")}const I=$||`CDK command failed with exit code ${n}`,y=a?`${I}
3
+ ${a}`:I;s(i(g(y)))})})}}export{J as CdkProcessManager};