@taskforcehq/taskforce 0.3.305 → 0.3.306

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) hide show
  1. package/dist/Taskforce.module.css +295 -89
  2. package/dist/TaskforceCore.js +215 -43
  3. package/dist/TaskforceCore.test.js +433 -28
  4. package/dist/assets/fonts/CourierPrime-Bold.woff2 +0 -0
  5. package/dist/assets/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  6. package/dist/assets/fonts/CourierPrime-Italic.woff2 +0 -0
  7. package/dist/assets/fonts/CourierPrime-Regular.woff2 +0 -0
  8. package/dist/assets/fonts/Noir_medium.woff2 +0 -0
  9. package/dist/assets/fonts/Noir_regular.woff2 +0 -0
  10. package/dist/assets/fonts/RussoOne-Regular.woff2 +0 -0
  11. package/dist/assets/fonts/SpecialElite-Regular.woff2 +0 -0
  12. package/dist/compat/workspaceSyncCompat.js +46 -2
  13. package/dist/compat/workspaceSyncCompat.test.js +38 -1
  14. package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
  15. package/dist/components/context/ContextAttachmentManager.js +549 -0
  16. package/dist/components/features/AgentsModule.d.ts +10 -1
  17. package/dist/components/features/AgentsModule.js +260 -15
  18. package/dist/components/features/AgentsModule.test.js +255 -5
  19. package/dist/components/features/DocumentViewer.d.ts +5 -1
  20. package/dist/components/features/DocumentViewer.js +60 -3
  21. package/dist/components/features/DocumentWorkspace.js +1 -1
  22. package/dist/components/features/DocumentWorkspace.test.js +33 -0
  23. package/dist/components/features/documentWorkspaceModel.d.ts +1 -1
  24. package/dist/components/task/TaskCard.js +19 -28
  25. package/dist/components/task/TaskCard.test.js +38 -3
  26. package/dist/components/task/TaskContextUpload.js +4 -526
  27. package/dist/components/task/TaskForm.js +85 -90
  28. package/dist/components/task/TaskForm.test.js +110 -8
  29. package/dist/components/task/TaskKanban.d.ts +2 -1
  30. package/dist/components/task/TaskKanban.js +94 -13
  31. package/dist/components/task/TaskKanban.test.js +36 -0
  32. package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
  33. package/dist/components/ui/EditableAvatarButton.js +18 -0
  34. package/dist/components/views/PlanComparisonPage.d.ts +9 -1
  35. package/dist/components/views/PlanComparisonPage.js +70 -14
  36. package/dist/components/views/PlanComparisonPage.test.js +238 -0
  37. package/dist/components/views/PlansPage.js +127 -36
  38. package/dist/components/views/PlansPage.test.js +294 -12
  39. package/dist/components/views/StandaloneLayout.js +283 -533
  40. package/dist/components/views/panels/PlanningDrawer.d.ts +8 -1
  41. package/dist/components/views/panels/PlanningDrawer.js +18 -11
  42. package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
  43. package/dist/components/views/panels/PlanningDrawer.test.js +141 -0
  44. package/dist/components/views/planningScope.d.ts +20 -0
  45. package/dist/components/views/planningScope.js +25 -0
  46. package/dist/components/views/planningScope.test.d.ts +1 -0
  47. package/dist/components/views/planningScope.test.js +60 -0
  48. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +26 -1
  49. package/dist/components/views/standalone/modals/AccountHubModal.js +76 -2
  50. package/dist/components/views/standalone/modals/AccountHubModal.test.js +20 -3
  51. package/dist/components/views/standalone/modals/AvatarImageManagerModal.d.ts +20 -0
  52. package/dist/components/views/standalone/modals/AvatarImageManagerModal.js +127 -0
  53. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +2 -9
  54. package/dist/components/views/standalone/modals/EditProfileModal.js +3 -2
  55. package/dist/components/views/standalone/modals/SyncEnableWarningModal.d.ts +9 -0
  56. package/dist/components/views/standalone/modals/SyncEnableWarningModal.js +6 -0
  57. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +1 -0
  58. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +24 -0
  59. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +2 -1
  60. package/dist/components/views/standalone/modals/SyncStatusModal.js +2 -2
  61. package/dist/core/AiProfileService.d.ts +39 -2
  62. package/dist/core/AiProfileService.js +639 -73
  63. package/dist/core/AiProfileService.test.js +19 -1
  64. package/dist/core/AiProfiles.test.js +566 -7
  65. package/dist/core/AttachmentLinkService.js +57 -15
  66. package/dist/core/AuthIdentityService.d.ts +107 -0
  67. package/dist/core/AuthIdentityService.js +284 -0
  68. package/dist/core/AuthTokenService.d.ts +4 -0
  69. package/dist/core/AuthTokenService.js +34 -6
  70. package/dist/core/AuthTokenService.test.js +3 -3
  71. package/dist/core/EntitlementsPolicy.test.js +2 -2
  72. package/dist/core/GlobalSettingsService.js +115 -6
  73. package/dist/core/PlanEntitlementService.d.ts +23 -2
  74. package/dist/core/PlanEntitlementService.js +343 -57
  75. package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
  76. package/dist/core/PlanFeatureCatalog.test.js +84 -0
  77. package/dist/core/PlanVersionPolicy.test.js +35 -2
  78. package/dist/core/PlanningEntities.test.js +52 -0
  79. package/dist/core/SignupMonetizationSettings.test.js +199 -27
  80. package/dist/core/SyncReconciliationService.js +2 -65
  81. package/dist/core/SystemAdmin.test.js +42 -42
  82. package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
  83. package/dist/core/Taskforce.d.ts +225 -14
  84. package/dist/core/Taskforce.js +765 -192
  85. package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
  86. package/dist/core/Taskforce.listTasksSlim.test.js +91 -0
  87. package/dist/core/UserProfileAvatarDrafts.test.js +30 -3
  88. package/dist/core/WorkspaceLifecycleService.js +0 -5
  89. package/dist/core/WorkspacePermissions.test.js +25 -1
  90. package/dist/core/WorkspacePlanMode.test.js +117 -16
  91. package/dist/core/shared.d.ts +1 -1
  92. package/dist/core/shared.js +5 -1
  93. package/dist/core/types.d.ts +68 -2
  94. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +26 -1
  95. package/dist/hooks/sync/bootstrap.js +2 -0
  96. package/dist/hooks/sync/controlPlane.d.ts +37 -0
  97. package/dist/hooks/sync/controlPlane.js +78 -0
  98. package/dist/hooks/sync/controlPlane.test.d.ts +1 -0
  99. package/dist/hooks/sync/controlPlane.test.js +121 -0
  100. package/dist/hooks/sync/lifecyclePolicy.d.ts +45 -0
  101. package/dist/hooks/sync/lifecyclePolicy.js +93 -0
  102. package/dist/hooks/sync/lifecyclePolicy.test.d.ts +1 -0
  103. package/dist/hooks/sync/lifecyclePolicy.test.js +124 -0
  104. package/dist/hooks/sync/orchestratorShared.d.ts +10 -1
  105. package/dist/hooks/sync/orchestratorShared.js +68 -47
  106. package/dist/hooks/sync/orchestratorShared.test.js +50 -1
  107. package/dist/hooks/sync/pullLifecycle.d.ts +31 -0
  108. package/dist/hooks/sync/pullLifecycle.js +24 -0
  109. package/dist/hooks/sync/pullLifecycle.test.d.ts +1 -0
  110. package/dist/hooks/sync/pullLifecycle.test.js +80 -0
  111. package/dist/hooks/sync/recovery.d.ts +16 -2
  112. package/dist/hooks/sync/recovery.js +171 -53
  113. package/dist/hooks/sync/recovery.test.d.ts +1 -0
  114. package/dist/hooks/sync/recovery.test.js +292 -0
  115. package/dist/hooks/sync/transfers.d.ts +15 -3
  116. package/dist/hooks/sync/transfers.js +179 -62
  117. package/dist/hooks/sync/transfers.test.d.ts +1 -0
  118. package/dist/hooks/sync/transfers.test.js +396 -0
  119. package/dist/hooks/sync/useRecentSyncEvents.d.ts +21 -0
  120. package/dist/hooks/sync/useRecentSyncEvents.js +92 -0
  121. package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +1 -0
  122. package/dist/hooks/sync/useRecentSyncEvents.test.js +124 -0
  123. package/dist/hooks/sync/useSyncStatusActions.d.ts +30 -0
  124. package/dist/hooks/sync/useSyncStatusActions.js +88 -0
  125. package/dist/hooks/sync/useSyncStatusActions.test.d.ts +1 -0
  126. package/dist/hooks/sync/useSyncStatusActions.test.js +133 -0
  127. package/dist/hooks/sync/useSyncStatusControls.d.ts +25 -0
  128. package/dist/hooks/sync/useSyncStatusControls.js +60 -0
  129. package/dist/hooks/sync/useSyncStatusControls.test.d.ts +1 -0
  130. package/dist/hooks/sync/useSyncStatusControls.test.js +88 -0
  131. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +12 -0
  132. package/dist/hooks/useSyncOrchestrator.d.ts +9 -2
  133. package/dist/hooks/useSyncOrchestrator.js +359 -178
  134. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +407 -0
  135. package/dist/hooks/useTaskData.js +8 -3
  136. package/dist/hooks/useTaskData.test.js +45 -0
  137. package/dist/hooks/useTaskforce.d.ts +60 -2
  138. package/dist/hooks/useTaskforce.js +549 -240
  139. package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
  140. package/dist/hooks/useTaskforce.runtime-routing.test.js +152 -0
  141. package/dist/hooks/useTaskforce.sync-behavior.test.js +2380 -212
  142. package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
  143. package/dist/hooks/useWorkspaceSyncController.js +6 -1
  144. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
  145. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +316 -35
  146. package/dist/localization/locales/en-US.d.ts +0 -1
  147. package/dist/localization/locales/en-US.js +0 -1
  148. package/dist/localization/locales/es-419.js +0 -1
  149. package/dist/localization/locales/pt-BR.js +0 -1
  150. package/dist/mcp/documentAssetRegistrar.js +3 -3
  151. package/dist/mcp/runtime.d.ts +2 -0
  152. package/dist/mcp/runtime.js +131 -64
  153. package/dist/mcp/runtime.test.js +291 -12
  154. package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
  155. package/dist/migrations/taskSchemaMigrations.js +171 -61
  156. package/dist/migrations/taskSchemaMigrations.test.js +15 -0
  157. package/dist/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  158. package/dist/resources/templates/workflows/collaborate.yaml +1 -1
  159. package/dist/resources/templates/workflows/evaluate.yaml +2 -2
  160. package/dist/resources/templates/workflows/plan.yaml +1 -1
  161. package/dist/resources/templates/workflows/review.yaml +2 -2
  162. package/dist/server/annotatedAttachmentsRoutes.test.js +3 -3
  163. package/dist/server/auth/providers/apple.d.ts +33 -0
  164. package/dist/server/auth/providers/apple.js +82 -0
  165. package/dist/server/auth/providers/appleClientSecret.d.ts +37 -0
  166. package/dist/server/auth/providers/appleClientSecret.js +65 -0
  167. package/dist/server/auth/providers/github.d.ts +26 -0
  168. package/dist/server/auth/providers/github.js +86 -0
  169. package/dist/server/auth/providers/google.d.ts +21 -0
  170. package/dist/server/auth/providers/google.js +56 -0
  171. package/dist/server/auth/providers/types.d.ts +21 -0
  172. package/dist/server/auth/providers/types.js +1 -0
  173. package/dist/server/auth.d.ts +2 -0
  174. package/dist/server/auth.js +6 -3
  175. package/dist/server/documentReviewRoutes.test.js +36 -3
  176. package/dist/server/index.cookieProxy.test.js +1 -0
  177. package/dist/server/index.d.ts +12 -0
  178. package/dist/server/index.js +120 -9
  179. package/dist/server/index.rateLimit.test.js +3 -0
  180. package/dist/server/index.test.js +106 -3
  181. package/dist/server/routes/admin.d.ts +3 -0
  182. package/dist/server/routes/admin.js +272 -19
  183. package/dist/server/routes/auth.d.ts +15 -2
  184. package/dist/server/routes/auth.js +994 -57
  185. package/dist/server/routes/authSupport.d.ts +1 -1
  186. package/dist/server/routes/authSupport.js +1 -1
  187. package/dist/server/routes/billing.js +361 -35
  188. package/dist/server/routes/billing.test.js +592 -12
  189. package/dist/server/routes/documents.d.ts +1 -0
  190. package/dist/server/routes/documents.js +172 -70
  191. package/dist/server/routes/shared.d.ts +28 -2
  192. package/dist/server/routes/shared.js +69 -4
  193. package/dist/server/routes/sync.integration.test.js +437 -39
  194. package/dist/server/routes/sync.js +61 -605
  195. package/dist/server/routes/syncAuxRoutes.d.ts +15 -0
  196. package/dist/server/routes/syncAuxRoutes.js +91 -0
  197. package/dist/server/routes/syncAuxRoutes.test.d.ts +1 -0
  198. package/dist/server/routes/syncAuxRoutes.test.js +158 -0
  199. package/dist/server/routes/syncPullApplyRoutes.d.ts +36 -0
  200. package/dist/server/routes/syncPullApplyRoutes.js +204 -0
  201. package/dist/server/routes/syncPushRoutes.d.ts +24 -0
  202. package/dist/server/routes/syncPushRoutes.js +212 -0
  203. package/dist/server/routes/syncRouteGuards.d.ts +36 -0
  204. package/dist/server/routes/syncRouteGuards.js +67 -0
  205. package/dist/server/routes/syncRouteGuards.test.d.ts +1 -0
  206. package/dist/server/routes/syncRouteGuards.test.js +94 -0
  207. package/dist/server/routes/syncRouteTypes.d.ts +13 -0
  208. package/dist/server/routes/syncRouteTypes.js +1 -0
  209. package/dist/server/routes/syncSnapshotRoutes.d.ts +66 -0
  210. package/dist/server/routes/syncSnapshotRoutes.js +180 -0
  211. package/dist/server/routes/tasks.js +43 -1
  212. package/dist/server/routes/workspaces.js +36 -5
  213. package/dist/server/routes.js +60 -3
  214. package/dist/server/routes.test.js +1824 -171
  215. package/dist/services/workflowExportSnapshots.test.js +2 -0
  216. package/dist/shared/aiProfileSeatScope.d.ts +5 -0
  217. package/dist/shared/aiProfileSeatScope.js +21 -0
  218. package/dist/shared/passwordPolicy.d.ts +13 -0
  219. package/dist/shared/passwordPolicy.js +84 -0
  220. package/dist/shared/passwordPolicy.test.d.ts +1 -0
  221. package/dist/shared/passwordPolicy.test.js +34 -0
  222. package/dist/storage/workspaceAssetStore.d.ts +15 -3
  223. package/dist/storage/workspaceAssetStore.js +132 -44
  224. package/dist/storage/workspaceAssetStore.test.js +93 -6
  225. package/dist/styles/fonts.css +70 -0
  226. package/dist/sync/collaborationSyncPayload.d.ts +6 -1
  227. package/dist/sync/collaborationSyncPayload.js +17 -11
  228. package/dist/sync/collaborationSyncPayload.test.js +13 -1
  229. package/dist/sync/contentSyncState.d.ts +4 -8
  230. package/dist/sync/contentSyncState.js +17 -70
  231. package/dist/sync/syncApplyHandlers.d.ts +7 -0
  232. package/dist/sync/syncApplyHandlers.js +28 -13
  233. package/dist/sync/syncApplyHandlers.test.js +70 -3
  234. package/dist/sync/syncFieldSemantics.d.ts +12 -0
  235. package/dist/sync/syncFieldSemantics.js +55 -0
  236. package/dist/sync/syncResourceAdapters.d.ts +44 -0
  237. package/dist/sync/syncResourceAdapters.js +77 -0
  238. package/dist/sync/syncService.d.ts +5 -0
  239. package/dist/sync/syncService.js +52 -0
  240. package/dist/sync/syncService.test.js +57 -3
  241. package/dist/sync/taskSyncChangeKey.d.ts +2 -0
  242. package/dist/sync/taskSyncChangeKey.js +143 -0
  243. package/dist/sync/taskSyncPayload.js +3 -8
  244. package/dist/sync/workspacePullFeed.js +4 -3
  245. package/dist/sync/workspacePullFeed.test.js +43 -0
  246. package/dist/sync/workspaceRepair.js +5 -2
  247. package/dist/sync/workspaceSyncModel.d.ts +45 -0
  248. package/dist/sync/workspaceSyncModel.js +183 -28
  249. package/dist/sync/workspaceSyncModel.test.js +291 -12
  250. package/dist/sync/workspaceSyncState.d.ts +17 -0
  251. package/dist/sync/workspaceSyncState.js +2 -1
  252. package/dist/sync/workspaceSyncSurface.js +3 -8
  253. package/dist/types.d.ts +17 -0
  254. package/dist/ui/assets/AgentsModule-DrLawwNl.js +1 -0
  255. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BG6P_GVW.js → AnnotatedAttachmentWorkspace-Z9_whf7F.js} +1 -1
  256. package/dist/ui/assets/CourierPrime-Bold-BuLj_dpw.woff2 +0 -0
  257. package/dist/ui/assets/CourierPrime-BoldItalic-BrK2pUkZ.woff2 +0 -0
  258. package/dist/ui/assets/CourierPrime-Italic-DFUZK5Ey.woff2 +0 -0
  259. package/dist/ui/assets/CourierPrime-Regular-BsKphzVf.woff2 +0 -0
  260. package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +252 -0
  261. package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +1 -0
  262. package/dist/ui/assets/{InitiativesModule-Dhm7M8e7.js → InitiativesModule-9E6ParrY.js} +1 -1
  263. package/dist/ui/assets/Noir_medium-bxQwKGzB.woff2 +0 -0
  264. package/dist/ui/assets/Noir_regular-ojf2kxlG.woff2 +0 -0
  265. package/dist/ui/assets/PlansPage-CvfnMiWq.css +1 -0
  266. package/dist/ui/assets/PlansPage-DRXscYxH.js +1 -0
  267. package/dist/ui/assets/RussoOne-Regular-Cu_qq_qC.woff2 +0 -0
  268. package/dist/ui/assets/SpecialElite-Regular-Di6gSvAS.woff2 +0 -0
  269. package/dist/ui/assets/{TaskSettings-ln0aF7xc.js → TaskSettings-DZX4jk7e.js} +1 -1
  270. package/dist/ui/assets/{WorkflowsModule-BcS4afRn.js → WorkflowsModule-BA7jcEpH.js} +1 -1
  271. package/dist/ui/assets/index-9eT8e0Lu.css +1 -0
  272. package/dist/ui/assets/index-C6k75jr8.js +6 -0
  273. package/dist/ui/assets/{vendor-icons-BSUaRwF8.js → vendor-icons-DuEd_65c.js} +1 -1
  274. package/dist/ui/fonts/CourierPrime-Bold.ttf +0 -0
  275. package/dist/ui/fonts/CourierPrime-Bold.woff2 +0 -0
  276. package/dist/ui/fonts/CourierPrime-BoldItalic.ttf +0 -0
  277. package/dist/ui/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  278. package/dist/ui/fonts/CourierPrime-Italic.ttf +0 -0
  279. package/dist/ui/fonts/CourierPrime-Italic.woff2 +0 -0
  280. package/dist/ui/fonts/CourierPrime-Regular.ttf +0 -0
  281. package/dist/ui/fonts/CourierPrime-Regular.woff2 +0 -0
  282. package/dist/ui/fonts/Noir_medium.otf +0 -0
  283. package/dist/ui/fonts/Noir_medium.woff2 +0 -0
  284. package/dist/ui/fonts/Noir_regular.otf +0 -0
  285. package/dist/ui/fonts/Noir_regular.woff2 +0 -0
  286. package/dist/ui/fonts/RussoOne-Regular.woff2 +0 -0
  287. package/dist/ui/fonts/SpecialElite-Regular.ttf +0 -0
  288. package/dist/ui/fonts/SpecialElite-Regular.woff2 +0 -0
  289. package/dist/ui/index.html +3 -3
  290. package/dist/utils/accountProfileSummaryCache.d.ts +5 -0
  291. package/dist/utils/assignees.d.ts +2 -0
  292. package/dist/utils/assignees.js +2 -0
  293. package/dist/utils/avatarUpload.d.ts +27 -0
  294. package/dist/utils/avatarUpload.js +132 -0
  295. package/dist/utils/avatarUpload.test.d.ts +1 -0
  296. package/dist/utils/avatarUpload.test.js +40 -0
  297. package/dist/utils/bootstrapTrace.d.ts +7 -0
  298. package/dist/utils/bootstrapTrace.js +29 -0
  299. package/dist/utils/commercialLifecyclePresentation.js +3 -0
  300. package/dist/utils/contextAssetEvents.d.ts +2 -0
  301. package/dist/utils/syncEventDetails.d.ts +3 -0
  302. package/dist/utils/syncEventDetails.js +64 -0
  303. package/dist/utils/syncEventDetails.test.d.ts +1 -0
  304. package/dist/utils/syncEventDetails.test.js +23 -0
  305. package/dist/utils/syncEventPresentation.d.ts +15 -0
  306. package/dist/utils/syncEventPresentation.js +129 -0
  307. package/dist/utils/syncEventPresentation.test.d.ts +1 -0
  308. package/dist/utils/syncEventPresentation.test.js +64 -0
  309. package/dist/utils/syncStatusPresentation.d.ts +72 -0
  310. package/dist/utils/syncStatusPresentation.js +217 -0
  311. package/dist/utils/syncStatusPresentation.test.d.ts +1 -0
  312. package/dist/utils/syncStatusPresentation.test.js +164 -0
  313. package/dist/utils/taskActivity.d.ts +2 -0
  314. package/dist/utils/taskActivity.js +228 -22
  315. package/dist/utils/taskActivity.test.js +119 -8
  316. package/dist/utils/workspaceSyncPresentation.d.ts +4 -0
  317. package/dist/utils/workspaceSyncPresentation.js +68 -2
  318. package/dist/utils/workspaceSyncPresentation.test.js +36 -1
  319. package/package.json +3 -2
  320. package/scripts/export-sqlite-to-postgres.mjs +7 -9
  321. package/scripts/run-e2e-realtime.sh +17 -3
  322. package/scripts/run-smoke.sh +17 -5
  323. package/scripts/run-soak.sh +17 -5
  324. package/src/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  325. package/src/resources/templates/workflows/collaborate.yaml +1 -1
  326. package/src/resources/templates/workflows/evaluate.yaml +2 -2
  327. package/src/resources/templates/workflows/plan.yaml +1 -1
  328. package/src/resources/templates/workflows/review.yaml +2 -2
  329. package/dist/ui/assets/AgentsModule-N2MnQBZk.js +0 -1
  330. package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +0 -1
  331. package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +0 -252
  332. package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
  333. package/dist/ui/assets/PlansPage-Cq0zXj3I.js +0 -1
  334. package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
  335. package/dist/ui/assets/index-C2-OXZV7.css +0 -1
  336. package/dist/ui/assets/index-DcSI5F86.js +0 -6
  337. 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', 'limited', 'disabled'],
