@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
@@ -6,9 +6,20 @@
6
6
  *
7
7
  * Routes registered here:
8
8
  * GET /api/taskforce/auth/runtime-config
9
+ * GET /api/taskforce/auth/providers
10
+ * GET /api/taskforce/auth/oauth/:provider/start
11
+ * GET /api/taskforce/auth/oauth/:provider/callback
12
+ * POST /api/taskforce/auth/oauth/apple/callback
13
+ * GET /api/taskforce/account/login-methods
14
+ * DELETE /api/taskforce/account/login-methods/:provider
15
+ * POST /api/taskforce/account/login-methods/password
9
16
  * GET /api/taskforce/auth/session
10
17
  * GET /api/taskforce/auth/admin-session
11
18
  * POST /api/taskforce/auth/profile
19
+ * POST /api/taskforce/auth/profile/avatar/init
20
+ * POST /api/taskforce/auth/profile/avatar/upload
21
+ * POST /api/taskforce/auth/profile/avatar/finalize
22
+ * POST /api/taskforce/auth/profile/avatar/discard
12
23
  * POST /api/taskforce/auth/login
13
24
  * POST /api/taskforce/auth/register
14
25
  * POST /api/taskforce/auth/verify-email/request
@@ -26,6 +37,9 @@
26
37
  * GET /api/taskforce/account/plan-preview
27
38
  * GET /api/taskforce/auth/workspace-members
28
39
  * GET /api/taskforce/workspace/assignee-options
40
+ * POST /api/taskforce/workspace/ai-profiles/avatar
41
+ * POST /api/taskforce/workspace/ai-profiles/avatar/upload
42
+ * POST /api/taskforce/workspace/ai-profiles/surface-type
29
43
  * GET /api/taskforce/admin/users
30
44
  * POST /api/taskforce/admin/users/invite
31
45
  * PATCH /api/taskforce/admin/users/:id/role
@@ -38,17 +52,22 @@
38
52
  import * as fs from 'fs';
39
53
  import * as path from 'path';
40
54
  import { randomUUID } from 'crypto';
41
- import { parseJsonBody } from './primitives.js';
55
+ import { parseJsonBody, parseFormBody } from './primitives.js';
56
+ import { buildGoogleAuthUrl, exchangeGoogleCode } from '../auth/providers/google.js';
57
+ import { buildAppleAuthUrl, exchangeAppleCode } from '../auth/providers/apple.js';
58
+ import { buildGithubAuthUrl, exchangeGithubCode } from '../auth/providers/github.js';
42
59
  import { hasValidSession, createClearSessionCookie, createSessionTokenCookie, resolveRequestAccess, resolveSessionCookieAttributesForRequest, } from '../auth.js';
43
60
  import { sendEmail } from '../email.js';
44
61
  import { resolveDeploymentConfig } from '../../config/deployment.js';
45
62
  import { resolveRuntimeModeContract } from '../../shared/runtimeContract.js';
46
- import { requestHeader, resolveErrorStatusCode } from './shared.js';
63
+ import { getRouteTimingNow, logSlowBootstrapRoute, requestBootstrapTraceId, requestHeader, resolveErrorStatusCode, serializeJsonWithTiming, setServerTimingHeader } from './shared.js';
47
64
  import { buildAuthFlowBehavior, buildSiteBehavior, getRouteEmailConfig, resolvePublicAuthBaseUrl, resolveRuntimeConfigBaseUrl, shouldExposeDevAuthTokens } from './authSupport.js';
48
65
  import { normalizeTaskDataRelativePath } from '../../storage/objectStorageClient.js';
66
+ import { clearAiProfileAvatarAssets, saveAiProfileAvatarAssets } from '../aiProfileAvatarAssets.js';
67
+ const AI_PROFILE_AVATAR_UPLOAD_MAX_JSON_BYTES = 16 * 1024 * 1024;
49
68
  // === SECTION: Registration ===
