Split out of the #156 roadmap spike (Tier 3: model extension). Hard blocker for AWS WAF.
Problem
UnifiedRule represents conditions as a flat array plus a single top-level connective:
conditions: UnifiedCondition[]
conditionLogic?: 'AND' | 'OR'
Plus a per-condition group?: number (added in #169) giving exactly one level of nesting — AND-within-group, OR-across-groups — which is precisely Vercel's conditionGroup[] model and is expressible in wirefilter.
AWS WAF statements are an arbitrarily recursive tree: AndStatement/OrStatement/NotStatement wrapping leaf matchers (ByteMatchStatement, SqliMatchStatement, GeoMatchStatement, RateBasedStatement, …), nestable to any depth. (A and (B or (C and not D))) has no representation in the current model.
This also caps what WirefilterParser (#178) can accept — it deliberately returns null for an OR nested inside an AND, because there'd be nowhere to put the result. Any Cloudflare rule with genuinely nested boolean logic (hand-authored, or written by another tool) currently falls back to empty conditions.
Proposed approach
Introduce a recursive condition node type alongside the existing flat representation:
type UnifiedConditionNode =
| { type: 'condition'; condition: UnifiedCondition }
| { type: 'and'; children: UnifiedConditionNode[] }
| { type: 'or'; children: UnifiedConditionNode[] }
| { type: 'not'; child: UnifiedConditionNode }
Backward compatibility is the whole difficulty here. conditions[] + group is in the public schema, in every existing user config, and in every template/example. Options:
- (a) Add
conditionTree?: UnifiedConditionNode as an optional alternative; keep conditions[] as the canonical form for the flat/one-level case and normalize between them internally. Existing configs untouched.
- (b) Make the tree canonical internally and treat
conditions[] purely as a serialization convenience, converting on load/save.
Recommend (a) — (b) touches the diffing/hashing path (CloudflareOptimizer.canonicalizeRule) and the translators for every provider at once, which is a lot of blast radius for a change whose only current beneficiary is a provider that doesn't exist yet.
Downstream work this unblocks/requires
ExpressionBuilder.fromUnifiedConditions — emit nested wirefilter
WirefilterParser — stop bailing on nested OR-in-AND, return a tree
RuleTranslator.unifiedToVercel — must reject (with a clear warning, like it already does for unmappable fields) any tree deeper than Vercel's two-level model rather than silently flattening it
- Diff/hash canonicalization for tree-shaped rules
Acceptance criteria
(A and (B or C)) round-trips through unified → Cloudflare → unified without loss
- A tree too deep for Vercel produces an explicit unsupported-feature warning, never a silent flatten
- Existing flat/one-level configs behave identically (full suite green, no schema break)
Split out of the #156 roadmap spike (Tier 3: model extension). Hard blocker for AWS WAF.
Problem
UnifiedRulerepresents conditions as a flat array plus a single top-level connective:Plus a per-condition
group?: number(added in #169) giving exactly one level of nesting — AND-within-group, OR-across-groups — which is precisely Vercel'sconditionGroup[]model and is expressible in wirefilter.AWS WAF statements are an arbitrarily recursive tree:
AndStatement/OrStatement/NotStatementwrapping leaf matchers (ByteMatchStatement,SqliMatchStatement,GeoMatchStatement,RateBasedStatement, …), nestable to any depth.(A and (B or (C and not D)))has no representation in the current model.This also caps what
WirefilterParser(#178) can accept — it deliberately returnsnullfor an OR nested inside an AND, because there'd be nowhere to put the result. Any Cloudflare rule with genuinely nested boolean logic (hand-authored, or written by another tool) currently falls back to empty conditions.Proposed approach
Introduce a recursive condition node type alongside the existing flat representation:
Backward compatibility is the whole difficulty here.
conditions[]+groupis in the public schema, in every existing user config, and in every template/example. Options:conditionTree?: UnifiedConditionNodeas an optional alternative; keepconditions[]as the canonical form for the flat/one-level case and normalize between them internally. Existing configs untouched.conditions[]purely as a serialization convenience, converting on load/save.Recommend (a) — (b) touches the diffing/hashing path (
CloudflareOptimizer.canonicalizeRule) and the translators for every provider at once, which is a lot of blast radius for a change whose only current beneficiary is a provider that doesn't exist yet.Downstream work this unblocks/requires
ExpressionBuilder.fromUnifiedConditions— emit nested wirefilterWirefilterParser— stop bailing on nested OR-in-AND, return a treeRuleTranslator.unifiedToVercel— must reject (with a clear warning, like it already does for unmappable fields) any tree deeper than Vercel's two-level model rather than silently flattening itAcceptance criteria
(A and (B or C))round-trips through unified → Cloudflare → unified without loss