create-workframe 0.1.11 → 0.1.13

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 (195) hide show
  1. package/README.md +1 -1
  2. package/bin/create-workframe.js +2 -2
  3. package/bin/workframe.js +130 -2
  4. package/docs/security.md +85 -0
  5. package/package.json +1 -1
  6. package/scripts/bundle-workframe-ui.mjs +9 -0
  7. package/scripts/sync-canonical-to-package.mjs +9 -0
  8. package/scripts/verify-public-deploy.sh +121 -3
  9. package/workframe-api/action_proxy.py +23 -17
  10. package/workframe-api/activity_feed.py +798 -0
  11. package/workframe-api/api_meta.py +157 -0
  12. package/workframe-api/auth_gate.py +566 -0
  13. package/workframe-api/avatar_registry.py +343 -0
  14. package/workframe-api/broker_audit.py +164 -0
  15. package/workframe-api/cell_authority.py +231 -0
  16. package/workframe-api/chat_bind.py +319 -0
  17. package/workframe-api/chat_sessions.py +509 -0
  18. package/workframe-api/chat_stream.py +694 -0
  19. package/workframe-api/cleanup_dogfood_smoke.py +252 -0
  20. package/workframe-api/credential_broker.py +162 -0
  21. package/workframe-api/credential_resolve.py +202 -0
  22. package/workframe-api/credential_store.py +245 -0
  23. package/workframe-api/crew_registry.py +250 -0
  24. package/workframe-api/db_schema.py +755 -0
  25. package/workframe-api/docker_gateway.py +166 -0
  26. package/workframe-api/doctor_runtime.py +109 -0
  27. package/workframe-api/domain/__init__.py +47 -0
  28. package/workframe-api/domain/entities.py +252 -0
  29. package/workframe-api/domain/schema/workframe-domain.schema.json +278 -0
  30. package/workframe-api/egress_policy.py +42 -0
  31. package/workframe-api/handler_modules/__init__.py +17 -0
  32. package/workframe-api/handler_modules/handler_admin.py +542 -0
  33. package/workframe-api/handler_modules/handler_auth.py +512 -0
  34. package/workframe-api/handler_modules/handler_chat.py +641 -0
  35. package/workframe-api/handler_modules/handler_install.py +178 -0
  36. package/workframe-api/handler_modules/handler_provider.py +325 -0
  37. package/workframe-api/handler_modules/handler_workspace.py +1420 -0
  38. package/workframe-api/health_monitor.py +80 -0
  39. package/workframe-api/hermes_admin.py +756 -0
  40. package/workframe-api/hermes_profiles.py +1328 -0
  41. package/workframe-api/install_api.py +123 -0
  42. package/workframe-api/kanban_cron.py +473 -0
  43. package/workframe-api/lane_bindings.py +423 -0
  44. package/workframe-api/llm_proxy.py +17 -43
  45. package/workframe-api/mention_helpers.py +180 -0
  46. package/workframe-api/mention_invoke.py +431 -0
  47. package/workframe-api/model_surface.py +1139 -0
  48. package/workframe-api/oauth_pending.py +85 -0
  49. package/workframe-api/oauth_redirect.py +381 -0
  50. package/workframe-api/openrouter_catalog.py +4 -4
  51. package/workframe-api/package.json +2 -2
  52. package/workframe-api/profile_api_lifecycle.py +66 -0
  53. package/workframe-api/profile_config_yaml.py +126 -0
  54. package/workframe-api/profile_gateway.py +436 -0
  55. package/workframe-api/provider_bindings.py +673 -0
  56. package/workframe-api/provider_bootstrap.py +347 -0
  57. package/workframe-api/provider_catalog.py +180 -0
  58. package/workframe-api/rooms.py +1613 -0
  59. package/workframe-api/route_registry.py +470 -0
  60. package/workframe-api/run-typecheck.mjs +26 -0
  61. package/workframe-api/run_authority.py +287 -0
  62. package/workframe-api/run_ledger.py +470 -0
  63. package/workframe-api/run_surface_wiring.py +344 -0
  64. package/workframe-api/runtime_cohort.py +752 -0
  65. package/workframe-api/runtime_tokens.py +127 -0
  66. package/workframe-api/server.py +1376 -19305
  67. package/workframe-api/snapshot_feed.py +49 -0
  68. package/workframe-api/stack_config.py +33 -1
  69. package/workframe-api/supervisor_client.py +154 -0
  70. package/workframe-api/test_api_meta_build_stamp.py +34 -0
  71. package/workframe-api/test_cell_authority.py +79 -0
  72. package/workframe-api/test_chat_bind.py +48 -0
  73. package/workframe-api/test_credential_lease_matrix.py +171 -0
  74. package/workframe-api/test_credential_resolve.py +51 -0
  75. package/workframe-api/test_credential_store.py +27 -0
  76. package/workframe-api/test_dogfood_flows.py +346 -0
  77. package/workframe-api/test_domain_entities.py +152 -0
  78. package/workframe-api/test_exception_hygiene.py +12 -0
  79. package/workframe-api/test_hermes_admin.py +72 -0
  80. package/workframe-api/test_install_wizard_resume.py +78 -0
  81. package/workframe-api/test_local_bootstrap.py +67 -0
  82. package/workframe-api/test_mention_helpers.py +35 -0
  83. package/workframe-api/test_mention_invoke.py +26 -0
  84. package/workframe-api/test_model_surface_consistency.py +117 -0
  85. package/workframe-api/test_oauth_pending.py +41 -0
  86. package/workframe-api/test_profile_model_yaml.py +99 -0
  87. package/workframe-api/test_provider_bindings.py +52 -0
  88. package/workframe-api/test_provider_catalog.py +28 -0
  89. package/workframe-api/test_public_routes.py +41 -0
  90. package/workframe-api/test_route_registry.py +245 -0
  91. package/workframe-api/test_run_authority.py +112 -0
  92. package/workframe-api/test_run_ledger.py +157 -0
  93. package/workframe-api/test_run_surface_wiring.py +130 -0
  94. package/workframe-api/test_runtime_cohort.py +85 -0
  95. package/workframe-api/test_secure_mode_docker_boundary.py +39 -0
  96. package/workframe-api/test_server_reexports.py +99 -0
  97. package/workframe-api/test_stack_config_install_smtp.py +7 -0
  98. package/workframe-api/time-bind-steps.py +75 -0
  99. package/workframe-api/time-perf.py +81 -0
  100. package/workframe-api/time-warm.py +107 -0
  101. package/workframe-api/turn_credentials.py +28 -13
  102. package/workframe-api/turn_overlay.py +715 -0
  103. package/workframe-api/updates.py +16 -1
  104. package/workframe-api/user_prefs.py +387 -0
  105. package/workframe-api/wf032_extract_remaining_handlers.py +417 -0
  106. package/workframe-api/workspace_bootstrap.py +536 -0
  107. package/workframe-api/workspace_files.py +344 -0
  108. package/workframe-api/workspace_messaging.py +206 -0
  109. package/workframe-api/zk_auth.py +3 -2
  110. package/workframe-supervisor/server.py +10 -1
  111. package/workframe-supervisor/test_supervisor_negative.py +75 -0
  112. package/workframe-ui/public/assets/{arc-DjpS9a2b.js → arc-aUUffclm.js} +1 -1
  113. package/workframe-ui/public/assets/architecture-7EHR7CIX-BSUP4S8J.js +1 -0
  114. package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-puCKmKHV.js → architectureDiagram-3BPJPVTR-DU6oaqIb.js} +1 -1
  115. package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-C2P0g-eH.js → blockDiagram-GPEHLZMM-D2p8BU6-.js} +1 -1
  116. package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-DdvYEITH.js → c4Diagram-AAUBKEIU-ouwyPmTJ.js} +1 -1
  117. package/workframe-ui/public/assets/channel-DOunZZ0X.js +1 -0
  118. package/workframe-ui/public/assets/{chunk-2J33WTMH-Cxe8QsEF.js → chunk-2J33WTMH-DsE7U5dj.js} +1 -1
  119. package/workframe-ui/public/assets/{chunk-3OPIFGDE-DPNyTwR3.js → chunk-3OPIFGDE-DM0kkZ9h.js} +1 -1
  120. package/workframe-ui/public/assets/{chunk-4BX2VUAB-XKG9gfRN.js → chunk-4BX2VUAB-CQ8mAyXp.js} +1 -1
  121. package/workframe-ui/public/assets/{chunk-55IACEB6-oPMTulgb.js → chunk-55IACEB6-DN1BDtbH.js} +1 -1
  122. package/workframe-ui/public/assets/{chunk-5ZQYHXKU-BWwXvpa_.js → chunk-5ZQYHXKU-BzK2KqOt.js} +1 -1
  123. package/workframe-ui/public/assets/{chunk-727SXJPM-Cml2twO_.js → chunk-727SXJPM-C3AxtOi9.js} +1 -1
  124. package/workframe-ui/public/assets/{chunk-AQP2D5EJ-DzrRBhQu.js → chunk-AQP2D5EJ-4mVwEFuD.js} +1 -1
  125. package/workframe-ui/public/assets/{chunk-BSJP7CBP-EZ7STU6h.js → chunk-BSJP7CBP-23kN2q0A.js} +1 -1
  126. package/workframe-ui/public/assets/{chunk-CSCIHK7Q-BGTQzWYw.js → chunk-CSCIHK7Q-CPBKVW-L.js} +2 -2
  127. package/workframe-ui/public/assets/{chunk-FMBD7UC4-Cm4stfRa.js → chunk-FMBD7UC4-CKqcYq7K.js} +1 -1
  128. package/workframe-ui/public/assets/{chunk-KSCS5N6A-exbMijjy.js → chunk-KSCS5N6A-JRQnhxQE.js} +1 -1
  129. package/workframe-ui/public/assets/{chunk-L5ZTLDWV-C75kbQwN.js → chunk-L5ZTLDWV-BLZrdIwa.js} +1 -1
  130. package/workframe-ui/public/assets/chunk-LZXEDZCA-DisG0XJT.js +2 -0
  131. package/workframe-ui/public/assets/{chunk-ND2GUHAM-D84zyIu2.js → chunk-ND2GUHAM-ClAOxArS.js} +1 -1
  132. package/workframe-ui/public/assets/{chunk-NZK2D7GU-CUNNo1bj.js → chunk-NZK2D7GU-XUE1aNkm.js} +1 -1
  133. package/workframe-ui/public/assets/{chunk-O5CBEL6O-DN6WEIh-.js → chunk-O5CBEL6O-DDN-Ifon.js} +1 -1
  134. package/workframe-ui/public/assets/chunk-QZHKN3VN-EVQ0VMd5.js +1 -0
  135. package/workframe-ui/public/assets/chunk-WU5MYG2G-k_-ZsLOS.js +1 -0
  136. package/workframe-ui/public/assets/{chunk-XPW4576I-Ct5VQv2D.js → chunk-XPW4576I-B4_iYZ6r.js} +1 -1
  137. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-W9WVQsby.js +1 -0
  138. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-W9WVQsby.js +1 -0
  139. package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-Bj4W5oMk.js → cose-bilkent-S5V4N54A-Cy1HhS7j.js} +1 -1
  140. package/workframe-ui/public/assets/{dagre-BM42HDAG-DAjHBkAy.js → dagre-BM42HDAG-Bjal81Ie.js} +1 -1
  141. package/workframe-ui/public/assets/{diagram-2AECGRRQ-Ch2gGFPG.js → diagram-2AECGRRQ-v5Gdvzp_.js} +1 -1
  142. package/workframe-ui/public/assets/{diagram-5GNKFQAL-BzIIvgYH.js → diagram-5GNKFQAL-DuyRdfmY.js} +1 -1
  143. package/workframe-ui/public/assets/{diagram-KO2AKTUF-Df0Q8nv6.js → diagram-KO2AKTUF-WwWCGbIW.js} +1 -1
  144. package/workframe-ui/public/assets/{diagram-LMA3HP47-D-G3ekmF.js → diagram-LMA3HP47-DTYwZALT.js} +1 -1
  145. package/workframe-ui/public/assets/{diagram-OG6HWLK6-TBHhT9f5.js → diagram-OG6HWLK6-B46kBIqE.js} +1 -1
  146. package/workframe-ui/public/assets/{dist-CPbiDkEZ.js → dist-mQWmB3z6.js} +1 -1
  147. package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-YxkUmLLV.js → erDiagram-TEJ5UH35-__40xO-y.js} +1 -1
  148. package/workframe-ui/public/assets/eventmodeling-FCH6USID-Cm1uRTxU.js +1 -0
  149. package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-kfZ7-RSd.js → flowDiagram-I6XJVG4X-Cwj6dVUz.js} +1 -1
  150. package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-DmTx3hzR.js → ganttDiagram-6RSMTGT7-D_IOSwAS.js} +1 -1
  151. package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-BXa0ewp3.js → gitGraph-WXDBUCRP-C1vGecee.js} +1 -1
  152. package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-CQr2qMxU.js → gitGraphDiagram-PVQCEYII-DMTxFJxn.js} +1 -1
  153. package/workframe-ui/public/assets/index-BdiM4-lI.js +130 -0
  154. package/workframe-ui/public/assets/index-Fnad47vV.css +1 -0
  155. package/workframe-ui/public/assets/{info-J43DQDTF-t-Qlx8zv.js → info-J43DQDTF-CwjmyPmD.js} +1 -1
  156. package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-CzkYofPx.js → infoDiagram-5YYISTIA-BGzh7ZL2.js} +1 -1
  157. package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-djK3Oxgb.js → ishikawaDiagram-YF4QCWOH-BLXdnR_5.js} +1 -1
  158. package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-CWxYXxdF.js → journeyDiagram-JHISSGLW-BK3behMe.js} +1 -1
  159. package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-D7leCAD9.js → kanban-definition-UN3LZRKU-Bx172cCl.js} +1 -1
  160. package/workframe-ui/public/assets/{line-BSFj2OC5.js → line-BTCEHb7l.js} +1 -1
  161. package/workframe-ui/public/assets/{linear-pfPwnu-o.js → linear-CkbydpnK.js} +1 -1
  162. package/workframe-ui/public/assets/{mermaid-parser.core-Dm2L9Ib_.js → mermaid-parser.core-DvrtArpk.js} +2 -2
  163. package/workframe-ui/public/assets/{mermaid.core-Bevx9bif.js → mermaid.core-yj_1x_qj.js} +3 -3
  164. package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-UkjDssmP.js → mindmap-definition-RKZ34NQL-BXbMltQ0.js} +1 -1
  165. package/workframe-ui/public/assets/{packet-YPE3B663-JO2ezH3_.js → packet-YPE3B663-9pVCBQ7S.js} +1 -1
  166. package/workframe-ui/public/assets/{pie-LRSECV5Y-DdMFsfR8.js → pie-LRSECV5Y-CdqoWesP.js} +1 -1
  167. package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-CEPHFBU1.js → pieDiagram-4H26LBE5-kGs-VfXd.js} +1 -1
  168. package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-_RF9_86k.js → quadrantDiagram-W4KKPZXB-D9D8gQw5.js} +1 -1
  169. package/workframe-ui/public/assets/{radar-GUYGQ44K-Cb-YfdaA.js → radar-GUYGQ44K-d4zMZpQS.js} +1 -1
  170. package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-DBVOxmBn.js → requirementDiagram-4Y6WPE33-BI7EQ9zc.js} +1 -1
  171. package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-BTpxsGUA.js → sankeyDiagram-5OEKKPKP-Bcz19jQB.js} +1 -1
  172. package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-DHP_Bq3l.js → sequenceDiagram-3UESZ5HK-iQiCEfYp.js} +1 -1
  173. package/workframe-ui/public/assets/{src-CY_By-r7.js → src-D6mvlP96.js} +1 -1
  174. package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-BVtp_LPR.js → stateDiagram-AJRCARHV-CxaTXs02.js} +1 -1
  175. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-CXikj9xi.js +1 -0
  176. package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-D38HJhop.js → timeline-definition-PNZ67QCA-Bej-EAnf.js} +1 -1
  177. package/workframe-ui/public/assets/{treeView-BLDUP644-CFCG3Sc_.js → treeView-BLDUP644-C7PnnzwX.js} +1 -1
  178. package/workframe-ui/public/assets/{treemap-LRROVOQU-BeiMs_dF.js → treemap-LRROVOQU-CqISLmkO.js} +1 -1
  179. package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-CKMeT6PW.js → vennDiagram-CIIHVFJN-eJMTTEW-.js} +1 -1
  180. package/workframe-ui/public/assets/{wardley-L42UT6IY-BljDy8ph.js → wardley-L42UT6IY-D4sMroBF.js} +1 -1
  181. package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-DEE8D94J.js → wardleyDiagram-YWT4CUSO-DC3DCUs7.js} +1 -1
  182. package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-BRaAWV_g.js → xychartDiagram-2RQKCTM6-BeMaM6Aq.js} +1 -1
  183. package/workframe-ui/public/index.html +7 -6
  184. package/workframe-ui/public/workframe-build.json +5 -0
  185. package/workframe-ui/public/assets/architecture-7EHR7CIX-CvhbXEvt.js +0 -1
  186. package/workframe-ui/public/assets/channel-D63s_yN9.js +0 -1
  187. package/workframe-ui/public/assets/chunk-LZXEDZCA-CsRdBqUB.js +0 -2
  188. package/workframe-ui/public/assets/chunk-QZHKN3VN-Dj_Yxhgo.js +0 -1
  189. package/workframe-ui/public/assets/chunk-WU5MYG2G-AFchshb6.js +0 -1
  190. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-Opj8t01I.js +0 -1
  191. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-Opj8t01I.js +0 -1
  192. package/workframe-ui/public/assets/eventmodeling-FCH6USID-LQOYL50n.js +0 -1
  193. package/workframe-ui/public/assets/index-B08aShJy.css +0 -1
  194. package/workframe-ui/public/assets/index-DCF3W3G_.js +0 -129
  195. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-CvJhS_BJ.js +0 -1
