@fjall/deploy-core 2.27.0 → 2.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/dist/.minified +1 -1
  2. package/dist/src/aws/organisations/backup.d.ts +7 -2
  3. package/dist/src/aws/organisations/backup.js +2 -2
  4. package/dist/src/aws/organisations/index.d.ts +1 -1
  5. package/dist/src/aws/organisations/index.js +1 -1
  6. package/dist/src/aws/organisations/oidcProvider.d.ts +37 -0
  7. package/dist/src/aws/organisations/oidcProvider.js +1 -1
  8. package/dist/src/aws/organisations/organisation.d.ts +21 -0
  9. package/dist/src/aws/organisations/organisation.js +1 -1
  10. package/dist/src/aws/utils/cloudformationEventHelpers.d.ts +45 -0
  11. package/dist/src/aws/utils/cloudformationEventHelpers.js +1 -1
  12. package/dist/src/aws/utils/cloudformationEventTypes.d.ts +1 -1
  13. package/dist/src/aws/utils/cloudformationEvents.d.ts +2 -1
  14. package/dist/src/aws/utils/cloudformationEvents.js +1 -1
  15. package/dist/src/aws/utils/stackStatus.js +1 -1
  16. package/dist/src/index.d.ts +16 -3
  17. package/dist/src/index.js +1 -1
  18. package/dist/src/orchestration/activeDeploymentGuard.d.ts +41 -0
  19. package/dist/src/orchestration/activeDeploymentGuard.js +8 -4
  20. package/dist/src/orchestration/application/applicationDeploy.d.ts +9 -0
  21. package/dist/src/orchestration/application/applicationDeploy.js +1 -1
  22. package/dist/src/orchestration/application/applicationDeployHelpers.d.ts +20 -3
  23. package/dist/src/orchestration/application/applicationDeployHelpers.js +3 -3
  24. package/dist/src/orchestration/application/applicationDestroy.js +1 -1
  25. package/dist/src/orchestration/application/approvalGate.d.ts +15 -1
  26. package/dist/src/orchestration/application/approvalGate.js +1 -1
  27. package/dist/src/orchestration/application/destructionGate.d.ts +164 -0
  28. package/dist/src/orchestration/application/destructionGate.js +5 -0
  29. package/dist/src/orchestration/application/dockerBuildHelper.js +1 -1
  30. package/dist/src/orchestration/application/leaseGate.d.ts +30 -0
  31. package/dist/src/orchestration/application/leaseGate.js +1 -0
  32. package/dist/src/orchestration/application/openNextBuild.d.ts +1 -1
  33. package/dist/src/orchestration/application/openNextBuild.js +3 -3
  34. package/dist/src/orchestration/application/plan/approvalToken.d.ts +15 -0
  35. package/dist/src/orchestration/application/plan/approvalToken.js +1 -1
  36. package/dist/src/orchestration/application/plan/changeSetEscalation.d.ts +46 -0
  37. package/dist/src/orchestration/application/plan/changeSetEscalation.js +1 -0
  38. package/dist/src/orchestration/application/plan/computeDeployPlan.d.ts +13 -0
  39. package/dist/src/orchestration/application/plan/computeDeployPlan.js +1 -1
  40. package/dist/src/orchestration/application/plan/index.d.ts +4 -1
  41. package/dist/src/orchestration/application/plan/index.js +1 -1
  42. package/dist/src/orchestration/application/plan/loadDeployPlan.d.ts +32 -0
  43. package/dist/src/orchestration/application/plan/loadDeployPlan.js +1 -0
  44. package/dist/src/orchestration/application/plan/pinnedNames.d.ts +31 -0
  45. package/dist/src/orchestration/application/plan/pinnedNames.js +1 -0
  46. package/dist/src/orchestration/application/plan/registryOverlay.d.ts +83 -0
  47. package/dist/src/orchestration/application/plan/registryOverlay.js +1 -0
  48. package/dist/src/orchestration/application/plan/renderDeployPlan.d.ts +4 -1
  49. package/dist/src/orchestration/application/plan/renderDeployPlan.js +1 -1
  50. package/dist/src/orchestration/application/plan/types.d.ts +31 -0
  51. package/dist/src/orchestration/application/plan/types.js +1 -0
  52. package/dist/src/orchestration/builders/frameworkRegistry.js +1 -1
  53. package/dist/src/orchestration/builders/index.d.ts +1 -0
  54. package/dist/src/orchestration/builders/index.js +1 -1
  55. package/dist/src/orchestration/builders/openNextBuilder.js +1 -1
  56. package/dist/src/orchestration/builders/staticSiteBuilder.d.ts +27 -0
  57. package/dist/src/orchestration/builders/staticSiteBuilder.js +2 -0
  58. package/dist/src/orchestration/contextHelpers.d.ts +37 -1
  59. package/dist/src/orchestration/contextHelpers.js +1 -1
  60. package/dist/src/orchestration/drift/classifyDriftFailure.d.ts +31 -0
  61. package/dist/src/orchestration/drift/classifyDriftFailure.js +1 -0
  62. package/dist/src/orchestration/drift/detectStackDrift.d.ts +37 -0
  63. package/dist/src/orchestration/drift/detectStackDrift.js +1 -0
  64. package/dist/src/orchestration/drift/driftPreFlight.d.ts +73 -0
  65. package/dist/src/orchestration/drift/driftPreFlight.js +7 -0
  66. package/dist/src/orchestration/drift/driftProbe.d.ts +113 -0
  67. package/dist/src/orchestration/drift/driftProbe.js +1 -0
  68. package/dist/src/orchestration/drift/driftSuspectJournal.d.ts +40 -0
  69. package/dist/src/orchestration/drift/driftSuspectJournal.js +1 -0
  70. package/dist/src/orchestration/drift/preFlightGate.d.ts +44 -0
  71. package/dist/src/orchestration/drift/preFlightGate.js +2 -0
  72. package/dist/src/orchestration/index.d.ts +19 -0
  73. package/dist/src/orchestration/index.js +1 -1
  74. package/dist/src/orchestration/organisation/cascadeDestroyHelpers.d.ts +7 -0
  75. package/dist/src/orchestration/organisation/cascadeDestroyHelpers.js +1 -1
  76. package/dist/src/orchestration/organisation/cascadeHelpers.js +1 -1
  77. package/dist/src/orchestration/organisation/organisationDestroy.js +3 -3
  78. package/dist/src/orchestration/organisationDeploy/infraSteps.d.ts +8 -1
  79. package/dist/src/orchestration/organisationDeploy/infraSteps.js +1 -1
  80. package/dist/src/orchestration/organisationDeploy/orgCascadeDeploy.js +5 -5
  81. package/dist/src/orchestration/organisationDeploy/orgContext.d.ts +21 -2
  82. package/dist/src/orchestration/organisationDeploy/orgContext.js +1 -1
  83. package/dist/src/orchestration/organisationDeploy/singleComponentDeploy.js +1 -1
  84. package/dist/src/orchestration/remediation/driftRepairGate.d.ts +66 -0
  85. package/dist/src/orchestration/remediation/driftRepairGate.js +1 -0
  86. package/dist/src/orchestration/remediation/forensicsCapture.d.ts +49 -0
  87. package/dist/src/orchestration/remediation/forensicsCapture.js +1 -0
  88. package/dist/src/orchestration/remediation/forgetResource.d.ts +423 -0
  89. package/dist/src/orchestration/remediation/forgetResource.js +1 -0
  90. package/dist/src/orchestration/remediation/pinRemediation.d.ts +121 -0
  91. package/dist/src/orchestration/remediation/pinRemediation.js +1 -0
  92. package/dist/src/orchestration/remediation/postFailureRepairOffer.d.ts +77 -0
  93. package/dist/src/orchestration/remediation/postFailureRepairOffer.js +14 -0
  94. package/dist/src/orchestration/remediation/recreatePreFlight.d.ts +47 -0
  95. package/dist/src/orchestration/remediation/recreatePreFlight.js +1 -0
  96. package/dist/src/orchestration/remediation/recreateResource.d.ts +110 -0
  97. package/dist/src/orchestration/remediation/recreateResource.js +1 -0
  98. package/dist/src/orchestration/remediation/remediationJournal.d.ts +88 -0
  99. package/dist/src/orchestration/remediation/remediationJournal.js +2 -0
  100. package/dist/src/orchestration/remediation/removalUpdate.d.ts +135 -0
  101. package/dist/src/orchestration/remediation/removalUpdate.js +1 -0
  102. package/dist/src/orchestration/remediation/retainFlip.d.ts +294 -0
  103. package/dist/src/orchestration/remediation/retainFlip.js +1 -0
  104. package/dist/src/orchestration/spawnHelpers.d.ts +9 -0
  105. package/dist/src/orchestration/spawnHelpers.js +1 -1
  106. package/dist/src/services/application/ApplicationStackService.d.ts +5 -3
  107. package/dist/src/services/application/ApplicationStackService.js +1 -1
  108. package/dist/src/services/application/applicationStackHelpers.d.ts +8 -6
  109. package/dist/src/services/application/applicationStackHelpers.js +3 -3
  110. package/dist/src/services/index.d.ts +2 -2
  111. package/dist/src/services/index.js +1 -1
  112. package/dist/src/services/infrastructure/CdkEventMonitoring.d.ts +15 -0
  113. package/dist/src/services/infrastructure/CdkEventMonitoring.js +1 -1
  114. package/dist/src/services/infrastructure/CdkOutputAnalyser.d.ts +8 -0
  115. package/dist/src/services/infrastructure/CdkOutputAnalyser.js +2 -1
  116. package/dist/src/services/infrastructure/CdkService.js +3 -3
  117. package/dist/src/services/infrastructure/CfnRegistryService.d.ts +68 -0
  118. package/dist/src/services/infrastructure/CfnRegistryService.js +1 -0
  119. package/dist/src/services/infrastructure/CloudFormationService.d.ts +5 -2
  120. package/dist/src/services/infrastructure/CloudFormationService.js +1 -1
  121. package/dist/src/services/infrastructure/EcsServiceResolver.d.ts +23 -3
  122. package/dist/src/services/infrastructure/EcsServiceResolver.js +1 -1
  123. package/dist/src/services/infrastructure/cfnRegistrySeed.d.ts +10 -0
  124. package/dist/src/services/infrastructure/cfnRegistrySeed.js +1 -0
  125. package/dist/src/services/infrastructure/changeSetProbe.d.ts +88 -0
  126. package/dist/src/services/infrastructure/changeSetProbe.js +1 -0
  127. package/dist/src/services/infrastructure/index.d.ts +2 -0
  128. package/dist/src/services/infrastructure/index.js +1 -1
  129. package/dist/src/types/callbackKeys.d.ts +1 -1
  130. package/dist/src/types/callbackKeys.js +1 -1
  131. package/dist/src/types/callbacks.d.ts +22 -0
  132. package/dist/src/types/config/entitlements.d.ts +9 -0
  133. package/dist/src/types/config/orgConfig.d.ts +6 -0
  134. package/dist/src/types/deployEvent.d.ts +10 -3
  135. package/dist/src/types/deployEvent.js +1 -1
  136. package/dist/src/types/deploymentEventSchema.d.ts +1 -0
  137. package/dist/src/types/deploymentEventSchema.js +1 -1
  138. package/dist/src/types/destruction.d.ts +212 -0
  139. package/dist/src/types/destruction.js +1 -0
  140. package/dist/src/types/detection/patternTypes.d.ts +9 -4
  141. package/dist/src/types/detection/patternTypes.js +1 -1
  142. package/dist/src/types/drift.d.ts +72 -0
  143. package/dist/src/types/drift.js +1 -0
  144. package/dist/src/types/frameworkBuilder.d.ts +14 -0
  145. package/dist/src/types/index.d.ts +44 -4
  146. package/dist/src/types/index.js +1 -1
  147. package/dist/src/types/operations.d.ts +2 -1
  148. package/dist/src/types/params.d.ts +142 -0
  149. package/dist/src/types/remediation.d.ts +126 -0
  150. package/dist/src/types/remediation.js +1 -0
  151. package/package.json +6 -4
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Change-set escalation — Layer 2 of the replacement oracle (drift-remediation
3
+ * plan § 2, [G4]). Runs the review-only change-set probe ONLY on rows whose
4
+ * Layer-1 verdict is `conditional`/`may` on a stateful or pinned-name
5
+ * resource: definite createOnly hits are already proven, and stateless
6
+ * unpinned conditionals never ticket, so probing either buys no proof. A
7
+ * non-engaged deploy issues zero probe calls.
8
+ *
9
+ * Verdict precedence: change-set > registry > cdk-spec.
10
+ * Replacement: False → the ceremony DOWNGRADES to a warning — the row
11
+ * becomes an in-place update (directive 2:
12
+ * never block on a false positive)
13
+ * Replacement: True → conditional upgrades to definite, verdictSource
14
+ * `"change-set"`
15
+ * Replacement: Conditional → stays worst-case (the registry verdict stands)
16
+ * probe failure → the registry verdict stands, annotated
17
+ * `"registry (change-set unverified)"` — degrade
18
+ * to the conservative verdict, never to silence
19
+ *
20
+ * A resource the change set omits entirely (including the benign
21
+ * `didn't contain changes` outcome) will not be touched by the update —
22
+ * treated as `False`.
23
+ */
24
+ import type { ChangeSetProbeRunner } from "../../../services/infrastructure/changeSetProbe.js";
25
+ import type { StackTemplatePair } from "./buildDeployPlan.js";
26
+ import type { DeployPlan, DeployPlanResourceChange } from "./types.js";
27
+ /**
28
+ * The [G4] trigger predicate: a replacement row that is neither proven
29
+ * (`will`) nor irrelevant (`none`), on a resource that would actually ticket
30
+ * — stateful (data loss) or pinned-name (CloudFormation refusal).
31
+ */
32
+ export declare function needsChangeSetEscalation(change: DeployPlanResourceChange): boolean;
33
+ export interface ApplyChangeSetEscalationParams {
34
+ plan: DeployPlan;
35
+ probe: ChangeSetProbeRunner;
36
+ /** The diffed template pairs — the probe submits `newTemplate`. */
37
+ stacks: readonly StackTemplatePair[];
38
+ abortSignal?: AbortSignal;
39
+ }
40
+ /**
41
+ * Apply the change-set escalation to an overlay-corrected plan. Never throws
42
+ * and never fails the plan: a failed or aborted probe leaves the conservative
43
+ * registry verdict in place, annotated unverified. One probe per escalated
44
+ * stack covers every candidate row in it.
45
+ */
46
+ export declare function applyChangeSetEscalation(params: ApplyChangeSetEscalationParams): Promise<DeployPlan>;
@@ -0,0 +1 @@
1
+ import{maskSensitiveOutput as k}from"@fjall/util";import{logger as u}from"@fjall/util/logger";import{isAborted as S}from"../../../aws/organisations/types.js";import{isDataLoss as w,isStatefulResourceType as g}from"./classify.js";import{recountActions as T}from"./registryOverlay.js";import{renderPlanSummary as C}from"./renderDeployPlan.js";const p="ChangeSetEscalation";function M(t){return t.action!=="replace"||t.retained||t.replacementMode!=="conditional"&&t.replacementMode!=="may"?!1:g(t.resourceType)||t.pinnedPhysicalName!==void 0}function b(t){return`change-set preflight proved this update applies in place (${t.replacementMode} verdict downgraded)`}function N(t){return{...t,action:"update",replacementMode:"none",destructive:!1,dataLoss:!1,propertyChanges:t.propertyChanges.map(r=>({...r,forcesReplacement:!1})),verdictSource:"change-set",interruptionNote:b(t)}}function L(t){return{...t,action:"replace",replacementMode:"will",destructive:!0,dataLoss:w({action:"replace",retained:t.retained,resourceType:t.resourceType}),verdictSource:"change-set"}}function $(t){return t.verdictSource!=="registry"?t:{...t,verdictSource:"registry (change-set unverified)"}}async function A(t){const{plan:r,probe:f,stacks:l,abortSignal:i}=t,c=r.changes.filter(M);if(c.length===0)return r;const m=new Map(l.map(e=>[e.stackName,e])),y=[...new Set(c.map(e=>e.stack))],a=new Map;for(const e of y){if(S(i)){a.set(e,"unverified");continue}const n=m.get(e);if(n===void 0){u.warn(p,"No synthesised template for escalated stack",{stackName:e}),a.set(e,"unverified");continue}const s=await f.probeStackReplacements({stackName:e,template:n.newTemplate,...i!==void 0?{abortSignal:i}:{}});if(!s.success){u.warn(p,"Change-set probe degraded to registry verdict",{stackName:e,error:k(s.error.message)}),a.set(e,"unverified");continue}a.set(e,s.data.replacementByLogicalId)}const v=new Set(c.map(e=>`${e.stack}|${e.address}`)),o=r.changes.map(e=>{if(!v.has(`${e.stack}|${e.address}`))return e;const n=a.get(e.stack);if(n===void 0||n==="unverified")return $(e);const s=n.get(e.address)??"False";return s==="False"?N(e):s==="True"?L(e):(u.warn(p,"Change set could not decide \u2014 keeping worst case",{stack:e.stack,address:e.address,resourceType:e.resourceType}),e)}),d=T(o);return d["no-op"]=r.counts["no-op"],{...r,changes:o,counts:d,hasDestructiveChanges:o.some(e=>e.destructive),summary:C({counts:d,changes:o})}}export{A as applyChangeSetEscalation,M as needsChangeSetEscalation};
@@ -6,6 +6,8 @@
6
6
  */
