@taskforcehq/taskforce 0.3.305 → 0.3.307

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 (440) hide show
  1. package/README.md +0 -2
  2. package/dist/Taskforce.module.css +603 -107
  3. package/dist/TaskforceCore.js +216 -43
  4. package/dist/TaskforceCore.test.js +611 -28
  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 +46 -2
  14. package/dist/compat/workspaceSyncCompat.test.js +38 -1
  15. package/dist/components/activity/EntityActivityTimeline.d.ts +34 -0
  16. package/dist/components/activity/EntityActivityTimeline.js +81 -0
  17. package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
  18. package/dist/components/context/ContextAttachmentManager.js +561 -0
  19. package/dist/components/features/AgentsModule.d.ts +14 -1
  20. package/dist/components/features/AgentsModule.js +366 -18
  21. package/dist/components/features/AgentsModule.test.js +507 -8
  22. package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +3 -1
  23. package/dist/components/features/AnnotatedAttachmentWorkspace.js +118 -4
  24. package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +52 -0
  25. package/dist/components/features/DocumentIndex.d.ts +2 -2
  26. package/dist/components/features/DocumentIndex.js +3 -4
  27. package/dist/components/features/DocumentViewer.d.ts +7 -1
  28. package/dist/components/features/DocumentViewer.js +62 -4
  29. package/dist/components/features/DocumentWorkspace.d.ts +10 -2
  30. package/dist/components/features/DocumentWorkspace.js +15 -4
  31. package/dist/components/features/DocumentWorkspace.test.js +51 -1
  32. package/dist/components/features/TaskSettings.d.ts +2 -0
  33. package/dist/components/features/TaskSettings.js +83 -78
  34. package/dist/components/features/TaskSettings.test.js +171 -82
  35. package/dist/components/features/documentWorkspaceModel.d.ts +1 -1
  36. package/dist/components/task/TaskActionHeader.js +11 -5
  37. package/dist/components/task/TaskActionHeader.test.js +10 -0
  38. package/dist/components/task/TaskCard.js +24 -29
  39. package/dist/components/task/TaskCard.test.js +38 -3
  40. package/dist/components/task/TaskContextUpload.js +4 -526
  41. package/dist/components/task/TaskContextUpload.test.js +3 -3
  42. package/dist/components/task/TaskForm.js +61 -116
  43. package/dist/components/task/TaskForm.test.js +110 -8
  44. package/dist/components/task/TaskKanban.d.ts +2 -1
  45. package/dist/components/task/TaskKanban.js +94 -21
  46. package/dist/components/task/TaskKanban.test.js +36 -0
  47. package/dist/components/task/TaskList.js +5 -1
  48. package/dist/components/task/TaskList.test.js +4 -0
  49. package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
  50. package/dist/components/ui/EditableAvatarButton.js +18 -0
  51. package/dist/components/ui/HierarchyActionBadge.d.ts +10 -0
  52. package/dist/components/ui/HierarchyActionBadge.js +5 -0
  53. package/dist/components/views/AppShellHeader.d.ts +3 -1
  54. package/dist/components/views/AppShellHeader.js +27 -11
  55. package/dist/components/views/AppShellHeader.test.js +18 -1
  56. package/dist/components/views/PlanComparisonPage.d.ts +9 -1
  57. package/dist/components/views/PlanComparisonPage.js +78 -14
  58. package/dist/components/views/PlanComparisonPage.test.js +257 -0
  59. package/dist/components/views/PlansPage.js +127 -36
  60. package/dist/components/views/PlansPage.test.js +294 -12
  61. package/dist/components/views/StandaloneLayout.js +529 -592
  62. package/dist/components/views/WidgetView.js +10 -8
  63. package/dist/components/views/panels/PlanningDrawer.d.ts +25 -5
  64. package/dist/components/views/panels/PlanningDrawer.js +270 -55
  65. package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
  66. package/dist/components/views/panels/PlanningDrawer.test.js +529 -0
  67. package/dist/components/views/planningScope.d.ts +20 -0
  68. package/dist/components/views/planningScope.js +25 -0
  69. package/dist/components/views/planningScope.test.d.ts +1 -0
  70. package/dist/components/views/planningScope.test.js +60 -0
  71. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +26 -1
  72. package/dist/components/views/standalone/modals/AccountHubModal.js +76 -2
  73. package/dist/components/views/standalone/modals/AccountHubModal.test.js +20 -3
  74. package/dist/components/views/standalone/modals/AvatarImageManagerModal.d.ts +20 -0
  75. package/dist/components/views/standalone/modals/AvatarImageManagerModal.js +127 -0
  76. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +2 -9
  77. package/dist/components/views/standalone/modals/EditProfileModal.js +3 -2
  78. package/dist/components/views/standalone/modals/SyncEnableWarningModal.d.ts +9 -0
  79. package/dist/components/views/standalone/modals/SyncEnableWarningModal.js +6 -0
  80. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +1 -0
  81. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +24 -0
  82. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +2 -1
  83. package/dist/components/views/standalone/modals/SyncStatusModal.js +2 -2
  84. package/dist/config/envSchema.js +1 -0
  85. package/dist/core/AiProfileService.d.ts +46 -2
  86. package/dist/core/AiProfileService.js +1073 -82
  87. package/dist/core/AiProfileService.test.js +19 -1
  88. package/dist/core/AiProfiles.test.js +1249 -44
  89. package/dist/core/AttachmentLinkService.d.ts +3 -0
  90. package/dist/core/AttachmentLinkService.js +110 -23
  91. package/dist/core/AuthIdentityService.d.ts +107 -0
  92. package/dist/core/AuthIdentityService.js +284 -0
  93. package/dist/core/AuthTokenService.d.ts +4 -0
  94. package/dist/core/AuthTokenService.js +34 -6
  95. package/dist/core/AuthTokenService.test.js +3 -3
  96. package/dist/core/EntitlementsPolicy.test.js +2 -2
  97. package/dist/core/GlobalSettingsService.js +127 -6
  98. package/dist/core/McpTokenService.d.ts +12 -0
  99. package/dist/core/McpTokenService.js +44 -1
  100. package/dist/core/PlanEntitlementService.d.ts +23 -2
  101. package/dist/core/PlanEntitlementService.js +343 -57
  102. package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
  103. package/dist/core/PlanFeatureCatalog.test.js +84 -0
  104. package/dist/core/PlanVersionPolicy.test.js +35 -2
  105. package/dist/core/PlanningEntities.test.js +418 -4
  106. package/dist/core/SignupMonetizationSettings.test.js +199 -27
  107. package/dist/core/SyncReconciliationService.js +2 -65
  108. package/dist/core/SystemAdmin.test.js +42 -42
  109. package/dist/core/TaskActivityService.d.ts +28 -1
  110. package/dist/core/TaskActivityService.js +227 -46
  111. package/dist/core/TaskActivitySync.test.d.ts +1 -0
  112. package/dist/core/TaskActivitySync.test.js +90 -0
  113. package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
  114. package/dist/core/TaskAuditTimeline.test.js +96 -0
  115. package/dist/core/Taskforce.d.ts +415 -40
  116. package/dist/core/Taskforce.ids.test.js +58 -0
  117. package/dist/core/Taskforce.js +1543 -266
  118. package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
  119. package/dist/core/Taskforce.listTasksSlim.test.js +91 -0
  120. package/dist/core/Taskforce.mcpAuth.test.js +109 -0
  121. package/dist/core/UserProfileAvatarDrafts.test.js +30 -3
  122. package/dist/core/WorkspaceLifecycleService.js +0 -5
  123. package/dist/core/WorkspacePermissions.test.js +25 -1
  124. package/dist/core/WorkspacePlanMode.test.js +117 -16
  125. package/dist/core/shared.d.ts +1 -1
  126. package/dist/core/shared.js +5 -1
  127. package/dist/core/types.d.ts +75 -2
  128. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +28 -2
  129. package/dist/hooks/bootstrapTimeouts.d.ts +4 -0
  130. package/dist/hooks/bootstrapTimeouts.js +8 -0
  131. package/dist/hooks/bootstrapTimeouts.test.d.ts +1 -0
  132. package/dist/hooks/bootstrapTimeouts.test.js +12 -0
  133. package/dist/hooks/sync/bootstrap.js +2 -0
  134. package/dist/hooks/sync/controlPlane.d.ts +37 -0
  135. package/dist/hooks/sync/controlPlane.js +78 -0
  136. package/dist/hooks/sync/controlPlane.test.d.ts +1 -0
  137. package/dist/hooks/sync/controlPlane.test.js +121 -0
  138. package/dist/hooks/sync/lifecyclePolicy.d.ts +45 -0
  139. package/dist/hooks/sync/lifecyclePolicy.js +93 -0
  140. package/dist/hooks/sync/lifecyclePolicy.test.d.ts +1 -0
  141. package/dist/hooks/sync/lifecyclePolicy.test.js +124 -0
  142. package/dist/hooks/sync/orchestratorShared.d.ts +13 -4
  143. package/dist/hooks/sync/orchestratorShared.js +129 -48
  144. package/dist/hooks/sync/orchestratorShared.test.js +50 -1
  145. package/dist/hooks/sync/pullLifecycle.d.ts +31 -0
  146. package/dist/hooks/sync/pullLifecycle.js +24 -0
  147. package/dist/hooks/sync/pullLifecycle.test.d.ts +1 -0
  148. package/dist/hooks/sync/pullLifecycle.test.js +80 -0
  149. package/dist/hooks/sync/recovery.d.ts +16 -2
  150. package/dist/hooks/sync/recovery.js +179 -53
  151. package/dist/hooks/sync/recovery.test.d.ts +1 -0
  152. package/dist/hooks/sync/recovery.test.js +292 -0
  153. package/dist/hooks/sync/transfers.d.ts +16 -3
  154. package/dist/hooks/sync/transfers.js +210 -66
  155. package/dist/hooks/sync/transfers.test.d.ts +1 -0
  156. package/dist/hooks/sync/transfers.test.js +928 -0
  157. package/dist/hooks/sync/useRecentSyncEvents.d.ts +21 -0
  158. package/dist/hooks/sync/useRecentSyncEvents.js +92 -0
  159. package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +1 -0
  160. package/dist/hooks/sync/useRecentSyncEvents.test.js +124 -0
  161. package/dist/hooks/sync/useSyncStatusActions.d.ts +30 -0
  162. package/dist/hooks/sync/useSyncStatusActions.js +88 -0
  163. package/dist/hooks/sync/useSyncStatusActions.test.d.ts +1 -0
  164. package/dist/hooks/sync/useSyncStatusActions.test.js +133 -0
  165. package/dist/hooks/sync/useSyncStatusControls.d.ts +25 -0
  166. package/dist/hooks/sync/useSyncStatusControls.js +60 -0
  167. package/dist/hooks/sync/useSyncStatusControls.test.d.ts +1 -0
  168. package/dist/hooks/sync/useSyncStatusControls.test.js +88 -0
  169. package/dist/hooks/useFilterSort.js +13 -13
  170. package/dist/hooks/useFilterSort.test.js +87 -1
  171. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +12 -0
  172. package/dist/hooks/useSyncOrchestrator.d.ts +9 -2
  173. package/dist/hooks/useSyncOrchestrator.js +409 -183
  174. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +407 -0
  175. package/dist/hooks/useTaskData.js +8 -3
  176. package/dist/hooks/useTaskData.test.js +45 -0
  177. package/dist/hooks/useTaskforce.d.ts +145 -52
  178. package/dist/hooks/useTaskforce.js +592 -253
  179. package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
  180. package/dist/hooks/useTaskforce.runtime-routing.test.js +246 -0
  181. package/dist/hooks/useTaskforce.sync-behavior.test.js +2468 -201
  182. package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
  183. package/dist/hooks/useWorkspaceSyncController.js +13 -2
  184. package/dist/hooks/useWorkspaceSyncController.test.js +48 -0
  185. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
  186. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +328 -40
  187. package/dist/localization/locales/en-US.d.ts +0 -1
  188. package/dist/localization/locales/en-US.js +0 -1
  189. package/dist/localization/locales/es-419.js +0 -1
  190. package/dist/localization/locales/pt-BR.js +0 -1
  191. package/dist/mcp/canonicalAssetHelpers.d.ts +16 -4
  192. package/dist/mcp/canonicalAssetHelpers.js +18 -10
  193. package/dist/mcp/canonicalAssetHelpers.test.d.ts +1 -0
  194. package/dist/mcp/canonicalAssetHelpers.test.js +67 -0
  195. package/dist/mcp/clientIntegrations.d.ts +1 -0
  196. package/dist/mcp/clientIntegrations.js +25 -0
  197. package/dist/mcp/clientIntegrations.test.js +14 -3
  198. package/dist/mcp/clientProfileDefaults.js +3 -3
  199. package/dist/mcp/collaborationRegistrar.js +7 -7
  200. package/dist/mcp/documentAssetRegistrar.js +116 -10
  201. package/dist/mcp/runtime.d.ts +3 -0
  202. package/dist/mcp/runtime.js +1099 -185
  203. package/dist/mcp/runtime.test.js +1491 -140
  204. package/dist/mcp/taskAttachmentTypes.d.ts +1 -1
  205. package/dist/mcp/taskPlanningRegistrar.js +48 -0
  206. package/dist/mcp/toolRegistry.d.ts +56 -0
  207. package/dist/mcp/toolRegistry.js +8 -0
  208. package/dist/mcp/toolRegistry.test.js +4 -0
  209. package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
  210. package/dist/migrations/taskSchemaMigrations.js +192 -61
  211. package/dist/migrations/taskSchemaMigrations.test.js +15 -0
  212. package/dist/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  213. package/dist/resources/templates/workflows/collaborate.yaml +1 -1
  214. package/dist/resources/templates/workflows/evaluate.yaml +2 -2
  215. package/dist/resources/templates/workflows/plan.yaml +1 -1
  216. package/dist/resources/templates/workflows/review.yaml +2 -2
  217. package/dist/server/aiProfileAvatarAssets.d.ts +49 -0
  218. package/dist/server/aiProfileAvatarAssets.js +319 -0
  219. package/dist/server/aiProfileAvatarAssets.test.d.ts +1 -0
  220. package/dist/server/aiProfileAvatarAssets.test.js +213 -0
  221. package/dist/server/annotatedAttachmentsRoutes.test.js +69 -10
  222. package/dist/server/auth/providers/apple.d.ts +33 -0
  223. package/dist/server/auth/providers/apple.js +82 -0
  224. package/dist/server/auth/providers/appleClientSecret.d.ts +37 -0
  225. package/dist/server/auth/providers/appleClientSecret.js +65 -0
  226. package/dist/server/auth/providers/github.d.ts +26 -0
  227. package/dist/server/auth/providers/github.js +86 -0
  228. package/dist/server/auth/providers/google.d.ts +21 -0
  229. package/dist/server/auth/providers/google.js +56 -0
  230. package/dist/server/auth/providers/types.d.ts +21 -0
  231. package/dist/server/auth/providers/types.js +1 -0
  232. package/dist/server/auth.d.ts +2 -0
  233. package/dist/server/auth.js +6 -3
  234. package/dist/server/documentReviewRoutes.test.js +36 -3
  235. package/dist/server/index.cookieProxy.test.js +1 -0
  236. package/dist/server/index.d.ts +12 -0
  237. package/dist/server/index.js +124 -9
  238. package/dist/server/index.rateLimit.test.js +3 -0
  239. package/dist/server/index.test.js +111 -3
  240. package/dist/server/routes/admin.d.ts +3 -0
  241. package/dist/server/routes/admin.js +459 -28
  242. package/dist/server/routes/annotatedAttachments.js +51 -0
  243. package/dist/server/routes/auth.d.ts +26 -2
  244. package/dist/server/routes/auth.js +1170 -71
  245. package/dist/server/routes/authSupport.d.ts +5 -1
  246. package/dist/server/routes/authSupport.js +13 -2
  247. package/dist/server/routes/billing.js +372 -35
  248. package/dist/server/routes/billing.test.js +595 -12
  249. package/dist/server/routes/documents.d.ts +1 -0
  250. package/dist/server/routes/documents.js +182 -70
  251. package/dist/server/routes/shared.d.ts +28 -2
  252. package/dist/server/routes/shared.js +69 -14
  253. package/dist/server/routes/sync.integration.test.js +658 -39
  254. package/dist/server/routes/sync.js +74 -608
  255. package/dist/server/routes/syncAuxRoutes.d.ts +15 -0
  256. package/dist/server/routes/syncAuxRoutes.js +91 -0
  257. package/dist/server/routes/syncAuxRoutes.test.d.ts +1 -0
  258. package/dist/server/routes/syncAuxRoutes.test.js +158 -0
  259. package/dist/server/routes/syncPullApplyRoutes.d.ts +36 -0
  260. package/dist/server/routes/syncPullApplyRoutes.js +204 -0
  261. package/dist/server/routes/syncPushRoutes.d.ts +24 -0
  262. package/dist/server/routes/syncPushRoutes.js +212 -0
  263. package/dist/server/routes/syncRouteGuards.d.ts +36 -0
  264. package/dist/server/routes/syncRouteGuards.js +67 -0
  265. package/dist/server/routes/syncRouteGuards.test.d.ts +1 -0
  266. package/dist/server/routes/syncRouteGuards.test.js +94 -0
  267. package/dist/server/routes/syncRouteTypes.d.ts +13 -0
  268. package/dist/server/routes/syncRouteTypes.js +1 -0
  269. package/dist/server/routes/syncSnapshotRoutes.d.ts +66 -0
  270. package/dist/server/routes/syncSnapshotRoutes.js +182 -0
  271. package/dist/server/routes/tasks.js +55 -10
  272. package/dist/server/routes/workspaces.js +36 -5
  273. package/dist/server/routes.js +147 -11
  274. package/dist/server/routes.test.js +2704 -183
  275. package/dist/services/workflowExportSnapshots.test.js +2 -0
  276. package/dist/shared/aiProfileSeatScope.d.ts +5 -0
  277. package/dist/shared/aiProfileSeatScope.js +21 -0
  278. package/dist/shared/aiProfileSurfaceType.d.ts +1 -1
  279. package/dist/shared/aiProfileSurfaceType.js +10 -8
  280. package/dist/shared/passwordPolicy.d.ts +13 -0
  281. package/dist/shared/passwordPolicy.js +84 -0
  282. package/dist/shared/passwordPolicy.test.d.ts +1 -0
  283. package/dist/shared/passwordPolicy.test.js +34 -0
  284. package/dist/storage/objectStorage.d.ts +1 -0
  285. package/dist/storage/objectStorage.js +2 -1
  286. package/dist/storage/objectStorageClient.d.ts +1 -0
  287. package/dist/storage/objectStorageClient.js +41 -8
  288. package/dist/storage/objectStorageClient.test.js +26 -1
  289. package/dist/storage/objectStorageConfig.js +3 -1
  290. package/dist/storage/objectStorageConfig.test.js +13 -1
  291. package/dist/storage/workspaceAssetStore.d.ts +15 -3
  292. package/dist/storage/workspaceAssetStore.js +132 -44
  293. package/dist/storage/workspaceAssetStore.test.js +93 -6
  294. package/dist/styles/fonts.css +70 -0
  295. package/dist/sync/collaborationSyncPayload.d.ts +6 -1
  296. package/dist/sync/collaborationSyncPayload.js +19 -11
  297. package/dist/sync/collaborationSyncPayload.test.js +14 -1
  298. package/dist/sync/collaborationSyncState.d.ts +3 -3
  299. package/dist/sync/collaborationSyncState.js +19 -12
  300. package/dist/sync/contentSyncPayload.d.ts +5 -1
  301. package/dist/sync/contentSyncPayload.js +9 -0
  302. package/dist/sync/contentSyncPayload.test.js +39 -0
  303. package/dist/sync/contentSyncState.d.ts +9 -9
  304. package/dist/sync/contentSyncState.js +248 -107
  305. package/dist/sync/planningSyncPayload.js +5 -0
  306. package/dist/sync/planningSyncPayload.test.js +24 -0
  307. package/dist/sync/syncApplyHandlers.d.ts +11 -4
  308. package/dist/sync/syncApplyHandlers.js +115 -42
  309. package/dist/sync/syncApplyHandlers.test.js +251 -4
  310. package/dist/sync/syncFieldSemantics.d.ts +12 -0
  311. package/dist/sync/syncFieldSemantics.js +55 -0
  312. package/dist/sync/syncResourceAdapters.d.ts +46 -0
  313. package/dist/sync/syncResourceAdapters.js +79 -0
  314. package/dist/sync/syncService.d.ts +6 -0
  315. package/dist/sync/syncService.js +65 -1
  316. package/dist/sync/syncService.test.js +96 -3
  317. package/dist/sync/taskSyncChangeKey.d.ts +2 -0
  318. package/dist/sync/taskSyncChangeKey.js +143 -0
  319. package/dist/sync/taskSyncPayload.js +3 -8
  320. package/dist/sync/workspacePullFeed.d.ts +4 -0
  321. package/dist/sync/workspacePullFeed.js +132 -72
  322. package/dist/sync/workspacePullFeed.test.js +92 -0
  323. package/dist/sync/workspaceRepair.js +5 -2
  324. package/dist/sync/workspaceSyncModel.d.ts +73 -6
  325. package/dist/sync/workspaceSyncModel.js +342 -73
  326. package/dist/sync/workspaceSyncModel.test.js +559 -14
  327. package/dist/sync/workspaceSyncState.d.ts +17 -0
  328. package/dist/sync/workspaceSyncState.js +2 -1
  329. package/dist/sync/workspaceSyncSurface.js +3 -8
  330. package/dist/types.d.ts +50 -2
  331. package/dist/ui/assets/AgentsModule-2y82_3DO.js +1 -0
  332. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BTLZZCiQ.js +3 -0
  333. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BVKew-mH.css +1 -0
  334. package/dist/ui/assets/ContextAttachmentManager-D2epuNnG.js +3 -0
  335. package/dist/ui/assets/CourierPrime-Bold-BuLj_dpw.woff2 +0 -0
  336. package/dist/ui/assets/CourierPrime-BoldItalic-BrK2pUkZ.woff2 +0 -0
  337. package/dist/ui/assets/CourierPrime-Italic-DFUZK5Ey.woff2 +0 -0
  338. package/dist/ui/assets/CourierPrime-Regular-BsKphzVf.woff2 +0 -0
  339. package/dist/ui/assets/DocumentWorkspace-D4rJKXzd.css +1 -0
  340. package/dist/ui/assets/DocumentWorkspace-sVa-3Do6.js +252 -0
  341. package/dist/ui/assets/EntityActivityTimeline-DyqvvhuF.css +1 -0
  342. package/dist/ui/assets/EntityActivityTimeline-Zods27y_.js +1 -0
  343. package/dist/ui/assets/{InitiativesModule-Dhm7M8e7.js → InitiativesModule-D4XseTwW.js} +1 -1
  344. package/dist/ui/assets/Noir_medium-bxQwKGzB.woff2 +0 -0
  345. package/dist/ui/assets/Noir_regular-ojf2kxlG.woff2 +0 -0
  346. package/dist/ui/assets/PlansPage-D5AcbO0L.js +1 -0
  347. package/dist/ui/assets/PlansPage-Dvqsz5zc.css +1 -0
  348. package/dist/ui/assets/RussoOne-Regular-Cu_qq_qC.woff2 +0 -0
  349. package/dist/ui/assets/SpecialElite-Regular-Di6gSvAS.woff2 +0 -0
  350. package/dist/ui/assets/TaskContextUpload-uC5qx4Bv.js +1 -0
  351. package/dist/ui/assets/TaskSettings-CqvPAsTv.js +9 -0
  352. package/dist/ui/assets/{WorkflowsModule-BcS4afRn.js → WorkflowsModule-oOA9bcdK.js} +4 -4
  353. package/dist/ui/assets/documentReferences-DPZuTgHh.js +1 -0
  354. package/dist/ui/assets/index-607WPo_X.js +4 -0
  355. package/dist/ui/assets/index-CYyodXsg.css +1 -0
  356. package/dist/ui/assets/{vendor-icons-BSUaRwF8.js → vendor-icons-B32hGuF2.js} +1 -1
  357. package/dist/ui/fonts/CourierPrime-Bold.ttf +0 -0
  358. package/dist/ui/fonts/CourierPrime-Bold.woff2 +0 -0
  359. package/dist/ui/fonts/CourierPrime-BoldItalic.ttf +0 -0
  360. package/dist/ui/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  361. package/dist/ui/fonts/CourierPrime-Italic.ttf +0 -0
  362. package/dist/ui/fonts/CourierPrime-Italic.woff2 +0 -0
  363. package/dist/ui/fonts/CourierPrime-Regular.ttf +0 -0
  364. package/dist/ui/fonts/CourierPrime-Regular.woff2 +0 -0
  365. package/dist/ui/fonts/Noir_medium.otf +0 -0
  366. package/dist/ui/fonts/Noir_medium.woff2 +0 -0
  367. package/dist/ui/fonts/Noir_regular.otf +0 -0
  368. package/dist/ui/fonts/Noir_regular.woff2 +0 -0
  369. package/dist/ui/fonts/RussoOne-Regular.woff2 +0 -0
  370. package/dist/ui/fonts/SpecialElite-Regular.ttf +0 -0
  371. package/dist/ui/fonts/SpecialElite-Regular.woff2 +0 -0
  372. package/dist/ui/index.html +3 -3
  373. package/dist/ui/product/agent manager.png +0 -0
  374. package/dist/ui/product/agent_manager_02.png +0 -0
  375. package/dist/utils/accountProfileSummaryCache.d.ts +5 -0
  376. package/dist/utils/aiProfileEvents.d.ts +7 -0
  377. package/dist/utils/aiProfileEvents.js +6 -0
  378. package/dist/utils/assignees.d.ts +8 -0
  379. package/dist/utils/assignees.js +10 -1
  380. package/dist/utils/assignees.test.d.ts +1 -0
  381. package/dist/utils/assignees.test.js +25 -0
  382. package/dist/utils/avatarDisplay.d.ts +1 -0
  383. package/dist/utils/avatarDisplay.js +15 -0
  384. package/dist/utils/avatarUpload.d.ts +27 -0
  385. package/dist/utils/avatarUpload.js +132 -0
  386. package/dist/utils/avatarUpload.test.d.ts +1 -0
  387. package/dist/utils/avatarUpload.test.js +40 -0
  388. package/dist/utils/bootstrapTrace.d.ts +7 -0
  389. package/dist/utils/bootstrapTrace.js +29 -0
  390. package/dist/utils/commercialLifecyclePresentation.js +3 -0
  391. package/dist/utils/contextAssetEvents.d.ts +2 -0
  392. package/dist/utils/contextFiles.d.ts +3 -1
  393. package/dist/utils/contextFiles.js +2 -1
  394. package/dist/utils/syncEventDetails.d.ts +3 -0
  395. package/dist/utils/syncEventDetails.js +64 -0
  396. package/dist/utils/syncEventDetails.test.d.ts +1 -0
  397. package/dist/utils/syncEventDetails.test.js +23 -0
  398. package/dist/utils/syncEventPresentation.d.ts +15 -0
  399. package/dist/utils/syncEventPresentation.js +129 -0
  400. package/dist/utils/syncEventPresentation.test.d.ts +1 -0
  401. package/dist/utils/syncEventPresentation.test.js +64 -0
  402. package/dist/utils/syncStatusPresentation.d.ts +72 -0
  403. package/dist/utils/syncStatusPresentation.js +217 -0
  404. package/dist/utils/syncStatusPresentation.test.d.ts +1 -0
  405. package/dist/utils/syncStatusPresentation.test.js +164 -0
  406. package/dist/utils/taskActivity.d.ts +9 -1
  407. package/dist/utils/taskActivity.js +290 -22
  408. package/dist/utils/taskActivity.test.js +141 -8
  409. package/dist/utils/taskSearch.d.ts +22 -0
  410. package/dist/utils/taskSearch.js +87 -0
  411. package/dist/utils/taskSearch.test.d.ts +1 -0
  412. package/dist/utils/taskSearch.test.js +69 -0
  413. package/dist/utils/taskUtils.js +10 -3
  414. package/dist/utils/taskUtils.test.d.ts +1 -0
  415. package/dist/utils/taskUtils.test.js +19 -0
  416. package/dist/utils/workspaceSyncPresentation.d.ts +4 -0
  417. package/dist/utils/workspaceSyncPresentation.js +68 -2
  418. package/dist/utils/workspaceSyncPresentation.test.js +36 -1
  419. package/package.json +3 -2
  420. package/scripts/export-sqlite-to-postgres.mjs +7 -9
  421. package/scripts/run-e2e-realtime.sh +17 -3
  422. package/scripts/run-smoke.sh +17 -5
  423. package/scripts/run-soak.sh +17 -5
  424. package/src/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  425. package/src/resources/templates/workflows/collaborate.yaml +1 -1
  426. package/src/resources/templates/workflows/evaluate.yaml +2 -2
  427. package/src/resources/templates/workflows/plan.yaml +1 -1
  428. package/src/resources/templates/workflows/review.yaml +2 -2
  429. package/dist/ui/assets/AgentsModule-N2MnQBZk.js +0 -1
  430. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BG6P_GVW.js +0 -3
  431. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +0 -1
  432. package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +0 -1
  433. package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +0 -252
  434. package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
  435. package/dist/ui/assets/PlansPage-Cq0zXj3I.js +0 -1
  436. package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
  437. package/dist/ui/assets/TaskSettings-ln0aF7xc.js +0 -9
  438. package/dist/ui/assets/index-C2-OXZV7.css +0 -1
  439. package/dist/ui/assets/index-DcSI5F86.js +0 -6
  440. package/scripts/migrate-planning-model.ts +0 -233
