@intentius/chant-lexicon-aws 0.0.8 → 0.0.9

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 (115) hide show
  1. package/dist/integrity.json +25 -10
  2. package/dist/manifest.json +1 -1
  3. package/dist/meta.json +5743 -896
  4. package/dist/rules/cf-refs.ts +99 -0
  5. package/dist/rules/ext001.ts +30 -21
  6. package/dist/rules/hardcoded-region.ts +1 -0
  7. package/dist/rules/iam-wildcard.ts +1 -0
  8. package/dist/rules/s3-encryption.ts +1 -0
  9. package/dist/rules/waw016.ts +86 -0
  10. package/dist/rules/waw017.ts +53 -0
  11. package/dist/rules/waw018.ts +71 -0
  12. package/dist/rules/waw019.ts +82 -0
  13. package/dist/rules/waw020.ts +64 -0
  14. package/dist/rules/waw021.ts +53 -0
  15. package/dist/rules/waw022.ts +43 -0
  16. package/dist/rules/waw023.ts +47 -0
  17. package/dist/rules/waw024.ts +54 -0
  18. package/dist/rules/waw025.ts +43 -0
  19. package/dist/rules/waw026.ts +46 -0
  20. package/dist/rules/waw027.ts +50 -0
  21. package/dist/rules/waw028.ts +47 -0
  22. package/dist/rules/waw029.ts +62 -0
  23. package/dist/rules/waw030.ts +246 -0
  24. package/dist/skills/chant-aws.md +388 -30
  25. package/dist/types/index.d.ts +1552 -1528
  26. package/package.json +2 -2
  27. package/src/actions/actions.test.ts +75 -0
  28. package/src/actions/dynamodb.ts +36 -0
  29. package/src/actions/ecr.ts +9 -0
  30. package/src/actions/ecs.ts +5 -0
  31. package/src/actions/iam.ts +3 -0
  32. package/src/actions/index.ts +9 -0
  33. package/src/actions/lambda.ts +11 -0
  34. package/src/actions/logs.ts +4 -0
  35. package/src/actions/s3.ts +34 -0
  36. package/src/actions/sns.ts +5 -0
  37. package/src/actions/sqs.ts +15 -0
  38. package/src/codegen/__snapshots__/snapshot.test.ts.snap +2 -2
  39. package/src/codegen/docs-links.test.ts +143 -0
  40. package/src/codegen/docs.ts +247 -132
  41. package/src/codegen/generate-lexicon.ts +8 -0
  42. package/src/codegen/generate-typescript.ts +25 -1
  43. package/src/composites/composites.test.ts +442 -0
  44. package/src/composites/fargate-alb.ts +253 -0
  45. package/src/composites/index.ts +20 -0
  46. package/src/composites/lambda-api.ts +20 -0
  47. package/src/composites/lambda-dynamodb.ts +64 -0
  48. package/src/composites/lambda-eventbridge.ts +36 -0
  49. package/src/composites/lambda-function.ts +76 -0
  50. package/src/composites/lambda-s3.ts +72 -0
  51. package/src/composites/lambda-sns.ts +30 -0
  52. package/src/composites/lambda-sqs.ts +44 -0
  53. package/src/composites/scheduled-lambda.ts +37 -0
  54. package/src/composites/vpc-default.ts +148 -0
  55. package/src/default-tags.test.ts +38 -0
  56. package/src/default-tags.ts +77 -0
  57. package/src/generated/index.d.ts +1552 -1528
  58. package/src/generated/lexicon-aws.json +5743 -896
  59. package/src/import/roundtrip-fixtures.test.ts +1 -1
  60. package/src/index.ts +21 -0
  61. package/src/integration.test.ts +71 -0
  62. package/src/intrinsics.ts +24 -13
  63. package/src/lint/post-synth/cf-refs.ts +99 -0
  64. package/src/lint/post-synth/ext001.test.ts +214 -31
  65. package/src/lint/post-synth/ext001.ts +30 -21
  66. package/src/lint/post-synth/waw013.test.ts +120 -0
  67. package/src/lint/post-synth/waw014.test.ts +121 -0
  68. package/src/lint/post-synth/waw015.test.ts +147 -0
  69. package/src/lint/post-synth/waw016.test.ts +141 -0
  70. package/src/lint/post-synth/waw016.ts +86 -0
  71. package/src/lint/post-synth/waw017.test.ts +130 -0
  72. package/src/lint/post-synth/waw017.ts +53 -0
  73. package/src/lint/post-synth/waw018.test.ts +109 -0
  74. package/src/lint/post-synth/waw018.ts +71 -0
  75. package/src/lint/post-synth/waw019.test.ts +138 -0
  76. package/src/lint/post-synth/waw019.ts +82 -0
  77. package/src/lint/post-synth/waw020.test.ts +125 -0
  78. package/src/lint/post-synth/waw020.ts +64 -0
  79. package/src/lint/post-synth/waw021.test.ts +81 -0
  80. package/src/lint/post-synth/waw021.ts +53 -0
  81. package/src/lint/post-synth/waw022.test.ts +54 -0
  82. package/src/lint/post-synth/waw022.ts +43 -0
  83. package/src/lint/post-synth/waw023.test.ts +53 -0
  84. package/src/lint/post-synth/waw023.ts +47 -0
  85. package/src/lint/post-synth/waw024.test.ts +64 -0
  86. package/src/lint/post-synth/waw024.ts +54 -0
  87. package/src/lint/post-synth/waw025.test.ts +42 -0
  88. package/src/lint/post-synth/waw025.ts +43 -0
  89. package/src/lint/post-synth/waw026.test.ts +54 -0
  90. package/src/lint/post-synth/waw026.ts +46 -0
  91. package/src/lint/post-synth/waw027.test.ts +63 -0
  92. package/src/lint/post-synth/waw027.ts +50 -0
  93. package/src/lint/post-synth/waw028.test.ts +68 -0
  94. package/src/lint/post-synth/waw028.ts +47 -0
  95. package/src/lint/post-synth/waw029.test.ts +179 -0
  96. package/src/lint/post-synth/waw029.ts +62 -0
  97. package/src/lint/post-synth/waw030.test.ts +800 -0
  98. package/src/lint/post-synth/waw030.ts +246 -0
  99. package/src/lint/rules/hardcoded-region.ts +1 -0
  100. package/src/lint/rules/iam-wildcard.ts +1 -0
  101. package/src/lint/rules/s3-encryption.ts +1 -0
  102. package/src/lsp/hover.ts +15 -0
  103. package/src/nested-stack-integration.test.ts +100 -0
  104. package/src/nested-stack.ts +1 -1
  105. package/src/plugin.ts +468 -36
  106. package/src/serializer.test.ts +330 -2
  107. package/src/serializer.ts +62 -1
  108. package/src/spec/fetch.ts +10 -0
  109. package/src/spec/parse.test.ts +141 -0
  110. package/src/spec/parse.ts +40 -0
  111. package/src/taggable.ts +44 -0
  112. package/src/testdata/nested-stacks/app.ts +26 -0
  113. package/src/testdata/nested-stacks/network/outputs.ts +17 -0
  114. package/src/testdata/nested-stacks/network/security.ts +17 -0
  115. package/src/testdata/nested-stacks/network/vpc.ts +54 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-aws",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "files": ["src/", "dist/"],
