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-liteCEL-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.comCompile 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'],
},
},
}); // trueParses 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.
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.
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
?: ternaryuser.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:
- regex pattern length is limited;
- regex input length is limited;
- backreferences are rejected;
- common nested and ambiguous repetition forms are rejected;
- invalid regex syntax is wrapped in
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 |
- Compile expressions before hot paths when possible and reuse
CelPrograminstances. - Treat context objects as data, not capability containers.
- Use plain data objects or null-prototype objects for untrusted context data.
- Keep regex support behind conservative limits, or isolate evaluation in a worker/process for hard timeouts.
- Keep
maxCollectionLengthandmaxCompareDepthlow for tenant-controlled input. - Use
explain()for admin tooling, not every production request. - Add regression tests for tenant expressions, mapping rules, and authorization conditions.
- Keep dependency audit clean in CI, including workspace packages.
compileCel(source, options?)
CelProgram
CelProgram.eval(context)
CelProgram.explain(context)
CelContext
CelOptions
CelExplainEntry
CelExplainResult
CelError@sourceregistry/monaco-cel-liteprovides Monaco Editor syntax highlighting, completion, hover documentation, and signature help for CEL-lite expressions.
npm test
npm run lint