@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,496 @@
1
+ /**
2
+ * The three generated artifacts, all built from one model.
3
+ *
4
+ * `lexicon-cedar.json`, `index.ts` and `index.d.ts` describe the same set of
5
+ * declarations, and the only way they cannot skew is if they are derived from
6
+ * the same pass — which is what {@link buildEmitModel} is for. Nothing here
7
+ * reads the clock or a random source: keys come out of the resolved schema
8
+ * already sorted (Rust `BTreeMap`, #1648 §5.2), everything else is sorted
9
+ * explicitly, so regenerating twice produces identical bytes.
10
+ */
11
+
12
+ import type { NamingStrategy } from "@intentius/chant/codegen/naming";
13
+ import type { CedarAttribute, CedarDecl, CedarTypeRef } from "../spec/parse";
14
+ import { deriveName, POLICY_TS_NAME, POLICY_TYPE } from "./naming";
15
+
16
+ export const LEXICON_NAME = "cedar";
17
+
18
+ // ── Model ─────────────────────────────────────────────────────────
19
+
20
+ export interface EmitEntity {
21
+ tsName: string;
22
+ /** `App::User` */
23
+ typeName: string;
24
+ namespace: string;
25
+ /** TS name of the attribute-record property class, e.g. `UserAttributes`. */
26
+ attributesTsName: string;
27
+ /** TS name of the entity-UID string type, e.g. `UserUid`. */
28
+ uidTsName: string;
29
+ memberOfTypes: string[];
30
+ attributes: CedarAttribute[];
31
+ enumValues?: string[];
32
+ }
33
+
34
+ export interface EmitAction {
35
+ tsName: string;
36
+ /** `App::Action::"read"` */
37
+ typeName: string;
38
+ namespace: string;
39
+ actionId: string;
40
+ /** TS name of the context-record property class, e.g. `ReadContext`. */
41
+ contextTsName: string;
42
+ principalTypes: string[];
43
+ resourceTypes: string[];
44
+ context: CedarAttribute[];
45
+ }
46
+
47
+ export interface EmitModel {
48
+ entities: EmitEntity[];
49
+ actions: EmitAction[];
50
+ /** entityType → the UID type name, for rendering `{type:"Entity"}` references. */
51
+ uidTypeByEntityType: Map<string, string>;
52
+ }
53
+
54
+ /**
55
+ * Assign every generated name, once.
56
+ *
57
+ * `NamingStrategy` names the primary declarations; the derived record names
58
+ * are de-duplicated against the same pool so a `UserAttributes` entity type in
59
+ * the schema cannot quietly take the name `User`'s attribute record wants.
60
+ */
61
+ export function buildEmitModel(decls: CedarDecl[], naming: NamingStrategy): EmitModel {
62
+ const taken = new Set<string>([POLICY_TS_NAME]);
63
+
64
+ const entities: EmitEntity[] = [];
65
+ const actions: EmitAction[] = [];
66
+
67
+ const sorted = [...decls].sort((a, b) => a.typeName.localeCompare(b.typeName));
68
+
69
+ // Primary names first, all of them, before any derived name is claimed —
70
+ // otherwise `UserAttributes` (derived) could take the name an entity type
71
+ // literally called `UserAttributes` is entitled to.
72
+ const primaryNames = new Map<string, string>();
73
+ for (const decl of sorted) {
74
+ const base = naming.resolve(decl.typeName);
75
+ if (!base) continue;
76
+ // `NamingStrategy`'s last phase qualifies a collision with the service
77
+ // name, which for Cedar is the namespace — so two declarations in the
78
+ // *same* namespace that reduce to the same short name (an entity type
79
+ // `ReadAction` beside an action `read`) both come back as `AppReadAction`.
80
+ // Silently letting the second overwrite the first in the registry is the
81
+ // one outcome worth refusing.
82
+ primaryNames.set(decl.typeName, deriveName(base, "", taken));
83
+ }
84
+
85
+ for (const decl of sorted) {
86
+ const tsName = primaryNames.get(decl.typeName);
87
+ if (!tsName) continue;
88
+
89
+ if (decl.kind === "entity") {
90
+ entities.push({
91
+ tsName,
92
+ typeName: decl.typeName,
93
+ namespace: decl.namespace,
94
+ attributesTsName: deriveName(tsName, "Attributes", taken),
95
+ uidTsName: deriveName(tsName, "Uid", taken),
96
+ memberOfTypes: decl.memberOfTypes,
97
+ attributes: decl.attributes,
98
+ ...(decl.enumValues ? { enumValues: decl.enumValues } : {}),
99
+ });
100
+ } else {
101
+ actions.push({
102
+ tsName,
103
+ typeName: decl.typeName,
104
+ namespace: decl.namespace,
105
+ actionId: decl.actionId,
106
+ contextTsName: deriveName(stripActionSuffix(tsName), "Context", taken),
107
+ principalTypes: decl.principalTypes,
108
+ resourceTypes: decl.resourceTypes,
109
+ context: decl.context,
110
+ });
111
+ }
112
+ }
113
+
114
+ entities.sort((a, b) => a.tsName.localeCompare(b.tsName));
115
+ actions.sort((a, b) => a.tsName.localeCompare(b.tsName));
116
+
117
+ const uidTypeByEntityType = new Map<string, string>();
118
+ for (const entity of entities) uidTypeByEntityType.set(entity.typeName, entity.uidTsName);
119
+
120
+ return { entities, actions, uidTypeByEntityType };
121
+ }
122
+
123
+ /** `ReadAction` → `Read`, so the context type reads `ReadContext`. */
124
+ function stripActionSuffix(tsName: string): string {
125
+ return tsName.endsWith("Action") && tsName.length > "Action".length
126
+ ? tsName.slice(0, -"Action".length)
127
+ : tsName;
128
+ }
129
+
130
+ // ── TypeScript type rendering ─────────────────────────────────────
131
+
132
+ /**
133
+ * Render one Cedar type as a TypeScript type expression.
134
+ *
135
+ * Extension types (`ipaddr`, `decimal`, `datetime`, `duration`) render as
136
+ * `string`: Cedar constructs them from string literals in policy text, and
137
+ * inventing branded TS types for values chant never evaluates would be
138
+ * decoration.
139
+ */
140
+ export function renderType(type: CedarTypeRef, model: EmitModel): string {
141
+ switch (type.kind) {
142
+ case "primitive":
143
+ return type.ts;
144
+ case "entity":
145
+ return model.uidTypeByEntityType.get(type.entityType) ?? "string";
146
+ case "set": {
147
+ const inner = renderType(type.element, model);
148
+ return /[|&]/.test(inner) ? `Array<${inner}>` : `${inner}[]`;
149
+ }
150
+ case "record":
151
+ return type.attributes.length === 0
152
+ ? "Record<string, never>"
153
+ : `{ ${type.attributes.map((a) => `${propertyKey(a.name)}${a.required ? "" : "?"}: ${renderType(a.type, model)};`).join(" ")} }`;
154
+ case "extension":
155
+ return "string";
156
+ case "opaque":
157
+ return "string";
158
+ }
159
+ }
160
+
161
+ const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
162
+
163
+ /** Quote an attribute name that is not a bare identifier — Cedar allows both. */
164
+ export function propertyKey(name: string): string {
165
+ return IDENTIFIER.test(name) ? name : JSON.stringify(name);
166
+ }
167
+
168
+ /** The Cedar surface syntax for a type, for doc comments and the registry. */
169
+ export function cedarTypeText(type: CedarTypeRef): string {
170
+ switch (type.kind) {
171
+ case "primitive":
172
+ return type.cedar;
173
+ case "entity":
174
+ return type.entityType;
175
+ case "set":
176
+ return `Set<${cedarTypeText(type.element)}>`;
177
+ case "record":
178
+ return `{ ${type.attributes.map((a) => `"${a.name}"${a.required ? "" : "?"}: ${cedarTypeText(a.type)}`).join(", ")} }`;
179
+ case "extension":
180
+ return type.name;
181
+ case "opaque":
182
+ return type.name;
183
+ }
184
+ }
185
+
186
+ // ── Registry ──────────────────────────────────────────────────────
187
+
188
+ interface RegistryEntry {
189
+ resourceType: string;
190
+ kind: "resource" | "property";
191
+ lexicon: string;
192
+ [key: string]: unknown;
193
+ }
194
+
195
+ /** `App::User` → `App::User.Attributes`, the record type's own spec name. */
196
+ export function attributesTypeName(entityTypeName: string): string {
197
+ return `${entityTypeName}.Attributes`;
198
+ }
199
+
200
+ /** `App::Action::"read"` → `App::Action::"read".Context`. */
201
+ export function contextTypeName(actionTypeName: string): string {
202
+ return `${actionTypeName}.Context`;
203
+ }
204
+
205
+ function attributeTable(attributes: CedarAttribute[]): Record<string, { type: string; required: boolean }> {
206
+ const out: Record<string, { type: string; required: boolean }> = {};
207
+ for (const attr of [...attributes].sort((a, b) => a.name.localeCompare(b.name))) {
208
+ out[attr.name] = { type: cedarTypeText(attr.type), required: attr.required };
209
+ }
210
+ return out;
211
+ }
212
+
213
+ /** Build `lexicon-cedar.json`. */
214
+ export function generateRegistry(model: EmitModel): string {
215
+ const entries: Record<string, RegistryEntry> = {};
216
+
217
+ entries[POLICY_TS_NAME] = {
218
+ resourceType: POLICY_TYPE,
219
+ kind: "resource",
220
+ lexicon: LEXICON_NAME,
221
+ description: "A Cedar policy — the unit this lexicon serializes to .cedar text and the JSON policy format.",
222
+ properties: {
223
+ effect: { type: "permit | forbid", required: false },
224
+ principal: { type: "scope", required: false },
225
+ action: { type: "scope", required: false },
226
+ resource: { type: "scope", required: false },
227
+ when: { type: "Set<expression>", required: false },
228
+ unless: { type: "Set<expression>", required: false },
229
+ annotations: { type: "Record<String, String>", required: false },
230
+ },
231
+ };
232
+
233
+ for (const entity of model.entities) {
234
+ entries[entity.tsName] = {
235
+ resourceType: entity.typeName,
236
+ kind: "resource",
237
+ lexicon: LEXICON_NAME,
238
+ namespace: entity.namespace,
239
+ memberOfTypes: entity.memberOfTypes,
240
+ properties: attributeTable(entity.attributes),
241
+ ...(entity.enumValues ? { enumValues: entity.enumValues } : {}),
242
+ };
243
+ entries[entity.attributesTsName] = {
244
+ resourceType: attributesTypeName(entity.typeName),
245
+ kind: "property",
246
+ lexicon: LEXICON_NAME,
247
+ namespace: entity.namespace,
248
+ properties: attributeTable(entity.attributes),
249
+ };
250
+ }
251
+
252
+ for (const action of model.actions) {
253
+ entries[action.tsName] = {
254
+ resourceType: action.typeName,
255
+ kind: "resource",
256
+ lexicon: LEXICON_NAME,
257
+ namespace: action.namespace,
258
+ principalTypes: action.principalTypes,
259
+ resourceTypes: action.resourceTypes,
260
+ properties: attributeTable(action.context),
261
+ };
262
+ entries[action.contextTsName] = {
263
+ resourceType: contextTypeName(action.typeName),
264
+ kind: "property",
265
+ lexicon: LEXICON_NAME,
266
+ namespace: action.namespace,
267
+ properties: attributeTable(action.context),
268
+ };
269
+ }
270
+
271
+ const sorted: Record<string, RegistryEntry> = {};
272
+ for (const key of Object.keys(entries).sort()) sorted[key] = entries[key];
273
+
274
+ return JSON.stringify(sorted, null, 2) + "\n";
275
+ }
276
+
277
+ // ── Shared declaration bodies ─────────────────────────────────────
278
+
279
+ const BANNER = "// Code generated by chant generate. DO NOT EDIT.";
280
+
281
+ /** A template-literal type for an entity UID: `` `App::User::"${string}"` ``. */
282
+ function uidTypeExpression(entityTypeName: string): string {
283
+ return "`" + entityTypeName + '::"${string}"`';
284
+ }
285
+
286
+ function docBlock(lines: string[], indent = ""): string[] {
287
+ if (lines.length === 0) return [];
288
+ return [`${indent}/**`, ...lines.map((l) => `${indent} * ${l}`), `${indent} */`];
289
+ }
290
+
291
+ function attributeLines(attributes: CedarAttribute[], model: EmitModel): string[] {
292
+ return attributes.map(
293
+ (attr) => ` ${propertyKey(attr.name)}${attr.required ? "" : "?"}: ${renderType(attr.type, model)};`,
294
+ );
295
+ }
296
+
297
+ function unionOr(members: string[], fallback: string): string {
298
+ return members.length > 0 ? members.join(" | ") : fallback;
299
+ }
300
+
301
+ /**
302
+ * The shared body of `index.ts` and `index.d.ts` — everything that is
303
+ * type-level and therefore identical in both.
304
+ */
305
+ function typeDeclarations(model: EmitModel): string[] {
306
+ const lines: string[] = [];
307
+
308
+ for (const entity of model.entities) {
309
+ lines.push(
310
+ ...docBlock([
311
+ `Entity UID of \`${entity.typeName}\`.`,
312
+ ...(entity.memberOfTypes.length > 0 ? [`Member of: ${entity.memberOfTypes.join(", ")}.`] : []),
313
+ ]),
314
+ `export type ${entity.uidTsName} = ${uidTypeExpression(entity.typeName)};`,
315
+ "",
316
+ `export interface ${entity.attributesTsName}Props {`,
317
+ ...attributeLines(entity.attributes, model),
318
+ "}",
319
+ "",
320
+ );
321
+ }
322
+
323
+ for (const action of model.actions) {
324
+ lines.push(
325
+ `export interface ${action.contextTsName}Props {`,
326
+ ...attributeLines(action.context, model),
327
+ "}",
328
+ "",
329
+ );
330
+ }
331
+
332
+ lines.push(
333
+ "/** Every entity type name the schema declares, as Cedar writes it. */",
334
+ `export type EntityTypeName = ${unionOr(model.entities.map((e) => JSON.stringify(e.typeName)), "never")};`,
335
+ "",
336
+ "/** Every entity UID the schema can produce. */",
337
+ `export type EntityUid = ${unionOr(model.entities.map((e) => e.uidTsName), "never")};`,
338
+ "",
339
+ "/** Every action UID the schema declares. */",
340
+ `export type ActionUid = ${unionOr(model.actions.map((a) => JSON.stringify(a.typeName)), "never")};`,
341
+ "",
342
+ "/** Anything a policy scope may name. */",
343
+ "export type PolicyRef = EntityUid | ActionUid;",
344
+ "",
345
+ "/** A Cedar policy either permits or forbids; there is no third effect. */",
346
+ 'export type PolicyEffect = "permit" | "forbid";',
347
+ "",
348
+ ...docBlock([
349
+ "One scope position (`principal`, `action`, `resource`).",
350
+ "",
351
+ "`{}` is the unconstrained form Cedar writes as a bare variable. The",
352
+ "constrained forms mirror the grammar: `== E`, `in E`, `in [E, …]`,",
353
+ "`is T`, and `is T in E`.",
354
+ ]),
355
+ "export type PolicyScope =",
356
+ " | Record<string, never>",
357
+ " | { eq: PolicyRef; in?: never; is?: never }",
358
+ " | { in: PolicyRef | PolicyRef[]; eq?: never; is?: never }",
359
+ " | { is: EntityTypeName; in?: PolicyRef | PolicyRef[]; eq?: never };",
360
+ "",
361
+ ...docBlock([
362
+ "The props of a `Cedar::Policy`.",
363
+ "",
364
+ "`when`/`unless` carry Cedar expression text. They stay strings because",
365
+ "Cedar's expression grammar is the policy language itself — typing it in",
366
+ "TypeScript is import/reconcile's problem (epic #1645 sub-issue 6), not",
367
+ "codegen's.",
368
+ ]),
369
+ "export interface PolicyProps {",
370
+ " /** Defaults to `permit` when omitted. */",
371
+ " effect?: PolicyEffect;",
372
+ " principal?: PolicyScope;",
373
+ " action?: PolicyScope;",
374
+ " resource?: PolicyScope;",
375
+ " /** Cedar expression strings, each emitted as its own `when { … }` clause. */",
376
+ " when?: string[];",
377
+ " /** Cedar expression strings, each emitted as its own `unless { … }` clause. */",
378
+ " unless?: string[];",
379
+ " /** Emitted as `@key(\"value\")`; an explicit `id` wins over the logical name. */",
380
+ " annotations?: Record<string, string>;",
381
+ "}",
382
+ "",
383
+ );
384
+
385
+ return lines;
386
+ }
387
+
388
+ // ── index.ts ──────────────────────────────────────────────────────
389
+
390
+ /** Build the runtime `index.ts`. */
391
+ export function generateRuntimeIndex(model: EmitModel): string {
392
+ const lines: string[] = [
393
+ BANNER,
394
+ 'import type { Declarable } from "@intentius/chant/declarable";',
395
+ 'import { createProperty, createResource } from "./runtime";',
396
+ "",
397
+ ...typeDeclarations(model),
398
+ ...docBlock([
399
+ "The policy resource. Declare one per policy:",
400
+ "",
401
+ "```ts",
402
+ 'export const allowOwnerRead = new Policy({',
403
+ ' principal: { is: "App::User" },',
404
+ " action: { eq: ReadAction },",
405
+ " when: ['resource.owner == principal'],",
406
+ "});",
407
+ "```",
408
+ ]),
409
+ `export const ${POLICY_TS_NAME} = createResource(${JSON.stringify(POLICY_TYPE)}, ${JSON.stringify(LEXICON_NAME)}, {}) as unknown as new (props: PolicyProps) => Declarable;`,
410
+ "",
411
+ ];
412
+
413
+ for (const entity of model.entities) {
414
+ lines.push(
415
+ `export const ${entity.tsName} = createResource(${JSON.stringify(entity.typeName)}, ${JSON.stringify(LEXICON_NAME)}, {}) as unknown as new (props: ${entity.attributesTsName}Props) => Declarable;`,
416
+ `export const ${entity.attributesTsName} = createProperty(${JSON.stringify(attributesTypeName(entity.typeName))}, ${JSON.stringify(LEXICON_NAME)}) as unknown as new (props: ${entity.attributesTsName}Props) => Declarable;`,
417
+ );
418
+ }
419
+ if (model.entities.length > 0) lines.push("");
420
+
421
+ for (const action of model.actions) {
422
+ lines.push(
423
+ ...docBlock([
424
+ `\`${action.typeName}\`.`,
425
+ ...(action.principalTypes.length > 0 ? [`Principals: ${action.principalTypes.join(", ")}.`] : []),
426
+ ...(action.resourceTypes.length > 0 ? [`Resources: ${action.resourceTypes.join(", ")}.`] : []),
427
+ ]),
428
+ `export const ${action.tsName} = ${JSON.stringify(action.typeName)} as const;`,
429
+ `export const ${action.contextTsName} = createProperty(${JSON.stringify(contextTypeName(action.typeName))}, ${JSON.stringify(LEXICON_NAME)}) as unknown as new (props: ${action.contextTsName}Props) => Declarable;`,
430
+ );
431
+ }
432
+ if (model.actions.length > 0) lines.push("");
433
+
434
+ lines.push(
435
+ "/** Every generated action constant, for exhaustive iteration. */",
436
+ `export const ALL_ACTIONS: readonly ActionUid[] = [${model.actions.map((a) => a.tsName).join(", ")}];`,
437
+ "",
438
+ "/** Every entity type name the schema declares. */",
439
+ `export const ALL_ENTITY_TYPES: readonly EntityTypeName[] = [${model.entities.map((e) => JSON.stringify(e.typeName)).join(", ")}];`,
440
+ "",
441
+ );
442
+
443
+ return lines.join("\n");
444
+ }
445
+
446
+ // ── index.d.ts ────────────────────────────────────────────────────
447
+
448
+ /**
449
+ * Build the declaration-only `index.d.ts`.
450
+ *
451
+ * It is validated standalone by core's `typecheckDTS`, which runs tsc in a
452
+ * temp directory with no `node_modules` beyond a stub for
453
+ * `@intentius/chant/declarable` — so this file may import that and nothing
454
+ * else, and every other type it uses has to be declared here.
455
+ */
456
+ export function generateTypes(model: EmitModel): string {
457
+ const lines: string[] = [
458
+ BANNER,
459
+ 'import type { Declarable } from "@intentius/chant/declarable";',
460
+ "",
461
+ ...typeDeclarations(model),
462
+ `export declare const ${POLICY_TS_NAME}: new (props: PolicyProps) => Declarable;`,
463
+ "",
464
+ ];
465
+
466
+ for (const entity of model.entities) {
467
+ lines.push(
468
+ `export declare const ${entity.tsName}: new (props: ${entity.attributesTsName}Props) => Declarable;`,
469
+ `export declare const ${entity.attributesTsName}: new (props: ${entity.attributesTsName}Props) => Declarable;`,
470
+ );
471
+ }
472
+ if (model.entities.length > 0) lines.push("");
473
+
474
+ for (const action of model.actions) {
475
+ lines.push(
476
+ `export declare const ${action.tsName}: ${JSON.stringify(action.typeName)};`,
477
+ `export declare const ${action.contextTsName}: new (props: ${action.contextTsName}Props) => Declarable;`,
478
+ );
479
+ }
480
+ if (model.actions.length > 0) lines.push("");
481
+
482
+ lines.push(
483
+ "export declare const ALL_ACTIONS: readonly ActionUid[];",
484
+ "export declare const ALL_ENTITY_TYPES: readonly EntityTypeName[];",
485
+ "",
486
+ );
487
+
488
+ return lines.join("\n");
489
+ }
490
+
491
+ // ── runtime.ts ────────────────────────────────────────────────────
492
+
493
+ /** The tiny re-export module `index.ts` imports its factories from. */
494
+ export const RUNTIME_TS = `${BANNER}
495
+ export { createProperty, createResource } from "@intentius/chant/runtime";
496
+ `;
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Entry point for `npm run generate` in lexicon-cedar.
4
+ *
5
+ * Generation reads the project's `.cedarschema` when there is one and the
6
+ * schema bundled at `src/spec/default-schema.cedarschema` when there is not
7
+ * (#1650), so this always has something to generate from and no longer needs
8
+ * the scaffold's swallow-and-continue guard.
9
+ */
10
+ import { generate, writeGeneratedFiles } from "./generate";
11
+ import { dirname } from "path";
12
+ import { fileURLToPath } from "url";
13
+
14
+ // <pkg>/src/codegen/generate-cli.ts → three levels up is the package root.
15
+ const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
16
+
17
+ const result = await generate({ verbose: true, projectRoot: pkgDir });
18
+ writeGeneratedFiles(result, pkgDir);
@@ -0,0 +1,128 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { generate } from "./generate";
5
+ import { buildEmitModel, generateRegistry, generateRuntimeIndex, generateTypes } from "./emit";
6
+ import { createNaming } from "./naming";
7
+ import { parseCedarSchema } from "../spec/parse";
8
+ import { requiredNames } from "../validate";
9
+
10
+ const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
11
+
12
+ function emitFrom(schema: string) {
13
+ const { decls } = parseCedarSchema(schema);
14
+ const model = buildEmitModel(decls, createNaming(decls));
15
+ return {
16
+ model,
17
+ registry: JSON.parse(generateRegistry(model)) as Record<string, { resourceType: string; kind: string }>,
18
+ indexTS: generateRuntimeIndex(model),
19
+ typesDTS: generateTypes(model),
20
+ };
21
+ }
22
+
23
+ describe("cedar generate", () => {
24
+ it("generates from the bundled default schema with no project schema in sight", async () => {
25
+ // The default is the reason this lexicon's generate step is gateable at
26
+ // all: every other lexicon has an upstream to fetch, cedar's input is the
27
+ // user's own file, and a fresh checkout has none (#1650).
28
+ const result = await generate({ projectRoot: join(pkgDir, "does-not-exist") });
29
+ expect(result.resources).toBe(16);
30
+ expect(result.warnings).toEqual([]);
31
+ });
32
+
33
+ it("is deterministic", async () => {
34
+ const a = await generate({ projectRoot: pkgDir });
35
+ const b = await generate({ projectRoot: pkgDir });
36
+ expect(b.lexiconJSON).toBe(a.lexiconJSON);
37
+ expect(b.typesDTS).toBe(a.typesDTS);
38
+ expect(b.indexTS).toBe(a.indexTS);
39
+ });
40
+
41
+ it("produces every name validate.ts requires", async () => {
42
+ const result = await generate({ projectRoot: pkgDir });
43
+ const keys = new Set(Object.keys(JSON.parse(result.lexiconJSON) as Record<string, unknown>));
44
+ expect(requiredNames.filter((n) => !keys.has(n))).toEqual([]);
45
+ });
46
+
47
+ it("keeps the required-name list above the tier-3 bar", () => {
48
+ expect(requiredNames.length).toBeGreaterThanOrEqual(30);
49
+ });
50
+
51
+ it("writes the runtime module index.ts imports its factories from", async () => {
52
+ const result = await generate({ projectRoot: pkgDir });
53
+ expect(result.extraArtifacts?.["runtime.ts"]).toContain("createResource");
54
+ });
55
+ });
56
+
57
+ describe("emitters", () => {
58
+ const schema = `
59
+ namespace App {
60
+ type Level = Long;
61
+ entity Group;
62
+ entity User in [Group] = { "level": Level, "nick"?: String, "peers": Set<User> };
63
+ action read appliesTo { principal: [User], resource: [Group], context: { "mfa": Bool } };
64
+ }
65
+ `;
66
+
67
+ it("names entity types, actions and their record types apart", () => {
68
+ const { registry } = emitFrom(schema);
69
+ expect(Object.keys(registry).sort()).toEqual([
70
+ "Group",
71
+ "GroupAttributes",
72
+ "Policy",
73
+ "ReadAction",
74
+ "ReadContext",
75
+ "User",
76
+ "UserAttributes",
77
+ ]);
78
+ expect(registry.ReadAction.resourceType).toBe('App::Action::"read"');
79
+ expect(registry.ReadContext.kind).toBe("property");
80
+ expect(registry.User.kind).toBe("resource");
81
+ });
82
+
83
+ it("emits optional attributes as optional and sets as arrays", () => {
84
+ const { indexTS } = emitFrom(schema);
85
+ expect(indexTS).toContain("nick?: string;");
86
+ expect(indexTS).toContain("peers: UserUid[];");
87
+ expect(indexTS).toContain("level: number;");
88
+ });
89
+
90
+ it("renders an entity reference as the referenced entity's UID type", () => {
91
+ const { indexTS } = emitFrom(schema);
92
+ expect(indexTS).toContain('export type UserUid = `App::User::"${string}"`;');
93
+ });
94
+
95
+ it("gives the Policy class the name users import, even against a schema that wants it", () => {
96
+ // `Policy` is a priority name, so a schema declaring its own `Policy`
97
+ // entity type gets qualified rather than taking the authoring class's name.
98
+ const { registry } = emitFrom(`namespace App { entity Policy = { "n": String }; }`);
99
+ expect(registry.Policy.resourceType).toBe("Cedar::Policy");
100
+ expect(registry.AppPolicy.resourceType).toBe("App::Policy");
101
+ });
102
+
103
+ it("qualifies an entity type that collides with an action name", () => {
104
+ const { registry } = emitFrom(`
105
+ namespace App {
106
+ entity ReadAction;
107
+ entity Doc;
108
+ action read appliesTo { principal: [ReadAction], resource: [Doc] };
109
+ }
110
+ `);
111
+ const types = Object.values(registry).map((e) => e.resourceType);
112
+ expect(types).toContain("App::ReadAction");
113
+ expect(types).toContain('App::Action::"read"');
114
+ // Both present under distinct keys; nothing silently overwritten.
115
+ expect(new Set(types).size).toBe(types.length);
116
+ });
117
+
118
+ it("emits a schema with no declarations without producing invalid TypeScript", () => {
119
+ const { typesDTS, indexTS } = emitFrom("");
120
+ expect(typesDTS).toContain("export type EntityUid = never;");
121
+ expect(indexTS).toContain("export const ALL_ACTIONS: readonly ActionUid[] = [];");
122
+ });
123
+
124
+ it("sorts registry keys", () => {
125
+ const keys = Object.keys(emitFrom(schema).registry);
126
+ expect(keys).toEqual([...keys].sort());
127
+ });
128
+ });