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