@intentius/chant-lexicon-cedar 0.44.8 → 0.44.9

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 (108) hide show
  1. package/README.md +58 -0
  2. package/dist/codegen/package.d.ts.map +1 -1
  3. package/dist/config.d.ts +25 -0
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/dogwood/cli.d.ts +206 -0
  6. package/dist/dogwood/cli.d.ts.map +1 -0
  7. package/dist/dogwood/event-schema.d.ts +161 -0
  8. package/dist/dogwood/event-schema.d.ts.map +1 -0
  9. package/dist/dogwood/index.d.ts +33 -0
  10. package/dist/dogwood/index.d.ts.map +1 -0
  11. package/dist/dogwood/macros.d.ts +96 -0
  12. package/dist/dogwood/macros.d.ts.map +1 -0
  13. package/dist/dogwood/policy.d.ts +120 -0
  14. package/dist/dogwood/policy.d.ts.map +1 -0
  15. package/dist/dogwood/scan.d.ts +109 -0
  16. package/dist/dogwood/scan.d.ts.map +1 -0
  17. package/dist/dogwood/serialize.d.ts +46 -0
  18. package/dist/dogwood/serialize.d.ts.map +1 -0
  19. package/dist/dogwood/temporal.d.ts +259 -0
  20. package/dist/dogwood/temporal.d.ts.map +1 -0
  21. package/dist/dogwood/upstream.d.ts +41 -0
  22. package/dist/dogwood/upstream.d.ts.map +1 -0
  23. package/dist/dogwood/window.d.ts +73 -0
  24. package/dist/dogwood/window.d.ts.map +1 -0
  25. package/dist/index.d.ts +4 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/integrity.json +13 -3
  28. package/dist/lint/audit-catalog.d.ts.map +1 -1
  29. package/dist/lint/post-synth/dogwood-helpers.d.ts +63 -0
  30. package/dist/lint/post-synth/dogwood-helpers.d.ts.map +1 -0
  31. package/dist/lint/post-synth/dwdc010.d.ts +25 -0
  32. package/dist/lint/post-synth/dwdc010.d.ts.map +1 -0
  33. package/dist/lint/post-synth/dwdc011.d.ts +19 -0
  34. package/dist/lint/post-synth/dwdc011.d.ts.map +1 -0
  35. package/dist/lint/post-synth/dwdc012.d.ts +21 -0
  36. package/dist/lint/post-synth/dwdc012.d.ts.map +1 -0
  37. package/dist/lint/post-synth/dwde010.d.ts +32 -0
  38. package/dist/lint/post-synth/dwde010.d.ts.map +1 -0
  39. package/dist/lint/post-synth/dwde011.d.ts +33 -0
  40. package/dist/lint/post-synth/dwde011.d.ts.map +1 -0
  41. package/dist/lint/post-synth/dwds010.d.ts +24 -0
  42. package/dist/lint/post-synth/dwds010.d.ts.map +1 -0
  43. package/dist/lint/post-synth/index.d.ts.map +1 -1
  44. package/dist/manifest.json +1 -1
  45. package/dist/okf/index.md +6 -0
  46. package/dist/okf/rules/DWDC010.md +11 -0
  47. package/dist/okf/rules/DWDC011.md +11 -0
  48. package/dist/okf/rules/DWDC012.md +11 -0
  49. package/dist/okf/rules/DWDE010.md +11 -0
  50. package/dist/okf/rules/DWDE011.md +11 -0
  51. package/dist/okf/rules/DWDS010.md +11 -0
  52. package/dist/policy-text.d.ts +53 -0
  53. package/dist/policy-text.d.ts.map +1 -0
  54. package/dist/rules/dogwood-helpers.ts +139 -0
  55. package/dist/rules/dwdc010.ts +62 -0
  56. package/dist/rules/dwdc011.ts +61 -0
  57. package/dist/rules/dwdc012.ts +46 -0
  58. package/dist/rules/dwde010.ts +130 -0
  59. package/dist/rules/dwde011.ts +108 -0
  60. package/dist/rules/dwds010.ts +46 -0
  61. package/dist/serializer.d.ts +10 -18
  62. package/dist/serializer.d.ts.map +1 -1
  63. package/dist/skills/chant-cedar-authoring.md +180 -0
  64. package/dist/skills/chant-cedar-avp-embedding.md +125 -0
  65. package/dist/skills/chant-cedar-meta-policy.md +119 -0
  66. package/package.json +2 -2
  67. package/src/codegen/package.ts +3 -2
  68. package/src/config.test.ts +12 -0
  69. package/src/config.ts +28 -0
  70. package/src/dogwood/cli.test.ts +392 -0
  71. package/src/dogwood/cli.ts +545 -0
  72. package/src/dogwood/event-schema.test.ts +218 -0
  73. package/src/dogwood/event-schema.ts +318 -0
  74. package/src/dogwood/index.ts +198 -0
  75. package/src/dogwood/macros.test.ts +104 -0
  76. package/src/dogwood/macros.ts +229 -0
  77. package/src/dogwood/policy.test.ts +94 -0
  78. package/src/dogwood/policy.ts +141 -0
  79. package/src/dogwood/scan.ts +287 -0
  80. package/src/dogwood/serialize.test.ts +331 -0
  81. package/src/dogwood/serialize.ts +209 -0
  82. package/src/dogwood/temporal.test.ts +272 -0
  83. package/src/dogwood/temporal.ts +592 -0
  84. package/src/dogwood/testdata/custom-kinds.dwschema +17 -0
  85. package/src/dogwood/testdata/default-macros.dw +23 -0
  86. package/src/dogwood/testdata/lowered-read-after-login.json +13 -0
  87. package/src/dogwood/testdata/max-window-raised.dwschema +25 -0
  88. package/src/dogwood/testdata/pinned.dwschema +31 -0
  89. package/src/dogwood/testdata/read-after-login.cedarschema +20 -0
  90. package/src/dogwood/testdata/read-after-login.dw +17 -0
  91. package/src/dogwood/testdata/temporal-policies.dw +53 -0
  92. package/src/dogwood/upstream.ts +41 -0
  93. package/src/dogwood/window.ts +124 -0
  94. package/src/index.ts +24 -0
  95. package/src/lint/audit-catalog.ts +56 -0
  96. package/src/lint/post-synth/dogwood-helpers.ts +139 -0
  97. package/src/lint/post-synth/dwd-post-synth.test.ts +256 -0
  98. package/src/lint/post-synth/dwdc010.ts +62 -0
  99. package/src/lint/post-synth/dwdc011.ts +61 -0
  100. package/src/lint/post-synth/dwdc012.ts +46 -0
  101. package/src/lint/post-synth/dwde-post-synth.test.ts +368 -0
  102. package/src/lint/post-synth/dwde010.ts +130 -0
  103. package/src/lint/post-synth/dwde011.ts +108 -0
  104. package/src/lint/post-synth/dwds010.ts +46 -0
  105. package/src/lint/post-synth/index.ts +12 -0
  106. package/src/lint/post-synth/post-synth.test.ts +7 -3
  107. package/src/policy-text.ts +128 -0
  108. package/src/serializer.ts +71 -109
