{"record":{"id":"33e6611ec7ac6aae","repo":"yikart/AiToEarn","slug":"channelwebhookchallengecodemissing","errorCode":null,"errorMessage":"ChannelWebhookChallengeCodeMissing","messagePattern":"ChannelWebhookChallengeCodeMissing","errorType":"http","errorClass":null,"httpStatus":400,"severity":"warning","filePath":"project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/linkedin/linkedin-webhook.provider.ts","lineNumber":41,"sourceCode":"      return\n    }\n    if (!this.verify(request)) {\n      this.logger.warn({ platform: AccountType.LinkedIn }, 'LinkedIn webhook signature invalid')\n      response.status(401).send(getCodeMessage(ResponseCode.ChannelWebhookInvalidSignature, undefined, getLocale()))\n      return\n    }\n    const payload: LinkedInWebhookPayload = LinkedInWebhookPayloadSchema.parse(request.body)\n    this.logger.log({\n      platform: AccountType.LinkedIn,\n      eventCount: payload.events?.length ?? 0,\n    }, 'LinkedIn webhook events received')\n    response.status(200).json({ status: 'ok' })\n  }\n\n  private handleChallenge(request: Request, response: Response): void {\n    const query = request.query as LinkedInWebhookChallengeQuery\n    if (!query.challengeCode) {\n      response.status(400).send(getCodeMessage(ResponseCode.ChannelWebhookChallengeCodeMissing, undefined, getLocale()))\n      return\n    }\n\n    const secret = this.config.webhookSecret || this.config.clientSecret\n    const challengeResponse = createHmac('sha256', secret).update(query.challengeCode).digest('hex')\n    response.status(200).json({ challengeCode: query.challengeCode, challengeResponse })\n  }\n\n  private verify(request: RawBodyRequest): boolean {\n    const rawBody = request.rawBody\n    const signature = this.getHeader(request, 'x-li-signature')\n    const secret = this.config.webhookSecret || this.config.clientSecret\n    if (!signature || !rawBody || !secret) {\n      return false\n    }\n\n    const digest = createHmac('sha256', secret).update(rawBody).digest('hex')\n    const expected = signature.startsWith('sha256=') ? `sha256=${digest}` : digest","sourceCodeStart":23,"sourceCodeEnd":59,"githubUrl":"https://github.com/yikart/AiToEarn/blob/d3aa8bea5b146a8675607cf0144d891aad3e9683/project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/linkedin/linkedin-webhook.provider.ts#L23-L59","documentation":"LinkedIn's challenge handshake sends a challengeCode query parameter; the provider must answer with an HMAC-SHA256 (hex) of that code. If challengeCode is absent the provider cannot build the response and returns 400 with ChannelWebhookChallengeCodeMissing.","triggerScenarios":"GET request to the LinkedIn webhook endpoint without a challengeCode query parameter (e.g., health checks, browsers, or misconfigured subscription URLs).","commonSituations":"Uptime monitors pinging the webhook GET URL; LinkedIn configured with a wrong callback URL that omits the challenge flow; manual URL testing without query params.","solutions":["Register the exact webhook URL LinkedIn documents so the challenge flow includes challengeCode","Exclude the webhook GET endpoint from synthetic health checks or give monitors a dedicated health route","Test the handshake by replicating LinkedIn's documented GET with challengeCode set"],"exampleFix":"// before\ncurl https://api.example.com/webhooks/linkedin\n// after\ncurl \"https://api.example.com/webhooks/linkedin?challengeCode=abc123\"","handlingStrategy":"validation","validationCode":"const url = new URL(callbackUrl)\nif (!url.searchParams.get('challengeCode')) {\n  console.warn('LinkedIn challenge handshake requires challengeCode query param')\n}","typeGuard":"function hasChallengeCode(q: unknown): q is { challengeCode: string } {\n  return typeof (q as any)?.challengeCode === 'string' && (q as any).challengeCode.length > 0\n}","tryCatchPattern":null,"preventionTips":["Don't point uptime monitors at the challenge endpoint","Register exactly the documented callback URL format","Simulate the full handshake before going live"],"tags":["webhook","challenge","linkedin","handshake"],"backgroundTag":"webhook-challenge-handshake-failed","analyzedSha":"d3aa8bea5b146a8675607cf0144d891aad3e9683","analyzedAt":"2026-08-31T14:19:24.185Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}