@intentius/chant-lexicon-aws 0.46.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 (114) 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/composites/agentcore-agent.d.ts +26 -1
  4. package/dist/composites/agentcore-agent.d.ts.map +1 -1
  5. package/dist/composites/bucket-deployment.d.ts +82 -0
  6. package/dist/composites/bucket-deployment.d.ts.map +1 -0
  7. package/dist/composites/dynamodb-table.d.ts +45 -0
  8. package/dist/composites/dynamodb-table.d.ts.map +1 -0
  9. package/dist/composites/ec2-instance-bundle.d.ts +61 -0
  10. package/dist/composites/ec2-instance-bundle.d.ts.map +1 -0
  11. package/dist/composites/ecr-repository.d.ts +49 -0
  12. package/dist/composites/ecr-repository.d.ts.map +1 -0
  13. package/dist/composites/fargate-service.d.ts +16 -0
  14. package/dist/composites/fargate-service.d.ts.map +1 -1
  15. package/dist/composites/index.d.ts +16 -2
  16. package/dist/composites/index.d.ts.map +1 -1
  17. package/dist/composites/monitoring-stack.d.ts +99 -0
  18. package/dist/composites/monitoring-stack.d.ts.map +1 -0
  19. package/dist/composites/nlb-service.d.ts +48 -0
  20. package/dist/composites/nlb-service.d.ts.map +1 -0
  21. package/dist/composites/step-functions-workflow.d.ts +36 -0
  22. package/dist/composites/step-functions-workflow.d.ts.map +1 -0
  23. package/dist/deep-observe.d.ts +45 -0
  24. package/dist/deep-observe.d.ts.map +1 -1
  25. package/dist/deep-topology.d.ts +104 -0
  26. package/dist/deep-topology.d.ts.map +1 -0
  27. package/dist/effect-receipt-row.d.ts +92 -0
  28. package/dist/effect-receipt-row.d.ts.map +1 -0
  29. package/dist/index.d.ts +6 -2
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/integrity.json +7 -4
  32. package/dist/lint/audit-catalog.d.ts.map +1 -1
  33. package/dist/lint/post-synth/cf-refs.d.ts +17 -0
  34. package/dist/lint/post-synth/cf-refs.d.ts.map +1 -1
  35. package/dist/lint/post-synth/index.d.ts.map +1 -1
  36. package/dist/lint/post-synth/waw061.d.ts +19 -0
  37. package/dist/lint/post-synth/waw061.d.ts.map +1 -0
  38. package/dist/lint/post-synth/waw062.d.ts +19 -0
  39. package/dist/lint/post-synth/waw062.d.ts.map +1 -0
  40. package/dist/lint/post-synth/waw063.d.ts +35 -0
  41. package/dist/lint/post-synth/waw063.d.ts.map +1 -0
  42. package/dist/manifest.json +1 -1
  43. package/dist/okf/index.md +3 -0
  44. package/dist/okf/rules/WAW061.md +17 -0
  45. package/dist/okf/rules/WAW062.md +27 -0
  46. package/dist/okf/rules/WAW063.md +19 -0
  47. package/dist/okf/types/Action.md +1 -0
  48. package/dist/okf/types/AutoScalingGroup.md +1 -0
  49. package/dist/okf/types/Bucket.md +1 -0
  50. package/dist/okf/types/ECRRepository.md +1 -0
  51. package/dist/okf/types/Function.md +1 -0
  52. package/dist/okf/types/IamPolicy.md +1 -0
  53. package/dist/okf/types/LaunchTemplate.md +1 -0
  54. package/dist/okf/types/LoadBalancer.md +1 -0
  55. package/dist/okf/types/ManagedPolicy.md +2 -0
  56. package/dist/okf/types/Map.md +1 -0
  57. package/dist/okf/types/Role.md +2 -0
  58. package/dist/okf/types/SecurityGroup.md +1 -0
  59. package/dist/okf/types/StateMachine.md +4 -0
  60. package/dist/okf/types/Subnet.md +4 -0
  61. package/dist/okf/types/Table.md +1 -0
  62. package/dist/okf/types/Type.md +3 -0
  63. package/dist/okf/types/Vpc.md +1 -0
  64. package/dist/op/activities/index.d.ts +6 -0
  65. package/dist/op/activities/index.d.ts.map +1 -1
  66. package/dist/plugin.d.ts.map +1 -1
  67. package/dist/receipt-store.d.ts +109 -0
  68. package/dist/receipt-store.d.ts.map +1 -0
  69. package/dist/rules/cf-refs.ts +40 -0
  70. package/dist/rules/waw061.ts +67 -0
  71. package/dist/rules/waw062.ts +0 -0
  72. package/dist/rules/waw063.ts +0 -0
  73. package/dist/serializer.d.ts.map +1 -1
  74. package/dist/skills/chant-aws-carve-terraform.md +26 -23
  75. package/package.json +3 -3
  76. package/src/api/read-client.test.ts +9 -0
  77. package/src/api/read-client.ts +15 -2
  78. package/src/composites/agentcore-agent.ts +35 -13
  79. package/src/composites/bucket-deployment.test.ts +84 -0
  80. package/src/composites/bucket-deployment.ts +153 -0
  81. package/src/composites/composites.test.ts +325 -0
  82. package/src/composites/dynamodb-table.test.ts +154 -0
  83. package/src/composites/dynamodb-table.ts +124 -0
  84. package/src/composites/ec2-instance-bundle.ts +134 -0
  85. package/src/composites/ecr-repository.test.ts +118 -0
  86. package/src/composites/ecr-repository.ts +117 -0
  87. package/src/composites/fargate-service.ts +19 -3
  88. package/src/composites/index.ts +21 -2
  89. package/src/composites/monitoring-stack.test.ts +130 -0
  90. package/src/composites/monitoring-stack.ts +164 -0
  91. package/src/composites/nlb-service.ts +117 -0
  92. package/src/composites/step-functions-workflow.ts +127 -0
  93. package/src/deep-observe.test.ts +201 -1
  94. package/src/deep-observe.ts +204 -0
  95. package/src/deep-topology.test.ts +392 -0
  96. package/src/deep-topology.ts +237 -0
  97. package/src/effect-receipt-row.test.ts +164 -0
  98. package/src/effect-receipt-row.ts +147 -0
  99. package/src/index.ts +28 -7
  100. package/src/lint/audit-catalog.ts +6 -0
  101. package/src/lint/post-synth/cf-refs.ts +40 -0
  102. package/src/lint/post-synth/index.ts +6 -0
  103. package/src/lint/post-synth/waw061.test.ts +99 -0
  104. package/src/lint/post-synth/waw061.ts +67 -0
  105. package/src/lint/post-synth/waw062.test.ts +109 -0
  106. package/src/lint/post-synth/waw062.ts +0 -0
  107. package/src/lint/post-synth/waw063.test.ts +220 -0
  108. package/src/lint/post-synth/waw063.ts +0 -0
  109. package/src/op/activities/index.ts +15 -0
  110. package/src/plugin.ts +41 -10
  111. package/src/receipt-store.test.ts +204 -0
  112. package/src/receipt-store.ts +315 -0
  113. package/src/serializer.ts +105 -2
  114. package/src/skills/chant-aws-carve-terraform.md +26 -23
