@fjall/deploy-core 0.89.2

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 (154) hide show
  1. package/LICENSE +21 -0
  2. package/dist/src/aws/AwsProvider.d.ts +39 -0
  3. package/dist/src/aws/AwsProvider.js +1 -0
  4. package/dist/src/aws/SimpleAwsProvider.d.ts +22 -0
  5. package/dist/src/aws/SimpleAwsProvider.js +73 -0
  6. package/dist/src/aws/index.d.ts +4 -0
  7. package/dist/src/aws/index.js +3 -0
  8. package/dist/src/aws/organisations/accounts.d.ts +21 -0
  9. package/dist/src/aws/organisations/accounts.js +99 -0
  10. package/dist/src/aws/organisations/backup.d.ts +12 -0
  11. package/dist/src/aws/organisations/backup.js +28 -0
  12. package/dist/src/aws/organisations/costAllocation.d.ts +12 -0
  13. package/dist/src/aws/organisations/costAllocation.js +26 -0
  14. package/dist/src/aws/organisations/identityCentre.d.ts +8 -0
  15. package/dist/src/aws/organisations/identityCentre.js +19 -0
  16. package/dist/src/aws/organisations/index.d.ts +16 -0
  17. package/dist/src/aws/organisations/index.js +12 -0
  18. package/dist/src/aws/organisations/ipam.d.ts +7 -0
  19. package/dist/src/aws/organisations/ipam.js +18 -0
  20. package/dist/src/aws/organisations/organisation.d.ts +12 -0
  21. package/dist/src/aws/organisations/organisation.js +94 -0
  22. package/dist/src/aws/organisations/organisationalUnits.d.ts +19 -0
  23. package/dist/src/aws/organisations/organisationalUnits.js +125 -0
  24. package/dist/src/aws/organisations/policies.d.ts +7 -0
  25. package/dist/src/aws/organisations/policies.js +36 -0
  26. package/dist/src/aws/organisations/ram.d.ts +7 -0
  27. package/dist/src/aws/organisations/ram.js +15 -0
  28. package/dist/src/aws/organisations/serviceAccess.d.ts +7 -0
  29. package/dist/src/aws/organisations/serviceAccess.js +38 -0
  30. package/dist/src/aws/organisations/trustedAccess.d.ts +7 -0
  31. package/dist/src/aws/organisations/trustedAccess.js +15 -0
  32. package/dist/src/aws/organisations/types.d.ts +29 -0
  33. package/dist/src/aws/organisations/types.js +16 -0
  34. package/dist/src/aws/utils/CloudFormationFailureAnalyser.d.ts +32 -0
  35. package/dist/src/aws/utils/CloudFormationFailureAnalyser.js +228 -0
  36. package/dist/src/aws/utils/cloudformationEvents.d.ts +98 -0
  37. package/dist/src/aws/utils/cloudformationEvents.js +596 -0
  38. package/dist/src/aws/utils/errors.d.ts +26 -0
  39. package/dist/src/aws/utils/errors.js +59 -0
  40. package/dist/src/aws/utils/regions.d.ts +1 -0
  41. package/dist/src/aws/utils/regions.js +1 -0
  42. package/dist/src/aws/utils/stackStatus.d.ts +23 -0
  43. package/dist/src/aws/utils/stackStatus.js +90 -0
  44. package/dist/src/index.d.ts +35 -0
  45. package/dist/src/index.js +45 -0
  46. package/dist/src/orchestration/applicationDeploy.d.ts +11 -0
  47. package/dist/src/orchestration/applicationDeploy.js +327 -0
  48. package/dist/src/orchestration/contextHelpers.d.ts +9 -0
  49. package/dist/src/orchestration/contextHelpers.js +14 -0
  50. package/dist/src/orchestration/deploy.d.ts +10 -0
  51. package/dist/src/orchestration/deploy.js +42 -0
  52. package/dist/src/orchestration/detectionPipeline.d.ts +23 -0
  53. package/dist/src/orchestration/detectionPipeline.js +65 -0
  54. package/dist/src/orchestration/dockerInterface.d.ts +56 -0
  55. package/dist/src/orchestration/dockerInterface.js +1 -0
  56. package/dist/src/orchestration/domainInterface.d.ts +37 -0
  57. package/dist/src/orchestration/domainInterface.js +1 -0
  58. package/dist/src/orchestration/index.d.ts +8 -0
  59. package/dist/src/orchestration/index.js +3 -0
  60. package/dist/src/orchestration/organisationDeploy.d.ts +16 -0
  61. package/dist/src/orchestration/organisationDeploy.js +382 -0
  62. package/dist/src/orchestration/organisationSetup.d.ts +42 -0
  63. package/dist/src/orchestration/organisationSetup.js +227 -0
  64. package/dist/src/orchestration/resolveOperation.d.ts +10 -0
  65. package/dist/src/orchestration/resolveOperation.js +53 -0
  66. package/dist/src/orchestration/serviceFactory.d.ts +15 -0
  67. package/dist/src/orchestration/serviceFactory.js +16 -0
  68. package/dist/src/services/application/ApplicationStackService.d.ts +93 -0
  69. package/dist/src/services/application/ApplicationStackService.js +436 -0
  70. package/dist/src/services/application/index.d.ts +1 -0
  71. package/dist/src/services/application/index.js +1 -0
  72. package/dist/src/services/infrastructure/CdkArgumentBuilder.d.ts +12 -0
  73. package/dist/src/services/infrastructure/CdkArgumentBuilder.js +67 -0
  74. package/dist/src/services/infrastructure/CdkCommandRunner.d.ts +30 -0
  75. package/dist/src/services/infrastructure/CdkCommandRunner.js +241 -0
  76. package/dist/src/services/infrastructure/CdkErrorFormatter.d.ts +4 -0
  77. package/dist/src/services/infrastructure/CdkErrorFormatter.js +194 -0
  78. package/dist/src/services/infrastructure/CdkEventMonitoring.d.ts +19 -0
  79. package/dist/src/services/infrastructure/CdkEventMonitoring.js +41 -0
  80. package/dist/src/services/infrastructure/CdkOutputAnalyser.d.ts +43 -0
  81. package/dist/src/services/infrastructure/CdkOutputAnalyser.js +125 -0
  82. package/dist/src/services/infrastructure/CdkOutputParser.d.ts +8 -0
  83. package/dist/src/services/infrastructure/CdkOutputParser.js +33 -0
  84. package/dist/src/services/infrastructure/CdkProcessManager.d.ts +20 -0
  85. package/dist/src/services/infrastructure/CdkProcessManager.js +244 -0
  86. package/dist/src/services/infrastructure/CdkService.d.ts +71 -0
  87. package/dist/src/services/infrastructure/CdkService.js +254 -0
  88. package/dist/src/services/infrastructure/CloudFormationService.d.ts +79 -0
  89. package/dist/src/services/infrastructure/CloudFormationService.js +249 -0
  90. package/dist/src/services/infrastructure/index.d.ts +8 -0
  91. package/dist/src/services/infrastructure/index.js +7 -0
  92. package/dist/src/services/supporting/CdkContextBuilder.d.ts +49 -0
  93. package/dist/src/services/supporting/CdkContextBuilder.js +44 -0
  94. package/dist/src/services/supporting/TemplateHashService.d.ts +67 -0
  95. package/dist/src/services/supporting/TemplateHashService.js +152 -0
  96. package/dist/src/services/supporting/helpers.d.ts +46 -0
  97. package/dist/src/services/supporting/helpers.js +81 -0
  98. package/dist/src/services/supporting/index.d.ts +3 -0
  99. package/dist/src/services/supporting/index.js +3 -0
  100. package/dist/src/types/FjallState.d.ts +50 -0
  101. package/dist/src/types/FjallState.js +118 -0
  102. package/dist/src/types/ProgressEvent.d.ts +35 -0
  103. package/dist/src/types/ProgressEvent.js +48 -0
  104. package/dist/src/types/apiClient.d.ts +34 -0
  105. package/dist/src/types/apiClient.js +1 -0
  106. package/dist/src/types/application/ApplicationServiceTypes.d.ts +56 -0
  107. package/dist/src/types/application/ApplicationServiceTypes.js +30 -0
  108. package/dist/src/types/application/index.d.ts +1 -0
  109. package/dist/src/types/application/index.js +1 -0
  110. package/dist/src/types/callbacks.d.ts +36 -0
  111. package/dist/src/types/callbacks.js +1 -0
  112. package/dist/src/types/constants.d.ts +6 -0
  113. package/dist/src/types/constants.js +6 -0
  114. package/dist/src/types/credentials.d.ts +30 -0
  115. package/dist/src/types/credentials.js +1 -0
  116. package/dist/src/types/deployment/DeploymentServiceTypes.d.ts +23 -0
  117. package/dist/src/types/deployment/DeploymentServiceTypes.js +1 -0
  118. package/dist/src/types/deployment/DeploymentTypes.d.ts +29 -0
  119. package/dist/src/types/deployment/DeploymentTypes.js +1 -0
  120. package/dist/src/types/deployment/cloudformation.d.ts +14 -0
  121. package/dist/src/types/deployment/cloudformation.js +1 -0
  122. package/dist/src/types/deployment/index.d.ts +5 -0
  123. package/dist/src/types/deployment/index.js +1 -0
  124. package/dist/src/types/deployment/parallel.d.ts +46 -0
  125. package/dist/src/types/deployment/parallel.js +10 -0
  126. package/dist/src/types/errors/CdkError.d.ts +14 -0
  127. package/dist/src/types/errors/CdkError.js +20 -0
  128. package/dist/src/types/errors/ServiceError.d.ts +86 -0
  129. package/dist/src/types/errors/ServiceError.js +119 -0
  130. package/dist/src/types/events.d.ts +40 -0
  131. package/dist/src/types/events.js +5 -0
  132. package/dist/src/types/index.d.ts +20 -0
  133. package/dist/src/types/index.js +9 -0
  134. package/dist/src/types/operations.d.ts +193 -0
  135. package/dist/src/types/operations.js +285 -0
  136. package/dist/src/types/orgConfig.d.ts +28 -0
  137. package/dist/src/types/orgConfig.js +11 -0
  138. package/dist/src/types/params.d.ts +74 -0
  139. package/dist/src/types/params.js +1 -0
  140. package/dist/src/types/patternDetection.d.ts +43 -0
  141. package/dist/src/types/patternDetection.js +92 -0
  142. package/dist/src/types/validation.d.ts +12 -0
  143. package/dist/src/types/validation.js +1 -0
  144. package/dist/src/util/fsHelpers.d.ts +4 -0
  145. package/dist/src/util/fsHelpers.js +16 -0
  146. package/dist/src/util/index.d.ts +3 -0
  147. package/dist/src/util/index.js +3 -0
  148. package/dist/src/util/securityHelpers.d.ts +31 -0
  149. package/dist/src/util/securityHelpers.js +124 -0
  150. package/dist/src/util/singleton.d.ts +2 -0
  151. package/dist/src/util/singleton.js +9 -0
  152. package/dist/src/util/sleep.d.ts +4 -0
  153. package/dist/src/util/sleep.js +4 -0
  154. package/package.json +42 -0
