Typed $-references
DollarPaths gives condition values typed autocomplete for cross-field comparisons - where they are accepted, how they resolve, and where they silently do not
A condition value that starts with $ is not a literal. At evaluation time the engine strips the $ and resolves the rest as a dot-path against the current request, so a rule can compare one field to another instead of to a constant. With a typed context, those references autocomplete. This page covers the type that produces them, the runtime that consumes them, and the three places where the two do not line up.
The type
export type DollarPaths<TContext> = `$${DotPaths<TContext>}`
export type FlexibleDollarPaths<TContext> = DollarPaths<TContext> | (string & {})
DollarPaths prefixes every member of DotPath.DotPaths<TContext> with $. For a context of { a: string; b: { c: number } } it is '$a' | '$b' | '$b.c', which the type-level test pins exactly.
FlexibleDollarPaths is the one the builders use. The (string & {}) arm is what keeps the literal suggestions visible: a bare string in a union swallows the literals and the IDE shows nothing, while string & {} is treated as a distinct branch so the editor still lists $subject.id, $resource.attributes.ownerId, and the rest. It is spliced directly into each method signature rather than nested inside a computed type, because TypeScript only surfaces the literals when the union is visible at the signature site.
access
.definePolicy('owner-only')
.rule('deny-non-owner', (r) =>
r
.deny()
.on('update', 'delete')
.of('post')
.when((w) =>
w
.resourceAttr('ownerId', 'neq', '$subject.id')
.attr('department', 'eq', '$resource.attributes.department'),
),
)
.build()
Build time versus evaluation time
Nothing about a $-reference is resolved while you build a policy. The string is stored verbatim in ICondition.value and serialises to JSON unchanged, which is why a policy loaded from a database behaves identically to one built in code.
resolveValue is the whole mechanism: if the value is a string that starts with <MathMl mathml="<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal">‘</mi><mo separator="true">,</mo><mi>i</mi><mi>t</mi><mi>c</mi><mi>a</mi><mi>l</mi><mi>l</mi><mi>s</mi><mi mathvariant="normal">‘</mi><mi>r</mi><mi>e</mi><mi>s</mi><mi>o</mi><mi>l</mi><mi>v</mi><mi>e</mi><mo stretchy="false">(</mo><mi>r</mi><mi>e</mi><mi>q</mi><mi>u</mi><mi>e</mi><mi>s</mi><mi>t</mi><mo separator="true">,</mo><mi>v</mi><mi>a</mi><mi>l</mi><mi>u</mi><mi>e</mi><mi mathvariant="normal">.</mi><mi>s</mi><mi>l</mi><mi>i</mi><mi>c</mi><mi>e</mi><mo stretchy="false">(</mo><mn>1</mn><mo stretchy="false">)</mo><mo stretchy="false">)</mo><mi mathvariant="normal">‘</mi><mo separator="true">;</mo><mi>o</mi><mi>t</mi><mi>h</mi><mi>e</mi><mi>r</mi><mi>w</mi><mi>i</mi><mi>s</mi><mi>e</mi><mi>i</mi><mi>t</mi><mi>r</mi><mi>e</mi><mi>t</mi><mi>u</mi><mi>r</mi><mi>n</mi><mi>s</mi><mi>t</mi><mi>h</mi><mi>e</mi><mi>v</mi><mi>a</mi><mi>l</mi><mi>u</mi><mi>e</mi><mi>u</mi><mi>n</mi><mi>t</mi><mi>o</mi><mi>u</mi><mi>c</mi><mi>h</mi><mi>e</mi><mi>d</mi><mi mathvariant="normal">.</mi><mi>T</mi><mi>h</mi><mi>a</mi><mi>t</mi><mi>m</mi><mi>e</mi><mi>a</mi><mi>n</mi><mi>s</mi><mi>a</mi><mi>l</mi><mi>i</mi><mi>t</mi><mi>e</mi><mi>r</mi><mi>a</mi><mi>l</mi><mi>v</mi><mi>a</mi><mi>l</mi><mi>u</mi><mi>e</mi><mi>b</mi><mi>e</mi><mi>g</mi><mi>i</mi><mi>n</mi><mi>n</mi><mi>i</mi><mi>n</mi><mi>g</mi><mi>w</mi><mi>i</mi><mi>t</mi><mi>h</mi><mi mathvariant="normal">‘</mi></mrow><annotation encoding="application/x-tex">, it calls resolve(request, value.slice(1)); otherwise it returns the value untouched. That means a literal value beginning with </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord">‘</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord mathnormal">i</span><span class="mord mathnormal">t</span><span class="mord mathnormal">c</span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.01968em;">ll</span><span class="mord mathnormal">s</span><span class="mord">‘</span><span class="mord mathnormal">reso</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal" style="margin-right:0.03588em;">v</span><span class="mord mathnormal">e</span><span class="mopen">(</span><span class="mord mathnormal">re</span><span class="mord mathnormal" style="margin-right:0.03588em;">q</span><span class="mord mathnormal">u</span><span class="mord mathnormal">es</span><span class="mord mathnormal">t</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord mathnormal" style="margin-right:0.03588em;">v</span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal">u</span><span class="mord mathnormal">e</span><span class="mord">.</span><span class="mord mathnormal">s</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal">i</span><span class="mord mathnormal">ce</span><span class="mopen">(</span><span class="mord">1</span><span class="mclose">))</span><span class="mord">‘</span><span class="mpunct">;</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord mathnormal">o</span><span class="mord mathnormal">t</span><span class="mord mathnormal">h</span><span class="mord mathnormal" style="margin-right:0.02778em;">er</span><span class="mord mathnormal" style="margin-right:0.02691em;">w</span><span class="mord mathnormal">i</span><span class="mord mathnormal">se</span><span class="mord mathnormal">i</span><span class="mord mathnormal">t</span><span class="mord mathnormal">re</span><span class="mord mathnormal">t</span><span class="mord mathnormal">u</span><span class="mord mathnormal" style="margin-right:0.02778em;">r</span><span class="mord mathnormal">n</span><span class="mord mathnormal">s</span><span class="mord mathnormal">t</span><span class="mord mathnormal">h</span><span class="mord mathnormal">e</span><span class="mord mathnormal" style="margin-right:0.03588em;">v</span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal">u</span><span class="mord mathnormal">e</span><span class="mord mathnormal">u</span><span class="mord mathnormal">n</span><span class="mord mathnormal">t</span><span class="mord mathnormal">o</span><span class="mord mathnormal">u</span><span class="mord mathnormal">c</span><span class="mord mathnormal">h</span><span class="mord mathnormal">e</span><span class="mord mathnormal">d</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.13889em;">T</span><span class="mord mathnormal">ha</span><span class="mord mathnormal">t</span><span class="mord mathnormal">m</span><span class="mord mathnormal">e</span><span class="mord mathnormal">an</span><span class="mord mathnormal">s</span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal">i</span><span class="mord mathnormal">t</span><span class="mord mathnormal" style="margin-right:0.02778em;">er</span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal" style="margin-right:0.03588em;">v</span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal">u</span><span class="mord mathnormal">e</span><span class="mord mathnormal">b</span><span class="mord mathnormal">e</span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mord mathnormal">innin</span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mord mathnormal" style="margin-right:0.02691em;">w</span><span class="mord mathnormal">i</span><span class="mord mathnormal">t</span><span class="mord mathnormal">h</span><span class="mord">‘</span></span></span></span>"/> cannot be expressed - it will always be treated as a reference.
What resolve() will and will not reach
$-references go through the same resolver as field paths, which is deliberately narrow.
Three consequences follow from that diagram:
- Only five roots exist.
subject,resource, andenvironmentare the object roots;actionandscopeare shorthands handled before the root check.<MathMl mathml="<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>p</mi><mi>o</mi><mi>l</mi><mi>i</mi><mi>c</mi><mi>y</mi><mi mathvariant="normal">.</mi><mi>i</mi><mi>d</mi><mi mathvariant="normal">‘</mi><mi>o</mi><mi>r</mi><mi mathvariant="normal">‘</mi></mrow><annotation encoding="application/x-tex">policy.idor</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal">p</span><span class="mord mathnormal">o</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal">i</span><span class="mord mathnormal" style="margin-right:0.03588em;">cy</span><span class="mord">.</span><span class="mord mathnormal">i</span><span class="mord mathnormal">d</span><span class="mord">‘</span><span class="mord mathnormal" style="margin-right:0.02778em;">or</span><span class="mord">‘</span></span></span></span>"/>request.headersresolve tonull. - Prototype segments are refused, twice over. The walk reads own properties only, so nothing on the prototype chain is reachable at all; on top of that
__proto__,constructorandprototypeare rejected at parse time so the path is refused once and memoised as invalid rather than walked per request. Own-property reads also fixed a subtler bug: everyObject.prototypemember resolved to a function on any object, so anexists-gated allow fired against a subject with no attributes. - A miss is
null, not an error. A typo in a$-path makes the comparison run againstnull, which foreqandneqis a silent wrong answer rather than a failure. This is exactly the class of bug a typed context prevents. - A value outside the attribute contract also resolves to
null.resolvenarrows what it found rather than asserting it: scalars, arrays of scalars, and plain objects whose values are scalars pass; aDate, a nested object, or a function does not. Adapters deserialise JSON and hand the result straight through, so those genuinely reach here, andnullis NotApplicable rather than a guess.
$environment.now is always available: the engine calls ensureEnvNow(req) after beforeEvaluate, defaulting environment.now to Date.now() when a hook has not pinned one. Pair it with before / after for "is this grant still valid" rules.
Where $-references are accepted
The value parameter of a builder method is typed ... | DotPath.FlexibleDollarPaths<TContext> on exactly six methods. Everything else takes a narrower type, which is where the type surface and the runtime diverge.
| Method | <MathMl mathml="<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal">‘</mi><mi>i</mi><mi>n</mi><mi>t</mi><mi>h</mi><mi>e</mi><mi>t</mi><mi>y</mi><mi>p</mi><mi>e</mi><mi mathvariant="normal">∣</mi><mi mathvariant="normal">‘</mi></mrow><annotation encoding="application/x-tex"> in the type | </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord">‘</span><span class="mord mathnormal">in</span><span class="mord mathnormal">t</span><span class="mord mathnormal">h</span><span class="mord mathnormal">e</span><span class="mord mathnormal">t</span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span><span class="mord mathnormal">p</span><span class="mord mathnormal">e</span><span class="mord">∣‘</span></span></span></span>"/> resolved at runtime | Notes |
|---|---|---|---|
check(field, op, value) | yes | yes | The general escape hatch; any operator, any path |
eq(field, value) | yes | yes | |
neq(field, value) | yes | yes | |
attr(key, op, value) | yes | yes | Key is a subject-attribute path |
resourceAttr(key, op, value) | yes | yes | Key narrows per .of() |
env(key, op, value) | yes | yes | Key is an environment path |
contains(field, value) | no - value: string | yes | A $-string compiles as a plain string and resolves normally |
matches(field, regex) | no - regex: string | refused | See the security note below |
gt / gte / lt / lte | no - value: number | n/a | Use check(field, 'gt', '$path') for a dynamic bound |
in(field, values) | element type allows $ | no | See the gotcha below |
exists / not_exists | no value parameter | n/a | |
role / roles / scope / scopes | no - constrained to TRole / TScope | n/a | These emit fixed field paths |
isOwner(ownerField?) | argument is the field, not the value | yes | Always emits value: '$subject.id' |
evalCondition short-circuits to false whenever the operator is matches and the value is a <MathMl mathml="<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi mathvariant="normal">‘</mi><mo>−</mo><mi>p</mi><mi>r</mi><mi>e</mi><mi>f</mi><mi>i</mi><mi>x</mi><mi>e</mi><mi>d</mi><mi>s</mi><mi>t</mi><mi>r</mi><mi>i</mi><mi>n</mi><mi>g</mi><mi mathvariant="normal">.</mi><mi>I</mi><mi>f</mi><mi>a</mi><mi mathvariant="normal">‘</mi></mrow><annotation encoding="application/x-tex">-prefixed string. If a </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.7778em;vertical-align:-0.0833em;"></span><span class="mord">‘</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal">p</span><span class="mord mathnormal">re</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mord mathnormal">i</span><span class="mord mathnormal">x</span><span class="mord mathnormal">e</span><span class="mord mathnormal">d</span><span class="mord mathnormal">s</span><span class="mord mathnormal">t</span><span class="mord mathnormal" style="margin-right:0.02778em;">r</span><span class="mord mathnormal">in</span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.07847em;">I</span><span class="mord mathnormal" style="margin-right:0.10764em;">f</span><span class="mord mathnormal">a</span><span class="mord">‘</span></span></span></span>"/> pattern were resolved, anyone who could write a subject, resource, or environment attribute could plant a catastrophic regex and stall the evaluation thread. The refusal is unconditional - it does not matter where the attribute came from. Regex patterns must be literals in the policy, and they are additionally capped at MAX_REGEX_LENGTH (128 characters) with the subject string capped at 2048.in(field, values) types its array elements as FieldValue | FlexibleDollarPaths, so w.in('subject.attributes.tier', ['<MathMl mathml="<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>r</mi><mi>e</mi><mi>s</mi><mi>o</mi><mi>u</mi><mi>r</mi><mi>c</mi><mi>e</mi><mi mathvariant="normal">.</mi><mi>a</mi><mi>t</mi><mi>t</mi><mi>r</mi><mi>i</mi><mi>b</mi><mi>u</mi><mi>t</mi><mi>e</mi><mi>s</mi><mi mathvariant="normal">.</mi><mi>t</mi><mi>i</mi><mi>e</mi><msup><mi>r</mi><mo mathvariant="normal" lspace="0em" rspace="0em">′</mo></msup><mo stretchy="false">]</mo><mo stretchy="false">)</mo><mi mathvariant="normal">‘</mi><mi>c</mi><mi>o</mi><mi>m</mi><mi>p</mi><mi>i</mi><mi>l</mi><mi>e</mi><mi>s</mi><mi mathvariant="normal">.</mi><mi>A</mi><mi>t</mi><mi>r</mi><mi>u</mi><mi>n</mi><mi>t</mi><mi>i</mi><mi>m</mi><mi>e</mi><mi mathvariant="normal">‘</mi><mi>r</mi><mi>e</mi><mi>s</mi><mi>o</mi><mi>l</mi><mi>v</mi><mi>e</mi><mi>V</mi><mi>a</mi><mi>l</mi><mi>u</mi><mi>e</mi><mi mathvariant="normal">‘</mi><mi>o</mi><mi>n</mi><mi>l</mi><mi>y</mi><mi>i</mi><mi>n</mi><mi>s</mi><mi>p</mi><mi>e</mi><mi>c</mi><mi>t</mi><mi>s</mi><mi>t</mi><mi>h</mi><mi>e</mi><mi>v</mi><mi>a</mi><mi>l</mi><mi>u</mi><mi>e</mi><mi>w</mi><mi>h</mi><mi>e</mi><mi>n</mi><mi>i</mi><mi>t</mi><mi>i</mi><mi>s</mi><mi>a</mi><mi>s</mi><mi>t</mi><mi>r</mi><mi>i</mi><mi>n</mi><mi>g</mi><mo separator="true">,</mo><mi>a</mi><mi>n</mi><mi>d</mi><mi>t</mi><mi>h</mi><mi>e</mi><mi>v</mi><mi>a</mi><mi>l</mi><mi>u</mi><mi>e</mi><mi>h</mi><mi>e</mi><mi>r</mi><mi>e</mi><mi>i</mi><mi>s</mi><mi>a</mi><mi>n</mi><mi>a</mi><mi>r</mi><mi>r</mi><mi>a</mi><mi>y</mi><mo>−</mo><mi>s</mi><mi>o</mi><mi>e</mi><mi>a</mi><mi>c</mi><mi>h</mi><mi>e</mi><mi>l</mi><mi>e</mi><mi>m</mi><mi>e</mi><mi>n</mi><mi>t</mi><mi>i</mi><mi>s</mi><mi>c</mi><mi>o</mi><mi>m</mi><mi>p</mi><mi>a</mi><mi>r</mi><mi>e</mi><mi>d</mi><mi>l</mi><mi>i</mi><mi>t</mi><mi>e</mi><mi>r</mi><mi>a</mi><mi>l</mi><mi>l</mi><mi>y</mi><mi>a</mi><mi>n</mi><mi>d</mi><mi>t</mi><mi>h</mi><mi>e</mi><mi>c</mi><mi>h</mi><mi>e</mi><mi>c</mi><mi>k</mi><mi>a</mi><mi>s</mi><mi>k</mi><mi>s</mi><mi>w</mi><mi>h</mi><mi>e</mi><mi>t</mi><mi>h</mi><mi>e</mi><mi>r</mi><mi>t</mi><mi>h</mi><mi>e</mi><mi>t</mi><mi>i</mi><mi>e</mi><mi>r</mi><mi>e</mi><mi>q</mi><mi>u</mi><mi>a</mi><mi>l</mi><mi>s</mi><mi>t</mi><mi>h</mi><mi>e</mi><mi>l</mi><mi>i</mi><mi>t</mi><mi>e</mi><mi>r</mi><mi>a</mi><mi>l</mi><mi>t</mi><mi>e</mi><mi>x</mi><mi>t</mi><mi mathvariant="normal">‘</mi></mrow><annotation encoding="application/x-tex">resource.attributes.tier&#x27;]) compiles. At runtime resolveValue only inspects the value when it is a string, and the value here is an array - so each element is compared literally and the check asks whether the tier equals the literal text </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.0019em;vertical-align:-0.25em;"></span><span class="mord mathnormal">reso</span><span class="mord mathnormal">u</span><span class="mord mathnormal">rce</span><span class="mord">.</span><span class="mord mathnormal">a</span><span class="mord mathnormal">tt</span><span class="mord mathnormal" style="margin-right:0.02778em;">r</span><span class="mord mathnormal">ib</span><span class="mord mathnormal">u</span><span class="mord mathnormal">t</span><span class="mord mathnormal">es</span><span class="mord">.</span><span class="mord mathnormal">t</span><span class="mord mathnormal">i</span><span class="mord mathnormal">e</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.02778em;">r</span><span class="msupsub"><span class="vlist-t"><span class="vlist-r"><span class="vlist" style="height:0.7519em;"><span style="top:-3.063em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord mtight">′</span></span></span></span></span></span></span></span></span><span class="mclose">])</span><span class="mord">‘</span><span class="mord mathnormal">co</span><span class="mord mathnormal">m</span><span class="mord mathnormal">p</span><span class="mord mathnormal">i</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal">es</span><span class="mord">.</span><span class="mord mathnormal">A</span><span class="mord mathnormal">t</span><span class="mord mathnormal" style="margin-right:0.02778em;">r</span><span class="mord mathnormal">u</span><span class="mord mathnormal">n</span><span class="mord mathnormal">t</span><span class="mord mathnormal">im</span><span class="mord mathnormal">e</span><span class="mord">‘</span><span class="mord mathnormal">reso</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal" style="margin-right:0.03588em;">v</span><span class="mord mathnormal">e</span><span class="mord mathnormal">Va</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal">u</span><span class="mord mathnormal">e</span><span class="mord">‘</span><span class="mord mathnormal">o</span><span class="mord mathnormal">n</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span><span class="mord mathnormal">in</span><span class="mord mathnormal">s</span><span class="mord mathnormal">p</span><span class="mord mathnormal">ec</span><span class="mord mathnormal">t</span><span class="mord mathnormal">s</span><span class="mord mathnormal">t</span><span class="mord mathnormal">h</span><span class="mord mathnormal">e</span><span class="mord mathnormal" style="margin-right:0.03588em;">v</span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal">u</span><span class="mord mathnormal">e</span><span class="mord mathnormal" style="margin-right:0.02691em;">w</span><span class="mord mathnormal">h</span><span class="mord mathnormal">e</span><span class="mord mathnormal">ni</span><span class="mord mathnormal">t</span><span class="mord mathnormal">i</span><span class="mord mathnormal">s</span><span class="mord mathnormal">a</span><span class="mord mathnormal">s</span><span class="mord mathnormal">t</span><span class="mord mathnormal" style="margin-right:0.02778em;">r</span><span class="mord mathnormal">in</span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mpunct">,</span><span class="mspace" style="margin-right:0.1667em;"></span><span class="mord mathnormal">an</span><span class="mord mathnormal">d</span><span class="mord mathnormal">t</span><span class="mord mathnormal">h</span><span class="mord mathnormal">e</span><span class="mord mathnormal" style="margin-right:0.03588em;">v</span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal">u</span><span class="mord mathnormal">e</span><span class="mord mathnormal">h</span><span class="mord mathnormal">ere</span><span class="mord mathnormal">i</span><span class="mord mathnormal">s</span><span class="mord mathnormal">ana</span><span class="mord mathnormal" style="margin-right:0.02778em;">rr</span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord mathnormal">soe</span><span class="mord mathnormal">a</span><span class="mord mathnormal">c</span><span class="mord mathnormal">h</span><span class="mord mathnormal">e</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal">e</span><span class="mord mathnormal">m</span><span class="mord mathnormal">e</span><span class="mord mathnormal">n</span><span class="mord mathnormal">t</span><span class="mord mathnormal">i</span><span class="mord mathnormal">sco</span><span class="mord mathnormal">m</span><span class="mord mathnormal">p</span><span class="mord mathnormal">a</span><span class="mord mathnormal">re</span><span class="mord mathnormal">d</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal">i</span><span class="mord mathnormal">t</span><span class="mord mathnormal" style="margin-right:0.02778em;">er</span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.01968em;">ll</span><span class="mord mathnormal" style="margin-right:0.03588em;">y</span><span class="mord mathnormal">an</span><span class="mord mathnormal">d</span><span class="mord mathnormal">t</span><span class="mord mathnormal">h</span><span class="mord mathnormal">ec</span><span class="mord mathnormal">h</span><span class="mord mathnormal">ec</span><span class="mord mathnormal">ka</span><span class="mord mathnormal">s</span><span class="mord mathnormal" style="margin-right:0.03148em;">k</span><span class="mord mathnormal">s</span><span class="mord mathnormal" style="margin-right:0.02691em;">w</span><span class="mord mathnormal">h</span><span class="mord mathnormal">e</span><span class="mord mathnormal">t</span><span class="mord mathnormal">h</span><span class="mord mathnormal" style="margin-right:0.02778em;">er</span><span class="mord mathnormal">t</span><span class="mord mathnormal">h</span><span class="mord mathnormal">e</span><span class="mord mathnormal">t</span><span class="mord mathnormal">i</span><span class="mord mathnormal">ere</span><span class="mord mathnormal" style="margin-right:0.03588em;">q</span><span class="mord mathnormal">u</span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal">s</span><span class="mord mathnormal">t</span><span class="mord mathnormal">h</span><span class="mord mathnormal">e</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal">i</span><span class="mord mathnormal">t</span><span class="mord mathnormal" style="margin-right:0.02778em;">er</span><span class="mord mathnormal">a</span><span class="mord mathnormal">lt</span><span class="mord mathnormal">e</span><span class="mord mathnormal">x</span><span class="mord mathnormal">t</span><span class="mord">‘</span></span></span></span>"/>resource.attributes.tier. Use check(field, 'eq', '$path') for a dynamic single value, or keep in arrays literal.Common patterns
Ownership
.when((w) => w.resourceAttr('ownerId', 'eq', '$subject.id'))
w.isOwner() is the shorthand and emits exactly { field: 'resource.attributes.ownerId', operator: 'eq', value: '$subject.id' }. Pass a different field to isOwner('resource.attributes.createdBy') when your column is named otherwise.
Cross-field equality
.when((w) => w.attr('department', 'eq', '$resource.attributes.department'))
The subject's department must match the resource's.
Self-action prevention
.rule('no-self-delete', (r) =>
r.deny().on('delete').of('user').when((w) => w.check('resource.id', 'eq', '$subject.id')),
)
Scope match
.when((w) => w.check('resource.attributes.scope', 'eq', '$scope'))
$scope uses the shorthand root, so it reads request.scope and yields null when the request carried no scope.
Temporal validity
.when((w) => w.check('subject.attributes.suspendedUntil', 'after', '$environment.now'))
Both operands are coerced to epoch milliseconds - numbers pass through, ISO-8601 strings are parsed, and anything else becomes NaN so the comparison fails closed.
Gotchas
- A
$-path that does not exist resolves tonull, silently.eqagainstnullisfalse;neqagainstnullistrue. A typo in adenyrule's$-path can therefore turn the rule permanently on. Typed contexts are the defence. - The type system does not cross-validate the two sides.
check('resource.attributes.tier', 'eq', '$subject.attributes.status')compiles even when the two resolve to disjoint unions, because the value type is a union of the field's type and every$-path. Treat$-references as unchecked on the value side. check()is the widest door. Its field parameter isFlexibleDotPaths, so with an open bag anywhere in your context it accepts any string on both sides. That is what makes it useful for paths you never typed, and what makes it the least protected method.
See also
- Typed context - where
DotPaths(and thereforeDollarPaths) comes from. - $-variables - the untyped view of the same feature.
- Conditions - operator-by-operator semantics.
- Methods reference - the builders these values are passed to.