@fjall/components-infrastructure 2.11.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.
@@ -1,9 +1,27 @@
1
1
  import { Construct } from "constructs";
2
+ export interface S3BlockPublicAccessProps {
3
+ /**
4
+ * Account whose account-level S3 Block Public Access is managed. Taken as a
5
+ * prop rather than read from `Stack.of(this).account` — that is a synth token
6
+ * that never matches a provider-account id when looking up config.
7
+ */
8
+ accountId: string;
9
+ }
2
10
  /**
3
- * Enables S3 Block Public Access at the account level via a Custom Resource.
4
- * All four public access flags are set to true (block all public access).
5
- * Stack deletion does NOT revert this setting security features are preserved.
11
+ * Manages account-level S3 Block Public Access via a Custom Resource.
12
+ *
13
+ * Off by default: Fjall observes-and-flags exposed buckets through the posture
14
+ * layer rather than hard-blocking at the account level. Set
15
+ * `s3BlockPublicAccess: "enforced"` on a provider account to switch all four
16
+ * flags on. The flags ride in the custom resource's properties (not hardcoded
17
+ * in the handler) so a config change reconciles on the next deploy. Stack
18
+ * deletion is a no-op — the account-level setting is left as-is.
19
+ *
20
+ * The handler runs under a fixed `/fjall/FjallS3BpaManager` role so the
21
+ * DenyS3PublicAccessChanges SCP can exempt its PutAccountPublicAccessBlock by
22
+ * ARN pattern. The role name is account-global, so this construct is created
23
+ * only in an account's home region (see Account's accountGlobals gate).
6
24
  */
7
25
  export declare class S3BlockPublicAccess extends Construct {
8
- constructor(scope: Construct, id: string);
26
+ constructor(scope: Construct, id: string, props: S3BlockPublicAccessProps);
9
27
  }
@@ -1,23 +1,41 @@
1
1
  import { Duration } from "aws-cdk-lib";
2
2
  import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
3
3
  import { Runtime } from "aws-cdk-lib/aws-lambda";
4
- import { Stack } from "aws-cdk-lib";
5
4
  import { Construct } from "constructs";
6
5
  import { CustomResource } from "../../resources/aws/utilities/customResource.js";
6
+ import { getConfig } from "../../utils/getConfig.js";
7
7
  /**
8
- * Enables S3 Block Public Access at the account level via a Custom Resource.
9
- * All four public access flags are set to true (block all public access).
10
- * Stack deletion does NOT revert this setting security features are preserved.
8
+ * Manages account-level S3 Block Public Access via a Custom Resource.
9
+ *
10
+ * Off by default: Fjall observes-and-flags exposed buckets through the posture
11
+ * layer rather than hard-blocking at the account level. Set
12
+ * `s3BlockPublicAccess: "enforced"` on a provider account to switch all four
13
+ * flags on. The flags ride in the custom resource's properties (not hardcoded
14
+ * in the handler) so a config change reconciles on the next deploy. Stack
15
+ * deletion is a no-op — the account-level setting is left as-is.
16
+ *
17
+ * The handler runs under a fixed `/fjall/FjallS3BpaManager` role so the
18
+ * DenyS3PublicAccessChanges SCP can exempt its PutAccountPublicAccessBlock by
19
+ * ARN pattern. The role name is account-global, so this construct is created
20
+ * only in an account's home region (see Account's accountGlobals gate).
11
21
  */
