@taskforcehq/taskforce 0.3.305 → 0.3.306

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) hide show
  1. package/dist/Taskforce.module.css +295 -89
  2. package/dist/TaskforceCore.js +215 -43
  3. package/dist/TaskforceCore.test.js +433 -28
  4. package/dist/assets/fonts/CourierPrime-Bold.woff2 +0 -0
  5. package/dist/assets/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  6. package/dist/assets/fonts/CourierPrime-Italic.woff2 +0 -0
  7. package/dist/assets/fonts/CourierPrime-Regular.woff2 +0 -0
  8. package/dist/assets/fonts/Noir_medium.woff2 +0 -0
  9. package/dist/assets/fonts/Noir_regular.woff2 +0 -0
  10. package/dist/assets/fonts/RussoOne-Regular.woff2 +0 -0
  11. package/dist/assets/fonts/SpecialElite-Regular.woff2 +0 -0
  12. package/dist/compat/workspaceSyncCompat.js +46 -2
  13. package/dist/compat/workspaceSyncCompat.test.js +38 -1
  14. package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
  15. package/dist/components/context/ContextAttachmentManager.js +549 -0
  16. package/dist/components/features/AgentsModule.d.ts +10 -1
  17. package/dist/components/features/AgentsModule.js +260 -15
  18. package/dist/components/features/AgentsModule.test.js +255 -5
  19. package/dist/components/features/DocumentViewer.d.ts +5 -1
  20. package/dist/components/features/DocumentViewer.js +60 -3
  21. package/dist/components/features/DocumentWorkspace.js +1 -1
  22. package/dist/components/features/DocumentWorkspace.test.js +33 -0
  23. package/dist/components/features/documentWorkspaceModel.d.ts +1 -1
  24. package/dist/components/task/TaskCard.js +19 -28
  25. package/dist/components/task/TaskCard.test.js +38 -3
  26. package/dist/components/task/TaskContextUpload.js +4 -526
  27. package/dist/components/task/TaskForm.js +85 -90
  28. package/dist/components/task/TaskForm.test.js +110 -8
  29. package/dist/components/task/TaskKanban.d.ts +2 -1
  30. package/dist/components/task/TaskKanban.js +94 -13
  31. package/dist/components/task/TaskKanban.test.js +36 -0
  32. package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
  33. package/dist/components/ui/EditableAvatarButton.js +18 -0
  34. package/dist/components/views/PlanComparisonPage.d.ts +9 -1
  35. package/dist/components/views/PlanComparisonPage.js +70 -14
  36. package/dist/components/views/PlanComparisonPage.test.js +238 -0
  37. package/dist/components/views/PlansPage.js +127 -36
  38. package/dist/components/views/PlansPage.test.js +294 -12
  39. package/dist/components/views/StandaloneLayout.js +283 -533
  40. package/dist/components/views/panels/PlanningDrawer.d.ts +8 -1
  41. package/dist/components/views/panels/PlanningDrawer.js +18 -11
  42. package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
  43. package/dist/components/views/panels/PlanningDrawer.test.js +141 -0
  44. package/dist/components/views/planningScope.d.ts +20 -0
  45. package/dist/components/views/planningScope.js +25 -0
  46. package/dist/components/views/planningScope.test.d.ts +1 -0
  47. package/dist/components/views/planningScope.test.js +60 -0
  48. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +26 -1
  49. package/dist/components/views/standalone/modals/AccountHubModal.js +76 -2
  50. package/dist/components/views/standalone/modals/AccountHubModal.test.js +20 -3
  51. package/dist/components/views/standalone/modals/AvatarImageManagerModal.d.ts +20 -0
  52. package/dist/components/views/standalone/modals/AvatarImageManagerModal.js +127 -0
  53. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +2 -9
  54. package/dist/components/views/standalone/modals/EditProfileModal.js +3 -2
  55. package/dist/components/views/standalone/modals/SyncEnableWarningModal.d.ts +9 -0
  56. package/dist/components/views/standalone/modals/SyncEnableWarningModal.js +6 -0
  57. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +1 -0
  58. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +24 -0
  59. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +2 -1
  60. package/dist/components/views/standalone/modals/SyncStatusModal.js +2 -2
  61. package/dist/core/AiProfileService.d.ts +39 -2
  62. package/dist/core/AiProfileService.js +639 -73
  63. package/dist/core/AiProfileService.test.js +19 -1
  64. package/dist/core/AiProfiles.test.js +566 -7
  65. package/dist/core/AttachmentLinkService.js +57 -15
  66. package/dist/core/AuthIdentityService.d.ts +107 -0
  67. package/dist/core/AuthIdentityService.js +284 -0
  68. package/dist/core/AuthTokenService.d.ts +4 -0
  69. package/dist/core/AuthTokenService.js +34 -6
  70. package/dist/core/AuthTokenService.test.js +3 -3
  71. package/dist/core/EntitlementsPolicy.test.js +2 -2
  72. package/dist/core/GlobalSettingsService.js +115 -6
  73. package/dist/core/PlanEntitlementService.d.ts +23 -2
  74. package/dist/core/PlanEntitlementService.js +343 -57
  75. package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
  76. package/dist/core/PlanFeatureCatalog.test.js +84 -0
  77. package/dist/core/PlanVersionPolicy.test.js +35 -2
  78. package/dist/core/PlanningEntities.test.js +52 -0
  79. package/dist/core/SignupMonetizationSettings.test.js +199 -27
  80. package/dist/core/SyncReconciliationService.js +2 -65
  81. package/dist/core/SystemAdmin.test.js +42 -42
  82. package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
  83. package/dist/core/Taskforce.d.ts +225 -14
  84. package/dist/core/Taskforce.js +765 -192
  85. package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
  86. package/dist/core/Taskforce.listTasksSlim.test.js +91 -0
  87. package/dist/core/UserProfileAvatarDrafts.test.js +30 -3
  88. package/dist/core/WorkspaceLifecycleService.js +0 -5
  89. package/dist/core/WorkspacePermissions.test.js +25 -1
  90. package/dist/core/WorkspacePlanMode.test.js +117 -16
  91. package/dist/core/shared.d.ts +1 -1
  92. package/dist/core/shared.js +5 -1
  93. package/dist/core/types.d.ts +68 -2
  94. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +26 -1
  95. package/dist/hooks/sync/bootstrap.js +2 -0
  96. package/dist/hooks/sync/controlPlane.d.ts +37 -0
  97. package/dist/hooks/sync/controlPlane.js +78 -0
  98. package/dist/hooks/sync/controlPlane.test.d.ts +1 -0
  99. package/dist/hooks/sync/controlPlane.test.js +121 -0
  100. package/dist/hooks/sync/lifecyclePolicy.d.ts +45 -0
  101. package/dist/hooks/sync/lifecyclePolicy.js +93 -0
  102. package/dist/hooks/sync/lifecyclePolicy.test.d.ts +1 -0
  103. package/dist/hooks/sync/lifecyclePolicy.test.js +124 -0
  104. package/dist/hooks/sync/orchestratorShared.d.ts +10 -1
  105. package/dist/hooks/sync/orchestratorShared.js +68 -47
  106. package/dist/hooks/sync/orchestratorShared.test.js +50 -1
  107. package/dist/hooks/sync/pullLifecycle.d.ts +31 -0
  108. package/dist/hooks/sync/pullLifecycle.js +24 -0
  109. package/dist/hooks/sync/pullLifecycle.test.d.ts +1 -0
  110. package/dist/hooks/sync/pullLifecycle.test.js +80 -0
  111. package/dist/hooks/sync/recovery.d.ts +16 -2
  112. package/dist/hooks/sync/recovery.js +171 -53
  113. package/dist/hooks/sync/recovery.test.d.ts +1 -0
  114. package/dist/hooks/sync/recovery.test.js +292 -0
  115. package/dist/hooks/sync/transfers.d.ts +15 -3
  116. package/dist/hooks/sync/transfers.js +179 -62
  117. package/dist/hooks/sync/transfers.test.d.ts +1 -0
  118. package/dist/hooks/sync/transfers.test.js +396 -0
  119. package/dist/hooks/sync/useRecentSyncEvents.d.ts +21 -0
  120. package/dist/hooks/sync/useRecentSyncEvents.js +92 -0
  121. package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +1 -0
  122. package/dist/hooks/sync/useRecentSyncEvents.test.js +124 -0
  123. package/dist/hooks/sync/useSyncStatusActions.d.ts +30 -0
  124. package/dist/hooks/sync/useSyncStatusActions.js +88 -0
  125. package/dist/hooks/sync/useSyncStatusActions.test.d.ts +1 -0
  126. package/dist/hooks/sync/useSyncStatusActions.test.js +133 -0
  127. package/dist/hooks/sync/useSyncStatusControls.d.ts +25 -0
  128. package/dist/hooks/sync/useSyncStatusControls.js +60 -0
  129. package/dist/hooks/sync/useSyncStatusControls.test.d.ts +1 -0
  130. package/dist/hooks/sync/useSyncStatusControls.test.js +88 -0
  131. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +12 -0
  132. package/dist/hooks/useSyncOrchestrator.d.ts +9 -2
  133. package/dist/hooks/useSyncOrchestrator.js +359 -178
  134. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +407 -0
  135. package/dist/hooks/useTaskData.js +8 -3
  136. package/dist/hooks/useTaskData.test.js +45 -0
  137. package/dist/hooks/useTaskforce.d.ts +60 -2
  138. package/dist/hooks/useTaskforce.js +549 -240
  139. package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
  140. package/dist/hooks/useTaskforce.runtime-routing.test.js +152 -0
  141. package/dist/hooks/useTaskforce.sync-behavior.test.js +2380 -212
  142. package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
  143. package/dist/hooks/useWorkspaceSyncController.js +6 -1
  144. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
  145. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +316 -35
  146. package/dist/localization/locales/en-US.d.ts +0 -1
  147. package/dist/localization/locales/en-US.js +0 -1
  148. package/dist/localization/locales/es-419.js +0 -1
  149. package/dist/localization/locales/pt-BR.js +0 -1
  150. package/dist/mcp/documentAssetRegistrar.js +3 -3
  151. package/dist/mcp/runtime.d.ts +2 -0
  152. package/dist/mcp/runtime.js +131 -64
  153. package/dist/mcp/runtime.test.js +291 -12
  154. package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
  155. package/dist/migrations/taskSchemaMigrations.js +171 -61
  156. package/dist/migrations/taskSchemaMigrations.test.js +15 -0
  157. package/dist/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  158. package/dist/resources/templates/workflows/collaborate.yaml +1 -1
  159. package/dist/resources/templates/workflows/evaluate.yaml +2 -2
  160. package/dist/resources/templates/workflows/plan.yaml +1 -1
  161. package/dist/resources/templates/workflows/review.yaml +2 -2
  162. package/dist/server/annotatedAttachmentsRoutes.test.js +3 -3
  163. package/dist/server/auth/providers/apple.d.ts +33 -0
  164. package/dist/server/auth/providers/apple.js +82 -0
  165. package/dist/server/auth/providers/appleClientSecret.d.ts +37 -0
  166. package/dist/server/auth/providers/appleClientSecret.js +65 -0
  167. package/dist/server/auth/providers/github.d.ts +26 -0
  168. package/dist/server/auth/providers/github.js +86 -0
  169. package/dist/server/auth/providers/google.d.ts +21 -0
  170. package/dist/server/auth/providers/google.js +56 -0
  171. package/dist/server/auth/providers/types.d.ts +21 -0
  172. package/dist/server/auth/providers/types.js +1 -0
  173. package/dist/server/auth.d.ts +2 -0
  174. package/dist/server/auth.js +6 -3
  175. package/dist/server/documentReviewRoutes.test.js +36 -3
  176. package/dist/server/index.cookieProxy.test.js +1 -0
  177. package/dist/server/index.d.ts +12 -0
  178. package/dist/server/index.js +120 -9
  179. package/dist/server/index.rateLimit.test.js +3 -0
  180. package/dist/server/index.test.js +106 -3
  181. package/dist/server/routes/admin.d.ts +3 -0
  182. package/dist/server/routes/admin.js +272 -19
  183. package/dist/server/routes/auth.d.ts +15 -2
  184. package/dist/server/routes/auth.js +994 -57
  185. package/dist/server/routes/authSupport.d.ts +1 -1
  186. package/dist/server/routes/authSupport.js +1 -1
  187. package/dist/server/routes/billing.js +361 -35
  188. package/dist/server/routes/billing.test.js +592 -12
  189. package/dist/server/routes/documents.d.ts +1 -0
  190. package/dist/server/routes/documents.js +172 -70
  191. package/dist/server/routes/shared.d.ts +28 -2
  192. package/dist/server/routes/shared.js +69 -4
  193. package/dist/server/routes/sync.integration.test.js +437 -39
  194. package/dist/server/routes/sync.js +61 -605
  195. package/dist/server/routes/syncAuxRoutes.d.ts +15 -0
  196. package/dist/server/routes/syncAuxRoutes.js +91 -0
  197. package/dist/server/routes/syncAuxRoutes.test.d.ts +1 -0
  198. package/dist/server/routes/syncAuxRoutes.test.js +158 -0
  199. package/dist/server/routes/syncPullApplyRoutes.d.ts +36 -0
  200. package/dist/server/routes/syncPullApplyRoutes.js +204 -0
  201. package/dist/server/routes/syncPushRoutes.d.ts +24 -0
  202. package/dist/server/routes/syncPushRoutes.js +212 -0
  203. package/dist/server/routes/syncRouteGuards.d.ts +36 -0
  204. package/dist/server/routes/syncRouteGuards.js +67 -0
  205. package/dist/server/routes/syncRouteGuards.test.d.ts +1 -0
  206. package/dist/server/routes/syncRouteGuards.test.js +94 -0
  207. package/dist/server/routes/syncRouteTypes.d.ts +13 -0
  208. package/dist/server/routes/syncRouteTypes.js +1 -0
  209. package/dist/server/routes/syncSnapshotRoutes.d.ts +66 -0
  210. package/dist/server/routes/syncSnapshotRoutes.js +180 -0
  211. package/dist/server/routes/tasks.js +43 -1
  212. package/dist/server/routes/workspaces.js +36 -5
  213. package/dist/server/routes.js +60 -3
  214. package/dist/server/routes.test.js +1824 -171
  215. package/dist/services/workflowExportSnapshots.test.js +2 -0
  216. package/dist/shared/aiProfileSeatScope.d.ts +5 -0
  217. package/dist/shared/aiProfileSeatScope.js +21 -0
  218. package/dist/shared/passwordPolicy.d.ts +13 -0
  219. package/dist/shared/passwordPolicy.js +84 -0
  220. package/dist/shared/passwordPolicy.test.d.ts +1 -0
  221. package/dist/shared/passwordPolicy.test.js +34 -0
  222. package/dist/storage/workspaceAssetStore.d.ts +15 -3
  223. package/dist/storage/workspaceAssetStore.js +132 -44
  224. package/dist/storage/workspaceAssetStore.test.js +93 -6
  225. package/dist/styles/fonts.css +70 -0
  226. package/dist/sync/collaborationSyncPayload.d.ts +6 -1
  227. package/dist/sync/collaborationSyncPayload.js +17 -11
  228. package/dist/sync/collaborationSyncPayload.test.js +13 -1
  229. package/dist/sync/contentSyncState.d.ts +4 -8
  230. package/dist/sync/contentSyncState.js +17 -70
  231. package/dist/sync/syncApplyHandlers.d.ts +7 -0
  232. package/dist/sync/syncApplyHandlers.js +28 -13
  233. package/dist/sync/syncApplyHandlers.test.js +70 -3
  234. package/dist/sync/syncFieldSemantics.d.ts +12 -0
  235. package/dist/sync/syncFieldSemantics.js +55 -0
  236. package/dist/sync/syncResourceAdapters.d.ts +44 -0
  237. package/dist/sync/syncResourceAdapters.js +77 -0
  238. package/dist/sync/syncService.d.ts +5 -0
  239. package/dist/sync/syncService.js +52 -0
  240. package/dist/sync/syncService.test.js +57 -3
  241. package/dist/sync/taskSyncChangeKey.d.ts +2 -0
  242. package/dist/sync/taskSyncChangeKey.js +143 -0
  243. package/dist/sync/taskSyncPayload.js +3 -8
  244. package/dist/sync/workspacePullFeed.js +4 -3
  245. package/dist/sync/workspacePullFeed.test.js +43 -0
  246. package/dist/sync/workspaceRepair.js +5 -2
  247. package/dist/sync/workspaceSyncModel.d.ts +45 -0
  248. package/dist/sync/workspaceSyncModel.js +183 -28
  249. package/dist/sync/workspaceSyncModel.test.js +291 -12
  250. package/dist/sync/workspaceSyncState.d.ts +17 -0
  251. package/dist/sync/workspaceSyncState.js +2 -1
  252. package/dist/sync/workspaceSyncSurface.js +3 -8
  253. package/dist/types.d.ts +17 -0
  254. package/dist/ui/assets/AgentsModule-DrLawwNl.js +1 -0
  255. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BG6P_GVW.js → AnnotatedAttachmentWorkspace-Z9_whf7F.js} +1 -1
  256. package/dist/ui/assets/CourierPrime-Bold-BuLj_dpw.woff2 +0 -0
  257. package/dist/ui/assets/CourierPrime-BoldItalic-BrK2pUkZ.woff2 +0 -0
  258. package/dist/ui/assets/CourierPrime-Italic-DFUZK5Ey.woff2 +0 -0
  259. package/dist/ui/assets/CourierPrime-Regular-BsKphzVf.woff2 +0 -0
  260. package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +252 -0
  261. package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +1 -0
  262. package/dist/ui/assets/{InitiativesModule-Dhm7M8e7.js → InitiativesModule-9E6ParrY.js} +1 -1
  263. package/dist/ui/assets/Noir_medium-bxQwKGzB.woff2 +0 -0
  264. package/dist/ui/assets/Noir_regular-ojf2kxlG.woff2 +0 -0
  265. package/dist/ui/assets/PlansPage-CvfnMiWq.css +1 -0
  266. package/dist/ui/assets/PlansPage-DRXscYxH.js +1 -0
  267. package/dist/ui/assets/RussoOne-Regular-Cu_qq_qC.woff2 +0 -0
  268. package/dist/ui/assets/SpecialElite-Regular-Di6gSvAS.woff2 +0 -0
  269. package/dist/ui/assets/{TaskSettings-ln0aF7xc.js → TaskSettings-DZX4jk7e.js} +1 -1
  270. package/dist/ui/assets/{WorkflowsModule-BcS4afRn.js → WorkflowsModule-BA7jcEpH.js} +1 -1
  271. package/dist/ui/assets/index-9eT8e0Lu.css +1 -0
  272. package/dist/ui/assets/index-C6k75jr8.js +6 -0
  273. package/dist/ui/assets/{vendor-icons-BSUaRwF8.js → vendor-icons-DuEd_65c.js} +1 -1
  274. package/dist/ui/fonts/CourierPrime-Bold.ttf +0 -0
  275. package/dist/ui/fonts/CourierPrime-Bold.woff2 +0 -0
  276. package/dist/ui/fonts/CourierPrime-BoldItalic.ttf +0 -0
  277. package/dist/ui/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  278. package/dist/ui/fonts/CourierPrime-Italic.ttf +0 -0
  279. package/dist/ui/fonts/CourierPrime-Italic.woff2 +0 -0
  280. package/dist/ui/fonts/CourierPrime-Regular.ttf +0 -0
  281. package/dist/ui/fonts/CourierPrime-Regular.woff2 +0 -0
  282. package/dist/ui/fonts/Noir_medium.otf +0 -0
  283. package/dist/ui/fonts/Noir_medium.woff2 +0 -0
  284. package/dist/ui/fonts/Noir_regular.otf +0 -0
  285. package/dist/ui/fonts/Noir_regular.woff2 +0 -0
  286. package/dist/ui/fonts/RussoOne-Regular.woff2 +0 -0
  287. package/dist/ui/fonts/SpecialElite-Regular.ttf +0 -0
  288. package/dist/ui/fonts/SpecialElite-Regular.woff2 +0 -0
  289. package/dist/ui/index.html +3 -3
  290. package/dist/utils/accountProfileSummaryCache.d.ts +5 -0
  291. package/dist/utils/assignees.d.ts +2 -0
  292. package/dist/utils/assignees.js +2 -0
  293. package/dist/utils/avatarUpload.d.ts +27 -0
  294. package/dist/utils/avatarUpload.js +132 -0
  295. package/dist/utils/avatarUpload.test.d.ts +1 -0
  296. package/dist/utils/avatarUpload.test.js +40 -0
  297. package/dist/utils/bootstrapTrace.d.ts +7 -0
  298. package/dist/utils/bootstrapTrace.js +29 -0
  299. package/dist/utils/commercialLifecyclePresentation.js +3 -0
  300. package/dist/utils/contextAssetEvents.d.ts +2 -0
  301. package/dist/utils/syncEventDetails.d.ts +3 -0
  302. package/dist/utils/syncEventDetails.js +64 -0
  303. package/dist/utils/syncEventDetails.test.d.ts +1 -0
  304. package/dist/utils/syncEventDetails.test.js +23 -0
  305. package/dist/utils/syncEventPresentation.d.ts +15 -0
  306. package/dist/utils/syncEventPresentation.js +129 -0
  307. package/dist/utils/syncEventPresentation.test.d.ts +1 -0
  308. package/dist/utils/syncEventPresentation.test.js +64 -0
  309. package/dist/utils/syncStatusPresentation.d.ts +72 -0
  310. package/dist/utils/syncStatusPresentation.js +217 -0
  311. package/dist/utils/syncStatusPresentation.test.d.ts +1 -0
  312. package/dist/utils/syncStatusPresentation.test.js +164 -0
  313. package/dist/utils/taskActivity.d.ts +2 -0
  314. package/dist/utils/taskActivity.js +228 -22
  315. package/dist/utils/taskActivity.test.js +119 -8
  316. package/dist/utils/workspaceSyncPresentation.d.ts +4 -0
  317. package/dist/utils/workspaceSyncPresentation.js +68 -2
  318. package/dist/utils/workspaceSyncPresentation.test.js +36 -1
  319. package/package.json +3 -2
  320. package/scripts/export-sqlite-to-postgres.mjs +7 -9
  321. package/scripts/run-e2e-realtime.sh +17 -3
  322. package/scripts/run-smoke.sh +17 -5
  323. package/scripts/run-soak.sh +17 -5
  324. package/src/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  325. package/src/resources/templates/workflows/collaborate.yaml +1 -1
  326. package/src/resources/templates/workflows/evaluate.yaml +2 -2
  327. package/src/resources/templates/workflows/plan.yaml +1 -1
  328. package/src/resources/templates/workflows/review.yaml +2 -2
  329. package/dist/ui/assets/AgentsModule-N2MnQBZk.js +0 -1
  330. package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +0 -1
  331. package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +0 -252
  332. package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
  333. package/dist/ui/assets/PlansPage-Cq0zXj3I.js +0 -1
  334. package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
  335. package/dist/ui/assets/index-C2-OXZV7.css +0 -1
  336. package/dist/ui/assets/index-DcSI5F86.js +0 -6
  337. package/scripts/migrate-planning-model.ts +0 -233
