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.
- backend/__init__.py +1 -0
- backend/alembic.ini +38 -0
- backend/api/__init__.py +1 -0
- backend/api/auth.py +466 -0
- backend/api/deps.py +61 -0
- backend/api/health.py +107 -0
- backend/api/mcp_chats.py +243 -0
- backend/api/mcp_http.py +1430 -0
- backend/api/metrics.py +183 -0
- backend/api/middleware/public_headers.py +53 -0
- backend/api/middleware/request_log.py +115 -0
- backend/api/middleware/security_headers.py +52 -0
- backend/api/oauth.py +1082 -0
- backend/api/pages.py +570 -0
- backend/api/public.py +60 -0
- backend/api/read.py +2223 -0
- backend/api/uploads.py +217 -0
- backend/api/users.py +61 -0
- backend/app/__init__.py +1 -0
- backend/app/bootstrap.py +148 -0
- backend/app/data_dir.py +31 -0
- backend/app/db.py +66 -0
- backend/app/errors.py +59 -0
- backend/app/main.py +241 -0
- backend/app/settings.py +315 -0
- backend/app/state.py +35 -0
- backend/app/version.py +32 -0
- backend/audit/__init__.py +1 -0
- backend/audit/emitter.py +130 -0
- backend/cli/__init__.py +1 -0
- backend/cli/integrations/__init__.py +1 -0
- backend/cli/integrations/_common.py +137 -0
- backend/cli/integrations/claude.py +112 -0
- backend/cli/integrations/codex.py +48 -0
- backend/cli/integrations/cursor.py +16 -0
- backend/cli/integrations/detect.py +99 -0
- backend/cli/main.py +1164 -0
- backend/cli/mcp_doctor.py +238 -0
- backend/cli/mcp_http_register.py +305 -0
- backend/cli/prod_setup.py +523 -0
- backend/llm/__init__.py +39 -0
- backend/llm/anthropic.py +122 -0
- backend/llm/base.py +109 -0
- backend/llm/openai.py +113 -0
- backend/llm/registry.py +17 -0
- backend/mcp/__init__.py +1 -0
- backend/mcp/agent_tools.py +85 -0
- backend/mcp/context_tools.py +162 -0
- backend/mcp/conversation.py +106 -0
- backend/mcp/entry.py +92 -0
- backend/mcp/envelope.py +53 -0
- backend/mcp/page_tools.py +150 -0
- backend/mcp/protocol/__init__.py +1 -0
- backend/mcp/protocol/prompts_resources.py +545 -0
- backend/mcp/protocol/runner.py +212 -0
- backend/mcp/protocol/server.py +87 -0
- backend/mcp/protocol/session_pool.py +27 -0
- backend/mcp/protocol/tool_descriptors.py +226 -0
- backend/mcp/read_tools.py +226 -0
- backend/mcp/review_tools.py +107 -0
- backend/mcp/router.py +208 -0
- backend/mcp/scopes.py +107 -0
- backend/mcp/stdio_server.py +11 -0
- backend/migrations/env.py +46 -0
- backend/migrations/script.py.mako +24 -0
- backend/migrations/versions/0001_p0_0_pages.py +118 -0
- backend/migrations/versions/0002_sprint2_workspace_client.py +223 -0
- backend/migrations/versions/0003_sprint2_job_discovery.py +178 -0
- backend/migrations/versions/0004_sprint2_library_item.py +177 -0
- backend/migrations/versions/0005_sprint2_agent_page.py +162 -0
- backend/migrations/versions/0006_sprint2_agent_input_bundle.py +140 -0
- backend/migrations/versions/0007_sprint2_context_claim_citation.py +202 -0
- backend/migrations/versions/0008_sprint2_change_review_audit.py +391 -0
- backend/migrations/versions/0009_sprint2_full_text_indexes.py +150 -0
- backend/migrations/versions/0010_sprint2_pgvector_readiness.py +25 -0
- backend/migrations/versions/0011_sprint2_workspace_tree.py +185 -0
- backend/migrations/versions/0012_sprint2_chat_ingress.py +272 -0
- backend/migrations/versions/0013_sprint4_identity.py +98 -0
- backend/migrations/versions/0014_sprint4_workspace_memberships.py +106 -0
- backend/migrations/versions/0015_sprint4_oauth.py +167 -0
- backend/migrations/versions/0016_sprint4_refresh_token_argon2id.py +30 -0
- backend/migrations/versions/0017_sprint4_oauth_consent_workspace.py +41 -0
- backend/migrations/versions/0018_sprint4_fix3_oauth_workspace_bound_tokens.py +222 -0
- backend/migrations/versions/0019_sprint5_audit_workspace_nullable.py +50 -0
- backend/migrations/versions/0020_sprint5_oauth_access_token_jti.py +89 -0
- backend/migrations/versions/0021_sprint5_app_metadata.py +31 -0
- backend/migrations/versions/0022_sprint6_api_token_mcp_metadata.py +24 -0
- backend/migrations/versions/0023_sprint6_page_links.py +250 -0
- backend/migrations/versions/0024_sprint6_session_generation.py +30 -0
- backend/migrations/versions/0025_sprint6_oauth_jti_refresh_restrict.py +61 -0
- backend/migrations/versions/0026_sprint6_mcp_client_created_by_fk.py +74 -0
- backend/migrations/versions/0027_sprint6_context_body_md_canonical.py +95 -0
- backend/migrations/versions/0028_sprint6_context_check_constraints.py +108 -0
- backend/migrations/versions/0029_sprint6_page_link_target_fk.py +61 -0
- backend/migrations/versions/0030_sprint6_actor_names_aliases.py +121 -0
- backend/migrations/versions/0031_sprint6_restore_draft_versions.py +50 -0
- backend/migrations/versions/0032_sprint6_rename_chat_to_ask.py +310 -0
- backend/migrations/versions/0033_sprint6_mcp_chats.py +190 -0
- backend/migrations/versions/0034_sprint6_library_domain_rename.py +378 -0
- backend/migrations/versions/0035_sprint6_api_token_preview.py +25 -0
- backend/migrations/versions/0036_sprint6_user_default_workspace.py +41 -0
- backend/migrations/versions/0037_sprint6_mcp_chat_owner_user.py +70 -0
- backend/migrations/versions/0038_sprint6_drop_review_assignee.py +35 -0
- backend/migrations/versions/0039_sprint6_changeset_origin_chat_session.py +68 -0
- backend/migrations/versions/0040_sprint6_mcp_per_call_artifact.py +104 -0
- backend/migrations/versions/0041_sprint6_review_audience_claim.py +58 -0
- backend/migrations/versions/0042_sprint6_context_page_public.py +33 -0
- backend/migrations/versions/0043_sprint6_ask_auto_title.py +43 -0
- backend/migrations/versions/0044_sprint6_review_risk_policy.py +96 -0
- backend/migrations/versions/0045_sprint6_auto_approve_by_risk.py +84 -0
- backend/migrations/versions/0046_sprint6_auto_approve_rename_allowed.py +94 -0
- backend/migrations/versions/0047_sprint6_library_item_tombstone.py +57 -0
- backend/models/__init__.py +98 -0
- backend/models/agent.py +128 -0
- backend/models/api_token.py +37 -0
- backend/models/app_metadata.py +10 -0
- backend/models/ask.py +177 -0
- backend/models/audit.py +51 -0
- backend/models/base.py +29 -0
- backend/models/bundle.py +102 -0
- backend/models/change.py +66 -0
- backend/models/context.py +293 -0
- backend/models/feedback.py +28 -0
- backend/models/job.py +58 -0
- backend/models/library.py +176 -0
- backend/models/library_discovery.py +152 -0
- backend/models/lint.py +57 -0
- backend/models/mcp_chat.py +186 -0
- backend/models/mcp_trace.py +96 -0
- backend/models/oauth.py +166 -0
- backend/models/review.py +89 -0
- backend/models/tree.py +147 -0
- backend/models/user.py +50 -0
- backend/models/workspace.py +207 -0
- backend/policy/__init__.py +1 -0
- backend/policy/auth.py +243 -0
- backend/policy/rbac.py +221 -0
- backend/policy/remote_ip.py +51 -0
- backend/policy/rest_scopes.py +405 -0
- backend/policy/trust_boundaries.py +196 -0
- backend/policy/workspace_resolution.py +150 -0
- backend/repositories/__init__.py +1 -0
- backend/repositories/audit_repo.py +38 -0
- backend/repositories/context_page_repo.py +216 -0
- backend/repositories/workspace_repo.py +15 -0
- backend/schemas/__init__.py +1 -0
- backend/schemas/activity.py +55 -0
- backend/schemas/agent.py +135 -0
- backend/schemas/ask.py +94 -0
- backend/schemas/auth.py +172 -0
- backend/schemas/common.py +194 -0
- backend/schemas/context_ingress.py +122 -0
- backend/schemas/context_write.py +521 -0
- backend/schemas/deployment.py +45 -0
- backend/schemas/health.py +51 -0
- backend/schemas/lint.py +78 -0
- backend/schemas/mcp/__init__.py +1 -0
- backend/schemas/mcp/agent_tools.py +49 -0
- backend/schemas/mcp/context_tools.py +81 -0
- backend/schemas/mcp/page_tools.py +103 -0
- backend/schemas/mcp/read_tools.py +161 -0
- backend/schemas/mcp/review_tools.py +137 -0
- backend/schemas/mcp_chat.py +195 -0
- backend/schemas/page.py +323 -0
- backend/schemas/read.py +425 -0
- backend/schemas/settings.py +52 -0
- backend/schemas/tree.py +52 -0
- backend/schemas/upload.py +16 -0
- backend/schemas/user.py +21 -0
- backend/scripts/__init__.py +1 -0
- backend/scripts/export_mcp_schemas.py +127 -0
- backend/scripts/export_openapi.py +17 -0
- backend/scripts/migrate_data.py +374 -0
- backend/scripts/seed_dev.py +378 -0
- backend/scripts/seed_remote_mcp_smoke.py +336 -0
- backend/services/__init__.py +1 -0
- backend/services/activity_service.py +476 -0
- backend/services/actor_identities.py +16 -0
- backend/services/agent_page_service.py +532 -0
- backend/services/anchor_resolver.py +248 -0
- backend/services/api_token_service.py +296 -0
- backend/services/ask_service.py +623 -0
- backend/services/audit_service.py +37 -0
- backend/services/bundle_expiration.py +103 -0
- backend/services/bundle_service.py +475 -0
- backend/services/change_review_audit.py +351 -0
- backend/services/citation_service.py +236 -0
- backend/services/context_ingress_service.py +694 -0
- backend/services/context_write_service.py +784 -0
- backend/services/credential_hashing.py +92 -0
- backend/services/deployment.py +269 -0
- backend/services/domain_read.py +875 -0
- backend/services/extraction_service.py +238 -0
- backend/services/feedback_errors.py +18 -0
- backend/services/feedback_resolver_service.py +58 -0
- backend/services/feedback_service.py +234 -0
- backend/services/job_queue.py +119 -0
- backend/services/library_service.py +747 -0
- backend/services/library_storage.py +55 -0
- backend/services/lint_service.py +147 -0
- backend/services/mcp_chat_service.py +900 -0
- backend/services/mcp_client_service.py +149 -0
- backend/services/mcp_surface_service.py +260 -0
- backend/services/mcp_trace_capture.py +381 -0
- backend/services/oauth_service.py +1800 -0
- backend/services/page_service.py +1879 -0
- backend/services/read_errors.py +68 -0
- backend/services/restricted_library_policy.py +32 -0
- backend/services/review_risk_policy.py +483 -0
- backend/services/review_service.py +1640 -0
- backend/services/search_service.py +442 -0
- backend/services/session_service.py +367 -0
- backend/services/settings_service.py +654 -0
- backend/services/tree_read.py +264 -0
- backend/services/tree_service.py +644 -0
- backend/services/user_admin_service.py +66 -0
- backend/services/workspace_membership_service.py +97 -0
- backend/services/workspace_read.py +329 -0
- backend/services/workspace_write.py +292 -0
- backend/static/assets/AccountSecurityScreen-DiqtgYNv.js +1 -0
- backend/static/assets/ActivityScreen-Bo-O3wrB.js +1 -0
- backend/static/assets/AdvancedSection-CTCsjBty.js +1 -0
- backend/static/assets/AgentScreen-FQXe7sbN.js +3 -0
- backend/static/assets/ArtifactChatsSection-BubCHY0m.js +1 -0
- backend/static/assets/AskScreen-Dqc4wuxZ.js +1 -0
- backend/static/assets/AuthScreen-vpDdVOyl.js +1 -0
- backend/static/assets/Breadcrumbs-DP6tKrMn.js +1 -0
- backend/static/assets/ChatsDetailScreen-DA4L4hTh.js +1 -0
- backend/static/assets/ChatsListScreen-Bn1PA3RK.js +1 -0
- backend/static/assets/ClientChip-DjqI-fCu.js +1 -0
- backend/static/assets/ContextScreen-DIHjETDO.js +1 -0
- backend/static/assets/DashboardScreen-S-7Ix7tx.js +1 -0
- backend/static/assets/Dialog-Vb4frv23.js +1 -0
- backend/static/assets/DiffViewer-B-Tm0Rlp.js +1 -0
- backend/static/assets/DiffViewerDialog-BeYlLkBC.js +1 -0
- backend/static/assets/FirstUseChecklist-C8AKYWCQ.js +1 -0
- backend/static/assets/FormField-BNzNKUos.js +1 -0
- backend/static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- backend/static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- backend/static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- backend/static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- backend/static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- backend/static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- backend/static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- backend/static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- backend/static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- backend/static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- backend/static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- backend/static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- backend/static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- backend/static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- backend/static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- backend/static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- backend/static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- backend/static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- backend/static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- backend/static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- backend/static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- backend/static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- backend/static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- backend/static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- backend/static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- backend/static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- backend/static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- backend/static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- backend/static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- backend/static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- backend/static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- backend/static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- backend/static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- backend/static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- backend/static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- backend/static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- backend/static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- backend/static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- backend/static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- backend/static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- backend/static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- backend/static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- backend/static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- backend/static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- backend/static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- backend/static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- backend/static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- backend/static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- backend/static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- backend/static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- backend/static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- backend/static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- backend/static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- backend/static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- backend/static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- backend/static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- backend/static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- backend/static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- backend/static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- backend/static/assets/LibraryScreen-DU8DIEN1.js +2 -0
- backend/static/assets/LintScreen-Ck8G5qFr.js +1 -0
- backend/static/assets/MarkdownView-BpTgdD6k.css +1 -0
- backend/static/assets/MarkdownView-CGRbHpcO.js +9 -0
- backend/static/assets/PageEditor-DBY5SoQN.js +64 -0
- backend/static/assets/PageList-yeqekuM2.js +1 -0
- backend/static/assets/PageReader-CbFNMzqu.js +1 -0
- backend/static/assets/PagesLanding-BuRXy1Mw.js +1 -0
- backend/static/assets/PdfLibraryPreview-DeOASwDs.js +1 -0
- backend/static/assets/PublishedPageReader-DgNxuHVX.js +1 -0
- backend/static/assets/ReaderTabs-CW9i9iAQ.js +1 -0
- backend/static/assets/ReviewScreen-CVJ-8CZV.js +1 -0
- backend/static/assets/SearchField-BapgB6om.js +1 -0
- backend/static/assets/SearchScreen-BzzrKHsg.js +1 -0
- backend/static/assets/SegmentedControl-DmN2cD1q.js +1 -0
- backend/static/assets/SettingsScreen-CGBXnRha.js +5 -0
- backend/static/assets/SetupScreen-DleZsBoV.js +1 -0
- backend/static/assets/StatusPill-DKi2M2_W.js +1 -0
- backend/static/assets/TabbedInspector-CGd3S1nF.js +1 -0
- backend/static/assets/TreeNodeActionDialogs-ByCKoAKa.js +1 -0
- backend/static/assets/WorkspaceLayout-DAuixhhE.js +1 -0
- backend/static/assets/WorkspacePanelChrome-CazCCL7R.js +1 -0
- backend/static/assets/agent-DSSb30Yg.js +1 -0
- backend/static/assets/arc-DOA1mDnW.js +1 -0
- backend/static/assets/architectureDiagram-3BPJPVTR-BlLZuU3B.js +36 -0
- backend/static/assets/blockDiagram-GPEHLZMM-TsVMwqa7.js +132 -0
- backend/static/assets/c4Diagram-AAUBKEIU-D1Jir-z6.js +10 -0
- backend/static/assets/channel-BPrSEbJD.js +1 -0
- backend/static/assets/chunk-2J33WTMH-DNdwDO-t.js +1 -0
- backend/static/assets/chunk-4BX2VUAB-C6UYYSFQ.js +1 -0
- backend/static/assets/chunk-55IACEB6-DYF1SBFk.js +1 -0
- backend/static/assets/chunk-727SXJPM-TKztczVt.js +206 -0
- backend/static/assets/chunk-AQP2D5EJ-BQ35mjEn.js +231 -0
- backend/static/assets/chunk-FMBD7UC4-B8CjS_KZ.js +15 -0
- backend/static/assets/chunk-ND2GUHAM-CKPlR7DV.js +1 -0
- backend/static/assets/chunk-QZHKN3VN-DgBTuQ_m.js +1 -0
- backend/static/assets/classDiagram-4FO5ZUOK-BOEcTU37.js +1 -0
- backend/static/assets/classDiagram-v2-Q7XG4LA2-BOEcTU37.js +1 -0
- backend/static/assets/context-GkNFjSlJ.js +1 -0
- backend/static/assets/cose-bilkent-S5V4N54A-B42Xuae7.js +1 -0
- backend/static/assets/cytoscape.esm-CkSuTymj.js +321 -0
- backend/static/assets/dagre-BM42HDAG-DnwvVl7c.js +4 -0
- backend/static/assets/datetime-BnjnLyld.js +1 -0
- backend/static/assets/defaultLocale-DX6XiGOO.js +1 -0
- backend/static/assets/diagram-2AECGRRQ-CHhcd3Tm.js +43 -0
- backend/static/assets/diagram-5GNKFQAL-CjfkjQTI.js +10 -0
- backend/static/assets/diagram-KO2AKTUF-C8P94VYb.js +3 -0
- backend/static/assets/diagram-LMA3HP47-D3LKUKZG.js +24 -0
- backend/static/assets/diagram-OG6HWLK6-CGk-FJDI.js +24 -0
- backend/static/assets/editor-core-Cdo7OTFt.js +1 -0
- backend/static/assets/editor-language-tools-CgxqIgMk.js +14 -0
- backend/static/assets/editor-parser-DSyH1Mzz.js +6 -0
- backend/static/assets/editor-react-DRW4r8ms.js +1 -0
- backend/static/assets/editor-view-BXEsFLud.js +11 -0
- backend/static/assets/editorCompletions-DuzU7UvS.js +1 -0
- backend/static/assets/erDiagram-TEJ5UH35-C1PyRkV0.js +85 -0
- backend/static/assets/flowDiagram-I6XJVG4X-3m6UNhdQ.js +162 -0
- backend/static/assets/ganttDiagram-6RSMTGT7-LQnXwYZS.js +292 -0
- backend/static/assets/gitGraphDiagram-PVQCEYII-BHa_Pet_.js +106 -0
- backend/static/assets/graph--OzhPTMs.js +1 -0
- backend/static/assets/index-BdTqqDwG.js +2 -0
- backend/static/assets/index-Cmd_pg4m.css +1 -0
- backend/static/assets/infoDiagram-5YYISTIA-C2mg4KKv.js +2 -0
- backend/static/assets/init-Gi6I4Gst.js +1 -0
- backend/static/assets/ishikawaDiagram-YF4QCWOH-CE-dFGYw.js +70 -0
- backend/static/assets/journeyDiagram-JHISSGLW-DqYwk8pB.js +139 -0
- backend/static/assets/kanban-definition-UN3LZRKU-BacEfSti.js +89 -0
- backend/static/assets/katex-HP8lGamR.js +257 -0
- backend/static/assets/layout-SsrduOYp.js +1 -0
- backend/static/assets/library-B6fRp5Gm.js +1 -0
- backend/static/assets/linear-DVlp5p9k.js +1 -0
- backend/static/assets/markdown-renderer-DePAehBt.js +298 -0
- backend/static/assets/mermaid.core-CYYJY67q.js +301 -0
- backend/static/assets/mindmap-definition-RKZ34NQL-DdKxWlof.js +96 -0
- backend/static/assets/oauth-DyP8Cxyk.js +1 -0
- backend/static/assets/ordinal-Cboi1Yqb.js +1 -0
- backend/static/assets/pages-CGETtzbr.js +1 -0
- backend/static/assets/pdf.worker.min-qwK7q_zL.mjs +28 -0
- backend/static/assets/permissions-ChixKY_L.js +1 -0
- backend/static/assets/pieDiagram-4H26LBE5-BEoevSrX.js +30 -0
- backend/static/assets/quadrantDiagram-W4KKPZXB-CwTyi4l7.js +7 -0
- backend/static/assets/react-vendor-B9Z7D9kT.css +1 -0
- backend/static/assets/react-vendor-D-A9oZv0.js +23 -0
- backend/static/assets/requirementDiagram-4Y6WPE33-C0ts8nzW.js +84 -0
- backend/static/assets/sankeyDiagram-5OEKKPKP-COb8cAU9.js +40 -0
- backend/static/assets/sequenceDiagram-3UESZ5HK-DQ4oJOSq.js +162 -0
- backend/static/assets/stateDiagram-AJRCARHV-DB7lbo8X.js +1 -0
- backend/static/assets/stateDiagram-v2-BHNVJYJU-BkBPd5nW.js +1 -0
- backend/static/assets/timeline-definition-PNZ67QCA-Bu-1ffiL.js +120 -0
- backend/static/assets/treeBreadcrumbs-CpaE1543.js +1 -0
- backend/static/assets/treeRouteState-BNlMLOl9.js +1 -0
- backend/static/assets/urlState-DNJd2clf.js +1 -0
- backend/static/assets/useAsyncLoad-BxqJIu7P.js +1 -0
- backend/static/assets/vennDiagram-CIIHVFJN-nVhYg_zO.js +34 -0
- backend/static/assets/wardley-L42UT6IY-BdL46ant.js +161 -0
- backend/static/assets/wardleyDiagram-YWT4CUSO-BGM9TwwH.js +78 -0
- backend/static/assets/xychartDiagram-2RQKCTM6-CMs6AC4_.js +7 -0
- backend/static/index.html +15 -0
- backend/utils/__init__.py +1 -0
- backend/utils/actor_display.py +26 -0
- backend/utils/content_hash.py +65 -0
- backend/utils/page_frontmatter.py +262 -0
- backend/utils/uuid.py +10 -0
- backend/worker/__init__.py +1 -0
- backend/worker/entry.py +146 -0
- backend/worker/runtime.py +455 -0
- dtxwiki-0.0.1.data/data/share/dtxwiki/DEPLOYMENT.md +261 -0
- dtxwiki-0.0.1.data/data/share/dtxwiki/infra/prod/.env.docker.example +24 -0
- dtxwiki-0.0.1.data/data/share/dtxwiki/infra/prod/.env.example +63 -0
- dtxwiki-0.0.1.data/data/share/dtxwiki/infra/prod/Caddyfile.docker +31 -0
- dtxwiki-0.0.1.data/data/share/dtxwiki/infra/prod/Caddyfile.example +28 -0
- dtxwiki-0.0.1.data/data/share/dtxwiki/infra/prod/docker-compose.yml +116 -0
- dtxwiki-0.0.1.data/data/share/dtxwiki/infra/prod/docker-entrypoint.sh +42 -0
- dtxwiki-0.0.1.data/data/share/dtxwiki/infra/prod/dtxwiki-worker.service +24 -0
- dtxwiki-0.0.1.data/data/share/dtxwiki/infra/prod/dtxwiki.service +25 -0
- dtxwiki-0.0.1.data/data/share/dtxwiki/infra/prod/install.sh +110 -0
- dtxwiki-0.0.1.data/data/share/dtxwiki/infra/prod/nginx.conf.example +38 -0
- dtxwiki-0.0.1.data/data/share/dtxwiki/tests/packaging/production_smoke.ps1 +110 -0
- dtxwiki-0.0.1.data/data/share/dtxwiki/tests/packaging/production_smoke.sh +86 -0
- dtxwiki-0.0.1.dist-info/METADATA +731 -0
- dtxwiki-0.0.1.dist-info/RECORD +422 -0
- dtxwiki-0.0.1.dist-info/WHEEL +5 -0
- dtxwiki-0.0.1.dist-info/entry_points.txt +4 -0
- dtxwiki-0.0.1.dist-info/licenses/LICENSE +186 -0
- dtxwiki-0.0.1.dist-info/licenses/NOTICE +4 -0
- 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
|
backend/api/__init__.py
ADDED
|
@@ -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"
|