12
22
  export class S3BlockPublicAccess extends Construct {
13
- constructor(scope, id) {
23
+ constructor(scope, id, props) {
14
24
  super(scope, id);
25
+ const account = getConfig().providerAccounts.find((pa) => pa.id === props.accountId);
26
+ const enforce = (account?.s3BlockPublicAccess ?? "off") === "enforced";
15
27
  new CustomResource(this, "S3BlockPublicAccess", {
16
28
  runtime: Runtime.NODEJS_22_X,
17
29
  timeout: Duration.minutes(5),
18
- lambdaDescription: "Enables S3 Block Public Access at account level",
30
+ lambdaDescription: "Manages S3 Block Public Access at account level",
31
+ rolePath: "/fjall/",
32
+ roleName: "FjallS3BpaManager",
19
33
  properties: {
20
- AccountId: Stack.of(this).account
34
+ AccountId: props.accountId,
35
+ BlockPublicAcls: String(enforce),
36
+ IgnorePublicAcls: String(enforce),
37
+ BlockPublicPolicy: String(enforce),
38
+ RestrictPublicBuckets: String(enforce)
21
39
  },
22
40
  inlinePolicy: [
23
41
  new PolicyStatement({
@@ -32,20 +50,22 @@ export class S3BlockPublicAccess extends Construct {
32
50
  inlineCode: `
33
51
  const { S3ControlClient, PutPublicAccessBlockCommand } = require('@aws-sdk/client-s3-control');
34
52
 
53
+ const toBool = (v) => v === 'true' || v === true;
54
+
35
55
  exports.handler = async (event) => {
36
56
  const physicalResourceId = event.PhysicalResourceId || event.LogicalResourceId || 's3-block-public-access';
37
57
  if (event.RequestType === 'Delete') {
38
58
  return { PhysicalResourceId: physicalResourceId };
39
59
  }
40
- const accountId = event.ResourceProperties.AccountId;
60
+ const props = event.ResourceProperties;
41
61
  const client = new S3ControlClient({});
42
62
  await client.send(new PutPublicAccessBlockCommand({
43
- AccountId: accountId,
63
+ AccountId: props.AccountId,
44
64
  PublicAccessBlockConfiguration: {
45
- BlockPublicAcls: true,
46
- IgnorePublicAcls: true,
47
- BlockPublicPolicy: true,
48
- RestrictPublicBuckets: true
65
+ BlockPublicAcls: toBool(props.BlockPublicAcls),
66
+ IgnorePublicAcls: toBool(props.IgnorePublicAcls),
67
+ BlockPublicPolicy: toBool(props.BlockPublicPolicy),
68
+ RestrictPublicBuckets: toBool(props.RestrictPublicBuckets)
49
69
  }
50
70
  }));
51
71
  return { PhysicalResourceId: physicalResourceId };
@@ -5,7 +5,12 @@ import { OrganisationPolicy } from "../../resources/aws/organisation/organisatio
5
5
  // default path.
6
6
  const EXEMPT_ROLE_PATTERNS = [
7
7
  "arn:aws:iam::*:role/fjall/FjallDeploy*",
8
- "arn:aws:iam::*:role/OrganizationAccountAccessRole"
8
+ "arn:aws:iam::*:role/OrganizationAccountAccessRole",
9
+ // The account-level S3 Block Public Access manager Lambda re-applies BPA on
10
+ // every deploy; its PutAccountPublicAccessBlock must survive
11
+ // DenyS3PublicAccessChanges. Pathed under /fjall/ so a member admin cannot
12
+ // forge an exempt role at the default path.
13
+ "arn:aws:iam::*:role/fjall/FjallS3BpaManager*"
9
14
  ];
10
15
  const SCP_BYTE_LIMIT = 5120;
11
16
  const IAM_POLICY_VERSION = "2012-10-17";
@@ -27,6 +27,13 @@ export declare class Account extends Stack {
27
27
  * independent, and Platform must take one but not the other.
28
28
  */
29
29
  protected receivesDeployRole(): boolean;
30
+ /**
31
+ * Whether this account creates the default-ECR-image helper (the CodeBuild
32
+ * project that seeds a placeholder application image). App-hosting accounts
33
+ * need it; the organisation root runs no workloads and overrides to `false`.
34
+ * Deliberately separate from the OIDC/IPAM gates — the axes are independent.
35
+ */
36
+ protected createsEcrDefaultImage(): boolean;
30
37
  enableGuardDuty(props?: GuardDutyDetectorProps): GuardDutyDetector;
31
38
  enableSecurityHub(props?: SecurityHubHubProps): SecurityHubHub;
32
39
  enableConfigRecorder(props?: ConfigRecorderProps): ConfigRecorder;
@@ -75,10 +75,12 @@ export class Account extends Stack {
75
75
  accountId: this.account,
76
76
  region: this.resolvedRegion
77
77
  });
78
- new EcrDefaultImage(this, "EcrDefaultImage", {
79
- region: this.resolvedRegion,
80
- accountId: this.account
81
- });
78
+ if (this.createsEcrDefaultImage()) {
79
+ new EcrDefaultImage(this, "EcrDefaultImage", {
80
+ region: this.resolvedRegion,
81
+ accountId: this.account
82
+ });
83
+ }
82
84
  const environment = config.environment ?? "unknown";
83
85
  if (config.disasterRecoveryRegion) {
84
86
  const isComplianceAccount = environment === "compliance";
@@ -95,7 +97,12 @@ export class Account extends Stack {
95
97
  exportName: "Environment",
96
98
  description: "Environment type for this account (e.g., production, staging, development)"
97
99
  });
98
- new S3BlockPublicAccess(this, "S3BlockPublicAccess");
100
+ // Account-level S3 Block Public Access is account-global and runs under a
101
+ // fixed-name role, so (like the OIDC/Monitoring/Audit globals above) it is
102
+ // created only in the home region to avoid a cross-region role collision.
103
+ if (!accountGlobalsConfigured) {
104
+ new S3BlockPublicAccess(this, "S3BlockPublicAccess", { accountId });
105
+ }
99
106
  new EbsDefaultEncryption(this, "EbsDefaultEncryption");
100
107
  }
101
108
  /**
@@ -108,6 +115,15 @@ export class Account extends Stack {
108
115
  receivesDeployRole() {
109
116
  return this.constructor === Account;
110
117
  }
118
+ /**
119
+ * Whether this account creates the default-ECR-image helper (the CodeBuild
120
+ * project that seeds a placeholder application image). App-hosting accounts
121
+ * need it; the organisation root runs no workloads and overrides to `false`.
122
+ * Deliberately separate from the OIDC/IPAM gates — the axes are independent.
123
+ */
124
+ createsEcrDefaultImage() {
125
+ return true;
126
+ }
111
127
  enableGuardDuty(props) {
112
128
  return new GuardDutyDetector(this, "GuardDuty", props);
113
129
  }
@@ -1,21 +1,21 @@
1
- import { type Environment, Stack, type StackProps } from "aws-cdk-lib";
1
+ import { type Environment } from "aws-cdk-lib";
2
+ import { type Construct } from "constructs";
3
+ import { Account, type AccountProps } from "./account.js";
2
4
  import { type CustomPermissionSets } from "../../config/aws/identityCenter.js";
3
5
  import { ScpPreset } from "../../config/aws/scpPreset.js";
4
6
  import type { ScpPresetProps } from "../../config/aws/scpPreset.js";
5
7
  import { OrganisationResource } from "../../resources/aws/organisation/index.js";
6
- type ExtendedStackProps = Omit<StackProps, "env"> & {
7
- env?: Required<Pick<Environment, "region" | "account">> & Partial<Omit<Environment, "region" | "account">>;
8
- };
9
8
  export type AccountsConfig = {
10
9
  readonly [key: string]: readonly string[] | string | AccountsConfig;
11
10
  };
12
- export interface OrganisationProps extends ExtendedStackProps {
11
+ export interface OrganisationProps extends AccountProps {
13
12
  organisationName: string;
14
13
  accounts: AccountsConfig;
15
14
  orgEmail: string;
16
15
  accountIds?: Record<string, string>;
17
16
  identityCenter?: boolean;
18
17
  allowedRegions?: string[];
18
+ env?: Required<Pick<Environment, "region" | "account">> & Partial<Omit<Environment, "region" | "account">>;
19
19
  }
20
20
  /**
21
21
  * Organisation CDK stack.
@@ -27,14 +27,26 @@ export interface OrganisationProps extends ExtendedStackProps {
27
27
  * - Identity Centre configuration
28
28
  * - Cost allocation tag auto-activation (daily Lambda)
29
29
  */
30
- export declare class Organisation extends Stack {
30
+ export declare class Organisation extends Account {
31
31
  readonly organisationType: "organisation";
32
32
  private org;
33
33
  private accountRefs;
34
34
  private accountMap;
35
35
  private accountsConfig;
36
36
  private identityCenter?;
37
- constructor(id: string, props: OrganisationProps);
37
+ constructor(scope: Construct, id: string, props: OrganisationProps);
38
+ /**
39
+ * The organisation root's OIDC deploy connector is owned by the customer-run
40
+ * Quick-Create CloudFormation stack, never the inherited Account connector.
41
+ * Returning `false` is also the short-circuit first clause of Account's OIDC
42
+ * gate, so no `OidcConnector` is synthesised on the root stack.
43
+ */
44
+ protected receivesDeployRole(): boolean;
45
+ /**
46
+ * The organisation root runs no application workloads, so it needs no
47
+ * default-ECR-image helper.
48
+ */
49
+ protected createsEcrDefaultImage(): boolean;
38
50
  private createAccountReferences;
39
51
  private setupOrganisationFeatures;
40
52
  private setupCostAllocationTagActivator;
@@ -45,4 +57,3 @@ export declare class Organisation extends Stack {
45
57
  getAccounts(): Record<string, string>;
46
58
  enableScps(props: ScpPresetProps): ScpPreset;
47
59
  }
48
- export {};
@@ -1,5 +1,5 @@
1
- import { Stack } from "aws-cdk-lib";
2
1
  import App from "../../app.js";
2
+ import { Account } from "./account.js";
3
3
  import { IdentityCenter } from "../../config/aws/identityCenter.js";
4
4
  import { ScpPreset } from "../../config/aws/scpPreset.js";
5
5
  import { OrganisationResource, OrganisationAccount, CostAllocationTagActivator } from "../../resources/aws/organisation/index.js";
@@ -17,23 +17,20 @@ import { extractAccountNames } from "../../utils/accountsUtils.js";
17
17
  * - Identity Centre configuration
18
18
  * - Cost allocation tag auto-activation (daily Lambda)
19
19
  */
20
- export class Organisation extends Stack {
20
+ export class Organisation extends Account {
21
21
  organisationType = "organisation";
22
22
  org;
23
23
  accountRefs = [];
24
24
  accountMap;
25
25
  accountsConfig;
26
26
  identityCenter;
27
- constructor(id, props) {
27
+ constructor(scope, id, props) {
28
28
  const config = getConfig();
29
- const env = props.env ?? {
30
- region: config.region,
31
- account: config.accountId ?? ""
32
- };
33
- if (!env.account) {
34
- throw new Error("Organisation requires an account ID. Provide it via env.account or ensure CDK context includes accountId.");
29
+ const accountId = props.accountId ?? props.env?.account ?? config.accountId;
30
+ if (!accountId) {
31
+ throw new Error("Organisation requires an account ID. Provide it via env.account, accountId, or ensure CDK context includes accountId.");
35
32
  }
36
- super(App.getInstance(), id, { ...props, env });
33
+ super(scope, id, { ...props, accountId });
37
34
  // Normalise account map keys to lowercase for case-insensitive lookups.
38
35
  // providerAccounts stores names as lowercase; ACCOUNTS config uses PascalCase.
39
36
  const rawMap = props.accountIds ?? config.accountIds ?? {};
@@ -57,6 +54,22 @@ export class Organisation extends Stack {
57
54
  this.createAccountReferences(props);
58
55
  this.setupOrganisationFeatures(props.identityCenter ?? true, managementAccountId);
59
56
  }
57
+ /**
58
+ * The organisation root's OIDC deploy connector is owned by the customer-run
59
+ * Quick-Create CloudFormation stack, never the inherited Account connector.
60
+ * Returning `false` is also the short-circuit first clause of Account's OIDC
61
+ * gate, so no `OidcConnector` is synthesised on the root stack.
62
+ */
63
+ receivesDeployRole() {
64
+ return false;
65
+ }
66
+ /**
67
+ * The organisation root runs no application workloads, so it needs no
68
+ * default-ECR-image helper.
69
+ */
70
+ createsEcrDefaultImage() {
71
+ return false;
72
+ }
60
73
  createAccountReferences(props) {
61
74
  const allNames = extractAccountNames(props.accounts);
62
75
  for (const accountName of allNames) {
@@ -1,3 +1,4 @@
1
+ import App from "../../app.js";
1
2
  import { Organisation } from "./organisation.js";
2
3
  import { Platform } from "./platform.js";
3
4
  import { Account } from "./account.js";
@@ -5,7 +6,7 @@ export class OrganisationFactory {
5
6
  static build(id, props) {
6
7
  switch (props.type) {
7
8
  case "organisation":
8
- return new Organisation(id, props);
9
+ return new Organisation(App.getInstance(), id, props);
9
10
  case "platform":
10
11
  return (scope) => new Platform(scope, id, props);
11
12
  case "account":
@@ -15,6 +15,17 @@ export interface LambdaFunctionProps {
15
15
  handler: string;
16
16
  lambdaDescription?: string;
17
17
  roleDescription?: string;
18
+ /**
19
+ * Fixed IAM path for the auto-generated execution role (e.g. "/fjall/").
20
+ * Used with roleName to produce an SCP-exemptable ARN.
21
+ */
22
+ rolePath?: string;
23
+ /**
24
+ * Fixed name for the auto-generated execution role. Account-global — only set
25
+ * on a Lambda confined to one (account, region) stack, else regional
26
+ * duplicates collide on the role name.
27
+ */
28
+ roleName?: string;
18
29
  runtime: Runtime;
19
30
  /** Lambda CPU architecture. Default: x86_64 */
20
31
  architecture?: Architecture;
@@ -31,6 +31,24 @@ function applyRoleDescription(fn, description) {
31
31
  if (cfnRole !== undefined)
32
32
  cfnRole.description = description;
33
33
  }
34
+ /**
35
+ * Pin a fixed IAM path and/or name onto CDK's auto-generated Lambda execution
36
+ * role. Reaches the L1 CfnRole (FunctionProps exposes neither) so a stable,
37
+ * predictable ARN can be matched by an SCP exemption pattern. A fixed name is
38
+ * account-global, so only set one on a Lambda that lives in a single
39
+ * (account, region) stack — never one duplicated across regions.
40
+ */
41
+ function applyRolePathAndName(fn, rolePath, roleName) {
42
+ if (rolePath === undefined && roleName === undefined)
43
+ return;
44
+ const cfnRole = fn.role?.node.defaultChild;
45
+ if (cfnRole === undefined)
46
+ return;
47
+ if (rolePath !== undefined)
48
+ cfnRole.path = rolePath;
49
+ if (roleName !== undefined)
50
+ cfnRole.roleName = roleName;
51
+ }
34
52
  /**
35
53
  * AWS Parameters and Secrets Lambda Extension configuration.
36
54
  * @see https://docs.aws.amazon.com/systems-manager/latest/userguide/ps-integration-lambda-extensions.html
@@ -61,6 +79,7 @@ export class SingletonFunction extends singletonFunction {
61
79
  });
62
80
  addPoliciesToRole(this, props.inlinePolicy);
63
81
  applyRoleDescription(this, props.roleDescription);
82
+ applyRolePathAndName(this, props.rolePath, props.roleName);
64
83
  }
65
84
  /**
66
85
  * The Lambda's execution role (auto-generated by CDK)
@@ -91,6 +110,7 @@ export class LambdaFunction extends Function {
91
110
  });
92
111
  addPoliciesToRole(this, props.inlinePolicy);
93
112
  applyRoleDescription(this, props.roleDescription);
113
+ applyRolePathAndName(this, props.rolePath, props.roleName);
94
114
  this.addSecretsSupport(props.secrets, props.ssmSecretsPath, props.secretsImport, props.appName, props.functionName, props.architecture);
95
115
  // Sanitise id for CloudFormation output keys (must be alphanumeric)
96
116
  const outputName = toPascalCase(id);
@@ -7,6 +7,10 @@ interface CustomResourceProps {
7
7
  runtime: Runtime;
8
8
  roleDescription?: string;
9
9
  lambdaDescription?: string;
10
+ /** Fixed IAM path for the handler's execution role (e.g. "/fjall/"). */
11
+ rolePath?: string;
12
+ /** Fixed name for the handler's execution role (account-global — see LambdaFunctionProps). */
13
+ roleName?: string;
10
14
  inlinePolicy: PolicyStatement[];
11
15
  properties?: {
12
16
  [key: string]: string;
@@ -39,6 +39,8 @@ export class CustomResource extends Construct {
39
39
  timeout,
40
40
  lambdaDescription: props.lambdaDescription ?? `${id} lambda`,
41
41
  roleDescription: props.roleDescription ?? `${id} custom resource lambda`,
42
+ rolePath: props.rolePath,
43
+ roleName: props.roleName,
42
44
  inlinePolicy: props.inlinePolicy
43
45
  })
44
46
  : new SingletonFunction(this, `${id}Lambda`, {
@@ -47,6 +49,8 @@ export class CustomResource extends Construct {
47
49
  runtime: props.runtime,
48
50
  lambdaDescription: props.lambdaDescription ?? `${id} lambda`,
49
51
  roleDescription: props.roleDescription ?? `${id} custom resource lambda`,
52
+ rolePath: props.rolePath,
53
+ roleName: props.roleName,
50
54
  inlinePolicy: props.inlinePolicy
51
55
  });
52
56
  const provider = new Provider(this, `${id}Provider`, {
@@ -99,12 +99,12 @@ function getProviderAccountsFromContext() {
99
99
  */
100
100
  function resolveEnvironmentFromAccountId(accountId) {
101
101
  const accounts = getProviderAccountsFromContext();
102
- return accounts.find((pa) => pa.id === accountId)?.environment;
102
+ return accounts.find((pa) => pa.id === accountId)?.environment ?? undefined;
103
103
  }
104
104
  /**
105
105
  * Look up environment from account name via orgConfig CDK context.
106
106
  */
107
107
  function resolveEnvironmentFromAccountName(accountName) {
108
108
  const accounts = getProviderAccountsFromContext();
109
- return accounts.find((pa) => pa.name === accountName)?.environment;
109
+ return (accounts.find((pa) => pa.name === accountName)?.environment ?? undefined);
110
110
  }
@@ -63,7 +63,7 @@ export function getConfig(accountName) {
63
63
  if (providerAccount) {
64
64
  config.accountId = providerAccount.id;
65
65
  config.accountName = providerAccount.name;
66
- config.environment = providerAccount.environment;
66
+ config.environment = providerAccount.environment ?? "unknown";
67
67
  }
68
68
  }
69
69
  // If we still don't have an account name - try to retrieve accountId from context
@@ -75,7 +75,7 @@ export function getConfig(accountName) {
75
75
  const providerAccount = providerAccounts.find((pa) => pa.id === accountId);
76
76
  if (providerAccount) {
77
77
  config.accountName = providerAccount.name;
78
- config.environment = providerAccount.environment;
78
+ config.environment = providerAccount.environment ?? "unknown";
79
79
  }
80
80
  }
81
81
  }
@@ -1,4 +1,4 @@
1
- import { VAULT_LOCK_MODES } from "@fjall/util/config";
1
+ import { VAULT_LOCK_MODES, S3_BPA_MODES } from "@fjall/util/config";
2
2
  import { maskSensitiveOutput } from "@fjall/util";
3
3
  import { FjallLogger } from "./validationLogger.js";
4
4
  /**
@@ -24,7 +24,10 @@ export function parseOrgConfig(raw) {
24
24
  item !== null &&
25
25
  typeof item.id === "string" &&
26
26
  typeof item.name === "string" &&
27
- typeof item.environment === "string" &&
27
+ // null environment (structural accounts) must survive — rejecting it drops the account at synth.
28
+ (typeof item.environment ===
29
+ "string" ||
30
+ item.environment === null) &&
28
31
  (item.managed === undefined ||
29
32
  typeof item.managed === "boolean") &&
30
33
  (item.oidcRoleArn === undefined ||
@@ -32,6 +35,9 @@ export function parseOrgConfig(raw) {
32
35
  "string") &&
33
36
  (item.vaultLock === undefined ||
34
37
  VAULT_LOCK_MODES.includes(item.vaultLock)) &&
38
+ (item.s3BlockPublicAccess ===
39
+ undefined ||
40
+ S3_BPA_MODES.includes(item.s3BlockPublicAccess)) &&
35
41
  (item.acknowledgeImmutableVaultLock ===
36
42
  undefined ||
37
43
  typeof item
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/components-infrastructure",
3
- "version": "2.11.1",
3
+ "version": "2.12.0",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "type": "module",
6
6
  "bin": {
@@ -63,8 +63,8 @@
63
63
  },
64
64
  "dependencies": {
65
65
  "@aws-sdk/client-organizations": "^3.1038.0",
66
- "@fjall/generator": "^2.11.1",
67
- "@fjall/util": "^2.11.1",
66
+ "@fjall/generator": "^2.12.0",
67
+ "@fjall/util": "^2.12.0",
68
68
  "constructs": "^10.0.0",
69
69
  "uuid": "^14.0.0"
70
70
  },
@@ -79,5 +79,5 @@
79
79
  "engines": {
80
80
  "node": ">=18.0.0"
81
81
  },
82
- "gitHead": "69823c3d7f2eacba419657464381119c5b5b5fd6"
82
+ "gitHead": "dca39a47da956d3d94c689dd782fe285d711d25e"
83
83
  }