From 41bb5f9e14d124f8bc0af240219db198b1b6b27e Mon Sep 17 00:00:00 2001 From: Griffen Fargo <3642037+gfargo@users.noreply.github.com> Date: Mon, 24 Aug 2026 18:03:29 -0400 Subject: [PATCH] fix: map unified allow/block to valid Vercel actions instead of passing through unifiedToVercel used `rule.action.type` directly with no mapping at all, so a rule with action: {type: 'allow'} or {type: 'block'} passed doorman's own local validation (against the wider 8-value unified ActionType) and was dispatched to Vercel's real API carrying a mitigate.action value Vercel's own native schema (log, deny, challenge, bypass, rate_limit, redirect -- 6 values, no allow/block) says is invalid. Added VercelActionType (types/vercel.ts), Vercel's actual native action vocabulary -- previously VercelMitigationAction.action reused the wider unified ActionType directly, so TypeScript never caught the mismatch either. This matches the pattern CloudflareAction/FastlyRequestActionType already establish: a dedicated native type per provider, not the shared unified one. Narrowing this type surfaced exactly the one call site that needed fixing. mapUnifiedActionToVercel maps allow -> bypass (skip further mitigation, Vercel's closest equivalent to letting a request through) and block -> deny (Vercel's actual block-like action); the other 6 unified action types are already valid Vercel actions and pass through unchanged. Every unified ActionType has a real Vercel action to land on, so this never needs to warn or drop a condition, unlike #261's operator fix. --- .../vercel/__tests__/translator.test.ts | 29 +++++++++++++++ src/lib/providers/vercel/translator.ts | 36 +++++++++++++++++-- src/lib/types/vercel.ts | 14 ++++++-- 3 files changed, 75 insertions(+), 4 deletions(-) diff --git a/src/lib/providers/vercel/__tests__/translator.test.ts b/src/lib/providers/vercel/__tests__/translator.test.ts index 91ede49..8be2239 100644 --- a/src/lib/providers/vercel/__tests__/translator.test.ts +++ b/src/lib/providers/vercel/__tests__/translator.test.ts @@ -253,6 +253,35 @@ describe('vercel/translator', () => { expect(result.conditionGroup[0]!.conditions).toHaveLength(1) }) + // Regression tests for #262: unifiedToVercel previously used + // `rule.action.type` directly with no mapping, so a rule with + // `action: {type: 'allow'}`/`{type: 'block'}` passed doorman's own + // local validation and reached Vercel's real API carrying an action + // value its own native schema (log/deny/challenge/bypass/rate_limit/ + // redirect only) says is invalid. + it('maps unified allow to Vercel bypass and block to Vercel deny, not the invalid native values', () => { + const mappings = [ + { unified: 'allow', vercel: 'bypass' }, + { unified: 'block', vercel: 'deny' }, + ] as const + + for (const { unified, vercel } of mappings) { + const rule = makeUnifiedRule({ action: { type: unified } }) + const { result } = unifiedToVercel(rule) + expect(result.action.mitigate.action).toBe(vercel) + } + }) + + it('passes the 6 already-native Vercel action types through unchanged', () => { + const nativeActions = ['log', 'deny', 'challenge', 'bypass', 'rate_limit', 'redirect'] as const + + for (const action of nativeActions) { + const rule = makeUnifiedRule({ action: { type: action } }) + const { result } = unifiedToVercel(rule) + expect(result.action.mitigate.action).toBe(action) + } + }) + it('translates unified operators back to Vercel operators', () => { const ops = [ { unified: 'eq', vercel: 'eq' }, diff --git a/src/lib/providers/vercel/translator.ts b/src/lib/providers/vercel/translator.ts index 5a6c58e..133e635 100644 --- a/src/lib/providers/vercel/translator.ts +++ b/src/lib/providers/vercel/translator.ts @@ -5,9 +5,10 @@ import type { VercelRuleCondition, VercelRuleOperator, VercelRuleType, + VercelActionType, } from '../../types/vercel' import type { UnifiedRule, UnifiedIPRule, UnifiedCondition, UnifiedAction } from '../../types/unified' -import type { Operator } from '../../types/common' +import type { Operator, ActionType } from '../../types/common' import type { TranslationResult, TranslationWarning } from '../../translators/TranslationTypes' import { TranslationWarningSystem } from '../../translators/TranslationWarningSystem' @@ -166,7 +167,7 @@ export function unifiedToVercel(rule: UnifiedRule): TranslationResult `bypass` (Vercel's closest equivalent: skip further mitigation, + * i.e. let the request through) and `block` -> `deny` (Vercel's actual + * block-like action) — both unconditional, meaning-preserving remaps rather + * than lossy drops, so every unified `ActionType` has a real Vercel action + * to land on and this never needs to warn or throw. + */ +function mapUnifiedActionToVercel(type: ActionType): VercelActionType { + const mapping: Record = { + log: 'log', + deny: 'deny', + challenge: 'challenge', + bypass: 'bypass', + rate_limit: 'rate_limit', + redirect: 'redirect', + allow: 'bypass', + block: 'deny', + } + + return mapping[type] +} + /** * Vercel types with no entry here (e.g. `target_path`, `protocol`, * `environment`, `geo_continent`, `ja4_digest`, `ja3_digest`, diff --git a/src/lib/types/vercel.ts b/src/lib/types/vercel.ts index 9b9d1c1..5b19342 100644 --- a/src/lib/types/vercel.ts +++ b/src/lib/types/vercel.ts @@ -3,7 +3,17 @@ * These types map directly to Vercel's Firewall API */ -import type { ActionType } from './common' +/** + * Vercel's native mitigation-action vocabulary — a strict subset of the + * unified `ActionType` (which also has `allow`/`block`, valid on + * Cloudflare/Fastly but not natively representable on Vercel; see + * `mapUnifiedActionToVercel` in providers/vercel/translator.ts, #262). + * Mirrors `actionTypeSchema` in schemas/firewallSchemas.ts — keep the two in + * sync. `CloudflareAction`/`FastlyRequestActionType` (types/cloudflare.ts, + * types/fastly.ts) already follow this same "dedicated native type, not the + * shared unified one" pattern; this brings Vercel in line with them. + */ +export type VercelActionType = 'log' | 'deny' | 'challenge' | 'bypass' | 'rate_limit' | 'redirect' /** * Vercel rule operators @@ -85,7 +95,7 @@ export interface VercelRedirect { * Vercel mitigation action */ export interface VercelMitigationAction { - action: ActionType + action: VercelActionType rateLimit?: VercelRateLimit | null redirect?: VercelRedirect | null actionDuration?: string | null // e.g., "1h", "1d", "permanent"