branta 3.2.1__tar.gz → 3.2.2__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.
Files changed (32) hide show
  1. {branta-3.2.1 → branta-3.2.2}/CHANGELOG.md +5 -0
  2. {branta-3.2.1 → branta-3.2.2}/CLAUDE.md +1 -1
  3. {branta-3.2.1 → branta-3.2.2}/PKG-INFO +1 -1
  4. {branta-3.2.1 → branta-3.2.2}/branta/v2/client.py +18 -4
  5. {branta-3.2.1 → branta-3.2.2}/branta/v2/serialization.py +11 -0
  6. {branta-3.2.1 → branta-3.2.2}/pyproject.toml +1 -1
  7. branta-3.2.2/tests/test_client.py +125 -0
  8. {branta-3.2.1 → branta-3.2.2}/.github/workflows/tests.yml +0 -0
  9. {branta-3.2.1 → branta-3.2.2}/.gitignore +0 -0
  10. {branta-3.2.1 → branta-3.2.2}/LICENSE +0 -0
  11. {branta-3.2.1 → branta-3.2.2}/README.md +0 -0
  12. {branta-3.2.1 → branta-3.2.2}/SECURITY.md +0 -0
  13. {branta-3.2.1 → branta-3.2.2}/branta/__init__.py +0 -0
  14. {branta-3.2.1 → branta-3.2.2}/branta/enums.py +0 -0
  15. {branta-3.2.1 → branta-3.2.2}/branta/exceptions.py +0 -0
  16. {branta-3.2.1 → branta-3.2.2}/branta/extensions.py +0 -0
  17. {branta-3.2.1 → branta-3.2.2}/branta/models.py +0 -0
  18. {branta-3.2.1 → branta-3.2.2}/branta/options.py +0 -0
  19. {branta-3.2.1 → branta-3.2.2}/branta/v2/__init__.py +0 -0
  20. {branta-3.2.1 → branta-3.2.2}/branta/v2/builder.py +0 -0
  21. {branta-3.2.1 → branta-3.2.2}/branta/v2/encryption.py +0 -0
  22. {branta-3.2.1 → branta-3.2.2}/branta/v2/parser.py +0 -0
  23. {branta-3.2.1 → branta-3.2.2}/branta/v2/secret_generator.py +0 -0
  24. {branta-3.2.1 → branta-3.2.2}/branta/v2/service.py +0 -0
  25. {branta-3.2.1 → branta-3.2.2}/example.py +0 -0
  26. {branta-3.2.1 → branta-3.2.2}/tests/__init__.py +0 -0
  27. {branta-3.2.1 → branta-3.2.2}/tests/test_builder.py +0 -0
  28. {branta-3.2.1 → branta-3.2.2}/tests/test_encryption.py +0 -0
  29. {branta-3.2.1 → branta-3.2.2}/tests/test_extensions.py +0 -0
  30. {branta-3.2.1 → branta-3.2.2}/tests/test_integration.py +0 -0
  31. {branta-3.2.1 → branta-3.2.2}/tests/test_qr_parser.py +0 -0
  32. {branta-3.2.1 → branta-3.2.2}/tests/test_service.py +0 -0
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [3.2.2] - 2026-09-30
8
+
9
+ ### Fixed
10
+ - Fixed a same-origin validation gap: if the first payment in a `get_payments` response had no `platform_logo_url`, validation silently stopped checking every later payment's logo URLs entirely. Logo-URL checks now run independently per payment, and now also cover `platform_logo_light_url`, `parent_platform.logo_url`/`logo_light_url`, and `child_platform.logo_url`/`logo_light_url` (previously only `platform_logo_url` was checked)
11
+
7
12
  ## [3.2.1] - 2026-08-29
8
13
 
9
14
  ### Fixed
@@ -39,7 +39,7 @@ pip install -e ".[dev]"
39
39
  - **ZK encryption:** Bitcoin addresses use a random secret (GUID); hash-ZK types (bolt11, ark, silent_payment) use a deterministic key from SHA256(normalized value). `add_payment` mutates `payment.destinations[*].value` to the encrypted form before POSTing.
40
40
  - **Metadata DEK-envelope:** If a payment has metadata and any ZK destination, a separate DEK is generated, metadata is encrypted with it, and the DEK is encrypted per-destination.
41
41
  - **Never surface lookup failures.** Swallow decryption errors; leave `is_encrypted=True` and value unchanged.
