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