devora-python 0.1.0__tar.gz → 0.1.1__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 (23) hide show
  1. devora_python-0.1.1/PKG-INFO +167 -0
  2. devora_python-0.1.1/README.md +144 -0
  3. {devora_python-0.1.0 → devora_python-0.1.1}/pyproject.toml +1 -1
  4. {devora_python-0.1.0 → devora_python-0.1.1}/src/devora_sdk/constants.py +1 -1
  5. devora_python-0.1.0/PKG-INFO +0 -132
  6. devora_python-0.1.0/README.md +0 -109
  7. {devora_python-0.1.0 → devora_python-0.1.1}/.gitignore +0 -0
  8. {devora_python-0.1.0 → devora_python-0.1.1}/LICENSE +0 -0
  9. {devora_python-0.1.0 → devora_python-0.1.1}/src/devora_sdk/__init__.py +0 -0
  10. {devora_python-0.1.0 → devora_python-0.1.1}/src/devora_sdk/api_url_gen.py +0 -0
  11. {devora_python-0.1.0 → devora_python-0.1.1}/src/devora_sdk/browser_session.py +0 -0
  12. {devora_python-0.1.0 → devora_python-0.1.1}/src/devora_sdk/guard.py +0 -0
  13. {devora_python-0.1.0 → devora_python-0.1.1}/src/devora_sdk/handler.py +0 -0
  14. {devora_python-0.1.0 → devora_python-0.1.1}/src/devora_sdk/hmac.py +0 -0
  15. {devora_python-0.1.0 → devora_python-0.1.1}/src/devora_sdk/models.py +0 -0
  16. {devora_python-0.1.0 → devora_python-0.1.1}/src/devora_sdk/policy.py +0 -0
  17. {devora_python-0.1.0 → devora_python-0.1.1}/src/devora_sdk/py.typed +0 -0
  18. {devora_python-0.1.0 → devora_python-0.1.1}/src/devora_sdk/replay.py +0 -0
  19. {devora_python-0.1.0 → devora_python-0.1.1}/src/devora_sdk/sdk.py +0 -0
  20. {devora_python-0.1.0 → devora_python-0.1.1}/src/devora_sdk/security.py +0 -0
  21. {devora_python-0.1.0 → devora_python-0.1.1}/src/devora_sdk/signing.py +0 -0
  22. {devora_python-0.1.0 → devora_python-0.1.1}/src/devora_sdk/transport.py +0 -0
  23. {devora_python-0.1.0 → devora_python-0.1.1}/src/devora_sdk/utils.py +0 -0
