You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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".
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:
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 allregion 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 (vercelToCloudflare table, 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 is ExpressionBuilder.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 own region → ip.geoip.subdivision_1 entry has the exact same collision as Bug 1 — don't carry that one forward as-is.
geo_country_region is documented in rules.md as "Region/state code" (e.g. "CA") — i.e. the same real-world concept as the unified region FieldType, not a separate one. It may belong in mapVercelTypeToUnified's explicit table alongside geo_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 in src), independently repeats FieldMapper's stale claims (including Cloudflare region/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
mapVercelTypeToUnified should not pass Vercel's region through as literal "region" — rename to a collision-free value (e.g. a Vercel-namespaced field the unified vocabulary doesn't otherwise use) and keep mapUnifiedTypeToVercel in sync for the reverse direction so the Vercel↔Unified round trip stays lossless. Add a regression test asserting a Vercel region condition never silently becomes a Cloudflare ip.geoip.subdivision_1 condition.
Make mapUnifiedFieldToCloudflare return string | null (matching mapUnifiedTypeToVercel/mapUnifiedFieldToFastly), have its caller push a TranslationWarning and drop the condition on null. Per-field, decide "drop with warning" (safe default; certainly correct for environment/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 the environment/ja3_digest/ja4_digest reachability claim once fixed.
What's wrong
Found while auditing
skills/doorman/references/rules.mdfor accuracy against the realFieldType/translator code (that doc fix landed separately, already onmain). Confirmed empirically by running the realvercelToUnified+unifiedToCloudflarefunctions directly against test fixtures withnpx tsx, not just reading the code — reproduced independently a second time while writing this issue up.Bug 1 (more severe): Vercel's
regionsilently collides with the unifiedregionfieldVercel's native
regioncondition type means the edge/deployment region a request is served from (e.g."sfo1","iad1") — the same category of concept as the neighboringenvironmentfield, nothing to do with the client.mapVercelTypeToUnified(src/lib/providers/vercel/translator.ts) has no entry for it, so it falls throughmapping[type] || typeand becomes a unified condition withfield: "region".The problem: the unified schema already has a field literally named
region, meaning something else entirely — the client's geographic subdivision (FieldTypeinsrc/lib/types/common.ts:38, "Geographic region"; mapped in Cloudflare'smapUnifiedFieldToCloudflare,src/lib/translators/ExpressionBuilder.ts:264, toip.geoip.subdivision_1). The two concepts collide on the string"region".Reproduced directly against the real translators:
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
regioncondition (client geo, e.g."CA") produces an indistinguishable-looking expression: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 allregionconditions (real or corrupted) with a warning — Fastly has no subdivision-level geo field at all (seefastly.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_idall fall through the samemapVercelTypeToUnifiedpass-through as Bug 1, and then fall throughmapUnifiedFieldToCloudflare'smapping[field] || fieldfallback (ExpressionBuilder.ts:279) with no entry, leaking the bare unmapped name into the generated expression:geo_continentisn'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:115currently claimsenvironment/ja3_digest/ja4_digestspecifically "have no unified-format equivalent at all, so they're not reachable through aprovider/providers-tagged config for any provider" and that "every other unified field maps to something on Cloudflare." Both halves are wrong:UnifiedCondition.fieldis schema-validated asfieldTypeSchema.or(z.string())(src/lib/schemas/unifiedSchemas.ts:31, "Allow custom fields") — arbitrary strings pass through, confirmed reachable by the repro above — andregion/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
TranslationWarninginstead of leaking a raw name:mapUnifiedTypeToVercel(src/lib/providers/vercel/translator.ts:375) returnsnulland its caller drops the condition with a warning;mapUnifiedFieldToFastly(src/lib/providers/fastly/translator.ts:87) does the same.mapUnifiedFieldToCloudflareis the only one of the three that silently falls through instead of returningnull.Context
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.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.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.cloudflare.md's field table and theenvironment/ja3_digest/ja4_digestreachability claim once fixed.