What's wrong
As of #269 (and #263 before it), ExpressionBuilder.fromUnifiedCondition compiles a keyed query/header/cookie condition to any(field["key"][*] <op> value) (value comparisons) or has_key(field, "key") (exists/not_exists) — the type-valid Cloudflare wirefilter construct, verified against the Ruleset Engine docs.
WirefilterParser.ts (the inverse — Cloudflare wirefilter → UnifiedCondition[], used by cloudflareToUnified) was deliberately not extended to understand this syntax when #263/#269 landed. Its tokenizer/grammar only handles the plain field["key"] <op> value / field["key"] exists shape it previously produced — no function-call syntax (any(, has_key(), no [*] wildcard indexing, and no comma tokenization (needed for has_key(map, "key")'s two arguments).
Concretely: parseWirefilterExpression('any(http.request.headers["x-custom"][*] eq "value")') returns null today, and cloudflareToUnified falls back to its generic "expression could not be parsed back into structured conditions" warning + empty conditions: [] for any rule with a keyed header/cookie/query condition. This is a safe degradation (matches the parser's own documented contract: unsupported constructs return null rather than being guessed at, and the caller already has a warning path for exactly this) but a real capability loss — doorman status/diff/backup/download against a Cloudflare zone with such a rule won't show its actual conditions, just a warning.
Suggested fix
Extend WirefilterParser.ts:
- Tokenizer (
tokenize()): add a COMMA token type (currently , gets silently absorbed into the "maximal run" WORD-reading branch, corrupting anything after it — needed for has_key(map, "key")'s argument separator).
- Grammar: add function-call parsing for
any( and has_key( before parseFieldRef's bare-field-reference path is tried (both start with a WORD token that's actually a function name, not a field path).
any(field["key"][*] <op> value) → a comparison node with key set (the [*] wildcard only appears inside any(...), never standalone).
has_key(field, "key") → an exists node with key set (mirrors the existing bracket-field["key"] exists handling, just via a function call instead).
- Reverse the field-name mapping for the keyed map fields specifically —
http.request.cookies (keyed cookie) needs to map back to cookie even though it's a different Cloudflare field name than the bare cookie case (http.cookie); same idea for http.request.uri.args (keyed query) vs. http.request.uri.query (bare). CLOUDFLARE_FIELD_TO_UNIFIED as it stands only has entries for the bare field names.
- Add round-trip tests for all three keyed types (header/cookie/query), covering value comparison, exists/not_exists, and negation — mirroring the round-trip test style already in
WirefilterParser.test.ts's "round-trip fidelity against ExpressionBuilder" describe block.
- Confirm this doesn't affect
CelParser.ts — it has its own separate tokenizer/grammar (only TokenStream itself is shared, per its docstring), so this should be a WirefilterParser.ts-only change.
Not urgent (the current behavior is safe, just less complete), but real follow-up work — tracked here rather than left implicit.
What's wrong
As of #269 (and #263 before it),
ExpressionBuilder.fromUnifiedConditioncompiles a keyedquery/header/cookiecondition toany(field["key"][*] <op> value)(value comparisons) orhas_key(field, "key")(exists/not_exists) — the type-valid Cloudflare wirefilter construct, verified against the Ruleset Engine docs.WirefilterParser.ts(the inverse — Cloudflare wirefilter →UnifiedCondition[], used bycloudflareToUnified) was deliberately not extended to understand this syntax when #263/#269 landed. Its tokenizer/grammar only handles the plainfield["key"] <op> value/field["key"] existsshape it previously produced — no function-call syntax (any(,has_key(), no[*]wildcard indexing, and no comma tokenization (needed forhas_key(map, "key")'s two arguments).Concretely:
parseWirefilterExpression('any(http.request.headers["x-custom"][*] eq "value")')returnsnulltoday, andcloudflareToUnifiedfalls back to its generic "expression could not be parsed back into structured conditions" warning + emptyconditions: []for any rule with a keyed header/cookie/query condition. This is a safe degradation (matches the parser's own documented contract: unsupported constructs returnnullrather than being guessed at, and the caller already has a warning path for exactly this) but a real capability loss —doorman status/diff/backup/downloadagainst a Cloudflare zone with such a rule won't show its actual conditions, just a warning.Suggested fix
Extend
WirefilterParser.ts:tokenize()): add aCOMMAtoken type (currently,gets silently absorbed into the "maximal run" WORD-reading branch, corrupting anything after it — needed forhas_key(map, "key")'s argument separator).any(andhas_key(beforeparseFieldRef's bare-field-reference path is tried (both start with aWORDtoken that's actually a function name, not a field path).any(field["key"][*] <op> value)→ acomparisonnode withkeyset (the[*]wildcard only appears insideany(...), never standalone).has_key(field, "key")→ anexistsnode withkeyset (mirrors the existing bracket-field["key"] existshandling, just via a function call instead).http.request.cookies(keyed cookie) needs to map back tocookieeven though it's a different Cloudflare field name than the bare cookie case (http.cookie); same idea forhttp.request.uri.args(keyed query) vs.http.request.uri.query(bare).CLOUDFLARE_FIELD_TO_UNIFIEDas it stands only has entries for the bare field names.WirefilterParser.test.ts's "round-trip fidelity against ExpressionBuilder" describe block.CelParser.ts— it has its own separate tokenizer/grammar (onlyTokenStreamitself is shared, per its docstring), so this should be aWirefilterParser.ts-only change.Not urgent (the current behavior is safe, just less complete), but real follow-up work — tracked here rather than left implicit.