sealedwebtoken 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.
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sealedwebtoken
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python SDK for Odysii Sealed Web Tokens (SWT)
|
|
5
|
+
Project-URL: Homepage, https://swt.odysii.in
|
|
6
|
+
Project-URL: Repository, https://github.com/snskar125/swt
|
|
7
|
+
Author: snskar125
|
|
8
|
+
License: ISC
|
|
9
|
+
Keywords: authentication,jwt,sealed-web-tokens,swt,tokens
|
|
10
|
+
Classifier: License :: OSI Approved :: ISC License (ISCL)
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
14
|
+
Classifier: Topic :: Security
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Provides-Extra: async
|
|
17
|
+
Requires-Dist: aiohttp>=3.9; extra == 'async'
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# sealedwebtoken
|
|
21
|
+
|
|
22
|
+
The official Python SDK for Odysii Sealed Web Tokens (SWT).
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Sync client only (zero dependencies)
|
|
28
|
+
pip install sealedwebtoken
|
|
29
|
+
|
|
30
|
+
# With async support (installs aiohttp)
|
|
31
|
+
pip install sealedwebtoken[async]
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
### Sync
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
import time
|
|
40
|
+
import swt
|
|
41
|
+
|
|
42
|
+
secret = "your-super-secret-key"
|
|
43
|
+
payload = {"user_id": 123, "role": "admin"}
|
|
44
|
+
|
|
45
|
+
# 1. Sign a token (expiresAt is required, max 31 days)
|
|
46
|
+
expires_at = int(time.time()) + 3600 # 1 hour from now
|
|
47
|
+
resp = swt.sign(payload, secret=secret, expires_at=expires_at)
|
|
48
|
+
print("Token:", resp.token)
|
|
49
|
+
print("Expires at:", resp.expires_at)
|
|
50
|
+
|
|
51
|
+
# 2. Verify a token
|
|
52
|
+
v = swt.verify(resp.token, secret=secret)
|
|
53
|
+
if v.valid:
|
|
54
|
+
print("Payload:", v.payload)
|
|
55
|
+
|
|
56
|
+
# 3. Revoke a token
|
|
57
|
+
swt.revoke(resp.token, secret=secret)
|
|
58
|
+
print("Token revoked")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Async
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
import asyncio, time
|
|
65
|
+
import swt
|
|
66
|
+
|
|
67
|
+
async def main():
|
|
68
|
+
client = swt.AsyncSWTClient()
|
|
69
|
+
expires_at = int(time.time()) + 3600
|
|
70
|
+
|
|
71
|
+
resp = await client.sign({"user_id": 42}, secret="mysecret123", expires_at=expires_at)
|
|
72
|
+
print("Token:", resp.token)
|
|
73
|
+
|
|
74
|
+
v = await client.verify(resp.token, secret="mysecret123")
|
|
75
|
+
if v.valid:
|
|
76
|
+
print("Payload:", v.payload)
|
|
77
|
+
|
|
78
|
+
await client.revoke(resp.token, secret="mysecret123")
|
|
79
|
+
print("Token revoked")
|
|
80
|
+
|
|
81
|
+
asyncio.run(main())
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Using the class-based client
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
import swt
|
|
88
|
+
|
|
89
|
+
# Create a reusable client
|
|
90
|
+
client = swt.SWTClient()
|
|
91
|
+
resp = client.sign({"user_id": 1}, secret="mysecret123", expires_at=...)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## API Reference
|
|
95
|
+
|
|
96
|
+
### `swt.sign(payload, *, secret, expires_at) → SignResponse`
|
|
97
|
+
|
|
98
|
+
- `payload` — `dict` to embed in the token (max 512 bytes serialised)
|
|
99
|
+
- `secret` — string, min 8 characters
|
|
100
|
+
- `expires_at` — Unix timestamp in seconds, **required**, max 31 days from now
|
|
101
|
+
|
|
102
|
+
### `swt.verify(token, *, secret=None) → VerifyResponse`
|
|
103
|
+
|
|
104
|
+
- `token` — the SWT token string
|
|
105
|
+
- `secret` — optional; if provided, validates the secret matches
|
|
106
|
+
|
|
107
|
+
### `swt.revoke(token, *, secret) → RevokeResponse`
|
|
108
|
+
|
|
109
|
+
- `token` — the SWT token string to revoke
|
|
110
|
+
- `secret` — the secret used when signing
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
swt/__init__.py,sha256=tbHi8fD7upx-Sj0p-136aQA_O5jLqgbnRN7KjeAAFpY,12684
|
|
2
|
+
sealedwebtoken-1.0.0.dist-info/METADATA,sha256=BSP5NlpDBSWCvrT63tJdK221cs0ohVqxo2K0F3ObNIk,2757
|
|
3
|
+
sealedwebtoken-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
4
|
+
sealedwebtoken-1.0.0.dist-info/RECORD,,
|
swt/__init__.py
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"""
|
|
2
|
+
swt — Official Python SDK for Odysii Sealed Web Tokens (SWT).
|
|
3
|
+
|
|
4
|
+
Provides both a synchronous client (SWTClient) and an async client
|
|
5
|
+
(AsyncSWTClient) that mirror the Node.js SDK's sign/verify/revoke API.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
15
|
+
# RESPONSE TYPES
|
|
16
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class SignResponse:
|
|
20
|
+
"""Returned by sign() on success."""
|
|
21
|
+
|
|
22
|
+
token: str
|
|
23
|
+
"""The issued SWT token string."""
|
|
24
|
+
|
|
25
|
+
expires_at: int
|
|
26
|
+
"""Unix timestamp (seconds) when the token expires."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class VerifyResponse:
|
|
31
|
+
"""Returned by verify() on success."""
|
|
32
|
+
|
|
33
|
+
valid: bool
|
|
34
|
+
"""True if the token is valid, not expired, and not revoked."""
|
|
35
|
+
|
|
36
|
+
payload: dict[str, Any] | None = None
|
|
37
|
+
"""The caller-supplied payload embedded in the token. None if invalid."""
|
|
38
|
+
|
|
39
|
+
expires_at: int | None = None
|
|
40
|
+
"""Unix timestamp when the token expires. None if invalid."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class RevokeResponse:
|
|
45
|
+
"""Returned by revoke() on success."""
|
|
46
|
+
|
|
47
|
+
success: bool
|
|
48
|
+
"""True if the token was revoked (or was already revoked)."""
|
|
49
|
+
|
|
50
|
+
message: str | None = None
|
|
51
|
+
"""Optional human-readable status message."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
55
|
+
# EXCEPTIONS
|
|
56
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
class SWTError(Exception):
|
|
59
|
+
"""Raised when the SWT API returns an error response."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, message: str, status_code: int | None = None) -> None:
|
|
62
|
+
super().__init__(message)
|
|
63
|
+
self.status_code = status_code
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
67
|
+
# CONSTANTS
|
|
68
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
_API_URL = "https://api.swt.odysii.in"
|
|
71
|
+
|
|
72
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
73
|
+
# SYNC CLIENT
|
|
74
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
class SWTClient:
|
|
77
|
+
"""
|
|
78
|
+
Synchronous SWT client using urllib (zero dependencies).
|
|
79
|
+
|
|
80
|
+
Example::
|
|
81
|
+
|
|
82
|
+
import swt
|
|
83
|
+
|
|
84
|
+
client = swt.SWTClient()
|
|
85
|
+
|
|
86
|
+
# Sign
|
|
87
|
+
import time
|
|
88
|
+
expires_at = int(time.time()) + 3600 # 1 hour from now
|
|
89
|
+
resp = client.sign({"user_id": 42}, secret="mysecret123", expires_at=expires_at)
|
|
90
|
+
print(resp.token)
|
|
91
|
+
|
|
92
|
+
# Verify
|
|
93
|
+
v = client.verify(resp.token, secret="mysecret123")
|
|
94
|
+
if v.valid:
|
|
95
|
+
print(v.payload)
|
|
96
|
+
|
|
97
|
+
# Revoke
|
|
98
|
+
client.revoke(resp.token, secret="mysecret123")
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
def __init__(self, base_url: str = _API_URL) -> None:
|
|
102
|
+
self._base_url = base_url.rstrip("/")
|
|
103
|
+
|
|
104
|
+
def _post(self, path: str, body: dict[str, Any]) -> dict[str, Any]:
|
|
105
|
+
"""Make a synchronous POST request using urllib (no third-party deps)."""
|
|
106
|
+
import urllib.request
|
|
107
|
+
import urllib.error
|
|
108
|
+
|
|
109
|
+
data = json.dumps(body).encode("utf-8")
|
|
110
|
+
req = urllib.request.Request(
|
|
111
|
+
f"{self._base_url}{path}",
|
|
112
|
+
data=data,
|
|
113
|
+
headers={
|
|
114
|
+
"Content-Type": "application/json",
|
|
115
|
+
"User-Agent": "odysii-swt-python/1.0.0",
|
|
116
|
+
},
|
|
117
|
+
method="POST",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
with urllib.request.urlopen(req) as resp:
|
|
122
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
123
|
+
except urllib.error.HTTPError as exc:
|
|
124
|
+
# Read the error body for the API error message
|
|
125
|
+
try:
|
|
126
|
+
error_body = json.loads(exc.read().decode("utf-8"))
|
|
127
|
+
msg = error_body.get("error", f"HTTP {exc.code}")
|
|
128
|
+
except Exception:
|
|
129
|
+
msg = f"HTTP {exc.code}"
|
|
130
|
+
raise SWTError(msg, status_code=exc.code) from exc
|
|
131
|
+
|
|
132
|
+
def sign(
|
|
133
|
+
self,
|
|
134
|
+
payload: dict[str, Any],
|
|
135
|
+
*,
|
|
136
|
+
secret: str,
|
|
137
|
+
expires_at: int,
|
|
138
|
+
) -> SignResponse:
|
|
139
|
+
"""
|
|
140
|
+
Issue a new SWT token.
|
|
141
|
+
|
|
142
|
+
:param payload: Arbitrary dict to embed in the token (max 512 bytes serialised).
|
|
143
|
+
:param secret: Secret used for signing (min 8 characters).
|
|
144
|
+
:param expires_at: Required Unix timestamp (seconds) for expiry. Max 31 days from now.
|
|
145
|
+
:raises SWTError: If the API returns an error.
|
|
146
|
+
"""
|
|
147
|
+
data = self._post("/v1/sign", {
|
|
148
|
+
"secret": secret,
|
|
149
|
+
"payload": payload,
|
|
150
|
+
"expiresAt": expires_at,
|
|
151
|
+
})
|
|
152
|
+
return SignResponse(token=data["token"], expires_at=data["expiresAt"])
|
|
153
|
+
|
|
154
|
+
def verify(
|
|
155
|
+
self,
|
|
156
|
+
token: str,
|
|
157
|
+
*,
|
|
158
|
+
secret: str | None = None,
|
|
159
|
+
) -> VerifyResponse:
|
|
160
|
+
"""
|
|
161
|
+
Verify a SWT token.
|
|
162
|
+
|
|
163
|
+
:param token: The token string to verify.
|
|
164
|
+
:param secret: Optional — if provided, also validates the secret matches.
|
|
165
|
+
:raises SWTError: If the API returns a non-auth error.
|
|
166
|
+
"""
|
|
167
|
+
body: dict[str, Any] = {"token": token}
|
|
168
|
+
if secret is not None:
|
|
169
|
+
body["secret"] = secret
|
|
170
|
+
|
|
171
|
+
data = self._post("/v1/verify", body)
|
|
172
|
+
return VerifyResponse(
|
|
173
|
+
valid=data.get("valid", False),
|
|
174
|
+
payload=data.get("payload"),
|
|
175
|
+
expires_at=data.get("expiresAt"),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def revoke(self, token: str, *, secret: str) -> RevokeResponse:
|
|
179
|
+
"""
|
|
180
|
+
Revoke a SWT token.
|
|
181
|
+
|
|
182
|
+
:param token: The token string to revoke.
|
|
183
|
+
:param secret: The secret that was used to sign the token.
|
|
184
|
+
:raises SWTError: If the API returns an error.
|
|
185
|
+
"""
|
|
186
|
+
data = self._post("/v1/revoke", {"token": token, "secret": secret})
|
|
187
|
+
return RevokeResponse(success=data.get("success", False), message=data.get("message"))
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
191
|
+
# ASYNC CLIENT
|
|
192
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
class AsyncSWTClient:
|
|
195
|
+
"""
|
|
196
|
+
Asynchronous SWT client using aiohttp.
|
|
197
|
+
|
|
198
|
+
Requires ``aiohttp`` to be installed::
|
|
199
|
+
|
|
200
|
+
pip install swt[async]
|
|
201
|
+
|
|
202
|
+
Example::
|
|
203
|
+
|
|
204
|
+
import asyncio, time
|
|
205
|
+
import swt
|
|
206
|
+
|
|
207
|
+
async def main():
|
|
208
|
+
client = swt.AsyncSWTClient()
|
|
209
|
+
expires_at = int(time.time()) + 3600
|
|
210
|
+
|
|
211
|
+
resp = await client.sign({"user_id": 42}, secret="mysecret123", expires_at=expires_at)
|
|
212
|
+
print(resp.token)
|
|
213
|
+
|
|
214
|
+
v = await client.verify(resp.token, secret="mysecret123")
|
|
215
|
+
if v.valid:
|
|
216
|
+
print(v.payload)
|
|
217
|
+
|
|
218
|
+
await client.revoke(resp.token, secret="mysecret123")
|
|
219
|
+
|
|
220
|
+
asyncio.run(main())
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
def __init__(self, base_url: str = _API_URL) -> None:
|
|
224
|
+
self._base_url = base_url.rstrip("/")
|
|
225
|
+
|
|
226
|
+
async def _post(self, path: str, body: dict[str, Any]) -> dict[str, Any]:
|
|
227
|
+
"""Make an async POST request using aiohttp."""
|
|
228
|
+
try:
|
|
229
|
+
import aiohttp
|
|
230
|
+
except ImportError as exc:
|
|
231
|
+
raise ImportError(
|
|
232
|
+
"AsyncSWTClient requires aiohttp. Install it with: pip install swt[async]"
|
|
233
|
+
) from exc
|
|
234
|
+
|
|
235
|
+
async with aiohttp.ClientSession() as session:
|
|
236
|
+
async with session.post(
|
|
237
|
+
f"{self._base_url}{path}",
|
|
238
|
+
json=body,
|
|
239
|
+
headers={
|
|
240
|
+
"Content-Type": "application/json",
|
|
241
|
+
"User-Agent": "odysii-swt-python/1.0.0",
|
|
242
|
+
},
|
|
243
|
+
) as resp:
|
|
244
|
+
data = await resp.json()
|
|
245
|
+
if not resp.ok:
|
|
246
|
+
msg = data.get("error", f"HTTP {resp.status}") if isinstance(data, dict) else f"HTTP {resp.status}"
|
|
247
|
+
raise SWTError(msg, status_code=resp.status)
|
|
248
|
+
return data
|
|
249
|
+
|
|
250
|
+
async def sign(
|
|
251
|
+
self,
|
|
252
|
+
payload: dict[str, Any],
|
|
253
|
+
*,
|
|
254
|
+
secret: str,
|
|
255
|
+
expires_at: int,
|
|
256
|
+
) -> SignResponse:
|
|
257
|
+
"""
|
|
258
|
+
Issue a new SWT token (async).
|
|
259
|
+
|
|
260
|
+
:param payload: Arbitrary dict to embed in the token (max 512 bytes serialised).
|
|
261
|
+
:param secret: Secret used for signing (min 8 characters).
|
|
262
|
+
:param expires_at: Required Unix timestamp (seconds) for expiry. Max 31 days from now.
|
|
263
|
+
:raises SWTError: If the API returns an error.
|
|
264
|
+
"""
|
|
265
|
+
data = await self._post("/v1/sign", {
|
|
266
|
+
"secret": secret,
|
|
267
|
+
"payload": payload,
|
|
268
|
+
"expiresAt": expires_at,
|
|
269
|
+
})
|
|
270
|
+
return SignResponse(token=data["token"], expires_at=data["expiresAt"])
|
|
271
|
+
|
|
272
|
+
async def verify(
|
|
273
|
+
self,
|
|
274
|
+
token: str,
|
|
275
|
+
*,
|
|
276
|
+
secret: str | None = None,
|
|
277
|
+
) -> VerifyResponse:
|
|
278
|
+
"""
|
|
279
|
+
Verify a SWT token (async).
|
|
280
|
+
|
|
281
|
+
:param token: The token string to verify.
|
|
282
|
+
:param secret: Optional — if provided, also validates the secret matches.
|
|
283
|
+
:raises SWTError: If the API returns a non-auth error.
|
|
284
|
+
"""
|
|
285
|
+
body: dict[str, Any] = {"token": token}
|
|
286
|
+
if secret is not None:
|
|
287
|
+
body["secret"] = secret
|
|
288
|
+
|
|
289
|
+
data = await self._post("/v1/verify", body)
|
|
290
|
+
return VerifyResponse(
|
|
291
|
+
valid=data.get("valid", False),
|
|
292
|
+
payload=data.get("payload"),
|
|
293
|
+
expires_at=data.get("expiresAt"),
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
async def revoke(self, token: str, *, secret: str) -> RevokeResponse:
|
|
297
|
+
"""
|
|
298
|
+
Revoke a SWT token (async).
|
|
299
|
+
|
|
300
|
+
:param token: The token string to revoke.
|
|
301
|
+
:param secret: The secret that was used to sign the token.
|
|
302
|
+
:raises SWTError: If the API returns an error.
|
|
303
|
+
"""
|
|
304
|
+
data = await self._post("/v1/revoke", {"token": token, "secret": secret})
|
|
305
|
+
return RevokeResponse(success=data.get("success", False), message=data.get("message"))
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
309
|
+
# MODULE-LEVEL CONVENIENCE (mirrors Node.js SDK's functional API)
|
|
310
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
311
|
+
|
|
312
|
+
_default_client = SWTClient()
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def sign(
|
|
316
|
+
payload: dict[str, Any],
|
|
317
|
+
*,
|
|
318
|
+
secret: str,
|
|
319
|
+
expires_at: int,
|
|
320
|
+
) -> SignResponse:
|
|
321
|
+
"""Module-level sign() — uses a shared default SWTClient."""
|
|
322
|
+
return _default_client.sign(payload, secret=secret, expires_at=expires_at)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def verify(token: str, *, secret: str | None = None) -> VerifyResponse:
|
|
326
|
+
"""Module-level verify() — uses a shared default SWTClient."""
|
|
327
|
+
return _default_client.verify(token, secret=secret)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def revoke(token: str, *, secret: str) -> RevokeResponse:
|
|
331
|
+
"""Module-level revoke() — uses a shared default SWTClient."""
|
|
332
|
+
return _default_client.revoke(token, secret=secret)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
__all__ = [
|
|
336
|
+
"SWTClient",
|
|
337
|
+
"AsyncSWTClient",
|
|
338
|
+
"SignResponse",
|
|
339
|
+
"VerifyResponse",
|
|
340
|
+
"RevokeResponse",
|
|
341
|
+
"SWTError",
|
|
342
|
+
"sign",
|
|
343
|
+
"verify",
|
|
344
|
+
"revoke",
|
|
345
|
+
]
|