@intentius/chant-lexicon-aws 0.45.0 → 0.49.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.
Files changed (147) hide show
  1. package/dist/api/read-client.d.ts +10 -2
  2. package/dist/api/read-client.d.ts.map +1 -1
  3. package/dist/components/cloud-executor.d.ts.map +1 -1
  4. package/dist/composites/agentcore-agent.d.ts +26 -1
  5. package/dist/composites/agentcore-agent.d.ts.map +1 -1
  6. package/dist/composites/bucket-deployment.d.ts +82 -0
  7. package/dist/composites/bucket-deployment.d.ts.map +1 -0
  8. package/dist/composites/dynamodb-table.d.ts +45 -0
  9. package/dist/composites/dynamodb-table.d.ts.map +1 -0
  10. package/dist/composites/ec2-instance-bundle.d.ts +61 -0
  11. package/dist/composites/ec2-instance-bundle.d.ts.map +1 -0
  12. package/dist/composites/ecr-repository.d.ts +49 -0
  13. package/dist/composites/ecr-repository.d.ts.map +1 -0
  14. package/dist/composites/fargate-service.d.ts +16 -0
  15. package/dist/composites/fargate-service.d.ts.map +1 -1
  16. package/dist/composites/index.d.ts +16 -2
  17. package/dist/composites/index.d.ts.map +1 -1
  18. package/dist/composites/lambda-function.d.ts +4 -4
  19. package/dist/composites/lambda-function.d.ts.map +1 -1
  20. package/dist/composites/monitoring-stack.d.ts +99 -0
  21. package/dist/composites/monitoring-stack.d.ts.map +1 -0
  22. package/dist/composites/nlb-service.d.ts +48 -0
  23. package/dist/composites/nlb-service.d.ts.map +1 -0
  24. package/dist/composites/step-functions-workflow.d.ts +36 -0
  25. package/dist/composites/step-functions-workflow.d.ts.map +1 -0
  26. package/dist/deep-observe.d.ts +45 -0
  27. package/dist/deep-observe.d.ts.map +1 -1
  28. package/dist/deep-topology.d.ts +104 -0
  29. package/dist/deep-topology.d.ts.map +1 -0
  30. package/dist/effect-receipt-row.d.ts +92 -0
  31. package/dist/effect-receipt-row.d.ts.map +1 -0
  32. package/dist/index.d.ts +6 -2
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/integrity.json +9 -4
  35. package/dist/lint/audit-catalog.d.ts.map +1 -1
  36. package/dist/lint/post-synth/cf-refs.d.ts +24 -0
  37. package/dist/lint/post-synth/cf-refs.d.ts.map +1 -1
  38. package/dist/lint/post-synth/index.d.ts.map +1 -1
  39. package/dist/lint/post-synth/waw059.d.ts +36 -0
  40. package/dist/lint/post-synth/waw059.d.ts.map +1 -0
  41. package/dist/lint/post-synth/waw060.d.ts +16 -0
  42. package/dist/lint/post-synth/waw060.d.ts.map +1 -0
  43. package/dist/lint/post-synth/waw061.d.ts +19 -0
  44. package/dist/lint/post-synth/waw061.d.ts.map +1 -0
  45. package/dist/lint/post-synth/waw062.d.ts +19 -0
  46. package/dist/lint/post-synth/waw062.d.ts.map +1 -0
  47. package/dist/lint/post-synth/waw063.d.ts +35 -0
  48. package/dist/lint/post-synth/waw063.d.ts.map +1 -0
  49. package/dist/manifest.json +1 -1
  50. package/dist/okf/index.md +5 -0
  51. package/dist/okf/rules/WAW059.md +25 -0
  52. package/dist/okf/rules/WAW060.md +17 -0
  53. package/dist/okf/rules/WAW061.md +17 -0
  54. package/dist/okf/rules/WAW062.md +27 -0
  55. package/dist/okf/rules/WAW063.md +19 -0
  56. package/dist/okf/types/Action.md +2 -0
  57. package/dist/okf/types/AutoScalingGroup.md +1 -0
  58. package/dist/okf/types/Bucket.md +2 -0
  59. package/dist/okf/types/ECRRepository.md +1 -0
  60. package/dist/okf/types/Function.md +1 -0
  61. package/dist/okf/types/GlobalTable.md +4 -0
  62. package/dist/okf/types/IamPolicy.md +3 -0
  63. package/dist/okf/types/InstanceProfile.md +4 -0
  64. package/dist/okf/types/LaunchTemplate.md +1 -0
  65. package/dist/okf/types/LoadBalancer.md +1 -0
  66. package/dist/okf/types/ManagedPolicy.md +4 -0
  67. package/dist/okf/types/Map.md +2 -0
  68. package/dist/okf/types/Queue.md +1 -0
  69. package/dist/okf/types/Role.md +3 -0
  70. package/dist/okf/types/SecurityGroup.md +1 -0
  71. package/dist/okf/types/StateMachine.md +4 -0
  72. package/dist/okf/types/Subnet.md +4 -0
  73. package/dist/okf/types/Table.md +2 -0
  74. package/dist/okf/types/Type.md +5 -0
  75. package/dist/okf/types/Vpc.md +1 -0
  76. package/dist/op/activities/aws-apply.d.ts +48 -5
  77. package/dist/op/activities/aws-apply.d.ts.map +1 -1
  78. package/dist/op/activities/index.d.ts +11 -4
  79. package/dist/op/activities/index.d.ts.map +1 -1
  80. package/dist/ownership.d.ts +18 -0
  81. package/dist/ownership.d.ts.map +1 -1
  82. package/dist/plugin.d.ts.map +1 -1
  83. package/dist/receipt-store.d.ts +109 -0
  84. package/dist/receipt-store.d.ts.map +1 -0
  85. package/dist/rules/cf-refs.ts +62 -0
  86. package/dist/rules/waw059.ts +353 -0
  87. package/dist/rules/waw060.ts +91 -0
  88. package/dist/rules/waw061.ts +67 -0
  89. package/dist/rules/waw062.ts +0 -0
  90. package/dist/rules/waw063.ts +0 -0
  91. package/dist/serializer.d.ts.map +1 -1
  92. package/dist/skills/chant-aws-carve-terraform.md +26 -23
  93. package/dist/teardown.d.ts +85 -0
  94. package/dist/teardown.d.ts.map +1 -0
  95. package/package.json +3 -3
  96. package/src/api/read-client.test.ts +9 -0
  97. package/src/api/read-client.ts +15 -2
  98. package/src/components/cloud-executor.ts +10 -1
  99. package/src/composites/agentcore-agent.ts +35 -13
  100. package/src/composites/bucket-deployment.test.ts +84 -0
  101. package/src/composites/bucket-deployment.ts +153 -0
  102. package/src/composites/composites.test.ts +325 -0
  103. package/src/composites/dynamodb-table.test.ts +154 -0
  104. package/src/composites/dynamodb-table.ts +124 -0
  105. package/src/composites/ec2-instance-bundle.ts +134 -0
  106. package/src/composites/ecr-repository.test.ts +118 -0
  107. package/src/composites/ecr-repository.ts +117 -0
  108. package/src/composites/fargate-service.ts +19 -3
  109. package/src/composites/index.ts +21 -2
  110. package/src/composites/monitoring-stack.test.ts +130 -0
  111. package/src/composites/monitoring-stack.ts +164 -0
  112. package/src/composites/nlb-service.ts +117 -0
  113. package/src/composites/step-functions-workflow.ts +127 -0
  114. package/src/deep-observe.test.ts +201 -1
  115. package/src/deep-observe.ts +204 -0
  116. package/src/deep-topology.test.ts +392 -0
  117. package/src/deep-topology.ts +237 -0
  118. package/src/effect-receipt-row.test.ts +164 -0
  119. package/src/effect-receipt-row.ts +147 -0
  120. package/src/index.ts +28 -7
  121. package/src/lifecycle-integration.test.ts +4 -0
  122. package/src/lint/audit-catalog.ts +11 -0
  123. package/src/lint/post-synth/cf-refs.ts +62 -0
  124. package/src/lint/post-synth/index.ts +10 -0
  125. package/src/lint/post-synth/waw059.test.ts +309 -0
  126. package/src/lint/post-synth/waw059.ts +353 -0
  127. package/src/lint/post-synth/waw060.test.ts +131 -0
  128. package/src/lint/post-synth/waw060.ts +91 -0
  129. package/src/lint/post-synth/waw061.test.ts +99 -0
  130. package/src/lint/post-synth/waw061.ts +67 -0
  131. package/src/lint/post-synth/waw062.test.ts +109 -0
  132. package/src/lint/post-synth/waw062.ts +0 -0
  133. package/src/lint/post-synth/waw063.test.ts +220 -0
  134. package/src/lint/post-synth/waw063.ts +0 -0
  135. package/src/op/activities/aws-apply.test.ts +111 -0
  136. package/src/op/activities/aws-apply.ts +100 -6
  137. package/src/op/activities/index.ts +20 -3
  138. package/src/ownership.test.ts +24 -1
  139. package/src/ownership.ts +37 -0
  140. package/src/plugin.ts +59 -10
  141. package/src/receipt-store.test.ts +204 -0
  142. package/src/receipt-store.ts +315 -0
  143. package/src/serializer-ownership.test.ts +18 -0
  144. package/src/serializer.ts +114 -3
  145. package/src/skills/chant-aws-carve-terraform.md +26 -23
  146. package/src/teardown.test.ts +258 -0
  147. package/src/teardown.ts +276 -0