7
7
  import { type Result } from "@fjall/generator";
8
8
  import type { CloudFormationError } from "../../../services/infrastructure/CloudFormationService.js";
9
+ import type { CfnRegistrySummaryReader } from "../../../services/infrastructure/CfnRegistryService.js";
10
+ import type { ChangeSetProbeRunner } from "../../../services/infrastructure/changeSetProbe.js";
9
11
  import type { DeployPlan } from "./types.js";
10
12
  /** The narrow CFN read surface the gate needs — CloudFormationService satisfies it. */
11
13
  export interface CfnTemplateReader {
@@ -20,5 +22,16 @@ export interface ComputeDeployPlanParams {
20
22
  cfnService: CfnTemplateReader;
21
23
  stacks: ComputeDeployPlanStack[];
22
24
  assemblyDigest: string;
25
+ /**
26
+ * Registry oracle for replacement-verdict recomputation. Optional — without
27
+ * it the plan carries raw cloudformation-diff verdicts (no `verdictSource`).
28
+ */
29
+ registry?: CfnRegistrySummaryReader;
30
+ /**
31
+ * Change-set escalation probe [G4]. Optional — without it conditional
32
+ * verdicts stay worst-cased into the ceremony (the sanctioned D2 fallback).
33
+ */
34
+ changeSetProbe?: ChangeSetProbeRunner;
35
+ abortSignal?: AbortSignal;
23
36
  }
24
37
  export declare function computeDeployPlan(params: ComputeDeployPlanParams): Promise<Result<DeployPlan, Error>>;
@@ -1 +1 @@
1
- import{success as o,failure as n}from"@fjall/generator";import{maskSensitiveOutput as i}from"@fjall/util";import{buildDeployPlan as m}from"./buildDeployPlan.js";function p(t){const r=t.trim();if(r==="")return o({});try{return o(JSON.parse(r))}catch(e){return n(new Error(`Failed to parse deployed template: ${i(e instanceof Error?e.message:String(e))}`))}}async function d(t){const r=[];for(const e of t.stacks){const s=await t.cfnService.getTemplate(e.stackName);let c={};if(s.success){const a=p(s.data);if(!a.success)return n(a.error);c=a.data}else if(s.error.errorType!=="stack_not_found")return n(s.error);r.push({stackName:e.stackName,oldTemplate:c,newTemplate:e.newTemplate})}return o(m(r,t.assemblyDigest))}export{d as computeDeployPlan};
1
+ import{success as i,failure as s}from"@fjall/generator";import{maskSensitiveOutput as c}from"@fjall/util";import{buildDeployPlan as f}from"./buildDeployPlan.js";import{applyChangeSetEscalation as u}from"./changeSetEscalation.js";import{applyRegistryOverlay as p}from"./registryOverlay.js";function d(e){const r=e.trim();if(r==="")return i({});try{return i(JSON.parse(r))}catch(t){return s(new Error(`Failed to parse deployed template: ${c(t instanceof Error?t.message:String(t))}`))}}async function k(e){const r=[];for(const o of e.stacks){const n=await e.cfnService.getTemplate(o.stackName);let l={};if(n.success){const a=d(n.data);if(!a.success)return s(a.error);l=a.data}else if(n.error.errorType!=="stack_not_found")return s(n.error);r.push({stackName:o.stackName,oldTemplate:l,newTemplate:o.newTemplate})}let t=f(r,e.assemblyDigest);return e.registry!==void 0&&(t=await p({plan:t,registry:e.registry,stacks:r,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}})),e.changeSetProbe!==void 0&&(t=await u({plan:t,probe:e.changeSetProbe,stacks:r,...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}})),i(t)}export{k as computeDeployPlan};
@@ -1,6 +1,9 @@
1
1
  /** Deploy-plan computation + rendering — the approval gate's read-side core. */
