@intentius/chant-lexicon-aws 0.37.2 → 0.40.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 (95) hide show
  1. package/dist/ambient.d.ts.map +1 -1
  2. package/dist/api/read-client.d.ts.map +1 -1
  3. package/dist/codegen/generate.d.ts.map +1 -1
  4. package/dist/codegen/naming.d.ts +18 -1
  5. package/dist/codegen/naming.d.ts.map +1 -1
  6. package/dist/composites/alb-shared.d.ts +7 -1
  7. package/dist/composites/alb-shared.d.ts.map +1 -1
  8. package/dist/composites/ec2-instance-role.d.ts +4 -1
  9. package/dist/composites/ec2-instance-role.d.ts.map +1 -1
  10. package/dist/composites/efs-with-access-point.d.ts +5 -1
  11. package/dist/composites/efs-with-access-point.d.ts.map +1 -1
  12. package/dist/composites/fargate-alb.d.ts +14 -1
  13. package/dist/composites/fargate-alb.d.ts.map +1 -1
  14. package/dist/composites/fargate-service.d.ts +19 -8
  15. package/dist/composites/fargate-service.d.ts.map +1 -1
  16. package/dist/composites/index.d.ts +2 -2
  17. package/dist/composites/index.d.ts.map +1 -1
  18. package/dist/composites/lambda-api.d.ts +3 -1
  19. package/dist/composites/lambda-api.d.ts.map +1 -1
  20. package/dist/composites/lambda-dynamodb.d.ts +9 -2
  21. package/dist/composites/lambda-dynamodb.d.ts.map +1 -1
  22. package/dist/composites/lambda-eventbridge.d.ts +9 -2
  23. package/dist/composites/lambda-eventbridge.d.ts.map +1 -1
  24. package/dist/composites/lambda-s3.d.ts +3 -1
  25. package/dist/composites/lambda-s3.d.ts.map +1 -1
  26. package/dist/composites/lambda-sns.d.ts +10 -2
  27. package/dist/composites/lambda-sns.d.ts.map +1 -1
  28. package/dist/composites/lambda-sqs.d.ts +9 -2
  29. package/dist/composites/lambda-sqs.d.ts.map +1 -1
  30. package/dist/composites/microvm-app.d.ts +25 -4
  31. package/dist/composites/microvm-app.d.ts.map +1 -1
  32. package/dist/composites/minimal-vpc.d.ts +10 -1
  33. package/dist/composites/minimal-vpc.d.ts.map +1 -1
  34. package/dist/composites/rds-instance.d.ts +1 -1
  35. package/dist/composites/rds-instance.d.ts.map +1 -1
  36. package/dist/composites/scheduled-lambda.d.ts +15 -3
  37. package/dist/composites/scheduled-lambda.d.ts.map +1 -1
  38. package/dist/composites/solr-fargate-service.d.ts +27 -1
  39. package/dist/composites/solr-fargate-service.d.ts.map +1 -1
  40. package/dist/generated/index.d.ts +57 -49
  41. package/dist/generated/index.d.ts.map +1 -1
  42. package/dist/import/generator.d.ts.map +1 -1
  43. package/dist/index.d.ts +2 -2
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/integrity.json +4 -4
  46. package/dist/manifest.json +1 -1
  47. package/dist/meta.json +722 -666
  48. package/dist/op/activities/floci.d.ts.map +1 -1
  49. package/dist/plugin.d.ts.map +1 -1
  50. package/dist/reference-catalog.d.ts.map +1 -1
  51. package/dist/spec/pin.d.ts +116 -0
  52. package/dist/spec/pin.d.ts.map +1 -0
  53. package/dist/spec/pinned-types.json +1652 -0
  54. package/dist/types/index.d.ts +679 -555
  55. package/dist/validate.d.ts.map +1 -1
  56. package/package.json +2 -2
  57. package/src/ambient.test.ts +21 -0
  58. package/src/ambient.ts +16 -0
  59. package/src/api/read-client.test.ts +31 -0
  60. package/src/api/read-client.ts +43 -1
  61. package/src/codegen/generate.ts +17 -3
  62. package/src/codegen/naming.ts +31 -2
  63. package/src/composites/alb-shared.ts +1 -1
  64. package/src/composites/ec2-instance-role.ts +1 -1
  65. package/src/composites/efs-with-access-point.ts +1 -1
  66. package/src/composites/fargate-alb.ts +1 -1
  67. package/src/composites/fargate-service.ts +15 -10
  68. package/src/composites/index.ts +2 -2
  69. package/src/composites/lambda-api.ts +3 -2
  70. package/src/composites/lambda-dynamodb.ts +4 -3
  71. package/src/composites/lambda-eventbridge.ts +4 -3
  72. package/src/composites/lambda-s3.ts +3 -2
  73. package/src/composites/lambda-sns.ts +4 -3
  74. package/src/composites/lambda-sqs.ts +4 -3
  75. package/src/composites/microvm-app.test.ts +39 -1
  76. package/src/composites/microvm-app.ts +32 -11
  77. package/src/composites/minimal-vpc.ts +1 -1
  78. package/src/composites/rds-instance.ts +1 -1
  79. package/src/composites/scheduled-lambda.ts +4 -3
  80. package/src/composites/solr-fargate-service.ts +7 -1
  81. package/src/generated/index.d.ts +679 -555
  82. package/src/generated/index.ts +58 -50
  83. package/src/generated/lexicon-aws.json +722 -666
  84. package/src/import/generator.ts +5 -4
  85. package/src/index.ts +2 -2
  86. package/src/lifecycle-integration.test.ts +25 -0
  87. package/src/op/activities/floci.test.ts +2 -2
  88. package/src/op/activities/floci.ts +6 -1
  89. package/src/plugin.ts +4 -0
  90. package/src/reference-catalog.test.ts +28 -0
  91. package/src/reference-catalog.ts +7 -0
  92. package/src/spec/pin.test.ts +248 -0
  93. package/src/spec/pin.ts +247 -0
  94. package/src/spec/pinned-types.json +1652 -0
  95. package/src/validate.ts +5 -0
