@taskforcehq/taskforce 0.3.304 → 0.3.306
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 +140 -247
- package/dist/Taskforce.module.css +295 -89
- package/dist/TaskforceCore.js +378 -111
- package/dist/TaskforceCore.test.js +1221 -107
- 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 +52 -2
- package/dist/compat/workspaceSyncCompat.test.js +38 -1
- package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
- package/dist/components/context/ContextAttachmentManager.js +549 -0
- package/dist/components/features/AgentsModule.d.ts +10 -1
- package/dist/components/features/AgentsModule.js +260 -15
- package/dist/components/features/AgentsModule.test.js +255 -5
- package/dist/components/features/DocumentIndex.d.ts +1 -1
- package/dist/components/features/DocumentIndex.js +1 -1
- package/dist/components/features/DocumentViewer.d.ts +6 -2
- package/dist/components/features/DocumentViewer.js +90 -10
- package/dist/components/features/DocumentWorkspace.d.ts +5 -22
- package/dist/components/features/DocumentWorkspace.js +12 -155
- package/dist/components/features/DocumentWorkspace.test.js +226 -6
- package/dist/components/features/TaskSettings.js +29 -3
- package/dist/components/features/TaskSettings.test.js +42 -1
- package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
- package/dist/components/features/documentWorkspaceModel.js +57 -0
- package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
- package/dist/components/features/useDocumentWorkspaceController.js +113 -0
- package/dist/components/task/TaskCard.js +19 -28
- package/dist/components/task/TaskCard.test.js +38 -3
- package/dist/components/task/TaskContextUpload.js +4 -526
- package/dist/components/task/TaskForm.d.ts +8 -8
- package/dist/components/task/TaskForm.js +362 -230
- package/dist/components/task/TaskForm.test.js +558 -381
- package/dist/components/task/TaskKanban.d.ts +2 -1
- package/dist/components/task/TaskKanban.js +96 -14
- package/dist/components/task/TaskKanban.test.js +36 -0
- package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
- package/dist/components/ui/EditableAvatarButton.js +18 -0
- package/dist/components/views/PlanComparisonPage.d.ts +9 -1
- package/dist/components/views/PlanComparisonPage.js +70 -17
- package/dist/components/views/PlanComparisonPage.test.js +289 -41
- package/dist/components/views/PlansPage.js +239 -106
- package/dist/components/views/PlansPage.test.js +593 -42
- package/dist/components/views/StandaloneLayout.js +310 -554
- package/dist/components/views/panels/FilterToolbar.test.js +6 -4
- package/dist/components/views/panels/PlanningDrawer.d.ts +8 -1
- package/dist/components/views/panels/PlanningDrawer.js +18 -11
- package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
- package/dist/components/views/panels/PlanningDrawer.test.js +141 -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 +27 -14
- package/dist/components/views/standalone/modals/AccountHubModal.js +87 -15
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +42 -2
- 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 -1
- package/dist/core/AiProfileService.d.ts +45 -3
- package/dist/core/AiProfileService.js +774 -63
- package/dist/core/AiProfileService.test.js +22 -2
- package/dist/core/AiProfiles.test.js +761 -2
- package/dist/core/AttachmentLinkService.js +57 -15
- 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.d.ts +1 -0
- package/dist/core/AuthTokenService.test.js +78 -0
- package/dist/core/EntitlementsPolicy.test.js +77 -15
- package/dist/core/GlobalSettingsService.js +115 -6
- package/dist/core/PlanEntitlementService.d.ts +30 -3
- package/dist/core/PlanEntitlementService.js +454 -93
- package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
- package/dist/core/PlanFeatureCatalog.test.js +84 -0
- package/dist/core/PlanVersionPolicy.test.js +54 -28
- package/dist/core/PlanningEntities.test.js +52 -0
- package/dist/core/SignupMonetizationSettings.test.js +241 -44
- package/dist/core/SyncReconciliationService.js +2 -65
- package/dist/core/SystemAdmin.test.js +244 -88
- package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
- package/dist/core/TaskTaxonomyValidation.test.js +53 -0
- package/dist/core/Taskforce.d.ts +228 -14
- package/dist/core/Taskforce.js +814 -201
- package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
- package/dist/core/Taskforce.listTasksSlim.test.js +91 -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 +128 -27
- package/dist/core/shared.d.ts +3 -1
- package/dist/core/shared.js +16 -1
- package/dist/core/types.d.ts +68 -2
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +26 -1
- package/dist/hooks/sync/auth.js +3 -1
- 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 +10 -1
- package/dist/hooks/sync/orchestratorShared.js +68 -47
- 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 +171 -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 +186 -65
- package/dist/hooks/sync/transfers.test.d.ts +1 -0
- package/dist/hooks/sync/transfers.test.js +396 -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/useSyncOrchestrator.aiProfiles.test.js +12 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +16 -6
- package/dist/hooks/useSyncOrchestrator.js +424 -175
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +487 -10
- package/dist/hooks/useTaskData.js +8 -3
- package/dist/hooks/useTaskData.test.js +45 -0
- package/dist/hooks/useTaskMutations.d.ts +2 -1
- package/dist/hooks/useTaskMutations.js +12 -1
- package/dist/hooks/useTaskMutations.test.js +30 -1
- package/dist/hooks/useTaskforce.d.ts +65 -4
- package/dist/hooks/useTaskforce.js +564 -243
- package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
- package/dist/hooks/useTaskforce.runtime-routing.test.js +152 -0
- package/dist/hooks/useTaskforce.sync-behavior.test.js +2454 -208
- package/dist/hooks/useWorkspaceSyncController.d.ts +4 -1
- package/dist/hooks/useWorkspaceSyncController.js +17 -2
- package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +316 -35
- 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/adminWorkspaceRegistrar.d.ts +2 -0
- package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
- package/dist/mcp/collaborationRegistrar.d.ts +2 -0
- package/dist/mcp/collaborationRegistrar.js +71 -0
- package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
- package/dist/mcp/documentAssetRegistrar.js +346 -0
- package/dist/mcp/runtime.d.ts +2 -0
- package/dist/mcp/runtime.js +2433 -3534
- package/dist/mcp/runtime.test.js +440 -2
- package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
- package/dist/mcp/taskPlanningRegistrar.js +418 -0
- package/dist/mcp/toolCatalog.d.ts +35 -0
- package/dist/mcp/toolCatalog.js +3 -0
- package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
- package/dist/migrations/taskSchemaMigrations.js +171 -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/annotatedAttachmentsRoutes.test.js +3 -3
- 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 +120 -9
- package/dist/server/index.rateLimit.test.js +3 -0
- package/dist/server/index.test.js +106 -3
- package/dist/server/routes/admin.d.ts +4 -5
- package/dist/server/routes/admin.js +410 -80
- package/dist/server/routes/annotatedAttachments.d.ts +1 -1
- package/dist/server/routes/annotatedAttachments.js +1 -1
- package/dist/server/routes/auth.d.ts +16 -6
- package/dist/server/routes/auth.js +1015 -118
- package/dist/server/routes/authSupport.d.ts +30 -0
- package/dist/server/routes/authSupport.js +81 -0
- package/dist/server/routes/billing.d.ts +1 -1
- package/dist/server/routes/billing.js +276 -34
- package/dist/server/routes/billing.test.js +760 -52
- package/dist/server/routes/documentReviews.d.ts +1 -1
- package/dist/server/routes/documentReviews.js +1 -1
- package/dist/server/routes/documents.d.ts +5 -2
- package/dist/server/routes/documents.js +242 -74
- package/dist/server/routes/primitives.d.ts +11 -0
- package/dist/server/routes/primitives.js +73 -0
- package/dist/server/routes/resources.d.ts +1 -1
- package/dist/server/routes/resources.js +1 -1
- package/dist/server/routes/shared.d.ts +29 -3
- package/dist/server/routes/shared.js +69 -4
- package/dist/server/routes/sync.d.ts +1 -1
- package/dist/server/routes/sync.integration.test.js +437 -39
- package/dist/server/routes/sync.js +62 -606
- 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 +180 -0
- package/dist/server/routes/tasks.d.ts +1 -1
- package/dist/server/routes/tasks.js +44 -2
- package/dist/server/routes/workspaces.d.ts +1 -1
- package/dist/server/routes/workspaces.js +37 -6
- package/dist/server/routes.d.ts +2 -9
- package/dist/server/routes.js +136 -165
- package/dist/server/routes.test.js +2457 -162
- 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/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/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 +17 -11
- package/dist/sync/collaborationSyncPayload.test.js +13 -1
- package/dist/sync/contentSyncState.d.ts +4 -8
- package/dist/sync/contentSyncState.js +33 -77
- package/dist/sync/syncApplyHandlers.d.ts +7 -0
- package/dist/sync/syncApplyHandlers.js +28 -13
- package/dist/sync/syncApplyHandlers.test.js +72 -3
- package/dist/sync/syncFieldSemantics.d.ts +12 -0
- package/dist/sync/syncFieldSemantics.js +55 -0
- package/dist/sync/syncResourceAdapters.d.ts +44 -0
- package/dist/sync/syncResourceAdapters.js +77 -0
- package/dist/sync/syncService.d.ts +6 -0
- package/dist/sync/syncService.js +60 -0
- package/dist/sync/syncService.test.js +71 -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.js +4 -3
- package/dist/sync/workspacePullFeed.test.js +43 -0
- package/dist/sync/workspaceRepair.js +5 -2
- package/dist/sync/workspaceSyncModel.d.ts +48 -0
- package/dist/sync/workspaceSyncModel.js +195 -31
- package/dist/sync/workspaceSyncModel.test.js +325 -12
- package/dist/sync/workspaceSyncState.d.ts +19 -0
- package/dist/sync/workspaceSyncState.js +3 -1
- package/dist/sync/workspaceSyncSurface.js +3 -8
- package/dist/types.d.ts +17 -0
- package/dist/ui/assets/AgentsModule-DrLawwNl.js +1 -0
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BlS-cqnl.js → AnnotatedAttachmentWorkspace-Z9_whf7F.js} +1 -1
- 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-B03MaSkw.js +252 -0
- package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +1 -0
- package/dist/ui/assets/{InitiativesModule-BjR6iBf9.js → InitiativesModule-9E6ParrY.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-CvfnMiWq.css +1 -0
- package/dist/ui/assets/PlansPage-DRXscYxH.js +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/TaskSettings-DZX4jk7e.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-DnFqdUME.js → WorkflowsModule-BA7jcEpH.js} +1 -1
- package/dist/ui/assets/index-9eT8e0Lu.css +1 -0
- package/dist/ui/assets/index-C6k75jr8.js +6 -0
- package/dist/ui/assets/{vendor-icons-I7ZwG1z_.js → vendor-icons-DuEd_65c.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/utils/accountProfileSummaryCache.d.ts +5 -0
- package/dist/utils/assignees.d.ts +2 -0
- package/dist/utils/assignees.js +2 -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.d.ts +19 -0
- package/dist/utils/commercialLifecyclePresentation.js +199 -0
- package/dist/utils/contextAssetEvents.d.ts +2 -0
- 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 +2 -0
- package/dist/utils/taskActivity.js +228 -22
- package/dist/utils/taskActivity.test.js +119 -8
- 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 +10 -2
- package/scripts/check-cloud-mcp.mjs +83 -0
- package/scripts/export-sqlite-to-postgres.mjs +7 -9
- package/scripts/playwright-auth.sh +5 -1
- package/scripts/run-billing-performance-smoke.sh +24 -0
- 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-BLWVbuKP.js +0 -1
- package/dist/ui/assets/DocumentWorkspace-BH_6DF6x.js +0 -250
- package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
- package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
- package/dist/ui/assets/PlansPage-CSDQ4sLa.js +0 -1
- package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
- package/dist/ui/assets/TaskSettings-CEDy34Jo.js +0 -9
- package/dist/ui/assets/index-DXUdtH1B.js +0 -6
- package/dist/ui/assets/index-Ii0B0rTO.css +0 -1
- package/scripts/migrate-planning-model.ts +0 -233
package/dist/core/Taskforce.js
CHANGED
|
@@ -7,7 +7,8 @@ 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 {
|
|
10
|
+
import { AuthIdentityService } from './AuthIdentityService.js';
|
|
11
|
+
import { archiveAiProfileService, getAiProfileByTokenService, listAiProfilesService, mergeAiProfilesService, resolveAiProfileService, updateAiProfileAvatarService, upsertAiProfileForSyncService, } from './AiProfileService.js';
|
|
11
12
|
import { syncCanonicalTaskAttachmentLinksService, } from './AttachmentLinkService.js';
|
|
12
13
|
import { addTaskEventService, buildTaskActivityService, listTaskEventsInternalService, listWorkspaceTaskEventsService, replaceTaskCommentsService, replaceTaskEventsService, upsertTaskEventForSyncService, } from './TaskActivityService.js';
|
|
13
14
|
import { addDeletedTaskRecordService, getDeletedTaskService, listDeletedTasksService, mapDeletedTaskRowService, normalizeTaskSnapshotForRestoreService, pruneDeletedTasksService, purgeExpiredDeletedTasksService, } from './DeletedTaskLifecycleService.js';
|
|
@@ -16,6 +17,8 @@ import { getGlobalSettingsService, hasPersistedGlobalSettingsService, readGlobal
|
|
|
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', '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();
|
|
@@ -408,12 +373,22 @@ export class TaskforceCore {
|
|
|
408
373
|
username: String(row.username || '').trim() || this.normalizeAiProfileUsername(row.name, 'ai'),
|
|
409
374
|
icon: this.normalizeAiProfileIcon(row.icon),
|
|
410
375
|
color: this.normalizeAiProfileColor(row.color),
|
|
376
|
+
avatarUrl: typeof row.avatar_url === 'string' && row.avatar_url.trim().length > 0 ? row.avatar_url.trim() : null,
|
|
377
|
+
avatarSourceUrl: typeof row.avatar_source_url === 'string' && row.avatar_source_url.trim().length > 0 ? row.avatar_source_url.trim() : null,
|
|
411
378
|
description: typeof row.description === 'string' && row.description.trim().length > 0 ? row.description.trim() : null,
|
|
412
379
|
role: typeof row.role === 'string' && row.role.trim().length > 0 ? row.role.trim() : null,
|
|
413
380
|
provider: typeof row.provider === 'string' && row.provider.trim().length > 0 ? row.provider.trim() : null,
|
|
414
381
|
model: typeof row.model === 'string' && row.model.trim().length > 0 ? row.model.trim() : null,
|
|
415
382
|
surfaceType: parseAiProfileSurfaceType(row.surface_type),
|
|
383
|
+
seatScope: parseAiProfileSeatScope(row.seat_scope),
|
|
416
384
|
providerMetadata,
|
|
385
|
+
archivedAt: typeof row.archived_at === 'string' ? row.archived_at : null,
|
|
386
|
+
archivedBy: typeof row.archived_by === 'string' && row.archived_by.trim().length > 0 ? row.archived_by.trim() : null,
|
|
387
|
+
archivedReason: typeof row.archived_reason === 'string' && row.archived_reason.trim().length > 0 ? row.archived_reason.trim() : null,
|
|
388
|
+
mergedIntoProfileId: typeof row.merged_into_profile_id === 'string' && row.merged_into_profile_id.trim().length > 0 ? row.merged_into_profile_id.trim() : null,
|
|
389
|
+
rosterStateUpdatedAt: typeof row.roster_state_updated_at === 'string' && row.roster_state_updated_at.trim().length > 0
|
|
390
|
+
? row.roster_state_updated_at.trim()
|
|
391
|
+
: null,
|
|
417
392
|
createdAt: String(row.created_at || '').trim(),
|
|
418
393
|
updatedAt: String(row.updated_at || '').trim(),
|
|
419
394
|
lastActiveAt: typeof row.last_active_at === 'string' ? row.last_active_at : null
|
|
@@ -431,12 +406,20 @@ export class TaskforceCore {
|
|
|
431
406
|
meta: {
|
|
432
407
|
profileToken: profile.profileToken,
|
|
433
408
|
workspaceId: profile.workspaceId,
|
|
409
|
+
avatarUrl: profile.avatarUrl ?? null,
|
|
410
|
+
avatarSourceUrl: profile.avatarSourceUrl ?? null,
|
|
434
411
|
lastActiveAt: profile.lastActiveAt ?? null,
|
|
435
412
|
description: profile.description ?? null,
|
|
436
413
|
role: profile.role ?? null,
|
|
437
414
|
provider: profile.provider ?? null,
|
|
438
415
|
model: profile.model ?? null,
|
|
439
|
-
surfaceType: profile.surfaceType ?? null
|
|
416
|
+
surfaceType: profile.surfaceType ?? null,
|
|
417
|
+
seatScope: profile.seatScope ?? null,
|
|
418
|
+
archivedAt: profile.archivedAt ?? null,
|
|
419
|
+
archivedBy: profile.archivedBy ?? null,
|
|
420
|
+
archivedReason: profile.archivedReason ?? null,
|
|
421
|
+
mergedIntoProfileId: profile.mergedIntoProfileId ?? null,
|
|
422
|
+
rosterStateUpdatedAt: profile.rosterStateUpdatedAt ?? null
|
|
440
423
|
}
|
|
441
424
|
};
|
|
442
425
|
}
|
|
@@ -1160,7 +1143,6 @@ export class TaskforceCore {
|
|
|
1160
1143
|
this.initializeSchema();
|
|
1161
1144
|
ensureTenantSchema(this.db);
|
|
1162
1145
|
ensureWorkspaceSchema(this.db);
|
|
1163
|
-
ensureParentTaskSchema(this.db);
|
|
1164
1146
|
ensureScheduleTaskSchema(this.db);
|
|
1165
1147
|
ensureAssigneeTaskSchema(this.db);
|
|
1166
1148
|
ensureTaskReferenceSchema(this.db);
|
|
@@ -1171,6 +1153,7 @@ export class TaskforceCore {
|
|
|
1171
1153
|
ensureAnnotatedAttachmentSessionsSchema(this.db);
|
|
1172
1154
|
ensureBillingSchema(this.db);
|
|
1173
1155
|
ensureEntitlementsSchema(this.db);
|
|
1156
|
+
ensureAuthIdentitySchema(this.db);
|
|
1174
1157
|
this.performMigrationIfNeeded();
|
|
1175
1158
|
this.planEntitlementService = new PlanEntitlementService(this.db, this.tenantId, {
|
|
1176
1159
|
getWorkspaceOwnerUserId: (wid) => this.getWorkspaceOwnerUserId(wid),
|
|
@@ -1182,6 +1165,8 @@ export class TaskforceCore {
|
|
|
1182
1165
|
this.planEntitlementService.backfillAccountEntitlements();
|
|
1183
1166
|
this.repairDetachedAnnotatedAttachmentSessions();
|
|
1184
1167
|
this.authTokenService = new AuthTokenService(this.db, this.tenantId);
|
|
1168
|
+
this.identityService = new AuthIdentityService(this.db, this.tenantId);
|
|
1169
|
+
this.authTokenService.setIdentityService(this.identityService);
|
|
1185
1170
|
this.mcpTokenService = new McpTokenService(this.db, this.tenantId, {
|
|
1186
1171
|
getAccountAccessStatus: (userId) => this.getAccountAccessStatus(userId),
|
|
1187
1172
|
});
|
|
@@ -1225,11 +1210,23 @@ export class TaskforceCore {
|
|
|
1225
1210
|
if (!accountId) {
|
|
1226
1211
|
throw new TaskforceRuleError('accountId is required', 400, 'ACCOUNT_ID_REQUIRED');
|
|
1227
1212
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1213
|
+
const existingEntitlement = this.hasAccountEntitlement(accountId)
|
|
1214
|
+
? this.getAccountEntitlement(accountId)
|
|
1215
|
+
: null;
|
|
1216
|
+
if (existingEntitlement) {
|
|
1217
|
+
if (options?.source !== 'invite-accept') {
|
|
1218
|
+
return existingEntitlement;
|
|
1219
|
+
}
|
|
1220
|
+
const existingAccess = this.getAccountAccessStatus(accountId);
|
|
1221
|
+
if (existingAccess.canAccessApp) {
|
|
1222
|
+
return existingAccess.entitlement || existingEntitlement;
|
|
1223
|
+
}
|
|
1230
1224
|
}
|
|
1231
1225
|
const now = options?.now || new Date().toISOString();
|
|
1232
|
-
const
|
|
1226
|
+
const inviteOutcome = options?.source === 'invite-accept'
|
|
1227
|
+
? this.planEntitlementService.resolveInviteAcceptanceDefaultEntitlement()
|
|
1228
|
+
: null;
|
|
1229
|
+
const onboardingOutcome = inviteOutcome || this.resolveRegistrationOnboardingOutcome();
|
|
1233
1230
|
this.transitionCanonicalCommercialState({
|
|
1234
1231
|
accountId,
|
|
1235
1232
|
planVersionId: onboardingOutcome.planVersionId,
|
|
@@ -1261,6 +1258,9 @@ export class TaskforceCore {
|
|
|
1261
1258
|
getAccountEntitlement(accountId) {
|
|
1262
1259
|
return this.planEntitlementService.getAccountEntitlement(accountId);
|
|
1263
1260
|
}
|
|
1261
|
+
countAiProfileUsage(workspaceId) {
|
|
1262
|
+
return this.planEntitlementService.countAiProfileUsage(workspaceId);
|
|
1263
|
+
}
|
|
1264
1264
|
countWorkspaceSeatUsage(workspaceId, options = {}) {
|
|
1265
1265
|
return this.planEntitlementService.countWorkspaceSeatUsage(workspaceId, options);
|
|
1266
1266
|
}
|
|
@@ -1312,6 +1312,26 @@ export class TaskforceCore {
|
|
|
1312
1312
|
}
|
|
1313
1313
|
}
|
|
1314
1314
|
}
|
|
1315
|
+
supportsWorkspaceAssetLinkTargetType() {
|
|
1316
|
+
try {
|
|
1317
|
+
if (this.db.provider === 'postgres') {
|
|
1318
|
+
const row = this.db.prepare(`
|
|
1319
|
+
SELECT 1
|
|
1320
|
+
FROM information_schema.columns
|
|
1321
|
+
WHERE table_schema = current_schema()
|
|
1322
|
+
AND table_name = 'workspace_asset_links'
|
|
1323
|
+
AND column_name = 'target_type'
|
|
1324
|
+
LIMIT 1
|
|
1325
|
+
`).get();
|
|
1326
|
+
return Boolean(row);
|
|
1327
|
+
}
|
|
1328
|
+
const columns = this.db.prepare(`PRAGMA table_info(workspace_asset_links)`).all();
|
|
1329
|
+
return columns.some((column) => String(column.name || '').trim() === 'target_type');
|
|
1330
|
+
}
|
|
1331
|
+
catch {
|
|
1332
|
+
return false;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1315
1335
|
initializeSchema() {
|
|
1316
1336
|
const syncEventsIdColumnSql = this.db.provider === 'postgres'
|
|
1317
1337
|
? 'id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY'
|
|
@@ -1347,10 +1367,7 @@ export class TaskforceCore {
|
|
|
1347
1367
|
scheduled_week_key TEXT,
|
|
1348
1368
|
order_in_day INTEGER,
|
|
1349
1369
|
workstream_id TEXT,
|
|
1350
|
-
|
|
1351
|
-
child_display_order INTEGER,
|
|
1352
|
-
FOREIGN KEY(workstream_id) REFERENCES workstreams(id) ON DELETE SET NULL,
|
|
1353
|
-
FOREIGN KEY(parent_task_id) REFERENCES tasks(id) ON DELETE RESTRICT
|
|
1370
|
+
FOREIGN KEY(workstream_id) REFERENCES workstreams(id) ON DELETE SET NULL
|
|
1354
1371
|
);
|
|
1355
1372
|
|
|
1356
1373
|
CREATE TABLE IF NOT EXISTS workspace_task_reference_sequences (
|
|
@@ -1637,12 +1654,20 @@ export class TaskforceCore {
|
|
|
1637
1654
|
username TEXT NOT NULL,
|
|
1638
1655
|
icon TEXT NOT NULL,
|
|
1639
1656
|
color TEXT NOT NULL,
|
|
1657
|
+
avatar_url TEXT,
|
|
1658
|
+
avatar_source_url TEXT,
|
|
1640
1659
|
description TEXT,
|
|
1641
1660
|
role TEXT,
|
|
1642
1661
|
provider TEXT,
|
|
1643
1662
|
model TEXT,
|
|
1644
1663
|
surface_type TEXT,
|
|
1664
|
+
seat_scope TEXT,
|
|
1645
1665
|
provider_metadata_json TEXT,
|
|
1666
|
+
archived_at TEXT,
|
|
1667
|
+
archived_by TEXT,
|
|
1668
|
+
archived_reason TEXT,
|
|
1669
|
+
merged_into_profile_id TEXT,
|
|
1670
|
+
roster_state_updated_at TEXT,
|
|
1646
1671
|
created_at TEXT NOT NULL,
|
|
1647
1672
|
updated_at TEXT NOT NULL,
|
|
1648
1673
|
last_active_at TEXT
|
|
@@ -1762,6 +1787,7 @@ export class TaskforceCore {
|
|
|
1762
1787
|
}
|
|
1763
1788
|
}
|
|
1764
1789
|
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_tasks_workstream ON tasks(tenant_id, workspace_id, workstream_id)`);
|
|
1790
|
+
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_tasks_assignee ON tasks(tenant_id, workspace_id, assignee, is_archived)`);
|
|
1765
1791
|
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_workstreams_initiative ON workstreams(tenant_id, workspace_id, initiative_id)`);
|
|
1766
1792
|
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_checklist_items_task_order ON checklist_items(tenant_id, workspace_id, task_id, sort_order)`);
|
|
1767
1793
|
if (this.db.provider === 'sqlite') {
|
|
@@ -1878,12 +1904,20 @@ export class TaskforceCore {
|
|
|
1878
1904
|
{ name: 'username', ddl: `ALTER TABLE ai_profiles ADD COLUMN username TEXT NOT NULL DEFAULT 'ai'` },
|
|
1879
1905
|
{ name: 'icon', ddl: `ALTER TABLE ai_profiles ADD COLUMN icon TEXT NOT NULL DEFAULT 'Bot'` },
|
|
1880
1906
|
{ name: 'color', ddl: `ALTER TABLE ai_profiles ADD COLUMN color TEXT NOT NULL DEFAULT '#64748b'` },
|
|
1907
|
+
{ name: 'avatar_url', ddl: `ALTER TABLE ai_profiles ADD COLUMN avatar_url TEXT` },
|
|
1908
|
+
{ name: 'avatar_source_url', ddl: `ALTER TABLE ai_profiles ADD COLUMN avatar_source_url TEXT` },
|
|
1881
1909
|
{ name: 'description', ddl: `ALTER TABLE ai_profiles ADD COLUMN description TEXT` },
|
|
1882
1910
|
{ name: 'role', ddl: `ALTER TABLE ai_profiles ADD COLUMN role TEXT` },
|
|
1883
1911
|
{ name: 'provider', ddl: `ALTER TABLE ai_profiles ADD COLUMN provider TEXT` },
|
|
1884
1912
|
{ name: 'model', ddl: `ALTER TABLE ai_profiles ADD COLUMN model TEXT` },
|
|
1885
1913
|
{ name: 'surface_type', ddl: `ALTER TABLE ai_profiles ADD COLUMN surface_type TEXT` },
|
|
1914
|
+
{ name: 'seat_scope', ddl: `ALTER TABLE ai_profiles ADD COLUMN seat_scope TEXT` },
|
|
1886
1915
|
{ name: 'provider_metadata_json', ddl: `ALTER TABLE ai_profiles ADD COLUMN provider_metadata_json TEXT` },
|
|
1916
|
+
{ name: 'archived_at', ddl: `ALTER TABLE ai_profiles ADD COLUMN archived_at TEXT` },
|
|
1917
|
+
{ name: 'archived_by', ddl: `ALTER TABLE ai_profiles ADD COLUMN archived_by TEXT` },
|
|
1918
|
+
{ name: 'archived_reason', ddl: `ALTER TABLE ai_profiles ADD COLUMN archived_reason TEXT` },
|
|
1919
|
+
{ name: 'merged_into_profile_id', ddl: `ALTER TABLE ai_profiles ADD COLUMN merged_into_profile_id TEXT` },
|
|
1920
|
+
{ name: 'roster_state_updated_at', ddl: `ALTER TABLE ai_profiles ADD COLUMN roster_state_updated_at TEXT` },
|
|
1887
1921
|
{ name: 'created_at', ddl: `ALTER TABLE ai_profiles ADD COLUMN created_at TEXT NOT NULL DEFAULT ''` },
|
|
1888
1922
|
{ name: 'updated_at', ddl: `ALTER TABLE ai_profiles ADD COLUMN updated_at TEXT NOT NULL DEFAULT ''` },
|
|
1889
1923
|
{ name: 'last_active_at', ddl: `ALTER TABLE ai_profiles ADD COLUMN last_active_at TEXT` }
|
|
@@ -1902,12 +1936,20 @@ export class TaskforceCore {
|
|
|
1902
1936
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS username TEXT NOT NULL DEFAULT 'ai'`,
|
|
1903
1937
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS icon TEXT NOT NULL DEFAULT 'Bot'`,
|
|
1904
1938
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS color TEXT NOT NULL DEFAULT '#64748b'`,
|
|
1939
|
+
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS avatar_url TEXT`,
|
|
1940
|
+
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS avatar_source_url TEXT`,
|
|
1905
1941
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS description TEXT`,
|
|
1906
1942
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS role TEXT`,
|
|
1907
1943
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS provider TEXT`,
|
|
1908
1944
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS model TEXT`,
|
|
1909
1945
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS surface_type TEXT`,
|
|
1946
|
+
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS seat_scope TEXT`,
|
|
1910
1947
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS provider_metadata_json TEXT`,
|
|
1948
|
+
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS archived_at TEXT`,
|
|
1949
|
+
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS archived_by TEXT`,
|
|
1950
|
+
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS archived_reason TEXT`,
|
|
1951
|
+
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS merged_into_profile_id TEXT`,
|
|
1952
|
+
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS roster_state_updated_at TEXT`,
|
|
1911
1953
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS created_at TEXT NOT NULL DEFAULT ''`,
|
|
1912
1954
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS updated_at TEXT NOT NULL DEFAULT ''`,
|
|
1913
1955
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS last_active_at TEXT`
|
|
@@ -1921,6 +1963,21 @@ export class TaskforceCore {
|
|
|
1921
1963
|
}
|
|
1922
1964
|
}
|
|
1923
1965
|
}
|
|
1966
|
+
this.db.exec(`
|
|
1967
|
+
UPDATE ai_profiles
|
|
1968
|
+
SET roster_state_updated_at = CASE
|
|
1969
|
+
WHEN archived_at IS NOT NULL AND TRIM(archived_at) <> '' THEN archived_at
|
|
1970
|
+
WHEN created_at IS NOT NULL AND TRIM(created_at) <> '' THEN created_at
|
|
1971
|
+
ELSE updated_at
|
|
1972
|
+
END
|
|
1973
|
+
WHERE roster_state_updated_at IS NULL
|
|
1974
|
+
OR TRIM(roster_state_updated_at) = ''
|
|
1975
|
+
OR (
|
|
1976
|
+
archived_at IS NOT NULL
|
|
1977
|
+
AND TRIM(archived_at) <> ''
|
|
1978
|
+
AND roster_state_updated_at < archived_at
|
|
1979
|
+
)
|
|
1980
|
+
`);
|
|
1924
1981
|
this.migrateSystemRoleBackfillIfNeeded();
|
|
1925
1982
|
this.migrateUserBetaAccessBackfillIfNeeded();
|
|
1926
1983
|
this.migrateDuplicateAiProfilesIfNeeded();
|
|
@@ -2414,6 +2471,42 @@ export class TaskforceCore {
|
|
|
2414
2471
|
}
|
|
2415
2472
|
return [];
|
|
2416
2473
|
}
|
|
2474
|
+
readLegacyWorkspaceTaxonomyStateFromConfig() {
|
|
2475
|
+
const legacyCategories = this.getLegacyCategoriesConfig();
|
|
2476
|
+
const legacyTypes = this.getLegacyTypesConfig();
|
|
2477
|
+
const legacyPriorities = this.getLegacyPrioritiesConfig();
|
|
2478
|
+
const legacyTaxonomies = this.getLegacyTaxonomiesConfig();
|
|
2479
|
+
if (legacyCategories.length === 0
|
|
2480
|
+
&& legacyTypes.length === 0
|
|
2481
|
+
&& legacyPriorities.length === 0
|
|
2482
|
+
&& legacyTaxonomies.length === 0) {
|
|
2483
|
+
return null;
|
|
2484
|
+
}
|
|
2485
|
+
return normalizeWorkspaceTaxonomyStatePayload({
|
|
2486
|
+
categories: legacyCategories,
|
|
2487
|
+
types: legacyTypes,
|
|
2488
|
+
priorities: legacyPriorities,
|
|
2489
|
+
taxonomies: legacyTaxonomies
|
|
2490
|
+
});
|
|
2491
|
+
}
|
|
2492
|
+
migrateLegacyWorkspaceTaxonomyStateIfNeeded(workspaceId) {
|
|
2493
|
+
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
2494
|
+
if (normalizedWorkspaceId !== 'default') {
|
|
2495
|
+
return null;
|
|
2496
|
+
}
|
|
2497
|
+
const stored = this.readStoredWorkspaceTaxonomyState(normalizedWorkspaceId);
|
|
2498
|
+
if (Object.keys(stored.state).length > 0) {
|
|
2499
|
+
return stored;
|
|
2500
|
+
}
|
|
2501
|
+
const legacyState = this.readLegacyWorkspaceTaxonomyStateFromConfig();
|
|
2502
|
+
if (!legacyState)
|
|
2503
|
+
return null;
|
|
2504
|
+
const persisted = this.writeWorkspaceTaxonomyState(legacyState, normalizedWorkspaceId);
|
|
2505
|
+
return {
|
|
2506
|
+
state: persisted.state,
|
|
2507
|
+
updatedAt: persisted.updatedAt
|
|
2508
|
+
};
|
|
2509
|
+
}
|
|
2417
2510
|
buildEmergencyWorkspaceTaxonomyState() {
|
|
2418
2511
|
const emergencyPack = getEmergencyStarterTaxonomyLibraryPack();
|
|
2419
2512
|
return {
|
|
@@ -2454,17 +2547,13 @@ export class TaskforceCore {
|
|
|
2454
2547
|
}
|
|
2455
2548
|
getEffectiveWorkspaceTaxonomyState(workspaceId) {
|
|
2456
2549
|
const { state } = this.readStoredWorkspaceTaxonomyState(workspaceId);
|
|
2457
|
-
const legacyCategories = this.getLegacyCategoriesConfig();
|
|
2458
|
-
const legacyTypes = this.getLegacyTypesConfig();
|
|
2459
|
-
const legacyPriorities = this.getLegacyPrioritiesConfig();
|
|
2460
|
-
const legacyTaxonomies = this.getLegacyTaxonomiesConfig();
|
|
2461
2550
|
const starterState = this.getResolvedStarterWorkspaceTaxonomyState();
|
|
2462
2551
|
const emergencyState = this.buildEmergencyWorkspaceTaxonomyState();
|
|
2463
2552
|
return normalizeWorkspaceTaxonomyStatePayload({
|
|
2464
|
-
categories: state.categories ||
|
|
2465
|
-
types: state.types ||
|
|
2466
|
-
priorities: state.priorities ||
|
|
2467
|
-
taxonomies: state.taxonomies ||
|
|
2553
|
+
categories: state.categories || starterState.categories || emergencyState.categories,
|
|
2554
|
+
types: state.types || starterState.types || emergencyState.types,
|
|
2555
|
+
priorities: state.priorities || starterState.priorities || emergencyState.priorities,
|
|
2556
|
+
taxonomies: state.taxonomies || starterState.taxonomies || emergencyState.taxonomies
|
|
2468
2557
|
});
|
|
2469
2558
|
}
|
|
2470
2559
|
ensureWorkspaceTaxonomyStateSeeded(workspaceId) {
|
|
@@ -2473,6 +2562,10 @@ export class TaskforceCore {
|
|
|
2473
2562
|
if (Object.keys(stored.state).length > 0) {
|
|
2474
2563
|
return stored;
|
|
2475
2564
|
}
|
|
2565
|
+
const migratedLegacy = this.migrateLegacyWorkspaceTaxonomyStateIfNeeded(normalizedWorkspaceId);
|
|
2566
|
+
if (migratedLegacy) {
|
|
2567
|
+
return migratedLegacy;
|
|
2568
|
+
}
|
|
2476
2569
|
const seededState = this.getEffectiveWorkspaceTaxonomyState(normalizedWorkspaceId);
|
|
2477
2570
|
const persisted = this.writeWorkspaceTaxonomyState(seededState, normalizedWorkspaceId);
|
|
2478
2571
|
return {
|
|
@@ -2629,6 +2722,9 @@ export class TaskforceCore {
|
|
|
2629
2722
|
getOnboardingPolicy() {
|
|
2630
2723
|
return this.planEntitlementService.getOnboardingPolicy();
|
|
2631
2724
|
}
|
|
2725
|
+
listOnboardingPlanOptions() {
|
|
2726
|
+
return this.planEntitlementService.listOnboardingPlanOptions();
|
|
2727
|
+
}
|
|
2632
2728
|
validateOnboardingPolicy(input) {
|
|
2633
2729
|
return this.planEntitlementService.validateOnboardingPolicy(input);
|
|
2634
2730
|
}
|
|
@@ -2670,31 +2766,58 @@ export class TaskforceCore {
|
|
|
2670
2766
|
listCanonicalTaskAttachments(taskId, workspaceId) {
|
|
2671
2767
|
try {
|
|
2672
2768
|
const workspaceAssetStore = new WorkspaceAssetStore(this.db, this.tenantId);
|
|
2673
|
-
const rows = this.
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2769
|
+
const rows = (this.supportsWorkspaceAssetLinkTargetType()
|
|
2770
|
+
? this.db.prepare(`
|
|
2771
|
+
SELECT
|
|
2772
|
+
a.asset_id,
|
|
2773
|
+
a.kind,
|
|
2774
|
+
a.reference_number,
|
|
2775
|
+
a.storage_provider,
|
|
2776
|
+
a.storage_key,
|
|
2777
|
+
a.logical_name,
|
|
2778
|
+
a.original_filename,
|
|
2779
|
+
a.updated_at,
|
|
2780
|
+
a.created_at,
|
|
2781
|
+
l.role,
|
|
2782
|
+
l.display_name
|
|
2783
|
+
FROM workspace_asset_links l
|
|
2784
|
+
JOIN workspace_assets a
|
|
2785
|
+
ON a.tenant_id = l.tenant_id
|
|
2786
|
+
AND a.workspace_id = l.workspace_id
|
|
2787
|
+
AND a.asset_id = l.asset_id
|
|
2788
|
+
WHERE l.tenant_id = ?
|
|
2789
|
+
AND l.workspace_id = ?
|
|
2790
|
+
AND l.target_type = 'task'
|
|
2791
|
+
AND l.task_id = ?
|
|
2792
|
+
AND l.deleted_at IS NULL
|
|
2793
|
+
AND a.deleted_at IS NULL
|
|
2794
|
+
ORDER BY COALESCE(l.sort_order, 2147483647) ASC, a.updated_at DESC, a.asset_id ASC
|
|
2795
|
+
`).all(this.tenantId, workspaceId, taskId)
|
|
2796
|
+
: this.db.prepare(`
|
|
2797
|
+
SELECT
|
|
2798
|
+
a.asset_id,
|
|
2799
|
+
a.kind,
|
|
2800
|
+
a.reference_number,
|
|
2801
|
+
a.storage_provider,
|
|
2802
|
+
a.storage_key,
|
|
2803
|
+
a.logical_name,
|
|
2804
|
+
a.original_filename,
|
|
2805
|
+
a.updated_at,
|
|
2806
|
+
a.created_at,
|
|
2807
|
+
l.role,
|
|
2808
|
+
l.display_name
|
|
2809
|
+
FROM workspace_asset_links l
|
|
2810
|
+
JOIN workspace_assets a
|
|
2811
|
+
ON a.tenant_id = l.tenant_id
|
|
2812
|
+
AND a.workspace_id = l.workspace_id
|
|
2813
|
+
AND a.asset_id = l.asset_id
|
|
2814
|
+
WHERE l.tenant_id = ?
|
|
2815
|
+
AND l.workspace_id = ?
|
|
2816
|
+
AND l.task_id = ?
|
|
2817
|
+
AND l.deleted_at IS NULL
|
|
2818
|
+
AND a.deleted_at IS NULL
|
|
2819
|
+
ORDER BY COALESCE(l.sort_order, 2147483647) ASC, a.updated_at DESC, a.asset_id ASC
|
|
2820
|
+
`).all(this.tenantId, workspaceId, taskId));
|
|
2698
2821
|
const attachments = rows.map((row) => {
|
|
2699
2822
|
const storageKey = String(row.storage_key || '').trim();
|
|
2700
2823
|
const storageProvider = String(row.storage_provider || 'local').trim();
|
|
@@ -3086,6 +3209,63 @@ export class TaskforceCore {
|
|
|
3086
3209
|
`).all(this.tenantId, normalizedWorkspaceId);
|
|
3087
3210
|
return rows.map((row) => this.rowToWorkstream(row));
|
|
3088
3211
|
}
|
|
3212
|
+
listPlanningBootstrap(workspaceId, diagnostics) {
|
|
3213
|
+
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
3214
|
+
const startedAt = Date.now();
|
|
3215
|
+
const initiatives = this.listInitiatives(workspaceId);
|
|
3216
|
+
const initiativesLoadedAt = Date.now();
|
|
3217
|
+
const workstreams = this.listWorkstreams(workspaceId);
|
|
3218
|
+
const workstreamsLoadedAt = Date.now();
|
|
3219
|
+
const planningTaskRows = this.db.prepare(`
|
|
3220
|
+
SELECT id, reference_number, title, status, workstream_id
|
|
3221
|
+
FROM tasks
|
|
3222
|
+
WHERE tenant_id = ? AND workspace_id = ? AND is_archived = 0
|
|
3223
|
+
AND workstream_id IS NOT NULL AND TRIM(workstream_id) <> ''
|
|
3224
|
+
ORDER BY created_at DESC
|
|
3225
|
+
`).all(this.tenantId, normalizedWorkspaceId);
|
|
3226
|
+
const taskRowsLoadedAt = Date.now();
|
|
3227
|
+
const workstreamTaskSummaries = new Map();
|
|
3228
|
+
for (const row of planningTaskRows) {
|
|
3229
|
+
const workstreamId = String(row.workstream_id || '').trim();
|
|
3230
|
+
if (!workstreamId)
|
|
3231
|
+
continue;
|
|
3232
|
+
const status = this.normalizeStatusValue(row.status);
|
|
3233
|
+
const referenceNumber = this.sanitizeTaskReferenceNumber(row.reference_number);
|
|
3234
|
+
const current = workstreamTaskSummaries.get(workstreamId) || {
|
|
3235
|
+
workstreamId,
|
|
3236
|
+
taskCount: 0,
|
|
3237
|
+
completedTaskCount: 0,
|
|
3238
|
+
tasks: [],
|
|
3239
|
+
};
|
|
3240
|
+
current.taskCount += 1;
|
|
3241
|
+
if (status === 'done' || status === 'cancelled') {
|
|
3242
|
+
current.completedTaskCount += 1;
|
|
3243
|
+
}
|
|
3244
|
+
current.tasks.push({
|
|
3245
|
+
id: String(row.id || '').trim(),
|
|
3246
|
+
referenceNumber,
|
|
3247
|
+
title: String(row.title || '').trim() || 'Untitled Task',
|
|
3248
|
+
status,
|
|
3249
|
+
});
|
|
3250
|
+
workstreamTaskSummaries.set(workstreamId, current);
|
|
3251
|
+
}
|
|
3252
|
+
const summariesBuiltAt = Date.now();
|
|
3253
|
+
if (diagnostics) {
|
|
3254
|
+
diagnostics.initiativeCount = initiatives.length;
|
|
3255
|
+
diagnostics.workstreamCount = workstreams.length;
|
|
3256
|
+
diagnostics.workstreamLinkedTaskCount = planningTaskRows.length;
|
|
3257
|
+
diagnostics.workstreamTaskSummaryCount = workstreamTaskSummaries.size;
|
|
3258
|
+
diagnostics.initiativeLoadMs = Math.max(0, initiativesLoadedAt - startedAt);
|
|
3259
|
+
diagnostics.workstreamLoadMs = Math.max(0, workstreamsLoadedAt - initiativesLoadedAt);
|
|
3260
|
+
diagnostics.taskSummaryQueryMs = Math.max(0, taskRowsLoadedAt - workstreamsLoadedAt);
|
|
3261
|
+
diagnostics.taskSummaryBuildMs = Math.max(0, summariesBuiltAt - taskRowsLoadedAt);
|
|
3262
|
+
}
|
|
3263
|
+
return {
|
|
3264
|
+
initiatives,
|
|
3265
|
+
workstreams,
|
|
3266
|
+
workstreamTaskSummaries: Array.from(workstreamTaskSummaries.values()),
|
|
3267
|
+
};
|
|
3268
|
+
}
|
|
3089
3269
|
createWorkstream(input, workspaceId) {
|
|
3090
3270
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
3091
3271
|
const id = String(input.id || '').trim() || this.generateEntityId('workstream');
|
|
@@ -3860,7 +4040,7 @@ export class TaskforceCore {
|
|
|
3860
4040
|
listTasks(workspaceId) {
|
|
3861
4041
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
3862
4042
|
const rows = this.db.prepare(`
|
|
3863
|
-
SELECT * FROM tasks
|
|
4043
|
+
SELECT * FROM tasks
|
|
3864
4044
|
WHERE tenant_id = ?
|
|
3865
4045
|
AND workspace_id = ?
|
|
3866
4046
|
AND is_archived = 0
|
|
@@ -3871,6 +4051,112 @@ export class TaskforceCore {
|
|
|
3871
4051
|
config: this.readConfig()
|
|
3872
4052
|
};
|
|
3873
4053
|
}
|
|
4054
|
+
/**
|
|
4055
|
+
* Filter-pushdown listing used by read-only surfaces that don't need the
|
|
4056
|
+
* full task graph (MCP list_tasks, CLI summaries). Avoids the per-task
|
|
4057
|
+
* comments/events/attachments/checklist subqueries and actor resolution
|
|
4058
|
+
* that rowToTask performs, and applies filters + sort + limit at the SQL
|
|
4059
|
+
* layer so hot polls don't load the entire workspace.
|
|
4060
|
+
*/
|
|
4061
|
+
listTasksSlim(workspaceId, filters = {}) {
|
|
4062
|
+
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
4063
|
+
const clauses = [
|
|
4064
|
+
'tenant_id = ?',
|
|
4065
|
+
'workspace_id = ?',
|
|
4066
|
+
'is_archived = 0',
|
|
4067
|
+
];
|
|
4068
|
+
const params = [this.tenantId, normalizedWorkspaceId];
|
|
4069
|
+
if (filters.category) {
|
|
4070
|
+
clauses.push('category = ?');
|
|
4071
|
+
params.push(filters.category);
|
|
4072
|
+
}
|
|
4073
|
+
if (filters.status) {
|
|
4074
|
+
clauses.push('status = ?');
|
|
4075
|
+
params.push(filters.status);
|
|
4076
|
+
}
|
|
4077
|
+
else if (filters.openOnly) {
|
|
4078
|
+
clauses.push("status IN ('task','on-hold','in-progress','review')");
|
|
4079
|
+
}
|
|
4080
|
+
if (filters.priority !== undefined) {
|
|
4081
|
+
clauses.push('priority = ?');
|
|
4082
|
+
params.push(Number(filters.priority));
|
|
4083
|
+
}
|
|
4084
|
+
if (filters.type) {
|
|
4085
|
+
clauses.push('type = ?');
|
|
4086
|
+
params.push(filters.type);
|
|
4087
|
+
}
|
|
4088
|
+
if (filters.assignee) {
|
|
4089
|
+
if (filters.assignee === 'unassigned') {
|
|
4090
|
+
clauses.push("(assignee IS NULL OR assignee = '' OR assignee = 'unassigned')");
|
|
4091
|
+
}
|
|
4092
|
+
else {
|
|
4093
|
+
clauses.push('assignee = ?');
|
|
4094
|
+
params.push(filters.assignee);
|
|
4095
|
+
}
|
|
4096
|
+
}
|
|
4097
|
+
const order = filters.order === 'asc' ? 'ASC' : 'DESC';
|
|
4098
|
+
const sort = filters.sort ?? 'created';
|
|
4099
|
+
let orderBy;
|
|
4100
|
+
if (sort === 'priority') {
|
|
4101
|
+
orderBy = `priority ${order}, created_at ${order}`;
|
|
4102
|
+
}
|
|
4103
|
+
else if (sort === 'updated') {
|
|
4104
|
+
orderBy = `COALESCE(updated_at, created_at) ${order}`;
|
|
4105
|
+
}
|
|
4106
|
+
else {
|
|
4107
|
+
orderBy = `created_at ${order}`;
|
|
4108
|
+
}
|
|
4109
|
+
let sql = `SELECT * FROM tasks WHERE ${clauses.join(' AND ')} ORDER BY ${orderBy}`;
|
|
4110
|
+
const rawLimit = Number(filters.limit);
|
|
4111
|
+
const limit = Number.isFinite(rawLimit) ? Math.max(0, Math.floor(rawLimit)) : 0;
|
|
4112
|
+
if (limit > 0) {
|
|
4113
|
+
sql += ' LIMIT ?';
|
|
4114
|
+
params.push(limit);
|
|
4115
|
+
}
|
|
4116
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
4117
|
+
return rows.map((row) => {
|
|
4118
|
+
const sanitizedAssignee = this.sanitizeAssignee(row.assignee, normalizedWorkspaceId);
|
|
4119
|
+
const scheduledDate = this.sanitizeDateOnly(row.scheduled_date);
|
|
4120
|
+
const dueDate = this.sanitizeDateOnly(row.due_date);
|
|
4121
|
+
const scheduledWeekKey = row.scheduled_week_key
|
|
4122
|
+
|| (scheduledDate ? this.dateOnlyToIsoWeekKey(scheduledDate) : null);
|
|
4123
|
+
const referenceNumber = this.sanitizeTaskReferenceNumber(row.reference_number);
|
|
4124
|
+
const localReferenceNumber = this.sanitizeTaskReferenceNumber(row.local_reference_number);
|
|
4125
|
+
let taxonomies = {};
|
|
4126
|
+
try {
|
|
4127
|
+
taxonomies = JSON.parse(row.taxonomies || '{}');
|
|
4128
|
+
}
|
|
4129
|
+
catch {
|
|
4130
|
+
taxonomies = {};
|
|
4131
|
+
}
|
|
4132
|
+
return {
|
|
4133
|
+
id: String(row.id || '').trim(),
|
|
4134
|
+
referenceNumber,
|
|
4135
|
+
localReferenceNumber,
|
|
4136
|
+
referenceLabel: deriveTaskReferenceLabel({ referenceNumber, localReferenceNumber }),
|
|
4137
|
+
title: row.title,
|
|
4138
|
+
status: this.normalizeStatusValue(row.status),
|
|
4139
|
+
priority: this.normalizePriorityValue(row.priority),
|
|
4140
|
+
complexity: row.complexity,
|
|
4141
|
+
type: row.type,
|
|
4142
|
+
category: row.category,
|
|
4143
|
+
approach: row.approach || undefined,
|
|
4144
|
+
createdAt: row.created_at,
|
|
4145
|
+
updatedAt: row.updated_at,
|
|
4146
|
+
completedAt: row.completed_at,
|
|
4147
|
+
canceledReason: row.canceled_reason,
|
|
4148
|
+
isArchived: row.is_archived === 1,
|
|
4149
|
+
scheduledDate,
|
|
4150
|
+
dueDate,
|
|
4151
|
+
scheduledWeekKey,
|
|
4152
|
+
orderInDay: this.sanitizeOrderInDay(row.order_in_day),
|
|
4153
|
+
workstreamId: this.sanitizeWorkstreamId(row.workstream_id),
|
|
4154
|
+
taxonomies,
|
|
4155
|
+
createdBy: this.normalizeCreatedBy(row.created_by, normalizedWorkspaceId, { preserveGenericAi: true }),
|
|
4156
|
+
assignee: sanitizedAssignee,
|
|
4157
|
+
};
|
|
4158
|
+
});
|
|
4159
|
+
}
|
|
3874
4160
|
getDataVersion() {
|
|
3875
4161
|
if (this.db.provider === 'sqlite') {
|
|
3876
4162
|
const version = this.db.pragma('data_version', { simple: true });
|
|
@@ -4011,9 +4297,9 @@ export class TaskforceCore {
|
|
|
4011
4297
|
`).get(this.tenantId, userId);
|
|
4012
4298
|
if (!existingUser) {
|
|
4013
4299
|
this.db.prepare(`
|
|
4014
|
-
INSERT INTO users (id, tenant_id, email, display_name,
|
|
4015
|
-
VALUES (?, ?, ?, ?, ?, ?, ?,
|
|
4016
|
-
`).run(userId, this.tenantId, email, displayName,
|
|
4300
|
+
INSERT INTO users (id, tenant_id, email, display_name, beta_access, is_disabled, created_at, updated_at)
|
|
4301
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
4302
|
+
`).run(userId, this.tenantId, email, displayName, 1, 0, now, now);
|
|
4017
4303
|
}
|
|
4018
4304
|
const membership = this.db.prepare(`
|
|
4019
4305
|
SELECT id, role FROM workspace_memberships
|
|
@@ -4058,16 +4344,17 @@ export class TaskforceCore {
|
|
|
4058
4344
|
`).get(this.tenantId, normalizedUserId, normalizedWorkspaceId);
|
|
4059
4345
|
if (!membership?.workspace_id)
|
|
4060
4346
|
return null;
|
|
4061
|
-
const
|
|
4347
|
+
const accountDisabled = Boolean(userRow.is_disabled);
|
|
4062
4348
|
const userBetaAccess = userRow.beta_access === undefined || userRow.beta_access === null
|
|
4063
4349
|
? true
|
|
4064
4350
|
: Boolean(userRow.beta_access);
|
|
4065
4351
|
const isSystemAdmin = this.normalizeSystemRole(userRow.system_role, 'user') === 'system_admin';
|
|
4066
4352
|
const membershipActive = !membership.status || String(membership.status).toLowerCase() === 'active';
|
|
4353
|
+
const workspaceDisabled = !membershipActive;
|
|
4067
4354
|
const baseRole = this.normalizeAccessRole(membership.role || fallbackRole, fallbackRole);
|
|
4068
4355
|
const permissionMode = this.normalizePermissionMode(membership.permission_mode, 'read-write');
|
|
4069
4356
|
const isMemberReadOnly = baseRole === 'member' && permissionMode === 'read-only';
|
|
4070
|
-
const resolvedRole = (
|
|
4357
|
+
const resolvedRole = (accountDisabled || workspaceDisabled || isMemberReadOnly)
|
|
4071
4358
|
? 'read-only'
|
|
4072
4359
|
: baseRole;
|
|
4073
4360
|
return {
|
|
@@ -4075,7 +4362,9 @@ export class TaskforceCore {
|
|
|
4075
4362
|
workspaceId: String(membership.workspace_id || normalizedWorkspaceId),
|
|
4076
4363
|
role: resolvedRole,
|
|
4077
4364
|
permissionMode,
|
|
4078
|
-
disabled,
|
|
4365
|
+
disabled: accountDisabled || workspaceDisabled,
|
|
4366
|
+
accountDisabled,
|
|
4367
|
+
workspaceDisabled,
|
|
4079
4368
|
betaAccess: userBetaAccess || isSystemAdmin
|
|
4080
4369
|
};
|
|
4081
4370
|
}
|
|
@@ -4294,7 +4583,68 @@ export class TaskforceCore {
|
|
|
4294
4583
|
hasAnySystemAdmin() {
|
|
4295
4584
|
return this.countSystemAdmins() > 0;
|
|
4296
4585
|
}
|
|
4297
|
-
|
|
4586
|
+
countSystemUsers(input) {
|
|
4587
|
+
const query = String(input?.query || '').trim().toLowerCase();
|
|
4588
|
+
const queryPattern = query ? `%${query}%` : '';
|
|
4589
|
+
const searchClause = query
|
|
4590
|
+
? `
|
|
4591
|
+
AND (
|
|
4592
|
+
LOWER(COALESCE(u.display_name, '')) LIKE ?
|
|
4593
|
+
OR LOWER(COALESCE(u.id, '')) LIKE ?
|
|
4594
|
+
OR LOWER(COALESCE(u.email, '')) LIKE ?
|
|
4595
|
+
OR LOWER(COALESCE(COALESCE(p.display_name, v.plan_id), '')) LIKE ?
|
|
4596
|
+
OR LOWER(COALESCE(v.plan_id, '')) LIKE ?
|
|
4597
|
+
OR LOWER(COALESCE(e.entitlement_state, '')) LIKE ?
|
|
4598
|
+
OR LOWER(COALESCE(u.system_role, 'user')) LIKE ?
|
|
4599
|
+
)`
|
|
4600
|
+
: '';
|
|
4601
|
+
const params = [this.tenantId];
|
|
4602
|
+
if (query) {
|
|
4603
|
+
params.push(queryPattern, queryPattern, queryPattern, queryPattern, queryPattern, queryPattern, queryPattern);
|
|
4604
|
+
}
|
|
4605
|
+
const row = this.db.prepare(`
|
|
4606
|
+
SELECT COUNT(*) AS count
|
|
4607
|
+
FROM users u
|
|
4608
|
+
LEFT JOIN account_entitlements e
|
|
4609
|
+
ON e.tenant_id = u.tenant_id
|
|
4610
|
+
AND e.account_id = u.id
|
|
4611
|
+
LEFT JOIN plan_versions v
|
|
4612
|
+
ON v.tenant_id = u.tenant_id
|
|
4613
|
+
AND v.plan_version_id = e.plan_version_id
|
|
4614
|
+
LEFT JOIN plan_catalog p
|
|
4615
|
+
ON p.tenant_id = u.tenant_id
|
|
4616
|
+
AND p.plan_id = v.plan_id
|
|
4617
|
+
WHERE u.tenant_id = ?
|
|
4618
|
+
${searchClause}
|
|
4619
|
+
`).get(...params);
|
|
4620
|
+
return Number(row?.count || 0);
|
|
4621
|
+
}
|
|
4622
|
+
listSystemUsers(input) {
|
|
4623
|
+
const query = String(input?.query || '').trim().toLowerCase();
|
|
4624
|
+
const queryPattern = query ? `%${query}%` : '';
|
|
4625
|
+
const sort = input?.sort === 'oldest' ? 'oldest' : 'newest';
|
|
4626
|
+
const limit = Number.isFinite(Number(input?.limit)) ? Math.max(1, Math.min(200, Math.floor(Number(input?.limit)))) : 50;
|
|
4627
|
+
const offset = Number.isFinite(Number(input?.offset)) ? Math.max(0, Math.floor(Number(input?.offset))) : 0;
|
|
4628
|
+
const orderByClause = sort === 'oldest'
|
|
4629
|
+
? `ORDER BY COALESCE(u.created_at, u.updated_at) ASC, COALESCE(u.updated_at, u.created_at) ASC, u.id ASC`
|
|
4630
|
+
: `ORDER BY COALESCE(u.created_at, u.updated_at) DESC, COALESCE(u.updated_at, u.created_at) DESC, u.id DESC`;
|
|
4631
|
+
const searchClause = query
|
|
4632
|
+
? `
|
|
4633
|
+
AND (
|
|
4634
|
+
LOWER(COALESCE(u.display_name, '')) LIKE ?
|
|
4635
|
+
OR LOWER(COALESCE(u.id, '')) LIKE ?
|
|
4636
|
+
OR LOWER(COALESCE(u.email, '')) LIKE ?
|
|
4637
|
+
OR LOWER(COALESCE(COALESCE(p.display_name, v.plan_id), '')) LIKE ?
|
|
4638
|
+
OR LOWER(COALESCE(v.plan_id, '')) LIKE ?
|
|
4639
|
+
OR LOWER(COALESCE(e.entitlement_state, '')) LIKE ?
|
|
4640
|
+
OR LOWER(COALESCE(u.system_role, 'user')) LIKE ?
|
|
4641
|
+
)`
|
|
4642
|
+
: '';
|
|
4643
|
+
const params = [this.tenantId];
|
|
4644
|
+
if (query) {
|
|
4645
|
+
params.push(queryPattern, queryPattern, queryPattern, queryPattern, queryPattern, queryPattern, queryPattern);
|
|
4646
|
+
}
|
|
4647
|
+
params.push(limit, offset);
|
|
4298
4648
|
const rows = this.db.prepare(`
|
|
4299
4649
|
SELECT
|
|
4300
4650
|
u.id AS user_id,
|
|
@@ -4321,8 +4671,10 @@ export class TaskforceCore {
|
|
|
4321
4671
|
ON p.tenant_id = u.tenant_id
|
|
4322
4672
|
AND p.plan_id = v.plan_id
|
|
4323
4673
|
WHERE u.tenant_id = ?
|
|
4324
|
-
|
|
4325
|
-
|
|
4674
|
+
${searchClause}
|
|
4675
|
+
${orderByClause}
|
|
4676
|
+
LIMIT ? OFFSET ?
|
|
4677
|
+
`).all(...params);
|
|
4326
4678
|
return rows
|
|
4327
4679
|
.filter((row) => row.user_id && row.email)
|
|
4328
4680
|
.map((row) => ({
|
|
@@ -4614,6 +4966,7 @@ export class TaskforceCore {
|
|
|
4614
4966
|
u.id AS user_id,
|
|
4615
4967
|
u.email AS email,
|
|
4616
4968
|
u.display_name AS display_name,
|
|
4969
|
+
u.avatar_url AS avatar_url,
|
|
4617
4970
|
u.is_disabled AS is_disabled,
|
|
4618
4971
|
u.created_at AS created_at,
|
|
4619
4972
|
u.updated_at AS updated_at,
|
|
@@ -4630,19 +4983,23 @@ export class TaskforceCore {
|
|
|
4630
4983
|
AND m.workspace_id = ?
|
|
4631
4984
|
ORDER BY COALESCE(m.updated_at, m.created_at) DESC, u.created_at DESC
|
|
4632
4985
|
`).all(this.tenantId, normalizedWorkspaceId);
|
|
4633
|
-
return rows.map((row) =>
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4986
|
+
return rows.map((row) => {
|
|
4987
|
+
const status = String(row.status || 'active');
|
|
4988
|
+
return {
|
|
4989
|
+
userId: row.user_id,
|
|
4990
|
+
email: row.email,
|
|
4991
|
+
displayName: row.display_name || null,
|
|
4992
|
+
avatarUrl: row.avatar_url || null,
|
|
4993
|
+
role: this.normalizeAccessRole(row.role || 'member', 'member'),
|
|
4994
|
+
permissionMode: this.normalizePermissionMode(row.permission_mode, 'read-write'),
|
|
4995
|
+
betaAccess: row.beta_access === undefined || row.beta_access === null ? true : Boolean(row.beta_access),
|
|
4996
|
+
status,
|
|
4997
|
+
disabled: Boolean(row.is_disabled) || status.toLowerCase() === 'disabled',
|
|
4998
|
+
createdAt: row.created_at || null,
|
|
4999
|
+
updatedAt: row.updated_at || null,
|
|
5000
|
+
emailVerifiedAt: row.email_verified_at || null
|
|
5001
|
+
};
|
|
5002
|
+
});
|
|
4646
5003
|
}
|
|
4647
5004
|
replaceWorkspaceUsersForSync(input) {
|
|
4648
5005
|
const workspaceId = String(input.workspaceId || 'default').trim() || 'default';
|
|
@@ -4686,9 +5043,9 @@ export class TaskforceCore {
|
|
|
4686
5043
|
}
|
|
4687
5044
|
else {
|
|
4688
5045
|
this.db.prepare(`
|
|
4689
|
-
INSERT INTO users (id, tenant_id, email, display_name,
|
|
4690
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?,
|
|
4691
|
-
`).run(userId, this.tenantId, email, displayName,
|
|
5046
|
+
INSERT INTO users (id, tenant_id, email, display_name, beta_access, is_disabled, created_at, updated_at, email_verified_at)
|
|
5047
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
5048
|
+
`).run(userId, this.tenantId, email, displayName, betaAccess ? 1 : 0, disabled ? 1 : 0, createdAt, updatedAt, emailVerifiedAt);
|
|
4692
5049
|
}
|
|
4693
5050
|
const membership = this.db.prepare(`
|
|
4694
5051
|
SELECT id
|
|
@@ -4731,8 +5088,8 @@ export class TaskforceCore {
|
|
|
4731
5088
|
tx();
|
|
4732
5089
|
return this.listWorkspaceUsers(workspaceId);
|
|
4733
5090
|
}
|
|
4734
|
-
listAiProfiles(workspaceId) {
|
|
4735
|
-
return listAiProfilesService(this.getAiProfileServiceDeps(), workspaceId);
|
|
5091
|
+
listAiProfiles(workspaceId, options) {
|
|
5092
|
+
return listAiProfilesService(this.getAiProfileServiceDeps(), workspaceId, options);
|
|
4736
5093
|
}
|
|
4737
5094
|
getAiProfileByToken(input) {
|
|
4738
5095
|
return getAiProfileByTokenService(this.getAiProfileServiceDeps(), input);
|
|
@@ -4743,8 +5100,14 @@ export class TaskforceCore {
|
|
|
4743
5100
|
mergeAiProfiles(input) {
|
|
4744
5101
|
return mergeAiProfilesService(this.getAiProfileServiceDeps(), input);
|
|
4745
5102
|
}
|
|
4746
|
-
|
|
4747
|
-
return
|
|
5103
|
+
archiveAiProfile(input) {
|
|
5104
|
+
return archiveAiProfileService(this.getAiProfileServiceDeps(), input).profile;
|
|
5105
|
+
}
|
|
5106
|
+
updateAiProfileAvatar(input) {
|
|
5107
|
+
return updateAiProfileAvatarService(this.getAiProfileServiceDeps(), input);
|
|
5108
|
+
}
|
|
5109
|
+
resolveAiProfile(input, options) {
|
|
5110
|
+
return resolveAiProfileService(this.getAiProfileServiceDeps(), input, options);
|
|
4748
5111
|
}
|
|
4749
5112
|
listAnnotatedAttachmentSessions(input) {
|
|
4750
5113
|
return listAnnotatedAttachmentSessionsService(this.getAnnotatedAttachmentServiceDeps(), input);
|
|
@@ -4774,17 +5137,30 @@ export class TaskforceCore {
|
|
|
4774
5137
|
const activeSessions = deps.sessionStore.listByWorkspace('default', { includeDeleted: false });
|
|
4775
5138
|
if (activeSessions.length === 0)
|
|
4776
5139
|
return;
|
|
4777
|
-
const hasActiveImageLink = this.
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
5140
|
+
const hasActiveImageLink = this.supportsWorkspaceAssetLinkTargetType()
|
|
5141
|
+
? this.db.prepare(`
|
|
5142
|
+
SELECT 1
|
|
5143
|
+
FROM workspace_asset_links
|
|
5144
|
+
WHERE tenant_id = ?
|
|
5145
|
+
AND workspace_id = ?
|
|
5146
|
+
AND target_type = 'task'
|
|
5147
|
+
AND task_id = ?
|
|
5148
|
+
AND asset_id = ?
|
|
5149
|
+
AND role = 'image'
|
|
5150
|
+
AND deleted_at IS NULL
|
|
5151
|
+
LIMIT 1
|
|
5152
|
+
`)
|
|
5153
|
+
: this.db.prepare(`
|
|
5154
|
+
SELECT 1
|
|
5155
|
+
FROM workspace_asset_links
|
|
5156
|
+
WHERE tenant_id = ?
|
|
5157
|
+
AND workspace_id = ?
|
|
5158
|
+
AND task_id = ?
|
|
5159
|
+
AND asset_id = ?
|
|
5160
|
+
AND role = 'image'
|
|
5161
|
+
AND deleted_at IS NULL
|
|
5162
|
+
LIMIT 1
|
|
5163
|
+
`);
|
|
4788
5164
|
for (const session of activeSessions) {
|
|
4789
5165
|
const taskId = String(session.taskId || '').trim();
|
|
4790
5166
|
const imageAssetId = String(session.baseImageAssetId || '').trim();
|
|
@@ -4912,7 +5288,8 @@ export class TaskforceCore {
|
|
|
4912
5288
|
aiProfileRowToRecord: (row) => this.aiProfileRowToRecord(row),
|
|
4913
5289
|
generateEntityId: (prefix) => this.generateEntityId(prefix),
|
|
4914
5290
|
issueProfileToken: () => `tfp_${randomBytes(24).toString('hex')}`,
|
|
4915
|
-
|
|
5291
|
+
countAiProfileUsage: (workspaceId) => this.countAiProfileUsage(workspaceId),
|
|
5292
|
+
createRuleError: (message, statusCode = 400, code = 'TASK_RULE_VIOLATION', details) => new TaskforceRuleError(message, statusCode, code, details),
|
|
4916
5293
|
};
|
|
4917
5294
|
}
|
|
4918
5295
|
resolveWorkspaceActor(workspaceId, actorRef) {
|
|
@@ -4942,6 +5319,7 @@ export class TaskforceCore {
|
|
|
4942
5319
|
FROM ai_profiles
|
|
4943
5320
|
WHERE tenant_id = ?
|
|
4944
5321
|
AND workspace_id = ?
|
|
5322
|
+
AND archived_at IS NULL
|
|
4945
5323
|
AND (
|
|
4946
5324
|
LOWER(name) = LOWER(?)
|
|
4947
5325
|
OR LOWER(username) = LOWER(?)
|
|
@@ -4952,6 +5330,22 @@ export class TaskforceCore {
|
|
|
4952
5330
|
if (aiProfileByName) {
|
|
4953
5331
|
return this.aiProfileToActor(this.aiProfileRowToRecord(aiProfileByName));
|
|
4954
5332
|
}
|
|
5333
|
+
const archivedAiProfileByName = this.db.prepare(`
|
|
5334
|
+
SELECT *
|
|
5335
|
+
FROM ai_profiles
|
|
5336
|
+
WHERE tenant_id = ?
|
|
5337
|
+
AND workspace_id = ?
|
|
5338
|
+
AND archived_at IS NOT NULL
|
|
5339
|
+
AND (
|
|
5340
|
+
LOWER(name) = LOWER(?)
|
|
5341
|
+
OR LOWER(username) = LOWER(?)
|
|
5342
|
+
)
|
|
5343
|
+
ORDER BY COALESCE(last_active_at, updated_at, created_at) DESC, id ASC
|
|
5344
|
+
LIMIT 1
|
|
5345
|
+
`).get(this.tenantId, normalizedWorkspaceId, raw, raw);
|
|
5346
|
+
if (archivedAiProfileByName) {
|
|
5347
|
+
return this.aiProfileToActor(this.aiProfileRowToRecord(archivedAiProfileByName));
|
|
5348
|
+
}
|
|
4955
5349
|
const workspaceUser = this.listWorkspaceUsers(normalizedWorkspaceId).find((user) => user.userId === raw);
|
|
4956
5350
|
if (workspaceUser) {
|
|
4957
5351
|
return this.workspaceUserToActor(workspaceUser);
|
|
@@ -5195,6 +5589,9 @@ export class TaskforceCore {
|
|
|
5195
5589
|
listPlanFeatureCatalog() {
|
|
5196
5590
|
return this.planEntitlementService.listPlanFeatureCatalog();
|
|
5197
5591
|
}
|
|
5592
|
+
updatePlanFeatureCatalogMarketingCopy(input) {
|
|
5593
|
+
return this.planEntitlementService.updatePlanFeatureCatalogMarketingCopy(input);
|
|
5594
|
+
}
|
|
5198
5595
|
listPlanVersions(planId) {
|
|
5199
5596
|
return this.planEntitlementService.listPlanVersions(planId);
|
|
5200
5597
|
}
|
|
@@ -5314,11 +5711,6 @@ export class TaskforceCore {
|
|
|
5314
5711
|
}
|
|
5315
5712
|
}
|
|
5316
5713
|
const now = new Date().toISOString();
|
|
5317
|
-
this.db.prepare(`
|
|
5318
|
-
UPDATE users
|
|
5319
|
-
SET is_disabled = ?, updated_at = ?
|
|
5320
|
-
WHERE tenant_id = ? AND id = ?
|
|
5321
|
-
`).run(disabled ? 1 : 0, now, this.tenantId, normalizedUserId);
|
|
5322
5714
|
if (membership?.id) {
|
|
5323
5715
|
this.db.prepare(`
|
|
5324
5716
|
UPDATE workspace_memberships
|
|
@@ -5492,9 +5884,7 @@ export class TaskforceCore {
|
|
|
5492
5884
|
if (!email || !email.includes('@')) {
|
|
5493
5885
|
throw new TaskforceRuleError('Valid email is required', 400, 'EMAIL_REQUIRED');
|
|
5494
5886
|
}
|
|
5495
|
-
|
|
5496
|
-
throw new TaskforceRuleError('Password must be at least 8 characters', 400, 'PASSWORD_TOO_SHORT');
|
|
5497
|
-
}
|
|
5887
|
+
assertPasswordPolicy(password, { email, displayName: input.displayName || null });
|
|
5498
5888
|
if (!workspaceId || workspaceId.toLowerCase() === 'default') {
|
|
5499
5889
|
throw new TaskforceRuleError('A non-default workspaceId is required', 400, 'WORKSPACE_ID_REQUIRED');
|
|
5500
5890
|
}
|
|
@@ -5511,20 +5901,16 @@ export class TaskforceCore {
|
|
|
5511
5901
|
email,
|
|
5512
5902
|
displayName
|
|
5513
5903
|
});
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
SET password_hash = ?, auth_provider = ?, external_auth_id = ?, beta_access = ?, is_disabled = 0, updated_at = ?
|
|
5525
|
-
WHERE tenant_id = ? AND id = ?
|
|
5526
|
-
`).run(hashPassword(password), 'password', email, betaAccess ? 1 : 0, now, this.tenantId, userId);
|
|
5527
|
-
}
|
|
5904
|
+
this.db.prepare(`
|
|
5905
|
+
UPDATE users
|
|
5906
|
+
SET
|
|
5907
|
+
email_verified_at = CASE WHEN ? = 1 THEN COALESCE(email_verified_at, ?) ELSE email_verified_at END,
|
|
5908
|
+
beta_access = ?,
|
|
5909
|
+
is_disabled = 0,
|
|
5910
|
+
updated_at = ?
|
|
5911
|
+
WHERE tenant_id = ? AND id = ?
|
|
5912
|
+
`).run(emailVerified ? 1 : 0, now, betaAccess ? 1 : 0, now, this.tenantId, userId);
|
|
5913
|
+
this.identityService.setPasswordIdentity(userId, password);
|
|
5528
5914
|
this.updateWorkspaceUserRole(userId, workspaceId, role);
|
|
5529
5915
|
this.setWorkspaceUserDisabled(userId, workspaceId, false);
|
|
5530
5916
|
return { userId, workspaceId, role };
|
|
@@ -5535,9 +5921,7 @@ export class TaskforceCore {
|
|
|
5535
5921
|
if (!email || !email.includes('@')) {
|
|
5536
5922
|
throw new TaskforceRuleError('Valid email is required', 400, 'EMAIL_REQUIRED');
|
|
5537
5923
|
}
|
|
5538
|
-
|
|
5539
|
-
throw new TaskforceRuleError('Password must be at least 8 characters', 400, 'PASSWORD_TOO_SHORT');
|
|
5540
|
-
}
|
|
5924
|
+
assertPasswordPolicy(password, { email, displayName: input.displayName || null });
|
|
5541
5925
|
const existing = this.db.prepare(`
|
|
5542
5926
|
SELECT id FROM users WHERE tenant_id = ? AND email = ? LIMIT 1
|
|
5543
5927
|
`).get(this.tenantId, email);
|
|
@@ -5566,9 +5950,10 @@ export class TaskforceCore {
|
|
|
5566
5950
|
const userId = this.generateEntityId('user');
|
|
5567
5951
|
const displayName = String(input.displayName || email.split('@')[0] || email).trim() || email;
|
|
5568
5952
|
this.db.prepare(`
|
|
5569
|
-
INSERT INTO users (id, tenant_id, email, display_name,
|
|
5570
|
-
VALUES (?, ?, ?, ?, ?, ?,
|
|
5571
|
-
`).run(userId, this.tenantId, email, displayName,
|
|
5953
|
+
INSERT INTO users (id, tenant_id, email, display_name, system_role, beta_access, is_disabled, created_at, updated_at)
|
|
5954
|
+
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)
|
|
5955
|
+
`).run(userId, this.tenantId, email, displayName, assignFirstSystemAdmin ? 'system_admin' : 'user', betaAccess ? 1 : 0, now, now);
|
|
5956
|
+
this.identityService.createPasswordIdentity(userId, password);
|
|
5572
5957
|
applyRegistrationCommercialState(userId, now);
|
|
5573
5958
|
return {
|
|
5574
5959
|
userId,
|
|
@@ -5602,6 +5987,151 @@ export class TaskforceCore {
|
|
|
5602
5987
|
commercialState: onboardingOutcome.commercialState
|
|
5603
5988
|
};
|
|
5604
5989
|
}
|
|
5990
|
+
// ─── OAuth state management (delegated to AuthIdentityService) ────────────
|
|
5991
|
+
/**
|
|
5992
|
+
* Creates an OAuth state record and returns the state token + PKCE materials.
|
|
5993
|
+
* Delegates to AuthIdentityService.createOAuthState.
|
|
5994
|
+
*/
|
|
5995
|
+
createOAuthLoginState(input) {
|
|
5996
|
+
return this.identityService.createOAuthState(input);
|
|
5997
|
+
}
|
|
5998
|
+
/**
|
|
5999
|
+
* Atomically consumes an OAuth state token and returns the row.
|
|
6000
|
+
* Returns null if invalid, expired, or already consumed.
|
|
6001
|
+
*/
|
|
6002
|
+
consumeOAuthLoginState(state) {
|
|
6003
|
+
return this.identityService.consumeOAuthState(state);
|
|
6004
|
+
}
|
|
6005
|
+
// ─── OAuth identity operations ─────────────────────────────────────────────
|
|
6006
|
+
/**
|
|
6007
|
+
* Finds the Taskforce user associated with a provider identity.
|
|
6008
|
+
* Returns { userId, identityId } or null if not yet linked.
|
|
6009
|
+
*/
|
|
6010
|
+
findUserByOAuthProvider(provider, providerSubject) {
|
|
6011
|
+
return this.identityService.findUserByProviderIdentity({ provider, providerSubject });
|
|
6012
|
+
}
|
|
6013
|
+
/**
|
|
6014
|
+
* Links an OAuth provider identity to an existing user.
|
|
6015
|
+
* Throws IDENTITY_ALREADY_EXISTS if the user already has an identity for this provider.
|
|
6016
|
+
*/
|
|
6017
|
+
linkOAuthProviderToUser(input) {
|
|
6018
|
+
return this.identityService.linkProviderIdentity(input);
|
|
6019
|
+
}
|
|
6020
|
+
/**
|
|
6021
|
+
* Lists all active login identities for a user.
|
|
6022
|
+
* Used by the Account Hub "Login methods" section.
|
|
6023
|
+
*/
|
|
6024
|
+
listLoginMethodsForUser(userId) {
|
|
6025
|
+
return this.identityService.listIdentitiesForUser(userId);
|
|
6026
|
+
}
|
|
6027
|
+
/**
|
|
6028
|
+
* Unlinks a provider identity from a user.
|
|
6029
|
+
* Throws LAST_IDENTITY_REQUIRED if it would be the last active method.
|
|
6030
|
+
*/
|
|
6031
|
+
unlinkLoginMethod(userId, provider) {
|
|
6032
|
+
return this.identityService.unlinkProviderIdentity({ userId, provider });
|
|
6033
|
+
}
|
|
6034
|
+
/**
|
|
6035
|
+
* Adds or replaces a password identity for a user that signed up via OAuth.
|
|
6036
|
+
* Uses upsert semantics (safe to call on accounts that already have a password).
|
|
6037
|
+
*/
|
|
6038
|
+
addPasswordToAccount(userId, password) {
|
|
6039
|
+
const user = this.db.prepare(`
|
|
6040
|
+
SELECT email, display_name
|
|
6041
|
+
FROM users
|
|
6042
|
+
WHERE tenant_id = ? AND id = ?
|
|
6043
|
+
LIMIT 1
|
|
6044
|
+
`).get(this.tenantId, String(userId || '').trim());
|
|
6045
|
+
assertPasswordPolicy(password, {
|
|
6046
|
+
email: user?.email || null,
|
|
6047
|
+
displayName: user?.display_name || null
|
|
6048
|
+
});
|
|
6049
|
+
return this.identityService.setPasswordIdentity(userId, password);
|
|
6050
|
+
}
|
|
6051
|
+
/**
|
|
6052
|
+
* Registers a new user account via an OAuth provider (no password).
|
|
6053
|
+
*
|
|
6054
|
+
* Creates the user row, applies commercial onboarding, and links the
|
|
6055
|
+
* provider identity — all in a single transaction.
|
|
6056
|
+
*
|
|
6057
|
+
* Returns the same shape as `registerPasswordUser` so callers can use
|
|
6058
|
+
* the same session-issuance path.
|
|
6059
|
+
*/
|
|
6060
|
+
registerOAuthUser(input) {
|
|
6061
|
+
const email = input.email ? String(input.email).trim().toLowerCase() : null;
|
|
6062
|
+
if (email && !email.includes('@')) {
|
|
6063
|
+
throw new TaskforceRuleError('Valid email is required', 400, 'EMAIL_REQUIRED');
|
|
6064
|
+
}
|
|
6065
|
+
// Duplicate guard — email uniqueness (if email is provided)
|
|
6066
|
+
if (email) {
|
|
6067
|
+
const existing = this.db.prepare(`
|
|
6068
|
+
SELECT id FROM users WHERE tenant_id = ? AND email = ? LIMIT 1
|
|
6069
|
+
`).get(this.tenantId, email);
|
|
6070
|
+
if (existing?.id) {
|
|
6071
|
+
throw new TaskforceRuleError('Email is already registered', 409, 'EMAIL_ALREADY_EXISTS');
|
|
6072
|
+
}
|
|
6073
|
+
}
|
|
6074
|
+
const assignFirstSystemAdmin = !this.hasAnySystemAdmin();
|
|
6075
|
+
const betaAccess = input.betaAccess !== false;
|
|
6076
|
+
const onboardingOutcome = this.resolveRegistrationOnboardingOutcome(input.planVersionId);
|
|
6077
|
+
const now = new Date().toISOString();
|
|
6078
|
+
const userId = this.generateEntityId('user');
|
|
6079
|
+
const displayName = String(input.displayName || (email ? email.split('@')[0] : '') || '').trim()
|
|
6080
|
+
|| (email || userId);
|
|
6081
|
+
this.db.prepare(`
|
|
6082
|
+
INSERT INTO users
|
|
6083
|
+
(id, tenant_id, email, email_verified_at, display_name, system_role,
|
|
6084
|
+
beta_access, is_disabled, created_at, updated_at)
|
|
6085
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?)
|
|
6086
|
+
`).run(userId, this.tenantId, email,
|
|
6087
|
+
// OAuth providers that attest email_verified give us a verified email up-front
|
|
6088
|
+
(email && input.emailVerified) ? now : null, displayName, assignFirstSystemAdmin ? 'system_admin' : 'user', betaAccess ? 1 : 0, now, now);
|
|
6089
|
+
const identityId = this.identityService.linkProviderIdentity({
|
|
6090
|
+
userId,
|
|
6091
|
+
provider: input.provider,
|
|
6092
|
+
providerSubject: input.providerSubject,
|
|
6093
|
+
providerEmail: email,
|
|
6094
|
+
providerEmailVerified: input.emailVerified ?? null,
|
|
6095
|
+
displayNameSnapshot: input.displayName ?? null,
|
|
6096
|
+
avatarUrlSnapshot: input.avatarUrl ?? null,
|
|
6097
|
+
});
|
|
6098
|
+
this.transitionCanonicalCommercialState({
|
|
6099
|
+
accountId: userId,
|
|
6100
|
+
planVersionId: onboardingOutcome.planVersionId,
|
|
6101
|
+
state: onboardingOutcome.commercialState,
|
|
6102
|
+
billingMirrorPlanId: onboardingOutcome.planId,
|
|
6103
|
+
source: 'registration',
|
|
6104
|
+
updatedBy: 'registration',
|
|
6105
|
+
createdAt: now,
|
|
6106
|
+
updatedAt: now
|
|
6107
|
+
});
|
|
6108
|
+
return {
|
|
6109
|
+
userId,
|
|
6110
|
+
workspaceId: null,
|
|
6111
|
+
role: 'member',
|
|
6112
|
+
identityId,
|
|
6113
|
+
planSelectionRequired: onboardingOutcome.requiresPlanSelection,
|
|
6114
|
+
checkoutPending: onboardingOutcome.checkoutPending,
|
|
6115
|
+
commercialState: onboardingOutcome.commercialState,
|
|
6116
|
+
};
|
|
6117
|
+
}
|
|
6118
|
+
/**
|
|
6119
|
+
* Looks up a user by email address (exact, case-insensitive).
|
|
6120
|
+
* Returns { userId, email } or null.
|
|
6121
|
+
*/
|
|
6122
|
+
findUserByEmail(email) {
|
|
6123
|
+
const normalized = String(email || '').trim().toLowerCase();
|
|
6124
|
+
if (!normalized)
|
|
6125
|
+
return null;
|
|
6126
|
+
const row = this.db.prepare(`
|
|
6127
|
+
SELECT id, email FROM users
|
|
6128
|
+
WHERE tenant_id = ? AND email = ? AND COALESCE(is_disabled, 0) = 0
|
|
6129
|
+
LIMIT 1
|
|
6130
|
+
`).get(this.tenantId, normalized);
|
|
6131
|
+
if (!row?.id)
|
|
6132
|
+
return null;
|
|
6133
|
+
return { userId: row.id, email: row.email || normalized };
|
|
6134
|
+
}
|
|
5605
6135
|
createOneTimeAuthToken(userId, purpose, ttlSeconds, workspaceId) {
|
|
5606
6136
|
return this.authTokenService.createOneTimeAuthToken(userId, purpose, ttlSeconds, workspaceId);
|
|
5607
6137
|
}
|
|
@@ -5704,19 +6234,23 @@ export class TaskforceCore {
|
|
|
5704
6234
|
if (!inspection.userId)
|
|
5705
6235
|
return inspection;
|
|
5706
6236
|
const user = this.db.prepare(`
|
|
5707
|
-
SELECT email
|
|
5708
|
-
FROM users
|
|
5709
|
-
WHERE tenant_id = ? AND id = ? LIMIT 1
|
|
6237
|
+
SELECT email FROM users WHERE tenant_id = ? AND id = ? LIMIT 1
|
|
5710
6238
|
`).get(this.tenantId, inspection.userId);
|
|
5711
6239
|
const workspaceId = String(inspection.workspaceId || '').trim();
|
|
5712
|
-
const
|
|
6240
|
+
const hasIdentity = this.identityService.hasAnyActiveIdentity(inspection.userId);
|
|
6241
|
+
const inviteeState = hasIdentity ? 'existing_account' : 'pending_setup';
|
|
6242
|
+
const identities = this.identityService.listIdentitiesForUser(inspection.userId);
|
|
6243
|
+
const availableMethods = identities.map(i => i.provider);
|
|
5713
6244
|
return {
|
|
5714
6245
|
state: inspection.state,
|
|
5715
6246
|
email: user?.email,
|
|
5716
6247
|
userId: inspection.userId,
|
|
5717
6248
|
workspaceId: workspaceId || 'default',
|
|
5718
|
-
|
|
5719
|
-
|
|
6249
|
+
inviteeState,
|
|
6250
|
+
availableMethods,
|
|
6251
|
+
// Backward-compatible fields
|
|
6252
|
+
inviteeKind: hasIdentity ? 'existing_user' : 'new_user',
|
|
6253
|
+
passwordRequired: !hasIdentity
|
|
5720
6254
|
};
|
|
5721
6255
|
}
|
|
5722
6256
|
completeInviteWorkspaceActivation(input) {
|
|
@@ -5773,14 +6307,20 @@ export class TaskforceCore {
|
|
|
5773
6307
|
role: this.normalizeAccessRole(membership?.role || 'member', 'member')
|
|
5774
6308
|
};
|
|
5775
6309
|
}
|
|
5776
|
-
|
|
6310
|
+
/**
|
|
6311
|
+
* Claims an invite by linking an OAuth provider identity to the invited user.
|
|
6312
|
+
*
|
|
6313
|
+
* Used when an invited user with `inviteeState: 'pending_setup'` completes an
|
|
6314
|
+
* OAuth flow with `flow_mode: 'invite_claim'`. The invite token must still be
|
|
6315
|
+
* valid and the user must have zero active identities (pending setup).
|
|
6316
|
+
*
|
|
6317
|
+
* Returns the same shape as `acceptInviteWithToken` so callers can issue a
|
|
6318
|
+
* session cookie on the same path.
|
|
6319
|
+
*/
|
|
6320
|
+
claimInviteWithOAuthProvider(input) {
|
|
5777
6321
|
const token = String(input.token || '').trim();
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
throw new TaskforceRuleError('Token and password are required', 400, 'TOKEN_PASSWORD_REQUIRED');
|
|
5781
|
-
}
|
|
5782
|
-
if (password.length < 8) {
|
|
5783
|
-
throw new TaskforceRuleError('Password must be at least 8 characters', 400, 'PASSWORD_TOO_SHORT');
|
|
6322
|
+
if (!token) {
|
|
6323
|
+
throw new TaskforceRuleError('Invite token is required', 400, 'TOKEN_REQUIRED');
|
|
5784
6324
|
}
|
|
5785
6325
|
const consumed = this.consumeOneTimeAuthToken(token, 'invite_accept');
|
|
5786
6326
|
if (!consumed?.userId) {
|
|
@@ -5801,38 +6341,111 @@ export class TaskforceCore {
|
|
|
5801
6341
|
throw new TaskforceRuleError('Invite token is missing workspace context', 400, 'INVITE_WORKSPACE_REQUIRED');
|
|
5802
6342
|
}
|
|
5803
6343
|
const user = this.db.prepare(`
|
|
5804
|
-
SELECT id, email
|
|
6344
|
+
SELECT id, email FROM users WHERE tenant_id = ? AND id = ? LIMIT 1
|
|
6345
|
+
`).get(this.tenantId, consumed.userId);
|
|
6346
|
+
if (!user?.id || !user.email) {
|
|
6347
|
+
throw new TaskforceRuleError('User not found', 404, 'USER_NOT_FOUND');
|
|
6348
|
+
}
|
|
6349
|
+
if (this.identityService.hasAnyActiveIdentity(consumed.userId)) {
|
|
6350
|
+
throw new TaskforceRuleError('Existing users must sign in before joining this workspace', 409, 'INVITE_SIGN_IN_REQUIRED');
|
|
6351
|
+
}
|
|
6352
|
+
const now = new Date().toISOString();
|
|
6353
|
+
const identityId = this.identityService.linkProviderIdentity({
|
|
6354
|
+
userId: consumed.userId,
|
|
6355
|
+
provider: input.provider,
|
|
6356
|
+
providerSubject: input.providerSubject,
|
|
6357
|
+
providerEmail: input.providerEmail ?? null,
|
|
6358
|
+
providerEmailVerified: input.providerEmailVerified ?? null,
|
|
6359
|
+
displayNameSnapshot: input.displayNameSnapshot ?? null,
|
|
6360
|
+
avatarUrlSnapshot: input.avatarUrlSnapshot ?? null,
|
|
6361
|
+
});
|
|
6362
|
+
// Mark email verified if the provider attested it
|
|
6363
|
+
if (input.providerEmailVerified) {
|
|
6364
|
+
this.db.prepare(`
|
|
6365
|
+
UPDATE users
|
|
6366
|
+
SET email_verified_at = COALESCE(email_verified_at, ?), is_disabled = 0, updated_at = ?
|
|
6367
|
+
WHERE tenant_id = ? AND id = ?
|
|
6368
|
+
`).run(now, now, this.tenantId, consumed.userId);
|
|
6369
|
+
}
|
|
6370
|
+
else {
|
|
6371
|
+
this.db.prepare(`
|
|
6372
|
+
UPDATE users SET is_disabled = 0, updated_at = ? WHERE tenant_id = ? AND id = ?
|
|
6373
|
+
`).run(now, this.tenantId, consumed.userId);
|
|
6374
|
+
}
|
|
6375
|
+
this.ensureDefaultOnboardingEntitlement(consumed.userId, {
|
|
6376
|
+
source: 'invite-accept',
|
|
6377
|
+
updatedBy: 'invite-accept',
|
|
6378
|
+
now
|
|
6379
|
+
});
|
|
6380
|
+
const activation = this.completeInviteWorkspaceActivation({
|
|
6381
|
+
userId: consumed.userId,
|
|
6382
|
+
workspaceId: inviteWorkspaceId
|
|
6383
|
+
});
|
|
6384
|
+
return { ...activation, identityId };
|
|
6385
|
+
}
|
|
6386
|
+
acceptInviteWithToken(input) {
|
|
6387
|
+
const token = String(input.token || '').trim();
|
|
6388
|
+
const password = String(input.password || '');
|
|
6389
|
+
if (!token || !password) {
|
|
6390
|
+
throw new TaskforceRuleError('Token and password are required', 400, 'TOKEN_PASSWORD_REQUIRED');
|
|
6391
|
+
}
|
|
6392
|
+
const inspected = this.inspectOneTimeAuthToken(token, 'invite_accept');
|
|
6393
|
+
if (inspected.state !== 'valid' || !inspected.userId) {
|
|
6394
|
+
if (inspected.state === 'expired') {
|
|
6395
|
+
throw new TaskforceRuleError('Invite token expired', 400, 'INVITE_TOKEN_EXPIRED');
|
|
6396
|
+
}
|
|
6397
|
+
if (inspected.state === 'revoked') {
|
|
6398
|
+
throw new TaskforceRuleError('Invite token revoked', 400, 'INVITE_TOKEN_REVOKED');
|
|
6399
|
+
}
|
|
6400
|
+
if (inspected.state === 'used') {
|
|
6401
|
+
throw new TaskforceRuleError('Invite token already used', 400, 'INVITE_TOKEN_USED');
|
|
6402
|
+
}
|
|
6403
|
+
throw new TaskforceRuleError('Invalid invite token', 400, 'INVITE_TOKEN_INVALID');
|
|
6404
|
+
}
|
|
6405
|
+
const inviteWorkspaceId = String(inspected.workspaceId || '').trim();
|
|
6406
|
+
if (!inviteWorkspaceId) {
|
|
6407
|
+
throw new TaskforceRuleError('Invite token is missing workspace context', 400, 'INVITE_WORKSPACE_REQUIRED');
|
|
6408
|
+
}
|
|
6409
|
+
const user = this.db.prepare(`
|
|
6410
|
+
SELECT id, email, display_name
|
|
5805
6411
|
FROM users
|
|
5806
6412
|
WHERE tenant_id = ? AND id = ?
|
|
5807
6413
|
LIMIT 1
|
|
5808
|
-
`).get(this.tenantId,
|
|
6414
|
+
`).get(this.tenantId, inspected.userId);
|
|
5809
6415
|
if (!user?.id || !user.email) {
|
|
5810
6416
|
throw new TaskforceRuleError('User not found', 404, 'USER_NOT_FOUND');
|
|
5811
6417
|
}
|
|
5812
|
-
|
|
6418
|
+
assertPasswordPolicy(password, {
|
|
6419
|
+
email: user.email,
|
|
6420
|
+
displayName: user.display_name || null
|
|
6421
|
+
});
|
|
6422
|
+
if (this.identityService.hasAnyActiveIdentity(inspected.userId)) {
|
|
5813
6423
|
throw new TaskforceRuleError('Existing users must sign in before joining this workspace', 409, 'INVITE_SIGN_IN_REQUIRED');
|
|
5814
6424
|
}
|
|
6425
|
+
const consumed = this.consumeOneTimeAuthToken(token, 'invite_accept');
|
|
6426
|
+
if (!consumed?.userId) {
|
|
6427
|
+
throw new TaskforceRuleError('Invalid invite token', 400, 'INVITE_TOKEN_INVALID');
|
|
6428
|
+
}
|
|
5815
6429
|
const now = new Date().toISOString();
|
|
6430
|
+
const identityId = this.identityService.setPasswordIdentity(consumed.userId, password);
|
|
5816
6431
|
this.db.prepare(`
|
|
5817
6432
|
UPDATE users
|
|
5818
|
-
SET
|
|
5819
|
-
password_hash = ?,
|
|
5820
|
-
auth_provider = ?,
|
|
5821
|
-
external_auth_id = ?,
|
|
5822
|
-
email_verified_at = COALESCE(email_verified_at, ?),
|
|
5823
|
-
is_disabled = 0,
|
|
5824
|
-
updated_at = ?
|
|
6433
|
+
SET email_verified_at = COALESCE(email_verified_at, ?), is_disabled = 0, updated_at = ?
|
|
5825
6434
|
WHERE tenant_id = ? AND id = ?
|
|
5826
|
-
`).run(
|
|
6435
|
+
`).run(now, now, this.tenantId, consumed.userId);
|
|
5827
6436
|
this.ensureDefaultOnboardingEntitlement(consumed.userId, {
|
|
5828
6437
|
source: 'invite-accept',
|
|
5829
6438
|
updatedBy: 'invite-accept',
|
|
5830
6439
|
now
|
|
5831
6440
|
});
|
|
5832
|
-
|
|
6441
|
+
const activation = this.completeInviteWorkspaceActivation({
|
|
5833
6442
|
userId: consumed.userId,
|
|
5834
6443
|
workspaceId: inviteWorkspaceId
|
|
5835
6444
|
});
|
|
6445
|
+
return {
|
|
6446
|
+
...activation,
|
|
6447
|
+
identityId
|
|
6448
|
+
};
|
|
5836
6449
|
}
|
|
5837
6450
|
joinInviteWithAuthenticatedUser(input) {
|
|
5838
6451
|
const token = String(input.token || '').trim();
|
|
@@ -5856,7 +6469,7 @@ export class TaskforceCore {
|
|
|
5856
6469
|
}
|
|
5857
6470
|
throw new TaskforceRuleError('Invalid invite token', 400, 'INVITE_TOKEN_INVALID');
|
|
5858
6471
|
}
|
|
5859
|
-
if (details.
|
|
6472
|
+
if (details.inviteeState !== 'existing_account') {
|
|
5860
6473
|
throw new TaskforceRuleError('This invite requires account setup before joining', 409, 'INVITE_PASSWORD_SETUP_REQUIRED');
|
|
5861
6474
|
}
|
|
5862
6475
|
if (details.userId !== authenticatedUserId) {
|
|
@@ -5890,32 +6503,31 @@ export class TaskforceCore {
|
|
|
5890
6503
|
const workspaceId = String(input.workspaceId || 'default').trim() || 'default';
|
|
5891
6504
|
if (!email || !password)
|
|
5892
6505
|
return { ok: false, code: 'INVALID_CREDENTIALS' };
|
|
5893
|
-
const
|
|
5894
|
-
|
|
5895
|
-
FROM users
|
|
5896
|
-
WHERE tenant_id = ? AND email = ?
|
|
5897
|
-
LIMIT 1
|
|
5898
|
-
`).get(this.tenantId, email);
|
|
5899
|
-
if (!row?.id || !row.password_hash)
|
|
6506
|
+
const authResult = this.identityService.authenticatePasswordIdentity(email, password);
|
|
6507
|
+
if (!authResult)
|
|
5900
6508
|
return { ok: false, code: 'INVALID_CREDENTIALS' };
|
|
5901
|
-
if (
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
6509
|
+
if (input.requireVerifiedEmail) {
|
|
6510
|
+
const userRow = this.db.prepare(`
|
|
6511
|
+
SELECT email_verified_at FROM users WHERE tenant_id = ? AND id = ? LIMIT 1
|
|
6512
|
+
`).get(this.tenantId, authResult.userId);
|
|
6513
|
+
if (!userRow?.email_verified_at) {
|
|
6514
|
+
return { ok: false, code: 'EMAIL_NOT_VERIFIED' };
|
|
6515
|
+
}
|
|
5905
6516
|
}
|
|
5906
|
-
const resolvedWorkspaceId = this.resolvePreferredUserWorkspaceId(
|
|
6517
|
+
const resolvedWorkspaceId = this.resolvePreferredUserWorkspaceId(authResult.userId, {
|
|
5907
6518
|
preferredWorkspaceId: workspaceId
|
|
5908
6519
|
}) || workspaceId;
|
|
5909
|
-
const access = this.resolveUserAccess(
|
|
6520
|
+
const access = this.resolveUserAccess(authResult.userId, resolvedWorkspaceId, input.fallbackRole || 'member');
|
|
5910
6521
|
if (!access) {
|
|
5911
6522
|
// Allow authenticated users without workspace memberships.
|
|
5912
6523
|
// Provisioning flow will create/switch to a workspace after login.
|
|
5913
6524
|
return {
|
|
5914
6525
|
ok: true,
|
|
5915
6526
|
access: {
|
|
5916
|
-
userId:
|
|
6527
|
+
userId: authResult.userId,
|
|
5917
6528
|
workspaceId: 'default',
|
|
5918
|
-
role: this.normalizeAccessRole(input.fallbackRole || 'member', 'member')
|
|
6529
|
+
role: this.normalizeAccessRole(input.fallbackRole || 'member', 'member'),
|
|
6530
|
+
identityId: authResult.identityId
|
|
5919
6531
|
}
|
|
5920
6532
|
};
|
|
5921
6533
|
}
|
|
@@ -5924,7 +6536,8 @@ export class TaskforceCore {
|
|
|
5924
6536
|
access: {
|
|
5925
6537
|
userId: access.userId,
|
|
5926
6538
|
workspaceId: access.workspaceId,
|
|
5927
|
-
role: access.role
|
|
6539
|
+
role: access.role,
|
|
6540
|
+
identityId: authResult.identityId
|
|
5928
6541
|
}
|
|
5929
6542
|
};
|
|
5930
6543
|
}
|