@fjall/components-infrastructure 2.30.3 → 2.31.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/dist/lib/config/aws/identityCenterMembership.d.ts +11 -0
  2. package/dist/lib/config/aws/identityCenterMembership.js +61 -0
  3. package/dist/lib/config/aws/oidcConnector.js +11 -3
  4. package/dist/lib/lambda-assets/cert-generator/asset/index.js +12 -3
  5. package/dist/lib/patterns/aws/compute.d.ts +2 -2
  6. package/dist/lib/patterns/aws/compute.js +18 -9
  7. package/dist/lib/patterns/aws/computeLambda.d.ts +15 -1
  8. package/dist/lib/patterns/aws/computeLambda.js +18 -2
  9. package/dist/lib/patterns/aws/computePropApplicability.d.ts +20 -0
  10. package/dist/lib/patterns/aws/computePropApplicability.js +87 -0
  11. package/dist/lib/resources/aws/compute/ecsImages.js +7 -2
  12. package/dist/lib/resources/aws/compute/imageTagParameter.d.ts +18 -1
  13. package/dist/lib/resources/aws/compute/imageTagParameter.js +32 -2
  14. package/dist/lib/resources/aws/compute/lambda.d.ts +1 -1
  15. package/dist/lib/resources/aws/compute/lambda.js +6 -4
  16. package/dist/lib/resources/aws/iam/identityCenter/user.js +5 -1
  17. package/dist/lib/resources/aws/logging/index.d.ts +1 -0
  18. package/dist/lib/resources/aws/logging/index.js +1 -0
  19. package/dist/lib/resources/aws/logging/logGroup.d.ts +7 -0
  20. package/dist/lib/resources/aws/logging/logGroup.js +7 -0
  21. package/dist/lib/resources/aws/logging/singletonLogGroup.d.ts +15 -0
  22. package/dist/lib/resources/aws/logging/singletonLogGroup.js +24 -0
  23. package/dist/lib/resources/aws/networking/vpcPeeringConnection.js +2 -1
  24. package/dist/lib/resources/aws/storage/bucketDeployment.d.ts +5 -6
  25. package/dist/lib/resources/aws/storage/bucketDeployment.js +14 -7
  26. package/dist/lib/resources/aws/utilities/awsCustomResource.js +5 -0
  27. package/dist/lib/resources/aws/utilities/customResource.js +5 -1
  28. package/dist/lib/utils/addSuffixToEmail.js +1 -5
  29. package/dist/lib/utils/validationLogger.d.ts +2 -2
  30. package/dist/lib/utils/validationLogger.js +4 -4
  31. package/package.json +4 -4
  32. package/dist/lib/__tests__/setup.d.ts +0 -48
  33. package/dist/lib/__tests__/setup.js +0 -1
  34. package/dist/lib/config/aws/accountId.d.ts +0 -6
  35. package/dist/lib/config/aws/accountId.js +0 -33
  36. package/dist/lib/config/aws/backupGlobalSettings.d.ts +0 -29
  37. package/dist/lib/config/aws/backupGlobalSettings.js +0 -51
  38. package/dist/lib/config/aws/costAllocationTags.d.ts +0 -12
  39. package/dist/lib/config/aws/costAllocationTags.js +0 -48
  40. package/dist/lib/config/aws/eventBus.d.ts +0 -7
  41. package/dist/lib/config/aws/eventBus.js +0 -24
  42. package/dist/lib/config/aws/identityCenterGroupMembership.d.ts +0 -10
  43. package/dist/lib/config/aws/identityCenterGroupMembership.js +0 -115
  44. package/dist/lib/config/aws/ipamDelegateAdmin.d.ts +0 -8
  45. package/dist/lib/config/aws/ipamDelegateAdmin.js +0 -58
  46. package/dist/lib/config/aws/ipamPoolId.d.ts +0 -10
  47. package/dist/lib/config/aws/ipamPoolId.js +0 -39
  48. package/dist/lib/config/aws/organisation.d.ts +0 -30
  49. package/dist/lib/config/aws/organisation.js +0 -95
  50. package/dist/lib/config/aws/organisationId.d.ts +0 -7
  51. package/dist/lib/config/aws/organisationId.js +0 -46
  52. package/dist/lib/config/aws/organisationsAccess.d.ts +0 -10
  53. package/dist/lib/config/aws/organisationsAccess.js +0 -50
  54. package/dist/lib/config/aws/ramSharing.d.ts +0 -4
  55. package/dist/lib/config/aws/ramSharing.js +0 -35
  56. package/dist/lib/config/monitoring.d.ts +0 -18
  57. package/dist/lib/config/monitoring.js +0 -22
  58. package/dist/lib/patterns/aws/cicdRole.d.ts +0 -67
  59. package/dist/lib/patterns/aws/cicdRole.js +0 -68
  60. package/dist/lib/patterns/aws/hostedZone.d.ts +0 -28
  61. package/dist/lib/patterns/aws/hostedZone.js +0 -150
  62. package/dist/lib/patterns/aws/managedAccount.d.ts +0 -9
  63. package/dist/lib/patterns/aws/managedAccount.js +0 -55
  64. package/dist/lib/patterns/aws/managedIdentityCenter.d.ts +0 -4
  65. package/dist/lib/patterns/aws/managedIdentityCenter.js +0 -24
  66. package/dist/lib/patterns/aws/managedOrganisation.d.ts +0 -36
  67. package/dist/lib/patterns/aws/managedOrganisation.js +0 -97
  68. package/dist/lib/patterns/aws/managedPlatform.d.ts +0 -12
  69. package/dist/lib/patterns/aws/managedPlatform.js +0 -29
  70. package/dist/lib/patterns/aws/subdomainHostedZone.d.ts +0 -9
  71. package/dist/lib/patterns/aws/subdomainHostedZone.js +0 -39
  72. package/dist/lib/resources/aws/cicd/cicdRole.d.ts +0 -65
  73. package/dist/lib/resources/aws/cicd/cicdRole.js +0 -191
  74. package/dist/lib/resources/aws/compute/ecsFreeTier.d.ts +0 -75
  75. package/dist/lib/resources/aws/compute/ecsFreeTier.js +0 -1
  76. package/dist/lib/resources/aws/compute/ecsSpot.d.ts +0 -75
  77. package/dist/lib/resources/aws/compute/ecsSpot.js +0 -1
  78. package/dist/lib/resources/aws/compute/utilities/capacityProviderDrainWaiter.d.ts +0 -20
  79. package/dist/lib/resources/aws/compute/utilities/capacityProviderDrainWaiter.js +0 -1
  80. package/dist/lib/resources/aws/database/database.d.ts +0 -14
  81. package/dist/lib/resources/aws/database/database.js +0 -28
  82. package/dist/lib/resources/aws/database/databaseInstance.d.ts +0 -15
  83. package/dist/lib/resources/aws/database/databaseInstance.js +0 -30
  84. package/dist/lib/resources/aws/iam/identityCenter/attachManagedPolicy.d.ts +0 -13
  85. package/dist/lib/resources/aws/iam/identityCenter/attachManagedPolicy.js +0 -52
  86. package/dist/lib/resources/aws/iam/securityGroup.d.ts +0 -5
  87. package/dist/lib/resources/aws/iam/securityGroup.js +0 -14
  88. package/dist/lib/resources/aws/monitoring/monitoringRole.d.ts +0 -28
  89. package/dist/lib/resources/aws/monitoring/monitoringRole.js +0 -125
  90. package/dist/lib/resources/aws/utilities/cfnOutput.d.ts +0 -5
  91. package/dist/lib/resources/aws/utilities/cfnOutput.js +0 -1
  92. package/dist/lib/utils/capitalizeString.d.ts +0 -1
  93. package/dist/lib/utils/capitalizeString.js +0 -7
