tikwebsign 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.
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.4
2
+ Name: tikwebsign
3
+ Version: 0.1.0
4
+ Summary: Safe HMAC request signing helpers for authorized APIs
5
+ Author: tikwebsign contributors
6
+ License: MIT
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+
10
+ # tikwebsign
11
+
12
+ مكتبة صغيرة لتوقيع طلبات HTTP الخاصة بواجهات API التي تملكها أو لديك تفويض صريح لاستخدامها. تستخدم **HMAC-SHA256** مع canonical request، ولا تطبق تجاوزات anti-bot أو فحص بيانات اعتماد أو جمع cookies/tokens من خدمات طرف ثالث.
13
+
14
+ ## التثبيت
15
+
16
+ ```bash
17
+ pip install -e .
18
+ ```
19
+
20
+ ## الاستخدام
21
+
22
+ ```python
23
+ import requests
24
+ import tikwebsign
25
+
26
+ url = "https://api.example.com/v1/items"
27
+ params = {"page": 1, "limit": 20}
28
+ body = {"filter": "active"}
29
+ signer = tikwebsign.RequestSigner("ضع-المفتاح-السري-في-متغير-بيئة", key_id="client-1")
30
+
31
+ # الدوال ترجع dicts؛ لذلك الاستدعاء الصحيح يتضمن الأقواس.
32
+ signed_params = tikwebsign.getparams(
33
+ signer=signer, method="POST", url=url, params=params, body=body
34
+ )
35
+ headers = tikwebsign.getheaders(
36
+ signer=signer, method="POST", url=url, params=params, body=body,
37
+ headers={"Content-Type": "application/json"}
38
+ )
39
+ params.update(signed_params)
40
+ response = requests.post(url, params=params, json=body, headers=headers, timeout=30)
41
+ response.raise_for_status()
42
+ ```
43
+
44
+ > لا يمكن جعل `params.update(tikwebsign.getparams)` يولّد توقيعًا صحيحًا دون معرفة method وURL وbody والسر. لذلك صُممت الدوال كـ `getparams(...)` و`getheaders(...)` حتى يوقّع الخادم نفس البيانات بالترتيب canonical نفسه.
45
+
46
+ ## صيغة التوقيع للخادم
47
+
48
+ يبني العميل النص التالي مفصولًا بـ newline:
49
+
50
+ ```text
51
+ METHOD
52
+ URL
53
+ sorted-urlencoded-query
54
+ sha256(body)
55
+ timestamp
56
+ nonce
57
+ ```
58
+
59
+ ثم يحسب `HMAC-SHA256(secret, canonical_request)` ويضع القيمة Base64URL في `X-Request-Signature`. يجب على الخادم التحقق من `timestamp` ضمن نافذة قصيرة (مثل 300 ثانية)، ومنع إعادة استخدام `nonce`، واستخدام مقارنة ثابتة الزمن.
60
+
61
+ ## توليد جهاز محلي
62
+
63
+ ```python
64
+ device = tikwebsign.generate_device(user_agent="my-client/1.0")
65
+ print(device.to_dict())
66
+ ```
67
+
68
+ هذا مولد هوية محلية فقط، ولا يتصل بأي خدمة خارجية ولا ينشئ tokens أو cookies خاصة بخدمة أخرى.
@@ -0,0 +1,59 @@
1
+ # tikwebsign
2
+
3
+ مكتبة صغيرة لتوقيع طلبات HTTP الخاصة بواجهات API التي تملكها أو لديك تفويض صريح لاستخدامها. تستخدم **HMAC-SHA256** مع canonical request، ولا تطبق تجاوزات anti-bot أو فحص بيانات اعتماد أو جمع cookies/tokens من خدمات طرف ثالث.
4
+
5
+ ## التثبيت
6
+
7
+ ```bash
8
+ pip install -e .
9
+ ```
10
+
11
+ ## الاستخدام
12
+
13
+ ```python
14
+ import requests
15
+ import tikwebsign
16
+
17
+ url = "https://api.example.com/v1/items"
18
+ params = {"page": 1, "limit": 20}
19
+ body = {"filter": "active"}
20
+ signer = tikwebsign.RequestSigner("ضع-المفتاح-السري-في-متغير-بيئة", key_id="client-1")
21
+
22
+ # الدوال ترجع dicts؛ لذلك الاستدعاء الصحيح يتضمن الأقواس.
23
+ signed_params = tikwebsign.getparams(
24
+ signer=signer, method="POST", url=url, params=params, body=body
25
+ )
26
+ headers = tikwebsign.getheaders(
27
+ signer=signer, method="POST", url=url, params=params, body=body,
28
+ headers={"Content-Type": "application/json"}
29
+ )
30
+ params.update(signed_params)
31
+ response = requests.post(url, params=params, json=body, headers=headers, timeout=30)
32
+ response.raise_for_status()
33
+ ```
34
+
35
+ > لا يمكن جعل `params.update(tikwebsign.getparams)` يولّد توقيعًا صحيحًا دون معرفة method وURL وbody والسر. لذلك صُممت الدوال كـ `getparams(...)` و`getheaders(...)` حتى يوقّع الخادم نفس البيانات بالترتيب canonical نفسه.
36
+
37
+ ## صيغة التوقيع للخادم
38
+
39
+ يبني العميل النص التالي مفصولًا بـ newline:
40
+
41
+ ```text
42
+ METHOD
43
+ URL
44
+ sorted-urlencoded-query
45
+ sha256(body)
46
+ timestamp
47
+ nonce
48
+ ```
49
+
50
+ ثم يحسب `HMAC-SHA256(secret, canonical_request)` ويضع القيمة Base64URL في `X-Request-Signature`. يجب على الخادم التحقق من `timestamp` ضمن نافذة قصيرة (مثل 300 ثانية)، ومنع إعادة استخدام `nonce`، واستخدام مقارنة ثابتة الزمن.
51
+
52
+ ## توليد جهاز محلي
53
+
54
+ ```python
55
+ device = tikwebsign.generate_device(user_agent="my-client/1.0")
56
+ print(device.to_dict())
57
+ ```
58
+
59
+ هذا مولد هوية محلية فقط، ولا يتصل بأي خدمة خارجية ولا ينشئ tokens أو cookies خاصة بخدمة أخرى.
@@ -0,0 +1,16 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "tikwebsign"
7
+ version = "0.1.0"
8
+ description = "Safe HMAC request signing helpers for authorized APIs"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = {text = "MIT"}
12
+ authors = [{name = "tikwebsign contributors"}]
13
+ dependencies = []
14
+
15
+ [tool.setuptools.packages.find]
16
+ where = ["."]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,33 @@
1
+ import base64
2
+ import hashlib
3
+ import hmac
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ sys.path.insert(0, str(Path(__file__).parents[1]))
8
+ import tikwebsign
9
+
10
+ signer = tikwebsign.RequestSigner("secret", key_id="k1")
11
+ params, headers = signer.sign(
12
+ method="post", url="https://api.example.test/v1", params={"b": 2, "a": 1},
13
+ body='{"x":1}', timestamp=1700000000, nonce="n1"
14
+ )
15
+ canonical = "\n".join((
16
+ "POST", "https://api.example.test/v1", "a=1&b=2",
17
+ hashlib.sha256(b'{"x":1}').hexdigest(), "1700000000", "n1"
18
+ ))
19
+ expected = base64.urlsafe_b64encode(
20
+ hmac.new(b"secret", canonical.encode(), hashlib.sha256).digest()
21
+ ).decode().rstrip("=")
22
+ assert params == {"ts": "1700000000", "nonce": "n1", "key_id": "k1"}
23
+ assert headers["X-Request-Signature"] == expected
24
+
25
+ base = {"q": "hello"}
26
+ base.update(tikwebsign.getparams(signer=signer, method="GET", url="https://x", params=base))
27
+ assert "ts" in base
28
+ assert "X-Request-Signature" in tikwebsign.getheaders(
29
+ signer=signer, method="GET", url="https://x", params={"q": "hello"}
30
+ )
31
+ device = tikwebsign.generate_device()
32
+ assert len(device.device_id) == 36
33
+ print("ok")
@@ -0,0 +1,37 @@
1
+ import base64
2
+ import hashlib
3
+ import hmac
4
+
5
+ import tikwebsign
6
+
7
+
8
+ def test_signature_is_deterministic_with_fixed_nonce_and_timestamp():
9
+ signer = tikwebsign.RequestSigner("secret", key_id="k1")
10
+ params, headers = signer.sign(
11
+ method="post", url="https://api.example.test/v1", params={"b": 2, "a": 1},
12
+ body='{"x":1}', timestamp=1700000000, nonce="n1"
13
+ )
14
+ canonical = "\n".join((
15
+ "POST", "https://api.example.test/v1", "a=1&b=2",
16
+ hashlib.sha256(b'{"x":1}').hexdigest(), "1700000000", "n1"
17
+ ))
18
+ expected = base64.urlsafe_b64encode(
19
+ hmac.new(b"secret", canonical.encode(), hashlib.sha256).digest()
20
+ ).decode().rstrip("=")
21
+ assert params == {"ts": "1700000000", "nonce": "n1", "key_id": "k1"}
22
+ assert headers["X-Request-Signature"] == expected
23
+
24
+
25
+ def test_helpers_return_mergeable_dicts():
26
+ signer = tikwebsign.RequestSigner("secret")
27
+ params = {"q": "hello"}
28
+ params.update(tikwebsign.getparams(signer=signer, method="GET", url="https://x", params=params))
29
+ headers = tikwebsign.getheaders(signer=signer, method="GET", url="https://x", params=params)
30
+ assert "ts" in params and "X-Request-Signature" in headers
31
+
32
+
33
+ def test_device_is_local_and_serializable():
34
+ device = tikwebsign.generate_device()
35
+ data = device.to_dict()
36
+ assert len(data["device_id"]) == 36
37
+ assert data["platform"] == "python"
@@ -0,0 +1,15 @@
1
+ """tikwebsign: safe request signing helpers for APIs you own or are authorized to use.
2
+
3
+ This package deliberately does not implement third-party anti-bot bypasses,
4
+ credential checking, login automation, or scraping of device tokens.
5
+ """
6
+ from .core import Device, RequestSigner, generate_device, getheaders, getparams
7
+
8
+ __all__ = [
9
+ "Device",
10
+ "RequestSigner",
11
+ "generate_device",
12
+ "getheaders",
13
+ "getparams",
14
+ ]
15
+ __version__ = "0.1.0"
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import hashlib
5
+ import hmac
6
+ import json
7
+ import secrets
8
+ import time
9
+ import uuid
10
+ from dataclasses import asdict, dataclass
11
+ from typing import Mapping, MutableMapping
12
+ from urllib.parse import urlencode
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class Device:
17
+ """Stable local client identity; contains no third-party cookies or tokens."""
18
+
19
+ device_id: str
20
+ user_agent: str
21
+ platform: str = "python"
22
+ created_at: int = 0
23
+
24
+ def to_dict(self) -> dict[str, str | int]:
25
+ return asdict(self)
26
+
27
+
28
+ def generate_device(*, user_agent: str = "tikwebsign/0.1", platform: str = "python") -> Device:
29
+ """Generate a local device identity for an authorized API client."""
30
+ return Device(
31
+ device_id=str(uuid.uuid4()),
32
+ user_agent=user_agent,
33
+ platform=platform,
34
+ created_at=int(time.time()),
35
+ )
36
+
37
+
38
+ def _canonical_query(params: Mapping[str, object]) -> str:
39
+ pairs: list[tuple[str, str]] = []
40
+ for key, value in params.items():
41
+ if value is None:
42
+ continue
43
+ if isinstance(value, (list, tuple)):
44
+ pairs.extend((str(key), str(item)) for item in value)
45
+ else:
46
+ pairs.append((str(key), str(value)))
47
+ return urlencode(sorted(pairs), doseq=True)
48
+
49
+
50
+ def _body_bytes(body: str | bytes | bytearray | Mapping[str, object] | None) -> bytes:
51
+ if body is None:
52
+ return b""
53
+ if isinstance(body, (bytes, bytearray)):
54
+ return bytes(body)
55
+ if isinstance(body, str):
56
+ return body.encode("utf-8")
57
+ return json.dumps(body, separators=(",", ":"), sort_keys=True, ensure_ascii=False).encode("utf-8")
58
+
59
+
60
+ class RequestSigner:
61
+ """HMAC-SHA256 signer for a first-party or explicitly authorized API."""
62
+
63
+ def __init__(self, secret: str | bytes, *, key_id: str = "default", ttl: int = 300):
64
+ self.secret = secret.encode() if isinstance(secret, str) else bytes(secret)
65
+ if not self.secret:
66
+ raise ValueError("secret must not be empty")
67
+ if ttl <= 0:
68
+ raise ValueError("ttl must be positive")
69
+ self.key_id, self.ttl = key_id, ttl
70
+
71
+ def sign(self, *, method: str, url: str, params: Mapping[str, object] | None = None,
72
+ body: str | bytes | bytearray | Mapping[str, object] | None = None,
73
+ timestamp: int | None = None, nonce: str | None = None) -> tuple[dict[str, str], dict[str, str]]:
74
+ timestamp = int(time.time()) if timestamp is None else int(timestamp)
75
+ nonce = secrets.token_urlsafe(18) if nonce is None else nonce
76
+ query = _canonical_query(params or {})
77
+ body_hash = hashlib.sha256(_body_bytes(body)).hexdigest()
78
+ canonical = "\n".join((method.upper(), url, query, body_hash, str(timestamp), nonce))
79
+ digest = hmac.new(self.secret, canonical.encode("utf-8"), hashlib.sha256).digest()
80
+ signature = base64.urlsafe_b64encode(digest).decode("ascii").rstrip("=")
81
+ signed_params = {"ts": str(timestamp), "nonce": nonce, "key_id": self.key_id}
82
+ signed_headers = {
83
+ "X-Api-Key-Id": self.key_id,
84
+ "X-Request-Timestamp": str(timestamp),
85
+ "X-Request-Nonce": nonce,
86
+ "X-Request-Signature": signature,
87
+ }
88
+ return signed_params, signed_headers
89
+
90
+
91
+ def getparams(*, signer: RequestSigner, method: str, url: str,
92
+ params: Mapping[str, object] | None = None,
93
+ body: str | bytes | bytearray | Mapping[str, object] | None = None) -> dict[str, str]:
94
+ """Return signed query parameters to merge with ``params``."""
95
+ signed, _ = signer.sign(method=method, url=url, params=params, body=body)
96
+ return signed
97
+
98
+
99
+ def getheaders(*, signer: RequestSigner, method: str, url: str,
100
+ params: Mapping[str, object] | None = None,
101
+ body: str | bytes | bytearray | Mapping[str, object] | None = None,
102
+ headers: Mapping[str, str] | None = None) -> dict[str, str]:
103
+ """Return signed headers, preserving caller headers without overwriting them."""
104
+ _, signed = signer.sign(method=method, url=url, params=params, body=body)
105
+ result = dict(headers or {})
106
+ result.update(signed)
107
+ return result
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.4
2
+ Name: tikwebsign
3
+ Version: 0.1.0
4
+ Summary: Safe HMAC request signing helpers for authorized APIs
5
+ Author: tikwebsign contributors
6
+ License: MIT
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+
10
+ # tikwebsign
11
+
12
+ مكتبة صغيرة لتوقيع طلبات HTTP الخاصة بواجهات API التي تملكها أو لديك تفويض صريح لاستخدامها. تستخدم **HMAC-SHA256** مع canonical request، ولا تطبق تجاوزات anti-bot أو فحص بيانات اعتماد أو جمع cookies/tokens من خدمات طرف ثالث.
13
+
14
+ ## التثبيت
15
+
16
+ ```bash
17
+ pip install -e .
18
+ ```
19
+
20
+ ## الاستخدام
21
+
22
+ ```python
23
+ import requests
24
+ import tikwebsign
25
+
26
+ url = "https://api.example.com/v1/items"
27
+ params = {"page": 1, "limit": 20}
28
+ body = {"filter": "active"}
29
+ signer = tikwebsign.RequestSigner("ضع-المفتاح-السري-في-متغير-بيئة", key_id="client-1")
30
+
31
+ # الدوال ترجع dicts؛ لذلك الاستدعاء الصحيح يتضمن الأقواس.
32
+ signed_params = tikwebsign.getparams(
33
+ signer=signer, method="POST", url=url, params=params, body=body
34
+ )
35
+ headers = tikwebsign.getheaders(
36
+ signer=signer, method="POST", url=url, params=params, body=body,
37
+ headers={"Content-Type": "application/json"}
38
+ )
39
+ params.update(signed_params)
40
+ response = requests.post(url, params=params, json=body, headers=headers, timeout=30)
41
+ response.raise_for_status()
42
+ ```
43
+
44
+ > لا يمكن جعل `params.update(tikwebsign.getparams)` يولّد توقيعًا صحيحًا دون معرفة method وURL وbody والسر. لذلك صُممت الدوال كـ `getparams(...)` و`getheaders(...)` حتى يوقّع الخادم نفس البيانات بالترتيب canonical نفسه.
45
+
46
+ ## صيغة التوقيع للخادم
47
+
48
+ يبني العميل النص التالي مفصولًا بـ newline:
49
+
50
+ ```text
51
+ METHOD
52
+ URL
53
+ sorted-urlencoded-query
54
+ sha256(body)
55
+ timestamp
56
+ nonce
57
+ ```
58
+
59
+ ثم يحسب `HMAC-SHA256(secret, canonical_request)` ويضع القيمة Base64URL في `X-Request-Signature`. يجب على الخادم التحقق من `timestamp` ضمن نافذة قصيرة (مثل 300 ثانية)، ومنع إعادة استخدام `nonce`، واستخدام مقارنة ثابتة الزمن.
60
+
61
+ ## توليد جهاز محلي
62
+
63
+ ```python
64
+ device = tikwebsign.generate_device(user_agent="my-client/1.0")
65
+ print(device.to_dict())
66
+ ```
67
+
68
+ هذا مولد هوية محلية فقط، ولا يتصل بأي خدمة خارجية ولا ينشئ tokens أو cookies خاصة بخدمة أخرى.
@@ -0,0 +1,10 @@
1
+ README.md
2
+ pyproject.toml
3
+ tests/run_tests.py
4
+ tests/test_core.py
5
+ tikwebsign/__init__.py
6
+ tikwebsign/core.py
7
+ tikwebsign.egg-info/PKG-INFO
8
+ tikwebsign.egg-info/SOURCES.txt
9
+ tikwebsign.egg-info/dependency_links.txt
10
+ tikwebsign.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+ dist
2
+ tests
3
+ tikwebsign