@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,265 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { readFileSync } from "fs";
3
+ import { join } from "path";
4
+ import { CedarParser, isPolicySetEnvelope } from "./parser";
5
+ import { extractClauses } from "./clause-text";
6
+
7
+ const testdata = (file: string) => readFileSync(join(import.meta.dirname, "testdata", file), "utf8");
8
+
9
+ const parse = (content: string) => new CedarParser().parse(content);
10
+
11
+ /** The one policy in a single-policy document. */
12
+ const only = (content: string) => {
13
+ const { entities } = parse(content);
14
+ expect(entities).toHaveLength(1);
15
+ return entities[0];
16
+ };
17
+
18
+ const byName = (content: string, name: string) => {
19
+ const found = parse(content).entities.find((e) => e.name === name);
20
+ if (!found) throw new Error(`no policy named ${name}`);
21
+ return found;
22
+ };
23
+
24
+ // ── 1–4. Scope constraints ─────────────────────────────────────────
25
+
26
+ describe("scope constraints", () => {
27
+ test("1. `== E` becomes an eq scope carrying the full entity UID", () => {
28
+ expect(only(testdata("simple.cedar")).props.principal).toEqual({ eq: 'App::User::"alice"' });
29
+ });
30
+
31
+ test("2. `is T` becomes an is scope", () => {
32
+ expect(only(testdata("simple.cedar")).props.resource).toEqual({ is: "App::Document" });
33
+ });
34
+
35
+ test("3. `is T in E` keeps both halves", () => {
36
+ expect(byName(testdata("full.cedar"), "full-grant").props.principal).toEqual({
37
+ is: "App::User",
38
+ in: 'App::Group::"engineering"',
39
+ });
40
+ });
41
+
42
+ test("4. an action set becomes an in scope with an array", () => {
43
+ expect(byName(testdata("realistic.cedar"), "owner-read").props.action).toEqual({
44
+ in: ['App::Action::"read"', 'App::Action::"list"'],
45
+ });
46
+ });
47
+
48
+ test("a one-element action set is Cedar's own normalization, not a lost prop", () => {
49
+ // `action in [X]` and `action in X` are the same constraint, and the module
50
+ // collapses the first into the second before this file ever sees it — so a
51
+ // round-trip rewrites the brackets away and cannot be made not to.
52
+ const { entities } = parse('@id("one") permit (principal, action in [App::Action::"read"], resource);');
53
+ expect(entities[0].props.action).toEqual({ in: 'App::Action::"read"' });
54
+ });
55
+ });
56
+
57
+ // ── 5–6. Effects and unconstrained scopes ──────────────────────────
58
+
59
+ describe("effect and defaults", () => {
60
+ test("5. forbid is captured as the effect", () => {
61
+ expect(byName(testdata("full.cedar"), "deny-anonymous").props.effect).toBe("forbid");
62
+ });
63
+
64
+ test("6. an unconstrained scope is left out — `{}` and absent are one value", () => {
65
+ const props = byName(testdata("full.cedar"), "deny-anonymous").props;
66
+ expect(props.principal).toBeUndefined();
67
+ expect(props.action).toBeUndefined();
68
+ expect(props.resource).toBeUndefined();
69
+ expect(Object.keys(props)).toEqual(["effect", "annotations"]);
70
+ });
71
+ });
72
+
73
+ // ── 7–9. Conditions ────────────────────────────────────────────────
74
+
75
+ describe("conditions", () => {
76
+ test("7. a when clause survives as the text it was written as", () => {
77
+ expect(byName(testdata("realistic.cedar"), "owner-read").props.when).toEqual([
78
+ "resource.owner == principal",
79
+ ]);
80
+ });
81
+
82
+ test("8. when and unless are captured separately, each in order", () => {
83
+ const props = byName(testdata("full.cedar"), "full-grant").props;
84
+ expect(props.when).toEqual([
85
+ "resource.owner == principal && context.mfa == true",
86
+ 'principal.department == "eng"',
87
+ ]);
88
+ expect(props.unless).toEqual([
89
+ 'resource.classification == "secret"',
90
+ 'context.ip.isInRange(ip("10.0.0.0/8"))',
91
+ ]);
92
+ });
93
+
94
+ test("9. braces inside a string literal do not end the clause", () => {
95
+ expect(byName(testdata("full.cedar"), "quoted-note").props.when).toEqual([
96
+ 'context.note == "a } b { c"',
97
+ ]);
98
+ });
99
+ });
100
+
101
+ // ── 10–11. Annotations ─────────────────────────────────────────────
102
+
103
+ describe("annotations", () => {
104
+ test("10. every annotation is captured, including the id", () => {
105
+ expect(byName(testdata("full.cedar"), "full-grant").props.annotations).toEqual({
106
+ doc: "Everything the authoring model can carry, in one policy.",
107
+ id: "full-grant",
108
+ owner: "platform",
109
+ ticket: "SEC-12",
110
+ });
111
+ });
112
+
113
+ test("11. escapes in an annotation value are decoded, not carried as source", () => {
114
+ expect(byName(testdata("full.cedar"), "quoted-note").props.annotations).toMatchObject({
115
+ note: 'say "hi" \\ done',
116
+ });
117
+ });
118
+ });
119
+
120
+ // ── 12–13. Templates and document shape ────────────────────────────
121
+
122
+ describe("templates", () => {
123
+ test("12. a policy with slots is read as a template, with the slots kept", () => {
124
+ const template = byName(testdata("full.cedar"), "share-with-slots");
125
+ expect(template.kind).toBe("template");
126
+ expect(template.props.principal).toEqual({ eq: "?principal" });
127
+ expect(template.props.resource).toEqual({ in: "?resource" });
128
+ });
129
+
130
+ test("13. static policies and templates come back from one document", () => {
131
+ const { entities } = parse(testdata("full.cedar"));
132
+ expect(entities.map((e) => `${e.kind}:${e.name}`)).toEqual([
133
+ "policy:full-grant",
134
+ "policy:deny-anonymous",
135
+ "policy:quoted-note",
136
+ "template:share-with-slots",
137
+ ]);
138
+ });
139
+ });
140
+
141
+ // ── 14–16. The JSON policy-set envelope ────────────────────────────
142
+
143
+ describe("the JSON policy-set envelope", () => {
144
+ test("14. an envelope parses to the same policies as the text beside it", () => {
145
+ const fromText = parse(testdata("full.cedar")).entities;
146
+ const fromJSON = parse(testdata("full.cedar.json")).entities;
147
+ expect(fromJSON.map((e) => `${e.kind}:${e.name}`)).toEqual(fromText.map((e) => `${e.kind}:${e.name}`));
148
+ expect(fromJSON[0].props.annotations).toEqual(fromText[0].props.annotations);
149
+ });
150
+
151
+ test("15. an envelope's conditions come back re-rendered, since there is no source to quote", () => {
152
+ const props = parse(testdata("full.cedar.json")).entities[0].props;
153
+ // Semantically the same clause as the text fixture's, defensively
154
+ // parenthesized on the way out of the module (#1648 §1).
155
+ expect(props.when).toEqual([
156
+ "((resource.owner) == principal) && ((context.mfa) == true)",
157
+ '(principal.department) == "eng"',
158
+ ]);
159
+ });
160
+
161
+ test("16. template links are reported rather than silently dropped", () => {
162
+ const { warnings } = parse(
163
+ JSON.stringify({
164
+ staticPolicies: {},
165
+ templates: {
166
+ t: {
167
+ effect: "permit",
168
+ principal: { op: "==", slot: "?principal" },
169
+ action: { op: "All" },
170
+ resource: { op: "All" },
171
+ conditions: [],
172
+ annotations: { id: "t" },
173
+ },
174
+ },
175
+ templateLinks: [{ templateId: "t", newId: "t0", values: { "?principal": { type: "App::User", id: "a" } } }],
176
+ }),
177
+ );
178
+ expect(warnings.join(" ")).toContain("template link(s) were not imported");
179
+ });
180
+
181
+ test("an envelope keyed by id names the policy even without an @id annotation", () => {
182
+ const { entities } = parse(
183
+ JSON.stringify({
184
+ staticPolicies: {
185
+ "from-the-key": {
186
+ effect: "permit",
187
+ principal: { op: "All" },
188
+ action: { op: "All" },
189
+ resource: { op: "All" },
190
+ conditions: [],
191
+ },
192
+ },
193
+ }),
194
+ );
195
+ expect(entities.map((e) => e.name)).toEqual(["from-the-key"]);
196
+ });
197
+
198
+ test("staticPolicies given as raw Cedar text is read as text", () => {
199
+ const { entities } = parse(JSON.stringify({ staticPolicies: testdata("simple.cedar") }));
200
+ expect(entities.map((e) => e.name)).toEqual(["allow-alice-read"]);
201
+ });
202
+ });
203
+
204
+ // ── 17–18. Document-level behaviour ────────────────────────────────
205
+
206
+ describe("documents", () => {
207
+ test("17. an empty document produces no entities and no noise", () => {
208
+ expect(parse(" \n ")).toEqual({ entities: [], warnings: [] });
209
+ });
210
+
211
+ test("18. text the module rejects raises rather than importing half a policy", () => {
212
+ expect(() => parse("this is not cedar")).toThrow(/could not parse the policy document/);
213
+ });
214
+
215
+ test("comments are dropped — they are in no grammar the module round-trips", () => {
216
+ const { entities } = parse(`// a note about alice\n${testdata("simple.cedar")}`);
217
+ expect(entities).toHaveLength(1);
218
+ expect(JSON.stringify(entities[0].props)).not.toContain("a note about alice");
219
+ });
220
+ });
221
+
222
+ // ── The envelope shape test ────────────────────────────────────────
223
+
224
+ describe("isPolicySetEnvelope", () => {
225
+ test("accepts the serializer's own document", () => {
226
+ expect(isPolicySetEnvelope(JSON.parse(testdata("full.cedar.json")))).toBe(true);
227
+ });
228
+
229
+ test("rejects a document carrying keys Cedar has never heard of", () => {
230
+ expect(isPolicySetEnvelope({ apiVersion: "v1", kind: "Pod" })).toBe(false);
231
+ expect(isPolicySetEnvelope({ staticPolicies: {}, Resources: {} })).toBe(false);
232
+ expect(isPolicySetEnvelope([])).toBe(false);
233
+ expect(isPolicySetEnvelope(null)).toBe(false);
234
+ expect(isPolicySetEnvelope({ templateLinks: [] })).toBe(false);
235
+ });
236
+ });
237
+
238
+ // ── The clause scanner on its own ──────────────────────────────────
239
+
240
+ describe("extractClauses", () => {
241
+ test("returns the bodies of every clause, in order", () => {
242
+ expect(
243
+ extractClauses('@id("x")\npermit (principal, action, resource)\nwhen { a == 1 }\nunless { b };'),
244
+ ).toEqual([
245
+ { kind: "when", body: "a == 1" },
246
+ { kind: "unless", body: "b" },
247
+ ]);
248
+ });
249
+
250
+ test("an annotation's parentheses are not mistaken for the scope's", () => {
251
+ expect(extractClauses('@doc("(not the scope)")\npermit (principal, action, resource);')).toEqual([]);
252
+ });
253
+
254
+ test("a comment inside a clause is skipped, braces and all", () => {
255
+ expect(extractClauses("permit (principal, action, resource)\nwhen {\n // } not the end\n a\n};")).toEqual([
256
+ { kind: "when", body: "// } not the end\n a" },
257
+ ]);
258
+ });
259
+
260
+ test("gives up rather than guessing at anything it does not recognize", () => {
261
+ expect(extractClauses("permit (principal, action, resource) when { unbalanced ;")).toBeNull();
262
+ expect(extractClauses("allow (principal, action, resource);")).toBeNull();
263
+ expect(extractClauses("permit (principal, action, resource); trailing")).toBeNull();
264
+ });
265
+ });
@@ -0,0 +1,352 @@
1
+ /**
2
+ * Cedar policy parser for `chant import`.
3
+ *
4
+ * Two surfaces come in and one IR goes out:
5
+ *
6
+ * - **`.cedar` policy text** — split by `policySetTextToParts`, then each
7
+ * policy handed to `policyToJson` (or `templateToJson`, for one carrying a
8
+ * slot). The Cedar grammar is never touched here; the module owns it.
9
+ * - **the JSON policy-set envelope** the serializer writes beside the text —
10
+ * `{ staticPolicies, templates, templateLinks }`, whose values are already
11
+ * in Cedar's JSON policy format.
12
+ *
13
+ * The IR is lossless with respect to the authoring model: effect, all three
14
+ * scopes, every `when`/`unless` clause in order, and every annotation. What is
15
+ * *not* in the model is not invented — Cedar comments are dropped by the
16
+ * module before this file sees them, and template links have no authoring
17
+ * shape yet, so they are reported as a warning rather than silently discarded.
18
+ *
19
+ * Unconstrained scopes are the one thing left out of `props`, because `{}` and
20
+ * absent are the same value to the serializer (`principal` with no constraint).
21
+ * Writing `principal: {}` into every imported policy would be noise, not
22
+ * fidelity.
23
+ */
24
+
25
+ import {
26
+ policyToJson,
27
+ policyToText,
28
+ splitPolicySet,
29
+ templateToJson,
30
+ type PolicyJson,
31
+ } from "../spec/wasm";
32
+ import { escapeCedarString, type CedarScope } from "../serializer";
33
+ import { extractClauses } from "./clause-text";
34
+
35
+ // ── IR ────────────────────────────────────────────────────────────
36
+
37
+ /**
38
+ * Whether Cedar read this as a static policy or as a template.
39
+ *
40
+ * The discriminant is not chant's: a policy is a template exactly when it
41
+ * carries a `?principal`/`?resource` slot, and `policyToJson` refuses one.
42
+ */
43
+ export type CedarEntityKind = "policy" | "template";
44
+
45
+ /** One imported Cedar policy, in the lossless intermediate shape. */
46
+ export interface CedarPolicyIR {
47
+ kind: CedarEntityKind;
48
+ /** The Cedar policy id — its `@id`, or the envelope key it was filed under. */
49
+ name: string;
50
+ /** The `Cedar::Policy` props, ready to hand to the authoring class. */
51
+ props: Record<string, unknown>;
52
+ }
53
+
54
+ export interface CedarParseResult {
55
+ entities: CedarPolicyIR[];
56
+ warnings: string[];
57
+ }
58
+
59
+ // ── PolicyJson → props ────────────────────────────────────────────
60
+
61
+ type EntityUidJson = { __entity: { type: string; id: string } } | { type: string; id: string };
62
+
63
+ /** `{ type: "App::User", id: "alice" }` → `App::User::"alice"`. */
64
+ function uidText(uid: EntityUidJson): string {
65
+ const parts = "__entity" in uid ? uid.__entity : uid;
66
+ return `${parts.type}::"${escapeCedarString(parts.id)}"`;
67
+ }
68
+
69
+ /**
70
+ * One scope constraint, back to the authoring form.
71
+ *
72
+ * `undefined` is the unconstrained case — see the note at the top of the file
73
+ * about why it is omitted rather than written as `{}`.
74
+ */
75
+ function scopeProps(constraint: unknown): CedarScope | undefined {
76
+ if (!constraint || typeof constraint !== "object") return undefined;
77
+ const scope = constraint as Record<string, unknown>;
78
+
79
+ const inTarget = (value: unknown): string | string[] | undefined => {
80
+ if (!value || typeof value !== "object") return undefined;
81
+ const target = value as Record<string, unknown>;
82
+ if (typeof target.slot === "string") return target.slot;
83
+ if (Array.isArray(target.entities)) return target.entities.map((e) => uidText(e as EntityUidJson));
84
+ if (target.entity) return uidText(target.entity as EntityUidJson);
85
+ return undefined;
86
+ };
87
+
88
+ switch (scope.op) {
89
+ case "is": {
90
+ if (typeof scope.entity_type !== "string") return undefined;
91
+ const within = inTarget(scope.in);
92
+ return within === undefined ? { is: scope.entity_type } : { is: scope.entity_type, in: within };
93
+ }
94
+ case "==":
95
+ if (typeof scope.slot === "string") return { eq: scope.slot };
96
+ return scope.entity ? { eq: uidText(scope.entity as EntityUidJson) } : undefined;
97
+ case "in": {
98
+ const within = inTarget(scope);
99
+ return within === undefined ? undefined : { in: within };
100
+ }
101
+ default:
102
+ // `All`, and anything a later Cedar version adds that this does not know.
103
+ return undefined;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Render one condition body back to Cedar text through the module.
109
+ *
110
+ * There is no entry point that renders an expression on its own, so the body
111
+ * is wrapped in the most boring policy that can hold it and the wrapper is
112
+ * sliced back off. The wrapper is the module's own output, so its shape is
113
+ * fixed: `permit(principal, action, resource) when { … };`.
114
+ */
115
+ function renderConditionBody(kind: "when" | "unless", body: unknown): string | undefined {
116
+ const probe = {
117
+ effect: "permit",
118
+ principal: { op: "All" },
119
+ action: { op: "All" },
120
+ resource: { op: "All" },
121
+ conditions: [{ kind, body }],
122
+ } as unknown as PolicyJson;
123
+
124
+ const rendered = policyToText(probe);
125
+ if (!rendered.ok) return undefined;
126
+
127
+ const opening = `${kind} { `;
128
+ const start = rendered.value.indexOf(opening);
129
+ if (start < 0 || !rendered.value.endsWith(" };")) return undefined;
130
+ return rendered.value.slice(start + opening.length, -3);
131
+ }
132
+
133
+ /** Re-parse one clause body and return the tree the module makes of it. */
134
+ function parseConditionBody(body: string): unknown {
135
+ const parsed = policyToJson(`permit (principal, action, resource) when { ${body} };`);
136
+ return parsed.ok ? parsed.value.conditions[0]?.body : undefined;
137
+ }
138
+
139
+ /**
140
+ * The `when` and `unless` clause texts, in order.
141
+ *
142
+ * Verbatim source wins when there is source to quote and the module agrees it
143
+ * means the same thing; otherwise the rendered form is used. `policyToJson`
144
+ * serializes deterministically with sorted keys (#1648 §5.2), so comparing two
145
+ * of its trees by their JSON text is sound.
146
+ */
147
+ function conditionProps(
148
+ json: PolicyJson,
149
+ sourceText: string | undefined,
150
+ ): { when: string[]; unless: string[]; warnings: string[] } {
151
+ const conditions = json.conditions ?? [];
152
+ const warnings: string[] = [];
153
+
154
+ const source = sourceText ? extractClauses(sourceText) : null;
155
+ const usable = source !== null && source.length === conditions.length ? source : null;
156
+
157
+ const when: string[] = [];
158
+ const unless: string[] = [];
159
+
160
+ for (const [index, condition] of conditions.entries()) {
161
+ const candidate = usable?.[index];
162
+ let text: string | undefined;
163
+
164
+ if (
165
+ candidate &&
166
+ candidate.kind === condition.kind &&
167
+ JSON.stringify(parseConditionBody(candidate.body)) === JSON.stringify(condition.body)
168
+ ) {
169
+ text = candidate.body;
170
+ } else {
171
+ text = renderConditionBody(condition.kind, condition.body);
172
+ }
173
+
174
+ if (text === undefined) {
175
+ warnings.push(`a ${condition.kind} clause could not be rendered back to Cedar text and was dropped`);
176
+ continue;
177
+ }
178
+
179
+ (condition.kind === "when" ? when : unless).push(text);
180
+ }
181
+
182
+ return { when, unless, warnings };
183
+ }
184
+
185
+ /**
186
+ * One `PolicyJson` as `Cedar::Policy` props.
187
+ *
188
+ * Key order matches the props interface, so the generator's `JSON.stringify`
189
+ * emits policies that read the way they would have been written.
190
+ */
191
+ function policyProps(
192
+ json: PolicyJson,
193
+ sourceText: string | undefined,
194
+ ): { props: Record<string, unknown>; warnings: string[] } {
195
+ const props: Record<string, unknown> = { effect: json.effect === "forbid" ? "forbid" : "permit" };
196
+
197
+ const principal = scopeProps(json.principal);
198
+ if (principal) props.principal = principal;
199
+ const action = scopeProps(json.action);
200
+ if (action) props.action = action;
201
+ const resource = scopeProps(json.resource);
202
+ if (resource) props.resource = resource;
203
+
204
+ const { when, unless, warnings } = conditionProps(json, sourceText);
205
+ if (when.length > 0) props.when = when;
206
+ if (unless.length > 0) props.unless = unless;
207
+
208
+ const annotations = json.annotations ?? {};
209
+ if (Object.keys(annotations).length > 0) props.annotations = { ...annotations };
210
+
211
+ return { props, warnings };
212
+ }
213
+
214
+ /** The id a policy answers to: its own `@id` first, then where it was filed. */
215
+ function policyId(json: PolicyJson, fallback: string): string {
216
+ const annotated = json.annotations?.id;
217
+ return typeof annotated === "string" && annotated.length > 0 ? annotated : fallback;
218
+ }
219
+
220
+ // ── Envelope shape ────────────────────────────────────────────────
221
+
222
+ const ENVELOPE_KEYS = new Set(["staticPolicies", "templates", "templateLinks"]);
223
+
224
+ /**
225
+ * Is this the JSON policy-set envelope?
226
+ *
227
+ * Deliberately strict about foreign keys: a CloudFormation template or a
228
+ * Kubernetes manifest must not walk in through a permissive shape test, and
229
+ * `detect.ts` leans on the same rule.
230
+ */
231
+ export function isPolicySetEnvelope(data: unknown): data is Record<string, unknown> {
232
+ if (!data || typeof data !== "object" || Array.isArray(data)) return false;
233
+ const keys = Object.keys(data);
234
+ if (keys.length === 0) return false;
235
+ if (!keys.every((key) => ENVELOPE_KEYS.has(key))) return false;
236
+ return "staticPolicies" in data || "templates" in data;
237
+ }
238
+
239
+ /** Normalize the three shapes `staticPolicies` is allowed to take into pairs. */
240
+ function envelopeEntries(value: unknown, fallbackPrefix: string): Array<[string, unknown]> {
241
+ if (Array.isArray(value)) return value.map((entry, index) => [`${fallbackPrefix}${index}`, entry]);
242
+ if (value && typeof value === "object") return Object.entries(value as Record<string, unknown>);
243
+ return [];
244
+ }
245
+
246
+ // ── Parser ────────────────────────────────────────────────────────
247
+
248
+ export class CedarParser {
249
+ parse(content: string): CedarParseResult {
250
+ if (!content.trim()) return { entities: [], warnings: [] };
251
+
252
+ const asJSON = tryParseJSON(content);
253
+ if (isPolicySetEnvelope(asJSON)) return this.parseEnvelope(asJSON);
254
+
255
+ return this.parseText(content);
256
+ }
257
+
258
+ // ── `.cedar` text ───────────────────────────────────────────────
259
+
260
+ private parseText(content: string): CedarParseResult {
261
+ const parts = splitPolicySet(content);
262
+ if (!parts.ok) throw new Error(`cedar: could not parse the policy document — ${parts.error}`);
263
+
264
+ const entities: CedarPolicyIR[] = [];
265
+ const warnings: string[] = [];
266
+
267
+ const collect = (
268
+ sources: string[],
269
+ kind: CedarEntityKind,
270
+ convert: (source: string) => ReturnType<typeof policyToJson>,
271
+ fallbackPrefix: string,
272
+ ): void => {
273
+ for (const [index, source] of sources.entries()) {
274
+ const converted = convert(source);
275
+ if (!converted.ok) {
276
+ warnings.push(`skipped a ${kind} that did not convert: ${converted.error}`);
277
+ continue;
278
+ }
279
+ entities.push(this.toIR(kind, converted.value, `${fallbackPrefix}${index}`, source, warnings));
280
+ }
281
+ };
282
+
283
+ collect(parts.value.policies, "policy", policyToJson, "policy");
284
+ collect(parts.value.templates, "template", templateToJson, "template");
285
+
286
+ return { entities, warnings };
287
+ }
288
+
289
+ // ── JSON policy-set envelope ────────────────────────────────────
290
+
291
+ private parseEnvelope(envelope: Record<string, unknown>): CedarParseResult {
292
+ // `staticPolicies` is allowed to be one blob of `.cedar` text, in which
293
+ // case the whole envelope is the text surface wearing a wrapper — templates
294
+ // and all, since the text is what says which is which.
295
+ const asText = typeof envelope.staticPolicies === "string" ? this.parseText(envelope.staticPolicies) : null;
296
+
297
+ const entities: CedarPolicyIR[] = [...(asText?.entities ?? [])];
298
+ const warnings: string[] = [...(asText?.warnings ?? [])];
299
+
300
+ const collect = (
301
+ value: unknown,
302
+ kind: CedarEntityKind,
303
+ convert: (source: string | PolicyJson) => ReturnType<typeof policyToJson>,
304
+ fallbackPrefix: string,
305
+ ): void => {
306
+ for (const [key, entry] of envelopeEntries(value, fallbackPrefix)) {
307
+ const converted = convert(entry as string | PolicyJson);
308
+ if (!converted.ok) {
309
+ warnings.push(`skipped ${kind} \`${key}\`: ${converted.error}`);
310
+ continue;
311
+ }
312
+ // A policy given as text carries its own layout; one given as JSON has
313
+ // none to quote, and its conditions come back as the module renders them.
314
+ const source = typeof entry === "string" ? entry : undefined;
315
+ entities.push(this.toIR(kind, converted.value, key, source, warnings));
316
+ }
317
+ };
318
+
319
+ if (!asText) collect(envelope.staticPolicies, "policy", policyToJson, "policy");
320
+ collect(envelope.templates, "template", templateToJson, "template");
321
+
322
+ const links = envelope.templateLinks;
323
+ if (Array.isArray(links) && links.length > 0) {
324
+ warnings.push(
325
+ `${links.length} template link(s) were not imported — a linked template has no authoring shape in this lexicon yet`,
326
+ );
327
+ }
328
+
329
+ return { entities, warnings };
330
+ }
331
+
332
+ private toIR(
333
+ kind: CedarEntityKind,
334
+ json: PolicyJson,
335
+ fallbackId: string,
336
+ sourceText: string | undefined,
337
+ warnings: string[],
338
+ ): CedarPolicyIR {
339
+ const name = policyId(json, fallbackId);
340
+ const { props, warnings: propWarnings } = policyProps(json, sourceText);
341
+ for (const warning of propWarnings) warnings.push(`${name}: ${warning}`);
342
+ return { kind, name, props };
343
+ }
344
+ }
345
+
346
+ function tryParseJSON(content: string): unknown {
347
+ try {
348
+ return JSON.parse(content);
349
+ } catch {
350
+ return undefined;
351
+ }
352
+ }