42
- - **Domain validation.** `platform_logo_url` must match `base_url` domain.
42
+ - **Domain validation.** `platform_logo_url`, `platform_logo_light_url`, `parent_platform.logo_url`/`logo_light_url`, and `child_platform.logo_url`/`logo_light_url` must each match `base_url`'s origin. Every payment in a GET response is checked, not just the first.
43
43
 
44
44
  ## Conventions
45
45
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: branta
3
- Version: 3.2.1
3
+ Version: 3.2.2
4
4
  Summary: Python SDK for the Branta V2 API — payment destination lookup and registration with zero-knowledge encryption support
5
5
  Project-URL: Homepage, https://branta.pro
6
6
  Project-URL: Repository, https://github.com/BrantaOps/branta-python
@@ -151,13 +151,27 @@ class BrantaClient:
151
151
  base_origin = f"{urlparse(base_url).scheme}://{urlparse(base_url).netloc}"
152
152
  except Exception:
153
153
  return
154
- for payment in payments:
155
- logo_url = payment.platform_logo_url
154
+
155
+ def check(logo_url: Optional[str], field_name: str) -> None:
156
156
  if not logo_url:
157
157
  return
158
158
  try:
159
159
  logo_origin = f"{urlparse(logo_url).scheme}://{urlparse(logo_url).netloc}"
160
160
  except Exception:
161
- raise BrantaPaymentException("platformLogoUrl domain does not match the configured baseUrl domain")
161
+ raise BrantaPaymentException(f"{field_name} domain does not match the configured base_url domain")
162
162
  if logo_origin != base_origin:
163
- raise BrantaPaymentException("platformLogoUrl domain does not match the configured baseUrl domain")
163
+ raise BrantaPaymentException(f"{field_name} domain does not match the configured base_url domain")
164
+
165
+ for payment in payments:
166
+ check(payment.platform_logo_url, "platform_logo_url")
167
+ check(payment.platform_logo_light_url, "platform_logo_light_url")
168
+ check(payment.parent_platform.logo_url if payment.parent_platform else None, "parent_platform.logo_url")
169
+ check(
170
+ payment.parent_platform.logo_light_url if payment.parent_platform else None,
171
+ "parent_platform.logo_light_url",
172
+ )
173
+ check(payment.child_platform.logo_url if payment.child_platform else None, "child_platform.logo_url")
174
+ check(
175
+ payment.child_platform.logo_light_url if payment.child_platform else None,
176
+ "child_platform.logo_light_url",
177
+ )
@@ -104,6 +104,17 @@ def payment_from_api(raw: Dict[str, Any]) -> Payment:
104
104
  if pp.get("logo_light_url") is not None:
105
105
  parent.logo_light_url = str(pp["logo_light_url"])
106
106
  payment.parent_platform = parent
107
+ if raw.get("child_platform") is not None:
108
+ cp = raw["child_platform"]
109
+ if isinstance(cp, dict):
110
+ child = Platform()
111
+ if cp.get("name") is not None:
112
+ child.name = str(cp["name"])
113
+ if cp.get("logo_url") is not None:
114
+ child.logo_url = str(cp["logo_url"])
115
+ if cp.get("logo_light_url") is not None:
116
+ child.logo_light_url = str(cp["logo_light_url"])
117
+ payment.child_platform = child
107
118
  if raw.get("btc_pay_server_plugin_version") is not None:
108
119
  payment.btc_pay_server_plugin_version = str(raw["btc_pay_server_plugin_version"])
109
120
  return payment
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "branta"
7
- version = "3.2.1"
7
+ version = "3.2.2"
8
8
  description = "Python SDK for the Branta V2 API — payment destination lookup and registration with zero-knowledge encryption support"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -0,0 +1,125 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+