@@ -1 +1 @@
1
- {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAA4B,KAAK,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAElG,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAIvF;;;;GAIG;AACH,wBAAsB,QAAQ,CAAC,IAAI,CAAC,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,cAAc,CAAC,CAapF"}
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAA4B,KAAK,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAElG,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAIvF;;;;GAIG;AACH,wBAAsB,QAAQ,CAAC,IAAI,CAAC,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,cAAc,CAAC,CAkBpF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-aws",
3
- "version": "0.37.2",
3
+ "version": "0.40.0",
4
4
  "description": "AWS CloudFormation lexicon for chant — declarative IaC in TypeScript",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -80,7 +80,7 @@
80
80
  "typescript": "^5.9.3"
81
81
  },
82
82
  "peerDependencies": {
83
- "@intentius/chant": "^0.37.2",
83
+ "@intentius/chant": "^0.39.0",
84
84
  "typescript": "^5.9.3"
85
85
  }
86
86
  }
@@ -42,6 +42,27 @@ describe("observeAwsAmbient (#1278)", () => {
42
42
  expect(found["sg-default"].attributes).toMatchObject({ GroupName: "default", VpcId: "vpc-1" });
43
43
  });
44
44
 
45
+ // The empty ones are the answer, and they were the ones being dropped: both
46
+ // other observers resolve outward from declarations, so a subnet is recorded
47
+ // only when something in it is.
48
+ it("reports subnets nothing occupies", async () => {
49
+ spawnMock.mockResolvedValue(
50
+ ok({
51
+ Subnets: [
52
+ { SubnetId: "subnet-used", VpcId: "vpc-1" },
53
+ { SubnetId: "subnet-empty-a", VpcId: "vpc-default" },
54
+ { SubnetId: "subnet-empty-b", VpcId: "vpc-default" },
55
+ ],
56
+ }),
57
+ );
58
+ const found = await observeAwsAmbient({
59
+ kinds: ["AWS::EC2::Subnet"],
60
+ observed: { public: { type: "AWS::EC2::Subnet", status: "OK", physicalId: "subnet-used" } },
61
+ });
62
+ expect(Object.keys(found).sort()).toEqual(["subnet-empty-a", "subnet-empty-b"]);
63
+ expect(found["subnet-empty-a"].attributes).toMatchObject({ VpcId: "vpc-default" });
64
+ });
65
+
45
66
  it("is bounded by the kinds the project declares", async () => {
46
67
  // A project managing security groups is not made to enumerate the account.
47
68
  const found = await observeAwsAmbient({ kinds: ["AWS::S3::Bucket"], observed: {} });
package/src/ambient.ts CHANGED
@@ -44,6 +44,22 @@ const ENUMERABLE: Record<string, { argv: string[]; key: string; id: string; name
44
44
  key: "NetworkInterfaces",
45
45
  id: "NetworkInterfaceId",
46
46
  },
47
+ // A subnet holding nothing is the same shape as an unused security group, and
48
+ // it was reachable by neither of the other paths. Both resolve outward from
49
+ // what is declared, so a subnet gets recorded only when something in it does —
50
+ // and the empty ones are exactly what "which of my subnets are empty" asks
51
+ // for.
52
+ //
53
+ // Found by measuring a snapshot rather than by reading the code: an estate
54
+ // with seven subnets recorded five, and the two it dropped were the two with
55
+ // nothing in them. The default VPC's subnets are the archetype again — nothing
56
+ // declares them, and the only one ever recorded was the one that happened to
57
+ // hold an instance.
58
+ "AWS::EC2::Subnet": {
59
+ argv: ["ec2", "describe-subnets"],
60
+ key: "Subnets",
61
+ id: "SubnetId",
62
+ },
47
63
  };
48
64
 
49
65
  /**
@@ -201,3 +201,34 @@ describe("Cloud Control", () => {
201
201
  expect(parseResourceDescription({ Properties: JSON.stringify(["not", "an", "object"]) })).toBeNull();
202
202
  });
203
203
  });
204
+
205
+ describe("the region an endpoint override cannot carry in its host", () => {
206
+ // An override is one host for every region, so the hostname says nothing and
207
+ // the region has to travel in the request. It did not, and a stack declared
208
+ // `region: "us-west-1"` was read against the emulator's default region —
209
+ // where it genuinely does not exist, so the read came back empty and the
210
+ // snapshot recorded that region as holding nothing.
211
+ test("the request names the region in its credential scope", async () => {
212
+ const { http, calls } = recording(() => respond(stackXml));
213
+ await describeStackResources("web", { endpoint: "http://localhost:4566", region: "us-west-1", http });
214
+ expect(calls[0].headers.authorization).toContain("/us-west-1/cloudformation/aws4_request");
215
+ });
216
+
217
+ test("Cloud Control carries it too, scoped to its own service", async () => {
218
+ const { http, calls } = recording(() => respond(JSON.stringify({ ResourceDescription: {} })));
219
+ await getResource("AWS::EC2::VPC", "vpc-01", { endpoint: "http://localhost:4566", region: "eu-west-1", http });
220
+ expect(calls[0].headers.authorization).toContain("/eu-west-1/cloudcontrolapi/aws4_request");
221
+ });
222
+
223
+ test("no region, no header — nothing invents one", async () => {
224
+ const { http, calls } = recording(() => respond(stackXml));
225
+ await describeStackResources("web", { endpoint: "http://localhost:4566", http });
226
+ expect(calls[0].headers.authorization).toBeUndefined();
227
+ });
228
+
229
+ test("it is a scope, not a signature — the placeholder says so", async () => {
230
+ const { http, calls } = recording(() => respond(stackXml));
231
+ await describeStackResources("web", { region: "us-west-1", http });
232
+ expect(calls[0].headers.authorization).toContain("Signature=unsigned");
233
+ });
234
+ });
@@ -69,6 +69,37 @@ export function serviceUrl(service: string, endpoint?: string, region = DEFAULT_
69
69
  return `${(endpoint ?? `https://${service}.${region}.amazonaws.com`).replace(/\/$/, "")}/`;
70
70
  }
71
71
 
72
+ /**
73
+ * How the request says which region it is for.
74
+ *
75
+ * Against real AWS the region is the hostname, which is why `serviceUrl` takes
76
+ * it. Against an endpoint override there is only one host for every region, so
77
+ * the hostname carries nothing and the region has to travel in the request —
78
+ * and every emulator reads it where a real SDK puts it, in the credential scope
79
+ * of the `Authorization` header.
80
+ *
81
+ * Without this, a stack observed with `region: "us-west-1"` was read against
82
+ * whatever region the emulator defaults to. The result is not an error: the
83
+ * stack genuinely does not exist in us-east-1, so `describeResources` returned
84
+ * an empty observation and the snapshot recorded the region as holding nothing.
85
+ * A three-region estate snapshotted as one region and nothing said so.
86
+ *
87
+ * This is NOT SigV4. The signature is a placeholder and real AWS rejects it —
88
+ * as it already rejects every request from this module, which is unsigned by
89
+ * design (see the header comment). It carries the scope, nothing more, and it
90
+ * must not be mistaken for the signed read path that would replace it.
91
+ */
92
+ function regionScope(service: string, region?: string): Record<string, string> {
93
+ if (!region) return {};
94
+ const day = new Date().toISOString().slice(0, 10).replace(/-/g, "");
95
+ const key = process.env.AWS_ACCESS_KEY_ID || "chant";
96
+ return {
97
+ authorization:
98
+ `AWS4-HMAC-SHA256 Credential=${key}/${day}/${region}/${service}/aws4_request, ` +
99
+ "SignedHeaders=host, Signature=unsigned",
100
+ };
101
+ }
102
+
72
103
  /* ── CloudFormation Query ─────────────────────────────────────────────────── */
73
104
 
74
105
  /**
@@ -128,7 +159,17 @@ export async function cfnQuery(
128
159
  const http = options.http ?? defaultHttp;
129
160
  const url = serviceUrl("cloudformation", options.endpoint, options.region);
130
161
  const body = new URLSearchParams({ Action: action, Version: CFN_API_VERSION, ...params }).toString();
131
- const res = await http(url, { headers: { "content-type": "application/x-www-form-urlencoded" }, body }, options.signal);
162
+ const res = await http(
163
+ url,
164
+ {
165
+ headers: {
166
+ "content-type": "application/x-www-form-urlencoded",
167
+ ...regionScope("cloudformation", options.region),
168
+ },
169
+ body,
170
+ },
171
+ options.signal,
172
+ );
132
173
  const err = xmlError(res.text);
133
174
  if (err || res.status >= 400) {
134
175
  throw new AwsReadError(
@@ -203,6 +244,7 @@ async function cloudControl(
203
244
  headers: {
204
245
  "content-type": "application/x-amz-json-1.0",
205
246
  "x-amz-target": `${CLOUD_CONTROL_TARGET_PREFIX}.${operation}`,
247
+ ...regionScope("cloudcontrolapi", options.region),
206
248
  },
207
249
  body: JSON.stringify(payload),
208
250
  },
@@ -14,12 +14,13 @@ import {
14
14
  type AugmentResult,
15
15
  } from "@intentius/chant/codegen/generate";
16
16
  import { fetchSchemaZip } from "../spec/fetch";
17
+ import { assertPinnedSpec } from "../spec/pin";
17
18
  import { parseCFNSchema, cfnShortName, type SchemaParseResult } from "../spec/parse";
18
19
  import { fetchCfnLintPatches, applyPatches } from "./patches";
19
20
  import { fetchCfnLintExtensions, loadExtensionSchemas, type ExtensionConstraint } from "./extensions";
20
21
  import { samResources } from "./sam";
21
22
  import { fallbackResources } from "./fallback";
22
- import { NamingStrategy, propertyTypeName, extractDefName } from "./naming";
23
+ import { NamingStrategy, publishedNames, propertyTypeName, extractDefName } from "./naming";
23
24
  import { generateLexiconJSON, lambdaRuntimeDeprecations } from "./generate-lexicon";
24
25
  import { generateTypeScriptDeclarations } from "./generate-typescript";
25
26
  import {
@@ -32,10 +33,17 @@ export type { GenerateOptions, GenerateResult };
32
33
 
33
34
  // AWS-specific state shared between pipeline callbacks
34
35
  let awsConstraints = new Map<string, ExtensionConstraint[]>();
36
+ /** chant #1459 — spec type → already-published TS name, reset per `generate()` call. */
37
+ let awsReservedNames: Record<string, string> = {};
35
38
 
36
39
  const awsPipelineConfig: GeneratePipelineConfig<SchemaParseResult> = {
37
40
  fetchSchemas: async (opts) => {
38
- return fetchSchemaZip(opts.force);
41
+ const schemas = await fetchSchemaZip(opts.force);
42
+ // #1390 — the registry schema has no version in its URL and is republished
43
+ // constantly, so without this any prepack regenerates against whatever
44
+ // CloudFormation shipped that morning.
45
+ assertPinnedSpec(schemas);
46
+ return schemas;
39
47
  },
40
48
 
41
49
  parseSchema: (_typeName, data) => {
@@ -44,7 +52,7 @@ const awsPipelineConfig: GeneratePipelineConfig<SchemaParseResult> = {
44
52
  return result;
45
53
  },
46
54
 
47
- createNaming: (results) => new NamingStrategy(results),
55
+ createNaming: (results) => new NamingStrategy(results, awsReservedNames),
48
56
 
49
57
  augmentSchemas: async (schemas, opts, log) => {
50
58
  const warnings: Array<{ file: string; error: string }> = [];
@@ -131,6 +139,12 @@ const awsPipelineConfig: GeneratePipelineConfig<SchemaParseResult> = {
131
139
  export async function generate(opts: GenerateOptions = {}): Promise<GenerateResult> {
132
140
  // Reset shared state
133
141
  awsConstraints = new Map();
142
+ // chant #1459 — names already published keep pointing at the types that
143
+ // published them. Skipped for a caller-supplied schema set, exactly as
144
+ // `augmentSchemas` is (see the pipeline's `opts.schemaSource` guard): a
145
+ // fixture run must depend on its fixtures, not on what the lexicon last
146
+ // shipped.
147
+ awsReservedNames = opts.schemaSource ? {} : publishedNames();
134
148
  return generatePipeline(awsPipelineConfig, opts);
135
149
  }
136
150
 
@@ -5,8 +5,11 @@
5
5
  * for extracting names from AWS CloudFormation types are defined here.
6
6
  */
7
7
 
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { join } from "node:path";
8
10
  import {
9
11
  NamingStrategy as CoreNamingStrategy,
12
+ reservedNamesFromSnapshot,
10
13
  type NamingConfig,
11
14
  type NamingInput,
12
15
  } from "@intentius/chant/codegen/naming";
@@ -110,11 +113,37 @@ const awsNamingConfig: NamingConfig = {
110
113
  * AWS-specific NamingStrategy — wraps the core algorithm with AWS data tables.
111
114
  */
112
115
  export class NamingStrategy extends CoreNamingStrategy {
113
- constructor(results: SchemaParseResult[]) {
116
+ /**
117
+ * @param reservedNames chant #1459 — spec type → already-published TS name,
118
+ * normally {@link publishedNames}. Passed in rather than read here on
119
+ * purpose: a fixture-driven codegen run (`./snapshot.test.ts`) must not
120
+ * inherit the repo's real 1600-entry surface, or its output would depend on
121
+ * whatever the lexicon last shipped instead of on its fixtures.
122
+ */
123
+ constructor(results: SchemaParseResult[], reservedNames: Record<string, string> = {}) {
114
124
  const inputs: NamingInput[] = results.map((r) => ({
115
125
  typeName: r.resource.typeName,
116
126
  propertyTypes: r.propertyTypes,
117
127
  }));
118
- super(inputs, awsNamingConfig);
128
+ super(inputs, { ...awsNamingConfig, reservedNames });
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Reserved names from the committed surface snapshot, or none when it cannot
134
+ * be read.
135
+ *
136
+ * Defensive: codegen must still run for a lexicon with no snapshot yet, and a
137
+ * missing or malformed snapshot should degrade to the previous naming
138
+ * behaviour rather than fail generation outright. `surface-diff` makes the
139
+ * consequence visible either way.
140
+ */
141
+ export function publishedNames(): Record<string, string> {
142
+ try {
143
+ const snapshotPath = join(import.meta.dirname, "..", "..", "surface.snapshot.json");
144
+ if (!existsSync(snapshotPath)) return {};
145
+ return reservedNamesFromSnapshot(JSON.parse(readFileSync(snapshotPath, "utf-8")));
146
+ } catch {
147
+ return {};
119
148
  }
120
149
  }
@@ -29,7 +29,7 @@ export interface AlbSharedProps {
29
29
  };
30
30
  }
31
31
 
32
- export const AlbShared = Composite<AlbSharedProps>((props) => {
32
+ export const AlbShared = Composite((props: AlbSharedProps) => {
33
33
  const listenerPort = props.listenerPort ?? 80;
34
34
  const protocol = props.protocol ?? "HTTP";
35
35
  const { defaults: defs } = props;
@@ -22,7 +22,7 @@ const EC2_ASSUME_ROLE = {
22
22
  ],
23
23
  };
24
24
 
25
- export const Ec2InstanceRole = Composite<Ec2InstanceRoleProps>((props) => {
25
+ export const Ec2InstanceRole = Composite((props: Ec2InstanceRoleProps) => {
26
26
  const { defaults } = props;
27
27
 
28
28
  const role = new Role(mergeDefaults({
@@ -28,7 +28,7 @@ export interface EfsWithAccessPointProps {
28
28
  throughputMode?: "bursting" | "provisioned" | "elastic";
29
29
  }
30
30
 
31
- export const EfsWithAccessPoint = Composite<EfsWithAccessPointProps>((props) => {
31
+ export const EfsWithAccessPoint = Composite((props: EfsWithAccessPointProps) => {
32
32
  // Ternary (not push-inside-if) keeps the `new`s out of control flow (EVL002).
33
33
  const ingressRules: InstanceType<typeof SecurityGroup_Ingress>[] = props.sourceSecurityGroupId
34
34
  ? [new SecurityGroup_Ingress({ IpProtocol: "tcp", FromPort: 2049, ToPort: 2049, SourceSecurityGroupId: props.sourceSecurityGroupId })]
@@ -68,7 +68,7 @@ export interface FargateAlbProps {
68
68
  };
69
69
  }
70
70
 
71
- export const FargateAlb = Composite<FargateAlbProps>((props) => {
71
+ export const FargateAlb = Composite((props: FargateAlbProps) => {
72
72
  const containerPort = props.containerPort ?? 80;
73
73
  const cpu = props.cpu ?? "256";
74
74
  const memory = props.memory ?? "512";
@@ -1,4 +1,4 @@
1
- import { Composite, mergeDefaults } from "@intentius/chant";
1
+ import { Composite, mergeDefaults, type Value } from "@intentius/chant";
2
2
  import {
3
3
  EcsService,
4
4
  EcsService_LoadBalancer,
@@ -33,11 +33,16 @@ import { Sub, Join, Select, Split } from "../intrinsics";
33
33
  import { ecsTrustPolicy } from "./ecs-trust-policy";
34
34
 
35
35
  export interface FargateServiceProps {
36
- // Wiring to shared ALB
37
- clusterArn: string;
38
- listenerArn: string;
39
- albSecurityGroupId: string;
40
- executionRoleArn: string;
36
+ // Wiring to shared ALB.
37
+ //
38
+ // `Value<string>` rather than `string`: these are exactly the props a
39
+ // consuming stack fills from a cross-stack reference, and the documented way
40
+ // to do that is `Ref(param)` — an Intrinsic, not a literal. Typing them
41
+ // `string` made the composite's own examples a type error (#1366).
42
+ clusterArn: Value<string>;
43
+ listenerArn: Value<string>;
44
+ albSecurityGroupId: Value<string>;
45
+ executionRoleArn: Value<string>;
41
46
 
42
47
  // Routing — at least one required
43
48
  priority: number;
@@ -45,7 +50,7 @@ export interface FargateServiceProps {
45
50
  hostHeaders?: string[];
46
51
 
47
52
  // Container
48
- image: string;
53
+ image: Value<string>;
49
54
  containerPort?: number;
50
55
  cpu?: string;
51
56
  memory?: string;
@@ -71,8 +76,8 @@ export interface FargateServiceProps {
71
76
  }>;
72
77
 
73
78
  // Networking
74
- vpcId: string;
75
- privateSubnetIds: string[];
79
+ vpcId: Value<string>;
80
+ privateSubnetIds: Array<Value<string>>;
76
81
  healthCheckPath?: string;
77
82
 
78
83
  // Ulimits (container-level)
@@ -90,7 +95,7 @@ export interface FargateServiceProps {
90
95
  };
91
96
  }
92
97
 
93
- export const FargateService = Composite<FargateServiceProps>((props) => {
98
+ export const FargateService = Composite((props: FargateServiceProps) => {
94
99
  if (!props.pathPatterns && !props.hostHeaders) {
95
100
  throw new Error("FargateService requires at least one of pathPatterns or hostHeaders");
96
101
  }
@@ -32,7 +32,7 @@ export { Ec2InstanceRole } from "./ec2-instance-role";
32
32
  export type { Ec2InstanceRoleProps } from "./ec2-instance-role";
33
33
  export { MinimalVpc } from "./minimal-vpc";
34
34
  export type { MinimalVpcProps } from "./minimal-vpc";
35
- export { MicrovmApp } from "./microvm-app";
36
- export type { MicrovmAppProps, MicrovmAppResult, MicrovmAppBuildConnectorProps } from "./microvm-app";
35
+ export { MicrovmApp, MICROVM_LIMITS } from "./microvm-app";
36
+ export type { MicrovmAppProps, MicrovmAppResult, MicrovmAppBuildConnectorProps, MicrovmMemoryMiB } from "./microvm-app";
37
37
  export { AgentCoreAgent } from "./agentcore-agent";
38
38
  export type { AgentCoreAgentProps, AgentCoreAgentResult } from "./agentcore-agent";
@@ -1,9 +1,10 @@
1
- import { Composite, mergeDefaults } from "@intentius/chant";
1
+ import { Composite, mergeDefaults, type Value } from "@intentius/chant";
2
2
  import { Permission } from "../generated";
3
3
  import { LambdaFunction, type LambdaFunctionProps, type LambdaFunctionResult } from "./lambda-function";
4
4
 
5
5
  export interface LambdaApiProps extends LambdaFunctionProps {
6
- sourceArn?: string;
6
+ /** `Value<string>`: an ARN is routinely built with `Sub` from pseudo-parameters (#1366). */
7
+ sourceArn?: Value<string>;
7
8
  defaults?: LambdaFunctionProps["defaults"] & {
8
9
  permission?: Partial<ConstructorParameters<typeof Permission>[0]>;
9
10
  };
@@ -1,4 +1,4 @@
1
- import { Composite, mergeDefaults } from "@intentius/chant";
1
+ import { Composite, mergeDefaults, type Value } from "@intentius/chant";
2
2
  import {
3
3
  Table,
4
4
  Table_AttributeDefinition,
@@ -11,7 +11,8 @@ import { dynamoDBActionsFor } from "../actions/dynamodb";
11
11
  import { LambdaFunction, type LambdaFunctionProps } from "./lambda-function";
12
12
 
13
13
  export interface LambdaDynamoDBProps extends LambdaFunctionProps {
14
- tableName?: string;
14
+ /** `Value<string>`: a name is routinely built with `Sub`/`Ref` (#1366). */
15
+ tableName?: Value<string>;
15
16
  partitionKey: string;
16
17
  sortKey?: string;
17
18
  access?: "ReadOnly" | "ReadWrite" | "Full" | "None";
@@ -27,7 +28,7 @@ export interface LambdaDynamoDBProps extends LambdaFunctionProps {
27
28
  };
28
29
  }
29
30
 
30
- export const LambdaDynamoDB = Composite<LambdaDynamoDBProps>((props) => {
31
+ export const LambdaDynamoDB = Composite((props: LambdaDynamoDBProps) => {
31
32
  // Conditional entries via spread keep the `new`s out of the `if` (EVL002).
32
33
  const attributeDefinitions = [
33
34
  new Table_AttributeDefinition({ AttributeName: props.partitionKey, AttributeType: "S" }),
@@ -1,9 +1,10 @@
1
- import { Composite, mergeDefaults } from "@intentius/chant";
1
+ import { Composite, mergeDefaults, type Value } from "@intentius/chant";
2
2
  import { EventRule, EventRule_Target, Permission } from "../generated";
3
3
  import { LambdaFunction, type LambdaFunctionProps } from "./lambda-function";
4
4
 
5
5
  export interface LambdaEventBridgeProps extends LambdaFunctionProps {
6
- ruleName?: string;
6
+ /** `Value<string>`: a name is routinely built with `Sub`/`Ref` (#1366). */
7
+ ruleName?: Value<string>;
7
8
  schedule?: string;
8
9
  eventPattern?: Record<string, unknown>;
9
10
  enabled?: boolean;
@@ -13,7 +14,7 @@ export interface LambdaEventBridgeProps extends LambdaFunctionProps {
13
14
  };
14
15
  }
15
16
 
16
- export const LambdaEventBridge = Composite<LambdaEventBridgeProps>((props) => {
17
+ export const LambdaEventBridge = Composite((props: LambdaEventBridgeProps) => {
17
18
  const { defaults } = props;
18
19
  const { role, func } = LambdaFunction(props);
19
20
 
@@ -1,4 +1,4 @@
1
- import { Composite, mergeDefaults } from "@intentius/chant";
1
+ import { Composite, mergeDefaults, type Value } from "@intentius/chant";
2
2
  import {
3
3
  Bucket,
4
4
  Bucket_BucketEncryption,
@@ -15,7 +15,8 @@ import { s3ActionsFor } from "../actions/s3";
15
15
  import { LambdaFunction, type LambdaFunctionProps, type LambdaFunctionResult } from "./lambda-function";
16
16
 
17
17
  export interface LambdaS3Props extends LambdaFunctionProps {
18
- bucketName?: string;
18
+ /** `Value<string>`: a name is routinely built with `Sub`/`Ref` (#1366). */
19
+ bucketName?: Value<string>;
19
20
  access?: "ReadOnly" | "ReadWrite" | "Full";
20
21
  trigger?: {
21
22
  events?: string[];
@@ -1,9 +1,10 @@
1
- import { Composite, mergeDefaults } from "@intentius/chant";
1
+ import { Composite, mergeDefaults, type Value } from "@intentius/chant";
2
2
  import { Topic, Subscription, Permission } from "../generated";
3
3
  import { LambdaFunction, type LambdaFunctionProps } from "./lambda-function";
4
4
 
5
5
  export interface LambdaSnsProps extends LambdaFunctionProps {
6
- topicName?: string;
6
+ /** `Value<string>`: a name is routinely built with `Sub`/`Ref` (#1366). */
7
+ topicName?: Value<string>;
7
8
  defaults?: LambdaFunctionProps["defaults"] & {
8
9
  topic?: Partial<ConstructorParameters<typeof Topic>[0]>;
9
10
  subscription?: Partial<ConstructorParameters<typeof Subscription>[0]>;
@@ -11,7 +12,7 @@ export interface LambdaSnsProps extends LambdaFunctionProps {
11
12
  };
12
13
  }
13
14
 
14
- export const LambdaSns = Composite<LambdaSnsProps>((props) => {
15
+ export const LambdaSns = Composite((props: LambdaSnsProps) => {
15
16
  const { defaults } = props;
16
17
  const { role, func } = LambdaFunction(props);
17
18
 
@@ -1,10 +1,11 @@
1
- import { Composite, mergeDefaults } from "@intentius/chant";
1
+ import { Composite, mergeDefaults, type Value } from "@intentius/chant";
2
2
  import { Queue, EventSourceMapping, Role_Policy } from "../generated";
3
3
  import { SQSActions } from "../actions/sqs";
4
4
  import { LambdaFunction, type LambdaFunctionProps } from "./lambda-function";
5
5
 
6
6
  export interface LambdaSqsProps extends LambdaFunctionProps {
7
- queueName?: string;
7
+ /** `Value<string>`: a name is routinely built with `Sub`/`Ref` (#1366). */
8
+ queueName?: Value<string>;
8
9
  batchSize?: number;
9
10
  maxBatchingWindow?: number;
10
11
  defaults?: LambdaFunctionProps["defaults"] & {
@@ -13,7 +14,7 @@ export interface LambdaSqsProps extends LambdaFunctionProps {
13
14
  };
14
15
  }
15
16
 
16
- export const LambdaSqs = Composite<LambdaSqsProps>((props) => {
17
+ export const LambdaSqs = Composite((props: LambdaSqsProps) => {
17
18
  const { defaults } = props;
18
19
 
19
20
  const queue = new Queue(mergeDefaults({
@@ -3,7 +3,7 @@ import { expandComposite } from "@intentius/chant";
3
3
  import { AttrRef } from "@intentius/chant/attrref";
4
4
  import { resolveAttrRefs } from "@intentius/chant/discovery/resolve";
5
5
  import { awsSerializer } from "../serializer";
6
- import { MicrovmApp } from "./microvm-app";
6
+ import { MicrovmApp, MICROVM_LIMITS } from "./microvm-app";
7
7
 
8
8
  const baseProps = {
9
9
  name: "worker-image",
@@ -232,3 +232,41 @@ describe("MicrovmApp", () => {
232
232
  });
233
233
  });
234
234
  });
235
+
236
+ describe("MICROVM_LIMITS", () => {
237
+ // The composite validates against these; a consumer driving the same service
238
+ // through a different control plane needs the same numbers, and copying them
239
+ // is how two sources of truth start (#1374).
240
+ test("is reachable from the package root", async () => {
241
+ const root = await import("../index");
242
+ expect(root.MICROVM_LIMITS).toBe(MICROVM_LIMITS);
243
+ });
244
+
245
+ test("is what the composite actually enforces", () => {
246
+ // Not a restatement of the constants — a probe through the public API, so
247
+ // the two cannot drift apart while both look right.
248
+ expect(() => MicrovmApp({ ...baseProps, memoryMiB: 3072 as never })).toThrow(
249
+ new RegExp(MICROVM_LIMITS.memoryMiB.join(", ")),
250
+ );
251
+ expect(() => MicrovmApp({ ...baseProps, name: "no spaces allowed" })).toThrow(/name must match/);
252
+ expect(() =>
253
+ MicrovmApp({ ...baseProps, name: "x".repeat(MICROVM_LIMITS.maxNameLength + 1) }),
254
+ ).toThrow(/≤64 chars/);
255
+ expect(() =>
256
+ MicrovmApp({ ...baseProps, environment: { AWS_REGION: "us-east-1" } }),
257
+ ).toThrow(/AWS_REGION/);
258
+ });
259
+
260
+ test("names every limit the composite checks", () => {
261
+ // A limit enforced and not named here is one a consumer cannot see.
262
+ expect(Object.keys(MICROVM_LIMITS).sort()).toEqual([
263
+ "connectorSubnets",
264
+ "maxEgressConnectors",
265
+ "maxEnvironmentVariables",
266
+ "maxNameLength",
267
+ "memoryMiB",
268
+ "namePattern",
269
+ "reservedEnvironmentKeys",
270
+ ]);
271
+ });
272
+ });
@@ -47,17 +47,38 @@ import { Sub } from "../intrinsics";
47
47
  const MICROVM_SERVICE_PRINCIPAL = "lambda.amazonaws.com";
48
48
  const CONNECTOR_MANAGED_RESOURCE_OPERATOR = "network-connectors.lambda.amazonaws.com";
49
49
 
50
- /** Documented baseline memory tiers (MiB) — the CFN schema types it as an open int, but the service accepts only these five; vCPU auto-scales with the tier. */
51
- const VALID_MEMORY_MIB = [512, 1024, 2048, 4096, 8192] as const;
52
- type MicrovmMemoryMiB = (typeof VALID_MEMORY_MIB)[number];
53
-
54
- const NAME_PATTERN = /^[a-zA-Z0-9-_]+$/;
55
- const MAX_NAME_LENGTH = 64;
56
- const RESERVED_ENV_KEYS = new Set(["AWS_REGION"]);
57
- const MAX_ENVIRONMENT_VARIABLES = 50;
58
- const MAX_EGRESS_CONNECTORS = 10;
59
- const MIN_CONNECTOR_SUBNETS = 1;
60
- const MAX_CONNECTOR_SUBNETS = 16;
50
+ /**
51
+ * The Lambda MicroVMs service's real limits, verified against the upstream
52
+ * `ran-isenberg/lambda-microvm-cdk-python` construct and the AWS docs.
53
+ *
54
+ * Exported because a consumer driving the same service through a different
55
+ * control plane needs the same numbers, and copying them is how two sources of
56
+ * truth start (#1374). The CFN schema types most of these as open ints and
57
+ * strings, so nothing but this object knows them.
58
+ */
59
+ export const MICROVM_LIMITS = {
60
+ /** Documented baseline memory tiers (MiB). The schema says int; the service accepts five values. vCPU auto-scales with the tier. */
61
+ memoryMiB: [512, 1024, 2048, 4096, 8192],
62
+ namePattern: /^[a-zA-Z0-9-_]+$/,
63
+ maxNameLength: 64,
64
+ /** Set by the service on every MicroVM; supplying it is rejected. */
65
+ reservedEnvironmentKeys: ["AWS_REGION"],
66
+ maxEnvironmentVariables: 50,
67
+ maxEgressConnectors: 10,
68
+ connectorSubnets: { min: 1, max: 16 },
69
+ } as const;
70
+
71
+ /** One of the five memory tiers {@link MICROVM_LIMITS} names. */
72
+ export type MicrovmMemoryMiB = (typeof MICROVM_LIMITS.memoryMiB)[number];
73
+
74
+ const VALID_MEMORY_MIB = MICROVM_LIMITS.memoryMiB;
75
+ const NAME_PATTERN = MICROVM_LIMITS.namePattern;
76
+ const MAX_NAME_LENGTH = MICROVM_LIMITS.maxNameLength;
77
+ const RESERVED_ENV_KEYS = new Set<string>(MICROVM_LIMITS.reservedEnvironmentKeys);
78
+ const MAX_ENVIRONMENT_VARIABLES = MICROVM_LIMITS.maxEnvironmentVariables;
79
+ const MAX_EGRESS_CONNECTORS = MICROVM_LIMITS.maxEgressConnectors;
80
+ const MIN_CONNECTOR_SUBNETS = MICROVM_LIMITS.connectorSubnets.min;
81
+ const MAX_CONNECTOR_SUBNETS = MICROVM_LIMITS.connectorSubnets.max;
61
82
 
62
83
  /**
63
84
  * `sts:AssumeRole` + `sts:TagSession` trust for `lambda.amazonaws.com` — the
@@ -21,7 +21,7 @@ export interface MinimalVpcProps {
21
21
  };
22
22
  }
23
23
 
24
- export const MinimalVpc = Composite<MinimalVpcProps>((props) => {
24
+ export const MinimalVpc = Composite((props: MinimalVpcProps) => {
25
25
  const { defaults } = props;
26
26
  const cidr = props.cidr ?? "10.0.0.0/24";
27
27
  const subnetCidr = props.subnetCidr ?? "10.0.0.0/25";
@@ -76,7 +76,7 @@ export interface RdsInstanceProps {
76
76
  };
77
77
  }
78
78
 
79
- export const RdsInstance = Composite<RdsInstanceProps>((props) => {
79
+ export const RdsInstance = Composite((props: RdsInstanceProps) => {
80
80
  const engine = props.engine ?? "postgres";
81
81
  const defaults = ENGINE_DEFAULTS.get(engine)!;
82
82
  const port = props.port ?? defaults.port;