@@ -0,0 +1,124 @@
1
+ import { Composite, mergeDefaults, type Value } from "@intentius/chant";
2
+ import {
3
+ Table,
4
+ Table_AttributeDefinition,
5
+ Table_KeySchema,
6
+ Table_GlobalSecondaryIndex,
7
+ Table_Projection,
8
+ Table_ProvisionedThroughput,
9
+ Table_StreamSpecification,
10
+ Table_TimeToLiveSpecification,
11
+ } from "../generated";
12
+
13
+ export interface DynamoDBKey {
14
+ name: string;
15
+ type?: "S" | "N" | "B";
16
+ }
17
+
18
+ export interface DynamoDBCapacity {
19
+ readCapacity: number;
20
+ writeCapacity: number;
21
+ }
22
+
23
+ export interface DynamoDBGlobalSecondaryIndex {
24
+ name: string;
25
+ partitionKey: DynamoDBKey;
26
+ sortKey?: DynamoDBKey;
27
+ /** Defaults to "ALL" — projecting the whole item is the common case; narrow it deliberately. */
28
+ projectionType?: "ALL" | "KEYS_ONLY" | "INCLUDE";
29
+ /** Only meaningful with projectionType "INCLUDE". */
30
+ nonKeyAttributes?: string[];
31
+ /** PROVISIONED tables only. Falls back to the table's own capacity when a GSI doesn't need its own. */
32
+ provisionedThroughput?: DynamoDBCapacity;
33
+ }
34
+
35
+ export interface DynamoDBTableProps {
36
+ /** `Value<string>`: a name is routinely built with `Sub`/`Ref` (#1366). */
37
+ tableName?: Value<string>;
38
+ partitionKey: DynamoDBKey;
39
+ sortKey?: DynamoDBKey;
40
+ /** Defaults to "PAY_PER_REQUEST" — the recommended default for most workloads (matches LambdaDynamoDB). */
41
+ billingMode?: "PAY_PER_REQUEST" | "PROVISIONED";
42
+ /** Required when billingMode is "PROVISIONED"; ignored otherwise. Falls back to 5/5 (the DynamoDB console default) if omitted. */
43
+ provisionedThroughput?: DynamoDBCapacity;
44
+ globalSecondaryIndexes?: DynamoDBGlobalSecondaryIndex[];
45
+ /** Attribute name that carries the expiry timestamp. Enables TimeToLiveSpecification when set. */
46
+ ttlAttribute?: string;
47
+ /** Enables StreamSpecification. `true` defaults to NEW_AND_OLD_IMAGES (the shape most stream consumers want). */
48
+ stream?: boolean | { viewType?: "NEW_IMAGE" | "OLD_IMAGE" | "NEW_AND_OLD_IMAGES" | "KEYS_ONLY" };
49
+ defaults?: {
50
+ table?: Partial<ConstructorParameters<typeof Table>[0]>;
51
+ };
52
+ }
53
+
54
+ const DEFAULT_CAPACITY: DynamoDBCapacity = { readCapacity: 5, writeCapacity: 5 };
55
+
56
+ function toProvisionedThroughput(capacity: DynamoDBCapacity): InstanceType<typeof Table_ProvisionedThroughput> {
57
+ return new Table_ProvisionedThroughput({
58
+ ReadCapacityUnits: capacity.readCapacity,
59
+ WriteCapacityUnits: capacity.writeCapacity,
60
+ });
61
+ }
62
+
63
+ function toKeySchema(partitionKey: DynamoDBKey, sortKey?: DynamoDBKey): InstanceType<typeof Table_KeySchema>[] {
64
+ // A ternary (not an `if`) keeps the `new`s unconditional per EVL002, and
65
+ // returning array literals directly from each branch — no spread — sidesteps
66
+ // EVL004 (outside a Composite factory, EVL004 only traces a spread source to a
67
+ // module-top-level const or a literal, not a local variable).
68
+ const partitionEntry = new Table_KeySchema({ AttributeName: partitionKey.name, KeyType: "HASH" });
69
+ const sortEntry = sortKey ? new Table_KeySchema({ AttributeName: sortKey.name, KeyType: "RANGE" }) : undefined;
70
+ return sortEntry ? [partitionEntry, sortEntry] : [partitionEntry];
71
+ }
72
+
73
+ export const DynamoDBTable = Composite((props: DynamoDBTableProps) => {
74
+ const billingMode = props.billingMode ?? "PAY_PER_REQUEST";
75
+ const { defaults } = props;
76
+
77
+ // Every key attribute used anywhere (table keys + every GSI's keys) needs exactly
78
+ // one AttributeDefinition. A Map dedupes by name so a GSI reusing the table's
79
+ // partition key (a common pattern) doesn't produce a duplicate definition.
80
+ const attributeTypes = new Map<string, "S" | "N" | "B">();
81
+ attributeTypes.set(props.partitionKey.name, props.partitionKey.type ?? "S");
82
+ if (props.sortKey) attributeTypes.set(props.sortKey.name, props.sortKey.type ?? "S");
83
+ for (const gsi of props.globalSecondaryIndexes ?? []) {
84
+ attributeTypes.set(gsi.partitionKey.name, gsi.partitionKey.type ?? "S");
85
+ if (gsi.sortKey) attributeTypes.set(gsi.sortKey.name, gsi.sortKey.type ?? "S");
86
+ }
87
+ const attributeDefinitions = [...attributeTypes.entries()].map(
88
+ ([AttributeName, AttributeType]) => new Table_AttributeDefinition({ AttributeName, AttributeType }),
89
+ );
90
+
91
+ const globalSecondaryIndexes = (props.globalSecondaryIndexes ?? []).map((gsi) => new Table_GlobalSecondaryIndex({
92
+ IndexName: gsi.name,
93
+ KeySchema: toKeySchema(gsi.partitionKey, gsi.sortKey),
94
+ Projection: new Table_Projection({
95
+ ProjectionType: gsi.projectionType ?? "ALL",
96
+ ...(gsi.nonKeyAttributes ? { NonKeyAttributes: gsi.nonKeyAttributes } : {}),
97
+ }),
98
+ ...(billingMode === "PROVISIONED"
99
+ ? { ProvisionedThroughput: toProvisionedThroughput(gsi.provisionedThroughput ?? props.provisionedThroughput ?? DEFAULT_CAPACITY) }
100
+ : {}),
101
+ }));
102
+
103
+ const streamConfig = props.stream === true ? {} : props.stream || undefined;
104
+ const ttlEnabled = props.ttlAttribute !== undefined;
105
+
106
+ const table = new Table(mergeDefaults({
107
+ TableName: props.tableName,
108
+ BillingMode: billingMode,
109
+ AttributeDefinitions: attributeDefinitions,
110
+ KeySchema: toKeySchema(props.partitionKey, props.sortKey),
111
+ ...(globalSecondaryIndexes.length > 0 ? { GlobalSecondaryIndexes: globalSecondaryIndexes } : {}),
112
+ ...(billingMode === "PROVISIONED"
113
+ ? { ProvisionedThroughput: toProvisionedThroughput(props.provisionedThroughput ?? DEFAULT_CAPACITY) }
114
+ : {}),
115
+ ...(streamConfig
116
+ ? { StreamSpecification: new Table_StreamSpecification({ StreamViewType: streamConfig.viewType ?? "NEW_AND_OLD_IMAGES" }) }
117
+ : {}),
118
+ ...(ttlEnabled
119
+ ? { TimeToLiveSpecification: new Table_TimeToLiveSpecification({ Enabled: true, AttributeName: props.ttlAttribute }) }
120
+ : {}),
121
+ }, defaults?.table));
122
+
123
+ return { table };
124
+ }, "DynamoDBTable");
@@ -0,0 +1,134 @@
1
+ import { Composite, mergeDefaults } from "@intentius/chant";
2
+ import { Instance, InstanceProfile, Role, SecurityGroup, SecurityGroup_Ingress } from "../generated";
3
+ import { Base64, Ref } from "../intrinsics";
4
+
5
+ const EC2_ASSUME_ROLE = {
6
+ Version: "2012-10-17" as const,
7
+ Statement: [
8
+ {
9
+ Effect: "Allow" as const,
10
+ Principal: { Service: "ec2.amazonaws.com" },
11
+ Action: "sts:AssumeRole",
12
+ },
13
+ ],
14
+ };
15
+
16
+ /**
17
+ * One inbound rule for the instance's security group. Exactly one of `cidr`,
18
+ * `cidrIpv6`, or `sourceSecurityGroupId` selects the traffic source — the same
19
+ * constraint CloudFormation enforces on `AWS::EC2::SecurityGroup.Ingress`.
20
+ */
21
+ export interface Ec2InstanceBundleIngressRule {
22
+ /** Starting port (or the only port, when `toPort` is omitted). */
23
+ fromPort: number;
24
+ /** Ending port of the range. Defaults to `fromPort` (a single port). */
25
+ toPort?: number;
26
+ /** IP protocol. Default: `"tcp"`. */
27
+ protocol?: string;
28
+ /** IPv4 CIDR source. */
29
+ cidr?: string;
30
+ /** IPv6 CIDR source. */
31
+ cidrIpv6?: string;
32
+ /** Security-group source (allow traffic from another SG rather than a CIDR). */
33
+ sourceSecurityGroupId?: string;
34
+ description?: string;
35
+ }
36
+
37
+ export interface Ec2InstanceBundleProps {
38
+ /** AMI id to launch. */
39
+ imageId: string;
40
+ /** VPC the instance and its security group live in. */
41
+ vpcId: string;
42
+ /** Subnet the instance launches into — fixes its AZ and public/private placement. */
43
+ subnetId: string;
44
+ /** EC2 instance type. Default: `"t3.micro"`. */
45
+ instanceType?: string;
46
+ /** SSH key pair name. Omit for keyless access (e.g. SSM Session Manager only). */
47
+ keyName?: string;
48
+ /** Plain-text user-data script; wrapped in `Fn::Base64` for the instance. */
49
+ userData?: string;
50
+ /** Inbound security-group rules. Default: none (no inbound traffic). */
51
+ ingress?: Ec2InstanceBundleIngressRule[];
52
+ /** IAM managed policy ARNs attached to the instance role. */
53
+ ManagedPolicyArns?: string[];
54
+ /** Inline IAM policies attached to the instance role. */
55
+ Policies?: ConstructorParameters<typeof Role>[0]["Policies"];
56
+ defaults?: {
57
+ role?: Partial<ConstructorParameters<typeof Role>[0]>;
58
+ instanceProfile?: Partial<ConstructorParameters<typeof InstanceProfile>[0]>;
59
+ sg?: Partial<ConstructorParameters<typeof SecurityGroup>[0]>;
60
+ instance?: Partial<ConstructorParameters<typeof Instance>[0]>;
61
+ };
62
+ }
63
+
64
+ // A rule needs exactly one traffic source. Kept as a lookup (not an if/else
65
+ // chain that builds the `SecurityGroup_Ingress` inline) so the `new` stays out
66
+ // of control flow (EVL002).
67
+ function ingressSource(rule: Ec2InstanceBundleIngressRule): Partial<Pick<
68
+ ConstructorParameters<typeof SecurityGroup_Ingress>[0],
69
+ "CidrIp" | "CidrIpv6" | "SourceSecurityGroupId"
70
+ >> {
71
+ if (rule.cidr) return { CidrIp: rule.cidr };
72
+ if (rule.cidrIpv6) return { CidrIpv6: rule.cidrIpv6 };
73
+ if (rule.sourceSecurityGroupId) return { SourceSecurityGroupId: rule.sourceSecurityGroupId };
74
+ throw new Error(
75
+ `Ec2InstanceBundle ingress rule for port ${rule.fromPort} needs one of cidr, cidrIpv6, or sourceSecurityGroupId`,
76
+ );
77
+ }
78
+
79
+ /**
80
+ * EC2 instance bundle — instance + security group + role/profile + subnet
81
+ * placement, the hand-rolled unit the aws-bench corpus showed 25 times
82
+ * (`ec2.Instance`) alongside 79 `ec2.SecurityGroup` instantiations (#1139).
83
+ * `Ec2InstanceRole` already covers the role/profile half; this composite adds
84
+ * the instance and its security group, wired to that same role.
85
+ */
86
+ export const Ec2InstanceBundle = Composite((props: Ec2InstanceBundleProps) => {
87
+ const { defaults } = props;
88
+ const instanceType = props.instanceType ?? "t3.micro";
89
+ const ingress = props.ingress ?? [];
90
+
91
+ const role = new Role(mergeDefaults({
92
+ AssumeRolePolicyDocument: EC2_ASSUME_ROLE,
93
+ ManagedPolicyArns: props.ManagedPolicyArns ?? [],
94
+ Policies: props.Policies ?? [],
95
+ }, defaults?.role));
96
+
97
+ const instanceProfile = new InstanceProfile(mergeDefaults({
98
+ Roles: [Ref(role)],
99
+ }, defaults?.instanceProfile));
100
+
101
+ // The list-length is dynamic (a caller-supplied array of rules, unlike the
102
+ // fixed one-or-zero-rule shape RdsInstance's ternary handles), so this
103
+ // `.map()` — and the `ingressSource(rule)` spread inside it — has to live
104
+ // in the factory rather than be pre-computed as a const above it. Both are
105
+ // the accepted, already-shipped shape for this in the codebase: EVL004
106
+ // exempts spreads inside the Composite factory precisely for this case,
107
+ // and EVL010's warning on the `.map()` here matches the same warning on
108
+ // FargateAlb, FargateService, and EksCluster's per-item resource lists.
109
+ const ingressRules = ingress.map((rule) => new SecurityGroup_Ingress({
110
+ IpProtocol: rule.protocol ?? "tcp",
111
+ FromPort: rule.fromPort,
112
+ ToPort: rule.toPort ?? rule.fromPort,
113
+ Description: rule.description,
114
+ ...ingressSource(rule),
115
+ }));
116
+
117
+ const sg = new SecurityGroup(mergeDefaults({
118
+ GroupDescription: "Security group for EC2 instance",
119
+ VpcId: props.vpcId,
120
+ SecurityGroupIngress: ingressRules.length > 0 ? ingressRules : undefined,
121
+ }, defaults?.sg));
122
+
123
+ const instance = new Instance(mergeDefaults({
124
+ ImageId: props.imageId,
125
+ InstanceType: instanceType,
126
+ SubnetId: props.subnetId,
127
+ SecurityGroupIds: [sg.GroupId],
128
+ IamInstanceProfile: Ref(instanceProfile),
129
+ KeyName: props.keyName,
130
+ UserData: props.userData ? Base64(props.userData) : undefined,
131
+ }, defaults?.instance));
132
+
133
+ return { role, instanceProfile, sg, instance };
134
+ }, "Ec2InstanceBundle");
@@ -0,0 +1,118 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { expandComposite } from "@intentius/chant";
3
+ import { EcrRepository } from "./ecr-repository";
4
+
5
+ // Generated property-kind instances (ECRRepository_ImageScanningConfiguration, ...) store
6
+ // their data on a non-enumerable `.props`, so `toEqual` against a plain object
7
+ // compares empty own-enumerable-property sets unless each instance is unwrapped first.
8
+ const p = (x: any) => x.props;
9
+
10
+ describe("EcrRepository", () => {
11
+ test("no props: single member, scan-on-push defaults on, default lifecycle policy present", () => {
12
+ const instance = EcrRepository({});
13
+ expect(Object.keys(instance.members)).toEqual(["repository"]);
14
+ const props = (instance.repository as any).props;
15
+ expect(p(props.ImageScanningConfiguration)).toEqual({ ScanOnPush: true });
16
+ expect(props.LifecyclePolicy).toBeDefined();
17
+ });
18
+
19
+ test("expandComposite produces the repository's logical name", () => {
20
+ const expanded = expandComposite("app", EcrRepository({}));
21
+ expect(expanded.has("appRepository")).toBe(true);
22
+ expect(expanded.size).toBe(1);
23
+ });
24
+
25
+ test("default lifecycle policy expires untagged images after 14 days", () => {
26
+ const props = (EcrRepository({}).repository as any).props;
27
+ const policy = JSON.parse(p(props.LifecyclePolicy).LifecyclePolicyText);
28
+ expect(policy.rules).toEqual([{
29
+ rulePriority: 1,
30
+ description: "Expire untagged images after 14 days",
31
+ selection: { tagStatus: "untagged", countType: "sinceImagePushed", countUnit: "days", countNumber: 14 },
32
+ action: { type: "expire" },
33
+ }]);
34
+ });
35
+
36
+ test("scanOnPush: false is threaded through, not just omitted", () => {
37
+ const props = (EcrRepository({ scanOnPush: false }).repository as any).props;
38
+ expect(p(props.ImageScanningConfiguration)).toEqual({ ScanOnPush: false });
39
+ });
40
+
41
+ test("empty lifecycleRules array ships the repository with no lifecycle policy at all", () => {
42
+ const props = (EcrRepository({ lifecycleRules: [] }).repository as any).props;
43
+ expect(props.LifecyclePolicy).toBeUndefined();
44
+ });
45
+
46
+ test("custom lifecycle rules render selection and action, omitting unset optional fields", () => {
47
+ const props = (EcrRepository({
48
+ lifecycleRules: [
49
+ { rulePriority: 1, tagStatus: "tagged", tagPrefixList: ["v"], countType: "imageCountMoreThan", countNumber: 10 },
50
+ ],
51
+ }).repository as any).props;
52
+ const policy = JSON.parse(p(props.LifecyclePolicy).LifecyclePolicyText);
53
+ expect(policy.rules).toEqual([{
54
+ rulePriority: 1,
55
+ selection: { tagStatus: "tagged", tagPrefixList: ["v"], countType: "imageCountMoreThan", countNumber: 10 },
56
+ action: { type: "expire" },
57
+ }]);
58
+ });
59
+
60
+ test("multiple lifecycle rules keep rulePriority order", () => {
61
+ const props = (EcrRepository({
62
+ lifecycleRules: [
63
+ { rulePriority: 1, tagStatus: "tagged", tagPrefixList: ["prod"], countType: "imageCountMoreThan", countNumber: 20 },
64
+ { rulePriority: 2, tagStatus: "untagged", countType: "sinceImagePushed", countUnit: "days", countNumber: 7 },
65
+ ],
66
+ }).repository as any).props;
67
+ const policy = JSON.parse(p(props.LifecyclePolicy).LifecyclePolicyText);
68
+ expect(policy.rules.map((r: any) => r.rulePriority)).toEqual([1, 2]);
69
+ });
70
+
71
+ test("no encryption block by default", () => {
72
+ const props = (EcrRepository({}).repository as any).props;
73
+ expect(props.EncryptionConfiguration).toBeUndefined();
74
+ });
75
+
76
+ test("encryption defaults its type to AES256 when only kmsKeyId-less block is given", () => {
77
+ const props = (EcrRepository({ encryption: {} }).repository as any).props;
78
+ expect(p(props.EncryptionConfiguration)).toEqual({ EncryptionType: "AES256" });
79
+ });
80
+
81
+ test("KMS encryption threads the key id through", () => {
82
+ const props = (EcrRepository({
83
+ encryption: { type: "KMS", kmsKeyId: "alias/my-key" },
84
+ }).repository as any).props;
85
+ expect(p(props.EncryptionConfiguration)).toEqual({ EncryptionType: "KMS", KmsKey: "alias/my-key" });
86
+ });
87
+
88
+ test("repositoryName is threaded through when given", () => {
89
+ const props = (EcrRepository({ repositoryName: "my-app" }).repository as any).props;
90
+ expect(props.RepositoryName).toBe("my-app");
91
+ });
92
+
93
+ test("imageTagMutability is threaded through; omitted when not set", () => {
94
+ const withIt = (EcrRepository({ imageTagMutability: "IMMUTABLE" }).repository as any).props;
95
+ expect(withIt.ImageTagMutability).toBe("IMMUTABLE");
96
+
97
+ const withoutIt = (EcrRepository({}).repository as any).props;
98
+ expect(withoutIt.ImageTagMutability).toBeUndefined();
99
+ });
100
+
101
+ test("repositoryPolicy is threaded through as RepositoryPolicyText", () => {
102
+ const policy = { Version: "2012-10-17", Statement: [{ Effect: "Allow", Principal: "*", Action: "ecr:GetDownloadUrlForLayer" }] };
103
+ const props = (EcrRepository({ repositoryPolicy: policy }).repository as any).props;
104
+ expect(props.RepositoryPolicyText).toEqual(policy);
105
+ });
106
+
107
+ test("emptyOnDelete is threaded through", () => {
108
+ const props = (EcrRepository({ emptyOnDelete: true }).repository as any).props;
109
+ expect(props.EmptyOnDelete).toBe(true);
110
+ });
111
+
112
+ test("defaults escape hatch reaches the repository (e.g. tags)", () => {
113
+ const props = (EcrRepository({
114
+ defaults: { repository: { Tags: [{ Key: "team", Value: "platform" } as any] } },
115
+ }).repository as any).props;
116
+ expect(props.Tags).toEqual([{ Key: "team", Value: "platform" }]);
117
+ });
118
+ });
@@ -0,0 +1,117 @@
1
+ import { Composite, mergeDefaults, type Value } from "@intentius/chant";
2
+ import {
3
+ ECRRepository,
4
+ ECRRepository_EncryptionConfiguration,
5
+ ECRRepository_ImageScanningConfiguration,
6
+ ECRRepository_LifecyclePolicy,
7
+ } from "../generated";
8
+
9
+ export interface EcrLifecycleRule {
10
+ /** Rules are evaluated in ascending priority order; each image is matched by exactly one rule. */
11
+ rulePriority: number;
12
+ description?: string;
13
+ /** Defaults to "any" — matches every image regardless of tag state. */
14
+ tagStatus?: "tagged" | "untagged" | "any";
15
+ /** Only meaningful with tagStatus "tagged". */
16
+ tagPrefixList?: string[];
17
+ /** Only meaningful with tagStatus "tagged". */
18
+ tagPatternList?: string[];
19
+ countType: "imageCountMoreThan" | "sinceImagePushed";
20
+ /** Required (and only meaningful) with countType "sinceImagePushed". */
21
+ countUnit?: "days";
22
+ countNumber: number;
23
+ }
24
+
25
+ export interface EcrEncryption {
26
+ /** Defaults to "AES256" (Amazon S3-managed keys) when this block is provided without a type. */
27
+ type?: "AES256" | "KMS" | "KMS_DSSE";
28
+ /** KMS/KMS_DSSE only. Falls back to the default AWS-managed ECR key when omitted. */
29
+ kmsKeyId?: string;
30
+ }
31
+
32
+ export interface EcrRepositoryProps {
33
+ /** `Value<string>`: a name is routinely built with `Sub`/`Ref` (#1366). Omitted, CloudFormation generates one. */
34
+ repositoryName?: Value<string>;
35
+ imageTagMutability?: "MUTABLE" | "IMMUTABLE" | "MUTABLE_WITH_EXCLUSION" | "IMMUTABLE_WITH_EXCLUSION";
36
+ /** Defaults to true — scanning freshly pushed images is the safe default for a new repository. */
37
+ scanOnPush?: boolean;
38
+ /**
39
+ * Lifecycle rules, evaluated in `rulePriority` order and rendered into the repository's
40
+ * `LifecyclePolicyText`. Defaults to a single "expire untagged images after 14 days" rule
41
+ * -- AWS's own starter policy -- when omitted. Pass `[]` to ship the repository with no
42
+ * lifecycle policy at all.
43
+ */
44
+ lifecycleRules?: EcrLifecycleRule[];
45
+ encryption?: EcrEncryption;
46
+ /** If true, deleting the repository force-deletes its contents instead of requiring it to be empty first. */
47
+ emptyOnDelete?: boolean;
48
+ /** JSON repository policy document (cross-account pull access, etc). */
49
+ repositoryPolicy?: Record<string, unknown>;
50
+ defaults?: {
51
+ repository?: Partial<ConstructorParameters<typeof ECRRepository>[0]>;
52
+ };
53
+ }
54
+
55
+ const DEFAULT_LIFECYCLE_RULES: EcrLifecycleRule[] = [
56
+ {
57
+ rulePriority: 1,
58
+ description: "Expire untagged images after 14 days",
59
+ tagStatus: "untagged",
60
+ countType: "sinceImagePushed",
61
+ countUnit: "days",
62
+ countNumber: 14,
63
+ },
64
+ ];
65
+
66
+ function toJsonRule(rule: EcrLifecycleRule): Record<string, unknown> {
67
+ // Plain conditional assignment (not spread) keeps this traceable for EVL004 —
68
+ // that rule only exempts spreads inside a Composite factory, and this helper
69
+ // runs outside one.
70
+ const selection: Record<string, unknown> = {
71
+ tagStatus: rule.tagStatus ?? "any",
72
+ countType: rule.countType,
73
+ countNumber: rule.countNumber,
74
+ };
75
+ if (rule.tagPrefixList) selection.tagPrefixList = rule.tagPrefixList;
76
+ if (rule.tagPatternList) selection.tagPatternList = rule.tagPatternList;
77
+ if (rule.countUnit) selection.countUnit = rule.countUnit;
78
+
79
+ const jsonRule: Record<string, unknown> = {
80
+ rulePriority: rule.rulePriority,
81
+ selection,
82
+ action: { type: "expire" },
83
+ };
84
+ if (rule.description) jsonRule.description = rule.description;
85
+ return jsonRule;
86
+ }
87
+
88
+ function toLifecyclePolicyText(rules: EcrLifecycleRule[]): string {
89
+ return JSON.stringify({ rules: rules.map(toJsonRule) });
90
+ }
91
+
92
+ export const EcrRepository = Composite((props: EcrRepositoryProps) => {
93
+ const { defaults } = props;
94
+ const scanOnPush = props.scanOnPush ?? true;
95
+ const lifecycleRules = props.lifecycleRules ?? DEFAULT_LIFECYCLE_RULES;
96
+
97
+ const repository = new ECRRepository(mergeDefaults({
98
+ RepositoryName: props.repositoryName,
99
+ ImageTagMutability: props.imageTagMutability,
100
+ EmptyOnDelete: props.emptyOnDelete,
101
+ ImageScanningConfiguration: new ECRRepository_ImageScanningConfiguration({ ScanOnPush: scanOnPush }),
102
+ ...(lifecycleRules.length > 0
103
+ ? { LifecyclePolicy: new ECRRepository_LifecyclePolicy({ LifecyclePolicyText: toLifecyclePolicyText(lifecycleRules) }) }
104
+ : {}),
105
+ ...(props.encryption
106
+ ? {
107
+ EncryptionConfiguration: new ECRRepository_EncryptionConfiguration({
108
+ EncryptionType: props.encryption.type ?? "AES256",
109
+ ...(props.encryption.kmsKeyId ? { KmsKey: props.encryption.kmsKeyId } : {}),
110
+ }),
111
+ }
112
+ : {}),
113
+ ...(props.repositoryPolicy ? { RepositoryPolicyText: props.repositoryPolicy } : {}),
114
+ }, defaults?.repository));
115
+
116
+ return { repository };
117
+ }, "EcrRepository");
@@ -32,6 +32,20 @@ import {
32
32
  import { Sub, Join, Select, Split } from "../intrinsics";
33
33
  import { ecsTrustPolicy } from "./ecs-trust-policy";
34
34
 
35
+ /**
36
+ * The ALB `ListenerRule`'s real `Priority` bounds (documented on
37
+ * `elasticloadbalancing:CreateRule`, and enforced the same way for a
38
+ * CloudFormation-managed rule) — not an opinion this composite holds.
39
+ *
40
+ * Exported for the same reason as `MICROVM_LIMITS` (#1374, #1420): a
41
+ * consumer routing to the same shared ALB through another control plane
42
+ * needs the same numbers, and copying them is how two sources of truth
43
+ * start.
44
+ */
45
+ export const FARGATE_SERVICE_LIMITS = {
46
+ priority: { min: 1, max: 50000 },
47
+ } as const;
48
+
35
49
  export interface FargateServiceProps {
36
50
  // Wiring to shared ALB.
37
51
  //
@@ -44,7 +58,7 @@ export interface FargateServiceProps {
44
58
  albSecurityGroupId: Value<string>;
45
59
  executionRoleArn: Value<string>;
46
60
 
47
- // Routing — at least one required
61
+ // Routing — at least one required. Bounds: {@link FARGATE_SERVICE_LIMITS.priority}.
48
62
  priority: number;
49
63
  pathPatterns?: string[];
50
64
  hostHeaders?: string[];
@@ -99,8 +113,10 @@ export const FargateService = Composite((props: FargateServiceProps) => {
99
113
  if (!props.pathPatterns && !props.hostHeaders) {
100
114
  throw new Error("FargateService requires at least one of pathPatterns or hostHeaders");
101
115
  }
102
- if (props.priority < 1 || props.priority > 50000) {
103
- throw new Error("FargateService priority must be between 1 and 50000");
116
+ if (props.priority < FARGATE_SERVICE_LIMITS.priority.min || props.priority > FARGATE_SERVICE_LIMITS.priority.max) {
117
+ throw new Error(
118
+ `FargateService priority must be between ${FARGATE_SERVICE_LIMITS.priority.min} and ${FARGATE_SERVICE_LIMITS.priority.max}`,
119
+ );
104
120
  }
105
121
 
106
122
  const containerPort = props.containerPort ?? 80;
@@ -17,6 +17,15 @@ export { LambdaEventBridge } from "./lambda-eventbridge";
17
17
  export type { LambdaEventBridgeProps } from "./lambda-eventbridge";
18
18
  export { LambdaDynamoDB } from "./lambda-dynamodb";
19
19
  export type { LambdaDynamoDBProps } from "./lambda-dynamodb";
20
+ export { EcrRepository } from "./ecr-repository";
21
+ export type { EcrRepositoryProps, EcrLifecycleRule, EcrEncryption } from "./ecr-repository";
22
+ export { DynamoDBTable } from "./dynamodb-table";
23
+ export type {
24
+ DynamoDBTableProps,
25
+ DynamoDBKey,
26
+ DynamoDBCapacity,
27
+ DynamoDBGlobalSecondaryIndex,
28
+ } from "./dynamodb-table";
20
29
  export { LambdaS3 } from "./lambda-s3";
21
30
  export type { LambdaS3Props } from "./lambda-s3";
22
31
  export { LambdaSns } from "./lambda-sns";
@@ -27,8 +36,10 @@ export { FargateAlb } from "./fargate-alb";
27
36
  export type { FargateAlbProps } from "./fargate-alb";
28
37
  export { AlbShared } from "./alb-shared";
29
38
  export type { AlbSharedProps } from "./alb-shared";
30
- export { FargateService } from "./fargate-service";
39
+ export { FargateService, FARGATE_SERVICE_LIMITS } from "./fargate-service";
31
40
  export type { FargateServiceProps } from "./fargate-service";
41
+ export { NlbService } from "./nlb-service";
42
+ export type { NlbServiceProps } from "./nlb-service";
32
43
  export { RdsInstance, RdsInstance as RdsPostgres } from "./rds-instance";
33
44
  export type { RdsInstanceProps, RdsInstanceProps as RdsPostgresProps } from "./rds-instance";
34
45
  export { EfsWithAccessPoint } from "./efs-with-access-point";
@@ -37,11 +48,19 @@ export { SolrFargateService } from "./solr-fargate-service";
37
48
  export type { SolrFargateServiceProps } from "./solr-fargate-service";
38
49
  export { Ec2InstanceRole } from "./ec2-instance-role";
39
50
  export type { Ec2InstanceRoleProps } from "./ec2-instance-role";
51
+ export { Ec2InstanceBundle } from "./ec2-instance-bundle";
52
+ export type { Ec2InstanceBundleProps, Ec2InstanceBundleIngressRule } from "./ec2-instance-bundle";
40
53
  export { MinimalVpc } from "./minimal-vpc";
41
54
  export type { MinimalVpcProps } from "./minimal-vpc";
42
55
  export { EksCluster } from "./eks-cluster";
43
56
  export type { EksClusterProps } from "./eks-cluster";
44
57
  export { MicrovmApp, MICROVM_LIMITS } from "./microvm-app";
45
58
  export type { MicrovmAppProps, MicrovmAppResult, MicrovmAppBuildConnectorProps, MicrovmMemoryMiB } from "./microvm-app";
46
- export { AgentCoreAgent, agentCoreDefaultEndpointArn } from "./agentcore-agent";
59
+ export { AgentCoreAgent, agentCoreDefaultEndpointArn, AGENTCORE_LIMITS } from "./agentcore-agent";
47
60
  export type { AgentCoreAgentProps, AgentCoreAgentResult } from "./agentcore-agent";
61
+ export { StepFunctionsWorkflow } from "./step-functions-workflow";
62
+ export type { StepFunctionsWorkflowProps } from "./step-functions-workflow";
63
+ export { MonitoringStack } from "./monitoring-stack";
64
+ export type { MonitoringStackProps, MonitoringMetricSpec } from "./monitoring-stack";
65
+ export { BucketDeployment } from "./bucket-deployment";
66
+ export type { BucketDeploymentProps, BucketDeploymentResult } from "./bucket-deployment";