@taskforcehq/taskforce 0.3.305 → 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/dist/Taskforce.module.css +295 -89
- package/dist/TaskforceCore.js +215 -43
- package/dist/TaskforceCore.test.js +433 -28
- package/dist/assets/fonts/CourierPrime-Bold.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-BoldItalic.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-Italic.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-Regular.woff2 +0 -0
- package/dist/assets/fonts/Noir_medium.woff2 +0 -0
- package/dist/assets/fonts/Noir_regular.woff2 +0 -0
- package/dist/assets/fonts/RussoOne-Regular.woff2 +0 -0
- package/dist/assets/fonts/SpecialElite-Regular.woff2 +0 -0
- package/dist/compat/workspaceSyncCompat.js +46 -2
- package/dist/compat/workspaceSyncCompat.test.js +38 -1
- package/dist/components/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/DocumentViewer.d.ts +5 -1
- package/dist/components/features/DocumentViewer.js +60 -3
- package/dist/components/features/DocumentWorkspace.js +1 -1
- package/dist/components/features/DocumentWorkspace.test.js +33 -0
- package/dist/components/features/documentWorkspaceModel.d.ts +1 -1
- 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.js +85 -90
- package/dist/components/task/TaskForm.test.js +110 -8
- package/dist/components/task/TaskKanban.d.ts +2 -1
- package/dist/components/task/TaskKanban.js +94 -13
- 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 -14
- package/dist/components/views/PlanComparisonPage.test.js +238 -0
- package/dist/components/views/PlansPage.js +127 -36
- package/dist/components/views/PlansPage.test.js +294 -12
- package/dist/components/views/StandaloneLayout.js +283 -533
- 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 +26 -1
- package/dist/components/views/standalone/modals/AccountHubModal.js +76 -2
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +20 -3
- package/dist/components/views/standalone/modals/AvatarImageManagerModal.d.ts +20 -0
- package/dist/components/views/standalone/modals/AvatarImageManagerModal.js +127 -0
- package/dist/components/views/standalone/modals/EditProfileModal.d.ts +2 -9
- package/dist/components/views/standalone/modals/EditProfileModal.js +3 -2
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.js +6 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +1 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +24 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +2 -1
- package/dist/components/views/standalone/modals/SyncStatusModal.js +2 -2
- package/dist/core/AiProfileService.d.ts +39 -2
- package/dist/core/AiProfileService.js +639 -73
- package/dist/core/AiProfileService.test.js +19 -1
- package/dist/core/AiProfiles.test.js +566 -7
- 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.js +3 -3
- package/dist/core/EntitlementsPolicy.test.js +2 -2
- package/dist/core/GlobalSettingsService.js +115 -6
- package/dist/core/PlanEntitlementService.d.ts +23 -2
- package/dist/core/PlanEntitlementService.js +343 -57
- package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
- package/dist/core/PlanFeatureCatalog.test.js +84 -0
- package/dist/core/PlanVersionPolicy.test.js +35 -2
- package/dist/core/PlanningEntities.test.js +52 -0
- package/dist/core/SignupMonetizationSettings.test.js +199 -27
- package/dist/core/SyncReconciliationService.js +2 -65
- package/dist/core/SystemAdmin.test.js +42 -42
- package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
- package/dist/core/Taskforce.d.ts +225 -14
- package/dist/core/Taskforce.js +765 -192
- 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 +117 -16
- package/dist/core/shared.d.ts +1 -1
- package/dist/core/shared.js +5 -1
- package/dist/core/types.d.ts +68 -2
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +26 -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 +15 -3
- package/dist/hooks/sync/transfers.js +179 -62
- 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 +9 -2
- package/dist/hooks/useSyncOrchestrator.js +359 -178
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +407 -0
- package/dist/hooks/useTaskData.js +8 -3
- package/dist/hooks/useTaskData.test.js +45 -0
- package/dist/hooks/useTaskforce.d.ts +60 -2
- package/dist/hooks/useTaskforce.js +549 -240
- 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 +2380 -212
- package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
- package/dist/hooks/useWorkspaceSyncController.js +6 -1
- 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/documentAssetRegistrar.js +3 -3
- package/dist/mcp/runtime.d.ts +2 -0
- package/dist/mcp/runtime.js +131 -64
- package/dist/mcp/runtime.test.js +291 -12
- 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 +3 -0
- package/dist/server/routes/admin.js +272 -19
- package/dist/server/routes/auth.d.ts +15 -2
- package/dist/server/routes/auth.js +994 -57
- package/dist/server/routes/authSupport.d.ts +1 -1
- package/dist/server/routes/authSupport.js +1 -1
- package/dist/server/routes/billing.js +361 -35
- package/dist/server/routes/billing.test.js +592 -12
- package/dist/server/routes/documents.d.ts +1 -0
- package/dist/server/routes/documents.js +172 -70
- package/dist/server/routes/shared.d.ts +28 -2
- package/dist/server/routes/shared.js +69 -4
- package/dist/server/routes/sync.integration.test.js +437 -39
- package/dist/server/routes/sync.js +61 -605
- 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.js +43 -1
- package/dist/server/routes/workspaces.js +36 -5
- package/dist/server/routes.js +60 -3
- package/dist/server/routes.test.js +1824 -171
- 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 +17 -70
- package/dist/sync/syncApplyHandlers.d.ts +7 -0
- package/dist/sync/syncApplyHandlers.js +28 -13
- package/dist/sync/syncApplyHandlers.test.js +70 -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 +5 -0
- package/dist/sync/syncService.js +52 -0
- package/dist/sync/syncService.test.js +57 -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 +45 -0
- package/dist/sync/workspaceSyncModel.js +183 -28
- package/dist/sync/workspaceSyncModel.test.js +291 -12
- package/dist/sync/workspaceSyncState.d.ts +17 -0
- package/dist/sync/workspaceSyncState.js +2 -1
- package/dist/sync/workspaceSyncSurface.js +3 -8
- package/dist/types.d.ts +17 -0
- package/dist/ui/assets/AgentsModule-DrLawwNl.js +1 -0
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BG6P_GVW.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-Dhm7M8e7.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-ln0aF7xc.js → TaskSettings-DZX4jk7e.js} +1 -1
- package/dist/ui/assets/{WorkflowsModule-BcS4afRn.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-BSUaRwF8.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.js +3 -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 +3 -2
- package/scripts/export-sqlite-to-postgres.mjs +7 -9
- package/scripts/run-e2e-realtime.sh +17 -3
- package/scripts/run-smoke.sh +17 -5
- package/scripts/run-soak.sh +17 -5
- package/src/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
- package/src/resources/templates/workflows/collaborate.yaml +1 -1
- package/src/resources/templates/workflows/evaluate.yaml +2 -2
- package/src/resources/templates/workflows/plan.yaml +1 -1
- package/src/resources/templates/workflows/review.yaml +2 -2
- package/dist/ui/assets/AgentsModule-N2MnQBZk.js +0 -1
- package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +0 -1
- package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +0 -252
- package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
- package/dist/ui/assets/PlansPage-Cq0zXj3I.js +0 -1
- package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
- package/dist/ui/assets/index-C2-OXZV7.css +0 -1
- package/dist/ui/assets/index-DcSI5F86.js +0 -6
- package/scripts/migrate-planning-model.ts +0 -233
package/dist/core/Taskforce.js
CHANGED
|
@@ -7,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', 'limited', 'disabled'],
|
|
83
|
-
configTemplate: {
|
|
84
|
-
[DOCUMENT_CLOUD_STORAGE_LIMIT_CONFIG_KEY]: 1024
|
|
85
|
-
}
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
featureKey: WORKSPACES_FEATURE_KEY,
|
|
89
|
-
label: 'Workspaces',
|
|
90
|
-
description: 'Allow multiple workspaces with a configurable workspace limit.',
|
|
91
|
-
allowedAccessModes: ['enabled', 'disabled'],
|
|
92
|
-
configTemplate: {
|
|
93
|
-
[WORKSPACES_LIMIT_CONFIG_KEY]: 1
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
];
|
|
97
|
-
const PLAN_FEATURE_DEPENDENCIES = Object.freeze(DEFAULT_PLAN_FEATURE_CATALOG.reduce((acc, feature) => {
|
|
98
|
-
if (Array.isArray(feature.dependsOn) && feature.dependsOn.length > 0) {
|
|
99
|
-
acc[feature.featureKey] = [...feature.dependsOn];
|
|
100
|
-
}
|
|
101
|
-
return acc;
|
|
102
|
-
}, {}));
|
|
103
61
|
function hashPassword(password) {
|
|
104
62
|
const iterations = 120000;
|
|
105
63
|
const salt = randomBytes(16).toString('hex');
|
|
@@ -121,6 +79,12 @@ function verifyPassword(password, encoded) {
|
|
|
121
79
|
return false;
|
|
122
80
|
return timingSafeEqual(actual, expected);
|
|
123
81
|
}
|
|
82
|
+
function assertPasswordPolicy(password, context) {
|
|
83
|
+
const validation = validatePasswordPolicy(password, context);
|
|
84
|
+
if (!validation.ok) {
|
|
85
|
+
throw new TaskforceRuleError(validation.message || 'Password does not meet the password policy.', 400, validation.code || 'PASSWORD_TOO_SHORT');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
124
88
|
function hashAuthToken(token) {
|
|
125
89
|
return createHash('sha256').update(token).digest('hex');
|
|
126
90
|
}
|
|
@@ -179,6 +143,7 @@ export class TaskforceCore {
|
|
|
179
143
|
db;
|
|
180
144
|
globalSettingsDb = null;
|
|
181
145
|
authTokenService;
|
|
146
|
+
identityService;
|
|
182
147
|
mcpTokenService;
|
|
183
148
|
planEntitlementService;
|
|
184
149
|
realtimeMutationListeners = new Set();
|
|
@@ -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,
|
|
@@ -1315,6 +1312,26 @@ export class TaskforceCore {
|
|
|
1315
1312
|
}
|
|
1316
1313
|
}
|
|
1317
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
|
+
}
|
|
1318
1335
|
initializeSchema() {
|
|
1319
1336
|
const syncEventsIdColumnSql = this.db.provider === 'postgres'
|
|
1320
1337
|
? 'id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY'
|
|
@@ -1350,10 +1367,7 @@ export class TaskforceCore {
|
|
|
1350
1367
|
scheduled_week_key TEXT,
|
|
1351
1368
|
order_in_day INTEGER,
|
|
1352
1369
|
workstream_id TEXT,
|
|
1353
|
-
|
|
1354
|
-
child_display_order INTEGER,
|
|
1355
|
-
FOREIGN KEY(workstream_id) REFERENCES workstreams(id) ON DELETE SET NULL,
|
|
1356
|
-
FOREIGN KEY(parent_task_id) REFERENCES tasks(id) ON DELETE RESTRICT
|
|
1370
|
+
FOREIGN KEY(workstream_id) REFERENCES workstreams(id) ON DELETE SET NULL
|
|
1357
1371
|
);
|
|
1358
1372
|
|
|
1359
1373
|
CREATE TABLE IF NOT EXISTS workspace_task_reference_sequences (
|
|
@@ -1640,12 +1654,20 @@ export class TaskforceCore {
|
|
|
1640
1654
|
username TEXT NOT NULL,
|
|
1641
1655
|
icon TEXT NOT NULL,
|
|
1642
1656
|
color TEXT NOT NULL,
|
|
1657
|
+
avatar_url TEXT,
|
|
1658
|
+
avatar_source_url TEXT,
|
|
1643
1659
|
description TEXT,
|
|
1644
1660
|
role TEXT,
|
|
1645
1661
|
provider TEXT,
|
|
1646
1662
|
model TEXT,
|
|
1647
1663
|
surface_type TEXT,
|
|
1664
|
+
seat_scope TEXT,
|
|
1648
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,
|
|
1649
1671
|
created_at TEXT NOT NULL,
|
|
1650
1672
|
updated_at TEXT NOT NULL,
|
|
1651
1673
|
last_active_at TEXT
|
|
@@ -1765,6 +1787,7 @@ export class TaskforceCore {
|
|
|
1765
1787
|
}
|
|
1766
1788
|
}
|
|
1767
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)`);
|
|
1768
1791
|
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_workstreams_initiative ON workstreams(tenant_id, workspace_id, initiative_id)`);
|
|
1769
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)`);
|
|
1770
1793
|
if (this.db.provider === 'sqlite') {
|
|
@@ -1881,12 +1904,20 @@ export class TaskforceCore {
|
|
|
1881
1904
|
{ name: 'username', ddl: `ALTER TABLE ai_profiles ADD COLUMN username TEXT NOT NULL DEFAULT 'ai'` },
|
|
1882
1905
|
{ name: 'icon', ddl: `ALTER TABLE ai_profiles ADD COLUMN icon TEXT NOT NULL DEFAULT 'Bot'` },
|
|
1883
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` },
|
|
1884
1909
|
{ name: 'description', ddl: `ALTER TABLE ai_profiles ADD COLUMN description TEXT` },
|
|
1885
1910
|
{ name: 'role', ddl: `ALTER TABLE ai_profiles ADD COLUMN role TEXT` },
|
|
1886
1911
|
{ name: 'provider', ddl: `ALTER TABLE ai_profiles ADD COLUMN provider TEXT` },
|
|
1887
1912
|
{ name: 'model', ddl: `ALTER TABLE ai_profiles ADD COLUMN model TEXT` },
|
|
1888
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` },
|
|
1889
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` },
|
|
1890
1921
|
{ name: 'created_at', ddl: `ALTER TABLE ai_profiles ADD COLUMN created_at TEXT NOT NULL DEFAULT ''` },
|
|
1891
1922
|
{ name: 'updated_at', ddl: `ALTER TABLE ai_profiles ADD COLUMN updated_at TEXT NOT NULL DEFAULT ''` },
|
|
1892
1923
|
{ name: 'last_active_at', ddl: `ALTER TABLE ai_profiles ADD COLUMN last_active_at TEXT` }
|
|
@@ -1905,12 +1936,20 @@ export class TaskforceCore {
|
|
|
1905
1936
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS username TEXT NOT NULL DEFAULT 'ai'`,
|
|
1906
1937
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS icon TEXT NOT NULL DEFAULT 'Bot'`,
|
|
1907
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`,
|
|
1908
1941
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS description TEXT`,
|
|
1909
1942
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS role TEXT`,
|
|
1910
1943
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS provider TEXT`,
|
|
1911
1944
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS model TEXT`,
|
|
1912
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`,
|
|
1913
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`,
|
|
1914
1953
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS created_at TEXT NOT NULL DEFAULT ''`,
|
|
1915
1954
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS updated_at TEXT NOT NULL DEFAULT ''`,
|
|
1916
1955
|
`ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS last_active_at TEXT`
|
|
@@ -1924,6 +1963,21 @@ export class TaskforceCore {
|
|
|
1924
1963
|
}
|
|
1925
1964
|
}
|
|
1926
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
|
+
`);
|
|
1927
1981
|
this.migrateSystemRoleBackfillIfNeeded();
|
|
1928
1982
|
this.migrateUserBetaAccessBackfillIfNeeded();
|
|
1929
1983
|
this.migrateDuplicateAiProfilesIfNeeded();
|
|
@@ -2668,6 +2722,9 @@ export class TaskforceCore {
|
|
|
2668
2722
|
getOnboardingPolicy() {
|
|
2669
2723
|
return this.planEntitlementService.getOnboardingPolicy();
|
|
2670
2724
|
}
|
|
2725
|
+
listOnboardingPlanOptions() {
|
|
2726
|
+
return this.planEntitlementService.listOnboardingPlanOptions();
|
|
2727
|
+
}
|
|
2671
2728
|
validateOnboardingPolicy(input) {
|
|
2672
2729
|
return this.planEntitlementService.validateOnboardingPolicy(input);
|
|
2673
2730
|
}
|
|
@@ -2709,31 +2766,58 @@ export class TaskforceCore {
|
|
|
2709
2766
|
listCanonicalTaskAttachments(taskId, workspaceId) {
|
|
2710
2767
|
try {
|
|
2711
2768
|
const workspaceAssetStore = new WorkspaceAssetStore(this.db, this.tenantId);
|
|
2712
|
-
const rows = this.
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
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));
|
|
2737
2821
|
const attachments = rows.map((row) => {
|
|
2738
2822
|
const storageKey = String(row.storage_key || '').trim();
|
|
2739
2823
|
const storageProvider = String(row.storage_provider || 'local').trim();
|
|
@@ -3125,6 +3209,63 @@ export class TaskforceCore {
|
|
|
3125
3209
|
`).all(this.tenantId, normalizedWorkspaceId);
|
|
3126
3210
|
return rows.map((row) => this.rowToWorkstream(row));
|
|
3127
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
|
+
}
|
|
3128
3269
|
createWorkstream(input, workspaceId) {
|
|
3129
3270
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
3130
3271
|
const id = String(input.id || '').trim() || this.generateEntityId('workstream');
|
|
@@ -3899,7 +4040,7 @@ export class TaskforceCore {
|
|
|
3899
4040
|
listTasks(workspaceId) {
|
|
3900
4041
|
const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
|
|
3901
4042
|
const rows = this.db.prepare(`
|
|
3902
|
-
SELECT * FROM tasks
|
|
4043
|
+
SELECT * FROM tasks
|
|
3903
4044
|
WHERE tenant_id = ?
|
|
3904
4045
|
AND workspace_id = ?
|
|
3905
4046
|
AND is_archived = 0
|
|
@@ -3910,6 +4051,112 @@ export class TaskforceCore {
|
|
|
3910
4051
|
config: this.readConfig()
|
|
3911
4052
|
};
|
|
3912
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
|
+
}
|
|
3913
4160
|
getDataVersion() {
|
|
3914
4161
|
if (this.db.provider === 'sqlite') {
|
|
3915
4162
|
const version = this.db.pragma('data_version', { simple: true });
|
|
@@ -4050,9 +4297,9 @@ export class TaskforceCore {
|
|
|
4050
4297
|
`).get(this.tenantId, userId);
|
|
4051
4298
|
if (!existingUser) {
|
|
4052
4299
|
this.db.prepare(`
|
|
4053
|
-
INSERT INTO users (id, tenant_id, email, display_name,
|
|
4054
|
-
VALUES (?, ?, ?, ?, ?, ?, ?,
|
|
4055
|
-
`).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);
|
|
4056
4303
|
}
|
|
4057
4304
|
const membership = this.db.prepare(`
|
|
4058
4305
|
SELECT id, role FROM workspace_memberships
|
|
@@ -4097,16 +4344,17 @@ export class TaskforceCore {
|
|
|
4097
4344
|
`).get(this.tenantId, normalizedUserId, normalizedWorkspaceId);
|
|
4098
4345
|
if (!membership?.workspace_id)
|
|
4099
4346
|
return null;
|
|
4100
|
-
const
|
|
4347
|
+
const accountDisabled = Boolean(userRow.is_disabled);
|
|
4101
4348
|
const userBetaAccess = userRow.beta_access === undefined || userRow.beta_access === null
|
|
4102
4349
|
? true
|
|
4103
4350
|
: Boolean(userRow.beta_access);
|
|
4104
4351
|
const isSystemAdmin = this.normalizeSystemRole(userRow.system_role, 'user') === 'system_admin';
|
|
4105
4352
|
const membershipActive = !membership.status || String(membership.status).toLowerCase() === 'active';
|
|
4353
|
+
const workspaceDisabled = !membershipActive;
|
|
4106
4354
|
const baseRole = this.normalizeAccessRole(membership.role || fallbackRole, fallbackRole);
|
|
4107
4355
|
const permissionMode = this.normalizePermissionMode(membership.permission_mode, 'read-write');
|
|
4108
4356
|
const isMemberReadOnly = baseRole === 'member' && permissionMode === 'read-only';
|
|
4109
|
-
const resolvedRole = (
|
|
4357
|
+
const resolvedRole = (accountDisabled || workspaceDisabled || isMemberReadOnly)
|
|
4110
4358
|
? 'read-only'
|
|
4111
4359
|
: baseRole;
|
|
4112
4360
|
return {
|
|
@@ -4114,7 +4362,9 @@ export class TaskforceCore {
|
|
|
4114
4362
|
workspaceId: String(membership.workspace_id || normalizedWorkspaceId),
|
|
4115
4363
|
role: resolvedRole,
|
|
4116
4364
|
permissionMode,
|
|
4117
|
-
disabled,
|
|
4365
|
+
disabled: accountDisabled || workspaceDisabled,
|
|
4366
|
+
accountDisabled,
|
|
4367
|
+
workspaceDisabled,
|
|
4118
4368
|
betaAccess: userBetaAccess || isSystemAdmin
|
|
4119
4369
|
};
|
|
4120
4370
|
}
|
|
@@ -4333,7 +4583,68 @@ export class TaskforceCore {
|
|
|
4333
4583
|
hasAnySystemAdmin() {
|
|
4334
4584
|
return this.countSystemAdmins() > 0;
|
|
4335
4585
|
}
|
|
4336
|
-
|
|
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);
|
|
4337
4648
|
const rows = this.db.prepare(`
|
|
4338
4649
|
SELECT
|
|
4339
4650
|
u.id AS user_id,
|
|
@@ -4360,8 +4671,10 @@ export class TaskforceCore {
|
|
|
4360
4671
|
ON p.tenant_id = u.tenant_id
|
|
4361
4672
|
AND p.plan_id = v.plan_id
|
|
4362
4673
|
WHERE u.tenant_id = ?
|
|
4363
|
-
|
|
4364
|
-
|
|
4674
|
+
${searchClause}
|
|
4675
|
+
${orderByClause}
|
|
4676
|
+
LIMIT ? OFFSET ?
|
|
4677
|
+
`).all(...params);
|
|
4365
4678
|
return rows
|
|
4366
4679
|
.filter((row) => row.user_id && row.email)
|
|
4367
4680
|
.map((row) => ({
|
|
@@ -4653,6 +4966,7 @@ export class TaskforceCore {
|
|
|
4653
4966
|
u.id AS user_id,
|
|
4654
4967
|
u.email AS email,
|
|
4655
4968
|
u.display_name AS display_name,
|
|
4969
|
+
u.avatar_url AS avatar_url,
|
|
4656
4970
|
u.is_disabled AS is_disabled,
|
|
4657
4971
|
u.created_at AS created_at,
|
|
4658
4972
|
u.updated_at AS updated_at,
|
|
@@ -4669,19 +4983,23 @@ export class TaskforceCore {
|
|
|
4669
4983
|
AND m.workspace_id = ?
|
|
4670
4984
|
ORDER BY COALESCE(m.updated_at, m.created_at) DESC, u.created_at DESC
|
|
4671
4985
|
`).all(this.tenantId, normalizedWorkspaceId);
|
|
4672
|
-
return rows.map((row) =>
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
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
|
+
});
|
|
4685
5003
|
}
|
|
4686
5004
|
replaceWorkspaceUsersForSync(input) {
|
|
4687
5005
|
const workspaceId = String(input.workspaceId || 'default').trim() || 'default';
|
|
@@ -4725,9 +5043,9 @@ export class TaskforceCore {
|
|
|
4725
5043
|
}
|
|
4726
5044
|
else {
|
|
4727
5045
|
this.db.prepare(`
|
|
4728
|
-
INSERT INTO users (id, tenant_id, email, display_name,
|
|
4729
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?,
|
|
4730
|
-
`).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);
|
|
4731
5049
|
}
|
|
4732
5050
|
const membership = this.db.prepare(`
|
|
4733
5051
|
SELECT id
|
|
@@ -4770,8 +5088,8 @@ export class TaskforceCore {
|
|
|
4770
5088
|
tx();
|
|
4771
5089
|
return this.listWorkspaceUsers(workspaceId);
|
|
4772
5090
|
}
|
|
4773
|
-
listAiProfiles(workspaceId) {
|
|
4774
|
-
return listAiProfilesService(this.getAiProfileServiceDeps(), workspaceId);
|
|
5091
|
+
listAiProfiles(workspaceId, options) {
|
|
5092
|
+
return listAiProfilesService(this.getAiProfileServiceDeps(), workspaceId, options);
|
|
4775
5093
|
}
|
|
4776
5094
|
getAiProfileByToken(input) {
|
|
4777
5095
|
return getAiProfileByTokenService(this.getAiProfileServiceDeps(), input);
|
|
@@ -4782,8 +5100,14 @@ export class TaskforceCore {
|
|
|
4782
5100
|
mergeAiProfiles(input) {
|
|
4783
5101
|
return mergeAiProfilesService(this.getAiProfileServiceDeps(), input);
|
|
4784
5102
|
}
|
|
4785
|
-
|
|
4786
|
-
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);
|
|
4787
5111
|
}
|
|
4788
5112
|
listAnnotatedAttachmentSessions(input) {
|
|
4789
5113
|
return listAnnotatedAttachmentSessionsService(this.getAnnotatedAttachmentServiceDeps(), input);
|
|
@@ -4813,17 +5137,30 @@ export class TaskforceCore {
|
|
|
4813
5137
|
const activeSessions = deps.sessionStore.listByWorkspace('default', { includeDeleted: false });
|
|
4814
5138
|
if (activeSessions.length === 0)
|
|
4815
5139
|
return;
|
|
4816
|
-
const hasActiveImageLink = this.
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
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
|
+
`);
|
|
4827
5164
|
for (const session of activeSessions) {
|
|
4828
5165
|
const taskId = String(session.taskId || '').trim();
|
|
4829
5166
|
const imageAssetId = String(session.baseImageAssetId || '').trim();
|
|
@@ -4982,6 +5319,7 @@ export class TaskforceCore {
|
|
|
4982
5319
|
FROM ai_profiles
|
|
4983
5320
|
WHERE tenant_id = ?
|
|
4984
5321
|
AND workspace_id = ?
|
|
5322
|
+
AND archived_at IS NULL
|
|
4985
5323
|
AND (
|
|
4986
5324
|
LOWER(name) = LOWER(?)
|
|
4987
5325
|
OR LOWER(username) = LOWER(?)
|
|
@@ -4992,6 +5330,22 @@ export class TaskforceCore {
|
|
|
4992
5330
|
if (aiProfileByName) {
|
|
4993
5331
|
return this.aiProfileToActor(this.aiProfileRowToRecord(aiProfileByName));
|
|
4994
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
|
+
}
|
|
4995
5349
|
const workspaceUser = this.listWorkspaceUsers(normalizedWorkspaceId).find((user) => user.userId === raw);
|
|
4996
5350
|
if (workspaceUser) {
|
|
4997
5351
|
return this.workspaceUserToActor(workspaceUser);
|
|
@@ -5235,6 +5589,9 @@ export class TaskforceCore {
|
|
|
5235
5589
|
listPlanFeatureCatalog() {
|
|
5236
5590
|
return this.planEntitlementService.listPlanFeatureCatalog();
|
|
5237
5591
|
}
|
|
5592
|
+
updatePlanFeatureCatalogMarketingCopy(input) {
|
|
5593
|
+
return this.planEntitlementService.updatePlanFeatureCatalogMarketingCopy(input);
|
|
5594
|
+
}
|
|
5238
5595
|
listPlanVersions(planId) {
|
|
5239
5596
|
return this.planEntitlementService.listPlanVersions(planId);
|
|
5240
5597
|
}
|
|
@@ -5354,11 +5711,6 @@ export class TaskforceCore {
|
|
|
5354
5711
|
}
|
|
5355
5712
|
}
|
|
5356
5713
|
const now = new Date().toISOString();
|
|
5357
|
-
this.db.prepare(`
|
|
5358
|
-
UPDATE users
|
|
5359
|
-
SET is_disabled = ?, updated_at = ?
|
|
5360
|
-
WHERE tenant_id = ? AND id = ?
|
|
5361
|
-
`).run(disabled ? 1 : 0, now, this.tenantId, normalizedUserId);
|
|
5362
5714
|
if (membership?.id) {
|
|
5363
5715
|
this.db.prepare(`
|
|
5364
5716
|
UPDATE workspace_memberships
|
|
@@ -5532,9 +5884,7 @@ export class TaskforceCore {
|
|
|
5532
5884
|
if (!email || !email.includes('@')) {
|
|
5533
5885
|
throw new TaskforceRuleError('Valid email is required', 400, 'EMAIL_REQUIRED');
|
|
5534
5886
|
}
|
|
5535
|
-
|
|
5536
|
-
throw new TaskforceRuleError('Password must be at least 8 characters', 400, 'PASSWORD_TOO_SHORT');
|
|
5537
|
-
}
|
|
5887
|
+
assertPasswordPolicy(password, { email, displayName: input.displayName || null });
|
|
5538
5888
|
if (!workspaceId || workspaceId.toLowerCase() === 'default') {
|
|
5539
5889
|
throw new TaskforceRuleError('A non-default workspaceId is required', 400, 'WORKSPACE_ID_REQUIRED');
|
|
5540
5890
|
}
|
|
@@ -5551,20 +5901,16 @@ export class TaskforceCore {
|
|
|
5551
5901
|
email,
|
|
5552
5902
|
displayName
|
|
5553
5903
|
});
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
SET password_hash = ?, auth_provider = ?, external_auth_id = ?, beta_access = ?, is_disabled = 0, updated_at = ?
|
|
5565
|
-
WHERE tenant_id = ? AND id = ?
|
|
5566
|
-
`).run(hashPassword(password), 'password', email, betaAccess ? 1 : 0, now, this.tenantId, userId);
|
|
5567
|
-
}
|
|
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);
|
|
5568
5914
|
this.updateWorkspaceUserRole(userId, workspaceId, role);
|
|
5569
5915
|
this.setWorkspaceUserDisabled(userId, workspaceId, false);
|
|
5570
5916
|
return { userId, workspaceId, role };
|
|
@@ -5575,9 +5921,7 @@ export class TaskforceCore {
|
|
|
5575
5921
|
if (!email || !email.includes('@')) {
|
|
5576
5922
|
throw new TaskforceRuleError('Valid email is required', 400, 'EMAIL_REQUIRED');
|
|
5577
5923
|
}
|
|
5578
|
-
|
|
5579
|
-
throw new TaskforceRuleError('Password must be at least 8 characters', 400, 'PASSWORD_TOO_SHORT');
|
|
5580
|
-
}
|
|
5924
|
+
assertPasswordPolicy(password, { email, displayName: input.displayName || null });
|
|
5581
5925
|
const existing = this.db.prepare(`
|
|
5582
5926
|
SELECT id FROM users WHERE tenant_id = ? AND email = ? LIMIT 1
|
|
5583
5927
|
`).get(this.tenantId, email);
|
|
@@ -5606,9 +5950,10 @@ export class TaskforceCore {
|
|
|
5606
5950
|
const userId = this.generateEntityId('user');
|
|
5607
5951
|
const displayName = String(input.displayName || email.split('@')[0] || email).trim() || email;
|
|
5608
5952
|
this.db.prepare(`
|
|
5609
|
-
INSERT INTO users (id, tenant_id, email, display_name,
|
|
5610
|
-
VALUES (?, ?, ?, ?, ?, ?,
|
|
5611
|
-
`).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);
|
|
5612
5957
|
applyRegistrationCommercialState(userId, now);
|
|
5613
5958
|
return {
|
|
5614
5959
|
userId,
|
|
@@ -5642,6 +5987,151 @@ export class TaskforceCore {
|
|
|
5642
5987
|
commercialState: onboardingOutcome.commercialState
|
|
5643
5988
|
};
|
|
5644
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
|
+
}
|
|
5645
6135
|
createOneTimeAuthToken(userId, purpose, ttlSeconds, workspaceId) {
|
|
5646
6136
|
return this.authTokenService.createOneTimeAuthToken(userId, purpose, ttlSeconds, workspaceId);
|
|
5647
6137
|
}
|
|
@@ -5744,19 +6234,23 @@ export class TaskforceCore {
|
|
|
5744
6234
|
if (!inspection.userId)
|
|
5745
6235
|
return inspection;
|
|
5746
6236
|
const user = this.db.prepare(`
|
|
5747
|
-
SELECT email
|
|
5748
|
-
FROM users
|
|
5749
|
-
WHERE tenant_id = ? AND id = ? LIMIT 1
|
|
6237
|
+
SELECT email FROM users WHERE tenant_id = ? AND id = ? LIMIT 1
|
|
5750
6238
|
`).get(this.tenantId, inspection.userId);
|
|
5751
6239
|
const workspaceId = String(inspection.workspaceId || '').trim();
|
|
5752
|
-
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);
|
|
5753
6244
|
return {
|
|
5754
6245
|
state: inspection.state,
|
|
5755
6246
|
email: user?.email,
|
|
5756
6247
|
userId: inspection.userId,
|
|
5757
6248
|
workspaceId: workspaceId || 'default',
|
|
5758
|
-
|
|
5759
|
-
|
|
6249
|
+
inviteeState,
|
|
6250
|
+
availableMethods,
|
|
6251
|
+
// Backward-compatible fields
|
|
6252
|
+
inviteeKind: hasIdentity ? 'existing_user' : 'new_user',
|
|
6253
|
+
passwordRequired: !hasIdentity
|
|
5760
6254
|
};
|
|
5761
6255
|
}
|
|
5762
6256
|
completeInviteWorkspaceActivation(input) {
|
|
@@ -5813,14 +6307,20 @@ export class TaskforceCore {
|
|
|
5813
6307
|
role: this.normalizeAccessRole(membership?.role || 'member', 'member')
|
|
5814
6308
|
};
|
|
5815
6309
|
}
|
|
5816
|
-
|
|
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) {
|
|
5817
6321
|
const token = String(input.token || '').trim();
|
|
5818
|
-
|
|
5819
|
-
|
|
5820
|
-
throw new TaskforceRuleError('Token and password are required', 400, 'TOKEN_PASSWORD_REQUIRED');
|
|
5821
|
-
}
|
|
5822
|
-
if (password.length < 8) {
|
|
5823
|
-
throw new TaskforceRuleError('Password must be at least 8 characters', 400, 'PASSWORD_TOO_SHORT');
|
|
6322
|
+
if (!token) {
|
|
6323
|
+
throw new TaskforceRuleError('Invite token is required', 400, 'TOKEN_REQUIRED');
|
|
5824
6324
|
}
|
|
5825
6325
|
const consumed = this.consumeOneTimeAuthToken(token, 'invite_accept');
|
|
5826
6326
|
if (!consumed?.userId) {
|
|
@@ -5841,38 +6341,111 @@ export class TaskforceCore {
|
|
|
5841
6341
|
throw new TaskforceRuleError('Invite token is missing workspace context', 400, 'INVITE_WORKSPACE_REQUIRED');
|
|
5842
6342
|
}
|
|
5843
6343
|
const user = this.db.prepare(`
|
|
5844
|
-
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
|
|
5845
6411
|
FROM users
|
|
5846
6412
|
WHERE tenant_id = ? AND id = ?
|
|
5847
6413
|
LIMIT 1
|
|
5848
|
-
`).get(this.tenantId,
|
|
6414
|
+
`).get(this.tenantId, inspected.userId);
|
|
5849
6415
|
if (!user?.id || !user.email) {
|
|
5850
6416
|
throw new TaskforceRuleError('User not found', 404, 'USER_NOT_FOUND');
|
|
5851
6417
|
}
|
|
5852
|
-
|
|
6418
|
+
assertPasswordPolicy(password, {
|
|
6419
|
+
email: user.email,
|
|
6420
|
+
displayName: user.display_name || null
|
|
6421
|
+
});
|
|
6422
|
+
if (this.identityService.hasAnyActiveIdentity(inspected.userId)) {
|
|
5853
6423
|
throw new TaskforceRuleError('Existing users must sign in before joining this workspace', 409, 'INVITE_SIGN_IN_REQUIRED');
|
|
5854
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
|
+
}
|
|
5855
6429
|
const now = new Date().toISOString();
|
|
6430
|
+
const identityId = this.identityService.setPasswordIdentity(consumed.userId, password);
|
|
5856
6431
|
this.db.prepare(`
|
|
5857
6432
|
UPDATE users
|
|
5858
|
-
SET
|
|
5859
|
-
password_hash = ?,
|
|
5860
|
-
auth_provider = ?,
|
|
5861
|
-
external_auth_id = ?,
|
|
5862
|
-
email_verified_at = COALESCE(email_verified_at, ?),
|
|
5863
|
-
is_disabled = 0,
|
|
5864
|
-
updated_at = ?
|
|
6433
|
+
SET email_verified_at = COALESCE(email_verified_at, ?), is_disabled = 0, updated_at = ?
|
|
5865
6434
|
WHERE tenant_id = ? AND id = ?
|
|
5866
|
-
`).run(
|
|
6435
|
+
`).run(now, now, this.tenantId, consumed.userId);
|
|
5867
6436
|
this.ensureDefaultOnboardingEntitlement(consumed.userId, {
|
|
5868
6437
|
source: 'invite-accept',
|
|
5869
6438
|
updatedBy: 'invite-accept',
|
|
5870
6439
|
now
|
|
5871
6440
|
});
|
|
5872
|
-
|
|
6441
|
+
const activation = this.completeInviteWorkspaceActivation({
|
|
5873
6442
|
userId: consumed.userId,
|
|
5874
6443
|
workspaceId: inviteWorkspaceId
|
|
5875
6444
|
});
|
|
6445
|
+
return {
|
|
6446
|
+
...activation,
|
|
6447
|
+
identityId
|
|
6448
|
+
};
|
|
5876
6449
|
}
|
|
5877
6450
|
joinInviteWithAuthenticatedUser(input) {
|
|
5878
6451
|
const token = String(input.token || '').trim();
|
|
@@ -5896,7 +6469,7 @@ export class TaskforceCore {
|
|
|
5896
6469
|
}
|
|
5897
6470
|
throw new TaskforceRuleError('Invalid invite token', 400, 'INVITE_TOKEN_INVALID');
|
|
5898
6471
|
}
|
|
5899
|
-
if (details.
|
|
6472
|
+
if (details.inviteeState !== 'existing_account') {
|
|
5900
6473
|
throw new TaskforceRuleError('This invite requires account setup before joining', 409, 'INVITE_PASSWORD_SETUP_REQUIRED');
|
|
5901
6474
|
}
|
|
5902
6475
|
if (details.userId !== authenticatedUserId) {
|
|
@@ -5930,32 +6503,31 @@ export class TaskforceCore {
|
|
|
5930
6503
|
const workspaceId = String(input.workspaceId || 'default').trim() || 'default';
|
|
5931
6504
|
if (!email || !password)
|
|
5932
6505
|
return { ok: false, code: 'INVALID_CREDENTIALS' };
|
|
5933
|
-
const
|
|
5934
|
-
|
|
5935
|
-
FROM users
|
|
5936
|
-
WHERE tenant_id = ? AND email = ?
|
|
5937
|
-
LIMIT 1
|
|
5938
|
-
`).get(this.tenantId, email);
|
|
5939
|
-
if (!row?.id || !row.password_hash)
|
|
5940
|
-
return { ok: false, code: 'INVALID_CREDENTIALS' };
|
|
5941
|
-
if (!verifyPassword(password, row.password_hash))
|
|
6506
|
+
const authResult = this.identityService.authenticatePasswordIdentity(email, password);
|
|
6507
|
+
if (!authResult)
|
|
5942
6508
|
return { ok: false, code: 'INVALID_CREDENTIALS' };
|
|
5943
|
-
if (input.requireVerifiedEmail
|
|
5944
|
-
|
|
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
|
+
}
|
|
5945
6516
|
}
|
|
5946
|
-
const resolvedWorkspaceId = this.resolvePreferredUserWorkspaceId(
|
|
6517
|
+
const resolvedWorkspaceId = this.resolvePreferredUserWorkspaceId(authResult.userId, {
|
|
5947
6518
|
preferredWorkspaceId: workspaceId
|
|
5948
6519
|
}) || workspaceId;
|
|
5949
|
-
const access = this.resolveUserAccess(
|
|
6520
|
+
const access = this.resolveUserAccess(authResult.userId, resolvedWorkspaceId, input.fallbackRole || 'member');
|
|
5950
6521
|
if (!access) {
|
|
5951
6522
|
// Allow authenticated users without workspace memberships.
|
|
5952
6523
|
// Provisioning flow will create/switch to a workspace after login.
|
|
5953
6524
|
return {
|
|
5954
6525
|
ok: true,
|
|
5955
6526
|
access: {
|
|
5956
|
-
userId:
|
|
6527
|
+
userId: authResult.userId,
|
|
5957
6528
|
workspaceId: 'default',
|
|
5958
|
-
role: this.normalizeAccessRole(input.fallbackRole || 'member', 'member')
|
|
6529
|
+
role: this.normalizeAccessRole(input.fallbackRole || 'member', 'member'),
|
|
6530
|
+
identityId: authResult.identityId
|
|
5959
6531
|
}
|
|
5960
6532
|
};
|
|
5961
6533
|
}
|
|
@@ -5964,7 +6536,8 @@ export class TaskforceCore {
|
|
|
5964
6536
|
access: {
|
|
5965
6537
|
userId: access.userId,
|
|
5966
6538
|
workspaceId: access.workspaceId,
|
|
5967
|
-
role: access.role
|
|
6539
|
+
role: access.role,
|
|
6540
|
+
identityId: authResult.identityId
|
|
5968
6541
|
}
|
|
5969
6542
|
};
|
|
5970
6543
|
}
|