@@ -2,6 +2,7 @@ import type { DatabaseAdapter } from '../storage/databaseAdapter.js';
2
2
  export type AttachmentLinkInput = string | {
3
3
  path: string;
4
4
  fsPath?: string;
5
+ assetId?: string | null;
5
6
  caption?: string;
6
7
  displayName?: string;
7
8
  originalFilename?: string;
@@ -12,4 +13,6 @@ export interface AttachmentLinkServiceDeps {
12
13
  tenantId: string;
13
14
  normalizeWorkspaceId: (workspaceId: string) => string;
14
15
  }
16
+ export type AttachmentLinkTargetType = 'task' | 'initiative' | 'workstream';
17
+ export declare function syncCanonicalAttachmentLinksForTargetService(deps: AttachmentLinkServiceDeps, targetType: AttachmentLinkTargetType, targetId: string, workspaceId: string, attachments: AttachmentLinkInput[]): void;
15
18
  export declare function syncCanonicalTaskAttachmentLinksService(deps: AttachmentLinkServiceDeps, taskId: string, workspaceId: string, attachments: AttachmentLinkInput[]): void;
@@ -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)
@@ -43,6 +63,25 @@ function normalizeAttachmentStorageKey(pathValue, fsPathValue) {
43
63
  }
44
64
  return null;
