@intentius/chant-lexicon-cedar 0.124.30 → 0.124.32

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,203 @@
1
+ /**
2
+ * Cedar lexicon serializer.
3
+ *
4
+ * Emits two views of the same policy set:
5
+ *
6
+ * - `.cedar` policy text — the primary output, and the surface every Cedar
7
+ * evaluator reads (AVP, cedar-agent, an embedded `cedar-wasm`).
8
+ * - the Cedar JSON policy format — a second file, written alongside, and the
9
+ * parse source for import (#1653). It is produced by handing the emitted
10
+ * `.cedar` text back to `cedar-wasm`, so what lands on disk is whatever
11
+ * Cedar itself says that text means. Deriving it from the in-memory model
12
+ * instead is what shipped first, and it wrote `when` bodies as
13
+ * `{ "__expr": "<text>" }` — a key Cedar's JSON grammar does not have, which
14
+ * every Cedar tool rejects with `unknown variant '__expr'`.
15
+ *
16
+ * The entity model this serializer reads is hand-shaped for now: one
17
+ * `Cedar::Policy` entity type whose `props` carry an effect, three scope
18
+ * constraints, and optional `when`/`unless` guards. Schema-driven codegen
19
+ * (#1650) generates typed classes onto exactly this shape, so nothing here
20
+ * changes when it lands — the guards become typed expressions rather than
21
+ * the opaque Cedar-expression strings they are today.
22
+ *
23
+ * A third view arrives when the build holds dogwood entities (#1658): `.dw`
24
+ * policy text and its `.dwschema`/`macros.dw` companions, rendered by
25
+ * `./dogwood/serialize.ts` and returned in the same result. Both legs share
26
+ * `./policy-text.ts` rather than a copy of it, because a `.dw` policy's head
27
+ * *is* a Cedar policy's head.
28
+ */
29
+ import { isPropertyDeclarable } from "@intentius/chant/declarable";
30
+ import { walkValue } from "@intentius/chant/serializer-walker";
31
+ import { policyToJson, splitPolicySet, templateToJson } from "./spec/wasm.js";
32
+ import { serializeDogwood } from "./dogwood/serialize.js";
33
+ import { schemaEntities } from "./schema-artifact.js";
34
+ import { conditionStrings, getProps, renderPolicyHead, resolvePolicyId, } from "./policy-text.js";
35
+ // ── The policy entity model ───────────────────────────────────────
36
+ /** The `entityType` this serializer reads. */
37
+ export const CEDAR_POLICY_TYPE = "Cedar::Policy";
38
+ /** Filename of the JSON policy-set companion to the primary `.cedar` output. */
39
+ export const CEDAR_JSON_FILENAME = "policies.cedar.json";
40
+ // ── Helpers ───────────────────────────────────────────────────────
41
+ //
42
+ // Rendering the dogwood dialect shares lives in `./policy-text.ts`, along with
43
+ // `resolvePolicyId` — the one rule linking a chant entity to a policy id, which
44
+ // #1652 extracted for the AVP observation and the `.dw` leg needs for the same
45
+ // reason. Re-exported here so this module stays the one import a caller needs.
46
+ export { conditionStrings, escapeCedarString, getProps, isRecord, policyIdFromLogicalName, renderPolicyHead, renderScope, resolvePolicyId, } from "./policy-text.js";
47
+ // ── Walker visitor ────────────────────────────────────────────────
48
+ /**
49
+ * Cedar has no intrinsic function syntax, so references collapse to the text
50
+ * a policy would name them by. Property names pass through verbatim.
51
+ */
52
+ const cedarVisitor = {
53
+ attrRef: (logicalName, attribute) => `${logicalName}.${attribute}`,
54
+ resourceRef: (logicalName) => logicalName,
55
+ propertyDeclarable: (entity, walk) => {
56
+ const out = {};
57
+ for (const [key, value] of Object.entries(getProps(entity))) {
58
+ if (key === "entityType" || key === "lexicon")
59
+ continue;
60
+ if (value === undefined)
61
+ continue;
62
+ out[key] = walk(value);
63
+ }
64
+ return out;
65
+ },
66
+ };
67
+ // ── Cedar policy text ─────────────────────────────────────────────
68
+ /**
69
+ * One policy, as `.cedar` text.
70
+ *
71
+ * Exported because the AVP embedding (#1652) needs exactly this string and
72
+ * nothing else — `AWS::VerifiedPermissions::Policy` carries its policy as
73
+ * `Definition.Static.Statement`, a single Cedar policy rather than a set. Two
74
+ * renderers would be two dialects; there is one.
75
+ *
76
+ * The head comes from `renderPolicyHead`, which the dogwood dialect's `.dw`
77
+ * leg also calls: three surfaces, one rendering of an annotation and a scope.
78
+ */
79
+ export function renderPolicyText(id, props) {
80
+ const lines = renderPolicyHead(id, props);
81
+ for (const clause of conditionStrings(props.when)) {
82
+ lines.push(`when { ${clause} }`);
83
+ }
84
+ for (const clause of conditionStrings(props.unless)) {
85
+ lines.push(`unless { ${clause} }`);
86
+ }
87
+ return lines.join("\n") + ";";
88
+ }
89
+ /**
90
+ * Build the JSON companion from the emitted `.cedar` text.
91
+ *
92
+ * Two things fall out of going through the module rather than around it. The
93
+ * condition bodies become real expression trees, which is the whole reason the
94
+ * file is worth writing; and a policy carrying a `?principal`/`?resource` slot
95
+ * lands under `templates` rather than `staticPolicies`, because Cedar — not
96
+ * this serializer — is what decides which one it is.
97
+ *
98
+ * Ids come from each policy's own `@id`, not from the emission order, so the
99
+ * keys here cannot drift from the annotations in the text beside them.
100
+ */
101
+ export function policySetJSON(text) {
102
+ const parts = splitPolicySet(text);
103
+ if (!parts.ok)
104
+ return { error: parts.error };
105
+ const doc = { staticPolicies: {}, templates: {}, templateLinks: [] };
106
+ const collect = (sources, convert, into, fallbackPrefix) => {
107
+ for (const [index, source] of sources.entries()) {
108
+ const converted = convert(source);
109
+ if (!converted.ok)
110
+ return converted.error;
111
+ const id = converted.value.annotations?.id;
112
+ into[typeof id === "string" && id.length > 0 ? id : `${fallbackPrefix}${index}`] = converted.value;
113
+ }
114
+ return undefined;
115
+ };
116
+ const policyError = collect(parts.value.policies, policyToJson, doc.staticPolicies, "policy");
117
+ if (policyError)
118
+ return { error: policyError };
119
+ const templateError = collect(parts.value.templates, templateToJson, doc.templates, "template");
120
+ if (templateError)
121
+ return { error: templateError };
122
+ return { doc };
123
+ }
124
+ /**
125
+ * Every `Cedar::Policy` in a build, with references walked and ids resolved.
126
+ *
127
+ * The serializer's own first pass, exported so the AVP embedding (#1652)
128
+ * renders from the same model rather than re-deriving it.
129
+ */
130
+ export function cedarPolicyRecords(entities) {
131
+ // Reverse map first: walkValue resolves a Declarable reference by identity,
132
+ // so every entity has to be known before any of them is walked.
133
+ const entityNames = new Map();
134
+ for (const [name, entity] of entities) {
135
+ entityNames.set(entity, name);
136
+ }
137
+ const records = [];
138
+ for (const [name, entity] of entities) {
139
+ if (isPropertyDeclarable(entity))
140
+ continue;
141
+ if (entity.entityType !== CEDAR_POLICY_TYPE)
142
+ continue;
143
+ const props = walkValue(getProps(entity), entityNames, cedarVisitor);
144
+ records.push({ name, id: resolvePolicyId(name, props), props });
145
+ }
146
+ return records;
147
+ }
148
+ // ── Serializer ────────────────────────────────────────────────────
149
+ export const cedarSerializer = {
150
+ name: "cedar",
151
+ rulePrefix: "CED",
152
+ // The dogwood temporal dialect ships inside this lexicon (epic #1646): a
153
+ // `.dw` file stripped of Cedar semantics is meaningless, so its checks are
154
+ // this serializer's, under their own published id family.
155
+ extraRulePrefixes: ["DWD"],
156
+ serialize(entities, _outputs) {
157
+ const policyText = [];
158
+ for (const { id, props } of cedarPolicyRecords(entities)) {
159
+ policyText.push(renderPolicyText(id, props));
160
+ }
161
+ // The dogwood dialect's own entities (#1658). Emitted from here rather
162
+ // than from a second serializer because they are one policy set with two
163
+ // halves — the `.dw` text and the `.cedar` text describe the same system,
164
+ // and a project holding only temporal policies still gets its files.
165
+ const dogwood = serializeDogwood(entities);
166
+ // The project schema, as a file beside the policies (#1697). This is what
167
+ // turns CEDE010's parse-only advisory into validation: `findSchema` in
168
+ // lint/post-synth/wasm-helpers.ts reads it straight back out of `files`.
169
+ const schemas = schemaEntities(entities);
170
+ if (policyText.length === 0 && !dogwood && schemas.length === 0)
171
+ return "";
172
+ const files = {};
173
+ const warnings = [...(dogwood?.warnings ?? [])];
174
+ for (const schema of schemas) {
175
+ if (files[schema.filename] !== undefined) {
176
+ warnings.push(`cedar: schema "${schema.name}" targets ${schema.filename}, which another schema already wrote; give one of them an explicit \`filename\``);
177
+ continue;
178
+ }
179
+ files[schema.filename] = schema.text;
180
+ }
181
+ let primary = "";
182
+ if (policyText.length > 0) {
183
+ primary = policyText.join("\n\n") + "\n";
184
+ const { doc, error } = policySetJSON(primary);
185
+ // A policy set Cedar cannot read is a real defect, but it is the lint and
186
+ // post-synth surface's to report (#1651) — the text is still the artifact
187
+ // every evaluator consumes, so it is emitted either way, with the module's
188
+ // own message carried out as a build warning rather than swallowed.
189
+ if (doc) {
190
+ files[CEDAR_JSON_FILENAME] = JSON.stringify(doc, null, 2) + "\n";
191
+ }
192
+ else {
193
+ warnings.push(`cedar: the emitted policy text did not parse, so ${CEDAR_JSON_FILENAME} was not written — ${error}`);
194
+ }
195
+ }
196
+ Object.assign(files, dogwood?.files ?? {});
197
+ return {
198
+ primary,
199
+ ...(Object.keys(files).length > 0 ? { files } : {}),
200
+ ...(warnings.length > 0 ? { warnings } : {}),
201
+ };
202
+ },
203
+ };
@@ -0,0 +1,96 @@
1
+ /**
2
+ * "Fetch" the Cedar schema — which for this lexicon means reading a file.
3
+ *
4
+ * Every other lexicon downloads one global upstream spec. Cedar's interesting
5
+ * spec is the *project's* schema (epic #1645), so there is nothing to fetch,
6
+ * nothing to cache, and no network at all. What replaces it is a small
7
+ * resolution order:
8
+ *
9
+ * 1. `cedar.schema` from the project's `chant.config.ts`
10
+ * 2. `schema.cedarschema` in the project root
11
+ * 3. the schema bundled at `./default-schema.cedarschema`
12
+ *
13
+ * Step 3 is the one that needs defending. Without it `generate()` has nothing
14
+ * to read in a checkout that has not written a schema yet — which is every
15
+ * fresh clone, `just check-lexicons`, and this lexicon's own examples — and a
16
+ * lexicon whose generate step only works after the user does something is a
17
+ * lexicon whose generate step is never gated. The default is a real
18
+ * application-authorization schema, not a stub, so the surface it produces is
19
+ * worth exercising.
20
+ *
21
+ * `requireProjectSchema` turns step 3 off for projects that have moved past it.
22
+ */
23
+ import { existsSync, readFileSync } from "fs";
24
+ import { isAbsolute, join, resolve } from "path";
25
+ import { moduleSourceDir } from "@intentius/chant/module-source";
26
+ import { CEDAR_DEFAULT_SCHEMA_PATH } from "../config.js";
27
+ /** Filename the bundled fallback schema ships under. */
28
+ export const DEFAULT_SCHEMA_FILENAME = "default-schema.cedarschema";
29
+ /** Map key used for the single schema entry, so `parseSchema` can tell them apart. */
30
+ export const DEFAULT_SCHEMA_KEY = "<bundled-default>";
31
+ let specDir;
32
+ /**
33
+ * Directory this module lives in, resolved on first use.
34
+ *
35
+ * Kept lazy so importing the module never touches `import.meta.url`; edge
36
+ * bundles that pull this file in through the lint barrel have no such URL and
37
+ * would crash at module init otherwise.
38
+ */
39
+ function resolveSpecDir() {
40
+ if (specDir !== undefined)
41
+ return specDir;
42
+ try {
43
+ specDir = moduleSourceDir(import.meta.url);
44
+ }
45
+ catch {
46
+ return undefined;
47
+ }
48
+ return specDir;
49
+ }
50
+ /** Absolute path to the schema bundled with this package. */
51
+ export function defaultSchemaPath() {
52
+ const dir = resolveSpecDir();
53
+ if (dir === undefined) {
54
+ throw new Error("cedar: the bundled default schema is not reachable from this runtime (no module URL).");
55
+ }
56
+ return join(dir, DEFAULT_SCHEMA_FILENAME);
57
+ }
58
+ /**
59
+ * Resolve which schema `generate()` should read, without loading it.
60
+ *
61
+ * Separated from {@link fetchSchemas} so tests and `coverage()` can ask the
62
+ * question without reading the file.
63
+ */
64
+ export function resolveSchemaPath(options = {}) {
65
+ const root = options.projectRoot ?? process.cwd();
66
+ const configured = options.config?.schema;
67
+ const candidates = configured
68
+ ? [isAbsolute(configured) ? configured : resolve(root, configured)]
69
+ : [resolve(root, CEDAR_DEFAULT_SCHEMA_PATH)];
70
+ for (const candidate of candidates) {
71
+ if (existsSync(candidate))
72
+ return { path: candidate, isDefault: false };
73
+ }
74
+ if (options.config?.validation?.requireProjectSchema) {
75
+ throw new Error(`cedar: no schema found at ${candidates.join(", ")}. ` +
76
+ "`cedar.validation.requireProjectSchema` is on, so the bundled default schema is not used.");
77
+ }
78
+ return { path: defaultSchemaPath(), isDefault: true };
79
+ }
80
+ /** Read the resolved schema. */
81
+ export function readSchemaSource(options = {}) {
82
+ const { path, isDefault } = resolveSchemaPath(options);
83
+ return { path, text: readFileSync(path, "utf-8"), isDefault };
84
+ }
85
+ /**
86
+ * The `fetchSchemas` callback for `generatePipeline`.
87
+ *
88
+ * Keyed by `<bundled-default>` or by the project schema's absolute path, which
89
+ * is how `parseSchema` knows whether the content pin applies.
90
+ */
91
+ export async function fetchSchemas(options) {
92
+ const source = readSchemaSource({ projectRoot: options?.projectRoot, config: options?.config });
93
+ return new Map([
94
+ [source.isDefault ? DEFAULT_SCHEMA_KEY : source.path, Buffer.from(source.text, "utf-8")],
95
+ ]);
96
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Turn a `.cedarschema` into the flat declaration list the emitters read.
3
+ *
4
+ * The canonical input is the human-readable syntax and the canonical
5
+ * intermediate is `schemaToJsonWithResolvedTypes`' output (#1648 §4). That
6
+ * choice buys one thing that matters here: `schemaToJson` emits
7
+ * `{type:"EntityOrCommon", name}` for every named type, which cannot
8
+ * distinguish an entity reference from a primitive from a common-type alias,
9
+ * so an emitter reading it would have to re-implement Cedar's own name
10
+ * resolution. The resolved form hands back `{type:"Entity", name:"App::User"}`
11
+ * for references and bare primitives for the rest.
12
+ *
13
+ * What it does *not* resolve is common types in attribute position: those come
14
+ * back as a bare qualified name (`{type:"App::TagSet"}`) with the definition
15
+ * still in `commonTypes`. {@link resolveTypeRef} closes that last hop, so
16
+ * nothing downstream ever sees an alias.
17
+ *
18
+ * One declaration per entity type and one per action, each carrying its own
19
+ * fully-qualified `typeName` — which is what lets `NamingStrategy` contest
20
+ * short names across namespaces the same way it does for every other lexicon.
21
+ */
22
+ import { resolveSchema } from "./wasm.js";
23
+ // ── Parsing ───────────────────────────────────────────────────────
24
+ /**
25
+ * Parse a `.cedarschema` into declarations plus the resolved JSON it came from.
26
+ *
27
+ * Throws with the wasm's own message on a schema that does not parse — which
28
+ * is the right failure for a project schema that is malformed, and reads far
29
+ * better than the alternative, since Cedar's parse errors carry `did you mean`
30
+ * suggestions.
31
+ */
32
+ export function parseCedarSchema(schemaText) {
33
+ const result = resolveSchema(schemaText);
34
+ if (!result.ok) {
35
+ throw new Error(`cedar: schema did not resolve — ${result.error}`);
36
+ }
37
+ const resolved = result.value;
38
+ const decls = [];
39
+ for (const namespace of Object.keys(resolved).sort()) {
40
+ decls.push(...declarationsFor(namespace, resolved[namespace]));
41
+ }
42
+ return { decls, resolved };
43
+ }
44
+ /**
45
+ * `parseSchema` for `generatePipeline`. One buffer in, many declarations out.
46
+ */
47
+ export function parseSchema(_typeName, data) {
48
+ return parseCedarSchema(data.toString("utf-8")).decls;
49
+ }
50
+ function declarationsFor(namespace, body) {
51
+ if (!body)
52
+ return [];
53
+ const commonTypes = body.commonTypes ?? {};
54
+ const decls = [];
55
+ for (const shortName of Object.keys(body.entityTypes ?? {}).sort()) {
56
+ const entity = body.entityTypes[shortName];
57
+ const typeName = qualify(namespace, shortName);
58
+ const attributes = recordAttributes(entity.shape, commonTypes, namespace);
59
+ decls.push({
60
+ kind: "entity",
61
+ typeName,
62
+ namespace,
63
+ shortName,
64
+ memberOfTypes: [...(entity.memberOfTypes ?? [])].sort(),
65
+ attributes,
66
+ ...(entity.enum ? { enumValues: [...entity.enum] } : {}),
67
+ // One "property type" per entity: its attribute record. Nested anonymous
68
+ // records are inlined rather than named, so this stays 1:1.
69
+ propertyTypes: [{ name: `${shortName}_Attributes` }],
70
+ enums: entity.enum ? [entity.enum] : [],
71
+ });
72
+ }
73
+ for (const actionId of Object.keys(body.actions ?? {}).sort()) {
74
+ const action = body.actions[actionId];
75
+ const appliesTo = action.appliesTo ?? {};
76
+ decls.push({
77
+ kind: "action",
78
+ typeName: actionUid(namespace, actionId),
79
+ namespace,
80
+ actionId,
81
+ principalTypes: [...(appliesTo.principalTypes ?? [])].sort(),
82
+ resourceTypes: [...(appliesTo.resourceTypes ?? [])].sort(),
83
+ context: recordAttributes(appliesTo.context, commonTypes, namespace),
84
+ propertyTypes: [{ name: `${actionId}_Context` }],
85
+ enums: [],
86
+ });
87
+ }
88
+ return decls;
89
+ }
90
+ /** `App` + `User` → `App::User`; the empty namespace yields the bare name. */
91
+ export function qualify(namespace, shortName) {
92
+ return namespace ? `${namespace}::${shortName}` : shortName;
93
+ }
94
+ /** The entity UID form an action is referenced by in a policy. */
95
+ export function actionUid(namespace, actionId) {
96
+ return namespace ? `${namespace}::Action::"${actionId}"` : `Action::"${actionId}"`;
97
+ }
98
+ function recordAttributes(shape, commonTypes, namespace) {
99
+ if (!shape || shape.type !== "Record" || !shape.attributes)
100
+ return [];
101
+ return Object.keys(shape.attributes)
102
+ .sort()
103
+ .map((name) => {
104
+ const attr = shape.attributes[name];
105
+ return {
106
+ name,
107
+ // Cedar attributes are required unless the schema says otherwise, so
108
+ // `required` absent means required — the opposite of JSON Schema.
109
+ required: attr.required !== false,
110
+ type: resolveTypeRef(attr, commonTypes, namespace),
111
+ };
112
+ });
113
+ }
114
+ const PRIMITIVES = {
115
+ String: { cedar: "String", ts: "string" },
116
+ Long: { cedar: "Long", ts: "number" },
117
+ Bool: { cedar: "Bool", ts: "boolean" },
118
+ };
119
+ const EXTENSIONS = new Set(["ipaddr", "decimal", "datetime", "duration"]);
120
+ /**
121
+ * Collapse one resolved type node, following common-type aliases.
122
+ *
123
+ * `seen` guards a schema whose common types reference each other — Cedar
124
+ * rejects true cycles, but a malformed one should not spin here.
125
+ */
126
+ export function resolveTypeRef(node, commonTypes, namespace, seen = new Set()) {
127
+ const primitive = PRIMITIVES[node.type];
128
+ if (primitive)
129
+ return { kind: "primitive", ...primitive };
130
+ if (node.type === "Entity") {
131
+ return { kind: "entity", entityType: node.name ?? "" };
132
+ }
133
+ if (node.type === "Set") {
134
+ const element = node.element
135
+ ? resolveTypeRef(node.element, commonTypes, namespace, seen)
136
+ : { kind: "opaque", name: "Set" };
137
+ return { kind: "set", element };
138
+ }
139
+ if (node.type === "Record") {
140
+ const attributes = Object.keys(node.attributes ?? {})
141
+ .sort()
142
+ .map((name) => {
143
+ const attr = node.attributes[name];
144
+ return {
145
+ name,
146
+ required: attr.required !== false,
147
+ type: resolveTypeRef(attr, commonTypes, namespace, seen),
148
+ };
149
+ });
150
+ return { kind: "record", attributes };
151
+ }
152
+ if (EXTENSIONS.has(node.type)) {
153
+ return { kind: "extension", name: node.type };
154
+ }
155
+ // `EntityOrCommon` should not survive resolution, but a defensive branch
156
+ // costs nothing and keeps a future wasm version from emitting `unknown`.
157
+ const named = node.type === "EntityOrCommon" ? (node.name ?? "") : node.type;
158
+ const commonKey = named.startsWith(`${namespace}::`) ? named.slice(namespace.length + 2) : named;
159
+ const definition = commonTypes[commonKey];
160
+ if (definition && !seen.has(named)) {
161
+ return resolveTypeRef(definition, commonTypes, namespace, new Set([...seen, named]));
162
+ }
163
+ return { kind: "opaque", name: named };
164
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * What this lexicon pins, and why it pins two different things (#1650, #1390).
3
+ *
4
+ * ## The version pin
5
+ *
6
+ * `@cedar-policy/cedar-wasm` and the Cedar *language* move independently: the
7
+ * package is 4.12.0, the grammar it implements is 4.5 (#1648 §0). A package
8
+ * bump that leaves the language at 4.5 cannot change what parses, so the
9
+ * language version is the value worth asserting at generation time and the
10
+ * package version is the one `upstreamPin` bumps. Both are recorded.
11
+ *
12
+ * The runtime assert is chant #1390's spirit applied to a dependency rather
13
+ * than to a downloaded spec: generation refuses when the installed package
14
+ * implements a different language version than the one whose resolved output
15
+ * produced the committed artifacts. Releases are roughly monthly, so this will
16
+ * fire — and when it does, the answer is to look at the new resolved JSON, not
17
+ * to publish types generated from a grammar nobody checked.
18
+ *
19
+ * ## The content pin
20
+ *
21
+ * The bundled default schema (`default-schema.cedarschema`) is what generation
22
+ * reads in this repo and in a fresh checkout, so it decides the published
23
+ * surface. There is no upstream URL to pin, so the pin is over content: a
24
+ * sha256 over the *resolved* JSON — the thing the emitters actually read —
25
+ * plus the covered entity-type and action names committed beside it.
26
+ *
27
+ * Resolved-JSON serialization is byte-deterministic (Rust `BTreeMap` ordering,
28
+ * 1 distinct serialization over 25 runs, #1648 §5.2), so the digest is stable.
29
+ * It moves when the default schema is edited, and — the case the pin exists for
30
+ * — when a cedar-wasm upgrade resolves the same schema differently. Both change
31
+ * the generated types, and neither is visible in a diff otherwise, because
32
+ * `src/generated/` is not committed.
33
+ *
34
+ * A user's own project schema is not pinned. It is their input, they own its
35
+ * churn, and refusing to generate because they edited their own schema would be
36
+ * absurd.
37
+ */
38
+ import { createHash } from "crypto";
39
+ import pinnedNames from "./pinned-names.json" with { type: "json" };
40
+ import { langVersion } from "./wasm.js";
41
+ /** The pinned `@cedar-policy/cedar-wasm` version — what `upstreamPin` bumps. */
42
+ export const CEDAR_WASM_VERSION = "4.12.0";
43
+ /** The Cedar *language* version that package implements (`getCedarLangVersion()`). */
44
+ export const CEDAR_LANG_VERSION = "4.5";
45
+ /** Env var that proceeds past a language-version mismatch for one run. */
46
+ export const ACCEPT_LANG_ENV = "CHANT_ACCEPT_CEDAR_LANG";
47
+ /** Env var that accepts a moved default-schema digest, printing the new pin. */
48
+ export const ACCEPT_SCHEMA_ENV = "CHANT_ACCEPT_CEDAR_SCHEMA";
49
+ /**
50
+ * The entity types and action UIDs the pinned default schema resolved to.
51
+ *
52
+ * Committed beside the digest for the same reason aws commits its type names:
53
+ * without the list, accepting a new pin is one opaque hash replacing another,
54
+ * and the generated artifacts cannot stand in because they are gitignored.
55
+ */
56
+ export const PINNED_SCHEMA_NAMES = pinnedNames;
57
+ /**
58
+ * The accepted default schema.
59
+ *
60
+ * To move it: run generation, read the refusal, confirm the delta is one you
61
+ * want, and paste the printed block here in its own commit.
62
+ */
63
+ export const CEDAR_SCHEMA_PIN = {
64
+ digest: "sha256:8f86a4e4fd6845c245c84d226277741c41d29da6a173f01477bda3a662dd3ec9",
65
+ declarations: 16,
66
+ accepted: "2026-08-10",
67
+ };
68
+ /**
69
+ * Digest the resolved schema JSON.
70
+ *
71
+ * Hashes the canonical serialization the wasm produced, not the authored
72
+ * `.cedarschema` text: reformatting a schema, or reordering its entity types,
73
+ * changes the text and not one byte of what the emitters read.
74
+ */
75
+ export function resolvedSchemaDigest(resolved) {
76
+ return `sha256:${createHash("sha256").update(JSON.stringify(resolved)).digest("hex")}`;
77
+ }
78
+ /** Compare a freshly resolved schema against a pin. `null` when it matches. */
79
+ export function schemaDrift(resolved, names, pinnedNamesList = PINNED_SCHEMA_NAMES, pin = CEDAR_SCHEMA_PIN) {
80
+ const digest = resolvedSchemaDigest(resolved);
81
+ if (digest === pin.digest)
82
+ return null;
83
+ const current = new Set(names);
84
+ const pinned = new Set(pinnedNamesList);
85
+ return {
86
+ digest,
87
+ declarations: current.size,
88
+ added: [...current].filter((n) => !pinned.has(n)).sort(),
89
+ removed: [...pinned].filter((n) => !current.has(n)).sort(),
90
+ };
91
+ }
92
+ /** The report a mismatch produces. */
93
+ export function schemaDriftMessage(drift, pin = CEDAR_SCHEMA_PIN) {
94
+ const delta = drift.declarations - pin.declarations;
95
+ const countLine = delta === 0
96
+ ? `${drift.declarations} declarations, unchanged in count`
97
+ : `${drift.declarations} declarations, ${delta > 0 ? "+" : ""}${delta} against the pin`;
98
+ const lines = [
99
+ "The bundled default Cedar schema no longer resolves to the pinned JSON.",
100
+ "",
101
+ ` pinned ${pin.digest} (${pin.declarations} declarations, accepted ${pin.accepted})`,
102
+ ` resolved ${drift.digest} (${countLine})`,
103
+ ];
104
+ if (drift.added.length > 0) {
105
+ lines.push(` added ${summarize(drift.added)}`);
106
+ }
107
+ if (drift.removed.length > 0) {
108
+ lines.push(` removed ${summarize(drift.removed)}`);
109
+ }
110
+ lines.push("", "Either the schema was edited or cedar-wasm resolves it differently than", "the version the committed surface was generated from. Both rewrite the", "generated types, and src/generated/ is not committed, so neither shows up", "in a diff on its own.", "", "Confirm the delta is one you want and update lexicons/cedar/src/spec/pin.ts", "and pinned-names.json, in their own commit:", "", ` digest: "${drift.digest}",`, ` declarations: ${drift.declarations},`, ` accepted: "<today>",`, "", `Or re-run with ${ACCEPT_SCHEMA_ENV}=1 to proceed this once and print the same block.`);
111
+ return lines.join("\n");
112
+ }
113
+ function summarize(names) {
114
+ return `${names.slice(0, 5).join(", ")}${names.length > 5 ? ` (+${names.length - 5} more)` : ""}`;
115
+ }
116
+ /**
117
+ * Refuse a default schema whose resolved JSON does not match the pin.
118
+ *
119
+ * Unlike aws, this one refuses on any digest move rather than only on a
120
+ * declaration-set change. It can afford to: the input is a file in this repo
121
+ * and a dependency at a pinned version, neither of which changes under the
122
+ * build's feet the way a republished CloudFormation archive does.
123
+ */
124
+ export function assertPinnedSchema(resolved, names, options = {}) {
125
+ const pin = options.pin ?? CEDAR_SCHEMA_PIN;
126
+ const drift = schemaDrift(resolved, names, options.pinnedNames ?? PINNED_SCHEMA_NAMES, pin);
127
+ if (!drift)
128
+ return;
129
+ const env = options.env ?? process.env;
130
+ const warn = options.warn ?? ((m) => console.error(m));
131
+ const message = schemaDriftMessage(drift, pin);
132
+ if (env[ACCEPT_SCHEMA_ENV]) {
133
+ warn(message);
134
+ return;
135
+ }
136
+ throw new Error(message);
137
+ }
138
+ /**
139
+ * Refuse to generate against a Cedar language version other than the pinned one.
140
+ *
141
+ * chant #1390's rule, one level up the supply chain: a lexicon does not emit
142
+ * types derived from a grammar nobody chose. `getCedarLangVersion()` is the
143
+ * right probe because it is the number that decides what parses — the package
144
+ * version moves monthly and mostly does not.
145
+ */
146
+ export function assertPinnedLangVersion(options = {}) {
147
+ const expected = options.expected ?? CEDAR_LANG_VERSION;
148
+ const actual = options.actual ?? langVersion();
149
+ if (actual === expected)
150
+ return;
151
+ const message = [
152
+ `Cedar language version mismatch: cedar-wasm reports ${actual}, this lexicon pins ${expected}.`,
153
+ "",
154
+ "The installed @cedar-policy/cedar-wasm implements a different grammar than",
155
+ "the one the generated entity and action types were derived from. Generation",
156
+ "refuses rather than emitting a surface nobody reviewed.",
157
+ "",
158
+ "To accept it: check the resolved schema JSON for shape changes, update",
159
+ `CEDAR_LANG_VERSION (and CEDAR_WASM_VERSION) in lexicons/cedar/src/spec/pin.ts,`,
160
+ "and refresh the schema pin in the same commit.",
161
+ "",
162
+ `Or re-run with ${ACCEPT_LANG_ENV}=1 to proceed this once.`,
163
+ ].join("\n");
164
+ const env = options.env ?? process.env;
165
+ if (env[ACCEPT_LANG_ENV]) {
166
+ (options.warn ?? ((m) => console.error(m)))(message);
167
+ return;
168
+ }
169
+ throw new Error(message);
170
+ }