@@ -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,7 @@
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
29
41
  * GET /api/taskforce/admin/users
30
42
  * POST /api/taskforce/admin/users/invite
31
43
  * PATCH /api/taskforce/admin/users/:id/role
@@ -38,17 +50,20 @@
38
50
  import * as fs from 'fs';
39
51
  import * as path from 'path';
40
52
  import { randomUUID } from 'crypto';
41
- import { parseJsonBody } from './primitives.js';
53
+ import { parseJsonBody, parseFormBody } from './primitives.js';
54
+ import { buildGoogleAuthUrl, exchangeGoogleCode } from '../auth/providers/google.js';
55
+ import { buildAppleAuthUrl, exchangeAppleCode } from '../auth/providers/apple.js';
56
+ import { buildGithubAuthUrl, exchangeGithubCode } from '../auth/providers/github.js';
42
57
  import { hasValidSession, createClearSessionCookie, createSessionTokenCookie, resolveRequestAccess, resolveSessionCookieAttributesForRequest, } from '../auth.js';
43
58
  import { sendEmail } from '../email.js';
44
59
  import { resolveDeploymentConfig } from '../../config/deployment.js';
45
60
  import { resolveRuntimeModeContract } from '../../shared/runtimeContract.js';
46
- import { requestHeader, resolveErrorStatusCode } from './shared.js';
61
+ import { getRouteTimingNow, logSlowBootstrapRoute, requestBootstrapTraceId, requestHeader, resolveErrorStatusCode, serializeJsonWithTiming, setServerTimingHeader } from './shared.js';
47
62
  import { buildAuthFlowBehavior, buildSiteBehavior, getRouteEmailConfig, resolvePublicAuthBaseUrl, resolveRuntimeConfigBaseUrl, shouldExposeDevAuthTokens } from './authSupport.js';
48
63
  import { normalizeTaskDataRelativePath } from '../../storage/objectStorageClient.js';
