@fjall/deploy-core 2.25.0 → 2.29.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/index.d.ts +1 -1
  3. package/dist/src/aws/index.js +1 -1
  4. package/dist/src/aws/organisations/backup.d.ts +7 -2
  5. package/dist/src/aws/organisations/backup.js +2 -2
  6. package/dist/src/aws/organisations/index.d.ts +2 -2
  7. package/dist/src/aws/organisations/index.js +1 -1
  8. package/dist/src/aws/organisations/oidcProvider.d.ts +37 -0
  9. package/dist/src/aws/organisations/oidcProvider.js +1 -1
  10. package/dist/src/aws/organisations/organisation.d.ts +21 -0
  11. package/dist/src/aws/organisations/organisation.js +1 -1
  12. package/dist/src/aws/organisations/organisationalUnits.d.ts +15 -0
  13. package/dist/src/aws/organisations/organisationalUnits.js +1 -1
  14. package/dist/src/aws/utils/cloudformationEventHelpers.d.ts +45 -0
  15. package/dist/src/aws/utils/cloudformationEventHelpers.js +1 -1
  16. package/dist/src/aws/utils/cloudformationEventTypes.d.ts +1 -1
  17. package/dist/src/aws/utils/cloudformationEvents.d.ts +2 -1
  18. package/dist/src/aws/utils/cloudformationEvents.js +1 -1
  19. package/dist/src/aws/utils/stackStatus.js +1 -1
  20. package/dist/src/index.d.ts +13 -2
  21. package/dist/src/index.js +1 -1
  22. package/dist/src/orchestration/activeDeploymentGuard.d.ts +41 -0
  23. package/dist/src/orchestration/activeDeploymentGuard.js +8 -4
  24. package/dist/src/orchestration/application/applicationDeploy.d.ts +9 -0
  25. package/dist/src/orchestration/application/applicationDeploy.js +1 -1
  26. package/dist/src/orchestration/application/applicationDeployHelpers.d.ts +20 -3
  27. package/dist/src/orchestration/application/applicationDeployHelpers.js +3 -3
  28. package/dist/src/orchestration/application/applicationDestroy.js +1 -1
  29. package/dist/src/orchestration/application/approvalGate.d.ts +15 -1
  30. package/dist/src/orchestration/application/approvalGate.js +1 -1
  31. package/dist/src/orchestration/application/databaseEndpointReconcile.d.ts +77 -0
  32. package/dist/src/orchestration/application/databaseEndpointReconcile.js +1 -0
  33. package/dist/src/orchestration/application/deploySessionPolicy.d.ts +9 -1
  34. package/dist/src/orchestration/application/deploySessionPolicy.js +1 -1
  35. package/dist/src/orchestration/application/destructionGate.d.ts +164 -0
  36. package/dist/src/orchestration/application/destructionGate.js +5 -0
  37. package/dist/src/orchestration/application/dockerBuildHelper.js +1 -1
  38. package/dist/src/orchestration/application/ecrCacheRepository.d.ts +14 -0
  39. package/dist/src/orchestration/application/ecrCacheRepository.js +1 -0
  40. package/dist/src/orchestration/application/leaseGate.d.ts +30 -0
  41. package/dist/src/orchestration/application/leaseGate.js +1 -0
  42. package/dist/src/orchestration/application/plan/approvalToken.d.ts +15 -0
  43. package/dist/src/orchestration/application/plan/approvalToken.js +1 -1
  44. package/dist/src/orchestration/application/plan/changeSetEscalation.d.ts +46 -0
  45. package/dist/src/orchestration/application/plan/changeSetEscalation.js +1 -0
  46. package/dist/src/orchestration/application/plan/computeDeployPlan.d.ts +13 -0
  47. package/dist/src/orchestration/application/plan/computeDeployPlan.js +1 -1
  48. package/dist/src/orchestration/application/plan/index.d.ts +4 -1
  49. package/dist/src/orchestration/application/plan/index.js +1 -1
  50. package/dist/src/orchestration/application/plan/loadDeployPlan.d.ts +32 -0
  51. package/dist/src/orchestration/application/plan/loadDeployPlan.js +1 -0
  52. package/dist/src/orchestration/application/plan/pinnedNames.d.ts +31 -0
  53. package/dist/src/orchestration/application/plan/pinnedNames.js +1 -0
  54. package/dist/src/orchestration/application/plan/registryOverlay.d.ts +83 -0
  55. package/dist/src/orchestration/application/plan/registryOverlay.js +1 -0
  56. package/dist/src/orchestration/application/plan/renderDeployPlan.d.ts +4 -1
  57. package/dist/src/orchestration/application/plan/renderDeployPlan.js +1 -1
  58. package/dist/src/orchestration/application/plan/types.d.ts +31 -0
  59. package/dist/src/orchestration/application/plan/types.js +1 -0
  60. package/dist/src/orchestration/contextHelpers.d.ts +37 -1
  61. package/dist/src/orchestration/contextHelpers.js +1 -1
  62. package/dist/src/orchestration/drift/classifyDriftFailure.d.ts +31 -0
  63. package/dist/src/orchestration/drift/classifyDriftFailure.js +1 -0
  64. package/dist/src/orchestration/drift/detectStackDrift.d.ts +37 -0
  65. package/dist/src/orchestration/drift/detectStackDrift.js +1 -0
  66. package/dist/src/orchestration/drift/driftPreFlight.d.ts +73 -0
  67. package/dist/src/orchestration/drift/driftPreFlight.js +7 -0
  68. package/dist/src/orchestration/drift/driftProbe.d.ts +113 -0
  69. package/dist/src/orchestration/drift/driftProbe.js +1 -0
  70. package/dist/src/orchestration/drift/driftSuspectJournal.d.ts +40 -0
  71. package/dist/src/orchestration/drift/driftSuspectJournal.js +1 -0
  72. package/dist/src/orchestration/drift/preFlightGate.d.ts +44 -0
  73. package/dist/src/orchestration/drift/preFlightGate.js +2 -0
  74. package/dist/src/orchestration/index.d.ts +19 -0
  75. package/dist/src/orchestration/index.js +1 -1
  76. package/dist/src/orchestration/organisation/cascadeDestroyHelpers.d.ts +7 -0
  77. package/dist/src/orchestration/organisation/cascadeDestroyHelpers.js +1 -1
  78. package/dist/src/orchestration/organisation/cascadeHelpers.js +1 -1
  79. package/dist/src/orchestration/organisation/organisationDestroy.js +3 -3
  80. package/dist/src/orchestration/organisationDeploy/infraSteps.d.ts +8 -1
  81. package/dist/src/orchestration/organisationDeploy/infraSteps.js +1 -1
  82. package/dist/src/orchestration/organisationDeploy/orgCascadeDeploy.js +5 -5
  83. package/dist/src/orchestration/organisationDeploy/orgContext.d.ts +28 -2
  84. package/dist/src/orchestration/organisationDeploy/orgContext.js +1 -1
  85. package/dist/src/orchestration/organisationDeploy/singleComponentDeploy.js +1 -1
  86. package/dist/src/orchestration/remediation/driftRepairGate.d.ts +66 -0
  87. package/dist/src/orchestration/remediation/driftRepairGate.js +1 -0
  88. package/dist/src/orchestration/remediation/forensicsCapture.d.ts +49 -0
  89. package/dist/src/orchestration/remediation/forensicsCapture.js +1 -0
  90. package/dist/src/orchestration/remediation/forgetResource.d.ts +423 -0
  91. package/dist/src/orchestration/remediation/forgetResource.js +1 -0
  92. package/dist/src/orchestration/remediation/pinRemediation.d.ts +121 -0
  93. package/dist/src/orchestration/remediation/pinRemediation.js +1 -0
  94. package/dist/src/orchestration/remediation/postFailureRepairOffer.d.ts +77 -0
  95. package/dist/src/orchestration/remediation/postFailureRepairOffer.js +14 -0
  96. package/dist/src/orchestration/remediation/recreatePreFlight.d.ts +47 -0
  97. package/dist/src/orchestration/remediation/recreatePreFlight.js +1 -0
  98. package/dist/src/orchestration/remediation/recreateResource.d.ts +110 -0
  99. package/dist/src/orchestration/remediation/recreateResource.js +1 -0
  100. package/dist/src/orchestration/remediation/remediationJournal.d.ts +88 -0
  101. package/dist/src/orchestration/remediation/remediationJournal.js +2 -0
  102. package/dist/src/orchestration/remediation/removalUpdate.d.ts +135 -0
  103. package/dist/src/orchestration/remediation/removalUpdate.js +1 -0
  104. package/dist/src/orchestration/remediation/retainFlip.d.ts +294 -0
  105. package/dist/src/orchestration/remediation/retainFlip.js +1 -0
  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/CdkArgumentBuilder.js +1 -1
  113. package/dist/src/services/infrastructure/CdkEventMonitoring.d.ts +15 -0
  114. package/dist/src/services/infrastructure/CdkEventMonitoring.js +1 -1
  115. package/dist/src/services/infrastructure/CdkOutputAnalyser.d.ts +8 -0
  116. package/dist/src/services/infrastructure/CdkOutputAnalyser.js +2 -1
  117. package/dist/src/services/infrastructure/CdkService.js +3 -3
  118. package/dist/src/services/infrastructure/CdkServiceTypes.d.ts +7 -0
  119. package/dist/src/services/infrastructure/CfnRegistryService.d.ts +68 -0
  120. package/dist/src/services/infrastructure/CfnRegistryService.js +1 -0
  121. package/dist/src/services/infrastructure/CloudFormationService.d.ts +5 -2
  122. package/dist/src/services/infrastructure/CloudFormationService.js +1 -1
  123. package/dist/src/services/infrastructure/EcsServiceResolver.d.ts +23 -3
  124. package/dist/src/services/infrastructure/EcsServiceResolver.js +1 -1
  125. package/dist/src/services/infrastructure/cdkServiceHelpers.js +1 -1
  126. package/dist/src/services/infrastructure/cfnRegistrySeed.d.ts +10 -0
  127. package/dist/src/services/infrastructure/cfnRegistrySeed.js +1 -0
  128. package/dist/src/services/infrastructure/changeSetProbe.d.ts +88 -0
  129. package/dist/src/services/infrastructure/changeSetProbe.js +1 -0
  130. package/dist/src/services/infrastructure/index.d.ts +2 -0
  131. package/dist/src/services/infrastructure/index.js +1 -1
  132. package/dist/src/services/supporting/CdkContextBuilder.d.ts +1 -0
  133. package/dist/src/services/supporting/CdkContextBuilder.js +1 -1
  134. package/dist/src/types/callbackKeys.d.ts +1 -1
  135. package/dist/src/types/callbackKeys.js +1 -1
  136. package/dist/src/types/callbacks.d.ts +22 -0
  137. package/dist/src/types/config/entitlements.d.ts +9 -0
  138. package/dist/src/types/config/orgConfig.d.ts +6 -0
  139. package/dist/src/types/deployment/DeploymentTypes.d.ts +1 -0
  140. package/dist/src/types/deploymentEventSchema.d.ts +1 -0
  141. package/dist/src/types/deploymentEventSchema.js +1 -1
  142. package/dist/src/types/destruction.d.ts +212 -0
  143. package/dist/src/types/destruction.js +1 -0
  144. package/dist/src/types/drift.d.ts +72 -0
  145. package/dist/src/types/drift.js +1 -0
  146. package/dist/src/types/index.d.ts +8 -2
  147. package/dist/src/types/index.js +1 -1
  148. package/dist/src/types/params.d.ts +161 -0
  149. package/dist/src/types/remediation.d.ts +126 -0
  150. package/dist/src/types/remediation.js +1 -0
  151. package/package.json +5 -4
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Shared [G2] pre-flight gate — the deploy-path wrapper around
3
+ * `runDriftPreFlight` + `partitionPreFlightByRemediation`. The application
4
+ * path and the single-component (platform/account) path both run this gate
5
+ * between synth and the CFN update; the converge/recreate warns and the
6
+ * block message are user-facing [G2] copy that must stay byte-identical
7
+ * across the two surfaces, so the wrapper owns them once.
8
+ *
9
+ * Fail-open on probe infrastructure errors (the runDriftPreFlight contract):
10
+ * in the FINDING flow only a CONFIRMED out-of-band deletion with no covering
11
+ * remediation blocks. The flipless-by-necessity absence re-verification
12
+ * (`verifyByNecessityBeforeConverge`) is the exception and fails CLOSED —
13
+ * those targets carry no Retain policy, so a converge that cannot re-prove
14
+ * their absence would issue a real physical delete. Each caller renders the
15
+ * blocked outcome in its own failure idiom — the message is identifier-only
16
+ * (stack, logical ID, resource type, physical ID) and pre-masked by
17
+ * construction.
18
+ */
19
+ import type { AwsProvider } from "../../aws/AwsProvider.js";
20
+ import type { CloudFormationService } from "../../services/infrastructure/CloudFormationService.js";
21
+ import type { DeployCallbacks } from "../../types/callbacks.js";
22
+ import { type ByNecessityConvergeTarget, type ForgetOrchestrationOptions } from "../remediation/forgetResource.js";
23
+ export interface DriftPreFlightGateParams {
24
+ stackNames: readonly string[];
25
+ /** cdk.out assembly dir for the synth-omission converge check. */
26
+ cdkOutPath: string;
27
+ cfnService: Pick<CloudFormationService, "getStackStatus">;
28
+ callbacks: Pick<DeployCallbacks, "onLog">;
29
+ abortSignal?: AbortSignal;
30
+ /** Journal/clock injection for the remediation partition and the
31
+ * by-necessity re-verification (tests; production uses the defaults). */
32
+ remediationOptions?: ForgetOrchestrationOptions;
33
+ }
34
+ export type DriftPreFlightGateOutcome = {
35
+ kind: "proceed";
36
+ /** Flipless-by-necessity targets re-proven absent against their owning
37
+ * service by THIS pre-flight — the destruction gate consumes this to
38
+ * suppress their already-consented, physically-no-op delete rows. */
39
+ verifiedAbsentByNecessity?: ByNecessityConvergeTarget[];
40
+ } | {
41
+ kind: "blocked";
42
+ message: string;
43
+ };
44
+ export declare function runDriftPreFlightGate(aws: AwsProvider, params: DriftPreFlightGateParams): Promise<DriftPreFlightGateOutcome>;
@@ -0,0 +1,2 @@
1
+ import{getErrorMessage as g,maskSensitiveOutput as h}from"@fjall/util";import{logger as u}from"@fjall/util/logger";import{DriftProbe as m}from"./driftProbe.js";import{formatDriftPreFlightBlock as p,runDriftPreFlight as y}from"./driftPreFlight.js";import{formatRecreateInFlightCures as b,partitionPreFlightByRemediation as k,synthTemplateDeclaresResource as v,verifyByNecessityBeforeConverge as w}from"../remediation/forgetResource.js";function N(r){const t=r.map(o=>` - ${o.stackName} \u203A ${o.logicalId} (operation ${o.opId}): ${o.reason}`);return[`Deploy halted: ${r.length} resource(s) from a forget remediation cannot be safely removed by this deploy.`,...t,"Each was verified deleted when the forget was consented, but carries no Retain policy (the policy change was structurally impossible \u2014 flipless by necessity), so this deploy's removal would issue a real physical delete. fjall therefore re-verifies absence against the owning service at every converge, and could not confirm it here. If the resource was recreated deliberately, keep it by re-declaring it in the fjall configuration (CloudFormation keeps tracking it). If it should still be removed, delete it against the owning service first, then re-run the deploy. Nothing was mutated."].join(`
2
+ `)}async function P(r,t){const o=await y({aws:r,probe:new m(r)},{stackNames:t.stackNames,cfnService:t.cfnService,...t.abortSignal!==void 0?{abortSignal:t.abortSignal}:{}}).catch(e=>(u.debug("DriftPreFlightGate","Pre-flight probe threw \u2014 failing open",{error:h(g(e))}),null)),{confirmed:l,unconfirmed:d}=o!==null&&o.success?o.data:{confirmed:[],unconfirmed:[]},c={aws:r,synthDeclaresTarget:(e,i)=>v(t.cdkOutPath,e,i),...t.abortSignal!==void 0?{abortSignal:t.abortSignal}:{}};if(l.length>0){const e=await k(c,l,t.remediationOptions);for(const{finding:i,opId:s}of e.converging)t.callbacks.onLog?.(`Forget remediation ${s} is mid-surgery for ${i.stackName} \u203A ${i.logicalId} \u2014 this deploy is the converge deploy and will complete it`,"warn");for(const{finding:i,opId:s,appName:f}of e.recreateConverging)t.callbacks.onLog?.(`Recreate remediation ${s} covers ${i.stackName} \u203A ${i.logicalId} \u2014 the physical resource was deleted outside CloudFormation. This deploy re-runs the surgery only if the plan still requires the pinned-name replacement (re-consent when prompted). If the deploy completes without re-creating it, the operation stays open \u2014 ${b(f)}`,"warn");if(e.blocked.length>0)return{kind:"blocked",message:p(e.blocked,e.inProgressOpIds,{flipNotLive:e.flipNotLive,flipUnverified:e.flipUnverified,stillDeclared:e.stillDeclared})}}for(const e of d)t.callbacks.onLog?.(`Possible out-of-band deletion (unconfirmed): ${e.stackName} \u203A ${e.logicalId} \u2014 ${e.verdictDetail}`,"warn");const a=new Map;for(const e of l){const i=a.get(e.stackName)??new Set;i.add(e.logicalId),a.set(e.stackName,i)}const n=await w(c,t.stackNames,a,t.remediationOptions);return n.blocked.length>0?{kind:"blocked",message:N(n.blocked)}:{kind:"proceed",...n.verifiedAbsent.length>0?{verifiedAbsentByNecessity:n.verifiedAbsent}:{}}}export{P as runDriftPreFlightGate};
@@ -49,3 +49,22 @@ export * from "./builders/index.js";
49
49
  export { buildDeployPlan, computeDeployPlan, computeAssemblyDigest, digestHead, classifyImpact, derivePropertyChanges, isDataLoss, isStatefulResourceType, renderPlanSummary, renderPlanLines, toWirePlanChanges, signApprovalToken, verifyApprovalToken, DEFAULT_APPROVAL_TTL_MS, APPROVAL_TOKEN_PATTERN } from "./application/plan/index.js";
