supaapps-platform93-webhooks 0.1.0a2__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.
@@ -0,0 +1,38 @@
1
+ # Local configuration and secrets
2
+ .env
3
+ .env.*
4
+ !.env.example
5
+ *.pem
6
+ *.key
7
+ *.p12
8
+ *.pfx
9
+
10
+ # Local Compose credentials
11
+ deploy/compose/secrets/
12
+
13
+ # Editors and operating systems
14
+ .DS_Store
15
+ .idea/
16
+ .vscode/
17
+
18
+ # Dependencies
19
+ node_modules/
20
+ **/node_modules/
21
+ vendor/
22
+
23
+ # Build and test output
24
+ dist/
25
+ build/
26
+ coverage/
27
+ tmp/
28
+ **/.next/
29
+ **/out/
30
+ **/test-results/
31
+ **/playwright-report/
32
+ *.log
33
+ *.test
34
+ *.prof
35
+ *.tsbuildinfo
36
+ /platform93
37
+ __pycache__/
38
+ *.py[cod]
@@ -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,11 @@
1
+ # supaapps-platform93-webhooks
2
+
3
+ Verification helpers for timestamped Platform93 webhook signatures over the exact raw
4
+ request body.
5
+
6
+ ```bash
7
+ pip install supaapps-platform93-webhooks
8
+ ```
9
+
10
+ See the [Platform93 repository](https://github.com/supaapps/platform93) for event
11
+ contracts and integration documentation.
@@ -0,0 +1,19 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "supaapps-platform93-webhooks"
7
+ version = "0.1.0-alpha.2"
8
+ description = "Platform93 webhook signature verification"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "Apache-2.0"
12
+
13
+ [project.urls]
14
+ Homepage = "https://github.com/supaapps/platform93"
15
+ Repository = "https://github.com/supaapps/platform93"
16
+ Issues = "https://github.com/supaapps/platform93/issues"
17
+
18
+ [tool.hatch.build.targets.wheel]
19
+ packages = ["src/platform93_webhooks"]
@@ -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,18 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ from platform93_webhooks import assert_supported_platform_event, is_platform_event, verify_webhook
5
+
6
+
7
+ fixture = json.loads(
8
+ (Path(__file__).parents[4] / "conformance" / "webhook.json").read_text()
9
+ )
10
+ event = verify_webhook(
11
+ fixture["raw_body"].encode(),
12
+ fixture["signature"],
13
+ fixture["secret"],
14
+ now=fixture["timestamp"],
15
+ )
16
+ assert event["id"] == "01900000-0000-7000-8000-000000000001"
17
+ assert is_platform_event(event)
18
+ assert_supported_platform_event(event)