49
64
  // === SECTION: Registration ===
50
65
  export function registerAuthRoutes(deps) {
51
- const { addRoute, core, context, requestWorkspaceId, requestUserId, ensureAppAccess, ensureAuthenticatedUser, ensureWorkspaceAdminRole, ensureCloudPlanData, requireCloudRuntime, auditAuthAction, resolveAdminRequestActor, resolveEffectiveObjectStorageConfig, getObjectStorageClient, } = deps;
66
+ const { addRoute, core, context, requestWorkspaceId, requestUserId, resolveAuthenticatedUserId, ensureAppAccess, ensureAuthenticatedUser, ensureWorkspaceAdminRole, ensureCloudPlanData, requireCloudRuntime, auditAuthAction, resolveAdminRequestActor, resolveEffectiveObjectStorageConfig, getObjectStorageClient, } = deps;
52
67
  const normalizeTeamWorkspaceRole = (value) => {
53
68
  const role = String(value || '').trim().toLowerCase();
54
69
  if (role === 'owner' || role === 'admin' || role === 'member' || role === 'read-only')
@@ -103,7 +118,7 @@ export function registerAuthRoutes(deps) {
103
118
  return null;
104
119
  };
105
120
  const buildAccountProfileSummary = (req) => {
106
- const userId = requestUserId(req);
121
+ const userId = resolveAuthenticatedUserId(req);
107
122
  const workspaceId = requestWorkspaceId(req);
108
123
  const access = core.getAccountAccessStatus(userId);
109
124
  const resolvedAccess = core.resolveUserAccess(userId, workspaceId, 'member');
@@ -125,8 +140,12 @@ export function registerAuthRoutes(deps) {
125
140
  catch {
126
141
  billingRow = undefined;
127
142
  }
128
- const planId = String(access.entitlement?.planId || billingRow?.tier || '').trim() || null;
143
+ const allowBillingPlanFallback = access.gate === 'plan_selection_required' || access.gate === 'checkout_pending';
144
+ const planId = String(access.entitlement?.planId
145
+ || (allowBillingPlanFallback ? billingRow?.tier : '')
146
+ || '').trim() || null;
129
147
  const planVersionId = String(access.entitlement?.planVersionId || '').trim() || null;
148
+ const entitlementState = access.entitlement?.state || null;
130
149
  let planRow;
131
150
  if (planId) {
132
151
  try {
@@ -151,6 +170,7 @@ export function registerAuthRoutes(deps) {
151
170
  teamPlanMode = 'personal';
152
171
  }
153
172
  }
173
+ const aiProfileSeatUsage = core.countAiProfileUsage(workspaceId);
154
174
  const billingConflictCode = resolveBillingConflictCodeForUser(userId, access.gate);
155
175
  return {
156
176
  stripeStatus: billingRow?.stripe_status || null,
@@ -161,7 +181,7 @@ export function registerAuthRoutes(deps) {
161
181
  trialEnd: billingRow?.trial_end || null,
162
182
  currentPeriodEnd: billingRow?.current_period_end || null,
163
183
  effectiveUntil: billingRow?.effective_until || null,
164
- entitlementState: access.entitlement?.state || billingRow?.entitlement_state || null,
184
+ entitlementState,
165
185
  planVersionId,
166
186
  planId,
167
187
  planName: planRow?.display_name ? String(planRow.display_name) : planId,
@@ -173,7 +193,8 @@ export function registerAuthRoutes(deps) {
173
193
  workspaceRole,
174
194
  canManageWorkspace,
175
195
  teamPlanMode,
176
- teamManagementAllowed: canManageWorkspace && teamPlanMode === 'team'
196
+ teamManagementAllowed: canManageWorkspace && teamPlanMode === 'team',
197
+ aiProfileSeatUsage
177
198
  };
178
199
  };
179
200
  // === SECTION: Email delivery helpers ===
@@ -187,7 +208,7 @@ export function registerAuthRoutes(deps) {
187
208
  const access = resolvedWorkspaceId
188
209
  ? core.resolveUserAccess(userId, resolvedWorkspaceId, fallbackRole)
189
210
  : null;
190
- return createSessionTokenCookie(`pwd:${userId}`, authConfig, {
211
+ return createSessionTokenCookie(`usr:${userId}`, authConfig, {
191
212
  role: (access?.role || fallbackRole),
192
213
  workspaceId: access?.workspaceId || '',
193
214
  userId,
@@ -241,6 +262,29 @@ export function registerAuthRoutes(deps) {
241
262
  await deleteStoredAvatarObject(draft.storageKey);
242
263
  }
243
264
  };
265
+ const parseBinaryBody = async (req, maxBytes) => {
266
+ const safeMaxBytes = Number.isFinite(maxBytes) && maxBytes > 0
267
+ ? Math.floor(maxBytes)
268
+ : MAX_AVATAR_UPLOAD_BYTES;
269
+ return new Promise((resolve, reject) => {
270
+ const chunks = [];
271
+ let receivedBytes = 0;
272
+ req.on('data', (chunk) => {
273
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
274
+ receivedBytes += buffer.length;
275
+ if (receivedBytes > safeMaxBytes) {
276
+ req.pause();
277
+ reject(Object.assign(new Error(`Payload too large (max ${safeMaxBytes} bytes)`), { statusCode: 413 }));
278
+ return;
279
+ }
280
+ chunks.push(buffer);
281
+ });
282
+ req.on('end', () => {
283
+ resolve(Buffer.concat(chunks));
284
+ });
285
+ req.on('error', reject);
286
+ });
287
+ };
244
288
  const logAuthLink = (kind, email, token, req) => {
245
289
  const base = resolvePublicAuthBaseUrl(req);
246
290
  const urlPath = kind === 'verify-email'
@@ -339,11 +383,650 @@ export function registerAuthRoutes(deps) {
339
383
  });
340
384
  res.end(`window.__TASKFORCE_SITE_CONFIG__ = ${payload};`);
341
385
  });
386
+ // GET /api/taskforce/auth/providers - Public: list enabled OAuth login providers (no secrets)
387
+ addRoute('GET', '/api/taskforce/auth/providers', async (_req, res) => {
388
+ const providers = core.getGlobalSettings().oauthProviders || {};
389
+ const enabled = [];
390
+ if (providers.google?.enabled === true)
391
+ enabled.push('google');
392
+ if (providers.github?.enabled === true)
393
+ enabled.push('github');
394
+ if (providers.apple?.enabled === true)
395
+ enabled.push('apple');
396
+ res.writeHead(200, { 'Content-Type': 'application/json' });
397
+ res.end(JSON.stringify({ providers: enabled }));
398
+ });
399
+ // === SECTION: OAuth 2.0 / OpenID Connect ===
400
+ const OAUTH_RETURN_TO_PATTERN = /^\/[A-Za-z0-9_\-./?=&%]*$/;
401
+ /**
402
+ * Validates a `return_to` value and defaults to '/' on failure.
403
+ * Prevents open-redirect attacks by only allowing same-site relative paths.
404
+ */
405
+ const sanitizeReturnTo = (raw) => {
406
+ const value = String(raw || '').trim();
407
+ if (!value || !OAUTH_RETURN_TO_PATTERN.test(value))
408
+ return '/';
409
+ return value;
410
+ };
411
+ /**
412
+ * Provider-agnostic session cookie builder for OAuth-authenticated users.
413
+ * Resolves workspace/role and mints the signed session token.
414
+ */
415
+ const buildOAuthSessionCookie = (req, authConfig, userId, identityId) => {
416
+ const resolvedWorkspaceId = core.resolvePreferredUserWorkspaceId(userId, {
417
+ preferredWorkspaceId: null,
418
+ persistedWorkspaceId: null
419
+ });
420
+ const fallbackRole = String(authConfig.defaultRole || 'member').trim() || 'member';
421
+ const access = resolvedWorkspaceId
422
+ ? core.resolveUserAccess(userId, resolvedWorkspaceId, fallbackRole)
423
+ : null;
424
+ return createSessionTokenCookie(`usr:${userId}`, authConfig, {
425
+ role: (access?.role || fallbackRole),
426
+ workspaceId: access?.workspaceId || '',
427
+ userId,
428
+ identityId,
429
+ ...resolveSessionCookieAttributesForRequest(req, authConfig),
430
+ });
431
+ };
432
+ /**
433
+ * Handles the post-exchange OAuth profile: find or create the user,
434
+ * link the provider identity if needed, and return { userId, identityId }.
435
+ *
436
+ * Resolution order:
437
+ * 1. Existing identity matching (provider, providerSubject) → login.
438
+ * 2. Existing user with same email (provider must have verified it) → link + login.
439
+ * 3. Self-registration allowed → register new user → login.
440
+ * 4. Otherwise → reject.
441
+ */
442
+ const resolveOAuthUser = (profile) => {
443
+ // Case 1: known identity
444
+ const found = core.findUserByOAuthProvider(profile.provider, profile.providerSubject);
445
+ if (found) {
446
+ // Update last_login_at snapshot
447
+ try {
448
+ const db = core.getDatabaseAdapter();
449
+ const tenantId = core.getTenantId();
450
+ db.prepare(`
451
+ UPDATE user_auth_identities
452
+ SET last_login_at = ?, avatar_url_snapshot = COALESCE(?, avatar_url_snapshot),
453
+ updated_at = ?
454
+ WHERE tenant_id = ? AND id = ?
455
+ `).run(new Date().toISOString(), profile.avatarUrl ?? null, new Date().toISOString(), tenantId, found.identityId);
456
+ }
457
+ catch { /* non-fatal */ }
458
+ return { userId: found.userId, identityId: found.identityId, isNew: false };
459
+ }
460
+ // Case 2: email match (provider must have verified it)
461
+ if (profile.email && profile.emailVerified) {
462
+ const byEmail = core.findUserByEmail(profile.email);
463
+ if (byEmail) {
464
+ try {
465
+ const identityId = core.linkOAuthProviderToUser({
466
+ userId: byEmail.userId,
467
+ provider: profile.provider,
468
+ providerSubject: profile.providerSubject,
469
+ providerEmail: profile.email,
470
+ providerEmailVerified: profile.emailVerified,
471
+ displayNameSnapshot: profile.displayName,
472
+ avatarUrlSnapshot: profile.avatarUrl,
473
+ });
474
+ return { userId: byEmail.userId, identityId, isNew: false };
475
+ }
476
+ catch (e) {
477
+ if (e?.code !== 'IDENTITY_ALREADY_EXISTS')
478
+ throw e;
479
+ // Race condition: already linked — re-query
480
+ const refound = core.findUserByOAuthProvider(profile.provider, profile.providerSubject);
481
+ if (refound)
482
+ return { ...refound, isNew: false };
483
+ throw e;
484
+ }
485
+ }
486
+ }
487
+ // Case 3: self-registration
488
+ const authBehavior = buildAuthFlowBehavior(core);
489
+ if (!authBehavior.allowSelfRegistration) {
490
+ throw Object.assign(new Error('Self-registration is currently disabled.'), {
491
+ code: 'REGISTRATION_DISABLED',
492
+ statusCode: 403
493
+ });
494
+ }
495
+ const created = core.registerOAuthUser({
496
+ email: profile.email,
497
+ emailVerified: profile.emailVerified,
498
+ provider: profile.provider,
499
+ providerSubject: profile.providerSubject,
500
+ displayName: profile.displayName,
501
+ avatarUrl: profile.avatarUrl,
502
+ betaAccess: authBehavior.defaultNewUserBetaAccess,
503
+ });
504
+ return { userId: created.userId, identityId: created.identityId, isNew: true };
505
+ };
506
+ // GET /api/taskforce/auth/oauth/:provider/start
507
+ // Initiates the OAuth authorisation redirect for Google, GitHub, or Apple.
508
+ addRoute('GET', '/api/taskforce/auth/oauth/:provider/start', async (req, res, params) => {
509
+ if (!requireCloudRuntime(res, 'OAuth Login'))
510
+ return;
511
+ const authConfig = context.authConfig;
512
+ if (!authConfig?.enabled) {
513
+ res.writeHead(400, { 'Content-Type': 'application/json' });
514
+ res.end(JSON.stringify({ success: false, error: 'Auth is not enabled.' }));
515
+ return;
516
+ }
517
+ const provider = String(params.provider || '').toLowerCase();
518
+ if (provider !== 'google' && provider !== 'github' && provider !== 'apple') {
519
+ res.writeHead(404, { 'Content-Type': 'application/json' });
520
+ res.end(JSON.stringify({ success: false, error: 'Unknown OAuth provider.' }));
521
+ return;
522
+ }
523
+ const oauthProviders = core.getGlobalSettings().oauthProviders || {};
524
+ const providerConfig = oauthProviders[provider];
525
+ if (!providerConfig?.enabled) {
526
+ res.writeHead(400, { 'Content-Type': 'application/json' });
527
+ res.end(JSON.stringify({ success: false, error: `${provider} login is not enabled.` }));
528
+ return;
529
+ }
530
+ const queryString = (req.url || '').split('?')[1] || '';
531
+ const queryParams = new URLSearchParams(queryString);
532
+ const returnTo = sanitizeReturnTo(queryParams.get('return_to'));
533
+ const clientIp = requestHeader(req, 'x-forwarded-for', '') || requestHeader(req, 'x-real-ip', '') || null;
534
+ const rawInviteToken = String(queryParams.get('invite_token') || '').trim();
535
+ const rawFlowHint = String(queryParams.get('flow') || '').trim().toLowerCase();
536
+ // Determine flow mode: link > invite_claim > login
537
+ let flowMode = 'login';
538
+ let validatedInviteToken = null;
539
+ let linkUserId = null;
540
+ if (rawFlowHint === 'link') {
541
+ // Provider-link flow: require an authenticated session
542
+ if (!authConfig || !hasValidSession(req, authConfig)) {
543
+ res.writeHead(401, { 'Content-Type': 'application/json' });
544
+ res.end(JSON.stringify({ success: false, error: 'Sign in required to link a provider.', code: 'AUTH_REQUIRED' }));
545
+ return;
546
+ }
547
+ const access = resolveRequestAccess(req, authConfig);
548
+ linkUserId = access?.userId || null;
549
+ if (!linkUserId) {
550
+ res.writeHead(401, { 'Content-Type': 'application/json' });
551
+ res.end(JSON.stringify({ success: false, error: 'Unable to resolve authenticated user.', code: 'AUTH_REQUIRED' }));
552
+ return;
553
+ }
554
+ flowMode = 'link';
555
+ }
556
+ else if (rawInviteToken) {
557
+ // Invite-claim flow
558
+ const inviteDetails = core.inspectInviteAcceptance(rawInviteToken);
559
+ if (inviteDetails.state !== 'valid') {
560
+ res.writeHead(400, { 'Content-Type': 'application/json' });
561
+ res.end(JSON.stringify({ success: false, error: 'Invite token is invalid or expired.', code: 'INVITE_TOKEN_INVALID' }));
562
+ return;
563
+ }
564
+ if (inviteDetails.inviteeState !== 'pending_setup') {
565
+ res.writeHead(400, { 'Content-Type': 'application/json' });
566
+ res.end(JSON.stringify({ success: false, error: 'This invite has already been claimed.', code: 'INVITE_ALREADY_CLAIMED' }));
567
+ return;
568
+ }
569
+ flowMode = 'invite_claim';
570
+ validatedInviteToken = rawInviteToken;
571
+ }
572
+ const { state, codeChallenge, nonce } = core.createOAuthLoginState({
573
+ provider,
574
+ flowMode,
575
+ userId: linkUserId,
576
+ returnTo,
577
+ inviteToken: validatedInviteToken,
578
+ createdIp: clientIp,
579
+ });
580
+ const baseUrl = resolvePublicAuthBaseUrl(req);
581
+ const redirectUri = `${baseUrl}/api/taskforce/auth/oauth/${provider}/callback`;
582
+ let authUrl;
583
+ if (provider === 'google') {
584
+ const cfg = providerConfig;
585
+ if (!cfg.clientId) {
586
+ res.writeHead(500, { 'Content-Type': 'application/json' });
587
+ res.end(JSON.stringify({ success: false, error: 'Google OAuth is not fully configured.' }));
588
+ return;
589
+ }
590
+ authUrl = buildGoogleAuthUrl({
591
+ clientId: cfg.clientId,
592
+ redirectUri,
593
+ state,
594
+ codeChallenge,
595
+ });
596
+ }
597
+ else if (provider === 'github') {
598
+ const cfg = providerConfig;
599
+ if (!cfg.clientId) {
600
+ res.writeHead(500, { 'Content-Type': 'application/json' });
601
+ res.end(JSON.stringify({ success: false, error: 'GitHub OAuth is not fully configured.' }));
602
+ return;
603
+ }
604
+ authUrl = buildGithubAuthUrl({
605
+ clientId: cfg.clientId,
606
+ redirectUri,
607
+ state,
608
+ codeChallenge,
609
+ });
610
+ }
611
+ else {
612
+ // Apple
613
+ const cfg = providerConfig;
614
+ if (!cfg.servicesId) {
615
+ res.writeHead(500, { 'Content-Type': 'application/json' });
616
+ res.end(JSON.stringify({ success: false, error: 'Apple Sign In is not fully configured.' }));
617
+ return;
618
+ }
619
+ authUrl = buildAppleAuthUrl({
620
+ servicesId: cfg.servicesId,
621
+ redirectUri,
622
+ state,
623
+ nonce,
624
+ });
625
+ }
626
+ res.writeHead(302, { Location: authUrl });
627
+ res.end();
628
+ });
629
+ // GET /api/taskforce/auth/oauth/:provider/callback
630
+ // Handles the OAuth callback for Google and GitHub (GET redirect with `code` + `state`).
631
+ addRoute('GET', '/api/taskforce/auth/oauth/:provider/callback', async (req, res, params) => {
632
+ if (!requireCloudRuntime(res, 'OAuth Login'))
633
+ return;
634
+ const authConfig = context.authConfig;
635
+ if (!authConfig?.enabled) {
636
+ res.writeHead(400, { 'Content-Type': 'application/json' });
637
+ res.end(JSON.stringify({ success: false, error: 'Auth is not enabled.' }));
638
+ return;
639
+ }
640
+ const provider = String(params.provider || '').toLowerCase();
641
+ if (provider !== 'google' && provider !== 'github') {
642
+ res.writeHead(404, { 'Content-Type': 'application/json' });
643
+ res.end(JSON.stringify({ success: false, error: 'Unknown OAuth provider.' }));
644
+ return;
645
+ }
646
+ const queryString = (req.url || '').split('?')[1] || '';
647
+ const queryParams = new URLSearchParams(queryString);
648
+ const stateParam = String(queryParams.get('state') || '').trim();
649
+ const codeParam = String(queryParams.get('code') || '').trim();
650
+ const errorParam = String(queryParams.get('error') || '').trim();
651
+ const baseUrl = resolvePublicAuthBaseUrl(req);
652
+ const redirectUri = `${baseUrl}/api/taskforce/auth/oauth/${provider}/callback`;
653
+ if (errorParam) {
654
+ auditAuthAction(req, 'auth-oauth-callback-denied', { provider, error: errorParam });
655
+ res.writeHead(302, { Location: `/login?error=oauth_denied&provider=${encodeURIComponent(provider)}` });
656
+ res.end();
657
+ return;
658
+ }
659
+ if (!stateParam || !codeParam) {
660
+ res.writeHead(302, { Location: `/login?error=oauth_invalid&provider=${encodeURIComponent(provider)}` });
661
+ res.end();
662
+ return;
663
+ }
664
+ // Consume state (single-use, validates expiry)
665
+ const stateRecord = core.consumeOAuthLoginState(stateParam);
666
+ if (!stateRecord) {
667
+ auditAuthAction(req, 'auth-oauth-state-invalid', { provider, stateProvided: true });
668
+ res.writeHead(302, { Location: `/login?error=oauth_state_invalid&provider=${encodeURIComponent(provider)}` });
669
+ res.end();
670
+ return;
671
+ }
672
+ if (stateRecord.provider !== provider) {
673
+ auditAuthAction(req, 'auth-oauth-state-provider-mismatch', { provider, stateProvider: stateRecord.provider });
674
+ res.writeHead(302, { Location: `/login?error=oauth_state_invalid&provider=${encodeURIComponent(provider)}` });
675
+ res.end();
676
+ return;
677
+ }
678
+ const codeVerifier = stateRecord.pkceVerifier || '';
679
+ const returnTo = sanitizeReturnTo(stateRecord.returnTo);
680
+ const oauthProviders = core.getGlobalSettings().oauthProviders || {};
681
+ try {
682
+ let profile;
683
+ if (provider === 'google') {
684
+ const cfg = (oauthProviders.google || {});
685
+ if (!cfg.clientId || !cfg.clientSecret) {
686
+ throw Object.assign(new Error('Google OAuth is not configured.'), { statusCode: 500 });
687
+ }
688
+ profile = await exchangeGoogleCode({
689
+ code: codeParam,
690
+ codeVerifier,
691
+ redirectUri,
692
+ clientId: cfg.clientId,
693
+ clientSecret: cfg.clientSecret,
694
+ });
695
+ }
696
+ else {
697
+ const cfg = (oauthProviders.github || {});
698
+ if (!cfg.clientId || !cfg.clientSecret) {
699
+ throw Object.assign(new Error('GitHub OAuth is not configured.'), { statusCode: 500 });
700
+ }
701
+ profile = await exchangeGithubCode({
702
+ code: codeParam,
703
+ codeVerifier,
704
+ redirectUri,
705
+ clientId: cfg.clientId,
706
+ clientSecret: cfg.clientSecret,
707
+ });
708
+ }
709
+ let userId;
710
+ let identityId;
711
+ let isNew = false;
712
+ if (stateRecord.flowMode === 'link') {
713
+ if (!stateRecord.userId) {
714
+ throw Object.assign(new Error('User ID missing from OAuth link state.'), { code: 'AUTH_REQUIRED', statusCode: 401 });
715
+ }
716
+ identityId = core.linkOAuthProviderToUser({
717
+ userId: stateRecord.userId,
718
+ provider: profile.provider,
719
+ providerSubject: profile.providerSubject,
720
+ providerEmail: profile.email,
721
+ providerEmailVerified: profile.emailVerified,
722
+ displayNameSnapshot: profile.displayName,
723
+ avatarUrlSnapshot: profile.avatarUrl,
724
+ });
725
+ userId = stateRecord.userId;
726
+ isNew = false;
727
+ }
728
+ else if (stateRecord.flowMode === 'invite_claim') {
729
+ if (!stateRecord.inviteToken) {
730
+ throw Object.assign(new Error('Invite token missing from OAuth state.'), { code: 'INVITE_TOKEN_MISSING', statusCode: 400 });
731
+ }
732
+ const claimed = core.claimInviteWithOAuthProvider({
733
+ token: stateRecord.inviteToken,
734
+ provider: profile.provider,
735
+ providerSubject: profile.providerSubject,
736
+ providerEmail: profile.email,
737
+ providerEmailVerified: profile.emailVerified,
738
+ displayNameSnapshot: profile.displayName,
739
+ avatarUrlSnapshot: profile.avatarUrl,
740
+ });
741
+ userId = claimed.userId;
742
+ identityId = claimed.identityId;
743
+ isNew = true;
744
+ }
745
+ else {
746
+ ({ userId, identityId, isNew } = resolveOAuthUser(profile));
747
+ }
748
+ const cookie = buildOAuthSessionCookie(req, authConfig, userId, identityId);
749
+ res.setHeader('Set-Cookie', cookie);
750
+ auditAuthAction(req, 'auth-oauth-login-success', {
751
+ provider,
752
+ userId,
753
+ isNew,
754
+ flowMode: stateRecord.flowMode,
755
+ email: profile.email || null,
756
+ });
757
+ res.writeHead(302, { Location: returnTo });
758
+ res.end();
759
+ }
760
+ catch (err) {
761
+ const code = err?.code || 'OAUTH_ERROR';
762
+ auditAuthAction(req, 'auth-oauth-callback-error', { provider, code, message: String(err?.message || '') });
763
+ if (code === 'REGISTRATION_DISABLED') {
764
+ res.writeHead(302, { Location: `/login?error=registration_disabled&provider=${encodeURIComponent(provider)}` });
765
+ }
766
+ else if (code === 'IDENTITY_ALREADY_EXISTS') {
767
+ res.writeHead(302, { Location: `${returnTo}?error=provider_already_linked&provider=${encodeURIComponent(provider)}` });
768
+ }
769
+ else if (code === 'INVITE_TOKEN_EXPIRED' || code === 'INVITE_TOKEN_REVOKED' || code === 'INVITE_TOKEN_USED' || code === 'INVITE_TOKEN_INVALID') {
770
+ res.writeHead(302, { Location: `/login?error=invite_invalid&provider=${encodeURIComponent(provider)}` });
771
+ }
772
+ else {
773
+ res.writeHead(302, { Location: `/login?error=oauth_failed&provider=${encodeURIComponent(provider)}` });
774
+ }
775
+ res.end();
776
+ }
777
+ });
778
+ // POST /api/taskforce/auth/oauth/apple/callback
779
+ // Apple Sign In sends `code`, `state`, `id_token` and optional `user` JSON via form_post.
780
+ addRoute('POST', '/api/taskforce/auth/oauth/apple/callback', async (req, res) => {
781
+ if (!requireCloudRuntime(res, 'Apple Sign In'))
782
+ return;
783
+ const authConfig = context.authConfig;
784
+ if (!authConfig?.enabled) {
785
+ res.writeHead(400, { 'Content-Type': 'application/json' });
786
+ res.end(JSON.stringify({ success: false, error: 'Auth is not enabled.' }));
787
+ return;
788
+ }
789
+ let formData;
790
+ try {
791
+ formData = await parseFormBody(req);
792
+ }
793
+ catch {
794
+ res.writeHead(400, { 'Content-Type': 'application/json' });
795
+ res.end(JSON.stringify({ success: false, error: 'Invalid form body.' }));
796
+ return;
797
+ }
798
+ const stateParam = String(formData.state || '').trim();
799
+ const codeParam = String(formData.code || '').trim();
800
+ const errorParam = String(formData.error || '').trim();
801
+ const userJson = formData.user ? String(formData.user).trim() : null;
802
+ if (errorParam) {
803
+ auditAuthAction(req, 'auth-oauth-callback-denied', { provider: 'apple', error: errorParam });
804
+ res.writeHead(302, { Location: '/login?error=oauth_denied&provider=apple' });
805
+ res.end();
806
+ return;
807
+ }
808
+ if (!stateParam || !codeParam) {
809
+ res.writeHead(302, { Location: '/login?error=oauth_invalid&provider=apple' });
810
+ res.end();
811
+ return;
812
+ }
813
+ const stateRecord = core.consumeOAuthLoginState(stateParam);
814
+ if (!stateRecord || stateRecord.provider !== 'apple') {
815
+ auditAuthAction(req, 'auth-oauth-state-invalid', { provider: 'apple', stateProvided: true });
816
+ res.writeHead(302, { Location: '/login?error=oauth_state_invalid&provider=apple' });
817
+ res.end();
818
+ return;
819
+ }
820
+ const returnTo = sanitizeReturnTo(stateRecord.returnTo);
821
+ const oauthProviders = core.getGlobalSettings().oauthProviders || {};
822
+ const appleCfg = (oauthProviders.apple || {});
823
+ if (!appleCfg.servicesId || !appleCfg.teamId || !appleCfg.keyId || !appleCfg.privateKeyPem) {
824
+ auditAuthAction(req, 'auth-oauth-callback-error', { provider: 'apple', code: 'APPLE_NOT_CONFIGURED' });
825
+ res.writeHead(302, { Location: '/login?error=oauth_failed&provider=apple' });
826
+ res.end();
827
+ return;
828
+ }
829
+ const baseUrl = resolvePublicAuthBaseUrl(req);
830
+ const redirectUri = `${baseUrl}/api/taskforce/auth/oauth/apple/callback`;
831
+ try {
832
+ const profile = await exchangeAppleCode({
833
+ code: codeParam,
834
+ redirectUri,
835
+ servicesId: appleCfg.servicesId,
836
+ teamId: appleCfg.teamId,
837
+ keyId: appleCfg.keyId,
838
+ privateKeyPem: appleCfg.privateKeyPem,
839
+ userJson,
840
+ });
841
+ let userId;
842
+ let identityId;
843
+ let isNew = false;
844
+ if (stateRecord.flowMode === 'link') {
845
+ if (!stateRecord.userId) {
846
+ throw Object.assign(new Error('User ID missing from OAuth link state.'), { code: 'AUTH_REQUIRED', statusCode: 401 });
847
+ }
848
+ identityId = core.linkOAuthProviderToUser({
849
+ userId: stateRecord.userId,
850
+ provider: 'apple',
851
+ providerSubject: profile.providerSubject,
852
+ providerEmail: profile.email,
853
+ providerEmailVerified: profile.emailVerified,
854
+ displayNameSnapshot: profile.displayName,
855
+ avatarUrlSnapshot: profile.avatarUrl,
856
+ });
857
+ userId = stateRecord.userId;
858
+ isNew = false;
859
+ }
860
+ else if (stateRecord.flowMode === 'invite_claim') {
861
+ if (!stateRecord.inviteToken) {
862
+ throw Object.assign(new Error('Invite token missing from OAuth state.'), { code: 'INVITE_TOKEN_MISSING', statusCode: 400 });
863
+ }
864
+ const claimed = core.claimInviteWithOAuthProvider({
865
+ token: stateRecord.inviteToken,
866
+ provider: 'apple',
867
+ providerSubject: profile.providerSubject,
868
+ providerEmail: profile.email,
869
+ providerEmailVerified: profile.emailVerified,
870
+ displayNameSnapshot: profile.displayName,
871
+ avatarUrlSnapshot: profile.avatarUrl,
872
+ });
873
+ userId = claimed.userId;
874
+ identityId = claimed.identityId;
875
+ isNew = true;
876
+ }
877
+ else {
878
+ ({ userId, identityId, isNew } = resolveOAuthUser(profile));
879
+ }
880
+ const cookie = buildOAuthSessionCookie(req, authConfig, userId, identityId);
881
+ res.setHeader('Set-Cookie', cookie);
882
+ auditAuthAction(req, 'auth-oauth-login-success', {
883
+ provider: 'apple',
884
+ userId,
885
+ isNew,
886
+ flowMode: stateRecord.flowMode,
887
+ email: profile.email || null,
888
+ });
889
+ res.writeHead(302, { Location: returnTo });
890
+ res.end();
891
+ }
892
+ catch (err) {
893
+ const code = err?.code || 'OAUTH_ERROR';
894
+ auditAuthAction(req, 'auth-oauth-callback-error', { provider: 'apple', code, message: String(err?.message || '') });
895
+ if (code === 'REGISTRATION_DISABLED') {
896
+ res.writeHead(302, { Location: '/login?error=registration_disabled&provider=apple' });
897
+ }
898
+ else if (code === 'IDENTITY_ALREADY_EXISTS') {
899
+ res.writeHead(302, { Location: `${returnTo}?error=provider_already_linked&provider=apple` });
900
+ }
901
+ else if (code === 'INVITE_TOKEN_EXPIRED' || code === 'INVITE_TOKEN_REVOKED' || code === 'INVITE_TOKEN_USED' || code === 'INVITE_TOKEN_INVALID') {
902
+ res.writeHead(302, { Location: '/login?error=invite_invalid&provider=apple' });
903
+ }
904
+ else {
905
+ res.writeHead(302, { Location: '/login?error=oauth_failed&provider=apple' });
906
+ }
907
+ res.end();
908
+ }
909
+ });
910
+ // GET /api/taskforce/account/login-methods
911
+ // Returns the list of active login identities for the authenticated user.
912
+ addRoute('GET', '/api/taskforce/account/login-methods', async (req, res) => {
913
+ if (!requireCloudRuntime(res, 'Login methods'))
914
+ return;
915
+ const authConfig = context.authConfig;
916
+ if (!authConfig?.enabled || !hasValidSession(req, authConfig)) {
917
+ res.writeHead(401, { 'Content-Type': 'application/json' });
918
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
919
+ return;
920
+ }
921
+ const access = resolveRequestAccess(req, authConfig);
922
+ const userId = access?.userId || '';
923
+ if (!userId) {
924
+ res.writeHead(401, { 'Content-Type': 'application/json' });
925
+ res.end(JSON.stringify({ success: false, error: 'Unable to resolve user.' }));
926
+ return;
927
+ }
928
+ const identities = core.listLoginMethodsForUser(userId);
929
+ res.writeHead(200, { 'Content-Type': 'application/json' });
930
+ res.end(JSON.stringify({
931
+ success: true,
932
+ methods: identities.map(id => ({
933
+ provider: id.provider,
934
+ providerEmail: id.providerEmail || null,
935
+ displayNameSnapshot: id.displayNameSnapshot || null,
936
+ avatarUrlSnapshot: id.avatarUrlSnapshot || null,
937
+ linkedAt: id.linkedAt || id.createdAt,
938
+ lastLoginAt: id.lastLoginAt || null,
939
+ }))
940
+ }));
941
+ });
942
+ // DELETE /api/taskforce/account/login-methods/:provider
943
+ // Unlinks a provider identity from the authenticated user.
944
+ // Rejects with 409 if it would be the last active login method.
945
+ addRoute('DELETE', '/api/taskforce/account/login-methods/:provider', async (req, res, params) => {
946
+ if (!requireCloudRuntime(res, 'Login methods'))
947
+ return;
948
+ const authConfig = context.authConfig;
949
+ if (!authConfig?.enabled || !hasValidSession(req, authConfig)) {
950
+ res.writeHead(401, { 'Content-Type': 'application/json' });
951
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
952
+ return;
953
+ }
954
+ const access = resolveRequestAccess(req, authConfig);
955
+ const userId = access?.userId || '';
956
+ if (!userId) {
957
+ res.writeHead(401, { 'Content-Type': 'application/json' });
958
+ res.end(JSON.stringify({ success: false, error: 'Unable to resolve user.' }));
959
+ return;
960
+ }
961
+ const provider = String(params.provider || '').toLowerCase();
962
+ if (!provider) {
963
+ res.writeHead(400, { 'Content-Type': 'application/json' });
964
+ res.end(JSON.stringify({ success: false, error: 'Provider is required.' }));
965
+ return;
966
+ }
967
+ try {
968
+ core.unlinkLoginMethod(userId, provider);
969
+ auditAuthAction(req, 'account-login-method-unlinked', { userId, provider });
970
+ res.writeHead(200, { 'Content-Type': 'application/json' });
971
+ res.end(JSON.stringify({ success: true }));
972
+ }
973
+ catch (err) {
974
+ const code = err?.code || 'UNLINK_ERROR';
975
+ const status = code === 'LAST_IDENTITY_REQUIRED' ? 409 : resolveErrorStatusCode(err);
976
+ res.writeHead(status, { 'Content-Type': 'application/json' });
977
+ res.end(JSON.stringify({ success: false, error: err?.message || 'Unlink failed.', code }));
978
+ }
979
+ });
980
+ // POST /api/taskforce/account/login-methods/password
981
+ // Adds (or replaces) a password identity for the authenticated user.
982
+ // Social-only accounts use this to add a password login method.
983
+ addRoute('POST', '/api/taskforce/account/login-methods/password', async (req, res) => {
984
+ if (!requireCloudRuntime(res, 'Login methods'))
985
+ return;
986
+ const authConfig = context.authConfig;
987
+ if (!authConfig?.enabled || !hasValidSession(req, authConfig)) {
988
+ res.writeHead(401, { 'Content-Type': 'application/json' });
989
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
990
+ return;
991
+ }
992
+ const access = resolveRequestAccess(req, authConfig);
993
+ const userId = access?.userId || '';
994
+ if (!userId) {
995
+ res.writeHead(401, { 'Content-Type': 'application/json' });
996
+ res.end(JSON.stringify({ success: false, error: 'Unable to resolve user.' }));
997
+ return;
998
+ }
999
+ let body;
1000
+ try {
1001
+ body = await parseJsonBody(req);
1002
+ }
1003
+ catch {
1004
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1005
+ res.end(JSON.stringify({ success: false, error: 'Invalid JSON body.' }));
1006
+ return;
1007
+ }
1008
+ const password = String(body?.password || '');
1009
+ try {
1010
+ core.addPasswordToAccount(userId, password);
1011
+ auditAuthAction(req, 'account-password-added', { userId });
1012
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1013
+ res.end(JSON.stringify({ success: true }));
1014
+ }
1015
+ catch (err) {
1016
+ const code = err?.code || 'ADD_PASSWORD_ERROR';
1017
+ const status = resolveErrorStatusCode(err);
1018
+ res.writeHead(status, { 'Content-Type': 'application/json' });
1019
+ res.end(JSON.stringify({ success: false, error: err?.message || 'Failed to add password.', code }));
1020
+ }
1021
+ });
342
1022
  // GET /api/taskforce/auth/session - Return current auth/session state
343
1023
  addRoute('GET', '/api/taskforce/auth/session', async (req, res) => {
1024
+ const routeStartedAt = getRouteTimingNow();
1025
+ const traceId = requestBootstrapTraceId(req);
344
1026
  const authConfig = context.authConfig;
345
1027
  const access = authConfig ? resolveRequestAccess(req, authConfig) : null;
346
1028
  const authenticated = Boolean(authConfig && hasValidSession(req, authConfig));
1029
+ const authResolvedAt = getRouteTimingNow();
347
1030
  let role = access?.role || authConfig?.defaultRole || 'member';
348
1031
  let workspaceId = String(access?.workspaceId || authConfig?.defaultWorkspaceId || '').trim();
349
1032
  let userId = access?.userId || 'anonymous';
@@ -351,11 +1034,16 @@ export function registerAuthRoutes(deps) {
351
1034
  let displayName = null;
352
1035
  let avatarUrl = null;
353
1036
  let betaAccess = true;
1037
+ let betaAccessResolvedAt = authResolvedAt;
1038
+ let userAccessResolvedAt = authResolvedAt;
1039
+ let identityResolvedAt = authResolvedAt;
354
1040
  if (authenticated && access?.userId) {
355
1041
  // Keep the session endpoint fast on reload; avatar draft cleanup is best-effort housekeeping.
356
1042
  void cleanupStaleAvatarDrafts(access.userId).catch(() => { });
357
1043
  betaAccess = core.getUserBetaAccess(access.userId);
1044
+ betaAccessResolvedAt = getRouteTimingNow();
358
1045
  const resolved = core.resolveUserAccess(access.userId, workspaceId, role);
1046
+ userAccessResolvedAt = getRouteTimingNow();
359
1047
  if (resolved) {
360
1048
  role = resolved.role;
361
1049
  workspaceId = resolved.workspaceId;
@@ -370,9 +1058,9 @@ export function registerAuthRoutes(deps) {
370
1058
  email = identity?.email || null;
371
1059
  displayName = identity?.displayName || null;
372
1060
  avatarUrl = identity?.avatarUrl || null;
1061
+ identityResolvedAt = getRouteTimingNow();
373
1062
  }
374
- res.writeHead(200, { 'Content-Type': 'application/json' });
375
- res.end(JSON.stringify({
1063
+ const payload = {
376
1064
  authenticated,
377
1065
  userId,
378
1066
  workspaceId,
@@ -381,7 +1069,34 @@ export function registerAuthRoutes(deps) {
381
1069
  displayName,
382
1070
  avatarUrl,
383
1071
  betaAccess
384
- }));
1072
+ };
1073
+ const serialized = serializeJsonWithTiming(payload, identityResolvedAt);
1074
+ const totalDurationMs = getRouteTimingNow() - routeStartedAt;
1075
+ setServerTimingHeader(res, [
1076
+ { name: 'auth-session', durationMs: totalDurationMs },
1077
+ { name: 'auth-session-cookie', durationMs: authResolvedAt - routeStartedAt },
1078
+ { name: 'auth-session-beta-access', durationMs: betaAccessResolvedAt - authResolvedAt },
1079
+ { name: 'auth-session-user-access', durationMs: userAccessResolvedAt - betaAccessResolvedAt },
1080
+ { name: 'auth-session-identity', durationMs: identityResolvedAt - userAccessResolvedAt },
1081
+ { name: 'auth-session-json', durationMs: serialized.durationMs },
1082
+ ]);
1083
+ logSlowBootstrapRoute({
1084
+ route: '/api/taskforce/auth/session',
1085
+ workspaceId: workspaceId || access?.workspaceId || authConfig?.defaultWorkspaceId || 'default',
1086
+ traceId,
1087
+ durationMs: totalDurationMs,
1088
+ details: {
1089
+ authenticated,
1090
+ payloadBytes: serialized.byteLength,
1091
+ cookieMs: authResolvedAt - routeStartedAt,
1092
+ betaAccessMs: betaAccessResolvedAt - authResolvedAt,
1093
+ userAccessMs: userAccessResolvedAt - betaAccessResolvedAt,
1094
+ identityMs: identityResolvedAt - userAccessResolvedAt,
1095
+ serializeMs: serialized.durationMs,
1096
+ }
1097
+ });
1098
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1099
+ res.end(serialized.body);
385
1100
  });
386
1101
  // GET /api/taskforce/auth/admin-session - Return global admin auth/session state
387
1102
  addRoute('GET', '/api/taskforce/auth/admin-session', async (req, res) => {
@@ -537,6 +1252,63 @@ export function registerAuthRoutes(deps) {
537
1252
  path: `/api/taskforce/context/${encodeURIComponent(relativePath)}`
538
1253
  }));
539
1254
  });
1255
+ addRoute('POST', '/api/taskforce/auth/profile/avatar/upload', async (req, res) => {
1256
+ if (!requireCloudRuntime(res, 'Authentication'))
1257
+ return;
1258
+ const authConfig = context.authConfig;
1259
+ if (!authConfig) {
1260
+ res.writeHead(500, { 'Content-Type': 'application/json' });
1261
+ res.end(JSON.stringify({ success: false, error: 'Auth context unavailable.' }));
1262
+ return;
1263
+ }
1264
+ const access = resolveRequestAccess(req, authConfig);
1265
+ const userId = String(access.userId || '').trim();
1266
+ if (!access.authenticated || !userId || userId === 'anonymous') {
1267
+ res.writeHead(401, { 'Content-Type': 'application/json' });
1268
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
1269
+ return;
1270
+ }
1271
+ const draftId = String(requestHeader(req, 'x-taskforce-avatar-draft-id', '') || '').trim();
1272
+ if (!draftId) {
1273
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1274
+ res.end(JSON.stringify({ success: false, error: 'Avatar draft id is required.', code: 'AVATAR_DRAFT_REQUIRED' }));
1275
+ return;
1276
+ }
1277
+ await cleanupStaleAvatarDrafts(userId);
1278
+ const draft = core.getUserAvatarUploadDraft(userId, draftId);
1279
+ if (!draft || draft.discardedAt) {
1280
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1281
+ res.end(JSON.stringify({ success: false, error: 'Avatar draft is invalid or expired.', code: 'AVATAR_DRAFT_INVALID' }));
1282
+ return;
1283
+ }
1284
+ const objectStorageClient = getObjectStorageClient();
1285
+ const effectiveObjectStorage = resolveEffectiveObjectStorageConfig();
1286
+ if (!objectStorageClient || effectiveObjectStorage.provider !== 'r2') {
1287
+ res.writeHead(501, { 'Content-Type': 'application/json' });
1288
+ res.end(JSON.stringify({ success: false, error: 'Avatar uploads require object storage.' }));
1289
+ return;
1290
+ }
1291
+ try {
1292
+ const body = await parseBinaryBody(req, MAX_AVATAR_UPLOAD_BYTES);
1293
+ if (body.length === 0) {
1294
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1295
+ res.end(JSON.stringify({ success: false, error: 'Avatar upload body is empty.', code: 'AVATAR_UPLOAD_EMPTY' }));
1296
+ return;
1297
+ }
1298
+ await objectStorageClient.putObject(draft.storageKey, body, draft.mimeType || 'application/octet-stream');
1299
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1300
+ res.end(JSON.stringify({ success: true, draftId, relativePath: draft.storageKey }));
1301
+ }
1302
+ catch (error) {
1303
+ const statusCode = resolveErrorStatusCode(error, 400);
1304
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
1305
+ res.end(JSON.stringify({
1306
+ success: false,
1307
+ error: String(error?.message || 'Failed to upload avatar.'),
1308
+ code: error?.code
1309
+ }));
1310
+ }
1311
+ });
540
1312
  addRoute('POST', '/api/taskforce/auth/profile/avatar/finalize', async (req, res) => {
541
1313
  if (!requireCloudRuntime(res, 'Authentication'))
542
1314
  return;
@@ -708,10 +1480,11 @@ export function registerAuthRoutes(deps) {
708
1480
  };
709
1481
  }
710
1482
  }
711
- const cookie = createSessionTokenCookie(`pwd:${access.userId}`, authConfig, {
1483
+ const cookie = createSessionTokenCookie(`usr:${access.userId}`, authConfig, {
712
1484
  role: access.role,
713
1485
  workspaceId: access.workspaceId,
714
1486
  userId: access.userId,
1487
+ identityId: authResult.access?.identityId,
715
1488
  ...resolveSessionCookieAttributesForRequest(req, authConfig)
716
1489
  });
717
1490
  auditAuthAction(req, 'auth-login-success', {
@@ -1016,7 +1789,7 @@ export function registerAuthRoutes(deps) {
1016
1789
  const password = String(body?.password || '');
1017
1790
  if (!token || !password) {
1018
1791
  res.writeHead(400, { 'Content-Type': 'application/json' });
1019
- res.end(JSON.stringify({ success: false, error: 'Token and password are required.' }));
1792
+ res.end(JSON.stringify({ success: false, error: 'Token and password are required.', code: 'TOKEN_PASSWORD_REQUIRED' }));
1020
1793
  return;
1021
1794
  }
1022
1795
  try {
@@ -1041,7 +1814,7 @@ export function registerAuthRoutes(deps) {
1041
1814
  statusCode: resolveErrorStatusCode(error, 400)
1042
1815
  });
1043
1816
  res.writeHead(resolveErrorStatusCode(error, 400), { 'Content-Type': 'application/json' });
1044
- res.end(JSON.stringify({ success: false, error: String(error?.message || 'Unable to reset password') }));
1817
+ res.end(JSON.stringify({ success: false, error: String(error?.message || 'Unable to reset password'), code: error?.code }));
1045
1818
  }
1046
1819
  });
1047
1820
  // === SECTION: Invite flows ===
@@ -1065,6 +1838,18 @@ export function registerAuthRoutes(deps) {
1065
1838
  res.end(JSON.stringify({ success: false, state: details.state, error: 'Invalid or expired invite token.' }));
1066
1839
  return;
1067
1840
  }
1841
+ // Augment availableMethods with enabled OAuth providers for pending_setup invitees
1842
+ const oauthProviders = core.getGlobalSettings().oauthProviders || {};
1843
+ const enabledOAuthProviders = [];
1844
+ if (oauthProviders.google?.enabled)
1845
+ enabledOAuthProviders.push('google');
1846
+ if (oauthProviders.github?.enabled)
1847
+ enabledOAuthProviders.push('github');
1848
+ if (oauthProviders.apple?.enabled)
1849
+ enabledOAuthProviders.push('apple');
1850
+ const augmentedMethods = details.inviteeState === 'pending_setup'
1851
+ ? [...(details.availableMethods || []), ...enabledOAuthProviders.filter(p => !(details.availableMethods || []).includes(p))]
1852
+ : (details.availableMethods || []);
1068
1853
  auditAuthAction(req, 'auth-invite-opened', {
1069
1854
  userId: details.userId,
1070
1855
  email: details.email,
@@ -1078,6 +1863,8 @@ export function registerAuthRoutes(deps) {
1078
1863
  state: details.state,
1079
1864
  email: details.email,
1080
1865
  workspaceId: details.workspaceId,
1866
+ inviteeState: details.inviteeState,
1867
+ availableMethods: augmentedMethods,
1081
1868
  inviteeKind: details.inviteeKind,
1082
1869
  passwordRequired: details.passwordRequired === true
1083
1870
  }));
@@ -1107,10 +1894,11 @@ export function registerAuthRoutes(deps) {
1107
1894
  }
1108
1895
  try {
1109
1896
  const accepted = core.acceptInviteWithToken({ token, password });
1110
- const cookie = createSessionTokenCookie(`pwd:${accepted.userId}`, authConfig, {
1897
+ const cookie = createSessionTokenCookie(`usr:${accepted.userId}`, authConfig, {
1111
1898
  role: accepted.role,
1112
1899
  workspaceId: accepted.workspaceId,
1113
1900
  userId: accepted.userId,
1901
+ identityId: accepted.identityId,
1114
1902
  ...resolveSessionCookieAttributesForRequest(req, authConfig)
1115
1903
  });
1116
1904
  res.setHeader('Set-Cookie', cookie);
@@ -1163,11 +1951,12 @@ export function registerAuthRoutes(deps) {
1163
1951
  }
1164
1952
  try {
1165
1953
  const accepted = core.joinInviteWithAuthenticatedUser({ token, userId: access.userId });
1166
- const sessionId = String(access.sessionId || `pwd:${accepted.userId}`).trim() || `pwd:${accepted.userId}`;
1954
+ const sessionId = String(access.sessionId || `usr:${accepted.userId}`).trim() || `usr:${accepted.userId}`;
1167
1955
  const cookie = createSessionTokenCookie(sessionId, authConfig, {
1168
1956
  role: accepted.role,
1169
1957
  workspaceId: accepted.workspaceId,
1170
1958
  userId: accepted.userId,
1959
+ identityId: access.identityId || undefined,
1171
1960
  ...resolveSessionCookieAttributesForRequest(req, authConfig)
1172
1961
  });
1173
1962
  res.setHeader('Set-Cookie', cookie);
@@ -1236,7 +2025,7 @@ export function registerAuthRoutes(deps) {
1236
2025
  return;
1237
2026
  }
1238
2027
  try {
1239
- const userId = requestUserId(req);
2028
+ const userId = resolveAuthenticatedUserId(req);
1240
2029
  const payload = core.getAccountAccessStatus(userId);
1241
2030
  res.writeHead(200, { 'Content-Type': 'application/json' });
1242
2031
  res.end(JSON.stringify(payload));
@@ -1359,6 +2148,31 @@ export function registerAuthRoutes(deps) {
1359
2148
  res.end(JSON.stringify({ error: String(error?.message || 'Unable to build plan preview'), code: error?.code, details: error?.details }));
1360
2149
  }
1361
2150
  });
2151
+ // GET /api/taskforce/account/ai-profile-seat-usage - Read cloud-authoritative AI seat usage for the current workspace.
2152
+ addRoute('GET', '/api/taskforce/account/ai-profile-seat-usage', async (req, res) => {
2153
+ if (!ensureCloudPlanData(req, res))
2154
+ return;
2155
+ if (!ensureAuthenticatedUser(req)) {
2156
+ res.writeHead(401, { 'Content-Type': 'application/json' });
2157
+ res.end(JSON.stringify({ error: 'Authentication required.' }));
2158
+ return;
2159
+ }
2160
+ try {
2161
+ const wid = requestWorkspaceId(req);
2162
+ const aiProfileSeatUsage = core.countAiProfileUsage(wid);
2163
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2164
+ res.end(JSON.stringify({ aiProfileSeatUsage }));
2165
+ }
2166
+ catch (error) {
2167
+ const statusCode = resolveErrorStatusCode(error, 400);
2168
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
2169
+ res.end(JSON.stringify({
2170
+ error: String(error?.message || 'Unable to build AI profile seat usage'),
2171
+ code: error?.code,
2172
+ details: error?.details
2173
+ }));
2174
+ }
2175
+ });
1362
2176
  // === SECTION: Workspace team management (workspace admin) ===
1363
2177
  // GET /api/taskforce/auth/workspace-members - Auth-backed workspace member list for local runtime identity hydration.
1364
2178
  addRoute('GET', '/api/taskforce/auth/workspace-members', async (req, res) => {
@@ -1383,12 +2197,17 @@ export function registerAuthRoutes(deps) {
1383
2197
  res.end(JSON.stringify({ error: 'Authentication required.' }));
1384
2198
  return;
1385
2199
  }
2200
+ const startedAt = getRouteTimingNow();
1386
2201
  const wid = requestWorkspaceId(req);
2202
+ const traceId = requestBootstrapTraceId(req);
2203
+ const adminRoleStartedAt = getRouteTimingNow();
1387
2204
  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) => ({
2205
+ const adminRoleResolvedAt = getRouteTimingNow();
2206
+ const usersStartedAt = getRouteTimingNow();
2207
+ const activeWorkspaceUsers = core.listWorkspaceUsers(wid)
2208
+ .filter((user) => String(user.status || '').trim().toLowerCase() === 'active' && user.disabled !== true);
2209
+ const usersLoadedAt = getRouteTimingNow();
2210
+ const workspaceUsers = activeWorkspaceUsers.map((user) => ({
1392
2211
  value: user.userId,
1393
2212
  label: String(user.displayName || user.email || user.userId).trim() || user.userId,
1394
2213
  icon: 'User',
@@ -1397,11 +2216,15 @@ export function registerAuthRoutes(deps) {
1397
2216
  userId: user.userId,
1398
2217
  email: user.email,
1399
2218
  displayName: user.displayName ?? null,
2219
+ avatarUrl: user.avatarUrl ?? null,
1400
2220
  status: user.status,
1401
2221
  disabled: user.disabled === true
1402
2222
  }));
2223
+ const usersMappedAt = getRouteTimingNow();
2224
+ const aiProfileDiagnostics = { metadataBackfillCount: 0 };
2225
+ const aiProfilesStartedAt = getRouteTimingNow();
1403
2226
  const aiProfiles = core
1404
- .listAiProfiles(wid)
2227
+ .listAiProfiles(wid, { diagnostics: aiProfileDiagnostics })
1405
2228
  .map((profile) => ({
1406
2229
  value: profile.id,
1407
2230
  label: profile.name,
@@ -1409,19 +2232,90 @@ export function registerAuthRoutes(deps) {
1409
2232
  username: profile.username,
1410
2233
  icon: profile.icon,
1411
2234
  color: profile.color,
2235
+ avatarUrl: profile.avatarUrl ?? null,
2236
+ avatarSourceUrl: profile.avatarSourceUrl ?? null,
1412
2237
  kind: 'agent',
1413
2238
  description: profile.description ?? null,
1414
2239
  role: profile.role ?? null,
1415
2240
  provider: profile.provider ?? null,
1416
2241
  model: profile.model ?? null,
1417
2242
  surfaceType: profile.surfaceType ?? null,
2243
+ seatScope: profile.seatScope ?? null,
2244
+ archivedAt: profile.archivedAt ?? null,
1418
2245
  createdAt: profile.createdAt,
1419
2246
  updatedAt: profile.updatedAt,
1420
2247
  lastActiveAt: profile.lastActiveAt ?? null
1421
2248
  }));
2249
+ const aiProfilesLoadedAt = getRouteTimingNow();
2250
+ const seatUsageStartedAt = getRouteTimingNow();
2251
+ const aiProfileSeatUsage = core.countAiProfileUsage(wid);
2252
+ const seatUsageLoadedAt = getRouteTimingNow();
1422
2253
  const assignees = [...workspaceUsers, ...aiProfiles];
2254
+ const serializeStartedAt = getRouteTimingNow();
2255
+ const serialized = serializeJsonWithTiming({ assignees, aiProfileSeatUsage }, serializeStartedAt);
2256
+ const totalDurationMs = Math.max(0, getRouteTimingNow() - startedAt);
2257
+ setServerTimingHeader(res, [
2258
+ { name: 'assignee-options', durationMs: totalDurationMs },
2259
+ { name: 'assignee-options-admin-role', durationMs: adminRoleResolvedAt - adminRoleStartedAt },
2260
+ { name: 'assignee-options-users', durationMs: usersLoadedAt - usersStartedAt },
2261
+ { name: 'assignee-options-users-map', durationMs: usersMappedAt - usersLoadedAt },
2262
+ { name: 'assignee-options-ai-profiles', durationMs: aiProfilesLoadedAt - aiProfilesStartedAt },
2263
+ { name: 'assignee-options-seat-usage', durationMs: seatUsageLoadedAt - seatUsageStartedAt },
2264
+ { name: 'assignee-options-json', durationMs: serialized.durationMs },
2265
+ ]);
2266
+ logSlowBootstrapRoute({
2267
+ route: '/api/taskforce/workspace/assignee-options',
2268
+ workspaceId: wid,
2269
+ traceId,
2270
+ durationMs: totalDurationMs,
2271
+ details: {
2272
+ humanAssigneeCount: workspaceUsers.length,
2273
+ workspaceUserCount: activeWorkspaceUsers.length,
2274
+ aiProfileCount: aiProfiles.length,
2275
+ metadataBackfillCount: aiProfileDiagnostics.metadataBackfillCount,
2276
+ responseBytes: serialized.byteLength,
2277
+ adminRoleMs: Math.max(0, adminRoleResolvedAt - adminRoleStartedAt),
2278
+ listWorkspaceUsersMs: Math.max(0, usersLoadedAt - usersStartedAt),
2279
+ workspaceUsersMapMs: Math.max(0, usersMappedAt - usersLoadedAt),
2280
+ listAiProfilesMs: Math.max(0, aiProfilesLoadedAt - aiProfilesStartedAt),
2281
+ countAiProfileUsageMs: Math.max(0, seatUsageLoadedAt - seatUsageStartedAt),
2282
+ jsonSerializeMs: serialized.durationMs,
2283
+ },
2284
+ });
1423
2285
  res.writeHead(200, { 'Content-Type': 'application/json' });
1424
- res.end(JSON.stringify({ assignees }));
2286
+ res.end(serialized.body);
2287
+ });
2288
+ // POST /api/taskforce/workspace/ai-profiles/avatar - Assign or clear an AI profile avatar image.
2289
+ addRoute('POST', '/api/taskforce/workspace/ai-profiles/avatar', async (req, res) => {
2290
+ if (!ensureWorkspaceAdminRole(req)) {
2291
+ res.writeHead(403, { 'Content-Type': 'application/json' });
2292
+ res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
2293
+ return;
2294
+ }
2295
+ const body = await parseJsonBody(req);
2296
+ const wid = requestWorkspaceId(req);
2297
+ const profileId = String(body?.profileId || '').trim();
2298
+ const avatarUrl = body?.avatarUrl === null ? null : String(body?.avatarUrl || '').trim();
2299
+ const avatarSourceUrl = body?.avatarSourceUrl === undefined
2300
+ ? undefined
2301
+ : body?.avatarSourceUrl === null
2302
+ ? null
2303
+ : String(body?.avatarSourceUrl || '').trim();
2304
+ if (!profileId) {
2305
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2306
+ res.end(JSON.stringify({ error: 'profileId is required.' }));
2307
+ return;
2308
+ }
2309
+ try {
2310
+ const profile = core.updateAiProfileAvatar({ workspaceId: wid, profileId, avatarUrl, avatarSourceUrl });
2311
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2312
+ res.end(JSON.stringify({ success: true, profile }));
2313
+ }
2314
+ catch (err) {
2315
+ const status = err?.statusCode ?? 500;
2316
+ res.writeHead(status, { 'Content-Type': 'application/json' });
2317
+ res.end(JSON.stringify({ error: err?.message ?? 'Failed to update AI profile avatar.' }));
2318
+ }
1425
2319
  });
1426
2320
  // POST /api/taskforce/workspace/ai-profiles/merge - Merge a duplicate AI profile into another, reassigning all references.
1427
2321
  addRoute('POST', '/api/taskforce/workspace/ai-profiles/merge', async (req, res) => {
@@ -1450,6 +2344,41 @@ export function registerAuthRoutes(deps) {
1450
2344
  res.end(JSON.stringify({ error: err?.message ?? 'Merge failed.' }));
1451
2345
  }
1452
2346
  });
2347
+ // POST /api/taskforce/workspace/ai-profiles/archive - Remove an AI profile from the active roster without deleting its history.
2348
+ addRoute('POST', '/api/taskforce/workspace/ai-profiles/archive', async (req, res) => {
2349
+ if (!ensureWorkspaceAdminRole(req)) {
2350
+ res.writeHead(403, { 'Content-Type': 'application/json' });
2351
+ res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
2352
+ return;
2353
+ }
2354
+ const body = await parseJsonBody(req);
2355
+ const wid = requestWorkspaceId(req);
2356
+ const profileId = String(body.profileId || '').trim();
2357
+ const reason = String(body.reason || '').trim() || 'manual_archive';
2358
+ if (!profileId) {
2359
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2360
+ res.end(JSON.stringify({ error: 'profileId is required.' }));
2361
+ return;
2362
+ }
2363
+ try {
2364
+ const profile = core.archiveAiProfile({
2365
+ workspaceId: wid,
2366
+ profileId,
2367
+ archivedBy: requestUserId(req),
2368
+ reason
2369
+ });
2370
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2371
+ res.end(JSON.stringify({ profile }));
2372
+ }
2373
+ catch (err) {
2374
+ const status = err?.statusCode ?? 500;
2375
+ res.writeHead(status, { 'Content-Type': 'application/json' });
2376
+ res.end(JSON.stringify({
2377
+ error: err?.message ?? 'Failed to archive AI profile.',
2378
+ details: err?.details ?? null
2379
+ }));
2380
+ }
2381
+ });
1453
2382
  // GET /api/taskforce/admin/users - List workspace users for team management.
1454
2383
  addRoute('GET', '/api/taskforce/admin/users', async (req, res) => {
1455
2384
  if (!ensureWorkspaceAdminRole(req)) {
@@ -1458,8 +2387,9 @@ export function registerAuthRoutes(deps) {
1458
2387
  return;
1459
2388
  }
1460
2389
  const wid = requestWorkspaceId(req);
2390
+ const actorUserId = resolveAuthenticatedUserId(req);
1461
2391
  const gate = core.evaluateFeatureAccess({
1462
- userId: requestUserId(req),
2392
+ userId: actorUserId,
1463
2393
  workspaceId: wid,
1464
2394
  featureKey: 'collaboration.team_management'
1465
2395
  });
@@ -1474,13 +2404,14 @@ export function registerAuthRoutes(deps) {
1474
2404
  });
1475
2405
  // POST /api/taskforce/admin/users/invite - Invite a user into a workspace.
1476
2406
  addRoute('POST', '/api/taskforce/admin/users/invite', async (req, res) => {
1477
- if (!ensureWorkspaceAdminRole(req)) {
2407
+ const body = await parseJsonBody(req);
2408
+ const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
2409
+ if (!ensureWorkspaceAdminRole(req, wid)) {
1478
2410
  res.writeHead(403, { 'Content-Type': 'application/json' });
1479
2411
  res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
1480
2412
  return;
1481
2413
  }
1482
- const body = await parseJsonBody(req);
1483
- const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
2414
+ const actorUserId = resolveAuthenticatedUserId(req);
1484
2415
  const email = String(body?.email || '').trim().toLowerCase();
1485
2416
  const role = String(body?.role || 'member').trim().toLowerCase();
1486
2417
  const permissionMode = String(body?.permissionMode || 'read-write').trim().toLowerCase();
@@ -1490,7 +2421,7 @@ export function registerAuthRoutes(deps) {
1490
2421
  return;
1491
2422
  }
1492
2423
  const inviteGate = core.evaluateFeatureAccess({
1493
- userId: requestUserId(req),
2424
+ userId: actorUserId,
1494
2425
  workspaceId: wid,
1495
2426
  featureKey: 'collaboration.invites'
1496
2427
  });
@@ -1513,8 +2444,8 @@ export function registerAuthRoutes(deps) {
1513
2444
  let inviteEmailError = null;
1514
2445
  if (inviteToken.token) {
1515
2446
  const workspace = core.getWorkspaceProfile(wid);
1516
- const actorIdentity = core.resolveUserIdentity(requestUserId(req));
1517
- const inviterEmailOrLabel = String(actorIdentity?.email || requestUserId(req) || 'Taskforce').trim();
2447
+ const actorIdentity = core.resolveUserIdentity(actorUserId);
2448
+ const inviterEmailOrLabel = String(actorIdentity?.email || actorUserId || 'Taskforce').trim();
1518
2449
  const delivery = await deliverInviteEmail(email, inviteToken.token, req, {
1519
2450
  workspaceName: String(workspace?.name || wid),
1520
2451
  inviterEmailOrLabel
@@ -1529,7 +2460,7 @@ export function registerAuthRoutes(deps) {
1529
2460
  role,
1530
2461
  permissionMode: role === 'member' ? (permissionMode === 'read-only' ? 'read-only' : 'read-write') : 'read-write',
1531
2462
  inviteEmailSent
1532
- });
2463
+ }, wid);
1533
2464
  res.writeHead(200, { 'Content-Type': 'application/json' });
1534
2465
  res.end(JSON.stringify({
1535
2466
  success: true,
@@ -1546,13 +2477,14 @@ export function registerAuthRoutes(deps) {
1546
2477
  });
1547
2478
  // PATCH /api/taskforce/admin/users/:id/role - Update workspace role for a user.
1548
2479
  addRoute('PATCH', '/api/taskforce/admin/users/:id/role', async (req, res, params) => {
1549
- if (!ensureWorkspaceAdminRole(req)) {
2480
+ const body = await parseJsonBody(req);
2481
+ const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
2482
+ if (!ensureWorkspaceAdminRole(req, wid)) {
1550
2483
  res.writeHead(403, { 'Content-Type': 'application/json' });
1551
2484
  res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
1552
2485
  return;
1553
2486
  }
1554
- const body = await parseJsonBody(req);
1555
- const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
2487
+ const actorUserId = resolveAuthenticatedUserId(req);
1556
2488
  const role = String(body?.role || '').trim().toLowerCase();
1557
2489
  if (role !== 'owner' && role !== 'admin' && role !== 'member') {
1558
2490
  res.writeHead(400, { 'Content-Type': 'application/json' });
@@ -1560,7 +2492,7 @@ export function registerAuthRoutes(deps) {
1560
2492
  return;
1561
2493
  }
1562
2494
  const gate = core.evaluateFeatureAccess({
1563
- userId: requestUserId(req),
2495
+ userId: actorUserId,
1564
2496
  workspaceId: wid,
1565
2497
  featureKey: 'collaboration.team_management'
1566
2498
  });
@@ -1576,7 +2508,7 @@ export function registerAuthRoutes(deps) {
1576
2508
  res.end(JSON.stringify({ error: 'User membership not found.' }));
1577
2509
  return;
1578
2510
  }
1579
- auditAuthAction(req, 'update-workspace-user-role', { workspaceId: wid, targetUserId: params.id, role });
2511
+ auditAuthAction(req, 'update-workspace-user-role', { workspaceId: wid, targetUserId: params.id, role }, wid);
1580
2512
  res.writeHead(200, { 'Content-Type': 'application/json' });
1581
2513
  res.end(JSON.stringify({ success: true }));
1582
2514
  }
@@ -1588,13 +2520,14 @@ export function registerAuthRoutes(deps) {
1588
2520
  });
1589
2521
  // PATCH /api/taskforce/admin/users/:id/permission-mode - Update member permission mode.
1590
2522
  addRoute('PATCH', '/api/taskforce/admin/users/:id/permission-mode', async (req, res, params) => {
1591
- if (!ensureWorkspaceAdminRole(req)) {
2523
+ const body = await parseJsonBody(req);
2524
+ const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
2525
+ if (!ensureWorkspaceAdminRole(req, wid)) {
1592
2526
  res.writeHead(403, { 'Content-Type': 'application/json' });
1593
2527
  res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
1594
2528
  return;
1595
2529
  }
1596
- const body = await parseJsonBody(req);
1597
- const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
2530
+ const actorUserId = resolveAuthenticatedUserId(req);
1598
2531
  const mode = String(body?.mode || '').trim().toLowerCase();
1599
2532
  if (mode !== 'read-only' && mode !== 'read-write') {
1600
2533
  res.writeHead(400, { 'Content-Type': 'application/json' });
@@ -1602,7 +2535,7 @@ export function registerAuthRoutes(deps) {
1602
2535
  return;
1603
2536
  }
1604
2537
  const gate = core.evaluateFeatureAccess({
1605
- userId: requestUserId(req),
2538
+ userId: actorUserId,
1606
2539
  workspaceId: wid,
1607
2540
  featureKey: 'collaboration.team_management'
1608
2541
  });
@@ -1618,7 +2551,7 @@ export function registerAuthRoutes(deps) {
1618
2551
  res.end(JSON.stringify({ error: 'User membership not found.' }));
1619
2552
  return;
1620
2553
  }
1621
- auditAuthAction(req, 'update-workspace-user-permission-mode', { workspaceId: wid, targetUserId: params.id, mode });
2554
+ auditAuthAction(req, 'update-workspace-user-permission-mode', { workspaceId: wid, targetUserId: params.id, mode }, wid);
1622
2555
  res.writeHead(200, { 'Content-Type': 'application/json' });
1623
2556
  res.end(JSON.stringify({ success: true }));
1624
2557
  }
@@ -1630,16 +2563,17 @@ export function registerAuthRoutes(deps) {
1630
2563
  });
1631
2564
  // PATCH /api/taskforce/admin/users/:id/disable - Disable or reactivate a workspace user.
1632
2565
  addRoute('PATCH', '/api/taskforce/admin/users/:id/disable', async (req, res, params) => {
1633
- if (!ensureWorkspaceAdminRole(req)) {
2566
+ const body = await parseJsonBody(req);
2567
+ const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
2568
+ if (!ensureWorkspaceAdminRole(req, wid)) {
1634
2569
  res.writeHead(403, { 'Content-Type': 'application/json' });
1635
2570
  res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
1636
2571
  return;
1637
2572
  }
1638
- const body = await parseJsonBody(req);
1639
- const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
2573
+ const actorUserId = resolveAuthenticatedUserId(req);
1640
2574
  const disabled = body?.disabled === true;
1641
2575
  const gate = core.evaluateFeatureAccess({
1642
- userId: requestUserId(req),
2576
+ userId: actorUserId,
1643
2577
  workspaceId: wid,
1644
2578
  featureKey: 'collaboration.team_management'
1645
2579
  });
@@ -1655,7 +2589,7 @@ export function registerAuthRoutes(deps) {
1655
2589
  res.end(JSON.stringify({ error: 'User membership not found.' }));
1656
2590
  return;
1657
2591
  }
1658
- auditAuthAction(req, 'set-workspace-user-disabled', { workspaceId: wid, targetUserId: params.id, disabled });
2592
+ auditAuthAction(req, 'set-workspace-user-disabled', { workspaceId: wid, targetUserId: params.id, disabled }, wid);
1659
2593
  res.writeHead(200, { 'Content-Type': 'application/json' });
1660
2594
  res.end(JSON.stringify({ success: true }));
1661
2595
  }
@@ -1667,15 +2601,16 @@ export function registerAuthRoutes(deps) {
1667
2601
  });
1668
2602
  // POST /api/taskforce/admin/users/:id/invite/revoke - Revoke invite acceptance token.
1669
2603
  addRoute('POST', '/api/taskforce/admin/users/:id/invite/revoke', async (req, res, params) => {
1670
- if (!ensureWorkspaceAdminRole(req)) {
2604
+ const body = await parseJsonBody(req);
2605
+ const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
2606
+ if (!ensureWorkspaceAdminRole(req, wid)) {
1671
2607
  res.writeHead(403, { 'Content-Type': 'application/json' });
1672
2608
  res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
1673
2609
  return;
1674
2610
  }
1675
- const body = await parseJsonBody(req);
1676
- const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
2611
+ const actorUserId = resolveAuthenticatedUserId(req);
1677
2612
  const gate = core.evaluateFeatureAccess({
1678
- userId: requestUserId(req),
2613
+ userId: actorUserId,
1679
2614
  workspaceId: wid,
1680
2615
  featureKey: 'collaboration.team_management'
1681
2616
  });
@@ -1685,21 +2620,22 @@ export function registerAuthRoutes(deps) {
1685
2620
  return;
1686
2621
  }
1687
2622
  const revoked = core.revokeInviteAcceptanceForUser(params.id);
1688
- auditAuthAction(req, 'revoke-invite', { workspaceId: wid, targetUserId: params.id, revoked: revoked.revoked });
2623
+ auditAuthAction(req, 'revoke-invite', { workspaceId: wid, targetUserId: params.id, revoked: revoked.revoked }, wid);
1689
2624
  res.writeHead(200, { 'Content-Type': 'application/json' });
1690
2625
  res.end(JSON.stringify({ success: true, revoked: revoked.revoked }));
1691
2626
  });
1692
2627
  // DELETE /api/taskforce/admin/users/:id - Remove workspace membership.
1693
2628
  addRoute('DELETE', '/api/taskforce/admin/users/:id', async (req, res, params) => {
1694
- if (!ensureWorkspaceAdminRole(req)) {
2629
+ const reqUrl = new URL(req.url || '/api/taskforce/admin/users/:id', 'http://localhost');
2630
+ const wid = String(reqUrl.searchParams.get('workspaceId') || requestWorkspaceId(req)).trim() || 'default';
2631
+ if (!ensureWorkspaceAdminRole(req, wid)) {
1695
2632
  res.writeHead(403, { 'Content-Type': 'application/json' });
1696
2633
  res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
1697
2634
  return;
1698
2635
  }
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';
2636
+ const actorUserId = resolveAuthenticatedUserId(req);
1701
2637
  const gate = core.evaluateFeatureAccess({
1702
- userId: requestUserId(req),
2638
+ userId: actorUserId,
1703
2639
  workspaceId: wid,
1704
2640
  featureKey: 'collaboration.team_management'
1705
2641
  });
@@ -1715,7 +2651,7 @@ export function registerAuthRoutes(deps) {
1715
2651
  res.end(JSON.stringify({ error: 'User membership not found.' }));
1716
2652
  return;
1717
2653
  }
1718
- auditAuthAction(req, 'remove-workspace-user', { workspaceId: wid, targetUserId: params.id });
2654
+ auditAuthAction(req, 'remove-workspace-user', { workspaceId: wid, targetUserId: params.id }, wid);
1719
2655
  res.writeHead(200, { 'Content-Type': 'application/json' });
1720
2656
  res.end(JSON.stringify({ success: true }));
1721
2657
  }
@@ -1733,8 +2669,9 @@ export function registerAuthRoutes(deps) {
1733
2669
  return;
1734
2670
  }
1735
2671
  const wid = requestWorkspaceId(req);
2672
+ const actorUserId = resolveAuthenticatedUserId(req);
1736
2673
  const gate = core.evaluateFeatureAccess({
1737
- userId: requestUserId(req),
2674
+ userId: actorUserId,
1738
2675
  workspaceId: wid,
1739
2676
  featureKey: 'collaboration.team_management'
1740
2677
  });