Login With Captcha Gate
Most forms gate submission on field values — email is present, password is filled in, that kind of thing. But sometimes availability depends on state the form doesn’t own. A captcha token comes from an external widget. A permission check lives on the server. A feature flag is set at the session level.
Umpire handles those through conditions: declared inputs that arrive alongside field values at evaluation time. This example shows a login form where submit depends on three separate rules — two field-driven, one condition-driven — and how they aggregate into a single availability result.
The Rules
Section titled “The Rules”import { check, enabledWhen, umpire } from '@umpire/core'
const loginFields = { email: { required: true, isEmpty: (value) => !value }, password: { required: true, isEmpty: (value) => !value }, submit: { required: true },}
type LoginConditions = { captchaToken: string | null}
const loginUmp = umpire<typeof loginFields, LoginConditions>({ fields: loginFields, rules: [ enabledWhen('submit', (_values, conditions) => !!conditions.captchaToken, { reason: 'Complete the captcha to continue', }), enabledWhen('submit', check('email', /^[^\s@]+@[^\s@]+\.[^\s@]+$/), { reason: 'Enter a valid email address', }), enabledWhen('submit', ({ password }) => !!password, { reason: 'Enter a password', }), ],})Step 1: No Captcha Yet
Section titled “Step 1: No Captcha Yet”const result = loginUmp.check( { email: 'user@example.com', password: 'hunter2' }, { captchaToken: null },)result.submit// {// enabled: false,// required: false,// reason: 'Complete the captcha to continue',// reasons: ['Complete the captcha to continue'],// }The field is disabled, so required is suppressed even though the field definition says required: true.
Step 2: Captcha Solved
Section titled “Step 2: Captcha Solved”const result = loginUmp.check( { email: 'user@example.com', password: 'hunter2' }, { captchaToken: 'cf-turnstile-xxxx' },)Now submit is enabled. The external gate has been satisfied and the field checks pass.
Step 3: Multiple Failures Aggregate
Section titled “Step 3: Multiple Failures Aggregate”const result = loginUmp.check( { email: 'not-an-email', password: '' }, { captchaToken: 'cf-turnstile-xxxx' },)result.submit// {// enabled: false,// required: false,// reason: 'Enter a valid email address',// reasons: ['Enter a valid email address', 'Enter a password'],// }Two rules fail:
- the email regex bridge via
check() - the password presence predicate
Because the email rule was declared first, its message becomes the primary reason.
Why Conditions Matter Here
Section titled “Why Conditions Matter Here”The captcha token is not a field value the form owns. It comes from an external system, so it belongs in conditions.
That same pattern applies to:
- feature flags
- plan tiers
- abuse cooldowns
- permission checks
- remote configuration gates