{"version":3,"sources":["../src/server/webhooks/verify.ts","../src/server/webhooks/handler.ts"],"sourcesContent":["/**\n * Webhook signature verification using HMAC-SHA256.\n * Uses the Web Crypto API for cross-runtime compatibility (Node 18+, Deno, Bun, edge).\n */\n\nfunction hexEncode(buffer: ArrayBuffer): string {\n  const bytes = new Uint8Array(buffer)\n  let hex = \"\"\n  for (const byte of bytes) {\n    hex += byte.toString(16).padStart(2, \"0\")\n  }\n  return hex\n}\n\nfunction hexDecode(hex: string): Uint8Array {\n  const bytes = new Uint8Array(hex.length / 2)\n  for (let i = 0; i < hex.length; i += 2) {\n    bytes[i / 2] = Number.parseInt(hex.substring(i, i + 2), 16)\n  }\n  return bytes\n}\n\n/**\n * Constant-time comparison of two byte arrays.\n * Prevents timing attacks by always comparing all bytes regardless of mismatch.\n */\nfunction timingSafeEqual(a: Uint8Array, b: Uint8Array): boolean {\n  if (a.length !== b.length) return false\n  let result = 0\n  for (let i = 0; i < a.length; i++) {\n    result |= (a[i] ?? 0) ^ (b[i] ?? 0)\n  }\n  return result === 0\n}\n\n/**\n * Verify a Contentstack webhook signature using HMAC-SHA256.\n *\n * @param body - The raw request body string\n * @param signature - The hex-encoded signature from the `X-Contentstack-Request-Signature` header\n * @param secret - The webhook secret configured in Contentstack\n * @returns `true` if the signature is valid\n */\nexport async function verifyWebhookSignature(\n  body: string,\n  signature: string,\n  secret: string,\n): Promise<boolean> {\n  const encoder = new TextEncoder()\n  const key = await globalThis.crypto.subtle.importKey(\n    \"raw\",\n    encoder.encode(secret),\n    { name: \"HMAC\", hash: \"SHA-256\" },\n    false,\n    [\"sign\"],\n  )\n\n  const expectedBuffer = await globalThis.crypto.subtle.sign(\"HMAC\", key, encoder.encode(body))\n  const expectedBytes = new Uint8Array(expectedBuffer)\n  const signatureBytes = hexDecode(signature)\n\n  return timingSafeEqual(expectedBytes, signatureBytes)\n}\n","import { ContentstackAuthError } from \"../../index.js\"\nimport type { WebhookEvent, WebhookHandlerConfig } from \"./types.js\"\nimport { verifyWebhookSignature } from \"./verify.js\"\n\nconst SIGNATURE_HEADER = \"x-contentstack-request-signature\"\n\nfunction parseWebhookBody(body: string): WebhookEvent {\n  const raw = JSON.parse(body) as Record<string, unknown>\n  const module = raw.module as string\n  const event = raw.event as string\n  const type = `${module}.${event}`\n\n  return {\n    ...raw,\n    type,\n    module,\n    event,\n  } as WebhookEvent\n}\n\n/**\n * Create a webhook handler with signature verification and typed event parsing.\n *\n * @example\n * ```ts\n * const handler = createWebhookHandler({ secret: process.env.WEBHOOK_SECRET })\n *\n * // In a route handler:\n * const event = await handler.verify(request)\n * if (event.type === \"entry.publish\") {\n *   console.log(event.data.entry.uid)\n * }\n * ```\n */\nexport function createWebhookHandler(config: WebhookHandlerConfig) {\n  return {\n    /**\n     * Read the request body, verify the signature, and return a typed event.\n     * Throws `ContentstackAuthError` if the signature is missing or invalid.\n     */\n    async verify(request: Request): Promise<WebhookEvent> {\n      const body = await request.text()\n      const signature = request.headers.get(SIGNATURE_HEADER)\n\n      if (!signature) {\n        throw new ContentstackAuthError(\"Missing webhook signature header\", {\n          status: 401,\n          requestPath: new URL(request.url).pathname,\n        })\n      }\n\n      const valid = await verifyWebhookSignature(body, signature, config.secret)\n      if (!valid) {\n        throw new ContentstackAuthError(\"Invalid webhook signature\", {\n          status: 401,\n          requestPath: new URL(request.url).pathname,\n        })\n      }\n\n      return parseWebhookBody(body)\n    },\n\n    /**\n     * Read the request body and return a typed event without verifying the signature.\n     */\n    async parse(request: Request): Promise<WebhookEvent> {\n      const body = await request.text()\n      return parseWebhookBody(body)\n    },\n  }\n}\n"],"mappings":";;;;;AAcA,SAAS,UAAU,KAAyB;AAC1C,QAAM,QAAQ,IAAI,WAAW,IAAI,SAAS,CAAC;AAC3C,WAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK,GAAG;AACtC,UAAM,IAAI,CAAC,IAAI,OAAO,SAAS,IAAI,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE;AAAA,EAC5D;AACA,SAAO;AACT;AAMA,SAAS,gBAAgB,GAAe,GAAwB;AAC9D,MAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,MAAI,SAAS;AACb,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK;AACjC,eAAW,EAAE,CAAC,KAAK,MAAM,EAAE,CAAC,KAAK;AAAA,EACnC;AACA,SAAO,WAAW;AACpB;AAUA,eAAsB,uBACpB,MACA,WACA,QACkB;AAClB,QAAM,UAAU,IAAI,YAAY;AAChC,QAAM,MAAM,MAAM,WAAW,OAAO,OAAO;AAAA,IACzC;AAAA,IACA,QAAQ,OAAO,MAAM;AAAA,IACrB,EAAE,MAAM,QAAQ,MAAM,UAAU;AAAA,IAChC;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AAEA,QAAM,iBAAiB,MAAM,WAAW,OAAO,OAAO,KAAK,QAAQ,KAAK,QAAQ,OAAO,IAAI,CAAC;AAC5F,QAAM,gBAAgB,IAAI,WAAW,cAAc;AACnD,QAAM,iBAAiB,UAAU,SAAS;AAE1C,SAAO,gBAAgB,eAAe,cAAc;AACtD;;;AC1DA,IAAM,mBAAmB;AAEzB,SAAS,iBAAiB,MAA4B;AACpD,QAAM,MAAM,KAAK,MAAM,IAAI;AAC3B,QAAM,SAAS,IAAI;AACnB,QAAM,QAAQ,IAAI;AAClB,QAAM,OAAO,GAAG,MAAM,IAAI,KAAK;AAE/B,SAAO;AAAA,IACL,GAAG;AAAA,IACH;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAgBO,SAAS,qBAAqB,QAA8B;AACjE,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA,IAKL,MAAM,OAAO,SAAyC;AACpD,YAAM,OAAO,MAAM,QAAQ,KAAK;AAChC,YAAM,YAAY,QAAQ,QAAQ,IAAI,gBAAgB;AAEtD,UAAI,CAAC,WAAW;AACd,cAAM,IAAI,sBAAsB,oCAAoC;AAAA,UAClE,QAAQ;AAAA,UACR,aAAa,IAAI,IAAI,QAAQ,GAAG,EAAE;AAAA,QACpC,CAAC;AAAA,MACH;AAEA,YAAM,QAAQ,MAAM,uBAAuB,MAAM,WAAW,OAAO,MAAM;AACzE,UAAI,CAAC,OAAO;AACV,cAAM,IAAI,sBAAsB,6BAA6B;AAAA,UAC3D,QAAQ;AAAA,UACR,aAAa,IAAI,IAAI,QAAQ,GAAG,EAAE;AAAA,QACpC,CAAC;AAAA,MACH;AAEA,aAAO,iBAAiB,IAAI;AAAA,IAC9B;AAAA;AAAA;AAAA;AAAA,IAKA,MAAM,MAAM,SAAyC;AACnD,YAAM,OAAO,MAAM,QAAQ,KAAK;AAChC,aAAO,iBAAiB,IAAI;AAAA,IAC9B;AAAA,EACF;AACF;","names":[]}