@fjall/deploy-core 2.27.0 → 2.30.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 (151) hide show
  1. package/dist/.minified +1 -1
  2. package/dist/src/aws/organisations/backup.d.ts +7 -2
  3. package/dist/src/aws/organisations/backup.js +2 -2
  4. package/dist/src/aws/organisations/index.d.ts +1 -1
  5. package/dist/src/aws/organisations/index.js +1 -1
  6. package/dist/src/aws/organisations/oidcProvider.d.ts +37 -0
  7. package/dist/src/aws/organisations/oidcProvider.js +1 -1
  8. package/dist/src/aws/organisations/organisation.d.ts +21 -0
  9. package/dist/src/aws/organisations/organisation.js +1 -1
  10. package/dist/src/aws/utils/cloudformationEventHelpers.d.ts +45 -0
  11. package/dist/src/aws/utils/cloudformationEventHelpers.js +1 -1
  12. package/dist/src/aws/utils/cloudformationEventTypes.d.ts +1 -1
  13. package/dist/src/aws/utils/cloudformationEvents.d.ts +2 -1
  14. package/dist/src/aws/utils/cloudformationEvents.js +1 -1
  15. package/dist/src/aws/utils/stackStatus.js +1 -1
  16. package/dist/src/index.d.ts +16 -3
  17. package/dist/src/index.js +1 -1
  18. package/dist/src/orchestration/activeDeploymentGuard.d.ts +41 -0
  19. package/dist/src/orchestration/activeDeploymentGuard.js +8 -4
  20. package/dist/src/orchestration/application/applicationDeploy.d.ts +9 -0
  21. package/dist/src/orchestration/application/applicationDeploy.js +1 -1
  22. package/dist/src/orchestration/application/applicationDeployHelpers.d.ts +20 -3
  23. package/dist/src/orchestration/application/applicationDeployHelpers.js +3 -3
  24. package/dist/src/orchestration/application/applicationDestroy.js +1 -1
  25. package/dist/src/orchestration/application/approvalGate.d.ts +15 -1
  26. package/dist/src/orchestration/application/approvalGate.js +1 -1
  27. package/dist/src/orchestration/application/destructionGate.d.ts +164 -0
  28. package/dist/src/orchestration/application/destructionGate.js +5 -0
  29. package/dist/src/orchestration/application/dockerBuildHelper.js +1 -1
  30. package/dist/src/orchestration/application/leaseGate.d.ts +30 -0
  31. package/dist/src/orchestration/application/leaseGate.js +1 -0
  32. package/dist/src/orchestration/application/openNextBuild.d.ts +1 -1
  33. package/dist/src/orchestration/application/openNextBuild.js +3 -3
  34. package/dist/src/orchestration/application/plan/approvalToken.d.ts +15 -0
  35. package/dist/src/orchestration/application/plan/approvalToken.js +1 -1
  36. package/dist/src/orchestration/application/plan/changeSetEscalation.d.ts +46 -0
  37. package/dist/src/orchestration/application/plan/changeSetEscalation.js +1 -0
  38. package/dist/src/orchestration/application/plan/computeDeployPlan.d.ts +13 -0
  39. package/dist/src/orchestration/application/plan/computeDeployPlan.js +1 -1
  40. package/dist/src/orchestration/application/plan/index.d.ts +4 -1
  41. package/dist/src/orchestration/application/plan/index.js +1 -1
  42. package/dist/src/orchestration/application/plan/loadDeployPlan.d.ts +32 -0
  43. package/dist/src/orchestration/application/plan/loadDeployPlan.js +1 -0
  44. package/dist/src/orchestration/application/plan/pinnedNames.d.ts +31 -0
  45. package/dist/src/orchestration/application/plan/pinnedNames.js +1 -0
  46. package/dist/src/orchestration/application/plan/registryOverlay.d.ts +83 -0
  47. package/dist/src/orchestration/application/plan/registryOverlay.js +1 -0
  48. package/dist/src/orchestration/application/plan/renderDeployPlan.d.ts +4 -1
  49. package/dist/src/orchestration/application/plan/renderDeployPlan.js +1 -1
  50. package/dist/src/orchestration/application/plan/types.d.ts +31 -0
  51. package/dist/src/orchestration/application/plan/types.js +1 -0
  52. package/dist/src/orchestration/builders/frameworkRegistry.js +1 -1
  53. package/dist/src/orchestration/builders/index.d.ts +1 -0
  54. package/dist/src/orchestration/builders/index.js +1 -1
  55. package/dist/src/orchestration/builders/openNextBuilder.js +1 -1
  56. package/dist/src/orchestration/builders/staticSiteBuilder.d.ts +27 -0
  57. package/dist/src/orchestration/builders/staticSiteBuilder.js +2 -0
  58. package/dist/src/orchestration/contextHelpers.d.ts +37 -1
  59. package/dist/src/orchestration/contextHelpers.js +1 -1
  60. package/dist/src/orchestration/drift/classifyDriftFailure.d.ts +31 -0
  61. package/dist/src/orchestration/drift/classifyDriftFailure.js +1 -0
  62. package/dist/src/orchestration/drift/detectStackDrift.d.ts +37 -0
  63. package/dist/src/orchestration/drift/detectStackDrift.js +1 -0
  64. package/dist/src/orchestration/drift/driftPreFlight.d.ts +73 -0
  65. package/dist/src/orchestration/drift/driftPreFlight.js +7 -0
  66. package/dist/src/orchestration/drift/driftProbe.d.ts +113 -0
  67. package/dist/src/orchestration/drift/driftProbe.js +1 -0
  68. package/dist/src/orchestration/drift/driftSuspectJournal.d.ts +40 -0
  69. package/dist/src/orchestration/drift/driftSuspectJournal.js +1 -0
  70. package/dist/src/orchestration/drift/preFlightGate.d.ts +44 -0
  71. package/dist/src/orchestration/drift/preFlightGate.js +2 -0
  72. package/dist/src/orchestration/index.d.ts +19 -0
  73. package/dist/src/orchestration/index.js +1 -1
  74. package/dist/src/orchestration/organisation/cascadeDestroyHelpers.d.ts +7 -0
  75. package/dist/src/orchestration/organisation/cascadeDestroyHelpers.js +1 -1
  76. package/dist/src/orchestration/organisation/cascadeHelpers.js +1 -1
  77. package/dist/src/orchestration/organisation/organisationDestroy.js +3 -3
  78. package/dist/src/orchestration/organisationDeploy/infraSteps.d.ts +8 -1
  79. package/dist/src/orchestration/organisationDeploy/infraSteps.js +1 -1
  80. package/dist/src/orchestration/organisationDeploy/orgCascadeDeploy.js +5 -5
  81. package/dist/src/orchestration/organisationDeploy/orgContext.d.ts +21 -2
  82. package/dist/src/orchestration/organisationDeploy/orgContext.js +1 -1
  83. package/dist/src/orchestration/organisationDeploy/singleComponentDeploy.js +1 -1
  84. package/dist/src/orchestration/remediation/driftRepairGate.d.ts +66 -0
  85. package/dist/src/orchestration/remediation/driftRepairGate.js +1 -0
  86. package/dist/src/orchestration/remediation/forensicsCapture.d.ts +49 -0
  87. package/dist/src/orchestration/remediation/forensicsCapture.js +1 -0
  88. package/dist/src/orchestration/remediation/forgetResource.d.ts +423 -0
  89. package/dist/src/orchestration/remediation/forgetResource.js +1 -0
  90. package/dist/src/orchestration/remediation/pinRemediation.d.ts +121 -0
  91. package/dist/src/orchestration/remediation/pinRemediation.js +1 -0
  92. package/dist/src/orchestration/remediation/postFailureRepairOffer.d.ts +77 -0
  93. package/dist/src/orchestration/remediation/postFailureRepairOffer.js +14 -0
  94. package/dist/src/orchestration/remediation/recreatePreFlight.d.ts +47 -0
  95. package/dist/src/orchestration/remediation/recreatePreFlight.js +1 -0
  96. package/dist/src/orchestration/remediation/recreateResource.d.ts +110 -0
  97. package/dist/src/orchestration/remediation/recreateResource.js +1 -0
  98. package/dist/src/orchestration/remediation/remediationJournal.d.ts +88 -0
  99. package/dist/src/orchestration/remediation/remediationJournal.js +2 -0
  100. package/dist/src/orchestration/remediation/removalUpdate.d.ts +135 -0
  101. package/dist/src/orchestration/remediation/removalUpdate.js +1 -0
  102. package/dist/src/orchestration/remediation/retainFlip.d.ts +294 -0
  103. package/dist/src/orchestration/remediation/retainFlip.js +1 -0
  104. package/dist/src/orchestration/spawnHelpers.d.ts +9 -0
  105. package/dist/src/orchestration/spawnHelpers.js +1 -1
  106. package/dist/src/services/application/ApplicationStackService.d.ts +5 -3
  107. package/dist/src/services/application/ApplicationStackService.js +1 -1
  108. package/dist/src/services/application/applicationStackHelpers.d.ts +8 -6
  109. package/dist/src/services/application/applicationStackHelpers.js +3 -3
  110. package/dist/src/services/index.d.ts +2 -2
  111. package/dist/src/services/index.js +1 -1
  112. package/dist/src/services/infrastructure/CdkEventMonitoring.d.ts +15 -0
  113. package/dist/src/services/infrastructure/CdkEventMonitoring.js +1 -1
  114. package/dist/src/services/infrastructure/CdkOutputAnalyser.d.ts +8 -0
  115. package/dist/src/services/infrastructure/CdkOutputAnalyser.js +2 -1
  116. package/dist/src/services/infrastructure/CdkService.js +3 -3
  117. package/dist/src/services/infrastructure/CfnRegistryService.d.ts +68 -0
  118. package/dist/src/services/infrastructure/CfnRegistryService.js +1 -0
  119. package/dist/src/services/infrastructure/CloudFormationService.d.ts +5 -2
  120. package/dist/src/services/infrastructure/CloudFormationService.js +1 -1
  121. package/dist/src/services/infrastructure/EcsServiceResolver.d.ts +23 -3
  122. package/dist/src/services/infrastructure/EcsServiceResolver.js +1 -1
  123. package/dist/src/services/infrastructure/cfnRegistrySeed.d.ts +10 -0
  124. package/dist/src/services/infrastructure/cfnRegistrySeed.js +1 -0
  125. package/dist/src/services/infrastructure/changeSetProbe.d.ts +88 -0
  126. package/dist/src/services/infrastructure/changeSetProbe.js +1 -0
  127. package/dist/src/services/infrastructure/index.d.ts +2 -0
  128. package/dist/src/services/infrastructure/index.js +1 -1
  129. package/dist/src/types/callbackKeys.d.ts +1 -1
  130. package/dist/src/types/callbackKeys.js +1 -1
  131. package/dist/src/types/callbacks.d.ts +22 -0
  132. package/dist/src/types/config/entitlements.d.ts +9 -0
  133. package/dist/src/types/config/orgConfig.d.ts +6 -0
  134. package/dist/src/types/deployEvent.d.ts +10 -3
  135. package/dist/src/types/deployEvent.js +1 -1
  136. package/dist/src/types/deploymentEventSchema.d.ts +1 -0
  137. package/dist/src/types/deploymentEventSchema.js +1 -1
  138. package/dist/src/types/destruction.d.ts +212 -0
  139. package/dist/src/types/destruction.js +1 -0
  140. package/dist/src/types/detection/patternTypes.d.ts +9 -4
  141. package/dist/src/types/detection/patternTypes.js +1 -1
  142. package/dist/src/types/drift.d.ts +72 -0
  143. package/dist/src/types/drift.js +1 -0
  144. package/dist/src/types/frameworkBuilder.d.ts +14 -0
  145. package/dist/src/types/index.d.ts +44 -4
  146. package/dist/src/types/index.js +1 -1
  147. package/dist/src/types/operations.d.ts +2 -1
  148. package/dist/src/types/params.d.ts +142 -0
  149. package/dist/src/types/remediation.d.ts +126 -0
  150. package/dist/src/types/remediation.js +1 -0
  151. package/package.json +6 -4