@@ -0,0 +1,11 @@
1
+ import { NestedStack, type NestedStackProps } from "aws-cdk-lib";
2
+ import { type Construct } from "constructs";
3
+ export interface IdentityCenterMembershipProps extends NestedStackProps {
4
+ identityStoreId: string;
5
+ groupId: string;
6
+ groupMembers: string[];
7
+ }
8
+ export declare function validateIdentityCenterMembershipProps(props: IdentityCenterMembershipProps): void;
9
+ export declare class IdentityCenterMembership extends NestedStack {
10
+ constructor(scope: Construct, id: string, props: IdentityCenterMembershipProps);
11
+ }
@@ -0,0 +1,61 @@
1
+ import * as customResources from "aws-cdk-lib/custom-resources";
2
+ import { NestedStack } from "aws-cdk-lib";
3
+ import { AwsCustomResource } from "../../resources/aws/utilities/awsCustomResource.js";
4
+ import { GroupMembership } from "../../resources/aws/iam/identityCenter/groupMembership.js";
5
+ import { stripAndCamelCase } from "../../utils/stripAndCamelCase.js";
6
+ const IDENTITY_STORE_SERVICE = "identityStore";
7
+ export function validateIdentityCenterMembershipProps(props) {
8
+ if (!props.identityStoreId) {
9
+ throw new Error("IdentityCenterMembership requires identityStoreId from the parent IdentityCenter construct.");
10
+ }
11
+ if (!props.groupId) {
12
+ throw new Error("IdentityCenterMembership requires groupId from a Group construct.");
13
+ }
14
+ if (props.groupMembers.length === 0) {
15
+ throw new Error("IdentityCenterMembership requires at least one member email. Empty groups should be skipped at the caller.");
16
+ }
17
+ const seen = new Set();
18
+ for (const member of props.groupMembers) {
19
+ if (!member.includes("@")) {
20
+ throw new Error(`IdentityCenterMembership: "${member}" is not a valid email — Identity Center memberships look up users by UserName (email)`);
21
+ }
22
+ if (seen.has(member)) {
23
+ throw new Error(`Duplicate member "${member}" in IdentityCenterMembership groupMembers.`);
24
+ }
25
+ seen.add(member);
26
+ }
27
+ }
28
+ export class IdentityCenterMembership extends NestedStack {
29
+ constructor(scope, id, props) {
30
+ super(scope, id, props);
31
+ validateIdentityCenterMembershipProps(props);
32
+ for (const member of props.groupMembers) {
33
+ const [localPart] = member.split("@");
34
+ const suffix = stripAndCamelCase(localPart ?? member);
35
+ const listUsersCall = {
36
+ service: IDENTITY_STORE_SERVICE,
37
+ action: "listUsers",
38
+ parameters: {
39
+ IdentityStoreId: props.identityStoreId,
40
+ Filters: [
41
+ {
42
+ AttributePath: "UserName",
43
+ AttributeValue: member
44
+ }
45
+ ]
46
+ },
47
+ physicalResourceId: customResources.PhysicalResourceId.of(`listUsers${suffix}`)
48
+ };
49
+ const userLookup = new AwsCustomResource(this, `User${suffix}`, {
50
+ onCreate: listUsersCall,
51
+ onUpdate: listUsersCall
52
+ });
53
+ const userId = userLookup.getResponseField("Users.0.UserId");
54
+ new GroupMembership(this, `Membership${suffix}`, {
55
+ identityStoreId: props.identityStoreId,
56
+ groupId: props.groupId,
57
+ userId
58
+ });
59
+ }
60
+ }
61
+ }
@@ -1,5 +1,5 @@
1
1
  import { DEV_BOUNDARY_POLICY_NAME, DEV_DEPLOY_POLICY_NAME, DEV_PROVISIONER_POLICY_NAME, DEV_SYNC_WRITER_POLICY_NAME, FJALL_OIDC_AUDIENCE, FJALL_OIDC_ISSUER_DOMAIN, FJALL_OIDC_PLACEHOLDER_THUMBPRINT, SECURITY_TIER_TO_BOUNDARY, buildDevBoundaryStatements, buildDevDeployPolicyStatements, buildDevDeployTrustStatements, buildDevProvisionerPolicyStatements, buildDevProvisionerTrustStatements, buildDevSyncWriterPolicyStatements, buildDevSyncWriterTrustStatements, buildNarrowedDeployTrustStatements, deployBoundaryName, devDeployRoleName, devProvisionerRoleName, devSyncWriterRoleName } from "@fjall/generator";
