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.
- {branta-3.2.1 → branta-3.2.2}/CHANGELOG.md +5 -0
- {branta-3.2.1 → branta-3.2.2}/CLAUDE.md +1 -1
- {branta-3.2.1 → branta-3.2.2}/PKG-INFO +1 -1
- {branta-3.2.1 → branta-3.2.2}/branta/v2/client.py +18 -4
- {branta-3.2.1 → branta-3.2.2}/branta/v2/serialization.py +11 -0
- {branta-3.2.1 → branta-3.2.2}/pyproject.toml +1 -1
- branta-3.2.2/tests/test_client.py +125 -0
- {branta-3.2.1 → branta-3.2.2}/.github/workflows/tests.yml +0 -0
- {branta-3.2.1 → branta-3.2.2}/.gitignore +0 -0
- {branta-3.2.1 → branta-3.2.2}/LICENSE +0 -0
- {branta-3.2.1 → branta-3.2.2}/README.md +0 -0
- {branta-3.2.1 → branta-3.2.2}/SECURITY.md +0 -0
- {branta-3.2.1 → branta-3.2.2}/branta/__init__.py +0 -0
- {branta-3.2.1 → branta-3.2.2}/branta/enums.py +0 -0
- {branta-3.2.1 → branta-3.2.2}/branta/exceptions.py +0 -0
- {branta-3.2.1 → branta-3.2.2}/branta/extensions.py +0 -0
- {branta-3.2.1 → branta-3.2.2}/branta/models.py +0 -0
- {branta-3.2.1 → branta-3.2.2}/branta/options.py +0 -0
- {branta-3.2.1 → branta-3.2.2}/branta/v2/__init__.py +0 -0
- {branta-3.2.1 → branta-3.2.2}/branta/v2/builder.py +0 -0
- {branta-3.2.1 → branta-3.2.2}/branta/v2/encryption.py +0 -0
- {branta-3.2.1 → branta-3.2.2}/branta/v2/parser.py +0 -0
- {branta-3.2.1 → branta-3.2.2}/branta/v2/secret_generator.py +0 -0
- {branta-3.2.1 → branta-3.2.2}/branta/v2/service.py +0 -0
- {branta-3.2.1 → branta-3.2.2}/example.py +0 -0
- {branta-3.2.1 → branta-3.2.2}/tests/__init__.py +0 -0
- {branta-3.2.1 → branta-3.2.2}/tests/test_builder.py +0 -0
- {branta-3.2.1 → branta-3.2.2}/tests/test_encryption.py +0 -0
- {branta-3.2.1 → branta-3.2.2}/tests/test_extensions.py +0 -0
- {branta-3.2.1 → branta-3.2.2}/tests/test_integration.py +0 -0
- {branta-3.2.1 → branta-3.2.2}/tests/test_qr_parser.py +0 -0
- {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`
|
|
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.
|
|
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
|
-
|
|
155
|
-
|
|
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("
|
|
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("
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|