@intentius/chant-lexicon-aws 0.0.2
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/README.md +438 -0
- package/package.json +30 -0
- package/src/codegen/__snapshots__/snapshot.test.ts.snap +197 -0
- package/src/codegen/docs-cli.ts +3 -0
- package/src/codegen/docs.ts +1206 -0
- package/src/codegen/extensions.ts +171 -0
- package/src/codegen/fallback.ts +33 -0
- package/src/codegen/generate-cli.ts +17 -0
- package/src/codegen/generate-lexicon.ts +98 -0
- package/src/codegen/generate-typescript.ts +257 -0
- package/src/codegen/generate.test.ts +125 -0
- package/src/codegen/generate.ts +226 -0
- package/src/codegen/idempotency.test.ts +28 -0
- package/src/codegen/naming.ts +120 -0
- package/src/codegen/package.test.ts +60 -0
- package/src/codegen/package.ts +84 -0
- package/src/codegen/patches.ts +98 -0
- package/src/codegen/rollback.test.ts +80 -0
- package/src/codegen/rollback.ts +20 -0
- package/src/codegen/sam.ts +387 -0
- package/src/codegen/snapshot.test.ts +84 -0
- package/src/codegen/typecheck.test.ts +50 -0
- package/src/codegen/typecheck.ts +4 -0
- package/src/codegen/versions.ts +37 -0
- package/src/coverage.ts +14 -0
- package/src/generated/index.d.ts +160753 -0
- package/src/generated/index.ts +14396 -0
- package/src/generated/lexicon-aws.json +114563 -0
- package/src/generated/runtime.ts +4 -0
- package/src/import/generator.test.ts +181 -0
- package/src/import/generator.ts +349 -0
- package/src/import/parser.test.ts +200 -0
- package/src/import/parser.ts +350 -0
- package/src/import/roundtrip-fixtures.test.ts +78 -0
- package/src/import/roundtrip.test.ts +195 -0
- package/src/index.ts +63 -0
- package/src/integration.test.ts +129 -0
- package/src/intrinsics.test.ts +167 -0
- package/src/intrinsics.ts +223 -0
- package/src/lint/post-synth/cf-refs.ts +91 -0
- package/src/lint/post-synth/cor020.ts +72 -0
- package/src/lint/post-synth/ext001.test.ts +68 -0
- package/src/lint/post-synth/ext001.ts +222 -0
- package/src/lint/post-synth/post-synth.test.ts +280 -0
- package/src/lint/post-synth/waw010.ts +49 -0
- package/src/lint/post-synth/waw011.ts +49 -0
- package/src/lint/post-synth/waw013.ts +45 -0
- package/src/lint/post-synth/waw014.ts +50 -0
- package/src/lint/post-synth/waw015.ts +100 -0
- package/src/lint/rules/hardcoded-region.ts +43 -0
- package/src/lint/rules/iam-wildcard.ts +66 -0
- package/src/lint/rules/index.ts +7 -0
- package/src/lint/rules/rules.test.ts +175 -0
- package/src/lint/rules/s3-encryption.ts +69 -0
- package/src/lsp/completions.test.ts +72 -0
- package/src/lsp/completions.ts +18 -0
- package/src/lsp/hover.test.ts +53 -0
- package/src/lsp/hover.ts +53 -0
- package/src/nested-stack.test.ts +83 -0
- package/src/nested-stack.ts +125 -0
- package/src/plugin.test.ts +316 -0
- package/src/plugin.ts +514 -0
- package/src/pseudo.test.ts +55 -0
- package/src/pseudo.ts +29 -0
- package/src/serializer.test.ts +507 -0
- package/src/serializer.ts +333 -0
- package/src/spec/fetch.test.ts +27 -0
- package/src/spec/fetch.ts +107 -0
- package/src/spec/parse.test.ts +153 -0
- package/src/spec/parse.ts +202 -0
- package/src/testdata/load-fixtures.ts +17 -0
- package/src/testdata/roundtrip/conditions.json +21 -0
- package/src/testdata/roundtrip/intrinsic-calls.json +31 -0
- package/src/testdata/roundtrip/intrinsics.json +18 -0
- package/src/testdata/roundtrip/multi-resource.json +37 -0
- package/src/testdata/roundtrip/parameters.json +23 -0
- package/src/testdata/roundtrip/simple.json +12 -0
- package/src/testdata/sam-fixtures/api.yaml +14 -0
- package/src/testdata/sam-fixtures/application.yaml +13 -0
- package/src/testdata/sam-fixtures/function.yaml +22 -0
- package/src/testdata/sam-fixtures/graphql-api.yaml +13 -0
- package/src/testdata/sam-fixtures/http-api.yaml +15 -0
- package/src/testdata/sam-fixtures/layer-version.yaml +15 -0
- package/src/testdata/sam-fixtures/multi-type-a.yaml +23 -0
- package/src/testdata/sam-fixtures/multi-type-b.yaml +29 -0
- package/src/testdata/sam-fixtures/simple-table.yaml +12 -0
- package/src/testdata/sam-fixtures/state-machine.yaml +14 -0
- package/src/testdata/schemas/aws-dynamodb-table.json +126 -0
- package/src/testdata/schemas/aws-iam-role.json +85 -0
- package/src/testdata/schemas/aws-lambda-function.json +90 -0
- package/src/testdata/schemas/aws-s3-bucket.json +83 -0
- package/src/testdata/schemas/aws-sns-topic.json +71 -0
- package/src/validate-cli.ts +19 -0
- package/src/validate.ts +34 -0
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
import type { LexiconPlugin, IntrinsicDef, SkillDefinition } from "@intentius/chant/lexicon";
|
|
2
|
+
import type { LintRule } from "@intentius/chant/lint/rule";
|
|
3
|
+
import type { PostSynthCheck } from "@intentius/chant/lint/post-synth";
|
|
4
|
+
import type { TemplateParser } from "@intentius/chant/import/parser";
|
|
5
|
+
import type { TypeScriptGenerator } from "@intentius/chant/import/generator";
|
|
6
|
+
import type { CompletionContext, CompletionItem, HoverContext, HoverInfo } from "@intentius/chant/lsp/types";
|
|
7
|
+
import type { McpToolContribution, McpResourceContribution } from "@intentius/chant/mcp/types";
|
|
8
|
+
import { awsSerializer } from "./serializer";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* AWS CloudFormation lexicon plugin.
|
|
12
|
+
*
|
|
13
|
+
* Provides serializer, lint rules, template detection,
|
|
14
|
+
* import parsing, and code generation for AWS CloudFormation.
|
|
15
|
+
*/
|
|
16
|
+
export const awsPlugin: LexiconPlugin = {
|
|
17
|
+
name: "aws",
|
|
18
|
+
serializer: awsSerializer,
|
|
19
|
+
|
|
20
|
+
lintRules(): LintRule[] {
|
|
21
|
+
// Lazy-load to avoid pulling in rules unless needed
|
|
22
|
+
const { hardcodedRegionRule } = require("./lint/rules/hardcoded-region");
|
|
23
|
+
const { s3EncryptionRule } = require("./lint/rules/s3-encryption");
|
|
24
|
+
const { iamWildcardRule } = require("./lint/rules/iam-wildcard");
|
|
25
|
+
return [hardcodedRegionRule, s3EncryptionRule, iamWildcardRule];
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
intrinsics(): IntrinsicDef[] {
|
|
29
|
+
return [
|
|
30
|
+
{ name: "Sub", description: "Fn::Sub template string interpolation" },
|
|
31
|
+
{ name: "Ref", description: "Reference a parameter or resource" },
|
|
32
|
+
{ name: "GetAtt", description: "Fn::GetAtt — get resource attribute" },
|
|
33
|
+
{ name: "If", description: "Fn::If — conditional value" },
|
|
34
|
+
{ name: "Join", description: "Fn::Join — join values with delimiter" },
|
|
35
|
+
{ name: "Select", description: "Fn::Select — select value by index" },
|
|
36
|
+
{ name: "Split", description: "Fn::Split — split string by delimiter" },
|
|
37
|
+
{ name: "Base64", description: "Fn::Base64 — encode to Base64" },
|
|
38
|
+
];
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
pseudoParameters(): string[] {
|
|
42
|
+
return [
|
|
43
|
+
"AWS::StackName",
|
|
44
|
+
"AWS::Region",
|
|
45
|
+
"AWS::AccountId",
|
|
46
|
+
"AWS::StackId",
|
|
47
|
+
"AWS::URLSuffix",
|
|
48
|
+
"AWS::NoValue",
|
|
49
|
+
"AWS::NotificationARNs",
|
|
50
|
+
"AWS::Partition",
|
|
51
|
+
];
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
initTemplates(): Record<string, string> {
|
|
55
|
+
return {
|
|
56
|
+
"_.ts": `export * from "./config";\n`,
|
|
57
|
+
"config.ts": `/**
|
|
58
|
+
* Shared bucket configuration — encryption, versioning, public access
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
import * as aws from "@intentius/chant-lexicon-aws";
|
|
62
|
+
|
|
63
|
+
// Encryption default — AES256 server-side encryption
|
|
64
|
+
export const encryptionDefault = new aws.ServerSideEncryptionByDefault({
|
|
65
|
+
sseAlgorithm: "AES256",
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Encryption rule wrapping the default
|
|
69
|
+
export const encryptionRule = new aws.ServerSideEncryptionRule({
|
|
70
|
+
serverSideEncryptionByDefault: encryptionDefault,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Bucket encryption configuration
|
|
74
|
+
export const bucketEncryption = new aws.BucketEncryption({
|
|
75
|
+
serverSideEncryptionConfiguration: [encryptionRule],
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Public access block — deny all public access
|
|
79
|
+
export const publicAccessBlock = new aws.PublicAccessBlockConfiguration({
|
|
80
|
+
blockPublicAcls: true,
|
|
81
|
+
blockPublicPolicy: true,
|
|
82
|
+
ignorePublicAcls: true,
|
|
83
|
+
restrictPublicBuckets: true,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Versioning — enabled
|
|
87
|
+
export const versioningEnabled = new aws.VersioningConfiguration({
|
|
88
|
+
status: "Enabled",
|
|
89
|
+
});
|
|
90
|
+
`,
|
|
91
|
+
"data-bucket.ts": `/**
|
|
92
|
+
* Data bucket — primary storage with encryption and versioning
|
|
93
|
+
*/
|
|
94
|
+
|
|
95
|
+
import * as aws from "@intentius/chant-lexicon-aws";
|
|
96
|
+
import * as _ from "./_";
|
|
97
|
+
|
|
98
|
+
export const dataBucket = new aws.Bucket({
|
|
99
|
+
bucketName: aws.Sub\`\${aws.AWS.StackName}-data\`,
|
|
100
|
+
versioningConfiguration: _.versioningEnabled,
|
|
101
|
+
bucketEncryption: _.bucketEncryption,
|
|
102
|
+
publicAccessBlockConfiguration: _.publicAccessBlock,
|
|
103
|
+
});
|
|
104
|
+
`,
|
|
105
|
+
"logs-bucket.ts": `/**
|
|
106
|
+
* Logs bucket — log delivery with encryption and versioning
|
|
107
|
+
*/
|
|
108
|
+
|
|
109
|
+
import * as aws from "@intentius/chant-lexicon-aws";
|
|
110
|
+
import * as _ from "./_";
|
|
111
|
+
|
|
112
|
+
export const logsBucket = new aws.Bucket({
|
|
113
|
+
bucketName: aws.Sub\`\${aws.AWS.StackName}-logs\`,
|
|
114
|
+
accessControl: "LogDeliveryWrite",
|
|
115
|
+
versioningConfiguration: _.versioningEnabled,
|
|
116
|
+
bucketEncryption: _.bucketEncryption,
|
|
117
|
+
publicAccessBlockConfiguration: _.publicAccessBlock,
|
|
118
|
+
});
|
|
119
|
+
`,
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
detectTemplate(data: unknown): boolean {
|
|
124
|
+
if (typeof data !== "object" || data === null) return false;
|
|
125
|
+
const obj = data as Record<string, unknown>;
|
|
126
|
+
|
|
127
|
+
// CloudFormation has AWSTemplateFormatVersion
|
|
128
|
+
if (obj.AWSTemplateFormatVersion !== undefined) return true;
|
|
129
|
+
|
|
130
|
+
// Or Resources with AWS::* types
|
|
131
|
+
if (typeof obj.Resources === "object" && obj.Resources !== null) {
|
|
132
|
+
for (const resource of Object.values(obj.Resources as Record<string, unknown>)) {
|
|
133
|
+
if (typeof resource === "object" && resource !== null) {
|
|
134
|
+
const type = (resource as Record<string, unknown>).Type;
|
|
135
|
+
if (typeof type === "string" && type.startsWith("AWS::")) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return false;
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
templateParser(): TemplateParser {
|
|
146
|
+
const { CFParser } = require("./import/parser");
|
|
147
|
+
return new CFParser();
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
templateGenerator(): TypeScriptGenerator {
|
|
151
|
+
const { CFGenerator } = require("./import/generator");
|
|
152
|
+
return new CFGenerator();
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
postSynthChecks(): PostSynthCheck[] {
|
|
156
|
+
// Lazy-load to avoid pulling in checks unless needed
|
|
157
|
+
const { waw010 } = require("./lint/post-synth/waw010");
|
|
158
|
+
const { waw011 } = require("./lint/post-synth/waw011");
|
|
159
|
+
const { cor020 } = require("./lint/post-synth/cor020");
|
|
160
|
+
const { ext001 } = require("./lint/post-synth/ext001");
|
|
161
|
+
const { waw013 } = require("./lint/post-synth/waw013");
|
|
162
|
+
const { waw014 } = require("./lint/post-synth/waw014");
|
|
163
|
+
const { waw015 } = require("./lint/post-synth/waw015");
|
|
164
|
+
return [waw010, waw011, cor020, ext001, waw013, waw014, waw015];
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
async generate(options?: { verbose?: boolean }): Promise<void> {
|
|
168
|
+
const { generate, writeGeneratedFiles } = await import("./codegen/generate");
|
|
169
|
+
const { dirname } = await import("path");
|
|
170
|
+
const { fileURLToPath } = await import("url");
|
|
171
|
+
|
|
172
|
+
const result = await generate({ verbose: options?.verbose ?? true });
|
|
173
|
+
const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
174
|
+
writeGeneratedFiles(result, pkgDir);
|
|
175
|
+
|
|
176
|
+
console.error(`Generated ${result.resources} resources, ${result.properties} property types, ${result.enums} enums`);
|
|
177
|
+
if (result.warnings.length > 0) {
|
|
178
|
+
console.error(`${result.warnings.length} warnings`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const { PINNED_VERSIONS } = await import("./codegen/versions");
|
|
182
|
+
console.error(`cfn-lint patches: ${PINNED_VERSIONS.cfnLint}`);
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
async validate(options?: { verbose?: boolean }): Promise<void> {
|
|
186
|
+
const { validate } = await import("./validate");
|
|
187
|
+
const result = await validate();
|
|
188
|
+
|
|
189
|
+
for (const check of result.checks) {
|
|
190
|
+
const status = check.ok ? "OK" : "FAIL";
|
|
191
|
+
const msg = check.error ? ` — ${check.error}` : "";
|
|
192
|
+
console.error(` [${status}] ${check.name}${msg}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!result.success) {
|
|
196
|
+
throw new Error("Validation failed");
|
|
197
|
+
}
|
|
198
|
+
console.error("All validation checks passed.");
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
async coverage(options?: { verbose?: boolean; minOverall?: number }): Promise<void> {
|
|
202
|
+
const { readFileSync } = await import("fs");
|
|
203
|
+
const { join, dirname } = await import("path");
|
|
204
|
+
const { fileURLToPath } = await import("url");
|
|
205
|
+
const { computeCoverage, checkThresholds, formatSummary, formatVerbose } = await import("./coverage");
|
|
206
|
+
|
|
207
|
+
const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
208
|
+
const lexiconPath = join(pkgDir, "src", "generated", "lexicon-aws.json");
|
|
209
|
+
const content = readFileSync(lexiconPath, "utf-8");
|
|
210
|
+
const report = computeCoverage(content);
|
|
211
|
+
|
|
212
|
+
if (options?.verbose) {
|
|
213
|
+
console.log(formatVerbose(report));
|
|
214
|
+
} else {
|
|
215
|
+
console.log(formatSummary(report));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (typeof options?.minOverall === "number") {
|
|
219
|
+
const result = checkThresholds(report, { minOverallPct: options.minOverall });
|
|
220
|
+
if (!result.ok) {
|
|
221
|
+
for (const v of result.violations) console.error(` FAIL: ${v}`);
|
|
222
|
+
throw new Error("Coverage below threshold");
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
async package(options?: { verbose?: boolean; force?: boolean }): Promise<void> {
|
|
228
|
+
const { packageLexicon } = await import("./codegen/package");
|
|
229
|
+
const { writeFileSync, mkdirSync } = await import("fs");
|
|
230
|
+
const { join, dirname } = await import("path");
|
|
231
|
+
const { fileURLToPath } = await import("url");
|
|
232
|
+
|
|
233
|
+
const { spec, stats } = await packageLexicon({ verbose: options?.verbose, force: options?.force });
|
|
234
|
+
|
|
235
|
+
// Write manifest and artifacts to dist/
|
|
236
|
+
const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
237
|
+
const distDir = join(pkgDir, "dist");
|
|
238
|
+
mkdirSync(join(distDir, "types"), { recursive: true });
|
|
239
|
+
mkdirSync(join(distDir, "rules"), { recursive: true });
|
|
240
|
+
mkdirSync(join(distDir, "skills"), { recursive: true });
|
|
241
|
+
|
|
242
|
+
writeFileSync(join(distDir, "manifest.json"), JSON.stringify(spec.manifest, null, 2));
|
|
243
|
+
writeFileSync(join(distDir, "meta.json"), spec.registry);
|
|
244
|
+
writeFileSync(join(distDir, "types", "index.d.ts"), spec.typesDTS);
|
|
245
|
+
|
|
246
|
+
for (const [name, content] of spec.rules) {
|
|
247
|
+
writeFileSync(join(distDir, "rules", name), content);
|
|
248
|
+
}
|
|
249
|
+
for (const [name, content] of spec.skills) {
|
|
250
|
+
writeFileSync(join(distDir, "skills", name), content);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Write integrity.json if available
|
|
254
|
+
if (spec.integrity) {
|
|
255
|
+
writeFileSync(join(distDir, "integrity.json"), JSON.stringify(spec.integrity, null, 2));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
console.error(`Packaged ${stats.resources} resources, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
|
|
259
|
+
|
|
260
|
+
// Produce .tgz via bun pm pack
|
|
261
|
+
const packProc = Bun.spawn(["bun", "pm", "pack"], {
|
|
262
|
+
cwd: pkgDir,
|
|
263
|
+
stdout: "pipe",
|
|
264
|
+
stderr: "pipe",
|
|
265
|
+
});
|
|
266
|
+
const packOut = await new Response(packProc.stdout).text();
|
|
267
|
+
const packErr = await new Response(packProc.stderr).text();
|
|
268
|
+
const packExit = await packProc.exited;
|
|
269
|
+
if (packExit === 0) {
|
|
270
|
+
console.error(`Tarball: ${packOut.trim()}`);
|
|
271
|
+
} else {
|
|
272
|
+
console.error(`bun pm pack failed: ${packErr}`);
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
async rollback(options?: { restore?: string; verbose?: boolean }): Promise<void> {
|
|
277
|
+
const { listSnapshots, restoreSnapshot } = await import("./codegen/rollback");
|
|
278
|
+
const { join, dirname } = await import("path");
|
|
279
|
+
const { fileURLToPath } = await import("url");
|
|
280
|
+
|
|
281
|
+
const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
282
|
+
const snapshotsDir = join(pkgDir, ".snapshots");
|
|
283
|
+
|
|
284
|
+
if (options?.restore) {
|
|
285
|
+
const generatedDir = join(pkgDir, "src", "generated");
|
|
286
|
+
restoreSnapshot(String(options.restore), generatedDir);
|
|
287
|
+
console.error(`Restored snapshot: ${options.restore}`);
|
|
288
|
+
} else {
|
|
289
|
+
const snapshots = listSnapshots(snapshotsDir);
|
|
290
|
+
if (snapshots.length === 0) {
|
|
291
|
+
console.error("No snapshots available.");
|
|
292
|
+
} else {
|
|
293
|
+
console.error(`Available snapshots (${snapshots.length}):`);
|
|
294
|
+
for (const s of snapshots) {
|
|
295
|
+
console.error(` ${s.timestamp} ${s.resourceCount} resources ${s.path}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
async docs(options?: { verbose?: boolean }): Promise<void> {
|
|
302
|
+
const { generateDocs } = await import("./codegen/docs");
|
|
303
|
+
await generateDocs(options);
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
skills(): SkillDefinition[] {
|
|
307
|
+
return [
|
|
308
|
+
{
|
|
309
|
+
name: "aws-cloudformation",
|
|
310
|
+
description: "AWS CloudFormation best practices and common patterns",
|
|
311
|
+
content: `---
|
|
312
|
+
name: aws-cloudformation
|
|
313
|
+
description: AWS CloudFormation best practices and common patterns
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
# AWS CloudFormation with Chant
|
|
317
|
+
|
|
318
|
+
## Common Resource Types
|
|
319
|
+
|
|
320
|
+
- \`AWS::S3::Bucket\` — Object storage
|
|
321
|
+
- \`AWS::Lambda::Function\` — Serverless compute
|
|
322
|
+
- \`AWS::DynamoDB::Table\` — NoSQL database
|
|
323
|
+
- \`AWS::IAM::Role\` — Identity and access management
|
|
324
|
+
- \`AWS::SNS::Topic\` — Pub/sub messaging
|
|
325
|
+
- \`AWS::SQS::Queue\` — Message queue
|
|
326
|
+
- \`AWS::EC2::SecurityGroup\` — Network firewall rules
|
|
327
|
+
|
|
328
|
+
## Intrinsic Functions
|
|
329
|
+
|
|
330
|
+
- \`Sub\` — String interpolation with \`\${}\` syntax
|
|
331
|
+
- \`Ref\` — Reference a resource or parameter
|
|
332
|
+
- \`GetAtt\` — Get a resource attribute (e.g. ARN)
|
|
333
|
+
- \`If\` — Conditional value based on a condition
|
|
334
|
+
- \`Join\` — Join strings with a delimiter
|
|
335
|
+
- \`Select\` — Pick an item from a list by index
|
|
336
|
+
|
|
337
|
+
## Pseudo Parameters
|
|
338
|
+
|
|
339
|
+
- \`AWS::StackName\` — Name of the current stack
|
|
340
|
+
- \`AWS::Region\` — Current deployment region
|
|
341
|
+
- \`AWS::AccountId\` — Current AWS account ID
|
|
342
|
+
- \`AWS::Partition\` — Partition (aws, aws-cn, aws-us-gov)
|
|
343
|
+
|
|
344
|
+
## Best Practices
|
|
345
|
+
|
|
346
|
+
1. **Always enable encryption** — Use \`BucketEncryption\` for S3, \`SSESpecification\` for DynamoDB
|
|
347
|
+
2. **Block public access** — Set \`PublicAccessBlockConfiguration\` on all S3 buckets
|
|
348
|
+
3. **Use least-privilege IAM** — Avoid \`*\` in IAM policy actions and resources
|
|
349
|
+
4. **Enable versioning** — Turn on \`VersioningConfiguration\` for data buckets
|
|
350
|
+
5. **Use Sub for dynamic names** — \`Sub\\\`\\\${AWS::StackName}-suffix\\\`\` for unique naming
|
|
351
|
+
6. **Share config via barrel files** — Put common settings in \`_.ts\` and import as \`* as _\`
|
|
352
|
+
`,
|
|
353
|
+
triggers: [
|
|
354
|
+
{ type: "file-pattern", value: "**/*.aws.ts" },
|
|
355
|
+
{ type: "context", value: "aws" },
|
|
356
|
+
],
|
|
357
|
+
parameters: [
|
|
358
|
+
{
|
|
359
|
+
name: "resourceType",
|
|
360
|
+
description: "AWS CloudFormation resource type (e.g. AWS::S3::Bucket)",
|
|
361
|
+
type: "string",
|
|
362
|
+
required: false,
|
|
363
|
+
},
|
|
364
|
+
],
|
|
365
|
+
examples: [
|
|
366
|
+
{
|
|
367
|
+
title: "S3 Bucket with encryption",
|
|
368
|
+
description: "Create an S3 bucket with server-side encryption enabled",
|
|
369
|
+
input: "Create an encrypted S3 bucket",
|
|
370
|
+
output: `new Bucket("MyBucket", {
|
|
371
|
+
BucketEncryption: {
|
|
372
|
+
ServerSideEncryptionConfiguration: [
|
|
373
|
+
{ ServerSideEncryptionByDefault: { SSEAlgorithm: "aws:kms" } }
|
|
374
|
+
]
|
|
375
|
+
},
|
|
376
|
+
PublicAccessBlockConfiguration: {
|
|
377
|
+
BlockPublicAcls: true,
|
|
378
|
+
BlockPublicPolicy: true,
|
|
379
|
+
IgnorePublicAcls: true,
|
|
380
|
+
RestrictPublicBuckets: true,
|
|
381
|
+
},
|
|
382
|
+
})`,
|
|
383
|
+
},
|
|
384
|
+
],
|
|
385
|
+
},
|
|
386
|
+
];
|
|
387
|
+
},
|
|
388
|
+
|
|
389
|
+
completionProvider(ctx: CompletionContext): CompletionItem[] {
|
|
390
|
+
const { awsCompletions } = require("./lsp/completions");
|
|
391
|
+
return awsCompletions(ctx);
|
|
392
|
+
},
|
|
393
|
+
|
|
394
|
+
hoverProvider(ctx: HoverContext): HoverInfo | undefined {
|
|
395
|
+
const { awsHover } = require("./lsp/hover");
|
|
396
|
+
return awsHover(ctx);
|
|
397
|
+
},
|
|
398
|
+
|
|
399
|
+
mcpTools(): McpToolContribution[] {
|
|
400
|
+
return [
|
|
401
|
+
{
|
|
402
|
+
name: "diff",
|
|
403
|
+
description: "Compare current build output against previous output for AWS CloudFormation",
|
|
404
|
+
inputSchema: {
|
|
405
|
+
type: "object",
|
|
406
|
+
properties: {
|
|
407
|
+
path: {
|
|
408
|
+
type: "string",
|
|
409
|
+
description: "Path to the infrastructure project directory",
|
|
410
|
+
},
|
|
411
|
+
output: {
|
|
412
|
+
type: "string",
|
|
413
|
+
description: "Path to the existing output file to compare against",
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
required: ["path"],
|
|
417
|
+
},
|
|
418
|
+
async handler(params: Record<string, unknown>): Promise<unknown> {
|
|
419
|
+
const { diffCommand } = await import("@intentius/chant/cli/commands/diff");
|
|
420
|
+
const result = await diffCommand({
|
|
421
|
+
path: (params.path as string) ?? ".",
|
|
422
|
+
output: params.output as string | undefined,
|
|
423
|
+
serializers: [awsSerializer],
|
|
424
|
+
});
|
|
425
|
+
return result;
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
];
|
|
429
|
+
},
|
|
430
|
+
|
|
431
|
+
mcpResources(): McpResourceContribution[] {
|
|
432
|
+
return [
|
|
433
|
+
{
|
|
434
|
+
uri: "resource-catalog",
|
|
435
|
+
name: "AWS Resource Catalog",
|
|
436
|
+
description: "JSON list of all supported AWS CloudFormation resource types",
|
|
437
|
+
mimeType: "application/json",
|
|
438
|
+
async handler(): Promise<string> {
|
|
439
|
+
const lexicon = require("./generated/lexicon-aws.json") as Record<string, { resourceType: string; kind: string }>;
|
|
440
|
+
const resources = Object.entries(lexicon)
|
|
441
|
+
.filter(([, entry]) => entry.kind === "resource")
|
|
442
|
+
.map(([className, entry]) => ({
|
|
443
|
+
className,
|
|
444
|
+
resourceType: entry.resourceType,
|
|
445
|
+
}));
|
|
446
|
+
return JSON.stringify(resources);
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
uri: "examples/aws-s3-bucket",
|
|
451
|
+
name: "AWS S3 Bucket Example",
|
|
452
|
+
description: "AWS S3 bucket with versioning and encryption",
|
|
453
|
+
mimeType: "text/typescript",
|
|
454
|
+
async handler(): Promise<string> {
|
|
455
|
+
return `import * as aws from "@intentius/chant-lexicon-aws";
|
|
456
|
+
|
|
457
|
+
// Encryption configuration
|
|
458
|
+
export const encryptionDefault = new aws.ServerSideEncryptionByDefault({
|
|
459
|
+
sseAlgorithm: "AES256",
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
export const encryptionRule = new aws.ServerSideEncryptionRule({
|
|
463
|
+
serverSideEncryptionByDefault: encryptionDefault,
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
export const bucketEncryption = new aws.BucketEncryption({
|
|
467
|
+
serverSideEncryptionConfiguration: [encryptionRule],
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
// Versioning
|
|
471
|
+
export const versioningEnabled = new aws.VersioningConfiguration({
|
|
472
|
+
status: "Enabled",
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// Create a versioned bucket with encryption
|
|
476
|
+
export const dataBucket = new aws.Bucket({
|
|
477
|
+
bucketName: aws.Sub\`\${aws.AWS.StackName}-data\`,
|
|
478
|
+
versioningConfiguration: versioningEnabled,
|
|
479
|
+
bucketEncryption: bucketEncryption,
|
|
480
|
+
});
|
|
481
|
+
`;
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
uri: "examples/cross-references",
|
|
486
|
+
name: "Cross References Example",
|
|
487
|
+
description: "Using AttrRefs for cross-resource references",
|
|
488
|
+
mimeType: "text/typescript",
|
|
489
|
+
async handler(): Promise<string> {
|
|
490
|
+
return `import * as aws from "@intentius/chant-lexicon-aws";
|
|
491
|
+
|
|
492
|
+
// Create a bucket
|
|
493
|
+
export const dataBucket = new aws.Bucket({
|
|
494
|
+
bucketName: "my-data-bucket",
|
|
495
|
+
versioningConfiguration: new aws.VersioningConfiguration({ status: "Enabled" }),
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
// Create a role that references the bucket's ARN
|
|
499
|
+
export const role = new aws.Role({
|
|
500
|
+
assumeRolePolicyDocument: {
|
|
501
|
+
Version: "2012-10-17",
|
|
502
|
+
Statement: [{
|
|
503
|
+
Effect: "Allow",
|
|
504
|
+
Principal: { Service: "lambda.amazonaws.com" },
|
|
505
|
+
Action: "sts:AssumeRole",
|
|
506
|
+
}],
|
|
507
|
+
},
|
|
508
|
+
});
|
|
509
|
+
`;
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
];
|
|
513
|
+
},
|
|
514
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
AWS,
|
|
4
|
+
StackName,
|
|
5
|
+
Region,
|
|
6
|
+
AccountId,
|
|
7
|
+
StackId,
|
|
8
|
+
URLSuffix,
|
|
9
|
+
NoValue,
|
|
10
|
+
NotificationARNs,
|
|
11
|
+
Partition,
|
|
12
|
+
} from "./pseudo";
|
|
13
|
+
|
|
14
|
+
describe("Pseudo-parameters", () => {
|
|
15
|
+
test.each([
|
|
16
|
+
{ param: StackName, name: "StackName" },
|
|
17
|
+
{ param: Region, name: "Region" },
|
|
18
|
+
{ param: AccountId, name: "AccountId" },
|
|
19
|
+
{ param: StackId, name: "StackId" },
|
|
20
|
+
{ param: URLSuffix, name: "URLSuffix" },
|
|
21
|
+
{ param: NoValue, name: "NoValue" },
|
|
22
|
+
{ param: NotificationARNs, name: "NotificationARNs" },
|
|
23
|
+
{ param: Partition, name: "Partition" },
|
|
24
|
+
])("$name serializes correctly", ({ param, name }) => {
|
|
25
|
+
expect(param.toJSON()).toEqual({ Ref: `AWS::${name}` });
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("AWS namespace", () => {
|
|
30
|
+
test("contains all pseudo-parameters", () => {
|
|
31
|
+
expect(AWS.StackName).toBe(StackName);
|
|
32
|
+
expect(AWS.Region).toBe(Region);
|
|
33
|
+
expect(AWS.AccountId).toBe(AccountId);
|
|
34
|
+
expect(AWS.StackId).toBe(StackId);
|
|
35
|
+
expect(AWS.URLSuffix).toBe(URLSuffix);
|
|
36
|
+
expect(AWS.NoValue).toBe(NoValue);
|
|
37
|
+
expect(AWS.NotificationARNs).toBe(NotificationARNs);
|
|
38
|
+
expect(AWS.Partition).toBe(Partition);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("pseudo-parameters are accessible via AWS namespace", () => {
|
|
42
|
+
expect(AWS.StackName.toJSON()).toEqual({ Ref: "AWS::StackName" });
|
|
43
|
+
expect(AWS.Region.toJSON()).toEqual({ Ref: "AWS::Region" });
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("toString", () => {
|
|
48
|
+
test("StackName toString for Sub templates", () => {
|
|
49
|
+
expect(StackName.toString()).toBe("${AWS::StackName}");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("Region toString for Sub templates", () => {
|
|
53
|
+
expect(Region.toString()).toBe("${AWS::Region}");
|
|
54
|
+
});
|
|
55
|
+
});
|
package/src/pseudo.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { PseudoParameter, createPseudoParameters } from "@intentius/chant/pseudo-parameter";
|
|
2
|
+
|
|
3
|
+
export { PseudoParameter };
|
|
4
|
+
|
|
5
|
+
export const { StackName, Region, AccountId, StackId, URLSuffix, NoValue, NotificationARNs, Partition } =
|
|
6
|
+
createPseudoParameters({
|
|
7
|
+
StackName: "AWS::StackName",
|
|
8
|
+
Region: "AWS::Region",
|
|
9
|
+
AccountId: "AWS::AccountId",
|
|
10
|
+
StackId: "AWS::StackId",
|
|
11
|
+
URLSuffix: "AWS::URLSuffix",
|
|
12
|
+
NoValue: "AWS::NoValue",
|
|
13
|
+
NotificationARNs: "AWS::NotificationARNs",
|
|
14
|
+
Partition: "AWS::Partition",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* AWS namespace containing all pseudo-parameters
|
|
19
|
+
*/
|
|
20
|
+
export const AWS = {
|
|
21
|
+
StackName,
|
|
22
|
+
Region,
|
|
23
|
+
AccountId,
|
|
24
|
+
StackId,
|
|
25
|
+
URLSuffix,
|
|
26
|
+
NoValue,
|
|
27
|
+
NotificationARNs,
|
|
28
|
+
Partition,
|
|
29
|
+
} as const;
|