devora-python 0.1.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,14 @@
1
+ node_modules/
2
+ dist/
3
+ build/
4
+ .turbo/
5
+ .venv/
6
+ __pycache__/
7
+ *.pyc
8
+ .pytest_cache/
9
+ .env
10
+ .env.*
11
+ !.env.example
12
+ core/src/constants/api-url.gen.ts
13
+ python/src/devora_sdk/api_url_gen.py
14
+ .DS_Store
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Devora
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,132 @@
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
+ ```
@@ -0,0 +1,109 @@
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
+ ```
@@ -0,0 +1,44 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.25"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "devora-python"
7
+ version = "0.1.0"
8
+ description = "Devora Python backend SDK for secure impersonation"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10,<4.0"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Devora" }]
14
+ keywords = ["devora", "sdk", "impersonation", "backend", "security"]
15
+ classifiers = [
16
+ "Development Status :: 5 - Production/Stable",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Programming Language :: Python :: 3.14",
24
+ "Typing :: Typed",
25
+ ]
26
+ dependencies = []
27
+
28
+ [project.urls]
29
+ Documentation = "https://docs.devora.sh"
30
+ Repository = "https://github.com/getdevora/devora-sdks"
31
+ Issues = "https://github.com/getdevora/devora-sdks/issues"
32
+
33
+ [tool.hatch.build.targets.wheel]
34
+ packages = ["src/devora_sdk"]
35
+ # Generated at build time and git-ignored; included explicitly so a .gitignore
36
+ # rule can never drop it from the distribution.
37
+ artifacts = ["src/devora_sdk/api_url_gen.py"]
38
+
39
+ [tool.hatch.build]
40
+ artifacts = ["src/devora_sdk/api_url_gen.py"]
41
+
42
+ [tool.hatch.build.targets.sdist]
43
+ # Tests and local tooling stay in the repository; the sdist carries the package.
44
+ include = ["src", "README.md", "LICENSE", "pyproject.toml"]
@@ -0,0 +1,92 @@
1
+ from .security import validate_timestamp_tolerance
2
+ from .constants import DEVORA_ENDPOINTS, ENDPOINT_METHODS, PROTECTED_ENDPOINTS, SECURITY_HEADERS
3
+ from .guard import (
4
+ METHOD_OVERRIDE_HEADERS,
5
+ GuardDecision,
6
+ ImpersonationContext,
7
+ InvalidImpersonationContext,
8
+ ScopeEndpoint,
9
+ SessionLivenessChecker,
10
+ coerce_impersonation_context,
11
+ evaluate_impersonation_guard,
12
+ policy_methods,
13
+ validate_impersonation_context,
14
+ )
15
+ from .handler import (
16
+ AdapterRequest,
17
+ ProcessRequestOptions,
18
+ async_process_request,
19
+ create_generic_handler,
20
+ process_request,
21
+ )
22
+ from .hmac import sha256_hex, sign_request
23
+ from .signing import (
24
+ CUSTOMER_TO_DEVORA,
25
+ DEVORA_TO_CUSTOMER,
26
+ build_canonical_string,
27
+ build_strict_query,
28
+ encode_path_param,
29
+ route_relative_path,
30
+ strict_encode,
31
+ )
32
+ from .transport import ControlPlaneError
33
+ from .models import (
34
+ DevoraImpersonationContext,
35
+ DevoraRequest,
36
+ DevoraResponse,
37
+ SDKRoute,
38
+ SDKStats,
39
+ ValidationResult,
40
+ )
41
+ from .policy import ScopeConfig, ScopeConfigFetcher
42
+ from .sdk import DevoraBackendSDK, devora_sdk
43
+ from .replay import InMemoryReplayStore, ReplayStore
44
+ from .browser_session import resolve_browser_session
45
+ from .constants import BROWSER_SESSION_BRIDGE_PATH
46
+
47
+ __all__ = [
48
+ "AdapterRequest",
49
+ "DEVORA_ENDPOINTS",
50
+ "ENDPOINT_METHODS",
51
+ "PROTECTED_ENDPOINTS",
52
+ "SECURITY_HEADERS",
53
+ "DevoraBackendSDK",
54
+ "DevoraImpersonationContext",
55
+ "DevoraRequest",
56
+ "DevoraResponse",
57
+ "GuardDecision",
58
+ "ImpersonationContext",
59
+ "InMemoryReplayStore",
60
+ "ProcessRequestOptions",
61
+ "ReplayStore",
62
+ "SDKRoute",
63
+ "SDKStats",
64
+ "ScopeConfig",
65
+ "ScopeEndpoint",
66
+ "ScopeConfigFetcher",
67
+ "SessionLivenessChecker",
68
+ "ValidationResult",
69
+ "async_process_request",
70
+ "resolve_browser_session",
71
+ "BROWSER_SESSION_BRIDGE_PATH",
72
+ "create_generic_handler",
73
+ "devora_sdk",
74
+ "evaluate_impersonation_guard",
75
+ "coerce_impersonation_context",
76
+ "InvalidImpersonationContext",
77
+ "METHOD_OVERRIDE_HEADERS",
78
+ "policy_methods",
79
+ "validate_timestamp_tolerance",
80
+ "process_request",
81
+ "sign_request",
82
+ "sha256_hex",
83
+ "build_canonical_string",
84
+ "build_strict_query",
85
+ "encode_path_param",
86
+ "route_relative_path",
87
+ "strict_encode",
88
+ "CUSTOMER_TO_DEVORA",
89
+ "DEVORA_TO_CUSTOMER",
90
+ "ControlPlaneError",
91
+ "validate_impersonation_context",
92
+ ]
@@ -0,0 +1,2 @@
1
+ # Generated by scripts/generate-api-url.mjs - do not edit
2
+ DEVORA_API_ORIGIN = "https://beloved-bee-737.convex.site"
@@ -0,0 +1,45 @@
1
+ """Browser-session bridge (server side). See @devorash/node browser-session.ts."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ from typing import Any, Optional
6
+
7
+ from .constants import TAB_REF_PATTERN
8
+ from .guard import validate_impersonation_context
9
+
10
+
11
+ def resolve_browser_session(
12
+ sdk: Any,
13
+ context: Any,
14
+ tab_ref: Any,
15
+ origin: Optional[str],
16
+ allowed_origins: Optional[list[str]] = None,
17
+ ) -> tuple[int, dict[str, Any]]:
18
+ """Return ``(status_code, body)`` for the bridge route.
19
+
20
+ Ordinary customer sessions return ``{"status": "none"}`` without contacting
21
+ Devora. Impersonated sessions receive a one-time resume code, or ``blocked``
22
+ when Devora cannot safely restore them.
23
+ """
24
+ if not isinstance(tab_ref, str) or not re.fullmatch(TAB_REF_PATTERN, tab_ref):
25
+ return 400, {"error": "Invalid tab reference"}
26
+ if context is None or getattr(context, "is_impersonation", None) is False:
27
+ return 200, {"status": "none"}
28
+ # Origin only matters once we're about to mint a resume code for a real
29
+ # impersonation session; ordinary traffic through this bridge is unaffected.
30
+ # An Origin header must be listed in allowed_origins (unconfigured fails
31
+ # closed). Devora binds every resume code to the origin that will redeem it,
32
+ # so a request without an Origin header cannot use one and gets none.
33
+ if origin and (allowed_origins is None or origin not in allowed_origins):
34
+ return 403, {"error": "Invalid request origin"}
35
+ # The same strict validation as the guard: malformed or expired contexts
36
+ # never mint a resume code.
37
+ if not origin or not validate_impersonation_context(context)["valid"]:
38
+ return 200, {"status": "blocked", "reason": "session_invalid"}
39
+ session_id = context.session_id
40
+ result = sdk.create_browser_resume_code(session_id, tab_ref, origin)
41
+ if result is None:
42
+ return 200, {"status": "blocked", "reason": "control_plane_unavailable"}
43
+ if "error" in result:
44
+ return 200, {"status": "blocked", "reason": "session_invalid"}
45
+ return 200, {"status": "resume", "code": result["code"]}
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ from .api_url_gen import DEVORA_API_ORIGIN
4
+
5
+
6
+ class DEVORA_ENDPOINTS:
7
+ USER_SEARCH = "/user/search"
8
+ USER_BY_ID = "/user/:id"
9
+ IMPERSONATE = "/impersonate/:id"
10
+ TERMINATE = "/impersonate/:id/terminate"
11
+ TEST = "/test"
12
+ HEALTH = "/health"
13
+
14
+
15
+ ENDPOINT_METHODS = {
16
+ DEVORA_ENDPOINTS.USER_SEARCH: "GET",
17
+ DEVORA_ENDPOINTS.USER_BY_ID: "GET",
18
+ DEVORA_ENDPOINTS.IMPERSONATE: "POST",
19
+ DEVORA_ENDPOINTS.TERMINATE: "DELETE",
20
+ DEVORA_ENDPOINTS.TEST: "GET",
21
+ DEVORA_ENDPOINTS.HEALTH: "GET",
22
+ }
23
+
24
+ PROTECTED_ENDPOINTS = (DEVORA_ENDPOINTS.TEST, DEVORA_ENDPOINTS.HEALTH)
25
+
26
+
27
+ class SECURITY_HEADERS:
28
+ SIGNATURE = "x-devora-signature"
29
+ SENT_AT = "x-devora-sent-at"
30
+ KEY_ID = "x-devora-key-id"
31
+ ORG_ID = "x-devora-org-id"
32
+ REQUEST_ID = "x-devora-request-id"
33
+ SIGNATURE_VERSION = "x-devora-signature-version"
34
+ SESSION_ID = "x-devora-session-id"
35
+
36
+
37
+ SDK_VERSION = "0.1.0"
38
+ DEFAULT_API_URL = DEVORA_API_ORIGIN
39
+ DEFAULT_TIMESTAMP_TOLERANCE_SECONDS = 300
40
+ DEFAULT_MAX_BODY_SIZE_BYTES = 1024 * 1024
41
+ WRITE_METHODS = {"POST", "PUT", "PATCH", "DELETE"}
42
+
43
+
44
+ # Browser-session bridge (see @devorash/core BROWSER_SESSION_BRIDGE).
45
+ BROWSER_SESSION_BRIDGE_PATH = "/api/devora/browser-session"
46
+ BROWSER_RESUME_CODE_ENDPOINT = "/api/sdk/browser-resume-code"
47
+ BROWSER_RESUME_ENDPOINT = "/api/sdk/browser-resume"
48
+ # \Z, not $: $ also matches before a trailing newline.
49
+ TAB_REF_PATTERN = r"^[A-Za-z0-9_-]{4,96}\Z"