@intentius/chant-lexicon-gcp 0.0.15

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 (122) hide show
  1. package/dist/integrity.json +36 -0
  2. package/dist/manifest.json +12 -0
  3. package/dist/meta.json +10919 -0
  4. package/dist/rules/gcp-helpers.ts +117 -0
  5. package/dist/rules/hardcoded-project.ts +58 -0
  6. package/dist/rules/hardcoded-region.ts +56 -0
  7. package/dist/rules/public-iam.ts +43 -0
  8. package/dist/rules/wgc101.ts +56 -0
  9. package/dist/rules/wgc102.ts +35 -0
  10. package/dist/rules/wgc103.ts +45 -0
  11. package/dist/rules/wgc104.ts +42 -0
  12. package/dist/rules/wgc105.ts +46 -0
  13. package/dist/rules/wgc106.ts +36 -0
  14. package/dist/rules/wgc107.ts +39 -0
  15. package/dist/rules/wgc108.ts +41 -0
  16. package/dist/rules/wgc109.ts +39 -0
  17. package/dist/rules/wgc110.ts +38 -0
  18. package/dist/rules/wgc111.ts +54 -0
  19. package/dist/rules/wgc112.ts +56 -0
  20. package/dist/rules/wgc113.ts +42 -0
  21. package/dist/rules/wgc201.ts +36 -0
  22. package/dist/rules/wgc202.ts +39 -0
  23. package/dist/rules/wgc203.ts +44 -0
  24. package/dist/rules/wgc204.ts +39 -0
  25. package/dist/rules/wgc301.ts +34 -0
  26. package/dist/rules/wgc302.ts +34 -0
  27. package/dist/rules/wgc303.ts +37 -0
  28. package/dist/skills/chant-gcp-patterns.md +367 -0
  29. package/dist/skills/chant-gcp-security.md +276 -0
  30. package/dist/skills/chant-gcp.md +108 -0
  31. package/dist/types/index.d.ts +26529 -0
  32. package/package.json +35 -0
  33. package/src/actions/index.ts +52 -0
  34. package/src/codegen/docs-cli.ts +7 -0
  35. package/src/codegen/docs.ts +820 -0
  36. package/src/codegen/generate-cli.ts +24 -0
  37. package/src/codegen/generate.ts +252 -0
  38. package/src/codegen/naming.test.ts +49 -0
  39. package/src/codegen/naming.ts +132 -0
  40. package/src/codegen/package.ts +66 -0
  41. package/src/composites/cloud-function.ts +117 -0
  42. package/src/composites/cloud-run-service.ts +124 -0
  43. package/src/composites/cloud-sql-instance.ts +126 -0
  44. package/src/composites/composites.test.ts +432 -0
  45. package/src/composites/gcs-bucket.ts +111 -0
  46. package/src/composites/gke-cluster.ts +125 -0
  47. package/src/composites/index.ts +20 -0
  48. package/src/composites/managed-certificate.ts +79 -0
  49. package/src/composites/private-service.ts +95 -0
  50. package/src/composites/pubsub-pipeline.ts +102 -0
  51. package/src/composites/secure-project.ts +128 -0
  52. package/src/composites/vpc-network.ts +165 -0
  53. package/src/coverage.test.ts +27 -0
  54. package/src/coverage.ts +51 -0
  55. package/src/default-labels.test.ts +111 -0
  56. package/src/default-labels.ts +93 -0
  57. package/src/generated/index.d.ts +26529 -0
  58. package/src/generated/index.ts +1723 -0
  59. package/src/generated/lexicon-gcp.json +10919 -0
  60. package/src/generated/runtime.ts +4 -0
  61. package/src/import/generator.test.ts +125 -0
  62. package/src/import/generator.ts +82 -0
  63. package/src/import/parser.test.ts +167 -0
  64. package/src/import/parser.ts +80 -0
  65. package/src/import/roundtrip.test.ts +66 -0
  66. package/src/index.ts +54 -0
  67. package/src/lint/post-synth/gcp-helpers.ts +117 -0
  68. package/src/lint/post-synth/index.ts +20 -0
  69. package/src/lint/post-synth/post-synth.test.ts +693 -0
  70. package/src/lint/post-synth/wgc101.ts +56 -0
  71. package/src/lint/post-synth/wgc102.ts +35 -0
  72. package/src/lint/post-synth/wgc103.ts +45 -0
  73. package/src/lint/post-synth/wgc104.ts +42 -0
  74. package/src/lint/post-synth/wgc105.ts +46 -0
  75. package/src/lint/post-synth/wgc106.ts +36 -0
  76. package/src/lint/post-synth/wgc107.ts +39 -0
  77. package/src/lint/post-synth/wgc108.ts +41 -0
  78. package/src/lint/post-synth/wgc109.ts +39 -0
  79. package/src/lint/post-synth/wgc110.ts +38 -0
  80. package/src/lint/post-synth/wgc111.ts +54 -0
  81. package/src/lint/post-synth/wgc112.ts +56 -0
  82. package/src/lint/post-synth/wgc113.ts +42 -0
  83. package/src/lint/post-synth/wgc201.ts +36 -0
  84. package/src/lint/post-synth/wgc202.ts +39 -0
  85. package/src/lint/post-synth/wgc203.ts +44 -0
  86. package/src/lint/post-synth/wgc204.ts +39 -0
  87. package/src/lint/post-synth/wgc301.ts +34 -0
  88. package/src/lint/post-synth/wgc302.ts +34 -0
  89. package/src/lint/post-synth/wgc303.ts +37 -0
  90. package/src/lint/rules/hardcoded-project.ts +58 -0
  91. package/src/lint/rules/hardcoded-region.ts +56 -0
  92. package/src/lint/rules/index.ts +3 -0
  93. package/src/lint/rules/public-iam.ts +43 -0
  94. package/src/lint/rules/rules.test.ts +63 -0
  95. package/src/lsp/completions.test.ts +67 -0
  96. package/src/lsp/completions.ts +17 -0
  97. package/src/lsp/hover.test.ts +66 -0
  98. package/src/lsp/hover.ts +54 -0
  99. package/src/package-cli.ts +24 -0
  100. package/src/plugin.test.ts +250 -0
  101. package/src/plugin.ts +405 -0
  102. package/src/pseudo.test.ts +40 -0
  103. package/src/pseudo.ts +19 -0
  104. package/src/serializer.test.ts +250 -0
  105. package/src/serializer.ts +232 -0
  106. package/src/skills/chant-gcp-patterns.md +367 -0
  107. package/src/skills/chant-gcp-security.md +276 -0
  108. package/src/skills/chant-gcp.md +108 -0
  109. package/src/spec/fetch.test.ts +16 -0
  110. package/src/spec/fetch.ts +121 -0
  111. package/src/spec/parse.test.ts +163 -0
  112. package/src/spec/parse.ts +432 -0
  113. package/src/testdata/compute-instance.yaml +93 -0
  114. package/src/testdata/iam-policy-member.yaml +66 -0
  115. package/src/testdata/manifests/compute-instance.yaml +18 -0
  116. package/src/testdata/manifests/full-app.yaml +34 -0
  117. package/src/testdata/manifests/storage-bucket.yaml +12 -0
  118. package/src/testdata/storage-bucket.yaml +100 -0
  119. package/src/validate-cli.ts +13 -0
  120. package/src/validate.test.ts +38 -0
  121. package/src/validate.ts +30 -0
  122. package/src/variables.ts +15 -0
