@fjall/deploy-core 2.12.0 → 2.14.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 (126) hide show
  1. package/dist/.minified +1 -1
  2. package/dist/src/aws/cloudtrail/orgTrailDelivery.d.ts +44 -0
  3. package/dist/src/aws/cloudtrail/orgTrailDelivery.js +1 -0
  4. package/dist/src/aws/index.d.ts +6 -2
  5. package/dist/src/aws/index.js +1 -1
  6. package/dist/src/aws/organisations/accountGlobals.d.ts +40 -0
  7. package/dist/src/aws/organisations/accountGlobals.js +1 -0
  8. package/dist/src/aws/organisations/accounts.d.ts +3 -1
  9. package/dist/src/aws/organisations/accounts.js +1 -1
  10. package/dist/src/aws/organisations/backup.d.ts +2 -1
  11. package/dist/src/aws/organisations/backup.js +2 -2
  12. package/dist/src/aws/organisations/importedAccounts.d.ts +16 -0
  13. package/dist/src/aws/organisations/importedAccounts.js +1 -0
  14. package/dist/src/aws/organisations/index.d.ts +3 -1
  15. package/dist/src/aws/organisations/index.js +1 -1
  16. package/dist/src/aws/organisations/organisationalUnits.d.ts +1 -1
  17. package/dist/src/aws/organisations/policies.js +1 -1
  18. package/dist/src/aws/organisations/rootAccess.d.ts +27 -0
  19. package/dist/src/aws/organisations/rootAccess.js +3 -0
  20. package/dist/src/aws/organisations/serviceAccess.d.ts +6 -0
  21. package/dist/src/aws/organisations/serviceAccess.js +1 -1
  22. package/dist/src/aws/organisations/types.d.ts +18 -0
  23. package/dist/src/aws/organisations/types.js +1 -1
  24. package/dist/src/aws/sts/assumeRoot.d.ts +46 -0
  25. package/dist/src/aws/sts/assumeRoot.js +1 -0
  26. package/dist/src/aws/targetReadiness.d.ts +70 -0
  27. package/dist/src/aws/targetReadiness.js +1 -0
  28. package/dist/src/aws/targetSetAdvisory.d.ts +24 -0
  29. package/dist/src/aws/targetSetAdvisory.js +1 -0
  30. package/dist/src/events/index.d.ts +2 -0
  31. package/dist/src/events/index.js +1 -1
  32. package/dist/src/index.d.ts +18 -14
  33. package/dist/src/index.js +1 -1
  34. package/dist/src/orchestration/accountsConfig.d.ts +11 -0
  35. package/dist/src/orchestration/accountsConfig.js +1 -1
  36. package/dist/src/orchestration/applicationDeploy.js +1 -1
  37. package/dist/src/orchestration/applicationDestroy.js +1 -1
  38. package/dist/src/orchestration/cascadeDestroyHelpers.d.ts +12 -1
  39. package/dist/src/orchestration/cascadeDestroyHelpers.js +1 -1
  40. package/dist/src/orchestration/cascadeHelpers.d.ts +36 -5
  41. package/dist/src/orchestration/cascadeHelpers.js +1 -1
  42. package/dist/src/orchestration/contextHelpers.d.ts +20 -2
  43. package/dist/src/orchestration/contextHelpers.js +1 -1
  44. package/dist/src/orchestration/index.d.ts +13 -4
  45. package/dist/src/orchestration/index.js +1 -1
  46. package/dist/src/orchestration/organisationDeploy/cascadeExecution.d.ts +28 -0
  47. package/dist/src/orchestration/organisationDeploy/cascadeExecution.js +1 -0
  48. package/dist/src/orchestration/organisationDeploy/infraSteps.d.ts +40 -0
  49. package/dist/src/orchestration/organisationDeploy/infraSteps.js +1 -0
  50. package/dist/src/orchestration/organisationDeploy/orgCascadeDeploy.d.ts +8 -0
  51. package/dist/src/orchestration/organisationDeploy/orgCascadeDeploy.js +5 -0
  52. package/dist/src/orchestration/organisationDeploy/orgContext.d.ts +12 -0
  53. package/dist/src/orchestration/organisationDeploy/orgContext.js +1 -0
  54. package/dist/src/orchestration/organisationDeploy/resolveCascadeAccounts.d.ts +15 -0
  55. package/dist/src/orchestration/organisationDeploy/resolveCascadeAccounts.js +1 -0
  56. package/dist/src/orchestration/organisationDeploy/singleComponentDeploy.d.ts +11 -0
  57. package/dist/src/orchestration/organisationDeploy/singleComponentDeploy.js +1 -0
  58. package/dist/src/orchestration/organisationDeploy/trailReconciliation.d.ts +21 -0
  59. package/dist/src/orchestration/organisationDeploy/trailReconciliation.js +1 -0
  60. package/dist/src/orchestration/organisationDeploy.d.ts +1 -5
  61. package/dist/src/orchestration/organisationDeploy.js +1 -5
  62. package/dist/src/orchestration/organisationDestroy.d.ts +1 -1
  63. package/dist/src/orchestration/organisationDestroy.js +2 -2
  64. package/dist/src/orchestration/organisationSetup.d.ts +23 -3
  65. package/dist/src/orchestration/organisationSetup.js +1 -1
  66. package/dist/src/orchestration/reconcileProviderAccounts.js +1 -1
  67. package/dist/src/orchestration/stackCleanup/bucketOps.d.ts +54 -0
  68. package/dist/src/orchestration/stackCleanup/bucketOps.js +1 -0
  69. package/dist/src/orchestration/stackCleanup/failedStack.d.ts +34 -0
  70. package/dist/src/orchestration/stackCleanup/failedStack.js +1 -0
  71. package/dist/src/orchestration/stackCleanup/logging.d.ts +9 -0
  72. package/dist/src/orchestration/stackCleanup/logging.js +1 -0
  73. package/dist/src/orchestration/stackCleanup/messages.d.ts +16 -0
  74. package/dist/src/orchestration/stackCleanup/messages.js +1 -0
  75. package/dist/src/orchestration/stackCleanup/orphanSweep.d.ts +25 -0
  76. package/dist/src/orchestration/stackCleanup/orphanSweep.js +1 -0
  77. package/dist/src/orchestration/stackCleanup/preEmpty.d.ts +35 -0
  78. package/dist/src/orchestration/stackCleanup/preEmpty.js +1 -0
  79. package/dist/src/orchestration/stackCleanup/stackResources.d.ts +9 -0
  80. package/dist/src/orchestration/stackCleanup/stackResources.js +1 -0
  81. package/dist/src/orchestration/stackCleanup.d.ts +13 -33
  82. package/dist/src/orchestration/stackCleanup.js +1 -1
  83. package/dist/src/orchestration/trailMigration/memberTrailCleanup.d.ts +43 -0
  84. package/dist/src/orchestration/trailMigration/memberTrailCleanup.js +1 -0
  85. package/dist/src/orchestration/trailMigration/trailMigration.d.ts +64 -0
  86. package/dist/src/orchestration/trailMigration/trailMigration.js +1 -0
  87. package/dist/src/orchestration/unlock/scpRemediation.d.ts +15 -0
  88. package/dist/src/orchestration/unlock/scpRemediation.js +1 -0
  89. package/dist/src/orchestration/unlock/unlockBucket.d.ts +37 -0
  90. package/dist/src/orchestration/unlock/unlockBucket.js +1 -0
  91. package/dist/src/orchestration/unlock/unlockQueue.d.ts +43 -0
  92. package/dist/src/orchestration/unlock/unlockQueue.js +1 -0
  93. package/dist/src/services/application/ApplicationStackService.d.ts +9 -10
  94. package/dist/src/services/application/ApplicationStackService.js +1 -1
  95. package/dist/src/services/application/applicationStackHelpers.d.ts +13 -8
  96. package/dist/src/services/application/applicationStackHelpers.js +3 -3
  97. package/dist/src/services/infrastructure/CdkArgumentBuilder.js +1 -1
  98. package/dist/src/services/infrastructure/CdkServiceTypes.d.ts +1 -0
  99. package/dist/src/services/infrastructure/cdkServiceHelpers.js +1 -1
  100. package/dist/src/services/supporting/CdkContextBuilder.d.ts +1 -0
  101. package/dist/src/services/supporting/CdkContextBuilder.js +1 -1
  102. package/dist/src/steps/stepRegistry.js +1 -1
  103. package/dist/src/types/FjallState.d.ts +7 -0
  104. package/dist/src/types/FjallState.js +1 -1
  105. package/dist/src/types/callbackKeys.d.ts +1 -1
  106. package/dist/src/types/callbackKeys.js +1 -1
  107. package/dist/src/types/callbacks.d.ts +58 -3
  108. package/dist/src/types/callbacks.js +1 -0
  109. package/dist/src/types/deployment/DeploymentTypes.d.ts +1 -0
  110. package/dist/src/types/deploymentEventSchema.d.ts +28 -3
  111. package/dist/src/types/deploymentEventSchema.js +1 -1
  112. package/dist/src/types/events.d.ts +12 -0
  113. package/dist/src/types/events.js +1 -0
  114. package/dist/src/types/index.d.ts +7 -11
  115. package/dist/src/types/index.js +1 -1
  116. package/dist/src/types/orgConfig.d.ts +8 -2
  117. package/dist/src/types/params.d.ts +18 -0
  118. package/dist/src/types/patternDetection.d.ts +0 -25
  119. package/dist/src/types/patternDetection.js +1 -1
  120. package/dist/src/types/stepDefinitions.d.ts +2 -0
  121. package/dist/src/types/stepDefinitions.js +1 -1
  122. package/dist/src/util/index.d.ts +1 -0
  123. package/dist/src/util/index.js +1 -1
  124. package/dist/src/util/sleepAbortable.d.ts +8 -0
  125. package/dist/src/util/sleepAbortable.js +1 -0
  126. package/package.json +8 -4
@@ -0,0 +1 @@
1
+ import{success as m,failure as k}from"@fjall/generator";import{logger as h}from"@fjall/util/logger";import{getErrorMessage as n,maskSensitiveOutput as d}from"@fjall/util";import{isAborted as y}from"../../aws/organisations/types.js";import{STACK_NOT_FOUND_PATTERN as g}from"../../types/constants.js";import{classifyBucketForPreEmpty as S,emptyS3Bucket as B}from"./bucketOps.js";import{STACK_CLEANUP_LOG as w,warnViaCallbacks as a}from"./logging.js";import{formatRetainedBucketsMessage as A}from"./messages.js";import{collectStackResources as E}from"./stackResources.js";async function b(i,c,s,o,p){let u;try{u=await E(i,s,e=>e.ResourceType==="AWS::S3::Bucket",e=>e.PhysicalResourceId,p)}catch(e){if(e instanceof Error&&e.message?.includes(g))return h.debug(w,`Stack ${s} does not exist, nothing to pre-empty`),m({matched:[],skipped:[]});const r=`Could not list ${s} resources for the pre-empty pass: ${d(n(e))}`;return a(r,o),k(new Error(r))}const t={matched:[],skipped:[]};for(const e of u){if(y(p))break;try{const r=await S(c,e,o,p);r==="matched"?(t.matched.push(e),await B(c,e,o,p)):(r==="retained"||r==="unreadable")&&t.skipped.push(e)}catch(r){const f=`Pre-empty pass failed for bucket ${e}: ${d(n(r))}`;a(f,o)}}return t.skipped.length>0&&o?.onStackCleanupProgress?.(s,"retained-buckets",{retainedBuckets:[...t.skipped],message:A(s,t.skipped)}),m(t)}function v(i){return i.success?i.data.matched:[]}export{v as capturedSweepBuckets,b as preEmptyStackBuckets};
@@ -0,0 +1,9 @@
1
+ import { type CloudFormationClient } from "@aws-sdk/client-cloudformation";
2
+ /** Paginate ListStackResources with a bounded page limit and collect matching resource IDs. */
3
+ export declare function collectStackResources(cfnClient: CloudFormationClient, stackName: string, filter: (resource: {
4
+ ResourceType?: string;
5
+ ResourceStatus?: string;
6
+ }) => boolean, getId: (resource: {
7
+ PhysicalResourceId?: string;
8
+ LogicalResourceId?: string;
9
+ }) => string | undefined, abortSignal?: AbortSignal): Promise<string[]>;
@@ -0,0 +1 @@
1
+ import{ListStackResourcesCommand as S}from"@aws-sdk/client-cloudformation";import{logger as k}from"@fjall/util/logger";import{composeSdkAbortSignal as l,isAborted as p}from"../../aws/organisations/types.js";import{STACK_CLEANUP_LOG as u}from"./logging.js";async function w(n,o,a,m,t){const r=[];let e,f=0;do{if(p(t))break;if(f++>=100){k.warn(u,"Reached 100 page limit listing stack resources",{stackName:o});break}const s=await n.send(new S({StackName:o,NextToken:e}),{abortSignal:l(t)});for(const c of s.StackResourceSummaries??[])if(a(c)){const i=m(c);i&&r.push(i)}e=s.NextToken}while(e);return r}export{w as collectStackResources};
@@ -1,36 +1,16 @@
1
1
  /**
2
- * Utility functions for cleaning up CloudFormation stacks stuck in failed states.
2
+ * Stack-cleanup barrel implementations live in `stackCleanup/<concern>.ts`.
3
+ * This path is the stable import surface for the production consumers
4
+ * (orchestration barrel, cascade/organisation destroy, trail migration,
5
+ * ApplicationStackService); keep every public symbol re-exported here.
3
6
  *
4
- * Only operates on stacks that never successfully deployed:
5
- * - ROLLBACK_FAILED -- creation failed, rollback failed
6
- * - ROLLBACK_COMPLETE -- creation failed, rollback succeeded
7
- * - DELETE_FAILED -- deletion already started
8
- *
9
- * Never touches UPDATE_ROLLBACK_FAILED (has live resources from previous deploy).
10
- *
11
- * Ported from cli/src/services/utils/stackCleanup.ts for deploy-core consumers
12
- * (webapp worker, CLI via deploy-core).
13
- */
14
- import type { DeployCallbacks } from "../types/callbacks.js";
15
- import { SAFE_CLEANUP_STATES, isCleanableState } from "../types/constants.js";
16
- export { SAFE_CLEANUP_STATES, isCleanableState };
17
- interface StackCleanupCredentials {
18
- accessKeyId: string;
19
- secretAccessKey: string;
20
- sessionToken?: string;
21
- }
22
- /**
23
- * Clean up a CloudFormation stack stuck in a failed state.
24
- * Best-effort: all errors are caught and logged, never throws.
25
- *
26
- * Flow:
27
- * 1. Check stack status -- skip if not in SAFE_CLEANUP_STATES
28
- * 2. Find and empty S3 buckets blocking deletion
29
- * 3. Delete the stack
30
- * 4. Poll for DELETE_COMPLETE (5min timeout, 5s interval)
31
- * 5. If DELETE_FAILED again, retry once with RetainResources for non-S3 failures
7
+ * Originally ported from cli/src/services/utils/stackCleanup.ts for
8
+ * deploy-core consumers (webapp worker, CLI via deploy-core).
32
9
  */
