{"record":{"id":"d782edf9a3c2b3d5","repo":"moeru-ai/airi","slug":"missing-signature","errorCode":"MISSING_SIGNATURE","errorMessage":"No signature","messagePattern":"No signature","errorType":"http","errorClass":"ApiError","httpStatus":400,"severity":"error","filePath":"server/apps/api/src/routes/stripe/operations/webhook.ts","lineNumber":97,"sourceCode":"\n/**\n * Verifies a Stripe webhook, maps a Checkout Session to a claim receipt,\n * then calls Payment CORE. Unknown events are ignored.\n */\nexport function createWebhookOperation(\n  stripe: Stripe | null,\n  webhookSecret: string | null,\n  payment: PaymentService,\n  db: Database,\n  metrics: RevenueMetrics | null,\n  productEventService: ProductEventService | null,\n) {\n  return async (signature: string | null, body: string): Promise<{ received: true }> => {\n    if (!stripe || !webhookSecret)\n      throw createServiceUnavailableError('Stripe is not configured', 'STRIPE_NOT_CONFIGURED')\n\n    if (!signature)\n      throw createBadRequestError('No signature', 'MISSING_SIGNATURE')\n\n    let event: Stripe.Event\n    try {\n      event = stripe.webhooks.constructEvent(body, signature, webhookSecret)\n    }\n    catch (err: unknown) {\n      throw createBadRequestError(`Webhook Error: ${errorMessageFromUnknown(err)}`, 'WEBHOOK_ERROR')\n    }\n\n    logger.withFields({ type: event.type, id: event.id }).log('Webhook event received')\n    metrics?.stripeEvents.add(1, { event_type: event.type })\n\n    switch (event.type) {\n      case 'checkout.session.completed':\n      case 'checkout.session.async_payment_succeeded': {\n        const session = parse(checkoutSessionSchema, event.data.object)\n        if (session.mode !== 'payment') {\n          logger.withFields({ sessionId: session.id, mode: session.mode }).log('Ignoring non-payment checkout session')","sourceCodeStart":79,"sourceCodeEnd":115,"githubUrl":"https://github.com/moeru-ai/airi/blob/438a067dde47aa0bdb46c2323d1fe293dc805218/server/apps/api/src/routes/stripe/operations/webhook.ts#L79-L115","documentation":"Stripe signs webhook deliveries with a signature header (Stripe-Signature). The webhook operation requires this header to verify authenticity; a request without it is rejected with 400 MISSING_SIGNATURE before any event parsing.","triggerScenarios":"POSTing to the webhook endpoint without the Stripe-Signature header: manual curl tests, proxies/load balancers stripping the header, or a misconfigured reverse proxy that does not forward raw headers.","commonSituations":"Testing the endpoint by hand with a raw JSON body; Caddy/nginx config dropping the signature header; hitting the endpoint with a tool that doesn't replicate Stripe's headers.","solutions":["Use the Stripe CLI (`stripe listen --forward-to localhost:PORT/...`) which sends proper signatures.","If behind a proxy, ensure the Stripe-Signature header is forwarded unchanged to the API.","Never send webhook events manually without replicating the signature header.","Check that the client/request library is not stripping custom headers."],"exampleFix":"// before\ncurl -X POST http://localhost:3000/api/webhooks/stripe -d '{\"type\":\"x\"}'\n// after\nstripe listen --forward-to localhost:3000/api/webhooks/stripe","handlingStrategy":"validation","validationCode":"const signature = req.headers.get('stripe-signature')\nif (!signature)\n  throw new Error('request missing stripe-signature header; not a genuine Stripe delivery')","typeGuard":null,"tryCatchPattern":"try {\n  const res = await fetch(webhookUrl, { method: 'POST', headers: { 'Stripe-Signature': signature, 'Content-Type': 'application/json' }, body: rawBody })\n} catch (e) {\n  if (e.code === 'MISSING_SIGNATURE') {\n    // ensure your HTTP client forwards the signature header\n  }\n}","preventionTips":["Always forward the Stripe-Signature header verbatim through proxies (check Caddy/nginx config).","Test webhooks with `stripe listen --forward-to` instead of manual curl.","Never re-serialize webhook bodies before verification."],"tags":["stripe","webhook","signature","bad-request"],"backgroundTag":"missing-required-argument","analyzedSha":"438a067dde47aa0bdb46c2323d1fe293dc805218","analyzedAt":"2026-09-17T01:14:42.644Z","contentChangedAt":"2026-09-17T01:14:42.644Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}