50
69
  export function registerAuthRoutes(deps) {
51
- const { addRoute, core, context, requestWorkspaceId, requestUserId, ensureAppAccess, ensureAuthenticatedUser, ensureWorkspaceAdminRole, ensureCloudPlanData, requireCloudRuntime, auditAuthAction, resolveAdminRequestActor, resolveEffectiveObjectStorageConfig, getObjectStorageClient, } = deps;
70
+ const { addRoute, core, context, requestWorkspaceId, requestUserId, resolveAuthenticatedUserId, ensureAppAccess, ensureAuthenticatedUser, ensureWorkspaceAdminRole, ensureCloudPlanData, requireCloudRuntime, auditAuthAction, resolveAdminRequestActor, resolveEffectiveObjectStorageConfig, getObjectStorageClient, workspaceAssetStore, ensureWithinDir, markAssetDeleted, appendStorageTelemetry, } = deps;
52
71
  const normalizeTeamWorkspaceRole = (value) => {
53
72
  const role = String(value || '').trim().toLowerCase();
54
73
  if (role === 'owner' || role === 'admin' || role === 'member' || role === 'read-only')
@@ -61,6 +80,56 @@ export function registerAuthRoutes(deps) {
61
80
  const stateAllows = state === 'active' || state === 'trialing' || state === 'grace';
62
81
  return teamAllowed && stateAllows ? 'team' : 'personal';
63
82
  };
83
+ const serializeAiProfilePublicRecord = (profile) => ({
84
+ id: profile.id,
85
+ workspaceId: profile.workspaceId,
86
+ name: profile.name,
87
+ username: profile.username,
88
+ icon: profile.icon,
89
+ color: profile.color,
90
+ avatarUrl: profile.avatarUrl ?? null,
91
+ avatarSourceUrl: profile.avatarSourceUrl ?? null,
92
+ avatarRevision: profile.avatarRevision ?? 0,
93
+ avatarUpdatedAt: profile.avatarUpdatedAt ?? null,
94
+ description: profile.description ?? null,
95
+ role: profile.role ?? null,
96
+ provider: profile.provider ?? null,
97
+ model: profile.model ?? null,
98
+ surfaceType: profile.surfaceType ?? null,
99
+ seatScope: profile.seatScope ?? null,
100
+ archivedAt: profile.archivedAt ?? null,
101
+ archivedBy: profile.archivedBy ?? null,
102
+ archivedReason: profile.archivedReason ?? null,
103
+ mergedIntoProfileId: profile.mergedIntoProfileId ?? null,
104
+ rosterStateUpdatedAt: profile.rosterStateUpdatedAt ?? null,
105
+ createdAt: profile.createdAt,
106
+ updatedAt: profile.updatedAt,
107
+ lastActiveAt: profile.lastActiveAt ?? null
108
+ });
109
+ const parseAiAvatarDataUrlPayload = (value, fallbackOriginalName) => {
110
+ if (!value || typeof value !== 'object')
111
+ return null;
112
+ const record = value;
113
+ const dataUrl = String(record.dataUrl || record.file || '').trim();
114
+ const dataMatch = dataUrl.match(/^data:([^;]+);base64,(.+)$/);
115
+ if (!dataMatch)
116
+ return null;
117
+ return {
118
+ mimeType: String(record.mimeType || dataMatch[1] || 'application/octet-stream').trim().toLowerCase(),
119
+ originalName: typeof record.originalName === 'string' && record.originalName.trim().length > 0
120
+ ? record.originalName.trim()
121
+ : fallbackOriginalName,
122
+ buffer: Buffer.from(dataMatch[2], 'base64')
123
+ };
124
+ };
125
+ const aiProfileAvatarAssetDeps = {
126
+ core,
127
+ workspaceAssetStore,
128
+ getObjectStorageClient,
129
+ ensureWithinDir,
130
+ markAssetDeleted,
131
+ appendStorageTelemetry,
132
+ };
64
133
  const resolveBillingConflictCodeForUser = (userIdRaw, gateRaw) => {
65
134
  const userId = String(userIdRaw || '').trim();
66
135
  const gate = String(gateRaw || '').trim().toLowerCase();
@@ -103,7 +172,7 @@ export function registerAuthRoutes(deps) {
103
172
  return null;
104
173
  };
105
174
  const buildAccountProfileSummary = (req) => {
106
- const userId = requestUserId(req);
175
+ const userId = resolveAuthenticatedUserId(req);
107
176
  const workspaceId = requestWorkspaceId(req);
108
177
  const access = core.getAccountAccessStatus(userId);
109
178
  const resolvedAccess = core.resolveUserAccess(userId, workspaceId, 'member');
@@ -125,8 +194,12 @@ export function registerAuthRoutes(deps) {
125
194
  catch {
126
195
  billingRow = undefined;
127
196
  }
128
- const planId = String(access.entitlement?.planId || billingRow?.tier || '').trim() || null;
197
+ const allowBillingPlanFallback = access.gate === 'plan_selection_required' || access.gate === 'checkout_pending';
198
+ const planId = String(access.entitlement?.planId
199
+ || (allowBillingPlanFallback ? billingRow?.tier : '')
200
+ || '').trim() || null;
129
201
  const planVersionId = String(access.entitlement?.planVersionId || '').trim() || null;
202
+ const entitlementState = access.entitlement?.state || null;
130
203
  let planRow;
131
204
  if (planId) {
132
205
  try {
@@ -151,6 +224,7 @@ export function registerAuthRoutes(deps) {
151
224
  teamPlanMode = 'personal';
152
225
  }
153
226
  }
227
+ const aiProfileSeatUsage = core.countAiProfileUsage(workspaceId);
154
228
  const billingConflictCode = resolveBillingConflictCodeForUser(userId, access.gate);
155
229
  return {
156
230
  stripeStatus: billingRow?.stripe_status || null,
@@ -161,7 +235,7 @@ export function registerAuthRoutes(deps) {
161
235
  trialEnd: billingRow?.trial_end || null,
162
236
  currentPeriodEnd: billingRow?.current_period_end || null,
163
237
  effectiveUntil: billingRow?.effective_until || null,
164
- entitlementState: access.entitlement?.state || billingRow?.entitlement_state || null,
238
+ entitlementState,
165
239
  planVersionId,
166
240
  planId,
167
241
  planName: planRow?.display_name ? String(planRow.display_name) : planId,
@@ -173,7 +247,8 @@ export function registerAuthRoutes(deps) {
173
247
  workspaceRole,
174
248
  canManageWorkspace,
175
249
  teamPlanMode,
176
- teamManagementAllowed: canManageWorkspace && teamPlanMode === 'team'
250
+ teamManagementAllowed: canManageWorkspace && teamPlanMode === 'team',
251
+ aiProfileSeatUsage
177
252
  };
178
253
  };
179
254
  // === SECTION: Email delivery helpers ===
@@ -187,7 +262,7 @@ export function registerAuthRoutes(deps) {
187
262
  const access = resolvedWorkspaceId
188
263
  ? core.resolveUserAccess(userId, resolvedWorkspaceId, fallbackRole)
189
264
  : null;
190
- return createSessionTokenCookie(`pwd:${userId}`, authConfig, {
265
+ return createSessionTokenCookie(`usr:${userId}`, authConfig, {
191
266
  role: (access?.role || fallbackRole),
192
267
  workspaceId: access?.workspaceId || '',
193
268
  userId,
@@ -241,6 +316,29 @@ export function registerAuthRoutes(deps) {
241
316
  await deleteStoredAvatarObject(draft.storageKey);
242
317
  }
243
318
  };
319
+ const parseBinaryBody = async (req, maxBytes) => {
320
+ const safeMaxBytes = Number.isFinite(maxBytes) && maxBytes > 0
321
+ ? Math.floor(maxBytes)
322
+ : MAX_AVATAR_UPLOAD_BYTES;
323
+ return new Promise((resolve, reject) => {
324
+ const chunks = [];
325
+ let receivedBytes = 0;
326
+ req.on('data', (chunk) => {
327
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
328
+ receivedBytes += buffer.length;
329
+ if (receivedBytes > safeMaxBytes) {
330
+ req.pause();
331
+ reject(Object.assign(new Error(`Payload too large (max ${safeMaxBytes} bytes)`), { statusCode: 413 }));
332
+ return;
333
+ }
334
+ chunks.push(buffer);
335
+ });
336
+ req.on('end', () => {
337
+ resolve(Buffer.concat(chunks));
338
+ });
339
+ req.on('error', reject);
340
+ });
341
+ };
244
342
  const logAuthLink = (kind, email, token, req) => {
245
343
  const base = resolvePublicAuthBaseUrl(req);
246
344
  const urlPath = kind === 'verify-email'
@@ -339,11 +437,650 @@ export function registerAuthRoutes(deps) {
339
437
  });
340
438
  res.end(`window.__TASKFORCE_SITE_CONFIG__ = ${payload};`);
341
439
  });
440
+ // GET /api/taskforce/auth/providers - Public: list enabled OAuth login providers (no secrets)
441
+ addRoute('GET', '/api/taskforce/auth/providers', async (_req, res) => {
442
+ const providers = core.getGlobalSettings().oauthProviders || {};
443
+ const enabled = [];
444
+ if (providers.google?.enabled === true)
445
+ enabled.push('google');
446
+ if (providers.github?.enabled === true)
447
+ enabled.push('github');
448
+ if (providers.apple?.enabled === true)
449
+ enabled.push('apple');
450
+ res.writeHead(200, { 'Content-Type': 'application/json' });
451
+ res.end(JSON.stringify({ providers: enabled }));
452
+ });
453
+ // === SECTION: OAuth 2.0 / OpenID Connect ===
454
+ const OAUTH_RETURN_TO_PATTERN = /^\/[A-Za-z0-9_\-./?=&%]*$/;
455
+ /**
456
+ * Validates a `return_to` value and defaults to '/' on failure.
457
+ * Prevents open-redirect attacks by only allowing same-site relative paths.
458
+ */
459
+ const sanitizeReturnTo = (raw) => {
460
+ const value = String(raw || '').trim();
461
+ if (!value || !OAUTH_RETURN_TO_PATTERN.test(value))
462
+ return '/';
463
+ return value;
464
+ };
465
+ /**
466
+ * Provider-agnostic session cookie builder for OAuth-authenticated users.
467
+ * Resolves workspace/role and mints the signed session token.
468
+ */
469
+ const buildOAuthSessionCookie = (req, authConfig, userId, identityId) => {
470
+ const resolvedWorkspaceId = core.resolvePreferredUserWorkspaceId(userId, {
471
+ preferredWorkspaceId: null,
472
+ persistedWorkspaceId: null
473
+ });
474
+ const fallbackRole = String(authConfig.defaultRole || 'member').trim() || 'member';
475
+ const access = resolvedWorkspaceId
476
+ ? core.resolveUserAccess(userId, resolvedWorkspaceId, fallbackRole)
477
+ : null;
478
+ return createSessionTokenCookie(`usr:${userId}`, authConfig, {
479
+ role: (access?.role || fallbackRole),
480
+ workspaceId: access?.workspaceId || '',
481
+ userId,
482
+ identityId,
483
+ ...resolveSessionCookieAttributesForRequest(req, authConfig),
484
+ });
485
+ };
486
+ /**
487
+ * Handles the post-exchange OAuth profile: find or create the user,
488
+ * link the provider identity if needed, and return { userId, identityId }.
489
+ *
490
+ * Resolution order:
491
+ * 1. Existing identity matching (provider, providerSubject) → login.
492
+ * 2. Existing user with same email (provider must have verified it) → link + login.
493
+ * 3. Self-registration allowed → register new user → login.
494
+ * 4. Otherwise → reject.
495
+ */
496
+ const resolveOAuthUser = (profile) => {
497
+ // Case 1: known identity
498
+ const found = core.findUserByOAuthProvider(profile.provider, profile.providerSubject);
499
+ if (found) {
500
+ // Update last_login_at snapshot
501
+ try {
502
+ const db = core.getDatabaseAdapter();
503
+ const tenantId = core.getTenantId();
504
+ db.prepare(`
505
+ UPDATE user_auth_identities
506
+ SET last_login_at = ?, avatar_url_snapshot = COALESCE(?, avatar_url_snapshot),
507
+ updated_at = ?
508
+ WHERE tenant_id = ? AND id = ?
509
+ `).run(new Date().toISOString(), profile.avatarUrl ?? null, new Date().toISOString(), tenantId, found.identityId);
510
+ }
511
+ catch { /* non-fatal */ }
512
+ return { userId: found.userId, identityId: found.identityId, isNew: false };
513
+ }
514
+ // Case 2: email match (provider must have verified it)
515
+ if (profile.email && profile.emailVerified) {
516
+ const byEmail = core.findUserByEmail(profile.email);
517
+ if (byEmail) {
518
+ try {
519
+ const identityId = core.linkOAuthProviderToUser({
520
+ userId: byEmail.userId,
521
+ provider: profile.provider,
522
+ providerSubject: profile.providerSubject,
523
+ providerEmail: profile.email,
524
+ providerEmailVerified: profile.emailVerified,
525
+ displayNameSnapshot: profile.displayName,
526
+ avatarUrlSnapshot: profile.avatarUrl,
527
+ });
528
+ return { userId: byEmail.userId, identityId, isNew: false };
529
+ }
530
+ catch (e) {
531
+ if (e?.code !== 'IDENTITY_ALREADY_EXISTS')
532
+ throw e;
533
+ // Race condition: already linked — re-query
534
+ const refound = core.findUserByOAuthProvider(profile.provider, profile.providerSubject);
535
+ if (refound)
536
+ return { ...refound, isNew: false };
537
+ throw e;
538
+ }
539
+ }
540
+ }
541
+ // Case 3: self-registration
542
+ const authBehavior = buildAuthFlowBehavior(core);
543
+ if (!authBehavior.allowSelfRegistration) {
544
+ throw Object.assign(new Error('Self-registration is currently disabled.'), {
545
+ code: 'REGISTRATION_DISABLED',
546
+ statusCode: 403
547
+ });
548
+ }
549
+ const created = core.registerOAuthUser({
550
+ email: profile.email,
551
+ emailVerified: profile.emailVerified,
552
+ provider: profile.provider,
553
+ providerSubject: profile.providerSubject,
554
+ displayName: profile.displayName,
555
+ avatarUrl: profile.avatarUrl,
556
+ betaAccess: authBehavior.defaultNewUserBetaAccess,
557
+ });
558
+ return { userId: created.userId, identityId: created.identityId, isNew: true };
559
+ };
560
+ // GET /api/taskforce/auth/oauth/:provider/start
561
+ // Initiates the OAuth authorisation redirect for Google, GitHub, or Apple.
562
+ addRoute('GET', '/api/taskforce/auth/oauth/:provider/start', async (req, res, params) => {
563
+ if (!requireCloudRuntime(res, 'OAuth Login'))
564
+ return;
565
+ const authConfig = context.authConfig;
566
+ if (!authConfig?.enabled) {
567
+ res.writeHead(400, { 'Content-Type': 'application/json' });
568
+ res.end(JSON.stringify({ success: false, error: 'Auth is not enabled.' }));
569
+ return;
570
+ }
571
+ const provider = String(params.provider || '').toLowerCase();
572
+ if (provider !== 'google' && provider !== 'github' && provider !== 'apple') {
573
+ res.writeHead(404, { 'Content-Type': 'application/json' });
574
+ res.end(JSON.stringify({ success: false, error: 'Unknown OAuth provider.' }));
575
+ return;
576
+ }
577
+ const oauthProviders = core.getGlobalSettings().oauthProviders || {};
578
+ const providerConfig = oauthProviders[provider];
579
+ if (!providerConfig?.enabled) {
580
+ res.writeHead(400, { 'Content-Type': 'application/json' });
581
+ res.end(JSON.stringify({ success: false, error: `${provider} login is not enabled.` }));
582
+ return;
583
+ }
584
+ const queryString = (req.url || '').split('?')[1] || '';
585
+ const queryParams = new URLSearchParams(queryString);
586
+ const returnTo = sanitizeReturnTo(queryParams.get('return_to'));
587
+ const clientIp = requestHeader(req, 'x-forwarded-for', '') || requestHeader(req, 'x-real-ip', '') || null;
588
+ const rawInviteToken = String(queryParams.get('invite_token') || '').trim();
589
+ const rawFlowHint = String(queryParams.get('flow') || '').trim().toLowerCase();
590
+ // Determine flow mode: link > invite_claim > login
591
+ let flowMode = 'login';
592
+ let validatedInviteToken = null;
593
+ let linkUserId = null;
594
+ if (rawFlowHint === 'link') {
595
+ // Provider-link flow: require an authenticated session
596
+ if (!authConfig || !hasValidSession(req, authConfig)) {
597
+ res.writeHead(401, { 'Content-Type': 'application/json' });
598
+ res.end(JSON.stringify({ success: false, error: 'Sign in required to link a provider.', code: 'AUTH_REQUIRED' }));
599
+ return;
600
+ }
601
+ const access = resolveRequestAccess(req, authConfig);
602
+ linkUserId = access?.userId || null;
603
+ if (!linkUserId) {
604
+ res.writeHead(401, { 'Content-Type': 'application/json' });
605
+ res.end(JSON.stringify({ success: false, error: 'Unable to resolve authenticated user.', code: 'AUTH_REQUIRED' }));
606
+ return;
607
+ }
608
+ flowMode = 'link';
609
+ }
610
+ else if (rawInviteToken) {
611
+ // Invite-claim flow
612
+ const inviteDetails = core.inspectInviteAcceptance(rawInviteToken);
613
+ if (inviteDetails.state !== 'valid') {
614
+ res.writeHead(400, { 'Content-Type': 'application/json' });
615
+ res.end(JSON.stringify({ success: false, error: 'Invite token is invalid or expired.', code: 'INVITE_TOKEN_INVALID' }));
616
+ return;
617
+ }
618
+ if (inviteDetails.inviteeState !== 'pending_setup') {
619
+ res.writeHead(400, { 'Content-Type': 'application/json' });
620
+ res.end(JSON.stringify({ success: false, error: 'This invite has already been claimed.', code: 'INVITE_ALREADY_CLAIMED' }));
621
+ return;
622
+ }
623
+ flowMode = 'invite_claim';
624
+ validatedInviteToken = rawInviteToken;
625
+ }
626
+ const { state, codeChallenge, nonce } = core.createOAuthLoginState({
627
+ provider,
628
+ flowMode,
629
+ userId: linkUserId,
630
+ returnTo,
631
+ inviteToken: validatedInviteToken,
632
+ createdIp: clientIp,
633
+ });
634
+ const baseUrl = resolvePublicAuthBaseUrl(req);
635
+ const redirectUri = `${baseUrl}/api/taskforce/auth/oauth/${provider}/callback`;
636
+ let authUrl;
637
+ if (provider === 'google') {
638
+ const cfg = providerConfig;
639
+ if (!cfg.clientId) {
640
+ res.writeHead(500, { 'Content-Type': 'application/json' });
641
+ res.end(JSON.stringify({ success: false, error: 'Google OAuth is not fully configured.' }));
642
+ return;
643
+ }
644
+ authUrl = buildGoogleAuthUrl({
645
+ clientId: cfg.clientId,
646
+ redirectUri,
647
+ state,
648
+ codeChallenge,
649
+ });
650
+ }
651
+ else if (provider === 'github') {
652
+ const cfg = providerConfig;
653
+ if (!cfg.clientId) {
654
+ res.writeHead(500, { 'Content-Type': 'application/json' });
655
+ res.end(JSON.stringify({ success: false, error: 'GitHub OAuth is not fully configured.' }));
656
+ return;
657
+ }
658
+ authUrl = buildGithubAuthUrl({
659
+ clientId: cfg.clientId,
660
+ redirectUri,
661
+ state,
662
+ codeChallenge,
663
+ });
664
+ }
665
+ else {
666
+ // Apple
667
+ const cfg = providerConfig;
668
+ if (!cfg.servicesId) {
669
+ res.writeHead(500, { 'Content-Type': 'application/json' });
670
+ res.end(JSON.stringify({ success: false, error: 'Apple Sign In is not fully configured.' }));
671
+ return;
672
+ }
673
+ authUrl = buildAppleAuthUrl({
674
+ servicesId: cfg.servicesId,
675
+ redirectUri,
676
+ state,
677
+ nonce,
678
+ });
679
+ }
680
+ res.writeHead(302, { Location: authUrl });
681
+ res.end();
682
+ });
683
+ // GET /api/taskforce/auth/oauth/:provider/callback
684
+ // Handles the OAuth callback for Google and GitHub (GET redirect with `code` + `state`).
685
+ addRoute('GET', '/api/taskforce/auth/oauth/:provider/callback', async (req, res, params) => {
686
+ if (!requireCloudRuntime(res, 'OAuth Login'))
687
+ return;
688
+ const authConfig = context.authConfig;
689
+ if (!authConfig?.enabled) {
690
+ res.writeHead(400, { 'Content-Type': 'application/json' });
691
+ res.end(JSON.stringify({ success: false, error: 'Auth is not enabled.' }));
692
+ return;
693
+ }
694
+ const provider = String(params.provider || '').toLowerCase();
695
+ if (provider !== 'google' && provider !== 'github') {
696
+ res.writeHead(404, { 'Content-Type': 'application/json' });
697
+ res.end(JSON.stringify({ success: false, error: 'Unknown OAuth provider.' }));
698
+ return;
699
+ }
700
+ const queryString = (req.url || '').split('?')[1] || '';
701
+ const queryParams = new URLSearchParams(queryString);
702
+ const stateParam = String(queryParams.get('state') || '').trim();
703
+ const codeParam = String(queryParams.get('code') || '').trim();
704
+ const errorParam = String(queryParams.get('error') || '').trim();
705
+ const baseUrl = resolvePublicAuthBaseUrl(req);
706
+ const redirectUri = `${baseUrl}/api/taskforce/auth/oauth/${provider}/callback`;
707
+ if (errorParam) {
708
+ auditAuthAction(req, 'auth-oauth-callback-denied', { provider, error: errorParam });
709
+ res.writeHead(302, { Location: `/login?error=oauth_denied&provider=${encodeURIComponent(provider)}` });
710
+ res.end();
711
+ return;
712
+ }
713
+ if (!stateParam || !codeParam) {
714
+ res.writeHead(302, { Location: `/login?error=oauth_invalid&provider=${encodeURIComponent(provider)}` });
715
+ res.end();
716
+ return;
717
+ }
718
+ // Consume state (single-use, validates expiry)
719
+ const stateRecord = core.consumeOAuthLoginState(stateParam);
720
+ if (!stateRecord) {
721
+ auditAuthAction(req, 'auth-oauth-state-invalid', { provider, stateProvided: true });
722
+ res.writeHead(302, { Location: `/login?error=oauth_state_invalid&provider=${encodeURIComponent(provider)}` });
723
+ res.end();
724
+ return;
725
+ }
726
+ if (stateRecord.provider !== provider) {
727
+ auditAuthAction(req, 'auth-oauth-state-provider-mismatch', { provider, stateProvider: stateRecord.provider });
728
+ res.writeHead(302, { Location: `/login?error=oauth_state_invalid&provider=${encodeURIComponent(provider)}` });
729
+ res.end();
730
+ return;
731
+ }
732
+ const codeVerifier = stateRecord.pkceVerifier || '';
733
+ const returnTo = sanitizeReturnTo(stateRecord.returnTo);
734
+ const oauthProviders = core.getGlobalSettings().oauthProviders || {};
735
+ try {
736
+ let profile;
737
+ if (provider === 'google') {
738
+ const cfg = (oauthProviders.google || {});
739
+ if (!cfg.clientId || !cfg.clientSecret) {
740
+ throw Object.assign(new Error('Google OAuth is not configured.'), { statusCode: 500 });
741
+ }
742
+ profile = await exchangeGoogleCode({
743
+ code: codeParam,
744
+ codeVerifier,
745
+ redirectUri,
746
+ clientId: cfg.clientId,
747
+ clientSecret: cfg.clientSecret,
748
+ });
749
+ }
750
+ else {
751
+ const cfg = (oauthProviders.github || {});
752
+ if (!cfg.clientId || !cfg.clientSecret) {
753
+ throw Object.assign(new Error('GitHub OAuth is not configured.'), { statusCode: 500 });
754
+ }
755
+ profile = await exchangeGithubCode({
756
+ code: codeParam,
757
+ codeVerifier,
758
+ redirectUri,
759
+ clientId: cfg.clientId,
760
+ clientSecret: cfg.clientSecret,
761
+ });
762
+ }
763
+ let userId;
764
+ let identityId;
765
+ let isNew = false;
766
+ if (stateRecord.flowMode === 'link') {
767
+ if (!stateRecord.userId) {
768
+ throw Object.assign(new Error('User ID missing from OAuth link state.'), { code: 'AUTH_REQUIRED', statusCode: 401 });
769
+ }
770
+ identityId = core.linkOAuthProviderToUser({
771
+ userId: stateRecord.userId,
772
+ provider: profile.provider,
773
+ providerSubject: profile.providerSubject,
774
+ providerEmail: profile.email,
775
+ providerEmailVerified: profile.emailVerified,
776
+ displayNameSnapshot: profile.displayName,
777
+ avatarUrlSnapshot: profile.avatarUrl,
778
+ });
779
+ userId = stateRecord.userId;
780
+ isNew = false;
781
+ }
782
+ else if (stateRecord.flowMode === 'invite_claim') {
783
+ if (!stateRecord.inviteToken) {
784
+ throw Object.assign(new Error('Invite token missing from OAuth state.'), { code: 'INVITE_TOKEN_MISSING', statusCode: 400 });
785
+ }
786
+ const claimed = core.claimInviteWithOAuthProvider({
787
+ token: stateRecord.inviteToken,
788
+ provider: profile.provider,
789
+ providerSubject: profile.providerSubject,
790
+ providerEmail: profile.email,
791
+ providerEmailVerified: profile.emailVerified,
792
+ displayNameSnapshot: profile.displayName,
793
+ avatarUrlSnapshot: profile.avatarUrl,
794
+ });
795
+ userId = claimed.userId;
796
+ identityId = claimed.identityId;
797
+ isNew = true;
798
+ }
799
+ else {
800
+ ({ userId, identityId, isNew } = resolveOAuthUser(profile));
801
+ }
802
+ const cookie = buildOAuthSessionCookie(req, authConfig, userId, identityId);
803
+ res.setHeader('Set-Cookie', cookie);
804
+ auditAuthAction(req, 'auth-oauth-login-success', {
805
+ provider,
806
+ userId,
807
+ isNew,
808
+ flowMode: stateRecord.flowMode,
809
+ email: profile.email || null,
810
+ });
811
+ res.writeHead(302, { Location: returnTo });
812
+ res.end();
813
+ }
814
+ catch (err) {
815
+ const code = err?.code || 'OAUTH_ERROR';
816
+ auditAuthAction(req, 'auth-oauth-callback-error', { provider, code, message: String(err?.message || '') });
817
+ if (code === 'REGISTRATION_DISABLED') {
818
+ res.writeHead(302, { Location: `/login?error=registration_disabled&provider=${encodeURIComponent(provider)}` });
819
+ }
820
+ else if (code === 'IDENTITY_ALREADY_EXISTS') {
821
+ res.writeHead(302, { Location: `${returnTo}?error=provider_already_linked&provider=${encodeURIComponent(provider)}` });
822
+ }
823
+ else if (code === 'INVITE_TOKEN_EXPIRED' || code === 'INVITE_TOKEN_REVOKED' || code === 'INVITE_TOKEN_USED' || code === 'INVITE_TOKEN_INVALID') {
824
+ res.writeHead(302, { Location: `/login?error=invite_invalid&provider=${encodeURIComponent(provider)}` });
825
+ }
826
+ else {
827
+ res.writeHead(302, { Location: `/login?error=oauth_failed&provider=${encodeURIComponent(provider)}` });
828
+ }
829
+ res.end();
830
+ }
831
+ });
832
+ // POST /api/taskforce/auth/oauth/apple/callback
833
+ // Apple Sign In sends `code`, `state`, `id_token` and optional `user` JSON via form_post.
834
+ addRoute('POST', '/api/taskforce/auth/oauth/apple/callback', async (req, res) => {
835
+ if (!requireCloudRuntime(res, 'Apple Sign In'))
836
+ return;
837
+ const authConfig = context.authConfig;
838
+ if (!authConfig?.enabled) {
839
+ res.writeHead(400, { 'Content-Type': 'application/json' });
840
+ res.end(JSON.stringify({ success: false, error: 'Auth is not enabled.' }));
841
+ return;
842
+ }
843
+ let formData;
844
+ try {
845
+ formData = await parseFormBody(req);
846
+ }
847
+ catch {
848
+ res.writeHead(400, { 'Content-Type': 'application/json' });
849
+ res.end(JSON.stringify({ success: false, error: 'Invalid form body.' }));
850
+ return;
851
+ }
852
+ const stateParam = String(formData.state || '').trim();
853
+ const codeParam = String(formData.code || '').trim();
854
+ const errorParam = String(formData.error || '').trim();
855
+ const userJson = formData.user ? String(formData.user).trim() : null;
856
+ if (errorParam) {
857
+ auditAuthAction(req, 'auth-oauth-callback-denied', { provider: 'apple', error: errorParam });
858
+ res.writeHead(302, { Location: '/login?error=oauth_denied&provider=apple' });
859
+ res.end();
860
+ return;
861
+ }
862
+ if (!stateParam || !codeParam) {
863
+ res.writeHead(302, { Location: '/login?error=oauth_invalid&provider=apple' });
864
+ res.end();
865
+ return;
866
+ }
867
+ const stateRecord = core.consumeOAuthLoginState(stateParam);
868
+ if (!stateRecord || stateRecord.provider !== 'apple') {
869
+ auditAuthAction(req, 'auth-oauth-state-invalid', { provider: 'apple', stateProvided: true });
870
+ res.writeHead(302, { Location: '/login?error=oauth_state_invalid&provider=apple' });
871
+ res.end();
872
+ return;
873
+ }
874
+ const returnTo = sanitizeReturnTo(stateRecord.returnTo);
875
+ const oauthProviders = core.getGlobalSettings().oauthProviders || {};
876
+ const appleCfg = (oauthProviders.apple || {});
877
+ if (!appleCfg.servicesId || !appleCfg.teamId || !appleCfg.keyId || !appleCfg.privateKeyPem) {
878
+ auditAuthAction(req, 'auth-oauth-callback-error', { provider: 'apple', code: 'APPLE_NOT_CONFIGURED' });
879
+ res.writeHead(302, { Location: '/login?error=oauth_failed&provider=apple' });
880
+ res.end();
881
+ return;
882
+ }
883
+ const baseUrl = resolvePublicAuthBaseUrl(req);
884
+ const redirectUri = `${baseUrl}/api/taskforce/auth/oauth/apple/callback`;
885
+ try {
886
+ const profile = await exchangeAppleCode({
887
+ code: codeParam,
888
+ redirectUri,
889
+ servicesId: appleCfg.servicesId,
890
+ teamId: appleCfg.teamId,
891
+ keyId: appleCfg.keyId,
892
+ privateKeyPem: appleCfg.privateKeyPem,
893
+ userJson,
894
+ });
895
+ let userId;
896
+ let identityId;
897
+ let isNew = false;
898
+ if (stateRecord.flowMode === 'link') {
899
+ if (!stateRecord.userId) {
900
+ throw Object.assign(new Error('User ID missing from OAuth link state.'), { code: 'AUTH_REQUIRED', statusCode: 401 });
901
+ }
902
+ identityId = core.linkOAuthProviderToUser({
903
+ userId: stateRecord.userId,
904
+ provider: 'apple',
905
+ providerSubject: profile.providerSubject,
906
+ providerEmail: profile.email,
907
+ providerEmailVerified: profile.emailVerified,
908
+ displayNameSnapshot: profile.displayName,
909
+ avatarUrlSnapshot: profile.avatarUrl,
910
+ });
911
+ userId = stateRecord.userId;
912
+ isNew = false;
913
+ }
914
+ else if (stateRecord.flowMode === 'invite_claim') {
915
+ if (!stateRecord.inviteToken) {
916
+ throw Object.assign(new Error('Invite token missing from OAuth state.'), { code: 'INVITE_TOKEN_MISSING', statusCode: 400 });
917
+ }
918
+ const claimed = core.claimInviteWithOAuthProvider({
919
+ token: stateRecord.inviteToken,
920
+ provider: 'apple',
921
+ providerSubject: profile.providerSubject,
922
+ providerEmail: profile.email,
923
+ providerEmailVerified: profile.emailVerified,
924
+ displayNameSnapshot: profile.displayName,
925
+ avatarUrlSnapshot: profile.avatarUrl,
926
+ });
927
+ userId = claimed.userId;
928
+ identityId = claimed.identityId;
929
+ isNew = true;
930
+ }
931
+ else {
932
+ ({ userId, identityId, isNew } = resolveOAuthUser(profile));
933
+ }
934
+ const cookie = buildOAuthSessionCookie(req, authConfig, userId, identityId);
935
+ res.setHeader('Set-Cookie', cookie);
936
+ auditAuthAction(req, 'auth-oauth-login-success', {
937
+ provider: 'apple',
938
+ userId,
939
+ isNew,
940
+ flowMode: stateRecord.flowMode,
941
+ email: profile.email || null,
942
+ });
943
+ res.writeHead(302, { Location: returnTo });
944
+ res.end();
945
+ }
946
+ catch (err) {
947
+ const code = err?.code || 'OAUTH_ERROR';
948
+ auditAuthAction(req, 'auth-oauth-callback-error', { provider: 'apple', code, message: String(err?.message || '') });
949
+ if (code === 'REGISTRATION_DISABLED') {
950
+ res.writeHead(302, { Location: '/login?error=registration_disabled&provider=apple' });
951
+ }
952
+ else if (code === 'IDENTITY_ALREADY_EXISTS') {
953
+ res.writeHead(302, { Location: `${returnTo}?error=provider_already_linked&provider=apple` });
954
+ }
955
+ else if (code === 'INVITE_TOKEN_EXPIRED' || code === 'INVITE_TOKEN_REVOKED' || code === 'INVITE_TOKEN_USED' || code === 'INVITE_TOKEN_INVALID') {
956
+ res.writeHead(302, { Location: '/login?error=invite_invalid&provider=apple' });
957
+ }
958
+ else {
959
+ res.writeHead(302, { Location: '/login?error=oauth_failed&provider=apple' });
960
+ }
961
+ res.end();
962
+ }
963
+ });
964
+ // GET /api/taskforce/account/login-methods
965
+ // Returns the list of active login identities for the authenticated user.
966
+ addRoute('GET', '/api/taskforce/account/login-methods', async (req, res) => {
967
+ if (!requireCloudRuntime(res, 'Login methods'))
968
+ return;
969
+ const authConfig = context.authConfig;
970
+ if (!authConfig?.enabled || !hasValidSession(req, authConfig)) {
971
+ res.writeHead(401, { 'Content-Type': 'application/json' });
972
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
973
+ return;
974
+ }
975
+ const access = resolveRequestAccess(req, authConfig);
976
+ const userId = access?.userId || '';
977
+ if (!userId) {
978
+ res.writeHead(401, { 'Content-Type': 'application/json' });
979
+ res.end(JSON.stringify({ success: false, error: 'Unable to resolve user.' }));
980
+ return;
981
+ }
982
+ const identities = core.listLoginMethodsForUser(userId);
983
+ res.writeHead(200, { 'Content-Type': 'application/json' });
984
+ res.end(JSON.stringify({
985
+ success: true,
986
+ methods: identities.map(id => ({
987
+ provider: id.provider,
988
+ providerEmail: id.providerEmail || null,
989
+ displayNameSnapshot: id.displayNameSnapshot || null,
990
+ avatarUrlSnapshot: id.avatarUrlSnapshot || null,
991
+ linkedAt: id.linkedAt || id.createdAt,
992
+ lastLoginAt: id.lastLoginAt || null,
993
+ }))
994
+ }));
995
+ });
996
+ // DELETE /api/taskforce/account/login-methods/:provider
997
+ // Unlinks a provider identity from the authenticated user.
998
+ // Rejects with 409 if it would be the last active login method.
999
+ addRoute('DELETE', '/api/taskforce/account/login-methods/:provider', async (req, res, params) => {
1000
+ if (!requireCloudRuntime(res, 'Login methods'))
1001
+ return;
1002
+ const authConfig = context.authConfig;
1003
+ if (!authConfig?.enabled || !hasValidSession(req, authConfig)) {
1004
+ res.writeHead(401, { 'Content-Type': 'application/json' });
1005
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
1006
+ return;
1007
+ }
1008
+ const access = resolveRequestAccess(req, authConfig);
1009
+ const userId = access?.userId || '';
1010
+ if (!userId) {
1011
+ res.writeHead(401, { 'Content-Type': 'application/json' });
1012
+ res.end(JSON.stringify({ success: false, error: 'Unable to resolve user.' }));
1013
+ return;
1014
+ }
1015
+ const provider = String(params.provider || '').toLowerCase();
1016
+ if (!provider) {
1017
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1018
+ res.end(JSON.stringify({ success: false, error: 'Provider is required.' }));
1019
+ return;
1020
+ }
1021
+ try {
1022
+ core.unlinkLoginMethod(userId, provider);
1023
+ auditAuthAction(req, 'account-login-method-unlinked', { userId, provider });
1024
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1025
+ res.end(JSON.stringify({ success: true }));
1026
+ }
1027
+ catch (err) {
1028
+ const code = err?.code || 'UNLINK_ERROR';
1029
+ const status = code === 'LAST_IDENTITY_REQUIRED' ? 409 : resolveErrorStatusCode(err);
1030
+ res.writeHead(status, { 'Content-Type': 'application/json' });
1031
+ res.end(JSON.stringify({ success: false, error: err?.message || 'Unlink failed.', code }));
1032
+ }
1033
+ });
1034
+ // POST /api/taskforce/account/login-methods/password
1035
+ // Adds (or replaces) a password identity for the authenticated user.
1036
+ // Social-only accounts use this to add a password login method.
1037
+ addRoute('POST', '/api/taskforce/account/login-methods/password', async (req, res) => {
1038
+ if (!requireCloudRuntime(res, 'Login methods'))
1039
+ return;
1040
+ const authConfig = context.authConfig;
1041
+ if (!authConfig?.enabled || !hasValidSession(req, authConfig)) {
1042
+ res.writeHead(401, { 'Content-Type': 'application/json' });
1043
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
1044
+ return;
1045
+ }
1046
+ const access = resolveRequestAccess(req, authConfig);
1047
+ const userId = access?.userId || '';
1048
+ if (!userId) {
1049
+ res.writeHead(401, { 'Content-Type': 'application/json' });
1050
+ res.end(JSON.stringify({ success: false, error: 'Unable to resolve user.' }));
1051
+ return;
1052
+ }
1053
+ let body;
1054
+ try {
1055
+ body = await parseJsonBody(req);
1056
+ }
1057
+ catch {
1058
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1059
+ res.end(JSON.stringify({ success: false, error: 'Invalid JSON body.' }));
1060
+ return;
1061
+ }
1062
+ const password = String(body?.password || '');
1063
+ try {
1064
+ core.addPasswordToAccount(userId, password);
1065
+ auditAuthAction(req, 'account-password-added', { userId });
1066
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1067
+ res.end(JSON.stringify({ success: true }));
1068
+ }
1069
+ catch (err) {
1070
+ const code = err?.code || 'ADD_PASSWORD_ERROR';
1071
+ const status = resolveErrorStatusCode(err);
1072
+ res.writeHead(status, { 'Content-Type': 'application/json' });
1073
+ res.end(JSON.stringify({ success: false, error: err?.message || 'Failed to add password.', code }));
1074
+ }
1075
+ });
342
1076
  // GET /api/taskforce/auth/session - Return current auth/session state
343
1077
  addRoute('GET', '/api/taskforce/auth/session', async (req, res) => {
1078
+ const routeStartedAt = getRouteTimingNow();
1079
+ const traceId = requestBootstrapTraceId(req);
344
1080
  const authConfig = context.authConfig;
345
1081
  const access = authConfig ? resolveRequestAccess(req, authConfig) : null;
346
1082
  const authenticated = Boolean(authConfig && hasValidSession(req, authConfig));
1083
+ const authResolvedAt = getRouteTimingNow();
347
1084
  let role = access?.role || authConfig?.defaultRole || 'member';
348
1085
  let workspaceId = String(access?.workspaceId || authConfig?.defaultWorkspaceId || '').trim();
349
1086
  let userId = access?.userId || 'anonymous';
@@ -351,11 +1088,16 @@ export function registerAuthRoutes(deps) {
351
1088
  let displayName = null;
352
1089
  let avatarUrl = null;
353
1090
  let betaAccess = true;
1091
+ let betaAccessResolvedAt = authResolvedAt;
1092
+ let userAccessResolvedAt = authResolvedAt;
1093
+ let identityResolvedAt = authResolvedAt;
354
1094
  if (authenticated && access?.userId) {
355
1095
  // Keep the session endpoint fast on reload; avatar draft cleanup is best-effort housekeeping.
356
1096
  void cleanupStaleAvatarDrafts(access.userId).catch(() => { });
357
1097
  betaAccess = core.getUserBetaAccess(access.userId);
1098
+ betaAccessResolvedAt = getRouteTimingNow();
358
1099
  const resolved = core.resolveUserAccess(access.userId, workspaceId, role);
1100
+ userAccessResolvedAt = getRouteTimingNow();
359
1101
  if (resolved) {
360
1102
  role = resolved.role;
361
1103
  workspaceId = resolved.workspaceId;
@@ -370,10 +1112,12 @@ export function registerAuthRoutes(deps) {
370
1112
  email = identity?.email || null;
371
1113
  displayName = identity?.displayName || null;
372
1114
  avatarUrl = identity?.avatarUrl || null;
1115
+ identityResolvedAt = getRouteTimingNow();
373
1116
  }
374
- res.writeHead(200, { 'Content-Type': 'application/json' });
375
- res.end(JSON.stringify({
1117
+ const payload = {
376
1118
  authenticated,
1119
+ authRequiredForApi: Boolean(authConfig?.enabled && authConfig.requiredForApi),
1120
+ runtimeMode: authConfig?.runtimeMode || 'local',
377
1121
  userId,
378
1122
  workspaceId,
379
1123
  role,
@@ -381,7 +1125,34 @@ export function registerAuthRoutes(deps) {
381
1125
  displayName,
382
1126
  avatarUrl,
383
1127
  betaAccess
384
- }));
1128
+ };
1129
+ const serialized = serializeJsonWithTiming(payload, identityResolvedAt);
1130
+ const totalDurationMs = getRouteTimingNow() - routeStartedAt;
1131
+ setServerTimingHeader(res, [
1132
+ { name: 'auth-session', durationMs: totalDurationMs },
1133
+ { name: 'auth-session-cookie', durationMs: authResolvedAt - routeStartedAt },
1134
+ { name: 'auth-session-beta-access', durationMs: betaAccessResolvedAt - authResolvedAt },
1135
+ { name: 'auth-session-user-access', durationMs: userAccessResolvedAt - betaAccessResolvedAt },
1136
+ { name: 'auth-session-identity', durationMs: identityResolvedAt - userAccessResolvedAt },
1137
+ { name: 'auth-session-json', durationMs: serialized.durationMs },
1138
+ ]);
1139
+ logSlowBootstrapRoute({
1140
+ route: '/api/taskforce/auth/session',
1141
+ workspaceId: workspaceId || access?.workspaceId || authConfig?.defaultWorkspaceId || 'default',
1142
+ traceId,
1143
+ durationMs: totalDurationMs,
1144
+ details: {
1145
+ authenticated,
1146
+ payloadBytes: serialized.byteLength,
1147
+ cookieMs: authResolvedAt - routeStartedAt,
1148
+ betaAccessMs: betaAccessResolvedAt - authResolvedAt,
1149
+ userAccessMs: userAccessResolvedAt - betaAccessResolvedAt,
1150
+ identityMs: identityResolvedAt - userAccessResolvedAt,
1151
+ serializeMs: serialized.durationMs,
1152
+ }
1153
+ });
1154
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1155
+ res.end(serialized.body);
385
1156
  });
386
1157
  // GET /api/taskforce/auth/admin-session - Return global admin auth/session state
387
1158
  addRoute('GET', '/api/taskforce/auth/admin-session', async (req, res) => {
@@ -537,6 +1308,63 @@ export function registerAuthRoutes(deps) {
537
1308
  path: `/api/taskforce/context/${encodeURIComponent(relativePath)}`
538
1309
  }));
539
1310
  });
1311
+ addRoute('POST', '/api/taskforce/auth/profile/avatar/upload', async (req, res) => {
1312
+ if (!requireCloudRuntime(res, 'Authentication'))
1313
+ return;
1314
+ const authConfig = context.authConfig;
1315
+ if (!authConfig) {
1316
+ res.writeHead(500, { 'Content-Type': 'application/json' });
1317
+ res.end(JSON.stringify({ success: false, error: 'Auth context unavailable.' }));
1318
+ return;
1319
+ }
1320
+ const access = resolveRequestAccess(req, authConfig);
1321
+ const userId = String(access.userId || '').trim();
1322
+ if (!access.authenticated || !userId || userId === 'anonymous') {
1323
+ res.writeHead(401, { 'Content-Type': 'application/json' });
1324
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
1325
+ return;
1326
+ }
1327
+ const draftId = String(requestHeader(req, 'x-taskforce-avatar-draft-id', '') || '').trim();
1328
+ if (!draftId) {
1329
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1330
+ res.end(JSON.stringify({ success: false, error: 'Avatar draft id is required.', code: 'AVATAR_DRAFT_REQUIRED' }));
1331
+ return;
1332
+ }
1333
+ await cleanupStaleAvatarDrafts(userId);
1334
+ const draft = core.getUserAvatarUploadDraft(userId, draftId);
1335
+ if (!draft || draft.discardedAt) {
1336
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1337
+ res.end(JSON.stringify({ success: false, error: 'Avatar draft is invalid or expired.', code: 'AVATAR_DRAFT_INVALID' }));
1338
+ return;
1339
+ }
1340
+ const objectStorageClient = getObjectStorageClient();
1341
+ const effectiveObjectStorage = resolveEffectiveObjectStorageConfig();
1342
+ if (!objectStorageClient || effectiveObjectStorage.provider !== 'r2') {
1343
+ res.writeHead(501, { 'Content-Type': 'application/json' });
1344
+ res.end(JSON.stringify({ success: false, error: 'Avatar uploads require object storage.' }));
1345
+ return;
1346
+ }
1347
+ try {
1348
+ const body = await parseBinaryBody(req, MAX_AVATAR_UPLOAD_BYTES);
1349
+ if (body.length === 0) {
1350
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1351
+ res.end(JSON.stringify({ success: false, error: 'Avatar upload body is empty.', code: 'AVATAR_UPLOAD_EMPTY' }));
1352
+ return;
1353
+ }
1354
+ await objectStorageClient.putObject(draft.storageKey, body, draft.mimeType || 'application/octet-stream');
1355
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1356
+ res.end(JSON.stringify({ success: true, draftId, relativePath: draft.storageKey }));
1357
+ }
1358
+ catch (error) {
1359
+ const statusCode = resolveErrorStatusCode(error, 400);
1360
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
1361
+ res.end(JSON.stringify({
1362
+ success: false,
1363
+ error: String(error?.message || 'Failed to upload avatar.'),
1364
+ code: error?.code
1365
+ }));
1366
+ }
1367
+ });
540
1368
  addRoute('POST', '/api/taskforce/auth/profile/avatar/finalize', async (req, res) => {
541
1369
  if (!requireCloudRuntime(res, 'Authentication'))
542
1370
  return;
@@ -708,10 +1536,11 @@ export function registerAuthRoutes(deps) {
708
1536
  };
709
1537
  }
710
1538
  }
711
- const cookie = createSessionTokenCookie(`pwd:${access.userId}`, authConfig, {
1539
+ const cookie = createSessionTokenCookie(`usr:${access.userId}`, authConfig, {
712
1540
  role: access.role,
713
1541
  workspaceId: access.workspaceId,
714
1542
  userId: access.userId,
1543
+ identityId: authResult.access?.identityId,
715
1544
  ...resolveSessionCookieAttributesForRequest(req, authConfig)
716
1545
  });
717
1546
  auditAuthAction(req, 'auth-login-success', {
@@ -1016,7 +1845,7 @@ export function registerAuthRoutes(deps) {
1016
1845
  const password = String(body?.password || '');
1017
1846
  if (!token || !password) {
1018
1847
  res.writeHead(400, { 'Content-Type': 'application/json' });
1019
- res.end(JSON.stringify({ success: false, error: 'Token and password are required.' }));
1848
+ res.end(JSON.stringify({ success: false, error: 'Token and password are required.', code: 'TOKEN_PASSWORD_REQUIRED' }));
1020
1849
  return;
1021
1850
  }
1022
1851
  try {
@@ -1041,7 +1870,7 @@ export function registerAuthRoutes(deps) {
1041
1870
  statusCode: resolveErrorStatusCode(error, 400)
1042
1871
  });
1043
1872
  res.writeHead(resolveErrorStatusCode(error, 400), { 'Content-Type': 'application/json' });
1044
- res.end(JSON.stringify({ success: false, error: String(error?.message || 'Unable to reset password') }));
1873
+ res.end(JSON.stringify({ success: false, error: String(error?.message || 'Unable to reset password'), code: error?.code }));
1045
1874
  }
1046
1875
  });
1047
1876
  // === SECTION: Invite flows ===
@@ -1065,6 +1894,18 @@ export function registerAuthRoutes(deps) {
1065
1894
  res.end(JSON.stringify({ success: false, state: details.state, error: 'Invalid or expired invite token.' }));
1066
1895
  return;
1067
1896
  }
1897
+ // Augment availableMethods with enabled OAuth providers for pending_setup invitees
1898
+ const oauthProviders = core.getGlobalSettings().oauthProviders || {};
1899
+ const enabledOAuthProviders = [];
1900
+ if (oauthProviders.google?.enabled)
1901
+ enabledOAuthProviders.push('google');
1902
+ if (oauthProviders.github?.enabled)
1903
+ enabledOAuthProviders.push('github');
1904
+ if (oauthProviders.apple?.enabled)
1905
+ enabledOAuthProviders.push('apple');
1906
+ const augmentedMethods = details.inviteeState === 'pending_setup'
1907
+ ? [...(details.availableMethods || []), ...enabledOAuthProviders.filter(p => !(details.availableMethods || []).includes(p))]
1908
+ : (details.availableMethods || []);
1068
1909
  auditAuthAction(req, 'auth-invite-opened', {
1069
1910
  userId: details.userId,
1070
1911
  email: details.email,
@@ -1078,6 +1919,8 @@ export function registerAuthRoutes(deps) {
1078
1919
  state: details.state,
1079
1920
  email: details.email,
1080
1921
  workspaceId: details.workspaceId,
1922
+ inviteeState: details.inviteeState,
1923
+ availableMethods: augmentedMethods,
1081
1924
  inviteeKind: details.inviteeKind,
1082
1925
  passwordRequired: details.passwordRequired === true
1083
1926
  }));
@@ -1107,10 +1950,11 @@ export function registerAuthRoutes(deps) {
1107
1950
  }
1108
1951
  try {
1109
1952
  const accepted = core.acceptInviteWithToken({ token, password });
1110
- const cookie = createSessionTokenCookie(`pwd:${accepted.userId}`, authConfig, {
1953
+ const cookie = createSessionTokenCookie(`usr:${accepted.userId}`, authConfig, {
1111
1954
  role: accepted.role,
1112
1955
  workspaceId: accepted.workspaceId,
1113
1956
  userId: accepted.userId,
1957
+ identityId: accepted.identityId,
1114
1958
  ...resolveSessionCookieAttributesForRequest(req, authConfig)
1115
1959
  });
1116
1960
  res.setHeader('Set-Cookie', cookie);
@@ -1163,11 +2007,12 @@ export function registerAuthRoutes(deps) {
1163
2007
  }
1164
2008
  try {
1165
2009
  const accepted = core.joinInviteWithAuthenticatedUser({ token, userId: access.userId });
1166
- const sessionId = String(access.sessionId || `pwd:${accepted.userId}`).trim() || `pwd:${accepted.userId}`;
2010
+ const sessionId = String(access.sessionId || `usr:${accepted.userId}`).trim() || `usr:${accepted.userId}`;
1167
2011
  const cookie = createSessionTokenCookie(sessionId, authConfig, {
1168
2012
  role: accepted.role,
1169
2013
  workspaceId: accepted.workspaceId,
1170
2014
  userId: accepted.userId,
2015
+ identityId: access.identityId || undefined,
1171
2016
  ...resolveSessionCookieAttributesForRequest(req, authConfig)
1172
2017
  });
1173
2018
  res.setHeader('Set-Cookie', cookie);
@@ -1236,7 +2081,7 @@ export function registerAuthRoutes(deps) {
1236
2081
  return;
1237
2082
  }
1238
2083
  try {
1239
- const userId = requestUserId(req);
2084
+ const userId = resolveAuthenticatedUserId(req);
1240
2085
  const payload = core.getAccountAccessStatus(userId);
1241
2086
  res.writeHead(200, { 'Content-Type': 'application/json' });
1242
2087
  res.end(JSON.stringify(payload));
@@ -1359,6 +2204,31 @@ export function registerAuthRoutes(deps) {
1359
2204
  res.end(JSON.stringify({ error: String(error?.message || 'Unable to build plan preview'), code: error?.code, details: error?.details }));
1360
2205
  }
1361
2206
  });
2207
+ // GET /api/taskforce/account/ai-profile-seat-usage - Read cloud-authoritative AI seat usage for the current workspace.
2208
+ addRoute('GET', '/api/taskforce/account/ai-profile-seat-usage', async (req, res) => {
2209
+ if (!ensureCloudPlanData(req, res))
2210
+ return;
2211
+ if (!ensureAuthenticatedUser(req)) {
2212
+ res.writeHead(401, { 'Content-Type': 'application/json' });
2213
+ res.end(JSON.stringify({ error: 'Authentication required.' }));
2214
+ return;
2215
+ }
2216
+ try {
2217
+ const wid = requestWorkspaceId(req);
2218
+ const aiProfileSeatUsage = core.countAiProfileUsage(wid);
2219
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2220
+ res.end(JSON.stringify({ aiProfileSeatUsage }));
2221
+ }
2222
+ catch (error) {
2223
+ const statusCode = resolveErrorStatusCode(error, 400);
2224
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
2225
+ res.end(JSON.stringify({
2226
+ error: String(error?.message || 'Unable to build AI profile seat usage'),
2227
+ code: error?.code,
2228
+ details: error?.details
2229
+ }));
2230
+ }
2231
+ });
1362
2232
  // === SECTION: Workspace team management (workspace admin) ===
1363
2233
  // GET /api/taskforce/auth/workspace-members - Auth-backed workspace member list for local runtime identity hydration.
1364
2234
  addRoute('GET', '/api/taskforce/auth/workspace-members', async (req, res) => {
@@ -1375,6 +2245,7 @@ export function registerAuthRoutes(deps) {
1375
2245
  res.end(JSON.stringify({ users }));
1376
2246
  });
1377
2247
  // GET /api/taskforce/workspace/assignee-options - Read active workspace humans and registered AI collaborators for task assignee selection.
2248
+ // Use ?kind=agent for Agent Manager roster loads that do not need human workspace members.
1378
2249
  addRoute('GET', '/api/taskforce/workspace/assignee-options', async (req, res) => {
1379
2250
  if (!ensureAppAccess(req, res))
1380
2251
  return;
@@ -1383,45 +2254,191 @@ export function registerAuthRoutes(deps) {
1383
2254
  res.end(JSON.stringify({ error: 'Authentication required.' }));
1384
2255
  return;
1385
2256
  }
2257
+ const startedAt = getRouteTimingNow();
1386
2258
  const wid = requestWorkspaceId(req);
1387
- const includeProfileToken = context.authConfig?.enabled !== true || ensureWorkspaceAdminRole(req);
1388
- const workspaceUsers = core
1389
- .listWorkspaceUsers(wid)
1390
- .filter((user) => String(user.status || '').trim().toLowerCase() === 'active' && user.disabled !== true)
1391
- .map((user) => ({
1392
- value: user.userId,
1393
- label: String(user.displayName || user.email || user.userId).trim() || user.userId,
1394
- icon: 'User',
1395
- color: '#22c55e',
1396
- kind: 'member',
1397
- userId: user.userId,
1398
- email: user.email,
1399
- displayName: user.displayName ?? null,
1400
- status: user.status,
1401
- disabled: user.disabled === true
1402
- }));
2259
+ const traceId = requestBootstrapTraceId(req);
2260
+ const reqUrl = new URL(req.url || '/api/taskforce/workspace/assignee-options', 'http://localhost');
2261
+ const kindFilter = String(reqUrl.searchParams.get('kind') || '').trim().toLowerCase();
2262
+ const agentsOnly = kindFilter === 'agent' || kindFilter === 'agents' || kindFilter === 'ai-profile' || kindFilter === 'ai-profiles';
2263
+ const usersStartedAt = getRouteTimingNow();
2264
+ const activeWorkspaceUsers = agentsOnly
2265
+ ? []
2266
+ : core.listWorkspaceUsers(wid)
2267
+ .filter((user) => String(user.status || '').trim().toLowerCase() === 'active' && user.disabled !== true);
2268
+ const usersLoadedAt = getRouteTimingNow();
2269
+ const workspaceUsers = agentsOnly
2270
+ ? []
2271
+ : activeWorkspaceUsers.map((user) => ({
2272
+ value: user.userId,
2273
+ label: String(user.displayName || user.email || user.userId).trim() || user.userId,
2274
+ icon: 'User',
2275
+ color: '#22c55e',
2276
+ kind: 'member',
2277
+ userId: user.userId,
2278
+ email: user.email,
2279
+ displayName: user.displayName ?? null,
2280
+ avatarUrl: user.avatarUrl ?? null,
2281
+ status: user.status,
2282
+ disabled: user.disabled === true
2283
+ }));
2284
+ const usersMappedAt = getRouteTimingNow();
2285
+ const aiProfileDiagnostics = { metadataBackfillCount: 0 };
2286
+ const aiProfilesStartedAt = getRouteTimingNow();
1403
2287
  const aiProfiles = core
1404
- .listAiProfiles(wid)
2288
+ .listAiProfiles(wid, { diagnostics: aiProfileDiagnostics })
1405
2289
  .map((profile) => ({
1406
2290
  value: profile.id,
1407
2291
  label: profile.name,
1408
- ...(includeProfileToken ? { profileToken: profile.profileToken } : {}),
1409
2292
  username: profile.username,
1410
2293
  icon: profile.icon,
1411
2294
  color: profile.color,
2295
+ avatarUrl: profile.avatarUrl ?? null,
2296
+ avatarSourceUrl: profile.avatarSourceUrl ?? null,
2297
+ avatarRevision: profile.avatarRevision ?? 0,
2298
+ avatarUpdatedAt: profile.avatarUpdatedAt ?? null,
1412
2299
  kind: 'agent',
1413
2300
  description: profile.description ?? null,
1414
2301
  role: profile.role ?? null,
1415
2302
  provider: profile.provider ?? null,
1416
2303
  model: profile.model ?? null,
1417
2304
  surfaceType: profile.surfaceType ?? null,
2305
+ seatScope: profile.seatScope ?? null,
2306
+ archivedAt: profile.archivedAt ?? null,
1418
2307
  createdAt: profile.createdAt,
1419
2308
  updatedAt: profile.updatedAt,
1420
2309
  lastActiveAt: profile.lastActiveAt ?? null
1421
2310
  }));
2311
+ const aiProfilesLoadedAt = getRouteTimingNow();
2312
+ const seatUsageStartedAt = getRouteTimingNow();
2313
+ const aiProfileSeatUsage = core.countAiProfileUsage(wid);
2314
+ const seatUsageLoadedAt = getRouteTimingNow();
1422
2315
  const assignees = [...workspaceUsers, ...aiProfiles];
2316
+ const serializeStartedAt = getRouteTimingNow();
2317
+ const serialized = serializeJsonWithTiming({ assignees, aiProfileSeatUsage }, serializeStartedAt);
2318
+ const totalDurationMs = Math.max(0, getRouteTimingNow() - startedAt);
2319
+ setServerTimingHeader(res, [
2320
+ { name: 'assignee-options', durationMs: totalDurationMs },
2321
+ { name: 'assignee-options-users', durationMs: usersLoadedAt - usersStartedAt },
2322
+ { name: 'assignee-options-users-map', durationMs: usersMappedAt - usersLoadedAt },
2323
+ { name: 'assignee-options-ai-profiles', durationMs: aiProfilesLoadedAt - aiProfilesStartedAt },
2324
+ { name: 'assignee-options-seat-usage', durationMs: seatUsageLoadedAt - seatUsageStartedAt },
2325
+ { name: 'assignee-options-json', durationMs: serialized.durationMs },
2326
+ ]);
2327
+ logSlowBootstrapRoute({
2328
+ route: '/api/taskforce/workspace/assignee-options',
2329
+ workspaceId: wid,
2330
+ traceId,
2331
+ durationMs: totalDurationMs,
2332
+ details: {
2333
+ humanAssigneeCount: workspaceUsers.length,
2334
+ workspaceUserCount: activeWorkspaceUsers.length,
2335
+ aiProfileCount: aiProfiles.length,
2336
+ agentsOnly,
2337
+ metadataBackfillCount: aiProfileDiagnostics.metadataBackfillCount,
2338
+ responseBytes: serialized.byteLength,
2339
+ listWorkspaceUsersMs: Math.max(0, usersLoadedAt - usersStartedAt),
2340
+ workspaceUsersMapMs: Math.max(0, usersMappedAt - usersLoadedAt),
2341
+ listAiProfilesMs: Math.max(0, aiProfilesLoadedAt - aiProfilesStartedAt),
2342
+ countAiProfileUsageMs: Math.max(0, seatUsageLoadedAt - seatUsageStartedAt),
2343
+ jsonSerializeMs: serialized.durationMs,
2344
+ },
2345
+ });
1423
2346
  res.writeHead(200, { 'Content-Type': 'application/json' });
1424
- res.end(JSON.stringify({ assignees }));
2347
+ res.end(serialized.body);
2348
+ });
2349
+ // POST /api/taskforce/workspace/ai-profiles/avatar - Clear an AI profile avatar image.
2350
+ addRoute('POST', '/api/taskforce/workspace/ai-profiles/avatar', async (req, res) => {
2351
+ if (!ensureWorkspaceAdminRole(req)) {
2352
+ res.writeHead(403, { 'Content-Type': 'application/json' });
2353
+ res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
2354
+ return;
2355
+ }
2356
+ const body = await parseJsonBody(req);
2357
+ const wid = requestWorkspaceId(req);
2358
+ const profileId = String(body?.profileId || '').trim();
2359
+ const avatarUrl = body?.avatarUrl === null ? null : String(body?.avatarUrl || '').trim();
2360
+ const avatarSourceUrl = body?.avatarSourceUrl === undefined
2361
+ ? undefined
2362
+ : body?.avatarSourceUrl === null
2363
+ ? null
2364
+ : String(body?.avatarSourceUrl || '').trim();
2365
+ if (!profileId) {
2366
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2367
+ res.end(JSON.stringify({ error: 'profileId is required.' }));
2368
+ return;
2369
+ }
2370
+ try {
2371
+ const shouldClearAvatar = avatarUrl === null && (avatarSourceUrl === null || avatarSourceUrl === undefined);
2372
+ if (!shouldClearAvatar) {
2373
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2374
+ res.end(JSON.stringify({
2375
+ error: 'AI profile avatar updates must use the stable avatar upload endpoint.',
2376
+ code: 'AI_PROFILE_AVATAR_STABLE_UPLOAD_REQUIRED'
2377
+ }));
2378
+ return;
2379
+ }
2380
+ const result = await clearAiProfileAvatarAssets(aiProfileAvatarAssetDeps, { workspaceId: wid, profileId });
2381
+ const profile = result.profile;
2382
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2383
+ res.end(JSON.stringify({ success: true, profile: serializeAiProfilePublicRecord(profile) }));
2384
+ }
2385
+ catch (err) {
2386
+ const status = err?.statusCode ?? 500;
2387
+ res.writeHead(status, { 'Content-Type': 'application/json' });
2388
+ res.end(JSON.stringify({ error: err?.message ?? 'Failed to update AI profile avatar.' }));
2389
+ }
2390
+ });
2391
+ // POST /api/taskforce/workspace/ai-profiles/avatar/upload - Upload stable AI profile avatar slots.
2392
+ addRoute('POST', '/api/taskforce/workspace/ai-profiles/avatar/upload', async (req, res) => {
2393
+ if (!ensureWorkspaceAdminRole(req)) {
2394
+ res.writeHead(403, { 'Content-Type': 'application/json' });
2395
+ res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
2396
+ return;
2397
+ }
2398
+ const body = await parseJsonBody(req, AI_PROFILE_AVATAR_UPLOAD_MAX_JSON_BYTES);
2399
+ const wid = requestWorkspaceId(req);
2400
+ const profileId = String(body?.profileId || '').trim();
2401
+ if (!profileId) {
2402
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2403
+ res.end(JSON.stringify({ error: 'profileId is required.' }));
2404
+ return;
2405
+ }
2406
+ const display = parseAiAvatarDataUrlPayload(body?.displayImage || body?.display, 'display-avatar');
2407
+ if (!display) {
2408
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2409
+ res.end(JSON.stringify({ error: 'displayImage data URL is required.' }));
2410
+ return;
2411
+ }
2412
+ const source = parseAiAvatarDataUrlPayload(body?.sourceImage || body?.source, 'source-avatar');
2413
+ try {
2414
+ const result = await saveAiProfileAvatarAssets(aiProfileAvatarAssetDeps, {
2415
+ workspaceId: wid,
2416
+ profileId,
2417
+ display,
2418
+ source,
2419
+ actorId: resolveAuthenticatedUserId(req)
2420
+ });
2421
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2422
+ res.end(JSON.stringify({
2423
+ success: true,
2424
+ profile: serializeAiProfilePublicRecord(result.profile),
2425
+ assets: {
2426
+ display: {
2427
+ assetId: result.displayAsset.assetId,
2428
+ storageKey: result.displayStorageKey,
2429
+ },
2430
+ source: {
2431
+ assetId: result.sourceAsset.assetId,
2432
+ storageKey: result.sourceStorageKey,
2433
+ }
2434
+ }
2435
+ }));
2436
+ }
2437
+ catch (err) {
2438
+ const status = err?.statusCode ?? 500;
2439
+ res.writeHead(status, { 'Content-Type': 'application/json' });
2440
+ res.end(JSON.stringify({ error: err?.message ?? 'Failed to upload AI profile avatar.' }));
2441
+ }
1425
2442
  });
1426
2443
  // POST /api/taskforce/workspace/ai-profiles/merge - Merge a duplicate AI profile into another, reassigning all references.
1427
2444
  addRoute('POST', '/api/taskforce/workspace/ai-profiles/merge', async (req, res) => {
@@ -1442,7 +2459,7 @@ export function registerAuthRoutes(deps) {
1442
2459
  try {
1443
2460
  const profile = core.mergeAiProfiles({ workspaceId: wid, keepId, mergeId });
1444
2461
  res.writeHead(200, { 'Content-Type': 'application/json' });
1445
- res.end(JSON.stringify({ profile }));
2462
+ res.end(JSON.stringify({ profile: serializeAiProfilePublicRecord(profile) }));
1446
2463
  }
1447
2464
  catch (err) {
1448
2465
  const status = err?.statusCode ?? 500;
@@ -1450,6 +2467,80 @@ export function registerAuthRoutes(deps) {
1450
2467
  res.end(JSON.stringify({ error: err?.message ?? 'Merge failed.' }));
1451
2468
  }
1452
2469
  });
2470
+ // POST /api/taskforce/workspace/ai-profiles/surface-type - Manually re-categorize an active AI profile.
2471
+ addRoute('POST', '/api/taskforce/workspace/ai-profiles/surface-type', async (req, res) => {
2472
+ if (!ensureWorkspaceAdminRole(req)) {
2473
+ res.writeHead(403, { 'Content-Type': 'application/json' });
2474
+ res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
2475
+ return;
2476
+ }
2477
+ const body = await parseJsonBody(req);
2478
+ const wid = requestWorkspaceId(req);
2479
+ const profileId = String(body.profileId || '').trim();
2480
+ const hasSurfaceType = Boolean(body && Object.prototype.hasOwnProperty.call(body, 'surfaceType'));
2481
+ if (!profileId) {
2482
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2483
+ res.end(JSON.stringify({ error: 'profileId is required.' }));
2484
+ return;
2485
+ }
2486
+ if (!hasSurfaceType) {
2487
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2488
+ res.end(JSON.stringify({ error: 'surfaceType is required.' }));
2489
+ return;
2490
+ }
2491
+ try {
2492
+ const profile = core.updateAiProfileSurfaceType({
2493
+ workspaceId: wid,
2494
+ profileId,
2495
+ surfaceType: body.surfaceType
2496
+ });
2497
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2498
+ res.end(JSON.stringify({ success: true, profile: serializeAiProfilePublicRecord(profile) }));
2499
+ }
2500
+ catch (err) {
2501
+ const status = err?.statusCode ?? 500;
2502
+ res.writeHead(status, { 'Content-Type': 'application/json' });
2503
+ res.end(JSON.stringify({
2504
+ error: err?.message ?? 'Failed to update AI profile category.',
2505
+ code: err?.code
2506
+ }));
2507
+ }
2508
+ });
2509
+ // POST /api/taskforce/workspace/ai-profiles/archive - Remove an AI profile from the active roster without deleting its history.
2510
+ addRoute('POST', '/api/taskforce/workspace/ai-profiles/archive', async (req, res) => {
2511
+ if (!ensureWorkspaceAdminRole(req)) {
2512
+ res.writeHead(403, { 'Content-Type': 'application/json' });
2513
+ res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
2514
+ return;
2515
+ }
2516
+ const body = await parseJsonBody(req);
2517
+ const wid = requestWorkspaceId(req);
2518
+ const profileId = String(body.profileId || '').trim();
2519
+ const reason = String(body.reason || '').trim() || 'manual_archive';
2520
+ if (!profileId) {
2521
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2522
+ res.end(JSON.stringify({ error: 'profileId is required.' }));
2523
+ return;
2524
+ }
2525
+ try {
2526
+ const profile = core.archiveAiProfile({
2527
+ workspaceId: wid,
2528
+ profileId,
2529
+ archivedBy: requestUserId(req),
2530
+ reason
2531
+ });
2532
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2533
+ res.end(JSON.stringify({ profile: serializeAiProfilePublicRecord(profile) }));
2534
+ }
2535
+ catch (err) {
2536
+ const status = err?.statusCode ?? 500;
2537
+ res.writeHead(status, { 'Content-Type': 'application/json' });
2538
+ res.end(JSON.stringify({
2539
+ error: err?.message ?? 'Failed to archive AI profile.',
2540
+ details: err?.details ?? null
2541
+ }));
2542
+ }
2543
+ });
1453
2544
  // GET /api/taskforce/admin/users - List workspace users for team management.
1454
2545
  addRoute('GET', '/api/taskforce/admin/users', async (req, res) => {
1455
2546
  if (!ensureWorkspaceAdminRole(req)) {
@@ -1458,8 +2549,9 @@ export function registerAuthRoutes(deps) {
1458
2549
  return;
1459
2550
  }
1460
2551
  const wid = requestWorkspaceId(req);
2552
+ const actorUserId = resolveAuthenticatedUserId(req);
1461
2553
  const gate = core.evaluateFeatureAccess({
1462
- userId: requestUserId(req),
2554
+ userId: actorUserId,
1463
2555
  workspaceId: wid,
1464
2556
  featureKey: 'collaboration.team_management'
1465
2557
  });
@@ -1474,13 +2566,14 @@ export function registerAuthRoutes(deps) {
1474
2566
  });
1475
2567
  // POST /api/taskforce/admin/users/invite - Invite a user into a workspace.
1476
2568
  addRoute('POST', '/api/taskforce/admin/users/invite', async (req, res) => {
1477
- if (!ensureWorkspaceAdminRole(req)) {
2569
+ const body = await parseJsonBody(req);
2570
+ const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
2571
+ if (!ensureWorkspaceAdminRole(req, wid)) {
1478
2572
  res.writeHead(403, { 'Content-Type': 'application/json' });
1479
2573
  res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
1480
2574
  return;
1481
2575
  }
1482
- const body = await parseJsonBody(req);
1483
- const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
2576
+ const actorUserId = resolveAuthenticatedUserId(req);
1484
2577
  const email = String(body?.email || '').trim().toLowerCase();
1485
2578
  const role = String(body?.role || 'member').trim().toLowerCase();
1486
2579
  const permissionMode = String(body?.permissionMode || 'read-write').trim().toLowerCase();
@@ -1490,7 +2583,7 @@ export function registerAuthRoutes(deps) {
1490
2583
  return;
1491
2584
  }
1492
2585
  const inviteGate = core.evaluateFeatureAccess({
1493
- userId: requestUserId(req),
2586
+ userId: actorUserId,
1494
2587
  workspaceId: wid,
1495
2588
  featureKey: 'collaboration.invites'
1496
2589
  });
@@ -1513,8 +2606,8 @@ export function registerAuthRoutes(deps) {
1513
2606
  let inviteEmailError = null;
1514
2607
  if (inviteToken.token) {
1515
2608
  const workspace = core.getWorkspaceProfile(wid);
1516
- const actorIdentity = core.resolveUserIdentity(requestUserId(req));
1517
- const inviterEmailOrLabel = String(actorIdentity?.email || requestUserId(req) || 'Taskforce').trim();
2609
+ const actorIdentity = core.resolveUserIdentity(actorUserId);
2610
+ const inviterEmailOrLabel = String(actorIdentity?.email || actorUserId || 'Taskforce').trim();
1518
2611
  const delivery = await deliverInviteEmail(email, inviteToken.token, req, {
1519
2612
  workspaceName: String(workspace?.name || wid),
1520
2613
  inviterEmailOrLabel
@@ -1529,7 +2622,7 @@ export function registerAuthRoutes(deps) {
1529
2622
  role,
1530
2623
  permissionMode: role === 'member' ? (permissionMode === 'read-only' ? 'read-only' : 'read-write') : 'read-write',
1531
2624
  inviteEmailSent
1532
- });
2625
+ }, wid);
1533
2626
  res.writeHead(200, { 'Content-Type': 'application/json' });
1534
2627
  res.end(JSON.stringify({
1535
2628
  success: true,
@@ -1546,13 +2639,14 @@ export function registerAuthRoutes(deps) {
1546
2639
  });
1547
2640
  // PATCH /api/taskforce/admin/users/:id/role - Update workspace role for a user.
1548
2641
  addRoute('PATCH', '/api/taskforce/admin/users/:id/role', async (req, res, params) => {
1549
- if (!ensureWorkspaceAdminRole(req)) {
2642
+ const body = await parseJsonBody(req);
2643
+ const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
2644
+ if (!ensureWorkspaceAdminRole(req, wid)) {
1550
2645
  res.writeHead(403, { 'Content-Type': 'application/json' });
1551
2646
  res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
1552
2647
  return;
1553
2648
  }
1554
- const body = await parseJsonBody(req);
1555
- const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
2649
+ const actorUserId = resolveAuthenticatedUserId(req);
1556
2650
  const role = String(body?.role || '').trim().toLowerCase();
1557
2651
  if (role !== 'owner' && role !== 'admin' && role !== 'member') {
1558
2652
  res.writeHead(400, { 'Content-Type': 'application/json' });
@@ -1560,7 +2654,7 @@ export function registerAuthRoutes(deps) {
1560
2654
  return;
1561
2655
  }
1562
2656
  const gate = core.evaluateFeatureAccess({
1563
- userId: requestUserId(req),
2657
+ userId: actorUserId,
1564
2658
  workspaceId: wid,
1565
2659
  featureKey: 'collaboration.team_management'
1566
2660
  });
@@ -1576,7 +2670,7 @@ export function registerAuthRoutes(deps) {
1576
2670
  res.end(JSON.stringify({ error: 'User membership not found.' }));
1577
2671
  return;
1578
2672
  }
1579
- auditAuthAction(req, 'update-workspace-user-role', { workspaceId: wid, targetUserId: params.id, role });
2673
+ auditAuthAction(req, 'update-workspace-user-role', { workspaceId: wid, targetUserId: params.id, role }, wid);
1580
2674
  res.writeHead(200, { 'Content-Type': 'application/json' });
1581
2675
  res.end(JSON.stringify({ success: true }));
1582
2676
  }
@@ -1588,13 +2682,14 @@ export function registerAuthRoutes(deps) {
1588
2682
  });
1589
2683
  // PATCH /api/taskforce/admin/users/:id/permission-mode - Update member permission mode.
1590
2684
  addRoute('PATCH', '/api/taskforce/admin/users/:id/permission-mode', async (req, res, params) => {
1591
- if (!ensureWorkspaceAdminRole(req)) {
2685
+ const body = await parseJsonBody(req);
2686
+ const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
2687
+ if (!ensureWorkspaceAdminRole(req, wid)) {
1592
2688
  res.writeHead(403, { 'Content-Type': 'application/json' });
1593
2689
  res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
1594
2690
  return;
1595
2691
  }
1596
- const body = await parseJsonBody(req);
1597
- const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
2692
+ const actorUserId = resolveAuthenticatedUserId(req);
1598
2693
  const mode = String(body?.mode || '').trim().toLowerCase();
1599
2694
  if (mode !== 'read-only' && mode !== 'read-write') {
1600
2695
  res.writeHead(400, { 'Content-Type': 'application/json' });
@@ -1602,7 +2697,7 @@ export function registerAuthRoutes(deps) {
1602
2697
  return;
1603
2698
  }
1604
2699
  const gate = core.evaluateFeatureAccess({
1605
- userId: requestUserId(req),
2700
+ userId: actorUserId,
1606
2701
  workspaceId: wid,
1607
2702
  featureKey: 'collaboration.team_management'
1608
2703
  });
@@ -1618,7 +2713,7 @@ export function registerAuthRoutes(deps) {
1618
2713
  res.end(JSON.stringify({ error: 'User membership not found.' }));
1619
2714
  return;
1620
2715
  }
1621
- auditAuthAction(req, 'update-workspace-user-permission-mode', { workspaceId: wid, targetUserId: params.id, mode });
2716
+ auditAuthAction(req, 'update-workspace-user-permission-mode', { workspaceId: wid, targetUserId: params.id, mode }, wid);
1622
2717
  res.writeHead(200, { 'Content-Type': 'application/json' });
1623
2718
  res.end(JSON.stringify({ success: true }));
1624
2719
  }
@@ -1630,16 +2725,17 @@ export function registerAuthRoutes(deps) {
1630
2725
  });
1631
2726
  // PATCH /api/taskforce/admin/users/:id/disable - Disable or reactivate a workspace user.
1632
2727
  addRoute('PATCH', '/api/taskforce/admin/users/:id/disable', async (req, res, params) => {
1633
- if (!ensureWorkspaceAdminRole(req)) {
2728
+ const body = await parseJsonBody(req);
2729
+ const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
2730
+ if (!ensureWorkspaceAdminRole(req, wid)) {
1634
2731
  res.writeHead(403, { 'Content-Type': 'application/json' });
1635
2732
  res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
1636
2733
  return;
1637
2734
  }
1638
- const body = await parseJsonBody(req);
1639
- const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
2735
+ const actorUserId = resolveAuthenticatedUserId(req);
1640
2736
  const disabled = body?.disabled === true;
1641
2737
  const gate = core.evaluateFeatureAccess({
1642
- userId: requestUserId(req),
2738
+ userId: actorUserId,
1643
2739
  workspaceId: wid,
1644
2740
  featureKey: 'collaboration.team_management'
1645
2741
  });
@@ -1655,7 +2751,7 @@ export function registerAuthRoutes(deps) {
1655
2751
  res.end(JSON.stringify({ error: 'User membership not found.' }));
1656
2752
  return;
1657
2753
  }
1658
- auditAuthAction(req, 'set-workspace-user-disabled', { workspaceId: wid, targetUserId: params.id, disabled });
2754
+ auditAuthAction(req, 'set-workspace-user-disabled', { workspaceId: wid, targetUserId: params.id, disabled }, wid);
1659
2755
  res.writeHead(200, { 'Content-Type': 'application/json' });
1660
2756
  res.end(JSON.stringify({ success: true }));
1661
2757
  }
@@ -1667,15 +2763,16 @@ export function registerAuthRoutes(deps) {
1667
2763
  });
1668
2764
  // POST /api/taskforce/admin/users/:id/invite/revoke - Revoke invite acceptance token.
1669
2765
  addRoute('POST', '/api/taskforce/admin/users/:id/invite/revoke', async (req, res, params) => {
1670
- if (!ensureWorkspaceAdminRole(req)) {
2766
+ const body = await parseJsonBody(req);
2767
+ const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
2768
+ if (!ensureWorkspaceAdminRole(req, wid)) {
1671
2769
  res.writeHead(403, { 'Content-Type': 'application/json' });
1672
2770
  res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
1673
2771
  return;
1674
2772
  }
1675
- const body = await parseJsonBody(req);
1676
- const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
2773
+ const actorUserId = resolveAuthenticatedUserId(req);
1677
2774
  const gate = core.evaluateFeatureAccess({
1678
- userId: requestUserId(req),
2775
+ userId: actorUserId,
1679
2776
  workspaceId: wid,
1680
2777
  featureKey: 'collaboration.team_management'
1681
2778
  });
@@ -1685,21 +2782,22 @@ export function registerAuthRoutes(deps) {
1685
2782
  return;
1686
2783
  }
1687
2784
  const revoked = core.revokeInviteAcceptanceForUser(params.id);
1688
- auditAuthAction(req, 'revoke-invite', { workspaceId: wid, targetUserId: params.id, revoked: revoked.revoked });
2785
+ auditAuthAction(req, 'revoke-invite', { workspaceId: wid, targetUserId: params.id, revoked: revoked.revoked }, wid);
1689
2786
  res.writeHead(200, { 'Content-Type': 'application/json' });
1690
2787
  res.end(JSON.stringify({ success: true, revoked: revoked.revoked }));
1691
2788
  });
1692
2789
  // DELETE /api/taskforce/admin/users/:id - Remove workspace membership.
1693
2790
  addRoute('DELETE', '/api/taskforce/admin/users/:id', async (req, res, params) => {
1694
- if (!ensureWorkspaceAdminRole(req)) {
2791
+ const reqUrl = new URL(req.url || '/api/taskforce/admin/users/:id', 'http://localhost');
2792
+ const wid = String(reqUrl.searchParams.get('workspaceId') || requestWorkspaceId(req)).trim() || 'default';
2793
+ if (!ensureWorkspaceAdminRole(req, wid)) {
1695
2794
  res.writeHead(403, { 'Content-Type': 'application/json' });
1696
2795
  res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
1697
2796
  return;
1698
2797
  }
1699
- const reqUrl = new URL(req.url || '/api/taskforce/admin/users/:id', 'http://localhost');
1700
- const wid = String(reqUrl.searchParams.get('workspaceId') || requestWorkspaceId(req)).trim() || 'default';
2798
+ const actorUserId = resolveAuthenticatedUserId(req);
1701
2799
  const gate = core.evaluateFeatureAccess({
1702
- userId: requestUserId(req),
2800
+ userId: actorUserId,
1703
2801
  workspaceId: wid,
1704
2802
  featureKey: 'collaboration.team_management'
1705
2803
  });
@@ -1715,7 +2813,7 @@ export function registerAuthRoutes(deps) {
1715
2813
  res.end(JSON.stringify({ error: 'User membership not found.' }));
1716
2814
  return;
1717
2815
  }
1718
- auditAuthAction(req, 'remove-workspace-user', { workspaceId: wid, targetUserId: params.id });
2816
+ auditAuthAction(req, 'remove-workspace-user', { workspaceId: wid, targetUserId: params.id }, wid);
1719
2817
  res.writeHead(200, { 'Content-Type': 'application/json' });
1720
2818
  res.end(JSON.stringify({ success: true }));
1721
2819
  }
@@ -1733,8 +2831,9 @@ export function registerAuthRoutes(deps) {
1733
2831
  return;
1734
2832
  }
1735
2833
  const wid = requestWorkspaceId(req);
2834
+ const actorUserId = resolveAuthenticatedUserId(req);
1736
2835
  const gate = core.evaluateFeatureAccess({
1737
- userId: requestUserId(req),
2836
+ userId: actorUserId,
1738
2837
  workspaceId: wid,
1739
2838
  featureKey: 'collaboration.team_management'
1740
2839
  });