A small, dependency-free CEL-inspired expression compiler and evaluator
Evaluate safe, auditable expressions for identity mapping, policy checks, group assignment, and multi-tenant configuration. CEL-lite is inspired by Google's Common Expression Language, but intentionally implements a smaller deterministic subset.
npm install @sourceregistry/cel-lite
CEL-lite has zero runtime dependencies and works in Node and browser runtimes.
import { compileCel } from '@sourceregistry/cel-lite';
const program = compileCel("has(user.email) ? lower(trim(user.email)) : null");
const result = program.eval({
user: { email: 'USER@EXAMPLE.COM' },
});
console.log(result); // user@example.com
Compile expressions once, then evaluate them against request-specific context objects.
const isStudent = compileCel("'student' in saml.attributes.eduPersonAffiliation");
isStudent.eval({
saml: {
attributes: {
eduPersonAffiliation: ['member', 'student'],
},
},
}); // true
compileCel(source, options?)Parses and validates an expression, then returns a reusable CelProgram.
import { compileCel } from '@sourceregistry/cel-lite';
const program = compileCel("coalesce(first(mail), 'n/a')", {
maxExpressionLength: 4096,
maxAstNodes: 2000,
});
Invalid expressions throw CelError with a message and source position when available.
program.eval(context)Evaluates the compiled expression against a plain context object.
const email = program.eval({
mail: ['user@example.com'],
});
Evaluation has no side effects, does not mutate the context, and only supports allow-listed functions.
program.explain(context)Evaluates the expression and returns a trace of intermediate AST values.
const explained = compileCel("size(groups) > 0 ? groups[0] : null").explain({
groups: ['Students', 'Staff'],
});
console.log(explained.result); // Students
console.table(explained.trace);
Use explain mode for admin previews, mapper debugging, and audit tooling. Keep trace limits configured for untrusted or high-volume inputs.
true, false, null
123, -1, 3.14
"string", 'string'
[1, 2, "a"]
== != < <= > >=
&& || !
+ in
?: ternary
user.email
saml.attributes["urn:mace:dir:attribute-def:mail"][0]
Missing paths resolve to undefined. Object access only reads own properties. Inherited properties and the keys __proto__, constructor, and prototype are blocked everywhere, including root identifiers.
For objects, in checks own properties only:
"email" in user // true when user has its own "email" property
"toString" in user // false
| Function | Description |
|---|---|
has(x) / exists(x) |
Checks defined values; arrays must be non-empty |
size(x) |
Length of an array/string or own-key object count |
first(x) |
First array element, otherwise the value itself |
last(x) |
Last array element, otherwise the value itself |
collect(a, b, ...) |
Collects arguments into an array |
lower(s) / upper(s) |
String casing |
trim(s) |
Trim whitespace |
contains(a, b) |
Array membership or string containment |
containsAny(arr, values) |
True when any value matches |
startsWith(s, prefix) |
String prefix check |
endsWith(s, suffix) |
String suffix check |
matches(s, regex) |
Guarded JavaScript regex test |
regexReplace(s, r, repl) |
Guarded global JavaScript regex replace |
coalesce(a, b, ...) |
First non-null, defined, non-empty-array value |
join(arr, sep) |
Join array items into a string |
split(s, sep) |
Split a string into an array |
Only these functions are callable. Arbitrary JavaScript functions, globals, imports, IO, network access, time access, mutation, loops, and user-defined functions are intentionally not supported.
matches and regexReplace use JavaScript RegExp, but CEL-lite applies conservative checks before compilation:
CelError.These guards reduce ReDoS risk without adding dependencies. JavaScript does not provide a synchronous regex timeout, so applications that accept arbitrary regex from untrusted users should run evaluation behind a host-level worker or process timeout.
CEL-lite enforces compile-time and runtime limits.
const program = compileCel("matches(email, pattern)", {
maxExpressionLength: 4096,
maxAstNodes: 2000,
maxCallDepth: 50,
maxTraceEntries: 5000,
maxCollectionLength: 10000,
maxStringLength: 65536,
maxCompareDepth: 50,
maxRegexPatternLength: 256,
maxRegexInputLength: 4096,
});
| Option | Default | Purpose |
|---|---|---|
maxExpressionLength |
4096 |
Maximum source string length |
maxAstNodes |
2000 |
Maximum parsed AST size |
maxCallDepth |
50 |
Maximum nested function calls |
maxTraceEntries |
5000 |
Maximum explain-mode trace entries |
maxCollectionLength |
10000 |
Maximum array/object size for expensive scans |
maxStringLength |
65536 |
Maximum string length for selected helpers |
maxCompareDepth |
50 |
Maximum recursive object/array compare depth |
maxRegexPatternLength |
256 |
Maximum regex pattern length |
maxRegexInputLength |
4096 |
Maximum input length for regex operations |
CelProgram instances.maxCollectionLength and maxCompareDepth low for tenant-controlled input.explain() for admin tooling, not every production request.compileCel(source, options?)
CelProgram
CelProgram.eval(context)
CelProgram.explain(context)
CelContext
CelOptions
CelExplainEntry
CelExplainResult
CelError
@sourceregistry/monaco-cel-lite provides Monaco Editor syntax highlighting, completion, hover documentation, and signature help for CEL-lite expressions.npm test
npm run lint