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,512 @@
1
+ """Auth / session / OAuth callback route handlers (WF-032 slice)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ import secrets
9
+ import sqlite3
10
+ import time
11
+ import urllib.parse
12
+ import uuid
13
+ from datetime import datetime, timezone
14
+ from typing import Any
15
+
16
+ import auth_rate_limit
17
+ import google_auth
18
+ import stack_config
19
+ from email_sender import APP_BASE_URL
20
+
21
+
22
+ def _srv():
23
+ import server
24
+
25
+ return server
26
+
27
+
28
+ class AuthRoutesMixin:
29
+
30
+ def _route_post_auth_google_start(self, body: dict) -> None:
31
+ srv = _srv()
32
+ invite_email = str(body.get("email") or "").strip().lower()
33
+ invite_token = str(body.get("invite_token") or "")
34
+ if invite_email:
35
+ allowed, deny_meta = srv._email_allowed_to_authenticate(invite_email)
36
+ if not allowed and not srv._invite_token_allows_email(invite_token, invite_email):
37
+ self._log_audit(
38
+ "login_denied_private",
39
+ "user",
40
+ invite_email,
41
+ str(deny_meta.get("error") or ""),
42
+ )
43
+ self._json(403, {"ok": False, **deny_meta})
44
+ return
45
+ try:
46
+ payload = google_auth.start_google_auth(invite_email, invite_token)
47
+ self._json(200, payload)
48
+ except ValueError as exc:
49
+ self._json(400, {"ok": False, "error": str(exc)})
50
+
51
+ def _route_post_auth_local_bootstrap(self, body: dict) -> None:
52
+ srv = _srv()
53
+ if srv.DEPLOYMENT_MODE != "single_user_local" or not srv._install_window_open():
54
+ self._json(403, {"ok": False, "error": "local_bootstrap_unavailable"})
55
+ return
56
+ email = str(body.get("email") or "").strip().lower()
57
+ if not email or "@" not in email:
58
+ self._json(400, {"ok": False, "error": "email_required"})
59
+ return
60
+ display = str(body.get("display_name") or "").strip() or email.split("@", 1)[0]
61
+ try:
62
+ result = _zk.create_session_for_email(email)
63
+ except (RuntimeError, OSError, sqlite3.Error) as exc:
64
+ srv._log_handler_error("POST /api/auth/local-bootstrap", exc)
65
+ self._json(500, {"ok": False, "error": str(exc)})
66
+ return
67
+ user_id = str(result["user_id"])
68
+ self.auth_user = user_id
69
+ self._ensure_user(user_id, display, email)
70
+ self._first_owner_bootstrap(user_id, display, email)
71
+ use_secure = srv._session_cookie_secure()
72
+ cookie_val = _zk.session_cookie_value(result["session_id"], secure=use_secure)
73
+ me_payload = srv._session_profile_payload(user_id)
74
+ self._json(
75
+ 200,
76
+ {
77
+ "ok": True,
78
+ "user_id": user_id,
79
+ "session_id": result["session_id"],
80
+ "refresh_token": result["refresh_token"],
81
+ **(me_payload or {}),
82
+ },
83
+ extra_headers=[("Set-Cookie", cookie_val)],
84
+ )
85
+
86
+ def _route_post_auth_bootstrap(self, body: dict) -> None:
87
+ srv = _srv()
88
+ if not srv.SECURE_MODE:
89
+ self._json(501, {"ok": False, "error": "bootstrap_requires_secure_mode"})
90
+ return
91
+ data = body if isinstance(body, dict) else {}
92
+ user_id = str(data.get("user_id", "") or secrets.token_hex(8))
93
+ user_email = str(data.get("email", ""))
94
+ user_display = str(data.get("display_name", "") or "Owner")
95
+ result = self._first_owner_bootstrap(user_id, user_display, user_email)
96
+ self._log_audit("user_created", "user", user_id, f"first-owner bootstrap: {user_display}")
97
+ self._json(200, result)
98
+
99
+ def _route_post_setup(self, body: dict) -> None:
100
+ srv = _srv()
101
+ data = body if isinstance(body, dict) else {}
102
+ agent_name = str(data.get("agent_name", "")).strip()
103
+ workframe_name = str(data.get("workframe_name", "")).strip()
104
+ if not agent_name:
105
+ self._json(400, {"ok": False, "error": "agent_name required"})
106
+ return
107
+ try:
108
+ db = srv._workframe_db()
109
+ ws = db.execute("SELECT * FROM workspaces WHERE slug='default' AND deleted_at IS NULL").fetchone()
110
+ if not ws:
111
+ ws_id = str(uuid.uuid4())
112
+ now = str(int(time.time()))
113
+ workspace_display_name = workframe_name or "Default Workspace"
114
+ ws_settings = stack_config.github_oauth_for_workspace_settings({})
115
+ settings_json = json.dumps(ws_settings, sort_keys=True) if ws_settings else "{}"
116
+ db.execute(
117
+ "INSERT INTO workspaces (id, slug, display_name, description, owner_id, status, created_at, updated_at, settings_json) VALUES (?, 'default', ?, '', '', 'active', ?, ?, ?)",
118
+ (ws_id, workspace_display_name, now, now, settings_json),
119
+ )
120
+ ws = db.execute("SELECT * FROM workspaces WHERE id = ?", (ws_id,)).fetchone()
121
+ elif workframe_name and str(ws["display_name"] or "").strip() != workframe_name:
122
+ now = str(int(time.time()))
123
+ db.execute(
124
+ "UPDATE workspaces SET display_name = ?, updated_at = ? WHERE id = ?",
125
+ (workframe_name, now, ws["id"]),
126
+ )
127
+ ws = db.execute("SELECT * FROM workspaces WHERE id = ?", (ws["id"],)).fetchone()
128
+ ws_id = ws["id"]
129
+ existing_agents = db.execute("SELECT COUNT(*) AS c FROM agent_profiles WHERE workspace_id = ? AND deleted_at IS NULL", (ws_id,)).fetchone()
130
+ if existing_agents and existing_agents["c"] > 0:
131
+ if agent_name:
132
+ now = str(int(time.time()))
133
+ db.execute(
134
+ """
135
+ UPDATE agent_profiles
136
+ SET display_name = ?, updated_at = ?
137
+ WHERE workspace_id = ? AND is_native = 1 AND deleted_at IS NULL
138
+ """,
139
+ (agent_name, now, ws_id),
140
+ )
141
+ db.commit()
142
+ db.close()
143
+ self._json(200, {"ok": True, "already_initialized": True, "workspace_id": ws_id})
144
+ return
145
+ import re as _re
146
+ agent_slug = _re.sub(r"[^a-z0-9-]", "", agent_name.lower().replace(" ", "-"))[:40]
147
+ agent_id = str(uuid.uuid4())
148
+ now = str(int(time.time()))
149
+ db.execute(
150
+ "INSERT INTO agent_profiles (id, workspace_id, slug, display_name, tagline, role, is_native, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, 1, 'active', ?, ?)",
151
+ (agent_id, ws_id, agent_slug, agent_name, str(data.get("agent_personality", "")), "native", now, now),
152
+ )
153
+ room_id = str(uuid.uuid4())
154
+ db.execute(
155
+ "INSERT INTO rooms (id, workspace_id, name, slug, topic, room_type, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, 'channel', 'active', ?, ?)",
156
+ (room_id, ws_id, "General", "general", "General discussion", now, now),
157
+ )
158
+ db.commit()
159
+ db.close()
160
+ hermes = srv._bootstrap_after_setup(str(data.get("agent_personality", "")))
161
+ self._json(
162
+ 200,
163
+ {
164
+ "ok": True,
165
+ "workspace_id": ws_id,
166
+ "agent_id": agent_id,
167
+ "room_id": room_id,
168
+ "hermes": hermes,
169
+ },
170
+ )
171
+ except Exception as exc:
172
+ self._json(500, {"ok": False, "error": str(exc)})
173
+
174
+ def _route_post_auth_start(self, body: dict) -> None:
175
+ srv = _srv()
176
+ email = str(body.get("email", "")).strip().lower()
177
+ if not email or "@" not in email:
178
+ self._json(400, {"ok": False, "error": "email required"})
179
+ return
180
+ client_ip = str(self.client_address[0] if self.client_address else "")
181
+ import auth_rate_limit
182
+
183
+ if not auth_rate_limit.allow_auth_request("start", client_ip, email):
184
+ self._json(429, {"ok": False, "error": "too_many_requests"})
185
+ return
186
+ allowed, deny_meta = srv._email_allowed_to_authenticate(email)
187
+ if not allowed:
188
+ self._log_audit("login_denied_private", "user", email, str(deny_meta.get("error") or ""))
189
+ self._json(403, {"ok": False, **deny_meta})
190
+ return
191
+ try:
192
+ base = APP_BASE_URL.strip().lower()
193
+ loopback = base.startswith("http://127.0.0.1") or base.startswith("http://localhost")
194
+ expose_otp = (
195
+ os.environ.get("WORKFRAME_E2E") == "1"
196
+ and srv._install_window_open()
197
+ and loopback
198
+ )
199
+ result = _zk.start_email_verification(
200
+ email,
201
+ dev_local_unsafe=srv.DEV_LOCAL_UNSAFE,
202
+ expose_otp=expose_otp,
203
+ )
204
+ except (RuntimeError, OSError, sqlite3.Error) as exc:
205
+ srv._log_handler_error("POST /api/auth/start", exc)
206
+ self._json(500, {"ok": False, "error": str(exc)})
207
+ return
208
+ if not srv.DEV_LOCAL_UNSAFE and not expose_otp:
209
+ result.pop("otp_code", None)
210
+ result.pop("_dev_warning", None)
211
+ result.pop("_e2e_warning", None)
212
+ self._log_audit("otp_requested", "user", email, f"challenge={result.get('challenge_id','')}")
213
+ self._json(200, {"ok": True, "status": "verification_sent", **result})
214
+
215
+ def _route_post_auth_verify(self, body: dict) -> None:
216
+ srv = _srv()
217
+ email = str(body.get("email", "")).strip().lower()
218
+ code = str(body.get("code", "")).strip()
219
+ if not email or not code:
220
+ self._json(400, {"ok": False, "error": "email and code required"})
221
+ return
222
+ client_ip = str(self.client_address[0] if self.client_address else "")
223
+ import auth_rate_limit
224
+
225
+ if not auth_rate_limit.allow_auth_request("verify", client_ip, email):
226
+ self._json(429, {"ok": False, "error": "too_many_requests"})
227
+ return
228
+ allowed, deny_meta = srv._email_allowed_to_authenticate(email)
229
+ if not allowed:
230
+ self._log_audit("login_denied_private", "user", email, str(deny_meta.get("error") or ""))
231
+ self._json(403, {"ok": False, **deny_meta})
232
+ return
233
+ try:
234
+ result = _zk.verify_email_code(email, code)
235
+ except ValueError as exc:
236
+ self._log_audit("login_failed", "user", email, f"verify failed: {exc}")
237
+ self._json(401, {"ok": False, "error": str(exc)})
238
+ return
239
+ except (RuntimeError, OSError, sqlite3.Error) as exc:
240
+ srv._log_handler_error("POST /api/auth/verify", exc)
241
+ self._json(500, {"ok": False, "error": str(exc)})
242
+ return
243
+ use_secure = srv._session_cookie_secure()
244
+ cookie_val = _zk.session_cookie_value(result["session_id"], secure=use_secure)
245
+ self.auth_user = result["user_id"]
246
+ self._ensure_user(result["user_id"], email, email)
247
+ if srv._install_window_open():
248
+ stack_config.mark_install_admin_verified(email)
249
+ self._log_audit("login", "session", result["session_id"],
250
+ f"user={result['user_id']} new={result['is_new_user']}")
251
+ me_payload = srv._session_profile_payload(result["user_id"])
252
+ self._json(
253
+ 200,
254
+ {
255
+ "ok": True,
256
+ "user_id": result["user_id"],
257
+ "session_id": result["session_id"],
258
+ "refresh_token": result["refresh_token"],
259
+ "expires_at": result["expires_at"],
260
+ "is_new_user": result["is_new_user"],
261
+ **(me_payload or {}),
262
+ },
263
+ extra_headers=[("Set-Cookie", cookie_val)],
264
+ )
265
+
266
+ def _route_post_auth_logout(self, body: dict) -> None:
267
+ srv = _srv()
268
+ sid = srv._session_id_from_request(self)
269
+ user_id = str(getattr(self, "auth_user", "") or "")
270
+ if sid:
271
+ try:
272
+ _zk.logout_session(sid)
273
+ except (RuntimeError, OSError, sqlite3.Error) as exc:
274
+ srv._log_handler_error("POST /api/auth/logout", exc)
275
+ use_secure = srv._session_cookie_secure()
276
+ cookie_val = _zk.clear_session_cookie(secure=use_secure)
277
+ self._log_audit("logout", "session", sid, f"user={user_id}")
278
+ self._json(200, {"ok": True}, extra_headers=[("Set-Cookie", cookie_val)])
279
+
280
+ def _route_post_auth_refresh(self, body: dict) -> None:
281
+ srv = _srv()
282
+ refresh_token = str(body.get("refresh_token", "")).strip()
283
+ if not refresh_token:
284
+ sid = srv._session_id_from_request(self)
285
+ if not sid:
286
+ self._json(400, {"ok": False, "error": "refresh_token required"})
287
+ return
288
+ validated = _zk.validate_session_token(sid)
289
+ if not validated:
290
+ self._json(401, {"ok": False, "error": "session_expired"})
291
+ return
292
+ use_secure = srv._session_cookie_secure()
293
+ cookie_val = _zk.session_cookie_value(validated["session_id"], secure=use_secure)
294
+ self._json(
295
+ 200,
296
+ {
297
+ "ok": True,
298
+ "user_id": validated["user_id"],
299
+ "session_id": validated["session_id"],
300
+ "expires_at": validated["expires_at"],
301
+ },
302
+ extra_headers=[("Set-Cookie", cookie_val)],
303
+ )
304
+ return
305
+ try:
306
+ result = _zk.refresh_session(refresh_token)
307
+ except ValueError as exc:
308
+ self._json(401, {"ok": False, "error": str(exc)})
309
+ return
310
+ except (RuntimeError, OSError, sqlite3.Error) as exc:
311
+ srv._log_handler_error("POST /api/auth/refresh", exc)
312
+ self._json(500, {"ok": False, "error": str(exc)})
313
+ return
314
+ use_secure = srv._session_cookie_secure()
315
+ cookie_val = _zk.session_cookie_value(result["session_id"], secure=use_secure)
316
+ self._log_audit("session_refreshed", "session", result["session_id"],
317
+ f"user={result['user_id']}")
318
+ self._json(
319
+ 200,
320
+ {
321
+ "ok": True,
322
+ "user_id": result["user_id"],
323
+ "session_id": result["session_id"],
324
+ "refresh_token": result["refresh_token"],
325
+ "expires_at": result["expires_at"],
326
+ },
327
+ extra_headers=[("Set-Cookie", cookie_val)],
328
+ )
329
+
330
+ def _route_get_auth_hermes_dashboard_gate(self, qs: dict[str, list[str]]) -> None:
331
+ srv = _srv()
332
+ status = srv._hermes_dashboard_gate_status(self)
333
+ if status == 204:
334
+ self.send_response(204)
335
+ self.end_headers()
336
+ return
337
+ self._json(403, {"ok": False, "error": "dashboard_forbidden"})
338
+
339
+ def _route_get_auth_google_callback(self, qs: dict[str, list[str]]) -> None:
340
+ srv = _srv()
341
+ code = (qs.get("code") or [""])[0]
342
+ state = (qs.get("state") or [""])[0]
343
+ if not code or not state:
344
+ self._json(400, {"ok": False, "error": "missing code or state"})
345
+ return
346
+ try:
347
+ profile = google_auth.exchange_google_code(code, state)
348
+ invite_token = str(profile.get("invite_token") or "")
349
+ allowed, deny_meta = srv._email_allowed_to_authenticate(profile["email"])
350
+ if not allowed and not srv._invite_token_allows_email(invite_token, profile["email"]):
351
+ self._log_audit(
352
+ "login_denied_private",
353
+ "user",
354
+ profile["email"],
355
+ str(deny_meta.get("error") or ""),
356
+ )
357
+ self._json(403, {"ok": False, **deny_meta})
358
+ return
359
+ result = _zk.create_session_for_email(profile["email"])
360
+ self.auth_user = result["user_id"]
361
+ self._ensure_user(
362
+ result["user_id"],
363
+ profile.get("display_name") or profile["email"],
364
+ profile["email"],
365
+ )
366
+ invite_token = str(profile.get("invite_token") or "")
367
+ use_secure = srv._session_cookie_secure()
368
+ cookie_val = _zk.session_cookie_value(result["session_id"], secure=use_secure)
369
+ redirect_to = f"{APP_BASE_URL.rstrip('/')}/onboarding"
370
+ if invite_token:
371
+ redirect_to = (
372
+ f"{APP_BASE_URL.rstrip('/')}/?invite_token="
373
+ f"{urllib.parse.quote(invite_token)}&email={urllib.parse.quote(profile['email'])}"
374
+ )
375
+ self.send_response(302)
376
+ self.send_header("Location", redirect_to)
377
+ self.send_header("Set-Cookie", cookie_val)
378
+ cors_origin = srv._cors_origin_for(self.headers)
379
+ if cors_origin:
380
+ self.send_header("Access-Control-Allow-Origin", cors_origin)
381
+ self.send_header("Vary", "Origin")
382
+ self.end_headers()
383
+ except (RuntimeError, OSError, ValueError) as exc:
384
+ srv._log_handler_error("GET /api/auth/google/callback", exc)
385
+ self._json(401, {"ok": False, "error": str(exc)})
386
+
387
+ def _route_get_oauth_github_callback(self, qs: dict[str, list[str]]) -> None:
388
+ srv = _srv()
389
+ user_id = str(getattr(self, "auth_user", "") or "")
390
+ if not user_id:
391
+ self._json(401, {"ok": False, "error": "no_session"})
392
+ return
393
+ code = qs.get("code", [""])[0]
394
+ state = qs.get("state", [""])[0]
395
+ result = srv._complete_github_oauth(user_id, str(code or ""), str(state or ""))
396
+ status = "ok" if result.get("ok") else "error"
397
+ message = urllib.parse.quote(str(result.get("error") or "connected"))
398
+ location = f"{APP_BASE_URL.rstrip('/')}/?provider_connect=github&status={status}&message={message}"
399
+ self.send_response(302)
400
+ self.send_header("Location", location)
401
+ self.end_headers()
402
+
403
+ def _route_get_oauth_discord_callback(self, qs: dict[str, list[str]]) -> None:
404
+ srv = _srv()
405
+ user_id = str(getattr(self, "auth_user", "") or "")
406
+ if not user_id:
407
+ self._json(401, {"ok": False, "error": "no_session"})
408
+ return
409
+ code = qs.get("code", [""])[0]
410
+ state = qs.get("state", [""])[0]
411
+ result = srv._complete_discord_oauth(user_id, str(code or ""), str(state or ""))
412
+ status = "ok" if result.get("ok") else "error"
413
+ message = urllib.parse.quote(str(result.get("error") or "connected"))
414
+ location = f"{APP_BASE_URL.rstrip('/')}/?provider_connect=discord&status={status}&message={message}"
415
+ self.send_response(302)
416
+ self.send_header("Location", location)
417
+ self.end_headers()
418
+
419
+ def _route_get_oauth_stripe_callback(self, qs: dict[str, list[str]]) -> None:
420
+ srv = _srv()
421
+ user_id = str(getattr(self, "auth_user", "") or "")
422
+ if not user_id:
423
+ self._json(401, {"ok": False, "error": "no_session"})
424
+ return
425
+ code = qs.get("code", [""])[0]
426
+ state = qs.get("state", [""])[0]
427
+ result = srv._complete_stripe_oauth(user_id, str(code or ""), str(state or ""))
428
+ status = "ok" if result.get("ok") else "error"
429
+ message = urllib.parse.quote(str(result.get("error") or "connected"))
430
+ location = f"{APP_BASE_URL.rstrip('/')}/?provider_connect=stripe&status={status}&message={message}"
431
+ self.send_response(302)
432
+ self.send_header("Location", location)
433
+ self.end_headers()
434
+
435
+ def _route_get_me(self, qs: dict[str, list[str]]) -> None:
436
+ srv = _srv()
437
+ user_id = str(getattr(self, "auth_user", "") or "")
438
+ if not user_id:
439
+ self._json(401, {"ok": False, "error": "no_session"})
440
+ return
441
+ payload = srv._session_profile_payload(user_id)
442
+ if not payload:
443
+ self._json(404, {"ok": False, "error": "user_not_found"})
444
+ return
445
+ self._json(200, payload)
446
+
447
+ def _route_get_me_onboarding(self, qs: dict[str, list[str]]) -> None:
448
+ srv = _srv()
449
+ user_id = str(getattr(self, "auth_user", "") or "")
450
+ if not user_id:
451
+ self._json(401, {"ok": False, "error": "no_session"})
452
+ return
453
+ self._json(200, srv._onboarding_payload(user_id))
454
+
455
+ def _route_patch_me(self, body: dict) -> None:
456
+ srv = _srv()
457
+ user_id = str(getattr(self, "auth_user", "") or "")
458
+ if not user_id:
459
+ self._json(401, {"ok": False, "error": "no_session"})
460
+ return
461
+ try:
462
+ srv._apply_me_profile_updates(user_id, body)
463
+ except ValueError as exc:
464
+ self._json(400, {"ok": False, "error": str(exc)})
465
+ return
466
+ payload = srv._session_profile_payload(user_id)
467
+ if not payload:
468
+ self._json(404, {"ok": False, "error": "user_not_found"})
469
+ return
470
+ self._json(200, payload)
471
+
472
+ def _route_patch_me_native_agent(self, body: dict) -> None:
473
+ srv = _srv()
474
+ user_id = str(getattr(self, "auth_user", "") or "")
475
+ if not user_id:
476
+ self._json(401, {"ok": False, "error": "no_session"})
477
+ return
478
+ native_slug = str(srv.NATIVE_PROFILE or "workframe-agent")
479
+ runtime = srv._runtime_profile_slug(user_id, native_slug)
480
+ soul = (
481
+ str(body.get("soul") or body.get("soul_md") or "").strip()
482
+ if ("soul" in body or "soul_md" in body)
483
+ else ""
484
+ )
485
+ display = str(body.get("display_name") or "").strip() if "display_name" in body else ""
486
+ tagline = str(body.get("tagline") or "").strip() if "tagline" in body else ""
487
+ srv._seed_native_user_overlay(
488
+ runtime,
489
+ native_slug,
490
+ display_name=display,
491
+ tagline=tagline,
492
+ user_soul=soul,
493
+ )
494
+ profile_patch: dict[str, Any] = {}
495
+ if display:
496
+ profile_patch["display_name"] = display
497
+ if tagline:
498
+ profile_patch["tagline"] = tagline
499
+ if profile_patch:
500
+ srv._sync_agent_profile_db(native_slug, profile_patch)
501
+ if "avatar_url" in body or "avatar_id" in body:
502
+ avatar_patch = srv._normalize_agent_avatar_patch(
503
+ str(body.get("avatar_url") or ""),
504
+ str(body.get("avatar_id") or ""),
505
+ )
506
+ if avatar_patch.get("avatar_url") or avatar_patch.get("avatar_id"):
507
+ stamp = datetime.now(timezone.utc).isoformat()
508
+ row_patch = {**avatar_patch, "updated_at": stamp}
509
+ srv._upsert_agent_registry_row(native_slug, row_patch)
510
+ srv._upsert_agent_registry_row(runtime, row_patch)
511
+ srv._sync_agent_profile_db(native_slug, {"avatar_url": avatar_patch.get("avatar_url", "")})
512
+ self._json(200, {"ok": True, "runtime_profile": runtime})