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.
- devora_python-0.1.0/.gitignore +14 -0
- devora_python-0.1.0/LICENSE +21 -0
- devora_python-0.1.0/PKG-INFO +132 -0
- devora_python-0.1.0/README.md +109 -0
- devora_python-0.1.0/pyproject.toml +44 -0
- devora_python-0.1.0/src/devora_sdk/__init__.py +92 -0
- devora_python-0.1.0/src/devora_sdk/api_url_gen.py +2 -0
- devora_python-0.1.0/src/devora_sdk/browser_session.py +45 -0
- devora_python-0.1.0/src/devora_sdk/constants.py +49 -0
- devora_python-0.1.0/src/devora_sdk/guard.py +485 -0
- devora_python-0.1.0/src/devora_sdk/handler.py +298 -0
- devora_python-0.1.0/src/devora_sdk/hmac.py +67 -0
- devora_python-0.1.0/src/devora_sdk/models.py +79 -0
- devora_python-0.1.0/src/devora_sdk/policy.py +208 -0
- devora_python-0.1.0/src/devora_sdk/py.typed +1 -0
- devora_python-0.1.0/src/devora_sdk/replay.py +41 -0
- devora_python-0.1.0/src/devora_sdk/sdk.py +390 -0
- devora_python-0.1.0/src/devora_sdk/security.py +37 -0
- devora_python-0.1.0/src/devora_sdk/signing.py +240 -0
- devora_python-0.1.0/src/devora_sdk/transport.py +158 -0
- devora_python-0.1.0/src/devora_sdk/utils.py +164 -0
|
@@ -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,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"
|