@intentius/chant-lexicon-cedar 0.44.14 → 0.45.0

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 (49) hide show
  1. package/README.md +13 -4
  2. package/dist/avp/client.d.ts +18 -0
  3. package/dist/avp/client.d.ts.map +1 -1
  4. package/dist/codegen/docs.d.ts +5 -9
  5. package/dist/codegen/docs.d.ts.map +1 -1
  6. package/dist/codegen/generate.d.ts +35 -8
  7. package/dist/codegen/generate.d.ts.map +1 -1
  8. package/dist/commands.d.ts +14 -0
  9. package/dist/commands.d.ts.map +1 -0
  10. package/dist/config.d.ts +38 -2
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/index.d.ts +5 -2
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/init-templates.d.ts +1 -1
  15. package/dist/integrity.json +4 -4
  16. package/dist/lint/post-synth/cede010.d.ts +5 -4
  17. package/dist/lint/post-synth/cede010.d.ts.map +1 -1
  18. package/dist/manifest.json +1 -1
  19. package/dist/plugin.d.ts.map +1 -1
  20. package/dist/rules/cede010.ts +5 -4
  21. package/dist/schema-artifact.d.ts +50 -0
  22. package/dist/schema-artifact.d.ts.map +1 -0
  23. package/dist/serializer.d.ts.map +1 -1
  24. package/dist/skills/chant-cedar-authoring.md +9 -3
  25. package/dist/spec/fetch.d.ts.map +1 -1
  26. package/package.json +2 -2
  27. package/src/avp/OWNERSHIP.md +4 -1
  28. package/src/avp/client.test.ts +35 -0
  29. package/src/avp/client.ts +29 -2
  30. package/src/codegen/docs.ts +5 -799
  31. package/src/codegen/generate-cli.ts +8 -6
  32. package/src/codegen/generate.ts +63 -14
  33. package/src/codegen/output-dir.test.ts +137 -0
  34. package/src/commands.test.ts +93 -0
  35. package/src/commands.ts +95 -0
  36. package/src/config.ts +50 -4
  37. package/src/index.ts +10 -2
  38. package/src/init-templates.test.ts +22 -4
  39. package/src/init-templates.ts +16 -16
  40. package/src/lint/post-synth/cede010.ts +5 -4
  41. package/src/plugin.ts +33 -8
  42. package/src/schema-artifact.test.ts +160 -0
  43. package/src/schema-artifact.ts +87 -0
  44. package/src/serializer.ts +17 -1
  45. package/src/skills/chant-cedar-authoring.md +9 -3
  46. package/src/spec/fetch.ts +23 -2
  47. package/dist/codegen/docs-dogwood.d.ts +0 -21
  48. package/dist/codegen/docs-dogwood.d.ts.map +0 -1
  49. package/src/codegen/docs-dogwood.ts +0 -1119
