@fjall/deploy-core 0.89.4 → 0.89.6

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 (202) hide show
  1. package/LICENSE +50 -21
  2. package/README.md +25 -0
  3. package/dist/.minified +1 -0
  4. package/dist/src/__test-utils__/awsMockHelpers.d.ts +20 -0
  5. package/dist/src/__test-utils__/awsMockHelpers.js +1 -0
  6. package/dist/src/__test-utils__/index.d.ts +1 -0
  7. package/dist/src/__test-utils__/index.js +1 -0
  8. package/dist/src/aws/AwsProvider.d.ts +2 -2
  9. package/dist/src/aws/AwsProvider.js +0 -1
  10. package/dist/src/aws/SimpleAwsProvider.d.ts +2 -3
  11. package/dist/src/aws/SimpleAwsProvider.js +1 -73
  12. package/dist/src/aws/index.d.ts +4 -2
  13. package/dist/src/aws/index.js +1 -3
  14. package/dist/src/aws/organisations/accounts.js +10 -10
  15. package/dist/src/aws/organisations/backup.js +4 -2
  16. package/dist/src/aws/organisations/costAllocation.js +4 -2
  17. package/dist/src/aws/organisations/delegatedAdmin.d.ts +9 -0
  18. package/dist/src/aws/organisations/delegatedAdmin.js +43 -0
  19. package/dist/src/aws/organisations/identityCentre.d.ts +1 -1
  20. package/dist/src/aws/organisations/identityCentre.js +6 -2
  21. package/dist/src/aws/organisations/index.d.ts +4 -3
  22. package/dist/src/aws/organisations/index.js +1 -12
  23. package/dist/src/aws/organisations/ipam.js +4 -2
  24. package/dist/src/aws/organisations/organisation.js +27 -18
  25. package/dist/src/aws/organisations/organisationalUnits.d.ts +26 -6
  26. package/dist/src/aws/organisations/organisationalUnits.js +149 -35
  27. package/dist/src/aws/organisations/policies.js +4 -3
  28. package/dist/src/aws/organisations/ram.js +6 -2
  29. package/dist/src/aws/organisations/serviceAccess.js +12 -6
  30. package/dist/src/aws/organisations/trustedAccess.js +6 -2
  31. package/dist/src/aws/organisations/types.d.ts +23 -1
  32. package/dist/src/aws/organisations/types.js +1 -16
  33. package/dist/src/aws/utils/__tests__/cloudformationTestHelpers.d.ts +6 -0
  34. package/dist/src/aws/utils/__tests__/cloudformationTestHelpers.js +1 -0
  35. package/dist/src/aws/utils/cloudformationEventHelpers.d.ts +48 -0
  36. package/dist/src/aws/utils/cloudformationEventHelpers.js +1 -0
  37. package/dist/src/aws/utils/cloudformationEventTypes.d.ts +45 -0
  38. package/dist/src/aws/utils/cloudformationEventTypes.js +1 -0
  39. package/dist/src/aws/utils/cloudformationEvents.d.ts +8 -54
  40. package/dist/src/aws/utils/cloudformationEvents.js +1 -596
  41. package/dist/src/aws/utils/index.d.ts +5 -0
  42. package/dist/src/aws/utils/index.js +1 -0
  43. package/dist/src/aws/utils/stackStatus.js +1 -90
  44. package/dist/src/events/index.d.ts +13 -0
  45. package/dist/src/events/index.js +1 -0
  46. package/dist/src/index.d.ts +34 -17
  47. package/dist/src/index.js +41 -21
  48. package/dist/src/orchestration/__tests__/cascadeTestHelpers.d.ts +12 -0
  49. package/dist/src/orchestration/__tests__/cascadeTestHelpers.js +78 -0
  50. package/dist/src/orchestration/activeDeploymentGuard.d.ts +10 -0
  51. package/dist/src/orchestration/activeDeploymentGuard.js +39 -0
  52. package/dist/src/orchestration/applicationDeploy.js +46 -224
  53. package/dist/src/orchestration/applicationDeployHelpers.d.ts +39 -0
  54. package/dist/src/orchestration/applicationDeployHelpers.js +223 -0
  55. package/dist/src/orchestration/applicationDestroy.d.ts +14 -0
  56. package/dist/src/orchestration/applicationDestroy.js +131 -0
  57. package/dist/src/orchestration/builders/dockerBuilder.d.ts +17 -0
  58. package/dist/src/orchestration/builders/dockerBuilder.js +98 -0
  59. package/dist/src/orchestration/builders/frameworkRegistry.d.ts +23 -0
  60. package/dist/src/orchestration/builders/frameworkRegistry.js +1 -0
  61. package/dist/src/orchestration/builders/index.d.ts +4 -0
  62. package/dist/src/orchestration/builders/index.js +1 -0
  63. package/dist/src/orchestration/builders/openNextBuilder.d.ts +21 -0
  64. package/dist/src/orchestration/builders/openNextBuilder.js +144 -0
  65. package/dist/src/orchestration/cascadeDestroyHelpers.d.ts +30 -0
  66. package/dist/src/orchestration/cascadeDestroyHelpers.js +1 -0
  67. package/dist/src/orchestration/cascadeHelpers.d.ts +46 -0
  68. package/dist/src/orchestration/cascadeHelpers.js +160 -0
  69. package/dist/src/orchestration/contextHelpers.d.ts +47 -2
  70. package/dist/src/orchestration/contextHelpers.js +94 -1
  71. package/dist/src/orchestration/destroy.d.ts +13 -0
  72. package/dist/src/orchestration/destroy.js +67 -0
  73. package/dist/src/orchestration/detectionPipeline.d.ts +2 -11
  74. package/dist/src/orchestration/detectionPipeline.js +29 -10
  75. package/dist/src/orchestration/dockerBuildHelper.d.ts +10 -0
  76. package/dist/src/orchestration/dockerBuildHelper.js +49 -0
  77. package/dist/src/orchestration/dockerInterface.d.ts +4 -2
  78. package/dist/src/orchestration/index.d.ts +8 -1
  79. package/dist/src/orchestration/index.js +1 -3
  80. package/dist/src/orchestration/manifestSecretParser.d.ts +11 -0
  81. package/dist/src/orchestration/manifestSecretParser.js +1 -0
  82. package/dist/src/orchestration/openNextBuild.d.ts +28 -0
  83. package/dist/src/orchestration/openNextBuild.js +243 -0
  84. package/dist/src/orchestration/organisationDeploy.js +130 -228
  85. package/dist/src/orchestration/organisationDestroy.d.ts +24 -0
  86. package/dist/src/orchestration/organisationDestroy.js +189 -0
  87. package/dist/src/orchestration/organisationSetup.d.ts +6 -4
  88. package/dist/src/orchestration/organisationSetup.js +28 -8
  89. package/dist/src/orchestration/resolveOperation.js +68 -6
  90. package/dist/src/orchestration/serviceFactory.d.ts +4 -0
  91. package/dist/src/orchestration/serviceFactory.js +1 -16
  92. package/dist/src/orchestration/spawnHelpers.d.ts +47 -0
  93. package/dist/src/orchestration/spawnHelpers.js +1 -0
  94. package/dist/src/orchestration/stackCleanup.d.ts +39 -0
  95. package/dist/src/orchestration/stackCleanup.js +1 -0
  96. package/dist/src/orchestration/welcomeImageHelper.d.ts +15 -0
  97. package/dist/src/orchestration/welcomeImageHelper.js +64 -0
  98. package/dist/src/services/application/ApplicationStackService.d.ts +21 -30
  99. package/dist/src/services/application/ApplicationStackService.js +16 -234
  100. package/dist/src/services/application/applicationStackHelpers.d.ts +46 -0
  101. package/dist/src/services/application/applicationStackHelpers.js +248 -0
  102. package/dist/src/services/application/index.d.ts +1 -0
  103. package/dist/src/services/application/index.js +1 -1
  104. package/dist/src/services/index.d.ts +6 -0
  105. package/dist/src/services/index.js +1 -0
  106. package/dist/src/services/infrastructure/CdkArgumentBuilder.js +1 -67
  107. package/dist/src/services/infrastructure/CdkCommandRunner.d.ts +10 -2
  108. package/dist/src/services/infrastructure/CdkCommandRunner.js +18 -15
  109. package/dist/src/services/infrastructure/CdkErrorFormatter.js +16 -194
  110. package/dist/src/services/infrastructure/CdkEventMonitoring.js +1 -41
  111. package/dist/src/services/infrastructure/CdkOutputAnalyser.js +1 -1
  112. package/dist/src/services/infrastructure/CdkOutputParser.js +2 -33
  113. package/dist/src/services/infrastructure/CdkProcessManager.d.ts +5 -0
  114. package/dist/src/services/infrastructure/CdkProcessManager.js +81 -47
  115. package/dist/src/services/infrastructure/CdkService.d.ts +7 -52
  116. package/dist/src/services/infrastructure/CdkService.js +41 -82
  117. package/dist/src/services/infrastructure/CdkServiceTypes.d.ts +50 -0
  118. package/dist/src/services/infrastructure/CdkServiceTypes.js +0 -0
  119. package/dist/src/services/infrastructure/CloudFormationService.js +9 -10
  120. package/dist/src/services/infrastructure/ICdkProcessManager.d.ts +27 -0
  121. package/dist/src/services/infrastructure/ICdkProcessManager.js +1 -0
  122. package/dist/src/services/infrastructure/__tests__/cloudFormationTestHelpers.d.ts +9 -0
  123. package/dist/src/services/infrastructure/__tests__/cloudFormationTestHelpers.js +1 -0
  124. package/dist/src/services/infrastructure/cdkServiceHelpers.d.ts +9 -0
  125. package/dist/src/services/infrastructure/cdkServiceHelpers.js +1 -0
  126. package/dist/src/services/infrastructure/constructMapEnrichment.d.ts +7 -0
  127. package/dist/src/services/infrastructure/constructMapEnrichment.js +1 -0
  128. package/dist/src/services/infrastructure/index.d.ts +3 -1
  129. package/dist/src/services/infrastructure/index.js +1 -7
  130. package/dist/src/services/supporting/TemplateHashService.js +1 -1
  131. package/dist/src/services/supporting/helpers.js +1 -81
  132. package/dist/src/services/supporting/index.js +1 -3
  133. package/dist/src/steps/index.d.ts +1 -0
  134. package/dist/src/steps/index.js +1 -0
  135. package/dist/src/steps/stepRegistry.d.ts +71 -0
  136. package/dist/src/steps/stepRegistry.js +505 -0
  137. package/dist/src/types/FjallState.js +1 -118
  138. package/dist/src/types/ProgressEvent.js +1 -48
  139. package/dist/src/types/application/ApplicationServiceTypes.js +1 -30
  140. package/dist/src/types/application/index.js +1 -1
  141. package/dist/src/types/callbacks.d.ts +76 -4
  142. package/dist/src/types/callbacks.js +0 -1
  143. package/dist/src/types/constants.d.ts +2 -0
  144. package/dist/src/types/constants.js +1 -6
  145. package/dist/src/types/credentials.d.ts +1 -1
  146. package/dist/src/types/credentials.js +0 -1
  147. package/dist/src/types/deployment/DeploymentServiceTypes.d.ts +5 -2
  148. package/dist/src/types/deployment/DeploymentServiceTypes.js +1 -1
  149. package/dist/src/types/deployment/DeploymentTypes.d.ts +1 -0
  150. package/dist/src/types/deployment/DeploymentTypes.js +0 -1
  151. package/dist/src/types/deployment/cloudformation.js +0 -1
  152. package/dist/src/types/deployment/index.d.ts +3 -1
  153. package/dist/src/types/deployment/index.js +1 -1
  154. package/dist/src/types/deployment/parallel.js +1 -10
  155. package/dist/src/types/deploymentEventSchema.d.ts +158 -0
  156. package/dist/src/types/deploymentEventSchema.js +1 -0
  157. package/dist/src/types/detection.d.ts +22 -0
  158. package/dist/src/types/detection.js +1 -0
  159. package/dist/src/types/entitlements.d.ts +31 -0
  160. package/dist/src/types/entitlements.js +0 -0
  161. package/dist/src/types/errors/CdkError.js +1 -20
  162. package/dist/src/types/errors/ServiceError.d.ts +2 -1
  163. package/dist/src/types/errors/ServiceError.js +1 -119
  164. package/dist/src/types/errors/index.d.ts +2 -0
  165. package/dist/src/types/errors/index.js +1 -0
  166. package/dist/src/types/events.d.ts +3 -9
  167. package/dist/src/types/events.js +0 -5
  168. package/dist/src/types/frameworkBuilder.d.ts +96 -0
  169. package/dist/src/types/frameworkBuilder.js +8 -0
  170. package/dist/src/types/index.d.ts +19 -4
  171. package/dist/src/types/index.js +1 -9
  172. package/dist/src/types/operations.d.ts +3 -2
  173. package/dist/src/types/operations.js +1 -285
  174. package/dist/src/types/orgConfig.d.ts +2 -10
  175. package/dist/src/types/orgConfig.js +0 -11
  176. package/dist/src/types/params.d.ts +61 -0
  177. package/dist/src/types/patternDetection.d.ts +14 -16
  178. package/dist/src/types/patternDetection.js +14 -18
  179. package/dist/src/types/patternTypes.d.ts +19 -0
  180. package/dist/src/types/patternTypes.js +1 -0
  181. package/dist/src/types/stepDefinitions.d.ts +163 -0
  182. package/dist/src/types/stepDefinitions.js +98 -0
  183. package/dist/src/types/validation.js +0 -1
  184. package/dist/src/util/dockerfileDetection.d.ts +5 -0
  185. package/dist/src/util/dockerfileDetection.js +1 -0
  186. package/dist/src/util/index.d.ts +4 -3
  187. package/dist/src/util/index.js +1 -3
  188. package/dist/src/util/sequencedCallbacks.d.ts +44 -0
  189. package/dist/src/util/sequencedCallbacks.js +1 -0
  190. package/package.json +49 -8
  191. package/dist/src/aws/utils/CloudFormationFailureAnalyser.d.ts +0 -32
  192. package/dist/src/aws/utils/CloudFormationFailureAnalyser.js +0 -228
  193. package/dist/src/aws/utils/errors.d.ts +0 -26
  194. package/dist/src/aws/utils/errors.js +0 -59
  195. package/dist/src/util/fsHelpers.d.ts +0 -4
  196. package/dist/src/util/fsHelpers.js +0 -16
  197. package/dist/src/util/securityHelpers.d.ts +0 -31
  198. package/dist/src/util/securityHelpers.js +0 -124
  199. package/dist/src/util/singleton.d.ts +0 -2
  200. package/dist/src/util/singleton.js +0 -9
  201. package/dist/src/util/sleep.d.ts +0 -4
  202. package/dist/src/util/sleep.js +0 -4
