@fjall/deploy-core 2.11.1 → 2.13.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 (57) hide show
  1. package/dist/.minified +1 -1
  2. package/dist/src/aws/cloudtrail/orgTrailDelivery.d.ts +44 -0
  3. package/dist/src/aws/cloudtrail/orgTrailDelivery.js +1 -0
  4. package/dist/src/aws/organisations/accounts.d.ts +3 -1
  5. package/dist/src/aws/organisations/accounts.js +1 -1
  6. package/dist/src/aws/organisations/backup.d.ts +3 -2
  7. package/dist/src/aws/organisations/backup.js +2 -2
  8. package/dist/src/aws/organisations/organisationalUnits.d.ts +1 -1
  9. package/dist/src/aws/organisations/organisationalUnits.js +1 -1
  10. package/dist/src/aws/organisations/policies.js +1 -1
  11. package/dist/src/aws/organisations/serviceAccess.js +1 -1
  12. package/dist/src/aws/organisations/types.d.ts +6 -0
  13. package/dist/src/events/index.d.ts +2 -0
  14. package/dist/src/events/index.js +1 -1
  15. package/dist/src/index.d.ts +6 -2
  16. package/dist/src/index.js +1 -1
  17. package/dist/src/orchestration/accountsConfig.d.ts +11 -0
  18. package/dist/src/orchestration/accountsConfig.js +1 -1
  19. package/dist/src/orchestration/cascadeDestroyHelpers.d.ts +1 -1
  20. package/dist/src/orchestration/cascadeDestroyHelpers.js +1 -1
  21. package/dist/src/orchestration/cascadeHelpers.d.ts +16 -0
  22. package/dist/src/orchestration/cascadeHelpers.js +1 -1
  23. package/dist/src/orchestration/contextHelpers.d.ts +3 -0
  24. package/dist/src/orchestration/contextHelpers.js +1 -1
  25. package/dist/src/orchestration/index.d.ts +5 -1
  26. package/dist/src/orchestration/index.js +1 -1
  27. package/dist/src/orchestration/organisationDeploy.js +5 -5
  28. package/dist/src/orchestration/organisationSetup.d.ts +5 -1
  29. package/dist/src/orchestration/organisationSetup.js +1 -1
  30. package/dist/src/orchestration/reconcileProviderAccounts.js +1 -1
  31. package/dist/src/orchestration/stackCleanup.d.ts +7 -0
  32. package/dist/src/orchestration/stackCleanup.js +1 -1
  33. package/dist/src/orchestration/trailMigration/memberTrailCleanup.d.ts +43 -0
  34. package/dist/src/orchestration/trailMigration/memberTrailCleanup.js +1 -0
  35. package/dist/src/orchestration/trailMigration/trailMigration.d.ts +64 -0
  36. package/dist/src/orchestration/trailMigration/trailMigration.js +1 -0
  37. package/dist/src/services/infrastructure/CdkArgumentBuilder.js +1 -1
  38. package/dist/src/services/infrastructure/CdkServiceTypes.d.ts +1 -0
  39. package/dist/src/services/infrastructure/cdkServiceHelpers.js +1 -1
  40. package/dist/src/services/supporting/CdkContextBuilder.d.ts +1 -0
  41. package/dist/src/services/supporting/CdkContextBuilder.js +1 -1
  42. package/dist/src/types/callbackKeys.d.ts +1 -1
  43. package/dist/src/types/callbackKeys.js +1 -1
  44. package/dist/src/types/callbacks.d.ts +15 -1
  45. package/dist/src/types/deployment/DeploymentTypes.d.ts +1 -0
  46. package/dist/src/types/deploymentEventSchema.d.ts +19 -3
  47. package/dist/src/types/deploymentEventSchema.js +1 -1
  48. package/dist/src/types/events.d.ts +12 -0
  49. package/dist/src/types/events.js +1 -0
  50. package/dist/src/types/index.d.ts +2 -1
  51. package/dist/src/types/index.js +1 -1
  52. package/dist/src/types/params.d.ts +6 -0
  53. package/dist/src/util/index.d.ts +1 -0
  54. package/dist/src/util/index.js +1 -1
  55. package/dist/src/util/sleepAbortable.d.ts +8 -0
  56. package/dist/src/util/sleepAbortable.js +1 -0
  57. package/package.json +6 -4
