starfish-protocol 1.0.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.
- starfish_protocol-1.0.0/PKG-INFO +9 -0
- starfish_protocol-1.0.0/pyproject.toml +17 -0
- starfish_protocol-1.0.0/setup.cfg +4 -0
- starfish_protocol-1.0.0/starfish_protocol/__init__.py +16 -0
- starfish_protocol-1.0.0/starfish_protocol/crypto.py +19 -0
- starfish_protocol-1.0.0/starfish_protocol/hash.py +34 -0
- starfish_protocol-1.0.0/starfish_protocol/merge.py +23 -0
- starfish_protocol-1.0.0/starfish_protocol/types.py +22 -0
- starfish_protocol-1.0.0/starfish_protocol.egg-info/PKG-INFO +9 -0
- starfish_protocol-1.0.0/starfish_protocol.egg-info/SOURCES.txt +12 -0
- starfish_protocol-1.0.0/starfish_protocol.egg-info/dependency_links.txt +1 -0
- starfish_protocol-1.0.0/starfish_protocol.egg-info/requires.txt +5 -0
- starfish_protocol-1.0.0/starfish_protocol.egg-info/top_level.txt +1 -0
- starfish_protocol-1.0.0/tests/test_hash.py +23 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: starfish-protocol
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Shared protocol primitives for the Starfish sync protocol
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: cryptography>=41.0
|
|
7
|
+
Provides-Extra: dev
|
|
8
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
9
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "starfish-protocol"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Shared protocol primitives for the Starfish sync protocol"
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
dependencies = ["cryptography>=41.0"]
|
|
11
|
+
|
|
12
|
+
[project.optional-dependencies]
|
|
13
|
+
dev = ["pytest>=7.0", "pytest-asyncio>=0.21"]
|
|
14
|
+
|
|
15
|
+
[tool.pytest.ini_options]
|
|
16
|
+
asyncio_mode = "auto"
|
|
17
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from starfish_protocol.hash import stable_stringify, compute_hash
|
|
2
|
+
from starfish_protocol.merge import deep_merge
|
|
3
|
+
from starfish_protocol.crypto import _derive_key, IV_BYTES, ENCRYPTED_KEY
|
|
4
|
+
from starfish_protocol.types import Timestamps, PullResult, PushSuccess
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"stable_stringify",
|
|
8
|
+
"compute_hash",
|
|
9
|
+
"deep_merge",
|
|
10
|
+
"_derive_key",
|
|
11
|
+
"IV_BYTES",
|
|
12
|
+
"ENCRYPTED_KEY",
|
|
13
|
+
"Timestamps",
|
|
14
|
+
"PullResult",
|
|
15
|
+
"PushSuccess",
|
|
16
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Shared cryptographic primitives for the Starfish sync protocol."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from cryptography.hazmat.primitives import hashes
|
|
5
|
+
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
|
6
|
+
|
|
7
|
+
IV_BYTES = 12
|
|
8
|
+
ENCRYPTED_KEY = "_encrypted"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _derive_key(secret: str, salt: str, info: bytes) -> bytes:
|
|
12
|
+
"""Derive a 256-bit AES key from a secret and salt using HKDF(SHA-256)."""
|
|
13
|
+
hkdf = HKDF(
|
|
14
|
+
algorithm=hashes.SHA256(),
|
|
15
|
+
length=32,
|
|
16
|
+
salt=salt.encode("utf-8"),
|
|
17
|
+
info=info,
|
|
18
|
+
)
|
|
19
|
+
return hkdf.derive(secret.encode("utf-8"))
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Deterministic hashing — must produce identical output to the TS implementation."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def stable_stringify(value: Any) -> str:
|
|
10
|
+
"""Deterministic JSON serialization with sorted keys (recursive).
|
|
11
|
+
|
|
12
|
+
Must produce identical output to the server's stableStringify.
|
|
13
|
+
"""
|
|
14
|
+
if value is None:
|
|
15
|
+
return "null"
|
|
16
|
+
if isinstance(value, bool):
|
|
17
|
+
return "true" if value else "false"
|
|
18
|
+
if isinstance(value, (int, float)):
|
|
19
|
+
return json.dumps(value, ensure_ascii=False)
|
|
20
|
+
if isinstance(value, str):
|
|
21
|
+
return json.dumps(value, ensure_ascii=False)
|
|
22
|
+
if isinstance(value, list):
|
|
23
|
+
return "[" + ",".join(stable_stringify(v) for v in value) + "]"
|
|
24
|
+
if isinstance(value, dict):
|
|
25
|
+
keys = sorted(value.keys())
|
|
26
|
+
pairs = [json.dumps(k, ensure_ascii=False) + ":" + stable_stringify(value[k]) for k in keys]
|
|
27
|
+
return "{" + ",".join(pairs) + "}"
|
|
28
|
+
return "null"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def compute_hash(data: dict[str, Any]) -> str:
|
|
32
|
+
"""Compute SHA-256 hex digest of the stable-stringified data."""
|
|
33
|
+
encoded = stable_stringify(data).encode("utf-8")
|
|
34
|
+
return hashlib.sha256(encoded).hexdigest()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Document merge utilities for conflict resolution."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def deep_merge(local: dict[str, Any], remote: dict[str, Any]) -> dict[str, Any]:
|
|
8
|
+
"""Remote-wins deep merge.
|
|
9
|
+
|
|
10
|
+
Recursively merges *remote* into *local*: nested dicts present on both sides
|
|
11
|
+
are merged recursively; for all other values the remote value wins.
|
|
12
|
+
|
|
13
|
+
This is the canonical conflict resolution strategy used across the Starfish
|
|
14
|
+
protocol — both the client SDK and the server-side replica manager rely on it.
|
|
15
|
+
"""
|
|
16
|
+
merged = {**local}
|
|
17
|
+
for key, remote_val in remote.items():
|
|
18
|
+
local_val = merged.get(key)
|
|
19
|
+
if isinstance(remote_val, dict) and isinstance(local_val, dict):
|
|
20
|
+
merged[key] = deep_merge(local_val, remote_val)
|
|
21
|
+
else:
|
|
22
|
+
merged[key] = remote_val
|
|
23
|
+
return merged
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Shared wire-format types for the Starfish sync protocol."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Union
|
|
6
|
+
|
|
7
|
+
Timestamps = dict[str, Union[int, "Timestamps"]]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class PullResult:
|
|
12
|
+
data: dict[str, Any]
|
|
13
|
+
hash: str
|
|
14
|
+
timestamp: int
|
|
15
|
+
author_pubkey: str | None = None
|
|
16
|
+
author_signature: str | None = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class PushSuccess:
|
|
21
|
+
hash: str
|
|
22
|
+
timestamp: int
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: starfish-protocol
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Shared protocol primitives for the Starfish sync protocol
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: cryptography>=41.0
|
|
7
|
+
Provides-Extra: dev
|
|
8
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
9
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
starfish_protocol/__init__.py
|
|
3
|
+
starfish_protocol/crypto.py
|
|
4
|
+
starfish_protocol/hash.py
|
|
5
|
+
starfish_protocol/merge.py
|
|
6
|
+
starfish_protocol/types.py
|
|
7
|
+
starfish_protocol.egg-info/PKG-INFO
|
|
8
|
+
starfish_protocol.egg-info/SOURCES.txt
|
|
9
|
+
starfish_protocol.egg-info/dependency_links.txt
|
|
10
|
+
starfish_protocol.egg-info/requires.txt
|
|
11
|
+
starfish_protocol.egg-info/top_level.txt
|
|
12
|
+
tests/test_hash.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
starfish_protocol
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Tests for hashing using shared test vectors."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import pathlib
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from starfish_protocol.hash import stable_stringify, compute_hash
|
|
9
|
+
|
|
10
|
+
VECTORS_PATH = pathlib.Path(__file__).parent.parent.parent.parent.parent / "tests" / "test-vectors" / "hash.json"
|
|
11
|
+
VECTORS = json.loads(VECTORS_PATH.read_text())
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.mark.parametrize("case", VECTORS["stableStringify"])
|
|
15
|
+
def test_stable_stringify(case):
|
|
16
|
+
result = stable_stringify(case["input"])
|
|
17
|
+
assert result == case["expected"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.mark.parametrize("case", VECTORS["computeHash"])
|
|
21
|
+
def test_compute_hash(case):
|
|
22
|
+
assert stable_stringify(case["input"]) == case["stableJson"]
|
|
23
|
+
assert compute_hash(case["input"]) == case["expectedHash"]
|