{"record":{"id":"d5b7d56bb8058879","repo":"toeverything/AFFiNE","slug":"captcha-verification-failed","errorCode":"captcha_verification_failed","errorMessage":"Invalid Credential","messagePattern":"Invalid Credential","errorType":"exception","errorClass":"CaptchaVerificationFailed","httpStatus":400,"severity":"error","filePath":"packages/backend/server/src/plugins/captcha/service.ts","lineNumber":177,"sourceCode":"      provider,\n      challenge,\n      resource,\n    };\n  }\n\n  assertValidCredential(credential: any): Credential {\n    try {\n      return validator.parse(credential);\n    } catch {\n      metrics.auth.counter('captcha_verification').add(1, {\n        provider:\n          credential?.provider === 'hashcash' ||\n          credential?.provider === 'turnstile'\n            ? credential.provider\n            : 'unknown',\n        result: 'invalid_credential',\n      });\n      throw new CaptchaVerificationFailed('Invalid Credential');\n    }\n  }\n\n  async verifyRequest(credential: Credential, req: Request) {\n    if (credential.provider === 'hashcash') {\n      if (!credential.challenge) {\n        metrics.auth.counter('captcha_verification').add(1, {\n          provider: 'hashcash',\n          result: 'missing_challenge',\n        });\n        throw new CaptchaVerificationFailed('Missing Challenge');\n      }\n      const resource = await this.challenges.consume<string>(\n        'captcha',\n        credential.challenge\n      );\n      if (!resource) {\n        metrics.auth.counter('captcha_verification').add(1, {","sourceCodeStart":159,"sourceCodeEnd":195,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b4c8548c09da21b2898443559a5b846f0ccf5dd8/packages/backend/server/src/plugins/captcha/service.ts#L159-L195","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Log/inspect the credential object at the boundary and compare it to the expected Credential schema (provider: 'hashcash'|'turnstile', token: string, optional challenge).","Use the official client-side captcha helper to mint the credential (Turnstile widget or the hashcash challenge flow) before calling the protected endpoint.","Fix payload key names in custom clients (e.g. token vs response) to match the schema.","Ensure the credential arrives as a parsed object, not a serialized string."],"exampleFix":"// before\nawait fetch('/api/auth/signIn', {\n  method: 'POST',\n  body: JSON.stringify({ email, password, credential: JSON.stringify(token) }),\n});\n\n// after — pass the structured credential object\nawait fetch('/api/auth/signIn', {\n  method: 'POST',\n  headers: { 'content-type': 'application/json' },\n  body: JSON.stringify({ email, password, credential: { provider: 'turnstile', token } }),\n});","handlingStrategy":"validation","validationCode":"import { z } from 'zod';\nconst credentialSchema = z.object({\n  provider: z.enum(['hashcash', 'turnstile']),\n  token: z.string().min(1),\n  challenge: z.string().optional(),\n});\nconst parsed = credentialSchema.safeParse(rawCredential);\nif (!parsed.success) fixCredentialShape(parsed.error);","typeGuard":"function isCaptchaCredential(v: unknown): v is { provider: 'hashcash' | 'turnstile'; token: string; challenge?: string } {\n  return (\n    !!v && typeof v === 'object' &&\n    ['hashcash', 'turnstile'].includes((v as any).provider) &&\n    typeof (v as any).token === 'string' && (v as any).token.length > 0\n  );\n}","tryCatchPattern":"try {\n  captchaService.assertValidCredential(credential);\n} catch (e) {\n  if (e?.code === 'captcha_verification_failed' && e.message === 'Invalid Credential') {\n    return badRequest('credential malformed — re-run the captcha widget');\n  }\n  throw e;\n}","preventionTips":["Mint credentials only through the provided client-side captcha helper/widget.","Validate the credential shape client-side before submitting protected requests.","Never stringify-then-nest the credential; send the structured object in JSON."],"tags":["captcha","validation","zod","credentials","input-shape"],"backgroundTag":"schema-validation-failed","analyzedSha":"b4c8548c09da21b2898443559a5b846f0ccf5dd8","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}