authlyx-api 0.1.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.
- authlyx_api-0.1.0/LICENSE +22 -0
- authlyx_api-0.1.0/PKG-INFO +88 -0
- authlyx_api-0.1.0/README.md +71 -0
- authlyx_api-0.1.0/pyproject.toml +31 -0
- authlyx_api-0.1.0/setup.cfg +4 -0
- authlyx_api-0.1.0/src/authlyx_api/__init__.py +5 -0
- authlyx_api-0.1.0/src/authlyx_api/sdk.py +573 -0
- authlyx_api-0.1.0/src/authlyx_api.egg-info/PKG-INFO +88 -0
- authlyx_api-0.1.0/src/authlyx_api.egg-info/SOURCES.txt +10 -0
- authlyx_api-0.1.0/src/authlyx_api.egg-info/dependency_links.txt +1 -0
- authlyx_api-0.1.0/src/authlyx_api.egg-info/requires.txt +1 -0
- authlyx_api-0.1.0/src/authlyx_api.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AuthlyX
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: authlyx-api
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AuthlyX Python SDK (v2) for AuthlyX API integration.
|
|
5
|
+
Author: AuthlyX
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://authly.cc
|
|
8
|
+
Keywords: authlyx,authentication,license,sdk
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: requests>=2.31.0
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# AuthlyX Python SDK (PyPI)
|
|
19
|
+
|
|
20
|
+
This folder is a publish-ready PyPI package version of the AuthlyX Python SDK.
|
|
21
|
+
|
|
22
|
+
Install:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install authlyx-api
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Quick start:
|
|
29
|
+
|
|
30
|
+
```py
|
|
31
|
+
from authlyx_api import AuthlyX
|
|
32
|
+
|
|
33
|
+
AuthlyXApp = AuthlyX(
|
|
34
|
+
ownerId="12345678",
|
|
35
|
+
appName="HI",
|
|
36
|
+
version="1.3",
|
|
37
|
+
secret="your-secret"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
AuthlyXApp.Init()
|
|
41
|
+
if not AuthlyXApp.response["success"]:
|
|
42
|
+
raise Exception(AuthlyXApp.response["message"])
|
|
43
|
+
|
|
44
|
+
AuthlyXApp.Login("12", "1")
|
|
45
|
+
print(AuthlyXApp.userData["SubscriptionLevel"])
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Optional parameters:
|
|
49
|
+
|
|
50
|
+
```py
|
|
51
|
+
from authlyx_api import AuthlyX
|
|
52
|
+
|
|
53
|
+
AuthlyXApp = AuthlyX(
|
|
54
|
+
ownerId="12345678",
|
|
55
|
+
appName="HI",
|
|
56
|
+
version="1.3",
|
|
57
|
+
secret="your-secret",
|
|
58
|
+
debug=False,
|
|
59
|
+
api="https://example.com/api/v2"
|
|
60
|
+
)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Unified login:
|
|
64
|
+
|
|
65
|
+
```py
|
|
66
|
+
# Username + password
|
|
67
|
+
AuthlyXApp.Login("12", "1")
|
|
68
|
+
|
|
69
|
+
# License key
|
|
70
|
+
AuthlyXApp.Login("XXXXX-XXXXX-XXXXX-XXXXX-XXXXX")
|
|
71
|
+
|
|
72
|
+
# Device login
|
|
73
|
+
AuthlyXApp.Login("YOUR_DEVICE_ID", deviceType="motherboard")
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Build locally:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
python -m pip install --upgrade build twine
|
|
80
|
+
python -m build
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Publish:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
twine upload dist/*
|
|
87
|
+
```
|
|
88
|
+
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# AuthlyX Python SDK (PyPI)
|
|
2
|
+
|
|
3
|
+
This folder is a publish-ready PyPI package version of the AuthlyX Python SDK.
|
|
4
|
+
|
|
5
|
+
Install:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install authlyx-api
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Quick start:
|
|
12
|
+
|
|
13
|
+
```py
|
|
14
|
+
from authlyx_api import AuthlyX
|
|
15
|
+
|
|
16
|
+
AuthlyXApp = AuthlyX(
|
|
17
|
+
ownerId="12345678",
|
|
18
|
+
appName="HI",
|
|
19
|
+
version="1.3",
|
|
20
|
+
secret="your-secret"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
AuthlyXApp.Init()
|
|
24
|
+
if not AuthlyXApp.response["success"]:
|
|
25
|
+
raise Exception(AuthlyXApp.response["message"])
|
|
26
|
+
|
|
27
|
+
AuthlyXApp.Login("12", "1")
|
|
28
|
+
print(AuthlyXApp.userData["SubscriptionLevel"])
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Optional parameters:
|
|
32
|
+
|
|
33
|
+
```py
|
|
34
|
+
from authlyx_api import AuthlyX
|
|
35
|
+
|
|
36
|
+
AuthlyXApp = AuthlyX(
|
|
37
|
+
ownerId="12345678",
|
|
38
|
+
appName="HI",
|
|
39
|
+
version="1.3",
|
|
40
|
+
secret="your-secret",
|
|
41
|
+
debug=False,
|
|
42
|
+
api="https://example.com/api/v2"
|
|
43
|
+
)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Unified login:
|
|
47
|
+
|
|
48
|
+
```py
|
|
49
|
+
# Username + password
|
|
50
|
+
AuthlyXApp.Login("12", "1")
|
|
51
|
+
|
|
52
|
+
# License key
|
|
53
|
+
AuthlyXApp.Login("XXXXX-XXXXX-XXXXX-XXXXX-XXXXX")
|
|
54
|
+
|
|
55
|
+
# Device login
|
|
56
|
+
AuthlyXApp.Login("YOUR_DEVICE_ID", deviceType="motherboard")
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Build locally:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
python -m pip install --upgrade build twine
|
|
63
|
+
python -m build
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Publish:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
twine upload dist/*
|
|
70
|
+
```
|
|
71
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "authlyx-api"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "AuthlyX Python SDK (v2) for AuthlyX API integration."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "AuthlyX" }]
|
|
14
|
+
dependencies = [
|
|
15
|
+
"requests>=2.31.0"
|
|
16
|
+
]
|
|
17
|
+
keywords = ["authlyx", "authentication", "license", "sdk"]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
21
|
+
"Operating System :: OS Independent"
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://authly.cc"
|
|
26
|
+
|
|
27
|
+
[tool.setuptools]
|
|
28
|
+
package-dir = {"" = "src"}
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.packages.find]
|
|
31
|
+
where = ["src"]
|
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import json
|
|
3
|
+
import uuid
|
|
4
|
+
import hashlib
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import re
|
|
8
|
+
import time
|
|
9
|
+
import subprocess
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AuthlyXLogger:
|
|
14
|
+
Enabled = True
|
|
15
|
+
AppName = "AuthlyX"
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def _mask_sensitive(text):
|
|
19
|
+
if not text:
|
|
20
|
+
return text
|
|
21
|
+
patterns = [
|
|
22
|
+
r'("session_id"\s*:\s*")([^"]+)(")',
|
|
23
|
+
r'("owner_id"\s*:\s*")([^"]+)(")',
|
|
24
|
+
r'("secret"\s*:\s*")([^"]+)(")',
|
|
25
|
+
r'("password"\s*:\s*")([^"]+)(")',
|
|
26
|
+
r'("key"\s*:\s*")([^"]+)(")',
|
|
27
|
+
r'("license_key"\s*:\s*")([^"]+)(")',
|
|
28
|
+
r'("hash"\s*:\s*")([^"]+)(")',
|
|
29
|
+
r'("request_id"\s*:\s*")([^"]+)(")',
|
|
30
|
+
r'("nonce"\s*:\s*")([^"]+)(")',
|
|
31
|
+
r'("hwid"\s*:\s*")([^"]+)(")',
|
|
32
|
+
r'("sid"\s*:\s*")([^"]+)(")',
|
|
33
|
+
r'(\bx-auth-signature\s*:\s*)([A-Za-z0-9+/=]+)',
|
|
34
|
+
r'(\bx-v2-signature\s*:\s*)([A-Za-z0-9+/=]+)',
|
|
35
|
+
]
|
|
36
|
+
out = str(text)
|
|
37
|
+
for p in patterns:
|
|
38
|
+
out = re.sub(
|
|
39
|
+
p,
|
|
40
|
+
lambda m: (m.group(1) + "***" + (m.group(3) if m.lastindex and m.lastindex >= 3 else "")),
|
|
41
|
+
out,
|
|
42
|
+
flags=re.IGNORECASE,
|
|
43
|
+
)
|
|
44
|
+
return out
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def Log(content):
|
|
48
|
+
if not AuthlyXLogger.Enabled:
|
|
49
|
+
return
|
|
50
|
+
if content is None:
|
|
51
|
+
return
|
|
52
|
+
s = str(content)
|
|
53
|
+
if not s.strip():
|
|
54
|
+
return
|
|
55
|
+
try:
|
|
56
|
+
app = (AuthlyXLogger.AppName or "default").strip() or "default"
|
|
57
|
+
program_data = os.environ.get("PROGRAMDATA") or ""
|
|
58
|
+
root = os.path.join(program_data, "AuthlyX", app)
|
|
59
|
+
os.makedirs(root, exist_ok=True)
|
|
60
|
+
log_path = os.path.join(root, f"{datetime.now(timezone.utc):%Y_%m_%d}.log")
|
|
61
|
+
line = f"[{datetime.now(timezone.utc):%H:%M:%S}] {AuthlyXLogger._mask_sensitive(s)}\n"
|
|
62
|
+
with open(log_path, "a", encoding="utf-8") as f:
|
|
63
|
+
f.write(line)
|
|
64
|
+
except Exception:
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Auth:
|
|
69
|
+
DefaultBaseUrl = "https://authly.cc/api/v2"
|
|
70
|
+
IpLookupUrl = "https://api.ipify.org"
|
|
71
|
+
|
|
72
|
+
def __init__(self, ownerId, appName, version, secret, debug=True, api=None):
|
|
73
|
+
self.ownerId = ownerId or ""
|
|
74
|
+
self.appName = appName or ""
|
|
75
|
+
self.version = version or ""
|
|
76
|
+
self.secret = secret or ""
|
|
77
|
+
self.baseUrl = self._normalize_base_url(api or Auth.DefaultBaseUrl)
|
|
78
|
+
self.loggingEnabled = True if debug is None else bool(debug)
|
|
79
|
+
|
|
80
|
+
AuthlyXLogger.AppName = self.appName or "AuthlyX"
|
|
81
|
+
AuthlyXLogger.Enabled = self.loggingEnabled
|
|
82
|
+
|
|
83
|
+
self.sessionId = ""
|
|
84
|
+
self.applicationHash = ""
|
|
85
|
+
self.initialized = False
|
|
86
|
+
self.cachedPublicIp = ""
|
|
87
|
+
self.cachedPublicIpExpiresAt = 0.0
|
|
88
|
+
|
|
89
|
+
self.response = {
|
|
90
|
+
"success": False,
|
|
91
|
+
"message": "",
|
|
92
|
+
"raw": "",
|
|
93
|
+
"code": "",
|
|
94
|
+
"status_code": 0,
|
|
95
|
+
"request_id": "",
|
|
96
|
+
"nonce": "",
|
|
97
|
+
"signature_kid": "",
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
self.userData = {
|
|
101
|
+
"Username": "",
|
|
102
|
+
"Email": "",
|
|
103
|
+
"LicenseKey": "",
|
|
104
|
+
"Subscription": "",
|
|
105
|
+
"SubscriptionLevel": "",
|
|
106
|
+
"ExpiryDate": "",
|
|
107
|
+
"DaysLeft": 0,
|
|
108
|
+
"LastLogin": "",
|
|
109
|
+
"Hwid": "",
|
|
110
|
+
"IpAddress": "",
|
|
111
|
+
"RegisteredAt": "",
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
self.variableData = {"VarKey": "", "VarValue": "", "UpdatedAt": ""}
|
|
115
|
+
|
|
116
|
+
self.updateData = {
|
|
117
|
+
"Available": False,
|
|
118
|
+
"LatestVersion": "",
|
|
119
|
+
"DownloadUrl": "",
|
|
120
|
+
"ForceUpdate": False,
|
|
121
|
+
"Changelog": "",
|
|
122
|
+
"ShowReminder": False,
|
|
123
|
+
"ReminderMessage": "",
|
|
124
|
+
"AllowedUntil": "",
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
self.chatMessages = {
|
|
128
|
+
"ChannelName": "",
|
|
129
|
+
"Messages": [],
|
|
130
|
+
"Count": 0,
|
|
131
|
+
"NextCursor": "",
|
|
132
|
+
"HasMore": False,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
self._calculate_application_hash()
|
|
136
|
+
AuthlyXLogger.Log(f"[SDK] AuthlyX initialized for app '{self.appName}' using '{self.baseUrl}'.")
|
|
137
|
+
|
|
138
|
+
def _normalize_base_url(self, api):
|
|
139
|
+
base = (api or "").strip()
|
|
140
|
+
if not base:
|
|
141
|
+
return Auth.DefaultBaseUrl
|
|
142
|
+
return base.rstrip("/")
|
|
143
|
+
|
|
144
|
+
def _reset_response(self):
|
|
145
|
+
self.response["success"] = False
|
|
146
|
+
self.response["message"] = ""
|
|
147
|
+
self.response["raw"] = ""
|
|
148
|
+
self.response["code"] = ""
|
|
149
|
+
self.response["status_code"] = 0
|
|
150
|
+
self.response["request_id"] = ""
|
|
151
|
+
self.response["nonce"] = ""
|
|
152
|
+
self.response["signature_kid"] = ""
|
|
153
|
+
|
|
154
|
+
def _set_failure(self, code, message, raw="", status_code=0):
|
|
155
|
+
self.response["success"] = False
|
|
156
|
+
self.response["code"] = code or ""
|
|
157
|
+
self.response["message"] = message or ""
|
|
158
|
+
self.response["raw"] = raw or ""
|
|
159
|
+
self.response["status_code"] = int(status_code or 0)
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
def _has_required_credentials(self):
|
|
163
|
+
return bool(self.ownerId and self.appName and self.version and self.secret)
|
|
164
|
+
|
|
165
|
+
def _canonical_json(self, obj):
|
|
166
|
+
return json.dumps(obj, separators=(",", ":"), sort_keys=True, ensure_ascii=False)
|
|
167
|
+
|
|
168
|
+
def _create_security_context(self):
|
|
169
|
+
request_id = str(uuid.uuid4())
|
|
170
|
+
nonce = os.urandom(16).hex()
|
|
171
|
+
ts = int(time.time() * 1000)
|
|
172
|
+
return request_id, nonce, ts
|
|
173
|
+
|
|
174
|
+
def _calculate_application_hash(self):
|
|
175
|
+
try:
|
|
176
|
+
path = sys.executable
|
|
177
|
+
if not path or not os.path.exists(path):
|
|
178
|
+
path = os.path.abspath(sys.argv[0]) if sys.argv and sys.argv[0] else ""
|
|
179
|
+
if not path or not os.path.exists(path):
|
|
180
|
+
self.applicationHash = "UNKNOWN_HASH"
|
|
181
|
+
return
|
|
182
|
+
h = hashlib.sha256()
|
|
183
|
+
with open(path, "rb") as f:
|
|
184
|
+
while True:
|
|
185
|
+
b = f.read(1024 * 1024)
|
|
186
|
+
if not b:
|
|
187
|
+
break
|
|
188
|
+
h.update(b)
|
|
189
|
+
self.applicationHash = h.hexdigest()
|
|
190
|
+
except Exception:
|
|
191
|
+
self.applicationHash = "UNKNOWN_HASH"
|
|
192
|
+
|
|
193
|
+
def _get_windows_sid(self):
|
|
194
|
+
try:
|
|
195
|
+
out = subprocess.check_output(
|
|
196
|
+
["whoami", "/user", "/fo", "csv", "/nh"],
|
|
197
|
+
stderr=subprocess.DEVNULL,
|
|
198
|
+
text=True,
|
|
199
|
+
).strip()
|
|
200
|
+
if not out:
|
|
201
|
+
return ""
|
|
202
|
+
cols = [c.strip().strip('"') for c in out.split(",")]
|
|
203
|
+
for c in cols:
|
|
204
|
+
if c.startswith("S-1-"):
|
|
205
|
+
return c
|
|
206
|
+
if len(cols) >= 2 and cols[1].startswith("S-1-"):
|
|
207
|
+
return cols[1]
|
|
208
|
+
return ""
|
|
209
|
+
except Exception:
|
|
210
|
+
return ""
|
|
211
|
+
|
|
212
|
+
def _get_system_identifier(self):
|
|
213
|
+
if sys.platform == "win32":
|
|
214
|
+
sid = self._get_windows_sid()
|
|
215
|
+
if sid:
|
|
216
|
+
return sid
|
|
217
|
+
try:
|
|
218
|
+
seed = (os.environ.get("USERNAME") or "") + "|" + (os.environ.get("COMPUTERNAME") or "") + "|" + (sys.platform or "")
|
|
219
|
+
return hashlib.sha256(seed.encode("utf-8", errors="ignore")).hexdigest()
|
|
220
|
+
except Exception:
|
|
221
|
+
return "UNKNOWN_SID"
|
|
222
|
+
|
|
223
|
+
def _get_public_ip(self):
|
|
224
|
+
now = time.time()
|
|
225
|
+
if self.cachedPublicIp and now < self.cachedPublicIpExpiresAt:
|
|
226
|
+
return self.cachedPublicIp
|
|
227
|
+
try:
|
|
228
|
+
r = requests.get(Auth.IpLookupUrl, timeout=10)
|
|
229
|
+
ip = (r.text or "").strip()
|
|
230
|
+
if ip:
|
|
231
|
+
self.cachedPublicIp = ip
|
|
232
|
+
self.cachedPublicIpExpiresAt = now + 600.0
|
|
233
|
+
return ip
|
|
234
|
+
except Exception:
|
|
235
|
+
return self.cachedPublicIp or ""
|
|
236
|
+
return ""
|
|
237
|
+
|
|
238
|
+
def _build_url(self, endpoint):
|
|
239
|
+
ep = (endpoint or "").lstrip("/")
|
|
240
|
+
return f"{self.baseUrl}/{ep}"
|
|
241
|
+
|
|
242
|
+
def _validate_response_metadata(self, headers, request_id, nonce):
|
|
243
|
+
resp_request_id = headers.get("x-v2-request-id") or ""
|
|
244
|
+
resp_nonce = headers.get("x-v2-nonce") or ""
|
|
245
|
+
sig_kid = headers.get("x-v2-signature-kid") or ""
|
|
246
|
+
if resp_request_id and resp_request_id != request_id:
|
|
247
|
+
return False, "AUTH_REQUEST_MISMATCH", "Response request_id does not match the original request.", sig_kid
|
|
248
|
+
if resp_nonce and resp_nonce != nonce:
|
|
249
|
+
return False, "AUTH_REQUEST_MISMATCH", "Response nonce does not match the original request.", sig_kid
|
|
250
|
+
return True, "", "", sig_kid
|
|
251
|
+
|
|
252
|
+
def _compute_days_left(self, expiry):
|
|
253
|
+
try:
|
|
254
|
+
if not expiry:
|
|
255
|
+
return 0
|
|
256
|
+
s = str(expiry).strip()
|
|
257
|
+
if not s:
|
|
258
|
+
return 0
|
|
259
|
+
if s.endswith("Z"):
|
|
260
|
+
dt = datetime.fromisoformat(s.replace("Z", "+00:00"))
|
|
261
|
+
else:
|
|
262
|
+
dt = datetime.fromisoformat(s)
|
|
263
|
+
diff = dt.timestamp() - time.time()
|
|
264
|
+
days = int(diff // (24 * 60 * 60))
|
|
265
|
+
return days if days > 0 else 0
|
|
266
|
+
except Exception:
|
|
267
|
+
return 0
|
|
268
|
+
|
|
269
|
+
def _load_update_data(self, obj):
|
|
270
|
+
try:
|
|
271
|
+
u = obj.get("update") if isinstance(obj, dict) else None
|
|
272
|
+
if not isinstance(u, dict):
|
|
273
|
+
return
|
|
274
|
+
self.updateData["Available"] = bool(u.get("available"))
|
|
275
|
+
self.updateData["LatestVersion"] = str(u.get("latest_version") or "")
|
|
276
|
+
self.updateData["DownloadUrl"] = u.get("download_url")
|
|
277
|
+
self.updateData["ForceUpdate"] = bool(u.get("force_update"))
|
|
278
|
+
self.updateData["Changelog"] = str(u.get("changelog") or "")
|
|
279
|
+
self.updateData["ShowReminder"] = bool(u.get("show_reminder"))
|
|
280
|
+
self.updateData["ReminderMessage"] = str(u.get("reminder_message") or "")
|
|
281
|
+
self.updateData["AllowedUntil"] = u.get("allowed_until")
|
|
282
|
+
except Exception:
|
|
283
|
+
return
|
|
284
|
+
|
|
285
|
+
def _load_user_data(self, obj):
|
|
286
|
+
if not isinstance(obj, dict):
|
|
287
|
+
return
|
|
288
|
+
|
|
289
|
+
user = obj.get("user") if isinstance(obj.get("user"), dict) else None
|
|
290
|
+
lic = obj.get("license") if isinstance(obj.get("license"), dict) else None
|
|
291
|
+
dev = obj.get("device") if isinstance(obj.get("device"), dict) else None
|
|
292
|
+
|
|
293
|
+
if user:
|
|
294
|
+
self.userData["Username"] = str(user.get("username") or "")
|
|
295
|
+
self.userData["Email"] = str(user.get("email") or self.userData.get("Email") or "")
|
|
296
|
+
self.userData["Subscription"] = str(user.get("subscription") or self.userData.get("Subscription") or "")
|
|
297
|
+
if user.get("subscription_level") is not None:
|
|
298
|
+
self.userData["SubscriptionLevel"] = str(user.get("subscription_level"))
|
|
299
|
+
self.userData["ExpiryDate"] = str(user.get("expiry_date") or self.userData.get("ExpiryDate") or "")
|
|
300
|
+
self.userData["LastLogin"] = str(user.get("last_login") or self.userData.get("LastLogin") or "")
|
|
301
|
+
self.userData["RegisteredAt"] = str(user.get("created_at") or user.get("registered_at") or self.userData.get("RegisteredAt") or "")
|
|
302
|
+
if user.get("ip_address"):
|
|
303
|
+
self.userData["IpAddress"] = str(user.get("ip_address") or "")
|
|
304
|
+
if user.get("sid") or user.get("hwid"):
|
|
305
|
+
self.userData["Hwid"] = str(user.get("sid") or user.get("hwid") or "")
|
|
306
|
+
|
|
307
|
+
if lic:
|
|
308
|
+
if lic.get("license_key"):
|
|
309
|
+
self.userData["LicenseKey"] = str(lic.get("license_key") or "")
|
|
310
|
+
if not self.userData.get("Subscription"):
|
|
311
|
+
self.userData["Subscription"] = str(lic.get("subscription") or "")
|
|
312
|
+
if not self.userData.get("SubscriptionLevel") and lic.get("subscription_level") is not None:
|
|
313
|
+
self.userData["SubscriptionLevel"] = str(lic.get("subscription_level"))
|
|
314
|
+
if not self.userData.get("ExpiryDate") and lic.get("expiry_date"):
|
|
315
|
+
self.userData["ExpiryDate"] = str(lic.get("expiry_date") or "")
|
|
316
|
+
|
|
317
|
+
if dev:
|
|
318
|
+
if not self.userData.get("Subscription") and dev.get("subscription"):
|
|
319
|
+
self.userData["Subscription"] = str(dev.get("subscription") or "")
|
|
320
|
+
if not self.userData.get("SubscriptionLevel") and dev.get("subscription_level") is not None:
|
|
321
|
+
self.userData["SubscriptionLevel"] = str(dev.get("subscription_level"))
|
|
322
|
+
if not self.userData.get("ExpiryDate") and dev.get("expiry_date"):
|
|
323
|
+
self.userData["ExpiryDate"] = str(dev.get("expiry_date") or "")
|
|
324
|
+
if not self.userData.get("LastLogin") and dev.get("last_login"):
|
|
325
|
+
self.userData["LastLogin"] = str(dev.get("last_login") or "")
|
|
326
|
+
if not self.userData.get("RegisteredAt") and (dev.get("registered_at") or dev.get("created_at")):
|
|
327
|
+
self.userData["RegisteredAt"] = str(dev.get("registered_at") or dev.get("created_at") or "")
|
|
328
|
+
if not self.userData.get("IpAddress") and dev.get("ip_address"):
|
|
329
|
+
self.userData["IpAddress"] = str(dev.get("ip_address") or "")
|
|
330
|
+
if not self.userData.get("Hwid") and (dev.get("hwid") or dev.get("sid")):
|
|
331
|
+
self.userData["Hwid"] = str(dev.get("sid") or dev.get("hwid") or "")
|
|
332
|
+
|
|
333
|
+
if not self.userData.get("Hwid"):
|
|
334
|
+
self.userData["Hwid"] = self._get_system_identifier()
|
|
335
|
+
if not self.userData.get("IpAddress"):
|
|
336
|
+
self.userData["IpAddress"] = self._get_public_ip()
|
|
337
|
+
self.userData["DaysLeft"] = self._compute_days_left(self.userData.get("ExpiryDate") or "")
|
|
338
|
+
|
|
339
|
+
def _load_variable_data(self, obj):
|
|
340
|
+
if not isinstance(obj, dict):
|
|
341
|
+
return
|
|
342
|
+
v = obj.get("variable") if isinstance(obj.get("variable"), dict) else None
|
|
343
|
+
if not v:
|
|
344
|
+
return
|
|
345
|
+
self.variableData["VarKey"] = str(v.get("var_key") or "")
|
|
346
|
+
self.variableData["VarValue"] = str(v.get("var_value") or "")
|
|
347
|
+
self.variableData["UpdatedAt"] = str(v.get("updated_at") or "")
|
|
348
|
+
|
|
349
|
+
def _load_chat_messages(self, obj):
|
|
350
|
+
if not isinstance(obj, dict):
|
|
351
|
+
return
|
|
352
|
+
chats = obj.get("chats") if isinstance(obj.get("chats"), dict) else None
|
|
353
|
+
if not chats:
|
|
354
|
+
return
|
|
355
|
+
self.chatMessages["ChannelName"] = str(chats.get("channel_name") or "")
|
|
356
|
+
self.chatMessages["Messages"] = chats.get("messages") if isinstance(chats.get("messages"), list) else []
|
|
357
|
+
self.chatMessages["Count"] = int(chats.get("count") or 0)
|
|
358
|
+
self.chatMessages["NextCursor"] = str(chats.get("next_cursor") or "")
|
|
359
|
+
self.chatMessages["HasMore"] = bool(chats.get("has_more"))
|
|
360
|
+
|
|
361
|
+
def _post_json(self, endpoint, payload):
|
|
362
|
+
self._reset_response()
|
|
363
|
+
if not self._has_required_credentials():
|
|
364
|
+
return self._set_failure("AUTH_CONFIG", "Missing AuthlyX credentials.")
|
|
365
|
+
|
|
366
|
+
url = self._build_url(endpoint)
|
|
367
|
+
request_id, nonce, ts = self._create_security_context()
|
|
368
|
+
|
|
369
|
+
body = dict(payload or {})
|
|
370
|
+
body["owner_id"] = self.ownerId
|
|
371
|
+
body["app_name"] = self.appName
|
|
372
|
+
body["version"] = self.version
|
|
373
|
+
body["secret"] = self.secret
|
|
374
|
+
body["request_id"] = request_id
|
|
375
|
+
body["nonce"] = nonce
|
|
376
|
+
body["timestamp"] = ts
|
|
377
|
+
body["hash"] = self.applicationHash
|
|
378
|
+
|
|
379
|
+
try:
|
|
380
|
+
AuthlyXLogger.Log(f"[SDK][REQUEST] POST {url} {self._canonical_json(body)}")
|
|
381
|
+
r = requests.post(
|
|
382
|
+
url,
|
|
383
|
+
json=body,
|
|
384
|
+
headers={
|
|
385
|
+
"x-request-id": request_id,
|
|
386
|
+
"x-auth-nonce": nonce,
|
|
387
|
+
"x-auth-timestamp": str(ts),
|
|
388
|
+
},
|
|
389
|
+
timeout=20,
|
|
390
|
+
)
|
|
391
|
+
text = r.text or ""
|
|
392
|
+
AuthlyXLogger.Log(f"[SDK][RESPONSE] {r.status_code} {text}")
|
|
393
|
+
|
|
394
|
+
self.response["status_code"] = int(r.status_code)
|
|
395
|
+
self.response["raw"] = text
|
|
396
|
+
self.response["request_id"] = request_id
|
|
397
|
+
self.response["nonce"] = nonce
|
|
398
|
+
|
|
399
|
+
ok, code, msg, kid = self._validate_response_metadata(r.headers or {}, request_id, nonce)
|
|
400
|
+
self.response["signature_kid"] = kid or ""
|
|
401
|
+
if not ok:
|
|
402
|
+
return self._set_failure(code, msg, text, r.status_code)
|
|
403
|
+
|
|
404
|
+
obj = None
|
|
405
|
+
try:
|
|
406
|
+
obj = r.json()
|
|
407
|
+
except Exception:
|
|
408
|
+
return self._set_failure("BAD_RESPONSE", "Invalid JSON response.", text, r.status_code)
|
|
409
|
+
|
|
410
|
+
self.response["success"] = bool(obj.get("success"))
|
|
411
|
+
self.response["message"] = str(obj.get("message") or "")
|
|
412
|
+
self.response["code"] = str(obj.get("code") or "")
|
|
413
|
+
|
|
414
|
+
if obj.get("session_id"):
|
|
415
|
+
self.sessionId = str(obj.get("session_id") or "")
|
|
416
|
+
|
|
417
|
+
if endpoint == "init":
|
|
418
|
+
self._load_update_data(obj)
|
|
419
|
+
|
|
420
|
+
if obj.get("success"):
|
|
421
|
+
self._load_user_data(obj)
|
|
422
|
+
self._load_variable_data(obj)
|
|
423
|
+
self._load_chat_messages(obj)
|
|
424
|
+
|
|
425
|
+
return bool(obj.get("success"))
|
|
426
|
+
except Exception as e:
|
|
427
|
+
return self._set_failure("NETWORK_ERROR", "Network error.", str(e), 0)
|
|
428
|
+
|
|
429
|
+
def _ensure_initialized(self):
|
|
430
|
+
if self.initialized and self.sessionId:
|
|
431
|
+
return True
|
|
432
|
+
return self._set_failure("NOT_INITIALIZED", "AuthlyX is not initialized. Call Init() first.")
|
|
433
|
+
|
|
434
|
+
def Init(self):
|
|
435
|
+
payload = {}
|
|
436
|
+
ok = self._post_json("init", payload)
|
|
437
|
+
self.initialized = bool(ok and self.sessionId)
|
|
438
|
+
return bool(ok)
|
|
439
|
+
|
|
440
|
+
def Login(self, identifier, password=None, deviceType=None):
|
|
441
|
+
if deviceType:
|
|
442
|
+
return self.DeviceLogin(deviceType, identifier)
|
|
443
|
+
if password is None:
|
|
444
|
+
return self.LicenseLogin(identifier)
|
|
445
|
+
return self.UsernameLogin(identifier, password)
|
|
446
|
+
|
|
447
|
+
def UsernameLogin(self, username, password):
|
|
448
|
+
if not self._ensure_initialized():
|
|
449
|
+
return False
|
|
450
|
+
payload = {
|
|
451
|
+
"session_id": self.sessionId,
|
|
452
|
+
"username": username or "",
|
|
453
|
+
"password": password or "",
|
|
454
|
+
"sid": self._get_system_identifier(),
|
|
455
|
+
"ip_address": self._get_public_ip(),
|
|
456
|
+
}
|
|
457
|
+
return self._post_json("login", payload)
|
|
458
|
+
|
|
459
|
+
def LicenseLogin(self, licenseKey):
|
|
460
|
+
if not self._ensure_initialized():
|
|
461
|
+
return False
|
|
462
|
+
payload = {
|
|
463
|
+
"session_id": self.sessionId,
|
|
464
|
+
"license_key": licenseKey or "",
|
|
465
|
+
"sid": self._get_system_identifier(),
|
|
466
|
+
"ip_address": self._get_public_ip(),
|
|
467
|
+
}
|
|
468
|
+
return self._post_json("licenses", payload)
|
|
469
|
+
|
|
470
|
+
def DeviceLogin(self, deviceType, deviceId):
|
|
471
|
+
if not self._ensure_initialized():
|
|
472
|
+
return False
|
|
473
|
+
payload = {
|
|
474
|
+
"session_id": self.sessionId,
|
|
475
|
+
"device_type": deviceType or "",
|
|
476
|
+
"device_id": deviceId or "",
|
|
477
|
+
"sid": self._get_system_identifier(),
|
|
478
|
+
"ip_address": self._get_public_ip(),
|
|
479
|
+
}
|
|
480
|
+
return self._post_json("device-auth", payload)
|
|
481
|
+
|
|
482
|
+
def Register(self, username, password, licenseKey, email=""):
|
|
483
|
+
if not self._ensure_initialized():
|
|
484
|
+
return False
|
|
485
|
+
payload = {
|
|
486
|
+
"session_id": self.sessionId,
|
|
487
|
+
"username": username or "",
|
|
488
|
+
"password": password or "",
|
|
489
|
+
"license_key": licenseKey or "",
|
|
490
|
+
"email": email or "",
|
|
491
|
+
"sid": self._get_system_identifier(),
|
|
492
|
+
"ip_address": self._get_public_ip(),
|
|
493
|
+
}
|
|
494
|
+
return self._post_json("register", payload)
|
|
495
|
+
|
|
496
|
+
def ChangePassword(self, oldPassword, newPassword):
|
|
497
|
+
if not self._ensure_initialized():
|
|
498
|
+
return False
|
|
499
|
+
payload = {"session_id": self.sessionId, "old_password": oldPassword or "", "new_password": newPassword or ""}
|
|
500
|
+
return self._post_json("change-password", payload)
|
|
501
|
+
|
|
502
|
+
def ExtendTime(self, username, licenseKey):
|
|
503
|
+
if not self._ensure_initialized():
|
|
504
|
+
return False
|
|
505
|
+
payload = {"session_id": self.sessionId, "username": username or "", "license_key": licenseKey or ""}
|
|
506
|
+
return self._post_json("extend", payload)
|
|
507
|
+
|
|
508
|
+
def GetVariable(self, varKey):
|
|
509
|
+
if not self._ensure_initialized():
|
|
510
|
+
return ""
|
|
511
|
+
payload = {"session_id": self.sessionId, "var_key": varKey or ""}
|
|
512
|
+
ok = self._post_json("variables", payload)
|
|
513
|
+
if ok:
|
|
514
|
+
return self.variableData.get("VarValue") or ""
|
|
515
|
+
return ""
|
|
516
|
+
|
|
517
|
+
def SetVariable(self, varKey, varValue):
|
|
518
|
+
if not self._ensure_initialized():
|
|
519
|
+
return False
|
|
520
|
+
payload = {"session_id": self.sessionId, "var_key": varKey or "", "var_value": varValue or ""}
|
|
521
|
+
return self._post_json("variables/set", payload)
|
|
522
|
+
|
|
523
|
+
def Log(self, message):
|
|
524
|
+
if not self._ensure_initialized():
|
|
525
|
+
return False
|
|
526
|
+
payload = {"session_id": self.sessionId, "message": message or ""}
|
|
527
|
+
return self._post_json("logs", payload)
|
|
528
|
+
|
|
529
|
+
def GetChats(self, channelName, limit=100, cursor=None):
|
|
530
|
+
if not self._ensure_initialized():
|
|
531
|
+
return False
|
|
532
|
+
payload = {"session_id": self.sessionId, "channel_name": channelName or "", "limit": int(limit or 100)}
|
|
533
|
+
if cursor:
|
|
534
|
+
payload["cursor"] = cursor
|
|
535
|
+
return self._post_json("chats/get", payload)
|
|
536
|
+
|
|
537
|
+
def SendChat(self, message, channelName=None):
|
|
538
|
+
if not self._ensure_initialized():
|
|
539
|
+
return False
|
|
540
|
+
payload = {"session_id": self.sessionId, "message": message or ""}
|
|
541
|
+
if channelName:
|
|
542
|
+
payload["channel_name"] = channelName
|
|
543
|
+
return self._post_json("chats/send", payload)
|
|
544
|
+
|
|
545
|
+
def ValidateSession(self):
|
|
546
|
+
if not self.initialized or not self.sessionId:
|
|
547
|
+
return self._set_failure("INVALID_SESSION", "No active session. Please login first.")
|
|
548
|
+
payload = {"session_id": self.sessionId}
|
|
549
|
+
return self._post_json("validate-session", payload)
|
|
550
|
+
|
|
551
|
+
def IsInitialized(self):
|
|
552
|
+
return bool(self.initialized)
|
|
553
|
+
|
|
554
|
+
def GetSessionId(self):
|
|
555
|
+
return self.sessionId or ""
|
|
556
|
+
|
|
557
|
+
def GetCurrentApplicationHash(self):
|
|
558
|
+
return self.applicationHash or ""
|
|
559
|
+
|
|
560
|
+
init = Init
|
|
561
|
+
login = Login
|
|
562
|
+
register = Register
|
|
563
|
+
extend_time = ExtendTime
|
|
564
|
+
get_variable = GetVariable
|
|
565
|
+
set_variable = SetVariable
|
|
566
|
+
get_chats = GetChats
|
|
567
|
+
send_chat = SendChat
|
|
568
|
+
validate_session = ValidateSession
|
|
569
|
+
change_password = ChangePassword
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
AuthlyX = Auth
|
|
573
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: authlyx-api
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AuthlyX Python SDK (v2) for AuthlyX API integration.
|
|
5
|
+
Author: AuthlyX
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://authly.cc
|
|
8
|
+
Keywords: authlyx,authentication,license,sdk
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: requests>=2.31.0
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# AuthlyX Python SDK (PyPI)
|
|
19
|
+
|
|
20
|
+
This folder is a publish-ready PyPI package version of the AuthlyX Python SDK.
|
|
21
|
+
|
|
22
|
+
Install:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install authlyx-api
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Quick start:
|
|
29
|
+
|
|
30
|
+
```py
|
|
31
|
+
from authlyx_api import AuthlyX
|
|
32
|
+
|
|
33
|
+
AuthlyXApp = AuthlyX(
|
|
34
|
+
ownerId="12345678",
|
|
35
|
+
appName="HI",
|
|
36
|
+
version="1.3",
|
|
37
|
+
secret="your-secret"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
AuthlyXApp.Init()
|
|
41
|
+
if not AuthlyXApp.response["success"]:
|
|
42
|
+
raise Exception(AuthlyXApp.response["message"])
|
|
43
|
+
|
|
44
|
+
AuthlyXApp.Login("12", "1")
|
|
45
|
+
print(AuthlyXApp.userData["SubscriptionLevel"])
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Optional parameters:
|
|
49
|
+
|
|
50
|
+
```py
|
|
51
|
+
from authlyx_api import AuthlyX
|
|
52
|
+
|
|
53
|
+
AuthlyXApp = AuthlyX(
|
|
54
|
+
ownerId="12345678",
|
|
55
|
+
appName="HI",
|
|
56
|
+
version="1.3",
|
|
57
|
+
secret="your-secret",
|
|
58
|
+
debug=False,
|
|
59
|
+
api="https://example.com/api/v2"
|
|
60
|
+
)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Unified login:
|
|
64
|
+
|
|
65
|
+
```py
|
|
66
|
+
# Username + password
|
|
67
|
+
AuthlyXApp.Login("12", "1")
|
|
68
|
+
|
|
69
|
+
# License key
|
|
70
|
+
AuthlyXApp.Login("XXXXX-XXXXX-XXXXX-XXXXX-XXXXX")
|
|
71
|
+
|
|
72
|
+
# Device login
|
|
73
|
+
AuthlyXApp.Login("YOUR_DEVICE_ID", deviceType="motherboard")
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Build locally:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
python -m pip install --upgrade build twine
|
|
80
|
+
python -m build
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Publish:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
twine upload dist/*
|
|
87
|
+
```
|
|
88
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/authlyx_api/__init__.py
|
|
5
|
+
src/authlyx_api/sdk.py
|
|
6
|
+
src/authlyx_api.egg-info/PKG-INFO
|
|
7
|
+
src/authlyx_api.egg-info/SOURCES.txt
|
|
8
|
+
src/authlyx_api.egg-info/dependency_links.txt
|
|
9
|
+
src/authlyx_api.egg-info/requires.txt
|
|
10
|
+
src/authlyx_api.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests>=2.31.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
authlyx_api
|