@@ -0,0 +1,84 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { BucketDeployment } from "./bucket-deployment";
3
+
4
+ describe("BucketDeployment", () => {
5
+ test("private-by-default: encrypted, public access fully blocked, retained on delete, no policy member", () => {
6
+ const c = BucketDeployment({});
7
+ expect(c.bucket).toBeDefined();
8
+ expect((c as unknown as Record<string, unknown>).bucketPolicy).toBeUndefined();
9
+
10
+ const props = (c.bucket as any).props;
11
+ expect(props.BucketEncryption.props.ServerSideEncryptionConfiguration).toHaveLength(1);
12
+ expect(props.PublicAccessBlockConfiguration.props).toEqual({
13
+ BlockPublicAcls: true,
14
+ BlockPublicPolicy: true,
15
+ IgnorePublicAcls: true,
16
+ RestrictPublicBuckets: true,
17
+ });
18
+ expect(props.VersioningConfiguration).toBeUndefined();
19
+ expect(props.WebsiteConfiguration).toBeUndefined();
20
+
21
+ const attrs = (c.bucket as any).attributes;
22
+ expect(attrs.DeletionPolicy).toBe("Retain");
23
+ });
24
+
25
+ test("bucketName threads through, versioning enables Status: Enabled", () => {
26
+ const c = BucketDeployment({ bucketName: "my-content-bucket", versioned: true });
27
+ const props = (c.bucket as any).props;
28
+ expect(props.BucketName).toBe("my-content-bucket");
29
+ expect(props.VersioningConfiguration.props).toEqual({ Status: "Enabled" });
30
+ });
31
+
32
+ test("removalPolicy: destroy maps to DeletionPolicy: Delete", () => {
33
+ const c = BucketDeployment({ removalPolicy: "destroy" });
34
+ expect((c.bucket as any).attributes.DeletionPolicy).toBe("Delete");
35
+ });
36
+
37
+ test("website shape: opens the public-access block, sets WebsiteConfiguration, and attaches a scoped public-read bucketPolicy", () => {
38
+ const c = BucketDeployment({
39
+ website: { indexDocument: "index.html", errorDocument: "error.html" },
40
+ }) as unknown as { bucket: any; bucketPolicy: any };
41
+
42
+ const bucketProps = c.bucket.props;
43
+ expect(bucketProps.PublicAccessBlockConfiguration.props).toEqual({
44
+ BlockPublicAcls: false,
45
+ BlockPublicPolicy: false,
46
+ IgnorePublicAcls: false,
47
+ RestrictPublicBuckets: false,
48
+ });
49
+ expect(bucketProps.WebsiteConfiguration.props).toEqual({
50
+ IndexDocument: "index.html",
51
+ ErrorDocument: "error.html",
52
+ });
53
+
54
+ expect(c.bucketPolicy).toBeDefined();
55
+ expect(c.bucketPolicy.props.Bucket).toBe(c.bucket); // bucket.Ref is the resource instance itself
56
+ const statement = c.bucketPolicy.props.PolicyDocument.Statement[0];
57
+ expect(statement).toEqual({ Effect: "Allow", Principal: "*", Action: "s3:GetObject", Resource: expect.anything() });
58
+ });
59
+
60
+ test("website without errorDocument omits it rather than writing undefined", () => {
61
+ const c = BucketDeployment({ website: { indexDocument: "index.html" } });
62
+ const websiteProps = (c.bucket as any).props.WebsiteConfiguration.props;
63
+ expect(websiteProps).toEqual({ IndexDocument: "index.html" });
64
+ expect("ErrorDocument" in websiteProps).toBe(false);
65
+ });
66
+
67
+ test("tags pass straight through to the bucket", () => {
68
+ const c = BucketDeployment({ tags: [{ Key: "env", Value: "prod" }] });
69
+ expect((c.bucket as any).props.Tags).toEqual([{ Key: "env", Value: "prod" }]);
70
+ });
71
+
72
+ test("defaults escape hatch reaches the bucket (e.g. object-lock)", () => {
73
+ const c = BucketDeployment({ defaults: { bucket: { ObjectLockEnabled: true } } });
74
+ expect((c.bucket as any).props.ObjectLockEnabled).toBe(true);
75
+ });
76
+
77
+ test("defaults escape hatch reaches the bucket policy", () => {
78
+ const c = BucketDeployment({
79
+ website: { indexDocument: "index.html" },
80
+ defaults: { bucketPolicy: { Bucket: "explicit-bucket-name" } },
81
+ }) as unknown as { bucketPolicy: any };
82
+ expect(c.bucketPolicy.props.Bucket).toBe("explicit-bucket-name");
83
+ });
84
+ });
@@ -0,0 +1,153 @@
1
+ /**
2
+ * BucketDeployment — the seeded-bucket shape: an S3 bucket sized to receive
3
+ * deploy-time content, encrypted and access-blocked by default, its
4
+ * deletion behaviour and (optionally) its public-read policy declared up
5
+ * front rather than left to CloudFormation's own defaults.
6
+ *
7
+ * `s3deploy.BucketDeployment` is the most-used CDK pattern across the
8
+ * aws-bench corpus (37 instantiations across 8 apps, chant#1139) and it
9
+ * names two things CDK bundles together that chant keeps apart on purpose:
10
+ *
11
+ * - The bucket itself: `AWS::S3::Bucket` plus its encryption, access-block
12
+ * and (for the static-site shape) website/public-policy configuration.
13
+ * That is a fixed set of CloudFormation resources, so it is what this
14
+ * composite declares — the same shape `LambdaS3` (../composites) already
15
+ * gives a bucket, minus the Lambda trigger wiring.
16
+ * - Uploading local files into it at deploy time. CDK does this with a
17
+ * Lambda-backed custom resource (`cr.AwsCustomResource`, the same family
18
+ * the epic notes maps to chant Ops rather than composites — chant has no
19
+ * CloudFormation custom-resource machinery, and inventing one just to
20
+ * reproduce a `PutObject` loop would be new plumbing for a solved
21
+ * problem). chant already ships this as a deploy-time **capability**,
22
+ * `s3-sync` (`../components/apply.ts`'s `createS3SyncCapability`, backed
23
+ * by the real `aws s3 sync`/`aws s3 cp` CLI through `CloudExecutor`, with
24
+ * the typed step-builder `s3Sync` in `../components/builders.ts`) — it
25
+ * was defined for exactly this and, before this composite, had no
26
+ * composite pairing it with a bucket to sync into. A component composes
27
+ * the two:
28
+ *
29
+ * const seeded = BucketDeployment({ bucketName: Sub`${AWS.StackName}-site` });
30
+ * // ...component `deploy` phase:
31
+ * s3Sync({ from: "archive:site-build", to: Sub`s3://${seeded.bucket.Ref}` })
32
+ *
33
+ * That split — graph-time declaration here, deploy-time action in the
34
+ * component layer — is the existing precedent (`s3-sync` shipped in #566
35
+ * unused by any composite until this one), not a new one invented for this
36
+ * composite.
37
+ *
38
+ * "Cleanup" (the epic's third word) is `removalPolicy`, CloudFormation's own
39
+ * `DeletionPolicy` on the bucket resource. CDK's `autoDeleteObjects` — the
40
+ * Lambda-backed hook that empties a bucket before CloudFormation is allowed
41
+ * to delete it — has the same custom-resource shape as content upload and
42
+ * the same answer: out of scope here, and worth an `s3 rm --recursive`
43
+ * step ahead of a stack teardown in the component that owns the deploy.
44
+ */
45
+ import { Composite, mergeDefaults, type Value } from "@intentius/chant";
46
+ import {
47
+ Bucket,
48
+ Bucket_BucketEncryption,
49
+ Bucket_ServerSideEncryptionRule,
50
+ Bucket_ServerSideEncryptionByDefault,
51
+ Bucket_PublicAccessBlockConfiguration,
52
+ Bucket_VersioningConfiguration,
53
+ Bucket_WebsiteConfiguration,
54
+ S3BucketPolicy,
55
+ } from "../generated";
56
+ import { Sub } from "../intrinsics";
57
+
58
+ export interface BucketDeploymentProps {
59
+ /** `Value<string>`: a name is routinely built with `Sub`/`Ref` (#1366). Omitted lets CloudFormation generate one. */
60
+ bucketName?: Value<string>;
61
+ /** Enable S3 object versioning. Default: false. */
62
+ versioned?: boolean;
63
+ /**
64
+ * Static-website hosting: sets `WebsiteConfiguration` and — since a
65
+ * website bucket is read by anonymous visitors, not IAM principals —
66
+ * opens the public-access block and attaches a public-read
67
+ * `S3BucketPolicy` scoped to `s3:GetObject`. Omitted, the bucket stays
68
+ * fully private, matching every other composite bucket's default
69
+ * (`LambdaS3`, ../composites).
70
+ */
71
+ website?: {
72
+ indexDocument: string;
73
+ errorDocument?: string;
74
+ };
75
+ /** What CloudFormation does to the bucket when the stack (or the resource) is removed. Default: "retain" — CloudFormation's own default, and the safe one for a bucket a deploy has just put content into. */
76
+ removalPolicy?: "retain" | "destroy";
77
+ tags?: Array<{ Key: string; Value: string }>;
78
+ defaults?: {
79
+ bucket?: Partial<ConstructorParameters<typeof Bucket>[0]>;
80
+ bucketPolicy?: Partial<ConstructorParameters<typeof S3BucketPolicy>[0]>;
81
+ };
82
+ }
83
+
84
+ export type BucketDeploymentResult =
85
+ | { bucket: InstanceType<typeof Bucket> }
86
+ | { bucket: InstanceType<typeof Bucket>; bucketPolicy: InstanceType<typeof S3BucketPolicy> };
87
+
88
+ export const BucketDeployment = Composite<BucketDeploymentProps>((props) => {
89
+ const { defaults } = props;
90
+ const publicRead = props.website !== undefined;
91
+
92
+ const encryptionDefault = new Bucket_ServerSideEncryptionByDefault({ SSEAlgorithm: "AES256" });
93
+ const encryptionRule = new Bucket_ServerSideEncryptionRule({ ServerSideEncryptionByDefault: encryptionDefault });
94
+ const bucketEncryption = new Bucket_BucketEncryption({ ServerSideEncryptionConfiguration: [encryptionRule] });
95
+
96
+ // A ternary, not an `if`, so the block-scoped `new` stays an expression
97
+ // (EVL002) — the same reasoning eks-cluster.ts's addon slots document.
98
+ const publicAccessBlock = new Bucket_PublicAccessBlockConfiguration(
99
+ publicRead
100
+ ? { BlockPublicAcls: false, BlockPublicPolicy: false, IgnorePublicAcls: false, RestrictPublicBuckets: false }
101
+ : { BlockPublicAcls: true, BlockPublicPolicy: true, IgnorePublicAcls: true, RestrictPublicBuckets: true },
102
+ );
103
+
104
+ const versioningConfiguration = props.versioned
105
+ ? new Bucket_VersioningConfiguration({ Status: "Enabled" })
106
+ : undefined;
107
+
108
+ const websiteConfiguration = props.website
109
+ ? new Bucket_WebsiteConfiguration({
110
+ IndexDocument: props.website.indexDocument,
111
+ ...(props.website.errorDocument ? { ErrorDocument: props.website.errorDocument } : {}),
112
+ })
113
+ : undefined;
114
+
115
+ const bucket = new Bucket(
116
+ mergeDefaults(
117
+ {
118
+ BucketName: props.bucketName,
119
+ BucketEncryption: bucketEncryption,
120
+ PublicAccessBlockConfiguration: publicAccessBlock,
121
+ ...(versioningConfiguration ? { VersioningConfiguration: versioningConfiguration } : {}),
122
+ ...(websiteConfiguration ? { WebsiteConfiguration: websiteConfiguration } : {}),
123
+ Tags: props.tags,
124
+ },
125
+ defaults?.bucket,
126
+ ),
127
+ { DeletionPolicy: props.removalPolicy === "destroy" ? "Delete" : "Retain" },
128
+ );
129
+
130
+ const bucketPolicy = publicRead
131
+ ? new S3BucketPolicy(
132
+ mergeDefaults(
133
+ {
134
+ Bucket: bucket.Ref,
135
+ PolicyDocument: {
136
+ Version: "2012-10-17",
137
+ Statement: [
138
+ { Effect: "Allow", Principal: "*", Action: "s3:GetObject", Resource: Sub`${bucket.Arn}/*` },
139
+ ],
140
+ },
141
+ },
142
+ defaults?.bucketPolicy,
143
+ ),
144
+ )
145
+ : undefined;
146
+
147
+ // Spread rather than a ternary between two object shapes (EksCluster's
148
+ // addon slots, ../eks-cluster.ts, hit the same union-return typing wall):
149
+ // a key that is sometimes present, never a key whose value is sometimes
150
+ // `undefined` — `CompositeMembers`' index signature requires `Declarable`,
151
+ // not `Declarable | undefined`.
152
+ return { bucket, ...(bucketPolicy ? { bucketPolicy } : {}) };
153
+ }, "BucketDeployment");
@@ -13,9 +13,12 @@ import { VpcDefault } from "./vpc-default";
13
13
  import { FargateAlb } from "./fargate-alb";