@@ -0,0 +1,178 @@
1
+ """Install / stack wizard route handlers (WF-032 slice)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ from typing import Any
7
+
8
+ import install_api
9
+ import site_meta
10
+ import stack_config
11
+ from supervisor_client import _maybe_sync_compose_public_url, _supervisor_ready, _supervisor_request
12
+
13
+
14
+ def _srv():
15
+ import server
16
+
17
+ return server
18
+
19
+
20
+ class InstallRoutesMixin:
21
+ def _route_get_install_status(self, qs: dict[str, list[str]]) -> None:
22
+ srv = _srv()
23
+ if install_api.install_window_open(str(srv._workframe_db_path())):
24
+ try:
25
+ srv._ensure_native_hermes_profile()
26
+ except Exception:
27
+ pass
28
+ payload = install_api.install_status_payload(
29
+ srv.DEPLOYMENT_MODE,
30
+ srv.SECURE_MODE,
31
+ srv.DEV_LOCAL_UNSAFE,
32
+ str(srv._workframe_db_path()),
33
+ )
34
+ self._json(200, payload)
35
+
36
+ def _route_get_install_stack(self, qs: dict[str, list[str]]) -> None:
37
+ srv = _srv()
38
+ if not srv._install_window_open() and not srv._role_allows(self, srv.OWNER_ADMIN_ROLES):
39
+ self._json(403, {"ok": False, "error": "forbidden", "required_role": "owner_or_admin"})
40
+ return
41
+ payload = stack_config.public_stack_payload()
42
+ payload["wizard"] = install_api.install_wizard_public_payload(str(srv._workframe_db_path()))
43
+ self._json(200, {"ok": True, **payload})
44
+
45
+ def _route_get_install_url_test(self, qs: dict[str, list[str]]) -> None:
46
+ srv = _srv()
47
+ if not srv._install_window_open():
48
+ self._json(403, {"ok": False, "error": "install_closed"})
49
+ return
50
+ url = (qs.get("url") or [""])[0] or stack_config.get_stack_config().get("app_base_url") or ""
51
+ self._json(200, install_api.url_test(str(url)))
52
+
53
+ def _route_get_install_publish_hints(self, qs: dict[str, list[str]]) -> None:
54
+ srv = _srv()
55
+ if not srv._install_window_open():
56
+ self._json(403, {"ok": False, "error": "install_closed"})
57
+ return
58
+ url = (qs.get("url") or [""])[0] or stack_config.get_stack_config().get("app_base_url") or ""
59
+ self._json(200, install_api.publish_hints_payload(str(url)))
60
+
61
+ def _route_post_install_email_test(self, body: dict) -> None:
62
+ srv = _srv()
63
+ if not srv._install_window_open():
64
+ self._json(403, {"ok": False, "error": "install_closed"})
65
+ return
66
+ if not srv._install_owner_session_ok(self):
67
+ self._json(403, {"ok": False, "error": "forbidden", "required_role": "owner_or_admin"})
68
+ return
69
+ try:
70
+ to_email = str(body.get("email", "")).strip()
71
+ result = install_api.smtp_test_send(to_email)
72
+ self._json(200, result)
73
+ except ValueError as exc:
74
+ self._json(400, {"ok": False, "error": str(exc), "hint": install_api.smtp_error_hint(exc)})
75
+ except Exception as exc:
76
+ self._json(500, {"ok": False, "error": str(exc), "hint": install_api.smtp_error_hint(exc)})
77
+
78
+ def _route_post_install_url_test(self, body: dict) -> None:
79
+ srv = _srv()
80
+ if not srv._install_window_open():
81
+ self._json(403, {"ok": False, "error": "install_closed"})
82
+ return
83
+ url = str(body.get("url") or body.get("app_base_url") or "").strip()
84
+ self._json(200, install_api.url_test(url))
85
+
86
+ def _route_post_install_setup_https(self, body: dict) -> None:
87
+ srv = _srv()
88
+ if not srv._install_window_open():
89
+ self._json(403, {"ok": False, "error": "install_closed"})
90
+ return
91
+ try:
92
+ host, port = install_api.normalize_setup_https(
93
+ str(body.get("host") or body.get("url") or body.get("app_base_url") or ""),
94
+ body.get("port"),
95
+ )
96
+ except ValueError as exc:
97
+ self._json(400, {"ok": False, "error": str(exc)})
98
+ return
99
+ if not _supervisor_ready():
100
+ self._json(503, {"ok": False, "error": "supervisor_unavailable"})
101
+ return
102
+ status, data = _supervisor_request(
103
+ "POST",
104
+ "/v1/host.setup_public_https",
105
+ {"host": host, "port": port},
106
+ timeout=600.0,
107
+ )
108
+ if status < 400:
109
+ sync = _maybe_sync_compose_public_url(f"https://{host}")
110
+ if isinstance(data, dict) and sync:
111
+ data = {**data, "compose_sync": sync}
112
+ self._json(status, data if isinstance(data, dict) else {"ok": False, "error": "invalid_response"})
113
+
114
+ def _route_post_install_complete(self, body: dict) -> None:
115
+ srv = _srv()
116
+ user_id = str(getattr(self, "auth_user", "") or "").strip()
117
+ if not user_id and srv.DEPLOYMENT_MODE != "single_user_local":
118
+ self._json(401, {"ok": False, "error": "no_session"})
119
+ return
120
+ self._json(200, self._complete_install(user_id, body))
121
+
122
+ def _route_post_install_stack_branding_asset(self, body: dict) -> None:
123
+ srv = _srv()
124
+ if not srv._role_allows(self, srv.OWNER_ADMIN_ROLES):
125
+ self._json(403, {"ok": False, "error": "forbidden", "required_role": "owner_or_admin"})
126
+ return
127
+ kind = str(body.get("kind") or "").strip().lower()
128
+ if kind not in {"og", "favicon"}:
129
+ self._json(400, {"ok": False, "error": "invalid_kind"})
130
+ return
131
+ raw_b64 = str(body.get("data_base64") or "").strip()
132
+ if raw_b64.startswith("data:"):
133
+ header, _, encoded = raw_b64.partition(",")
134
+ content_type = header.split(";")[0].replace("data:", "").strip()
135
+ raw_b64 = encoded
136
+ else:
137
+ content_type = str(body.get("content_type") or "").strip()
138
+ try:
139
+ data = base64.b64decode(raw_b64, validate=False)
140
+ except Exception:
141
+ self._json(400, {"ok": False, "error": "invalid_base64"})
142
+ return
143
+ try:
144
+ site_meta.save_branding_asset(kind, data, content_type)
145
+ except ValueError as exc:
146
+ self._json(400, {"ok": False, "error": str(exc)})
147
+ return
148
+ meta = srv._public_site_meta_payload()
149
+ self._json(
150
+ 200,
151
+ {
152
+ "ok": True,
153
+ "kind": kind,
154
+ "og_image": meta.get("og_image"),
155
+ "favicon": meta.get("favicon"),
156
+ },
157
+ )
158
+
159
+ def _route_patch_install_stack(self, body: dict) -> None:
160
+ srv = _srv()
161
+ if not srv._install_window_open() and not srv._role_allows(self, srv.OWNER_ADMIN_ROLES):
162
+ self._json(403, {"ok": False, "error": "forbidden", "required_role": "owner_or_admin"})
163
+ return
164
+ if not srv._install_owner_session_ok(self):
165
+ self._json(403, {"ok": False, "error": "forbidden", "required_role": "owner_or_admin"})
166
+ return
167
+ try:
168
+ updated = stack_config.patch_stack_config(body if isinstance(body, dict) else {})
169
+ compose_sync = None
170
+ if isinstance(body, dict) and body.get("app_base_url"):
171
+ compose_sync = _maybe_sync_compose_public_url(str(body.get("app_base_url") or ""))
172
+ srv.DEPLOYMENT_MODE = srv._resolve_deployment_mode()
173
+ payload: dict[str, Any] = {"ok": True, **updated}
174
+ if compose_sync is not None:
175
+ payload["compose_sync"] = compose_sync
176
+ self._json(200, payload)
177
+ except ValueError as exc:
178
+ self._json(400, {"ok": False, "error": str(exc)})
@@ -0,0 +1,325 @@
1
+ """Provider credentials / OAuth connect / internal LLM proxy (WF-032 slice)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sqlite3
6
+ from typing import Any
7
+
8
+ import action_proxy
9
+ import internal_proxy_auth
10
+ import llm_proxy
11
+ import openrouter_catalog
12
+ import platform_auth
13
+ import run_surface_wiring
14
+
15
+
16
+ def _srv():
17
+ import server
18
+
19
+ return server
20
+
21
+
22
+ class ProviderRoutesMixin:
23
+
24
+ def _handle_internal_llm_proxy(self, method: str, path: str) -> bool:
25
+ srv = _srv()
26
+ if not path.startswith("/internal/llm/"):
27
+ return False
28
+ body: bytes | None = None
29
+ if method.upper() in {"POST", "PUT", "PATCH"}:
30
+ length = int(self.headers.get("Content-Length") or 0)
31
+ body = self.rfile.read(length) if length > 0 else b""
32
+ return llm_proxy.handle_proxy_request(
33
+ self,
34
+ path,
35
+ method,
36
+ body,
37
+ resolve_secret=srv._resolve_secret_for_lease,
38
+ )
39
+
40
+ def _handle_internal_action_proxy(self, method: str, path: str) -> bool:
41
+ srv = _srv()
42
+ if not path.startswith("/internal/action/"):
43
+ return False
44
+ body: bytes | None = None
45
+ if method.upper() in {"POST", "PUT", "PATCH"}:
46
+ length = int(self.headers.get("Content-Length") or 0)
47
+ body = self.rfile.read(length) if length > 0 else b""
48
+ return action_proxy.handle_proxy_request(
49
+ self,
50
+ path,
51
+ method,
52
+ body,
53
+ resolve_secret=srv._resolve_secret_for_lease,
54
+ )
55
+
56
+ def _handle_internal_run_record(self, path: str) -> bool:
57
+ srv = _srv()
58
+ if path != "/internal/runs/record":
59
+ return False
60
+ ok, err = internal_proxy_auth.authorize_internal_proxy(self)
61
+ if not ok:
62
+ self._json(403, {"error": err or "forbidden"})
63
+ return True
64
+ try:
65
+ body = self._read_json()
66
+ if not isinstance(body, dict):
67
+ raise ValueError("body must be a JSON object")
68
+ result = run_surface_wiring.record_automated_surface_run(body)
69
+ self._json(200, result)
70
+ except ValueError as exc:
71
+ self._json(400, {"ok": False, "error": str(exc)})
72
+ except Exception as exc: # noqa: BLE001
73
+ srv._log_handler_error("internal.runs.record", exc)
74
+ self._json(500, {"ok": False, "error": str(exc)})
75
+ return True
76
+
77
+ def _route_get_user_credentials(self, qs: dict[str, list[str]]) -> None:
78
+ srv = _srv()
79
+ user_id = str(getattr(self, "auth_user", "") or "")
80
+ if not user_id:
81
+ if srv.DEV_LOCAL_UNSAFE:
82
+ user_id = "dev"
83
+ else:
84
+ self._json(401, {"ok": False, "error": "no_authenticated_user"})
85
+ return
86
+ try:
87
+ conn = sqlite3.connect(str(srv.AUTH_DB_PATH.parent / "workframe.db"), timeout=3.0)
88
+ conn.row_factory = sqlite3.Row
89
+ creds = conn.execute(
90
+ "SELECT id, workspace_id, user_id, agent_profile_id, provider, credential_type, label, is_active, created_at, updated_at FROM credential_bindings WHERE user_id = ? AND deleted_at IS NULL ORDER BY created_at DESC",
91
+ (user_id,),
92
+ ).fetchall()
93
+ conn.close()
94
+ except Exception:
95
+ creds = []
96
+ self._json(200, {"ok": True, "credentials": [dict(c) for c in creds]})
97
+
98
+ def _route_get_me_credentials(self, qs: dict[str, list[str]]) -> None:
99
+ srv = _srv()
100
+ user_id = str(getattr(self, "auth_user", "") or "")
101
+ if not user_id:
102
+ self._json(401, {"ok": False, "error": "no_session"})
103
+ return
104
+ try:
105
+ conn = sqlite3.connect(str(srv._workframe_db_path()), timeout=3.0)
106
+ conn.row_factory = sqlite3.Row
107
+ rows = conn.execute(
108
+ """SELECT id, workspace_id, user_id, agent_profile_id, provider, credential_type,
109
+ credential_ref, label, is_active, created_at, updated_at
110
+ FROM credential_bindings
111
+ WHERE user_id = ? AND deleted_at IS NULL
112
+ ORDER BY updated_at DESC, created_at DESC""",
113
+ (user_id,),
114
+ ).fetchall()
115
+ conn.close()
116
+ except sqlite3.Error as exc:
117
+ self._json(500, {"ok": False, "error": f"db_error: {exc}"})
118
+ return
119
+ self._json(200, {"ok": True, "credentials": [dict(row) for row in rows]})
120
+
121
+ def _route_get_me_providers(self, qs: dict[str, list[str]]) -> None:
122
+ srv = _srv()
123
+ user_id = str(getattr(self, "auth_user", "") or "")
124
+ if not user_id:
125
+ self._json(401, {"ok": False, "error": "no_session"})
126
+ return
127
+ workspace_id = qs.get("workspace_id", [""])[0]
128
+ self._json(200, srv.list_user_providers(user_id, workspace_id))
129
+
130
+ def _route_post_me_credentials(self, body: dict) -> None:
131
+ srv = _srv()
132
+ user_id = str(getattr(self, "auth_user", "") or "")
133
+ if not user_id:
134
+ self._json(401, {"ok": False, "error": "no_session"})
135
+ return
136
+ provider = str(body.get("provider", "")).strip()
137
+ credential_type = str(body.get("credential_type", "api_key")).strip() or "api_key"
138
+ secret = str(body.get("secret", "")).strip()
139
+ spec = srv._catalog_provider(provider)
140
+ if spec:
141
+ credential_type = str(spec.get("connect_mode") or credential_type)
142
+ env_var = str(body.get("env_var", "")).strip() or (
143
+ str(spec.get("env_var") or "") if spec else ""
144
+ ) or srv._default_credential_env_var(provider, credential_type)
145
+ label = str(body.get("label", "")).strip() or env_var
146
+ if not provider:
147
+ self._json(400, {"ok": False, "error": "provider required"})
148
+ return
149
+ if not secret:
150
+ self._json(400, {"ok": False, "error": "secret required"})
151
+ return
152
+ payload = srv._store_user_credential(user_id, provider, credential_type, secret, env_var, label)
153
+ cred_ref = str(payload["credential_ref"])
154
+ now = srv._utc_now()
155
+ try:
156
+ conn = sqlite3.connect(str(srv._workframe_db_path()), timeout=3.0)
157
+ conn.row_factory = sqlite3.Row
158
+ existing = conn.execute(
159
+ """SELECT id FROM credential_bindings
160
+ WHERE user_id = ? AND provider = ? AND credential_type = ?
161
+ AND credential_ref = ? AND deleted_at IS NULL
162
+ ORDER BY updated_at DESC, created_at DESC LIMIT 1""",
163
+ (user_id, provider, credential_type, cred_ref),
164
+ ).fetchone()
165
+ if existing:
166
+ cred_id = str(existing["id"])
167
+ conn.execute(
168
+ """UPDATE credential_bindings
169
+ SET label = ?, is_active = 1, updated_at = ?, deleted_at = NULL
170
+ WHERE id = ?""",
171
+ (label, now, cred_id),
172
+ )
173
+ else:
174
+ cred_id = str(payload["credential_id"])
175
+ conn.execute(
176
+ """INSERT INTO credential_bindings
177
+ (id, workspace_id, user_id, agent_profile_id, provider, credential_type,
178
+ credential_ref, label, is_active, created_by, created_at, updated_at)
179
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""",
180
+ (cred_id, None, user_id, None, provider, credential_type, cred_ref, label, 1, user_id, now, now),
181
+ )
182
+ conn.execute(
183
+ """UPDATE credential_bindings
184
+ SET is_active = 0, deleted_at = ?, updated_at = ?
185
+ WHERE user_id = ? AND provider = ? AND deleted_at IS NULL AND id != ?""",
186
+ (now, now, user_id, provider, cred_id),
187
+ )
188
+ conn.commit()
189
+ conn.close()
190
+ except sqlite3.Error as exc:
191
+ self._json(500, {"ok": False, "error": f"db_error: {exc}"})
192
+ return
193
+ self._log_audit("credential_stored", "credential_binding", cred_id, f"provider={provider}")
194
+ health: dict[str, Any] = {}
195
+ if provider == "openrouter":
196
+ secret_probe = srv._credential_secret(
197
+ {
198
+ "credential_ref": cred_ref,
199
+ "scope": "user",
200
+ "user_id": user_id,
201
+ },
202
+ user_id,
203
+ )
204
+ if secret_probe:
205
+ health = openrouter_catalog.probe_account(secret_probe)
206
+ try:
207
+ srv._bootstrap_model_after_llm_connect(user_id, str(body.get("workspace_id") or ""), provider)
208
+ except (OSError, RuntimeError, ValueError) as exc:
209
+ srv._log_handler_error("POST /api/me/credentials provider bootstrap", exc)
210
+ self._json(200, {
211
+ "ok": True,
212
+ "credential_id": cred_id,
213
+ "provider": provider,
214
+ "credential_type": credential_type,
215
+ "label": label,
216
+ "is_active": 1,
217
+ "user_id": user_id,
218
+ "created_at": now,
219
+ "updated_at": now,
220
+ "health": health,
221
+ **payload,
222
+ })
223
+
224
+ def _route_post_me_telegram_link(self, body: dict) -> None:
225
+ srv = _srv()
226
+ user_id = str(getattr(self, "auth_user", "") or "")
227
+ if not user_id:
228
+ self._json(401, {"ok": False, "error": "no_session"})
229
+ return
230
+ result = platform_auth.verify_telegram_login(body if isinstance(body, dict) else {})
231
+ if not result.get("ok"):
232
+ self._json(400, {"ok": False, "error": result.get("error") or "telegram_link_failed"})
233
+ return
234
+ patch = result.get("platform_ids") if isinstance(result.get("platform_ids"), dict) else {}
235
+ if patch:
236
+ srv._merge_user_platform_ids(user_id, {str(k): str(v) for k, v in patch.items()})
237
+ self._json(200, {"ok": True, "provider": "telegram", "platform_ids": patch})
238
+
239
+ def _route_pattern_get_me_oauth_status(self, path: str, qs: dict[str, list[str]]) -> None:
240
+ srv = _srv()
241
+ parts = path.strip("/").split("/")
242
+ if len(parts) != 5 or parts[2] != "oauth" or parts[4] != "status":
243
+ self._json(404, {"error": "not_found"})
244
+ return
245
+ user_id = str(getattr(self, "auth_user", "") or "")
246
+ if not user_id:
247
+ self._json(401, {"ok": False, "error": "no_session"})
248
+ return
249
+ provider_id = parts[3]
250
+ session_id = str(qs.get("session_id", [""])[0] or "").strip()
251
+ if not session_id:
252
+ self._json(400, {"ok": False, "error": "session_id required"})
253
+ return
254
+ self._json(200, srv.device_oauth_status(user_id, provider_id, session_id))
255
+
256
+ def _route_pattern_get_agent_credentials(self, path: str, qs: dict[str, list[str]]) -> None:
257
+ srv = _srv()
258
+ parts = path.strip("/").split("/")
259
+ if len(parts) != 4:
260
+ self._json(404, {"error": "not_found"})
261
+ return
262
+ agent_id = parts[2]
263
+ if not srv._role_allows(self, srv.OWNER_ADMIN_ROLES):
264
+ self._json(403, {"ok": False, "error": "forbidden"})
265
+ return
266
+ try:
267
+ conn = sqlite3.connect(str(srv._workframe_db_path()), timeout=3.0)
268
+ conn.row_factory = sqlite3.Row
269
+ rows = conn.execute(
270
+ """SELECT id, workspace_id, user_id, agent_profile_id, provider,
271
+ credential_type, credential_ref, label, is_active,
272
+ expires_at, created_by, created_at, updated_at
273
+ FROM credential_bindings
274
+ WHERE agent_profile_id = ? AND deleted_at IS NULL
275
+ ORDER BY created_at DESC""",
276
+ (agent_id,),
277
+ ).fetchall()
278
+ conn.close()
279
+ except sqlite3.Error as exc:
280
+ self._json(500, {"ok": False, "error": str(exc)})
281
+ return
282
+ self._json(200, {
283
+ "ok": True,
284
+ "scope": "agent_profile",
285
+ "agent_profile_id": agent_id,
286
+ "credentials": [dict(r) for r in rows],
287
+ })
288
+
289
+ def _route_pattern_post_me_oauth_start(self, path: str, body: dict) -> None:
290
+ srv = _srv()
291
+ parts = path.strip("/").split("/")
292
+ if len(parts) != 5 or parts[2] != "oauth" or parts[4] != "start":
293
+ self._json(404, {"error": "not_found"})
294
+ return
295
+ user_id = str(getattr(self, "auth_user", "") or "")
296
+ if not user_id:
297
+ self._json(401, {"ok": False, "error": "no_session"})
298
+ return
299
+ provider_id = parts[3]
300
+ workspace_id = str(body.get("workspace_id") or "")
301
+ self._json(200, srv.start_user_oauth(user_id, provider_id, workspace_id))
302
+
303
+ def _route_pattern_post_me_providers_disconnect(self, path: str, body: dict) -> None:
304
+ srv = _srv()
305
+ parts = path.strip("/").split("/")
306
+ if len(parts) != 5 or parts[2] != "providers" or parts[4] != "disconnect":
307
+ self._json(404, {"error": "not_found"})
308
+ return
309
+ user_id = str(getattr(self, "auth_user", "") or "")
310
+ if not user_id:
311
+ self._json(401, {"ok": False, "error": "no_session"})
312
+ return
313
+ self._json(200, srv.disconnect_user_provider(user_id, parts[3]))
314
+
315
+ def _route_pattern_delete_me_credentials(self, path: str, body: dict) -> None:
316
+ srv = _srv()
317
+ parts = path.strip("/").split("/")
318
+ if len(parts) != 4 or parts[2] != "credentials":
319
+ self._json(404, {"error": "not_found"})
320
+ return
321
+ user_id = str(getattr(self, "auth_user", "") or "")
322
+ if not user_id:
323
+ self._json(401, {"ok": False, "error": "no_session"})
324
+ return
325
+ self._json(200, srv.disconnect_user_credential(user_id, parts[3]))