45
65
  }
66
+ function extractAttachmentAssetId(attachment) {
67
+ const explicitAssetId = typeof attachment === 'string'
68
+ ? ''
69
+ : String(attachment.assetId || '').trim();
70
+ if (explicitAssetId)
71
+ return explicitAssetId;
72
+ const pathValue = typeof attachment === 'string'
73
+ ? attachment.trim()
74
+ : String(attachment.path || '').trim();
75
+ const match = pathValue.match(/^\/api\/taskforce\/documents\/([^/?#]+)\/content(?:[?#].*)?$/);
76
+ if (!match)
77
+ return null;
78
+ try {
79
+ return decodeURIComponent(match[1]).trim() || null;
80
+ }
81
+ catch {
82
+ return match[1].trim() || null;
83
+ }
84
+ }
46
85
  function ensureExternalReferenceAsset(deps, storageKeyRaw, workspaceId, attachment) {
47
86
  const storageKey = String(storageKeyRaw || '').trim();
48
87
  const normalizedWorkspaceId = deps.normalizeWorkspaceId(workspaceId);
@@ -104,44 +143,89 @@ function ensureExternalReferenceAsset(deps, storageKeyRaw, workspaceId, attachme
104
143
  `).run(assetId, deps.tenantId, normalizedWorkspaceId, 'file', logicalName, logicalName, mimeType, 'external', storageKey, urlHash, Buffer.byteLength(storageKey, 'utf8'), null, null, 1, 'clean', null, null, null, timestamp, timestamp);
105
144
  return assetId;
106
145
  }
107
- export function syncCanonicalTaskAttachmentLinksService(deps, taskId, workspaceId, attachments) {
108
- const normalizedTaskId = String(taskId || '').trim();
146
+ export function syncCanonicalAttachmentLinksForTargetService(deps, targetType, targetId, workspaceId, attachments) {
147
+ const normalizedTargetId = String(targetId || '').trim();
109
148
  const normalizedWorkspaceId = deps.normalizeWorkspaceId(workspaceId);
110
- if (!normalizedTaskId)
149
+ if (!normalizedTargetId)
111
150
  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);
151
+ const supportsTargetType = supportsWorkspaceAssetLinkTargetType(deps.db);
152
+ const safeTargetType = targetType === 'initiative' || targetType === 'workstream'
153
+ ? targetType
154
+ : 'task';
155
+ const nowIso = new Date().toISOString();
156
+ if (supportsTargetType) {
157
+ deps.db.prepare(`
158
+ UPDATE workspace_asset_links
159
+ SET deleted_at = ?, updated_at = ?
160
+ WHERE tenant_id = ? AND workspace_id = ? AND target_type = ? AND task_id = ? AND deleted_at IS NULL
161
+ `).run(nowIso, nowIso, deps.tenantId, normalizedWorkspaceId, safeTargetType, normalizedTargetId);
162
+ }
163
+ else {
164
+ if (safeTargetType !== 'task')
165
+ return;
166
+ deps.db.prepare(`
167
+ UPDATE workspace_asset_links
168
+ SET deleted_at = ?, updated_at = ?
169
+ WHERE tenant_id = ? AND workspace_id = ? AND task_id = ? AND deleted_at IS NULL
170
+ `).run(nowIso, nowIso, deps.tenantId, normalizedWorkspaceId, normalizedTargetId);
171
+ }
117
172
  const selectAsset = deps.db.prepare(`
118
173
  SELECT asset_id, kind
119
174
  FROM workspace_assets
120
175
  WHERE tenant_id = ? AND workspace_id = ? AND storage_key = ? AND deleted_at IS NULL
121
176
  LIMIT 1
122
177
  `);
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
178
+ const selectAssetById = deps.db.prepare(`
179
+ SELECT asset_id, kind
180
+ FROM workspace_assets
181
+ WHERE tenant_id = ? AND workspace_id = ? AND asset_id = ? AND deleted_at IS NULL
182
+ LIMIT 1
132
183
  `);
184
+ const upsertLink = supportsTargetType
185
+ ? deps.db.prepare(`
186
+ INSERT INTO workspace_asset_links (
187
+ tenant_id, workspace_id, asset_id, target_type, task_id, role, display_name, sort_order, created_at, updated_at, deleted_at
188
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)
189
+ ON CONFLICT (tenant_id, workspace_id, asset_id, task_id, role) DO UPDATE SET
190
+ target_type = excluded.target_type,
191
+ display_name = excluded.display_name,
192
+ sort_order = excluded.sort_order,
193
+ updated_at = excluded.updated_at,
194
+ deleted_at = NULL
195
+ `)
196
+ : deps.db.prepare(`
197
+ INSERT INTO workspace_asset_links (
198
+ tenant_id, workspace_id, asset_id, task_id, role, display_name, sort_order, created_at, updated_at, deleted_at
199
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)
200
+ ON CONFLICT (tenant_id, workspace_id, asset_id, task_id, role) DO UPDATE SET
201
+ display_name = excluded.display_name,
202
+ sort_order = excluded.sort_order,
203
+ updated_at = excluded.updated_at,
204
+ deleted_at = NULL
205
+ `);
133
206
  attachments.forEach((attachment, index) => {
134
207
  const storageKey = normalizeAttachmentStorageKey(typeof attachment === 'string' ? attachment : attachment.path, typeof attachment === 'string' ? null : attachment.fsPath);
135
- if (!storageKey)
208
+ const attachmentAssetId = extractAttachmentAssetId(attachment);
209
+ if (!storageKey && !attachmentAssetId)
136
210
  return;
137
- const row = selectAsset.get(deps.tenantId, normalizedWorkspaceId, storageKey);
211
+ const row = (storageKey
212
+ ? selectAsset.get(deps.tenantId, normalizedWorkspaceId, storageKey)
213
+ : null)
214
+ || (attachmentAssetId
215
+ ? selectAssetById.get(deps.tenantId, normalizedWorkspaceId, attachmentAssetId)
216
+ : null);
217
+ const isExternalStorageKey = typeof storageKey === 'string' && /^https?:\/\//i.test(storageKey);
218
+ const hasExplicitFileStorageKey = typeof storageKey === 'string'
219
+ && typeof attachment !== 'string'
220
+ && typeof attachment.fsPath === 'string'
221
+ && attachment.fsPath.trim().length > 0;
138
222
  const assetId = String(row?.asset_id || '').trim()
139
- || ((/^https?:\/\//i.test(storageKey) || (typeof attachment !== 'string' && typeof attachment.fsPath === 'string' && attachment.fsPath.trim().length > 0))
223
+ || (isExternalStorageKey || hasExplicitFileStorageKey
140
224
  ? ensureExternalReferenceAsset(deps, storageKey, normalizedWorkspaceId, attachment)
141
225
  : '');
142
226
  if (!assetId)
143
227
  return;
144
- const role = /^https?:\/\//i.test(storageKey)
228
+ const role = isExternalStorageKey
145
229
  ? 'reference'
146
230
  : String(row?.kind || '').trim() === 'image'
147
231
  ? 'image'
@@ -150,8 +234,11 @@ export function syncCanonicalTaskAttachmentLinksService(deps, taskId, workspaceI
150
234
  ? null
151
235
  : (typeof attachment === 'string'
152
236
  ? null
153
- : buildAttachmentDisplayName(attachment, /^https?:\/\//i.test(storageKey) ? null : path.basename(storageKey)));
237
+ : buildAttachmentDisplayName(attachment, isExternalStorageKey || !storageKey ? null : path.basename(storageKey)));
154
238
  const timestamp = typeof attachment === 'string' ? new Date().toISOString() : (attachment.timestamp || new Date().toISOString());
155
- upsertLink.run(deps.tenantId, normalizedWorkspaceId, assetId, normalizedTaskId, role, displayName, index, timestamp, timestamp);
239
+ upsertLink.run(deps.tenantId, normalizedWorkspaceId, assetId, safeTargetType, normalizedTargetId, role, displayName, index, timestamp, timestamp);
156
240
  });
157
241
  }
242
+ export function syncCanonicalTaskAttachmentLinksService(deps, taskId, workspaceId, attachments) {
243
+ syncCanonicalAttachmentLinksForTargetService(deps, 'task', taskId, workspaceId, attachments);
244
+ }
@@ -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
  }