33
- export declare function cleanupFailedStack(stackName: string, region: string, credentials: StackCleanupCredentials, options?: {
34
- timeoutMs?: number;
35
- pollMs?: number;
36
- }, callbacks?: DeployCallbacks): Promise<void>;
10
+ export { SAFE_CLEANUP_STATES, isCleanableState } from "../types/constants.js";
11
+ export { isQuarantineDetail, isRetainedBucketsDetail } from "../types/callbacks.js";
12
+ export { BUCKET_GONE_ERROR_NAMES, PRE_EMPTY_TAG_KEYS, emptyS3Bucket, isBucketGoneError, type EmptyBucketOutcome } from "./stackCleanup/bucketOps.js";
13
+ export { formatQuarantineSuspectedMessage, formatRetainedBucketsMessage } from "./stackCleanup/messages.js";
14
+ export { capturedSweepBuckets, preEmptyStackBuckets, type PreEmptyBucketsSummary } from "./stackCleanup/preEmpty.js";
15
+ export { cleanupFailedStack } from "./stackCleanup/failedStack.js";
16
+ export { sweepOrphanedDestroyBuckets, type OrphanSweepSummary } from "./stackCleanup/orphanSweep.js";
@@ -1 +1 @@
1
- import{CloudFormationClient as $,DescribeStacksCommand as k,DeleteStackCommand as C,ListStackResourcesCommand as _}from"@aws-sdk/client-cloudformation";import{S3Client as m,ListObjectVersionsCommand as h,DeleteObjectsCommand as A}from"@aws-sdk/client-s3";import{NodeHttpHandler as f}from"@smithy/node-http-handler";import{logger as s}from"@fjall/util/logger";import{getErrorMessage as S,maskSensitiveOutput as w,sleep as M}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as g,SAFE_CLEANUP_STATES as P,isCleanableState as T}from"../types/constants.js";const L=1e3;async function R(e,t,n){let d,o;n?.onStackCleanupProgress?.(t,"emptying-bucket");const u=1e3;let l=0;for(;l++<u;){let r;try{r=await e.send(new h({Bucket:t,KeyMarker:d,VersionIdMarker:o}))}catch(c){if(c instanceof Error&&(c.name==="NoSuchBucket"||c.message?.includes("NoSuchBucket"))){s.debug("stackCleanup",`Bucket ${t} no longer exists, skipping`);return}const E=`Unexpected error emptying bucket ${t}: ${w(S(c))}`;s.warn("stackCleanup",E),n?.onLog?.(E,"warn");return}const i=[...r.Versions??[],...r.DeleteMarkers??[]];if(i.length===0)break;for(let c=0;c<i.length;c+=L){const E=i.slice(c,c+L);try{await e.send(new A({Bucket:t,Delete:{Objects:E.map(a=>({Key:a.Key,VersionId:a.VersionId})),Quiet:!0}}))}catch(a){const p=`Failed to delete batch from ${t}: ${w(S(a))}`;s.warn("stackCleanup",p),n?.onLog?.(p,"warn")}}if(!r.IsTruncated)break;d=r.NextKeyMarker,o=r.NextVersionIdMarker}if(l>u){const r=`Bucket ${t} reached ${u} page limit \u2014 some objects may remain`;s.warn("stackCleanup",r),n?.onLog?.(r,"warn")}s.debug("stackCleanup",`Emptied bucket ${t}`)}async function y(e,t,n,d){const o=[];let u,r=0;do{if(r++>=100){s.warn("stackCleanup","Reached 100 page limit listing stack resources",{stackName:t});break}const i=await e.send(new _({StackName:t,NextToken:u}));for(const c of i.StackResourceSummaries??[])if(n(c)){const E=d(c);E&&o.push(E)}u=i.NextToken}while(u);return o}async function F(e,t){return y(e,t,n=>n.ResourceType==="AWS::S3::Bucket"&&n.ResourceStatus==="DELETE_FAILED",n=>n.PhysicalResourceId)}async function H(e,t,n,d,o){const u=d?.timeoutMs??3e5,l=d?.pollMs??5e3;try{const r=new $({region:t,credentials:n,requestHandler:new f({requestTimeout:15e3})});let i;try{i=(await r.send(new k({StackName:e}))).Stacks?.[0]?.StackStatus}catch(a){if(a instanceof Error&&a.message?.includes(g)){s.debug("stackCleanup",`Stack ${e} does not exist, no cleanup needed`);return}s.warn("stackCleanup",`Failed to check stack status: ${w(S(a))}`,{stackName:e,region:t});return}if(!i||!T(i)){s.debug("stackCleanup",`Stack ${e} status ${i??"unknown"} is not cleanable, skipping`);return}s.warn("stackCleanup",`Cleaning up ${e} stack in ${i} state`,{region:t}),o?.onStackCleanupProgress?.(e,"deleting-stack");const c=new m({region:t,credentials:n,requestHandler:new f({requestTimeout:15e3})});try{const a=await F(r,e);for(const p of a)s.warn("stackCleanup",`Emptying bucket ${p}`,{region:t}),await R(c,p,o)}catch(a){const p=`Failed to empty S3 buckets: ${w(S(a))}`;s.warn("stackCleanup",p,{stackName:e,region:t}),o?.onLog?.(p,"warn")}await r.send(new C({StackName:e})),o?.onStackCleanupProgress?.(e,"waiting");const E=await D(r,e,u,l);if(E==="DELETE_COMPLETE"){s.warn("stackCleanup",`${e} stack deleted successfully`,{region:t}),o?.onStackCleanupProgress?.(e,"complete");return}if(E==="DELETE_FAILED"){s.warn("stackCleanup",`${e} still in DELETE_FAILED, retrying with RetainResources`,{region:t});const a=await I(r,e);if(a.length===0)s.warn("stackCleanup",`${e} in DELETE_FAILED but no non-bucket resources to retain \u2014 cannot retry`,{region:t}),o?.onStackCleanupProgress?.(e,"error");else{await r.send(new C({StackName:e,RetainResources:a}));const p=await D(r,e,u,l);p==="DELETE_COMPLETE"?(s.warn("stackCleanup",`${e} stack deleted on retry (retained: ${a.join(", ")})`,{region:t}),o?.onStackCleanupProgress?.(e,"complete")):(s.warn("stackCleanup",`${e} stack still not deleted after retry: ${p}`,{region:t}),o?.onStackCleanupProgress?.(e,"error"))}}}catch(r){s.warn("stackCleanup",`Stack cleanup failed: ${w(S(r))}`,{stackName:e,region:t}),o?.onStackCleanupProgress?.(e,"error")}}async function D(e,t,n,d){const o=Date.now();for(;Date.now()-o<n;){await M(d);try{const l=(await e.send(new k({StackName:t}))).Stacks?.[0]?.StackStatus;if(!l||l==="DELETE_COMPLETE")return"DELETE_COMPLETE";if(l==="DELETE_FAILED")return"DELETE_FAILED";s.debug("stackCleanup",`Waiting for ${t}: ${l}`)}catch(u){if(u instanceof Error&&u.message?.includes(g))return"DELETE_COMPLETE";throw s.debug("stackCleanup",`Unexpected error polling ${t}: ${S(u)}`),u}}return"TIMEOUT"}async function I(e,t){return y(e,t,n=>n.ResourceStatus==="DELETE_FAILED"&&n.ResourceType!=="AWS::S3::Bucket",n=>n.LogicalResourceId)}export{P as SAFE_CLEANUP_STATES,H as cleanupFailedStack,T as isCleanableState};
1
+ import{SAFE_CLEANUP_STATES as r,isCleanableState as a}from"../types/constants.js";import{isQuarantineDetail as p,isRetainedBucketsDetail as s}from"../types/callbacks.js";import{BUCKET_GONE_ERROR_NAMES as u,PRE_EMPTY_TAG_KEYS as E,emptyS3Bucket as i,isBucketGoneError as m}from"./stackCleanup/bucketOps.js";import{formatQuarantineSuspectedMessage as n,formatRetainedBucketsMessage as f}from"./stackCleanup/messages.js";import{capturedSweepBuckets as B,preEmptyStackBuckets as _}from"./stackCleanup/preEmpty.js";import{cleanupFailedStack as d}from"./stackCleanup/failedStack.js";import{sweepOrphanedDestroyBuckets as R}from"./stackCleanup/orphanSweep.js";export{u as BUCKET_GONE_ERROR_NAMES,E as PRE_EMPTY_TAG_KEYS,r as SAFE_CLEANUP_STATES,B as capturedSweepBuckets,d as cleanupFailedStack,i as emptyS3Bucket,n as formatQuarantineSuspectedMessage,f as formatRetainedBucketsMessage,m as isBucketGoneError,a as isCleanableState,p as isQuarantineDetail,s as isRetainedBucketsDetail,_ as preEmptyStackBuckets,R as sweepOrphanedDestroyBuckets};
@@ -0,0 +1,43 @@
1
+ import { type S3Client } from "@aws-sdk/client-s3";
2
+ import { type CloudTrailClient } from "@aws-sdk/client-cloudtrail";
3
+ import { type KMSClient } from "@aws-sdk/client-kms";
4
+ import { type Result } from "@fjall/generator";
5
+ import type { DeployCallbacks } from "../../types/callbacks.js";
6
+ export interface DecommissionClients {
7
+ cloudTrailClient: CloudTrailClient;
8
+ s3Client: S3Client;
9
+ kmsClient: KMSClient;
10
+ }
11
+ export interface DecommissionInput {
12
+ accountId: string;
13
+ /** Trail name the CFN draining deploy should have deleted. */
14
+ trailName: string;
15
+ bucketName: string;
16
+ /** CMK to schedule for deletion; omitted ⇒ KMS step is skipped. */
17
+ keyArn?: string;
18
+ acknowledgeTrailHistoryLoss: boolean;
19
+ abortSignal?: AbortSignal;
20
+ }
21
+ export type DecommissionOutcome = {
22
+ outcome: "decommissioned";
23
+ } | {
24
+ outcome: "blocked-trail-still-exists";
25
+ } | {
26
+ outcome: "blocked-awaiting-acknowledgement";
27
+ bucketName: string;
28
+ };
29
+ /**
30
+ * Decommission a drained member account's trail storage: empty + delete the
31
+ * retained bucket, then schedule the CMK for deletion. Every step probes
32
+ * first and treats already-gone as success, so the whole function converges
33
+ * on re-run after any partial failure.
34
+ *
35
+ * The trail resource itself is never deleted here — CloudFormation owns it,
36
+ * and a trail still present means the draining deploy has not happened
37
+ * (or rolled back), so nothing is touched.
38
+ *
39
+ * Bucket deletion is SDK-side by design: the bucket is RETAIN in the
40
+ * template, and CloudFormation must never be pointed at a non-empty
41
+ * CloudTrail bucket (delivery continues mid-delete, wedging the stack).
42
+ */
43
+ export declare function decommissionMemberTrailStorage(clients: DecommissionClients, input: DecommissionInput, callbacks?: DeployCallbacks): Promise<Result<DecommissionOutcome>>;
@@ -0,0 +1 @@
1
+ import{DeleteBucketCommand as f,HeadBucketCommand as y,ListObjectVersionsCommand as k}from"@aws-sdk/client-s3";import{DescribeTrailsCommand as g}from"@aws-sdk/client-cloudtrail";import{DescribeKeyCommand as w,ScheduleKeyDeletionCommand as S}from"@aws-sdk/client-kms";import{success as o,failure as a}from"@fjall/generator";import{getErrorMessage as s,maskSensitiveOutput as c}from"@fjall/util";import{logger as E}from"@fjall/util/logger";import{composeSdkAbortSignal as l,extractErrorName as m}from"../../aws/organisations/types.js";import{emptyS3Bucket as C,isBucketGoneError as b}from"../stackCleanup.js";const p=14,N=new Set(["NotFoundException","KMSInvalidStateException"]);async function D(t,e,n){try{const r=await t.send(new g({trailNameList:[e]}),{abortSignal:l(n)});return o((r.trailList??[]).length>0)}catch(r){return m(r)==="TrailNotFoundException"?o(!1):a(new Error(`Failed to probe trail ${e}: ${c(s(r))}`))}}async function h(t,e,n){try{return await t.send(new y({Bucket:e}),{abortSignal:l(n)}),o(!0)}catch(r){return b(r)?o(!1):a(new Error(`Failed to probe bucket ${e}: ${c(s(r))}`))}}async function K(t,e,n){try{const r=await t.send(new k({Bucket:e,MaxKeys:1}),{abortSignal:l(n)});return(r.Versions??[]).length===0&&(r.DeleteMarkers??[]).length===0}catch(r){return E.debug("memberTrailCleanup",`Emptiness probe failed for ${e}; treating as not proven empty`,{error:c(s(r))}),!1}}async function M(t,e,n){try{const i=(await t.send(new w({KeyId:e}),{abortSignal:l(n)})).KeyMetadata?.KeyState;if(i==="PendingDeletion"||i==="PendingReplicaDeletion")return o(void 0)}catch(r){return m(r)==="NotFoundException"?o(void 0):a(new Error(`Failed to probe CMK: ${c(s(r))}`))}try{return await t.send(new S({KeyId:e,PendingWindowInDays:p}),{abortSignal:l(n)}),o(void 0)}catch(r){return N.has(m(r))?o(void 0):a(new Error(`Failed to schedule CMK deletion: ${c(s(r))}`))}}async function O(t,e,n){const r=await D(t.cloudTrailClient,e.trailName,e.abortSignal);if(!r.success)return a(r.error);if(r.data)return o({outcome:"blocked-trail-still-exists"});const i=await h(t.s3Client,e.bucketName,e.abortSignal);if(!i.success)return a(i.error);if(i.data){if(!await K(t.s3Client,e.bucketName,e.abortSignal)&&e.acknowledgeTrailHistoryLoss!==!0)return o({outcome:"blocked-awaiting-acknowledgement",bucketName:e.bucketName});await C(t.s3Client,e.bucketName,n,e.abortSignal);try{await t.s3Client.send(new f({Bucket:e.bucketName}),{abortSignal:l(e.abortSignal)})}catch(u){if(!b(u))return a(new Error(`Failed to delete bucket ${e.bucketName}: ${c(s(u))}`))}}if(e.keyArn!==void 0&&e.keyArn!==""){const d=await M(t.kmsClient,e.keyArn,e.abortSignal);if(!d.success)return a(d.error)}return o({outcome:"decommissioned"})}export{O as decommissionMemberTrailStorage};
@@ -0,0 +1,64 @@
1
+ import { type S3Client } from "@aws-sdk/client-s3";
2
+ import { type Result } from "@fjall/generator";
3
+ import { ORG_TRAIL_BUCKET_OUTPUT_KEY, TRAIL_BUCKET_OUTPUT_KEY, TRAIL_KEY_ARN_OUTPUT_KEY, type ProviderAccount, type TrailLifecycleState } from "@fjall/util/config";
4
+ import { type DecommissionClients, type DecommissionOutcome } from "./memberTrailCleanup.js";
5
+ import type { DeployCallbacks } from "../../types/callbacks.js";
6
+ export { ORG_TRAIL_BUCKET_OUTPUT_KEY, TRAIL_BUCKET_OUTPUT_KEY, TRAIL_KEY_ARN_OUTPUT_KEY };
7
+ export interface MemberTrailFacts {
8
+ /**
9
+ * Org-trail delivery verified for this account. `undefined` = could not be
10
+ * determined (probe failed or recency scan truncated) — never advance on it.
11
+ */
12
+ orgDeliveryVerified?: boolean;
13
+ /** Outcome of this run's decommission attempt (draining accounts only). */
14
+ decommissionOutcome?: DecommissionOutcome["outcome"] | "failed";
15
+ }
16
+ export type TrailMigrationTransition = {
17
+ action: "advance";
18
+ to: TrailLifecycleState;
19
+ } | {
20
+ action: "hold";
21
+ reason: string;
22
+ };
23
+ /**
24
+ * Pure transition truth table for the org-trail migration. Uncertainty never
25
+ * advances state: a probe that failed or could not complete holds the account
26
+ * where it is and reports why.
27
+ */
28
+ export declare function decideNextTransition(state: TrailLifecycleState | undefined, facts: MemberTrailFacts): TrailMigrationTransition;
29
+ export interface TrailMigrationDeps {
30
+ /** Management-account S3 client — the org trail bucket lives there. */
31
+ managementS3Client: S3Client;
32
+ /** Assume the member role and build the decommission clients. */
33
+ getMemberClients: (account: ProviderAccount, region: string) => Promise<Result<DecommissionClients>>;
34
+ }
35
+ export interface TrailMigrationInput {
36
+ orgId: string;
37
+ orgTrailBucketName: string;
38
+ /** Cascade-deployed accounts (platform + members). */
39
+ accounts: ProviderAccount[];
40
+ /** accountId → home-region stack outputs captured by this run's cascade. */
41
+ cascadeOutputs: Map<string, Record<string, string>>;
42
+ defaultRegion: string;
43
+ recencyWindowMs?: number;
44
+ abortSignal?: AbortSignal;
45
+ }
46
+ export interface TrailMigrationOutcome {
47
+ accountId: string;
48
+ accountName: string;
49
+ from: TrailLifecycleState | undefined;
50
+ /** Set only when the account advanced this run. */
51
+ to?: TrailLifecycleState;
52
+ heldReason?: string;
53
+ }
54
+ /**
55
+ * Reconcile every cascade account's trail lifecycle against AWS ground truth.
56
+ * Two-invocation convergence by design: deploy N creates the org trail
57
+ * (overlap with member trails is sanctioned for up to a day), deploy N+1's
58
+ * cheap probes advance states. Facts are re-derived from AWS on every run —
59
+ * the persisted lifecycle is an intent cache, so lost persistence self-heals.
60
+ *
61
+ * Never throws and never fails the deploy: every problem is reported through
62
+ * callbacks and the account simply holds its current state.
63
+ */
64
+ export declare function reconcileTrailMigration(deps: TrailMigrationDeps, input: TrailMigrationInput, callbacks: DeployCallbacks): Promise<TrailMigrationOutcome[]>;
@@ -0,0 +1 @@
1
+ import{maskSensitiveOutput as h}from"@fjall/util";import{ACCOUNT_TRAIL_NAME as N,ORG_TRAIL_BUCKET_OUTPUT_KEY as $,TRAIL_BUCKET_OUTPUT_KEY as p,TRAIL_KEY_ARN_OUTPUT_KEY as T}from"@fjall/util/config";import{verifyOrgTrailDelivery as L}from"../../aws/cloudtrail/orgTrailDelivery.js";import{decommissionMemberTrailStorage as R}from"./memberTrailCleanup.js";function w(r,i){if(r==="account")return{action:"hold",reason:"pinned to a per-account trail"};if(r==="org")return{action:"hold",reason:"already migrated to the org trail"};if(r===void 0)return i.orgDeliveryVerified===!0?{action:"advance",to:"draining"}:{action:"hold",reason:i.orgDeliveryVerified===!1?"org-trail delivery not yet verified (first delivery can take up to 24h)":"org-trail delivery could not be determined"};switch(i.decommissionOutcome){case"decommissioned":return{action:"advance",to:"org"};case"blocked-trail-still-exists":return{action:"hold",reason:"per-account trail still exists \u2014 draining deploy not yet applied"};case"blocked-awaiting-acknowledgement":return{action:"hold",reason:"awaiting acknowledgeTrailHistoryLoss before discarding history"};case"failed":return{action:"hold",reason:"decommission attempt failed"};default:return{action:"hold",reason:"decommission was not attempted"}}}function a(r,i){r.onTrailMigrationPhase?.({...i,...i.detail!==void 0?{detail:h(i.detail)}:{}})}async function C(r,i,o){const d=[],m=i.accounts.filter(e=>e.trailLifecycle===void 0),I=i.accounts.filter(e=>e.trailLifecycle==="draining"),f=new Map;if(m.length>0){a(o,{accountId:"*",accountName:"organisation",phase:"verify-delivery",status:"started",detail:`Checking org-trail delivery for ${m.length} account(s) in s3://${i.orgTrailBucketName}`});const e=await L(r.managementS3Client,{bucketName:i.orgTrailBucketName,orgId:i.orgId,accountIds:m.map(n=>n.id),...i.recencyWindowMs!==void 0?{recencyWindowMs:i.recencyWindowMs}:{},...i.abortSignal!==void 0?{abortSignal:i.abortSignal}:{}});if(e.success){for(const n of e.data.perAccount)f.set(n.accountId,n.recentDelivery===void 0?void 0:n.delivered&&n.recentDelivery);a(o,{accountId:"*",accountName:"organisation",phase:"verify-delivery",status:"completed"})}else a(o,{accountId:"*",accountName:"organisation",phase:"verify-delivery",status:"failed",detail:e.error.message})}for(const e of m){const n={...f.get(e.id)!==void 0?{orgDeliveryVerified:f.get(e.id)}:{}},s=w(void 0,n);s.action==="advance"?(o.onTrailLifecycleChanged?.(e.id,void 0,s.to),o.onLog?.(`${e.name}: org-trail delivery verified \u2014 trail lifecycle advanced to "draining" (the next deploy removes its per-account trail)`,"info"),d.push({accountId:e.id,accountName:e.name,from:void 0,to:s.to})):d.push({accountId:e.id,accountName:e.name,from:void 0,heldReason:s.reason})}for(const e of I){const n=e.region??i.defaultRegion,s=i.cascadeOutputs.get(e.id),c=s?.[p],y=s?.[T];if(c===void 0||c===""){const t=`stack output ${p} unavailable for this run`;o.onProgress?.({type:"warning",message:`${e.name}: trail decommission skipped \u2014 ${t}. The account stays "draining"; the next deploy retries.`}),d.push({accountId:e.id,accountName:e.name,from:"draining",heldReason:t});continue}a(o,{accountId:e.id,accountName:e.name,phase:"decommission",status:"started"});let l;const u=await r.getMemberClients(e,n);if(!u.success)l={decommissionOutcome:"failed"},o.onProgress?.({type:"warning",message:h(`${e.name}: trail decommission failed \u2014 ${u.error.message}. The account stays "draining"; the next deploy retries.`)}),a(o,{accountId:e.id,accountName:e.name,phase:"decommission",status:"failed",detail:u.error.message});else{const t=await R(u.data,{accountId:e.id,trailName:N,bucketName:c,...y!==void 0&&y!==""?{keyArn:y}:{},acknowledgeTrailHistoryLoss:e.acknowledgeTrailHistoryLoss===!0,...i.abortSignal!==void 0?{abortSignal:i.abortSignal}:{}},o);if(!t.success)l={decommissionOutcome:"failed"},o.onProgress?.({type:"warning",message:h(`${e.name}: trail decommission failed \u2014 ${t.error.message}. The account stays "draining"; the next deploy retries.`)}),a(o,{accountId:e.id,accountName:e.name,phase:"decommission",status:"failed",detail:t.error.message});else if(l={decommissionOutcome:t.data.outcome},t.data.outcome==="blocked-awaiting-acknowledgement"){const v=`${e.name}: per-account trail removed, but its log history in s3://${c} will be DISCARDED on decommission. Back the bucket up if the history matters, then set acknowledgeTrailHistoryLoss: true for account ${e.id} and redeploy.`;o.onProgress?.({type:"warning",message:v}),a(o,{accountId:e.id,accountName:e.name,phase:"decommission",status:"blocked",detail:v})}else t.data.outcome==="blocked-trail-still-exists"?a(o,{accountId:e.id,accountName:e.name,phase:"decommission",status:"blocked",detail:"per-account trail still exists \u2014 the draining deploy has not been applied yet"}):a(o,{accountId:e.id,accountName:e.name,phase:"decommission",status:"completed",detail:`bucket s3://${c} removed; CMK scheduled for deletion`})}const g=w("draining",l);g.action==="advance"?(o.onTrailLifecycleChanged?.(e.id,"draining",g.to),o.onLog?.(`${e.name}: trail storage decommissioned \u2014 trail lifecycle advanced to "org"`,"info"),d.push({accountId:e.id,accountName:e.name,from:"draining",to:g.to})):d.push({accountId:e.id,accountName:e.name,from:"draining",heldReason:g.reason})}return d}export{$ as ORG_TRAIL_BUCKET_OUTPUT_KEY,p as TRAIL_BUCKET_OUTPUT_KEY,T as TRAIL_KEY_ARN_OUTPUT_KEY,w as decideNextTransition,C as reconcileTrailMigration};
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Resource a root-task unlock acts on. The kind only varies the message
3
+ * wording — both unlock engines share one remediation story (the SCP
4
+ * carve-out covers the five s3/sqs policy-management actions together).
5
+ */
6
+ export interface UnlockResource {
7
+ kind: "bucket" | "queue";
8
+ name: string;
9
+ }
10
+ /**
11
+ * Failure copy for an AccessDenied hitting a root session mid-unlock.
12
+ * Coupled value at 2 sites (unlockBucket + unlockQueue): the remediation
13
+ * story must stay identical, so the copy lives here rather than per-engine.
14
+ */
15
+ export declare function formatScpSuspectedFailure(action: string, resource: UnlockResource, accountId: string): string;
@@ -0,0 +1 @@
1
+ function n(t,e,o){return`Access denied for the root session calling ${t} on ${e.kind} ${e.name} (account ${o}). If the organisation's SCPs predate the root-task unlock carve-out, DenyRootUserActions may still block the five s3/sqs policy-management actions \u2014 re-deploy the organisation to refresh its SCPs, then retry.`}export{n as formatScpSuspectedFailure};
@@ -0,0 +1,37 @@
1
+ import type { STSClient } from "@aws-sdk/client-sts";
2
+ import { type Result } from "@fjall/generator";
3
+ export interface UnlockBucketInput {
4
+ /** Member account holding the quarantined bucket. */
5
+ accountId: string;
6
+ bucketName: string;
7
+ /** Bucket region — also pins the regional STS endpoint requirement. */
8
+ region: string;
9
+ abortSignal?: AbortSignal;
10
+ }
11
+ /**
12
+ * "no-policy" means the bucket carries no policy at all — it is not
13
+ * policy-locked and there is nothing to unlock. Consumers phrase this
14
+ * differently from a real unlock.
15
+ */
16
+ export type UnlockBucketReport = {
17
+ outcome: "unlocked";
18
+ capturedPolicy: string;
19
+ warning: string;
20
+ } | {
21
+ outcome: "no-policy";
22
+ };
23
+ /**
24
+ * Remove a stranded deny policy from a quarantined bucket via an STS
25
+ * assume-root session bounded by the S3UnlockBucketPolicy root-task policy.
26
+ *
27
+ * Ordering is load-bearing: the stranded policy is captured with
28
+ * GetBucketPolicy BEFORE DeleteBucketPolicy destroys it — the captured JSON
29
+ * is the only forensic record of what the policy carried, and the operator
30
+ * needs it to re-put legitimate statements.
31
+ *
32
+ * The STS client must use management-account or delegated-admin credentials
33
+ * and carry an explicit region (assumeRootForTask enforces both). Session
34
+ * credentials are credential VALUES — they exist only inside this function
35
+ * and never reach a sink; error strings are masked.
36
+ */
37
+ export declare function unlockBucket(stsClient: STSClient, input: UnlockBucketInput): Promise<Result<UnlockBucketReport>>;
@@ -0,0 +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};
@@ -0,0 +1,43 @@
1
+ import type { STSClient } from "@aws-sdk/client-sts";
2
+ import { type Result } from "@fjall/generator";
3
+ export interface UnlockQueueInput {
4
+ /** Member account holding the policy-locked queue. */
5
+ accountId: string;
6
+ queueName: string;
7
+ /** Queue region — also pins the regional STS endpoint requirement. */
8
+ region: string;
9
+ abortSignal?: AbortSignal;
10
+ }
11
+ /**
12
+ * "no-policy" means the queue carries no resource policy at all — it is not
13
+ * policy-locked and there is nothing to unlock. Consumers phrase this
14
+ * differently from a real unlock.
15
+ */
16
+ export type UnlockQueueReport = {
17
+ outcome: "unlocked";
18
+ capturedPolicy: string;
19
+ warning: string;
20
+ } | {
21
+ outcome: "no-policy";
22
+ };
23
+ /**
24
+ * Remove a stranded deny policy from a locked SQS queue via an STS
25
+ * assume-root session bounded by the SQSUnlockQueuePolicy root-task policy.
26
+ *
27
+ * Ordering is load-bearing: the stranded policy is captured with
28
+ * GetQueueAttributes BEFORE SetQueueAttributes clears it — the captured JSON
29
+ * is the only forensic record of what the policy carried, and the operator
30
+ * needs it to re-put legitimate statements. SQS has no DeleteQueuePolicy
31
+ * API; setting Policy to the empty string is the documented removal path.
32
+ *
33
+ * The queue URL is composed deterministically from region/account/name
34
+ * because GetQueueUrl is not in SQSUnlockQueuePolicy. This assumes the
35
+ * standard aws partition URL shape (sqs.<region>.amazonaws.com) — GovCloud
36
+ * and CN partitions are out of scope for the unlock engines.
37
+ *
38
+ * The STS client must use management-account or delegated-admin credentials
39
+ * and carry an explicit region (assumeRootForTask enforces both). Session
40
+ * credentials are credential VALUES — they exist only inside this function
41
+ * and never reach a sink; error strings are masked.
42
+ */
43
+ export declare function unlockQueue(stsClient: STSClient, input: UnlockQueueInput): Promise<Result<UnlockQueueReport>>;
@@ -0,0 +1 @@
1
+ import{SQSClient as A,GetQueueAttributesCommand as Q,SetQueueAttributesCommand as N}from"@aws-sdk/client-sqs";import{success as d,failure as t}from"@fjall/generator";import{getErrorMessage as S,maskSensitiveOutput as p}from"@fjall/util";import{composeSdkAbortSignal as f,extractErrorName as w,isAccessDenied as g}from"../../aws/organisations/types.js";import{assumeRootForTask as q,MAX_ROOT_SESSION_SECONDS as b}from"../../aws/sts/assumeRoot.js";import{formatScpSuspectedFailure as y}from"./scpRemediation.js";const h="arn:aws:iam::aws:policy/root-task/SQSUnlockQueuePolicy",k=new Set(["QueueDoesNotExist","AWS.SimpleQueueService.NonExistentQueue"]);function O(i){return`The unlock cleared the ENTIRE queue policy on ${i} \u2014 including any legitimate access grants it carried (cross-account SendMessage permissions, S3/SNS/EventBridge event-source allow statements), not just the stranded deny. Review the captured policy and re-put sane statements before relying on the queue.`}async function v(i,E){const{accountId:o,queueName:e,region:n,abortSignal:s}=E;if(n==="")return t(new Error("A queue region is required to unlock a queue."));const a=await q(i,{targetAccountId:o,taskPolicyArn:h,durationSeconds:b,abortSignal:s});if(!a.success)return t(a.error);const c=new A({region:n,credentials:a.data.credentials}),l=`https://sqs.${n}.amazonaws.com/${o}/${e}`;let u;try{u=(await c.send(new Q({QueueUrl:l,AttributeNames:["Policy"]}),{abortSignal:f(s)})).Attributes?.Policy}catch(r){const m=w(r);return k.has(m)?t(new Error(`Queue ${e} was not found in region ${n} \u2014 check the queue name and region.`)):g(m)?t(new Error(y("GetQueueAttributes",{kind:"queue",name:e},o))):t(new Error(`Failed to capture the queue policy on ${e}: ${p(S(r))}`))}if(u===void 0||u==="")return d({outcome:"no-policy"});try{await c.send(new N({QueueUrl:l,Attributes:{Policy:""}}),{abortSignal:f(s)})}catch(r){return g(w(r))?t(new Error(y("SetQueueAttributes",{kind:"queue",name:e},o))):t(new Error(`Captured the queue policy on ${e} but failed to clear it: ${p(S(r))}`))}return d({outcome:"unlocked",capturedPolicy:u,warning:O(e)})}export{v as unlockQueue};
@@ -1,6 +1,7 @@
1
1
  import type { CdkService } from "../infrastructure/CdkService.js";
