@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,315 @@
1
+ /**
2
+ * The `cedar-wasm` seam for the cedar post-synth checks.
3
+ *
4
+ * Cedar's real parser and validator ship as `@cedar-policy/cedar-wasm`, which
5
+ * runs in-process — no CLI on PATH, no Docker. Everything the checks need from
6
+ * it goes through this module so the traps live in one place. The traps, all
7
+ * of them empirically verified against 4.12.0 (chant #1648):
8
+ *
9
+ * 1. **`type: "success"` does not mean valid.** `failure` means the *call*
10
+ * failed; `success` means validation *ran*, and can carry any number of
11
+ * `validationErrors`. {@link validatePolicySet} asserts both.
12
+ * 2. **`validationErrors` ordering is non-deterministic** — the validator
13
+ * parallelizes across policies and never re-sorts. Forty identical calls
14
+ * produced six distinct orderings. Everything this module returns is sorted
15
+ * by `policyId` then `message`, so a check's output is stable.
16
+ * 3. **Malformed input throws instead of returning a failure answer.** Missing
17
+ * fields on a call struct raise a bare serde `Error` whose `line 1 column N`
18
+ * refers to the internal call struct, not to anything the user wrote — it is
19
+ * meaningless to surface. {@link call} wraps every entry point and normalizes
20
+ * both channels into one result type.
21
+ * 4. **A bare-string policy set gets synthesized ids** (`policy0`, `policy1`),
22
+ * and `@id` annotations do *not* become the policy id. Everything here
23
+ * passes `staticPolicies` as a `Record<PolicyId, Policy>` keyed by chant's
24
+ * own ids, so a diagnostic names something a reader can find.
25
+ * 5. **Only `strict` validation mode exists** in this build; any other value
26
+ * throws. The settings are left at their default rather than passed.
27
+ *
28
+ * Only the `/nodejs` flavor is imported. The root/`esm` flavor imports the
29
+ * `.wasm` as an ES module, which Bun cannot start (`__wbindgen_start is not a
30
+ * function`) and Node only supports behind an experimental warning printed on
31
+ * every run; `/web` needs an explicit `initSync`. `/nodejs` is the one flavor
32
+ * that loads under `node`, `tsx`, `vitest`, and `bun` alike.
33
+ *
34
+ * Excluded from check auto-discovery by the "helper" filename filter.
35
+ */
36
+ import { createRequire } from "module";
37
+ import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
38
+ import type * as CedarWasm from "@cedar-policy/cedar-wasm/nodejs";
39
+ import {
40
+ isRecord,
41
+ type CedarPolicyJson,
42
+ type CedarPolicySet,
43
+ type CedarPolicySetDoc,
44
+ } from "./cedar-helpers";
45
+
46
+ // ── Loading ───────────────────────────────────────────────────────
47
+
48
+ type Wasm = typeof CedarWasm;
49
+
50
+ let cached: Wasm | undefined;
51
+ let loadError: string | undefined;
52
+
53
+ /**
54
+ * The wasm module, loaded on first use.
55
+ *
56
+ * Lazy and `require`-based on purpose. The post-synth barrel is imported
57
+ * statically by `plugin.ts`, and every `chant lint`/`chant audit` run loads
58
+ * every active lexicon's plugin — a top-level import would pull 4 MB of wasm
59
+ * into any build that merely has cedar installed. `check()` is synchronous, so
60
+ * `await import()` is not available; `createRequire` is. The `/nodejs` flavor
61
+ * is CommonJS with a synchronous `readFileSync` init, so there is nothing to
62
+ * await anyway.
63
+ */
64
+ export function loadWasm(): Wasm | undefined {
65
+ if (cached || loadError) return cached;
66
+ try {
67
+ const require = createRequire(import.meta.url);
68
+ cached = require("@cedar-policy/cedar-wasm/nodejs") as Wasm;
69
+ } catch (err) {
70
+ loadError = err instanceof Error ? err.message : String(err);
71
+ }
72
+ return cached;
73
+ }
74
+
75
+ /** Why the wasm could not be loaded, if it could not be. */
76
+ export function wasmLoadError(): string | undefined {
77
+ loadWasm();
78
+ return loadError;
79
+ }
80
+
81
+ /** Reset the module cache. Test-only — lets a test exercise the load-failure path. */
82
+ export function resetWasmCache(): void {
83
+ cached = undefined;
84
+ loadError = undefined;
85
+ }
86
+
87
+ // ── Calling ───────────────────────────────────────────────────────
88
+
89
+ export type WasmResult<T> = { ok: true; value: T } | { ok: false; message: string };
90
+
91
+ /**
92
+ * Run a wasm call, turning a thrown serde error into a result rather than an
93
+ * exception (trap 3 above). The thrown message is deliberately not passed
94
+ * through verbatim: its positions point into the internal call struct.
95
+ */
96
+ export function call<T>(what: string, fn: () => T): WasmResult<T> {
97
+ try {
98
+ return { ok: true, value: fn() };
99
+ } catch (err) {
100
+ const raw = err instanceof Error ? err.message : String(err);
101
+ // Strip the internal `at line 1 column 42` suffix — it locates a field in
102
+ // the serialized call struct, never anything in the user's policy.
103
+ const message = raw.replace(/\s+at line \d+ column \d+\.?$/, "");
104
+ return { ok: false, message: `${what} could not be run by cedar-wasm: ${message}` };
105
+ }
106
+ }
107
+
108
+ /** The first line of a Cedar `DetailedError`, with its `help` when it has one. */
109
+ export function describeError(error: { message: string; help?: string | null }): string {
110
+ return error.help ? `${error.message} (${error.help})` : error.message;
111
+ }
112
+
113
+ // ── Condition bodies ──────────────────────────────────────────────
114
+
115
+ /**
116
+ * chant's serializer carries a `when`/`unless` body as Cedar expression
117
+ * *source* under a `__expr` key, because the authored model holds expression
118
+ * strings until schema-driven codegen (#1650) makes them typed trees. `__expr`
119
+ * is not part of Cedar's JSON policy grammar — the deserializer rejects it
120
+ * ("unknown variant `__expr`") — so anything handed to the wasm has to have
121
+ * those bodies parsed into real expression JSON first.
122
+ *
123
+ * There is no expression-level entry point in the package, so the expression
124
+ * is parsed inside a throwaway policy and the resulting condition body lifted
125
+ * back out. A source expression that does not parse is the finding CEDC011
126
+ * reports.
127
+ */
128
+ export const EXPR_KEY = "__expr";
129
+
130
+ export interface ExpressionFailure {
131
+ /** The policy key the clause belongs to. */
132
+ key: string;
133
+ /** `when` or `unless`, as the document spelled it. */
134
+ kind: string;
135
+ /** The expression source that failed. */
136
+ expression: string;
137
+ message: string;
138
+ }
139
+
140
+ export interface NormalizedPolicySet {
141
+ /** A policy set the wasm will accept, keyed by chant's own policy ids. */
142
+ policySet: CedarWasm.PolicySet;
143
+ /** Condition bodies whose expression source did not parse. */
144
+ expressionFailures: ExpressionFailure[];
145
+ }
146
+
147
+ function parseExpression(wasm: Wasm, expression: string): WasmResult<unknown> {
148
+ const answer = call("condition expression", () =>
149
+ wasm.policyToJson(`permit(principal, action, resource) when { ${expression} };`),
150
+ );
151
+ if (!answer.ok) return answer;
152
+ if (answer.value.type === "failure") {
153
+ return { ok: false, message: answer.value.errors.map(describeError).join("; ") };
154
+ }
155
+ const conditions = answer.value.json.conditions;
156
+ const body = Array.isArray(conditions) && isRecord(conditions[0]) ? conditions[0].body : undefined;
157
+ if (body === undefined) return { ok: false, message: "expression parsed to no condition body" };
158
+ return { ok: true, value: body };
159
+ }
160
+
161
+ /** Replace every `__expr` body in a policy set with parsed expression JSON. */
162
+ export function normalizePolicySet(wasm: Wasm, set: CedarPolicySet): NormalizedPolicySet {
163
+ const staticPolicies: Record<string, unknown> = {};
164
+ const templates: Record<string, unknown> = {};
165
+ const expressionFailures: ExpressionFailure[] = [];
166
+
167
+ for (const entry of set.entries) {
168
+ const target = entry.template ? templates : staticPolicies;
169
+ target[entry.key] = normalizePolicy(wasm, entry.key, entry.policy, expressionFailures);
170
+ }
171
+
172
+ const doc: CedarPolicySetDoc = set.doc ?? {};
173
+ const policySet = {
174
+ staticPolicies,
175
+ templates,
176
+ templateLinks: Array.isArray(doc.templateLinks) ? doc.templateLinks : [],
177
+ } as unknown as CedarWasm.PolicySet;
178
+
179
+ return { policySet, expressionFailures };
180
+ }
181
+
182
+ function normalizePolicy(
183
+ wasm: Wasm,
184
+ key: string,
185
+ policy: CedarPolicyJson,
186
+ failures: ExpressionFailure[],
187
+ ): CedarPolicyJson {
188
+ if (!Array.isArray(policy.conditions)) return policy;
189
+
190
+ const conditions = policy.conditions.map((clause) => {
191
+ if (!isRecord(clause) || !isRecord(clause.body)) return clause;
192
+ const expression = clause.body[EXPR_KEY];
193
+ if (typeof expression !== "string") return clause;
194
+
195
+ const parsed = parseExpression(wasm, expression);
196
+ if (!parsed.ok) {
197
+ failures.push({
198
+ key,
199
+ kind: typeof clause.kind === "string" ? clause.kind : "when",
200
+ expression,
201
+ message: parsed.message,
202
+ });
203
+ // Leave a body the deserializer accepts so the rest of the set still
204
+ // validates — the unparseable clause is reported by CEDC011, not by a
205
+ // cascade of confusing structural errors from every other check.
206
+ return { ...clause, body: { Value: true } };
207
+ }
208
+ return { ...clause, body: parsed.value };
209
+ });
210
+
211
+ return { ...policy, conditions };
212
+ }
213
+
214
+ // ── Schema discovery ──────────────────────────────────────────────
215
+
216
+ /** A Cedar schema found beside the policies in the build output. */
217
+ export interface DiscoveredSchema {
218
+ /** Human-readable `.cedarschema` source, or a parsed JSON schema object. */
219
+ schema: CedarWasm.Schema;
220
+ /** The filename it came from. */
221
+ source: string;
222
+ }
223
+
224
+ /**
225
+ * The project's Cedar schema, if the build emitted one beside the policies.
226
+ *
227
+ * Two forms, both of which the wasm accepts — but only as the right JS type: a
228
+ * `string` is *always* read as human-readable `.cedarschema`, never as JSON, so
229
+ * a `.cedarschema.json` file has to be `JSON.parse`d into a live object first
230
+ * or the parser reports "unexpected token `{`" against perfectly valid JSON.
231
+ *
232
+ * Reads `ctx.outputs` only. Nothing emits a schema today — schema-driven
233
+ * codegen is #1650 — which is exactly why CEDE010 says so out loud instead of
234
+ * passing silently.
235
+ */
236
+ export function findSchema(ctx: PostSynthContext): DiscoveredSchema | undefined {
237
+ for (const [, output] of ctx.outputs) {
238
+ if (typeof output === "string") continue;
239
+ for (const [filename, content] of Object.entries(output.files ?? {})) {
240
+ if (typeof content !== "string") continue;
241
+ if (filename.endsWith(".cedarschema.json")) {
242
+ try {
243
+ const parsed: unknown = JSON.parse(content);
244
+ if (isRecord(parsed)) return { schema: parsed as CedarWasm.Schema, source: filename };
245
+ } catch {
246
+ continue; // a schema that is not JSON is not a schema we can use
247
+ }
248
+ } else if (filename.endsWith(".cedarschema")) {
249
+ return { schema: content, source: filename };
250
+ }
251
+ }
252
+ }
253
+ return undefined;
254
+ }
255
+
256
+ // ── Validation ────────────────────────────────────────────────────
257
+
258
+ /** One validator finding, already flattened and safe to sort. */
259
+ export interface ValidationFinding {
260
+ policyId: string;
261
+ message: string;
262
+ }
263
+
264
+ export interface ValidationOutcome {
265
+ /** Sorted by `policyId` then `message` — the wasm's own order is not stable. */
266
+ errors: ValidationFinding[];
267
+ /** Same sort. */
268
+ warnings: ValidationFinding[];
269
+ /** Set when the call itself failed (a parse error, a malformed call struct). */
270
+ failure?: string;
271
+ }
272
+
273
+ function sortFindings(findings: ValidationFinding[]): ValidationFinding[] {
274
+ return [...findings].sort(
275
+ (a, b) => a.policyId.localeCompare(b.policyId) || a.message.localeCompare(b.message),
276
+ );
277
+ }
278
+
279
+ function flatten(errors: CedarWasm.ValidationError[]): ValidationFinding[] {
280
+ return errors.map((e) => ({ policyId: e.policyId, message: describeError(e.error) }));
281
+ }
282
+
283
+ /** Validate a normalized policy set against a schema. Never throws. */
284
+ export function validatePolicySet(
285
+ wasm: Wasm,
286
+ policySet: CedarWasm.PolicySet,
287
+ schema: CedarWasm.Schema,
288
+ ): ValidationOutcome {
289
+ const answer = call("validate", () => wasm.validate({ schema, policies: policySet }));
290
+ if (!answer.ok) return { errors: [], warnings: [], failure: answer.message };
291
+
292
+ if (answer.value.type === "failure") {
293
+ return {
294
+ errors: [],
295
+ warnings: [],
296
+ failure: answer.value.errors.map(describeError).join("; "),
297
+ };
298
+ }
299
+
300
+ // `success` only means validation RAN. The findings are in the arrays.
301
+ return {
302
+ errors: sortFindings(flatten(answer.value.validationErrors)),
303
+ warnings: sortFindings(flatten(answer.value.validationWarnings)),
304
+ };
305
+ }
306
+
307
+ /** Parse-only gate, for when there is no schema to validate against. */
308
+ export function parsePolicySet(wasm: Wasm, policySet: CedarWasm.PolicySet): WasmResult<string[]> {
309
+ const answer = call("checkParsePolicySet", () => wasm.checkParsePolicySet(policySet));
310
+ if (!answer.ok) return answer;
311
+ if (answer.value.type === "failure") {
312
+ return { ok: true, value: answer.value.errors.map(describeError).sort() };
313
+ }
314
+ return { ok: true, value: [] };
315
+ }
@@ -0,0 +1,12 @@
1
+ import type { LintRule } from "@intentius/chant/lint/rule";
2
+ import { cedarPolicyShapeRule } from "./policy-shape";
3
+
4
+ export { cedarPolicyShapeRule, CEDAR_EFFECTS } from "./policy-shape";
5
+
6
+ /**
7
+ * All lint rules provided by this lexicon (imported by plugin.ts's lintRules()).
8
+ *
9
+ * Pre-synth rules are wired by hand — unlike post-synth checks, they are not
10
+ * auto-discovered. Add the import above and the entry below.
11
+ */
12
+ export const rules: LintRule[] = [cedarPolicyShapeRule];
@@ -0,0 +1,90 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { createMockLintContext } from "@intentius/chant-test-utils";
3
+ import { cedarPolicyShapeRule } from "./policy-shape";
4
+ import { rules } from ".";
5
+
6
+ function check(source: string, filePath = "policies.ts") {
7
+ return cedarPolicyShapeRule.check(createMockLintContext(source, filePath));
8
+ }
9
+
10
+ const IMPORT = `import { Policy } from "@intentius/chant-lexicon-cedar";\n`;
11
+
12
+ describe("CEDC001: Cedar policy shape", () => {
13
+ test("is wired into the lexicon's rule list", () => {
14
+ expect(rules.map((r) => r.id)).toContain("CEDC001");
15
+ expect(cedarPolicyShapeRule.severity).toBe("error");
16
+ expect(cedarPolicyShapeRule.category).toBe("correctness");
17
+ });
18
+
19
+ test('flags effect: "deny" — the word every other policy language uses', () => {
20
+ const diags = check(`${IMPORT}export const p = new Policy("blockOutsiders", { effect: "deny" });`);
21
+ expect(diags).toHaveLength(1);
22
+ expect(diags[0].ruleId).toBe("CEDC001");
23
+ expect(diags[0].severity).toBe("error");
24
+ expect(diags[0].message).toContain('"deny"');
25
+ expect(diags[0].line).toBe(2);
26
+ expect(diags[0].column).toBeGreaterThan(0);
27
+ });
28
+
29
+ test("flags any other non-Cedar effect", () => {
30
+ const diags = check(`${IMPORT}export const p = new Policy("p", { effect: "Permit" });`);
31
+ expect(diags).toHaveLength(1);
32
+ expect(diags[0].message).toContain("Permit");
33
+ });
34
+
35
+ test("flags an empty when entry", () => {
36
+ const diags = check(`${IMPORT}export const p = new Policy("p", { when: ["", "context.mfa"] });`);
37
+ expect(diags).toHaveLength(1);
38
+ expect(diags[0].message).toContain("empty when clause");
39
+ });
40
+
41
+ test("flags a whitespace-only unless entry", () => {
42
+ const diags = check(`${IMPORT}export const p = new Policy("p", { unless: [" "] });`);
43
+ expect(diags).toHaveLength(1);
44
+ expect(diags[0].message).toContain("empty unless clause");
45
+ });
46
+
47
+ test("flags an empty guard given as a bare string rather than an array", () => {
48
+ const diags = check(`${IMPORT}export const p = new Policy("p", { when: "" });`);
49
+ expect(diags).toHaveLength(1);
50
+ });
51
+
52
+ test("reports both problems on one declaration", () => {
53
+ const diags = check(`${IMPORT}export const p = new Policy("p", { effect: "allow", when: [""] });`);
54
+ expect(diags).toHaveLength(2);
55
+ });
56
+
57
+ test("works on a factory call as well as a constructor", () => {
58
+ const source = `import { policy } from "@intentius/chant-lexicon-cedar";\nexport const p = policy({ effect: "deny" });`;
59
+ expect(check(source)).toHaveLength(1);
60
+ });
61
+
62
+ test("passes a well-formed permit", () => {
63
+ const source = `${IMPORT}export const p = new Policy("allowAdminRead", {
64
+ effect: "permit",
65
+ principal: { in: 'Chant::Group::"admins"' },
66
+ when: ["context.mfa == true"],
67
+ });`;
68
+ expect(check(source)).toHaveLength(0);
69
+ });
70
+
71
+ test("passes a well-formed forbid, and an omitted effect", () => {
72
+ expect(check(`${IMPORT}export const p = new Policy("p", { effect: "forbid" });`)).toHaveLength(0);
73
+ expect(check(`${IMPORT}export const p = new Policy("p", { when: ["resource.frozen"] });`)).toHaveLength(0);
74
+ });
75
+
76
+ test("ignores an effect built from anything but a string literal", () => {
77
+ const source = `${IMPORT}const e = "deny";\nexport const p = new Policy("p", { effect: e });`;
78
+ expect(check(source)).toHaveLength(0);
79
+ });
80
+
81
+ test("ignores a file that imports nothing from this lexicon", () => {
82
+ const source = `import { Bucket } from "@intentius/chant-lexicon-aws";\nexport const b = new Bucket("b", { effect: "deny", when: [""] });`;
83
+ expect(check(source)).toHaveLength(0);
84
+ });
85
+
86
+ test("ignores a call to something not imported from this lexicon", () => {
87
+ const source = `${IMPORT}export const other = new Something({ effect: "deny" });`;
88
+ expect(check(source)).toHaveLength(0);
89
+ });
90
+ });
@@ -0,0 +1,148 @@
1
+ /**
2
+ * CEDC001: a Cedar policy declaration must have a usable effect and no empty guard
3
+ *
4
+ * Two authoring mistakes that the serializer cannot report and the emitted
5
+ * artifact hides:
6
+ *
7
+ * - **An effect that is not `permit` or `forbid`.** Cedar has exactly two, and
8
+ * the serializer coerces anything that is not the literal string `"forbid"`
9
+ * into `permit`. So `effect: "deny"` — the word every other policy language
10
+ * in the world uses, and the one a reader will reach for — silently emits a
11
+ * *grant*. There is no downstream stage that can catch this: by the time the
12
+ * policy set exists, the mistake looks like a deliberate permit.
13
+ * - **An empty `when`/`unless` entry.** `when: ["", "context.mfa"]` emits
14
+ * `when { }`, which is not a Cedar expression; the policy set then fails to
15
+ * parse and the error arrives as a byte offset into a generated file. The
16
+ * typo is here, in the source, and so is the line number worth printing.
17
+ *
18
+ * Source-level because both are visible only before serialization, and both
19
+ * are about a literal the author typed. A guard built from a variable or a
20
+ * template expression is out of reach of an AST rule and is left alone —
21
+ * CEDC011 covers the emitted form.
22
+ *
23
+ * Scoped by import: only calls to bindings imported from this lexicon are
24
+ * considered, so an unrelated `{ effect: "deny" }` in a file that has nothing
25
+ * to do with Cedar is never flagged.
26
+ */
27
+ import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
28
+ import * as ts from "typescript";
29
+
30
+ /** Module specifiers whose exports construct Cedar declarables. */
31
+ const CEDAR_MODULE = /chant-lexicon-cedar/;
32
+
33
+ /** The two effects Cedar has. */
34
+ export const CEDAR_EFFECTS = ["permit", "forbid"] as const;
35
+
36
+ function importedCedarBindings(sourceFile: ts.SourceFile): Set<string> {
37
+ const names = new Set<string>();
38
+ for (const statement of sourceFile.statements) {
39
+ if (!ts.isImportDeclaration(statement)) continue;
40
+ if (!ts.isStringLiteral(statement.moduleSpecifier)) continue;
41
+ if (!CEDAR_MODULE.test(statement.moduleSpecifier.text)) continue;
42
+
43
+ const bindings = statement.importClause?.namedBindings;
44
+ if (bindings && ts.isNamedImports(bindings)) {
45
+ for (const element of bindings.elements) names.add(element.name.text);
46
+ }
47
+ if (statement.importClause?.name) names.add(statement.importClause.name.text);
48
+ }
49
+ return names;
50
+ }
51
+
52
+ /** The callee identifier of a `new X(…)` / `X(…)` expression, if it has one. */
53
+ function calleeName(node: ts.CallExpression | ts.NewExpression): string | undefined {
54
+ return ts.isIdentifier(node.expression) ? node.expression.text : undefined;
55
+ }
56
+
57
+ /** The last object-literal argument — where chant resources carry their props. */
58
+ function propsLiteral(node: ts.CallExpression | ts.NewExpression): ts.ObjectLiteralExpression | undefined {
59
+ const args = [...(node.arguments ?? [])];
60
+ for (let i = args.length - 1; i >= 0; i--) {
61
+ const arg = args[i];
62
+ if (ts.isObjectLiteralExpression(arg)) return arg;
63
+ }
64
+ return undefined;
65
+ }
66
+
67
+ function property(obj: ts.ObjectLiteralExpression, key: string): ts.PropertyAssignment | undefined {
68
+ for (const prop of obj.properties) {
69
+ if (!ts.isPropertyAssignment(prop)) continue;
70
+ if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name)) continue;
71
+ if (prop.name.text === key) return prop;
72
+ }
73
+ return undefined;
74
+ }
75
+
76
+ function lineCol(sourceFile: ts.SourceFile, node: ts.Node): { line: number; column: number } {
77
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
78
+ return { line: line + 1, column: character + 1 };
79
+ }
80
+
81
+ export const cedarPolicyShapeRule: LintRule = {
82
+ id: "CEDC001",
83
+ severity: "error",
84
+ category: "correctness",
85
+ description:
86
+ "A Cedar policy declaration must use effect permit or forbid, and must not carry an empty when/unless clause",
87
+
88
+ check(context: LintContext): LintDiagnostic[] {
89
+ const { sourceFile } = context;
90
+ const diagnostics: LintDiagnostic[] = [];
91
+ const cedarBindings = importedCedarBindings(sourceFile);
92
+ if (cedarBindings.size === 0) return diagnostics;
93
+
94
+ const report = (node: ts.Node, message: string): void => {
95
+ const { line, column } = lineCol(sourceFile, node);
96
+ diagnostics.push({
97
+ file: sourceFile.fileName,
98
+ line,
99
+ column,
100
+ ruleId: "CEDC001",
101
+ severity: "error",
102
+ message,
103
+ });
104
+ };
105
+
106
+ function inspect(node: ts.CallExpression | ts.NewExpression): void {
107
+ const callee = calleeName(node);
108
+ if (!callee || !cedarBindings.has(callee)) return;
109
+ const props = propsLiteral(node);
110
+ if (!props) return;
111
+
112
+ const effect = property(props, "effect");
113
+ if (effect && ts.isStringLiteral(effect.initializer)) {
114
+ const value = effect.initializer.text;
115
+ if (!(CEDAR_EFFECTS as readonly string[]).includes(value)) {
116
+ report(
117
+ effect,
118
+ `Cedar policy effect "${value}" is not a Cedar effect — the language has only permit and forbid, and anything that is not "forbid" is emitted as a permit. Write effect: "forbid" if that is what was meant.`,
119
+ );
120
+ }
121
+ }
122
+
123
+ for (const clause of ["when", "unless"] as const) {
124
+ const guard = property(props, clause);
125
+ if (!guard) continue;
126
+ const entries = ts.isArrayLiteralExpression(guard.initializer)
127
+ ? guard.initializer.elements
128
+ : [guard.initializer];
129
+ for (const entry of entries) {
130
+ if (!ts.isStringLiteral(entry) && !ts.isNoSubstitutionTemplateLiteral(entry)) continue;
131
+ if (entry.text.trim() !== "") continue;
132
+ report(
133
+ entry,
134
+ `Cedar policy has an empty ${clause} clause. An empty guard serializes to ${clause} { }, which is not a Cedar expression and makes the whole policy set unparseable — write the condition, or drop the entry.`,
135
+ );
136
+ }
137
+ }
138
+ }
139
+
140
+ function visit(node: ts.Node): void {
141
+ if (ts.isCallExpression(node) || ts.isNewExpression(node)) inspect(node);
142
+ ts.forEachChild(node, visit);
143
+ }
144
+
145
+ visit(sourceFile);
146
+ return diagnostics;
147
+ },
148
+ };
@@ -0,0 +1,111 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { existsSync, readFileSync } from "fs";
3
+ import { dirname, join } from "path";
4
+ import { fileURLToPath } from "url";
5
+ import type { CompletionContext } from "@intentius/chant/lsp/types";
6
+ import { completions } from "./completions";
7
+
8
+ // The registry is a build artifact of `npm run generate`, and a fresh clone has
9
+ // not run it. Everything below that names a generated class skips when it is
10
+ // absent; the two cases that do not depend on it always run.
11
+ const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
12
+ const lexiconPath = join(pkgDir, "src", "generated", "lexicon-cedar.json");
13
+ const hasGenerated =
14
+ existsSync(lexiconPath) &&
15
+ (() => {
16
+ try {
17
+ return Object.keys(JSON.parse(readFileSync(lexiconPath, "utf-8"))).length > 0;
18
+ } catch {
19
+ return false;
20
+ }
21
+ })();
22
+
23
+ function ctx(partial: Partial<CompletionContext>): CompletionContext {
24
+ return {
25
+ uri: "file:///policies.ts",
26
+ content: "",
27
+ position: { line: 0, character: 0 },
28
+ wordAtCursor: "",
29
+ linePrefix: "",
30
+ ...partial,
31
+ };
32
+ }
33
+
34
+ describe("cedar completions", () => {
35
+ test("returns nothing in an unrelated position", () => {
36
+ expect(completions(ctx({ linePrefix: "const x = 1" }))).toEqual([]);
37
+ });
38
+
39
+ test("returns an array even with no cursor context", () => {
40
+ expect(Array.isArray(completions(ctx({})))).toBe(true);
41
+ });
42
+
43
+ test.skipIf(!hasGenerated)("suggests Policy after `new `", () => {
44
+ const items = completions(ctx({ linePrefix: "export const p = new " }));
45
+
46
+ expect(items.length).toBeGreaterThan(0);
47
+ expect(items.map((i) => i.label)).toContain("Policy");
48
+ expect(items.every((i) => i.kind === "resource")).toBe(true);
49
+ });
50
+
51
+ test.skipIf(!hasGenerated)("suggests schema-derived entity classes", () => {
52
+ const labels = completions(ctx({ linePrefix: "new " })).map((i) => i.label);
53
+
54
+ expect(labels).toContain("Document");
55
+ expect(labels).toContain("User");
56
+ });
57
+
58
+ test.skipIf(!hasGenerated)("narrows on a typed prefix", () => {
59
+ const items = completions(ctx({ linePrefix: "new Docu", wordAtCursor: "Docu" }));
60
+
61
+ expect(items.map((i) => i.label)).toContain("Document");
62
+ expect(items.every((i) => i.label.toLowerCase().startsWith("docu"))).toBe(true);
63
+ });
64
+
65
+ test.skipIf(!hasGenerated)("carries the Cedar type name as the detail", () => {
66
+ const policy = completions(ctx({ linePrefix: "new Poli", wordAtCursor: "Poli" })).find(
67
+ (i) => i.label === "Policy",
68
+ );
69
+
70
+ expect(policy?.detail).toBe("Cedar::Policy");
71
+ });
72
+
73
+ test.skipIf(!hasGenerated)("suggests Policy props inside the constructor", () => {
74
+ const content = "export const p = new Policy({\n ";
75
+ const labels = completions(
76
+ ctx({
77
+ content,
78
+ position: { line: 1, character: 2 },
79
+ linePrefix: " ",
80
+ }),
81
+ ).map((i) => i.label);
82
+
83
+ expect(labels).toContain("effect");
84
+ expect(labels).toContain("principal");
85
+ expect(labels).toContain("when");
86
+ });
87
+
88
+ test.skipIf(!hasGenerated)("suggests action constants in an `eq` scope", () => {
89
+ const items = completions(ctx({ linePrefix: " action: { eq: " }));
90
+
91
+ expect(items.map((i) => i.label)).toContain("ReadAction");
92
+ expect(items.map((i) => i.label)).toContain("WriteAction");
93
+ // Entity classes are not actions and must not leak into this position.
94
+ expect(items.map((i) => i.label)).not.toContain("Document");
95
+ });
96
+
97
+ test.skipIf(!hasGenerated)("suggests action constants inside an `in` list", () => {
98
+ const labels = completions(ctx({ linePrefix: " action: { in: [Rea", wordAtCursor: "Rea" })).map(
99
+ (i) => i.label,
100
+ );
101
+
102
+ expect(labels).toEqual(["ReadAction"]);
103
+ });
104
+
105
+ test.skipIf(!hasGenerated)("documents an action's principals and resources", () => {
106
+ const read = completions(ctx({ linePrefix: " action: { eq: " })).find((i) => i.label === "ReadAction");
107
+
108
+ expect(read?.documentation).toContain("App::User");
109
+ expect(read?.documentation).toContain("App::Document");
110
+ });
111
+ });