dtxwiki 0.0.1__py3-none-any.whl

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 (422) hide show
  1. backend/__init__.py +1 -0
  2. backend/alembic.ini +38 -0
  3. backend/api/__init__.py +1 -0
  4. backend/api/auth.py +466 -0
  5. backend/api/deps.py +61 -0
  6. backend/api/health.py +107 -0
  7. backend/api/mcp_chats.py +243 -0
  8. backend/api/mcp_http.py +1430 -0
  9. backend/api/metrics.py +183 -0
  10. backend/api/middleware/public_headers.py +53 -0
  11. backend/api/middleware/request_log.py +115 -0
  12. backend/api/middleware/security_headers.py +52 -0
  13. backend/api/oauth.py +1082 -0
  14. backend/api/pages.py +570 -0
  15. backend/api/public.py +60 -0
  16. backend/api/read.py +2223 -0
  17. backend/api/uploads.py +217 -0
  18. backend/api/users.py +61 -0
  19. backend/app/__init__.py +1 -0
  20. backend/app/bootstrap.py +148 -0
  21. backend/app/data_dir.py +31 -0
  22. backend/app/db.py +66 -0
  23. backend/app/errors.py +59 -0
  24. backend/app/main.py +241 -0
  25. backend/app/settings.py +315 -0
  26. backend/app/state.py +35 -0
  27. backend/app/version.py +32 -0
  28. backend/audit/__init__.py +1 -0
  29. backend/audit/emitter.py +130 -0
  30. backend/cli/__init__.py +1 -0
  31. backend/cli/integrations/__init__.py +1 -0
  32. backend/cli/integrations/_common.py +137 -0
  33. backend/cli/integrations/claude.py +112 -0
  34. backend/cli/integrations/codex.py +48 -0
  35. backend/cli/integrations/cursor.py +16 -0
  36. backend/cli/integrations/detect.py +99 -0
  37. backend/cli/main.py +1164 -0
  38. backend/cli/mcp_doctor.py +238 -0
  39. backend/cli/mcp_http_register.py +305 -0
  40. backend/cli/prod_setup.py +523 -0
  41. backend/llm/__init__.py +39 -0
  42. backend/llm/anthropic.py +122 -0
  43. backend/llm/base.py +109 -0
  44. backend/llm/openai.py +113 -0
  45. backend/llm/registry.py +17 -0
  46. backend/mcp/__init__.py +1 -0
  47. backend/mcp/agent_tools.py +85 -0
  48. backend/mcp/context_tools.py +162 -0
  49. backend/mcp/conversation.py +106 -0
  50. backend/mcp/entry.py +92 -0
  51. backend/mcp/envelope.py +53 -0
  52. backend/mcp/page_tools.py +150 -0
  53. backend/mcp/protocol/__init__.py +1 -0
  54. backend/mcp/protocol/prompts_resources.py +545 -0
  55. backend/mcp/protocol/runner.py +212 -0
  56. backend/mcp/protocol/server.py +87 -0
  57. backend/mcp/protocol/session_pool.py +27 -0
  58. backend/mcp/protocol/tool_descriptors.py +226 -0
  59. backend/mcp/read_tools.py +226 -0
  60. backend/mcp/review_tools.py +107 -0
  61. backend/mcp/router.py +208 -0
  62. backend/mcp/scopes.py +107 -0
  63. backend/mcp/stdio_server.py +11 -0
  64. backend/migrations/env.py +46 -0
  65. backend/migrations/script.py.mako +24 -0
  66. backend/migrations/versions/0001_p0_0_pages.py +118 -0
  67. backend/migrations/versions/0002_sprint2_workspace_client.py +223 -0
  68. backend/migrations/versions/0003_sprint2_job_discovery.py +178 -0
  69. backend/migrations/versions/0004_sprint2_library_item.py +177 -0
  70. backend/migrations/versions/0005_sprint2_agent_page.py +162 -0
  71. backend/migrations/versions/0006_sprint2_agent_input_bundle.py +140 -0
  72. backend/migrations/versions/0007_sprint2_context_claim_citation.py +202 -0
  73. backend/migrations/versions/0008_sprint2_change_review_audit.py +391 -0
  74. backend/migrations/versions/0009_sprint2_full_text_indexes.py +150 -0
  75. backend/migrations/versions/0010_sprint2_pgvector_readiness.py +25 -0
  76. backend/migrations/versions/0011_sprint2_workspace_tree.py +185 -0
  77. backend/migrations/versions/0012_sprint2_chat_ingress.py +272 -0
  78. backend/migrations/versions/0013_sprint4_identity.py +98 -0
  79. backend/migrations/versions/0014_sprint4_workspace_memberships.py +106 -0
  80. backend/migrations/versions/0015_sprint4_oauth.py +167 -0
  81. backend/migrations/versions/0016_sprint4_refresh_token_argon2id.py +30 -0
  82. backend/migrations/versions/0017_sprint4_oauth_consent_workspace.py +41 -0
  83. backend/migrations/versions/0018_sprint4_fix3_oauth_workspace_bound_tokens.py +222 -0
  84. backend/migrations/versions/0019_sprint5_audit_workspace_nullable.py +50 -0
  85. backend/migrations/versions/0020_sprint5_oauth_access_token_jti.py +89 -0
  86. backend/migrations/versions/0021_sprint5_app_metadata.py +31 -0
  87. backend/migrations/versions/0022_sprint6_api_token_mcp_metadata.py +24 -0
  88. backend/migrations/versions/0023_sprint6_page_links.py +250 -0
  89. backend/migrations/versions/0024_sprint6_session_generation.py +30 -0
  90. backend/migrations/versions/0025_sprint6_oauth_jti_refresh_restrict.py +61 -0
  91. backend/migrations/versions/0026_sprint6_mcp_client_created_by_fk.py +74 -0
  92. backend/migrations/versions/0027_sprint6_context_body_md_canonical.py +95 -0
  93. backend/migrations/versions/0028_sprint6_context_check_constraints.py +108 -0
  94. backend/migrations/versions/0029_sprint6_page_link_target_fk.py +61 -0
  95. backend/migrations/versions/0030_sprint6_actor_names_aliases.py +121 -0
  96. backend/migrations/versions/0031_sprint6_restore_draft_versions.py +50 -0
  97. backend/migrations/versions/0032_sprint6_rename_chat_to_ask.py +310 -0
  98. backend/migrations/versions/0033_sprint6_mcp_chats.py +190 -0
  99. backend/migrations/versions/0034_sprint6_library_domain_rename.py +378 -0
  100. backend/migrations/versions/0035_sprint6_api_token_preview.py +25 -0
  101. backend/migrations/versions/0036_sprint6_user_default_workspace.py +41 -0
  102. backend/migrations/versions/0037_sprint6_mcp_chat_owner_user.py +70 -0
  103. backend/migrations/versions/0038_sprint6_drop_review_assignee.py +35 -0
  104. backend/migrations/versions/0039_sprint6_changeset_origin_chat_session.py +68 -0
  105. backend/migrations/versions/0040_sprint6_mcp_per_call_artifact.py +104 -0
  106. backend/migrations/versions/0041_sprint6_review_audience_claim.py +58 -0
  107. backend/migrations/versions/0042_sprint6_context_page_public.py +33 -0
  108. backend/migrations/versions/0043_sprint6_ask_auto_title.py +43 -0
  109. backend/migrations/versions/0044_sprint6_review_risk_policy.py +96 -0
  110. backend/migrations/versions/0045_sprint6_auto_approve_by_risk.py +84 -0
  111. backend/migrations/versions/0046_sprint6_auto_approve_rename_allowed.py +94 -0
  112. backend/migrations/versions/0047_sprint6_library_item_tombstone.py +57 -0
  113. backend/models/__init__.py +98 -0
  114. backend/models/agent.py +128 -0
  115. backend/models/api_token.py +37 -0
  116. backend/models/app_metadata.py +10 -0
  117. backend/models/ask.py +177 -0
  118. backend/models/audit.py +51 -0
  119. backend/models/base.py +29 -0
  120. backend/models/bundle.py +102 -0
  121. backend/models/change.py +66 -0
  122. backend/models/context.py +293 -0
  123. backend/models/feedback.py +28 -0
  124. backend/models/job.py +58 -0
  125. backend/models/library.py +176 -0
  126. backend/models/library_discovery.py +152 -0
  127. backend/models/lint.py +57 -0
  128. backend/models/mcp_chat.py +186 -0
  129. backend/models/mcp_trace.py +96 -0
  130. backend/models/oauth.py +166 -0
  131. backend/models/review.py +89 -0
  132. backend/models/tree.py +147 -0
  133. backend/models/user.py +50 -0
  134. backend/models/workspace.py +207 -0
  135. backend/policy/__init__.py +1 -0
  136. backend/policy/auth.py +243 -0
  137. backend/policy/rbac.py +221 -0
  138. backend/policy/remote_ip.py +51 -0
  139. backend/policy/rest_scopes.py +405 -0
  140. backend/policy/trust_boundaries.py +196 -0
  141. backend/policy/workspace_resolution.py +150 -0
  142. backend/repositories/__init__.py +1 -0
  143. backend/repositories/audit_repo.py +38 -0
  144. backend/repositories/context_page_repo.py +216 -0
  145. backend/repositories/workspace_repo.py +15 -0
  146. backend/schemas/__init__.py +1 -0
  147. backend/schemas/activity.py +55 -0
  148. backend/schemas/agent.py +135 -0
  149. backend/schemas/ask.py +94 -0
  150. backend/schemas/auth.py +172 -0
  151. backend/schemas/common.py +194 -0
  152. backend/schemas/context_ingress.py +122 -0
  153. backend/schemas/context_write.py +521 -0
  154. backend/schemas/deployment.py +45 -0
  155. backend/schemas/health.py +51 -0
  156. backend/schemas/lint.py +78 -0
  157. backend/schemas/mcp/__init__.py +1 -0
  158. backend/schemas/mcp/agent_tools.py +49 -0
  159. backend/schemas/mcp/context_tools.py +81 -0
  160. backend/schemas/mcp/page_tools.py +103 -0
  161. backend/schemas/mcp/read_tools.py +161 -0
  162. backend/schemas/mcp/review_tools.py +137 -0
  163. backend/schemas/mcp_chat.py +195 -0
  164. backend/schemas/page.py +323 -0
  165. backend/schemas/read.py +425 -0
  166. backend/schemas/settings.py +52 -0
  167. backend/schemas/tree.py +52 -0
  168. backend/schemas/upload.py +16 -0
  169. backend/schemas/user.py +21 -0
  170. backend/scripts/__init__.py +1 -0
  171. backend/scripts/export_mcp_schemas.py +127 -0
  172. backend/scripts/export_openapi.py +17 -0
  173. backend/scripts/migrate_data.py +374 -0
  174. backend/scripts/seed_dev.py +378 -0
  175. backend/scripts/seed_remote_mcp_smoke.py +336 -0
  176. backend/services/__init__.py +1 -0
  177. backend/services/activity_service.py +476 -0
  178. backend/services/actor_identities.py +16 -0
  179. backend/services/agent_page_service.py +532 -0
  180. backend/services/anchor_resolver.py +248 -0
  181. backend/services/api_token_service.py +296 -0
  182. backend/services/ask_service.py +623 -0
  183. backend/services/audit_service.py +37 -0
  184. backend/services/bundle_expiration.py +103 -0
  185. backend/services/bundle_service.py +475 -0
  186. backend/services/change_review_audit.py +351 -0
  187. backend/services/citation_service.py +236 -0
  188. backend/services/context_ingress_service.py +694 -0
  189. backend/services/context_write_service.py +784 -0
  190. backend/services/credential_hashing.py +92 -0
  191. backend/services/deployment.py +269 -0
  192. backend/services/domain_read.py +875 -0
  193. backend/services/extraction_service.py +238 -0
  194. backend/services/feedback_errors.py +18 -0
  195. backend/services/feedback_resolver_service.py +58 -0
  196. backend/services/feedback_service.py +234 -0
  197. backend/services/job_queue.py +119 -0
  198. backend/services/library_service.py +747 -0
  199. backend/services/library_storage.py +55 -0
  200. backend/services/lint_service.py +147 -0
  201. backend/services/mcp_chat_service.py +900 -0
  202. backend/services/mcp_client_service.py +149 -0
  203. backend/services/mcp_surface_service.py +260 -0
  204. backend/services/mcp_trace_capture.py +381 -0
  205. backend/services/oauth_service.py +1800 -0
  206. backend/services/page_service.py +1879 -0
  207. backend/services/read_errors.py +68 -0
  208. backend/services/restricted_library_policy.py +32 -0
  209. backend/services/review_risk_policy.py +483 -0
  210. backend/services/review_service.py +1640 -0
  211. backend/services/search_service.py +442 -0
  212. backend/services/session_service.py +367 -0
  213. backend/services/settings_service.py +654 -0
  214. backend/services/tree_read.py +264 -0
  215. backend/services/tree_service.py +644 -0
  216. backend/services/user_admin_service.py +66 -0
  217. backend/services/workspace_membership_service.py +97 -0
  218. backend/services/workspace_read.py +329 -0
  219. backend/services/workspace_write.py +292 -0
  220. backend/static/assets/AccountSecurityScreen-DiqtgYNv.js +1 -0
  221. backend/static/assets/ActivityScreen-Bo-O3wrB.js +1 -0
  222. backend/static/assets/AdvancedSection-CTCsjBty.js +1 -0
  223. backend/static/assets/AgentScreen-FQXe7sbN.js +3 -0
  224. backend/static/assets/ArtifactChatsSection-BubCHY0m.js +1 -0
  225. backend/static/assets/AskScreen-Dqc4wuxZ.js +1 -0
  226. backend/static/assets/AuthScreen-vpDdVOyl.js +1 -0
  227. backend/static/assets/Breadcrumbs-DP6tKrMn.js +1 -0
  228. backend/static/assets/ChatsDetailScreen-DA4L4hTh.js +1 -0
  229. backend/static/assets/ChatsListScreen-Bn1PA3RK.js +1 -0
  230. backend/static/assets/ClientChip-DjqI-fCu.js +1 -0
  231. backend/static/assets/ContextScreen-DIHjETDO.js +1 -0
  232. backend/static/assets/DashboardScreen-S-7Ix7tx.js +1 -0
  233. backend/static/assets/Dialog-Vb4frv23.js +1 -0
  234. backend/static/assets/DiffViewer-B-Tm0Rlp.js +1 -0
  235. backend/static/assets/DiffViewerDialog-BeYlLkBC.js +1 -0
  236. backend/static/assets/FirstUseChecklist-C8AKYWCQ.js +1 -0
  237. backend/static/assets/FormField-BNzNKUos.js +1 -0
  238. backend/static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  239. backend/static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  240. backend/static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  241. backend/static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  242. backend/static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  243. backend/static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  244. backend/static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  245. backend/static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  246. backend/static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  247. backend/static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  248. backend/static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  249. backend/static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  250. backend/static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  251. backend/static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  252. backend/static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  253. backend/static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  254. backend/static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  255. backend/static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  256. backend/static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  257. backend/static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  258. backend/static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  259. backend/static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  260. backend/static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  261. backend/static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  262. backend/static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  263. backend/static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  264. backend/static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  265. backend/static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  266. backend/static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  267. backend/static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  268. backend/static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  269. backend/static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  270. backend/static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  271. backend/static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  272. backend/static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  273. backend/static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  274. backend/static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  275. backend/static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  276. backend/static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  277. backend/static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  278. backend/static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  279. backend/static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  280. backend/static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  281. backend/static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  282. backend/static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  283. backend/static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  284. backend/static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  285. backend/static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  286. backend/static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  287. backend/static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  288. backend/static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  289. backend/static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  290. backend/static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  291. backend/static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  292. backend/static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  293. backend/static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  294. backend/static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  295. backend/static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  296. backend/static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  297. backend/static/assets/LibraryScreen-DU8DIEN1.js +2 -0
  298. backend/static/assets/LintScreen-Ck8G5qFr.js +1 -0
  299. backend/static/assets/MarkdownView-BpTgdD6k.css +1 -0
  300. backend/static/assets/MarkdownView-CGRbHpcO.js +9 -0
  301. backend/static/assets/PageEditor-DBY5SoQN.js +64 -0
  302. backend/static/assets/PageList-yeqekuM2.js +1 -0
  303. backend/static/assets/PageReader-CbFNMzqu.js +1 -0
  304. backend/static/assets/PagesLanding-BuRXy1Mw.js +1 -0
  305. backend/static/assets/PdfLibraryPreview-DeOASwDs.js +1 -0
  306. backend/static/assets/PublishedPageReader-DgNxuHVX.js +1 -0
  307. backend/static/assets/ReaderTabs-CW9i9iAQ.js +1 -0
  308. backend/static/assets/ReviewScreen-CVJ-8CZV.js +1 -0
  309. backend/static/assets/SearchField-BapgB6om.js +1 -0
  310. backend/static/assets/SearchScreen-BzzrKHsg.js +1 -0
  311. backend/static/assets/SegmentedControl-DmN2cD1q.js +1 -0
  312. backend/static/assets/SettingsScreen-CGBXnRha.js +5 -0
  313. backend/static/assets/SetupScreen-DleZsBoV.js +1 -0
  314. backend/static/assets/StatusPill-DKi2M2_W.js +1 -0
  315. backend/static/assets/TabbedInspector-CGd3S1nF.js +1 -0
  316. backend/static/assets/TreeNodeActionDialogs-ByCKoAKa.js +1 -0
  317. backend/static/assets/WorkspaceLayout-DAuixhhE.js +1 -0
  318. backend/static/assets/WorkspacePanelChrome-CazCCL7R.js +1 -0
  319. backend/static/assets/agent-DSSb30Yg.js +1 -0
  320. backend/static/assets/arc-DOA1mDnW.js +1 -0
  321. backend/static/assets/architectureDiagram-3BPJPVTR-BlLZuU3B.js +36 -0
  322. backend/static/assets/blockDiagram-GPEHLZMM-TsVMwqa7.js +132 -0
  323. backend/static/assets/c4Diagram-AAUBKEIU-D1Jir-z6.js +10 -0
  324. backend/static/assets/channel-BPrSEbJD.js +1 -0
  325. backend/static/assets/chunk-2J33WTMH-DNdwDO-t.js +1 -0
  326. backend/static/assets/chunk-4BX2VUAB-C6UYYSFQ.js +1 -0
  327. backend/static/assets/chunk-55IACEB6-DYF1SBFk.js +1 -0
  328. backend/static/assets/chunk-727SXJPM-TKztczVt.js +206 -0
  329. backend/static/assets/chunk-AQP2D5EJ-BQ35mjEn.js +231 -0
  330. backend/static/assets/chunk-FMBD7UC4-B8CjS_KZ.js +15 -0
  331. backend/static/assets/chunk-ND2GUHAM-CKPlR7DV.js +1 -0
  332. backend/static/assets/chunk-QZHKN3VN-DgBTuQ_m.js +1 -0
  333. backend/static/assets/classDiagram-4FO5ZUOK-BOEcTU37.js +1 -0
  334. backend/static/assets/classDiagram-v2-Q7XG4LA2-BOEcTU37.js +1 -0
  335. backend/static/assets/context-GkNFjSlJ.js +1 -0
  336. backend/static/assets/cose-bilkent-S5V4N54A-B42Xuae7.js +1 -0
  337. backend/static/assets/cytoscape.esm-CkSuTymj.js +321 -0
  338. backend/static/assets/dagre-BM42HDAG-DnwvVl7c.js +4 -0
  339. backend/static/assets/datetime-BnjnLyld.js +1 -0
  340. backend/static/assets/defaultLocale-DX6XiGOO.js +1 -0
  341. backend/static/assets/diagram-2AECGRRQ-CHhcd3Tm.js +43 -0
  342. backend/static/assets/diagram-5GNKFQAL-CjfkjQTI.js +10 -0
  343. backend/static/assets/diagram-KO2AKTUF-C8P94VYb.js +3 -0
  344. backend/static/assets/diagram-LMA3HP47-D3LKUKZG.js +24 -0
  345. backend/static/assets/diagram-OG6HWLK6-CGk-FJDI.js +24 -0
  346. backend/static/assets/editor-core-Cdo7OTFt.js +1 -0
  347. backend/static/assets/editor-language-tools-CgxqIgMk.js +14 -0
  348. backend/static/assets/editor-parser-DSyH1Mzz.js +6 -0
  349. backend/static/assets/editor-react-DRW4r8ms.js +1 -0
  350. backend/static/assets/editor-view-BXEsFLud.js +11 -0
  351. backend/static/assets/editorCompletions-DuzU7UvS.js +1 -0
  352. backend/static/assets/erDiagram-TEJ5UH35-C1PyRkV0.js +85 -0
  353. backend/static/assets/flowDiagram-I6XJVG4X-3m6UNhdQ.js +162 -0
  354. backend/static/assets/ganttDiagram-6RSMTGT7-LQnXwYZS.js +292 -0
  355. backend/static/assets/gitGraphDiagram-PVQCEYII-BHa_Pet_.js +106 -0
  356. backend/static/assets/graph--OzhPTMs.js +1 -0
  357. backend/static/assets/index-BdTqqDwG.js +2 -0
  358. backend/static/assets/index-Cmd_pg4m.css +1 -0
  359. backend/static/assets/infoDiagram-5YYISTIA-C2mg4KKv.js +2 -0
  360. backend/static/assets/init-Gi6I4Gst.js +1 -0
  361. backend/static/assets/ishikawaDiagram-YF4QCWOH-CE-dFGYw.js +70 -0
  362. backend/static/assets/journeyDiagram-JHISSGLW-DqYwk8pB.js +139 -0
  363. backend/static/assets/kanban-definition-UN3LZRKU-BacEfSti.js +89 -0
  364. backend/static/assets/katex-HP8lGamR.js +257 -0
  365. backend/static/assets/layout-SsrduOYp.js +1 -0
  366. backend/static/assets/library-B6fRp5Gm.js +1 -0
  367. backend/static/assets/linear-DVlp5p9k.js +1 -0
  368. backend/static/assets/markdown-renderer-DePAehBt.js +298 -0
  369. backend/static/assets/mermaid.core-CYYJY67q.js +301 -0
  370. backend/static/assets/mindmap-definition-RKZ34NQL-DdKxWlof.js +96 -0
  371. backend/static/assets/oauth-DyP8Cxyk.js +1 -0
  372. backend/static/assets/ordinal-Cboi1Yqb.js +1 -0
  373. backend/static/assets/pages-CGETtzbr.js +1 -0
  374. backend/static/assets/pdf.worker.min-qwK7q_zL.mjs +28 -0
  375. backend/static/assets/permissions-ChixKY_L.js +1 -0
  376. backend/static/assets/pieDiagram-4H26LBE5-BEoevSrX.js +30 -0
  377. backend/static/assets/quadrantDiagram-W4KKPZXB-CwTyi4l7.js +7 -0
  378. backend/static/assets/react-vendor-B9Z7D9kT.css +1 -0
  379. backend/static/assets/react-vendor-D-A9oZv0.js +23 -0
  380. backend/static/assets/requirementDiagram-4Y6WPE33-C0ts8nzW.js +84 -0
  381. backend/static/assets/sankeyDiagram-5OEKKPKP-COb8cAU9.js +40 -0
  382. backend/static/assets/sequenceDiagram-3UESZ5HK-DQ4oJOSq.js +162 -0
  383. backend/static/assets/stateDiagram-AJRCARHV-DB7lbo8X.js +1 -0
  384. backend/static/assets/stateDiagram-v2-BHNVJYJU-BkBPd5nW.js +1 -0
  385. backend/static/assets/timeline-definition-PNZ67QCA-Bu-1ffiL.js +120 -0
  386. backend/static/assets/treeBreadcrumbs-CpaE1543.js +1 -0
  387. backend/static/assets/treeRouteState-BNlMLOl9.js +1 -0
  388. backend/static/assets/urlState-DNJd2clf.js +1 -0
  389. backend/static/assets/useAsyncLoad-BxqJIu7P.js +1 -0
  390. backend/static/assets/vennDiagram-CIIHVFJN-nVhYg_zO.js +34 -0
  391. backend/static/assets/wardley-L42UT6IY-BdL46ant.js +161 -0
  392. backend/static/assets/wardleyDiagram-YWT4CUSO-BGM9TwwH.js +78 -0
  393. backend/static/assets/xychartDiagram-2RQKCTM6-CMs6AC4_.js +7 -0
  394. backend/static/index.html +15 -0
  395. backend/utils/__init__.py +1 -0
  396. backend/utils/actor_display.py +26 -0
  397. backend/utils/content_hash.py +65 -0
  398. backend/utils/page_frontmatter.py +262 -0
  399. backend/utils/uuid.py +10 -0
  400. backend/worker/__init__.py +1 -0
  401. backend/worker/entry.py +146 -0
  402. backend/worker/runtime.py +455 -0
  403. dtxwiki-0.0.1.data/data/share/dtxwiki/DEPLOYMENT.md +261 -0
  404. dtxwiki-0.0.1.data/data/share/dtxwiki/infra/prod/.env.docker.example +24 -0
  405. dtxwiki-0.0.1.data/data/share/dtxwiki/infra/prod/.env.example +63 -0
  406. dtxwiki-0.0.1.data/data/share/dtxwiki/infra/prod/Caddyfile.docker +31 -0
  407. dtxwiki-0.0.1.data/data/share/dtxwiki/infra/prod/Caddyfile.example +28 -0
  408. dtxwiki-0.0.1.data/data/share/dtxwiki/infra/prod/docker-compose.yml +116 -0
  409. dtxwiki-0.0.1.data/data/share/dtxwiki/infra/prod/docker-entrypoint.sh +42 -0
  410. dtxwiki-0.0.1.data/data/share/dtxwiki/infra/prod/dtxwiki-worker.service +24 -0
  411. dtxwiki-0.0.1.data/data/share/dtxwiki/infra/prod/dtxwiki.service +25 -0
  412. dtxwiki-0.0.1.data/data/share/dtxwiki/infra/prod/install.sh +110 -0
  413. dtxwiki-0.0.1.data/data/share/dtxwiki/infra/prod/nginx.conf.example +38 -0
  414. dtxwiki-0.0.1.data/data/share/dtxwiki/tests/packaging/production_smoke.ps1 +110 -0
  415. dtxwiki-0.0.1.data/data/share/dtxwiki/tests/packaging/production_smoke.sh +86 -0
  416. dtxwiki-0.0.1.dist-info/METADATA +731 -0
  417. dtxwiki-0.0.1.dist-info/RECORD +422 -0
  418. dtxwiki-0.0.1.dist-info/WHEEL +5 -0
  419. dtxwiki-0.0.1.dist-info/entry_points.txt +4 -0
  420. dtxwiki-0.0.1.dist-info/licenses/LICENSE +186 -0
  421. dtxwiki-0.0.1.dist-info/licenses/NOTICE +4 -0
  422. dtxwiki-0.0.1.dist-info/top_level.txt +1 -0
