provide-uterm-cloudflare 0.5.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.
- provide_uterm_cloudflare-0.5.0/PKG-INFO +157 -0
- provide_uterm_cloudflare-0.5.0/README.md +123 -0
- provide_uterm_cloudflare-0.5.0/VERSION +1 -0
- provide_uterm_cloudflare-0.5.0/pyproject.toml +110 -0
- provide_uterm_cloudflare-0.5.0/setup.cfg +4 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/__init__.py +10 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/__main__.py +4 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/api/__init__.py +10 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/api/_pam.py +99 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/api/_profiles.py +240 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/api/_tunnel_api.py +412 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/api/http_routes/__init__.py +13 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/api/http_routes/_dispatch.py +140 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/api/http_routes/_hijack.py +308 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/api/http_routes/_recording.py +109 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/api/http_routes/_session.py +390 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/api/http_routes/_shared.py +258 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/api/tunnel_routes.py +116 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/api/ws_routes.py +429 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/auth/__init__.py +3 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/auth/jwt.py +404 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/bridge/__init__.py +3 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/bridge/hijack.py +18 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/cf_types.py +99 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/cli.py +82 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/config.py +300 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/contracts.py +286 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/do/__init__.py +1 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/do/_sse.py +99 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/do/_webhook_crypto.py +133 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/do/_webhooks.py +280 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/do/persistence.py +73 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/do/session_runtime/__init__.py +32 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/do/session_runtime/auth.py +193 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/do/session_runtime/fetch.py +415 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/do/session_runtime/flow_control.py +135 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/do/session_runtime/io.py +638 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/do/session_runtime/lifecycle.py +266 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/do/session_runtime/runtime.py +223 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/do/session_runtime/ws_helpers.py +327 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/do/ushell.py +167 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/entry/__init__.py +57 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/entry/auth.py +118 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/entry/fallback_stubs.py +185 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/entry/handlers.py +337 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/entry/registry.py +63 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/entry/route_defs.py +154 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/entry/security.py +89 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/entry/share_tokens.py +58 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/entry/spa.py +89 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/py.typed +0 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/state/__init__.py +3 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/state/_row_utils.py +79 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/state/registry.py +194 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/state/store.py +576 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/ui/__init__.py +3 -0
- provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/ui/assets.py +89 -0
- provide_uterm_cloudflare-0.5.0/src/provide_uterm_cloudflare.egg-info/PKG-INFO +157 -0
- provide_uterm_cloudflare-0.5.0/src/provide_uterm_cloudflare.egg-info/SOURCES.txt +168 -0
- provide_uterm_cloudflare-0.5.0/src/provide_uterm_cloudflare.egg-info/dependency_links.txt +1 -0
- provide_uterm_cloudflare-0.5.0/src/provide_uterm_cloudflare.egg-info/entry_points.txt +2 -0
- provide_uterm_cloudflare-0.5.0/src/provide_uterm_cloudflare.egg-info/requires.txt +17 -0
- provide_uterm_cloudflare-0.5.0/src/provide_uterm_cloudflare.egg-info/top_level.txt +1 -0
- provide_uterm_cloudflare-0.5.0/tests/test_alarm.py +86 -0
- provide_uterm_cloudflare-0.5.0/tests/test_api_contracts.py +425 -0
- provide_uterm_cloudflare-0.5.0/tests/test_api_contracts_2.py +172 -0
- provide_uterm_cloudflare-0.5.0/tests/test_auth_jwt.py +427 -0
- provide_uterm_cloudflare-0.5.0/tests/test_backpressure_wiring.py +143 -0
- provide_uterm_cloudflare-0.5.0/tests/test_branch_coverage.py +27 -0
- provide_uterm_cloudflare-0.5.0/tests/test_branch_coverage_2.py +160 -0
- provide_uterm_cloudflare-0.5.0/tests/test_branch_coverage_part1.py +397 -0
- provide_uterm_cloudflare-0.5.0/tests/test_branch_coverage_part2.py +378 -0
- provide_uterm_cloudflare-0.5.0/tests/test_browser_ws_visibility.py +266 -0
- provide_uterm_cloudflare-0.5.0/tests/test_cf_cli.py +185 -0
- provide_uterm_cloudflare-0.5.0/tests/test_cf_resume.py +455 -0
- provide_uterm_cloudflare-0.5.0/tests/test_conformance_parity.py +137 -0
- provide_uterm_cloudflare-0.5.0/tests/test_contracts.py +207 -0
- provide_uterm_cloudflare-0.5.0/tests/test_cookie_only_coverage.py +292 -0
- provide_uterm_cloudflare-0.5.0/tests/test_coverage2.py +340 -0
- provide_uterm_cloudflare-0.5.0/tests/test_coverage3.py +441 -0
- provide_uterm_cloudflare-0.5.0/tests/test_coverage3_2.py +248 -0
- provide_uterm_cloudflare-0.5.0/tests/test_coverage_entry_registry.py +28 -0
- provide_uterm_cloudflare-0.5.0/tests/test_coverage_entry_registry_part1.py +182 -0
- provide_uterm_cloudflare-0.5.0/tests/test_coverage_entry_registry_part2.py +215 -0
- provide_uterm_cloudflare-0.5.0/tests/test_coverage_entry_registry_part3.py +236 -0
- provide_uterm_cloudflare-0.5.0/tests/test_coverage_gaps.py +28 -0
- provide_uterm_cloudflare-0.5.0/tests/test_coverage_gaps_part1.py +177 -0
- provide_uterm_cloudflare-0.5.0/tests/test_coverage_gaps_part2.py +451 -0
- provide_uterm_cloudflare-0.5.0/tests/test_coverage_gaps_part3.py +216 -0
- provide_uterm_cloudflare-0.5.0/tests/test_coverage_jwt.py +427 -0
- provide_uterm_cloudflare-0.5.0/tests/test_coverage_session_runtime.py +150 -0
- provide_uterm_cloudflare-0.5.0/tests/test_credentials_reload.py +284 -0
- provide_uterm_cloudflare-0.5.0/tests/test_cross_compat.py +300 -0
- provide_uterm_cloudflare-0.5.0/tests/test_deckmux_presence.py +322 -0
- provide_uterm_cloudflare-0.5.0/tests/test_do_route_defs.py +417 -0
- provide_uterm_cloudflare-0.5.0/tests/test_do_ushell.py +355 -0
- provide_uterm_cloudflare-0.5.0/tests/test_e2e_full_stack.py +346 -0
- provide_uterm_cloudflare-0.5.0/tests/test_e2e_pam_relay.py +192 -0
- provide_uterm_cloudflare-0.5.0/tests/test_e2e_playwright_proxy.py +252 -0
- provide_uterm_cloudflare-0.5.0/tests/test_e2e_recording.py +187 -0
- provide_uterm_cloudflare-0.5.0/tests/test_e2e_sse_webhooks.py +493 -0
- provide_uterm_cloudflare-0.5.0/tests/test_e2e_tunnel.py +230 -0
- provide_uterm_cloudflare-0.5.0/tests/test_e2e_wrangler.py +73 -0
- provide_uterm_cloudflare-0.5.0/tests/test_e2e_ws.py +464 -0
- provide_uterm_cloudflare-0.5.0/tests/test_entry.py +11 -0
- provide_uterm_cloudflare-0.5.0/tests/test_entry_unit.py +28 -0
- provide_uterm_cloudflare-0.5.0/tests/test_entry_unit_auth_part2.py +153 -0
- provide_uterm_cloudflare-0.5.0/tests/test_entry_unit_part1.py +38 -0
- provide_uterm_cloudflare-0.5.0/tests/test_entry_unit_part2.py +450 -0
- provide_uterm_cloudflare-0.5.0/tests/test_entry_unit_part3.py +159 -0
- provide_uterm_cloudflare-0.5.0/tests/test_fallback_imports.py +319 -0
- provide_uterm_cloudflare-0.5.0/tests/test_finding_15.py +16 -0
- provide_uterm_cloudflare-0.5.0/tests/test_flow_control.py +164 -0
- provide_uterm_cloudflare-0.5.0/tests/test_hibernate_wake_contract.py +259 -0
- provide_uterm_cloudflare-0.5.0/tests/test_hijack.py +29 -0
- provide_uterm_cloudflare-0.5.0/tests/test_http_channel.py +65 -0
- provide_uterm_cloudflare-0.5.0/tests/test_http_routes_coverage.py +481 -0
- provide_uterm_cloudflare-0.5.0/tests/test_http_routes_coverage2.py +299 -0
- provide_uterm_cloudflare-0.5.0/tests/test_http_routes_coverage_2.py +27 -0
- provide_uterm_cloudflare-0.5.0/tests/test_http_routes_coverage_2_part1.py +361 -0
- provide_uterm_cloudflare-0.5.0/tests/test_http_routes_coverage_2_part2.py +276 -0
- provide_uterm_cloudflare-0.5.0/tests/test_http_routes_coverage_3.py +208 -0
- provide_uterm_cloudflare-0.5.0/tests/test_http_routes_csrf.py +75 -0
- provide_uterm_cloudflare-0.5.0/tests/test_jwks_resilience.py +104 -0
- provide_uterm_cloudflare-0.5.0/tests/test_owned_input_fencing.py +219 -0
- provide_uterm_cloudflare-0.5.0/tests/test_owned_input_fencing_hijack_tunnel.py +538 -0
- provide_uterm_cloudflare-0.5.0/tests/test_owned_input_fencing_worker_lifecycle.py +304 -0
- provide_uterm_cloudflare-0.5.0/tests/test_pam_endpoint.py +185 -0
- provide_uterm_cloudflare-0.5.0/tests/test_persistence.py +140 -0
- provide_uterm_cloudflare-0.5.0/tests/test_profiles.py +451 -0
- provide_uterm_cloudflare-0.5.0/tests/test_recording.py +27 -0
- provide_uterm_cloudflare-0.5.0/tests/test_recording_part1.py +361 -0
- provide_uterm_cloudflare-0.5.0/tests/test_recording_part2.py +231 -0
- provide_uterm_cloudflare-0.5.0/tests/test_registry.py +222 -0
- provide_uterm_cloudflare-0.5.0/tests/test_security_hardening.py +383 -0
- provide_uterm_cloudflare-0.5.0/tests/test_security_headers.py +296 -0
- provide_uterm_cloudflare-0.5.0/tests/test_security_page_routes.py +110 -0
- provide_uterm_cloudflare-0.5.0/tests/test_session_lifecycle_security_scenarios.py +756 -0
- provide_uterm_cloudflare-0.5.0/tests/test_session_runtime_coverage.py +365 -0
- provide_uterm_cloudflare-0.5.0/tests/test_session_runtime_unit.py +27 -0
- provide_uterm_cloudflare-0.5.0/tests/test_session_runtime_unit_2.py +471 -0
- provide_uterm_cloudflare-0.5.0/tests/test_session_runtime_unit_3.py +494 -0
- provide_uterm_cloudflare-0.5.0/tests/test_session_runtime_unit_part1.py +364 -0
- provide_uterm_cloudflare-0.5.0/tests/test_session_runtime_unit_part2.py +388 -0
- provide_uterm_cloudflare-0.5.0/tests/test_session_visibility.py +307 -0
- provide_uterm_cloudflare-0.5.0/tests/test_share_token_ttl.py +157 -0
- provide_uterm_cloudflare-0.5.0/tests/test_share_tokens.py +138 -0
- provide_uterm_cloudflare-0.5.0/tests/test_spa_sri.py +25 -0
- provide_uterm_cloudflare-0.5.0/tests/test_sse.py +177 -0
- provide_uterm_cloudflare-0.5.0/tests/test_store.py +57 -0
- provide_uterm_cloudflare-0.5.0/tests/test_store_coverage.py +292 -0
- provide_uterm_cloudflare-0.5.0/tests/test_tunnel_integration.py +470 -0
- provide_uterm_cloudflare-0.5.0/tests/test_tunnel_invite_do.py +158 -0
- provide_uterm_cloudflare-0.5.0/tests/test_tunnel_routes.py +28 -0
- provide_uterm_cloudflare-0.5.0/tests/test_tunnel_routes_part1.py +404 -0
- provide_uterm_cloudflare-0.5.0/tests/test_tunnel_routes_part2.py +383 -0
- provide_uterm_cloudflare-0.5.0/tests/test_tunnel_routes_part3.py +136 -0
- provide_uterm_cloudflare-0.5.0/tests/test_ui_assets.py +265 -0
- provide_uterm_cloudflare-0.5.0/tests/test_ushell_vendor_guard.py +41 -0
- provide_uterm_cloudflare-0.5.0/tests/test_webhook_crypto.py +86 -0
- provide_uterm_cloudflare-0.5.0/tests/test_webhook_mutate_gate.py +81 -0
- provide_uterm_cloudflare-0.5.0/tests/test_webhooks.py +27 -0
- provide_uterm_cloudflare-0.5.0/tests/test_webhooks_dispatch.py +126 -0
- provide_uterm_cloudflare-0.5.0/tests/test_webhooks_part1.py +97 -0
- provide_uterm_cloudflare-0.5.0/tests/test_webhooks_part2.py +515 -0
- provide_uterm_cloudflare-0.5.0/tests/test_worker_route_defs.py +247 -0
- provide_uterm_cloudflare-0.5.0/tests/test_ws_helpers.py +277 -0
- provide_uterm_cloudflare-0.5.0/tests/test_ws_routes.py +27 -0
- provide_uterm_cloudflare-0.5.0/tests/test_ws_routes_part1.py +472 -0
- provide_uterm_cloudflare-0.5.0/tests/test_ws_routes_part2.py +281 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: provide-uterm-cloudflare
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Cloudflare Workers runtime for provide-uterm control plane
|
|
5
|
+
Author: provide.io llc
|
|
6
|
+
License-Expression: AGPL-3.0-or-later
|
|
7
|
+
Project-URL: Homepage, https://github.com/provide-io/provide-uterm
|
|
8
|
+
Project-URL: Repository, https://github.com/provide-io/provide-uterm
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
15
|
+
Classifier: Framework :: AsyncIO
|
|
16
|
+
Requires-Python: >=3.11
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: pyjwt>=2.9
|
|
19
|
+
Requires-Dist: provide-uterm>=0.5.0
|
|
20
|
+
Requires-Dist: provide-uterm-server>=0.5.0
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=9.0; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest-asyncio>=1.3; extra == "dev"
|
|
24
|
+
Requires-Dist: pytest-cov>=7.0; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest-playwright>=0.7.2; extra == "dev"
|
|
26
|
+
Requires-Dist: ruff>=0.15; extra == "dev"
|
|
27
|
+
Requires-Dist: mypy>=1.19; extra == "dev"
|
|
28
|
+
Requires-Dist: workers-py>=0.4.0; extra == "dev"
|
|
29
|
+
Requires-Dist: websockets>=16.0; extra == "dev"
|
|
30
|
+
Requires-Dist: cryptography>=44.0; extra == "dev"
|
|
31
|
+
Requires-Dist: provide-uterm-server[annotation,server]>=0.5.0; extra == "dev"
|
|
32
|
+
Requires-Dist: starlette>=0.40; extra == "dev"
|
|
33
|
+
Requires-Dist: httpx2>=2.10; extra == "dev"
|
|
34
|
+
|
|
35
|
+
# provide-uterm-cloudflare
|
|
36
|
+
|
|
37
|
+
Cloudflare Workers companion package for [`provide-uterm`](../../README.md). Runs the provide-uterm control plane on Cloudflare Workers using Durable Objects, with a fleet-wide session registry backed by Workers KV.
|
|
38
|
+
|
|
39
|
+
## What it does
|
|
40
|
+
|
|
41
|
+
Each terminal session gets its own Durable Object (`SessionRuntime`). The DO arbitrates WebSocket traffic between the runtime worker connector and browser clients, stores hijack leases and snapshots in SQLite, and publishes events to all connected browsers. A fleet-wide session list is maintained in Workers KV.
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install provide-uterm-cloudflare
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Or install from the monorepo with `uv`:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
uv pip install -e packages/provide-uterm-cloudflare
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Deploy
|
|
56
|
+
|
|
57
|
+
The worker entrypoint is `src/worker_entry.py` (referenced by `main` in
|
|
58
|
+
`wrangler.toml`). It lives at the package `src/` root on purpose: wrangler bundles
|
|
59
|
+
the directory of the `main` file, so anchoring it there preserves the full
|
|
60
|
+
`provide/uterm/cloudflare/` tree in the bundle and the worker's qualified imports
|
|
61
|
+
resolve as-is.
|
|
62
|
+
|
|
63
|
+
The Pyodide runtime also needs a flat vendor tree of the pure-Python deps the
|
|
64
|
+
worker imports (`structlog`, `provide.telemetry`, and the `provide.uterm.*`
|
|
65
|
+
modules). `pywrangler sync` produces a layout the worker can't import, so build it
|
|
66
|
+
with the helper script, then deploy with `wrangler` directly (not `pywrangler
|
|
67
|
+
deploy`, which would re-sync and overwrite the tree):
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
bash .ci/vendor_cf_worker.sh # build python_modules/
|
|
71
|
+
cd packages/provide-uterm-cloudflare
|
|
72
|
+
CLOUDFLARE_API_TOKEN=… npx wrangler deploy # publish
|
|
73
|
+
|
|
74
|
+
# Required secrets (AUTH_MODE is jwt-only; the worker 500s without these):
|
|
75
|
+
npx wrangler secret put WORKER_BEARER_TOKEN # >=32 high-entropy chars
|
|
76
|
+
npx wrangler secret put WEBHOOK_SECRET_KEY # base64 AES-256 key
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Key features
|
|
80
|
+
|
|
81
|
+
- **Durable Object per session** — `SessionRuntime` DO holds all session state (leases, snapshots, event sequence) in SQLite.
|
|
82
|
+
- **Fleet-wide session registry** — `SESSION_REGISTRY` Workers KV namespace; `GET /api/sessions` returns all active sessions across the fleet.
|
|
83
|
+
- **CF Access JWT auth** — validates Cloudflare Access JWTs via JWKS; `JWT_DEFAULT_ROLE` env var assigns a role when the JWT carries no role claim.
|
|
84
|
+
- **Hijack REST API** — `POST /hijack/{id}/acquire`, `POST /hijack/{id}/send`, `POST /hijack/{id}/release`, `GET /hijack/{id}/snapshot`.
|
|
85
|
+
- **WebSocket proxy** — three WS endpoints per session:
|
|
86
|
+
- `/ws/worker/{worker_id}/term` — runtime worker protocol (JSON frames)
|
|
87
|
+
- `/ws/browser/{worker_id}/term` — browser/operator protocol (JSON frames)
|
|
88
|
+
- `/ws/raw/{worker_id}/term` — raw stream mode for `uterm listen` telnet/SSH gateways
|
|
89
|
+
- **Hibernation-safe** — uses CF WebSocket Hibernation API; state survives DO sleep/wake cycles.
|
|
90
|
+
- **WS session resumption** — browser reconnects reclaim their role and hijack ownership via one-time tokens stored in DO SQLite; see `docs/cf-do-architecture.md`.
|
|
91
|
+
- **Quick-connect** — `POST /api/connect` creates sessions in KV; SPA serves the connect form at `/app/connect`. Supports shell, websocket, and ushell connector types.
|
|
92
|
+
|
|
93
|
+
## Auth modes
|
|
94
|
+
|
|
95
|
+
Set `AUTH_MODE` in `wrangler.toml` or `.dev.vars`. `jwt` is the **only**
|
|
96
|
+
supported value — the worker is always internet-facing, so any other mode
|
|
97
|
+
(`dev`/`none` are removed) raises a `ValueError` at config load.
|
|
98
|
+
|
|
99
|
+
| Mode | Behavior |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `jwt` | Validates CF Access JWT; role from claim or `JWT_DEFAULT_ROLE`. CF Access service-token JWTs (with a `common_name` claim and no human `email` claim) are accepted, but are only granted the admin role when `JWT_SERVICE_TOKEN_ADMIN=1` is set (defaults off); otherwise they get their roles from the normal claim/scope/default-role path. |
|
|
102
|
+
|
|
103
|
+
`WORKER_BEARER_TOKEN` is also required and must clear a 32-character /
|
|
104
|
+
non-placeholder entropy floor.
|
|
105
|
+
|
|
106
|
+
## Current gaps
|
|
107
|
+
|
|
108
|
+
- The quick-connect form creates sessions in KV but the CF worker cannot run
|
|
109
|
+
shell/SSH/telnet connectors itself — a worker process must bridge in via WS.
|
|
110
|
+
- The hijack REST surface is intended to match the FastAPI contract, but there
|
|
111
|
+
are still backend-parity gaps; treat `docs/protocol-matrix.md` as the target
|
|
112
|
+
contract, not a guarantee that every edge case is identical today.
|
|
113
|
+
|
|
114
|
+
## Commands
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
uv run pywrangler dev # local dev server (sync deps + wrangler dev)
|
|
118
|
+
uv run pywrangler deploy # deploy to Cloudflare
|
|
119
|
+
uterm-cf build # build only
|
|
120
|
+
uterm-cf deploy --env production
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Docker alternative
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
# Build and run from repo root
|
|
127
|
+
docker build -f docker/Dockerfile.cf -t provide-uterm-cf .
|
|
128
|
+
docker run --rm -p 27788:27788 provide-uterm-cf
|
|
129
|
+
|
|
130
|
+
# JWT auth test
|
|
131
|
+
docker run --rm -p 27788:27788 \
|
|
132
|
+
-e AUTH_MODE=jwt \
|
|
133
|
+
-e JWT_JWKS_URL=https://<team>.cloudflareaccess.com/cdn-cgi/access/certs \
|
|
134
|
+
-e JWT_ISSUER=https://<team>.cloudflareaccess.com \
|
|
135
|
+
-e JWT_AUDIENCE=<aud-tag> \
|
|
136
|
+
provide-uterm-cf
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Tests
|
|
140
|
+
|
|
141
|
+
Unit tests (no network required):
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
uv run pytest tests/ -v
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
E2E tests against a local `wrangler dev` instance or the live worker:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
E2E=1 uv run pytest -m e2e -v
|
|
151
|
+
REAL_CF=1 REAL_CF_URL=https://provide-uterm-cloudflare.neurotic.workers.dev uv run pytest -m e2e -v
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Related
|
|
155
|
+
|
|
156
|
+
- Main package: [`provide-uterm`](../../README.md)
|
|
157
|
+
- Terraform for KV provisioning: `terraform/`
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# provide-uterm-cloudflare
|
|
2
|
+
|
|
3
|
+
Cloudflare Workers companion package for [`provide-uterm`](../../README.md). Runs the provide-uterm control plane on Cloudflare Workers using Durable Objects, with a fleet-wide session registry backed by Workers KV.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
Each terminal session gets its own Durable Object (`SessionRuntime`). The DO arbitrates WebSocket traffic between the runtime worker connector and browser clients, stores hijack leases and snapshots in SQLite, and publishes events to all connected browsers. A fleet-wide session list is maintained in Workers KV.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install provide-uterm-cloudflare
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or install from the monorepo with `uv`:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
uv pip install -e packages/provide-uterm-cloudflare
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Deploy
|
|
22
|
+
|
|
23
|
+
The worker entrypoint is `src/worker_entry.py` (referenced by `main` in
|
|
24
|
+
`wrangler.toml`). It lives at the package `src/` root on purpose: wrangler bundles
|
|
25
|
+
the directory of the `main` file, so anchoring it there preserves the full
|
|
26
|
+
`provide/uterm/cloudflare/` tree in the bundle and the worker's qualified imports
|
|
27
|
+
resolve as-is.
|
|
28
|
+
|
|
29
|
+
The Pyodide runtime also needs a flat vendor tree of the pure-Python deps the
|
|
30
|
+
worker imports (`structlog`, `provide.telemetry`, and the `provide.uterm.*`
|
|
31
|
+
modules). `pywrangler sync` produces a layout the worker can't import, so build it
|
|
32
|
+
with the helper script, then deploy with `wrangler` directly (not `pywrangler
|
|
33
|
+
deploy`, which would re-sync and overwrite the tree):
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
bash .ci/vendor_cf_worker.sh # build python_modules/
|
|
37
|
+
cd packages/provide-uterm-cloudflare
|
|
38
|
+
CLOUDFLARE_API_TOKEN=… npx wrangler deploy # publish
|
|
39
|
+
|
|
40
|
+
# Required secrets (AUTH_MODE is jwt-only; the worker 500s without these):
|
|
41
|
+
npx wrangler secret put WORKER_BEARER_TOKEN # >=32 high-entropy chars
|
|
42
|
+
npx wrangler secret put WEBHOOK_SECRET_KEY # base64 AES-256 key
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Key features
|
|
46
|
+
|
|
47
|
+
- **Durable Object per session** — `SessionRuntime` DO holds all session state (leases, snapshots, event sequence) in SQLite.
|
|
48
|
+
- **Fleet-wide session registry** — `SESSION_REGISTRY` Workers KV namespace; `GET /api/sessions` returns all active sessions across the fleet.
|
|
49
|
+
- **CF Access JWT auth** — validates Cloudflare Access JWTs via JWKS; `JWT_DEFAULT_ROLE` env var assigns a role when the JWT carries no role claim.
|
|
50
|
+
- **Hijack REST API** — `POST /hijack/{id}/acquire`, `POST /hijack/{id}/send`, `POST /hijack/{id}/release`, `GET /hijack/{id}/snapshot`.
|
|
51
|
+
- **WebSocket proxy** — three WS endpoints per session:
|
|
52
|
+
- `/ws/worker/{worker_id}/term` — runtime worker protocol (JSON frames)
|
|
53
|
+
- `/ws/browser/{worker_id}/term` — browser/operator protocol (JSON frames)
|
|
54
|
+
- `/ws/raw/{worker_id}/term` — raw stream mode for `uterm listen` telnet/SSH gateways
|
|
55
|
+
- **Hibernation-safe** — uses CF WebSocket Hibernation API; state survives DO sleep/wake cycles.
|
|
56
|
+
- **WS session resumption** — browser reconnects reclaim their role and hijack ownership via one-time tokens stored in DO SQLite; see `docs/cf-do-architecture.md`.
|
|
57
|
+
- **Quick-connect** — `POST /api/connect` creates sessions in KV; SPA serves the connect form at `/app/connect`. Supports shell, websocket, and ushell connector types.
|
|
58
|
+
|
|
59
|
+
## Auth modes
|
|
60
|
+
|
|
61
|
+
Set `AUTH_MODE` in `wrangler.toml` or `.dev.vars`. `jwt` is the **only**
|
|
62
|
+
supported value — the worker is always internet-facing, so any other mode
|
|
63
|
+
(`dev`/`none` are removed) raises a `ValueError` at config load.
|
|
64
|
+
|
|
65
|
+
| Mode | Behavior |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `jwt` | Validates CF Access JWT; role from claim or `JWT_DEFAULT_ROLE`. CF Access service-token JWTs (with a `common_name` claim and no human `email` claim) are accepted, but are only granted the admin role when `JWT_SERVICE_TOKEN_ADMIN=1` is set (defaults off); otherwise they get their roles from the normal claim/scope/default-role path. |
|
|
68
|
+
|
|
69
|
+
`WORKER_BEARER_TOKEN` is also required and must clear a 32-character /
|
|
70
|
+
non-placeholder entropy floor.
|
|
71
|
+
|
|
72
|
+
## Current gaps
|
|
73
|
+
|
|
74
|
+
- The quick-connect form creates sessions in KV but the CF worker cannot run
|
|
75
|
+
shell/SSH/telnet connectors itself — a worker process must bridge in via WS.
|
|
76
|
+
- The hijack REST surface is intended to match the FastAPI contract, but there
|
|
77
|
+
are still backend-parity gaps; treat `docs/protocol-matrix.md` as the target
|
|
78
|
+
contract, not a guarantee that every edge case is identical today.
|
|
79
|
+
|
|
80
|
+
## Commands
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
uv run pywrangler dev # local dev server (sync deps + wrangler dev)
|
|
84
|
+
uv run pywrangler deploy # deploy to Cloudflare
|
|
85
|
+
uterm-cf build # build only
|
|
86
|
+
uterm-cf deploy --env production
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Docker alternative
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Build and run from repo root
|
|
93
|
+
docker build -f docker/Dockerfile.cf -t provide-uterm-cf .
|
|
94
|
+
docker run --rm -p 27788:27788 provide-uterm-cf
|
|
95
|
+
|
|
96
|
+
# JWT auth test
|
|
97
|
+
docker run --rm -p 27788:27788 \
|
|
98
|
+
-e AUTH_MODE=jwt \
|
|
99
|
+
-e JWT_JWKS_URL=https://<team>.cloudflareaccess.com/cdn-cgi/access/certs \
|
|
100
|
+
-e JWT_ISSUER=https://<team>.cloudflareaccess.com \
|
|
101
|
+
-e JWT_AUDIENCE=<aud-tag> \
|
|
102
|
+
provide-uterm-cf
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Tests
|
|
106
|
+
|
|
107
|
+
Unit tests (no network required):
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
uv run pytest tests/ -v
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
E2E tests against a local `wrangler dev` instance or the live worker:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
E2E=1 uv run pytest -m e2e -v
|
|
117
|
+
REAL_CF=1 REAL_CF_URL=https://provide-uterm-cloudflare.neurotic.workers.dev uv run pytest -m e2e -v
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Related
|
|
121
|
+
|
|
122
|
+
- Main package: [`provide-uterm`](../../README.md)
|
|
123
|
+
- Terraform for KV provisioning: `terraform/`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.5.0
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "provide-uterm-cloudflare"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Cloudflare Workers runtime for provide-uterm control plane"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "AGPL-3.0-or-later"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "provide.io llc"},
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
|
|
22
|
+
"Framework :: AsyncIO",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"pyjwt>=2.9",
|
|
26
|
+
"provide-uterm>=0.5.0",
|
|
27
|
+
"provide-uterm-server>=0.5.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
dev = [
|
|
32
|
+
"pytest>=9.0",
|
|
33
|
+
"pytest-asyncio>=1.3",
|
|
34
|
+
"pytest-cov>=7.0",
|
|
35
|
+
"pytest-playwright>=0.7.2",
|
|
36
|
+
"ruff>=0.15",
|
|
37
|
+
"mypy>=1.19",
|
|
38
|
+
"workers-py>=0.4.0",
|
|
39
|
+
"websockets>=16.0",
|
|
40
|
+
"cryptography>=44.0",
|
|
41
|
+
# The cross-compat tests boot a real FastAPI server and compare it against
|
|
42
|
+
# the Worker, and that server refuses to build without annotation support.
|
|
43
|
+
# The Worker runtime itself does not need it, so it belongs here rather than
|
|
44
|
+
# in this package's runtime dependencies.
|
|
45
|
+
#
|
|
46
|
+
# [server] as well as [annotation]: fastapi/uvicorn live under provide-uterm-
|
|
47
|
+
# server's server/cli/tunnel extras, not [annotation], so app/factory_impl.py
|
|
48
|
+
# raised ModuleNotFoundError: No module named 'fastapi' the moment the
|
|
49
|
+
# isolated env stopped receiving it transitively — the same failure mode the
|
|
50
|
+
# starlette note below describes.
|
|
51
|
+
"provide-uterm-server[annotation,server]>=0.5.0",
|
|
52
|
+
# tests/conftest.py imports starlette.testclient directly, and TestClient
|
|
53
|
+
# needs httpx2 at import time. Neither is reachable through the line above:
|
|
54
|
+
# provide-uterm-server declares fastapi under its server/cli/tunnel extras,
|
|
55
|
+
# not under [annotation]. Both arrived transitively until 2026-08-06, when
|
|
56
|
+
# the same commit that passed on 08-05 began failing CI's isolated
|
|
57
|
+
# `uv run --package provide-uterm-cloudflare --extra dev` env with
|
|
58
|
+
# ModuleNotFoundError: No module named 'starlette'. A test's direct import
|
|
59
|
+
# belongs in its own package's dev extra rather than inherited by luck.
|
|
60
|
+
"starlette>=0.40",
|
|
61
|
+
"httpx2>=2.10",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
[project.urls]
|
|
65
|
+
Homepage = "https://github.com/provide-io/provide-uterm"
|
|
66
|
+
Repository = "https://github.com/provide-io/provide-uterm"
|
|
67
|
+
|
|
68
|
+
[project.scripts]
|
|
69
|
+
uterm-cf = "provide.uterm.cloudflare.cli:main"
|
|
70
|
+
|
|
71
|
+
[tool.setuptools.dynamic]
|
|
72
|
+
version = {file = "VERSION"}
|
|
73
|
+
|
|
74
|
+
[tool.setuptools.packages.find]
|
|
75
|
+
where = ["src"]
|
|
76
|
+
namespaces = true
|
|
77
|
+
|
|
78
|
+
[tool.setuptools.package-data]
|
|
79
|
+
"provide.uterm.cloudflare" = ["py.typed"]
|
|
80
|
+
|
|
81
|
+
[tool.pytest.ini_options]
|
|
82
|
+
consider_namespace_packages = true
|
|
83
|
+
asyncio_mode = "auto"
|
|
84
|
+
pythonpath = ["src"]
|
|
85
|
+
markers = [
|
|
86
|
+
"e2e: end-to-end tests against a live pywrangler dev server (skipped by default; use -m e2e or E2E=1)",
|
|
87
|
+
"real_cf: requires real Cloudflare deployment with live KV + full WS support (skipped unless REAL_CF=1)",
|
|
88
|
+
"slow: slow tests (>10s); skipped unless SLOW=1 or REAL_CF=1",
|
|
89
|
+
"playwright: Playwright browser UI tests",
|
|
90
|
+
]
|
|
91
|
+
addopts = [
|
|
92
|
+
"--cov=provide.uterm.cloudflare",
|
|
93
|
+
"--cov-branch",
|
|
94
|
+
"--cov-report=term-missing",
|
|
95
|
+
"--cov-fail-under=100",
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
[tool.coverage.run]
|
|
99
|
+
source = ["provide.uterm.cloudflare"]
|
|
100
|
+
branch = true
|
|
101
|
+
|
|
102
|
+
[tool.coverage.report]
|
|
103
|
+
fail_under = 100
|
|
104
|
+
show_missing = true
|
|
105
|
+
skip_covered = false
|
|
106
|
+
[tool.mutmut]
|
|
107
|
+
source_paths = ["src/"]
|
|
108
|
+
also_copy = ["wrangler.toml"]
|
|
109
|
+
pytest_add_cli_args = ["-o", "addopts=", "--no-header", "-q"]
|
|
110
|
+
pytest_add_cli_args_test_selection = ["tests/"]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
__version__ = version("provide-uterm-cloudflare")
|
|
5
|
+
except PackageNotFoundError: # pragma: no cover
|
|
6
|
+
__version__ = "0.0.0" # pragma: no cover
|
|
7
|
+
|
|
8
|
+
from .config import CloudflareConfig
|
|
9
|
+
|
|
10
|
+
__all__ = ["CloudflareConfig", "__version__"]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
__all__ = ["route_http"]
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def __getattr__(name: str) -> object: # pragma: no cover
|
|
5
|
+
"""Lazy import to avoid module loading issues during Pyodide validation."""
|
|
6
|
+
if name == "route_http":
|
|
7
|
+
from .http_routes import route_http
|
|
8
|
+
|
|
9
|
+
return route_http
|
|
10
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 provide.io llc. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
"""
|
|
4
|
+
POST /api/pam-events — accept PAM session notifications from a local bridge.
|
|
5
|
+
|
|
6
|
+
The pam_uterm.so module writes to a Unix socket on the SSH host. A local
|
|
7
|
+
bridge (e.g. scripts/deckmux_demo_server.py or a dedicated forwarder) reads
|
|
8
|
+
those events and POSTs them here so the operator dashboard can reflect
|
|
9
|
+
live SSH sessions.
|
|
10
|
+
|
|
11
|
+
Wire format (same JSON as pam_uterm.so):
|
|
12
|
+
{"event":"open", "username":"alice","tty":"/dev/pts/3","pid":12345,"mode":"notify"}
|
|
13
|
+
{"event":"close", "username":"alice","tty":"/dev/pts/3","pid":12345}
|
|
14
|
+
|
|
15
|
+
On "open" (notify mode), a read-only observer session entry is written to KV.
|
|
16
|
+
On "close", the session is removed from KV.
|
|
17
|
+
|
|
18
|
+
Capture mode (LD_PRELOAD) is a local-server-only capability — the CF edge
|
|
19
|
+
cannot receive a Unix socket connection from the SSH host, so capture events
|
|
20
|
+
are accepted but treated identically to notify events (session visible in
|
|
21
|
+
dashboard but no live I/O).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import re
|
|
27
|
+
import time
|
|
28
|
+
from typing import TYPE_CHECKING, Any
|
|
29
|
+
|
|
30
|
+
_TTY_SLUG_RE = re.compile(r"[^a-zA-Z0-9]+")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _tty_slug(tty: str) -> str:
|
|
34
|
+
"""/dev/pts/3 → last component slug."""
|
|
35
|
+
basename = tty.split("/")[-1] if "/" in tty else tty
|
|
36
|
+
return _TTY_SLUG_RE.sub("-", basename).strip("-") or "tty"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async def handle_pam_event(request: object, env: object) -> object:
|
|
40
|
+
"""Handle POST /api/pam-events."""
|
|
41
|
+
if TYPE_CHECKING:
|
|
42
|
+
from provide.uterm.cloudflare.cf_types import json_response
|
|
43
|
+
else:
|
|
44
|
+
try:
|
|
45
|
+
from provide.uterm.cloudflare.cf_types import json_response
|
|
46
|
+
except ImportError: # pragma: no cover
|
|
47
|
+
from cf_types import json_response # type: ignore[import-not-found,no-redef]
|
|
48
|
+
|
|
49
|
+
method = str(getattr(request, "method", "GET")).upper()
|
|
50
|
+
if method != "POST":
|
|
51
|
+
return json_response({"error": "method_not_allowed"}, status=405)
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
raw = await request.json() # type: ignore[attr-defined] # ty:ignore[unresolved-attribute]
|
|
55
|
+
body: dict[str, Any] = raw.to_py() if hasattr(raw, "to_py") else raw
|
|
56
|
+
except Exception:
|
|
57
|
+
return json_response({"error": "invalid_json"}, status=400)
|
|
58
|
+
|
|
59
|
+
event = str(body.get("event") or "")
|
|
60
|
+
if event not in ("open", "close"):
|
|
61
|
+
return json_response({"error": "unknown_event", "event": event}, status=422)
|
|
62
|
+
|
|
63
|
+
username = str(body.get("username") or "")
|
|
64
|
+
if not username:
|
|
65
|
+
return json_response({"error": "missing_username"}, status=422)
|
|
66
|
+
|
|
67
|
+
tty = str(body.get("tty") or "")
|
|
68
|
+
slug = _tty_slug(tty)
|
|
69
|
+
session_id = f"pam-{username}-{slug}"
|
|
70
|
+
|
|
71
|
+
kv = getattr(env, "SESSION_REGISTRY", None)
|
|
72
|
+
|
|
73
|
+
if event == "open":
|
|
74
|
+
entry: dict[str, Any] = {
|
|
75
|
+
"session_id": session_id,
|
|
76
|
+
"display_name": f"{username} ({tty or 'pam'})",
|
|
77
|
+
"created_at": time.time(),
|
|
78
|
+
"connector_type": "shell",
|
|
79
|
+
"lifecycle_state": "running",
|
|
80
|
+
"input_mode": "open",
|
|
81
|
+
"connected": True,
|
|
82
|
+
"auto_start": False,
|
|
83
|
+
"tags": ["pam", str(body.get("mode") or "notify"), username],
|
|
84
|
+
"recording_enabled": False,
|
|
85
|
+
"recording_available": False,
|
|
86
|
+
"owner": username,
|
|
87
|
+
"visibility": "operator",
|
|
88
|
+
"last_error": None,
|
|
89
|
+
}
|
|
90
|
+
if kv is not None:
|
|
91
|
+
import json as _json
|
|
92
|
+
|
|
93
|
+
await kv.put(f"session:{session_id}", _json.dumps({**entry, "hijacked": False}))
|
|
94
|
+
return json_response({"ok": True, "session_id": session_id, "action": "created"})
|
|
95
|
+
|
|
96
|
+
# event == "close"
|
|
97
|
+
if kv is not None:
|
|
98
|
+
await kv.delete(f"session:{session_id}")
|
|
99
|
+
return json_response({"ok": True, "session_id": session_id, "action": "deleted"})
|