@@ -0,0 +1,294 @@
1
+ /**
2
+ * The retain flip — phase 2/3 of the two-deploy `forget` orchestration
3
+ * (drift-remediation plan § 3.2). The flip template derives from the LIVE
4
+ * template (`GetTemplate`, never synth): the ONLY mutations are
5
+ * `DeletionPolicy: Retain` + `UpdateReplacePolicy: Retain` on the target
6
+ * logical IDs plus the in-template `fjall:forget` metadata marker [G3] — the
7
+ * AWS-side resume state that survives machine loss.
8
+ *
9
+ * Two proof guards, both required before the flip may apply:
10
+ * (a) `verifyRetainFlipDiff` — a pure diff-guard that hard-fails if anything
11
+ * beyond the allowed keys differs from the fetched template;
12
+ * (b) `proveFlipMetadataOnly` — a `fjall-preflight-` review change set that
13
+ * proves every reported change targets only
14
+ * `{DeletionPolicy, UpdateReplacePolicy, Metadata}` on the target IDs.
15
+ * Any property-level delta aborts to `repair` with the offending paths
16
+ * attached (paths only, never values). Non-target changes are tolerated ONLY
17
+ * as transitive dynamic reference re-evaluations caused by the targets —
18
+ * CloudFormation conservatively re-reports every Ref/GetAtt dependent of a
19
+ * metadata-touched resource (2026-07-12 live defect #4: the BC Account
20
+ * stack's bucket, bucket policy and key alias re-evaluated off a flipped KMS
21
+ * key, wrongly aborting the consented forget) — see
22
+ * `classifyProofViolations`.
23
+ *
24
+ * [G10] `didn't contain changes` is NOT taken as "flip already live" —
25
+ * `GetTemplate` re-verification (`readRetainFlipState`) is the sole authority
26
+ * for the already-applied/resume-skip branch.
27
+ *
28
+ * FLIPLESS fast-path (2026-07-12 live defect #5): the UpdateStack here is
29
+ * reserved for `policyPendingTargets` — targets whose live policies actually
30
+ * need changing. Targets already carrying the spec's policy (the CDK default
31
+ * for stateful types is Retain) are advanced by the orchestrators WITHOUT any
32
+ * UpdateStack and WITHOUT a marker: CloudFormation re-resolves Ref/GetAtt
33
+ * dependents of any modified dead resource at execution time, so even a
34
+ * metadata-only Modify of a dead, referenced target rolls the update back —
35
+ * and the policy the update would assert is already live, so the mutation
36
+ * guarantees nothing the live template does not already prove. The same
37
+ * incident's OUTBOUND direction (a policy-pending target whose OWN references
38
+ * resolve against dead siblings — modifying it is structurally impossible)
39
+ * is advanced flipless BY NECESSITY once the target itself is verified
40
+ * physically deleted: the converge's removal of an already-deleted resource
41
+ * is a physical no-op, so the Retain guarantee is not needed (the forget
42
+ * orchestrator's classification; journal `fliplessByNecessity`).
43
+ *
44
+ * The flip `UpdateStack` OMITS the `Tags` parameter (CloudFormation preserves
45
+ * existing stack tags, so no tag-driven resource updates can ride along),
46
+ * echoes the stack's capabilities unioned with the proof's fixed set, and
47
+ * echoes `UsePreviousValue: true` for every live stack parameter — omitting
48
+ * `Parameters` would resolve the template DEFAULTS (e.g. `<Service>ImageTag`
49
+ * back to "latest"), turning the metadata-only flip into a real resource
50
+ * update. The proof change set echoes the same parameters for the same
51
+ * reason. Immediately before the UpdateStack, the live template digest is
52
+ * re-checked against the template the flip derived from — the proof change
53
+ * set is review-only and never executed, so nothing else binds the apply to
54
+ * the proven bytes.
55
+ *
56
+ * [G8] boundary audit (2026-07-08): the flip's mutations —
57
+ * cloudformation:CreateChangeSet/DescribeChangeSet/DeleteChangeSet/UpdateStack
58
+ * — target the app's OWN stack, which the deploy session policy's
59
+ * CFN-mutation Deny exempts via its `stack/${appPascal}*` NotResource
60
+ * (`deploySessionPolicy.ts`); template staging rides the inherit-admin allow
61
+ * like the change-set probe's. A repair aimed at a foreign `--stack` under an
62
+ * OIDC deploy session correctly AccessDenies — defence-in-depth, not a gap.
63
+ * No session-policy change needed.
64
+ *
65
+ * Failure messages are pre-masked at construction.
66
+ */
67
+ import { Capability, type Change } from "@aws-sdk/client-cloudformation";
68
+ import { type Result } from "@fjall/generator";
69
+ import type { AwsProvider } from "../../aws/AwsProvider.js";
70
+ /** The in-template resume marker [G3]. */
71
+ export declare const FORGET_MARKER_KEY = "fjall:forget";
72
+ /** The recreate orchestration's in-template resume marker (Phase 7) — a
73
+ * distinct key so the two surgeries' resume/ownership scans never adopt each
74
+ * other's markers. */
75
+ export declare const RECREATE_MARKER_KEY = "fjall:recreate";
76
+ export type FlipDeletionPolicy = "Retain" | "Snapshot" | "Delete";
77
+ /**
78
+ * What the flip stamps: the marker key and the DeletionPolicy /
79
+ * UpdateReplacePolicy value per target. The forget flip retains everything;
80
+ * the recreate snapshot-flip stamps Snapshot where the resource type supports
81
+ * it and Delete otherwise. Every read/derive/diff helper below takes the spec
82
+ * so both surgeries share one proven flip machine; the default is the forget
83
+ * spec, keeping every pre-Phase-7 call site byte-identical.
84
+ */
85
+ export interface FlipSpec {
86
+ markerKey: string;
87
+ policyForTarget: (logicalId: string) => FlipDeletionPolicy;
88
+ }
89
+ export declare const FORGET_FLIP_SPEC: FlipSpec;
90
+ /**
91
+ * Proof and apply MUST request the same capability set: a proof that
92
+ * hardcodes a superset the apply lacks means a transform-bearing stack proves
93
+ * clean and then dies at UpdateStack with InsufficientCapabilities. Union of
94
+ * the stack's recorded capabilities (invalid values filtered) with the fixed
95
+ * proof set — one helper, both call sites.
96
+ */
97
+ export declare function composeFlipCapabilities(recorded: readonly string[]): Capability[];
98
+ /**
99
+ * Echo `UsePreviousValue: true` for every live stack parameter on both flip
100
+ * CFN calls (mirrors `CloudFormationService.updateStackParameters`). Deployed
101
+ * Compute stacks carry `<Service>ImageTag` CfnParameters whose live value is
102
+ * a content hash but whose template default is "latest" — omitting
103
+ * `Parameters` resolves the DEFAULTS, so the proof would report Image →
104
+ * repo:latest → Replacement on the task definition and hard-fail every
105
+ * attempt, and the apply would roll images back. Zero-parameter stacks omit
106
+ * the property entirely.
107
+ */
108
+ export declare function echoPreviousParameters(parameterKeys: readonly string[]): {
109
+ Parameters: Array<{
110
+ ParameterKey: string;
111
+ UsePreviousValue: true;
112
+ }>;
113
+ } | Record<string, never>;
114
+ export interface RetainFlipOptions {
115
+ pollIntervalMs?: number;
116
+ changeSetTimeoutMs?: number;
117
+ updateTimeoutMs?: number;
118
+ /** Injectable clock for timeout tests. */
119
+ now?: () => number;
120
+ }
121
+ export declare function computeTemplateDigest(templateBody: string): string;
122
+ export interface RetainFlipState {
123
+ /** Every target carries the spec's policy + marker. */
124
+ allFlipped: boolean;
125
+ /** Targets no longer present in the live template (removal landed). */
126
+ absentTargets: string[];
127
+ /** Targets present but not (fully) flipped. */
128
+ unflippedTargets: string[];
129
+ /**
130
+ * Targets present whose DeletionPolicy/UpdateReplacePolicy do NOT match the
131
+ * spec — the ones an UpdateStack would actually have to change. Disjoint
132
+ * from the flipless set (present, policies satisfied, marker absent =
133
+ * `unflippedTargets` minus this list). The POLICIES are the safety property
134
+ * the flip exists to guarantee; the marker is resume/ownership bookkeeping —
135
+ * so this list, not `unflippedTargets`, gates the flip UpdateStack and the
136
+ * [G2] partition's flip-live check (2026-07-12 live defect #5:
137
+ * CloudFormation re-resolves Ref/GetAtt dependents of ANY modified dead
138
+ * resource at execution time, so an UpdateStack that merely re-states an
139
+ * already-live Retain policy to add a marker rolls back — and is
140
+ * unnecessary, because the policy it would assert is already live).
141
+ */
142
+ policyPendingTargets: string[];
143
+ /** Distinct opIds found in the targets' markers (the spec's key only). */
144
+ markerOpIds: string[];
145
+ }
146
+ /**
147
+ * [G3]/[G10] authority helper: read the flip state straight from the live
148
+ * template. Shared by the flip-applied verify, resume re-derivation, and the
149
+ * [G2] pre-flight's mid-surgery check.
150
+ */
151
+ export declare function readRetainFlipState(liveTemplateBody: string, targets: readonly string[], stackName: string, spec?: FlipSpec): Result<RetainFlipState, Error>;
152
+ /**
153
+ * [G3] recovery scan: every logical ID in the live template whose
154
+ * fjall:forget marker carries the given opId. Recovery is LIVE-TEMPLATE
155
+ * scoped: markers embed only `{opId, at}` per-resource, so a marked resource
156
+ * that was since stripped (converged away, manually edited) is invisible here
157
+ * and the recovered set may be a PROPER SUBSET of the operation's original
158
+ * target set. Callers that adopt a foreign operation from this scan MUST
159
+ * prove completeness via hash identity — `computeRemediationOpId(stackName,
160
+ * recovered)` reproducing the marker's opId is the only proof the full set
161
+ * was recovered; adopting a subset mints a different opId whose surgery
162
+ * meets the original markers as foreign and livelocks.
163
+ */
164
+ export declare function findMarkedResourcesByOpId(liveTemplateBody: string, opId: string, stackName: string, markerKey?: string): Result<string[], Error>;
165
+ /**
166
+ * CFN resource Type per logical ID, read from the live template — the
167
+ * adoption seam records these on targets recovered from markers alone (no
168
+ * journal, no drift finding to carry the type). Unreadable template or
169
+ * missing/typeless resource simply omits the entry.
170
+ */
171
+ export declare function readResourceTypes(liveTemplateBody: string, logicalIds: readonly string[], stackName: string): Record<string, string>;
172
+ export interface DeriveRetainFlipParams {
173
+ liveTemplateBody: string;
174
+ stackName: string;
175
+ targets: readonly string[];
176
+ opId: string;
177
+ /** ISO-8601 stamp recorded in the marker. */
178
+ at: string;
179
+ /** Marker key + per-target policy — defaults to the forget flip. */
180
+ spec?: FlipSpec;
181
+ }
182
+ export interface RetainFlipDerivation {
183
+ flipTemplateBody: string;
184
+ liveTemplateDigest: string;
185
+ /** True when every target already carried the policy + OUR opId's marker. */
186
+ alreadyApplied: boolean;
187
+ }
188
+ /**
189
+ * Build the flip template from the live template: mutate ONLY
190
+ * DeletionPolicy/UpdateReplacePolicy → the spec's per-target policy and stamp
191
+ * the spec's marker on the target logical IDs. A target carrying a DIFFERENT
192
+ * operation's marker fails closed — two overlapping surgeries on one resource
193
+ * means the operator's model of the stack is wrong.
194
+ */
195
+ export declare function deriveRetainFlipTemplate(params: DeriveRetainFlipParams): Result<RetainFlipDerivation, Error>;
196
+ /**
197
+ * Pure diff-guard (§3.2 guard a): strip the allowed flip keys from both
198
+ * templates and require byte-identical remainders. Any residual difference
199
+ * hard-fails with the offending paths (paths only, never values).
200
+ */
201
+ export declare function verifyRetainFlipDiff(liveTemplateBody: string, flipTemplateBody: string, targets: readonly string[], stackName: string, markerKey?: string): Result<void, Error>;
202
+ export interface FlipProofResult {
203
+ /** True when CFN reported the flip contains no changes at all [G10] —
204
+ * the caller MUST re-verify via GetTemplate, never assume flip-live. */
205
+ noChanges: boolean;
206
+ }
207
+ /**
208
+ * Classify the proof change set's reported changes; an empty return proves
209
+ * the flip metadata-only. Targets may only carry Modify actions scoped to
210
+ * `ALLOWED_FLIP_ATTRIBUTES` with no replacement. Non-targets are tolerated
211
+ * ONLY via `isReferenceReEvaluationOf`, computed to a fixpoint because the
212
+ * re-evaluation propagates transitively (2026-07-12 live defect #4: the BC
213
+ * Account stack's bucket policy re-evaluated off the bucket's ARN, which
214
+ * itself re-evaluated off the flipped KMS key's ARN). A logical ID reported
215
+ * by several change entries is tolerated only if EVERY entry qualifies.
216
+ */
217
+ export declare function classifyProofViolations(changes: readonly Change[], targets: readonly string[]): string[];
218
+ export interface ProveFlipMetadataOnlyParams {
219
+ stackName: string;
220
+ flipTemplateBody: string;
221
+ targets: readonly string[];
222
+ /** Live parameter keys, echoed UsePreviousValue on the change set — see
223
+ * `echoPreviousParameters`. */
224
+ parameterKeys: readonly string[];
225
+ abortSignal?: AbortSignal;
226
+ }
227
+ /**
228
+ * Change-set proof (§3.2 guard b): create a review-only `fjall-preflight-`
229
+ * change set for the flip template and prove every reported change targets
230
+ * only {DeletionPolicy, UpdateReplacePolicy, Metadata} on the target IDs —
231
+ * tolerating non-target changes only where `classifyProofViolations` proves
232
+ * them transitive dynamic reference re-evaluations of the targets.
233
+ * Nothing is ever executed; the change set is deleted in `finally`.
234
+ */
235
+ export declare function proveFlipMetadataOnly(aws: AwsProvider, params: ProveFlipMetadataOnlyParams, options?: RetainFlipOptions): Promise<Result<FlipProofResult, Error>>;
236
+ export interface ApplyRetainFlipParams {
237
+ stackName: string;
238
+ flipTemplateBody: string;
239
+ /** The stack's existing capabilities — unioned with the proof's fixed set
240
+ * via `composeFlipCapabilities` so proof and apply request the same set. */
241
+ capabilities: readonly string[];
242
+ /** Live parameter keys, echoed UsePreviousValue on the UpdateStack — see
243
+ * `echoPreviousParameters`. */
244
+ parameterKeys: readonly string[];
245
+ /** Digest of the live template the flip derived from — re-checked against
246
+ * a fresh GetTemplate immediately before the UpdateStack (the proof change
247
+ * set is never executed, so nothing else binds apply to proof). */
248
+ derivedFromTemplateDigest: string;
249
+ /** Names the resume command in the mid-surgery abort message. */
250
+ opId: string;
251
+ /** Failure-message label — the recreate arm's removal update reuses this
252
+ * apply machine and labels itself honestly. Default "Retain-flip". */
253
+ operationLabel?: string;
254
+ /** Mid-surgery digest-mismatch cure line — defaults to the forget arm's
255
+ * `fjall drift resume <opId>` teach. */
256
+ resumeHint?: string;
257
+ abortSignal?: AbortSignal;
258
+ }
259
+ export type ApplyRetainFlipOutcome = {
260
+ outcome: "updated";
261
+ }
262
+ /** CFN reported "No updates are to be performed" — the caller re-verifies
263
+ * the flip via GetTemplate [G10], never assumes it landed. */
264
+ | {
265
+ outcome: "no-updates";
266
+ };
267
+ /**
268
+ * Execute the flip via direct SDK UpdateStack — flip TemplateBody, Tags
269
+ * OMITTED (CFN preserves existing stack tags), capabilities echoed — then
270
+ * poll to a terminal status. A metadata-only update issues no call against
271
+ * the dead TARGET itself, but CloudFormation re-resolves Ref/GetAtt
272
+ * attributes of every modified resource for its dependents at execution time
273
+ * — which fails for a dead target with live referrers (2026-07-12 live
274
+ * defect #5: "Unable to retrieve Arn attribute"). Callers MUST therefore
275
+ * reserve this UpdateStack for policy-pending targets and guard the
276
+ * referrer topology first (the forget orchestrator's flipless fast-path +
277
+ * referrer guard; the recreate arm's pre-flight removal derivation).
278
+ */
279
+ export declare function applyRetainFlip(aws: AwsProvider, params: ApplyRetainFlipParams, options?: RetainFlipOptions): Promise<Result<ApplyRetainFlipOutcome, Error>>;
280
+ /**
281
+ * Inline body up to CFN's 51,200-byte limit; larger templates stage in the
282
+ * CDK bootstrap assets bucket (the change-set probe's staging shape). Shared
283
+ * with the recreate arm's removal update (`removalUpdate.ts`).
284
+ */
285
+ export declare function resolveTemplateLocation(aws: AwsProvider, keySuffix: string, templateBody: string, abortSignal?: AbortSignal): Promise<Result<{
286
+ location: {
287
+ TemplateBody: string;
288
+ } | {
289
+ TemplateURL: string;
290
+ };
291
+ key?: string;
292
+ }, Error>>;
293
+ /** Best-effort cleanup of the staged template — logged, never thrown. */
294
+ export declare function deleteStagedTemplate(aws: AwsProvider, key: string): Promise<void>;
@@ -0,0 +1 @@
1
+ import{createHash as J,randomUUID as C}from"node:crypto";import{Capability as I,CloudFormationClient as _,CreateChangeSetCommand as V,DeleteChangeSetCommand as W,DescribeChangeSetCommand as M,DescribeStacksCommand as X,GetTemplateCommand as z,UpdateStackCommand as q}from"@aws-sdk/client-cloudformation";import{DeleteObjectCommand as Q,PutObjectCommand as Z,S3Client as P}from"@aws-sdk/client-s3";import{success as y,failure as g}from"@fjall/generator";import{getErrorMessage as b,maskSensitiveOutput as w}from"@fjall/util";import{logger as O}from"@fjall/util/logger";import{composeSdkAbortSignal as S,isAborted as $}from"../../aws/organisations/types.js";import{bootstrapAssetsBucketName as v,MAX_INLINE_TEMPLATE_BYTES as ee,PREFLIGHT_CHANGE_SET_PREFIX as te}from"../../services/infrastructure/changeSetProbe.js";import{sleepAbortable as N}from"../../util/sleepAbortable.js";const j="RetainFlip",k="fjall:forget",we="fjall:recreate",D={markerKey:k,policyForTarget:()=>"Retain"},ne=new Set(["DeletionPolicy","UpdateReplacePolicy","Metadata"]),L=/didn't contain changes|No updates are to be performed/i,re=new Set(["CREATE_COMPLETE","FAILED"]),oe=new Set(Object.values(I));function ae(e){return oe.has(e)}const ie=[I.CAPABILITY_IAM,I.CAPABILITY_NAMED_IAM,I.CAPABILITY_AUTO_EXPAND];function F(e){return[...new Set([...e.filter(ae),...ie])]}function U(e){return e.length===0?{}:{Parameters:e.map(n=>({ParameterKey:n,UsePreviousValue:!0}))}}const x=3e3,se=9e4,ce=10*6e4;function A(e,n){let r;try{r=JSON.parse(e)}catch{return g(new Error(`The live template for ${n} is not JSON (YAML-authored stacks are not supported by the retain flip) \u2014 aborting to manual repair.`))}return typeof r!="object"||r===null||Array.isArray(r)?g(new Error(`The live template for ${n} did not parse to an object \u2014 aborting to manual repair.`)):y(r)}function B(e){return J("sha256").update(e).digest("hex")}function le(e,n,r,t=D){const o=A(e,r);if(!o.success)return o;const a=o.data.Resources??{},i=[],s=[],c=[],f=new Set;for(const p of n){const l=a[p];if(l===void 0){i.push(p);continue}const u=K(l,t.markerKey);u!==void 0&&f.add(u.opId);const d=t.policyForTarget(p),m=l.DeletionPolicy===d&&l.UpdateReplacePolicy===d;m||c.push(p),(!m||u===void 0)&&s.push(p)}return y({allFlipped:i.length===0&&s.length===0,absentTargets:i,unflippedTargets:s,policyPendingTargets:c,markerOpIds:[...f]})}function K(e,n){const r=e.Metadata;if(typeof r!="object"||r===null)return;const t=r[n];if(typeof t!="object"||t===null)return;const o=t.opId;return typeof o=="string"?{opId:o}:void 0}function be(e,n,r,t=k){const o=A(e,r);if(!o.success)return o;const a=[];for(const[i,s]of Object.entries(o.data.Resources??{}))K(s,t)?.opId===n&&a.push(i);return y(a)}function Ae(e,n,r){const t=A(e,r);if(!t.success)return{};const o=t.data.Resources??{},a={};for(const i of n){const s=o[i]?.Type;typeof s=="string"&&(a[i]=s)}return a}function Re(e){const{liveTemplateBody:n,stackName:r,targets:t,opId:o,at:a}=e,i=e.spec??D,s=A(n,r);if(!s.success)return s;const c=le(n,t,r,i);if(!c.success)return c;if(c.data.absentTargets.length>0)return g(new Error(`Cannot derive the retain flip for ${r}: the live template no longer contains ${c.data.absentTargets.join(", ")}.`));const f=c.data.markerOpIds.filter(u=>u!==o);if(f.length>0)return g(new Error(`Cannot derive the retain flip for ${r}: target resources carry a ${i.markerKey} marker from a different operation (${f.join(", ")}) \u2014 resolve that operation first (fjall drift resume <opId>).`));const p=s.data,l=p.Resources??{};for(const u of t){const d=l[u];if(d===void 0)continue;const m=i.policyForTarget(u);d.DeletionPolicy=m,d.UpdateReplacePolicy=m;const h=typeof d.Metadata=="object"&&d.Metadata!==null?d.Metadata:{};h[i.markerKey]={opId:o,at:a},d.Metadata=h}return y({flipTemplateBody:JSON.stringify(p),liveTemplateDigest:B(n),alreadyApplied:c.data.allFlipped})}function Ie(e,n,r,t,o=k){const a=A(e,t);if(!a.success)return a;const i=A(n,t);if(!i.success)return i;const s=new Set(r);for(const f of[a.data,i.data]){const p=f.Resources??{};for(const l of Object.keys(p)){if(!s.has(l))continue;const u=p[l];if(u===void 0)continue;delete u.DeletionPolicy,delete u.UpdateReplacePolicy;const d=u.Metadata;typeof d=="object"&&d!==null&&(delete d[o],Object.keys(d).length===0&&delete u.Metadata)}}const c=G(a.data,i.data,"");return c.length>0?g(new Error(`Retain-flip diff-guard failed for ${t}: the flip template differs from the live template beyond the allowed keys at ${c.slice(0,10).join(", ")}${c.length>10?` (+${c.length-10} more)`:""} \u2014 aborting to manual repair, nothing was mutated.`)):y(void 0)}function G(e,n,r){if(e===n)return[];if(!(typeof e=="object"&&e!==null)||!(typeof n=="object"&&n!==null)||Array.isArray(e)!==Array.isArray(n))return JSON.stringify(e)===JSON.stringify(n)?[]:[r||"(root)"];const a=new Set([...Object.keys(e),...Object.keys(n)]),i=[];for(const s of a)i.push(...G(e[s],n[s],r===""?s:`${r}/${s}`));return i}function fe(e,n){if(e.Action!=="Modify"||e.Replacement==="True"||e.Replacement==="Conditional")return!1;const r=e.Details??[];if(r.length===0)return!1;const t=new Set(r.flatMap(a=>a.Target?.Attribute!==void 0?[a.Target.Attribute]:[]));return(e.Scope??[]).every(a=>t.has(a))?r.every(a=>{if(a.Evaluation!=="Dynamic"||a.ChangeSource!=="ResourceReference"&&a.ChangeSource!=="ResourceAttribute")return!1;const i=a.CausingEntity?.split(".")[0];return i!==void 0&&i!==""&&n.has(i)}):!1}function ue(e,n){const r=new Set(n),t=[],o=new Map;for(const s of e){if(s.Type!=="Resource")continue;const c=s.ResourceChange,f=c?.LogicalResourceId;if(c===void 0||f===void 0)continue;if(!r.has(f)){const l=o.get(f)??[];l.push(c),o.set(f,l);continue}if(c.Action!=="Modify"){t.push(`${f} (action ${c.Action??"unknown"})`);continue}if(c.Replacement==="True"||c.Replacement==="Conditional"){t.push(`${f} (replacement ${c.Replacement})`);continue}const p=[...c.Scope??[],...(c.Details??[]).flatMap(l=>l.Target?.Attribute!==void 0?[l.Target.Attribute]:[])];for(const l of p)ne.has(l)||t.push(`${f}/${l}`)}const a=new Set(n);let i=!0;for(;i;){i=!1;for(const[s,c]of o)a.has(s)||c.every(f=>fe(f,a))&&(a.add(s),i=!0)}for(const s of o.keys())a.has(s)||t.push(`${s} (not a flip target)`);return t}async function $e(e,n,r){const{stackName:t,abortSignal:o}=n,a=r?.pollIntervalMs??x,i=r?.changeSetTimeoutMs??se,s=r?.now??(()=>Date.now()),c=e.getClient(_),f=`${te}flip-${C()}`;let p;try{const l=await Y(e,f,n.flipTemplateBody,o);if(!l.success)return l;p=l.data.key,await c.send(new V({StackName:t,ChangeSetName:f,ChangeSetType:"UPDATE",IncludeNestedStacks:!0,Capabilities:F([]),Description:"fjall retain-flip metadata-only proof \u2014 review-only, never executed",...U(n.parameterKeys),...l.data.location}),{abortSignal:S(o)});const u=s();let d,m;for(;!$(o);){const R=await c.send(new M({StackName:t,ChangeSetName:f}),{abortSignal:S(o)});if(d=R.Status,m=R.StatusReason,d!==void 0&&re.has(d))break;if(s()-u>=i)return g(new Error(`Retain-flip proof timed out after ${i}ms for ${t} (last status: ${d??"unknown"})`));await N(a,o)}if($(o))return g(new Error(`Retain-flip proof aborted while polling ${t}`));if(d==="FAILED")return m!==void 0&&L.test(m)?y({noChanges:!0}):g(new Error(`Retain-flip proof change set failed for ${t}: ${w(m??"no status reason reported")}`));const h=[];let T;do{if($(o))return g(new Error(`Retain-flip proof aborted while reading ${t}`));const R=await c.send(new M({StackName:t,ChangeSetName:f,...T!==void 0?{NextToken:T}:{}}),{abortSignal:S(o)});h.push(...R.Changes??[]),T=R.NextToken}while(T!==void 0);const E=ue(h,n.targets);return E.length>0?g(new Error(`Retain flip is NOT metadata-only for ${t} \u2014 the review change set reported property-level changes at ${[...new Set(E)].join(", ")}. Aborting to manual repair; nothing was mutated.`)):y({noChanges:!1})}catch(l){return g(new Error(`Retain-flip proof failed for ${t}: ${w(b(l))}`))}finally{await c.send(new W({StackName:t,ChangeSetName:f}),{abortSignal:S()}).catch(l=>{O.warn(j,"Failed to delete retain-flip proof change set",{stackName:t,changeSetName:f,error:w(b(l))})}),p!==void 0&&await H(e,p)}}async function ke(e,n,r){const{stackName:t,abortSignal:o}=n,a=n.operationLabel??"Retain-flip",i=r?.pollIntervalMs??x,s=r?.updateTimeoutMs??ce,c=r?.now??(()=>Date.now()),f=e.getClient(_),p=`flip-apply-${C()}`;let l;try{const u=await Y(e,p,n.flipTemplateBody,o);if(!u.success)return u;l=u.data.key;const d=await f.send(new z({StackName:t,TemplateStage:"Original"}),{abortSignal:S(o)});if(B(d.TemplateBody??"")!==n.derivedFromTemplateDigest)return g(new Error(`${a} apply aborted for ${t}: the live template changed mid-surgery (after the metadata-only proof) \u2014 nothing was mutated. ${n.resumeHint??`Re-run 'fjall drift resume ${n.opId}' to re-verify from the current template.`}`));try{await f.send(new q({StackName:t,Capabilities:F(n.capabilities),...U(n.parameterKeys),...u.data.location}),{abortSignal:S(o)})}catch(h){if(L.test(b(h)))return y({outcome:"no-updates"});throw h}const m=c();for(;!$(o);){const T=(await f.send(new X({StackName:t}),{abortSignal:S(o)})).Stacks?.[0],E=T?.StackStatus??"UNKNOWN";if(E==="UPDATE_COMPLETE")return y({outcome:"updated"});if(!E.endsWith("_IN_PROGRESS"))return g(new Error(`${a} UpdateStack on ${t} ended ${E}: ${w(T?.StackStatusReason??"no status reason reported")}`));if(c()-m>=s)return g(new Error(`${a} UpdateStack on ${t} did not settle within ${s}ms (last status: ${E}). ${n.resumeHint??"Re-run 'fjall drift resume' once the stack settles."}`));await N(i,o)}return g(new Error(`${a} UpdateStack aborted while polling ${t}`))}catch(u){return g(new Error(`${a} UpdateStack failed for ${t}: ${w(b(u))}`))}finally{l!==void 0&&await H(e,l)}}async function Y(e,n,r,t){if(Buffer.byteLength(r,"utf8")<=ee)return y({location:{TemplateBody:r}});const o=e.getRegion(),a=v(e.getAccountId(),o),i=`fjall-preflight/${n}.template.json`;try{return await e.getClient(P).send(new Z({Bucket:a,Key:i,Body:r,ContentType:"application/json"}),{abortSignal:S(t)}),y({location:{TemplateURL:`https://${a}.s3.${o}.amazonaws.com/${i}`},key:i})}catch(s){return g(new Error(`Retain flip could not stage the template in ${a}: ${w(b(s))}`))}}async function H(e,n){const r=v(e.getAccountId(),e.getRegion());try{await e.getClient(P).send(new Q({Bucket:r,Key:n}),{abortSignal:S()})}catch(t){O.warn(j,"Failed to delete staged retain-flip template",{bucket:r,key:n,error:w(b(t))})}}export{D as FORGET_FLIP_SPEC,k as FORGET_MARKER_KEY,we as RECREATE_MARKER_KEY,ke as applyRetainFlip,ue as classifyProofViolations,F as composeFlipCapabilities,B as computeTemplateDigest,H as deleteStagedTemplate,Re as deriveRetainFlipTemplate,U as echoPreviousParameters,be as findMarkedResourcesByOpId,$e as proveFlipMetadataOnly,Ae as readResourceTypes,le as readRetainFlipState,Y as resolveTemplateLocation,Ie as verifyRetainFlipDiff};
@@ -18,6 +18,10 @@ export type SpawnResult = {
18
18
  type: "timeout";
19
19
  stdout: string;
20
20
  stderr: string;
21
+ } | {
22
+ type: "aborted";
23
+ stdout: string;
24
+ stderr: string;
21
25
  } | {
22
26
  type: "enoent";
23
27
  error: Error;
@@ -35,6 +39,10 @@ export declare function destroyProcessStreams(childProcess: ReturnType<typeof sp
35
39
  export declare function isEnoentError(err: unknown): err is NodeJS.ErrnoException;
36
40
  /**
37
41
  * Spawn a child process with timeout handling and stream collection.
42
+ *
43
+ * `abortSignal` is the caller's cancellation signal (`DeployParams.abortSignal`
44
+ * — Ctrl-C, or `fjall deployments cancel`). Without it a cancelled deploy still
45
+ * waits out the full build timeout, because nothing else can reach the child.
38
46
  */
39
47
  export declare function spawnWithTimeout(options: {
40
48
  command: string;
@@ -42,6 +50,7 @@ export declare function spawnWithTimeout(options: {
42
50
  cwd: string;
43
51
  env: Record<string, string | undefined>;
44
52
  timeout: number;
53
+ abortSignal?: AbortSignal;
45
54
  onStdoutData?: (data: string) => void;
46
55
  onStderrData?: (data: string) => void;
47
56
  }): Promise<SpawnResult>;
@@ -1 +1 @@
1
- import{spawn as S}from"child_process";import{logger as a}from"@fjall/util/logger";function w(o){try{o.stdout?.destroy()}catch(r){a.debug("spawnHelpers","stdout destroy failed",{error:String(r)})}try{o.stderr?.destroy()}catch(r){a.debug("spawnHelpers","stderr destroy failed",{error:String(r)})}}function E(o){return o instanceof Error&&"code"in o&&o.code==="ENOENT"}function k(o){const{command:r,args:p,cwd:l,env:m,timeout:f,onStdoutData:y,onStderrData:g}=o;return new Promise(h=>{let u=!1;const n=t=>{u||(u=!0,h(t))},e=S(r,p,{cwd:l,shell:!1,stdio:["ignore","pipe","pipe"],env:m}),i=[],d=[];e.stdout?.on("data",t=>{const s=t.toString();i.push(s),y?.(s)}),e.stderr?.on("data",t=>{const s=t.toString();d.push(s),g?.(s)});const c=setTimeout(()=>{w(e),e.kill("SIGTERM"),n({type:"timeout",stdout:i.join(""),stderr:d.join("")})},f);e.on("close",t=>{clearTimeout(c),n({type:"success",code:t??1,stdout:i.join(""),stderr:d.join("")})}),e.on("error",t=>{clearTimeout(c),E(t)?n({type:"enoent",error:t}):n({type:"spawn_error",error:t})})})}export{w as destroyProcessStreams,E as isEnoentError,k as spawnWithTimeout};
1
+ import{spawn as j}from"child_process";import{logger as l}from"@fjall/util/logger";function m(e){try{e.stdout?.destroy()}catch(s){l.debug("spawnHelpers","stdout destroy failed",{error:String(s)})}try{e.stderr?.destroy()}catch(s){l.debug("spawnHelpers","stderr destroy failed",{error:String(s)})}}function v(e){return e instanceof Error&&"code"in e&&e.code==="ENOENT"}function x(e){const{command:s,args:f,cwd:y,env:g,timeout:S,abortSignal:a,onStdoutData:b,onStderrData:E}=e;return new Promise(h=>{let c=!1;const r=t=>{c||(c=!0,h(t))};if(a?.aborted===!0){r({type:"aborted",stdout:"",stderr:""});return}const o=j(s,f,{cwd:y,shell:!1,stdio:["ignore","pipe","pipe"],env:g}),d=[],i=[];o.stdout?.on("data",t=>{const n=t.toString();d.push(n),b?.(n)}),o.stderr?.on("data",t=>{const n=t.toString();i.push(n),E?.(n)});const w=setTimeout(()=>{m(o),o.kill("SIGTERM"),u(),r({type:"timeout",stdout:d.join(""),stderr:i.join("")})},S),p=()=>{m(o),o.kill("SIGTERM"),u(),r({type:"aborted",stdout:d.join(""),stderr:i.join("")})},u=()=>{clearTimeout(w),a?.removeEventListener("abort",p)};a?.addEventListener("abort",p,{once:!0}),o.on("close",t=>{u(),r({type:"success",code:t??1,stdout:d.join(""),stderr:i.join("")})}),o.on("error",t=>{u(),v(t)?r({type:"enoent",error:t}):r({type:"spawn_error",error:t})})})}export{m as destroyProcessStreams,v as isEnoentError,x as spawnWithTimeout};
@@ -3,6 +3,8 @@ import type { CloudFormationService } from "../infrastructure/CloudFormationServ
3
3
  import type { AwsProvider } from "../../aws/AwsProvider.js";
4
4
  import type { DeployCallbacks } from "../../types/callbacks.js";
5
5
  import type { ResourceEvent } from "../../types/events.js";
6
+ import type { DeployParams } from "../../types/params.js";
7
+ import { type LeaseDeniedError } from "../../orchestration/application/leaseGate.js";
6
8
  import type { DeploymentContext } from "../../types/deployment/DeploymentTypes.js";
7
9
  import { type ApplicationStack } from "../../types/operations.js";
8
10
  import type { ParallelDeploymentResult } from "../../types/deployment/parallel.js";
@@ -62,7 +64,7 @@ export declare class ApplicationStackService {
62
64
  /**
63
65
  * Destroy a specific stack type
64
66
  */
65
- destroyStack(stackType: ApplicationStack, context: DeploymentContext, callbacks?: StackCallbacks, useCdkOut?: boolean, abortSignal?: AbortSignal): Promise<Result<StackDeploymentData, ApplicationError>>;
67
+ destroyStack(stackType: ApplicationStack, context: DeploymentContext, callbacks?: StackCallbacks, useCdkOut?: boolean, abortSignal?: AbortSignal, assertLeaseForStack?: DeployParams["assertLeaseForStack"]): Promise<Result<StackDeploymentData, ApplicationError | LeaseDeniedError>>;
66
68
  /**
67
69
  * Destroy multiple stacks in parallel within a destroy phase.
68
70
  *
@@ -84,8 +86,8 @@ export declare class ApplicationStackService {
84
86
  * Destroy all stacks for an application in reverse order.
85
87
  * Supports parallel destruction for OpenNext patterns (nextjs/payload).
86
88
  */
87
- destroyAllStacks(context: DeploymentContext, callbacks?: DestroyAllStacksCallbacks, resources?: AppResourceFlags, abortSignal?: AbortSignal): Promise<Result<{
89
+ destroyAllStacks(context: DeploymentContext, callbacks?: DestroyAllStacksCallbacks, resources?: AppResourceFlags, abortSignal?: AbortSignal, assertLeaseForStack?: DeployParams["assertLeaseForStack"]): Promise<Result<{
88
90
  message: string;
89
- }, ApplicationError>>;
91
+ }, ApplicationError | LeaseDeniedError>>;
90
92
  }
91
93
  export {};
@@ -1 +1 @@
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};
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{assertLeaseBeforeStack as B}from"../../orchestration/application/leaseGate.js";import{getApplicationStackName as h}from"../../types/operations.js";import{success as f,failure as y}from"@fjall/generator";import{ApplicationError as w}from"../../types/application/ApplicationServiceTypes.js";import{logger as l}from"@fjall/util/logger";import{maskSensitiveOutput as P}from"@fjall/util";import{convertCloudFormationOutputsToRecord as O}from"../supporting/helpers.js";import{destroyAllStacks as E,mapSettledResults as A,resolveWebsiteUrl as $}from"./applicationStackHelpers.js";class J{cdkService;cloudFormationService;aws;constructor(e,o,t){this.cdkService=e,this.cloudFormationService=o,this.aws=t}async runCdkSynth(e){return this.cdkService.runCdkSynth(e)}async deployStack(e,o,t,n){const u=o.target,i=h(u,e),c=await this.cdkService.runCdkDeploy(o,i,t?.onOutput,t?.onResourceProgress,this.aws,void 0,n?.parameters);if(c.success){const s=c.data;l.debug("ApplicationStackService","CDK deploy result",{stackName:i,stackType:e,success:!0,message:s.message,status:s.status,skipped:s.details?.skipped,hasOutput:!!s.details?.output});const a=await this.cloudFormationService.getStackOutputs(i);l.debug("ApplicationStackService","Stack outputs after deploy",{stackName:i,hasOutputs:a.success&&a.data.length>0,outputCount:a.success?a.data.length:0});const r=a.success&&a.data.length>0?O(a.data):void 0;return f({stackType:e,stackName:i,skipped:s.details?.skipped===!0,outputs:r})}else{const s=P(c.error||`Failed to deploy ${e} infrastructure`);return l.error("ApplicationStackService","Stack deployment failed",{stackType:e,target:o.target,error:s}),y(new w(s,{errorType:"deployment_failed",appName:o.target,operation:"deployStack",stackType:e}))}}async deployStacksInParallel(e,o,t){const n=t?.onOutput,u=t?.onResourceProgress,i=t?.onStackComplete,c=await Promise.allSettled(e.map(async s=>{const a=Date.now(),r=await this.deployStack(s,o,{onOutput:n?p=>n(p,s):void 0,onResourceProgress:u?p=>u(p,s):void 0}),S=Date.now()-a,d={stack:s,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&&l.debug("deployStacksInParallel",`Stack ${s} failed`,{error:r.error.message}),i?.(s,r.success,S,r.success?void 0:r.error),d}));return f(A(c,e))}async destroyStack(e,o,t,n,u,i){const c=o.target,s=h(c,e);let a=[],r=n===!0;this.aws!==void 0&&!r&&(r=(await this.cdkService.runCdkSynth(o)).success);const S=await B(i,s,u,"destroy");if(S!==void 0)return y(S);if(this.aws!==void 0)if(r){const m=await N(this.aws.getClient(R),this.aws.getClient(v),s,{onLog:t?.onLog,onStackCleanupProgress:t?.onStackCleanupProgress},u);a=D(m)}else l.debug("ApplicationStackService","Skipping pre-empty pass after failed validation synth \u2014 the destroy will surface the error",{stackName:s});else l.debug("ApplicationStackService","No AWS provider available, skipping pre-empty pass",{stackName:s});const d=n===!0||r&&o.assemblyDir===void 0,p=await this.cdkService.runCdkDestroy(o,s,t?.onOutput,t?.onResourceProgress,this.aws,d);if(p.success)return this.aws!==void 0&&a.length>0&&await F(this.aws.getClient(v),a,{onLog:t?.onLog,onStackCleanupProgress:t?.onStackCleanupProgress},u),f({stackType:e,stackName:s,skipped:!1,outputs:void 0});{const m=P(p.error||`Failed to destroy ${e} stack`);return l.error("ApplicationStackService","Stack destroy failed",{stackType:e,target:o.target,error:m}),y(new w(m,{errorType:"destroy_failed",appName:o.target,operation:"destroyStack",stackType:e}))}}async destroyStacksInParallel(e,o,t,n,u){const i=t?.onOutput,c=t?.onResourceProgress,s=t?.onStackComplete,a=await Promise.allSettled(e.map(async r=>{const S=Date.now(),d=await this.destroyStack(r,o,{onOutput:i?k=>i(k,r):void 0,onResourceProgress:c?k=>c(k,r):void 0,onLog:t?.onLog,onStackCleanupProgress:t?.onStackCleanupProgress},n,u),p=Date.now()-S,m=d.success?"":d.error?.message??"",C=m.includes(_)||m.includes(L),g={stack:r,success:d.success||C,error:d.success||C?void 0:d.error,duration:p};return!g.success&&g.error&&l.debug("destroyStacksInParallel",`Stack ${r} failed`,{error:g.error.message}),s?.(r,g.success,p,g.success?void 0:g.error),g}));return f(A(a,e))}async getStackOutputs(e,o){const t=h(e,o),n=await this.cloudFormationService.getStackOutputs(t);return n.success?f(O(n.data)):y(new w(`Failed to get outputs for ${t}`,{errorType:"stack_error",appName:e,operation:"getStackOutputs",stackType:o,details:n.error}))}async resolveWebsiteUrl(e){return $(e,this.cloudFormationService)}async destroyAllStacks(e,o,t,n,u){return E(this,e,o,t,n,u)}}export{J as ApplicationStackService};
@@ -8,6 +8,8 @@ import { ApplicationError, type StackDeploymentData } from "../../types/applicat
8
8
  import type { ApplicationStackService } from "./ApplicationStackService.js";
9
9
  import type { CloudFormationService } from "../infrastructure/CloudFormationService.js";
10
10
  import type { DeployCallbacks } from "../../types/callbacks.js";
11
+ import type { DeployParams } from "../../types/params.js";
12
+ import { LeaseDeniedError } from "../../orchestration/application/leaseGate.js";
11
13
  /** Shared with ApplicationStackService.destroyAllStacks — the two signatures must not drift. */
12
14
  export interface DestroyAllStacksCallbacks {
13
15
  onOutput?: (chunk: string, stackId?: ApplicationStack) => void;
@@ -15,30 +17,30 @@ export interface DestroyAllStacksCallbacks {
15
17
  onLog?: DeployCallbacks["onLog"];
16
18
  onStackCleanupProgress?: DeployCallbacks["onStackCleanupProgress"];
17
19
  onStackStart?: (stackType: ApplicationStack, stackName: string) => void;
18
- onStackComplete?: (stackType: ApplicationStack, result: Result<StackDeploymentData, ApplicationError>) => void | Promise<void>;
20
+ onStackComplete?: (stackType: ApplicationStack, result: Result<StackDeploymentData, ApplicationError | LeaseDeniedError>) => void | Promise<void>;
19
21
  onParallelPhaseStart?: (stacks: readonly ApplicationStack[], description: string) => void;
20
22
  onParallelPhaseComplete?: (results: ParallelDeploymentResult[]) => void;
21
23
  }
22
24
  /**
23
25
  * Convert "doesn't exist" errors to success for destroy operations.
24
26
  */
25
- export declare function convertDestroyResultIfNotExists(result: Result<StackDeploymentData, ApplicationError>, stackType: ApplicationStack, appName: string): Result<StackDeploymentData, ApplicationError>;
27
+ export declare function convertDestroyResultIfNotExists(result: Result<StackDeploymentData, ApplicationError | LeaseDeniedError>, stackType: ApplicationStack, appName: string): Result<StackDeploymentData, ApplicationError | LeaseDeniedError>;
26
28
  /**
27
29
  * Handle destroy errors and determine if we should continue or fail
28
30
  */
29
- export declare function handleDestroyError(result: Result<StackDeploymentData, ApplicationError>, stackType: ApplicationStack): {
31
+ export declare function handleDestroyError(result: Result<StackDeploymentData, ApplicationError | LeaseDeniedError>, stackType: ApplicationStack): {
30
32
  continue: boolean;
31
33
  result: Result<{
32
34
  message: string;
33
- }, ApplicationError>;
35
+ }, ApplicationError | LeaseDeniedError>;
34
36
  } | null;
35
37
  /**
36
38
  * Destroy all stacks for an application in reverse order.
37
39
  * Supports parallel destruction for OpenNext patterns (nextjs/payload).
38
40
  */
39
- export declare function destroyAllStacks(service: ApplicationStackService, context: DeploymentContext, callbacks?: DestroyAllStacksCallbacks, resources?: AppResourceFlags, abortSignal?: AbortSignal): Promise<Result<{
41
+ export declare function destroyAllStacks(service: ApplicationStackService, context: DeploymentContext, callbacks?: DestroyAllStacksCallbacks, resources?: AppResourceFlags, abortSignal?: AbortSignal, assertLeaseForStack?: DeployParams["assertLeaseForStack"]): Promise<Result<{
40
42
  message: string;
41
- }, ApplicationError>>;
43
+ }, ApplicationError | LeaseDeniedError>>;
42
44
  /**
43
45
  * Map Promise.allSettled results to ParallelDeploymentResult[], handling
44
46
  * rejected promises with a fallback stack identity.
@@ -1,4 +1,4 @@
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}
1
+ import{maskSensitiveOutput as k}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as A,CDK_NO_STACKS_MATCH as _,INFRASTRUCTURE_FILENAME as I}from"../../types/constants.js";import{APPLICATION_STACKS as L,getApplicationStackName as g,getApplicationDestroyOrder as V,getParallelDestroyGroups as v}from"../../types/operations.js";import{FrameworkRegistry as B}from"../../orchestration/builders/frameworkRegistry.js";import{success as T,failure as l,isFailure as $}from"@fjall/generator";import{ApplicationError as h}from"../../types/application/ApplicationServiceTypes.js";import{assertLeaseBeforeStack as E,LeaseDeniedError as j}from"../../orchestration/application/leaseGate.js";import{logger as x}from"@fjall/util/logger";function U(t,r,e){if(t.success)return t;const n=$(t)?t.error.message:"";return n.includes(A)||n.includes(_)?T({stackName:g(e,r),stackType:r,outputs:{},skipped:!0}):t}function K(t,r){if(t.success)return null;if($(t)&&t.error instanceof j)return{continue:!1,result:l(t.error)};const e=$(t)?k(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 ${I} for syntax errors`;return{continue:!1,result:l(new h(`CDK synthesis failed: ${s}`,{errorType:"synth_failed",operation:`destroy-${r}`}))}}return e.includes(A)||e.includes(_)?{continue:!0,result:T({message:"Stack already deleted"})}:{continue:!1,result:l(new h(`Failed to destroy ${r} stack: ${e}`,{errorType:"destroy_failed",operation:`destroy-${r}`}))}}async function X(t,r,e,n,s,p){const o=r.target,O=B.createDefault().resolve({appPath:r.path});let N,R;if(O){const u=O.builder.plan({appPath:r.path},O.detection);N=u.parallelDestroy,R=u.destroyOrder}else N=!1,R=V({pattern:null,resources:n});if(N){const u=v();for(const D of u){const c=D.stacks;if(c.length===1){const a=c[0],S=g(o,a),y=await E(p,S,s,"destroy");if(y!==void 0)return l(y);e?.onStackStart?.(a,S);const m=await t.destroyStack(a,r,{onOutput:e?.onOutput?f=>e.onOutput?.(f,a):void 0,onResourceProgress:e?.onResourceProgress?f=>e.onResourceProgress?.(f,a):void 0,onLog:e?.onLog,onStackCleanupProgress:e?.onStackCleanupProgress},void 0,s,p),i=U(m,a,o);e?.onStackComplete&&await e.onStackComplete(a,i);const d=K(i,a);if(d){if(d.continue)continue;return d.result}}else{const a=await E(p,g(o,c[0]),s,"destroy");if(a!==void 0)return l(a);const S=await t.runCdkSynth(r);if(!S.success)return l(new h(S.error,{errorType:"synth_failed",appName:o,operation:"destroy-parallel-synth"}));const y=await E(p,g(o,c[0]),s,"destroy");if(y!==void 0)return l(y);e?.onParallelPhaseStart?.(c,D.description);for(const i of c)e?.onStackStart?.(i,g(o,i));const m=await t.destroyStacksInParallel(c,r,{onOutput:e?.onOutput,onResourceProgress:e?.onResourceProgress,onLog:e?.onLog,onStackCleanupProgress:e?.onStackCleanupProgress,onStackComplete:async(i,d,f,C)=>{const P=C?.message!==void 0?k(C.message):"Stack destruction failed",M=d?T({stackName:g(o,i),stackType:i,outputs:{}}):l(C instanceof h?C:new h(P,{errorType:"destroy_failed",appName:o,operation:`destroy-${i}`}));await e?.onStackComplete?.(i,M)}},!0,s);if(m.success){e?.onParallelPhaseComplete?.(m.data);const i=m.data.filter(d=>!d.success);if(i.length>0){const d=i.filter(f=>f.error&&!f.error.message.includes(A)&&!f.error.message.includes(_));if(d.length>0){const f=d.map(P=>P.stack).join(", "),C=d.map(P=>`${P.stack}: ${P.error?.message??"Unknown error"}`).join(`
2
+ `);return l(new h(`Failed to destroy stacks: ${f}
3
3
 
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};
4
+ ${C}`,{errorType:"destroy_failed",appName:o,operation:"destroy-parallel"}))}}}}}}else for(const u of R){const D=g(o,u),c=await E(p,D,s,"destroy");if(c!==void 0)return l(c);e?.onStackStart?.(u,D);const a=await t.destroyStack(u,r,{onOutput:e?.onOutput?m=>e.onOutput?.(m,u):void 0,onResourceProgress:e?.onResourceProgress?m=>e.onResourceProgress?.(m,u):void 0,onLog:e?.onLog,onStackCleanupProgress:e?.onStackCleanupProgress},void 0,s,p),S=U(a,u,o);e?.onStackComplete&&await e.onStackComplete(u,S);const y=K(S,u);if(y){if(y.continue)continue;return y.result}}return T({message:"All stacks destroyed"})}function Y(t,r){return t.map((e,n)=>e.status==="fulfilled"?e.value:{stack:r[n]??L.NETWORK,success:!1,error:e.reason instanceof Error?e.reason:new Error(String(e.reason)),duration:0})}async function Z(t,r){try{const e=g(t,L.COMPUTE),n=await r.getStackOutputs(e);if(n.success&&n.data.length>0){const o=n.data.find(O=>O.OutputKey?.includes("LoadBalancerUrl"));if(o?.OutputValue)return o.OutputValue.toLowerCase();const w=n.data.find(O=>O.OutputKey?.includes("LoadBalancerDnsName"));if(w?.OutputValue)return`http://${w.OutputValue.toLowerCase()}`}const s=g(t,L.CDN),p=await r.getStackOutputs(s);if(p.success&&p.data.length>0){const o=p.data.find(w=>w.OutputKey?.includes("DistributionDomainName"));if(o?.OutputValue)return`https://${o.OutputValue.toLowerCase()}`}return}catch(e){x.warn("ApplicationStackService","URL resolution failed",{error:k(e instanceof Error?e.message:String(e))});return}}export{U as convertDestroyResultIfNotExists,X as destroyAllStacks,K as handleDestroyError,Y as mapSettledResults,Z as resolveWebsiteUrl};
@@ -1,5 +1,5 @@
1
- export { CdkService, CdkArgumentBuilder, CdkProcessManager, CdkEventMonitor, startStackMonitoring, DEFAULT_DEPLOY_TIMEOUT_MS, isCdkError, formatInfrastructureError, getStructuralHint, getSourceContext, hasCdkDifferences, parseDiffOutput, CloudFormationService, CloudFormationError, EcsService, EcsError, EcsServiceResolver, type CfnExportsClient } from "./infrastructure/index.js";
2
- export type { CdkContext, CdkOptions, CdkOutput, CheckDifferencesResult, CdkServiceOptions, ICdkProcessManager, DiffDetails, CloudFormationCallbacks, ECSServiceInfo, ECSClusterInfo, DeploymentStatus, ECSDeploymentOptions } from "./infrastructure/index.js";
1
+ export { CdkService, CdkArgumentBuilder, CdkProcessManager, CdkEventMonitor, startStackMonitoring, DEFAULT_DEPLOY_TIMEOUT_MS, isCdkError, formatInfrastructureError, getStructuralHint, getSourceContext, hasCdkDifferences, parseDiffOutput, CloudFormationService, CloudFormationError, CfnRegistryService, CFN_REGISTRY_SEED, EcsService, EcsError, EcsServiceResolver, type CfnExportsClient } from "./infrastructure/index.js";
2
+ export type { CdkContext, CdkOptions, CdkOutput, CheckDifferencesResult, CdkServiceOptions, ICdkProcessManager, DiffDetails, CloudFormationCallbacks, CfnRegistrySchemaSummary, CfnRegistryServiceOptions, CfnRegistrySummaryReader, ECSServiceInfo, ECSClusterInfo, DeploymentStatus, ECSDeploymentOptions } from "./infrastructure/index.js";
3
3
  export { TemplateHashService, TemplateHashError, type TemplateComparisonResult } from "./supporting/index.js";
4
4
  export { CdkContextBuilder } from "./supporting/index.js";
5
5
  export { emitProgress, PROGRESS_MESSAGES, parseBuildPhase, buildStepContextBuildConfig, convertCloudFormationOutputsToRecord, type CloudFormationOutput } from "./supporting/index.js";
@@ -1 +1 @@
1
- import{CdkService as o,CdkArgumentBuilder as t,CdkProcessManager as i,CdkEventMonitor as a,startStackMonitoring as c,DEFAULT_DEPLOY_TIMEOUT_MS as s,isCdkError as n,formatInfrastructureError as u,getStructuralHint as S,getSourceContext as d,hasCdkDifferences as E,parseDiffOutput as p,CloudFormationService as C,CloudFormationError as l,EcsService as m,EcsError as f,EcsServiceResolver as k}from"./infrastructure/index.js";import{TemplateHashService as x,TemplateHashError as g}from"./supporting/index.js";import{CdkContextBuilder as T}from"./supporting/index.js";import{emitProgress as P,PROGRESS_MESSAGES as h,parseBuildPhase as A,buildStepContextBuildConfig as B,convertCloudFormationOutputsToRecord as D}from"./supporting/index.js";import{ApplicationStackService as R}from"./application/index.js";export{R as ApplicationStackService,t as CdkArgumentBuilder,T as CdkContextBuilder,a as CdkEventMonitor,i as CdkProcessManager,o as CdkService,l as CloudFormationError,C as CloudFormationService,s as DEFAULT_DEPLOY_TIMEOUT_MS,f as EcsError,m as EcsService,k as EcsServiceResolver,h as PROGRESS_MESSAGES,g as TemplateHashError,x as TemplateHashService,B as buildStepContextBuildConfig,D as convertCloudFormationOutputsToRecord,P as emitProgress,u as formatInfrastructureError,d as getSourceContext,S as getStructuralHint,E as hasCdkDifferences,n as isCdkError,A as parseBuildPhase,p as parseDiffOutput,c as startStackMonitoring};
1
+ import{CdkService as o,CdkArgumentBuilder as t,CdkProcessManager as i,CdkEventMonitor as a,startStackMonitoring as c,DEFAULT_DEPLOY_TIMEOUT_MS as s,isCdkError as S,formatInfrastructureError as n,getStructuralHint as u,getSourceContext as E,hasCdkDifferences as d,parseDiffOutput as C,CloudFormationService as p,CloudFormationError as f,CfnRegistryService as l,CFN_REGISTRY_SEED as m,EcsService as v,EcsError as k,EcsServiceResolver as g}from"./infrastructure/index.js";import{TemplateHashService as R,TemplateHashError as T}from"./supporting/index.js";import{CdkContextBuilder as _}from"./supporting/index.js";import{emitProgress as F,PROGRESS_MESSAGES as O,parseBuildPhase as P,buildStepContextBuildConfig as h,convertCloudFormationOutputsToRecord as A}from"./supporting/index.js";import{ApplicationStackService as G}from"./application/index.js";export{G as ApplicationStackService,m as CFN_REGISTRY_SEED,t as CdkArgumentBuilder,_ as CdkContextBuilder,a as CdkEventMonitor,i as CdkProcessManager,o as CdkService,l as CfnRegistryService,f as CloudFormationError,p as CloudFormationService,s as DEFAULT_DEPLOY_TIMEOUT_MS,k as EcsError,v as EcsService,g as EcsServiceResolver,O as PROGRESS_MESSAGES,T as TemplateHashError,R as TemplateHashService,h as buildStepContextBuildConfig,A as convertCloudFormationOutputsToRecord,F as emitProgress,n as formatInfrastructureError,E as getSourceContext,u as getStructuralHint,d as hasCdkDifferences,S as isCdkError,P as parseBuildPhase,C as parseDiffOutput,c as startStackMonitoring};
@@ -11,6 +11,21 @@ export declare function startStackMonitoring(monitor: CloudFormationEventMonitor
11
11
  failureReason?: string;
12
12
  logPath?: string;
13
13
  }>;
14
+ /** Grace budget for the monitor to finish analysing after a failed deploy. */
15
+ export declare const MONITORING_ANALYSIS_GRACE_MS = 10000;
16
+ /**
17
+ * Await the monitor's failure analysis after a failed CDK exit, bounded by a
18
+ * grace timeout so a hung monitor can never deadlock the deploy result (Q12).
19
+ *
20
+ * Two waits are required: `waitForStackComplete` resolves from its own poll
21
+ * loop the moment the stack reaches a terminal state, while the analysis is
22
+ * computed slightly LATER by the event poll loop (`handleStackCompletion`
23
+ * awaits a DescribeStacks before storing it) — so after the monitoring
24
+ * promise settles, the analysis is polled briefly within the same grace
25
+ * budget. A null `monitoringPromise` means monitoring never started (CDK
26
+ * failed pre-CloudFormation): return immediately, no analysis is coming.
27
+ */
28
+ export declare function settleMonitoringAnalysis(monitor: CloudFormationEventMonitor | null, monitoringPromise: Promise<unknown> | null, graceMs?: number): Promise<FailureAnalysis | null>;
14
29
  export declare class CdkEventMonitor {
15
30
  private eventLogWriterFactory?;
16
31
  private onFailureAnalysis?;
@@ -1 +1 @@
1
- import{logger as u}from"@fjall/util/logger";import{getErrorMessage as m}from"@fjall/util";import{CloudFormationFailureAnalyser as y}from"@fjall/util/aws";const g=18e5;function v(n,e,t){return n.waitForStackComplete(e,{timeout:g,pollInterval:2e3,onResourceUpdate:t,onStackComplete:(o,r)=>{}}).catch(o=>(u.debug("CdkService","Stack monitoring failed",{error:m(o)}),{success:!1,failureReason:"Monitoring failed"}))}class f{eventLogWriterFactory;onFailureAnalysis;constructor(e){this.eventLogWriterFactory=e?.eventLogWriterFactory,this.onFailureAnalysis=e?.onFailureAnalysis}async createEventMonitor(e,t,o,r,a){const s=await import("../../aws/utils/cloudformationEvents.js"),i=new s.CloudFormationEventMonitor(a,{failureAnalyser:new y,eventLogWriterFactory:this.eventLogWriterFactory,...this.onFailureAnalysis!==void 0?{onFailureAnalysis:this.onFailureAnalysis}:{}}),l=`${e}-${Date.now()}`,c=r.target||r.deployType||"deployment";return i.enableLogging(l,t,o,c),i}}export{f as CdkEventMonitor,g as DEFAULT_DEPLOY_TIMEOUT_MS,v as startStackMonitoring};
1
+ import{logger as u}from"@fjall/util/logger";import{getErrorMessage as y,sleep as d}from"@fjall/util";import{CloudFormationFailureAnalyser as g}from"@fjall/util/aws";const F=18e5;function L(t,e,n){return t.waitForStackComplete(e,{timeout:F,pollInterval:2e3,onResourceUpdate:n,onStackComplete:(o,r)=>{}}).catch(o=>(u.debug("CdkService","Stack monitoring failed",{error:y(o)}),{success:!1,failureReason:"Monitoring failed"}))}const m=1e4,p=200;async function S(t,e,n=m){if(t===null)return null;if(e===null)return t.getFailureAnalysis();const o=Date.now()+n;let r;const i=new Promise(a=>{r=setTimeout(()=>a("grace-elapsed"),n)});try{if(await Promise.race([e,i])==="grace-elapsed")return t.getFailureAnalysis()}finally{r!==void 0&&clearTimeout(r)}for(;t.getFailureAnalysis()===null&&Date.now()<o;)await d(p);return t.getFailureAnalysis()}class M{eventLogWriterFactory;onFailureAnalysis;constructor(e){this.eventLogWriterFactory=e?.eventLogWriterFactory,this.onFailureAnalysis=e?.onFailureAnalysis}async createEventMonitor(e,n,o,r,i){const a=await import("../../aws/utils/cloudformationEvents.js"),s=new a.CloudFormationEventMonitor(i,{failureAnalyser:new g,eventLogWriterFactory:this.eventLogWriterFactory,...this.onFailureAnalysis!==void 0?{onFailureAnalysis:this.onFailureAnalysis}:{}}),l=`${e}-${Date.now()}`,c=r.target||r.deployType||"deployment";return s.enableLogging(l,n,o,c),s}}export{M as CdkEventMonitor,F as DEFAULT_DEPLOY_TIMEOUT_MS,m as MONITORING_ANALYSIS_GRACE_MS,S as settleMonitoringAnalysis,L as startStackMonitoring};
@@ -1,3 +1,4 @@
1
+ import type { FailureAnalysis } from "@fjall/util/aws";
1
2
  import { type ResourceEvent, type CloudFormationEventMonitor } from "../../aws/utils/cloudformationEvents.js";
2
3
  import type { StepOutput } from "../../types/deployment/DeploymentTypes.js";
3
4
  import type { Result } from "@fjall/generator";
@@ -6,6 +7,13 @@ import type { CdkOutput } from "./CdkService.js";
6
7
  * Analyse CDK deploy output and classify the result as success/skip/failure.
7
8
  */
8
9
  export declare function analyseDeployOutput(cdkOutput: string, result: Result<CdkOutput, string>, stackName: string): Result<StepOutput, string>;
10
+ /**
11
+ * Compose the returned deploy error from the monitor's settled failure
12
+ * analysis (Q12: the analysed root cause reaches the RETURNED error, not just
13
+ * the `onFailureAnalysis` stream). The analysis is already masked at the
14
+ * monitor boundary (`maskFailureAnalysis`).
15
+ */
16
+ export declare function formatAnalysedDeployFailure(analysis: FailureAnalysis): string;
9
17
  /**
10
18
  * Classify a bootstrap error into a user-friendly message.
11
19
  */
@@ -1 +1,2 @@
1
- import{logger as p}from"@fjall/util/logger";import{maskSensitiveOutput as u}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as f,CDK_NO_STACKS_MATCH as o}from"../../aws/utils/cloudformationEvents.js";import{success as i,failure as l}from"@fjall/generator";import{formatInfrastructureError as k}from"./CdkErrorFormatter.js";import{startStackMonitoring as N}from"./CdkEventMonitoring.js";function x(e,t,s){const r=new RegExp(`${s}[:\\s|]*.*?(?:stack has no resources|skipping deployment)`,"i").test(e),c=e.includes("no changes")||e.includes("Stack is up to date")||e.includes("No changes to deploy"),m=e.includes(o)||!t.success&&t.error.includes(o);if(t.success)return i({message:r?"Stack has no resources, skipped":c?"Infrastructure already up to date":"Deployment successful",status:c?"NO_CHANGES":void 0,details:{output:e,skipped:r}});if(m)return i({message:"Stack not defined, skipped",status:"SKIPPED",details:{output:e,skipped:!0}});let n=u(t.error||"Deployment failed");if(e.includes("failed:")){const d=e.match(/failed: .*?: (.*?)(?:\n|$)/);d&&(n=u(d[1]))}return p.error("CdkService","CDK deployment returned failure",{errorMessage:n}),l(n)}function A(e){return e.includes("Access Denied")||e.includes("AccessDenied")?"Access denied. Check your AWS credentials and permissions":e.includes("ExpiredToken")?"AWS credentials have expired. Please refresh your credentials":e.includes("ENOTFOUND")||e.includes("ECONNREFUSED")?"Network error. Please check your internet connection and try again":e.includes("timeout")?"Bootstrap operation timed out":k(e)}function P(e){return e.success?i({message:"Stack destroyed successfully"}):e.error.includes(f)||e.error.includes(o)?i({message:"Stack already deleted or does not exist",details:{skipped:!0}}):l(e.error||"Destroy failed")}function S(e){const t=e.match(/Deploying stack\s+([^\s]+)/);if(t)return{detected:!0,stackName:t[1]};const s=e.match(/^([^\s|]+)\s*\|\s*\d+\/\d+/m);if(s)return{detected:!0,stackName:s[1]};const a=e.match(/arn:aws:cloudformation:[^:]+:[^:]+:stack\/([^/]+)\//);return a?{detected:!0,stackName:a[1]}:{detected:!1}}function T(e,t,s,a){return r=>{if(e.cdkOutput+=r,t?.(r),!e.stackDetected){const c=S(r);c.detected&&c.stackName&&(e.actualStackName=c.stackName,e.stackDetected=!0),e.stackDetected&&s&&!e.monitoringPromise&&(e.monitoringPromise=N(s,e.actualStackName,a))}}}export{A as analyseBootstrapError,x as analyseDeployOutput,P as analyseDestroyResult,T as createEnhancedOutputCallback,S as detectStackNameFromChunk};
1
+ import{logger as f}from"@fjall/util/logger";import{maskSensitiveOutput as u}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as m,CDK_NO_STACKS_MATCH as a}from"../../aws/utils/cloudformationEvents.js";import{success as i,failure as l}from"@fjall/generator";import{formatInfrastructureError as k}from"./CdkErrorFormatter.js";import{startStackMonitoring as S}from"./CdkEventMonitoring.js";function x(e,t,s){const c=new RegExp(`${s}[:\\s|]*.*?(?:stack has no resources|skipping deployment)`,"i").test(e),r=e.includes("no changes")||e.includes("Stack is up to date")||e.includes("No changes to deploy"),p=e.includes(a)||!t.success&&t.error.includes(a);if(t.success)return i({message:c?"Stack has no resources, skipped":r?"Infrastructure already up to date":"Deployment successful",status:r?"NO_CHANGES":void 0,details:{output:e,skipped:c}});if(p)return i({message:"Stack not defined, skipped",status:"SKIPPED",details:{output:e,skipped:!0}});let n=u(t.error||"Deployment failed");if(e.includes("failed:")){const d=e.match(/failed: .*?: (.*?)(?:\n|$)/);d&&(n=u(d[1]))}return f.error("CdkService","CDK deployment returned failure",{errorMessage:n}),l(n)}function A(e){const t=[e.summary],{resource:s,reason:o}=e.rootCause;if(t.push(`Root cause: ${s.logicalId} \u2014 ${o}`),e.driftSuspects!==void 0&&e.driftSuspects.length>0){const c=e.driftSuspects.map(r=>`${r.logicalId} (${r.resourceType})`).join(", ");t.push(`Out-of-band deletion suspected: ${c}. Run 'fjall drift detect' to confirm; confirmed deletions are remediated with 'fjall drift repair'.`)}return t.join(`
2
+ `)}function P(e){return e.includes("Access Denied")||e.includes("AccessDenied")?"Access denied. Check your AWS credentials and permissions":e.includes("ExpiredToken")?"AWS credentials have expired. Please refresh your credentials":e.includes("ENOTFOUND")||e.includes("ECONNREFUSED")?"Network error. Please check your internet connection and try again":e.includes("timeout")?"Bootstrap operation timed out":k(e)}function T(e){return e.success?i({message:"Stack destroyed successfully"}):e.error.includes(m)||e.error.includes(a)?i({message:"Stack already deleted or does not exist",details:{skipped:!0}}):l(e.error||"Destroy failed")}function N(e){const t=e.match(/Deploying stack\s+([^\s]+)/);if(t)return{detected:!0,stackName:t[1]};const s=e.match(/^([^\s|]+)\s*\|\s*\d+\/\d+/m);if(s)return{detected:!0,stackName:s[1]};const o=e.match(/arn:aws:cloudformation:[^:]+:[^:]+:stack\/([^/]+)\//);return o?{detected:!0,stackName:o[1]}:{detected:!1}}function R(e,t,s,o){return c=>{if(e.cdkOutput+=c,t?.(c),!e.stackDetected){const r=N(c);r.detected&&r.stackName&&(e.actualStackName=r.stackName,e.stackDetected=!0),e.stackDetected&&s&&!e.monitoringPromise&&(e.monitoringPromise=S(s,e.actualStackName,o))}}}export{P as analyseBootstrapError,x as analyseDeployOutput,T as analyseDestroyResult,R as createEnhancedOutputCallback,N as detectStackNameFromChunk,A as formatAnalysedDeployFailure};