zerodev-kora 1.0.0__py3-none-any.whl

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.
kora/__init__.py ADDED
@@ -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"]
kora/client.py ADDED
@@ -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
kora/heartbeat.py ADDED
@@ -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
kora/machine.py ADDED
@@ -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()
kora/offline.py ADDED
@@ -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
kora/signature.py ADDED
@@ -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
kora/types.py ADDED
@@ -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,11 @@
1
+ kora/__init__.py,sha256=QHaQJasFkOIUyBJXGNCf-2wHZz0T4e1BcgcE_6BRKTQ,281
2
+ kora/client.py,sha256=RQiOoMzIxcTGnVNoh4ccfBdA0aczttY_L9ZFpb_7LvI,3521
3
+ kora/heartbeat.py,sha256=xnEk0QVRwqIqmX5EdmEvShO7N_APoHDkdNPnn217tEY,834
4
+ kora/machine.py,sha256=oenKnRfUmBCt7Ud8m1VOmuI0MhD4YP5tp37YG34l7g0,491
5
+ kora/offline.py,sha256=9ex-G5i6YwFL_3m8xkkjjDtpdKS4O6JO69JhSn59s1w,972
6
+ kora/signature.py,sha256=ztUf_7ZKrUMkCjgAMJ5HjmyuQ8vkrpo-pIsTu6fnXfc,540
7
+ kora/types.py,sha256=-QhVXlCif3pxgeM-MoTiSTBYo4gSmJml1_83J5Tx6go,531
8
+ zerodev_kora-1.0.0.dist-info/METADATA,sha256=tw06xzr2ulP1y79iQyJQrPKapZxqxGAZb6Lk0HvuAzI,933
9
+ zerodev_kora-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
10
+ zerodev_kora-1.0.0.dist-info/top_level.txt,sha256=6ce2HiMBAqXR9oVFQg2DdRVGDHWahWin31Nrxwj7yjE,5
11
+ zerodev_kora-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ kora