backend/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """dtxWiki backend package."""
backend/alembic.ini ADDED
@@ -0,0 +1,38 @@
1
+ [alembic]
2
+ script_location = backend/migrations
3
+ prepend_sys_path = .
4
+ path_separator = os
5
+ sqlalchemy.url = sqlite+pysqlite:///dtxwiki.sqlite
6
+
7
+ [loggers]
8
+ keys = root,sqlalchemy,alembic
9
+
10
+ [handlers]
11
+ keys = console
12
+
13
+ [formatters]
14
+ keys = generic
15
+
16
+ [logger_root]
17
+ level = WARNING
18
+ handlers = console
19
+ qualname =
20
+
21
+ [logger_sqlalchemy]
22
+ level = WARNING
23
+ handlers =
24
+ qualname = sqlalchemy.engine
25
+
26
+ [logger_alembic]
27
+ level = INFO
28
+ handlers =
29
+ qualname = alembic
30
+
31
+ [handler_console]
32
+ class = StreamHandler
33
+ args = (sys.stderr,)
34
+ level = NOTSET
35
+ formatter = generic
36
+
37
+ [formatter_generic]
38
+ format = %(levelname)-5.5s [%(name)s] %(message)s
@@ -0,0 +1 @@
1
+ """HTTP API routers."""
backend/api/auth.py ADDED
@@ -0,0 +1,466 @@
1
+ from typing import Annotated
2
+ from uuid import UUID
3
+
4
+ from fastapi import APIRouter, Depends, Path, Request, Response
5
+ from fastapi.responses import JSONResponse
6
+
7
+ from backend.api.deps import SessionFactoryDep
8
+ from backend.app.errors import build_rest_error_response
9
+ from backend.app.settings import Settings
10
+ from backend.schemas.auth import (
11
+ ApiTokenCreateRequest,
12
+ ApiTokenDeleteResponse,
13
+ ApiTokenIssueResponse,
14
+ ApiTokenListResponse,
15
+ ApiTokenRevokeResponse,
16
+ ApiTokenUpdateRequest,
17
+ AuthLogoutResponse,
18
+ AuthMeResponse,
19
+ AuthSessionResponse,
20
+ LoginRequest,
21
+ PasswordChangeRequest,
22
+ SignupRequest,
23
+ )
24
+ from backend.schemas.common import RESTErrorBody
25
+ from backend.services.api_token_service import (
26
+ ApiTokenNotFoundError,
27
+ ApiTokenRecord,
28
+ ApiTokenService,
29
+ ApiTokenServiceError,
30
+ InvalidApiTokenRequestError,
31
+ )
32
+ from backend.services.audit_service import AuditService
33
+ from backend.services.credential_hashing import CredentialHasher
34
+ from backend.services.session_service import (
35
+ AuthenticatedUser,
36
+ DuplicateEmailError,
37
+ InvalidCredentialsError,
38
+ InvalidSessionError,
39
+ PasswordPolicyError,
40
+ SessionResult,
41
+ SessionService,
42
+ SessionServiceError,
43
+ SessionTokenCodec,
44
+ SuspendedUserError,
45
+ )
46
+
47
+ router = APIRouter(prefix="/auth", tags=["auth"])
48
+
49
+ REST_ERROR_RESPONSES = {
50
+ 401: {"model": RESTErrorBody},
51
+ 403: {"model": RESTErrorBody},
52
+ 404: {"model": RESTErrorBody},
53
+ 409: {"model": RESTErrorBody},
54
+ 422: {"model": RESTErrorBody},
55
+ }
56
+
57
+
58
+ def get_session_service(
59
+ request: Request,
60
+ session_factory: SessionFactoryDep,
61
+ ) -> SessionService:
62
+ if hasattr(request.app.state, "session_service"):
63
+ return request.app.state.session_service
64
+ settings = request.app.state.settings
65
+ service = SessionService(
66
+ session_factory=session_factory,
67
+ credential_hasher=CredentialHasher(),
68
+ token_codec=SessionTokenCodec(
69
+ secret=session_secret(settings),
70
+ ttl_seconds=settings.session_ttl_seconds,
71
+ ),
72
+ default_workspace_slug=settings.workspace_slug,
73
+ audit_service=AuditService(),
74
+ )
75
+ request.app.state.session_service = service
76
+ return service
77
+
78
+
79
+ SessionServiceDep = Annotated[SessionService, Depends(get_session_service)]
80
+
81
+
82
+ def get_api_token_service(
83
+ request: Request,
84
+ session_factory: SessionFactoryDep,
85
+ ) -> ApiTokenService:
86
+ if hasattr(request.app.state, "api_token_service"):
87
+ return request.app.state.api_token_service
88
+ service = ApiTokenService(
89
+ session_factory=session_factory,
90
+ credential_hasher=CredentialHasher(),
91
+ )
92
+ request.app.state.api_token_service = service
93
+ return service
94
+
95
+
96
+ ApiTokenServiceDep = Annotated[ApiTokenService, Depends(get_api_token_service)]
97
+
98
+
99
+ @router.post(
100
+ "/signup",
101
+ status_code=201,
102
+ response_model=AuthSessionResponse,
103
+ responses=REST_ERROR_RESPONSES,
104
+ )
105
+ def signup(
106
+ request_body: SignupRequest,
107
+ response: Response,
108
+ request: Request,
109
+ session_service: SessionServiceDep,
110
+ ):
111
+ try:
112
+ result = session_service.signup(
113
+ email=request_body.email,
114
+ password=request_body.password,
115
+ display_name=request_body.display_name,
116
+ )
117
+ except SessionServiceError as exc:
118
+ return auth_error_response(exc)
119
+ set_session_cookie(
120
+ response,
121
+ request.app.state.settings,
122
+ request,
123
+ result,
124
+ remember_me=request_body.remember_me,
125
+ )
126
+ return {"data": session_payload(result)}
127
+
128
+
129
+ @router.post(
130
+ "/login",
131
+ response_model=AuthSessionResponse,
132
+ responses=REST_ERROR_RESPONSES,
133
+ )
134
+ def login(
135
+ request_body: LoginRequest,
136
+ response: Response,
137
+ request: Request,
138
+ session_service: SessionServiceDep,
139
+ ):
140
+ try:
141
+ result = session_service.login(
142
+ email=request_body.email,
143
+ password=request_body.password,
144
+ )
145
+ except SessionServiceError as exc:
146
+ return auth_error_response(exc)
147
+ set_session_cookie(
148
+ response,
149
+ request.app.state.settings,
150
+ request,
151
+ result,
152
+ remember_me=request_body.remember_me,
153
+ )
154
+ return {"data": session_payload(result)}
155
+
156
+
157
+ @router.post(
158
+ "/logout",
159
+ response_model=AuthLogoutResponse,
160
+ responses=REST_ERROR_RESPONSES,
161
+ )
162
+ def logout(
163
+ request: Request,
164
+ response: Response,
165
+ session_service: SessionServiceDep,
166
+ ):
167
+ token = request.cookies.get(request.app.state.settings.session_cookie_name)
168
+ try:
169
+ session_service.logout(token)
170
+ except InvalidSessionError:
171
+ pass
172
+ clear_session_cookie(response, request.app.state.settings, request)
173
+ return {"data": {"logged_out": True}}
174
+
175
+
176
+ @router.get(
177
+ "/me",
178
+ response_model=AuthMeResponse,
179
+ responses=REST_ERROR_RESPONSES,
180
+ )
181
+ def me(
182
+ request: Request,
183
+ session_service: SessionServiceDep,
184
+ ):
185
+ try:
186
+ user = current_session_user(request, session_service)
187
+ except SessionServiceError as exc:
188
+ return auth_error_response(exc)
189
+ return {"data": user_payload(user)}
190
+
191
+
192
+ @router.post(
193
+ "/password",
194
+ response_model=AuthSessionResponse,
195
+ responses=REST_ERROR_RESPONSES,
196
+ )
197
+ def change_password(
198
+ request_body: PasswordChangeRequest,
199
+ response: Response,
200
+ request: Request,
201
+ session_service: SessionServiceDep,
202
+ ):
203
+ try:
204
+ user = current_session_user(request, session_service)
205
+ result = session_service.change_password(
206
+ user_id=user.user_id,
207
+ current_password=request_body.current_password,
208
+ new_password=request_body.new_password,
209
+ )
210
+ except SessionServiceError as exc:
211
+ return auth_error_response(exc)
212
+ set_session_cookie(response, request.app.state.settings, request, result)
213
+ return {"data": session_payload(result)}
214
+
215
+
216
+ @router.get(
217
+ "/tokens",
218
+ response_model=ApiTokenListResponse,
219
+ responses=REST_ERROR_RESPONSES,
220
+ )
221
+ def list_api_tokens(
222
+ request: Request,
223
+ session_service: SessionServiceDep,
224
+ api_token_service: ApiTokenServiceDep,
225
+ ):
226
+ try:
227
+ user = current_session_user(request, session_service)
228
+ tokens = api_token_service.list_tokens(user.user_id)
229
+ except SessionServiceError as exc:
230
+ return auth_error_response(exc)
231
+ return {"data": [api_token_payload(token) for token in tokens]}
232
+
233
+
234
+ @router.post(
235
+ "/tokens",
236
+ status_code=201,
237
+ response_model=ApiTokenIssueResponse,
238
+ responses=REST_ERROR_RESPONSES,
239
+ )
240
+ def create_api_token(
241
+ request_body: ApiTokenCreateRequest,
242
+ request: Request,
243
+ session_service: SessionServiceDep,
244
+ api_token_service: ApiTokenServiceDep,
245
+ ):
246
+ try:
247
+ user = current_session_user(request, session_service)
248
+ issued = api_token_service.create_token(
249
+ user_id=user.user_id,
250
+ name=request_body.name,
251
+ scopes=request_body.scopes,
252
+ expires_at=request_body.expires_at,
253
+ mcp_client_type=request_body.mcp_client_type,
254
+ )
255
+ except SessionServiceError as exc:
256
+ return auth_error_response(exc)
257
+ except ApiTokenServiceError as exc:
258
+ return api_token_error_response(exc)
259
+ return {
260
+ "data": {
261
+ "api_token": api_token_payload(issued.api_token),
262
+ "plain_token": issued.plain_token,
263
+ }
264
+ }
265
+
266
+
267
+ @router.patch(
268
+ "/tokens/{token_id}",
269
+ response_model=ApiTokenRevokeResponse,
270
+ responses=REST_ERROR_RESPONSES,
271
+ )
272
+ def update_api_token(
273
+ token_id: Annotated[UUID, Path()],
274
+ request_body: ApiTokenUpdateRequest,
275
+ request: Request,
276
+ session_service: SessionServiceDep,
277
+ api_token_service: ApiTokenServiceDep,
278
+ ):
279
+ try:
280
+ user = current_session_user(request, session_service)
281
+ updated = api_token_service.update_token(
282
+ user_id=user.user_id,
283
+ token_id=token_id,
284
+ name=request_body.name,
285
+ )
286
+ except SessionServiceError as exc:
287
+ return auth_error_response(exc)
288
+ except ApiTokenServiceError as exc:
289
+ return api_token_error_response(exc)
290
+ return {"data": api_token_payload(updated)}
291
+
292
+
293
+ @router.delete(
294
+ "/tokens/{token_id}/record",
295
+ response_model=ApiTokenDeleteResponse,
296
+ responses=REST_ERROR_RESPONSES,
297
+ )
298
+ def delete_revoked_api_token(
299
+ token_id: Annotated[UUID, Path()],
300
+ request: Request,
301
+ session_service: SessionServiceDep,
302
+ api_token_service: ApiTokenServiceDep,
303
+ ):
304
+ try:
305
+ user = current_session_user(request, session_service)
306
+ deleted_id = api_token_service.delete_revoked_token(
307
+ user_id=user.user_id,
308
+ token_id=token_id,
309
+ )
310
+ except SessionServiceError as exc:
311
+ return auth_error_response(exc)
312
+ except ApiTokenServiceError as exc:
313
+ return api_token_error_response(exc)
314
+ return {"data": {"id": deleted_id, "deleted": True}}
315
+
316
+
317
+ @router.delete(
318
+ "/tokens/{token_id}",
319
+ response_model=ApiTokenRevokeResponse,
320
+ responses=REST_ERROR_RESPONSES,
321
+ )
322
+ def revoke_api_token(
323
+ token_id: Annotated[UUID, Path()],
324
+ request: Request,
325
+ session_service: SessionServiceDep,
326
+ api_token_service: ApiTokenServiceDep,
327
+ ):
328
+ try:
329
+ user = current_session_user(request, session_service)
330
+ revoked = api_token_service.revoke_token(
331
+ user_id=user.user_id,
332
+ token_id=token_id,
333
+ )
334
+ except SessionServiceError as exc:
335
+ return auth_error_response(exc)
336
+ except ApiTokenServiceError as exc:
337
+ return api_token_error_response(exc)
338
+ return {"data": api_token_payload(revoked)}
339
+
340
+
341
+ def current_session_user(
342
+ request: Request,
343
+ session_service: SessionService,
344
+ ) -> AuthenticatedUser:
345
+ token = request.cookies.get(request.app.state.settings.session_cookie_name)
346
+ if token is None:
347
+ raise InvalidSessionError("session cookie is missing")
348
+ return session_service.authenticate_session(token)
349
+
350
+
351
+ def session_secret(settings: Settings) -> str:
352
+ return settings.effective_session_secret
353
+
354
+
355
+ def set_session_cookie(
356
+ response: Response,
357
+ settings: Settings,
358
+ request: Request,
359
+ result: SessionResult,
360
+ *,
361
+ remember_me: bool = True,
362
+ ) -> None:
363
+ cookie_options = {
364
+ "key": settings.session_cookie_name,
365
+ "value": result.session_token,
366
+ "httponly": True,
367
+ "secure": session_cookie_secure_for_request(settings, request),
368
+ "samesite": "lax",
369
+ }
370
+ if remember_me:
371
+ cookie_options["max_age"] = settings.session_ttl_seconds
372
+ cookie_options["expires"] = result.expires_at
373
+ response.set_cookie(**cookie_options)
374
+
375
+
376
+ def clear_session_cookie(
377
+ response: Response,
378
+ settings: Settings,
379
+ request: Request,
380
+ ) -> None:
381
+ response.delete_cookie(
382
+ key=settings.session_cookie_name,
383
+ httponly=True,
384
+ secure=session_cookie_secure_for_request(settings, request),
385
+ samesite="lax",
386
+ )
387
+
388
+
389
+ def session_cookie_secure_for_request(settings: Settings, request: Request) -> bool:
390
+ if settings.session_cookie_secure is not None:
391
+ return settings.session_cookie_secure
392
+ if settings.public_base_url is not None or settings.is_production:
393
+ return settings.effective_session_cookie_secure
394
+ if settings.behind_proxy:
395
+ forwarded_proto = request.headers.get("x-forwarded-proto", "")
396
+ first_proto = forwarded_proto.split(",", 1)[0].strip().lower()
397
+ if first_proto == "https":
398
+ return True
399
+ return settings.effective_session_cookie_secure
400
+
401
+
402
+ def session_payload(result: SessionResult) -> dict:
403
+ return {
404
+ "user": user_payload(result.user),
405
+ "expires_at": result.expires_at,
406
+ }
407
+
408
+
409
+ def user_payload(user: AuthenticatedUser) -> dict:
410
+ return {
411
+ "user_id": user.user_id,
412
+ "email": user.email,
413
+ "display_name": user.display_name,
414
+ "status": user.status,
415
+ }
416
+
417
+
418
+ def api_token_payload(token: ApiTokenRecord) -> dict:
419
+ return {
420
+ "id": token.id,
421
+ "name": token.name,
422
+ "token_preview": token.token_preview,
423
+ "scopes": token.scopes,
424
+ "mcp_client_type": token.mcp_client_type,
425
+ "mcp_client_label": token.mcp_client_label,
426
+ "created_at": token.created_at,
427
+ "last_used_at": token.last_used_at,
428
+ "expires_at": token.expires_at,
429
+ "revoked_at": token.revoked_at,
430
+ }
431
+
432
+
433
+ def auth_error_response(exc: SessionServiceError) -> JSONResponse:
434
+ return build_rest_error_response(
435
+ error_code=exc.error_code,
436
+ message=str(exc) or exc.error_code,
437
+ status_code=status_code_for_auth_error(exc),
438
+ )
439
+
440
+
441
+ def api_token_error_response(exc: ApiTokenServiceError) -> JSONResponse:
442
+ return build_rest_error_response(
443
+ error_code=exc.error_code,
444
+ message=str(exc) or exc.error_code,
445
+ status_code=status_code_for_api_token_error(exc),
446
+ )
447
+
448
+
449
+ def status_code_for_auth_error(exc: SessionServiceError) -> int:
450
+ if isinstance(exc, DuplicateEmailError):
451
+ return 409
452
+ if isinstance(exc, InvalidCredentialsError | InvalidSessionError):
453
+ return 401
454
+ if isinstance(exc, SuspendedUserError):
455
+ return 403
456
+ if isinstance(exc, PasswordPolicyError):
457
+ return 422
458
+ return 500
459
+
460
+
461
+ def status_code_for_api_token_error(exc: ApiTokenServiceError) -> int:
462
+ if isinstance(exc, ApiTokenNotFoundError):
463
+ return 404
464
+ if isinstance(exc, InvalidApiTokenRequestError):
465
+ return 422
466
+ return 401
backend/api/deps.py ADDED
@@ -0,0 +1,61 @@
1
+ from typing import Annotated
2
+
3
+ from fastapi import Depends, Request
4
+ from fastapi.responses import JSONResponse
5
+ from sqlalchemy.orm import Session, sessionmaker
6
+
7
+ from backend.app.errors import build_rest_error_response
8
+ from backend.app.state import get_session_factory_from_app
9
+ from backend.policy.auth import ActorContext
10
+ from backend.policy.rest_scopes import require_token_scope
11
+ from backend.services.read_errors import (
12
+ BundleExpiredError,
13
+ ChatOwnerUnresolvableError,
14
+ DuplicateSlugError,
15
+ ReadNotFoundError,
16
+ ReadServiceError,
17
+ ReadValidationError,
18
+ RejectedByPolicyError,
19
+ ReviewAlreadyClaimedError,
20
+ ScopeDeniedError,
21
+ StaleBaseVersionError,
22
+ UnsupportedRetrievalModeError,
23
+ )
24
+
25
+
26
+ def get_session_factory(request: Request) -> sessionmaker[Session]:
27
+ return get_session_factory_from_app(request)
28
+
29
+
30
+ RestActorDep = Annotated[ActorContext, Depends(require_token_scope)]
31
+ SessionFactoryDep = Annotated[sessionmaker[Session], Depends(get_session_factory)]
32
+
33
+
34
+ def read_service_error_response(exc: ReadServiceError) -> JSONResponse:
35
+ return build_rest_error_response(
36
+ error_code=exc.error_code,
37
+ message=str(exc) or exc.error_code,
38
+ status_code=status_code_for_read_error(exc),
39
+ )
40
+
41
+
42
+ def status_code_for_read_error(exc: ReadServiceError) -> int:
43
+ if isinstance(exc, ReadNotFoundError):
44
+ return 404
45
+ if isinstance(exc, ScopeDeniedError | RejectedByPolicyError):
46
+ return 403
47
+ if isinstance(exc, BundleExpiredError):
48
+ return 410
49
+ if isinstance(exc, DuplicateSlugError):
50
+ return 409
51
+ if isinstance(exc, ReadValidationError):
52
+ return 422
53
+ if isinstance(exc, StaleBaseVersionError):
54
+ return 409
55
+ if isinstance(exc, UnsupportedRetrievalModeError):
56
+ return 422
57
+ if isinstance(exc, ChatOwnerUnresolvableError):
58
+ return 422
59
+ if isinstance(exc, ReviewAlreadyClaimedError):
60
+ return 409
61
+ return 500
backend/api/health.py ADDED
@@ -0,0 +1,107 @@
1
+ from fastapi import APIRouter, Request
2
+ from fastapi.responses import JSONResponse
3
+
4
+ from backend.app.db import database_health, database_ping
5
+ from backend.app.settings import Settings
6
+ from backend.schemas.health import (
7
+ HealthLiveResponse,
8
+ HealthReadyResponse,
9
+ HealthSmokeResponse,
10
+ )
11
+ from backend.services.oauth_service import _load_private_key
12
+
13
+ router = APIRouter(tags=["health"])
14
+ health_root_router = APIRouter(tags=["health"])
15
+ root_router = health_root_router
16
+
17
+
18
+ @router.get(
19
+ "/health",
20
+ response_model=HealthSmokeResponse,
21
+ )
22
+ def read_health(request: Request) -> HealthSmokeResponse:
23
+ settings: Settings = request.app.state.settings
24
+ return HealthSmokeResponse.model_validate({
25
+ "app": settings.app_name,
26
+ "version": settings.app_version,
27
+ "database": database_health(settings),
28
+ })
29
+
30
+
31
+ @health_root_router.get("/health/live", response_model=HealthLiveResponse)
32
+ def read_liveness(request: Request) -> HealthLiveResponse:
33
+ settings: Settings = request.app.state.settings
34
+ return HealthLiveResponse(
35
+ status="alive",
36
+ version=settings.app_version,
37
+ )
38
+
39
+
40
+ @health_root_router.get(
41
+ "/health/ready",
42
+ response_model=HealthReadyResponse,
43
+ responses={503: {"model": HealthReadyResponse}},
44
+ )
45
+ def read_readiness(request: Request) -> JSONResponse:
46
+ return _ready_response(request)
47
+
48
+
49
+ def _ready_response(request: Request) -> JSONResponse:
50
+ payload, status_code = _ready_payload(request)
51
+ return JSONResponse(payload.model_dump(), status_code=status_code)
52
+
53
+
54
+ def _ready_payload(request: Request) -> tuple[HealthReadyResponse, int]:
55
+ components = {
56
+ "db": _check_db(request),
57
+ "app_version": _check_app_version(request),
58
+ "mcp": _check_mcp(request),
59
+ "oauth_jwk": _check_oauth_jwk(request),
60
+ }
61
+ ready = all(value in {"ok", "n/a"} for value in components.values())
62
+ return (
63
+ HealthReadyResponse.model_validate({
64
+ "status": "ready" if ready else "unhealthy",
65
+ "components": components,
66
+ }),
67
+ 200 if ready else 503,
68
+ )
69
+
70
+
71
+ def _check_db(request: Request) -> str:
72
+ settings: Settings = request.app.state.settings
73
+ ping = database_ping(settings)
74
+ return ping["status"]
75
+
76
+
77
+ def _check_app_version(request: Request) -> str:
78
+ app_version_synced = getattr(request.app.state, "app_version_synced", None)
79
+ if app_version_synced is True:
80
+ return "ok"
81
+ if app_version_synced is False:
82
+ return "drift"
83
+ return "unsynced"
84
+
85
+
86
+ def _check_mcp(request: Request) -> str:
87
+ settings: Settings = request.app.state.settings
88
+ mcp_mode = getattr(settings, "mcp_http_mode", "json_stateless")
89
+ if mcp_mode == "json_stateless":
90
+ return "n/a"
91
+ if getattr(request.app.state, "mcp_session_manager", None) is not None:
92
+ return "ok"
93
+ return "manager_missing"
94
+
95
+
96
+ def _check_oauth_jwk(request: Request) -> str:
97
+ settings: Settings = request.app.state.settings
98
+ if not (
99
+ settings.oauth_jwt_private_key_pem
100
+ or settings.oauth_jwt_private_key_path
101
+ ):
102
+ return "ephemeral_unsafe"
103
+ try:
104
+ _load_private_key(settings)
105
+ except Exception:
106
+ return "invalid"
107
+ return "ok"