@intentius/chant-lexicon-cedar 0.124.31 → 0.124.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/dist/agentcore/embed.js +250 -0
  2. package/dist/agentcore/enforcement.js +81 -0
  3. package/dist/agentcore/scan.js +93 -0
  4. package/dist/avp/ambient.js +100 -0
  5. package/dist/avp/client.js +318 -0
  6. package/dist/avp/describe-resources.js +144 -0
  7. package/dist/avp/embed.js +125 -0
  8. package/dist/avp/live-export.js +151 -0
  9. package/dist/avp/ownership.js +144 -0
  10. package/dist/avp/statement.js +45 -0
  11. package/dist/avp/store.js +105 -0
  12. package/dist/avp/testdata/mock-transport.js +100 -0
  13. package/dist/codegen/docs-cli.js +6 -0
  14. package/dist/codegen/docs.js +133 -0
  15. package/dist/codegen/emit.js +331 -0
  16. package/dist/codegen/generate-cli.js +18 -0
  17. package/dist/codegen/generate.js +122 -0
  18. package/dist/codegen/naming.js +90 -0
  19. package/dist/codegen/package.js +33 -0
  20. package/dist/commands.js +86 -0
  21. package/dist/composites/deny-by-default-set.js +51 -0
  22. package/dist/composites/index.js +9 -0
  23. package/dist/composites/owner-can-manage.js +48 -0
  24. package/dist/config.js +159 -0
  25. package/dist/coverage.js +112 -0
  26. package/dist/detect.js +61 -0
  27. package/dist/dogwood/cli.js +477 -0
  28. package/dist/dogwood/event-schema.js +219 -0
  29. package/dist/dogwood/index.js +32 -0
  30. package/dist/dogwood/macros.js +175 -0
  31. package/dist/dogwood/policy.js +58 -0
  32. package/dist/dogwood/replay-activity.js +313 -0
  33. package/dist/dogwood/replay-op.js +134 -0
  34. package/dist/dogwood/scan.js +234 -0
  35. package/dist/dogwood/serialize.js +199 -0
  36. package/dist/dogwood/temporal.js +354 -0
  37. package/dist/dogwood/trace.js +269 -0
  38. package/dist/dogwood/upstream.js +40 -0
  39. package/dist/dogwood/window.js +82 -0
  40. package/dist/gate-policy-pack.js +189 -0
  41. package/dist/gate-policy.js +168 -0
  42. package/dist/gate-schema.js +122 -0
  43. package/dist/generated/index.js +94 -0
  44. package/dist/generated/runtime.js +2 -0
  45. package/dist/import/adapter.js +54 -0
  46. package/dist/import/clause-text.js +166 -0
  47. package/dist/import/generator.js +178 -0
  48. package/dist/import/parser.js +273 -0
  49. package/dist/index.js +79 -0
  50. package/dist/init-templates.js +330 -0
  51. package/dist/integrity.json +2 -2
  52. package/dist/lint/audit-catalog.js +65 -0
  53. package/dist/lint/audit-lineage.js +23 -0
  54. package/dist/lint/post-synth/cedar-helpers.js +123 -0
  55. package/dist/lint/post-synth/cedc010.js +59 -0
  56. package/dist/lint/post-synth/cedc011.js +29 -0
  57. package/dist/lint/post-synth/cedc012.js +34 -0
  58. package/dist/lint/post-synth/cedc013.js +45 -0
  59. package/dist/lint/post-synth/cedc014.js +54 -0
  60. package/dist/lint/post-synth/cede010.js +51 -0
  61. package/dist/lint/post-synth/cede011.js +36 -0
  62. package/dist/lint/post-synth/ceds010.js +31 -0
  63. package/dist/lint/post-synth/ceds011.js +22 -0
  64. package/dist/lint/post-synth/ceds012.js +31 -0
  65. package/dist/lint/post-synth/dogwood-helpers.js +94 -0
  66. package/dist/lint/post-synth/dwdc010.js +38 -0
  67. package/dist/lint/post-synth/dwdc011.js +34 -0
  68. package/dist/lint/post-synth/dwdc012.js +24 -0
  69. package/dist/lint/post-synth/dwdc013.js +29 -0
  70. package/dist/lint/post-synth/dwde010.js +84 -0
  71. package/dist/lint/post-synth/dwde011.js +67 -0
  72. package/dist/lint/post-synth/dwds010.js +21 -0
  73. package/dist/lint/post-synth/index.js +36 -0
  74. package/dist/lint/post-synth/wasm-helpers.js +234 -0
  75. package/dist/lint/rules/index.js +9 -0
  76. package/dist/lint/rules/policy-shape.js +114 -0
  77. package/dist/lsp/completions.js +52 -0
  78. package/dist/lsp/hover.js +56 -0
  79. package/dist/lsp/registry.d.ts.map +1 -1
  80. package/dist/lsp/registry.js +51 -0
  81. package/dist/manifest.json +1 -1
  82. package/dist/mcp/index.js +82 -0
  83. package/dist/mcp/policy-coverage.js +134 -0
  84. package/dist/op/activities/index.js +16 -0
  85. package/dist/package-cli.js +18 -0
  86. package/dist/plugin.js +304 -0
  87. package/dist/policy-text.js +117 -0
  88. package/dist/schema-artifact.js +66 -0
  89. package/dist/serializer.js +203 -0
  90. package/dist/spec/fetch.js +96 -0
  91. package/dist/spec/parse.js +164 -0
  92. package/dist/spec/pin.js +170 -0
  93. package/dist/spec/wasm.js +194 -0
  94. package/dist/validate-cli.js +7 -0
  95. package/dist/validate.js +74 -0
  96. package/package.json +7 -7
  97. package/src/codegen/package.ts +2 -2
  98. package/src/coverage.ts +2 -2
  99. package/src/import/generator.ts +2 -2
  100. package/src/lsp/registry.ts +2 -1
  101. package/src/spec/fetch.ts +2 -2
