Repository navigation
Vercel region silently collides with unified region; 8 legacy fields leak invalid Cloudflare wirefilter #273
Description
What's wrong
Found while auditing skills/doorman/references/rules.md for accuracy against the real FieldType/translator code (that doc fix landed separately, already on main). Confirmed empirically by running the real vercelToUnified + unifiedToCloudflare functions directly against test fixtures with npx tsx, not just reading the code — reproduced independently a second time while writing this issue up.
Bug 1 (more severe): Vercel's region silently collides with the unified region field
Vercel's native region condition type means the edge/deployment region a request is served from (e.g. "sfo1", "iad1") — the same category of concept as the neighboring environment field, nothing to do with the client. mapVercelTypeToUnified (src/lib/providers/vercel/translator.ts) has no entry for it, so it falls through mapping[type] || type and becomes a unified condition with field: "region".
The problem: the unified schema already has a field literally named region, meaning something else entirely — the client's geographic subdivision (FieldType in src/lib/types/common.ts:38, "Geographic region"; mapped in Cloudflare's mapUnifiedFieldToCloudflare, src/lib/translators/ExpressionBuilder.ts:264, to ip.geoip.subdivision_1). The two concepts collide on the string "region".
Reproduced directly against the real translators:
vercelToUnified({ type: 'region', op: 'eq', value: 'sfo1' })
-> unified condition: { field: 'region', operator: 'eq', value: 'sfo1' } (0 warnings)
unifiedToCloudflare(...)
-> expression: 'ip.geoip.subdivision_1 eq "sfo1"' (0 warnings)
This is syntactically valid wirefilter — it will sync to Cloudflare with no error and no warning — but it compares a Vercel edge-region slug against a client ISO subdivision code, which can never match. A rule keyed off Vercel's edge region (e.g. region-conditional deny/challenge/bypass logic) silently becomes a Cloudflare rule whose condition can never fire once migrated: a deny/challenge rule gives false confidence, and a bypass/allow rule silently blocks everyone it was meant to exempt.
For contrast, a genuine unified region condition (client geo, e.g. "CA") produces an indistinguishable-looking expression:
unifiedToCloudflare({ conditions: [{ field: 'region', operator: 'eq', value: 'CA' }], ... })
-> expression: 'ip.geoip.subdivision_1 eq "CA"'
There's no way, once a condition reaches Cloudflare's translator, to tell "real client-region condition" from "corrupted Vercel edge-region pass-through" apart — the corruption already happened one step earlier, silently, inside mapVercelTypeToUnified.
This is Cloudflare-specific: mapUnifiedFieldToFastly (src/lib/providers/fastly/translator.ts) already drops all region conditions (real or corrupted) with a warning — Fastly has no subdivision-level geo field at all (see fastly.md) — so it's accidentally immune to this particular collision.
Bug 2 (separate, less severe): 8 other legacy-only Vercel fields leak invalid wirefilter syntax
geo_continent, geo_country_region, protocol, target_path, environment, ja3_digest, ja4_digest, rate_limit_api_id all fall through the same mapVercelTypeToUnified pass-through as Bug 1, and then fall through mapUnifiedFieldToCloudflare's mapping[field] || field fallback (ExpressionBuilder.ts:279) with no entry, leaking the bare unmapped name into the generated expression:
geo_continent eq "NA"
geo_continent isn't a real wirefilter field — again with zero warnings. Unlike Bug 1 this probably fails loudly (Cloudflare's Rulesets API likely rejects the unknown field outright), but that's unverified against a live zone (same caveat as #269).
skills/doorman/references/cloudflare.md:115 currently claims environment/ja3_digest/ja4_digest specifically "have no unified-format equivalent at all, so they're not reachable through a provider/providers-tagged config for any provider" and that "every other unified field maps to something on Cloudflare." Both halves are wrong: UnifiedCondition.field is schema-validated as fieldTypeSchema.or(z.string()) (src/lib/schemas/unifiedSchemas.ts:31, "Allow custom fields") — arbitrary strings pass through, confirmed reachable by the repro above — and region/the other 7 don't map to anything valid either. Needs a doc fix alongside the code fix.
Every other unsupported-field case in this codebase drops the condition with a TranslationWarning instead of leaking a raw name: mapUnifiedTypeToVercel (src/lib/providers/vercel/translator.ts:375) returns null and its caller drops the condition with a warning; mapUnifiedFieldToFastly (src/lib/providers/fastly/translator.ts:87) does the same. mapUnifiedFieldToCloudflare is the only one of the three that silently falls through instead of returning null.
Context
- An orphaned pre-Unified-model implementation,
FieldMapper.ts(vercelToCloudflaretable, lines 13-35), has plausible real Cloudflare names for 4 of these 8 (geo_continent→ip.geoip.continent,geo_country_region→ip.geoip.subdivision_1,protocol→ssl,target_path→http.request.uri.path) — never ported into the live path. Confirmed dead: its only caller isExpressionBuilder.fromVercelCondition/fromVercelConditionGroups, which have no production callers of their own (only their own tests exercise them). Don't trust its mappings blindly — verify against Cloudflare's real wirefilter docs or a live zone before reusing (Cloudflare keyed header/cookie conditions likely emit wirefilter the API rejects (Array-vs-String, and indexing a String field) #269 found FieldMapper's header/cookie mappings were wrong in a different way). Critically, FieldMapper's ownregion→ip.geoip.subdivision_1entry has the exact same collision as Bug 1 — don't carry that one forward as-is. geo_country_regionis documented inrules.mdas "Region/state code" (e.g."CA") — i.e. the same real-world concept as the unifiedregionFieldType, not a separate one. It may belong inmapVercelTypeToUnified's explicit table alongsidegeo_country→country/geo_city→city/geo_as_number→asn, rather than in the "drop with warning" bucket — but this needs the same live-Cloudflare verification as the rest of Bug 2 before committing to it.- A second orphaned file,
src/lib/utils/compatibility.ts(zero importers anywhere insrc), independently repeats FieldMapper's stale claims (including Cloudflareregion/protocol/target_path/geo_continent"support" that doesn't reflect live behavior). Not user-facing today since nothing imports it, but misleading dead weight.
Suggested fix
mapVercelTypeToUnifiedshould not pass Vercel'sregionthrough as literal"region"— rename to a collision-free value (e.g. a Vercel-namespaced field the unified vocabulary doesn't otherwise use) and keepmapUnifiedTypeToVercelin sync for the reverse direction so the Vercel↔Unified round trip stays lossless. Add a regression test asserting a Vercelregioncondition never silently becomes a Cloudflareip.geoip.subdivision_1condition.- Make
mapUnifiedFieldToCloudflarereturnstring | null(matchingmapUnifiedTypeToVercel/mapUnifiedFieldToFastly), have its caller push aTranslationWarningand drop the condition onnull. Per-field, decide "drop with warning" (safe default; certainly correct forenvironment/ja3_digest/ja4_digest/rate_limit_api_id, which have no Cloudflare equivalent) vs. "add a real mapping" (geo_continent/geo_country_region/protocol/target_path) — the latter only after verifying against live Cloudflare or authoritative wirefilter docs, not by copying FieldMapper.ts unchecked. - Regression tests for both bugs (mutation-verified). Update
cloudflare.md's field table and theenvironment/ja3_digest/ja4_digestreachability claim once fixed.
Activity
gfargo commentedon Aug 24, 2026
Duplicate of #270 and #271, filed independently in the same investigation before I noticed those already existed (filed earlier the same day, in a prior session). Same root cause, same two failure modes.
This issue does have two additional details not in #270/#271 that are worth keeping on the record:
geo_country_region(one of the 8 fields 8 legacy-only Vercel condition types produce invalid Cloudflare filter syntax with no warning #271 flagged as "needs verification, drop or map") turned out not to need external Cloudflare verification at all —rules.mdalready documents it as Vercel's real client-subdivision field ("Region/state code", e.g."CA"), the same concept as unifiedregion, not a separate one. It now maps there directly.threat_score(Cloudflare'scf.threat_score) hit this exact same silent-fallback bug for an undeclared field. Now properly mapped and added to the unifiedFieldTypevocabulary.Both are covered in PR #275, which fixes #270 and #271. Closing this one as a duplicate.
fix: vercel region collides with unified region; 8 legacy fields leak…
chore(release): 3.15.1 [skip ci]
github-actions commentedon Aug 24, 2026
🎉 This issue has been resolved in version 3.15.1 🎉
The release is available on:
Your semantic-release bot 📦🚀