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.
Files changed (170) hide show
  1. provide_uterm_cloudflare-0.5.0/PKG-INFO +157 -0
  2. provide_uterm_cloudflare-0.5.0/README.md +123 -0
  3. provide_uterm_cloudflare-0.5.0/VERSION +1 -0
  4. provide_uterm_cloudflare-0.5.0/pyproject.toml +110 -0
  5. provide_uterm_cloudflare-0.5.0/setup.cfg +4 -0
  6. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/__init__.py +10 -0
  7. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/__main__.py +4 -0
  8. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/api/__init__.py +10 -0
  9. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/api/_pam.py +99 -0
  10. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/api/_profiles.py +240 -0
  11. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/api/_tunnel_api.py +412 -0
  12. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/api/http_routes/__init__.py +13 -0
  13. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/api/http_routes/_dispatch.py +140 -0
  14. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/api/http_routes/_hijack.py +308 -0
  15. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/api/http_routes/_recording.py +109 -0
  16. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/api/http_routes/_session.py +390 -0
  17. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/api/http_routes/_shared.py +258 -0
  18. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/api/tunnel_routes.py +116 -0
  19. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/api/ws_routes.py +429 -0
  20. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/auth/__init__.py +3 -0
  21. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/auth/jwt.py +404 -0
  22. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/bridge/__init__.py +3 -0
  23. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/bridge/hijack.py +18 -0
  24. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/cf_types.py +99 -0
  25. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/cli.py +82 -0
  26. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/config.py +300 -0
  27. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/contracts.py +286 -0
  28. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/do/__init__.py +1 -0
  29. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/do/_sse.py +99 -0
  30. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/do/_webhook_crypto.py +133 -0
  31. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/do/_webhooks.py +280 -0
  32. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/do/persistence.py +73 -0
  33. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/do/session_runtime/__init__.py +32 -0
  34. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/do/session_runtime/auth.py +193 -0
  35. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/do/session_runtime/fetch.py +415 -0
  36. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/do/session_runtime/flow_control.py +135 -0
  37. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/do/session_runtime/io.py +638 -0
  38. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/do/session_runtime/lifecycle.py +266 -0
  39. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/do/session_runtime/runtime.py +223 -0
  40. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/do/session_runtime/ws_helpers.py +327 -0
  41. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/do/ushell.py +167 -0
  42. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/entry/__init__.py +57 -0
  43. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/entry/auth.py +118 -0
  44. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/entry/fallback_stubs.py +185 -0
  45. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/entry/handlers.py +337 -0
  46. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/entry/registry.py +63 -0
  47. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/entry/route_defs.py +154 -0
  48. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/entry/security.py +89 -0
  49. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/entry/share_tokens.py +58 -0
  50. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/entry/spa.py +89 -0
  51. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/py.typed +0 -0
  52. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/state/__init__.py +3 -0
  53. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/state/_row_utils.py +79 -0
  54. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/state/registry.py +194 -0
  55. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/state/store.py +576 -0
  56. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/ui/__init__.py +3 -0
  57. provide_uterm_cloudflare-0.5.0/src/provide/uterm/cloudflare/ui/assets.py +89 -0
  58. provide_uterm_cloudflare-0.5.0/src/provide_uterm_cloudflare.egg-info/PKG-INFO +157 -0
  59. provide_uterm_cloudflare-0.5.0/src/provide_uterm_cloudflare.egg-info/SOURCES.txt +168 -0
  60. provide_uterm_cloudflare-0.5.0/src/provide_uterm_cloudflare.egg-info/dependency_links.txt +1 -0
  61. provide_uterm_cloudflare-0.5.0/src/provide_uterm_cloudflare.egg-info/entry_points.txt +2 -0
  62. provide_uterm_cloudflare-0.5.0/src/provide_uterm_cloudflare.egg-info/requires.txt +17 -0
  63. provide_uterm_cloudflare-0.5.0/src/provide_uterm_cloudflare.egg-info/top_level.txt +1 -0
  64. provide_uterm_cloudflare-0.5.0/tests/test_alarm.py +86 -0
  65. provide_uterm_cloudflare-0.5.0/tests/test_api_contracts.py +425 -0
  66. provide_uterm_cloudflare-0.5.0/tests/test_api_contracts_2.py +172 -0
  67. provide_uterm_cloudflare-0.5.0/tests/test_auth_jwt.py +427 -0
  68. provide_uterm_cloudflare-0.5.0/tests/test_backpressure_wiring.py +143 -0
  69. provide_uterm_cloudflare-0.5.0/tests/test_branch_coverage.py +27 -0
  70. provide_uterm_cloudflare-0.5.0/tests/test_branch_coverage_2.py +160 -0
  71. provide_uterm_cloudflare-0.5.0/tests/test_branch_coverage_part1.py +397 -0
  72. provide_uterm_cloudflare-0.5.0/tests/test_branch_coverage_part2.py +378 -0
  73. provide_uterm_cloudflare-0.5.0/tests/test_browser_ws_visibility.py +266 -0
  74. provide_uterm_cloudflare-0.5.0/tests/test_cf_cli.py +185 -0
  75. provide_uterm_cloudflare-0.5.0/tests/test_cf_resume.py +455 -0
  76. provide_uterm_cloudflare-0.5.0/tests/test_conformance_parity.py +137 -0
  77. provide_uterm_cloudflare-0.5.0/tests/test_contracts.py +207 -0
  78. provide_uterm_cloudflare-0.5.0/tests/test_cookie_only_coverage.py +292 -0
  79. provide_uterm_cloudflare-0.5.0/tests/test_coverage2.py +340 -0
  80. provide_uterm_cloudflare-0.5.0/tests/test_coverage3.py +441 -0
  81. provide_uterm_cloudflare-0.5.0/tests/test_coverage3_2.py +248 -0
  82. provide_uterm_cloudflare-0.5.0/tests/test_coverage_entry_registry.py +28 -0
  83. provide_uterm_cloudflare-0.5.0/tests/test_coverage_entry_registry_part1.py +182 -0
  84. provide_uterm_cloudflare-0.5.0/tests/test_coverage_entry_registry_part2.py +215 -0
  85. provide_uterm_cloudflare-0.5.0/tests/test_coverage_entry_registry_part3.py +236 -0
  86. provide_uterm_cloudflare-0.5.0/tests/test_coverage_gaps.py +28 -0
  87. provide_uterm_cloudflare-0.5.0/tests/test_coverage_gaps_part1.py +177 -0
  88. provide_uterm_cloudflare-0.5.0/tests/test_coverage_gaps_part2.py +451 -0
  89. provide_uterm_cloudflare-0.5.0/tests/test_coverage_gaps_part3.py +216 -0
  90. provide_uterm_cloudflare-0.5.0/tests/test_coverage_jwt.py +427 -0
  91. provide_uterm_cloudflare-0.5.0/tests/test_coverage_session_runtime.py +150 -0
  92. provide_uterm_cloudflare-0.5.0/tests/test_credentials_reload.py +284 -0
  93. provide_uterm_cloudflare-0.5.0/tests/test_cross_compat.py +300 -0
  94. provide_uterm_cloudflare-0.5.0/tests/test_deckmux_presence.py +322 -0
  95. provide_uterm_cloudflare-0.5.0/tests/test_do_route_defs.py +417 -0
  96. provide_uterm_cloudflare-0.5.0/tests/test_do_ushell.py +355 -0
  97. provide_uterm_cloudflare-0.5.0/tests/test_e2e_full_stack.py +346 -0
  98. provide_uterm_cloudflare-0.5.0/tests/test_e2e_pam_relay.py +192 -0
  99. provide_uterm_cloudflare-0.5.0/tests/test_e2e_playwright_proxy.py +252 -0
  100. provide_uterm_cloudflare-0.5.0/tests/test_e2e_recording.py +187 -0
  101. provide_uterm_cloudflare-0.5.0/tests/test_e2e_sse_webhooks.py +493 -0
  102. provide_uterm_cloudflare-0.5.0/tests/test_e2e_tunnel.py +230 -0
  103. provide_uterm_cloudflare-0.5.0/tests/test_e2e_wrangler.py +73 -0
  104. provide_uterm_cloudflare-0.5.0/tests/test_e2e_ws.py +464 -0
  105. provide_uterm_cloudflare-0.5.0/tests/test_entry.py +11 -0
  106. provide_uterm_cloudflare-0.5.0/tests/test_entry_unit.py +28 -0
  107. provide_uterm_cloudflare-0.5.0/tests/test_entry_unit_auth_part2.py +153 -0
  108. provide_uterm_cloudflare-0.5.0/tests/test_entry_unit_part1.py +38 -0
  109. provide_uterm_cloudflare-0.5.0/tests/test_entry_unit_part2.py +450 -0
  110. provide_uterm_cloudflare-0.5.0/tests/test_entry_unit_part3.py +159 -0
  111. provide_uterm_cloudflare-0.5.0/tests/test_fallback_imports.py +319 -0
  112. provide_uterm_cloudflare-0.5.0/tests/test_finding_15.py +16 -0
  113. provide_uterm_cloudflare-0.5.0/tests/test_flow_control.py +164 -0
  114. provide_uterm_cloudflare-0.5.0/tests/test_hibernate_wake_contract.py +259 -0
  115. provide_uterm_cloudflare-0.5.0/tests/test_hijack.py +29 -0
  116. provide_uterm_cloudflare-0.5.0/tests/test_http_channel.py +65 -0
  117. provide_uterm_cloudflare-0.5.0/tests/test_http_routes_coverage.py +481 -0
  118. provide_uterm_cloudflare-0.5.0/tests/test_http_routes_coverage2.py +299 -0
  119. provide_uterm_cloudflare-0.5.0/tests/test_http_routes_coverage_2.py +27 -0
  120. provide_uterm_cloudflare-0.5.0/tests/test_http_routes_coverage_2_part1.py +361 -0
  121. provide_uterm_cloudflare-0.5.0/tests/test_http_routes_coverage_2_part2.py +276 -0
  122. provide_uterm_cloudflare-0.5.0/tests/test_http_routes_coverage_3.py +208 -0
  123. provide_uterm_cloudflare-0.5.0/tests/test_http_routes_csrf.py +75 -0
  124. provide_uterm_cloudflare-0.5.0/tests/test_jwks_resilience.py +104 -0
  125. provide_uterm_cloudflare-0.5.0/tests/test_owned_input_fencing.py +219 -0
  126. provide_uterm_cloudflare-0.5.0/tests/test_owned_input_fencing_hijack_tunnel.py +538 -0
  127. provide_uterm_cloudflare-0.5.0/tests/test_owned_input_fencing_worker_lifecycle.py +304 -0
  128. provide_uterm_cloudflare-0.5.0/tests/test_pam_endpoint.py +185 -0
  129. provide_uterm_cloudflare-0.5.0/tests/test_persistence.py +140 -0
  130. provide_uterm_cloudflare-0.5.0/tests/test_profiles.py +451 -0
  131. provide_uterm_cloudflare-0.5.0/tests/test_recording.py +27 -0
  132. provide_uterm_cloudflare-0.5.0/tests/test_recording_part1.py +361 -0
  133. provide_uterm_cloudflare-0.5.0/tests/test_recording_part2.py +231 -0
  134. provide_uterm_cloudflare-0.5.0/tests/test_registry.py +222 -0
  135. provide_uterm_cloudflare-0.5.0/tests/test_security_hardening.py +383 -0
  136. provide_uterm_cloudflare-0.5.0/tests/test_security_headers.py +296 -0
  137. provide_uterm_cloudflare-0.5.0/tests/test_security_page_routes.py +110 -0
  138. provide_uterm_cloudflare-0.5.0/tests/test_session_lifecycle_security_scenarios.py +756 -0
  139. provide_uterm_cloudflare-0.5.0/tests/test_session_runtime_coverage.py +365 -0
  140. provide_uterm_cloudflare-0.5.0/tests/test_session_runtime_unit.py +27 -0
  141. provide_uterm_cloudflare-0.5.0/tests/test_session_runtime_unit_2.py +471 -0
  142. provide_uterm_cloudflare-0.5.0/tests/test_session_runtime_unit_3.py +494 -0
  143. provide_uterm_cloudflare-0.5.0/tests/test_session_runtime_unit_part1.py +364 -0
  144. provide_uterm_cloudflare-0.5.0/tests/test_session_runtime_unit_part2.py +388 -0
  145. provide_uterm_cloudflare-0.5.0/tests/test_session_visibility.py +307 -0
  146. provide_uterm_cloudflare-0.5.0/tests/test_share_token_ttl.py +157 -0
  147. provide_uterm_cloudflare-0.5.0/tests/test_share_tokens.py +138 -0
  148. provide_uterm_cloudflare-0.5.0/tests/test_spa_sri.py +25 -0
  149. provide_uterm_cloudflare-0.5.0/tests/test_sse.py +177 -0
  150. provide_uterm_cloudflare-0.5.0/tests/test_store.py +57 -0
  151. provide_uterm_cloudflare-0.5.0/tests/test_store_coverage.py +292 -0
  152. provide_uterm_cloudflare-0.5.0/tests/test_tunnel_integration.py +470 -0
  153. provide_uterm_cloudflare-0.5.0/tests/test_tunnel_invite_do.py +158 -0
  154. provide_uterm_cloudflare-0.5.0/tests/test_tunnel_routes.py +28 -0
  155. provide_uterm_cloudflare-0.5.0/tests/test_tunnel_routes_part1.py +404 -0
  156. provide_uterm_cloudflare-0.5.0/tests/test_tunnel_routes_part2.py +383 -0
  157. provide_uterm_cloudflare-0.5.0/tests/test_tunnel_routes_part3.py +136 -0
  158. provide_uterm_cloudflare-0.5.0/tests/test_ui_assets.py +265 -0
  159. provide_uterm_cloudflare-0.5.0/tests/test_ushell_vendor_guard.py +41 -0
  160. provide_uterm_cloudflare-0.5.0/tests/test_webhook_crypto.py +86 -0
  161. provide_uterm_cloudflare-0.5.0/tests/test_webhook_mutate_gate.py +81 -0
  162. provide_uterm_cloudflare-0.5.0/tests/test_webhooks.py +27 -0
  163. provide_uterm_cloudflare-0.5.0/tests/test_webhooks_dispatch.py +126 -0
  164. provide_uterm_cloudflare-0.5.0/tests/test_webhooks_part1.py +97 -0
  165. provide_uterm_cloudflare-0.5.0/tests/test_webhooks_part2.py +515 -0
  166. provide_uterm_cloudflare-0.5.0/tests/test_worker_route_defs.py +247 -0
  167. provide_uterm_cloudflare-0.5.0/tests/test_ws_helpers.py +277 -0
  168. provide_uterm_cloudflare-0.5.0/tests/test_ws_routes.py +27 -0
  169. provide_uterm_cloudflare-0.5.0/tests/test_ws_routes_part1.py +472 -0
  170. 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__": # pragma: no cover
4
+ raise SystemExit(main()) # pragma: no cover
@@ -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"})