@fjall/deploy-core 2.33.0 → 3.0.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 (210) hide show
  1. package/dist/.minified +1 -1
  2. package/dist/src/aws/SimpleAwsProvider.js +1 -1
  3. package/dist/src/aws/cloudtrail/orgTrailDelivery.js +1 -1
  4. package/dist/src/aws/organisations/accountGlobals.js +1 -1
  5. package/dist/src/aws/organisations/accounts.js +1 -1
  6. package/dist/src/aws/organisations/backup.js +2 -2
  7. package/dist/src/aws/organisations/costAllocation.js +1 -1
  8. package/dist/src/aws/organisations/delegatedAdmin.js +2 -2
  9. package/dist/src/aws/organisations/identityCentre.js +1 -1
  10. package/dist/src/aws/organisations/identityCentreSnapshot.js +1 -1
  11. package/dist/src/aws/organisations/importedAccounts.js +1 -1
  12. package/dist/src/aws/organisations/ipam.js +1 -1
  13. package/dist/src/aws/organisations/oidcProvider.js +1 -1
  14. package/dist/src/aws/organisations/organisation.js +1 -1
  15. package/dist/src/aws/organisations/organisationalUnits.js +1 -1
  16. package/dist/src/aws/organisations/pagination.js +1 -1
  17. package/dist/src/aws/organisations/policies.js +1 -1
  18. package/dist/src/aws/organisations/ram.js +1 -1
  19. package/dist/src/aws/organisations/rootAccess.js +3 -3
  20. package/dist/src/aws/organisations/serviceAccess.js +1 -1
  21. package/dist/src/aws/organisations/trustedAccess.js +1 -1
  22. package/dist/src/aws/organisations/types.js +1 -1
  23. package/dist/src/aws/regions/regionOptStatus.js +1 -1
  24. package/dist/src/aws/sts/assumeRoot.js +1 -1
  25. package/dist/src/aws/targetReadiness.js +1 -1
  26. package/dist/src/aws/targetSetAdvisory.js +1 -1
  27. package/dist/src/aws/utils/arnParser.js +1 -1
  28. package/dist/src/aws/utils/awsErrorHandler.js +1 -1
  29. package/dist/src/aws/utils/cloudformationEventHelpers.js +1 -1
  30. package/dist/src/aws/utils/cloudformationEvents.js +1 -1
  31. package/dist/src/index.d.ts +4 -2
  32. package/dist/src/index.js +1 -1
  33. package/dist/src/orchestration/activeDeploymentGuard.js +5 -5
  34. package/dist/src/orchestration/application/applicationDeploy.js +1 -1
  35. package/dist/src/orchestration/application/applicationDeployHelpers.js +3 -3
  36. package/dist/src/orchestration/application/applicationDestroy.js +1 -1
  37. package/dist/src/orchestration/application/approvalGate.js +1 -1
  38. package/dist/src/orchestration/application/artefactCollection.js +1 -1
  39. package/dist/src/orchestration/application/buildArgResolver.js +1 -1
  40. package/dist/src/orchestration/application/buildGroupValidator.js +1 -1
  41. package/dist/src/orchestration/application/buildSecretResolver.js +1 -1
  42. package/dist/src/orchestration/application/buildSecretSession.js +1 -1
  43. package/dist/src/orchestration/application/codeOnlyDeploy.js +1 -1
  44. package/dist/src/orchestration/application/databaseEndpointReconcile.js +1 -1
  45. package/dist/src/orchestration/application/deploySessionPolicy.js +1 -1
  46. package/dist/src/orchestration/application/destroyPreFlight.js +1 -1
  47. package/dist/src/orchestration/application/destructionGate.js +3 -3
  48. package/dist/src/orchestration/application/detectionPipeline.js +1 -1
  49. package/dist/src/orchestration/application/dockerBuildHelper.js +1 -1
  50. package/dist/src/orchestration/application/ecrCacheRepository.js +1 -1
  51. package/dist/src/orchestration/application/lambdaCodeOnlyRollout.js +1 -1
  52. package/dist/src/orchestration/application/leaseGate.js +1 -1
  53. package/dist/src/orchestration/application/openNextBuild.js +3 -3
  54. package/dist/src/orchestration/application/plan/approvalToken.js +1 -1
  55. package/dist/src/orchestration/application/plan/assemblyDigest.js +2 -2
  56. package/dist/src/orchestration/application/plan/buildDeployPlan.js +1 -1
  57. package/dist/src/orchestration/application/plan/changeSetEscalation.js +1 -1
  58. package/dist/src/orchestration/application/plan/classify.js +1 -1
  59. package/dist/src/orchestration/application/plan/computeDeployPlan.js +1 -1
  60. package/dist/src/orchestration/application/plan/loadDeployPlan.js +1 -1
  61. package/dist/src/orchestration/application/plan/pinnedNames.js +1 -1
  62. package/dist/src/orchestration/application/plan/registryOverlay.js +1 -1
  63. package/dist/src/orchestration/application/plan/renderDeployPlan.js +1 -1
  64. package/dist/src/orchestration/application/serviceImageTagsRollback.js +1 -1
  65. package/dist/src/orchestration/application/taskDefinitionRoll.js +1 -1
  66. package/dist/src/orchestration/application/welcomeImageHelper.js +1 -1
  67. package/dist/src/orchestration/builders/dockerBuilder.js +1 -1
  68. package/dist/src/orchestration/builders/frameworkRegistry.js +1 -1
  69. package/dist/src/orchestration/builders/openNextBuilder.js +1 -1
  70. package/dist/src/orchestration/builders/staticSiteBuilder.js +2 -2
  71. package/dist/src/orchestration/contextHelpers.js +1 -1
  72. package/dist/src/orchestration/deploy.js +1 -1
  73. package/dist/src/orchestration/destroy.js +1 -1
  74. package/dist/src/orchestration/dockerBuildParamsKeys.js +1 -1
  75. package/dist/src/orchestration/dockerInterface.d.ts +16 -0
  76. package/dist/src/orchestration/domain/caaPreflight.d.ts +56 -0
  77. package/dist/src/orchestration/domain/caaPreflight.js +1 -0
  78. package/dist/src/orchestration/domain/delegatedDomainDeploy.d.ts +79 -0
  79. package/dist/src/orchestration/domain/delegatedDomainDeploy.js +1 -0
  80. package/dist/src/orchestration/domain/delegationDestroyMirror.d.ts +141 -0
  81. package/dist/src/orchestration/domain/delegationDestroyMirror.js +1 -0
  82. package/dist/src/orchestration/domain/index.d.ts +10 -0
  83. package/dist/src/orchestration/domain/index.js +1 -0
  84. package/dist/src/orchestration/domain/nsPropagationGate.d.ts +59 -0
  85. package/dist/src/orchestration/domain/nsPropagationGate.js +1 -0
  86. package/dist/src/orchestration/domain/types.d.ts +75 -0
  87. package/dist/src/orchestration/domain/types.js +1 -0
  88. package/dist/src/orchestration/domainInterface.d.ts +10 -0
  89. package/dist/src/orchestration/drift/classifyDriftFailure.js +1 -1
  90. package/dist/src/orchestration/drift/detectStackDrift.js +1 -1
  91. package/dist/src/orchestration/drift/driftPreFlight.js +5 -5
  92. package/dist/src/orchestration/drift/driftProbe.d.ts +12 -6
  93. package/dist/src/orchestration/drift/driftProbe.js +1 -1
  94. package/dist/src/orchestration/drift/driftSuspectJournal.js +1 -1
  95. package/dist/src/orchestration/drift/preFlightGate.d.ts +10 -2
  96. package/dist/src/orchestration/drift/preFlightGate.js +2 -2
  97. package/dist/src/orchestration/drift/route53Names.d.ts +35 -0
  98. package/dist/src/orchestration/drift/route53Names.js +1 -0
  99. package/dist/src/orchestration/drift/route53RecordPreflight.d.ts +105 -0
  100. package/dist/src/orchestration/drift/route53RecordPreflight.js +2 -0
  101. package/dist/src/orchestration/index.d.ts +3 -1
  102. package/dist/src/orchestration/index.js +1 -1
  103. package/dist/src/orchestration/manifestSecretParser.js +1 -1
  104. package/dist/src/orchestration/organisation/accountsConfig.js +1 -1
  105. package/dist/src/orchestration/organisation/cascadeDestroyHelpers.js +1 -1
  106. package/dist/src/orchestration/organisation/cascadeHelpers.d.ts +18 -1
  107. package/dist/src/orchestration/organisation/cascadeHelpers.js +1 -1
  108. package/dist/src/orchestration/organisation/cascadeSummary.js +1 -1
  109. package/dist/src/orchestration/organisation/organisationDeploy.js +1 -1
  110. package/dist/src/orchestration/organisation/organisationDestroy.js +2 -2
  111. package/dist/src/orchestration/organisation/organisationSetup.js +1 -1
  112. package/dist/src/orchestration/organisation/reconcileProviderAccounts.js +1 -1
  113. package/dist/src/orchestration/organisationDeploy/cascadeExecution.js +1 -1
  114. package/dist/src/orchestration/organisationDeploy/infraSteps.js +1 -1
  115. package/dist/src/orchestration/organisationDeploy/orgCascadeDeploy.js +3 -3
  116. package/dist/src/orchestration/organisationDeploy/orgContext.js +1 -1
  117. package/dist/src/orchestration/organisationDeploy/resolveCascadeAccounts.js +1 -1
  118. package/dist/src/orchestration/organisationDeploy/singleComponentDeploy.js +1 -1
  119. package/dist/src/orchestration/organisationDeploy/trailReconciliation.js +1 -1
  120. package/dist/src/orchestration/probes/deleteBlockProbes.js +1 -1
  121. package/dist/src/orchestration/remediation/driftRepairGate.js +1 -1
  122. package/dist/src/orchestration/remediation/forensicsCapture.js +1 -1
  123. package/dist/src/orchestration/remediation/forgetResource.js +1 -1
  124. package/dist/src/orchestration/remediation/pinRemediation.js +1 -1
  125. package/dist/src/orchestration/remediation/postFailureRepairOffer.js +8 -8
  126. package/dist/src/orchestration/remediation/recreatePreFlight.js +1 -1
  127. package/dist/src/orchestration/remediation/recreateResource.js +1 -1
  128. package/dist/src/orchestration/remediation/remediationJournal.js +2 -2
  129. package/dist/src/orchestration/remediation/removalUpdate.js +1 -1
  130. package/dist/src/orchestration/remediation/retainFlip.js +1 -1
  131. package/dist/src/orchestration/resolveOperation.js +1 -1
  132. package/dist/src/orchestration/restart/restartApplication.js +1 -1
  133. package/dist/src/orchestration/restart/secretsDrift.js +1 -1
  134. package/dist/src/orchestration/secretArnResolver.js +1 -1
  135. package/dist/src/orchestration/serviceFactory.js +1 -1
  136. package/dist/src/orchestration/spawnHelpers.js +1 -1
  137. package/dist/src/orchestration/stackCleanup/bucketOps.js +1 -1
  138. package/dist/src/orchestration/stackCleanup/failedStack.js +1 -1
  139. package/dist/src/orchestration/stackCleanup/logging.js +1 -1
  140. package/dist/src/orchestration/stackCleanup/messages.js +1 -1
  141. package/dist/src/orchestration/stackCleanup/orphanSweep.js +1 -1
  142. package/dist/src/orchestration/stackCleanup/preEmpty.js +1 -1
  143. package/dist/src/orchestration/stackCleanup/stackResources.js +1 -1
  144. package/dist/src/orchestration/stepLifecycle.js +1 -1
  145. package/dist/src/orchestration/trailMigration/memberTrailCleanup.js +1 -1
  146. package/dist/src/orchestration/trailMigration/trailMigration.js +1 -1
  147. package/dist/src/orchestration/unlock/bucketPolicyTriage.js +1 -1
  148. package/dist/src/orchestration/unlock/restoreAndReconcileQuarantinedBucket.js +1 -1
  149. package/dist/src/orchestration/unlock/restoreBucketPolicy.js +1 -1
  150. package/dist/src/orchestration/unlock/scpRemediation.js +1 -1
  151. package/dist/src/orchestration/unlock/toResourcePolicyStatements.js +1 -1
  152. package/dist/src/orchestration/unlock/unlockBucket.js +1 -1
  153. package/dist/src/orchestration/unlock/unlockQueue.js +1 -1
  154. package/dist/src/services/application/ApplicationStackService.d.ts +7 -0
  155. package/dist/src/services/application/ApplicationStackService.js +1 -1
  156. package/dist/src/services/application/applicationStackHelpers.js +3 -3
  157. package/dist/src/services/infrastructure/CdkArgumentBuilder.js +1 -1
  158. package/dist/src/services/infrastructure/CdkCommandRunner.js +2 -2
  159. package/dist/src/services/infrastructure/CdkErrorFormatter.js +10 -10
  160. package/dist/src/services/infrastructure/CdkEventMonitoring.js +1 -1
  161. package/dist/src/services/infrastructure/CdkOutputAnalyser.js +2 -2
  162. package/dist/src/services/infrastructure/CdkOutputParser.js +2 -2
  163. package/dist/src/services/infrastructure/CdkProcessManager.js +3 -3
  164. package/dist/src/services/infrastructure/CdkService.d.ts +1 -1
  165. package/dist/src/services/infrastructure/CdkService.js +2 -2
  166. package/dist/src/services/infrastructure/CdkServiceTypes.d.ts +39 -0
  167. package/dist/src/services/infrastructure/CfnRegistryService.js +1 -1
  168. package/dist/src/services/infrastructure/CloudFormationService.js +1 -1
  169. package/dist/src/services/infrastructure/EcrImageInspectorService.js +1 -1
  170. package/dist/src/services/infrastructure/EcsService.js +1 -1
  171. package/dist/src/services/infrastructure/EcsServiceResolver.js +1 -1
  172. package/dist/src/services/infrastructure/LambdaService.js +1 -1
  173. package/dist/src/services/infrastructure/SsmParameterMetadataService.js +1 -1
  174. package/dist/src/services/infrastructure/cancelInFlightStack.js +3 -3
  175. package/dist/src/services/infrastructure/cdkServiceHelpers.js +1 -1
  176. package/dist/src/services/infrastructure/changeSetProbe.js +1 -1
  177. package/dist/src/services/infrastructure/constructMapEnrichment.js +1 -1
  178. package/dist/src/services/supporting/CdkContextBuilder.d.ts +1 -0
  179. package/dist/src/services/supporting/CdkContextBuilder.js +1 -1
  180. package/dist/src/services/supporting/TemplateHashService.js +1 -1
  181. package/dist/src/services/supporting/helpers.js +1 -1
  182. package/dist/src/steps/stepRegistry.js +1 -1
  183. package/dist/src/types/ProgressEvent.js +1 -1
  184. package/dist/src/types/application/ApplicationServiceTypes.js +1 -1
  185. package/dist/src/types/callbacks.js +1 -1
  186. package/dist/src/types/config/FjallState.js +1 -1
  187. package/dist/src/types/constants.js +1 -1
  188. package/dist/src/types/deployEmitter.js +1 -1
  189. package/dist/src/types/deployEvent.d.ts +1 -0
  190. package/dist/src/types/deployEvent.js +1 -1
  191. package/dist/src/types/deployEventParse.js +1 -1
  192. package/dist/src/types/deployment/DeploymentServiceTypes.js +1 -1
  193. package/dist/src/types/deployment/DeploymentTypes.d.ts +1 -0
  194. package/dist/src/types/deploymentEventSchema.d.ts +1 -0
  195. package/dist/src/types/deploymentEventSchema.js +1 -1
  196. package/dist/src/types/detection/patternDetection.js +1 -1
  197. package/dist/src/types/detection/patternTypes.d.ts +9 -0
  198. package/dist/src/types/drift.js +1 -1
  199. package/dist/src/types/errors/CdkError.js +1 -1
  200. package/dist/src/types/errors/ServiceError.js +1 -1
  201. package/dist/src/types/operations.d.ts +26 -2
  202. package/dist/src/types/operations.js +1 -1
  203. package/dist/src/types/params.d.ts +20 -0
  204. package/dist/src/types/remediation.js +1 -1
  205. package/dist/src/types/stepDefinitions.d.ts +3 -0
  206. package/dist/src/types/stepDefinitions.js +1 -1
  207. package/dist/src/util/dockerfileDetection.js +1 -1
  208. package/dist/src/util/sequencedCallbacks.js +1 -1
  209. package/dist/src/util/sleepAbortable.js +1 -1
  210. package/package.json +5 -4
