@intentius/chant-lexicon-cedar 0.124.31 → 0.124.33

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 (101) hide show
  1. package/dist/agentcore/embed.js +250 -0
  2. package/dist/agentcore/enforcement.js +81 -0
  3. package/dist/agentcore/scan.js +93 -0
  4. package/dist/avp/ambient.js +100 -0
  5. package/dist/avp/client.js +318 -0
  6. package/dist/avp/describe-resources.js +144 -0
  7. package/dist/avp/embed.js +125 -0
  8. package/dist/avp/live-export.js +151 -0
  9. package/dist/avp/ownership.js +144 -0
  10. package/dist/avp/statement.js +45 -0
  11. package/dist/avp/store.js +105 -0
  12. package/dist/avp/testdata/mock-transport.js +100 -0
  13. package/dist/codegen/docs-cli.js +6 -0
  14. package/dist/codegen/docs.js +133 -0
  15. package/dist/codegen/emit.js +331 -0
  16. package/dist/codegen/generate-cli.js +18 -0
  17. package/dist/codegen/generate.js +122 -0
  18. package/dist/codegen/naming.js +90 -0
  19. package/dist/codegen/package.js +33 -0
  20. package/dist/commands.js +86 -0
  21. package/dist/composites/deny-by-default-set.js +51 -0
  22. package/dist/composites/index.js +9 -0
  23. package/dist/composites/owner-can-manage.js +48 -0
  24. package/dist/config.js +159 -0
  25. package/dist/coverage.js +112 -0
  26. package/dist/detect.js +61 -0
  27. package/dist/dogwood/cli.js +477 -0
  28. package/dist/dogwood/event-schema.js +219 -0
  29. package/dist/dogwood/index.js +32 -0
  30. package/dist/dogwood/macros.js +175 -0
  31. package/dist/dogwood/policy.js +58 -0
  32. package/dist/dogwood/replay-activity.js +313 -0
  33. package/dist/dogwood/replay-op.js +134 -0
  34. package/dist/dogwood/scan.js +234 -0
  35. package/dist/dogwood/serialize.js +199 -0
  36. package/dist/dogwood/temporal.js +354 -0
  37. package/dist/dogwood/trace.js +269 -0
  38. package/dist/dogwood/upstream.js +40 -0
  39. package/dist/dogwood/window.js +82 -0
  40. package/dist/gate-policy-pack.js +189 -0
  41. package/dist/gate-policy.js +168 -0
  42. package/dist/gate-schema.js +122 -0
  43. package/dist/generated/index.js +94 -0
  44. package/dist/generated/runtime.js +2 -0
  45. package/dist/import/adapter.js +54 -0
  46. package/dist/import/clause-text.js +166 -0
  47. package/dist/import/generator.js +178 -0
  48. package/dist/import/parser.js +273 -0
  49. package/dist/index.js +79 -0
  50. package/dist/init-templates.js +330 -0
  51. package/dist/integrity.json +2 -2
  52. package/dist/lint/audit-catalog.js +65 -0
  53. package/dist/lint/audit-lineage.js +23 -0
  54. package/dist/lint/post-synth/cedar-helpers.js +123 -0
  55. package/dist/lint/post-synth/cedc010.js +59 -0
  56. package/dist/lint/post-synth/cedc011.js +29 -0
  57. package/dist/lint/post-synth/cedc012.js +34 -0
  58. package/dist/lint/post-synth/cedc013.js +45 -0
  59. package/dist/lint/post-synth/cedc014.js +54 -0
  60. package/dist/lint/post-synth/cede010.js +51 -0
  61. package/dist/lint/post-synth/cede011.js +36 -0
  62. package/dist/lint/post-synth/ceds010.js +31 -0
  63. package/dist/lint/post-synth/ceds011.js +22 -0
  64. package/dist/lint/post-synth/ceds012.js +31 -0
  65. package/dist/lint/post-synth/dogwood-helpers.js +94 -0
  66. package/dist/lint/post-synth/dwdc010.js +38 -0
  67. package/dist/lint/post-synth/dwdc011.js +34 -0
  68. package/dist/lint/post-synth/dwdc012.js +24 -0
  69. package/dist/lint/post-synth/dwdc013.js +29 -0
  70. package/dist/lint/post-synth/dwde010.js +84 -0
  71. package/dist/lint/post-synth/dwde011.js +67 -0
  72. package/dist/lint/post-synth/dwds010.js +21 -0
  73. package/dist/lint/post-synth/index.js +36 -0
  74. package/dist/lint/post-synth/wasm-helpers.js +234 -0
  75. package/dist/lint/rules/index.js +9 -0
  76. package/dist/lint/rules/policy-shape.js +114 -0
  77. package/dist/lsp/completions.js +52 -0
  78. package/dist/lsp/hover.js +56 -0
  79. package/dist/lsp/registry.d.ts.map +1 -1
  80. package/dist/lsp/registry.js +51 -0
  81. package/dist/manifest.json +1 -1
  82. package/dist/mcp/index.js +82 -0
  83. package/dist/mcp/policy-coverage.js +134 -0
  84. package/dist/op/activities/index.js +16 -0
  85. package/dist/package-cli.js +18 -0
  86. package/dist/plugin.js +304 -0
  87. package/dist/policy-text.js +117 -0
  88. package/dist/schema-artifact.js +66 -0
  89. package/dist/serializer.js +203 -0
  90. package/dist/spec/fetch.js +96 -0
  91. package/dist/spec/parse.js +164 -0
  92. package/dist/spec/pin.js +170 -0
  93. package/dist/spec/wasm.js +194 -0
  94. package/dist/validate-cli.js +7 -0
  95. package/dist/validate.js +74 -0
  96. package/package.json +7 -7
  97. package/src/codegen/package.ts +2 -2
  98. package/src/coverage.ts +2 -2
  99. package/src/import/generator.ts +2 -2
  100. package/src/lsp/registry.ts +2 -1
  101. package/src/spec/fetch.ts +2 -2