14
14
  import { AlbShared } from "./alb-shared";
15
15
  import { FargateService } from "./fargate-service";
16
+ import { NlbService } from "./nlb-service";
16
17
  import { RdsInstance } from "./rds-instance";
17
18
  import { Ec2InstanceRole } from "./ec2-instance-role";
19
+ import { Ec2InstanceBundle } from "./ec2-instance-bundle";
18
20
  import { MinimalVpc } from "./minimal-vpc";
21
+ import { StepFunctionsWorkflow } from "./step-functions-workflow";
19
22
 
20
23
  const baseProps = {
21
24
  name: "TestFunc",
@@ -639,6 +642,112 @@ describe("FargateService", () => {
639
642
  });
640
643
  });
641
644
 
645
+ describe("NlbService", () => {
646
+ const nlbProps = {
647
+ vpcId: "vpc-123",
648
+ subnetIds: ["subnet-pub1", "subnet-pub2"],
649
+ };
650
+
651
+ test("returns 3 members with correct names", () => {
652
+ const instance = NlbService(nlbProps);
653
+ const names = Object.keys(instance.members);
654
+ expect(names).toEqual(["nlb", "targetGroup", "listener"]);
655
+ });
656
+
657
+ test("expandComposite produces correct logical names", () => {
658
+ const expanded = expandComposite("app", NlbService(nlbProps));
659
+ expect(expanded.has("appNlb")).toBe(true);
660
+ expect(expanded.has("appTargetGroup")).toBe(true);
661
+ expect(expanded.has("appListener")).toBe(true);
662
+ expect(expanded.size).toBe(3);
663
+ });
664
+
665
+ test("NLB has Type network and given subnets", () => {
666
+ const instance = NlbService(nlbProps);
667
+ const nlbResourceProps = (instance.nlb as any).props;
668
+ expect(nlbResourceProps.Type).toBe("network");
669
+ expect(nlbResourceProps.Scheme).toBe("internet-facing");
670
+ expect(nlbResourceProps.Subnets).toEqual(["subnet-pub1", "subnet-pub2"]);
671
+ });
672
+
673
+ test("no security group is created", () => {
674
+ const instance = NlbService(nlbProps);
675
+ expect(Object.keys(instance.members)).not.toContain("albSg");
676
+ expect(Object.keys(instance.members)).not.toContain("nlbSg");
677
+ });
678
+
679
+ test("defaults: TCP protocol, port 80, ip target type", () => {
680
+ const instance = NlbService(nlbProps);
681
+ const tgProps = (instance.targetGroup as any).props;
682
+ expect(tgProps.Protocol).toBe("TCP");
683
+ expect(tgProps.Port).toBe(80);
684
+ expect(tgProps.TargetType).toBe("ip");
685
+ expect(tgProps.HealthCheckProtocol).toBe("TCP");
686
+
687
+ const listenerProps = (instance.listener as any).props;
688
+ expect(listenerProps.Protocol).toBe("TCP");
689
+ expect(listenerProps.Port).toBe(80);
690
+ });
691
+
692
+ test("targetPort defaults to listenerPort but can be overridden", () => {
693
+ const instance = NlbService({ ...nlbProps, listenerPort: 443, targetPort: 8443 });
694
+ const tgProps = (instance.targetGroup as any).props;
695
+ expect(tgProps.Port).toBe(8443);
696
+ const listenerProps = (instance.listener as any).props;
697
+ expect(listenerProps.Port).toBe(443);
698
+ });
699
+
700
+ test("listener forwards to the target group", () => {
701
+ const instance = NlbService(nlbProps);
702
+ const listenerProps = (instance.listener as any).props;
703
+ expect(listenerProps.DefaultActions).toHaveLength(1);
704
+ const action = (listenerProps.DefaultActions[0] as any).props;
705
+ expect(action.Type).toBe("forward");
706
+ });
707
+
708
+ test("static targets are registered on the target group", () => {
709
+ const instance = NlbService({
710
+ ...nlbProps,
711
+ targetType: "instance",
712
+ targets: [{ id: "i-0123456789abcdef0", port: 8080 }],
713
+ });
714
+ const tgProps = (instance.targetGroup as any).props;
715
+ expect(tgProps.TargetType).toBe("instance");
716
+ expect(tgProps.Targets).toHaveLength(1);
717
+ const target = (tgProps.Targets[0] as any).props;
718
+ expect(target.Id).toBe("i-0123456789abcdef0");
719
+ expect(target.Port).toBe(8080);
720
+ });
721
+
722
+ test("internal scheme is respected", () => {
723
+ const instance = NlbService({ ...nlbProps, scheme: "internal" });
724
+ const nlbResourceProps = (instance.nlb as any).props;
725
+ expect(nlbResourceProps.Scheme).toBe("internal");
726
+ });
727
+
728
+ test("TLS protocol adds certificate to listener", () => {
729
+ const instance = NlbService({
730
+ ...nlbProps,
731
+ protocol: "TLS",
732
+ certificateArn: "arn:aws:acm:us-east-1:123:certificate/abc",
733
+ });
734
+ const listenerProps = (instance.listener as any).props;
735
+ expect(listenerProps.Protocol).toBe("TLS");
736
+ expect(listenerProps.Certificates).toHaveLength(1);
737
+ const cert = (listenerProps.Certificates[0] as any).props;
738
+ expect(cert.CertificateArn).toBe("arn:aws:acm:us-east-1:123:certificate/abc");
739
+
740
+ const tgProps = (instance.targetGroup as any).props;
741
+ expect(tgProps.Protocol).toBe("TLS");
742
+ });
743
+
744
+ test("throws when protocol is TLS without a certificateArn", () => {
745
+ expect(() =>
746
+ NlbService({ ...nlbProps, protocol: "TLS" }),
747
+ ).toThrow("NlbService requires certificateArn when protocol is TLS");
748
+ });
749
+ });
750
+
642
751
  describe("RdsInstance", () => {
643
752
  const rdsProps = {
644
753
  vpcId: "vpc-123",
@@ -897,6 +1006,140 @@ describe("Ec2InstanceRole", () => {
897
1006
  });
898
1007
  });
899
1008
 
1009
+ describe("Ec2InstanceBundle", () => {
1010
+ const instanceProps = {
1011
+ imageId: "ami-0abcdef1234567890",
1012
+ vpcId: "vpc-123",
1013
+ subnetId: "subnet-456",
1014
+ };
1015
+
1016
+ test("returns role, instanceProfile, sg, instance members", () => {
1017
+ const instance = Ec2InstanceBundle(instanceProps);
1018
+ expect(Object.keys(instance.members)).toEqual(["role", "instanceProfile", "sg", "instance"]);
1019
+ });
1020
+
1021
+ test("expandComposite produces correct logical names", () => {
1022
+ const expanded = expandComposite("web", Ec2InstanceBundle(instanceProps));
1023
+ expect(expanded.has("webRole")).toBe(true);
1024
+ expect(expanded.has("webInstanceProfile")).toBe(true);
1025
+ expect(expanded.has("webSg")).toBe(true);
1026
+ expect(expanded.has("webInstance")).toBe(true);
1027
+ expect(expanded.size).toBe(4);
1028
+ });
1029
+
1030
+ test("role has EC2 trust policy", () => {
1031
+ const instance = Ec2InstanceBundle(instanceProps);
1032
+ const roleProps = (instance.role as any).props;
1033
+ expect(roleProps.AssumeRolePolicyDocument.Statement[0].Principal.Service).toBe("ec2.amazonaws.com");
1034
+ });
1035
+
1036
+ test("instanceProfile references the role", () => {
1037
+ const instance = Ec2InstanceBundle(instanceProps);
1038
+ const profileProps = (instance.instanceProfile as any).props;
1039
+ expect(profileProps.Roles).toHaveLength(1);
1040
+ });
1041
+
1042
+ test("ManagedPolicyArns and Policies pass through to the role", () => {
1043
+ const arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore";
1044
+ const instance = Ec2InstanceBundle({
1045
+ ...instanceProps,
1046
+ ManagedPolicyArns: [arn],
1047
+ Policies: [{ PolicyName: "extra", PolicyDocument: { Version: "2012-10-17", Statement: [] } }],
1048
+ });
1049
+ const roleProps = (instance.role as any).props;
1050
+ expect(roleProps.ManagedPolicyArns).toContain(arn);
1051
+ expect(roleProps.Policies).toHaveLength(1);
1052
+ });
1053
+
1054
+ test("instance wires ImageId, InstanceType default, subnet, SG, and instance profile", () => {
1055
+ const instance = Ec2InstanceBundle(instanceProps);
1056
+ const instanceProps_ = (instance.instance as any).props;
1057
+ expect(instanceProps_.ImageId).toBe("ami-0abcdef1234567890");
1058
+ expect(instanceProps_.InstanceType).toBe("t3.micro");
1059
+ expect(instanceProps_.SubnetId).toBe("subnet-456");
1060
+ expect(instanceProps_.SecurityGroupIds).toHaveLength(1);
1061
+ expect(instanceProps_.IamInstanceProfile).toBeDefined();
1062
+ });
1063
+
1064
+ test("custom instanceType is respected", () => {
1065
+ const instance = Ec2InstanceBundle({ ...instanceProps, instanceType: "m5.large" });
1066
+ expect((instance.instance as any).props.InstanceType).toBe("m5.large");
1067
+ });
1068
+
1069
+ test("keyName is passed through when provided", () => {
1070
+ const instance = Ec2InstanceBundle({ ...instanceProps, keyName: "my-key" });
1071
+ expect((instance.instance as any).props.KeyName).toBe("my-key");
1072
+ });
1073
+
1074
+ test("keyName is omitted by default", () => {
1075
+ const instance = Ec2InstanceBundle(instanceProps);
1076
+ expect((instance.instance as any).props.KeyName).toBeUndefined();
1077
+ });
1078
+
1079
+ test("userData is wrapped in Fn::Base64", () => {
1080
+ const instance = Ec2InstanceBundle({ ...instanceProps, userData: "#!/bin/bash\necho hi" });
1081
+ const userData = (instance.instance as any).props.UserData;
1082
+ expect(JSON.stringify(userData)).toContain("Fn::Base64");
1083
+ });
1084
+
1085
+ test("no ingress rules by default", () => {
1086
+ const instance = Ec2InstanceBundle(instanceProps);
1087
+ expect((instance.sg as any).props.SecurityGroupIngress).toBeUndefined();
1088
+ });
1089
+
1090
+ test("ingress with cidr produces a CidrIp rule", () => {
1091
+ const instance = Ec2InstanceBundle({
1092
+ ...instanceProps,
1093
+ ingress: [{ fromPort: 22, cidr: "10.0.0.0/16", description: "ssh" }],
1094
+ });
1095
+ const rules = (instance.sg as any).props.SecurityGroupIngress;
1096
+ expect(rules).toHaveLength(1);
1097
+ const rule = (rules[0] as any).props;
1098
+ expect(rule.CidrIp).toBe("10.0.0.0/16");
1099
+ expect(rule.FromPort).toBe(22);
1100
+ expect(rule.ToPort).toBe(22);
1101
+ expect(rule.IpProtocol).toBe("tcp");
1102
+ expect(rule.Description).toBe("ssh");
1103
+ });
1104
+
1105
+ test("ingress with sourceSecurityGroupId produces a SG-sourced rule", () => {
1106
+ const instance = Ec2InstanceBundle({
1107
+ ...instanceProps,
1108
+ ingress: [{ fromPort: 80, toPort: 8080, sourceSecurityGroupId: "sg-abc123" }],
1109
+ });
1110
+ const rule = ((instance.sg as any).props.SecurityGroupIngress[0] as any).props;
1111
+ expect(rule.SourceSecurityGroupId).toBe("sg-abc123");
1112
+ expect(rule.FromPort).toBe(80);
1113
+ expect(rule.ToPort).toBe(8080);
1114
+ });
1115
+
1116
+ test("ingress with cidrIpv6 produces an IPv6 rule", () => {
1117
+ const instance = Ec2InstanceBundle({
1118
+ ...instanceProps,
1119
+ ingress: [{ fromPort: 443, cidrIpv6: "::/0" }],
1120
+ });
1121
+ const rule = ((instance.sg as any).props.SecurityGroupIngress[0] as any).props;
1122
+ expect(rule.CidrIpv6).toBe("::/0");
1123
+ });
1124
+
1125
+ test("ingress rule with no source throws", () => {
1126
+ expect(() =>
1127
+ Ec2InstanceBundle({ ...instanceProps, ingress: [{ fromPort: 22 }] }),
1128
+ ).toThrow(/needs one of cidr, cidrIpv6, or sourceSecurityGroupId/);
1129
+ });
1130
+
1131
+ test("multiple ingress rules are all applied", () => {
1132
+ const instance = Ec2InstanceBundle({
1133
+ ...instanceProps,
1134
+ ingress: [
1135
+ { fromPort: 22, cidr: "10.0.0.0/16" },
1136
+ { fromPort: 443, cidr: "0.0.0.0/0" },
1137
+ ],
1138
+ });
1139
+ expect((instance.sg as any).props.SecurityGroupIngress).toHaveLength(2);
1140
+ });
1141
+ });
1142
+
900
1143
  describe("MinimalVpc", () => {
901
1144
  test("returns 8 members", () => {
902
1145
  const instance = MinimalVpc({});
@@ -930,3 +1173,85 @@ describe("MinimalVpc", () => {
930
1173
  expect(expanded.size).toBe(8);
931
1174
  });
932
1175
  });
1176
+
1177
+ describe("StepFunctionsWorkflow", () => {
1178
+ const passDefinition = { StartAt: "Hello", States: { Hello: { Type: "Pass", End: true } } };
1179
+
1180
+ test("returns role, logGroup, stateMachine members", () => {
1181
+ const instance = StepFunctionsWorkflow({ definition: passDefinition });
1182
+ expect(Object.keys(instance.members)).toEqual(["role", "logGroup", "stateMachine"]);
1183
+ });
1184
+
1185
+ test("role has states.amazonaws.com trust policy", () => {
1186
+ const instance = StepFunctionsWorkflow({ definition: passDefinition });
1187
+ const roleProps = (instance.role as any).props;
1188
+ expect(roleProps.AssumeRolePolicyDocument.Statement[0].Principal.Service).toBe("states.amazonaws.com");
1189
+ });
1190
+
1191
+ test("role always carries the log-delivery policy", () => {
1192
+ const instance = StepFunctionsWorkflow({ definition: passDefinition });
1193
+ const roleProps = (instance.role as any).props;
1194
+ const names = roleProps.Policies.map((p: any) => p.props.PolicyName);
1195
+ expect(names).toContain("StepFunctionsLogDelivery");
1196
+ });
1197
+
1198
+ test("lambdaFunctionArns grants InvokeFunction on those ARNs", () => {
1199
+ const instance = StepFunctionsWorkflow({
1200
+ definition: passDefinition,
1201
+ lambdaFunctionArns: ["arn:aws:lambda:us-east-1:123456789012:function:worker"],
1202
+ });
1203
+ const roleProps = (instance.role as any).props;
1204
+ const invokePolicy = roleProps.Policies.find((p: any) => p.props.PolicyName === "InvokeLambdaTasks");
1205
+ expect(invokePolicy).toBeDefined();
1206
+ expect(invokePolicy.props.PolicyDocument.Statement[0].Resource).toEqual([
1207
+ "arn:aws:lambda:us-east-1:123456789012:function:worker",
1208
+ ]);
1209
+ });
1210
+
1211
+ test("without lambdaFunctionArns, no InvokeLambdaTasks policy", () => {
1212
+ const instance = StepFunctionsWorkflow({ definition: passDefinition });
1213
+ const roleProps = (instance.role as any).props;
1214
+ const names = roleProps.Policies.map((p: any) => p.props.PolicyName);
1215
+ expect(names).not.toContain("InvokeLambdaTasks");
1216
+ });
1217
+
1218
+ test("stateMachine carries the definition object as Definition (not a string)", () => {
1219
+ const instance = StepFunctionsWorkflow({ definition: passDefinition });
1220
+ const smProps = (instance.stateMachine as any).props;
1221
+ expect(smProps.Definition).toEqual(passDefinition);
1222
+ expect(smProps.DefinitionString).toBeUndefined();
1223
+ });
1224
+
1225
+ test("stateMachine references role.Arn and defaults to STANDARD", () => {
1226
+ const instance = StepFunctionsWorkflow({ definition: passDefinition });
1227
+ const smProps = (instance.stateMachine as any).props;
1228
+ expect(smProps.RoleArn).toBeInstanceOf(AttrRef);
1229
+ expect(smProps.StateMachineType).toBe("STANDARD");
1230
+ });
1231
+
1232
+ test("logGroup defaults to 14-day retention, overridable", () => {
1233
+ const defaultInstance = StepFunctionsWorkflow({ definition: passDefinition });
1234
+ expect((defaultInstance.logGroup as any).props.RetentionInDays).toBe(14);
1235
+
1236
+ const overridden = StepFunctionsWorkflow({ definition: passDefinition, logRetentionDays: 30 });
1237
+ expect((overridden.logGroup as any).props.RetentionInDays).toBe(30);
1238
+ });
1239
+
1240
+ test("LoggingConfiguration wires the log group and defaults level to ALL", () => {
1241
+ const instance = StepFunctionsWorkflow({ definition: passDefinition });
1242
+ const smProps = (instance.stateMachine as any).props;
1243
+ const loggingProps = (smProps.LoggingConfiguration as any).props;
1244
+ expect(loggingProps.Level).toBe("ALL");
1245
+ const destProps = (loggingProps.Destinations[0] as any).props;
1246
+ const cwProps = (destProps.CloudWatchLogsLogGroup as any).props;
1247
+ expect(cwProps.LogGroupArn).toBeInstanceOf(AttrRef);
1248
+ });
1249
+
1250
+ test("expandComposite produces 3 entries", () => {
1251
+ const expanded = expandComposite("workflow", StepFunctionsWorkflow({ definition: passDefinition }));
1252
+ expect(expanded.has("workflowRole")).toBe(true);
1253
+ expect(expanded.has("workflowLogGroup")).toBe(true);
1254
+ expect(expanded.has("workflowStateMachine")).toBe(true);
1255
+ expect(expanded.size).toBe(3);
1256
+ });
1257
+ });