50
50
  export type { DeployPlan, DeployPlanResourceChange, StackTemplatePair, CfnTemplateReader, ComputeDeployPlanParams, ComputeDeployPlanStack, ImpactClassification, SignApprovalTokenOptions, SignedApprovalToken, VerifyApprovalTokenOptions, VerifyApprovalTokenResult } from "./application/plan/index.js";
51
51
  export { runApprovalGate, type RunApprovalGateParams, type ApprovalGateOutcome } from "./application/approvalGate.js";
52
+ export { runDestructionGate, buildDestructionTicket, evaluateDestructionConsents, computeTicketDigest, legalRemediationVerbs, executableRemediationVerbs, advertisableRemediationVerbs, renderDestructionTicketLines, renderConsentVerdictLines, type RunDestructionGateParams, type DestructionGateOutcome, type EvaluateDestructionConsentsParams } from "./application/destructionGate.js";
53
+ export { loadDeployPlan, createMemoisedPlanLoader, type LoadDeployPlanParams, type DeployPlanLoader } from "./application/plan/loadDeployPlan.js";
54
+ export { classifyDriftFailure, fetchLastOperationEvents } from "./drift/classifyDriftFailure.js";
55
+ export { DriftProbe, type DriftProbeOptions, type DriftProbeRunner, type ProbeDriftSuspectsParams } from "./drift/driftProbe.js";
56
+ export { clearDriftSuspects, defaultDriftJournalDir, readDriftSuspects, recordDriftSuspects, type DriftJournalOptions, type RecordDriftSuspectsParams } from "./drift/driftSuspectJournal.js";
57
+ export { detectStackDrift, type DetectStackDriftDeps, type DetectStackDriftParams, type StackDriftReport } from "./drift/detectStackDrift.js";
58
+ export { runDriftPreFlight, formatDriftPreFlightBlock, DRIFT_PREFLIGHT_TRIGGER_STATUSES, DRIFT_PREFLIGHT_BUDGET_MS, type RunDriftPreFlightParams, type DriftPreFlightBlockExtras, type DriftPreFlightOutcome } from "./drift/driftPreFlight.js";
59
+ export { archiveCompletedRemediationJournal, computeRemediationOpId, defaultRemediationJournalDir, findRemediationJournalByOpId, listActiveRemediationOps, listActiveRemediationOpsForContext, readRemediationJournal, sweepExpiredRemediationJournals, writeRemediationJournal, REMEDIATION_JOURNAL_RETENTION_DAYS, type RemediationJournalContext, type RemediationJournalOptions } from "./remediation/remediationJournal.js";
60
+ export { captureRemediationForensics, defaultRemediationForensicsDir, type CaptureRemediationForensicsParams, type ForensicsCaptureOptions, type RemediationForensicsBundle } from "./remediation/forensicsCapture.js";
61
+ export { applyRetainFlip, composeFlipCapabilities, computeTemplateDigest, deriveRetainFlipTemplate, findMarkedResourcesByOpId, proveFlipMetadataOnly, readRetainFlipState, verifyRetainFlipDiff, FORGET_MARKER_KEY, FORGET_FLIP_SPEC, RECREATE_MARKER_KEY, type ApplyRetainFlipOutcome, type ApplyRetainFlipParams, type DeriveRetainFlipParams, type FlipDeletionPolicy, type FlipProofResult, type FlipSpec, type RetainFlipDerivation, type RetainFlipState } from "./remediation/retainFlip.js";
62
+ export { assessForgetResumability, completeForgetAfterConverge, completeForgetAfterDeploy, formatRecreateInFlightCures, partitionPreFlightByRemediation, runForgetSurgery, synthTemplateDeclaresResource, type CompleteForgetAfterDeployParams, type CompleteForgetOutcome, type CompleteForgetParams, type ForeignMarkerOperation, type ForgetOrchestrationOptions, type ForgetResumeAssessment, type ForgetResumeSource, type ForgetSurgeryParams, type ForgetSurgeryReport, type PreFlightRemediationDeps, type PreFlightRemediationPartition } from "./remediation/forgetResource.js";
63
+ export { runRecreateSurgery, snapshotPolicyForType, SNAPSHOT_CAPABLE_RESOURCE_TYPES, type RecreateOrchestrationOptions, type RecreateSurgeryParams, type RecreateSurgeryReport } from "./remediation/recreateResource.js";
64
+ export { runRecreatePreFlight, type RecreatePreFlightStack, type RunRecreatePreFlightParams } from "./remediation/recreatePreFlight.js";
65
+ export { deriveRemovalTemplate, findRemovalDeletionFailures, proveRemovalTargetsOnly, type ProveRemovalTargetsOnlyParams, type RemovalDeletionFailure, type RemovalDerivation, type RemovalProofResult, type StrippedExport } from "./remediation/removalUpdate.js";
66
+ export { advertisableDriftVerbs, buildDriftRepairTicket, runDriftRepairGate, type DriftRepairGateOutcome, type RunDriftRepairGateParams } from "./remediation/driftRepairGate.js";
67
+ export { formatPinOutcomeDetail, pinProbeCoverage, renderPinReportLines, runPinRemediation, type PinnedProperty, type PinProbeCoverage, type PinRemediationReport, type PinReportOnlyReason, type PinResourceOutcome, type PinTarget, type RunPinRemediationParams } from "./remediation/pinRemediation.js";
68
+ export { composePostFailureRepairOffer, formatDriftRepairOffer, DriftRepairAvailableError, type ComposePostFailureRepairOfferParams } from "./remediation/postFailureRepairOffer.js";
69
+ export { checkDeployPathStackAvailability, isDeployPathBlockingStatus, StackUnavailableError } from "./activeDeploymentGuard.js";
70
+ export { assertLeaseBeforeStack, LeaseDeniedError } from "./application/leaseGate.js";
@@ -1 +1 @@
1
- import{deploy as o}from"./deploy.js";import{destroy as i}from"./destroy.js";import{restart as a,probeSecretsDrift as n}from"./restart/restartApplication.js";import{computeSecretsDrift as l,deriveStaleParameters as m,extractConsumedSsmParameters as u,ssmParameterNameFromValueFrom as p}from"./restart/secretsDrift.js";import{deployOrganisation as d}from"./organisation/organisationDeploy.js";import{destroyOrganisation as A}from"./organisation/organisationDestroy.js";import{cleanupFailedStack as T,emptyS3Bucket as P,preEmptyStackBuckets as _,formatQuarantineSuspectedMessage as y,formatRetainedBucketsMessage as g,isQuarantineDetail as E,isRetainedBucketsDetail as R,PRE_EMPTY_TAG_KEYS as B,isCleanableState as O,SAFE_CLEANUP_STATES as k}from"./stackCleanup.js";import{partitionAccounts as U,buildRegionList as C,buildAccountRegionPairs as L,cascadeHomeRegion as D,cascadeOperationKey as K}from"./organisation/cascadeHelpers.js";import{projectScalarSummary as b,projectAccountRows as F}from"./organisation/cascadeSummary.js";import{reconcileProviderAccounts as M,mergeReconciledProviderAccounts as G}from"./organisation/reconcileProviderAccounts.js";import{decideNextTransition as Q,reconcileTrailMigration as V,ORG_TRAIL_BUCKET_OUTPUT_KEY as h,TRAIL_BUCKET_OUTPUT_KEY as H,TRAIL_KEY_ARN_OUTPUT_KEY as j}from"./trailMigration/trailMigration.js";import{decommissionMemberTrailStorage as W}from"./trailMigration/memberTrailCleanup.js";import{unlockBucket as z}from"./unlock/unlockBucket.js";import{unlockQueue as X}from"./unlock/unlockQueue.js";import{triageBucketPolicy as $,isEnforceSslStatement as ee}from"./unlock/bucketPolicyTriage.js";import{toResourcePolicyStatements as oe}from"./unlock/toResourcePolicyStatements.js";import{restoreBucketPolicy as ie,synthesiseEnforceSslDocument as se,ensureEnforceSsl as ae}from"./unlock/restoreBucketPolicy.js";import{restoreAndReconcileQuarantinedBucket as ce}from"./unlock/restoreAndReconcileQuarantinedBucket.js";import{parseAccountsConfiguration as me,flattenAccountsToEnvironments as ue,extractAllAccountNames as pe,accountsConfigToOUTree as fe,isStringArray as de,isAccountsConfig as Se,isOuOnlyAccountBucket as Ae,OU_ONLY_ACCOUNT_BUCKETS as xe}from"./organisation/accountsConfig.js";import{resolveBuildSecrets as Pe,resolveSecretRefValue as _e,resolveBuildSecretSessionProvider as ye,sourcedRefsFromBuildArgs as ge}from"./application/buildSecretResolver.js";import{buildBuildSecretSessionPolicy as Re,partitionForRegion as Be}from"./application/buildSecretSession.js";import{buildDeploySessionPolicy as ke}from"./application/deploySessionPolicy.js";import{resolveBuildArgs as Ue}from"./application/buildArgResolver.js";import{validateBuildGroup as Le}from"./application/buildGroupValidator.js";import{runOpenNextBuild as Ke}from"./application/openNextBuild.js";import{runOrganisationSetup as be,ORG_SETUP_PHASES as Fe}from"./organisation/organisationSetup.js";export*from"./builders/index.js";import{buildDeployPlan as Ge,computeDeployPlan as Ie,computeAssemblyDigest as Qe,digestHead as Ve,classifyImpact as he,derivePropertyChanges as He,isDataLoss as je,isStatefulResourceType as we,renderPlanSummary as We,renderPlanLines as qe,toWirePlanChanges as ze,signApprovalToken as Je,verifyApprovalToken as Xe,DEFAULT_APPROVAL_TTL_MS as Ze,APPROVAL_TOKEN_PATTERN as $e}from"./application/plan/index.js";import{runApprovalGate as rr}from"./application/approvalGate.js";export{$e as APPROVAL_TOKEN_PATTERN,Ze as DEFAULT_APPROVAL_TTL_MS,Fe as ORG_SETUP_PHASES,h as ORG_TRAIL_BUCKET_OUTPUT_KEY,xe as OU_ONLY_ACCOUNT_BUCKETS,B as PRE_EMPTY_TAG_KEYS,k as SAFE_CLEANUP_STATES,H as TRAIL_BUCKET_OUTPUT_KEY,j as TRAIL_KEY_ARN_OUTPUT_KEY,fe as accountsConfigToOUTree,L as buildAccountRegionPairs,Re as buildBuildSecretSessionPolicy,Ge as buildDeployPlan,ke as buildDeploySessionPolicy,C as buildRegionList,D as cascadeHomeRegion,K as cascadeOperationKey,he as classifyImpact,T as cleanupFailedStack,Qe as computeAssemblyDigest,Ie as computeDeployPlan,l as computeSecretsDrift,Q as decideNextTransition,W as decommissionMemberTrailStorage,o as deploy,d as deployOrganisation,He as derivePropertyChanges,m as deriveStaleParameters,i as destroy,A as destroyOrganisation,Ve as digestHead,P as emptyS3Bucket,ae as ensureEnforceSsl,pe as extractAllAccountNames,u as extractConsumedSsmParameters,ue as flattenAccountsToEnvironments,y as formatQuarantineSuspectedMessage,g as formatRetainedBucketsMessage,Se as isAccountsConfig,O as isCleanableState,je as isDataLoss,ee as isEnforceSslStatement,Ae as isOuOnlyAccountBucket,E as isQuarantineDetail,R as isRetainedBucketsDetail,we as isStatefulResourceType,de as isStringArray,G as mergeReconciledProviderAccounts,me as parseAccountsConfiguration,U as partitionAccounts,Be as partitionForRegion,_ as preEmptyStackBuckets,n as probeSecretsDrift,F as projectAccountRows,b as projectScalarSummary,M as reconcileProviderAccounts,V as reconcileTrailMigration,qe as renderPlanLines,We as renderPlanSummary,Ue as resolveBuildArgs,ye as resolveBuildSecretSessionProvider,Pe as resolveBuildSecrets,_e as resolveSecretRefValue,a as restart,ce as restoreAndReconcileQuarantinedBucket,ie as restoreBucketPolicy,rr as runApprovalGate,Ke as runOpenNextBuild,be as runOrganisationSetup,Je as signApprovalToken,ge as sourcedRefsFromBuildArgs,p as ssmParameterNameFromValueFrom,se as synthesiseEnforceSslDocument,oe as toResourcePolicyStatements,ze as toWirePlanChanges,$ as triageBucketPolicy,z as unlockBucket,X as unlockQueue,Le as validateBuildGroup,Xe as verifyApprovalToken};
1
+ import{deploy as t}from"./deploy.js";import{destroy as i}from"./destroy.js";import{restart as n,probeSecretsDrift as s}from"./restart/restartApplication.js";import{computeSecretsDrift as p,deriveStaleParameters as c,extractConsumedSsmParameters as m,ssmParameterNameFromValueFrom as u}from"./restart/secretsDrift.js";import{deployOrganisation as d}from"./organisation/organisationDeploy.js";import{destroyOrganisation as T}from"./organisation/organisationDestroy.js";import{cleanupFailedStack as x,emptyS3Bucket as P,preEmptyStackBuckets as A,formatQuarantineSuspectedMessage as E,formatRetainedBucketsMessage as D,isQuarantineDetail as y,isRetainedBucketsDetail as _,PRE_EMPTY_TAG_KEYS as g,isCleanableState as O,SAFE_CLEANUP_STATES as F}from"./stackCleanup.js";import{partitionAccounts as k,buildRegionList as B,buildAccountRegionPairs as b,cascadeHomeRegion as C,cascadeOperationKey as L}from"./organisation/cascadeHelpers.js";import{projectScalarSummary as I,projectAccountRows as h}from"./organisation/cascadeSummary.js";import{reconcileProviderAccounts as G,mergeReconciledProviderAccounts as K}from"./organisation/reconcileProviderAccounts.js";import{decideNextTransition as Y,reconcileTrailMigration as V,ORG_TRAIL_BUCKET_OUTPUT_KEY as J,TRAIL_BUCKET_OUTPUT_KEY as H,TRAIL_KEY_ARN_OUTPUT_KEY as Q}from"./trailMigration/trailMigration.js";import{decommissionMemberTrailStorage as j}from"./trailMigration/memberTrailCleanup.js";import{unlockBucket as q}from"./unlock/unlockBucket.js";import{unlockQueue as X}from"./unlock/unlockQueue.js";import{triageBucketPolicy as $,isEnforceSslStatement as ee}from"./unlock/bucketPolicyTriage.js";import{toResourcePolicyStatements as te}from"./unlock/toResourcePolicyStatements.js";import{restoreBucketPolicy as ie,synthesiseEnforceSslDocument as ae,ensureEnforceSsl as ne}from"./unlock/restoreBucketPolicy.js";import{restoreAndReconcileQuarantinedBucket as le}from"./unlock/restoreAndReconcileQuarantinedBucket.js";import{parseAccountsConfiguration as ce,flattenAccountsToEnvironments as me,extractAllAccountNames as ue,accountsConfigToOUTree as fe,isStringArray as de,isAccountsConfig as Re,isOuOnlyAccountBucket as Te,OU_ONLY_ACCOUNT_BUCKETS as Se}from"./organisation/accountsConfig.js";import{resolveBuildSecrets as Pe,resolveSecretRefValue as Ae,resolveBuildSecretSessionProvider as Ee,sourcedRefsFromBuildArgs as De}from"./application/buildSecretResolver.js";import{buildBuildSecretSessionPolicy as _e,partitionForRegion as ge}from"./application/buildSecretSession.js";import{buildDeploySessionPolicy as Fe}from"./application/deploySessionPolicy.js";import{resolveBuildArgs as ke}from"./application/buildArgResolver.js";import{validateBuildGroup as be}from"./application/buildGroupValidator.js";import{runOpenNextBuild as Le}from"./application/openNextBuild.js";import{runOrganisationSetup as Ie,ORG_SETUP_PHASES as he}from"./organisation/organisationSetup.js";export*from"./builders/index.js";import{buildDeployPlan as Ke,computeDeployPlan as Me,computeAssemblyDigest as Ye,digestHead as Ve,classifyImpact as Je,derivePropertyChanges as He,isDataLoss as Qe,isStatefulResourceType as we,renderPlanSummary as je,renderPlanLines as We,toWirePlanChanges as qe,signApprovalToken as ze,verifyApprovalToken as Xe,DEFAULT_APPROVAL_TTL_MS as Ze,APPROVAL_TOKEN_PATTERN as $e}from"./application/plan/index.js";import{runApprovalGate as rr}from"./application/approvalGate.js";import{runDestructionGate as or,buildDestructionTicket as ir,evaluateDestructionConsents as ar,computeTicketDigest as nr,legalRemediationVerbs as sr,executableRemediationVerbs as lr,advertisableRemediationVerbs as pr,renderDestructionTicketLines as cr,renderConsentVerdictLines as mr}from"./application/destructionGate.js";import{loadDeployPlan as fr,createMemoisedPlanLoader as dr}from"./application/plan/loadDeployPlan.js";import{classifyDriftFailure as Tr,fetchLastOperationEvents as Sr}from"./drift/classifyDriftFailure.js";import{DriftProbe as Pr}from"./drift/driftProbe.js";import{clearDriftSuspects as Er,defaultDriftJournalDir as Dr,readDriftSuspects as yr,recordDriftSuspects as _r}from"./drift/driftSuspectJournal.js";import{detectStackDrift as Or}from"./drift/detectStackDrift.js";import{runDriftPreFlight as vr,formatDriftPreFlightBlock as kr,DRIFT_PREFLIGHT_TRIGGER_STATUSES as Br,DRIFT_PREFLIGHT_BUDGET_MS as br}from"./drift/driftPreFlight.js";import{archiveCompletedRemediationJournal as Lr,computeRemediationOpId as Ur,defaultRemediationJournalDir as Ir,findRemediationJournalByOpId as hr,listActiveRemediationOps as Nr,listActiveRemediationOpsForContext as Gr,readRemediationJournal as Kr,sweepExpiredRemediationJournals as Mr,writeRemediationJournal as Yr,REMEDIATION_JOURNAL_RETENTION_DAYS as Vr}from"./remediation/remediationJournal.js";import{captureRemediationForensics as Hr,defaultRemediationForensicsDir as Qr}from"./remediation/forensicsCapture.js";import{applyRetainFlip as jr,composeFlipCapabilities as Wr,computeTemplateDigest as qr,deriveRetainFlipTemplate as zr,findMarkedResourcesByOpId as Xr,proveFlipMetadataOnly as Zr,readRetainFlipState as $r,verifyRetainFlipDiff as et,FORGET_MARKER_KEY as rt,FORGET_FLIP_SPEC as tt,RECREATE_MARKER_KEY as ot}from"./remediation/retainFlip.js";import{assessForgetResumability as at,completeForgetAfterConverge as nt,completeForgetAfterDeploy as st,formatRecreateInFlightCures as lt,partitionPreFlightByRemediation as pt,runForgetSurgery as ct,synthTemplateDeclaresResource as mt}from"./remediation/forgetResource.js";import{runRecreateSurgery as ft,snapshotPolicyForType as dt,SNAPSHOT_CAPABLE_RESOURCE_TYPES as Rt}from"./remediation/recreateResource.js";import{runRecreatePreFlight as St}from"./remediation/recreatePreFlight.js";import{deriveRemovalTemplate as Pt,findRemovalDeletionFailures as At,proveRemovalTargetsOnly as Et}from"./remediation/removalUpdate.js";import{advertisableDriftVerbs as yt,buildDriftRepairTicket as _t,runDriftRepairGate as gt}from"./remediation/driftRepairGate.js";import{formatPinOutcomeDetail as Ft,pinProbeCoverage as vt,renderPinReportLines as kt,runPinRemediation as Bt}from"./remediation/pinRemediation.js";import{composePostFailureRepairOffer as Ct,formatDriftRepairOffer as Lt,DriftRepairAvailableError as Ut}from"./remediation/postFailureRepairOffer.js";import{checkDeployPathStackAvailability as ht,isDeployPathBlockingStatus as Nt,StackUnavailableError as Gt}from"./activeDeploymentGuard.js";import{assertLeaseBeforeStack as Mt,LeaseDeniedError as Yt}from"./application/leaseGate.js";export{$e as APPROVAL_TOKEN_PATTERN,Ze as DEFAULT_APPROVAL_TTL_MS,br as DRIFT_PREFLIGHT_BUDGET_MS,Br as DRIFT_PREFLIGHT_TRIGGER_STATUSES,Pr as DriftProbe,Ut as DriftRepairAvailableError,tt as FORGET_FLIP_SPEC,rt as FORGET_MARKER_KEY,Yt as LeaseDeniedError,he as ORG_SETUP_PHASES,J as ORG_TRAIL_BUCKET_OUTPUT_KEY,Se as OU_ONLY_ACCOUNT_BUCKETS,g as PRE_EMPTY_TAG_KEYS,ot as RECREATE_MARKER_KEY,Vr as REMEDIATION_JOURNAL_RETENTION_DAYS,F as SAFE_CLEANUP_STATES,Rt as SNAPSHOT_CAPABLE_RESOURCE_TYPES,Gt as StackUnavailableError,H as TRAIL_BUCKET_OUTPUT_KEY,Q as TRAIL_KEY_ARN_OUTPUT_KEY,fe as accountsConfigToOUTree,yt as advertisableDriftVerbs,pr as advertisableRemediationVerbs,jr as applyRetainFlip,Lr as archiveCompletedRemediationJournal,Mt as assertLeaseBeforeStack,at as assessForgetResumability,b as buildAccountRegionPairs,_e as buildBuildSecretSessionPolicy,Ke as buildDeployPlan,Fe as buildDeploySessionPolicy,ir as buildDestructionTicket,_t as buildDriftRepairTicket,B as buildRegionList,Hr as captureRemediationForensics,C as cascadeHomeRegion,L as cascadeOperationKey,ht as checkDeployPathStackAvailability,Tr as classifyDriftFailure,Je as classifyImpact,x as cleanupFailedStack,Er as clearDriftSuspects,nt as completeForgetAfterConverge,st as completeForgetAfterDeploy,Wr as composeFlipCapabilities,Ct as composePostFailureRepairOffer,Ye as computeAssemblyDigest,Me as computeDeployPlan,Ur as computeRemediationOpId,p as computeSecretsDrift,qr as computeTemplateDigest,nr as computeTicketDigest,dr as createMemoisedPlanLoader,Y as decideNextTransition,j as decommissionMemberTrailStorage,Dr as defaultDriftJournalDir,Qr as defaultRemediationForensicsDir,Ir as defaultRemediationJournalDir,t as deploy,d as deployOrganisation,He as derivePropertyChanges,Pt as deriveRemovalTemplate,zr as deriveRetainFlipTemplate,c as deriveStaleParameters,i as destroy,T as destroyOrganisation,Or as detectStackDrift,Ve as digestHead,P as emptyS3Bucket,ne as ensureEnforceSsl,ar as evaluateDestructionConsents,lr as executableRemediationVerbs,ue as extractAllAccountNames,m as extractConsumedSsmParameters,Sr as fetchLastOperationEvents,Xr as findMarkedResourcesByOpId,hr as findRemediationJournalByOpId,At as findRemovalDeletionFailures,me as flattenAccountsToEnvironments,kr as formatDriftPreFlightBlock,Lt as formatDriftRepairOffer,Ft as formatPinOutcomeDetail,E as formatQuarantineSuspectedMessage,lt as formatRecreateInFlightCures,D as formatRetainedBucketsMessage,Re as isAccountsConfig,O as isCleanableState,Qe as isDataLoss,Nt as isDeployPathBlockingStatus,ee as isEnforceSslStatement,Te as isOuOnlyAccountBucket,y as isQuarantineDetail,_ as isRetainedBucketsDetail,we as isStatefulResourceType,de as isStringArray,sr as legalRemediationVerbs,Nr as listActiveRemediationOps,Gr as listActiveRemediationOpsForContext,fr as loadDeployPlan,K as mergeReconciledProviderAccounts,ce as parseAccountsConfiguration,k as partitionAccounts,ge as partitionForRegion,pt as partitionPreFlightByRemediation,vt as pinProbeCoverage,A as preEmptyStackBuckets,s as probeSecretsDrift,h as projectAccountRows,I as projectScalarSummary,Zr as proveFlipMetadataOnly,Et as proveRemovalTargetsOnly,yr as readDriftSuspects,Kr as readRemediationJournal,$r as readRetainFlipState,G as reconcileProviderAccounts,V as reconcileTrailMigration,_r as recordDriftSuspects,mr as renderConsentVerdictLines,cr as renderDestructionTicketLines,kt as renderPinReportLines,We as renderPlanLines,je as renderPlanSummary,ke as resolveBuildArgs,Ee as resolveBuildSecretSessionProvider,Pe as resolveBuildSecrets,Ae as resolveSecretRefValue,n as restart,le as restoreAndReconcileQuarantinedBucket,ie as restoreBucketPolicy,rr as runApprovalGate,or as runDestructionGate,vr as runDriftPreFlight,gt as runDriftRepairGate,ct as runForgetSurgery,Le as runOpenNextBuild,Ie as runOrganisationSetup,Bt as runPinRemediation,St as runRecreatePreFlight,ft as runRecreateSurgery,ze as signApprovalToken,dt as snapshotPolicyForType,De as sourcedRefsFromBuildArgs,u as ssmParameterNameFromValueFrom,Mr as sweepExpiredRemediationJournals,mt as synthTemplateDeclaresResource,ae as synthesiseEnforceSslDocument,te as toResourcePolicyStatements,qe as toWirePlanChanges,$ as triageBucketPolicy,q as unlockBucket,X as unlockQueue,be as validateBuildGroup,Xe as verifyApprovalToken,et as verifyRetainFlipDiff,Yr as writeRemediationJournal};
@@ -5,6 +5,7 @@
5
5
  * for destroy operations. No bootstrap step (unlike deploy).
