MCPcopy Create free account
hub / github.com/liveblocks/liveblocks / WebhookHandler

Class WebhookHandler

packages/liveblocks-python/liveblocks/webhooks.py:406–525  ·  view source on GitHub ↗

Verify incoming Liveblocks webhook requests. Usage:: handler = WebhookHandler("whsec_...") event = handler.verify_request(headers=request.headers, raw_body=request.body)

Source from the content-addressed store, hash-verified

404
405
406class WebhookHandler:
407 """Verify incoming Liveblocks webhook requests.
408
409 Usage::
410
411 handler = WebhookHandler("whsec_...")
412 event = handler.verify_request(headers=request.headers, raw_body=request.body)
413 """
414
415 def __init__(self, secret: str) -> None:
416 if not secret or not isinstance(secret, str):
417 raise ValueError("Secret is required and must be a non-empty string")
418
419 if not secret.startswith(_SECRET_PREFIX):
420 raise ValueError("Invalid secret, must start with whsec_")
421
422 secret_key = secret[len(_SECRET_PREFIX) :]
423 try:
424 self._secret_bytes = base64.b64decode(secret_key)
425 except Exception:
426 raise ValueError(
427 "Webhook secret contains invalid base64 after the 'whsec_' prefix. "
428 "Please copy the full secret from your Liveblocks dashboard."
429 ) from None
430
431 def verify_request(self, *, headers: dict[str, str], raw_body: str) -> WebhookEvent:
432 """Verify a webhook request and return the parsed event.
433
434 Args:
435 headers: The HTTP headers as a string-to-string mapping.
436 raw_body: The raw request body as a string (do **not** parse it first).
437
438 Returns:
439 The parsed webhook event dictionary.
440
441 Raises:
442 ValueError: If the request cannot be verified.
443 """
444 webhook_id, timestamp, raw_signatures = self._verify_headers(headers)
445
446 if not isinstance(raw_body, str):
447 raise ValueError(
448 f"Invalid raw_body, must be a string, got {type(raw_body).__name__!r} instead. "
449 "Make sure you pass the raw request body string, not a parsed object."
450 )
451
452 self._verify_timestamp(timestamp)
453
454 signature = self._sign(f"{webhook_id}.{timestamp}.{raw_body}")
455
456 expected_signatures = [
457 parts[1] for raw_sig in raw_signatures.split(" ") if len(parts := raw_sig.split(",")) > 1
458 ]
459
460 if not any(hmac.compare_digest(signature, s) for s in expected_signatures):
461 raise ValueError(
462 f"Invalid signature for webhook {webhook_id}. "
463 "Make sure you are using the correct webhook secret "

Calls

no outgoing calls