@fjall/deploy-core 2.16.0 → 2.17.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
- 152 files minified at 2026-06-14T08:04:17.640Z
1
+ 152 files minified at 2026-06-15T10:18:02.165Z
@@ -40,6 +40,42 @@ export declare function buildParamsContext(params: {
40
40
  fjallAccountGlobalsConfigured?: boolean;
41
41
  fjallAccountTrailState?: string;
42
42
  };
43
+ /**
44
+ * Effective `skipOidc` for the single-component (platform/account) deploy path.
45
+ *
46
+ * A standalone single account's OIDC provider + FjallDeploy role are created by
47
+ * the connect-time Quick-Create stack (FjallOIDCConnector), so the Account
48
+ * stack must NOT recreate the account-global `https://fjall.io` provider — a
49
+ * second create throws EntityAlreadyExistsException and rolls the stack back.
50
+ * An org MEMBER is the opposite case: its provider + role are created AND
51
+ * managed by its own Account-stack OidcConnector, so skipping there drops the
52
+ * construct from the template and lets CloudFormation DELETE the provider and
53
+ * deploy role the account depends on. An explicit caller value always wins.
54
+ *
55
+ * Discriminator — two independent reasons to skip:
56
+ * 1. Solo estate — no organisation-tier account anywhere → Quick-Create owns
57
+ * the provider → skip. Absent org config reads as solo (favours the
58
+ * connected-account case that breaks).
59
+ * 2. Standalone management account — the estate HAS an organisation-tier
60
+ * account AND this deploy targets it (`targetIsOrganisationTier`). The only
61
+ * way an `account`-type deploy targets the organisation-tier (management)
62
+ * account is a management account connected standalone via Quick-Create; a
63
+ * real organisation deploys its root through `deployType: "organisation"`,
64
+ * which returns undefined here. Its provider is Quick-Create-owned → skip.
65
+ * Any other `account` target is an org member → do NOT skip. `targetIsOrganisationTier`
66
+ * is the resolved tier of the deploy target, supplied by the orchestrator — the
67
+ * helper never re-derives it from the estate.
68
+ *
69
+ * Maintainer note: Quick-Create is the canonical OWNER of the provider +
70
+ * FjallDeploy role for a standalone account — a deploy can only authenticate by
71
+ * assuming that role, so the provider must already exist before any deploy runs.
72
+ * Do NOT "fix" a removed-provider report by re-enabling Account-stack creation
73
+ * for a standalone account: a genuinely-deleted provider is restored by RECONNECT
74
+ * (re-running Quick-Create), not by redeploy. Re-enabling creation reintroduces
75
+ * the collision. Equally, do NOT widen the skip to org members — that deletes a
76
+ * provider the Account stack legitimately owns.
77
+ */
78
+ export declare function resolveAccountBootstrapSkipOidc(deployType: "organisation" | "platform" | "account", orgConfig: OrgConfig | undefined, explicitSkipOidc: boolean | undefined, targetIsOrganisationTier?: boolean): boolean | undefined;
43
79
  /** Forward onOutput callback — reduces lambda repetition across orchestration files. */
44
80
  export declare function forwardOutput(callbacks: DeployCallbacks): (chunk: string) => void;
45
81
  /** Forward onResourceProgress callback — reduces lambda repetition across orchestration files. */
@@ -1 +1 @@
1
- import{success as f,failure as i}from"@fjall/generator";import{getErrorMessage as m,maskSensitiveOutput as d,sleep as y}from"@fjall/util";import{logger as O}from"@fjall/util/logger";import{SimpleAwsProvider as R}from"../aws/SimpleAwsProvider.js";const S={account:"active",draining:"draining",org:"removed"};function w(e,r){return e!==void 0&&e!==""&&r!==void 0&&r!==""&&e!==r}function M(e){const r=w(e.region,e.primaryRegion);return{...e.orgConfig!==void 0?{orgConfig:JSON.stringify(e.orgConfig)}:{},...e.identity!==void 0?{fjallOrgId:e.identity.fjallOrgId}:{},...e.skipOidc?{fjallOidcConfigured:!0}:{},...r?{fjallAccountGlobalsConfigured:!0}:{},...e.trailLifecycle!==void 0?{fjallAccountTrailState:S[e.trailLifecycle]}:{}}}function _(e){return r=>e.onOutput?.(r)}function L(e){return r=>e.onResourceProgress?.(r)}function D(e){if(!e.success||e.data.length===0)return;const r={};for(const t of e.data)t.OutputKey&&t.OutputValue!==void 0&&(r[t.OutputKey]=t.OutputValue);return Object.keys(r).length>0?r:void 0}const E="OrganizationAccountAccessRole",l=5,$=5e3,b=3e4;function K(e){return`arn:aws:iam::${e}:role/${E}`}function T(e,r){return r===void 0?y(e):r.aborted?Promise.resolve():new Promise(t=>{const n=()=>{t()};r.addEventListener("abort",n,{once:!0}),y(e).then(()=>{r.removeEventListener("abort",n),t()})})}async function P(e,r,t,n,o){if(!e.assumeRole)return i(new Error("AwsProvider does not support assumeRole"));const u=K(r),g=e.assumeRole.bind(e);let s;for(let c=0;c<=l;c++)try{s=await g(u,n);break}catch(a){const p=a instanceof Error?a.name:void 0;if(p==="AccessDenied"||p==="AccessDeniedException")return i(new Error(`Access denied assuming ${E} in account ${r}. The role may not exist or may not trust the management account.`));if(c<l){const A=Math.min($*2**c,b);if(O.debug("assumeCascadeRole",`Attempt ${c+1} failed for account ${r}, retrying in ${Math.round(A/1e3)}s`,{error:d(m(a))}),await T(A,o),o?.aborted)return i(new Error(`Aborted while retrying assume-role for account ${r}`));continue}return i(new Error(`Failed to assume role in account ${r} after ${l+1} attempts: ${d(m(a))}`))}if(!s)return i(new Error(`Failed to assume role in account ${r}`));const C=new R({accessKeyId:s.accessKeyId,secretAccessKey:s.secretAccessKey,sessionToken:s.sessionToken,region:t,accountId:r});return f({provider:C,credentials:{accessKeyId:s.accessKeyId,secretAccessKey:s.secretAccessKey,sessionToken:s.sessionToken}})}async function j(e,r,t,n){const o=await e.cdkService.runCdkSynth(r,u=>t.onCdkOutput?.(u,"synth"));if(!o.success){const u=new Error(d(`${n}: ${o.error}`));return t.onError?.(u),i(u)}return f(void 0)}async function B(e,r,t){t.onCDKBootstrap?.("bootstrapping");const n=await e.cdkService.runCdkBootstrap(r,_(t));if(!n.success){t.onCDKBootstrap?.("failed");const o=new Error(d(`Bootstrap failed: ${n.error}`));return t.onError?.(o),i(o)}return t.onCDKBootstrap?.("complete"),f(void 0)}export{P as assumeCascadeRole,B as bootstrapOrFail,K as buildCascadeRoleArn,M as buildParamsContext,D as collectStackOutputs,_ as forwardOutput,L as forwardResourceProgress,j as synthOrFail,w as targetsNonPrimaryRegion};
1
+ import{success as f,failure as i}from"@fjall/generator";import{getErrorMessage as m,maskSensitiveOutput as d,sleep as y}from"@fjall/util";import{logger as C}from"@fjall/util/logger";import{hasOrganisationTierAccount as R}from"../aws/organisations/accountGlobals.js";import{SimpleAwsProvider as S}from"../aws/SimpleAwsProvider.js";const w={account:"active",draining:"draining",org:"removed"};function T(e,r){return e!==void 0&&e!==""&&r!==void 0&&r!==""&&e!==r}function D(e){const r=T(e.region,e.primaryRegion);return{...e.orgConfig!==void 0?{orgConfig:JSON.stringify(e.orgConfig)}:{},...e.identity!==void 0?{fjallOrgId:e.identity.fjallOrgId}:{},...e.skipOidc?{fjallOidcConfigured:!0}:{},...r?{fjallAccountGlobalsConfigured:!0}:{},...e.trailLifecycle!==void 0?{fjallAccountTrailState:w[e.trailLifecycle]}:{}}}function P(e,r,t,n){if(t!==void 0)return t;if(e==="account")return R(r?.providerAccounts)?n===!0:!0}function _(e){return r=>e.onOutput?.(r)}function B(e){return r=>e.onResourceProgress?.(r)}function j(e){if(!e.success||e.data.length===0)return;const r={};for(const t of e.data)t.OutputKey&&t.OutputValue!==void 0&&(r[t.OutputKey]=t.OutputValue);return Object.keys(r).length>0?r:void 0}const E="OrganizationAccountAccessRole",l=5,$=5e3,b=3e4;function h(e){return`arn:aws:iam::${e}:role/${E}`}function K(e,r){return r===void 0?y(e):r.aborted?Promise.resolve():new Promise(t=>{const n=()=>{t()};r.addEventListener("abort",n,{once:!0}),y(e).then(()=>{r.removeEventListener("abort",n),t()})})}async function F(e,r,t,n,o){if(!e.assumeRole)return i(new Error("AwsProvider does not support assumeRole"));const u=h(r),g=e.assumeRole.bind(e);let s;for(let c=0;c<=l;c++)try{s=await g(u,n);break}catch(a){const p=a instanceof Error?a.name:void 0;if(p==="AccessDenied"||p==="AccessDeniedException")return i(new Error(`Access denied assuming ${E} in account ${r}. The role may not exist or may not trust the management account.`));if(c<l){const A=Math.min($*2**c,b);if(C.debug("assumeCascadeRole",`Attempt ${c+1} failed for account ${r}, retrying in ${Math.round(A/1e3)}s`,{error:d(m(a))}),await K(A,o),o?.aborted)return i(new Error(`Aborted while retrying assume-role for account ${r}`));continue}return i(new Error(`Failed to assume role in account ${r} after ${l+1} attempts: ${d(m(a))}`))}if(!s)return i(new Error(`Failed to assume role in account ${r}`));const O=new S({accessKeyId:s.accessKeyId,secretAccessKey:s.secretAccessKey,sessionToken:s.sessionToken,region:t,accountId:r});return f({provider:O,credentials:{accessKeyId:s.accessKeyId,secretAccessKey:s.secretAccessKey,sessionToken:s.sessionToken}})}async function Y(e,r,t,n){const o=await e.cdkService.runCdkSynth(r,u=>t.onCdkOutput?.(u,"synth"));if(!o.success){const u=new Error(d(`${n}: ${o.error}`));return t.onError?.(u),i(u)}return f(void 0)}async function I(e,r,t){t.onCDKBootstrap?.("bootstrapping");const n=await e.cdkService.runCdkBootstrap(r,_(t));if(!n.success){t.onCDKBootstrap?.("failed");const o=new Error(d(`Bootstrap failed: ${n.error}`));return t.onError?.(o),i(o)}return t.onCDKBootstrap?.("complete"),f(void 0)}export{F as assumeCascadeRole,I as bootstrapOrFail,h as buildCascadeRoleArn,D as buildParamsContext,j as collectStackOutputs,_ as forwardOutput,B as forwardResourceProgress,P as resolveAccountBootstrapSkipOidc,Y as synthOrFail,T as targetsNonPrimaryRegion};
@@ -8,5 +8,5 @@ export interface OrgDetailsForSynth {
8
8
  rootId: string;
9
9
  managementAccountId: string;
10
10
  }
11
- export declare function buildOrgContext(params: DeployParams, services: DeployServices, operation: OrganisationOperation, deployType: "organisation" | "platform" | "account", orgDetails: OrgDetailsForSynth, accountName?: string, trailLifecycle?: ProviderAccount["trailLifecycle"]): import("../../types/deployment/DeploymentTypes.js").DeploymentContext;
11
+ export declare function buildOrgContext(params: DeployParams, services: DeployServices, operation: OrganisationOperation, deployType: "organisation" | "platform" | "account", orgDetails: OrgDetailsForSynth, accountName?: string, trailLifecycle?: ProviderAccount["trailLifecycle"], targetIsOrganisationTier?: boolean): import("../../types/deployment/DeploymentTypes.js").DeploymentContext;
12
12
  export declare function resolveOrgDetails(services: DeployServices, abortSignal?: AbortSignal): Promise<Result<OrgDetailsForSynth>>;
@@ -1 +1 @@
1
- import{success as a,failure as c}from"@fjall/generator";import{OrganizationsClient as s}from"@aws-sdk/client-organizations";import{CdkContextBuilder as u}from"../../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as m}from"../../types/deployment/index.js";import{ensureOrganisationExists as f}from"../../aws/organisations/organisation.js";import{buildParamsContext as l}from"../contextHelpers.js";function b(t,n,r,o,i,d,g){const e=n.awsProvider.getRegion();return u.buildDeploymentContext({deployType:o,target:r.target,path:r.path,region:e,accountName:d,callerIdentity:m(n.awsProvider.getAccountId()),orgId:i.orgId,rootId:i.rootId,managementAccountId:i.managementAccountId,...l({orgConfig:t.orgConfig,identity:t.identity,skipOidc:t.options?.skipOidc,...o!=="organisation"?{region:e,primaryRegion:t.orgConfig?.primaryRegion}:{},trailLifecycle:g})},{verbose:t.options?.verbose,infraOnly:t.options?.infraOnly},t.orgConfig)}async function v(t,n){const r=t.awsProvider.getClient(s),o=await f(r,n);return o.success?a({orgId:o.data.orgId,rootId:o.data.rootId,managementAccountId:o.data.managementAccountId}):c(o.error)}export{b as buildOrgContext,v as resolveOrgDetails};
1
+ import{success as a,failure as s}from"@fjall/generator";import{OrganizationsClient as u}from"@aws-sdk/client-organizations";import{CdkContextBuilder as f}from"../../services/supporting/CdkContextBuilder.js";import{stubCallerIdentity as m}from"../../types/deployment/index.js";import{ensureOrganisationExists as l}from"../../aws/organisations/organisation.js";import{buildParamsContext as I,resolveAccountBootstrapSkipOidc as C}from"../contextHelpers.js";function A(t,n,r,o,i,g,d,c){const e=n.awsProvider.getRegion();return f.buildDeploymentContext({deployType:o,target:r.target,path:r.path,region:e,accountName:g,callerIdentity:m(n.awsProvider.getAccountId()),orgId:i.orgId,rootId:i.rootId,managementAccountId:i.managementAccountId,...I({orgConfig:t.orgConfig,identity:t.identity,skipOidc:C(o,t.orgConfig,t.options?.skipOidc,c),...o!=="organisation"?{region:e,primaryRegion:t.orgConfig?.primaryRegion}:{},trailLifecycle:d})},{verbose:t.options?.verbose,infraOnly:t.options?.infraOnly},t.orgConfig)}async function k(t,n){const r=t.awsProvider.getClient(u),o=await l(r,n);return o.success?a({orgId:o.data.orgId,rootId:o.data.rootId,managementAccountId:o.data.managementAccountId}):s(o.error)}export{A as buildOrgContext,k as resolveOrgDetails};
@@ -1 +1 @@
1
- import{success as A}from"@fjall/generator";import{BackupClient as b}from"@aws-sdk/client-backup";import{IAMClient as N}from"@aws-sdk/client-iam";import{getOrganisationStackName as w}from"../../types/operations.js";import{accountHasDisasterRecovery as D,describeBackupVaultExists as k}from"../../aws/organisations/backup.js";import{describeAccountGlobalsExist as y,buildMissingAccountGlobalsAdvisory as I}from"../../aws/organisations/accountGlobals.js";import{targetsNonPrimaryRegion as L,synthOrFail as T,bootstrapOrFail as h,forwardOutput as M,forwardResourceProgress as v}from"../contextHelpers.js";import{accountTier as F}from"@fjall/util";import{buildOrgContext as G,resolveOrgDetails as Y}from"./orgContext.js";import{INFRA_STEPS as o,INFRA_STEP_TOTAL as i,failPrepareStep as c,maskAndFail as u,readStackOutputsBestEffort as x}from"./infraSteps.js";async function W(e,n,a,s,S){const{callbacks:t}=e;t.onStepComplete?.(o.CONNECT.id,o.CONNECT.name,"completed",0,i),t.onStepStart?.(o.PREPARE.id,o.PREPARE.name,1,i);const p=n.awsProvider.getRegion(),l=e.orgConfig?.primaryRegion;if(l!==void 0&&L(p,l)){const r=await y(n.awsProvider.getClient(N),e.abortSignal);if(!r.success)return c(t),u(t,r.error.message);if(!r.data)return c(t),u(t,I({target:a.target,deployRegion:p,primaryRegion:l,...e.orgConfig!==void 0?{providerAccounts:e.orgConfig.providerAccounts}:{}}))}const g=await Y(n,e.abortSignal);if(!g.success)return c(t),u(t,g.error.message);const f=s==="account"?e.orgConfig?.providerAccounts.find(r=>r.name===a.target):e.orgConfig?.providerAccounts.find(r=>F(r)==="platform"),d=G(e,n,a,s,g.data,s==="account"?a.target:void 0,f?.trailLifecycle),m=e.orgConfig?.disasterRecoveryRegion;if(s==="account"&&(m!==void 0&&m!=="")&&(f===void 0||D(f.environment,m))){const r=await k(n.awsProvider.getClient(b),e.abortSignal);if(!r.success)return c(t),u(t,r.error.message);d.fjallAdoptBackupVault=r.data}t.onLog?.(`Synthesising ${s} infrastructure\u2026`,"info");const R=await T(n,d,t,"CDK synthesis failed");if(!R.success)return c(t),R;const P=await h(n,d,t);if(!P.success)return c(t),P;t.onStepComplete?.(o.PREPARE.id,o.PREPARE.name,"completed",1,i);const C=w(a.type);t.onStepStart?.(o.DEPLOY.id,o.DEPLOY.name,2,i);const O=await n.cdkService.runCdkDeploy(d,C,M(t),v(t),n.awsProvider);if(!O.success)return t.onStepComplete?.(o.DEPLOY.id,o.DEPLOY.name,"error",2,i),u(t,O.error);t.onStepComplete?.(o.DEPLOY.id,o.DEPLOY.name,"completed",2,i);const E=await x(n,t,C,"Failed to read stack outputs (non-critical)");return t.onStepStart?.(o.MONITORING.id,o.MONITORING.name,3,i),t.onStepComplete?.(o.MONITORING.id,o.MONITORING.name,"completed",3,i),A({target:a.target,deploymentType:"organisation",outputs:E,durationMs:Date.now()-S})}export{W as deploySingleComponent};
1
+ import{success as N}from"@fjall/generator";import{BackupClient as w}from"@aws-sdk/client-backup";import{IAMClient as D}from"@aws-sdk/client-iam";import{getOrganisationStackName as I}from"../../types/operations.js";import{accountHasDisasterRecovery as k,describeBackupVaultExists as y}from"../../aws/organisations/backup.js";import{describeAccountGlobalsExist as L,buildMissingAccountGlobalsAdvisory as T}from"../../aws/organisations/accountGlobals.js";import{targetsNonPrimaryRegion as h,synthOrFail as M,bootstrapOrFail as v,forwardOutput as F,forwardResourceProgress as G}from"../contextHelpers.js";import{accountTier as S}from"@fjall/util";import{buildOrgContext as Y,resolveOrgDetails as x}from"./orgContext.js";import{INFRA_STEPS as o,INFRA_STEP_TOTAL as i,failPrepareStep as c,maskAndFail as d,readStackOutputsBestEffort as B}from"./infraSteps.js";async function X(e,r,a,s,E){const{callbacks:t}=e;t.onStepComplete?.(o.CONNECT.id,o.CONNECT.name,"completed",0,i),t.onStepStart?.(o.PREPARE.id,o.PREPARE.name,1,i);const R=r.awsProvider.getRegion(),l=e.orgConfig?.primaryRegion;if(l!==void 0&&h(R,l)){const n=await L(r.awsProvider.getClient(D),e.abortSignal);if(!n.success)return c(t),d(t,n.error.message);if(!n.data)return c(t),d(t,T({target:a.target,deployRegion:R,primaryRegion:l,...e.orgConfig!==void 0?{providerAccounts:e.orgConfig.providerAccounts}:{}}))}const f=await x(r,e.abortSignal);if(!f.success)return c(t),d(t,f.error.message);const u=s==="account"?e.orgConfig?.providerAccounts.find(n=>n.name===a.target):e.orgConfig?.providerAccounts.find(n=>S(n)==="platform"),A=s==="account"&&u!==void 0&&S(u)==="organisation",g=Y(e,r,a,s,f.data,s==="account"?a.target:void 0,u?.trailLifecycle,A),m=e.orgConfig?.disasterRecoveryRegion;if(s==="account"&&(m!==void 0&&m!=="")&&(u===void 0||k(u.environment,m))){const n=await y(r.awsProvider.getClient(w),e.abortSignal);if(!n.success)return c(t),d(t,n.error.message);g.fjallAdoptBackupVault=n.data}t.onLog?.(`Synthesising ${s} infrastructure\u2026`,"info");const p=await M(r,g,t,"CDK synthesis failed");if(!p.success)return c(t),p;const P=await v(r,g,t);if(!P.success)return c(t),P;t.onStepComplete?.(o.PREPARE.id,o.PREPARE.name,"completed",1,i);const C=I(a.type);t.onStepStart?.(o.DEPLOY.id,o.DEPLOY.name,2,i);const O=await r.cdkService.runCdkDeploy(g,C,F(t),G(t),r.awsProvider);if(!O.success)return t.onStepComplete?.(o.DEPLOY.id,o.DEPLOY.name,"error",2,i),d(t,O.error);t.onStepComplete?.(o.DEPLOY.id,o.DEPLOY.name,"completed",2,i);const b=await B(r,t,C,"Failed to read stack outputs (non-critical)");return t.onStepStart?.(o.MONITORING.id,o.MONITORING.name,3,i),t.onStepComplete?.(o.MONITORING.id,o.MONITORING.name,"completed",3,i),N({target:a.target,deploymentType:"organisation",outputs:b,durationMs:Date.now()-E})}export{X as deploySingleComponent};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/deploy-core",
3
- "version": "2.16.0",
3
+ "version": "2.17.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",
@@ -78,8 +78,8 @@
78
78
  "@aws-sdk/client-sqs": "^3.1067.0",
79
79
  "@aws-sdk/client-sso-admin": "^3.1038.0",
80
80
  "@aws-sdk/client-sts": "^3.1038.0",
81
- "@fjall/generator": "^2.16.0",
82
- "@fjall/util": "^2.16.0",
81
+ "@fjall/generator": "^2.17.0",
82
+ "@fjall/util": "^2.17.0",
83
83
  "@smithy/node-http-handler": "^4.6.1",
84
84
  "tsx": "^4.21.0",
85
85
  "zod": "^4.4.3"
@@ -88,5 +88,5 @@
88
88
  "@types/node": "^25.6.0",
89
89
  "vitest": "^4.1.5"
90
90
  },
91
- "gitHead": "2383b19f1e7db980ae603a6c75dca2c61b7a1d42"
91
+ "gitHead": "21cfe1aae339e12183af2813ec81f581b9b77d49"
92
92
  }