zerodev-kora 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.
- zerodev_kora-1.0.0/PKG-INFO +46 -0
- zerodev_kora-1.0.0/README.md +35 -0
- zerodev_kora-1.0.0/pyproject.toml +19 -0
- zerodev_kora-1.0.0/setup.cfg +4 -0
- zerodev_kora-1.0.0/src/kora/__init__.py +8 -0
- zerodev_kora-1.0.0/src/kora/client.py +98 -0
- zerodev_kora-1.0.0/src/kora/heartbeat.py +29 -0
- zerodev_kora-1.0.0/src/kora/machine.py +16 -0
- zerodev_kora-1.0.0/src/kora/offline.py +27 -0
- zerodev_kora-1.0.0/src/kora/signature.py +13 -0
- zerodev_kora-1.0.0/src/kora/types.py +17 -0
- zerodev_kora-1.0.0/src/zerodev_kora.egg-info/PKG-INFO +46 -0
- zerodev_kora-1.0.0/src/zerodev_kora.egg-info/SOURCES.txt +14 -0
- zerodev_kora-1.0.0/src/zerodev_kora.egg-info/dependency_links.txt +1 -0
- zerodev_kora-1.0.0/src/zerodev_kora.egg-info/requires.txt +5 -0
- zerodev_kora-1.0.0/src/zerodev_kora.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zerodev-kora
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python SDK for Kora licensing
|
|
5
|
+
Requires-Python: >=3.8
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: cryptography>=41.0.0
|
|
8
|
+
Requires-Dist: requests>=2.31.0
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
11
|
+
|
|
12
|
+
# zerodev-kora
|
|
13
|
+
|
|
14
|
+
Official Python SDK for Kora licensing.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install zerodev-kora
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from kora import KoraClient
|
|
26
|
+
|
|
27
|
+
client = KoraClient(
|
|
28
|
+
server_url="https://api.yourdomain.com",
|
|
29
|
+
product="MySoftware",
|
|
30
|
+
public_key="-----BEGIN PUBLIC KEY-----\n..."
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
result = client.validate("XXXXX-XXXXX-XXXXX-XXXXX")
|
|
34
|
+
if result.valid:
|
|
35
|
+
print(f"License valid for: {result.user}")
|
|
36
|
+
else:
|
|
37
|
+
print(f"Validation failed: {result.message}")
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Offline Verification
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
offline = client.verify_offline(signed_offline_token)
|
|
44
|
+
if offline.valid:
|
|
45
|
+
print("Offline license is valid!")
|
|
46
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# zerodev-kora
|
|
2
|
+
|
|
3
|
+
Official Python SDK for Kora licensing.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install zerodev-kora
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from kora import KoraClient
|
|
15
|
+
|
|
16
|
+
client = KoraClient(
|
|
17
|
+
server_url="https://api.yourdomain.com",
|
|
18
|
+
product="MySoftware",
|
|
19
|
+
public_key="-----BEGIN PUBLIC KEY-----\n..."
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
result = client.validate("XXXXX-XXXXX-XXXXX-XXXXX")
|
|
23
|
+
if result.valid:
|
|
24
|
+
print(f"License valid for: {result.user}")
|
|
25
|
+
else:
|
|
26
|
+
print(f"Validation failed: {result.message}")
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Offline Verification
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
offline = client.verify_offline(signed_offline_token)
|
|
33
|
+
if offline.valid:
|
|
34
|
+
print("Offline license is valid!")
|
|
35
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "zerodev-kora"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Official Python SDK for Kora licensing"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"cryptography>=41.0.0",
|
|
13
|
+
"requests>=2.31.0"
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
dev = [
|
|
18
|
+
"pytest>=7.0.0"
|
|
19
|
+
]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from .client import Client
|
|
2
|
+
from .signature import Signature
|
|
3
|
+
from .machine import Machine
|
|
4
|
+
from .heartbeat import Heartbeat
|
|
5
|
+
from .offline import Offline
|
|
6
|
+
from .types import ValidationResponse
|
|
7
|
+
|
|
8
|
+
__all__ = ["Client", "Signature", "Machine", "Heartbeat", "Offline", "ValidationResponse"]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Optional, Dict, Any
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
from .signature import Signature
|
|
6
|
+
from .machine import Machine
|
|
7
|
+
from .heartbeat import Heartbeat
|
|
8
|
+
from .offline import Offline
|
|
9
|
+
from .types import ValidationResponse
|
|
10
|
+
|
|
11
|
+
class Client:
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
url: str,
|
|
15
|
+
product: str,
|
|
16
|
+
key: Optional[str] = None,
|
|
17
|
+
public_key: Optional[str] = None,
|
|
18
|
+
signature_header: str = "x-kora-signature",
|
|
19
|
+
heartbeat_interval_sec: float = 0.0,
|
|
20
|
+
machine_id: Optional[str] = None
|
|
21
|
+
):
|
|
22
|
+
self.url = url.rstrip("/")
|
|
23
|
+
self.product = product
|
|
24
|
+
self.key = key
|
|
25
|
+
self.public_key = public_key
|
|
26
|
+
self.signature_header = signature_header
|
|
27
|
+
self.heartbeat_interval_sec = heartbeat_interval_sec
|
|
28
|
+
self.machine_id = machine_id
|
|
29
|
+
self.signature_handler = Signature()
|
|
30
|
+
self.machine_handler = Machine()
|
|
31
|
+
self.offline_handler = Offline()
|
|
32
|
+
self.heartbeat_runner = None
|
|
33
|
+
self.session_token = None
|
|
34
|
+
|
|
35
|
+
def token(self) -> Optional[str]:
|
|
36
|
+
return self.session_token
|
|
37
|
+
|
|
38
|
+
def hwid(self) -> str:
|
|
39
|
+
if self.machine_id:
|
|
40
|
+
return self.machine_id
|
|
41
|
+
return self.machine_handler.hwid()
|
|
42
|
+
|
|
43
|
+
def offline(self) -> Offline:
|
|
44
|
+
return self.offline_handler
|
|
45
|
+
|
|
46
|
+
def validate(self, custom_key: Optional[str] = None, extra: Optional[Dict[str, Any]] = None) -> ValidationResponse:
|
|
47
|
+
target_key = custom_key if custom_key else self.key
|
|
48
|
+
if not target_key:
|
|
49
|
+
raise ValueError("License key is required")
|
|
50
|
+
|
|
51
|
+
payload = {
|
|
52
|
+
"license_key": target_key,
|
|
53
|
+
"product_name": self.product,
|
|
54
|
+
"hwid": self.hwid(),
|
|
55
|
+
"identifier": self.hwid()
|
|
56
|
+
}
|
|
57
|
+
if self.session_token:
|
|
58
|
+
payload["session_id"] = self.session_token
|
|
59
|
+
if extra:
|
|
60
|
+
payload.update(extra)
|
|
61
|
+
|
|
62
|
+
target_url = f"{self.url}/licenses/validate"
|
|
63
|
+
response = requests.post(target_url, json=payload, headers={"Accept": "application/json"}, timeout=15)
|
|
64
|
+
text = response.text
|
|
65
|
+
|
|
66
|
+
if self.public_key:
|
|
67
|
+
sig = response.headers.get(self.signature_header)
|
|
68
|
+
if not sig or not self.signature_handler.verify(text, sig, self.public_key):
|
|
69
|
+
raise ValueError("Response signature verification failed")
|
|
70
|
+
|
|
71
|
+
data = response.json()
|
|
72
|
+
session_info = data.get("session")
|
|
73
|
+
session_token = session_info.get("token") if isinstance(session_info, dict) else None
|
|
74
|
+
if session_token:
|
|
75
|
+
self.session_token = session_token
|
|
76
|
+
if self.heartbeat_interval_sec > 0 and self.heartbeat_runner is None:
|
|
77
|
+
self.heartbeat_runner = Heartbeat(self.heartbeat_interval_sec, lambda: self.validate(target_key))
|
|
78
|
+
self.heartbeat_runner.start()
|
|
79
|
+
|
|
80
|
+
return ValidationResponse(
|
|
81
|
+
valid=data.get("valid", False),
|
|
82
|
+
message=data.get("message", ""),
|
|
83
|
+
product=data.get("product"),
|
|
84
|
+
user=data.get("user"),
|
|
85
|
+
status=data.get("status"),
|
|
86
|
+
expires_at=data.get("expires_at"),
|
|
87
|
+
addons=data.get("addons"),
|
|
88
|
+
nonce=data.get("nonce"),
|
|
89
|
+
timestamp=data.get("timestamp"),
|
|
90
|
+
variables=data.get("variables"),
|
|
91
|
+
user_variables=data.get("user_variables"),
|
|
92
|
+
session_token=session_token
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def stop(self) -> None:
|
|
96
|
+
if self.heartbeat_runner is not None:
|
|
97
|
+
self.heartbeat_runner.stop()
|
|
98
|
+
self.heartbeat_runner = None
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
from typing import Callable
|
|
3
|
+
|
|
4
|
+
class Heartbeat:
|
|
5
|
+
def __init__(self, interval_sec: float, ping: Callable[[], None]):
|
|
6
|
+
self.interval_sec = interval_sec
|
|
7
|
+
self.ping = ping
|
|
8
|
+
self.stop_event = threading.Event()
|
|
9
|
+
self.thread = None
|
|
10
|
+
|
|
11
|
+
def start(self) -> None:
|
|
12
|
+
if self.thread is not None and self.thread.is_alive():
|
|
13
|
+
return
|
|
14
|
+
self.stop_event.clear()
|
|
15
|
+
self.thread = threading.Thread(target=self._run, daemon=True)
|
|
16
|
+
self.thread.start()
|
|
17
|
+
|
|
18
|
+
def _run(self) -> None:
|
|
19
|
+
while not self.stop_event.wait(self.interval_sec):
|
|
20
|
+
try:
|
|
21
|
+
self.ping()
|
|
22
|
+
except Exception:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
def stop(self) -> None:
|
|
26
|
+
if self.thread is None:
|
|
27
|
+
return
|
|
28
|
+
self.stop_event.set()
|
|
29
|
+
self.thread = None
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
import platform
|
|
3
|
+
import hashlib
|
|
4
|
+
|
|
5
|
+
class Machine:
|
|
6
|
+
def mac(self) -> str:
|
|
7
|
+
mac_int = uuid.getnode()
|
|
8
|
+
return ":".join(f"{(mac_int >> ele) & 0xff:02x}" for ele in range(40, -1, -8))
|
|
9
|
+
|
|
10
|
+
def hwid(self) -> str:
|
|
11
|
+
h = hashlib.sha256()
|
|
12
|
+
h.update(platform.system().encode("utf-8"))
|
|
13
|
+
h.update(platform.machine().encode("utf-8"))
|
|
14
|
+
h.update(platform.node().encode("utf-8"))
|
|
15
|
+
h.update(self.mac().encode("utf-8"))
|
|
16
|
+
return h.hexdigest()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import time
|
|
3
|
+
from typing import Optional, Dict, Any
|
|
4
|
+
from .signature import Signature
|
|
5
|
+
|
|
6
|
+
class Offline:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.signature = Signature()
|
|
9
|
+
|
|
10
|
+
def verify(self, content: str, public_key_pem: str) -> Optional[Dict[str, Any]]:
|
|
11
|
+
try:
|
|
12
|
+
data = json.loads(content)
|
|
13
|
+
if "signature" not in data or "expires_at" not in data:
|
|
14
|
+
return None
|
|
15
|
+
sig = data["signature"]
|
|
16
|
+
copy_data = dict(data)
|
|
17
|
+
del copy_data["signature"]
|
|
18
|
+
serialized = json.dumps(copy_data, separators=(",", ":"), sort_keys=True)
|
|
19
|
+
if not self.signature.verify(serialized, sig, public_key_pem):
|
|
20
|
+
return None
|
|
21
|
+
now_ms = int(time.time() * 1000)
|
|
22
|
+
allowed_until = data["expires_at"] + data.get("grace_period_ms", 0)
|
|
23
|
+
if now_ms > allowed_until:
|
|
24
|
+
return None
|
|
25
|
+
return data
|
|
26
|
+
except Exception:
|
|
27
|
+
return None
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
from cryptography.hazmat.primitives.serialization import load_pem_public_key
|
|
3
|
+
from cryptography.exceptions import InvalidSignature
|
|
4
|
+
|
|
5
|
+
class Signature:
|
|
6
|
+
def verify(self, body: str, signature_base64: str, public_key_pem: str) -> bool:
|
|
7
|
+
try:
|
|
8
|
+
key = load_pem_public_key(public_key_pem.encode("utf-8"))
|
|
9
|
+
signature = base64.b64decode(signature_base64)
|
|
10
|
+
key.verify(signature, body.encode("utf-8"))
|
|
11
|
+
return True
|
|
12
|
+
except (InvalidSignature, Exception):
|
|
13
|
+
return False
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional, Dict, Any, List
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class ValidationResponse:
|
|
6
|
+
valid: bool
|
|
7
|
+
message: str
|
|
8
|
+
product: Optional[str] = None
|
|
9
|
+
user: Optional[str] = None
|
|
10
|
+
status: Optional[str] = None
|
|
11
|
+
expires_at: Optional[str] = None
|
|
12
|
+
addons: Optional[List[str]] = None
|
|
13
|
+
nonce: Optional[str] = None
|
|
14
|
+
timestamp: Optional[int] = None
|
|
15
|
+
variables: Optional[Dict[str, Any]] = None
|
|
16
|
+
user_variables: Optional[Dict[str, Any]] = None
|
|
17
|
+
session_token: Optional[str] = None
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zerodev-kora
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python SDK for Kora licensing
|
|
5
|
+
Requires-Python: >=3.8
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: cryptography>=41.0.0
|
|
8
|
+
Requires-Dist: requests>=2.31.0
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
11
|
+
|
|
12
|
+
# zerodev-kora
|
|
13
|
+
|
|
14
|
+
Official Python SDK for Kora licensing.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install zerodev-kora
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from kora import KoraClient
|
|
26
|
+
|
|
27
|
+
client = KoraClient(
|
|
28
|
+
server_url="https://api.yourdomain.com",
|
|
29
|
+
product="MySoftware",
|
|
30
|
+
public_key="-----BEGIN PUBLIC KEY-----\n..."
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
result = client.validate("XXXXX-XXXXX-XXXXX-XXXXX")
|
|
34
|
+
if result.valid:
|
|
35
|
+
print(f"License valid for: {result.user}")
|
|
36
|
+
else:
|
|
37
|
+
print(f"Validation failed: {result.message}")
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Offline Verification
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
offline = client.verify_offline(signed_offline_token)
|
|
44
|
+
if offline.valid:
|
|
45
|
+
print("Offline license is valid!")
|
|
46
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/kora/__init__.py
|
|
4
|
+
src/kora/client.py
|
|
5
|
+
src/kora/heartbeat.py
|
|
6
|
+
src/kora/machine.py
|
|
7
|
+
src/kora/offline.py
|
|
8
|
+
src/kora/signature.py
|
|
9
|
+
src/kora/types.py
|
|
10
|
+
src/zerodev_kora.egg-info/PKG-INFO
|
|
11
|
+
src/zerodev_kora.egg-info/SOURCES.txt
|
|
12
|
+
src/zerodev_kora.egg-info/dependency_links.txt
|
|
13
|
+
src/zerodev_kora.egg-info/requires.txt
|
|
14
|
+
src/zerodev_kora.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
kora
|