@@ -22,7 +22,7 @@
22
22
  "prepack": "bun run bundle && bun run validate"
23
23
  },
24
24
  "dependencies": {
25
- "@intentius/chant": "0.0.5",
25
+ "@intentius/chant": "0.0.9",
26
26
  "fflate": "^0.8.2",
27
27
  "js-yaml": "^4.1.0"
28
28
  },
@@ -0,0 +1,75 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { S3Actions } from "./s3";
3
+ import { LambdaActions } from "./lambda";
4
+ import { DynamoDBActions } from "./dynamodb";
5
+ import { SQSActions } from "./sqs";
6
+ import { SNSActions } from "./sns";
7
+ import { IAMActions } from "./iam";
8
+ import { ECRActions } from "./ecr";
9
+ import { LogsActions } from "./logs";
10
+ import { ECSActions } from "./ecs";
11
+
12
+ const allConstants = {
13
+ S3Actions,
14
+ LambdaActions,
15
+ DynamoDBActions,
16
+ SQSActions,
17
+ SNSActions,
18
+ IAMActions,
19
+ ECRActions,
20
+ LogsActions,
21
+ ECSActions,
22
+ };
23
+
24
+ describe("Action Constants", () => {
25
+ for (const [name, constant] of Object.entries(allConstants)) {
26
+ describe(name, () => {
27
+ test("every action string matches serviceName:actionName pattern", () => {
28
+ for (const [group, actions] of Object.entries(constant)) {
29
+ for (const action of actions) {
30
+ expect(action).toMatch(
31
+ /^[a-z][a-z0-9]*:[A-Z*][A-Za-z0-9*]*$/,
32
+ );
33
+ }
34
+ }
35
+ });
36
+
37
+ test("no duplicate actions within a group", () => {
38
+ for (const [group, actions] of Object.entries(constant)) {
39
+ const unique = new Set(actions);
40
+ expect(unique.size).toBe(
41
+ actions.length,
42
+ );
43
+ }
44
+ });
45
+ });
46
+ }
47
+
48
+ describe("S3Actions broad groups are supersets", () => {
49
+ test("ReadWrite contains all ReadOnly actions", () => {
50
+ for (const action of S3Actions.ReadOnly) {
51
+ expect(S3Actions.ReadWrite).toContain(action);
52
+ }
53
+ });
54
+
55
+ test("ReadWrite contains all WriteOnly actions", () => {
56
+ for (const action of S3Actions.WriteOnly) {
57
+ expect(S3Actions.ReadWrite).toContain(action);
58
+ }
59
+ });
60
+ });
61
+
62
+ describe("DynamoDBActions broad groups are supersets", () => {
63
+ test("ReadWrite contains all ReadOnly actions", () => {
64
+ for (const action of DynamoDBActions.ReadOnly) {
65
+ expect(DynamoDBActions.ReadWrite).toContain(action);
66
+ }
67
+ });
68
+
69
+ test("ReadWrite contains all WriteOnly actions", () => {
70
+ for (const action of DynamoDBActions.WriteOnly) {
71
+ expect(DynamoDBActions.ReadWrite).toContain(action);
72
+ }
73
+ });
74
+ });
75
+ });
@@ -0,0 +1,36 @@
1
+ export const DynamoDBActions = {
2
+ // Broad groups
3
+ ReadOnly: [
4
+ "dynamodb:GetItem",
5
+ "dynamodb:BatchGetItem",
6
+ "dynamodb:Query",
7
+ "dynamodb:Scan",
8
+ "dynamodb:DescribeTable",
9
+ "dynamodb:ConditionCheckItem",
10
+ ],
11
+ WriteOnly: [
12
+ "dynamodb:PutItem",
13
+ "dynamodb:UpdateItem",
14
+ "dynamodb:DeleteItem",
15
+ "dynamodb:BatchWriteItem",
16
+ ],
17
+ ReadWrite: [
18
+ "dynamodb:GetItem",
19
+ "dynamodb:BatchGetItem",
20
+ "dynamodb:Query",
21
+ "dynamodb:Scan",
22
+ "dynamodb:DescribeTable",
23
+ "dynamodb:ConditionCheckItem",
24
+ "dynamodb:PutItem",
25
+ "dynamodb:UpdateItem",
26
+ "dynamodb:DeleteItem",
27
+ "dynamodb:BatchWriteItem",
28
+ ],
29
+ Full: ["dynamodb:*"],
30
+
31
+ // Operation-specific
32
+ GetItem: ["dynamodb:GetItem", "dynamodb:BatchGetItem"],
33
+ PutItem: ["dynamodb:PutItem", "dynamodb:BatchWriteItem"],
34
+ Query: ["dynamodb:Query"],
35
+ Scan: ["dynamodb:Scan"],
36
+ } as const;
@@ -0,0 +1,9 @@
1
+ export const ECRActions = {
2
+ Pull: [
3
+ "ecr:GetAuthorizationToken",
4
+ "ecr:BatchCheckLayerAvailability",
5
+ "ecr:GetDownloadUrlForLayer",
6
+ "ecr:BatchGetImage",
7
+ ],
8
+ Full: ["ecr:*"],
9
+ } as const;
@@ -0,0 +1,5 @@
1
+ export const ECSActions = {
2
+ RunTask: ["ecs:RunTask", "ecs:StopTask", "ecs:DescribeTasks"],
3
+ Service: ["ecs:CreateService", "ecs:UpdateService", "ecs:DeleteService", "ecs:DescribeServices"],
4
+ Full: ["ecs:*"],
5
+ } as const;
@@ -0,0 +1,3 @@
1
+ export const IAMActions = {
2
+ PassRole: ["iam:PassRole"],
3
+ } as const;
@@ -0,0 +1,9 @@
1
+ export { S3Actions } from "./s3";
2
+ export { LambdaActions } from "./lambda";
3
+ export { DynamoDBActions } from "./dynamodb";
4
+ export { SQSActions } from "./sqs";
5
+ export { SNSActions } from "./sns";
6
+ export { IAMActions } from "./iam";
7
+ export { ECRActions } from "./ecr";
8
+ export { LogsActions } from "./logs";
9
+ export { ECSActions } from "./ecs";
@@ -0,0 +1,11 @@
1
+ export const LambdaActions = {
2
+ Invoke: ["lambda:InvokeFunction", "lambda:InvokeAsync"],
3
+ ReadOnly: [
4
+ "lambda:GetFunction",
5
+ "lambda:GetFunctionConfiguration",
6
+ "lambda:GetPolicy",
7
+ "lambda:ListVersionsByFunction",
8
+ "lambda:ListAliases",
9
+ ],
10
+ Full: ["lambda:*"],
11
+ } as const;
@@ -0,0 +1,4 @@
1
+ export const LogsActions = {
2
+ Write: ["logs:CreateLogStream", "logs:PutLogEvents"],
3
+ Full: ["logs:*"],
4
+ } as const;
@@ -0,0 +1,34 @@
1
+ export const S3Actions = {
2
+ // Broad groups (from AWS managed policies)
3
+ ReadOnly: [
4
+ "s3:GetObject",
5
+ "s3:GetObjectVersion",
6
+ "s3:GetBucketLocation",
7
+ "s3:ListBucket",
8
+ "s3:ListBucketVersions",
9
+ ],
10
+ WriteOnly: [
11
+ "s3:PutObject",
12
+ "s3:DeleteObject",
13
+ "s3:PutObjectAcl",
14
+ "s3:AbortMultipartUpload",
15
+ ],
16
+ ReadWrite: [
17
+ "s3:GetObject",
18
+ "s3:GetObjectVersion",
19
+ "s3:GetBucketLocation",
20
+ "s3:ListBucket",
21
+ "s3:ListBucketVersions",
22
+ "s3:PutObject",
23
+ "s3:DeleteObject",
24
+ "s3:PutObjectAcl",
25
+ "s3:AbortMultipartUpload",
26
+ ],
27
+ Full: ["s3:*"],
28
+
29
+ // Operation-specific
30
+ GetObject: ["s3:GetObject", "s3:GetObjectVersion"],
31
+ PutObject: ["s3:PutObject", "s3:AbortMultipartUpload"],
32
+ DeleteObject: ["s3:DeleteObject", "s3:DeleteObjectVersion"],
33
+ ListObjects: ["s3:ListBucket", "s3:ListBucketVersions"],
34
+ } as const;
@@ -0,0 +1,5 @@
1
+ export const SNSActions = {
2
+ Publish: ["sns:Publish"],
3
+ Subscribe: ["sns:Subscribe", "sns:Unsubscribe"],
4
+ Full: ["sns:*"],
5
+ } as const;
@@ -0,0 +1,15 @@
1
+ export const SQSActions = {
2
+ SendMessage: [
3
+ "sqs:SendMessage",
4
+ "sqs:GetQueueUrl",
5
+ "sqs:GetQueueAttributes",
6
+ ],
7
+ ReceiveMessage: [
8
+ "sqs:ReceiveMessage",
9
+ "sqs:DeleteMessage",
10
+ "sqs:ChangeMessageVisibility",
11
+ "sqs:GetQueueUrl",
12
+ "sqs:GetQueueAttributes",
13
+ ],
14
+ Full: ["sqs:*"],
15
+ } as const;
@@ -169,7 +169,7 @@ exports[`snapshot tests Bucket .d.ts class declaration 1`] = `
169
169
  Tags?: Bucket_Tag[];
170
170
  /** Enables multiple versions of all objects in this bucket. */
171
171
  VersioningConfiguration?: Bucket_VersioningConfiguration;
172
- });
172
+ }, attributes?: CFResourceAttributes);
173
173
  readonly Arn: string;
174
174
  }"
175
175
  `;
