@intentius/chant-lexicon-cedar 0.44.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.
Files changed (265) hide show
  1. package/README.md +190 -0
  2. package/dist/avp/ambient.d.ts +54 -0
  3. package/dist/avp/ambient.d.ts.map +1 -0
  4. package/dist/avp/client.d.ts +127 -0
  5. package/dist/avp/client.d.ts.map +1 -0
  6. package/dist/avp/describe-resources.d.ts +42 -0
  7. package/dist/avp/describe-resources.d.ts.map +1 -0
  8. package/dist/avp/embed.d.ts +120 -0
  9. package/dist/avp/embed.d.ts.map +1 -0
  10. package/dist/avp/live-export.d.ts +94 -0
  11. package/dist/avp/live-export.d.ts.map +1 -0
  12. package/dist/avp/ownership.d.ts +97 -0
  13. package/dist/avp/ownership.d.ts.map +1 -0
  14. package/dist/avp/statement.d.ts +22 -0
  15. package/dist/avp/statement.d.ts.map +1 -0
  16. package/dist/avp/store.d.ts +82 -0
  17. package/dist/avp/store.d.ts.map +1 -0
  18. package/dist/avp/testdata/mock-transport.d.ts +55 -0
  19. package/dist/avp/testdata/mock-transport.d.ts.map +1 -0
  20. package/dist/codegen/docs-cli.d.ts +3 -0
  21. package/dist/codegen/docs-cli.d.ts.map +1 -0
  22. package/dist/codegen/docs.d.ts +26 -0
  23. package/dist/codegen/docs.d.ts.map +1 -0
  24. package/dist/codegen/emit.d.ts +85 -0
  25. package/dist/codegen/emit.d.ts.map +1 -0
  26. package/dist/codegen/generate-cli.d.ts +3 -0
  27. package/dist/codegen/generate-cli.d.ts.map +1 -0
  28. package/dist/codegen/generate.d.ts +37 -0
  29. package/dist/codegen/generate.d.ts.map +1 -0
  30. package/dist/codegen/naming.d.ts +48 -0
  31. package/dist/codegen/naming.d.ts.map +1 -0
  32. package/dist/codegen/package.d.ts +10 -0
  33. package/dist/codegen/package.d.ts.map +1 -0
  34. package/dist/composites/deny-by-default-set.d.ts +58 -0
  35. package/dist/composites/deny-by-default-set.d.ts.map +1 -0
  36. package/dist/composites/index.d.ts +12 -0
  37. package/dist/composites/index.d.ts.map +1 -0
  38. package/dist/composites/owner-can-manage.d.ts +48 -0
  39. package/dist/composites/owner-can-manage.d.ts.map +1 -0
  40. package/dist/config.d.ts +102 -0
  41. package/dist/config.d.ts.map +1 -0
  42. package/dist/coverage.d.ts +59 -0
  43. package/dist/coverage.d.ts.map +1 -0
  44. package/dist/detect.d.ts +18 -0
  45. package/dist/detect.d.ts.map +1 -0
  46. package/dist/generated/index.d.ts +251 -0
  47. package/dist/generated/index.d.ts.map +1 -0
  48. package/dist/generated/runtime.d.ts +2 -0
  49. package/dist/generated/runtime.d.ts.map +1 -0
  50. package/dist/import/adapter.d.ts +21 -0
  51. package/dist/import/adapter.d.ts.map +1 -0
  52. package/dist/import/clause-text.d.ts +38 -0
  53. package/dist/import/clause-text.d.ts.map +1 -0
  54. package/dist/import/generator.d.ts +53 -0
  55. package/dist/import/generator.d.ts.map +1 -0
  56. package/dist/import/parser.d.ts +57 -0
  57. package/dist/import/parser.d.ts.map +1 -0
  58. package/dist/index.d.ts +34 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/init-templates.d.ts +32 -0
  61. package/dist/init-templates.d.ts.map +1 -0
  62. package/dist/integrity.json +22 -0
  63. package/dist/lint/audit-catalog.d.ts +29 -0
  64. package/dist/lint/audit-catalog.d.ts.map +1 -0
  65. package/dist/lint/post-synth/cedar-helpers.d.ts +97 -0
  66. package/dist/lint/post-synth/cedar-helpers.d.ts.map +1 -0
  67. package/dist/lint/post-synth/cedc010.d.ts +17 -0
  68. package/dist/lint/post-synth/cedc010.d.ts.map +1 -0
  69. package/dist/lint/post-synth/cedc011.d.ts +17 -0
  70. package/dist/lint/post-synth/cedc011.d.ts.map +1 -0
  71. package/dist/lint/post-synth/cedc012.d.ts +21 -0
  72. package/dist/lint/post-synth/cedc012.d.ts.map +1 -0
  73. package/dist/lint/post-synth/cedc013.d.ts +20 -0
  74. package/dist/lint/post-synth/cedc013.d.ts.map +1 -0
  75. package/dist/lint/post-synth/cedc014.d.ts +19 -0
  76. package/dist/lint/post-synth/cedc014.d.ts.map +1 -0
  77. package/dist/lint/post-synth/cede010.d.ts +34 -0
  78. package/dist/lint/post-synth/cede010.d.ts.map +1 -0
  79. package/dist/lint/post-synth/cede011.d.ts +22 -0
  80. package/dist/lint/post-synth/cede011.d.ts.map +1 -0
  81. package/dist/lint/post-synth/ceds010.d.ts +24 -0
  82. package/dist/lint/post-synth/ceds010.d.ts.map +1 -0
  83. package/dist/lint/post-synth/ceds011.d.ts +19 -0
  84. package/dist/lint/post-synth/ceds011.d.ts.map +1 -0
  85. package/dist/lint/post-synth/ceds012.d.ts +17 -0
  86. package/dist/lint/post-synth/ceds012.d.ts.map +1 -0
  87. package/dist/lint/post-synth/index.d.ts +3 -0
  88. package/dist/lint/post-synth/index.d.ts.map +1 -0
  89. package/dist/lint/post-synth/wasm-helpers.d.ts +108 -0
  90. package/dist/lint/post-synth/wasm-helpers.d.ts.map +1 -0
  91. package/dist/lint/rules/index.d.ts +10 -0
  92. package/dist/lint/rules/index.d.ts.map +1 -0
  93. package/dist/lint/rules/policy-shape.d.ts +31 -0
  94. package/dist/lint/rules/policy-shape.d.ts.map +1 -0
  95. package/dist/lsp/completions.d.ts +24 -0
  96. package/dist/lsp/completions.d.ts.map +1 -0
  97. package/dist/lsp/hover.d.ts +11 -0
  98. package/dist/lsp/hover.d.ts.map +1 -0
  99. package/dist/lsp/registry.d.ts +45 -0
  100. package/dist/lsp/registry.d.ts.map +1 -0
  101. package/dist/manifest.json +8 -0
  102. package/dist/mcp/index.d.ts +34 -0
  103. package/dist/mcp/index.d.ts.map +1 -0
  104. package/dist/mcp/policy-coverage.d.ts +81 -0
  105. package/dist/mcp/policy-coverage.d.ts.map +1 -0
  106. package/dist/meta.json +699 -0
  107. package/dist/okf/index.md +37 -0
  108. package/dist/okf/rules/CEDC001.md +11 -0
  109. package/dist/okf/rules/CEDC010.md +11 -0
  110. package/dist/okf/rules/CEDC011.md +15 -0
  111. package/dist/okf/rules/CEDC012.md +11 -0
  112. package/dist/okf/rules/CEDC013.md +15 -0
  113. package/dist/okf/rules/CEDC014.md +15 -0
  114. package/dist/okf/rules/CEDE010.md +15 -0
  115. package/dist/okf/rules/CEDE011.md +15 -0
  116. package/dist/okf/rules/CEDS010.md +15 -0
  117. package/dist/okf/rules/CEDS011.md +11 -0
  118. package/dist/okf/rules/CEDS012.md +15 -0
  119. package/dist/okf/types/AdminAction.md +14 -0
  120. package/dist/okf/types/Application.md +14 -0
  121. package/dist/okf/types/ApproveAction.md +13 -0
  122. package/dist/okf/types/CommentAction.md +14 -0
  123. package/dist/okf/types/CreateAction.md +14 -0
  124. package/dist/okf/types/DeleteAction.md +14 -0
  125. package/dist/okf/types/Document.md +18 -0
  126. package/dist/okf/types/Folder.md +15 -0
  127. package/dist/okf/types/Group.md +14 -0
  128. package/dist/okf/types/ListAction.md +14 -0
  129. package/dist/okf/types/Policy.md +29 -0
  130. package/dist/okf/types/ReadAction.md +14 -0
  131. package/dist/okf/types/ServiceAccount.md +15 -0
  132. package/dist/okf/types/ShareAction.md +14 -0
  133. package/dist/okf/types/Team.md +14 -0
  134. package/dist/okf/types/User.md +18 -0
  135. package/dist/okf/types/WriteAction.md +14 -0
  136. package/dist/package-cli.d.ts +3 -0
  137. package/dist/package-cli.d.ts.map +1 -0
  138. package/dist/plugin.d.ts +8 -0
  139. package/dist/plugin.d.ts.map +1 -0
  140. package/dist/rules/cedar-helpers.ts +209 -0
  141. package/dist/rules/cedc010.ts +80 -0
  142. package/dist/rules/cedc011.ts +47 -0
  143. package/dist/rules/cedc012.ts +64 -0
  144. package/dist/rules/cedc013.ts +68 -0
  145. package/dist/rules/cedc014.ts +73 -0
  146. package/dist/rules/cede010.ts +89 -0
  147. package/dist/rules/cede011.ts +58 -0
  148. package/dist/rules/ceds010.ts +57 -0
  149. package/dist/rules/ceds011.ts +42 -0
  150. package/dist/rules/ceds012.ts +50 -0
  151. package/dist/rules/policy-shape.ts +148 -0
  152. package/dist/rules/wasm-helpers.ts +315 -0
  153. package/dist/serializer.d.ts +138 -0
  154. package/dist/serializer.d.ts.map +1 -0
  155. package/dist/spec/fetch.d.ts +71 -0
  156. package/dist/spec/fetch.d.ts.map +1 -0
  157. package/dist/spec/parse.d.ts +113 -0
  158. package/dist/spec/parse.d.ts.map +1 -0
  159. package/dist/spec/pin.d.ts +116 -0
  160. package/dist/spec/pin.d.ts.map +1 -0
  161. package/dist/spec/pinned-names.json +18 -0
  162. package/dist/spec/wasm.d.ts +143 -0
  163. package/dist/spec/wasm.d.ts.map +1 -0
  164. package/dist/types/index.d.ts +219 -0
  165. package/dist/validate-cli.d.ts +3 -0
  166. package/dist/validate-cli.d.ts.map +1 -0
  167. package/dist/validate.d.ts +25 -0
  168. package/dist/validate.d.ts.map +1 -0
  169. package/package.json +75 -0
  170. package/src/avp/OWNERSHIP.md +125 -0
  171. package/src/avp/ambient.test.ts +113 -0
  172. package/src/avp/ambient.ts +124 -0
  173. package/src/avp/client.ts +310 -0
  174. package/src/avp/describe-resources.test.ts +316 -0
  175. package/src/avp/describe-resources.ts +215 -0
  176. package/src/avp/embed.test.ts +114 -0
  177. package/src/avp/embed.ts +190 -0
  178. package/src/avp/live-export.test.ts +232 -0
  179. package/src/avp/live-export.ts +185 -0
  180. package/src/avp/ownership.test.ts +101 -0
  181. package/src/avp/ownership.ts +170 -0
  182. package/src/avp/statement.ts +50 -0
  183. package/src/avp/store.ts +152 -0
  184. package/src/avp/testdata/mock-transport.ts +147 -0
  185. package/src/codegen/docs-cli.ts +7 -0
  186. package/src/codegen/docs.ts +873 -0
  187. package/src/codegen/emit.ts +496 -0
  188. package/src/codegen/generate-cli.ts +18 -0
  189. package/src/codegen/generate.test.ts +128 -0
  190. package/src/codegen/generate.ts +123 -0
  191. package/src/codegen/naming.ts +101 -0
  192. package/src/codegen/package.ts +52 -0
  193. package/src/composites/composites.test.ts +206 -0
  194. package/src/composites/deny-by-default-set.ts +98 -0
  195. package/src/composites/index.ts +13 -0
  196. package/src/composites/owner-can-manage.ts +80 -0
  197. package/src/config-namespace.test.ts +44 -0
  198. package/src/config.test.ts +82 -0
  199. package/src/config.ts +119 -0
  200. package/src/coverage.test.ts +61 -0
  201. package/src/coverage.ts +166 -0
  202. package/src/detect.test.ts +64 -0
  203. package/src/detect.ts +59 -0
  204. package/src/generated/index.d.ts +219 -0
  205. package/src/generated/index.ts +279 -0
  206. package/src/generated/lexicon-cedar.json +699 -0
  207. package/src/generated/runtime.ts +2 -0
  208. package/src/import/adapter.ts +63 -0
  209. package/src/import/clause-text.ts +178 -0
  210. package/src/import/generator.test.ts +133 -0
  211. package/src/import/generator.ts +219 -0
  212. package/src/import/parser.test.ts +265 -0
  213. package/src/import/parser.ts +352 -0
  214. package/src/import/roundtrip.test.ts +127 -0
  215. package/src/import/testdata/full.cedar +36 -0
  216. package/src/import/testdata/full.cedar.json +238 -0
  217. package/src/import/testdata/realistic.cedar +35 -0
  218. package/src/import/testdata/simple.cedar +6 -0
  219. package/src/index.ts +94 -0
  220. package/src/init-templates.test.ts +158 -0
  221. package/src/init-templates.ts +358 -0
  222. package/src/lint/audit-catalog.ts +130 -0
  223. package/src/lint/post-synth/cedar-helpers.ts +209 -0
  224. package/src/lint/post-synth/cedc010.ts +80 -0
  225. package/src/lint/post-synth/cedc011.ts +47 -0
  226. package/src/lint/post-synth/cedc012.ts +64 -0
  227. package/src/lint/post-synth/cedc013.ts +68 -0
  228. package/src/lint/post-synth/cedc014.ts +73 -0
  229. package/src/lint/post-synth/cede010.ts +89 -0
  230. package/src/lint/post-synth/cede011.ts +58 -0
  231. package/src/lint/post-synth/ceds010.ts +57 -0
  232. package/src/lint/post-synth/ceds011.ts +42 -0
  233. package/src/lint/post-synth/ceds012.ts +50 -0
  234. package/src/lint/post-synth/index.ts +25 -0
  235. package/src/lint/post-synth/post-synth.test.ts +474 -0
  236. package/src/lint/post-synth/wasm-helpers.ts +315 -0
  237. package/src/lint/rules/index.ts +12 -0
  238. package/src/lint/rules/policy-shape.test.ts +90 -0
  239. package/src/lint/rules/policy-shape.ts +148 -0
  240. package/src/lsp/completions.test.ts +111 -0
  241. package/src/lsp/completions.ts +55 -0
  242. package/src/lsp/hover.test.ts +82 -0
  243. package/src/lsp/hover.ts +75 -0
  244. package/src/lsp/registry.ts +74 -0
  245. package/src/mcp/index.ts +103 -0
  246. package/src/mcp/policy-coverage.test.ts +176 -0
  247. package/src/mcp/policy-coverage.ts +205 -0
  248. package/src/package-cli.ts +22 -0
  249. package/src/plugin.test.ts +115 -0
  250. package/src/plugin.ts +312 -0
  251. package/src/serializer.test.ts +502 -0
  252. package/src/serializer.ts +335 -0
  253. package/src/skills/chant-cedar-authoring.md +180 -0
  254. package/src/skills/chant-cedar-avp-embedding.md +125 -0
  255. package/src/skills/chant-cedar-meta-policy.md +119 -0
  256. package/src/spec/default-schema.cedarschema +108 -0
  257. package/src/spec/fetch.ts +107 -0
  258. package/src/spec/parse.test.ts +123 -0
  259. package/src/spec/parse.ts +253 -0
  260. package/src/spec/pin.test.ts +88 -0
  261. package/src/spec/pin.ts +243 -0
  262. package/src/spec/pinned-names.json +18 -0
  263. package/src/spec/wasm.ts +283 -0
  264. package/src/validate-cli.ts +8 -0
  265. package/src/validate.ts +85 -0
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The `cedar` namespace is read, not just declared (#1344, #1650).
3
+ *
4
+ * A `configSchema` that nothing consumes is worse than none: it validates a key
5
+ * that has no effect, which reads as a working knob. These tests pin the path
6
+ * from `chant.config.ts` to the schema `generate()` actually opens.
7
+ */
8
+
9
+ import { describe, expect, it } from "vitest";
10
+ import { mkdtempSync, writeFileSync } from "node:fs";
11
+ import { tmpdir } from "node:os";
12
+ import { dirname, join } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+ import { loadCedarConfig } from "./config";
15
+ import { resolveSchemaPath } from "./spec/fetch";
16
+
17
+ const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
18
+
19
+ describe("loadCedarConfig", () => {
20
+ it("reads the namespace out of a project config", async () => {
21
+ const dir = mkdtempSync(join(tmpdir(), "cedar-config-"));
22
+ writeFileSync(
23
+ join(dir, "chant.config.json"),
24
+ JSON.stringify({ lexicons: ["cedar"], cedar: { schema: "authz/app.cedarschema" } }),
25
+ );
26
+ expect(await loadCedarConfig(dir)).toEqual({ schema: "authz/app.cedarschema" });
27
+ });
28
+
29
+ it("returns an empty namespace when the project declares none", async () => {
30
+ const dir = mkdtempSync(join(tmpdir(), "cedar-config-"));
31
+ writeFileSync(join(dir, "chant.config.json"), JSON.stringify({ lexicons: ["cedar"] }));
32
+ expect(await loadCedarConfig(dir)).toEqual({});
33
+ });
34
+
35
+ it("reaches the schema the example's config points at", async () => {
36
+ const example = join(pkgDir, "examples", "basic-policies");
37
+ const config = await loadCedarConfig(example);
38
+ expect(config.schema).toBe("schema.cedarschema");
39
+ expect(resolveSchemaPath({ projectRoot: example, config })).toEqual({
40
+ path: join(example, "schema.cedarschema"),
41
+ isDefault: false,
42
+ });
43
+ });
44
+ });
@@ -0,0 +1,82 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { cedarConfigSchema, CEDAR_DEFAULT_SCHEMA_PATH } from "./config";
3
+ import { cedarPlugin } from "./plugin";
4
+ import { resolveSchemaPath, defaultSchemaPath } from "./spec/fetch";
5
+ import { dirname, join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
9
+
10
+ describe("cedar config namespace", () => {
11
+ it("is declared on the plugin, so core validates it at load", () => {
12
+ expect(cedarPlugin.configSchema).toBe(cedarConfigSchema);
13
+ });
14
+
15
+ it("accepts the documented shape", () => {
16
+ const parsed = cedarConfigSchema.parse({
17
+ schema: "authz/app.cedarschema",
18
+ validation: { mode: "strict", warnings: "warn", requireProjectSchema: true },
19
+ });
20
+ expect(parsed.schema).toBe("authz/app.cedarschema");
21
+ });
22
+
23
+ it("rejects an unknown key inside the namespace", () => {
24
+ expect(() => cedarConfigSchema.parse({ shema: "app.cedarschema" })).toThrow();
25
+ });
26
+
27
+ it("rejects an unknown key one level down, where a typo actually lands", () => {
28
+ expect(() => cedarConfigSchema.parse({ validation: { requireSchema: true } })).toThrow();
29
+ });
30
+
31
+ it("rejects a validation mode cedar-wasm does not implement", () => {
32
+ // `ValidationMode` in 4.12.0 is `"strict"` and nothing else; anything more
33
+ // throws from inside the wasm rather than returning a failure (#1648 §5.7).
34
+ expect(() => cedarConfigSchema.parse({ validation: { mode: "permissive" } })).toThrow();
35
+ });
36
+
37
+ it("leaves everything optional, so an empty namespace is legal", () => {
38
+ expect(cedarConfigSchema.parse({})).toEqual({});
39
+ });
40
+ });
41
+
42
+ describe("schema resolution", () => {
43
+ it("falls back to the bundled default when the project has no schema", () => {
44
+ const resolved = resolveSchemaPath({ projectRoot: join(pkgDir, "nowhere") });
45
+ expect(resolved.isDefault).toBe(true);
46
+ expect(resolved.path).toBe(defaultSchemaPath());
47
+ });
48
+
49
+ it("reads the configured path when it exists", () => {
50
+ const example = join(pkgDir, "examples", "basic-policies");
51
+ const resolved = resolveSchemaPath({ projectRoot: example, config: { schema: "schema.cedarschema" } });
52
+ expect(resolved.isDefault).toBe(false);
53
+ expect(resolved.path).toBe(join(example, "schema.cedarschema"));
54
+ });
55
+
56
+ it("looks for the default filename when nothing is configured", () => {
57
+ const example = join(pkgDir, "examples", "basic-policies");
58
+ expect(CEDAR_DEFAULT_SCHEMA_PATH).toBe("schema.cedarschema");
59
+ expect(resolveSchemaPath({ projectRoot: example }).path).toBe(join(example, CEDAR_DEFAULT_SCHEMA_PATH));
60
+ });
61
+
62
+ it("refuses the fallback when requireProjectSchema is on", () => {
63
+ expect(() =>
64
+ resolveSchemaPath({
65
+ projectRoot: join(pkgDir, "nowhere"),
66
+ config: { validation: { requireProjectSchema: true } },
67
+ }),
68
+ ).toThrow(/no schema found/);
69
+ });
70
+ });
71
+
72
+ describe("upstream pin declaration", () => {
73
+ it("points at a constant its own pattern finds", async () => {
74
+ const pin = cedarPlugin.upstreamPin;
75
+ expect(pin).toBeDefined();
76
+ const { readFileSync } = await import("node:fs");
77
+ const source = readFileSync(join(pkgDir, pin!.file), "utf-8");
78
+ const match = pin!.pattern.exec(source);
79
+ expect(match?.[1]).toBe("4.12.0");
80
+ expect(pin!.replace("4.13.0", match![0])).toContain('"4.13.0"');
81
+ });
82
+ });
package/src/config.ts ADDED
@@ -0,0 +1,119 @@
1
+ /**
2
+ * The `cedar` namespace in `chant.config.ts` (#1344, #1650).
3
+ *
4
+ * ```ts
5
+ * import type { ChantConfig } from "@intentius/chant/config";
6
+ * import "@intentius/chant-lexicon-cedar"; // brings the `cedar` key into ChantConfig
7
+ *
8
+ * export default {
9
+ * lexicons: ["cedar"],
10
+ * cedar: {
11
+ * schema: "authz/app.cedarschema",
12
+ * validation: { mode: "strict", warnings: "warn" },
13
+ * },
14
+ * } satisfies ChantConfig;
15
+ * ```
16
+ *
17
+ * Cedar is the one lexicon whose codegen input is the *project's* file rather
18
+ * than a global upstream (epic #1645), so this namespace is not a convenience —
19
+ * it is where `generate()` is told what to read. A typo in the path is the
20
+ * difference between "your entity types" and "the bundled default schema",
21
+ * which is exactly the kind of silent fallback `z.strictObject` exists to stop
22
+ * one level down.
23
+ */
24
+
25
+ import { z } from "zod";
26
+ import type { ChantConfig } from "@intentius/chant/config";
27
+
28
+ /**
29
+ * Where `generate()` looks when `cedar.schema` is unset.
30
+ *
31
+ * Relative to the project root (the directory holding `chant.config.ts`).
32
+ */
33
+ export const CEDAR_DEFAULT_SCHEMA_PATH = "schema.cedarschema";
34
+
35
+ /**
36
+ * `strictObject`, not `object`. Core applies `.strict()` to the top level of a
37
+ * declared namespace itself; `validation` is nested, and is the lexicon's own
38
+ * to close.
39
+ */
40
+ export const cedarConfigSchema = z.strictObject({
41
+ /**
42
+ * Path to the project's human-readable Cedar schema, relative to the project
43
+ * root. Defaults to {@link CEDAR_DEFAULT_SCHEMA_PATH}; when neither the
44
+ * configured path nor the default exists, generation falls back to the
45
+ * schema bundled at `src/spec/default-schema.cedarschema` so a fresh
46
+ * checkout still produces artifacts.
47
+ */
48
+ schema: z.string().optional(),
49
+
50
+ /**
51
+ * Knobs for the `cedar-wasm` validator. `mode` is a single-variant enum on
52
+ * purpose: `ValidationMode` in cedar-wasm 4.12.0 accepts `"strict"` and
53
+ * nothing else, and passing anything further throws (#1648 §5.7).
54
+ */
55
+ validation: z
56
+ .strictObject({
57
+ /** Validator mode. Only `"strict"` exists in this build of cedar-wasm. */
58
+ mode: z.literal("strict").optional(),
59
+ /**
60
+ * What to do with `validationWarnings` — the validator reports things
61
+ * like "policy is impossible" here, separately from errors.
62
+ */
63
+ warnings: z.enum(["ignore", "warn", "error"]).optional(),
64
+ /**
65
+ * Refuse to build when no project schema is found rather than falling
66
+ * back to the bundled default. Off by default so `chant init` works;
67
+ * worth turning on once a project has its own schema, because
68
+ * `checkParseSchema("")` succeeds and an empty schema validates
69
+ * everything clean (#1648 §1).
70
+ */
71
+ requireProjectSchema: z.boolean().optional(),
72
+ })
73
+ .optional(),
74
+ });
75
+
76
+ export type CedarConfig = z.infer<typeof cedarConfigSchema>;
77
+
78
+ declare module "@intentius/chant/config" {
79
+ interface ChantConfig {
80
+ cedar?: CedarConfig;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Compile-time proof that the augmentation above reaches `ChantConfig` (#1344).
86
+ *
87
+ * Without it this line is `Property 'cedar' does not exist on type
88
+ * 'ChantConfig'`. It lives here rather than in a test because the root tsconfig
89
+ * excludes test files, so a compile-time claim asserted in one is checked by
90
+ * nothing.
91
+ */
92
+ export type CedarConfigNamespace = NonNullable<ChantConfig["cedar"]>;
93
+
94
+ /**
95
+ * Read the `cedar` namespace out of the project's config.
96
+ *
97
+ * Searches upward from `startDir`, so `chant generate` finds the config from a
98
+ * subdirectory the same way `chant build` does. A project with no config, or
99
+ * one with no `cedar` key, gets `{}` — the schema resolution order in
100
+ * `spec/fetch.ts` handles that case without needing to know why.
101
+ *
102
+ * Core has already validated the namespace against {@link cedarConfigSchema}
103
+ * by the time this reads it, so the parse here is a type narrowing rather than
104
+ * a second gate; it stays because a caller can point this at a directory core
105
+ * never loaded.
106
+ */
107
+ export async function loadCedarConfig(startDir: string): Promise<CedarConfig> {
108
+ const { loadChantConfigUpward } = await import("@intentius/chant/config");
109
+ try {
110
+ const { config } = await loadChantConfigUpward(startDir);
111
+ const parsed = cedarConfigSchema.safeParse(config.cedar ?? {});
112
+ return parsed.success ? parsed.data : {};
113
+ } catch {
114
+ // A project whose config does not load is not this function's problem to
115
+ // report — every other command reports it first, and generation falling
116
+ // back to the bundled schema is the same behaviour as having no config.
117
+ return {};
118
+ }
119
+ }
@@ -0,0 +1,61 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { mkdtempSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { computeCedarCoverage, formatCedarCoverage } from "./coverage";
7
+
8
+ const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
9
+
10
+ function generatedDirWith(registry: Record<string, unknown>): string {
11
+ const dir = mkdtempSync(join(tmpdir(), "cedar-coverage-"));
12
+ writeFileSync(join(dir, "lexicon-cedar.json"), JSON.stringify(registry));
13
+ return dir;
14
+ }
15
+
16
+ describe("cedar coverage", () => {
17
+ it("reports full coverage for artifacts generated from the same schema", () => {
18
+ const report = computeCedarCoverage({ projectRoot: join(pkgDir, "nowhere") });
19
+ expect(report.isDefaultSchema).toBe(true);
20
+ expect(report.entityTypes).toBe(7);
21
+ expect(report.actions).toBe(9);
22
+ expect(report.overallPct).toBe(100);
23
+ });
24
+
25
+ it("names the declarations the artifacts do not reach", () => {
26
+ // The failure this exists to catch: a schema action with no generated
27
+ // constant is an action a policy can only name as a hand-typed string.
28
+ const generatedDir = generatedDirWith({
29
+ User: { resourceType: "App::User", kind: "resource" },
30
+ UserAttributes: { resourceType: "App::User.Attributes", kind: "property" },
31
+ });
32
+
33
+ const report = computeCedarCoverage({ projectRoot: join(pkgDir, "nowhere"), generatedDir });
34
+ expect(report.entityTypesCovered).toBe(1);
35
+ expect(report.actionsCovered).toBe(0);
36
+ expect(report.actionPct).toBe(0);
37
+ expect(report.overallPct).toBeLessThan(100);
38
+
39
+ const gap = report.items.find((i) => i.name === 'App::Action::"read"');
40
+ expect(gap?.covered).toBe(false);
41
+ expect(formatCedarCoverage(report, true)).toContain("no generated declaration");
42
+ });
43
+
44
+ it("reports against a project schema when one is configured", () => {
45
+ const example = join(pkgDir, "examples", "basic-policies");
46
+ const report = computeCedarCoverage({ projectRoot: example, config: { schema: "schema.cedarschema" } });
47
+ expect(report.isDefaultSchema).toBe(false);
48
+ // The example's schema is a subset of the default one, so artifacts
49
+ // generated from either cover it.
50
+ expect(report.overallPct).toBe(100);
51
+ });
52
+
53
+ it("treats a schema with nothing in it as covered rather than dividing by zero", () => {
54
+ const generatedDir = generatedDirWith({});
55
+ const empty = mkdtempSync(join(tmpdir(), "cedar-empty-"));
56
+ writeFileSync(join(empty, "schema.cedarschema"), "");
57
+ const report = computeCedarCoverage({ projectRoot: empty, generatedDir });
58
+ expect(report.overallPct).toBe(100);
59
+ expect(report.entityTypes).toBe(0);
60
+ });
61
+ });
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Schema coverage for the cedar lexicon.
3
+ *
4
+ * Core's `computeCoverage` measures CloudFormation-shaped dimensions —
5
+ * property constraints, create-only/write-only lifecycle, return attributes —
6
+ * none of which a Cedar schema has. Reporting 0% across four axes that cannot
7
+ * apply would be worse than reporting nothing, so this asks the question that
8
+ * does mean something here: **is every entity type and every action in the
9
+ * schema reachable from the generated artifacts?**
10
+ *
11
+ * A gap is a real defect, not a nice-to-have. An action with no generated
12
+ * constant is an action a policy can only name as a hand-typed string, which
13
+ * is the exact failure mode the lexicon exists to remove.
14
+ */
15
+
16
+ import { existsSync, readFileSync } from "fs";
17
+ import { dirname, join } from "path";
18
+ import { fileURLToPath } from "url";
19
+ import { LEXICON_JSON_FILENAME } from "./codegen/generate";
20
+ import { attributesTypeName, contextTypeName } from "./codegen/emit";
21
+ import { readSchemaSource, type ResolveSchemaOptions } from "./spec/fetch";
22
+ import { parseCedarSchema } from "./spec/parse";
23
+
24
+ export interface CedarCoverageItem {
25
+ /** Fully-qualified declaration name from the schema. */
26
+ name: string;
27
+ kind: "entity" | "action";
28
+ /** True when a registry entry names this declaration. */
29
+ covered: boolean;
30
+ /** True when the record type beside it (attributes / context) is present too. */
31
+ recordCovered: boolean;
32
+ /** Attributes the schema declares for it. */
33
+ attributes: number;
34
+ }
35
+
36
+ export interface CedarCoverageReport {
37
+ /** Where the schema came from. */
38
+ schemaPath: string;
39
+ /** True when the bundled default schema was used. */
40
+ isDefaultSchema: boolean;
41
+ entityTypes: number;
42
+ entityTypesCovered: number;
43
+ actions: number;
44
+ actionsCovered: number;
45
+ recordTypes: number;
46
+ recordTypesCovered: number;
47
+ entityPct: number;
48
+ actionPct: number;
49
+ recordPct: number;
50
+ overallPct: number;
51
+ items: CedarCoverageItem[];
52
+ }
53
+
54
+ export interface CedarCoverageOptions extends ResolveSchemaOptions {
55
+ /** Directory holding the generated artifacts. Defaults to `<package>/src/generated`. */
56
+ generatedDir?: string;
57
+ verbose?: boolean;
58
+ /** Fail when `overallPct` falls below this. */
59
+ minOverall?: number;
60
+ }
61
+
62
+ function defaultGeneratedDir(): string {
63
+ return join(dirname(fileURLToPath(import.meta.url)), "generated");
64
+ }
65
+
66
+ function pct(covered: number, total: number): number {
67
+ return total === 0 ? 100 : Math.round((covered / total) * 100);
68
+ }
69
+
70
+ /** Compute schema coverage against the artifacts already on disk. */
71
+ export function computeCedarCoverage(options: CedarCoverageOptions = {}): CedarCoverageReport {
72
+ const generatedDir = options.generatedDir ?? defaultGeneratedDir();
73
+ const registryPath = join(generatedDir, LEXICON_JSON_FILENAME);
74
+
75
+ const covered = new Set<string>();
76
+ if (existsSync(registryPath)) {
77
+ const registry = JSON.parse(readFileSync(registryPath, "utf-8")) as Record<string, { resourceType?: string }>;
78
+ for (const entry of Object.values(registry)) {
79
+ if (entry.resourceType) covered.add(entry.resourceType);
80
+ }
81
+ }
82
+
83
+ const source = readSchemaSource(options);
84
+ const { decls } = parseCedarSchema(source.text);
85
+
86
+ const items: CedarCoverageItem[] = decls
87
+ .map((decl) => {
88
+ const recordName =
89
+ decl.kind === "entity" ? attributesTypeName(decl.typeName) : contextTypeName(decl.typeName);
90
+ return {
91
+ name: decl.typeName,
92
+ kind: decl.kind,
93
+ covered: covered.has(decl.typeName),
94
+ recordCovered: covered.has(recordName),
95
+ attributes: decl.kind === "entity" ? decl.attributes.length : decl.context.length,
96
+ };
97
+ })
98
+ .sort((a, b) => a.name.localeCompare(b.name));
99
+
100
+ const entities = items.filter((i) => i.kind === "entity");
101
+ const actions = items.filter((i) => i.kind === "action");
102
+ const entityTypesCovered = entities.filter((i) => i.covered).length;
103
+ const actionsCovered = actions.filter((i) => i.covered).length;
104
+ const recordTypesCovered = items.filter((i) => i.recordCovered).length;
105
+
106
+ const entityPct = pct(entityTypesCovered, entities.length);
107
+ const actionPct = pct(actionsCovered, actions.length);
108
+ const recordPct = pct(recordTypesCovered, items.length);
109
+
110
+ return {
111
+ schemaPath: source.path,
112
+ isDefaultSchema: source.isDefault,
113
+ entityTypes: entities.length,
114
+ entityTypesCovered,
115
+ actions: actions.length,
116
+ actionsCovered,
117
+ recordTypes: items.length,
118
+ recordTypesCovered,
119
+ entityPct,
120
+ actionPct,
121
+ recordPct,
122
+ overallPct: Math.round((entityPct + actionPct + recordPct) / 3),
123
+ items,
124
+ };
125
+ }
126
+
127
+ /** Human-readable summary. */
128
+ export function formatCedarCoverage(report: CedarCoverageReport, verbose = false): string {
129
+ const lines = [
130
+ `Cedar schema coverage — ${report.isDefaultSchema ? "bundled default schema" : report.schemaPath}`,
131
+ ` Entity types: ${report.entityTypesCovered}/${report.entityTypes} (${report.entityPct}%)`,
132
+ ` Actions: ${report.actionsCovered}/${report.actions} (${report.actionPct}%)`,
133
+ ` Record types: ${report.recordTypesCovered}/${report.recordTypes} (${report.recordPct}%)`,
134
+ ` Overall: ${report.overallPct}%`,
135
+ ];
136
+
137
+ if (verbose) {
138
+ lines.push("");
139
+ for (const item of report.items) {
140
+ const gaps: string[] = [];
141
+ if (!item.covered) gaps.push("no generated declaration");
142
+ if (!item.recordCovered) gaps.push(item.kind === "entity" ? "no attributes type" : "no context type");
143
+ lines.push(
144
+ gaps.length === 0
145
+ ? ` ${item.name}: covered (${item.attributes} attribute(s))`
146
+ : ` ${item.name}: ${gaps.join(", ")}`,
147
+ );
148
+ }
149
+ }
150
+
151
+ return lines.join("\n");
152
+ }
153
+
154
+ /**
155
+ * Print the report, and throw when it falls below `minOverall`.
156
+ */
157
+ export function analyzeCedarCoverage(options: CedarCoverageOptions = {}): CedarCoverageReport {
158
+ const report = computeCedarCoverage(options);
159
+ console.error(formatCedarCoverage(report, options.verbose));
160
+
161
+ if (options.minOverall !== undefined && report.overallPct < options.minOverall) {
162
+ throw new Error(`Cedar schema coverage ${report.overallPct}% is below the ${options.minOverall}% minimum`);
163
+ }
164
+
165
+ return report;
166
+ }
@@ -0,0 +1,64 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { readFileSync } from "fs";
3
+ import { join } from "path";
4
+ import { detectTemplate } from "./detect";
5
+
6
+ const testdata = (file: string) => readFileSync(join(import.meta.dirname, "import", "testdata", file), "utf8");
7
+
8
+ describe("what cedar claims", () => {
9
+ test("the JSON policy-set envelope, parsed", () => {
10
+ expect(detectTemplate(JSON.parse(testdata("full.cedar.json")))).toBe(true);
11
+ });
12
+
13
+ test("the JSON policy-set envelope, as a raw string", () => {
14
+ expect(detectTemplate(testdata("full.cedar.json"))).toBe(true);
15
+ });
16
+
17
+ test("`.cedar` policy text", () => {
18
+ expect(detectTemplate(testdata("simple.cedar"))).toBe(true);
19
+ expect(detectTemplate(testdata("realistic.cedar"))).toBe(true);
20
+ });
21
+
22
+ test("a document that is only templates", () => {
23
+ expect(detectTemplate('@id("t") permit (principal == ?principal, action, resource);')).toBe(true);
24
+ });
25
+ });
26
+
27
+ describe("what cedar leaves alone", () => {
28
+ test("another lexicon's JSON", () => {
29
+ expect(detectTemplate({ apiVersion: "apps/v1", kind: "Deployment", metadata: { name: "web" } })).toBe(false);
30
+ expect(detectTemplate({ AWSTemplateFormatVersion: "2010-09-09", Resources: {} })).toBe(false);
31
+ expect(detectTemplate({ services: { web: { image: "nginx" } } })).toBe(false);
32
+ expect(detectTemplate({ $schema: "https://schema.management.azure.com/", resources: [] })).toBe(false);
33
+ });
34
+
35
+ test("an empty policy set — `{}` parses as one, and claiming it would swallow everything", () => {
36
+ expect(detectTemplate({})).toBe(false);
37
+ expect(detectTemplate({ staticPolicies: {} })).toBe(false);
38
+ expect(detectTemplate({ staticPolicies: {}, templates: {}, templateLinks: [] })).toBe(false);
39
+ expect(detectTemplate("")).toBe(false);
40
+ expect(detectTemplate(" ")).toBe(false);
41
+ });
42
+
43
+ test("an envelope with one foreign key beside the known ones", () => {
44
+ expect(detectTemplate({ staticPolicies: { p: {} }, Resources: {} })).toBe(false);
45
+ });
46
+
47
+ test("a well-formed envelope whose policies are not Cedar", () => {
48
+ expect(detectTemplate({ staticPolicies: { p: { effect: "maybe" } } })).toBe(false);
49
+ });
50
+
51
+ test("text that is not Cedar", () => {
52
+ expect(detectTemplate("FROM node:20\nRUN npm ci\n")).toBe(false);
53
+ expect(detectTemplate("apiVersion: v1\nkind: Pod\n")).toBe(false);
54
+ expect(detectTemplate("# a readme\n\nSome prose.\n")).toBe(false);
55
+ });
56
+
57
+ test("scalars, arrays and nothing", () => {
58
+ expect(detectTemplate(null)).toBe(false);
59
+ expect(detectTemplate(undefined)).toBe(false);
60
+ expect(detectTemplate(42)).toBe(false);
61
+ expect(detectTemplate([])).toBe(false);
62
+ expect(detectTemplate([{ staticPolicies: {} }])).toBe(false);
63
+ });
64
+ });
package/src/detect.ts ADDED
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Edge-safe template detection for cedar (#426, #1653).
3
+ *
4
+ * Two documents belong to this lexicon: the JSON policy-set envelope the
5
+ * serializer writes, and `.cedar` policy text. The first is recognized by its
6
+ * shape and then confirmed by `checkParsePolicySet`; the second only by asking
7
+ * the module to split it.
8
+ *
9
+ * The shape test is strict on purpose. `chant import` walks every loaded
10
+ * plugin's `detectTemplate` in turn, so a lexicon that answers `true` for
11
+ * someone else's JSON steals the file. `staticPolicies`/`templates`/
12
+ * `templateLinks` and nothing else is a document no other lexicon writes, and
13
+ * a policy set with no policies in it is not claimed at all — `{}` parses
14
+ * perfectly well as an empty Cedar policy set, and matching it would swallow
15
+ * every empty JSON object in the repo.
16
+ */
17
+
18
+ import { isPolicySetEnvelope } from "./import/parser";
19
+ import { parsesAsPolicySet, splitPolicySet } from "./spec/wasm";
20
+
21
+ /** Does the envelope actually carry a policy or a template? */
22
+ function hasContent(envelope: Record<string, unknown>): boolean {
23
+ const count = (value: unknown): number => {
24
+ if (Array.isArray(value)) return value.length;
25
+ if (value && typeof value === "object") return Object.keys(value).length;
26
+ if (typeof value === "string") return value.trim().length > 0 ? 1 : 0;
27
+ return 0;
28
+ };
29
+ return count(envelope.staticPolicies) + count(envelope.templates) > 0;
30
+ }
31
+
32
+ /** Is this text a Cedar policy document with at least one policy in it? */
33
+ function isPolicyText(content: string): boolean {
34
+ if (!content.trim()) return false;
35
+ const parts = splitPolicySet(content);
36
+ return parts.ok && parts.value.policies.length + parts.value.templates.length > 0;
37
+ }
38
+
39
+ export function detectTemplate(data: unknown): boolean {
40
+ if (typeof data === "string") {
41
+ const trimmed = data.trim();
42
+ if (!trimmed) return false;
43
+
44
+ // A string may still be the envelope — `chant import` hands the parsed
45
+ // object around, but the audit discovery path passes raw file contents.
46
+ if (trimmed.startsWith("{")) {
47
+ try {
48
+ return detectTemplate(JSON.parse(trimmed));
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+ return isPolicyText(trimmed);
54
+ }
55
+
56
+ if (!isPolicySetEnvelope(data)) return false;
57
+ if (!hasContent(data)) return false;
58
+ return parsesAsPolicySet(data);
59
+ }