@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.
- package/README.md +71 -21
- package/dist/Taskforce.module.css +3394 -5981
- package/dist/TaskforceCore.CategoryFallback.test.js +5 -2
- package/dist/TaskforceCore.filter-persistence.test.js +402 -0
- package/dist/TaskforceCore.js +388 -71
- package/dist/TaskforceCore.routing.test.js +311 -0
- package/dist/TaskforceCore.test.js +3344 -615
- package/dist/TaskforceWidget.js +3 -2
- package/dist/__tests__/localizationCatalogs.test.js +48 -31
- package/dist/__tests__/workflowSimulation.test.js +2 -2
- package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.d.ts +60 -0
- package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.js +324 -0
- package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.test.js +278 -0
- package/dist/annotatedAttachments/payload.d.ts +2 -0
- package/dist/annotatedAttachments/payload.js +54 -0
- package/dist/annotatedAttachments/payload.test.js +67 -0
- package/dist/annotatedAttachments/service.d.ts +49 -0
- package/dist/annotatedAttachments/service.js +121 -0
- package/dist/annotatedAttachments/types.d.ts +86 -0
- package/dist/cli.js +74 -25
- package/dist/compat/deploymentCompat.d.ts +12 -0
- package/dist/compat/deploymentCompat.js +43 -0
- package/dist/compat/workspaceSyncCompat.d.ts +6 -0
- package/dist/compat/workspaceSyncCompat.js +79 -0
- package/dist/compat/workspaceSyncCompat.test.js +87 -0
- package/dist/components/features/AgentsModule.d.ts +5 -0
- package/dist/components/features/AgentsModule.js +131 -0
- package/dist/components/features/AgentsModule.test.js +102 -0
- package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +24 -0
- package/dist/components/features/AnnotatedAttachmentWorkspace.js +1942 -0
- package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +2831 -0
- package/dist/components/features/DocumentGenerateModal.js +8 -16
- package/dist/components/features/DocumentIndex.d.ts +3 -4
- package/dist/components/features/DocumentIndex.js +22 -10
- package/dist/components/features/DocumentViewer.d.ts +6 -2
- package/dist/components/features/DocumentViewer.js +1000 -18
- package/dist/components/features/DocumentWorkspace.d.ts +40 -2
- package/dist/components/features/DocumentWorkspace.js +96 -16
- package/dist/components/features/DocumentWorkspace.test.js +1202 -0
- package/dist/components/features/InitiativesModule.d.ts +22 -0
- package/dist/components/features/InitiativesModule.js +33 -0
- package/dist/components/features/TaskSettings.d.ts +41 -32
- package/dist/components/features/TaskSettings.js +798 -313
- package/dist/components/features/TaskSettings.test.js +1221 -47
- package/dist/components/features/WorkflowsModule.d.ts +27 -0
- package/dist/components/features/WorkflowsModule.js +21 -0
- package/dist/components/features/documentWorkspaceFilters.d.ts +10 -0
- package/dist/components/features/documentWorkspaceFilters.js +11 -0
- package/dist/components/features/settings/CategoryManager.d.ts +2 -0
- package/dist/components/features/settings/CategoryManager.js +15 -8
- package/dist/components/features/settings/PriorityManager.d.ts +18 -0
- package/dist/components/features/settings/PriorityManager.js +51 -0
- package/dist/components/features/settings/TaxonomyEditor.d.ts +2 -1
- package/dist/components/features/settings/TaxonomyEditor.js +41 -18
- package/dist/components/features/settings/TaxonomyLibraryManager.d.ts +26 -0
- package/dist/components/features/settings/TaxonomyLibraryManager.js +301 -0
- package/dist/components/features/settings/TaxonomyLibraryManager.test.js +141 -0
- package/dist/components/features/settings/TaxonomyManager.d.ts +35 -12
- package/dist/components/features/settings/TaxonomyManager.js +59 -27
- package/dist/components/features/settings/TypeManager.d.ts +11 -0
- package/dist/components/features/settings/TypeManager.js +48 -11
- package/dist/components/features/settings/{WorkflowEditorPrototype.d.ts → WorkflowEditor.d.ts} +2 -2
- package/dist/components/features/settings/{WorkflowEditorPrototype.js → WorkflowEditor.js} +3 -3
- package/dist/components/task/TaskActionHeader.d.ts +12 -2
- package/dist/components/task/TaskActionHeader.js +64 -8
- package/dist/components/task/TaskActionHeader.test.js +73 -0
- package/dist/components/task/TaskCard.d.ts +14 -16
- package/dist/components/task/TaskCard.js +144 -174
- package/dist/components/task/TaskCard.test.js +416 -64
- package/dist/components/task/TaskContextUpload.d.ts +6 -5
- package/dist/components/task/TaskContextUpload.js +199 -64
- package/dist/components/task/TaskContextUpload.test.js +331 -14
- package/dist/components/task/TaskForm.d.ts +23 -29
- package/dist/components/task/TaskForm.js +348 -169
- package/dist/components/task/TaskForm.test.js +419 -23
- package/dist/components/task/TaskKanban.d.ts +20 -21
- package/dist/components/task/TaskKanban.js +176 -316
- package/dist/components/task/TaskKanban.planning-drop.test.js +93 -0
- package/dist/components/task/TaskList.d.ts +22 -11
- package/dist/components/task/TaskList.js +98 -12
- package/dist/components/task/TaskList.test.js +206 -7
- package/dist/components/task/TaskReferenceBadge.d.ts +12 -0
- package/dist/components/task/TaskReferenceBadge.js +5 -0
- package/dist/components/task/TaskStatusActions.d.ts +12 -0
- package/dist/components/task/TaskStatusActions.js +18 -0
- package/dist/components/task/TaskStatusActions.test.js +45 -0
- package/dist/components/ui/ImageReferenceBadge.d.ts +12 -0
- package/dist/components/ui/ImageReferenceBadge.js +5 -0
- package/dist/components/ui/LevelSelector.d.ts +2 -1
- package/dist/components/ui/LevelSelector.js +2 -2
- package/dist/components/ui/Markdown.d.ts +1 -2
- package/dist/components/ui/Markdown.js +2 -13
- package/dist/components/ui/Modal.d.ts +3 -2
- package/dist/components/ui/Modal.js +4 -3
- package/dist/components/ui/MultiSelectFilter.d.ts +3 -1
- package/dist/components/ui/MultiSelectFilter.js +2 -2
- package/dist/components/ui/ReferenceBadgeButton.d.ts +13 -0
- package/dist/components/ui/ReferenceBadgeButton.js +6 -0
- package/dist/components/ui/TaxonomyDropdown.d.ts +7 -1
- package/dist/components/ui/TaxonomyDropdown.js +58 -23
- package/dist/components/ui/TopNoticeLayer.d.ts +7 -0
- package/dist/components/ui/TopNoticeLayer.js +14 -0
- package/dist/components/ui/TopNoticeLayer.test.js +25 -0
- package/dist/components/views/AppShellHeader.d.ts +13 -0
- package/dist/components/views/AppShellHeader.js +19 -0
- package/dist/components/views/AppShellHeader.test.js +21 -0
- package/dist/components/views/PlanComparisonPage.d.ts +73 -0
- package/dist/components/views/PlanComparisonPage.js +196 -0
- package/dist/components/views/PlanComparisonPage.test.js +251 -0
- package/dist/components/views/PlansPage.d.ts +14 -0
- package/dist/components/views/PlansPage.js +355 -0
- package/dist/components/views/StandaloneLayout.d.ts +2 -2
- package/dist/components/views/StandaloneLayout.js +2227 -1188
- package/dist/components/views/WidgetView.d.ts +2 -2
- package/dist/components/views/WidgetView.js +149 -43
- package/dist/components/views/chrome/WorkspaceChromeSections.d.ts +57 -0
- package/dist/components/views/chrome/WorkspaceChromeSections.js +38 -0
- package/dist/components/views/chrome/WorkspaceChromeSections.test.js +54 -0
- package/dist/components/views/chrome/workspaceChrome.d.ts +31 -0
- package/dist/components/views/chrome/workspaceChrome.js +108 -0
- package/dist/components/views/chrome/workspaceChrome.test.js +52 -0
- package/dist/components/views/panels/DocumentFilterToolbar.d.ts +10 -0
- package/dist/components/views/panels/DocumentFilterToolbar.js +7 -0
- package/dist/components/views/panels/DocumentFilterToolbar.test.js +19 -0
- package/dist/components/views/panels/FilterToolbar.d.ts +68 -0
- package/dist/components/views/panels/FilterToolbar.js +21 -0
- package/dist/components/views/panels/FilterToolbar.test.d.ts +1 -0
- package/dist/components/views/panels/FilterToolbar.test.js +45 -0
- package/dist/components/views/panels/FilterToolbarControls.d.ts +47 -0
- package/dist/components/views/panels/FilterToolbarControls.js +61 -0
- package/dist/components/views/panels/PlanningDrawer.d.ts +115 -0
- package/dist/components/views/panels/PlanningDrawer.js +220 -0
- package/dist/components/views/panels/ScheduleSidebar.d.ts +52 -0
- package/dist/components/views/panels/ScheduleSidebar.js +127 -0
- package/dist/components/views/panels/scheduleUtils.d.ts +14 -0
- package/dist/components/views/panels/scheduleUtils.js +79 -0
- package/dist/components/views/teamManagementAccess.d.ts +6 -0
- package/dist/components/views/teamManagementAccess.js +18 -0
- package/dist/components/views/teamManagementAccess.test.d.ts +1 -0
- package/dist/components/views/teamManagementAccess.test.js +44 -0
- package/dist/config/clientDeployment.d.ts +2 -0
- package/dist/config/clientDeployment.js +13 -0
- package/dist/config/cloudEnvironment.d.ts +15 -0
- package/dist/config/cloudEnvironment.js +60 -0
- package/dist/config/deployment.d.ts +20 -0
- package/dist/config/deployment.js +67 -0
- package/dist/config/deployment.test.d.ts +1 -0
- package/dist/config/deployment.test.js +88 -0
- package/dist/config/envSchema.js +36 -8
- package/dist/config/envSchema.test.js +28 -0
- package/dist/core/AiProfileService.d.ts +66 -0
- package/dist/core/AiProfileService.js +320 -0
- package/dist/core/AiProfileService.test.d.ts +1 -0
- package/dist/core/AiProfileService.test.js +164 -0
- package/dist/core/AiProfiles.test.d.ts +1 -0
- package/dist/core/AiProfiles.test.js +449 -0
- package/dist/core/AttachmentLinkService.d.ts +15 -0
- package/dist/core/AttachmentLinkService.js +157 -0
- package/dist/core/AuthTokenService.d.ts +44 -0
- package/dist/core/AuthTokenService.js +207 -0
- package/dist/core/CreatedBy.test.js +134 -6
- package/dist/core/DeletedTaskLifecycleService.d.ts +33 -0
- package/dist/core/DeletedTaskLifecycleService.js +158 -0
- package/dist/core/DeletedTaskTrash.test.d.ts +1 -0
- package/dist/core/DeletedTaskTrash.test.js +104 -0
- package/dist/core/EntitlementsPolicy.test.js +78 -5
- package/dist/core/GlobalSettingsService.d.ts +16 -0
- package/dist/core/GlobalSettingsService.js +370 -0
- package/dist/core/JsonBackupRuntime.test.d.ts +1 -0
- package/dist/core/JsonBackupRuntime.test.js +155 -0
- package/dist/core/McpTokenService.d.ts +118 -0
- package/dist/core/McpTokenService.js +921 -0
- package/dist/core/PlanEntitlementService.d.ts +201 -0
- package/dist/core/PlanEntitlementService.js +1442 -0
- package/dist/core/PlanVersionPolicy.test.d.ts +1 -0
- package/dist/core/PlanVersionPolicy.test.js +178 -0
- package/dist/core/PlanningEntities.test.d.ts +1 -0
- package/dist/core/PlanningEntities.test.js +95 -0
- package/dist/core/ScheduleFields.test.js +15 -19
- package/dist/core/SignupMonetizationSettings.test.d.ts +1 -0
- package/dist/core/SignupMonetizationSettings.test.js +159 -0
- package/dist/core/Stats.test.d.ts +1 -0
- package/dist/core/Stats.test.js +36 -0
- package/dist/core/SyncReconciliationService.d.ts +93 -0
- package/dist/core/SyncReconciliationService.js +406 -0
- package/dist/core/SystemAdmin.test.js +578 -13
- package/dist/core/TaskActivityService.d.ts +21 -0
- package/dist/core/TaskActivityService.js +130 -0
- package/dist/core/TaskAttachmentsCanonical.test.d.ts +1 -0
- package/dist/core/TaskAttachmentsCanonical.test.js +306 -0
- package/dist/core/TaskAuditTimeline.test.d.ts +1 -0
- package/dist/core/TaskAuditTimeline.test.js +132 -0
- package/dist/core/TaskTaxonomyValidation.test.js +338 -15
- package/dist/core/Taskforce.d.ts +930 -183
- package/dist/core/Taskforce.ids.test.js +252 -0
- package/dist/core/Taskforce.js +3848 -2532
- package/dist/core/Taskforce.mcpAuth.test.d.ts +1 -0
- package/dist/core/Taskforce.mcpAuth.test.js +343 -0
- package/dist/core/Taskforce.realtimeMutation.test.js +57 -2
- package/dist/core/TaxonomySettingsService.d.ts +18 -0
- package/dist/core/TaxonomySettingsService.js +162 -0
- package/dist/core/WorkspaceDelete.test.js +32 -13
- package/dist/core/WorkspaceLifecycleService.d.ts +11 -0
- package/dist/core/WorkspaceLifecycleService.js +88 -0
- package/dist/core/WorkspacePermissions.test.js +95 -0
- package/dist/core/WorkspacePlanMode.test.js +114 -1
- package/dist/core/WorkspacePreferenceService.d.ts +41 -0
- package/dist/core/WorkspacePreferenceService.js +246 -0
- package/dist/core/WorkspaceSyncSettings.test.d.ts +1 -0
- package/dist/core/WorkspaceSyncSettings.test.js +65 -0
- package/dist/core/__tests__/TaskAttachmentsCanonical.test.d.ts +1 -0
- package/dist/core/__tests__/TaskAttachmentsCanonical.test.js +212 -0
- package/dist/core/shared.d.ts +36 -0
- package/dist/core/shared.js +128 -0
- package/dist/core/types.d.ts +614 -0
- package/dist/core/types.js +27 -0
- package/dist/documentReviews/DocumentReviewCommentStore.d.ts +56 -0
- package/dist/documentReviews/DocumentReviewCommentStore.js +230 -0
- package/dist/documentReviews/DocumentReviewSessionStore.d.ts +52 -0
- package/dist/documentReviews/DocumentReviewSessionStore.js +193 -0
- package/dist/documentReviews/DocumentReviewSessionStore.test.d.ts +1 -0
- package/dist/documentReviews/DocumentReviewSessionStore.test.js +127 -0
- package/dist/documentReviews/service.d.ts +68 -0
- package/dist/documentReviews/service.js +210 -0
- package/dist/documentReviews/types.d.ts +58 -0
- package/dist/documentReviews/types.js +1 -0
- package/dist/hooks/auth/useAuthGuardPolicy.d.ts +2 -0
- package/dist/hooks/auth/useAuthGuardPolicy.js +1 -0
- package/dist/hooks/tasks/useCurrentTaskRelations.d.ts +15 -0
- package/dist/hooks/tasks/useCurrentTaskRelations.js +37 -0
- package/dist/hooks/tasks/useTaskActions.d.ts +3 -0
- package/dist/hooks/tasks/useTaskActions.js +2 -0
- package/dist/hooks/tasks/useTaskEditorNavigation.d.ts +65 -0
- package/dist/hooks/tasks/useTaskEditorNavigation.js +235 -0
- package/dist/hooks/tasks/useTaskFilters.d.ts +2 -0
- package/dist/hooks/tasks/useTaskFilters.js +2 -0
- package/dist/hooks/tasks/useTaskFormActions.d.ts +58 -0
- package/dist/hooks/tasks/useTaskFormActions.js +365 -0
- package/dist/hooks/tasks/useTaskRelationships.d.ts +16 -0
- package/dist/hooks/tasks/useTaskRelationships.js +64 -0
- package/dist/hooks/ui/useResourceExport.d.ts +19 -0
- package/dist/hooks/ui/useResourceExport.js +84 -0
- package/dist/hooks/ui/useSettingsModel.d.ts +203 -0
- package/dist/hooks/ui/useSettingsModel.js +125 -0
- package/dist/hooks/ui/useSettingsPersistence.d.ts +36 -0
- package/dist/hooks/ui/useSettingsPersistence.js +254 -0
- package/dist/hooks/ui/useSettingsTaxonomyManagement.d.ts +119 -0
- package/dist/hooks/ui/useSettingsTaxonomyManagement.js +536 -0
- package/dist/hooks/ui/useUiNotice.d.ts +1 -0
- package/dist/hooks/ui/useUiNotice.js +1 -0
- package/dist/hooks/ui/useWorkflowTemplates.d.ts +37 -0
- package/dist/hooks/ui/useWorkflowTemplates.js +111 -0
- package/dist/hooks/ui/useZenMode.d.ts +5 -0
- package/dist/hooks/ui/useZenMode.js +32 -0
- package/dist/hooks/useFilterSort.d.ts +60 -0
- package/dist/hooks/useFilterSort.js +362 -0
- package/dist/hooks/useFilterSort.test.d.ts +1 -0
- package/dist/hooks/useFilterSort.test.js +333 -0
- package/dist/hooks/useRealtimeSync.d.ts +1 -0
- package/dist/hooks/useRealtimeSync.js +24 -9
- package/dist/hooks/useRealtimeSync.test.js +28 -1
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.d.ts +1 -0
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +34 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +65 -103
- package/dist/hooks/useSyncOrchestrator.js +1538 -670
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +700 -174
- package/dist/hooks/useTaskData.d.ts +35 -0
- package/dist/hooks/useTaskData.js +243 -0
- package/dist/hooks/useTaskData.test.d.ts +1 -0
- package/dist/hooks/useTaskData.test.js +180 -0
- package/dist/hooks/useTaskMutations.d.ts +46 -0
- package/dist/hooks/useTaskMutations.js +472 -0
- package/dist/hooks/useTaskMutations.test.d.ts +1 -0
- package/dist/hooks/useTaskMutations.test.js +80 -0
- package/dist/hooks/useTaskforce.contract.test.d.ts +1 -0
- package/dist/hooks/useTaskforce.contract.test.js +7 -0
- package/dist/hooks/useTaskforce.d.ts +260 -295
- package/dist/hooks/useTaskforce.js +1954 -2623
- package/dist/hooks/useTaskforce.priority-filters.test.js +6 -11
- package/dist/hooks/useTaskforce.sync-behavior.test.d.ts +1 -0
- package/dist/hooks/useTaskforce.sync-behavior.test.js +1120 -0
- package/dist/hooks/useUiNotice.d.ts +16 -0
- package/dist/hooks/useUiNotice.js +49 -0
- package/dist/hooks/useWorkspaceSyncController.d.ts +23 -46
- package/dist/hooks/useWorkspaceSyncController.js +280 -461
- package/dist/hooks/useWorkspaceSyncController.test.js +106 -199
- package/dist/hooks/workspace/useTaskforceApiRouting.d.ts +8 -0
- package/dist/hooks/workspace/useTaskforceApiRouting.js +91 -0
- package/dist/hooks/workspace/useTaskforceApiRouting.test.d.ts +1 -0
- package/dist/hooks/workspace/useTaskforceApiRouting.test.js +60 -0
- package/dist/hooks/workspace/useWorkspaceSyncController.d.ts +1 -0
- package/dist/hooks/workspace/useWorkspaceSyncController.js +1 -0
- package/dist/hooks/workspace/workspaceLocalState.d.ts +30 -0
- package/dist/hooks/workspace/workspaceLocalState.js +101 -0
- package/dist/localization/locales/en-US.d.ts +1 -8
- package/dist/localization/locales/en-US.js +4 -11
- package/dist/localization/locales/es-419.js +2 -9
- package/dist/localization/locales/pt-BR.js +2 -9
- package/dist/localization/localizationCatalogs.test.d.ts +1 -0
- package/dist/localization/localizationCatalogs.test.js +37 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +9 -15
- package/dist/mcp/clientIntegrations.d.ts +33 -0
- package/dist/mcp/clientIntegrations.js +199 -0
- package/dist/mcp/clientIntegrations.test.d.ts +1 -0
- package/dist/mcp/clientIntegrations.test.js +110 -0
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.js +2 -0
- package/dist/mcp/optionResolution.test.js +6 -2
- package/dist/mcp/runtime.d.ts +120 -0
- package/dist/mcp/runtime.js +5668 -0
- package/dist/mcp/runtime.test.d.ts +1 -0
- package/dist/mcp/runtime.test.js +3299 -0
- package/dist/mcp/server.js +2 -1793
- package/dist/mcp/structuredCommentValidation.test.d.ts +1 -0
- package/dist/{__tests__ → mcp}/structuredCommentValidation.test.js +1 -1
- package/dist/mcp/structuredDocValidation.js +1 -2
- package/dist/mcp/structuredDocValidation.test.d.ts +1 -0
- package/dist/{__tests__ → mcp}/structuredDocValidation.test.js +9 -4
- package/dist/mcp/toolRegistry.d.ts +570 -0
- package/dist/mcp/toolRegistry.js +90 -0
- package/dist/mcp/toolRegistry.test.d.ts +1 -0
- package/dist/mcp/toolRegistry.test.js +30 -0
- package/dist/migrations/billingSchemaParity.test.js +111 -0
- package/dist/migrations/taskSchemaMigrations.d.ts +5 -0
- package/dist/migrations/taskSchemaMigrations.js +681 -39
- package/dist/migrations/taskSchemaMigrations.test.d.ts +1 -0
- package/dist/migrations/taskSchemaMigrations.test.js +210 -0
- package/dist/plugins/vite.js +13 -7
- package/dist/public/apple-touch-icon.png +0 -0
- package/dist/public/archive/phoenix/apple-touch-icon.png +0 -0
- package/dist/public/archive/phoenix/favicon.ico +0 -0
- package/dist/public/archive/phoenix/site.webmanifest +1 -0
- package/dist/public/favicon-96x96.png +0 -0
- package/dist/public/favicon.ico +0 -0
- package/dist/public/favicon.svg +17 -0
- package/dist/public/site.webmanifest +21 -1
- package/dist/public/web-app-manifest-192x192.png +0 -0
- package/dist/public/web-app-manifest-512x512.png +0 -0
- package/dist/releaseVersion.test.d.ts +1 -0
- package/dist/{__tests__/releaseVersion.test.js → releaseVersion.test.js} +1 -1
- package/dist/resources/templates/WORKFLOW_V2_POLICY.md +1 -1
- package/dist/resources/templates/workflow-sources/workflowDocs.mjs +62 -52
- package/dist/resources/templates/workflows/collaborate.yaml +11 -5
- package/dist/resources/templates/workflows/do.yaml +8 -2
- package/dist/resources/templates/workflows/evaluate.yaml +13 -7
- package/dist/resources/templates/workflows/execute.yaml +40 -23
- package/dist/resources/templates/workflows/plan.yaml +15 -8
- package/dist/resources/templates/workflows/review.yaml +14 -8
- package/dist/runtime/appGateDefinitions.d.ts +32 -0
- package/dist/runtime/appGateDefinitions.js +32 -0
- package/dist/runtime/appGates.d.ts +29 -0
- package/dist/runtime/appGates.js +53 -0
- package/dist/runtime/appGates.test.d.ts +1 -0
- package/dist/runtime/appGates.test.js +42 -0
- package/dist/runtime/migrationFlags.d.ts +1 -0
- package/dist/runtime/migrationFlags.js +1 -0
- package/dist/server/annotatedAttachmentsRoutes.test.d.ts +1 -0
- package/dist/server/annotatedAttachmentsRoutes.test.js +473 -0
- package/dist/server/auth.js +14 -2
- package/dist/server/auth.test.js +21 -1
- package/dist/server/cors.js +11 -5
- package/dist/server/cors.test.js +6 -0
- package/dist/server/documentReviewRoutes.test.d.ts +1 -0
- package/dist/server/documentReviewRoutes.test.js +157 -0
- package/dist/server/index.cookieProxy.test.js +46 -1
- package/dist/server/index.d.ts +20 -0
- package/dist/server/index.js +168 -41
- package/dist/server/index.rateLimit.test.js +6 -0
- package/dist/server/index.test.js +133 -2
- package/dist/server/mockStripe.d.ts +72 -0
- package/dist/server/mockStripe.js +357 -0
- package/dist/server/realtimeSyncWs.js +4 -0
- package/dist/server/routes/admin.d.ts +114 -0
- package/dist/server/routes/admin.js +2226 -0
- package/dist/server/routes/annotatedAttachments.d.ts +24 -0
- package/dist/server/routes/annotatedAttachments.js +235 -0
- package/dist/server/routes/auth.d.ts +61 -0
- package/dist/server/routes/auth.js +1296 -0
- package/dist/server/{routes.billing.d.ts → routes/billing.d.ts} +3 -2
- package/dist/server/routes/billing.js +2001 -0
- package/dist/server/routes/billing.test.d.ts +1 -0
- package/dist/server/routes/billing.test.js +1631 -0
- package/dist/server/routes/documentReviews.d.ts +11 -0
- package/dist/server/routes/documentReviews.js +191 -0
- package/dist/server/routes/documents.d.ts +118 -0
- package/dist/server/routes/documents.js +1113 -0
- package/dist/server/routes/resources.d.ts +25 -0
- package/dist/server/routes/resources.js +225 -0
- package/dist/server/routes/shared.d.ts +35 -0
- package/dist/server/routes/shared.js +305 -0
- package/dist/server/routes/shared.test.d.ts +1 -0
- package/dist/server/routes/shared.test.js +32 -0
- package/dist/server/{routes.sync.d.ts → routes/sync.d.ts} +5 -5
- package/dist/server/routes/sync.integration.test.d.ts +1 -0
- package/dist/server/routes/sync.integration.test.js +2003 -0
- package/dist/server/routes/sync.js +3143 -0
- package/dist/server/routes/tasks.d.ts +72 -0
- package/dist/server/routes/tasks.js +1252 -0
- package/dist/server/routes/workspaces.d.ts +32 -0
- package/dist/server/routes/workspaces.js +307 -0
- package/dist/server/routes.d.ts +8 -9
- package/dist/server/routes.js +968 -4092
- package/dist/server/routes.test.js +9043 -2415
- package/dist/server/runtimeMode.d.ts +1 -1
- package/dist/server/runtimeMode.js +3 -18
- package/dist/server/runtimeMode.test.js +3 -3
- package/dist/server/securityHeaders.d.ts +1 -1
- package/dist/server/securityHeaders.js +65 -6
- package/dist/server/securityHeaders.test.js +40 -2
- package/dist/services/templateEngine.test.d.ts +1 -0
- package/dist/{__tests__ → services}/templateEngine.test.js +1 -1
- package/dist/services/templates.test.d.ts +1 -0
- package/dist/{__tests__ → services}/templates.test.js +1 -1
- package/dist/services/workflowDocumentFixtures.test.d.ts +1 -0
- package/dist/{__tests__ → services}/workflowDocumentFixtures.test.js +4 -4
- package/dist/services/workflowExportMatrix.test.d.ts +1 -0
- package/dist/{__tests__ → services}/workflowExportMatrix.test.js +1 -1
- package/dist/services/workflowExportSnapshots.test.d.ts +1 -0
- package/dist/services/workflowGeneration.test.d.ts +1 -0
- package/dist/services/workflowSimulation.test.d.ts +1 -0
- package/dist/services/workflowSimulation.test.js +113 -0
- package/dist/services/workflowV2Policy.test.d.ts +1 -0
- package/dist/{__tests__ → services}/workflowV2Policy.test.js +2 -2
- package/dist/shared/aiProfileSurfaceType.d.ts +9 -0
- package/dist/shared/aiProfileSurfaceType.js +49 -0
- package/dist/shared/customTaxonomyLibrary.d.ts +11 -0
- package/dist/shared/customTaxonomyLibrary.js +276 -0
- package/dist/shared/customTaxonomyLibrary.test.d.ts +1 -0
- package/dist/shared/customTaxonomyLibrary.test.js +20 -0
- package/dist/shared/taxonomyCatalog.d.ts +19 -0
- package/dist/shared/taxonomyCatalog.js +36 -0
- package/dist/shared/taxonomyDefaults.d.ts +29 -0
- package/dist/shared/taxonomyDefaults.js +26 -0
- package/dist/shared/taxonomyLibrary.d.ts +15 -0
- package/dist/shared/taxonomyLibrary.js +98 -0
- package/dist/shared/taxonomyLibrary.test.d.ts +1 -0
- package/dist/shared/taxonomyLibrary.test.js +26 -0
- package/dist/shared/taxonomyState.d.ts +36 -0
- package/dist/shared/taxonomyState.js +73 -0
- package/dist/shared/taxonomyState.test.d.ts +1 -0
- package/dist/shared/taxonomyState.test.js +71 -0
- package/dist/shared/workflowDefaults.d.ts +9 -0
- package/dist/shared/workflowDefaults.js +11 -0
- package/dist/storage/documentIntegrity.d.ts +2 -0
- package/dist/storage/documentIntegrity.js +34 -17
- package/dist/storage/documentPurge.d.ts +2 -1
- package/dist/storage/documentPurge.js +35 -8
- package/dist/storage/index.d.ts +4 -0
- package/dist/storage/index.js +2 -0
- package/dist/storage/objectStorage.d.ts +1 -1
- package/dist/storage/objectStorageClient.d.ts +6 -4
- package/dist/storage/objectStorageClient.js +77 -29
- package/dist/storage/objectStorageClient.test.d.ts +1 -0
- package/dist/storage/objectStorageClient.test.js +57 -0
- package/dist/storage/objectStorageConfig.js +3 -1
- package/dist/storage/objectStorageConfig.test.d.ts +1 -0
- package/dist/storage/objectStorageConfig.test.js +38 -0
- package/dist/storage/postgresAdapter.d.ts +5 -0
- package/dist/storage/postgresAdapter.js +2 -2
- package/dist/storage/postgresAdapter.test.d.ts +1 -0
- package/dist/storage/postgresAdapter.test.js +29 -0
- package/dist/storage/taskAssetStore.d.ts +7 -4
- package/dist/storage/taskAssetStore.js +65 -60
- package/dist/storage/workspaceAssetStore.d.ts +122 -0
- package/dist/storage/workspaceAssetStore.js +496 -0
- package/dist/storage/workspaceAssetStore.test.d.ts +1 -0
- package/dist/storage/workspaceAssetStore.test.js +501 -0
- package/dist/sync/cloudSyncApi.d.ts +33 -2
- package/dist/sync/cloudSyncApi.js +37 -15
- package/dist/sync/cloudSyncApi.test.js +60 -3
- package/dist/sync/index.d.ts +2 -0
- package/dist/sync/index.js +3 -0
- package/dist/sync/syncService.d.ts +25 -0
- package/dist/sync/syncService.js +184 -16
- package/dist/sync/syncService.test.js +289 -25
- package/dist/sync/workspaceRepair.d.ts +114 -0
- package/dist/sync/workspaceRepair.js +566 -0
- package/dist/sync/workspaceSyncModel.d.ts +230 -2
- package/dist/sync/workspaceSyncModel.js +456 -17
- package/dist/sync/workspaceSyncModel.test.js +642 -0
- package/dist/sync/workspaceSyncState.d.ts +12 -0
- package/dist/sync/workspaceSyncState.js +12 -0
- package/dist/sync/workspaceSyncSurface.d.ts +33 -0
- package/dist/sync/workspaceSyncSurface.js +74 -0
- package/dist/types.d.ts +143 -18
- package/dist/types.js +9 -24
- package/dist/ui/assets/AgentsModule-Bdq3T1Ts.js +1 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +1 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-jKUVo-PJ.js +3 -0
- package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +1 -0
- package/dist/ui/assets/DocumentWorkspace-DuQa-uIC.js +250 -0
- package/dist/ui/assets/InitiativesModule-C8Pi2VTk.js +1 -0
- package/dist/ui/assets/PlansPage-B6BDECSi.js +1 -0
- package/dist/ui/assets/PlansPage-BlVC_lRq.css +1 -0
- package/dist/ui/assets/TaskSettings-BwB9NtmJ.css +1 -0
- package/dist/ui/assets/TaskSettings-DUJgRTPU.js +8 -0
- package/dist/ui/assets/WorkflowsModule-b6TLuHwJ.js +5 -0
- package/dist/ui/assets/index-BiiF1cLQ.css +1 -0
- package/dist/ui/assets/index-xBWLPOEb.js +6 -0
- package/dist/ui/assets/vendor-dnd-DRzYolkg.js +5 -0
- package/dist/ui/assets/{vendor-icons-ZFG3SGwo.js → vendor-icons-QZyhEwgT.js} +1 -1
- package/dist/ui/assets/{vendor-markdown-CKYkQHfj.js → vendor-markdown-BUxTU7dS.js} +1 -1
- package/dist/ui/assets/vendor-react-CKJs5o3c.js +61 -0
- package/dist/ui/assets/vendor-router-BbWMxlnO.js +3 -0
- package/dist/ui/branding/logos/logo.png +0 -0
- package/dist/ui/branding/logos/logo_dark.svg +27 -0
- package/dist/ui/branding/logos/logo_favicon.svg +35 -0
- package/dist/ui/branding/logos/logo_light.svg +27 -0
- package/dist/ui/favicon/apple-touch-icon.png +0 -0
- package/dist/ui/favicon/archive/phoenix/apple-touch-icon.png +0 -0
- package/dist/ui/favicon/archive/phoenix/favicon.ico +0 -0
- package/dist/ui/favicon/archive/phoenix/site.webmanifest +1 -0
- package/dist/ui/favicon/favicon-96x96.png +0 -0
- package/dist/ui/favicon/favicon.ico +0 -0
- package/dist/ui/favicon/favicon.svg +17 -0
- package/dist/ui/favicon/site.webmanifest +21 -1
- package/dist/ui/favicon/web-app-manifest-192x192.png +0 -0
- package/dist/ui/favicon/web-app-manifest-512x512.png +0 -0
- package/dist/ui/index.html +10 -7
- package/dist/ui/product/screen_01.png +0 -0
- package/dist/ui/product/screen_02.png +0 -0
- package/dist/ui/product/screen_03.png +0 -0
- package/dist/utils/annotatedAttachments.d.ts +20 -0
- package/dist/utils/annotatedAttachments.js +74 -0
- package/dist/utils/annotatedAttachments.test.d.ts +1 -0
- package/dist/utils/annotatedAttachments.test.js +108 -0
- package/dist/utils/assignees.d.ts +26 -0
- package/dist/utils/assignees.js +132 -0
- package/dist/utils/canonicalAssetKind.d.ts +16 -0
- package/dist/utils/canonicalAssetKind.js +39 -0
- package/dist/utils/canonicalAssetKind.test.d.ts +1 -0
- package/dist/utils/canonicalAssetKind.test.js +24 -0
- package/dist/utils/coalescedRefresh.d.ts +5 -0
- package/dist/utils/coalescedRefresh.js +16 -0
- package/dist/utils/coalescedRefresh.test.d.ts +1 -0
- package/dist/utils/coalescedRefresh.test.js +30 -0
- package/dist/utils/constants.d.ts +16 -3
- package/dist/utils/constants.icons.test.js +2 -1
- package/dist/utils/constants.js +70 -13
- package/dist/utils/contextAssetEvents.d.ts +7 -0
- package/dist/utils/contextAssetEvents.js +6 -0
- package/dist/utils/contextFiles.d.ts +14 -3
- package/dist/utils/contextFiles.js +40 -8
- package/dist/utils/documentManagerApi.d.ts +6 -0
- package/dist/utils/documentManagerApi.js +19 -0
- package/dist/utils/documentNames.d.ts +24 -0
- package/dist/utils/documentNames.js +91 -0
- package/dist/utils/documentReferences.d.ts +9 -0
- package/dist/utils/documentReferences.js +11 -0
- package/dist/utils/entityReferences.d.ts +9 -0
- package/dist/utils/entityReferences.js +32 -0
- package/dist/utils/imageReferences.d.ts +9 -0
- package/dist/utils/imageReferences.js +11 -0
- package/dist/utils/planParser.d.ts +2 -2
- package/dist/utils/planParser.js +11 -11
- package/dist/utils/planParser.test.d.ts +1 -0
- package/dist/{__tests__ → utils}/planParser.test.js +22 -22
- package/dist/utils/planningReferences.d.ts +17 -0
- package/dist/utils/planningReferences.js +30 -0
- package/dist/utils/planningReferences.test.d.ts +1 -0
- package/dist/utils/planningReferences.test.js +17 -0
- package/dist/utils/taskActivity.d.ts +9 -0
- package/dist/utils/taskActivity.js +147 -0
- package/dist/utils/taskActivity.test.d.ts +1 -0
- package/dist/utils/taskActivity.test.js +34 -0
- package/dist/utils/taskDraftDefaults.d.ts +21 -0
- package/dist/utils/taskDraftDefaults.js +40 -0
- package/dist/utils/taskDraftDefaults.test.d.ts +1 -0
- package/dist/utils/taskDraftDefaults.test.js +34 -0
- package/dist/utils/taskNormalization.d.ts +25 -0
- package/dist/utils/taskNormalization.js +161 -0
- package/dist/utils/taskNormalization.test.d.ts +1 -0
- package/dist/utils/taskNormalization.test.js +158 -0
- package/dist/utils/taskReferences.d.ts +5 -0
- package/dist/utils/taskReferences.js +11 -0
- package/dist/utils/taskforceApiClient.d.ts +2 -0
- package/dist/utils/taskforceApiClient.js +101 -3
- package/dist/utils/taskforceApiClient.test.d.ts +1 -0
- package/dist/utils/taskforceApiClient.test.js +44 -0
- package/dist/utils/taxonomyFilterOptions.d.ts +2 -0
- package/dist/utils/taxonomyFilterOptions.js +17 -0
- package/dist/utils/taxonomyFormDefaults.d.ts +3 -0
- package/dist/utils/taxonomyFormDefaults.js +28 -0
- package/dist/utils/taxonomyFormDefaults.test.d.ts +1 -0
- package/dist/utils/taxonomyFormDefaults.test.js +63 -0
- package/dist/utils/taxonomyLifecycle.d.ts +3 -0
- package/dist/utils/taxonomyLifecycle.js +17 -0
- package/dist/utils/taxonomySorting.d.ts +7 -0
- package/dist/utils/taxonomySorting.js +74 -0
- package/dist/utils/theme.d.ts +15 -0
- package/dist/utils/theme.js +23 -0
- package/dist/utils/uiNotice.d.ts +7 -0
- package/dist/utils/uiNotice.js +6 -0
- package/dist/utils/uiNotice.test.d.ts +1 -0
- package/dist/utils/uiNotice.test.js +18 -0
- package/dist/utils/workspaceIdentity.d.ts +14 -0
- package/dist/utils/workspaceIdentity.js +21 -0
- package/dist/utils/workspaceIdentity.test.js +30 -1
- package/dist/utils/workspaceSyncPresentation.d.ts +20 -0
- package/dist/utils/workspaceSyncPresentation.js +21 -0
- package/dist/utils/workspaceSyncPresentation.test.d.ts +1 -0
- package/dist/utils/workspaceSyncPresentation.test.js +36 -0
- package/package.json +30 -23
- package/scripts/bootstrap-playwright-auth.mjs +155 -0
- package/scripts/check-compat-boundaries.mjs +41 -0
- package/scripts/check-env.ts +9 -2
- package/scripts/debug-login.mjs +32 -0
- package/scripts/deploy-prod.sh +41 -0
- package/scripts/deploy-staging.sh +23 -0
- package/scripts/migrate-planning-model.ts +233 -0
- package/scripts/migrate-task-data-assets.mjs +408 -0
- package/scripts/playwright-auth.sh +11 -0
- package/scripts/run-dev-proxy.sh +2 -0
- package/scripts/update-homebrew-formula.sh +2 -2
- package/scripts/with-esbuild.sh +11 -0
- package/src/resources/templates/WORKFLOW_V2_POLICY.md +1 -1
- package/src/resources/templates/workflow-sources/workflowDocs.mjs +62 -52
- package/src/resources/templates/workflows/collaborate.yaml +11 -5
- package/src/resources/templates/workflows/do.yaml +8 -2
- package/src/resources/templates/workflows/evaluate.yaml +13 -7
- package/src/resources/templates/workflows/execute.yaml +40 -23
- package/src/resources/templates/workflows/plan.yaml +15 -8
- package/src/resources/templates/workflows/review.yaml +14 -8
- package/dist/TaskforceScreenshot.d.ts +0 -11
- package/dist/TaskforceScreenshot.js +0 -113
- package/dist/compat/priorityCompat.d.ts +0 -2
- package/dist/compat/priorityCompat.js +0 -24
- package/dist/components/features/settings/TaxonomyManager 2.d.ts +0 -52
- package/dist/components/features/settings/TaxonomyManager 2.js +0 -75
- package/dist/components/task/TaskKanban 2.d.ts +0 -76
- package/dist/components/task/TaskKanban 2.js +0 -1004
- package/dist/components/task/TaskScreenshotSection.d.ts +0 -15
- package/dist/components/task/TaskScreenshotSection.js +0 -22
- package/dist/components/views/StandaloneLayout 2.d.ts +0 -8
- package/dist/components/views/StandaloneLayout 2.js +0 -1660
- package/dist/core/InitiativeTemplates.test.js +0 -50
- package/dist/core/ParentLifecycle.test.js +0 -98
- package/dist/migrations/clientMigrations.d.ts +0 -9
- package/dist/migrations/clientMigrations.js +0 -74
- package/dist/migrations/flags.d.ts +0 -6
- package/dist/migrations/flags.js +0 -8
- package/dist/migrations/taskMigrations.d.ts +0 -71
- package/dist/migrations/taskMigrations.js +0 -236
- package/dist/migrations/taskMigrations.test.js +0 -34
- package/dist/server/routes.billing.js +0 -991
- package/dist/server/routes.billing.test.js +0 -387
- package/dist/server/routes.sync.integration.test.js +0 -470
- package/dist/server/routes.sync.js +0 -1141
- package/dist/soak/syncSoakHarness.d.ts +0 -6
- package/dist/soak/syncSoakHarness.js +0 -10
- package/dist/ui/assets/index-0W5vyCpn.css +0 -1
- package/dist/ui/assets/index-MCZuNNfn.js +0 -23
- package/dist/ui/assets/logo_white_01-0Vx-iIIv.png +0 -0
- package/dist/ui/assets/vendor-dnd-BI1K77C1.js +0 -5
- package/dist/ui/assets/vendor-react-DdIDsh7f.js +0 -9
- package/dist/ui/assets/vendor-router-BtiNU7Dl.js +0 -3
- package/dist/utils/taskHierarchy.d.ts +0 -4
- package/dist/utils/taskHierarchy.js +0 -69
- package/dist/utils/taskHierarchy.test.js +0 -38
- package/dist/utils/taskParenting.d.ts +0 -3
- package/dist/utils/taskParenting.js +0 -24
- package/dist/utils/taskParenting.test.js +0 -21
- package/dist/utils/taskRelations.d.ts +0 -6
- package/dist/utils/taskRelations.js +0 -24
- package/dist/utils/taskRelations.test.js +0 -34
- package/scripts/check-compat-boundaries.js +0 -48
- /package/dist/{__tests__/planParser.test.d.ts → TaskforceCore.filter-persistence.test.d.ts} +0 -0
- /package/dist/{__tests__/releaseVersion.test.d.ts → TaskforceCore.routing.test.d.ts} +0 -0
- /package/dist/{__tests__/structuredCommentValidation.test.d.ts → annotatedAttachments/AnnotatedAttachmentSessionStore.test.d.ts} +0 -0
- /package/dist/{__tests__/structuredDocValidation.test.d.ts → annotatedAttachments/payload.test.d.ts} +0 -0
- /package/dist/{__tests__/templateEngine.test.d.ts → annotatedAttachments/types.js} +0 -0
- /package/dist/{__tests__/templates.test.d.ts → compat/workspaceSyncCompat.test.d.ts} +0 -0
- /package/dist/{__tests__/workflowDocumentFixtures.test.d.ts → components/features/AgentsModule.test.d.ts} +0 -0
- /package/dist/{__tests__/workflowExportMatrix.test.d.ts → components/features/AnnotatedAttachmentWorkspace.test.d.ts} +0 -0
- /package/dist/{__tests__/workflowExportSnapshots.test.d.ts → components/features/DocumentWorkspace.test.d.ts} +0 -0
- /package/dist/{__tests__/workflowGeneration.test.d.ts → components/features/settings/TaxonomyLibraryManager.test.d.ts} +0 -0
- /package/dist/{__tests__/workflowV2Policy.test.d.ts → components/task/TaskActionHeader.test.d.ts} +0 -0
- /package/dist/{core/InitiativeTemplates.test.d.ts → components/task/TaskKanban.planning-drop.test.d.ts} +0 -0
- /package/dist/{core/ParentLifecycle.test.d.ts → components/task/TaskStatusActions.test.d.ts} +0 -0
- /package/dist/{migrations/taskMigrations.test.d.ts → components/ui/TopNoticeLayer.test.d.ts} +0 -0
- /package/dist/{server/routes.billing.test.d.ts → components/views/AppShellHeader.test.d.ts} +0 -0
- /package/dist/{server/routes.sync.integration.test.d.ts → components/views/PlanComparisonPage.test.d.ts} +0 -0
- /package/dist/{utils/taskHierarchy.test.d.ts → components/views/chrome/WorkspaceChromeSections.test.d.ts} +0 -0
- /package/dist/{utils/taskParenting.test.d.ts → components/views/chrome/workspaceChrome.test.d.ts} +0 -0
- /package/dist/{utils/taskRelations.test.d.ts → components/views/panels/DocumentFilterToolbar.test.d.ts} +0 -0
- /package/dist/public/{android-chrome-192x192.png → archive/phoenix/android-chrome-192x192.png} +0 -0
- /package/dist/public/{android-chrome-512x512.png → archive/phoenix/android-chrome-512x512.png} +0 -0
- /package/dist/public/{favicon-16x16.png → archive/phoenix/favicon-16x16.png} +0 -0
- /package/dist/public/{favicon-32x32.png → archive/phoenix/favicon-32x32.png} +0 -0
- /package/dist/public/{favicon.png → archive/phoenix/favicon.png} +0 -0
- /package/dist/{__tests__ → services}/workflowExportSnapshots.test.js +0 -0
- /package/dist/{__tests__ → services}/workflowGeneration.test.js +0 -0
- /package/dist/ui/favicon/{android-chrome-192x192.png → archive/phoenix/android-chrome-192x192.png} +0 -0
- /package/dist/ui/favicon/{android-chrome-512x512.png → archive/phoenix/android-chrome-512x512.png} +0 -0
- /package/dist/ui/favicon/{favicon-16x16.png → archive/phoenix/favicon-16x16.png} +0 -0
- /package/dist/ui/favicon/{favicon-32x32.png → archive/phoenix/favicon-32x32.png} +0 -0
- /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'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
|
+
}
|