relayed 0.0.3__tar.gz → 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: relayed
3
- Version: 0.0.3
3
+ Version: 0.1.0
4
4
  Summary: Python client for Relayed, a webhook delivery service
5
5
  Project-URL: Homepage, https://github.com/JoeCosta7/Relayed
6
6
  Author-email: Joseph Costa <joedc3711@gmail.com>
@@ -6,7 +6,10 @@ from relayed._errors import (
6
6
  RelayedConflictError,
7
7
  RelayedClientError,
8
8
  RelayedServerError,
9
+ RelayedTimestampError,
10
+ RelayedSignatureError
9
11
  )
12
+ from relayed._webhooks import verify_webhook
10
13
 
11
14
  __all__ = [
12
15
  "RelayedClient",
@@ -16,6 +19,9 @@ __all__ = [
16
19
  "RelayedConflictError",
17
20
  "RelayedClientError",
18
21
  "RelayedServerError",
22
+ "RelayedTimestampError",
23
+ "RelayedSignatureError",
24
+ "verify_webhook"
19
25
  ]
20
26
 
21
27
  from importlib.metadata import version
@@ -21,3 +21,8 @@ class RelayedClientError(RelayedError):
21
21
  class RelayedServerError(RelayedError):
22
22
  """5xx — something went wrong on Relayed's end."""
23
23
 
24
+ class RelayedTimestampError(RelayedError):
25
+ """Raised when the timestamp of the webhook is outside the allowed tolerance."""
26
+
27
+ class RelayedSignatureError(RelayedError):
28
+ """Raised when the signature of the webhook does not match the expected signature."""
@@ -0,0 +1,19 @@
1
+
2
+ from datetime import datetime, timezone
3
+ import hmac
4
+
5
+ from ._errors import RelayedTimestampError, RelayedSignatureError
6
+
7
+ def verify_webhook(body: bytes,signature: str,timestamp: str,secret: str,tolerance_seconds: int = 300,) -> None:
8
+ try:
9
+ timestamp_int = int(timestamp)
10
+ except (ValueError, TypeError):
11
+ raise RelayedTimestampError("Webhook timestamp is missing or invalid.")
12
+ current_timestamp = int(datetime.now(timezone.utc).timestamp())
13
+ if abs(current_timestamp - timestamp_int) > tolerance_seconds:
14
+ raise RelayedTimestampError("Webhook timestamp is outside the allowed tolerance.")
15
+ reconstructed_payload = str(timestamp_int).encode("utf-8") + b"." + body
16
+ expected_signature = hmac.new(secret.encode(), reconstructed_payload, digestmod="sha256").hexdigest()
17
+ if not hmac.compare_digest(expected_signature, signature):
18
+ raise RelayedSignatureError("Webhook signature does not match the expected signature.")
19
+ return None
@@ -0,0 +1,62 @@
1
+ import hmac
2
+ from datetime import datetime, timezone
3
+
4
+ import pytest
5
+
6
+ from relayed._webhooks import verify_webhook
7
+ from relayed import RelayedSignatureError, RelayedTimestampError
8
+
9
+
10
+ def _sign(body: bytes, timestamp: int, secret: str) -> str:
11
+ """Produce the signature the worker would produce for this timestamp+body."""
12
+ signed_payload = str(timestamp).encode("utf-8") + b"." + body
13
+ return hmac.new(secret.encode(), signed_payload, digestmod="sha256").hexdigest()
14
+
15
+
16
+ def _now() -> int:
17
+ return int(datetime.now(timezone.utc).timestamp())
18
+
19
+
20
+ SECRET = "test-webhook-secret"
21
+
22
+
23
+ def test_verify_webhook_happy_path():
24
+ """A correctly-signed webhook with a fresh timestamp verifies without raising."""
25
+ body = b'{"event": "delivery.succeeded"}'
26
+ timestamp = _now()
27
+ signature = _sign(body, timestamp, SECRET)
28
+ assert verify_webhook(body, signature, str(timestamp), SECRET) is None
29
+
30
+
31
+ @pytest.mark.parametrize("bad_timestamp", ["", "abc", "1234abc", None])
32
+ def test_verify_webhook_invalid_timestamp_format(bad_timestamp):
33
+ """Malformed timestamp values raise RelayedTimestampError."""
34
+ body = b'{"event": "delivery.succeeded"}'
35
+ with pytest.raises(RelayedTimestampError):
36
+ verify_webhook(body, "0" * 64, bad_timestamp, SECRET)
37
+
38
+
39
+ def test_verify_webhook_timestamp_too_old():
40
+ """A timestamp more than tolerance_seconds in the past raises RelayedTimestampError."""
41
+ body = b'{"event": "delivery.succeeded"}'
42
+ timestamp = _now() - 400
43
+ signature = _sign(body, timestamp, SECRET)
44
+ with pytest.raises(RelayedTimestampError):
45
+ verify_webhook(body, signature, str(timestamp), SECRET)
46
+
47
+
48
+ def test_verify_webhook_timestamp_too_new():
49
+ """A timestamp more than tolerance_seconds in the future raises RelayedTimestampError."""
50
+ body = b'{"event": "delivery.succeeded"}'
51
+ timestamp = _now() + 400
52
+ signature = _sign(body, timestamp, SECRET)
53
+ with pytest.raises(RelayedTimestampError):
54
+ verify_webhook(body, signature, str(timestamp), SECRET)
55
+
56
+
57
+ def test_verify_webhook_bad_signature():
58
+ """A well-formed but incorrect signature raises RelayedSignatureError."""
59
+ body = b'{"event": "delivery.succeeded"}'
60
+ timestamp = _now()
61
+ with pytest.raises(RelayedSignatureError):
62
+ verify_webhook(body, "0" * 64, str(timestamp), SECRET)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes