@taskforcehq/taskforce 0.3.170 → 0.3.301

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 (698) hide show
  1. package/README.md +71 -21
  2. package/dist/Taskforce.module.css +3394 -5981
  3. package/dist/TaskforceCore.CategoryFallback.test.js +5 -2
  4. package/dist/TaskforceCore.filter-persistence.test.js +402 -0
  5. package/dist/TaskforceCore.js +388 -71
  6. package/dist/TaskforceCore.routing.test.js +311 -0
  7. package/dist/TaskforceCore.test.js +3344 -615
  8. package/dist/TaskforceWidget.js +3 -2
  9. package/dist/__tests__/localizationCatalogs.test.js +48 -31
  10. package/dist/__tests__/workflowSimulation.test.js +2 -2
  11. package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.d.ts +60 -0
  12. package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.js +324 -0
  13. package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.test.js +278 -0
  14. package/dist/annotatedAttachments/payload.d.ts +2 -0
  15. package/dist/annotatedAttachments/payload.js +54 -0
  16. package/dist/annotatedAttachments/payload.test.js +67 -0
  17. package/dist/annotatedAttachments/service.d.ts +49 -0
  18. package/dist/annotatedAttachments/service.js +121 -0
  19. package/dist/annotatedAttachments/types.d.ts +86 -0
  20. package/dist/cli.js +74 -25
  21. package/dist/compat/deploymentCompat.d.ts +12 -0
  22. package/dist/compat/deploymentCompat.js +43 -0
  23. package/dist/compat/workspaceSyncCompat.d.ts +6 -0
  24. package/dist/compat/workspaceSyncCompat.js +79 -0
  25. package/dist/compat/workspaceSyncCompat.test.js +87 -0
  26. package/dist/components/features/AgentsModule.d.ts +5 -0
  27. package/dist/components/features/AgentsModule.js +131 -0
  28. package/dist/components/features/AgentsModule.test.js +102 -0
  29. package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +24 -0
  30. package/dist/components/features/AnnotatedAttachmentWorkspace.js +1942 -0
  31. package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +2831 -0
  32. package/dist/components/features/DocumentGenerateModal.js +8 -16
  33. package/dist/components/features/DocumentIndex.d.ts +3 -4
  34. package/dist/components/features/DocumentIndex.js +22 -10
  35. package/dist/components/features/DocumentViewer.d.ts +6 -2
  36. package/dist/components/features/DocumentViewer.js +1000 -18
  37. package/dist/components/features/DocumentWorkspace.d.ts +40 -2
  38. package/dist/components/features/DocumentWorkspace.js +96 -16
  39. package/dist/components/features/DocumentWorkspace.test.js +1202 -0
  40. package/dist/components/features/InitiativesModule.d.ts +22 -0
  41. package/dist/components/features/InitiativesModule.js +33 -0
  42. package/dist/components/features/TaskSettings.d.ts +41 -32
  43. package/dist/components/features/TaskSettings.js +798 -313
  44. package/dist/components/features/TaskSettings.test.js +1221 -47
  45. package/dist/components/features/WorkflowsModule.d.ts +27 -0
  46. package/dist/components/features/WorkflowsModule.js +21 -0
  47. package/dist/components/features/documentWorkspaceFilters.d.ts +10 -0
  48. package/dist/components/features/documentWorkspaceFilters.js +11 -0
  49. package/dist/components/features/settings/CategoryManager.d.ts +2 -0
  50. package/dist/components/features/settings/CategoryManager.js +15 -8
  51. package/dist/components/features/settings/PriorityManager.d.ts +18 -0
  52. package/dist/components/features/settings/PriorityManager.js +51 -0
  53. package/dist/components/features/settings/TaxonomyEditor.d.ts +2 -1
  54. package/dist/components/features/settings/TaxonomyEditor.js +41 -18
  55. package/dist/components/features/settings/TaxonomyLibraryManager.d.ts +26 -0
  56. package/dist/components/features/settings/TaxonomyLibraryManager.js +301 -0
  57. package/dist/components/features/settings/TaxonomyLibraryManager.test.js +141 -0
  58. package/dist/components/features/settings/TaxonomyManager.d.ts +35 -12
  59. package/dist/components/features/settings/TaxonomyManager.js +59 -27
  60. package/dist/components/features/settings/TypeManager.d.ts +11 -0
  61. package/dist/components/features/settings/TypeManager.js +48 -11
  62. package/dist/components/features/settings/{WorkflowEditorPrototype.d.ts → WorkflowEditor.d.ts} +2 -2
  63. package/dist/components/features/settings/{WorkflowEditorPrototype.js → WorkflowEditor.js} +3 -3
  64. package/dist/components/task/TaskActionHeader.d.ts +12 -2
  65. package/dist/components/task/TaskActionHeader.js +64 -8
  66. package/dist/components/task/TaskActionHeader.test.js +73 -0
  67. package/dist/components/task/TaskCard.d.ts +14 -16
  68. package/dist/components/task/TaskCard.js +144 -174
  69. package/dist/components/task/TaskCard.test.js +416 -64
  70. package/dist/components/task/TaskContextUpload.d.ts +6 -5
  71. package/dist/components/task/TaskContextUpload.js +199 -64
  72. package/dist/components/task/TaskContextUpload.test.js +331 -14
  73. package/dist/components/task/TaskForm.d.ts +23 -29
  74. package/dist/components/task/TaskForm.js +348 -169
  75. package/dist/components/task/TaskForm.test.js +419 -23
  76. package/dist/components/task/TaskKanban.d.ts +20 -21
  77. package/dist/components/task/TaskKanban.js +176 -316
  78. package/dist/components/task/TaskKanban.planning-drop.test.js +93 -0
  79. package/dist/components/task/TaskList.d.ts +22 -11
  80. package/dist/components/task/TaskList.js +98 -12
  81. package/dist/components/task/TaskList.test.js +206 -7
  82. package/dist/components/task/TaskReferenceBadge.d.ts +12 -0
  83. package/dist/components/task/TaskReferenceBadge.js +5 -0
  84. package/dist/components/task/TaskStatusActions.d.ts +12 -0
  85. package/dist/components/task/TaskStatusActions.js +18 -0
  86. package/dist/components/task/TaskStatusActions.test.js +45 -0
  87. package/dist/components/ui/ImageReferenceBadge.d.ts +12 -0
  88. package/dist/components/ui/ImageReferenceBadge.js +5 -0
  89. package/dist/components/ui/LevelSelector.d.ts +2 -1
  90. package/dist/components/ui/LevelSelector.js +2 -2
  91. package/dist/components/ui/Markdown.d.ts +1 -2
  92. package/dist/components/ui/Markdown.js +2 -13
  93. package/dist/components/ui/Modal.d.ts +3 -2
  94. package/dist/components/ui/Modal.js +4 -3
  95. package/dist/components/ui/MultiSelectFilter.d.ts +3 -1
  96. package/dist/components/ui/MultiSelectFilter.js +2 -2
  97. package/dist/components/ui/ReferenceBadgeButton.d.ts +13 -0
  98. package/dist/components/ui/ReferenceBadgeButton.js +6 -0
  99. package/dist/components/ui/TaxonomyDropdown.d.ts +7 -1
  100. package/dist/components/ui/TaxonomyDropdown.js +58 -23
  101. package/dist/components/ui/TopNoticeLayer.d.ts +7 -0
  102. package/dist/components/ui/TopNoticeLayer.js +14 -0
  103. package/dist/components/ui/TopNoticeLayer.test.js +25 -0
  104. package/dist/components/views/AppShellHeader.d.ts +13 -0
  105. package/dist/components/views/AppShellHeader.js +19 -0
  106. package/dist/components/views/AppShellHeader.test.js +21 -0
  107. package/dist/components/views/PlanComparisonPage.d.ts +73 -0
  108. package/dist/components/views/PlanComparisonPage.js +196 -0
  109. package/dist/components/views/PlanComparisonPage.test.js +251 -0
  110. package/dist/components/views/PlansPage.d.ts +14 -0
  111. package/dist/components/views/PlansPage.js +355 -0
  112. package/dist/components/views/StandaloneLayout.d.ts +2 -2
  113. package/dist/components/views/StandaloneLayout.js +2227 -1188
  114. package/dist/components/views/WidgetView.d.ts +2 -2
  115. package/dist/components/views/WidgetView.js +149 -43
  116. package/dist/components/views/chrome/WorkspaceChromeSections.d.ts +57 -0
  117. package/dist/components/views/chrome/WorkspaceChromeSections.js +38 -0
  118. package/dist/components/views/chrome/WorkspaceChromeSections.test.js +54 -0
  119. package/dist/components/views/chrome/workspaceChrome.d.ts +31 -0
  120. package/dist/components/views/chrome/workspaceChrome.js +108 -0
  121. package/dist/components/views/chrome/workspaceChrome.test.js +52 -0
  122. package/dist/components/views/panels/DocumentFilterToolbar.d.ts +10 -0
  123. package/dist/components/views/panels/DocumentFilterToolbar.js +7 -0
  124. package/dist/components/views/panels/DocumentFilterToolbar.test.js +19 -0
  125. package/dist/components/views/panels/FilterToolbar.d.ts +68 -0
  126. package/dist/components/views/panels/FilterToolbar.js +21 -0
  127. package/dist/components/views/panels/FilterToolbar.test.d.ts +1 -0
  128. package/dist/components/views/panels/FilterToolbar.test.js +45 -0
  129. package/dist/components/views/panels/FilterToolbarControls.d.ts +47 -0
  130. package/dist/components/views/panels/FilterToolbarControls.js +61 -0
  131. package/dist/components/views/panels/PlanningDrawer.d.ts +115 -0
  132. package/dist/components/views/panels/PlanningDrawer.js +220 -0
  133. package/dist/components/views/panels/ScheduleSidebar.d.ts +52 -0
  134. package/dist/components/views/panels/ScheduleSidebar.js +127 -0
  135. package/dist/components/views/panels/scheduleUtils.d.ts +14 -0
  136. package/dist/components/views/panels/scheduleUtils.js +79 -0
  137. package/dist/components/views/teamManagementAccess.d.ts +6 -0
  138. package/dist/components/views/teamManagementAccess.js +18 -0
  139. package/dist/components/views/teamManagementAccess.test.d.ts +1 -0
  140. package/dist/components/views/teamManagementAccess.test.js +44 -0
  141. package/dist/config/clientDeployment.d.ts +2 -0
  142. package/dist/config/clientDeployment.js +13 -0
  143. package/dist/config/cloudEnvironment.d.ts +15 -0
  144. package/dist/config/cloudEnvironment.js +60 -0
  145. package/dist/config/deployment.d.ts +20 -0
  146. package/dist/config/deployment.js +67 -0
  147. package/dist/config/deployment.test.d.ts +1 -0
  148. package/dist/config/deployment.test.js +88 -0
  149. package/dist/config/envSchema.js +36 -8
  150. package/dist/config/envSchema.test.js +28 -0
  151. package/dist/core/AiProfileService.d.ts +66 -0
  152. package/dist/core/AiProfileService.js +320 -0
  153. package/dist/core/AiProfileService.test.d.ts +1 -0
  154. package/dist/core/AiProfileService.test.js +164 -0
  155. package/dist/core/AiProfiles.test.d.ts +1 -0
  156. package/dist/core/AiProfiles.test.js +449 -0
  157. package/dist/core/AttachmentLinkService.d.ts +15 -0
  158. package/dist/core/AttachmentLinkService.js +157 -0
  159. package/dist/core/AuthTokenService.d.ts +44 -0
  160. package/dist/core/AuthTokenService.js +207 -0
  161. package/dist/core/CreatedBy.test.js +134 -6
  162. package/dist/core/DeletedTaskLifecycleService.d.ts +33 -0
  163. package/dist/core/DeletedTaskLifecycleService.js +158 -0
  164. package/dist/core/DeletedTaskTrash.test.d.ts +1 -0
  165. package/dist/core/DeletedTaskTrash.test.js +104 -0
  166. package/dist/core/EntitlementsPolicy.test.js +78 -5
  167. package/dist/core/GlobalSettingsService.d.ts +16 -0
  168. package/dist/core/GlobalSettingsService.js +370 -0
  169. package/dist/core/JsonBackupRuntime.test.d.ts +1 -0
  170. package/dist/core/JsonBackupRuntime.test.js +155 -0
  171. package/dist/core/McpTokenService.d.ts +118 -0
  172. package/dist/core/McpTokenService.js +921 -0
  173. package/dist/core/PlanEntitlementService.d.ts +201 -0
  174. package/dist/core/PlanEntitlementService.js +1442 -0
  175. package/dist/core/PlanVersionPolicy.test.d.ts +1 -0
  176. package/dist/core/PlanVersionPolicy.test.js +178 -0
  177. package/dist/core/PlanningEntities.test.d.ts +1 -0
  178. package/dist/core/PlanningEntities.test.js +95 -0
  179. package/dist/core/ScheduleFields.test.js +15 -19
  180. package/dist/core/SignupMonetizationSettings.test.d.ts +1 -0
  181. package/dist/core/SignupMonetizationSettings.test.js +159 -0
  182. package/dist/core/Stats.test.d.ts +1 -0
  183. package/dist/core/Stats.test.js +36 -0
  184. package/dist/core/SyncReconciliationService.d.ts +93 -0
  185. package/dist/core/SyncReconciliationService.js +406 -0
  186. package/dist/core/SystemAdmin.test.js +578 -13
  187. package/dist/core/TaskActivityService.d.ts +21 -0
  188. package/dist/core/TaskActivityService.js +130 -0
  189. package/dist/core/TaskAttachmentsCanonical.test.d.ts +1 -0
  190. package/dist/core/TaskAttachmentsCanonical.test.js +306 -0
  191. package/dist/core/TaskAuditTimeline.test.d.ts +1 -0
  192. package/dist/core/TaskAuditTimeline.test.js +132 -0
  193. package/dist/core/TaskTaxonomyValidation.test.js +338 -15
  194. package/dist/core/Taskforce.d.ts +930 -183
  195. package/dist/core/Taskforce.ids.test.js +252 -0
  196. package/dist/core/Taskforce.js +3848 -2532
  197. package/dist/core/Taskforce.mcpAuth.test.d.ts +1 -0
  198. package/dist/core/Taskforce.mcpAuth.test.js +343 -0
  199. package/dist/core/Taskforce.realtimeMutation.test.js +57 -2
  200. package/dist/core/TaxonomySettingsService.d.ts +18 -0
  201. package/dist/core/TaxonomySettingsService.js +162 -0
  202. package/dist/core/WorkspaceDelete.test.js +32 -13
  203. package/dist/core/WorkspaceLifecycleService.d.ts +11 -0
  204. package/dist/core/WorkspaceLifecycleService.js +88 -0
  205. package/dist/core/WorkspacePermissions.test.js +95 -0
  206. package/dist/core/WorkspacePlanMode.test.js +114 -1
  207. package/dist/core/WorkspacePreferenceService.d.ts +41 -0
  208. package/dist/core/WorkspacePreferenceService.js +246 -0
  209. package/dist/core/WorkspaceSyncSettings.test.d.ts +1 -0
  210. package/dist/core/WorkspaceSyncSettings.test.js +65 -0
  211. package/dist/core/__tests__/TaskAttachmentsCanonical.test.d.ts +1 -0
  212. package/dist/core/__tests__/TaskAttachmentsCanonical.test.js +212 -0
  213. package/dist/core/shared.d.ts +36 -0
  214. package/dist/core/shared.js +128 -0
  215. package/dist/core/types.d.ts +614 -0
  216. package/dist/core/types.js +27 -0
  217. package/dist/documentReviews/DocumentReviewCommentStore.d.ts +56 -0
  218. package/dist/documentReviews/DocumentReviewCommentStore.js +230 -0
  219. package/dist/documentReviews/DocumentReviewSessionStore.d.ts +52 -0
  220. package/dist/documentReviews/DocumentReviewSessionStore.js +193 -0
  221. package/dist/documentReviews/DocumentReviewSessionStore.test.d.ts +1 -0
  222. package/dist/documentReviews/DocumentReviewSessionStore.test.js +127 -0
  223. package/dist/documentReviews/service.d.ts +68 -0
  224. package/dist/documentReviews/service.js +210 -0
  225. package/dist/documentReviews/types.d.ts +58 -0
  226. package/dist/documentReviews/types.js +1 -0
  227. package/dist/hooks/auth/useAuthGuardPolicy.d.ts +2 -0
  228. package/dist/hooks/auth/useAuthGuardPolicy.js +1 -0
  229. package/dist/hooks/tasks/useCurrentTaskRelations.d.ts +15 -0
  230. package/dist/hooks/tasks/useCurrentTaskRelations.js +37 -0
  231. package/dist/hooks/tasks/useTaskActions.d.ts +3 -0
  232. package/dist/hooks/tasks/useTaskActions.js +2 -0
  233. package/dist/hooks/tasks/useTaskEditorNavigation.d.ts +65 -0
  234. package/dist/hooks/tasks/useTaskEditorNavigation.js +235 -0
  235. package/dist/hooks/tasks/useTaskFilters.d.ts +2 -0
  236. package/dist/hooks/tasks/useTaskFilters.js +2 -0
  237. package/dist/hooks/tasks/useTaskFormActions.d.ts +58 -0
  238. package/dist/hooks/tasks/useTaskFormActions.js +365 -0
  239. package/dist/hooks/tasks/useTaskRelationships.d.ts +16 -0
  240. package/dist/hooks/tasks/useTaskRelationships.js +64 -0
  241. package/dist/hooks/ui/useResourceExport.d.ts +19 -0
  242. package/dist/hooks/ui/useResourceExport.js +84 -0
  243. package/dist/hooks/ui/useSettingsModel.d.ts +203 -0
  244. package/dist/hooks/ui/useSettingsModel.js +125 -0
  245. package/dist/hooks/ui/useSettingsPersistence.d.ts +36 -0
  246. package/dist/hooks/ui/useSettingsPersistence.js +254 -0
  247. package/dist/hooks/ui/useSettingsTaxonomyManagement.d.ts +119 -0
  248. package/dist/hooks/ui/useSettingsTaxonomyManagement.js +536 -0
  249. package/dist/hooks/ui/useUiNotice.d.ts +1 -0
  250. package/dist/hooks/ui/useUiNotice.js +1 -0
  251. package/dist/hooks/ui/useWorkflowTemplates.d.ts +37 -0
  252. package/dist/hooks/ui/useWorkflowTemplates.js +111 -0
  253. package/dist/hooks/ui/useZenMode.d.ts +5 -0
  254. package/dist/hooks/ui/useZenMode.js +32 -0
  255. package/dist/hooks/useFilterSort.d.ts +60 -0
  256. package/dist/hooks/useFilterSort.js +362 -0
  257. package/dist/hooks/useFilterSort.test.d.ts +1 -0
  258. package/dist/hooks/useFilterSort.test.js +333 -0
  259. package/dist/hooks/useRealtimeSync.d.ts +1 -0
  260. package/dist/hooks/useRealtimeSync.js +24 -9
  261. package/dist/hooks/useRealtimeSync.test.js +28 -1
  262. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.d.ts +1 -0
  263. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +34 -0
  264. package/dist/hooks/useSyncOrchestrator.d.ts +65 -103
  265. package/dist/hooks/useSyncOrchestrator.js +1538 -670
  266. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +700 -174
  267. package/dist/hooks/useTaskData.d.ts +35 -0
  268. package/dist/hooks/useTaskData.js +243 -0
  269. package/dist/hooks/useTaskData.test.d.ts +1 -0
  270. package/dist/hooks/useTaskData.test.js +180 -0
  271. package/dist/hooks/useTaskMutations.d.ts +46 -0
  272. package/dist/hooks/useTaskMutations.js +472 -0
  273. package/dist/hooks/useTaskMutations.test.d.ts +1 -0
  274. package/dist/hooks/useTaskMutations.test.js +80 -0
  275. package/dist/hooks/useTaskforce.contract.test.d.ts +1 -0
  276. package/dist/hooks/useTaskforce.contract.test.js +7 -0
  277. package/dist/hooks/useTaskforce.d.ts +260 -295
  278. package/dist/hooks/useTaskforce.js +1954 -2623
  279. package/dist/hooks/useTaskforce.priority-filters.test.js +6 -11
  280. package/dist/hooks/useTaskforce.sync-behavior.test.d.ts +1 -0
  281. package/dist/hooks/useTaskforce.sync-behavior.test.js +1120 -0
  282. package/dist/hooks/useUiNotice.d.ts +16 -0
  283. package/dist/hooks/useUiNotice.js +49 -0
  284. package/dist/hooks/useWorkspaceSyncController.d.ts +23 -46
  285. package/dist/hooks/useWorkspaceSyncController.js +280 -461
  286. package/dist/hooks/useWorkspaceSyncController.test.js +106 -199
  287. package/dist/hooks/workspace/useTaskforceApiRouting.d.ts +8 -0
  288. package/dist/hooks/workspace/useTaskforceApiRouting.js +91 -0
  289. package/dist/hooks/workspace/useTaskforceApiRouting.test.d.ts +1 -0
  290. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +60 -0
  291. package/dist/hooks/workspace/useWorkspaceSyncController.d.ts +1 -0
  292. package/dist/hooks/workspace/useWorkspaceSyncController.js +1 -0
  293. package/dist/hooks/workspace/workspaceLocalState.d.ts +30 -0
  294. package/dist/hooks/workspace/workspaceLocalState.js +101 -0
  295. package/dist/localization/locales/en-US.d.ts +1 -8
  296. package/dist/localization/locales/en-US.js +4 -11
  297. package/dist/localization/locales/es-419.js +2 -9
  298. package/dist/localization/locales/pt-BR.js +2 -9
  299. package/dist/localization/localizationCatalogs.test.d.ts +1 -0
  300. package/dist/localization/localizationCatalogs.test.js +37 -0
  301. package/dist/main.d.ts +1 -0
  302. package/dist/main.js +9 -15
  303. package/dist/mcp/clientIntegrations.d.ts +33 -0
  304. package/dist/mcp/clientIntegrations.js +199 -0
  305. package/dist/mcp/clientIntegrations.test.d.ts +1 -0
  306. package/dist/mcp/clientIntegrations.test.js +110 -0
  307. package/dist/mcp/index.d.ts +2 -0
  308. package/dist/mcp/index.js +2 -0
  309. package/dist/mcp/optionResolution.test.js +6 -2
  310. package/dist/mcp/runtime.d.ts +120 -0
  311. package/dist/mcp/runtime.js +5668 -0
  312. package/dist/mcp/runtime.test.d.ts +1 -0
  313. package/dist/mcp/runtime.test.js +3299 -0
  314. package/dist/mcp/server.js +2 -1793
  315. package/dist/mcp/structuredCommentValidation.test.d.ts +1 -0
  316. package/dist/{__tests__ → mcp}/structuredCommentValidation.test.js +1 -1
  317. package/dist/mcp/structuredDocValidation.js +1 -2
  318. package/dist/mcp/structuredDocValidation.test.d.ts +1 -0
  319. package/dist/{__tests__ → mcp}/structuredDocValidation.test.js +9 -4
  320. package/dist/mcp/toolRegistry.d.ts +570 -0
  321. package/dist/mcp/toolRegistry.js +90 -0
  322. package/dist/mcp/toolRegistry.test.d.ts +1 -0
  323. package/dist/mcp/toolRegistry.test.js +30 -0
  324. package/dist/migrations/billingSchemaParity.test.js +111 -0
  325. package/dist/migrations/taskSchemaMigrations.d.ts +5 -0
  326. package/dist/migrations/taskSchemaMigrations.js +681 -39
  327. package/dist/migrations/taskSchemaMigrations.test.d.ts +1 -0
  328. package/dist/migrations/taskSchemaMigrations.test.js +210 -0
  329. package/dist/plugins/vite.js +13 -7
  330. package/dist/public/apple-touch-icon.png +0 -0
  331. package/dist/public/archive/phoenix/apple-touch-icon.png +0 -0
  332. package/dist/public/archive/phoenix/favicon.ico +0 -0
  333. package/dist/public/archive/phoenix/site.webmanifest +1 -0
  334. package/dist/public/favicon-96x96.png +0 -0
  335. package/dist/public/favicon.ico +0 -0
  336. package/dist/public/favicon.svg +17 -0
  337. package/dist/public/site.webmanifest +21 -1
  338. package/dist/public/web-app-manifest-192x192.png +0 -0
  339. package/dist/public/web-app-manifest-512x512.png +0 -0
  340. package/dist/releaseVersion.test.d.ts +1 -0
  341. package/dist/{__tests__/releaseVersion.test.js → releaseVersion.test.js} +1 -1
  342. package/dist/resources/templates/WORKFLOW_V2_POLICY.md +1 -1
  343. package/dist/resources/templates/workflow-sources/workflowDocs.mjs +62 -52
  344. package/dist/resources/templates/workflows/collaborate.yaml +11 -5
  345. package/dist/resources/templates/workflows/do.yaml +8 -2
  346. package/dist/resources/templates/workflows/evaluate.yaml +13 -7
  347. package/dist/resources/templates/workflows/execute.yaml +40 -23
  348. package/dist/resources/templates/workflows/plan.yaml +15 -8
  349. package/dist/resources/templates/workflows/review.yaml +14 -8
  350. package/dist/runtime/appGateDefinitions.d.ts +32 -0
  351. package/dist/runtime/appGateDefinitions.js +32 -0
  352. package/dist/runtime/appGates.d.ts +29 -0
  353. package/dist/runtime/appGates.js +53 -0
  354. package/dist/runtime/appGates.test.d.ts +1 -0
  355. package/dist/runtime/appGates.test.js +42 -0
  356. package/dist/runtime/migrationFlags.d.ts +1 -0
  357. package/dist/runtime/migrationFlags.js +1 -0
  358. package/dist/server/annotatedAttachmentsRoutes.test.d.ts +1 -0
  359. package/dist/server/annotatedAttachmentsRoutes.test.js +473 -0
  360. package/dist/server/auth.js +14 -2
  361. package/dist/server/auth.test.js +21 -1
  362. package/dist/server/cors.js +11 -5
  363. package/dist/server/cors.test.js +6 -0
  364. package/dist/server/documentReviewRoutes.test.d.ts +1 -0
  365. package/dist/server/documentReviewRoutes.test.js +157 -0
  366. package/dist/server/index.cookieProxy.test.js +46 -1
  367. package/dist/server/index.d.ts +20 -0
  368. package/dist/server/index.js +168 -41
  369. package/dist/server/index.rateLimit.test.js +6 -0
  370. package/dist/server/index.test.js +133 -2
  371. package/dist/server/mockStripe.d.ts +72 -0
  372. package/dist/server/mockStripe.js +357 -0
  373. package/dist/server/realtimeSyncWs.js +4 -0
  374. package/dist/server/routes/admin.d.ts +114 -0
  375. package/dist/server/routes/admin.js +2226 -0
  376. package/dist/server/routes/annotatedAttachments.d.ts +24 -0
  377. package/dist/server/routes/annotatedAttachments.js +235 -0
  378. package/dist/server/routes/auth.d.ts +61 -0
  379. package/dist/server/routes/auth.js +1296 -0
  380. package/dist/server/{routes.billing.d.ts → routes/billing.d.ts} +3 -2
  381. package/dist/server/routes/billing.js +2001 -0
  382. package/dist/server/routes/billing.test.d.ts +1 -0
  383. package/dist/server/routes/billing.test.js +1631 -0
  384. package/dist/server/routes/documentReviews.d.ts +11 -0
  385. package/dist/server/routes/documentReviews.js +191 -0
  386. package/dist/server/routes/documents.d.ts +118 -0
  387. package/dist/server/routes/documents.js +1113 -0
  388. package/dist/server/routes/resources.d.ts +25 -0
  389. package/dist/server/routes/resources.js +225 -0
  390. package/dist/server/routes/shared.d.ts +35 -0
  391. package/dist/server/routes/shared.js +305 -0
  392. package/dist/server/routes/shared.test.d.ts +1 -0
  393. package/dist/server/routes/shared.test.js +32 -0
  394. package/dist/server/{routes.sync.d.ts → routes/sync.d.ts} +5 -5
  395. package/dist/server/routes/sync.integration.test.d.ts +1 -0
  396. package/dist/server/routes/sync.integration.test.js +2003 -0
  397. package/dist/server/routes/sync.js +3143 -0
  398. package/dist/server/routes/tasks.d.ts +72 -0
  399. package/dist/server/routes/tasks.js +1252 -0
  400. package/dist/server/routes/workspaces.d.ts +32 -0
  401. package/dist/server/routes/workspaces.js +307 -0
  402. package/dist/server/routes.d.ts +8 -9
  403. package/dist/server/routes.js +968 -4092
  404. package/dist/server/routes.test.js +9043 -2415
  405. package/dist/server/runtimeMode.d.ts +1 -1
  406. package/dist/server/runtimeMode.js +3 -18
  407. package/dist/server/runtimeMode.test.js +3 -3
  408. package/dist/server/securityHeaders.d.ts +1 -1
  409. package/dist/server/securityHeaders.js +65 -6
  410. package/dist/server/securityHeaders.test.js +40 -2
  411. package/dist/services/templateEngine.test.d.ts +1 -0
  412. package/dist/{__tests__ → services}/templateEngine.test.js +1 -1
  413. package/dist/services/templates.test.d.ts +1 -0
  414. package/dist/{__tests__ → services}/templates.test.js +1 -1
  415. package/dist/services/workflowDocumentFixtures.test.d.ts +1 -0
  416. package/dist/{__tests__ → services}/workflowDocumentFixtures.test.js +4 -4
  417. package/dist/services/workflowExportMatrix.test.d.ts +1 -0
  418. package/dist/{__tests__ → services}/workflowExportMatrix.test.js +1 -1
  419. package/dist/services/workflowExportSnapshots.test.d.ts +1 -0
  420. package/dist/services/workflowGeneration.test.d.ts +1 -0
  421. package/dist/services/workflowSimulation.test.d.ts +1 -0
  422. package/dist/services/workflowSimulation.test.js +113 -0
  423. package/dist/services/workflowV2Policy.test.d.ts +1 -0
  424. package/dist/{__tests__ → services}/workflowV2Policy.test.js +2 -2
  425. package/dist/shared/aiProfileSurfaceType.d.ts +9 -0
  426. package/dist/shared/aiProfileSurfaceType.js +49 -0
  427. package/dist/shared/customTaxonomyLibrary.d.ts +11 -0
  428. package/dist/shared/customTaxonomyLibrary.js +276 -0
  429. package/dist/shared/customTaxonomyLibrary.test.d.ts +1 -0
  430. package/dist/shared/customTaxonomyLibrary.test.js +20 -0
  431. package/dist/shared/taxonomyCatalog.d.ts +19 -0
  432. package/dist/shared/taxonomyCatalog.js +36 -0
  433. package/dist/shared/taxonomyDefaults.d.ts +29 -0
  434. package/dist/shared/taxonomyDefaults.js +26 -0
  435. package/dist/shared/taxonomyLibrary.d.ts +15 -0
  436. package/dist/shared/taxonomyLibrary.js +98 -0
  437. package/dist/shared/taxonomyLibrary.test.d.ts +1 -0
  438. package/dist/shared/taxonomyLibrary.test.js +26 -0
  439. package/dist/shared/taxonomyState.d.ts +36 -0
  440. package/dist/shared/taxonomyState.js +73 -0
  441. package/dist/shared/taxonomyState.test.d.ts +1 -0
  442. package/dist/shared/taxonomyState.test.js +71 -0
  443. package/dist/shared/workflowDefaults.d.ts +9 -0
  444. package/dist/shared/workflowDefaults.js +11 -0
  445. package/dist/storage/documentIntegrity.d.ts +2 -0
  446. package/dist/storage/documentIntegrity.js +34 -17
  447. package/dist/storage/documentPurge.d.ts +2 -1
  448. package/dist/storage/documentPurge.js +35 -8
  449. package/dist/storage/index.d.ts +4 -0
  450. package/dist/storage/index.js +2 -0
  451. package/dist/storage/objectStorage.d.ts +1 -1
  452. package/dist/storage/objectStorageClient.d.ts +6 -4
  453. package/dist/storage/objectStorageClient.js +77 -29
  454. package/dist/storage/objectStorageClient.test.d.ts +1 -0
  455. package/dist/storage/objectStorageClient.test.js +57 -0
  456. package/dist/storage/objectStorageConfig.js +3 -1
  457. package/dist/storage/objectStorageConfig.test.d.ts +1 -0
  458. package/dist/storage/objectStorageConfig.test.js +38 -0
  459. package/dist/storage/postgresAdapter.d.ts +5 -0
  460. package/dist/storage/postgresAdapter.js +2 -2
  461. package/dist/storage/postgresAdapter.test.d.ts +1 -0
  462. package/dist/storage/postgresAdapter.test.js +29 -0
  463. package/dist/storage/taskAssetStore.d.ts +7 -4
  464. package/dist/storage/taskAssetStore.js +65 -60
  465. package/dist/storage/workspaceAssetStore.d.ts +122 -0
  466. package/dist/storage/workspaceAssetStore.js +496 -0
  467. package/dist/storage/workspaceAssetStore.test.d.ts +1 -0
  468. package/dist/storage/workspaceAssetStore.test.js +501 -0
  469. package/dist/sync/cloudSyncApi.d.ts +33 -2
  470. package/dist/sync/cloudSyncApi.js +37 -15
  471. package/dist/sync/cloudSyncApi.test.js +60 -3
  472. package/dist/sync/index.d.ts +2 -0
  473. package/dist/sync/index.js +3 -0
  474. package/dist/sync/syncService.d.ts +25 -0
  475. package/dist/sync/syncService.js +184 -16
  476. package/dist/sync/syncService.test.js +289 -25
  477. package/dist/sync/workspaceRepair.d.ts +114 -0
  478. package/dist/sync/workspaceRepair.js +566 -0
  479. package/dist/sync/workspaceSyncModel.d.ts +230 -2
  480. package/dist/sync/workspaceSyncModel.js +456 -17
  481. package/dist/sync/workspaceSyncModel.test.js +642 -0
  482. package/dist/sync/workspaceSyncState.d.ts +12 -0
  483. package/dist/sync/workspaceSyncState.js +12 -0
  484. package/dist/sync/workspaceSyncSurface.d.ts +33 -0
  485. package/dist/sync/workspaceSyncSurface.js +74 -0
  486. package/dist/types.d.ts +143 -18
  487. package/dist/types.js +9 -24
  488. package/dist/ui/assets/AgentsModule-Bdq3T1Ts.js +1 -0
  489. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +1 -0
  490. package/dist/ui/assets/AnnotatedAttachmentWorkspace-jKUVo-PJ.js +3 -0
  491. package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +1 -0
  492. package/dist/ui/assets/DocumentWorkspace-DuQa-uIC.js +250 -0
  493. package/dist/ui/assets/InitiativesModule-C8Pi2VTk.js +1 -0
  494. package/dist/ui/assets/PlansPage-B6BDECSi.js +1 -0
  495. package/dist/ui/assets/PlansPage-BlVC_lRq.css +1 -0
  496. package/dist/ui/assets/TaskSettings-BwB9NtmJ.css +1 -0
  497. package/dist/ui/assets/TaskSettings-DUJgRTPU.js +8 -0
  498. package/dist/ui/assets/WorkflowsModule-b6TLuHwJ.js +5 -0
  499. package/dist/ui/assets/index-BiiF1cLQ.css +1 -0
  500. package/dist/ui/assets/index-xBWLPOEb.js +6 -0
  501. package/dist/ui/assets/vendor-dnd-DRzYolkg.js +5 -0
  502. package/dist/ui/assets/{vendor-icons-ZFG3SGwo.js → vendor-icons-QZyhEwgT.js} +1 -1
  503. package/dist/ui/assets/{vendor-markdown-CKYkQHfj.js → vendor-markdown-BUxTU7dS.js} +1 -1
  504. package/dist/ui/assets/vendor-react-CKJs5o3c.js +61 -0
  505. package/dist/ui/assets/vendor-router-BbWMxlnO.js +3 -0
  506. package/dist/ui/branding/logos/logo.png +0 -0
  507. package/dist/ui/branding/logos/logo_dark.svg +27 -0
  508. package/dist/ui/branding/logos/logo_favicon.svg +35 -0
  509. package/dist/ui/branding/logos/logo_light.svg +27 -0
  510. package/dist/ui/favicon/apple-touch-icon.png +0 -0
  511. package/dist/ui/favicon/archive/phoenix/apple-touch-icon.png +0 -0
  512. package/dist/ui/favicon/archive/phoenix/favicon.ico +0 -0
  513. package/dist/ui/favicon/archive/phoenix/site.webmanifest +1 -0
  514. package/dist/ui/favicon/favicon-96x96.png +0 -0
  515. package/dist/ui/favicon/favicon.ico +0 -0
  516. package/dist/ui/favicon/favicon.svg +17 -0
  517. package/dist/ui/favicon/site.webmanifest +21 -1
  518. package/dist/ui/favicon/web-app-manifest-192x192.png +0 -0
  519. package/dist/ui/favicon/web-app-manifest-512x512.png +0 -0
  520. package/dist/ui/index.html +10 -7
  521. package/dist/ui/product/screen_01.png +0 -0
  522. package/dist/ui/product/screen_02.png +0 -0
  523. package/dist/ui/product/screen_03.png +0 -0
  524. package/dist/utils/annotatedAttachments.d.ts +20 -0
  525. package/dist/utils/annotatedAttachments.js +74 -0
  526. package/dist/utils/annotatedAttachments.test.d.ts +1 -0
  527. package/dist/utils/annotatedAttachments.test.js +108 -0
  528. package/dist/utils/assignees.d.ts +26 -0
  529. package/dist/utils/assignees.js +132 -0
  530. package/dist/utils/canonicalAssetKind.d.ts +16 -0
  531. package/dist/utils/canonicalAssetKind.js +39 -0
  532. package/dist/utils/canonicalAssetKind.test.d.ts +1 -0
  533. package/dist/utils/canonicalAssetKind.test.js +24 -0
  534. package/dist/utils/coalescedRefresh.d.ts +5 -0
  535. package/dist/utils/coalescedRefresh.js +16 -0
  536. package/dist/utils/coalescedRefresh.test.d.ts +1 -0
  537. package/dist/utils/coalescedRefresh.test.js +30 -0
  538. package/dist/utils/constants.d.ts +16 -3
  539. package/dist/utils/constants.icons.test.js +2 -1
  540. package/dist/utils/constants.js +70 -13
  541. package/dist/utils/contextAssetEvents.d.ts +7 -0
  542. package/dist/utils/contextAssetEvents.js +6 -0
  543. package/dist/utils/contextFiles.d.ts +14 -3
  544. package/dist/utils/contextFiles.js +40 -8
  545. package/dist/utils/documentManagerApi.d.ts +6 -0
  546. package/dist/utils/documentManagerApi.js +19 -0
  547. package/dist/utils/documentNames.d.ts +24 -0
  548. package/dist/utils/documentNames.js +91 -0
  549. package/dist/utils/documentReferences.d.ts +9 -0
  550. package/dist/utils/documentReferences.js +11 -0
  551. package/dist/utils/entityReferences.d.ts +9 -0
  552. package/dist/utils/entityReferences.js +32 -0
  553. package/dist/utils/imageReferences.d.ts +9 -0
  554. package/dist/utils/imageReferences.js +11 -0
  555. package/dist/utils/planParser.d.ts +2 -2
  556. package/dist/utils/planParser.js +11 -11
  557. package/dist/utils/planParser.test.d.ts +1 -0
  558. package/dist/{__tests__ → utils}/planParser.test.js +22 -22
  559. package/dist/utils/planningReferences.d.ts +17 -0
  560. package/dist/utils/planningReferences.js +30 -0
  561. package/dist/utils/planningReferences.test.d.ts +1 -0
  562. package/dist/utils/planningReferences.test.js +17 -0
  563. package/dist/utils/taskActivity.d.ts +9 -0
  564. package/dist/utils/taskActivity.js +147 -0
  565. package/dist/utils/taskActivity.test.d.ts +1 -0
  566. package/dist/utils/taskActivity.test.js +34 -0
  567. package/dist/utils/taskDraftDefaults.d.ts +21 -0
  568. package/dist/utils/taskDraftDefaults.js +40 -0
  569. package/dist/utils/taskDraftDefaults.test.d.ts +1 -0
  570. package/dist/utils/taskDraftDefaults.test.js +34 -0
  571. package/dist/utils/taskNormalization.d.ts +25 -0
  572. package/dist/utils/taskNormalization.js +161 -0
  573. package/dist/utils/taskNormalization.test.d.ts +1 -0
  574. package/dist/utils/taskNormalization.test.js +158 -0
  575. package/dist/utils/taskReferences.d.ts +5 -0
  576. package/dist/utils/taskReferences.js +11 -0
  577. package/dist/utils/taskforceApiClient.d.ts +2 -0
  578. package/dist/utils/taskforceApiClient.js +101 -3
  579. package/dist/utils/taskforceApiClient.test.d.ts +1 -0
  580. package/dist/utils/taskforceApiClient.test.js +44 -0
  581. package/dist/utils/taxonomyFilterOptions.d.ts +2 -0
  582. package/dist/utils/taxonomyFilterOptions.js +17 -0
  583. package/dist/utils/taxonomyFormDefaults.d.ts +3 -0
  584. package/dist/utils/taxonomyFormDefaults.js +28 -0
  585. package/dist/utils/taxonomyFormDefaults.test.d.ts +1 -0
  586. package/dist/utils/taxonomyFormDefaults.test.js +63 -0
  587. package/dist/utils/taxonomyLifecycle.d.ts +3 -0
  588. package/dist/utils/taxonomyLifecycle.js +17 -0
  589. package/dist/utils/taxonomySorting.d.ts +7 -0
  590. package/dist/utils/taxonomySorting.js +74 -0
  591. package/dist/utils/theme.d.ts +15 -0
  592. package/dist/utils/theme.js +23 -0
  593. package/dist/utils/uiNotice.d.ts +7 -0
  594. package/dist/utils/uiNotice.js +6 -0
  595. package/dist/utils/uiNotice.test.d.ts +1 -0
  596. package/dist/utils/uiNotice.test.js +18 -0
  597. package/dist/utils/workspaceIdentity.d.ts +14 -0
  598. package/dist/utils/workspaceIdentity.js +21 -0
  599. package/dist/utils/workspaceIdentity.test.js +30 -1
  600. package/dist/utils/workspaceSyncPresentation.d.ts +20 -0
  601. package/dist/utils/workspaceSyncPresentation.js +21 -0
  602. package/dist/utils/workspaceSyncPresentation.test.d.ts +1 -0
  603. package/dist/utils/workspaceSyncPresentation.test.js +36 -0
  604. package/package.json +30 -23
  605. package/scripts/bootstrap-playwright-auth.mjs +155 -0
  606. package/scripts/check-compat-boundaries.mjs +41 -0
  607. package/scripts/check-env.ts +9 -2
  608. package/scripts/debug-login.mjs +32 -0
  609. package/scripts/deploy-prod.sh +41 -0
  610. package/scripts/deploy-staging.sh +23 -0
  611. package/scripts/migrate-planning-model.ts +233 -0
  612. package/scripts/migrate-task-data-assets.mjs +408 -0
  613. package/scripts/playwright-auth.sh +11 -0
  614. package/scripts/run-dev-proxy.sh +2 -0
  615. package/scripts/update-homebrew-formula.sh +2 -2
  616. package/scripts/with-esbuild.sh +11 -0
  617. package/src/resources/templates/WORKFLOW_V2_POLICY.md +1 -1
  618. package/src/resources/templates/workflow-sources/workflowDocs.mjs +62 -52
  619. package/src/resources/templates/workflows/collaborate.yaml +11 -5
  620. package/src/resources/templates/workflows/do.yaml +8 -2
  621. package/src/resources/templates/workflows/evaluate.yaml +13 -7
  622. package/src/resources/templates/workflows/execute.yaml +40 -23
  623. package/src/resources/templates/workflows/plan.yaml +15 -8
  624. package/src/resources/templates/workflows/review.yaml +14 -8
  625. package/dist/TaskforceScreenshot.d.ts +0 -11
  626. package/dist/TaskforceScreenshot.js +0 -113
  627. package/dist/compat/priorityCompat.d.ts +0 -2
  628. package/dist/compat/priorityCompat.js +0 -24
  629. package/dist/components/features/settings/TaxonomyManager 2.d.ts +0 -52
  630. package/dist/components/features/settings/TaxonomyManager 2.js +0 -75
  631. package/dist/components/task/TaskKanban 2.d.ts +0 -76
  632. package/dist/components/task/TaskKanban 2.js +0 -1004
  633. package/dist/components/task/TaskScreenshotSection.d.ts +0 -15
  634. package/dist/components/task/TaskScreenshotSection.js +0 -22
  635. package/dist/components/views/StandaloneLayout 2.d.ts +0 -8
  636. package/dist/components/views/StandaloneLayout 2.js +0 -1660
  637. package/dist/core/InitiativeTemplates.test.js +0 -50
  638. package/dist/core/ParentLifecycle.test.js +0 -98
  639. package/dist/migrations/clientMigrations.d.ts +0 -9
  640. package/dist/migrations/clientMigrations.js +0 -74
  641. package/dist/migrations/flags.d.ts +0 -6
  642. package/dist/migrations/flags.js +0 -8
  643. package/dist/migrations/taskMigrations.d.ts +0 -71
  644. package/dist/migrations/taskMigrations.js +0 -236
  645. package/dist/migrations/taskMigrations.test.js +0 -34
  646. package/dist/server/routes.billing.js +0 -991
  647. package/dist/server/routes.billing.test.js +0 -387
  648. package/dist/server/routes.sync.integration.test.js +0 -470
  649. package/dist/server/routes.sync.js +0 -1141
  650. package/dist/soak/syncSoakHarness.d.ts +0 -6
  651. package/dist/soak/syncSoakHarness.js +0 -10
  652. package/dist/ui/assets/index-0W5vyCpn.css +0 -1
  653. package/dist/ui/assets/index-MCZuNNfn.js +0 -23
  654. package/dist/ui/assets/logo_white_01-0Vx-iIIv.png +0 -0
  655. package/dist/ui/assets/vendor-dnd-BI1K77C1.js +0 -5
  656. package/dist/ui/assets/vendor-react-DdIDsh7f.js +0 -9
  657. package/dist/ui/assets/vendor-router-BtiNU7Dl.js +0 -3
  658. package/dist/utils/taskHierarchy.d.ts +0 -4
  659. package/dist/utils/taskHierarchy.js +0 -69
  660. package/dist/utils/taskHierarchy.test.js +0 -38
  661. package/dist/utils/taskParenting.d.ts +0 -3
  662. package/dist/utils/taskParenting.js +0 -24
  663. package/dist/utils/taskParenting.test.js +0 -21
  664. package/dist/utils/taskRelations.d.ts +0 -6
  665. package/dist/utils/taskRelations.js +0 -24
  666. package/dist/utils/taskRelations.test.js +0 -34
  667. package/scripts/check-compat-boundaries.js +0 -48
  668. /package/dist/{__tests__/planParser.test.d.ts → TaskforceCore.filter-persistence.test.d.ts} +0 -0
  669. /package/dist/{__tests__/releaseVersion.test.d.ts → TaskforceCore.routing.test.d.ts} +0 -0
  670. /package/dist/{__tests__/structuredCommentValidation.test.d.ts → annotatedAttachments/AnnotatedAttachmentSessionStore.test.d.ts} +0 -0
  671. /package/dist/{__tests__/structuredDocValidation.test.d.ts → annotatedAttachments/payload.test.d.ts} +0 -0
  672. /package/dist/{__tests__/templateEngine.test.d.ts → annotatedAttachments/types.js} +0 -0
  673. /package/dist/{__tests__/templates.test.d.ts → compat/workspaceSyncCompat.test.d.ts} +0 -0
  674. /package/dist/{__tests__/workflowDocumentFixtures.test.d.ts → components/features/AgentsModule.test.d.ts} +0 -0
  675. /package/dist/{__tests__/workflowExportMatrix.test.d.ts → components/features/AnnotatedAttachmentWorkspace.test.d.ts} +0 -0
  676. /package/dist/{__tests__/workflowExportSnapshots.test.d.ts → components/features/DocumentWorkspace.test.d.ts} +0 -0
  677. /package/dist/{__tests__/workflowGeneration.test.d.ts → components/features/settings/TaxonomyLibraryManager.test.d.ts} +0 -0
  678. /package/dist/{__tests__/workflowV2Policy.test.d.ts → components/task/TaskActionHeader.test.d.ts} +0 -0
  679. /package/dist/{core/InitiativeTemplates.test.d.ts → components/task/TaskKanban.planning-drop.test.d.ts} +0 -0
  680. /package/dist/{core/ParentLifecycle.test.d.ts → components/task/TaskStatusActions.test.d.ts} +0 -0
  681. /package/dist/{migrations/taskMigrations.test.d.ts → components/ui/TopNoticeLayer.test.d.ts} +0 -0
  682. /package/dist/{server/routes.billing.test.d.ts → components/views/AppShellHeader.test.d.ts} +0 -0
  683. /package/dist/{server/routes.sync.integration.test.d.ts → components/views/PlanComparisonPage.test.d.ts} +0 -0
  684. /package/dist/{utils/taskHierarchy.test.d.ts → components/views/chrome/WorkspaceChromeSections.test.d.ts} +0 -0
  685. /package/dist/{utils/taskParenting.test.d.ts → components/views/chrome/workspaceChrome.test.d.ts} +0 -0
  686. /package/dist/{utils/taskRelations.test.d.ts → components/views/panels/DocumentFilterToolbar.test.d.ts} +0 -0
  687. /package/dist/public/{android-chrome-192x192.png → archive/phoenix/android-chrome-192x192.png} +0 -0
  688. /package/dist/public/{android-chrome-512x512.png → archive/phoenix/android-chrome-512x512.png} +0 -0
  689. /package/dist/public/{favicon-16x16.png → archive/phoenix/favicon-16x16.png} +0 -0
  690. /package/dist/public/{favicon-32x32.png → archive/phoenix/favicon-32x32.png} +0 -0
  691. /package/dist/public/{favicon.png → archive/phoenix/favicon.png} +0 -0
  692. /package/dist/{__tests__ → services}/workflowExportSnapshots.test.js +0 -0
  693. /package/dist/{__tests__ → services}/workflowGeneration.test.js +0 -0
  694. /package/dist/ui/favicon/{android-chrome-192x192.png → archive/phoenix/android-chrome-192x192.png} +0 -0
  695. /package/dist/ui/favicon/{android-chrome-512x512.png → archive/phoenix/android-chrome-512x512.png} +0 -0
  696. /package/dist/ui/favicon/{favicon-16x16.png → archive/phoenix/favicon-16x16.png} +0 -0
  697. /package/dist/ui/favicon/{favicon-32x32.png → archive/phoenix/favicon-32x32.png} +0 -0
  698. /package/dist/ui/favicon/{favicon.png → archive/phoenix/favicon.png} +0 -0
@@ -0,0 +1,2226 @@
1
+ /**
2
+ * Admin and platform route module.
3
+ *
4
+ * Handles platform plan/entitlement management, system-level admin operations,
5
+ * cloud MCP endpoints (HTTP transport + OAuth 2.0 PKCE flow), and
6
+ * per-user MCP token/connector settings.
7
+ *
8
+ * Routes registered here:
9
+ * GET/POST /api/platform/plans
10
+ * GET/POST /api/platform/plans/:planId/versions
11
+ * GET /api/platform/features
12
+ * GET/PATCH /api/platform/plans/:planId/versions/:versionId/features
13
+ * GET/PATCH /api/platform/accounts/:accountId/entitlement
14
+ * GET /api/platform/accounts/:accountId/entitlement-history
15
+ * GET /api/platform/accounts/search
16
+ * GET /api/taskforce/admin/system-users
17
+ * GET /api/taskforce/admin/audit-logs
18
+ * GET /api/taskforce/admin/mcp-audit-logs
19
+ * PATCH /api/taskforce/admin/system-users/:id/role
20
+ * GET /api/taskforce/admin/collaboration-telemetry
21
+ * PATCH /api/taskforce/admin/system-users/:id/disable
22
+ * PATCH /api/taskforce/admin/system-users/:id/beta-access
23
+ * GET/POST /api/taskforce/admin/email-settings
24
+ * GET /api/taskforce/admin/email-settings/health
25
+ * POST /api/taskforce/admin/email-settings/test
26
+ * GET/POST /api/taskforce/admin/system-settings
27
+ * GET/POST /api/taskforce/admin/object-storage-settings
28
+ * GET /api/taskforce/admin/object-storage-telemetry
29
+ * POST /api/taskforce/admin/object-storage-self-test
30
+ * GET /api/taskforce/admin/quarantine
31
+ * POST /api/taskforce/admin/quarantine/delete
32
+ * GET/POST /api/taskforce/admin/storage-integrity-settings
33
+ * POST /api/taskforce/admin/storage-integrity/scan
34
+ * GET /api/taskforce/admin/storage-integrity/history
35
+ * GET /api/taskforce/admin/document-purge/history
36
+ * POST /api/taskforce/admin/storage-integrity/repair
37
+ * POST /api/taskforce/admin/workspace-repair/scan
38
+ * POST /api/taskforce/admin/workspace-repair/apply
39
+ * POST/GET/DELETE/OPTIONS /mcp (+ /mcp/)
40
+ * GET /.well-known/oauth-authorization-server
41
+ * GET /.well-known/oauth-protected-resource
42
+ * POST /oauth/mcp/register
43
+ * GET/POST /oauth/mcp/authorize
44
+ * POST /oauth/mcp/token
45
+ * GET/POST /api/taskforce/settings/mcp/tokens (+ revoke, regenerate)
46
+ * GET /api/taskforce/settings/mcp/connectors
47
+ * POST /api/taskforce/settings/mcp/connectors/:id/disconnect
48
+ * POST /api/taskforce/settings/mcp/test-connection
49
+ */
50
+ import * as fs from 'fs';
51
+ import * as path from 'path';
52
+ import { TaskforceRuleError } from '../../core/Taskforce.js';
53
+ import { parseJsonBody } from '../routes.js';
54
+ import { resolveRequestAccess } from '../auth.js';
55
+ import { resolveEffectiveEmailConfig, sendEmail } from '../email.js';
56
+ import { WorkspaceAssetStore } from '../../storage/workspaceAssetStore.js';
57
+ import { scanDocumentIntegrity } from '../../storage/documentIntegrity.js';
58
+ import { normalizeStorageIntegrityScheduleSettings, resolveStorageIntegrityScheduleSettings, } from '../../storage/integritySchedule.js';
59
+ import { resolveVirusScanConfigFromEnv } from '../../security/virusScan.js';
60
+ import { applyPrefixedRateLimitHeaders } from '../rateLimit.js';
61
+ import { createTaskforceMcpServer, isMcpWriteToolName } from '../../mcp/runtime.js';
62
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
63
+ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
64
+ import { randomUUID } from 'crypto';
65
+ import { resolveDeploymentConfig } from '../../config/deployment.js';
66
+ import { getTaskforceCloudEnvironmentConfig, resolveTaskforceCloudEnvironmentFromBaseUrl } from '../../config/cloudEnvironment.js';
67
+ import { resolveErrorStatusCode } from './shared.js';
68
+ import { applyWorkspaceRepair, scanWorkspaceRepair } from '../../sync/workspaceRepair.js';
69
+ // === SECTION: Registration ===
70
+ export function registerAdminRoutes(deps) {
71
+ const { addRoute, core, context, requestWorkspaceId, requestUserId, requestRole, requestHeader, requestRuntimeMode, ensureAppAccess, ensureAdminRole, ensureCloudPlanData, requireCloudRuntime, resolveAuthenticatedUserId, resolveAdminRequestActor, auditAdminAction, resolveEffectiveObjectStorageConfig, getObjectStorageClient, taskAssetStore, normalizeDocumentPathInput, ensureWithinDir, getAssetMetadata, markAssetDeleted, clearAssetDeleted, removeAssetMetadata, getStorageTelemetry, appendStorageTelemetry, getStorageIntegrityHistory, appendStorageIntegrityHistory, getDocumentPurgeHistory, appendDocumentPurgeHistory, getCollaborationTelemetry, isCollaborationPolicyEnabled, resolveRuntimeConfigBaseUrl, resolvePublicAuthBaseUrl, parseFormBody, escapeHtml, normalizeMcpOauthScopeString, MCP_TOKEN_SCOPE_VALUES, MCP_CLOUD_TOOL_SCOPE_REQUIREMENTS, MCP_WRITE_RATE_LIMIT_ENABLED, MCP_WRITE_RATE_LIMIT_ACTOR_MAX_REQUESTS, MCP_WRITE_RATE_LIMIT_WORKSPACE_MAX_REQUESTS, MCP_WRITE_RATE_LIMIT_WINDOW_MS, } = deps;
72
+ const baseVirusScanConfig = resolveVirusScanConfigFromEnv(process.env);
73
+ const workspaceAssetStore = typeof core.getDatabaseAdapter === 'function' && typeof core.getTenantId === 'function'
74
+ ? new WorkspaceAssetStore(core.getDatabaseAdapter(), core.getTenantId())
75
+ : null;
76
+ // === SECTION: Email helpers ===
77
+ const getEmailConfig = () => {
78
+ if (context.authConfig?.runtimeMode !== 'cloud') {
79
+ return resolveEffectiveEmailConfig(process.env, { provider: 'console' });
80
+ }
81
+ const globalSettings = core.getGlobalSettings();
82
+ return resolveEffectiveEmailConfig(process.env, globalSettings.email || null);
83
+ };
84
+ const getAuthBehavior = () => {
85
+ const globalSettings = core.getGlobalSettings();
86
+ const authSettings = globalSettings.auth || {};
87
+ const authRateLimitMaxRequestsRaw = Number(authSettings.authRateLimitMaxRequests);
88
+ const authRateLimitWindowMsRaw = Number(authSettings.authRateLimitWindowMs);
89
+ const signupSettings = core.getSignupMonetizationSettings();
90
+ return {
91
+ allowSelfRegistration: authSettings.allowSelfRegistration !== false,
92
+ requireVerifiedEmail: authSettings.requireVerifiedEmail !== false,
93
+ defaultNewUserBetaAccess: authSettings.defaultNewUserBetaAccess === true,
94
+ authRateLimitEnabled: authSettings.authRateLimitEnabled !== false,
95
+ authRateLimitMaxRequests: Number.isFinite(authRateLimitMaxRequestsRaw) && authRateLimitMaxRequestsRaw >= 1
96
+ ? Math.floor(authRateLimitMaxRequestsRaw)
97
+ : 10,
98
+ authRateLimitWindowMs: Number.isFinite(authRateLimitWindowMsRaw) && authRateLimitWindowMsRaw >= 1000
99
+ ? Math.floor(authRateLimitWindowMsRaw)
100
+ : 15 * 60_000,
101
+ signupMonetizationStrategy: signupSettings.strategy,
102
+ defaultSignupPlanVersionId: signupSettings.defaultSignupPlanVersionId
103
+ };
104
+ };
105
+ const getSiteBehavior = () => {
106
+ const globalSettings = core.getGlobalSettings();
107
+ const siteSettings = globalSettings.site || {};
108
+ return {
109
+ showRunTaskforceLocally: siteSettings.showRunTaskforceLocally !== false,
110
+ pricingPageEnabled: siteSettings.pricingPageEnabled !== false
111
+ };
112
+ };
113
+ const resolveEffectiveVirusScanSettings = () => {
114
+ const settings = core.getGlobalSettings().objectStorage || {};
115
+ const asyncEnabled = typeof settings.virusScanAsyncEnabled === 'boolean'
116
+ ? settings.virusScanAsyncEnabled
117
+ : String(process.env.TASKFORCE_VIRUS_SCAN_ASYNC || 'true').trim().toLowerCase() !== 'false';
118
+ const failClosed = typeof settings.virusScanFailClosed === 'boolean'
119
+ ? settings.virusScanFailClosed
120
+ : baseVirusScanConfig.failClosed;
121
+ return {
122
+ asyncEnabled: Boolean(taskAssetStore) && asyncEnabled,
123
+ failClosed
124
+ };
125
+ };
126
+ // === SECTION: MCP helpers ===
127
+ const writeMcpHttpError = (res, statusCode, message) => {
128
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
129
+ res.end(JSON.stringify({
130
+ jsonrpc: '2.0',
131
+ error: { code: -32000, message },
132
+ id: null,
133
+ }));
134
+ };
135
+ const resolveMcpOauthIssuer = (req) => {
136
+ const runtimeBaseUrl = resolveRuntimeConfigBaseUrl(req);
137
+ if (runtimeBaseUrl)
138
+ return runtimeBaseUrl;
139
+ return resolveDeploymentConfig(process.env).cloudMcpBaseUrl || resolvePublicAuthBaseUrl(req);
140
+ };
141
+ const resolveCloudLoginBaseUrl = (req) => {
142
+ const requestBaseUrl = resolveRuntimeConfigBaseUrl(req);
143
+ const deployment = resolveDeploymentConfig(process.env, { requestBaseUrl });
144
+ const configured = String(deployment.cloudAuthBaseUrl || deployment.cloudBaseUrl || '').replace(/\/+$/, '');
145
+ const inferredEnvironment = resolveTaskforceCloudEnvironmentFromBaseUrl(configured || requestBaseUrl);
146
+ if (inferredEnvironment) {
147
+ return getTaskforceCloudEnvironmentConfig(inferredEnvironment).appBaseUrl.replace(/\/+$/, '');
148
+ }
149
+ return String(configured || resolvePublicAuthBaseUrl(req)).replace(/\/+$/, '');
150
+ };
151
+ const resolveMcpOauthAuthorizeUrl = (req) => `${resolveMcpOauthIssuer(req)}/oauth/mcp/authorize`;
152
+ const resolveMcpOauthTokenUrl = (req) => `${resolveMcpOauthIssuer(req)}/oauth/mcp/token`;
153
+ const resolveMcpOauthRegisterUrl = (req) => `${resolveMcpOauthIssuer(req)}/oauth/mcp/register`;
154
+ const resolveMcpOauthResourceUrl = (req) => `${resolveMcpOauthIssuer(req)}/mcp`;
155
+ const renderMcpOauthAuthorizePage = (input) => {
156
+ const options = input.workspaces.map((workspace) => {
157
+ const selected = workspace.id === input.preselectedWorkspaceId ? ' selected' : '';
158
+ return `<option value="${escapeHtml(workspace.id)}"${selected}>${escapeHtml(workspace.name)} (${escapeHtml(workspace.role)})</option>`;
159
+ }).join('');
160
+ const scopeBadges = input.scopes.map((scope) => `<span style="display:inline-flex;padding:6px 10px;border-radius:999px;background:#272742;border:1px solid rgba(255,255,255,0.09);font-size:12px;">${escapeHtml(scope)}</span>`).join('');
161
+ const termsUrl = 'https://taskforcehq.com/legal/terms';
162
+ const privacyUrl = 'https://taskforcehq.com/legal/privacy';
163
+ return `<!doctype html>
164
+ <html lang="en">
165
+ <head>
166
+ <meta charset="utf-8" />
167
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
168
+ <title>Authorize Taskforce MCP Connector</title>
169
+ <style>
170
+ :root { color-scheme: dark; }
171
+ body { margin: 0; font-family: Inter, system-ui, sans-serif; background: #111126; color: #f8f7ff; }
172
+ .wrap { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 24px; }
173
+ .card { width: min(560px, 100%); background: rgba(22,22,44,0.95); border: 1px solid rgba(255,255,255,0.08); border-radius: 20px; box-shadow: 0 20px 80px rgba(0,0,0,0.35); padding: 28px; }
174
+ h1 { margin: 0 0 8px; font-size: 24px; }
175
+ p { margin: 0 0 16px; color: rgba(255,255,255,0.72); line-height: 1.45; }
176
+ label { display: block; font-size: 13px; font-weight: 600; margin: 18px 0 8px; color: rgba(255,255,255,0.9); }
177
+ select { width: 100%; border-radius: 14px; border: 1px solid rgba(255,255,255,0.1); background: #1a1a34; color: #fff; padding: 12px 14px; font-size: 14px; }
178
+ .scopes { display:flex; flex-wrap:wrap; gap:8px; margin-top:8px; }
179
+ .meta { margin-top: 18px; padding: 14px 16px; border-radius: 14px; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); }
180
+ .meta p { margin: 10px 0 0; font-size: 13px; color: rgba(255,255,255,0.68); }
181
+ .error { margin-bottom: 16px; padding: 12px 14px; border-radius: 12px; background: rgba(255,99,132,0.12); color: #ffb6c6; border: 1px solid rgba(255,99,132,0.25); }
182
+ .actions { display:flex; justify-content:flex-end; gap:12px; margin-top: 24px; }
183
+ button { border-radius: 12px; padding: 11px 16px; font-size: 14px; font-weight: 600; cursor: pointer; border: 1px solid rgba(255,255,255,0.08); }
184
+ button[name="decision"][value="deny"] { background: transparent; color: rgba(255,255,255,0.85); }
185
+ button[name="decision"][value="approve"] { background: #8d6bff; color: white; border-color: rgba(141,107,255,0.6); }
186
+ a { color: #b9d6ff; }
187
+ </style>
188
+ </head>
189
+ <body>
190
+ <div class="wrap">
191
+ <form class="card" method="post" action="/oauth/mcp/authorize">
192
+ <h1>Authorize Taskforce Connector</h1>
193
+ <p>${escapeHtml(input.clientName)} wants access to Taskforce MCP for ${escapeHtml(input.userName)}.</p>
194
+ ${input.error ? `<div class="error">${escapeHtml(input.error)}</div>` : ''}
195
+ <label for="workspaceId">Workspace</label>
196
+ <select id="workspaceId" name="workspaceId" required>${options}</select>
197
+ <label>Requested Scopes</label>
198
+ <div class="scopes">${scopeBadges}</div>
199
+ <div class="meta">
200
+ <div><strong>Client:</strong> ${escapeHtml(input.clientName)}</div>
201
+ <div><strong>Redirect URI:</strong> ${escapeHtml(input.redirectUri)}</div>
202
+ <p>By authorizing this connector, you agree to Taskforce&apos;s <a href="${termsUrl}" target="_blank" rel="noopener noreferrer">Terms of Service</a> and acknowledge the <a href="${privacyUrl}" target="_blank" rel="noopener noreferrer">Privacy Policy</a>.</p>
203
+ </div>
204
+ <input type="hidden" name="client_id" value="${escapeHtml(input.clientId)}" />
205
+ <input type="hidden" name="redirect_uri" value="${escapeHtml(input.redirectUri)}" />
206
+ <input type="hidden" name="scope" value="${escapeHtml(input.scopes.join(' '))}" />
207
+ <input type="hidden" name="state" value="${escapeHtml(input.state)}" />
208
+ <input type="hidden" name="code_challenge" value="${escapeHtml(input.codeChallenge)}" />
209
+ <input type="hidden" name="code_challenge_method" value="${escapeHtml(input.codeChallengeMethod)}" />
210
+ <div class="actions">
211
+ <button type="submit" name="decision" value="deny">Cancel</button>
212
+ <button type="submit" name="decision" value="approve">Authorize</button>
213
+ </div>
214
+ </form>
215
+ </div>
216
+ </body>
217
+ </html>`;
218
+ };
219
+ const extractMcpWriteToolNames = (body) => {
220
+ const messages = Array.isArray(body) ? body : [body];
221
+ const names = messages.flatMap((message) => {
222
+ if (!message || typeof message !== 'object')
223
+ return [];
224
+ const method = String(message.method || '').trim();
225
+ if (method !== 'tools/call')
226
+ return [];
227
+ const toolName = String(message?.params?.name || '').trim();
228
+ return toolName && isMcpWriteToolName(toolName) ? [toolName] : [];
229
+ });
230
+ return Array.from(new Set(names));
231
+ };
232
+ const extractFirstMcpToolName = (body) => {
233
+ const messages = Array.isArray(body) ? body : [body];
234
+ for (const message of messages) {
235
+ if (!message || typeof message !== 'object')
236
+ continue;
237
+ if (String(message.method || '').trim() !== 'tools/call')
238
+ continue;
239
+ const toolName = String(message?.params?.name || '').trim();
240
+ if (toolName)
241
+ return toolName;
242
+ }
243
+ return null;
244
+ };
245
+ const findDeniedCloudMcpToolName = (body, scopes) => {
246
+ const messages = Array.isArray(body) ? body : [body];
247
+ for (const message of messages) {
248
+ if (!message || typeof message !== 'object')
249
+ continue;
250
+ if (String(message.method || '').trim() !== 'tools/call')
251
+ continue;
252
+ const toolName = String(message?.params?.name || '').trim();
253
+ if (!toolName)
254
+ continue;
255
+ const requiredScope = MCP_CLOUD_TOOL_SCOPE_REQUIREMENTS[toolName];
256
+ if (!requiredScope || !scopes.includes(requiredScope))
257
+ return toolName;
258
+ }
259
+ return null;
260
+ };
261
+ const authenticateCloudMcpRequest = (req, res) => {
262
+ const authorization = requestHeader(req, 'authorization', '').trim();
263
+ const bearerToken = authorization.toLowerCase().startsWith('bearer ')
264
+ ? authorization.slice(7).trim()
265
+ : '';
266
+ const requestedWorkspaceId = requestHeader(req, 'x-taskforce-workspace-id', '').trim();
267
+ const aiProfileToken = requestHeader(req, 'x-taskforce-ai-profile-token', '').trim() || null;
268
+ const aiProfileName = requestHeader(req, 'x-taskforce-ai-name', '').trim() || null;
269
+ const aiProfileDescription = requestHeader(req, 'x-taskforce-ai-description', '').trim() || null;
270
+ const aiProfileRole = requestHeader(req, 'x-taskforce-ai-role', '').trim() || null;
271
+ const aiProfileProvider = requestHeader(req, 'x-taskforce-ai-provider', '').trim() || null;
272
+ const aiProfileModel = requestHeader(req, 'x-taskforce-ai-model', '').trim() || null;
273
+ const aiProfileSurfaceType = requestHeader(req, 'x-taskforce-ai-surface-type', '').trim() || null;
274
+ if (bearerToken) {
275
+ const tokenAuth = core.authenticateMcpAccessToken(bearerToken);
276
+ const oauthAuth = tokenAuth ? null : core.authenticateMcpOAuthAccessToken(bearerToken);
277
+ if ((!tokenAuth || tokenAuth.status !== 'active') && (!oauthAuth || oauthAuth.status !== 'active')) {
278
+ core.addMcpAuditLog({
279
+ workspaceId: requestedWorkspaceId || 'default',
280
+ actorType: 'token',
281
+ actorUserId: 'anonymous',
282
+ toolName: 'auth',
283
+ details: {
284
+ requestId: String(req.headers['x-request-id'] || req.headers['x-railway-request-id'] || '').trim() || randomUUID(),
285
+ ip: req.socket?.remoteAddress || null,
286
+ userAgent: requestHeader(req, 'user-agent', '') || null,
287
+ outcome: 'deny',
288
+ reasonCode: 'invalid_credential',
289
+ workspaceIdHint: requestedWorkspaceId || null
290
+ }
291
+ });
292
+ writeMcpHttpError(res, 401, 'Unauthorized.');
293
+ return null;
294
+ }
295
+ const resolvedWorkspaceId = tokenAuth?.workspaceId || oauthAuth?.workspaceId || '';
296
+ if (requestedWorkspaceId && requestedWorkspaceId !== resolvedWorkspaceId) {
297
+ core.addMcpAuditLog({
298
+ workspaceId: resolvedWorkspaceId || requestedWorkspaceId || 'default',
299
+ actorType: tokenAuth ? 'token' : 'oauth',
300
+ actorUserId: tokenAuth?.userId || oauthAuth?.userId || 'anonymous',
301
+ tokenId: tokenAuth?.tokenId || oauthAuth?.grantId || null,
302
+ toolName: 'auth',
303
+ details: {
304
+ requestId: String(req.headers['x-request-id'] || req.headers['x-railway-request-id'] || '').trim() || randomUUID(),
305
+ ip: req.socket?.remoteAddress || null,
306
+ userAgent: requestHeader(req, 'user-agent', '') || null,
307
+ outcome: 'deny',
308
+ reasonCode: 'workspace_mismatch',
309
+ requestedWorkspaceId,
310
+ credentialWorkspaceId: resolvedWorkspaceId
311
+ }
312
+ });
313
+ writeMcpHttpError(res, 403, 'Forbidden.');
314
+ return null;
315
+ }
316
+ return {
317
+ workspaceId: resolvedWorkspaceId,
318
+ authenticatedViaToken: tokenAuth || null,
319
+ authenticatedViaOAuth: oauthAuth || null,
320
+ userId: null,
321
+ aiProfileToken,
322
+ aiProfileName,
323
+ aiProfileDescription,
324
+ aiProfileRole,
325
+ aiProfileProvider,
326
+ aiProfileModel,
327
+ aiProfileSurfaceType,
328
+ };
329
+ }
330
+ core.addMcpAuditLog({
331
+ workspaceId: requestedWorkspaceId || 'default',
332
+ actorType: 'user',
333
+ actorUserId: resolveAuthenticatedUserId(req) || 'anonymous',
334
+ toolName: 'auth',
335
+ details: {
336
+ requestId: String(req.headers['x-request-id'] || req.headers['x-railway-request-id'] || '').trim() || randomUUID(),
337
+ ip: req.socket?.remoteAddress || null,
338
+ userAgent: requestHeader(req, 'user-agent', '') || null,
339
+ outcome: 'deny',
340
+ reasonCode: 'missing_bearer'
341
+ }
342
+ });
343
+ writeMcpHttpError(res, 401, 'Unauthorized.');
344
+ return null;
345
+ };
346
+ const recordCloudMcpCredentialUsage = (req, tokenAuth, oauthAuth) => {
347
+ if (tokenAuth) {
348
+ core.recordMcpAccessTokenUsage({
349
+ tokenId: tokenAuth.tokenId,
350
+ workspaceId: tokenAuth.workspaceId,
351
+ ip: req.socket?.remoteAddress || null,
352
+ userAgent: requestHeader(req, 'user-agent', '') || null,
353
+ });
354
+ return;
355
+ }
356
+ if (oauthAuth) {
357
+ core.recordMcpOAuthConnectorGrantUsage({
358
+ grantId: oauthAuth.grantId,
359
+ workspaceId: oauthAuth.workspaceId,
360
+ });
361
+ }
362
+ };
363
+ const applyCloudMcpWriteRateLimit = (req, res, auth, body) => {
364
+ if (!MCP_WRITE_RATE_LIMIT_ENABLED)
365
+ return true;
366
+ const writeTools = extractMcpWriteToolNames(body);
367
+ if (writeTools.length === 0)
368
+ return true;
369
+ const actorKey = auth.authenticatedViaToken?.tokenId
370
+ ? `token:${auth.authenticatedViaToken.tokenId}`
371
+ : auth.authenticatedViaOAuth?.grantId
372
+ ? `oauth:${auth.authenticatedViaOAuth.grantId}`
373
+ : auth.userId
374
+ ? `user:${auth.userId}`
375
+ : `workspace:${auth.workspaceId}`;
376
+ const actorResult = core.checkMcpRateLimit({
377
+ scope: 'actor',
378
+ key: actorKey,
379
+ limit: MCP_WRITE_RATE_LIMIT_ACTOR_MAX_REQUESTS,
380
+ windowMs: MCP_WRITE_RATE_LIMIT_WINDOW_MS
381
+ });
382
+ const workspaceResult = core.checkMcpRateLimit({
383
+ scope: 'workspace',
384
+ key: auth.workspaceId,
385
+ limit: MCP_WRITE_RATE_LIMIT_WORKSPACE_MAX_REQUESTS,
386
+ windowMs: MCP_WRITE_RATE_LIMIT_WINDOW_MS
387
+ });
388
+ applyPrefixedRateLimitHeaders(res, actorResult, 'X-MCP-Write-RateLimit');
389
+ applyPrefixedRateLimitHeaders(res, workspaceResult, 'X-MCP-Write-Workspace-RateLimit');
390
+ if (actorResult.allowed && workspaceResult.allowed)
391
+ return true;
392
+ const retryAfterSeconds = Math.max(actorResult.retryAfterSeconds, workspaceResult.retryAfterSeconds);
393
+ res.setHeader('Retry-After', String(retryAfterSeconds));
394
+ core.addMcpAuditLog({
395
+ workspaceId: auth.workspaceId,
396
+ actorType: auth.authenticatedViaToken ? 'token' : auth.authenticatedViaOAuth ? 'oauth' : 'user',
397
+ actorUserId: auth.authenticatedViaToken?.userId || auth.authenticatedViaOAuth?.userId || auth.userId || 'anonymous',
398
+ tokenId: auth.authenticatedViaToken?.tokenId || auth.authenticatedViaOAuth?.grantId || null,
399
+ toolName: extractFirstMcpToolName(body) || 'rate_limit',
400
+ details: {
401
+ requestId: String(req.headers['x-request-id'] || req.headers['x-railway-request-id'] || '').trim() || randomUUID(),
402
+ ip: req.socket?.remoteAddress || null,
403
+ userAgent: requestHeader(req, 'user-agent', '') || null,
404
+ outcome: 'deny',
405
+ reasonCode: actorResult.allowed ? 'workspace_write_rate_limited' : 'actor_write_rate_limited',
406
+ actorLimit: actorResult.limit,
407
+ workspaceLimit: workspaceResult.limit
408
+ }
409
+ });
410
+ writeMcpHttpError(res, 429, 'Write rate limit exceeded. Retry later.');
411
+ return false;
412
+ };
413
+ const handleCloudMcpRequest = async (req, res) => {
414
+ if (requestRuntimeMode(req) !== 'cloud') {
415
+ writeMcpHttpError(res, 400, 'The /mcp HTTP endpoint is only available in cloud runtime. In local runtime, use the local stdio MCP server (for example: `taskforce mcp <project-path>`).');
416
+ return;
417
+ }
418
+ if (!ensureAppAccess(req, res))
419
+ return;
420
+ if (req.method !== 'POST' && req.method !== 'GET' && req.method !== 'DELETE') {
421
+ writeMcpHttpError(res, 405, 'Method not allowed.');
422
+ return;
423
+ }
424
+ const auth = authenticateCloudMcpRequest(req, res);
425
+ if (!auth)
426
+ return;
427
+ if (req.method !== 'POST') {
428
+ writeMcpHttpError(res, 405, 'Method not allowed.');
429
+ return;
430
+ }
431
+ let body = undefined;
432
+ try {
433
+ body = await parseJsonBody(req);
434
+ }
435
+ catch (error) {
436
+ writeMcpHttpError(res, resolveErrorStatusCode(error, 400), 'Invalid JSON body.');
437
+ return;
438
+ }
439
+ const authScopes = auth.authenticatedViaToken?.scopes || auth.authenticatedViaOAuth?.scopes || [];
440
+ const deniedToolName = findDeniedCloudMcpToolName(body, authScopes);
441
+ if (deniedToolName) {
442
+ core.addMcpAuditLog({
443
+ workspaceId: auth.workspaceId,
444
+ actorType: auth.authenticatedViaToken ? 'token' : auth.authenticatedViaOAuth ? 'oauth' : 'user',
445
+ actorUserId: auth.authenticatedViaToken?.userId || auth.authenticatedViaOAuth?.userId || auth.userId || 'anonymous',
446
+ tokenId: auth.authenticatedViaToken?.tokenId || auth.authenticatedViaOAuth?.grantId || null,
447
+ toolName: deniedToolName,
448
+ details: {
449
+ requestId: String(req.headers['x-request-id'] || req.headers['x-railway-request-id'] || '').trim() || randomUUID(),
450
+ ip: req.socket?.remoteAddress || null,
451
+ userAgent: requestHeader(req, 'user-agent', '') || null,
452
+ outcome: 'deny',
453
+ reasonCode: 'scope_denied',
454
+ scopes: authScopes
455
+ }
456
+ });
457
+ writeMcpHttpError(res, 403, 'Forbidden.');
458
+ return;
459
+ }
460
+ if (!applyCloudMcpWriteRateLimit(req, res, auth, body))
461
+ return;
462
+ try {
463
+ const runtime = createTaskforceMcpServer({
464
+ projectRoot: core.getPaths().projectRoot,
465
+ tenantId: core.getTenantId(),
466
+ workspaceId: auth.workspaceId,
467
+ serverName: 'taskforce-cloud',
468
+ logStartup: false,
469
+ runtimeMode: 'cloud',
470
+ auth: {
471
+ scopes: authScopes,
472
+ userId: auth.authenticatedViaToken?.userId || auth.authenticatedViaOAuth?.userId || auth.userId || null,
473
+ tokenId: auth.authenticatedViaToken?.tokenId || auth.authenticatedViaOAuth?.grantId || null,
474
+ aiProfileToken: auth.aiProfileToken,
475
+ aiProfileName: auth.aiProfileName,
476
+ aiProfileDescription: auth.aiProfileDescription,
477
+ aiProfileRole: auth.aiProfileRole,
478
+ aiProfileProvider: auth.aiProfileProvider,
479
+ aiProfileModel: auth.aiProfileModel,
480
+ aiProfileSurfaceType: auth.aiProfileSurfaceType,
481
+ actorType: auth.authenticatedViaToken ? 'token' : auth.authenticatedViaOAuth ? 'oauth' : 'user',
482
+ requestId: String(req.headers['x-request-id'] || req.headers['x-railway-request-id'] || '').trim() || randomUUID(),
483
+ ip: req.socket?.remoteAddress || null,
484
+ userAgent: requestHeader(req, 'user-agent', '') || null,
485
+ },
486
+ });
487
+ const transport = new StreamableHTTPServerTransport({
488
+ sessionIdGenerator: undefined,
489
+ enableJsonResponse: true,
490
+ });
491
+ await runtime.server.connect(transport);
492
+ if (!isInitializeRequest(body)) {
493
+ // Stateless cloud sessions still require a valid MCP JSON-RPC request;
494
+ // let the SDK validate and respond rather than trying to track sessions server-side.
495
+ }
496
+ await transport.handleRequest(req, res, body);
497
+ recordCloudMcpCredentialUsage(req, auth.authenticatedViaToken, auth.authenticatedViaOAuth);
498
+ }
499
+ catch (error) {
500
+ if (!res.headersSent) {
501
+ writeMcpHttpError(res, 500, 'Internal server error.');
502
+ }
503
+ }
504
+ };
505
+ // === SECTION: Platform plan management ===
506
+ addRoute('GET', '/api/platform/plans', async (req, res) => {
507
+ if (!ensureCloudPlanData(req, res))
508
+ return;
509
+ if (!ensureAdminRole(req)) {
510
+ res.writeHead(403, { 'Content-Type': 'application/json' });
511
+ res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
512
+ return;
513
+ }
514
+ const plans = core.listPlanCatalog();
515
+ res.writeHead(200, { 'Content-Type': 'application/json' });
516
+ res.end(JSON.stringify({ success: true, plans }));
517
+ });
518
+ addRoute('POST', '/api/platform/plans', async (req, res) => {
519
+ if (!ensureCloudPlanData(req, res))
520
+ return;
521
+ if (!ensureAdminRole(req)) {
522
+ res.writeHead(403, { 'Content-Type': 'application/json' });
523
+ res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
524
+ return;
525
+ }
526
+ const body = await parseJsonBody(req);
527
+ try {
528
+ const plan = core.upsertPlanCatalog({
529
+ planId: body?.planId,
530
+ displayName: body?.displayName,
531
+ status: body?.status,
532
+ initialMode: body?.initialMode,
533
+ initialTrialDays: body?.initialTrialDays === undefined ? undefined : Number(body?.initialTrialDays),
534
+ initialSeatLimit: body?.initialSeatLimit === null
535
+ ? null
536
+ : (body?.initialSeatLimit === undefined ? undefined : Number(body?.initialSeatLimit))
537
+ });
538
+ auditAdminAction(req, 'platform-plan-upsert', { planId: plan.planId, status: plan.status });
539
+ res.writeHead(200, { 'Content-Type': 'application/json' });
540
+ res.end(JSON.stringify({ success: true, plan }));
541
+ }
542
+ catch (error) {
543
+ const statusCode = resolveErrorStatusCode(error, 400);
544
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
545
+ res.end(JSON.stringify({ success: false, error: String(error?.message || 'Unable to upsert plan'), code: error?.code }));
546
+ }
547
+ });
548
+ addRoute('GET', '/api/platform/plans/:planId/versions', async (req, res, params) => {
549
+ if (!ensureCloudPlanData(req, res))
550
+ return;
551
+ if (!ensureAdminRole(req)) {
552
+ res.writeHead(403, { 'Content-Type': 'application/json' });
553
+ res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
554
+ return;
555
+ }
556
+ try {
557
+ const versions = core.listPlanVersions(params.planId);
558
+ res.writeHead(200, { 'Content-Type': 'application/json' });
559
+ res.end(JSON.stringify({ success: true, versions }));
560
+ }
561
+ catch (error) {
562
+ const statusCode = resolveErrorStatusCode(error, 400);
563
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
564
+ res.end(JSON.stringify({ success: false, error: String(error?.message || 'Unable to list plan versions'), code: error?.code }));
565
+ }
566
+ });
567
+ addRoute('GET', '/api/platform/features', async (req, res) => {
568
+ if (!ensureCloudPlanData(req, res))
569
+ return;
570
+ if (!ensureAdminRole(req)) {
571
+ res.writeHead(403, { 'Content-Type': 'application/json' });
572
+ res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
573
+ return;
574
+ }
575
+ try {
576
+ const features = core.listPlanFeatureCatalog();
577
+ res.writeHead(200, { 'Content-Type': 'application/json' });
578
+ res.end(JSON.stringify({ success: true, features }));
579
+ }
580
+ catch (error) {
581
+ const statusCode = resolveErrorStatusCode(error, 400);
582
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
583
+ res.end(JSON.stringify({ success: false, error: String(error?.message || 'Unable to list feature catalog'), code: error?.code }));
584
+ }
585
+ });
586
+ addRoute('POST', '/api/platform/plans/:planId/versions', async (req, res, params) => {
587
+ if (!ensureCloudPlanData(req, res))
588
+ return;
589
+ if (!ensureAdminRole(req)) {
590
+ res.writeHead(403, { 'Content-Type': 'application/json' });
591
+ res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
592
+ return;
593
+ }
594
+ const body = await parseJsonBody(req);
595
+ try {
596
+ const versionNumber = Math.max(1, Math.floor(Number(body?.versionNumber || 1)));
597
+ const isDefault = body?.isDefault === true;
598
+ const planVersionId = `${params.planId}-v${versionNumber}`;
599
+ if (isDefault) {
600
+ try {
601
+ const db = core.getDatabaseAdapter();
602
+ const tenantId = core.getTenantId();
603
+ const mappingCountRow = db.prepare(`
604
+ SELECT COUNT(*) AS count
605
+ FROM billing_price_mappings
606
+ WHERE tenant_id = ?
607
+ AND plan_version_id = ?
608
+ AND active = 1
609
+ AND billing_interval IN ('month', 'year')
610
+ `).get(tenantId, planVersionId);
611
+ const mappingCount = Number(mappingCountRow?.count || 0);
612
+ if (mappingCount < 1) {
613
+ res.writeHead(409, { 'Content-Type': 'application/json' });
614
+ res.end(JSON.stringify({
615
+ success: false,
616
+ error: 'Default version requires at least one active price mapping (month or year).',
617
+ code: 'PLAN_VERSION_PRICING_REQUIRED'
618
+ }));
619
+ return;
620
+ }
621
+ }
622
+ catch {
623
+ // If billing mapping storage is unavailable in this runtime, skip default guardrail check.
624
+ }
625
+ }
626
+ let version;
627
+ try {
628
+ version = core.updatePlanVersion({
629
+ planId: params.planId,
630
+ versionNumber,
631
+ isDefault,
632
+ seatLimit: body?.seatLimit === null ? null : (body?.seatLimit === undefined ? undefined : Number(body?.seatLimit)),
633
+ trialDays: body?.trialDays === undefined ? undefined : Number(body?.trialDays),
634
+ metadata: body?.metadata && typeof body.metadata === 'object' ? body.metadata : {}
635
+ });
636
+ }
637
+ catch (error) {
638
+ if (error?.code === 'PLAN_VERSION_NOT_FOUND') {
639
+ version = core.createPlanVersion({
640
+ planId: params.planId,
641
+ versionNumber,
642
+ isDefault,
643
+ seatLimit: body?.seatLimit === null ? null : (body?.seatLimit === undefined ? undefined : Number(body?.seatLimit)),
644
+ trialDays: body?.trialDays === undefined ? undefined : Number(body?.trialDays),
645
+ metadata: body?.metadata && typeof body.metadata === 'object' ? body.metadata : {}
646
+ });
647
+ }
648
+ else {
649
+ throw error;
650
+ }
651
+ }
652
+ auditAdminAction(req, 'platform-plan-version-upsert', { planId: params.planId, planVersionId: version.planVersionId });
653
+ res.writeHead(200, { 'Content-Type': 'application/json' });
654
+ res.end(JSON.stringify({ success: true, version }));
655
+ }
656
+ catch (error) {
657
+ const statusCode = resolveErrorStatusCode(error, 400);
658
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
659
+ res.end(JSON.stringify({ success: false, error: String(error?.message || 'Unable to upsert plan version'), code: error?.code }));
660
+ }
661
+ });
662
+ addRoute('GET', '/api/platform/plans/:planId/versions/:versionId/features', async (req, res, params) => {
663
+ if (!ensureCloudPlanData(req, res))
664
+ return;
665
+ if (!ensureAdminRole(req)) {
666
+ res.writeHead(403, { 'Content-Type': 'application/json' });
667
+ res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
668
+ return;
669
+ }
670
+ try {
671
+ const version = core.getPlanVersionFeatures(params.planId, params.versionId);
672
+ res.writeHead(200, { 'Content-Type': 'application/json' });
673
+ res.end(JSON.stringify({ success: true, version }));
674
+ }
675
+ catch (error) {
676
+ const statusCode = resolveErrorStatusCode(error, 400);
677
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
678
+ res.end(JSON.stringify({ success: false, error: String(error?.message || 'Unable to read plan features'), code: error?.code }));
679
+ }
680
+ });
681
+ addRoute('PATCH', '/api/platform/plans/:planId/versions/:versionId/features', async (req, res, params) => {
682
+ if (!ensureCloudPlanData(req, res))
683
+ return;
684
+ if (!ensureAdminRole(req)) {
685
+ res.writeHead(403, { 'Content-Type': 'application/json' });
686
+ res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
687
+ return;
688
+ }
689
+ const body = await parseJsonBody(req);
690
+ const features = Array.isArray(body?.features) ? body.features : [];
691
+ try {
692
+ const version = core.updatePlanVersionFeatures({
693
+ planVersionId: params.versionId,
694
+ features: features.map((feature) => ({
695
+ featureKey: String(feature?.featureKey || ''),
696
+ access: String(feature?.access || 'disabled'),
697
+ config: feature?.config && typeof feature.config === 'object' ? feature.config : {}
698
+ }))
699
+ });
700
+ auditAdminAction(req, 'platform-plan-features-updated', {
701
+ planId: params.planId,
702
+ planVersionId: params.versionId,
703
+ featureCount: features.length
704
+ });
705
+ res.writeHead(200, { 'Content-Type': 'application/json' });
706
+ res.end(JSON.stringify({ success: true, version }));
707
+ }
708
+ catch (error) {
709
+ const statusCode = resolveErrorStatusCode(error, 400);
710
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
711
+ res.end(JSON.stringify({ success: false, error: String(error?.message || 'Unable to update plan features'), code: error?.code }));
712
+ }
713
+ });
714
+ addRoute('GET', '/api/platform/accounts/:accountId/entitlement', async (req, res, params) => {
715
+ if (!ensureAdminRole(req)) {
716
+ res.writeHead(403, { 'Content-Type': 'application/json' });
717
+ res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
718
+ return;
719
+ }
720
+ try {
721
+ const entitlement = core.getAccountEntitlement(params.accountId);
722
+ res.writeHead(200, { 'Content-Type': 'application/json' });
723
+ res.end(JSON.stringify({ success: true, entitlement }));
724
+ }
725
+ catch (error) {
726
+ const statusCode = resolveErrorStatusCode(error, 400);
727
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
728
+ res.end(JSON.stringify({ success: false, error: String(error?.message || 'Unable to read entitlement'), code: error?.code }));
729
+ }
730
+ });
731
+ addRoute('PATCH', '/api/platform/accounts/:accountId/entitlement', async (req, res, params) => {
732
+ if (!ensureAdminRole(req)) {
733
+ res.writeHead(403, { 'Content-Type': 'application/json' });
734
+ res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
735
+ return;
736
+ }
737
+ const body = await parseJsonBody(req);
738
+ const actor = resolveAdminRequestActor(req);
739
+ try {
740
+ const entitlement = core.updateAccountEntitlement({
741
+ accountId: params.accountId,
742
+ planVersionId: body?.planVersionId,
743
+ entitlementState: body?.state,
744
+ effectiveAt: body?.effectiveAt ?? undefined,
745
+ effectiveUntil: body?.effectiveUntil ?? undefined,
746
+ actorUserId: actor.userId,
747
+ actorRole: actor.systemRole,
748
+ reason: typeof body?.reason === 'string' ? body.reason : undefined,
749
+ requestId: String(req.headers['x-request-id'] || '').trim() || undefined
750
+ });
751
+ auditAdminAction(req, 'platform-account-entitlement-updated', {
752
+ accountId: params.accountId,
753
+ planVersionId: entitlement.planVersionId,
754
+ state: entitlement.state
755
+ });
756
+ res.writeHead(200, { 'Content-Type': 'application/json' });
757
+ res.end(JSON.stringify({ success: true, entitlement }));
758
+ }
759
+ catch (error) {
760
+ const statusCode = resolveErrorStatusCode(error, 400);
761
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
762
+ res.end(JSON.stringify({ success: false, error: String(error?.message || 'Unable to update entitlement'), code: error?.code }));
763
+ }
764
+ });
765
+ addRoute('GET', '/api/platform/accounts/:accountId/entitlement-history', async (req, res, params) => {
766
+ if (!ensureAdminRole(req)) {
767
+ res.writeHead(403, { 'Content-Type': 'application/json' });
768
+ res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
769
+ return;
770
+ }
771
+ const reqUrl = new URL(req.url || '/api/platform/accounts/:accountId/entitlement-history', 'http://localhost');
772
+ const limitRaw = Number(reqUrl.searchParams.get('limit') || '100');
773
+ const offsetRaw = Number(reqUrl.searchParams.get('offset') || '0');
774
+ const limit = Number.isFinite(limitRaw) ? Math.max(1, Math.min(500, Math.floor(limitRaw))) : 100;
775
+ const offset = Number.isFinite(offsetRaw) ? Math.max(0, Math.floor(offsetRaw)) : 0;
776
+ const events = core.listEntitlementAuditLog(params.accountId, limit, offset);
777
+ res.writeHead(200, { 'Content-Type': 'application/json' });
778
+ res.end(JSON.stringify({ success: true, events, limit, offset }));
779
+ });
780
+ addRoute('GET', '/api/platform/accounts/search', async (req, res) => {
781
+ if (!ensureAdminRole(req)) {
782
+ res.writeHead(403, { 'Content-Type': 'application/json' });
783
+ res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
784
+ return;
785
+ }
786
+ const reqUrl = new URL(req.url || '/api/platform/accounts/search', 'http://localhost');
787
+ const query = String(reqUrl.searchParams.get('q') || '').trim();
788
+ const limitRaw = Number(reqUrl.searchParams.get('limit') || '20');
789
+ const limit = Number.isFinite(limitRaw) ? Math.max(1, Math.min(100, Math.floor(limitRaw))) : 20;
790
+ const accounts = core.searchAccountsByEmail(query, limit);
791
+ res.writeHead(200, { 'Content-Type': 'application/json' });
792
+ res.end(JSON.stringify({ success: true, accounts }));
793
+ });
794
+ // === SECTION: System admin user management ===
795
+ // GET /api/taskforce/admin/system-users - List users with global system roles
796
+ addRoute('GET', '/api/taskforce/admin/system-users', async (req, res) => {
797
+ if (!ensureAdminRole(req)) {
798
+ res.writeHead(403, { 'Content-Type': 'application/json' });
799
+ res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
800
+ return;
801
+ }
802
+ const users = core.listSystemUsers();
803
+ res.writeHead(200, { 'Content-Type': 'application/json' });
804
+ res.end(JSON.stringify({ users }));
805
+ });
806
+ // GET /api/taskforce/admin/audit-logs - Read recent admin audit events
807
+ addRoute('GET', '/api/taskforce/admin/audit-logs', async (req, res) => {
808
+ if (!ensureAdminRole(req)) {
809
+ res.writeHead(403, { 'Content-Type': 'application/json' });
810
+ res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
811
+ return;
812
+ }
813
+ const workspaceId = 'system';
814
+ const reqUrl = new URL(req.url || '/api/taskforce/admin/audit-logs', 'http://localhost');
815
+ const limitRaw = Number(reqUrl.searchParams.get('limit') || '100');
816
+ const offsetRaw = Number(reqUrl.searchParams.get('offset') || '0');
817
+ const from = String(reqUrl.searchParams.get('from') || '').trim();
818
+ const to = String(reqUrl.searchParams.get('to') || '').trim();
819
+ const limit = Number.isFinite(limitRaw) ? Math.max(1, Math.min(500, Math.floor(limitRaw))) : 100;
820
+ const offset = Number.isFinite(offsetRaw) ? Math.max(0, Math.floor(offsetRaw)) : 0;
821
+ const filters = { from, to };
822
+ const events = core.listAdminAuditLogs(workspaceId, limit, offset, filters).map((event) => ({
823
+ id: event.id,
824
+ action: event.action,
825
+ actorUserId: event.actorUserId,
826
+ actorRole: event.actorRole,
827
+ workspaceId: event.workspaceId,
828
+ details: event.details,
829
+ createdAt: event.ts
830
+ }));
831
+ const total = core.countAdminAuditLogs(workspaceId, filters);
832
+ const page = Math.floor(offset / limit) + 1;
833
+ const pages = Math.max(1, Math.ceil(total / limit));
834
+ res.writeHead(200, { 'Content-Type': 'application/json' });
835
+ res.end(JSON.stringify({ events, limit, offset, total, page, pages, from: from || null, to: to || null }));
836
+ });
837
+ // GET /api/taskforce/admin/mcp-audit-logs - Read recent MCP audit events
838
+ addRoute('GET', '/api/taskforce/admin/mcp-audit-logs', async (req, res) => {
839
+ if (!ensureAdminRole(req)) {
840
+ res.writeHead(403, { 'Content-Type': 'application/json' });
841
+ res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
842
+ return;
843
+ }
844
+ const reqUrl = new URL(req.url || '/api/taskforce/admin/mcp-audit-logs', 'http://localhost');
845
+ const limitRaw = Number(reqUrl.searchParams.get('limit') || '100');
846
+ const offsetRaw = Number(reqUrl.searchParams.get('offset') || '0');
847
+ const from = String(reqUrl.searchParams.get('from') || '').trim();
848
+ const to = String(reqUrl.searchParams.get('to') || '').trim();
849
+ const limit = Number.isFinite(limitRaw) ? Math.max(1, Math.min(500, Math.floor(limitRaw))) : 100;
850
+ const offset = Number.isFinite(offsetRaw) ? Math.max(0, Math.floor(offsetRaw)) : 0;
851
+ const filters = { from, to };
852
+ const events = core.listMcpAuditLogs(limit, offset, filters).map((event) => ({
853
+ id: event.id,
854
+ workspaceId: event.workspaceId,
855
+ actorType: event.actorType,
856
+ actorUserId: event.actorUserId,
857
+ tokenId: event.tokenId,
858
+ toolName: event.toolName,
859
+ requestId: event.requestId,
860
+ ip: event.ip,
861
+ userAgent: event.userAgent,
862
+ outcome: event.outcome,
863
+ reasonCode: event.reasonCode,
864
+ details: event.details,
865
+ createdAt: event.ts
866
+ }));
867
+ const total = core.countMcpAuditLogs(filters);
868
+ const page = Math.floor(offset / limit) + 1;
869
+ const pages = Math.max(1, Math.ceil(total / limit));
870
+ res.writeHead(200, { 'Content-Type': 'application/json' });
871
+ res.end(JSON.stringify({ events, limit, offset, total, page, pages, from: from || null, to: to || null }));
872
+ });
873
+ // PATCH /api/taskforce/admin/system-users/:id/role - Update global system role
874
+ addRoute('PATCH', '/api/taskforce/admin/system-users/:id/role', async (req, res, params) => {
875
+ if (!ensureAdminRole(req)) {
876
+ res.writeHead(403, { 'Content-Type': 'application/json' });
877
+ res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
878
+ return;
879
+ }
880
+ const body = await parseJsonBody(req);
881
+ const role = String(body?.systemRole || '').trim();
882
+ if (role !== 'system_admin' && role !== 'user') {
883
+ res.writeHead(400, { 'Content-Type': 'application/json' });
884
+ res.end(JSON.stringify({ error: 'systemRole must be system_admin or user' }));
885
+ return;
886
+ }
887
+ try {
888
+ const success = core.setSystemRole(params.id, role);
889
+ if (!success) {
890
+ res.writeHead(404, { 'Content-Type': 'application/json' });
891
+ res.end(JSON.stringify({ error: 'User not found' }));
892
+ return;
893
+ }
894
+ auditAdminAction(req, 'update-system-role', { targetUserId: params.id, systemRole: role });
895
+ res.writeHead(200, { 'Content-Type': 'application/json' });
896
+ res.end(JSON.stringify({ success: true }));
897
+ }
898
+ catch (error) {
899
+ const statusCode = resolveErrorStatusCode(error, 400);
900
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
901
+ res.end(JSON.stringify({ error: String(error?.message || 'Unable to update system role'), code: error?.code }));
902
+ }
903
+ });
904
+ // GET /api/taskforce/admin/collaboration-telemetry - Read collaboration rollout telemetry counters
905
+ addRoute('GET', '/api/taskforce/admin/collaboration-telemetry', async (req, res) => {
906
+ if (!ensureAdminRole(req)) {
907
+ res.writeHead(403, { 'Content-Type': 'application/json' });
908
+ res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
909
+ return;
910
+ }
911
+ const telemetry = getCollaborationTelemetry();
912
+ res.writeHead(200, { 'Content-Type': 'application/json' });
913
+ res.end(JSON.stringify({
914
+ success: true,
915
+ policyEnabled: isCollaborationPolicyEnabled(),
916
+ telemetry
917
+ }));
918
+ });
919
+ // PATCH /api/taskforce/admin/system-users/:id/disable - Enable/disable a user globally
920
+ addRoute('PATCH', '/api/taskforce/admin/system-users/:id/disable', async (req, res, params) => {
921
+ if (!ensureAdminRole(req)) {
922
+ res.writeHead(403, { 'Content-Type': 'application/json' });
923
+ res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
924
+ return;
925
+ }
926
+ const body = await parseJsonBody(req);
927
+ const disabled = Boolean(body?.disabled);
928
+ try {
929
+ const success = core.setUserDisabledGlobal(params.id, disabled);
930
+ if (!success) {
931
+ res.writeHead(404, { 'Content-Type': 'application/json' });
932
+ res.end(JSON.stringify({ error: 'User not found' }));
933
+ return;
934
+ }
935
+ auditAdminAction(req, 'set-user-disabled-global', { targetUserId: params.id, disabled });
936
+ res.writeHead(200, { 'Content-Type': 'application/json' });
937
+ res.end(JSON.stringify({ success: true }));
938
+ }
939
+ catch (error) {
940
+ const statusCode = resolveErrorStatusCode(error, 400);
941
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
942
+ res.end(JSON.stringify({ error: String(error?.message || 'Unable to update user disabled state'), code: error?.code }));
943
+ }
944
+ });
945
+ // PATCH /api/taskforce/admin/system-users/:id/beta-access - Enable/disable global beta access
946
+ addRoute('PATCH', '/api/taskforce/admin/system-users/:id/beta-access', async (req, res, params) => {
947
+ if (!ensureAdminRole(req)) {
948
+ res.writeHead(403, { 'Content-Type': 'application/json' });
949
+ res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
950
+ return;
951
+ }
952
+ const body = await parseJsonBody(req);
953
+ const enabled = body?.enabled !== false;
954
+ const success = core.setUserBetaAccessGlobal(params.id, enabled);
955
+ if (!success) {
956
+ res.writeHead(404, { 'Content-Type': 'application/json' });
957
+ res.end(JSON.stringify({ error: 'User not found' }));
958
+ return;
959
+ }
960
+ auditAdminAction(req, 'set-user-beta-access-global', { targetUserId: params.id, enabled });
961
+ res.writeHead(200, { 'Content-Type': 'application/json' });
962
+ res.end(JSON.stringify({ success: true }));
963
+ });
964
+ // === SECTION: Admin email settings ===
965
+ // GET /api/taskforce/admin/email-settings - Read email provider settings
966
+ addRoute('GET', '/api/taskforce/admin/email-settings', async (req, res) => {
967
+ if (!ensureAdminRole(req)) {
968
+ res.writeHead(403, { 'Content-Type': 'application/json' });
969
+ res.end(JSON.stringify({ error: 'Forbidden: admin role required.' }));
970
+ return;
971
+ }
972
+ if (context.authConfig?.runtimeMode !== 'cloud') {
973
+ const effective = getEmailConfig();
974
+ res.writeHead(200, { 'Content-Type': 'application/json' });
975
+ res.end(JSON.stringify({
976
+ settings: { provider: 'console', from: effective.from, replyTo: '' },
977
+ status: { effectiveProvider: 'console', hasProviderApiKey: false, hasStoredProviderApiKey: false }
978
+ }));
979
+ return;
980
+ }
981
+ const settings = core.getGlobalSettings().email || {};
982
+ const effective = getEmailConfig();
983
+ res.writeHead(200, { 'Content-Type': 'application/json' });
984
+ res.end(JSON.stringify({
985
+ settings: {
986
+ provider: settings.provider || effective.provider,
987
+ from: settings.from || effective.from,
988
+ replyTo: settings.replyTo || effective.replyTo || ''
989
+ },
990
+ status: {
991
+ effectiveProvider: effective.provider,
992
+ hasProviderApiKey: effective.provider === 'resend' || effective.provider === 'google-workspace'
993
+ ? Boolean(effective.providerApiKey)
994
+ : true,
995
+ hasStoredProviderApiKey: typeof settings.providerApiKey === 'string' && settings.providerApiKey.trim().length > 0
996
+ }
997
+ }));
998
+ });
999
+ // GET /api/taskforce/admin/email-settings/health - Delivery readiness snapshot
1000
+ addRoute('GET', '/api/taskforce/admin/email-settings/health', async (req, res) => {
1001
+ if (!ensureAdminRole(req)) {
1002
+ res.writeHead(403, { 'Content-Type': 'application/json' });
1003
+ res.end(JSON.stringify({ error: 'Forbidden: admin role required.' }));
1004
+ return;
1005
+ }
1006
+ const settings = core.getGlobalSettings().email || {};
1007
+ const effective = getEmailConfig();
1008
+ const keyRequired = effective.provider === 'resend' || effective.provider === 'google-workspace';
1009
+ const ready = keyRequired ? Boolean(effective.providerApiKey) : true;
1010
+ const reason = ready
1011
+ ? null
1012
+ : effective.provider === 'google-workspace'
1013
+ ? 'Missing Google app password.'
1014
+ : 'Missing provider API key.';
1015
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1016
+ res.end(JSON.stringify({
1017
+ success: true,
1018
+ health: {
1019
+ provider: effective.provider,
1020
+ from: effective.from,
1021
+ replyTo: effective.replyTo || '',
1022
+ ready,
1023
+ reason,
1024
+ hasStoredProviderApiKey: typeof settings.providerApiKey === 'string' && settings.providerApiKey.trim().length > 0,
1025
+ hasEffectiveProviderApiKey: Boolean(effective.providerApiKey)
1026
+ }
1027
+ }));
1028
+ });
1029
+ // GET /api/taskforce/admin/system-settings - Read auth/system behavior settings
1030
+ addRoute('GET', '/api/taskforce/admin/system-settings', async (req, res) => {
1031
+ if (!ensureAdminRole(req)) {
1032
+ res.writeHead(403, { 'Content-Type': 'application/json' });
1033
+ res.end(JSON.stringify({ error: 'Forbidden: admin role required.' }));
1034
+ return;
1035
+ }
1036
+ const authSettings = core.getGlobalSettings().auth || {};
1037
+ const effective = getAuthBehavior();
1038
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1039
+ res.end(JSON.stringify({
1040
+ settings: {
1041
+ ...(typeof authSettings.allowSelfRegistration === 'boolean' ? { allowSelfRegistration: authSettings.allowSelfRegistration } : {}),
1042
+ ...(typeof authSettings.requireVerifiedEmail === 'boolean' ? { requireVerifiedEmail: authSettings.requireVerifiedEmail } : {}),
1043
+ ...(typeof authSettings.defaultNewUserBetaAccess === 'boolean' ? { defaultNewUserBetaAccess: authSettings.defaultNewUserBetaAccess } : {}),
1044
+ ...(typeof authSettings.authRateLimitEnabled === 'boolean' ? { authRateLimitEnabled: authSettings.authRateLimitEnabled } : {}),
1045
+ ...(typeof authSettings.authRateLimitMaxRequests === 'number' ? { authRateLimitMaxRequests: authSettings.authRateLimitMaxRequests } : {}),
1046
+ ...(typeof authSettings.authRateLimitWindowMs === 'number' ? { authRateLimitWindowMs: authSettings.authRateLimitWindowMs } : {}),
1047
+ ...((authSettings.signupMonetizationStrategy === 'auto_assign_default' || authSettings.signupMonetizationStrategy === 'plan_selection_required')
1048
+ ? { signupMonetizationStrategy: authSettings.signupMonetizationStrategy }
1049
+ : {}),
1050
+ ...(typeof authSettings.defaultSignupPlanVersionId === 'string' || authSettings.defaultSignupPlanVersionId === null
1051
+ ? { defaultSignupPlanVersionId: authSettings.defaultSignupPlanVersionId ?? null }
1052
+ : {})
1053
+ },
1054
+ status: {
1055
+ allowSelfRegistration: effective.allowSelfRegistration,
1056
+ requireVerifiedEmail: effective.requireVerifiedEmail,
1057
+ defaultNewUserBetaAccess: effective.defaultNewUserBetaAccess,
1058
+ authRateLimitEnabled: effective.authRateLimitEnabled,
1059
+ authRateLimitMaxRequests: effective.authRateLimitMaxRequests,
1060
+ authRateLimitWindowMs: effective.authRateLimitWindowMs,
1061
+ signupMonetizationStrategy: effective.signupMonetizationStrategy,
1062
+ defaultSignupPlanVersionId: effective.defaultSignupPlanVersionId
1063
+ }
1064
+ }));
1065
+ });
1066
+ const handleGetSiteSettings = async (req, res) => {
1067
+ if (!ensureAdminRole(req)) {
1068
+ res.writeHead(403, { 'Content-Type': 'application/json' });
1069
+ res.end(JSON.stringify({ error: 'Forbidden: admin role required.' }));
1070
+ return;
1071
+ }
1072
+ const siteSettings = core.getGlobalSettings().site || {};
1073
+ const siteBehavior = getSiteBehavior();
1074
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1075
+ res.end(JSON.stringify({
1076
+ settings: {
1077
+ ...(typeof siteSettings.showRunTaskforceLocally === 'boolean'
1078
+ ? { showRunTaskforceLocally: siteSettings.showRunTaskforceLocally }
1079
+ : {}),
1080
+ ...(typeof siteSettings.pricingPageEnabled === 'boolean'
1081
+ ? { pricingPageEnabled: siteSettings.pricingPageEnabled }
1082
+ : {})
1083
+ },
1084
+ status: {
1085
+ showRunTaskforceLocally: siteBehavior.showRunTaskforceLocally,
1086
+ pricingPageEnabled: siteBehavior.pricingPageEnabled
1087
+ }
1088
+ }));
1089
+ };
1090
+ addRoute('GET', '/api/taskforce/admin/site-settings', handleGetSiteSettings);
1091
+ addRoute('GET', '/api/taskforce/admin/site_settings', handleGetSiteSettings);
1092
+ // GET /api/taskforce/admin/object-storage-settings - Read object storage admin overrides
1093
+ addRoute('GET', '/api/taskforce/admin/object-storage-settings', async (req, res) => {
1094
+ if (!ensureAdminRole(req)) {
1095
+ res.writeHead(403, { 'Content-Type': 'application/json' });
1096
+ res.end(JSON.stringify({ error: 'Forbidden: admin role required.' }));
1097
+ return;
1098
+ }
1099
+ const settings = core.getGlobalSettings().objectStorage || {};
1100
+ const effective = resolveEffectiveObjectStorageConfig();
1101
+ const defaults = {
1102
+ keyPrefix: 'taskforce',
1103
+ signedUploadTtlSeconds: 600,
1104
+ signedDownloadTtlSeconds: 300,
1105
+ virusScanAsyncEnabled: true,
1106
+ virusScanFailClosed: baseVirusScanConfig.failClosed
1107
+ };
1108
+ const effectiveR2 = effective.provider === 'r2' ? effective.r2 : undefined;
1109
+ const effectiveScanSettings = resolveEffectiveVirusScanSettings();
1110
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1111
+ res.end(JSON.stringify({
1112
+ settings: {
1113
+ ...(typeof settings.keyPrefix === 'string' ? { keyPrefix: settings.keyPrefix } : {}),
1114
+ ...(typeof settings.signedUploadTtlSeconds === 'number' ? { signedUploadTtlSeconds: settings.signedUploadTtlSeconds } : {}),
1115
+ ...(typeof settings.signedDownloadTtlSeconds === 'number' ? { signedDownloadTtlSeconds: settings.signedDownloadTtlSeconds } : {}),
1116
+ ...(typeof settings.virusScanAsyncEnabled === 'boolean' ? { virusScanAsyncEnabled: settings.virusScanAsyncEnabled } : {}),
1117
+ ...(typeof settings.virusScanFailClosed === 'boolean' ? { virusScanFailClosed: settings.virusScanFailClosed } : {})
1118
+ },
1119
+ status: {
1120
+ provider: effective.provider,
1121
+ keyPrefix: typeof effectiveR2?.keyPrefix === 'string' ? effectiveR2.keyPrefix : defaults.keyPrefix,
1122
+ signedUploadTtlSeconds: typeof effectiveR2?.signedUploadTtlSeconds === 'number' ? effectiveR2.signedUploadTtlSeconds : defaults.signedUploadTtlSeconds,
1123
+ signedDownloadTtlSeconds: typeof effectiveR2?.signedDownloadTtlSeconds === 'number' ? effectiveR2.signedDownloadTtlSeconds : defaults.signedDownloadTtlSeconds,
1124
+ virusScanAsyncEnabled: effectiveScanSettings.asyncEnabled,
1125
+ virusScanFailClosed: effectiveScanSettings.failClosed
1126
+ }
1127
+ }));
1128
+ });
1129
+ // GET /api/taskforce/admin/object-storage-telemetry - Read upload/scan/purge telemetry
1130
+ addRoute('GET', '/api/taskforce/admin/object-storage-telemetry', async (req, res) => {
1131
+ if (!ensureAdminRole(req)) {
1132
+ res.writeHead(403, { 'Content-Type': 'application/json' });
1133
+ res.end(JSON.stringify({ error: 'Forbidden: admin role required.' }));
1134
+ return;
1135
+ }
1136
+ const telemetry = getStorageTelemetry();
1137
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1138
+ res.end(JSON.stringify({ success: true, telemetry }));
1139
+ });
1140
+ // POST /api/taskforce/admin/object-storage-self-test - Verify effective object storage config with live read/write/delete cycle
1141
+ addRoute('POST', '/api/taskforce/admin/object-storage-self-test', async (req, res) => {
1142
+ if (!ensureAdminRole(req)) {
1143
+ res.writeHead(403, { 'Content-Type': 'application/json' });
1144
+ res.end(JSON.stringify({ error: 'Forbidden: admin role required.' }));
1145
+ return;
1146
+ }
1147
+ const effective = resolveEffectiveObjectStorageConfig();
1148
+ const objectStorageClient = getObjectStorageClient();
1149
+ if (effective.provider !== 'r2' || !effective.r2 || !objectStorageClient) {
1150
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1151
+ res.end(JSON.stringify({
1152
+ success: false,
1153
+ error: 'Object storage self-test requires provider=r2.',
1154
+ status: {
1155
+ provider: effective.provider
1156
+ }
1157
+ }));
1158
+ return;
1159
+ }
1160
+ const workspaceId = requestWorkspaceId(req);
1161
+ const relativePath = `system/self-test/${workspaceId}/object-storage-self-test-${randomUUID()}.txt`;
1162
+ const body = Buffer.from(`Taskforce object storage self-test ${new Date().toISOString()}\n`, 'utf8');
1163
+ const steps = [];
1164
+ const status = {
1165
+ provider: effective.provider,
1166
+ bucket: effective.r2.bucket,
1167
+ endpoint: effective.r2.endpoint,
1168
+ accountId: effective.r2.accountId,
1169
+ keyPrefix: effective.r2.keyPrefix
1170
+ };
1171
+ const fail = (step, error) => {
1172
+ steps.push({
1173
+ step,
1174
+ ok: false,
1175
+ detail: String(error?.message || error || 'Unknown error')
1176
+ });
1177
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1178
+ res.end(JSON.stringify({
1179
+ success: false,
1180
+ failedStep: step,
1181
+ error: String(error?.message || error || 'Unknown error'),
1182
+ relativePath,
1183
+ status,
1184
+ steps
1185
+ }));
1186
+ };
1187
+ try {
1188
+ await objectStorageClient.putObject(relativePath, body, 'text/plain; charset=utf-8');
1189
+ steps.push({ step: 'put', ok: true });
1190
+ }
1191
+ catch (error) {
1192
+ fail('put', error);
1193
+ return;
1194
+ }
1195
+ try {
1196
+ const exists = await objectStorageClient.objectExists(relativePath);
1197
+ if (!exists)
1198
+ throw new Error('Object was not found after upload.');
1199
+ steps.push({ step: 'head', ok: true });
1200
+ }
1201
+ catch (error) {
1202
+ fail('head', error);
1203
+ return;
1204
+ }
1205
+ try {
1206
+ const object = await objectStorageClient.getObject(relativePath);
1207
+ if (!object)
1208
+ throw new Error('Object could not be read after upload.');
1209
+ if (!object.body.equals(body))
1210
+ throw new Error('Downloaded object body does not match uploaded payload.');
1211
+ steps.push({ step: 'get', ok: true });
1212
+ }
1213
+ catch (error) {
1214
+ fail('get', error);
1215
+ return;
1216
+ }
1217
+ try {
1218
+ await objectStorageClient.deleteObject(relativePath);
1219
+ steps.push({ step: 'delete', ok: true });
1220
+ }
1221
+ catch (error) {
1222
+ fail('delete', error);
1223
+ return;
1224
+ }
1225
+ try {
1226
+ const exists = await objectStorageClient.objectExists(relativePath);
1227
+ if (exists)
1228
+ throw new Error('Object still exists after delete.');
1229
+ steps.push({ step: 'verify-delete', ok: true });
1230
+ }
1231
+ catch (error) {
1232
+ fail('verify-delete', error);
1233
+ return;
1234
+ }
1235
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1236
+ res.end(JSON.stringify({
1237
+ success: true,
1238
+ relativePath,
1239
+ status,
1240
+ steps
1241
+ }));
1242
+ });
1243
+ // GET /api/taskforce/admin/orphaned-assets - List canonical assets without active task links
1244
+ addRoute('GET', '/api/taskforce/admin/orphaned-assets', async (req, res) => {
1245
+ if (!ensureAdminRole(req)) {
1246
+ res.writeHead(403, { 'Content-Type': 'application/json' });
1247
+ res.end(JSON.stringify({ error: 'Forbidden: admin role required.' }));
1248
+ return;
1249
+ }
1250
+ if (!workspaceAssetStore) {
1251
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1252
+ res.end(JSON.stringify({ success: true, assets: [] }));
1253
+ return;
1254
+ }
1255
+ const reqUrl = new URL(req.url || '/api/taskforce/admin/orphaned-assets', 'http://localhost');
1256
+ const workspaceId = String(reqUrl.searchParams.get('workspaceId') || requestWorkspaceId(req) || '').trim();
1257
+ const assets = workspaceAssetStore.listAllByWorkspace(workspaceId, {
1258
+ includeDeleted: false
1259
+ }).filter((asset) => workspaceAssetStore.listLinksForAsset(asset.assetId, workspaceId).every((link) => !!link.deletedAt))
1260
+ .map((asset) => ({
1261
+ assetId: asset.assetId,
1262
+ workspaceId: asset.workspaceId,
1263
+ kind: asset.kind,
1264
+ originalFilename: asset.originalFilename,
1265
+ storageKey: asset.storageKey,
1266
+ storageProvider: asset.storageProvider,
1267
+ updatedAt: asset.updatedAt,
1268
+ sizeBytes: asset.sizeBytes,
1269
+ documentId: asset.documentId
1270
+ }));
1271
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1272
+ res.end(JSON.stringify({ success: true, assets }));
1273
+ });
1274
+ // POST /api/taskforce/admin/orphaned-assets/attach - Attach an orphaned canonical asset to a task
1275
+ addRoute('POST', '/api/taskforce/admin/orphaned-assets/attach', async (req, res) => {
1276
+ if (!ensureAdminRole(req)) {
1277
+ res.writeHead(403, { 'Content-Type': 'application/json' });
1278
+ res.end(JSON.stringify({ error: 'Forbidden: admin role required.' }));
1279
+ return;
1280
+ }
1281
+ if (!workspaceAssetStore) {
1282
+ res.writeHead(500, { 'Content-Type': 'application/json' });
1283
+ res.end(JSON.stringify({ error: 'Workspace asset store unavailable.' }));
1284
+ return;
1285
+ }
1286
+ const body = await parseJsonBody(req);
1287
+ const assetId = String(body?.assetId || '').trim();
1288
+ const taskId = String(body?.taskId || '').trim();
1289
+ const workspaceId = String(body?.workspaceId || requestWorkspaceId(req) || '').trim();
1290
+ const role = body?.role === 'reference' ? 'reference' : body?.role === 'image' ? 'image' : 'attachment';
1291
+ if (!assetId || !taskId || !workspaceId) {
1292
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1293
+ res.end(JSON.stringify({ error: 'assetId, taskId, and workspaceId are required.' }));
1294
+ return;
1295
+ }
1296
+ const asset = workspaceAssetStore.get(assetId, workspaceId);
1297
+ if (!asset || asset.deletedAt) {
1298
+ res.writeHead(404, { 'Content-Type': 'application/json' });
1299
+ res.end(JSON.stringify({ error: 'Asset not found.' }));
1300
+ return;
1301
+ }
1302
+ workspaceAssetStore.upsertLink({
1303
+ workspaceId,
1304
+ assetId,
1305
+ taskId,
1306
+ role,
1307
+ deletedAt: null
1308
+ });
1309
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1310
+ res.end(JSON.stringify({ success: true, assetId, taskId, workspaceId, role }));
1311
+ });
1312
+ // POST /api/taskforce/admin/orphaned-assets/delete - Hard-delete an orphaned asset from storage and metadata
1313
+ addRoute('POST', '/api/taskforce/admin/orphaned-assets/delete', async (req, res) => {
1314
+ if (!ensureAdminRole(req)) {
1315
+ res.writeHead(403, { 'Content-Type': 'application/json' });
1316
+ res.end(JSON.stringify({ error: 'Forbidden: admin role required.' }));
1317
+ return;
1318
+ }
1319
+ if (!workspaceAssetStore) {
1320
+ res.writeHead(500, { 'Content-Type': 'application/json' });
1321
+ res.end(JSON.stringify({ error: 'Workspace asset store unavailable.' }));
1322
+ return;
1323
+ }
1324
+ const body = await parseJsonBody(req);
1325
+ const assetId = String(body?.assetId || '').trim();
1326
+ const workspaceId = String(body?.workspaceId || requestWorkspaceId(req) || '').trim();
1327
+ if (!assetId || !workspaceId) {
1328
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1329
+ res.end(JSON.stringify({ error: 'assetId and workspaceId are required.' }));
1330
+ return;
1331
+ }
1332
+ const asset = workspaceAssetStore.get(assetId, workspaceId);
1333
+ if (!asset || asset.deletedAt) {
1334
+ res.writeHead(404, { 'Content-Type': 'application/json' });
1335
+ res.end(JSON.stringify({ error: 'Asset not found.' }));
1336
+ return;
1337
+ }
1338
+ const objectStorageClient = getObjectStorageClient();
1339
+ const { basePath } = core.getPaths();
1340
+ try {
1341
+ if (asset.storageProvider === 'r2' && objectStorageClient) {
1342
+ await objectStorageClient.deleteObject(asset.storageKey);
1343
+ }
1344
+ else {
1345
+ const fullPath = ensureWithinDir(path.join(basePath, asset.storageKey), basePath);
1346
+ if (fullPath && fs.existsSync(fullPath))
1347
+ fs.unlinkSync(fullPath);
1348
+ }
1349
+ }
1350
+ catch (error) {
1351
+ res.writeHead(500, { 'Content-Type': 'application/json' });
1352
+ res.end(JSON.stringify({ error: String(error?.message || error || 'Failed to delete asset.') }));
1353
+ return;
1354
+ }
1355
+ const deletedAt = new Date().toISOString();
1356
+ workspaceAssetStore.markDeleted(assetId, workspaceId, deletedAt);
1357
+ if (asset.kind === 'image') {
1358
+ core.deleteAnnotatedAttachmentSessionsForImage({
1359
+ workspaceId,
1360
+ imageAssetId: assetId,
1361
+ deletedAt,
1362
+ });
1363
+ }
1364
+ const links = workspaceAssetStore.listLinksForAsset(assetId, workspaceId, { includeDeleted: true });
1365
+ for (const link of links) {
1366
+ workspaceAssetStore.deleteLink({
1367
+ workspaceId,
1368
+ assetId,
1369
+ taskId: link.taskId,
1370
+ role: link.role
1371
+ });
1372
+ }
1373
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1374
+ res.end(JSON.stringify({ success: true, assetId, workspaceId }));
1375
+ });
1376
+ // GET /api/taskforce/admin/quarantine - List quarantined assets
1377
+ addRoute('GET', '/api/taskforce/admin/quarantine', async (req, res) => {
1378
+ if (!ensureAdminRole(req)) {
1379
+ res.writeHead(403, { 'Content-Type': 'application/json' });
1380
+ res.end(JSON.stringify({ error: 'Forbidden: admin role required.' }));
1381
+ return;
1382
+ }
1383
+ if (!taskAssetStore && !workspaceAssetStore) {
1384
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1385
+ res.end(JSON.stringify({ error: 'Quarantine is unavailable without DB-backed asset metadata.' }));
1386
+ return;
1387
+ }
1388
+ const reqUrl = new URL(req.url || '/api/taskforce/admin/quarantine', 'http://localhost');
1389
+ const limitRaw = Number(reqUrl.searchParams.get('limit') || '200');
1390
+ const limit = Number.isFinite(limitRaw) ? Math.max(1, Math.min(1000, Math.floor(limitRaw))) : 200;
1391
+ const items = workspaceAssetStore
1392
+ ? workspaceAssetStore.listQuarantined(limit).map((asset) => ({
1393
+ workspaceId: asset.workspaceId,
1394
+ path: asset.storageKey,
1395
+ provider: asset.storageProvider,
1396
+ contentType: asset.mimeType,
1397
+ quarantineStatus: 'quarantined',
1398
+ scan: {
1399
+ engine: asset.scanEngine || 'none',
1400
+ verdict: asset.scanStatus === 'infected' ? 'infected' : asset.scanStatus,
1401
+ details: asset.scanDetails || null,
1402
+ scannedAt: asset.updatedAt
1403
+ }
1404
+ }))
1405
+ : taskAssetStore.listQuarantined(limit);
1406
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1407
+ res.end(JSON.stringify({ success: true, items }));
1408
+ });
1409
+ // POST /api/taskforce/admin/quarantine/delete - Hard-delete quarantined asset from storage + metadata
1410
+ addRoute('POST', '/api/taskforce/admin/quarantine/delete', async (req, res) => {
1411
+ if (!ensureAdminRole(req)) {
1412
+ res.writeHead(403, { 'Content-Type': 'application/json' });
1413
+ res.end(JSON.stringify({ error: 'Forbidden: admin role required.' }));
1414
+ return;
1415
+ }
1416
+ const body = await parseJsonBody(req);
1417
+ const normalizedPath = normalizeDocumentPathInput(body?.path);
1418
+ if (!normalizedPath) {
1419
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1420
+ res.end(JSON.stringify({ error: 'path is required.' }));
1421
+ return;
1422
+ }
1423
+ const metadata = getAssetMetadata(normalizedPath);
1424
+ if (!metadata) {
1425
+ res.writeHead(404, { 'Content-Type': 'application/json' });
1426
+ res.end(JSON.stringify({ error: 'Metadata not found.' }));
1427
+ return;
1428
+ }
1429
+ if (metadata.quarantineStatus !== 'quarantined') {
1430
+ res.writeHead(409, { 'Content-Type': 'application/json' });
1431
+ res.end(JSON.stringify({ error: 'Asset is not quarantined.' }));
1432
+ return;
1433
+ }
1434
+ const objectStorageClient = getObjectStorageClient();
1435
+ const { basePath } = core.getPaths();
1436
+ if (objectStorageClient && metadata.provider === 'r2') {
1437
+ await objectStorageClient.deleteObject(normalizedPath);
1438
+ }
1439
+ else {
1440
+ const filePath = ensureWithinDir(path.join(basePath, normalizedPath), basePath);
1441
+ if (filePath && fs.existsSync(filePath)) {
1442
+ fs.unlinkSync(filePath);
1443
+ }
1444
+ }
1445
+ removeAssetMetadata(normalizedPath);
1446
+ appendStorageTelemetry('quarantine.deleted', {
1447
+ path: normalizedPath,
1448
+ status: 'deleted',
1449
+ provider: metadata.provider
1450
+ });
1451
+ auditAdminAction(req, 'quarantine-delete', { path: normalizedPath, provider: metadata.provider });
1452
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1453
+ res.end(JSON.stringify({ success: true, path: normalizedPath }));
1454
+ });
1455
+ // GET /api/taskforce/admin/storage-integrity-settings - Read integrity schedule settings
1456
+ addRoute('GET', '/api/taskforce/admin/storage-integrity-settings', async (req, res) => {
1457
+ if (!ensureAdminRole(req)) {
1458
+ res.writeHead(403, { 'Content-Type': 'application/json' });
1459
+ res.end(JSON.stringify({ error: 'Forbidden: admin role required.' }));
1460
+ return;
1461
+ }
1462
+ const settings = resolveStorageIntegrityScheduleSettings(core.getGlobalSettings());
1463
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1464
+ res.end(JSON.stringify({ success: true, settings }));
1465
+ });
1466
+ // POST /api/taskforce/admin/storage-integrity/scan - Scan registry vs local/R2 object integrity
1467
+ addRoute('POST', '/api/taskforce/admin/storage-integrity/scan', async (req, res) => {
1468
+ if (!ensureAdminRole(req)) {
1469
+ res.writeHead(403, { 'Content-Type': 'application/json' });
1470
+ res.end(JSON.stringify({ error: 'Forbidden: admin role required.' }));
1471
+ return;
1472
+ }
1473
+ const body = await parseJsonBody(req);
1474
+ const verifyHash = body?.verifyHash === true;
1475
+ const includeDeleted = body?.includeDeleted === true;
1476
+ const requireMirror = body?.requireMirror === true;
1477
+ const limitRaw = Number(body?.limit);
1478
+ const limit = Number.isFinite(limitRaw) ? Math.max(1, Math.floor(limitRaw)) : 500;
1479
+ const { basePath } = core.getPaths();
1480
+ const result = await scanDocumentIntegrity({
1481
+ basePath,
1482
+ objectStorage: resolveEffectiveObjectStorageConfig(),
1483
+ verifyHash,
1484
+ includeDeleted,
1485
+ requireMirror,
1486
+ limit,
1487
+ assetStore: taskAssetStore || undefined,
1488
+ workspaceAssetStore: workspaceAssetStore || undefined
1489
+ });
1490
+ appendStorageIntegrityHistory(req, {
1491
+ options: { verifyHash, includeDeleted, requireMirror, limit },
1492
+ summary: {
1493
+ totalChecked: result.summary.totalChecked,
1494
+ totalIssues: result.summary.totalIssues,
1495
+ byCode: result.summary.byCode,
1496
+ skippedDeleted: result.summary.skippedDeleted
1497
+ }
1498
+ });
1499
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1500
+ res.end(JSON.stringify({ success: true, ...result }));
1501
+ });
1502
+ // POST /api/taskforce/admin/storage-integrity-settings - Save integrity schedule settings
1503
+ addRoute('POST', '/api/taskforce/admin/storage-integrity-settings', async (req, res) => {
1504
+ if (!ensureAdminRole(req)) {
1505
+ res.writeHead(403, { 'Content-Type': 'application/json' });
1506
+ res.end(JSON.stringify({ error: 'Forbidden: admin role required.' }));
1507
+ return;
1508
+ }
1509
+ const body = await parseJsonBody(req);
1510
+ const normalized = normalizeStorageIntegrityScheduleSettings(body || {});
1511
+ core.updateGlobalSettings({ storageIntegritySchedule: normalized });
1512
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1513
+ res.end(JSON.stringify({ success: true }));
1514
+ });
1515
+ // GET /api/taskforce/admin/storage-integrity/history - List recent integrity scan history
1516
+ addRoute('GET', '/api/taskforce/admin/storage-integrity/history', async (req, res) => {
1517
+ if (!ensureAdminRole(req)) {
1518
+ res.writeHead(403, { 'Content-Type': 'application/json' });
1519
+ res.end(JSON.stringify({ error: 'Forbidden: admin role required.' }));
1520
+ return;
1521
+ }
1522
+ const history = getStorageIntegrityHistory();
1523
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1524
+ res.end(JSON.stringify({ success: true, history }));
1525
+ });
1526
+ // GET /api/taskforce/admin/document-purge/history - List recent purge runs
1527
+ addRoute('GET', '/api/taskforce/admin/document-purge/history', async (req, res) => {
1528
+ if (!ensureAdminRole(req)) {
1529
+ res.writeHead(403, { 'Content-Type': 'application/json' });
1530
+ res.end(JSON.stringify({ error: 'Forbidden: admin role required.' }));
1531
+ return;
1532
+ }
1533
+ const history = getDocumentPurgeHistory();
1534
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1535
+ res.end(JSON.stringify({ success: true, history }));
1536
+ });
1537
+ // POST /api/taskforce/admin/storage-integrity/repair - Repair registry state for integrity findings
1538
+ addRoute('POST', '/api/taskforce/admin/storage-integrity/repair', async (req, res) => {
1539
+ if (!ensureAdminRole(req)) {
1540
+ res.writeHead(403, { 'Content-Type': 'application/json' });
1541
+ res.end(JSON.stringify({ error: 'Forbidden: admin role required.' }));
1542
+ return;
1543
+ }
1544
+ const body = await parseJsonBody(req);
1545
+ const action = String(body?.action || '').trim();
1546
+ const normalizedPath = normalizeDocumentPathInput(body?.path);
1547
+ if (!normalizedPath) {
1548
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1549
+ res.end(JSON.stringify({ error: 'path is required.' }));
1550
+ return;
1551
+ }
1552
+ if (action === 'mark-deleted') {
1553
+ const metadata = markAssetDeleted(normalizedPath);
1554
+ if (!metadata && workspaceAssetStore) {
1555
+ const workspaceId = String(body?.workspaceId || requestWorkspaceId(req) || '').trim() || 'default';
1556
+ const canonicalAsset = workspaceAssetStore.getByStorageKey(normalizedPath, workspaceId);
1557
+ if (canonicalAsset) {
1558
+ const deletedAt = new Date().toISOString();
1559
+ const updated = workspaceAssetStore.markDeleted(canonicalAsset.assetId, workspaceId, deletedAt);
1560
+ if (canonicalAsset.kind === 'image') {
1561
+ core.deleteAnnotatedAttachmentSessionsForImage({
1562
+ workspaceId,
1563
+ imageAssetId: canonicalAsset.assetId,
1564
+ deletedAt,
1565
+ });
1566
+ }
1567
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1568
+ res.end(JSON.stringify({ success: true, action, metadata: updated }));
1569
+ return;
1570
+ }
1571
+ }
1572
+ if (!metadata) {
1573
+ res.writeHead(404, { 'Content-Type': 'application/json' });
1574
+ res.end(JSON.stringify({ error: 'Metadata not found.' }));
1575
+ return;
1576
+ }
1577
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1578
+ res.end(JSON.stringify({ success: true, action, metadata }));
1579
+ return;
1580
+ }
1581
+ if (action === 'clear-deleted') {
1582
+ const metadata = clearAssetDeleted(normalizedPath);
1583
+ if (!metadata) {
1584
+ res.writeHead(404, { 'Content-Type': 'application/json' });
1585
+ res.end(JSON.stringify({ error: 'Metadata not found.' }));
1586
+ return;
1587
+ }
1588
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1589
+ res.end(JSON.stringify({ success: true, action, metadata }));
1590
+ return;
1591
+ }
1592
+ if (action === 'remove-entry') {
1593
+ const removed = removeAssetMetadata(normalizedPath);
1594
+ if (!removed) {
1595
+ res.writeHead(404, { 'Content-Type': 'application/json' });
1596
+ res.end(JSON.stringify({ error: 'Metadata not found.' }));
1597
+ return;
1598
+ }
1599
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1600
+ res.end(JSON.stringify({ success: true, action, path: normalizedPath }));
1601
+ return;
1602
+ }
1603
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1604
+ res.end(JSON.stringify({ error: 'Unsupported action. Use mark-deleted, clear-deleted, or remove-entry.' }));
1605
+ });
1606
+ // POST /api/taskforce/admin/workspace-repair/scan - Scan canonical workspace sync surfaces for drift
1607
+ addRoute('POST', '/api/taskforce/admin/workspace-repair/scan', async (req, res) => {
1608
+ if (!ensureAdminRole(req)) {
1609
+ res.writeHead(403, { 'Content-Type': 'application/json' });
1610
+ res.end(JSON.stringify({ error: 'Forbidden: admin role required.' }));
1611
+ return;
1612
+ }
1613
+ const body = await parseJsonBody(req);
1614
+ const workspaceId = String(body?.workspaceId || '').trim() || requestWorkspaceId(req) || 'default';
1615
+ const report = scanWorkspaceRepair({
1616
+ workspaceId,
1617
+ basePath: core.getPaths().basePath,
1618
+ tasks: core.listTasks(workspaceId)?.tasks || [],
1619
+ archivedTasks: core.listArchive(workspaceId)?.archived || [],
1620
+ deletedTasks: core.listWorkspaceSyncDeletes(workspaceId) || [],
1621
+ workspaceAssetStore,
1622
+ });
1623
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1624
+ res.end(JSON.stringify({ success: true, ...report }));
1625
+ });
1626
+ // POST /api/taskforce/admin/workspace-repair/apply - Apply deterministic canonical workspace repairs
1627
+ addRoute('POST', '/api/taskforce/admin/workspace-repair/apply', async (req, res) => {
1628
+ if (!ensureAdminRole(req)) {
1629
+ res.writeHead(403, { 'Content-Type': 'application/json' });
1630
+ res.end(JSON.stringify({ error: 'Forbidden: admin role required.' }));
1631
+ return;
1632
+ }
1633
+ const body = await parseJsonBody(req);
1634
+ const workspaceId = String(body?.workspaceId || '').trim() || requestWorkspaceId(req) || 'default';
1635
+ const report = applyWorkspaceRepair({
1636
+ workspaceId,
1637
+ basePath: core.getPaths().basePath,
1638
+ tasks: core.listTasks(workspaceId)?.tasks || [],
1639
+ archivedTasks: core.listArchive(workspaceId)?.archived || [],
1640
+ deletedTasks: core.listWorkspaceSyncDeletes(workspaceId) || [],
1641
+ workspaceAssetStore,
1642
+ upsertDocumentWatermark: (targetWorkspaceId, documentPath, contentHash, updatedAt) => {
1643
+ core.upsertDocumentWatermark(targetWorkspaceId, documentPath, contentHash, updatedAt);
1644
+ }
1645
+ });
1646
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1647
+ res.end(JSON.stringify({ success: true, ...report }));
1648
+ });
1649
+ // POST /api/taskforce/admin/object-storage-settings - Save object storage admin overrides
1650
+ addRoute('POST', '/api/taskforce/admin/object-storage-settings', async (req, res) => {
1651
+ if (!ensureAdminRole(req)) {
1652
+ res.writeHead(403, { 'Content-Type': 'application/json' });
1653
+ res.end(JSON.stringify({ error: 'Forbidden: admin role required.' }));
1654
+ return;
1655
+ }
1656
+ const body = await parseJsonBody(req);
1657
+ const keyPrefixRaw = String(body?.keyPrefix ?? '').trim();
1658
+ const uploadTtlRaw = Number(body?.signedUploadTtlSeconds);
1659
+ const downloadTtlRaw = Number(body?.signedDownloadTtlSeconds);
1660
+ const defaults = {
1661
+ keyPrefix: 'taskforce',
1662
+ signedUploadTtlSeconds: 600,
1663
+ signedDownloadTtlSeconds: 300,
1664
+ virusScanAsyncEnabled: true,
1665
+ virusScanFailClosed: baseVirusScanConfig.failClosed
1666
+ };
1667
+ const keyPrefix = typeof body?.keyPrefix === 'string'
1668
+ ? keyPrefixRaw.replace(/^\/+|\/+$/g, '')
1669
+ : defaults.keyPrefix;
1670
+ const signedUploadTtlSeconds = Number.isFinite(uploadTtlRaw) && uploadTtlRaw > 0 ? Math.floor(uploadTtlRaw) : defaults.signedUploadTtlSeconds;
1671
+ const signedDownloadTtlSeconds = Number.isFinite(downloadTtlRaw) && downloadTtlRaw > 0 ? Math.floor(downloadTtlRaw) : defaults.signedDownloadTtlSeconds;
1672
+ const virusScanAsyncEnabled = typeof body?.virusScanAsyncEnabled === 'boolean' ? body.virusScanAsyncEnabled : defaults.virusScanAsyncEnabled;
1673
+ const virusScanFailClosed = typeof body?.virusScanFailClosed === 'boolean' ? body.virusScanFailClosed : defaults.virusScanFailClosed;
1674
+ core.updateGlobalSettings({
1675
+ objectStorage: { keyPrefix, signedUploadTtlSeconds, signedDownloadTtlSeconds, virusScanAsyncEnabled, virusScanFailClosed }
1676
+ });
1677
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1678
+ res.end(JSON.stringify({ success: true }));
1679
+ });
1680
+ // POST /api/taskforce/admin/system-settings - Save auth/system behavior settings
1681
+ addRoute('POST', '/api/taskforce/admin/system-settings', async (req, res) => {
1682
+ if (!ensureAdminRole(req)) {
1683
+ res.writeHead(403, { 'Content-Type': 'application/json' });
1684
+ res.end(JSON.stringify({ error: 'Forbidden: admin role required.' }));
1685
+ return;
1686
+ }
1687
+ const body = await parseJsonBody(req);
1688
+ const signupMonetizationStrategy = body?.signupMonetizationStrategy === 'plan_selection_required'
1689
+ ? 'plan_selection_required'
1690
+ : 'auto_assign_default';
1691
+ const defaultSignupPlanVersionId = typeof body?.defaultSignupPlanVersionId === 'string'
1692
+ ? body.defaultSignupPlanVersionId.trim() || null
1693
+ : body?.defaultSignupPlanVersionId === null
1694
+ ? null
1695
+ : undefined;
1696
+ const validation = core.validateSignupMonetizationSettings({
1697
+ strategy: signupMonetizationStrategy,
1698
+ ...(defaultSignupPlanVersionId !== undefined ? { defaultSignupPlanVersionId } : {})
1699
+ });
1700
+ if (!validation.valid) {
1701
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1702
+ res.end(JSON.stringify({ error: validation.message || 'Invalid signup monetization settings.', code: validation.code, details: validation.details }));
1703
+ return;
1704
+ }
1705
+ core.updateGlobalSettings({
1706
+ auth: {
1707
+ ...(typeof body?.allowSelfRegistration === 'boolean' ? { allowSelfRegistration: body.allowSelfRegistration } : {}),
1708
+ ...(typeof body?.requireVerifiedEmail === 'boolean' ? { requireVerifiedEmail: body.requireVerifiedEmail } : {}),
1709
+ ...(typeof body?.defaultNewUserBetaAccess === 'boolean' ? { defaultNewUserBetaAccess: body.defaultNewUserBetaAccess } : {}),
1710
+ ...(typeof body?.authRateLimitEnabled === 'boolean' ? { authRateLimitEnabled: body.authRateLimitEnabled } : {}),
1711
+ ...(typeof body?.authRateLimitMaxRequests === 'number' && Number.isFinite(body.authRateLimitMaxRequests)
1712
+ ? { authRateLimitMaxRequests: Math.max(1, Math.floor(body.authRateLimitMaxRequests)) } : {}),
1713
+ ...(typeof body?.authRateLimitWindowMs === 'number' && Number.isFinite(body.authRateLimitWindowMs)
1714
+ ? { authRateLimitWindowMs: Math.max(1000, Math.floor(body.authRateLimitWindowMs)) } : {}),
1715
+ signupMonetizationStrategy,
1716
+ ...(defaultSignupPlanVersionId !== undefined ? { defaultSignupPlanVersionId } : {})
1717
+ }
1718
+ });
1719
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1720
+ res.end(JSON.stringify({ success: true }));
1721
+ });
1722
+ const handlePostSiteSettings = async (req, res) => {
1723
+ if (!ensureAdminRole(req)) {
1724
+ res.writeHead(403, { 'Content-Type': 'application/json' });
1725
+ res.end(JSON.stringify({ error: 'Forbidden: admin role required.' }));
1726
+ return;
1727
+ }
1728
+ const body = await parseJsonBody(req);
1729
+ core.updateGlobalSettings({
1730
+ site: {
1731
+ ...(typeof body?.showRunTaskforceLocally === 'boolean'
1732
+ ? { showRunTaskforceLocally: body.showRunTaskforceLocally }
1733
+ : {}),
1734
+ ...(typeof body?.pricingPageEnabled === 'boolean'
1735
+ ? { pricingPageEnabled: body.pricingPageEnabled }
1736
+ : {})
1737
+ }
1738
+ });
1739
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1740
+ res.end(JSON.stringify({ success: true }));
1741
+ };
1742
+ addRoute('POST', '/api/taskforce/admin/site-settings', handlePostSiteSettings);
1743
+ addRoute('POST', '/api/taskforce/admin/site_settings', handlePostSiteSettings);
1744
+ // POST /api/taskforce/admin/email-settings - Save email settings (including provider API key)
1745
+ addRoute('POST', '/api/taskforce/admin/email-settings', async (req, res) => {
1746
+ if (!ensureAdminRole(req)) {
1747
+ res.writeHead(403, { 'Content-Type': 'application/json' });
1748
+ res.end(JSON.stringify({ error: 'Forbidden: admin role required.' }));
1749
+ return;
1750
+ }
1751
+ if (!requireCloudRuntime(res, 'Email settings'))
1752
+ return;
1753
+ const body = await parseJsonBody(req);
1754
+ const provider = String(body?.provider || '').trim().toLowerCase();
1755
+ const from = String(body?.from || '').trim();
1756
+ const replyTo = String(body?.replyTo || '').trim();
1757
+ const providerApiKeyRaw = typeof body?.providerApiKey === 'string' ? body.providerApiKey : undefined;
1758
+ const providerApiKey = providerApiKeyRaw?.trim();
1759
+ const clearProviderApiKey = body?.clearProviderApiKey === true;
1760
+ const emailUpdate = {
1761
+ ...(provider === 'console' || provider === 'resend' || provider === 'google-workspace' ? { provider: provider } : {}),
1762
+ ...(from ? { from } : {}),
1763
+ replyTo
1764
+ };
1765
+ if (typeof providerApiKeyRaw === 'string') {
1766
+ emailUpdate.providerApiKey = providerApiKey || '';
1767
+ }
1768
+ else if (clearProviderApiKey) {
1769
+ emailUpdate.providerApiKey = '';
1770
+ }
1771
+ core.updateGlobalSettings({ email: emailUpdate });
1772
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1773
+ res.end(JSON.stringify({ success: true }));
1774
+ });
1775
+ // POST /api/taskforce/admin/email-settings/test - Send test email with effective config
1776
+ addRoute('POST', '/api/taskforce/admin/email-settings/test', async (req, res) => {
1777
+ if (!ensureAdminRole(req)) {
1778
+ res.writeHead(403, { 'Content-Type': 'application/json' });
1779
+ res.end(JSON.stringify({ error: 'Forbidden: admin role required.' }));
1780
+ return;
1781
+ }
1782
+ if (!requireCloudRuntime(res, 'Test email delivery'))
1783
+ return;
1784
+ const body = await parseJsonBody(req);
1785
+ const to = String(body?.to || '').trim();
1786
+ if (!to || !to.includes('@')) {
1787
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1788
+ res.end(JSON.stringify({ success: false, error: 'Valid test email address is required.' }));
1789
+ return;
1790
+ }
1791
+ const config = getEmailConfig();
1792
+ const result = await sendEmail({
1793
+ to,
1794
+ subject: 'Taskforce email settings test',
1795
+ text: `This is a Taskforce test email.\n\nProvider: ${config.provider}\nFrom: ${config.from}\nReply-To: ${config.replyTo || '(none)'}`
1796
+ }, config);
1797
+ if (!result.ok) {
1798
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1799
+ res.end(JSON.stringify({ success: false, error: result.error || 'Test email failed.', provider: result.provider }));
1800
+ return;
1801
+ }
1802
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1803
+ res.end(JSON.stringify({ success: true, provider: result.provider, messageId: result.messageId || null }));
1804
+ });
1805
+ // === SECTION: Cloud MCP HTTP transport ===
1806
+ addRoute('POST', '/mcp', async (req, res) => { await handleCloudMcpRequest(req, res); });
1807
+ addRoute('POST', '/mcp/', async (req, res) => { await handleCloudMcpRequest(req, res); });
1808
+ addRoute('GET', '/mcp', async (req, res) => { await handleCloudMcpRequest(req, res); });
1809
+ addRoute('GET', '/mcp/', async (req, res) => { await handleCloudMcpRequest(req, res); });
1810
+ addRoute('DELETE', '/mcp', async (req, res) => { await handleCloudMcpRequest(req, res); });
1811
+ addRoute('DELETE', '/mcp/', async (req, res) => { await handleCloudMcpRequest(req, res); });
1812
+ addRoute('OPTIONS', '/mcp', async (_req, res) => {
1813
+ res.writeHead(204, {
1814
+ 'Access-Control-Allow-Origin': '*',
1815
+ 'Access-Control-Allow-Headers': 'authorization, content-type, accept, last-event-id, mcp-protocol-version, mcp-session-id, x-taskforce-workspace-id, x-taskforce-ai-profile-token, x-taskforce-ai-name, x-taskforce-ai-description, x-taskforce-ai-role, x-taskforce-ai-provider, x-taskforce-ai-model, x-taskforce-ai-surface-type',
1816
+ 'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS'
1817
+ });
1818
+ res.end();
1819
+ });
1820
+ addRoute('OPTIONS', '/mcp/', async (_req, res) => {
1821
+ res.writeHead(204, {
1822
+ 'Access-Control-Allow-Origin': '*',
1823
+ 'Access-Control-Allow-Headers': 'authorization, content-type, accept, last-event-id, mcp-protocol-version, mcp-session-id, x-taskforce-workspace-id, x-taskforce-ai-profile-token, x-taskforce-ai-name, x-taskforce-ai-description, x-taskforce-ai-role, x-taskforce-ai-provider, x-taskforce-ai-model, x-taskforce-ai-surface-type',
1824
+ 'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS'
1825
+ });
1826
+ res.end();
1827
+ });
1828
+ // === SECTION: MCP OAuth 2.0 ===
1829
+ addRoute('GET', '/.well-known/oauth-authorization-server', async (req, res) => {
1830
+ const issuer = resolveMcpOauthIssuer(req);
1831
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1832
+ res.end(JSON.stringify({
1833
+ issuer,
1834
+ authorization_endpoint: resolveMcpOauthAuthorizeUrl(req),
1835
+ token_endpoint: resolveMcpOauthTokenUrl(req),
1836
+ registration_endpoint: resolveMcpOauthRegisterUrl(req),
1837
+ response_types_supported: ['code'],
1838
+ grant_types_supported: ['authorization_code'],
1839
+ token_endpoint_auth_methods_supported: ['none'],
1840
+ code_challenge_methods_supported: ['S256'],
1841
+ scopes_supported: ['tasks:read', 'tasks:write', 'workspace:read'],
1842
+ }));
1843
+ });
1844
+ addRoute('GET', '/.well-known/oauth-protected-resource', async (req, res) => {
1845
+ const issuer = resolveMcpOauthIssuer(req);
1846
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1847
+ res.end(JSON.stringify({
1848
+ resource: resolveMcpOauthResourceUrl(req),
1849
+ authorization_servers: [issuer],
1850
+ bearer_methods_supported: ['header'],
1851
+ }));
1852
+ });
1853
+ addRoute('POST', '/oauth/mcp/register', async (req, res) => {
1854
+ if (!ensureAppAccess(req, res))
1855
+ return;
1856
+ const body = await parseJsonBody(req);
1857
+ try {
1858
+ const provider = (() => {
1859
+ const explicit = String(body?.provider || '').trim().toLowerCase();
1860
+ if (explicit === 'chatgpt' || explicit === 'claude')
1861
+ return explicit;
1862
+ const values = Array.isArray(body?.redirect_uris) ? body.redirect_uris : [];
1863
+ for (const value of values) {
1864
+ try {
1865
+ const hostname = new URL(String(value || '').trim()).hostname.toLowerCase();
1866
+ if (hostname.includes('anthropic.com') || hostname.includes('claude.ai'))
1867
+ return 'claude';
1868
+ if (hostname.includes('openai.com') || hostname.includes('chatgpt.com') || hostname.includes('chat.openai.com'))
1869
+ return 'chatgpt';
1870
+ }
1871
+ catch { /* ignore invalid redirect URI */ }
1872
+ }
1873
+ return 'chatgpt';
1874
+ })();
1875
+ const client = core.registerMcpOAuthClient({
1876
+ provider,
1877
+ clientName: body?.client_name,
1878
+ redirectUris: body?.redirect_uris,
1879
+ });
1880
+ res.writeHead(201, { 'Content-Type': 'application/json' });
1881
+ res.end(JSON.stringify({
1882
+ client_id: client.id,
1883
+ client_name: client.clientName,
1884
+ redirect_uris: client.redirectUris,
1885
+ grant_types: ['authorization_code'],
1886
+ response_types: ['code'],
1887
+ token_endpoint_auth_method: 'none',
1888
+ scope: 'tasks:read tasks:write workspace:read',
1889
+ }));
1890
+ }
1891
+ catch (error) {
1892
+ const statusCode = resolveErrorStatusCode(error);
1893
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
1894
+ res.end(JSON.stringify({
1895
+ error: 'invalid_client_metadata',
1896
+ error_description: error.message,
1897
+ code: error?.code || 'MCP_OAUTH_CLIENT_CREATE_FAILED',
1898
+ }));
1899
+ }
1900
+ });
1901
+ addRoute('GET', '/oauth/mcp/authorize', async (req, res) => {
1902
+ if (!ensureAppAccess(req, res))
1903
+ return;
1904
+ const reqUrl = new URL(req.url || '/oauth/mcp/authorize', resolveMcpOauthIssuer(req));
1905
+ const clientId = String(reqUrl.searchParams.get('client_id') || '').trim();
1906
+ const redirectUri = String(reqUrl.searchParams.get('redirect_uri') || '').trim();
1907
+ const responseType = String(reqUrl.searchParams.get('response_type') || '').trim();
1908
+ const state = String(reqUrl.searchParams.get('state') || '').trim();
1909
+ const codeChallenge = String(reqUrl.searchParams.get('code_challenge') || '').trim();
1910
+ const codeChallengeMethod = String(reqUrl.searchParams.get('code_challenge_method') || 'S256').trim() || 'S256';
1911
+ const scopes = normalizeMcpOauthScopeString(reqUrl.searchParams.get('scope'));
1912
+ if (responseType !== 'code' || !clientId || !redirectUri || !codeChallenge) {
1913
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1914
+ res.end(JSON.stringify({ error: 'invalid_request', error_description: 'client_id, redirect_uri, response_type=code, and code_challenge are required.' }));
1915
+ return;
1916
+ }
1917
+ const access = context.authConfig ? resolveRequestAccess(req, context.authConfig) : null;
1918
+ if (!access?.authenticated || !access.userId || access.userId === 'anonymous') {
1919
+ res.writeHead(302, { Location: `${resolveCloudLoginBaseUrl(req)}/login?next=${encodeURIComponent(reqUrl.toString())}` });
1920
+ res.end();
1921
+ return;
1922
+ }
1923
+ try {
1924
+ const clientRecord = core.getMcpOAuthClient(clientId);
1925
+ if (!clientRecord)
1926
+ throw new TaskforceRuleError('OAuth client not found', 404, 'MCP_OAUTH_CLIENT_NOT_FOUND');
1927
+ if (!clientRecord.redirectUris.includes(redirectUri))
1928
+ throw new TaskforceRuleError('Redirect URI is not registered for this OAuth client', 400, 'MCP_OAUTH_REDIRECT_URI_INVALID');
1929
+ const workspaces = core.listUserWorkspaces(access.userId)
1930
+ .map((workspace) => ({ id: String(workspace.id || '').trim(), name: String(workspace.name || workspace.slug || workspace.id || 'Workspace'), role: String(workspace.role || 'member') }))
1931
+ .filter((workspace) => Boolean(workspace.id));
1932
+ if (workspaces.length === 0)
1933
+ throw new TaskforceRuleError('No accessible workspaces found for this user', 403, 'MCP_OAUTH_WORKSPACE_REQUIRED');
1934
+ const preferredWorkspaceId = workspaces.find((workspace) => workspace.id === String(access.workspaceId || '').trim())?.id || workspaces[0].id;
1935
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
1936
+ res.end(renderMcpOauthAuthorizePage({ userName: String(access.userId), workspaces, preselectedWorkspaceId: preferredWorkspaceId, clientName: clientRecord.clientName, clientId: clientRecord.id, redirectUri, scopes, state, codeChallenge, codeChallengeMethod }));
1937
+ }
1938
+ catch (error) {
1939
+ const statusCode = resolveErrorStatusCode(error);
1940
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
1941
+ res.end(JSON.stringify({ error: 'invalid_request', error_description: error.message, code: error?.code || 'MCP_OAUTH_AUTHORIZE_FAILED' }));
1942
+ }
1943
+ });
1944
+ addRoute('POST', '/oauth/mcp/authorize', async (req, res) => {
1945
+ if (!ensureAppAccess(req, res))
1946
+ return;
1947
+ const access = context.authConfig ? resolveRequestAccess(req, context.authConfig) : null;
1948
+ if (!access?.authenticated || !access.userId || access.userId === 'anonymous') {
1949
+ res.writeHead(401, { 'Content-Type': 'application/json' });
1950
+ res.end(JSON.stringify({ error: 'login_required' }));
1951
+ return;
1952
+ }
1953
+ const body = await parseFormBody(req);
1954
+ const clientId = String(body.client_id || '').trim();
1955
+ const redirectUri = String(body.redirect_uri || '').trim();
1956
+ const state = String(body.state || '').trim();
1957
+ const workspaceId = String(body.workspaceId || '').trim();
1958
+ const decision = String(body.decision || 'deny').trim().toLowerCase();
1959
+ if (!clientId || !redirectUri) {
1960
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1961
+ res.end(JSON.stringify({ error: 'invalid_request', error_description: 'client_id and redirect_uri are required.' }));
1962
+ return;
1963
+ }
1964
+ if (decision !== 'approve') {
1965
+ const denyTarget = new URL(redirectUri);
1966
+ denyTarget.searchParams.set('error', 'access_denied');
1967
+ if (state)
1968
+ denyTarget.searchParams.set('state', state);
1969
+ res.writeHead(302, { Location: denyTarget.toString() });
1970
+ res.end();
1971
+ return;
1972
+ }
1973
+ try {
1974
+ const scopes = normalizeMcpOauthScopeString(body.scope);
1975
+ const clientRecord = core.getMcpOAuthClient(clientId);
1976
+ if (!clientRecord)
1977
+ throw new TaskforceRuleError('OAuth client not found', 404, 'MCP_OAUTH_CLIENT_NOT_FOUND');
1978
+ const issued = core.issueMcpOAuthAuthorizationCode({
1979
+ provider: clientRecord.provider,
1980
+ clientId,
1981
+ userId: access.userId,
1982
+ workspaceId,
1983
+ scopes,
1984
+ redirectUri,
1985
+ codeChallenge: String(body.code_challenge || '').trim(),
1986
+ codeChallengeMethod: String(body.code_challenge_method || 'S256').trim() || 'S256',
1987
+ });
1988
+ core.addAdminAuditLog({
1989
+ action: 'mcp.connector.authorize',
1990
+ actorUserId: access.userId,
1991
+ actorRole: String(access.role || 'member'),
1992
+ workspaceId,
1993
+ details: { provider: clientRecord.provider, clientId, scopes }
1994
+ });
1995
+ const approveTarget = new URL(redirectUri);
1996
+ approveTarget.searchParams.set('code', issued.code);
1997
+ if (state)
1998
+ approveTarget.searchParams.set('state', state);
1999
+ res.writeHead(302, { Location: approveTarget.toString() });
2000
+ res.end();
2001
+ }
2002
+ catch (error) {
2003
+ const statusCode = resolveErrorStatusCode(error);
2004
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
2005
+ res.end(JSON.stringify({ error: 'access_denied', error_description: error.message, code: error?.code || 'MCP_OAUTH_AUTHORIZE_FAILED' }));
2006
+ }
2007
+ });
2008
+ addRoute('POST', '/oauth/mcp/token', async (req, res) => {
2009
+ if (!ensureAppAccess(req, res))
2010
+ return;
2011
+ const body = await parseFormBody(req);
2012
+ if (String(body.grant_type || '').trim() !== 'authorization_code') {
2013
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2014
+ res.end(JSON.stringify({ error: 'unsupported_grant_type', error_description: 'Only authorization_code is supported.' }));
2015
+ return;
2016
+ }
2017
+ try {
2018
+ const clientId = String(body.client_id || '').trim();
2019
+ const clientRecord = core.getMcpOAuthClient(clientId);
2020
+ if (!clientRecord)
2021
+ throw new TaskforceRuleError('OAuth client not found', 404, 'MCP_OAUTH_CLIENT_NOT_FOUND');
2022
+ const result = core.exchangeMcpOAuthAuthorizationCode({
2023
+ provider: clientRecord.provider,
2024
+ clientId,
2025
+ code: String(body.code || '').trim(),
2026
+ redirectUri: String(body.redirect_uri || '').trim(),
2027
+ codeVerifier: String(body.code_verifier || '').trim(),
2028
+ });
2029
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
2030
+ res.end(JSON.stringify({ access_token: result.accessToken, token_type: result.tokenType, scope: result.scopes.join(' ') }));
2031
+ }
2032
+ catch (error) {
2033
+ const statusCode = resolveErrorStatusCode(error);
2034
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
2035
+ res.end(JSON.stringify({ error: 'invalid_grant', error_description: error.message, code: error?.code || 'MCP_OAUTH_TOKEN_EXCHANGE_FAILED' }));
2036
+ }
2037
+ });
2038
+ // === SECTION: MCP token and connector settings ===
2039
+ addRoute('GET', '/api/taskforce/settings/mcp/tokens', async (req, res) => {
2040
+ if (!ensureAppAccess(req, res))
2041
+ return;
2042
+ const userId = resolveAuthenticatedUserId(req);
2043
+ if (!userId || userId === 'anonymous') {
2044
+ res.writeHead(401, { 'Content-Type': 'application/json' });
2045
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
2046
+ return;
2047
+ }
2048
+ const reqUrl = new URL(req.url || '/api/taskforce/settings/mcp/tokens', 'http://localhost');
2049
+ const page = Number.parseInt(String(reqUrl.searchParams.get('page') || '1'), 10);
2050
+ const pageSize = Number.parseInt(String(reqUrl.searchParams.get('pageSize') || '20'), 10);
2051
+ try {
2052
+ const result = core.listMcpAccessTokens({ userId, workspaceId: requestWorkspaceId(req), page, pageSize });
2053
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2054
+ res.end(JSON.stringify({ success: true, ...result }));
2055
+ }
2056
+ catch (error) {
2057
+ const statusCode = resolveErrorStatusCode(error);
2058
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
2059
+ res.end(JSON.stringify({ success: false, error: error.message, code: error?.code || 'MCP_TOKEN_LIST_FAILED' }));
2060
+ }
2061
+ });
2062
+ addRoute('GET', '/api/taskforce/settings/mcp/connectors', async (req, res) => {
2063
+ if (!ensureAppAccess(req, res))
2064
+ return;
2065
+ const userId = resolveAuthenticatedUserId(req);
2066
+ if (!userId || userId === 'anonymous') {
2067
+ res.writeHead(401, { 'Content-Type': 'application/json' });
2068
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
2069
+ return;
2070
+ }
2071
+ const reqUrl = new URL(req.url || '/api/taskforce/settings/mcp/connectors', 'http://localhost');
2072
+ const provider = String(reqUrl.searchParams.get('provider') || '').trim();
2073
+ try {
2074
+ const items = core.listMcpOAuthConnectorGrants({ userId, workspaceId: requestWorkspaceId(req), provider: provider || null });
2075
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2076
+ res.end(JSON.stringify({ success: true, items }));
2077
+ }
2078
+ catch (error) {
2079
+ const statusCode = resolveErrorStatusCode(error);
2080
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
2081
+ res.end(JSON.stringify({ success: false, error: error.message, code: error?.code || 'MCP_CONNECTOR_LIST_FAILED' }));
2082
+ }
2083
+ });
2084
+ addRoute('POST', '/api/taskforce/settings/mcp/connectors/:id/disconnect', async (req, res, params) => {
2085
+ if (!ensureAppAccess(req, res))
2086
+ return;
2087
+ const userId = resolveAuthenticatedUserId(req);
2088
+ if (!userId || userId === 'anonymous') {
2089
+ res.writeHead(401, { 'Content-Type': 'application/json' });
2090
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
2091
+ return;
2092
+ }
2093
+ const wid = requestWorkspaceId(req);
2094
+ try {
2095
+ const record = core.revokeMcpOAuthConnectorGrant({ actorUserId: userId, workspaceId: wid, grantId: params.id });
2096
+ core.addAdminAuditLog({ action: 'mcp.connector.disconnect', actorUserId: userId, actorRole: requestRole(req), workspaceId: wid, details: { provider: record.provider, clientId: record.clientId, grantId: record.id } });
2097
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2098
+ res.end(JSON.stringify({ success: true, record }));
2099
+ }
2100
+ catch (error) {
2101
+ const statusCode = resolveErrorStatusCode(error);
2102
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
2103
+ res.end(JSON.stringify({ success: false, error: error.message, code: error?.code || 'MCP_CONNECTOR_DISCONNECT_FAILED' }));
2104
+ }
2105
+ });
2106
+ addRoute('POST', '/api/taskforce/settings/mcp/tokens', async (req, res) => {
2107
+ if (!ensureAppAccess(req, res))
2108
+ return;
2109
+ const userId = resolveAuthenticatedUserId(req);
2110
+ if (!userId || userId === 'anonymous') {
2111
+ res.writeHead(401, { 'Content-Type': 'application/json' });
2112
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
2113
+ return;
2114
+ }
2115
+ const body = await parseJsonBody(req);
2116
+ try {
2117
+ const created = core.createMcpAccessToken({ userId, workspaceId: requestWorkspaceId(req), name: body?.name, scopes: body?.scopes, expiresAt: typeof body?.expiresAt === 'string' ? body.expiresAt : null, createdByIp: req.socket?.remoteAddress || null });
2118
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2119
+ res.end(JSON.stringify({ success: true, token: created.token, record: created.record }));
2120
+ }
2121
+ catch (error) {
2122
+ const statusCode = resolveErrorStatusCode(error);
2123
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
2124
+ res.end(JSON.stringify({ success: false, error: error.message, code: error?.code || 'MCP_TOKEN_CREATE_FAILED' }));
2125
+ }
2126
+ });
2127
+ addRoute('POST', '/api/taskforce/settings/mcp/tokens/:id/revoke', async (req, res, params) => {
2128
+ if (!ensureAppAccess(req, res))
2129
+ return;
2130
+ const userId = resolveAuthenticatedUserId(req);
2131
+ if (!userId || userId === 'anonymous') {
2132
+ res.writeHead(401, { 'Content-Type': 'application/json' });
2133
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
2134
+ return;
2135
+ }
2136
+ try {
2137
+ const record = core.revokeMcpAccessToken({ actorUserId: userId, workspaceId: requestWorkspaceId(req), tokenId: params.id });
2138
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2139
+ res.end(JSON.stringify({ success: true, record }));
2140
+ }
2141
+ catch (error) {
2142
+ const statusCode = resolveErrorStatusCode(error);
2143
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
2144
+ res.end(JSON.stringify({ success: false, error: error.message, code: error?.code || 'MCP_TOKEN_REVOKE_FAILED' }));
2145
+ }
2146
+ });
2147
+ addRoute('POST', '/api/taskforce/settings/mcp/tokens/:id/regenerate', async (req, res, params) => {
2148
+ if (!ensureAppAccess(req, res))
2149
+ return;
2150
+ const userId = resolveAuthenticatedUserId(req);
2151
+ if (!userId || userId === 'anonymous') {
2152
+ res.writeHead(401, { 'Content-Type': 'application/json' });
2153
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
2154
+ return;
2155
+ }
2156
+ try {
2157
+ const regenerated = core.regenerateMcpAccessToken({ actorUserId: userId, workspaceId: requestWorkspaceId(req), tokenId: params.id, createdByIp: req.socket?.remoteAddress || null });
2158
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2159
+ res.end(JSON.stringify({ success: true, token: regenerated.token, record: regenerated.record }));
2160
+ }
2161
+ catch (error) {
2162
+ const statusCode = resolveErrorStatusCode(error);
2163
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
2164
+ res.end(JSON.stringify({ success: false, error: error.message, code: error?.code || 'MCP_TOKEN_REGENERATE_FAILED' }));
2165
+ }
2166
+ });
2167
+ addRoute('POST', '/api/taskforce/settings/mcp/test-connection', async (req, res) => {
2168
+ if (!ensureAppAccess(req, res))
2169
+ return;
2170
+ const userId = resolveAuthenticatedUserId(req);
2171
+ if (!userId || userId === 'anonymous') {
2172
+ res.writeHead(401, { 'Content-Type': 'application/json' });
2173
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
2174
+ return;
2175
+ }
2176
+ const body = await parseJsonBody(req);
2177
+ const token = typeof body?.token === 'string' ? body.token.trim() : '';
2178
+ if (!token) {
2179
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2180
+ res.end(JSON.stringify({ success: false, error: 'Token is required.', code: 'MCP_TOKEN_REQUIRED' }));
2181
+ return;
2182
+ }
2183
+ const wid = requestWorkspaceId(req);
2184
+ const requiredScopes = Array.isArray(body?.requiredScopes)
2185
+ ? body.requiredScopes.map((scope) => String(scope || '').trim()).filter((scope) => MCP_TOKEN_SCOPE_VALUES.has(scope))
2186
+ : [];
2187
+ try {
2188
+ core.listMcpAccessTokens({ userId, workspaceId: wid, page: 1, pageSize: 1 });
2189
+ const inspection = core.inspectMcpAccessToken(token);
2190
+ if (!inspection) {
2191
+ res.writeHead(401, { 'Content-Type': 'application/json' });
2192
+ res.end(JSON.stringify({ success: false, error: 'Token is invalid.', code: 'MCP_TOKEN_INVALID' }));
2193
+ return;
2194
+ }
2195
+ if (inspection.status === 'revoked') {
2196
+ res.writeHead(401, { 'Content-Type': 'application/json' });
2197
+ res.end(JSON.stringify({ success: false, error: 'Token has been revoked.', code: 'MCP_TOKEN_REVOKED' }));
2198
+ return;
2199
+ }
2200
+ if (inspection.status === 'expired') {
2201
+ res.writeHead(401, { 'Content-Type': 'application/json' });
2202
+ res.end(JSON.stringify({ success: false, error: 'Token has expired.', code: 'MCP_TOKEN_EXPIRED' }));
2203
+ return;
2204
+ }
2205
+ if (inspection.workspaceId !== wid) {
2206
+ res.writeHead(403, { 'Content-Type': 'application/json' });
2207
+ res.end(JSON.stringify({ success: false, error: 'Token belongs to a different workspace.', code: 'MCP_TOKEN_WORKSPACE_MISMATCH', details: { expectedWorkspaceId: wid, tokenWorkspaceId: inspection.workspaceId } }));
2208
+ return;
2209
+ }
2210
+ const missingScopes = requiredScopes.filter((scope) => !inspection.scopes.includes(scope));
2211
+ if (missingScopes.length > 0) {
2212
+ res.writeHead(403, { 'Content-Type': 'application/json' });
2213
+ res.end(JSON.stringify({ success: false, error: 'Token is missing required scopes.', code: 'MCP_TOKEN_MISSING_SCOPES', details: { requiredScopes, missingScopes, grantedScopes: inspection.scopes } }));
2214
+ return;
2215
+ }
2216
+ const workspace = core.getWorkspaceProfile(wid);
2217
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2218
+ res.end(JSON.stringify({ success: true, result: { tokenId: inspection.tokenId, tokenName: inspection.name, workspaceId: wid, workspaceName: String(workspace?.name || wid), scopes: inspection.scopes, requiredScopes, endpoint: '/mcp' } }));
2219
+ }
2220
+ catch (error) {
2221
+ const statusCode = resolveErrorStatusCode(error);
2222
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
2223
+ res.end(JSON.stringify({ success: false, error: error.message, code: error?.code || 'MCP_TEST_CONNECTION_FAILED' }));
2224
+ }
2225
+ });
2226
+ }