@@ -1,10 +1,11 @@
1
1
  import { success, failure } from "@fjall/generator";
2
- import { logger } from "@fjall/util";
3
2
  import { ORGANISATION_TYPES, getOrganisationStackName } from "../types/operations.js";
4
3
  import { CdkContextBuilder } from "../services/supporting/CdkContextBuilder.js";
5
- import { CloudFormationService } from "../services/infrastructure/CloudFormationService.js";
6
- import { SimpleAwsProvider } from "../aws/SimpleAwsProvider.js";
7
- import { buildParamsContext } from "./contextHelpers.js";
4
+ import { stubCallerIdentity } from "../types/deployment/index.js";
5
+ import { buildParamsContext, collectStackOutputs, synthOrFail, bootstrapOrFail, forwardOutput, forwardResourceProgress } from "./contextHelpers.js";
6
+ import { partitionAccounts, deployCascadeAccount, readPlatformIpamPoolIds, deployDomains } from "./cascadeHelpers.js";
7
+ import { maskSensitiveOutput } from "@fjall/util";
8
+ import { INFRA_STEP_NAME, STEP_IDS } from "../types/stepDefinitions.js";
8
9
  /**
9
10
  * Organisation deployment orchestration.
10
11
  *
@@ -41,57 +42,81 @@ function buildOrgContext(params, services, operation, deployType, accountName) {
41
42
  path: operation.path,
42
43
  region: services.awsProvider.getRegion(),
43
44
  accountName,
44
- callerIdentity: {
45
- Account: services.awsProvider.getAccountId() ?? "",
46
- Arn: "",
47
- UserId: ""
48
- },
49
- ...buildParamsContext(params)
45
+ callerIdentity: stubCallerIdentity(services.awsProvider.getAccountId()),
46
+ ...buildParamsContext({
47
+ orgConfig: params.orgConfig,
48
+ identity: params.identity,
49
+ skipOidc: params.options?.skipOidc
50
+ })
50
51
  }, {
51
52
  verbose: params.options?.verbose,
52
53
  infraOnly: params.options?.infraOnly
53
54
  }, params.orgConfig);
54
55
  }
56
+ const INFRA_STEPS = {
57
+ CONNECT: { id: STEP_IDS.CONNECT, name: INFRA_STEP_NAME.CONNECT },
58
+ PREPARE: { id: STEP_IDS.PREPARE_ENVIRONMENT, name: INFRA_STEP_NAME.PREPARE },
59
+ DEPLOY: { id: STEP_IDS.DEPLOY, name: INFRA_STEP_NAME.DEPLOY },
60
+ MONITORING: { id: STEP_IDS.MONITORING, name: INFRA_STEP_NAME.MONITORING },
61
+ ORG_DEPLOY: {
62
+ id: STEP_IDS.ORG_DEPLOY,
63
+ name: "Deploying organisation infrastructure"
64
+ }
65
+ };
66
+ const INFRA_STEP_TOTAL = 4;
55
67
  /**
56
68
  * Deploy a single organisation component (platform or account).
69
+ *
70
+ * Emits 4 named steps from INFRASTRUCTURE_STEP_NAMES: Connect securely →
71
+ * Prepare environment → Deploy infrastructure → Enable monitoring.
57
72
  */
