toeverything/AFFiNE · error · CaptchaVerificationFailed

captcha_verification_failed

captcha_verification_failed

Error message

Invalid Credential

What it means

Thrown by CaptchaService.assertValidCredential when the raw credential object passed in fails the zod validator — wrong shape, missing required fields (provider, token, challenge), or wrong types. The service normalizes any malformed credential into CaptchaVerificationFailed('Invalid Credential') so callers get one clean error instead of zod internals, and counts it under the 'invalid_credential' metric.

Source

Thrown at packages/backend/server/src/plugins/captcha/service.ts:177

      provider,
      challenge,
      resource,
    };
  }

  assertValidCredential(credential: any): Credential {
    try {
      return validator.parse(credential);
    } catch {
      metrics.auth.counter('captcha_verification').add(1, {
        provider:
          credential?.provider === 'hashcash' ||
          credential?.provider === 'turnstile'
            ? credential.provider
            : 'unknown',
        result: 'invalid_credential',
      });
      throw new CaptchaVerificationFailed('Invalid Credential');
    }
  }

  async verifyRequest(credential: Credential, req: Request) {
    if (credential.provider === 'hashcash') {
      if (!credential.challenge) {
        metrics.auth.counter('captcha_verification').add(1, {
          provider: 'hashcash',
          result: 'missing_challenge',
        });
        throw new CaptchaVerificationFailed('Missing Challenge');
      }
      const resource = await this.challenges.consume<string>(
        'captcha',
        credential.challenge
      );
      if (!resource) {
        metrics.auth.counter('captcha_verification').add(1, {

View on GitHub (pinned to b4c8548c09)

Solutions

  1. Log/inspect the credential object at the boundary and compare it to the expected Credential schema (provider: 'hashcash'|'turnstile', token: string, optional challenge).
  2. Use the official client-side captcha helper to mint the credential (Turnstile widget or the hashcash challenge flow) before calling the protected endpoint.
  3. Fix payload key names in custom clients (e.g. token vs response) to match the schema.
  4. Ensure the credential arrives as a parsed object, not a serialized string.

Example fix

// before
await fetch('/api/auth/signIn', {
  method: 'POST',
  body: JSON.stringify({ email, password, credential: JSON.stringify(token) }),
});

// after — pass the structured credential object
await fetch('/api/auth/signIn', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ email, password, credential: { provider: 'turnstile', token } }),
});
Defensive patterns

Strategy: validation

Validate before calling

import { z } from 'zod';
const credentialSchema = z.object({
  provider: z.enum(['hashcash', 'turnstile']),
  token: z.string().min(1),
  challenge: z.string().optional(),
});
const parsed = credentialSchema.safeParse(rawCredential);
if (!parsed.success) fixCredentialShape(parsed.error);

Type guard

function isCaptchaCredential(v: unknown): v is { provider: 'hashcash' | 'turnstile'; token: string; challenge?: string } {
  return (
    !!v && typeof v === 'object' &&
    ['hashcash', 'turnstile'].includes((v as any).provider) &&
    typeof (v as any).token === 'string' && (v as any).token.length > 0
  );
}

Try / catch

try {
  captchaService.assertValidCredential(credential);
} catch (e) {
  if (e?.code === 'captcha_verification_failed' && e.message === 'Invalid Credential') {
    return badRequest('credential malformed — re-run the captcha widget');
  }
  throw e;
}

Prevention

When it happens

Trigger: Client sends a captcha credential without a provider field, with provider not equal to 'hashcash' or 'turnstile', without a token string, or with a challenge of the wrong type; hand-rolled API clients or scripts posting form data that never went through the captcha widget; JSON payloads where credential is a string instead of an object.

Common situations: Custom automation hitting sign-up/sign-in endpoints without generating a Turnstile token or hashcash stamp; frontend changes that renamed the credential payload keys; proxy/middleware stripping the captcha header before it reaches the service.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18). Data as JSON: /api/errors/d5b7d56bb8058879. Report an issue: GitHub.