6
6
  */
7
7
  import type { DestroyParams } from "../../types/params.js";
8
+ import { type LeaseDeniedError } from "../application/leaseGate.js";
8
9
  import type { DeployCallbacks } from "../../types/callbacks.js";
9
10
  import type { OrganisationOperation } from "../../types/operations.js";
10
11
  import type { DeployServices } from "../serviceFactory.js";
@@ -16,6 +17,12 @@ export interface CascadeDestroyAccountResult {
16
17
  duration: number;
17
18
  error?: string;
18
19
  skipped?: boolean;
20
+ /**
21
+ * Set when the failure was a §5.3 lease denial. The aggregator must
22
+ * propagate it as a typed overall failure — a denial is a cancel/supersede
23
+ * signal, not a partial-failure to fold into the skip-root warnings path.
24
+ */
25
+ leaseDenied?: LeaseDeniedError;
19
26
  }
20
27
  export interface DestroyCascadeAccountOptions {
21
28
  /**
@@ -1 +1 @@
1
- import{join as b}from"path";import{logger as f}from"@fjall/util/logger";import{maskSensitiveOutput as s,getErrorMessage as v}from"@fjall/util";import{stubCallerIdentity as B}from"../../types/deployment/index.js";import{CloudFormationClient as P,DescribeStacksCommand as _}from"@aws-sdk/client-cloudformation";import{S3Client as w}from"@aws-sdk/client-s3";import{BackupClient as x}from"@aws-sdk/client-backup";import{IAMClient as F}from"@aws-sdk/client-iam";import{accountHasDisasterRecovery as M,describeSurvivingBackupVault as K,formatSurvivingVaultWarning as L}from"../../aws/organisations/backup.js";import{cleanupOrphanedOidcProvider as V}from"../../aws/organisations/oidcProvider.js";import{composeSdkAbortSignal as j}from"../../aws/organisations/types.js";import{ORGANISATION_TYPES as E,getOrganisationStackName as U}from"../../types/operations.js";import{CdkContextBuilder as G}from"../../services/supporting/CdkContextBuilder.js";import{regionSuffix as H}from"../../types/config/FjallState.js";import{buildParamsContext as W,assumeCascadeRole as Y,forwardOutput as N}from"../contextHelpers.js";import{cascadeOperationKey as q}from"./cascadeHelpers.js";import{capturedSweepBuckets as z,cleanupFailedStack as J,preEmptyStackBuckets as Q,sweepOrphanedDestroyBuckets as O}from"../stackCleanup.js";import{STACK_NOT_FOUND_PATTERN as X,STACK_FAILED_STATE_PATTERN as Z}from"../../types/constants.js";async function we(a,n,d,e,t,r,o,y){const S=Date.now(),c=q(e.name,r),l=(i,p=i)=>(o.onCascadeAccountComplete?.(c,!1,p,r),{accountName:e.name,accountId:e.id,region:r,success:!1,duration:Date.now()-S,error:i});o.onCascadeAccountStart?.(c,e.id,r,t);const g=await Y(n.awsProvider,e.id,r,`fjall-cascade-destroy-${e.name}`);if(!g.success){const i=s(g.error.message);return l(`AssumeRole failed: ${i}`,i)}const{provider:u,credentials:k}=g.data,T=b(d.path,`cdk.out.${e.id}.${H(r)}`),A=G.buildDeploymentContext({deployType:t,target:d.target,path:d.path,assemblyDir:T,region:r,accountName:e.name,callerIdentity:B(e.id),...W({orgConfig:a.orgConfig,identity:a.identity,region:r,primaryRegion:y?.primaryRegion})},{verbose:a.options?.verbose},a.orgConfig);o.onCascadeAccountPhaseChange?.(c,"synth",r);const D=await n.cdkService.runCdkSynth(A,N(o));if(!D.success)return l(s(`Synth failed: ${D.error}`));o.onCascadeAccountPhaseChange?.(c,"destroy",r);const m=U(t==="platform"?E.PLATFORM:E.ACCOUNT);M(e.environment,a.orgConfig?.disasterRecoveryRegion)&&await ee(u.getClient(x),r,o,a.abortSignal);const I=await Q(u.getClient(P),u.getClient(w),m,o,a.abortSignal),C=z(I),h=await n.cdkService.runCdkDestroy(A,m,N(o),i=>o.onCascadeAccountResourceProgress?.(c,i,r),u,!0,k);if(!h.success){const i=h.error;if(i.includes(Z)){f.warn("cascadeDestroy",`CDK destroy failed on ${m} in failed state, retrying via CloudFormation API`,{region:r,account:e.name});try{await J(m,r,k,{accountId:e.id,abortSignal:a.abortSignal},o)}catch($){const R=`cleanupFailedStack threw for ${m}: ${s(v($))}`;f.warn("cascadeDestroy",R),o.onLog?.(R,"warn")}const p=await te(m,u.getClient(P),a.abortSignal);return p.deleted?(C.length>0&&await O(u.getClient(w),C,o,a.abortSignal),o.onCascadeAccountComplete?.(c,!0,void 0,r),{accountName:e.name,accountId:e.id,region:r,success:!0,duration:Date.now()-S}):p.error?l(s(p.error)):l(s(`Stack ${m} cleanup attempted but stack still exists in ${r}`))}return l(s(i))}return C.length>0&&await O(u.getClient(w),C,o,a.abortSignal),(y?.primaryRegion===void 0||r===y.primaryRegion)&&await V(u.getClient(F),o,a.abortSignal),o.onCascadeAccountComplete?.(c,!0,void 0,r),{accountName:e.name,accountId:e.id,region:r,success:!0,duration:Date.now()-S}}async function ee(a,n,d,e){try{const t=await K(a,n,e);if(!t.success){f.debug("cascadeDestroy","Backup-vault survival probe failed",{region:n,error:s(t.error.message)});return}if(t.data===null)return;d.onProgress?.({type:"warning",message:L(t.data),metadata:{source:"backup-vault-survival"}}),f.warn("cascadeDestroy","Backup vault survives destroy",{region:n,vaultName:t.data.vaultName,recoveryPointCount:t.data.recoveryPointCount,lockPermanent:t.data.lockPermanent})}catch(t){f.debug("cascadeDestroy","Backup-vault survival probe threw",{region:n,error:s(v(t))})}}async function te(a,n,d){try{const t=(await n.send(new _({StackName:a}),{abortSignal:j(d)})).Stacks?.[0]?.StackStatus;return!t||t==="DELETE_COMPLETE"?{deleted:!0}:{deleted:!1,error:`Stack still in ${t} after cleanup attempt`}}catch(e){if(e instanceof Error&&e.message?.includes(X))return{deleted:!0};const t=s(v(e));return f.debug("cascadeDestroy","Stack verification failed",{error:t}),{deleted:!1,error:`Stack verification failed: ${t}`}}}export{we as destroyCascadeAccount};
1
+ import{join as $}from"path";import{logger as l}from"@fjall/util/logger";import{maskSensitiveOutput as s,getErrorMessage as w}from"@fjall/util";import{stubCallerIdentity as F}from"../../types/deployment/index.js";import{CloudFormationClient as E,DescribeStacksCommand as _}from"@aws-sdk/client-cloudformation";import{S3Client as k}from"@aws-sdk/client-s3";import{BackupClient as x}from"@aws-sdk/client-backup";import{IAMClient as L}from"@aws-sdk/client-iam";import{accountHasDisasterRecovery as M,describeSurvivingBackupVault as K,formatSurvivingVaultWarning as V}from"../../aws/organisations/backup.js";import{cleanupOrphanedOidcProvider as j}from"../../aws/organisations/oidcProvider.js";import{composeSdkAbortSignal as U}from"../../aws/organisations/types.js";import{assertLeaseBeforeStack as G}from"../application/leaseGate.js";import{ORGANISATION_TYPES as N,getOrganisationStackName as H}from"../../types/operations.js";import{CdkContextBuilder as W}from"../../services/supporting/CdkContextBuilder.js";import{regionSuffix as Y}from"../../types/config/FjallState.js";import{buildParamsContext as q,assumeCascadeRole as z,forwardOutput as O}from"../contextHelpers.js";import{cascadeOperationKey as J}from"./cascadeHelpers.js";import{capturedSweepBuckets as Q,cleanupFailedStack as X,preEmptyStackBuckets as Z,sweepOrphanedDestroyBuckets as T}from"../stackCleanup.js";import{STACK_NOT_FOUND_PATTERN as ee,STACK_FAILED_STATE_PATTERN as te}from"../../types/constants.js";async function De(a,n,c,e,t,r,o,S){const y=Date.now(),m=J(e.name,r),f=(i,p=i)=>(o.onCascadeAccountComplete?.(m,!1,p,r),{accountName:e.name,accountId:e.id,region:r,success:!1,duration:Date.now()-y,error:i});o.onCascadeAccountStart?.(m,e.id,r,t);const g=await z(n.awsProvider,e.id,r,`fjall-cascade-destroy-${e.name}`);if(!g.success){const i=s(g.error.message);return f(`AssumeRole failed: ${i}`,i)}const{provider:d,credentials:A}=g.data,b=$(c.path,`cdk.out.${e.id}.${Y(r)}`),D=W.buildDeploymentContext({deployType:t,target:c.target,path:c.path,assemblyDir:b,region:r,accountName:e.name,callerIdentity:F(e.id),...q({orgConfig:a.orgConfig,identity:a.identity,region:r,primaryRegion:S?.primaryRegion})},{verbose:a.options?.verbose},a.orgConfig);o.onCascadeAccountPhaseChange?.(m,"synth",r);const h=await n.cdkService.runCdkSynth(D,O(o));if(!h.success)return f(s(`Synth failed: ${h.error}`));o.onCascadeAccountPhaseChange?.(m,"destroy",r);const u=H(t==="platform"?N.PLATFORM:N.ACCOUNT),v=await G(a.assertLeaseForStack,u,a.abortSignal,"destroy");if(v!==void 0)return{...f(v.message),leaseDenied:v};M(e.environment,a.orgConfig?.disasterRecoveryRegion)&&await re(d.getClient(x),r,o,a.abortSignal);const B=await Z(d.getClient(E),d.getClient(k),u,o,a.abortSignal),C=Q(B),R=await n.cdkService.runCdkDestroy(D,u,O(o),i=>o.onCascadeAccountResourceProgress?.(m,i,r),d,!0,A);if(!R.success){const i=R.error;if(i.includes(te)){l.warn("cascadeDestroy",`CDK destroy failed on ${u} in failed state, retrying via CloudFormation API`,{region:r,account:e.name});try{await X(u,r,A,{accountId:e.id,abortSignal:a.abortSignal},o)}catch(I){const P=`cleanupFailedStack threw for ${u}: ${s(w(I))}`;l.warn("cascadeDestroy",P),o.onLog?.(P,"warn")}const p=await ae(u,d.getClient(E),a.abortSignal);return p.deleted?(C.length>0&&await T(d.getClient(k),C,o,a.abortSignal),o.onCascadeAccountComplete?.(m,!0,void 0,r),{accountName:e.name,accountId:e.id,region:r,success:!0,duration:Date.now()-y}):p.error?f(s(p.error)):f(s(`Stack ${u} cleanup attempted but stack still exists in ${r}`))}return f(s(i))}return C.length>0&&await T(d.getClient(k),C,o,a.abortSignal),(S?.primaryRegion===void 0||r===S.primaryRegion)&&await j(d.getClient(L),o,a.abortSignal),o.onCascadeAccountComplete?.(m,!0,void 0,r),{accountName:e.name,accountId:e.id,region:r,success:!0,duration:Date.now()-y}}async function re(a,n,c,e){try{const t=await K(a,n,e);if(!t.success){l.debug("cascadeDestroy","Backup-vault survival probe failed",{region:n,error:s(t.error.message)});return}if(t.data===null)return;c.onProgress?.({type:"warning",message:V(t.data),metadata:{source:"backup-vault-survival"}}),l.warn("cascadeDestroy","Backup vault survives destroy",{region:n,vaultName:t.data.vaultName,recoveryPointCount:t.data.recoveryPointCount,lockPermanent:t.data.lockPermanent})}catch(t){l.debug("cascadeDestroy","Backup-vault survival probe threw",{region:n,error:s(w(t))})}}async function ae(a,n,c){try{const t=(await n.send(new _({StackName:a}),{abortSignal:U(c)})).Stacks?.[0]?.StackStatus;return!t||t==="DELETE_COMPLETE"?{deleted:!0}:{deleted:!1,error:`Stack still in ${t} after cleanup attempt`}}catch(e){if(e instanceof Error&&e.message?.includes(ee))return{deleted:!0};const t=s(w(e));return l.debug("cascadeDestroy","Stack verification failed",{error:t}),{deleted:!1,error:`Stack verification failed: ${t}`}}}export{De as destroyCascadeAccount};
@@ -1 +1 @@
1
- import{join as L}from"path";import{success as B,failure as h}from"@fjall/generator";import{logger as y}from"@fjall/util/logger";import{maskSensitiveOutput as p,mapSettledWithConcurrency as U}from"@fjall/util";import{ORGANISATION_TYPES as R,getOrganisationStackName as j}from"../../types/operations.js";import{CdkContextBuilder as _}from"../../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as K}from"../../types/deployment/index.js";import{CloudFormationService as H}from"../../services/infrastructure/CloudFormationService.js";import{getCascadeStateFilePath as z,regionSuffix as G}from"../../types/config/FjallState.js";import{BackupClient as Y}from"@aws-sdk/client-backup";import{accountHasDisasterRecovery as W,describeBackupVaultExists as X}from"../../aws/organisations/backup.js";import{buildParamsContext as q,collectStackOutputs as M,assumeCascadeRole as v,forwardOutput as x}from"../contextHelpers.js";import{accountTier as T}from"@fjall/util";import{DEFAULT_REGION as b}from"../../aws/utils/regions.js";const J=4;function le(o){const n=o.find(e=>T(e)==="platform"),c=o.filter(e=>T(e)==="account");return{platformAccount:n,memberAccounts:c}}function Q(o){const n=o?.primaryRegion??b,c=o?.secondaryRegions??[],e=new Set([n,...c]),i=o?.disasterRecoveryRegion;return i&&e.add(i),[...e]}function ge(o){return Q(o)[0]??b}function Z(o,n){return`${o} (${n})`}function Ce(o,n){const c=[];for(const e of n)for(const i of o)c.push({account:i,region:e});return c}import{buildCascadeRoleArn as $e}from"../contextHelpers.js";async function he(o,n,c,e){c.onLog?.(`Verifying cascade role access for ${n.length} account(s)\u2026`,"info");const i=await U(n,J,async a=>v(o.awsProvider,a.id,a.region??o.awsProvider.getRegion(),`fjall-preflight-${a.name}`,e)),s=[];return i.forEach((a,t)=>{const r=n[t];if(r){if(a.status==="rejected"){s.push({accountId:r.id,accountName:r.name,error:a.reason instanceof Error?a.reason.message:String(a.reason)});return}a.value.success||s.push({accountId:r.id,accountName:r.name,error:a.value.error.message})}}),s.length===0&&c.onLog?.(`Cascade role access verified for ${n.length} account(s)`,"info"),s}async function ye(o,n,c,e,i,s,a){const t=a?.region??e.region??n.awsProvider.getRegion(),r=Z(e.name,t),u=a?.orgConfig??o.orgConfig,l=a?.ipamPoolId;s.onCascadeAccountStart?.(r,e.id,t,i);const d=await v(n.awsProvider,e.id,t,`fjall-cascade-${e.name}`,o.abortSignal);if(!d.success)return s.onCascadeAccountComplete?.(r,!1,p(d.error.message),t),h(new Error(`Failed to assume role for ${e.name}: ${p(d.error.message)}`));const{provider:g,credentials:C}=d.data,P=L(o.workingDirectory,"fjall",i==="platform"?R.PLATFORM:R.ACCOUNT),D=L(P,`cdk.out.${e.id}.${G(t)}`),w=_.buildDeploymentContext({deployType:i,target:c.target,path:P,assemblyDir:D,environment:e.environment??void 0,region:t,accountName:e.name,callerIdentity:K(e.id),ipamPoolId:l,...q({orgConfig:u,identity:o.identity,skipOidc:o.options?.skipOidc,region:t,primaryRegion:a?.primaryRegion,trailLifecycle:e.trailLifecycle})},{verbose:o.options?.verbose},u);if(W(e.environment,u?.disasterRecoveryRegion)){const f=await X(g.getClient(Y),o.abortSignal);if(!f.success)return s.onCascadeAccountComplete?.(r,!1,p(f.error.message),t),h(new Error(`Backup vault probe failed for ${e.name}: ${p(f.error.message)}`));w.fjallAdoptBackupVault=f.data}s.onCascadeAccountPhaseChange?.(r,"synth",t);const S=await n.cdkService.runCdkSynth(w,x(s),C);if(!S.success)return s.onCascadeAccountComplete?.(r,!1,p(`Synth failed: ${S.error}`),t),h(new Error(`Synth failed for ${e.name}: ${p(S.error)}`));const m=j(i==="platform"?R.PLATFORM:R.ACCOUNT),k=z(P,e.id,t),$=new H(g),{changed:V,currentHash:I}=await n.hashService.compareCascadeStack(D,m,k);if(!V&&o.options?.force!==!0&&await $.stackExists(m)){const f=await $.getStackOutputs(m);f.success||y.debug("cascadeHelpers","Failed to read outputs for skipped cascade account (non-critical)",{stackName:m,account:e.name});const F=M(f);return s.onLog?.(`${e.name}: no infrastructure changes \u2014 skipping deploy`,"info"),s.onCascadeAccountComplete?.(r,!0,void 0,t,F,!0),B({outputs:F,skipped:!0})}s.onCascadeAccountPhaseChange?.(r,"bootstrap",t);const A=await n.cdkService.runCdkBootstrap(w,x(s),C);if(!A.success)return s.onCascadeAccountComplete?.(r,!1,p(`Bootstrap failed: ${A.error}`),t),h(new Error(`Bootstrap failed for ${e.name}: ${p(A.error)}`));s.onCascadeAccountPhaseChange?.(r,"deploy",t);const O=await n.cdkService.runCdkDeploy(w,m,x(s),f=>s.onCascadeAccountResourceProgress?.(r,f,t),g,C);if(!O.success)return s.onCascadeAccountComplete?.(r,!1,p(O.error),t),h(new Error(p(O.error)));const E=await $.getStackOutputs(m);E.success||y.debug("cascadeHelpers","Failed to read cascade account stack outputs (non-critical)",{stackName:m,account:e.name});const N=M(E);return I!==void 0&&((await n.hashService.persistCascadeStack(k,m,I)).success||y.debug("cascadeHelpers","Failed to persist cascade hash state (non-critical)",{stackName:m,account:e.name})),s.onCascadeAccountComplete?.(r,!0,void 0,t,N,!1),B({outputs:N,skipped:!1})}async function Re(o,n,c,e){const i=new Map,s=o.awsProvider.getRegion(),a=await v(o.awsProvider,n.id,s,`fjall-ipam-read-${n.name}`,e);if(!a.success)return y.debug("organisationDeploy",`Cannot read Platform outputs: ${a.error.message}`),i;const t=new H(a.data.provider),r=j(R.PLATFORM),u=await t.getStackOutputs(r);if(!u.success)return y.debug("organisationDeploy",`Failed to read Platform stack outputs: ${u.error.message}`),i;const l=/^IpamPoolId(\d{12})(\w+)$/;for(const d of u.data){const g=d.OutputKey?.match(l);if(g&&d.OutputValue){const C=`${g[1]}-${g[2]}`;i.set(C,d.OutputValue)}}return i.size>0&&c.onLog?.(`Read ${i.size} IPAM pool ID(s) from Platform stack`,"info"),i}async function we(o,n){const c=o.getDomains();if(c.length===0)return{domainsDeployed:0,errors:[]};n.onCascadePhaseStart?.("domains");const e=c.filter(t=>t.type==="apex"),i=c.filter(t=>t.type==="delegated");let s=0;const a=[];for(const t of e){const r=await o.deployDomain(t.name,n);r.success?s++:a.push(`${t.name}: ${r.error.message}`)}if(i.length>0){const t=await Promise.allSettled(i.map(r=>o.deployDomain(r.name,n)));for(let r=0;r<t.length;r++){const u=t[r],l=i[r];if(!(!u||!l))if(u.status==="fulfilled")u.value.success?s++:a.push(`${l.name}: ${u.value.error.message}`);else{const d=u.reason instanceof Error?u.reason.message:String(u.reason);a.push(`${l.name}: ${d}`)}}}return n.onCascadePhaseComplete?.("domains"),{domainsDeployed:s,errors:a}}export{J as CASCADE_MAX_CONCURRENCY,Ce as buildAccountRegionPairs,$e as buildCascadeRoleArn,Q as buildRegionList,ge as cascadeHomeRegion,Z as cascadeOperationKey,ye as deployCascadeAccount,we as deployDomains,le as partitionAccounts,he as probeCascadeRoles,Re as readPlatformIpamPoolIds};
1
+ import{join as b}from"path";import{success as j,failure as C}from"@fjall/generator";import{logger as R}from"@fjall/util/logger";import{maskSensitiveOutput as l,mapSettledWithConcurrency as K}from"@fjall/util";import{ORGANISATION_TYPES as S,getOrganisationStackName as H}from"../../types/operations.js";import{CdkContextBuilder as z}from"../../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as Y}from"../../types/deployment/index.js";import{CloudFormationService as M}from"../../services/infrastructure/CloudFormationService.js";import{getCascadeStateFilePath as W,regionSuffix as X}from"../../types/config/FjallState.js";import{BackupClient as q}from"@aws-sdk/client-backup";import{accountHasDisasterRecovery as J,describeBackupVaultExists as Q}from"../../aws/organisations/backup.js";import{buildParamsContext as Z,collectStackOutputs as T,assumeCascadeRole as k,forwardOutput as E,reconcileOidcProviderIfCarried as V}from"../contextHelpers.js";import{assertLeaseBeforeStack as ee}from"../application/leaseGate.js";import{runDriftPreFlightGate as te}from"../drift/preFlightGate.js";import{accountTier as G}from"@fjall/util";import{DEFAULT_REGION as U}from"../../aws/utils/regions.js";const oe=4;function we(e){const a=e.find(t=>G(t)==="platform"),c=e.filter(t=>G(t)==="account");return{platformAccount:a,memberAccounts:c}}function re(e){const a=e?.primaryRegion??U,c=e?.secondaryRegions??[],t=new Set([a,...c]),i=e?.disasterRecoveryRegion;return i&&t.add(i),[...t]}function Pe(e){return re(e)[0]??U}function ne(e,a){return`${e} (${a})`}function $e(e,a){const c=[];for(const t of a)for(const i of e)c.push({account:i,region:t});return c}import{buildCascadeRoleArn as Ee}from"../contextHelpers.js";async function Ae(e,a,c,t){c.onLog?.(`Verifying cascade role access for ${a.length} account(s)\u2026`,"info");const i=await K(a,oe,async s=>k(e.awsProvider,s.id,s.region??e.awsProvider.getRegion(),`fjall-preflight-${s.name}`,t)),r=[];return i.forEach((s,o)=>{const n=a[o];if(n){if(s.status==="rejected"){r.push({accountId:n.id,accountName:n.name,error:s.reason instanceof Error?s.reason.message:String(s.reason)});return}s.value.success||r.push({accountId:n.id,accountName:n.name,error:s.value.error.message})}}),r.length===0&&c.onLog?.(`Cascade role access verified for ${a.length} account(s)`,"info"),r}async function Oe(e,a,c,t,i,r,s){const o=s?.region??t.region??a.awsProvider.getRegion(),n=ne(t.name,o),u=s?.orgConfig??e.orgConfig,g=s?.ipamPoolId;r.onCascadeAccountStart?.(n,t.id,o,i);const f=await k(a.awsProvider,t.id,o,`fjall-cascade-${t.name}`,e.abortSignal);if(!f.success)return r.onCascadeAccountComplete?.(n,!1,l(f.error.message),o),C(new Error(`Failed to assume role for ${t.name}: ${l(f.error.message)}`));const{provider:m,credentials:y}=f.data,P=b(e.workingDirectory,"fjall",i==="platform"?S.PLATFORM:S.ACCOUNT),$=b(P,`cdk.out.${t.id}.${X(o)}`),h=z.buildDeploymentContext({deployType:i,target:c.target,path:P,assemblyDir:$,environment:t.environment??void 0,region:o,accountName:t.name,callerIdentity:Y(t.id),ipamPoolId:g,...Z({orgConfig:u,identity:e.identity,skipOidc:e.options?.skipOidc,region:o,primaryRegion:s?.primaryRegion,trailLifecycle:t.trailLifecycle})},{verbose:e.options?.verbose},u);if(J(t.environment,u?.disasterRecoveryRegion)){const p=await Q(m.getClient(q),e.abortSignal);if(!p.success)return r.onCascadeAccountComplete?.(n,!1,l(p.error.message),o),C(new Error(`Backup vault probe failed for ${t.name}: ${l(p.error.message)}`));h.fjallAdoptBackupVault=p.data}r.onCascadeAccountPhaseChange?.(n,"synth",o);const A=await a.cdkService.runCdkSynth(h,E(r),y);if(!A.success)return r.onCascadeAccountComplete?.(n,!1,l(`Synth failed: ${A.error}`),o),C(new Error(`Synth failed for ${t.name}: ${l(A.error)}`));const d=H(i==="platform"?S.PLATFORM:S.ACCOUNT),I=W(P,t.id,o),w=new M(m),{changed:_,currentHash:F}=await a.hashService.compareCascadeStack($,d,I);if(!_&&e.options?.force!==!0&&await w.stackExists(d)){const p=await w.getStackOutputs(d);p.success||R.debug("cascadeHelpers","Failed to read outputs for skipped cascade account (non-critical)",{stackName:d,account:t.name});const B=T(p);return await V(h,m,r,e.abortSignal),r.onLog?.(`${t.name}: no infrastructure changes \u2014 skipping deploy`,"info"),r.onCascadeAccountComplete?.(n,!0,void 0,o,B,!0),j({outputs:B,skipped:!0})}const O=await te(m,{stackNames:[d],cdkOutPath:$,cfnService:w,callbacks:r,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}});if(O.kind==="blocked")return r.onCascadeAccountComplete?.(n,!1,O.message,o),C(new Error(O.message));const v=await ee(e.assertLeaseForStack,d,e.abortSignal);if(v!==void 0)return r.onCascadeAccountComplete?.(n,!1,v.message,o),C(v);r.onCascadeAccountPhaseChange?.(n,"bootstrap",o);const x=await a.cdkService.runCdkBootstrap(h,E(r),y);if(!x.success)return r.onCascadeAccountComplete?.(n,!1,l(`Bootstrap failed: ${x.error}`),o),C(new Error(`Bootstrap failed for ${t.name}: ${l(x.error)}`));r.onCascadeAccountPhaseChange?.(n,"deploy",o);const D=await a.cdkService.runCdkDeploy(h,d,E(r),p=>r.onCascadeAccountResourceProgress?.(n,p,o),m,y);if(!D.success)return r.onCascadeAccountComplete?.(n,!1,l(D.error),o),C(new Error(l(D.error)));const N=await w.getStackOutputs(d);N.success||R.debug("cascadeHelpers","Failed to read cascade account stack outputs (non-critical)",{stackName:d,account:t.name});const L=T(N);return await V(h,m,r,e.abortSignal),F!==void 0&&((await a.hashService.persistCascadeStack(I,d,F)).success||R.debug("cascadeHelpers","Failed to persist cascade hash state (non-critical)",{stackName:d,account:t.name})),r.onCascadeAccountComplete?.(n,!0,void 0,o,L,!1),j({outputs:L,skipped:!1})}async function ve(e,a,c,t){const i=new Map,r=e.awsProvider.getRegion(),s=await k(e.awsProvider,a.id,r,`fjall-ipam-read-${a.name}`,t);if(!s.success)return R.debug("organisationDeploy",`Cannot read Platform outputs: ${s.error.message}`),i;const o=new M(s.data.provider),n=H(S.PLATFORM),u=await o.getStackOutputs(n);if(!u.success)return R.debug("organisationDeploy",`Failed to read Platform stack outputs: ${u.error.message}`),i;const g=/^IpamPoolId(\d{12})(\w+)$/;for(const f of u.data){const m=f.OutputKey?.match(g);if(m&&f.OutputValue){const y=`${m[1]}-${m[2]}`;i.set(y,f.OutputValue)}}return i.size>0&&c.onLog?.(`Read ${i.size} IPAM pool ID(s) from Platform stack`,"info"),i}async function xe(e,a){const c=e.getDomains();if(c.length===0)return{domainsDeployed:0,errors:[]};a.onCascadePhaseStart?.("domains");const t=c.filter(o=>o.type==="apex"),i=c.filter(o=>o.type==="delegated");let r=0;const s=[];for(const o of t){const n=await e.deployDomain(o.name,a);n.success?r++:s.push(`${o.name}: ${n.error.message}`)}if(i.length>0){const o=await Promise.allSettled(i.map(n=>e.deployDomain(n.name,a)));for(let n=0;n<o.length;n++){const u=o[n],g=i[n];if(!(!u||!g))if(u.status==="fulfilled")u.value.success?r++:s.push(`${g.name}: ${u.value.error.message}`);else{const f=u.reason instanceof Error?u.reason.message:String(u.reason);s.push(`${g.name}: ${f}`)}}}return a.onCascadePhaseComplete?.("domains"),{domainsDeployed:r,errors:s}}export{oe as CASCADE_MAX_CONCURRENCY,$e as buildAccountRegionPairs,Ee as buildCascadeRoleArn,re as buildRegionList,Pe as cascadeHomeRegion,ne as cascadeOperationKey,Oe as deployCascadeAccount,xe as deployDomains,we as partitionAccounts,Ae as probeCascadeRoles,ve as readPlatformIpamPoolIds};
@@ -1,3 +1,3 @@
1
- import{CloudFormationClient as $}from"@aws-sdk/client-cloudformation";import{S3Client as T}from"@aws-sdk/client-s3";import{IAMClient as _}from"@aws-sdk/client-iam";import{success as P,failure as M}from"@fjall/generator";import{maskSensitiveOutput as O,getErrorMessage as k,mapSettledWithConcurrency as v}from"@fjall/util";import{stubCallerIdentity as b}from"../../types/deployment/index.js";import{ORGANISATION_TYPES as L,getOrganisationStackName as x}from"../../types/operations.js";import{CdkContextBuilder as B}from"../../services/supporting/CdkContextBuilder.js";import{buildParamsContext as G,synthOrFail as Y,forwardOutput as j,forwardResourceProgress as F}from"../contextHelpers.js";import{destroyCascadeAccount as N}from"./cascadeDestroyHelpers.js";import{cleanupOrphanedOidcProvider as U}from"../../aws/organisations/oidcProvider.js";import{capturedSweepBuckets as H,preEmptyStackBuckets as W,sweepOrphanedDestroyBuckets as X}from"../stackCleanup.js";import{partitionAccounts as q,buildRegionList as z,buildAccountRegionPairs as J,cascadeHomeRegion as K,CASCADE_MAX_CONCURRENCY as Q}from"./cascadeHelpers.js";import{projectScalarSummary as V}from"./cascadeSummary.js";import{STEP_IDS as Z}from"../../types/stepDefinitions.js";const D=Z.ORG_DESTROY,E="Destroying organisation infrastructure";async function Ct(o,r,c){const S=Date.now(),{callbacks:t}=o,l=o.orgConfig?.providerAccounts??[],g=K(o.orgConfig),m=z(o.orgConfig),{platformAccount:u,memberAccounts:f}=q(l),h=o.options?.cascade!==!1;t.onLog?.(`Destroying organisation infrastructure (${l.length} accounts, ${m.length} region(s))`,"info");const n=[],p=[],w=[];let R;if(h){t.onCascadeStart?.();const A=Date.now();if(f.length>0){t.onCascadePhaseStart?.("accounts");const a=J(f,m),d=await v(a,Q,({account:i,region:s})=>N(o,r,c,i,"account",s,t,{primaryRegion:g}));for(let i=0;i<d.length;i++){const s=d[i],e=a[i];if(!(!s||!e))if(s.status==="fulfilled")if(s.value.success)p.push(`Account-${e.account.name}-${e.region}`),w.push({accountId:e.account.id,accountName:e.account.name,region:e.region,result:"succeeded",durationMs:s.value.duration});else{const y=O(s.value.error??"Unknown error");w.push({accountId:e.account.id,accountName:e.account.name,region:e.region,result:"failed",durationMs:s.value.duration,error:y}),n.push({accountId:e.account.id,error:y})}else{const y=O(k(s.reason));w.push({accountId:e.account.id,accountName:e.account.name,region:e.region,result:"failed",durationMs:0,error:y}),n.push({accountId:e.account.id,error:y})}}t.onCascadePhaseComplete?.("accounts")}if(u){t.onCascadePhaseStart?.("platform");const a=await N(o,r,c,u,"platform",u.region??g,t);if(a.success)p.push("Platform"),R={accountId:u.id,result:"succeeded",durationMs:a.duration};else{const d=O(a.error??"Platform destroy failed");n.push({accountId:u.id,error:d}),R={accountId:u.id,result:"failed",durationMs:a.duration,error:d}}t.onCascadePhaseComplete?.("platform")}const C={members:w,...R!==void 0?{platform:R}:{},domainsDeployed:!1,errors:n,totalDurationMs:Date.now()-A};if(t.onCascadeComplete?.(V(C)),t.onCascadeLedger?.(C),n.length>0){const a=n.map(i=>` ${i.accountId}: ${i.error}`).join(`
2
- `),d=new Error(`Cascade destroy completed with ${n.length} failure(s):
3
- ${a}`);t.onError?.(d),t.onLog?.(d.message,"warn")}}if(n.length>0)return t.onLog?.("Skipping organisation root stack destroy due to cascade failures","warn"),P({target:c.target,deploymentType:"organisation",stacksDestroyed:p,durationMs:Date.now()-S,warnings:n.map(C=>`${C.accountId}: ${C.error}`)});t.onStepStart?.(D,E,0,1);const I=await tt(o,r,c,r.awsProvider.getRegion(),t);if(I.success)p.push("Organisation"),t.onStepComplete?.(D,E,"completed",0,1);else return t.onStepComplete?.(D,E,"error",0,1),M(I.error);return P({target:c.target,deploymentType:"organisation",stacksDestroyed:p,durationMs:Date.now()-S})}async function tt(o,r,c,S,t){const l=B.buildDeploymentContext({deployType:"organisation",target:c.target,path:c.path,region:S,callerIdentity:b(r.awsProvider.getAccountId()),...G({orgConfig:o.orgConfig,identity:o.identity})},{verbose:o.options?.verbose},o.orgConfig),g=x(L.ORGANISATION);t.onLog?.("Synthesising organisation infrastructure\u2026","info");const m=await Y(r,l,t,"Organisation synth failed");if(!m.success)return m;const u=await W(r.awsProvider.getClient($),r.awsProvider.getClient(T),g,t,o.abortSignal),f=H(u);t.onLog?.(`Destroying ${g} stack\u2026`,"info");const h=await r.cdkService.runCdkDestroy(l,g,j(t),F(t),r.awsProvider,!0);if(!h.success){const n=new Error(O(`Organisation destroy failed: ${h.error}`));return t.onError?.(n),M(n)}return f.length>0&&await X(r.awsProvider.getClient(T),f,t,o.abortSignal),await U(r.awsProvider.getClient(_),t,o.abortSignal),P(void 0)}export{Ct as destroyOrganisation};
1
+ import{CloudFormationClient as B}from"@aws-sdk/client-cloudformation";import{S3Client as L}from"@aws-sdk/client-s3";import{IAMClient as G}from"@aws-sdk/client-iam";import{success as k,failure as D}from"@fjall/generator";import{maskSensitiveOutput as E,getErrorMessage as x,mapSettledWithConcurrency as Y}from"@fjall/util";import{stubCallerIdentity as F}from"../../types/deployment/index.js";import{ORGANISATION_TYPES as N,getOrganisationStackName as T}from"../../types/operations.js";import{CdkContextBuilder as U}from"../../services/supporting/CdkContextBuilder.js";import{buildParamsContext as j,synthOrFail as H,forwardOutput as W,forwardResourceProgress as X}from"../contextHelpers.js";import{destroyCascadeAccount as v}from"./cascadeDestroyHelpers.js";import{emitOrgStackTargets as q}from"../organisationDeploy/infraSteps.js";import{assertLeaseBeforeStack as z}from"../application/leaseGate.js";import{cleanupOrphanedOidcProvider as J}from"../../aws/organisations/oidcProvider.js";import{capturedSweepBuckets as K,preEmptyStackBuckets as Q,sweepOrphanedDestroyBuckets as V}from"../stackCleanup.js";import{partitionAccounts as Z,buildRegionList as tt,buildAccountRegionPairs as b,cascadeHomeRegion as ot,CASCADE_MAX_CONCURRENCY as et}from"./cascadeHelpers.js";import{projectScalarSummary as rt}from"./cascadeSummary.js";import{STEP_IDS as nt}from"../../types/stepDefinitions.js";const M=nt.ORG_DESTROY,$="Destroying organisation infrastructure";async function It(o,e,u){const R=Date.now(),{callbacks:t}=o,p=o.orgConfig?.providerAccounts??[],d=ot(o.orgConfig),m=tt(o.orgConfig),{platformAccount:n,memberAccounts:C}=Z(p),f=o.options?.cascade!==!1;t.onLog?.(`Destroying organisation infrastructure (${p.length} accounts, ${m.length} region(s))`,"info");const S=e.awsProvider.getAccountId(),y=e.awsProvider.getRegion();q(t,[{stackName:T(N.ORGANISATION),...S!==""?{accountId:S}:{},...y!==""?{region:y}:{}},...f&&n!==void 0?[{stackName:T(N.PLATFORM),accountId:n.id,region:n.region??d}]:[],...f?b(C,m).map(({account:P,region:l})=>({stackName:T(N.ACCOUNT),accountId:P.id,region:l})):[]]);const s=[],w=[],I=[];let A,h;if(f){t.onCascadeStart?.();const P=Date.now();if(C.length>0){t.onCascadePhaseStart?.("accounts");const a=b(C,m),g=await Y(a,et,({account:c,region:i})=>v(o,e,u,c,"account",i,t,{primaryRegion:d}));for(let c=0;c<g.length;c++){const i=g[c],r=a[c];if(!(!i||!r))if(i.status==="fulfilled")if(i.value.success)w.push(`Account-${r.account.name}-${r.region}`),I.push({accountId:r.account.id,accountName:r.account.name,region:r.region,result:"succeeded",durationMs:i.value.duration});else{const O=E(i.value.error??"Unknown error");I.push({accountId:r.account.id,accountName:r.account.name,region:r.region,result:"failed",durationMs:i.value.duration,error:O}),s.push({accountId:r.account.id,error:O}),h??=i.value.leaseDenied}else{const O=E(x(i.reason));I.push({accountId:r.account.id,accountName:r.account.name,region:r.region,result:"failed",durationMs:0,error:O}),s.push({accountId:r.account.id,error:O})}}t.onCascadePhaseComplete?.("accounts")}if(n){t.onCascadePhaseStart?.("platform");const a=await v(o,e,u,n,"platform",n.region??d,t);if(a.success)w.push("Platform"),A={accountId:n.id,result:"succeeded",durationMs:a.duration};else{const g=E(a.error??"Platform destroy failed");s.push({accountId:n.id,error:g}),h??=a.leaseDenied,A={accountId:n.id,result:"failed",durationMs:a.duration,error:g}}t.onCascadePhaseComplete?.("platform")}const l={members:I,...A!==void 0?{platform:A}:{},domainsDeployed:!1,errors:s,totalDurationMs:Date.now()-P};if(t.onCascadeComplete?.(rt(l)),t.onCascadeLedger?.(l),s.length>0){const a=s.map(c=>` ${c.accountId}: ${c.error}`).join(`
2
+ `),g=new Error(`Cascade destroy completed with ${s.length} failure(s):
3
+ ${a}`);t.onError?.(g),t.onLog?.(g.message,"warn")}}if(h!==void 0)return t.onError?.(h),D(h);if(s.length>0)return t.onLog?.("Skipping organisation root stack destroy due to cascade failures","warn"),k({target:u.target,deploymentType:"organisation",stacksDestroyed:w,durationMs:Date.now()-R,warnings:s.map(l=>`${l.accountId}: ${l.error}`)});t.onStepStart?.(M,$,0,1);const _=await at(o,e,u,e.awsProvider.getRegion(),t);if(_.success)w.push("Organisation"),t.onStepComplete?.(M,$,"completed",0,1);else return t.onStepComplete?.(M,$,"error",0,1),D(_.error);return k({target:u.target,deploymentType:"organisation",stacksDestroyed:w,durationMs:Date.now()-R})}async function at(o,e,u,R,t){const p=U.buildDeploymentContext({deployType:"organisation",target:u.target,path:u.path,region:R,callerIdentity:F(e.awsProvider.getAccountId()),...j({orgConfig:o.orgConfig,identity:o.identity})},{verbose:o.options?.verbose},o.orgConfig),d=T(N.ORGANISATION);t.onLog?.("Synthesising organisation infrastructure\u2026","info");const m=await H(e,p,t,"Organisation synth failed");if(!m.success)return m;const n=await z(o.assertLeaseForStack,d,o.abortSignal,"destroy");if(n!==void 0)return t.onError?.(n),D(n);const C=await Q(e.awsProvider.getClient(B),e.awsProvider.getClient(L),d,t,o.abortSignal),f=K(C);t.onLog?.(`Destroying ${d} stack\u2026`,"info");const S=await e.cdkService.runCdkDestroy(p,d,W(t),X(t),e.awsProvider,!0);if(!S.success){const y=new Error(E(`Organisation destroy failed: ${S.error}`));return t.onError?.(y),D(y)}return f.length>0&&await V(e.awsProvider.getClient(L),f,t,o.abortSignal),await J(e.awsProvider.getClient(G),t,o.abortSignal),k(void 0)}export{It as destroyOrganisation};
@@ -1,5 +1,5 @@
1
1
  import { type Result } from "@fjall/generator";
2
- import type { DeployCallbacks } from "../../types/callbacks.js";
2
+ import type { DeployCallbacks, StackTarget } from "../../types/callbacks.js";
3
3
  import type { DeployServices } from "../serviceFactory.js";
4
4
  export declare const INFRA_STEPS: {
5
5
  readonly CONNECT: {
@@ -32,6 +32,13 @@ export declare const INFRA_STEPS: {
32
32
  };
33
33
  };
34
34
  export declare const INFRA_STEP_TOTAL = 4;
35
+ /**
36
+ * Emit §5.9 reconciler coordinates for the org path — post-synth, before the
37
+ * first mutating CFN op — so a crashed run's in-flight stacks can be located
38
+ * by name/account/region. No-ops when no consumer wired the callback or the
39
+ * operation touches no stacks.
40
+ */
41
+ export declare function emitOrgStackTargets(callbacks: DeployCallbacks, targets: readonly StackTarget[]): void;
35
42
  /** Complete the single-component PREPARE step as errored. */
36
43
  export declare function failPrepareStep(callbacks: DeployCallbacks): void;
37
44
  /** Mask a failure message, surface it via onError, and return the failure. */
@@ -1 +1 @@
1
- import{failure as P}from"@fjall/generator";import{collectStackOutputs as R}from"../contextHelpers.js";import{maskSensitiveOutput as S}from"@fjall/util";import{INFRA_STEP_NAME as r,STEP_IDS as t,STEP_NAMES as A}from"../../types/stepDefinitions.js";const n={CONNECT:{id:t.CONNECT,name:r.CONNECT},PREPARE:{id:t.PREPARE_ENVIRONMENT,name:r.PREPARE},DEPLOY:{id:t.DEPLOY,name:r.DEPLOY},MONITORING:{id:t.MONITORING,name:r.MONITORING},ORG_DEPLOY:{id:t.ORG_DEPLOY,name:A.ORG_DEPLOY},CASCADE_PLATFORM:{id:t.CASCADE_PLATFORM,name:A.CASCADE_PLATFORM},CASCADE_ACCOUNTS:{id:t.CASCADE_ACCOUNTS,name:A.CASCADE_ACCOUNTS}},i=4;function u(E){E.onStepComplete?.(n.PREPARE.id,n.PREPARE.name,"error",1,i)}function p(E,o){const e=new Error(S(o));return E.onError?.(e),P(e)}async function c(E,o,e,C){const O=await E.cfnService.getStackOutputs(e);return O.success||o.onLog?.(C,"debug"),R(O)}export{n as INFRA_STEPS,i as INFRA_STEP_TOTAL,u as failPrepareStep,p as maskAndFail,c as readStackOutputsBestEffort};
1
+ import{failure as S}from"@fjall/generator";import{collectStackOutputs as C}from"../contextHelpers.js";import{maskSensitiveOutput as P}from"@fjall/util";import{INFRA_STEP_NAME as n,STEP_IDS as e,STEP_NAMES as o}from"../../types/stepDefinitions.js";const A={CONNECT:{id:e.CONNECT,name:n.CONNECT},PREPARE:{id:e.PREPARE_ENVIRONMENT,name:n.PREPARE},DEPLOY:{id:e.DEPLOY,name:n.DEPLOY},MONITORING:{id:e.MONITORING,name:n.MONITORING},ORG_DEPLOY:{id:e.ORG_DEPLOY,name:o.ORG_DEPLOY},CASCADE_PLATFORM:{id:e.CASCADE_PLATFORM,name:o.CASCADE_PLATFORM},CASCADE_ACCOUNTS:{id:e.CASCADE_ACCOUNTS,name:o.CASCADE_ACCOUNTS}},a=4;function m(t,r){t.onStackTargets===void 0||r.length===0||t.onStackTargets(r)}function c(t){t.onStepComplete?.(A.PREPARE.id,A.PREPARE.name,"error",1,a)}function p(t,r){const E=new Error(P(r));return t.onError?.(E),S(E)}async function s(t,r,E,i){const O=await t.cfnService.getStackOutputs(E);return O.success||r.onLog?.(i,"debug"),C(O)}export{A as INFRA_STEPS,a as INFRA_STEP_TOTAL,m as emitOrgStackTargets,c as failPrepareStep,p as maskAndFail,s as readStackOutputsBestEffort};
@@ -1,5 +1,5 @@
1
- import{join as W}from"node:path";import{success as _,failure as Y}from"@fjall/generator";import{ORGANISATION_TYPES as B,getOrganisationStackName as K}from"../../types/operations.js";import{synthOrFail as q,bootstrapOrFail as z,forwardOutput as J,forwardResourceProgress as Q}from"../contextHelpers.js";import{partitionAccounts as U,probeCascadeRoles as V}from"../organisation/cascadeHelpers.js";import{accountTier as X,maskSensitiveOutput as l}from"@fjall/util";import{buildOrgContext as Z,resolveOrgDetails as v}from"./orgContext.js";import{INFRA_STEPS as L,maskAndFail as R,readStackOutputsBestEffort as x}from"./infraSteps.js";import{resolveCascadeAccounts as ee}from"./resolveCascadeAccounts.js";import{executeCascade as te}from"./cascadeExecution.js";import{reconcileOrgTrailOutputs as oe}from"./trailReconciliation.js";async function ge(s,t,O,H){const{callbacks:e,options:$}=s,{providerAccounts:u,effectiveOrgConfig:M,defaultRegion:j}=await ee(s,t),g=await v(t,s.abortSignal);if(!g.success)return R(e,g.error.message);const G=u.find(o=>X(o)==="organisation"),d=Z(s,t,O,"organisation",g.data,void 0,G?.trailLifecycle),m=$?.cascade!==!1,{platformAccount:p,memberAccounts:k}=U(u),P=m&&p!==void 0?1:0,D=m&&k.length>0?1:0,n=2+P+D,I=m?[...p!==void 0?[p]:[],...k]:[];if(I.length>0){const o=await V(t,I,e,s.abortSignal),r=p!==void 0?o.find(a=>a.accountId===p.id):void 0;if(r!==void 0)return R(e,`Pre-flight cascade role check failed for the platform account ${r.accountName} (${r.accountId}): ${r.error}`);for(const a of o)e.onProgress?.({type:"warning",message:l(`Pre-flight cascade role check failed for ${a.accountName} (${a.accountId}) \u2014 its cascade deploy is expected to fail: ${a.error}`)})}e.onCascadeAccountsReconciled?.({hasPlatformAccount:P>0,hasMemberAccounts:D>0});const{id:h,name:S}=L.PREPARE;e.onStepStart?.(h,S,0,n),e.onLog?.("Synthesising organisation infrastructure\u2026","info");const E=await q(t,d,e,"CDK synthesis failed");if(!E.success)return e.onStepComplete?.(h,S,"error",0,n),E;const N=await z(t,d,e);if(!N.success)return e.onStepComplete?.(h,S,"error",0,n),N;e.onStepComplete?.(h,S,"completed",0,n);const{id:C,name:b}=L.ORG_DEPLOY,c=K(B.ORGANISATION);let F=!0;const f=await t.hashService.getTemplateHashes(W(d.path,"cdk.out"));if(f.success){const o=await t.hashService.compareWithState(f.data,d.path);o.success?F=o.data.stackChanges.get(c)??!0:e.onLog?.(l(`Org root change detection failed \u2014 deploying to be safe: ${o.error.message}`),"warn")}else e.onLog?.(l(`Org root template hashing failed \u2014 deploying to be safe: ${f.error.message}`),"warn");const A=F||$?.force===!0||!await t.cfnService.stackExists(c);e.onOrgChangesDetected?.({hasOrgChanges:A});let w;if(A){e.onStepStart?.(C,b,1,n);const o=await t.cdkService.runCdkDeploy(d,c,J(e),Q(e),t.awsProvider);if(!o.success)return e.onStepComplete?.(C,b,"error",1,n),R(e,o.error);w=await x(t,e,c,"Failed to read org stack outputs (non-critical)");const r=f.success?f.data.get(c):void 0;if(r!==void 0){const a=await t.hashService.updateStateAfterDeploy(d.path,new Map([[c,r]]));a.success||e.onLog?.(`Warning: failed to update state file \u2014 next deploy may re-deploy the org root: ${l(a.error.message)}`,"warn")}e.onStepComplete?.(C,b,"completed",1,n)}else e.onLog?.("Organisation root: no infrastructure changes \u2014 skipping deploy","info"),w=await x(t,e,c,"Failed to read org stack outputs (non-critical)");const i=[],y=[];let T=A;if(m&&u.length>0){const{anyCascadeDeployHappened:o}=await te(s,t,O,{providerAccounts:u,effectiveOrgConfig:M,totalSteps:n,cascadeErrors:i,allCascadeOutputs:y});if(o&&(T=!0),await oe(s,t,{orgOutputs:w,allCascadeOutputs:y,orgDetails:g.data,providerAccounts:u,defaultRegion:j}),i.length>0){const r=i.map(a=>` ${a.accountId}: ${a.error}`).join(`
2
- `);e.onLog?.(l(`Cascade failed for ${i.length} target(s):
3
- ${r}`),"warn")}}if(i.length>0){const o=i.map(a=>l(`${a.accountId}: ${a.error}`)).join(`
4
- `),r=new Error(`Organisation root deployed, but the cascade failed for ${i.length} target(s):
5
- ${o}`);return e.onError?.(r),Y(r)}return _({target:O.target,deploymentType:"organisation",outputs:w,artefacts:[],...y.length>0?{cascadeOutputs:y}:{},...T?{}:{noChanges:!0},durationMs:Date.now()-H})}export{ge as deployOrgWithCascade};
1
+ import{join as _}from"node:path";import{OrganizationsClient as Q}from"@aws-sdk/client-organizations";import{success as V,failure as v}from"@fjall/generator";import{ORGANISATION_TYPES as A,getOrganisationStackName as R}from"../../types/operations.js";import{synthOrFail as X,bootstrapOrFail as Z,forwardOutput as tt,forwardResourceProgress as et}from"../contextHelpers.js";import{partitionAccounts as ot,probeCascadeRoles as at,buildRegionList as rt,buildAccountRegionPairs as nt}from"../organisation/cascadeHelpers.js";import{assertLeaseBeforeStack as ct}from"../application/leaseGate.js";import{accountTier as it,maskSensitiveOutput as g}from"@fjall/util";import{findDevelopmentOuId as st}from"../../aws/organisations/organisationalUnits.js";import{buildOrgContext as dt,resolveOrgDetails as ut}from"./orgContext.js";import{INFRA_STEPS as B,emitOrgStackTargets as lt,maskAndFail as O,readStackOutputsBestEffort as Y}from"./infraSteps.js";import{resolveCascadeAccounts as gt}from"./resolveCascadeAccounts.js";import{executeCascade as ft}from"./cascadeExecution.js";import{reconcileOrgTrailOutputs as pt}from"./trailReconciliation.js";import{runDriftPreFlightGate as mt}from"../drift/preFlightGate.js";async function Et(n,e,I,z){const{callbacks:t,options:D}=n,{providerAccounts:f,effectiveOrgConfig:K,defaultRegion:U}=await gt(n,e),p=await ut(e,n.abortSignal);if(!p.success)return O(t,p.error.message);const w=await st(e.awsProvider.getClient(Q),p.data.rootId,n.abortSignal);if(!w.success)return O(t,w.error.message);const q={...p.data,...w.data!==void 0?{devOuId:w.data}:{}},J=f.find(o=>it(o)==="organisation"),d=dt(n,e,I,"organisation",q,void 0,J?.trailLifecycle),l=D?.cascade!==!1,{platformAccount:s,memberAccounts:P}=ot(f),E=l&&s!==void 0?1:0,L=l&&P.length>0?1:0,c=2+E+L,T=l?[...s!==void 0?[s]:[],...P]:[];if(T.length>0){const o=await at(e,T,t,n.abortSignal),a=s!==void 0?o.find(r=>r.accountId===s.id):void 0;if(a!==void 0)return O(t,`Pre-flight cascade role check failed for the platform account ${a.accountName} (${a.accountId}): ${a.error}`);for(const r of o)t.onProgress?.({type:"warning",message:g(`Pre-flight cascade role check failed for ${r.accountName} (${r.accountId}) \u2014 its cascade deploy is expected to fail: ${r.error}`)})}t.onCascadeAccountsReconciled?.({hasPlatformAccount:E>0,hasMemberAccounts:L>0});const{id:C,name:y}=B.PREPARE;t.onStepStart?.(C,y,0,c),t.onLog?.("Synthesising organisation infrastructure\u2026","info");const F=await X(e,d,t,"CDK synthesis failed");if(!F.success)return t.onStepComplete?.(C,y,"error",0,c),F;const x=e.awsProvider.getAccountId(),N=e.awsProvider.getRegion();lt(t,[{stackName:R(A.ORGANISATION),...x!==""?{accountId:x}:{},...N!==""?{region:N}:{}},...l&&s!==void 0?[{stackName:R(A.PLATFORM),accountId:s.id,region:s.region??N}]:[],...l?nt(P,rt(n.orgConfig)).map(({account:o,region:a})=>({stackName:R(A.ACCOUNT),accountId:o.id,region:a})):[]]);const G=await Z(e,d,t);if(!G.success)return t.onStepComplete?.(C,y,"error",0,c),G;t.onStepComplete?.(C,y,"completed",0,c);const{id:m,name:h}=B.ORG_DEPLOY,i=R(A.ORGANISATION);let H=!0;const S=await e.hashService.getTemplateHashes(_(d.path,"cdk.out"));if(S.success){const o=await e.hashService.compareWithState(S.data,d.path);o.success?H=o.data.stackChanges.get(i)??!0:t.onLog?.(g(`Org root change detection failed \u2014 deploying to be safe: ${o.error.message}`),"warn")}else t.onLog?.(g(`Org root template hashing failed \u2014 deploying to be safe: ${S.error.message}`),"warn");const $=H||D?.force===!0||!await e.cfnService.stackExists(i);t.onOrgChangesDetected?.({hasOrgChanges:$});let k;if($){t.onStepStart?.(m,h,1,c);const o=await mt(e.awsProvider,{stackNames:[i],cdkOutPath:_(d.path,"cdk.out"),cfnService:e.cfnService,callbacks:t,...n.abortSignal!==void 0?{abortSignal:n.abortSignal}:{}});if(o.kind==="blocked")return t.onStepComplete?.(m,h,"error",1,c),O(t,o.message);const a=await ct(n.assertLeaseForStack,i,n.abortSignal);if(a!==void 0)return t.onStepComplete?.(m,h,"error",1,c),t.onError?.(a),v(a);const r=await e.cdkService.runCdkDeploy(d,i,tt(t),et(t),e.awsProvider);if(!r.success)return t.onStepComplete?.(m,h,"error",1,c),O(t,r.error);k=await Y(e,t,i,"Failed to read org stack outputs (non-critical)");const j=S.success?S.data.get(i):void 0;if(j!==void 0){const W=await e.hashService.updateStateAfterDeploy(d.path,new Map([[i,j]]));W.success||t.onLog?.(`Warning: failed to update state file \u2014 next deploy may re-deploy the org root: ${g(W.error.message)}`,"warn")}t.onStepComplete?.(m,h,"completed",1,c)}else t.onLog?.("Organisation root: no infrastructure changes \u2014 skipping deploy","info"),k=await Y(e,t,i,"Failed to read org stack outputs (non-critical)");const u=[],b=[];let M=$;if(l&&f.length>0){const{anyCascadeDeployHappened:o}=await ft(n,e,I,{providerAccounts:f,effectiveOrgConfig:K,totalSteps:c,cascadeErrors:u,allCascadeOutputs:b});if(o&&(M=!0),await pt(n,e,{orgOutputs:k,allCascadeOutputs:b,orgDetails:p.data,providerAccounts:f,defaultRegion:U}),u.length>0){const a=u.map(r=>` ${r.accountId}: ${r.error}`).join(`
2
+ `);t.onLog?.(g(`Cascade failed for ${u.length} target(s):
3
+ ${a}`),"warn")}}if(u.length>0){const o=u.map(r=>g(`${r.accountId}: ${r.error}`)).join(`
4
+ `),a=new Error(`Organisation root deployed, but the cascade failed for ${u.length} target(s):
5
+ ${o}`);return t.onError?.(a),v(a)}return V({target:I.target,deploymentType:"organisation",outputs:k,artefacts:[],...b.length>0?{cascadeOutputs:b}:{},...M?{}:{noChanges:!0},durationMs:Date.now()-z})}export{Et as deployOrgWithCascade};
@@ -5,11 +5,37 @@ import type { OrganisationOperation } from "../../types/operations.js";
5
5
  import type { DeployServices } from "../serviceFactory.js";
6
6
  export interface OrgDetailsForSynth {
7
7
  orgId: string;
8
- rootId: string;
8
+ /**
9
+ * Absent when the deploy credentials cannot list roots — a member-account
10
+ * single-component deploy. Only the Organisation stack consumes it.
11
+ */
12
+ rootId?: string;
9
13
  managementAccountId: string;
14
+ /**
15
+ * Development OU id, resolved only for organisation-tier synths (the dev
16
+ * isolation SCP is synthesised by the Organisation stack alone). Absent =
17
+ * the org has no development OU, or the deploy path never synthesises the
18
+ * org stack.
19
+ */
20
+ devOuId?: string;
10
21
  }
11
22
  export declare function buildOrgContext(params: DeployParams, services: DeployServices, operation: OrganisationOperation, deployType: "organisation" | "platform" | "account", orgDetails: OrgDetailsForSynth | undefined, accountName?: string, trailLifecycle?: ProviderAccount["trailLifecycle"], targetIsOrganisationTier?: boolean): import("../../types/deployment/DeploymentTypes.js").DeploymentContext;
12
- export declare function resolveOrgDetails(services: DeployServices, abortSignal?: AbortSignal): Promise<Result<OrgDetailsForSynth>>;
23
+ export declare function resolveOrgDetails(services: DeployServices, abortSignal?: AbortSignal): Promise<Result<OrgDetailsForSynth & {
24
+ rootId: string;
25
+ }>>;
26
+ /**
27
+ * Resolve org details for a single-component (account/platform) deploy in an
28
+ * org-managed estate. Read-only and member-safe: DescribeOrganization is
29
+ * callable from any account in the organisation, while ListRoots is
30
+ * management-account-only — the cascade path resolves full details under
31
+ * management credentials, but a standalone `deploy account` runs under the
32
+ * target account's own credentials and must not require (or create)
33
+ * organisation-level access. The Account and Platform stacks consume only
34
+ * orgId (the exported OrganisationId output), so a best-effort rootId is
35
+ * sufficient here. Resolves undefined when no organisation exists yet — the
36
+ * component synthesises standalone, exactly like the solo path.
37
+ */
38
+ export declare function resolveOrgDetailsForMemberComponent(services: DeployServices, abortSignal?: AbortSignal): Promise<Result<OrgDetailsForSynth | undefined>>;
13
39
  /**
14
40
  * Resolve org details for a SOLO-estate single-component deploy. Read-only — a
15
41
  * standalone account deploy must NEVER create an organisation (the mutating
@@ -1 +1 @@
1
- import{success as s,failure as u}from"@fjall/generator";import{OrganizationsClient as a}from"@aws-sdk/client-organizations";import{CdkContextBuilder as l}from"../../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as m}from"../../types/deployment/index.js";import{ensureOrganisationExists as I,describeOrganisation as f}from"../../aws/organisations/organisation.js";import{buildParamsContext as C,resolveAccountBootstrapSkipOidc as O}from"../contextHelpers.js";function w(o,n,r,t,e,d,c,g){const i=n.awsProvider.getRegion();return l.buildDeploymentContext({deployType:t,target:r.target,path:r.path,region:i,accountName:d,callerIdentity:m(n.awsProvider.getAccountId()),orgId:e?.orgId,rootId:e?.rootId,managementAccountId:e?.managementAccountId,...C({orgConfig:o.orgConfig,identity:o.identity,skipOidc:O(t,o.orgConfig,o.options?.skipOidc,g),...t!=="organisation"?{region:i,primaryRegion:o.orgConfig?.primaryRegion}:{},trailLifecycle:c})},{verbose:o.options?.verbose,infraOnly:o.options?.infraOnly},o.orgConfig)}async function P(o,n){const r=o.awsProvider.getClient(a),t=await I(r,n);return t.success?s({orgId:t.data.orgId,rootId:t.data.rootId,managementAccountId:t.data.managementAccountId}):u(t.error)}async function k(o,n){const r=o.awsProvider.getClient(a),t=await f(r,n,{tolerateAccessDenied:!0});if(!(!t.success||t.data===null))return{orgId:t.data.orgId,rootId:t.data.rootId,managementAccountId:t.data.managementAccountId}}export{w as buildOrgContext,P as resolveOrgDetails,k as resolveOrgDetailsForSolo};
1
+ import{success as i,failure as c}from"@fjall/generator";import{OrganizationsClient as d}from"@aws-sdk/client-organizations";import{CdkContextBuilder as l}from"../../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as I}from"../../types/deployment/index.js";import{ensureOrganisationExists as m,describeOrganisation as f,describeOrganisationAsMember as C}from"../../aws/organisations/organisation.js";import{buildParamsContext as O,resolveAccountBootstrapSkipOidc as v}from"../contextHelpers.js";function P(o,n,e,t,r,s,u,g){const a=n.awsProvider.getRegion();return l.buildDeploymentContext({deployType:t,target:e.target,path:e.path,region:a,accountName:s,callerIdentity:I(n.awsProvider.getAccountId()),orgId:r?.orgId,rootId:r?.rootId,managementAccountId:r?.managementAccountId,devOuId:r?.devOuId,...O({orgConfig:o.orgConfig,identity:o.identity,skipOidc:v(t,o.orgConfig,o.options?.skipOidc,g),...t!=="organisation"?{region:a,primaryRegion:o.orgConfig?.primaryRegion}:{},trailLifecycle:u})},{verbose:o.options?.verbose,infraOnly:o.options?.infraOnly},o.orgConfig)}async function k(o,n){const e=o.awsProvider.getClient(d),t=await m(e,n);return t.success?i({orgId:t.data.orgId,rootId:t.data.rootId,managementAccountId:t.data.managementAccountId}):c(t.error)}async function R(o,n){const e=o.awsProvider.getClient(d),t=await C(e,n);return t.success?t.data===null?i(void 0):i({orgId:t.data.orgId,managementAccountId:t.data.managementAccountId,...t.data.rootId!==void 0?{rootId:t.data.rootId}:{}}):c(t.error)}async function h(o,n){const e=o.awsProvider.getClient(d),t=await f(e,n,{tolerateAccessDenied:!0});if(!(!t.success||t.data===null))return{orgId:t.data.orgId,rootId:t.data.rootId,managementAccountId:t.data.managementAccountId}}export{P as buildOrgContext,k as resolveOrgDetails,R as resolveOrgDetailsForMemberComponent,h as resolveOrgDetailsForSolo};
@@ -1 +1 @@
1
- import{success as N}from"@fjall/generator";import{BackupClient as w}from"@aws-sdk/client-backup";import{IAMClient as D}from"@aws-sdk/client-iam";import{getOrganisationStackName as I}from"../../types/operations.js";import{accountHasDisasterRecovery as k,describeBackupVaultExists as y}from"../../aws/organisations/backup.js";import{describeAccountGlobalsExist as L,buildMissingAccountGlobalsAdvisory as T}from"../../aws/organisations/accountGlobals.js";import{targetsNonPrimaryRegion as h,synthOrFail as v,bootstrapOrFail as F,forwardOutput as M,forwardResourceProgress as G}from"../contextHelpers.js";import{accountTier as S}from"@fjall/util";import{buildOrgContext as Y,resolveOrgDetails as x,resolveOrgDetailsForSolo as B}from"./orgContext.js";import{hasOrganisationTierAccount as V}from"../../aws/organisations/accountGlobals.js";import{INFRA_STEPS as o,INFRA_STEP_TOTAL as i,failPrepareStep as c,maskAndFail as d,readStackOutputsBestEffort as _}from"./infraSteps.js";async function ot(r,n,a,s,E){const{callbacks:t}=r;t.onStepComplete?.(o.CONNECT.id,o.CONNECT.name,"completed",0,i),t.onStepStart?.(o.PREPARE.id,o.PREPARE.name,1,i);const R=n.awsProvider.getRegion(),l=r.orgConfig?.primaryRegion;if(l!==void 0&&h(R,l)){const e=await L(n.awsProvider.getClient(D),r.abortSignal);if(!e.success)return c(t),d(t,e.error.message);if(!e.data)return c(t),d(t,T({target:a.target,deployRegion:R,primaryRegion:l,...r.orgConfig!==void 0?{providerAccounts:r.orgConfig.providerAccounts}:{}}))}let f;if(V(r.orgConfig?.providerAccounts)){const e=await x(n,r.abortSignal);if(!e.success)return c(t),d(t,e.error.message);f=e.data}else f=await B(n,r.abortSignal);const u=s==="account"?r.orgConfig?.providerAccounts.find(e=>e.name===a.target):r.orgConfig?.providerAccounts.find(e=>S(e)==="platform"),A=s==="account"&&u!==void 0&&S(u)==="organisation",g=Y(r,n,a,s,f,s==="account"?a.target:void 0,u?.trailLifecycle,A),m=r.orgConfig?.disasterRecoveryRegion;if(s==="account"&&(m!==void 0&&m!=="")&&(u===void 0||k(u.environment,m))){const e=await y(n.awsProvider.getClient(w),r.abortSignal);if(!e.success)return c(t),d(t,e.error.message);g.fjallAdoptBackupVault=e.data}t.onLog?.(`Synthesising ${s} infrastructure\u2026`,"info");const p=await v(n,g,t,"CDK synthesis failed");if(!p.success)return c(t),p;const O=await F(n,g,t);if(!O.success)return c(t),O;t.onStepComplete?.(o.PREPARE.id,o.PREPARE.name,"completed",1,i);const P=I(a.type);t.onStepStart?.(o.DEPLOY.id,o.DEPLOY.name,2,i);const C=await n.cdkService.runCdkDeploy(g,P,M(t),G(t),n.awsProvider);if(!C.success)return t.onStepComplete?.(o.DEPLOY.id,o.DEPLOY.name,"error",2,i),d(t,C.error);t.onStepComplete?.(o.DEPLOY.id,o.DEPLOY.name,"completed",2,i);const b=await _(n,t,P,"Failed to read stack outputs (non-critical)");return t.onStepStart?.(o.MONITORING.id,o.MONITORING.name,3,i),t.onStepComplete?.(o.MONITORING.id,o.MONITORING.name,"completed",3,i),N({target:a.target,deploymentType:"organisation",outputs:b,artefacts:[],durationMs:Date.now()-E})}export{ot as deploySingleComponent};
1
+ import{join as h}from"path";import{failure as A,success as v}from"@fjall/generator";import{BackupClient as y}from"@aws-sdk/client-backup";import{IAMClient as T}from"@aws-sdk/client-iam";import{logger as M}from"@fjall/util/logger";import{getOrganisationStackName as G}from"../../types/operations.js";import{accountHasDisasterRecovery as Y,describeBackupVaultExists as x}from"../../aws/organisations/backup.js";import{describeAccountGlobalsExist as B,buildMissingAccountGlobalsAdvisory as V}from"../../aws/organisations/accountGlobals.js";import{targetsNonPrimaryRegion as _,reconcileOidcProviderIfCarried as j,synthOrFail as $,bootstrapOrFail as H,forwardOutput as K,forwardResourceProgress as q}from"../contextHelpers.js";import{accountTier as k,getErrorMessage as z,maskSensitiveOutput as N}from"@fjall/util";import{runDriftPreFlightGate as J}from"../drift/preFlightGate.js";import{completeForgetAfterDeploy as Q}from"../remediation/forgetResource.js";import{composePostFailureRepairOffer as U}from"../remediation/postFailureRepairOffer.js";import{buildOrgContext as W,resolveOrgDetailsForMemberComponent as X,resolveOrgDetailsForSolo as Z}from"./orgContext.js";import{hasOrganisationTierAccount as tt}from"../../aws/organisations/accountGlobals.js";import{INFRA_STEPS as n,INFRA_STEP_TOTAL as a,emitOrgStackTargets as ot,failPrepareStep as c,maskAndFail as l,readStackOutputsBestEffort as et}from"./infraSteps.js";import{assertLeaseBeforeStack as rt}from"../application/leaseGate.js";async function Et(o,r,d,g,D){const{callbacks:t}=o;t.onStepComplete?.(n.CONNECT.id,n.CONNECT.name,"completed",0,a),t.onStepStart?.(n.PREPARE.id,n.PREPARE.name,1,a);const P=r.awsProvider.getRegion(),f=o.orgConfig?.primaryRegion;if(f!==void 0&&_(P,f)){const e=await B(r.awsProvider.getClient(T),o.abortSignal);if(!e.success)return c(t),l(t,e.error.message);if(!e.data)return c(t),l(t,V({target:d.target,deployRegion:P,primaryRegion:f,...o.orgConfig!==void 0?{providerAccounts:o.orgConfig.providerAccounts}:{}}))}let m;if(tt(o.orgConfig?.providerAccounts)){const e=await X(r,o.abortSignal);if(!e.success)return c(t),l(t,e.error.message);m=e.data}else m=await Z(r,o.abortSignal);const s=g==="account"?o.orgConfig?.providerAccounts.find(e=>e.id===r.awsProvider.getAccountId()):o.orgConfig?.providerAccounts.find(e=>k(e)==="platform"),I=g==="account"&&s!==void 0&&k(s)==="organisation",u=W(o,r,d,g,m,g==="account"?s?.name??d.target:void 0,s?.trailLifecycle,I),S=o.orgConfig?.disasterRecoveryRegion;if(g==="account"&&(S!==void 0&&S!=="")&&(s===void 0||Y(s.environment,S))){const e=await x(r.awsProvider.getClient(y),o.abortSignal);if(!e.success)return c(t),l(t,e.error.message);u.fjallAdoptBackupVault=e.data}t.onLog?.(`Synthesising ${g} infrastructure\u2026`,"info");const R=await $(r,u,t,"CDK synthesis failed");if(!R.success)return c(t),R;const i=G(d.type),b=r.awsProvider.getAccountId(),O=r.awsProvider.getRegion();ot(t,[{stackName:i,...b!==""?{accountId:b}:{},...O!==""?{region:O}:{}}]);const w=await J(r.awsProvider,{stackNames:[i],cdkOutPath:h(d.path,"cdk.out"),cfnService:r.cfnService,callbacks:t,...o.abortSignal!==void 0?{abortSignal:o.abortSignal}:{}});if(w.kind==="blocked")return c(t),l(t,w.message);const C=await H(r,u,t);if(!C.success)return c(t),C;t.onStepComplete?.(n.PREPARE.id,n.PREPARE.name,"completed",1,a);const p=await rt(o.assertLeaseForStack,i,o.abortSignal);if(p!==void 0)return t.onError?.(p),A(p);t.onStepStart?.(n.DEPLOY.id,n.DEPLOY.name,2,a);const E=await r.cdkService.runCdkDeploy(u,i,K(t),q(t),r.awsProvider);if(!E.success){t.onStepComplete?.(n.DEPLOY.id,n.DEPLOY.name,"error",2,a);const e=await U(r.awsProvider,{stackNames:[i],repairTarget:`--stack ${i}`,originalError:new Error(N(E.error)),cfnService:r.cfnService,...o.abortSignal!==void 0?{abortSignal:o.abortSignal}:{}});return t.onError?.(e),A(e)}t.onStepComplete?.(n.DEPLOY.id,n.DEPLOY.name,"completed",2,a);const F=await et(r,t,i,"Failed to read stack outputs (non-critical)");return await j(u,r.awsProvider,t,o.abortSignal),t.onStepStart?.(n.MONITORING.id,n.MONITORING.name,3,a),t.onStepComplete?.(n.MONITORING.id,n.MONITORING.name,"completed",3,a),await Q(r.awsProvider,{stackNames:[i],onLog:(e,L)=>t.onLog?.(e,L),...o.abortSignal!==void 0?{abortSignal:o.abortSignal}:{}}).catch(e=>{M.debug("SingleComponentDeploy","Forget completion sweep failed",{error:N(z(e))})}),v({target:d.target,deploymentType:"organisation",outputs:F,artefacts:[],durationMs:Date.now()-D})}export{Et as deploySingleComponent};
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The `fjall drift repair` ceremony — the drift-flavoured twin of the deploy
3
+ * destruction gate. Builds a `DestructionTicket` from CONFIRMED drift
4
+ * findings (never unconfirmed — fail-closed verdicts refuse remediation) and
5
+ * evaluates `--remediate` consents through the SAME engine primitives
6
+ * (`evaluateDestructionConsents`, digest TOCTOU, [G6] verdict table,
7
+ * `executableRemediationVerbs`) so the two ceremonies cannot drift apart.
8
+ *
9
+ * Deliberately no plan loader and no interactive resolver: repair consents
10
+ * arrive as flags ([G5] — the agent surface teaches the flag SHAPE, the
11
+ * operator types the physical name). All output lines are identifier-only
12
+ * (physical names, verbs) — pre-masked by construction.
13
+ */
14
+ import { type DriftFinding } from "../../types/drift.js";
15
+ import type { DestructionConsent, DestructionConsentReport, DestructionOutcome, DestructionTicket, RemediationVerb } from "../../types/destruction.js";
16
+ /**
17
+ * Build the repair ticket from confirmed-deleted findings. Returns undefined
18
+ * when nothing is confirmed (unconfirmed/stack-missing stay repair-only and
19
+ * never ticket). `physicalName` is the probed live physical id ([G6] — the
20
+ * exact string the operator must type); findings without one (membership
21
+ * listing could not resolve an id) fall back to the
22
+ * `remediationPlaceholderPhysicalId` address, same as the plan gate — the
23
+ * shared builder is what keeps this [G6] address byte-agreed with the
24
+ * journal target's physicalId, so the consent covers the target operated on.
25
+ */
26
+ export declare function buildDriftRepairTicket(findings: readonly DriftFinding[]): DestructionTicket | undefined;
27
+ /**
28
+ * [I1] The verbs a drift-DETECT surface may ADVERTISE for one finding — the
29
+ * verdict vocabulary reduced to what this gate would let execute. The
30
+ * drift-detect twin of `advertisableRemediationVerbs`: a confirmed-deleted
31
+ * finding tickets as `resource-deleted` (see `buildDriftRepairTicket` above),
32
+ * whose only execution arm is forget, so advertising recreate sells a consent
33
+ * the execution check withholds (consent-not-executable). Non-ticketing
34
+ * verdicts (unconfirmed, stack-missing, present) keep their vocabulary
35
+ * unchanged — they never enter the consent ceremony, so there is no gate to
36
+ * disagree with and `repair` stays the honest report-only teach.
37
+ * Consumers: the CLI drift-detect agent block rows + confirmed-verbs help
38
+ * line and the human `formatVerbList` line.
39
+ */
40
+ export declare function advertisableDriftVerbs(finding: Pick<DriftFinding, "verdict" | "verbs">): RemediationVerb[];
41
+ export interface RunDriftRepairGateParams {
42
+ ticket: DestructionTicket;
43
+ consents: readonly DestructionConsent[];
44
+ /** Digest the consents were minted against (agent two-step) — TOCTOU. */
45
+ consentDigest?: string;
46
+ }
47
+ export type DriftRepairGateOutcome =
48
+ /** Every resource consented with an executable verb — run the surgery. */
49
+ {
50
+ kind: "proceed";
51
+ /** The consented verb per exact physical name. */
52
+ verbsByPhysicalName: ReadonlyMap<string, RemediationVerb>;
53
+ report: DestructionConsentReport;
54
+ } | {
55
+ kind: "withheld";
56
+ outcome: DestructionOutcome;
57
+ report: DestructionConsentReport;
58
+ reasons: string[];
59
+ };
60
+ /**
61
+ * Evaluate repair consents — the exact fail-closed semantics of the deploy
62
+ * gate (matched/unmatched/wrong-verb rows, uncovered fails closed, digest
63
+ * mismatch stales everything, abort verb = operator decline, non-executable
64
+ * verb withholds truthfully).
65
+ */
66
+ export declare function runDriftRepairGate(params: RunDriftRepairGateParams): DriftRepairGateOutcome;
@@ -0,0 +1 @@
1
+ import{remediationPlaceholderPhysicalId as u}from"../../types/remediation.js";import{computeTicketDigest as f,evaluateDestructionConsents as p,executableRemediationVerbs as n,legalRemediationVerbs as m}from"../application/destructionGate.js";const b="drift-repair";function g(c){const i=c.filter(e=>e.verdict==="confirmed-deleted").map(e=>({stack:e.stackName,logicalId:e.logicalId,resourceType:e.resourceType,physicalName:e.physicalId??u(e.stackName,e.logicalId),finding:"resource-deleted",cause:`deleted outside CloudFormation \u2014 ${e.verdictDetail}`,offendingProperties:[],verdictSource:"drift-probe",dataLossRisk:!1,verbs:[...m("resource-deleted")]}));if(i.length!==0)return{ticketDigest:f(b,i),resources:i}}function D(c){if(c.verdict!=="confirmed-deleted")return[...c.verbs];const i=n("resource-deleted");return c.verbs.filter(e=>i.includes(e))}function N(c){const{ticket:i,consents:e,consentDigest:d}=c,o=p({ticket:i,consents:e,...d!==void 0?{consentDigest:d}:{}}),l=new Map(i.resources.map(t=>[t.physicalName,t])),s=[],a=[];if(o.satisfied)for(const t of o.rows){if(t.verdict!=="matched")continue;const r=l.get(t.physicalName);if(r!==void 0){if(t.verb==="abort"){s.push(`${t.physicalName}: aborted by operator consent`);continue}n(r.finding).includes(t.verb)||a.push(`${t.physicalName}: verb '${t.verb}' is accepted vocabulary but not yet executable via 'fjall drift repair' for ${r.finding} \u2014 executable today: ${n(r.finding).join(", ")||"none (abort only)"}`)}}return o.satisfied&&s.length===0&&a.length===0?{kind:"proceed",verbsByPhysicalName:new Map(o.rows.filter(r=>r.verdict==="matched").map(r=>[r.physicalName,r.verb])),report:o}:{kind:"withheld",outcome:s.length>0?"declined":a.length>0?"consent-not-executable":o.rows.length>0||o.digestMismatch?"consents-invalid":"awaiting-consent",report:o,reasons:[...s,...a]}}export{D as advertisableDriftVerbs,g as buildDriftRepairTicket,N as runDriftRepairGate};
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Forensics barrier for remediation surgery (drift-remediation plan § 3.2
3
+ * phase 1, Q13). Persists the pre-mutation evidence — `GetTemplate(Original)`,
4
+ * `DescribeStacks`, the last 50 stack events, and the drift-probe findings —
5
+ * to `~/.fjall/forensics/remediation/<opId>/<capturedAt>/` BEFORE any
6
+ * mutation. The capture-stamp segment scopes the bundle to ONE incarnation of
7
+ * the opId (opId has no incarnation component): an archive-and-restart of the
8
+ * same stack + logical-ID set captures into its OWN directory instead of
9
+ * replacing the prior incarnation's evidence, so an archived done journal's
10
+ * `forensicsPath` keeps resolving to the artefacts it recorded.
11
+ *
12
+ * Capture failure ABORTS the operation (the `onPolicyCaptured` precedent,
13
+ * unlockBucket.ts): no capture → no mutate. Every artefact is masked before
14
+ * persisting (boundary 4 — stack-event reasons and template bodies can embed
15
+ * credential values) and written atomically (tmp + rename).
16
+ *
17
+ * Failure messages are pre-masked at construction — callers may surface them
18
+ * to callback/log sinks directly.
19
+ */
20
+ import { type Result } from "@fjall/generator";
21
+ import type { AwsProvider } from "../../aws/AwsProvider.js";
22
+ import type { DriftFinding } from "../../types/drift.js";
23
+ export interface ForensicsCaptureOptions {
24
+ /** Override the forensics root — tests point this at a temp dir. */
25
+ forensicsDir?: string;
26
+ /** Injectable clock for the manifest timestamp. */
27
+ now?: () => Date;
28
+ }
29
+ export declare function defaultRemediationForensicsDir(): string;
30
+ export interface CaptureRemediationForensicsParams {
31
+ stackName: string;
32
+ opId: string;
33
+ /** The drift-probe evidence that motivated the surgery. */
34
+ findings: readonly DriftFinding[];
35
+ abortSignal?: AbortSignal;
36
+ }
37
+ export interface RemediationForensicsBundle {
38
+ forensicsPath: string;
39
+ files: string[];
40
+ /** The raw live template body — reused by the flip derivation so the flip
41
+ * provably derives from the same bytes the forensics preserved. */
42
+ liveTemplateBody: string;
43
+ /** The stack's current capabilities, echoed on the flip UpdateStack. */
44
+ capabilities: string[];
45
+ /** Live parameter keys — echoed UsePreviousValue on both flip CFN calls. */
46
+ parameterKeys: string[];
47
+ stackStatus: string;
48
+ }
49
+ export declare function captureRemediationForensics(aws: AwsProvider, params: CaptureRemediationForensicsParams, options?: ForensicsCaptureOptions): Promise<Result<RemediationForensicsBundle, Error>>;
@@ -0,0 +1 @@
1
+ import{CloudFormationClient as F,DescribeStackEventsCommand as O,DescribeStacksCommand as D,GetTemplateCommand as B}from"@aws-sdk/client-cloudformation";import{mkdir as K,rename as _,writeFile as x}from"node:fs/promises";import{homedir as I}from"node:os";import{join as u}from"node:path";import{success as J,failure as c}from"@fjall/generator";import{getErrorMessage as $,maskSensitiveOutput as p}from"@fjall/util";import{composeSdkAbortSignal as g,isAborted as M}from"../../aws/organisations/types.js";const S=50,R=20;function G(){return u(I(),".fjall","forensics","remediation")}async function q(A,k,b){const{stackName:t,opId:w,abortSignal:o}=k,l=A.getClient(F);let m,v,h,y,E,d,T;try{const e=await l.send(new B({StackName:t,TemplateStage:"Original"}),{abortSignal:g(o)});if(e.TemplateBody===void 0||e.TemplateBody==="")return c(new Error(`Forensics capture aborted the remediation of ${t}: GetTemplate returned an empty template body \u2014 nothing was mutated.`));m=e.TemplateBody;const n=(await l.send(new D({StackName:t}),{abortSignal:g(o)})).Stacks?.[0];if(n===void 0)return c(new Error(`Forensics capture aborted the remediation of ${t}: DescribeStacks returned no stack \u2014 nothing was mutated.`));v=n,h=n.Capabilities??[],y=(n.Parameters??[]).flatMap(r=>r.ParameterKey!==void 0?[r.ParameterKey]:[]),E=n.StackStatus??"UNKNOWN";const s=[];let a,C=0;do{if(M(o))throw new Error(`aborted while reading the stack events of ${t}`);const r=await l.send(new O({StackName:t,...a!==void 0?{NextToken:a}:{}}),{abortSignal:g(o)});s.push(...r.StackEvents??[]),a=r.NextToken,C+=1}while(a!==void 0&&s.length<S&&C<R);T=a!==void 0||s.length>S,d=s.slice(0,S)}catch(e){return c(new Error(`Forensics capture failed for ${t} \u2014 aborting before any mutation: ${p($(e))}`))}const P=b?.forensicsDir??G(),N=(b?.now?.()??new Date).toISOString(),i=u(P,w,N.replace(/[^0-9A-Za-z.-]/g,"")),j=[{name:"live-template.json",content:m},{name:"stack-description.json",content:JSON.stringify(v,null,2)},{name:"stack-events.json",content:JSON.stringify(d,null,2)},{name:"drift-findings.json",content:JSON.stringify(k.findings,null,2)},{name:"manifest.json",content:JSON.stringify({opId:w,stackName:t,capturedAt:N,files:["live-template.json","stack-description.json","stack-events.json","drift-findings.json"],...T?{eventsTruncated:!0,eventsCaptured:d.length}:{}},null,2)}];try{await K(i,{recursive:!0});for(const e of j){const f=u(i,e.name),n=`${f}.tmp-${process.pid}`;await x(n,p(e.content),"utf8"),await _(n,f)}}catch(e){return c(new Error(`Forensics capture could not persist the evidence bundle for ${t} at ${i} \u2014 aborting before any mutation: ${p($(e))}`))}return J({forensicsPath:i,files:j.map(e=>e.name),liveTemplateBody:m,capabilities:h,parameterKeys:y,stackStatus:E})}export{q as captureRemediationForensics,G as defaultRemediationForensicsDir};