@@ -0,0 +1,84 @@
1
+ import { DOGWOOD_BINARY_NAME, DOGWOOD_SEARCH_ORDER, findDogwoodBinary, formatDogwoodDiagnostic, runDogwoodValidate, } from "../../dogwood/cli.js";
2
+ import { describeBundle, planDogwoodRuns } from "./dogwood-helpers.js";
3
+ export const dwde010 = {
4
+ id: "DWDE010",
5
+ description: "Emitted dogwood policy sets validate clean under `dogwood validate`, when the binary is available",
6
+ check(ctx) {
7
+ const plan = planDogwoodRuns(ctx);
8
+ if (!plan.hasPolicies)
9
+ return [];
10
+ const binary = findDogwoodBinary();
11
+ if (!binary) {
12
+ return [
13
+ {
14
+ checkId: "DWDE010",
15
+ severity: "info",
16
+ message: `This build emitted dogwood .dw policies, but no \`${DOGWOOD_BINARY_NAME}\` binary was found, so full .dw validation did not run — the DWDC walls checked what TypeScript can answer and nothing checked macro expansion, the temporal type check, or the Cedar body against the action schema. chant looked at ${DOGWOOD_SEARCH_ORDER}. See chant #1659.`,
17
+ lexicon: plan.lexicon,
18
+ },
19
+ ];
20
+ }
21
+ if (plan.blocked) {
22
+ return [
23
+ {
24
+ checkId: "DWDE010",
25
+ severity: "info",
26
+ message: `This build emitted dogwood .dw policies and \`${DOGWOOD_BINARY_NAME}\` is available, but ${plan.blocked}, so full .dw validation did not run. See chant #1659.`,
27
+ lexicon: plan.lexicon,
28
+ },
29
+ ];
30
+ }
31
+ const diagnostics = [];
32
+ for (const prepared of plan.bundles) {
33
+ const where = describeBundle(prepared);
34
+ const result = runDogwoodValidate(binary.path, prepared.bundle);
35
+ if (result.kind === "unusable") {
36
+ diagnostics.push({
37
+ checkId: "DWDE010",
38
+ severity: "warning",
39
+ message: `Dogwood policy set ${where} could not be validated — ${result.reason}. The policy set was neither accepted nor rejected.`,
40
+ entity: prepared.source,
41
+ lexicon: prepared.lexicon,
42
+ });
43
+ continue;
44
+ }
45
+ if (result.kind === "fatal") {
46
+ for (const finding of [result.error, ...result.related]) {
47
+ diagnostics.push({
48
+ checkId: "DWDE010",
49
+ severity: "error",
50
+ message: `Dogwood policy set ${where} could not be parsed or lowered: ${formatDogwoodDiagnostic(finding)}`,
51
+ entity: prepared.source,
52
+ lexicon: prepared.lexicon,
53
+ });
54
+ }
55
+ continue;
56
+ }
57
+ if (result.kind === "rejected") {
58
+ for (const finding of result.errors) {
59
+ diagnostics.push({
60
+ checkId: "DWDE010",
61
+ severity: "error",
62
+ message: `Dogwood policy set ${where} fails \`dogwood validate\`: ${formatDogwoodDiagnostic(finding)}`,
63
+ entity: prepared.source,
64
+ lexicon: prepared.lexicon,
65
+ });
66
+ }
67
+ }
68
+ // Upstream's own warnings ride along at warning severity. They do not
69
+ // fail `dogwood validate` (`passed` ignores them), and they should not
70
+ // fail a build either — but dropping them would throw away the half of
71
+ // the report that says a policy is legal and pointless.
72
+ for (const finding of result.warnings) {
73
+ diagnostics.push({
74
+ checkId: "DWDE010",
75
+ severity: "warning",
76
+ message: `Dogwood policy set ${where} draws a \`dogwood validate\` warning: ${formatDogwoodDiagnostic(finding)}`,
77
+ entity: prepared.source,
78
+ lexicon: prepared.lexicon,
79
+ });
80
+ }
81
+ }
82
+ return diagnostics;
83
+ },
84
+ };
@@ -0,0 +1,67 @@
1
+ import { findDogwoodBinary, runDogwoodLower } from "../../dogwood/cli.js";
2
+ import { describeBundle, planDogwoodRuns } from "./dogwood-helpers.js";
3
+ import { loadWasm, validatePolicySet } from "./wasm-helpers.js";
4
+ export const dwde011 = {
5
+ id: "DWDE011",
6
+ description: "The Cedar `dogwood lower` produces validates clean against the augmented schema (cedar-wasm validate)",
7
+ check(ctx) {
8
+ const plan = planDogwoodRuns(ctx);
9
+ if (!plan.hasPolicies || plan.blocked || plan.bundles.length === 0)
10
+ return [];
11
+ const binary = findDogwoodBinary();
12
+ if (!binary)
13
+ return [];
14
+ // A missing validator is not this check's finding to report; the cedar leg
15
+ // says so already through CEDC010.
16
+ const wasm = loadWasm();
17
+ if (!wasm)
18
+ return [];
19
+ const diagnostics = [];
20
+ for (const prepared of plan.bundles) {
21
+ const where = describeBundle(prepared);
22
+ const lowered = runDogwoodLower(binary.path, prepared.bundle);
23
+ if (lowered.kind === "unusable") {
24
+ diagnostics.push({
25
+ checkId: "DWDE011",
26
+ severity: "warning",
27
+ message: `Dogwood policy set ${where} could not be lowered to Cedar — ${lowered.reason}. Its non-temporal body was not checked by Cedar's validator.`,
28
+ entity: prepared.source,
29
+ lexicon: prepared.lexicon,
30
+ });
31
+ continue;
32
+ }
33
+ if (lowered.kind === "fatal") {
34
+ // The same fatal DWDE010 reports from its own run. Saying it twice
35
+ // helps nobody, so this arm stays quiet and lets the validate leg own
36
+ // the parse/lower channel.
37
+ continue;
38
+ }
39
+ const { cedarPolicies, cedarSchema } = lowered.value;
40
+ // The lowered policies go in as Cedar text. cedar-wasm synthesizes ids
41
+ // (`policy0`, `policy1`) for a bare-string set and does not read `@id`
42
+ // annotations as ids (#1648), so a finding names the .dw file it came
43
+ // from as well as the id the validator used.
44
+ const outcome = validatePolicySet(wasm, { staticPolicies: cedarPolicies }, cedarSchema);
45
+ if (outcome.failure) {
46
+ diagnostics.push({
47
+ checkId: "DWDE011",
48
+ severity: "error",
49
+ message: `The Cedar lowered from dogwood policy set ${where} could not be validated: ${outcome.failure}`,
50
+ entity: prepared.source,
51
+ lexicon: prepared.lexicon,
52
+ });
53
+ continue;
54
+ }
55
+ for (const finding of outcome.errors) {
56
+ diagnostics.push({
57
+ checkId: "DWDE011",
58
+ severity: "error",
59
+ message: `The Cedar lowered from dogwood policy set ${where} fails Cedar validation (lowered policy "${finding.policyId}"): ${finding.message}`,
60
+ entity: prepared.source,
61
+ lexicon: prepared.lexicon,
62
+ });
63
+ }
64
+ }
65
+ return diagnostics;
66
+ },
67
+ };
@@ -0,0 +1,21 @@
1
+ import { dogwoodSchemaFiles, readEventSchema } from "../../dogwood/scan.js";
2
+ export const dwds010 = {
3
+ id: "DWDS010",
4
+ description: "An emitted dogwood event schema pins its temporal predicates to a request-side value",
5
+ check(ctx) {
6
+ const diagnostics = [];
7
+ for (const schema of dogwoodSchemaFiles(ctx)) {
8
+ const facts = readEventSchema(schema.text);
9
+ if (facts.kinds.length === 0 || facts.hasPin)
10
+ continue;
11
+ diagnostics.push({
12
+ checkId: "DWDS010",
13
+ severity: "warning",
14
+ message: `Dogwood event schema "${schema.source}" declares no pinned field, so temporal predicates correlate across every principal — wider than upstream's default, which pins callerPrincipal to the deciding request's principal. Add a pinned field, or record why cross-principal correlation is wanted.`,
15
+ entity: schema.source,
16
+ lexicon: schema.lexicon,
17
+ });
18
+ }
19
+ return diagnostics;
20
+ },
21
+ };
@@ -0,0 +1,36 @@
1
+ import { cedc010 } from "./cedc010.js";
2
+ import { cedc011 } from "./cedc011.js";
3
+ import { cedc012 } from "./cedc012.js";
4
+ import { cedc013 } from "./cedc013.js";
5
+ import { cedc014 } from "./cedc014.js";
6
+ import { cede010 } from "./cede010.js";
7
+ import { cede011 } from "./cede011.js";
8
+ import { ceds010 } from "./ceds010.js";
9
+ import { ceds011 } from "./ceds011.js";
10
+ import { ceds012 } from "./ceds012.js";
11
+ import { dwdc010 } from "./dwdc010.js";
12
+ import { dwdc011 } from "./dwdc011.js";
13
+ import { dwdc012 } from "./dwdc012.js";
14
+ import { dwdc013 } from "./dwdc013.js";
15
+ import { dwde010 } from "./dwde010.js";
16
+ import { dwde011 } from "./dwde011.js";
17
+ import { dwds010 } from "./dwds010.js";
18
+ export const postSynthChecks = [
19
+ cedc010,
20
+ cedc011,
21
+ cedc012,
22
+ cedc013,
23
+ cedc014,
24
+ cede010,
25
+ cede011,
26
+ ceds010,
27
+ ceds011,
28
+ ceds012,
29
+ dwdc010,
30
+ dwdc011,
31
+ dwdc012,
32
+ dwdc013,
33
+ dwde010,
34
+ dwde011,
35
+ dwds010,
36
+ ];
@@ -0,0 +1,234 @@
1
+ /**
2
+ * The `cedar-wasm` seam for the cedar post-synth checks.
3
+ *
4
+ * Cedar's real parser and validator ship as `@cedar-policy/cedar-wasm`, which
5
+ * runs in-process — no CLI on PATH, no Docker. Everything the checks need from
6
+ * it goes through this module so the traps live in one place. The traps, all
7
+ * of them empirically verified against 4.12.0 (chant #1648):
8
+ *
9
+ * 1. **`type: "success"` does not mean valid.** `failure` means the *call*
10
+ * failed; `success` means validation *ran*, and can carry any number of
11
+ * `validationErrors`. {@link validatePolicySet} asserts both.
12
+ * 2. **`validationErrors` ordering is non-deterministic** — the validator
13
+ * parallelizes across policies and never re-sorts. Forty identical calls
14
+ * produced six distinct orderings. Everything this module returns is sorted
15
+ * by `policyId` then `message`, so a check's output is stable.
16
+ * 3. **Malformed input throws instead of returning a failure answer.** Missing
17
+ * fields on a call struct raise a bare serde `Error` whose `line 1 column N`
18
+ * refers to the internal call struct, not to anything the user wrote — it is
19
+ * meaningless to surface. {@link call} wraps every entry point and normalizes
20
+ * both channels into one result type.
21
+ * 4. **A bare-string policy set gets synthesized ids** (`policy0`, `policy1`),
22
+ * and `@id` annotations do *not* become the policy id. Everything here
23
+ * passes `staticPolicies` as a `Record<PolicyId, Policy>` keyed by chant's
24
+ * own ids, so a diagnostic names something a reader can find.
25
+ * 5. **Only `strict` validation mode exists** in this build; any other value
26
+ * throws. The settings are left at their default rather than passed.
27
+ *
28
+ * Only the `/nodejs` flavor is imported. The root/`esm` flavor imports the
29
+ * `.wasm` as an ES module, which Bun cannot start (`__wbindgen_start is not a
30
+ * function`) and Node only supports behind an experimental warning printed on
31
+ * every run; `/web` needs an explicit `initSync`. `/nodejs` is the one flavor
32
+ * that loads under `node`, `tsx`, `vitest`, and `bun` alike.
33
+ *
34
+ * Excluded from check auto-discovery by the "helper" filename filter.
35
+ */
36
+ import { createRequire } from "module";
37
+ import { isRecord, } from "./cedar-helpers.js";
38
+ let cached;
39
+ let loadError;
40
+ /**
41
+ * The wasm module, loaded on first use.
42
+ *
43
+ * Lazy and `require`-based on purpose. The post-synth barrel is imported
44
+ * statically by `plugin.ts`, and every `chant lint`/`chant audit` run loads
45
+ * every active lexicon's plugin — a top-level import would pull 4 MB of wasm
46
+ * into any build that merely has cedar installed. `check()` is synchronous, so
47
+ * `await import()` is not available; `createRequire` is. The `/nodejs` flavor
48
+ * is CommonJS with a synchronous `readFileSync` init, so there is nothing to
49
+ * await anyway.
50
+ */
51
+ export function loadWasm() {
52
+ if (cached || loadError)
53
+ return cached;
54
+ try {
55
+ const require = createRequire(import.meta.url);
56
+ cached = require("@cedar-policy/cedar-wasm/nodejs");
57
+ }
58
+ catch (err) {
59
+ loadError = err instanceof Error ? err.message : String(err);
60
+ }
61
+ return cached;
62
+ }
63
+ /** Why the wasm could not be loaded, if it could not be. */
64
+ export function wasmLoadError() {
65
+ loadWasm();
66
+ return loadError;
67
+ }
68
+ /** Reset the module cache. Test-only — lets a test exercise the load-failure path. */
69
+ export function resetWasmCache() {
70
+ cached = undefined;
71
+ loadError = undefined;
72
+ }
73
+ /**
74
+ * Run a wasm call, turning a thrown serde error into a result rather than an
75
+ * exception (trap 3 above). The thrown message is deliberately not passed
76
+ * through verbatim: its positions point into the internal call struct.
77
+ */
78
+ export function call(what, fn) {
79
+ try {
80
+ return { ok: true, value: fn() };
81
+ }
82
+ catch (err) {
83
+ const raw = err instanceof Error ? err.message : String(err);
84
+ // Strip the internal `at line 1 column 42` suffix — it locates a field in
85
+ // the serialized call struct, never anything in the user's policy.
86
+ const message = raw.replace(/\s+at line \d+ column \d+\.?$/, "");
87
+ return { ok: false, message: `${what} could not be run by cedar-wasm: ${message}` };
88
+ }
89
+ }
90
+ /** The first line of a Cedar `DetailedError`, with its `help` when it has one. */
91
+ export function describeError(error) {
92
+ return error.help ? `${error.message} (${error.help})` : error.message;
93
+ }
94
+ // ── Condition bodies ──────────────────────────────────────────────
95
+ /**
96
+ * chant's serializer carries a `when`/`unless` body as Cedar expression
97
+ * *source* under a `__expr` key, because the authored model holds expression
98
+ * strings until schema-driven codegen (#1650) makes them typed trees. `__expr`
99
+ * is not part of Cedar's JSON policy grammar — the deserializer rejects it
100
+ * ("unknown variant `__expr`") — so anything handed to the wasm has to have
101
+ * those bodies parsed into real expression JSON first.
102
+ *
103
+ * There is no expression-level entry point in the package, so the expression
104
+ * is parsed inside a throwaway policy and the resulting condition body lifted
105
+ * back out. A source expression that does not parse is the finding CEDC011
106
+ * reports.
107
+ */
108
+ export const EXPR_KEY = "__expr";
109
+ function parseExpression(wasm, expression) {
110
+ const answer = call("condition expression", () => wasm.policyToJson(`permit(principal, action, resource) when { ${expression} };`));
111
+ if (!answer.ok)
112
+ return answer;
113
+ if (answer.value.type === "failure") {
114
+ return { ok: false, message: answer.value.errors.map(describeError).join("; ") };
115
+ }
116
+ const conditions = answer.value.json.conditions;
117
+ const body = Array.isArray(conditions) && isRecord(conditions[0]) ? conditions[0].body : undefined;
118
+ if (body === undefined)
119
+ return { ok: false, message: "expression parsed to no condition body" };
120
+ return { ok: true, value: body };
121
+ }
122
+ /** Replace every `__expr` body in a policy set with parsed expression JSON. */
123
+ export function normalizePolicySet(wasm, set) {
124
+ const staticPolicies = {};
125
+ const templates = {};
126
+ const expressionFailures = [];
127
+ for (const entry of set.entries) {
128
+ const target = entry.template ? templates : staticPolicies;
129
+ target[entry.key] = normalizePolicy(wasm, entry.key, entry.policy, expressionFailures);
130
+ }
131
+ const doc = set.doc ?? {};
132
+ const policySet = {
133
+ staticPolicies,
134
+ templates,
135
+ templateLinks: Array.isArray(doc.templateLinks) ? doc.templateLinks : [],
136
+ };
137
+ return { policySet, expressionFailures };
138
+ }
139
+ function normalizePolicy(wasm, key, policy, failures) {
140
+ if (!Array.isArray(policy.conditions))
141
+ return policy;
142
+ const conditions = policy.conditions.map((clause) => {
143
+ if (!isRecord(clause) || !isRecord(clause.body))
144
+ return clause;
145
+ const expression = clause.body[EXPR_KEY];
146
+ if (typeof expression !== "string")
147
+ return clause;
148
+ const parsed = parseExpression(wasm, expression);
149
+ if (!parsed.ok) {
150
+ failures.push({
151
+ key,
152
+ kind: typeof clause.kind === "string" ? clause.kind : "when",
153
+ expression,
154
+ message: parsed.message,
155
+ });
156
+ // Leave a body the deserializer accepts so the rest of the set still
157
+ // validates — the unparseable clause is reported by CEDC011, not by a
158
+ // cascade of confusing structural errors from every other check.
159
+ return { ...clause, body: { Value: true } };
160
+ }
161
+ return { ...clause, body: parsed.value };
162
+ });
163
+ return { ...policy, conditions };
164
+ }
165
+ /**
166
+ * The project's Cedar schema, if the build emitted one beside the policies.
167
+ *
168
+ * Two forms, both of which the wasm accepts — but only as the right JS type: a
169
+ * `string` is *always* read as human-readable `.cedarschema`, never as JSON, so
170
+ * a `.cedarschema.json` file has to be `JSON.parse`d into a live object first
171
+ * or the parser reports "unexpected token `{`" against perfectly valid JSON.
172
+ *
173
+ * Reads `ctx.outputs` only. Nothing emits a schema today — schema-driven
174
+ * codegen is #1650 — which is exactly why CEDE010 says so out loud instead of
175
+ * passing silently.
176
+ */
177
+ export function findSchema(ctx) {
178
+ for (const [, output] of ctx.outputs) {
179
+ if (typeof output === "string")
180
+ continue;
181
+ for (const [filename, content] of Object.entries(output.files ?? {})) {
182
+ if (typeof content !== "string")
183
+ continue;
184
+ if (filename.endsWith(".cedarschema.json")) {
185
+ try {
186
+ const parsed = JSON.parse(content);
187
+ if (isRecord(parsed))
188
+ return { schema: parsed, source: filename };
189
+ }
190
+ catch {
191
+ continue; // a schema that is not JSON is not a schema we can use
192
+ }
193
+ }
194
+ else if (filename.endsWith(".cedarschema")) {
195
+ return { schema: content, source: filename };
196
+ }
197
+ }
198
+ }
199
+ return undefined;
200
+ }
201
+ function sortFindings(findings) {
202
+ return [...findings].sort((a, b) => a.policyId.localeCompare(b.policyId) || a.message.localeCompare(b.message));
203
+ }
204
+ function flatten(errors) {
205
+ return errors.map((e) => ({ policyId: e.policyId, message: describeError(e.error) }));
206
+ }
207
+ /** Validate a normalized policy set against a schema. Never throws. */
208
+ export function validatePolicySet(wasm, policySet, schema) {
209
+ const answer = call("validate", () => wasm.validate({ schema, policies: policySet }));
210
+ if (!answer.ok)
211
+ return { errors: [], warnings: [], failure: answer.message };
212
+ if (answer.value.type === "failure") {
213
+ return {
214
+ errors: [],
215
+ warnings: [],
216
+ failure: answer.value.errors.map(describeError).join("; "),
217
+ };
218
+ }
219
+ // `success` only means validation RAN. The findings are in the arrays.
220
+ return {
221
+ errors: sortFindings(flatten(answer.value.validationErrors)),
222
+ warnings: sortFindings(flatten(answer.value.validationWarnings)),
223
+ };
224
+ }
225
+ /** Parse-only gate, for when there is no schema to validate against. */
226
+ export function parsePolicySet(wasm, policySet) {
227
+ const answer = call("checkParsePolicySet", () => wasm.checkParsePolicySet(policySet));
228
+ if (!answer.ok)
229
+ return answer;
230
+ if (answer.value.type === "failure") {
231
+ return { ok: true, value: answer.value.errors.map(describeError).sort() };
232
+ }
233
+ return { ok: true, value: [] };
234
+ }
@@ -0,0 +1,9 @@
1
+ import { cedarPolicyShapeRule } from "./policy-shape.js";
2
+ export { cedarPolicyShapeRule, CEDAR_EFFECTS } from "./policy-shape.js";
3
+ /**
4
+ * All lint rules provided by this lexicon (imported by plugin.ts's lintRules()).
5
+ *
6
+ * Pre-synth rules are wired by hand — unlike post-synth checks, they are not
7
+ * auto-discovered. Add the import above and the entry below.
8
+ */
9
+ export const rules = [cedarPolicyShapeRule];
@@ -0,0 +1,114 @@
1
+ import { ts } from "@intentius/chant/lint/typescript";
2
+ /** Module specifiers whose exports construct Cedar declarables. */
3
+ const CEDAR_MODULE = /chant-lexicon-cedar/;
4
+ /** The two effects Cedar has. */
5
+ export const CEDAR_EFFECTS = ["permit", "forbid"];
6
+ function importedCedarBindings(sourceFile) {
7
+ const names = new Set();
8
+ for (const statement of sourceFile.statements) {
9
+ if (!ts.isImportDeclaration(statement))
10
+ continue;
11
+ if (!ts.isStringLiteral(statement.moduleSpecifier))
12
+ continue;
13
+ if (!CEDAR_MODULE.test(statement.moduleSpecifier.text))
14
+ continue;
15
+ const bindings = statement.importClause?.namedBindings;
16
+ if (bindings && ts.isNamedImports(bindings)) {
17
+ for (const element of bindings.elements)
18
+ names.add(element.name.text);
19
+ }
20
+ if (statement.importClause?.name)
21
+ names.add(statement.importClause.name.text);
22
+ }
23
+ return names;
24
+ }
25
+ /** The callee identifier of a `new X(…)` / `X(…)` expression, if it has one. */
26
+ function calleeName(node) {
27
+ return ts.isIdentifier(node.expression) ? node.expression.text : undefined;
28
+ }
29
+ /** The last object-literal argument — where chant resources carry their props. */
30
+ function propsLiteral(node) {
31
+ const args = [...(node.arguments ?? [])];
32
+ for (let i = args.length - 1; i >= 0; i--) {
33
+ const arg = args[i];
34
+ if (ts.isObjectLiteralExpression(arg))
35
+ return arg;
36
+ }
37
+ return undefined;
38
+ }
39
+ function property(obj, key) {
40
+ for (const prop of obj.properties) {
41
+ if (!ts.isPropertyAssignment(prop))
42
+ continue;
43
+ if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name))
44
+ continue;
45
+ if (prop.name.text === key)
46
+ return prop;
47
+ }
48
+ return undefined;
49
+ }
50
+ function lineCol(sourceFile, node) {
51
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
52
+ return { line: line + 1, column: character + 1 };
53
+ }
54
+ export const cedarPolicyShapeRule = {
55
+ id: "CEDC001",
56
+ severity: "error",
57
+ category: "correctness",
58
+ description: "A Cedar policy declaration must use effect permit or forbid, and must not carry an empty when/unless clause",
59
+ check(context) {
60
+ const { sourceFile } = context;
61
+ const diagnostics = [];
62
+ const cedarBindings = importedCedarBindings(sourceFile);
63
+ if (cedarBindings.size === 0)
64
+ return diagnostics;
65
+ const report = (node, message) => {
66
+ const { line, column } = lineCol(sourceFile, node);
67
+ diagnostics.push({
68
+ file: sourceFile.fileName,
69
+ line,
70
+ column,
71
+ ruleId: "CEDC001",
72
+ severity: "error",
73
+ message,
74
+ });
75
+ };
76
+ function inspect(node) {
77
+ const callee = calleeName(node);
78
+ if (!callee || !cedarBindings.has(callee))
79
+ return;
80
+ const props = propsLiteral(node);
81
+ if (!props)
82
+ return;
83
+ const effect = property(props, "effect");
84
+ if (effect && ts.isStringLiteral(effect.initializer)) {
85
+ const value = effect.initializer.text;
86
+ if (!CEDAR_EFFECTS.includes(value)) {
87
+ report(effect, `Cedar policy effect "${value}" is not a Cedar effect — the language has only permit and forbid, and anything that is not "forbid" is emitted as a permit. Write effect: "forbid" if that is what was meant.`);
88
+ }
89
+ }
90
+ for (const clause of ["when", "unless"]) {
91
+ const guard = property(props, clause);
92
+ if (!guard)
93
+ continue;
94
+ const entries = ts.isArrayLiteralExpression(guard.initializer)
95
+ ? guard.initializer.elements
96
+ : [guard.initializer];
97
+ for (const entry of entries) {
98
+ if (!ts.isStringLiteral(entry) && !ts.isNoSubstitutionTemplateLiteral(entry))
99
+ continue;
100
+ if (entry.text.trim() !== "")
101
+ continue;
102
+ report(entry, `Cedar policy has an empty ${clause} clause. An empty guard serializes to ${clause} { }, which is not a Cedar expression and makes the whole policy set unparseable — write the condition, or drop the entry.`);
103
+ }
104
+ }
105
+ }
106
+ function visit(node) {
107
+ if (ts.isCallExpression(node) || ts.isNewExpression(node))
108
+ inspect(node);
109
+ ts.forEachChild(node, visit);
110
+ }
111
+ visit(sourceFile);
112
+ return diagnostics;
113
+ },
114
+ };
@@ -0,0 +1,52 @@
1
+ import { lexiconCompletions } from "@intentius/chant/lsp/lexicon-providers";
2
+ import { cedarIndex, cedarRegistry } from "./registry.js";
3
+ /**
4
+ * Completions over the schema-derived registry.
5
+ *
6
+ * Two shapes come out of core's generic provider: class names after `new `,
7
+ * and property names inside a constructor's object literal. Both are useful
8
+ * here — `new Policy({` completes `effect`/`principal`/`action`/…, and
9
+ * `new Doc` completes `Document`.
10
+ *
11
+ * The third shape is Cedar's own: a scope names an *action constant*, not a
12
+ * class, and those are plain `const` exports rather than constructors, so no
13
+ * `new ` ever precedes them. {@link actionCompletions} covers that position.
14
+ */
15
+ export function completions(ctx) {
16
+ const generic = lexiconCompletions(ctx, cedarIndex(), "Cedar declaration");
17
+ if (generic.length > 0)
18
+ return generic;
19
+ return actionCompletions(ctx);
20
+ }
21
+ /**
22
+ * Action constants in a scope position.
23
+ *
24
+ * `action: { eq: <cursor> }` and `action: { in: [<cursor>` are where an action
25
+ * is named, and the generated constants (`ReadAction`, `WriteAction`, …) are
26
+ * the only correct values. Matching on the surrounding text rather than on a
27
+ * parsed AST keeps this cheap and is what the other lexicons' providers do.
28
+ */
29
+ export function actionCompletions(ctx) {
30
+ const linePrefix = ctx.linePrefix ?? "";
31
+ if (!/\baction\s*:\s*\{\s*(eq|in)\s*:\s*\[?\s*[A-Za-z]*$/.test(linePrefix))
32
+ return [];
33
+ const prefix = (ctx.wordAtCursor ?? "").toLowerCase();
34
+ const registry = cedarRegistry();
35
+ return Object.entries(registry)
36
+ .filter(([, entry]) => entry.kind === "resource" && entry.resourceType.includes("::Action::"))
37
+ .filter(([className]) => !prefix || className.toLowerCase().startsWith(prefix))
38
+ .sort(([a], [b]) => a.localeCompare(b))
39
+ .map(([className, entry]) => ({
40
+ label: className,
41
+ insertText: className,
42
+ kind: "resource",
43
+ detail: entry.resourceType,
44
+ documentation: [
45
+ `Cedar action: \`${entry.resourceType}\``,
46
+ entry.principalTypes?.length ? `Principals: ${entry.principalTypes.join(", ")}` : "",
47
+ entry.resourceTypes?.length ? `Resources: ${entry.resourceTypes.join(", ")}` : "",
48
+ ]
49
+ .filter(Boolean)
50
+ .join("\n"),
51
+ }));
52
+ }