@@ -40,5 +40,9 @@ export interface OrgSetupResult {
40
40
  * Runs up to 13 phases sequentially. Non-fatal phase failures are recorded
41
41
  * and execution continues. The only fatal failure is phase 1
42
42
  * (create-organisation) since all subsequent phases depend on the org ID.
43
+ *
44
+ * @param abortSignal Optional shutdown signal — short-circuits the long
45
+ * account-creation and policy-enable polls so a SIGTERM-driven worker
46
+ * shutdown is not stalled by in-flight sleeps
43
47
  */
44
- export declare function runOrganisationSetup(awsProvider: AwsProvider, config: OrgSetupConfig, callbacks?: OrgSetupCallbacks): Promise<Result<OrgSetupResult>>;
48
+ export declare function runOrganisationSetup(awsProvider: AwsProvider, config: OrgSetupConfig, callbacks?: OrgSetupCallbacks, abortSignal?: AbortSignal): Promise<Result<OrgSetupResult>>;
@@ -1 +1 @@
1
- import{success as C,failure as l}from"@fjall/generator";import{OrganizationsClient as L}from"@aws-sdk/client-organizations";import{RAMClient as D}from"@aws-sdk/client-ram";import{CloudFormationClient as F}from"@aws-sdk/client-cloudformation";import{EC2Client as j}from"@aws-sdk/client-ec2";import{BackupClient as B}from"@aws-sdk/client-backup";import{CostExplorerClient as W}from"@aws-sdk/client-cost-explorer";import{SSOAdminClient as z}from"@aws-sdk/client-sso-admin";import{ensureOrganisationExists as G}from"../aws/organisations/organisation.js";import{enablePolicyTypes as K}from"../aws/organisations/policies.js";import{enableServiceAccess as q}from"../aws/organisations/serviceAccess.js";import{enableRamSharing as H}from"../aws/organisations/ram.js";import{activateTrustedAccess as J}from"../aws/organisations/trustedAccess.js";import{enableIpamDelegatedAdmin as Q}from"../aws/organisations/ipam.js";import{updateBackupGlobalSettings as V}from"../aws/organisations/backup.js";import{listAccounts as E,createAccount as X}from"../aws/organisations/accounts.js";import{ensureOrganisationalUnitsExist as Y,placeAccountsInOUs as Z,buildAccountToOUMap as _}from"../aws/organisations/organisationalUnits.js";import{activateCostAllocationTags as $}from"../aws/organisations/costAllocation.js";import{checkIdentityCentreStatus as b}from"../aws/organisations/identityCentre.js";import{registerSecurityDelegates as k}from"../aws/organisations/delegatedAdmin.js";import{isOULeaf as ee}from"../aws/organisations/types.js";async function Ee(r,o,e){const n=[],i=[],t=[],s=[];let u;const p=r.getClient(L),O=r.getClient(D),U=r.getClient(F),R=r.getClient(j),N=r.getClient(B),T=r.getClient(W),x=r.getClient(z);e?.onPhaseStart?.("create-organisation"),e?.onProgress?.("Ensuring AWS Organisation exists");const g=await G(p);if(!g.success)return e?.onError?.("create-organisation",g.error),e?.onPhaseComplete?.("create-organisation","error"),l(g.error);const{orgId:M,rootId:P}=g.data;if(e?.onPhaseComplete?.("create-organisation","completed"),n.push("create-organisation"),await c("enable-policies",()=>(e?.onProgress?.("Enabling organisation policy types"),K(p,P)),n,t,e),await c("enable-service-access",()=>(e?.onProgress?.("Enabling AWS service access"),q(p)),n,t,e),await c("enable-ram-sharing",()=>(e?.onProgress?.("Enabling RAM sharing"),H(O)),n,t,e),await c("activate-trusted-access",()=>(e?.onProgress?.("Activating CloudFormation trusted access"),J(U)),n,t,e),o.platformAccountId){const a=o.platformAccountId;await c("enable-ipam",()=>(e?.onProgress?.("Enabling IPAM delegated administrator"),Q(R,a)),n,t,e)}await c("configure-backup",()=>(e?.onProgress?.("Updating backup global settings"),V(N)),n,t,e),e?.onPhaseStart?.("create-accounts"),e?.onProgress?.("Checking for missing accounts");const d=await te(p,o.accounts,s);let y=[];d.success?(y=d.data,n.push("create-accounts"),e?.onPhaseComplete?.("create-accounts","completed")):(t.push({phase:"create-accounts",error:d.error.message}),e?.onError?.("create-accounts",d.error),e?.onPhaseComplete?.("create-accounts","error"));let h={};e?.onPhaseStart?.("create-organisational-units"),e?.onProgress?.("Ensuring organisational units exist");const f=await Y(p,P,o.organisationalUnits);f.success?(h=f.data,n.push("create-organisational-units"),e?.onPhaseComplete?.("create-organisational-units","completed")):(t.push({phase:"create-organisational-units",error:f.error.message}),e?.onError?.("create-organisational-units",f.error),e?.onPhaseComplete?.("create-organisational-units","error"));const m=Array.isArray(o.organisationalUnits)?void 0:o.organisationalUnits,A=o.accountPlacements??[],S=A.length===0&&m!==void 0?ne(m,y):A;if(Object.keys(h).length===0)i.push("place-accounts"),e?.onPhaseStart?.("place-accounts"),e?.onPhaseComplete?.("place-accounts","skipped");else if(o.accountPlacements===void 0&&m===void 0){const a=new Error("Account placements not provided despite OUs being created. Caller must populate accountPlacements (flat-list mode cannot derive placements internally).");t.push({phase:"place-accounts",error:a.message}),e?.onPhaseStart?.("place-accounts"),e?.onError?.("place-accounts",a),e?.onPhaseComplete?.("place-accounts","error")}else if(S.length===0)i.push("place-accounts"),e?.onPhaseStart?.("place-accounts"),e?.onPhaseComplete?.("place-accounts","skipped");else{const a=m?_(m,h):void 0;await c("place-accounts",()=>(e?.onProgress?.("Placing accounts in organisational units"),Z(p,h,S,a)),n,t,e)}const w=o.costAllocationTags??[];if(w.length>0?await c("activate-cost-tags",()=>(e?.onProgress?.("Activating cost allocation tags"),$(T,w.map(a=>({TagKey:a})))),n,t,e):(i.push("activate-cost-tags"),e?.onPhaseStart?.("activate-cost-tags"),e?.onPhaseComplete?.("activate-cost-tags","skipped")),o.skipIdentityCentre)i.push("check-identity-centre"),e?.onPhaseStart?.("check-identity-centre"),e?.onPhaseComplete?.("check-identity-centre","skipped");else{e?.onPhaseStart?.("check-identity-centre"),e?.onProgress?.("Checking Identity Centre status");const a=await b(x);a.success?(u=a.data.enabled?"enabled":"not-enabled",n.push("check-identity-centre"),e?.onPhaseComplete?.("check-identity-centre","completed")):(t.push({phase:"check-identity-centre",error:a.error.message}),e?.onError?.("check-identity-centre",a.error),e?.onPhaseComplete?.("check-identity-centre","error"))}const I=o.securityDelegateAccountId;return I?await c("register-security-delegates",()=>(e?.onProgress?.("Registering security service delegated administrators"),k(p,I)),n,t,e):(i.push("register-security-delegates"),e?.onPhaseStart?.("register-security-delegates"),e?.onPhaseComplete?.("register-security-delegates","skipped")),C({organisationId:M,createdAccounts:s,identityCentreStatus:u,phasesCompleted:n,phasesSkipped:i,errors:t})}async function c(r,o,e,n,i){i?.onPhaseStart?.(r);const t=await o();t.success?(e.push(r),i?.onPhaseComplete?.(r,"completed")):(n.push({phase:r,error:t.error.message}),i?.onError?.(r,t.error),i?.onPhaseComplete?.(r,"error"))}async function te(r,o,e){const n=await E(r);if(!n.success)return l(n.error);const i=new Set(n.data.map(s=>s.Name?.toLowerCase()).filter(s=>s!==void 0));for(const s of o){if(i.has(s.name.toLowerCase()))continue;const u=await X(r,s.name,s.email);if(!u.success)return l(u.error);e.push({name:u.data.accountName,accountId:u.data.accountId})}if(e.length===0)return C(n.data);const t=await E(r);return t.success?C(t.data):l(t.error)}function v(r){const o=[];for(const e of Object.values(r))ee(e)?o.push(...e):o.push(...v(e));return o}function ne(r,o){const e=v(r),n=new Map;for(const t of o){const s=t.Name?.toLowerCase();s&&t.Id&&n.set(s,t)}const i=[];for(const t of e){const s=n.get(t.toLowerCase());s?.Id&&s.Name&&i.push({id:s.Id,name:s.Name,environment:t})}return i}export{Ee as runOrganisationSetup};
1
+ import{success as A,failure as h}from"@fjall/generator";import{OrganizationsClient as K}from"@aws-sdk/client-organizations";import{RAMClient as W}from"@aws-sdk/client-ram";import{CloudFormationClient as B}from"@aws-sdk/client-cloudformation";import{EC2Client as $}from"@aws-sdk/client-ec2";import{BackupClient as z}from"@aws-sdk/client-backup";import{CostExplorerClient as G}from"@aws-sdk/client-cost-explorer";import{SSOAdminClient as q}from"@aws-sdk/client-sso-admin";import{ensureOrganisationExists as H}from"../aws/organisations/organisation.js";import{enablePolicyTypes as J}from"../aws/organisations/policies.js";import{enableServiceAccess as Q}from"../aws/organisations/serviceAccess.js";import{enableRamSharing as V}from"../aws/organisations/ram.js";import{activateTrustedAccess as X}from"../aws/organisations/trustedAccess.js";import{enableIpamDelegatedAdmin as Y}from"../aws/organisations/ipam.js";import{updateBackupGlobalSettings as Z}from"../aws/organisations/backup.js";import{listAccounts as U,createAccount as _}from"../aws/organisations/accounts.js";import{ensureOrganisationalUnitsExist as b,placeAccountsInOUs as k,buildAccountToOUMap as ee}from"../aws/organisations/organisationalUnits.js";import{activateCostAllocationTags as te}from"../aws/organisations/costAllocation.js";import{checkIdentityCentreStatus as ne}from"../aws/organisations/identityCentre.js";import{registerSecurityDelegates as oe}from"../aws/organisations/delegatedAdmin.js";import{isOULeaf as re}from"../aws/organisations/types.js";async function Ne(t,n,e,a){const o=[],i=[],r=[],m=[];let s;const c=t.getClient(K),R=t.getClient(W),x=t.getClient(B),T=t.getClient($),M=t.getClient(z),L=t.getClient(G),F=t.getClient(q);e?.onPhaseStart?.("create-organisation"),e?.onProgress?.("Ensuring AWS Organisation exists");const f=await H(c);if(!f.success)return e?.onError?.("create-organisation",f.error),e?.onPhaseComplete?.("create-organisation","error"),h(f.error);const{orgId:j,rootId:w,managementAccountId:D}=f.data;if(e?.onPhaseComplete?.("create-organisation","completed"),o.push("create-organisation"),await p("enable-policies",()=>(e?.onProgress?.("Enabling organisation policy types"),J(c,w,{abortSignal:a})),o,r,e),await p("enable-service-access",()=>(e?.onProgress?.("Enabling AWS service access"),Q(c)),o,r,e),await p("enable-ram-sharing",()=>(e?.onProgress?.("Enabling RAM sharing"),V(R)),o,r,e),await p("activate-trusted-access",()=>(e?.onProgress?.("Activating CloudFormation trusted access"),X(x)),o,r,e),n.platformAccountId){const u=n.platformAccountId;await p("enable-ipam",()=>(e?.onProgress?.("Enabling IPAM delegated administrator"),Y(T,u)),o,r,e)}await p("configure-backup",()=>(e?.onProgress?.("Updating backup global settings"),Z(M)),o,r,e),e?.onPhaseStart?.("create-accounts"),e?.onProgress?.("Checking for missing accounts");const P=await se(c,n.accounts,m,e,a);let I=[];P.success?(I=P.data,o.push("create-accounts"),e?.onPhaseComplete?.("create-accounts","completed")):C("create-accounts",P.error,r,e);let l={};e?.onPhaseStart?.("create-organisational-units"),e?.onProgress?.("Ensuring organisational units exist");const y=await b(c,w,n.organisationalUnits);y.success?(l=y.data,o.push("create-organisational-units"),e?.onPhaseComplete?.("create-organisational-units","completed")):C("create-organisational-units",y.error,r,e);const d=Array.isArray(n.organisationalUnits)?void 0:n.organisationalUnits,S=n.accountPlacements??[],E=S.length===0&&d!==void 0?ie(d,I,D):S;if(Object.keys(l).length===0)g("place-accounts",i,e);else if(n.accountPlacements===void 0&&d===void 0){const u=new Error("Account placements not provided despite OUs being created. Caller must populate accountPlacements (flat-list mode cannot derive placements internally).");r.push({phase:"place-accounts",error:u.message}),e?.onPhaseStart?.("place-accounts"),e?.onError?.("place-accounts",u),e?.onPhaseComplete?.("place-accounts","error")}else if(E.length===0)g("place-accounts",i,e);else{const u=d?ee(d,l):void 0;await p("place-accounts",()=>(e?.onProgress?.("Placing accounts in organisational units"),k(c,l,E,u)),o,r,e)}const O=n.costAllocationTags??[];if(O.length>0?await p("activate-cost-tags",()=>(e?.onProgress?.("Activating cost allocation tags"),te(L,O.map(u=>({TagKey:u})))),o,r,e):g("activate-cost-tags",i,e),n.skipIdentityCentre)g("check-identity-centre",i,e);else{e?.onPhaseStart?.("check-identity-centre"),e?.onProgress?.("Checking Identity Centre status");const u=await ne(F);u.success?(s=u.data.enabled?"enabled":"not-enabled",o.push("check-identity-centre"),e?.onPhaseComplete?.("check-identity-centre","completed")):C("check-identity-centre",u.error,r,e)}const v=n.securityDelegateAccountId;return v?await p("register-security-delegates",()=>(e?.onProgress?.("Registering security service delegated administrators"),oe(c,v)),o,r,e):g("register-security-delegates",i,e),A({organisationId:j,createdAccounts:m,identityCentreStatus:s,phasesCompleted:o,phasesSkipped:i,errors:r})}function C(t,n,e,a){e.push({phase:t,error:n.message}),a?.onError?.(t,n),a?.onPhaseComplete?.(t,"error")}function g(t,n,e){n.push(t),e?.onPhaseStart?.(t),e?.onPhaseComplete?.(t,"skipped")}async function p(t,n,e,a,o){o?.onPhaseStart?.(t);const i=await n();i.success?(e.push(t),o?.onPhaseComplete?.(t,"completed")):C(t,i.error,a,o)}async function se(t,n,e,a,o){const i=await U(t);if(!i.success)return h(i.error);const r=new Set(i.data.map(s=>s.Name?.toLowerCase()).filter(s=>s!==void 0));for(const s of n){if(r.has(s.name.toLowerCase()))continue;a?.onProgress?.(`Account "${s.name}" is not yet a member of this AWS Organisation. Fjall will create a new account (with a new account ID). If "${s.name}" already exists as a standalone Fjall-connected account, abort now and import it into the organisation instead, because creating here produces a duplicate account. See the account-import runbook.`);const c=await _(t,s.name,s.email,void 0,void 0,o);if(!c.success)return h(c.error);e.push({name:c.data.accountName,accountId:c.data.accountId})}if(e.length===0)return A(i.data);const m=await U(t);return m.success?A(m.data):h(m.error)}function N(t){const n=[];for(const[e,a]of Object.entries(t))if(re(a))for(const o of a)n.push({accountName:o,placementKey:e});else n.push(...N(a));return n}function ie(t,n,e){const a=N(t),o=new Map;for(const r of n){const m=r.Name?.toLowerCase();m&&r.Id&&o.set(m,r)}const i=[];for(const{accountName:r,placementKey:m}of a){const s=o.get(r.toLowerCase());!s?.Id||!s.Name||e!==void 0&&s.Id===e||i.push({id:s.Id,name:s.Name,environment:m})}return i}export{Ne as runOrganisationSetup};
@@ -1 +1 @@
1
- import{z as u}from"zod";import{success as N,failure as c}from"@fjall/generator";import{STRUCTURAL_ENVIRONMENTS as v}from"@fjall/util";import{OrganizationsClient as w}from"@aws-sdk/client-organizations";import{listAccounts as g}from"../aws/organisations/accounts.js";import{parseAccountsConfiguration as C,flattenAccountsToEnvironments as E}from"./accountsConfig.js";const T=u.object({Id:u.string().min(1),Name:u.string().min(1)});async function L(e,s){const n=await C(s);if(!n.success)return c(new Error(`Failed to parse ACCOUNTS configuration: ${n.error.message}`));if(n.data===null)return c(new Error("ACCOUNTS configuration file not found"));const r=E(n.data),f=new Map;for(const{accountName:o,environment:t}of r)f.set(o.toLowerCase(),{environment:t,displayName:o});let m;try{m=e.awsProvider.getClient(w)}catch(o){return c(o instanceof Error?o:new Error(String(o)))}const a=await g(m);if(!a.success)return c(a.error);const d=new Map;for(const o of a.data){const t=T.safeParse(o);t.success&&d.set(t.data.Name.toLowerCase(),{id:t.data.Id,name:t.data.Name})}const p=[],l=[];for(const[o,{environment:t,displayName:A}]of f){const i=d.get(o);if(!i){l.push(A);continue}t!==v.ROOT&&p.push({id:i.id,name:i.name,environment:t})}return N({providerAccounts:p,missingAccountNames:l})}function M(e,s){const n=new Map;for(const r of e?.providerAccounts??[])n.set(r.id,r);for(const r of s)n.has(r.id)||n.set(r.id,r);return{...e??{},providerAccounts:Array.from(n.values())}}export{M as mergeReconciledProviderAccounts,L as reconcileProviderAccounts};
1
+ import{z as u}from"zod";import{success as T,failure as i}from"@fjall/generator";import{environmentToTier as v,stageFromWireEnvironment as y,normaliseError as g}from"@fjall/util";import{OrganizationsClient as E}from"@aws-sdk/client-organizations";import{listAccounts as P}from"../aws/organisations/accounts.js";import{parseAccountsConfiguration as S,flattenAccountsToEnvironments as h}from"./accountsConfig.js";const w=6e4,O=u.object({Id:u.string().min(1),Name:u.string().min(1)});async function b(s,c){let n;const r=new Promise((o,e)=>{n=setTimeout(()=>e(new Error(`Timed out parsing ACCOUNTS configuration after ${w}ms`)),w)});let t;try{t=await Promise.race([S(c),r])}catch(o){return i(g(o))}finally{n!==void 0&&clearTimeout(n)}if(!t.success)return i(new Error(`Failed to parse ACCOUNTS configuration: ${t.error.message}`));if(t.data===null)return i(new Error("ACCOUNTS configuration file not found"));const C=h(t.data),f=new Map;for(const{accountName:o,environment:e}of C)f.set(o.toLowerCase(),{environment:e,displayName:o});let d;try{d=s.awsProvider.getClient(E)}catch(o){return i(g(o))}const a=await P(d);if(!a.success)return i(a.error);const l=new Map;for(const o of a.data){const e=O.safeParse(o);e.success&&l.set(e.data.Name.toLowerCase(),{id:e.data.Id,name:e.data.Name})}const p=[],A=[];for(const[o,{environment:e,displayName:N}]of f){const m=l.get(o);if(!m){A.push(N);continue}v(e)!=="organisation"&&p.push({id:m.id,name:m.name,tier:v(e),environment:y(e)})}return T({providerAccounts:p,missingAccountNames:A})}function z(s,c){const n=new Map;for(const r of s?.providerAccounts??[])n.set(r.id,r);for(const r of c)n.has(r.id)||n.set(r.id,r);return{...s??{},providerAccounts:Array.from(n.values())}}export{z as mergeReconciledProviderAccounts,b as reconcileProviderAccounts};
@@ -11,6 +11,7 @@
11
11
  * Ported from cli/src/services/utils/stackCleanup.ts for deploy-core consumers
12
12
  * (webapp worker, CLI via deploy-core).
13
13
  */
14
+ import { S3Client } from "@aws-sdk/client-s3";
14
15
  import type { DeployCallbacks } from "../types/callbacks.js";
15
16
  import { SAFE_CLEANUP_STATES, isCleanableState } from "../types/constants.js";
16
17
  export { SAFE_CLEANUP_STATES, isCleanableState };
@@ -19,6 +20,12 @@ interface StackCleanupCredentials {
19
20
  secretAccessKey: string;
20
21
  sessionToken?: string;
21
22
  }
23
+ /**
24
+ * Empty an S3 bucket by deleting all object versions and delete markers.
25
+ * Handles NoSuchBucket gracefully (bucket already gone).
26
+ * Exported for the org-trail migration's member-bucket decommission.
27
+ */
28
+ export declare function emptyS3Bucket(s3Client: S3Client, bucketName: string, callbacks?: DeployCallbacks): Promise<void>;
22
29
  /**
23
30
  * Clean up a CloudFormation stack stuck in a failed state.
24
31
  * Best-effort: all errors are caught and logged, never throws.
@@ -1 +1 @@
1
- import{CloudFormationClient as $,DescribeStacksCommand as k,DeleteStackCommand as C,ListStackResourcesCommand as _}from"@aws-sdk/client-cloudformation";import{S3Client as m,ListObjectVersionsCommand as h,DeleteObjectsCommand as A}from"@aws-sdk/client-s3";import{NodeHttpHandler as f}from"@smithy/node-http-handler";import{logger as s}from"@fjall/util/logger";import{getErrorMessage as S,maskSensitiveOutput as w,sleep as M}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as g,SAFE_CLEANUP_STATES as P,isCleanableState as T}from"../types/constants.js";const L=1e3;async function R(e,t,n){let d,o;n?.onStackCleanupProgress?.(t,"emptying-bucket");const u=1e3;let l=0;for(;l++<u;){let r;try{r=await e.send(new h({Bucket:t,KeyMarker:d,VersionIdMarker:o}))}catch(c){if(c instanceof Error&&(c.name==="NoSuchBucket"||c.message?.includes("NoSuchBucket"))){s.debug("stackCleanup",`Bucket ${t} no longer exists, skipping`);return}const E=`Unexpected error emptying bucket ${t}: ${w(S(c))}`;s.warn("stackCleanup",E),n?.onLog?.(E,"warn");return}const i=[...r.Versions??[],...r.DeleteMarkers??[]];if(i.length===0)break;for(let c=0;c<i.length;c+=L){const E=i.slice(c,c+L);try{await e.send(new A({Bucket:t,Delete:{Objects:E.map(a=>({Key:a.Key,VersionId:a.VersionId})),Quiet:!0}}))}catch(a){const p=`Failed to delete batch from ${t}: ${w(S(a))}`;s.warn("stackCleanup",p),n?.onLog?.(p,"warn")}}if(!r.IsTruncated)break;d=r.NextKeyMarker,o=r.NextVersionIdMarker}if(l>u){const r=`Bucket ${t} reached ${u} page limit \u2014 some objects may remain`;s.warn("stackCleanup",r),n?.onLog?.(r,"warn")}s.debug("stackCleanup",`Emptied bucket ${t}`)}async function y(e,t,n,d){const o=[];let u,r=0;do{if(r++>=100){s.warn("stackCleanup","Reached 100 page limit listing stack resources",{stackName:t});break}const i=await e.send(new _({StackName:t,NextToken:u}));for(const c of i.StackResourceSummaries??[])if(n(c)){const E=d(c);E&&o.push(E)}u=i.NextToken}while(u);return o}async function F(e,t){return y(e,t,n=>n.ResourceType==="AWS::S3::Bucket"&&n.ResourceStatus==="DELETE_FAILED",n=>n.PhysicalResourceId)}async function H(e,t,n,d,o){const u=d?.timeoutMs??3e5,l=d?.pollMs??5e3;try{const r=new $({region:t,credentials:n,requestHandler:new f({requestTimeout:15e3})});let i;try{i=(await r.send(new k({StackName:e}))).Stacks?.[0]?.StackStatus}catch(a){if(a instanceof Error&&a.message?.includes(g)){s.debug("stackCleanup",`Stack ${e} does not exist, no cleanup needed`);return}s.warn("stackCleanup",`Failed to check stack status: ${w(S(a))}`,{stackName:e,region:t});return}if(!i||!T(i)){s.debug("stackCleanup",`Stack ${e} status ${i??"unknown"} is not cleanable, skipping`);return}s.warn("stackCleanup",`Cleaning up ${e} stack in ${i} state`,{region:t}),o?.onStackCleanupProgress?.(e,"deleting-stack");const c=new m({region:t,credentials:n,requestHandler:new f({requestTimeout:15e3})});try{const a=await F(r,e);for(const p of a)s.warn("stackCleanup",`Emptying bucket ${p}`,{region:t}),await R(c,p,o)}catch(a){const p=`Failed to empty S3 buckets: ${w(S(a))}`;s.warn("stackCleanup",p,{stackName:e,region:t}),o?.onLog?.(p,"warn")}await r.send(new C({StackName:e})),o?.onStackCleanupProgress?.(e,"waiting");const E=await D(r,e,u,l);if(E==="DELETE_COMPLETE"){s.warn("stackCleanup",`${e} stack deleted successfully`,{region:t}),o?.onStackCleanupProgress?.(e,"complete");return}if(E==="DELETE_FAILED"){s.warn("stackCleanup",`${e} still in DELETE_FAILED, retrying with RetainResources`,{region:t});const a=await I(r,e);if(a.length===0)s.warn("stackCleanup",`${e} in DELETE_FAILED but no non-bucket resources to retain \u2014 cannot retry`,{region:t}),o?.onStackCleanupProgress?.(e,"error");else{await r.send(new C({StackName:e,RetainResources:a}));const p=await D(r,e,u,l);p==="DELETE_COMPLETE"?(s.warn("stackCleanup",`${e} stack deleted on retry (retained: ${a.join(", ")})`,{region:t}),o?.onStackCleanupProgress?.(e,"complete")):(s.warn("stackCleanup",`${e} stack still not deleted after retry: ${p}`,{region:t}),o?.onStackCleanupProgress?.(e,"error"))}}}catch(r){s.warn("stackCleanup",`Stack cleanup failed: ${w(S(r))}`,{stackName:e,region:t}),o?.onStackCleanupProgress?.(e,"error")}}async function D(e,t,n,d){const o=Date.now();for(;Date.now()-o<n;){await M(d);try{const l=(await e.send(new k({StackName:t}))).Stacks?.[0]?.StackStatus;if(!l||l==="DELETE_COMPLETE")return"DELETE_COMPLETE";if(l==="DELETE_FAILED")return"DELETE_FAILED";s.debug("stackCleanup",`Waiting for ${t}: ${l}`)}catch(u){if(u instanceof Error&&u.message?.includes(g))return"DELETE_COMPLETE";throw s.debug("stackCleanup",`Unexpected error polling ${t}: ${S(u)}`),u}}return"TIMEOUT"}async function I(e,t){return y(e,t,n=>n.ResourceStatus==="DELETE_FAILED"&&n.ResourceType!=="AWS::S3::Bucket",n=>n.LogicalResourceId)}export{P as SAFE_CLEANUP_STATES,H as cleanupFailedStack,T as isCleanableState};
1
+ import{CloudFormationClient as $,DescribeStacksCommand as k,DeleteStackCommand as C,ListStackResourcesCommand as _}from"@aws-sdk/client-cloudformation";import{S3Client as m,ListObjectVersionsCommand as h,DeleteObjectsCommand as A}from"@aws-sdk/client-s3";import{NodeHttpHandler as f}from"@smithy/node-http-handler";import{logger as o}from"@fjall/util/logger";import{getErrorMessage as S,maskSensitiveOutput as w,sleep as M}from"@fjall/util";import{STACK_NOT_FOUND_PATTERN as g,SAFE_CLEANUP_STATES as P,isCleanableState as T}from"../types/constants.js";const L=1e3;async function R(e,t,n){let d,s;n?.onStackCleanupProgress?.(t,"emptying-bucket");const u=1e3;let l=0;for(;l++<u;){let r;try{r=await e.send(new h({Bucket:t,KeyMarker:d,VersionIdMarker:s}))}catch(c){if(c instanceof Error&&(c.name==="NoSuchBucket"||c.message?.includes("NoSuchBucket"))){o.debug("stackCleanup",`Bucket ${t} no longer exists, skipping`);return}const E=`Unexpected error emptying bucket ${t}: ${w(S(c))}`;o.warn("stackCleanup",E),n?.onLog?.(E,"warn");return}const i=[...r.Versions??[],...r.DeleteMarkers??[]];if(i.length===0)break;for(let c=0;c<i.length;c+=L){const E=i.slice(c,c+L);try{await e.send(new A({Bucket:t,Delete:{Objects:E.map(a=>({Key:a.Key,VersionId:a.VersionId})),Quiet:!0}}))}catch(a){const p=`Failed to delete batch from ${t}: ${w(S(a))}`;o.warn("stackCleanup",p),n?.onLog?.(p,"warn")}}if(!r.IsTruncated)break;d=r.NextKeyMarker,s=r.NextVersionIdMarker}if(l>u){const r=`Bucket ${t} reached ${u} page limit \u2014 some objects may remain`;o.warn("stackCleanup",r),n?.onLog?.(r,"warn")}o.debug("stackCleanup",`Emptied bucket ${t}`)}async function y(e,t,n,d){const s=[];let u,r=0;do{if(r++>=100){o.warn("stackCleanup","Reached 100 page limit listing stack resources",{stackName:t});break}const i=await e.send(new _({StackName:t,NextToken:u}));for(const c of i.StackResourceSummaries??[])if(n(c)){const E=d(c);E&&s.push(E)}u=i.NextToken}while(u);return s}async function F(e,t){return y(e,t,n=>n.ResourceType==="AWS::S3::Bucket"&&n.ResourceStatus==="DELETE_FAILED",n=>n.PhysicalResourceId)}async function H(e,t,n,d,s){const u=d?.timeoutMs??3e5,l=d?.pollMs??5e3;try{const r=new $({region:t,credentials:n,requestHandler:new f({requestTimeout:15e3})});let i;try{i=(await r.send(new k({StackName:e}))).Stacks?.[0]?.StackStatus}catch(a){if(a instanceof Error&&a.message?.includes(g)){o.debug("stackCleanup",`Stack ${e} does not exist, no cleanup needed`);return}o.warn("stackCleanup",`Failed to check stack status: ${w(S(a))}`,{stackName:e,region:t});return}if(!i||!T(i)){o.debug("stackCleanup",`Stack ${e} status ${i??"unknown"} is not cleanable, skipping`);return}o.warn("stackCleanup",`Cleaning up ${e} stack in ${i} state`,{region:t}),s?.onStackCleanupProgress?.(e,"deleting-stack");const c=new m({region:t,credentials:n,requestHandler:new f({requestTimeout:15e3})});try{const a=await F(r,e);for(const p of a)o.warn("stackCleanup",`Emptying bucket ${p}`,{region:t}),await R(c,p,s)}catch(a){const p=`Failed to empty S3 buckets: ${w(S(a))}`;o.warn("stackCleanup",p,{stackName:e,region:t}),s?.onLog?.(p,"warn")}await r.send(new C({StackName:e})),s?.onStackCleanupProgress?.(e,"waiting");const E=await D(r,e,u,l);if(E==="DELETE_COMPLETE"){o.warn("stackCleanup",`${e} stack deleted successfully`,{region:t}),s?.onStackCleanupProgress?.(e,"complete");return}if(E==="DELETE_FAILED"){o.warn("stackCleanup",`${e} still in DELETE_FAILED, retrying with RetainResources`,{region:t});const a=await I(r,e);if(a.length===0)o.warn("stackCleanup",`${e} in DELETE_FAILED but no non-bucket resources to retain \u2014 cannot retry`,{region:t}),s?.onStackCleanupProgress?.(e,"error");else{await r.send(new C({StackName:e,RetainResources:a}));const p=await D(r,e,u,l);p==="DELETE_COMPLETE"?(o.warn("stackCleanup",`${e} stack deleted on retry (retained: ${a.join(", ")})`,{region:t}),s?.onStackCleanupProgress?.(e,"complete")):(o.warn("stackCleanup",`${e} stack still not deleted after retry: ${p}`,{region:t}),s?.onStackCleanupProgress?.(e,"error"))}}}catch(r){o.warn("stackCleanup",`Stack cleanup failed: ${w(S(r))}`,{stackName:e,region:t}),s?.onStackCleanupProgress?.(e,"error")}}async function D(e,t,n,d){const s=Date.now();for(;Date.now()-s<n;){await M(d);try{const l=(await e.send(new k({StackName:t}))).Stacks?.[0]?.StackStatus;if(!l||l==="DELETE_COMPLETE")return"DELETE_COMPLETE";if(l==="DELETE_FAILED")return"DELETE_FAILED";o.debug("stackCleanup",`Waiting for ${t}: ${l}`)}catch(u){if(u instanceof Error&&u.message?.includes(g))return"DELETE_COMPLETE";throw o.debug("stackCleanup",`Unexpected error polling ${t}: ${S(u)}`),u}}return"TIMEOUT"}async function I(e,t){return y(e,t,n=>n.ResourceStatus==="DELETE_FAILED"&&n.ResourceType!=="AWS::S3::Bucket",n=>n.LogicalResourceId)}export{P as SAFE_CLEANUP_STATES,H as cleanupFailedStack,R as emptyS3Bucket,T as isCleanableState};
@@ -0,0 +1,43 @@
1
+ import { type S3Client } from "@aws-sdk/client-s3";
2
+ import { type CloudTrailClient } from "@aws-sdk/client-cloudtrail";
3
+ import { type KMSClient } from "@aws-sdk/client-kms";
4
+ import { type Result } from "@fjall/generator";
5
+ import type { DeployCallbacks } from "../../types/callbacks.js";
6
+ export interface DecommissionClients {
7
+ cloudTrailClient: CloudTrailClient;
8
+ s3Client: S3Client;
9
+ kmsClient: KMSClient;
10
+ }
11
+ export interface DecommissionInput {
12
+ accountId: string;
13
+ /** Trail name the CFN draining deploy should have deleted. */
14
+ trailName: string;
15
+ bucketName: string;
16
+ /** CMK to schedule for deletion; omitted ⇒ KMS step is skipped. */
17
+ keyArn?: string;
18
+ acknowledgeTrailHistoryLoss: boolean;
19
+ abortSignal?: AbortSignal;
20
+ }
21
+ export type DecommissionOutcome = {
22
+ outcome: "decommissioned";
23
+ } | {
24
+ outcome: "blocked-trail-still-exists";
25
+ } | {
26
+ outcome: "blocked-awaiting-acknowledgement";
27
+ bucketName: string;
28
+ };
29
+ /**
30
+ * Decommission a drained member account's trail storage: empty + delete the
31
+ * retained bucket, then schedule the CMK for deletion. Every step probes
32
+ * first and treats already-gone as success, so the whole function converges
33
+ * on re-run after any partial failure.
34
+ *
35
+ * The trail resource itself is never deleted here — CloudFormation owns it,
36
+ * and a trail still present means the draining deploy has not happened
37
+ * (or rolled back), so nothing is touched.
38
+ *
39
+ * Bucket deletion is SDK-side by design: the bucket is RETAIN in the
40
+ * template, and CloudFormation must never be pointed at a non-empty
41
+ * CloudTrail bucket (delivery continues mid-delete, wedging the stack).
42
+ */
43
+ export declare function decommissionMemberTrailStorage(clients: DecommissionClients, input: DecommissionInput, callbacks?: DeployCallbacks): Promise<Result<DecommissionOutcome>>;
@@ -0,0 +1 @@
1
+ import{DeleteBucketCommand as f,HeadBucketCommand as y,ListObjectVersionsCommand as E}from"@aws-sdk/client-s3";import{DescribeTrailsCommand as S}from"@aws-sdk/client-cloudtrail";import{DescribeKeyCommand as k,ScheduleKeyDeletionCommand as w}from"@aws-sdk/client-kms";import{success as o,failure as a}from"@fjall/generator";import{getErrorMessage as s,maskSensitiveOutput as c}from"@fjall/util";import{logger as g}from"@fjall/util/logger";import{SDK_TIMEOUT_MS as N,extractErrorName as u}from"../../aws/organisations/types.js";import{emptyS3Bucket as C}from"../stackCleanup.js";const p=14,b=new Set(["NoSuchBucket","NotFound","404"]),h=new Set(["NotFoundException","KMSInvalidStateException"]);function d(t){const e=AbortSignal.timeout(N);return t!==void 0?AbortSignal.any([t,e]):e}async function D(t,e,n){try{const r=await t.send(new S({trailNameList:[e]}),{abortSignal:d(n)});return o((r.trailList??[]).length>0)}catch(r){return u(r)==="TrailNotFoundException"?o(!1):a(new Error(`Failed to probe trail ${e}: ${c(s(r))}`))}}async function K(t,e,n){try{return await t.send(new y({Bucket:e}),{abortSignal:d(n)}),o(!0)}catch(r){return b.has(u(r))?o(!1):a(new Error(`Failed to probe bucket ${e}: ${c(s(r))}`))}}async function M(t,e,n){try{const r=await t.send(new E({Bucket:e,MaxKeys:1}),{abortSignal:d(n)});return(r.Versions??[]).length===0&&(r.DeleteMarkers??[]).length===0}catch(r){return g.debug("memberTrailCleanup",`Emptiness probe failed for ${e}; treating as not proven empty`,{error:c(s(r))}),!1}}async function _(t,e,n){try{const i=(await t.send(new k({KeyId:e}),{abortSignal:d(n)})).KeyMetadata?.KeyState;if(i==="PendingDeletion"||i==="PendingReplicaDeletion")return o(void 0)}catch(r){return u(r)==="NotFoundException"?o(void 0):a(new Error(`Failed to probe CMK: ${c(s(r))}`))}try{return await t.send(new w({KeyId:e,PendingWindowInDays:p}),{abortSignal:d(n)}),o(void 0)}catch(r){return h.has(u(r))?o(void 0):a(new Error(`Failed to schedule CMK deletion: ${c(s(r))}`))}}async function R(t,e,n){const r=await D(t.cloudTrailClient,e.trailName,e.abortSignal);if(!r.success)return a(r.error);if(r.data)return o({outcome:"blocked-trail-still-exists"});const i=await K(t.s3Client,e.bucketName,e.abortSignal);if(!i.success)return a(i.error);if(i.data){if(!await M(t.s3Client,e.bucketName,e.abortSignal)&&e.acknowledgeTrailHistoryLoss!==!0)return o({outcome:"blocked-awaiting-acknowledgement",bucketName:e.bucketName});await C(t.s3Client,e.bucketName,n);try{await t.s3Client.send(new f({Bucket:e.bucketName}),{abortSignal:d(e.abortSignal)})}catch(m){if(!b.has(u(m)))return a(new Error(`Failed to delete bucket ${e.bucketName}: ${c(s(m))}`))}}if(e.keyArn!==void 0&&e.keyArn!==""){const l=await _(t.kmsClient,e.keyArn,e.abortSignal);if(!l.success)return a(l.error)}return o({outcome:"decommissioned"})}export{R as decommissionMemberTrailStorage};
@@ -0,0 +1,64 @@
1
+ import { type S3Client } from "@aws-sdk/client-s3";
2
+ import { type Result } from "@fjall/generator";
3
+ import { ORG_TRAIL_BUCKET_OUTPUT_KEY, TRAIL_BUCKET_OUTPUT_KEY, TRAIL_KEY_ARN_OUTPUT_KEY, type ProviderAccount, type TrailLifecycleState } from "@fjall/util/config";
4
+ import { type DecommissionClients, type DecommissionOutcome } from "./memberTrailCleanup.js";
5
+ import type { DeployCallbacks } from "../../types/callbacks.js";
6
+ export { ORG_TRAIL_BUCKET_OUTPUT_KEY, TRAIL_BUCKET_OUTPUT_KEY, TRAIL_KEY_ARN_OUTPUT_KEY };
7
+ export interface MemberTrailFacts {
8
+ /**
9
+ * Org-trail delivery verified for this account. `undefined` = could not be
10
+ * determined (probe failed or recency scan truncated) — never advance on it.
11
+ */
12
+ orgDeliveryVerified?: boolean;
13
+ /** Outcome of this run's decommission attempt (draining accounts only). */
14
+ decommissionOutcome?: DecommissionOutcome["outcome"] | "failed";
15
+ }
16
+ export type TrailMigrationTransition = {
17
+ action: "advance";
18
+ to: TrailLifecycleState;
19
+ } | {
20
+ action: "hold";
21
+ reason: string;
22
+ };
23
+ /**
24
+ * Pure transition truth table for the org-trail migration. Uncertainty never
25
+ * advances state: a probe that failed or could not complete holds the account
26
+ * where it is and reports why.
27
+ */
28
+ export declare function decideNextTransition(state: TrailLifecycleState | undefined, facts: MemberTrailFacts): TrailMigrationTransition;
29
+ export interface TrailMigrationDeps {
30
+ /** Management-account S3 client — the org trail bucket lives there. */
31
+ managementS3Client: S3Client;
32
+ /** Assume the member role and build the decommission clients. */
33
+ getMemberClients: (account: ProviderAccount, region: string) => Promise<Result<DecommissionClients>>;
34
+ }
35
+ export interface TrailMigrationInput {
36
+ orgId: string;
37
+ orgTrailBucketName: string;
38
+ /** Cascade-deployed accounts (platform + members). */
39
+ accounts: ProviderAccount[];
40
+ /** accountId → home-region stack outputs captured by this run's cascade. */
41
+ cascadeOutputs: Map<string, Record<string, string>>;
42
+ defaultRegion: string;
43
+ recencyWindowMs?: number;
44
+ abortSignal?: AbortSignal;
45
+ }
46
+ export interface TrailMigrationOutcome {
47
+ accountId: string;
48
+ accountName: string;
49
+ from: TrailLifecycleState | undefined;
50
+ /** Set only when the account advanced this run. */
51
+ to?: TrailLifecycleState;
52
+ heldReason?: string;
53
+ }
54
+ /**
55
+ * Reconcile every cascade account's trail lifecycle against AWS ground truth.
56
+ * Two-invocation convergence by design: deploy N creates the org trail
57
+ * (overlap with member trails is sanctioned for up to a day), deploy N+1's
58
+ * cheap probes advance states. Facts are re-derived from AWS on every run —
59
+ * the persisted lifecycle is an intent cache, so lost persistence self-heals.
60
+ *
61
+ * Never throws and never fails the deploy: every problem is reported through
62
+ * callbacks and the account simply holds its current state.
63
+ */
64
+ export declare function reconcileTrailMigration(deps: TrailMigrationDeps, input: TrailMigrationInput, callbacks: DeployCallbacks): Promise<TrailMigrationOutcome[]>;
@@ -0,0 +1 @@
1
+ import{maskSensitiveOutput as h}from"@fjall/util";import{ACCOUNT_TRAIL_NAME as N,ORG_TRAIL_BUCKET_OUTPUT_KEY as $,TRAIL_BUCKET_OUTPUT_KEY as p,TRAIL_KEY_ARN_OUTPUT_KEY as T}from"@fjall/util/config";import{verifyOrgTrailDelivery as L}from"../../aws/cloudtrail/orgTrailDelivery.js";import{decommissionMemberTrailStorage as R}from"./memberTrailCleanup.js";function w(r,i){if(r==="account")return{action:"hold",reason:"pinned to a per-account trail"};if(r==="org")return{action:"hold",reason:"already migrated to the org trail"};if(r===void 0)return i.orgDeliveryVerified===!0?{action:"advance",to:"draining"}:{action:"hold",reason:i.orgDeliveryVerified===!1?"org-trail delivery not yet verified (first delivery can take up to 24h)":"org-trail delivery could not be determined"};switch(i.decommissionOutcome){case"decommissioned":return{action:"advance",to:"org"};case"blocked-trail-still-exists":return{action:"hold",reason:"per-account trail still exists \u2014 draining deploy not yet applied"};case"blocked-awaiting-acknowledgement":return{action:"hold",reason:"awaiting acknowledgeTrailHistoryLoss before discarding history"};case"failed":return{action:"hold",reason:"decommission attempt failed"};default:return{action:"hold",reason:"decommission was not attempted"}}}function a(r,i){r.onTrailMigrationPhase?.({...i,...i.detail!==void 0?{detail:h(i.detail)}:{}})}async function C(r,i,o){const d=[],m=i.accounts.filter(e=>e.trailLifecycle===void 0),I=i.accounts.filter(e=>e.trailLifecycle==="draining"),f=new Map;if(m.length>0){a(o,{accountId:"*",accountName:"organisation",phase:"verify-delivery",status:"started",detail:`Checking org-trail delivery for ${m.length} account(s) in s3://${i.orgTrailBucketName}`});const e=await L(r.managementS3Client,{bucketName:i.orgTrailBucketName,orgId:i.orgId,accountIds:m.map(n=>n.id),...i.recencyWindowMs!==void 0?{recencyWindowMs:i.recencyWindowMs}:{},...i.abortSignal!==void 0?{abortSignal:i.abortSignal}:{}});if(e.success){for(const n of e.data.perAccount)f.set(n.accountId,n.recentDelivery===void 0?void 0:n.delivered&&n.recentDelivery);a(o,{accountId:"*",accountName:"organisation",phase:"verify-delivery",status:"completed"})}else a(o,{accountId:"*",accountName:"organisation",phase:"verify-delivery",status:"failed",detail:e.error.message})}for(const e of m){const n={...f.get(e.id)!==void 0?{orgDeliveryVerified:f.get(e.id)}:{}},s=w(void 0,n);s.action==="advance"?(o.onTrailLifecycleChanged?.(e.id,void 0,s.to),o.onLog?.(`${e.name}: org-trail delivery verified \u2014 trail lifecycle advanced to "draining" (the next deploy removes its per-account trail)`,"info"),d.push({accountId:e.id,accountName:e.name,from:void 0,to:s.to})):d.push({accountId:e.id,accountName:e.name,from:void 0,heldReason:s.reason})}for(const e of I){const n=e.region??i.defaultRegion,s=i.cascadeOutputs.get(e.id),c=s?.[p],y=s?.[T];if(c===void 0||c===""){const t=`stack output ${p} unavailable for this run`;o.onProgress?.({type:"warning",message:`${e.name}: trail decommission skipped \u2014 ${t}. The account stays "draining"; the next deploy retries.`}),d.push({accountId:e.id,accountName:e.name,from:"draining",heldReason:t});continue}a(o,{accountId:e.id,accountName:e.name,phase:"decommission",status:"started"});let l;const u=await r.getMemberClients(e,n);if(!u.success)l={decommissionOutcome:"failed"},o.onProgress?.({type:"warning",message:h(`${e.name}: trail decommission failed \u2014 ${u.error.message}. The account stays "draining"; the next deploy retries.`)}),a(o,{accountId:e.id,accountName:e.name,phase:"decommission",status:"failed",detail:u.error.message});else{const t=await R(u.data,{accountId:e.id,trailName:N,bucketName:c,...y!==void 0&&y!==""?{keyArn:y}:{},acknowledgeTrailHistoryLoss:e.acknowledgeTrailHistoryLoss===!0,...i.abortSignal!==void 0?{abortSignal:i.abortSignal}:{}},o);if(!t.success)l={decommissionOutcome:"failed"},o.onProgress?.({type:"warning",message:h(`${e.name}: trail decommission failed \u2014 ${t.error.message}. The account stays "draining"; the next deploy retries.`)}),a(o,{accountId:e.id,accountName:e.name,phase:"decommission",status:"failed",detail:t.error.message});else if(l={decommissionOutcome:t.data.outcome},t.data.outcome==="blocked-awaiting-acknowledgement"){const v=`${e.name}: per-account trail removed, but its log history in s3://${c} will be DISCARDED on decommission. Back the bucket up if the history matters, then set acknowledgeTrailHistoryLoss: true for account ${e.id} and redeploy.`;o.onProgress?.({type:"warning",message:v}),a(o,{accountId:e.id,accountName:e.name,phase:"decommission",status:"blocked",detail:v})}else t.data.outcome==="blocked-trail-still-exists"?a(o,{accountId:e.id,accountName:e.name,phase:"decommission",status:"blocked",detail:"per-account trail still exists \u2014 the draining deploy has not been applied yet"}):a(o,{accountId:e.id,accountName:e.name,phase:"decommission",status:"completed",detail:`bucket s3://${c} removed; CMK scheduled for deletion`})}const g=w("draining",l);g.action==="advance"?(o.onTrailLifecycleChanged?.(e.id,"draining",g.to),o.onLog?.(`${e.name}: trail storage decommissioned \u2014 trail lifecycle advanced to "org"`,"info"),d.push({accountId:e.id,accountName:e.name,from:"draining",to:g.to})):d.push({accountId:e.id,accountName:e.name,from:"draining",heldReason:g.reason})}return d}export{$ as ORG_TRAIL_BUCKET_OUTPUT_KEY,p as TRAIL_BUCKET_OUTPUT_KEY,T as TRAIL_KEY_ARN_OUTPUT_KEY,w as decideNextTransition,C as reconcileTrailMigration};
@@ -1 +1 @@
1
- import{filterDangerousEnvVars as u}from"@fjall/util";class s{buildContextArgs(r){const e=[];return r?.accountId&&e.push("-c",`accountId=${r.accountId}`),r?.environment&&e.push("-c",`environment=${r.environment}`),r?.managedAccount&&e.push("-c","managedAccount=true"),r?.accountName&&e.push("-c",`accountName=${r.accountName}`),r?.orgId&&e.push("-c",`orgId=${r.orgId}`),r?.rootId&&e.push("-c",`rootId=${r.rootId}`),r?.managementAccountId&&e.push("-c",`managementAccountId=${r.managementAccountId}`),r?.ipamPoolId&&e.push("-c",`ipamPoolId=${r.ipamPoolId}`),r?.fjallOrgId&&e.push("-c",`fjallOrgId=${r.fjallOrgId}`),r?.fjallOidcConfigured&&e.push("-c",`fjallOidcConfigured=${r.fjallOidcConfigured}`),r?.fjallAccountGlobalsConfigured&&e.push("-c",`fjallAccountGlobalsConfigured=${r.fjallAccountGlobalsConfigured}`),r?.orgConfig&&e.push("-c",`orgConfig=${r.orgConfig}`),r?.fjallAdoptBackupVault&&e.push("-c","fjallAdoptBackupVault=true"),r?.resolvedSecretArns&&e.push("-c",`fjallResolvedSecretArns=${r.resolvedSecretArns}`),e}buildParameterArgs(r){if(r===void 0)return[];const e=Object.entries(r);if(e.length===0)return[];const o=[];for(const[a,n]of e){if(!/^[A-Za-z][A-Za-z0-9]*$/.test(a))throw new Error(`Invalid CloudFormation parameter name "${a}": must match /^[A-Za-z][A-Za-z0-9]*$/ (alphanumeric, leading letter, no separators).`);if(n==="")throw new Error(`CloudFormation parameter "${a}" has an empty value.`);if(/[,\n\r]/.test(n))throw new Error(`CloudFormation parameter "${a}" value contains "," or newline \u2014 cdk's --parameters splits on "," so the deploy would silently fragment.`);o.push("--parameters",`${a}=${n}`)}return o}injectCascadeCredentials(r,e){e&&(r.AWS_ACCESS_KEY_ID=e.accessKeyId,r.AWS_SECRET_ACCESS_KEY=e.secretAccessKey,delete r.AWS_SESSION_TOKEN,e.sessionToken&&(r.AWS_SESSION_TOKEN=e.sessionToken))}buildCdkEnv(r){const e={...u(process.env),CI:"true",FORCE_COLOR:"0",CDK_DISABLE_VERSION_CHECK:"1"};return r?.context?.region&&(e.AWS_REGION=r.context.region,e.AWS_DEFAULT_REGION=r.context.region,e.CDK_DEFAULT_REGION=r.context.region),r?.context?.accountId&&(e.CDK_DEFAULT_ACCOUNT=r.context.accountId),this.injectCascadeCredentials(e,r?.credentials),e}}export{s as CdkArgumentBuilder};
1
+ import{filterDangerousEnvVars as u}from"@fjall/util";class s{buildContextArgs(r){const a=[];return r?.accountId&&a.push("-c",`accountId=${r.accountId}`),r?.environment&&a.push("-c",`environment=${r.environment}`),r?.managedAccount&&a.push("-c","managedAccount=true"),r?.accountName&&a.push("-c",`accountName=${r.accountName}`),r?.orgId&&a.push("-c",`orgId=${r.orgId}`),r?.rootId&&a.push("-c",`rootId=${r.rootId}`),r?.managementAccountId&&a.push("-c",`managementAccountId=${r.managementAccountId}`),r?.ipamPoolId&&a.push("-c",`ipamPoolId=${r.ipamPoolId}`),r?.fjallOrgId&&a.push("-c",`fjallOrgId=${r.fjallOrgId}`),r?.fjallOidcConfigured&&a.push("-c",`fjallOidcConfigured=${r.fjallOidcConfigured}`),r?.fjallAccountGlobalsConfigured&&a.push("-c",`fjallAccountGlobalsConfigured=${r.fjallAccountGlobalsConfigured}`),r?.fjallAccountTrailState&&a.push("-c",`fjallAccountTrailState=${r.fjallAccountTrailState}`),r?.orgConfig&&a.push("-c",`orgConfig=${r.orgConfig}`),r?.fjallAdoptBackupVault&&a.push("-c","fjallAdoptBackupVault=true"),r?.resolvedSecretArns&&a.push("-c",`fjallResolvedSecretArns=${r.resolvedSecretArns}`),a}buildParameterArgs(r){if(r===void 0)return[];const a=Object.entries(r);if(a.length===0)return[];const n=[];for(const[e,l]of a){if(!/^[A-Za-z][A-Za-z0-9]*$/.test(e))throw new Error(`Invalid CloudFormation parameter name "${e}": must match /^[A-Za-z][A-Za-z0-9]*$/ (alphanumeric, leading letter, no separators).`);if(l==="")throw new Error(`CloudFormation parameter "${e}" has an empty value.`);if(/[,\n\r]/.test(l))throw new Error(`CloudFormation parameter "${e}" value contains "," or newline \u2014 cdk's --parameters splits on "," so the deploy would silently fragment.`);n.push("--parameters",`${e}=${l}`)}return n}injectCascadeCredentials(r,a){a&&(r.AWS_ACCESS_KEY_ID=a.accessKeyId,r.AWS_SECRET_ACCESS_KEY=a.secretAccessKey,delete r.AWS_SESSION_TOKEN,a.sessionToken&&(r.AWS_SESSION_TOKEN=a.sessionToken))}buildCdkEnv(r){const a={...u(process.env),CI:"true",FORCE_COLOR:"0",CDK_DISABLE_VERSION_CHECK:"1"};return r?.context?.region&&(a.AWS_REGION=r.context.region,a.AWS_DEFAULT_REGION=r.context.region,a.CDK_DEFAULT_REGION=r.context.region),r?.context?.accountId&&(a.CDK_DEFAULT_ACCOUNT=r.context.accountId),this.injectCascadeCredentials(a,r?.credentials),a}}export{s as CdkArgumentBuilder};
@@ -13,6 +13,7 @@ export interface CdkContext {
13
13
  fjallOrgId?: string;
14
14
  fjallOidcConfigured?: string;
15
15
  fjallAccountGlobalsConfigured?: string;
16
+ fjallAccountTrailState?: string;
16
17
  orgConfig?: string;
17
18
  fjallAdoptBackupVault?: string;
18
19
  /**
@@ -1 +1 @@
1
- import{getApplicationStackName as l,getOrganisationStackName as i,isApplicationStack as u}from"../../types/operations.js";const p=5e3;function t(e,a){if(e&&!e.includes("*"))return e;if(e){const o=e.match(/\*?(\w+)\*?/);if(o?.[1]){const n=o[1],r=a.target;return u(n)?l(r,n):`${r}${n}`}return e}}function c(e){const a=e.deployType;return a==="organisation"||a==="platform"||a==="account"?i(a):`${e.target}Network`}function f(e,a,o){return{accountId:a,region:o,environment:e.environment,managedAccount:e.isManagedAccount,accountName:e.accountName,orgId:e.orgId,rootId:e.rootId,managementAccountId:e.managementAccountId,ipamPoolId:e.ipamPoolId,fjallOrgId:e.fjallOrgId,fjallOidcConfigured:e.fjallOidcConfigured?"true":void 0,fjallAccountGlobalsConfigured:e.fjallAccountGlobalsConfigured?"true":void 0,orgConfig:e.orgConfig,fjallAdoptBackupVault:e.fjallAdoptBackupVault?"true":void 0,resolvedSecretArns:e.resolvedSecretArns}}export{p as STACK_DETECTION_FALLBACK_MS,f as buildDeploymentCdkContext,c as getFallbackStackName,t as resolveStackName};
1
+ import{getApplicationStackName as l,getOrganisationStackName as i,isApplicationStack as u}from"../../types/operations.js";const t=5e3;function c(a,e){if(a&&!a.includes("*"))return a;if(a){const o=a.match(/\*?(\w+)\*?/);if(o?.[1]){const n=o[1],r=e.target;return u(n)?l(r,n):`${r}${n}`}return a}}function f(a){const e=a.deployType;return e==="organisation"||e==="platform"||e==="account"?i(e):`${a.target}Network`}function p(a,e,o){return{accountId:e,region:o,environment:a.environment,managedAccount:a.isManagedAccount,accountName:a.accountName,orgId:a.orgId,rootId:a.rootId,managementAccountId:a.managementAccountId,ipamPoolId:a.ipamPoolId,fjallOrgId:a.fjallOrgId,fjallOidcConfigured:a.fjallOidcConfigured?"true":void 0,fjallAccountGlobalsConfigured:a.fjallAccountGlobalsConfigured?"true":void 0,fjallAccountTrailState:a.fjallAccountTrailState,orgConfig:a.orgConfig,fjallAdoptBackupVault:a.fjallAdoptBackupVault?"true":void 0,resolvedSecretArns:a.resolvedSecretArns}}export{t as STACK_DETECTION_FALLBACK_MS,p as buildDeploymentCdkContext,f as getFallbackStackName,c as resolveStackName};
@@ -33,6 +33,7 @@ export declare class CdkContextBuilder {
33
33
  fjallOrgId?: string;
34
34
  fjallOidcConfigured?: boolean;
35
35
  fjallAccountGlobalsConfigured?: boolean;
36
+ fjallAccountTrailState?: string;
36
37
  orgConfig?: string;
37
38
  }, options: {
38
39
  verbose?: boolean;
@@ -1 +1 @@
1
- import{DEFAULT_REGION as r}from"@fjall/generator";class t{static buildDeploymentContext(e,o,a){return{deployType:e.deployType,target:e.target,path:e.path,...e.assemblyDir!==void 0?{assemblyDir:e.assemblyDir}:{},...e.environment!==void 0?{environment:e.environment}:{},options:o,stackOutputs:e.stackOutputs||{},callerIdentity:e.callerIdentity,region:e.region||a?.primaryRegion||r,isManagedAccount:e.isManagedAccount,accountName:e.accountName,logPath:e.logPath,orgId:e.orgId,rootId:e.rootId,managementAccountId:e.managementAccountId,ipamPoolId:e.ipamPoolId,fjallOrgId:e.fjallOrgId,fjallOidcConfigured:e.fjallOidcConfigured,fjallAccountGlobalsConfigured:e.fjallAccountGlobalsConfigured,orgConfig:e.orgConfig}}static updateContext(e,o){return{...e,...o}}}export{t as CdkContextBuilder};
1
+ import{DEFAULT_REGION as l}from"@fjall/generator";class d{static buildDeploymentContext(e,o,a){return{deployType:e.deployType,target:e.target,path:e.path,...e.assemblyDir!==void 0?{assemblyDir:e.assemblyDir}:{},...e.environment!==void 0?{environment:e.environment}:{},options:o,stackOutputs:e.stackOutputs||{},callerIdentity:e.callerIdentity,region:e.region||a?.primaryRegion||l,isManagedAccount:e.isManagedAccount,accountName:e.accountName,logPath:e.logPath,orgId:e.orgId,rootId:e.rootId,managementAccountId:e.managementAccountId,ipamPoolId:e.ipamPoolId,fjallOrgId:e.fjallOrgId,fjallOidcConfigured:e.fjallOidcConfigured,fjallAccountGlobalsConfigured:e.fjallAccountGlobalsConfigured,fjallAccountTrailState:e.fjallAccountTrailState,orgConfig:e.orgConfig}}static updateContext(e,o){return{...e,...o}}}export{d as CdkContextBuilder};
@@ -1,6 +1,6 @@
1
1
  import type { DeployCallbacks } from "./callbacks.js";
2
2
  /**
3
- * Every {@link DeployCallbacks} key (43 callbacks + the `verbose` flag),
3
+ * Every {@link DeployCallbacks} key (45 callbacks + the `verbose` flag),
4
4
  * derived from the type-checked {@link CALLBACK_KEY_PRESENCE} map so the list
5
5
  * can never silently diverge from the interface.
6
6
  */
@@ -1 +1 @@
1
- const e={onStepStart:!0,onStepComplete:!0,onProgress:!0,onLog:!0,onOutput:!0,onResourceProgress:!0,onParallelPhaseStart:!0,onParallelPhaseComplete:!0,onParallelStackResourceProgress:!0,onDockerProgress:!0,onBuildPushStart:!0,onBuildPushProgress:!0,onBuildPushComplete:!0,onTaskDefRegistered:!0,onECSUpdate:!0,onECSProgress:!0,onECSComplete:!0,onMigrationsStart:!0,onMigrationsComplete:!0,onCDKBootstrap:!0,onCdkOutput:!0,onCascadeStart:!0,onCascadeAccountsReconciled:!0,onOrgChangesDetected:!0,onCascadeMissingAccounts:!0,onCascadePhaseStart:!0,onCascadePhaseComplete:!0,onCascadeAccountStart:!0,onCascadeAccountPhaseChange:!0,onCascadeAccountResourceProgress:!0,onCascadeAccountComplete:!0,onCascadeComplete:!0,onCascadeLedger:!0,onStackCleanupProgress:!0,onDetectionComplete:!0,onDetectionPhaseChange:!0,onSSOUrl:!0,onAuthComplete:!0,onOpenNextBuildStart:!0,onOpenNextProgress:!0,onOpenNextBuildComplete:!0,onOpenNextBuildError:!0,onError:!0,verbose:!0},t=Object.keys(e);export{t as ALL_CALLBACK_KEYS};
1
+ const e={onStepStart:!0,onStepComplete:!0,onProgress:!0,onLog:!0,onOutput:!0,onResourceProgress:!0,onParallelPhaseStart:!0,onParallelPhaseComplete:!0,onParallelStackResourceProgress:!0,onDockerProgress:!0,onBuildPushStart:!0,onBuildPushProgress:!0,onBuildPushComplete:!0,onTaskDefRegistered:!0,onECSUpdate:!0,onECSProgress:!0,onECSComplete:!0,onMigrationsStart:!0,onMigrationsComplete:!0,onCDKBootstrap:!0,onCdkOutput:!0,onCascadeStart:!0,onCascadeAccountsReconciled:!0,onOrgChangesDetected:!0,onCascadeMissingAccounts:!0,onCascadePhaseStart:!0,onCascadePhaseComplete:!0,onCascadeAccountStart:!0,onCascadeAccountPhaseChange:!0,onCascadeAccountResourceProgress:!0,onCascadeAccountComplete:!0,onCascadeComplete:!0,onCascadeLedger:!0,onTrailMigrationPhase:!0,onTrailLifecycleChanged:!0,onStackCleanupProgress:!0,onDetectionComplete:!0,onDetectionPhaseChange:!0,onSSOUrl:!0,onAuthComplete:!0,onOpenNextBuildStart:!0,onOpenNextProgress:!0,onOpenNextBuildComplete:!0,onOpenNextBuildError:!0,onError:!0,verbose:!0},t=Object.keys(e);export{t as ALL_CALLBACK_KEYS};
@@ -1,4 +1,5 @@
1
- import type { ProgressEvent, ResourceEvent, AwsAuthResult, CascadeDeploymentResult, CascadePhase, BuildPushStartEvent, BuildPushProgressEvent, BuildPushCompleteEvent, TaskDefRegisteredEvent, ECSCompleteEvent, MigrationsStartEvent, MigrationsCompleteEvent } from "./events.js";
1
+ import type { ProgressEvent, ResourceEvent, AwsAuthResult, CascadeDeploymentResult, CascadePhase, BuildPushStartEvent, BuildPushProgressEvent, BuildPushCompleteEvent, TaskDefRegisteredEvent, ECSCompleteEvent, MigrationsStartEvent, MigrationsCompleteEvent, TrailMigrationPhaseEvent } from "./events.js";
2
+ import type { TrailLifecycleState } from "@fjall/util/config";
2
3
  import type { DetectionResult } from "./detection.js";
3
4
  import type { CascadeLedger } from "../orchestration/cascadeSummary.js";
4
5
  export type StepCompleteStatus = "completed" | "skipped" | "error";
@@ -159,6 +160,19 @@ export interface DeployCallbacks {
159
160
  * summary from it; the webapp/worker ignores it and reads the scalar above.
160
161
  */
161
162
  onCascadeLedger?: (ledger: CascadeLedger) => void;
163
+ /**
164
+ * @emittedBy engine — per-account progress through the org-trail migration
165
+ * reconciler (delivery verification, storage decommission). `detail` is
166
+ * pre-masked by the engine.
167
+ */
168
+ onTrailMigrationPhase?: (event: TrailMigrationPhaseEvent) => void;
169
+ /**
170
+ * @emittedBy engine — fired when the reconciler advances an account's
171
+ * `trailLifecycle` (undefined → "draining" → "org"). Consumers persist the
172
+ * new state to their config store (CLI: org-config API push; worker:
173
+ * ConnectedAwsAccount update) — the engine holds no durable state itself.
174
+ */
175
+ onTrailLifecycleChanged?: (accountId: string, from: TrailLifecycleState | undefined, to: TrailLifecycleState) => void;
162
176
  /** @emittedBy engine — progress during failed-state stack cleanup (S3 emptying, deletion). */
163
177
  onStackCleanupProgress?: (stackName: string, phase: "emptying-bucket" | "deleting-stack" | "waiting" | "complete" | "error") => void;
164
178
  /**
@@ -23,6 +23,7 @@ export interface DeploymentContext {
23
23
  fjallOrgId?: string;
24
24
  fjallOidcConfigured?: boolean;
25
25
  fjallAccountGlobalsConfigured?: boolean;
26
+ fjallAccountTrailState?: string;
26
27
  orgConfig?: string;
27
28
  fjallAdoptBackupVault?: boolean;
28
29
  resolvedSecretArns?: string;
@@ -15,23 +15,24 @@ export declare const DEPLOYMENT_EVENT_RESOURCE_CATEGORIES: readonly ["security",
15
15
  export type DeploymentEventResourceCategory = (typeof DEPLOYMENT_EVENT_RESOURCE_CATEGORIES)[number];
16
16
  export declare const CASCADE_PHASES: readonly ["platform", "domains", "accounts"];
17
17
  export declare const CASCADE_ACCOUNT_STATUSES: readonly ["started", "deploying", "completed", "failed"];
18
- export declare const DEPLOYMENT_EVENT_TYPES: readonly ["step", "resource", "docker", "ecs", "error", "complete", "cascade_phase", "cascade_account", "cascade_missing_accounts", "parallel_phase", "detection", "log"];
18
+ export declare const DEPLOYMENT_EVENT_TYPES: readonly ["step", "resource", "docker", "ecs", "error", "complete", "cascade_phase", "cascade_account", "cascade_missing_accounts", "parallel_phase", "detection", "trail_migration", "log"];
19
19
  export type DeploymentEventType = (typeof DEPLOYMENT_EVENT_TYPES)[number];
20
20
  export type DeploymentEventCascadePhase = (typeof CASCADE_PHASES)[number];
21
21
  export type DeploymentEventCascadeAccountStatus = (typeof CASCADE_ACCOUNT_STATUSES)[number];
22
22
  export declare const DeploymentEventSchema: z.ZodObject<{
23
23
  type: z.ZodEnum<{
24
24
  step: "step";
25
+ error: "error";
25
26
  resource: "resource";
26
27
  docker: "docker";
27
28
  ecs: "ecs";
28
- error: "error";
29
29
  complete: "complete";
30
30
  cascade_phase: "cascade_phase";
31
31
  cascade_account: "cascade_account";
32
32
  cascade_missing_accounts: "cascade_missing_accounts";
33
33
  parallel_phase: "parallel_phase";
34
34
  detection: "detection";
35
+ trail_migration: "trail_migration";
35
36
  log: "log";
36
37
  }>;
37
38
  timestamp: z.ZodString;
@@ -101,9 +102,9 @@ export declare const DeploymentEventSchema: z.ZodObject<{
101
102
  operationKey: z.ZodString;
102
103
  status: z.ZodEnum<{
103
104
  started: "started";
104
- deploying: "deploying";
105
105
  completed: "completed";
106
106
  failed: "failed";
107
+ deploying: "deploying";
107
108
  }>;
108
109
  error: z.ZodOptional<z.ZodString>;
109
110
  phase: z.ZodOptional<z.ZodEnum<{
@@ -147,6 +148,21 @@ export declare const DeploymentEventSchema: z.ZodObject<{
147
148
  }, z.core.$strict>;
148
149
  requiredSecrets: z.ZodOptional<z.ZodArray<z.ZodString>>;
149
150
  }, z.core.$strict>>;
151
+ trailMigration: z.ZodOptional<z.ZodObject<{
152
+ accountId: z.ZodString;
153
+ accountName: z.ZodString;
154
+ phase: z.ZodEnum<{
155
+ "verify-delivery": "verify-delivery";
156
+ decommission: "decommission";
157
+ }>;
158
+ status: z.ZodEnum<{
159
+ started: "started";
160
+ completed: "completed";
161
+ blocked: "blocked";
162
+ failed: "failed";
163
+ }>;
164
+ detail: z.ZodOptional<z.ZodString>;
165
+ }, z.core.$strict>>;
150
166
  log: z.ZodOptional<z.ZodObject<{
151
167
  message: z.ZodString;
152
168
  level: z.ZodEnum<{
@@ -1 +1 @@
1
- import{z as e}from"zod";const r=["security","network","compute","database","storage","monitoring","dns","identity","bootstrap","events","registry","backup"],n=["platform","domains","accounts"],c=["started","deploying","completed","failed"],i=["step","resource","docker","ecs","error","complete","cascade_phase","cascade_account","cascade_missing_accounts","parallel_phase","detection","log"],m={step:"step",resource:"resource",docker:"docker",ecs:"ecs",error:"error",complete:null,cascade_phase:"cascadePhase",cascade_account:"cascadeAccount",cascade_missing_accounts:"cascadeMissingAccounts",parallel_phase:"parallelPhase",detection:"detection",log:"log"},p=e.object({type:e.enum(i),timestamp:e.string().max(64),sequence:e.number().int().nonnegative().optional(),step:e.object({id:e.string().max(256),name:e.string().max(256),index:e.number(),total:e.number(),status:e.string().max(64)}).strict().optional(),resource:e.object({logicalId:e.string().max(256),resourceType:e.string().max(256),category:e.enum(r),group:e.string().max(128).optional(),constructPath:e.string().max(512).optional(),displayName:e.string().max(256),status:e.string().max(64),statusReason:e.string().max(2048).optional(),physicalId:e.string().max(2048).optional(),expectedDurationSeconds:e.number().optional(),stack:e.string().max(256).optional()}).strict().optional(),docker:e.object({message:e.string().max(2048),percentage:e.number().optional()}).strict().optional(),ecs:e.object({status:e.string().max(64),message:e.string().max(2048).optional(),percentage:e.number().optional()}).strict().optional(),error:e.object({message:e.string().max(4096),category:e.string().max(128).optional(),remediation:e.array(e.string().max(1024)).max(10).optional()}).strict().optional(),message:e.string().max(2048).optional(),cascadePhase:e.object({phase:e.enum(n),status:e.enum(["started","completed"])}).strict().optional(),cascadeAccount:e.object({accountId:e.string().max(32),region:e.string().max(32),operationKey:e.string().max(256),status:e.enum(c),error:e.string().max(2048).optional(),phase:e.enum(["bootstrap","synth","deploy","destroy"]).optional(),cascadePhase:e.enum(n).optional()}).strict().optional(),cascadeMissingAccounts:e.object({accountNames:e.array(e.string().max(256)).max(256)}).strict().optional(),parallelPhase:e.object({stacks:e.array(e.string().max(256)).max(20),status:e.enum(["started","completed"]),results:e.array(e.object({stack:e.string().max(256),success:e.boolean(),error:e.string().max(2048).optional()}).strict()).max(20).optional()}).strict().optional(),detection:e.object({pattern:e.string().max(128).nullable(),hasDockerfile:e.boolean(),hasDifferences:e.boolean(),resources:e.object({hasNetwork:e.boolean(),hasCompute:e.boolean(),hasDatabase:e.boolean(),hasStorage:e.boolean(),hasMessaging:e.boolean(),hasCdn:e.boolean()}).strict(),requiredSecrets:e.array(e.string().max(512)).max(100).optional()}).strict().optional(),log:e.object({message:e.string().max(2048),level:e.enum(["info","debug","warn"])}).strict().optional()}).strict().superRefine((t,s)=>{const a=m[t.type],o=a?t[a]:void 0;a&&o==null&&s.addIssue({code:e.ZodIssueCode.custom,message:`"${a}" is required when type is "${t.type}"`,path:[a]}),t.type==="complete"&&!t.message&&s.addIssue({code:e.ZodIssueCode.custom,message:'"message" is required when type is "complete"',path:["message"]})});function u(t){if(t==="platform")return"platform";if(t==="account")return"accounts"}export{c as CASCADE_ACCOUNT_STATUSES,n as CASCADE_PHASES,r as DEPLOYMENT_EVENT_RESOURCE_CATEGORIES,i as DEPLOYMENT_EVENT_TYPES,p as DeploymentEventSchema,u as toCascadePhase};
1
+ import{z as e}from"zod";import{TRAIL_MIGRATION_PHASES as r,TRAIL_MIGRATION_STATUSES as i}from"./events.js";const c=["security","network","compute","database","storage","monitoring","dns","identity","bootstrap","events","registry","backup"],n=["platform","domains","accounts"],m=["started","deploying","completed","failed"],l=["step","resource","docker","ecs","error","complete","cascade_phase","cascade_account","cascade_missing_accounts","parallel_phase","detection","trail_migration","log"],p={step:"step",resource:"resource",docker:"docker",ecs:"ecs",error:"error",complete:null,cascade_phase:"cascadePhase",cascade_account:"cascadeAccount",cascade_missing_accounts:"cascadeMissingAccounts",parallel_phase:"parallelPhase",detection:"detection",trail_migration:"trailMigration",log:"log"},d=e.object({type:e.enum(l),timestamp:e.string().max(64),sequence:e.number().int().nonnegative().optional(),step:e.object({id:e.string().max(256),name:e.string().max(256),index:e.number(),total:e.number(),status:e.string().max(64)}).strict().optional(),resource:e.object({logicalId:e.string().max(256),resourceType:e.string().max(256),category:e.enum(c),group:e.string().max(128).optional(),constructPath:e.string().max(512).optional(),displayName:e.string().max(256),status:e.string().max(64),statusReason:e.string().max(2048).optional(),physicalId:e.string().max(2048).optional(),expectedDurationSeconds:e.number().optional(),stack:e.string().max(256).optional()}).strict().optional(),docker:e.object({message:e.string().max(2048),percentage:e.number().optional()}).strict().optional(),ecs:e.object({status:e.string().max(64),message:e.string().max(2048).optional(),percentage:e.number().optional()}).strict().optional(),error:e.object({message:e.string().max(4096),category:e.string().max(128).optional(),remediation:e.array(e.string().max(1024)).max(10).optional()}).strict().optional(),message:e.string().max(2048).optional(),cascadePhase:e.object({phase:e.enum(n),status:e.enum(["started","completed"])}).strict().optional(),cascadeAccount:e.object({accountId:e.string().max(32),region:e.string().max(32),operationKey:e.string().max(256),status:e.enum(m),error:e.string().max(2048).optional(),phase:e.enum(["bootstrap","synth","deploy","destroy"]).optional(),cascadePhase:e.enum(n).optional()}).strict().optional(),cascadeMissingAccounts:e.object({accountNames:e.array(e.string().max(256)).max(256)}).strict().optional(),parallelPhase:e.object({stacks:e.array(e.string().max(256)).max(20),status:e.enum(["started","completed"]),results:e.array(e.object({stack:e.string().max(256),success:e.boolean(),error:e.string().max(2048).optional()}).strict()).max(20).optional()}).strict().optional(),detection:e.object({pattern:e.string().max(128).nullable(),hasDockerfile:e.boolean(),hasDifferences:e.boolean(),resources:e.object({hasNetwork:e.boolean(),hasCompute:e.boolean(),hasDatabase:e.boolean(),hasStorage:e.boolean(),hasMessaging:e.boolean(),hasCdn:e.boolean()}).strict(),requiredSecrets:e.array(e.string().max(512)).max(100).optional()}).strict().optional(),trailMigration:e.object({accountId:e.string().max(32),accountName:e.string().max(256),phase:e.enum(r),status:e.enum(i),detail:e.string().max(2048).optional()}).strict().optional(),log:e.object({message:e.string().max(2048),level:e.enum(["info","debug","warn"])}).strict().optional()}).strict().superRefine((t,o)=>{const a=p[t.type],s=a?t[a]:void 0;a&&s==null&&o.addIssue({code:e.ZodIssueCode.custom,message:`"${a}" is required when type is "${t.type}"`,path:[a]}),t.type==="complete"&&!t.message&&o.addIssue({code:e.ZodIssueCode.custom,message:'"message" is required when type is "complete"',path:["message"]})});function x(t){if(t==="platform")return"platform";if(t==="account")return"accounts"}export{m as CASCADE_ACCOUNT_STATUSES,n as CASCADE_PHASES,c as DEPLOYMENT_EVENT_RESOURCE_CATEGORIES,l as DEPLOYMENT_EVENT_TYPES,d as DeploymentEventSchema,x as toCascadePhase};
@@ -88,3 +88,15 @@ export interface MigrationsCompleteEvent {
88
88
  /** ECS stoppedReason verbatim when success=false. */
89
89
  reason?: string;
90
90
  }
91
+ export declare const TRAIL_MIGRATION_PHASES: readonly ["verify-delivery", "decommission"];
92
+ export type TrailMigrationPhase = (typeof TRAIL_MIGRATION_PHASES)[number];
93
+ export declare const TRAIL_MIGRATION_STATUSES: readonly ["started", "completed", "blocked", "failed"];
94
+ export type TrailMigrationStatus = (typeof TRAIL_MIGRATION_STATUSES)[number];
95
+ export interface TrailMigrationPhaseEvent {
96
+ accountId: string;
97
+ accountName: string;
98
+ phase: TrailMigrationPhase;
99
+ status: TrailMigrationStatus;
100
+ /** Pre-masked human-readable detail (block reason, failure summary). */
101
+ detail?: string;
102
+ }
@@ -0,0 +1 @@
1
+ const e=["verify-delivery","decommission"],o=["started","completed","blocked","failed"];export{e as TRAIL_MIGRATION_PHASES,o as TRAIL_MIGRATION_STATUSES};
@@ -2,7 +2,8 @@ export { DeploymentEventSchema, DEPLOYMENT_EVENT_TYPES, DEPLOYMENT_EVENT_RESOURC
2
2
  export type { DeploymentEvent, DeploymentEventType, DeploymentEventResourceCategory, DeploymentEventCascadePhase, DeploymentEventCascadeAccountStatus } from "./deploymentEventSchema.js";
3
3
  export type { AwsCredentials, DeployIdentity } from "./credentials.js";
4
4
  export type { DeployCallbacks, StepCompleteStatus } from "./callbacks.js";
5
- export type { ProgressEvent, ProgressEventType, ResourceEvent, AwsAuthResult, CascadeDeploymentResult, CascadePhase, BuildPushStartEvent, BuildPushProgressEvent, BuildPushCompleteEvent, TaskDefRegisteredEvent, ECSCompleteEvent, MigrationsStartEvent, MigrationsCompleteEvent } from "./events.js";
5
+ export type { ProgressEvent, ProgressEventType, ResourceEvent, AwsAuthResult, CascadeDeploymentResult, CascadePhase, BuildPushStartEvent, BuildPushProgressEvent, BuildPushCompleteEvent, TaskDefRegisteredEvent, ECSCompleteEvent, MigrationsStartEvent, MigrationsCompleteEvent, TrailMigrationPhase, TrailMigrationStatus, TrailMigrationPhaseEvent } from "./events.js";
6
+ export { TRAIL_MIGRATION_PHASES, TRAIL_MIGRATION_STATUSES } from "./events.js";
6
7
  export type { ApiClientInterface, EntitlementsData } from "./apiClient.js";
7
8
  export type { DeployParams, DeployOptions, DeploymentType, DeployResult, DestroyParams, DestroyOptions, DestroyResult } from "./params.js";
8
9
  export type { OrgConfig, ProviderAccount, SSOSession } from "./orgConfig.js";
@@ -1 +1 @@
1
- import{DeploymentEventSchema as r,DEPLOYMENT_EVENT_TYPES as E,DEPLOYMENT_EVENT_RESOURCE_CATEGORIES as a,CASCADE_PHASES as o,CASCADE_ACCOUNT_STATUSES as A,toCascadePhase as S}from"./deploymentEventSchema.js";import{stubCallerIdentity as T}from"./deployment/index.js";import{ProgressReporter as _}from"./ProgressEvent.js";import{APPLICATION_STACKS as i,ORGANISATION_TYPES as N,APPLICATION_DEPLOY_ORDER as R,APPLICATION_DESTROY_ORDER as l,OPENNEXT_DEPLOY_ORDER as D,OPENNEXT_DESTROY_ORDER as m,PARALLEL_DEPLOY_GROUPS as n,PARALLEL_DESTROY_GROUPS as s,OPENNEXT_PARALLEL_GROUPS as c,isApplicationOperation as C,isOrganisationOperation as L,getParallelDeployGroups as I,getParallelDestroyGroups as f,getApplicationDeployOrder as x,getApplicationDestroyOrder as d,getApplicationStackName as F,getOrganisationStackName as g,isApplicationStack as U,getApplicationStepName as Y,getApplicationStepId as y,toPascalCase as M}from"./operations.js";import{PARALLEL_OPERATION_TYPES as h}from"./deployment/index.js";import{isOpenNextPattern as K,OPENNEXT_PATTERNS as k}from"./patternTypes.js";import{deriveResourcesFromManifestStacks as H}from"./patternDetection.js";import{detectPattern as v}from"./patternDetection.js";import{detectPayloadPattern as V}from"./patternDetection.js";import{detectDatabase as q}from"./patternDetection.js";import{STACK_NOT_FOUND_PATTERN as B,STACK_FAILED_STATE_PATTERN as J,CDK_NO_STACKS_MATCH as Q,INFRASTRUCTURE_FILENAME as W}from"./constants.js";import{ApplicationError as $,wrapApplicationError as ee}from"./application/index.js";import{FjallStateFileSchema as re,readStateFile as Ee,writeStateFile as ae,createEmptyState as oe,deleteStateFile as Ae,updateTemplateHash as Se,getStateFilePath as pe}from"./FjallState.js";import{STEP_IDS as Pe,STEP_NAMES as _e,INFRASTRUCTURE_STEP_NAMES as Oe,INFRA_STEP_NAME as ie}from"./stepDefinitions.js";export{R as APPLICATION_DEPLOY_ORDER,l as APPLICATION_DESTROY_ORDER,i as APPLICATION_STACKS,$ as ApplicationError,A as CASCADE_ACCOUNT_STATUSES,o as CASCADE_PHASES,Q as CDK_NO_STACKS_MATCH,a as DEPLOYMENT_EVENT_RESOURCE_CATEGORIES,E as DEPLOYMENT_EVENT_TYPES,r as DeploymentEventSchema,re as FjallStateFileSchema,W as INFRASTRUCTURE_FILENAME,Oe as INFRASTRUCTURE_STEP_NAMES,ie as INFRA_STEP_NAME,D as OPENNEXT_DEPLOY_ORDER,m as OPENNEXT_DESTROY_ORDER,c as OPENNEXT_PARALLEL_GROUPS,k as OPENNEXT_PATTERNS,N as ORGANISATION_TYPES,n as PARALLEL_DEPLOY_GROUPS,s as PARALLEL_DESTROY_GROUPS,h as PARALLEL_OPERATION_TYPES,_ as ProgressReporter,J as STACK_FAILED_STATE_PATTERN,B as STACK_NOT_FOUND_PATTERN,Pe as STEP_IDS,_e as STEP_NAMES,oe as createEmptyState,Ae as deleteStateFile,H as deriveResourcesFromManifestStacks,q as detectDatabase,v as detectPattern,V as detectPayloadPattern,x as getApplicationDeployOrder,d as getApplicationDestroyOrder,F as getApplicationStackName,y as getApplicationStepId,Y as getApplicationStepName,g as getOrganisationStackName,I as getParallelDeployGroups,f as getParallelDestroyGroups,pe as getStateFilePath,C as isApplicationOperation,U as isApplicationStack,K as isOpenNextPattern,L as isOrganisationOperation,Ee as readStateFile,T as stubCallerIdentity,S as toCascadePhase,M as toPascalCase,Se as updateTemplateHash,ee as wrapApplicationError,ae as writeStateFile};
1
+ import{DeploymentEventSchema as r,DEPLOYMENT_EVENT_TYPES as E,DEPLOYMENT_EVENT_RESOURCE_CATEGORIES as o,CASCADE_PHASES as a,CASCADE_ACCOUNT_STATUSES as A,toCascadePhase as S}from"./deploymentEventSchema.js";import{TRAIL_MIGRATION_PHASES as p,TRAIL_MIGRATION_STATUSES as _}from"./events.js";import{stubCallerIdentity as O}from"./deployment/index.js";import{ProgressReporter as N}from"./ProgressEvent.js";import{APPLICATION_STACKS as l,ORGANISATION_TYPES as m,APPLICATION_DEPLOY_ORDER as D,APPLICATION_DESTROY_ORDER as I,OPENNEXT_DEPLOY_ORDER as L,OPENNEXT_DESTROY_ORDER as n,PARALLEL_DEPLOY_GROUPS as s,PARALLEL_DESTROY_GROUPS as c,OPENNEXT_PARALLEL_GROUPS as C,isApplicationOperation as f,isOrganisationOperation as x,getParallelDeployGroups as d,getParallelDestroyGroups as F,getApplicationDeployOrder as g,getApplicationDestroyOrder as U,getApplicationStackName as Y,getOrganisationStackName as M,isApplicationStack as G,getApplicationStepName as y,getApplicationStepId as h,toPascalCase as u}from"./operations.js";import{PARALLEL_OPERATION_TYPES as k}from"./deployment/index.js";import{isOpenNextPattern as X,OPENNEXT_PATTERNS as b}from"./patternTypes.js";import{deriveResourcesFromManifestStacks as w}from"./patternDetection.js";import{detectPattern as j}from"./patternDetection.js";import{detectPayloadPattern as z}from"./patternDetection.js";import{detectDatabase as J}from"./patternDetection.js";import{STACK_NOT_FOUND_PATTERN as W,STACK_FAILED_STATE_PATTERN as Z,CDK_NO_STACKS_MATCH as $,INFRASTRUCTURE_FILENAME as ee}from"./constants.js";import{ApplicationError as re,wrapApplicationError as Ee}from"./application/index.js";import{FjallStateFileSchema as ae,readStateFile as Ae,writeStateFile as Se,createEmptyState as Te,deleteStateFile as pe,updateTemplateHash as _e,getStateFilePath as Pe}from"./FjallState.js";import{STEP_IDS as Re,STEP_NAMES as Ne,INFRASTRUCTURE_STEP_NAMES as ie,INFRA_STEP_NAME as le}from"./stepDefinitions.js";export{D as APPLICATION_DEPLOY_ORDER,I as APPLICATION_DESTROY_ORDER,l as APPLICATION_STACKS,re as ApplicationError,A as CASCADE_ACCOUNT_STATUSES,a as CASCADE_PHASES,$ as CDK_NO_STACKS_MATCH,o as DEPLOYMENT_EVENT_RESOURCE_CATEGORIES,E as DEPLOYMENT_EVENT_TYPES,r as DeploymentEventSchema,ae as FjallStateFileSchema,ee as INFRASTRUCTURE_FILENAME,ie as INFRASTRUCTURE_STEP_NAMES,le as INFRA_STEP_NAME,L as OPENNEXT_DEPLOY_ORDER,n as OPENNEXT_DESTROY_ORDER,C as OPENNEXT_PARALLEL_GROUPS,b as OPENNEXT_PATTERNS,m as ORGANISATION_TYPES,s as PARALLEL_DEPLOY_GROUPS,c as PARALLEL_DESTROY_GROUPS,k as PARALLEL_OPERATION_TYPES,N as ProgressReporter,Z as STACK_FAILED_STATE_PATTERN,W as STACK_NOT_FOUND_PATTERN,Re as STEP_IDS,Ne as STEP_NAMES,p as TRAIL_MIGRATION_PHASES,_ as TRAIL_MIGRATION_STATUSES,Te as createEmptyState,pe as deleteStateFile,w as deriveResourcesFromManifestStacks,J as detectDatabase,j as detectPattern,z as detectPayloadPattern,g as getApplicationDeployOrder,U as getApplicationDestroyOrder,Y as getApplicationStackName,h as getApplicationStepId,y as getApplicationStepName,M as getOrganisationStackName,d as getParallelDeployGroups,F as getParallelDestroyGroups,Pe as getStateFilePath,f as isApplicationOperation,G as isApplicationStack,X as isOpenNextPattern,x as isOrganisationOperation,Ae as readStateFile,O as stubCallerIdentity,S as toCascadePhase,u as toPascalCase,_e as updateTemplateHash,Ee as wrapApplicationError,Se as writeStateFile};
@@ -49,6 +49,12 @@ export interface DeployParams {
49
49
  * If omitted, the domains cascade phase is skipped.
50
50
  */
51
51
  domainProvider?: DomainDeployProvider;
52
+ /**
53
+ * Caller shutdown signal. Long-running SDK probes (org-trail delivery
54
+ * verification, member-trail cleanup) compose this with their own
55
+ * per-request timeouts so SIGTERM is not stalled by an in-flight call.
56
+ */
57
+ abortSignal?: AbortSignal;
52
58
  }
53
59
  export interface DeployOptions {
54
60
  skipConfirmation?: boolean;
@@ -1,4 +1,5 @@
1
1
  export { DANGEROUS_ENV_VARS, filterDangerousEnvVars, maskSensitiveOutput, parseShellArgs, sleep, singleton } from "@fjall/util";
2
2
  export { hasDockerfile } from "./dockerfileDetection.js";
3
+ export { sleepAbortable } from "./sleepAbortable.js";
3
4
  export { createSequencedCallbacks } from "./sequencedCallbacks.js";
4
5
  export type { CallbackMetadata, SequencedCallbackEvent, SequencedCallbacksResult } from "./sequencedCallbacks.js";
@@ -1 +1 @@
1
- import{DANGEROUS_ENV_VARS as s,filterDangerousEnvVars as o,maskSensitiveOutput as t,parseShellArgs as a,sleep as l,singleton as n}from"@fjall/util";import{hasDockerfile as f}from"./dockerfileDetection.js";import{createSequencedCallbacks as S}from"./sequencedCallbacks.js";export{s as DANGEROUS_ENV_VARS,S as createSequencedCallbacks,o as filterDangerousEnvVars,f as hasDockerfile,t as maskSensitiveOutput,a as parseShellArgs,n as singleton,l as sleep};
1
+ import{DANGEROUS_ENV_VARS as o,filterDangerousEnvVars as s,maskSensitiveOutput as t,parseShellArgs as l,sleep as a,singleton as p}from"@fjall/util";import{hasDockerfile as n}from"./dockerfileDetection.js";import{sleepAbortable as m}from"./sleepAbortable.js";import{createSequencedCallbacks as c}from"./sequencedCallbacks.js";export{o as DANGEROUS_ENV_VARS,c as createSequencedCallbacks,s as filterDangerousEnvVars,n as hasDockerfile,t as maskSensitiveOutput,l as parseShellArgs,p as singleton,a as sleep,m as sleepAbortable};
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Promise-based sleep that short-circuits when the supplied abort signal
3
+ * fires. Long-running poll loops MUST use this for inter-poll delays so a
4
+ * shutdown signal (e.g. SIGTERM in the webapp deployment worker) does not
5
+ * stall for the full sleep duration. Resolves (never rejects) on abort;
6
+ * callers check `signal.aborted` after waking.
7
+ */
8
+ export declare function sleepAbortable(ms: number, signal?: AbortSignal): Promise<void>;
@@ -0,0 +1 @@
1
+ function i(o,e){return e?.aborted===!0?Promise.resolve():new Promise(t=>{const n=setTimeout(()=>{e?.removeEventListener("abort",r),t()},o),r=()=>{clearTimeout(n),t()};e?.addEventListener("abort",r,{once:!0})})}export{i as sleepAbortable};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/deploy-core",
3
- "version": "2.11.1",
3
+ "version": "2.13.0",
4
4
  "description": "Shared deployment engine for Fjall — used by CLI and webapp worker",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",
@@ -65,17 +65,19 @@
65
65
  "dependencies": {
66
66
  "@aws-sdk/client-backup": "^3.1038.0",
67
67
  "@aws-sdk/client-cloudformation": "^3.1038.0",
68
+ "@aws-sdk/client-cloudtrail": "^3.1065.0",
68
69
  "@aws-sdk/client-cost-explorer": "^3.1038.0",
69
70
  "@aws-sdk/client-ec2": "^3.1038.0",
70
71
  "@aws-sdk/client-ecr": "^3.1039.0",
72
+ "@aws-sdk/client-kms": "^3.1065.0",
71
73
  "@aws-sdk/client-organizations": "^3.1038.0",
72
74
  "@aws-sdk/client-ram": "^3.1038.0",
73
75
  "@aws-sdk/client-s3": "^3.1038.0",
74
76
  "@aws-sdk/client-secrets-manager": "^3.1038.0",
75
77
  "@aws-sdk/client-sso-admin": "^3.1038.0",
76
78
  "@aws-sdk/client-sts": "^3.1038.0",
77
- "@fjall/generator": "^2.11.1",
78
- "@fjall/util": "^2.11.1",
79
+ "@fjall/generator": "^2.13.0",
80
+ "@fjall/util": "^2.13.0",
79
81
  "@smithy/node-http-handler": "^4.6.1",
80
82
  "tsx": "^4.21.0",
81
83
  "zod": "^4.4.3"
@@ -84,5 +86,5 @@
84
86
  "@types/node": "^25.6.0",
85
87
  "vitest": "^4.1.5"
86
88
  },
87
- "gitHead": "69823c3d7f2eacba419657464381119c5b5b5fd6"
89
+ "gitHead": "5b16c5731256628f829d4168c65cf165b3516f9a"
88
90
  }