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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
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
+ ]