@intentius/chant-lexicon-aws 0.0.5 → 0.0.8

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.
package/src/plugin.ts CHANGED
@@ -1,4 +1,6 @@
1
+ import { createRequire } from "module";
1
2
  import type { LexiconPlugin, IntrinsicDef, SkillDefinition } from "@intentius/chant/lexicon";
3
+ const require = createRequire(import.meta.url);
2
4
  import type { LintRule } from "@intentius/chant/lint/rule";
3
5
  import type { PostSynthCheck } from "@intentius/chant/lint/post-synth";
4
6
  import type { TemplateParser } from "@intentius/chant/import/parser";
@@ -54,68 +56,67 @@ export const awsPlugin: LexiconPlugin = {
54
56
 
55
57
  initTemplates(): Record<string, string> {
56
58
  return {
57
- "_.ts": `export * from "./config";\n`,
58
59
  "config.ts": `/**
59
60
  * Shared bucket configuration — encryption, versioning, public access
60
61
  */
61
62
 
62
- import * as aws from "@intentius/chant-lexicon-aws";
63
+ import { ServerSideEncryptionByDefault, ServerSideEncryptionRule, BucketEncryption, PublicAccessBlockConfiguration, VersioningConfiguration } from "@intentius/chant-lexicon-aws";
63
64
 
64
65
  // Encryption default — AES256 server-side encryption
65
- export const encryptionDefault = new aws.ServerSideEncryptionByDefault({
66
- sseAlgorithm: "AES256",
66
+ export const encryptionDefault = new ServerSideEncryptionByDefault({
67
+ SSEAlgorithm: "AES256",
67
68
  });
68
69
 
69
70
  // Encryption rule wrapping the default
70
- export const encryptionRule = new aws.ServerSideEncryptionRule({
71
- serverSideEncryptionByDefault: encryptionDefault,
71
+ export const encryptionRule = new ServerSideEncryptionRule({
72
+ ServerSideEncryptionByDefault: encryptionDefault,
72
73
  });
73
74
 
74
75
  // Bucket encryption configuration
75
- export const bucketEncryption = new aws.BucketEncryption({
76
- serverSideEncryptionConfiguration: [encryptionRule],
76
+ export const bucketEncryption = new BucketEncryption({
77
+ ServerSideEncryptionConfiguration: [encryptionRule],
77
78
  });
78
79
 
79
80
  // Public access block — deny all public access
80
- export const publicAccessBlock = new aws.PublicAccessBlockConfiguration({
81
- blockPublicAcls: true,
82
- blockPublicPolicy: true,
83
- ignorePublicAcls: true,
84
- restrictPublicBuckets: true,
81
+ export const publicAccessBlock = new PublicAccessBlockConfiguration({
82
+ BlockPublicAcls: true,
83
+ BlockPublicPolicy: true,
84
+ IgnorePublicAcls: true,
85
+ RestrictPublicBuckets: true,
85
86
  });
86
87
 
87
88
  // Versioning — enabled
88
- export const versioningEnabled = new aws.VersioningConfiguration({
89
- status: "Enabled",
89
+ export const versioningEnabled = new VersioningConfiguration({
90
+ Status: "Enabled",
90
91
  });
91
92
  `,
92
93
  "data-bucket.ts": `/**
93
94
  * Data bucket — primary storage with encryption and versioning
94
95
  */
95
96
 
96
- import * as aws from "@intentius/chant-lexicon-aws";
97
- import * as _ from "./_";
97
+ import { Bucket, Sub, AWS } from "@intentius/chant-lexicon-aws";
98
+ import { versioningEnabled, bucketEncryption, publicAccessBlock } from "./config";
98
99
 
99
- export const dataBucket = new aws.Bucket({
100
- bucketName: aws.Sub\`\${aws.AWS.StackName}-data\`,
101
- versioningConfiguration: _.versioningEnabled,
102
- bucketEncryption: _.bucketEncryption,
103
- publicAccessBlockConfiguration: _.publicAccessBlock,
100
+ export const dataBucket = new Bucket({
101
+ BucketName: Sub\`\${AWS.StackName}-\${AWS.AccountId}-data\`,
102
+ VersioningConfiguration: versioningEnabled,
103
+ BucketEncryption: bucketEncryption,
104
+ PublicAccessBlockConfiguration: publicAccessBlock,
104
105
  });
105
106
  `,
106
107
  "logs-bucket.ts": `/**
107
108
  * Logs bucket — log delivery with encryption and versioning
108
109
  */
109
110
 
110
- import * as aws from "@intentius/chant-lexicon-aws";
111
- import * as _ from "./_";
111
+ import { Bucket, Sub, AWS } from "@intentius/chant-lexicon-aws";
112
+ import { versioningEnabled, bucketEncryption, publicAccessBlock } from "./config";
112
113
 
113
- export const logsBucket = new aws.Bucket({
114
- bucketName: aws.Sub\`\${aws.AWS.StackName}-logs\`,
115
- accessControl: "LogDeliveryWrite",
116
- versioningConfiguration: _.versioningEnabled,
117
- bucketEncryption: _.bucketEncryption,
118
- publicAccessBlockConfiguration: _.publicAccessBlock,
114
+ export const logsBucket = new Bucket({
115
+ BucketName: Sub\`\${AWS.StackName}-\${AWS.AccountId}-logs\`,
116
+ AccessControl: "LogDeliveryWrite",
117
+ VersioningConfiguration: versioningEnabled,
118
+ BucketEncryption: bucketEncryption,
119
+ PublicAccessBlockConfiguration: publicAccessBlock,
119
120
  });
120
121
  `,
121
122
  };
@@ -185,18 +186,9 @@ export const logsBucket = new aws.Bucket({
185
186
 
186
187
  async validate(options?: { verbose?: boolean }): Promise<void> {
187
188
  const { validate } = await import("./validate");
189
+ const { printValidationResult } = await import("@intentius/chant/codegen/validate");
188
190
  const result = await validate();
189
-
190
- for (const check of result.checks) {
191
- const status = check.ok ? "OK" : "FAIL";
192
- const msg = check.error ? ` — ${check.error}` : "";
193
- console.error(` [${status}] ${check.name}${msg}`);
194
- }
195
-
196
- if (!result.success) {
197
- throw new Error("Validation failed");
198
- }
199
- console.error("All validation checks passed.");
191
+ printValidationResult(result);
200
192
  },
201
193
 
202
194
  async coverage(options?: { verbose?: boolean; minOverall?: number }): Promise<void> {
@@ -227,75 +219,29 @@ export const logsBucket = new aws.Bucket({
227
219
 
228
220
  async package(options?: { verbose?: boolean; force?: boolean }): Promise<void> {
229
221
  const { packageLexicon } = await import("./codegen/package");
230
- const { writeFileSync, mkdirSync } = await import("fs");
222
+ const { writeBundleSpec } = await import("@intentius/chant/codegen/package");
231
223
  const { join, dirname } = await import("path");
232
224
  const { fileURLToPath } = await import("url");
233
225
 
234
226
  const { spec, stats } = await packageLexicon({ verbose: options?.verbose, force: options?.force });
235
227
 
236
- // Write manifest and artifacts to dist/
237
228
  const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
238
229
  const distDir = join(pkgDir, "dist");
239
- mkdirSync(join(distDir, "types"), { recursive: true });
240
- mkdirSync(join(distDir, "rules"), { recursive: true });
241
- mkdirSync(join(distDir, "skills"), { recursive: true });
242
-
243
- writeFileSync(join(distDir, "manifest.json"), JSON.stringify(spec.manifest, null, 2));
244
- writeFileSync(join(distDir, "meta.json"), spec.registry);
245
- writeFileSync(join(distDir, "types", "index.d.ts"), spec.typesDTS);
246
-
247
- for (const [name, content] of spec.rules) {
248
- writeFileSync(join(distDir, "rules", name), content);
249
- }
250
- for (const [name, content] of spec.skills) {
251
- writeFileSync(join(distDir, "skills", name), content);
252
- }
253
-
254
- // Write integrity.json if available
255
- if (spec.integrity) {
256
- writeFileSync(join(distDir, "integrity.json"), JSON.stringify(spec.integrity, null, 2));
257
- }
230
+ writeBundleSpec(spec, distDir);
258
231
 
259
232
  console.error(`Packaged ${stats.resources} resources, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
260
233
 
261
- // Produce .tgz via bun pm pack
262
- const packProc = Bun.spawn(["bun", "pm", "pack"], {
263
- cwd: pkgDir,
264
- stdout: "pipe",
265
- stderr: "pipe",
266
- });
267
- const packOut = await new Response(packProc.stdout).text();
268
- const packErr = await new Response(packProc.stderr).text();
269
- const packExit = await packProc.exited;
234
+ // Produce .tgz via pack command
235
+ const { getRuntime } = await import("@intentius/chant/runtime-adapter");
236
+ const rt = getRuntime();
237
+ const { stdout: packOut, stderr: packErr, exitCode: packExit } = await rt.spawn(
238
+ rt.commands.packCmd,
239
+ { cwd: pkgDir },
240
+ );
270
241
  if (packExit === 0) {
271
242
  console.error(`Tarball: ${packOut.trim()}`);
272
243
  } else {
273
- console.error(`bun pm pack failed: ${packErr}`);
274
- }
275
- },
276
-
277
- async rollback(options?: { restore?: string; verbose?: boolean }): Promise<void> {
278
- const { listSnapshots, restoreSnapshot } = await import("./codegen/rollback");
279
- const { join, dirname } = await import("path");
280
- const { fileURLToPath } = await import("url");
281
-
282
- const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
283
- const snapshotsDir = join(pkgDir, ".snapshots");
284
-
285
- if (options?.restore) {
286
- const generatedDir = join(pkgDir, "src", "generated");
287
- restoreSnapshot(String(options.restore), generatedDir);
288
- console.error(`Restored snapshot: ${options.restore}`);
289
- } else {
290
- const snapshots = listSnapshots(snapshotsDir);
291
- if (snapshots.length === 0) {
292
- console.error("No snapshots available.");
293
- } else {
294
- console.error(`Available snapshots (${snapshots.length}):`);
295
- for (const s of snapshots) {
296
- console.error(` ${s.timestamp} ${s.resourceCount} resources ${s.path}`);
297
- }
298
- }
244
+ console.error(`${rt.commands.packCmd.join(" ")} failed: ${packErr}`);
299
245
  }
300
246
  },
301
247
 
@@ -307,49 +253,80 @@ export const logsBucket = new aws.Bucket({
307
253
  skills(): SkillDefinition[] {
308
254
  return [
309
255
  {
310
- name: "aws-cloudformation",
311
- description: "AWS CloudFormation best practices and common patterns",
256
+ name: "chant-aws",
257
+ description: "AWS CloudFormation template management workflows, patterns, and troubleshooting",
312
258
  content: `---
313
- name: aws-cloudformation
314
- description: AWS CloudFormation best practices and common patterns
259
+ skill: chant-aws
260
+ description: Build, validate, and deploy CloudFormation templates from a chant project
261
+ user-invocable: true
315
262
  ---
316
263
 
317
- # AWS CloudFormation with Chant
264
+ # Deploying CloudFormation from Chant
265
+
266
+ This project defines CloudFormation resources as TypeScript in \`src/\`. Use these steps to build, validate, and deploy.
267
+
268
+ ## Build the template
269
+
270
+ \`\`\`bash
271
+ chant build src/ --output stack.json
272
+ \`\`\`
273
+
274
+ ## Validate before deploying
275
+
276
+ \`\`\`bash
277
+ chant lint src/
278
+ aws cloudformation validate-template --template-body file://stack.json
279
+ \`\`\`
280
+
281
+ ## Deploy a new stack
282
+
283
+ \`\`\`bash
284
+ aws cloudformation deploy \\
285
+ --template-file stack.json \\
286
+ --stack-name <stack-name> \\
287
+ --capabilities CAPABILITY_NAMED_IAM
288
+ \`\`\`
289
+
290
+ Add \`--parameter-overrides Key=Value\` if the template has parameters.
291
+
292
+ ## Update an existing stack
318
293
 
319
- ## Common Resource Types
294
+ 1. Edit the TypeScript source
295
+ 2. Rebuild: \`chant build src/ --output stack.json\`
296
+ 3. Preview changes:
297
+ \`\`\`bash
298
+ aws cloudformation create-change-set \\
299
+ --stack-name <stack-name> \\
300
+ --template-body file://stack.json \\
301
+ --change-set-name update-$(date +%s) \\
302
+ --capabilities CAPABILITY_NAMED_IAM
303
+ aws cloudformation describe-change-set \\
304
+ --stack-name <stack-name> \\
305
+ --change-set-name update-<id>
306
+ \`\`\`
307
+ 4. Execute: \`aws cloudformation execute-change-set --stack-name <stack-name> --change-set-name update-<id>\`
320
308
 
321
- - \`AWS::S3::Bucket\` Object storage
322
- - \`AWS::Lambda::Function\` — Serverless compute
323
- - \`AWS::DynamoDB::Table\` — NoSQL database
324
- - \`AWS::IAM::Role\` — Identity and access management
325
- - \`AWS::SNS::Topic\` — Pub/sub messaging
326
- - \`AWS::SQS::Queue\` — Message queue
327
- - \`AWS::EC2::SecurityGroup\` — Network firewall rules
309
+ Or deploy directly: \`aws cloudformation deploy --template-file stack.json --stack-name <stack-name> --capabilities CAPABILITY_NAMED_IAM\`
328
310
 
329
- ## Intrinsic Functions
311
+ ## Delete a stack
330
312
 
331
- - \`Sub\` — String interpolation with \`\${}\` syntax
332
- - \`Ref\` Reference a resource or parameter
333
- - \`GetAtt\` Get a resource attribute (e.g. ARN)
334
- - \`If\` — Conditional value based on a condition
335
- - \`Join\` — Join strings with a delimiter
336
- - \`Select\` — Pick an item from a list by index
313
+ \`\`\`bash
314
+ aws cloudformation delete-stack --stack-name <stack-name>
315
+ aws cloudformation wait stack-delete-complete --stack-name <stack-name>
316
+ \`\`\`
337
317
 
338
- ## Pseudo Parameters
318
+ ## Check stack status
339
319
 
340
- - \`AWS::StackName\` — Name of the current stack
341
- - \`AWS::Region\` Current deployment region
342
- - \`AWS::AccountId\` Current AWS account ID
343
- - \`AWS::Partition\` — Partition (aws, aws-cn, aws-us-gov)
320
+ \`\`\`bash
321
+ aws cloudformation describe-stacks --stack-name <stack-name>
322
+ aws cloudformation describe-stack-events --stack-name <stack-name> --max-items 10
323
+ \`\`\`
344
324
 
345
- ## Best Practices
325
+ ## Troubleshooting deploy failures
346
326
 
347
- 1. **Always enable encryption** — Use \`BucketEncryption\` for S3, \`SSESpecification\` for DynamoDB
348
- 2. **Block public access** — Set \`PublicAccessBlockConfiguration\` on all S3 buckets
349
- 3. **Use least-privilege IAM** Avoid \`*\` in IAM policy actions and resources
350
- 4. **Enable versioning** — Turn on \`VersioningConfiguration\` for data buckets
351
- 5. **Use Sub for dynamic names** — \`Sub\\\`\\\${AWS::StackName}-suffix\\\`\` for unique naming
352
- 6. **Share config via barrel files** — Put common settings in \`_.ts\` and import as \`* as _\`
327
+ - Check events: \`aws cloudformation describe-stack-events --stack-name <stack-name>\`
328
+ - Rollback stuck: \`aws cloudformation continue-update-rollback --stack-name <stack-name>\`
329
+ - Drift: \`aws cloudformation detect-stack-drift --stack-name <stack-name>\`
353
330
  `,
354
331
  triggers: [
355
332
  { type: "file-pattern", value: "**/*.aws.ts" },
@@ -453,31 +430,31 @@ description: AWS CloudFormation best practices and common patterns
453
430
  description: "AWS S3 bucket with versioning and encryption",
454
431
  mimeType: "text/typescript",
455
432
  async handler(): Promise<string> {
456
- return `import * as aws from "@intentius/chant-lexicon-aws";
433
+ return `import { ServerSideEncryptionByDefault, ServerSideEncryptionRule, BucketEncryption, VersioningConfiguration, Bucket, Sub, AWS } from "@intentius/chant-lexicon-aws";
457
434
 
458
435
  // Encryption configuration
459
- export const encryptionDefault = new aws.ServerSideEncryptionByDefault({
460
- sseAlgorithm: "AES256",
436
+ export const encryptionDefault = new ServerSideEncryptionByDefault({
437
+ SSEAlgorithm: "AES256",
461
438
  });
462
439
 
463
- export const encryptionRule = new aws.ServerSideEncryptionRule({
464
- serverSideEncryptionByDefault: encryptionDefault,
440
+ export const encryptionRule = new ServerSideEncryptionRule({
441
+ ServerSideEncryptionByDefault: encryptionDefault,
465
442
  });
466
443
 
467
- export const bucketEncryption = new aws.BucketEncryption({
468
- serverSideEncryptionConfiguration: [encryptionRule],
444
+ export const bucketEncryption = new BucketEncryption({
445
+ ServerSideEncryptionConfiguration: [encryptionRule],
469
446
  });
470
447
 
471
448
  // Versioning
472
- export const versioningEnabled = new aws.VersioningConfiguration({
473
- status: "Enabled",
449
+ export const versioningEnabled = new VersioningConfiguration({
450
+ Status: "Enabled",
474
451
  });
475
452
 
476
- // Create a versioned bucket with encryption
477
- export const dataBucket = new aws.Bucket({
478
- bucketName: aws.Sub\`\${aws.AWS.StackName}-data\`,
479
- versioningConfiguration: versioningEnabled,
480
- bucketEncryption: bucketEncryption,
453
+ // Create a versioned bucket with encryption (AccountId ensures global uniqueness)
454
+ export const dataBucket = new Bucket({
455
+ BucketName: Sub\`\${AWS.StackName}-\${AWS.AccountId}-data\`,
456
+ VersioningConfiguration: versioningEnabled,
457
+ BucketEncryption: bucketEncryption,
481
458
  });
482
459
  `;
483
460
  },
@@ -488,17 +465,17 @@ export const dataBucket = new aws.Bucket({
488
465
  description: "Using AttrRefs for cross-resource references",
489
466
  mimeType: "text/typescript",
490
467
  async handler(): Promise<string> {
491
- return `import * as aws from "@intentius/chant-lexicon-aws";
468
+ return `import { Bucket, VersioningConfiguration, Role } from "@intentius/chant-lexicon-aws";
492
469
 
493
470
  // Create a bucket
494
- export const dataBucket = new aws.Bucket({
495
- bucketName: "my-data-bucket",
496
- versioningConfiguration: new aws.VersioningConfiguration({ status: "Enabled" }),
471
+ export const dataBucket = new Bucket({
472
+ BucketName: "my-data-bucket",
473
+ VersioningConfiguration: new VersioningConfiguration({ Status: "Enabled" }),
497
474
  });
498
475
 
499
476
  // Create a role that references the bucket's ARN
500
- export const role = new aws.Role({
501
- assumeRolePolicyDocument: {
477
+ export const role = new Role({
478
+ AssumeRolePolicyDocument: {
502
479
  Version: "2012-10-17",
503
480
  Statement: [{
504
481
  Effect: "Allow",
@@ -18,9 +18,9 @@ class MockBucket implements Declarable {
18
18
  readonly lexicon = "aws";
19
19
  readonly entityType = "AWS::S3::Bucket";
20
20
  readonly arn: AttrRef;
21
- readonly props: { bucketName?: string; versioningConfiguration?: { status: string } };
21
+ readonly props: { BucketName?: string; VersioningConfiguration?: { Status: string } };
22
22
 
23
- constructor(props: { bucketName?: string; versioningConfiguration?: { status: string } } = {}) {
23
+ constructor(props: { BucketName?: string; VersioningConfiguration?: { Status: string } } = {}) {
24
24
  this.props = props;
25
25
  this.arn = new AttrRef(this, "Arn");
26
26
  }
@@ -57,7 +57,7 @@ describe("awsSerializer.serialize", () => {
57
57
 
58
58
  test("serializes resources", () => {
59
59
  const entities = new Map<string, Declarable>();
60
- entities.set("MyBucket", new MockBucket({ bucketName: "my-bucket" }));
60
+ entities.set("MyBucket", new MockBucket({ BucketName: "my-bucket" }));
61
61
 
62
62
  const output = awsSerializer.serialize(entities);
63
63
  const template = JSON.parse(output);
@@ -86,8 +86,8 @@ describe("awsSerializer.serialize", () => {
86
86
  test("serializes nested properties", () => {
87
87
  const entities = new Map<string, Declarable>();
88
88
  entities.set("MyBucket", new MockBucket({
89
- bucketName: "my-bucket",
90
- versioningConfiguration: { status: "Enabled" },
89
+ BucketName: "my-bucket",
90
+ VersioningConfiguration: { Status: "Enabled" },
91
91
  }));
92
92
 
93
93
  const output = awsSerializer.serialize(entities);
@@ -98,21 +98,20 @@ describe("awsSerializer.serialize", () => {
98
98
  });
99
99
  });
100
100
 
101
- test("converts property names to PascalCase", () => {
101
+ test("passes through property names verbatim", () => {
102
102
  const entities = new Map<string, Declarable>();
103
- entities.set("MyBucket", new MockBucket({ bucketName: "test" }));
103
+ entities.set("MyBucket", new MockBucket({ BucketName: "test" }));
104
104
 
105
105
  const output = awsSerializer.serialize(entities);
106
106
  const template = JSON.parse(output);
107
107
 
108
108
  expect(template.Resources.MyBucket.Properties.BucketName).toBeDefined();
109
- expect(template.Resources.MyBucket.Properties.bucketName).toBeUndefined();
110
109
  });
111
110
 
112
111
  test("handles multiple resources", () => {
113
112
  const entities = new Map<string, Declarable>();
114
- entities.set("DataBucket", new MockBucket({ bucketName: "data-bucket" }));
115
- entities.set("LogsBucket", new MockBucket({ bucketName: "logs-bucket" }));
113
+ entities.set("DataBucket", new MockBucket({ BucketName: "data-bucket" }));
114
+ entities.set("LogsBucket", new MockBucket({ BucketName: "logs-bucket" }));
116
115
 
117
116
  const output = awsSerializer.serialize(entities);
118
117
  const template = JSON.parse(output);
@@ -125,7 +124,7 @@ describe("awsSerializer.serialize", () => {
125
124
  test("handles resources and parameters together", () => {
126
125
  const entities = new Map<string, Declarable>();
127
126
  entities.set("Env", new Parameter("String"));
128
- entities.set("MyBucket", new MockBucket({ bucketName: "bucket" }));
127
+ entities.set("MyBucket", new MockBucket({ BucketName: "bucket" }));
129
128
 
130
129
  const output = awsSerializer.serialize(entities);
131
130
  const template = JSON.parse(output);
@@ -154,9 +153,9 @@ class MockEncryption implements Declarable {
154
153
  readonly lexicon = "aws";
155
154
  readonly entityType = "AWS::S3::Bucket.BucketEncryption";
156
155
  readonly kind = "property" as const;
157
- readonly props: { serverSideEncryptionConfiguration: unknown[] };
156
+ readonly props: { ServerSideEncryptionConfiguration: unknown[] };
158
157
 
159
- constructor(props: { serverSideEncryptionConfiguration: unknown[] }) {
158
+ constructor(props: { ServerSideEncryptionConfiguration: unknown[] }) {
160
159
  this.props = props;
161
160
  }
162
161
  }
@@ -164,14 +163,14 @@ class MockEncryption implements Declarable {
164
163
  describe("property-kind Declarables", () => {
165
164
  test("property-kind Declarables are inlined into parent properties", () => {
166
165
  const encryption = new MockEncryption({
167
- serverSideEncryptionConfiguration: [
168
- { serverSideEncryptionByDefault: { sseAlgorithm: "AES256" } },
166
+ ServerSideEncryptionConfiguration: [
167
+ { ServerSideEncryptionByDefault: { SSEAlgorithm: "AES256" } },
169
168
  ],
170
169
  });
171
170
 
172
- const bucket = new MockBucket({ bucketName: "my-bucket" });
171
+ const bucket = new MockBucket({ BucketName: "my-bucket" });
173
172
  // Manually set encryption as a prop
174
- (bucket.props as Record<string, unknown>).encryption = encryption;
173
+ (bucket.props as Record<string, unknown>).BucketEncryption = encryption;
175
174
 
176
175
  const entities = new Map<string, Declarable>();
177
176
  entities.set("DataEncryption", encryption);
@@ -181,23 +180,23 @@ describe("property-kind Declarables", () => {
181
180
  const template = JSON.parse(output);
182
181
 
183
182
  // Encryption should be inlined, not a Ref
184
- expect(template.Resources.MyBucket.Properties.Encryption).toEqual({
183
+ expect(template.Resources.MyBucket.Properties.BucketEncryption).toEqual({
185
184
  ServerSideEncryptionConfiguration: [
186
- { ServerSideEncryptionByDefault: { SseAlgorithm: "AES256" } },
185
+ { ServerSideEncryptionByDefault: { SSEAlgorithm: "AES256" } },
187
186
  ],
188
187
  });
189
188
  });
190
189
 
191
190
  test("property-kind Declarables do NOT appear as standalone Resources", () => {
192
191
  const encryption = new MockEncryption({
193
- serverSideEncryptionConfiguration: [
194
- { serverSideEncryptionByDefault: { sseAlgorithm: "AES256" } },
192
+ ServerSideEncryptionConfiguration: [
193
+ { ServerSideEncryptionByDefault: { SSEAlgorithm: "AES256" } },
195
194
  ],
196
195
  });
197
196
 
198
197
  const entities = new Map<string, Declarable>();
199
198
  entities.set("DataEncryption", encryption);
200
- entities.set("MyBucket", new MockBucket({ bucketName: "my-bucket" }));
199
+ entities.set("MyBucket", new MockBucket({ BucketName: "my-bucket" }));
201
200
 
202
201
  const output = awsSerializer.serialize(entities);
203
202
  const template = JSON.parse(output);
@@ -207,16 +206,16 @@ describe("property-kind Declarables", () => {
207
206
  });
208
207
 
209
208
  test("resource-kind Declarables still emit Ref when referenced", () => {
210
- const sourceBucket = new MockBucket({ bucketName: "source" });
209
+ const sourceBucket = new MockBucket({ BucketName: "source" });
211
210
 
212
211
  class MockConfig implements Declarable {
213
212
  readonly [DECLARABLE_MARKER] = true as const;
214
213
  readonly lexicon = "aws";
215
214
  readonly entityType = "AWS::S3::ReplicationDestination";
216
- readonly props: { bucket: Declarable };
215
+ readonly props: { Bucket: Declarable };
217
216
 
218
- constructor(bucket: Declarable) {
219
- this.props = { bucket };
217
+ constructor(Bucket: Declarable) {
218
+ this.props = { Bucket };
220
219
  }
221
220
  }
222
221
 
@@ -233,7 +232,7 @@ describe("property-kind Declarables", () => {
233
232
 
234
233
  describe("intrinsic serialization", () => {
235
234
  test("handles AttrRef in properties", () => {
236
- const source = new MockBucket({ bucketName: "source" });
235
+ const source = new MockBucket({ BucketName: "source" });
237
236
  // Set the logical name on the AttrRef before using it
238
237
  (source.arn as Record<string, unknown>)._setLogicalName("SourceBucket");
239
238
 
@@ -241,10 +240,10 @@ describe("intrinsic serialization", () => {
241
240
  readonly [DECLARABLE_MARKER] = true as const;
242
241
  readonly lexicon = "aws";
243
242
  readonly entityType = "AWS::S3::ReplicationConfiguration";
244
- readonly props: { sourceArn: AttrRef };
243
+ readonly props: { SourceArn: AttrRef };
245
244
 
246
- constructor(sourceArn: AttrRef) {
247
- this.props = { sourceArn };
245
+ constructor(SourceArn: AttrRef) {
246
+ this.props = { SourceArn };
248
247
  }
249
248
  }
250
249
 
@@ -256,14 +255,14 @@ describe("intrinsic serialization", () => {
256
255
  const template = JSON.parse(output);
257
256
 
258
257
  expect(template.Resources.Replication.Properties.SourceArn).toEqual({
259
- "Fn::GetAttr": ["SourceBucket", "Arn"],
258
+ "Fn::GetAtt": ["SourceBucket", "Arn"],
260
259
  });
261
260
  });
262
261
  });
263
262
 
264
263
  describe("LexiconOutput serialization", () => {
265
264
  test("generates CF Outputs section for LexiconOutputs", () => {
266
- const bucket = new MockBucket({ bucketName: "data-bucket" });
265
+ const bucket = new MockBucket({ BucketName: "data-bucket" });
267
266
  const lexiconOutput = new LexiconOutput(bucket.arn, "DataBucketArn");
268
267
  lexiconOutput._setSourceEntity("dataBucket");
269
268
 
@@ -275,13 +274,13 @@ describe("LexiconOutput serialization", () => {
275
274
 
276
275
  expect(template.Outputs).toBeDefined();
277
276
  expect(template.Outputs.DataBucketArn).toEqual({
278
- Value: { "Fn::GetAttr": ["dataBucket", "Arn"] },
277
+ Value: { "Fn::GetAtt": ["dataBucket", "Arn"] },
279
278
  });
280
279
  });
281
280
 
282
281
  test("generates multiple CF Outputs", () => {
283
- const dataBucket = new MockBucket({ bucketName: "data-bucket" });
284
- const logsBucket = new MockBucket({ bucketName: "logs-bucket" });
282
+ const dataBucket = new MockBucket({ BucketName: "data-bucket" });
283
+ const logsBucket = new MockBucket({ BucketName: "logs-bucket" });
285
284
 
286
285
  const dataOutput = new LexiconOutput(dataBucket.arn, "DataBucketArn");
287
286
  dataOutput._setSourceEntity("dataBucket");
@@ -298,16 +297,16 @@ describe("LexiconOutput serialization", () => {
298
297
  expect(template.Outputs).toBeDefined();
299
298
  expect(Object.keys(template.Outputs)).toHaveLength(2);
300
299
  expect(template.Outputs.DataBucketArn.Value).toEqual({
301
- "Fn::GetAttr": ["dataBucket", "Arn"],
300
+ "Fn::GetAtt": ["dataBucket", "Arn"],
302
301
  });
303
302
  expect(template.Outputs.LogsBucketArn.Value).toEqual({
304
- "Fn::GetAttr": ["logsBucket", "Arn"],
303
+ "Fn::GetAtt": ["logsBucket", "Arn"],
305
304
  });
306
305
  });
307
306
 
308
307
  test("omits Outputs section when no LexiconOutputs provided", () => {
309
308
  const entities = new Map<string, Declarable>();
310
- entities.set("MyBucket", new MockBucket({ bucketName: "bucket" }));
309
+ entities.set("MyBucket", new MockBucket({ BucketName: "bucket" }));
311
310
 
312
311
  const result = awsSerializer.serialize(entities);
313
312
  const template = JSON.parse(result);
@@ -317,7 +316,7 @@ describe("LexiconOutput serialization", () => {
317
316
 
318
317
  test("omits Outputs section when empty LexiconOutputs array", () => {
319
318
  const entities = new Map<string, Declarable>();
320
- entities.set("MyBucket", new MockBucket({ bucketName: "bucket" }));
319
+ entities.set("MyBucket", new MockBucket({ BucketName: "bucket" }));
321
320
 
322
321
  const result = awsSerializer.serialize(entities, []);
323
322
  const template = JSON.parse(result as string);
@@ -430,7 +429,7 @@ describe("nested stack serialization", () => {
430
429
  Resources: {},
431
430
  });
432
431
 
433
- const bucket = new MockBucket({ bucketName: "data" });
432
+ const bucket = new MockBucket({ BucketName: "data" });
434
433
 
435
434
  const entities = new Map<string, Declarable>();
436
435
  entities.set("network", stack as unknown as Declarable);
@@ -446,7 +445,7 @@ describe("nested stack serialization", () => {
446
445
 
447
446
  test("without nested stacks returns plain string", () => {
448
447
  const entities = new Map<string, Declarable>();
449
- entities.set("MyBucket", new MockBucket({ bucketName: "bucket" }));
448
+ entities.set("MyBucket", new MockBucket({ BucketName: "bucket" }));
450
449
 
451
450
  const result = awsSerializer.serialize(entities);
452
451
  expect(typeof result).toBe("string");
@@ -460,7 +459,7 @@ describe("nested stack serialization", () => {
460
459
  subnet: { Type: "AWS::EC2::Subnet" },
461
460
  },
462
461
  Outputs: {
463
- subnetId: { Value: { "Fn::GetAttr": ["subnet", "SubnetId"] } },
462
+ subnetId: { Value: { "Fn::GetAtt": ["subnet", "SubnetId"] } },
464
463
  },
465
464
  });
466
465
 
@@ -472,7 +471,7 @@ describe("nested stack serialization", () => {
472
471
  entityType: "AWS::Lambda::Function",
473
472
  kind: "resource" as const,
474
473
  props: {
475
- vpcConfig: { subnetIds: [subnetRef] },
474
+ VpcConfig: { SubnetIds: [subnetRef] },
476
475
  },
477
476
  } as unknown as Declarable;
478
477