Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions src/lib/providers/vercel/__tests__/translator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
36 changes: 34 additions & 2 deletions src/lib/providers/vercel/translator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -166,7 +167,7 @@ export function unifiedToVercel(rule: UnifiedRule): TranslationResult<VercelCust
conditionGroup: conditionGroups,
action: {
mitigate: {
action: rule.action.type,
action: mapUnifiedActionToVercel(rule.action.type),
rateLimit: rule.action.rateLimit
? {
requests: rule.action.rateLimit.requests,
Expand Down Expand Up @@ -347,6 +348,37 @@ function mapUnifiedOperatorToVercel(op: Operator): { op: VercelRuleOperator; for
return mapping[op] ?? null
}

/**
* Maps a unified action type to Vercel's native action enum
* (`VercelActionType`, types/vercel.ts) — a strict subset of the unified
* `ActionType`. Previously `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 unified schema) and was dispatched to Vercel's real API carrying a
* `mitigate.action` value Vercel's own native schema says is invalid. See
* #262.
*
* `allow` -> `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<ActionType, VercelActionType> = {
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`,
Expand Down
14 changes: 12 additions & 2 deletions src/lib/types/vercel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
Loading