2
- export type { DeployPlan, DeployPlanResourceChange } from "./types.js";
2
+ export { REPLACEMENT_VERDICT_SOURCES, type DeployPlan, type DeployPlanResourceChange, type ReplacementVerdictSource } from "./types.js";
3
3
  export { buildDeployPlan, type StackTemplatePair } from "./buildDeployPlan.js";
4
+ export { applyRegistryOverlay, overlayResourceChange, registryVerdictForProperty, type ApplyRegistryOverlayParams, type OverlayResourceChangeResult, type RegistryVerdictDisagreement } from "./registryOverlay.js";
5
+ export { applyChangeSetEscalation, needsChangeSetEscalation, type ApplyChangeSetEscalationParams } from "./changeSetEscalation.js";
6
+ export { findPinnedPhysicalName, resourcePropertiesFromTemplate } from "./pinnedNames.js";
4
7
  export { computeDeployPlan, type CfnTemplateReader, type ComputeDeployPlanParams, type ComputeDeployPlanStack } from "./computeDeployPlan.js";
5
8
  export { computeAssemblyDigest, digestHead } from "./assemblyDigest.js";
6
9
  export { classifyImpact, derivePropertyChanges, isDataLoss, isStatefulResourceType, type ImpactClassification } from "./classify.js";
