@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,592 @@
1
+ /**
2
+ * Typed builders for dogwood's temporal sub-language.
3
+ *
4
+ * These target the **parser primitives**, not the named aggregates. The #1657
5
+ * verification (and the correction comment on epic #1646) is explicit about
6
+ * why: `formerly`, `previous`, `since`, `exists`, `tp()`, `count for … where`
7
+ * and `sum … for … where` are the only temporal keywords in upstream's
8
+ * `extension/temporal/grammar.pest`. `count_within`, `sum_within`,
9
+ * `count_distinct_within` and `bind` are macros in `default_macros.dw` — a
10
+ * swappable standard library that a caller passing `--macros` replaces
11
+ * wholesale — and `once` is not even in that library, only in the examples.
12
+ *
13
+ * So the aggregates are expressible here as macro *calls* ({@link call}, and
14
+ * the convenience wrappers in `./macros.ts`), never as first-class operators.
15
+ * A call that resolves to nothing at the other end is a missing macro, which
16
+ * is a comprehensible failure; a first-class builder that silently emits a
17
+ * name the callee's library does not define is not.
18
+ *
19
+ * Everything below renders to the surface syntax the pest grammar accepts.
20
+ * Where the grammar's precedence would bind differently from the tree the
21
+ * author built, the renderer parenthesises — it never relies on the reader
22
+ * knowing that `!` binds tighter than `since`, or that an aggregate's `where`
23
+ * body is greedy.
24
+ */
25
+
26
+ import { escapeCedarString } from "../policy-text";
27
+ import {
28
+ renderWindow,
29
+ renderWindowValue,
30
+ window,
31
+ windowValue,
32
+ type TemporalWindow,
33
+ type WindowLike,
34
+ type WindowParam,
35
+ type WindowValue,
36
+ } from "./window";
37
+
38
+ /** What the three past-only operators accept: a window, or a macro's `?w`. */
39
+ export type WindowArgument = WindowLike | WindowParam;
40
+
41
+ // ── Names ─────────────────────────────────────────────────────────
42
+
43
+ /** A bare binder or field name. */
44
+ const IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
45
+ /** `?p` (call-site value splice) or `$t` (macro-introduced fresh binder). */
46
+ const SIGIL = /^[?$][A-Za-z_][A-Za-z0-9_]*$/;
47
+ /** `input.user`, `output.result` — the dotted path a predicate argument names. */
48
+ const FIELD_PATH = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/;
49
+ /** `Drupe::Action::"Read"` — a qualified action with a quoted id. */
50
+ const QUALIFIED_ACTION = /^[A-Za-z_][A-Za-z0-9_]*(::[A-Za-z_][A-Za-z0-9_]*)*::"[^"]*"$/;
51
+ /** `String`, `Long`, `Timepoint`, `MyApp::OAuthUser`. */
52
+ const TYPE_NAME = /^[A-Za-z_][A-Za-z0-9_]*(::[A-Za-z_][A-Za-z0-9_]*)*$/;
53
+
54
+ function assertName(pattern: RegExp, value: string, what: string): string {
55
+ if (!pattern.test(value)) throw new Error(`dogwood: ${what} — got "${value}"`);
56
+ return value;
57
+ }
58
+
59
+ /**
60
+ * A binder position: a plain identifier, or a macro sigil.
61
+ *
62
+ * The sigils are legal only inside a macro body; upstream's well-formedness
63
+ * pass rejects them elsewhere. `./macros.ts` is where they belong, and it is
64
+ * what carries that warning to the author.
65
+ */
66
+ export function binderName(value: string): string {
67
+ if (IDENT.test(value) || SIGIL.test(value)) return value;
68
+ throw new Error(
69
+ `dogwood: a binder must be an identifier, or a "?param"/"$binder" sigil inside a macro body — got "${value}"`,
70
+ );
71
+ }
72
+
73
+ // ── Terms ─────────────────────────────────────────────────────────
74
+
75
+ /** `(x: T)` — a declaration site in an `exists` or a `for` list. */
76
+ export interface TypedBinder {
77
+ readonly name: string;
78
+ readonly type: string;
79
+ }
80
+
81
+ /** `(t: Timepoint)`, `(amount: Long)`, `($t: Timepoint)` inside a macro. */
82
+ export function typedBinder(name: string, type: string): TypedBinder {
83
+ return { name: binderName(name), type: assertName(TYPE_NAME, type, "a binder type must be a Cedar type name") };
84
+ }
85
+
86
+ /** Anything that renders in term position. */
87
+ export type TemporalTerm = TermText | ArrayTerm | CountTerm | SumTerm | CallNode;
88
+
89
+ /** A leaf term already in its surface form (a literal, a path, an entity UID). */
90
+ export interface TermText {
91
+ readonly op: "term";
92
+ readonly text: string;
93
+ }
94
+
95
+ /** `[a, b, c]`. */
96
+ export interface ArrayTerm {
97
+ readonly op: "array";
98
+ readonly items: readonly TemporalTerm[];
99
+ }
100
+
101
+ /** `count for (t: Timepoint). where φ`. */
102
+ export interface CountTerm {
103
+ readonly op: "count";
104
+ readonly binders: readonly TypedBinder[];
105
+ readonly where: TemporalCondition;
106
+ }
107
+
108
+ /** `sum a for (a: Long), (t: Timepoint). where φ`. */
109
+ export interface SumTerm {
110
+ readonly op: "sum";
111
+ readonly over: string;
112
+ readonly binders: readonly TypedBinder[];
113
+ readonly where: TemporalCondition;
114
+ }
115
+
116
+ /**
117
+ * What a builder accepts in term position.
118
+ *
119
+ * Numbers and booleans lift to literals because there is only one thing they
120
+ * can mean. A bare `string` deliberately does not: `"alice"` is a Cedar string
121
+ * literal and `alice` is a binder reference, and guessing which one the author
122
+ * meant is how a policy silently stops matching. Say which with {@link str},
123
+ * {@link varRef}, {@link ctx} or {@link entityUid}.
124
+ */
125
+ export type TermInput = TemporalTerm | number | boolean;
126
+
127
+ function liftTerm(value: TermInput): TemporalTerm {
128
+ if (typeof value === "number") return int(value);
129
+ if (typeof value === "boolean") return bool(value);
130
+ return value;
131
+ }
132
+
133
+ /** A raw term, emitted verbatim. The escape hatch for a shape not modelled here. */
134
+ export function term(text: string): TermText {
135
+ return { op: "term", text };
136
+ }
137
+
138
+ /** A Cedar string literal: `"alice"`. */
139
+ export function str(value: string): TermText {
140
+ return term(`"${escapeCedarString(value)}"`);
141
+ }
142
+
143
+ /** An integer literal. Upstream's grammar admits a leading `-`. */
144
+ export function int(value: number): TermText {
145
+ if (!Number.isInteger(value)) {
146
+ throw new Error(`dogwood: an integer term must be a whole number — use decimalOf() for ${value}`);
147
+ }
148
+ return term(String(value));
149
+ }
150
+
151
+ /** `true` / `false`. */
152
+ export function bool(value: boolean): TermText {
153
+ return term(value ? "true" : "false");
154
+ }
155
+
156
+ /** `decimal("1.50")` — Cedar's decimal constructor, which dogwood admits as a term. */
157
+ export function decimalOf(value: string): TermText {
158
+ return term(`decimal("${escapeCedarString(value)}")`);
159
+ }
160
+
161
+ /** `context.input.user` — a field of the request context record. */
162
+ export function ctx(path: string): TermText {
163
+ assertName(FIELD_PATH, path, "a context path must be dotted identifiers");
164
+ return term(`context.${path}`);
165
+ }
166
+
167
+ /** `principal`, `resource`, `principal.dept` — the request scope entities. */
168
+ export function scopeRef(root: "principal" | "resource", ...attrs: string[]): TermText {
169
+ for (const attr of attrs) assertName(IDENT, attr, "a scope attribute must be an identifier");
170
+ return term([root, ...attrs].join("."));
171
+ }
172
+
173
+ /** `Drupe::OAuthUser::"alice"` — an entity UID literal. */
174
+ export function entityUid(uid: string): TermText {
175
+ assertName(QUALIFIED_ACTION, uid, 'an entity UID looks like Ns::Type::"id"');
176
+ return term(uid);
177
+ }
178
+
179
+ /** A bare binder reference — a `for`-declared variable, or a macro sigil. */
180
+ export function varRef(name: string): TermText {
181
+ return term(binderName(name));
182
+ }
183
+
184
+ /** `*` — the grammar's wildcard term. */
185
+ export function wildcard(): TermText {
186
+ return term("*");
187
+ }
188
+
189
+ /** `[a, b]`. */
190
+ export function arrayOf(...items: TermInput[]): ArrayTerm {
191
+ return { op: "array", items: items.map(liftTerm) };
192
+ }
193
+
194
+ /**
195
+ * `count for (t: Timepoint). where φ` — the primitive behind `count_within`.
196
+ *
197
+ * The `for` list is the aggregation domain and is mandatory in the grammar,
198
+ * so it is a required argument here too.
199
+ */
200
+ export function count(binders: TypedBinder[], where: TemporalCondition): CountTerm {
201
+ if (binders.length === 0) throw new Error("dogwood: count needs at least one typed binder in its for-list");
202
+ return { op: "count", binders, where };
203
+ }
204
+
205
+ /** `sum a for (a: Long), (t: Timepoint). where φ` — the primitive behind `sum_within`. */
206
+ export function sum(over: string, binders: TypedBinder[], where: TemporalCondition): SumTerm {
207
+ if (binders.length === 0) throw new Error("dogwood: sum needs at least one typed binder in its for-list");
208
+ return { op: "sum", over: binderName(over), binders, where };
209
+ }
210
+
211
+ // ── Conditions ────────────────────────────────────────────────────
212
+
213
+ /** Anything that renders in condition position. */
214
+ export type TemporalCondition =
215
+ | PredicateNode
216
+ | FormerlyNode
217
+ | PreviousNode
218
+ | SinceNode
219
+ | ExistsNode
220
+ | TpNode
221
+ | AndNode
222
+ | NotNode
223
+ | ComparisonNode
224
+ | CallNode
225
+ | SigilCondition
226
+ | RawCondition;
227
+
228
+ /** `Drupe::Action::"Login"::response{ input.user: context.input.user }`. */
229
+ export interface PredicateNode {
230
+ readonly op: "predicate";
231
+ readonly action: string;
232
+ readonly kind: string;
233
+ readonly args: ReadonlyArray<{ readonly path: string; readonly value: TemporalTerm }>;
234
+ }
235
+
236
+ /** `formerly within 1h φ`. */
237
+ export interface FormerlyNode {
238
+ readonly op: "formerly";
239
+ readonly window: WindowValue;
240
+ readonly body: TemporalCondition;
241
+ }
242
+
243
+ /** `previous within 30s φ`. */
244
+ export interface PreviousNode {
245
+ readonly op: "previous";
246
+ readonly window: WindowValue;
247
+ readonly body: TemporalCondition;
248
+ }
249
+
250
+ /** `φ since within 1h ψ` — infix, with the window on the operator. */
251
+ export interface SinceNode {
252
+ readonly op: "since";
253
+ readonly left: TemporalCondition;
254
+ readonly window: WindowValue;
255
+ readonly right: TemporalCondition;
256
+ }
257
+
258
+ /** `exists (total: Long). φ`. */
259
+ export interface ExistsNode {
260
+ readonly op: "exists";
261
+ readonly binder: TypedBinder;
262
+ readonly body: TemporalCondition;
263
+ }
264
+
265
+ /** `tp(t)` — binds the timepoint under evaluation. */
266
+ export interface TpNode {
267
+ readonly op: "tp";
268
+ readonly binder: string;
269
+ }
270
+
271
+ /** `φ && ψ`. */
272
+ export interface AndNode {
273
+ readonly op: "and";
274
+ readonly operands: readonly TemporalCondition[];
275
+ }
276
+
277
+ /** `!φ`. */
278
+ export interface NotNode {
279
+ readonly op: "not";
280
+ readonly body: TemporalCondition;
281
+ }
282
+
283
+ /** `a == b`, `0 < count …`. */
284
+ export interface ComparisonNode {
285
+ readonly op: "compare";
286
+ readonly left: TemporalTerm;
287
+ readonly operator: ComparisonOperator;
288
+ readonly right: TemporalTerm;
289
+ }
290
+
291
+ /** The six operators `cmp_op` admits. */
292
+ export type ComparisonOperator = "==" | "!=" | "<" | "<=" | ">" | ">=";
293
+
294
+ /** A macro invocation — `once(1h, φ)`. Legal in both condition and term position. */
295
+ export interface CallNode {
296
+ readonly op: "call";
297
+ readonly name: string;
298
+ readonly args: readonly MacroArg[];
299
+ }
300
+
301
+ /** `1h` as a macro-call argument — bare, with no `within` keyword. */
302
+ export interface IntervalArg {
303
+ readonly op: "interval";
304
+ readonly window: TemporalWindow;
305
+ }
306
+
307
+ /** What a macro call takes: an interval, a condition, or a term. */
308
+ export type MacroArg = IntervalArg | TemporalCondition | TemporalTerm;
309
+
310
+ /**
311
+ * Temporal source text emitted verbatim.
312
+ *
313
+ * The one builder that can produce something the walls exist to catch — a
314
+ * `formerly` with no window, an event kind the schema never declared. That is
315
+ * deliberate: an escape hatch nobody can reach makes the walls untestable, and
316
+ * DWDC012 reads the serialized text precisely because this exists.
317
+ */
318
+ export interface RawCondition {
319
+ readonly op: "raw";
320
+ readonly text: string;
321
+ }
322
+
323
+ /**
324
+ * `?s` — a macro parameter standing for an entire condition.
325
+ *
326
+ * Its own node rather than a {@link RawCondition} because the grammar treats
327
+ * it as an atom (`refinable = (predicate | sigil_cond) ~ field_block*`), so
328
+ * `formerly within ?w ?s` needs no parentheses where arbitrary raw text would.
329
+ * Build one with `macroCondition()` in `./macros.ts`, which is where the rule
330
+ * that sigils are legal only inside a macro body is documented.
331
+ */
332
+ export interface SigilCondition {
333
+ readonly op: "sigil";
334
+ readonly param: string;
335
+ }
336
+
337
+ // ── Condition builders ────────────────────────────────────────────
338
+
339
+ /**
340
+ * `Ns::Action::"Name"::kind{ field: term, … }`.
341
+ *
342
+ * The event kind is mandatory in the grammar and author-defined in practice —
343
+ * `request`/`response`/`error` are the conventional kinds the default event
344
+ * schema declares, not a fixed set. Which kinds are legal for a given policy
345
+ * set is the emitted `.dwschema`'s business, and DWDC010's.
346
+ */
347
+ export function predicate(
348
+ action: string,
349
+ kind: string,
350
+ args: Record<string, TermInput> = {},
351
+ ): PredicateNode {
352
+ assertName(QUALIFIED_ACTION, action, 'a predicate action looks like Ns::Action::"Name"');
353
+ assertName(IDENT, kind, "an event kind must be an identifier");
354
+ return {
355
+ op: "predicate",
356
+ action,
357
+ kind,
358
+ args: Object.entries(args).map(([path, value]) => ({
359
+ path: assertName(FIELD_PATH, path, "a predicate field path must be dotted identifiers"),
360
+ value: liftTerm(value),
361
+ })),
362
+ };
363
+ }
364
+
365
+ /** `formerly within <w> φ` — held at some point in the window. */
366
+ export function formerly(w: WindowArgument, body: TemporalCondition): FormerlyNode {
367
+ return { op: "formerly", window: windowValue(w), body };
368
+ }
369
+
370
+ /** `previous within <w> φ` — held at the immediately preceding timepoint in the window. */
371
+ export function previous(w: WindowArgument, body: TemporalCondition): PreviousNode {
372
+ return { op: "previous", window: windowValue(w), body };
373
+ }
374
+
375
+ /** `φ since within <w> ψ` — φ has held continuously since ψ, inside the window. */
376
+ export function since(left: TemporalCondition, w: WindowArgument, right: TemporalCondition): SinceNode {
377
+ return { op: "since", left, window: windowValue(w), right };
378
+ }
379
+
380
+ /** `exists (name: Type). φ`. */
381
+ export function exists(binder: TypedBinder, body: TemporalCondition): ExistsNode {
382
+ return { op: "exists", binder, body };
383
+ }
384
+
385
+ /** `tp(t)`. */
386
+ export function tp(binder: string): TpNode {
387
+ return { op: "tp", binder: binderName(binder) };
388
+ }
389
+
390
+ /** `φ && ψ && …`. A single operand collapses to itself. */
391
+ export function and(...operands: TemporalCondition[]): TemporalCondition {
392
+ if (operands.length === 0) throw new Error("dogwood: and() needs at least one operand");
393
+ if (operands.length === 1) return operands[0];
394
+ return { op: "and", operands };
395
+ }
396
+
397
+ /** `!φ`. */
398
+ export function not(body: TemporalCondition): NotNode {
399
+ return { op: "not", body };
400
+ }
401
+
402
+ /** `a <op> b`. */
403
+ export function compare(left: TermInput, operator: ComparisonOperator, right: TermInput): ComparisonNode {
404
+ return { op: "compare", left: liftTerm(left), operator, right: liftTerm(right) };
405
+ }
406
+
407
+ /**
408
+ * A macro invocation.
409
+ *
410
+ * Nothing here checks that the macro exists: the library is `--macros`, chosen
411
+ * by whoever runs `dogwood validate`, and chant is not that. `./macros.ts`
412
+ * carries typed wrappers for the four the default library ships.
413
+ */
414
+ export function call(name: string, args: MacroArg[] = []): CallNode {
415
+ assertName(IDENT, name, "a macro name must be an identifier");
416
+ return { op: "call", name, args };
417
+ }
418
+
419
+ /** `1h` in macro-call argument position — the interval without the `within`. */
420
+ export function interval(w: WindowLike): IntervalArg {
421
+ return { op: "interval", window: window(w) };
422
+ }
423
+
424
+ /** Temporal source text, passed through untouched. See {@link RawCondition}. */
425
+ export function raw(text: string): RawCondition {
426
+ return { op: "raw", text };
427
+ }
428
+
429
+ /** `?s` in condition position. See {@link SigilCondition}. */
430
+ export function sigilCondition(param: string): SigilCondition {
431
+ if (!SIGIL.test(param)) throw new Error(`dogwood: a condition sigil looks like "?s" — got "${param}"`);
432
+ return { op: "sigil", param };
433
+ }
434
+
435
+ // ── Rendering ─────────────────────────────────────────────────────
436
+
437
+ function isTerm(node: MacroArg): node is TemporalTerm {
438
+ return node.op === "term" || node.op === "array" || node.op === "count" || node.op === "sum";
439
+ }
440
+
441
+ function isAggregate(t: TemporalTerm): boolean {
442
+ return t.op === "count" || t.op === "sum";
443
+ }
444
+
445
+ /** Render a term. */
446
+ export function renderTerm(t: TemporalTerm): string {
447
+ switch (t.op) {
448
+ case "term":
449
+ return t.text;
450
+ case "array":
451
+ return `[${t.items.map(renderTerm).join(", ")}]`;
452
+ case "count":
453
+ return `count ${renderForBinders(t.binders)} where ${renderCondition(t.where)}`;
454
+ case "sum":
455
+ return `sum ${t.over} ${renderForBinders(t.binders)} where ${renderCondition(t.where)}`;
456
+ case "call":
457
+ return renderCall(t);
458
+ }
459
+ }
460
+
461
+ function renderForBinders(binders: readonly TypedBinder[]): string {
462
+ return `for ${binders.map((b) => `(${b.name}: ${b.type})`).join(", ")}.`;
463
+ }
464
+
465
+ /**
466
+ * A comparison operand.
467
+ *
468
+ * An aggregate's `where` body is greedy, so `count for (…). where φ == 3`
469
+ * reads `== 3` as part of φ. Upstream's `paren_agg` rule exists for exactly
470
+ * this and is always legal, so aggregates and macro calls are parenthesised on
471
+ * both sides rather than only where the greed would actually bite — the
472
+ * emitted text should not depend on which side of the operator it landed on.
473
+ */
474
+ function renderComparisonOperand(t: TemporalTerm): string {
475
+ return isAggregate(t) || t.op === "call" ? `(${renderTerm(t)})` : renderTerm(t);
476
+ }
477
+
478
+ function renderCall(node: CallNode): string {
479
+ const args = node.args.map((arg) => {
480
+ if (arg.op === "interval") return renderWindow(arg.window);
481
+ if (isTerm(arg)) return renderTerm(arg);
482
+ return renderCondition(arg);
483
+ });
484
+ return `${node.name}(${args.join(", ")})`;
485
+ }
486
+
487
+ /**
488
+ * An `atom` — the operand of `formerly`, `previous` and the right of `since`.
489
+ *
490
+ * The grammar's `atom` is `"(" condition ")" | tp_op | call | refinable |
491
+ * comparison`; everything else has to be parenthesised.
492
+ */
493
+ function renderAtom(node: TemporalCondition): string {
494
+ switch (node.op) {
495
+ case "predicate":
496
+ case "sigil":
497
+ case "tp":
498
+ case "call":
499
+ case "compare":
500
+ return renderCondition(node);
501
+ default:
502
+ return `(${renderCondition(node)})`;
503
+ }
504
+ }
505
+
506
+ /**
507
+ * A `neg_conjunct` — what `!` negates, and what sits left of `since`.
508
+ *
509
+ * `!` binds tighter than `since` and `&&`, so `!a since within W b` negates
510
+ * only `a`. Anything looser gets parentheses.
511
+ */
512
+ function renderNegConjunct(node: TemporalCondition): string {
513
+ switch (node.op) {
514
+ case "and":
515
+ case "since":
516
+ case "exists":
517
+ return `(${renderCondition(node)})`;
518
+ default:
519
+ return renderCondition(node);
520
+ }
521
+ }
522
+
523
+ /**
524
+ * A `conjunct_or_since` — one operand of an `&&` chain.
525
+ *
526
+ * `since` is allowed here bare (it is part of the same rule). `exists` binds
527
+ * maximally to the right, so an unparenthesised one would swallow every
528
+ * operand after it.
529
+ */
530
+ function renderConjunct(node: TemporalCondition): string {
531
+ switch (node.op) {
532
+ case "and":
533
+ case "exists":
534
+ return `(${renderCondition(node)})`;
535
+ default:
536
+ return renderCondition(node);
537
+ }
538
+ }
539
+
540
+ /** Render a temporal condition to the surface syntax the pest grammar accepts. */
541
+ export function renderCondition(node: TemporalCondition): string {
542
+ switch (node.op) {
543
+ case "raw":
544
+ return node.text;
545
+
546
+ case "sigil":
547
+ return node.param;
548
+
549
+ case "predicate": {
550
+ const args = node.args.map((a) => `${a.path}: ${renderTerm(a.value)}`).join(", ");
551
+ return `${node.action}::${node.kind}{${args.length > 0 ? ` ${args} ` : ""}}`;
552
+ }
553
+
554
+ case "formerly":
555
+ return `formerly within ${renderWindowValue(node.window)} ${renderAtom(node.body)}`;
556
+
557
+ case "previous":
558
+ return `previous within ${renderWindowValue(node.window)} ${renderAtom(node.body)}`;
559
+
560
+ case "since":
561
+ return `${renderNegConjunct(node.left)} since within ${renderWindowValue(node.window)} ${renderAtom(node.right)}`;
562
+
563
+ case "exists":
564
+ return `exists (${node.binder.name}: ${node.binder.type}). ${renderCondition(node.body)}`;
565
+
566
+ case "tp":
567
+ return `tp(${node.binder})`;
568
+
569
+ case "and":
570
+ return node.operands.map(renderConjunct).join(" && ");
571
+
572
+ case "not":
573
+ return `!${renderNegConjunct(node.body)}`;
574
+
575
+ case "compare":
576
+ return `${renderComparisonOperand(node.left)} ${node.operator} ${renderComparisonOperand(node.right)}`;
577
+
578
+ case "call":
579
+ return renderCall(node);
580
+ }
581
+ }
582
+
583
+ /**
584
+ * `temporal { … }` — the extension marker.
585
+ *
586
+ * It is a `primary` in the Cedar expression grammar as well as a clause tag,
587
+ * so this is also how a temporal condition is embedded mid-expression:
588
+ * `when { is_small(context.input.shares) && temporal { once(1h, …) } }`.
589
+ */
590
+ export function temporalMarker(condition: TemporalCondition): string {
591
+ return `temporal { ${renderCondition(condition)} }`;
592
+ }
@@ -0,0 +1,17 @@
1
+ // Adapted from dogwood-policy/dogwood@5063bcc2d6d6cf5024d1b0498e6cc8ef52cbcf0c (Apache-2.0):
2
+ // dogwood-docs/examples/login_attempt_custom_kind/event.dwschema
3
+ // Upstream's shape; the bytes are what chant's typed builders emit.
4
+
5
+ // Author-defined event kinds — `attempt` decides, `outcome` is history — and
6
+ // a renamed injected principal field.
7
+
8
+ decision event <A>::attempt {
9
+ ...inputs(A),
10
+ actor: principalType(A),
11
+ }
12
+
13
+ event <A>::outcome {
14
+ ...inputs(A),
15
+ ...outputs(A),
16
+ actor: principalType(A),
17
+ }
@@ -0,0 +1,23 @@
1
+ // Adapted from dogwood-policy/dogwood@5063bcc2d6d6cf5024d1b0498e6cc8ef52cbcf0c (Apache-2.0):
2
+ // dogwood-language/configuration/default_macros.dw
3
+ // Upstream's shape; the bytes are what chant's typed builders emit.
4
+
5
+ // Counts the timepoints within window `?w` at which condition `?s` held.
6
+ def temporal count_within(?w, ?s) {
7
+ count for ($t: Timepoint). where (formerly within ?w (?s && tp($t)))
8
+ };
9
+
10
+ // Sums the numeric value `?a` over occurrences of `?body` within window `?w`.
11
+ def temporal sum_within(?a, ?w, ?body) {
12
+ sum ?a for (?a: Long), ($t: Timepoint). where (formerly within ?w (?body && tp($t)))
13
+ };
14
+
15
+ // Counts the distinct key values `?k` for which `?s` held within window `?w`.
16
+ def temporal count_distinct_within(?k, ?w, ?s) {
17
+ count for (?k: String). where (formerly within ?w ?s)
18
+ };
19
+
20
+ // A "let"-style binder: names an aggregate result `?n` and uses it in `?B`.
21
+ def temporal bind(?n, ?A, ?B) {
22
+ exists (?n: Long). (?A == ?n && ?B)
23
+ };
@@ -0,0 +1,13 @@
1
+ {
2
+ "cedar_policies": "@id(\"read_after_login\")\npermit (\n principal,\n action == Drupe::Action::\"Read\",\n resource\n)\nwhen { context.policy_0__temporal_0 };\n",
3
+ "cedar_schema": "namespace Drupe {\n type LoginInput = { user: String };\n type ReadInput = { user: String };\n entity Gateway;\n entity OAuthUser = { id: String } tags String;\n action \"Login\" appliesTo {\n principal: [OAuthUser],\n resource: [Gateway],\n context: { input: LoginInput }\n };\n action \"Read\" appliesTo {\n principal: [OAuthUser],\n resource: [Gateway],\n context: { input: ReadInput, policy_0__temporal_0: __cedar::Bool }\n };\n}\n",
4
+ "cedar_schema_json": "{\"Drupe\":{\"commonTypes\":{\"LoginInput\":{\"type\":\"Record\",\"attributes\":{\"user\":{\"type\":\"EntityOrCommon\",\"name\":\"String\"}}},\"ReadInput\":{\"type\":\"Record\",\"attributes\":{\"user\":{\"type\":\"EntityOrCommon\",\"name\":\"String\"}}}},\"entityTypes\":{\"Gateway\":{},\"OAuthUser\":{\"shape\":{\"type\":\"Record\",\"attributes\":{\"id\":{\"type\":\"EntityOrCommon\",\"name\":\"String\"}}},\"tags\":{\"type\":\"EntityOrCommon\",\"name\":\"String\"}}},\"actions\":{\"Login\":{\"appliesTo\":{\"resourceTypes\":[\"Gateway\"],\"principalTypes\":[\"OAuthUser\"],\"context\":{\"type\":\"Record\",\"attributes\":{\"input\":{\"type\":\"EntityOrCommon\",\"name\":\"LoginInput\"}}}}},\"Read\":{\"appliesTo\":{\"resourceTypes\":[\"Gateway\"],\"principalTypes\":[\"OAuthUser\"],\"context\":{\"type\":\"Record\",\"attributes\":{\"input\":{\"type\":\"EntityOrCommon\",\"name\":\"ReadInput\"},\"policy_0__temporal_0\":{\"type\":\"EntityOrCommon\",\"name\":\"__cedar::Bool\"}}}}}}}}",
5
+ "self_contained": false,
6
+ "temporal_fields": [
7
+ "policy_0__temporal_0"
8
+ ],
9
+ "provider_fields": [],
10
+ "decision_kinds": [
11
+ "request"
12
+ ]
13
+ }
@@ -0,0 +1,25 @@
1
+ // Adapted from dogwood-policy/dogwood@5063bcc2d6d6cf5024d1b0498e6cc8ef52cbcf0c (Apache-2.0):
2
+ // dogwood-docs/examples/max_window_raised/event.dwschema
3
+ // Upstream's shape; the bytes are what chant's typed builders emit.
4
+
5
+ // Raises the look-back cap from the 24h default to 30 days. The directive
6
+ // must come first, before any event declaration.
7
+ //
8
+ // No pinned field: this schema correlates across principals on purpose.
9
+
10
+ max_window = 30d
11
+
12
+ decision event <A>::request {
13
+ ...inputs(A),
14
+ callerPrincipal: principalType(A),
15
+ callerResource: resourceType(A),
16
+ requestId: String,
17
+ }
18
+
19
+ event <A>::response {
20
+ ...inputs(A),
21
+ ...outputs(A),
22
+ callerPrincipal: principalType(A),
23
+ callerResource: resourceType(A),
24
+ requestId: String,
25
+ }