supaapps-platform93-webhooks 0.1.0a2__py3-none-any.whl

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.
@@ -0,0 +1,17 @@
1
+ from .verifier import (
2
+ PLATFORM_EVENT_VERSIONS,
3
+ Platform93Event,
4
+ assert_supported_platform_event,
5
+ is_custom_event,
6
+ is_platform_event,
7
+ verify_webhook,
8
+ )
9
+
10
+ __all__ = [
11
+ "PLATFORM_EVENT_VERSIONS",
12
+ "Platform93Event",
13
+ "assert_supported_platform_event",
14
+ "is_custom_event",
15
+ "is_platform_event",
16
+ "verify_webhook",
17
+ ]
@@ -0,0 +1,87 @@
1
+ import hashlib
2
+ import hmac
3
+ import json
4
+ import time
5
+ from typing import Any, Literal, TypedDict
6
+
7
+
8
+ class Platform93Event(TypedDict):
9
+ specversion: Literal["1.0"]
10
+ id: str
11
+ source: str
12
+ type: str
13
+ contract_source: Literal["platform93", "application"]
14
+ time: str
15
+ application_id: str
16
+ schema_version: str
17
+ subject: str | None
18
+ actor: dict[str, Any] | None
19
+ correlation_id: str | None
20
+ causation_id: str | None
21
+ data: dict[str, Any]
22
+
23
+
24
+ PLATFORM_EVENT_VERSIONS = dict.fromkeys((
25
+ "organization.created", "organization.retired", "organization.restored",
26
+ "application.created", "application.retired", "application.restored",
27
+ "delegation.created", "delegation.exchanged", "delegation.revoked",
28
+ "entitlement.granted", "local_entitlement_request.created", "local_entitlement_request.approved",
29
+ "oauth.consent_revoked", "platform93.webhook.test", "user.created",
30
+ "storage.object.upload_requested", "storage.object.ready", "storage.object.deleted",
31
+ "storage.provider.verified", "storage.provider.disabled",
32
+ "user.email_verified", "user.email_unverified", "user.organization_verified",
33
+ "user.organization_unverified", "user.email_changed", "user.password_reset",
34
+ "user.pending_deletion", "user.anonymized", "user.deleted", "user.suspended", "user.restored",
35
+ "workspace.invitation_created", "workspace.invitation_accepted", "workspace.owner_transferred",
36
+ ), "1.0")
37
+
38
+
39
+ def verify_webhook(
40
+ raw_body: bytes,
41
+ header: str,
42
+ secret: str,
43
+ tolerance_seconds: int = 300,
44
+ now: float | None = None,
45
+ ) -> Platform93Event:
46
+ values: dict[str, list[str]] = {}
47
+ for part in header.split(","):
48
+ if "=" in part:
49
+ key, value = part.split("=", 1)
50
+ values.setdefault(key, []).append(value)
51
+ timestamp = int(values.get("t", ["0"])[0])
52
+ current = time.time() if now is None else now
53
+ if not timestamp or abs(current - timestamp) > tolerance_seconds:
54
+ raise ValueError("Platform93 webhook timestamp rejected")
55
+ expected = hmac.new(
56
+ secret.encode(), str(timestamp).encode() + b"." + raw_body, hashlib.sha256
57
+ ).hexdigest()
58
+ if not any(
59
+ hmac.compare_digest(expected, signature) for signature in values.get("v1", [])
60
+ ):
61
+ raise ValueError("Platform93 webhook signature rejected")
62
+ event = json.loads(raw_body)
63
+ if (
64
+ event.get("specversion") != "1.0"
65
+ or not isinstance(event.get("type"), str)
66
+ or event.get("contract_source") not in ("platform93", "application")
67
+ or not isinstance(event.get("schema_version"), str)
68
+ or not isinstance(event.get("data"), dict)
69
+ ):
70
+ raise ValueError("Platform93 webhook envelope rejected")
71
+ return event
72
+
73
+
74
+ def is_platform_event(event: Platform93Event) -> bool:
75
+ return event["contract_source"] == "platform93" and event["type"] in PLATFORM_EVENT_VERSIONS
76
+
77
+
78
+ def is_custom_event(event: Platform93Event) -> bool:
79
+ return event["contract_source"] == "application"
80
+
81
+
82
+ def assert_supported_platform_event(event: Platform93Event) -> None:
83
+ if not is_platform_event(event):
84
+ raise ValueError("Unknown Platform93 event contract")
85
+ supported = PLATFORM_EVENT_VERSIONS[event["type"]]
86
+ if event["schema_version"].split(".", 1)[0] != supported.split(".", 1)[0]:
87
+ raise ValueError("Unsupported Platform93 event schema version")
@@ -0,0 +1,22 @@
1
+ Metadata-Version: 2.4
2
+ Name: supaapps-platform93-webhooks
3
+ Version: 0.1.0a2
4
+ Summary: Platform93 webhook signature verification
5
+ Project-URL: Homepage, https://github.com/supaapps/platform93
6
+ Project-URL: Repository, https://github.com/supaapps/platform93
7
+ Project-URL: Issues, https://github.com/supaapps/platform93/issues
8
+ License-Expression: Apache-2.0
9
+ Requires-Python: >=3.11
10
+ Description-Content-Type: text/markdown
11
+
12
+ # supaapps-platform93-webhooks
13
+
14
+ Verification helpers for timestamped Platform93 webhook signatures over the exact raw
15
+ request body.
16
+
17
+ ```bash
18
+ pip install supaapps-platform93-webhooks
19
+ ```
20
+
21
+ See the [Platform93 repository](https://github.com/supaapps/platform93) for event
22
+ contracts and integration documentation.
@@ -0,0 +1,5 @@
1
+ platform93_webhooks/__init__.py,sha256=nogaE1VVG2lP9zHOEtX8YImQQT15E_5VkNB0JNznJ2A,355
2
+ platform93_webhooks/verifier.py,sha256=Euin5AZajllOKO5l3du-PaGkT2kZjsiAZJwUXuXsHP8,3371
3
+ supaapps_platform93_webhooks-0.1.0a2.dist-info/METADATA,sha256=jWXZApSbgTyrd_jwkT1ZmalO8aUiRzxAI2-KIpHXrr0,724
4
+ supaapps_platform93_webhooks-0.1.0a2.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
5
+ supaapps_platform93_webhooks-0.1.0a2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any