@@ -1 +1 @@
1
- import{buildDeployPlan as o}from"./buildDeployPlan.js";import{computeDeployPlan as a}from"./computeDeployPlan.js";import{computeAssemblyDigest as s,digestHead as l}from"./assemblyDigest.js";import{classifyImpact as m,derivePropertyChanges as i,isDataLoss as P,isStatefulResourceType as f}from"./classify.js";import{renderPlanSummary as T,renderPlanLines as y,toWirePlanChanges as d}from"./renderDeployPlan.js";import{signApprovalToken as x,verifyApprovalToken as L,DEFAULT_APPROVAL_TTL_MS as c,APPROVAL_TOKEN_PATTERN as g}from"./approvalToken.js";export{g as APPROVAL_TOKEN_PATTERN,c as DEFAULT_APPROVAL_TTL_MS,o as buildDeployPlan,m as classifyImpact,s as computeAssemblyDigest,a as computeDeployPlan,i as derivePropertyChanges,l as digestHead,P as isDataLoss,f as isStatefulResourceType,y as renderPlanLines,T as renderPlanSummary,x as signApprovalToken,d as toWirePlanChanges,L as verifyApprovalToken};
1
+ import{REPLACEMENT_VERDICT_SOURCES as o}from"./types.js";import{buildDeployPlan as t}from"./buildDeployPlan.js";import{applyRegistryOverlay as s,overlayResourceChange as n,registryVerdictForProperty as l}from"./registryOverlay.js";import{applyChangeSetEscalation as m,needsChangeSetEscalation as y}from"./changeSetEscalation.js";import{findPinnedPhysicalName as f,resourcePropertiesFromTemplate as T}from"./pinnedNames.js";import{computeDeployPlan as d}from"./computeDeployPlan.js";import{computeAssemblyDigest as x,digestHead as A}from"./assemblyDigest.js";import{classifyImpact as R,derivePropertyChanges as u,isDataLoss as C,isStatefulResourceType as L}from"./classify.js";import{renderPlanSummary as _,renderPlanLines as h,toWirePlanChanges as v}from"./renderDeployPlan.js";import{signApprovalToken as O,verifyApprovalToken as N,DEFAULT_APPROVAL_TTL_MS as V,APPROVAL_TOKEN_PATTERN as F}from"./approvalToken.js";export{F as APPROVAL_TOKEN_PATTERN,V as DEFAULT_APPROVAL_TTL_MS,o as REPLACEMENT_VERDICT_SOURCES,m as applyChangeSetEscalation,s as applyRegistryOverlay,t as buildDeployPlan,R as classifyImpact,x as computeAssemblyDigest,d as computeDeployPlan,u as derivePropertyChanges,A as digestHead,f as findPinnedPhysicalName,C as isDataLoss,L as isStatefulResourceType,y as needsChangeSetEscalation,n as overlayResourceChange,l as registryVerdictForProperty,h as renderPlanLines,_ as renderPlanSummary,T as resourcePropertiesFromTemplate,O as signApprovalToken,v as toWirePlanChanges,N as verifyApprovalToken};
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Deploy-plan loading — reads each changed stack's synthesised template from
3
+ * cdk.out and delegates to {@link computeDeployPlan} (live-template diff +
4
+ * registry overlay). Extracted from the approval gate so the unconditional
5
+ * destruction gate shares the SAME computation: `createMemoisedPlanLoader`
6
+ * guarantees a plan-gated deploy never diffs twice, while a non-engaged deploy
7
+ * computes the plan exactly once for the destruction gate.
8
+ */
9
+ import { type Result } from "@fjall/generator";
10
+ import type { CfnRegistrySummaryReader } from "../../../services/infrastructure/CfnRegistryService.js";
11
+ import type { ChangeSetProbeRunner } from "../../../services/infrastructure/changeSetProbe.js";
12
+ import { type CfnTemplateReader } from "./computeDeployPlan.js";
13
+ import type { DeployPlan } from "./types.js";
14
+ export interface LoadDeployPlanParams {
15
+ cfnService: CfnTemplateReader;
16
+ cdkOutPath: string;
17
+ changedStacks: string[];
18
+ assemblyDigest: string;
19
+ registry?: CfnRegistrySummaryReader;
20
+ changeSetProbe?: ChangeSetProbeRunner;
21
+ abortSignal?: AbortSignal;
22
+ }
23
+ /** A zero-argument plan supplier shared between the gates. */
24
+ export type DeployPlanLoader = () => Promise<Result<DeployPlan, Error>>;
25
+ export declare function loadDeployPlan(params: LoadDeployPlanParams): Promise<Result<DeployPlan, Error>>;
26
+ /**
27
+ * Memoise the plan computation for one deploy invocation: the first call
28
+ * computes (template reads + live diff + registry overlay), every later call
29
+ * returns the same settled promise — including a failure, which both gates
30
+ * must observe identically.
31
+ */
32
+ export declare function createMemoisedPlanLoader(params: LoadDeployPlanParams): DeployPlanLoader;
@@ -0,0 +1 @@
1
+ import{join as c}from"node:path";import{readFile as a}from"node:fs/promises";import{failure as l}from"@fjall/generator";import{maskSensitiveOutput as s}from"@fjall/util";import{computeDeployPlan as f}from"./computeDeployPlan.js";async function d(e){const t=[];for(const r of e.changedStacks){const i=c(e.cdkOutPath,`${r}.template.json`);let o;try{o=JSON.parse(await a(i,"utf8"))}catch(n){return l(new Error(s(`Failed to read synthesised template for ${r}: ${n instanceof Error?n.message:String(n)}`)))}t.push({stackName:r,newTemplate:o})}return f({cfnService:e.cfnService,stacks:t,assemblyDigest:e.assemblyDigest,...e.registry!==void 0?{registry:e.registry}:{},...e.changeSetProbe!==void 0?{changeSetProbe:e.changeSetProbe}:{},...e.abortSignal!==void 0?{abortSignal:e.abortSignal}:{}})}function y(e){let t;return()=>(t??=d(e),t)}export{y as createMemoisedPlanLoader,d as loadDeployPlan};
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Pinned-name detection: a resource is custom-named iff the property named by
3
+ * the registry schema's `primaryIdentifier` (fallback: the small
4
+ * `@fjall/util` map for types whose identifier is a generated attribute) is
5
+ * explicitly present in the synthesised template with a LITERAL value. The
6
+ * template is the source of truth — CDK-generated names arrive as intrinsics
7
+ * (`Ref` / `Fn::*`) or are omitted entirely, so a literal string here means
8
+ * the author pinned the physical name.
9
+ *
10
+ * `replacement-required ∧ custom-named ⇒ pinned-name-replacement`: the update
11
+ * fails at apply time with "CloudFormation cannot update a stack when a
12
+ * custom-named resource requires replacing" — the Phase 2 destruction gate
13
+ * consumes this signal; Phase 1 annotates it on the plan row.
14
+ */
15
+ /**
16
+ * Detect a pinned physical name on a template resource. Returns the literal
17
+ * name, or undefined when the resource is CDK-named (intrinsic or absent) or
18
+ * the naming property cannot be determined.
19
+ */
20
+ export declare function findPinnedPhysicalName(params: {
21
+ resourceType: string;
22
+ /** The resource's template `Properties` block. */
23
+ properties: Record<string, unknown> | undefined;
24
+ /** Registry `primaryIdentifier` pointers, when the oracle resolved them. */
25
+ primaryIdentifier?: readonly string[];
26
+ }): string | undefined;
27
+ /**
28
+ * Extract a resource's `Properties` block from a parsed CloudFormation
29
+ * template without asserting the template's overall shape.
30
+ */
31
+ export declare function resourcePropertiesFromTemplate(template: unknown, logicalId: string): Record<string, unknown> | undefined;
@@ -0,0 +1 @@
1
+ import{PHYSICAL_NAME_FALLBACK_PROPERTIES as f}from"@fjall/util";const o="/properties/";function d(e){if(e===void 0||e.length!==1)return;const r=e[0];if(r===void 0||!r.startsWith(o))return;const n=r.slice(o.length);if(!(n===""||n.includes("/")))return n}function u(e){if(typeof e=="string"&&e!=="")return e;if(typeof e=="number")return String(e)}function s(e){if(e.properties===void 0)return;const r=d(e.primaryIdentifier);if(r!==void 0){const t=u(e.properties[r]);if(t!==void 0)return t}const n=f[e.resourceType];if(n!==void 0)return u(e.properties[n])}function p(e,r){if(typeof e!="object"||e===null)return;const n=e.Resources;if(typeof n!="object"||n===null)return;const t=n[r];if(typeof t!="object"||t===null)return;const i=t.Properties;if(!(typeof i!="object"||i===null||Array.isArray(i)))return i}export{s as findPinnedPhysicalName,p as resourcePropertiesFromTemplate};
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Registry verdict overlay — Layer 1 of the replacement oracle. The
3
+ * cloudformation-diff verdict (Layer 0) is computed from a stale bundled CDK
4
+ * resource spec and is retained only as the trigger; every changed property
5
+ * path is recomputed against the live registry catalogue
6
+ * (`createOnlyProperties` / `conditionalCreateOnlyProperties`):
7
+ *
8
+ * path ∈ createOnly → `will` (destructive)
9
+ * path ∈ conditionalCreateOnly → `conditional` (destructive worst-case,
10
+ * pending the Phase 3 change-set escalation)
11
+ * path in NEITHER → `none` — even where cloudformation-diff
12
+ * said WILL_REPLACE (kills the false
13
+ * AWS::RDS::DBInstance.Port replacement)
14
+ *
15
+ * Nested-path conservatism: cloudformation-diff reports top-level property
16
+ * names, so a change to a property with a createOnly SUB-pointer
17
+ * (`/properties/Foo/Bar`) is `conditional`, never silently `none`.
18
+ *
19
+ * Fail-closed degradation: when the oracle is unavailable for a type, the
20
+ * original diff verdict stands, annotated `verdictSource: "cdk-spec
21
+ * (unverified)"` — the oracle never fails the deploy.
22
+ *
23
+ * Every Layer-0 / Layer-1 disagreement is logged as stale-spec telemetry.
24
+ */
25
+ import type { CfnRegistrySchemaSummary } from "../../../services/infrastructure/CfnRegistryService.js";
26
+ import type { CfnRegistrySummaryReader } from "../../../services/infrastructure/CfnRegistryService.js";
27
+ import type { ReplacementMode } from "../../../types/deployEvent.js";
28
+ import type { StackTemplatePair } from "./buildDeployPlan.js";
29
+ import type { DeployPlan, DeployPlanResourceChange } from "./types.js";
30
+ type RegistryPathVerdict = "will" | "conditional" | "none";
31
+ /**
32
+ * Advisory interruption notes for in-place updates that still disrupt service.
33
+ * Keyed `<resourceType>#<topLevelProperty>`. Display-only — never gates.
34
+ * Exported for the probe-parity pin: any note teaching "pin it" must name a
35
+ * pair `pinProbeCoverage` covers, or the note teaches a consent the
36
+ * destruction gate would halt as unprobeable.
37
+ */
38
+ export declare const INTERRUPTION_NOTES: Readonly<Record<string, string>>;
39
+ /** Recompute one changed top-level property's verdict from the catalogue. */
40
+ export declare function registryVerdictForProperty(propertyName: string, summary: CfnRegistrySchemaSummary): RegistryPathVerdict;
41
+ /** A Layer-0 vs Layer-1 disagreement — logged as stale-spec telemetry. */
42
+ export interface RegistryVerdictDisagreement {
43
+ stack: string;
44
+ address: string;
45
+ resourceType: string;
46
+ cdkSpecMode: ReplacementMode;
47
+ registryMode: ReplacementMode;
48
+ /** The changed property paths whose verdicts drove the registry mode. */
49
+ paths: string[];
50
+ }
51
+ export interface OverlayResourceChangeResult {
52
+ change: DeployPlanResourceChange;
53
+ disagreement?: RegistryVerdictDisagreement;
54
+ }
55
+ /**
56
+ * Pure per-row recomputation. `summary === undefined` means the oracle was
57
+ * unavailable — the diff verdict stands, annotated unverified.
58
+ */
59
+ export declare function overlayResourceChange(change: DeployPlanResourceChange, summary: CfnRegistrySchemaSummary | undefined, templateProperties: Record<string, unknown> | undefined): OverlayResourceChangeResult;
60
+ /** Per-action tally — shared with the change-set escalation's recount. */
61
+ export declare function recountActions(changes: readonly DeployPlanResourceChange[]): {
62
+ create: number;
63
+ update: number;
64
+ replace: number;
65
+ delete: number;
66
+ read: number;
67
+ "no-op": number;
68
+ };
69
+ export interface ApplyRegistryOverlayParams {
70
+ plan: DeployPlan;
71
+ registry: CfnRegistrySummaryReader;
72
+ /** The diffed template pairs — pinned-name detection reads Properties. */
73
+ stacks: readonly StackTemplatePair[];
74
+ abortSignal?: AbortSignal;
75
+ }
76
+ /**
77
+ * Apply the registry overlay to a computed plan. Never throws and never
78
+ * fails the plan: unavailable summaries degrade per-row to
79
+ * `"cdk-spec (unverified)"`. Summaries are fetched once per distinct resource
80
+ * type (the service memoises per run underneath).
81
+ */
82
+ export declare function applyRegistryOverlay(params: ApplyRegistryOverlayParams): Promise<DeployPlan>;
83
+ export {};
@@ -0,0 +1 @@
1
+ import{maskSensitiveOutput as S}from"@fjall/util";import{logger as m}from"@fjall/util/logger";import{isAborted as g}from"../../../aws/organisations/types.js";import{isDataLoss as O}from"./classify.js";import{findPinnedPhysicalName as v,resourcePropertiesFromTemplate as h}from"./pinnedNames.js";import{renderPlanSummary as R}from"./renderDeployPlan.js";const P="RegistryOverlay",C={"AWS::RDS::DBInstance#Port":"port changes apply in place with a brief interruption (instance reboot + dependent-service re-point window); to keep the live port instead, pin it \u2014 set `port` in your fjall config to the current live value and the change disappears","AWS::RDS::DBCluster#Port":"port changes apply in place with a brief interruption (cluster reboot + dependent-service re-point window); to keep the live port instead, pin it \u2014 set `port` in your fjall config to the current live value and the change disappears"},N="/properties/";function k(e,t){const o=`${N}${e}`;if(t.createOnlyProperties.includes(o))return"will";if(t.conditionalCreateOnlyProperties.includes(o))return"conditional";const i=`${o}/`;return t.createOnlyProperties.some(s=>s.startsWith(i))||t.conditionalCreateOnlyProperties.some(s=>s.startsWith(i))?"conditional":"none"}function I(e){return e.includes("will")?"will":e.includes("conditional")?"conditional":"none"}function b(e,t){const o=t.map(i=>C[`${e.resourceType}#${i}`]).filter(i=>i!==void 0);if(o.length!==0)return o.join("; ")}function T(e){return(e.action==="update"||e.action==="replace")&&!e.retained}function D(e){return T(e)||e.action==="delete"}function M(e,t,o){if(e.action==="delete"&&t!==void 0){const n=v({resourceType:e.resourceType,properties:o,primaryIdentifier:t.primaryIdentifier});return{change:{...e,...n!==void 0?{pinnedPhysicalName:n}:{}}}}if(!T(e))return{change:e};if(t===void 0||e.propertyChanges.length===0)return{change:{...e,verdictSource:"cdk-spec (unverified)"}};const i=e.propertyChanges.map(n=>({path:n.path,verdict:k(n.path,t)})),p=I(i.map(n=>n.verdict)),s=p==="none"?"update":"replace",c=p,y=p!=="none",d=O({action:s,retained:e.retained,resourceType:e.resourceType}),u=e.propertyChanges.map((n,w)=>({...n,forcesReplacement:i[w]?.verdict!=="none"})),r=i.filter(n=>n.verdict==="none").map(n=>n.path),a=p==="none"?b(e,r):void 0,l=v({resourceType:e.resourceType,properties:o,primaryIdentifier:t.primaryIdentifier}),f={...e,action:s,replacementMode:c,destructive:y,dataLoss:d,propertyChanges:u,verdictSource:"registry",...a!==void 0?{interruptionNote:a}:{},...l!==void 0?{pinnedPhysicalName:l}:{}};return e.replacementMode===c?{change:f}:{change:f,disagreement:{stack:e.stack,address:e.address,resourceType:e.resourceType,cdkSpecMode:e.replacementMode,registryMode:c,paths:i.map(n=>n.path)}}}function x(e){const t={create:0,update:0,replace:0,delete:0,read:0,"no-op":0};for(const o of e)t[o.action]+=1;return t}async function B(e){const{plan:t,registry:o,stacks:i,abortSignal:p}=e,s=new Set(t.changes.filter(D).map(r=>r.resourceType));if(s.size===0)return t;const c=new Map;for(const r of s){if(g(p))break;const a=await o.getSchemaSummary(r,p);a.success?c.set(r,a.data):m.warn(P,"Registry oracle unavailable for type",{typeName:r,error:S(a.error.message)})}const y=new Map(i.map(r=>[r.stackName,r])),d=t.changes.map(r=>{const a=y.get(r.stack),l=r.action==="delete"?h(a?.oldTemplate,r.address):h(a?.newTemplate,r.address),{change:f,disagreement:n}=M(r,c.get(r.resourceType),l);return n!==void 0&&m.warn(P,"Registry verdict disagrees with bundled CDK spec",{...n,paths:n.paths.join(",")}),f}),u=x(d);return u["no-op"]=t.counts["no-op"],{...t,changes:d,counts:u,hasDestructiveChanges:d.some(r=>r.destructive),summary:R({counts:u,changes:d})}}export{C as INTERRUPTION_NOTES,B as applyRegistryOverlay,M as overlayResourceChange,x as recountActions,k as registryVerdictForProperty};
@@ -14,5 +14,8 @@ export declare function renderPlanSummary(plan: Pick<DeployPlan, "counts" | "cha
14
14
  /** One line per resource change, grouped by action, e.g.
15
15
  * "± replace AWS::ECS::Service WebAppCompute/Service (data loss)". */
16
16
  export declare function renderPlanLines(plan: Pick<DeployPlan, "changes">): string[];
17
- /** Map a computed plan's changes down to the wire `plan.available` PlanChange[]. */
17
+ /** Map a computed plan's changes down to the wire `plan.available` PlanChange[].
18
+ * `verdictSource` is deliberately NOT threaded here — the wire shape is a
19
+ * cross-package contract; it lands with the Phase 2 destruction-ticket wire
20
+ * contract. */
18
21
  export declare function toWirePlanChanges(plan: Pick<DeployPlan, "changes">): PlanChange[];
@@ -1 +1 @@
1
- const p={create:0,update:1,replace:2,delete:3,read:4,"no-op":5},c={create:"+",update:"~",replace:"\xB1",delete:"-",read:">","no-op":" "};function i(a){const{counts:e}=a,t=[];if(e.create>0&&t.push(`${e.create} to create`),e.update>0&&t.push(`${e.update} to update`),e.replace>0&&t.push(`${e.replace} to replace`),e.delete>0&&t.push(`${e.delete} to destroy`),e.read>0&&t.push(`${e.read} to read`),t.length===0)return"No changes";const s=a.changes.filter(n=>n.destructive).length,o=a.changes.filter(n=>n.dataLoss).length,r=[];s>0&&r.push(`${s} destructive`),o>0&&r.push(`${o} with data loss`);const d=t.join(", ");return r.length>0?`${d} (${r.join(", ")})`:d}function u(a){return a.changes.slice().sort((e,t)=>p[e.action]-p[t.action]||`${e.stack}/${e.address}`.localeCompare(`${t.stack}/${t.address}`)).map(e=>{const t=[];e.replacementMode==="may"&&t.push("may replace"),e.dataLoss&&t.push("data loss"),e.retained&&t.push("retained");const s=t.length>0?` (${t.join(", ")})`:"";return`${c[e.action]} ${e.action.padEnd(7)} ${e.resourceType} ${e.stack}/${e.address}${s}`})}function l(a){return a.changes.map(e=>({address:e.address,resourceType:e.resourceType,action:e.action,stack:e.stack,replacementMode:e.replacementMode,dataLoss:e.dataLoss,...e.propertyChanges.length>0&&{propertyChanges:e.propertyChanges}}))}export{u as renderPlanLines,i as renderPlanSummary,l as toWirePlanChanges};
1
+ const i={create:0,update:1,replace:2,delete:3,read:4,"no-op":5},c={create:"+",update:"~",replace:"\xB1",delete:"-",read:">","no-op":" "};function p(a){const{counts:e}=a,t=[];if(e.create>0&&t.push(`${e.create} to create`),e.update>0&&t.push(`${e.update} to update`),e.replace>0&&t.push(`${e.replace} to replace`),e.delete>0&&t.push(`${e.delete} to destroy`),e.read>0&&t.push(`${e.read} to read`),t.length===0)return"No changes";const s=a.changes.filter(n=>n.destructive).length,o=a.changes.filter(n=>n.dataLoss).length,r=[];s>0&&r.push(`${s} destructive`),o>0&&r.push(`${o} with data loss`);const d=t.join(", ");return r.length>0?`${d} (${r.join(", ")})`:d}function u(a){return a.changes.slice().sort((e,t)=>i[e.action]-i[t.action]||`${e.stack}/${e.address}`.localeCompare(`${t.stack}/${t.address}`)).map(e=>{const t=[];e.replacementMode==="may"&&t.push("may replace"),e.replacementMode==="conditional"&&t.push("conditional replace"),e.dataLoss&&t.push("data loss"),e.retained&&t.push("retained"),(e.verdictSource==="cdk-spec (unverified)"||e.verdictSource==="registry (change-set unverified)")&&t.push("unverified");const s=t.length>0?` (${t.join(", ")})`:"";return`${c[e.action]} ${e.action.padEnd(7)} ${e.resourceType} ${e.stack}/${e.address}${s}`})}function l(a){return a.changes.map(e=>({address:e.address,resourceType:e.resourceType,action:e.action,stack:e.stack,replacementMode:e.replacementMode,dataLoss:e.dataLoss,...e.propertyChanges.length>0&&{propertyChanges:e.propertyChanges}}))}export{u as renderPlanLines,p as renderPlanSummary,l as toWirePlanChanges};
@@ -8,6 +8,20 @@
8
8
  * maps a DeployPlan down to the wire shape.
9
9
  */
10
10
  import type { ChangeCounts, PlanAction, PlanPropertyChange, ReplacementMode } from "../../../types/deployEvent.js";
11
+ /**
12
+ * Which oracle layer produced a row's replacement verdict. Precedence:
13
+ * change-set > registry > cdk-spec. `"change-set"` rows carry the
14
+ * authoritative verdict from the Phase 3 review-only change-set probe
15
+ * (`applyChangeSetEscalation`). `"registry (change-set unverified)"` marks a
16
+ * registry-conditional row the probe was escalated for but could not verify
17
+ * (probe denied, timed out, or aborted) — the conservative registry verdict
18
+ * stands. `"cdk-spec (unverified)"` marks rows the registry oracle could not
19
+ * verify (oracle unavailable, or a replacement verdict with no property paths
20
+ * to recompute from) — the stale bundled CDK spec is never silently treated
21
+ * as authoritative.
22
+ */
23
+ export declare const REPLACEMENT_VERDICT_SOURCES: readonly ["registry", "registry (change-set unverified)", "change-set", "cdk-spec (unverified)"];
24
+ export type ReplacementVerdictSource = (typeof REPLACEMENT_VERDICT_SOURCES)[number];
11
25
  /** A single resource change in a computed deploy plan. */
12
26
  export interface DeployPlanResourceChange {
13
27
  /** The stack the resource belongs to. */
@@ -28,6 +42,23 @@ export interface DeployPlanResourceChange {
28
42
  retained: boolean;
29
43
  /** Per-property change detail — paths only, never values (no secret leak). */
30
44
  propertyChanges: PlanPropertyChange[];
45
+ /**
46
+ * Oracle layer behind the replacement verdict. Absent when no verdict
47
+ * question arises (creates, imports, rows the overlay never evaluated).
48
+ */
49
+ verdictSource?: ReplacementVerdictSource;
50
+ /**
51
+ * Advisory service-interruption note for an in-place update (e.g. an RDS
52
+ * port change reboots the instance). Display-only — never gates.
53
+ */
54
+ interruptionNote?: string;
55
+ /**
56
+ * Literal physical name pinned in the template (via the registry
57
+ * `primaryIdentifier` or the `@fjall/util` fallback map). A pinned name on
58
+ * a replacement row means CloudFormation will REFUSE the update
59
+ * ("cannot update a stack when a custom-named resource requires replacing").
60
+ */
61
+ pinnedPhysicalName?: string;
31
62
  }
32
63
  /** A fully computed deploy plan, ready to render and to gate on. */
33
64
  export interface DeployPlan {
@@ -0,0 +1 @@
1
+ const e=["registry","registry (change-set unverified)","change-set","cdk-spec (unverified)"];export{e as REPLACEMENT_VERDICT_SOURCES};
@@ -1 +1 @@
1
- import{openNextBuilder as u}from"./openNextBuilder.js";import{dockerBuilder as d}from"./dockerBuilder.js";class n{builders;constructor(e){this.builders=[...e].sort((r,t)=>r.priority-t.priority)}resolve(e,r){if(r){const t=this.builders.find(o=>o.name===r);if(!t)return null;const i=t.detect(e);return{builder:t,detection:{...i,detected:!0}}}for(const t of this.builders){const i=t.detect(e);if(i.detected)return{builder:t,detection:i}}return null}static createDefault(){return new n([u,d])}static create(e){return new n(e)}}export{n as FrameworkRegistry};
1
+ import{openNextBuilder as d}from"./openNextBuilder.js";import{staticSiteBuilder as n}from"./staticSiteBuilder.js";import{dockerBuilder as c}from"./dockerBuilder.js";class o{builders;constructor(e){this.builders=[...e].sort((r,t)=>r.priority-t.priority)}resolve(e,r){if(r){const t=this.builders.find(u=>u.name===r);if(!t)return null;const i=t.detect(e);return{builder:t,detection:{...i,detected:!0}}}for(const t of this.builders){const i=t.detect(e);if(i.detected)return{builder:t,detection:i}}return null}static createDefault(){return new o([d,n,c])}static create(e){return new o(e)}}export{o as FrameworkRegistry};
@@ -1,4 +1,5 @@
1
1
  export { dockerBuilder } from "./dockerBuilder.js";
2
2
  export { openNextBuilder } from "./openNextBuilder.js";
3
+ export { staticSiteBuilder } from "./staticSiteBuilder.js";
3
4
  export { FrameworkRegistry } from "./frameworkRegistry.js";
4
5
  export type { ResolvedBuilder } from "./frameworkRegistry.js";
@@ -1 +1 @@
1
- import{dockerBuilder as o}from"./dockerBuilder.js";import{openNextBuilder as m}from"./openNextBuilder.js";import{FrameworkRegistry as x}from"./frameworkRegistry.js";export{x as FrameworkRegistry,o as dockerBuilder,m as openNextBuilder};
1
+ import{dockerBuilder as o}from"./dockerBuilder.js";import{openNextBuilder as i}from"./openNextBuilder.js";import{staticSiteBuilder as p}from"./staticSiteBuilder.js";import{FrameworkRegistry as d}from"./frameworkRegistry.js";export{d as FrameworkRegistry,o as dockerBuilder,i as openNextBuilder,p as staticSiteBuilder};
@@ -1 +1 @@
1
- import{basename as i}from"path";import{success as p}from"@fjall/generator";import{logger as d}from"@fjall/util/logger";import{OPENNEXT_DEPLOY_ORDER as l,OPENNEXT_DESTROY_ORDER as c}from"../../types/operations.js";import{readInfrastructureContent as m}from"../../types/detection/patternDetection.js";import{runOpenNextBuild as f,BUILD_TIMEOUT_MS as y,IMPORT_MAP_TIMEOUT_MS as x}from"../application/openNextBuild.js";function o(r){return{hasNetwork:!0,hasCompute:!0,hasDatabase:r,hasStorage:!0,hasMessaging:!0,hasCdn:!0}}const E={name:"opennext",priority:10,detect(r){const t={detected:!1,pattern:null,reason:"No OpenNext pattern found",resources:o(!1),hasDatabase:!1,hasDockerfile:!1},e=m(r.appPath);if(!e)return{...t,reason:"No infrastructure.ts found"};if(!e.includes("PatternFactory"))return{...t,reason:"No PatternFactory reference in infrastructure.ts"};if(e.includes('type: "payload"'))return{detected:!0,pattern:"payload",reason:'PatternFactory with type: "payload" detected',resources:o(!0),hasDatabase:!0,hasDockerfile:!1};if(e.includes('type: "nextjs"')){const n=e.includes("DatabaseFactory.build(")||e.includes("database:");return{detected:!0,pattern:"nextjs",reason:'PatternFactory with type: "nextjs" detected',resources:o(n),hasDatabase:n,hasDockerfile:!1}}return{...t,reason:"PatternFactory found but no recognised pattern type"}},plan(r,t){const e=t.pattern==="payload"?[{name:"payload-import-map",binary:"npx",args:["payload","generate:importmap"],timeoutMs:x}]:[];return{builderName:"opennext",deployOrder:l,destroyOrder:c,parallelDestroy:!0,preBuildCommands:e,buildCommand:{name:"open-next-build",binary:"open-next",args:["build"],timeoutMs:y},requiresDockerBuild:!1,resources:t.resources}},async build(r,t,e,n){if(n?.skipBuild||n?.infraOnly)return d.debug("openNextBuilder","Build skipped",{skipBuild:n.skipBuild,infraOnly:n.infraOnly}),p(void 0);const s={kind:"application",appName:i(r)||"app",path:r},u=t.preBuildCommands.some(a=>a.name==="payload-import-map")?"payload":"nextjs";return await f(s,u,{onOpenNextBuildStart:()=>e.onBuildStart?.("opennext"),onOpenNextProgress:a=>e.onBuildProgress?.("opennext",a),onOpenNextBuildComplete:()=>e.onBuildComplete?.("opennext"),onOpenNextBuildError:a=>e.onBuildError?.("opennext",a)})}};export{E as openNextBuilder};
1
+ import{basename as d}from"path";import{success as i,parseInfrastructure as l,isOpenNextPatternType as c}from"@fjall/generator";import{logger as f}from"@fjall/util/logger";import{OPENNEXT_DEPLOY_ORDER as m,OPENNEXT_DESTROY_ORDER as y}from"../../types/operations.js";import{readInfrastructureContent as x}from"../../types/detection/patternDetection.js";import{runOpenNextBuild as N,BUILD_TIMEOUT_MS as O,IMPORT_MAP_TIMEOUT_MS as B}from"../application/openNextBuild.js";function p(r){return{hasNetwork:!0,hasCompute:!0,hasDatabase:r,hasStorage:!0,hasMessaging:!0,hasCdn:!0}}const M={name:"opennext",priority:10,detect(r){const t={detected:!1,pattern:null,reason:"No OpenNext pattern found",resources:p(!1),hasDatabase:!1,hasDockerfile:!1},e=x(r.appPath);if(e===null||e==="")return{...t,reason:"No infrastructure.ts found"};const a=l(e).patternResources.find(o=>c(o.type));if(a===void 0)return{...t,reason:'No PatternFactory with type: "payload" or "nextjs" in infrastructure.ts'};if(a.type==="payload")return{detected:!0,pattern:"payload",reason:'PatternFactory with type: "payload" detected',resources:p(!0),hasDatabase:!0,hasDockerfile:!1};if(a.type==="nextjs"){const o=a.config.database!==void 0;return{detected:!0,pattern:"nextjs",reason:'PatternFactory with type: "nextjs" detected',resources:p(o),hasDatabase:o,hasDockerfile:!1}}return{...t,reason:"PatternFactory found but no recognised pattern type"}},plan(r,t){const e=t.pattern==="payload"?[{name:"payload-import-map",binary:"npx",args:["payload","generate:importmap"],timeoutMs:B}]:[];return{builderName:"opennext",deployOrder:m,destroyOrder:y,parallelDestroy:!0,preBuildCommands:e,buildCommand:{name:"open-next-build",binary:"open-next",args:["build"],timeoutMs:O},requiresDockerBuild:!1,resources:t.resources}},async build(r,t,e,n){if(n?.skipBuild||n?.infraOnly)return f.debug("openNextBuilder","Build skipped",{skipBuild:n.skipBuild,infraOnly:n.infraOnly}),i(void 0);const a={kind:"application",appName:d(r)||"app",path:r},u=t.preBuildCommands.some(s=>s.name==="payload-import-map")?"payload":"nextjs";return await N(a,u,{onOpenNextBuildStart:()=>e.onBuildStart?.("opennext"),onOpenNextProgress:s=>e.onBuildProgress?.("opennext",s),onOpenNextBuildComplete:()=>e.onBuildComplete?.("opennext"),onOpenNextBuildError:s=>e.onBuildError?.("opennext",s)},void 0,n?.abortSignal)}};export{M as openNextBuilder};
@@ -0,0 +1,27 @@
1
+ /**
2
+ * StaticSiteBuilder — FrameworkBuilder implementation for static-site apps.
3
+ *
4
+ * Detects a `type: "staticsite"` PatternFactory call in infrastructure.ts,
5
+ * produces a single-stack (Cdn) build plan, and runs the app's own build
6
+ * command to produce the static artefacts that CDK's BucketDeployment then
7
+ * uploads to S3.
8
+ *
9
+ * Priority 50 — strictly between OpenNext (10) and Docker (100). This is
10
+ * load-bearing: without it the Docker catch-all (priority 100) would claim
11
+ * the static site and emit a container deployment plan.
12
+ *
13
+ * Unlike OpenNext, the build command and its working directory are not
14
+ * hardcoded — they are the app's own, read out of infrastructure.ts with the
15
+ * generator's AST parser (`@fjall/generator` is already a dependency, and its
16
+ * barrel already pulls the parser in, so this costs nothing over a regex).
17
+ */
18
+ import type { FrameworkBuilder } from "../../types/frameworkBuilder.js";
19
+ /** Default static-site build timeout: 10 minutes (mirrors the OpenNext budget) */
20
+ export declare const STATIC_BUILD_TIMEOUT_MS = 600000;
21
+ /**
22
+ * Static-site FrameworkBuilder.
23
+ *
24
+ * Priority 50 — checked after OpenNext (more specific) and before Docker
25
+ * (the fallback catch-all).
26
+ */
27
+ export declare const staticSiteBuilder: FrameworkBuilder;
@@ -0,0 +1,2 @@
1
+ import{existsSync as y}from"fs";import{resolve as f}from"path";import{success as m,failure as u,parseInfrastructure as p}from"@fjall/generator";import{filterDangerousEnvVars as b,maskSensitiveOutput as d,parseShellArgs as B}from"@fjall/util";import{logger as D}from"@fjall/util/logger";import{getApplicationDeployOrder as E,getApplicationDestroyOrder as h}from"../../types/operations.js";import{readInfrastructureContent as g}from"../../types/detection/patternDetection.js";import{spawnWithTimeout as w}from"../spawnHelpers.js";const O=6e5,S={hasNetwork:!1,hasCompute:!1,hasDatabase:!1,hasStorage:!1,hasMessaging:!1,hasCdn:!0};function C(o){return p(o).patternResources.some(r=>r.type==="staticsite")}function P(o){const r=g(o);if(r===null||r==="")return;const t=p(r).patternResources.find(n=>n.type==="staticsite");if(t===void 0||t.type!=="staticsite")return;const{source:i,build:s}=t.config,a=s?.command,c=s?.outputDir;if(!(i===void 0||i===""||a===void 0||a===""||c===void 0||c===""))return{source:i,command:a,outputDir:c}}const R={name:"staticsite",priority:50,detect(o){const r={detected:!1,pattern:null,reason:"No static-site pattern found",resources:S,hasDatabase:!1,hasDockerfile:!1},t=g(o.appPath);return t===null||t===""?{...r,reason:"No infrastructure.ts found"}:C(t)?{detected:!0,pattern:"staticsite",reason:'PatternFactory with type: "staticsite" detected',resources:S,hasDatabase:!1,hasDockerfile:!1}:{...r,reason:'No PatternFactory with type: "staticsite"'}},plan(o,r){const t=P(o.appPath);let i=null;if(t!==void 0){const[s,...a]=B(t.command);s!==void 0&&(i={name:"static-site-build",binary:s,args:a,timeoutMs:O,cwd:t.source})}return{builderName:"staticsite",deployOrder:E({resources:r.resources}),destroyOrder:h({resources:r.resources}),parallelDestroy:!1,preBuildCommands:[],buildCommand:i,requiresDockerBuild:!1,artefactDir:t?.outputDir,resources:r.resources}},async build(o,r,t,i){if(i?.skipBuild||i?.infraOnly)return D.debug("staticSiteBuilder","Build skipped",{skipBuild:i.skipBuild,infraOnly:i.infraOnly}),m(void 0);const{buildCommand:s}=r;if(s===null){const e='Static-site build command could not be read from infrastructure.ts. The pattern needs an inline build block \u2014 build: { command: "npm run build", outputDir: "out" } \u2014 with literal strings, not variables or expressions.';return t.onBuildError?.("staticsite",e),u(new Error(e))}const a=f(o,s.cwd??"."),c=b(globalThis.process.env);t.onBuildStart?.("staticsite"),t.onBuildProgress?.("staticsite",d("Starting static-site build..."));const n=await w({command:s.binary,args:[...s.args],cwd:a,env:{...c,FORCE_COLOR:"0"},timeout:s.timeoutMs,...i?.abortSignal!==void 0&&{abortSignal:i.abortSignal},onStdoutData:e=>t.onBuildProgress?.("staticsite",d(e.trim())),onStderrData:e=>t.onBuildProgress?.("staticsite",d(e.trim()))});if(n.type==="timeout"){const e=`Static-site build timed out after ${s.timeoutMs/1e3} seconds`;return t.onBuildError?.("staticsite",e),u(new Error(e))}if(n.type==="aborted"){const e="Static-site build cancelled";return t.onBuildError?.("staticsite",e),u(new Error(e))}if(n.type==="enoent"){const e=`Static-site build command not found: ${s.binary}. Ensure it is installed and in PATH.`;return t.onBuildError?.("staticsite",e),u(new Error(e))}if(n.type==="spawn_error"){const e=d(`Failed to spawn static-site build: ${n.error.message}`);return t.onBuildError?.("staticsite",e),u(new Error(e))}if(n.code!==0){const e=d(n.stderr||n.stdout);return t.onBuildError?.("staticsite",`Static-site build failed (exit code ${n.code})`),u(new Error(`Static-site build failed (exit code ${n.code}):
2
+ ${e}`))}if(r.artefactDir!==void 0){const e=f(a,r.artefactDir);if(!y(e)){const l=`Static-site build completed but output directory '${r.artefactDir}' was not found`;return t.onBuildError?.("staticsite",l),u(new Error(l))}}return t.onBuildComplete?.("staticsite"),t.onBuildProgress?.("staticsite",d("Static-site build completed successfully")),m(void 0)}};export{O as STATIC_BUILD_TIMEOUT_MS,R as staticSiteBuilder};
@@ -3,7 +3,7 @@ import type { TrailLifecycleState } from "@fjall/util/config";
3
3
  import type { ResourceEvent } from "@fjall/util/aws";
4
4
  import type { OrgConfig } from "../types/config/orgConfig.js";
5
5
  import type { DeployIdentity } from "../types/credentials.js";
6
- import type { AwsProviderCredentials } from "../aws/AwsProvider.js";
6
+ import type { AwsProvider, AwsProviderCredentials } from "../aws/AwsProvider.js";
7
7
  import { SimpleAwsProvider } from "../aws/SimpleAwsProvider.js";
8
8
  import type { DeploymentContext } from "../types/deployment/DeploymentTypes.js";
9
9
  import type { DeployCallbacks } from "../types/callbacks.js";
@@ -76,6 +76,42 @@ export declare function buildParamsContext(params: {
76
76
  * provider the Account stack legitimately owns.
77
77
  */
78
78
  export declare function resolveAccountBootstrapSkipOidc(deployType: "organisation" | "platform" | "account", orgConfig: OrgConfig | undefined, explicitSkipOidc: boolean | undefined, targetIsOrganisationTier?: boolean): boolean | undefined;
79
+ /** Synth-time context flags the OIDC gate mirror reads — see {@link synthCarriedOidcConnector}. */
80
+ export interface SynthOidcContextFlags {
81
+ fjallOrgId?: string;
82
+ fjallOidcConfigured?: boolean;
83
+ fjallAccountGlobalsConfigured?: boolean;
84
+ }
85
+ /**
86
+ * Whether the synthesised Account/Platform stack carried the OidcConnector —
87
+ * the deploy-core mirror of the construct gate in
88
+ * `components/infrastructure/lib/patterns/aws/account.ts` (`receivesDeployRole()
89
+ * && fjallOrgId && !oidcAlreadyConfigured && !accountGlobalsConfigured`).
90
+ * `receivesDeployRole` is structural here: only platform/account deploys reach
91
+ * the post-deploy OIDC reconcile, and both patterns receive the deploy role
92
+ * (the Organisation pattern, which does not, only synthesises under
93
+ * `deployType: "organisation"`). An empty-string `fjallOrgId` reads as absent,
94
+ * matching both coupled sites: the construct gate tests truthiness and
95
+ * CdkArgumentBuilder drops the `-c fjallOrgId` argument entirely for `""`.
96
+ *
97
+ * Gates {@link reconcileOidcProviderIfCarried}: the reconcile enforces
98
+ * provider presence only where the current template claims ownership. A stack
99
+ * that deliberately skipped the connector — solo/standalone-management
100
+ * (Quick-Create owns its provider; restore is RECONNECT, never redeploy),
101
+ * non-home region, or no org identity — must never have a provider created
102
+ * for it on the deploy path.
103
+ */
104
+ export declare function synthCarriedOidcConnector(context: SynthOidcContextFlags): boolean;
105
+ /**
106
+ * Post-CFN OIDC provider reconcile, gated on {@link synthCarriedOidcConnector}.
107
+ * A pre-2.20.0 → current template migration deletes the account-global OIDC
108
+ * provider during CloudFormation's cleanup phase (after the replacement
109
+ * adopt-or-create resource has already run), so callers MUST invoke this only
110
+ * after their deploy — or unchanged-skip probe — returns. Composing the gate
111
+ * with the reconcile here keeps any future deploy path from invoking the
112
+ * reconcile ungated.
113
+ */
114
+ export declare function reconcileOidcProviderIfCarried(context: SynthOidcContextFlags, provider: Pick<AwsProvider, "getClient">, callbacks: DeployCallbacks, abortSignal?: AbortSignal): Promise<void>;
79
115
  /** Forward onOutput callback — reduces lambda repetition across orchestration files. */
80
116
  export declare function forwardOutput(callbacks: DeployCallbacks): (chunk: string) => void;
81
117
  /** Forward onResourceProgress callback — reduces lambda repetition across orchestration files. */
@@ -1 +1 @@
1
- import{success as f,failure as i}from"@fjall/generator";import{getErrorMessage as m,maskSensitiveOutput as d,sleep as y}from"@fjall/util";import{logger as C}from"@fjall/util/logger";import{hasOrganisationTierAccount as R}from"../aws/organisations/accountGlobals.js";import{SimpleAwsProvider as S}from"../aws/SimpleAwsProvider.js";const w={account:"active",draining:"draining",org:"removed"};function T(e,r){return e!==void 0&&e!==""&&r!==void 0&&r!==""&&e!==r}function D(e){const r=T(e.region,e.primaryRegion);return{...e.orgConfig!==void 0?{orgConfig:JSON.stringify(e.orgConfig)}:{},...e.identity!==void 0?{fjallOrgId:e.identity.fjallOrgId}:{},...e.skipOidc?{fjallOidcConfigured:!0}:{},...r?{fjallAccountGlobalsConfigured:!0}:{},...e.trailLifecycle!==void 0?{fjallAccountTrailState:w[e.trailLifecycle]}:{}}}function P(e,r,t,n){if(t!==void 0)return t;if(e==="account")return R(r?.providerAccounts)?n===!0:!0}function _(e){return r=>e.onOutput?.(r)}function B(e){return r=>e.onResourceProgress?.(r)}function j(e){if(!e.success||e.data.length===0)return;const r={};for(const t of e.data)t.OutputKey&&t.OutputValue!==void 0&&(r[t.OutputKey]=t.OutputValue);return Object.keys(r).length>0?r:void 0}const E="OrganizationAccountAccessRole",l=5,$=5e3,b=3e4;function h(e){return`arn:aws:iam::${e}:role/${E}`}function K(e,r){return r===void 0?y(e):r.aborted?Promise.resolve():new Promise(t=>{const n=()=>{t()};r.addEventListener("abort",n,{once:!0}),y(e).then(()=>{r.removeEventListener("abort",n),t()})})}async function F(e,r,t,n,o){if(!e.assumeRole)return i(new Error("AwsProvider does not support assumeRole"));const u=h(r),g=e.assumeRole.bind(e);let s;for(let c=0;c<=l;c++)try{s=await g(u,n);break}catch(a){const p=a instanceof Error?a.name:void 0;if(p==="AccessDenied"||p==="AccessDeniedException")return i(new Error(`Access denied assuming ${E} in account ${r}. The role may not exist or may not trust the management account.`));if(c<l){const A=Math.min($*2**c,b);if(C.debug("assumeCascadeRole",`Attempt ${c+1} failed for account ${r}, retrying in ${Math.round(A/1e3)}s`,{error:d(m(a))}),await K(A,o),o?.aborted)return i(new Error(`Aborted while retrying assume-role for account ${r}`));continue}return i(new Error(`Failed to assume role in account ${r} after ${l+1} attempts: ${d(m(a))}`))}if(!s)return i(new Error(`Failed to assume role in account ${r}`));const O=new S({accessKeyId:s.accessKeyId,secretAccessKey:s.secretAccessKey,sessionToken:s.sessionToken,region:t,accountId:r});return f({provider:O,credentials:{accessKeyId:s.accessKeyId,secretAccessKey:s.secretAccessKey,sessionToken:s.sessionToken}})}async function Y(e,r,t,n){const o=await e.cdkService.runCdkSynth(r,u=>t.onCdkOutput?.(u,"synth"));if(!o.success){const u=new Error(d(`${n}: ${o.error}`));return t.onError?.(u),i(u)}return f(void 0)}async function I(e,r,t){t.onCDKBootstrap?.("bootstrapping");const n=await e.cdkService.runCdkBootstrap(r,_(t));if(!n.success){t.onCDKBootstrap?.("failed");const o=new Error(d(`Bootstrap failed: ${n.error}`));return t.onError?.(o),i(o)}return t.onCDKBootstrap?.("complete"),f(void 0)}export{F as assumeCascadeRole,I as bootstrapOrFail,h as buildCascadeRoleArn,D as buildParamsContext,j as collectStackOutputs,_ as forwardOutput,B as forwardResourceProgress,P as resolveAccountBootstrapSkipOidc,Y as synthOrFail,T as targetsNonPrimaryRegion};
1
+ import{success as f,failure as s}from"@fjall/generator";import{getErrorMessage as m,maskSensitiveOutput as a,sleep as y}from"@fjall/util";import{logger as O}from"@fjall/util/logger";import{IAMClient as R}from"@aws-sdk/client-iam";import{hasOrganisationTierAccount as S}from"../aws/organisations/accountGlobals.js";import{reconcileOidcProviderAfterDeploy as w}from"../aws/organisations/oidcProvider.js";import{SimpleAwsProvider as b}from"../aws/SimpleAwsProvider.js";const h={account:"active",draining:"draining",org:"removed"};function T(e,r){return e!==void 0&&e!==""&&r!==void 0&&r!==""&&e!==r}function F(e){const r=T(e.region,e.primaryRegion);return{...e.orgConfig!==void 0?{orgConfig:JSON.stringify(e.orgConfig)}:{},...e.identity!==void 0?{fjallOrgId:e.identity.fjallOrgId}:{},...e.skipOidc?{fjallOidcConfigured:!0}:{},...r?{fjallAccountGlobalsConfigured:!0}:{},...e.trailLifecycle!==void 0?{fjallAccountTrailState:h[e.trailLifecycle]}:{}}}function Y(e,r,t,n){if(t!==void 0)return t;if(e==="account")return S(r?.providerAccounts)?n===!0:!0}function _(e){return e.fjallOrgId!==void 0&&e.fjallOrgId!==""&&e.fjallOidcConfigured!==!0&&e.fjallAccountGlobalsConfigured!==!0}async function G(e,r,t,n){_(e)&&await w(r.getClient(R),t,n)}function $(e){return r=>e.onOutput?.(r)}function N(e){return r=>e.onResourceProgress?.(r)}function U(e){if(!e.success||e.data.length===0)return;const r={};for(const t of e.data)t.OutputKey&&t.OutputValue!==void 0&&(r[t.OutputKey]=t.OutputValue);return Object.keys(r).length>0?r:void 0}const g="OrganizationAccountAccessRole",l=5,v=5e3,K=3e4;function x(e){return`arn:aws:iam::${e}:role/${g}`}function M(e,r){return r===void 0?y(e):r.aborted?Promise.resolve():new Promise(t=>{const n=()=>{t()};r.addEventListener("abort",n,{once:!0}),y(e).then(()=>{r.removeEventListener("abort",n),t()})})}async function V(e,r,t,n,o){if(!e.assumeRole)return s(new Error("AwsProvider does not support assumeRole"));const u=x(r),E=e.assumeRole.bind(e);let i;for(let c=0;c<=l;c++)try{i=await E(u,n);break}catch(d){const p=d instanceof Error?d.name:void 0;if(p==="AccessDenied"||p==="AccessDeniedException")return s(new Error(`Access denied assuming ${g} in account ${r}. The role may not exist or may not trust the management account.`));if(c<l){const A=Math.min(v*2**c,K);if(O.debug("assumeCascadeRole",`Attempt ${c+1} failed for account ${r}, retrying in ${Math.round(A/1e3)}s`,{error:a(m(d))}),await M(A,o),o?.aborted)return s(new Error(`Aborted while retrying assume-role for account ${r}`));continue}return s(new Error(`Failed to assume role in account ${r} after ${l+1} attempts: ${a(m(d))}`))}if(!i)return s(new Error(`Failed to assume role in account ${r}`));const C=new b({accessKeyId:i.accessKeyId,secretAccessKey:i.secretAccessKey,sessionToken:i.sessionToken,region:t,accountId:r});return f({provider:C,credentials:{accessKeyId:i.accessKeyId,secretAccessKey:i.secretAccessKey,sessionToken:i.sessionToken}})}async function z(e,r,t,n){const o=await e.cdkService.runCdkSynth(r,u=>t.onCdkOutput?.(u,"synth"));if(!o.success){const u=new Error(a(`${n}: ${o.error}`));return t.onError?.(u),s(u)}return f(void 0)}async function J(e,r,t){t.onCDKBootstrap?.("bootstrapping");const n=await e.cdkService.runCdkBootstrap(r,$(t));if(!n.success){t.onCDKBootstrap?.("failed");const o=new Error(a(`Bootstrap failed: ${n.error}`));return t.onError?.(o),s(o)}return t.onCDKBootstrap?.("complete"),f(void 0)}export{V as assumeCascadeRole,J as bootstrapOrFail,x as buildCascadeRoleArn,F as buildParamsContext,U as collectStackOutputs,$ as forwardOutput,N as forwardResourceProgress,G as reconcileOidcProviderIfCarried,Y as resolveAccountBootstrapSkipOidc,_ as synthCarriedOidcConnector,z as synthOrFail,T as targetsNonPrimaryRegion};
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Class 1 drift classification over a stack's last CloudFormation operation
3
+ * (drift-remediation plan § Phase 5 item 1). Fetches the failure events of
4
+ * the most recent operation via `DescribeStackEvents` (newest-first), maps
5
+ * them onto the shared NotFound anchors (`@fjall/util/aws` `driftSuspects`,
6
+ * derived from the Phase 0 BC fixture `__tests__/fixtures/bc-stack-events.json`)
7
+ * and returns out-of-band-deletion SUSPECTS — never verdicts; `driftProbe.ts`
8
+ * confirms.
9
+ *
10
+ * Suspect `statusReason`s are masked at construction (a CloudFormation status
11
+ * reason can embed anything a failed handler echoed), so journal writes and
12
+ * CLI sinks may surface them directly.
13
+ */
14
+ import { type Result } from "@fjall/generator";
15
+ import { type ResourceEvent } from "@fjall/util/aws";
16
+ import type { AwsProvider } from "../../aws/AwsProvider.js";
17
+ import type { DriftSuspect } from "../../types/drift.js";
18
+ /**
19
+ * Fetch the events of the stack's most recent operation, newest-first. Pages
20
+ * `DescribeStackEvents` until the operation's opening event (inclusive — the
21
+ * shared `isOperationOpeningEvent` boundary, which a CancelUpdateStack
22
+ * rollback's "User Initiated" stamp does NOT satisfy) or the page budget is
23
+ * exhausted.
24
+ */
25
+ export declare function fetchLastOperationEvents(aws: AwsProvider, stackName: string, abortSignal?: AbortSignal): Promise<Result<ResourceEvent[], Error>>;
26
+ /**
27
+ * Classify a stack's last-operation events into drift suspects. Pure —
28
+ * callers fetch events via {@link fetchLastOperationEvents} or hand over a
29
+ * monitor's history.
30
+ */
31
+ export declare function classifyDriftFailure(stackName: string, events: readonly ResourceEvent[]): DriftSuspect[];
@@ -0,0 +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};
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The drift-detection pipeline for one stack: last-operation event scan →
3
+ * suspect classification → journal merge → probe → journal upkeep. Shared by
4
+ * `fjall drift detect` and the [G2] deploy pre-flight so both surfaces report
5
+ * identical findings for identical state.
6
+ *
7
+ * Journal upkeep: confirmed or unconfirmed findings (the wedge persists or
8
+ * could not be ruled out) re-record the journal so the next deploy re-probes;
9
+ * all-present or stack-missing findings clear it.
10
+ */
11
+ import { type Result } from "@fjall/generator";
12
+ import type { AwsProvider } from "../../aws/AwsProvider.js";
13
+ import type { DriftFinding, DriftSuspect } from "../../types/drift.js";
14
+ import type { DriftProbeRunner } from "./driftProbe.js";
15
+ import { type DriftJournalOptions } from "./driftSuspectJournal.js";
16
+ export interface DetectStackDriftDeps {
17
+ aws: AwsProvider;
18
+ probe: DriftProbeRunner;
19
+ journal?: DriftJournalOptions;
20
+ /** Injectable clock for journal timestamps. */
21
+ now?: () => Date;
22
+ }
23
+ export interface DetectStackDriftParams {
24
+ stackName: string;
25
+ abortSignal?: AbortSignal;
26
+ }
27
+ export interface StackDriftReport {
28
+ stackName: string;
29
+ suspects: DriftSuspect[];
30
+ findings: DriftFinding[];
31
+ /** How many suspects came from the live event scan vs the journal. */
32
+ suspectSources: {
33
+ events: number;
34
+ journal: number;
35
+ };
36
+ }
37
+ export declare function detectStackDrift(deps: DetectStackDriftDeps, params: DetectStackDriftParams): Promise<Result<StackDriftReport, Error>>;
@@ -0,0 +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};