@@ -0,0 +1,133 @@
1
+ /**
2
+ * The cedar lexicon's Starlight site.
3
+ *
4
+ * Prose pages are authored under `docs/pages/*.mdx`, each tagged with a
5
+ * Diátaxis quadrant (chant #1731/#1733); the pipeline builds the sidebar from
6
+ * that tag. Two more pages come out of the pipeline itself: the generated
7
+ * rules table (`rules`) and the serialization reference. Both are linked from
8
+ * the sidebar automatically.
9
+ *
10
+ * Cross-namespace links are written as full `/chant/...` paths. This site's
11
+ * base is `/chant/lexicons/cedar/`, so a bare `/guide/...` would be rewritten
12
+ * to `/chant/lexicons/cedar/guide/...`; the rehype plugin's idempotency check
13
+ * leaves an already-project-rooted path alone. Sibling pages use `../slug/`,
14
+ * because `./slug` from an MDX body resolves as a child.
15
+ */
16
+ import { dirname, join } from "path";
17
+ import { fileURLToPath } from "url";
18
+ import { docsPipeline, writeDocsSite } from "@intentius/chant/codegen/docs";
19
+ const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
20
+ // ── index ─────────────────────────────────────────────────────────
21
+ const overview = `The **cedar** lexicon is the typed authoring layer above [Cedar](https://www.cedarpolicy.com/), the vendor-neutral authorization policy language that joined the CNCF as a Sandbox project in December 2025.
22
+
23
+ Cedar is deliberately abstraction-free: no variables, no modules, no loops, and templates carrying exactly two slots (\`?principal\`, \`?resource\`). Its own toolchain validates and evaluates — it checks policies after they are written and decides requests at runtime. Everything upstream of the policy text is unowned, which is where this lexicon lives.
24
+
25
+ \`\`\`bash
26
+ npm install --save-dev @intentius/chant-lexicon-cedar
27
+ \`\`\`
28
+
29
+ ## Quick start
30
+
31
+ \`\`\`typescript
32
+ import { Policy, ReadAction } from "@intentius/chant-lexicon-cedar";
33
+
34
+ export const ownerRead = new Policy({
35
+ effect: "permit",
36
+ principal: { is: "App::User" },
37
+ action: { eq: ReadAction },
38
+ resource: { is: "App::Document" },
39
+ when: ["resource.owner == principal"],
40
+ });
41
+ \`\`\`
42
+
43
+ \`ReadAction\` and \`"App::User"\` are generated from *your* Cedar schema, so a
44
+ renamed entity type is a compiler-guided refactor and a typo'd action is a
45
+ compile error — not a validation failure after the text is written.
46
+
47
+ ## What comes out
48
+
49
+ | File | Who reads it |
50
+ |------|--------------|
51
+ | \`<name>.cedar\` | Every Cedar evaluator — Amazon Verified Permissions, cedar-agent, an embedded \`cedar-wasm\` |
52
+ | \`policies.cedar.json\` | The Cedar JSON policy format; also the parse source for import |
53
+
54
+ chant appears in neither. An emitted policy set walks away and is consumed by
55
+ any evaluator with chant nowhere in sight.
56
+
57
+ ## Cedar is a target, never a gate
58
+
59
+ There is no \`cedarGate()\` and there will not be one.
60
+ [Organizational policy](/chant/guide/organizational-policy/) in chant is
61
+ TypeScript post-synth checks; a second policy engine would duplicate the lint
62
+ engine. chant compiles *to* Cedar; it is not governed *by* Cedar.
63
+
64
+ ## The dogwood dialect
65
+
66
+ Cedar with temporal operators, shipping inside this lexicon as a pre-release
67
+ surface under the \`DWD\` id family: a policy that can depend on what already
68
+ happened in a session. Start at [The Dogwood Dialect](./dogwood), which is
69
+ honest about upstream's governance before it shows you a builder.`;
70
+ // ── Output format, for the generated serialization page ───────────
71
+ const outputFormat = `The cedar lexicon emits two views of one policy set.
72
+
73
+ **\`<name>.cedar\`** — the primary output, and the surface every Cedar evaluator reads.
74
+
75
+ \`\`\`cedar
76
+ @id("owner-read")
77
+ @doc("Owners always read their own documents.")
78
+ permit (
79
+ principal is App::User,
80
+ action in [App::Action::"read", App::Action::"list"],
81
+ resource is App::Document
82
+ )
83
+ when { resource.owner == principal };
84
+ \`\`\`
85
+
86
+ **\`policies.cedar.json\`** — the Cedar JSON policy format, written alongside.
87
+
88
+ \`\`\`json
89
+ {
90
+ "staticPolicies": {
91
+ "owner-read": {
92
+ "effect": "permit",
93
+ "principal": { "op": "is", "entity_type": "App::User" },
94
+ "action": { "op": "in", "entities": [{ "type": "App::Action", "id": "read" }] },
95
+ "resource": { "op": "is", "entity_type": "App::Document" },
96
+ "conditions": [{ "kind": "when", "body": { "__expr": "resource.owner == principal" } }],
97
+ "annotations": { "id": "owner-read" }
98
+ }
99
+ },
100
+ "templates": {},
101
+ "templateLinks": []
102
+ }
103
+ \`\`\`
104
+
105
+ Both come from the same structured model in one pass, so they cannot drift. The
106
+ JSON form is also the parse source for import — see [Importing](../importing/).
107
+ `;
108
+ /**
109
+ * Generate the docs site for the cedar lexicon.
110
+ */
111
+ export async function generateDocs(options) {
112
+ const config = {
113
+ name: "cedar",
114
+ displayName: "Cedar",
115
+ description: "Typed authoring for Cedar authorization policies",
116
+ distDir: join(pkgDir, "dist"),
117
+ outDir: join(pkgDir, "docs"),
118
+ srcDir: join(pkgDir, "src"),
119
+ basePath: process.env.DOCS_BASE_PATH ?? "/chant/lexicons/cedar/",
120
+ overview,
121
+ outputFormat,
122
+ // Cedar declarations are namespaced `App::Document` and
123
+ // `App::Action::"read"`, so the first segment is the namespace — which is
124
+ // the only grouping a Cedar schema has.
125
+ serviceFromType: (type) => type.split("::")[0] ?? type,
126
+ };
127
+ const result = docsPipeline(config);
128
+ writeDocsSite(config, result);
129
+ if (options?.verbose) {
130
+ console.error(`Generated docs: ${result.pages.size} pages, ${result.stats.resources} resources, ` +
131
+ `${result.stats.properties} property types, ${result.stats.rules} rules`);
132
+ }
133
+ }
@@ -0,0 +1,331 @@
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
+ import { deriveName, POLICY_TS_NAME, POLICY_TYPE } from "./naming.js";
12
+ export const LEXICON_NAME = "cedar";
13
+ /**
14
+ * Assign every generated name, once.
15
+ *
16
+ * `NamingStrategy` names the primary declarations; the derived record names
17
+ * are de-duplicated against the same pool so a `UserAttributes` entity type in
18
+ * the schema cannot quietly take the name `User`'s attribute record wants.
19
+ */
20
+ export function buildEmitModel(decls, naming) {
21
+ const taken = new Set([POLICY_TS_NAME]);
22
+ const entities = [];
23
+ const actions = [];
24
+ const sorted = [...decls].sort((a, b) => a.typeName.localeCompare(b.typeName));
25
+ // Primary names first, all of them, before any derived name is claimed —
26
+ // otherwise `UserAttributes` (derived) could take the name an entity type
27
+ // literally called `UserAttributes` is entitled to.
28
+ const primaryNames = new Map();
29
+ for (const decl of sorted) {
30
+ const base = naming.resolve(decl.typeName);
31
+ if (!base)
32
+ continue;
33
+ // `NamingStrategy`'s last phase qualifies a collision with the service
34
+ // name, which for Cedar is the namespace — so two declarations in the
35
+ // *same* namespace that reduce to the same short name (an entity type
36
+ // `ReadAction` beside an action `read`) both come back as `AppReadAction`.
37
+ // Silently letting the second overwrite the first in the registry is the
38
+ // one outcome worth refusing.
39
+ primaryNames.set(decl.typeName, deriveName(base, "", taken));
40
+ }
41
+ for (const decl of sorted) {
42
+ const tsName = primaryNames.get(decl.typeName);
43
+ if (!tsName)
44
+ continue;
45
+ if (decl.kind === "entity") {
46
+ entities.push({
47
+ tsName,
48
+ typeName: decl.typeName,
49
+ namespace: decl.namespace,
50
+ attributesTsName: deriveName(tsName, "Attributes", taken),
51
+ uidTsName: deriveName(tsName, "Uid", taken),
52
+ memberOfTypes: decl.memberOfTypes,
53
+ attributes: decl.attributes,
54
+ ...(decl.enumValues ? { enumValues: decl.enumValues } : {}),
55
+ });
56
+ }
57
+ else {
58
+ actions.push({
59
+ tsName,
60
+ typeName: decl.typeName,
61
+ namespace: decl.namespace,
62
+ actionId: decl.actionId,
63
+ contextTsName: deriveName(stripActionSuffix(tsName), "Context", taken),
64
+ principalTypes: decl.principalTypes,
65
+ resourceTypes: decl.resourceTypes,
66
+ context: decl.context,
67
+ });
68
+ }
69
+ }
70
+ entities.sort((a, b) => a.tsName.localeCompare(b.tsName));
71
+ actions.sort((a, b) => a.tsName.localeCompare(b.tsName));
72
+ const uidTypeByEntityType = new Map();
73
+ for (const entity of entities)
74
+ uidTypeByEntityType.set(entity.typeName, entity.uidTsName);
75
+ return { entities, actions, uidTypeByEntityType };
76
+ }
77
+ /** `ReadAction` → `Read`, so the context type reads `ReadContext`. */
78
+ function stripActionSuffix(tsName) {
79
+ return tsName.endsWith("Action") && tsName.length > "Action".length
80
+ ? tsName.slice(0, -"Action".length)
81
+ : tsName;
82
+ }
83
+ // ── TypeScript type rendering ─────────────────────────────────────
84
+ /**
85
+ * Render one Cedar type as a TypeScript type expression.
86
+ *
87
+ * Extension types (`ipaddr`, `decimal`, `datetime`, `duration`) render as
88
+ * `string`: Cedar constructs them from string literals in policy text, and
89
+ * inventing branded TS types for values chant never evaluates would be
90
+ * decoration.
91
+ */
92
+ export function renderType(type, model) {
93
+ switch (type.kind) {
94
+ case "primitive":
95
+ return type.ts;
96
+ case "entity":
97
+ return model.uidTypeByEntityType.get(type.entityType) ?? "string";
98
+ case "set": {
99
+ const inner = renderType(type.element, model);
100
+ return /[|&]/.test(inner) ? `Array<${inner}>` : `${inner}[]`;
101
+ }
102
+ case "record":
103
+ return type.attributes.length === 0
104
+ ? "Record<string, never>"
105
+ : `{ ${type.attributes.map((a) => `${propertyKey(a.name)}${a.required ? "" : "?"}: ${renderType(a.type, model)};`).join(" ")} }`;
106
+ case "extension":
107
+ return "string";
108
+ case "opaque":
109
+ return "string";
110
+ }
111
+ }
112
+ const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
113
+ /** Quote an attribute name that is not a bare identifier — Cedar allows both. */
114
+ export function propertyKey(name) {
115
+ return IDENTIFIER.test(name) ? name : JSON.stringify(name);
116
+ }
117
+ /** The Cedar surface syntax for a type, for doc comments and the registry. */
118
+ export function cedarTypeText(type) {
119
+ switch (type.kind) {
120
+ case "primitive":
121
+ return type.cedar;
122
+ case "entity":
123
+ return type.entityType;
124
+ case "set":
125
+ return `Set<${cedarTypeText(type.element)}>`;
126
+ case "record":
127
+ return `{ ${type.attributes.map((a) => `"${a.name}"${a.required ? "" : "?"}: ${cedarTypeText(a.type)}`).join(", ")} }`;
128
+ case "extension":
129
+ return type.name;
130
+ case "opaque":
131
+ return type.name;
132
+ }
133
+ }
134
+ /** `App::User` → `App::User.Attributes`, the record type's own spec name. */
135
+ export function attributesTypeName(entityTypeName) {
136
+ return `${entityTypeName}.Attributes`;
137
+ }
138
+ /** `App::Action::"read"` → `App::Action::"read".Context`. */
139
+ export function contextTypeName(actionTypeName) {
140
+ return `${actionTypeName}.Context`;
141
+ }
142
+ function attributeTable(attributes) {
143
+ const out = {};
144
+ for (const attr of [...attributes].sort((a, b) => a.name.localeCompare(b.name))) {
145
+ out[attr.name] = { type: cedarTypeText(attr.type), required: attr.required };
146
+ }
147
+ return out;
148
+ }
149
+ /** Build `lexicon-cedar.json`. */
150
+ export function generateRegistry(model) {
151
+ const entries = {};
152
+ entries[POLICY_TS_NAME] = {
153
+ resourceType: POLICY_TYPE,
154
+ kind: "resource",
155
+ lexicon: LEXICON_NAME,
156
+ description: "A Cedar policy — the unit this lexicon serializes to .cedar text and the JSON policy format.",
157
+ properties: {
158
+ effect: { type: "permit | forbid", required: false },
159
+ principal: { type: "scope", required: false },
160
+ action: { type: "scope", required: false },
161
+ resource: { type: "scope", required: false },
162
+ when: { type: "Set<expression>", required: false },
163
+ unless: { type: "Set<expression>", required: false },
164
+ annotations: { type: "Record<String, String>", required: false },
165
+ },
166
+ };
167
+ for (const entity of model.entities) {
168
+ entries[entity.tsName] = {
169
+ resourceType: entity.typeName,
170
+ kind: "resource",
171
+ lexicon: LEXICON_NAME,
172
+ namespace: entity.namespace,
173
+ memberOfTypes: entity.memberOfTypes,
174
+ properties: attributeTable(entity.attributes),
175
+ ...(entity.enumValues ? { enumValues: entity.enumValues } : {}),
176
+ };
177
+ entries[entity.attributesTsName] = {
178
+ resourceType: attributesTypeName(entity.typeName),
179
+ kind: "property",
180
+ lexicon: LEXICON_NAME,
181
+ namespace: entity.namespace,
182
+ properties: attributeTable(entity.attributes),
183
+ };
184
+ }
185
+ for (const action of model.actions) {
186
+ entries[action.tsName] = {
187
+ resourceType: action.typeName,
188
+ kind: "resource",
189
+ lexicon: LEXICON_NAME,
190
+ namespace: action.namespace,
191
+ principalTypes: action.principalTypes,
192
+ resourceTypes: action.resourceTypes,
193
+ properties: attributeTable(action.context),
194
+ };
195
+ entries[action.contextTsName] = {
196
+ resourceType: contextTypeName(action.typeName),
197
+ kind: "property",
198
+ lexicon: LEXICON_NAME,
199
+ namespace: action.namespace,
200
+ properties: attributeTable(action.context),
201
+ };
202
+ }
203
+ const sorted = {};
204
+ for (const key of Object.keys(entries).sort())
205
+ sorted[key] = entries[key];
206
+ return JSON.stringify(sorted, null, 2) + "\n";
207
+ }
208
+ // ── Shared declaration bodies ─────────────────────────────────────
209
+ const BANNER = "// Code generated by chant generate. DO NOT EDIT.";
210
+ /** A template-literal type for an entity UID: `` `App::User::"${string}"` ``. */
211
+ function uidTypeExpression(entityTypeName) {
212
+ return "`" + entityTypeName + '::"${string}"`';
213
+ }
214
+ function docBlock(lines, indent = "") {
215
+ if (lines.length === 0)
216
+ return [];
217
+ return [`${indent}/**`, ...lines.map((l) => `${indent} * ${l}`), `${indent} */`];
218
+ }
219
+ function attributeLines(attributes, model) {
220
+ return attributes.map((attr) => ` ${propertyKey(attr.name)}${attr.required ? "" : "?"}: ${renderType(attr.type, model)};`);
221
+ }
222
+ function unionOr(members, fallback) {
223
+ return members.length > 0 ? members.join(" | ") : fallback;
224
+ }
225
+ /**
226
+ * The shared body of `index.ts` and `index.d.ts` — everything that is
227
+ * type-level and therefore identical in both.
228
+ */
229
+ function typeDeclarations(model) {
230
+ const lines = [];
231
+ for (const entity of model.entities) {
232
+ lines.push(...docBlock([
233
+ `Entity UID of \`${entity.typeName}\`.`,
234
+ ...(entity.memberOfTypes.length > 0 ? [`Member of: ${entity.memberOfTypes.join(", ")}.`] : []),
235
+ ]), `export type ${entity.uidTsName} = ${uidTypeExpression(entity.typeName)};`, "", `export interface ${entity.attributesTsName}Props {`, ...attributeLines(entity.attributes, model), "}", "");
236
+ }
237
+ for (const action of model.actions) {
238
+ lines.push(`export interface ${action.contextTsName}Props {`, ...attributeLines(action.context, model), "}", "");
239
+ }
240
+ lines.push("/** Every entity type name the schema declares, as Cedar writes it. */", `export type EntityTypeName = ${unionOr(model.entities.map((e) => JSON.stringify(e.typeName)), "never")};`, "", "/** Every entity UID the schema can produce. */", `export type EntityUid = ${unionOr(model.entities.map((e) => e.uidTsName), "never")};`, "", "/** Every action UID the schema declares. */", `export type ActionUid = ${unionOr(model.actions.map((a) => JSON.stringify(a.typeName)), "never")};`, "", "/** Anything a policy scope may name. */", "export type PolicyRef = EntityUid | ActionUid;", "", "/** A Cedar policy either permits or forbids; there is no third effect. */", 'export type PolicyEffect = "permit" | "forbid";', "", ...docBlock([
241
+ "One scope position (`principal`, `action`, `resource`).",
242
+ "",
243
+ "`{}` is the unconstrained form Cedar writes as a bare variable. The",
244
+ "constrained forms mirror the grammar: `== E`, `in E`, `in [E, …]`,",
245
+ "`is T`, and `is T in E`.",
246
+ ]), "export type PolicyScope =", " | Record<string, never>", " | { eq: PolicyRef; in?: never; is?: never }", " | { in: PolicyRef | PolicyRef[]; eq?: never; is?: never }", " | { is: EntityTypeName; in?: PolicyRef | PolicyRef[]; eq?: never };", "", ...docBlock([
247
+ "The props of a `Cedar::Policy`.",
248
+ "",
249
+ "`when`/`unless` carry Cedar expression text. They stay strings because",
250
+ "Cedar's expression grammar is the policy language itself — typing it in",
251
+ "TypeScript is import/reconcile's problem (epic #1645 sub-issue 6), not",
252
+ "codegen's.",
253
+ ]), "export interface PolicyProps {", " /** Defaults to `permit` when omitted. */", " effect?: PolicyEffect;", " principal?: PolicyScope;", " action?: PolicyScope;", " resource?: PolicyScope;", " /** Cedar expression strings, each emitted as its own `when { … }` clause. */", " when?: string[];", " /** Cedar expression strings, each emitted as its own `unless { … }` clause. */", " unless?: string[];", " /** Emitted as `@key(\"value\")`; an explicit `id` wins over the logical name. */", " annotations?: Record<string, string>;", "}", "");
254
+ return lines;
255
+ }
256
+ // ── index.ts ──────────────────────────────────────────────────────
257
+ /** Build the runtime `index.ts`. */
258
+ export function generateRuntimeIndex(model) {
259
+ const lines = [
260
+ BANNER,
261
+ 'import type { Declarable } from "@intentius/chant/declarable";',
262
+ 'import { createProperty, createResource } from "./runtime";',
263
+ "",
264
+ ...typeDeclarations(model),
265
+ ...docBlock([
266
+ "The policy resource. Declare one per policy:",
267
+ "",
268
+ "```ts",
269
+ 'export const allowOwnerRead = new Policy({',
270
+ ' principal: { is: "App::User" },',
271
+ " action: { eq: ReadAction },",
272
+ " when: ['resource.owner == principal'],",
273
+ "});",
274
+ "```",
275
+ ]),
276
+ `export const ${POLICY_TS_NAME} = createResource(${JSON.stringify(POLICY_TYPE)}, ${JSON.stringify(LEXICON_NAME)}, {}) as unknown as new (props: PolicyProps) => Declarable;`,
277
+ "",
278
+ ];
279
+ for (const entity of model.entities) {
280
+ lines.push(`export const ${entity.tsName} = createResource(${JSON.stringify(entity.typeName)}, ${JSON.stringify(LEXICON_NAME)}, {}) as unknown as new (props: ${entity.attributesTsName}Props) => Declarable;`, `export const ${entity.attributesTsName} = createProperty(${JSON.stringify(attributesTypeName(entity.typeName))}, ${JSON.stringify(LEXICON_NAME)}) as unknown as new (props: ${entity.attributesTsName}Props) => Declarable;`);
281
+ }
282
+ if (model.entities.length > 0)
283
+ lines.push("");
284
+ for (const action of model.actions) {
285
+ lines.push(...docBlock([
286
+ `\`${action.typeName}\`.`,
287
+ ...(action.principalTypes.length > 0 ? [`Principals: ${action.principalTypes.join(", ")}.`] : []),
288
+ ...(action.resourceTypes.length > 0 ? [`Resources: ${action.resourceTypes.join(", ")}.`] : []),
289
+ ]), `export const ${action.tsName} = ${JSON.stringify(action.typeName)} as const;`, `export const ${action.contextTsName} = createProperty(${JSON.stringify(contextTypeName(action.typeName))}, ${JSON.stringify(LEXICON_NAME)}) as unknown as new (props: ${action.contextTsName}Props) => Declarable;`);
290
+ }
291
+ if (model.actions.length > 0)
292
+ lines.push("");
293
+ lines.push("/** Every generated action constant, for exhaustive iteration. */", `export const ALL_ACTIONS: readonly ActionUid[] = [${model.actions.map((a) => a.tsName).join(", ")}];`, "", "/** Every entity type name the schema declares. */", `export const ALL_ENTITY_TYPES: readonly EntityTypeName[] = [${model.entities.map((e) => JSON.stringify(e.typeName)).join(", ")}];`, "");
294
+ return lines.join("\n");
295
+ }
296
+ // ── index.d.ts ────────────────────────────────────────────────────
297
+ /**
298
+ * Build the declaration-only `index.d.ts`.
299
+ *
300
+ * It is validated standalone by core's `typecheckDTS`, which runs tsc in a
301
+ * temp directory with no `node_modules` beyond a stub for
302
+ * `@intentius/chant/declarable` — so this file may import that and nothing
303
+ * else, and every other type it uses has to be declared here.
304
+ */
305
+ export function generateTypes(model) {
306
+ const lines = [
307
+ BANNER,
308
+ 'import type { Declarable } from "@intentius/chant/declarable";',
309
+ "",
310
+ ...typeDeclarations(model),
311
+ `export declare const ${POLICY_TS_NAME}: new (props: PolicyProps) => Declarable;`,
312
+ "",
313
+ ];
314
+ for (const entity of model.entities) {
315
+ lines.push(`export declare const ${entity.tsName}: new (props: ${entity.attributesTsName}Props) => Declarable;`, `export declare const ${entity.attributesTsName}: new (props: ${entity.attributesTsName}Props) => Declarable;`);
316
+ }
317
+ if (model.entities.length > 0)
318
+ lines.push("");
319
+ for (const action of model.actions) {
320
+ lines.push(`export declare const ${action.tsName}: ${JSON.stringify(action.typeName)};`, `export declare const ${action.contextTsName}: new (props: ${action.contextTsName}Props) => Declarable;`);
321
+ }
322
+ if (model.actions.length > 0)
323
+ lines.push("");
324
+ lines.push("export declare const ALL_ACTIONS: readonly ActionUid[];", "export declare const ALL_ENTITY_TYPES: readonly EntityTypeName[];", "");
325
+ return lines.join("\n");
326
+ }
327
+ // ── runtime.ts ────────────────────────────────────────────────────
328
+ /** The tiny re-export module `index.ts` imports its factories from. */
329
+ export const RUNTIME_TS = `${BANNER}
330
+ export { createProperty, createResource } from "@intentius/chant/runtime";
331
+ `;
@@ -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
+ * This script is the package's own build step, so the project root is the
11
+ * package root and the output is `src/generated/` (the monorepo case of
12
+ * `resolveGeneratedDir`). A consumer runs `chant generate --lexicon cedar`
13
+ * instead, which writes into the consumer's tree (#1696).
14
+ */
15
+ import { generate, packageDir, resolveGeneratedDir, writeGeneratedFiles } from "./generate.js";
16
+ const pkgDir = packageDir();
17
+ const result = await generate({ verbose: true, projectRoot: pkgDir });
18
+ writeGeneratedFiles(result, resolveGeneratedDir({ projectRoot: pkgDir }));
@@ -0,0 +1,122 @@
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
+ import { generatePipeline, writeGeneratedArtifacts } from "@intentius/chant/codegen/generate";
16
+ import { existsSync, realpathSync } from "fs";
17
+ import { dirname, isAbsolute, resolve } from "path";
18
+ import { fileURLToPath } from "url";
19
+ import { CEDAR_DEFAULT_OUT_DIR } from "../config.js";
20
+ import { createNaming } from "./naming.js";
21
+ import { buildEmitModel, generateRegistry, generateRuntimeIndex, generateTypes, RUNTIME_TS, } from "./emit.js";
22
+ import { DEFAULT_SCHEMA_KEY, fetchSchemas, resolveSchemaPath } from "../spec/fetch.js";
23
+ import { parseCedarSchema } from "../spec/parse.js";
24
+ import { assertPinnedLangVersion, assertPinnedSchema } from "../spec/pin.js";
25
+ /** Filename of the generated registry. */
26
+ export const LEXICON_JSON_FILENAME = "lexicon-cedar.json";
27
+ /** This package's root. `<pkg>/src/codegen/generate.ts` is three levels down. */
28
+ export function packageDir() {
29
+ return dirname(dirname(dirname(fileURLToPath(import.meta.url))));
30
+ }
31
+ function samePath(a, b) {
32
+ const real = (p) => (existsSync(p) ? realpathSync(p) : resolve(p));
33
+ return real(a) === real(b);
34
+ }
35
+ /**
36
+ * Where generated artifacts go (#1696).
37
+ *
38
+ * Three cases, in order:
39
+ *
40
+ * 1. `cedar.outDir` is set: that directory, resolved against the project root.
41
+ * 2. The project root *is* this package (the monorepo checkout, `npm run
42
+ * generate`, `chant dev check-lexicon`): `<pkg>/src/generated`, which
43
+ * `src/index.ts` re-exports so the package's own surface stays whole.
44
+ * 3. Anything else is a consumer: `<project>/src/generated/cedar`.
45
+ *
46
+ * Case 2 is what the old code assumed everywhere. A consumer's project root
47
+ * and the installed package are different directories, and writing into the
48
+ * second means the output is gone after `npm ci`.
49
+ */
50
+ export function resolveGeneratedDir(options = {}) {
51
+ const root = options.projectRoot ?? process.cwd();
52
+ const configured = options.config?.outDir;
53
+ if (configured)
54
+ return isAbsolute(configured) ? configured : resolve(root, configured);
55
+ const pkg = packageDir();
56
+ if (samePath(root, pkg))
57
+ return resolve(pkg, "src", "generated");
58
+ return resolve(root, CEDAR_DEFAULT_OUT_DIR);
59
+ }
60
+ /**
61
+ * Run the code generation pipeline.
62
+ */
63
+ export async function generate(options = {}) {
64
+ // Refuse a grammar nobody chose before reading anything (#1390's rule).
65
+ assertPinnedLangVersion();
66
+ // The three emit callbacks each need the same model, and the pipeline hands
67
+ // them the results separately. Memoizing on the results array keeps one
68
+ // build per run without a mutable `let` whose assignment TypeScript cannot
69
+ // see happening inside a callback.
70
+ const models = new Map();
71
+ const modelFor = (results, naming) => {
72
+ const cached = models.get(results);
73
+ if (cached)
74
+ return cached;
75
+ const built = buildEmitModel(results, naming);
76
+ models.set(results, built);
77
+ return built;
78
+ };
79
+ const result = await generatePipeline({
80
+ fetchSchemas: () => fetchSchemas({ projectRoot: options.projectRoot, config: options.config }),
81
+ parseSchema: (typeName, data) => {
82
+ const { decls, resolved } = parseCedarSchema(data.toString("utf-8"));
83
+ // The content pin covers the schema this package ships, not a user's.
84
+ // Theirs is their input; its churn is theirs to own.
85
+ if (typeName === DEFAULT_SCHEMA_KEY) {
86
+ assertPinnedSchema(resolved, decls.map((d) => d.typeName).sort());
87
+ }
88
+ return decls;
89
+ },
90
+ createNaming: (results) => createNaming(results),
91
+ generateRegistry: (results, naming) => generateRegistry(modelFor(results, naming)),
92
+ generateTypes: (results, naming) => generateTypes(modelFor(results, naming)),
93
+ generateRuntimeIndex: (results, naming) => generateRuntimeIndex(modelFor(results, naming)),
94
+ generateExtraArtifacts: () => ({ "runtime.ts": RUNTIME_TS }),
95
+ }, options);
96
+ if (options.verbose) {
97
+ const source = resolveSchemaPath({ projectRoot: options.projectRoot, config: options.config });
98
+ console.error(`Generated ${result.resources} declaration(s) from ` +
99
+ `${source.isDefault ? "the bundled default schema" : source.path}`);
100
+ }
101
+ return result;
102
+ }
103
+ /** The files one generate run produces, keyed by filename. */
104
+ export function generatedFiles(result) {
105
+ return {
106
+ [LEXICON_JSON_FILENAME]: result.lexiconJSON,
107
+ "index.d.ts": result.typesDTS,
108
+ "index.ts": result.indexTS,
109
+ ...(result.extraArtifacts ?? {}),
110
+ };
111
+ }
112
+ /**
113
+ * Write generated files into `outDir`. With no directory given, writes where
114
+ * {@link resolveGeneratedDir} says for the current working directory.
115
+ */
116
+ export function writeGeneratedFiles(result, outDir) {
117
+ writeGeneratedArtifacts({
118
+ baseDir: outDir ?? resolveGeneratedDir(),
119
+ generatedSubdir: ".",
120
+ files: generatedFiles(result),
121
+ });
122
+ }