58
73
  async function deploySingleComponent(params, services, operation, deployType, startTime) {
59
74
  const { callbacks } = params;
75
+ // Step 1: Connect securely — already seeded by the webapp trigger.
76
+ // Complete it now: credentials are available by the time deploy-core runs.
77
+ callbacks.onStepComplete?.(INFRA_STEPS.CONNECT.id, INFRA_STEPS.CONNECT.name, "completed", 0, INFRA_STEP_TOTAL);
78
+ // Step 2: Prepare environment (synth + bootstrap)
79
+ callbacks.onStepStart?.(INFRA_STEPS.PREPARE.id, INFRA_STEPS.PREPARE.name, 1, INFRA_STEP_TOTAL);
60
80
  const context = buildOrgContext(params, services, operation, deployType, deployType === "account" ? operation.target : undefined);
61
81
  // Synth
62
82
  callbacks.onLog?.(`Synthesising ${deployType} infrastructure…`, "info");
63
- const synthResult = await services.cdkService.runCdkSynth(context, (chunk) => callbacks.onCdkOutput?.(chunk, "synth"));
83
+ const synthResult = await synthOrFail(services, context, callbacks, "CDK synthesis failed");
64
84
  if (!synthResult.success) {
65
- const error = new Error(`CDK synthesis failed: ${synthResult.error}`);
66
- callbacks.onError?.(error);
67
- return failure(error);
85
+ callbacks.onStepComplete?.(INFRA_STEPS.PREPARE.id, INFRA_STEPS.PREPARE.name, "error", 1, INFRA_STEP_TOTAL);
86
+ return synthResult;
68
87
  }
69
88
  // Bootstrap
70
- callbacks.onCDKBootstrap?.("bootstrapping");
71
- const bootstrapResult = await services.cdkService.runCdkBootstrap(context, (chunk) => callbacks.onOutput?.(chunk));
72
- if (!bootstrapResult.success) {
73
- callbacks.onCDKBootstrap?.("failed");
74
- const error = new Error(`Bootstrap failed: ${bootstrapResult.error}`);
75
- callbacks.onError?.(error);
76
- return failure(error);
89
+ const bsResult = await bootstrapOrFail(services, context, callbacks);
90
+ if (!bsResult.success) {
91
+ callbacks.onStepComplete?.(INFRA_STEPS.PREPARE.id, INFRA_STEPS.PREPARE.name, "error", 1, INFRA_STEP_TOTAL);
92
+ return bsResult;
77
93
  }
78
- callbacks.onCDKBootstrap?.("complete");
79
- // Deploy
94
+ callbacks.onStepComplete?.(INFRA_STEPS.PREPARE.id, INFRA_STEPS.PREPARE.name, "completed", 1, INFRA_STEP_TOTAL);
95
+ // Step 3: Deploy infrastructure
80
96
  const stackName = getOrganisationStackName(operation.type);
81
- const stepId = `${deployType}-deploy`;
82
- const stepName = `Deploying ${deployType} infrastructure`;
83
- callbacks.onStepStart?.(stepId, stepName, 0, 1);
84
- const deployResult = await services.cdkService.runCdkDeploy(context, stackName, (chunk) => callbacks.onOutput?.(chunk), (event) => callbacks.onResourceProgress?.(event), services.awsProvider);
97
+ callbacks.onStepStart?.(INFRA_STEPS.DEPLOY.id, INFRA_STEPS.DEPLOY.name, 2, INFRA_STEP_TOTAL);
98
+ const deployResult = await services.cdkService.runCdkDeploy(context, stackName, forwardOutput(callbacks), forwardResourceProgress(callbacks), services.awsProvider);
85
99
  if (!deployResult.success) {
86
- callbacks.onStepComplete?.(stepId, stepName, "error", 0, 1);
87
- const error = new Error(deployResult.error);
100
+ callbacks.onStepComplete?.(INFRA_STEPS.DEPLOY.id, INFRA_STEPS.DEPLOY.name, "error", 2, INFRA_STEP_TOTAL);
101
+ const error = new Error(maskSensitiveOutput(deployResult.error));
88
102
  callbacks.onError?.(error);
89
103
  return failure(error);
90
104
  }
91
- callbacks.onStepComplete?.(stepId, stepName, "completed", 0, 1);
105
+ callbacks.onStepComplete?.(INFRA_STEPS.DEPLOY.id, INFRA_STEPS.DEPLOY.name, "completed", 2, INFRA_STEP_TOTAL);
106
+ // Capture CloudFormation outputs (OIDC role ARN, etc.)
107
+ const outputsResult = await services.cfnService.getStackOutputs(stackName);
108
+ if (!outputsResult.success) {
109
+ callbacks.onLog?.("Failed to read stack outputs (non-critical)", "debug");
110
+ }
111
+ const outputs = collectStackOutputs(outputsResult);
112
+ // Step 4: Enable monitoring — CloudTrail + alarms are part of the
113
+ // Account stack. Signal post-deploy readiness.
114
+ callbacks.onStepStart?.(INFRA_STEPS.MONITORING.id, INFRA_STEPS.MONITORING.name, 3, INFRA_STEP_TOTAL);
115
+ callbacks.onStepComplete?.(INFRA_STEPS.MONITORING.id, INFRA_STEPS.MONITORING.name, "completed", 3, INFRA_STEP_TOTAL);
92
116
  return success({
93
117
  target: operation.target,
94
118
  deploymentType: "organisation",
119
+ outputs,
95
120
  durationMs: Date.now() - startTime
96
121
  });
97
122
  }
@@ -102,54 +127,67 @@ async function deployOrgWithCascade(params, services, operation, startTime) {
102
127
  const { callbacks, options } = params;
103
128
  const providerAccounts = params.orgConfig?.providerAccounts ?? [];
104
129
  const context = buildOrgContext(params, services, operation, "organisation");
130
+ // Compute step counts upfront so pre-deploy failures can reference them
131
+ const cascadeEnabled = options?.cascade !== false;
132
+ const cascadeAccountCount = cascadeEnabled ? providerAccounts.length : 0;
133
+ // Steps: prepare (synth+bootstrap) + org-deploy + cascade accounts
134
+ const totalSteps = 2 + cascadeAccountCount;
135
+ const { id: prepareStepId, name: prepareStepName } = INFRA_STEPS.PREPARE;
136
+ // Step 0: Prepare (synth + bootstrap)
137
+ callbacks.onStepStart?.(prepareStepId, prepareStepName, 0, totalSteps);
105
138
  // Synth
106
139
  callbacks.onLog?.("Synthesising organisation infrastructure…", "info");
107
- const synthResult = await services.cdkService.runCdkSynth(context, (chunk) => callbacks.onCdkOutput?.(chunk, "synth"));
140
+ const synthResult = await synthOrFail(services, context, callbacks, "CDK synthesis failed");
108
141
  if (!synthResult.success) {
109
- const error = new Error(`CDK synthesis failed: ${synthResult.error}`);
110
- callbacks.onError?.(error);
111
- return failure(error);
142
+ callbacks.onStepComplete?.(prepareStepId, prepareStepName, "error", 0, totalSteps);
143
+ return synthResult;
112
144
  }
113
145
  // Bootstrap org account
114
- callbacks.onCDKBootstrap?.("bootstrapping");
115
- const bootstrapResult = await services.cdkService.runCdkBootstrap(context, (chunk) => callbacks.onOutput?.(chunk));
116
- if (!bootstrapResult.success) {
117
- callbacks.onCDKBootstrap?.("failed");
118
- const error = new Error(`Bootstrap failed: ${bootstrapResult.error}`);
119
- callbacks.onError?.(error);
120
- return failure(error);
146
+ const bsResult = await bootstrapOrFail(services, context, callbacks);
147
+ if (!bsResult.success) {
148
+ callbacks.onStepComplete?.(prepareStepId, prepareStepName, "error", 0, totalSteps);
149
+ return bsResult;
121
150
  }
122
- callbacks.onCDKBootstrap?.("complete");
123
- // Deploy org infrastructure
124
- const orgStepId = "organisation-deploy";
125
- const orgStepName = "Deploying organisation infrastructure";
126
- const cascadeEnabled = options?.cascade !== false;
127
- const cascadeAccountCount = cascadeEnabled ? providerAccounts.length : 0;
128
- const totalSteps = 1 + cascadeAccountCount;
129
- callbacks.onStepStart?.(orgStepId, orgStepName, 0, totalSteps);
151
+ callbacks.onStepComplete?.(prepareStepId, prepareStepName, "completed", 0, totalSteps);
152
+ // Step 1: Deploy org infrastructure
153
+ const { id: orgStepId, name: orgStepName } = INFRA_STEPS.ORG_DEPLOY;
154
+ callbacks.onStepStart?.(orgStepId, orgStepName, 1, totalSteps);
130
155
  const orgStackName = getOrganisationStackName(ORGANISATION_TYPES.ORGANISATION);
131
- const orgResult = await services.cdkService.runCdkDeploy(context, orgStackName, (chunk) => callbacks.onOutput?.(chunk), (event) => callbacks.onResourceProgress?.(event), services.awsProvider);
156
+ const orgResult = await services.cdkService.runCdkDeploy(context, orgStackName, forwardOutput(callbacks), forwardResourceProgress(callbacks), services.awsProvider);
132
157
  if (!orgResult.success) {
133
- callbacks.onStepComplete?.(orgStepId, orgStepName, "error", 0, totalSteps);
134
- const error = new Error(orgResult.error);
158
+ callbacks.onStepComplete?.(orgStepId, orgStepName, "error", 1, totalSteps);
159
+ const error = new Error(maskSensitiveOutput(orgResult.error));
135
160
  callbacks.onError?.(error);
136
161
  return failure(error);
137
162
  }
138
- callbacks.onStepComplete?.(orgStepId, orgStepName, "completed", 0, totalSteps);
163
+ // Capture org root stack outputs (OIDC role ARN, etc.)
164
+ const orgOutputsResult = await services.cfnService.getStackOutputs(orgStackName);
165
+ if (!orgOutputsResult.success) {
166
+ callbacks.onLog?.("Failed to read org stack outputs (non-critical)", "debug");
167
+ }
168
+ const orgOutputs = collectStackOutputs(orgOutputsResult);
169
+ callbacks.onStepComplete?.(orgStepId, orgStepName, "completed", 1, totalSteps);
139
170
  // Cascade to platform + domains + member accounts
171
+ const cascadeErrors = [];
172
+ const allCascadeOutputs = [];
140
173
  if (cascadeEnabled && providerAccounts.length > 0) {
141
174
  callbacks.onCascadeStart?.();
142
- const cascadeErrors = [];
143
175
  let accountsDeployed = 0;
144
176
  let platformDeployed = false;
145
177
  let domainsDeployed = false;
146
178
  // Phase 1: Deploy platform account
147
- const platformAccount = providerAccounts.find((acc) => acc.environment === "platform");
179
+ const { platformAccount, memberAccounts } = partitionAccounts(providerAccounts);
148
180
  if (platformAccount) {
149
181
  callbacks.onCascadePhaseStart?.("platform");
150
182
  const platformResult = await deployCascadeAccount(params, services, operation, platformAccount, "platform", callbacks);
151
183
  if (platformResult.success) {
152
184
  platformDeployed = true;
185
+ if (platformResult.data.outputs) {
186
+ allCascadeOutputs.push({
187
+ accountId: platformAccount.id,
188
+ outputs: platformResult.data.outputs
189
+ });
190
+ }
153
191
  }
154
192
  else {
155
193
  cascadeErrors.push({
@@ -157,6 +195,7 @@ async function deployOrgWithCascade(params, services, operation, startTime) {
157
195
  error: platformResult.error.message
158
196
  });
159
197
  }
198
+ callbacks.onCascadePhaseComplete?.("platform");
160
199
  }
161
200
  // Phase 1.5: Read Platform stack outputs for IPAM pool IDs
162
201
  let ipamPoolIds = new Map();
@@ -168,24 +207,43 @@ async function deployOrgWithCascade(params, services, operation, startTime) {
168
207
  const domainResult = await deployDomains(params.domainProvider, callbacks);
169
208
  domainsDeployed = domainResult.domainsDeployed > 0;
170
209
  for (const err of domainResult.errors) {
171
- cascadeErrors.push({ accountId: "domains", error: err });
210
+ cascadeErrors.push({
211
+ accountId: "domains",
212
+ error: maskSensitiveOutput(err)
213
+ });
172
214
  }
173
215
  }
174
216
  // Phase 3: Deploy member accounts in parallel
175
- const memberAccounts = providerAccounts.filter((acc) => acc.environment !== "root" && acc.environment !== "platform");
176
217
  if (memberAccounts.length > 0) {
177
218
  callbacks.onCascadePhaseStart?.("accounts");
178
219
  const region = services.awsProvider.getRegion();
179
- const memberResults = await Promise.all(memberAccounts.map((account) => {
220
+ const memberSettled = await Promise.allSettled(memberAccounts.map((account) => {
180
221
  const regionSuffix = region.replace(/-/g, "");
181
222
  const ipamPoolId = ipamPoolIds.get(`${account.id}-${regionSuffix}`);
182
223
  return deployCascadeAccount(params, services, operation, account, "account", callbacks, ipamPoolId);
183
224
  }));
184
- for (let j = 0; j < memberResults.length; j++) {
185
- const result = memberResults[j];
225
+ memberSettled.forEach((settled, j) => {
186
226
  const account = memberAccounts[j];
227
+ if (!account)
228
+ return;
229
+ if (settled.status === "rejected") {
230
+ cascadeErrors.push({
231
+ accountId: account.id,
232
+ error: maskSensitiveOutput(settled.reason instanceof Error
233
+ ? settled.reason.message
234
+ : String(settled.reason))
235
+ });
236
+ return;
237
+ }
238
+ const result = settled.value;
187
239
  if (result.success) {
188
240
  accountsDeployed++;
241
+ if (result.data.outputs) {
242
+ allCascadeOutputs.push({
243
+ accountId: account.id,
244
+ outputs: result.data.outputs
245
+ });
246
+ }
189
247
  }
190
248
  else {
191
249
  cascadeErrors.push({
@@ -193,7 +251,8 @@ async function deployOrgWithCascade(params, services, operation, startTime) {
193
251
  error: result.error.message
194
252
  });
195
253
  }
196
- }
254
+ });
255
+ callbacks.onCascadePhaseComplete?.("accounts");
197
256
  }
198
257
  callbacks.onCascadeComplete?.({
199
258
  platformDeployed,
@@ -206,177 +265,20 @@ async function deployOrgWithCascade(params, services, operation, startTime) {
206
265
  const errorSummary = cascadeErrors
207
266
  .map((e) => ` ${e.accountId}: ${e.error}`)
208
267
  .join("\n");
209
- callbacks.onLog?.(`Cascade completed with ${cascadeErrors.length} failure(s):\n${errorSummary}`, "warn");
268
+ callbacks.onLog?.(maskSensitiveOutput(`Cascade completed with ${cascadeErrors.length} failure(s):\n${errorSummary}`), "warn");
210
269
  }
211
270
  }
271
+ const warnings = cascadeErrors.length > 0
272
+ ? cascadeErrors.map((e) => maskSensitiveOutput(`${e.accountId}: ${e.error}`))
273
+ : undefined;
212
274
  return success({
213
275
  target: operation.target,
214
276
  deploymentType: "organisation",
215
- durationMs: Date.now() - startTime
216
- });
217
- }
218
- /**
219
- * Deploy a single cascade account (platform or member).
220
- * Assumes the target account's role, sets env credentials, and deploys.
221
- */
222
- async function deployCascadeAccount(params, services, operation, account, deployType, callbacks, ipamPoolId) {
223
- const operationKey = `${account.name}-${account.id}`;
224
- const region = services.awsProvider.getRegion();
225
- callbacks.onCascadeAccountStart?.(operationKey, account.id, region);
226
- // Assume role in target account
227
- if (!services.awsProvider.assumeRole) {
228
- const error = new Error(`Cannot cascade to account ${account.name}: AwsProvider does not support assumeRole`);
229
- callbacks.onCascadeAccountComplete?.(operationKey, false, error.message, region);
230
- return failure(error);
231
- }
232
- const roleArn = `arn:aws:iam::${account.id}:role/fjall-deployment-role`;
233
- let assumedCredentials;
234
- try {
235
- assumedCredentials = await services.awsProvider.assumeRole(roleArn, `fjall-cascade-${account.name}`);
236
- }
237
- catch (err) {
238
- const message = err instanceof Error ? err.message : String(err);
239
- callbacks.onCascadeAccountComplete?.(operationKey, false, message, region);
240
- return failure(new Error(`Failed to assume role for ${account.name}: ${message}`));
241
- }
242
- // Build context for this account (includes IPAM pool ID if available)
243
- const accountContext = CdkContextBuilder.buildDeploymentContext({
244
- deployType,
245
- target: operation.target,
246
- path: operation.path,
247
- region,
248
- accountName: account.name,
249
- callerIdentity: { Account: account.id, Arn: "", UserId: "" },
250
- ipamPoolId,
251
- ...buildParamsContext(params)
252
- }, { verbose: params.options?.verbose }, params.orgConfig);
253
- // Create account-scoped provider for CDK monitoring
254
- const accountProvider = new SimpleAwsProvider({
255
- accessKeyId: assumedCredentials.accessKeyId,
256
- secretAccessKey: assumedCredentials.secretAccessKey,
257
- sessionToken: assumedCredentials.sessionToken,
258
- region,
259
- accountId: account.id
277
+ outputs: orgOutputs,
278
+ ...(allCascadeOutputs.length > 0
279
+ ? { cascadeOutputs: allCascadeOutputs }
280
+ : {}),
281
+ durationMs: Date.now() - startTime,
282
+ warnings
260
283
  });
261
- // Export account credentials for CDK subprocess
262
- accountProvider.exportToEnv();
263
- // Bootstrap the target account
264
- callbacks.onCascadeAccountPhaseChange?.(operationKey, "synth", region);
265
- const bootstrapResult = await services.cdkService.runCdkBootstrap(accountContext, (chunk) => callbacks.onOutput?.(chunk));
266
- if (!bootstrapResult.success) {
267
- services.awsProvider.exportToEnv();
268
- callbacks.onCascadeAccountComplete?.(operationKey, false, `Bootstrap failed: ${bootstrapResult.error}`, region);
269
- return failure(new Error(`Bootstrap failed for ${account.name}: ${bootstrapResult.error}`));
270
- }
271
- // Deploy the account stack
272
- callbacks.onCascadeAccountPhaseChange?.(operationKey, "deploy", region);
273
- const stackName = getOrganisationStackName(deployType === "platform"
274
- ? ORGANISATION_TYPES.PLATFORM
275
- : ORGANISATION_TYPES.ACCOUNT);
276
- const deployResult = await services.cdkService.runCdkDeploy(accountContext, stackName, (chunk) => callbacks.onOutput?.(chunk), (event) => callbacks.onCascadeAccountResourceProgress?.(operationKey, event, region), accountProvider);
277
- // Restore parent credentials
278
- services.awsProvider.exportToEnv();
279
- if (!deployResult.success) {
280
- callbacks.onCascadeAccountComplete?.(operationKey, false, deployResult.error, region);
281
- return failure(new Error(deployResult.error));
282
- }
283
- callbacks.onCascadeAccountComplete?.(operationKey, true, undefined, region);
284
- return success(undefined);
285
- }
286
- /**
287
- * Read Platform stack outputs to extract IPAM pool IDs for member accounts.
288
- * Output keys follow the pattern `IpamPoolId{12-digit-accountId}{regionSuffix}`.
289
- * Returns a map keyed by `{accountId}-{regionSuffix}` → pool ID.
290
- * Non-fatal: returns empty map on any error.
291
- */
292
- async function readPlatformIpamPoolIds(services, platformAccount, callbacks) {
293
- const poolIds = new Map();
294
- // Assume role in platform account to read its stack outputs
295
- if (!services.awsProvider.assumeRole) {
296
- logger.debug("organisationDeploy", "Cannot read Platform outputs: assumeRole not available");
297
- return poolIds;
298
- }
299
- const region = services.awsProvider.getRegion();
300
- const roleArn = `arn:aws:iam::${platformAccount.id}:role/fjall-deployment-role`;
301
- let assumedCredentials;
302
- try {
303
- assumedCredentials = await services.awsProvider.assumeRole(roleArn, `fjall-ipam-read-${platformAccount.name}`);
304
- }
305
- catch (err) {
306
- const message = err instanceof Error ? err.message : String(err);
307
- logger.debug("organisationDeploy", `Cannot read Platform outputs: AssumeRole failed: ${message}`);
308
- return poolIds;
309
- }
310
- // Create a temporary provider scoped to the platform account
311
- const platformProvider = new SimpleAwsProvider({
312
- accessKeyId: assumedCredentials.accessKeyId,
313
- secretAccessKey: assumedCredentials.secretAccessKey,
314
- sessionToken: assumedCredentials.sessionToken,
315
- region,
316
- accountId: platformAccount.id
317
- });
318
- const platformCfn = new CloudFormationService(platformProvider);
319
- const outputsResult = await platformCfn.getStackOutputs("Platform");
320
- if (!outputsResult.success) {
321
- logger.debug("organisationDeploy", `Failed to read Platform stack outputs: ${outputsResult.error.message}`);
322
- return poolIds;
323
- }
324
- const ipamPattern = /^IpamPoolId(\d{12})(\w+)$/;
325
- for (const output of outputsResult.data) {
326
- const match = output.OutputKey?.match(ipamPattern);
327
- if (match && output.OutputValue) {
328
- const key = `${match[1]}-${match[2]}`;
329
- poolIds.set(key, output.OutputValue);
330
- }
331
- }
332
- if (poolIds.size > 0) {
333
- callbacks.onLog?.(`Read ${poolIds.size} IPAM pool ID(s) from Platform stack`, "info");
334
- }
335
- return poolIds;
336
- }
337
- /**
338
- * Deploy configured domains: apex domains sequentially, then delegated
339
- * domains in parallel. Delegates to the caller-provided DomainDeployProvider.
340
- */
341
- async function deployDomains(provider, callbacks) {
342
- const domains = provider.getDomains();
343
- if (domains.length === 0) {
344
- return { domainsDeployed: 0, errors: [] };
345
- }
346
- callbacks.onCascadePhaseStart?.("domains");
347
- const apexDomains = domains.filter((d) => d.type === "apex");
348
- const delegatedDomains = domains.filter((d) => d.type === "delegated");
349
- let domainsDeployed = 0;
350
- const errors = [];
351
- // Phase A: Apex domains (sequential — delegation roles must exist first)
352
- for (const domain of apexDomains) {
353
- const result = await provider.deployDomain(domain.name, callbacks);
354
- if (result.success) {
355
- domainsDeployed++;
356
- }
357
- else {
358
- errors.push(`${domain.name}: ${result.error.message}`);
359
- }
360
- }
361
- // Phase B: Delegated subdomains (parallel — independent of each other)
362
- if (delegatedDomains.length > 0) {
363
- const subdomainResults = await Promise.allSettled(delegatedDomains.map(async (domain) => {
364
- const result = await provider.deployDomain(domain.name, callbacks);
365
- if (result.success) {
366
- domainsDeployed++;
367
- }
368
- else {
369
- errors.push(`${domain.name}: ${result.error.message}`);
370
- }
371
- }));
372
- for (const result of subdomainResults) {
373
- if (result.status === "rejected") {
374
- const message = result.reason instanceof Error
375
- ? result.reason.message
376
- : String(result.reason);
377
- errors.push(`Subdomain deploy error: ${message}`);
378
- }
379
- }
380
- }
381
- return { domainsDeployed, errors };
382
284
  }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Organisation destroy orchestration.
3
+ *
4
+ * Receives pre-authenticated credentials and destroys all organisation
5
+ * infrastructure in cascade order:
6
+ * 1. Member accounts across ALL regions in parallel
7
+ * 2. Platform account in primary region
8
+ * 3. Organisation root stack
9
+ *
10
+ * Auth, verification, and interactive prompts are the caller's job
11
+ * (per engine/consumer boundary).
12
+ */
13
+ import { type Result } from "@fjall/generator";
14
+ import type { DestroyParams, DestroyResult } from "../types/params.js";
15
+ import type { OrganisationOperation } from "../types/operations.js";
16
+ import type { DeployServices } from "./serviceFactory.js";
17
+ /**
18
+ * Destroy organisation infrastructure with cascade.
19
+ *
20
+ * The cascade ordering is: members (parallel, multi-region) -> platform -> org stack.
21
+ * If any member or platform destruction fails, the org stack is NOT destroyed
22
+ * (to avoid orphaning resources).
23
+ */
24
+ export declare function destroyOrganisation(params: DestroyParams, services: DeployServices, operation: OrganisationOperation): Promise<Result<DestroyResult>>;