@@ -1,15 +1,11 @@
1
1
  /**
2
2
  * The cedar lexicon's Starlight site.
3
3
  *
4
- * Pages are declared as `extraPages` rather than left as hand-written files in
5
- * docs/. The pipeline rebuilds the sidebar from the pages it knows about on
6
- * every run, and Starlight does not auto-discover, so a page the config has
7
- * never heard of exists on disk and is reachable only by typing its URL
8
- * (chant #1312).
9
- *
10
- * Two more pages come out of the pipeline itself: the generated rules table
11
- * (`rules`) and the serialization reference. Both are linked from the sidebar
12
- * automatically.
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.
13
9
  *
14
10
  * Cross-namespace links are written as full `/chant/...` paths. This site's
15
11
  * base is `/chant/lexicons/cedar/`, so a bare `/guide/...` would be rewritten
@@ -21,13 +17,6 @@
21
17
  import { dirname, join } from "path";
22
18
  import { fileURLToPath } from "url";
23
19
  import { docsPipeline, writeDocsSite, type DocsConfig } from "@intentius/chant/codegen/docs";
24
- import {
25
- dogwoodEventSchemas,
26
- dogwoodOverview,
27
- dogwoodReplay,
28
- dogwoodTemporalPolicies,
29
- dogwoodValidation,
30
- } from "./docs-dogwood";
31
20
 
32
21
  const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
33
22
 
@@ -83,688 +72,6 @@ surface under the \`DWD\` id family: a policy that can depend on what already
83
72
  happened in a session. Start at [The Dogwood Dialect](./dogwood), which is
84
73
  honest about upstream's governance before it shows you a builder.`;
85
74
 
86
- // ── getting-started ───────────────────────────────────────────────
87
-
88
- const gettingStarted = `Three steps, in this order. The second one is the one people skip.
89
-
90
- ## 1. Scaffold
91
-
92
- \`\`\`bash
93
- npx chant init --lexicon cedar --template default my-authz
94
- cd my-authz && npm install
95
- \`\`\`
96
-
97
- Three templates ship: \`default\` (a permit/forbid pair), \`avp-embedding\` (a
98
- multi-tenant store bound for Verified Permissions), and \`gateway-policy-set\`
99
- (API routes as entities, behind a deny floor). Each writes a
100
- \`schema.cedarschema\` at the project root and a \`src/policies.ts\` typed
101
- against it.
102
-
103
- ## 2. Generate
104
-
105
- \`\`\`bash
106
- npx chant generate --lexicon cedar
107
- \`\`\`
108
-
109
- This is not optional and it is not a one-time setup step. The classes and
110
- action constants \`policies.ts\` imports **do not exist** until generate has read
111
- your schema. Re-run it whenever the schema changes.
112
-
113
- What it produces, per declaration:
114
-
115
- | Schema | Generated |
116
- |---|---|
117
- | \`entity Document in [Folder] = { … }\` | \`Document\`, \`DocumentAttributes\`, \`DocumentUid\` |
118
- | \`action read appliesTo { … }\` | \`ReadAction\`, \`ReadContext\` |
119
- | — | \`Policy\`, \`EntityTypeName\`, \`ActionUid\`, \`PolicyScope\`, \`ALL_ACTIONS\`, \`ALL_ENTITY_TYPES\` |
120
-
121
- ## 3. Build
122
-
123
- \`\`\`bash
124
- npx chant build
125
- \`\`\`
126
-
127
- Both artifacts land in \`dist/\`. Every emitted policy set is validated against
128
- your schema by \`cedar-wasm\` — the real Cedar validator, running in-process.
129
- No CLI on PATH, no Docker.
130
-
131
- ## Then
132
-
133
- \`\`\`bash
134
- npx chant coverage --lexicon cedar # is every schema declaration generated?
135
- \`\`\`
136
-
137
- The MCP tool \`cedar:coverage\` answers the harder question — which schema
138
- declarations the *policy set* actually reaches. See [Lint Rules](../lint-rules/).
139
-
140
- ## The example projects
141
-
142
- Two ship with the lexicon:
143
-
144
- - \`examples/getting-started\` — one permit, one forbid, against the bundled default schema.
145
- - \`examples/basic-policies\` — a project-local \`schema.cedarschema\` and a four-policy set.
146
-
147
- Both are exercised in CI: the emitted \`.cedar\` text is handed straight to
148
- \`cedar-wasm\`, so a policy set chant is happy with but Cedar rejects fails the
149
- build rather than the deploy.
150
- `;
151
-
152
- // ── schema ────────────────────────────────────────────────────────
153
-
154
- const schemaPage = `Unlike every other lexicon, cedar's interesting spec is not one global upstream. It is **your** schema.
155
-
156
- The pinned upstream here is the Cedar *grammar* — \`@cedar-policy/cedar-wasm\`,
157
- whose language version is asserted before anything is emitted. The schema is
158
- the input.
159
-
160
- ## Where it is looked for
161
-
162
- 1. \`cedar.schema\` in \`chant.config.ts\`
163
- 2. \`schema.cedarschema\` in the project root
164
- 3. the schema bundled with the lexicon
165
-
166
- Step 3 exists so \`generate()\` has something to read in a fresh clone — a
167
- generate step that only works after the user does something is a generate step
168
- nothing gates. It is a real application-authorization model, not a stub.
169
-
170
- **Turn it off once you have your own:**
171
-
172
- \`\`\`typescript
173
- // chant.config.ts
174
- import type { ChantConfig } from "@intentius/chant";
175
- import "@intentius/chant-lexicon-cedar";
176
-
177
- export default {
178
- lexicons: ["cedar"],
179
- cedar: {
180
- schema: "authz/app.cedarschema",
181
- validation: {
182
- mode: "strict",
183
- warnings: "warn",
184
- requireProjectSchema: true,
185
- },
186
- },
187
- } satisfies ChantConfig;
188
- \`\`\`
189
-
190
- Without \`requireProjectSchema\`, a typo in the path is the difference between
191
- "your entity types" and "the bundled default" — and the build succeeds either
192
- way.
193
-
194
- ## Writing one
195
-
196
- \`\`\`
197
- namespace App {
198
- type Level = Long;
199
- type TagSet = Set<String>;
200
-
201
- entity Group = { "name": String };
202
-
203
- entity User in [Group] = {
204
- "email": String,
205
- "level": Level,
206
- "manager"?: User,
207
- "roles": TagSet,
208
- };
209
-
210
- entity Document = {
211
- "title": String,
212
- "owner": User,
213
- "classification": String,
214
- };
215
-
216
- action read, write appliesTo {
217
- principal: [User],
218
- resource: [Document],
219
- context: { "mfa": Bool }
220
- };
221
- }
222
- \`\`\`
223
-
224
- Common types (\`type Level = Long\`) are resolved away by the codegen — real
225
- schemas use them, so the resolver collapses them rather than pretending they
226
- are absent.
227
-
228
- ## Config keys
229
-
230
- | Key | Meaning |
231
- |-----|---------|
232
- | \`schema\` | Path to the \`.cedarschema\`, relative to the project root |
233
- | \`validation.mode\` | \`"strict"\`. The only mode cedar-wasm 4.12 accepts |
234
- | \`validation.warnings\` | \`"ignore"\`, \`"warn"\`, or \`"error"\` — the validator reports "policy is impossible" here, separately from errors |
235
- | \`validation.requireProjectSchema\` | Refuse to fall back to the bundled default |
236
-
237
- The namespace is a \`strictObject\`, nested levels included: a typo inside
238
- \`validation\` is a config error, not a silently ignored key.
239
-
240
- ## Both syntaxes
241
-
242
- Cedar's schema grammar has a human-readable form and a JSON form. The codegen
243
- consumes the human-readable one; \`cedar-wasm\` converts between them
244
- (\`schemaToJson\`, \`schemaToText\`) if you need the other.
245
-
246
- ## The pin
247
-
248
- The language version is pinned separately from the package version. A package
249
- bump that leaves the language at 4.x cannot change what parses, so
250
- \`CEDAR_WASM_VERSION\` is what the self-upgrade tooling moves and
251
- \`CEDAR_LANG_VERSION\` is what \`generate()\` asserts at runtime. Both live in
252
- \`src/spec/pin.ts\`, beside a content pin over the bundled default schema.
253
- `;
254
-
255
- // ── resources ─────────────────────────────────────────────────────
256
-
257
- const resourcesPage = `Everything below \`Policy\` is generated from your schema, so the exact list depends on it. What follows is the shape.
258
-
259
- ## Policy
260
-
261
- The one declaration this lexicon serializes. One \`Policy\` per policy.
262
-
263
- \`\`\`typescript
264
- import { Policy } from "@intentius/chant-lexicon-cedar";
265
-
266
- export const ownerRead = new Policy({ /* … */ });
267
- \`\`\`
268
-
269
- Its props are covered in [Policies](../policies/).
270
-
271
- ## Entity classes
272
-
273
- Each \`entity T = { … }\` in the schema produces three things:
274
-
275
- | Generated | Kind | What it is |
276
- |---|---|---|
277
- | \`Document\` | resource class | The entity type, for declaring entities |
278
- | \`DocumentAttributes\` | property class | Its attribute record, usable standalone |
279
- | \`DocumentUid\` | type | \`` + "`" + `App::Document::"\${string}"` + "`" + `\` — a template-literal type |
280
-
281
- \`DocumentUid\` is the one that earns its keep. It makes a mistyped namespace a
282
- compile error:
283
-
284
- \`\`\`typescript
285
- import type { DocumentUid } from "@intentius/chant-lexicon-cedar";
286
-
287
- const contract: DocumentUid = 'App::Document::"contract-2026"'; // ok
288
- const typo: DocumentUid = 'App::Docmnt::"contract-2026"'; // compile error
289
- \`\`\`
290
-
291
- ## Action constants
292
-
293
- Each action produces a \`const\` (not a class — an action UID is a value, not a
294
- constructor) and a context record type:
295
-
296
- \`\`\`typescript
297
- import { ReadAction, type ReadContextProps } from "@intentius/chant-lexicon-cedar";
298
-
299
- // ReadAction === 'App::Action::"read"'
300
- \`\`\`
301
-
302
- ## Schema-wide types
303
-
304
- | Name | What it holds |
305
- |---|---|
306
- | \`EntityTypeName\` | Union of every entity type name, as Cedar writes it |
307
- | \`EntityUid\` | Union of every entity UID type |
308
- | \`ActionUid\` | Union of every action UID |
309
- | \`PolicyRef\` | \`EntityUid \\| ActionUid\` — anything a scope may name |
310
- | \`PolicyScope\` | One scope position |
311
- | \`ALL_ACTIONS\` | Every action constant, for exhaustive iteration |
312
- | \`ALL_ENTITY_TYPES\` | Every entity type name |
313
-
314
- \`ALL_ACTIONS\` is what a cross-domain post-synth check iterates when asking "is
315
- any action uncovered".
316
-
317
- ## Naming
318
-
319
- Names are derived from the schema and de-duplicated against a single pool, so a
320
- schema declaring an entity type literally called \`UserAttributes\` beside a
321
- \`User\` does not get its name stolen by the derived record type. Two
322
- declarations in the same namespace reducing to the same short name are both
323
- qualified rather than one silently overwriting the other.
324
-
325
- ## Coverage
326
-
327
- \`\`\`bash
328
- npx chant coverage --lexicon cedar --verbose
329
- \`\`\`
330
-
331
- Reports whether every entity type and every action in the schema is reachable
332
- from the generated artifacts. A gap is a defect: an action with no generated
333
- constant is one a policy can only name as a hand-typed string, which is the
334
- failure mode this lexicon exists to remove.
335
- `;
336
-
337
- // ── policies ──────────────────────────────────────────────────────
338
-
339
- const policiesPage = `A policy is a \`Policy\` value. The serializer turns each one into a \`.cedar\` block and an entry in the JSON policy set.
340
-
341
- ## Props
342
-
343
- | Prop | Meaning |
344
- |------|---------|
345
- | \`effect\` | \`"permit"\` or \`"forbid"\`. Defaults to \`permit\` |
346
- | \`principal\`, \`action\`, \`resource\` | Scope constraints. Omit for unconstrained |
347
- | \`when\` | Cedar expression strings, one \`when { … }\` clause each |
348
- | \`unless\` | Cedar expression strings, one \`unless { … }\` clause each |
349
- | \`annotations\` | \`Record<string, string>\`, emitted as \`@key("value")\` |
350
-
351
- ## Scope forms
352
-
353
- The scope type mirrors the grammar exactly:
354
-
355
- | Written | Emitted |
356
- |---|---|
357
- | \`{}\` or omitted | \`principal\` |
358
- | \`{ eq: X }\` | \`principal == X\` |
359
- | \`{ in: X }\` | \`principal in X\` |
360
- | \`{ in: [X, Y] }\` | \`principal in [X, Y]\` |
361
- | \`{ is: "App::User" }\` | \`principal is App::User\` |
362
- | \`{ is: "App::User", in: X }\` | \`principal is App::User in X\` |
363
-
364
- \`is\` takes an \`EntityTypeName\`; \`eq\` and \`in\` take a \`PolicyRef\`. Both are
365
- schema-derived unions, so an entity type the schema never declared does not
366
- compile.
367
-
368
- ## Conditions
369
-
370
- \`when\` and \`unless\` carry Cedar expression **text**:
371
-
372
- \`\`\`typescript
373
- export const ownerWrite = new Policy({
374
- effect: "permit",
375
- principal: { is: "App::User" },
376
- action: { eq: WriteAction },
377
- resource: { is: "App::Document" },
378
- when: ["resource.owner == principal", "context.mfa == true"],
379
- unless: ['resource.classification == "confidential"'],
380
- });
381
- \`\`\`
382
-
383
- Each array element becomes its own clause. They stay strings on purpose:
384
- Cedar's expression grammar *is* the policy language, and typing it in
385
- TypeScript is [import and reconcile](../importing/)'s problem, not codegen's.
386
-
387
- ## Policy ids
388
-
389
- The id comes from the export's logical name, kebab-cased — \`allowAdminRead\`
390
- becomes \`@id("allow-admin-read")\`. Set \`annotations.id\` to pin one:
391
-
392
- \`\`\`typescript
393
- export const anything = new Policy({
394
- annotations: { id: "tenant-isolation", owner: "platform" },
395
- // …
396
- });
397
- \`\`\`
398
-
399
- An explicit \`id\` wins. \`@id\` is emitted first, then your annotations in
400
- declaration order.
401
-
402
- ## permit and forbid
403
-
404
- Cedar is default-deny, so a \`permit\` is what grants anything at all. A
405
- \`forbid\` is not the absence of a grant — it beats every \`permit\` in the set
406
- unconditionally, which makes it the only construct that survives a wider grant
407
- somebody adds next quarter.
408
-
409
- Two consequences worth internalizing:
410
-
411
- - **A forbid with no guard denies everything**, and no permit can lift it. The
412
- \`DenyByDefaultSet\` [composite](../composites/) throws rather than emit one.
413
- - **"Nobody wrote a permit" and "a forbid says no" evaluate identically** and
414
- read completely differently in review. Give sensitive resources an explicit
415
- floor.
416
-
417
- ## The JSON companion
418
-
419
- Every build writes \`policies.cedar.json\` beside the \`.cedar\` text — the same
420
- policy set in Cedar's JSON policy format, produced from the same structured
421
- model rather than by re-parsing the text.
422
-
423
- One deliberate gap: condition bodies are expression *ASTs* in that format, and
424
- the model carries expression text. They are written as \`{ "__expr": "<text>" }\`,
425
- Cedar's own escape for source-given expressions, so the file stays
426
- machine-readable instead of inventing a private key. Producing real trees means
427
- parsing Cedar expression text, which lands with import.
428
- `;
429
-
430
- // ── composites ────────────────────────────────────────────────────
431
-
432
- const compositesPage = `Cedar has no functions, no modules, and no loops, so every repeated policy shape is copy-paste in \`.cedar\` text. A TypeScript factory is the only place the abstraction can live.
433
-
434
- These follow chant's general
435
- [composite resources](/chant/guide/composite-resources/) pattern: a function
436
- returning declared resources, discovered like any other export.
437
-
438
- \`\`\`typescript
439
- import { OwnerCanManage, DenyByDefaultSet } from "@intentius/chant-lexicon-cedar";
440
- \`\`\`
441
-
442
- ## OwnerCanManage
443
-
444
- "The owner of a thing may act on it" — the most-repeated shape in any policy
445
- set, and where the two classic mistakes get made: the \`when\` guard names an
446
- attribute the schema spells differently, or the grant is written wide and the
447
- scoping clause is forgotten.
448
-
449
- \`\`\`typescript
450
- import { ReadAction, WriteAction } from "@intentius/chant-lexicon-cedar";
451
-
452
- export const docOwner = OwnerCanManage({
453
- entityType: "App::Document",
454
- actions: [ReadAction, WriteAction],
455
- principal: "App::User",
456
- });
457
- \`\`\`
458
-
459
- Emits:
460
-
461
- \`\`\`cedar
462
- @id("doc-owner")
463
- @composite("OwnerCanManage")
464
- @scopedTo("App::Document")
465
- permit (
466
- principal is App::User,
467
- action in [App::Action::"read", App::Action::"write"],
468
- resource is App::Document
469
- )
470
- when { resource.owner == principal };
471
- \`\`\`
472
-
473
- | Option | Default | Notes |
474
- |---|---|---|
475
- | \`entityType\` | required | \`EntityTypeName\` — schema-checked |
476
- | \`actions\` | unconstrained | One action emits \`==\`, several emit \`in [ … ]\` |
477
- | \`ownerAttribute\` | \`"owner"\` | The attribute holding the owner |
478
- | \`principal\` | unconstrained | A bare type string becomes \`is T\`; a full scope passes through |
479
- | \`when\` | — | Appended after the ownership test |
480
- | \`unless\` | — | Omitted entirely when not asked for |
481
- | \`annotations\` | — | Merged over the generated ones; an explicit \`id\` wins |
482
-
483
- Leaving \`actions\` off produces a wide grant, so it has to be asked for by
484
- omitting the field rather than arriving by accident.
485
-
486
- ## DenyByDefaultSet
487
-
488
- A guarded \`forbid\` and the permits it governs, returned from one call. The
489
- pattern teams write by hand is a forbid at the top of a file and a pile of
490
- permits under it with nothing tying the two together — delete the forbid and
491
- the permits keep working, wider than anyone intended.
492
-
493
- \`\`\`typescript
494
- import { DeleteAction } from "@intentius/chant-lexicon-cedar";
495
-
496
- const guarded = DenyByDefaultSet({
497
- policies: [docOwner],
498
- entityType: "App::Document",
499
- actions: DeleteAction,
500
- when: ['resource.classification == "confidential"'],
501
- unless: ['principal == App::User::"archivist"'],
502
- });
503
-
504
- export const confidentialFloor = guarded.floor;
505
- export const documentOwnerGrant = guarded.members[0];
506
- \`\`\`
507
-
508
- | Returned | What it is |
509
- |---|---|
510
- | \`floor\` | The \`forbid\` policy |
511
- | \`members\` | The permits, unchanged, in the order given |
512
- | \`all\` | \`[floor, ...members]\` |
513
-
514
- \`when\` is required. An unguarded forbid overrides every permit in the set, so
515
- the result would authorize nothing — the composite throws rather than emit it.
516
-
517
- ## Where composites go in a build
518
-
519
- They return \`Declarable\` values like any other resource, so exporting them
520
- from a discovered file is all that is needed:
521
-
522
- \`\`\`typescript
523
- export const [floor, grant] = DenyByDefaultSet({ /* … */ }).all;
524
- \`\`\`
525
-
526
- The floor is emitted first. Cedar's evaluation is order-independent — a forbid
527
- wins wherever it sits — but a file that reads floor-first matches how the set
528
- is reasoned about.
529
- `;
530
-
531
- // ── lint-rules ────────────────────────────────────────────────────
532
-
533
- const lintRulesPage = `Rules under the \`CED\` prefix. The complete generated table is on [All Rules](../rules/); this page is the reasoning.
534
-
535
- ## The two engines, and why there is only one
536
-
537
- Cedar's own validator runs in-process through \`@cedar-policy/cedar-wasm\` — the
538
- real thing, not a reimplementation. It answers "is this policy well-formed
539
- against this schema".
540
-
541
- Everything else is a TypeScript check. There is no \`cedarGate()\`, because
542
- organizational policy in chant is post-synth checks and a second policy engine
543
- would duplicate the lint engine.
544
-
545
- ## The bare-permit wall
546
-
547
- \`\`\`cedar
548
- permit (principal, action, resource);
549
- \`\`\`
550
-
551
- Every scope unconstrained, no conditions. Legal Cedar, validates clean, grants
552
- everything to everyone. The rule is **env-aware**:
553
-
554
- | Environment | Verdict |
555
- |---|---|
556
- | dev / local | warn |
557
- | staging | warn |
558
- | prod | **fail** |
559
-
560
- Env-aware rather than absolute because an absolute rule gets suppressed the
561
- first time it fires during development, and a suppressed rule protects nothing.
562
- A permit with any constrained scope position, or any \`when\`/\`unless\`, is not
563
- bare.
564
-
565
- ## Schema-absent references
566
-
567
- A policy naming an entity type or action the schema does not declare fails
568
- **everywhere**, dev included. Two failure modes hide behind it:
569
-
570
- - **The typo** — \`App::Documnt\`. Generated classes make this a compile error
571
- before any check runs; it survives only inside \`when\`/\`unless\` expression
572
- strings.
573
- - **The silent no-op** — a scope naming an absent entity type *parses*, and
574
- Cedar's request-envelope resolver answers "success, nothing". The policy is
575
- well-formed, deployable, and can never fire.
576
-
577
- ## Cross-domain checks
578
-
579
- The differentiator, and the thing no Cedar tool can express: Cedar only ever
580
- sees policies, while a chant build holds the policies *and* the infrastructure
581
- they govern in one entity graph. So a post-synth check can span both:
582
-
583
- - an entity id in a policy references an actual resource declaration
584
- - every declared bucket is covered by at least one \`forbid\`
585
- - no schema action lacks any policy
586
-
587
- These are ordinary post-synth checks. Nothing exotic — they just need both
588
- halves in the same build, which is the arrangement chant already has.
589
-
590
- ## Coverage, two ways
591
-
592
- \`\`\`bash
593
- npx chant coverage --lexicon cedar # schema -> generated artifacts
594
- \`\`\`
595
-
596
- The MCP tool asks the other half:
597
-
598
- \`\`\`
599
- cedar:coverage { "path": ".", "format": "text" }
600
- \`\`\`
601
-
602
- It builds the policy set, hands each policy to Cedar's own
603
- \`getValidRequestEnvsPolicy\` alongside the schema, and reports:
604
-
605
- | Field | Meaning |
606
- |---|---|
607
- | \`uncovered\` | Declarations no policy can apply to |
608
- | \`forbidOnly\` | Declarations reachable only from a \`forbid\` — nothing grants them |
609
- | \`inert\` | Policies whose request envelope is empty; they can never fire |
610
- | \`unresolved\` | Policies the resolver rejected outright |
611
- | \`parseErrors\` | Why the set would not split into policies |
612
-
613
- Container entity types — the ones that appear in no action's \`appliesTo\` and
614
- exist only to be \`in\` — show as uncovered even under a bare permit. That is
615
- the resolver telling the truth: no request can name them.
616
-
617
- ## What is not here yet
618
-
619
- The post-synth checks encoding all of the above are
620
- [INTENTIUS/chant#1651](https://github.com/INTENTIUS/chant/issues/1651). Today
621
- the lexicon ships the source-level rule set and the validation plumbing; the
622
- checks that span policies and estate land there.
623
- `;
624
-
625
- // ── importing ─────────────────────────────────────────────────────
626
-
627
- const importingPage = `Bringing an existing Cedar policy set into typed source, and pulling console edits back.
628
-
629
- ## The round trip
630
-
631
- \`\`\`
632
- .cedar text -> JSON policy format -> TypeScript -> .cedar text
633
- \`\`\`
634
-
635
- The JSON policy format is the parse source, not the \`.cedar\` text. That is why
636
- the serializer produces it from the same structured model rather than by
637
- re-parsing what it just wrote: the two views cannot drift, and the import path
638
- reads a format with an actual grammar rather than doing a second parse of the
639
- surface syntax.
640
-
641
- \`cedar-wasm\` converts in both directions — \`policyToJson\`, \`policyToText\`,
642
- \`policySetTextToParts\` — so a set that exists only as \`.cedar\` text is one
643
- call away from the importable form.
644
-
645
- ## What round-tripping has to preserve
646
-
647
- | Carried | Notes |
648
- |---|---|
649
- | Effect | \`permit\` / \`forbid\` |
650
- | All three scope positions | Including \`is T in E\` |
651
- | \`when\` / \`unless\` clauses | In order |
652
- | Annotations | Including \`@id\`, which becomes the export name's override |
653
-
654
- The one asymmetry today is condition bodies. The JSON policy format wants an
655
- expression *tree*; the model carries expression *text*, written as
656
- \`{ "__expr": "…" }\` — Cedar's own escape for a source-given expression.
657
- Producing real trees means parsing Cedar expression text, which is precisely
658
- the work the import path has to do anyway.
659
-
660
- ## Reconcile
661
-
662
- The interesting case is not the initial import. It is the policy somebody edited
663
- in a console: a \`ReconcileOp\` pulls it back into source, and the diff is
664
- reviewable.
665
-
666
- An **ambient permit** — one found in a policy store that no source file declares
667
- — is not housekeeping. It is a standing grant somebody made outside review, and
668
- it is a security finding.
669
-
670
- ## Ownership
671
-
672
- AVP policy *stores* are taggable; individual policies are not. The ownership
673
- channel is store-scoped until finer granularity is proven, and no channel is
674
- declared until its read paths are implemented — \`chant dev check-lexicon\` has
675
- a tier-2 gate for exactly the failure of declaring a marker channel on a path
676
- the plugin does not implement.
677
-
678
- ## Status
679
-
680
- The JSON policy-format parser, the TypeScript generator, and the
681
- \`ReconcileOp\` example are
682
- [INTENTIUS/chant#1653](https://github.com/INTENTIUS/chant/issues/1653). The
683
- serializer already emits the format they read, which is why it exists as a
684
- first-class output rather than a debugging aid.
685
- `;
686
-
687
- // ── avp ───────────────────────────────────────────────────────────
688
-
689
- const avpPage = `Amazon Verified Permissions is one deployment vehicle for Cedar. It is not the only one, and the lexicon does not privilege it.
690
-
691
- ## The seam
692
-
693
- chant already ships the deployment half. \`AWS::VerifiedPermissions::Policy\` in
694
- the [aws lexicon](/chant/lexicons/aws/) carries its policy text in
695
- \`definition.static.statement\`, typed \`CedarPolicy\` — which is to say,
696
- \`string\`. That string is the seam.
697
-
698
- \`\`\`
699
- cedar lexicon aws lexicon
700
- schema -> Policy -> .cedar text -> VerifiedPermissionsPolicy.definition.static.statement
701
- \`\`\`
702
-
703
- Everything upstream of the string — the schema, entity types, actions, scope
704
- constraints — belongs to this lexicon. Everything downstream — the policy
705
- store, the CloudFormation \`ApplyOp\`, the IAM around it — belongs to the aws
706
- lexicon and already works.
707
-
708
- ## What ships today
709
-
710
- A policy's statement text is what the serializer emits for that one entity —
711
- the same bytes as the \`.cedar\` file, so the deployed policy and the reviewed
712
- file cannot disagree.
713
-
714
- \`\`\`typescript
715
- import { Policy, ReadAction } from "@intentius/chant-lexicon-cedar";
716
-
717
- export const ownerRead = new Policy({
718
- effect: "permit",
719
- principal: { is: "App::User" },
720
- action: { eq: ReadAction },
721
- resource: { is: "App::Document" },
722
- when: ["resource.owner == principal"],
723
- });
724
- \`\`\`
725
-
726
- The \`avp-embedding\` init template scaffolds a multi-tenant store's schema and
727
- a three-policy set shaped for one.
728
-
729
- ## What is deferred
730
-
731
- A typed handoff — a \`VerifiedPermissionsPolicy\` whose \`statement\` accepts a
732
- \`Policy\` value directly rather than a string a caller assembled — is landing
733
- with [INTENTIUS/chant#1652](https://github.com/INTENTIUS/chant/issues/1652),
734
- along with \`describeResources()\`/\`observeAmbient()\` against a live policy
735
- store and the ownership-channel design.
736
-
737
- Until it does:
738
-
739
- - **Do not hand-type a statement string.** A prose
740
- \`"permit(principal, action, resource);"\` inside an AVP resource is exactly
741
- what this lexicon exists to remove, and the bare-permit wall fails it in a
742
- prod build.
743
- - **Do not tag individual policies for ownership.** Stores are taggable;
744
- policies are not.
745
- - **Treat an ambient permit as a finding.** A permit in a store that no source
746
- file declares is a standing grant made outside review.
747
-
748
- ## The other evaluators
749
-
750
- If the target is not AVP there is no embedding step at all — emit the files and
751
- ship them.
752
-
753
- | Target | How |
754
- |---|---|
755
- | cedar-agent | Point it at the emitted \`.cedar\` and an entity store |
756
- | Embedded \`cedar-wasm\` | Load the policy text in-process, call \`isAuthorized\` |
757
- | Edge / Cloudflare-style | The same file, read at the edge |
758
-
759
- ## Cedar for Kubernetes
760
-
761
- The CNCF push includes Cedar as a Kubernetes authorizer with policies as CRDs.
762
- Those kinds belong to the [k8s lexicon](/chant/lexicons/k8s/)'s CRD sources —
763
- the same rule that kept \`helm.cattle.io\` out of k3s. What this lexicon does
764
- there is lint the policy text embedded in those kinds, the pattern the ARGO
765
- rules already use.
766
- `;
767
-
768
75
  // ── Output format, for the generated serialization page ───────────
