devora-django 0.1.0__tar.gz → 0.1.2__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,199 @@
1
+ Metadata-Version: 2.5
2
+ Name: devora-django
3
+ Version: 0.1.2
4
+ Summary: Django adapter for the Devora Python backend SDK
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: devora,django,impersonation,sdk,security
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Framework :: Django
14
+ Classifier: Framework :: Django :: 5.2
15
+ Classifier: Framework :: Django :: 6.0
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: <4.0,>=3.10
25
+ Requires-Dist: devora-python<0.2.0,>=0.1.2
26
+ Requires-Dist: django!=6.0.0,!=6.0.1,!=6.0.2,!=6.0.3,!=6.0.4,!=6.0.5,!=6.0.6,!=6.0.7,<7.0,>=5.2.17
27
+ Requires-Dist: sqlparse>=0.6.0
28
+ Description-Content-Type: text/markdown
29
+
30
+ # devora-django
31
+
32
+ Recording, masking and capture policy are configured in the Devora dashboard and
33
+ authorized server-side for each session. This backend SDK takes no capture
34
+ settings, and `devora_sdk()` ignores keyword arguments it does not recognize,
35
+ so check option names carefully. See
36
+ [capture settings](https://github.com/getdevora/devora-sdks/blob/main/SETTINGS.md).
37
+
38
+ Django adapter for the Devora Python backend SDK.
39
+
40
+ ## Requirements
41
+
42
+ - Python `>=3.10,<4.0`
43
+ - Django `>=5.2.17,<7.0`, excluding `6.0.0` through `6.0.7` (on Django 6.0, use `6.0.8` or newer)
44
+ - Outbound HTTPS access from your backend to the Devora API
45
+
46
+ ## Install
47
+
48
+ ```bash
49
+ pip install devora-python devora-django
50
+ ```
51
+
52
+ ## Quick Start
53
+
54
+ ```python
55
+ # devora_integration.py
56
+ import os
57
+
58
+ from devora_sdk import DEVORA_ENDPOINTS, devora_sdk
59
+
60
+ sdk = devora_sdk(
61
+ api_key=os.environ["DEVORA_API_KEY"], # pk_server_live_...
62
+ secret_key=os.environ["DEVORA_SECRET_KEY"], # sk_server_live_...
63
+ org_id=os.environ["DEVORA_ORG_ID"],
64
+ )
65
+
66
+
67
+ @sdk.register(DEVORA_ENDPOINTS.USER_SEARCH)
68
+ def search_users(req):
69
+ # Match name, email and the exact user ID with a parameterised query.
70
+ term = req.query.get("term", "")
71
+ limit = int(req.query.get("limit", 10))
72
+ users = search_customer_users(term, limit)
73
+ return {
74
+ "users": [
75
+ {
76
+ "id": u.id,
77
+ "name": u.name,
78
+ "email": u.email,
79
+ "attributes": {"company": u.company, "role": u.role, "plan": u.plan},
80
+ }
81
+ for u in users
82
+ ]
83
+ }
84
+
85
+
86
+ @sdk.register(DEVORA_ENDPOINTS.TERMINATE)
87
+ def terminate(req):
88
+ session_id = req.params["id"]
89
+ reason = (req.body or {}).get("reason")
90
+ # YOU IMPLEMENT: mark this Devora session revoked so every credential issued for it
91
+ # is rejected, including one issued after this call. Must be idempotent: Devora
92
+ # retries with a new request id (up to 5 attempts over 6 hours).
93
+ auth.revoke_impersonation_session(session_id=session_id, reason=reason)
94
+ return {"success": True}
95
+ ```
96
+
97
+ User search should match name, email and the exact user ID. `attributes` are optional display fields such as company, role or plan: up to 12 per user, lowercase keys like `last_login`, and string, number, boolean or `null` values. Devora drops invalid entries silently; see [Search results and templates](https://docs.devora.sh/guide/search-results) for the limits and how your team lays out results.
98
+
99
+ ```python
100
+ # urls.py
101
+ from django.urls import include, path
102
+ from devora_sdk_django import django_urlpatterns
103
+ from .devora_integration import sdk
104
+
105
+ urlpatterns = [
106
+ path("devora/", include(django_urlpatterns(sdk))),
107
+ ]
108
+ ```
109
+
110
+ Register every handler before building the URL patterns; routes registered
111
+ later are not mounted. If any handler is an `async def` function, use
112
+ `async_django_urlpatterns(sdk)` instead (it runs sync handlers in a worker
113
+ thread too); `django_urlpatterns` answers an async handler with
114
+ `400 HANDLER_ERROR`.
115
+
116
+ Each verified request is claimed once from Devora before your handler runs, so
117
+ replay protection needs no storage on your side; your backend only needs
118
+ outbound HTTPS access to the Devora API. See
119
+ [SIGNING.md](https://github.com/getdevora/devora-sdks/blob/main/SIGNING.md#request-claims).
120
+
121
+ The terminate body is `{"reason": ..., "terminatedBy": ...}`; Devora retries a
122
+ failed call (up to 5 attempts over 6 hours) with a new request id, so keep the
123
+ handler idempotent. Return a 2xx, or 429/503 with `Retry-After` when overloaded; any
124
+ other 4xx stops the retries. See
125
+ [Session lifecycle & cleanup](https://docs.devora.sh/guide/session-lifecycle).
126
+
127
+ For protected application routes, install the guard middleware and return the
128
+ context dict your `IMPERSONATE` handler stored, read back from your
129
+ authenticated request state.
130
+
131
+ ```python
132
+ # devora_guard.py
133
+ from devora_sdk_django import create_impersonation_guard
134
+ from .devora_integration import sdk
135
+
136
+
137
+ def get_impersonation_context(request):
138
+ # YOU IMPLEMENT: return the dict your IMPERSONATE handler stored, read from
139
+ # your verified session (never from headers or JSON the browser can set), or None.
140
+ return getattr(request.user, "devora", None)
141
+
142
+
143
+ DevoraGuardMiddleware = create_impersonation_guard(
144
+ sdk=sdk,
145
+ get_impersonation_context=get_impersonation_context,
146
+ )
147
+ ```
148
+
149
+ Add `"yourapp.devora_guard.DevoraGuardMiddleware"` to `settings.MIDDLEWARE`,
150
+ after your own authentication middleware, so `request.user` is set when the
151
+ guard runs.
152
+
153
+ Blocked API calls get a JSON error. Blocked page loads and form posts (an
154
+ `Accept` header with `text/html` and without `application/json`, no JSON body,
155
+ not an XHR) get a small, uncached HTML page with the reason and a back link. To show your own template, pass `render_blocked`, a
156
+ synchronous function that receives the request, the status code and the error
157
+ body and returns a response:
158
+
159
+ ```python
160
+ from django.shortcuts import render
161
+
162
+
163
+ def render_blocked(request, status_code, body):
164
+ return render(request, "impersonation_blocked.html", {"error": body.get("error")}, status=status_code)
165
+
166
+
167
+ DevoraGuardMiddleware = create_impersonation_guard(
168
+ sdk=sdk,
169
+ get_impersonation_context=get_impersonation_context,
170
+ render_blocked=render_blocked,
171
+ )
172
+ ```
173
+
174
+ `expires_at` must be a Unix timestamp in milliseconds. If your JWT stores Unix
175
+ seconds, multiply by `1000` when building the impersonation context. `actor`,
176
+ `subject`, `auth_method`, `authorization_source`, and `recording_allowed` are
177
+ all required — the guard rejects the context as invalid without them, even
178
+ though the dataclass marks them optional for construction convenience.
179
+
180
+ ## Cross-Origin-Opener-Policy
181
+
182
+ Django's `SecurityMiddleware` sends `Cross-Origin-Opener-Policy: same-origin`
183
+ by default. That cuts the page opened by a Devora impersonation link off from
184
+ the dashboard tab that opened it, so the link cannot be redeemed. Set
185
+ `SECURE_CROSS_ORIGIN_OPENER_POLICY = None`, or send `unsafe-none` on the
186
+ route your Devora links land on.
187
+
188
+ ## Request body limits
189
+
190
+ SDK views read at most `max_body_size + 1` bytes before parsing (1 MiB by default)
191
+ and reject oversized declared lengths without reading. The browser-session view
192
+ uses a 4 KiB limit. Async SDK views perform this bounded read off the event loop.
193
+ Set `DATA_UPLOAD_MAX_MEMORY_SIZE = 1024 * 1024` and keep earlier middleware from
194
+ buffering larger SDK bodies. WSGI/ASGI servers and reverse proxies also need
195
+ request-size and read-timeout limits: Django's ASGI handler can spool a request
196
+ before a view runs, and the SDK cannot bound that prior allocation.
197
+
198
+ Options, error codes and the browser-session bridge are documented in the
199
+ [Python reference](https://docs.devora.sh/reference/python).
@@ -0,0 +1,170 @@
1
+ # devora-django
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
+ Django adapter for the Devora Python backend SDK.
10
+
11
+ ## Requirements
12
+
13
+ - Python `>=3.10,<4.0`
14
+ - Django `>=5.2.17,<7.0`, excluding `6.0.0` through `6.0.7` (on Django 6.0, use `6.0.8` or newer)
15
+ - Outbound HTTPS access from your backend to the Devora API
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ pip install devora-python devora-django
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ ```python
26
+ # devora_integration.py
27
+ import os
28
+
29
+ from devora_sdk import DEVORA_ENDPOINTS, devora_sdk
30
+
31
+ sdk = devora_sdk(
32
+ api_key=os.environ["DEVORA_API_KEY"], # pk_server_live_...
33
+ secret_key=os.environ["DEVORA_SECRET_KEY"], # sk_server_live_...
34
+ org_id=os.environ["DEVORA_ORG_ID"],
35
+ )
36
+
37
+
38
+ @sdk.register(DEVORA_ENDPOINTS.USER_SEARCH)
39
+ def search_users(req):
40
+ # Match name, email and the exact user ID with a parameterised query.
41
+ term = req.query.get("term", "")
42
+ limit = int(req.query.get("limit", 10))
43
+ users = search_customer_users(term, limit)
44
+ return {
45
+ "users": [
46
+ {
47
+ "id": u.id,
48
+ "name": u.name,
49
+ "email": u.email,
50
+ "attributes": {"company": u.company, "role": u.role, "plan": u.plan},
51
+ }
52
+ for u in users
53
+ ]
54
+ }
55
+
56
+
57
+ @sdk.register(DEVORA_ENDPOINTS.TERMINATE)
58
+ def terminate(req):
59
+ session_id = req.params["id"]
60
+ reason = (req.body or {}).get("reason")
61
+ # YOU IMPLEMENT: mark this Devora session revoked so every credential issued for it
62
+ # is rejected, including one issued after this call. Must be idempotent: Devora
63
+ # retries with a new request id (up to 5 attempts over 6 hours).
64
+ auth.revoke_impersonation_session(session_id=session_id, reason=reason)
65
+ return {"success": True}
66
+ ```
67
+
68
+ User search should match name, email and the exact user ID. `attributes` are optional display fields such as company, role or plan: up to 12 per user, lowercase keys like `last_login`, and string, number, boolean or `null` values. Devora drops invalid entries silently; see [Search results and templates](https://docs.devora.sh/guide/search-results) for the limits and how your team lays out results.
69
+
70
+ ```python
71
+ # urls.py
72
+ from django.urls import include, path
73
+ from devora_sdk_django import django_urlpatterns
74
+ from .devora_integration import sdk
75
+
76
+ urlpatterns = [
77
+ path("devora/", include(django_urlpatterns(sdk))),
78
+ ]
79
+ ```
80
+
81
+ Register every handler before building the URL patterns; routes registered
82
+ later are not mounted. If any handler is an `async def` function, use
83
+ `async_django_urlpatterns(sdk)` instead (it runs sync handlers in a worker
84
+ thread too); `django_urlpatterns` answers an async handler with
85
+ `400 HANDLER_ERROR`.
86
+
87
+ Each verified request is claimed once from Devora before your handler runs, so
88
+ replay protection needs no storage on your side; your backend only needs
89
+ outbound HTTPS access to the Devora API. See
90
+ [SIGNING.md](https://github.com/getdevora/devora-sdks/blob/main/SIGNING.md#request-claims).
91
+
92
+ The terminate body is `{"reason": ..., "terminatedBy": ...}`; Devora retries a
93
+ failed call (up to 5 attempts over 6 hours) with a new request id, so keep the
94
+ handler idempotent. Return a 2xx, or 429/503 with `Retry-After` when overloaded; any
95
+ other 4xx stops the retries. See
96
+ [Session lifecycle & cleanup](https://docs.devora.sh/guide/session-lifecycle).
97
+
98
+ For protected application routes, install the guard middleware and return the
99
+ context dict your `IMPERSONATE` handler stored, read back from your
100
+ authenticated request state.
101
+
102
+ ```python
103
+ # devora_guard.py
104
+ from devora_sdk_django import create_impersonation_guard
105
+ from .devora_integration import sdk
106
+
107
+
108
+ def get_impersonation_context(request):
109
+ # YOU IMPLEMENT: return the dict your IMPERSONATE handler stored, read from
110
+ # your verified session (never from headers or JSON the browser can set), or None.
111
+ return getattr(request.user, "devora", None)
112
+
113
+
114
+ DevoraGuardMiddleware = create_impersonation_guard(
115
+ sdk=sdk,
116
+ get_impersonation_context=get_impersonation_context,
117
+ )
118
+ ```
119
+
120
+ Add `"yourapp.devora_guard.DevoraGuardMiddleware"` to `settings.MIDDLEWARE`,
121
+ after your own authentication middleware, so `request.user` is set when the
122
+ guard runs.
123
+
124
+ Blocked API calls get a JSON error. Blocked page loads and form posts (an
125
+ `Accept` header with `text/html` and without `application/json`, no JSON body,
126
+ not an XHR) get a small, uncached HTML page with the reason and a back link. To show your own template, pass `render_blocked`, a
127
+ synchronous function that receives the request, the status code and the error
128
+ body and returns a response:
129
+
130
+ ```python
131
+ from django.shortcuts import render
132
+
133
+
134
+ def render_blocked(request, status_code, body):
135
+ return render(request, "impersonation_blocked.html", {"error": body.get("error")}, status=status_code)
136
+
137
+
138
+ DevoraGuardMiddleware = create_impersonation_guard(
139
+ sdk=sdk,
140
+ get_impersonation_context=get_impersonation_context,
141
+ render_blocked=render_blocked,
142
+ )
143
+ ```
144
+
145
+ `expires_at` must be a Unix timestamp in milliseconds. If your JWT stores Unix
146
+ seconds, multiply by `1000` when building the impersonation context. `actor`,
147
+ `subject`, `auth_method`, `authorization_source`, and `recording_allowed` are
148
+ all required — the guard rejects the context as invalid without them, even
149
+ though the dataclass marks them optional for construction convenience.
150
+
151
+ ## Cross-Origin-Opener-Policy
152
+
153
+ Django's `SecurityMiddleware` sends `Cross-Origin-Opener-Policy: same-origin`
154
+ by default. That cuts the page opened by a Devora impersonation link off from
155
+ the dashboard tab that opened it, so the link cannot be redeemed. Set
156
+ `SECURE_CROSS_ORIGIN_OPENER_POLICY = None`, or send `unsafe-none` on the
157
+ route your Devora links land on.
158
+
159
+ ## Request body limits
160
+
161
+ SDK views read at most `max_body_size + 1` bytes before parsing (1 MiB by default)
162
+ and reject oversized declared lengths without reading. The browser-session view
163
+ uses a 4 KiB limit. Async SDK views perform this bounded read off the event loop.
164
+ Set `DATA_UPLOAD_MAX_MEMORY_SIZE = 1024 * 1024` and keep earlier middleware from
165
+ buffering larger SDK bodies. WSGI/ASGI servers and reverse proxies also need
166
+ request-size and read-timeout limits: Django's ASGI handler can spool a request
167
+ before a view runs, and the SDK cannot bound that prior allocation.
168
+
169
+ Options, error codes and the browser-session bridge are documented in the
170
+ [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-django"
7
- version = "0.1.0"
7
+ version = "0.1.2"
8
8
  description = "Django adapter for the Devora Python backend SDK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10,<4.0"
@@ -27,7 +27,7 @@ classifiers = [
27
27
  "Typing :: Typed",
28
28
  ]
29
29
  dependencies = [
30
- "devora-python>=0.1.0,<0.2.0",
30
+ "devora-python>=0.1.2,<0.2.0",
31
31
  # Security floors: the lowest versions without known advisories (audited 2026-09-28).
32
32
  "Django>=5.2.17,<7.0,!=6.0.0,!=6.0.1,!=6.0.2,!=6.0.3,!=6.0.4,!=6.0.5,!=6.0.6,!=6.0.7",
33
33
  "sqlparse>=0.6.0",
@@ -59,6 +59,39 @@ class DjangoImpersonationGuardOptions:
59
59
  on_liveness_unavailable: str = "deny"
60
60
  is_impersonation_allowed: Optional[Callable[[Any, ImpersonationContext], bool]] = None
61
61
  bridge_path: Optional[str] = None
62
+ render_blocked: Optional[Callable[[Any, int, Mapping[str, Any]], Any]] = None
63
+
64
+
65
+ def _wants_html(request: Any) -> bool:
66
+ """A browser page load or form post, as opposed to a fetch/XHR/API call."""
67
+ headers = request.headers
68
+ if headers.get("X-Requested-With") == "XMLHttpRequest":
69
+ return False
70
+ if "application/json" in (headers.get("Content-Type") or ""):
71
+ return False
72
+ accept = headers.get("Accept") or ""
73
+ return "text/html" in accept and "application/json" not in accept
74
+
75
+
76
+ def _blocked_html_response(request: Any, status_code: int, body: Mapping[str, Any]) -> Any:
77
+ """Default page for a blocked page load or form post: plain, escaped, never cached."""
78
+ from django.http import HttpResponse
79
+ from django.utils.html import escape
80
+
81
+ message = escape(str(body.get("error") or "This action is not allowed during impersonation"))
82
+ html = (
83
+ '<!doctype html><html lang="en"><head><meta charset="utf-8">'
84
+ '<meta name="viewport" content="width=device-width, initial-scale=1">'
85
+ "<title>Action not allowed</title></head>"
86
+ '<body style="font-family:system-ui,sans-serif;max-width:32rem;margin:4rem auto;padding:0 1rem">'
87
+ "<h1 style=\"font-size:1.25rem\">This action is not allowed in this session</h1>"
88
+ f"<p>{message}.</p>"
89
+ '<p><a href="javascript:history.back()">Go back</a></p>'
90
+ "</body></html>"
91
+ )
92
+ response = HttpResponse(html, status=status_code, content_type="text/html; charset=utf-8")
93
+ response["Cache-Control"] = "private, no-store"
94
+ return response
62
95
 
63
96
 
64
97
  def django_urlpatterns(sdk: Any, options: Optional[DjangoAdapterOptions] = None) -> list[Any]:
@@ -234,6 +267,7 @@ def create_impersonation_guard(
234
267
  on_liveness_unavailable: str = "deny",
235
268
  is_impersonation_allowed: Optional[Callable[[Any, ImpersonationContext], bool]] = None,
236
269
  bridge_path: Optional[str] = None,
270
+ render_blocked: Optional[Callable[[Any, int, Mapping[str, Any]], Any]] = None,
237
271
  ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
238
272
  """Django middleware factory (also usable as a view decorator).
239
273
 
@@ -245,6 +279,11 @@ def create_impersonation_guard(
245
279
  including ``SCRIPT_NAME``). Deny rules also match ``path_info`` and the path
246
280
  with an ``i18n_patterns`` language prefix removed. ``bridge_path`` enables the
247
281
  read-scope allowance for a mounted browser-session bridge (off by default).
282
+
283
+ Blocked API calls (fetch/XHR, JSON) get a JSON error. Blocked page loads and
284
+ form posts get a small HTML page instead, or whatever
285
+ ``render_blocked(request, status_code, body)`` returns, so a server-rendered
286
+ app can show its own template.
248
287
  """
249
288
  options = DjangoImpersonationGuardOptions(
250
289
  sdk=sdk,
@@ -257,6 +296,7 @@ def create_impersonation_guard(
257
296
  on_liveness_unavailable=on_liveness_unavailable,
258
297
  is_impersonation_allowed=is_impersonation_allowed,
259
298
  bridge_path=bridge_path,
299
+ render_blocked=render_blocked,
260
300
  )
261
301
  liveness = (
262
302
  SessionLivenessChecker(sdk, liveness_cache_ttl_ms, on_liveness_unavailable)
@@ -324,6 +364,9 @@ def create_impersonation_guard(
324
364
  )
325
365
 
326
366
  status_code, body = _guard_response(decision.status_code, decision.body, options.blocked_response)
367
+ if _wants_html(request):
368
+ render = options.render_blocked or _blocked_html_response
369
+ return render(request, status_code, body)
327
370
  return _private_json_response(body, status=status_code)
328
371
 
329
372
  def guard_failed() -> Any:
@@ -1,141 +0,0 @@
1
- Metadata-Version: 2.5
2
- Name: devora-django
3
- Version: 0.1.0
4
- Summary: Django adapter for the Devora Python backend SDK
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: devora,django,impersonation,sdk,security
12
- Classifier: Development Status :: 5 - Production/Stable
13
- Classifier: Framework :: Django
14
- Classifier: Framework :: Django :: 5.2
15
- Classifier: Framework :: Django :: 6.0
16
- Classifier: Intended Audience :: Developers
17
- Classifier: Programming Language :: Python :: 3
18
- Classifier: Programming Language :: Python :: 3.10
19
- Classifier: Programming Language :: Python :: 3.11
20
- Classifier: Programming Language :: Python :: 3.12
21
- Classifier: Programming Language :: Python :: 3.13
22
- Classifier: Programming Language :: Python :: 3.14
23
- Classifier: Typing :: Typed
24
- Requires-Python: <4.0,>=3.10
25
- Requires-Dist: devora-python<0.2.0,>=0.1.0
26
- Requires-Dist: django!=6.0.0,!=6.0.1,!=6.0.2,!=6.0.3,!=6.0.4,!=6.0.5,!=6.0.6,!=6.0.7,<7.0,>=5.2.17
27
- Requires-Dist: sqlparse>=0.6.0
28
- Description-Content-Type: text/markdown
29
-
30
- # devora-django
31
-
32
- Recording, masking and activity preferences are configured in Devora Settings.
33
- SDK initialization overrides are ignored. New sessions retain the server's policy
34
- snapshot across exchange and resume. Developer privacy labels take effect only
35
- when selected in Settings; sensitive-field protection remains mandatory.
36
- See [migration details](https://github.com/getdevora/devora-sdks/blob/main/SETTINGS.md).
37
-
38
- Django adapter for the Devora Python backend SDK.
39
-
40
- ## Requirements
41
-
42
- - Python `>=3.10,<4.0`
43
- - Django `>=5.2,<7.0`
44
-
45
- ## Install
46
-
47
- ```bash
48
- pip install devora-python devora-django
49
- ```
50
-
51
- ## Quick Start
52
-
53
- ```python
54
- # devora_integration.py
55
- from devora_sdk import DEVORA_ENDPOINTS, devora_sdk
56
-
57
- sdk = devora_sdk(
58
- api_key="pk_server_live_...",
59
- secret_key="sk_server_live_...",
60
- org_id="org_...",
61
- )
62
-
63
-
64
- @sdk.register(DEVORA_ENDPOINTS.USER_SEARCH)
65
- def search_users(req):
66
- return {"users": search_customer_users(req.query.get("term", ""))}
67
- ```
68
-
69
- ```python
70
- # urls.py
71
- from django.urls import include, path
72
- from devora_sdk_django import django_urlpatterns
73
- from .devora_integration import sdk
74
-
75
- urlpatterns = [
76
- path("devora/", include(django_urlpatterns(sdk))),
77
- ]
78
- ```
79
-
80
- Use `async_django_urlpatterns(sdk)` instead when your exposed handlers are
81
- async functions.
82
-
83
- For protected application routes, install the guard middleware and extract a
84
- trusted impersonation context from your authenticated request state.
85
-
86
- ```python
87
- from devora_sdk import ImpersonationContext
88
- from devora_sdk_django import create_impersonation_guard
89
- from .devora_integration import sdk
90
-
91
-
92
- def get_impersonation_context(request):
93
- devora = getattr(request.user, "devora", None)
94
- if not devora:
95
- return None
96
- return ImpersonationContext(
97
- is_impersonation=devora["isImpersonation"],
98
- scope=devora["scope"],
99
- session_id=devora["sessionId"],
100
- expires_at=devora["expiresAt"],
101
- actor=devora["actor"],
102
- subject=devora["subject"],
103
- auth_method=devora["authMethod"],
104
- authorization_source=devora["authorizationSource"],
105
- recording_allowed=devora["recordingAllowed"],
106
- impersonator=devora.get("impersonator"),
107
- )
108
-
109
-
110
- DevoraGuardMiddleware = create_impersonation_guard(
111
- sdk=sdk,
112
- get_impersonation_context=get_impersonation_context,
113
- )
114
- ```
115
-
116
- Add `"yourapp.devora_integration.DevoraGuardMiddleware"` to `settings.MIDDLEWARE`,
117
- after your own authentication middleware.
118
-
119
- `expires_at` must be a Unix timestamp in milliseconds. If your JWT stores Unix
120
- seconds, multiply by `1000` when building the impersonation context. `actor`,
121
- `subject`, `auth_method`, `authorization_source`, and `recording_allowed` are
122
- all required — the guard rejects the context as invalid without them, even
123
- though the dataclass marks them optional for construction convenience.
124
-
125
- ## Cross-Origin-Opener-Policy
126
-
127
- Django's `SecurityMiddleware` sends `Cross-Origin-Opener-Policy: same-origin`
128
- by default. That cuts the page opened by a Devora impersonation link off from
129
- the dashboard tab that opened it, so the link cannot be redeemed. Set
130
- `SECURE_CROSS_ORIGIN_OPENER_POLICY = None`, or send `unsafe-none` on the
131
- route your Devora links land on.
132
-
133
- ## Request body limits
134
-
135
- SDK views read at most `max_body_size + 1` bytes before parsing (1 MiB by default)
136
- and reject oversized declared lengths without reading. The browser-session view
137
- uses a 4 KiB limit. Async SDK views perform this bounded read off the event loop.
138
- Set `DATA_UPLOAD_MAX_MEMORY_SIZE = 1024 * 1024` and keep earlier middleware from
139
- buffering larger SDK bodies. WSGI/ASGI servers and reverse proxies also need
140
- request-size and read-timeout limits: Django's ASGI handler can spool a request
141
- before a view runs, and the SDK cannot bound that prior allocation.
@@ -1,112 +0,0 @@
1
- # devora-django
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
- Django adapter for the Devora Python backend SDK.
10
-
11
- ## Requirements
12
-
13
- - Python `>=3.10,<4.0`
14
- - Django `>=5.2,<7.0`
15
-
16
- ## Install
17
-
18
- ```bash
19
- pip install devora-python devora-django
20
- ```
21
-
22
- ## Quick Start
23
-
24
- ```python
25
- # devora_integration.py
26
- from devora_sdk import DEVORA_ENDPOINTS, devora_sdk
27
-
28
- sdk = devora_sdk(
29
- api_key="pk_server_live_...",
30
- secret_key="sk_server_live_...",
31
- org_id="org_...",
32
- )
33
-
34
-
35
- @sdk.register(DEVORA_ENDPOINTS.USER_SEARCH)
36
- def search_users(req):
37
- return {"users": search_customer_users(req.query.get("term", ""))}
38
- ```
39
-
40
- ```python
41
- # urls.py
42
- from django.urls import include, path
43
- from devora_sdk_django import django_urlpatterns
44
- from .devora_integration import sdk
45
-
46
- urlpatterns = [
47
- path("devora/", include(django_urlpatterns(sdk))),
48
- ]
49
- ```
50
-
51
- Use `async_django_urlpatterns(sdk)` instead when your exposed handlers are
52
- async functions.
53
-
54
- For protected application routes, install the guard middleware and extract a
55
- trusted impersonation context from your authenticated request state.
56
-
57
- ```python
58
- from devora_sdk import ImpersonationContext
59
- from devora_sdk_django import create_impersonation_guard
60
- from .devora_integration import sdk
61
-
62
-
63
- def get_impersonation_context(request):
64
- devora = getattr(request.user, "devora", None)
65
- if not devora:
66
- return None
67
- return ImpersonationContext(
68
- is_impersonation=devora["isImpersonation"],
69
- scope=devora["scope"],
70
- session_id=devora["sessionId"],
71
- expires_at=devora["expiresAt"],
72
- actor=devora["actor"],
73
- subject=devora["subject"],
74
- auth_method=devora["authMethod"],
75
- authorization_source=devora["authorizationSource"],
76
- recording_allowed=devora["recordingAllowed"],
77
- impersonator=devora.get("impersonator"),
78
- )
79
-
80
-
81
- DevoraGuardMiddleware = create_impersonation_guard(
82
- sdk=sdk,
83
- get_impersonation_context=get_impersonation_context,
84
- )
85
- ```
86
-
87
- Add `"yourapp.devora_integration.DevoraGuardMiddleware"` to `settings.MIDDLEWARE`,
88
- after your own authentication middleware.
89
-
90
- `expires_at` must be a Unix timestamp in milliseconds. If your JWT stores Unix
91
- seconds, multiply by `1000` when building the impersonation context. `actor`,
92
- `subject`, `auth_method`, `authorization_source`, and `recording_allowed` are
93
- all required — the guard rejects the context as invalid without them, even
94
- though the dataclass marks them optional for construction convenience.
95
-
96
- ## Cross-Origin-Opener-Policy
97
-
98
- Django's `SecurityMiddleware` sends `Cross-Origin-Opener-Policy: same-origin`
99
- by default. That cuts the page opened by a Devora impersonation link off from
100
- the dashboard tab that opened it, so the link cannot be redeemed. Set
101
- `SECURE_CROSS_ORIGIN_OPENER_POLICY = None`, or send `unsafe-none` on the
102
- route your Devora links land on.
103
-
104
- ## Request body limits
105
-
106
- SDK views read at most `max_body_size + 1` bytes before parsing (1 MiB by default)
107
- and reject oversized declared lengths without reading. The browser-session view
108
- uses a 4 KiB limit. Async SDK views perform this bounded read off the event loop.
109
- Set `DATA_UPLOAD_MAX_MEMORY_SIZE = 1024 * 1024` and keep earlier middleware from
110
- buffering larger SDK bodies. WSGI/ASGI servers and reverse proxies also need
111
- request-size and read-timeout limits: Django's ASGI handler can spool a request
112
- before a view runs, and the SDK cannot bound that prior allocation.
File without changes
File without changes