2
- import { CfnOutput, Duration, Stack } from "aws-cdk-lib";
2
+ import { Aws, CfnOutput, Duration, Stack } from "aws-cdk-lib";
3
3
  import * as iam from "aws-cdk-lib/aws-iam";
4
4
  import { Runtime } from "aws-cdk-lib/aws-lambda";
5
5
  import { Construct } from "constructs";
@@ -80,15 +80,23 @@ export class OidcConnector extends Construct {
80
80
  resourceType: "Custom::FjallOidcProvider",
81
81
  properties: { IssuerUrl: issuerUrl },
82
82
  inlinePolicy: [
83
+ // iam:ListOpenIDConnectProviders supports no resource-level
84
+ // permission — the star grants provider-list metadata only.
85
+ new iam.PolicyStatement({
86
+ effect: iam.Effect.ALLOW,
87
+ actions: ["iam:ListOpenIDConnectProviders"],
88
+ resources: ["*"]
89
+ }),
83
90
  new iam.PolicyStatement({
84
91
  effect: iam.Effect.ALLOW,
85
92
  actions: [
86
- "iam:ListOpenIDConnectProviders",
87
93
  "iam:GetOpenIDConnectProvider",
88
94
  "iam:CreateOpenIDConnectProvider",
89
95
  "iam:AddClientIDToOpenIDConnectProvider"
90
96
  ],
91
- resources: ["*"]
97
+ resources: [
98
+ `arn:${Aws.PARTITION}:iam::${Aws.ACCOUNT_ID}:oidc-provider/${issuerDomain}`
99
+ ]
92
100
  })
93
101
  ],
94
102
  inlineCode: OIDC_PROVIDER_HANDLER
@@ -3,11 +3,20 @@ var __defProp = Object.defineProperty;
3
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __esm = (fn, res) => function __init() {
7
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ var __esm = (fn, res, err) => function __init() {
7
+ if (err) throw err[0];
8
+ try {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ } catch (e) {
11
+ throw err = [e], e;
12
+ }
8
13
  };
9
14
  var __commonJS = (cb, mod) => function __require() {
10
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
15
+ try {
16
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
17
+ } catch (e) {
18
+ throw mod = 0, e;
19
+ }
11
20
  };
12
21
  var __export = (target, all) => {
13
22
  for (var name in all)
@@ -4,9 +4,9 @@ import { type SecretImport } from "../../resources/aws/secrets/index.js";
4
4
  import { type ComputeType, type IEcsCompute, type ILambdaCompute, type IEc2Compute, type AnyCompute, isCompute, isEcsCompute, isLambdaCompute, isEc2Compute } from "./interfaces/compute.js";
5
5
  import type App from "../../app.js";
6
6
  import { EcsCompute, type EcsComputeProps, type EcsServiceConfig, type ServiceLogAlarm, type EcsContainerConfig, type EcsScalingConfig, type EcsClusterConfig, type EcsRoutingConfig, type EcsCapacityProviderConfig, type ContainerDependency, type EcsMigrationsConfig, ECS_CAPACITY_PROVIDER_CONFIG, getEcsCapacityProviderConfig, ScalingType, type EcsCapacityProvider, type Ec2CapacityConfig, validateEcsProps, buildContainerConfigs, expandMigrationsSugar, type ResolvedScalingConfig, resolveScalingConfig } from "./computeEcs.js";
7
- import { LambdaCompute, type LambdaComputeProps, type ContainerLambdaProps, type CodeLambdaProps, type FunctionUrlConfig, type ResolvedLambdaDeployment, resolveLambdaDeployment, Architecture, HttpMethod, InvokeMode, type FunctionUrlCorsOptions } from "./computeLambda.js";
7
+ import { LambdaCompute, type LambdaComputeProps, type ContainerLambdaProps, type CodeLambdaProps, type FunctionUrlConfig, type ResolvedLambdaDeployment, resolveLambdaDeployment, lambdaImageKey, Architecture, HttpMethod, InvokeMode, type FunctionUrlCorsOptions } from "./computeLambda.js";
8
8
  import { Ec2Compute, type Ec2ComputeProps, type SshConfig } from "./computeEc2.js";
9
- export { EcsCompute, type EcsComputeProps, type EcsServiceConfig, type ServiceLogAlarm, type EcsContainerConfig, type EcsScalingConfig, type EcsClusterConfig, type EcsRoutingConfig, type EcsCapacityProviderConfig, ECS_CAPACITY_PROVIDER_CONFIG, getEcsCapacityProviderConfig, ScalingType, type EcsCapacityProvider, type Ec2CapacityConfig, type ContainerDependency, type EcsMigrationsConfig, validateEcsProps, buildContainerConfigs, expandMigrationsSugar, type ResolvedScalingConfig, resolveScalingConfig, LambdaCompute, type LambdaComputeProps, type ContainerLambdaProps, type CodeLambdaProps, type FunctionUrlConfig, type ResolvedLambdaDeployment, resolveLambdaDeployment, Architecture, HttpMethod, InvokeMode, type FunctionUrlCorsOptions, Ec2Compute, type Ec2ComputeProps, type SshConfig };
9
+ export { EcsCompute, type EcsComputeProps, type EcsServiceConfig, type ServiceLogAlarm, type EcsContainerConfig, type EcsScalingConfig, type EcsClusterConfig, type EcsRoutingConfig, type EcsCapacityProviderConfig, ECS_CAPACITY_PROVIDER_CONFIG, getEcsCapacityProviderConfig, ScalingType, type EcsCapacityProvider, type Ec2CapacityConfig, type ContainerDependency, type EcsMigrationsConfig, validateEcsProps, buildContainerConfigs, expandMigrationsSugar, type ResolvedScalingConfig, resolveScalingConfig, LambdaCompute, type LambdaComputeProps, type ContainerLambdaProps, type CodeLambdaProps, type FunctionUrlConfig, type ResolvedLambdaDeployment, resolveLambdaDeployment, lambdaImageKey, Architecture, HttpMethod, InvokeMode, type FunctionUrlCorsOptions, Ec2Compute, type Ec2ComputeProps, type SshConfig };
10
10
  export type { ComputeType } from "./interfaces/compute.js";
11
11
  /**
12
12
  * Configuration defaults for each compute type.
@@ -3,18 +3,19 @@ import { Runtime } from "aws-cdk-lib/aws-lambda";
3
3
  import { buildParameterPath } from "@fjall/util";
4
4
  import { isCompute, isEcsCompute, isLambdaCompute, isEc2Compute } from "./interfaces/compute.js";
5
5
  import { warnIfPropertiesIgnored } from "../../utils/validationLogger.js";
6
+ import { COMPUTE_WARN_GROUPS, CONTAINER_LAMBDA_ONLY_PROPS, CODE_LAMBDA_ONLY_PROPS } from "./computePropApplicability.js";
6
7
  import { applyInferredPublicBuildArgs } from "./computeBuildArgInference.js";
7
8
  import { DEFAULT_ECS_FALLBACK_IMAGE, DEFAULT_EC2_INSTANCE_TYPE } from "../../resources/aws/compute/ecsConstants.js";
8
9
  // Import and re-export from per-pattern files
9
10
  import { EcsCompute, ECS_CAPACITY_PROVIDER_CONFIG, getEcsCapacityProviderConfig, ScalingType, validateEcsProps, buildContainerConfigs, expandMigrationsSugar, resolveScalingConfig } from "./computeEcs.js";
10
- import { LambdaCompute, resolveLambdaDeployment, Architecture, HttpMethod, InvokeMode } from "./computeLambda.js";
11
+ import { LambdaCompute, resolveLambdaDeployment, lambdaImageKey, Architecture, HttpMethod, InvokeMode } from "./computeLambda.js";
11
12
  import { Ec2Compute } from "./computeEc2.js";
12
13
  // Re-export everything from per-pattern files
13
14
  export {
14
15
  // ECS
15
16
  EcsCompute, ECS_CAPACITY_PROVIDER_CONFIG, getEcsCapacityProviderConfig, ScalingType, validateEcsProps, buildContainerConfigs, expandMigrationsSugar, resolveScalingConfig,
16
17
  // Lambda
17
- LambdaCompute, resolveLambdaDeployment, Architecture, HttpMethod, InvokeMode,
18
+ LambdaCompute, resolveLambdaDeployment, lambdaImageKey, Architecture, HttpMethod, InvokeMode,
18
19
  // EC2
19
20
  Ec2Compute };
20
21
  export const COMPUTE_TYPE_CONFIG = {
@@ -90,12 +91,20 @@ export function getComputeTypeConfig(type) {
90
91
  * TypeScript's compile-time checks may not apply.
91
92
  */
92
93
  function validateComputeProps(props) {
93
- // ECS-specific options on non-ECS compute
94
- warnIfPropertiesIgnored(props, ["capacityProvider", "ec2Config", "services", "cluster"], "ecs", "ECS compute");
95
- // Lambda-specific options on non-Lambda compute
96
- warnIfPropertiesIgnored(props, ["functionUrl", "handler", "runtime"], "lambda", "Lambda compute");
97
- // EC2-specific options on non-EC2 compute
98
- warnIfPropertiesIgnored(props, ["ssh", "userData", "machineImage"], "ec2", "EC2 compute");
94
+ for (const group of COMPUTE_WARN_GROUPS) {
95
+ warnIfPropertiesIgnored(props, group.properties, group.applicableTypes, group.typeName);
96
+ }
97
+ // capacityProvider/ec2Config are EcsServiceConfig-level keys, not top-level
98
+ // EcsComputeProps, so the guard Record in computePropApplicability.ts would
99
+ // reject them hand-curated misuse hints stay outside it.
100
+ warnIfPropertiesIgnored(props, ["capacityProvider", "ec2Config"], "ecs", "ECS compute");
101
+ if (props.type === "lambda") {
102
+ // warnIfPropertyIgnored reads only `.type` + presence, so a
103
+ // deployment-keyed view reuses the same machinery for the second axis.
104
+ const byDeployment = { ...props, type: props.deployment };
105
+ warnIfPropertiesIgnored(byDeployment, CONTAINER_LAMBDA_ONLY_PROPS, "container", "container-deployment Lambda", "deployment");
106
+ warnIfPropertiesIgnored(byDeployment, CODE_LAMBDA_ONLY_PROPS, "code", "code-deployment Lambda", "deployment");
107
+ }
99
108
  // ECS-specific validation - type narrowing handled by discriminated union
100
109
  if (props.type === "ecs") {
101
110
  validateEcsProps(props);
@@ -249,7 +258,7 @@ export class ComputeFactory {
249
258
  if (lambdaComputeProps.deployment === "container" &&
250
259
  lambdaComputeProps.docker !== undefined) {
251
260
  manifestLambda.docker = lambdaComputeProps.docker;
252
- manifestLambda.imageKey = lambdaComputeProps.image ?? id;
261
+ manifestLambda.imageKey = lambdaImageKey(lambdaComputeProps, id);
253
262
  }
254
263
  collector.addLambda(manifestLambda);
255
264
  return new LambdaCompute(scope, id, lambdaComputeProps);
@@ -185,7 +185,9 @@ export interface CodeLambdaProps extends BaseLambdaProps {
185
185
  * Lambda compute configuration.
186
186
  *
187
187
  * Discriminated union ensuring type-safe Lambda configuration:
188
- * - `deployment: "container"` requires `ecrRepository`
188
+ * - `deployment: "container"` takes `ecrRepository` (bring-your-own image)
189
+ * and/or `docker` (fjall builds and pushes; `ecrRepository` then defaults
190
+ * to the app's shared default container registry)
189
191
  * - `deployment: "code"` requires `code` and allows `handler`/`runtime`
190
192
  *
191
193
  * @example
@@ -205,6 +207,18 @@ export interface ResolvedLambdaDeployment {
205
207
  handler: string;
206
208
  runtime: Runtime;
207
209
  }
210
+ /**
211
+ * Shared derivation of a container Lambda's image key. Consumed by both the
212
+ * synth-side CFN image-tag parameter (below) and the manifest `imageKey`
213
+ * (compute.ts) — `fjall deploy` matches manifest imageKey → built image tag →
214
+ * CFN parameter name, so the two derivations must stay byte-identical.
215
+ *
216
+ * An explicit empty-string `image` is treated as absent (falls back to `id`):
217
+ * `??` alone would let `image: ""` through as an empty build/tag key.
218
+ */
219
+ export declare function lambdaImageKey(props: {
220
+ image?: string;
221
+ }, id: string): string;
208
222
  /**
209
223
  * Resolve Lambda deployment configuration from props.
210
224
  * Handles container vs code deployment types.
@@ -10,6 +10,18 @@ import { evaluateBakeGuard } from "@fjall/util/docker";
10
10
  import { toKebab } from "@fjall/util";
11
11
  // Re-export Lambda types from CDK for user convenience
12
12
  export { Architecture, HttpMethod, InvokeMode } from "aws-cdk-lib/aws-lambda";
13
+ /**
14
+ * Shared derivation of a container Lambda's image key. Consumed by both the
15
+ * synth-side CFN image-tag parameter (below) and the manifest `imageKey`
16
+ * (compute.ts) — `fjall deploy` matches manifest imageKey → built image tag →
17
+ * CFN parameter name, so the two derivations must stay byte-identical.
18
+ *
19
+ * An explicit empty-string `image` is treated as absent (falls back to `id`):
20
+ * `??` alone would let `image: ""` through as an empty build/tag key.
21
+ */
22
+ export function lambdaImageKey(props, id) {
23
+ return props.image !== undefined && props.image !== "" ? props.image : id;
24
+ }
13
25
  /**
14
26
  * Resolve Lambda deployment configuration from props.
15
27
  * Handles container vs code deployment types.
@@ -33,8 +45,12 @@ export function resolveLambdaDeployment(scope, id, props) {
33
45
  if (findings.length > 0) {
34
46
  throw new Error(`Lambda '${id}': ${findings.map((f) => f.message).join(" ")}`);
35
47
  }
36
- const tagKey = props.image ?? id;
37
- const param = getOrCreateImageTagParameter(scope, tagKey);
48
+ const tagKey = lambdaImageKey(props, id);
49
+ const param = getOrCreateImageTagParameter(scope, tagKey, {
50
+ kind: "lambda-image",
51
+ declaredBy: id,
52
+ docker: props.docker
53
+ });
38
54
  return {
39
55
  code: Code.fromEcrImage(props.ecrRepository, {
40
56
  tagOrDigest: param.valueAsString
@@ -0,0 +1,20 @@
1
+ import type { ComputeType } from "./interfaces/compute.js";
2
+ import type { EcsComputeProps } from "./computeEcsTypes.js";
3
+ import type { LambdaComputeProps } from "./computeLambda.js";
4
+ import type { Ec2ComputeProps } from "./computeEc2.js";
5
+ type KeysOfUnion<T> = T extends unknown ? keyof T : never;
6
+ type AllComputePropKeys = keyof EcsComputeProps | KeysOfUnion<LambdaComputeProps> | keyof Ec2ComputeProps;
7
+ export interface ComputeWarnGroup {
8
+ properties: string[];
9
+ applicableTypes: ComputeType[];
10
+ typeName: string;
11
+ }
12
+ export declare const COMPUTE_WARN_GROUPS: readonly ComputeWarnGroup[];
13
+ /**
14
+ * Runtime key set for tests — derived from the compiled guard, never
15
+ * re-listed (typescript-standards § compile-time guards).
16
+ */
17
+ export declare const ALL_COMPUTE_PROP_KEYS: AllComputePropKeys[];
18
+ export declare const CONTAINER_LAMBDA_ONLY_PROPS: string[];
19
+ export declare const CODE_LAMBDA_ONLY_PROPS: string[];
20
+ export {};
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Compile-time lockstep guard for `validateComputeProps`'s ignored-prop
3
+ * warnings. The Record over the full top-level key union rejects BOTH a
4
+ * missing key (a new prop added to any compute props interface fails TS2741
5
+ * here until classified) and a stale extra key (a removed/renamed prop fails
6
+ * TS2353). `"all"` means the prop is legitimate on every compute type and
7
+ * never warns.
8
+ */
9
+ const COMPUTE_PROP_APPLICABILITY = {
10
+ type: "all",
11
+ vpc: "all",
12
+ appName: ["ecs", "lambda"],
13
+ docker: ["ecs", "lambda"],
14
+ ecrRepository: ["ecs", "lambda"],
15
+ cluster: ["ecs"],
16
+ services: ["ecs"],
17
+ alertsTopic: ["ecs"],
18
+ applicationId: ["ecs"],
19
+ deployment: ["lambda"],
20
+ functionUrl: ["lambda"],
21
+ handler: ["lambda"],
22
+ runtime: ["lambda"],
23
+ code: ["lambda"],
24
+ image: ["lambda"],
25
+ timeout: ["lambda"],
26
+ memorySize: ["lambda"],
27
+ ephemeralStorageSize: ["lambda"],
28
+ architecture: ["lambda"],
29
+ description: ["lambda"],
30
+ roleDescription: ["lambda"],
31
+ inlinePolicy: ["lambda"],
32
+ environment: ["lambda"],
33
+ secrets: ["lambda"],
34
+ ssmSecretsPath: ["lambda"],
35
+ secretsImport: ["lambda"],
36
+ connections: ["lambda"],
37
+ instanceType: ["ec2"],
38
+ ssh: ["ec2"],
39
+ userData: ["ec2"],
40
+ machineImage: ["ec2"],
41
+ minCapacity: ["ec2"],
42
+ maxCapacity: ["ec2"],
43
+ spotCapacityPercentage: ["ec2"]
44
+ };
45
+ const TYPE_LABELS = {
46
+ ecs: "ECS",
47
+ lambda: "Lambda",
48
+ ec2: "EC2"
49
+ };
50
+ export const COMPUTE_WARN_GROUPS = (() => {
51
+ const groups = new Map();
52
+ for (const [prop, applicability] of Object.entries(COMPUTE_PROP_APPLICABILITY)) {
53
+ if (applicability === "all")
54
+ continue;
55
+ const key = applicability.join("+");
56
+ const existing = groups.get(key);
57
+ if (existing !== undefined) {
58
+ existing.properties.push(prop);
59
+ }
60
+ else {
61
+ groups.set(key, {
62
+ properties: [prop],
63
+ applicableTypes: [...applicability],
64
+ typeName: applicability.map((t) => TYPE_LABELS[t]).join(" or ") + " compute"
65
+ });
66
+ }
67
+ }
68
+ return [...groups.values()];
69
+ })();
70
+ /**
71
+ * Runtime key set for tests — derived from the compiled guard, never
72
+ * re-listed (typescript-standards § compile-time guards).
73
+ */
74
+ export const ALL_COMPUTE_PROP_KEYS = Object.keys(COMPUTE_PROP_APPLICABILITY);
75
+ const LAMBDA_DEPLOYMENT_APPLICABILITY = {
76
+ ecrRepository: "container",
77
+ docker: "container",
78
+ image: "container",
79
+ code: "code",
80
+ handler: "code",
81
+ runtime: "code"
82
+ };
83
+ const deploymentProps = (d) => Object.entries(LAMBDA_DEPLOYMENT_APPLICABILITY)
84
+ .filter(([, v]) => v === d)
85
+ .map(([k]) => k);
86
+ export const CONTAINER_LAMBDA_ONLY_PROPS = deploymentProps("container");
87
+ export const CODE_LAMBDA_ONLY_PROPS = deploymentProps("code");
@@ -7,6 +7,11 @@ export function getContainerImage(ctx, serviceName, containerConfig, serviceProp
7
7
  if (!imageSource) {
8
8
  return ContainerImage.fromRegistry(DEFAULT_ECS_FALLBACK_IMAGE);
9
9
  }
10
+ const entity = {
11
+ kind: "ecs-service",
12
+ declaredBy: serviceName,
13
+ docker: serviceProps.docker
14
+ };
10
15
  if (typeof imageSource === "string") {
11
16
  const isFullRegistryUrl = /^(docker\.io|registry\.hub\.docker\.com|ghcr\.io)\//i.test(imageSource) ||
12
17
  imageSource.startsWith("public.ecr.aws/") ||
@@ -14,11 +19,11 @@ export function getContainerImage(ctx, serviceName, containerConfig, serviceProp
14
19
  if (isFullRegistryUrl) {
15
20
  return ContainerImage.fromRegistry(imageSource);
16
21
  }
17
- const param = getOrCreateImageTagParameter(ctx.scope, serviceName);
22
+ const param = getOrCreateImageTagParameter(ctx.scope, serviceName, entity);
18
23
  return ContainerImage.fromEcrRepository(Repository.fromRepositoryName(ctx.scope, `${serviceName}${containerConfig.name}EcrRepo`, imageSource), param.valueAsString);
19
24
  }
20
25
  if (imageSource instanceof Repository) {
21
- const param = getOrCreateImageTagParameter(ctx.scope, serviceName);
26
+ const param = getOrCreateImageTagParameter(ctx.scope, serviceName, entity);
22
27
  return ContainerImage.fromEcrRepository(imageSource, param.valueAsString);
23
28
  }
24
29
  return imageSource;
@@ -1,5 +1,15 @@
1
1
  import { CfnParameter } from "aws-cdk-lib";
2
2
  import { type Construct } from "constructs";
3
+ import { type DockerBuild } from "@fjall/util/manifest/schemas";
4
+ /** Which key namespace is registering the parameter. */
5
+ export type ImageTagEntityKind = "ecs-service" | "lambda-image";
6
+ /** Identity of the entity requesting the parameter, for collision reporting. */
7
+ export interface ImageTagEntity {
8
+ readonly kind: ImageTagEntityKind;
9
+ /** ECS: the service name. Lambda: the function's construct id. */
10
+ readonly declaredBy: string;
11
+ readonly docker: DockerBuild | undefined;
12
+ }
3
13
  /**
4
14
  * Get or create the per-key image-tag CfnParameter (logical id from
5
15
  * `imageTagParameterName`). Idempotent by logical id: a second call with the
@@ -8,5 +18,12 @@ import { type Construct } from "constructs";
8
18
  * than once within a stack). Distinct keys that happen to collide on the same
9
19
  * PascalCase logical id fail closed rather than silently aliasing two
10
20
  * different things onto one parameter.
21
+ *
22
+ * Cross-entity reuse (an ECS service name and a Lambda `image` key sharing
23
+ * one build key) is allowed only when both declare fingerprint-identical
24
+ * docker configs — the deliberate shared-build case. Any divergence
25
+ * (including docker-vs-no-docker) throws at synth, mirroring deploy-core's
26
+ * `assertNoCrossEntityBuildKeyDivergence`: a shared parameter with divergent
27
+ * builds means a deploy of either entity silently rolls both onto one image.
11
28
  */
12
- export declare function getOrCreateImageTagParameter(scope: Construct, key: string): CfnParameter;
29
+ export declare function getOrCreateImageTagParameter(scope: Construct, key: string, entity: ImageTagEntity): CfnParameter;
@@ -1,8 +1,16 @@
1
1
  import { CfnParameter, Stack } from "aws-cdk-lib";
2
2
  import { imageTagParameterName } from "@fjall/util";
3
+ import { dockerBuildFingerprint } from "@fjall/util/manifest/schemas";
4
+ /** Per-parameter entity metadata; WeakMap keeps the registry per-stack and test-safe. */
5
+ const registrations = new WeakMap();
3
6
  function buildImageTagDescription(key) {
4
7
  return `Image tag for ${key}. Set by fjall deploy to the content-hash tag.`;
5
8
  }
9
+ function describeEntity(kind, declaredBy) {
10
+ return kind === "ecs-service"
11
+ ? `ECS service "${declaredBy}"`
12
+ : `Lambda function "${declaredBy}"`;
13
+ }
6
14
  /**
7
15
  * Get or create the per-key image-tag CfnParameter (logical id from
8
16
  * `imageTagParameterName`). Idempotent by logical id: a second call with the
@@ -11,8 +19,15 @@ function buildImageTagDescription(key) {
11
19
  * than once within a stack). Distinct keys that happen to collide on the same
12
20
  * PascalCase logical id fail closed rather than silently aliasing two
13
21
  * different things onto one parameter.
22
+ *
23
+ * Cross-entity reuse (an ECS service name and a Lambda `image` key sharing
24
+ * one build key) is allowed only when both declare fingerprint-identical
25
+ * docker configs — the deliberate shared-build case. Any divergence
26
+ * (including docker-vs-no-docker) throws at synth, mirroring deploy-core's
27
+ * `assertNoCrossEntityBuildKeyDivergence`: a shared parameter with divergent
28
+ * builds means a deploy of either entity silently rolls both onto one image.
14
29
  */
15
- export function getOrCreateImageTagParameter(scope, key) {
30
+ export function getOrCreateImageTagParameter(scope, key, entity) {
16
31
  const stack = Stack.of(scope);
17
32
  const paramLogicalId = imageTagParameterName(key);
18
33
  const description = buildImageTagDescription(key);
@@ -22,11 +37,26 @@ export function getOrCreateImageTagParameter(scope, key) {
22
37
  throw new Error(`CfnParameter logical-ID collision: "${paramLogicalId}" is already registered for a different service. ` +
23
38
  `Distinct service/lambda names cannot share a PascalCase identifier — rename one of the conflicting entries.`);
24
39
  }
40
+ const prior = registrations.get(existing);
41
+ const fingerprint = dockerBuildFingerprint(entity.docker);
42
+ if (prior !== undefined && prior.fingerprint !== fingerprint) {
43
+ throw new Error(`Image-tag parameter collision: ${describeEntity(prior.kind, prior.declaredBy)} and ` +
44
+ `${describeEntity(entity.kind, entity.declaredBy)} share the build key "${key}" ` +
45
+ `(CfnParameter "${paramLogicalId}") but declare different docker configs — ` +
46
+ `a deploy of either would silently roll both onto one image. Rename one of them, ` +
47
+ `or make the docker configs identical to deliberately share the build.`);
48
+ }
25
49
  return existing;
26
50
  }
27
- return new CfnParameter(stack, paramLogicalId, {
51
+ const param = new CfnParameter(stack, paramLogicalId, {
28
52
  type: "String",
29
53
  default: "latest",
30
54
  description
31
55
  });
56
+ registrations.set(param, {
57
+ kind: entity.kind,
58
+ declaredBy: entity.declaredBy,
59
+ fingerprint: dockerBuildFingerprint(entity.docker)
60
+ });
61
+ return param;
32
62
  }
@@ -2,7 +2,7 @@ import { SingletonFunction as singletonFunction, Function, Code, type Runtime, A
2
2
  import { type Bucket } from "aws-cdk-lib/aws-s3";
3
3
  import { PolicyStatement, type IRole } from "aws-cdk-lib/aws-iam";
4
4
  import { type IVpc } from "aws-cdk-lib/aws-ec2";
5
- import { RetentionDays } from "aws-cdk-lib/aws-logs";
5
+ import type { RetentionDays } from "aws-cdk-lib/aws-logs";
6
6
  import { type IQueue } from "aws-cdk-lib/aws-sqs";
7
7
  import { type ITable } from "aws-cdk-lib/aws-dynamodb";
8
8
  import { type Construct } from "constructs";
@@ -7,8 +7,7 @@ import { createHash } from "node:crypto";
7
7
  import { SqsEventSource, DynamoEventSource, S3EventSource } from "aws-cdk-lib/aws-lambda-event-sources";
8
8
  import { EventType } from "aws-cdk-lib/aws-s3";
9
9
  import { PolicyStatement, Effect } from "aws-cdk-lib/aws-iam";
10
- import { RetentionDays } from "aws-cdk-lib/aws-logs";
11
- import { LogGroup } from "../logging/logGroup.js";
10
+ import { DEFAULT_FRAMEWORK_LOG_RETENTION, LogGroup } from "../logging/logGroup.js";
12
11
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
12
  import { buildParameterPath } from "@fjall/util";
14
13
  import { resolveImportedSecret } from "../secrets/index.js";
@@ -95,7 +94,10 @@ export class SingletonFunction extends singletonFunction {
95
94
  runtime: props.runtime,
96
95
  ephemeralStorageSize: props.ephemeralStorageSize
97
96
  ? Size.mebibytes(props.ephemeralStorageSize)
98
- : undefined
97
+ : undefined,
98
+ logGroup: new LogGroup(scope, `${id}LogGroup`, {
99
+ retention: props.logGroupRetention ?? DEFAULT_FRAMEWORK_LOG_RETENTION
100
+ })
99
101
  });
100
102
  addPoliciesToRole(this, props.inlinePolicy);
101
103
  applyRoleDescription(this, props.roleDescription);
@@ -125,7 +127,7 @@ export class LambdaFunction extends Function {
125
127
  description: props.lambdaDescription ?? `${id} Lambda`,
126
128
  environment: props.environment,
127
129
  logGroup: new LogGroup(scope, `${id}LogGroup`, {
128
- retention: props.logGroupRetention ?? RetentionDays.ONE_WEEK
130
+ retention: props.logGroupRetention ?? DEFAULT_FRAMEWORK_LOG_RETENTION
129
131
  })
130
132
  });
131
133
  addPoliciesToRole(this, props.inlinePolicy);
@@ -6,6 +6,7 @@ import { Code, Runtime } from "aws-cdk-lib/aws-lambda";
6
6
  import { Provider } from "aws-cdk-lib/custom-resources";
7
7
  import { Construct } from "constructs";
8
8
  import { SingletonFunction } from "../../compute/lambda.js";
9
+ import { DEFAULT_FRAMEWORK_LOG_RETENTION, LogGroup } from "../../logging/logGroup.js";
9
10
  const __dirname = dirname(fileURLToPath(import.meta.url));
10
11
  /**
11
12
  * Resolves relative to the compiled location too — the build step copies the
@@ -51,7 +52,10 @@ export class IdentityStoreUserProvider extends Construct {
51
52
  ]
52
53
  });
53
54
  const provider = new Provider(this, "Provider", {
54
- onEventHandler: handler
55
+ onEventHandler: handler,
56
+ logGroup: new LogGroup(this, "ProviderLogGroup", {
57
+ retention: DEFAULT_FRAMEWORK_LOG_RETENTION
58
+ })
55
59
  });
56
60
  this.serviceToken = provider.serviceToken;
57
61
  }
@@ -1,2 +1,3 @@
1
1
  export * from "./cloudTrail.js";
2
2
  export * from "./logGroup.js";
3
+ export * from "./singletonLogGroup.js";
@@ -1,2 +1,3 @@
1
1
  export * from "./cloudTrail.js";
2
2
  export * from "./logGroup.js";
3
+ export * from "./singletonLogGroup.js";
@@ -1,5 +1,12 @@
1
1
  import * as Logs from "aws-cdk-lib/aws-logs";
2
2
  import { type Construct } from "constructs";
3
+ /**
4
+ * Default retention for framework-owned lambda log groups. Coupled across
5
+ * every framework lambda construct (LambdaFunction, SingletonFunction, the
6
+ * custom-resource Provider groups, and the stack-singleton groups) — drift
7
+ * would silently give sibling framework lambdas different retention.
8
+ */
9
+ export declare const DEFAULT_FRAMEWORK_LOG_RETENTION = Logs.RetentionDays.ONE_WEEK;
3
10
  export declare class LogGroup extends Logs.LogGroup {
4
11
  constructor(scope: Construct, id: string, props?: Logs.LogGroupProps);
5
12
  }
@@ -1,4 +1,11 @@
1
1
  import * as Logs from "aws-cdk-lib/aws-logs";
2
+ /**
3
+ * Default retention for framework-owned lambda log groups. Coupled across
4
+ * every framework lambda construct (LambdaFunction, SingletonFunction, the
5
+ * custom-resource Provider groups, and the stack-singleton groups) — drift
6
+ * would silently give sibling framework lambdas different retention.
7
+ */
8
+ export const DEFAULT_FRAMEWORK_LOG_RETENTION = Logs.RetentionDays.ONE_WEEK;
2
9
  export class LogGroup extends Logs.LogGroup {
3
10
  constructor(scope, id, props) {
4
11
  super(scope, id, props);
@@ -0,0 +1,15 @@
1
+ import { type Construct } from "constructs";
2
+ import { LogGroup } from "./logGroup.js";
3
+ /**
4
+ * Returns the stack-scoped shared LogGroup for a family of framework lambdas
5
+ * that CDK dedupes to one function per stack (AwsCustomResource's AWS679f…
6
+ * singleton, BucketDeployment's custom-resource handler). Every call site in
7
+ * the stack MUST hand the singleton lambda the SAME log group — CDK applies
8
+ * the first instantiation's props and silently ignores later ones, which
9
+ * would leave per-site groups synthesised but unused.
10
+ *
11
+ * The group is nameless (CDK-generated name), so it never collides with the
12
+ * `/aws/lambda/<fn>` group the Lambda service auto-created on existing
13
+ * deployments; the auto-created group simply stops receiving logs.
14
+ */
15
+ export declare function stackSingletonLogGroup(scope: Construct, purpose: "AwsCustomResource" | "BucketDeployment"): LogGroup;
@@ -0,0 +1,24 @@
1
+ import { Stack } from "aws-cdk-lib";
2
+ import { DEFAULT_FRAMEWORK_LOG_RETENTION, LogGroup } from "./logGroup.js";
3
+ /**
4
+ * Returns the stack-scoped shared LogGroup for a family of framework lambdas
5
+ * that CDK dedupes to one function per stack (AwsCustomResource's AWS679f…
6
+ * singleton, BucketDeployment's custom-resource handler). Every call site in
7
+ * the stack MUST hand the singleton lambda the SAME log group — CDK applies
8
+ * the first instantiation's props and silently ignores later ones, which
9
+ * would leave per-site groups synthesised but unused.
10
+ *
11
+ * The group is nameless (CDK-generated name), so it never collides with the
12
+ * `/aws/lambda/<fn>` group the Lambda service auto-created on existing
13
+ * deployments; the auto-created group simply stops receiving logs.
14
+ */
15
+ export function stackSingletonLogGroup(scope, purpose) {
16
+ const stack = Stack.of(scope);
17
+ const id = `Fjall${purpose}LogGroup`;
18
+ const existing = stack.node.tryFindChild(id);
19
+ if (existing !== undefined)
20
+ return existing;
21
+ return new LogGroup(stack, id, {
22
+ retention: DEFAULT_FRAMEWORK_LOG_RETENTION
23
+ });
24
+ }