@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,502 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { DECLARABLE_MARKER, type Declarable } from "@intentius/chant/declarable";
3
+ import type { SerializerResult } from "@intentius/chant/serializer";
4
+ import { checkParsePolicySet, type PolicySet } from "@cedar-policy/cedar-wasm/nodejs";
5
+ import { cedarSerializer, CEDAR_JSON_FILENAME, policyIdFromLogicalName } from "./serializer";
6
+
7
+ // ── Mock entities ──────────────────────────────────────────────────
8
+ //
9
+ // The typed policy classes arrive with schema-driven codegen (#1650); until
10
+ // then the serializer is exercised against the same shape they will produce.
11
+
12
+ function mockPolicy(props: Record<string, unknown>): Declarable {
13
+ return {
14
+ [DECLARABLE_MARKER]: true,
15
+ lexicon: "cedar",
16
+ entityType: "Cedar::Policy",
17
+ kind: "resource",
18
+ props,
19
+ } as unknown as Declarable;
20
+ }
21
+
22
+ function mockProperty(entityType: string, props: Record<string, unknown>): Declarable {
23
+ return {
24
+ [DECLARABLE_MARKER]: true,
25
+ lexicon: "cedar",
26
+ entityType,
27
+ kind: "property",
28
+ props,
29
+ } as unknown as Declarable;
30
+ }
31
+
32
+ function entitiesOf(...pairs: Array<[string, Declarable]>): Map<string, Declarable> {
33
+ return new Map(pairs);
34
+ }
35
+
36
+ /** Every non-empty serialization is a SerializerResult — text plus JSON. */
37
+ function result(entities: Map<string, Declarable>): SerializerResult {
38
+ const out = cedarSerializer.serialize(entities);
39
+ if (typeof out === "string") throw new Error(`expected a SerializerResult, got: ${JSON.stringify(out)}`);
40
+ return out;
41
+ }
42
+
43
+ function text(entities: Map<string, Declarable>): string {
44
+ return result(entities).primary;
45
+ }
46
+
47
+ function json(entities: Map<string, Declarable>): {
48
+ staticPolicies: Record<string, Record<string, unknown>>;
49
+ templates: Record<string, Record<string, unknown>>;
50
+ templateLinks: unknown[];
51
+ } {
52
+ const files = result(entities).files ?? {};
53
+ return JSON.parse(files[CEDAR_JSON_FILENAME] as string);
54
+ }
55
+
56
+ // ── 1–2. Identity ──────────────────────────────────────────────────
57
+
58
+ describe("cedar serializer identity", () => {
59
+ test("name is the lexicon name", () => {
60
+ expect(cedarSerializer.name).toBe("cedar");
61
+ });
62
+
63
+ test("rulePrefix is CED", () => {
64
+ expect(cedarSerializer.rulePrefix).toBe("CED");
65
+ });
66
+ });
67
+
68
+ // ── 3. Empty ───────────────────────────────────────────────────────
69
+
70
+ describe("empty input", () => {
71
+ test("an empty map serializes to an empty string", () => {
72
+ expect(cedarSerializer.serialize(new Map())).toBe("");
73
+ });
74
+
75
+ test("a map with nothing this lexicon owns also serializes to an empty string", () => {
76
+ const foreign = {
77
+ [DECLARABLE_MARKER]: true,
78
+ lexicon: "docker",
79
+ entityType: "Docker::Compose::Service",
80
+ kind: "resource",
81
+ props: { image: "nginx" },
82
+ } as unknown as Declarable;
83
+ expect(cedarSerializer.serialize(entitiesOf(["api", foreign]))).toBe("");
84
+ });
85
+ });
86
+
87
+ // ── 4. Single resource ─────────────────────────────────────────────
88
+
89
+ describe("a single policy", () => {
90
+ const entities = entitiesOf([
91
+ "allowAliceRead",
92
+ mockPolicy({
93
+ effect: "permit",
94
+ principal: { eq: 'User::"alice"' },
95
+ action: { eq: 'Action::"read"' },
96
+ resource: { is: "Photo" },
97
+ }),
98
+ ]);
99
+
100
+ test("produces canonical Cedar policy text", () => {
101
+ expect(text(entities)).toBe(
102
+ `@id("allow-alice-read")
103
+ permit (
104
+ principal == User::"alice",
105
+ action == Action::"read",
106
+ resource is Photo
107
+ );
108
+ `,
109
+ );
110
+ });
111
+
112
+ test("produces a Cedar JSON policy-set file alongside it", () => {
113
+ const doc = json(entities);
114
+ expect(Object.keys(doc.staticPolicies)).toEqual(["allow-alice-read"]);
115
+ expect(doc.templates).toEqual({});
116
+ expect(doc.templateLinks).toEqual([]);
117
+ expect(doc.staticPolicies["allow-alice-read"]).toMatchObject({
118
+ effect: "permit",
119
+ principal: { op: "==", entity: { type: "User", id: "alice" } },
120
+ action: { op: "==", entity: { type: "Action", id: "read" } },
121
+ resource: { op: "is", entity_type: "Photo" },
122
+ });
123
+ });
124
+
125
+ test("the JSON file is the only extra file emitted", () => {
126
+ expect(Object.keys(result(entities).files ?? {})).toEqual([CEDAR_JSON_FILENAME]);
127
+ });
128
+ });
129
+
130
+ // ── 5. Generated name ──────────────────────────────────────────────
131
+
132
+ describe("policy id generation", () => {
133
+ test("a camelCase export becomes a kebab-case @id", () => {
134
+ const out = text(entitiesOf(["allowAdminReadAll", mockPolicy({ effect: "permit" })]));
135
+ expect(out).toContain('@id("allow-admin-read-all")');
136
+ });
137
+
138
+ test("acronyms and underscores collapse the same way", () => {
139
+ expect(policyIdFromLogicalName("allowMFAUsers")).toBe("allow-mfa-users");
140
+ expect(policyIdFromLogicalName("legacy_policy_name")).toBe("legacy-policy-name");
141
+ expect(policyIdFromLogicalName("simple")).toBe("simple");
142
+ });
143
+ });
144
+
145
+ // ── 6. Explicit name preserved ─────────────────────────────────────
146
+
147
+ describe("explicit policy id", () => {
148
+ const entities = entitiesOf([
149
+ "allowAdminRead",
150
+ mockPolicy({ effect: "permit", annotations: { id: "SOC2-AC-3" } }),
151
+ ]);
152
+
153
+ test("an author-set @id wins over the derived one", () => {
154
+ const out = text(entities);
155
+ expect(out).toContain('@id("SOC2-AC-3")');
156
+ expect(out).not.toContain("allow-admin-read");
157
+ });
158
+
159
+ test("the JSON policy is keyed by the same id", () => {
160
+ expect(Object.keys(json(entities).staticPolicies)).toEqual(["SOC2-AC-3"]);
161
+ });
162
+ });
163
+
164
+ // ── 7. Multi-policy join ───────────────────────────────────────────
165
+
166
+ describe("multiple policies", () => {
167
+ const entities = entitiesOf(
168
+ ["allowRead", mockPolicy({ effect: "permit", action: { eq: 'Action::"read"' } })],
169
+ ["denyDelete", mockPolicy({ effect: "forbid", action: { eq: 'Action::"delete"' } })],
170
+ );
171
+
172
+ test("policies are separated by a blank line and each ends in a semicolon", () => {
173
+ const out = text(entities);
174
+ expect(out.split("\n\n")).toHaveLength(2);
175
+ expect(out.endsWith(";\n")).toBe(true);
176
+ expect(out.match(/;/g)).toHaveLength(2);
177
+ });
178
+
179
+ test("both land in the JSON policy set, in declaration order", () => {
180
+ expect(Object.keys(json(entities).staticPolicies)).toEqual(["allow-read", "deny-delete"]);
181
+ });
182
+ });
183
+
184
+ // ── 8. Defaults ────────────────────────────────────────────────────
185
+
186
+ describe("defaults", () => {
187
+ test("omitted scopes emit the unconstrained Cedar form", () => {
188
+ const out = text(entitiesOf(["allowAnything", mockPolicy({ effect: "permit" })]));
189
+ expect(out).toContain("permit (\n principal,\n action,\n resource\n)");
190
+ });
191
+
192
+ test("an omitted effect defaults to permit", () => {
193
+ expect(text(entitiesOf(["bare", mockPolicy({})]))).toContain("permit (");
194
+ });
195
+
196
+ test("an unconstrained scope is op: All in the JSON form", () => {
197
+ const doc = json(entitiesOf(["allowAnything", mockPolicy({})]));
198
+ expect(doc.staticPolicies["allow-anything"]).toMatchObject({
199
+ principal: { op: "All" },
200
+ action: { op: "All" },
201
+ resource: { op: "All" },
202
+ conditions: [],
203
+ });
204
+ });
205
+ });
206
+
207
+ // ── 9. Overrides win ───────────────────────────────────────────────
208
+
209
+ describe("overrides", () => {
210
+ test("an explicit forbid overrides the permit default", () => {
211
+ const out = text(entitiesOf(["denyAll", mockPolicy({ effect: "forbid" })]));
212
+ expect(out).toContain("forbid (");
213
+ expect(out).not.toContain("permit (");
214
+ });
215
+
216
+ test("an explicit scope overrides the unconstrained default", () => {
217
+ const out = text(entitiesOf([
218
+ "scoped",
219
+ mockPolicy({ principal: { in: 'Group::"admins"' } }),
220
+ ]));
221
+ expect(out).toContain('principal in Group::"admins",');
222
+ });
223
+ });
224
+
225
+ // ── 10. kind: "property" is skipped ────────────────────────────────
226
+
227
+ describe("property-kind entities", () => {
228
+ test("a property Declarable is not emitted as its own policy", () => {
229
+ const entities = entitiesOf(
230
+ ["allowRead", mockPolicy({ effect: "permit" })],
231
+ ["someScope", mockProperty("Cedar::Scope", { eq: 'User::"alice"' })],
232
+ );
233
+ expect(text(entities).match(/;/g)).toHaveLength(1);
234
+ expect(Object.keys(json(entities).staticPolicies)).toEqual(["allow-read"]);
235
+ });
236
+
237
+ test("a property Declarable referenced from a policy is inlined, not named", () => {
238
+ const scope = mockProperty("Cedar::Scope", { eq: 'User::"alice"' });
239
+ const entities = entitiesOf(
240
+ ["allowRead", mockPolicy({ effect: "permit", principal: scope })],
241
+ ["aliceScope", scope],
242
+ );
243
+ expect(text(entities)).toContain('principal == User::"alice",');
244
+ });
245
+ });
246
+
247
+ // ── 11. Key ordering ───────────────────────────────────────────────
248
+
249
+ describe("key ordering", () => {
250
+ test("scope clauses emit principal, action, resource regardless of prop order", () => {
251
+ const out = text(entitiesOf([
252
+ "shuffled",
253
+ mockPolicy({
254
+ resource: { is: "Photo" },
255
+ when: ["resource.owner == principal"],
256
+ action: { eq: 'Action::"view"' },
257
+ effect: "permit",
258
+ principal: { is: "User" },
259
+ }),
260
+ ]));
261
+ const lines = out.trim().split("\n");
262
+ expect(lines).toEqual([
263
+ '@id("shuffled")',
264
+ "permit (",
265
+ " principal is User,",
266
+ ' action == Action::"view",',
267
+ " resource is Photo",
268
+ ")",
269
+ "when { resource.owner == principal };",
270
+ ]);
271
+ });
272
+
273
+ test("@id precedes the author's own annotations", () => {
274
+ const out = text(entitiesOf([
275
+ "annotated",
276
+ mockPolicy({ annotations: { owner: "platform", ticket: "SEC-12" } }),
277
+ ]));
278
+ expect(out.split("\n").slice(0, 3)).toEqual([
279
+ '@id("annotated")',
280
+ '@owner("platform")',
281
+ '@ticket("SEC-12")',
282
+ ]);
283
+ });
284
+ });
285
+
286
+ // ── 12. Cedar-specific: forbid with unless ─────────────────────────
287
+
288
+ describe("forbid with guards", () => {
289
+ const entities = entitiesOf([
290
+ "denyUnlessMfa",
291
+ mockPolicy({
292
+ effect: "forbid",
293
+ principal: { is: "User", in: 'Group::"contractors"' },
294
+ action: { in: ['Action::"delete"', 'Action::"purge"'] },
295
+ when: ["resource.classification == \"restricted\""],
296
+ unless: ["context.mfaAuthenticated", "principal.isBreakGlass"],
297
+ }),
298
+ ]);
299
+
300
+ test("renders is/in, an action set, and every guard clause", () => {
301
+ expect(text(entities)).toBe(
302
+ `@id("deny-unless-mfa")
303
+ forbid (
304
+ principal is User in Group::"contractors",
305
+ action in [Action::"delete", Action::"purge"],
306
+ resource
307
+ )
308
+ when { resource.classification == "restricted" }
309
+ unless { context.mfaAuthenticated }
310
+ unless { principal.isBreakGlass };
311
+ `,
312
+ );
313
+ });
314
+
315
+ test("the JSON form carries the same scope and guards", () => {
316
+ expect(json(entities).staticPolicies["deny-unless-mfa"]).toMatchObject({
317
+ effect: "forbid",
318
+ principal: { op: "is", entity_type: "User", in: { entity: { type: "Group", id: "contractors" } } },
319
+ action: {
320
+ op: "in",
321
+ entities: [
322
+ { type: "Action", id: "delete" },
323
+ { type: "Action", id: "purge" },
324
+ ],
325
+ },
326
+ resource: { op: "All" },
327
+ conditions: [
328
+ {
329
+ kind: "when",
330
+ body: {
331
+ "==": {
332
+ left: { ".": { left: { Var: "resource" }, attr: "classification" } },
333
+ right: { Value: "restricted" },
334
+ },
335
+ },
336
+ },
337
+ { kind: "unless", body: { ".": { left: { Var: "context" }, attr: "mfaAuthenticated" } } },
338
+ { kind: "unless", body: { ".": { left: { Var: "principal" }, attr: "isBreakGlass" } } },
339
+ ],
340
+ });
341
+ });
342
+ });
343
+
344
+ // ── The JSON companion is real Cedar, not a chant-shaped file ──────
345
+ //
346
+ // It shipped writing `when` bodies as `{ "__expr": "<text>" }`, which is not a
347
+ // key in Cedar's JSON grammar — the module answers `unknown variant '__expr'`
348
+ // (#1664). Asserting the document is well-formed JSON never caught it; only
349
+ // handing it back to Cedar does.
350
+
351
+ describe("the JSON companion as Cedar sees it", () => {
352
+ const entities = entitiesOf(
353
+ [
354
+ "denyUnlessMfa",
355
+ mockPolicy({
356
+ effect: "forbid",
357
+ principal: { is: "User", in: 'Group::"contractors"' },
358
+ action: { in: ['Action::"delete"', 'Action::"purge"'] },
359
+ when: ['resource.classification == "restricted"'],
360
+ unless: ["context.mfaAuthenticated"],
361
+ }),
362
+ ],
363
+ ["allowRead", mockPolicy({ action: { eq: 'Action::"read"' } })],
364
+ );
365
+
366
+ test("the emitted policy set parses as Cedar JSON", () => {
367
+ const answer = checkParsePolicySet(json(entities) as unknown as PolicySet);
368
+ expect(answer.type, JSON.stringify(answer)).toBe("success");
369
+ });
370
+
371
+ test("the emitted text parses too, and both describe the same policies", () => {
372
+ const answer = checkParsePolicySet({ staticPolicies: text(entities) });
373
+ expect(answer.type, JSON.stringify(answer)).toBe("success");
374
+ expect(Object.keys(json(entities).staticPolicies)).toEqual(["deny-unless-mfa", "allow-read"]);
375
+ });
376
+
377
+ test("golden: the expression trees are exactly what the module makes of the text", () => {
378
+ // `policyToJson` is deterministic with sorted keys (#1648 §5.2), so this
379
+ // compares safely. `policyToText` is not golden-testable and is not used.
380
+ expect(json(entities).staticPolicies["deny-unless-mfa"]).toEqual({
381
+ effect: "forbid",
382
+ principal: { op: "is", entity_type: "User", in: { entity: { type: "Group", id: "contractors" } } },
383
+ action: {
384
+ op: "in",
385
+ entities: [
386
+ { type: "Action", id: "delete" },
387
+ { type: "Action", id: "purge" },
388
+ ],
389
+ },
390
+ resource: { op: "All" },
391
+ conditions: [
392
+ {
393
+ kind: "when",
394
+ body: {
395
+ "==": {
396
+ left: { ".": { left: { Var: "resource" }, attr: "classification" } },
397
+ right: { Value: "restricted" },
398
+ },
399
+ },
400
+ },
401
+ { kind: "unless", body: { ".": { left: { Var: "context" }, attr: "mfaAuthenticated" } } },
402
+ ],
403
+ annotations: { id: "deny-unless-mfa" },
404
+ });
405
+ });
406
+
407
+ test("a policy carrying a slot is filed as a template, because Cedar says it is one", () => {
408
+ const doc = json(entitiesOf([
409
+ "shareWithSlots",
410
+ mockPolicy({ principal: { eq: "?principal" }, resource: { in: "?resource" } }),
411
+ ]));
412
+ expect(Object.keys(doc.staticPolicies)).toEqual([]);
413
+ expect(Object.keys(doc.templates)).toEqual(["share-with-slots"]);
414
+ expect(checkParsePolicySet(doc as unknown as PolicySet).type).toBe("success");
415
+ });
416
+
417
+ test("text the module rejects yields a warning and no JSON file, not a broken one", () => {
418
+ const out = result(entitiesOf(["broken", mockPolicy({ when: ["=== nonsense ==="] })]));
419
+ expect(out.files).toBeUndefined();
420
+ expect(out.warnings?.join(" ")).toContain(`${CEDAR_JSON_FILENAME} was not written`);
421
+ expect(out.primary).toContain("=== nonsense ===");
422
+ });
423
+ });
424
+
425
+ // ── Cedar-specific: annotations ────────────────────────────────────
426
+
427
+ describe("annotations", () => {
428
+ test("each annotation becomes its own @key(\"value\") line", () => {
429
+ const out = text(entitiesOf([
430
+ "documented",
431
+ mockPolicy({ annotations: { description: "read-only access", owner: "platform" } }),
432
+ ]));
433
+ expect(out).toContain('@description("read-only access")');
434
+ expect(out).toContain('@owner("platform")');
435
+ });
436
+
437
+ test("quotes and backslashes in an annotation value are escaped", () => {
438
+ const out = text(entitiesOf([
439
+ "quoted",
440
+ mockPolicy({ annotations: { note: 'say "hi"\\done' } }),
441
+ ]));
442
+ expect(out).toContain('@note("say \\"hi\\"\\\\done")');
443
+ });
444
+
445
+ test("annotations reach the JSON form with the id merged in", () => {
446
+ const doc = json(entitiesOf([
447
+ "documented",
448
+ mockPolicy({ annotations: { owner: "platform" } }),
449
+ ]));
450
+ expect(doc.staticPolicies["documented"].annotations).toEqual({
451
+ id: "documented",
452
+ owner: "platform",
453
+ });
454
+ });
455
+ });
456
+
457
+ // ── Cedar-specific: entity UID parsing in the JSON form ────────────
458
+
459
+ describe("entity UID handling", () => {
460
+ test("a namespaced type keeps its namespace", () => {
461
+ const doc = json(entitiesOf([
462
+ "namespaced",
463
+ mockPolicy({ principal: { eq: 'PhotoApp::User::"alice"' } }),
464
+ ]));
465
+ expect(doc.staticPolicies["namespaced"].principal).toEqual({
466
+ op: "==",
467
+ entity: { type: "PhotoApp::User", id: "alice" },
468
+ });
469
+ });
470
+
471
+ test("a single in-scope emits entity, a set of two emits entities", () => {
472
+ const doc = json(entitiesOf([
473
+ "grouped",
474
+ mockPolicy({
475
+ principal: { in: 'Group::"admins"' },
476
+ action: { in: ['Action::"read"', 'Action::"list"'] },
477
+ }),
478
+ ]));
479
+ expect(doc.staticPolicies["grouped"].principal).toEqual({
480
+ op: "in",
481
+ entity: { type: "Group", id: "admins" },
482
+ });
483
+ expect(doc.staticPolicies["grouped"].action).toEqual({
484
+ op: "in",
485
+ entities: [
486
+ { type: "Action", id: "read" },
487
+ { type: "Action", id: "list" },
488
+ ],
489
+ });
490
+ });
491
+
492
+ test("a one-element action set is Cedar's own normalization of `in E`", () => {
493
+ // `action in [E]` and `action in E` are one constraint. The JSON leg is now
494
+ // whatever the module makes of the emitted text, so the brackets are gone
495
+ // by the time it is written — which is Cedar's reading, not a lost prop.
496
+ const doc = json(entitiesOf(["single", mockPolicy({ action: { in: ['Action::"read"'] } })]));
497
+ expect(doc.staticPolicies["single"].action).toEqual({
498
+ op: "in",
499
+ entity: { type: "Action", id: "read" },
500
+ });
501
+ });
502
+ });