@@ -0,0 +1,392 @@
1
+ /**
2
+ * The `dogwood` CLI adapter (#1659).
3
+ *
4
+ * Every test here injects a runner. Nothing in the gating suite executes a
5
+ * real binary — the epic's own rule ("nothing in gating CI executes the
6
+ * dogwood binary"), and the reason the adapter takes a runner at all. The
7
+ * JSON fixtures are upstream's two report shapes, read from the pinned
8
+ * `dogwood-cli/src/ops.rs` and `dogwood-cli/src/error.rs` structs rather than
9
+ * from the human text, which is the surface most likely to be retuned by a
10
+ * sync.
11
+ */
12
+ import { describe, test, expect, afterEach } from "vitest";
13
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
14
+ import { tmpdir } from "os";
15
+ import { join } from "path";
16
+ import { fileURLToPath } from "url";
17
+ import {
18
+ DOGWOOD_BINARY_ENV,
19
+ configureDogwoodCli,
20
+ findDogwoodBinary,
21
+ formatDogwoodDiagnostic,
22
+ parseLowerOutput,
23
+ parseValidateOutput,
24
+ resetDogwoodCli,
25
+ runDogwoodLower,
26
+ runDogwoodValidate,
27
+ type DogwoodRun,
28
+ type DogwoodRunner,
29
+ } from "./cli";
30
+
31
+ const TESTDATA = join(fileURLToPath(new URL(".", import.meta.url)), "testdata");
32
+
33
+ /**
34
+ * A real `dogwood lower --format json` artifact set, adapted from
35
+ * dogwood-policy/dogwood@5063bcc2d6d6cf5024d1b0498e6cc8ef52cbcf0c (Apache-2.0),
36
+ * `dogwood-docs/examples/read_after_login`.
37
+ */
38
+ const LOWERED_READ_AFTER_LOGIN: unknown = JSON.parse(
39
+ readFileSync(join(TESTDATA, "lowered-read-after-login.json"), "utf-8"),
40
+ );
41
+
42
+ afterEach(() => {
43
+ resetDogwoodCli();
44
+ delete process.env[DOGWOOD_BINARY_ENV];
45
+ });
46
+
47
+ function run(over: Partial<DogwoodRun>): DogwoodRun {
48
+ return { status: 0, stdout: "", stderr: "", ...over };
49
+ }
50
+
51
+ function json(value: unknown, status = 0): DogwoodRun {
52
+ return run({ status, stdout: JSON.stringify(value) });
53
+ }
54
+
55
+ // ── The validate report shape ──────────────────────────────────────
56
+
57
+ describe("parseValidateOutput — the ValidateReport shape", () => {
58
+ test("a clean report is a pass", () => {
59
+ const result = parseValidateOutput(
60
+ json({ passed: true, passed_without_warnings: true, errors: [], warnings: [] }),
61
+ );
62
+ expect(result).toEqual({ kind: "passed", warnings: [] });
63
+ });
64
+
65
+ test("warnings do not fail the report, and are carried through", () => {
66
+ const result = parseValidateOutput(
67
+ json({
68
+ passed: true,
69
+ passed_without_warnings: false,
70
+ errors: [],
71
+ warnings: [{ severity: "warning", message: "policy is impossible", spanned: false }],
72
+ }),
73
+ );
74
+ expect(result.kind).toBe("passed");
75
+ if (result.kind !== "passed") return;
76
+ expect(result.warnings).toHaveLength(1);
77
+ expect(result.warnings[0].message).toBe("policy is impossible");
78
+ });
79
+
80
+ test("a rejection carries every finding, with its code, help and byte spans", () => {
81
+ const result = parseValidateOutput(
82
+ json(
83
+ {
84
+ passed: false,
85
+ passed_without_warnings: false,
86
+ errors: [
87
+ {
88
+ severity: "error",
89
+ code: "extension",
90
+ message:
91
+ 'predicate `Drupe::Action::"NoSuchAction"::request` does not name a declared event',
92
+ labels: [{ start: 120, len: 42, message: "here" }],
93
+ spanned: true,
94
+ },
95
+ {
96
+ severity: "error",
97
+ code: "cedar",
98
+ message: "attribute `input.nosuchfield` in context for Drupe::Action::\"Read\" not found",
99
+ labels: [],
100
+ help: "did you mean `user`?",
101
+ spanned: false,
102
+ },
103
+ ],
104
+ warnings: [],
105
+ },
106
+ 2,
107
+ ),
108
+ );
109
+ expect(result.kind).toBe("rejected");
110
+ if (result.kind !== "rejected") return;
111
+ expect(result.errors).toHaveLength(2);
112
+ expect(result.errors[0].code).toBe("extension");
113
+ expect(result.errors[0].labels).toEqual([{ start: 120, len: 42, message: "here" }]);
114
+ expect(result.errors[1].help).toBe("did you mean `user`?");
115
+ });
116
+
117
+ test("the report wins over the exit code — passed:false at exit 0 is still a rejection", () => {
118
+ const result = parseValidateOutput(
119
+ json({
120
+ passed: false,
121
+ passed_without_warnings: false,
122
+ errors: [{ severity: "error", message: "nope", spanned: false }],
123
+ warnings: [],
124
+ }),
125
+ );
126
+ expect(result.kind).toBe("rejected");
127
+ });
128
+
129
+ test("and in the other direction — a clean report at a non-zero exit is still a pass", () => {
130
+ const result = parseValidateOutput(
131
+ json({ passed: true, passed_without_warnings: true, errors: [], warnings: [] }, 2),
132
+ );
133
+ expect(result.kind).toBe("passed");
134
+ });
135
+ });
136
+
137
+ // ── The fatal OpError shape ────────────────────────────────────────
138
+
139
+ describe("parseValidateOutput — the OpError shape", () => {
140
+ test("a fatal parse error is its own arm, not an entry in errors[]", () => {
141
+ const result = parseValidateOutput(
142
+ json(
143
+ {
144
+ severity: "error",
145
+ message: "expected `within` after `formerly`",
146
+ labels: [{ start: 64, len: 8 }],
147
+ spanned: true,
148
+ related: [{ severity: "error", message: "and a second parse error", spanned: false }],
149
+ },
150
+ 2,
151
+ ),
152
+ );
153
+ expect(result.kind).toBe("fatal");
154
+ if (result.kind !== "fatal") return;
155
+ expect(result.error.message).toBe("expected `within` after `formerly`");
156
+ expect(result.error.labels).toEqual([{ start: 64, len: 8 }]);
157
+ expect(result.related).toHaveLength(1);
158
+ });
159
+
160
+ test("a span-less fatal (the OpError::message path) still parses", () => {
161
+ const result = parseValidateOutput(
162
+ json({ severity: "error", message: "rhai implementation has no script", spanned: false }, 2),
163
+ );
164
+ expect(result.kind).toBe("fatal");
165
+ });
166
+
167
+ test("an internal `at line N column M` tail is not passed through", () => {
168
+ const result = parseValidateOutput(
169
+ json({ severity: "error", message: "missing field `policies` at line 1 column 42", spanned: false }, 2),
170
+ );
171
+ expect(result.kind).toBe("fatal");
172
+ if (result.kind !== "fatal") return;
173
+ expect(result.error.message).toBe("missing field `policies`");
174
+ });
175
+ });
176
+
177
+ // ── The exit-2 ambiguity ───────────────────────────────────────────
178
+
179
+ describe("parseValidateOutput — exit 2 is not a verdict", () => {
180
+ test("an unknown flag exits 2 with nothing on stdout, and is unusable rather than rejected", () => {
181
+ // clap's own usage error. The published guide claims exit 1 for a bad
182
+ // flag; the binary spends 2 on it, the same code a rejected policy set
183
+ // gets. Reading the code alone would fail the build over a flag rename.
184
+ const result = parseValidateOutput(
185
+ run({
186
+ status: 2,
187
+ stdout: "",
188
+ stderr: "error: unexpected argument '--nosuchflag' found\n\nUsage: dogwood validate <POLICIES> --policy-schema <FILE>",
189
+ }),
190
+ );
191
+ expect(result.kind).toBe("unusable");
192
+ if (result.kind !== "unusable") return;
193
+ expect(result.reason).toContain("without a JSON report on stdout");
194
+ expect(result.reason).toContain("--nosuchflag");
195
+ });
196
+
197
+ test("a spawn failure is unusable, not a verdict", () => {
198
+ const result = parseValidateOutput(run({ status: null, error: "spawnSync dogwood ENOENT" }));
199
+ expect(result.kind).toBe("unusable");
200
+ if (result.kind !== "unusable") return;
201
+ expect(result.reason).toContain("ENOENT");
202
+ });
203
+
204
+ test("output that is not JSON at all is unusable", () => {
205
+ const result = parseValidateOutput(run({ status: 0, stdout: "OK: validation passed with no errors." }));
206
+ expect(result.kind).toBe("unusable");
207
+ });
208
+
209
+ test("JSON in neither shape is unusable", () => {
210
+ const result = parseValidateOutput(json({ something: "else" }, 2));
211
+ expect(result.kind).toBe("unusable");
212
+ if (result.kind !== "unusable") return;
213
+ expect(result.reason).toContain("neither the report nor the error shape");
214
+ });
215
+ });
216
+
217
+ // ── Rendering ──────────────────────────────────────────────────────
218
+
219
+ describe("formatDogwoodDiagnostic", () => {
220
+ test("renders code, message, help and byte ranges on one line", () => {
221
+ const text = formatDogwoodDiagnostic({
222
+ severity: "error",
223
+ code: "extension",
224
+ message: "temporal window `48h` exceeds the maximum allowed window `24h`",
225
+ labels: [{ start: 100, len: 20, message: "this window" }],
226
+ help: "raise max_window",
227
+ spanned: true,
228
+ });
229
+ expect(text).toBe(
230
+ "[extension] temporal window `48h` exceeds the maximum allowed window `24h` (raise max_window) at bytes 100-120: this window",
231
+ );
232
+ });
233
+
234
+ test("a span-less finding renders as just its message", () => {
235
+ expect(formatDogwoodDiagnostic({ severity: "error", message: "bare", labels: [], spanned: false })).toBe(
236
+ "bare",
237
+ );
238
+ });
239
+ });
240
+
241
+ // ── lower ──────────────────────────────────────────────────────────
242
+
243
+ describe("parseLowerOutput", () => {
244
+ test("reads the artifact shape, including the JSON schema as a string", () => {
245
+ const result = parseLowerOutput(run({ status: 0, stdout: JSON.stringify(LOWERED_READ_AFTER_LOGIN) }));
246
+ expect(result.kind).toBe("lowered");
247
+ if (result.kind !== "lowered") return;
248
+ expect(result.value.cedarPolicies).toContain("context.policy_0__temporal_0");
249
+ expect(result.value.temporalFields).toEqual(["policy_0__temporal_0"]);
250
+ // False because a temporal field was hoisted — the lowered Cedar needs
251
+ // dogwood at authorize time to fill the slot.
252
+ expect(result.value.selfContained).toBe(false);
253
+ expect(JSON.parse(result.value.cedarSchemaJson)).toHaveProperty("Drupe");
254
+ });
255
+
256
+ test("a failed lower comes back through the fatal channel", () => {
257
+ const result = parseLowerOutput(
258
+ json({ severity: "error", message: "unknown macro `once`", labels: [], spanned: false }, 2),
259
+ );
260
+ expect(result.kind).toBe("fatal");
261
+ });
262
+ });
263
+
264
+ // ── Invocation ─────────────────────────────────────────────────────
265
+
266
+ describe("the invocation itself", () => {
267
+ test("materializes the bundle and passes every schema half by path", () => {
268
+ let seen: { args: string[]; files: Record<string, string> } | undefined;
269
+ const runner: DogwoodRunner = (_binary, args) => {
270
+ const files: Record<string, string> = {};
271
+ for (let i = 0; i < args.length; i++) {
272
+ const value = args[i];
273
+ if (value.startsWith("/") || value.includes("chant-dogwood-")) {
274
+ try {
275
+ files[value.replace(/^.*\//, "")] = readFileSync(value, "utf-8");
276
+ } catch {
277
+ // not a path we wrote
278
+ }
279
+ }
280
+ }
281
+ seen = { args, files };
282
+ return json({ passed: true, passed_without_warnings: true, errors: [], warnings: [] });
283
+ };
284
+ configureDogwoodCli({ runner });
285
+
286
+ const result = runDogwoodValidate("/opt/dogwood", {
287
+ policies: "permit (principal, action, resource);",
288
+ policySchema: "namespace Ns {}",
289
+ eventSchema: "decision event <A>::request {}",
290
+ macros: "def temporal once(?w, ?s) { formerly within ?w ?s };",
291
+ });
292
+
293
+ expect(result.kind).toBe("passed");
294
+ expect(seen?.args[0]).toBe("validate");
295
+ expect(seen?.args).toContain("--policy-schema");
296
+ expect(seen?.args).toContain("--event-schema");
297
+ expect(seen?.args).toContain("--macros");
298
+ expect(seen?.args.slice(-2)).toEqual(["--format", "json"]);
299
+ expect(seen?.files["policies.dw"]).toBe("permit (principal, action, resource);");
300
+ expect(seen?.files["events.dwschema"]).toBe("decision event <A>::request {}");
301
+ });
302
+
303
+ test("omits the service-schema flags a bundle does not carry", () => {
304
+ let args: string[] = [];
305
+ configureDogwoodCli({
306
+ runner: (_b, a) => {
307
+ args = a;
308
+ return json({ passed: true, passed_without_warnings: true, errors: [], warnings: [] });
309
+ },
310
+ });
311
+ runDogwoodValidate("dogwood", { policies: "// none", policySchema: "namespace Ns {}" });
312
+ expect(args).not.toContain("--event-schema");
313
+ expect(args).not.toContain("--macros");
314
+ expect(args).not.toContain("--providers");
315
+ });
316
+
317
+ test("never passes --emit, which the CLI ignores under --format json", () => {
318
+ let args: string[] = [];
319
+ configureDogwoodCli({
320
+ runner: (_b, a) => {
321
+ args = a;
322
+ return run({ status: 0, stdout: JSON.stringify(LOWERED_READ_AFTER_LOGIN) });
323
+ },
324
+ });
325
+ runDogwoodLower("dogwood", { policies: "// none", policySchema: "namespace Ns {}" });
326
+ expect(args[0]).toBe("lower");
327
+ expect(args).not.toContain("--emit");
328
+ });
329
+
330
+ test("a throwing runner is an unusable result, not an exception", () => {
331
+ configureDogwoodCli({
332
+ runner: () => {
333
+ throw new Error("boom");
334
+ },
335
+ });
336
+ const result = runDogwoodValidate("dogwood", { policies: "", policySchema: "" });
337
+ expect(result.kind).toBe("unusable");
338
+ });
339
+ });
340
+
341
+ // ── Locating the binary ────────────────────────────────────────────
342
+
343
+ describe("findDogwoodBinary", () => {
344
+ test("an explicit override wins and is reported as such", () => {
345
+ configureDogwoodCli({ binary: "/opt/dogwood/bin/dogwood" });
346
+ expect(findDogwoodBinary()).toEqual({ path: "/opt/dogwood/bin/dogwood", source: "override" });
347
+ });
348
+
349
+ test("a null override forces absence, so a machine that has one still tests the advisory", () => {
350
+ configureDogwoodCli({ binary: null });
351
+ expect(findDogwoodBinary()).toBeUndefined();
352
+ });
353
+
354
+ test("the environment variable is honoured when it names something executable", () => {
355
+ // `process.execPath` is a real executable on every platform the suite runs
356
+ // on, and standing in for the binary here costs nothing — resolution is an
357
+ // access check, not a run.
358
+ process.env[DOGWOOD_BINARY_ENV] = process.execPath;
359
+ expect(findDogwoodBinary()).toEqual({ path: process.execPath, source: "env" });
360
+ });
361
+
362
+ test("an environment variable pointing at nothing resolves past it", () => {
363
+ process.env[DOGWOOD_BINARY_ENV] = "/nonexistent/dogwood";
364
+ const found = findDogwoodBinary();
365
+ expect(found?.source).not.toBe("env");
366
+ });
367
+
368
+ test("cedar.dogwood.binary in a chant.config.json is read, walking up from the start directory", () => {
369
+ const root = mkdtempSync(join(tmpdir(), "chant-dogwood-config-"));
370
+ try {
371
+ writeFileSync(
372
+ join(root, "chant.config.json"),
373
+ JSON.stringify({ cedar: { dogwood: { binary: process.execPath } } }),
374
+ );
375
+ const nested = join(root, "src", "stack");
376
+ mkdirSync(nested, { recursive: true });
377
+ expect(findDogwoodBinary(nested)).toEqual({ path: process.execPath, source: "config" });
378
+ } finally {
379
+ rmSync(root, { recursive: true, force: true });
380
+ }
381
+ });
382
+
383
+ test("an unparseable chant.config.json is walked past rather than thrown from", () => {
384
+ const root = mkdtempSync(join(tmpdir(), "chant-dogwood-config-"));
385
+ try {
386
+ writeFileSync(join(root, "chant.config.json"), "{ not json");
387
+ expect(() => findDogwoodBinary(root)).not.toThrow();
388
+ } finally {
389
+ rmSync(root, { recursive: true, force: true });
390
+ }
391
+ });
392
+ });