diff --git a/CHANGELOG.md b/CHANGELOG.md index cca7677..69e2f9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Check our main [developer changelog](https://developer.paddle.com/?utm_source=dx ### Added - Added support for `wechat_pay` payment method type. See [related changelog](https://developer.paddle.com/changelog/2025/wechat-pay-payment-method?utm_source=dx&utm_medium=paddle-python-sdk) +- Added `api_key_exposure.created` event support. See [related changelog](https://developer.paddle.com/changelog/2025/secret-scanning?utm_source=dx&utm_medium=paddle-python-sdk) - Added `grand_total_tax` to transaction totals and adjusted totals. See [related changelog](https://developer.paddle.com/changelog/2026/grand-total-tax-field?utm_source=dx&utm_medium=paddle-python-sdk) ## 1.12.0 - 2025-11-12 diff --git a/paddle_billing/Entities/Events/EventTypeName.py b/paddle_billing/Entities/Events/EventTypeName.py index 5d07c17..91bce10 100644 --- a/paddle_billing/Entities/Events/EventTypeName.py +++ b/paddle_billing/Entities/Events/EventTypeName.py @@ -10,6 +10,7 @@ class EventTypeName(PaddleStrEnum, metaclass=PaddleStrEnumMeta): ApiKeyCreated: "EventTypeName" = "api_key.created" ApiKeyExpired: "EventTypeName" = "api_key.expired" ApiKeyExpiring: "EventTypeName" = "api_key.expiring" + ApiKeyExposureCreated: "EventTypeName" = "api_key_exposure.created" ApiKeyRevoked: "EventTypeName" = "api_key.revoked" ApiKeyUpdated: "EventTypeName" = "api_key.updated" BusinessCreated: "EventTypeName" = "business.created" diff --git a/paddle_billing/Notifications/Entities/ApiKeyExposure.py b/paddle_billing/Notifications/Entities/ApiKeyExposure.py new file mode 100644 index 0000000..c32ae2e --- /dev/null +++ b/paddle_billing/Notifications/Entities/ApiKeyExposure.py @@ -0,0 +1,36 @@ +from __future__ import annotations +from dataclasses import dataclass +from datetime import datetime +from typing import Any + +from paddle_billing.Notifications.Entities.Entity import Entity +from paddle_billing.Notifications.Entities.ApiKeyExposures import ( + ApiKeyExposureActionTaken, + ApiKeyExposureRiskLevel, + ApiKeyExposureSource, +) + + +@dataclass +class ApiKeyExposure(Entity): + id: str + api_key_id: str + risk_level: ApiKeyExposureRiskLevel + action_taken: ApiKeyExposureActionTaken + source: ApiKeyExposureSource + reference: str + description: str | None + created_at: datetime + + @staticmethod + def from_dict(data: dict[str, Any]) -> ApiKeyExposure: + return ApiKeyExposure( + id=data["id"], + api_key_id=data["api_key_id"], + risk_level=ApiKeyExposureRiskLevel(data["risk_level"]), + action_taken=ApiKeyExposureActionTaken(data["action_taken"]), + source=ApiKeyExposureSource(data["source"]), + reference=data["reference"], + description=data.get("description"), + created_at=datetime.fromisoformat(data["created_at"]), + ) diff --git a/paddle_billing/Notifications/Entities/ApiKeyExposures/ApiKeyExposureActionTaken.py b/paddle_billing/Notifications/Entities/ApiKeyExposures/ApiKeyExposureActionTaken.py new file mode 100644 index 0000000..69a2641 --- /dev/null +++ b/paddle_billing/Notifications/Entities/ApiKeyExposures/ApiKeyExposureActionTaken.py @@ -0,0 +1,6 @@ +from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta + + +class ApiKeyExposureActionTaken(PaddleStrEnum, metaclass=PaddleStrEnumMeta): + Revoked: "ApiKeyExposureActionTaken" = "revoked" + None_: "ApiKeyExposureActionTaken" = "none" diff --git a/paddle_billing/Notifications/Entities/ApiKeyExposures/ApiKeyExposureRiskLevel.py b/paddle_billing/Notifications/Entities/ApiKeyExposures/ApiKeyExposureRiskLevel.py new file mode 100644 index 0000000..327f649 --- /dev/null +++ b/paddle_billing/Notifications/Entities/ApiKeyExposures/ApiKeyExposureRiskLevel.py @@ -0,0 +1,6 @@ +from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta + + +class ApiKeyExposureRiskLevel(PaddleStrEnum, metaclass=PaddleStrEnumMeta): + High: "ApiKeyExposureRiskLevel" = "high" + Low: "ApiKeyExposureRiskLevel" = "low" diff --git a/paddle_billing/Notifications/Entities/ApiKeyExposures/ApiKeyExposureSource.py b/paddle_billing/Notifications/Entities/ApiKeyExposures/ApiKeyExposureSource.py new file mode 100644 index 0000000..94e9f8d --- /dev/null +++ b/paddle_billing/Notifications/Entities/ApiKeyExposures/ApiKeyExposureSource.py @@ -0,0 +1,5 @@ +from paddle_billing.PaddleStrEnum import PaddleStrEnum, PaddleStrEnumMeta + + +class ApiKeyExposureSource(PaddleStrEnum, metaclass=PaddleStrEnumMeta): + Github: "ApiKeyExposureSource" = "github" diff --git a/paddle_billing/Notifications/Entities/ApiKeyExposures/__init__.py b/paddle_billing/Notifications/Entities/ApiKeyExposures/__init__.py new file mode 100644 index 0000000..7e6fe1d --- /dev/null +++ b/paddle_billing/Notifications/Entities/ApiKeyExposures/__init__.py @@ -0,0 +1,3 @@ +from paddle_billing.Notifications.Entities.ApiKeyExposures.ApiKeyExposureActionTaken import ApiKeyExposureActionTaken +from paddle_billing.Notifications.Entities.ApiKeyExposures.ApiKeyExposureRiskLevel import ApiKeyExposureRiskLevel +from paddle_billing.Notifications.Entities.ApiKeyExposures.ApiKeyExposureSource import ApiKeyExposureSource diff --git a/paddle_billing/Notifications/Entities/Simulations/ApiKeyExposure.py b/paddle_billing/Notifications/Entities/Simulations/ApiKeyExposure.py new file mode 100644 index 0000000..d11140b --- /dev/null +++ b/paddle_billing/Notifications/Entities/Simulations/ApiKeyExposure.py @@ -0,0 +1,41 @@ +from __future__ import annotations +from dataclasses import dataclass +from datetime import datetime +from typing import Any + +from paddle_billing.Notifications.Entities.ApiKeyExposures import ( + ApiKeyExposureActionTaken, + ApiKeyExposureRiskLevel, + ApiKeyExposureSource, +) +from paddle_billing.Notifications.Entities.Simulations.SimulationEntity import SimulationEntity +from paddle_billing.Undefined import Undefined + + +@dataclass +class ApiKeyExposure(SimulationEntity): + id: str | Undefined = Undefined() + api_key_id: str | Undefined = Undefined() + risk_level: ApiKeyExposureRiskLevel | Undefined = Undefined() + action_taken: ApiKeyExposureActionTaken | Undefined = Undefined() + source: ApiKeyExposureSource | Undefined = Undefined() + reference: str | Undefined = Undefined() + description: str | None | Undefined = Undefined() + created_at: datetime | Undefined = Undefined() + + @staticmethod + def from_dict(data: dict[str, Any]) -> ApiKeyExposure: + return ApiKeyExposure( + id=data.get("id", Undefined()), + api_key_id=data.get("api_key_id", Undefined()), + risk_level=ApiKeyExposureRiskLevel(data["risk_level"]) if data.get("risk_level") else Undefined(), + action_taken=ApiKeyExposureActionTaken(data["action_taken"]) if data.get("action_taken") else Undefined(), + source=ApiKeyExposureSource(data["source"]) if data.get("source") else Undefined(), + reference=data.get("reference", Undefined()), + description=data.get("description", Undefined()), + created_at=( + datetime.fromisoformat(data["created_at"]) + if data.get("created_at") + else data.get("created_at", Undefined()) + ), + ) diff --git a/paddle_billing/Notifications/Entities/Simulations/__init__.py b/paddle_billing/Notifications/Entities/Simulations/__init__.py index 0f15e6d..defd2fb 100644 --- a/paddle_billing/Notifications/Entities/Simulations/__init__.py +++ b/paddle_billing/Notifications/Entities/Simulations/__init__.py @@ -1,5 +1,6 @@ from paddle_billing.Notifications.Entities.Simulations.Address import Address from paddle_billing.Notifications.Entities.Simulations.Adjustment import Adjustment +from paddle_billing.Notifications.Entities.Simulations.ApiKeyExposure import ApiKeyExposure from paddle_billing.Notifications.Entities.Simulations.Business import Business from paddle_billing.Notifications.Entities.Simulations.ClientToken import ClientToken from paddle_billing.Notifications.Entities.Simulations.Customer import Customer diff --git a/paddle_billing/Notifications/Events/ApiKeyExposureCreated.py b/paddle_billing/Notifications/Events/ApiKeyExposureCreated.py new file mode 100644 index 0000000..36181cf --- /dev/null +++ b/paddle_billing/Notifications/Events/ApiKeyExposureCreated.py @@ -0,0 +1,17 @@ +from datetime import datetime + +from paddle_billing.Entities.Event import Event +from paddle_billing.Entities.Events import EventTypeName + +from paddle_billing.Notifications.Entities.ApiKeyExposure import ApiKeyExposure + + +class ApiKeyExposureCreated(Event): + def __init__( + self, + event_id: str, + event_type: EventTypeName, + occurred_at: datetime, + data: ApiKeyExposure, + ): + super().__init__(event_id, event_type, occurred_at, data) diff --git a/tests/Functional/Resources/Simulations/_fixtures/payload/api_key_exposure.created.json b/tests/Functional/Resources/Simulations/_fixtures/payload/api_key_exposure.created.json new file mode 100644 index 0000000..13afe4f --- /dev/null +++ b/tests/Functional/Resources/Simulations/_fixtures/payload/api_key_exposure.created.json @@ -0,0 +1,10 @@ +{ + "id": "apkexp_01jkas9tppn3bhadwpcyag45zd", + "api_key_id": "apikey_01jkdpbhazdpn3wpcya45as9tg", + "risk_level": "high", + "action_taken": "revoked", + "source": "github", + "reference": "https://github.com/octocat/Hello-World/blob/12345600b9cbe38a219f39a9941c9319b600c002/foo/bar.txt", + "description": "Exposed in Commit 12345600b9cbe38a219f39a9941c9319b600c002", + "created_at": "2025-08-19T12:58:38.746382Z" +} diff --git a/tests/Functional/Resources/Simulations/test_SimulationsClient.py b/tests/Functional/Resources/Simulations/test_SimulationsClient.py index c7b1f30..9c77364 100644 --- a/tests/Functional/Resources/Simulations/test_SimulationsClient.py +++ b/tests/Functional/Resources/Simulations/test_SimulationsClient.py @@ -167,6 +167,7 @@ def test_create_simulation_uses_expected_payload( ("api_key.created", "ApiKey"), ("api_key.expired", "ApiKey"), ("api_key.expiring", "ApiKey"), + ("api_key_exposure.created", "ApiKeyExposure"), ("api_key.revoked", "ApiKey"), ("api_key.updated", "ApiKey"), ("business.created", "Business"), @@ -219,6 +220,7 @@ def test_create_simulation_uses_expected_payload( "api_key.created", "api_key.expired", "api_key.expiring", + "api_key_exposure.created", "api_key.revoked", "api_key.updated", "business.created", diff --git a/tests/Unit/Entities/Notifications/test_NotificationEvent.py b/tests/Unit/Entities/Notifications/test_NotificationEvent.py index 6c27bf4..f54d19a 100644 --- a/tests/Unit/Entities/Notifications/test_NotificationEvent.py +++ b/tests/Unit/Entities/Notifications/test_NotificationEvent.py @@ -24,6 +24,7 @@ class TestNotificationEvent: ("api_key.created", "ApiKey"), ("api_key.expired", "ApiKey"), ("api_key.expiring", "ApiKey"), + ("api_key_exposure.created", "ApiKeyExposure"), ("api_key.revoked", "ApiKey"), ("api_key.updated", "ApiKey"), ("business.created", "Business"), @@ -78,6 +79,7 @@ class TestNotificationEvent: "api_key.created", "api_key.expired", "api_key.expiring", + "api_key_exposure.created", "api_key.revoked", "api_key.updated", "business.created", diff --git a/tests/Unit/Entities/_fixtures/notification/entity/api_key_exposure.created.json b/tests/Unit/Entities/_fixtures/notification/entity/api_key_exposure.created.json new file mode 100644 index 0000000..13afe4f --- /dev/null +++ b/tests/Unit/Entities/_fixtures/notification/entity/api_key_exposure.created.json @@ -0,0 +1,10 @@ +{ + "id": "apkexp_01jkas9tppn3bhadwpcyag45zd", + "api_key_id": "apikey_01jkdpbhazdpn3wpcya45as9tg", + "risk_level": "high", + "action_taken": "revoked", + "source": "github", + "reference": "https://github.com/octocat/Hello-World/blob/12345600b9cbe38a219f39a9941c9319b600c002/foo/bar.txt", + "description": "Exposed in Commit 12345600b9cbe38a219f39a9941c9319b600c002", + "created_at": "2025-08-19T12:58:38.746382Z" +} diff --git a/tests/Unit/Entities/test_Event.py b/tests/Unit/Entities/test_Event.py index 8a1490c..be2071b 100644 --- a/tests/Unit/Entities/test_Event.py +++ b/tests/Unit/Entities/test_Event.py @@ -30,6 +30,7 @@ class TestEvent: ("api_key.created", "ApiKey"), ("api_key.expired", "ApiKey"), ("api_key.expiring", "ApiKey"), + ("api_key_exposure.created", "ApiKeyExposure"), ("api_key.revoked", "ApiKey"), ("api_key.updated", "ApiKey"), ("business.created", "Business"), @@ -85,6 +86,7 @@ class TestEvent: "api_key.created", "api_key.expired", "api_key.expiring", + "api_key_exposure.created", "api_key.revoked", "api_key.updated", "business.created",