2
2
  import type { CloudFormationService } from "../infrastructure/CloudFormationService.js";
3
3
  import type { AwsProvider } from "../../aws/AwsProvider.js";
4
+ import type { DeployCallbacks } from "../../types/callbacks.js";
4
5
  import type { ResourceEvent } from "../../types/events.js";
5
6
  import type { DeploymentContext } from "../../types/deployment/DeploymentTypes.js";
6
7
  import { type ApplicationStack } from "../../types/operations.js";
@@ -8,9 +9,12 @@ import type { ParallelDeploymentResult } from "../../types/deployment/parallel.j
8
9
  import type { AppResourceFlags } from "../../types/patternDetection.js";
9
10
  import { type Result } from "@fjall/generator";
10
11
  import { ApplicationError, type StackDeploymentData } from "../../types/application/ApplicationServiceTypes.js";
12
+ import { type DestroyAllStacksCallbacks } from "./applicationStackHelpers.js";
11
13
  interface StackCallbacks {
12
14
  onOutput?: (chunk: string) => void;
13
15
  onResourceProgress?: (event: ResourceEvent) => void;
16
+ onLog?: DeployCallbacks["onLog"];
17
+ onStackCleanupProgress?: DeployCallbacks["onStackCleanupProgress"];
14
18
  }