5
+ import pytest
6
+
7
+ from branta.enums import BrantaServerBaseUrl, PrivacyMode
8
+ from branta.exceptions import BrantaPaymentException
9
+ from branta.options import BrantaClientOptions
10
+ from branta.v2.client import BrantaClient
11
+
12
+ # Matches BrantaServerBaseUrl.Localhost's value.
13
+ SAME_ORIGIN = "http://localhost:3000"
14
+ OTHER_ORIGIN = "https://attacker.example"
15
+
16
+ DESTINATIONS = [{"value": "test-destination"}]
17
+
18
+
19
+ class _FakeResponse:
20
+ def __init__(self, status: int, body: str) -> None:
21
+ self.status = status
22
+ self._body = body
23
+
24
+ @property
25
+ def ok(self) -> bool:
26
+ return 200 <= self.status < 300
27
+
28
+ async def text(self) -> str:
29
+ return self._body
30
+
31
+ async def __aenter__(self) -> "_FakeResponse":
32
+ return self
33
+
34
+ async def __aexit__(self, *exc: object) -> bool:
35
+ return False
36
+
37
+
38
+ class _FakeSession:
39
+ def __init__(self, response: _FakeResponse) -> None:
40
+ self._response = response
41
+
42
+ def get(self, url: str, headers: dict | None = None) -> _FakeResponse:
43
+ return self._response
44
+
45
+
46
+ def client_with_response(body: object) -> BrantaClient:
47
+ session = _FakeSession(_FakeResponse(200, json.dumps(body)))
48
+ options = BrantaClientOptions(base_url=BrantaServerBaseUrl.Localhost, privacy=PrivacyMode.Loose)
49
+ return BrantaClient(default_options=options, session=session) # type: ignore[arg-type]
50
+
51
+
52
+ async def test_checks_every_payments_logo_not_just_the_first() -> None:
53
+ client = client_with_response(
54
+ [
55
+ {"destinations": DESTINATIONS},
56
+ {"destinations": DESTINATIONS, "platform_logo_url": f"{OTHER_ORIGIN}/logo.png"},
57
+ ]
58
+ )
59
+
60
+ with pytest.raises(BrantaPaymentException):
61
+ await client.get_payments("value")
62
+
63
+
64
+ async def test_catches_mismatched_platform_logo_light_url() -> None:
65
+ client = client_with_response(
66
+ [{"destinations": DESTINATIONS, "platform_logo_light_url": f"{OTHER_ORIGIN}/logo-light.png"}]
67
+ )
68
+
69
+ with pytest.raises(BrantaPaymentException, match="platform_logo_light_url"):
70
+ await client.get_payments("value")
71
+
72
+
73
+ async def test_catches_mismatched_parent_platform_logo_url() -> None:
74
+ client = client_with_response(
75
+ [{"destinations": DESTINATIONS, "parent_platform": {"logo_url": f"{OTHER_ORIGIN}/logo.png"}}]
76
+ )
77
+
78
+ with pytest.raises(BrantaPaymentException, match="parent_platform.logo_url"):
79
+ await client.get_payments("value")
80
+
81
+
82
+ async def test_catches_mismatched_parent_platform_logo_light_url() -> None:
83
+ client = client_with_response(
84
+ [{"destinations": DESTINATIONS, "parent_platform": {"logo_light_url": f"{OTHER_ORIGIN}/logo-light.png"}}]
85
+ )
86
+
87
+ with pytest.raises(BrantaPaymentException, match="parent_platform.logo_light_url"):
88
+ await client.get_payments("value")
89
+
90
+
91
+ async def test_catches_mismatched_child_platform_logo_url() -> None:
92
+ client = client_with_response(
93
+ [{"destinations": DESTINATIONS, "child_platform": {"logo_url": f"{OTHER_ORIGIN}/logo.png"}}]
94
+ )
95
+
96
+ with pytest.raises(BrantaPaymentException, match="child_platform.logo_url"):
97
+ await client.get_payments("value")
98
+
99
+
100
+ async def test_catches_mismatched_child_platform_logo_light_url() -> None:
101
+ client = client_with_response(
102
+ [{"destinations": DESTINATIONS, "child_platform": {"logo_light_url": f"{OTHER_ORIGIN}/logo-light.png"}}]
103
+ )
104
+
105
+ with pytest.raises(BrantaPaymentException, match="child_platform.logo_light_url"):
106
+ await client.get_payments("value")
107
+
108
+
109
+ async def test_does_not_throw_when_all_logo_fields_are_same_origin_or_absent() -> None:
110
+ client = client_with_response(
111
+ [
112
+ {
113
+ "destinations": DESTINATIONS,
114
+ "platform_logo_url": f"{SAME_ORIGIN}/a.png",
115
+ "platform_logo_light_url": f"{SAME_ORIGIN}/b.png",
116
+ "parent_platform": {"logo_url": f"{SAME_ORIGIN}/c.png", "logo_light_url": f"{SAME_ORIGIN}/d.png"},
117
+ "child_platform": {"logo_url": f"{SAME_ORIGIN}/e.png"},
118
+ },
119
+ {"destinations": DESTINATIONS},
120
+ ]
121
+ )
122
+
123
+ payments = await client.get_payments("value")
124
+
125
+ assert len(payments) == 2
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes