allowly 0.2.0__tar.gz → 0.3.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: allowly
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Python SDK for the Allowly API
5
5
  Project-URL: Repository, https://github.com/Allowly-AI/allowly-sdk-python
6
6
  License: MIT
@@ -8,7 +8,7 @@ License-File: LICENSE
8
8
  Requires-Python: >=3.10
9
9
  Requires-Dist: httpx>=0.27.0
10
10
  Provides-Extra: dev
11
- Requires-Dist: allowly-receipt-format<4.0.0,>=3.0.0; extra == 'dev'
11
+ Requires-Dist: allowly-receipt-format<5.0.0,>=4.0.0; extra == 'dev'
12
12
  Requires-Dist: cryptography>=42; extra == 'dev'
13
13
  Requires-Dist: fastmcp<3,>=2.9; extra == 'dev'
14
14
  Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
@@ -17,7 +17,7 @@ Requires-Dist: respx>=0.21; extra == 'dev'
17
17
  Provides-Extra: fastmcp
18
18
  Requires-Dist: fastmcp<3,>=2.9; extra == 'fastmcp'
19
19
  Provides-Extra: verifier
20
- Requires-Dist: allowly-receipt-format<4.0.0,>=3.0.0; extra == 'verifier'
20
+ Requires-Dist: allowly-receipt-format<5.0.0,>=4.0.0; extra == 'verifier'
21
21
  Description-Content-Type: text/markdown
22
22
 
23
23
  # Allowly Python SDK
@@ -112,9 +112,14 @@ integration examples honest and makes SDK gaps visible early.
112
112
  ## Offline receipt verification
113
113
 
114
114
  Install `allowly[verifier]` to verify signed receipts locally. The extra uses
115
- `allowly-receipt-format>=3.0.0,<4.0.0`, which verifies receipt wire format 3 (the package major equals the wire format). `alg` and
115
+ `allowly-receipt-format>=4.0.0,<5.0.0`, which verifies receipt wire format 4 (the package major equals the wire format). `alg` and
116
116
  `key_id` are signed top-level fields, and `signature` is the base64url string.
117
117
 
118
+ Wire format 4 also supports daily `receipt.checkpoint` commitments. For an
119
+ untrusted receipt bundle, pass caller-trusted key fingerprints to
120
+ `verify_receipt`; a fingerprint copied from that same bundle is not a trust
121
+ anchor.
122
+
118
123
  Key-document fetching requires HTTPS by default. For the documented local
119
124
  Caddy endpoint only, pass `dangerously_allow_insecure_base_url=True` and its
120
125
  `edge_token` to `fetch_keys_doc`, matching the client options above.
@@ -90,9 +90,14 @@ integration examples honest and makes SDK gaps visible early.
90
90
  ## Offline receipt verification
91
91
 
92
92
  Install `allowly[verifier]` to verify signed receipts locally. The extra uses
93
- `allowly-receipt-format>=3.0.0,<4.0.0`, which verifies receipt wire format 3 (the package major equals the wire format). `alg` and
93
+ `allowly-receipt-format>=4.0.0,<5.0.0`, which verifies receipt wire format 4 (the package major equals the wire format). `alg` and
94
94
  `key_id` are signed top-level fields, and `signature` is the base64url string.
95
95
 
96
+ Wire format 4 also supports daily `receipt.checkpoint` commitments. For an
97
+ untrusted receipt bundle, pass caller-trusted key fingerprints to
98
+ `verify_receipt`; a fingerprint copied from that same bundle is not a trust
99
+ anchor.
100
+
96
101
  Key-document fetching requires HTTPS by default. For the documented local
97
102
  Caddy endpoint only, pass `dangerously_allow_insecure_base_url=True` and its
98
103
  `edge_token` to `fetch_keys_doc`, matching the client options above.
@@ -21,7 +21,7 @@ from typing import Any
21
21
  from urllib.parse import quote, urlparse
22
22
 
23
23
  # Offline verification is powered by the published reference verifier,
24
- # allowly-receipt-format 3.x (import path allowly_receipt_format). It ships as an
24
+ # allowly-receipt-format 4.x (import path allowly_receipt_format). It ships as an
25
25
  # optional extra so the core SDK stays dependency-light:
26
26
  # pip install 'allowly[verifier]'
27
27
  def _import_verifier():
@@ -35,7 +35,7 @@ def _import_verifier():
35
35
  return verify_receipt, load_keys_from_json, VerificationError, PublicKey
36
36
  except ImportError as exc:
37
37
  raise ImportError(
38
- "Receipt verification requires allowly-receipt-format>=3.0.0. "
38
+ "Receipt verification requires allowly-receipt-format>=4.0.0. "
39
39
  "Install the verifier extra: pip install 'allowly[verifier]'"
40
40
  ) from exc
41
41
 
@@ -52,6 +52,7 @@ def verify_receipt(
52
52
  public_keys: list[PublicKey],
53
53
  *,
54
54
  expected_workspace_id: str,
55
+ trusted_key_fingerprints: set[str] | frozenset[str] | None = None,
55
56
  now: datetime | None = None,
56
57
  ) -> None:
57
58
  _verify_receipt(
@@ -59,6 +60,7 @@ def verify_receipt(
59
60
  public_keys,
60
61
  now=now,
61
62
  expected_workspace_id=expected_workspace_id,
63
+ trusted_key_fingerprints=trusted_key_fingerprints,
62
64
  )
63
65
 
64
66
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "allowly"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Python SDK for the Allowly API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -21,10 +21,10 @@ fastmcp = [
21
21
  "fastmcp>=2.9,<3",
22
22
  ]
23
23
  verifier = [
24
- "allowly-receipt-format>=3.0.0,<4.0.0",
24
+ "allowly-receipt-format>=4.0.0,<5.0.0",
25
25
  ]
26
26
  dev = [
27
- "allowly-receipt-format>=3.0.0,<4.0.0",
27
+ "allowly-receipt-format>=4.0.0,<5.0.0",
28
28
  "cryptography>=42",
29
29
  "fastmcp>=2.9,<3",
30
30
  "pytest>=8.0",
@@ -59,7 +59,7 @@ def _authorization_payload(**overrides) -> dict:
59
59
 
60
60
 
61
61
  SIGNED_RECEIPT = {
62
- "schema_version": "3",
62
+ "schema_version": "4",
63
63
  "receipt_id": "rcp_abc",
64
64
  "decision": "allow",
65
65
  "alg": "Ed25519",
@@ -1434,7 +1434,7 @@ async def test_receipts_get_pending(client):
1434
1434
  @pytest.mark.asyncio
1435
1435
  async def test_receipts_get_signed(client):
1436
1436
  signed = {
1437
- "schema_version": "3", "receipt_id": "rcp_abc", "decision": "allow",
1437
+ "schema_version": "4", "receipt_id": "rcp_abc", "decision": "allow",
1438
1438
  "alg": "Ed25519", "key_id": "k", "signature": "sig",
1439
1439
  }
1440
1440
  respx.get(f"{BASE}/v1/receipts/rcp_abc").mock(return_value=httpx.Response(200, json={
@@ -1677,7 +1677,7 @@ async def test_fetch_signed_default_timeout_covers_one_signer_tick(client, monke
1677
1677
 
1678
1678
  monkeypatch.setattr("allowly.client.asyncio.sleep", fake_sleep)
1679
1679
 
1680
- signed = {"schema_version": "3", "receipt_id": "rcp_abc"}
1680
+ signed = {"schema_version": "4", "receipt_id": "rcp_abc"}
1681
1681
 
1682
1682
  async def fake_get(receipt_id):
1683
1683
  if fake_now >= 61.0:
@@ -1,9 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import base64
3
4
  import hashlib
4
5
 
5
6
  import httpx
6
7
  import pytest
8
+ from allowly_receipt_format import canonicalize
9
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
7
10
 
8
11
  from allowly.verify import (
9
12
  VerificationError,
@@ -27,21 +30,30 @@ VALID_DOC = {
27
30
  ],
28
31
  }
29
32
 
30
- V2_DOC = {
33
+ def _b64u(value: bytes) -> str:
34
+ return base64.urlsafe_b64encode(value).rstrip(b"=").decode()
35
+
36
+
37
+ _TEST_PRIVATE_KEY = Ed25519PrivateKey.from_private_bytes(bytes(32))
38
+ _TEST_PUBLIC_KEY = _TEST_PRIVATE_KEY.public_key().public_bytes_raw()
39
+
40
+
41
+ V4_DOC = {
31
42
  "workspace_id": "ws_test",
32
43
  "keys": [
33
44
  {
34
45
  "key_id": "test-key/v1",
35
46
  "alg": "Ed25519",
36
- "public_key": "O2onvM62pC1io6jQKm8Nc2UyFXcd4kOmOsBIoYtZ2ik",
47
+ "public_key": _b64u(_TEST_PUBLIC_KEY),
48
+ "public_key_fingerprint": f"sha256:{hashlib.sha256(_TEST_PUBLIC_KEY).hexdigest()}",
37
49
  "active_from": "2026-01-01T00:00:00.000Z",
38
50
  "active_until": None,
39
51
  }
40
52
  ],
41
53
  }
42
54
 
43
- V2_RECEIPT = {
44
- "schema_version": "3",
55
+ V4_RECEIPT = {
56
+ "schema_version": "4",
45
57
  "receipt_id": "rcp_01HXZMINIMAL0000000000000",
46
58
  "workspace_id": "ws_test",
47
59
  "issued_at": "2026-04-21T14:32:17.482Z",
@@ -56,8 +68,8 @@ V2_RECEIPT = {
56
68
  "engine_version": "2026-04-17.1",
57
69
  "alg": "Ed25519",
58
70
  "key_id": "test-key/v1",
59
- "signature": "O98ntdo49t38E6a2M-19qjaC-2TzTw8tYqOn-fsUAYzfyf0dWTx9uje9NQlkvCl-fP68o_ATkBSW3mpyguoODQ",
60
71
  }
72
+ V4_RECEIPT["signature"] = _b64u(_TEST_PRIVATE_KEY.sign(canonicalize(V4_RECEIPT)))
61
73
 
62
74
 
63
75
  @pytest.fixture(autouse=True)
@@ -206,14 +218,26 @@ def test_verify_receipt_requires_and_forwards_workspace_binding(monkeypatch):
206
218
  seen.update(kwargs)
207
219
 
208
220
  monkeypatch.setattr("allowly.verify._verify_receipt", verifier)
209
- verify_receipt({}, [], expected_workspace_id="ws_1")
221
+ trusted = {"sha256:" + "0" * 64}
222
+ verify_receipt(
223
+ {},
224
+ [],
225
+ expected_workspace_id="ws_1",
226
+ trusted_key_fingerprints=trusted,
227
+ )
210
228
  assert seen["expected_workspace_id"] == "ws_1"
229
+ assert seen["trusted_key_fingerprints"] == trusted
211
230
 
212
231
 
213
- def test_verify_v2_receipt_with_signed_algorithm_and_key_id():
214
- keys = load_keys_from_json(V2_DOC)
232
+ def test_verify_v4_receipt_with_signed_algorithm_and_key_id():
233
+ keys = load_keys_from_json(V4_DOC)
215
234
 
216
- verify_receipt(V2_RECEIPT, keys, expected_workspace_id="ws_test")
235
+ verify_receipt(
236
+ V4_RECEIPT,
237
+ keys,
238
+ expected_workspace_id="ws_test",
239
+ trusted_key_fingerprints={V4_DOC["keys"][0]["public_key_fingerprint"]},
240
+ )
217
241
 
218
242
 
219
243
  def test_fetch_keys_doc_encodes_workspace_id():
@@ -4,7 +4,7 @@ requires-python = ">=3.10"
4
4
 
5
5
  [[package]]
6
6
  name = "allowly"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  source = { editable = "." }
9
9
  dependencies = [
10
10
  { name = "httpx" },
@@ -28,8 +28,8 @@ verifier = [
28
28
 
29
29
  [package.metadata]
30
30
  requires-dist = [
31
- { name = "allowly-receipt-format", marker = "extra == 'dev'", specifier = ">=3.0.0,<4.0.0" },
32
- { name = "allowly-receipt-format", marker = "extra == 'verifier'", specifier = ">=3.0.0,<4.0.0" },
31
+ { name = "allowly-receipt-format", marker = "extra == 'dev'", specifier = ">=4.0.0,<5.0.0" },
32
+ { name = "allowly-receipt-format", marker = "extra == 'verifier'", specifier = ">=4.0.0,<5.0.0" },
33
33
  { name = "cryptography", marker = "extra == 'dev'", specifier = ">=42" },
34
34
  { name = "fastmcp", marker = "extra == 'dev'", specifier = ">=2.9,<3" },
35
35
  { name = "fastmcp", marker = "extra == 'fastmcp'", specifier = ">=2.9,<3" },
@@ -42,14 +42,14 @@ provides-extras = ["fastmcp", "verifier", "dev"]
42
42
 
43
43
  [[package]]
44
44
  name = "allowly-receipt-format"
45
- version = "3.0.0"
45
+ version = "4.0.0"
46
46
  source = { registry = "https://pypi.org/simple" }
47
47
  dependencies = [
48
48
  { name = "cryptography" },
49
49
  ]
50
- sdist = { url = "https://files.pythonhosted.org/packages/0c/5a/fb1023f3c4419ea07d130db2f13122305ba9027e8d65d17da9db0654ea03/allowly_receipt_format-3.0.0.tar.gz", hash = "sha256:f34519f78d00e094e5c1763ff745db50e7701a3c5eb878745a7a63ea8a579bad", size = 13249, upload-time = "2026-07-23T02:57:31.751Z" }
50
+ sdist = { url = "https://files.pythonhosted.org/packages/86/4d/cd30ade1b597ee0ee1bdbfea33fd6f77c294999f4cc42504152690df13e2/allowly_receipt_format-4.0.0.tar.gz", hash = "sha256:27cd96a06edea1a23c340d9015ef1c0434b6dc1d4b2d7b56653d18b3991d6eec", size = 18059, upload-time = "2026-08-02T07:14:52.971Z" }
51
51
  wheels = [
52
- { url = "https://files.pythonhosted.org/packages/f5/e5/3637697a384c6adbe38729b3b31ae8629635938a8e361c0ce7d87fb9c16f/allowly_receipt_format-3.0.0-py3-none-any.whl", hash = "sha256:98456d2abb2993069f52d9b1b06e7e9f64ebab53e6332cd7cf429e5eb73e6b28", size = 12676, upload-time = "2026-07-23T02:57:30.495Z" },
52
+ { url = "https://files.pythonhosted.org/packages/60/d9/45545ccdda5810e31d907bb5d26d5c4953864425a79b94708b61fccd23fe/allowly_receipt_format-4.0.0-py3-none-any.whl", hash = "sha256:18a038b9ffe9be25c63bd2f39b1f26fffbebb89e43622de9d2b479b06ea14b93", size = 16630, upload-time = "2026-08-02T07:14:51.615Z" },
53
53
  ]
54
54
 
55
55
  [[package]]
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