15
19
  interface DeployStackOptions {
16
20
  /**
@@ -23,6 +27,8 @@ interface DeployStackOptions {
23
27
  interface ParallelStackCallbacks {
24
28
  onOutput?: (chunk: string, stackId: ApplicationStack) => void;
25
29
  onResourceProgress?: (event: ResourceEvent, stackId: ApplicationStack) => void;
30
+ onLog?: DeployCallbacks["onLog"];
31
+ onStackCleanupProgress?: DeployCallbacks["onStackCleanupProgress"];
26
32
  onStackComplete?: (stack: ApplicationStack, success: boolean, duration: number, error?: Error) => void;
27
33
  }
28
34
  /**
@@ -56,7 +62,7 @@ export declare class ApplicationStackService {
56
62
  /**
57
63
  * Destroy a specific stack type
58
64
  */
59
- destroyStack(stackType: ApplicationStack, context: DeploymentContext, callbacks?: StackCallbacks, useCdkOut?: boolean): Promise<Result<StackDeploymentData, ApplicationError>>;
65
+ destroyStack(stackType: ApplicationStack, context: DeploymentContext, callbacks?: StackCallbacks, useCdkOut?: boolean, abortSignal?: AbortSignal): Promise<Result<StackDeploymentData, ApplicationError>>;
60
66
  /**
61
67
  * Destroy multiple stacks in parallel within a destroy phase.
62
68
  *
@@ -64,7 +70,7 @@ export declare class ApplicationStackService {
64
70
  * be run beforehand. Multiple CDK commands cannot run in parallel because they
65
71
  * all try to synthesise to the same cdk.out directory.
66
72
  */
67
- destroyStacksInParallel(stacks: readonly ApplicationStack[], context: DeploymentContext, callbacks?: ParallelStackCallbacks, useCdkOut?: boolean): Promise<Result<ParallelDeploymentResult[], ApplicationError>>;
73
+ destroyStacksInParallel(stacks: readonly ApplicationStack[], context: DeploymentContext, callbacks?: ParallelStackCallbacks, useCdkOut?: boolean, abortSignal?: AbortSignal): Promise<Result<ParallelDeploymentResult[], ApplicationError>>;
68
74
  /**
69
75
  * Get stack outputs for a deployed stack
70
76
  */
@@ -78,14 +84,7 @@ export declare class ApplicationStackService {
78
84
  * Destroy all stacks for an application in reverse order.
79
85
  * Supports parallel destruction for OpenNext patterns (nextjs/payload).
80
86
  */
81
- destroyAllStacks(context: DeploymentContext, callbacks?: {
82
- onOutput?: (chunk: string, stackId?: ApplicationStack) => void;
83
- onResourceProgress?: (event: ResourceEvent, stackId?: ApplicationStack) => void;
84
- onStackStart?: (stackType: ApplicationStack, stackName: string) => void;
85
- onStackComplete?: (stackType: ApplicationStack, result: Result<StackDeploymentData, ApplicationError>) => void | Promise<void>;
86
- onParallelPhaseStart?: (stacks: readonly ApplicationStack[], description: string) => void;
87
- onParallelPhaseComplete?: (results: ParallelDeploymentResult[]) => void;
88
- }, resources?: AppResourceFlags): Promise<Result<{
87
+ destroyAllStacks(context: DeploymentContext, callbacks?: DestroyAllStacksCallbacks, resources?: AppResourceFlags, abortSignal?: AbortSignal): Promise<Result<{
89
88
  message: string;
90
89
  }, ApplicationError>>;
91
90
  }
@@ -1 +1 @@
1
- import{STACK_NOT_FOUND_PATTERN as P,CDK_NO_STACKS_MATCH as A}from"../../types/constants.js";import{getApplicationStackName as y}from"../../types/operations.js";import{success as m,failure as k}from"@fjall/generator";import{ApplicationError as h}from"../../types/application/ApplicationServiceTypes.js";import{logger as S}from"@fjall/util/logger";import{maskSensitiveOutput as v}from"@fjall/util";import{convertCloudFormationOutputsToRecord as w}from"../supporting/helpers.js";import{destroyAllStacks as R,mapSettledResults as O,resolveWebsiteUrl as D}from"./applicationStackHelpers.js";class E{cdkService;cloudFormationService;aws;constructor(e,t,r){this.cdkService=e,this.cloudFormationService=t,this.aws=r}async runCdkSynth(e){return this.cdkService.runCdkSynth(e)}async deployStack(e,t,r,n){const i=t.target,u=y(i,e),c=await this.cdkService.runCdkDeploy(t,u,r?.onOutput,r?.onResourceProgress,this.aws,void 0,n?.parameters);if(c.success){const s=c.data;S.debug("ApplicationStackService","CDK deploy result",{stackName:u,stackType:e,success:!0,message:s.message,status:s.status,skipped:s.details?.skipped,hasOutput:!!s.details?.output});const o=await this.cloudFormationService.getStackOutputs(u);S.debug("ApplicationStackService","Stack outputs after deploy",{stackName:u,hasOutputs:o.success&&o.data.length>0,outputCount:o.success?o.data.length:0});const a=o.success&&o.data.length>0?w(o.data):void 0;return m({stackType:e,stackName:u,skipped:s.details?.skipped===!0,outputs:a})}else{const s=v(c.error||`Failed to deploy ${e} infrastructure`);return S.error("ApplicationStackService","Stack deployment failed",{stackType:e,target:t.target,error:s}),k(new h(s,{errorType:"deployment_failed",appName:t.target,operation:"deployStack",stackType:e}))}}async deployStacksInParallel(e,t,r){const n=r?.onOutput,i=r?.onResourceProgress,u=r?.onStackComplete,c=await Promise.allSettled(e.map(async s=>{const o=Date.now(),a=await this.deployStack(s,t,{onOutput:n?p=>n(p,s):void 0,onResourceProgress:i?p=>i(p,s):void 0}),d=Date.now()-o,g={stack:s,success:a.success,error:a.success?void 0:a.error,duration:d,...a.success&&a.data.outputs!==void 0?{outputs:a.data.outputs}:{}};return!a.success&&a.error&&S.debug("deployStacksInParallel",`Stack ${s} failed`,{error:a.error.message}),u?.(s,a.success,d,a.success?void 0:a.error),g}));return m(O(c,e))}async destroyStack(e,t,r,n){const i=t.target,u=y(i,e),c=await this.cdkService.runCdkDestroy(t,u,r?.onOutput,r?.onResourceProgress,this.aws,n);if(c.success)return m({stackType:e,stackName:u,skipped:!1,outputs:void 0});{const s=v(c.error||`Failed to destroy ${e} stack`);return S.error("ApplicationStackService","Stack destroy failed",{stackType:e,target:t.target,error:s}),k(new h(s,{errorType:"destroy_failed",appName:t.target,operation:"destroyStack",stackType:e}))}}async destroyStacksInParallel(e,t,r,n){const i=r?.onOutput,u=r?.onResourceProgress,c=r?.onStackComplete,s=await Promise.allSettled(e.map(async o=>{const a=Date.now(),d=await this.destroyStack(o,t,{onOutput:i?f=>i(f,o):void 0,onResourceProgress:u?f=>u(f,o):void 0},n),g=Date.now()-a,p=d.success?"":d.error?.message??"",C=p.includes(P)||p.includes(A),l={stack:o,success:d.success||C,error:d.success||C?void 0:d.error,duration:g};return!l.success&&l.error&&S.debug("destroyStacksInParallel",`Stack ${o} failed`,{error:l.error.message}),c?.(o,l.success,g,l.success?void 0:l.error),l}));return m(O(s,e))}async getStackOutputs(e,t){const r=y(e,t),n=await this.cloudFormationService.getStackOutputs(r);return n.success?m(w(n.data)):k(new h(`Failed to get outputs for ${r}`,{errorType:"stack_error",appName:e,operation:"getStackOutputs",stackType:t,details:n.error}))}async resolveWebsiteUrl(e){return D(e,this.cloudFormationService)}async destroyAllStacks(e,t,r){return R(this,e,t,r)}}export{E as ApplicationStackService};
1
+ import{CloudFormationClient as R}from"@aws-sdk/client-cloudformation";import{S3Client as v}from"@aws-sdk/client-s3";import{capturedSweepBuckets as D,preEmptyStackBuckets as N,sweepOrphanedDestroyBuckets as F}from"../../orchestration/stackCleanup.js";import{STACK_NOT_FOUND_PATTERN as _,CDK_NO_STACKS_MATCH as L}from"../../types/constants.js";import{getApplicationStackName as y}from"../../types/operations.js";import{success as g,failure as k}from"@fjall/generator";import{ApplicationError as h}from"../../types/application/ApplicationServiceTypes.js";import{logger as d}from"@fjall/util/logger";import{maskSensitiveOutput as P}from"@fjall/util";import{convertCloudFormationOutputsToRecord as O}from"../supporting/helpers.js";import{destroyAllStacks as $,mapSettledResults as A,resolveWebsiteUrl as B}from"./applicationStackHelpers.js";class z{cdkService;cloudFormationService;aws;constructor(e,s,t){this.cdkService=e,this.cloudFormationService=s,this.aws=t}async runCdkSynth(e){return this.cdkService.runCdkSynth(e)}async deployStack(e,s,t,a){const c=s.target,i=y(c,e),n=await this.cdkService.runCdkDeploy(s,i,t?.onOutput,t?.onResourceProgress,this.aws,void 0,a?.parameters);if(n.success){const o=n.data;d.debug("ApplicationStackService","CDK deploy result",{stackName:i,stackType:e,success:!0,message:o.message,status:o.status,skipped:o.details?.skipped,hasOutput:!!o.details?.output});const u=await this.cloudFormationService.getStackOutputs(i);d.debug("ApplicationStackService","Stack outputs after deploy",{stackName:i,hasOutputs:u.success&&u.data.length>0,outputCount:u.success?u.data.length:0});const r=u.success&&u.data.length>0?O(u.data):void 0;return g({stackType:e,stackName:i,skipped:o.details?.skipped===!0,outputs:r})}else{const o=P(n.error||`Failed to deploy ${e} infrastructure`);return d.error("ApplicationStackService","Stack deployment failed",{stackType:e,target:s.target,error:o}),k(new h(o,{errorType:"deployment_failed",appName:s.target,operation:"deployStack",stackType:e}))}}async deployStacksInParallel(e,s,t){const a=t?.onOutput,c=t?.onResourceProgress,i=t?.onStackComplete,n=await Promise.allSettled(e.map(async o=>{const u=Date.now(),r=await this.deployStack(o,s,{onOutput:a?m=>a(m,o):void 0,onResourceProgress:c?m=>c(m,o):void 0}),S=Date.now()-u,p={stack:o,success:r.success,error:r.success?void 0:r.error,duration:S,...r.success&&r.data.outputs!==void 0?{outputs:r.data.outputs}:{}};return!r.success&&r.error&&d.debug("deployStacksInParallel",`Stack ${o} failed`,{error:r.error.message}),i?.(o,r.success,S,r.success?void 0:r.error),p}));return g(A(n,e))}async destroyStack(e,s,t,a,c){const i=s.target,n=y(i,e);let o=[];if(this.aws!==void 0)if(a===!0||(await this.cdkService.runCdkSynth(s)).success){const S=await N(this.aws.getClient(R),this.aws.getClient(v),n,{onLog:t?.onLog,onStackCleanupProgress:t?.onStackCleanupProgress},c);o=D(S)}else d.debug("ApplicationStackService","Skipping pre-empty pass after failed validation synth \u2014 the destroy will surface the error",{stackName:n});else d.debug("ApplicationStackService","No AWS provider available, skipping pre-empty pass",{stackName:n});const u=await this.cdkService.runCdkDestroy(s,n,t?.onOutput,t?.onResourceProgress,this.aws,a);if(u.success)return this.aws!==void 0&&o.length>0&&await F(this.aws.getClient(v),o,{onLog:t?.onLog,onStackCleanupProgress:t?.onStackCleanupProgress},c),g({stackType:e,stackName:n,skipped:!1,outputs:void 0});{const r=P(u.error||`Failed to destroy ${e} stack`);return d.error("ApplicationStackService","Stack destroy failed",{stackType:e,target:s.target,error:r}),k(new h(r,{errorType:"destroy_failed",appName:s.target,operation:"destroyStack",stackType:e}))}}async destroyStacksInParallel(e,s,t,a,c){const i=t?.onOutput,n=t?.onResourceProgress,o=t?.onStackComplete,u=await Promise.allSettled(e.map(async r=>{const S=Date.now(),p=await this.destroyStack(r,s,{onOutput:i?f=>i(f,r):void 0,onResourceProgress:n?f=>n(f,r):void 0,onLog:t?.onLog,onStackCleanupProgress:t?.onStackCleanupProgress},a,c),m=Date.now()-S,C=p.success?"":p.error?.message??"",w=C.includes(_)||C.includes(L),l={stack:r,success:p.success||w,error:p.success||w?void 0:p.error,duration:m};return!l.success&&l.error&&d.debug("destroyStacksInParallel",`Stack ${r} failed`,{error:l.error.message}),o?.(r,l.success,m,l.success?void 0:l.error),l}));return g(A(u,e))}async getStackOutputs(e,s){const t=y(e,s),a=await this.cloudFormationService.getStackOutputs(t);return a.success?g(O(a.data)):k(new h(`Failed to get outputs for ${t}`,{errorType:"stack_error",appName:e,operation:"getStackOutputs",stackType:s,details:a.error}))}async resolveWebsiteUrl(e){return B(e,this.cloudFormationService)}async destroyAllStacks(e,s,t,a){return $(this,e,s,t,a)}}export{z as ApplicationStackService};
@@ -7,6 +7,18 @@ import { type Result } from "@fjall/generator";
7
7
  import { ApplicationError, type StackDeploymentData } from "../../types/application/ApplicationServiceTypes.js";
8
8
  import type { ApplicationStackService } from "./ApplicationStackService.js";
9
9
  import type { CloudFormationService } from "../infrastructure/CloudFormationService.js";
10
+ import type { DeployCallbacks } from "../../types/callbacks.js";
11
+ /** Shared with ApplicationStackService.destroyAllStacks — the two signatures must not drift. */
12
+ export interface DestroyAllStacksCallbacks {
13
+ onOutput?: (chunk: string, stackId?: ApplicationStack) => void;
14
+ onResourceProgress?: (event: ResourceEvent, stackId?: ApplicationStack) => void;
15
+ onLog?: DeployCallbacks["onLog"];
16
+ onStackCleanupProgress?: DeployCallbacks["onStackCleanupProgress"];
17
+ onStackStart?: (stackType: ApplicationStack, stackName: string) => void;
18
+ onStackComplete?: (stackType: ApplicationStack, result: Result<StackDeploymentData, ApplicationError>) => void | Promise<void>;
19
+ onParallelPhaseStart?: (stacks: readonly ApplicationStack[], description: string) => void;
20
+ onParallelPhaseComplete?: (results: ParallelDeploymentResult[]) => void;
21
+ }
10
22
  /**
11
23
  * Convert "doesn't exist" errors to success for destroy operations.
12
24
  */
@@ -24,14 +36,7 @@ export declare function handleDestroyError(result: Result<StackDeploymentData, A
24
36
  * Destroy all stacks for an application in reverse order.
25
37
  * Supports parallel destruction for OpenNext patterns (nextjs/payload).
26
38
  */
27
- export declare function destroyAllStacks(service: ApplicationStackService, context: DeploymentContext, callbacks?: {
28
- onOutput?: (chunk: string, stackId?: ApplicationStack) => void;
29
- onResourceProgress?: (event: ResourceEvent, stackId?: ApplicationStack) => void;
30
- onStackStart?: (stackType: ApplicationStack, stackName: string) => void;
31
- onStackComplete?: (stackType: ApplicationStack, result: Result<StackDeploymentData, ApplicationError>) => void | Promise<void>;
32
- onParallelPhaseStart?: (stacks: readonly ApplicationStack[], description: string) => void;
33
- onParallelPhaseComplete?: (results: ParallelDeploymentResult[]) => void;
34
- }, resources?: AppResourceFlags): Promise<Result<{
39
+ export declare function destroyAllStacks(service: ApplicationStackService, context: DeploymentContext, callbacks?: DestroyAllStacksCallbacks, resources?: AppResourceFlags, abortSignal?: AbortSignal): Promise<Result<{
35
40
  message: string;
36
41
  }, ApplicationError>>;
37
42
  /**
@@ -1,4 +1,4 @@
1
- import{maskSensitiveOutput as P}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as T,CDK_NO_STACKS_MATCH as D,INFRASTRUCTURE_FILENAME as $}from"../../types/constants.js";import{APPLICATION_STACKS as N,getApplicationStackName as S,getApplicationDestroyOrder as U,getParallelDestroyGroups as K}from"../../types/operations.js";import{FrameworkRegistry as L}from"../../orchestration/builders/frameworkRegistry.js";import{success as k,failure as h,isFailure as R}from"@fjall/generator";import{ApplicationError as O}from"../../types/application/ApplicationServiceTypes.js";import{logger as F}from"@fjall/util/logger";function A(t,r,e){if(t.success)return t;const n=R(t)?t.error.message:"";return n.includes(T)||n.includes(D)?k({stackName:S(e,r),stackType:r,outputs:{},skipped:!0}):t}function E(t,r){if(t.success)return null;const e=R(t)?P(t.error.message):"";if(e.includes("TSError")||e.includes("Unable to compile TypeScript")||e.includes("Subprocess exited with error")){const n=e.match(/infrastructure\.ts\(\d+,\d+\): error TS\d+: .+/),s=n?n[0]:`Check ${$} for syntax errors`;return{continue:!1,result:h(new O(`CDK synthesis failed: ${s}`,{errorType:"synth_failed",operation:`destroy-${r}`}))}}return e.includes(T)||e.includes(D)?{continue:!0,result:k({message:"Stack already deleted"})}:{continue:!1,result:h(new O(`Failed to destroy ${r} stack: ${e}`,{errorType:"destroy_failed",operation:`destroy-${r}`}))}}async function G(t,r,e,n){const s=r.target,l=L.createDefault().resolve({appPath:r.path});let f,y;if(l){const u=l.builder.plan({appPath:r.path},l.detection);f=u.parallelDestroy,y=u.destroyOrder}else f=!1,y=U({pattern:null,resources:n});if(f){const u=K();for(const C of u){const m=C.stacks;if(m.length===1){const a=m[0],p=S(s,a);e?.onStackStart?.(a,p);const o=await t.destroyStack(a,r,{onOutput:e?.onOutput?d=>e.onOutput?.(d,a):void 0,onResourceProgress:e?.onResourceProgress?d=>e.onResourceProgress?.(d,a):void 0}),i=A(o,a,s);e?.onStackComplete&&await e.onStackComplete(a,i);const c=E(i,a);if(c){if(c.continue)continue;return c.result}}else{const a=await t.runCdkSynth(r);if(!a.success)return h(new O(a.error,{errorType:"synth_failed",appName:s,operation:"destroy-parallel-synth"}));e?.onParallelPhaseStart?.(m,C.description);for(const o of m)e?.onStackStart?.(o,S(s,o));const p=await t.destroyStacksInParallel(m,r,{onOutput:e?.onOutput,onResourceProgress:e?.onResourceProgress,onStackComplete:async(o,i,c,d)=>{const g=d?.message!==void 0?P(d.message):"Stack destruction failed",_=i?k({stackName:S(s,o),stackType:o,outputs:{}}):h(d instanceof O?d:new O(g,{errorType:"destroy_failed",appName:s,operation:`destroy-${o}`}));await e?.onStackComplete?.(o,_)}},!0);if(p.success){e?.onParallelPhaseComplete?.(p.data);const o=p.data.filter(i=>!i.success);if(o.length>0){const i=o.filter(c=>c.error&&!c.error.message.includes(T)&&!c.error.message.includes(D));if(i.length>0){const c=i.map(g=>g.stack).join(", "),d=i.map(g=>`${g.stack}: ${g.error?.message??"Unknown error"}`).join(`
2
- `);return h(new O(`Failed to destroy stacks: ${c}
1
+ import{maskSensitiveOutput as T}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as D,CDK_NO_STACKS_MATCH as N,INFRASTRUCTURE_FILENAME as $}from"../../types/constants.js";import{APPLICATION_STACKS as R,getApplicationStackName as g,getApplicationDestroyOrder as U,getParallelDestroyGroups as K}from"../../types/operations.js";import{FrameworkRegistry as F}from"../../orchestration/builders/frameworkRegistry.js";import{success as w,failure as P,isFailure as A}from"@fjall/generator";import{ApplicationError as C}from"../../types/application/ApplicationServiceTypes.js";import{logger as M}from"@fjall/util/logger";function E(t,r,e){if(t.success)return t;const s=A(t)?t.error.message:"";return s.includes(D)||s.includes(N)?w({stackName:g(e,r),stackType:r,outputs:{},skipped:!0}):t}function L(t,r){if(t.success)return null;const e=A(t)?T(t.error.message):"";if(e.includes("TSError")||e.includes("Unable to compile TypeScript")||e.includes("Subprocess exited with error")){const s=e.match(/infrastructure\.ts\(\d+,\d+\): error TS\d+: .+/),d=s?s[0]:`Check ${$} for syntax errors`;return{continue:!1,result:P(new C(`CDK synthesis failed: ${d}`,{errorType:"synth_failed",operation:`destroy-${r}`}))}}return e.includes(D)||e.includes(N)?{continue:!0,result:w({message:"Stack already deleted"})}:{continue:!1,result:P(new C(`Failed to destroy ${r} stack: ${e}`,{errorType:"destroy_failed",operation:`destroy-${r}`}))}}async function W(t,r,e,s,d){const u=r.target,f=F.createDefault().resolve({appPath:r.path});let y,k;if(f){const a=f.builder.plan({appPath:r.path},f.detection);y=a.parallelDestroy,k=a.destroyOrder}else y=!1,k=U({pattern:null,resources:s});if(y){const a=K();for(const h of a){const m=h.stacks;if(m.length===1){const n=m[0],l=g(u,n);e?.onStackStart?.(n,l);const o=await t.destroyStack(n,r,{onOutput:e?.onOutput?c=>e.onOutput?.(c,n):void 0,onResourceProgress:e?.onResourceProgress?c=>e.onResourceProgress?.(c,n):void 0,onLog:e?.onLog,onStackCleanupProgress:e?.onStackCleanupProgress},void 0,d),i=E(o,n,u);e?.onStackComplete&&await e.onStackComplete(n,i);const p=L(i,n);if(p){if(p.continue)continue;return p.result}}else{const n=await t.runCdkSynth(r);if(!n.success)return P(new C(n.error,{errorType:"synth_failed",appName:u,operation:"destroy-parallel-synth"}));e?.onParallelPhaseStart?.(m,h.description);for(const o of m)e?.onStackStart?.(o,g(u,o));const l=await t.destroyStacksInParallel(m,r,{onOutput:e?.onOutput,onResourceProgress:e?.onResourceProgress,onLog:e?.onLog,onStackCleanupProgress:e?.onStackCleanupProgress,onStackComplete:async(o,i,p,c)=>{const O=c?.message!==void 0?T(c.message):"Stack destruction failed",_=i?w({stackName:g(u,o),stackType:o,outputs:{}}):P(c instanceof C?c:new C(O,{errorType:"destroy_failed",appName:u,operation:`destroy-${o}`}));await e?.onStackComplete?.(o,_)}},!0,d);if(l.success){e?.onParallelPhaseComplete?.(l.data);const o=l.data.filter(i=>!i.success);if(o.length>0){const i=o.filter(p=>p.error&&!p.error.message.includes(D)&&!p.error.message.includes(N));if(i.length>0){const p=i.map(O=>O.stack).join(", "),c=i.map(O=>`${O.stack}: ${O.error?.message??"Unknown error"}`).join(`
2
+ `);return P(new C(`Failed to destroy stacks: ${p}
3
3
 
4
- ${d}`,{errorType:"destroy_failed",appName:s,operation:"destroy-parallel"}))}}}}}}else for(const u of y){const C=S(s,u);e?.onStackStart?.(u,C);const m=await t.destroyStack(u,r,{onOutput:e?.onOutput?o=>e.onOutput?.(o,u):void 0,onResourceProgress:e?.onResourceProgress?o=>e.onResourceProgress?.(o,u):void 0}),a=A(m,u,s);e?.onStackComplete&&await e.onStackComplete(u,a);const p=E(a,u);if(p){if(p.continue)continue;return p.result}}return k({message:"All stacks destroyed"})}function W(t,r){return t.map((e,n)=>e.status==="fulfilled"?e.value:{stack:r[n]??N.NETWORK,success:!1,error:e.reason instanceof Error?e.reason:new Error(String(e.reason)),duration:0})}async function H(t,r){try{const e=S(t,N.COMPUTE),n=await r.getStackOutputs(e);if(n.success&&n.data.length>0){const l=n.data.find(y=>y.OutputKey?.includes("LoadBalancerUrl"));if(l?.OutputValue)return l.OutputValue.toLowerCase();const f=n.data.find(y=>y.OutputKey?.includes("LoadBalancerDnsName"));if(f?.OutputValue)return`http://${f.OutputValue.toLowerCase()}`}const s=S(t,N.CDN),w=await r.getStackOutputs(s);if(w.success&&w.data.length>0){const l=w.data.find(f=>f.OutputKey?.includes("DistributionDomainName"));if(l?.OutputValue)return`https://${l.OutputValue.toLowerCase()}`}return}catch(e){F.warn("ApplicationStackService","URL resolution failed",{error:P(e instanceof Error?e.message:String(e))});return}}export{A as convertDestroyResultIfNotExists,G as destroyAllStacks,E as handleDestroyError,W as mapSettledResults,H as resolveWebsiteUrl};
4
+ ${c}`,{errorType:"destroy_failed",appName:u,operation:"destroy-parallel"}))}}}}}}else for(const a of k){const h=g(u,a);e?.onStackStart?.(a,h);const m=await t.destroyStack(a,r,{onOutput:e?.onOutput?o=>e.onOutput?.(o,a):void 0,onResourceProgress:e?.onResourceProgress?o=>e.onResourceProgress?.(o,a):void 0,onLog:e?.onLog,onStackCleanupProgress:e?.onStackCleanupProgress},void 0,d),n=E(m,a,u);e?.onStackComplete&&await e.onStackComplete(a,n);const l=L(n,a);if(l){if(l.continue)continue;return l.result}}return w({message:"All stacks destroyed"})}function H(t,r){return t.map((e,s)=>e.status==="fulfilled"?e.value:{stack:r[s]??R.NETWORK,success:!1,error:e.reason instanceof Error?e.reason:new Error(String(e.reason)),duration:0})}async function q(t,r){try{const e=g(t,R.COMPUTE),s=await r.getStackOutputs(e);if(s.success&&s.data.length>0){const S=s.data.find(y=>y.OutputKey?.includes("LoadBalancerUrl"));if(S?.OutputValue)return S.OutputValue.toLowerCase();const f=s.data.find(y=>y.OutputKey?.includes("LoadBalancerDnsName"));if(f?.OutputValue)return`http://${f.OutputValue.toLowerCase()}`}const d=g(t,R.CDN),u=await r.getStackOutputs(d);if(u.success&&u.data.length>0){const S=u.data.find(f=>f.OutputKey?.includes("DistributionDomainName"));if(S?.OutputValue)return`https://${S.OutputValue.toLowerCase()}`}return}catch(e){M.warn("ApplicationStackService","URL resolution failed",{error:T(e instanceof Error?e.message:String(e))});return}}export{E as convertDestroyResultIfNotExists,W as destroyAllStacks,L as handleDestroyError,H as mapSettledResults,q as resolveWebsiteUrl};
@@ -1 +1 @@
1
- import{filterDangerousEnvVars as u}from"@fjall/util";class s{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?.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){if(r===void 0)return[];const e=Object.entries(r);if(e.length===0)return[];const o=[];for(const[a,n]of e){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.`);o.push("--parameters",`${a}=${n}`)}return o}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={...u(process.env),CI:"true",FORCE_COLOR:"0",CDK_DISABLE_VERSION_CHECK:"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{s as CdkArgumentBuilder};
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};
@@ -13,6 +13,7 @@ export interface CdkContext {
13
13
  fjallOrgId?: string;
14
14
  fjallOidcConfigured?: string;
15
15
  fjallAccountGlobalsConfigured?: string;
16
+ fjallAccountTrailState?: string;
16
17
  orgConfig?: string;
17
18
  fjallAdoptBackupVault?: string;
18
19
  /**
@@ -1 +1 @@
1
- import{getApplicationStackName as l,getOrganisationStackName as i,isApplicationStack as u}from"../../types/operations.js";const p=5e3;function t(e,a){if(e&&!e.includes("*"))return e;if(e){const o=e.match(/\*?(\w+)\*?/);if(o?.[1]){const n=o[1],r=a.target;return u(n)?l(r,n):`${r}${n}`}return e}}function c(e){const a=e.deployType;return a==="organisation"||a==="platform"||a==="account"?i(a):`${e.target}Network`}function f(e,a,o){return{accountId:a,region:o,environment:e.environment,managedAccount:e.isManagedAccount,accountName:e.accountName,orgId:e.orgId,rootId:e.rootId,managementAccountId:e.managementAccountId,ipamPoolId:e.ipamPoolId,fjallOrgId:e.fjallOrgId,fjallOidcConfigured:e.fjallOidcConfigured?"true":void 0,fjallAccountGlobalsConfigured:e.fjallAccountGlobalsConfigured?"true":void 0,orgConfig:e.orgConfig,fjallAdoptBackupVault:e.fjallAdoptBackupVault?"true":void 0,resolvedSecretArns:e.resolvedSecretArns}}export{p as STACK_DETECTION_FALLBACK_MS,f as buildDeploymentCdkContext,c as getFallbackStackName,t as resolveStackName};
1
+ import{getApplicationStackName as l,getOrganisationStackName as i,isApplicationStack as u}from"../../types/operations.js";const t=5e3;function c(a,e){if(a&&!a.includes("*"))return a;if(a){const o=a.match(/\*?(\w+)\*?/);if(o?.[1]){const n=o[1],r=e.target;return u(n)?l(r,n):`${r}${n}`}return a}}function f(a){const e=a.deployType;return e==="organisation"||e==="platform"||e==="account"?i(e):`${a.target}Network`}function p(a,e,o){return{accountId:e,region:o,environment:a.environment,managedAccount:a.isManagedAccount,accountName:a.accountName,orgId:a.orgId,rootId:a.rootId,managementAccountId:a.managementAccountId,ipamPoolId:a.ipamPoolId,fjallOrgId:a.fjallOrgId,fjallOidcConfigured:a.fjallOidcConfigured?"true":void 0,fjallAccountGlobalsConfigured:a.fjallAccountGlobalsConfigured?"true":void 0,fjallAccountTrailState:a.fjallAccountTrailState,orgConfig:a.orgConfig,fjallAdoptBackupVault:a.fjallAdoptBackupVault?"true":void 0,resolvedSecretArns:a.resolvedSecretArns}}export{t as STACK_DETECTION_FALLBACK_MS,p as buildDeploymentCdkContext,f as getFallbackStackName,c as resolveStackName};
@@ -33,6 +33,7 @@ export declare class CdkContextBuilder {
33
33
  fjallOrgId?: string;
34
34
  fjallOidcConfigured?: boolean;
35
35
  fjallAccountGlobalsConfigured?: boolean;
36
+ fjallAccountTrailState?: string;
36
37
  orgConfig?: string;
37
38
  }, options: {
38
39
  verbose?: boolean;
@@ -1 +1 @@
1
- import{DEFAULT_REGION as r}from"@fjall/generator";class t{static buildDeploymentContext(e,o,a){return{deployType:e.deployType,target:e.target,path:e.path,...e.assemblyDir!==void 0?{assemblyDir:e.assemblyDir}:{},...e.environment!==void 0?{environment:e.environment}:{},options:o,stackOutputs:e.stackOutputs||{},callerIdentity:e.callerIdentity,region:e.region||a?.primaryRegion||r,isManagedAccount:e.isManagedAccount,accountName:e.accountName,logPath:e.logPath,orgId:e.orgId,rootId:e.rootId,managementAccountId:e.managementAccountId,ipamPoolId:e.ipamPoolId,fjallOrgId:e.fjallOrgId,fjallOidcConfigured:e.fjallOidcConfigured,fjallAccountGlobalsConfigured:e.fjallAccountGlobalsConfigured,orgConfig:e.orgConfig}}static updateContext(e,o){return{...e,...o}}}export{t as CdkContextBuilder};
1
+ import{DEFAULT_REGION as l}from"@fjall/generator";class d{static buildDeploymentContext(e,o,a){return{deployType:e.deployType,target:e.target,path:e.path,...e.assemblyDir!==void 0?{assemblyDir:e.assemblyDir}:{},...e.environment!==void 0?{environment:e.environment}:{},options:o,stackOutputs:e.stackOutputs||{},callerIdentity:e.callerIdentity,region:e.region||a?.primaryRegion||l,isManagedAccount:e.isManagedAccount,accountName:e.accountName,logPath:e.logPath,orgId:e.orgId,rootId:e.rootId,managementAccountId:e.managementAccountId,ipamPoolId:e.ipamPoolId,fjallOrgId:e.fjallOrgId,fjallOidcConfigured:e.fjallOidcConfigured,fjallAccountGlobalsConfigured:e.fjallAccountGlobalsConfigured,fjallAccountTrailState:e.fjallAccountTrailState,orgConfig:e.orgConfig}}static updateContext(e,o){return{...e,...o}}}export{d as CdkContextBuilder};
@@ -1 +1 @@
1
- import{APPLICATION_STACKS as n,APPLICATION_DEPLOY_ORDER as A,getApplicationStepName as t,getApplicationStepId as a}from"../types/index.js";import{STEP_IDS as e,STEP_NAMES as s,INFRA_STEP_NAME as d}from"../types/index.js";const O={Network:e.NETWORK_DESTROY,Storage:e.STORAGE_DESTROY,Messaging:e.MESSAGING_DESTROY,Database:e.DATABASE_DESTROY,Compute:e.COMPUTE_DESTROY,Cdn:e.CDN_DESTROY};function S(f){return O[f]??`${f.toLowerCase()}-destroy`}class p{static DEPLOYMENT_STEPS=new Map([["application-deploy",[{id:e.AUTH,name:s.AUTH},{id:e.DETECT_CONFIG,name:s.PREPARE_DEPLOY},{id:e.BOOTSTRAP,name:s.BOOTSTRAP,conditions:{requiresInfra:!0}},{id:a(n.NETWORK,"deploy"),name:t(n.NETWORK,"deploy"),conditions:{requiresInfra:!0,requiresNetwork:!0}},{id:a(n.STORAGE,"deploy"),name:t(n.STORAGE,"deploy"),conditions:{requiresInfra:!0,requiresStorage:!0}},{id:a(n.MESSAGING,"deploy"),name:t(n.MESSAGING,"deploy"),conditions:{requiresInfra:!0,requiresMessaging:!0}},{id:a(n.DATABASE,"deploy"),name:t(n.DATABASE,"deploy"),conditions:{requiresInfra:!0,requiresDatabase:!0}},{id:e.DOCKER_OPERATIONS,name:"Docker operations",conditions:{requiresDocker:!0,requiresCompute:!0,skipForInfraOnly:!0}},{id:e.TAG_ECR_IMAGES,name:"Tagging container images",conditions:{requiresImageTagging:!0,requiresCompute:!0,skipForInfraOnly:!0}},{id:a(n.COMPUTE,"deploy"),name:t(n.COMPUTE,"deploy"),conditions:{requiresInfra:!0,requiresCompute:!0}},{id:a(n.CDN,"deploy"),name:t(n.CDN,"deploy"),conditions:{requiresInfra:!0,requiresCdn:!0}}]],["application-destroy",[{id:e.AUTH,name:s.AUTH},{id:e.DETECT_CONFIG,name:s.CHECK_INFRA_STATE},{id:a(n.CDN,"destroy"),name:t(n.CDN,"destroy"),conditions:{requiresCdn:!0}},{id:a(n.COMPUTE,"destroy"),name:t(n.COMPUTE,"destroy")},{id:a(n.DATABASE,"destroy"),name:t(n.DATABASE,"destroy")},{id:a(n.MESSAGING,"destroy"),name:t(n.MESSAGING,"destroy"),conditions:{requiresMessaging:!0}},{id:a(n.STORAGE,"destroy"),name:t(n.STORAGE,"destroy"),conditions:{requiresStorage:!0}},{id:a(n.NETWORK,"destroy"),name:t(n.NETWORK,"destroy")}]],["organisation-deploy",[{id:e.AUTH,name:s.AUTH},{id:e.ORG_SETUP,name:s.ORG_SETUP},{id:e.PREPARE_ENVIRONMENT,name:d.PREPARE},{id:e.ORG_DEPLOY,name:s.ORG_DEPLOY,conditions:{requiresOrgChanges:!0}},{id:e.CASCADE_PLATFORM,name:s.CASCADE_PLATFORM,conditions:{requiresPlatformAccount:!0}},{id:e.CASCADE_ACCOUNTS,name:s.CASCADE_ACCOUNTS,conditions:{requiresMemberAccounts:!0}}]],["organisation-destroy",[{id:e.AUTH,name:s.AUTH},{id:e.CFN_CHECK,name:s.CHECK_INFRA_STATE},{id:e.CASCADE_ACCOUNTS,name:"Destroying account infrastructure",conditions:{requiresMemberAccounts:!0}},{id:e.CASCADE_PLATFORM,name:"Destroying platform infrastructure",conditions:{requiresPlatformAccount:!0}},{id:e.DESTROY,name:"Destroying organisation infrastructure"}]],["platform-deploy",[{id:e.AUTH,name:s.AUTH},{id:e.CONNECT,name:d.CONNECT},{id:e.PREPARE_ENVIRONMENT,name:d.PREPARE},{id:e.DEPLOY,name:d.DEPLOY},{id:e.MONITORING,name:d.MONITORING}]],["platform-destroy",[{id:e.AUTH,name:s.AUTH},{id:e.CFN_CHECK,name:"Checking infrastructure state"},{id:e.ORG_DESTROY,name:"Destroying platform infrastructure"}]],["account-deploy",[{id:e.AUTH,name:s.AUTH},{id:e.CONNECT,name:d.CONNECT},{id:e.PREPARE_ENVIRONMENT,name:d.PREPARE},{id:e.DEPLOY,name:d.DEPLOY},{id:e.MONITORING,name:d.MONITORING}]],["account-destroy",[{id:e.AUTH,name:s.AUTH},{id:e.CFN_CHECK,name:"Checking infrastructure state"},{id:e.ORG_DESTROY,name:"Destroying account infrastructure"}]]]);static getSteps(r){if(r.deploymentType==="application"&&r.operation==="deploy"&&r.deployOnly)return[{id:e.AUTH,name:s.AUTH},{id:e.DOCKER_OPERATIONS,name:"Build and push Docker container"},{id:a(n.COMPUTE,"deploy"),name:t(n.COMPUTE,"deploy")}];const o=`${r.deploymentType}-${r.operation}`;return(this.DEPLOYMENT_STEPS.get(o)??[]).filter(i=>{if(r.deploymentType==="application"){const l=r.builderName==="opennext";if(i.conditions?.requiresInfra&&r.deployOnly)return!1;if(r.resources){const c=r.resources;if(i.conditions?.requiresNetwork&&!c.hasNetwork||i.conditions?.requiresCompute&&!c.hasCompute||i.conditions?.requiresDatabase&&!c.hasDatabase||i.conditions?.requiresStorage&&!c.hasStorage||i.conditions?.requiresMessaging&&!c.hasMessaging||i.conditions?.requiresCdn&&!c.hasCdn)return!1}else if(r.operation==="deploy"&&!l&&(i.conditions?.requiresNetwork||i.conditions?.requiresCompute||i.conditions?.requiresDatabase||i.conditions?.requiresStorage||i.conditions?.requiresMessaging||i.conditions?.requiresCdn))return!1;if(i.conditions?.requiresDocker||i.id===e.DOCKER_OPERATIONS)return l||r.infraOnly||r.resources&&!r.resources.hasCompute?!1:r.deployOnly?r.hasDockerfile===!0:r.hasDockerfile===!0||r.hasDockerfile===!1&&!r.isManagedAccount;if(i.conditions?.requiresImageTagging)return r.infraOnly||l||r.resources&&!r.resources.hasCompute?!1:r.hasDockerfile===!1}return!(i.conditions?.requiresMemberAccounts&&!r.hasMemberAccounts||i.conditions?.requiresPlatformAccount&&!r.hasPlatformAccount||i.conditions?.requiresOrgChanges&&r.hasOrgChanges!==!0||i.conditions?.requiresPlatformChanges&&r.hasPlatformChanges!==!0||i.conditions?.requiresAccountChanges&&r.hasAccountChanges!==!0||i.conditions?.requiresDomainConfiguration&&!r.hasDomainConfiguration||i.conditions?.requiresDomainChanges&&r.hasDomainChanges!==!0)}).map(i=>i.id===e.DOCKER_OPERATIONS?{...i,name:r.hasDockerfile?"Building and pushing Docker image":"Initialising container repository"}:i)}static getStepNames(r){return this.getSteps(r).map(o=>o.name)}static getStepIndex(r,o){return this.getSteps(o).findIndex(E=>E.id===r)}static getStepById(r,o="application"){const u=`${o}-deploy`,i=(this.DEPLOYMENT_STEPS.get(u)||[]).find(T=>T.id===r);if(i)return i;const l=`${o}-destroy`;return(this.DEPLOYMENT_STEPS.get(l)||[]).find(T=>T.id===r)}static isStepIncluded(r,o){return this.getSteps(o).some(E=>E.id===r)}static createContext(r,o,u){return{deploymentType:r,operation:o,deployOnly:u?.deployOnly??!1,infraOnly:u?.infraOnly??!1,isManagedAccount:u?.isManagedAccount??!1,hasDockerfile:u?.hasDockerfile??!1,pattern:u?.pattern??null,builderName:u?.builderName,resources:u?.resources}}static getDeploymentTypes(){return["application","organisation","platform","account"]}static getStackNames(r){switch(r){case"application":return[...A];case"organisation":return["Organisation"];case"platform":return["Platform"];case"account":return["Account"];default:return[]}}static getInfraStepIds(){return[e.CFN_CHECK,e.BOOTSTRAP,e.DIFF,e.NETWORK,e.STORAGE,e.MESSAGING,e.DATABASE,e.COMPUTE,e.CDN]}static getDockerStepIds(){return[e.ECR_INIT,e.DOCKER_DEPLOY]}}export{p as StepRegistry,S as getDestroyStepId};
1
+ import{APPLICATION_STACKS as n,APPLICATION_DEPLOY_ORDER as l,getApplicationStepName as t,getApplicationStepId as a}from"../types/index.js";import{STEP_IDS as e,STEP_NAMES as s,INFRA_STEP_NAME as d}from"../types/index.js";const O={Network:e.NETWORK_DESTROY,Storage:e.STORAGE_DESTROY,Messaging:e.MESSAGING_DESTROY,Database:e.DATABASE_DESTROY,Compute:e.COMPUTE_DESTROY,Cdn:e.CDN_DESTROY};function C(A){return O[A]??`${A.toLowerCase()}-destroy`}class p{static DEPLOYMENT_STEPS=new Map([["application-deploy",[{id:e.AUTH,name:s.AUTH},{id:e.DETECT_CONFIG,name:s.PREPARE_DEPLOY},{id:e.TARGET_READINESS,name:s.TARGET_READINESS,conditions:{requiresInfra:!0}},{id:e.BOOTSTRAP,name:s.BOOTSTRAP,conditions:{requiresInfra:!0}},{id:a(n.NETWORK,"deploy"),name:t(n.NETWORK,"deploy"),conditions:{requiresInfra:!0,requiresNetwork:!0}},{id:a(n.STORAGE,"deploy"),name:t(n.STORAGE,"deploy"),conditions:{requiresInfra:!0,requiresStorage:!0}},{id:a(n.MESSAGING,"deploy"),name:t(n.MESSAGING,"deploy"),conditions:{requiresInfra:!0,requiresMessaging:!0}},{id:a(n.DATABASE,"deploy"),name:t(n.DATABASE,"deploy"),conditions:{requiresInfra:!0,requiresDatabase:!0}},{id:e.DOCKER_OPERATIONS,name:"Docker operations",conditions:{requiresDocker:!0,requiresCompute:!0,skipForInfraOnly:!0}},{id:e.TAG_ECR_IMAGES,name:"Tagging container images",conditions:{requiresImageTagging:!0,requiresCompute:!0,skipForInfraOnly:!0}},{id:a(n.COMPUTE,"deploy"),name:t(n.COMPUTE,"deploy"),conditions:{requiresInfra:!0,requiresCompute:!0}},{id:a(n.CDN,"deploy"),name:t(n.CDN,"deploy"),conditions:{requiresInfra:!0,requiresCdn:!0}}]],["application-destroy",[{id:e.AUTH,name:s.AUTH},{id:e.DETECT_CONFIG,name:s.CHECK_INFRA_STATE},{id:a(n.CDN,"destroy"),name:t(n.CDN,"destroy"),conditions:{requiresCdn:!0}},{id:a(n.COMPUTE,"destroy"),name:t(n.COMPUTE,"destroy")},{id:a(n.DATABASE,"destroy"),name:t(n.DATABASE,"destroy")},{id:a(n.MESSAGING,"destroy"),name:t(n.MESSAGING,"destroy"),conditions:{requiresMessaging:!0}},{id:a(n.STORAGE,"destroy"),name:t(n.STORAGE,"destroy"),conditions:{requiresStorage:!0}},{id:a(n.NETWORK,"destroy"),name:t(n.NETWORK,"destroy")}]],["organisation-deploy",[{id:e.AUTH,name:s.AUTH},{id:e.ORG_SETUP,name:s.ORG_SETUP},{id:e.PREPARE_ENVIRONMENT,name:d.PREPARE},{id:e.ORG_DEPLOY,name:s.ORG_DEPLOY,conditions:{requiresOrgChanges:!0}},{id:e.CASCADE_PLATFORM,name:s.CASCADE_PLATFORM,conditions:{requiresPlatformAccount:!0}},{id:e.CASCADE_ACCOUNTS,name:s.CASCADE_ACCOUNTS,conditions:{requiresMemberAccounts:!0}}]],["organisation-destroy",[{id:e.AUTH,name:s.AUTH},{id:e.CFN_CHECK,name:s.CHECK_INFRA_STATE},{id:e.CASCADE_ACCOUNTS,name:"Destroying account infrastructure",conditions:{requiresMemberAccounts:!0}},{id:e.CASCADE_PLATFORM,name:"Destroying platform infrastructure",conditions:{requiresPlatformAccount:!0}},{id:e.DESTROY,name:"Destroying organisation infrastructure"}]],["platform-deploy",[{id:e.AUTH,name:s.AUTH},{id:e.CONNECT,name:d.CONNECT},{id:e.PREPARE_ENVIRONMENT,name:d.PREPARE},{id:e.DEPLOY,name:d.DEPLOY},{id:e.MONITORING,name:d.MONITORING}]],["platform-destroy",[{id:e.AUTH,name:s.AUTH},{id:e.CFN_CHECK,name:"Checking infrastructure state"},{id:e.ORG_DESTROY,name:"Destroying platform infrastructure"}]],["account-deploy",[{id:e.AUTH,name:s.AUTH},{id:e.CONNECT,name:d.CONNECT},{id:e.PREPARE_ENVIRONMENT,name:d.PREPARE},{id:e.DEPLOY,name:d.DEPLOY},{id:e.MONITORING,name:d.MONITORING}]],["account-destroy",[{id:e.AUTH,name:s.AUTH},{id:e.CFN_CHECK,name:"Checking infrastructure state"},{id:e.ORG_DESTROY,name:"Destroying account infrastructure"}]]]);static getSteps(r){if(r.deploymentType==="application"&&r.operation==="deploy"&&r.deployOnly)return[{id:e.AUTH,name:s.AUTH},{id:e.DOCKER_OPERATIONS,name:"Build and push Docker container"},{id:a(n.COMPUTE,"deploy"),name:t(n.COMPUTE,"deploy")}];const o=`${r.deploymentType}-${r.operation}`;return(this.DEPLOYMENT_STEPS.get(o)??[]).filter(i=>{if(r.deploymentType==="application"){const T=r.builderName==="opennext";if(i.conditions?.requiresInfra&&r.deployOnly)return!1;if(r.resources){const E=r.resources;if(i.conditions?.requiresNetwork&&!E.hasNetwork||i.conditions?.requiresCompute&&!E.hasCompute||i.conditions?.requiresDatabase&&!E.hasDatabase||i.conditions?.requiresStorage&&!E.hasStorage||i.conditions?.requiresMessaging&&!E.hasMessaging||i.conditions?.requiresCdn&&!E.hasCdn)return!1}else if(r.operation==="deploy"&&!T&&(i.conditions?.requiresNetwork||i.conditions?.requiresCompute||i.conditions?.requiresDatabase||i.conditions?.requiresStorage||i.conditions?.requiresMessaging||i.conditions?.requiresCdn))return!1;if(i.conditions?.requiresDocker||i.id===e.DOCKER_OPERATIONS)return T||r.infraOnly||r.resources&&!r.resources.hasCompute?!1:r.deployOnly?r.hasDockerfile===!0:r.hasDockerfile===!0||r.hasDockerfile===!1&&!r.isManagedAccount;if(i.conditions?.requiresImageTagging)return r.infraOnly||T||r.resources&&!r.resources.hasCompute?!1:r.hasDockerfile===!1}return!(i.conditions?.requiresMemberAccounts&&!r.hasMemberAccounts||i.conditions?.requiresPlatformAccount&&!r.hasPlatformAccount||i.conditions?.requiresOrgChanges&&r.hasOrgChanges!==!0||i.conditions?.requiresPlatformChanges&&r.hasPlatformChanges!==!0||i.conditions?.requiresAccountChanges&&r.hasAccountChanges!==!0||i.conditions?.requiresDomainConfiguration&&!r.hasDomainConfiguration||i.conditions?.requiresDomainChanges&&r.hasDomainChanges!==!0)}).map(i=>i.id===e.DOCKER_OPERATIONS?{...i,name:r.hasDockerfile?"Building and pushing Docker image":"Initialising container repository"}:i)}static getStepNames(r){return this.getSteps(r).map(o=>o.name)}static getStepIndex(r,o){return this.getSteps(o).findIndex(c=>c.id===r)}static getStepById(r,o="application"){const u=`${o}-deploy`,i=(this.DEPLOYMENT_STEPS.get(u)||[]).find(f=>f.id===r);if(i)return i;const T=`${o}-destroy`;return(this.DEPLOYMENT_STEPS.get(T)||[]).find(f=>f.id===r)}static isStepIncluded(r,o){return this.getSteps(o).some(c=>c.id===r)}static createContext(r,o,u){return{deploymentType:r,operation:o,deployOnly:u?.deployOnly??!1,infraOnly:u?.infraOnly??!1,isManagedAccount:u?.isManagedAccount??!1,hasDockerfile:u?.hasDockerfile??!1,pattern:u?.pattern??null,builderName:u?.builderName,resources:u?.resources}}static getDeploymentTypes(){return["application","organisation","platform","account"]}static getStackNames(r){switch(r){case"application":return[...l];case"organisation":return["Organisation"];case"platform":return["Platform"];case"account":return["Account"];default:return[]}}static getInfraStepIds(){return[e.CFN_CHECK,e.TARGET_READINESS,e.BOOTSTRAP,e.DIFF,e.NETWORK,e.STORAGE,e.MESSAGING,e.DATABASE,e.COMPUTE,e.CDN]}static getDockerStepIds(){return[e.ECR_INIT,e.DOCKER_DEPLOY]}}export{p as StepRegistry,C as getDestroyStepId};