@@ -0,0 +1,167 @@
1
+ Metadata-Version: 2.5
2
+ Name: devora-python
3
+ Version: 0.1.1
4
+ Summary: Devora Python backend SDK for secure impersonation
5
+ Project-URL: Documentation, https://docs.devora.sh
6
+ Project-URL: Repository, https://github.com/getdevora/devora-sdks
7
+ Project-URL: Issues, https://github.com/getdevora/devora-sdks/issues
8
+ Author: Devora
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: backend,devora,impersonation,sdk,security
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: <4.0,>=3.10
22
+ Description-Content-Type: text/markdown
23
+
24
+ # devora-python
25
+
26
+ Recording, masking and capture policy are configured in the Devora dashboard and
27
+ authorized server-side for each session. This backend SDK takes no capture
28
+ settings, and `devora_sdk()` ignores keyword arguments it does not recognize,
29
+ so check option names carefully. See
30
+ [capture settings](https://github.com/getdevora/devora-sdks/blob/main/SETTINGS.md).
31
+
32
+ Python backend core SDK for Devora customer integrations.
33
+
34
+ ## Requirements
35
+
36
+ - Python `>=3.10,<4.0`
37
+ - In production, a replay store shared by every worker (the example below uses
38
+ Redis 6.2 or newer, for `SET ... PXAT`)
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ pip install devora-python redis
44
+ ```
45
+
46
+ ## Quick Start
47
+
48
+ ```python
49
+ import os
50
+ from dataclasses import asdict
51
+
52
+ import redis
53
+ from devora_sdk import DEVORA_ENDPOINTS, devora_sdk
54
+
55
+ # Replay protection shared by every worker and instance (required in production).
56
+ redis_client = redis.Redis.from_url(os.environ["REDIS_URL"])
57
+
58
+
59
+ class RedisReplayStore:
60
+ def consume(self, namespace: str, request_id: str, expires_at: int) -> bool:
61
+ # Atomic insert-if-absent kept until expires_at; an exception makes the SDK fail closed (503).
62
+ key = f"devora:replay:{namespace}:{request_id}"
63
+ return bool(redis_client.set(key, b"1", nx=True, pxat=expires_at))
64
+
65
+
66
+ sdk = devora_sdk(
67
+ api_key=os.environ["DEVORA_API_KEY"], # pk_server_live_...
68
+ secret_key=os.environ["DEVORA_SECRET_KEY"], # sk_server_live_...
69
+ org_id=os.environ["DEVORA_ORG_ID"],
70
+ replay_store=RedisReplayStore(),
71
+ )
72
+
73
+
74
+ @sdk.register(DEVORA_ENDPOINTS.USER_SEARCH)
75
+ def search_users(req):
76
+ return {"users": search_customer_users(req.query.get("term", ""))}
77
+
78
+
79
+ @sdk.register(DEVORA_ENDPOINTS.IMPERSONATE)
80
+ def impersonate(req):
81
+ ctx = req.devora_context
82
+ if not ctx:
83
+ raise ValueError("Missing impersonation context")
84
+ # Store the whole verified context in the token: the scope guard reads every
85
+ # field of it back. Expire the token no later than ctx.expires_at.
86
+ devora = {**asdict(ctx), "is_impersonation": True}
87
+ token = create_customer_token(ctx.target_user["id"], devora)
88
+ return {"token": token}
89
+ ```
90
+
91
+ Register every handler, then mount the SDK with the
92
+ [Django](https://pypi.org/project/devora-django/) or
93
+ [FastAPI](https://pypi.org/project/devora-fastapi/) adapter. For any other
94
+ framework, pass the request exactly as received to `process_request` (sync) or
95
+ `await async_process_request` (async):
96
+
97
+ ```python
98
+ from devora_sdk import AdapterRequest, process_request
99
+ from devora_sdk.utils import get_error_status_code
100
+
101
+
102
+ def handle_devora(method: str, path: str, query: str, body: bytes, headers) -> tuple[int, dict]:
103
+ # path: still percent-encoded and relative to your Devora mount, e.g. "/user/search"
104
+ # query: the raw query string without "?"; body: the raw bytes (cap the read yourself)
105
+ # headers: a mapping, or (name, value) pairs so duplicate headers stay visible
106
+ result = process_request(sdk, sdk.get_routes(), AdapterRequest(method, path, query, body, headers))
107
+ status = 200 if result["success"] else get_error_status_code(result.get("errorCode"))
108
+ return status, result # send as JSON with Cache-Control: private, no-store
109
+ ```
110
+
111
+ `async_process_request` runs signature verification, the replay store and
112
+ synchronous handlers in a worker thread, so it never blocks the event loop.
113
+ `process_request` cannot run `async def` handlers.
114
+
115
+ ### Production replay store
116
+
117
+ Every signed request carries a single-use id. In production the SDK needs a
118
+ shared, atomic `replay_store` so a captured request cannot be replayed against
119
+ another worker or instance. The environment comes from `environment=`, else
120
+ `DEVORA_ENV`, else `NODE_ENV`. Anything other than `development` or `test`
121
+ (including unset) is production, and production refuses to start without a
122
+ `replay_store`. Any store whose `consume` is an atomic insert-if-absent works;
123
+ see the
124
+ [replay-store contract](https://github.com/getdevora/devora-sdks/blob/main/SIGNING.md#replay-store).
125
+
126
+ **Local development only:** to run without Redis, omit `replay_store` and set
127
+ `DEVORA_ENV=development` (or pass `environment="development"`). The SDK then
128
+ keeps request ids in memory, which protects a single process only. Never use
129
+ this in production.
130
+
131
+ `consume` must be a regular method returning `True` or `False`; the SDK calls
132
+ it synchronously, including from `async_process_request` (in a worker thread).
133
+ Use a synchronous client such as `redis.Redis`: an `async def consume` fails
134
+ closed with a 503. Keep it a single short round trip, and do not let the store
135
+ evict these keys before they expire. A unique-key database insert with an
136
+ expiry column works too.
137
+
138
+ The guard's session-liveness checker caches at most 1,024 results and permits at
139
+ most 64 distinct concurrent lookups per checker. Requests for the same session
140
+ share a lookup. Live/ended results use the configured TTL (five seconds by
141
+ default); unavailable results use at most one second. Cache hits never extend a
142
+ verdict's lifetime. Capacity exhaustion follows the configured unavailable
143
+ policy, which denies access by default.
144
+
145
+ ### Cold-start latency
146
+
147
+ The impersonation guard's scope policy is fetched lazily on first use, so the
148
+ first request handled by a freshly started worker process pays for that fetch
149
+ synchronously (and any request racing it gets a `503
150
+ IMPERSONATION_POLICY_UNAVAILABLE` rather than waiting). If your deployment
151
+ runs multiple worker processes (gunicorn, uWSGI, etc.), pass
152
+ `prefetch_scope_config=True` to start that fetch in a background thread as soon
153
+ as `devora_sdk(...)` is constructed, so the cache is usually warm by the first
154
+ request:
155
+
156
+ ```python
157
+ sdk = devora_sdk(
158
+ api_key=os.environ["DEVORA_API_KEY"],
159
+ secret_key=os.environ["DEVORA_SECRET_KEY"],
160
+ org_id=os.environ["DEVORA_ORG_ID"],
161
+ replay_store=RedisReplayStore(),
162
+ prefetch_scope_config=True,
163
+ )
164
+ ```
165
+
166
+ Options, error codes and the guard are documented in the
167
+ [Python reference](https://docs.devora.sh/reference/python).
@@ -0,0 +1,144 @@
1
+ # devora-python
2
+
3
+ Recording, masking and capture policy are configured in the Devora dashboard and
4
+ authorized server-side for each session. This backend SDK takes no capture
5
+ settings, and `devora_sdk()` ignores keyword arguments it does not recognize,
6
+ so check option names carefully. See
7
+ [capture settings](https://github.com/getdevora/devora-sdks/blob/main/SETTINGS.md).
8
+
9
+ Python backend core SDK for Devora customer integrations.
10
+
11
+ ## Requirements
12
+
13
+ - Python `>=3.10,<4.0`
14
+ - In production, a replay store shared by every worker (the example below uses
15
+ Redis 6.2 or newer, for `SET ... PXAT`)
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ pip install devora-python redis
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ ```python
26
+ import os
27
+ from dataclasses import asdict
28
+
29
+ import redis
30
+ from devora_sdk import DEVORA_ENDPOINTS, devora_sdk
31
+
32
+ # Replay protection shared by every worker and instance (required in production).
33
+ redis_client = redis.Redis.from_url(os.environ["REDIS_URL"])
34
+
35
+
36
+ class RedisReplayStore:
37
+ def consume(self, namespace: str, request_id: str, expires_at: int) -> bool:
38
+ # Atomic insert-if-absent kept until expires_at; an exception makes the SDK fail closed (503).
39
+ key = f"devora:replay:{namespace}:{request_id}"
40
+ return bool(redis_client.set(key, b"1", nx=True, pxat=expires_at))
41
+
42
+
43
+ sdk = devora_sdk(
44
+ api_key=os.environ["DEVORA_API_KEY"], # pk_server_live_...
45
+ secret_key=os.environ["DEVORA_SECRET_KEY"], # sk_server_live_...
46
+ org_id=os.environ["DEVORA_ORG_ID"],
47
+ replay_store=RedisReplayStore(),
48
+ )
49
+
50
+
51
+ @sdk.register(DEVORA_ENDPOINTS.USER_SEARCH)
52
+ def search_users(req):
53
+ return {"users": search_customer_users(req.query.get("term", ""))}
54
+
55
+
56
+ @sdk.register(DEVORA_ENDPOINTS.IMPERSONATE)
57
+ def impersonate(req):
58
+ ctx = req.devora_context
59
+ if not ctx:
60
+ raise ValueError("Missing impersonation context")
61
+ # Store the whole verified context in the token: the scope guard reads every
62
+ # field of it back. Expire the token no later than ctx.expires_at.
63
+ devora = {**asdict(ctx), "is_impersonation": True}
64
+ token = create_customer_token(ctx.target_user["id"], devora)
65
+ return {"token": token}
66
+ ```
67
+
68
+ Register every handler, then mount the SDK with the
69
+ [Django](https://pypi.org/project/devora-django/) or
70
+ [FastAPI](https://pypi.org/project/devora-fastapi/) adapter. For any other
71
+ framework, pass the request exactly as received to `process_request` (sync) or
72
+ `await async_process_request` (async):
73
+
74
+ ```python
75
+ from devora_sdk import AdapterRequest, process_request
76
+ from devora_sdk.utils import get_error_status_code
77
+
78
+
79
+ def handle_devora(method: str, path: str, query: str, body: bytes, headers) -> tuple[int, dict]:
80
+ # path: still percent-encoded and relative to your Devora mount, e.g. "/user/search"
81
+ # query: the raw query string without "?"; body: the raw bytes (cap the read yourself)
82
+ # headers: a mapping, or (name, value) pairs so duplicate headers stay visible
83
+ result = process_request(sdk, sdk.get_routes(), AdapterRequest(method, path, query, body, headers))
84
+ status = 200 if result["success"] else get_error_status_code(result.get("errorCode"))
85
+ return status, result # send as JSON with Cache-Control: private, no-store
86
+ ```
87
+
88
+ `async_process_request` runs signature verification, the replay store and
89
+ synchronous handlers in a worker thread, so it never blocks the event loop.
90
+ `process_request` cannot run `async def` handlers.
91
+
92
+ ### Production replay store
93
+
94
+ Every signed request carries a single-use id. In production the SDK needs a
95
+ shared, atomic `replay_store` so a captured request cannot be replayed against
96
+ another worker or instance. The environment comes from `environment=`, else
97
+ `DEVORA_ENV`, else `NODE_ENV`. Anything other than `development` or `test`
98
+ (including unset) is production, and production refuses to start without a
99
+ `replay_store`. Any store whose `consume` is an atomic insert-if-absent works;
100
+ see the
101
+ [replay-store contract](https://github.com/getdevora/devora-sdks/blob/main/SIGNING.md#replay-store).
102
+
103
+ **Local development only:** to run without Redis, omit `replay_store` and set
104
+ `DEVORA_ENV=development` (or pass `environment="development"`). The SDK then
105
+ keeps request ids in memory, which protects a single process only. Never use
106
+ this in production.
107
+
108
+ `consume` must be a regular method returning `True` or `False`; the SDK calls
109
+ it synchronously, including from `async_process_request` (in a worker thread).
110
+ Use a synchronous client such as `redis.Redis`: an `async def consume` fails
111
+ closed with a 503. Keep it a single short round trip, and do not let the store
112
+ evict these keys before they expire. A unique-key database insert with an
113
+ expiry column works too.
114
+
115
+ The guard's session-liveness checker caches at most 1,024 results and permits at
116
+ most 64 distinct concurrent lookups per checker. Requests for the same session
117
+ share a lookup. Live/ended results use the configured TTL (five seconds by
118
+ default); unavailable results use at most one second. Cache hits never extend a
119
+ verdict's lifetime. Capacity exhaustion follows the configured unavailable
120
+ policy, which denies access by default.
121
+
122
+ ### Cold-start latency
123
+
124
+ The impersonation guard's scope policy is fetched lazily on first use, so the
125
+ first request handled by a freshly started worker process pays for that fetch
126
+ synchronously (and any request racing it gets a `503
127
+ IMPERSONATION_POLICY_UNAVAILABLE` rather than waiting). If your deployment
128
+ runs multiple worker processes (gunicorn, uWSGI, etc.), pass
129
+ `prefetch_scope_config=True` to start that fetch in a background thread as soon
130
+ as `devora_sdk(...)` is constructed, so the cache is usually warm by the first
131
+ request:
132
+
133
+ ```python
134
+ sdk = devora_sdk(
135
+ api_key=os.environ["DEVORA_API_KEY"],
136
+ secret_key=os.environ["DEVORA_SECRET_KEY"],
137
+ org_id=os.environ["DEVORA_ORG_ID"],
138
+ replay_store=RedisReplayStore(),
139
+ prefetch_scope_config=True,
140
+ )
141
+ ```
142
+
143
+ Options, error codes and the guard are documented in the
144
+ [Python reference](https://docs.devora.sh/reference/python).
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "devora-python"
7
- version = "0.1.0"
7
+ version = "0.1.1"
8
8
  description = "Devora Python backend SDK for secure impersonation"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10,<4.0"
@@ -34,7 +34,7 @@ class SECURITY_HEADERS:
34
34
  SESSION_ID = "x-devora-session-id"
35
35
 
36
36
 
37
- SDK_VERSION = "0.1.0"
37
+ SDK_VERSION = "0.1.1"
38
38
  DEFAULT_API_URL = DEVORA_API_ORIGIN
39
39
  DEFAULT_TIMESTAMP_TOLERANCE_SECONDS = 300
40
40
  DEFAULT_MAX_BODY_SIZE_BYTES = 1024 * 1024
@@ -1,132 +0,0 @@
1
- Metadata-Version: 2.5
2
- Name: devora-python
3
- Version: 0.1.0
4
- Summary: Devora Python backend SDK for secure impersonation
5
- Project-URL: Documentation, https://docs.devora.sh
6
- Project-URL: Repository, https://github.com/getdevora/devora-sdks
7
- Project-URL: Issues, https://github.com/getdevora/devora-sdks/issues
8
- Author: Devora
9
- License-Expression: MIT
10
- License-File: LICENSE
11
- Keywords: backend,devora,impersonation,sdk,security
12
- Classifier: Development Status :: 5 - Production/Stable
13
- Classifier: Intended Audience :: Developers
14
- Classifier: Programming Language :: Python :: 3
15
- Classifier: Programming Language :: Python :: 3.10
16
- Classifier: Programming Language :: Python :: 3.11
17
- Classifier: Programming Language :: Python :: 3.12
18
- Classifier: Programming Language :: Python :: 3.13
19
- Classifier: Programming Language :: Python :: 3.14
20
- Classifier: Typing :: Typed
21
- Requires-Python: <4.0,>=3.10
22
- Description-Content-Type: text/markdown
23
-
24
- # devora-python
25
-
26
- Recording, masking and activity preferences are configured in Devora Settings.
27
- SDK initialization overrides are ignored. New sessions retain the server's policy
28
- snapshot across exchange and resume. Developer privacy labels take effect only
29
- when selected in Settings; sensitive-field protection remains mandatory.
30
- See [migration details](https://github.com/getdevora/devora-sdks/blob/main/SETTINGS.md).
31
-
32
- Python backend core SDK for Devora customer integrations.
33
-
34
- ## Requirements
35
-
36
- - Python `>=3.10,<4.0`
37
-
38
- ## Install
39
-
40
- ```bash
41
- pip install devora-python
42
- ```
43
-
44
- ## Quick Start
45
-
46
- ```python
47
- from devora_sdk import DEVORA_ENDPOINTS, devora_sdk
48
-
49
- sdk = devora_sdk(
50
- api_key="pk_server_live_...",
51
- secret_key="sk_server_live_...",
52
- org_id="org_...",
53
- )
54
-
55
- @sdk.register(DEVORA_ENDPOINTS.USER_SEARCH)
56
- def search_users(req):
57
- return {"users": search_customer_users(req.query.get("term", ""))}
58
-
59
- @sdk.register(DEVORA_ENDPOINTS.IMPERSONATE)
60
- def impersonate(req):
61
- context = req.devora_context
62
- token = create_customer_token(context.target_user["id"], context)
63
- return {"token": token}
64
- ```
65
-
66
- Use `process_request()` for sync frameworks, `async_process_request()` for
67
- async frameworks, or use the Django and FastAPI adapter packages.
68
-
69
- ### Production replay store
70
-
71
- Every signed request carries a single-use id. In production the SDK needs a
72
- shared, atomic `replay_store` so a captured request cannot be replayed against
73
- another worker or instance; the in-memory store is for
74
- `environment="development"` or `"test"` only, and any other environment refuses
75
- to start without one.
76
-
77
- ```python
78
- import os
79
-
80
- import redis
81
- from devora_sdk import devora_sdk
82
-
83
- client = redis.Redis.from_url(os.environ["REDIS_URL"])
84
-
85
-
86
- class RedisReplayStore:
87
- def consume(self, namespace: str, request_id: str, expires_at: int) -> bool:
88
- # Atomic insert-if-absent that lives until expires_at (Unix ms).
89
- # Exceptions propagate: the SDK then fails closed with a 503.
90
- return bool(
91
- client.set(f"devora:replay:{namespace}:{request_id}", b"1", nx=True, pxat=expires_at)
92
- )
93
-
94
-
95
- sdk = devora_sdk(
96
- api_key=os.environ["DEVORA_API_KEY"],
97
- secret_key=os.environ["DEVORA_SECRET_KEY"],
98
- org_id=os.environ["DEVORA_ORG_ID"],
99
- replay_store=RedisReplayStore(),
100
- )
101
- ```
102
-
103
- `consume` is called synchronously, including from `async_process_request`;
104
- keep it a single short round trip. Do not let the store evict these keys
105
- before they expire. A unique-key database insert with an expiry column works
106
- too.
107
-
108
- The guard's session-liveness checker caches at most 1,024 results and permits at
109
- most 64 distinct concurrent lookups per checker. Requests for the same session
110
- share a lookup. Live/ended results use the configured TTL (five seconds by
111
- default); unavailable results use at most one second. Cache hits never extend a
112
- verdict's lifetime. Capacity exhaustion follows the configured unavailable
113
- policy, which denies access by default.
114
-
115
- ### Cold-start latency
116
-
117
- The impersonation guard's scope policy is fetched lazily on first use, so the
118
- first request handled by a freshly started worker process pays for that fetch
119
- synchronously (and any request racing it gets a `503
120
- IMPERSONATION_POLICY_UNAVAILABLE` rather than waiting). If your deployment
121
- runs multiple worker processes (gunicorn, uWSGI, etc.), pass
122
- `prefetch_scope_config=True` to warm the cache in the background as soon as
123
- `devora_sdk(...)` is constructed, before the process starts serving traffic:
124
-
125
- ```python
126
- sdk = devora_sdk(
127
- api_key="pk_server_live_...",
128
- secret_key="sk_server_live_...",
129
- org_id="org_...",
130
- prefetch_scope_config=True,
131
- )
132
- ```
@@ -1,109 +0,0 @@
1
- # devora-python
2
-
3
- Recording, masking and activity preferences are configured in Devora Settings.
4
- SDK initialization overrides are ignored. New sessions retain the server's policy
5
- snapshot across exchange and resume. Developer privacy labels take effect only
6
- when selected in Settings; sensitive-field protection remains mandatory.
7
- See [migration details](https://github.com/getdevora/devora-sdks/blob/main/SETTINGS.md).
8
-
9
- Python backend core SDK for Devora customer integrations.
10
-
11
- ## Requirements
12
-
13
- - Python `>=3.10,<4.0`
14
-
15
- ## Install
16
-
17
- ```bash
18
- pip install devora-python
19
- ```
20
-
21
- ## Quick Start
22
-
23
- ```python
24
- from devora_sdk import DEVORA_ENDPOINTS, devora_sdk
25
-
26
- sdk = devora_sdk(
27
- api_key="pk_server_live_...",
28
- secret_key="sk_server_live_...",
29
- org_id="org_...",
30
- )
31
-
32
- @sdk.register(DEVORA_ENDPOINTS.USER_SEARCH)
33
- def search_users(req):
34
- return {"users": search_customer_users(req.query.get("term", ""))}
35
-
36
- @sdk.register(DEVORA_ENDPOINTS.IMPERSONATE)
37
- def impersonate(req):
38
- context = req.devora_context
39
- token = create_customer_token(context.target_user["id"], context)
40
- return {"token": token}
41
- ```
42
-
43
- Use `process_request()` for sync frameworks, `async_process_request()` for
44
- async frameworks, or use the Django and FastAPI adapter packages.
45
-
46
- ### Production replay store
47
-
48
- Every signed request carries a single-use id. In production the SDK needs a
49
- shared, atomic `replay_store` so a captured request cannot be replayed against
50
- another worker or instance; the in-memory store is for
51
- `environment="development"` or `"test"` only, and any other environment refuses
52
- to start without one.
53
-
54
- ```python
55
- import os
56
-
57
- import redis
58
- from devora_sdk import devora_sdk
59
-
60
- client = redis.Redis.from_url(os.environ["REDIS_URL"])
61
-
62
-
63
- class RedisReplayStore:
64
- def consume(self, namespace: str, request_id: str, expires_at: int) -> bool:
65
- # Atomic insert-if-absent that lives until expires_at (Unix ms).
66
- # Exceptions propagate: the SDK then fails closed with a 503.
67
- return bool(
68
- client.set(f"devora:replay:{namespace}:{request_id}", b"1", nx=True, pxat=expires_at)
69
- )
70
-
71
-
72
- sdk = devora_sdk(
73
- api_key=os.environ["DEVORA_API_KEY"],
74
- secret_key=os.environ["DEVORA_SECRET_KEY"],
75
- org_id=os.environ["DEVORA_ORG_ID"],
76
- replay_store=RedisReplayStore(),
77
- )
78
- ```
79
-
80
- `consume` is called synchronously, including from `async_process_request`;
81
- keep it a single short round trip. Do not let the store evict these keys
82
- before they expire. A unique-key database insert with an expiry column works
83
- too.
84
-
85
- The guard's session-liveness checker caches at most 1,024 results and permits at
86
- most 64 distinct concurrent lookups per checker. Requests for the same session
87
- share a lookup. Live/ended results use the configured TTL (five seconds by
88
- default); unavailable results use at most one second. Cache hits never extend a
89
- verdict's lifetime. Capacity exhaustion follows the configured unavailable
90
- policy, which denies access by default.
91
-
92
- ### Cold-start latency
93
-
94
- The impersonation guard's scope policy is fetched lazily on first use, so the
95
- first request handled by a freshly started worker process pays for that fetch
96
- synchronously (and any request racing it gets a `503
97
- IMPERSONATION_POLICY_UNAVAILABLE` rather than waiting). If your deployment
98
- runs multiple worker processes (gunicorn, uWSGI, etc.), pass
99
- `prefetch_scope_config=True` to warm the cache in the background as soon as
100
- `devora_sdk(...)` is constructed, before the process starts serving traffic:
101
-
102
- ```python
103
- sdk = devora_sdk(
104
- api_key="pk_server_live_...",
105
- secret_key="sk_server_live_...",
106
- org_id="org_...",
107
- prefetch_scope_config=True,
108
- )
109
- ```
File without changes
File without changes