@@ -91,6 +91,16 @@ export interface DockerBuildParams {
91
91
  * both for the whole group rather than trying to push two variants.
92
92
  */
93
93
  targetsLambda?: boolean;
94
+ /**
95
+ * Caller abort signal (deploy cancel, worker SIGTERM, wave sibling-failure
96
+ * controller). CANONICAL RATIONALE — providers thread this to the underlying
97
+ * buildx subprocess so an in-flight build/push can be interrupted instead of
98
+ * running minutes past the cancellation. Killing buildx mid-push is safe:
99
+ * ECR deduplicates blobs so a retry re-uploads only what is missing, and the
100
+ * registry cache tag is mutable by design. Absent → the build runs to
101
+ * completion (pre-2.34 behaviour).
102
+ */
103
+ abortSignal?: AbortSignal;
94
104
  }
95
105
  export interface DockerBuildResult {
96
106
  imageUri: string;
@@ -178,6 +188,12 @@ export interface DockerProvider {
178
188
  beginEcrSession?(params: {
179
189
  accountId: string;
180
190
  region: string;
191
+ /**
192
+ * Declared from birth per robustness-standards § "Optional-capability seam
193
+ * methods carry `abortSignal?`" — implementers compose it with their SDK
194
+ * timeouts via `composeSdkAbortSignal`; ignoring it is permitted.
195
+ */
196
+ abortSignal?: AbortSignal;
181
197
  }): Promise<Result<void>>;
182
198
  endEcrSession?(): Promise<void>;
183
199
  buildAndPush(params: DockerBuildParams, onProgress?: DockerProgressCallback, onBuildxEvent?: (event: BuildxProgressEvent) => void): Promise<Result<DockerBuildResult>>;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * CAA preflight — run before the FIRST certificate issuance for a delegated
3
+ * child domain (D8; dev-substrate design gap D1). ACM consults CAA up the
4
+ * parent chain regardless of where the validation records live: a parent CAA
5
+ * record set that does not authorise Amazon's CA makes ACM REFUSE issuance
6
+ * even when the NS delegation is perfect, so the `phase: "full"` deploy would
7
+ * hang exactly like an unpropagated delegation.
8
+ *
9
+ * RFC 8659 semantics: the RELEVANT record set is the closest ancestor (or the
10
+ * name itself) that has any CAA records — once a set is found the walk stops.
11
+ * No CAA records anywhere ⇒ issuance is permitted by spec ⇒ pass.
12
+ */
13
+ import { type Result } from "@fjall/generator";
14
+ import type { DeployCallbacks } from "../../types/callbacks.js";
15
+ import { DomainDelegationError, type PublicCaaRecord, type PublicDnsResolver } from "./types.js";
16
+ /**
17
+ * The CA domains ACM issues under (AWS documented set). A CAA `issue` /
18
+ * `issuewild` value naming any of these authorises ACM.
19
+ */
20
+ export declare const ACM_CAA_ISSUERS: readonly ["amazon.com", "amazontrust.com", "awstrust.com", "amazonawstrust.com"];
21
+ export interface CaaPreflightParams {
22
+ /** The domain certificates will be requested for (the child zone name). */
23
+ certificateDomain: string;
24
+ /** Injectable resolvers (tests use fakes); defaults to 1.1.1.1 + 8.8.8.8. */
25
+ resolvers?: readonly PublicDnsResolver[];
26
+ abortSignal?: AbortSignal;
27
+ callbacks?: DeployCallbacks;
28
+ }
29
+ export type CaaPreflightVerdict =
30
+ /** No CAA record set exists anywhere up the chain — issuance permitted. */
31
+ {
32
+ authorised: true;
33
+ reason: "no-caa-found";
34
+ }
35
+ /** The relevant set authorises ACM (or restricts nothing). */
36
+ | {
37
+ authorised: true;
38
+ reason: "acm-authorised" | "no-issue-restriction";
39
+ /** The name carrying the relevant CAA record set. */
40
+ caaName: string;
41
+ records: string[];
42
+ };
43
+ /** `certificateDomain` and every ancestor down to the registrable pair. */
44
+ export declare function caaAncestorChain(certificateDomain: string): string[];
45
+ /** Render a CAA record in zone-file form for value-echoing errors. */
46
+ export declare function formatCaaRecord(record: PublicCaaRecord): string;
47
+ /**
48
+ * Walk the parent chain and evaluate the relevant CAA record set. Read-only.
49
+ *
50
+ * Fail-closed polarity (fail-closed ADR): a restrictive set fails with the
51
+ * EXACT records found and the cure (add an ACM-authorising CAA record, or
52
+ * issue certificates outside ACM); a chain that cannot be READ also fails
53
+ * closed — issuance cannot be proven safe — naming the `skipCaaPreflight`
54
+ * pre-answer for an operator who has verified the chain out of band.
55
+ */
56
+ export declare function runCaaPreflight(params: CaaPreflightParams): Promise<Result<CaaPreflightVerdict, DomainDelegationError>>;
@@ -0,0 +1 @@
1
+ var w=Object.defineProperty;var o=(e,r)=>w(e,"name",{value:r,configurable:!0});import{success as d,failure as l}from"@fjall/generator";import{getErrorMessage as $,maskSensitiveOutput as k}from"@fjall/util";import{isAborted as y}from"../../aws/organisations/types.js";import{createPublicResolvers as b,isDnsEmptyAnswer as D,normaliseDnsName as m,DomainDelegationError as h}from"./types.js";const A=["amazon.com","amazontrust.com","awstrust.com","amazonawstrust.com"];function v(e){const r=[];let t=m(e);for(;t.split(".").length>=2;)r.push(t),t=t.split(".").slice(1).join(".");return r}o(v,"caaAncestorChain");function M(e){const r=e.issue!==void 0?["issue",e.issue]:e.issuewild!==void 0?["issuewild",e.issuewild]:["iodef",e.iodef??""];return`${e.critical} ${r[0]} "${r[1]}"`}o(M,"formatCaaRecord");function S(e){const r=e.split(";")[0]??"";return m(r)}o(S,"issuerDomain");function g(e){const r=new Set(A);return e.some(t=>r.has(S(t)))}o(g,"authorisesAcm");async function P(e,r){const t=[];for(const c of r)try{const i=await c.resolveCaa(e);return i.length===0?{kind:"empty"}:{kind:"records",records:i}}catch(i){if(D(i))return{kind:"empty"};t.push(`${c.address}: ${k($(i))}`)}return{kind:"unverifiable",detail:t.join("; ")}}o(P,"queryCaaAcrossResolvers");async function E(e){const r=e.resolvers??b(),t=r.map(i=>i.address).join(", "),c=v(e.certificateDomain);for(const i of c){if(y(e.abortSignal))return l(new h(`CAA preflight for ${e.certificateDomain} aborted before ${i} was checked \u2014 no certificate was requested. Re-run the deploy to resume.`,"aborted",{certificateDomain:e.certificateDomain},!0));const n=await P(i,r);if(n.kind==="unverifiable")return l(new h(`CAA preflight for ${e.certificateDomain}: could not read CAA records for ${i} from any public resolver (${t}): ${n.detail}. ACM issuance cannot be proven safe, so the certificate phase is withheld (fail closed). Cure: retry once DNS resolution recovers, or verify the chain yourself (\`dig +short CAA ${i}\`) and pre-answer this check with skipCaaPreflight (CLI: --skip-caa-preflight).`,"caa_unverifiable",{certificateDomain:e.certificateDomain,name:i},!0));if(n.kind==="empty")continue;const a=n.records.map(M),f=n.records.map(s=>s.issue).filter(s=>s!==void 0),u=n.records.map(s=>s.issuewild).filter(s=>s!==void 0);if(f.length===0&&u.length===0)return e.callbacks?.onProgress?.({type:"info",message:`CAA preflight: ${i} carries CAA records but no issue/issuewild restriction \u2014 ACM issuance permitted`}),d({authorised:!0,reason:"no-issue-restriction",caaName:i,records:a});const p=f.length===0||g(f),C=u.length===0||g(u);return p&&C?(e.callbacks?.onProgress?.({type:"info",message:`CAA preflight: ${i} authorises ACM issuance (${a.join("; ")})`}),d({authorised:!0,reason:"acm-authorised",caaName:i,records:a})):l(new h(`CAA preflight for ${e.certificateDomain}: the CAA record set at ${i} does not authorise ACM \u2014 found [${a.join("; ")}]. ACM issues via ${A.join("/")} and will refuse issuance regardless of NS delegation, so the certificate phase is withheld before it can hang. Cure: add a CAA record authorising ACM at ${i} (e.g. \`${i}. CAA 0 issue "amazon.com"\`${u.length>0?` and \`${i}. CAA 0 issuewild "amazon.com"\``:""}), then re-run; or issue certificates outside ACM and set skipCaaPreflight (CLI: --skip-caa-preflight).`,"caa_restrictive",{certificateDomain:e.certificateDomain,name:i,records:a}))}return e.callbacks?.onProgress?.({type:"info",message:`CAA preflight: no CAA records anywhere up the chain from ${e.certificateDomain} \u2014 ACM issuance permitted by spec`}),d({authorised:!0,reason:"no-caa-found"})}o(E,"runCaaPreflight");export{A as ACM_CAA_ISSUERS,v as caaAncestorChain,M as formatCaaRecord,E as runCaaPreflight};
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Two-phase delegated-domain deploy orchestration (D8): deploy the domain
3
+ * stack at `phase: "zone"` (child zone + child-writes NS delegation, NO
4
+ * certificates), hold at the bounded public-resolver propagation gate, run
5
+ * the CAA preflight, then deploy `phase: "full"`. Generalises the
6
+ * dev-substrate design's R2 two-step gate (design :104-121) for every
7
+ * delegated child domain.
8
+ *
9
+ * The construct side of the contract is `composeDelegatedDomain`'s `phase`
10
+ * prop; the caller (Phase-3 CLI / worker) supplies the deployer that maps a
11
+ * phase onto its CDK invocation. deploy-core owns the ordering, the gate and
12
+ * the fail-closed stops — orchestration ships here, not in docs.
13
+ *
14
+ * Idempotent resume: when the zone phase already succeeded, a re-run skips
15
+ * straight to the propagation gate — "zone exists, no cert" is an explicit
16
+ * retryable intermediate, never an error state.
17
+ */
18
+ import { type Result } from "@fjall/generator";
19
+ import type { DeployCallbacks } from "../../types/callbacks.js";
20
+ import { type NsPropagationVerdict } from "./nsPropagationGate.js";
21
+ import { DomainDelegationError, type DomainDeployPhase, type PublicDnsResolver } from "./types.js";
22
+ /** What the caller's zone-phase probe reports for idempotent resume. */
23
+ export interface DelegatedZonePhaseState {
24
+ /** True when the zone phase has already deployed successfully. */
25
+ complete: boolean;
26
+ /**
27
+ * The child zone's authoritative name servers, when the caller can read
28
+ * them (domain-stack outputs). Fed to the propagation gate so a STALE
29
+ * parent delegation (pointing at a previous zone's NS set) is not
30
+ * mistaken for propagation.
31
+ */
32
+ nameServers?: readonly string[];
33
+ }
34
+ /**
35
+ * Caller-supplied deploy seam for ONE delegated domain — the delegated-domain
36
+ * mirror of `DomainDeployProvider.deployDomain`, split by phase. The CLI maps
37
+ * `deployPhase` onto its CDK deploy with the `phase` context injected;
38
+ * `zonePhaseState` probes the deployed stack (DescribeStacks + outputs).
39
+ */
40
+ export interface DelegatedDomainDeployer {
41
+ /** Fully-qualified child zone name, e.g. `dev.example.com`. */
42
+ readonly zoneName: string;
43
+ deployPhase(phase: DomainDeployPhase, callbacks: DeployCallbacks): Promise<Result<void, Error>>;
44
+ zonePhaseState(): Promise<Result<DelegatedZonePhaseState, Error>>;
45
+ }
46
+ export interface DeployDelegatedDomainParams {
47
+ deployer: DelegatedDomainDeployer;
48
+ callbacks: DeployCallbacks;
49
+ /**
50
+ * Pre-answer for the CAA preflight (fail-closed ADR shape): skip the check
51
+ * when the operator has verified the chain out of band. The skip is loud —
52
+ * a warning names it — never silent.
53
+ */
54
+ skipCaaPreflight?: boolean;
55
+ propagation?: {
56
+ maxAttempts?: number;
57
+ intervalMs?: number;
58
+ };
59
+ /** Injectable resolvers shared by the gate and the CAA preflight. */
60
+ resolvers?: readonly PublicDnsResolver[];
61
+ /** Injectable clock for the propagation gate. */
62
+ sleepFn?: (ms: number) => Promise<void>;
63
+ abortSignal?: AbortSignal;
64
+ /** Cure spelling for resume — defaults to `fjall domain deploy <zone>`. */
65
+ retryCommand?: string;
66
+ }
67
+ export interface DelegatedDomainDeployOutcome {
68
+ /** "resumed" when the zone phase was already deployed and was skipped. */
69
+ zonePhase: "deployed" | "resumed";
70
+ propagation: NsPropagationVerdict;
71
+ caaPreflight: "passed" | "skipped";
72
+ fullPhase: "deployed";
73
+ }
74
+ /**
75
+ * Run the full two-phase delegated-domain deploy. Every stop is fail-closed
76
+ * with a cure; every success path reports which steps ran so surfaces can
77
+ * render resume vs fresh honestly.
78
+ */
79
+ export declare function deployDelegatedDomain(params: DeployDelegatedDomainParams): Promise<Result<DelegatedDomainDeployOutcome, DomainDelegationError>>;
@@ -0,0 +1 @@
1
+ var $=Object.defineProperty;var d=(e,r)=>$(e,"name",{value:r,configurable:!0});import{success as v,failure as p}from"@fjall/generator";import{getErrorMessage as f,maskSensitiveOutput as c}from"@fjall/util";import{isAborted as b}from"../../aws/organisations/types.js";import{waitForNsPropagation as z}from"./nsPropagationGate.js";import{runCaaPreflight as w}from"./caaPreflight.js";import{DomainDelegationError as g}from"./types.js";const C=4;function i(e,r,o){e.onProgress?.({type:"step",message:o,metadata:{source:"domain-delegation",stepIndex:r,totalSteps:C}})}d(i,"step");async function F(e){const{deployer:r,callbacks:o}=e,t=r.zoneName,a=e.retryCommand??`fjall domain deploy ${t}`;let h="deployed",l;const n=await r.zonePhaseState();if(n.success&&n.data.complete)h="resumed",l=n.data.nameServers,i(o,0,`Zone phase for ${t} already deployed \u2014 resuming at the propagation gate`);else{n.success||o.onProgress?.({type:"info",message:`Could not probe the zone-phase state for ${t} (${c(f(n.error))}) \u2014 deploying the zone phase (idempotent)`}),i(o,0,`Deploying ${t} zone phase (phase: "zone" \u2014 child zone + NS delegation, no certificates)`);const s=await r.deployPhase("zone",o);if(!s.success)return p(new g(`Delegated-domain deploy for ${t}: the zone-phase deploy failed: ${c(f(s.error))}. No certificate was requested. Cure: fix the reported stack failure and re-run \`${a}\` \u2014 a completed zone phase is skipped on resume.`,"zone_phase_failed",{zoneName:t}));const S=await r.zonePhaseState();S.success&&(l=S.data.nameServers)}if(b(e.abortSignal))return p(P(t,a));i(o,1,`Waiting for the NS delegation of ${t} to propagate (public resolvers)`);const u=await z({zoneName:t,...l!==void 0?{expectedNameServers:l}:{},...e.propagation?.maxAttempts!==void 0?{maxAttempts:e.propagation.maxAttempts}:{},...e.propagation?.intervalMs!==void 0?{intervalMs:e.propagation.intervalMs}:{},...e.resolvers!==void 0?{resolvers:e.resolvers}:{},...e.sleepFn!==void 0?{sleepFn:e.sleepFn}:{},...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{},callbacks:o,retryCommand:a});if(!u.success)return u;let m="passed";if(e.skipCaaPreflight===!0)m="skipped",o.onProgress?.({type:"warning",message:`CAA preflight for ${t} SKIPPED (skipCaaPreflight) \u2014 if a parent CAA record restricts issuance, ACM will hang until CloudFormation rolls back`});else{i(o,2,`CAA preflight for ${t} (parent chain, public resolvers)`);const s=await w({certificateDomain:t,...e.resolvers!==void 0?{resolvers:e.resolvers}:{},...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{},callbacks:o});if(!s.success)return s}if(b(e.abortSignal))return p(P(t,a));i(o,3,`Deploying ${t} full phase (phase: "full" \u2014 certificates)`);const y=await r.deployPhase("full",o);return y.success?v({zonePhase:h,propagation:u.data,caaPreflight:m,fullPhase:"deployed"}):p(new g(`Delegated-domain deploy for ${t}: the full-phase deploy failed: ${c(f(y.error))}. The zone phase is deployed and the delegation is live. Cure: fix the reported stack failure and re-run \`${a}\` \u2014 the deploy resumes at the propagation gate and re-runs only the full phase.`,"full_phase_failed",{zoneName:t}))}d(F,"deployDelegatedDomain");function P(e,r){return new g(`Delegated-domain deploy for ${e} aborted between phases \u2014 the zone phase (when deployed) is retained. Re-run \`${r}\` to resume.`,"aborted",{zoneName:e},!0)}d(P,"abortedMidDeploy");export{F as deployDelegatedDomain};
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Destroy mirror for delegated child domains (D8; dev-substrate design :121):
3
+ * the child's NS record is deleted from the PARENT zone BEFORE the CFN
4
+ * destroy. Destroying first releases the child zone's name-server set back to
5
+ * the Route53 pool while the parent still delegates to it — a
6
+ * subdomain-takeover window. Delete-first trades a harmless orphaned-child-
7
+ * zone leak (if the subsequent destroy fails) for eliminating that window.
8
+ *
9
+ * Ownership detection: when the NS record is CFN-owned by the child stack's
10
+ * `CrossAccountZoneDelegationRecord` custom resource, the CFN destroy deletes
11
+ * it in-stack — the manual delete is SKIPPED (deleting it out-of-band first
12
+ * would fail the custom resource's own Delete). Only an out-of-stack record
13
+ * (written by an earlier orchestration or by hand) is deleted here.
14
+ *
15
+ * Consent: the deletion is destructive-adjacent and is NEVER a silent side
16
+ * effect — it is named as a row on the destruction ticket for the stack
17
+ * destroy and evaluated through the SAME `evaluateDestructionConsents` +
18
+ * digest TOCTOU path as every other ceremony (destruction-ceremony ADR: one
19
+ * primitive, five renderings). Consent withheld ⇒ nothing is deleted and the
20
+ * destroy does not start (exit-4 semantics at the surface).
21
+ */
22
+ import { Route53Client } from "@aws-sdk/client-route-53";
23
+ import { type Result } from "@fjall/generator";
24
+ import type { DeployCallbacks } from "../../types/callbacks.js";
25
+ import type { DestructionConsent, DestructionConsentGate, DestructionConsentReport, DestructionOutcome, DestructionTicket } from "../../types/destruction.js";
26
+ import type { AwsProvider } from "../../aws/AwsProvider.js";
27
+ import { DomainDelegationError } from "./types.js";
28
+ /**
29
+ * The custom-resource type CDK's `CrossAccountZoneDelegationRecord` emits —
30
+ * its presence in the child stack's deployed template proves the NS record is
31
+ * CFN-owned and will be deleted by the stack destroy itself.
32
+ */
33
+ export declare const CROSS_ACCOUNT_DELEGATION_RESOURCE_TYPE = "Custom::CrossAccountZoneDelegation";
34
+ /**
35
+ * Inspect a deployed CloudFormation template (JSON — the only form CDK
36
+ * emits) for the delegation custom resource. An unreadable template is a
37
+ * failure, not a guess: mis-detecting ownership either leaves a dangling
38
+ * delegation (takeover window) or breaks the custom resource's own Delete.
39
+ */
40
+ export declare function templateOwnsDelegationRecord(templateBody: string): Result<boolean, Error>;
41
+ export type NsDeleteOutcome = {
42
+ kind: "deleted";
43
+ recordValues: readonly string[];
44
+ }
45
+ /** The record was already gone — NotFound on delete is success. */
46
+ | {
47
+ kind: "record-absent";
48
+ }
49
+ /** The parent zone itself is gone — nothing can dangle. */
50
+ | {
51
+ kind: "parent-zone-absent";
52
+ };
53
+ export interface DeleteChildNsParams {
54
+ /** Base provider; the delegation role is assumed from it by literal ARN. */
55
+ aws: AwsProvider;
56
+ /** Parent DelegationRole ARN — always a literal (VD2-a), never an import. */
57
+ delegationRoleArn: string;
58
+ parentZoneName: string;
59
+ childZoneName: string;
60
+ abortSignal?: AbortSignal;
61
+ callbacks?: DeployCallbacks;
62
+ /** Test seam: replaces the assume-role + client construction. */
63
+ route53ClientFactory?: (roleArn: string) => Promise<Result<Pick<Route53Client, "send">, Error>>;
64
+ }
65
+ /**
66
+ * Delete the child zone's NS record from the parent zone via the parent
67
+ * DelegationRole (assumed by literal ARN). Idempotent: an absent record or an
68
+ * absent parent zone is success, so the mirror self-heals across retries.
69
+ * Every SDK call composes the caller's abort signal with the per-call
70
+ * timeout (robustness standards).
71
+ */
72
+ export declare function deleteChildNsFromParent(params: DeleteChildNsParams): Promise<Result<NsDeleteOutcome, DomainDelegationError>>;
73
+ /** How the NS delegation was handled ahead of the stack destroy. */
74
+ export type DelegationDestroyNsOutcome = NsDeleteOutcome
75
+ /** In-stack custom resource — the CFN destroy deletes it itself. */
76
+ | {
77
+ kind: "cfn-owned";
78
+ }
79
+ /**
80
+ * Case-3 externally-owned parent: fjall cannot delete the record; the
81
+ * operator consented `forget` (the record survives until removed by hand).
82
+ */
83
+ | {
84
+ kind: "external-forget";
85
+ };
86
+ export type DelegatedDomainDestroyOutcome = {
87
+ kind: "destroyed";
88
+ nsDelegation: DelegationDestroyNsOutcome;
89
+ }
90
+ /** Consent withheld — nothing was deleted, the destroy did not start. */
91
+ | {
92
+ kind: "withheld";
93
+ outcome: DestructionOutcome;
94
+ ticket: DestructionTicket;
95
+ report?: DestructionConsentReport;
96
+ reasons: string[];
97
+ };
98
+ export interface DelegatedDomainDestroyParams {
99
+ /** The delegated child zone being destroyed, e.g. `dev.example.com`. */
100
+ childZoneName: string;
101
+ /** The parent zone holding the NS delegation, e.g. `example.com`. */
102
+ parentZoneName: string;
103
+ /**
104
+ * Parent DelegationRole ARN (literal, from the managed-domain binding).
105
+ * Absent ⇒ Case-3 externally-owned parent: the record cannot be deleted by
106
+ * fjall, and proceeding requires an explicit `forget` consent.
107
+ */
108
+ delegationRoleArn?: string;
109
+ /** The child domain stack's name (ticket addressing + template fetch). */
110
+ stackName: string;
111
+ /**
112
+ * Fetch the child stack's deployed template for ownership detection.
113
+ * `undefined` data ⇒ the stack no longer exists (a previous destroy got
114
+ * further than this step) — the mirror still runs, because the NS record
115
+ * may be the surviving half.
116
+ */
117
+ getTemplate: (abortSignal?: AbortSignal) => Promise<Result<string | undefined, Error>>;
118
+ /** Executes the CFN destroy — invoked ONLY after the mirror step. */
119
+ destroyStack: (callbacks: DeployCallbacks) => Promise<Result<void, Error>>;
120
+ aws: AwsProvider;
121
+ callbacks: DeployCallbacks;
122
+ /** Flag-supplied consents (`--remediate <physicalName>=<verb>`). */
123
+ consents?: readonly DestructionConsent[];
124
+ /** Digest the consents were minted against (TOCTOU binding). */
125
+ consentDigest?: string;
126
+ /** Interactive ceremony resolver — consulted only when no flags came. */
127
+ consentGate?: DestructionConsentGate;
128
+ abortSignal?: AbortSignal;
129
+ /** Test seam forwarded to `deleteChildNsFromParent`. */
130
+ route53ClientFactory?: DeleteChildNsParams["route53ClientFactory"];
131
+ }
132
+ /** The exact consent string for the NS row: `<childZone>/NS`. */
133
+ export declare function nsDelegationPhysicalName(childZoneName: string): string;
134
+ /**
135
+ * Orchestrate the destroy of a delegated child domain stack: ownership
136
+ * detection → destruction-consent ceremony (the NS deletion named on the
137
+ * ticket) → NS delete (delete-first, idempotent) → CFN destroy. The
138
+ * ordering is structural — `destroyStack` is only reachable after the mirror
139
+ * step resolved — so no caller can destroy first by accident.
140
+ */
141
+ export declare function runDelegatedDomainDestroy(params: DelegatedDomainDestroyParams): Promise<Result<DelegatedDomainDestroyOutcome, Error>>;
@@ -0,0 +1 @@
1
+ var T=Object.defineProperty;var u=(e,t)=>T(e,"name",{value:t,configurable:!0});import{Route53Client as E,ListHostedZonesByNameCommand as Z,ListResourceRecordSetsCommand as x,ChangeResourceRecordSetsCommand as I}from"@aws-sdk/client-route-53";import{success as l,failure as d}from"@fjall/generator";import{getErrorMessage as y,maskSensitiveOutput as f}from"@fjall/util";import{SimpleAwsProvider as O}from"../../aws/SimpleAwsProvider.js";import{parseArn as L}from"../../aws/utils/arnParser.js";import{composeSdkAbortSignal as P,isAborted as F}from"../../aws/organisations/types.js";import{computeTicketDigest as _,evaluateDestructionConsents as D,renderConsentVerdictLines as j,renderDestructionTicketLines as z}from"../application/destructionGate.js";import{DomainDelegationError as b,normaliseDnsName as S}from"./types.js";const C="Custom::CrossAccountZoneDelegation";function M(e){let t;try{t=JSON.parse(e)}catch(a){return d(new Error(`Delegation destroy mirror: the child stack template is not readable JSON (${f(y(a))}) \u2014 NS-record ownership cannot be determined.`))}if(typeof t!="object"||t===null)return d(new Error("Delegation destroy mirror: the child stack template parsed to a non-object \u2014 NS-record ownership cannot be determined."));const n=t.Resources;if(n===void 0||typeof n!="object")return l(!1);const i=Object.values(n).some(a=>typeof a=="object"&&a!==null&&a.Type===C);return l(i)}u(M,"templateOwnsDelegationRecord");async function B(e,t){if(e.assumeRole===void 0)return d(new Error("the AWS provider does not support assumeRole"));const n=L(t)?.accountId;if(n===void 0||n==="")return d(new Error(`'${t}' is not a valid IAM role ARN`));try{const i=await e.assumeRole(t,"fjall-domain-destroy-mirror"),a=new O({accessKeyId:i.accessKeyId,secretAccessKey:i.secretAccessKey,...i.sessionToken!==void 0?{sessionToken:i.sessionToken}:{},region:"us-east-1",accountId:n});return l(a.getClient(E))}catch(i){return d(new Error(f(y(i))))}}u(B,"defaultRoute53ClientFactory");function H(e){if(!(e instanceof Error))return!1;const t=e.name,n=e.message.toLowerCase();return t==="InvalidChangeBatch"&&(n.includes("not found")||n.includes("does not exist"))}u(H,"isRecordNotFoundChange");async function K(e){const{parentZoneName:t,childZoneName:n}=e,i=u(s=>d(new b(`Destroy mirror for ${n}: deleting the NS delegation record from parent zone ${t} failed: ${s}. The stack destroy was NOT started \u2014 delete-first is the rule, because a dangling delegation is a subdomain-takeover window. Cure: re-run the destroy, or delete the ${n} NS record in ${t} yourself and re-run.`,"ns_delete_failed",{parentZoneName:t,childZoneName:n},!0)),"fail"),a=e.route53ClientFactory!==void 0?await e.route53ClientFactory(e.delegationRoleArn):await B(e.aws,e.delegationRoleArn);if(!a.success)return d(new b(`Destroy mirror for ${n}: could not assume the parent DelegationRole ${e.delegationRoleArn}: ${f(y(a.error))}. The stack destroy was NOT started. Cure: verify the role exists and trusts this account (it is created by the parent Domain stack), or delete the ${n} NS record in ${t} yourself and re-run.`,"delegation_role_unusable",{delegationRoleArn:e.delegationRoleArn}));const w=a.data,g={abortSignal:P(e.abortSignal)};let h;try{const r=((await w.send(new Z({DNSName:t,MaxItems:100}),g)).HostedZones??[]).filter(m=>m.Name!==void 0&&S(m.Name)===S(t));h=(r.find(m=>m.Config?.PrivateZone!==!0)??r[0])?.Id?.replace("/hostedzone/","")}catch(s){return i(f(y(s)))}if(h===void 0)return e.callbacks?.onProgress?.({type:"info",message:`Destroy mirror: parent zone ${t} was not found \u2014 no delegation record can dangle, continuing to the stack destroy`}),l({kind:"parent-zone-absent"});let c;try{const r=((await w.send(new x({HostedZoneId:h,StartRecordName:n,StartRecordType:"NS",MaxItems:1}),g)).ResourceRecordSets??[])[0];c=r!==void 0&&r.Name!==void 0&&S(r.Name)===S(n)&&r.Type==="NS"?r:void 0}catch(s){return i(f(y(s)))}if(c===void 0)return e.callbacks?.onProgress?.({type:"info",message:`Destroy mirror: no NS record for ${n} exists in ${t} \u2014 already deleted (idempotent), continuing to the stack destroy`}),l({kind:"record-absent"});try{await w.send(new I({HostedZoneId:h,ChangeBatch:{Comment:`fjall destroy mirror: remove NS delegation for ${n} before the stack destroy`,Changes:[{Action:"DELETE",ResourceRecordSet:c}]}}),g)}catch(s){return H(s)?l({kind:"record-absent"}):i(f(y(s)))}const R=(c.ResourceRecords??[]).map(s=>s.Value).filter(s=>s!==void 0);return e.callbacks?.onProgress?.({type:"info",message:`Destroy mirror: deleted the NS delegation for ${n} from ${t} (was: ${R.join(", ")})`}),l({kind:"deleted",recordValues:R})}u(K,"deleteChildNsFromParent");function v(e){return`${S(e)}/NS`}u(v,"nsDelegationPhysicalName");function V(e,t){const n={stack:e.stackName,logicalId:"ParentZoneNsDelegation",resourceType:"AWS::Route53::RecordSet",physicalName:v(e.childZoneName),finding:"data-loss-delete",cause:t==="deletable"?`NS delegation for ${e.childZoneName} in parent zone ${e.parentZoneName} is not CloudFormation-owned \u2014 it is deleted from the parent BEFORE the stack destroy (destroying first releases the child zone's name servers back to the Route53 pool while the parent still delegates: a subdomain-takeover window)`:`NS delegation for ${e.childZoneName} lives in the externally-owned parent ${e.parentZoneName} \u2014 fjall cannot delete it; consent 'forget' to destroy the stack anyway, then remove the record at the parent yourself (until removed it is a dangling delegation)`,offendingProperties:[],verdictSource:"change-set",dataLossRisk:t==="deletable",verbs:t==="deletable"?["abort","recreate"]:["abort","forget"]};return{ticketDigest:_(`domain-destroy:${e.stackName}:${e.childZoneName}`,[n]),resources:[n]}}u(V,"buildNsTicket");async function te(e){const{callbacks:t,childZoneName:n,parentZoneName:i}=e,a=await e.getTemplate(e.abortSignal);if(!a.success)return d(new b(`Destroy mirror for ${n}: could not read the deployed template of ${e.stackName} to determine NS-record ownership: ${f(y(a.error))}. Nothing was destroyed. Cure: retry the destroy; if the failure persists, inspect the stack with \`aws cloudformation get-template --stack-name ${e.stackName}\`.`,"template_unreadable",{stackName:e.stackName},!0));let w=!1;if(a.data!==void 0){const o=M(a.data);if(!o.success)return d(new b(`${f(o.error.message)} Nothing was destroyed. Cure: retry the destroy; if the failure persists, inspect the stack with \`aws cloudformation get-template --stack-name ${e.stackName}\`.`,"template_unreadable",{stackName:e.stackName},!0));w=o.data}if(w){t.onProgress?.({type:"info",message:`NS delegation for ${n} is CloudFormation-owned (${C}) \u2014 the stack destroy removes it in-stack, no pre-destroy delete needed`});const o=await e.destroyStack(t);return o.success?l({kind:"destroyed",nsDelegation:{kind:"cfn-owned"}}):d(o.error)}const g=e.delegationRoleArn,h=g!==void 0?"deletable":"external",c=V(e,h),R=h==="deletable"?"recreate":"forget",s=e.consents??[];let r=D({ticket:c,consents:s,...e.consentDigest!==void 0?{consentDigest:e.consentDigest}:{}});const N=[];if(!r.satisfied&&e.consentGate!==void 0&&s.length===0&&e.consentDigest===void 0){const o=await e.consentGate.resolve({target:e.stackName,ticket:c});o.kind==="abort"?N.push("declined at the interactive destruction ceremony \u2014 nothing was deleted, nothing was destroyed"):r=D({ticket:c,consents:o.consents,consentDigest:o.ticketDigest})}const m=r.satisfied&&r.rows.some(o=>o.verdict==="matched"&&o.verb==="abort");if(!r.satisfied||m||N.length>0){const o=N.length>0||m?"declined":r.rows.length>0||r.digestMismatch?"consents-invalid":"awaiting-consent",A=o==="declined"?`Destroy of ${e.stackName} declined \u2014 the NS-delegation removal was not consented; nothing was deleted, nothing was destroyed`:`Destroying ${e.stackName} deletes the NS delegation for ${n} from ${i} \u2014 named consent required; nothing was deleted, nothing was destroyed`;t.onLog?.(A,"warn");for(const k of z(c))t.onLog?.(k,"warn");for(const k of j(r))t.onLog?.(k,"warn");for(const k of N)t.onLog?.(k,"warn");return o!=="declined"&&t.onLog?.(`Consent with --remediate ${v(n)}=${R} (exact, case-sensitive). No blanket flag exists: --auto-approve, --skip-confirmation and approval tokens never satisfy this gate.`,"warn"),l({kind:"withheld",outcome:o,ticket:c,report:r,reasons:N})}if(F(e.abortSignal))return d(new b(`Destroy of ${e.stackName} aborted before the NS-delegation removal \u2014 nothing was deleted, nothing was destroyed. Re-run the destroy to resume.`,"aborted",{stackName:e.stackName},!0));let $;if(g!==void 0){const o=await K({aws:e.aws,delegationRoleArn:g,parentZoneName:i,childZoneName:n,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{},callbacks:t,...e.route53ClientFactory!==void 0?{route53ClientFactory:e.route53ClientFactory}:{}});if(!o.success)return d(o.error);$=o.data}else $={kind:"external-forget"},t.onProgress?.({type:"warning",message:`NS delegation for ${n} remains in the externally-owned parent ${i} (consented 'forget') \u2014 remove it at the parent to close the dangling delegation`});const p=await e.destroyStack(t);return p.success?l({kind:"destroyed",nsDelegation:$}):d(p.error)}u(te,"runDelegatedDomainDestroy");export{C as CROSS_ACCOUNT_DELEGATION_RESOURCE_TYPE,K as deleteChildNsFromParent,v as nsDelegationPhysicalName,te as runDelegatedDomainDestroy,M as templateOwnsDelegationRecord};
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Delegated-domain orchestration (D8, domain gold-plating): two-phase
3
+ * zone → propagation-gate → CAA-preflight → full deploy, and the
4
+ * delete-NS-before-destroy mirror with its consent ceremony.
5
+ */
6
+ export { DOMAIN_DELEGATION_ERROR_TYPES, DomainDelegationError, DEFAULT_PUBLIC_RESOLVER_ADDRESSES, PUBLIC_DNS_QUERY_TIMEOUT_MS, createPublicResolvers, normaliseDnsName, isDnsEmptyAnswer, type DomainDeployPhase, type DomainDelegationErrorType, type PublicCaaRecord, type PublicDnsResolver } from "./types.js";
7
+ export { NS_PROPAGATION_DEFAULT_MAX_ATTEMPTS, NS_PROPAGATION_DEFAULT_INTERVAL_MS, waitForNsPropagation, type NsPropagationGateParams, type NsPropagationVerdict } from "./nsPropagationGate.js";
8
+ export { ACM_CAA_ISSUERS, caaAncestorChain, formatCaaRecord, runCaaPreflight, type CaaPreflightParams, type CaaPreflightVerdict } from "./caaPreflight.js";
9
+ export { deployDelegatedDomain, type DelegatedDomainDeployer, type DelegatedZonePhaseState, type DeployDelegatedDomainParams, type DelegatedDomainDeployOutcome } from "./delegatedDomainDeploy.js";
10
+ export { CROSS_ACCOUNT_DELEGATION_RESOURCE_TYPE, templateOwnsDelegationRecord, deleteChildNsFromParent, nsDelegationPhysicalName, runDelegatedDomainDestroy, type NsDeleteOutcome, type DeleteChildNsParams, type DelegationDestroyNsOutcome, type DelegatedDomainDestroyOutcome, type DelegatedDomainDestroyParams } from "./delegationDestroyMirror.js";
@@ -0,0 +1 @@
1
+ import{DOMAIN_DELEGATION_ERROR_TYPES as r,DomainDelegationError as a,DEFAULT_PUBLIC_RESOLVER_ADDRESSES as _,PUBLIC_DNS_QUERY_TIMEOUT_MS as E,createPublicResolvers as t,normaliseDnsName as A,isDnsEmptyAnswer as D}from"./types.js";import{NS_PROPAGATION_DEFAULT_MAX_ATTEMPTS as R,NS_PROPAGATION_DEFAULT_INTERVAL_MS as S,waitForNsPropagation as T}from"./nsPropagationGate.js";import{ACM_CAA_ISSUERS as m,caaAncestorChain as l,formatCaaRecord as s,runCaaPreflight as N}from"./caaPreflight.js";import{deployDelegatedDomain as P}from"./delegatedDomainDeploy.js";import{CROSS_ACCOUNT_DELEGATION_RESOURCE_TYPE as I,templateOwnsDelegationRecord as U,deleteChildNsFromParent as p,nsDelegationPhysicalName as L,runDelegatedDomainDestroy as c}from"./delegationDestroyMirror.js";export{m as ACM_CAA_ISSUERS,I as CROSS_ACCOUNT_DELEGATION_RESOURCE_TYPE,_ as DEFAULT_PUBLIC_RESOLVER_ADDRESSES,r as DOMAIN_DELEGATION_ERROR_TYPES,a as DomainDelegationError,S as NS_PROPAGATION_DEFAULT_INTERVAL_MS,R as NS_PROPAGATION_DEFAULT_MAX_ATTEMPTS,E as PUBLIC_DNS_QUERY_TIMEOUT_MS,l as caaAncestorChain,t as createPublicResolvers,p as deleteChildNsFromParent,P as deployDelegatedDomain,s as formatCaaRecord,D as isDnsEmptyAnswer,A as normaliseDnsName,L as nsDelegationPhysicalName,N as runCaaPreflight,c as runDelegatedDomainDestroy,U as templateOwnsDelegationRecord,T as waitForNsPropagation};
@@ -0,0 +1,59 @@
1
+ /**
2
+ * NS propagation gate — the bounded public-recursive-resolver check between
3
+ * the `phase: "zone"` and `phase: "full"` deploys of a delegated child domain
4
+ * (D8; dev-substrate design R2 refinement). A bare `GetHostedZone` proves the
5
+ * child zone EXISTS; only a public resolver proves the parent's delegation
6
+ * has PROPAGATED — and issuing a DNS-validated certificate before then hangs
7
+ * ACM until CloudFormation rolls back (~1 h).
8
+ *
9
+ * Fail-closed on bound-expiry with `delegation_unreachable` (the design's 422
10
+ * vocabulary), naming the retry command and the manual-NS option (Case-3
11
+ * externally-owned parent) per the fail-closed ADR.
12
+ */
13
+ import { type Result } from "@fjall/generator";
14
+ import type { DeployCallbacks } from "../../types/callbacks.js";
15
+ import { DomainDelegationError, type PublicDnsResolver } from "./types.js";
16
+ /** ~2 minutes of polling by default: 12 attempts x 10s interval. */
17
+ export declare const NS_PROPAGATION_DEFAULT_MAX_ATTEMPTS = 12;
18
+ export declare const NS_PROPAGATION_DEFAULT_INTERVAL_MS = 10000;
19
+ export interface NsPropagationGateParams {
20
+ /** Fully-qualified delegated child zone, e.g. `dev.example.com`. */
21
+ zoneName: string;
22
+ /**
23
+ * The child zone's authoritative name servers, when the caller knows them
24
+ * (domain-stack outputs). Supplied ⇒ a public NS answer must overlap this
25
+ * set — an answer pointing at a DIFFERENT NS set is a STALE delegation (a
26
+ * prior zone's name servers still answering), exactly the state that hangs
27
+ * certificate validation, so the gate keeps polling rather than passing.
28
+ */
29
+ expectedNameServers?: readonly string[];
30
+ maxAttempts?: number;
31
+ intervalMs?: number;
32
+ /** Injectable resolvers (tests use fakes); defaults to 1.1.1.1 + 8.8.8.8. */
33
+ resolvers?: readonly PublicDnsResolver[];
34
+ /** Injectable clock (tests make the interval instant). */
35
+ sleepFn?: (ms: number) => Promise<void>;
36
+ abortSignal?: AbortSignal;
37
+ callbacks?: DeployCallbacks;
38
+ /**
39
+ * The command the cure names for resuming after propagation completes.
40
+ * Phase-3 surfaces pass their own spelling; the default is the CLI verb.
41
+ */
42
+ retryCommand?: string;
43
+ }
44
+ export interface NsPropagationVerdict {
45
+ /** The name servers the succeeding resolver returned. */
46
+ nameServers: string[];
47
+ /** 1-based attempt count at success. */
48
+ attempts: number;
49
+ /** Which public resolver observed the delegation. */
50
+ resolverAddress: string;
51
+ }
52
+ /**
53
+ * Poll the public resolvers until the child zone's NS delegation is visible,
54
+ * up to `maxAttempts` x `intervalMs`. Read-only; safe to re-run any number of
55
+ * times. Expiry returns `delegation_unreachable` — the zone phase stays
56
+ * deployed and retained, so the cure is a re-run (which resumes at this gate),
57
+ * never a rollback.
58
+ */
59
+ export declare function waitForNsPropagation(params: NsPropagationGateParams): Promise<Result<NsPropagationVerdict, DomainDelegationError>>;
@@ -0,0 +1 @@
1
+ var S=Object.defineProperty;var d=(e,t)=>S(e,"name",{value:t,configurable:!0});import{success as b,failure as c}from"@fjall/generator";import{getErrorMessage as y,maskSensitiveOutput as z,sleep as w}from"@fjall/util";import{isAborted as g}from"../../aws/organisations/types.js";import{createPublicResolvers as x,normaliseDnsName as p,DomainDelegationError as h}from"./types.js";const A=12,P=1e4;function k(e,t){const n=new Set(t.map(p));return e.some(a=>n.has(p(a)))}d(k,"overlaps");async function j(e){const t=e.maxAttempts??A,n=e.intervalMs??P,a=e.resolvers??x(),f=e.sleepFn??w,i=e.retryCommand??`fjall domain deploy ${e.zoneName}`,u=a.map(o=>o.address);let l;for(let o=1;o<=t;o++){if(g(e.abortSignal))return c(m(e.zoneName,i));for(const s of a){let r;try{r=await s.resolveNs(e.zoneName)}catch(v){e.callbacks?.onProgress?.({type:"debug",message:`NS propagation check ${o}/${t} for ${e.zoneName} via ${s.address}: ${z(y(v))}`});continue}if(r.length!==0){if(e.expectedNameServers!==void 0&&e.expectedNameServers.length>0&&!k(r,e.expectedNameServers)){l=r,e.callbacks?.onProgress?.({type:"debug",message:`NS propagation check ${o}/${t} for ${e.zoneName} via ${s.address}: answer [${r.join(", ")}] does not overlap the child zone's name servers yet (stale delegation still cached)`});continue}return e.callbacks?.onProgress?.({type:"info",message:`NS delegation for ${e.zoneName} is publicly resolvable (attempt ${o}/${t}, via ${s.address})`}),b({nameServers:r,attempts:o,resolverAddress:s.address})}}o<t&&(e.callbacks?.onProgress?.({type:"spinner",message:`Waiting for NS delegation of ${e.zoneName} to propagate (attempt ${o}/${t}, next check in ${Math.round(n/1e3)}s)`}),await f(n))}if(g(e.abortSignal))return c(m(e.zoneName,i));const N=l!==void 0&&e.expectedNameServers!==void 0?`; the parent currently delegates to [${l.join(", ")}] but the child zone's name servers are [${e.expectedNameServers.join(", ")}] \u2014 a stale delegation from an earlier zone`:"",$=Math.round(t*n/1e3);return c(new h(`NS propagation gate for ${e.zoneName}: the delegation did not become publicly resolvable within ${t} attempts over ~${$}s (resolvers: ${u.join(", ")})${N}. The zone phase is deployed and retained \u2014 nothing was rolled back and no certificate was requested. Cure: once the parent delegation has propagated, re-run \`${i}\` (the zone phase is skipped on resume and the deploy continues from this gate); if the parent zone is externally owned (Case 3), add the child zone's NS records at the parent registrar manually, then re-run \`${i}\`.`,"delegation_unreachable",{zoneName:e.zoneName,maxAttempts:t,intervalMs:n,resolvers:u},!0))}d(j,"waitForNsPropagation");function m(e,t){return new h(`NS propagation gate for ${e} aborted \u2014 the zone phase remains deployed and no certificate was requested. Re-run \`${t}\` to resume at this gate.`,"aborted",{zoneName:e},!0)}d(m,"abortedError");export{P as NS_PROPAGATION_DEFAULT_INTERVAL_MS,A as NS_PROPAGATION_DEFAULT_MAX_ATTEMPTS,j as waitForNsPropagation};
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Shared contracts for the delegated-domain orchestration (D8 of the domain
3
+ * gold-plating design, 2026-07-17): the two-phase zone → propagation-gate →
4
+ * full deploy, the CAA preflight before first certificate issuance, and the
5
+ * destroy mirror that removes the child NS from the parent BEFORE the CFN
6
+ * destroy.
7
+ *
8
+ * `DomainDeployPhase` is deploy-core's copy of the construct-side wire
9
+ * contract (`components/infrastructure/lib/utils/domainTypes.ts`, shared with
10
+ * `DevSubstrate.phase`): the CLI injects `phase` into synth and
11
+ * `composeDelegatedDomain` early-returns before certificate issuance when the
12
+ * phase is `"zone"`. deploy-core does not depend on components-infrastructure
13
+ * (layer boundary), so the two-literal union is re-declared here; both sides
14
+ * document the other as the counterpart.
15
+ */
16
+ import { BaseServiceError } from "../../types/errors/ServiceError.js";
17
+ /** Two-step deploy phase — the construct-side gate's wire vocabulary. */
18
+ export type DomainDeployPhase = "zone" | "full";
19
+ /**
20
+ * Typed failure vocabulary for the delegation orchestration. The codes are
21
+ * part of the surface contract: `delegation_unreachable` mirrors the
22
+ * dev-substrate design's 422 reason (design :104-121), and Phase-3 CLI verbs
23
+ * map them to exit codes / TOON blocks without string-matching messages.
24
+ */
25
+ export declare const DOMAIN_DELEGATION_ERROR_TYPES: readonly ["delegation_unreachable", "caa_restrictive", "caa_unverifiable", "zone_phase_failed", "full_phase_failed", "delegation_role_unusable", "template_unreadable", "ns_delete_failed", "aborted"];
26
+ export type DomainDelegationErrorType = (typeof DOMAIN_DELEGATION_ERROR_TYPES)[number];
27
+ /**
28
+ * Delegation-orchestration error. Messages are pre-masked at construction
29
+ * (identifier-only content plus `maskSensitiveOutput`-routed SDK text) and
30
+ * every non-interactive stop names its cure per the fail-closed ADR
31
+ * (decisions/2026-07-12-fail-closed-default-policy.md).
32
+ */
33
+ export declare class DomainDelegationError extends BaseServiceError {
34
+ readonly errorType: DomainDelegationErrorType;
35
+ constructor(message: string, errorType: DomainDelegationErrorType, details?: unknown, recoverable?: boolean);
36
+ }
37
+ /**
38
+ * One CAA record as node's resolver returns it. Structurally compatible with
39
+ * `dns.CaaRecord` — declared here so fakes in tests and non-node resolvers
40
+ * need no dns import.
41
+ */
42
+ export interface PublicCaaRecord {
43
+ critical: number;
44
+ issue?: string;
45
+ issuewild?: string;
46
+ iodef?: string;
47
+ }
48
+ /**
49
+ * A recursive resolver pinned to ONE public address. The propagation gate and
50
+ * the CAA preflight take these by injection so tests run against fakes; the
51
+ * default set queries Cloudflare and Google (the answer must be visible to
52
+ * the public internet, not merely to the deployer's split-horizon resolver).
53
+ */
54
+ export interface PublicDnsResolver {
55
+ readonly address: string;
56
+ resolveNs(name: string): Promise<string[]>;
57
+ resolveCaa(name: string): Promise<PublicCaaRecord[]>;
58
+ }
59
+ export declare const DEFAULT_PUBLIC_RESOLVER_ADDRESSES: readonly ["1.1.1.1", "8.8.8.8"];
60
+ /** Per-query timeout — mirrors DomainService.verifyDomain's 10s bound. */
61
+ export declare const PUBLIC_DNS_QUERY_TIMEOUT_MS = 10000;
62
+ /**
63
+ * Build one `node:dns` resolver per public address, each bounded by the
64
+ * shared 10s query timeout with a single try (the gate's outer loop owns
65
+ * retrying — stacking resolver-level retries on top would stretch the bound).
66
+ */
67
+ export declare function createPublicResolvers(addresses?: readonly string[]): PublicDnsResolver[];
68
+ /** Case/trailing-dot-insensitive DNS name comparison form. */
69
+ export declare function normaliseDnsName(name: string): string;
70
+ /**
71
+ * True when a resolver error is a DEFINITIVE empty answer (the name or RRset
72
+ * does not exist) rather than a lookup failure. The CAA preflight treats only
73
+ * these as "no CAA here"; anything else is unverifiable and fails closed.
74
+ */
75
+ export declare function isDnsEmptyAnswer(error: unknown): boolean;
@@ -0,0 +1 @@
1
+ var a=Object.defineProperty;var t=(e,r)=>a(e,"name",{value:r,configurable:!0});import{Resolver as l}from"node:dns/promises";import{BaseServiceError as i}from"../../types/errors/ServiceError.js";const D=["delegation_unreachable","caa_restrictive","caa_unverifiable","zone_phase_failed","full_phase_failed","delegation_role_unusable","template_unreadable","ns_delete_failed","aborted"];class S extends i{static{t(this,"DomainDelegationError")}errorType;constructor(r,o,s,n=!1){super(`DOMAIN_${o.toUpperCase()}`,r,s,n),this.errorType=o}}const _=["1.1.1.1","8.8.8.8"],c=1e4;function m(e=_){return e.map(r=>{const o=new l({timeout:c,tries:1});return o.setServers([r]),{address:r,resolveNs:t(s=>o.resolveNs(s),"resolveNs"),resolveCaa:t(s=>o.resolveCaa(s),"resolveCaa")}})}t(m,"createPublicResolvers");function N(e){return e.trim().toLowerCase().replace(/\.$/,"")}t(N,"normaliseDnsName");const p=new Set(["ENODATA","ENOTFOUND"]);function v(e){if(!(e instanceof Error))return!1;const r=e.code;return typeof r=="string"&&p.has(r)}t(v,"isDnsEmptyAnswer");export{_ as DEFAULT_PUBLIC_RESOLVER_ADDRESSES,D as DOMAIN_DELEGATION_ERROR_TYPES,S as DomainDelegationError,c as PUBLIC_DNS_QUERY_TIMEOUT_MS,m as createPublicResolvers,v as isDnsEmptyAnswer,N as normaliseDnsName};
@@ -1,5 +1,6 @@
1
1
  import type { Result } from "@fjall/generator";
2
2
  import type { DeployCallbacks } from "../types/callbacks.js";
3
+ import type { DelegatedDomainDeployer } from "./domain/delegatedDomainDeploy.js";
3
4
  /**
4
5
  * Configuration for a single domain to deploy.
5
6
  */
@@ -34,4 +35,13 @@ export interface DomainDeployProvider {
34
35
  * roles; delegated domains create NS records in the parent zone.
35
36
  */
36
37
  deployDomain(domainName: string, callbacks: DeployCallbacks): Promise<Result<void>>;
38
+ /**
39
+ * Optional phase-aware seam for delegated child domains (D8). Returning a
40
+ * deployer routes the domain through the engine's two-phase orchestration
41
+ * (`deployDelegatedDomain`: zone deploy → bounded NS-propagation gate →
42
+ * CAA preflight → full deploy) instead of the single-shot `deployDomain`.
43
+ * Return `undefined` (or omit the method) for domains that carry no
44
+ * child-writes delegation — they keep the legacy single-shot path.
45
+ */
46
+ delegatedDeployer?(domainName: string): DelegatedDomainDeployer | undefined;
37
47
  }
@@ -1 +1 @@
1
- import{CloudFormationClient as p,DescribeStackEventsCommand as m}from"@aws-sdk/client-cloudformation";import{success as a,failure as d}from"@fjall/generator";import{getErrorMessage as g,maskSensitiveOutput as l}from"@fjall/util";import{classifyDriftSuspectEvents as y,isOperationOpeningEvent as R}from"@fjall/util/aws";import{logger as E}from"@fjall/util/logger";import{composeSdkAbortSignal as S,isAborted as v}from"../../aws/organisations/types.js";const I="DriftClassify",h=5;function T(e){return e.includes("FAILED")}function D(e){return e.LogicalResourceId===void 0||e.ResourceType===void 0||e.ResourceStatus===void 0?null:{logicalId:e.LogicalResourceId,resourceType:e.ResourceType,status:e.ResourceStatus,timestamp:e.Timestamp??new Date(0),...e.PhysicalResourceId!==void 0&&e.PhysicalResourceId!==""?{physicalId:e.PhysicalResourceId}:{},...e.ResourceStatusReason!==void 0?{statusReason:e.ResourceStatusReason}:{}}}async function L(e,n,r){const t=e.getClient(p),o=[];let s,u=0;try{do{if(v(r))return d(new Error(`Drift event scan aborted for ${n}`));const i=await t.send(new m({StackName:n,...s!==void 0?{NextToken:s}:{}}),{abortSignal:S(r)});u+=1;for(const f of i.StackEvents??[]){const c=D(f);if(c!==null&&(o.push(c),R(c,n)))return a(o)}if(s=i.NextToken,u>=h&&s!==void 0)return E.debug(I,"Event scan truncated at page budget",{stackName:n,pages:u,eventsCollected:o.length}),a(o)}while(s!==void 0);return a(o)}catch(i){return d(new Error(`Failed to read stack events for ${n}: ${l(g(i))}`))}}function O(e,n){const r=n.filter(t=>T(t.status));return y(r).map(t=>({stackName:e,logicalId:t.logicalId,resourceType:t.resourceType,...t.physicalId!==void 0?{physicalId:t.physicalId}:{},failedStatus:t.failedStatus,statusReason:l(t.statusReason)}))}export{O as classifyDriftFailure,L as fetchLastOperationEvents};
1
+ var m=Object.defineProperty;var r=(e,n)=>m(e,"name",{value:n,configurable:!0});import{CloudFormationClient as g,DescribeStackEventsCommand as y}from"@aws-sdk/client-cloudformation";import{success as d,failure as l}from"@fjall/generator";import{getErrorMessage as R,maskSensitiveOutput as f}from"@fjall/util";import{classifyDriftSuspectEvents as E,isOperationOpeningEvent as S}from"@fjall/util/aws";import{logger as v}from"@fjall/util/logger";import{composeSdkAbortSignal as I,isAborted as h}from"../../aws/organisations/types.js";const T="DriftClassify",D=5;function b(e){return e.includes("FAILED")}r(b,"isFailedStatus");function C(e){return e.LogicalResourceId===void 0||e.ResourceType===void 0||e.ResourceStatus===void 0?null:{logicalId:e.LogicalResourceId,resourceType:e.ResourceType,status:e.ResourceStatus,timestamp:e.Timestamp??new Date(0),...e.PhysicalResourceId!==void 0&&e.PhysicalResourceId!==""?{physicalId:e.PhysicalResourceId}:{},...e.ResourceStatusReason!==void 0?{statusReason:e.ResourceStatusReason}:{}}}r(C,"toResourceEvent");async function $(e,n,i){const t=e.getClient(g),o=[];let s,c=0;try{do{if(h(i))return l(new Error(`Drift event scan aborted for ${n}`));const u=await t.send(new y({StackName:n,...s!==void 0?{NextToken:s}:{}}),{abortSignal:I(i)});c+=1;for(const p of u.StackEvents??[]){const a=C(p);if(a!==null&&(o.push(a),S(a,n)))return d(o)}if(s=u.NextToken,c>=D&&s!==void 0)return v.debug(T,"Event scan truncated at page budget",{stackName:n,pages:c,eventsCollected:o.length}),d(o)}while(s!==void 0);return d(o)}catch(u){return l(new Error(`Failed to read stack events for ${n}: ${f(R(u))}`))}}r($,"fetchLastOperationEvents");function G(e,n){const i=n.filter(t=>b(t.status));return E(i).map(t=>({stackName:e,logicalId:t.logicalId,resourceType:t.resourceType,...t.physicalId!==void 0?{physicalId:t.physicalId}:{},failedStatus:t.failedStatus,statusReason:f(t.statusReason)}))}r(G,"classifyDriftFailure");export{G as classifyDriftFailure,$ as fetchLastOperationEvents};
@@ -1 +1 @@
1
- import{STACK_NOT_FOUND_PATTERN as b}from"@fjall/util/aws";import{success as g,failure as S}from"@fjall/generator";import{classifyDriftFailure as D,fetchLastOperationEvents as h}from"./classifyDriftFailure.js";import{clearDriftSuspects as j,readDriftSuspects as _,recordDriftSuspects as I}from"./driftSuspectJournal.js";async function E(e,m){const{stackName:t,abortSignal:o}=m,n=e.aws.getAccountId(),a=e.aws.getRegion(),r=await h(e.aws,t,o);let c=[];if(r.success)c=D(t,r.data);else if(!r.error.message.includes(b))return S(r.error);const l=await _(n,a,t,e.journal),v=new Set(c.map(s=>s.logicalId)),f=(l?.suspects??[]).filter(s=>!v.has(s.logicalId)),i=[...c,...f];if(i.length===0)return g({stackName:t,suspects:[],findings:[],suspectSources:{events:0,journal:0}});const u=await e.probe.probeSuspects({stackName:t,suspects:i,...o!==void 0?{abortSignal:o}:{}});if(!u.success)return S(u.error);const d=u.data,p=d.filter(s=>s.verdict==="confirmed-deleted"||s.verdict==="unconfirmed").map(({verdict:s,verdictDetail:R,probeMethod:A,verbs:N,...w})=>w);return p.length>0?await I({accountId:n,region:a,stackName:t,recordedAt:(e.now?.()??new Date).toISOString(),suspects:p},e.journal):l!==null&&await j(n,a,t,e.journal),g({stackName:t,suspects:i,findings:d,suspectSources:{events:c.length,journal:f.length}})}export{E as detectStackDrift};
1
+ var D=Object.defineProperty;var S=(e,o)=>D(e,"name",{value:o,configurable:!0});import{STACK_NOT_FOUND_PATTERN as h}from"@fjall/util/aws";import{success as m,failure as v}from"@fjall/generator";import{classifyDriftFailure as j,fetchLastOperationEvents as _}from"./classifyDriftFailure.js";import{clearDriftSuspects as I,readDriftSuspects as R,recordDriftSuspects as A}from"./driftSuspectJournal.js";async function x(e,o){const{stackName:t,abortSignal:n}=o,a=e.aws.getAccountId(),i=e.aws.getRegion(),r=await _(e.aws,t,n);let c=[];if(r.success)c=j(t,r.data);else if(!r.error.message.includes(h))return v(r.error);const f=await R(a,i,t,e.journal),w=new Set(c.map(s=>s.logicalId)),d=(f?.suspects??[]).filter(s=>!w.has(s.logicalId)),u=[...c,...d];if(u.length===0)return m({stackName:t,suspects:[],findings:[],suspectSources:{events:0,journal:0}});const l=await e.probe.probeSuspects({stackName:t,suspects:u,...n!==void 0?{abortSignal:n}:{}});if(!l.success)return v(l.error);const p=l.data,g=p.filter(s=>s.verdict==="confirmed-deleted"||s.verdict==="unconfirmed").map(({verdict:s,verdictDetail:N,probeMethod:O,verbs:T,...b})=>b);return g.length>0?await A({accountId:a,region:i,stackName:t,recordedAt:(e.now?.()??new Date).toISOString(),suspects:g},e.journal):f!==null&&await I(a,i,t,e.journal),m({stackName:t,suspects:u,findings:p,suspectSources:{events:c.length,journal:d.length}})}S(x,"detectStackDrift");export{x as detectStackDrift};
@@ -1,7 +1,7 @@
1
- import{success as g}from"@fjall/generator";import{detectStackDrift as $}from"./detectStackDrift.js";import{readDriftSuspects as v}from"./driftSuspectJournal.js";const y=new Set(["UPDATE_ROLLBACK_COMPLETE","UPDATE_ROLLBACK_FAILED","UPDATE_FAILED"]),b=9e4;async function D(i,l){const n=l.budgetMs??b,c=l.abortSignal!==void 0?AbortSignal.any([l.abortSignal,AbortSignal.timeout(n)]):AbortSignal.any([AbortSignal.timeout(n)]),r={confirmed:[],unconfirmed:[],probedStacks:[]},u=await Promise.all(l.stackNames.map(async o=>{const a=await l.cfnService.getStackStatus(o,void 0,c);return{stackName:o,status:a.success?a.data?.status??null:null}}));for(const{stackName:o,status:a}of u){if(!(a!==null&&y.has(a))){const t=await v(i.aws.getAccountId(),i.aws.getRegion(),o,i.journal);if(t===null||t.suspects.length===0)continue}r.probedStacks.push(o);const d=await $(i,{stackName:o,abortSignal:c});if(d.success)for(const t of d.data.findings)t.verdict==="confirmed-deleted"?r.confirmed.push(t):(t.verdict==="unconfirmed"||t.verdict==="stack-missing")&&r.unconfirmed.push(t)}return g(r)}function I(i,l=[],n={}){const c=i.map(e=>` ${e.stackName} \u203A ${e.logicalId} (${e.resourceType})${e.physicalId!==void 0?` [${e.physicalId}]`:""} \u2014 ${e.verdictDetail}`),r=l.map(e=>`Remediation ${e} is in progress \u2014 run 'fjall drift resume ${e}' to continue it.`),u=(n.flipNotLive??[]).map(({finding:e,opId:s})=>`Remediation ${s} recorded its Retain flip on ${e.stackName} \u203A ${e.logicalId}, but the flip is no longer live on the stack \u2014 re-run 'fjall drift repair' to re-verify before deploying.`),o=(n.flipUnverified??[]).map(({finding:e,opId:s,message:h})=>`Remediation ${s} should complete with this deploy, but ${e.stackName} \u203A ${e.logicalId} could not be verified: the live template could not be read to confirm the Retain flip is still live \u2014 ${h}. Retry the deploy.`),a=(n.stillDeclared??[]).map(({finding:e,opId:s})=>`Remediation ${s} is ready to converge, but ${e.stackName} \u203A ${e.logicalId} is still declared in your fjall config \u2014 remove the resource from the config, then deploy to complete the forget.`),f=[...r,...u,...o,...a],d=new Set((n.flipUnverified??[]).map(({finding:e})=>`${e.stackName}/${e.logicalId}`)),t=i.length>0&&i.every(e=>d.has(`${e.stackName}/${e.logicalId}`)),m=t?`Deploy blocked: the Retain-flip state of ${i.length} mid-remediation resource(s) could not be verified (the live template could not be read):
2
- `:`Deploy blocked: CloudFormation still tracks ${i.length} resource(s) deleted outside CloudFormation \u2014 deploying would fail and re-wedge the stack:
3
- `,p=t?"Retry the deploy \u2014 verification is transient when the live template read fails; if it keeps failing, check CloudFormation access to the stack template.":"Run 'fjall drift detect' for the full report, then 'fjall drift repair --remediate <physicalName>=<verb>' to remediate confirmed deletions.";return m+`${c.join(`
1
+ var $=Object.defineProperty;var m=(t,o)=>$(t,"name",{value:o,configurable:!0});import{success as v}from"@fjall/generator";import{detectStackDrift as y}from"./detectStackDrift.js";import{readDriftSuspects as b}from"./driftSuspectJournal.js";const k=new Set(["UPDATE_ROLLBACK_COMPLETE","UPDATE_ROLLBACK_FAILED","UPDATE_FAILED"]),S=9e4;async function A(t,o){const n=o.budgetMs??S,c=o.abortSignal!==void 0?AbortSignal.any([o.abortSignal,AbortSignal.timeout(n)]):AbortSignal.any([AbortSignal.timeout(n)]),r={confirmed:[],unconfirmed:[],probedStacks:[]},u=await Promise.all(o.stackNames.map(async l=>{const a=await o.cfnService.getStackStatus(l,void 0,c);return{stackName:l,status:a.success?a.data?.status??null:null}}));for(const{stackName:l,status:a}of u){if(!(a!==null&&k.has(a))){const i=await b(t.aws.getAccountId(),t.aws.getRegion(),l,t.journal);if(i===null||i.suspects.length===0)continue}r.probedStacks.push(l);const d=await y(t,{stackName:l,abortSignal:c});if(d.success)for(const i of d.data.findings)i.verdict==="confirmed-deleted"?r.confirmed.push(i):(i.verdict==="unconfirmed"||i.verdict==="stack-missing")&&r.unconfirmed.push(i)}return v(r)}m(A,"runDriftPreFlight");function w(t,o=[],n={}){const c=t.map(e=>` ${e.stackName} \u203A ${e.logicalId} (${e.resourceType})${e.physicalId!==void 0?` [${e.physicalId}]`:""} \u2014 ${e.verdictDetail}`),r=o.map(e=>`Remediation ${e} is in progress \u2014 run 'fjall drift resume ${e}' to continue it.`),u=(n.flipNotLive??[]).map(({finding:e,opId:s})=>`Remediation ${s} recorded its Retain flip on ${e.stackName} \u203A ${e.logicalId}, but the flip is no longer live on the stack \u2014 re-run 'fjall drift repair' to re-verify before deploying.`),l=(n.flipUnverified??[]).map(({finding:e,opId:s,message:g})=>`Remediation ${s} should complete with this deploy, but ${e.stackName} \u203A ${e.logicalId} could not be verified: the live template could not be read to confirm the Retain flip is still live \u2014 ${g}. Retry the deploy.`),a=(n.stillDeclared??[]).map(({finding:e,opId:s})=>`Remediation ${s} is ready to converge, but ${e.stackName} \u203A ${e.logicalId} is still declared in your fjall config \u2014 remove the resource from the config, then deploy to complete the forget.`),f=[...r,...u,...l,...a],d=new Set((n.flipUnverified??[]).map(({finding:e})=>`${e.stackName}/${e.logicalId}`)),i=t.length>0&&t.every(e=>d.has(`${e.stackName}/${e.logicalId}`)),p=i?`Deploy blocked: the Retain-flip state of ${t.length} mid-remediation resource(s) could not be verified (the live template could not be read):
2
+ `:`Deploy blocked: CloudFormation still tracks ${t.length} resource(s) deleted outside CloudFormation \u2014 deploying would fail and re-wedge the stack:
3
+ `,h=i?"Retry the deploy \u2014 verification is transient when the live template read fails; if it keeps failing, check CloudFormation access to the stack template.":"Run 'fjall drift detect' for the full report, then 'fjall drift repair --remediate <physicalName>=<verb>' to remediate confirmed deletions.";return p+`${c.join(`
4
4
  `)}
5
- ${p}${f.length>0?`
5
+ ${h}${f.length>0?`
6
6
  ${f.join(`
7
- `)}`:""}`}export{b as DRIFT_PREFLIGHT_BUDGET_MS,y as DRIFT_PREFLIGHT_TRIGGER_STATUSES,I as formatDriftPreFlightBlock,D as runDriftPreFlight};
7
+ `)}`:""}`}m(w,"formatDriftPreFlightBlock");export{S as DRIFT_PREFLIGHT_BUDGET_MS,k as DRIFT_PREFLIGHT_TRIGGER_STATUSES,w as formatDriftPreFlightBlock,A as runDriftPreFlight};