@taskforcehq/taskforce 0.3.305 → 0.3.307
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 +0 -2
- package/dist/Taskforce.module.css +603 -107
- package/dist/TaskforceCore.js +216 -43
- package/dist/TaskforceCore.test.js +611 -28
- package/dist/assets/fonts/CourierPrime-Bold.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-BoldItalic.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-Italic.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-Regular.woff2 +0 -0
- package/dist/assets/fonts/Noir_medium.woff2 +0 -0
- package/dist/assets/fonts/Noir_regular.woff2 +0 -0
- package/dist/assets/fonts/RussoOne-Regular.woff2 +0 -0
- package/dist/assets/fonts/SpecialElite-Regular.woff2 +0 -0
- package/dist/compat/workspaceSyncCompat.js +46 -2
- package/dist/compat/workspaceSyncCompat.test.js +38 -1
- package/dist/components/activity/EntityActivityTimeline.d.ts +34 -0
- package/dist/components/activity/EntityActivityTimeline.js +81 -0
- package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
- package/dist/components/context/ContextAttachmentManager.js +561 -0
- package/dist/components/features/AgentsModule.d.ts +14 -1
- package/dist/components/features/AgentsModule.js +366 -18
- package/dist/components/features/AgentsModule.test.js +507 -8
- package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +3 -1
- package/dist/components/features/AnnotatedAttachmentWorkspace.js +118 -4
- package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +52 -0
- package/dist/components/features/DocumentIndex.d.ts +2 -2
- package/dist/components/features/DocumentIndex.js +3 -4
- package/dist/components/features/DocumentViewer.d.ts +7 -1
- package/dist/components/features/DocumentViewer.js +62 -4
- package/dist/components/features/DocumentWorkspace.d.ts +10 -2
- package/dist/components/features/DocumentWorkspace.js +15 -4
- package/dist/components/features/DocumentWorkspace.test.js +51 -1
- package/dist/components/features/TaskSettings.d.ts +2 -0
- package/dist/components/features/TaskSettings.js +83 -78
- package/dist/components/features/TaskSettings.test.js +171 -82
- package/dist/components/features/documentWorkspaceModel.d.ts +1 -1
- package/dist/components/task/TaskActionHeader.js +11 -5
- package/dist/components/task/TaskActionHeader.test.js +10 -0
- package/dist/components/task/TaskCard.js +24 -29
- package/dist/components/task/TaskCard.test.js +38 -3
- package/dist/components/task/TaskContextUpload.js +4 -526
- package/dist/components/task/TaskContextUpload.test.js +3 -3
- package/dist/components/task/TaskForm.js +61 -116
- package/dist/components/task/TaskForm.test.js +110 -8
- package/dist/components/task/TaskKanban.d.ts +2 -1
- package/dist/components/task/TaskKanban.js +94 -21
- package/dist/components/task/TaskKanban.test.js +36 -0
- package/dist/components/task/TaskList.js +5 -1
- package/dist/components/task/TaskList.test.js +4 -0
- package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
- package/dist/components/ui/EditableAvatarButton.js +18 -0
- package/dist/components/ui/HierarchyActionBadge.d.ts +10 -0
- package/dist/components/ui/HierarchyActionBadge.js +5 -0
- package/dist/components/views/AppShellHeader.d.ts +3 -1
- package/dist/components/views/AppShellHeader.js +27 -11
- package/dist/components/views/AppShellHeader.test.js +18 -1
- package/dist/components/views/PlanComparisonPage.d.ts +9 -1
- package/dist/components/views/PlanComparisonPage.js +78 -14
- package/dist/components/views/PlanComparisonPage.test.js +257 -0
- package/dist/components/views/PlansPage.js +127 -36
- package/dist/components/views/PlansPage.test.js +294 -12
- package/dist/components/views/StandaloneLayout.js +529 -592
- package/dist/components/views/WidgetView.js +10 -8
- package/dist/components/views/panels/PlanningDrawer.d.ts +25 -5
- package/dist/components/views/panels/PlanningDrawer.js +270 -55
- package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
- package/dist/components/views/panels/PlanningDrawer.test.js +529 -0
- package/dist/components/views/planningScope.d.ts +20 -0
- package/dist/components/views/planningScope.js +25 -0
- package/dist/components/views/planningScope.test.d.ts +1 -0
- package/dist/components/views/planningScope.test.js +60 -0
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +26 -1
- package/dist/components/views/standalone/modals/AccountHubModal.js +76 -2
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +20 -3
- package/dist/components/views/standalone/modals/AvatarImageManagerModal.d.ts +20 -0
- package/dist/components/views/standalone/modals/AvatarImageManagerModal.js +127 -0
- package/dist/components/views/standalone/modals/EditProfileModal.d.ts +2 -9
- package/dist/components/views/standalone/modals/EditProfileModal.js +3 -2
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.js +6 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +1 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +24 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +2 -1
- package/dist/components/views/standalone/modals/SyncStatusModal.js +2 -2
- package/dist/config/envSchema.js +1 -0
- package/dist/core/AiProfileService.d.ts +46 -2
- package/dist/core/AiProfileService.js +1073 -82
- package/dist/core/AiProfileService.test.js +19 -1
- package/dist/core/AiProfiles.test.js +1249 -44
- package/dist/core/AttachmentLinkService.d.ts +3 -0
- package/dist/core/AttachmentLinkService.js +110 -23
- package/dist/core/AuthIdentityService.d.ts +107 -0
- package/dist/core/AuthIdentityService.js +284 -0
- package/dist/core/AuthTokenService.d.ts +4 -0
- package/dist/core/AuthTokenService.js +34 -6
- package/dist/core/AuthTokenService.test.js +3 -3
- package/dist/core/EntitlementsPolicy.test.js +2 -2
- package/dist/core/GlobalSettingsService.js +127 -6
- package/dist/core/McpTokenService.d.ts +12 -0
- package/dist/core/McpTokenService.js +44 -1
- package/dist/core/PlanEntitlementService.d.ts +23 -2
- package/dist/core/PlanEntitlementService.js +343 -57
- package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
- package/dist/core/PlanFeatureCatalog.test.js +84 -0
- package/dist/core/PlanVersionPolicy.test.js +35 -2
- package/dist/core/PlanningEntities.test.js +418 -4
- package/dist/core/SignupMonetizationSettings.test.js +199 -27
- package/dist/core/SyncReconciliationService.js +2 -65
- package/dist/core/SystemAdmin.test.js +42 -42
- package/dist/core/TaskActivityService.d.ts +28 -1
- package/dist/core/TaskActivityService.js +227 -46
- package/dist/core/TaskActivitySync.test.d.ts +1 -0
- package/dist/core/TaskActivitySync.test.js +90 -0
- package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
- package/dist/core/TaskAuditTimeline.test.js +96 -0
- package/dist/core/Taskforce.d.ts +415 -40
- package/dist/core/Taskforce.ids.test.js +58 -0
- package/dist/core/Taskforce.js +1543 -266
- package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
- package/dist/core/Taskforce.listTasksSlim.test.js +91 -0
- package/dist/core/Taskforce.mcpAuth.test.js +109 -0
- package/dist/core/UserProfileAvatarDrafts.test.js +30 -3
- package/dist/core/WorkspaceLifecycleService.js +0 -5
- package/dist/core/WorkspacePermissions.test.js +25 -1
- package/dist/core/WorkspacePlanMode.test.js +117 -16
- package/dist/core/shared.d.ts +1 -1
- package/dist/core/shared.js +5 -1
- package/dist/core/types.d.ts +75 -2
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +28 -2
- package/dist/hooks/bootstrapTimeouts.d.ts +4 -0
- package/dist/hooks/bootstrapTimeouts.js +8 -0
- package/dist/hooks/bootstrapTimeouts.test.d.ts +1 -0
- package/dist/hooks/bootstrapTimeouts.test.js +12 -0
- package/dist/hooks/sync/bootstrap.js +2 -0
- package/dist/hooks/sync/controlPlane.d.ts +37 -0
- package/dist/hooks/sync/controlPlane.js +78 -0
- package/dist/hooks/sync/controlPlane.test.d.ts +1 -0
- package/dist/hooks/sync/controlPlane.test.js +121 -0
- package/dist/hooks/sync/lifecyclePolicy.d.ts +45 -0
- package/dist/hooks/sync/lifecyclePolicy.js +93 -0
- package/dist/hooks/sync/lifecyclePolicy.test.d.ts +1 -0
- package/dist/hooks/sync/lifecyclePolicy.test.js +124 -0
- package/dist/hooks/sync/orchestratorShared.d.ts +13 -4
- package/dist/hooks/sync/orchestratorShared.js +129 -48
- package/dist/hooks/sync/orchestratorShared.test.js +50 -1
- package/dist/hooks/sync/pullLifecycle.d.ts +31 -0
- package/dist/hooks/sync/pullLifecycle.js +24 -0
- package/dist/hooks/sync/pullLifecycle.test.d.ts +1 -0
- package/dist/hooks/sync/pullLifecycle.test.js +80 -0
- package/dist/hooks/sync/recovery.d.ts +16 -2
- package/dist/hooks/sync/recovery.js +179 -53
- package/dist/hooks/sync/recovery.test.d.ts +1 -0
- package/dist/hooks/sync/recovery.test.js +292 -0
- package/dist/hooks/sync/transfers.d.ts +16 -3
- package/dist/hooks/sync/transfers.js +210 -66
- package/dist/hooks/sync/transfers.test.d.ts +1 -0
- package/dist/hooks/sync/transfers.test.js +928 -0
- package/dist/hooks/sync/useRecentSyncEvents.d.ts +21 -0
- package/dist/hooks/sync/useRecentSyncEvents.js +92 -0
- package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +1 -0
- package/dist/hooks/sync/useRecentSyncEvents.test.js +124 -0
- package/dist/hooks/sync/useSyncStatusActions.d.ts +30 -0
- package/dist/hooks/sync/useSyncStatusActions.js +88 -0
- package/dist/hooks/sync/useSyncStatusActions.test.d.ts +1 -0
- package/dist/hooks/sync/useSyncStatusActions.test.js +133 -0
- package/dist/hooks/sync/useSyncStatusControls.d.ts +25 -0
- package/dist/hooks/sync/useSyncStatusControls.js +60 -0
- package/dist/hooks/sync/useSyncStatusControls.test.d.ts +1 -0
- package/dist/hooks/sync/useSyncStatusControls.test.js +88 -0
- package/dist/hooks/useFilterSort.js +13 -13
- package/dist/hooks/useFilterSort.test.js +87 -1
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +12 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +9 -2
- package/dist/hooks/useSyncOrchestrator.js +409 -183
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +407 -0
- package/dist/hooks/useTaskData.js +8 -3
- package/dist/hooks/useTaskData.test.js +45 -0
- package/dist/hooks/useTaskforce.d.ts +145 -52
- package/dist/hooks/useTaskforce.js +592 -253
- package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
- package/dist/hooks/useTaskforce.runtime-routing.test.js +246 -0
- package/dist/hooks/useTaskforce.sync-behavior.test.js +2468 -201
- package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
- package/dist/hooks/useWorkspaceSyncController.js +13 -2
- package/dist/hooks/useWorkspaceSyncController.test.js +48 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +328 -40
- package/dist/localization/locales/en-US.d.ts +0 -1
- package/dist/localization/locales/en-US.js +0 -1
- package/dist/localization/locales/es-419.js +0 -1
- package/dist/localization/locales/pt-BR.js +0 -1
- package/dist/mcp/canonicalAssetHelpers.d.ts +16 -4
- package/dist/mcp/canonicalAssetHelpers.js +18 -10
- package/dist/mcp/canonicalAssetHelpers.test.d.ts +1 -0
- package/dist/mcp/canonicalAssetHelpers.test.js +67 -0
- package/dist/mcp/clientIntegrations.d.ts +1 -0
- package/dist/mcp/clientIntegrations.js +25 -0
- package/dist/mcp/clientIntegrations.test.js +14 -3
- package/dist/mcp/clientProfileDefaults.js +3 -3
- package/dist/mcp/collaborationRegistrar.js +7 -7
- package/dist/mcp/documentAssetRegistrar.js +116 -10
- package/dist/mcp/runtime.d.ts +3 -0
- package/dist/mcp/runtime.js +1099 -185
- package/dist/mcp/runtime.test.js +1491 -140
- package/dist/mcp/taskAttachmentTypes.d.ts +1 -1
- package/dist/mcp/taskPlanningRegistrar.js +48 -0
- package/dist/mcp/toolRegistry.d.ts +56 -0
- package/dist/mcp/toolRegistry.js +8 -0
- package/dist/mcp/toolRegistry.test.js +4 -0
- package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
- package/dist/migrations/taskSchemaMigrations.js +192 -61
- package/dist/migrations/taskSchemaMigrations.test.js +15 -0
- package/dist/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
- package/dist/resources/templates/workflows/collaborate.yaml +1 -1
- package/dist/resources/templates/workflows/evaluate.yaml +2 -2
- package/dist/resources/templates/workflows/plan.yaml +1 -1
- package/dist/resources/templates/workflows/review.yaml +2 -2
- package/dist/server/aiProfileAvatarAssets.d.ts +49 -0
- package/dist/server/aiProfileAvatarAssets.js +319 -0
- package/dist/server/aiProfileAvatarAssets.test.d.ts +1 -0
- package/dist/server/aiProfileAvatarAssets.test.js +213 -0
- package/dist/server/annotatedAttachmentsRoutes.test.js +69 -10
- package/dist/server/auth/providers/apple.d.ts +33 -0
- package/dist/server/auth/providers/apple.js +82 -0
- package/dist/server/auth/providers/appleClientSecret.d.ts +37 -0
- package/dist/server/auth/providers/appleClientSecret.js +65 -0
- package/dist/server/auth/providers/github.d.ts +26 -0
- package/dist/server/auth/providers/github.js +86 -0
- package/dist/server/auth/providers/google.d.ts +21 -0
- package/dist/server/auth/providers/google.js +56 -0
- package/dist/server/auth/providers/types.d.ts +21 -0
- package/dist/server/auth/providers/types.js +1 -0
- package/dist/server/auth.d.ts +2 -0
- package/dist/server/auth.js +6 -3
- package/dist/server/documentReviewRoutes.test.js +36 -3
- package/dist/server/index.cookieProxy.test.js +1 -0
- package/dist/server/index.d.ts +12 -0
- package/dist/server/index.js +124 -9
- package/dist/server/index.rateLimit.test.js +3 -0
- package/dist/server/index.test.js +111 -3
- package/dist/server/routes/admin.d.ts +3 -0
- package/dist/server/routes/admin.js +459 -28
- package/dist/server/routes/annotatedAttachments.js +51 -0
- package/dist/server/routes/auth.d.ts +26 -2
- package/dist/server/routes/auth.js +1170 -71
- package/dist/server/routes/authSupport.d.ts +5 -1
- package/dist/server/routes/authSupport.js +13 -2
- package/dist/server/routes/billing.js +372 -35
- package/dist/server/routes/billing.test.js +595 -12
- package/dist/server/routes/documents.d.ts +1 -0
- package/dist/server/routes/documents.js +182 -70
- package/dist/server/routes/shared.d.ts +28 -2
- package/dist/server/routes/shared.js +69 -14
- package/dist/server/routes/sync.integration.test.js +658 -39
- package/dist/server/routes/sync.js +74 -608
- package/dist/server/routes/syncAuxRoutes.d.ts +15 -0
- package/dist/server/routes/syncAuxRoutes.js +91 -0
- package/dist/server/routes/syncAuxRoutes.test.d.ts +1 -0
- package/dist/server/routes/syncAuxRoutes.test.js +158 -0
- package/dist/server/routes/syncPullApplyRoutes.d.ts +36 -0
- package/dist/server/routes/syncPullApplyRoutes.js +204 -0
- package/dist/server/routes/syncPushRoutes.d.ts +24 -0
- package/dist/server/routes/syncPushRoutes.js +212 -0
- package/dist/server/routes/syncRouteGuards.d.ts +36 -0
- package/dist/server/routes/syncRouteGuards.js +67 -0
- package/dist/server/routes/syncRouteGuards.test.d.ts +1 -0
- package/dist/server/routes/syncRouteGuards.test.js +94 -0
- package/dist/server/routes/syncRouteTypes.d.ts +13 -0
- package/dist/server/routes/syncRouteTypes.js +1 -0
- package/dist/server/routes/syncSnapshotRoutes.d.ts +66 -0
- package/dist/server/routes/syncSnapshotRoutes.js +182 -0
- package/dist/server/routes/tasks.js +55 -10
- package/dist/server/routes/workspaces.js +36 -5
- package/dist/server/routes.js +147 -11
- package/dist/server/routes.test.js +2704 -183
- package/dist/services/workflowExportSnapshots.test.js +2 -0
- package/dist/shared/aiProfileSeatScope.d.ts +5 -0
- package/dist/shared/aiProfileSeatScope.js +21 -0
- package/dist/shared/aiProfileSurfaceType.d.ts +1 -1
- package/dist/shared/aiProfileSurfaceType.js +10 -8
- package/dist/shared/passwordPolicy.d.ts +13 -0
- package/dist/shared/passwordPolicy.js +84 -0
- package/dist/shared/passwordPolicy.test.d.ts +1 -0
- package/dist/shared/passwordPolicy.test.js +34 -0
- package/dist/storage/objectStorage.d.ts +1 -0
- package/dist/storage/objectStorage.js +2 -1
- package/dist/storage/objectStorageClient.d.ts +1 -0
- package/dist/storage/objectStorageClient.js +41 -8
- package/dist/storage/objectStorageClient.test.js +26 -1
- package/dist/storage/objectStorageConfig.js +3 -1
- package/dist/storage/objectStorageConfig.test.js +13 -1
- package/dist/storage/workspaceAssetStore.d.ts +15 -3
- package/dist/storage/workspaceAssetStore.js +132 -44
- package/dist/storage/workspaceAssetStore.test.js +93 -6
- package/dist/styles/fonts.css +70 -0
- package/dist/sync/collaborationSyncPayload.d.ts +6 -1
- package/dist/sync/collaborationSyncPayload.js +19 -11
- package/dist/sync/collaborationSyncPayload.test.js +14 -1
- package/dist/sync/collaborationSyncState.d.ts +3 -3
- package/dist/sync/collaborationSyncState.js +19 -12
- package/dist/sync/contentSyncPayload.d.ts +5 -1
- package/dist/sync/contentSyncPayload.js +9 -0
- package/dist/sync/contentSyncPayload.test.js +39 -0
- package/dist/sync/contentSyncState.d.ts +9 -9
- package/dist/sync/contentSyncState.js +248 -107
- package/dist/sync/planningSyncPayload.js +5 -0
- package/dist/sync/planningSyncPayload.test.js +24 -0
- package/dist/sync/syncApplyHandlers.d.ts +11 -4
- package/dist/sync/syncApplyHandlers.js +115 -42
- package/dist/sync/syncApplyHandlers.test.js +251 -4
- package/dist/sync/syncFieldSemantics.d.ts +12 -0
- package/dist/sync/syncFieldSemantics.js +55 -0
- package/dist/sync/syncResourceAdapters.d.ts +46 -0
- package/dist/sync/syncResourceAdapters.js +79 -0
- package/dist/sync/syncService.d.ts +6 -0
- package/dist/sync/syncService.js +65 -1
- package/dist/sync/syncService.test.js +96 -3
- package/dist/sync/taskSyncChangeKey.d.ts +2 -0
- package/dist/sync/taskSyncChangeKey.js +143 -0
- package/dist/sync/taskSyncPayload.js +3 -8
- package/dist/sync/workspacePullFeed.d.ts +4 -0
- package/dist/sync/workspacePullFeed.js +132 -72
- package/dist/sync/workspacePullFeed.test.js +92 -0
- package/dist/sync/workspaceRepair.js +5 -2
- package/dist/sync/workspaceSyncModel.d.ts +73 -6
- package/dist/sync/workspaceSyncModel.js +342 -73
- package/dist/sync/workspaceSyncModel.test.js +559 -14
- package/dist/sync/workspaceSyncState.d.ts +17 -0
- package/dist/sync/workspaceSyncState.js +2 -1
- package/dist/sync/workspaceSyncSurface.js +3 -8
- package/dist/types.d.ts +50 -2
- package/dist/ui/assets/AgentsModule-2y82_3DO.js +1 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BTLZZCiQ.js +3 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BVKew-mH.css +1 -0
- package/dist/ui/assets/ContextAttachmentManager-D2epuNnG.js +3 -0
- package/dist/ui/assets/CourierPrime-Bold-BuLj_dpw.woff2 +0 -0
- package/dist/ui/assets/CourierPrime-BoldItalic-BrK2pUkZ.woff2 +0 -0
- package/dist/ui/assets/CourierPrime-Italic-DFUZK5Ey.woff2 +0 -0
- package/dist/ui/assets/CourierPrime-Regular-BsKphzVf.woff2 +0 -0
- package/dist/ui/assets/DocumentWorkspace-D4rJKXzd.css +1 -0
- package/dist/ui/assets/DocumentWorkspace-sVa-3Do6.js +252 -0
- package/dist/ui/assets/EntityActivityTimeline-DyqvvhuF.css +1 -0
- package/dist/ui/assets/EntityActivityTimeline-Zods27y_.js +1 -0
- package/dist/ui/assets/{InitiativesModule-Dhm7M8e7.js → InitiativesModule-D4XseTwW.js} +1 -1
- package/dist/ui/assets/Noir_medium-bxQwKGzB.woff2 +0 -0
- package/dist/ui/assets/Noir_regular-ojf2kxlG.woff2 +0 -0
- package/dist/ui/assets/PlansPage-D5AcbO0L.js +1 -0
- package/dist/ui/assets/PlansPage-Dvqsz5zc.css +1 -0
- package/dist/ui/assets/RussoOne-Regular-Cu_qq_qC.woff2 +0 -0
- package/dist/ui/assets/SpecialElite-Regular-Di6gSvAS.woff2 +0 -0
- package/dist/ui/assets/TaskContextUpload-uC5qx4Bv.js +1 -0
- package/dist/ui/assets/TaskSettings-CqvPAsTv.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-BcS4afRn.js → WorkflowsModule-oOA9bcdK.js} +4 -4
- package/dist/ui/assets/documentReferences-DPZuTgHh.js +1 -0
- package/dist/ui/assets/index-607WPo_X.js +4 -0
- package/dist/ui/assets/index-CYyodXsg.css +1 -0
- package/dist/ui/assets/{vendor-icons-BSUaRwF8.js → vendor-icons-B32hGuF2.js} +1 -1
- package/dist/ui/fonts/CourierPrime-Bold.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-Bold.woff2 +0 -0
- package/dist/ui/fonts/CourierPrime-BoldItalic.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-BoldItalic.woff2 +0 -0
- package/dist/ui/fonts/CourierPrime-Italic.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-Italic.woff2 +0 -0
- package/dist/ui/fonts/CourierPrime-Regular.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-Regular.woff2 +0 -0
- package/dist/ui/fonts/Noir_medium.otf +0 -0
- package/dist/ui/fonts/Noir_medium.woff2 +0 -0
- package/dist/ui/fonts/Noir_regular.otf +0 -0
- package/dist/ui/fonts/Noir_regular.woff2 +0 -0
- package/dist/ui/fonts/RussoOne-Regular.woff2 +0 -0
- package/dist/ui/fonts/SpecialElite-Regular.ttf +0 -0
- package/dist/ui/fonts/SpecialElite-Regular.woff2 +0 -0
- package/dist/ui/index.html +3 -3
- package/dist/ui/product/agent manager.png +0 -0
- package/dist/ui/product/agent_manager_02.png +0 -0
- package/dist/utils/accountProfileSummaryCache.d.ts +5 -0
- package/dist/utils/aiProfileEvents.d.ts +7 -0
- package/dist/utils/aiProfileEvents.js +6 -0
- package/dist/utils/assignees.d.ts +8 -0
- package/dist/utils/assignees.js +10 -1
- package/dist/utils/assignees.test.d.ts +1 -0
- package/dist/utils/assignees.test.js +25 -0
- package/dist/utils/avatarDisplay.d.ts +1 -0
- package/dist/utils/avatarDisplay.js +15 -0
- package/dist/utils/avatarUpload.d.ts +27 -0
- package/dist/utils/avatarUpload.js +132 -0
- package/dist/utils/avatarUpload.test.d.ts +1 -0
- package/dist/utils/avatarUpload.test.js +40 -0
- package/dist/utils/bootstrapTrace.d.ts +7 -0
- package/dist/utils/bootstrapTrace.js +29 -0
- package/dist/utils/commercialLifecyclePresentation.js +3 -0
- package/dist/utils/contextAssetEvents.d.ts +2 -0
- package/dist/utils/contextFiles.d.ts +3 -1
- package/dist/utils/contextFiles.js +2 -1
- package/dist/utils/syncEventDetails.d.ts +3 -0
- package/dist/utils/syncEventDetails.js +64 -0
- package/dist/utils/syncEventDetails.test.d.ts +1 -0
- package/dist/utils/syncEventDetails.test.js +23 -0
- package/dist/utils/syncEventPresentation.d.ts +15 -0
- package/dist/utils/syncEventPresentation.js +129 -0
- package/dist/utils/syncEventPresentation.test.d.ts +1 -0
- package/dist/utils/syncEventPresentation.test.js +64 -0
- package/dist/utils/syncStatusPresentation.d.ts +72 -0
- package/dist/utils/syncStatusPresentation.js +217 -0
- package/dist/utils/syncStatusPresentation.test.d.ts +1 -0
- package/dist/utils/syncStatusPresentation.test.js +164 -0
- package/dist/utils/taskActivity.d.ts +9 -1
- package/dist/utils/taskActivity.js +290 -22
- package/dist/utils/taskActivity.test.js +141 -8
- package/dist/utils/taskSearch.d.ts +22 -0
- package/dist/utils/taskSearch.js +87 -0
- package/dist/utils/taskSearch.test.d.ts +1 -0
- package/dist/utils/taskSearch.test.js +69 -0
- package/dist/utils/taskUtils.js +10 -3
- package/dist/utils/taskUtils.test.d.ts +1 -0
- package/dist/utils/taskUtils.test.js +19 -0
- package/dist/utils/workspaceSyncPresentation.d.ts +4 -0
- package/dist/utils/workspaceSyncPresentation.js +68 -2
- package/dist/utils/workspaceSyncPresentation.test.js +36 -1
- package/package.json +3 -2
- package/scripts/export-sqlite-to-postgres.mjs +7 -9
- package/scripts/run-e2e-realtime.sh +17 -3
- package/scripts/run-smoke.sh +17 -5
- package/scripts/run-soak.sh +17 -5
- package/src/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
- package/src/resources/templates/workflows/collaborate.yaml +1 -1
- package/src/resources/templates/workflows/evaluate.yaml +2 -2
- package/src/resources/templates/workflows/plan.yaml +1 -1
- package/src/resources/templates/workflows/review.yaml +2 -2
- package/dist/ui/assets/AgentsModule-N2MnQBZk.js +0 -1
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BG6P_GVW.js +0 -3
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +0 -1
- package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +0 -1
- package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +0 -252
- package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
- package/dist/ui/assets/PlansPage-Cq0zXj3I.js +0 -1
- package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
- package/dist/ui/assets/TaskSettings-ln0aF7xc.js +0 -9
- package/dist/ui/assets/index-C2-OXZV7.css +0 -1
- package/dist/ui/assets/index-DcSI5F86.js +0 -6
- package/scripts/migrate-planning-model.ts +0 -233
package/dist/core/Taskforce.js
CHANGED
|
@@ -7,15 +7,18 @@ import { TemplateEngine } from '../services/templateEngine.js';
|
|
|
7
7
|
import { createDatabaseAdapter } from '../storage/createDatabaseAdapter.js';
|
|
8
8
|
import { WorkspaceAssetStore } from '../storage/workspaceAssetStore.js';
|
|
9
9
|
import { AuthTokenService } from './AuthTokenService.js';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
10
|
+
import { AuthIdentityService } from './AuthIdentityService.js';
|
|
11
|
+
import { archiveAiProfileService, getAiProfileByTokenService, listAiProfilesService, mergeAiProfilesService, resolveAiProfileService, updateAiProfileAvatarService, updateAiProfileSurfaceTypeService, upsertAiProfileForSyncService, } from './AiProfileService.js';
|
|
12
|
+
import { syncCanonicalAttachmentLinksForTargetService, syncCanonicalTaskAttachmentLinksService, } from './AttachmentLinkService.js';
|
|
13
|
+
import { addEntityEventService, addTaskEventService, buildEntityActivityService, buildTaskActivityService, getEntityEventForSyncService, getTaskEventForSyncService, listEntityEventsInternalService, listWorkspaceEntityEventsForSyncService, listTaskEventsInternalService, listWorkspaceTaskEventsForSyncService, replaceEntityEventsService, replaceTaskCommentsService, replaceTaskEventsService, upsertEntityEventForSyncService, upsertTaskEventForSyncService, } from './TaskActivityService.js';
|
|
13
14
|
import { addDeletedTaskRecordService, getDeletedTaskService, listDeletedTasksService, mapDeletedTaskRowService, normalizeTaskSnapshotForRestoreService, pruneDeletedTasksService, purgeExpiredDeletedTasksService, } from './DeletedTaskLifecycleService.js';
|
|
14
15
|
import { applyWorkspaceSyncSnapshotService, deleteDocumentWatermarkService, getDocumentWatermarkService, listTasksForSyncService, listWorkspaceSyncDeletesSinceService, readPushIdempotencyService, recordSyncEventService, upsertDocumentWatermarkService, writePushIdempotencyService, } from './SyncReconciliationService.js';
|
|
15
16
|
import { getGlobalSettingsService, hasPersistedGlobalSettingsService, readGlobalConfigService, updateGlobalSettingsService, writeGlobalConfigService, } from './GlobalSettingsService.js';
|
|
16
17
|
import { getWorkspaceTaxonomyStateService, upsertWorkspaceTaxonomyStateService, } from './TaxonomySettingsService.js';
|
|
17
18
|
import { normalizeWorkspaceTaxonomyStatePayload, } from '../shared/taxonomyState.js';
|
|
18
19
|
import { parseAiProfileSurfaceType } from '../shared/aiProfileSurfaceType.js';
|
|
20
|
+
import { parseAiProfileSeatScope } from '../shared/aiProfileSeatScope.js';
|
|
21
|
+
import { validatePasswordPolicy } from '../shared/passwordPolicy.js';
|
|
19
22
|
import { normalizeTaskforceTheme } from '../utils/theme.js';
|
|
20
23
|
import { McpTokenService } from './McpTokenService.js';
|
|
21
24
|
import { PlanEntitlementService } from './PlanEntitlementService.js';
|
|
@@ -38,7 +41,7 @@ import { getDocumentReferenceLabel } from '../utils/documentReferences.js';
|
|
|
38
41
|
import { getImageReferenceLabel } from '../utils/imageReferences.js';
|
|
39
42
|
import { getCanonicalDocumentName } from '../utils/documentNames.js';
|
|
40
43
|
import { DEFAULT_TASKFORCE_THEME } from '../utils/theme.js';
|
|
41
|
-
import { ensureAssigneeTaskSchema, ensureAnnotatedAttachmentSessionsSchema, ensureBillingSchema, ensureDocumentReviewSessionsSchema, ensureDocumentReferenceSchema, ensureEntitlementsSchema,
|
|
44
|
+
import { ensureAssigneeTaskSchema, ensureAnnotatedAttachmentSessionsSchema, ensureAuthIdentitySchema, ensureBillingSchema, ensureDocumentReviewSessionsSchema, ensureDocumentReferenceSchema, ensureEntitlementsSchema, ensureScheduleTaskSchema, ensureTaskAssetsSchema, ensureTaskReferenceSchema, ensureTenantSchema, ensureWorkspaceAssetsSchema, ensureWorkspaceSchema } from '../migrations/taskSchemaMigrations.js';
|
|
42
45
|
export class TaskforceRuleError extends Error {
|
|
43
46
|
statusCode;
|
|
44
47
|
code;
|
|
@@ -54,52 +57,7 @@ export class TaskforceRuleError extends Error {
|
|
|
54
57
|
const WORKSPACE_SYNC_SETTINGS_KEY_ALLOWLIST = new Set([]);
|
|
55
58
|
const GENERIC_AI_ACTOR_COLOR = '#6B7280';
|
|
56
59
|
const GENERIC_AI_ACTOR_ICON = 'Bot';
|
|
57
|
-
const DOCUMENT_CLOUD_STORAGE_FEATURE_KEY = 'context.uploads';
|
|
58
|
-
const LEGACY_REMOVED_PLAN_FEATURE_KEYS = Object.freeze([
|
|
59
|
-
'documents.workspace_access'
|
|
60
|
-
]);
|
|
61
|
-
const WORKSPACES_FEATURE_KEY = 'workspace.switching';
|
|
62
|
-
const DOCUMENT_CLOUD_STORAGE_LIMIT_CONFIG_KEY = 'storageLimitMb';
|
|
63
|
-
const WORKSPACES_LIMIT_CONFIG_KEY = 'maxWorkspaces';
|
|
64
|
-
const MCP_ACCESS_TOKEN_MAX_ACTIVE_PER_USER_WORKSPACE = 10;
|
|
65
|
-
const MCP_ACCESS_TOKEN_DEFAULT_PAGE_SIZE = 20;
|
|
66
|
-
const MCP_ACCESS_TOKEN_MAX_PAGE_SIZE = 100;
|
|
67
|
-
const MCP_OAUTH_AUTH_CODE_TTL_MS = 10 * 60 * 1000;
|
|
68
|
-
const MCP_ACCESS_TOKEN_ALLOWED_SCOPES = ['tasks:read', 'tasks:write', 'workspace:read'];
|
|
69
60
|
const DELETED_TASK_RETENTION_MS = 90 * 24 * 60 * 60 * 1000;
|
|
70
|
-
const DEFAULT_PLAN_FEATURE_CATALOG = [
|
|
71
|
-
{
|
|
72
|
-
featureKey: 'collaboration.team_management',
|
|
73
|
-
label: 'Team Management',
|
|
74
|
-
description: 'Allow managing members, invites, roles, and collaboration settings.',
|
|
75
|
-
allowedAccessModes: ['enabled', 'disabled'],
|
|
76
|
-
configTemplate: {}
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
featureKey: DOCUMENT_CLOUD_STORAGE_FEATURE_KEY,
|
|
80
|
-
label: 'Document Cloud Storage',
|
|
81
|
-
description: 'Allow cloud document storage with an optional storage allowance in MB.',
|
|
82
|
-
allowedAccessModes: ['enabled', 'limited', 'disabled'],
|
|
83
|
-
configTemplate: {
|
|
84
|
-
[DOCUMENT_CLOUD_STORAGE_LIMIT_CONFIG_KEY]: 1024
|
|
85
|
-
}
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
featureKey: WORKSPACES_FEATURE_KEY,
|
|
89
|
-
label: 'Workspaces',
|
|
90
|
-
description: 'Allow multiple workspaces with a configurable workspace limit.',
|
|
91
|
-
allowedAccessModes: ['enabled', 'disabled'],
|
|
92
|
-
configTemplate: {
|
|
93
|
-
[WORKSPACES_LIMIT_CONFIG_KEY]: 1
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
];
|
|
97
|
-
const PLAN_FEATURE_DEPENDENCIES = Object.freeze(DEFAULT_PLAN_FEATURE_CATALOG.reduce((acc, feature) => {
|
|
98
|
-
if (Array.isArray(feature.dependsOn) && feature.dependsOn.length > 0) {
|
|
99
|
-
acc[feature.featureKey] = [...feature.dependsOn];
|
|
100
|
-
}
|
|
101
|
-
return acc;
|
|
102
|
-
}, {}));
|
|
103
61
|
function hashPassword(password) {
|
|
104
62
|
const iterations = 120000;
|
|
105
63
|
const salt = randomBytes(16).toString('hex');
|
|
@@ -121,6 +79,12 @@ function verifyPassword(password, encoded) {
|
|
|
121
79
|
return false;
|
|
122
80
|
return timingSafeEqual(actual, expected);
|
|
123
81
|
}
|
|
82
|
+
function assertPasswordPolicy(password, context) {
|
|
83
|
+
const validation = validatePasswordPolicy(password, context);
|
|
84
|
+
if (!validation.ok) {
|
|
85
|
+
throw new TaskforceRuleError(validation.message || 'Password does not meet the password policy.', 400, validation.code || 'PASSWORD_TOO_SHORT');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
124
88
|
function hashAuthToken(token) {
|
|
125
89
|
return createHash('sha256').update(token).digest('hex');
|
|
126
90
|
}
|
|
@@ -179,6 +143,7 @@ export class TaskforceCore {
|
|
|
179
143
|
db;
|
|
180
144
|
globalSettingsDb = null;
|
|
181
145
|
authTokenService;
|
|
146
|
+
identityService;
|
|
182
147
|
mcpTokenService;
|
|
183
148
|
planEntitlementService;
|
|
184
149
|
realtimeMutationListeners = new Set();
|
|
@@ -303,6 +268,15 @@ export class TaskforceCore {
|
|
|
303
268
|
const normalized = String(value || '').trim().toLowerCase();
|
|
304
269
|
return normalized === 'user' || normalized === 'human';
|
|
305
270
|
}
|
|
271
|
+
isStableActorReference(value) {
|
|
272
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
273
|
+
if (!normalized)
|
|
274
|
+
return false;
|
|
275
|
+
return normalized === 'user'
|
|
276
|
+
|| normalized === 'system'
|
|
277
|
+
|| normalized.startsWith('user-')
|
|
278
|
+
|| normalized.startsWith('ai-profile-');
|
|
279
|
+
}
|
|
306
280
|
normalizeAiProfileName(name) {
|
|
307
281
|
const normalized = String(name || '').trim();
|
|
308
282
|
if (!normalized) {
|
|
@@ -408,12 +382,24 @@ export class TaskforceCore {
|
|
|
408
382
|
username: String(row.username || '').trim() || this.normalizeAiProfileUsername(row.name, 'ai'),
|
|
409
383
|
icon: this.normalizeAiProfileIcon(row.icon),
|
|
410
384
|
color: this.normalizeAiProfileColor(row.color),
|
|
385
|
+
avatarUrl: typeof row.avatar_url === 'string' && row.avatar_url.trim().length > 0 ? row.avatar_url.trim() : null,
|
|
386
|
+
avatarSourceUrl: typeof row.avatar_source_url === 'string' && row.avatar_source_url.trim().length > 0 ? row.avatar_source_url.trim() : null,
|
|
387
|
+
avatarRevision: Number.isFinite(Number(row.avatar_revision)) ? Math.max(0, Math.floor(Number(row.avatar_revision))) : 0,
|
|
388
|
+
avatarUpdatedAt: typeof row.avatar_updated_at === 'string' && row.avatar_updated_at.trim().length > 0 ? row.avatar_updated_at.trim() : null,
|
|
411
389
|
description: typeof row.description === 'string' && row.description.trim().length > 0 ? row.description.trim() : null,
|
|
412
390
|
role: typeof row.role === 'string' && row.role.trim().length > 0 ? row.role.trim() : null,
|
|
413
391
|
provider: typeof row.provider === 'string' && row.provider.trim().length > 0 ? row.provider.trim() : null,
|
|
414
392
|
model: typeof row.model === 'string' && row.model.trim().length > 0 ? row.model.trim() : null,
|
|
415
393
|
surfaceType: parseAiProfileSurfaceType(row.surface_type),
|
|
394
|
+
seatScope: parseAiProfileSeatScope(row.seat_scope),
|
|
416
395
|
providerMetadata,
|
|
396
|
+
archivedAt: typeof row.archived_at === 'string' ? row.archived_at : null,
|
|
397
|
+
archivedBy: typeof row.archived_by === 'string' && row.archived_by.trim().length > 0 ? row.archived_by.trim() : null,
|
|
398
|
+
archivedReason: typeof row.archived_reason === 'string' && row.archived_reason.trim().length > 0 ? row.archived_reason.trim() : null,
|
|
399
|
+
mergedIntoProfileId: typeof row.merged_into_profile_id === 'string' && row.merged_into_profile_id.trim().length > 0 ? row.merged_into_profile_id.trim() : null,
|
|
400
|
+
rosterStateUpdatedAt: typeof row.roster_state_updated_at === 'string' && row.roster_state_updated_at.trim().length > 0
|
|
401
|
+
? row.roster_state_updated_at.trim()
|
|
402
|
+
: null,
|
|
417
403
|
createdAt: String(row.created_at || '').trim(),
|
|
418
404
|
updatedAt: String(row.updated_at || '').trim(),
|
|
419
405
|
lastActiveAt: typeof row.last_active_at === 'string' ? row.last_active_at : null
|
|
@@ -429,17 +415,77 @@ export class TaskforceCore {
|
|
|
429
415
|
color: profile.color,
|
|
430
416
|
isNamed: true,
|
|
431
417
|
meta: {
|
|
432
|
-
profileToken: profile.profileToken,
|
|
433
418
|
workspaceId: profile.workspaceId,
|
|
419
|
+
avatarUrl: profile.avatarUrl ?? null,
|
|
420
|
+
avatarSourceUrl: profile.avatarSourceUrl ?? null,
|
|
434
421
|
lastActiveAt: profile.lastActiveAt ?? null,
|
|
435
422
|
description: profile.description ?? null,
|
|
436
423
|
role: profile.role ?? null,
|
|
437
424
|
provider: profile.provider ?? null,
|
|
438
425
|
model: profile.model ?? null,
|
|
439
|
-
surfaceType: profile.surfaceType ?? null
|
|
426
|
+
surfaceType: profile.surfaceType ?? null,
|
|
427
|
+
seatScope: profile.seatScope ?? null,
|
|
428
|
+
archivedAt: profile.archivedAt ?? null,
|
|
429
|
+
archivedBy: profile.archivedBy ?? null,
|
|
430
|
+
archivedReason: profile.archivedReason ?? null,
|
|
431
|
+
mergedIntoProfileId: profile.mergedIntoProfileId ?? null,
|
|
432
|
+
rosterStateUpdatedAt: profile.rosterStateUpdatedAt ?? null
|
|
440
433
|
}
|
|
441
434
|
};
|
|
442
435
|
}
|
|
436
|
+
resolveCanonicalAiProfileId(rowById, profileId) {
|
|
437
|
+
let currentId = String(profileId || '').trim();
|
|
438
|
+
const visited = new Set();
|
|
439
|
+
while (currentId) {
|
|
440
|
+
if (visited.has(currentId))
|
|
441
|
+
break;
|
|
442
|
+
visited.add(currentId);
|
|
443
|
+
const row = rowById.get(currentId);
|
|
444
|
+
const mergedIntoProfileId = String(row?.merged_into_profile_id || '').trim();
|
|
445
|
+
if (!mergedIntoProfileId || mergedIntoProfileId === currentId)
|
|
446
|
+
break;
|
|
447
|
+
currentId = mergedIntoProfileId;
|
|
448
|
+
}
|
|
449
|
+
return currentId;
|
|
450
|
+
}
|
|
451
|
+
findCanonicalAiProfileIdsByAlias(workspaceId, actorRef) {
|
|
452
|
+
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
453
|
+
const rawActorRef = String(actorRef || '').trim();
|
|
454
|
+
if (!rawActorRef)
|
|
455
|
+
return [];
|
|
456
|
+
const matches = this.db.prepare(`
|
|
457
|
+
SELECT *
|
|
458
|
+
FROM ai_profiles
|
|
459
|
+
WHERE tenant_id = ?
|
|
460
|
+
AND workspace_id = ?
|
|
461
|
+
AND (
|
|
462
|
+
LOWER(name) = LOWER(?)
|
|
463
|
+
OR LOWER(username) = LOWER(?)
|
|
464
|
+
)
|
|
465
|
+
ORDER BY COALESCE(last_active_at, updated_at, created_at) DESC, id ASC
|
|
466
|
+
`).all(this.tenantId, normalizedWorkspaceId, rawActorRef, rawActorRef);
|
|
467
|
+
const rowById = new Map(matches
|
|
468
|
+
.map((row) => [String(row.id || '').trim(), row])
|
|
469
|
+
.filter(([id]) => Boolean(id)));
|
|
470
|
+
return [...new Set(matches
|
|
471
|
+
.map((row) => this.resolveCanonicalAiProfileId(rowById, String(row.id || '').trim()))
|
|
472
|
+
.filter(Boolean))];
|
|
473
|
+
}
|
|
474
|
+
resolveUniqueAiProfileActorByAlias(workspaceId, actorRef) {
|
|
475
|
+
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
476
|
+
const uniqueCanonicalIds = this.findCanonicalAiProfileIdsByAlias(normalizedWorkspaceId, actorRef);
|
|
477
|
+
if (uniqueCanonicalIds.length !== 1)
|
|
478
|
+
return null;
|
|
479
|
+
const resolvedRow = this.db.prepare(`
|
|
480
|
+
SELECT *
|
|
481
|
+
FROM ai_profiles
|
|
482
|
+
WHERE tenant_id = ?
|
|
483
|
+
AND workspace_id = ?
|
|
484
|
+
AND id = ?
|
|
485
|
+
LIMIT 1
|
|
486
|
+
`).get(this.tenantId, normalizedWorkspaceId, uniqueCanonicalIds[0]);
|
|
487
|
+
return resolvedRow ? this.aiProfileToActor(this.aiProfileRowToRecord(resolvedRow)) : null;
|
|
488
|
+
}
|
|
443
489
|
canonicalizeActorReference(workspaceId, value, fallback) {
|
|
444
490
|
const raw = String(value || '').trim();
|
|
445
491
|
if (!raw)
|
|
@@ -548,8 +594,21 @@ export class TaskforceCore {
|
|
|
548
594
|
normalized.fsPath = raw.fsPath.trim();
|
|
549
595
|
if (typeof raw.caption === 'string' && raw.caption.trim())
|
|
550
596
|
normalized.caption = raw.caption.trim();
|
|
597
|
+
if (typeof raw.displayName === 'string' && raw.displayName.trim())
|
|
598
|
+
normalized.displayName = raw.displayName.trim();
|
|
599
|
+
if (typeof raw.originalFilename === 'string' && raw.originalFilename.trim())
|
|
600
|
+
normalized.originalFilename = raw.originalFilename.trim();
|
|
551
601
|
if (typeof raw.timestamp === 'string' && raw.timestamp.trim())
|
|
552
602
|
normalized.timestamp = raw.timestamp.trim();
|
|
603
|
+
if (typeof raw.assetId === 'string' && raw.assetId.trim())
|
|
604
|
+
normalized.assetId = raw.assetId.trim();
|
|
605
|
+
const referenceNumber = this.sanitizeTaskReferenceNumber(raw.referenceNumber);
|
|
606
|
+
if (referenceNumber !== null)
|
|
607
|
+
normalized.referenceNumber = referenceNumber;
|
|
608
|
+
if (typeof raw.referenceLabel === 'string' && raw.referenceLabel.trim())
|
|
609
|
+
normalized.referenceLabel = raw.referenceLabel.trim();
|
|
610
|
+
if (typeof raw.taskId === 'string' && raw.taskId.trim())
|
|
611
|
+
normalized.taskId = raw.taskId.trim();
|
|
553
612
|
return normalized;
|
|
554
613
|
})
|
|
555
614
|
.filter((attachment) => attachment !== null);
|
|
@@ -569,15 +628,27 @@ export class TaskforceCore {
|
|
|
569
628
|
actorProfile
|
|
570
629
|
};
|
|
571
630
|
}
|
|
631
|
+
listEntityEventsInternal(entityType, entityId, workspaceId) {
|
|
632
|
+
return listEntityEventsInternalService(this.getTaskActivityServiceDeps(), entityType, entityId, workspaceId);
|
|
633
|
+
}
|
|
572
634
|
listTaskEventsInternal(taskId, workspaceId) {
|
|
573
635
|
return listTaskEventsInternalService(this.getTaskActivityServiceDeps(), taskId, workspaceId);
|
|
574
636
|
}
|
|
637
|
+
buildEntityActivity(comments, entityEvents, hiddenActions) {
|
|
638
|
+
return buildEntityActivityService(comments, entityEvents, hiddenActions?.length ? { hiddenActions } : undefined);
|
|
639
|
+
}
|
|
575
640
|
buildTaskActivity(comments, taskEvents) {
|
|
576
641
|
return buildTaskActivityService(comments, taskEvents);
|
|
577
642
|
}
|
|
643
|
+
addEntityEvent(input) {
|
|
644
|
+
addEntityEventService(this.getTaskActivityServiceDeps(), input);
|
|
645
|
+
}
|
|
578
646
|
addTaskEvent(input) {
|
|
579
647
|
addTaskEventService(this.getTaskActivityServiceDeps(), input);
|
|
580
648
|
}
|
|
649
|
+
replaceEntityEvents(entityType, entityId, workspaceId, entityEvents) {
|
|
650
|
+
replaceEntityEventsService(this.getTaskActivityServiceDeps(), entityType, entityId, workspaceId, entityEvents);
|
|
651
|
+
}
|
|
581
652
|
replaceTaskEvents(taskId, workspaceId, taskEvents) {
|
|
582
653
|
replaceTaskEventsService(this.getTaskActivityServiceDeps(), taskId, workspaceId, taskEvents);
|
|
583
654
|
}
|
|
@@ -1083,6 +1154,65 @@ export class TaskforceCore {
|
|
|
1083
1154
|
}
|
|
1084
1155
|
return (existingValue ?? null) === (nextValue ?? null);
|
|
1085
1156
|
}
|
|
1157
|
+
buildPlanningEntityEventChanges(existing, next, fields) {
|
|
1158
|
+
const changes = {};
|
|
1159
|
+
for (const field of fields) {
|
|
1160
|
+
const previousValue = existing[field] ?? null;
|
|
1161
|
+
const nextValue = next[field] ?? null;
|
|
1162
|
+
if (JSON.stringify(previousValue) === JSON.stringify(nextValue))
|
|
1163
|
+
continue;
|
|
1164
|
+
changes[field] = { from: previousValue, to: nextValue };
|
|
1165
|
+
}
|
|
1166
|
+
return changes;
|
|
1167
|
+
}
|
|
1168
|
+
getPlanningEntityEventAction(entityType, changes) {
|
|
1169
|
+
const fields = Object.keys(changes);
|
|
1170
|
+
if (fields.length === 1 && fields[0] === 'attachments') {
|
|
1171
|
+
return `${entityType}-attachments-changed`;
|
|
1172
|
+
}
|
|
1173
|
+
if (entityType === 'workstream' && fields.length === 1 && fields[0] === 'initiativeId') {
|
|
1174
|
+
return 'workstream-relationship-changed';
|
|
1175
|
+
}
|
|
1176
|
+
return `${entityType}-updated`;
|
|
1177
|
+
}
|
|
1178
|
+
addWorkstreamTaskMembershipEvents(input) {
|
|
1179
|
+
const previousWorkstreamId = this.sanitizeWorkstreamId(input.previousWorkstreamId);
|
|
1180
|
+
const nextWorkstreamId = this.sanitizeWorkstreamId(input.nextWorkstreamId);
|
|
1181
|
+
if (previousWorkstreamId === nextWorkstreamId)
|
|
1182
|
+
return;
|
|
1183
|
+
const details = {
|
|
1184
|
+
taskId: input.task.id,
|
|
1185
|
+
taskTitle: input.task.title,
|
|
1186
|
+
changes: {
|
|
1187
|
+
workstreamId: {
|
|
1188
|
+
from: previousWorkstreamId ?? null,
|
|
1189
|
+
to: nextWorkstreamId ?? null,
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
1193
|
+
if (previousWorkstreamId) {
|
|
1194
|
+
this.addEntityEvent({
|
|
1195
|
+
entityType: 'workstream',
|
|
1196
|
+
entityId: previousWorkstreamId,
|
|
1197
|
+
workspaceId: input.workspaceId,
|
|
1198
|
+
action: 'workstream-task-removed',
|
|
1199
|
+
actor: input.actor,
|
|
1200
|
+
createdAt: input.createdAt,
|
|
1201
|
+
details,
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
if (nextWorkstreamId) {
|
|
1205
|
+
this.addEntityEvent({
|
|
1206
|
+
entityType: 'workstream',
|
|
1207
|
+
entityId: nextWorkstreamId,
|
|
1208
|
+
workspaceId: input.workspaceId,
|
|
1209
|
+
action: 'workstream-task-added',
|
|
1210
|
+
actor: input.actor,
|
|
1211
|
+
createdAt: input.createdAt,
|
|
1212
|
+
details,
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1086
1216
|
assertWorkstreamLinkAllowed(workstreamId, workspaceId) {
|
|
1087
1217
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
1088
1218
|
if (!workstreamId)
|
|
@@ -1160,7 +1290,6 @@ export class TaskforceCore {
|
|
|
1160
1290
|
this.initializeSchema();
|
|
1161
1291
|
ensureTenantSchema(this.db);
|
|
1162
1292
|
ensureWorkspaceSchema(this.db);
|
|
1163
|
-
ensureParentTaskSchema(this.db);
|
|
1164
1293
|
ensureScheduleTaskSchema(this.db);
|
|
1165
1294
|
ensureAssigneeTaskSchema(this.db);
|
|
1166
1295
|
ensureTaskReferenceSchema(this.db);
|
|
@@ -1171,6 +1300,7 @@ export class TaskforceCore {
|
|
|
1171
1300
|
ensureAnnotatedAttachmentSessionsSchema(this.db);
|
|
1172
1301
|
ensureBillingSchema(this.db);
|
|
1173
1302
|
ensureEntitlementsSchema(this.db);
|
|
1303
|
+
ensureAuthIdentitySchema(this.db);
|
|
1174
1304
|
this.performMigrationIfNeeded();
|
|
1175
1305
|
this.planEntitlementService = new PlanEntitlementService(this.db, this.tenantId, {
|
|
1176
1306
|
getWorkspaceOwnerUserId: (wid) => this.getWorkspaceOwnerUserId(wid),
|
|
@@ -1182,6 +1312,8 @@ export class TaskforceCore {
|
|
|
1182
1312
|
this.planEntitlementService.backfillAccountEntitlements();
|
|
1183
1313
|
this.repairDetachedAnnotatedAttachmentSessions();
|
|
1184
1314
|
this.authTokenService = new AuthTokenService(this.db, this.tenantId);
|
|
1315
|
+
this.identityService = new AuthIdentityService(this.db, this.tenantId);
|
|
1316
|
+
this.authTokenService.setIdentityService(this.identityService);
|
|
1185
1317
|
this.mcpTokenService = new McpTokenService(this.db, this.tenantId, {
|
|
1186
1318
|
getAccountAccessStatus: (userId) => this.getAccountAccessStatus(userId),
|
|
1187
1319
|
});
|
|
@@ -1225,11 +1357,23 @@ export class TaskforceCore {
|
|
|
1225
1357
|
if (!accountId) {
|
|
1226
1358
|
throw new TaskforceRuleError('accountId is required', 400, 'ACCOUNT_ID_REQUIRED');
|
|
1227
1359
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1360
|
+
const existingEntitlement = this.hasAccountEntitlement(accountId)
|
|
1361
|
+
? this.getAccountEntitlement(accountId)
|
|
1362
|
+
: null;
|
|
1363
|
+
if (existingEntitlement) {
|
|
1364
|
+
if (options?.source !== 'invite-accept') {
|
|
1365
|
+
return existingEntitlement;
|
|
1366
|
+
}
|
|
1367
|
+
const existingAccess = this.getAccountAccessStatus(accountId);
|
|
1368
|
+
if (existingAccess.canAccessApp) {
|
|
1369
|
+
return existingAccess.entitlement || existingEntitlement;
|
|
1370
|
+
}
|
|
1230
1371
|
}
|
|
1231
1372
|
const now = options?.now || new Date().toISOString();
|
|
1232
|
-
const
|
|
1373
|
+
const inviteOutcome = options?.source === 'invite-accept'
|
|
1374
|
+
? this.planEntitlementService.resolveInviteAcceptanceDefaultEntitlement()
|
|
1375
|
+
: null;
|
|
1376
|
+
const onboardingOutcome = inviteOutcome || this.resolveRegistrationOnboardingOutcome();
|
|
1233
1377
|
this.transitionCanonicalCommercialState({
|
|
1234
1378
|
accountId,
|
|
1235
1379
|
planVersionId: onboardingOutcome.planVersionId,
|
|
@@ -1315,6 +1459,26 @@ export class TaskforceCore {
|
|
|
1315
1459
|
}
|
|
1316
1460
|
}
|
|
1317
1461
|
}
|
|
1462
|
+
supportsWorkspaceAssetLinkTargetType() {
|
|
1463
|
+
try {
|
|
1464
|
+
if (this.db.provider === 'postgres') {
|
|
1465
|
+
const row = this.db.prepare(`
|
|
1466
|
+
SELECT 1
|
|
1467
|
+
FROM information_schema.columns
|
|
1468
|
+
WHERE table_schema = current_schema()
|
|
1469
|
+
AND table_name = 'workspace_asset_links'
|
|
1470
|
+
AND column_name = 'target_type'
|
|
1471
|
+
LIMIT 1
|
|
1472
|
+
`).get();
|
|
1473
|
+
return Boolean(row);
|
|
1474
|
+
}
|
|
1475
|
+
const columns = this.db.prepare(`PRAGMA table_info(workspace_asset_links)`).all();
|
|
1476
|
+
return columns.some((column) => String(column.name || '').trim() === 'target_type');
|
|
1477
|
+
}
|
|
1478
|
+
catch {
|
|
1479
|
+
return false;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1318
1482
|
initializeSchema() {
|
|
1319
1483
|
const syncEventsIdColumnSql = this.db.provider === 'postgres'
|
|
1320
1484
|
? 'id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY'
|
|
@@ -1350,10 +1514,7 @@ export class TaskforceCore {
|
|
|
1350
1514
|
scheduled_week_key TEXT,
|
|
1351
1515
|
order_in_day INTEGER,
|
|
1352
1516
|
workstream_id TEXT,
|
|
1353
|
-
|
|
1354
|
-
child_display_order INTEGER,
|
|
1355
|
-
FOREIGN KEY(workstream_id) REFERENCES workstreams(id) ON DELETE SET NULL,
|
|
1356
|
-
FOREIGN KEY(parent_task_id) REFERENCES tasks(id) ON DELETE RESTRICT
|
|
1517
|
+
FOREIGN KEY(workstream_id) REFERENCES workstreams(id) ON DELETE SET NULL
|
|
1357
1518
|
);
|
|
1358
1519
|
|
|
1359
1520
|
CREATE TABLE IF NOT EXISTS workspace_task_reference_sequences (
|
|
@@ -1456,6 +1617,19 @@ export class TaskforceCore {
|
|
|
1456
1617
|
FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE
|
|
1457
1618
|
);
|
|
1458
1619
|
|
|
1620
|
+
CREATE TABLE IF NOT EXISTS entity_events (
|
|
1621
|
+
id TEXT PRIMARY KEY,
|
|
1622
|
+
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
1623
|
+
workspace_id TEXT NOT NULL DEFAULT 'default',
|
|
1624
|
+
entity_type TEXT NOT NULL,
|
|
1625
|
+
entity_id TEXT NOT NULL,
|
|
1626
|
+
action TEXT NOT NULL,
|
|
1627
|
+
actor TEXT NOT NULL,
|
|
1628
|
+
actor_type TEXT NOT NULL,
|
|
1629
|
+
details_json TEXT,
|
|
1630
|
+
created_at TEXT NOT NULL
|
|
1631
|
+
);
|
|
1632
|
+
|
|
1459
1633
|
CREATE TABLE IF NOT EXISTS deleted_tasks (
|
|
1460
1634
|
id TEXT PRIMARY KEY,
|
|
1461
1635
|
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
@@ -1584,6 +1758,9 @@ export class TaskforceCore {
|
|
|
1584
1758
|
token_prefix TEXT NOT NULL,
|
|
1585
1759
|
token_hash TEXT NOT NULL,
|
|
1586
1760
|
status TEXT NOT NULL DEFAULT 'active',
|
|
1761
|
+
ai_profile_id TEXT,
|
|
1762
|
+
ai_profile_token TEXT,
|
|
1763
|
+
ai_profile_name TEXT,
|
|
1587
1764
|
last_used_at TEXT,
|
|
1588
1765
|
revoked_at TEXT,
|
|
1589
1766
|
revoked_by_user_id TEXT,
|
|
@@ -1640,12 +1817,22 @@ export class TaskforceCore {
|
|
|
1640
1817
|
username TEXT NOT NULL,
|
|
1641
1818
|
icon TEXT NOT NULL,
|
|
1642
1819
|
color TEXT NOT NULL,
|
|
1820
|
+
avatar_url TEXT,
|
|
1821
|
+
avatar_source_url TEXT,
|
|
1822
|
+
avatar_revision INTEGER NOT NULL DEFAULT 0,
|
|
1823
|
+
avatar_updated_at TEXT,
|
|
1643
1824
|
description TEXT,
|
|
1644
1825
|
role TEXT,
|
|
1645
1826
|
provider TEXT,
|
|
1646
1827
|
model TEXT,
|
|
1647
1828
|
surface_type TEXT,
|
|
1829
|
+
seat_scope TEXT,
|
|
1648
1830
|
provider_metadata_json TEXT,
|
|
1831
|
+
archived_at TEXT,
|
|
1832
|
+
archived_by TEXT,
|
|
1833
|
+
archived_reason TEXT,
|
|
1834
|
+
merged_into_profile_id TEXT,
|
|
1835
|
+
roster_state_updated_at TEXT,
|
|
1649
1836
|
created_at TEXT NOT NULL,
|
|
1650
1837
|
updated_at TEXT NOT NULL,
|
|
1651
1838
|
last_active_at TEXT
|
|
@@ -1660,6 +1847,8 @@ export class TaskforceCore {
|
|
|
1660
1847
|
CREATE INDEX IF NOT EXISTS idx_admin_audit_logs_workspace_created ON admin_audit_logs(workspace_id, created_at);
|
|
1661
1848
|
CREATE INDEX IF NOT EXISTS idx_task_events_task_created ON task_events(tenant_id, workspace_id, task_id, created_at);
|
|
1662
1849
|
CREATE INDEX IF NOT EXISTS idx_task_events_workspace_created ON task_events(tenant_id, workspace_id, created_at);
|
|
1850
|
+
CREATE INDEX IF NOT EXISTS idx_entity_events_entity_created ON entity_events(tenant_id, workspace_id, entity_type, entity_id, created_at);
|
|
1851
|
+
CREATE INDEX IF NOT EXISTS idx_entity_events_workspace_created ON entity_events(tenant_id, workspace_id, created_at);
|
|
1663
1852
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_deleted_tasks_workspace_task ON deleted_tasks(tenant_id, workspace_id, task_id);
|
|
1664
1853
|
CREATE INDEX IF NOT EXISTS idx_deleted_tasks_workspace_deleted ON deleted_tasks(tenant_id, workspace_id, deleted_at);
|
|
1665
1854
|
CREATE INDEX IF NOT EXISTS idx_mcp_audit_logs_workspace_created ON mcp_audit_logs(tenant_id, workspace_id, created_at);
|
|
@@ -1764,7 +1953,27 @@ export class TaskforceCore {
|
|
|
1764
1953
|
// Best-effort schema repair for older deployments.
|
|
1765
1954
|
}
|
|
1766
1955
|
}
|
|
1956
|
+
if (this.db.provider === 'sqlite') {
|
|
1957
|
+
this.db.exec(`
|
|
1958
|
+
INSERT OR IGNORE INTO entity_events (
|
|
1959
|
+
id, tenant_id, workspace_id, entity_type, entity_id, action, actor, actor_type, details_json, created_at
|
|
1960
|
+
)
|
|
1961
|
+
SELECT id, tenant_id, workspace_id, 'task', task_id, action, actor, actor_type, details_json, created_at
|
|
1962
|
+
FROM task_events
|
|
1963
|
+
`);
|
|
1964
|
+
}
|
|
1965
|
+
else {
|
|
1966
|
+
this.db.exec(`
|
|
1967
|
+
INSERT INTO entity_events (
|
|
1968
|
+
id, tenant_id, workspace_id, entity_type, entity_id, action, actor, actor_type, details_json, created_at
|
|
1969
|
+
)
|
|
1970
|
+
SELECT id, tenant_id, workspace_id, 'task', task_id, action, actor, actor_type, details_json, created_at
|
|
1971
|
+
FROM task_events
|
|
1972
|
+
ON CONFLICT (id) DO NOTHING
|
|
1973
|
+
`);
|
|
1974
|
+
}
|
|
1767
1975
|
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_tasks_workstream ON tasks(tenant_id, workspace_id, workstream_id)`);
|
|
1976
|
+
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_tasks_assignee ON tasks(tenant_id, workspace_id, assignee, is_archived)`);
|
|
1768
1977
|
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_workstreams_initiative ON workstreams(tenant_id, workspace_id, initiative_id)`);
|
|
1769
1978
|
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_checklist_items_task_order ON checklist_items(tenant_id, workspace_id, task_id, sort_order)`);
|
|
1770
1979
|
if (this.db.provider === 'sqlite') {
|
|
@@ -1783,6 +1992,28 @@ export class TaskforceCore {
|
|
|
1783
1992
|
}
|
|
1784
1993
|
}
|
|
1785
1994
|
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_auth_tokens_workspace_purpose ON auth_tokens(workspace_id, purpose)`);
|
|
1995
|
+
if (this.db.provider === 'sqlite') {
|
|
1996
|
+
const mcpOauthGrantColumns = this.db.prepare(`PRAGMA table_info(mcp_oauth_connector_grants)`).all();
|
|
1997
|
+
if (!mcpOauthGrantColumns.some((c) => c.name === 'ai_profile_id')) {
|
|
1998
|
+
this.db.exec(`ALTER TABLE mcp_oauth_connector_grants ADD COLUMN ai_profile_id TEXT`);
|
|
1999
|
+
}
|
|
2000
|
+
if (!mcpOauthGrantColumns.some((c) => c.name === 'ai_profile_token')) {
|
|
2001
|
+
this.db.exec(`ALTER TABLE mcp_oauth_connector_grants ADD COLUMN ai_profile_token TEXT`);
|
|
2002
|
+
}
|
|
2003
|
+
if (!mcpOauthGrantColumns.some((c) => c.name === 'ai_profile_name')) {
|
|
2004
|
+
this.db.exec(`ALTER TABLE mcp_oauth_connector_grants ADD COLUMN ai_profile_name TEXT`);
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
else {
|
|
2008
|
+
try {
|
|
2009
|
+
this.db.exec(`ALTER TABLE mcp_oauth_connector_grants ADD COLUMN IF NOT EXISTS ai_profile_id TEXT`);
|
|
2010
|
+
this.db.exec(`ALTER TABLE mcp_oauth_connector_grants ADD COLUMN IF NOT EXISTS ai_profile_token TEXT`);
|
|
2011
|
+
this.db.exec(`ALTER TABLE mcp_oauth_connector_grants ADD COLUMN IF NOT EXISTS ai_profile_name TEXT`);
|
|
2012
|
+
}
|
|
2013
|
+
catch {
|
|
2014
|
+
// Best-effort schema repair for existing deployments.
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
1786
2017
|
if (this.db.provider === 'sqlite') {
|
|
1787
2018
|
const mcpAuditColumns = this.db.prepare(`PRAGMA table_info(mcp_audit_logs)`).all();
|
|
1788
2019
|
if (!mcpAuditColumns.some((c) => c.name === 'request_id')) {
|
|
@@ -1881,12 +2112,22 @@ export class TaskforceCore {
|
|
|
1881
2112
|
{ name: 'username', ddl: `ALTER TABLE ai_profiles ADD COLUMN username TEXT NOT NULL DEFAULT 'ai'` },
|
|
1882
2113
|
{ name: 'icon', ddl: `ALTER TABLE ai_profiles ADD COLUMN icon TEXT NOT NULL DEFAULT 'Bot'` },
|
|
1883
2114
|
{ name: 'color', ddl: `ALTER TABLE ai_profiles ADD COLUMN color TEXT NOT NULL DEFAULT '#64748b'` },
|
|
2115
|
+
{ name: 'avatar_url', ddl: `ALTER TABLE ai_profiles ADD COLUMN avatar_url TEXT` },
|
|
2116
|
+
{ name: 'avatar_source_url', ddl: `ALTER TABLE ai_profiles ADD COLUMN avatar_source_url TEXT` },
|
|
2117
|
+
{ name: 'avatar_revision', ddl: `ALTER TABLE ai_profiles ADD COLUMN avatar_revision INTEGER NOT NULL DEFAULT 0` },
|
|
2118
|
+
{ name: 'avatar_updated_at', ddl: `ALTER TABLE ai_profiles ADD COLUMN avatar_updated_at TEXT` },
|
|
1884
2119
|
{ name: 'description', ddl: `ALTER TABLE ai_profiles ADD COLUMN description TEXT` },
|
|
1885
2120
|
{ name: 'role', ddl: `ALTER TABLE ai_profiles ADD COLUMN role TEXT` },
|
|
1886
2121
|
{ name: 'provider', ddl: `ALTER TABLE ai_profiles ADD COLUMN provider TEXT` },
|
|
1887
2122
|
{ name: 'model', ddl: `ALTER TABLE ai_profiles ADD COLUMN model TEXT` },
|
|
1888
2123
|
{ name: 'surface_type', ddl: `ALTER TABLE ai_profiles ADD COLUMN surface_type TEXT` },
|
|
2124
|
+
{ name: 'seat_scope', ddl: `ALTER TABLE ai_profiles ADD COLUMN seat_scope TEXT` },
|
|
1889
2125
|
{ name: 'provider_metadata_json', ddl: `ALTER TABLE ai_profiles ADD COLUMN provider_metadata_json TEXT` },
|
|
2126
|
+
{ name: 'archived_at', ddl: `ALTER TABLE ai_profiles ADD COLUMN archived_at TEXT` },
|
|
2127
|
+
{ name: 'archived_by', ddl: `ALTER TABLE ai_profiles ADD COLUMN archived_by TEXT` },
|
|
2128
|
+
{ name: 'archived_reason', ddl: `ALTER TABLE ai_profiles ADD COLUMN archived_reason TEXT` },
|
|
2129
|
+
{ name: 'merged_into_profile_id', ddl: `ALTER TABLE ai_profiles ADD COLUMN merged_into_profile_id TEXT` },
|
|
2130
|
+
{ name: 'roster_state_updated_at', ddl: `ALTER TABLE ai_profiles ADD COLUMN roster_state_updated_at TEXT` },
|
|
1890
2131
|
{ name: 'created_at', ddl: `ALTER TABLE ai_profiles ADD COLUMN created_at TEXT NOT NULL DEFAULT ''` },
|
|
1891
2132
|
{ name: 'updated_at', ddl: `ALTER TABLE ai_profiles ADD COLUMN updated_at TEXT NOT NULL DEFAULT ''` },
|
|
1892
2133
|
{ name: 'last_active_at', ddl: `ALTER TABLE ai_profiles ADD COLUMN last_active_at TEXT` }
|
|
@@ -1905,12 +2146,22 @@ export class TaskforceCore {
|
|
|
1905
2146
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS username TEXT NOT NULL DEFAULT 'ai'`,
|
|
1906
2147
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS icon TEXT NOT NULL DEFAULT 'Bot'`,
|
|
1907
2148
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS color TEXT NOT NULL DEFAULT '#64748b'`,
|
|
2149
|
+
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS avatar_url TEXT`,
|
|
2150
|
+
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS avatar_source_url TEXT`,
|
|
2151
|
+
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS avatar_revision INTEGER NOT NULL DEFAULT 0`,
|
|
2152
|
+
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS avatar_updated_at TEXT`,
|
|
1908
2153
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS description TEXT`,
|
|
1909
2154
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS role TEXT`,
|
|
1910
2155
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS provider TEXT`,
|
|
1911
2156
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS model TEXT`,
|
|
1912
2157
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS surface_type TEXT`,
|
|
2158
|
+
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS seat_scope TEXT`,
|
|
1913
2159
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS provider_metadata_json TEXT`,
|
|
2160
|
+
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS archived_at TEXT`,
|
|
2161
|
+
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS archived_by TEXT`,
|
|
2162
|
+
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS archived_reason TEXT`,
|
|
2163
|
+
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS merged_into_profile_id TEXT`,
|
|
2164
|
+
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS roster_state_updated_at TEXT`,
|
|
1914
2165
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS created_at TEXT NOT NULL DEFAULT ''`,
|
|
1915
2166
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS updated_at TEXT NOT NULL DEFAULT ''`,
|
|
1916
2167
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS last_active_at TEXT`
|
|
@@ -1924,10 +2175,45 @@ export class TaskforceCore {
|
|
|
1924
2175
|
}
|
|
1925
2176
|
}
|
|
1926
2177
|
}
|
|
2178
|
+
this.db.exec(`
|
|
2179
|
+
UPDATE ai_profiles
|
|
2180
|
+
SET roster_state_updated_at = CASE
|
|
2181
|
+
WHEN archived_at IS NOT NULL AND TRIM(archived_at) <> '' THEN archived_at
|
|
2182
|
+
WHEN created_at IS NOT NULL AND TRIM(created_at) <> '' THEN created_at
|
|
2183
|
+
ELSE updated_at
|
|
2184
|
+
END
|
|
2185
|
+
WHERE roster_state_updated_at IS NULL
|
|
2186
|
+
OR TRIM(roster_state_updated_at) = ''
|
|
2187
|
+
OR (
|
|
2188
|
+
archived_at IS NOT NULL
|
|
2189
|
+
AND TRIM(archived_at) <> ''
|
|
2190
|
+
AND roster_state_updated_at < archived_at
|
|
2191
|
+
)
|
|
2192
|
+
`);
|
|
2193
|
+
this.db.exec(`
|
|
2194
|
+
UPDATE ai_profiles
|
|
2195
|
+
SET avatar_revision = CASE
|
|
2196
|
+
WHEN (
|
|
2197
|
+
(avatar_url IS NOT NULL AND TRIM(avatar_url) <> '')
|
|
2198
|
+
OR (avatar_source_url IS NOT NULL AND TRIM(avatar_source_url) <> '')
|
|
2199
|
+
) THEN 1
|
|
2200
|
+
ELSE 0
|
|
2201
|
+
END,
|
|
2202
|
+
avatar_updated_at = CASE
|
|
2203
|
+
WHEN (
|
|
2204
|
+
(avatar_url IS NOT NULL AND TRIM(avatar_url) <> '')
|
|
2205
|
+
OR (avatar_source_url IS NOT NULL AND TRIM(avatar_source_url) <> '')
|
|
2206
|
+
) THEN COALESCE(NULLIF(TRIM(avatar_updated_at), ''), NULLIF(TRIM(updated_at), ''), NULLIF(TRIM(created_at), ''))
|
|
2207
|
+
ELSE NULL
|
|
2208
|
+
END
|
|
2209
|
+
WHERE avatar_revision IS NULL
|
|
2210
|
+
OR avatar_revision = 0
|
|
2211
|
+
`);
|
|
1927
2212
|
this.migrateSystemRoleBackfillIfNeeded();
|
|
1928
2213
|
this.migrateUserBetaAccessBackfillIfNeeded();
|
|
1929
2214
|
this.migrateDuplicateAiProfilesIfNeeded();
|
|
1930
2215
|
this.migrateLegacyActorReferencesIfNeeded();
|
|
2216
|
+
this.migrateLegacyCommentActorReferencesIfNeeded();
|
|
1931
2217
|
// Ensure sync watermark index exists on existing databases (added in v0.3.52+).
|
|
1932
2218
|
this.db.exec(`
|
|
1933
2219
|
CREATE INDEX IF NOT EXISTS idx_tasks_sync_watermark ON tasks(tenant_id, workspace_id, updated_at, id);
|
|
@@ -2074,7 +2360,7 @@ export class TaskforceCore {
|
|
|
2074
2360
|
if (existing?.payload)
|
|
2075
2361
|
return;
|
|
2076
2362
|
const rows = this.db.prepare(`
|
|
2077
|
-
SELECT id, workspace_id, name, username, created_at, updated_at, last_active_at
|
|
2363
|
+
SELECT id, workspace_id, name, username, seat_scope, created_at, updated_at, last_active_at
|
|
2078
2364
|
FROM ai_profiles
|
|
2079
2365
|
WHERE tenant_id = ?
|
|
2080
2366
|
`).all(this.tenantId);
|
|
@@ -2083,7 +2369,8 @@ export class TaskforceCore {
|
|
|
2083
2369
|
const workspaceId = this.normalizeWorkspaceId(row.workspace_id);
|
|
2084
2370
|
const normalizedName = String(row.name || '').trim().toLowerCase();
|
|
2085
2371
|
const normalizedUsername = String(row.username || '').trim().toLowerCase();
|
|
2086
|
-
const
|
|
2372
|
+
const seatScope = String(row.seat_scope || 'unknown').trim().toLowerCase() || 'unknown';
|
|
2373
|
+
const dedupeKey = `${workspaceId}::${seatScope}::${normalizedName || normalizedUsername}`;
|
|
2087
2374
|
if (!normalizedName && !normalizedUsername)
|
|
2088
2375
|
continue;
|
|
2089
2376
|
const list = groups.get(dedupeKey) || [];
|
|
@@ -2131,15 +2418,19 @@ export class TaskforceCore {
|
|
|
2131
2418
|
if (existing?.payload)
|
|
2132
2419
|
return;
|
|
2133
2420
|
const rows = this.db.prepare(`
|
|
2134
|
-
SELECT id, workspace_id, name, username
|
|
2421
|
+
SELECT id, workspace_id, name, username, merged_into_profile_id
|
|
2135
2422
|
FROM ai_profiles
|
|
2136
2423
|
WHERE tenant_id = ?
|
|
2137
2424
|
`).all(this.tenantId);
|
|
2425
|
+
const rowById = new Map(rows
|
|
2426
|
+
.map((row) => [String(row.id || '').trim(), row])
|
|
2427
|
+
.filter(([id]) => Boolean(id)));
|
|
2138
2428
|
const aliasesByWorkspace = new Map();
|
|
2139
2429
|
for (const row of rows) {
|
|
2140
2430
|
const workspaceId = this.normalizeWorkspaceId(row.workspace_id);
|
|
2141
2431
|
const profileId = String(row.id || '').trim();
|
|
2142
|
-
|
|
2432
|
+
const canonicalProfileId = this.resolveCanonicalAiProfileId(rowById, profileId);
|
|
2433
|
+
if (!canonicalProfileId)
|
|
2143
2434
|
continue;
|
|
2144
2435
|
const aliases = [
|
|
2145
2436
|
String(row.name || '').trim().toLowerCase(),
|
|
@@ -2154,8 +2445,8 @@ export class TaskforceCore {
|
|
|
2154
2445
|
}
|
|
2155
2446
|
for (const alias of aliases) {
|
|
2156
2447
|
const ids = aliasMap.get(alias) || [];
|
|
2157
|
-
if (!ids.includes(
|
|
2158
|
-
ids.push(
|
|
2448
|
+
if (!ids.includes(canonicalProfileId))
|
|
2449
|
+
ids.push(canonicalProfileId);
|
|
2159
2450
|
aliasMap.set(alias, ids);
|
|
2160
2451
|
}
|
|
2161
2452
|
}
|
|
@@ -2196,6 +2487,135 @@ export class TaskforceCore {
|
|
|
2196
2487
|
updated_at = excluded.updated_at
|
|
2197
2488
|
`).run(this.tenantId, migrationKey, JSON.stringify({ completedAt: now }), now);
|
|
2198
2489
|
}
|
|
2490
|
+
migrateLegacyCommentActorReferencesIfNeeded() {
|
|
2491
|
+
const migrationKey = 'migration.actor_refs.canonicalize_comments.v2';
|
|
2492
|
+
const existing = this.db.prepare(`
|
|
2493
|
+
SELECT payload
|
|
2494
|
+
FROM system_settings
|
|
2495
|
+
WHERE tenant_id = ? AND settings_key = ?
|
|
2496
|
+
LIMIT 1
|
|
2497
|
+
`).get(this.tenantId, migrationKey);
|
|
2498
|
+
if (existing?.payload)
|
|
2499
|
+
return;
|
|
2500
|
+
const commentRows = this.db.prepare(`
|
|
2501
|
+
SELECT id, task_id, workspace_id, author, submitted_author
|
|
2502
|
+
FROM comments
|
|
2503
|
+
WHERE tenant_id = ?
|
|
2504
|
+
`).all(this.tenantId);
|
|
2505
|
+
const updateCommentAuthor = this.db.prepare(`
|
|
2506
|
+
UPDATE comments
|
|
2507
|
+
SET author = ?
|
|
2508
|
+
WHERE tenant_id = ? AND workspace_id = ? AND id = ?
|
|
2509
|
+
`);
|
|
2510
|
+
const updateTaskSyncWatermark = this.db.prepare(`
|
|
2511
|
+
UPDATE tasks
|
|
2512
|
+
SET updated_at = ?
|
|
2513
|
+
WHERE tenant_id = ? AND workspace_id = ? AND id = ?
|
|
2514
|
+
`);
|
|
2515
|
+
const selectTaskSyncWatermark = this.db.prepare(`
|
|
2516
|
+
SELECT updated_at, created_at
|
|
2517
|
+
FROM tasks
|
|
2518
|
+
WHERE tenant_id = ? AND workspace_id = ? AND id = ?
|
|
2519
|
+
LIMIT 1
|
|
2520
|
+
`);
|
|
2521
|
+
let repairedCount = 0;
|
|
2522
|
+
let ambiguousCount = 0;
|
|
2523
|
+
let skippedCount = 0;
|
|
2524
|
+
const touchedTaskIdsByWorkspace = new Map();
|
|
2525
|
+
const now = new Date().toISOString();
|
|
2526
|
+
const nowMs = Date.parse(now);
|
|
2527
|
+
const tx = this.db.transaction(() => {
|
|
2528
|
+
for (const row of commentRows) {
|
|
2529
|
+
const workspaceId = this.normalizeWorkspaceId(row.workspace_id);
|
|
2530
|
+
const commentId = String(row.id || '').trim();
|
|
2531
|
+
const taskId = String(row.task_id || '').trim();
|
|
2532
|
+
const rawAuthor = String(row.author || '').trim();
|
|
2533
|
+
const rawSubmittedAuthor = String(row.submitted_author || '').trim();
|
|
2534
|
+
const normalizedAuthor = rawAuthor.toLowerCase();
|
|
2535
|
+
if (!commentId) {
|
|
2536
|
+
skippedCount += 1;
|
|
2537
|
+
continue;
|
|
2538
|
+
}
|
|
2539
|
+
if (this.isStableActorReference(rawAuthor)
|
|
2540
|
+
|| this.isHumanActorReference(rawAuthor)
|
|
2541
|
+
|| normalizedAuthor === 'system') {
|
|
2542
|
+
skippedCount += 1;
|
|
2543
|
+
continue;
|
|
2544
|
+
}
|
|
2545
|
+
const candidateIds = new Set();
|
|
2546
|
+
let hasAmbiguousEvidence = false;
|
|
2547
|
+
const evidenceValues = [rawAuthor, rawSubmittedAuthor];
|
|
2548
|
+
for (const value of evidenceValues) {
|
|
2549
|
+
const rawValue = String(value || '').trim();
|
|
2550
|
+
const normalizedValue = rawValue.toLowerCase();
|
|
2551
|
+
if (!rawValue)
|
|
2552
|
+
continue;
|
|
2553
|
+
if (this.isHumanActorReference(rawValue)
|
|
2554
|
+
|| this.isGenericAiActorReference(rawValue)
|
|
2555
|
+
|| normalizedValue === 'system'
|
|
2556
|
+
|| normalizedValue.startsWith('user-')) {
|
|
2557
|
+
continue;
|
|
2558
|
+
}
|
|
2559
|
+
if (normalizedValue.startsWith('ai-profile-')) {
|
|
2560
|
+
const actor = this.resolveWorkspaceActor(workspaceId, rawValue);
|
|
2561
|
+
if (actor?.kind === 'ai' && actor.meta?.unresolved !== true) {
|
|
2562
|
+
candidateIds.add(actor.id);
|
|
2563
|
+
}
|
|
2564
|
+
continue;
|
|
2565
|
+
}
|
|
2566
|
+
const matches = this.findCanonicalAiProfileIdsByAlias(workspaceId, rawValue);
|
|
2567
|
+
if (matches.length === 1) {
|
|
2568
|
+
candidateIds.add(matches[0]);
|
|
2569
|
+
}
|
|
2570
|
+
else if (matches.length > 1) {
|
|
2571
|
+
hasAmbiguousEvidence = true;
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
if (!hasAmbiguousEvidence && candidateIds.size === 1) {
|
|
2575
|
+
updateCommentAuthor.run([...candidateIds][0], this.tenantId, workspaceId, commentId);
|
|
2576
|
+
if (taskId) {
|
|
2577
|
+
let touchedTaskIds = touchedTaskIdsByWorkspace.get(workspaceId);
|
|
2578
|
+
if (!touchedTaskIds) {
|
|
2579
|
+
touchedTaskIds = new Set();
|
|
2580
|
+
touchedTaskIdsByWorkspace.set(workspaceId, touchedTaskIds);
|
|
2581
|
+
}
|
|
2582
|
+
touchedTaskIds.add(taskId);
|
|
2583
|
+
}
|
|
2584
|
+
repairedCount += 1;
|
|
2585
|
+
continue;
|
|
2586
|
+
}
|
|
2587
|
+
if (hasAmbiguousEvidence || candidateIds.size > 1) {
|
|
2588
|
+
ambiguousCount += 1;
|
|
2589
|
+
continue;
|
|
2590
|
+
}
|
|
2591
|
+
skippedCount += 1;
|
|
2592
|
+
}
|
|
2593
|
+
for (const [workspaceId, touchedTaskIds] of touchedTaskIdsByWorkspace.entries()) {
|
|
2594
|
+
for (const taskId of touchedTaskIds) {
|
|
2595
|
+
const taskRow = selectTaskSyncWatermark.get(this.tenantId, workspaceId, taskId);
|
|
2596
|
+
const existingWatermark = String(taskRow?.updated_at || taskRow?.created_at || '').trim();
|
|
2597
|
+
const existingWatermarkMs = Date.parse(existingWatermark);
|
|
2598
|
+
const nextWatermarkMs = Number.isFinite(existingWatermarkMs)
|
|
2599
|
+
? Math.max(existingWatermarkMs, nowMs) + 1
|
|
2600
|
+
: nowMs;
|
|
2601
|
+
updateTaskSyncWatermark.run(new Date(nextWatermarkMs).toISOString(), this.tenantId, workspaceId, taskId);
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
});
|
|
2605
|
+
tx();
|
|
2606
|
+
this.db.prepare(`
|
|
2607
|
+
INSERT INTO system_settings (tenant_id, settings_key, payload, updated_at)
|
|
2608
|
+
VALUES (?, ?, ?, ?)
|
|
2609
|
+
ON CONFLICT (tenant_id, settings_key) DO UPDATE SET
|
|
2610
|
+
payload = excluded.payload,
|
|
2611
|
+
updated_at = excluded.updated_at
|
|
2612
|
+
`).run(this.tenantId, migrationKey, JSON.stringify({
|
|
2613
|
+
completedAt: now,
|
|
2614
|
+
repairedCount,
|
|
2615
|
+
ambiguousCount,
|
|
2616
|
+
skippedCount,
|
|
2617
|
+
}), now);
|
|
2618
|
+
}
|
|
2199
2619
|
getWorkspaceOwnerUserId(workspaceId) {
|
|
2200
2620
|
const normalizedWorkspaceId = String(workspaceId || '').trim();
|
|
2201
2621
|
if (!normalizedWorkspaceId)
|
|
@@ -2668,6 +3088,9 @@ export class TaskforceCore {
|
|
|
2668
3088
|
getOnboardingPolicy() {
|
|
2669
3089
|
return this.planEntitlementService.getOnboardingPolicy();
|
|
2670
3090
|
}
|
|
3091
|
+
listOnboardingPlanOptions() {
|
|
3092
|
+
return this.planEntitlementService.listOnboardingPlanOptions();
|
|
3093
|
+
}
|
|
2671
3094
|
validateOnboardingPolicy(input) {
|
|
2672
3095
|
return this.planEntitlementService.validateOnboardingPolicy(input);
|
|
2673
3096
|
}
|
|
@@ -2709,31 +3132,58 @@ export class TaskforceCore {
|
|
|
2709
3132
|
listCanonicalTaskAttachments(taskId, workspaceId) {
|
|
2710
3133
|
try {
|
|
2711
3134
|
const workspaceAssetStore = new WorkspaceAssetStore(this.db, this.tenantId);
|
|
2712
|
-
const rows = this.
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
3135
|
+
const rows = (this.supportsWorkspaceAssetLinkTargetType()
|
|
3136
|
+
? this.db.prepare(`
|
|
3137
|
+
SELECT
|
|
3138
|
+
a.asset_id,
|
|
3139
|
+
a.kind,
|
|
3140
|
+
a.reference_number,
|
|
3141
|
+
a.storage_provider,
|
|
3142
|
+
a.storage_key,
|
|
3143
|
+
a.logical_name,
|
|
3144
|
+
a.original_filename,
|
|
3145
|
+
a.updated_at,
|
|
3146
|
+
a.created_at,
|
|
3147
|
+
l.role,
|
|
3148
|
+
l.display_name
|
|
3149
|
+
FROM workspace_asset_links l
|
|
3150
|
+
JOIN workspace_assets a
|
|
3151
|
+
ON a.tenant_id = l.tenant_id
|
|
3152
|
+
AND a.workspace_id = l.workspace_id
|
|
3153
|
+
AND a.asset_id = l.asset_id
|
|
3154
|
+
WHERE l.tenant_id = ?
|
|
3155
|
+
AND l.workspace_id = ?
|
|
3156
|
+
AND l.target_type = 'task'
|
|
3157
|
+
AND l.task_id = ?
|
|
3158
|
+
AND l.deleted_at IS NULL
|
|
3159
|
+
AND a.deleted_at IS NULL
|
|
3160
|
+
ORDER BY COALESCE(l.sort_order, 2147483647) ASC, a.updated_at DESC, a.asset_id ASC
|
|
3161
|
+
`).all(this.tenantId, workspaceId, taskId)
|
|
3162
|
+
: this.db.prepare(`
|
|
3163
|
+
SELECT
|
|
3164
|
+
a.asset_id,
|
|
3165
|
+
a.kind,
|
|
3166
|
+
a.reference_number,
|
|
3167
|
+
a.storage_provider,
|
|
3168
|
+
a.storage_key,
|
|
3169
|
+
a.logical_name,
|
|
3170
|
+
a.original_filename,
|
|
3171
|
+
a.updated_at,
|
|
3172
|
+
a.created_at,
|
|
3173
|
+
l.role,
|
|
3174
|
+
l.display_name
|
|
3175
|
+
FROM workspace_asset_links l
|
|
3176
|
+
JOIN workspace_assets a
|
|
3177
|
+
ON a.tenant_id = l.tenant_id
|
|
3178
|
+
AND a.workspace_id = l.workspace_id
|
|
3179
|
+
AND a.asset_id = l.asset_id
|
|
3180
|
+
WHERE l.tenant_id = ?
|
|
3181
|
+
AND l.workspace_id = ?
|
|
3182
|
+
AND l.task_id = ?
|
|
3183
|
+
AND l.deleted_at IS NULL
|
|
3184
|
+
AND a.deleted_at IS NULL
|
|
3185
|
+
ORDER BY COALESCE(l.sort_order, 2147483647) ASC, a.updated_at DESC, a.asset_id ASC
|
|
3186
|
+
`).all(this.tenantId, workspaceId, taskId));
|
|
2737
3187
|
const attachments = rows.map((row) => {
|
|
2738
3188
|
const storageKey = String(row.storage_key || '').trim();
|
|
2739
3189
|
const storageProvider = String(row.storage_provider || 'local').trim();
|
|
@@ -2841,6 +3291,9 @@ export class TaskforceCore {
|
|
|
2841
3291
|
syncCanonicalTaskAttachmentLinks(taskId, workspaceId, attachments) {
|
|
2842
3292
|
syncCanonicalTaskAttachmentLinksService(this.getAttachmentLinkServiceDeps(), taskId, workspaceId, attachments);
|
|
2843
3293
|
}
|
|
3294
|
+
syncCanonicalPlanningAttachmentLinks(targetType, targetId, workspaceId, attachments) {
|
|
3295
|
+
syncCanonicalAttachmentLinksForTargetService(this.getAttachmentLinkServiceDeps(), targetType, targetId, workspaceId, attachments);
|
|
3296
|
+
}
|
|
2844
3297
|
rowToTask(row) {
|
|
2845
3298
|
const workspaceId = this.normalizeWorkspaceId(row?.workspace_id);
|
|
2846
3299
|
const commentsRaw = this.db.prepare('SELECT * FROM comments WHERE tenant_id = ? AND workspace_id = ? AND task_id = ? ORDER BY timestamp ASC').all(this.tenantId, workspaceId, row.id);
|
|
@@ -2899,12 +3352,21 @@ export class TaskforceCore {
|
|
|
2899
3352
|
checklistItems
|
|
2900
3353
|
};
|
|
2901
3354
|
}
|
|
2902
|
-
rowToInitiative(row) {
|
|
3355
|
+
rowToInitiative(row, options) {
|
|
3356
|
+
const id = String(row.id || '').trim();
|
|
3357
|
+
const workspaceId = this.normalizeWorkspaceId(typeof row.workspace_id === 'string' ? row.workspace_id : undefined);
|
|
2903
3358
|
const referenceNumber = this.sanitizeTaskReferenceNumber(row.reference_number);
|
|
2904
3359
|
const comments = this.normalizeIncomingComments(this.parseJsonField(row.comments_json, []));
|
|
3360
|
+
const includeActivity = options?.includeActivity === true;
|
|
3361
|
+
const entityEvents = includeActivity
|
|
3362
|
+
? this.listEntityEventsInternal('initiative', id, workspaceId)
|
|
3363
|
+
: undefined;
|
|
3364
|
+
const activity = includeActivity
|
|
3365
|
+
? this.buildEntityActivity(comments, entityEvents || [])
|
|
3366
|
+
: undefined;
|
|
2905
3367
|
const attachments = this.normalizePlanningAttachments(this.parseJsonField(row.attachments_json, []));
|
|
2906
3368
|
return {
|
|
2907
|
-
id
|
|
3369
|
+
id,
|
|
2908
3370
|
referenceNumber,
|
|
2909
3371
|
referenceLabel: formatInitiativeReference(referenceNumber),
|
|
2910
3372
|
title: String(row.title || '').trim() || 'Untitled Initiative',
|
|
@@ -2916,14 +3378,27 @@ export class TaskforceCore {
|
|
|
2916
3378
|
isArchived: Boolean(row.is_archived),
|
|
2917
3379
|
comments,
|
|
2918
3380
|
attachments,
|
|
3381
|
+
...(includeActivity ? {
|
|
3382
|
+
activity,
|
|
3383
|
+
entityEvents,
|
|
3384
|
+
} : {}),
|
|
2919
3385
|
};
|
|
2920
3386
|
}
|
|
2921
|
-
rowToWorkstream(row) {
|
|
3387
|
+
rowToWorkstream(row, options) {
|
|
3388
|
+
const id = String(row.id || '').trim();
|
|
3389
|
+
const workspaceId = this.normalizeWorkspaceId(typeof row.workspace_id === 'string' ? row.workspace_id : undefined);
|
|
2922
3390
|
const referenceNumber = this.sanitizeTaskReferenceNumber(row.reference_number);
|
|
2923
3391
|
const comments = this.normalizeIncomingComments(this.parseJsonField(row.comments_json, []));
|
|
3392
|
+
const includeActivity = options?.includeActivity === true;
|
|
3393
|
+
const entityEvents = includeActivity
|
|
3394
|
+
? this.listEntityEventsInternal('workstream', id, workspaceId)
|
|
3395
|
+
: undefined;
|
|
3396
|
+
const activity = includeActivity
|
|
3397
|
+
? this.buildEntityActivity(comments, entityEvents || [])
|
|
3398
|
+
: undefined;
|
|
2924
3399
|
const attachments = this.normalizePlanningAttachments(this.parseJsonField(row.attachments_json, []));
|
|
2925
3400
|
return {
|
|
2926
|
-
id
|
|
3401
|
+
id,
|
|
2927
3402
|
referenceNumber,
|
|
2928
3403
|
referenceLabel: formatWorkstreamReference(referenceNumber),
|
|
2929
3404
|
initiativeId: typeof row.initiative_id === 'string' && row.initiative_id.trim().length > 0 ? row.initiative_id.trim() : null,
|
|
@@ -2936,6 +3411,10 @@ export class TaskforceCore {
|
|
|
2936
3411
|
isArchived: Boolean(row.is_archived),
|
|
2937
3412
|
comments,
|
|
2938
3413
|
attachments,
|
|
3414
|
+
...(includeActivity ? {
|
|
3415
|
+
activity,
|
|
3416
|
+
entityEvents,
|
|
3417
|
+
} : {}),
|
|
2939
3418
|
};
|
|
2940
3419
|
}
|
|
2941
3420
|
rowToChecklistItem(row) {
|
|
@@ -2988,7 +3467,7 @@ export class TaskforceCore {
|
|
|
2988
3467
|
`).get(this.tenantId, normalizedWorkspaceId, normalizedId);
|
|
2989
3468
|
if (!row)
|
|
2990
3469
|
return null;
|
|
2991
|
-
return this.rowToInitiative(row);
|
|
3470
|
+
return this.rowToInitiative(row, { includeActivity: true });
|
|
2992
3471
|
}
|
|
2993
3472
|
listInitiatives(workspaceId) {
|
|
2994
3473
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
@@ -3000,7 +3479,45 @@ export class TaskforceCore {
|
|
|
3000
3479
|
`).all(this.tenantId, normalizedWorkspaceId);
|
|
3001
3480
|
return rows.map((row) => this.rowToInitiative(row));
|
|
3002
3481
|
}
|
|
3003
|
-
|
|
3482
|
+
isTransientPlanningHydrationError(error) {
|
|
3483
|
+
const message = error instanceof Error ? String(error.message || '').trim() : String(error || '').trim();
|
|
3484
|
+
if (!message)
|
|
3485
|
+
return false;
|
|
3486
|
+
return /(timed out|timeout|ETIMEDOUT|query canceled|statement timeout|database timeout|postgres worker)/i.test(message);
|
|
3487
|
+
}
|
|
3488
|
+
getInitiativeForWriteResult(id, workspaceId, fallback, context) {
|
|
3489
|
+
try {
|
|
3490
|
+
return this.getInitiative(id, workspaceId) || fallback;
|
|
3491
|
+
}
|
|
3492
|
+
catch (error) {
|
|
3493
|
+
if (!this.isTransientPlanningHydrationError(error))
|
|
3494
|
+
throw error;
|
|
3495
|
+
console.warn('[Taskforce] Falling back to post-write initiative snapshot after hydration failed', {
|
|
3496
|
+
context,
|
|
3497
|
+
initiativeId: id,
|
|
3498
|
+
workspaceId,
|
|
3499
|
+
message: error instanceof Error ? error.message : String(error),
|
|
3500
|
+
});
|
|
3501
|
+
return fallback;
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
getWorkstreamForWriteResult(id, workspaceId, fallback, context) {
|
|
3505
|
+
try {
|
|
3506
|
+
return this.getWorkstream(id, workspaceId) || fallback;
|
|
3507
|
+
}
|
|
3508
|
+
catch (error) {
|
|
3509
|
+
if (!this.isTransientPlanningHydrationError(error))
|
|
3510
|
+
throw error;
|
|
3511
|
+
console.warn('[Taskforce] Falling back to post-write workstream snapshot after hydration failed', {
|
|
3512
|
+
context,
|
|
3513
|
+
workstreamId: id,
|
|
3514
|
+
workspaceId,
|
|
3515
|
+
message: error instanceof Error ? error.message : String(error),
|
|
3516
|
+
});
|
|
3517
|
+
return fallback;
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
createInitiative(input, workspaceId, options) {
|
|
3004
3521
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
3005
3522
|
const id = String(input.id || '').trim() || this.generateEntityId('initiative');
|
|
3006
3523
|
const referenceNumber = this.reserveInitiativeReferenceNumber(normalizedWorkspaceId, input.referenceNumber);
|
|
@@ -3039,18 +3556,37 @@ export class TaskforceCore {
|
|
|
3039
3556
|
sortOrder: initiative.order,
|
|
3040
3557
|
isArchived: initiative.isArchived ? 1 : 0,
|
|
3041
3558
|
});
|
|
3042
|
-
|
|
3559
|
+
this.syncCanonicalPlanningAttachmentLinks('initiative', initiative.id, normalizedWorkspaceId, initiative.attachments || []);
|
|
3560
|
+
if (options?.skipEntityEvents !== true) {
|
|
3561
|
+
this.addEntityEvent({
|
|
3562
|
+
entityType: 'initiative',
|
|
3563
|
+
entityId: initiative.id,
|
|
3564
|
+
workspaceId: normalizedWorkspaceId,
|
|
3565
|
+
action: 'initiative-created',
|
|
3566
|
+
actor: options?.actorRef || 'system',
|
|
3567
|
+
createdAt: initiative.updatedAt || initiative.createdAt,
|
|
3568
|
+
details: {
|
|
3569
|
+
title: initiative.title,
|
|
3570
|
+
ownerId: initiative.ownerId || null,
|
|
3571
|
+
}
|
|
3572
|
+
});
|
|
3573
|
+
}
|
|
3574
|
+
return this.getInitiativeForWriteResult(initiative.id, normalizedWorkspaceId, initiative, 'createInitiative:post-write');
|
|
3043
3575
|
}
|
|
3044
|
-
updateInitiative(id, patch, workspaceId) {
|
|
3576
|
+
updateInitiative(id, patch, workspaceId, options) {
|
|
3045
3577
|
const existing = this.getInitiative(id, workspaceId);
|
|
3046
3578
|
if (!existing) {
|
|
3047
3579
|
throw new TaskforceRuleError(`Initiative not found: ${id}`, 404, 'INITIATIVE_NOT_FOUND');
|
|
3048
3580
|
}
|
|
3049
3581
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
3582
|
+
const nextAttachments = Object.prototype.hasOwnProperty.call(patch, 'attachments') && Array.isArray(patch.attachments)
|
|
3583
|
+
? patch.attachments
|
|
3584
|
+
: existing.attachments;
|
|
3050
3585
|
const next = {
|
|
3051
3586
|
...existing,
|
|
3052
3587
|
...patch,
|
|
3053
3588
|
id: existing.id,
|
|
3589
|
+
attachments: nextAttachments,
|
|
3054
3590
|
updatedAt: String(patch.updatedAt || '').trim() || new Date().toISOString(),
|
|
3055
3591
|
referenceLabel: formatInitiativeReference(patch.referenceNumber ?? existing.referenceNumber),
|
|
3056
3592
|
};
|
|
@@ -3080,25 +3616,68 @@ export class TaskforceCore {
|
|
|
3080
3616
|
sortOrder: next.order,
|
|
3081
3617
|
isArchived: next.isArchived ? 1 : 0,
|
|
3082
3618
|
});
|
|
3083
|
-
|
|
3619
|
+
this.syncCanonicalPlanningAttachmentLinks('initiative', existing.id, normalizedWorkspaceId, next.attachments || []);
|
|
3620
|
+
const hydrated = this.getInitiativeForWriteResult(existing.id, normalizedWorkspaceId, next, 'updateInitiative:post-write');
|
|
3621
|
+
if (options?.skipEntityEvents !== true) {
|
|
3622
|
+
const changes = this.buildPlanningEntityEventChanges(existing, next, ['title', 'description', 'ownerId', 'attachments']);
|
|
3623
|
+
if (Object.keys(changes).length > 0) {
|
|
3624
|
+
this.addEntityEvent({
|
|
3625
|
+
entityType: 'initiative',
|
|
3626
|
+
entityId: existing.id,
|
|
3627
|
+
workspaceId: normalizedWorkspaceId,
|
|
3628
|
+
action: this.getPlanningEntityEventAction('initiative', changes),
|
|
3629
|
+
actor: options?.actorRef || 'system',
|
|
3630
|
+
createdAt: next.updatedAt,
|
|
3631
|
+
details: { changes }
|
|
3632
|
+
});
|
|
3633
|
+
return this.getInitiativeForWriteResult(existing.id, normalizedWorkspaceId, hydrated, 'updateInitiative:post-event');
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3636
|
+
return hydrated;
|
|
3084
3637
|
}
|
|
3085
|
-
archiveInitiative(id, workspaceId) {
|
|
3638
|
+
archiveInitiative(id, workspaceId, options) {
|
|
3086
3639
|
const existing = this.getInitiative(id, workspaceId);
|
|
3087
3640
|
if (!existing) {
|
|
3088
3641
|
throw new TaskforceRuleError(`Initiative not found: ${id}`, 404, 'INITIATIVE_NOT_FOUND');
|
|
3089
3642
|
}
|
|
3090
3643
|
if (existing.isArchived)
|
|
3091
3644
|
return existing;
|
|
3092
|
-
|
|
3645
|
+
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
3646
|
+
const archived = this.updateInitiative(id, { isArchived: true }, workspaceId, { ...options, skipEntityEvents: true });
|
|
3647
|
+
if (options?.skipEntityEvents !== true) {
|
|
3648
|
+
this.addEntityEvent({
|
|
3649
|
+
entityType: 'initiative',
|
|
3650
|
+
entityId: id,
|
|
3651
|
+
workspaceId: normalizedWorkspaceId,
|
|
3652
|
+
action: 'initiative-archived',
|
|
3653
|
+
actor: options?.actorRef || 'system',
|
|
3654
|
+
createdAt: archived.updatedAt || new Date().toISOString(),
|
|
3655
|
+
});
|
|
3656
|
+
return this.getInitiativeForWriteResult(id, normalizedWorkspaceId, archived, 'archiveInitiative:post-event');
|
|
3657
|
+
}
|
|
3658
|
+
return archived;
|
|
3093
3659
|
}
|
|
3094
|
-
unarchiveInitiative(id, workspaceId) {
|
|
3660
|
+
unarchiveInitiative(id, workspaceId, options) {
|
|
3095
3661
|
const existing = this.getInitiative(id, workspaceId);
|
|
3096
3662
|
if (!existing) {
|
|
3097
3663
|
throw new TaskforceRuleError(`Initiative not found: ${id}`, 404, 'INITIATIVE_NOT_FOUND');
|
|
3098
3664
|
}
|
|
3099
3665
|
if (!existing.isArchived)
|
|
3100
3666
|
return existing;
|
|
3101
|
-
|
|
3667
|
+
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
3668
|
+
const restored = this.updateInitiative(id, { isArchived: false }, workspaceId, { ...options, skipEntityEvents: true });
|
|
3669
|
+
if (options?.skipEntityEvents !== true) {
|
|
3670
|
+
this.addEntityEvent({
|
|
3671
|
+
entityType: 'initiative',
|
|
3672
|
+
entityId: id,
|
|
3673
|
+
workspaceId: normalizedWorkspaceId,
|
|
3674
|
+
action: 'initiative-unarchived',
|
|
3675
|
+
actor: options?.actorRef || 'system',
|
|
3676
|
+
createdAt: restored.updatedAt || new Date().toISOString(),
|
|
3677
|
+
});
|
|
3678
|
+
return this.getInitiativeForWriteResult(id, normalizedWorkspaceId, restored, 'unarchiveInitiative:post-event');
|
|
3679
|
+
}
|
|
3680
|
+
return restored;
|
|
3102
3681
|
}
|
|
3103
3682
|
getWorkstream(id, workspaceId) {
|
|
3104
3683
|
const normalizedId = String(id || '').trim();
|
|
@@ -3113,7 +3692,7 @@ export class TaskforceCore {
|
|
|
3113
3692
|
`).get(this.tenantId, normalizedWorkspaceId, normalizedId);
|
|
3114
3693
|
if (!row)
|
|
3115
3694
|
return null;
|
|
3116
|
-
return this.rowToWorkstream(row);
|
|
3695
|
+
return this.rowToWorkstream(row, { includeActivity: true });
|
|
3117
3696
|
}
|
|
3118
3697
|
listWorkstreams(workspaceId) {
|
|
3119
3698
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
@@ -3125,7 +3704,64 @@ export class TaskforceCore {
|
|
|
3125
3704
|
`).all(this.tenantId, normalizedWorkspaceId);
|
|
3126
3705
|
return rows.map((row) => this.rowToWorkstream(row));
|
|
3127
3706
|
}
|
|
3128
|
-
|
|
3707
|
+
listPlanningBootstrap(workspaceId, diagnostics) {
|
|
3708
|
+
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
3709
|
+
const startedAt = Date.now();
|
|
3710
|
+
const initiatives = this.listInitiatives(workspaceId);
|
|
3711
|
+
const initiativesLoadedAt = Date.now();
|
|
3712
|
+
const workstreams = this.listWorkstreams(workspaceId);
|
|
3713
|
+
const workstreamsLoadedAt = Date.now();
|
|
3714
|
+
const planningTaskRows = this.db.prepare(`
|
|
3715
|
+
SELECT id, reference_number, title, status, workstream_id
|
|
3716
|
+
FROM tasks
|
|
3717
|
+
WHERE tenant_id = ? AND workspace_id = ? AND is_archived = 0
|
|
3718
|
+
AND workstream_id IS NOT NULL AND TRIM(workstream_id) <> ''
|
|
3719
|
+
ORDER BY created_at DESC
|
|
3720
|
+
`).all(this.tenantId, normalizedWorkspaceId);
|
|
3721
|
+
const taskRowsLoadedAt = Date.now();
|
|
3722
|
+
const workstreamTaskSummaries = new Map();
|
|
3723
|
+
for (const row of planningTaskRows) {
|
|
3724
|
+
const workstreamId = String(row.workstream_id || '').trim();
|
|
3725
|
+
if (!workstreamId)
|
|
3726
|
+
continue;
|
|
3727
|
+
const status = this.normalizeStatusValue(row.status);
|
|
3728
|
+
const referenceNumber = this.sanitizeTaskReferenceNumber(row.reference_number);
|
|
3729
|
+
const current = workstreamTaskSummaries.get(workstreamId) || {
|
|
3730
|
+
workstreamId,
|
|
3731
|
+
taskCount: 0,
|
|
3732
|
+
completedTaskCount: 0,
|
|
3733
|
+
tasks: [],
|
|
3734
|
+
};
|
|
3735
|
+
current.taskCount += 1;
|
|
3736
|
+
if (status === 'done' || status === 'cancelled') {
|
|
3737
|
+
current.completedTaskCount += 1;
|
|
3738
|
+
}
|
|
3739
|
+
current.tasks.push({
|
|
3740
|
+
id: String(row.id || '').trim(),
|
|
3741
|
+
referenceNumber,
|
|
3742
|
+
title: String(row.title || '').trim() || 'Untitled Task',
|
|
3743
|
+
status,
|
|
3744
|
+
});
|
|
3745
|
+
workstreamTaskSummaries.set(workstreamId, current);
|
|
3746
|
+
}
|
|
3747
|
+
const summariesBuiltAt = Date.now();
|
|
3748
|
+
if (diagnostics) {
|
|
3749
|
+
diagnostics.initiativeCount = initiatives.length;
|
|
3750
|
+
diagnostics.workstreamCount = workstreams.length;
|
|
3751
|
+
diagnostics.workstreamLinkedTaskCount = planningTaskRows.length;
|
|
3752
|
+
diagnostics.workstreamTaskSummaryCount = workstreamTaskSummaries.size;
|
|
3753
|
+
diagnostics.initiativeLoadMs = Math.max(0, initiativesLoadedAt - startedAt);
|
|
3754
|
+
diagnostics.workstreamLoadMs = Math.max(0, workstreamsLoadedAt - initiativesLoadedAt);
|
|
3755
|
+
diagnostics.taskSummaryQueryMs = Math.max(0, taskRowsLoadedAt - workstreamsLoadedAt);
|
|
3756
|
+
diagnostics.taskSummaryBuildMs = Math.max(0, summariesBuiltAt - taskRowsLoadedAt);
|
|
3757
|
+
}
|
|
3758
|
+
return {
|
|
3759
|
+
initiatives,
|
|
3760
|
+
workstreams,
|
|
3761
|
+
workstreamTaskSummaries: Array.from(workstreamTaskSummaries.values()),
|
|
3762
|
+
};
|
|
3763
|
+
}
|
|
3764
|
+
createWorkstream(input, workspaceId, options) {
|
|
3129
3765
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
3130
3766
|
const id = String(input.id || '').trim() || this.generateEntityId('workstream');
|
|
3131
3767
|
const referenceNumber = this.reserveWorkstreamReferenceNumber(normalizedWorkspaceId, input.referenceNumber);
|
|
@@ -3168,14 +3804,33 @@ export class TaskforceCore {
|
|
|
3168
3804
|
sortOrder: workstream.order,
|
|
3169
3805
|
isArchived: workstream.isArchived ? 1 : 0,
|
|
3170
3806
|
});
|
|
3171
|
-
|
|
3807
|
+
this.syncCanonicalPlanningAttachmentLinks('workstream', workstream.id, normalizedWorkspaceId, workstream.attachments || []);
|
|
3808
|
+
if (options?.skipEntityEvents !== true) {
|
|
3809
|
+
this.addEntityEvent({
|
|
3810
|
+
entityType: 'workstream',
|
|
3811
|
+
entityId: workstream.id,
|
|
3812
|
+
workspaceId: normalizedWorkspaceId,
|
|
3813
|
+
action: 'workstream-created',
|
|
3814
|
+
actor: options?.actorRef || 'system',
|
|
3815
|
+
createdAt: workstream.updatedAt || workstream.createdAt,
|
|
3816
|
+
details: {
|
|
3817
|
+
title: workstream.title,
|
|
3818
|
+
ownerId: workstream.ownerId || null,
|
|
3819
|
+
initiativeId: workstream.initiativeId || null,
|
|
3820
|
+
}
|
|
3821
|
+
});
|
|
3822
|
+
}
|
|
3823
|
+
return this.getWorkstreamForWriteResult(workstream.id, normalizedWorkspaceId, workstream, 'createWorkstream:post-write');
|
|
3172
3824
|
}
|
|
3173
|
-
updateWorkstream(id, patch, workspaceId) {
|
|
3825
|
+
updateWorkstream(id, patch, workspaceId, options) {
|
|
3174
3826
|
const existing = this.getWorkstream(id, workspaceId);
|
|
3175
3827
|
if (!existing) {
|
|
3176
3828
|
throw new TaskforceRuleError(`Workstream not found: ${id}`, 404, 'WORKSTREAM_NOT_FOUND');
|
|
3177
3829
|
}
|
|
3178
3830
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
3831
|
+
const nextAttachments = Object.prototype.hasOwnProperty.call(patch, 'attachments') && Array.isArray(patch.attachments)
|
|
3832
|
+
? patch.attachments
|
|
3833
|
+
: existing.attachments;
|
|
3179
3834
|
const next = {
|
|
3180
3835
|
...existing,
|
|
3181
3836
|
...patch,
|
|
@@ -3183,6 +3838,7 @@ export class TaskforceCore {
|
|
|
3183
3838
|
initiativeId: Object.prototype.hasOwnProperty.call(patch, 'initiativeId')
|
|
3184
3839
|
? this.sanitizeInitiativeId(patch.initiativeId)
|
|
3185
3840
|
: existing.initiativeId,
|
|
3841
|
+
attachments: nextAttachments,
|
|
3186
3842
|
updatedAt: String(patch.updatedAt || '').trim() || new Date().toISOString(),
|
|
3187
3843
|
referenceLabel: formatWorkstreamReference(patch.referenceNumber ?? existing.referenceNumber),
|
|
3188
3844
|
};
|
|
@@ -3215,41 +3871,94 @@ export class TaskforceCore {
|
|
|
3215
3871
|
sortOrder: next.order,
|
|
3216
3872
|
isArchived: next.isArchived ? 1 : 0,
|
|
3217
3873
|
});
|
|
3218
|
-
|
|
3874
|
+
this.syncCanonicalPlanningAttachmentLinks('workstream', existing.id, normalizedWorkspaceId, next.attachments || []);
|
|
3875
|
+
const hydrated = this.getWorkstreamForWriteResult(existing.id, normalizedWorkspaceId, next, 'updateWorkstream:post-write');
|
|
3876
|
+
if (options?.skipEntityEvents !== true) {
|
|
3877
|
+
const changes = this.buildPlanningEntityEventChanges(existing, next, ['title', 'description', 'ownerId', 'initiativeId', 'attachments']);
|
|
3878
|
+
if (Object.keys(changes).length > 0) {
|
|
3879
|
+
this.addEntityEvent({
|
|
3880
|
+
entityType: 'workstream',
|
|
3881
|
+
entityId: existing.id,
|
|
3882
|
+
workspaceId: normalizedWorkspaceId,
|
|
3883
|
+
action: this.getPlanningEntityEventAction('workstream', changes),
|
|
3884
|
+
actor: options?.actorRef || 'system',
|
|
3885
|
+
createdAt: next.updatedAt,
|
|
3886
|
+
details: { changes }
|
|
3887
|
+
});
|
|
3888
|
+
return this.getWorkstreamForWriteResult(existing.id, normalizedWorkspaceId, hydrated, 'updateWorkstream:post-event');
|
|
3889
|
+
}
|
|
3890
|
+
}
|
|
3891
|
+
return hydrated;
|
|
3219
3892
|
}
|
|
3220
|
-
archiveWorkstream(id, workspaceId) {
|
|
3893
|
+
archiveWorkstream(id, workspaceId, options) {
|
|
3221
3894
|
const existing = this.getWorkstream(id, workspaceId);
|
|
3222
3895
|
if (!existing) {
|
|
3223
3896
|
throw new TaskforceRuleError(`Workstream not found: ${id}`, 404, 'WORKSTREAM_NOT_FOUND');
|
|
3224
3897
|
}
|
|
3225
3898
|
if (existing.isArchived)
|
|
3226
3899
|
return existing;
|
|
3227
|
-
|
|
3900
|
+
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
3901
|
+
const archived = this.updateWorkstream(id, { isArchived: true }, workspaceId, { ...options, skipEntityEvents: true });
|
|
3902
|
+
if (options?.skipEntityEvents !== true) {
|
|
3903
|
+
this.addEntityEvent({
|
|
3904
|
+
entityType: 'workstream',
|
|
3905
|
+
entityId: id,
|
|
3906
|
+
workspaceId: normalizedWorkspaceId,
|
|
3907
|
+
action: 'workstream-archived',
|
|
3908
|
+
actor: options?.actorRef || 'system',
|
|
3909
|
+
createdAt: archived.updatedAt || new Date().toISOString(),
|
|
3910
|
+
});
|
|
3911
|
+
return this.getWorkstreamForWriteResult(id, normalizedWorkspaceId, archived, 'archiveWorkstream:post-event');
|
|
3912
|
+
}
|
|
3913
|
+
return archived;
|
|
3228
3914
|
}
|
|
3229
|
-
unarchiveWorkstream(id, workspaceId) {
|
|
3915
|
+
unarchiveWorkstream(id, workspaceId, options) {
|
|
3230
3916
|
const existing = this.getWorkstream(id, workspaceId);
|
|
3231
3917
|
if (!existing) {
|
|
3232
3918
|
throw new TaskforceRuleError(`Workstream not found: ${id}`, 404, 'WORKSTREAM_NOT_FOUND');
|
|
3233
3919
|
}
|
|
3234
3920
|
if (!existing.isArchived)
|
|
3235
3921
|
return existing;
|
|
3236
|
-
|
|
3922
|
+
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
3923
|
+
const restored = this.updateWorkstream(id, { isArchived: false }, workspaceId, { ...options, skipEntityEvents: true });
|
|
3924
|
+
if (options?.skipEntityEvents !== true) {
|
|
3925
|
+
this.addEntityEvent({
|
|
3926
|
+
entityType: 'workstream',
|
|
3927
|
+
entityId: id,
|
|
3928
|
+
workspaceId: normalizedWorkspaceId,
|
|
3929
|
+
action: 'workstream-unarchived',
|
|
3930
|
+
actor: options?.actorRef || 'system',
|
|
3931
|
+
createdAt: restored.updatedAt || new Date().toISOString(),
|
|
3932
|
+
});
|
|
3933
|
+
return this.getWorkstreamForWriteResult(id, normalizedWorkspaceId, restored, 'unarchiveWorkstream:post-event');
|
|
3934
|
+
}
|
|
3935
|
+
return restored;
|
|
3237
3936
|
}
|
|
3238
3937
|
upsertInitiativeForSync(initiative, workspaceId) {
|
|
3239
3938
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
3240
3939
|
const existing = this.getInitiative(initiative.id, normalizedWorkspaceId);
|
|
3241
3940
|
if (existing) {
|
|
3242
|
-
|
|
3941
|
+
const existingUpdatedAtMs = Date.parse(String(existing.updatedAt || existing.createdAt || '').trim());
|
|
3942
|
+
const incomingUpdatedAtMs = Date.parse(String(initiative.updatedAt || initiative.createdAt || '').trim());
|
|
3943
|
+
if (Number.isFinite(existingUpdatedAtMs) && Number.isFinite(incomingUpdatedAtMs) && incomingUpdatedAtMs < existingUpdatedAtMs) {
|
|
3944
|
+
return existing;
|
|
3945
|
+
}
|
|
3946
|
+
return this.updateInitiative(existing.id, initiative, normalizedWorkspaceId, { skipEntityEvents: true });
|
|
3243
3947
|
}
|
|
3244
|
-
return this.createInitiative(initiative, normalizedWorkspaceId);
|
|
3948
|
+
return this.createInitiative(initiative, normalizedWorkspaceId, { skipEntityEvents: true });
|
|
3245
3949
|
}
|
|
3246
3950
|
upsertWorkstreamForSync(workstream, workspaceId) {
|
|
3247
3951
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
3248
3952
|
const existing = this.getWorkstream(workstream.id, normalizedWorkspaceId);
|
|
3249
3953
|
if (existing) {
|
|
3250
|
-
|
|
3954
|
+
const existingUpdatedAtMs = Date.parse(String(existing.updatedAt || existing.createdAt || '').trim());
|
|
3955
|
+
const incomingUpdatedAtMs = Date.parse(String(workstream.updatedAt || workstream.createdAt || '').trim());
|
|
3956
|
+
if (Number.isFinite(existingUpdatedAtMs) && Number.isFinite(incomingUpdatedAtMs) && incomingUpdatedAtMs < existingUpdatedAtMs) {
|
|
3957
|
+
return existing;
|
|
3958
|
+
}
|
|
3959
|
+
return this.updateWorkstream(existing.id, workstream, normalizedWorkspaceId, { skipEntityEvents: true });
|
|
3251
3960
|
}
|
|
3252
|
-
return this.createWorkstream(workstream, normalizedWorkspaceId);
|
|
3961
|
+
return this.createWorkstream(workstream, normalizedWorkspaceId, { skipEntityEvents: true });
|
|
3253
3962
|
}
|
|
3254
3963
|
listChecklistItems(taskId, workspaceId) {
|
|
3255
3964
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
@@ -3899,7 +4608,7 @@ export class TaskforceCore {
|
|
|
3899
4608
|
listTasks(workspaceId) {
|
|
3900
4609
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
3901
4610
|
const rows = this.db.prepare(`
|
|
3902
|
-
SELECT * FROM tasks
|
|
4611
|
+
SELECT * FROM tasks
|
|
3903
4612
|
WHERE tenant_id = ?
|
|
3904
4613
|
AND workspace_id = ?
|
|
3905
4614
|
AND is_archived = 0
|
|
@@ -3910,30 +4619,136 @@ export class TaskforceCore {
|
|
|
3910
4619
|
config: this.readConfig()
|
|
3911
4620
|
};
|
|
3912
4621
|
}
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
4622
|
+
/**
|
|
4623
|
+
* Filter-pushdown listing used by read-only surfaces that don't need the
|
|
4624
|
+
* full task graph (MCP list_tasks, CLI summaries). Avoids the per-task
|
|
4625
|
+
* comments/events/attachments/checklist subqueries and actor resolution
|
|
4626
|
+
* that rowToTask performs, and applies filters + sort + limit at the SQL
|
|
4627
|
+
* layer so hot polls don't load the entire workspace.
|
|
4628
|
+
*/
|
|
4629
|
+
listTasksSlim(workspaceId, filters = {}) {
|
|
4630
|
+
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
4631
|
+
const clauses = [
|
|
4632
|
+
'tenant_id = ?',
|
|
4633
|
+
'workspace_id = ?',
|
|
4634
|
+
'is_archived = 0',
|
|
4635
|
+
];
|
|
4636
|
+
const params = [this.tenantId, normalizedWorkspaceId];
|
|
4637
|
+
if (filters.category) {
|
|
4638
|
+
clauses.push('category = ?');
|
|
4639
|
+
params.push(filters.category);
|
|
3917
4640
|
}
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
getDatabaseProvider() {
|
|
3922
|
-
return this.db.provider;
|
|
3923
|
-
}
|
|
3924
|
-
isDatabaseHealthy() {
|
|
3925
|
-
try {
|
|
3926
|
-
const row = this.db.prepare('SELECT 1 as ok').get();
|
|
3927
|
-
return row?.ok === 1 || row?.ok === undefined;
|
|
4641
|
+
if (filters.status) {
|
|
4642
|
+
clauses.push('status = ?');
|
|
4643
|
+
params.push(filters.status);
|
|
3928
4644
|
}
|
|
3929
|
-
|
|
3930
|
-
|
|
4645
|
+
else if (filters.openOnly) {
|
|
4646
|
+
clauses.push("status IN ('task','on-hold','in-progress','review')");
|
|
3931
4647
|
}
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
4648
|
+
if (filters.priority !== undefined) {
|
|
4649
|
+
clauses.push('priority = ?');
|
|
4650
|
+
params.push(Number(filters.priority));
|
|
4651
|
+
}
|
|
4652
|
+
if (filters.type) {
|
|
4653
|
+
clauses.push('type = ?');
|
|
4654
|
+
params.push(filters.type);
|
|
4655
|
+
}
|
|
4656
|
+
if (filters.assignee) {
|
|
4657
|
+
if (filters.assignee === 'unassigned') {
|
|
4658
|
+
clauses.push("(assignee IS NULL OR assignee = '' OR assignee = 'unassigned')");
|
|
4659
|
+
}
|
|
4660
|
+
else {
|
|
4661
|
+
clauses.push('assignee = ?');
|
|
4662
|
+
params.push(filters.assignee);
|
|
4663
|
+
}
|
|
4664
|
+
}
|
|
4665
|
+
const order = filters.order === 'asc' ? 'ASC' : 'DESC';
|
|
4666
|
+
const sort = filters.sort ?? 'created';
|
|
4667
|
+
let orderBy;
|
|
4668
|
+
if (sort === 'priority') {
|
|
4669
|
+
orderBy = `priority ${order}, created_at ${order}`;
|
|
4670
|
+
}
|
|
4671
|
+
else if (sort === 'updated') {
|
|
4672
|
+
orderBy = `COALESCE(updated_at, created_at) ${order}`;
|
|
4673
|
+
}
|
|
4674
|
+
else {
|
|
4675
|
+
orderBy = `created_at ${order}`;
|
|
4676
|
+
}
|
|
4677
|
+
let sql = `SELECT * FROM tasks WHERE ${clauses.join(' AND ')} ORDER BY ${orderBy}`;
|
|
4678
|
+
const rawLimit = Number(filters.limit);
|
|
4679
|
+
const limit = Number.isFinite(rawLimit) ? Math.max(0, Math.floor(rawLimit)) : 0;
|
|
4680
|
+
if (limit > 0) {
|
|
4681
|
+
sql += ' LIMIT ?';
|
|
4682
|
+
params.push(limit);
|
|
4683
|
+
}
|
|
4684
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
4685
|
+
return rows.map((row) => {
|
|
4686
|
+
const sanitizedAssignee = this.sanitizeAssignee(row.assignee, normalizedWorkspaceId);
|
|
4687
|
+
const scheduledDate = this.sanitizeDateOnly(row.scheduled_date);
|
|
4688
|
+
const dueDate = this.sanitizeDateOnly(row.due_date);
|
|
4689
|
+
const scheduledWeekKey = row.scheduled_week_key
|
|
4690
|
+
|| (scheduledDate ? this.dateOnlyToIsoWeekKey(scheduledDate) : null);
|
|
4691
|
+
const referenceNumber = this.sanitizeTaskReferenceNumber(row.reference_number);
|
|
4692
|
+
const localReferenceNumber = this.sanitizeTaskReferenceNumber(row.local_reference_number);
|
|
4693
|
+
let taxonomies = {};
|
|
4694
|
+
try {
|
|
4695
|
+
taxonomies = JSON.parse(row.taxonomies || '{}');
|
|
4696
|
+
}
|
|
4697
|
+
catch {
|
|
4698
|
+
taxonomies = {};
|
|
4699
|
+
}
|
|
4700
|
+
return {
|
|
4701
|
+
id: String(row.id || '').trim(),
|
|
4702
|
+
referenceNumber,
|
|
4703
|
+
localReferenceNumber,
|
|
4704
|
+
referenceLabel: deriveTaskReferenceLabel({ referenceNumber, localReferenceNumber }),
|
|
4705
|
+
title: row.title,
|
|
4706
|
+
status: this.normalizeStatusValue(row.status),
|
|
4707
|
+
priority: this.normalizePriorityValue(row.priority),
|
|
4708
|
+
complexity: row.complexity,
|
|
4709
|
+
type: row.type,
|
|
4710
|
+
category: row.category,
|
|
4711
|
+
approach: row.approach || undefined,
|
|
4712
|
+
createdAt: row.created_at,
|
|
4713
|
+
updatedAt: row.updated_at,
|
|
4714
|
+
completedAt: row.completed_at,
|
|
4715
|
+
canceledReason: row.canceled_reason,
|
|
4716
|
+
isArchived: row.is_archived === 1,
|
|
4717
|
+
scheduledDate,
|
|
4718
|
+
dueDate,
|
|
4719
|
+
scheduledWeekKey,
|
|
4720
|
+
orderInDay: this.sanitizeOrderInDay(row.order_in_day),
|
|
4721
|
+
workstreamId: this.sanitizeWorkstreamId(row.workstream_id),
|
|
4722
|
+
taxonomies,
|
|
4723
|
+
createdBy: this.normalizeCreatedBy(row.created_by, normalizedWorkspaceId, { preserveGenericAi: true }),
|
|
4724
|
+
assignee: sanitizedAssignee,
|
|
4725
|
+
};
|
|
4726
|
+
});
|
|
4727
|
+
}
|
|
4728
|
+
getDataVersion() {
|
|
4729
|
+
if (this.db.provider === 'sqlite') {
|
|
4730
|
+
const version = this.db.pragma('data_version', { simple: true });
|
|
4731
|
+
return Number.isFinite(version) ? version : 0;
|
|
4732
|
+
}
|
|
4733
|
+
// Fallback signal for non-SQLite backends until provider-specific versioning is implemented.
|
|
4734
|
+
return 0;
|
|
4735
|
+
}
|
|
4736
|
+
getDatabaseProvider() {
|
|
4737
|
+
return this.db.provider;
|
|
4738
|
+
}
|
|
4739
|
+
isDatabaseHealthy() {
|
|
4740
|
+
try {
|
|
4741
|
+
const row = this.db.prepare('SELECT 1 as ok').get();
|
|
4742
|
+
return row?.ok === 1 || row?.ok === undefined;
|
|
4743
|
+
}
|
|
4744
|
+
catch {
|
|
4745
|
+
return false;
|
|
4746
|
+
}
|
|
4747
|
+
}
|
|
4748
|
+
listArchive(workspaceId) {
|
|
4749
|
+
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
4750
|
+
const rows = this.db.prepare(`
|
|
4751
|
+
SELECT * FROM tasks
|
|
3937
4752
|
WHERE tenant_id = ?
|
|
3938
4753
|
AND workspace_id = ?
|
|
3939
4754
|
AND is_archived = 1
|
|
@@ -4050,9 +4865,9 @@ export class TaskforceCore {
|
|
|
4050
4865
|
`).get(this.tenantId, userId);
|
|
4051
4866
|
if (!existingUser) {
|
|
4052
4867
|
this.db.prepare(`
|
|
4053
|
-
INSERT INTO users (id, tenant_id, email, display_name,
|
|
4054
|
-
VALUES (?, ?, ?, ?, ?, ?, ?,
|
|
4055
|
-
`).run(userId, this.tenantId, email, displayName,
|
|
4868
|
+
INSERT INTO users (id, tenant_id, email, display_name, beta_access, is_disabled, created_at, updated_at)
|
|
4869
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
4870
|
+
`).run(userId, this.tenantId, email, displayName, 1, 0, now, now);
|
|
4056
4871
|
}
|
|
4057
4872
|
const membership = this.db.prepare(`
|
|
4058
4873
|
SELECT id, role FROM workspace_memberships
|
|
@@ -4097,16 +4912,17 @@ export class TaskforceCore {
|
|
|
4097
4912
|
`).get(this.tenantId, normalizedUserId, normalizedWorkspaceId);
|
|
4098
4913
|
if (!membership?.workspace_id)
|
|
4099
4914
|
return null;
|
|
4100
|
-
const
|
|
4915
|
+
const accountDisabled = Boolean(userRow.is_disabled);
|
|
4101
4916
|
const userBetaAccess = userRow.beta_access === undefined || userRow.beta_access === null
|
|
4102
4917
|
? true
|
|
4103
4918
|
: Boolean(userRow.beta_access);
|
|
4104
4919
|
const isSystemAdmin = this.normalizeSystemRole(userRow.system_role, 'user') === 'system_admin';
|
|
4105
4920
|
const membershipActive = !membership.status || String(membership.status).toLowerCase() === 'active';
|
|
4921
|
+
const workspaceDisabled = !membershipActive;
|
|
4106
4922
|
const baseRole = this.normalizeAccessRole(membership.role || fallbackRole, fallbackRole);
|
|
4107
4923
|
const permissionMode = this.normalizePermissionMode(membership.permission_mode, 'read-write');
|
|
4108
4924
|
const isMemberReadOnly = baseRole === 'member' && permissionMode === 'read-only';
|
|
4109
|
-
const resolvedRole = (
|
|
4925
|
+
const resolvedRole = (accountDisabled || workspaceDisabled || isMemberReadOnly)
|
|
4110
4926
|
? 'read-only'
|
|
4111
4927
|
: baseRole;
|
|
4112
4928
|
return {
|
|
@@ -4114,7 +4930,9 @@ export class TaskforceCore {
|
|
|
4114
4930
|
workspaceId: String(membership.workspace_id || normalizedWorkspaceId),
|
|
4115
4931
|
role: resolvedRole,
|
|
4116
4932
|
permissionMode,
|
|
4117
|
-
disabled,
|
|
4933
|
+
disabled: accountDisabled || workspaceDisabled,
|
|
4934
|
+
accountDisabled,
|
|
4935
|
+
workspaceDisabled,
|
|
4118
4936
|
betaAccess: userBetaAccess || isSystemAdmin
|
|
4119
4937
|
};
|
|
4120
4938
|
}
|
|
@@ -4292,6 +5110,91 @@ export class TaskforceCore {
|
|
|
4292
5110
|
`).run(now, now, this.tenantId, normalizedUserId, cutoff);
|
|
4293
5111
|
return staleDrafts;
|
|
4294
5112
|
}
|
|
5113
|
+
createTaskAttachmentUploadDraft(input) {
|
|
5114
|
+
const draftId = String(input.draftId || '').trim();
|
|
5115
|
+
const workspaceId = String(input.workspaceId || '').trim();
|
|
5116
|
+
const taskId = String(input.taskId || '').trim();
|
|
5117
|
+
const actorRef = String(input.actorRef || '').trim() || 'system';
|
|
5118
|
+
const storageKey = String(input.storageKey || '').trim();
|
|
5119
|
+
const mimeType = String(input.mimeType || 'application/octet-stream').trim() || 'application/octet-stream';
|
|
5120
|
+
const originalFilename = String(input.originalFilename || '').trim() || 'attachment';
|
|
5121
|
+
const sizeBytes = Number.isFinite(Number(input.sizeBytes)) ? Math.max(0, Math.floor(Number(input.sizeBytes))) : 0;
|
|
5122
|
+
const storageProvider = input.storageProvider === 'r2' || input.storageProvider === 'external' ? input.storageProvider : 'local';
|
|
5123
|
+
if (!draftId)
|
|
5124
|
+
throw new TaskforceRuleError('draftId is required', 400, 'TASK_ATTACHMENT_DRAFT_ID_REQUIRED');
|
|
5125
|
+
if (!workspaceId)
|
|
5126
|
+
throw new TaskforceRuleError('workspaceId is required', 400, 'WORKSPACE_ID_REQUIRED');
|
|
5127
|
+
if (!taskId)
|
|
5128
|
+
throw new TaskforceRuleError('taskId is required', 400, 'TASK_ID_REQUIRED');
|
|
5129
|
+
if (!storageKey)
|
|
5130
|
+
throw new TaskforceRuleError('storageKey is required', 400, 'TASK_ATTACHMENT_STORAGE_KEY_REQUIRED');
|
|
5131
|
+
const now = new Date().toISOString();
|
|
5132
|
+
this.db.prepare(`
|
|
5133
|
+
INSERT INTO task_attachment_upload_drafts (
|
|
5134
|
+
draft_id, tenant_id, workspace_id, task_id, actor_ref, storage_key, mime_type, original_filename,
|
|
5135
|
+
size_bytes, storage_provider, consumed_at, discarded_at, created_at, updated_at
|
|
5136
|
+
)
|
|
5137
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?)
|
|
5138
|
+
`).run(draftId, this.tenantId, workspaceId, taskId, actorRef, storageKey, mimeType, originalFilename, sizeBytes, storageProvider, now, now);
|
|
5139
|
+
return this.getTaskAttachmentUploadDraft(draftId);
|
|
5140
|
+
}
|
|
5141
|
+
getTaskAttachmentUploadDraft(draftId) {
|
|
5142
|
+
const normalizedDraftId = String(draftId || '').trim();
|
|
5143
|
+
if (!normalizedDraftId)
|
|
5144
|
+
return null;
|
|
5145
|
+
const row = this.db.prepare(`
|
|
5146
|
+
SELECT draft_id, workspace_id, task_id, actor_ref, storage_key, mime_type, original_filename,
|
|
5147
|
+
size_bytes, storage_provider, consumed_at, discarded_at, created_at, updated_at
|
|
5148
|
+
FROM task_attachment_upload_drafts
|
|
5149
|
+
WHERE tenant_id = ? AND draft_id = ?
|
|
5150
|
+
LIMIT 1
|
|
5151
|
+
`).get(this.tenantId, normalizedDraftId);
|
|
5152
|
+
if (!row?.draft_id)
|
|
5153
|
+
return null;
|
|
5154
|
+
return {
|
|
5155
|
+
draftId: String(row.draft_id),
|
|
5156
|
+
workspaceId: String(row.workspace_id || ''),
|
|
5157
|
+
taskId: String(row.task_id || ''),
|
|
5158
|
+
actorRef: String(row.actor_ref || 'system'),
|
|
5159
|
+
storageKey: String(row.storage_key || ''),
|
|
5160
|
+
mimeType: String(row.mime_type || 'application/octet-stream'),
|
|
5161
|
+
originalFilename: String(row.original_filename || 'attachment'),
|
|
5162
|
+
sizeBytes: Number(row.size_bytes || 0),
|
|
5163
|
+
storageProvider: String(row.storage_provider || 'local') === 'r2'
|
|
5164
|
+
? 'r2'
|
|
5165
|
+
: String(row.storage_provider || 'local') === 'external'
|
|
5166
|
+
? 'external'
|
|
5167
|
+
: 'local',
|
|
5168
|
+
consumedAt: row.consumed_at ? String(row.consumed_at) : null,
|
|
5169
|
+
discardedAt: row.discarded_at ? String(row.discarded_at) : null,
|
|
5170
|
+
createdAt: String(row.created_at || ''),
|
|
5171
|
+
updatedAt: String(row.updated_at || ''),
|
|
5172
|
+
};
|
|
5173
|
+
}
|
|
5174
|
+
markTaskAttachmentUploadDraftConsumed(draftId) {
|
|
5175
|
+
const existing = this.getTaskAttachmentUploadDraft(draftId);
|
|
5176
|
+
if (!existing || existing.discardedAt)
|
|
5177
|
+
return null;
|
|
5178
|
+
const now = new Date().toISOString();
|
|
5179
|
+
this.db.prepare(`
|
|
5180
|
+
UPDATE task_attachment_upload_drafts
|
|
5181
|
+
SET consumed_at = COALESCE(consumed_at, ?), updated_at = ?
|
|
5182
|
+
WHERE tenant_id = ? AND draft_id = ? AND discarded_at IS NULL
|
|
5183
|
+
`).run(now, now, this.tenantId, existing.draftId);
|
|
5184
|
+
return this.getTaskAttachmentUploadDraft(existing.draftId);
|
|
5185
|
+
}
|
|
5186
|
+
discardTaskAttachmentUploadDraft(draftId) {
|
|
5187
|
+
const existing = this.getTaskAttachmentUploadDraft(draftId);
|
|
5188
|
+
if (!existing || existing.discardedAt)
|
|
5189
|
+
return existing;
|
|
5190
|
+
const now = new Date().toISOString();
|
|
5191
|
+
this.db.prepare(`
|
|
5192
|
+
UPDATE task_attachment_upload_drafts
|
|
5193
|
+
SET discarded_at = ?, updated_at = ?
|
|
5194
|
+
WHERE tenant_id = ? AND draft_id = ? AND discarded_at IS NULL
|
|
5195
|
+
`).run(now, now, this.tenantId, existing.draftId);
|
|
5196
|
+
return this.getTaskAttachmentUploadDraft(existing.draftId);
|
|
5197
|
+
}
|
|
4295
5198
|
getSystemRole(userId) {
|
|
4296
5199
|
const normalizedUserId = String(userId || '').trim();
|
|
4297
5200
|
if (!normalizedUserId || normalizedUserId === 'anonymous')
|
|
@@ -4333,7 +5236,68 @@ export class TaskforceCore {
|
|
|
4333
5236
|
hasAnySystemAdmin() {
|
|
4334
5237
|
return this.countSystemAdmins() > 0;
|
|
4335
5238
|
}
|
|
4336
|
-
|
|
5239
|
+
countSystemUsers(input) {
|
|
5240
|
+
const query = String(input?.query || '').trim().toLowerCase();
|
|
5241
|
+
const queryPattern = query ? `%${query}%` : '';
|
|
5242
|
+
const searchClause = query
|
|
5243
|
+
? `
|
|
5244
|
+
AND (
|
|
5245
|
+
LOWER(COALESCE(u.display_name, '')) LIKE ?
|
|
5246
|
+
OR LOWER(COALESCE(u.id, '')) LIKE ?
|
|
5247
|
+
OR LOWER(COALESCE(u.email, '')) LIKE ?
|
|
5248
|
+
OR LOWER(COALESCE(COALESCE(p.display_name, v.plan_id), '')) LIKE ?
|
|
5249
|
+
OR LOWER(COALESCE(v.plan_id, '')) LIKE ?
|
|
5250
|
+
OR LOWER(COALESCE(e.entitlement_state, '')) LIKE ?
|
|
5251
|
+
OR LOWER(COALESCE(u.system_role, 'user')) LIKE ?
|
|
5252
|
+
)`
|
|
5253
|
+
: '';
|
|
5254
|
+
const params = [this.tenantId];
|
|
5255
|
+
if (query) {
|
|
5256
|
+
params.push(queryPattern, queryPattern, queryPattern, queryPattern, queryPattern, queryPattern, queryPattern);
|
|
5257
|
+
}
|
|
5258
|
+
const row = this.db.prepare(`
|
|
5259
|
+
SELECT COUNT(*) AS count
|
|
5260
|
+
FROM users u
|
|
5261
|
+
LEFT JOIN account_entitlements e
|
|
5262
|
+
ON e.tenant_id = u.tenant_id
|
|
5263
|
+
AND e.account_id = u.id
|
|
5264
|
+
LEFT JOIN plan_versions v
|
|
5265
|
+
ON v.tenant_id = u.tenant_id
|
|
5266
|
+
AND v.plan_version_id = e.plan_version_id
|
|
5267
|
+
LEFT JOIN plan_catalog p
|
|
5268
|
+
ON p.tenant_id = u.tenant_id
|
|
5269
|
+
AND p.plan_id = v.plan_id
|
|
5270
|
+
WHERE u.tenant_id = ?
|
|
5271
|
+
${searchClause}
|
|
5272
|
+
`).get(...params);
|
|
5273
|
+
return Number(row?.count || 0);
|
|
5274
|
+
}
|
|
5275
|
+
listSystemUsers(input) {
|
|
5276
|
+
const query = String(input?.query || '').trim().toLowerCase();
|
|
5277
|
+
const queryPattern = query ? `%${query}%` : '';
|
|
5278
|
+
const sort = input?.sort === 'oldest' ? 'oldest' : 'newest';
|
|
5279
|
+
const limit = Number.isFinite(Number(input?.limit)) ? Math.max(1, Math.min(200, Math.floor(Number(input?.limit)))) : 50;
|
|
5280
|
+
const offset = Number.isFinite(Number(input?.offset)) ? Math.max(0, Math.floor(Number(input?.offset))) : 0;
|
|
5281
|
+
const orderByClause = sort === 'oldest'
|
|
5282
|
+
? `ORDER BY COALESCE(u.created_at, u.updated_at) ASC, COALESCE(u.updated_at, u.created_at) ASC, u.id ASC`
|
|
5283
|
+
: `ORDER BY COALESCE(u.created_at, u.updated_at) DESC, COALESCE(u.updated_at, u.created_at) DESC, u.id DESC`;
|
|
5284
|
+
const searchClause = query
|
|
5285
|
+
? `
|
|
5286
|
+
AND (
|
|
5287
|
+
LOWER(COALESCE(u.display_name, '')) LIKE ?
|
|
5288
|
+
OR LOWER(COALESCE(u.id, '')) LIKE ?
|
|
5289
|
+
OR LOWER(COALESCE(u.email, '')) LIKE ?
|
|
5290
|
+
OR LOWER(COALESCE(COALESCE(p.display_name, v.plan_id), '')) LIKE ?
|
|
5291
|
+
OR LOWER(COALESCE(v.plan_id, '')) LIKE ?
|
|
5292
|
+
OR LOWER(COALESCE(e.entitlement_state, '')) LIKE ?
|
|
5293
|
+
OR LOWER(COALESCE(u.system_role, 'user')) LIKE ?
|
|
5294
|
+
)`
|
|
5295
|
+
: '';
|
|
5296
|
+
const params = [this.tenantId];
|
|
5297
|
+
if (query) {
|
|
5298
|
+
params.push(queryPattern, queryPattern, queryPattern, queryPattern, queryPattern, queryPattern, queryPattern);
|
|
5299
|
+
}
|
|
5300
|
+
params.push(limit, offset);
|
|
4337
5301
|
const rows = this.db.prepare(`
|
|
4338
5302
|
SELECT
|
|
4339
5303
|
u.id AS user_id,
|
|
@@ -4360,8 +5324,10 @@ export class TaskforceCore {
|
|
|
4360
5324
|
ON p.tenant_id = u.tenant_id
|
|
4361
5325
|
AND p.plan_id = v.plan_id
|
|
4362
5326
|
WHERE u.tenant_id = ?
|
|
4363
|
-
|
|
4364
|
-
|
|
5327
|
+
${searchClause}
|
|
5328
|
+
${orderByClause}
|
|
5329
|
+
LIMIT ? OFFSET ?
|
|
5330
|
+
`).all(...params);
|
|
4365
5331
|
return rows
|
|
4366
5332
|
.filter((row) => row.user_id && row.email)
|
|
4367
5333
|
.map((row) => ({
|
|
@@ -4653,6 +5619,7 @@ export class TaskforceCore {
|
|
|
4653
5619
|
u.id AS user_id,
|
|
4654
5620
|
u.email AS email,
|
|
4655
5621
|
u.display_name AS display_name,
|
|
5622
|
+
u.avatar_url AS avatar_url,
|
|
4656
5623
|
u.is_disabled AS is_disabled,
|
|
4657
5624
|
u.created_at AS created_at,
|
|
4658
5625
|
u.updated_at AS updated_at,
|
|
@@ -4669,19 +5636,23 @@ export class TaskforceCore {
|
|
|
4669
5636
|
AND m.workspace_id = ?
|
|
4670
5637
|
ORDER BY COALESCE(m.updated_at, m.created_at) DESC, u.created_at DESC
|
|
4671
5638
|
`).all(this.tenantId, normalizedWorkspaceId);
|
|
4672
|
-
return rows.map((row) =>
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
5639
|
+
return rows.map((row) => {
|
|
5640
|
+
const status = String(row.status || 'active');
|
|
5641
|
+
return {
|
|
5642
|
+
userId: row.user_id,
|
|
5643
|
+
email: row.email,
|
|
5644
|
+
displayName: row.display_name || null,
|
|
5645
|
+
avatarUrl: row.avatar_url || null,
|
|
5646
|
+
role: this.normalizeAccessRole(row.role || 'member', 'member'),
|
|
5647
|
+
permissionMode: this.normalizePermissionMode(row.permission_mode, 'read-write'),
|
|
5648
|
+
betaAccess: row.beta_access === undefined || row.beta_access === null ? true : Boolean(row.beta_access),
|
|
5649
|
+
status,
|
|
5650
|
+
disabled: Boolean(row.is_disabled) || status.toLowerCase() === 'disabled',
|
|
5651
|
+
createdAt: row.created_at || null,
|
|
5652
|
+
updatedAt: row.updated_at || null,
|
|
5653
|
+
emailVerifiedAt: row.email_verified_at || null
|
|
5654
|
+
};
|
|
5655
|
+
});
|
|
4685
5656
|
}
|
|
4686
5657
|
replaceWorkspaceUsersForSync(input) {
|
|
4687
5658
|
const workspaceId = String(input.workspaceId || 'default').trim() || 'default';
|
|
@@ -4725,9 +5696,9 @@ export class TaskforceCore {
|
|
|
4725
5696
|
}
|
|
4726
5697
|
else {
|
|
4727
5698
|
this.db.prepare(`
|
|
4728
|
-
INSERT INTO users (id, tenant_id, email, display_name,
|
|
4729
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?,
|
|
4730
|
-
`).run(userId, this.tenantId, email, displayName,
|
|
5699
|
+
INSERT INTO users (id, tenant_id, email, display_name, beta_access, is_disabled, created_at, updated_at, email_verified_at)
|
|
5700
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
5701
|
+
`).run(userId, this.tenantId, email, displayName, betaAccess ? 1 : 0, disabled ? 1 : 0, createdAt, updatedAt, emailVerifiedAt);
|
|
4731
5702
|
}
|
|
4732
5703
|
const membership = this.db.prepare(`
|
|
4733
5704
|
SELECT id
|
|
@@ -4770,8 +5741,8 @@ export class TaskforceCore {
|
|
|
4770
5741
|
tx();
|
|
4771
5742
|
return this.listWorkspaceUsers(workspaceId);
|
|
4772
5743
|
}
|
|
4773
|
-
listAiProfiles(workspaceId) {
|
|
4774
|
-
return listAiProfilesService(this.getAiProfileServiceDeps(), workspaceId);
|
|
5744
|
+
listAiProfiles(workspaceId, options) {
|
|
5745
|
+
return listAiProfilesService(this.getAiProfileServiceDeps(), workspaceId, options);
|
|
4775
5746
|
}
|
|
4776
5747
|
getAiProfileByToken(input) {
|
|
4777
5748
|
return getAiProfileByTokenService(this.getAiProfileServiceDeps(), input);
|
|
@@ -4782,8 +5753,17 @@ export class TaskforceCore {
|
|
|
4782
5753
|
mergeAiProfiles(input) {
|
|
4783
5754
|
return mergeAiProfilesService(this.getAiProfileServiceDeps(), input);
|
|
4784
5755
|
}
|
|
4785
|
-
|
|
4786
|
-
return
|
|
5756
|
+
archiveAiProfile(input) {
|
|
5757
|
+
return archiveAiProfileService(this.getAiProfileServiceDeps(), input).profile;
|
|
5758
|
+
}
|
|
5759
|
+
updateAiProfileAvatar(input) {
|
|
5760
|
+
return updateAiProfileAvatarService(this.getAiProfileServiceDeps(), input);
|
|
5761
|
+
}
|
|
5762
|
+
updateAiProfileSurfaceType(input) {
|
|
5763
|
+
return updateAiProfileSurfaceTypeService(this.getAiProfileServiceDeps(), input);
|
|
5764
|
+
}
|
|
5765
|
+
resolveAiProfile(input, options) {
|
|
5766
|
+
return resolveAiProfileService(this.getAiProfileServiceDeps(), input, options);
|
|
4787
5767
|
}
|
|
4788
5768
|
listAnnotatedAttachmentSessions(input) {
|
|
4789
5769
|
return listAnnotatedAttachmentSessionsService(this.getAnnotatedAttachmentServiceDeps(), input);
|
|
@@ -4813,17 +5793,30 @@ export class TaskforceCore {
|
|
|
4813
5793
|
const activeSessions = deps.sessionStore.listByWorkspace('default', { includeDeleted: false });
|
|
4814
5794
|
if (activeSessions.length === 0)
|
|
4815
5795
|
return;
|
|
4816
|
-
const hasActiveImageLink = this.
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
5796
|
+
const hasActiveImageLink = this.supportsWorkspaceAssetLinkTargetType()
|
|
5797
|
+
? this.db.prepare(`
|
|
5798
|
+
SELECT 1
|
|
5799
|
+
FROM workspace_asset_links
|
|
5800
|
+
WHERE tenant_id = ?
|
|
5801
|
+
AND workspace_id = ?
|
|
5802
|
+
AND target_type = 'task'
|
|
5803
|
+
AND task_id = ?
|
|
5804
|
+
AND asset_id = ?
|
|
5805
|
+
AND role = 'image'
|
|
5806
|
+
AND deleted_at IS NULL
|
|
5807
|
+
LIMIT 1
|
|
5808
|
+
`)
|
|
5809
|
+
: this.db.prepare(`
|
|
5810
|
+
SELECT 1
|
|
5811
|
+
FROM workspace_asset_links
|
|
5812
|
+
WHERE tenant_id = ?
|
|
5813
|
+
AND workspace_id = ?
|
|
5814
|
+
AND task_id = ?
|
|
5815
|
+
AND asset_id = ?
|
|
5816
|
+
AND role = 'image'
|
|
5817
|
+
AND deleted_at IS NULL
|
|
5818
|
+
LIMIT 1
|
|
5819
|
+
`);
|
|
4827
5820
|
for (const session of activeSessions) {
|
|
4828
5821
|
const taskId = String(session.taskId || '').trim();
|
|
4829
5822
|
const imageAssetId = String(session.baseImageAssetId || '').trim();
|
|
@@ -4909,6 +5902,9 @@ export class TaskforceCore {
|
|
|
4909
5902
|
upsertTaskEventForSync(event) {
|
|
4910
5903
|
return upsertTaskEventForSyncService(this.getTaskActivityServiceDeps(), event);
|
|
4911
5904
|
}
|
|
5905
|
+
upsertEntityEventForSync(event) {
|
|
5906
|
+
return upsertEntityEventForSyncService(this.getTaskActivityServiceDeps(), event);
|
|
5907
|
+
}
|
|
4912
5908
|
deleteDocumentReviewSessionForSync(input) {
|
|
4913
5909
|
const deps = this.getDocumentReviewServiceDeps();
|
|
4914
5910
|
return deps.sessionStore.softDelete(input.sessionId, deps.normalizeWorkspaceId(input.workspaceId), input.deletedAt || undefined);
|
|
@@ -4976,21 +5972,9 @@ export class TaskforceCore {
|
|
|
4976
5972
|
if (aiProfile) {
|
|
4977
5973
|
return this.aiProfileToActor(this.aiProfileRowToRecord(aiProfile));
|
|
4978
5974
|
}
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
FROM ai_profiles
|
|
4983
|
-
WHERE tenant_id = ?
|
|
4984
|
-
AND workspace_id = ?
|
|
4985
|
-
AND (
|
|
4986
|
-
LOWER(name) = LOWER(?)
|
|
4987
|
-
OR LOWER(username) = LOWER(?)
|
|
4988
|
-
)
|
|
4989
|
-
ORDER BY COALESCE(last_active_at, updated_at, created_at) DESC, id ASC
|
|
4990
|
-
LIMIT 1
|
|
4991
|
-
`).get(this.tenantId, normalizedWorkspaceId, raw, raw);
|
|
4992
|
-
if (aiProfileByName) {
|
|
4993
|
-
return this.aiProfileToActor(this.aiProfileRowToRecord(aiProfileByName));
|
|
5975
|
+
const uniqueAiProfileByAlias = this.resolveUniqueAiProfileActorByAlias(normalizedWorkspaceId, raw);
|
|
5976
|
+
if (uniqueAiProfileByAlias) {
|
|
5977
|
+
return uniqueAiProfileByAlias;
|
|
4994
5978
|
}
|
|
4995
5979
|
const workspaceUser = this.listWorkspaceUsers(normalizedWorkspaceId).find((user) => user.userId === raw);
|
|
4996
5980
|
if (workspaceUser) {
|
|
@@ -5235,6 +6219,9 @@ export class TaskforceCore {
|
|
|
5235
6219
|
listPlanFeatureCatalog() {
|
|
5236
6220
|
return this.planEntitlementService.listPlanFeatureCatalog();
|
|
5237
6221
|
}
|
|
6222
|
+
updatePlanFeatureCatalogMarketingCopy(input) {
|
|
6223
|
+
return this.planEntitlementService.updatePlanFeatureCatalogMarketingCopy(input);
|
|
6224
|
+
}
|
|
5238
6225
|
listPlanVersions(planId) {
|
|
5239
6226
|
return this.planEntitlementService.listPlanVersions(planId);
|
|
5240
6227
|
}
|
|
@@ -5354,11 +6341,6 @@ export class TaskforceCore {
|
|
|
5354
6341
|
}
|
|
5355
6342
|
}
|
|
5356
6343
|
const now = new Date().toISOString();
|
|
5357
|
-
this.db.prepare(`
|
|
5358
|
-
UPDATE users
|
|
5359
|
-
SET is_disabled = ?, updated_at = ?
|
|
5360
|
-
WHERE tenant_id = ? AND id = ?
|
|
5361
|
-
`).run(disabled ? 1 : 0, now, this.tenantId, normalizedUserId);
|
|
5362
6344
|
if (membership?.id) {
|
|
5363
6345
|
this.db.prepare(`
|
|
5364
6346
|
UPDATE workspace_memberships
|
|
@@ -5532,9 +6514,7 @@ export class TaskforceCore {
|
|
|
5532
6514
|
if (!email || !email.includes('@')) {
|
|
5533
6515
|
throw new TaskforceRuleError('Valid email is required', 400, 'EMAIL_REQUIRED');
|
|
5534
6516
|
}
|
|
5535
|
-
|
|
5536
|
-
throw new TaskforceRuleError('Password must be at least 8 characters', 400, 'PASSWORD_TOO_SHORT');
|
|
5537
|
-
}
|
|
6517
|
+
assertPasswordPolicy(password, { email, displayName: input.displayName || null });
|
|
5538
6518
|
if (!workspaceId || workspaceId.toLowerCase() === 'default') {
|
|
5539
6519
|
throw new TaskforceRuleError('A non-default workspaceId is required', 400, 'WORKSPACE_ID_REQUIRED');
|
|
5540
6520
|
}
|
|
@@ -5551,20 +6531,16 @@ export class TaskforceCore {
|
|
|
5551
6531
|
email,
|
|
5552
6532
|
displayName
|
|
5553
6533
|
});
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
SET password_hash = ?, auth_provider = ?, external_auth_id = ?, beta_access = ?, is_disabled = 0, updated_at = ?
|
|
5565
|
-
WHERE tenant_id = ? AND id = ?
|
|
5566
|
-
`).run(hashPassword(password), 'password', email, betaAccess ? 1 : 0, now, this.tenantId, userId);
|
|
5567
|
-
}
|
|
6534
|
+
this.db.prepare(`
|
|
6535
|
+
UPDATE users
|
|
6536
|
+
SET
|
|
6537
|
+
email_verified_at = CASE WHEN ? = 1 THEN COALESCE(email_verified_at, ?) ELSE email_verified_at END,
|
|
6538
|
+
beta_access = ?,
|
|
6539
|
+
is_disabled = 0,
|
|
6540
|
+
updated_at = ?
|
|
6541
|
+
WHERE tenant_id = ? AND id = ?
|
|
6542
|
+
`).run(emailVerified ? 1 : 0, now, betaAccess ? 1 : 0, now, this.tenantId, userId);
|
|
6543
|
+
this.identityService.setPasswordIdentity(userId, password);
|
|
5568
6544
|
this.updateWorkspaceUserRole(userId, workspaceId, role);
|
|
5569
6545
|
this.setWorkspaceUserDisabled(userId, workspaceId, false);
|
|
5570
6546
|
return { userId, workspaceId, role };
|
|
@@ -5575,9 +6551,7 @@ export class TaskforceCore {
|
|
|
5575
6551
|
if (!email || !email.includes('@')) {
|
|
5576
6552
|
throw new TaskforceRuleError('Valid email is required', 400, 'EMAIL_REQUIRED');
|
|
5577
6553
|
}
|
|
5578
|
-
|
|
5579
|
-
throw new TaskforceRuleError('Password must be at least 8 characters', 400, 'PASSWORD_TOO_SHORT');
|
|
5580
|
-
}
|
|
6554
|
+
assertPasswordPolicy(password, { email, displayName: input.displayName || null });
|
|
5581
6555
|
const existing = this.db.prepare(`
|
|
5582
6556
|
SELECT id FROM users WHERE tenant_id = ? AND email = ? LIMIT 1
|
|
5583
6557
|
`).get(this.tenantId, email);
|
|
@@ -5606,9 +6580,10 @@ export class TaskforceCore {
|
|
|
5606
6580
|
const userId = this.generateEntityId('user');
|
|
5607
6581
|
const displayName = String(input.displayName || email.split('@')[0] || email).trim() || email;
|
|
5608
6582
|
this.db.prepare(`
|
|
5609
|
-
INSERT INTO users (id, tenant_id, email, display_name,
|
|
5610
|
-
VALUES (?, ?, ?, ?, ?, ?,
|
|
5611
|
-
`).run(userId, this.tenantId, email, displayName,
|
|
6583
|
+
INSERT INTO users (id, tenant_id, email, display_name, system_role, beta_access, is_disabled, created_at, updated_at)
|
|
6584
|
+
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)
|
|
6585
|
+
`).run(userId, this.tenantId, email, displayName, assignFirstSystemAdmin ? 'system_admin' : 'user', betaAccess ? 1 : 0, now, now);
|
|
6586
|
+
this.identityService.createPasswordIdentity(userId, password);
|
|
5612
6587
|
applyRegistrationCommercialState(userId, now);
|
|
5613
6588
|
return {
|
|
5614
6589
|
userId,
|
|
@@ -5642,6 +6617,151 @@ export class TaskforceCore {
|
|
|
5642
6617
|
commercialState: onboardingOutcome.commercialState
|
|
5643
6618
|
};
|
|
5644
6619
|
}
|
|
6620
|
+
// ─── OAuth state management (delegated to AuthIdentityService) ────────────
|
|
6621
|
+
/**
|
|
6622
|
+
* Creates an OAuth state record and returns the state token + PKCE materials.
|
|
6623
|
+
* Delegates to AuthIdentityService.createOAuthState.
|
|
6624
|
+
*/
|
|
6625
|
+
createOAuthLoginState(input) {
|
|
6626
|
+
return this.identityService.createOAuthState(input);
|
|
6627
|
+
}
|
|
6628
|
+
/**
|
|
6629
|
+
* Atomically consumes an OAuth state token and returns the row.
|
|
6630
|
+
* Returns null if invalid, expired, or already consumed.
|
|
6631
|
+
*/
|
|
6632
|
+
consumeOAuthLoginState(state) {
|
|
6633
|
+
return this.identityService.consumeOAuthState(state);
|
|
6634
|
+
}
|
|
6635
|
+
// ─── OAuth identity operations ─────────────────────────────────────────────
|
|
6636
|
+
/**
|
|
6637
|
+
* Finds the Taskforce user associated with a provider identity.
|
|
6638
|
+
* Returns { userId, identityId } or null if not yet linked.
|
|
6639
|
+
*/
|
|
6640
|
+
findUserByOAuthProvider(provider, providerSubject) {
|
|
6641
|
+
return this.identityService.findUserByProviderIdentity({ provider, providerSubject });
|
|
6642
|
+
}
|
|
6643
|
+
/**
|
|
6644
|
+
* Links an OAuth provider identity to an existing user.
|
|
6645
|
+
* Throws IDENTITY_ALREADY_EXISTS if the user already has an identity for this provider.
|
|
6646
|
+
*/
|
|
6647
|
+
linkOAuthProviderToUser(input) {
|
|
6648
|
+
return this.identityService.linkProviderIdentity(input);
|
|
6649
|
+
}
|
|
6650
|
+
/**
|
|
6651
|
+
* Lists all active login identities for a user.
|
|
6652
|
+
* Used by the Account Hub "Login methods" section.
|
|
6653
|
+
*/
|
|
6654
|
+
listLoginMethodsForUser(userId) {
|
|
6655
|
+
return this.identityService.listIdentitiesForUser(userId);
|
|
6656
|
+
}
|
|
6657
|
+
/**
|
|
6658
|
+
* Unlinks a provider identity from a user.
|
|
6659
|
+
* Throws LAST_IDENTITY_REQUIRED if it would be the last active method.
|
|
6660
|
+
*/
|
|
6661
|
+
unlinkLoginMethod(userId, provider) {
|
|
6662
|
+
return this.identityService.unlinkProviderIdentity({ userId, provider });
|
|
6663
|
+
}
|
|
6664
|
+
/**
|
|
6665
|
+
* Adds or replaces a password identity for a user that signed up via OAuth.
|
|
6666
|
+
* Uses upsert semantics (safe to call on accounts that already have a password).
|
|
6667
|
+
*/
|
|
6668
|
+
addPasswordToAccount(userId, password) {
|
|
6669
|
+
const user = this.db.prepare(`
|
|
6670
|
+
SELECT email, display_name
|
|
6671
|
+
FROM users
|
|
6672
|
+
WHERE tenant_id = ? AND id = ?
|
|
6673
|
+
LIMIT 1
|
|
6674
|
+
`).get(this.tenantId, String(userId || '').trim());
|
|
6675
|
+
assertPasswordPolicy(password, {
|
|
6676
|
+
email: user?.email || null,
|
|
6677
|
+
displayName: user?.display_name || null
|
|
6678
|
+
});
|
|
6679
|
+
return this.identityService.setPasswordIdentity(userId, password);
|
|
6680
|
+
}
|
|
6681
|
+
/**
|
|
6682
|
+
* Registers a new user account via an OAuth provider (no password).
|
|
6683
|
+
*
|
|
6684
|
+
* Creates the user row, applies commercial onboarding, and links the
|
|
6685
|
+
* provider identity — all in a single transaction.
|
|
6686
|
+
*
|
|
6687
|
+
* Returns the same shape as `registerPasswordUser` so callers can use
|
|
6688
|
+
* the same session-issuance path.
|
|
6689
|
+
*/
|
|
6690
|
+
registerOAuthUser(input) {
|
|
6691
|
+
const email = input.email ? String(input.email).trim().toLowerCase() : null;
|
|
6692
|
+
if (email && !email.includes('@')) {
|
|
6693
|
+
throw new TaskforceRuleError('Valid email is required', 400, 'EMAIL_REQUIRED');
|
|
6694
|
+
}
|
|
6695
|
+
// Duplicate guard — email uniqueness (if email is provided)
|
|
6696
|
+
if (email) {
|
|
6697
|
+
const existing = this.db.prepare(`
|
|
6698
|
+
SELECT id FROM users WHERE tenant_id = ? AND email = ? LIMIT 1
|
|
6699
|
+
`).get(this.tenantId, email);
|
|
6700
|
+
if (existing?.id) {
|
|
6701
|
+
throw new TaskforceRuleError('Email is already registered', 409, 'EMAIL_ALREADY_EXISTS');
|
|
6702
|
+
}
|
|
6703
|
+
}
|
|
6704
|
+
const assignFirstSystemAdmin = !this.hasAnySystemAdmin();
|
|
6705
|
+
const betaAccess = input.betaAccess !== false;
|
|
6706
|
+
const onboardingOutcome = this.resolveRegistrationOnboardingOutcome(input.planVersionId);
|
|
6707
|
+
const now = new Date().toISOString();
|
|
6708
|
+
const userId = this.generateEntityId('user');
|
|
6709
|
+
const displayName = String(input.displayName || (email ? email.split('@')[0] : '') || '').trim()
|
|
6710
|
+
|| (email || userId);
|
|
6711
|
+
this.db.prepare(`
|
|
6712
|
+
INSERT INTO users
|
|
6713
|
+
(id, tenant_id, email, email_verified_at, display_name, system_role,
|
|
6714
|
+
beta_access, is_disabled, created_at, updated_at)
|
|
6715
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?)
|
|
6716
|
+
`).run(userId, this.tenantId, email,
|
|
6717
|
+
// OAuth providers that attest email_verified give us a verified email up-front
|
|
6718
|
+
(email && input.emailVerified) ? now : null, displayName, assignFirstSystemAdmin ? 'system_admin' : 'user', betaAccess ? 1 : 0, now, now);
|
|
6719
|
+
const identityId = this.identityService.linkProviderIdentity({
|
|
6720
|
+
userId,
|
|
6721
|
+
provider: input.provider,
|
|
6722
|
+
providerSubject: input.providerSubject,
|
|
6723
|
+
providerEmail: email,
|
|
6724
|
+
providerEmailVerified: input.emailVerified ?? null,
|
|
6725
|
+
displayNameSnapshot: input.displayName ?? null,
|
|
6726
|
+
avatarUrlSnapshot: input.avatarUrl ?? null,
|
|
6727
|
+
});
|
|
6728
|
+
this.transitionCanonicalCommercialState({
|
|
6729
|
+
accountId: userId,
|
|
6730
|
+
planVersionId: onboardingOutcome.planVersionId,
|
|
6731
|
+
state: onboardingOutcome.commercialState,
|
|
6732
|
+
billingMirrorPlanId: onboardingOutcome.planId,
|
|
6733
|
+
source: 'registration',
|
|
6734
|
+
updatedBy: 'registration',
|
|
6735
|
+
createdAt: now,
|
|
6736
|
+
updatedAt: now
|
|
6737
|
+
});
|
|
6738
|
+
return {
|
|
6739
|
+
userId,
|
|
6740
|
+
workspaceId: null,
|
|
6741
|
+
role: 'member',
|
|
6742
|
+
identityId,
|
|
6743
|
+
planSelectionRequired: onboardingOutcome.requiresPlanSelection,
|
|
6744
|
+
checkoutPending: onboardingOutcome.checkoutPending,
|
|
6745
|
+
commercialState: onboardingOutcome.commercialState,
|
|
6746
|
+
};
|
|
6747
|
+
}
|
|
6748
|
+
/**
|
|
6749
|
+
* Looks up a user by email address (exact, case-insensitive).
|
|
6750
|
+
* Returns { userId, email } or null.
|
|
6751
|
+
*/
|
|
6752
|
+
findUserByEmail(email) {
|
|
6753
|
+
const normalized = String(email || '').trim().toLowerCase();
|
|
6754
|
+
if (!normalized)
|
|
6755
|
+
return null;
|
|
6756
|
+
const row = this.db.prepare(`
|
|
6757
|
+
SELECT id, email FROM users
|
|
6758
|
+
WHERE tenant_id = ? AND email = ? AND COALESCE(is_disabled, 0) = 0
|
|
6759
|
+
LIMIT 1
|
|
6760
|
+
`).get(this.tenantId, normalized);
|
|
6761
|
+
if (!row?.id)
|
|
6762
|
+
return null;
|
|
6763
|
+
return { userId: row.id, email: row.email || normalized };
|
|
6764
|
+
}
|
|
5645
6765
|
createOneTimeAuthToken(userId, purpose, ttlSeconds, workspaceId) {
|
|
5646
6766
|
return this.authTokenService.createOneTimeAuthToken(userId, purpose, ttlSeconds, workspaceId);
|
|
5647
6767
|
}
|
|
@@ -5667,6 +6787,9 @@ export class TaskforceCore {
|
|
|
5667
6787
|
revokeMcpAccessToken(input) {
|
|
5668
6788
|
return this.mcpTokenService.revokeMcpAccessToken(input);
|
|
5669
6789
|
}
|
|
6790
|
+
deleteMcpAccessToken(input) {
|
|
6791
|
+
return this.mcpTokenService.deleteMcpAccessToken(input);
|
|
6792
|
+
}
|
|
5670
6793
|
regenerateMcpAccessToken(input) {
|
|
5671
6794
|
return this.mcpTokenService.regenerateMcpAccessToken(input);
|
|
5672
6795
|
}
|
|
@@ -5700,6 +6823,9 @@ export class TaskforceCore {
|
|
|
5700
6823
|
authenticateMcpOAuthAccessToken(token, workspaceId) {
|
|
5701
6824
|
return this.mcpTokenService.authenticateMcpOAuthAccessToken(token, workspaceId);
|
|
5702
6825
|
}
|
|
6826
|
+
bindMcpOAuthConnectorGrantAiProfile(input) {
|
|
6827
|
+
return this.mcpTokenService.bindMcpOAuthConnectorGrantAiProfile(input);
|
|
6828
|
+
}
|
|
5703
6829
|
recordMcpOAuthConnectorGrantUsage(input) {
|
|
5704
6830
|
return this.mcpTokenService.recordMcpOAuthConnectorGrantUsage(input);
|
|
5705
6831
|
}
|
|
@@ -5744,19 +6870,23 @@ export class TaskforceCore {
|
|
|
5744
6870
|
if (!inspection.userId)
|
|
5745
6871
|
return inspection;
|
|
5746
6872
|
const user = this.db.prepare(`
|
|
5747
|
-
SELECT email
|
|
5748
|
-
FROM users
|
|
5749
|
-
WHERE tenant_id = ? AND id = ? LIMIT 1
|
|
6873
|
+
SELECT email FROM users WHERE tenant_id = ? AND id = ? LIMIT 1
|
|
5750
6874
|
`).get(this.tenantId, inspection.userId);
|
|
5751
6875
|
const workspaceId = String(inspection.workspaceId || '').trim();
|
|
5752
|
-
const
|
|
6876
|
+
const hasIdentity = this.identityService.hasAnyActiveIdentity(inspection.userId);
|
|
6877
|
+
const inviteeState = hasIdentity ? 'existing_account' : 'pending_setup';
|
|
6878
|
+
const identities = this.identityService.listIdentitiesForUser(inspection.userId);
|
|
6879
|
+
const availableMethods = identities.map(i => i.provider);
|
|
5753
6880
|
return {
|
|
5754
6881
|
state: inspection.state,
|
|
5755
6882
|
email: user?.email,
|
|
5756
6883
|
userId: inspection.userId,
|
|
5757
6884
|
workspaceId: workspaceId || 'default',
|
|
5758
|
-
|
|
5759
|
-
|
|
6885
|
+
inviteeState,
|
|
6886
|
+
availableMethods,
|
|
6887
|
+
// Backward-compatible fields
|
|
6888
|
+
inviteeKind: hasIdentity ? 'existing_user' : 'new_user',
|
|
6889
|
+
passwordRequired: !hasIdentity
|
|
5760
6890
|
};
|
|
5761
6891
|
}
|
|
5762
6892
|
completeInviteWorkspaceActivation(input) {
|
|
@@ -5813,14 +6943,20 @@ export class TaskforceCore {
|
|
|
5813
6943
|
role: this.normalizeAccessRole(membership?.role || 'member', 'member')
|
|
5814
6944
|
};
|
|
5815
6945
|
}
|
|
5816
|
-
|
|
6946
|
+
/**
|
|
6947
|
+
* Claims an invite by linking an OAuth provider identity to the invited user.
|
|
6948
|
+
*
|
|
6949
|
+
* Used when an invited user with `inviteeState: 'pending_setup'` completes an
|
|
6950
|
+
* OAuth flow with `flow_mode: 'invite_claim'`. The invite token must still be
|
|
6951
|
+
* valid and the user must have zero active identities (pending setup).
|
|
6952
|
+
*
|
|
6953
|
+
* Returns the same shape as `acceptInviteWithToken` so callers can issue a
|
|
6954
|
+
* session cookie on the same path.
|
|
6955
|
+
*/
|
|
6956
|
+
claimInviteWithOAuthProvider(input) {
|
|
5817
6957
|
const token = String(input.token || '').trim();
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
throw new TaskforceRuleError('Token and password are required', 400, 'TOKEN_PASSWORD_REQUIRED');
|
|
5821
|
-
}
|
|
5822
|
-
if (password.length < 8) {
|
|
5823
|
-
throw new TaskforceRuleError('Password must be at least 8 characters', 400, 'PASSWORD_TOO_SHORT');
|
|
6958
|
+
if (!token) {
|
|
6959
|
+
throw new TaskforceRuleError('Invite token is required', 400, 'TOKEN_REQUIRED');
|
|
5824
6960
|
}
|
|
5825
6961
|
const consumed = this.consumeOneTimeAuthToken(token, 'invite_accept');
|
|
5826
6962
|
if (!consumed?.userId) {
|
|
@@ -5841,38 +6977,111 @@ export class TaskforceCore {
|
|
|
5841
6977
|
throw new TaskforceRuleError('Invite token is missing workspace context', 400, 'INVITE_WORKSPACE_REQUIRED');
|
|
5842
6978
|
}
|
|
5843
6979
|
const user = this.db.prepare(`
|
|
5844
|
-
SELECT id, email
|
|
6980
|
+
SELECT id, email FROM users WHERE tenant_id = ? AND id = ? LIMIT 1
|
|
6981
|
+
`).get(this.tenantId, consumed.userId);
|
|
6982
|
+
if (!user?.id || !user.email) {
|
|
6983
|
+
throw new TaskforceRuleError('User not found', 404, 'USER_NOT_FOUND');
|
|
6984
|
+
}
|
|
6985
|
+
if (this.identityService.hasAnyActiveIdentity(consumed.userId)) {
|
|
6986
|
+
throw new TaskforceRuleError('Existing users must sign in before joining this workspace', 409, 'INVITE_SIGN_IN_REQUIRED');
|
|
6987
|
+
}
|
|
6988
|
+
const now = new Date().toISOString();
|
|
6989
|
+
const identityId = this.identityService.linkProviderIdentity({
|
|
6990
|
+
userId: consumed.userId,
|
|
6991
|
+
provider: input.provider,
|
|
6992
|
+
providerSubject: input.providerSubject,
|
|
6993
|
+
providerEmail: input.providerEmail ?? null,
|
|
6994
|
+
providerEmailVerified: input.providerEmailVerified ?? null,
|
|
6995
|
+
displayNameSnapshot: input.displayNameSnapshot ?? null,
|
|
6996
|
+
avatarUrlSnapshot: input.avatarUrlSnapshot ?? null,
|
|
6997
|
+
});
|
|
6998
|
+
// Mark email verified if the provider attested it
|
|
6999
|
+
if (input.providerEmailVerified) {
|
|
7000
|
+
this.db.prepare(`
|
|
7001
|
+
UPDATE users
|
|
7002
|
+
SET email_verified_at = COALESCE(email_verified_at, ?), is_disabled = 0, updated_at = ?
|
|
7003
|
+
WHERE tenant_id = ? AND id = ?
|
|
7004
|
+
`).run(now, now, this.tenantId, consumed.userId);
|
|
7005
|
+
}
|
|
7006
|
+
else {
|
|
7007
|
+
this.db.prepare(`
|
|
7008
|
+
UPDATE users SET is_disabled = 0, updated_at = ? WHERE tenant_id = ? AND id = ?
|
|
7009
|
+
`).run(now, this.tenantId, consumed.userId);
|
|
7010
|
+
}
|
|
7011
|
+
this.ensureDefaultOnboardingEntitlement(consumed.userId, {
|
|
7012
|
+
source: 'invite-accept',
|
|
7013
|
+
updatedBy: 'invite-accept',
|
|
7014
|
+
now
|
|
7015
|
+
});
|
|
7016
|
+
const activation = this.completeInviteWorkspaceActivation({
|
|
7017
|
+
userId: consumed.userId,
|
|
7018
|
+
workspaceId: inviteWorkspaceId
|
|
7019
|
+
});
|
|
7020
|
+
return { ...activation, identityId };
|
|
7021
|
+
}
|
|
7022
|
+
acceptInviteWithToken(input) {
|
|
7023
|
+
const token = String(input.token || '').trim();
|
|
7024
|
+
const password = String(input.password || '');
|
|
7025
|
+
if (!token || !password) {
|
|
7026
|
+
throw new TaskforceRuleError('Token and password are required', 400, 'TOKEN_PASSWORD_REQUIRED');
|
|
7027
|
+
}
|
|
7028
|
+
const inspected = this.inspectOneTimeAuthToken(token, 'invite_accept');
|
|
7029
|
+
if (inspected.state !== 'valid' || !inspected.userId) {
|
|
7030
|
+
if (inspected.state === 'expired') {
|
|
7031
|
+
throw new TaskforceRuleError('Invite token expired', 400, 'INVITE_TOKEN_EXPIRED');
|
|
7032
|
+
}
|
|
7033
|
+
if (inspected.state === 'revoked') {
|
|
7034
|
+
throw new TaskforceRuleError('Invite token revoked', 400, 'INVITE_TOKEN_REVOKED');
|
|
7035
|
+
}
|
|
7036
|
+
if (inspected.state === 'used') {
|
|
7037
|
+
throw new TaskforceRuleError('Invite token already used', 400, 'INVITE_TOKEN_USED');
|
|
7038
|
+
}
|
|
7039
|
+
throw new TaskforceRuleError('Invalid invite token', 400, 'INVITE_TOKEN_INVALID');
|
|
7040
|
+
}
|
|
7041
|
+
const inviteWorkspaceId = String(inspected.workspaceId || '').trim();
|
|
7042
|
+
if (!inviteWorkspaceId) {
|
|
7043
|
+
throw new TaskforceRuleError('Invite token is missing workspace context', 400, 'INVITE_WORKSPACE_REQUIRED');
|
|
7044
|
+
}
|
|
7045
|
+
const user = this.db.prepare(`
|
|
7046
|
+
SELECT id, email, display_name
|
|
5845
7047
|
FROM users
|
|
5846
7048
|
WHERE tenant_id = ? AND id = ?
|
|
5847
7049
|
LIMIT 1
|
|
5848
|
-
`).get(this.tenantId,
|
|
7050
|
+
`).get(this.tenantId, inspected.userId);
|
|
5849
7051
|
if (!user?.id || !user.email) {
|
|
5850
7052
|
throw new TaskforceRuleError('User not found', 404, 'USER_NOT_FOUND');
|
|
5851
7053
|
}
|
|
5852
|
-
|
|
7054
|
+
assertPasswordPolicy(password, {
|
|
7055
|
+
email: user.email,
|
|
7056
|
+
displayName: user.display_name || null
|
|
7057
|
+
});
|
|
7058
|
+
if (this.identityService.hasAnyActiveIdentity(inspected.userId)) {
|
|
5853
7059
|
throw new TaskforceRuleError('Existing users must sign in before joining this workspace', 409, 'INVITE_SIGN_IN_REQUIRED');
|
|
5854
7060
|
}
|
|
7061
|
+
const consumed = this.consumeOneTimeAuthToken(token, 'invite_accept');
|
|
7062
|
+
if (!consumed?.userId) {
|
|
7063
|
+
throw new TaskforceRuleError('Invalid invite token', 400, 'INVITE_TOKEN_INVALID');
|
|
7064
|
+
}
|
|
5855
7065
|
const now = new Date().toISOString();
|
|
7066
|
+
const identityId = this.identityService.setPasswordIdentity(consumed.userId, password);
|
|
5856
7067
|
this.db.prepare(`
|
|
5857
7068
|
UPDATE users
|
|
5858
|
-
SET
|
|
5859
|
-
password_hash = ?,
|
|
5860
|
-
auth_provider = ?,
|
|
5861
|
-
external_auth_id = ?,
|
|
5862
|
-
email_verified_at = COALESCE(email_verified_at, ?),
|
|
5863
|
-
is_disabled = 0,
|
|
5864
|
-
updated_at = ?
|
|
7069
|
+
SET email_verified_at = COALESCE(email_verified_at, ?), is_disabled = 0, updated_at = ?
|
|
5865
7070
|
WHERE tenant_id = ? AND id = ?
|
|
5866
|
-
`).run(
|
|
7071
|
+
`).run(now, now, this.tenantId, consumed.userId);
|
|
5867
7072
|
this.ensureDefaultOnboardingEntitlement(consumed.userId, {
|
|
5868
7073
|
source: 'invite-accept',
|
|
5869
7074
|
updatedBy: 'invite-accept',
|
|
5870
7075
|
now
|
|
5871
7076
|
});
|
|
5872
|
-
|
|
7077
|
+
const activation = this.completeInviteWorkspaceActivation({
|
|
5873
7078
|
userId: consumed.userId,
|
|
5874
7079
|
workspaceId: inviteWorkspaceId
|
|
5875
7080
|
});
|
|
7081
|
+
return {
|
|
7082
|
+
...activation,
|
|
7083
|
+
identityId
|
|
7084
|
+
};
|
|
5876
7085
|
}
|
|
5877
7086
|
joinInviteWithAuthenticatedUser(input) {
|
|
5878
7087
|
const token = String(input.token || '').trim();
|
|
@@ -5896,7 +7105,7 @@ export class TaskforceCore {
|
|
|
5896
7105
|
}
|
|
5897
7106
|
throw new TaskforceRuleError('Invalid invite token', 400, 'INVITE_TOKEN_INVALID');
|
|
5898
7107
|
}
|
|
5899
|
-
if (details.
|
|
7108
|
+
if (details.inviteeState !== 'existing_account') {
|
|
5900
7109
|
throw new TaskforceRuleError('This invite requires account setup before joining', 409, 'INVITE_PASSWORD_SETUP_REQUIRED');
|
|
5901
7110
|
}
|
|
5902
7111
|
if (details.userId !== authenticatedUserId) {
|
|
@@ -5930,32 +7139,31 @@ export class TaskforceCore {
|
|
|
5930
7139
|
const workspaceId = String(input.workspaceId || 'default').trim() || 'default';
|
|
5931
7140
|
if (!email || !password)
|
|
5932
7141
|
return { ok: false, code: 'INVALID_CREDENTIALS' };
|
|
5933
|
-
const
|
|
5934
|
-
|
|
5935
|
-
FROM users
|
|
5936
|
-
WHERE tenant_id = ? AND email = ?
|
|
5937
|
-
LIMIT 1
|
|
5938
|
-
`).get(this.tenantId, email);
|
|
5939
|
-
if (!row?.id || !row.password_hash)
|
|
5940
|
-
return { ok: false, code: 'INVALID_CREDENTIALS' };
|
|
5941
|
-
if (!verifyPassword(password, row.password_hash))
|
|
7142
|
+
const authResult = this.identityService.authenticatePasswordIdentity(email, password);
|
|
7143
|
+
if (!authResult)
|
|
5942
7144
|
return { ok: false, code: 'INVALID_CREDENTIALS' };
|
|
5943
|
-
if (input.requireVerifiedEmail
|
|
5944
|
-
|
|
7145
|
+
if (input.requireVerifiedEmail) {
|
|
7146
|
+
const userRow = this.db.prepare(`
|
|
7147
|
+
SELECT email_verified_at FROM users WHERE tenant_id = ? AND id = ? LIMIT 1
|
|
7148
|
+
`).get(this.tenantId, authResult.userId);
|
|
7149
|
+
if (!userRow?.email_verified_at) {
|
|
7150
|
+
return { ok: false, code: 'EMAIL_NOT_VERIFIED' };
|
|
7151
|
+
}
|
|
5945
7152
|
}
|
|
5946
|
-
const resolvedWorkspaceId = this.resolvePreferredUserWorkspaceId(
|
|
7153
|
+
const resolvedWorkspaceId = this.resolvePreferredUserWorkspaceId(authResult.userId, {
|
|
5947
7154
|
preferredWorkspaceId: workspaceId
|
|
5948
7155
|
}) || workspaceId;
|
|
5949
|
-
const access = this.resolveUserAccess(
|
|
7156
|
+
const access = this.resolveUserAccess(authResult.userId, resolvedWorkspaceId, input.fallbackRole || 'member');
|
|
5950
7157
|
if (!access) {
|
|
5951
7158
|
// Allow authenticated users without workspace memberships.
|
|
5952
7159
|
// Provisioning flow will create/switch to a workspace after login.
|
|
5953
7160
|
return {
|
|
5954
7161
|
ok: true,
|
|
5955
7162
|
access: {
|
|
5956
|
-
userId:
|
|
7163
|
+
userId: authResult.userId,
|
|
5957
7164
|
workspaceId: 'default',
|
|
5958
|
-
role: this.normalizeAccessRole(input.fallbackRole || 'member', 'member')
|
|
7165
|
+
role: this.normalizeAccessRole(input.fallbackRole || 'member', 'member'),
|
|
7166
|
+
identityId: authResult.identityId
|
|
5959
7167
|
}
|
|
5960
7168
|
};
|
|
5961
7169
|
}
|
|
@@ -5964,7 +7172,8 @@ export class TaskforceCore {
|
|
|
5964
7172
|
access: {
|
|
5965
7173
|
userId: access.userId,
|
|
5966
7174
|
workspaceId: access.workspaceId,
|
|
5967
|
-
role: access.role
|
|
7175
|
+
role: access.role,
|
|
7176
|
+
identityId: authResult.identityId
|
|
5968
7177
|
}
|
|
5969
7178
|
};
|
|
5970
7179
|
}
|
|
@@ -6027,9 +7236,47 @@ export class TaskforceCore {
|
|
|
6027
7236
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
6028
7237
|
return this.listTaskEventsInternal(taskId, normalizedWorkspaceId);
|
|
6029
7238
|
}
|
|
7239
|
+
taskExistsForSync(id, workspaceId) {
|
|
7240
|
+
return this.taskExists(id, this.normalizeWorkspaceId(workspaceId || undefined));
|
|
7241
|
+
}
|
|
7242
|
+
initiativeExistsForSync(id, workspaceId) {
|
|
7243
|
+
return this.initiativeExists(id, this.normalizeWorkspaceId(workspaceId || undefined));
|
|
7244
|
+
}
|
|
7245
|
+
workstreamExistsForSync(id, workspaceId) {
|
|
7246
|
+
return this.workstreamExists(id, this.normalizeWorkspaceId(workspaceId || undefined));
|
|
7247
|
+
}
|
|
7248
|
+
recordTaskEvent(input) {
|
|
7249
|
+
this.addTaskEvent({
|
|
7250
|
+
taskId: input.taskId,
|
|
7251
|
+
workspaceId: this.normalizeWorkspaceId(input.workspaceId || undefined),
|
|
7252
|
+
action: input.action,
|
|
7253
|
+
actor: input.actor || 'system',
|
|
7254
|
+
createdAt: input.createdAt,
|
|
7255
|
+
details: input.details,
|
|
7256
|
+
});
|
|
7257
|
+
}
|
|
6030
7258
|
listTaskEventsForSync(input) {
|
|
6031
7259
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(input.workspaceId || undefined);
|
|
6032
|
-
return
|
|
7260
|
+
return listWorkspaceTaskEventsForSyncService(this.getTaskActivityServiceDeps(), normalizedWorkspaceId);
|
|
7261
|
+
}
|
|
7262
|
+
getTaskEventForSync(input) {
|
|
7263
|
+
const normalizedWorkspaceId = this.normalizeWorkspaceId(input.workspaceId || undefined);
|
|
7264
|
+
return getTaskEventForSyncService(this.getTaskActivityServiceDeps(), {
|
|
7265
|
+
workspaceId: normalizedWorkspaceId,
|
|
7266
|
+
taskId: input.taskId,
|
|
7267
|
+
eventId: input.eventId,
|
|
7268
|
+
});
|
|
7269
|
+
}
|
|
7270
|
+
listEntityEventsForSync(input) {
|
|
7271
|
+
const normalizedWorkspaceId = this.normalizeWorkspaceId(input.workspaceId || undefined);
|
|
7272
|
+
return listWorkspaceEntityEventsForSyncService(this.getTaskActivityServiceDeps(), normalizedWorkspaceId);
|
|
7273
|
+
}
|
|
7274
|
+
getEntityEventForSync(input) {
|
|
7275
|
+
const normalizedWorkspaceId = this.normalizeWorkspaceId(input.workspaceId || undefined);
|
|
7276
|
+
return getEntityEventForSyncService(this.getTaskActivityServiceDeps(), {
|
|
7277
|
+
workspaceId: normalizedWorkspaceId,
|
|
7278
|
+
eventId: input.eventId,
|
|
7279
|
+
});
|
|
6033
7280
|
}
|
|
6034
7281
|
getScheduleBoard(weekKey, showWeekends = false, workspaceId) {
|
|
6035
7282
|
const parsedWeek = this.parseIsoWeekKeyToDates(weekKey);
|
|
@@ -6293,6 +7540,16 @@ export class TaskforceCore {
|
|
|
6293
7540
|
workstreamId: task.workstreamId || null
|
|
6294
7541
|
}
|
|
6295
7542
|
});
|
|
7543
|
+
if (task.workstreamId) {
|
|
7544
|
+
this.addWorkstreamTaskMembershipEvents({
|
|
7545
|
+
task,
|
|
7546
|
+
workspaceId: normalizedWorkspaceId,
|
|
7547
|
+
actor: options?.actorRef || createdBy || 'system',
|
|
7548
|
+
createdAt: task.updatedAt || task.createdAt,
|
|
7549
|
+
previousWorkstreamId: null,
|
|
7550
|
+
nextWorkstreamId: task.workstreamId,
|
|
7551
|
+
});
|
|
7552
|
+
}
|
|
6296
7553
|
}
|
|
6297
7554
|
if (persistBackup) {
|
|
6298
7555
|
this.persistBackup();
|
|
@@ -6614,6 +7871,16 @@ export class TaskforceCore {
|
|
|
6614
7871
|
createdAt: nextTask.updatedAt,
|
|
6615
7872
|
details: { changes }
|
|
6616
7873
|
});
|
|
7874
|
+
if (changes.workstreamId) {
|
|
7875
|
+
this.addWorkstreamTaskMembershipEvents({
|
|
7876
|
+
task: nextTask,
|
|
7877
|
+
workspaceId: normalizedWorkspaceId,
|
|
7878
|
+
actor: options?.actorRef || 'system',
|
|
7879
|
+
createdAt: nextTask.updatedAt,
|
|
7880
|
+
previousWorkstreamId: existing.workstreamId || null,
|
|
7881
|
+
nextWorkstreamId: nextTask.workstreamId || null,
|
|
7882
|
+
});
|
|
7883
|
+
}
|
|
6617
7884
|
}
|
|
6618
7885
|
}
|
|
6619
7886
|
return this.getTask(taskId, normalizedWorkspaceId);
|
|
@@ -6633,7 +7900,11 @@ export class TaskforceCore {
|
|
|
6633
7900
|
}
|
|
6634
7901
|
addComment(id, text, author, workspaceId, options) {
|
|
6635
7902
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
6636
|
-
const existing = this.db.prepare(
|
|
7903
|
+
const existing = this.db.prepare(`
|
|
7904
|
+
SELECT id, updated_at, created_at
|
|
7905
|
+
FROM tasks
|
|
7906
|
+
WHERE tenant_id = ? AND workspace_id = ? AND id = ?
|
|
7907
|
+
`).get(this.tenantId, normalizedWorkspaceId, id);
|
|
6637
7908
|
if (!existing)
|
|
6638
7909
|
return null;
|
|
6639
7910
|
const submittedAuthor = String(author || '').trim() || 'user';
|
|
@@ -6658,7 +7929,13 @@ export class TaskforceCore {
|
|
|
6658
7929
|
submitted_author: comment.submittedAuthor,
|
|
6659
7930
|
timestamp: comment.timestamp
|
|
6660
7931
|
});
|
|
6661
|
-
|
|
7932
|
+
const existingWatermark = String(existing.updated_at || existing.created_at || '').trim();
|
|
7933
|
+
const existingWatermarkMs = Date.parse(existingWatermark);
|
|
7934
|
+
const commentTimestampMs = Date.parse(comment.timestamp);
|
|
7935
|
+
const nextTaskUpdatedAt = Number.isFinite(existingWatermarkMs) && existingWatermarkMs >= commentTimestampMs
|
|
7936
|
+
? new Date(existingWatermarkMs + 1).toISOString()
|
|
7937
|
+
: comment.timestamp;
|
|
7938
|
+
this.db.prepare('UPDATE tasks SET updated_at = ? WHERE tenant_id = ? AND workspace_id = ? AND id = ?').run(nextTaskUpdatedAt, this.tenantId, normalizedWorkspaceId, id);
|
|
6662
7939
|
if (options?.skipTaskEvents !== true) {
|
|
6663
7940
|
this.addTaskEvent({
|
|
6664
7941
|
taskId: id,
|