fastapi-cachex 0.3.2__tar.gz → 0.3.5__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.
Files changed (41) hide show
  1. {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/PKG-INFO +253 -12
  2. {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/README.md +251 -9
  3. {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/__init__.py +19 -0
  4. fastapi_cachex-0.3.5/fastapi_cachex/backends/base.py +175 -0
  5. fastapi_cachex-0.3.5/fastapi_cachex/backends/codec.py +79 -0
  6. {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/backends/memcached.py +126 -47
  7. {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/backends/memory.py +108 -65
  8. {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/backends/redis.py +94 -165
  9. fastapi_cachex-0.3.5/fastapi_cachex/cache.py +632 -0
  10. {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/dependencies.py +13 -1
  11. {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/manager.py +6 -12
  12. {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/proxy.py +2 -2
  13. {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/routes.py +82 -120
  14. {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/session/config.py +43 -1
  15. {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/session/manager.py +55 -55
  16. {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/session/middleware.py +48 -46
  17. {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/session/security.py +9 -2
  18. {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/session/token_serializers.py +4 -4
  19. {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/state/manager.py +47 -82
  20. fastapi_cachex-0.3.5/fastapi_cachex/types.py +73 -0
  21. {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/pyproject.toml +10 -4
  22. {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/pyproject.toml.orig +7 -4
  23. fastapi_cachex-0.3.2/fastapi_cachex/backends/base.py +0 -70
  24. fastapi_cachex-0.3.2/fastapi_cachex/cache.py +0 -416
  25. fastapi_cachex-0.3.2/fastapi_cachex/types.py +0 -34
  26. {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/backends/__init__.py +0 -0
  27. {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/backends/config.py +0 -0
  28. {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/directives.py +0 -0
  29. {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/exceptions.py +0 -0
  30. {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/manager_proxy.py +0 -0
  31. {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/py.typed +0 -0
  32. {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/session/__init__.py +0 -0
  33. {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/session/dependencies.py +0 -0
  34. {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/session/exceptions.py +0 -0
  35. {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/session/models.py +0 -0
  36. {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/session/proxy.py +0 -0
  37. {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/state/__init__.py +0 -0
  38. {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/state/dependencies.py +0 -0
  39. {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/state/exceptions.py +0 -0
  40. {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/state/models.py +0 -0
  41. {fastapi_cachex-0.3.2 → fastapi_cachex-0.3.5}/fastapi_cachex/state/proxy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-cachex
3
- Version: 0.3.2
3
+ Version: 0.3.5
4
4
  Summary: A caching library for FastAPI with support for Cache-Control, ETag, and multiple backends.
5
5
  Keywords: fastapi,cache,etag,cache-control,redis,memcached,in-memory
6
6
  Author: allen0099
@@ -20,12 +20,12 @@ Classifier: Framework :: FastAPI
20
20
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
21
  Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
22
22
  Requires-Dist: fastapi
23
+ Requires-Dist: itsdangerous
23
24
  Requires-Dist: pydantic
24
25
  Requires-Dist: pyjwt>=2.9.0 ; extra == 'jwt'
25
26
  Requires-Dist: pymemcache ; extra == 'memcache'
26
27
  Requires-Dist: redis[hiredis]>=5.3.0 ; extra == 'redis'
27
28
  Requires-Dist: orjson ; extra == 'redis'
28
- Requires-Dist: itsdangerous ; extra == 'starlette'
29
29
  Requires-Python: >=3.10
30
30
  Project-URL: Homepage, https://github.com/allen0099/FastAPI-CacheX
31
31
  Project-URL: Repository, https://github.com/allen0099/FastAPI-CacheX.git
@@ -33,7 +33,6 @@ Project-URL: Issues, https://github.com/allen0099/FastAPI-CacheX/issues
33
33
  Provides-Extra: jwt
34
34
  Provides-Extra: memcache
35
35
  Provides-Extra: redis
36
- Provides-Extra: starlette
37
36
  Description-Content-Type: text/markdown
38
37
 
39
38
  # FastAPI-Cache X
@@ -72,7 +71,9 @@ A high-performance caching extension for FastAPI, providing comprehensive HTTP c
72
71
  - Secure session management with HMAC-SHA256 token signing
73
72
  - Optional JWT token format for interoperability (install extra `jwt`)
74
73
  - IP address and User-Agent binding (optional security features)
75
- - Header and bearer token support (API-first architecture)
74
+ - Header, bearer token and cookie transports (cookies via
75
+ `FastAPICacheXSessionMiddleware`; the older `SessionMiddleware` is deprecated
76
+ and removed in 0.4.0 — see [Session Management Guide](docs/SESSION.md))
76
77
  - Automatic session renewal (sliding expiration)
77
78
  - Flash messages for cross-request communication
78
79
  - Multiple backend support (Redis, Memcached, In-Memory)
@@ -102,11 +103,19 @@ A high-performance caching extension for FastAPI, providing comprehensive HTTP c
102
103
  uv add fastapi-cachex
103
104
  ```
104
105
 
105
- To enable JWT token format support for sessions:
106
+ Everything in the core package works with the in-memory backend. The other
107
+ backends and the optional session transports ship as extras:
106
108
 
107
- ```bash
108
- uv add "fastapi-cachex[jwt]"
109
- ```
109
+ | Extra | Install | Pulls in | Needed for |
110
+ |-------|---------|----------|------------|
111
+ | `redis` | `uv add "fastapi-cachex[redis]"` | `redis[hiredis]`, `orjson` | `AsyncRedisCacheBackend` |
112
+ | `memcache` | `uv add "fastapi-cachex[memcache]"` | `pymemcache` | `MemcachedBackend` (note: `memcache`, not `memcached`) |
113
+ | `jwt` | `uv add "fastapi-cachex[jwt]"` | `PyJWT` | `SessionConfig(token_format="jwt")` |
114
+
115
+ The `starlette` extra is gone: `itsdangerous` is a base dependency now, so
116
+ `FastAPICacheXSessionMiddleware` works on a plain install.
117
+
118
+ Extras combine: `uv add "fastapi-cachex[redis,jwt]"`.
110
119
 
111
120
  ### Development Installation
112
121
 
@@ -145,9 +154,88 @@ async def non_store_endpoint():
145
154
  @app.get("/clear_cache")
146
155
  async def remove_cache(cache: CacheBackend):
147
156
  await cache.clear_path("/path/to/clear") # Clear cache for a specific path
148
- await cache.clear_pattern("/path/to/clear/*") # Clear cache for a specific pattern
157
+ # Patterns match the whole key, not just the path
158
+ await cache.clear_pattern("GET|||*|||/path/to/clear/*")
159
+ ```
160
+
161
+ `clear_pattern` globs the whole logical key. HTTP cache keys look like
162
+ `method|||host|||path|||query`, so a pattern that is only a path matches
163
+ nothing — use `clear_path(path, include_params=True)` when the path is what you
164
+ mean, and keep `clear_pattern` for keys you built yourself.
165
+
166
+ ### Invalidating a Single Cached Route
167
+
168
+ `clear_path`/`clear_pattern` work on ranges of keys. To drop exactly the entry a
169
+ `@cache`-decorated route would use — typically right after a mutation — call
170
+ `invalidate()`, which rebuilds that route's key with the same key builder and
171
+ deletes it:
172
+
173
+ ```python
174
+ from fastapi import Request
175
+ from starlette.requests import Request as StarletteRequest
176
+
177
+ from fastapi_cachex import cache, invalidate
178
+
179
+
180
+ @app.get("/items/{item_id}")
181
+ @cache(ttl=300)
182
+ async def read_item(item_id: int):
183
+ return await load(item_id)
184
+
185
+
186
+ @app.post("/items/{item_id}")
187
+ async def update_item(item_id: int, request: Request):
188
+ await save(item_id)
189
+ # Build the key the cached GET would have used: same host and headers,
190
+ # GET method, the cached path, no query string.
191
+ scope = dict(request.scope)
192
+ scope["method"] = "GET"
193
+ scope["path"] = f"/items/{item_id}"
194
+ scope["query_string"] = b""
195
+ return {"invalidated": await invalidate(StarletteRequest(scope))}
149
196
  ```
150
197
 
198
+ `invalidate(request, key_builder=None)` returns `True` when an entry existed and
199
+ was removed, `False` otherwise (including when no backend is configured — it
200
+ never raises). The request you hand it must produce the cached route's key:
201
+ same method, host, path and query string. If the cached route uses a custom
202
+ `key_builder`, pass the same one here, or the key will not match.
203
+
204
+ ### Cache Monitoring Routes
205
+
206
+ `add_routes()` mounts two read-only endpoints that report what is currently in
207
+ the backend:
208
+
209
+ ```python
210
+ from fastapi import Depends, FastAPI
211
+ from fastapi_cachex import add_routes
212
+
213
+ app = FastAPI()
214
+ add_routes(
215
+ app,
216
+ prefix="/admin/cache", # default "" -> /cached-hits, /cached-records
217
+ include_in_schema=False, # default: hidden from OpenAPI
218
+ dependencies=[Depends(verify_admin)],
219
+ )
220
+ ```
221
+
222
+ - `GET {prefix}/cached-hits` — per-route hit counts and cache key information.
223
+ - `GET {prefix}/cached-records` — every cached record with its size, expiry and
224
+ a preview of the cached content.
225
+
226
+ > [!WARNING]
227
+ > **These routes have no authentication of their own.** `include_in_schema=False`
228
+ > only hides them from the OpenAPI document; anyone who guesses the path can read
229
+ > them. `/cached-records` includes a preview of the cached content and exposes
230
+ > your whole route structure. In production always pass
231
+ > `dependencies=[Depends(your_auth)]`, or mount them on an internal-only app.
232
+
233
+ > [!NOTE]
234
+ > The `ttl_remaining` field is not available on the Redis backend.
235
+ > `AsyncRedisCacheBackend.get_cache_data()` does not issue a per-key `TTL`
236
+ > lookup, so Redis-backed entries are reported as never expiring. Expiry itself
237
+ > still happens — only the monitoring view is blind to it.
238
+
151
239
  ### Application-Level Caching (Manual Get/Set)
152
240
 
153
241
  Beyond HTTP response caching via `@cache`, you can cache arbitrary JSON-serializable
@@ -157,6 +245,7 @@ namespaced wrapper around whichever backend is configured via `BackendProxy`.
157
245
  ```python
158
246
  from fastapi_cachex import AppCache, CacheManager
159
247
 
248
+
160
249
  @app.get("/expensive")
161
250
  async def expensive_operation(cache: AppCache):
162
251
  result = await cache.get("expensive:result")
@@ -172,16 +261,25 @@ await manager.set("user:42", {"name": "Alice"})
172
261
  user = await manager.get("user:42") # {"name": "Alice"}
173
262
  await manager.delete("user:42")
174
263
  await manager.clear_prefix() # clear everything under "myapp:"
264
+
265
+ # Compute-on-miss: `factory` runs only when the key is missing, expired, or
266
+ # undecodable. It may be sync or async.
267
+ profile = await manager.get_or_set("user:42", lambda: load_user(42), ttl=300)
268
+
269
+ # Glob over this manager's namespace, using the backend's native pattern
270
+ # support (Redis SCAN) rather than enumerating every key.
271
+ await manager.clear_pattern("user:*") # matches "myapp:user:*"
175
272
  ```
176
273
 
177
- `CacheManager.get()` returns `None` (or a supplied `default=`) on a cache miss —
274
+ `get_or_set()` provides no stampede protection: concurrent misses for the same
275
+ key each run `factory`. `CacheManager.get()` returns `None` (or a supplied `default=`) on a cache miss —
178
276
  it never raises for missing or corrupted entries. `CacheManager` keys live under
179
277
  their own `cache:`-prefixed namespace by default, separate from the HTTP route
180
278
  cache and OAuth state, so `clear()`/`clear_prefix()` never touch unrelated cache
181
279
  entries.
182
280
 
183
281
  **Note**: `clear()`/`clear_prefix()` are implemented via the backend's
184
- `get_all_keys()`. Since Memcached doesn't support key enumeration (see
282
+ `get_all_keys()` and `delete_many()` (one batched `DEL` on Redis). Since Memcached doesn't support key enumeration (see
185
283
  [Memcached limitations](#memcached)), these two methods are no-ops on a
186
284
  Memcached backend — `get()`/`set()`/`delete()`/`has()` work normally. Use
187
285
  Redis or the in-memory backend if you need bulk clearing.
@@ -204,19 +302,126 @@ This ensures that:
204
302
  - Different query parameters get separate cache entries
205
303
  - The same endpoint with different parameters can be cached independently
206
304
 
305
+ Query parameters are taken in the order the client sent them, without sorting, so
306
+ `?a=1&b=2` and `?b=2&a=1` are two distinct cache entries for the same logical request.
307
+
207
308
  All backends automatically namespace keys with a prefix (e.g., `fastapi_cachex:`) to avoid conflicts with other applications.
208
309
 
209
310
  `CacheManager` (see [Application-Level Caching](#application-level-caching-manual-getset)) uses a separate, simpler `cache:`-prefixed key namespace instead of this `|||`-separated format, since its keys aren't tied to HTTP requests.
210
311
 
312
+ > [!WARNING]
313
+ > **The default cache key carries no user identity.** The backend is shared by
314
+ > every worker and every caller, so caching an authenticated endpoint with the
315
+ > default key builder will serve one user's response to the next user who hits
316
+ > the same path.
317
+ >
318
+ > For any endpoint whose response depends on who is asking, do one of:
319
+ >
320
+ > 1. **`private=True`** — the response is never read from or written to the
321
+ > shared backend. `Cache-Control: private` still lets the user's own browser
322
+ > cache it, and `If-None-Match` revalidation still works against freshly
323
+ > rendered content.
324
+ > 2. **A key builder that includes the caller's identity** — use this when you
325
+ > do want a server-side cache per user.
326
+
327
+ ```python
328
+ from fastapi_cachex import cache
329
+ from fastapi_cachex.types import CACHE_KEY_SEPARATOR
330
+
331
+
332
+ # 1. Keep it out of the shared cache entirely.
333
+ @app.get("/me/profile")
334
+ @cache(ttl=60, private=True)
335
+ async def my_profile(user: CurrentUser):
336
+ return user.profile
337
+
338
+
339
+ # 2. Or give each user their own entry.
340
+ def per_user_key(request: Request) -> str:
341
+ # `request.state.user_id` is populated by your authentication layer after
342
+ # it has verified the caller — never read the identity straight off an
343
+ # unverified request header (see the note below).
344
+ user_id = getattr(request.state, "user_id", "anonymous")
345
+ return (
346
+ f"{request.method}{CACHE_KEY_SEPARATOR}"
347
+ f"{request.headers.get('host', 'unknown')}{CACHE_KEY_SEPARATOR}"
348
+ f"{request.url.path}{CACHE_KEY_SEPARATOR}"
349
+ f"{request.query_params}{CACHE_KEY_SEPARATOR}{user_id}"
350
+ )
351
+
352
+
353
+ @app.get("/me/dashboard")
354
+ @cache(ttl=60, private=True, key_builder=per_user_key)
355
+ async def my_dashboard(user: CurrentUser):
356
+ return build_dashboard(user)
357
+ ```
358
+
359
+ > [!CAUTION]
360
+ > The key builder decides who sees whose data, so the identity it reads must
361
+ > come from something already verified — a claim from a checked token, a user
362
+ > your dependency resolved, or a value your auth middleware wrote to
363
+ > `request.state`.
364
+ >
365
+ > ```python
366
+ > # ❌ Never do this: anyone can send this header.
367
+ > user_id = request.headers.get("x-user-id", "anonymous")
368
+ > ```
369
+ >
370
+ > A key built from a raw request header is a horizontal privilege escalation:
371
+ > sending `X-User-Id: <someone-else>` returns that user's cached response.
372
+
211
373
  ### Cache Hit Behavior
212
374
 
213
375
  When a cached entry is valid (within TTL):
214
- - **Default behavior**: Returns the cached content with HTTP 200 status code directly without re-executing the endpoint handler
376
+ - **Default behavior**: Returns the cached content directly, with the status code and headers the handler originally produced, without re-executing the endpoint handler
215
377
  - **With `If-None-Match` header**: Returns HTTP 304 Not Modified if the ETag matches
216
378
  - **With `no-cache` directive**: Forces revalidation with fresh content before deciding on 304
379
+ - **With `private=True`**: Nothing is read from or written to the shared backend; the handler runs every time and only `If-None-Match` revalidation applies
217
380
 
218
381
  This means **cached hits are extremely fast** - the endpoint handler function is never executed.
219
382
 
383
+ Only successful responses are stored. A response the handler *returns* with a
384
+ non-2xx status (for example `Response(..., status_code=404)`) is passed straight
385
+ through and never cached, so a transient error cannot replace or poison the last
386
+ good entry. `206 Partial Content` is excluded as well, since its body is only
387
+ meaningful for the `Range` request that produced it. `Set-Cookie` is never
388
+ stored or replayed.
389
+
390
+ ### Atomic backend primitives
391
+
392
+ Every backend exposes two atomic operations on top of `get`/`set`/`delete`, for
393
+ values that are read and written by many concurrent requests:
394
+
395
+ ```python
396
+ from fastapi_cachex import BackendProxy
397
+
398
+ backend = BackendProxy.get()
399
+
400
+ # Fixed-window counter: created on first use, `ttl` applies only then.
401
+ hits = await backend.increment(f"resend:{user_id}", ttl=86400)
402
+ if hits > 3:
403
+ raise TooManyRequests()
404
+
405
+ # One-shot value: of several concurrent callers exactly one gets the entry.
406
+ grant = await backend.get_and_delete(f"grant:{token}")
407
+ ```
408
+
409
+ - `increment(key, delta=1, ttl=None) -> int` — Memory does the read-modify-write
410
+ under its lock, Redis runs a Lua script (`EXISTS` + `INCRBY` + `EXPIRE`) and
411
+ Memcached uses `ADD` + `INCR`/`DECR` (Memcached counters stop at 0). The
412
+ counter is visible through `get()` as a `CacheEntry` with fingerprint
413
+ `COUNTER_FINGERPRINT` and the decimal value as content, so `delete`/`clear*`
414
+ and the monitoring routes treat it like any other entry. Incrementing a key
415
+ that holds a cached response raises `CacheXError`.
416
+ - `get_and_delete(key) -> CacheEntry | None` — Memory pops under its lock, Redis
417
+ uses `GETDEL` (server 6.2+) and Memcached returns the value only when its own
418
+ `DELETE` won. `StateManager.consume_state`, `CacheManager.delete` and
419
+ `invalidate()` are built on it.
420
+
421
+ Both have a non-atomic fallback on `BaseCacheBackend`, so a third-party backend
422
+ that only implements the abstract methods keeps working; override them to get
423
+ real atomicity.
424
+
220
425
  ### In-Memory Cache (default)
221
426
 
222
427
  If you don't specify a backend, FastAPI-CacheX will use the in-memory cache by default.
@@ -249,6 +454,11 @@ BackendProxy.set(backend)
249
454
  - Keys are namespaced with `fastapi_cachex:` prefix to avoid conflicts
250
455
  - Consider using Redis backend if you need pattern-based cache clearing
251
456
 
457
+ The synchronous pymemcache client runs in worker threads and is connection-pooled,
458
+ so concurrent requests never share a socket. Writes wait for the server's
459
+ acknowledgement (`default_noreply=False`), which keeps a value readable from
460
+ any pooled connection as soon as `set()` returns.
461
+
252
462
  ### Redis
253
463
 
254
464
  ```python
@@ -277,6 +487,33 @@ backend = AsyncRedisCacheBackend(
277
487
  BackendProxy.set(backend)
278
488
  ```
279
489
 
490
+ **Configuring from a model**: `RedisConfig` is a pydantic model with the same
491
+ settings and validation, which is handy when they come from environment
492
+ variables or a settings file:
493
+
494
+ ```python
495
+ from fastapi_cachex.backends import AsyncRedisCacheBackend
496
+ from fastapi_cachex.backends.config import RedisConfig
497
+
498
+ config = RedisConfig(
499
+ host="127.0.0.1",
500
+ port=6379,
501
+ password=None, # SecretStr | None
502
+ db=0,
503
+ encoding="utf-8", # how the client decodes server responses
504
+ socket_timeout=1.0, # seconds; applies to reads/writes
505
+ socket_connect_timeout=1.0,
506
+ key_prefix="fastapi_cachex:",
507
+ protocol=2, # RESP version, 2 or 3
508
+ )
509
+ backend = AsyncRedisCacheBackend.load_from_config(config)
510
+ BackendProxy.set(backend)
511
+ ```
512
+
513
+ Keep `protocol=2` unless you need RESP3 features *and* your `hiredis` build
514
+ supports it (RESP3 needs hiredis >= 3.0). Redis 8.0 speaks RESP3, but an older
515
+ hiredis will fail to negotiate it.
516
+
280
517
  ## Performance Considerations
281
518
 
282
519
  ### Cache Hit Performance
@@ -303,8 +540,12 @@ async def expensive_operation():
303
540
 
304
541
  - [Cache Flow Explanation](docs/CACHE_FLOW.md)
305
542
  - [Development Guide](docs/DEVELOPMENT.md)
543
+ - [Known Limitations and Planned Work](docs/BACKLOG.md)
544
+ - [Changelog](CHANGELOG.md)
306
545
  - [Contributing Guidelines](docs/CONTRIBUTING.md)
307
546
  - [Session Management Guide](docs/SESSION.md) - Complete guide for session features
547
+ - [State Management Guide](docs/STATE.md) - One-shot OAuth/CSRF state tokens
548
+ - [JWT Claims Guide](docs/JWT_CLAIMS.md) - Claim design and extension points for JWT session tokens
308
549
 
309
550
  ## License
310
551