@fjall/components-infrastructure 4.2.0 → 4.4.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.
@@ -1,4 +1,5 @@
1
- import { type GovernancePreset } from "@fjall/generator";
1
+ import { type GovernancePreset, type MachineTrustMode } from "@fjall/generator";
2
+ import { type AccountTier } from "@fjall/util";
2
3
  import { Construct } from "constructs";
3
4
  export interface OidcConnectorProps {
4
5
  fjallOrgId: string;
@@ -19,6 +20,27 @@ export interface OidcConnectorProps {
19
20
  * other stage ⇒ deploy role only.
20
21
  */
21
22
  accountStage?: string;
23
+ /**
24
+ * Account TIER of the connected account (organisation | platform | account) —
25
+ * the structural TIER axis, from the account stack's polymorphic
26
+ * `deployRoleAccountTier()`; NEVER the wire `environment` / stage axis (that is
27
+ * `accountStage`). Drives the §4.4a deploy-trust governance expand: a
28
+ * governance tier (organisation/platform) plus the synth account id widens the
29
+ * `FjallDeploy` machine subject to ALSO accept the account-scoped literal
30
+ * `org:{orgId}:acct:{awsAccountId}` (StringEquals array = OR of exact
31
+ * literals), so per-account deploy issuance can later flip. App-tier
32
+ * ("account") or absent ⇒ generic-only scalar (unchanged, fail-safe).
33
+ */
34
+ accountTier?: AccountTier;
35
+ /**
36
+ * §4.4a Step-3 contract knob, forwarded verbatim to
37
+ * `buildNarrowedDeployTrustStatements`. Absent ⇒ `"expanded"` (the safe
38
+ * shared default). `"account-scoped"` renders the machine statement with the
39
+ * account-scoped literal ONLY and throws at synth unless `accountTier` is
40
+ * governance — opt in solely where per-account issuance is known-flipped
41
+ * (see `MachineTrustMode` in `@fjall/generator`).
42
+ */
43
+ machineTrustMode?: MachineTrustMode;
22
44
  }
23
45
  export declare class OidcConnector extends Construct {
24
46
  readonly deployRoleArn: string;
@@ -115,10 +115,20 @@ export class OidcConnector extends Construct {
115
115
  statements: SECURITY_TIER_TO_BOUNDARY[props.securityTier].map((statement) => iam.PolicyStatement.fromJson(statement))
116
116
  })
117
117
  : undefined;
118
+ // Concrete when env.account is set, a token otherwise — both render. Feeds
119
+ // the governance-expand machine subject and the dev-tier surface below.
120
+ const accountId = Stack.of(this).account;
118
121
  const trustParams = {
119
122
  fjallOrgId: props.fjallOrgId,
120
123
  providerArn,
121
- issuerDomain
124
+ issuerDomain,
125
+ accountId,
126
+ ...(props.accountTier !== undefined && {
127
+ accountTier: props.accountTier
128
+ }),
129
+ ...(props.machineTrustMode !== undefined && {
130
+ machineTrustMode: props.machineTrustMode
131
+ })
122
132
  };
123
133
  const deployRole = new Role(this, "DeployRole", {
124
134
  roleName: `FjallDeploy${props.fjallOrgId}`,
@@ -141,7 +151,6 @@ export class OidcConnector extends Construct {
141
151
  // with the Quick-Create yaml's IsDevelopmentStage-conditional resources
142
152
  // and the webapp dev-gate-remediate pass).
143
153
  if (props.accountStage === "development") {
144
- const accountId = Stack.of(this).account;
145
154
  new ManagedPolicy(this, "DevBoundary", {
146
155
  managedPolicyName: DEV_BOUNDARY_POLICY_NAME,
147
156
  description: "Fjall dev-tier permissions boundary — ceiling for the per-slot task roles the dev provisioner creates",
@@ -12,7 +12,8 @@ import type { SecurityHubHubProps } from "../../config/aws/securityHubHub.js";
12
12
  import type { ConfigRecorderProps } from "../../config/aws/configRecorder.js";
13
13
  import type { ConfigRulePresetProps } from "../../config/aws/configRulePreset.js";
14
14
  import type { OrganisationType } from "./interfaces/organisation.js";
15
- import { type GovernancePreset } from "@fjall/generator";
15
+ import { type GovernancePreset, type MachineTrustMode } from "@fjall/generator";
16
+ import { type AccountTier } from "@fjall/util";
16
17
  export interface AccountProps extends StackProps {
17
18
  accountId?: string;
18
19
  region?: string;
@@ -33,6 +34,18 @@ export interface AccountProps extends StackProps {
33
34
  * `securityTier`.
34
35
  */
35
36
  alarmSubscriptions?: SharedAlarmTopicProps["subscriptions"];
37
+ /**
38
+ * §4.4a Step-3 contract knob, forwarded to this account's `OidcConnector`.
39
+ * Absent ⇒ `"expanded"` (the safe shared default — governance trust accepts
40
+ * both the generic and account-scoped machine subjects).
41
+ * `"account-scoped"` contracts the FjallDeploy machine trust to the
42
+ * account-scoped literal only; it throws at synth on a non-governance tier.
43
+ * Set it ONLY in a deployment whose per-account deploy-token issuance is
44
+ * known-flipped (Fjall's own governance accounts) — for any other org it
45
+ * would lock governance CI out at STS. Sourced from the generated
46
+ * `infrastructure.ts`, same as `securityTier`.
47
+ */
48
+ machineTrustMode?: MachineTrustMode;
36
49
  }
37
50
  export declare class Account extends Stack {
38
51
  readonly organisationType: OrganisationType;
@@ -47,6 +60,19 @@ export declare class Account extends Stack {
47
60
  * independent, and Platform must take one but not the other.
48
61
  */
49
62
  protected receivesDeployRole(): boolean;
63
+ /**
64
+ * Account TIER passed to this account's `OidcConnector` for the §4.4a
65
+ * deploy-trust governance expand. A polymorphic METHOD, not a read of the
66
+ * `organisationType` field: the connector is instantiated in THIS base
67
+ * constructor, and subclass field initialisers run AFTER `super()`, so
68
+ * `this.organisationType` would read "account" for every subclass here (the
69
+ * same construction-order reason `receivesDeployRole()` is a method). Base ⇒
70
+ * "account" (app-tier, generic-only trust); `Platform` ⇒ "platform" and
71
+ * `Organisation` ⇒ "organisation" override. Governance tiers widen the deploy
72
+ * machine subject; kept in lockstep with `organisationType` by the
73
+ * Platform-synth deploy-trust parity test.
74
+ */
75
+ protected deployRoleAccountTier(): AccountTier;
50
76
  /**
51
77
  * Whether this account creates the default-ECR-image helper (the CodeBuild
52
78
  * project that seeds a placeholder application image). App-hosting accounts
@@ -81,8 +81,12 @@ export class Account extends Stack {
81
81
  new OidcConnector(this, "OidcConnector", {
82
82
  fjallOrgId,
83
83
  accountStage: environment,
84
+ accountTier: this.deployRoleAccountTier(),
84
85
  ...(props.securityTier !== undefined && {
85
86
  securityTier: props.securityTier
87
+ }),
88
+ ...(props.machineTrustMode !== undefined && {
89
+ machineTrustMode: props.machineTrustMode
86
90
  })
87
91
  });
88
92
  }
@@ -146,6 +150,21 @@ export class Account extends Stack {
146
150
  receivesDeployRole() {
147
151
  return this.constructor === Account;
148
152
  }
153
+ /**
154
+ * Account TIER passed to this account's `OidcConnector` for the §4.4a
155
+ * deploy-trust governance expand. A polymorphic METHOD, not a read of the
156
+ * `organisationType` field: the connector is instantiated in THIS base
157
+ * constructor, and subclass field initialisers run AFTER `super()`, so
158
+ * `this.organisationType` would read "account" for every subclass here (the
159
+ * same construction-order reason `receivesDeployRole()` is a method). Base ⇒
160
+ * "account" (app-tier, generic-only trust); `Platform` ⇒ "platform" and
161
+ * `Organisation` ⇒ "organisation" override. Governance tiers widen the deploy
162
+ * machine subject; kept in lockstep with `organisationType` by the
163
+ * Platform-synth deploy-trust parity test.
164
+ */
165
+ deployRoleAccountTier() {
166
+ return "account";
167
+ }
149
168
  /**
150
169
  * Whether this account creates the default-ECR-image helper (the CodeBuild
151
170
  * project that seeds a placeholder application image). App-hosting accounts
@@ -5,6 +5,7 @@ import { type IdentityCentreConfig } from "../../config/aws/identityCentreConfig
5
5
  import { ScpPreset } from "../../config/aws/scpPreset.js";
6
6
  import type { ScpPresetProps } from "../../config/aws/scpPreset.js";
7
7
  import { OrganisationResource } from "../../resources/aws/organisation/index.js";
8
+ import { type AccountTier } from "@fjall/util";
8
9
  export type AccountsConfig = {
9
10
  readonly [key: string]: readonly string[] | string | AccountsConfig;
10
11
  };
@@ -58,6 +59,14 @@ export declare class Organisation extends Account {
58
59
  * gate, so no `OidcConnector` is synthesised on the root stack.
59
60
  */
60
61
  protected receivesDeployRole(): boolean;
62
+ /**
63
+ * Account TIER = organisation (the AWS Organizations management account). The
64
+ * root never instantiates an `OidcConnector` (`receivesDeployRole()` is false
65
+ * — its deploy trust is owned by the Quick-Create template), so this override
66
+ * is not reached on the CDK path today; it keeps `deployRoleAccountTier()`
67
+ * honest with `organisationType` for any future caller.
68
+ */
69
+ protected deployRoleAccountTier(): AccountTier;
61
70
  /**
62
71
  * The organisation root runs no application workloads, so it needs no
63
72
  * default-ECR-image helper.
@@ -125,6 +125,16 @@ export class Organisation extends Account {
125
125
  receivesDeployRole() {
126
126
  return false;
127
127
  }
128
+ /**
129
+ * Account TIER = organisation (the AWS Organizations management account). The
130
+ * root never instantiates an `OidcConnector` (`receivesDeployRole()` is false
131
+ * — its deploy trust is owned by the Quick-Create template), so this override
132
+ * is not reached on the CDK path today; it keeps `deployRoleAccountTier()`
133
+ * honest with `organisationType` for any future caller.
134
+ */
135
+ deployRoleAccountTier() {
136
+ return "organisation";
137
+ }
128
138
  /**
129
139
  * The organisation root runs no application workloads, so it needs no
130
140
  * default-ECR-image helper.
@@ -4,6 +4,7 @@ import { type AccountsConfig } from "./organisation.js";
4
4
  import { SecurityServicesAdmin } from "../../config/aws/securityServicesAdmin.js";
5
5
  import type { SecurityServicesAdminProps } from "../../config/aws/securityServicesAdmin.js";
6
6
  import type { OrganisationType } from "./interfaces/organisation.js";
7
+ import { type AccountTier } from "@fjall/util";
7
8
  export interface PlatformProps extends AccountProps {
8
9
  accounts?: AccountsConfig;
9
10
  }
@@ -11,5 +12,6 @@ export declare class Platform extends Account {
11
12
  readonly organisationType: OrganisationType;
12
13
  constructor(scope: Construct, id: string, props: PlatformProps);
13
14
  protected receivesDeployRole(): boolean;
15
+ protected deployRoleAccountTier(): AccountTier;
14
16
  enableSecurityServicesAdmin(props: SecurityServicesAdminProps): SecurityServicesAdmin;
15
17
  }
@@ -26,6 +26,9 @@ export class Platform extends Account {
26
26
  receivesDeployRole() {
27
27
  return true;
28
28
  }
29
+ deployRoleAccountTier() {
30
+ return "platform";
31
+ }
29
32
  enableSecurityServicesAdmin(props) {
30
33
  return new SecurityServicesAdmin(this, "SecurityServicesAdmin", props);
31
34
  }
@@ -11,7 +11,7 @@ export declare const CLICKHOUSE_DATABASE_NAME = "analytics";
11
11
  * columnar scans than Graviton2.
12
12
  *
13
13
  * Settings are tuned for 1 vCPU sustained (max_threads=1 across all profiles,
14
- * max_concurrent_queries=4) — see clickhouseUserData.ts. Bumping to a larger
14
+ * max_concurrent_queries=8) — see clickhouseUserData.ts. Bumping to a larger
15
15
  * instance MUST be paired with raising those thread/concurrency caps.
16
16
  *
17
17
  * Next size up: `m7g.large` (2 vCPU, 8 GiB) for sustained workloads, or
@@ -11,7 +11,7 @@ export const CLICKHOUSE_DATABASE_NAME = "analytics";
11
11
  * columnar scans than Graviton2.
12
12
  *
13
13
  * Settings are tuned for 1 vCPU sustained (max_threads=1 across all profiles,
14
- * max_concurrent_queries=4) — see clickhouseUserData.ts. Bumping to a larger
14
+ * max_concurrent_queries=8) — see clickhouseUserData.ts. Bumping to a larger
15
15
  * instance MUST be paired with raising those thread/concurrency caps.
16
16
  *
17
17
  * Next size up: `m7g.large` (2 vCPU, 8 GiB) for sustained workloads, or
@@ -90,7 +90,14 @@ export const PROFILE_NAME_PATTERN = NAME_PATTERN;
90
90
  * from the pre-Phase-4b inline `<app_writer>` / `<audit_writer>` /
91
91
  * `<backup_reader>` / `<schema_admin>` blocks at
92
92
  * `clickhouseUserData.ts:296-382` — zero behavioural drift from the
93
- * pre-4b prod resource caps.
93
+ * pre-4b prod resource caps, EXCEPT high_throughput_ingest's
94
+ * `maxConcurrentQueriesForUser`, deliberately raised 2→6 (2026-07-28):
95
+ * the webapp overview fires 6–10 queries per page load and the cap of 2
96
+ * threw TOO_MANY_SIMULTANEOUS_QUERIES instead of queueing (CH does not
97
+ * queue past this cap — `queue_max_wait_ms` empirically does not apply).
98
+ * Consumers pair it with a client-side FIFO gate
99
+ * (`webapp/app/.server/lib/clickhouse/appQuerySlots.ts`) whose limit
100
+ * MUST mirror this value — a users.xml parity test enforces the pair.
94
101
  *
95
102
  * Frozen via `Object.freeze` on the outer record AND each sub-object so
96
103
  * a consumer cannot mutate the defaults in place. Pass your own
@@ -101,7 +108,7 @@ const _ClickHouseDefaultProfilesRaw = {
101
108
  high_throughput_ingest: {
102
109
  maxThreads: 1,
103
110
  maxInsertThreads: 1,
104
- maxConcurrentQueriesForUser: 2,
111
+ maxConcurrentQueriesForUser: 6,
105
112
  logQueriesMinQueryDurationMs: 100,
106
113
  optimizeMoveToPrewhere: true,
107
114
  useQueryConditionCache: true,
@@ -119,13 +119,16 @@ export function generateServerConfigXml(options) {
119
119
  <!-- Filesystem page cache handles uncompressed-block warmth on the
120
120
  host side; an in-CH uncompressed cache would just duplicate it. -->
121
121
  <uncompressed_cache_size>0</uncompressed_cache_size>
122
- <!-- Sized for m7g.medium (1 vCPU sustained). 4 concurrent queries is the
123
- sweet spot: enough headroom for the OPTIMIZE FINAL sidecar + a single
124
- dashboard read + one async insert + one ad-hoc, but low enough that
125
- context switching does not dominate at the kernel scheduler level.
126
- Bump in lockstep with instance size if DEFAULT_CLICKHOUSE_INSTANCE_TYPE
127
- moves to a >=2 vCPU host. -->
128
- <max_concurrent_queries>4</max_concurrent_queries>
122
+ <!-- Server-wide cap across ALL users. Raised 4→8 (2026-07-28) alongside
123
+ high_throughput_ingest's per-user cap (2→6): the webapp overview
124
+ fires 6-10 queries per page load and N app tasks share this cap, so
125
+ 4 starved dashboard reads with TOO_MANY_SIMULTANEOUS_QUERIES (CH
126
+ rejects rather than queues past the cap). On the 1 vCPU m7g.medium
127
+ the extra slots time-slice rather than parallelise — acceptable for
128
+ short cache-hot dashboard reads; max_threads=1 per query keeps the
129
+ scheduler sane. Bump further in lockstep with instance size if
130
+ DEFAULT_CLICKHOUSE_INSTANCE_TYPE moves to a >=2 vCPU host. -->
131
+ <max_concurrent_queries>8</max_concurrent_queries>
129
132
  <background_pool_size>2</background_pool_size>
130
133
  <background_schedule_pool_size>2</background_schedule_pool_size>
131
134
  <background_merges_mutations_concurrency_ratio>2</background_merges_mutations_concurrency_ratio>
@@ -0,0 +1,37 @@
1
+ import { type EngineCompat } from "@fjall/util/manifest/schemas";
2
+ /**
3
+ * The constructs' own package version — the assembly's `synthesisedBy`.
4
+ * `undefined` only when the package layout is broken enough that the upward
5
+ * walk finds no matching `package.json`.
6
+ */
7
+ export declare const CONSTRUCTS_VERSION: string | undefined;
8
+ /**
9
+ * Pure core of the emitter: derive the `engineCompat` block from a given
10
+ * version string (or undefined). Both the absent/empty guard and the
11
+ * `${major}.0.0` non-integer-major derivation live here — the single source of
12
+ * the derivation, delegated to by both `MINIMUM_ENGINE_VERSION` and
13
+ * `buildEngineCompat` — so the fail-safe and malformed-version paths are
14
+ * unit-testable directly, without mocking module-load state. Returns undefined
15
+ * when the version is absent, empty, or has no valid integer major (fail-safe:
16
+ * the caller stamps no block).
17
+ */
18
+ export declare function buildEngineCompatFrom(version: string | undefined): EngineCompat | undefined;
19
+ /**
20
+ * The compatible deploy-engine floor for assemblies this constructs version
21
+ * synthesises: `${major}.0.0`, derived from `CONSTRUCTS_VERSION`. `undefined`
22
+ * when the version is unresolved or has a non-integer major (in which case
23
+ * `buildEngineCompat` stamps no block — fail-safe).
24
+ */
25
+ export declare const MINIMUM_ENGINE_VERSION: string | undefined;
26
+ /**
27
+ * Build the `engineCompat` block to stamp into the manifest, or `undefined`
28
+ * when the constructs' own version cannot be resolved (fail-safe: the caller
29
+ * stamps no block and the deploy degrades to a constraint-free proceed).
30
+ *
31
+ * Phase 1 emits only the two required fields. The verdict classification of the
32
+ * emitted floor is proven in `@fjall/deploy-core`'s
33
+ * `orchestration/application/__tests__/engineCompat.test.ts` (a `4.0.0` floor
34
+ * refuses a 3.x engine, proceeds for 4.x/5.x); this module's tests prove the
35
+ * emitted shape matches that floor and reads back through the gate's reader.
36
+ */
37
+ export declare function buildEngineCompat(): EngineCompat | undefined;
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Synth-time engine-compatibility stamp (the contract's self-honest emitter).
3
+ *
4
+ * The constructs package is the one place that knows which deploy engine can
5
+ * safely deploy the assembly THIS version of the constructs produced, so it is
6
+ * the correct emitter of the engine ↔ assembly version contract. At synth,
7
+ * `writeManifest` stamps the `engineCompat` block this module builds into
8
+ * `cdk.out/fjall-manifest.json`; the deploy engine (`@fjall/deploy-core`) reads
9
+ * it back at deploy-start (`readEngineCompatEnvelope` + `evaluateEngineCompat`)
10
+ * and refuses an incompatible pairing before any build / push / CloudFormation
11
+ * mutation. Both the CLI and the webapp worker inherit that one chokepoint.
12
+ *
13
+ * `synthesisedBy` is the constructs' OWN package version — audit/diagnostics
14
+ * only, never a gate input. It is resolved at module load by the same upward
15
+ * `package.json` walk `@fjall/deploy-core`'s `DEPLOY_CORE_VERSION` uses: the
16
+ * source layout (`lib/utils/engineCompat.ts`) and the compiled layout
17
+ * (`dist/lib/utils/engineCompat.js`) sit at different depths from the package
18
+ * root, so a fixed relative path would resolve the wrong file in one of them;
19
+ * matching on `name` finds the right `package.json` from both, and skips any
20
+ * unrelated `package.json` higher up (e.g. a consuming app's).
21
+ *
22
+ * `minimumEngineVersion` is the PRIMARY refuse gate. Under Lerna FIXED lockstep
23
+ * every `@fjall/*` package shares one version, so the compatible engine floor
24
+ * is this constructs major with a zeroed minor/patch (`${major}.0.0`) — DERIVED,
25
+ * never hardcoded, so it advances to `5.0.0` on the next major with no
26
+ * hand-edit. A same-major-or-newer engine proceeds; an older-major engine
27
+ * hard-refuses.
28
+ *
29
+ * The optional `maximumEngineMajor` / `minimumAwsCdkCli` bounds are deliberately
30
+ * NOT emitted in Phase 1 — they are ADR open question Q5 (engine-ahead cap) and
31
+ * the Phase-2 FM1 aws-cdk floor graft respectively, both unresolved and
32
+ * deferred. This emitter stamps only the two required fields.
33
+ *
34
+ * Fail-safe: if the constructs' own version cannot be resolved (a broken
35
+ * package layout), `buildEngineCompat` returns undefined and the caller stamps
36
+ * NO block — degrading to the constraint-free "proceed" behaviour that predates
37
+ * this contract rather than breaking synth. A resolution failure is logged, not
38
+ * silently swallowed.
39
+ */
40
+ import { fileURLToPath } from "url";
41
+ import { dirname, join, parse } from "path";
42
+ import { existsSync, readFileSync } from "fs";
43
+ import { getErrorMessage, maskSensitiveOutput } from "@fjall/util";
44
+ import { FjallLogger } from "./validationLogger.js";
45
+ const PACKAGE_NAME = "@fjall/components-infrastructure";
46
+ function resolveConstructsVersion() {
47
+ try {
48
+ let dir = dirname(fileURLToPath(import.meta.url));
49
+ const { root } = parse(dir);
50
+ for (;;) {
51
+ const candidate = join(dir, "package.json");
52
+ if (existsSync(candidate)) {
53
+ const pkg = JSON.parse(readFileSync(candidate, "utf8"));
54
+ if (pkg.name === PACKAGE_NAME &&
55
+ typeof pkg.version === "string" &&
56
+ pkg.version.length > 0) {
57
+ return pkg.version;
58
+ }
59
+ }
60
+ if (dir === root)
61
+ return undefined;
62
+ dir = dirname(dir);
63
+ }
64
+ }
65
+ catch (error) {
66
+ FjallLogger.warn(`Could not resolve ${PACKAGE_NAME} version for engineCompat: ` +
67
+ maskSensitiveOutput(getErrorMessage(error)));
68
+ return undefined;
69
+ }
70
+ }
71
+ /**
72
+ * The constructs' own package version — the assembly's `synthesisedBy`.
73
+ * `undefined` only when the package layout is broken enough that the upward
74
+ * walk finds no matching `package.json`.
75
+ */
76
+ export const CONSTRUCTS_VERSION = resolveConstructsVersion();
77
+ /**
78
+ * Pure core of the emitter: derive the `engineCompat` block from a given
79
+ * version string (or undefined). Both the absent/empty guard and the
80
+ * `${major}.0.0` non-integer-major derivation live here — the single source of
81
+ * the derivation, delegated to by both `MINIMUM_ENGINE_VERSION` and
82
+ * `buildEngineCompat` — so the fail-safe and malformed-version paths are
83
+ * unit-testable directly, without mocking module-load state. Returns undefined
84
+ * when the version is absent, empty, or has no valid integer major (fail-safe:
85
+ * the caller stamps no block).
86
+ */
87
+ export function buildEngineCompatFrom(version) {
88
+ if (version === undefined || version.length === 0)
89
+ return undefined;
90
+ const major = Number(version.split(".")[0]);
91
+ if (!Number.isInteger(major) || major < 0)
92
+ return undefined;
93
+ return {
94
+ synthesisedBy: version,
95
+ minimumEngineVersion: `${major}.0.0`
96
+ };
97
+ }
98
+ /**
99
+ * The compatible deploy-engine floor for assemblies this constructs version
100
+ * synthesises: `${major}.0.0`, derived from `CONSTRUCTS_VERSION`. `undefined`
101
+ * when the version is unresolved or has a non-integer major (in which case
102
+ * `buildEngineCompat` stamps no block — fail-safe).
103
+ */
104
+ export const MINIMUM_ENGINE_VERSION = buildEngineCompatFrom(CONSTRUCTS_VERSION)?.minimumEngineVersion;
105
+ /**
106
+ * Build the `engineCompat` block to stamp into the manifest, or `undefined`
107
+ * when the constructs' own version cannot be resolved (fail-safe: the caller
108
+ * stamps no block and the deploy degrades to a constraint-free proceed).
109
+ *
110
+ * Phase 1 emits only the two required fields. The verdict classification of the
111
+ * emitted floor is proven in `@fjall/deploy-core`'s
112
+ * `orchestration/application/__tests__/engineCompat.test.ts` (a `4.0.0` floor
113
+ * refuses a 3.x engine, proceeds for 4.x/5.x); this module's tests prove the
114
+ * emitted shape matches that floor and reads back through the gate's reader.
115
+ */
116
+ export function buildEngineCompat() {
117
+ return buildEngineCompatFrom(CONSTRUCTS_VERSION);
118
+ }
@@ -19,6 +19,7 @@ import { createHash } from "crypto";
19
19
  import { buildConstructMap, constructMapToRecord, ACCOUNT_CONSTRUCT_GROUPS } from "@fjall/util/constructMap";
20
20
  import { getErrorMessage, maskSensitiveOutput } from "@fjall/util";
21
21
  import { FJALL_MANIFEST_FILENAME, MANIFEST_SCHEMA_VERSION, FjallManifestSchema } from "@fjall/util/manifest/schemas";
22
+ import { buildEngineCompat } from "./engineCompat.js";
22
23
  import { FjallLogger } from "./validationLogger.js";
23
24
  /**
24
25
  * Collected manifest data during synthesis.
@@ -170,6 +171,12 @@ export function writeManifest(assembly, collector) {
170
171
  const stacks = computeStackHashes(cdkOutPath);
171
172
  // Build resource map from CDK manifest metadata
172
173
  const constructMap = buildConstructMap(cdkOutPath, collector.getConstructGroups());
174
+ const engineCompat = buildEngineCompat();
175
+ if (engineCompat === undefined) {
176
+ FjallLogger.warn("Could not determine constructs version — manifest stamped with no " +
177
+ "engineCompat block; the deploy engine will proceed without a " +
178
+ "version-compatibility gate.");
179
+ }
173
180
  const manifest = {
174
181
  version: MANIFEST_SCHEMA_VERSION,
175
182
  generatedAt: new Date().toISOString(),
@@ -177,6 +184,7 @@ export function writeManifest(assembly, collector) {
177
184
  services: collector.getServices(),
178
185
  lambdas: collector.getLambdas(),
179
186
  stacks,
187
+ ...(engineCompat !== undefined ? { engineCompat } : {}),
180
188
  ...(constructMap.size > 0
181
189
  ? { resourceMap: constructMapToRecord(constructMap) }
182
190
  : {})
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/components-infrastructure",
3
- "version": "4.2.0",
3
+ "version": "4.4.0",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "type": "module",
6
6
  "bin": {
@@ -67,8 +67,8 @@
67
67
  },
68
68
  "dependencies": {
69
69
  "@aws-sdk/client-organizations": "^3.1038.0",
70
- "@fjall/generator": "^4.2.0",
71
- "@fjall/util": "^4.2.0",
70
+ "@fjall/generator": "^4.4.0",
71
+ "@fjall/util": "^4.4.0",
72
72
  "constructs": "^10.6.0"
73
73
  },
74
74
  "overrides": {
@@ -82,5 +82,5 @@
82
82
  "engines": {
83
83
  "node": ">=18.0.0"
84
84
  },
85
- "gitHead": "c06f7ad6b4b63a99cc8e2fcf4f216ff1c14748c6"
85
+ "gitHead": "c700128600ab453fa63ce28b3effe8965373be43"
86
86
  }