@@ -191,7 +191,7 @@ exports[`snapshot tests Function .d.ts class declaration 1`] = `
191
191
  MemorySize?: number;
192
192
  /** The identifier of the function's runtime. */
193
193
  Runtime?: "java17" | "java21" | "nodejs18.x" | "nodejs20.x" | "python3.11" | "python3.12";
194
- });
194
+ }, attributes?: CFResourceAttributes);
195
195
  readonly Arn: string;
196
196
  }"
197
197
  `;
@@ -0,0 +1,143 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { existsSync, readdirSync, readFileSync } from "fs";
3
+ import { join, basename } from "path";
4
+
5
+ const docsDir = join(import.meta.dir, "..", "..", "docs", "src", "content", "docs");
6
+ const docsSource = join(import.meta.dir, "docs.ts");
7
+ const docsExist = existsSync(docsDir);
8
+
9
+ /**
10
+ * Collect all page slugs from the generated docs directory.
11
+ */
12
+ function getPageSlugs(): Set<string> {
13
+ if (!docsExist) return new Set();
14
+ const slugs = new Set<string>();
15
+ for (const file of readdirSync(docsDir)) {
16
+ if (file.endsWith(".mdx")) {
17
+ slugs.add(basename(file, ".mdx"));
18
+ }
19
+ }
20
+ return slugs;
21
+ }
22
+
23
+ /**
24
+ * Extract markdown links: [text](href)
25
+ */
26
+ function extractMarkdownLinks(content: string): Array<{ text: string; href: string; line: number }> {
27
+ const links: Array<{ text: string; href: string; line: number }> = [];
28
+ const lines = content.split("\n");
29
+ for (let i = 0; i < lines.length; i++) {
30
+ const regex = /\[([^\]]+)\]\(([^)]+)\)/g;
31
+ let match;
32
+ while ((match = regex.exec(lines[i])) !== null) {
33
+ const href = match[2];
34
+ if (href.startsWith("http://") || href.startsWith("https://")) continue;
35
+ if (href.startsWith("#")) continue;
36
+ links.push({ text: match[1], href, line: i + 1 });
37
+ }
38
+ }
39
+ return links;
40
+ }
41
+
42
+ /**
43
+ * Check if a relative link target exists as a page slug.
44
+ */
45
+ function resolveTarget(href: string, slugs: Set<string>): string | null {
46
+ const pathPart = href.split("#")[0].replace(/\/$/, "");
47
+ if (!pathPart) return null;
48
+
49
+ let target: string | undefined;
50
+ if (pathPart.startsWith("./")) target = pathPart.slice(2);
51
+ else if (pathPart.startsWith("../")) target = pathPart.slice(3);
52
+ else if (pathPart.startsWith("/chant/lexicons/aws/")) {
53
+ target = pathPart.replace("/chant/lexicons/aws/", "").replace(/\/$/, "") || "index";
54
+ } else if (!pathPart.includes("/") && !pathPart.startsWith(".")) {
55
+ target = pathPart;
56
+ }
57
+
58
+ if (target === undefined) return null;
59
+ return slugs.has(target) ? null : `target page "${target}" does not exist`;
60
+ }
61
+
62
+ describe("docs internal links", () => {
63
+ const slugs = getPageSlugs();
64
+
65
+ test("page slugs are discovered", () => {
66
+ if (!docsExist) return; // generated docs not present (e.g. CI)
67
+ expect(slugs.size).toBeGreaterThan(5);
68
+ expect(slugs.has("composites")).toBe(true);
69
+ expect(slugs.has("nested-stacks")).toBe(true);
70
+ expect(slugs.has("index")).toBe(true);
71
+ });
72
+
73
+ // Validate generated MDX files (skip if docs not generated)
74
+ for (const file of (docsExist ? readdirSync(docsDir) : [])) {
75
+ if (!file.endsWith(".mdx")) continue;
76
+ const slug = basename(file, ".mdx");
77
+
78
+ test(`${slug}.mdx — internal links resolve to existing pages`, () => {
79
+ const content = readFileSync(join(docsDir, file), "utf-8");
80
+ const links = extractMarkdownLinks(content);
81
+ const errors: string[] = [];
82
+ for (const link of links) {
83
+ const error = resolveTarget(link.href, slugs);
84
+ if (error) errors.push(`line ${link.line}: [${link.text}](${link.href}) — ${error}`);
85
+ }
86
+ if (errors.length > 0) {
87
+ throw new Error(`Broken links in ${file}:\n${errors.join("\n")}`);
88
+ }
89
+ });
90
+
91
+ test(`${slug}.mdx — non-index pages use ../ not ./ for cross-page links`, () => {
92
+ if (slug === "index") return;
93
+ const content = readFileSync(join(docsDir, file), "utf-8");
94
+ const links = extractMarkdownLinks(content);
95
+ const errors: string[] = [];
96
+ for (const link of links) {
97
+ const pathPart = link.href.split("#")[0];
98
+ if (pathPart.startsWith("./")) {
99
+ const target = pathPart.slice(2).replace(/\/$/, "");
100
+ if (slugs.has(target)) {
101
+ errors.push(`line ${link.line}: [${link.text}](${link.href}) — use "../${target}/" instead`);
102
+ }
103
+ }
104
+ }
105
+ if (errors.length > 0) {
106
+ throw new Error(`Broken ./ links in non-index page ${file}:\n${errors.join("\n")}`);
107
+ }
108
+ });
109
+ }
110
+
111
+ // Validate source docs.ts — catches broken links before regeneration
112
+ test("docs.ts source — cross-page links use ../ not ./", () => {
113
+ if (!docsExist) return; // needs generated slugs for validation
114
+ const content = readFileSync(docsSource, "utf-8");
115
+ const links = extractMarkdownLinks(content);
116
+ const errors: string[] = [];
117
+ for (const link of links) {
118
+ const pathPart = link.href.split("#")[0];
119
+ // Links in docs.ts extraPages are rendered on non-index pages,
120
+ // so they must use ../ to navigate to sibling pages
121
+ if (pathPart.startsWith("./") && slugs.has(pathPart.slice(2).replace(/\/$/, ""))) {
122
+ errors.push(`line ${link.line}: [${link.text}](${link.href}) — use "../" prefix for cross-page links`);
123
+ }
124
+ }
125
+ if (errors.length > 0) {
126
+ throw new Error(`docs.ts has ./ links that will break on non-index pages:\n${errors.join("\n")}`);
127
+ }
128
+ });
129
+
130
+ test("docs.ts source — link targets exist as pages", () => {
131
+ if (!docsExist) return; // needs generated slugs for validation
132
+ const content = readFileSync(docsSource, "utf-8");
133
+ const links = extractMarkdownLinks(content);
134
+ const errors: string[] = [];
135
+ for (const link of links) {
136
+ const error = resolveTarget(link.href, slugs);
137
+ if (error) errors.push(`line ${link.line}: [${link.text}](${link.href}) — ${error}`);
138
+ }
139
+ if (errors.length > 0) {
140
+ throw new Error(`docs.ts has links to non-existent pages:\n${errors.join("\n")}`);
141
+ }
142
+ });
143
+ });