@@ -0,0 +1,44 @@
1
+ /**
2
+ * WGC203: GKE node pool with cloud-platform OAuth scope
3
+ */
4
+
5
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
6
+ import { parseGcpManifests, getSpec, getResourceName } from "./gcp-helpers";
7
+
8
+ export const wgc203: PostSynthCheck = {
9
+ id: "WGC203",
10
+ description: "ContainerNodePool using overly broad cloud-platform OAuth scope",
11
+
12
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
13
+ const diagnostics: PostSynthDiagnostic[] = [];
14
+
15
+ for (const [, output] of ctx.outputs) {
16
+ if (typeof output !== "string") continue;
17
+
18
+ for (const manifest of parseGcpManifests(output)) {
19
+ if (manifest.kind !== "ContainerNodePool") continue;
20
+
21
+ const spec = getSpec(manifest);
22
+ if (!spec) continue;
23
+
24
+ const nodeConfig = spec.nodeConfig as Record<string, unknown> | undefined;
25
+ const oauthScopes = nodeConfig?.oauthScopes as string[] | undefined;
26
+
27
+ if (Array.isArray(oauthScopes) && oauthScopes.some(s =>
28
+ (typeof s === "string" && s.includes("cloud-platform")) ||
29
+ (typeof s === "object" && s !== null && JSON.stringify(s).includes("cloud-platform")),
30
+ )) {
31
+ diagnostics.push({
32
+ checkId: "WGC203",
33
+ severity: "warning",
34
+ message: `ContainerNodePool "${getResourceName(manifest)}" uses cloud-platform OAuth scope — this grants overly broad access; prefer workload identity with fine-grained IAM`,
35
+ entity: getResourceName(manifest),
36
+ lexicon: "gcp",
37
+ });
38
+ }
39
+ }
40
+ }
41
+
42
+ return diagnostics;
43
+ },
44
+ };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * WGC204: ComputeInstance without shielded VM config
3
+ */
4
+
5
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
6
+ import { parseGcpManifests, getSpec, getResourceName } from "./gcp-helpers";
7
+
8
+ export const wgc204: PostSynthCheck = {
9
+ id: "WGC204",
10
+ description: "ComputeInstance without shielded VM configuration",
11
+
12
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
13
+ const diagnostics: PostSynthDiagnostic[] = [];
14
+
15
+ for (const [, output] of ctx.outputs) {
16
+ if (typeof output !== "string") continue;
17
+
18
+ for (const manifest of parseGcpManifests(output)) {
19
+ if (manifest.kind !== "ComputeInstance") continue;
20
+
21
+ const spec = getSpec(manifest);
22
+ if (!spec) continue;
23
+
24
+ const shieldedConfig = spec.shieldedInstanceConfig as Record<string, unknown> | undefined;
25
+ if (!shieldedConfig) {
26
+ diagnostics.push({
27
+ checkId: "WGC204",
28
+ severity: "info",
29
+ message: `ComputeInstance "${getResourceName(manifest)}" has no shielded VM configuration — consider enabling secureboot, vtpm, and integrity monitoring`,
30
+ entity: getResourceName(manifest),
31
+ lexicon: "gcp",
32
+ });
33
+ }
34
+ }
35
+ }
36
+
37
+ return diagnostics;
38
+ },
39
+ };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * WGC301: No audit logging config (IAMAuditConfig) in output
3
+ */
4
+
5
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
6
+ import { parseGcpManifests } from "./gcp-helpers";
7
+
8
+ export const wgc301: PostSynthCheck = {
9
+ id: "WGC301",
10
+ description: "No IAMAuditConfig resource found in output — audit logging may not be configured",
11
+
12
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
13
+ const diagnostics: PostSynthDiagnostic[] = [];
14
+
15
+ for (const [, output] of ctx.outputs) {
16
+ if (typeof output !== "string") continue;
17
+
18
+ const manifests = parseGcpManifests(output);
19
+ if (manifests.length === 0) continue;
20
+
21
+ const hasAuditConfig = manifests.some(m => m.kind === "IAMAuditConfig");
22
+ if (!hasAuditConfig) {
23
+ diagnostics.push({
24
+ checkId: "WGC301",
25
+ severity: "info",
26
+ message: "No IAMAuditConfig resource found — consider adding audit logging for compliance",
27
+ lexicon: "gcp",
28
+ });
29
+ }
30
+ }
31
+
32
+ return diagnostics;
33
+ },
34
+ };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * WGC302: Service API not explicitly enabled
3
+ */
4
+
5
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
6
+ import { parseGcpManifests } from "./gcp-helpers";
7
+
8
+ export const wgc302: PostSynthCheck = {
9
+ id: "WGC302",
10
+ description: "No Service resource found — GCP APIs may not be explicitly enabled",
11
+
12
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
13
+ const diagnostics: PostSynthDiagnostic[] = [];
14
+
15
+ for (const [, output] of ctx.outputs) {
16
+ if (typeof output !== "string") continue;
17
+
18
+ const manifests = parseGcpManifests(output);
19
+ if (manifests.length === 0) continue;
20
+
21
+ const hasService = manifests.some(m => m.kind === "Service" && m.apiVersion?.includes("serviceusage.cnrm"));
22
+ if (!hasService) {
23
+ diagnostics.push({
24
+ checkId: "WGC302",
25
+ severity: "info",
26
+ message: "No Service (serviceusage) resource found — consider explicitly enabling required GCP APIs",
27
+ lexicon: "gcp",
28
+ });
29
+ }
30
+ }
31
+
32
+ return diagnostics;
33
+ },
34
+ };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * WGC303: Missing VPC Service Controls perimeter
3
+ */
4
+
5
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
6
+ import { parseGcpManifests } from "./gcp-helpers";
7
+
8
+ export const wgc303: PostSynthCheck = {
9
+ id: "WGC303",
10
+ description: "No AccessContextManager ServicePerimeter found — VPC Service Controls not configured",
11
+
12
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
13
+ const diagnostics: PostSynthDiagnostic[] = [];
14
+
15
+ for (const [, output] of ctx.outputs) {
16
+ if (typeof output !== "string") continue;
17
+
18
+ const manifests = parseGcpManifests(output);
19
+ if (manifests.length === 0) continue;
20
+
21
+ const hasPerimeter = manifests.some(m =>
22
+ m.kind === "AccessContextManagerServicePerimeter" ||
23
+ (m.apiVersion?.includes("accesscontextmanager") && m.kind?.includes("ServicePerimeter")),
24
+ );
25
+ if (!hasPerimeter) {
26
+ diagnostics.push({
27
+ checkId: "WGC303",
28
+ severity: "info",
29
+ message: "No VPC Service Controls perimeter found — consider adding for data exfiltration protection",
30
+ lexicon: "gcp",
31
+ });
32
+ }
33
+ }
34
+
35
+ return diagnostics;
36
+ },
37
+ };
@@ -0,0 +1,58 @@
1
+ import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
2
+ import * as ts from "typescript";
3
+
4
+ /**
5
+ * WGC001: Hardcoded GCP Project ID
6
+ *
7
+ * Detects hardcoded GCP project IDs in code.
8
+ * Project IDs should use GCP.ProjectId pseudo-parameter instead.
9
+ */
10
+ export const hardcodedProjectRule: LintRule = {
11
+ id: "WGC001",
12
+ severity: "warning",
13
+ category: "security",
14
+ description: "Detects hardcoded GCP project IDs — use GCP.ProjectId pseudo-parameter instead",
15
+
16
+ check(context: LintContext): LintDiagnostic[] {
17
+ const { sourceFile } = context;
18
+ const diagnostics: LintDiagnostic[] = [];
19
+
20
+ // GCP project ID pattern: lowercase letters, digits, hyphens, 6-30 chars
21
+ // We look for strings in annotation contexts like "cnrm.cloud.google.com/project-id"
22
+ const projectIdPattern = /^[a-z][a-z0-9-]{4,28}[a-z0-9]$/;
23
+
24
+ // Common false positives
25
+ const falsePositives = new Set([
26
+ "chant", "default", "kube-system", "kube-public", "kube-node-lease",
27
+ "config-connector", "cnrm-system", "my-project",
28
+ ]);
29
+
30
+ function visit(node: ts.Node): void {
31
+ if (ts.isPropertyAssignment(node) || ts.isPropertyDeclaration(node)) {
32
+ const propName = node.name?.getText(sourceFile) ?? "";
33
+ // Only flag project-id annotation values
34
+ if (propName.includes("project-id") || propName.includes("projectId") || propName === '"cnrm.cloud.google.com/project-id"') {
35
+ const initializer = ts.isPropertyAssignment(node) ? node.initializer : undefined;
36
+ if (initializer && ts.isStringLiteral(initializer)) {
37
+ const value = initializer.text;
38
+ if (projectIdPattern.test(value) && !falsePositives.has(value)) {
39
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(initializer.getStart());
40
+ diagnostics.push({
41
+ file: sourceFile.fileName,
42
+ line: line + 1,
43
+ column: character + 1,
44
+ ruleId: "WGC001",
45
+ severity: "warning",
46
+ message: `Hardcoded project ID "${value}" detected. Use GCP.ProjectId pseudo-parameter instead.`,
47
+ });
48
+ }
49
+ }
50
+ }
51
+ }
52
+ ts.forEachChild(node, visit);
53
+ }
54
+
55
+ visit(sourceFile);
56
+ return diagnostics;
57
+ },
58
+ };
@@ -0,0 +1,56 @@
1
+ import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
2
+ import * as ts from "typescript";
3
+
4
+ /**
5
+ * WGC002: Hardcoded GCP Region/Zone
6
+ *
7
+ * Detects hardcoded GCP region and zone strings in code.
8
+ * Regions/zones should use GCP.Region or GCP.Zone pseudo-parameters instead.
9
+ */
10
+ export const hardcodedRegionRule: LintRule = {
11
+ id: "WGC002",
12
+ severity: "warning",
13
+ category: "security",
14
+ description: "Detects hardcoded GCP region/zone strings — use GCP.Region/GCP.Zone pseudo-parameters instead",
15
+
16
+ check(context: LintContext): LintDiagnostic[] {
17
+ const { sourceFile } = context;
18
+ const diagnostics: LintDiagnostic[] = [];
19
+
20
+ // GCP region pattern: continent-direction[N] (e.g., us-central1, europe-west4)
21
+ const regionPattern = /^(us|europe|asia|australia|southamerica|northamerica|me|africa)-(central|east|west|south|north|northeast|southeast|southwest|northwest)\d+$/;
22
+ // GCP zone pattern: region-[a-f] (e.g., us-central1-a)
23
+ const zonePattern = /^(us|europe|asia|australia|southamerica|northamerica|me|africa)-(central|east|west|south|north|northeast|southeast|southwest|northwest)\d+-[a-f]$/;
24
+
25
+ function visit(node: ts.Node): void {
26
+ if (ts.isStringLiteral(node)) {
27
+ const value = node.text;
28
+ if (zonePattern.test(value)) {
29
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
30
+ diagnostics.push({
31
+ file: sourceFile.fileName,
32
+ line: line + 1,
33
+ column: character + 1,
34
+ ruleId: "WGC002",
35
+ severity: "warning",
36
+ message: `Hardcoded zone "${value}" detected. Use GCP.Zone pseudo-parameter instead.`,
37
+ });
38
+ } else if (regionPattern.test(value)) {
39
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
40
+ diagnostics.push({
41
+ file: sourceFile.fileName,
42
+ line: line + 1,
43
+ column: character + 1,
44
+ ruleId: "WGC002",
45
+ severity: "warning",
46
+ message: `Hardcoded region "${value}" detected. Use GCP.Region pseudo-parameter instead.`,
47
+ });
48
+ }
49
+ }
50
+ ts.forEachChild(node, visit);
51
+ }
52
+
53
+ visit(sourceFile);
54
+ return diagnostics;
55
+ },
56
+ };
@@ -0,0 +1,3 @@
1
+ export { hardcodedProjectRule } from "./hardcoded-project";
2
+ export { hardcodedRegionRule } from "./hardcoded-region";
3
+ export { publicIamRule } from "./public-iam";
@@ -0,0 +1,43 @@
1
+ import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
2
+ import * as ts from "typescript";
3
+
4
+ /**
5
+ * WGC003: Public IAM Binding
6
+ *
7
+ * Warns on allUsers or allAuthenticatedUsers IAM bindings,
8
+ * which grant public access to GCP resources.
9
+ */
10
+ export const publicIamRule: LintRule = {
11
+ id: "WGC003",
12
+ severity: "warning",
13
+ category: "security",
14
+ description: "Warns on allUsers/allAuthenticatedUsers IAM members — grants public access",
15
+
16
+ check(context: LintContext): LintDiagnostic[] {
17
+ const { sourceFile } = context;
18
+ const diagnostics: LintDiagnostic[] = [];
19
+
20
+ const publicMembers = new Set(["allUsers", "allAuthenticatedUsers"]);
21
+
22
+ function visit(node: ts.Node): void {
23
+ if (ts.isStringLiteral(node)) {
24
+ const value = node.text;
25
+ if (publicMembers.has(value)) {
26
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
27
+ diagnostics.push({
28
+ file: sourceFile.fileName,
29
+ line: line + 1,
30
+ column: character + 1,
31
+ ruleId: "WGC003",
32
+ severity: "warning",
33
+ message: `Public IAM member "${value}" detected. This grants access to everyone${value === "allAuthenticatedUsers" ? " with a Google account" : " on the internet"}.`,
34
+ });
35
+ }
36
+ }
37
+ ts.forEachChild(node, visit);
38
+ }
39
+
40
+ visit(sourceFile);
41
+ return diagnostics;
42
+ },
43
+ };
@@ -0,0 +1,63 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { hardcodedProjectRule } from "./hardcoded-project";
3
+ import { hardcodedRegionRule } from "./hardcoded-region";
4
+ import { publicIamRule } from "./public-iam";
5
+ import * as ts from "typescript";
6
+
7
+ function makeContext(code: string) {
8
+ const sourceFile = ts.createSourceFile("test.ts", code, ts.ScriptTarget.Latest, true);
9
+ return { sourceFile };
10
+ }
11
+
12
+ describe("WGC001: hardcoded project", () => {
13
+ test("has correct id and severity", () => {
14
+ expect(hardcodedProjectRule.id).toBe("WGC001");
15
+ expect(hardcodedProjectRule.severity).toBe("warning");
16
+ });
17
+ });
18
+
19
+ describe("WGC002: hardcoded region", () => {
20
+ test("detects hardcoded region", () => {
21
+ const ctx = makeContext('const region = "us-central1";');
22
+ const diags = hardcodedRegionRule.check(ctx);
23
+ expect(diags.length).toBeGreaterThanOrEqual(1);
24
+ expect(diags[0].ruleId).toBe("WGC002");
25
+ expect(diags[0].message).toContain("us-central1");
26
+ });
27
+
28
+ test("detects hardcoded zone", () => {
29
+ const ctx = makeContext('const zone = "us-central1-a";');
30
+ const diags = hardcodedRegionRule.check(ctx);
31
+ expect(diags.length).toBeGreaterThanOrEqual(1);
32
+ expect(diags[0].message).toContain("us-central1-a");
33
+ });
34
+
35
+ test("no diagnostics for non-region strings", () => {
36
+ const ctx = makeContext('const name = "my-app";');
37
+ const diags = hardcodedRegionRule.check(ctx);
38
+ expect(diags).toHaveLength(0);
39
+ });
40
+ });
41
+
42
+ describe("WGC003: public IAM", () => {
43
+ test("detects allUsers", () => {
44
+ const ctx = makeContext('const member = "allUsers";');
45
+ const diags = publicIamRule.check(ctx);
46
+ expect(diags.length).toBeGreaterThanOrEqual(1);
47
+ expect(diags[0].ruleId).toBe("WGC003");
48
+ expect(diags[0].message).toContain("allUsers");
49
+ });
50
+
51
+ test("detects allAuthenticatedUsers", () => {
52
+ const ctx = makeContext('const member = "allAuthenticatedUsers";');
53
+ const diags = publicIamRule.check(ctx);
54
+ expect(diags.length).toBeGreaterThanOrEqual(1);
55
+ expect(diags[0].message).toContain("allAuthenticatedUsers");
56
+ });
57
+
58
+ test("no diagnostics for normal member", () => {
59
+ const ctx = makeContext('const member = "user:admin@example.com";');
60
+ const diags = publicIamRule.check(ctx);
61
+ expect(diags).toHaveLength(0);
62
+ });
63
+ });
@@ -0,0 +1,67 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { existsSync, readFileSync } from "fs";
3
+ import { join, dirname } from "path";
4
+ import { fileURLToPath } from "url";
5
+
6
+ const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
7
+ const lexiconPath = join(pkgDir, "src", "generated", "lexicon-gcp.json");
8
+ const hasGenerated = existsSync(lexiconPath) && (() => {
9
+ try {
10
+ const content = JSON.parse(readFileSync(lexiconPath, "utf-8"));
11
+ return Object.keys(content).length > 0;
12
+ } catch { return false; }
13
+ })();
14
+
15
+ describe("gcpCompletions", () => {
16
+ test("returns empty for non-constructor context", async () => {
17
+ const { gcpCompletions } = await import("./completions");
18
+ const items = gcpCompletions({
19
+ uri: "file:///a.ts",
20
+ content: "const x = 42",
21
+ position: { line: 0, character: 13 },
22
+ wordAtCursor: "42",
23
+ linePrefix: "const x = 42",
24
+ } as any);
25
+ expect(items).toHaveLength(0);
26
+ });
27
+
28
+ test.skipIf(!hasGenerated)(
29
+ "returns completions for 'new S' prefix including Storage*",
30
+ async () => {
31
+ const { gcpCompletions } = await import("./completions");
32
+ const result = gcpCompletions({
33
+ uri: "file:///test.ts",
34
+ content: "const x = new S",
35
+ linePrefix: "const x = new S",
36
+ wordAtCursor: "S",
37
+ position: { line: 0, character: 15 },
38
+ } as any);
39
+
40
+ expect(result).toBeDefined();
41
+ expect(Array.isArray(result)).toBe(true);
42
+ const labels = result.map((c: any) => c.label ?? c);
43
+ expect(labels.some((l: string) => l.startsWith("Storage"))).toBe(true);
44
+ },
45
+ );
46
+
47
+ test.skipIf(!hasGenerated)(
48
+ "returns completions for 'new Compute' including ComputeInstance",
49
+ async () => {
50
+ const { gcpCompletions } = await import("./completions");
51
+ const result = gcpCompletions({
52
+ uri: "file:///test.ts",
53
+ content: "const x = new Compute",
54
+ linePrefix: "const x = new Compute",
55
+ wordAtCursor: "Compute",
56
+ position: { line: 0, character: 21 },
57
+ } as any);
58
+
59
+ expect(result).toBeDefined();
60
+ expect(Array.isArray(result)).toBe(true);
61
+ if (result.length > 0) {
62
+ const labels = result.map((c: any) => c.label ?? c);
63
+ expect(labels.some((l: string) => l.includes("Compute"))).toBe(true);
64
+ }
65
+ },
66
+ );
67
+ });
@@ -0,0 +1,17 @@
1
+ import { createRequire } from "module";
2
+ import type { CompletionContext, CompletionItem } from "@intentius/chant/lsp/types";
3
+ import { LexiconIndex, lexiconCompletions, type LexiconEntry } from "@intentius/chant/lsp/lexicon-providers";
4
+ const require = createRequire(import.meta.url);
5
+
6
+ let cachedIndex: LexiconIndex | null = null;
7
+
8
+ function getIndex(): LexiconIndex {
9
+ if (cachedIndex) return cachedIndex;
10
+ const data = require("../generated/lexicon-gcp.json") as Record<string, LexiconEntry>;
11
+ cachedIndex = new LexiconIndex(data);
12
+ return cachedIndex;
13
+ }
14
+
15
+ export function gcpCompletions(ctx: CompletionContext): CompletionItem[] {
16
+ return lexiconCompletions(ctx, getIndex(), "GCP Config Connector resource");
17
+ }
@@ -0,0 +1,66 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { existsSync, readFileSync } from "fs";
3
+ import { join, dirname } from "path";
4
+ import { fileURLToPath } from "url";
5
+
6
+ const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
7
+ const lexiconPath = join(pkgDir, "src", "generated", "lexicon-gcp.json");
8
+ const hasGenerated = existsSync(lexiconPath) && (() => {
9
+ try {
10
+ const content = JSON.parse(readFileSync(lexiconPath, "utf-8"));
11
+ return Object.keys(content).length > 0;
12
+ } catch { return false; }
13
+ })();
14
+
15
+ describe("gcpHover", () => {
16
+ test("returns undefined for unknown word", async () => {
17
+ const { gcpHover } = await import("./hover");
18
+ const info = gcpHover({
19
+ uri: "file:///a.ts",
20
+ content: "xyz",
21
+ position: { line: 0, character: 1 },
22
+ word: "NotAResource12345",
23
+ lineText: "xyz",
24
+ } as any);
25
+ expect(info).toBeUndefined();
26
+ });
27
+
28
+ test.skipIf(!hasGenerated)(
29
+ "returns hover info for StorageBucket",
30
+ async () => {
31
+ const { gcpHover } = await import("./hover");
32
+ const result = gcpHover({
33
+ word: "StorageBucket",
34
+ position: { line: 0, character: 0 },
35
+ } as any);
36
+
37
+ expect(result).toBeDefined();
38
+ if (result) {
39
+ expect(typeof result).toBe("object");
40
+ }
41
+ },
42
+ );
43
+
44
+ test.skipIf(!hasGenerated)(
45
+ "returns hover info for ComputeInstance",
46
+ async () => {
47
+ const { gcpHover } = await import("./hover");
48
+ const result = gcpHover({
49
+ word: "ComputeInstance",
50
+ position: { line: 0, character: 0 },
51
+ } as any);
52
+
53
+ expect(result).toBeDefined();
54
+ },
55
+ );
56
+
57
+ test.skipIf(!hasGenerated)("returns undefined for empty string", async () => {
58
+ const { gcpHover } = await import("./hover");
59
+ const result = gcpHover({
60
+ word: "",
61
+ position: { line: 0, character: 0 },
62
+ } as any);
63
+
64
+ expect(result).toBeUndefined();
65
+ });
66
+ });
@@ -0,0 +1,54 @@
1
+ import { createRequire } from "module";
2
+ import type { HoverContext, HoverInfo } from "@intentius/chant/lsp/types";
3
+ import { LexiconIndex, lexiconHover, type LexiconEntry } from "@intentius/chant/lsp/lexicon-providers";
4
+ const require = createRequire(import.meta.url);
5
+
6
+ let cachedIndex: LexiconIndex | null = null;
7
+
8
+ function getIndex(): LexiconIndex {
9
+ if (cachedIndex) return cachedIndex;
10
+ const data = require("../generated/lexicon-gcp.json") as Record<string, LexiconEntry>;
11
+ cachedIndex = new LexiconIndex(data);
12
+ return cachedIndex;
13
+ }
14
+
15
+ export function gcpHover(ctx: HoverContext): HoverInfo | undefined {
16
+ return lexiconHover(ctx, getIndex(), resourceHover);
17
+ }
18
+
19
+ function resourceHover(className: string, entry: LexiconEntry): HoverInfo | undefined {
20
+ const lines: string[] = [];
21
+
22
+ lines.push(`**${className}**`);
23
+ lines.push("");
24
+ lines.push(`GCP Config Connector resource: \`${entry.resourceType}\``);
25
+
26
+ if (entry.apiVersion) {
27
+ lines.push(`API Version: \`${entry.apiVersion}\``);
28
+ }
29
+
30
+ if (entry.gvkKind) {
31
+ lines.push(`Kind: \`${entry.gvkKind}\``);
32
+ }
33
+
34
+ const customAttrs = Object.entries(entry.attrs ?? {})
35
+ .filter(([k]) => k !== "apiVersion" && k !== "kind" && k !== "name" && k !== "namespace" && k !== "uid");
36
+ if (customAttrs.length > 0) {
37
+ lines.push("");
38
+ lines.push("**Attributes:**");
39
+ for (const [key, value] of customAttrs) {
40
+ lines.push(`- \`${key}\` → \`${value}\``);
41
+ }
42
+ }
43
+
44
+ // Link to Config Connector docs
45
+ const parts = entry.resourceType.split("::");
46
+ if (parts.length >= 3) {
47
+ const service = parts[1].toLowerCase();
48
+ const kind = parts[2].toLowerCase();
49
+ lines.push("");
50
+ lines.push(`[Config Connector docs](https://cloud.google.com/config-connector/docs/reference/resource-docs/${service}/${service}${kind})`);
51
+ }
52
+
53
+ return { contents: lines.join("\n") };
54
+ }
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Thin entry point for `bun run bundle` in lexicon-gcp.
4
+ * Generates src/generated/ files and writes dist/ bundle.
5
+ *
6
+ * NOTE: Uses top-level await (matching AWS/Azure pattern) to avoid
7
+ * event loop references from async wrappers keeping the process alive.
8
+ */
9
+ import { packageLexicon } from "./codegen/package";
10
+ import { writeBundleSpec } from "@intentius/chant/codegen/package";
11
+ import { dirname, join } from "path";
12
+ import { fileURLToPath } from "url";
13
+
14
+ const pkgDir = dirname(fileURLToPath(import.meta.url));
15
+ const distDir = join(pkgDir, "..", "dist");
16
+
17
+ const verbose = process.argv.includes("--verbose") || !process.argv.includes("--quiet");
18
+ const force = process.argv.includes("--force");
19
+
20
+ const { spec, stats } = await packageLexicon({ verbose, force });
21
+ writeBundleSpec(spec, distDir);
22
+
23
+ console.error(`Packaged ${stats.resources} resources, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
24
+ console.error(`dist/ written to ${distDir}`);