sealedwebtoken 1.0.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,230 @@
1
+ # Logs
2
+
3
+ logs
4
+ _.log
5
+ npm-debug.log_
6
+ yarn-debug.log*
7
+ yarn-error.log*
8
+ lerna-debug.log*
9
+ .pnpm-debug.log*
10
+
11
+ # Diagnostic reports (https://nodejs.org/api/report.html)
12
+
13
+ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
14
+
15
+ # Runtime data
16
+
17
+ pids
18
+ _.pid
19
+ _.seed
20
+ \*.pid.lock
21
+
22
+ # Directory for instrumented libs generated by jscoverage/JSCover
23
+
24
+ lib-cov
25
+
26
+ # Coverage directory used by tools like istanbul
27
+
28
+ coverage
29
+ \*.lcov
30
+
31
+ # nyc test coverage
32
+
33
+ .nyc_output
34
+
35
+ # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
36
+
37
+ .grunt
38
+
39
+ # Bower dependency directory (https://bower.io/)
40
+
41
+ bower_components
42
+
43
+ # node-waf configuration
44
+
45
+ .lock-wscript
46
+
47
+ # Compiled binary addons (https://nodejs.org/api/addons.html)
48
+
49
+ build/Release
50
+
51
+ # Dependency directories
52
+
53
+ node_modules/
54
+ jspm_packages/
55
+
56
+ # Snowpack dependency directory (https://snowpack.dev/)
57
+
58
+ web_modules/
59
+
60
+ # TypeScript cache
61
+
62
+ \*.tsbuildinfo
63
+
64
+ # Optional npm cache directory
65
+
66
+ .npm
67
+
68
+ # Optional eslint cache
69
+
70
+ .eslintcache
71
+
72
+ # Optional stylelint cache
73
+
74
+ .stylelintcache
75
+
76
+ # Microbundle cache
77
+
78
+ .rpt2_cache/
79
+ .rts2_cache_cjs/
80
+ .rts2_cache_es/
81
+ .rts2_cache_umd/
82
+
83
+ # Optional REPL history
84
+
85
+ .node_repl_history
86
+
87
+ # Output of 'npm pack'
88
+
89
+ \*.tgz
90
+
91
+ # Yarn Integrity file
92
+
93
+ .yarn-integrity
94
+
95
+ # parcel-bundler cache (https://parceljs.org/)
96
+
97
+ .cache
98
+ .parcel-cache
99
+
100
+ # Next.js build output
101
+
102
+ .next
103
+ out
104
+
105
+ # Nuxt.js build / generate output
106
+
107
+ .nuxt
108
+ dist
109
+
110
+ # Gatsby files
111
+
112
+ .cache/
113
+
114
+ # Comment in the public line in if your project uses Gatsby and not Next.js
115
+
116
+ # https://nextjs.org/blog/next-9-1#public-directory-support
117
+
118
+ # public
119
+
120
+ # vuepress build output
121
+
122
+ .vuepress/dist
123
+
124
+ # vuepress v2.x temp and cache directory
125
+
126
+ .temp
127
+ .cache
128
+
129
+ # Docusaurus cache and generated files
130
+
131
+ .docusaurus
132
+
133
+ # Serverless directories
134
+
135
+ .serverless/
136
+
137
+ # FuseBox cache
138
+
139
+ .fusebox/
140
+
141
+ # DynamoDB Local files
142
+
143
+ .dynamodb/
144
+
145
+ # TernJS port file
146
+
147
+ .tern-port
148
+
149
+ # Stores VSCode versions used for testing VSCode extensions
150
+
151
+ .vscode-test
152
+
153
+ # yarn v2
154
+
155
+ .yarn/cache
156
+ .yarn/unplugged
157
+ .yarn/build-state.yml
158
+ .yarn/install-state.gz
159
+ .pnp.\*
160
+
161
+ # wrangler project
162
+
163
+ .dev.vars*
164
+ !.dev.vars.example
165
+ .env*
166
+ !.env.example
167
+ .wrangler/
168
+
169
+ # Python SDK / Standards
170
+ # Byte-compiled / optimized / DLL files
171
+ __pycache__/
172
+ *.py[cod]
173
+ *$py.class
174
+
175
+ # C extensions
176
+ *.so
177
+
178
+ # Distribution / packaging
179
+ build/
180
+ develop-eggs/
181
+ dist/
182
+ downloads/
183
+ eggs/
184
+ .eggs/
185
+ lib/
186
+ lib64/
187
+ parts/
188
+ sdist/
189
+ var/
190
+ wheels/
191
+ share/python-wheels/
192
+ *.egg-info/
193
+ .installed.cfg
194
+ *.egg
195
+ MANIFEST
196
+
197
+ # PyInstaller
198
+ # Usually these files are written by a python script from a template
199
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
200
+ *.manifest
201
+ *.spec
202
+
203
+ # Installer logs
204
+ pip-log.txt
205
+ pip-delete-this-directory.txt
206
+
207
+ # Unit test / coverage reports
208
+ htmlcov/
209
+ .tox/
210
+ .nox/
211
+ .coverage
212
+ .coverage.*
213
+ .cache
214
+ nosetests.xml
215
+ coverage.xml
216
+ *.cover
217
+ *.py,cover
218
+ .hypothesis/
219
+ .pytest_cache/
220
+ cover/
221
+
222
+ # Environments
223
+ .env
224
+ .venv
225
+ env/
226
+ venv/
227
+ ENV/
228
+ env.bak/
229
+ venv.bak/
230
+ test_env/
@@ -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,91 @@
1
+ # sealedwebtoken
2
+
3
+ The official Python SDK for Odysii Sealed Web Tokens (SWT).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ # Sync client only (zero dependencies)
9
+ pip install sealedwebtoken
10
+
11
+ # With async support (installs aiohttp)
12
+ pip install sealedwebtoken[async]
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ### Sync
18
+
19
+ ```python
20
+ import time
21
+ import swt
22
+
23
+ secret = "your-super-secret-key"
24
+ payload = {"user_id": 123, "role": "admin"}
25
+
26
+ # 1. Sign a token (expiresAt is required, max 31 days)
27
+ expires_at = int(time.time()) + 3600 # 1 hour from now
28
+ resp = swt.sign(payload, secret=secret, expires_at=expires_at)
29
+ print("Token:", resp.token)
30
+ print("Expires at:", resp.expires_at)
31
+
32
+ # 2. Verify a token
33
+ v = swt.verify(resp.token, secret=secret)
34
+ if v.valid:
35
+ print("Payload:", v.payload)
36
+
37
+ # 3. Revoke a token
38
+ swt.revoke(resp.token, secret=secret)
39
+ print("Token revoked")
40
+ ```
41
+
42
+ ### Async
43
+
44
+ ```python
45
+ import asyncio, time
46
+ import swt
47
+
48
+ async def main():
49
+ client = swt.AsyncSWTClient()
50
+ expires_at = int(time.time()) + 3600
51
+
52
+ resp = await client.sign({"user_id": 42}, secret="mysecret123", expires_at=expires_at)
53
+ print("Token:", resp.token)
54
+
55
+ v = await client.verify(resp.token, secret="mysecret123")
56
+ if v.valid:
57
+ print("Payload:", v.payload)
58
+
59
+ await client.revoke(resp.token, secret="mysecret123")
60
+ print("Token revoked")
61
+
62
+ asyncio.run(main())
63
+ ```
64
+
65
+ ### Using the class-based client
66
+
67
+ ```python
68
+ import swt
69
+
70
+ # Create a reusable client
71
+ client = swt.SWTClient()
72
+ resp = client.sign({"user_id": 1}, secret="mysecret123", expires_at=...)
73
+ ```
74
+
75
+ ## API Reference
76
+
77
+ ### `swt.sign(payload, *, secret, expires_at) → SignResponse`
78
+
79
+ - `payload` — `dict` to embed in the token (max 512 bytes serialised)
80
+ - `secret` — string, min 8 characters
81
+ - `expires_at` — Unix timestamp in seconds, **required**, max 31 days from now
82
+
83
+ ### `swt.verify(token, *, secret=None) → VerifyResponse`
84
+
85
+ - `token` — the SWT token string
86
+ - `secret` — optional; if provided, validates the secret matches
87
+
88
+ ### `swt.revoke(token, *, secret) → RevokeResponse`
89
+
90
+ - `token` — the SWT token string to revoke
91
+ - `secret` — the secret used when signing
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "sealedwebtoken"
7
+ version = "1.0.0"
8
+ description = "Official Python SDK for Odysii Sealed Web Tokens (SWT)"
9
+ readme = "README.md"
10
+ license = { text = "ISC" }
11
+ authors = [{ name = "snskar125" }]
12
+ keywords = ["swt", "sealed-web-tokens", "jwt", "authentication", "tokens"]
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "License :: OSI Approved :: ISC License (ISCL)",
16
+ "Operating System :: OS Independent",
17
+ "Topic :: Internet :: WWW/HTTP",
18
+ "Topic :: Security",
19
+ ]
20
+ requires-python = ">=3.9"
21
+
22
+ # Zero required dependencies — sync client uses stdlib urllib only
23
+ dependencies = []
24
+
25
+ [project.optional-dependencies]
26
+ # Install aiohttp to use AsyncSWTClient
27
+ async = ["aiohttp>=3.9"]
28
+
29
+ [project.urls]
30
+ Homepage = "https://swt.odysii.in"
31
+ Repository = "https://github.com/snskar125/swt"
32
+
33
+ [tool.hatch.build.targets.wheel]
34
+ packages = ["swt"]
@@ -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
+ ]
@@ -0,0 +1,142 @@
1
+ """
2
+ Test suite for the Python SWT SDK.
3
+ Runs against the live API — requires network access.
4
+ """
5
+
6
+ import sys
7
+ import time
8
+
9
+ # (Removed sys.path so we test the actual installed pip package)
10
+
11
+ import swt
12
+
13
+ # ─── Config ───────────────────────────────────────────────────────────────────
14
+
15
+ SECRET = "test-secret-swt-2026"
16
+ PAYLOAD = {"userId": "py-test-001", "role": "tester"}
17
+ EXPIRES_AT = int(time.time()) + 3600 # 1 hour from now
18
+
19
+ PASS = "\033[92m✓\033[0m"
20
+ FAIL = "\033[91m✗\033[0m"
21
+
22
+ def check(label: str, condition: bool, detail: str = "") -> None:
23
+ status = PASS if condition else FAIL
24
+ print(f" {status} {label}" + (f" — {detail}" if detail else ""))
25
+ if not condition:
26
+ sys.exit(1)
27
+
28
+ # ─── Tests ────────────────────────────────────────────────────────────────────
29
+
30
+ print("\n🔐 SWT Python SDK — Live API Test\n")
31
+
32
+ # 1. SIGN
33
+ print("1. sign()")
34
+ try:
35
+ resp = swt.sign(PAYLOAD, secret=SECRET, expires_at=EXPIRES_AT)
36
+ check("returns a token string", isinstance(resp.token, str) and resp.token.startswith("swt_"))
37
+ check("token has correct format (swt_<jti>.<blob>)", resp.token.count(".") == 1)
38
+ check("returns expiresAt as int", isinstance(resp.expires_at, int))
39
+ check("expiresAt matches what we sent", resp.expires_at == EXPIRES_AT)
40
+ TOKEN = resp.token
41
+ print(f" token: {TOKEN[:60]}...")
42
+ except swt.SWTError as e:
43
+ check("sign() succeeded", False, str(e))
44
+
45
+ print()
46
+
47
+ # 2. VERIFY — valid token
48
+ print("2. verify() — valid token")
49
+ try:
50
+ v = swt.verify(TOKEN, secret=SECRET)
51
+ check("valid is True", v.valid is True)
52
+ check("payload matches", v.payload == PAYLOAD)
53
+ check("expiresAt is returned", v.expires_at == EXPIRES_AT)
54
+ except swt.SWTError as e:
55
+ check("verify() succeeded", False, str(e))
56
+
57
+ print()
58
+
59
+ # 3. VERIFY — wrong secret
60
+ print("3. verify() — wrong secret")
61
+ try:
62
+ v = swt.verify(TOKEN, secret="wrong-secret-123")
63
+ check("valid is False for wrong secret", v.valid is False)
64
+ except swt.SWTError:
65
+ # API may return 401 which throws SWTError — also acceptable
66
+ check("rejected with wrong secret", True, "raised SWTError")
67
+
68
+ print()
69
+
70
+ # 4. VERIFY — no secret (token-only check)
71
+ print("4. verify() — no secret (token only)")
72
+ try:
73
+ v = swt.verify(TOKEN)
74
+ check("valid is True without secret", v.valid is True)
75
+ check("payload is present", v.payload is not None)
76
+ except swt.SWTError as e:
77
+ check("verify() without secret succeeded", False, str(e))
78
+
79
+ print()
80
+
81
+ # 5. VERIFY — malformed token
82
+ print("5. verify() — malformed token")
83
+ try:
84
+ v = swt.verify("not-a-real-token", secret=SECRET)
85
+ check("valid is False for malformed token", v.valid is False)
86
+ except swt.SWTError:
87
+ check("rejected malformed token", True, "raised SWTError")
88
+
89
+ print()
90
+
91
+ # 6. REVOKE
92
+ print("6. revoke()")
93
+ try:
94
+ r = swt.revoke(TOKEN, secret=SECRET)
95
+ check("success is True", r.success is True)
96
+ except swt.SWTError as e:
97
+ check("revoke() succeeded", False, str(e))
98
+
99
+ print()
100
+
101
+ # 7. VERIFY — after revocation
102
+ print("7. verify() — after revocation")
103
+ try:
104
+ v = swt.verify(TOKEN, secret=SECRET)
105
+ check("valid is False after revoke", v.valid is False)
106
+ except swt.SWTError:
107
+ check("rejected revoked token", True, "raised SWTError")
108
+
109
+ print()
110
+
111
+ # 8. REVOKE — wrong secret
112
+ print("8. revoke() — wrong secret")
113
+ resp2 = swt.sign({"test": "revoke-auth"}, secret=SECRET, expires_at=int(time.time()) + 3600)
114
+ try:
115
+ swt.revoke(resp2.token, secret="wrong-secret-123")
116
+ check("should have rejected", False, "expected SWTError but didn't get one")
117
+ except swt.SWTError as e:
118
+ check("rejected revoke with wrong secret", True, f"SWTError: {e}")
119
+
120
+ print()
121
+
122
+ # 9. SIGN — expired expiresAt
123
+ print("9. sign() — expiresAt in the past")
124
+ try:
125
+ swt.sign(PAYLOAD, secret=SECRET, expires_at=int(time.time()) - 100)
126
+ check("should have rejected past expiresAt", False, "expected SWTError")
127
+ except swt.SWTError as e:
128
+ check("rejected past expiresAt", True, f"SWTError: {e}")
129
+
130
+ print()
131
+
132
+ # 10. SIGN — expiresAt beyond 31 days
133
+ print("10. sign() — expiresAt > 31 days")
134
+ try:
135
+ far_future = int(time.time()) + (32 * 24 * 60 * 60)
136
+ swt.sign(PAYLOAD, secret=SECRET, expires_at=far_future)
137
+ check("should have rejected >31 day TTL", False, "expected SWTError")
138
+ except swt.SWTError as e:
139
+ check("rejected >31 day TTL", True, f"SWTError: {e}")
140
+
141
+ print()
142
+ print("✅ All tests passed\n")