@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.
- package/dist/agentcore/embed.js +250 -0
- package/dist/agentcore/enforcement.js +81 -0
- package/dist/agentcore/scan.js +93 -0
- package/dist/avp/ambient.js +100 -0
- package/dist/avp/client.js +318 -0
- package/dist/avp/describe-resources.js +144 -0
- package/dist/avp/embed.js +125 -0
- package/dist/avp/live-export.js +151 -0
- package/dist/avp/ownership.js +144 -0
- package/dist/avp/statement.js +45 -0
- package/dist/avp/store.js +105 -0
- package/dist/avp/testdata/mock-transport.js +100 -0
- package/dist/codegen/docs-cli.js +6 -0
- package/dist/codegen/docs.js +133 -0
- package/dist/codegen/emit.js +331 -0
- package/dist/codegen/generate-cli.js +18 -0
- package/dist/codegen/generate.js +122 -0
- package/dist/codegen/naming.js +90 -0
- package/dist/codegen/package.js +33 -0
- package/dist/commands.js +86 -0
- package/dist/composites/deny-by-default-set.js +51 -0
- package/dist/composites/index.js +9 -0
- package/dist/composites/owner-can-manage.js +48 -0
- package/dist/config.js +159 -0
- package/dist/coverage.js +112 -0
- package/dist/detect.js +61 -0
- package/dist/dogwood/cli.js +477 -0
- package/dist/dogwood/event-schema.js +219 -0
- package/dist/dogwood/index.js +32 -0
- package/dist/dogwood/macros.js +175 -0
- package/dist/dogwood/policy.js +58 -0
- package/dist/dogwood/replay-activity.js +313 -0
- package/dist/dogwood/replay-op.js +134 -0
- package/dist/dogwood/scan.js +234 -0
- package/dist/dogwood/serialize.js +199 -0
- package/dist/dogwood/temporal.js +354 -0
- package/dist/dogwood/trace.js +269 -0
- package/dist/dogwood/upstream.js +40 -0
- package/dist/dogwood/window.js +82 -0
- package/dist/gate-policy-pack.js +189 -0
- package/dist/gate-policy.js +168 -0
- package/dist/gate-schema.js +122 -0
- package/dist/generated/index.js +94 -0
- package/dist/generated/runtime.js +2 -0
- package/dist/import/adapter.js +54 -0
- package/dist/import/clause-text.js +166 -0
- package/dist/import/generator.js +178 -0
- package/dist/import/parser.js +273 -0
- package/dist/index.js +79 -0
- package/dist/init-templates.js +330 -0
- package/dist/integrity.json +2 -2
- package/dist/lint/audit-catalog.js +65 -0
- package/dist/lint/audit-lineage.js +23 -0
- package/dist/lint/post-synth/cedar-helpers.js +123 -0
- package/dist/lint/post-synth/cedc010.js +59 -0
- package/dist/lint/post-synth/cedc011.js +29 -0
- package/dist/lint/post-synth/cedc012.js +34 -0
- package/dist/lint/post-synth/cedc013.js +45 -0
- package/dist/lint/post-synth/cedc014.js +54 -0
- package/dist/lint/post-synth/cede010.js +51 -0
- package/dist/lint/post-synth/cede011.js +36 -0
- package/dist/lint/post-synth/ceds010.js +31 -0
- package/dist/lint/post-synth/ceds011.js +22 -0
- package/dist/lint/post-synth/ceds012.js +31 -0
- package/dist/lint/post-synth/dogwood-helpers.js +94 -0
- package/dist/lint/post-synth/dwdc010.js +38 -0
- package/dist/lint/post-synth/dwdc011.js +34 -0
- package/dist/lint/post-synth/dwdc012.js +24 -0
- package/dist/lint/post-synth/dwdc013.js +29 -0
- package/dist/lint/post-synth/dwde010.js +84 -0
- package/dist/lint/post-synth/dwde011.js +67 -0
- package/dist/lint/post-synth/dwds010.js +21 -0
- package/dist/lint/post-synth/index.js +36 -0
- package/dist/lint/post-synth/wasm-helpers.js +234 -0
- package/dist/lint/rules/index.js +9 -0
- package/dist/lint/rules/policy-shape.js +114 -0
- package/dist/lsp/completions.js +52 -0
- package/dist/lsp/hover.js +56 -0
- package/dist/lsp/registry.d.ts.map +1 -1
- package/dist/lsp/registry.js +51 -0
- package/dist/manifest.json +1 -1
- package/dist/mcp/index.js +82 -0
- package/dist/mcp/policy-coverage.js +134 -0
- package/dist/op/activities/index.js +16 -0
- package/dist/package-cli.js +18 -0
- package/dist/plugin.js +304 -0
- package/dist/policy-text.js +117 -0
- package/dist/schema-artifact.js +66 -0
- package/dist/serializer.js +203 -0
- package/dist/spec/fetch.js +96 -0
- package/dist/spec/parse.js +164 -0
- package/dist/spec/pin.js +170 -0
- package/dist/spec/wasm.js +194 -0
- package/dist/validate-cli.js +7 -0
- package/dist/validate.js +74 -0
- package/package.json +7 -7
- package/src/codegen/package.ts +2 -2
- package/src/coverage.ts +2 -2
- package/src/import/generator.ts +2 -2
- package/src/lsp/registry.ts +2 -1
- 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
|
+
}
|
package/dist/spec/pin.js
ADDED
|
@@ -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
|
+
}
|