@@ -0,0 +1,79 @@
1
+ import { CloudFormationClient } from "@aws-sdk/client-cloudformation";
2
+ import type { AwsProvider } from "../../aws/AwsProvider.js";
3
+ import { type CloudFormationStackStatus, type CloudFormationOutput } from "../../aws/utils/stackStatus.js";
4
+ import { type ProgressCallbacks } from "../../types/ProgressEvent.js";
5
+ import { type Result } from "@fjall/generator";
6
+ import { BaseServiceError } from "../../types/errors/ServiceError.js";
7
+ /**
8
+ * CloudFormation-specific error
9
+ */
10
+ export declare class CloudFormationError extends BaseServiceError {
11
+ readonly errorType: "stack_not_found" | "stack_in_progress" | "stack_failed" | "invalid_state" | "throttled" | "network_error" | "auth_error" | "timeout" | "unknown";
12
+ readonly stackName?: string | undefined;
13
+ readonly stackStatus?: string | undefined;
14
+ constructor(message: string, errorType: "stack_not_found" | "stack_in_progress" | "stack_failed" | "invalid_state" | "throttled" | "network_error" | "auth_error" | "timeout" | "unknown", stackName?: string | undefined, stackStatus?: string | undefined, details?: unknown, recoverable?: boolean);
15
+ }
16
+ /**
17
+ * Callbacks for CloudFormation operations
18
+ */
19
+ export interface CloudFormationCallbacks extends ProgressCallbacks {
20
+ onStackCheck?: (stackName: string) => void;
21
+ onStackFound?: (stackName: string, status: string) => void;
22
+ onStackNotFound?: (stackName: string) => void;
23
+ onOutputsRetrieved?: (stackName: string, outputCount: number) => void;
24
+ onExportsRetrieved?: (exportCount: number) => void;
25
+ }
26
+ /**
27
+ * CloudFormation service using the AwsProvider interface.
28
+ *
29
+ * Unlike the CLI version which lazily initialises AwsContext,
30
+ * deploy-core receives a pre-configured AwsProvider at construction.
31
+ */
32
+ export declare class CloudFormationService {
33
+ private aws;
34
+ constructor(aws: AwsProvider);
35
+ /**
36
+ * Classify an AWS SDK error into a typed CloudFormationError.
37
+ */
38
+ private classifyAwsError;
39
+ /**
40
+ * Get stack outputs for services that need them
41
+ */
42
+ getStackOutputs(stackName: string, callbacks?: CloudFormationCallbacks): Promise<Result<CloudFormationOutput[], CloudFormationError>>;
43
+ /**
44
+ * Get stack status directly
45
+ */
46
+ getStackStatus(stackName: string, callbacks?: CloudFormationCallbacks): Promise<Result<CloudFormationStackStatus | null, CloudFormationError>>;
47
+ /**
48
+ * Internal helper to list all CloudFormation exports with pagination.
49
+ */
50
+ private listAllExports;
51
+ /**
52
+ * Look up specific CloudFormation exports by exact name.
53
+ */
54
+ getExportsByNames(names: string[]): Promise<Result<Map<string, string>, CloudFormationError>>;
55
+ /**
56
+ * List all CloudFormation exports
57
+ */
58
+ listExports(callbacks?: CloudFormationCallbacks): Promise<Result<Array<{
59
+ Name: string;
60
+ Value: string;
61
+ }>, CloudFormationError>>;
62
+ /**
63
+ * Delete a CloudFormation stack directly via the API.
64
+ * Unlike CDK destroy, this works on stacks in DELETE_FAILED state.
65
+ */
66
+ deleteStack(stackName: string): Promise<Result<void, CloudFormationError>>;
67
+ /**
68
+ * Check whether a CloudFormation stack exists and is in a deployable state.
69
+ */
70
+ stackExists(stackName: string, client?: CloudFormationClient): Promise<boolean>;
71
+ /**
72
+ * Poll stack status until deletion completes, fails, or times out.
73
+ */
74
+ waitForDeleteComplete(stackName: string, options?: {
75
+ timeoutMs?: number;
76
+ pollIntervalMs?: number;
77
+ onProgress?: (message: string) => void;
78
+ }): Promise<Result<void, CloudFormationError>>;
79
+ }
@@ -0,0 +1,249 @@
1
+ import { CloudFormationClient, DeleteStackCommand, DescribeStacksCommand, ListExportsCommand } from "@aws-sdk/client-cloudformation";
2
+ import { stackStatusMap } from "../../aws/utils/stackStatus.js";
3
+ import { success, failure } from "@fjall/generator";
4
+ import { BaseServiceError } from "../../types/errors/ServiceError.js";
5
+ import { getErrorMessage, logger } from "@fjall/util";
6
+ import { sleep } from "../../util/sleep.js";
7
+ import { STACK_NOT_FOUND_PATTERN } from "../../aws/utils/cloudformationEvents.js";
8
+ /**
9
+ * CloudFormation-specific error
10
+ */
11
+ export class CloudFormationError extends BaseServiceError {
12
+ errorType;
13
+ stackName;
14
+ stackStatus;
15
+ constructor(message, errorType, stackName, stackStatus, details, recoverable = false) {
16
+ super(`CFN_${errorType.toUpperCase()}`, message, details, recoverable);
17
+ this.errorType = errorType;
18
+ this.stackName = stackName;
19
+ this.stackStatus = stackStatus;
20
+ }
21
+ }
22
+ /**
23
+ * CloudFormation service using the AwsProvider interface.
24
+ *
25
+ * Unlike the CLI version which lazily initialises AwsContext,
26
+ * deploy-core receives a pre-configured AwsProvider at construction.
27
+ */
28
+ export class CloudFormationService {
29
+ aws;
30
+ constructor(aws) {
31
+ this.aws = aws;
32
+ }
33
+ /**
34
+ * Classify an AWS SDK error into a typed CloudFormationError.
35
+ */
36
+ classifyAwsError(error, fallbackMessage, stackName) {
37
+ const errorCode = error instanceof Error && "code" in error
38
+ ? String(error.code)
39
+ : undefined;
40
+ const errorMessage = getErrorMessage(error);
41
+ if (errorCode === "CredentialsError" || errorCode === "UnauthorizedError") {
42
+ return new CloudFormationError(`AWS credentials error: ${errorMessage}`, "auth_error", stackName, undefined, error, false);
43
+ }
44
+ if (errorCode === "Throttling" ||
45
+ errorCode === "TooManyRequestsException") {
46
+ return new CloudFormationError(`AWS rate limit exceeded: ${errorMessage}`, "throttled", stackName, undefined, error, true);
47
+ }
48
+ if (errorCode === "NetworkingError" || errorCode === "ENOTFOUND") {
49
+ return new CloudFormationError(`Network error: ${errorMessage}`, "network_error", stackName, undefined, error, true);
50
+ }
51
+ return new CloudFormationError(`${fallbackMessage}: ${errorMessage}`, "unknown", stackName, undefined, error);
52
+ }
53
+ /**
54
+ * Get stack outputs for services that need them
55
+ */
56
+ async getStackOutputs(stackName, callbacks) {
57
+ callbacks?.onStackCheck?.(stackName);
58
+ const client = this.aws.getClient(CloudFormationClient);
59
+ const command = new DescribeStacksCommand({ StackName: stackName });
60
+ try {
61
+ const response = await client.send(command);
62
+ const stack = response.Stacks?.[0];
63
+ if (!stack?.Outputs) {
64
+ return success([]);
65
+ }
66
+ const outputs = stack.Outputs.map((output) => ({
67
+ OutputKey: output.OutputKey,
68
+ OutputValue: output.OutputValue,
69
+ ExportName: output.ExportName
70
+ }));
71
+ callbacks?.onOutputsRetrieved?.(stackName, outputs.length);
72
+ return success(outputs);
73
+ }
74
+ catch (error) {
75
+ if (error instanceof Error &&
76
+ error.name === "ValidationError" &&
77
+ error.message?.includes(STACK_NOT_FOUND_PATTERN)) {
78
+ callbacks?.onStackNotFound?.(stackName);
79
+ return success([]);
80
+ }
81
+ return failure(new CloudFormationError(`Failed to get outputs for stack ${stackName}: ${getErrorMessage(error)}`, "unknown", stackName, undefined, error));
82
+ }
83
+ }
84
+ /**
85
+ * Get stack status directly
86
+ */
87
+ async getStackStatus(stackName, callbacks) {
88
+ callbacks?.onStackCheck?.(stackName);
89
+ const client = this.aws.getClient(CloudFormationClient);
90
+ const command = new DescribeStacksCommand({ StackName: stackName });
91
+ try {
92
+ const response = await client.send(command);
93
+ const stack = response.Stacks?.[0];
94
+ if (!stack) {
95
+ return success({
96
+ status: "DOES_NOT_EXIST",
97
+ safeToRedeploy: "Yes",
98
+ description: "Stack does not exist yet"
99
+ });
100
+ }
101
+ const status = stack.StackStatus || "UNKNOWN";
102
+ const statusInfo = stackStatusMap[status] || stackStatusMap["UNKNOWN"];
103
+ callbacks?.onStackFound?.(stackName, status);
104
+ return success({
105
+ status,
106
+ safeToRedeploy: statusInfo.safeToRedeploy,
107
+ description: statusInfo.description,
108
+ statusReason: stack.StackStatusReason
109
+ });
110
+ }
111
+ catch (error) {
112
+ if (error instanceof Error &&
113
+ error.name === "ValidationError" &&
114
+ error.message?.includes(STACK_NOT_FOUND_PATTERN)) {
115
+ return success({
116
+ status: "DOES_NOT_EXIST",
117
+ safeToRedeploy: "Yes",
118
+ description: "Stack does not exist yet"
119
+ });
120
+ }
121
+ return failure(this.classifyAwsError(error, `Failed to get stack status for ${stackName}`, stackName));
122
+ }
123
+ }
124
+ /**
125
+ * Internal helper to list all CloudFormation exports with pagination.
126
+ */
127
+ async listAllExports(earlyExit) {
128
+ const client = this.aws.getClient(CloudFormationClient);
129
+ const allExports = [];
130
+ try {
131
+ let nextToken;
132
+ do {
133
+ const command = new ListExportsCommand({ NextToken: nextToken });
134
+ const response = await client.send(command);
135
+ const pageExports = response.Exports || [];
136
+ for (const exp of pageExports) {
137
+ if (exp.Name && exp.Value) {
138
+ allExports.push({ Name: exp.Name, Value: exp.Value });
139
+ }
140
+ }
141
+ if (earlyExit?.(pageExports)) {
142
+ break;
143
+ }
144
+ nextToken = response.NextToken;
145
+ } while (nextToken);
146
+ return success(allExports);
147
+ }
148
+ catch (error) {
149
+ return failure(new CloudFormationError(`Failed to list exports: ${getErrorMessage(error)}`, "unknown", undefined, undefined, error, false));
150
+ }
151
+ }
152
+ /**
153
+ * Look up specific CloudFormation exports by exact name.
154
+ */
155
+ async getExportsByNames(names) {
156
+ if (names.length === 0) {
157
+ return success(new Map());
158
+ }
159
+ const nameSet = new Set(names);
160
+ const found = new Map();
161
+ const result = await this.listAllExports((pageExports) => {
162
+ for (const exp of pageExports) {
163
+ if (exp.Name && nameSet.has(exp.Name) && exp.Value) {
164
+ found.set(exp.Name, exp.Value);
165
+ }
166
+ }
167
+ return found.size >= nameSet.size;
168
+ });
169
+ if (!result.success) {
170
+ return failure(result.error);
171
+ }
172
+ return success(found);
173
+ }
174
+ /**
175
+ * List all CloudFormation exports
176
+ */
177
+ async listExports(callbacks) {
178
+ const result = await this.listAllExports();
179
+ if (result.success) {
180
+ callbacks?.onExportsRetrieved?.(result.data.length);
181
+ }
182
+ return result;
183
+ }
184
+ /**
185
+ * Delete a CloudFormation stack directly via the API.
186
+ * Unlike CDK destroy, this works on stacks in DELETE_FAILED state.
187
+ */
188
+ async deleteStack(stackName) {
189
+ const client = this.aws.getClient(CloudFormationClient);
190
+ try {
191
+ await client.send(new DeleteStackCommand({ StackName: stackName }));
192
+ return success(undefined);
193
+ }
194
+ catch (error) {
195
+ return failure(this.classifyAwsError(error, `Failed to delete stack ${stackName}`, stackName));
196
+ }
197
+ }
198
+ /**
199
+ * Check whether a CloudFormation stack exists and is in a deployable state.
200
+ */
201
+ async stackExists(stackName, client) {
202
+ const cfnClient = client ?? this.aws.getClient(CloudFormationClient);
203
+ try {
204
+ const response = await cfnClient.send(new DescribeStacksCommand({ StackName: stackName }));
205
+ const status = response.Stacks?.[0]?.StackStatus;
206
+ return !!status && status !== "REVIEW_IN_PROGRESS";
207
+ }
208
+ catch (error) {
209
+ if (error instanceof Error &&
210
+ error.message?.includes(STACK_NOT_FOUND_PATTERN)) {
211
+ return false;
212
+ }
213
+ logger.debug("CloudFormationService", "Error checking stack existence, assuming exists", {
214
+ stackName,
215
+ error: getErrorMessage(error)
216
+ });
217
+ return true;
218
+ }
219
+ }
220
+ /**
221
+ * Poll stack status until deletion completes, fails, or times out.
222
+ */
223
+ async waitForDeleteComplete(stackName, options) {
224
+ const timeoutMs = options?.timeoutMs ?? 10 * 60 * 1000;
225
+ const pollIntervalMs = options?.pollIntervalMs ?? 5000;
226
+ const start = Date.now();
227
+ while (Date.now() - start < timeoutMs) {
228
+ const statusResult = await this.getStackStatus(stackName);
229
+ if (!statusResult.success) {
230
+ if (statusResult.error.recoverable) {
231
+ options?.onProgress?.(`Transient error polling stack ${stackName}, retrying: ${statusResult.error.message}`);
232
+ await sleep(pollIntervalMs);
233
+ continue;
234
+ }
235
+ return failure(statusResult.error);
236
+ }
237
+ const status = statusResult.data?.status;
238
+ if (status === "DELETE_COMPLETE" || status === "DOES_NOT_EXIST") {
239
+ return success(undefined);
240
+ }
241
+ if (status === "DELETE_FAILED") {
242
+ return failure(new CloudFormationError(`Stack ${stackName} deletion failed: ${statusResult.data?.statusReason || "unknown reason"}`, "stack_failed", stackName, status, undefined, false));
243
+ }
244
+ options?.onProgress?.(`Stack ${stackName} status: ${status ?? "unknown"}`);
245
+ await sleep(pollIntervalMs);
246
+ }
247
+ return failure(new CloudFormationError(`Timed out waiting for stack ${stackName} deletion after ${Math.round(timeoutMs / 1000)}s`, "timeout", stackName, undefined, undefined, true));
248
+ }
249
+ }
@@ -0,0 +1,8 @@
1
+ export { CdkService } from "./CdkService.js";
2
+ export type { CdkContext, CdkOptions, CdkOutput, CheckDifferencesResult, CdkServiceOptions } from "./CdkService.js";
3
+ export { CdkArgumentBuilder } from "./CdkArgumentBuilder.js";
4
+ export { CdkProcessManager } from "./CdkProcessManager.js";
5
+ export { CdkEventMonitor, startStackMonitoring, DEFAULT_DEPLOY_TIMEOUT_MS } from "./CdkEventMonitoring.js";
6
+ export { isCdkError, formatInfrastructureError, getStructuralHint, getSourceContext } from "./CdkErrorFormatter.js";
7
+ export { hasCdkDifferences, parseDiffOutput, type DiffDetails } from "./CdkOutputParser.js";
8
+ export { CloudFormationService, CloudFormationError, type CloudFormationCallbacks } from "./CloudFormationService.js";
@@ -0,0 +1,7 @@
1
+ export { CdkService } from "./CdkService.js";
2
+ export { CdkArgumentBuilder } from "./CdkArgumentBuilder.js";
3
+ export { CdkProcessManager } from "./CdkProcessManager.js";
4
+ export { CdkEventMonitor, startStackMonitoring, DEFAULT_DEPLOY_TIMEOUT_MS } from "./CdkEventMonitoring.js";
5
+ export { isCdkError, formatInfrastructureError, getStructuralHint, getSourceContext } from "./CdkErrorFormatter.js";
6
+ export { hasCdkDifferences, parseDiffOutput } from "./CdkOutputParser.js";
7
+ export { CloudFormationService, CloudFormationError } from "./CloudFormationService.js";
@@ -0,0 +1,49 @@
1
+ import type { DeploymentContext } from "../../types/deployment/DeploymentTypes.js";
2
+ import type { CallerIdentity } from "../../types/deployment/DeploymentServiceTypes.js";
3
+ import type { StackOutputsRecord } from "../../types/deployment/cloudformation.js";
4
+ import type { OrgConfig } from "../../types/orgConfig.js";
5
+ /**
6
+ * Builder for creating standardised CDK deployment contexts.
7
+ * Ensures all required fields are present with proper defaults.
8
+ *
9
+ * Unlike the CLI version, this uses OrgConfig (injected) rather than
10
+ * Config.loadConfig() (file-based) for region resolution.
11
+ */
12
+ export declare class CdkContextBuilder {
13
+ /**
14
+ * Build a standardised deployment context for CDK operations.
15
+ * Ensures all required fields are present with proper defaults.
16
+ */
17
+ static buildDeploymentContext(input: {
18
+ deployType: "application" | "organisation" | "platform" | "account";
19
+ target: string;
20
+ path: string;
21
+ callerIdentity?: CallerIdentity;
22
+ region?: string;
23
+ stackOutputs?: StackOutputsRecord;
24
+ isManagedAccount?: boolean;
25
+ accountName?: string;
26
+ logPath?: string;
27
+ imageVersion?: string;
28
+ orgId?: string;
29
+ rootId?: string;
30
+ managementAccountId?: string;
31
+ ipamPoolId?: string;
32
+ fjallOrgId?: string;
33
+ orgConfig?: string;
34
+ }, options: {
35
+ verbose?: boolean;
36
+ infraOnly?: boolean;
37
+ }, orgConfig?: OrgConfig): DeploymentContext;
38
+ /**
39
+ * Build context from an existing context with updated options
40
+ */
41
+ static updateContext(existingContext: DeploymentContext, updates: Partial<{
42
+ options: {
43
+ verbose?: boolean;
44
+ infraOnly?: boolean;
45
+ };
46
+ stackOutputs: StackOutputsRecord;
47
+ region: string;
48
+ }>): DeploymentContext;
49
+ }
@@ -0,0 +1,44 @@
1
+ import { DEFAULT_REGION } from "@fjall/generator";
2
+ /**
3
+ * Builder for creating standardised CDK deployment contexts.
4
+ * Ensures all required fields are present with proper defaults.
5
+ *
6
+ * Unlike the CLI version, this uses OrgConfig (injected) rather than
7
+ * Config.loadConfig() (file-based) for region resolution.
8
+ */
9
+ export class CdkContextBuilder {
10
+ /**
11
+ * Build a standardised deployment context for CDK operations.
12
+ * Ensures all required fields are present with proper defaults.
13
+ */
14
+ static buildDeploymentContext(input, options, orgConfig) {
15
+ return {
16
+ deployType: input.deployType,
17
+ target: input.target,
18
+ path: input.path,
19
+ options: options,
20
+ stackOutputs: input.stackOutputs || {},
21
+ callerIdentity: input.callerIdentity,
22
+ region: input.region || orgConfig?.primaryRegion || DEFAULT_REGION,
23
+ isManagedAccount: input.isManagedAccount,
24
+ accountName: input.accountName,
25
+ logPath: input.logPath,
26
+ imageVersion: input.imageVersion,
27
+ orgId: input.orgId,
28
+ rootId: input.rootId,
29
+ managementAccountId: input.managementAccountId,
30
+ ipamPoolId: input.ipamPoolId,
31
+ fjallOrgId: input.fjallOrgId,
32
+ orgConfig: input.orgConfig
33
+ };
34
+ }
35
+ /**
36
+ * Build context from an existing context with updated options
37
+ */
38
+ static updateContext(existingContext, updates) {
39
+ return {
40
+ ...existingContext,
41
+ ...updates
42
+ };
43
+ }
44
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * TemplateHashService - Hash-based change detection for CDK templates
3
+ *
4
+ * Replaces slow CDK diff with fast local hash comparison.
5
+ * Uses SHA-256 hashing of synthesised templates.
6
+ */
7
+ import { type Result } from "@fjall/generator";
8
+ import { BaseServiceError } from "../../types/errors/ServiceError.js";
9
+ /**
10
+ * TemplateHash-specific error types
11
+ */
12
+ export declare class TemplateHashError extends BaseServiceError {
13
+ readonly errorType: "read_failed" | "hash_failed" | "state_write_failed" | "cdk_out_not_found";
14
+ constructor(message: string, errorType: "read_failed" | "hash_failed" | "state_write_failed" | "cdk_out_not_found", details?: unknown, recoverable?: boolean);
15
+ }
16
+ /**
17
+ * Result of comparing template hashes with state
18
+ */
19
+ export interface TemplateComparisonResult {
20
+ /** Map of stack name to whether it has changes (true = changed or new) */
21
+ stackChanges: Map<string, boolean>;
22
+ /** Current hashes for all stacks */
23
+ currentHashes: Map<string, string>;
24
+ /** Number of stacks with changes */
25
+ changedCount: number;
26
+ /** Number of stacks unchanged */
27
+ unchangedCount: number;
28
+ }
29
+ /**
30
+ * Template Hash Service - provides hash-based change detection
31
+ */
32
+ export declare class TemplateHashService {
33
+ /**
34
+ * Compute SHA-256 hash of a template file
35
+ */
36
+ computeTemplateHash(templatePath: string): Promise<Result<string, TemplateHashError>>;
37
+ /**
38
+ * Get hashes for all templates in cdk.out directory.
39
+ * Returns map of stack name to hash.
40
+ */
41
+ getTemplateHashes(cdkOutPath: string): Promise<Result<Map<string, string>, TemplateHashError>>;
42
+ /**
43
+ * Compare current template hashes with stored state.
44
+ * Returns which stacks have changed.
45
+ */
46
+ compareWithState(currentHashes: Map<string, string>, appPath: string): Promise<Result<TemplateComparisonResult, TemplateHashError>>;
47
+ /**
48
+ * Update state file with new hashes after successful deployment
49
+ */
50
+ updateStateAfterDeploy(appPath: string, deployedStacks: Map<string, string>, stackStatuses?: Map<string, string>): Promise<Result<void, TemplateHashError>>;
51
+ /**
52
+ * Get state file path for an application (exposed for testing/logging)
53
+ */
54
+ getStateFilePath(appPath: string): string;
55
+ /**
56
+ * Check if a specific stack has changes
57
+ */
58
+ stackHasChanges(comparison: TemplateComparisonResult, stackName: string): boolean;
59
+ /**
60
+ * Get list of stacks that have changes
61
+ */
62
+ getChangedStacks(comparison: TemplateComparisonResult): string[];
63
+ /**
64
+ * Get list of stacks that are unchanged
65
+ */
66
+ getUnchangedStacks(comparison: TemplateComparisonResult): string[];
67
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * TemplateHashService - Hash-based change detection for CDK templates
3
+ *
4
+ * Replaces slow CDK diff with fast local hash comparison.
5
+ * Uses SHA-256 hashing of synthesised templates.
6
+ */
7
+ import { createHash } from "crypto";
8
+ import { readFile, readdir } from "fs/promises";
9
+ import { join, basename } from "path";
10
+ import { fileExists } from "../../util/fsHelpers.js";
11
+ import { success, failure } from "@fjall/generator";
12
+ import { BaseServiceError } from "../../types/errors/ServiceError.js";
13
+ import { readStateFile, writeStateFile, createEmptyState, updateTemplateHash, getStateFilePath } from "../../types/FjallState.js";
14
+ /**
15
+ * TemplateHash-specific error types
16
+ */
17
+ export class TemplateHashError extends BaseServiceError {
18
+ errorType;
19
+ constructor(message, errorType, details, recoverable = false) {
20
+ super(`TEMPLATE_HASH_${errorType.toUpperCase()}`, message, details, recoverable);
21
+ this.errorType = errorType;
22
+ }
23
+ }
24
+ /** CDK template file extension */
25
+ const TEMPLATE_EXTENSION = ".template.json";
26
+ /**
27
+ * Template Hash Service - provides hash-based change detection
28
+ */
29
+ export class TemplateHashService {
30
+ /**
31
+ * Compute SHA-256 hash of a template file
32
+ */
33
+ async computeTemplateHash(templatePath) {
34
+ try {
35
+ const content = await readFile(templatePath, "utf-8");
36
+ const normalised = JSON.stringify(JSON.parse(content));
37
+ const hash = createHash("sha256").update(normalised).digest("hex");
38
+ return success(hash);
39
+ }
40
+ catch (error) {
41
+ return failure(new TemplateHashError(`Failed to hash template: ${templatePath}`, "hash_failed", { path: templatePath, error }));
42
+ }
43
+ }
44
+ /**
45
+ * Get hashes for all templates in cdk.out directory.
46
+ * Returns map of stack name to hash.
47
+ */
48
+ async getTemplateHashes(cdkOutPath) {
49
+ if (!(await fileExists(cdkOutPath))) {
50
+ return failure(new TemplateHashError(`CDK output directory not found: ${cdkOutPath}`, "cdk_out_not_found", { path: cdkOutPath }));
51
+ }
52
+ try {
53
+ const files = await readdir(cdkOutPath);
54
+ const templateFiles = files.filter((f) => f.endsWith(TEMPLATE_EXTENSION));
55
+ const hashes = new Map();
56
+ for (const file of templateFiles) {
57
+ const stackName = basename(file, TEMPLATE_EXTENSION);
58
+ const templatePath = join(cdkOutPath, file);
59
+ const hashResult = await this.computeTemplateHash(templatePath);
60
+ if (!hashResult.success) {
61
+ return failure(hashResult.error);
62
+ }
63
+ hashes.set(stackName, hashResult.data);
64
+ }
65
+ return success(hashes);
66
+ }
67
+ catch (error) {
68
+ return failure(new TemplateHashError(`Failed to read CDK output directory: ${cdkOutPath}`, "read_failed", { path: cdkOutPath, error }));
69
+ }
70
+ }
71
+ /**
72
+ * Compare current template hashes with stored state.
73
+ * Returns which stacks have changed.
74
+ */
75
+ async compareWithState(currentHashes, appPath) {
76
+ const state = await readStateFile(appPath);
77
+ const stackChanges = new Map();
78
+ let changedCount = 0;
79
+ let unchangedCount = 0;
80
+ for (const [stackName, currentHash] of currentHashes) {
81
+ const storedEntry = state?.templateHashes[stackName];
82
+ const hasChanged = !storedEntry || storedEntry.hash !== currentHash;
83
+ stackChanges.set(stackName, hasChanged);
84
+ if (hasChanged) {
85
+ changedCount++;
86
+ }
87
+ else {
88
+ unchangedCount++;
89
+ }
90
+ }
91
+ // Detect stacks that were previously deployed but no longer synthesised (removed stacks)
92
+ if (state?.templateHashes) {
93
+ for (const stackName of Object.keys(state.templateHashes)) {
94
+ if (!currentHashes.has(stackName)) {
95
+ stackChanges.set(stackName, true);
96
+ changedCount++;
97
+ }
98
+ }
99
+ }
100
+ return success({
101
+ stackChanges,
102
+ currentHashes,
103
+ changedCount,
104
+ unchangedCount
105
+ });
106
+ }
107
+ /**
108
+ * Update state file with new hashes after successful deployment
109
+ */
110
+ async updateStateAfterDeploy(appPath, deployedStacks, stackStatuses) {
111
+ try {
112
+ let state = (await readStateFile(appPath)) ?? createEmptyState();
113
+ for (const [stackName, hash] of deployedStacks) {
114
+ const status = stackStatuses?.get(stackName);
115
+ state = updateTemplateHash(state, stackName, hash, status);
116
+ }
117
+ await writeStateFile(appPath, state);
118
+ return success(undefined);
119
+ }
120
+ catch (error) {
121
+ return failure(new TemplateHashError(`Failed to write state file: ${getStateFilePath(appPath)}`, "state_write_failed", { appPath, error }));
122
+ }
123
+ }
124
+ /**
125
+ * Get state file path for an application (exposed for testing/logging)
126
+ */
127
+ getStateFilePath(appPath) {
128
+ return getStateFilePath(appPath);
129
+ }
130
+ /**
131
+ * Check if a specific stack has changes
132
+ */
133
+ stackHasChanges(comparison, stackName) {
134
+ return comparison.stackChanges.get(stackName) ?? true;
135
+ }
136
+ /**
137
+ * Get list of stacks that have changes
138
+ */
139
+ getChangedStacks(comparison) {
140
+ return Array.from(comparison.stackChanges.entries())
141
+ .filter(([, hasChanges]) => hasChanges)
142
+ .map(([stackName]) => stackName);
143
+ }
144
+ /**
145
+ * Get list of stacks that are unchanged
146
+ */
147
+ getUnchangedStacks(comparison) {
148
+ return Array.from(comparison.stackChanges.entries())
149
+ .filter(([, hasChanges]) => !hasChanges)
150
+ .map(([stackName]) => stackName);
151
+ }
152
+ }