83
- configTemplate: {
84
- [DOCUMENT_CLOUD_STORAGE_LIMIT_CONFIG_KEY]: 1024
85
- }
86
- },
87
- {
88
- featureKey: WORKSPACES_FEATURE_KEY,
89
- label: 'Workspaces',
90
- description: 'Allow multiple workspaces with a configurable workspace limit.',
91
- allowedAccessModes: ['enabled', 'disabled'],
92
- configTemplate: {
93
- [WORKSPACES_LIMIT_CONFIG_KEY]: 1
94
- }
95
- }
96
- ];
97
- const PLAN_FEATURE_DEPENDENCIES = Object.freeze(DEFAULT_PLAN_FEATURE_CATALOG.reduce((acc, feature) => {
98
- if (Array.isArray(feature.dependsOn) && feature.dependsOn.length > 0) {
99
- acc[feature.featureKey] = [...feature.dependsOn];
100
- }
101
- return acc;
102
- }, {}));
103
61
  function hashPassword(password) {
104
62
  const iterations = 120000;
105
63
  const salt = randomBytes(16).toString('hex');
@@ -121,6 +79,12 @@ function verifyPassword(password, encoded) {
121
79
  return false;
122
80
  return timingSafeEqual(actual, expected);
123
81
  }
82
+ function assertPasswordPolicy(password, context) {
83
+ const validation = validatePasswordPolicy(password, context);
84
+ if (!validation.ok) {
85
+ throw new TaskforceRuleError(validation.message || 'Password does not meet the password policy.', 400, validation.code || 'PASSWORD_TOO_SHORT');
86
+ }
87
+ }
124
88
  function hashAuthToken(token) {
125
89
  return createHash('sha256').update(token).digest('hex');
126
90
  }
@@ -179,6 +143,7 @@ export class TaskforceCore {
179
143
  db;
180
144
  globalSettingsDb = null;
181
145
  authTokenService;
146
+ identityService;
182
147
  mcpTokenService;
183
148
  planEntitlementService;
184
149
  realtimeMutationListeners = new Set();
@@ -408,12 +373,22 @@ export class TaskforceCore {
408
373
  username: String(row.username || '').trim() || this.normalizeAiProfileUsername(row.name, 'ai'),
409
374
  icon: this.normalizeAiProfileIcon(row.icon),
410
375
  color: this.normalizeAiProfileColor(row.color),
376
+ avatarUrl: typeof row.avatar_url === 'string' && row.avatar_url.trim().length > 0 ? row.avatar_url.trim() : null,
377
+ avatarSourceUrl: typeof row.avatar_source_url === 'string' && row.avatar_source_url.trim().length > 0 ? row.avatar_source_url.trim() : null,
411
378
  description: typeof row.description === 'string' && row.description.trim().length > 0 ? row.description.trim() : null,
412
379
  role: typeof row.role === 'string' && row.role.trim().length > 0 ? row.role.trim() : null,
413
380
  provider: typeof row.provider === 'string' && row.provider.trim().length > 0 ? row.provider.trim() : null,
414
381
  model: typeof row.model === 'string' && row.model.trim().length > 0 ? row.model.trim() : null,
415
382
  surfaceType: parseAiProfileSurfaceType(row.surface_type),
383
+ seatScope: parseAiProfileSeatScope(row.seat_scope),
416
384
  providerMetadata,
385
+ archivedAt: typeof row.archived_at === 'string' ? row.archived_at : null,
386
+ archivedBy: typeof row.archived_by === 'string' && row.archived_by.trim().length > 0 ? row.archived_by.trim() : null,
387
+ archivedReason: typeof row.archived_reason === 'string' && row.archived_reason.trim().length > 0 ? row.archived_reason.trim() : null,
388
+ mergedIntoProfileId: typeof row.merged_into_profile_id === 'string' && row.merged_into_profile_id.trim().length > 0 ? row.merged_into_profile_id.trim() : null,
389
+ rosterStateUpdatedAt: typeof row.roster_state_updated_at === 'string' && row.roster_state_updated_at.trim().length > 0
390
+ ? row.roster_state_updated_at.trim()
391
+ : null,
417
392
  createdAt: String(row.created_at || '').trim(),
418
393
  updatedAt: String(row.updated_at || '').trim(),
419
394
  lastActiveAt: typeof row.last_active_at === 'string' ? row.last_active_at : null
@@ -431,12 +406,20 @@ export class TaskforceCore {
431
406
  meta: {
432
407
  profileToken: profile.profileToken,
433
408
  workspaceId: profile.workspaceId,
409
+ avatarUrl: profile.avatarUrl ?? null,
410
+ avatarSourceUrl: profile.avatarSourceUrl ?? null,
434
411
  lastActiveAt: profile.lastActiveAt ?? null,
435
412
  description: profile.description ?? null,
436
413
  role: profile.role ?? null,
437
414
  provider: profile.provider ?? null,
438
415
  model: profile.model ?? null,
439
- surfaceType: profile.surfaceType ?? null
416
+ surfaceType: profile.surfaceType ?? null,
417
+ seatScope: profile.seatScope ?? null,
418
+ archivedAt: profile.archivedAt ?? null,
419
+ archivedBy: profile.archivedBy ?? null,
420
+ archivedReason: profile.archivedReason ?? null,
421
+ mergedIntoProfileId: profile.mergedIntoProfileId ?? null,
422
+ rosterStateUpdatedAt: profile.rosterStateUpdatedAt ?? null
440
423
  }
441
424
  };
442
425
  }
@@ -1160,7 +1143,6 @@ export class TaskforceCore {
1160
1143
  this.initializeSchema();
1161
1144
  ensureTenantSchema(this.db);
1162
1145
  ensureWorkspaceSchema(this.db);
1163
- ensureParentTaskSchema(this.db);
1164
1146
  ensureScheduleTaskSchema(this.db);
1165
1147
  ensureAssigneeTaskSchema(this.db);
1166
1148
  ensureTaskReferenceSchema(this.db);
@@ -1171,6 +1153,7 @@ export class TaskforceCore {
1171
1153
  ensureAnnotatedAttachmentSessionsSchema(this.db);
1172
1154
  ensureBillingSchema(this.db);
1173
1155
  ensureEntitlementsSchema(this.db);
1156
+ ensureAuthIdentitySchema(this.db);
1174
1157
  this.performMigrationIfNeeded();
1175
1158
  this.planEntitlementService = new PlanEntitlementService(this.db, this.tenantId, {
1176
1159
  getWorkspaceOwnerUserId: (wid) => this.getWorkspaceOwnerUserId(wid),
@@ -1182,6 +1165,8 @@ export class TaskforceCore {
1182
1165
  this.planEntitlementService.backfillAccountEntitlements();
1183
1166
  this.repairDetachedAnnotatedAttachmentSessions();
1184
1167
  this.authTokenService = new AuthTokenService(this.db, this.tenantId);
1168
+ this.identityService = new AuthIdentityService(this.db, this.tenantId);
1169
+ this.authTokenService.setIdentityService(this.identityService);
1185
1170
  this.mcpTokenService = new McpTokenService(this.db, this.tenantId, {
1186
1171
  getAccountAccessStatus: (userId) => this.getAccountAccessStatus(userId),
1187
1172
  });
@@ -1225,11 +1210,23 @@ export class TaskforceCore {
1225
1210
  if (!accountId) {
1226
1211
  throw new TaskforceRuleError('accountId is required', 400, 'ACCOUNT_ID_REQUIRED');
1227
1212
  }
1228
- 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,
@@ -1315,6 +1312,26 @@ export class TaskforceCore {
1315
1312
  }
1316
1313
  }
1317
1314
  }
1315
+ supportsWorkspaceAssetLinkTargetType() {
1316
+ try {
1317
+ if (this.db.provider === 'postgres') {
1318
+ const row = this.db.prepare(`
1319
+ SELECT 1
1320
+ FROM information_schema.columns
1321
+ WHERE table_schema = current_schema()
1322
+ AND table_name = 'workspace_asset_links'
1323
+ AND column_name = 'target_type'
1324
+ LIMIT 1
1325
+ `).get();
1326
+ return Boolean(row);
1327
+ }
1328
+ const columns = this.db.prepare(`PRAGMA table_info(workspace_asset_links)`).all();
1329
+ return columns.some((column) => String(column.name || '').trim() === 'target_type');
1330
+ }
1331
+ catch {
1332
+ return false;
1333
+ }
1334
+ }
1318
1335
  initializeSchema() {
1319
1336
  const syncEventsIdColumnSql = this.db.provider === 'postgres'
1320
1337
  ? 'id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY'
@@ -1350,10 +1367,7 @@ export class TaskforceCore {
1350
1367
  scheduled_week_key TEXT,
1351
1368
  order_in_day INTEGER,
1352
1369
  workstream_id TEXT,
1353
- parent_task_id TEXT,
1354
- child_display_order INTEGER,
1355
- FOREIGN KEY(workstream_id) REFERENCES workstreams(id) ON DELETE SET NULL,
1356
- FOREIGN KEY(parent_task_id) REFERENCES tasks(id) ON DELETE RESTRICT
1370
+ FOREIGN KEY(workstream_id) REFERENCES workstreams(id) ON DELETE SET NULL
1357
1371
  );
1358
1372
 
1359
1373
  CREATE TABLE IF NOT EXISTS workspace_task_reference_sequences (
@@ -1640,12 +1654,20 @@ export class TaskforceCore {
1640
1654
  username TEXT NOT NULL,
1641
1655
  icon TEXT NOT NULL,
1642
1656
  color TEXT NOT NULL,
1657
+ avatar_url TEXT,
1658
+ avatar_source_url TEXT,
1643
1659
  description TEXT,
1644
1660
  role TEXT,
1645
1661
  provider TEXT,
1646
1662
  model TEXT,
1647
1663
  surface_type TEXT,
1664
+ seat_scope TEXT,
1648
1665
  provider_metadata_json TEXT,
1666
+ archived_at TEXT,
1667
+ archived_by TEXT,
1668
+ archived_reason TEXT,
1669
+ merged_into_profile_id TEXT,
1670
+ roster_state_updated_at TEXT,
1649
1671
  created_at TEXT NOT NULL,
1650
1672
  updated_at TEXT NOT NULL,
1651
1673
  last_active_at TEXT
@@ -1765,6 +1787,7 @@ export class TaskforceCore {
1765
1787
  }
1766
1788
  }
1767
1789
  this.db.exec(`CREATE INDEX IF NOT EXISTS idx_tasks_workstream ON tasks(tenant_id, workspace_id, workstream_id)`);
1790
+ this.db.exec(`CREATE INDEX IF NOT EXISTS idx_tasks_assignee ON tasks(tenant_id, workspace_id, assignee, is_archived)`);
1768
1791
  this.db.exec(`CREATE INDEX IF NOT EXISTS idx_workstreams_initiative ON workstreams(tenant_id, workspace_id, initiative_id)`);
1769
1792
  this.db.exec(`CREATE INDEX IF NOT EXISTS idx_checklist_items_task_order ON checklist_items(tenant_id, workspace_id, task_id, sort_order)`);
1770
1793
  if (this.db.provider === 'sqlite') {
@@ -1881,12 +1904,20 @@ export class TaskforceCore {
1881
1904
  { name: 'username', ddl: `ALTER TABLE ai_profiles ADD COLUMN username TEXT NOT NULL DEFAULT 'ai'` },
1882
1905
  { name: 'icon', ddl: `ALTER TABLE ai_profiles ADD COLUMN icon TEXT NOT NULL DEFAULT 'Bot'` },
1883
1906
  { name: 'color', ddl: `ALTER TABLE ai_profiles ADD COLUMN color TEXT NOT NULL DEFAULT '#64748b'` },
1907
+ { name: 'avatar_url', ddl: `ALTER TABLE ai_profiles ADD COLUMN avatar_url TEXT` },
1908
+ { name: 'avatar_source_url', ddl: `ALTER TABLE ai_profiles ADD COLUMN avatar_source_url TEXT` },
1884
1909
  { name: 'description', ddl: `ALTER TABLE ai_profiles ADD COLUMN description TEXT` },
1885
1910
  { name: 'role', ddl: `ALTER TABLE ai_profiles ADD COLUMN role TEXT` },
1886
1911
  { name: 'provider', ddl: `ALTER TABLE ai_profiles ADD COLUMN provider TEXT` },
1887
1912
  { name: 'model', ddl: `ALTER TABLE ai_profiles ADD COLUMN model TEXT` },
1888
1913
  { name: 'surface_type', ddl: `ALTER TABLE ai_profiles ADD COLUMN surface_type TEXT` },
1914
+ { name: 'seat_scope', ddl: `ALTER TABLE ai_profiles ADD COLUMN seat_scope TEXT` },
1889
1915
  { name: 'provider_metadata_json', ddl: `ALTER TABLE ai_profiles ADD COLUMN provider_metadata_json TEXT` },
1916
+ { name: 'archived_at', ddl: `ALTER TABLE ai_profiles ADD COLUMN archived_at TEXT` },
1917
+ { name: 'archived_by', ddl: `ALTER TABLE ai_profiles ADD COLUMN archived_by TEXT` },
1918
+ { name: 'archived_reason', ddl: `ALTER TABLE ai_profiles ADD COLUMN archived_reason TEXT` },
1919
+ { name: 'merged_into_profile_id', ddl: `ALTER TABLE ai_profiles ADD COLUMN merged_into_profile_id TEXT` },
1920
+ { name: 'roster_state_updated_at', ddl: `ALTER TABLE ai_profiles ADD COLUMN roster_state_updated_at TEXT` },
1890
1921
  { name: 'created_at', ddl: `ALTER TABLE ai_profiles ADD COLUMN created_at TEXT NOT NULL DEFAULT ''` },
1891
1922
  { name: 'updated_at', ddl: `ALTER TABLE ai_profiles ADD COLUMN updated_at TEXT NOT NULL DEFAULT ''` },
1892
1923
  { name: 'last_active_at', ddl: `ALTER TABLE ai_profiles ADD COLUMN last_active_at TEXT` }
@@ -1905,12 +1936,20 @@ export class TaskforceCore {
1905
1936
  `ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS username TEXT NOT NULL DEFAULT 'ai'`,
1906
1937
  `ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS icon TEXT NOT NULL DEFAULT 'Bot'`,
1907
1938
  `ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS color TEXT NOT NULL DEFAULT '#64748b'`,
1939
+ `ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS avatar_url TEXT`,
1940
+ `ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS avatar_source_url TEXT`,
1908
1941
  `ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS description TEXT`,
1909
1942
  `ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS role TEXT`,
1910
1943
  `ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS provider TEXT`,
1911
1944
  `ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS model TEXT`,
1912
1945
  `ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS surface_type TEXT`,
1946
+ `ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS seat_scope TEXT`,
1913
1947
  `ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS provider_metadata_json TEXT`,
1948
+ `ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS archived_at TEXT`,
1949
+ `ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS archived_by TEXT`,
1950
+ `ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS archived_reason TEXT`,
1951
+ `ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS merged_into_profile_id TEXT`,
1952
+ `ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS roster_state_updated_at TEXT`,
1914
1953
  `ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS created_at TEXT NOT NULL DEFAULT ''`,
1915
1954
  `ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS updated_at TEXT NOT NULL DEFAULT ''`,
1916
1955
  `ALTER TABLE ai_profiles ADD COLUMN IF NOT EXISTS last_active_at TEXT`
@@ -1924,6 +1963,21 @@ export class TaskforceCore {
1924
1963
  }
1925
1964
  }
1926
1965
  }
1966
+ this.db.exec(`
1967
+ UPDATE ai_profiles
1968
+ SET roster_state_updated_at = CASE
1969
+ WHEN archived_at IS NOT NULL AND TRIM(archived_at) <> '' THEN archived_at
1970
+ WHEN created_at IS NOT NULL AND TRIM(created_at) <> '' THEN created_at
1971
+ ELSE updated_at
1972
+ END
1973
+ WHERE roster_state_updated_at IS NULL
1974
+ OR TRIM(roster_state_updated_at) = ''
1975
+ OR (
1976
+ archived_at IS NOT NULL
1977
+ AND TRIM(archived_at) <> ''
1978
+ AND roster_state_updated_at < archived_at
1979
+ )
1980
+ `);
1927
1981
  this.migrateSystemRoleBackfillIfNeeded();
1928
1982
  this.migrateUserBetaAccessBackfillIfNeeded();
1929
1983
  this.migrateDuplicateAiProfilesIfNeeded();
@@ -2668,6 +2722,9 @@ export class TaskforceCore {
2668
2722
  getOnboardingPolicy() {
2669
2723
  return this.planEntitlementService.getOnboardingPolicy();
2670
2724
  }
2725
+ listOnboardingPlanOptions() {
2726
+ return this.planEntitlementService.listOnboardingPlanOptions();
2727
+ }
2671
2728
  validateOnboardingPolicy(input) {
2672
2729
  return this.planEntitlementService.validateOnboardingPolicy(input);
2673
2730
  }
@@ -2709,31 +2766,58 @@ export class TaskforceCore {
2709
2766
  listCanonicalTaskAttachments(taskId, workspaceId) {
2710
2767
  try {
2711
2768
  const workspaceAssetStore = new WorkspaceAssetStore(this.db, this.tenantId);
2712
- const rows = this.db.prepare(`
2713
- SELECT
2714
- a.asset_id,
2715
- a.kind,
2716
- a.reference_number,
2717
- a.storage_provider,
2718
- a.storage_key,
2719
- a.logical_name,
2720
- a.original_filename,
2721
- a.updated_at,
2722
- a.created_at,
2723
- l.role,
2724
- l.display_name
2725
- FROM workspace_asset_links l
2726
- JOIN workspace_assets a
2727
- ON a.tenant_id = l.tenant_id
2728
- AND a.workspace_id = l.workspace_id
2729
- AND a.asset_id = l.asset_id
2730
- WHERE l.tenant_id = ?
2731
- AND l.workspace_id = ?
2732
- AND l.task_id = ?
2733
- AND l.deleted_at IS NULL
2734
- AND a.deleted_at IS NULL
2735
- ORDER BY COALESCE(l.sort_order, 2147483647) ASC, a.updated_at DESC, a.asset_id ASC
2736
- `).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));
2737
2821
  const attachments = rows.map((row) => {
2738
2822
  const storageKey = String(row.storage_key || '').trim();
2739
2823
  const storageProvider = String(row.storage_provider || 'local').trim();
@@ -3125,6 +3209,63 @@ export class TaskforceCore {
3125
3209
  `).all(this.tenantId, normalizedWorkspaceId);
3126
3210
  return rows.map((row) => this.rowToWorkstream(row));
3127
3211
  }
3212
+ listPlanningBootstrap(workspaceId, diagnostics) {
3213
+ const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
3214
+ const startedAt = Date.now();
3215
+ const initiatives = this.listInitiatives(workspaceId);
3216
+ const initiativesLoadedAt = Date.now();
3217
+ const workstreams = this.listWorkstreams(workspaceId);
3218
+ const workstreamsLoadedAt = Date.now();
3219
+ const planningTaskRows = this.db.prepare(`
3220
+ SELECT id, reference_number, title, status, workstream_id
3221
+ FROM tasks
3222
+ WHERE tenant_id = ? AND workspace_id = ? AND is_archived = 0
3223
+ AND workstream_id IS NOT NULL AND TRIM(workstream_id) <> ''
3224
+ ORDER BY created_at DESC
3225
+ `).all(this.tenantId, normalizedWorkspaceId);
3226
+ const taskRowsLoadedAt = Date.now();
3227
+ const workstreamTaskSummaries = new Map();
3228
+ for (const row of planningTaskRows) {
3229
+ const workstreamId = String(row.workstream_id || '').trim();
3230
+ if (!workstreamId)
3231
+ continue;
3232
+ const status = this.normalizeStatusValue(row.status);
3233
+ const referenceNumber = this.sanitizeTaskReferenceNumber(row.reference_number);
3234
+ const current = workstreamTaskSummaries.get(workstreamId) || {
3235
+ workstreamId,
3236
+ taskCount: 0,
3237
+ completedTaskCount: 0,
3238
+ tasks: [],
3239
+ };
3240
+ current.taskCount += 1;
3241
+ if (status === 'done' || status === 'cancelled') {
3242
+ current.completedTaskCount += 1;
3243
+ }
3244
+ current.tasks.push({
3245
+ id: String(row.id || '').trim(),
3246
+ referenceNumber,
3247
+ title: String(row.title || '').trim() || 'Untitled Task',
3248
+ status,
3249
+ });
3250
+ workstreamTaskSummaries.set(workstreamId, current);
3251
+ }
3252
+ const summariesBuiltAt = Date.now();
3253
+ if (diagnostics) {
3254
+ diagnostics.initiativeCount = initiatives.length;
3255
+ diagnostics.workstreamCount = workstreams.length;
3256
+ diagnostics.workstreamLinkedTaskCount = planningTaskRows.length;
3257
+ diagnostics.workstreamTaskSummaryCount = workstreamTaskSummaries.size;
3258
+ diagnostics.initiativeLoadMs = Math.max(0, initiativesLoadedAt - startedAt);
3259
+ diagnostics.workstreamLoadMs = Math.max(0, workstreamsLoadedAt - initiativesLoadedAt);
3260
+ diagnostics.taskSummaryQueryMs = Math.max(0, taskRowsLoadedAt - workstreamsLoadedAt);
3261
+ diagnostics.taskSummaryBuildMs = Math.max(0, summariesBuiltAt - taskRowsLoadedAt);
3262
+ }
3263
+ return {
3264
+ initiatives,
3265
+ workstreams,
3266
+ workstreamTaskSummaries: Array.from(workstreamTaskSummaries.values()),
3267
+ };
3268
+ }
3128
3269
  createWorkstream(input, workspaceId) {
3129
3270
  const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
3130
3271
  const id = String(input.id || '').trim() || this.generateEntityId('workstream');
@@ -3899,7 +4040,7 @@ export class TaskforceCore {
3899
4040
  listTasks(workspaceId) {
3900
4041
  const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
3901
4042
  const rows = this.db.prepare(`
3902
- SELECT * FROM tasks
4043
+ SELECT * FROM tasks
3903
4044
  WHERE tenant_id = ?
3904
4045
  AND workspace_id = ?
3905
4046
  AND is_archived = 0
@@ -3910,6 +4051,112 @@ export class TaskforceCore {
3910
4051
  config: this.readConfig()
3911
4052
  };
3912
4053
  }
4054
+ /**
4055
+ * Filter-pushdown listing used by read-only surfaces that don't need the
4056
+ * full task graph (MCP list_tasks, CLI summaries). Avoids the per-task
4057
+ * comments/events/attachments/checklist subqueries and actor resolution
4058
+ * that rowToTask performs, and applies filters + sort + limit at the SQL
4059
+ * layer so hot polls don't load the entire workspace.
4060
+ */
4061
+ listTasksSlim(workspaceId, filters = {}) {
4062
+ const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
4063
+ const clauses = [
4064
+ 'tenant_id = ?',
4065
+ 'workspace_id = ?',
4066
+ 'is_archived = 0',
4067
+ ];
4068
+ const params = [this.tenantId, normalizedWorkspaceId];
4069
+ if (filters.category) {
4070
+ clauses.push('category = ?');
4071
+ params.push(filters.category);
4072
+ }
4073
+ if (filters.status) {
4074
+ clauses.push('status = ?');
4075
+ params.push(filters.status);
4076
+ }
4077
+ else if (filters.openOnly) {
4078
+ clauses.push("status IN ('task','on-hold','in-progress','review')");
4079
+ }
4080
+ if (filters.priority !== undefined) {
4081
+ clauses.push('priority = ?');
4082
+ params.push(Number(filters.priority));
4083
+ }
4084
+ if (filters.type) {
4085
+ clauses.push('type = ?');
4086
+ params.push(filters.type);
4087
+ }
4088
+ if (filters.assignee) {
4089
+ if (filters.assignee === 'unassigned') {
4090
+ clauses.push("(assignee IS NULL OR assignee = '' OR assignee = 'unassigned')");
4091
+ }
4092
+ else {
4093
+ clauses.push('assignee = ?');
4094
+ params.push(filters.assignee);
4095
+ }
4096
+ }
4097
+ const order = filters.order === 'asc' ? 'ASC' : 'DESC';
4098
+ const sort = filters.sort ?? 'created';
4099
+ let orderBy;
4100
+ if (sort === 'priority') {
4101
+ orderBy = `priority ${order}, created_at ${order}`;
4102
+ }
4103
+ else if (sort === 'updated') {
4104
+ orderBy = `COALESCE(updated_at, created_at) ${order}`;
4105
+ }
4106
+ else {
4107
+ orderBy = `created_at ${order}`;
4108
+ }
4109
+ let sql = `SELECT * FROM tasks WHERE ${clauses.join(' AND ')} ORDER BY ${orderBy}`;
4110
+ const rawLimit = Number(filters.limit);
4111
+ const limit = Number.isFinite(rawLimit) ? Math.max(0, Math.floor(rawLimit)) : 0;
4112
+ if (limit > 0) {
4113
+ sql += ' LIMIT ?';
4114
+ params.push(limit);
4115
+ }
4116
+ const rows = this.db.prepare(sql).all(...params);
4117
+ return rows.map((row) => {
4118
+ const sanitizedAssignee = this.sanitizeAssignee(row.assignee, normalizedWorkspaceId);
4119
+ const scheduledDate = this.sanitizeDateOnly(row.scheduled_date);
4120
+ const dueDate = this.sanitizeDateOnly(row.due_date);
4121
+ const scheduledWeekKey = row.scheduled_week_key
4122
+ || (scheduledDate ? this.dateOnlyToIsoWeekKey(scheduledDate) : null);
4123
+ const referenceNumber = this.sanitizeTaskReferenceNumber(row.reference_number);
4124
+ const localReferenceNumber = this.sanitizeTaskReferenceNumber(row.local_reference_number);
4125
+ let taxonomies = {};
4126
+ try {
4127
+ taxonomies = JSON.parse(row.taxonomies || '{}');
4128
+ }
4129
+ catch {
4130
+ taxonomies = {};
4131
+ }
4132
+ return {
4133
+ id: String(row.id || '').trim(),
4134
+ referenceNumber,
4135
+ localReferenceNumber,
4136
+ referenceLabel: deriveTaskReferenceLabel({ referenceNumber, localReferenceNumber }),
4137
+ title: row.title,
4138
+ status: this.normalizeStatusValue(row.status),
4139
+ priority: this.normalizePriorityValue(row.priority),
4140
+ complexity: row.complexity,
4141
+ type: row.type,
4142
+ category: row.category,
4143
+ approach: row.approach || undefined,
4144
+ createdAt: row.created_at,
4145
+ updatedAt: row.updated_at,
4146
+ completedAt: row.completed_at,
4147
+ canceledReason: row.canceled_reason,
4148
+ isArchived: row.is_archived === 1,
4149
+ scheduledDate,
4150
+ dueDate,
4151
+ scheduledWeekKey,
4152
+ orderInDay: this.sanitizeOrderInDay(row.order_in_day),
4153
+ workstreamId: this.sanitizeWorkstreamId(row.workstream_id),
4154
+ taxonomies,
4155
+ createdBy: this.normalizeCreatedBy(row.created_by, normalizedWorkspaceId, { preserveGenericAi: true }),
4156
+ assignee: sanitizedAssignee,
4157
+ };
4158
+ });
4159
+ }
3913
4160
  getDataVersion() {
3914
4161
  if (this.db.provider === 'sqlite') {
3915
4162
  const version = this.db.pragma('data_version', { simple: true });
@@ -4050,9 +4297,9 @@ export class TaskforceCore {
4050
4297
  `).get(this.tenantId, userId);
4051
4298
  if (!existingUser) {
4052
4299
  this.db.prepare(`
4053
- INSERT INTO users (id, tenant_id, email, display_name, auth_provider, external_auth_id, beta_access, is_disabled, created_at, updated_at)
4054
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
4055
- `).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);
4056
4303
  }
4057
4304
  const membership = this.db.prepare(`
4058
4305
  SELECT id, role FROM workspace_memberships
@@ -4097,16 +4344,17 @@ export class TaskforceCore {
4097
4344
  `).get(this.tenantId, normalizedUserId, normalizedWorkspaceId);
4098
4345
  if (!membership?.workspace_id)
4099
4346
  return null;
4100
- const disabled = Boolean(userRow.is_disabled);
4347
+ const accountDisabled = Boolean(userRow.is_disabled);
4101
4348
  const userBetaAccess = userRow.beta_access === undefined || userRow.beta_access === null
4102
4349
  ? true
4103
4350
  : Boolean(userRow.beta_access);
4104
4351
  const isSystemAdmin = this.normalizeSystemRole(userRow.system_role, 'user') === 'system_admin';
4105
4352
  const membershipActive = !membership.status || String(membership.status).toLowerCase() === 'active';
4353
+ const workspaceDisabled = !membershipActive;
4106
4354
  const baseRole = this.normalizeAccessRole(membership.role || fallbackRole, fallbackRole);
4107
4355
  const permissionMode = this.normalizePermissionMode(membership.permission_mode, 'read-write');
4108
4356
  const isMemberReadOnly = baseRole === 'member' && permissionMode === 'read-only';
4109
- const resolvedRole = (disabled || !membershipActive || isMemberReadOnly)
4357
+ const resolvedRole = (accountDisabled || workspaceDisabled || isMemberReadOnly)
4110
4358
  ? 'read-only'
4111
4359
  : baseRole;
4112
4360
  return {
@@ -4114,7 +4362,9 @@ export class TaskforceCore {
4114
4362
  workspaceId: String(membership.workspace_id || normalizedWorkspaceId),
4115
4363
  role: resolvedRole,
4116
4364
  permissionMode,
4117
- disabled,
4365
+ disabled: accountDisabled || workspaceDisabled,
4366
+ accountDisabled,
4367
+ workspaceDisabled,
4118
4368
  betaAccess: userBetaAccess || isSystemAdmin
4119
4369
  };
4120
4370
  }
@@ -4333,7 +4583,68 @@ export class TaskforceCore {
4333
4583
  hasAnySystemAdmin() {
4334
4584
  return this.countSystemAdmins() > 0;
4335
4585
  }
4336
- 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);
4337
4648
  const rows = this.db.prepare(`
4338
4649
  SELECT
4339
4650
  u.id AS user_id,
@@ -4360,8 +4671,10 @@ export class TaskforceCore {
4360
4671
  ON p.tenant_id = u.tenant_id
4361
4672
  AND p.plan_id = v.plan_id
4362
4673
  WHERE u.tenant_id = ?
4363
- ORDER BY COALESCE(u.updated_at, u.created_at) DESC, u.created_at DESC
4364
- `).all(this.tenantId);
4674
+ ${searchClause}
4675
+ ${orderByClause}
4676
+ LIMIT ? OFFSET ?
4677
+ `).all(...params);
4365
4678
  return rows
4366
4679
  .filter((row) => row.user_id && row.email)
4367
4680
  .map((row) => ({
@@ -4653,6 +4966,7 @@ export class TaskforceCore {
4653
4966
  u.id AS user_id,
4654
4967
  u.email AS email,
4655
4968
  u.display_name AS display_name,
4969
+ u.avatar_url AS avatar_url,
4656
4970
  u.is_disabled AS is_disabled,
4657
4971
  u.created_at AS created_at,
4658
4972
  u.updated_at AS updated_at,
@@ -4669,19 +4983,23 @@ export class TaskforceCore {
4669
4983
  AND m.workspace_id = ?
4670
4984
  ORDER BY COALESCE(m.updated_at, m.created_at) DESC, u.created_at DESC
4671
4985
  `).all(this.tenantId, normalizedWorkspaceId);
4672
- return rows.map((row) => ({
4673
- userId: row.user_id,
4674
- email: row.email,
4675
- displayName: row.display_name || null,
4676
- role: this.normalizeAccessRole(row.role || 'member', 'member'),
4677
- permissionMode: this.normalizePermissionMode(row.permission_mode, 'read-write'),
4678
- betaAccess: row.beta_access === undefined || row.beta_access === null ? true : Boolean(row.beta_access),
4679
- status: String(row.status || 'active'),
4680
- disabled: Boolean(row.is_disabled),
4681
- createdAt: row.created_at || null,
4682
- updatedAt: row.updated_at || null,
4683
- emailVerifiedAt: row.email_verified_at || null
4684
- }));
4986
+ return rows.map((row) => {
4987
+ const status = String(row.status || 'active');
4988
+ return {
4989
+ userId: row.user_id,
4990
+ email: row.email,
4991
+ displayName: row.display_name || null,
4992
+ avatarUrl: row.avatar_url || null,
4993
+ role: this.normalizeAccessRole(row.role || 'member', 'member'),
4994
+ permissionMode: this.normalizePermissionMode(row.permission_mode, 'read-write'),
4995
+ betaAccess: row.beta_access === undefined || row.beta_access === null ? true : Boolean(row.beta_access),
4996
+ status,
4997
+ disabled: Boolean(row.is_disabled) || status.toLowerCase() === 'disabled',
4998
+ createdAt: row.created_at || null,
4999
+ updatedAt: row.updated_at || null,
5000
+ emailVerifiedAt: row.email_verified_at || null
5001
+ };
5002
+ });
4685
5003
  }
4686
5004
  replaceWorkspaceUsersForSync(input) {
4687
5005
  const workspaceId = String(input.workspaceId || 'default').trim() || 'default';
@@ -4725,9 +5043,9 @@ export class TaskforceCore {
4725
5043
  }
4726
5044
  else {
4727
5045
  this.db.prepare(`
4728
- INSERT INTO users (id, tenant_id, email, display_name, auth_provider, external_auth_id, beta_access, is_disabled, created_at, updated_at, email_verified_at)
4729
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
4730
- `).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);
4731
5049
  }
4732
5050
  const membership = this.db.prepare(`
4733
5051
  SELECT id
@@ -4770,8 +5088,8 @@ export class TaskforceCore {
4770
5088
  tx();
4771
5089
  return this.listWorkspaceUsers(workspaceId);
4772
5090
  }
4773
- listAiProfiles(workspaceId) {
4774
- return listAiProfilesService(this.getAiProfileServiceDeps(), workspaceId);
5091
+ listAiProfiles(workspaceId, options) {
5092
+ return listAiProfilesService(this.getAiProfileServiceDeps(), workspaceId, options);
4775
5093
  }
4776
5094
  getAiProfileByToken(input) {
4777
5095
  return getAiProfileByTokenService(this.getAiProfileServiceDeps(), input);
@@ -4782,8 +5100,14 @@ export class TaskforceCore {
4782
5100
  mergeAiProfiles(input) {
4783
5101
  return mergeAiProfilesService(this.getAiProfileServiceDeps(), input);
4784
5102
  }
4785
- resolveAiProfile(input) {
4786
- 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);
4787
5111
  }
4788
5112
  listAnnotatedAttachmentSessions(input) {
4789
5113
  return listAnnotatedAttachmentSessionsService(this.getAnnotatedAttachmentServiceDeps(), input);
@@ -4813,17 +5137,30 @@ export class TaskforceCore {
4813
5137
  const activeSessions = deps.sessionStore.listByWorkspace('default', { includeDeleted: false });
4814
5138
  if (activeSessions.length === 0)
4815
5139
  return;
4816
- const hasActiveImageLink = this.db.prepare(`
4817
- SELECT 1
4818
- FROM workspace_asset_links
4819
- WHERE tenant_id = ?
4820
- AND workspace_id = ?
4821
- AND task_id = ?
4822
- AND asset_id = ?
4823
- AND role = 'image'
4824
- AND deleted_at IS NULL
4825
- LIMIT 1
4826
- `);
5140
+ const hasActiveImageLink = this.supportsWorkspaceAssetLinkTargetType()
5141
+ ? this.db.prepare(`
5142
+ SELECT 1
5143
+ FROM workspace_asset_links
5144
+ WHERE tenant_id = ?
5145
+ AND workspace_id = ?
5146
+ AND target_type = 'task'
5147
+ AND task_id = ?
5148
+ AND asset_id = ?
5149
+ AND role = 'image'
5150
+ AND deleted_at IS NULL
5151
+ LIMIT 1
5152
+ `)
5153
+ : this.db.prepare(`
5154
+ SELECT 1
5155
+ FROM workspace_asset_links
5156
+ WHERE tenant_id = ?
5157
+ AND workspace_id = ?
5158
+ AND task_id = ?
5159
+ AND asset_id = ?
5160
+ AND role = 'image'
5161
+ AND deleted_at IS NULL
5162
+ LIMIT 1
5163
+ `);
4827
5164
  for (const session of activeSessions) {
4828
5165
  const taskId = String(session.taskId || '').trim();
4829
5166
  const imageAssetId = String(session.baseImageAssetId || '').trim();
@@ -4982,6 +5319,7 @@ export class TaskforceCore {
4982
5319
  FROM ai_profiles
4983
5320
  WHERE tenant_id = ?
4984
5321
  AND workspace_id = ?
5322
+ AND archived_at IS NULL
4985
5323
  AND (
4986
5324
  LOWER(name) = LOWER(?)
4987
5325
  OR LOWER(username) = LOWER(?)
@@ -4992,6 +5330,22 @@ export class TaskforceCore {
4992
5330
  if (aiProfileByName) {
4993
5331
  return this.aiProfileToActor(this.aiProfileRowToRecord(aiProfileByName));
4994
5332
  }
5333
+ const archivedAiProfileByName = this.db.prepare(`
5334
+ SELECT *
5335
+ FROM ai_profiles
5336
+ WHERE tenant_id = ?
5337
+ AND workspace_id = ?
5338
+ AND archived_at IS NOT NULL
5339
+ AND (
5340
+ LOWER(name) = LOWER(?)
5341
+ OR LOWER(username) = LOWER(?)
5342
+ )
5343
+ ORDER BY COALESCE(last_active_at, updated_at, created_at) DESC, id ASC
5344
+ LIMIT 1
5345
+ `).get(this.tenantId, normalizedWorkspaceId, raw, raw);
5346
+ if (archivedAiProfileByName) {
5347
+ return this.aiProfileToActor(this.aiProfileRowToRecord(archivedAiProfileByName));
5348
+ }
4995
5349
  const workspaceUser = this.listWorkspaceUsers(normalizedWorkspaceId).find((user) => user.userId === raw);
4996
5350
  if (workspaceUser) {
4997
5351
  return this.workspaceUserToActor(workspaceUser);
@@ -5235,6 +5589,9 @@ export class TaskforceCore {
5235
5589
  listPlanFeatureCatalog() {
5236
5590
  return this.planEntitlementService.listPlanFeatureCatalog();
5237
5591
  }
5592
+ updatePlanFeatureCatalogMarketingCopy(input) {
5593
+ return this.planEntitlementService.updatePlanFeatureCatalogMarketingCopy(input);
5594
+ }
5238
5595
  listPlanVersions(planId) {
5239
5596
  return this.planEntitlementService.listPlanVersions(planId);
5240
5597
  }
@@ -5354,11 +5711,6 @@ export class TaskforceCore {
5354
5711
  }
5355
5712
  }
5356
5713
  const now = new Date().toISOString();
5357
- this.db.prepare(`
5358
- UPDATE users
5359
- SET is_disabled = ?, updated_at = ?
5360
- WHERE tenant_id = ? AND id = ?
5361
- `).run(disabled ? 1 : 0, now, this.tenantId, normalizedUserId);
5362
5714
  if (membership?.id) {
5363
5715
  this.db.prepare(`
5364
5716
  UPDATE workspace_memberships
@@ -5532,9 +5884,7 @@ export class TaskforceCore {
5532
5884
  if (!email || !email.includes('@')) {
5533
5885
  throw new TaskforceRuleError('Valid email is required', 400, 'EMAIL_REQUIRED');
5534
5886
  }
5535
- if (!password || password.length < 8) {
5536
- throw new TaskforceRuleError('Password must be at least 8 characters', 400, 'PASSWORD_TOO_SHORT');
5537
- }
5887
+ assertPasswordPolicy(password, { email, displayName: input.displayName || null });
5538
5888
  if (!workspaceId || workspaceId.toLowerCase() === 'default') {
5539
5889
  throw new TaskforceRuleError('A non-default workspaceId is required', 400, 'WORKSPACE_ID_REQUIRED');
5540
5890
  }
@@ -5551,20 +5901,16 @@ export class TaskforceCore {
5551
5901
  email,
5552
5902
  displayName
5553
5903
  });
5554
- if (emailVerified) {
5555
- this.db.prepare(`
5556
- UPDATE users
5557
- SET password_hash = ?, auth_provider = ?, external_auth_id = ?, email_verified_at = ?, beta_access = ?, is_disabled = 0, updated_at = ?
5558
- WHERE tenant_id = ? AND id = ?
5559
- `).run(hashPassword(password), 'password', email, now, betaAccess ? 1 : 0, now, this.tenantId, userId);
5560
- }
5561
- else {
5562
- this.db.prepare(`
5563
- UPDATE users
5564
- SET password_hash = ?, auth_provider = ?, external_auth_id = ?, beta_access = ?, is_disabled = 0, updated_at = ?
5565
- WHERE tenant_id = ? AND id = ?
5566
- `).run(hashPassword(password), 'password', email, betaAccess ? 1 : 0, now, this.tenantId, userId);
5567
- }
5904
+ this.db.prepare(`
5905
+ UPDATE users
5906
+ SET
5907
+ email_verified_at = CASE WHEN ? = 1 THEN COALESCE(email_verified_at, ?) ELSE email_verified_at END,
5908
+ beta_access = ?,
5909
+ is_disabled = 0,
5910
+ updated_at = ?
5911
+ WHERE tenant_id = ? AND id = ?
5912
+ `).run(emailVerified ? 1 : 0, now, betaAccess ? 1 : 0, now, this.tenantId, userId);
5913
+ this.identityService.setPasswordIdentity(userId, password);
5568
5914
  this.updateWorkspaceUserRole(userId, workspaceId, role);
5569
5915
  this.setWorkspaceUserDisabled(userId, workspaceId, false);
5570
5916
  return { userId, workspaceId, role };
@@ -5575,9 +5921,7 @@ export class TaskforceCore {
5575
5921
  if (!email || !email.includes('@')) {
5576
5922
  throw new TaskforceRuleError('Valid email is required', 400, 'EMAIL_REQUIRED');
5577
5923
  }
5578
- if (!password || password.length < 8) {
5579
- throw new TaskforceRuleError('Password must be at least 8 characters', 400, 'PASSWORD_TOO_SHORT');
5580
- }
5924
+ assertPasswordPolicy(password, { email, displayName: input.displayName || null });
5581
5925
  const existing = this.db.prepare(`
5582
5926
  SELECT id FROM users WHERE tenant_id = ? AND email = ? LIMIT 1
5583
5927
  `).get(this.tenantId, email);
@@ -5606,9 +5950,10 @@ export class TaskforceCore {
5606
5950
  const userId = this.generateEntityId('user');
5607
5951
  const displayName = String(input.displayName || email.split('@')[0] || email).trim() || email;
5608
5952
  this.db.prepare(`
5609
- INSERT INTO users (id, tenant_id, email, display_name, auth_provider, external_auth_id, password_hash, system_role, beta_access, is_disabled, created_at, updated_at)
5610
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)
5611
- `).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);
5612
5957
  applyRegistrationCommercialState(userId, now);
5613
5958
  return {
5614
5959
  userId,
@@ -5642,6 +5987,151 @@ export class TaskforceCore {
5642
5987
  commercialState: onboardingOutcome.commercialState
5643
5988
  };
5644
5989
  }
5990
+ // ─── OAuth state management (delegated to AuthIdentityService) ────────────
5991
+ /**
5992
+ * Creates an OAuth state record and returns the state token + PKCE materials.
5993
+ * Delegates to AuthIdentityService.createOAuthState.
5994
+ */
5995
+ createOAuthLoginState(input) {
5996
+ return this.identityService.createOAuthState(input);
5997
+ }
5998
+ /**
5999
+ * Atomically consumes an OAuth state token and returns the row.
6000
+ * Returns null if invalid, expired, or already consumed.
6001
+ */
6002
+ consumeOAuthLoginState(state) {
6003
+ return this.identityService.consumeOAuthState(state);
6004
+ }
6005
+ // ─── OAuth identity operations ─────────────────────────────────────────────
6006
+ /**
6007
+ * Finds the Taskforce user associated with a provider identity.
6008
+ * Returns { userId, identityId } or null if not yet linked.
6009
+ */
6010
+ findUserByOAuthProvider(provider, providerSubject) {
6011
+ return this.identityService.findUserByProviderIdentity({ provider, providerSubject });
6012
+ }
6013
+ /**
6014
+ * Links an OAuth provider identity to an existing user.
6015
+ * Throws IDENTITY_ALREADY_EXISTS if the user already has an identity for this provider.
6016
+ */
6017
+ linkOAuthProviderToUser(input) {
6018
+ return this.identityService.linkProviderIdentity(input);
6019
+ }
6020
+ /**
6021
+ * Lists all active login identities for a user.
6022
+ * Used by the Account Hub "Login methods" section.
6023
+ */
6024
+ listLoginMethodsForUser(userId) {
6025
+ return this.identityService.listIdentitiesForUser(userId);
6026
+ }
6027
+ /**
6028
+ * Unlinks a provider identity from a user.
6029
+ * Throws LAST_IDENTITY_REQUIRED if it would be the last active method.
6030
+ */
6031
+ unlinkLoginMethod(userId, provider) {
6032
+ return this.identityService.unlinkProviderIdentity({ userId, provider });
6033
+ }
6034
+ /**
6035
+ * Adds or replaces a password identity for a user that signed up via OAuth.
6036
+ * Uses upsert semantics (safe to call on accounts that already have a password).
6037
+ */
6038
+ addPasswordToAccount(userId, password) {
6039
+ const user = this.db.prepare(`
6040
+ SELECT email, display_name
6041
+ FROM users
6042
+ WHERE tenant_id = ? AND id = ?
6043
+ LIMIT 1
6044
+ `).get(this.tenantId, String(userId || '').trim());
6045
+ assertPasswordPolicy(password, {
6046
+ email: user?.email || null,
6047
+ displayName: user?.display_name || null
6048
+ });
6049
+ return this.identityService.setPasswordIdentity(userId, password);
6050
+ }
6051
+ /**
6052
+ * Registers a new user account via an OAuth provider (no password).
6053
+ *
6054
+ * Creates the user row, applies commercial onboarding, and links the
6055
+ * provider identity — all in a single transaction.
6056
+ *
6057
+ * Returns the same shape as `registerPasswordUser` so callers can use
6058
+ * the same session-issuance path.
6059
+ */
6060
+ registerOAuthUser(input) {
6061
+ const email = input.email ? String(input.email).trim().toLowerCase() : null;
6062
+ if (email && !email.includes('@')) {
6063
+ throw new TaskforceRuleError('Valid email is required', 400, 'EMAIL_REQUIRED');
6064
+ }
6065
+ // Duplicate guard — email uniqueness (if email is provided)
6066
+ if (email) {
6067
+ const existing = this.db.prepare(`
6068
+ SELECT id FROM users WHERE tenant_id = ? AND email = ? LIMIT 1
6069
+ `).get(this.tenantId, email);
6070
+ if (existing?.id) {
6071
+ throw new TaskforceRuleError('Email is already registered', 409, 'EMAIL_ALREADY_EXISTS');
6072
+ }
6073
+ }
6074
+ const assignFirstSystemAdmin = !this.hasAnySystemAdmin();
6075
+ const betaAccess = input.betaAccess !== false;
6076
+ const onboardingOutcome = this.resolveRegistrationOnboardingOutcome(input.planVersionId);
6077
+ const now = new Date().toISOString();
6078
+ const userId = this.generateEntityId('user');
6079
+ const displayName = String(input.displayName || (email ? email.split('@')[0] : '') || '').trim()
6080
+ || (email || userId);
6081
+ this.db.prepare(`
6082
+ INSERT INTO users
6083
+ (id, tenant_id, email, email_verified_at, display_name, system_role,
6084
+ beta_access, is_disabled, created_at, updated_at)
6085
+ VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?)
6086
+ `).run(userId, this.tenantId, email,
6087
+ // OAuth providers that attest email_verified give us a verified email up-front
6088
+ (email && input.emailVerified) ? now : null, displayName, assignFirstSystemAdmin ? 'system_admin' : 'user', betaAccess ? 1 : 0, now, now);
6089
+ const identityId = this.identityService.linkProviderIdentity({
6090
+ userId,
6091
+ provider: input.provider,
6092
+ providerSubject: input.providerSubject,
6093
+ providerEmail: email,
6094
+ providerEmailVerified: input.emailVerified ?? null,
6095
+ displayNameSnapshot: input.displayName ?? null,
6096
+ avatarUrlSnapshot: input.avatarUrl ?? null,
6097
+ });
6098
+ this.transitionCanonicalCommercialState({
6099
+ accountId: userId,
6100
+ planVersionId: onboardingOutcome.planVersionId,
6101
+ state: onboardingOutcome.commercialState,
6102
+ billingMirrorPlanId: onboardingOutcome.planId,
6103
+ source: 'registration',
6104
+ updatedBy: 'registration',
6105
+ createdAt: now,
6106
+ updatedAt: now
6107
+ });
6108
+ return {
6109
+ userId,
6110
+ workspaceId: null,
6111
+ role: 'member',
6112
+ identityId,
6113
+ planSelectionRequired: onboardingOutcome.requiresPlanSelection,
6114
+ checkoutPending: onboardingOutcome.checkoutPending,
6115
+ commercialState: onboardingOutcome.commercialState,
6116
+ };
6117
+ }
6118
+ /**
6119
+ * Looks up a user by email address (exact, case-insensitive).
6120
+ * Returns { userId, email } or null.
6121
+ */
6122
+ findUserByEmail(email) {
6123
+ const normalized = String(email || '').trim().toLowerCase();
6124
+ if (!normalized)
6125
+ return null;
6126
+ const row = this.db.prepare(`
6127
+ SELECT id, email FROM users
6128
+ WHERE tenant_id = ? AND email = ? AND COALESCE(is_disabled, 0) = 0
6129
+ LIMIT 1
6130
+ `).get(this.tenantId, normalized);
6131
+ if (!row?.id)
6132
+ return null;
6133
+ return { userId: row.id, email: row.email || normalized };
6134
+ }
5645
6135
  createOneTimeAuthToken(userId, purpose, ttlSeconds, workspaceId) {
5646
6136
  return this.authTokenService.createOneTimeAuthToken(userId, purpose, ttlSeconds, workspaceId);
5647
6137
  }
@@ -5744,19 +6234,23 @@ export class TaskforceCore {
5744
6234
  if (!inspection.userId)
5745
6235
  return inspection;
5746
6236
  const user = this.db.prepare(`
5747
- SELECT email, password_hash
5748
- FROM users
5749
- WHERE tenant_id = ? AND id = ? LIMIT 1
6237
+ SELECT email FROM users WHERE tenant_id = ? AND id = ? LIMIT 1
5750
6238
  `).get(this.tenantId, inspection.userId);
5751
6239
  const workspaceId = String(inspection.workspaceId || '').trim();
5752
- const 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);
5753
6244
  return {
5754
6245
  state: inspection.state,
5755
6246
  email: user?.email,
5756
6247
  userId: inspection.userId,
5757
6248
  workspaceId: workspaceId || 'default',
5758
- inviteeKind: hasPassword ? 'existing_user' : 'new_user',
5759
- passwordRequired: !hasPassword
6249
+ inviteeState,
6250
+ availableMethods,
6251
+ // Backward-compatible fields
6252
+ inviteeKind: hasIdentity ? 'existing_user' : 'new_user',
6253
+ passwordRequired: !hasIdentity
5760
6254
  };
5761
6255
  }
5762
6256
  completeInviteWorkspaceActivation(input) {
@@ -5813,14 +6307,20 @@ export class TaskforceCore {
5813
6307
  role: this.normalizeAccessRole(membership?.role || 'member', 'member')
5814
6308
  };
5815
6309
  }
5816
- 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) {
5817
6321
  const token = String(input.token || '').trim();
5818
- const password = String(input.password || '');
5819
- if (!token || !password) {
5820
- throw new TaskforceRuleError('Token and password are required', 400, 'TOKEN_PASSWORD_REQUIRED');
5821
- }
5822
- if (password.length < 8) {
5823
- throw new TaskforceRuleError('Password must be at least 8 characters', 400, 'PASSWORD_TOO_SHORT');
6322
+ if (!token) {
6323
+ throw new TaskforceRuleError('Invite token is required', 400, 'TOKEN_REQUIRED');
5824
6324
  }
5825
6325
  const consumed = this.consumeOneTimeAuthToken(token, 'invite_accept');
5826
6326
  if (!consumed?.userId) {
@@ -5841,38 +6341,111 @@ export class TaskforceCore {
5841
6341
  throw new TaskforceRuleError('Invite token is missing workspace context', 400, 'INVITE_WORKSPACE_REQUIRED');
5842
6342
  }
5843
6343
  const user = this.db.prepare(`
5844
- SELECT id, email, 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
5845
6411
  FROM users
5846
6412
  WHERE tenant_id = ? AND id = ?
5847
6413
  LIMIT 1
5848
- `).get(this.tenantId, consumed.userId);
6414
+ `).get(this.tenantId, inspected.userId);
5849
6415
  if (!user?.id || !user.email) {
5850
6416
  throw new TaskforceRuleError('User not found', 404, 'USER_NOT_FOUND');
5851
6417
  }
5852
- 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)) {
5853
6423
  throw new TaskforceRuleError('Existing users must sign in before joining this workspace', 409, 'INVITE_SIGN_IN_REQUIRED');
5854
6424
  }
6425
+ const consumed = this.consumeOneTimeAuthToken(token, 'invite_accept');
6426
+ if (!consumed?.userId) {
6427
+ throw new TaskforceRuleError('Invalid invite token', 400, 'INVITE_TOKEN_INVALID');
6428
+ }
5855
6429
  const now = new Date().toISOString();
6430
+ const identityId = this.identityService.setPasswordIdentity(consumed.userId, password);
5856
6431
  this.db.prepare(`
5857
6432
  UPDATE users
5858
- SET
5859
- password_hash = ?,
5860
- auth_provider = ?,
5861
- external_auth_id = ?,
5862
- email_verified_at = COALESCE(email_verified_at, ?),
5863
- is_disabled = 0,
5864
- updated_at = ?
6433
+ SET email_verified_at = COALESCE(email_verified_at, ?), is_disabled = 0, updated_at = ?
5865
6434
  WHERE tenant_id = ? AND id = ?
5866
- `).run(hashPassword(password), 'password', user.email, now, now, this.tenantId, consumed.userId);
6435
+ `).run(now, now, this.tenantId, consumed.userId);
5867
6436
  this.ensureDefaultOnboardingEntitlement(consumed.userId, {
5868
6437
  source: 'invite-accept',
5869
6438
  updatedBy: 'invite-accept',
5870
6439
  now
5871
6440
  });
5872
- return this.completeInviteWorkspaceActivation({
6441
+ const activation = this.completeInviteWorkspaceActivation({
5873
6442
  userId: consumed.userId,
5874
6443
  workspaceId: inviteWorkspaceId
5875
6444
  });
6445
+ return {
6446
+ ...activation,
6447
+ identityId
6448
+ };
5876
6449
  }
5877
6450
  joinInviteWithAuthenticatedUser(input) {
5878
6451
  const token = String(input.token || '').trim();
@@ -5896,7 +6469,7 @@ export class TaskforceCore {
5896
6469
  }
5897
6470
  throw new TaskforceRuleError('Invalid invite token', 400, 'INVITE_TOKEN_INVALID');
5898
6471
  }
5899
- if (details.inviteeKind !== 'existing_user') {
6472
+ if (details.inviteeState !== 'existing_account') {
5900
6473
  throw new TaskforceRuleError('This invite requires account setup before joining', 409, 'INVITE_PASSWORD_SETUP_REQUIRED');
5901
6474
  }
5902
6475
  if (details.userId !== authenticatedUserId) {
@@ -5930,32 +6503,31 @@ export class TaskforceCore {
5930
6503
  const workspaceId = String(input.workspaceId || 'default').trim() || 'default';
5931
6504
  if (!email || !password)
5932
6505
  return { ok: false, code: 'INVALID_CREDENTIALS' };
5933
- const row = this.db.prepare(`
5934
- SELECT id, password_hash, email_verified_at
5935
- FROM users
5936
- WHERE tenant_id = ? AND email = ?
5937
- LIMIT 1
5938
- `).get(this.tenantId, email);
5939
- if (!row?.id || !row.password_hash)
5940
- return { ok: false, code: 'INVALID_CREDENTIALS' };
5941
- if (!verifyPassword(password, row.password_hash))
6506
+ const authResult = this.identityService.authenticatePasswordIdentity(email, password);
6507
+ if (!authResult)
5942
6508
  return { ok: false, code: 'INVALID_CREDENTIALS' };
5943
- if (input.requireVerifiedEmail && !row.email_verified_at) {
5944
- 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
+ }
5945
6516
  }
5946
- const resolvedWorkspaceId = this.resolvePreferredUserWorkspaceId(row.id, {
6517
+ const resolvedWorkspaceId = this.resolvePreferredUserWorkspaceId(authResult.userId, {
5947
6518
  preferredWorkspaceId: workspaceId
5948
6519
  }) || workspaceId;
5949
- const access = this.resolveUserAccess(row.id, resolvedWorkspaceId, input.fallbackRole || 'member');
6520
+ const access = this.resolveUserAccess(authResult.userId, resolvedWorkspaceId, input.fallbackRole || 'member');
5950
6521
  if (!access) {
5951
6522
  // Allow authenticated users without workspace memberships.
5952
6523
  // Provisioning flow will create/switch to a workspace after login.
5953
6524
  return {
5954
6525
  ok: true,
5955
6526
  access: {
5956
- userId: row.id,
6527
+ userId: authResult.userId,
5957
6528
  workspaceId: 'default',
5958
- role: this.normalizeAccessRole(input.fallbackRole || 'member', 'member')
6529
+ role: this.normalizeAccessRole(input.fallbackRole || 'member', 'member'),
6530
+ identityId: authResult.identityId
5959
6531
  }
5960
6532
  };
5961
6533
  }
@@ -5964,7 +6536,8 @@ export class TaskforceCore {
5964
6536
  access: {
5965
6537
  userId: access.userId,
5966
6538
  workspaceId: access.workspaceId,
5967
- role: access.role
6539
+ role: access.role,
6540
+ identityId: authResult.identityId
5968
6541
  }
5969
6542
  };
5970
6543
  }