@fjall/deploy-core 2.9.1 → 2.12.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.
package/dist/.minified CHANGED
@@ -1 +1 @@
1
- 120 files minified at 2026-06-02T21:42:55.155Z
1
+ 121 files minified at 2026-06-09T10:15:43.279Z
@@ -22,7 +22,7 @@ export declare function updateBackupGlobalSettings(client: BackupClient, setting
22
22
  * dead work that fails on a `backup:DescribeBackupVault` permission those
23
23
  * accounts never grant. Keep this predicate in lockstep with account.ts:122-125.
24
24
  */
25
- export declare function accountHasDisasterRecovery(environment: string | undefined, disasterRecoveryRegion: string | undefined): boolean;
25
+ export declare function accountHasDisasterRecovery(environment: string | null | undefined, disasterRecoveryRegion: string | undefined): boolean;
26
26
  /**
27
27
  * Probe whether the disaster-recovery backup vault already exists in the target
28
28
  * account/region, to decide adopt-vs-create. A retained, vault-locked DR vault
@@ -34,3 +34,31 @@ export declare function accountHasDisasterRecovery(environment: string | undefin
34
34
  * into a broken by-reference deploy.
35
35
  */
36
36
  export declare function describeBackupVaultExists(client: BackupClient): Promise<Result<boolean>>;
37
+ /**
38
+ * A backup vault that survives stack teardown (vault + KMS key are
39
+ * RemovalPolicy.RETAIN). Populated from a single DescribeBackupVault call —
40
+ * deliberately NOT paginating recovery points, which on a populated vault would
41
+ * be tens of API calls inside a destroy gate. Per-point expiry detail belongs
42
+ * in the dedicated `fjall backup status` view (G4).
43
+ */
44
+ export interface SurvivingBackupVault {
45
+ vaultName: string;
46
+ region: string;
47
+ locked: boolean;
48
+ lockDate?: Date;
49
+ lockPermanent: boolean;
50
+ recoveryPointCount: number;
51
+ encryptionKeyArn?: string;
52
+ }
53
+ /**
54
+ * Probe the fixed-name DR vault for what survives a destroy. Returns
55
+ * success(null) when no vault exists (ResourceNotFoundException); every other
56
+ * error propagates as failure for the caller to treat as best-effort.
57
+ */
58
+ export declare function describeSurvivingBackupVault(client: BackupClient, region: string): Promise<Result<SurvivingBackupVault | null>>;
59
+ /**
60
+ * Honest, non-suppressible warning for a backup vault a destroy cannot remove.
61
+ * The vault and its KMS key are RemovalPolicy.RETAIN — CloudFormation marks them
62
+ * DELETE_SKIPPED and reports the destroy clean; this makes the orphan visible.
63
+ */
64
+ export declare function formatSurvivingVaultWarning(vault: SurvivingBackupVault): string;
@@ -1 +1,2 @@
1
- import{DescribeBackupVaultCommand as c,UpdateGlobalSettingsCommand as l}from"@aws-sdk/client-backup";import{success as r,failure as n}from"@fjall/generator";import{getErrorMessage as o,maskSensitiveOutput as p}from"@fjall/util";import{SDK_TIMEOUT_MS as i}from"./types.js";const s="backupVault",d={enableCrossAccountBackup:!0,enableDelegatedAdministrator:!0,enableMpa:!1};async function S(e,t){try{const a={...d,...t},u={isCrossAccountBackupEnabled:a.enableCrossAccountBackup.toString(),isDelegatedAdministratorEnabled:a.enableDelegatedAdministrator.toString(),isMpaEnabled:a.enableMpa.toString()};return await e.send(new l({GlobalSettings:u}),{abortSignal:AbortSignal.timeout(i)}),r(void 0)}catch(a){return n(new Error(`Failed to update backup global settings: ${o(a)}`))}}function A(e,t){return t===void 0||t===""?!1:e==="production"||e==="compliance"}async function E(e){try{return await e.send(new c({BackupVaultName:s}),{abortSignal:AbortSignal.timeout(i)}),r(!0)}catch(t){return t instanceof Error&&t.name==="ResourceNotFoundException"?r(!1):n(new Error(`Failed to describe backup vault "${s}": ${p(o(t))}`))}}export{s as BACKUP_VAULT_NAME,A as accountHasDisasterRecovery,E as describeBackupVaultExists,S as updateBackupGlobalSettings};
1
+ import{DescribeBackupVaultCommand as s,UpdateGlobalSettingsCommand as p}from"@aws-sdk/client-backup";import{success as o,failure as i}from"@fjall/generator";import{getErrorMessage as c,maskSensitiveOutput as l}from"@fjall/util";import{SDK_TIMEOUT_MS as u}from"./types.js";const a="backupVault",m={enableCrossAccountBackup:!0,enableDelegatedAdministrator:!0,enableMpa:!1};async function y(e,t){try{const n={...m,...t},r={isCrossAccountBackupEnabled:n.enableCrossAccountBackup.toString(),isDelegatedAdministratorEnabled:n.enableDelegatedAdministrator.toString(),isMpaEnabled:n.enableMpa.toString()};return await e.send(new p({GlobalSettings:r}),{abortSignal:AbortSignal.timeout(u)}),o(void 0)}catch(n){return i(new Error(`Failed to update backup global settings: ${c(n)}`))}}function S(e,t){return t===void 0||t===""?!1:e==="production"||e==="compliance"}async function A(e){try{return await e.send(new s({BackupVaultName:a}),{abortSignal:AbortSignal.timeout(u)}),o(!0)}catch(t){return t instanceof Error&&t.name==="ResourceNotFoundException"?o(!1):i(new Error(`Failed to describe backup vault "${a}": ${l(c(t))}`))}}async function E(e,t){try{const n=await e.send(new s({BackupVaultName:a}),{abortSignal:AbortSignal.timeout(u)}),r=n.LockDate,d=r!==void 0&&r.getTime()<=Date.now();return o({vaultName:n.BackupVaultName??a,region:t,locked:n.Locked??!1,...r!==void 0&&{lockDate:r},lockPermanent:d,recoveryPointCount:n.NumberOfRecoveryPoints??0,...n.EncryptionKeyArn!==void 0&&{encryptionKeyArn:n.EncryptionKeyArn}})}catch(n){return n instanceof Error&&n.name==="ResourceNotFoundException"?o(null):i(new Error(`Failed to describe surviving backup vault "${a}": ${l(c(n))}`))}}function v(e){const t=e.locked?e.lockPermanent?"compliance-locked, PERMANENT \u2014 cannot be removed by anyone, including AWS or the root user":e.lockDate!==void 0?`compliance-locked, immutable from ${e.lockDate.toISOString().slice(0,10)}`:"governance-locked \u2014 removable by privileged IAM":"unlocked";return[`Backup vault "${e.vaultName}" in ${e.region} SURVIVES this destroy.`,` \u2022 Lock: ${t}`,` \u2022 Recovery points retained: ${e.recoveryPointCount}`,...e.encryptionKeyArn!==void 0?[` \u2022 Retained KMS key (also orphaned): ${e.encryptionKeyArn}`]:[]," The vault and its KMS key are RemovalPolicy.RETAIN \u2014 destroy cannot delete them."].join(`
2
+ `)}export{a as BACKUP_VAULT_NAME,S as accountHasDisasterRecovery,A as describeBackupVaultExists,E as describeSurvivingBackupVault,v as formatSurvivingVaultWarning,y as updateBackupGlobalSettings};
@@ -1 +1 @@
1
- import{CreateOrganizationalUnitCommand as A,ListOrganizationalUnitsForParentCommand as U,ListParentsCommand as L,MoveAccountCommand as P}from"@aws-sdk/client-organizations";import{success as u,failure as d}from"@fjall/generator";import{extractErrorName as y,isOULeaf as I,SDK_TIMEOUT_MS as m,AWS_ERROR_NAMES as N}from"./types.js";import{getErrorMessage as g}from"@fjall/util";async function C(r,a){const t=await r.send(new U({ParentId:a}),{abortSignal:AbortSignal.timeout(m)});let e=t.OrganizationalUnits??[],n=t.NextToken;for(;n;){const o=await r.send(new U({ParentId:a,NextToken:n}),{abortSignal:AbortSignal.timeout(m)});e=e.concat(o.OrganizationalUnits??[]),n=o.NextToken}return e}async function x(r,a){try{return(await r.send(new L({ChildId:a}),{abortSignal:AbortSignal.timeout(m)})).Parents?.[0]?.Id}catch(t){if(y(t)===N.CHILD_NOT_FOUND)return;throw t}}async function b(r,a,t,e){const n=e.find(o=>o.Name===t);if(n?.Id)return u(n.Id);try{const s=(await r.send(new A({Name:t,ParentId:a}),{abortSignal:AbortSignal.timeout(m)})).OrganizationalUnit;return s?.Id?u(s.Id):d(new Error(`OU "${t}" was created but has no ID`))}catch(o){return d(new Error(`Failed to create OU "${t}": ${g(o)}`))}}function S(r){return r.charAt(0).toUpperCase()+r.slice(1)}async function E(r,a,t){const e={};if(t.length===0)return u(e);const n=await C(r,a);for(const o of t){const s=S(o),i=await b(r,a,s,n);if(!i.success)return d(i.error);e[o.toLowerCase()]=i.data,n.push({Id:i.data,Name:s})}return u(e)}async function p(r,a,t,e,n,o){const s=await C(r,a);for(const[i,c]of Object.entries(t)){const f=S(i),w=await b(r,a,f,s);if(!w.success)return d(w.error);const O=w.data,h=n?`${n}.${i.toLowerCase()}`:i.toLowerCase();if(e[h]=O,n){const l=i.toLowerCase();o.has(l)||(e[l]=O)}if(s.push({Id:O,Name:f}),!I(c)){const l=await p(r,O,c,e,h,o);if(!l.success)return l}}return u(void 0)}async function k(r,a,t){try{if(Array.isArray(t))return await E(r,a,t);const e={},n=new Set(Object.keys(t).map(s=>s.toLowerCase())),o=await p(r,a,t,e,"",n);return o.success?u(e):d(o.error)}catch(e){return d(new Error(`Failed to ensure OUs exist: ${g(e)}`))}}function T(r,a,t=""){const e={};for(const[n,o]of Object.entries(r)){const s=t?`${t}.${n.toLowerCase()}`:n.toLowerCase(),i=a[s];if(I(o)){if(i)for(const c of o)e[c.toLowerCase()]=i}else Object.assign(e,T(o,a,s))}return e}async function v(r,a,t,e){try{if(t.length===0)return u({moved:0,alreadyPlaced:0});let n=0,o=0;for(const s of t){if(s.environment==="root")continue;const i=e?e[s.name.toLowerCase()]:a[s.environment.toLowerCase()];if(!i)continue;const c=await x(r,s.id);if(c){if(c===i){o++;continue}try{await r.send(new P({AccountId:s.id,SourceParentId:c,DestinationParentId:i}),{abortSignal:AbortSignal.timeout(m)}),n++}catch(f){if(y(f)===N.ACCOUNT_NOT_FOUND)continue;throw f}}}return u({moved:n,alreadyPlaced:o})}catch(n){return d(new Error(`Failed to place accounts in OUs: ${g(n)}`))}}export{T as buildAccountToOUMap,k as ensureOrganisationalUnitsExist,v as placeAccountsInOUs};
1
+ import{CreateOrganizationalUnitCommand as A,ListOrganizationalUnitsForParentCommand as U,ListParentsCommand as L,MoveAccountCommand as P}from"@aws-sdk/client-organizations";import{success as u,failure as d}from"@fjall/generator";import{extractErrorName as y,isOULeaf as I,SDK_TIMEOUT_MS as w,AWS_ERROR_NAMES as N}from"./types.js";import{accountTier as x,getErrorMessage as g}from"@fjall/util";async function C(r,a){const t=await r.send(new U({ParentId:a}),{abortSignal:AbortSignal.timeout(w)});let e=t.OrganizationalUnits??[],n=t.NextToken;for(;n;){const o=await r.send(new U({ParentId:a,NextToken:n}),{abortSignal:AbortSignal.timeout(w)});e=e.concat(o.OrganizationalUnits??[]),n=o.NextToken}return e}async function E(r,a){try{return(await r.send(new L({ChildId:a}),{abortSignal:AbortSignal.timeout(w)})).Parents?.[0]?.Id}catch(t){if(y(t)===N.CHILD_NOT_FOUND)return;throw t}}async function b(r,a,t,e){const n=e.find(o=>o.Name===t);if(n?.Id)return u(n.Id);try{const s=(await r.send(new A({Name:t,ParentId:a}),{abortSignal:AbortSignal.timeout(w)})).OrganizationalUnit;return s?.Id?u(s.Id):d(new Error(`OU "${t}" was created but has no ID`))}catch(o){return d(new Error(`Failed to create OU "${t}": ${g(o)}`))}}function S(r){return r.charAt(0).toUpperCase()+r.slice(1)}async function T(r,a,t){const e={};if(t.length===0)return u(e);const n=await C(r,a);for(const o of t){const s=S(o),i=await b(r,a,s,n);if(!i.success)return d(i.error);e[o.toLowerCase()]=i.data,n.push({Id:i.data,Name:s})}return u(e)}async function p(r,a,t,e,n,o){const s=await C(r,a);for(const[i,c]of Object.entries(t)){const f=S(i),m=await b(r,a,f,s);if(!m.success)return d(m.error);const O=m.data,h=n?`${n}.${i.toLowerCase()}`:i.toLowerCase();if(e[h]=O,n){const l=i.toLowerCase();o.has(l)||(e[l]=O)}if(s.push({Id:O,Name:f}),!I(c)){const l=await p(r,O,c,e,h,o);if(!l.success)return l}}return u(void 0)}async function z(r,a,t){try{if(Array.isArray(t))return await T(r,a,t);const e={},n=new Set(Object.keys(t).map(s=>s.toLowerCase())),o=await p(r,a,t,e,"",n);return o.success?u(e):d(o.error)}catch(e){return d(new Error(`Failed to ensure OUs exist: ${g(e)}`))}}function $(r,a,t=""){const e={};for(const[n,o]of Object.entries(r)){const s=t?`${t}.${n.toLowerCase()}`:n.toLowerCase(),i=a[s];if(I(o)){if(i)for(const c of o)e[c.toLowerCase()]=i}else Object.assign(e,$(o,a,s))}return e}async function M(r,a,t,e){try{if(t.length===0)return u({moved:0,alreadyPlaced:0});let n=0,o=0;for(const s of t){if(x(s)==="organisation")continue;const i=e?e[s.name.toLowerCase()]:a[s.environment.toLowerCase()];if(!i)continue;const c=await E(r,s.id);if(c){if(c===i){o++;continue}try{await r.send(new P({AccountId:s.id,SourceParentId:c,DestinationParentId:i}),{abortSignal:AbortSignal.timeout(w)}),n++}catch(f){if(y(f)===N.ACCOUNT_NOT_FOUND)continue;throw f}}}return u({moved:n,alreadyPlaced:o})}catch(n){return d(new Error(`Failed to place accounts in OUs: ${g(n)}`))}}export{$ as buildAccountToOUMap,z as ensureOrganisationalUnitsExist,M as placeAccountsInOUs};
@@ -26,5 +26,5 @@ export interface CascadeDestroyAccountResult {
26
26
  export declare function destroyCascadeAccount(params: DestroyParams, services: DeployServices, operation: OrganisationOperation, account: {
27
27
  id: string;
28
28
  name: string;
29
- environment: string;
29
+ environment: string | null;
30
30
  }, deployType: "platform" | "account", region: string, callbacks: DeployCallbacks): Promise<CascadeDestroyAccountResult>;
@@ -1 +1 @@
1
- import{logger as S}from"@fjall/util/logger";import{maskSensitiveOutput as s,getErrorMessage as E}from"@fjall/util";import{stubCallerIdentity as P}from"../types/deployment/index.js";import{CloudFormationClient as $,DescribeStacksCommand as R}from"@aws-sdk/client-cloudformation";import{NodeHttpHandler as O}from"@smithy/node-http-handler";import{ORGANISATION_TYPES as h,getOrganisationStackName as _}from"../types/operations.js";import{CdkContextBuilder as k}from"../services/supporting/CdkContextBuilder.js";import{buildParamsContext as F,assumeCascadeRole as x,forwardOutput as I}from"./contextHelpers.js";import{cleanupFailedStack as L}from"./stackCleanup.js";import{STACK_NOT_FOUND_PATTERN as M,STACK_FAILED_STATE_PATTERN as K}from"../types/constants.js";async function V(c,i,m,t,o,e,r){const n=Date.now(),a=`${t.name} (${e})`;r.onCascadeAccountStart?.(a,t.id,e,o);const f=await x(i.awsProvider,t.id,e,`fjall-cascade-destroy-${t.name}`);if(!f.success)return r.onCascadeAccountComplete?.(a,!1,s(f.error.message),e),{accountName:t.name,accountId:t.id,region:e,success:!1,duration:Date.now()-n,error:`AssumeRole failed: ${s(f.error.message)}`};const{provider:v,credentials:l}=f.data,A=k.buildDeploymentContext({deployType:o,target:m.target,path:m.path,region:e,accountName:t.name,callerIdentity:P(t.id),...F({orgConfig:c.orgConfig,identity:c.identity})},{verbose:c.options?.verbose},c.orgConfig);r.onCascadeAccountPhaseChange?.(a,"synth",e);const w=await i.cdkService.runCdkSynth(A,I(r));if(!w.success){const d=s(`Synth failed: ${w.error}`);return r.onCascadeAccountComplete?.(a,!1,d,e),{accountName:t.name,accountId:t.id,region:e,success:!1,duration:Date.now()-n,error:d}}r.onCascadeAccountPhaseChange?.(a,"destroy",e);const u=_(o==="platform"?h.PLATFORM:h.ACCOUNT),y=await i.cdkService.runCdkDestroy(A,u,I(r),d=>r.onCascadeAccountResourceProgress?.(a,d,e),v,!0,l);if(!y.success){const d=y.error;if(d.includes(K)){S.warn("cascadeDestroy",`CDK destroy failed on ${u} in failed state, retrying via CloudFormation API`,{region:e,account:t.name});try{await L(u,e,l,void 0,r)}catch(C){const T=`cleanupFailedStack threw for ${u}: ${s(E(C))}`;S.warn("cascadeDestroy",T),r.onLog?.(T,"warn")}const p=await H(u,e,l);if(p.deleted)return r.onCascadeAccountComplete?.(a,!0,void 0,e),{accountName:t.name,accountId:t.id,region:e,success:!0,duration:Date.now()-n};if(p.error){const C=s(p.error);return r.onCascadeAccountComplete?.(a,!1,C,e),{accountName:t.name,accountId:t.id,region:e,success:!1,duration:Date.now()-n,error:C}}const N=s(`Stack ${u} cleanup attempted but stack still exists in ${e}`);return r.onCascadeAccountComplete?.(a,!1,N,e),{accountName:t.name,accountId:t.id,region:e,success:!1,duration:Date.now()-n,error:N}}const D=s(d);return r.onCascadeAccountComplete?.(a,!1,D,e),{accountName:t.name,accountId:t.id,region:e,success:!1,duration:Date.now()-n,error:D}}return r.onCascadeAccountComplete?.(a,!0,void 0,e),{accountName:t.name,accountId:t.id,region:e,success:!0,duration:Date.now()-n}}async function H(c,i,m){try{const e=(await new $({region:i,credentials:m,requestHandler:new O({requestTimeout:15e3})}).send(new R({StackName:c}))).Stacks?.[0]?.StackStatus;return!e||e==="DELETE_COMPLETE"?{deleted:!0}:{deleted:!1,error:`Stack still in ${e} after cleanup attempt`}}catch(t){if(t instanceof Error&&t.message?.includes(M))return{deleted:!0};const o=s(E(t));return S.debug("cascadeDestroy","Stack verification failed",{error:o}),{deleted:!1,error:`Stack verification failed: ${o}`}}}export{V as destroyCascadeAccount};
1
+ import{logger as f}from"@fjall/util/logger";import{maskSensitiveOutput as s,getErrorMessage as y}from"@fjall/util";import{stubCallerIdentity as E}from"../types/deployment/index.js";import{CloudFormationClient as I,DescribeStacksCommand as R}from"@aws-sdk/client-cloudformation";import{BackupClient as $}from"@aws-sdk/client-backup";import{accountHasDisasterRecovery as g,describeSurvivingBackupVault as O,formatSurvivingVaultWarning as _}from"../aws/organisations/backup.js";import{NodeHttpHandler as B}from"@smithy/node-http-handler";import{ORGANISATION_TYPES as k,getOrganisationStackName as F}from"../types/operations.js";import{CdkContextBuilder as x}from"../services/supporting/CdkContextBuilder.js";import{buildParamsContext as L,assumeCascadeRole as M,forwardOutput as T}from"./contextHelpers.js";import{cleanupFailedStack as H}from"./stackCleanup.js";import{STACK_NOT_FOUND_PATTERN as K,STACK_FAILED_STATE_PATTERN as V}from"../types/constants.js";async function re(n,o,i,e,c,t,r){const d=Date.now(),a=`${e.name} (${t})`;r.onCascadeAccountStart?.(a,e.id,t,c);const l=await M(o.awsProvider,e.id,t,`fjall-cascade-destroy-${e.name}`);if(!l.success)return r.onCascadeAccountComplete?.(a,!1,s(l.error.message),t),{accountName:e.name,accountId:e.id,region:t,success:!1,duration:Date.now()-d,error:`AssumeRole failed: ${s(l.error.message)}`};const{provider:w,credentials:p}=l.data,S=x.buildDeploymentContext({deployType:c,target:i.target,path:i.path,region:t,accountName:e.name,callerIdentity:E(e.id),...L({orgConfig:n.orgConfig,identity:n.identity})},{verbose:n.options?.verbose},n.orgConfig);r.onCascadeAccountPhaseChange?.(a,"synth",t);const A=await o.cdkService.runCdkSynth(S,T(r));if(!A.success){const u=s(`Synth failed: ${A.error}`);return r.onCascadeAccountComplete?.(a,!1,u,t),{accountName:e.name,accountId:e.id,region:t,success:!1,duration:Date.now()-d,error:u}}r.onCascadeAccountPhaseChange?.(a,"destroy",t);const m=F(c==="platform"?k.PLATFORM:k.ACCOUNT);g(e.environment,n.orgConfig?.disasterRecoveryRegion)&&await q(w.getClient($),t,r);const D=await o.cdkService.runCdkDestroy(S,m,T(r),u=>r.onCascadeAccountResourceProgress?.(a,u,t),w,!0,p);if(!D.success){const u=D.error;if(u.includes(V)){f.warn("cascadeDestroy",`CDK destroy failed on ${m} in failed state, retrying via CloudFormation API`,{region:t,account:e.name});try{await H(m,t,p,void 0,r)}catch(C){const h=`cleanupFailedStack threw for ${m}: ${s(y(C))}`;f.warn("cascadeDestroy",h),r.onLog?.(h,"warn")}const v=await U(m,t,p);if(v.deleted)return r.onCascadeAccountComplete?.(a,!0,void 0,t),{accountName:e.name,accountId:e.id,region:t,success:!0,duration:Date.now()-d};if(v.error){const C=s(v.error);return r.onCascadeAccountComplete?.(a,!1,C,t),{accountName:e.name,accountId:e.id,region:t,success:!1,duration:Date.now()-d,error:C}}const P=s(`Stack ${m} cleanup attempted but stack still exists in ${t}`);return r.onCascadeAccountComplete?.(a,!1,P,t),{accountName:e.name,accountId:e.id,region:t,success:!1,duration:Date.now()-d,error:P}}const N=s(u);return r.onCascadeAccountComplete?.(a,!1,N,t),{accountName:e.name,accountId:e.id,region:t,success:!1,duration:Date.now()-d,error:N}}return r.onCascadeAccountComplete?.(a,!0,void 0,t),{accountName:e.name,accountId:e.id,region:t,success:!0,duration:Date.now()-d}}async function q(n,o,i){try{const e=await O(n,o);if(!e.success){f.debug("cascadeDestroy","Backup-vault survival probe failed",{region:o,error:s(e.error.message)});return}if(e.data===null)return;i.onProgress?.({type:"warning",message:_(e.data),metadata:{source:"backup-vault-survival"}}),f.warn("cascadeDestroy","Backup vault survives destroy",{region:o,vaultName:e.data.vaultName,recoveryPointCount:e.data.recoveryPointCount,lockPermanent:e.data.lockPermanent})}catch(e){f.debug("cascadeDestroy","Backup-vault survival probe threw",{region:o,error:s(y(e))})}}async function U(n,o,i){try{const t=(await new I({region:o,credentials:i,requestHandler:new B({requestTimeout:15e3})}).send(new R({StackName:n}))).Stacks?.[0]?.StackStatus;return!t||t==="DELETE_COMPLETE"?{deleted:!0}:{deleted:!1,error:`Stack still in ${t} after cleanup attempt`}}catch(e){if(e instanceof Error&&e.message?.includes(K))return{deleted:!0};const c=s(y(e));return f.debug("cascadeDestroy","Stack verification failed",{error:c}),{deleted:!1,error:`Stack verification failed: ${c}`}}}export{re as destroyCascadeAccount};
@@ -1 +1 @@
1
- import{join as T}from"path";import{success as L,failure as C}from"@fjall/generator";import{logger as R}from"@fjall/util/logger";import{maskSensitiveOutput as m}from"@fjall/util";import{ORGANISATION_TYPES as h,getOrganisationStackName as M}from"../types/operations.js";import{CdkContextBuilder as V}from"../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as _}from"../types/deployment/index.js";import{CloudFormationService as B}from"../services/infrastructure/CloudFormationService.js";import{getCascadeStateFilePath as G}from"../types/FjallState.js";import{BackupClient as z}from"@aws-sdk/client-backup";import{accountHasDisasterRecovery as K,describeBackupVaultExists as Y}from"../aws/organisations/backup.js";import{buildParamsContext as X,collectStackOutputs as U,assumeCascadeRole as b,forwardOutput as k}from"./contextHelpers.js";import{STRUCTURAL_ENVIRONMENTS as v}from"@fjall/util";import{DEFAULT_REGION as q}from"../aws/utils/regions.js";const de=4;function pe(s){const n=s.find(e=>e.environment===v.PLATFORM),i=s.filter(e=>e.environment!==v.ROOT&&e.environment!==v.PLATFORM);return{platformAccount:n,memberAccounts:i}}function me(s){const n=s?.primaryRegion??q,i=s?.secondaryRegions??[],e=[n,...i],a=s?.disasterRecoveryRegion;return a&&!e.includes(a)&&e.push(a),e}function fe(s,n){const i=[];for(const e of n)for(const a of s)i.push({account:a,region:e});return i}import{buildCascadeRoleArn as Ae}from"./contextHelpers.js";async function le(s,n,i,e,a,r,u){const t=u?.region??e.region??n.awsProvider.getRegion(),o=`${e.name} (${t})`,c=u?.orgConfig??s.orgConfig,f=u?.ipamPoolId;r.onCascadeAccountStart?.(o,e.id,t,a);const d=await b(n.awsProvider,e.id,t,`fjall-cascade-${e.name}`);if(!d.success)return r.onCascadeAccountComplete?.(o,!1,m(d.error.message),t),C(new Error(`Failed to assume role for ${e.name}: ${m(d.error.message)}`));const{provider:g,credentials:y}=d.data,P=T(s.workingDirectory,"fjall",a==="platform"?h.PLATFORM:h.ACCOUNT),j=t.replace(/-/g,""),D=T(P,`cdk.out.${e.id}.${j}`),A=V.buildDeploymentContext({deployType:a,target:i.target,path:P,assemblyDir:D,environment:e.environment,region:t,accountName:e.name,callerIdentity:_(e.id),ipamPoolId:f,...X({orgConfig:c,identity:s.identity,skipOidc:s.options?.skipOidc,skipAccountGlobals:u?.skipAccountGlobals})},{verbose:s.options?.verbose},c);if(K(e.environment,c?.disasterRecoveryRegion)){const p=await Y(g.getClient(z));if(!p.success)return r.onCascadeAccountComplete?.(o,!1,m(p.error.message),t),C(new Error(`Backup vault probe failed for ${e.name}: ${m(p.error.message)}`));A.fjallAdoptBackupVault=p.data}r.onCascadeAccountPhaseChange?.(o,"synth",t);const w=await n.cdkService.runCdkSynth(A,k(r),y);if(!w.success)return r.onCascadeAccountComplete?.(o,!1,m(`Synth failed: ${w.error}`),t),C(new Error(`Synth failed for ${e.name}: ${m(w.error)}`));const l=M(a==="platform"?h.PLATFORM:h.ACCOUNT),x=G(P,e.id,t),O=new B(g),{changed:H,currentHash:E}=await n.hashService.compareCascadeStack(D,l,x);if(!H&&s.options?.force!==!0&&await O.stackExists(l)){const p=await O.getStackOutputs(l);p.success||R.debug("cascadeHelpers","Failed to read outputs for skipped cascade account (non-critical)",{stackName:l,account:e.name});const N=U(p);return r.onLog?.(`${e.name}: no infrastructure changes \u2014 skipping deploy`,"info"),r.onCascadeAccountComplete?.(o,!0,void 0,t,N,!0),L({outputs:N,skipped:!0})}r.onCascadeAccountPhaseChange?.(o,"bootstrap",t);const S=await n.cdkService.runCdkBootstrap(A,k(r),y);if(!S.success)return r.onCascadeAccountComplete?.(o,!1,m(`Bootstrap failed: ${S.error}`),t),C(new Error(`Bootstrap failed for ${e.name}: ${m(S.error)}`));r.onCascadeAccountPhaseChange?.(o,"deploy",t);const $=await n.cdkService.runCdkDeploy(A,l,k(r),p=>r.onCascadeAccountResourceProgress?.(o,p,t),g,y);if(!$.success)return r.onCascadeAccountComplete?.(o,!1,m($.error),t),C(new Error(m($.error)));const I=await O.getStackOutputs(l);I.success||R.debug("cascadeHelpers","Failed to read cascade account stack outputs (non-critical)",{stackName:l,account:e.name});const F=U(I);return E!==void 0&&((await n.hashService.persistCascadeStack(x,l,E)).success||R.debug("cascadeHelpers","Failed to persist cascade hash state (non-critical)",{stackName:l,account:e.name})),r.onCascadeAccountComplete?.(o,!0,void 0,t,F,!1),L({outputs:F,skipped:!1})}async function ge(s,n,i){const e=new Map,a=s.awsProvider.getRegion(),r=await b(s.awsProvider,n.id,a,`fjall-ipam-read-${n.name}`);if(!r.success)return R.debug("organisationDeploy",`Cannot read Platform outputs: ${r.error.message}`),e;const u=new B(r.data.provider),t=M(h.PLATFORM),o=await u.getStackOutputs(t);if(!o.success)return R.debug("organisationDeploy",`Failed to read Platform stack outputs: ${o.error.message}`),e;const c=/^IpamPoolId(\d{12})(\w+)$/;for(const f of o.data){const d=f.OutputKey?.match(c);if(d&&f.OutputValue){const g=`${d[1]}-${d[2]}`;e.set(g,f.OutputValue)}}return e.size>0&&i.onLog?.(`Read ${e.size} IPAM pool ID(s) from Platform stack`,"info"),e}async function Ce(s,n){const i=s.getDomains();if(i.length===0)return{domainsDeployed:0,errors:[]};n.onCascadePhaseStart?.("domains");const e=i.filter(t=>t.type==="apex"),a=i.filter(t=>t.type==="delegated");let r=0;const u=[];for(const t of e){const o=await s.deployDomain(t.name,n);o.success?r++:u.push(`${t.name}: ${o.error.message}`)}if(a.length>0){const t=await Promise.allSettled(a.map(o=>s.deployDomain(o.name,n)));for(let o=0;o<t.length;o++){const c=t[o],f=a[o];if(!(!c||!f))if(c.status==="fulfilled")c.value.success?r++:u.push(`${f.name}: ${c.value.error.message}`);else{const d=c.reason instanceof Error?c.reason.message:String(c.reason);u.push(`${f.name}: ${d}`)}}}return n.onCascadePhaseComplete?.("domains"),{domainsDeployed:r,errors:u}}export{de as CASCADE_MAX_CONCURRENCY,fe as buildAccountRegionPairs,Ae as buildCascadeRoleArn,me as buildRegionList,le as deployCascadeAccount,Ce as deployDomains,pe as partitionAccounts,ge as readPlatformIpamPoolIds};
1
+ import{join as N}from"path";import{success as B,failure as C}from"@fjall/generator";import{logger as R}from"@fjall/util/logger";import{maskSensitiveOutput as m}from"@fjall/util";import{ORGANISATION_TYPES as h,getOrganisationStackName as L}from"../types/operations.js";import{CdkContextBuilder as U}from"../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as V}from"../types/deployment/index.js";import{CloudFormationService as M}from"../services/infrastructure/CloudFormationService.js";import{getCascadeStateFilePath as _}from"../types/FjallState.js";import{BackupClient as z}from"@aws-sdk/client-backup";import{accountHasDisasterRecovery as K,describeBackupVaultExists as Y}from"../aws/organisations/backup.js";import{buildParamsContext as X,collectStackOutputs as T,assumeCascadeRole as b,forwardOutput as k}from"./contextHelpers.js";import{accountTier as j}from"@fjall/util";import{DEFAULT_REGION as q}from"../aws/utils/regions.js";const de=4;function pe(r){const n=r.find(e=>j(e)==="platform"),i=r.filter(e=>j(e)==="account");return{platformAccount:n,memberAccounts:i}}function me(r){const n=r?.primaryRegion??q,i=r?.secondaryRegions??[],e=[n,...i],a=r?.disasterRecoveryRegion;return a&&!e.includes(a)&&e.push(a),e}function fe(r,n){const i=[];for(const e of n)for(const a of r)i.push({account:a,region:e});return i}import{buildCascadeRoleArn as ye}from"./contextHelpers.js";async function le(r,n,i,e,a,s,u){const t=u?.region??e.region??n.awsProvider.getRegion(),o=`${e.name} (${t})`,c=u?.orgConfig??r.orgConfig,f=u?.ipamPoolId;s.onCascadeAccountStart?.(o,e.id,t,a);const d=await b(n.awsProvider,e.id,t,`fjall-cascade-${e.name}`);if(!d.success)return s.onCascadeAccountComplete?.(o,!1,m(d.error.message),t),C(new Error(`Failed to assume role for ${e.name}: ${m(d.error.message)}`));const{provider:g,credentials:A}=d.data,P=N(r.workingDirectory,"fjall",a==="platform"?h.PLATFORM:h.ACCOUNT),H=t.replace(/-/g,""),D=N(P,`cdk.out.${e.id}.${H}`),y=U.buildDeploymentContext({deployType:a,target:i.target,path:P,assemblyDir:D,environment:e.environment??void 0,region:t,accountName:e.name,callerIdentity:V(e.id),ipamPoolId:f,...X({orgConfig:c,identity:r.identity,skipOidc:r.options?.skipOidc,skipAccountGlobals:u?.skipAccountGlobals})},{verbose:r.options?.verbose},c);if(K(e.environment,c?.disasterRecoveryRegion)){const p=await Y(g.getClient(z));if(!p.success)return s.onCascadeAccountComplete?.(o,!1,m(p.error.message),t),C(new Error(`Backup vault probe failed for ${e.name}: ${m(p.error.message)}`));y.fjallAdoptBackupVault=p.data}s.onCascadeAccountPhaseChange?.(o,"synth",t);const w=await n.cdkService.runCdkSynth(y,k(s),A);if(!w.success)return s.onCascadeAccountComplete?.(o,!1,m(`Synth failed: ${w.error}`),t),C(new Error(`Synth failed for ${e.name}: ${m(w.error)}`));const l=L(a==="platform"?h.PLATFORM:h.ACCOUNT),v=_(P,e.id,t),$=new M(g),{changed:G,currentHash:x}=await n.hashService.compareCascadeStack(D,l,v);if(!G&&r.options?.force!==!0&&await $.stackExists(l)){const p=await $.getStackOutputs(l);p.success||R.debug("cascadeHelpers","Failed to read outputs for skipped cascade account (non-critical)",{stackName:l,account:e.name});const F=T(p);return s.onLog?.(`${e.name}: no infrastructure changes \u2014 skipping deploy`,"info"),s.onCascadeAccountComplete?.(o,!0,void 0,t,F,!0),B({outputs:F,skipped:!0})}s.onCascadeAccountPhaseChange?.(o,"bootstrap",t);const S=await n.cdkService.runCdkBootstrap(y,k(s),A);if(!S.success)return s.onCascadeAccountComplete?.(o,!1,m(`Bootstrap failed: ${S.error}`),t),C(new Error(`Bootstrap failed for ${e.name}: ${m(S.error)}`));s.onCascadeAccountPhaseChange?.(o,"deploy",t);const O=await n.cdkService.runCdkDeploy(y,l,k(s),p=>s.onCascadeAccountResourceProgress?.(o,p,t),g,A);if(!O.success)return s.onCascadeAccountComplete?.(o,!1,m(O.error),t),C(new Error(m(O.error)));const I=await $.getStackOutputs(l);I.success||R.debug("cascadeHelpers","Failed to read cascade account stack outputs (non-critical)",{stackName:l,account:e.name});const E=T(I);return x!==void 0&&((await n.hashService.persistCascadeStack(v,l,x)).success||R.debug("cascadeHelpers","Failed to persist cascade hash state (non-critical)",{stackName:l,account:e.name})),s.onCascadeAccountComplete?.(o,!0,void 0,t,E,!1),B({outputs:E,skipped:!1})}async function ge(r,n,i){const e=new Map,a=r.awsProvider.getRegion(),s=await b(r.awsProvider,n.id,a,`fjall-ipam-read-${n.name}`);if(!s.success)return R.debug("organisationDeploy",`Cannot read Platform outputs: ${s.error.message}`),e;const u=new M(s.data.provider),t=L(h.PLATFORM),o=await u.getStackOutputs(t);if(!o.success)return R.debug("organisationDeploy",`Failed to read Platform stack outputs: ${o.error.message}`),e;const c=/^IpamPoolId(\d{12})(\w+)$/;for(const f of o.data){const d=f.OutputKey?.match(c);if(d&&f.OutputValue){const g=`${d[1]}-${d[2]}`;e.set(g,f.OutputValue)}}return e.size>0&&i.onLog?.(`Read ${e.size} IPAM pool ID(s) from Platform stack`,"info"),e}async function Ce(r,n){const i=r.getDomains();if(i.length===0)return{domainsDeployed:0,errors:[]};n.onCascadePhaseStart?.("domains");const e=i.filter(t=>t.type==="apex"),a=i.filter(t=>t.type==="delegated");let s=0;const u=[];for(const t of e){const o=await r.deployDomain(t.name,n);o.success?s++:u.push(`${t.name}: ${o.error.message}`)}if(a.length>0){const t=await Promise.allSettled(a.map(o=>r.deployDomain(o.name,n)));for(let o=0;o<t.length;o++){const c=t[o],f=a[o];if(!(!c||!f))if(c.status==="fulfilled")c.value.success?s++:u.push(`${f.name}: ${c.value.error.message}`);else{const d=c.reason instanceof Error?c.reason.message:String(c.reason);u.push(`${f.name}: ${d}`)}}}return n.onCascadePhaseComplete?.("domains"),{domainsDeployed:s,errors:u}}export{de as CASCADE_MAX_CONCURRENCY,fe as buildAccountRegionPairs,ye as buildCascadeRoleArn,me as buildRegionList,le as deployCascadeAccount,Ce as deployDomains,pe as partitionAccounts,ge as readPlatformIpamPoolIds};
@@ -1 +1 @@
1
- import{join as E}from"path";import{success as $,failure as d}from"@fjall/generator";import{logger as H}from"@fjall/util/logger";import{maskSensitiveOutput as u}from"@fjall/util";import{deriveResourcesFromManifestStacks as F}from"../types/patternDetection.js";import{emitProgress as C,PROGRESS_MESSAGES as D}from"../services/supporting/helpers.js";import{parseRequiredSecretsFromManifest as M}from"./manifestSecretParser.js";import{parseDockerServicesFromManifest as x}from"@fjall/util/manifest";async function K(o,t,P,e){e.onDetectionPhaseChange?.("detect","started");const f=t.frameworkRegistry.resolve({appPath:o.path}),p=f?.detection.pattern??null,y=f?.detection.hasDatabase??!1;e.onLog?.(`Pattern detected: ${p??"standard"}`,"info"),e.onDetectionPhaseChange?.("detect","completed");const r=E(o.path,"cdk.out");e.onDetectionPhaseChange?.("synth","started"),C(e,D.SYNTH);const g=await t.cdkService.runCdkSynth(P,s=>e.onCdkOutput?.(s,"synth"));if(!g.success)return d(new Error(u(`CDK synthesis failed: ${g.error}`)));e.onDetectionPhaseChange?.("synth","completed");const a=M(r);a.length>0&&e.onLog?.(`Found ${a.length} required secret path(s) in manifest`,"info"),e.onDetectionPhaseChange?.("hash","started"),C(e,D.HASH);const i=await t.hashService.getTemplateHashes(r);if(!i.success)return d(new Error(u(`Template hash computation failed: ${i.error.message}`)));const c=i.data,h=await t.hashService.compareWithState(c,o.path);if(!h.success)return d(new Error(u(`Hash comparison failed: ${h.error.message}`)));const n=h.data;e.onDetectionPhaseChange?.("hash","completed");const m=new Map(n.stackChanges);for(const[s,v]of n.stackChanges)v||await t.cfnService.stackExists(s)||(H.debug("detectionPipeline","Stale hash detected \u2014 stack missing in CFN",{stackName:s}),m.set(s,!0));const k=Array.from(c.keys()),S=F(k),w=x(r),R=S.hasCompute&&w.length>0,l=Array.from(m.values()).some(Boolean);return e.onLog?.(`Detection complete: ${n.changedCount} changed, ${n.unchangedCount} unchanged`,"info"),$({pattern:p,hasDatabase:y,hasDifferences:l,stackChanges:m,currentHashes:c,resources:S,hasDockerfile:R,requiredSecrets:a})}export{K as runDetectionPipeline};
1
+ import{join as A}from"path";import{success as N,failure as n}from"@fjall/generator";import{logger as F}from"@fjall/util/logger";import{maskSensitiveOutput as o}from"@fjall/util";import{deriveResourcesFromManifestStacks as H}from"../types/patternDetection.js";import{emitProgress as P,PROGRESS_MESSAGES as w}from"../services/supporting/helpers.js";import{parseRequiredSecretsFromManifest as M,parseImportedSecretNamesFromManifest as O}from"./manifestSecretParser.js";import{resolveSecretArns as L}from"./secretArnResolver.js";import{parseDockerServicesFromManifest as q}from"@fjall/util/manifest";async function Y(h,r,a,e){e.onDetectionPhaseChange?.("detect","started");const S=r.frameworkRegistry.resolve({appPath:h.path}),C=S?.detection.pattern??null,l=S?.detection.hasDatabase??!1;e.onLog?.(`Pattern detected: ${C??"standard"}`,"info"),e.onDetectionPhaseChange?.("detect","completed");const i=A(h.path,"cdk.out");e.onDetectionPhaseChange?.("synth","started"),P(e,w.SYNTH);const y=await r.cdkService.runCdkSynth(a,t=>e.onCdkOutput?.(t,"synth"));if(!y.success)return n(new Error(o(`CDK synthesis failed: ${y.error}`)));if(e.onDetectionPhaseChange?.("synth","completed"),a.resolvedSecretArns===void 0){const t=O(i);if(t.length>0){e.onLog?.(`Resolving ${t.length} imported secret ARN(s)\u2026`,"info");const s=await L(r.awsProvider,t);if(!s.success)return n(new Error(o(s.error.message)));a.resolvedSecretArns=JSON.stringify(s.data);const g=await r.cdkService.runCdkSynth(a,$=>e.onCdkOutput?.($,"synth"));if(!g.success)return n(new Error(o(`CDK synthesis failed: ${g.error}`)))}}const d=M(i);d.length>0&&e.onLog?.(`Found ${d.length} required secret path(s) in manifest`,"info"),e.onDetectionPhaseChange?.("hash","started"),P(e,w.HASH);const m=await r.hashService.getTemplateHashes(i);if(!m.success)return n(new Error(o(`Template hash computation failed: ${m.error.message}`)));const u=m.data,f=await r.hashService.compareWithState(u,h.path);if(!f.success)return n(new Error(o(`Hash comparison failed: ${f.error.message}`)));const c=f.data;e.onDetectionPhaseChange?.("hash","completed");const p=new Map(c.stackChanges);for(const[t,s]of c.stackChanges)s||await r.cfnService.stackExists(t)||(F.debug("detectionPipeline","Stale hash detected \u2014 stack missing in CFN",{stackName:t}),p.set(t,!0));const R=Array.from(u.keys()),D=H(R),v=q(i),k=D.hasCompute&&v.length>0,E=Array.from(p.values()).some(Boolean);return e.onLog?.(`Detection complete: ${c.changedCount} changed, ${c.unchangedCount} unchanged`,"info"),N({pattern:C,hasDatabase:l,hasDifferences:E,stackChanges:p,currentHashes:u,resources:D,hasDockerfile:k,requiredSecrets:d})}export{Y as runDetectionPipeline};
@@ -9,3 +9,23 @@
9
9
  * parsed, or contains no secrets. Never throws.
10
10
  */
11
11
  export declare function parseRequiredSecretsFromManifest(cdkOutPath: string): string[];
12
+ /**
13
+ * Extract name-form Secrets Manager imports from the Fjall manifest.
14
+ *
15
+ * Reads `fjall-manifest.json` and collects every `importedSecretNames` entry
16
+ * from ECS services and Lambda functions — the externally-managed secrets the
17
+ * construct declared `secretsImport: { id, name }` for. deploy-core resolves
18
+ * each to its complete ARN via DescribeSecret and re-injects them as CDK
19
+ * context so the construct renders an authorising complete-ARN `valueFrom`.
20
+ *
21
+ * Source-driven, not template-scanning: the name is read from the declared
22
+ * import, so it is found regardless of how CDK renders the suffixless ARN — a
23
+ * plain string in an env-bound stack (concrete account+region) or an Fn::Join
24
+ * in a token-env stack. The arn-form escape hatch never reaches this field (the
25
+ * synth-time writer excludes it), so a cross-account complete ARN is never
26
+ * handed to the fail-closed resolver.
27
+ *
28
+ * Returns an empty array if the manifest does not exist, cannot be parsed, or
29
+ * declares no imports. Never throws.
30
+ */
31
+ export declare function parseImportedSecretNamesFromManifest(cdkOutPath: string): string[];
@@ -1 +1 @@
1
- import{readFileSync as m}from"fs";import{join as y}from"path";import{FJALL_MANIFEST_FILENAME as p}from"@fjall/util/constructMap";import{logger as f}from"@fjall/util/logger";function a(t){return typeof t=="object"&&t!==null&&!Array.isArray(t)}function l(t){const n=y(t,p);let i;try{i=m(n,"utf-8")}catch{return f.debug("manifestSecretParser","Manifest file not readable \u2014 no secrets extracted",{path:n}),[]}let r;try{r=JSON.parse(i)}catch{return f.debug("manifestSecretParser","Manifest is not valid JSON \u2014 no secrets extracted",{path:n}),[]}if(!a(r))return[];const o=[];if(Array.isArray(r.services))for(const s of r.services){if(!a(s))continue;const e=s;if(!(!Array.isArray(e.secrets)||e.secrets.length===0||typeof e.ssmSecretsPath!="string"))for(const c of e.secrets)typeof c=="string"&&o.push(`${e.ssmSecretsPath}/${c}`)}if(Array.isArray(r.lambdas))for(const s of r.lambdas){if(!a(s))continue;const e=s;if(!(!Array.isArray(e.secrets)||e.secrets.length===0||typeof e.ssmSecretsPath!="string"))for(const c of e.secrets)typeof c=="string"&&o.push(`${e.ssmSecretsPath}/${c}`)}return o}export{l as parseRequiredSecretsFromManifest};
1
+ import{readFileSync as m}from"fs";import{join as p}from"path";import{FJALL_MANIFEST_FILENAME as d}from"@fjall/util/constructMap";import{logger as f}from"@fjall/util/logger";function i(a){return typeof a=="object"&&a!==null&&!Array.isArray(a)}function S(a){const n=p(a,d);let c;try{c=m(n,"utf-8")}catch{return f.debug("manifestSecretParser","Manifest file not readable \u2014 no secrets extracted",{path:n}),[]}let r;try{r=JSON.parse(c)}catch{return f.debug("manifestSecretParser","Manifest is not valid JSON \u2014 no secrets extracted",{path:n}),[]}if(!i(r))return[];const o=[];if(Array.isArray(r.services))for(const s of r.services){if(!i(s))continue;const e=s;if(!(!Array.isArray(e.secrets)||e.secrets.length===0||typeof e.ssmSecretsPath!="string"))for(const t of e.secrets)typeof t=="string"&&o.push(`${e.ssmSecretsPath}/${t}`)}if(Array.isArray(r.lambdas))for(const s of r.lambdas){if(!i(s))continue;const e=s;if(!(!Array.isArray(e.secrets)||e.secrets.length===0||typeof e.ssmSecretsPath!="string"))for(const t of e.secrets)typeof t=="string"&&o.push(`${e.ssmSecretsPath}/${t}`)}return o}function A(a){const n=p(a,d);let c;try{c=m(n,"utf-8")}catch{return f.debug("manifestSecretParser","Manifest file not readable \u2014 no imported secret names extracted",{path:n}),[]}let r;try{r=JSON.parse(c)}catch{return f.debug("manifestSecretParser","Manifest is not valid JSON \u2014 no imported secret names extracted",{path:n}),[]}if(!i(r))return[];const o=new Set;for(const s of[r.services,r.lambdas])if(Array.isArray(s)){for(const e of s)if(!(!i(e)||!Array.isArray(e.importedSecretNames)))for(const t of e.importedSecretNames)typeof t=="string"&&t!==""&&o.add(t)}return Array.from(o)}export{A as parseImportedSecretNamesFromManifest,S as parseRequiredSecretsFromManifest};
@@ -1,5 +1,5 @@
1
- import{join as Ie}from"node:path";import{success as re,failure as h}from"@fjall/generator";import{OrganizationsClient as we}from"@aws-sdk/client-organizations";import{BackupClient as Ne}from"@aws-sdk/client-backup";import{ORGANISATION_TYPES as K,getOrganisationStackName as me}from"../types/operations.js";import{CdkContextBuilder as ye}from"../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as De}from"../types/deployment/index.js";import{ensureOrganisationExists as ke}from"../aws/organisations/organisation.js";import{accountHasDisasterRecovery as Te,describeBackupVaultExists as be}from"../aws/organisations/backup.js";import{buildParamsContext as Le,collectStackOutputs as ae,synthOrFail as ge,bootstrapOrFail as fe,forwardOutput as Ce,forwardResourceProgress as Se}from"./contextHelpers.js";import{partitionAccounts as Ae,deployCascadeAccount as Ee,readPlatformIpamPoolIds as Me,deployDomains as _e,buildRegionList as $e,buildAccountRegionPairs as Fe,CASCADE_MAX_CONCURRENCY as Ge}from"./cascadeHelpers.js";import{projectScalarSummary as Oe}from"./cascadeSummary.js";import{reconcileProviderAccounts as Ye,mergeReconciledProviderAccounts as ve}from"./reconcileProviderAccounts.js";import{maskSensitiveOutput as i,STRUCTURAL_ENVIRONMENTS as xe,mapSettledWithConcurrency as Ue}from"@fjall/util";import{INFRA_STEP_NAME as X,STEP_IDS as b,STEP_NAMES as se}from"../types/stepDefinitions.js";async function at(o,t,s){const g=Date.now();switch(s.type){case K.ORGANISATION:return We(o,t,s,g);case K.PLATFORM:return he(o,t,s,"platform",g);case K.ACCOUNT:return he(o,t,s,"account",g);default:{const e=s.type;return h(new Error(`Unsupported organisation type: ${String(e)}`))}}}function Re(o,t,s,g,e,r){return ye.buildDeploymentContext({deployType:g,target:s.target,path:s.path,region:t.awsProvider.getRegion(),accountName:r,callerIdentity:De(t.awsProvider.getAccountId()),orgId:e.orgId,rootId:e.rootId,managementAccountId:e.managementAccountId,...Le({orgConfig:o.orgConfig,identity:o.identity,skipOidc:o.options?.skipOidc})},{verbose:o.options?.verbose,infraOnly:o.options?.infraOnly},o.orgConfig)}async function Pe(o){const t=o.awsProvider.getClient(we),s=await ke(t);return s.success?re({orgId:s.data.orgId,rootId:s.data.rootId,managementAccountId:s.data.managementAccountId}):h(s.error)}const n={CONNECT:{id:b.CONNECT,name:X.CONNECT},PREPARE:{id:b.PREPARE_ENVIRONMENT,name:X.PREPARE},DEPLOY:{id:b.DEPLOY,name:X.DEPLOY},MONITORING:{id:b.MONITORING,name:X.MONITORING},ORG_DEPLOY:{id:b.ORG_DEPLOY,name:se.ORG_DEPLOY},CASCADE_PLATFORM:{id:b.CASCADE_PLATFORM,name:se.CASCADE_PLATFORM},CASCADE_ACCOUNTS:{id:b.CASCADE_ACCOUNTS,name:se.CASCADE_ACCOUNTS}},S=4;async function he(o,t,s,g,e){const{callbacks:r}=o;r.onStepComplete?.(n.CONNECT.id,n.CONNECT.name,"completed",0,S),r.onStepStart?.(n.PREPARE.id,n.PREPARE.name,1,S);const p=await Pe(t);if(!p.success){r.onStepComplete?.(n.PREPARE.id,n.PREPARE.name,"error",1,S);const l=new Error(i(p.error.message));return r.onError?.(l),h(l)}const $=Re(o,t,s,g,p.data,g==="account"?s.target:void 0),L=o.orgConfig?.disasterRecoveryRegion,W=L!==void 0&&L!=="",M=g==="account"?o.orgConfig?.providerAccounts.find(l=>l.name===s.target):void 0;if(g==="account"&&W&&(M===void 0||Te(M.environment,L))){const l=await be(t.awsProvider.getClient(Ne));if(!l.success){r.onStepComplete?.(n.PREPARE.id,n.PREPARE.name,"error",1,S);const y=new Error(i(l.error.message));return r.onError?.(y),h(y)}$.fjallAdoptBackupVault=l.data}r.onLog?.(`Synthesising ${g} infrastructure\u2026`,"info");const _=await ge(t,$,r,"CDK synthesis failed");if(!_.success)return r.onStepComplete?.(n.PREPARE.id,n.PREPARE.name,"error",1,S),_;const j=await fe(t,$,r);if(!j.success)return r.onStepComplete?.(n.PREPARE.id,n.PREPARE.name,"error",1,S),j;r.onStepComplete?.(n.PREPARE.id,n.PREPARE.name,"completed",1,S);const V=me(s.type);r.onStepStart?.(n.DEPLOY.id,n.DEPLOY.name,2,S);const F=await t.cdkService.runCdkDeploy($,V,Ce(r),Se(r),t.awsProvider);if(!F.success){r.onStepComplete?.(n.DEPLOY.id,n.DEPLOY.name,"error",2,S);const l=new Error(i(F.error));return r.onError?.(l),h(l)}r.onStepComplete?.(n.DEPLOY.id,n.DEPLOY.name,"completed",2,S);const G=await t.cfnService.getStackOutputs(V);G.success||r.onLog?.("Failed to read stack outputs (non-critical)","debug");const d=ae(G);return r.onStepStart?.(n.MONITORING.id,n.MONITORING.name,3,S),r.onStepComplete?.(n.MONITORING.id,n.MONITORING.name,"completed",3,S),re({target:s.target,deploymentType:"organisation",outputs:d,durationMs:Date.now()-e})}async function We(o,t,s,g){const{callbacks:e,options:r}=o;let p=o.orgConfig?.providerAccounts??[];if(p.length===0||p.every(a=>a.environment===xe.ROOT)){const a=await Ye(t,o.workingDirectory);if(a.success){const{providerAccounts:c,missingAccountNames:C}=a.data;c.length>0&&(p=ve(o.orgConfig,c).providerAccounts,e.onLog?.(`Reconciled ${c.length} account(s) from AWS Organizations`,"info")),C.length>0&&(e.onCascadeMissingAccounts?.(C),e.onProgress?.({type:"warning",message:i(`Accounts declared in ACCOUNTS but not yet in AWS Organizations (cascade will skip): ${C.join(", ")}`)}))}else e.onProgress?.({type:"warning",message:i(`Could not reconcile accounts from AWS Organizations \u2014 cascade may skip accounts: ${a.error.message}`)})}const L=o.orgConfig?.primaryRegion??t.awsProvider.getRegion();p=p.map(a=>a.region!==void 0?a:{...a,region:L});const W=o.orgConfig!==void 0?{...o.orgConfig,providerAccounts:p}:p.length>0?{providerAccounts:p}:void 0,M=await Pe(t);if(!M.success){const a=new Error(i(M.error.message));return e.onError?.(a),h(a)}const N=Re(o,t,s,"organisation",M.data),_=r?.cascade!==!1,{platformAccount:j,memberAccounts:V}=Ae(p),F=_&&j!==void 0?1:0,G=_&&V.length>0?1:0,d=2+F+G;e.onCascadeAccountsReconciled?.({hasPlatformAccount:F>0,hasMemberAccounts:G>0});const{id:l,name:y}=n.PREPARE;e.onStepStart?.(l,y,0,d),e.onLog?.("Synthesising organisation infrastructure\u2026","info");const ce=await ge(t,N,e,"CDK synthesis failed");if(!ce.success)return e.onStepComplete?.(l,y,"error",0,d),ce;const ie=await fe(t,N,e);if(!ie.success)return e.onStepComplete?.(l,y,"error",0,d),ie;e.onStepComplete?.(l,y,"completed",0,d);const{id:J,name:Q}=n.ORG_DEPLOY,D=me(K.ORGANISATION);let de=!0;const Y=await t.hashService.getTemplateHashes(Ie(N.path,"cdk.out"));if(Y.success){const a=await t.hashService.compareWithState(Y.data,N.path);a.success?de=a.data.stackChanges.get(D)??!0:e.onLog?.(i(`Org root change detection failed \u2014 deploying to be safe: ${a.error.message}`),"warn")}else e.onLog?.(i(`Org root template hashing failed \u2014 deploying to be safe: ${Y.error.message}`),"warn");const Z=de||r?.force===!0||!await t.cfnService.stackExists(D);e.onOrgChangesDetected?.({hasOrgChanges:Z});let ee;if(Z){e.onStepStart?.(J,Q,1,d);const a=await t.cdkService.runCdkDeploy(N,D,Ce(e),Se(e),t.awsProvider);if(!a.success){e.onStepComplete?.(J,Q,"error",1,d);const R=new Error(i(a.error));return e.onError?.(R),h(R)}const c=await t.cfnService.getStackOutputs(D);c.success||e.onLog?.("Failed to read org stack outputs (non-critical)","debug"),ee=ae(c);const C=Y.success?Y.data.get(D):void 0;if(C!==void 0){const R=await t.hashService.updateStateAfterDeploy(N.path,new Map([[D,C]]));R.success||e.onLog?.(`Warning: failed to update state file \u2014 next deploy may re-deploy the org root: ${i(R.error.message)}`,"warn")}e.onStepComplete?.(J,Q,"completed",1,d)}else{e.onLog?.("Organisation root: no infrastructure changes \u2014 skipping deploy","info");const a=await t.cfnService.getStackOutputs(D);a.success||e.onLog?.("Failed to read org stack outputs (non-critical)","debug"),ee=ae(a)}const f=[],H=[];let z=Z;if(_&&p.length>0){e.onCascadeStart?.();const a=Date.now();let c=2,C=!1,R,te=!1;const B=[],ue=u=>({members:B,...R!==void 0?{platform:R}:{},domainsDeployed:te,errors:f,totalDurationMs:u}),{platformAccount:A,memberAccounts:q}=Ae(p);if(A){const{id:u,name:m}=n.CASCADE_PLATFORM;e.onStepStart?.(u,m,c,d),e.onCascadePhaseStart?.("platform");let E;const oe=Date.now();try{E=await Ee(o,t,s,A,"platform",e,{orgConfig:W})}catch(P){const x=i(P instanceof Error?P.message:String(P));f.push({accountId:A.id,error:x}),e.onCascadePhaseComplete?.("platform"),e.onStepComplete?.(u,m,"error",c,d),E=h(new Error(x))}const v=Date.now()-oe;if(E.success){C=!0;const P=E.data.skipped===!0;P||(z=!0),E.data.outputs&&H.push({accountId:A.id,outputs:E.data.outputs}),e.onCascadePhaseComplete?.("platform"),e.onStepComplete?.(u,m,P?"skipped":"completed",c,d),R={accountId:A.id,result:P?"skipped":"succeeded",durationMs:v}}else f.some(P=>P.accountId===A.id)||(f.push({accountId:A.id,error:i(E.error.message)}),e.onCascadePhaseComplete?.("platform"),e.onStepComplete?.(u,m,"error",c,d)),R={accountId:A.id,result:"failed",durationMs:v,error:i(E.error.message)};c++}let le=new Map;if(C&&A&&(le=await Me(t,A,e)),o.domainProvider){const u=await _e(o.domainProvider,e);te=u.domainsDeployed>0,te&&(z=!0);for(const m of u.errors)f.push({accountId:"domains",error:i(m)})}if(A!==void 0&&!C&&q.length>0){const{id:u,name:m}=n.CASCADE_ACCOUNTS;e.onStepStart?.(u,m,c,d),e.onStepComplete?.(u,m,"skipped",c,d),e.onLog?.("Skipping account cascade \u2014 platform deployment failed; platform is a prerequisite for member accounts.","warn")}else if(q.length>0){const{id:u,name:m}=n.CASCADE_ACCOUNTS;e.onStepStart?.(u,m,c,d),e.onCascadePhaseStart?.("accounts");const E=$e(o.orgConfig),oe=E[0]??L,v=Fe(q,E);(await Ue(v,Ge,async({account:I,region:U})=>{const k=U.replace(/-/g,""),O=le.get(`${I.id}-${k}`),T=Date.now();return{result:await Ee(o,t,s,I,"account",e,{ipamPoolId:O,orgConfig:W,region:U,skipAccountGlobals:U!==oe}),durationMs:Date.now()-T}})).forEach((I,U)=>{const k=v[U];if(!k)return;const O=k.account;if(I.status==="rejected"){const w=i(I.reason instanceof Error?I.reason.message:String(I.reason));B.push({accountId:O.id,accountName:O.name,region:k.region,result:"failed",durationMs:0,error:w}),f.push({accountId:O.id,error:w});return}const{result:T,durationMs:ne}=I.value;if(T.success){const w=T.data.skipped===!0;w||(z=!0),T.data.outputs&&H.push({accountId:O.id,outputs:T.data.outputs}),B.push({accountId:O.id,accountName:O.name,region:k.region,result:w?"skipped":"succeeded",durationMs:ne})}else{const w=i(T.error.message);B.push({accountId:O.id,accountName:O.name,region:k.region,result:"failed",durationMs:ne,error:w}),f.push({accountId:O.id,error:w})}});const x=Oe(ue(0));e.onCascadePhaseComplete?.("accounts"),e.onStepComplete?.(u,m,x.accountsFailed>0?"error":x.accountsSkipped===q.length?"skipped":"completed",c,d)}const pe=ue(Date.now()-a);if(e.onCascadeComplete?.(Oe(pe)),e.onCascadeLedger?.(pe),f.length>0){const u=f.map(m=>` ${m.accountId}: ${m.error}`).join(`
1
+ import{join as we}from"node:path";import{success as re,failure as h}from"@fjall/generator";import{OrganizationsClient as Ie}from"@aws-sdk/client-organizations";import{BackupClient as ye}from"@aws-sdk/client-backup";import{ORGANISATION_TYPES as K,getOrganisationStackName as me}from"../types/operations.js";import{CdkContextBuilder as Ne}from"../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as De}from"../types/deployment/index.js";import{ensureOrganisationExists as ke}from"../aws/organisations/organisation.js";import{accountHasDisasterRecovery as be,describeBackupVaultExists as Te}from"../aws/organisations/backup.js";import{buildParamsContext as Le,collectStackOutputs as ae,synthOrFail as ge,bootstrapOrFail as fe,forwardOutput as Ce,forwardResourceProgress as Se}from"./contextHelpers.js";import{partitionAccounts as Ae,deployCascadeAccount as Ee,readPlatformIpamPoolIds as Me,deployDomains as _e,buildRegionList as $e,buildAccountRegionPairs as Fe,CASCADE_MAX_CONCURRENCY as Ge}from"./cascadeHelpers.js";import{projectScalarSummary as Oe}from"./cascadeSummary.js";import{reconcileProviderAccounts as Ye,mergeReconciledProviderAccounts as xe}from"./reconcileProviderAccounts.js";import{accountTier as ve,maskSensitiveOutput as i,mapSettledWithConcurrency as Ue}from"@fjall/util";import{INFRA_STEP_NAME as X,STEP_IDS as T,STEP_NAMES as se}from"../types/stepDefinitions.js";async function at(o,t,s){const g=Date.now();switch(s.type){case K.ORGANISATION:return We(o,t,s,g);case K.PLATFORM:return he(o,t,s,"platform",g);case K.ACCOUNT:return he(o,t,s,"account",g);default:{const e=s.type;return h(new Error(`Unsupported organisation type: ${String(e)}`))}}}function Re(o,t,s,g,e,r){return Ne.buildDeploymentContext({deployType:g,target:s.target,path:s.path,region:t.awsProvider.getRegion(),accountName:r,callerIdentity:De(t.awsProvider.getAccountId()),orgId:e.orgId,rootId:e.rootId,managementAccountId:e.managementAccountId,...Le({orgConfig:o.orgConfig,identity:o.identity,skipOidc:o.options?.skipOidc})},{verbose:o.options?.verbose,infraOnly:o.options?.infraOnly},o.orgConfig)}async function Pe(o){const t=o.awsProvider.getClient(Ie),s=await ke(t);return s.success?re({orgId:s.data.orgId,rootId:s.data.rootId,managementAccountId:s.data.managementAccountId}):h(s.error)}const n={CONNECT:{id:T.CONNECT,name:X.CONNECT},PREPARE:{id:T.PREPARE_ENVIRONMENT,name:X.PREPARE},DEPLOY:{id:T.DEPLOY,name:X.DEPLOY},MONITORING:{id:T.MONITORING,name:X.MONITORING},ORG_DEPLOY:{id:T.ORG_DEPLOY,name:se.ORG_DEPLOY},CASCADE_PLATFORM:{id:T.CASCADE_PLATFORM,name:se.CASCADE_PLATFORM},CASCADE_ACCOUNTS:{id:T.CASCADE_ACCOUNTS,name:se.CASCADE_ACCOUNTS}},S=4;async function he(o,t,s,g,e){const{callbacks:r}=o;r.onStepComplete?.(n.CONNECT.id,n.CONNECT.name,"completed",0,S),r.onStepStart?.(n.PREPARE.id,n.PREPARE.name,1,S);const p=await Pe(t);if(!p.success){r.onStepComplete?.(n.PREPARE.id,n.PREPARE.name,"error",1,S);const l=new Error(i(p.error.message));return r.onError?.(l),h(l)}const $=Re(o,t,s,g,p.data,g==="account"?s.target:void 0),L=o.orgConfig?.disasterRecoveryRegion,W=L!==void 0&&L!=="",M=g==="account"?o.orgConfig?.providerAccounts.find(l=>l.name===s.target):void 0;if(g==="account"&&W&&(M===void 0||be(M.environment,L))){const l=await Te(t.awsProvider.getClient(ye));if(!l.success){r.onStepComplete?.(n.PREPARE.id,n.PREPARE.name,"error",1,S);const N=new Error(i(l.error.message));return r.onError?.(N),h(N)}$.fjallAdoptBackupVault=l.data}r.onLog?.(`Synthesising ${g} infrastructure\u2026`,"info");const _=await ge(t,$,r,"CDK synthesis failed");if(!_.success)return r.onStepComplete?.(n.PREPARE.id,n.PREPARE.name,"error",1,S),_;const j=await fe(t,$,r);if(!j.success)return r.onStepComplete?.(n.PREPARE.id,n.PREPARE.name,"error",1,S),j;r.onStepComplete?.(n.PREPARE.id,n.PREPARE.name,"completed",1,S);const H=me(s.type);r.onStepStart?.(n.DEPLOY.id,n.DEPLOY.name,2,S);const F=await t.cdkService.runCdkDeploy($,H,Ce(r),Se(r),t.awsProvider);if(!F.success){r.onStepComplete?.(n.DEPLOY.id,n.DEPLOY.name,"error",2,S);const l=new Error(i(F.error));return r.onError?.(l),h(l)}r.onStepComplete?.(n.DEPLOY.id,n.DEPLOY.name,"completed",2,S);const G=await t.cfnService.getStackOutputs(H);G.success||r.onLog?.("Failed to read stack outputs (non-critical)","debug");const d=ae(G);return r.onStepStart?.(n.MONITORING.id,n.MONITORING.name,3,S),r.onStepComplete?.(n.MONITORING.id,n.MONITORING.name,"completed",3,S),re({target:s.target,deploymentType:"organisation",outputs:d,durationMs:Date.now()-e})}async function We(o,t,s,g){const{callbacks:e,options:r}=o;let p=o.orgConfig?.providerAccounts??[];if(p.length===0||p.every(a=>ve(a)==="organisation")){const a=await Ye(t,o.workingDirectory);if(a.success){const{providerAccounts:c,missingAccountNames:C}=a.data;c.length>0&&(p=xe(o.orgConfig,c).providerAccounts,e.onLog?.(`Reconciled ${c.length} account(s) from AWS Organizations`,"info")),C.length>0&&(e.onCascadeMissingAccounts?.(C),e.onProgress?.({type:"warning",message:i(`Accounts declared in ACCOUNTS but not yet in AWS Organizations (cascade will skip): ${C.join(", ")}`)}))}else e.onProgress?.({type:"warning",message:i(`Could not reconcile accounts from AWS Organizations \u2014 cascade may skip accounts: ${a.error.message}`)})}const L=o.orgConfig?.primaryRegion??t.awsProvider.getRegion();p=p.map(a=>a.region!==void 0?a:{...a,region:L});const W=o.orgConfig!==void 0?{...o.orgConfig,providerAccounts:p}:p.length>0?{providerAccounts:p}:void 0,M=await Pe(t);if(!M.success){const a=new Error(i(M.error.message));return e.onError?.(a),h(a)}const y=Re(o,t,s,"organisation",M.data),_=r?.cascade!==!1,{platformAccount:j,memberAccounts:H}=Ae(p),F=_&&j!==void 0?1:0,G=_&&H.length>0?1:0,d=2+F+G;e.onCascadeAccountsReconciled?.({hasPlatformAccount:F>0,hasMemberAccounts:G>0});const{id:l,name:N}=n.PREPARE;e.onStepStart?.(l,N,0,d),e.onLog?.("Synthesising organisation infrastructure\u2026","info");const ce=await ge(t,y,e,"CDK synthesis failed");if(!ce.success)return e.onStepComplete?.(l,N,"error",0,d),ce;const ie=await fe(t,y,e);if(!ie.success)return e.onStepComplete?.(l,N,"error",0,d),ie;e.onStepComplete?.(l,N,"completed",0,d);const{id:J,name:Q}=n.ORG_DEPLOY,D=me(K.ORGANISATION);let de=!0;const Y=await t.hashService.getTemplateHashes(we(y.path,"cdk.out"));if(Y.success){const a=await t.hashService.compareWithState(Y.data,y.path);a.success?de=a.data.stackChanges.get(D)??!0:e.onLog?.(i(`Org root change detection failed \u2014 deploying to be safe: ${a.error.message}`),"warn")}else e.onLog?.(i(`Org root template hashing failed \u2014 deploying to be safe: ${Y.error.message}`),"warn");const Z=de||r?.force===!0||!await t.cfnService.stackExists(D);e.onOrgChangesDetected?.({hasOrgChanges:Z});let ee;if(Z){e.onStepStart?.(J,Q,1,d);const a=await t.cdkService.runCdkDeploy(y,D,Ce(e),Se(e),t.awsProvider);if(!a.success){e.onStepComplete?.(J,Q,"error",1,d);const R=new Error(i(a.error));return e.onError?.(R),h(R)}const c=await t.cfnService.getStackOutputs(D);c.success||e.onLog?.("Failed to read org stack outputs (non-critical)","debug"),ee=ae(c);const C=Y.success?Y.data.get(D):void 0;if(C!==void 0){const R=await t.hashService.updateStateAfterDeploy(y.path,new Map([[D,C]]));R.success||e.onLog?.(`Warning: failed to update state file \u2014 next deploy may re-deploy the org root: ${i(R.error.message)}`,"warn")}e.onStepComplete?.(J,Q,"completed",1,d)}else{e.onLog?.("Organisation root: no infrastructure changes \u2014 skipping deploy","info");const a=await t.cfnService.getStackOutputs(D);a.success||e.onLog?.("Failed to read org stack outputs (non-critical)","debug"),ee=ae(a)}const f=[],V=[];let z=Z;if(_&&p.length>0){e.onCascadeStart?.();const a=Date.now();let c=2,C=!1,R,te=!1;const B=[],ue=u=>({members:B,...R!==void 0?{platform:R}:{},domainsDeployed:te,errors:f,totalDurationMs:u}),{platformAccount:A,memberAccounts:q}=Ae(p);if(A){const{id:u,name:m}=n.CASCADE_PLATFORM;e.onStepStart?.(u,m,c,d),e.onCascadePhaseStart?.("platform");let E;const oe=Date.now();try{E=await Ee(o,t,s,A,"platform",e,{orgConfig:W})}catch(P){const v=i(P instanceof Error?P.message:String(P));f.push({accountId:A.id,error:v}),e.onCascadePhaseComplete?.("platform"),e.onStepComplete?.(u,m,"error",c,d),E=h(new Error(v))}const x=Date.now()-oe;if(E.success){C=!0;const P=E.data.skipped===!0;P||(z=!0),E.data.outputs&&V.push({accountId:A.id,outputs:E.data.outputs}),e.onCascadePhaseComplete?.("platform"),e.onStepComplete?.(u,m,P?"skipped":"completed",c,d),R={accountId:A.id,result:P?"skipped":"succeeded",durationMs:x}}else f.some(P=>P.accountId===A.id)||(f.push({accountId:A.id,error:i(E.error.message)}),e.onCascadePhaseComplete?.("platform"),e.onStepComplete?.(u,m,"error",c,d)),R={accountId:A.id,result:"failed",durationMs:x,error:i(E.error.message)};c++}let le=new Map;if(C&&A&&(le=await Me(t,A,e)),o.domainProvider){const u=await _e(o.domainProvider,e);te=u.domainsDeployed>0,te&&(z=!0);for(const m of u.errors)f.push({accountId:"domains",error:i(m)})}if(A!==void 0&&!C&&q.length>0){const{id:u,name:m}=n.CASCADE_ACCOUNTS;e.onStepStart?.(u,m,c,d),e.onStepComplete?.(u,m,"skipped",c,d),e.onLog?.("Skipping account cascade \u2014 platform deployment failed; platform is a prerequisite for member accounts.","warn")}else if(q.length>0){const{id:u,name:m}=n.CASCADE_ACCOUNTS;e.onStepStart?.(u,m,c,d),e.onCascadePhaseStart?.("accounts");const E=$e(o.orgConfig),oe=E[0]??L,x=Fe(q,E);(await Ue(x,Ge,async({account:w,region:U})=>{const k=U.replace(/-/g,""),O=le.get(`${w.id}-${k}`),b=Date.now();return{result:await Ee(o,t,s,w,"account",e,{ipamPoolId:O,orgConfig:W,region:U,skipAccountGlobals:U!==oe}),durationMs:Date.now()-b}})).forEach((w,U)=>{const k=x[U];if(!k)return;const O=k.account;if(w.status==="rejected"){const I=i(w.reason instanceof Error?w.reason.message:String(w.reason));B.push({accountId:O.id,accountName:O.name,region:k.region,result:"failed",durationMs:0,error:I}),f.push({accountId:O.id,error:I});return}const{result:b,durationMs:ne}=w.value;if(b.success){const I=b.data.skipped===!0;I||(z=!0),b.data.outputs&&V.push({accountId:O.id,outputs:b.data.outputs}),B.push({accountId:O.id,accountName:O.name,region:k.region,result:I?"skipped":"succeeded",durationMs:ne})}else{const I=i(b.error.message);B.push({accountId:O.id,accountName:O.name,region:k.region,result:"failed",durationMs:ne,error:I}),f.push({accountId:O.id,error:I})}});const v=Oe(ue(0));e.onCascadePhaseComplete?.("accounts"),e.onStepComplete?.(u,m,v.accountsFailed>0?"error":v.accountsSkipped===q.length?"skipped":"completed",c,d)}const pe=ue(Date.now()-a);if(e.onCascadeComplete?.(Oe(pe)),e.onCascadeLedger?.(pe),f.length>0){const u=f.map(m=>` ${m.accountId}: ${m.error}`).join(`
2
2
  `);e.onLog?.(i(`Cascade failed for ${f.length} target(s):
3
3
  ${u}`),"warn")}}if(f.length>0){const a=f.map(C=>i(`${C.accountId}: ${C.error}`)).join(`
4
4
  `),c=new Error(`Organisation root deployed, but the cascade failed for ${f.length} target(s):
5
- ${a}`);return e.onError?.(c),h(c)}return re({target:s.target,deploymentType:"organisation",outputs:ee,...H.length>0?{cascadeOutputs:H}:{},...z?{}:{noChanges:!0},durationMs:Date.now()-g})}export{at as deployOrganisation};
5
+ ${a}`);return e.onError?.(c),h(c)}return re({target:s.target,deploymentType:"organisation",outputs:ee,...V.length>0?{cascadeOutputs:V}:{},...z?{}:{noChanges:!0},durationMs:Date.now()-g})}export{at as deployOrganisation};
@@ -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 C,failure as f}from"@fjall/generator";import{OrganizationsClient as L}from"@aws-sdk/client-organizations";import{RAMClient as F}from"@aws-sdk/client-ram";import{CloudFormationClient as j}from"@aws-sdk/client-cloudformation";import{EC2Client as D}from"@aws-sdk/client-ec2";import{BackupClient as W}from"@aws-sdk/client-backup";import{CostExplorerClient as B}from"@aws-sdk/client-cost-explorer";import{SSOAdminClient as $}from"@aws-sdk/client-sso-admin";import{ensureOrganisationExists as z}from"../aws/organisations/organisation.js";import{enablePolicyTypes as G}from"../aws/organisations/policies.js";import{enableServiceAccess as K}from"../aws/organisations/serviceAccess.js";import{enableRamSharing as q}from"../aws/organisations/ram.js";import{activateTrustedAccess as H}from"../aws/organisations/trustedAccess.js";import{enableIpamDelegatedAdmin as J}from"../aws/organisations/ipam.js";import{updateBackupGlobalSettings as Q}from"../aws/organisations/backup.js";import{listAccounts as E,createAccount as V}from"../aws/organisations/accounts.js";import{ensureOrganisationalUnitsExist as X,placeAccountsInOUs as Y,buildAccountToOUMap as Z}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(n,o,e){const r=[],s=[],t=[],a=[];let c;const u=n.getClient(L),O=n.getClient(F),U=n.getClient(j),R=n.getClient(D),x=n.getClient(W),N=n.getClient(B),T=n.getClient($);e?.onPhaseStart?.("create-organisation"),e?.onProgress?.("Ensuring AWS Organisation exists");const g=await z(u);if(!g.success)return e?.onError?.("create-organisation",g.error),e?.onPhaseComplete?.("create-organisation","error"),f(g.error);const{orgId:M,rootId:P}=g.data;if(e?.onPhaseComplete?.("create-organisation","completed"),r.push("create-organisation"),await p("enable-policies",()=>(e?.onProgress?.("Enabling organisation policy types"),G(u,P)),r,t,e),await p("enable-service-access",()=>(e?.onProgress?.("Enabling AWS service access"),K(u)),r,t,e),await p("enable-ram-sharing",()=>(e?.onProgress?.("Enabling RAM sharing"),q(O)),r,t,e),await p("activate-trusted-access",()=>(e?.onProgress?.("Activating CloudFormation trusted access"),H(U)),r,t,e),o.platformAccountId){const i=o.platformAccountId;await p("enable-ipam",()=>(e?.onProgress?.("Enabling IPAM delegated administrator"),J(R,i)),r,t,e)}await p("configure-backup",()=>(e?.onProgress?.("Updating backup global settings"),Q(x)),r,t,e),e?.onPhaseStart?.("create-accounts"),e?.onProgress?.("Checking for missing accounts");const d=await te(u,o.accounts,a,e);let y=[];d.success?(y=d.data,r.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 l=await X(u,P,o.organisationalUnits);l.success?(h=l.data,r.push("create-organisational-units"),e?.onPhaseComplete?.("create-organisational-units","completed")):(t.push({phase:"create-organisational-units",error:l.error.message}),e?.onError?.("create-organisational-units",l.error),e?.onPhaseComplete?.("create-organisational-units","error"));const m=Array.isArray(o.organisationalUnits)?void 0:o.organisationalUnits,A=o.accountPlacements??[],w=A.length===0&&m!==void 0?ne(m,y):A;if(Object.keys(h).length===0)s.push("place-accounts"),e?.onPhaseStart?.("place-accounts"),e?.onPhaseComplete?.("place-accounts","skipped");else if(o.accountPlacements===void 0&&m===void 0){const i=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:i.message}),e?.onPhaseStart?.("place-accounts"),e?.onError?.("place-accounts",i),e?.onPhaseComplete?.("place-accounts","error")}else if(w.length===0)s.push("place-accounts"),e?.onPhaseStart?.("place-accounts"),e?.onPhaseComplete?.("place-accounts","skipped");else{const i=m?Z(m,h):void 0;await p("place-accounts",()=>(e?.onProgress?.("Placing accounts in organisational units"),Y(u,h,w,i)),r,t,e)}const S=o.costAllocationTags??[];if(S.length>0?await p("activate-cost-tags",()=>(e?.onProgress?.("Activating cost allocation tags"),_(N,S.map(i=>({TagKey:i})))),r,t,e):(s.push("activate-cost-tags"),e?.onPhaseStart?.("activate-cost-tags"),e?.onPhaseComplete?.("activate-cost-tags","skipped")),o.skipIdentityCentre)s.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 i=await b(T);i.success?(c=i.data.enabled?"enabled":"not-enabled",r.push("check-identity-centre"),e?.onPhaseComplete?.("check-identity-centre","completed")):(t.push({phase:"check-identity-centre",error:i.error.message}),e?.onError?.("check-identity-centre",i.error),e?.onPhaseComplete?.("check-identity-centre","error"))}const I=o.securityDelegateAccountId;return I?await p("register-security-delegates",()=>(e?.onProgress?.("Registering security service delegated administrators"),k(u,I)),r,t,e):(s.push("register-security-delegates"),e?.onPhaseStart?.("register-security-delegates"),e?.onPhaseComplete?.("register-security-delegates","skipped")),C({organisationId:M,createdAccounts:a,identityCentreStatus:c,phasesCompleted:r,phasesSkipped:s,errors:t})}async function p(n,o,e,r,s){s?.onPhaseStart?.(n);const t=await o();t.success?(e.push(n),s?.onPhaseComplete?.(n,"completed")):(r.push({phase:n,error:t.error.message}),s?.onError?.(n,t.error),s?.onPhaseComplete?.(n,"error"))}async function te(n,o,e,r){const s=await E(n);if(!s.success)return f(s.error);const t=new Set(s.data.map(c=>c.Name?.toLowerCase()).filter(c=>c!==void 0));for(const c of o){if(t.has(c.name.toLowerCase()))continue;r?.onProgress?.(`Account "${c.name}" is not yet a member of this AWS Organisation. Fjall will create a new account (with a new account ID). If "${c.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 u=await V(n,c.name,c.email);if(!u.success)return f(u.error);e.push({name:u.data.accountName,accountId:u.data.accountId})}if(e.length===0)return C(s.data);const a=await E(n);return a.success?C(a.data):f(a.error)}function v(n){const o=[];for(const e of Object.values(n))ee(e)?o.push(...e):o.push(...v(e));return o}function ne(n,o){const e=v(n),r=new Map;for(const t of o){const a=t.Name?.toLowerCase();a&&t.Id&&r.set(a,t)}const s=[];for(const t of e){const a=r.get(t.toLowerCase());a?.Id&&a.Name&&s.push({id:a.Id,name:a.Name,environment:t})}return s}export{Ee 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 m}from"zod";import{success as A,failure as c}from"@fjall/generator";import{environmentToTier as v,stageFromWireEnvironment as w}from"@fjall/util";import{OrganizationsClient as N}from"@aws-sdk/client-organizations";import{listAccounts as C}from"../aws/organisations/accounts.js";import{parseAccountsConfiguration as y,flattenAccountsToEnvironments as E}from"./accountsConfig.js";const T=m.object({Id:m.string().min(1),Name:m.string().min(1)});async function O(t,i){const e=await y(i);if(!e.success)return c(new Error(`Failed to parse ACCOUNTS configuration: ${e.error.message}`));if(e.data===null)return c(new Error("ACCOUNTS configuration file not found"));const r=E(e.data),u=new Map;for(const{accountName:o,environment:n}of r)u.set(o.toLowerCase(),{environment:n,displayName:o});let f;try{f=t.awsProvider.getClient(N)}catch(o){return c(o instanceof Error?o:new Error(String(o)))}const s=await C(f);if(!s.success)return c(s.error);const d=new Map;for(const o of s.data){const n=T.safeParse(o);n.success&&d.set(n.data.Name.toLowerCase(),{id:n.data.Id,name:n.data.Name})}const p=[],l=[];for(const[o,{environment:n,displayName:g}]of u){const a=d.get(o);if(!a){l.push(g);continue}v(n)!=="organisation"&&p.push({id:a.id,name:a.name,tier:v(n),environment:w(n)})}return A({providerAccounts:p,missingAccountNames:l})}function R(t,i){const e=new Map;for(const r of t?.providerAccounts??[])e.set(r.id,r);for(const r of i)e.has(r.id)||e.set(r.id,r);return{...t??{},providerAccounts:Array.from(e.values())}}export{R as mergeReconciledProviderAccounts,O as reconcileProviderAccounts};
@@ -0,0 +1,19 @@
1
+ import { type Result } from "@fjall/generator";
2
+ import type { AwsProvider } from "../aws/AwsProvider.js";
3
+ /**
4
+ * Resolve each externally-managed secret NAME to its COMPLETE ARN (with the AWS
5
+ * 6-char suffix) via DescribeSecret. The complete ARN is what
6
+ * `Secret.fromSecretCompleteArn` needs to render a plain-string `valueFrom` and an
7
+ * exact-ARN `grantRead` — the only shape real Secrets Manager authorises at ECS
8
+ * task launch (the suffixless `fromSecretNameV2` shape is AccessDenied regardless
9
+ * of grant width: the 2026-06-04 outage).
10
+ *
11
+ * Fail-CLOSED: a missing secret or a DescribeSecret AccessDenied aborts the deploy
12
+ * at this cheap pre-synth point, NEVER silently falling back to the suffixless
13
+ * shape. This deliberately inverts the non-fatal IPAM-pool resolution precedent —
14
+ * a mis-resolution here would reintroduce the exact production failure.
15
+ *
16
+ * Issues DescribeSecret ONLY — never GetSecretValue — so no secret value can reach
17
+ * the resolved-ARN context map, the logs, or the synthesised template.
18
+ */
19
+ export declare function resolveSecretArns(awsProvider: AwsProvider, names: string[]): Promise<Result<Record<string, string>>>;
@@ -0,0 +1 @@
1
+ import{SecretsManagerClient as a,DescribeSecretCommand as m}from"@aws-sdk/client-secrets-manager";import{success as d,failure as n}from"@fjall/generator";import{getErrorMessage as l,maskSensitiveOutput as p}from"@fjall/util";const u=3e4;async function g(s,c){const i=s.getClient(a),o={};for(const r of c)try{const e=(await i.send(new m({SecretId:r}),{abortSignal:AbortSignal.timeout(u)})).ARN;if(e===void 0||e==="")return n(new Error(`DescribeSecret for "${r}" returned no ARN \u2014 cannot resolve a complete ARN.`));o[r]=e}catch(t){const e=t instanceof Error?t.name:"";return e==="ResourceNotFoundException"?n(new Error(`Imported secret "${r}" does not exist in this account/region. Create it, or reference it by complete ARN via the secretsImport { arn } escape hatch.`)):e==="AccessDeniedException"||e==="AccessDenied"?n(new Error(`The deploy identity lacks secretsmanager:DescribeSecret on "${r}" \u2014 grant DescribeSecret on this secret so its complete ARN can be resolved.`)):n(new Error(`Failed to resolve imported secret "${r}": ${p(l(t))}`))}return d(o)}export{g as resolveSecretArns};
@@ -1 +1 @@
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?.orgConfig&&a.push("-c",`orgConfig=${r.orgConfig}`),r?.fjallAdoptBackupVault&&a.push("-c","fjallAdoptBackupVault=true"),a}buildParameterArgs(r){if(r===void 0)return[];const a=Object.entries(r);if(a.length===0)return[];const o=[];for(const[e,n]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(n==="")throw new Error(`CloudFormation parameter "${e}" has an empty value.`);if(/[,\n\r]/.test(n))throw new Error(`CloudFormation parameter "${e}" value contains "," or newline \u2014 cdk's --parameters splits on "," so the deploy would silently fragment.`);o.push("--parameters",`${e}=${n}`)}return o}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};
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,3 +1,3 @@
1
- import{spawn as K}from"child_process";import{existsSync as E}from"fs";import{join as T}from"path";import{createRequire as A}from"module";import{logger as $}from"@fjall/util/logger";import{filterDangerousEnvVars as M,maskSensitiveOutput as g}from"@fjall/util";import{success as H,failure as i}from"@fjall/generator";import{getErrorMessage as w}from"@fjall/util";function L(){try{const e=A(import.meta.url).resolve("aws-cdk/bin/cdk");return{command:process.execPath,prefixArgs:[e]}}catch(h){return $.debug("CdkService","Failed to resolve aws-cdk binary, falling back to npx",{error:h instanceof Error?h.message:String(h)}),{command:"npx",prefixArgs:["cdk"]}}}const x=L(),j=5e3;class v{runningProcesses=new Map;processCounter=0;argBuilder;exitHandler;sigintHandler;sigtermHandler;constructor(e){this.argBuilder=e,this.exitHandler=()=>this.cleanup(),this.sigintHandler=()=>{this.cleanup(),process.exit(130)},this.sigtermHandler=()=>{this.cleanup(),process.exit(143)},process.on("exit",this.exitHandler),process.on("SIGINT",this.sigintHandler),process.on("SIGTERM",this.sigtermHandler)}forceKillProcess(e){e.stdout?.destroy(),e.stderr?.destroy(),e.kill("SIGTERM");const c=setTimeout(()=>{e.exitCode===null&&e.kill("SIGKILL")},j);c.unref(),e.once("exit",()=>clearTimeout(c))}cleanup(){for(const[e,c]of this.runningProcesses)c.killed||(c.stdout?.destroy(),c.stderr?.destroy(),c.kill("SIGTERM"));this.runningProcesses.clear()}dispose(){this.cleanup(),process.removeListener("exit",this.exitHandler),process.removeListener("SIGINT",this.sigintHandler),process.removeListener("SIGTERM",this.sigtermHandler)}async runCdkCommandPassthrough(e,c,r){return new Promise(t=>{if(!E(e)){t(i(`Directory not found: ${e}`));return}const a=T(e,"cdk.json");if(!E(a)){t(i(`No CDK project found in ${e}`));return}const m=this.argBuilder.buildContextArgs(r?.context),f=[...x.prefixArgs,...c,...m],S=this.argBuilder.buildCdkEnv(r),d=K(x.command,f,{cwd:e,env:S,stdio:"inherit",shell:!1,detached:!1});if(!d.pid){d.on("error",u=>{$.debug("CdkProcess","Spawn error on failed child process",{error:u.message})}),t(i(`Failed to spawn CDK process - no PID. cwd=${e}, args=${f.join(" ")}`));return}const C=`cdk-passthrough-${++this.processCounter}`;this.runningProcesses.set(C,d);let o=!1,l=!1;const p=r?.timeout??1800*1e3,k=setTimeout(()=>{d.killed||(o=!0,this.forceKillProcess(d))},p);d.on("close",u=>{if(clearTimeout(k),this.runningProcesses.delete(C),!l){if(l=!0,o){t(i("CDK command timed out"));return}u===0||r?.ignoreExitCode?t(H({exitCode:u||0})):t(i(`CDK command failed with exit code ${u}`))}}),d.on("error",u=>{clearTimeout(k),this.runningProcesses.delete(C),!(l||o)&&(l=!0,t(i(`Failed to run CDK command: ${u.message}`)))})})}async runCdkCommand(e,c,r){return new Promise(t=>{if(!E(e)){t(i(`Directory not found: ${e}`));return}if(!r?.skipProjectCheck){const s=T(e,"cdk.json");if(!E(s)){t(i(`No CDK project found in ${e}`));return}}let a="",m="",f=!1;const S=this.argBuilder.buildContextArgs(r?.context),d=[...x.prefixArgs,...c,...S],C={...this.argBuilder.buildCdkEnv(r),NO_COLOR:"1",...r?.extraEnv?M(r.extraEnv):{}};$.debug("CdkService","Spawning CDK process",{command:`${x.command} ${d.join(" ")}`,workingDir:e});const o=K(x.command,d,{cwd:e,env:C,stdio:["ignore","pipe","pipe"],shell:!1,detached:!1});if(!o.pid){const s=`Failed to spawn CDK process - no PID. cwd=${e}, args=${d.join(" ")}`;o.on("error",n=>{$.debug("CdkProcess","Deferred spawn error on failed child process",{error:n.message})}),t(i(s));return}const l=`cdk-${++this.processCounter}`;this.runningProcesses.set(l,o);let p=!1;const k=r?.timeout??1800*1e3,u=setTimeout(()=>{o.killed||(f=!0,this.forceKillProcess(o))},k);o.stdout?.on("data",s=>{const n=s.toString();a+=n,r?.outputCallback&&r.outputCallback(g(n)),r?.cdkOutputLogger?.writeCdkOutput("stdout",g(n))}),o.stderr?.on("data",s=>{const n=s.toString();r?.cdkOutputLogger?.writeCdkOutput("stderr",g(n)),!n.includes("deprecated")&&!n.includes("npm WARN")&&!n.includes("ENOENT")&&(m+=n),r?.errorCallback&&r.errorCallback(g(n))}),o.on("error",s=>{clearTimeout(u),this.runningProcesses.delete(l),!(p||f)&&(p=!0,t(i(w(s))))}),o.on("close",s=>{if(clearTimeout(u),this.runningProcesses.delete(l),p)return;if(p=!0,f){t(i("CDK command timed out"));return}const n=s===0||r?.ignoreExitCode===!0&&s===1,O=a+(m?`
2
- ${m}`:"");if(n){const P=r?.combineOutput?O:a;t(H({output:g(P),exitCode:s||0}));return}let b=m;if(a){const P=a.match(/❌.*?Error:(.*)$/m);P&&(b=P[1]?.trim()??"")}const I=b||`CDK command failed with exit code ${s}`,y=a?`${I}
3
- ${a}`:I;t(i(g(y)))})})}}export{v as CdkProcessManager};
1
+ import{spawn as K}from"child_process";import{existsSync as E}from"fs";import{join as b}from"path";import{createRequire as A}from"module";import{logger as T}from"@fjall/util/logger";import{filterDangerousEnvVars as L,maskSensitiveOutput as g}from"@fjall/util";import{success as O,failure as i}from"@fjall/generator";import{getErrorMessage as w}from"@fjall/util";function j(){try{const e=A(import.meta.url).resolve("aws-cdk/bin/cdk");return{command:process.execPath,prefixArgs:[e]}}catch(h){return T.debug("CdkService","Failed to resolve aws-cdk binary, falling back to npx",{error:h instanceof Error?h.message:String(h)}),{command:"npx",prefixArgs:["cdk"]}}}const x=j(),G=5e3,H=1800*1e3;class J{runningProcesses=new Map;processCounter=0;argBuilder;exitHandler;sigintHandler;sigtermHandler;constructor(e){this.argBuilder=e,this.exitHandler=()=>this.cleanup(),this.sigintHandler=()=>{this.cleanup(),process.exit(130)},this.sigtermHandler=()=>{this.cleanup(),process.exit(143)},process.on("exit",this.exitHandler),process.on("SIGINT",this.sigintHandler),process.on("SIGTERM",this.sigtermHandler)}forceKillProcess(e){e.stdout?.destroy(),e.stderr?.destroy(),e.kill("SIGTERM");const c=setTimeout(()=>{e.exitCode===null&&e.kill("SIGKILL")},G);c.unref(),e.once("exit",()=>clearTimeout(c))}cleanup(){for(const[e,c]of this.runningProcesses)c.killed||(c.stdout?.destroy(),c.stderr?.destroy(),c.kill("SIGTERM"));this.runningProcesses.clear()}dispose(){this.cleanup(),process.removeListener("exit",this.exitHandler),process.removeListener("SIGINT",this.sigintHandler),process.removeListener("SIGTERM",this.sigtermHandler)}async runCdkCommandPassthrough(e,c,r){return new Promise(t=>{if(!E(e)){t(i(`Directory not found: ${e}`));return}const a=b(e,"cdk.json");if(!E(a)){t(i(`No CDK project found in ${e}`));return}const m=this.argBuilder.buildContextArgs(r?.context),f=[...x.prefixArgs,...c,...m],S=this.argBuilder.buildCdkEnv(r),d=K(x.command,f,{cwd:e,env:S,stdio:"inherit",shell:!1,detached:!1});if(!d.pid){d.on("error",u=>{T.debug("CdkProcess","Spawn error on failed child process",{error:u.message})}),t(i(`Failed to spawn CDK process - no PID. cwd=${e}, args=${f.join(" ")}`));return}const C=`cdk-passthrough-${++this.processCounter}`;this.runningProcesses.set(C,d);let o=!1,l=!1;const p=r?.timeout??H,k=setTimeout(()=>{d.killed||(o=!0,this.forceKillProcess(d))},p);d.on("close",u=>{if(clearTimeout(k),this.runningProcesses.delete(C),!l){if(l=!0,o){t(i("CDK command timed out"));return}u===0||r?.ignoreExitCode?t(O({exitCode:u||0})):t(i(`CDK command failed with exit code ${u}`))}}),d.on("error",u=>{clearTimeout(k),this.runningProcesses.delete(C),!(l||o)&&(l=!0,t(i(`Failed to run CDK command: ${u.message}`)))})})}async runCdkCommand(e,c,r){return new Promise(t=>{if(!E(e)){t(i(`Directory not found: ${e}`));return}if(!r?.skipProjectCheck){const s=b(e,"cdk.json");if(!E(s)){t(i(`No CDK project found in ${e}`));return}}let a="",m="",f=!1;const S=this.argBuilder.buildContextArgs(r?.context),d=[...x.prefixArgs,...c,...S],C={...this.argBuilder.buildCdkEnv(r),NO_COLOR:"1",...r?.extraEnv?L(r.extraEnv):{}};T.debug("CdkService","Spawning CDK process",{command:`${x.command} ${d.join(" ")}`,workingDir:e});const o=K(x.command,d,{cwd:e,env:C,stdio:["ignore","pipe","pipe"],shell:!1,detached:!1});if(!o.pid){const s=`Failed to spawn CDK process - no PID. cwd=${e}, args=${d.join(" ")}`;o.on("error",n=>{T.debug("CdkProcess","Deferred spawn error on failed child process",{error:n.message})}),t(i(s));return}const l=`cdk-${++this.processCounter}`;this.runningProcesses.set(l,o);let p=!1;const k=r?.timeout??H,u=setTimeout(()=>{o.killed||(f=!0,this.forceKillProcess(o))},k);o.stdout?.on("data",s=>{const n=s.toString();a+=n,r?.outputCallback&&r.outputCallback(g(n)),r?.cdkOutputLogger?.writeCdkOutput("stdout",g(n))}),o.stderr?.on("data",s=>{const n=s.toString();r?.cdkOutputLogger?.writeCdkOutput("stderr",g(n)),!n.includes("deprecated")&&!n.includes("npm WARN")&&!n.includes("ENOENT")&&(m+=n),r?.errorCallback&&r.errorCallback(g(n))}),o.on("error",s=>{clearTimeout(u),this.runningProcesses.delete(l),!(p||f)&&(p=!0,t(i(w(s))))}),o.on("close",s=>{if(clearTimeout(u),this.runningProcesses.delete(l),p)return;if(p=!0,f){t(i("CDK command timed out"));return}const n=s===0||r?.ignoreExitCode===!0&&s===1,M=a+(m?`
2
+ ${m}`:"");if(n){const P=r?.combineOutput?M:a;t(O({output:g(P),exitCode:s||0}));return}let $=m;if(a){const P=a.match(/❌.*?Error:(.*)$/m);P&&($=P[1]?.trim()??"")}const I=$||`CDK command failed with exit code ${s}`,y=a?`${I}
3
+ ${a}`:I;t(i(g(y)))})})}}export{J as CdkProcessManager};
@@ -15,6 +15,14 @@ export interface CdkContext {
15
15
  fjallAccountGlobalsConfigured?: string;
16
16
  orgConfig?: string;
17
17
  fjallAdoptBackupVault?: string;
18
+ /**
19
+ * JSON `Record<secretName, completeArn>` of deploy-time-resolved externally-managed
20
+ * Secrets Manager ARNs. Emitted as `-c fjallResolvedSecretArns=<json>` and consumed
21
+ * by `resolveImportedSecret` in @fjall/components-infrastructure
22
+ * (`resources/aws/secrets/secret.ts`) via `fromSecretCompleteArn`. Keep the context
23
+ * key literal in sync with that reader.
24
+ */
25
+ resolvedSecretArns?: string;
18
26
  }
19
27
  export interface CdkOptions {
20
28
  verbose?: boolean;
@@ -1 +1 @@
1
- import{getApplicationStackName as i,getOrganisationStackName as u,isApplicationStack as l}from"../../types/operations.js";const p=5e3;function t(a,o){if(a&&!a.includes("*"))return a;if(a){const e=a.match(/\*?(\w+)\*?/);if(e?.[1]){const n=e[1],r=o.target;return l(n)?i(r,n):`${r}${n}`}return a}}function c(a){const o=a.deployType;return o==="organisation"||o==="platform"||o==="account"?u(o):`${a.target}Network`}function f(a,o,e){return{accountId:o,region:e,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,orgConfig:a.orgConfig,fjallAdoptBackupVault:a.fjallAdoptBackupVault?"true":void 0}}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 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};
@@ -25,6 +25,7 @@ export interface DeploymentContext {
25
25
  fjallAccountGlobalsConfigured?: boolean;
26
26
  orgConfig?: string;
27
27
  fjallAdoptBackupVault?: boolean;
28
+ resolvedSecretArns?: string;
28
29
  options: {
29
30
  verbose?: boolean;
30
31
  infraOnly?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/deploy-core",
3
- "version": "2.9.1",
3
+ "version": "2.12.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",
@@ -71,10 +71,11 @@
71
71
  "@aws-sdk/client-organizations": "^3.1038.0",
72
72
  "@aws-sdk/client-ram": "^3.1038.0",
73
73
  "@aws-sdk/client-s3": "^3.1038.0",
74
+ "@aws-sdk/client-secrets-manager": "^3.1038.0",
74
75
  "@aws-sdk/client-sso-admin": "^3.1038.0",
75
76
  "@aws-sdk/client-sts": "^3.1038.0",
76
- "@fjall/generator": "^2.9.1",
77
- "@fjall/util": "^2.9.1",
77
+ "@fjall/generator": "^2.12.0",
78
+ "@fjall/util": "^2.12.0",
78
79
  "@smithy/node-http-handler": "^4.6.1",
79
80
  "tsx": "^4.21.0",
80
81
  "zod": "^4.4.3"
@@ -83,5 +84,5 @@
83
84
  "@types/node": "^25.6.0",
84
85
  "vitest": "^4.1.5"
85
86
  },
86
- "gitHead": "a97423cf3df727994364a0907fa2b5c544a86b0d"
87
+ "gitHead": "dca39a47da956d3d94c689dd782fe285d711d25e"
87
88
  }