@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
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
+
function supportsWorkspaceAssetLinkTargetType(db) {
|
|
4
|
+
try {
|
|
5
|
+
if (db.provider === 'postgres') {
|
|
6
|
+
const row = db.prepare(`
|
|
7
|
+
SELECT 1
|
|
8
|
+
FROM information_schema.columns
|
|
9
|
+
WHERE table_schema = current_schema()
|
|
10
|
+
AND table_name = 'workspace_asset_links'
|
|
11
|
+
AND column_name = 'target_type'
|
|
12
|
+
LIMIT 1
|
|
13
|
+
`).get();
|
|
14
|
+
return Boolean(row);
|
|
15
|
+
}
|
|
16
|
+
const columns = db.prepare(`PRAGMA table_info(workspace_asset_links)`).all();
|
|
17
|
+
return columns.some((column) => String(column.name || '').trim() === 'target_type');
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
3
23
|
function buildAttachmentDisplayName(attachment, fallbackNameRaw) {
|
|
4
24
|
const caption = typeof attachment === 'string' ? '' : String(attachment.caption || '').trim();
|
|
5
25
|
if (caption)
|
|
@@ -109,27 +129,49 @@ export function syncCanonicalTaskAttachmentLinksService(deps, taskId, workspaceI
|
|
|
109
129
|
const normalizedWorkspaceId = deps.normalizeWorkspaceId(workspaceId);
|
|
110
130
|
if (!normalizedTaskId)
|
|
111
131
|
return;
|
|
112
|
-
deps.db
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
132
|
+
const supportsTargetType = supportsWorkspaceAssetLinkTargetType(deps.db);
|
|
133
|
+
if (supportsTargetType) {
|
|
134
|
+
deps.db.prepare(`
|
|
135
|
+
UPDATE workspace_asset_links
|
|
136
|
+
SET deleted_at = ?, updated_at = ?
|
|
137
|
+
WHERE tenant_id = ? AND workspace_id = ? AND target_type = 'task' AND task_id = ? AND deleted_at IS NULL
|
|
138
|
+
`).run(new Date().toISOString(), new Date().toISOString(), deps.tenantId, normalizedWorkspaceId, normalizedTaskId);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
deps.db.prepare(`
|
|
142
|
+
UPDATE workspace_asset_links
|
|
143
|
+
SET deleted_at = ?, updated_at = ?
|
|
144
|
+
WHERE tenant_id = ? AND workspace_id = ? AND task_id = ? AND deleted_at IS NULL
|
|
145
|
+
`).run(new Date().toISOString(), new Date().toISOString(), deps.tenantId, normalizedWorkspaceId, normalizedTaskId);
|
|
146
|
+
}
|
|
117
147
|
const selectAsset = deps.db.prepare(`
|
|
118
148
|
SELECT asset_id, kind
|
|
119
149
|
FROM workspace_assets
|
|
120
150
|
WHERE tenant_id = ? AND workspace_id = ? AND storage_key = ? AND deleted_at IS NULL
|
|
121
151
|
LIMIT 1
|
|
122
152
|
`);
|
|
123
|
-
const upsertLink =
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
153
|
+
const upsertLink = supportsTargetType
|
|
154
|
+
? deps.db.prepare(`
|
|
155
|
+
INSERT INTO workspace_asset_links (
|
|
156
|
+
tenant_id, workspace_id, asset_id, target_type, task_id, role, display_name, sort_order, created_at, updated_at, deleted_at
|
|
157
|
+
) VALUES (?, ?, ?, 'task', ?, ?, ?, ?, ?, ?, NULL)
|
|
158
|
+
ON CONFLICT (tenant_id, workspace_id, asset_id, task_id, role) DO UPDATE SET
|
|
159
|
+
target_type = excluded.target_type,
|
|
160
|
+
display_name = excluded.display_name,
|
|
161
|
+
sort_order = excluded.sort_order,
|
|
162
|
+
updated_at = excluded.updated_at,
|
|
163
|
+
deleted_at = NULL
|
|
164
|
+
`)
|
|
165
|
+
: deps.db.prepare(`
|
|
166
|
+
INSERT INTO workspace_asset_links (
|
|
167
|
+
tenant_id, workspace_id, asset_id, task_id, role, display_name, sort_order, created_at, updated_at, deleted_at
|
|
168
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)
|
|
169
|
+
ON CONFLICT (tenant_id, workspace_id, asset_id, task_id, role) DO UPDATE SET
|
|
170
|
+
display_name = excluded.display_name,
|
|
171
|
+
sort_order = excluded.sort_order,
|
|
172
|
+
updated_at = excluded.updated_at,
|
|
173
|
+
deleted_at = NULL
|
|
174
|
+
`);
|
|
133
175
|
attachments.forEach((attachment, index) => {
|
|
134
176
|
const storageKey = normalizeAttachmentStorageKey(typeof attachment === 'string' ? attachment : attachment.path, typeof attachment === 'string' ? null : attachment.fsPath);
|
|
135
177
|
if (!storageKey)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { DatabaseAdapter } from '../storage/databaseAdapter.js';
|
|
2
|
+
import type { UserAuthProvider, UserAuthIdentity } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Manages user_auth_identities — the canonical source for login methods.
|
|
5
|
+
* One Taskforce account can have at most one identity per provider.
|
|
6
|
+
*/
|
|
7
|
+
export declare class AuthIdentityService {
|
|
8
|
+
private readonly db;
|
|
9
|
+
private readonly tenantId;
|
|
10
|
+
constructor(db: DatabaseAdapter, tenantId: string);
|
|
11
|
+
private generateIdentityId;
|
|
12
|
+
/**
|
|
13
|
+
* Creates a password identity for a user. Throws if one already exists.
|
|
14
|
+
*/
|
|
15
|
+
createPasswordIdentity(userId: string, password: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Upserts a password identity — creates if absent, replaces hash if present.
|
|
18
|
+
* Used by password-reset and invite-accept flows.
|
|
19
|
+
*/
|
|
20
|
+
setPasswordIdentity(userId: string, password: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Verifies email + password against the identity table.
|
|
23
|
+
* Returns {userId, identityId} or null.
|
|
24
|
+
* Also checks users.is_disabled.
|
|
25
|
+
*/
|
|
26
|
+
authenticatePasswordIdentity(email: string, password: string): {
|
|
27
|
+
userId: string;
|
|
28
|
+
identityId: string;
|
|
29
|
+
} | null;
|
|
30
|
+
/**
|
|
31
|
+
* Finds the user associated with a provider identity.
|
|
32
|
+
*/
|
|
33
|
+
findUserByProviderIdentity(input: {
|
|
34
|
+
provider: UserAuthProvider;
|
|
35
|
+
providerSubject: string;
|
|
36
|
+
}): {
|
|
37
|
+
userId: string;
|
|
38
|
+
identityId: string;
|
|
39
|
+
} | null;
|
|
40
|
+
/**
|
|
41
|
+
* Links a social provider identity to an existing user.
|
|
42
|
+
* Throws if the user already has an identity for that provider.
|
|
43
|
+
*/
|
|
44
|
+
linkProviderIdentity(input: {
|
|
45
|
+
userId: string;
|
|
46
|
+
provider: UserAuthProvider;
|
|
47
|
+
providerSubject: string;
|
|
48
|
+
providerEmail?: string | null;
|
|
49
|
+
providerEmailVerified?: boolean | null;
|
|
50
|
+
displayNameSnapshot?: string | null;
|
|
51
|
+
avatarUrlSnapshot?: string | null;
|
|
52
|
+
metadataJson?: string | null;
|
|
53
|
+
}): string;
|
|
54
|
+
listIdentitiesForUser(userId: string): UserAuthIdentity[];
|
|
55
|
+
hasAnyActiveIdentity(userId: string): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Unlinks a provider identity from a user.
|
|
58
|
+
* Rejects if it is the last active identity (would lock the user out).
|
|
59
|
+
*/
|
|
60
|
+
unlinkProviderIdentity(input: {
|
|
61
|
+
userId: string;
|
|
62
|
+
provider: UserAuthProvider;
|
|
63
|
+
}): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Creates an OAuth state record in auth_oauth_states.
|
|
66
|
+
*
|
|
67
|
+
* Returns the opaque `state` token (stored in the redirect URL as `?state=`),
|
|
68
|
+
* the PKCE `codeVerifier` (sent to the provider on token exchange),
|
|
69
|
+
* the PKCE `codeChallenge` (sent to the provider on the auth redirect),
|
|
70
|
+
* and a `nonce` (needed for Apple's id_token nonce binding).
|
|
71
|
+
*/
|
|
72
|
+
createOAuthState(input: {
|
|
73
|
+
provider: string;
|
|
74
|
+
flowMode: string;
|
|
75
|
+
userId?: string | null;
|
|
76
|
+
inviteToken?: string | null;
|
|
77
|
+
returnTo?: string | null;
|
|
78
|
+
createdIp?: string | null;
|
|
79
|
+
}): {
|
|
80
|
+
state: string;
|
|
81
|
+
codeVerifier: string;
|
|
82
|
+
codeChallenge: string;
|
|
83
|
+
nonce: string;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Atomically consumes an OAuth state token.
|
|
87
|
+
*
|
|
88
|
+
* Returns the state row if it exists, is unexpired, and has not been consumed.
|
|
89
|
+
* Returns null if the token is invalid, expired, or already consumed.
|
|
90
|
+
* Once consumed, the same token will return null on any subsequent call.
|
|
91
|
+
*/
|
|
92
|
+
consumeOAuthState(state: string): OAuthStateRecord | null;
|
|
93
|
+
}
|
|
94
|
+
export interface OAuthStateRecord {
|
|
95
|
+
state: string;
|
|
96
|
+
tenantId: string;
|
|
97
|
+
provider: string;
|
|
98
|
+
flowMode: string;
|
|
99
|
+
userId: string | null;
|
|
100
|
+
inviteToken: string | null;
|
|
101
|
+
returnTo: string | null;
|
|
102
|
+
pkceVerifier: string | null;
|
|
103
|
+
nonce: string | null;
|
|
104
|
+
expiresAt: string;
|
|
105
|
+
createdAt: string;
|
|
106
|
+
createdIp: string | null;
|
|
107
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { randomBytes, createHash } from 'crypto';
|
|
2
|
+
import { TaskforceRuleError } from './types.js';
|
|
3
|
+
import { hashPassword, verifyPassword, generateUuidV7 } from './shared.js';
|
|
4
|
+
import { validatePasswordPolicy } from '../shared/passwordPolicy.js';
|
|
5
|
+
function mapRow(row) {
|
|
6
|
+
return {
|
|
7
|
+
id: row.id,
|
|
8
|
+
tenantId: row.tenant_id,
|
|
9
|
+
userId: row.user_id,
|
|
10
|
+
provider: row.provider,
|
|
11
|
+
providerSubject: row.provider_subject,
|
|
12
|
+
providerEmail: row.provider_email,
|
|
13
|
+
providerEmailVerified: row.provider_email_verified === null ? null : Boolean(row.provider_email_verified),
|
|
14
|
+
displayNameSnapshot: row.display_name_snapshot,
|
|
15
|
+
avatarUrlSnapshot: row.avatar_url_snapshot,
|
|
16
|
+
createdAt: row.created_at,
|
|
17
|
+
updatedAt: row.updated_at,
|
|
18
|
+
lastLoginAt: row.last_login_at,
|
|
19
|
+
linkedAt: row.linked_at,
|
|
20
|
+
disabledAt: row.disabled_at,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Manages user_auth_identities — the canonical source for login methods.
|
|
25
|
+
* One Taskforce account can have at most one identity per provider.
|
|
26
|
+
*/
|
|
27
|
+
export class AuthIdentityService {
|
|
28
|
+
db;
|
|
29
|
+
tenantId;
|
|
30
|
+
constructor(db, tenantId) {
|
|
31
|
+
this.db = db;
|
|
32
|
+
this.tenantId = tenantId;
|
|
33
|
+
}
|
|
34
|
+
generateIdentityId() {
|
|
35
|
+
return `identity-${generateUuidV7()}`;
|
|
36
|
+
}
|
|
37
|
+
// ─── Password identity ─────────────────────────────────────────────────────
|
|
38
|
+
/**
|
|
39
|
+
* Creates a password identity for a user. Throws if one already exists.
|
|
40
|
+
*/
|
|
41
|
+
createPasswordIdentity(userId, password) {
|
|
42
|
+
const normalizedUserId = String(userId || '').trim();
|
|
43
|
+
if (!normalizedUserId)
|
|
44
|
+
throw new TaskforceRuleError('userId is required', 400, 'USER_ID_REQUIRED');
|
|
45
|
+
const validation = validatePasswordPolicy(password);
|
|
46
|
+
if (!validation.ok) {
|
|
47
|
+
throw new TaskforceRuleError(validation.message || 'Password does not meet the password policy.', 400, validation.code || 'PASSWORD_TOO_SHORT');
|
|
48
|
+
}
|
|
49
|
+
const existing = this.db.prepare(`
|
|
50
|
+
SELECT id FROM user_auth_identities
|
|
51
|
+
WHERE tenant_id = ? AND user_id = ? AND provider = 'password'
|
|
52
|
+
LIMIT 1
|
|
53
|
+
`).get(this.tenantId, normalizedUserId);
|
|
54
|
+
if (existing?.id) {
|
|
55
|
+
throw new TaskforceRuleError('Password identity already exists for this user', 409, 'IDENTITY_ALREADY_EXISTS');
|
|
56
|
+
}
|
|
57
|
+
const now = new Date().toISOString();
|
|
58
|
+
const identityId = this.generateIdentityId();
|
|
59
|
+
this.db.prepare(`
|
|
60
|
+
INSERT INTO user_auth_identities
|
|
61
|
+
(id, tenant_id, user_id, provider, password_hash, created_at, updated_at, linked_at)
|
|
62
|
+
VALUES (?, ?, ?, 'password', ?, ?, ?, ?)
|
|
63
|
+
`).run(identityId, this.tenantId, normalizedUserId, hashPassword(password), now, now, now);
|
|
64
|
+
return identityId;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Upserts a password identity — creates if absent, replaces hash if present.
|
|
68
|
+
* Used by password-reset and invite-accept flows.
|
|
69
|
+
*/
|
|
70
|
+
setPasswordIdentity(userId, password) {
|
|
71
|
+
const normalizedUserId = String(userId || '').trim();
|
|
72
|
+
if (!normalizedUserId)
|
|
73
|
+
throw new TaskforceRuleError('userId is required', 400, 'USER_ID_REQUIRED');
|
|
74
|
+
const validation = validatePasswordPolicy(password);
|
|
75
|
+
if (!validation.ok) {
|
|
76
|
+
throw new TaskforceRuleError(validation.message || 'Password does not meet the password policy.', 400, validation.code || 'PASSWORD_TOO_SHORT');
|
|
77
|
+
}
|
|
78
|
+
const now = new Date().toISOString();
|
|
79
|
+
const existing = this.db.prepare(`
|
|
80
|
+
SELECT id FROM user_auth_identities
|
|
81
|
+
WHERE tenant_id = ? AND user_id = ? AND provider = 'password'
|
|
82
|
+
LIMIT 1
|
|
83
|
+
`).get(this.tenantId, normalizedUserId);
|
|
84
|
+
if (existing?.id) {
|
|
85
|
+
this.db.prepare(`
|
|
86
|
+
UPDATE user_auth_identities
|
|
87
|
+
SET password_hash = ?, updated_at = ?, disabled_at = NULL
|
|
88
|
+
WHERE tenant_id = ? AND id = ?
|
|
89
|
+
`).run(hashPassword(password), now, this.tenantId, existing.id);
|
|
90
|
+
return existing.id;
|
|
91
|
+
}
|
|
92
|
+
const identityId = this.generateIdentityId();
|
|
93
|
+
this.db.prepare(`
|
|
94
|
+
INSERT INTO user_auth_identities
|
|
95
|
+
(id, tenant_id, user_id, provider, password_hash, created_at, updated_at, linked_at)
|
|
96
|
+
VALUES (?, ?, ?, 'password', ?, ?, ?, ?)
|
|
97
|
+
`).run(identityId, this.tenantId, normalizedUserId, hashPassword(password), now, now, now);
|
|
98
|
+
return identityId;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Verifies email + password against the identity table.
|
|
102
|
+
* Returns {userId, identityId} or null.
|
|
103
|
+
* Also checks users.is_disabled.
|
|
104
|
+
*/
|
|
105
|
+
authenticatePasswordIdentity(email, password) {
|
|
106
|
+
const normalizedEmail = String(email || '').trim().toLowerCase();
|
|
107
|
+
if (!normalizedEmail || !password)
|
|
108
|
+
return null;
|
|
109
|
+
const user = this.db.prepare(`
|
|
110
|
+
SELECT id FROM users
|
|
111
|
+
WHERE tenant_id = ? AND email = ? AND COALESCE(is_disabled, 0) = 0
|
|
112
|
+
LIMIT 1
|
|
113
|
+
`).get(this.tenantId, normalizedEmail);
|
|
114
|
+
if (!user?.id)
|
|
115
|
+
return null;
|
|
116
|
+
const identity = this.db.prepare(`
|
|
117
|
+
SELECT id, password_hash FROM user_auth_identities
|
|
118
|
+
WHERE tenant_id = ? AND user_id = ? AND provider = 'password' AND disabled_at IS NULL
|
|
119
|
+
LIMIT 1
|
|
120
|
+
`).get(this.tenantId, user.id);
|
|
121
|
+
if (!identity?.id || !identity.password_hash)
|
|
122
|
+
return null;
|
|
123
|
+
if (!verifyPassword(password, identity.password_hash))
|
|
124
|
+
return null;
|
|
125
|
+
// Update last_login_at
|
|
126
|
+
this.db.prepare(`
|
|
127
|
+
UPDATE user_auth_identities SET last_login_at = ? WHERE tenant_id = ? AND id = ?
|
|
128
|
+
`).run(new Date().toISOString(), this.tenantId, identity.id);
|
|
129
|
+
return { userId: user.id, identityId: identity.id };
|
|
130
|
+
}
|
|
131
|
+
// ─── Social identity ───────────────────────────────────────────────────────
|
|
132
|
+
/**
|
|
133
|
+
* Finds the user associated with a provider identity.
|
|
134
|
+
*/
|
|
135
|
+
findUserByProviderIdentity(input) {
|
|
136
|
+
const row = this.db.prepare(`
|
|
137
|
+
SELECT id, user_id FROM user_auth_identities
|
|
138
|
+
WHERE tenant_id = ? AND provider = ? AND provider_subject = ? AND disabled_at IS NULL
|
|
139
|
+
LIMIT 1
|
|
140
|
+
`).get(this.tenantId, input.provider, input.providerSubject);
|
|
141
|
+
if (!row?.id || !row.user_id)
|
|
142
|
+
return null;
|
|
143
|
+
return { userId: row.user_id, identityId: row.id };
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Links a social provider identity to an existing user.
|
|
147
|
+
* Throws if the user already has an identity for that provider.
|
|
148
|
+
*/
|
|
149
|
+
linkProviderIdentity(input) {
|
|
150
|
+
const normalizedUserId = String(input.userId || '').trim();
|
|
151
|
+
if (!normalizedUserId)
|
|
152
|
+
throw new TaskforceRuleError('userId is required', 400, 'USER_ID_REQUIRED');
|
|
153
|
+
if (input.provider === 'password') {
|
|
154
|
+
throw new TaskforceRuleError('Use setPasswordIdentity for password provider', 400, 'INVALID_PROVIDER');
|
|
155
|
+
}
|
|
156
|
+
const existing = this.db.prepare(`
|
|
157
|
+
SELECT id FROM user_auth_identities
|
|
158
|
+
WHERE tenant_id = ? AND user_id = ? AND provider = ?
|
|
159
|
+
LIMIT 1
|
|
160
|
+
`).get(this.tenantId, normalizedUserId, input.provider);
|
|
161
|
+
if (existing?.id) {
|
|
162
|
+
throw new TaskforceRuleError(`${input.provider} identity already linked to this account`, 409, 'IDENTITY_ALREADY_EXISTS');
|
|
163
|
+
}
|
|
164
|
+
const now = new Date().toISOString();
|
|
165
|
+
const identityId = this.generateIdentityId();
|
|
166
|
+
this.db.prepare(`
|
|
167
|
+
INSERT INTO user_auth_identities
|
|
168
|
+
(id, tenant_id, user_id, provider, provider_subject, provider_email,
|
|
169
|
+
provider_email_verified, display_name_snapshot, avatar_url_snapshot,
|
|
170
|
+
metadata_json, created_at, updated_at, linked_at)
|
|
171
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
172
|
+
`).run(identityId, this.tenantId, normalizedUserId, input.provider, input.providerSubject, input.providerEmail ?? null, input.providerEmailVerified === null || input.providerEmailVerified === undefined
|
|
173
|
+
? null
|
|
174
|
+
: (input.providerEmailVerified ? 1 : 0), input.displayNameSnapshot ?? null, input.avatarUrlSnapshot ?? null, input.metadataJson ?? null, now, now, now);
|
|
175
|
+
return identityId;
|
|
176
|
+
}
|
|
177
|
+
// ─── Listing and unlinking ──────────────────────────────────────────────────
|
|
178
|
+
listIdentitiesForUser(userId) {
|
|
179
|
+
const normalizedUserId = String(userId || '').trim();
|
|
180
|
+
if (!normalizedUserId)
|
|
181
|
+
return [];
|
|
182
|
+
const rows = this.db.prepare(`
|
|
183
|
+
SELECT * FROM user_auth_identities
|
|
184
|
+
WHERE tenant_id = ? AND user_id = ? AND disabled_at IS NULL
|
|
185
|
+
ORDER BY created_at ASC
|
|
186
|
+
`).all(this.tenantId, normalizedUserId);
|
|
187
|
+
return rows.map(mapRow);
|
|
188
|
+
}
|
|
189
|
+
hasAnyActiveIdentity(userId) {
|
|
190
|
+
const normalizedUserId = String(userId || '').trim();
|
|
191
|
+
if (!normalizedUserId)
|
|
192
|
+
return false;
|
|
193
|
+
const row = this.db.prepare(`
|
|
194
|
+
SELECT id FROM user_auth_identities
|
|
195
|
+
WHERE tenant_id = ? AND user_id = ? AND disabled_at IS NULL
|
|
196
|
+
LIMIT 1
|
|
197
|
+
`).get(this.tenantId, normalizedUserId);
|
|
198
|
+
return Boolean(row?.id);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Unlinks a provider identity from a user.
|
|
202
|
+
* Rejects if it is the last active identity (would lock the user out).
|
|
203
|
+
*/
|
|
204
|
+
unlinkProviderIdentity(input) {
|
|
205
|
+
const normalizedUserId = String(input.userId || '').trim();
|
|
206
|
+
if (!normalizedUserId)
|
|
207
|
+
throw new TaskforceRuleError('userId is required', 400, 'USER_ID_REQUIRED');
|
|
208
|
+
const identities = this.listIdentitiesForUser(normalizedUserId);
|
|
209
|
+
if (identities.length <= 1) {
|
|
210
|
+
throw new TaskforceRuleError('Cannot remove the last login method. Add another login method first.', 409, 'LAST_IDENTITY_REQUIRED');
|
|
211
|
+
}
|
|
212
|
+
const target = identities.find(i => i.provider === input.provider);
|
|
213
|
+
if (!target)
|
|
214
|
+
return false;
|
|
215
|
+
const now = new Date().toISOString();
|
|
216
|
+
this.db.prepare(`
|
|
217
|
+
UPDATE user_auth_identities
|
|
218
|
+
SET disabled_at = ?, updated_at = ?
|
|
219
|
+
WHERE tenant_id = ? AND id = ?
|
|
220
|
+
`).run(now, now, this.tenantId, target.id);
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
// ─── OAuth state machine ───────────────────────────────────────────────────
|
|
224
|
+
/**
|
|
225
|
+
* Creates an OAuth state record in auth_oauth_states.
|
|
226
|
+
*
|
|
227
|
+
* Returns the opaque `state` token (stored in the redirect URL as `?state=`),
|
|
228
|
+
* the PKCE `codeVerifier` (sent to the provider on token exchange),
|
|
229
|
+
* the PKCE `codeChallenge` (sent to the provider on the auth redirect),
|
|
230
|
+
* and a `nonce` (needed for Apple's id_token nonce binding).
|
|
231
|
+
*/
|
|
232
|
+
createOAuthState(input) {
|
|
233
|
+
const state = randomBytes(32).toString('base64url');
|
|
234
|
+
const codeVerifier = randomBytes(32).toString('base64url');
|
|
235
|
+
const codeChallenge = createHash('sha256').update(codeVerifier).digest('base64url');
|
|
236
|
+
const nonce = randomBytes(16).toString('base64url');
|
|
237
|
+
const now = new Date().toISOString();
|
|
238
|
+
const expiresAt = new Date(Date.now() + 10 * 60 * 1000).toISOString(); // 10 min
|
|
239
|
+
this.db.prepare(`
|
|
240
|
+
INSERT INTO auth_oauth_states
|
|
241
|
+
(state, tenant_id, provider, flow_mode, user_id, invite_token, return_to,
|
|
242
|
+
pkce_verifier, nonce, expires_at, created_at, created_ip)
|
|
243
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
244
|
+
`).run(state, this.tenantId, input.provider, input.flowMode, input.userId ?? null, input.inviteToken ?? null, input.returnTo ?? null, codeVerifier, nonce, expiresAt, now, input.createdIp ?? null);
|
|
245
|
+
return { state, codeVerifier, codeChallenge, nonce };
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Atomically consumes an OAuth state token.
|
|
249
|
+
*
|
|
250
|
+
* Returns the state row if it exists, is unexpired, and has not been consumed.
|
|
251
|
+
* Returns null if the token is invalid, expired, or already consumed.
|
|
252
|
+
* Once consumed, the same token will return null on any subsequent call.
|
|
253
|
+
*/
|
|
254
|
+
consumeOAuthState(state) {
|
|
255
|
+
const now = new Date().toISOString();
|
|
256
|
+
const row = this.db.prepare(`
|
|
257
|
+
SELECT state, tenant_id, provider, flow_mode, user_id, invite_token, return_to,
|
|
258
|
+
pkce_verifier, nonce, expires_at, consumed_at, created_at, created_ip
|
|
259
|
+
FROM auth_oauth_states
|
|
260
|
+
WHERE state = ? AND tenant_id = ? AND consumed_at IS NULL AND expires_at > ?
|
|
261
|
+
LIMIT 1
|
|
262
|
+
`).get(state, this.tenantId, now);
|
|
263
|
+
if (!row)
|
|
264
|
+
return null;
|
|
265
|
+
this.db.prepare(`
|
|
266
|
+
UPDATE auth_oauth_states SET consumed_at = ?
|
|
267
|
+
WHERE state = ? AND tenant_id = ?
|
|
268
|
+
`).run(now, state, this.tenantId);
|
|
269
|
+
return {
|
|
270
|
+
state: row.state,
|
|
271
|
+
tenantId: row.tenant_id,
|
|
272
|
+
provider: row.provider,
|
|
273
|
+
flowMode: row.flow_mode,
|
|
274
|
+
userId: row.user_id ?? null,
|
|
275
|
+
inviteToken: row.invite_token ?? null,
|
|
276
|
+
returnTo: row.return_to ?? null,
|
|
277
|
+
pkceVerifier: row.pkce_verifier ?? null,
|
|
278
|
+
nonce: row.nonce ?? null,
|
|
279
|
+
expiresAt: row.expires_at,
|
|
280
|
+
createdAt: row.created_at,
|
|
281
|
+
createdIp: row.created_ip ?? null,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DatabaseAdapter } from '../storage/databaseAdapter.js';
|
|
2
2
|
import type { AuthTokenPurpose, AuthTokenInspection } from './types.js';
|
|
3
|
+
import type { AuthIdentityService } from './AuthIdentityService.js';
|
|
3
4
|
/**
|
|
4
5
|
* Handles one-time auth token lifecycle (email verification, password reset, invite acceptance)
|
|
5
6
|
* and the thin read/write helpers for email-verification and password state.
|
|
@@ -7,7 +8,10 @@ import type { AuthTokenPurpose, AuthTokenInspection } from './types.js';
|
|
|
7
8
|
export declare class AuthTokenService {
|
|
8
9
|
private readonly db;
|
|
9
10
|
private readonly tenantId;
|
|
11
|
+
private identityService;
|
|
10
12
|
constructor(db: DatabaseAdapter, tenantId: string);
|
|
13
|
+
/** Called by Taskforce after AuthIdentityService is initialized to avoid circular construction. */
|
|
14
|
+
setIdentityService(svc: AuthIdentityService): void;
|
|
11
15
|
private generateEntityId;
|
|
12
16
|
createOneTimeAuthToken(userId: string, purpose: AuthTokenPurpose, ttlSeconds: number, workspaceId?: string | null): string;
|
|
13
17
|
consumeOneTimeAuthToken(token: string, purpose: AuthTokenPurpose): {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from 'crypto';
|
|
2
2
|
import { TaskforceRuleError } from './types.js';
|
|
3
3
|
import { hashAuthToken, hashPassword, generateUuidV7 } from './shared.js';
|
|
4
|
+
import { validatePasswordPolicy } from '../shared/passwordPolicy.js';
|
|
4
5
|
/**
|
|
5
6
|
* Handles one-time auth token lifecycle (email verification, password reset, invite acceptance)
|
|
6
7
|
* and the thin read/write helpers for email-verification and password state.
|
|
@@ -8,10 +9,15 @@ import { hashAuthToken, hashPassword, generateUuidV7 } from './shared.js';
|
|
|
8
9
|
export class AuthTokenService {
|
|
9
10
|
db;
|
|
10
11
|
tenantId;
|
|
12
|
+
identityService = null;
|
|
11
13
|
constructor(db, tenantId) {
|
|
12
14
|
this.db = db;
|
|
13
15
|
this.tenantId = tenantId;
|
|
14
16
|
}
|
|
17
|
+
/** Called by Taskforce after AuthIdentityService is initialized to avoid circular construction. */
|
|
18
|
+
setIdentityService(svc) {
|
|
19
|
+
this.identityService = svc;
|
|
20
|
+
}
|
|
15
21
|
generateEntityId(prefix) {
|
|
16
22
|
return `${prefix}-${generateUuidV7()}`;
|
|
17
23
|
}
|
|
@@ -190,18 +196,40 @@ export class AuthTokenService {
|
|
|
190
196
|
}
|
|
191
197
|
resetPasswordWithToken(input) {
|
|
192
198
|
const password = String(input.password || '');
|
|
193
|
-
|
|
194
|
-
|
|
199
|
+
const inspected = this.inspectOneTimeAuthToken(input.token, 'password_reset');
|
|
200
|
+
if (inspected.state !== 'valid' || !inspected.userId)
|
|
201
|
+
return false;
|
|
202
|
+
const user = this.db.prepare(`
|
|
203
|
+
SELECT email, display_name
|
|
204
|
+
FROM users
|
|
205
|
+
WHERE tenant_id = ? AND id = ?
|
|
206
|
+
LIMIT 1
|
|
207
|
+
`).get(this.tenantId, inspected.userId);
|
|
208
|
+
const validation = validatePasswordPolicy(password, {
|
|
209
|
+
email: user?.email || null,
|
|
210
|
+
displayName: user?.display_name || null
|
|
211
|
+
});
|
|
212
|
+
if (!validation.ok) {
|
|
213
|
+
throw new TaskforceRuleError(validation.message || 'Password does not meet the password policy.', 400, validation.code || 'PASSWORD_TOO_SHORT');
|
|
195
214
|
}
|
|
196
215
|
const consumed = this.consumeOneTimeAuthToken(input.token, 'password_reset');
|
|
197
216
|
if (!consumed?.userId)
|
|
198
217
|
return false;
|
|
199
218
|
const now = new Date().toISOString();
|
|
219
|
+
if (this.identityService) {
|
|
220
|
+
this.identityService.setPasswordIdentity(consumed.userId, password);
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
// Fallback path — should not occur in normal operation (identityService always set).
|
|
224
|
+
this.db.prepare(`
|
|
225
|
+
UPDATE user_auth_identities
|
|
226
|
+
SET password_hash = ?, updated_at = ?, disabled_at = NULL
|
|
227
|
+
WHERE tenant_id = ? AND user_id = ? AND provider = 'password'
|
|
228
|
+
`).run(hashPassword(password), now, this.tenantId, consumed.userId);
|
|
229
|
+
}
|
|
200
230
|
this.db.prepare(`
|
|
201
|
-
UPDATE users
|
|
202
|
-
|
|
203
|
-
WHERE tenant_id = ? AND id = ?
|
|
204
|
-
`).run(hashPassword(password), 'password', now, this.tenantId, consumed.userId);
|
|
231
|
+
UPDATE users SET is_disabled = 0, updated_at = ? WHERE tenant_id = ? AND id = ?
|
|
232
|
+
`).run(now, this.tenantId, consumed.userId);
|
|
205
233
|
return true;
|
|
206
234
|
}
|
|
207
235
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { TaskforceCore } from './Taskforce';
|
|
6
|
+
function makeCore() {
|
|
7
|
+
const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'taskforce-auth-token-test-'));
|
|
8
|
+
const core = new TaskforceCore({
|
|
9
|
+
projectRoot,
|
|
10
|
+
storagePath: path.join(projectRoot, '.taskforce')
|
|
11
|
+
});
|
|
12
|
+
core.updateGlobalSettings({
|
|
13
|
+
billing: {
|
|
14
|
+
onboardingPolicy: {
|
|
15
|
+
mode: 'require_plan_selection',
|
|
16
|
+
planId: null
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
core.upsertPlanCatalog({ planId: 'starter', displayName: 'Starter', status: 'active' });
|
|
21
|
+
return {
|
|
22
|
+
core,
|
|
23
|
+
cleanup: () => {
|
|
24
|
+
core.close();
|
|
25
|
+
fs.rmSync(projectRoot, { recursive: true, force: true });
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
describe('Auth token lifecycle', () => {
|
|
30
|
+
it('keeps previously issued verification tokens valid until they are used or expire', () => {
|
|
31
|
+
const { core, cleanup } = makeCore();
|
|
32
|
+
try {
|
|
33
|
+
core.registerPasswordUser({
|
|
34
|
+
email: 'verify@example.com',
|
|
35
|
+
password: 'ValidPassphrase123!',
|
|
36
|
+
displayName: 'Verify User',
|
|
37
|
+
betaAccess: true
|
|
38
|
+
});
|
|
39
|
+
const first = String(core.requestEmailVerification({ email: 'verify@example.com' }).token || '');
|
|
40
|
+
const second = String(core.requestEmailVerification({ email: 'verify@example.com' }).token || '');
|
|
41
|
+
expect(first).toMatch(/^[a-f0-9]{48}$/);
|
|
42
|
+
expect(second).toMatch(/^[a-f0-9]{48}$/);
|
|
43
|
+
expect(first).not.toBe(second);
|
|
44
|
+
expect(core.inspectOneTimeAuthToken(first, 'email_verification').state).toBe('valid');
|
|
45
|
+
expect(core.inspectOneTimeAuthToken(second, 'email_verification').state).toBe('valid');
|
|
46
|
+
expect(core.verifyEmailWithToken(first)).toBe(true);
|
|
47
|
+
expect(core.inspectOneTimeAuthToken(first, 'email_verification').state).toBe('used');
|
|
48
|
+
expect(core.inspectOneTimeAuthToken(second, 'email_verification').state).toBe('valid');
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
cleanup();
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
it('keeps previously issued password reset tokens valid until they are used or expire', () => {
|
|
55
|
+
const { core, cleanup } = makeCore();
|
|
56
|
+
try {
|
|
57
|
+
core.registerPasswordUser({
|
|
58
|
+
email: 'reset@example.com',
|
|
59
|
+
password: 'ValidPassphrase123!',
|
|
60
|
+
displayName: 'Reset User',
|
|
61
|
+
betaAccess: true
|
|
62
|
+
});
|
|
63
|
+
const first = String(core.requestPasswordReset({ email: 'reset@example.com' }).token || '');
|
|
64
|
+
const second = String(core.requestPasswordReset({ email: 'reset@example.com' }).token || '');
|
|
65
|
+
expect(first).toMatch(/^[a-f0-9]{48}$/);
|
|
66
|
+
expect(second).toMatch(/^[a-f0-9]{48}$/);
|
|
67
|
+
expect(first).not.toBe(second);
|
|
68
|
+
expect(core.inspectOneTimeAuthToken(first, 'password_reset').state).toBe('valid');
|
|
69
|
+
expect(core.inspectOneTimeAuthToken(second, 'password_reset').state).toBe('valid');
|
|
70
|
+
expect(core.resetPasswordWithToken({ token: first, password: 'UpdatedPass456!' })).toBe(true);
|
|
71
|
+
expect(core.inspectOneTimeAuthToken(first, 'password_reset').state).toBe('used');
|
|
72
|
+
expect(core.inspectOneTimeAuthToken(second, 'password_reset').state).toBe('valid');
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
cleanup();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
});
|