@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,63 @@
1
+ /**
2
+ * The seam between the cedar-shaped parser/generator above and the core
3
+ * `TemplateParser` / `TypeScriptGenerator` that `chant import` drives.
4
+ *
5
+ * Core's pipeline speaks `TemplateIR` — a flat list of `ResourceIR` with a
6
+ * `type` string it may reorganize into category files. A Cedar policy maps
7
+ * cleanly onto one: the logical id is the policy id, and the type records
8
+ * whether Cedar read it as a static policy or as a template, so the mapping
9
+ * reverses without guessing.
10
+ */
11
+
12
+ import type { TemplateIR, TemplateParser, ResourceIR } from "@intentius/chant/import/parser";
13
+ import type { GeneratedFile, TypeScriptGenerator } from "@intentius/chant/import/generator";
14
+ import { CedarParser, type CedarEntityKind, type CedarPolicyIR } from "./parser";
15
+ import { CedarGenerator, loadActionConstants } from "./generator";
16
+
17
+ /** The `ResourceIR.type` each IR kind is filed under. */
18
+ const TYPE_BY_KIND: Record<CedarEntityKind, string> = {
19
+ policy: "Cedar::Policy",
20
+ template: "Cedar::Template",
21
+ };
22
+
23
+ const KIND_BY_TYPE: Record<string, CedarEntityKind> = {
24
+ "Cedar::Policy": "policy",
25
+ "Cedar::Template": "template",
26
+ };
27
+
28
+ function toResource(entity: CedarPolicyIR): ResourceIR {
29
+ return {
30
+ logicalId: entity.name,
31
+ type: TYPE_BY_KIND[entity.kind],
32
+ properties: entity.props,
33
+ };
34
+ }
35
+
36
+ function toPolicyIR(resource: ResourceIR): CedarPolicyIR {
37
+ return {
38
+ kind: KIND_BY_TYPE[resource.type] ?? "policy",
39
+ name: resource.logicalId,
40
+ props: resource.properties,
41
+ };
42
+ }
43
+
44
+ /** `TemplateParser` over `.cedar` text and the JSON policy-set envelope. */
45
+ export class CedarTemplateParser implements TemplateParser {
46
+ parse(content: string): TemplateIR {
47
+ const { entities, warnings } = new CedarParser().parse(content);
48
+ return {
49
+ resources: entities.map(toResource),
50
+ parameters: [],
51
+ ...(warnings.length > 0 ? { metadata: { warnings } } : {}),
52
+ };
53
+ }
54
+ }
55
+
56
+ /** `TypeScriptGenerator` emitting `new Policy({ … })` per imported policy. */
57
+ export class CedarTemplateGenerator implements TypeScriptGenerator {
58
+ generate(ir: TemplateIR): GeneratedFile[] {
59
+ const generator = new CedarGenerator({ actionConstants: loadActionConstants() });
60
+ const { source } = generator.generate(ir.resources.map(toPolicyIR));
61
+ return [{ path: "main.ts", content: source }];
62
+ }
63
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Recover the `when`/`unless` bodies of a policy exactly as they were written.
3
+ *
4
+ * The round-trip this lexicon promises is byte-identical `.cedar` output, and
5
+ * the JSON leg cannot deliver it: an expression that goes to `PolicyJson` and
6
+ * back comes out defensively parenthesized — `resource.owner == principal`
7
+ * returns as `(resource.owner) == principal` — and `formatPolicies` does not
8
+ * undo that. The tree is semantically lossless; the text is not preserved.
9
+ *
10
+ * So when there *is* source text, the condition bodies are lifted out of it
11
+ * verbatim, and the module still decides what they mean: every body this file
12
+ * hands back is re-parsed and compared against the tree `policyToJson` produced
13
+ * for the same clause before the importer trusts it (see `parser.ts`). A
14
+ * mismatch falls back to the rendered form.
15
+ *
16
+ * What is here is a *delimiter scanner*, not a Cedar parser. It knows three
17
+ * things — that a `"` starts a string with backslash escapes, that `//` runs
18
+ * to end of line, and that `@`, `(` and `{` have partners — and it knows the
19
+ * two clause keywords. It has no opinion about expressions, which remains the
20
+ * module's job. Anything it does not recognize returns `null`, and the caller
21
+ * uses the rendered bodies instead.
22
+ */
23
+
24
+ /** One `when`/`unless` clause, with its body as the author typed it. */
25
+ export interface SourceClause {
26
+ kind: "when" | "unless";
27
+ /** The text between the braces, trimmed. */
28
+ body: string;
29
+ }
30
+
31
+ const WHITESPACE = /\s/;
32
+ const WORD_CHAR = /[A-Za-z0-9_]/;
33
+
34
+ class Scanner {
35
+ private index = 0;
36
+
37
+ constructor(private readonly text: string) {}
38
+
39
+ atEnd(): boolean {
40
+ return this.index >= this.text.length;
41
+ }
42
+
43
+ peek(): string {
44
+ return this.text[this.index] ?? "";
45
+ }
46
+
47
+ /** Consume whitespace and `//` line comments. */
48
+ skipTrivia(): void {
49
+ while (this.index < this.text.length) {
50
+ const ch = this.text[this.index];
51
+ if (WHITESPACE.test(ch)) {
52
+ this.index++;
53
+ continue;
54
+ }
55
+ if (ch === "/" && this.text[this.index + 1] === "/") {
56
+ while (this.index < this.text.length && this.text[this.index] !== "\n") this.index++;
57
+ continue;
58
+ }
59
+ return;
60
+ }
61
+ }
62
+
63
+ /** Consume one `[A-Za-z0-9_]+` run, or `null` if the cursor is not on one. */
64
+ word(): string | null {
65
+ const start = this.index;
66
+ while (this.index < this.text.length && WORD_CHAR.test(this.text[this.index])) this.index++;
67
+ return this.index > start ? this.text.slice(start, this.index) : null;
68
+ }
69
+
70
+ /** Consume the single character `ch`, reporting whether it was there. */
71
+ expect(ch: string): boolean {
72
+ if (this.text[this.index] !== ch) return false;
73
+ this.index++;
74
+ return true;
75
+ }
76
+
77
+ /**
78
+ * Consume a balanced `open`…`close` run starting at the cursor and return
79
+ * what was between them.
80
+ *
81
+ * Strings and line comments are skipped wholesale, so a `}` inside
82
+ * `"}; hi {"` or behind a `//` never closes anything.
83
+ */
84
+ delimited(open: string, close: string): string | null {
85
+ if (!this.expect(open)) return null;
86
+ const start = this.index;
87
+ let depth = 1;
88
+
89
+ while (this.index < this.text.length) {
90
+ const ch = this.text[this.index];
91
+
92
+ if (ch === '"') {
93
+ if (!this.skipString()) return null;
94
+ continue;
95
+ }
96
+ if (ch === "/" && this.text[this.index + 1] === "/") {
97
+ this.skipTrivia();
98
+ continue;
99
+ }
100
+ if (ch === open) depth++;
101
+ if (ch === close) {
102
+ depth--;
103
+ if (depth === 0) {
104
+ const body = this.text.slice(start, this.index);
105
+ this.index++;
106
+ return body;
107
+ }
108
+ }
109
+ this.index++;
110
+ }
111
+ return null;
112
+ }
113
+
114
+ /** Consume a double-quoted literal, honouring backslash escapes. */
115
+ private skipString(): boolean {
116
+ this.index++; // opening quote
117
+ while (this.index < this.text.length) {
118
+ const ch = this.text[this.index];
119
+ if (ch === "\\") {
120
+ this.index += 2;
121
+ continue;
122
+ }
123
+ this.index++;
124
+ if (ch === '"') return true;
125
+ }
126
+ return false;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Split one policy's source into its clause bodies.
132
+ *
133
+ * Returns `null` for anything that does not walk cleanly — an unbalanced
134
+ * brace, a keyword that is neither `when` nor `unless`, trailing text after
135
+ * the terminating `;`. `null` is not a diagnosis, only a decision to use the
136
+ * module's rendering instead.
137
+ */
138
+ export function extractClauses(policyText: string): SourceClause[] | null {
139
+ const scanner = new Scanner(policyText);
140
+
141
+ // Annotations: `@key( "value" )`, any number of them.
142
+ scanner.skipTrivia();
143
+ while (scanner.peek() === "@") {
144
+ scanner.expect("@");
145
+ if (scanner.word() === null) return null;
146
+ scanner.skipTrivia();
147
+ if (scanner.delimited("(", ")") === null) return null;
148
+ scanner.skipTrivia();
149
+ }
150
+
151
+ const effect = scanner.word();
152
+ if (effect !== "permit" && effect !== "forbid") return null;
153
+
154
+ // The scope block. Its contents are of no interest here; skipping it is what
155
+ // keeps the `(` of `@id(` from being mistaken for the scope's own.
156
+ scanner.skipTrivia();
157
+ if (scanner.delimited("(", ")") === null) return null;
158
+
159
+ const clauses: SourceClause[] = [];
160
+ for (;;) {
161
+ scanner.skipTrivia();
162
+ if (scanner.expect(";")) break;
163
+
164
+ const keyword = scanner.word();
165
+ if (keyword !== "when" && keyword !== "unless") return null;
166
+
167
+ scanner.skipTrivia();
168
+ const body = scanner.delimited("{", "}");
169
+ if (body === null) return null;
170
+
171
+ clauses.push({ kind: keyword, body: body.trim() });
172
+ }
173
+
174
+ scanner.skipTrivia();
175
+ if (!scanner.atEnd()) return null;
176
+
177
+ return clauses;
178
+ }
@@ -0,0 +1,133 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { readFileSync } from "fs";
3
+ import { join } from "path";
4
+ import { CedarParser } from "./parser";
5
+ import { CedarGenerator, loadActionConstants, sanitizeName } from "./generator";
6
+ import type { CedarPolicyIR } from "./parser";
7
+
8
+ const testdata = (file: string) => readFileSync(join(import.meta.dirname, "testdata", file), "utf8");
9
+
10
+ const generate = (entities: CedarPolicyIR[], actionConstants?: Record<string, string>) =>
11
+ new CedarGenerator({ actionConstants }).generate(entities).source;
12
+
13
+ const fromFixture = (file: string, actionConstants?: Record<string, string>) =>
14
+ generate(new CedarParser().parse(testdata(file)).entities, actionConstants);
15
+
16
+ // ── 1–3. The shape of a generated policy ───────────────────────────
17
+
18
+ describe("emitting a policy", () => {
19
+ test("1. a policy becomes an exported Policy construction", () => {
20
+ expect(fromFixture("simple.cedar")).toContain("export const allowAliceRead = new Policy({");
21
+ });
22
+
23
+ test("2. every captured prop is emitted, with identifier keys unquoted", () => {
24
+ const source = fromFixture("simple.cedar");
25
+ expect(source).toContain('effect: "permit"');
26
+ expect(source).toContain('principal: {\n eq: "App::User::\\"alice\\""\n }');
27
+ expect(source).toContain('resource: {\n is: "App::Document"\n }');
28
+ expect(source).toContain('id: "allow-alice-read"');
29
+ });
30
+
31
+ test("3. when and unless clauses are emitted as the strings they are", () => {
32
+ const source = fromFixture("realistic.cedar");
33
+ expect(source).toContain('when: [\n "resource.owner == principal"\n ]');
34
+ expect(source).toContain('unless: [\n "context.mfa == false"\n ]');
35
+ });
36
+ });
37
+
38
+ // ── 4–5. The import line ───────────────────────────────────────────
39
+
40
+ describe("the import line", () => {
41
+ test("4. one line, naming the authoring class", () => {
42
+ const lines = fromFixture("simple.cedar").split("\n");
43
+ expect(lines[0]).toBe('import { Policy } from "@intentius/chant-lexicon-cedar";');
44
+ expect(lines.filter((l) => l.startsWith("import "))).toHaveLength(1);
45
+ });
46
+
47
+ test("5. generated action constants join it, sorted, when the schema is known", () => {
48
+ const source = fromFixture("realistic.cedar", loadActionConstants());
49
+ expect(source.split("\n")[0]).toBe(
50
+ 'import { DeleteAction, ListAction, Policy, ReadAction, WriteAction } from "@intentius/chant-lexicon-cedar";',
51
+ );
52
+ });
53
+ });
54
+
55
+ // ── 6–7. Typed refs vs. literal fallback ───────────────────────────
56
+
57
+ describe("action refs", () => {
58
+ test("6. a known action UID is emitted as its generated constant", () => {
59
+ const source = fromFixture("realistic.cedar", loadActionConstants());
60
+ expect(source).toContain("action: {\n in: [\n ReadAction,\n ListAction\n ]\n }");
61
+ expect(source).toContain("action: {\n eq: DeleteAction\n }");
62
+ });
63
+
64
+ test("7. with no registry the UID literal is the fallback, and still typechecks", () => {
65
+ const source = fromFixture("realistic.cedar");
66
+ expect(source).toContain('eq: "App::Action::\\"delete\\""');
67
+ expect(source).not.toContain("DeleteAction");
68
+ });
69
+
70
+ test("an action UID inside a when clause is left as expression text", () => {
71
+ const source = generate(
72
+ [{ kind: "policy", name: "p", props: { effect: "permit", when: ['action == App::Action::"read"'] } }],
73
+ { 'App::Action::"read"': "ReadAction" },
74
+ );
75
+ expect(source).toContain('"action == App::Action::\\"read\\""');
76
+ expect(source.split("\n")[0]).toBe('import { Policy } from "@intentius/chant-lexicon-cedar";');
77
+ });
78
+ });
79
+
80
+ // ── 8–9. Names ─────────────────────────────────────────────────────
81
+
82
+ describe("export names", () => {
83
+ test("8. kebab-case and snake_case ids become camelCase exports", () => {
84
+ expect(sanitizeName("allow-alice-read")).toBe("allowAliceRead");
85
+ expect(sanitizeName("legacy_policy_name")).toBe("legacyPolicyName");
86
+ expect(sanitizeName("simple")).toBe("simple");
87
+ expect(sanitizeName("SOC2-AC-3")).toBe("soc2AC3");
88
+ expect(sanitizeName("2fa-required")).toBe("policy2faRequired");
89
+ expect(sanitizeName("class")).toBe("classPolicy");
90
+ expect(sanitizeName("!!!")).toBe("policy");
91
+ });
92
+
93
+ test("9. two ids that reduce to the same identifier still get one export each", () => {
94
+ const source = generate([
95
+ { kind: "policy", name: "allow-read", props: { effect: "permit" } },
96
+ { kind: "policy", name: "allow.read", props: { effect: "forbid" } },
97
+ ]);
98
+ expect(source).toContain("export const allowRead = ");
99
+ expect(source).toContain("export const allowRead2 = ");
100
+ });
101
+ });
102
+
103
+ // ── 10–11. Templates and empty input ───────────────────────────────
104
+
105
+ describe("templates and empty documents", () => {
106
+ test("10. a template is emitted with its slots, and called out in a comment", () => {
107
+ const source = fromFixture("full.cedar");
108
+ expect(source).toContain("/** Cedar template");
109
+ expect(source).toContain("export const shareWithSlots = new Policy({");
110
+ expect(source).toContain('eq: "?principal"');
111
+ expect(source).toContain('in: "?resource"');
112
+ });
113
+
114
+ test("11. an empty document still emits a compilable file, with a warning", () => {
115
+ const { source, warnings } = new CedarGenerator().generate([]);
116
+ expect(source).toBe('import { Policy } from "@intentius/chant-lexicon-cedar";\n');
117
+ expect(warnings).toEqual(["no Cedar policies were found in this document"]);
118
+ });
119
+ });
120
+
121
+ // ── The registry lookup ────────────────────────────────────────────
122
+
123
+ describe("loadActionConstants", () => {
124
+ test("maps every generated action UID to its constant", () => {
125
+ const constants = loadActionConstants();
126
+ expect(constants['App::Action::"read"']).toBe("ReadAction");
127
+ expect(Object.values(constants).every((name) => name.endsWith("Action"))).toBe(true);
128
+ });
129
+
130
+ test("a package with no generated registry yields no constants, not a throw", () => {
131
+ expect(loadActionConstants("/nowhere/at/all")).toEqual({});
132
+ });
133
+ });
@@ -0,0 +1,219 @@
1
+ /**
2
+ * TypeScript generator for `chant import`.
3
+ *
4
+ * Turns the parser's IR back into the authoring source it came from: one
5
+ * `import` line, then an `export const … = new Policy({ … })` per policy.
6
+ *
7
+ * `Policy` is the schema-driven class `chant generate` emits, and the scope
8
+ * refs it accepts are string-literal unions — so `action: { eq: 'App::Action::"read"' }`
9
+ * is already checked against the schema by the compiler, and an action the
10
+ * schema never declared is an error at the import site. Where the generated
11
+ * registry is on hand, an action UID is emitted as the generated *constant*
12
+ * (`ReadAction`) instead, which reads better and survives a rename; without a
13
+ * registry the literal is the fallback, and it typechecks just the same.
14
+ */
15
+
16
+ import { existsSync, readFileSync } from "fs";
17
+ import { dirname, join } from "path";
18
+ import { fileURLToPath } from "url";
19
+ import type { CedarPolicyIR } from "./parser";
20
+
21
+ /** The package this generated source imports from. */
22
+ export const CEDAR_PACKAGE = "@intentius/chant-lexicon-cedar";
23
+
24
+ /** The authoring class every imported policy is emitted against. */
25
+ const POLICY_CLASS = "Policy";
26
+
27
+ export interface CedarGenerateOptions {
28
+ /**
29
+ * `App::Action::"read"` → `ReadAction`, from the generated registry.
30
+ *
31
+ * Empty by default: the generator is a pure function of its IR, and the
32
+ * adapter is what goes looking for a registry to fill this in.
33
+ */
34
+ actionConstants?: Record<string, string>;
35
+ }
36
+
37
+ export interface CedarGenerateResult {
38
+ source: string;
39
+ warnings: string[];
40
+ }
41
+
42
+ // ── Identifier sanitization ───────────────────────────────────────
43
+
44
+ /**
45
+ * A word no export may be called. Not the full reserved list — only the ones a
46
+ * policy id plausibly reduces to.
47
+ */
48
+ const RESERVED = new Set([
49
+ "break", "case", "catch", "class", "const", "continue", "debugger", "default", "delete", "do",
50
+ "else", "enum", "export", "extends", "false", "finally", "for", "function", "if", "import",
51
+ "in", "instanceof", "new", "null", "return", "super", "switch", "this", "throw", "true", "try",
52
+ "typeof", "var", "void", "while", "with", "yield", "let", "static", "await", "interface",
53
+ "package", "private", "protected", "public", "implements",
54
+ ]);
55
+
56
+ /**
57
+ * A Cedar policy id as a JavaScript identifier.
58
+ *
59
+ * `allow-alice-read` → `allowAliceRead`, `legacy_policy` → `legacyPolicy`,
60
+ * `SOC2-AC-3` → `soc2AC3`. A leading segment that is already mixed case keeps
61
+ * its shape apart from the first letter; one that is all caps is lowered whole,
62
+ * so an id like `SOC2` does not come back as `sOC2`.
63
+ */
64
+ export function sanitizeName(id: string): string {
65
+ const segments = id.split(/[^A-Za-z0-9]+/).filter(Boolean);
66
+ if (segments.length === 0) return "policy";
67
+
68
+ const [first, ...rest] = segments;
69
+ const head = /[a-z]/.test(first) ? first[0].toLowerCase() + first.slice(1) : first.toLowerCase();
70
+ const tail = rest.map((segment) => segment[0].toUpperCase() + segment.slice(1)).join("");
71
+
72
+ let name = head + tail;
73
+ if (/^[0-9]/.test(name)) name = `policy${name[0].toUpperCase()}${name.slice(1)}`;
74
+ if (RESERVED.has(name)) name = `${name}Policy`;
75
+ return name;
76
+ }
77
+
78
+ /** Give every export a distinct name, in a way a second run reproduces. */
79
+ function uniqueNames(entities: CedarPolicyIR[]): string[] {
80
+ const taken = new Set<string>();
81
+ return entities.map((entity) => {
82
+ const base = sanitizeName(entity.name);
83
+ let candidate = base;
84
+ let n = 2;
85
+ while (taken.has(candidate)) candidate = `${base}${n++}`;
86
+ taken.add(candidate);
87
+ return candidate;
88
+ });
89
+ }
90
+
91
+ // ── Props emission ────────────────────────────────────────────────
92
+
93
+ /**
94
+ * A marker no Cedar value can contain, so a substitution cannot be confused
95
+ * for content. The props go through `JSON.stringify` as data; identifiers are
96
+ * swapped in afterwards by matching the encoded marker exactly.
97
+ */
98
+ const MARKER = "\u0000chant:";
99
+
100
+ const SCOPE_KEYS = ["principal", "action", "resource"] as const;
101
+
102
+ /**
103
+ * Swap recognized action UIDs for their generated constant, in scope positions
104
+ * only.
105
+ *
106
+ * Not inside `when`/`unless`: those are Cedar expression text, where the UID is
107
+ * part of a larger expression the compiler never sees as a ref.
108
+ */
109
+ function markScopeConstants(
110
+ props: Record<string, unknown>,
111
+ actionConstants: Record<string, string>,
112
+ used: Set<string>,
113
+ ): Record<string, unknown> {
114
+ if (Object.keys(actionConstants).length === 0) return props;
115
+
116
+ const mark = (value: unknown): unknown => {
117
+ if (typeof value !== "string") return value;
118
+ const constant = actionConstants[value];
119
+ if (!constant) return value;
120
+ used.add(constant);
121
+ return `${MARKER}${constant}`;
122
+ };
123
+
124
+ const out: Record<string, unknown> = { ...props };
125
+ for (const key of SCOPE_KEYS) {
126
+ const scope = out[key];
127
+ if (!scope || typeof scope !== "object") continue;
128
+ const marked: Record<string, unknown> = { ...(scope as Record<string, unknown>) };
129
+ if ("eq" in marked) marked.eq = mark(marked.eq);
130
+ if (Array.isArray(marked.in)) marked.in = marked.in.map(mark);
131
+ else if ("in" in marked) marked.in = mark(marked.in);
132
+ out[key] = marked;
133
+ }
134
+ return out;
135
+ }
136
+
137
+ /** `{"effect": "permit"}` → `{ effect: "permit" }`, and markers → identifiers. */
138
+ function emitProps(props: Record<string, unknown>): string {
139
+ const json = JSON.stringify(props, null, 2)
140
+ // Unquote keys that are already valid identifiers. A quote inside a string
141
+ // value is escaped as `\"` by `JSON.stringify`, so no value can present the
142
+ // `"ident":` shape this matches.
143
+ .replace(/"([A-Za-z_$][A-Za-z0-9_$]*)":/g, "$1:");
144
+
145
+ return json.replace(/"\\u0000chant:([A-Za-z_$][A-Za-z0-9_$]*)"/g, "$1");
146
+ }
147
+
148
+ // ── Generator ─────────────────────────────────────────────────────
149
+
150
+ export class CedarGenerator {
151
+ private readonly actionConstants: Record<string, string>;
152
+
153
+ constructor(options: CedarGenerateOptions = {}) {
154
+ this.actionConstants = options.actionConstants ?? {};
155
+ }
156
+
157
+ generate(entities: CedarPolicyIR[]): CedarGenerateResult {
158
+ const warnings: string[] = [];
159
+ const imports = new Set<string>([POLICY_CLASS]);
160
+ const names = uniqueNames(entities);
161
+ const blocks: string[] = [];
162
+
163
+ for (const [index, entity] of entities.entries()) {
164
+ const props = markScopeConstants(entity.props, this.actionConstants, imports);
165
+
166
+ if (entity.kind === "template") {
167
+ blocks.push(
168
+ "/** Cedar template — its `?principal`/`?resource` slots are filled at link time. */",
169
+ );
170
+ }
171
+ blocks.push(`export const ${names[index]} = new ${POLICY_CLASS}(${emitProps(props)});`);
172
+ blocks.push("");
173
+ }
174
+
175
+ if (entities.length === 0) warnings.push("no Cedar policies were found in this document");
176
+
177
+ const importLine = `import { ${[...imports].sort().join(", ")} } from "${CEDAR_PACKAGE}";`;
178
+ const source = [importLine, "", ...blocks].join("\n");
179
+
180
+ return { source: source.endsWith("\n") ? source : `${source}\n`, warnings };
181
+ }
182
+ }
183
+
184
+ // ── Generated registry lookup ─────────────────────────────────────
185
+
186
+ /** Filename of the registry `chant generate` writes beside the typed classes. */
187
+ const REGISTRY_FILE = "lexicon-cedar.json";
188
+
189
+ interface RegistryEntry {
190
+ resourceType?: string;
191
+ kind?: string;
192
+ }
193
+
194
+ /**
195
+ * Read `App::Action::"read"` → `ReadAction` out of the generated registry.
196
+ *
197
+ * Best-effort by design: a project that has not run `chant generate` has no
198
+ * registry, and the generator falls back to emitting UID literals — which the
199
+ * `Policy` class accepts and the compiler still checks.
200
+ */
201
+ export function loadActionConstants(packageDir?: string): Record<string, string> {
202
+ const base = packageDir ?? dirname(dirname(fileURLToPath(import.meta.url)));
203
+ const path = join(base, "generated", REGISTRY_FILE);
204
+ if (!existsSync(path)) return {};
205
+
206
+ try {
207
+ const registry = JSON.parse(readFileSync(path, "utf-8")) as Record<string, RegistryEntry>;
208
+ const constants: Record<string, string> = {};
209
+ for (const [tsName, entry] of Object.entries(registry)) {
210
+ if (entry.kind !== "resource") continue;
211
+ if (typeof entry.resourceType !== "string") continue;
212
+ if (!/::Action::"/.test(entry.resourceType)) continue;
213
+ constants[entry.resourceType] = tsName;
214
+ }
215
+ return constants;
216
+ } catch {
217
+ return {};
218
+ }
219
+ }