@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,123 @@
1
+ /**
2
+ * Schema-driven code generation for the cedar lexicon (#1650).
3
+ *
4
+ * The pipeline is core's; what is unusual is its input. Every other lexicon
5
+ * fetches one global upstream spec, so `fetchSchemas` is an HTTP call and a
6
+ * cache. Cedar's spec-of-interest is the project's own `.cedarschema` (epic
7
+ * #1645), so `fetchSchemas` is a file read with a fallback, and the pinned
8
+ * upstream is the *grammar* — `@cedar-policy/cedar-wasm`, whose language
9
+ * version is asserted before anything is emitted.
10
+ *
11
+ * Order matters in {@link generate}: the language-version assert runs before
12
+ * the schema is resolved, because resolution is the thing whose output shape a
13
+ * grammar change would move.
14
+ */
15
+
16
+ import { generatePipeline, writeGeneratedArtifacts } from "@intentius/chant/codegen/generate";
17
+ import type { GenerateOptions, GenerateResult } from "@intentius/chant/codegen/generate";
18
+ import type { NamingStrategy } from "@intentius/chant/codegen/naming";
19
+ import { dirname } from "path";
20
+ import { fileURLToPath } from "url";
21
+ import { createNaming } from "./naming";
22
+ import {
23
+ buildEmitModel,
24
+ generateRegistry,
25
+ generateRuntimeIndex,
26
+ generateTypes,
27
+ RUNTIME_TS,
28
+ type EmitModel,
29
+ } from "./emit";
30
+ import { DEFAULT_SCHEMA_KEY, fetchSchemas, resolveSchemaPath } from "../spec/fetch";
31
+ import { parseCedarSchema, type CedarDecl } from "../spec/parse";
32
+ import { assertPinnedLangVersion, assertPinnedSchema } from "../spec/pin";
33
+
34
+ /** Filename of the generated registry. */
35
+ export const LEXICON_JSON_FILENAME = "lexicon-cedar.json";
36
+
37
+ export interface CedarGenerateOptions extends GenerateOptions {
38
+ /** Project root the `cedar.schema` path is resolved against. Defaults to `process.cwd()`. */
39
+ projectRoot?: string;
40
+ /** The `cedar` config namespace, when the caller has already loaded it. */
41
+ config?: { schema?: string; validation?: { requireProjectSchema?: boolean } };
42
+ }
43
+
44
+ /**
45
+ * Run the code generation pipeline.
46
+ */
47
+ export async function generate(options: CedarGenerateOptions = {}): Promise<GenerateResult> {
48
+ // Refuse a grammar nobody chose before reading anything (#1390's rule).
49
+ assertPinnedLangVersion();
50
+
51
+ // The three emit callbacks each need the same model, and the pipeline hands
52
+ // them the results separately. Memoizing on the results array keeps one
53
+ // build per run without a mutable `let` whose assignment TypeScript cannot
54
+ // see happening inside a callback.
55
+ const models = new Map<readonly CedarDecl[], EmitModel>();
56
+ const modelFor = (results: CedarDecl[], naming: NamingStrategy): EmitModel => {
57
+ const cached = models.get(results);
58
+ if (cached) return cached;
59
+ const built = buildEmitModel(results, naming);
60
+ models.set(results, built);
61
+ return built;
62
+ };
63
+
64
+ const result = await generatePipeline<CedarDecl>(
65
+ {
66
+ fetchSchemas: () => fetchSchemas({ projectRoot: options.projectRoot, config: options.config }),
67
+
68
+ parseSchema: (typeName, data) => {
69
+ const { decls, resolved } = parseCedarSchema(data.toString("utf-8"));
70
+
71
+ // The content pin covers the schema this package ships, not a user's.
72
+ // Theirs is their input; its churn is theirs to own.
73
+ if (typeName === DEFAULT_SCHEMA_KEY) {
74
+ assertPinnedSchema(
75
+ resolved,
76
+ decls.map((d) => d.typeName).sort(),
77
+ );
78
+ }
79
+
80
+ return decls;
81
+ },
82
+
83
+ createNaming: (results) => createNaming(results),
84
+
85
+ generateRegistry: (results, naming) => generateRegistry(modelFor(results, naming)),
86
+
87
+ generateTypes: (results, naming) => generateTypes(modelFor(results, naming)),
88
+
89
+ generateRuntimeIndex: (results, naming) => generateRuntimeIndex(modelFor(results, naming)),
90
+
91
+ generateExtraArtifacts: () => ({ "runtime.ts": RUNTIME_TS }),
92
+ },
93
+ options,
94
+ );
95
+
96
+ if (options.verbose) {
97
+ const source = resolveSchemaPath({ projectRoot: options.projectRoot, config: options.config });
98
+ console.error(
99
+ `Generated ${result.resources} declaration(s) from ` +
100
+ `${source.isDefault ? "the bundled default schema" : source.path}`,
101
+ );
102
+ }
103
+
104
+ return result;
105
+ }
106
+
107
+ /**
108
+ * Write generated files to the package directory.
109
+ */
110
+ export function writeGeneratedFiles(result: GenerateResult, pkgDir?: string): void {
111
+ // This module lives at <pkg>/src/codegen/generate.ts, so the package root is
112
+ // three levels up — the scaffold's two-level default wrote into src/src/.
113
+ const dir = pkgDir ?? dirname(dirname(dirname(fileURLToPath(import.meta.url))));
114
+ writeGeneratedArtifacts({
115
+ baseDir: dir,
116
+ files: {
117
+ [LEXICON_JSON_FILENAME]: result.lexiconJSON,
118
+ "index.d.ts": result.typesDTS,
119
+ "index.ts": result.indexTS,
120
+ ...(result.extraArtifacts ?? {}),
121
+ },
122
+ });
123
+ }
@@ -0,0 +1,101 @@
1
+ import { NamingStrategy, type NamingConfig, type NamingInput } from "@intentius/chant/codegen/naming";
2
+
3
+ /**
4
+ * The name of the policy resource every generated bundle carries.
5
+ *
6
+ * It is not schema-derived — a Cedar schema declares entity types and actions,
7
+ * never policies — so it is claimed as a priority name before any schema type
8
+ * gets to contest a short name. A project whose schema declares its own
9
+ * `Policy` entity type therefore gets `AppPolicy` (or whatever its namespace
10
+ * is), and the authoring class keeps the name users import.
11
+ */
12
+ export const POLICY_TS_NAME = "Policy";
13
+
14
+ /** The `entityType` of the policy resource, matching the serializer. */
15
+ export const POLICY_TYPE = "Cedar::Policy";
16
+
17
+ /** `read` → `Read`; `service-account` → `ServiceAccount`. */
18
+ export function pascalCase(value: string): string {
19
+ return value
20
+ .split(/[^a-zA-Z0-9]+/)
21
+ .filter(Boolean)
22
+ .map((part) => part[0].toUpperCase() + part.slice(1))
23
+ .join("");
24
+ }
25
+
26
+ /** Split `App::Action::"read"` into its action id, or null for a non-action. */
27
+ export function actionIdOf(typeName: string): string | null {
28
+ const match = /(?:^|::)Action::"(.*)"$/.exec(typeName);
29
+ return match ? match[1] : null;
30
+ }
31
+
32
+ /**
33
+ * Naming configuration for the cedar lexicon.
34
+ *
35
+ * Two shapes go through this rather than one: `App::User` (an entity type) and
36
+ * `App::Action::"read"` (an action). Both are entities in Cedar's model, so
37
+ * both are contested in the same pool — a schema with an entity type called
38
+ * `Read` and an action called `read` collides, and phase 4 qualifies the loser
39
+ * with its namespace rather than one silently overwriting the other.
40
+ */
41
+ export const namingConfig: NamingConfig = {
42
+ priorityNames: {
43
+ [POLICY_TYPE]: POLICY_TS_NAME,
44
+ },
45
+
46
+ priorityAliases: {},
47
+
48
+ priorityPropertyAliases: {},
49
+
50
+ serviceAbbreviations: {},
51
+
52
+ /**
53
+ * Actions get an `Action` suffix. Without it `read`/`write`/`delete` become
54
+ * `Read`/`Write`/`Delete`, which read as entity types at the import site and
55
+ * collide with them in schemas that have both.
56
+ */
57
+ shortName: (typeName: string) => {
58
+ const actionId = actionIdOf(typeName);
59
+ if (actionId !== null) return `${pascalCase(actionId)}Action`;
60
+ return pascalCase(typeName.split("::").pop() ?? typeName);
61
+ },
62
+
63
+ /** The namespace — `App::User` → `App`, and the empty namespace → `Cedar`. */
64
+ serviceName: (typeName: string) => {
65
+ const segments = typeName.split("::");
66
+ return segments.length > 1 ? segments[0] : "Cedar";
67
+ },
68
+ };
69
+
70
+ /**
71
+ * Create a NamingStrategy instance from parsed results.
72
+ *
73
+ * `Cedar::Policy` is injected as an input because it has to be *in* the contest
74
+ * to win it: `priorityNames` is applied over the input type names, so a policy
75
+ * type that never appears there cannot claim `Policy` before a schema's own
76
+ * `Policy` entity type takes it in the short-name phase.
77
+ */
78
+ export function createNaming(inputs: NamingInput[]): NamingStrategy {
79
+ return new NamingStrategy([{ typeName: POLICY_TYPE, propertyTypes: [] }, ...inputs], namingConfig);
80
+ }
81
+
82
+ /**
83
+ * The names derived from a primary name — the entity's attribute record and
84
+ * the action's context record.
85
+ *
86
+ * These are not contested by `NamingStrategy` (it only names the primary
87
+ * declarations), so they are de-duplicated against everything already taken.
88
+ * The suffix loop is deliberately boring: a schema that manages to collide
89
+ * `UserAttributes` twice gets `UserAttributes2`, which is ugly and correct,
90
+ * rather than one entry silently overwriting the other in the registry.
91
+ */
92
+ export function deriveName(base: string, suffix: string, taken: Set<string>): string {
93
+ let candidate = `${base}${suffix}`;
94
+ let n = 2;
95
+ while (taken.has(candidate)) {
96
+ candidate = `${base}${suffix}${n}`;
97
+ n++;
98
+ }
99
+ taken.add(candidate);
100
+ return candidate;
101
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Cedar lexicon packaging — delegates to core's packagePipeline.
3
+ */
4
+
5
+ import { readFileSync } from "fs";
6
+ import { dirname, join } from "path";
7
+ import { fileURLToPath } from "url";
8
+ import {
9
+ packagePipeline,
10
+ type PackageOptions,
11
+ type PackageResult,
12
+ } from "@intentius/chant/codegen/package";
13
+ import { generate } from "./generate";
14
+
15
+ export type { PackageOptions, PackageResult };
16
+
17
+ const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
18
+
19
+ /**
20
+ * Package the Cedar lexicon into a distributable BundleSpec.
21
+ */
22
+ export async function packageLexicon(opts: PackageOptions = {}): Promise<PackageResult> {
23
+ const pkgJson = JSON.parse(readFileSync(join(pkgDir, "..", "package.json"), "utf-8")) as {
24
+ version?: string;
25
+ };
26
+
27
+ return packagePipeline(
28
+ {
29
+ // Codegen reads this package's own directory as the project root, so
30
+ // bundling produces the default-schema surface no matter where the
31
+ // command is run from (#1650).
32
+ generate: (genOpts) => generate({ ...genOpts, projectRoot: pkgDir }),
33
+
34
+ buildManifest: () => ({
35
+ name: "cedar",
36
+ version: pkgJson.version ?? "0.0.0",
37
+ chantVersion: ">=0.1.0",
38
+ namespace: "Cedar",
39
+ intrinsics: [],
40
+ pseudoParameters: {},
41
+ }),
42
+
43
+ srcDir: pkgDir,
44
+
45
+ // Skills, initTemplates and the docs site are #1654.
46
+ collectSkills: () => new Map(),
47
+
48
+ version: pkgJson.version ?? "0.0.0",
49
+ },
50
+ opts,
51
+ );
52
+ }
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Props, types and defaults for the two composites — plus the thing that
3
+ * matters more: what they serialize to, checked against cedar-wasm.
4
+ *
5
+ * A composite that produces the right `props` object and the wrong Cedar text
6
+ * is still broken, and the serializer is the only place the two meet.
7
+ */
8
+
9
+ import { describe, expect, it } from "vitest";
10
+ import { checkParsePolicySet, getValidRequestEnvsPolicy } from "@cedar-policy/cedar-wasm/nodejs";
11
+ import { readFileSync } from "fs";
12
+ import { dirname, join } from "path";
13
+ import { fileURLToPath } from "url";
14
+ import type { Declarable } from "@intentius/chant/declarable";
15
+ import { DenyByDefaultSet, OwnerCanManage } from "./index";
16
+ import { cedarSerializer } from "../serializer";
17
+ import { DeleteAction, ReadAction, WriteAction } from "../generated/index";
18
+
19
+ const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
20
+ const schema = readFileSync(join(pkgDir, "src", "spec", "default-schema.cedarschema"), "utf-8");
21
+
22
+ function props(entity: Declarable): Record<string, unknown> {
23
+ return (entity as unknown as { props: Record<string, unknown> }).props;
24
+ }
25
+
26
+ /** Serialize a named set of policies to `.cedar` text. */
27
+ function emit(entities: Record<string, Declarable>): string {
28
+ const map = new Map<string, Declarable>(Object.entries(entities));
29
+ const result = cedarSerializer.serialize(map);
30
+ return typeof result === "string" ? result : result.primary;
31
+ }
32
+
33
+ describe("OwnerCanManage", () => {
34
+ it("pins the resource scope and the ownership guard together", () => {
35
+ const p = props(OwnerCanManage({ entityType: "App::Document", actions: ReadAction }));
36
+
37
+ expect(p.effect).toBe("permit");
38
+ expect(p.resource).toEqual({ is: "App::Document" });
39
+ expect(p.when).toEqual(["resource.owner == principal"]);
40
+ });
41
+
42
+ it("defaults the owner attribute to `owner` and takes an override", () => {
43
+ expect(props(OwnerCanManage({ entityType: "App::Folder" })).when).toEqual([
44
+ "resource.owner == principal",
45
+ ]);
46
+
47
+ expect(
48
+ props(OwnerCanManage({ entityType: "App::Folder", ownerAttribute: "custodian" })).when,
49
+ ).toEqual(["resource.custodian == principal"]);
50
+ });
51
+
52
+ it("emits `==` for one action and `in` for several", () => {
53
+ expect(props(OwnerCanManage({ entityType: "App::Document", actions: ReadAction })).action).toEqual({
54
+ eq: ReadAction,
55
+ });
56
+ expect(props(OwnerCanManage({ entityType: "App::Document", actions: [ReadAction] })).action).toEqual({
57
+ eq: ReadAction,
58
+ });
59
+ expect(
60
+ props(OwnerCanManage({ entityType: "App::Document", actions: [ReadAction, WriteAction] })).action,
61
+ ).toEqual({ in: [ReadAction, WriteAction] });
62
+ });
63
+
64
+ it("leaves the action position unconstrained only when actions are omitted", () => {
65
+ expect(props(OwnerCanManage({ entityType: "App::Document" })).action).toEqual({});
66
+ });
67
+
68
+ it("widens the principal to anyone by default and narrows on request", () => {
69
+ expect(props(OwnerCanManage({ entityType: "App::Document" })).principal).toEqual({});
70
+ expect(props(OwnerCanManage({ entityType: "App::Document", principal: "App::User" })).principal).toEqual({
71
+ is: "App::User",
72
+ });
73
+ expect(
74
+ props(OwnerCanManage({ entityType: "App::Document", principal: { eq: 'App::User::"root"' } })).principal,
75
+ ).toEqual({ eq: 'App::User::"root"' });
76
+ });
77
+
78
+ it("appends extra guards after the ownership test", () => {
79
+ const p = props(
80
+ OwnerCanManage({
81
+ entityType: "App::Document",
82
+ actions: WriteAction,
83
+ when: ["context.mfa == true"],
84
+ unless: ['resource.classification == "confidential"'],
85
+ }),
86
+ );
87
+
88
+ expect(p.when).toEqual(["resource.owner == principal", "context.mfa == true"]);
89
+ expect(p.unless).toEqual(['resource.classification == "confidential"']);
90
+ });
91
+
92
+ it("omits `unless` entirely when none is asked for", () => {
93
+ expect("unless" in props(OwnerCanManage({ entityType: "App::Document" }))).toBe(false);
94
+ });
95
+
96
+ it("annotates its provenance and lets the caller override the id", () => {
97
+ const p = props(
98
+ OwnerCanManage({ entityType: "App::Document", annotations: { id: "doc-owner", team: "docs" } }),
99
+ );
100
+
101
+ expect(p.annotations).toEqual({
102
+ composite: "OwnerCanManage",
103
+ scopedTo: "App::Document",
104
+ id: "doc-owner",
105
+ team: "docs",
106
+ });
107
+ });
108
+
109
+ it("serializes to Cedar that parses and resolves to the intended envelope", () => {
110
+ const text = emit({
111
+ docOwnerRead: OwnerCanManage({
112
+ entityType: "App::Document",
113
+ actions: [ReadAction, WriteAction],
114
+ principal: "App::User",
115
+ }),
116
+ });
117
+
118
+ expect(text).toContain("permit (");
119
+ expect(text).toContain("resource is App::Document");
120
+ expect(checkParsePolicySet({ staticPolicies: text }).type).toBe("success");
121
+
122
+ const envs = getValidRequestEnvsPolicy(text.trim(), schema);
123
+ expect(envs.type).toBe("success");
124
+ if (envs.type === "success") {
125
+ expect(envs.principals).toEqual(["App::User"]);
126
+ expect(envs.resources).toEqual(["App::Document"]);
127
+ expect(envs.actions.sort()).toEqual([ReadAction, WriteAction].sort());
128
+ }
129
+ });
130
+ });
131
+
132
+ describe("DenyByDefaultSet", () => {
133
+ const member = OwnerCanManage({ entityType: "App::Document", actions: ReadAction });
134
+
135
+ it("returns the floor first, the members unchanged, and both in `all`", () => {
136
+ const set = DenyByDefaultSet({
137
+ policies: [member],
138
+ entityType: "App::Document",
139
+ when: ['resource.classification == "secret"'],
140
+ });
141
+
142
+ expect(props(set.floor).effect).toBe("forbid");
143
+ expect(set.members).toEqual([member]);
144
+ expect(set.all[0]).toBe(set.floor);
145
+ expect(set.all).toHaveLength(2);
146
+ });
147
+
148
+ it("does not mutate the caller's array", () => {
149
+ const policies = [member];
150
+ const set = DenyByDefaultSet({ policies, when: ["context.mfa == false"] });
151
+
152
+ expect(set.members).not.toBe(policies);
153
+ expect(policies).toHaveLength(1);
154
+ });
155
+
156
+ it("defaults both the resource and the action position to unconstrained", () => {
157
+ const p = props(DenyByDefaultSet({ policies: [], when: ["context.mfa == false"] }).floor);
158
+
159
+ expect(p.resource).toEqual({});
160
+ expect(p.action).toEqual({});
161
+ expect(p.principal).toEqual({});
162
+ });
163
+
164
+ it("narrows on entityType, actions and principal", () => {
165
+ const p = props(
166
+ DenyByDefaultSet({
167
+ policies: [],
168
+ entityType: "App::Document",
169
+ actions: [DeleteAction, WriteAction],
170
+ principal: "App::User",
171
+ when: ['resource.classification == "confidential"'],
172
+ unless: ['principal == App::User::"archivist"'],
173
+ }).floor,
174
+ );
175
+
176
+ expect(p.resource).toEqual({ is: "App::Document" });
177
+ expect(p.action).toEqual({ in: [DeleteAction, WriteAction] });
178
+ expect(p.principal).toEqual({ is: "App::User" });
179
+ expect(p.unless).toEqual(['principal == App::User::"archivist"']);
180
+ });
181
+
182
+ it("records the member count and the composite name in annotations", () => {
183
+ const set = DenyByDefaultSet({ policies: [member, member], when: ["context.mfa == false"] });
184
+
185
+ expect(props(set.floor).annotations).toEqual({ composite: "DenyByDefaultSet", members: "2" });
186
+ });
187
+
188
+ it("refuses an unguarded forbid", () => {
189
+ expect(() => DenyByDefaultSet({ policies: [member], when: [] })).toThrow(/at least one condition/);
190
+ });
191
+
192
+ it("serializes floor-then-members as Cedar that parses", () => {
193
+ const set = DenyByDefaultSet({
194
+ policies: [member],
195
+ entityType: "App::Document",
196
+ when: ['resource.classification == "confidential"'],
197
+ unless: ["context.mfa == true"],
198
+ });
199
+
200
+ const text = emit({ secretFloor: set.floor, docOwnerRead: set.members[0] });
201
+
202
+ expect(text.indexOf("forbid (")).toBeLessThan(text.indexOf("permit ("));
203
+ expect(text).toContain("unless { context.mfa == true }");
204
+ expect(checkParsePolicySet({ staticPolicies: text }).type).toBe("success");
205
+ });
206
+ });
@@ -0,0 +1,98 @@
1
+ /**
2
+ * DenyByDefaultSet — a forbid floor under a group of permits.
3
+ *
4
+ * Cedar is already default-deny, so a floor is not about the absence of a
5
+ * permit; it is about the presence of a bad one. A `forbid` beats every
6
+ * `permit` in the same set unconditionally, which makes a forbid the only
7
+ * construct that survives a later, wider grant. The pattern teams write by hand
8
+ * is a guarded forbid at the top of a file and a pile of permits under it, with
9
+ * nothing tying the two together — delete the forbid and the permits keep
10
+ * working, wider than anyone intended.
11
+ *
12
+ * This composite returns both halves from one call, so the floor cannot be
13
+ * dropped without dropping the members with it.
14
+ */
15
+
16
+ import type { Declarable } from "@intentius/chant/declarable";
17
+ import { Policy } from "../generated/index";
18
+ import type { EntityTypeName, PolicyRef, PolicyScope } from "../generated/index";
19
+
20
+ export interface DenyByDefaultSetOpts {
21
+ /**
22
+ * The permits the floor sits under. They are returned unchanged — the floor
23
+ * constrains them at evaluation time, not at construction time.
24
+ */
25
+ policies: Declarable[];
26
+ /**
27
+ * The entity type the floor covers. Omit to make it apply to every resource,
28
+ * which is the strongest form and worth stating deliberately.
29
+ */
30
+ entityType?: EntityTypeName;
31
+ /**
32
+ * Actions the floor forbids. Defaults to every action, i.e. an unconstrained
33
+ * action position.
34
+ */
35
+ actions?: PolicyRef | PolicyRef[];
36
+ /**
37
+ * The conditions under which the denial applies. Required: a forbid with no
38
+ * guard denies everything unconditionally and no permit can lift it, which
39
+ * is a policy set that authorizes nothing.
40
+ */
41
+ when: string[];
42
+ /** Carve-outs from the denial. The break-glass principal usually lives here. */
43
+ unless?: string[];
44
+ /** Narrow the floor's principal. A bare entity type becomes `principal is T`. */
45
+ principal?: EntityTypeName | PolicyScope;
46
+ /** Merged over the generated annotations; an explicit `id` wins. */
47
+ annotations?: Record<string, string>;
48
+ }
49
+
50
+ export interface DenyByDefaultSetResources {
51
+ /** The `forbid` policy. Emitted first in declaration order. */
52
+ floor: Declarable;
53
+ /** The permits, unchanged and in the order given. */
54
+ members: Declarable[];
55
+ /** `[floor, ...members]`, for `export const … = DenyByDefaultSet(…).all`. */
56
+ all: Declarable[];
57
+ }
58
+
59
+ function scope(value: EntityTypeName | PolicyScope | undefined): PolicyScope {
60
+ if (value === undefined) return {};
61
+ return typeof value === "string" ? { is: value } : value;
62
+ }
63
+
64
+ function actionScope(actions: DenyByDefaultSetOpts["actions"]): PolicyScope {
65
+ if (actions === undefined) return {};
66
+ if (Array.isArray(actions)) {
67
+ return actions.length === 1 ? { eq: actions[0] } : { in: actions };
68
+ }
69
+ return { eq: actions };
70
+ }
71
+
72
+ /**
73
+ * A guarded `forbid` plus the permits it governs.
74
+ */
75
+ export function DenyByDefaultSet(opts: DenyByDefaultSetOpts): DenyByDefaultSetResources {
76
+ if (opts.when.length === 0) {
77
+ throw new Error(
78
+ "DenyByDefaultSet: `when` must carry at least one condition. An unguarded forbid " +
79
+ "overrides every permit in the set, so the result would authorize nothing.",
80
+ );
81
+ }
82
+
83
+ const floor = new Policy({
84
+ effect: "forbid",
85
+ principal: scope(opts.principal),
86
+ action: actionScope(opts.actions),
87
+ resource: scope(opts.entityType),
88
+ when: opts.when,
89
+ ...(opts.unless && opts.unless.length > 0 ? { unless: opts.unless } : {}),
90
+ annotations: {
91
+ composite: "DenyByDefaultSet",
92
+ members: String(opts.policies.length),
93
+ ...(opts.annotations ?? {}),
94
+ },
95
+ });
96
+
97
+ return { floor, members: [...opts.policies], all: [floor, ...opts.policies] };
98
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Cedar lexicon composites.
3
+ *
4
+ * Cedar has no functions, no modules, no loops, and templates with exactly two
5
+ * slots — so every repeated policy shape is copy-paste in `.cedar` text. These
6
+ * factories are where that repetition goes instead.
7
+ */
8
+
9
+ export { OwnerCanManage } from "./owner-can-manage";
10
+ export type { OwnerCanManageOpts } from "./owner-can-manage";
11
+
12
+ export { DenyByDefaultSet } from "./deny-by-default-set";
13
+ export type { DenyByDefaultSetOpts, DenyByDefaultSetResources } from "./deny-by-default-set";
@@ -0,0 +1,80 @@
1
+ /**
2
+ * OwnerCanManage — the ownership grant, written once.
3
+ *
4
+ * "The owner of a thing may act on it" is the most-repeated shape in every
5
+ * Cedar policy set, and hand-writing it per entity type is where the two
6
+ * classic mistakes get made: the `when` guard names an attribute the schema
7
+ * spells differently, or the grant is written wide and the scoping clause is
8
+ * forgotten. Cedar itself cannot help — it has no functions, no modules, and
9
+ * templates carry exactly two slots. A TypeScript factory is the only place
10
+ * this abstraction can live.
11
+ *
12
+ * The composite emits a permit whose resource scope is pinned with `is` and
13
+ * whose ownership test is a `when` clause, so both halves arrive together or
14
+ * neither does.
15
+ */
16
+
17
+ import type { Declarable } from "@intentius/chant/declarable";
18
+ import { Policy } from "../generated/index";
19
+ import type { EntityTypeName, PolicyRef, PolicyScope } from "../generated/index";
20
+
21
+ export interface OwnerCanManageOpts {
22
+ /** The entity type owners act on, e.g. `"App::Document"`. */
23
+ entityType: EntityTypeName;
24
+ /**
25
+ * Actions the owner is granted. A single action is emitted as `== A`, several
26
+ * as `in [A, …]`, and none leaves the action position unconstrained — which
27
+ * is a wide grant, so it has to be asked for by omitting the field.
28
+ */
29
+ actions?: PolicyRef | PolicyRef[];
30
+ /**
31
+ * The attribute on the resource that holds the owner. Defaults to `owner`,
32
+ * the name the bundled schema and most Cedar examples use.
33
+ */
34
+ ownerAttribute?: string;
35
+ /**
36
+ * Narrow the principal beyond "anyone". A bare entity type string becomes
37
+ * `principal is T`; a full scope is passed through.
38
+ */
39
+ principal?: EntityTypeName | PolicyScope;
40
+ /** Extra `when` clauses, appended after the ownership test. */
41
+ when?: string[];
42
+ /** `unless` clauses. An MFA requirement is the usual one. */
43
+ unless?: string[];
44
+ /** Merged over the generated annotations; an explicit `id` wins. */
45
+ annotations?: Record<string, string>;
46
+ }
47
+
48
+ function actionScope(actions: OwnerCanManageOpts["actions"]): PolicyScope {
49
+ if (actions === undefined) return {};
50
+ if (Array.isArray(actions)) {
51
+ return actions.length === 1 ? { eq: actions[0] } : { in: actions };
52
+ }
53
+ return { eq: actions };
54
+ }
55
+
56
+ function principalScope(principal: OwnerCanManageOpts["principal"]): PolicyScope {
57
+ if (principal === undefined) return {};
58
+ return typeof principal === "string" ? { is: principal } : principal;
59
+ }
60
+
61
+ /**
62
+ * One permit: the owner of an `entityType` may take `actions` on it.
63
+ */
64
+ export function OwnerCanManage(opts: OwnerCanManageOpts): Declarable {
65
+ const ownerAttribute = opts.ownerAttribute ?? "owner";
66
+
67
+ return new Policy({
68
+ effect: "permit",
69
+ principal: principalScope(opts.principal),
70
+ action: actionScope(opts.actions),
71
+ resource: { is: opts.entityType },
72
+ when: [`resource.${ownerAttribute} == principal`, ...(opts.when ?? [])],
73
+ ...(opts.unless && opts.unless.length > 0 ? { unless: opts.unless } : {}),
74
+ annotations: {
75
+ composite: "OwnerCanManage",
76
+ scopedTo: opts.entityType,
77
+ ...(opts.annotations ?? {}),
78
+ },
79
+ });
80
+ }