769
76
 
770
77
  const outputFormat = `The cedar lexicon emits two views of one policy set.
@@ -823,107 +130,6 @@ export async function generateDocs(options?: { verbose?: boolean }): Promise<voi
823
130
  // `App::Action::"read"`, so the first segment is the namespace — which is
824
131
  // the only grouping a Cedar schema has.
825
132
  serviceFromType: (type: string) => type.split("::")[0] ?? type,
826
- extraPages: [
827
- {
828
- slug: "getting-started",
829
- title: "Getting Started",
830
- description: "Scaffold, generate, build — in that order.",
831
- content: gettingStarted,
832
- },
833
- {
834
- slug: "schema",
835
- title: "Schema",
836
- description: "The .cedarschema this lexicon's codegen reads, and how it is resolved.",
837
- content: schemaPage,
838
- },
839
- {
840
- slug: "resources",
841
- title: "Resources",
842
- description: "Policy, and the entity and action declarations generated from your schema.",
843
- content: resourcesPage,
844
- },
845
- {
846
- slug: "policies",
847
- title: "Policies",
848
- description: "Policy props, scope forms, conditions, annotations, and ids.",
849
- content: policiesPage,
850
- },
851
- {
852
- slug: "composites",
853
- title: "Composites",
854
- description: "OwnerCanManage and DenyByDefaultSet — the shapes Cedar has nowhere to put.",
855
- content: compositesPage,
856
- },
857
- {
858
- slug: "lint-rules",
859
- title: "Lint Rules",
860
- description: "The bare-permit wall, schema-absent references, and cross-domain checks.",
861
- content: lintRulesPage,
862
- },
863
- {
864
- slug: "importing",
865
- title: "Importing",
866
- description: "The JSON policy format round trip, reconcile, and ownership.",
867
- content: importingPage,
868
- },
869
- {
870
- slug: "avp",
871
- title: "Verified Permissions",
872
- description: "The AVP statement seam, and the other Cedar evaluators.",
873
- content: avpPage,
874
- },
875
- // The dogwood dialect (#1662). `sidebar: false` on each, because these
876
- // five belong under one group rather than flat after the cedar pages —
877
- // the group is declared in `sidebarExtra` below, and `buildSidebar`
878
- // would otherwise list them twice.
879
- {
880
- slug: "dogwood",
881
- title: "The Dogwood Dialect",
882
- description: "Cedar with temporal operators — what ships, and what pre-release means here.",
883
- content: dogwoodOverview,
884
- sidebar: false,
885
- },
886
- {
887
- slug: "dogwood-temporal-policies",
888
- title: "Temporal Policies",
889
- description: "The typed builders, the parser primitives, and which operators are really macros.",
890
- content: dogwoodTemporalPolicies,
891
- sidebar: false,
892
- },
893
- {
894
- slug: "dogwood-event-schemas",
895
- title: "Event Schemas",
896
- description: "The .dwschema surface, the callerPrincipal pin, and what opting out of it widens.",
897
- content: dogwoodEventSchemas,
898
- sidebar: false,
899
- },
900
- {
901
- slug: "dogwood-validation",
902
- title: "Dogwood Validation",
903
- description: "The DWD walls that always run, and the CLI-gated checks that need the binary.",
904
- content: dogwoodValidation,
905
- sidebar: false,
906
- },
907
- {
908
- slug: "dogwood-replay",
909
- title: "Replay",
910
- description: "Typed traces, PolicyReplayOp, and the both-bags trap that makes half a trace pass.",
911
- content: dogwoodReplay,
912
- sidebar: false,
913
- },
914
- ],
915
- sidebarExtra: [
916
- {
917
- label: "Dogwood",
918
- items: [
919
- { label: "The Dialect", slug: "dogwood" },
920
- { label: "Temporal Policies", slug: "dogwood-temporal-policies" },
921
- { label: "Event Schemas", slug: "dogwood-event-schemas" },
922
- { label: "Validation", slug: "dogwood-validation" },
923
- { label: "Replay", slug: "dogwood-replay" },
924
- ],
925
- },
926
- ],
927
133
  };
928
134
 
929
135
  const result = docsPipeline(config);