@taskforcehq/taskforce 0.3.304 → 0.3.306

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (383) hide show
  1. package/README.md +140 -247
  2. package/dist/Taskforce.module.css +295 -89
  3. package/dist/TaskforceCore.js +378 -111
  4. package/dist/TaskforceCore.test.js +1221 -107
  5. package/dist/assets/fonts/CourierPrime-Bold.woff2 +0 -0
  6. package/dist/assets/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  7. package/dist/assets/fonts/CourierPrime-Italic.woff2 +0 -0
  8. package/dist/assets/fonts/CourierPrime-Regular.woff2 +0 -0
  9. package/dist/assets/fonts/Noir_medium.woff2 +0 -0
  10. package/dist/assets/fonts/Noir_regular.woff2 +0 -0
  11. package/dist/assets/fonts/RussoOne-Regular.woff2 +0 -0
  12. package/dist/assets/fonts/SpecialElite-Regular.woff2 +0 -0
  13. package/dist/compat/workspaceSyncCompat.js +52 -2
  14. package/dist/compat/workspaceSyncCompat.test.js +38 -1
  15. package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
  16. package/dist/components/context/ContextAttachmentManager.js +549 -0
  17. package/dist/components/features/AgentsModule.d.ts +10 -1
  18. package/dist/components/features/AgentsModule.js +260 -15
  19. package/dist/components/features/AgentsModule.test.js +255 -5
  20. package/dist/components/features/DocumentIndex.d.ts +1 -1
  21. package/dist/components/features/DocumentIndex.js +1 -1
  22. package/dist/components/features/DocumentViewer.d.ts +6 -2
  23. package/dist/components/features/DocumentViewer.js +90 -10
  24. package/dist/components/features/DocumentWorkspace.d.ts +5 -22
  25. package/dist/components/features/DocumentWorkspace.js +12 -155
  26. package/dist/components/features/DocumentWorkspace.test.js +226 -6
  27. package/dist/components/features/TaskSettings.js +29 -3
  28. package/dist/components/features/TaskSettings.test.js +42 -1
  29. package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
  30. package/dist/components/features/documentWorkspaceModel.js +57 -0
  31. package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
  32. package/dist/components/features/useDocumentWorkspaceController.js +113 -0
  33. package/dist/components/task/TaskCard.js +19 -28
  34. package/dist/components/task/TaskCard.test.js +38 -3
  35. package/dist/components/task/TaskContextUpload.js +4 -526
  36. package/dist/components/task/TaskForm.d.ts +8 -8
  37. package/dist/components/task/TaskForm.js +362 -230
  38. package/dist/components/task/TaskForm.test.js +558 -381
  39. package/dist/components/task/TaskKanban.d.ts +2 -1
  40. package/dist/components/task/TaskKanban.js +96 -14
  41. package/dist/components/task/TaskKanban.test.js +36 -0
  42. package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
  43. package/dist/components/ui/EditableAvatarButton.js +18 -0
  44. package/dist/components/views/PlanComparisonPage.d.ts +9 -1
  45. package/dist/components/views/PlanComparisonPage.js +70 -17
  46. package/dist/components/views/PlanComparisonPage.test.js +289 -41
  47. package/dist/components/views/PlansPage.js +239 -106
  48. package/dist/components/views/PlansPage.test.js +593 -42
  49. package/dist/components/views/StandaloneLayout.js +310 -554
  50. package/dist/components/views/panels/FilterToolbar.test.js +6 -4
  51. package/dist/components/views/panels/PlanningDrawer.d.ts +8 -1
  52. package/dist/components/views/panels/PlanningDrawer.js +18 -11
  53. package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
  54. package/dist/components/views/panels/PlanningDrawer.test.js +141 -0
  55. package/dist/components/views/planningScope.d.ts +20 -0
  56. package/dist/components/views/planningScope.js +25 -0
  57. package/dist/components/views/planningScope.test.d.ts +1 -0
  58. package/dist/components/views/planningScope.test.js +60 -0
  59. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +27 -14
  60. package/dist/components/views/standalone/modals/AccountHubModal.js +87 -15
  61. package/dist/components/views/standalone/modals/AccountHubModal.test.js +42 -2
  62. package/dist/components/views/standalone/modals/AvatarImageManagerModal.d.ts +20 -0
  63. package/dist/components/views/standalone/modals/AvatarImageManagerModal.js +127 -0
  64. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +2 -9
  65. package/dist/components/views/standalone/modals/EditProfileModal.js +3 -2
  66. package/dist/components/views/standalone/modals/SyncEnableWarningModal.d.ts +9 -0
  67. package/dist/components/views/standalone/modals/SyncEnableWarningModal.js +6 -0
  68. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +1 -0
  69. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +24 -0
  70. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +2 -1
  71. package/dist/components/views/standalone/modals/SyncStatusModal.js +2 -2
  72. package/dist/config/envSchema.js +1 -1
  73. package/dist/core/AiProfileService.d.ts +45 -3
  74. package/dist/core/AiProfileService.js +774 -63
  75. package/dist/core/AiProfileService.test.js +22 -2
  76. package/dist/core/AiProfiles.test.js +761 -2
  77. package/dist/core/AttachmentLinkService.js +57 -15
  78. package/dist/core/AuthIdentityService.d.ts +107 -0
  79. package/dist/core/AuthIdentityService.js +284 -0
  80. package/dist/core/AuthTokenService.d.ts +4 -0
  81. package/dist/core/AuthTokenService.js +34 -6
  82. package/dist/core/AuthTokenService.test.d.ts +1 -0
  83. package/dist/core/AuthTokenService.test.js +78 -0
  84. package/dist/core/EntitlementsPolicy.test.js +77 -15
  85. package/dist/core/GlobalSettingsService.js +115 -6
  86. package/dist/core/PlanEntitlementService.d.ts +30 -3
  87. package/dist/core/PlanEntitlementService.js +454 -93
  88. package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
  89. package/dist/core/PlanFeatureCatalog.test.js +84 -0
  90. package/dist/core/PlanVersionPolicy.test.js +54 -28
  91. package/dist/core/PlanningEntities.test.js +52 -0
  92. package/dist/core/SignupMonetizationSettings.test.js +241 -44
  93. package/dist/core/SyncReconciliationService.js +2 -65
  94. package/dist/core/SystemAdmin.test.js +244 -88
  95. package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
  96. package/dist/core/TaskTaxonomyValidation.test.js +53 -0
  97. package/dist/core/Taskforce.d.ts +228 -14
  98. package/dist/core/Taskforce.js +814 -201
  99. package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
  100. package/dist/core/Taskforce.listTasksSlim.test.js +91 -0
  101. package/dist/core/UserProfileAvatarDrafts.test.js +30 -3
  102. package/dist/core/WorkspaceLifecycleService.js +0 -5
  103. package/dist/core/WorkspacePermissions.test.js +25 -1
  104. package/dist/core/WorkspacePlanMode.test.js +128 -27
  105. package/dist/core/shared.d.ts +3 -1
  106. package/dist/core/shared.js +16 -1
  107. package/dist/core/types.d.ts +68 -2
  108. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +26 -1
  109. package/dist/hooks/sync/auth.js +3 -1
  110. package/dist/hooks/sync/bootstrap.js +2 -0
  111. package/dist/hooks/sync/controlPlane.d.ts +37 -0
  112. package/dist/hooks/sync/controlPlane.js +78 -0
  113. package/dist/hooks/sync/controlPlane.test.d.ts +1 -0
  114. package/dist/hooks/sync/controlPlane.test.js +121 -0
  115. package/dist/hooks/sync/lifecyclePolicy.d.ts +45 -0
  116. package/dist/hooks/sync/lifecyclePolicy.js +93 -0
  117. package/dist/hooks/sync/lifecyclePolicy.test.d.ts +1 -0
  118. package/dist/hooks/sync/lifecyclePolicy.test.js +124 -0
  119. package/dist/hooks/sync/orchestratorShared.d.ts +10 -1
  120. package/dist/hooks/sync/orchestratorShared.js +68 -47
  121. package/dist/hooks/sync/orchestratorShared.test.js +50 -1
  122. package/dist/hooks/sync/pullLifecycle.d.ts +31 -0
  123. package/dist/hooks/sync/pullLifecycle.js +24 -0
  124. package/dist/hooks/sync/pullLifecycle.test.d.ts +1 -0
  125. package/dist/hooks/sync/pullLifecycle.test.js +80 -0
  126. package/dist/hooks/sync/recovery.d.ts +16 -2
  127. package/dist/hooks/sync/recovery.js +171 -53
  128. package/dist/hooks/sync/recovery.test.d.ts +1 -0
  129. package/dist/hooks/sync/recovery.test.js +292 -0
  130. package/dist/hooks/sync/transfers.d.ts +16 -3
  131. package/dist/hooks/sync/transfers.js +186 -65
  132. package/dist/hooks/sync/transfers.test.d.ts +1 -0
  133. package/dist/hooks/sync/transfers.test.js +396 -0
  134. package/dist/hooks/sync/useRecentSyncEvents.d.ts +21 -0
  135. package/dist/hooks/sync/useRecentSyncEvents.js +92 -0
  136. package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +1 -0
  137. package/dist/hooks/sync/useRecentSyncEvents.test.js +124 -0
  138. package/dist/hooks/sync/useSyncStatusActions.d.ts +30 -0
  139. package/dist/hooks/sync/useSyncStatusActions.js +88 -0
  140. package/dist/hooks/sync/useSyncStatusActions.test.d.ts +1 -0
  141. package/dist/hooks/sync/useSyncStatusActions.test.js +133 -0
  142. package/dist/hooks/sync/useSyncStatusControls.d.ts +25 -0
  143. package/dist/hooks/sync/useSyncStatusControls.js +60 -0
  144. package/dist/hooks/sync/useSyncStatusControls.test.d.ts +1 -0
  145. package/dist/hooks/sync/useSyncStatusControls.test.js +88 -0
  146. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +12 -0
  147. package/dist/hooks/useSyncOrchestrator.d.ts +16 -6
  148. package/dist/hooks/useSyncOrchestrator.js +424 -175
  149. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +487 -10
  150. package/dist/hooks/useTaskData.js +8 -3
  151. package/dist/hooks/useTaskData.test.js +45 -0
  152. package/dist/hooks/useTaskMutations.d.ts +2 -1
  153. package/dist/hooks/useTaskMutations.js +12 -1
  154. package/dist/hooks/useTaskMutations.test.js +30 -1
  155. package/dist/hooks/useTaskforce.d.ts +65 -4
  156. package/dist/hooks/useTaskforce.js +564 -243
  157. package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
  158. package/dist/hooks/useTaskforce.runtime-routing.test.js +152 -0
  159. package/dist/hooks/useTaskforce.sync-behavior.test.js +2454 -208
  160. package/dist/hooks/useWorkspaceSyncController.d.ts +4 -1
  161. package/dist/hooks/useWorkspaceSyncController.js +17 -2
  162. package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
  163. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
  164. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +316 -35
  165. package/dist/localization/locales/en-US.d.ts +0 -1
  166. package/dist/localization/locales/en-US.js +0 -1
  167. package/dist/localization/locales/es-419.js +0 -1
  168. package/dist/localization/locales/pt-BR.js +0 -1
  169. package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
  170. package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
  171. package/dist/mcp/collaborationRegistrar.d.ts +2 -0
  172. package/dist/mcp/collaborationRegistrar.js +71 -0
  173. package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
  174. package/dist/mcp/documentAssetRegistrar.js +346 -0
  175. package/dist/mcp/runtime.d.ts +2 -0
  176. package/dist/mcp/runtime.js +2433 -3534
  177. package/dist/mcp/runtime.test.js +440 -2
  178. package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
  179. package/dist/mcp/taskPlanningRegistrar.js +418 -0
  180. package/dist/mcp/toolCatalog.d.ts +35 -0
  181. package/dist/mcp/toolCatalog.js +3 -0
  182. package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
  183. package/dist/migrations/taskSchemaMigrations.js +171 -61
  184. package/dist/migrations/taskSchemaMigrations.test.js +15 -0
  185. package/dist/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  186. package/dist/resources/templates/workflows/collaborate.yaml +1 -1
  187. package/dist/resources/templates/workflows/evaluate.yaml +2 -2
  188. package/dist/resources/templates/workflows/plan.yaml +1 -1
  189. package/dist/resources/templates/workflows/review.yaml +2 -2
  190. package/dist/server/annotatedAttachmentsRoutes.test.js +3 -3
  191. package/dist/server/auth/providers/apple.d.ts +33 -0
  192. package/dist/server/auth/providers/apple.js +82 -0
  193. package/dist/server/auth/providers/appleClientSecret.d.ts +37 -0
  194. package/dist/server/auth/providers/appleClientSecret.js +65 -0
  195. package/dist/server/auth/providers/github.d.ts +26 -0
  196. package/dist/server/auth/providers/github.js +86 -0
  197. package/dist/server/auth/providers/google.d.ts +21 -0
  198. package/dist/server/auth/providers/google.js +56 -0
  199. package/dist/server/auth/providers/types.d.ts +21 -0
  200. package/dist/server/auth/providers/types.js +1 -0
  201. package/dist/server/auth.d.ts +2 -0
  202. package/dist/server/auth.js +6 -3
  203. package/dist/server/documentReviewRoutes.test.js +36 -3
  204. package/dist/server/index.cookieProxy.test.js +1 -0
  205. package/dist/server/index.d.ts +12 -0
  206. package/dist/server/index.js +120 -9
  207. package/dist/server/index.rateLimit.test.js +3 -0
  208. package/dist/server/index.test.js +106 -3
  209. package/dist/server/routes/admin.d.ts +4 -5
  210. package/dist/server/routes/admin.js +410 -80
  211. package/dist/server/routes/annotatedAttachments.d.ts +1 -1
  212. package/dist/server/routes/annotatedAttachments.js +1 -1
  213. package/dist/server/routes/auth.d.ts +16 -6
  214. package/dist/server/routes/auth.js +1015 -118
  215. package/dist/server/routes/authSupport.d.ts +30 -0
  216. package/dist/server/routes/authSupport.js +81 -0
  217. package/dist/server/routes/billing.d.ts +1 -1
  218. package/dist/server/routes/billing.js +276 -34
  219. package/dist/server/routes/billing.test.js +760 -52
  220. package/dist/server/routes/documentReviews.d.ts +1 -1
  221. package/dist/server/routes/documentReviews.js +1 -1
  222. package/dist/server/routes/documents.d.ts +5 -2
  223. package/dist/server/routes/documents.js +242 -74
  224. package/dist/server/routes/primitives.d.ts +11 -0
  225. package/dist/server/routes/primitives.js +73 -0
  226. package/dist/server/routes/resources.d.ts +1 -1
  227. package/dist/server/routes/resources.js +1 -1
  228. package/dist/server/routes/shared.d.ts +29 -3
  229. package/dist/server/routes/shared.js +69 -4
  230. package/dist/server/routes/sync.d.ts +1 -1
  231. package/dist/server/routes/sync.integration.test.js +437 -39
  232. package/dist/server/routes/sync.js +62 -606
  233. package/dist/server/routes/syncAuxRoutes.d.ts +15 -0
  234. package/dist/server/routes/syncAuxRoutes.js +91 -0
  235. package/dist/server/routes/syncAuxRoutes.test.d.ts +1 -0
  236. package/dist/server/routes/syncAuxRoutes.test.js +158 -0
  237. package/dist/server/routes/syncPullApplyRoutes.d.ts +36 -0
  238. package/dist/server/routes/syncPullApplyRoutes.js +204 -0
  239. package/dist/server/routes/syncPushRoutes.d.ts +24 -0
  240. package/dist/server/routes/syncPushRoutes.js +212 -0
  241. package/dist/server/routes/syncRouteGuards.d.ts +36 -0
  242. package/dist/server/routes/syncRouteGuards.js +67 -0
  243. package/dist/server/routes/syncRouteGuards.test.d.ts +1 -0
  244. package/dist/server/routes/syncRouteGuards.test.js +94 -0
  245. package/dist/server/routes/syncRouteTypes.d.ts +13 -0
  246. package/dist/server/routes/syncRouteTypes.js +1 -0
  247. package/dist/server/routes/syncSnapshotRoutes.d.ts +66 -0
  248. package/dist/server/routes/syncSnapshotRoutes.js +180 -0
  249. package/dist/server/routes/tasks.d.ts +1 -1
  250. package/dist/server/routes/tasks.js +44 -2
  251. package/dist/server/routes/workspaces.d.ts +1 -1
  252. package/dist/server/routes/workspaces.js +37 -6
  253. package/dist/server/routes.d.ts +2 -9
  254. package/dist/server/routes.js +136 -165
  255. package/dist/server/routes.test.js +2457 -162
  256. package/dist/services/workflowExportSnapshots.test.js +2 -0
  257. package/dist/shared/aiProfileSeatScope.d.ts +5 -0
  258. package/dist/shared/aiProfileSeatScope.js +21 -0
  259. package/dist/shared/passwordPolicy.d.ts +13 -0
  260. package/dist/shared/passwordPolicy.js +84 -0
  261. package/dist/shared/passwordPolicy.test.d.ts +1 -0
  262. package/dist/shared/passwordPolicy.test.js +34 -0
  263. package/dist/storage/workspaceAssetStore.d.ts +15 -3
  264. package/dist/storage/workspaceAssetStore.js +132 -44
  265. package/dist/storage/workspaceAssetStore.test.js +93 -6
  266. package/dist/styles/fonts.css +70 -0
  267. package/dist/sync/collaborationSyncPayload.d.ts +6 -1
  268. package/dist/sync/collaborationSyncPayload.js +17 -11
  269. package/dist/sync/collaborationSyncPayload.test.js +13 -1
  270. package/dist/sync/contentSyncState.d.ts +4 -8
  271. package/dist/sync/contentSyncState.js +33 -77
  272. package/dist/sync/syncApplyHandlers.d.ts +7 -0
  273. package/dist/sync/syncApplyHandlers.js +28 -13
  274. package/dist/sync/syncApplyHandlers.test.js +72 -3
  275. package/dist/sync/syncFieldSemantics.d.ts +12 -0
  276. package/dist/sync/syncFieldSemantics.js +55 -0
  277. package/dist/sync/syncResourceAdapters.d.ts +44 -0
  278. package/dist/sync/syncResourceAdapters.js +77 -0
  279. package/dist/sync/syncService.d.ts +6 -0
  280. package/dist/sync/syncService.js +60 -0
  281. package/dist/sync/syncService.test.js +71 -3
  282. package/dist/sync/taskSyncChangeKey.d.ts +2 -0
  283. package/dist/sync/taskSyncChangeKey.js +143 -0
  284. package/dist/sync/taskSyncPayload.js +3 -8
  285. package/dist/sync/workspacePullFeed.js +4 -3
  286. package/dist/sync/workspacePullFeed.test.js +43 -0
  287. package/dist/sync/workspaceRepair.js +5 -2
  288. package/dist/sync/workspaceSyncModel.d.ts +48 -0
  289. package/dist/sync/workspaceSyncModel.js +195 -31
  290. package/dist/sync/workspaceSyncModel.test.js +325 -12
  291. package/dist/sync/workspaceSyncState.d.ts +19 -0
  292. package/dist/sync/workspaceSyncState.js +3 -1
  293. package/dist/sync/workspaceSyncSurface.js +3 -8
  294. package/dist/types.d.ts +17 -0
  295. package/dist/ui/assets/AgentsModule-DrLawwNl.js +1 -0
  296. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BlS-cqnl.js → AnnotatedAttachmentWorkspace-Z9_whf7F.js} +1 -1
  297. package/dist/ui/assets/CourierPrime-Bold-BuLj_dpw.woff2 +0 -0
  298. package/dist/ui/assets/CourierPrime-BoldItalic-BrK2pUkZ.woff2 +0 -0
  299. package/dist/ui/assets/CourierPrime-Italic-DFUZK5Ey.woff2 +0 -0
  300. package/dist/ui/assets/CourierPrime-Regular-BsKphzVf.woff2 +0 -0
  301. package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +252 -0
  302. package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +1 -0
  303. package/dist/ui/assets/{InitiativesModule-BjR6iBf9.js → InitiativesModule-9E6ParrY.js} +1 -1
  304. package/dist/ui/assets/Noir_medium-bxQwKGzB.woff2 +0 -0
  305. package/dist/ui/assets/Noir_regular-ojf2kxlG.woff2 +0 -0
  306. package/dist/ui/assets/PlansPage-CvfnMiWq.css +1 -0
  307. package/dist/ui/assets/PlansPage-DRXscYxH.js +1 -0
  308. package/dist/ui/assets/RussoOne-Regular-Cu_qq_qC.woff2 +0 -0
  309. package/dist/ui/assets/SpecialElite-Regular-Di6gSvAS.woff2 +0 -0
  310. package/dist/ui/assets/TaskSettings-DZX4jk7e.js +9 -0
  311. package/dist/ui/assets/{WorkflowsModule-DnFqdUME.js → WorkflowsModule-BA7jcEpH.js} +1 -1
  312. package/dist/ui/assets/index-9eT8e0Lu.css +1 -0
  313. package/dist/ui/assets/index-C6k75jr8.js +6 -0
  314. package/dist/ui/assets/{vendor-icons-I7ZwG1z_.js → vendor-icons-DuEd_65c.js} +1 -1
  315. package/dist/ui/fonts/CourierPrime-Bold.ttf +0 -0
  316. package/dist/ui/fonts/CourierPrime-Bold.woff2 +0 -0
  317. package/dist/ui/fonts/CourierPrime-BoldItalic.ttf +0 -0
  318. package/dist/ui/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  319. package/dist/ui/fonts/CourierPrime-Italic.ttf +0 -0
  320. package/dist/ui/fonts/CourierPrime-Italic.woff2 +0 -0
  321. package/dist/ui/fonts/CourierPrime-Regular.ttf +0 -0
  322. package/dist/ui/fonts/CourierPrime-Regular.woff2 +0 -0
  323. package/dist/ui/fonts/Noir_medium.otf +0 -0
  324. package/dist/ui/fonts/Noir_medium.woff2 +0 -0
  325. package/dist/ui/fonts/Noir_regular.otf +0 -0
  326. package/dist/ui/fonts/Noir_regular.woff2 +0 -0
  327. package/dist/ui/fonts/RussoOne-Regular.woff2 +0 -0
  328. package/dist/ui/fonts/SpecialElite-Regular.ttf +0 -0
  329. package/dist/ui/fonts/SpecialElite-Regular.woff2 +0 -0
  330. package/dist/ui/index.html +3 -3
  331. package/dist/utils/accountProfileSummaryCache.d.ts +5 -0
  332. package/dist/utils/assignees.d.ts +2 -0
  333. package/dist/utils/assignees.js +2 -0
  334. package/dist/utils/avatarUpload.d.ts +27 -0
  335. package/dist/utils/avatarUpload.js +132 -0
  336. package/dist/utils/avatarUpload.test.d.ts +1 -0
  337. package/dist/utils/avatarUpload.test.js +40 -0
  338. package/dist/utils/bootstrapTrace.d.ts +7 -0
  339. package/dist/utils/bootstrapTrace.js +29 -0
  340. package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
  341. package/dist/utils/commercialLifecyclePresentation.js +199 -0
  342. package/dist/utils/contextAssetEvents.d.ts +2 -0
  343. package/dist/utils/syncEventDetails.d.ts +3 -0
  344. package/dist/utils/syncEventDetails.js +64 -0
  345. package/dist/utils/syncEventDetails.test.d.ts +1 -0
  346. package/dist/utils/syncEventDetails.test.js +23 -0
  347. package/dist/utils/syncEventPresentation.d.ts +15 -0
  348. package/dist/utils/syncEventPresentation.js +129 -0
  349. package/dist/utils/syncEventPresentation.test.d.ts +1 -0
  350. package/dist/utils/syncEventPresentation.test.js +64 -0
  351. package/dist/utils/syncStatusPresentation.d.ts +72 -0
  352. package/dist/utils/syncStatusPresentation.js +217 -0
  353. package/dist/utils/syncStatusPresentation.test.d.ts +1 -0
  354. package/dist/utils/syncStatusPresentation.test.js +164 -0
  355. package/dist/utils/taskActivity.d.ts +2 -0
  356. package/dist/utils/taskActivity.js +228 -22
  357. package/dist/utils/taskActivity.test.js +119 -8
  358. package/dist/utils/workspaceSyncPresentation.d.ts +4 -0
  359. package/dist/utils/workspaceSyncPresentation.js +68 -2
  360. package/dist/utils/workspaceSyncPresentation.test.js +36 -1
  361. package/package.json +10 -2
  362. package/scripts/check-cloud-mcp.mjs +83 -0
  363. package/scripts/export-sqlite-to-postgres.mjs +7 -9
  364. package/scripts/playwright-auth.sh +5 -1
  365. package/scripts/run-billing-performance-smoke.sh +24 -0
  366. package/scripts/run-e2e-realtime.sh +17 -3
  367. package/scripts/run-smoke.sh +17 -5
  368. package/scripts/run-soak.sh +17 -5
  369. package/src/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  370. package/src/resources/templates/workflows/collaborate.yaml +1 -1
  371. package/src/resources/templates/workflows/evaluate.yaml +2 -2
  372. package/src/resources/templates/workflows/plan.yaml +1 -1
  373. package/src/resources/templates/workflows/review.yaml +2 -2
  374. package/dist/ui/assets/AgentsModule-BLWVbuKP.js +0 -1
  375. package/dist/ui/assets/DocumentWorkspace-BH_6DF6x.js +0 -250
  376. package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
  377. package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
  378. package/dist/ui/assets/PlansPage-CSDQ4sLa.js +0 -1
  379. package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
  380. package/dist/ui/assets/TaskSettings-CEDy34Jo.js +0 -9
  381. package/dist/ui/assets/index-DXUdtH1B.js +0 -6
  382. package/dist/ui/assets/index-Ii0B0rTO.css +0 -1
  383. package/scripts/migrate-planning-model.ts +0 -233
@@ -0,0 +1,30 @@
1
+ import type { IncomingMessage } from 'http';
2
+ import type { TaskforceCore } from '../../core/Taskforce.js';
3
+ import type { RouteContext } from './primitives.js';
4
+ export declare function resolveBooleanSetting(value: unknown, fallback: boolean): boolean;
5
+ export declare function getRouteEmailConfig(core: TaskforceCore, context: RouteContext): import("../email.js").TaskforceEmailConfig;
6
+ export declare function buildAuthFlowBehavior(core: TaskforceCore): {
7
+ allowSelfRegistration: boolean;
8
+ requireVerifiedEmail: boolean;
9
+ defaultNewUserBetaAccess: boolean;
10
+ authRateLimitEnabled: boolean;
11
+ authRateLimitMaxRequests: number;
12
+ authRateLimitWindowMs: number;
13
+ };
14
+ export declare function buildAuthBehavior(core: TaskforceCore): {
15
+ onboardingPolicyMode: import("../../core/types.js").OnboardingPolicyMode;
16
+ onboardingPlanId: string | null;
17
+ allowSelfRegistration: boolean;
18
+ requireVerifiedEmail: boolean;
19
+ defaultNewUserBetaAccess: boolean;
20
+ authRateLimitEnabled: boolean;
21
+ authRateLimitMaxRequests: number;
22
+ authRateLimitWindowMs: number;
23
+ };
24
+ export declare function buildSiteBehavior(core: TaskforceCore): {
25
+ showRunTaskforceLocally: boolean;
26
+ pricingPageEnabled: boolean;
27
+ };
28
+ export declare function shouldExposeDevAuthTokens(): boolean;
29
+ export declare function resolveRuntimeConfigBaseUrl(req: IncomingMessage): string;
30
+ export declare function resolvePublicAuthBaseUrl(req: IncomingMessage): string;
@@ -0,0 +1,81 @@
1
+ import { resolveEffectiveEmailConfig } from '../email.js';
2
+ import { resolveDeploymentConfig } from '../../config/deployment.js';
3
+ export function resolveBooleanSetting(value, fallback) {
4
+ if (typeof value === 'boolean')
5
+ return value;
6
+ if (typeof value === 'number')
7
+ return value !== 0;
8
+ const normalized = String(value || '').trim().toLowerCase();
9
+ if (!normalized)
10
+ return fallback;
11
+ if (['1', 'true', 'yes', 'on'].includes(normalized))
12
+ return true;
13
+ if (['0', 'false', 'no', 'off'].includes(normalized))
14
+ return false;
15
+ return fallback;
16
+ }
17
+ export function getRouteEmailConfig(core, context) {
18
+ if (context.authConfig?.runtimeMode !== 'cloud') {
19
+ return resolveEffectiveEmailConfig(process.env, { provider: 'console' });
20
+ }
21
+ const globalSettings = core.getGlobalSettings();
22
+ return resolveEffectiveEmailConfig(process.env, globalSettings.email || null);
23
+ }
24
+ export function buildAuthFlowBehavior(core) {
25
+ const globalSettings = core.getGlobalSettings();
26
+ const authSettings = globalSettings.auth || {};
27
+ const authRateLimitMaxRequestsRaw = Number(authSettings.authRateLimitMaxRequests);
28
+ const authRateLimitWindowMsRaw = Number(authSettings.authRateLimitWindowMs);
29
+ return {
30
+ allowSelfRegistration: resolveBooleanSetting(authSettings.allowSelfRegistration, true),
31
+ requireVerifiedEmail: resolveBooleanSetting(authSettings.requireVerifiedEmail, true),
32
+ defaultNewUserBetaAccess: resolveBooleanSetting(authSettings.defaultNewUserBetaAccess, false),
33
+ authRateLimitEnabled: resolveBooleanSetting(authSettings.authRateLimitEnabled, true),
34
+ authRateLimitMaxRequests: Number.isFinite(authRateLimitMaxRequestsRaw) && authRateLimitMaxRequestsRaw >= 1
35
+ ? Math.floor(authRateLimitMaxRequestsRaw)
36
+ : 10,
37
+ authRateLimitWindowMs: Number.isFinite(authRateLimitWindowMsRaw) && authRateLimitWindowMsRaw >= 1000
38
+ ? Math.floor(authRateLimitWindowMsRaw)
39
+ : 15 * 60_000,
40
+ };
41
+ }
42
+ export function buildAuthBehavior(core) {
43
+ const authBehavior = buildAuthFlowBehavior(core);
44
+ const onboardingPolicy = core.getOnboardingPolicy();
45
+ return {
46
+ ...authBehavior,
47
+ onboardingPolicyMode: onboardingPolicy.mode,
48
+ onboardingPlanId: onboardingPolicy.planId
49
+ };
50
+ }
51
+ export function buildSiteBehavior(core) {
52
+ const globalSettings = core.getGlobalSettings();
53
+ const siteSettings = globalSettings.site || {};
54
+ return {
55
+ showRunTaskforceLocally: resolveBooleanSetting(siteSettings.showRunTaskforceLocally, true),
56
+ pricingPageEnabled: resolveBooleanSetting(siteSettings.pricingPageEnabled, true)
57
+ };
58
+ }
59
+ export function shouldExposeDevAuthTokens() {
60
+ const explicit = String(process.env.TASKFORCE_AUTH_EXPOSE_DEV_TOKENS || '').trim().toLowerCase();
61
+ return explicit === '1' || explicit === 'true' || explicit === 'yes';
62
+ }
63
+ export function resolveRuntimeConfigBaseUrl(req) {
64
+ const host = String(req.headers['x-forwarded-host'] || req.headers.host || '').trim();
65
+ if (!host)
66
+ return '';
67
+ const proto = String(req.headers['x-forwarded-proto'] || '').trim().toLowerCase() === 'https'
68
+ ? 'https'
69
+ : (req.socket?.encrypted ? 'https' : 'http');
70
+ return `${proto}://${host}`.replace(/\/+$/, '');
71
+ }
72
+ export function resolvePublicAuthBaseUrl(req) {
73
+ const configured = String(process.env.TASKFORCE_AUTH_PUBLIC_BASE_URL || '').trim().replace(/\/+$/, '');
74
+ if (configured)
75
+ return configured;
76
+ const host = String(req.headers['x-forwarded-host'] || req.headers.host || '').trim();
77
+ const proto = String(req.headers['x-forwarded-proto'] || '').trim().toLowerCase() === 'http' ? 'http' : 'https';
78
+ if (!host)
79
+ return resolveDeploymentConfig(process.env).cloudBaseUrl || 'https://app.taskforcehq.ai';
80
+ return `${proto}://${host}`;
81
+ }
@@ -1,6 +1,6 @@
1
1
  import type { IncomingMessage } from 'http';
2
2
  import type { TaskforceCore } from '../../core/Taskforce.js';
3
- import type { RouteHandler, RouteContext } from '../routes.js';
3
+ import { type RouteHandler, type RouteContext } from './primitives.js';
4
4
  export type BillingInterval = 'month' | 'year';
5
5
  export type BillingPricingType = 'stripe' | 'free';
6
6
  export interface BillingRouteDependencies {
@@ -1,5 +1,5 @@
1
1
  import Stripe from 'stripe';
2
- import { parseJsonBody } from '../routes.js';
2
+ import { parseJsonBody } from './primitives.js';
3
3
  import { resolveTaskforceCloudEnvironmentFromHostname } from '../../config/cloudEnvironment.js';
4
4
  import { buildMockStripeHostedCheckoutUrl, completeMockStripeCheckoutSession, createMockStripeClient, isMockStripeClient, isMockStripeEnabled, readMockStripeCheckoutSession, renderMockStripeCheckoutHtml } from '../mockStripe.js';
5
5
  const BILLING_CONFIG_KEY_PLAN = 'billing.config.v1';
@@ -8,16 +8,63 @@ const BILLING_DIAGNOSTIC_RETENTION_DAYS = 30;
8
8
  const BILLING_DIAGNOSTIC_MAX_PAYLOAD_BYTES = 8 * 1024;
9
9
  const BILLING_CHECKOUT_ATTEMPT_RETENTION_DAYS = 14;
10
10
  const BILLING_STATUS_RECONCILIATION_TTL_MS = 30_000;
11
+ const PUBLIC_PRICING_CACHE_TTL_MS = 60_000;
11
12
  const TASKFORCE_PROXY_ORIGIN_HOST_HEADER = 'x-taskforce-origin-host';
12
13
  const TASKFORCE_PROXY_ORIGIN_PROTO_HEADER = 'x-taskforce-origin-proto';
13
14
  const TASKFORCE_PROXY_ORIGIN_BASE_URL_HEADER = 'x-taskforce-origin-base-url';
14
15
  const TASKFORCE_PROXY_ORIGIN_RUNTIME_HEADER = 'x-taskforce-origin-runtime';
16
+ const INTERNAL_PUBLIC_PRICING_FEATURE_KEYS = new Set([
17
+ 'collaboration.invites',
18
+ 'collaboration.seat_limit'
19
+ ]);
20
+ const publicPricingCache = new Map();
15
21
  function parsePricingType(raw) {
16
22
  const value = String(raw || '').trim().toLowerCase();
17
23
  if (value === 'stripe' || value === 'free')
18
24
  return value;
19
25
  return null;
20
26
  }
27
+ function readTableVersionStats(core, tableName) {
28
+ const db = core.getDatabaseAdapter();
29
+ const tenantId = core.getTenantId();
30
+ try {
31
+ const row = db.prepare(`
32
+ SELECT COUNT(*) AS count, MAX(updated_at) AS max_updated_at
33
+ FROM ${tableName}
34
+ WHERE tenant_id = ?
35
+ `).get(tenantId);
36
+ return {
37
+ count: Number(row?.count || 0),
38
+ maxUpdatedAt: String(row?.max_updated_at || '')
39
+ };
40
+ }
41
+ catch (error) {
42
+ const message = String(error?.message || '');
43
+ if (message.toLowerCase().includes(`no such table: ${tableName}`)) {
44
+ return { count: 0, maxUpdatedAt: '' };
45
+ }
46
+ throw error;
47
+ }
48
+ }
49
+ function buildPublicPricingCacheVersion(core) {
50
+ const catalog = readTableVersionStats(core, 'plan_catalog');
51
+ const versions = readTableVersionStats(core, 'plan_versions');
52
+ const prices = readTableVersionStats(core, 'billing_price_mappings');
53
+ const features = readTableVersionStats(core, 'plan_feature_matrix');
54
+ const featureCatalogOverrides = readTableVersionStats(core, 'plan_feature_catalog_overrides');
55
+ return [
56
+ catalog.count,
57
+ catalog.maxUpdatedAt,
58
+ versions.count,
59
+ versions.maxUpdatedAt,
60
+ prices.count,
61
+ prices.maxUpdatedAt,
62
+ features.count,
63
+ features.maxUpdatedAt,
64
+ featureCatalogOverrides.count,
65
+ featureCatalogOverrides.maxUpdatedAt
66
+ ].join('|');
67
+ }
21
68
  function buildFreePricingSyntheticId(planVersionId, interval) {
22
69
  return `free:${planVersionId}:${interval}`;
23
70
  }
@@ -664,7 +711,11 @@ function resolveCheckoutLifecycleGuard(core, input) {
664
711
  : currentOpenAttempt;
665
712
  const attemptStatus = String(checkoutAttempt?.status || '').trim().toLowerCase();
666
713
  const attemptPlanVersionId = String(checkoutAttempt?.plan_version_id || '').trim();
714
+ const sourceLabel = String(input.source || 'checkout-confirm').trim() || 'checkout-confirm';
667
715
  if (attemptStatus === 'expired') {
716
+ console.warn(`[Taskforce Billing] Lifecycle guard blocked expired checkout for user=${input.userId} source=${sourceLabel} `
717
+ + `session=${String(input.sessionId || '').trim() || '(none)'} subscription=${String(input.subscriptionId || '').trim() || '(none)'} `
718
+ + `requestedPlanVersionId=${String(input.planVersionId || '').trim() || '(none)'} attemptPlanVersionId=${attemptPlanVersionId || '(none)'}`);
668
719
  return {
669
720
  blocked: true,
670
721
  code: 'CHECKOUT_SESSION_EXPIRED',
@@ -672,6 +723,9 @@ function resolveCheckoutLifecycleGuard(core, input) {
672
723
  };
673
724
  }
674
725
  if (attemptStatus === 'superseded') {
726
+ console.warn(`[Taskforce Billing] Lifecycle guard blocked superseded checkout for user=${input.userId} source=${sourceLabel} `
727
+ + `session=${String(input.sessionId || '').trim() || '(none)'} subscription=${String(input.subscriptionId || '').trim() || '(none)'} `
728
+ + `requestedPlanVersionId=${String(input.planVersionId || '').trim() || '(none)'} attemptPlanVersionId=${attemptPlanVersionId || '(none)'}`);
675
729
  return {
676
730
  blocked: true,
677
731
  code: 'CHECKOUT_SESSION_SUPERSEDED',
@@ -679,6 +733,9 @@ function resolveCheckoutLifecycleGuard(core, input) {
679
733
  };
680
734
  }
681
735
  if (attemptStatus === 'open' && attemptPlanVersionId && input.planVersionId && attemptPlanVersionId !== input.planVersionId) {
736
+ console.warn(`[Taskforce Billing] Lifecycle guard blocked mismatched open checkout for user=${input.userId} source=${sourceLabel} `
737
+ + `session=${String(input.sessionId || '').trim() || '(none)'} subscription=${String(input.subscriptionId || '').trim() || '(none)'} `
738
+ + `requestedPlanVersionId=${String(input.planVersionId || '').trim() || '(none)'} attemptPlanVersionId=${attemptPlanVersionId}`);
682
739
  return {
683
740
  blocked: true,
684
741
  code: 'CHECKOUT_SESSION_SUPERSEDED',
@@ -688,6 +745,9 @@ function resolveCheckoutLifecycleGuard(core, input) {
688
745
  const currentBilling = getBillingMirrorRow(core, input.userId);
689
746
  const currentSubscriptionId = String(currentBilling?.stripe_subscription_id || '').trim();
690
747
  if (!currentOpenAttempt && input.subscriptionId && currentSubscriptionId && currentSubscriptionId !== input.subscriptionId) {
748
+ console.warn(`[Taskforce Billing] Lifecycle guard blocked stale subscription linkage for user=${input.userId} source=${sourceLabel} `
749
+ + `currentSubscriptionId=${currentSubscriptionId} requestedSubscriptionId=${String(input.subscriptionId || '').trim() || '(none)'} `
750
+ + `requestedPlanVersionId=${String(input.planVersionId || '').trim() || '(none)'}`);
691
751
  return {
692
752
  blocked: true,
693
753
  code: 'CHECKOUT_SESSION_SUPERSEDED',
@@ -701,17 +761,28 @@ function resolveCheckoutLifecycleGuard(core, input) {
701
761
  const currentPlanVersionId = String(entitlement.plan_version_id || '').trim();
702
762
  const updatedBy = String(entitlement.updated_by || '').trim().toLowerCase();
703
763
  const expiredPendingCheckout = currentState === 'checkout_pending' && isIsoTimestampExpired(entitlement.effective_until);
704
- if (expiredPendingCheckout || (currentState === 'pending_plan_selection' && updatedBy === 'checkout-expired')) {
764
+ const fromStripeWebhook = input.source === 'stripe-webhook';
765
+ if (!fromStripeWebhook && (expiredPendingCheckout || (currentState === 'pending_plan_selection' && updatedBy === 'checkout-expired'))) {
766
+ console.warn(`[Taskforce Billing] Lifecycle guard blocked expired entitlement recovery for user=${input.userId} source=${sourceLabel} `
767
+ + `entitlementState=${currentState || '(none)'} updatedBy=${updatedBy || '(none)'} `
768
+ + `requestedPlanVersionId=${String(input.planVersionId || '').trim() || '(none)'} currentPlanVersionId=${currentPlanVersionId || '(none)'}`);
705
769
  return {
706
770
  blocked: true,
707
771
  code: 'CHECKOUT_SESSION_EXPIRED',
708
772
  message: 'This checkout session expired. Choose a plan and start checkout again.'
709
773
  };
710
774
  }
775
+ if (fromStripeWebhook && (expiredPendingCheckout || (currentState === 'pending_plan_selection' && updatedBy === 'checkout-expired'))) {
776
+ console.info(`[Taskforce Billing] Lifecycle guard allowed authoritative webhook recovery for user=${input.userId} `
777
+ + `subscription=${String(input.subscriptionId || '').trim() || '(none)'} requestedPlanVersionId=${String(input.planVersionId || '').trim() || '(none)'} `
778
+ + `entitlementState=${currentState || '(none)'} updatedBy=${updatedBy || '(none)'}`);
779
+ }
711
780
  if (currentState === 'checkout_pending'
712
781
  && currentPlanVersionId
713
782
  && input.planVersionId
714
783
  && currentPlanVersionId !== input.planVersionId) {
784
+ console.warn(`[Taskforce Billing] Lifecycle guard blocked superseded checkout_pending entitlement for user=${input.userId} source=${sourceLabel} `
785
+ + `currentPlanVersionId=${currentPlanVersionId} requestedPlanVersionId=${String(input.planVersionId || '').trim() || '(none)'}`);
715
786
  return {
716
787
  blocked: true,
717
788
  code: 'CHECKOUT_SESSION_SUPERSEDED',
@@ -795,11 +866,15 @@ function applySubscriptionSnapshot(core, input) {
795
866
  ? (currentPeriodEnd && currentPeriodEnd > nowIso ? 'active' : 'canceled')
796
867
  : null;
797
868
  const mappedCanonicalState = canceledState || mapStripeStatusToCanonicalState(stripeStatus);
869
+ const existingPlanVersionId = String(existingEntitlement?.plan_version_id || '').trim();
798
870
  const preserveExistingAccess = mappedCanonicalState === 'checkout_pending'
871
+ && Boolean(planVersionId)
872
+ && existingPlanVersionId === planVersionId
799
873
  && isCommercialStateAppAccessible(existingEntitlement?.entitlement_state);
800
874
  const canonicalState = preserveExistingAccess
801
875
  ? String(existingEntitlement?.entitlement_state || 'active').trim().toLowerCase()
802
876
  : mappedCanonicalState;
877
+ const billingConfirmedSource = source === 'stripe-webhook' || source === 'checkout-confirm';
803
878
  const effectiveUntil = stripeStatus === 'canceled'
804
879
  ? currentPeriodEnd
805
880
  : (currentPeriodEnd || null);
@@ -826,7 +901,7 @@ function applySubscriptionSnapshot(core, input) {
826
901
  });
827
902
  }
828
903
  }
829
- else if (planVersionId) {
904
+ else if (planVersionId && billingConfirmedSource) {
830
905
  core.transitionCanonicalCommercialState({
831
906
  accountId: userId,
832
907
  planVersionId: planVersionId || existingEntitlement?.plan_version_id || core.getDefaultPlanVersionIdByMode('personal', { allowFallback: true }),
@@ -1402,6 +1477,23 @@ export function registerBillingRoutes(deps) {
1402
1477
  res.end(JSON.stringify({ error: `Stripe price interval (${stripeInterval}) does not match requested interval (${interval}).`, code: 'STRIPE_PRICE_INTERVAL_MISMATCH' }));
1403
1478
  return;
1404
1479
  }
1480
+ const conflictingMapping = db.prepare(`
1481
+ SELECT plan_version_id
1482
+ FROM billing_price_mappings
1483
+ WHERE tenant_id = ?
1484
+ AND stripe_price_id = ?
1485
+ AND active = 1
1486
+ AND plan_version_id <> ?
1487
+ LIMIT 1
1488
+ `).get(tenantId, stripePriceId, planVersionId);
1489
+ if (conflictingMapping?.plan_version_id) {
1490
+ res.writeHead(409, { 'Content-Type': 'application/json' });
1491
+ res.end(JSON.stringify({
1492
+ error: `Stripe price ${stripePriceId} is already mapped to ${String(conflictingMapping.plan_version_id)}.`,
1493
+ code: 'BILLING_PRICE_ALREADY_ASSIGNED'
1494
+ }));
1495
+ return;
1496
+ }
1405
1497
  unitAmount = typeof stripePrice.unit_amount === 'number' ? stripePrice.unit_amount : null;
1406
1498
  currency = stripePrice.currency || null;
1407
1499
  }
@@ -1769,8 +1861,8 @@ export function registerBillingRoutes(deps) {
1769
1861
  const stripe = getStripeClient();
1770
1862
  let currentSubscriptionId = String(currentBilling?.stripe_subscription_id || '').trim();
1771
1863
  const currentCustomerId = String(currentBilling?.stripe_customer_id || '').trim();
1772
- const currentPlanVersionId = String(currentEntitlement?.plan_version_id || '').trim();
1773
- const currentInterval = parseInterval(String(currentBilling?.billing_interval || '').trim());
1864
+ let currentPlanVersionId = String(currentEntitlement?.plan_version_id || '').trim();
1865
+ let currentInterval = parseInterval(String(currentBilling?.billing_interval || '').trim());
1774
1866
  const stripeAccountDiscovery = await discoverStripeAccountStateForUser(core, {
1775
1867
  userId,
1776
1868
  normalizedEmail: normalizedUserEmail,
@@ -1786,8 +1878,13 @@ export function registerBillingRoutes(deps) {
1786
1878
  return;
1787
1879
  }
1788
1880
  let resolvedCustomerId = currentCustomerId || String(stripeAccountDiscovery.preferredCustomerId || '').trim();
1789
- if (!currentSubscriptionId && stripeAccountDiscovery.activeSubscriptions.length === 1) {
1790
- const activeSubscription = stripeAccountDiscovery.activeSubscriptions[0];
1881
+ const activeSubscription = stripeAccountDiscovery.activeSubscriptions.length === 1
1882
+ ? stripeAccountDiscovery.activeSubscriptions[0]
1883
+ : null;
1884
+ let discoveredPlanVersionId = null;
1885
+ let discoveredInterval = null;
1886
+ let discoveredStripePriceId = null;
1887
+ if (!currentSubscriptionId && activeSubscription) {
1791
1888
  currentSubscriptionId = String(activeSubscription.subscription?.id || '').trim();
1792
1889
  resolvedCustomerId = String(activeSubscription.customerId || '').trim() || resolvedCustomerId;
1793
1890
  if (currentSubscriptionId || resolvedCustomerId) {
@@ -1804,7 +1901,28 @@ export function registerBillingRoutes(deps) {
1804
1901
  stripeCustomerId: resolvedCustomerId
1805
1902
  });
1806
1903
  }
1807
- if (currentSubscriptionId && currentPlanVersionId === planVersionId && currentInterval === interval) {
1904
+ if (activeSubscription) {
1905
+ const activeSubscriptionPriceId = String(activeSubscription.subscription?.items?.data?.[0]?.price?.id || '').trim();
1906
+ discoveredStripePriceId = activeSubscriptionPriceId || null;
1907
+ const activePlanVersionId = resolvePlanVersionIdForBillingEvent(core, [
1908
+ activeSubscription.subscription?.metadata?.planVersionId,
1909
+ activeSubscription.subscription?.metadata?.plan_version_id
1910
+ ], activeSubscriptionPriceId).planVersionId;
1911
+ discoveredPlanVersionId = activePlanVersionId;
1912
+ discoveredInterval = parseInterval(activeSubscription.subscription?.items?.data?.[0]?.price?.recurring?.interval || '');
1913
+ if (!currentPlanVersionId && activePlanVersionId) {
1914
+ currentPlanVersionId = activePlanVersionId;
1915
+ }
1916
+ if (!currentInterval) {
1917
+ currentInterval = discoveredInterval;
1918
+ }
1919
+ }
1920
+ const isCurrentRequestedPlan = (currentPlanVersionId === planVersionId && currentInterval === interval) || (Boolean(currentSubscriptionId)
1921
+ && discoveredPlanVersionId === planVersionId
1922
+ && discoveredInterval === interval) || (Boolean(currentSubscriptionId)
1923
+ && discoveredStripePriceId === priceId
1924
+ && discoveredInterval === interval);
1925
+ if (currentSubscriptionId && isCurrentRequestedPlan) {
1808
1926
  res.writeHead(200, { 'Content-Type': 'application/json' });
1809
1927
  res.end(JSON.stringify({
1810
1928
  checkoutState: 'already_current_plan',
@@ -1816,6 +1934,17 @@ export function registerBillingRoutes(deps) {
1816
1934
  if (currentSubscriptionId) {
1817
1935
  const current = await stripe.subscriptions.retrieve(currentSubscriptionId);
1818
1936
  const itemId = current.items.data[0]?.id;
1937
+ const currentStripePriceId = String(current.items.data[0]?.price?.id || '').trim();
1938
+ const currentSubscriptionInterval = parseInterval(current.items.data[0]?.price?.recurring?.interval || '');
1939
+ if (currentStripePriceId === priceId && currentSubscriptionInterval === interval) {
1940
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1941
+ res.end(JSON.stringify({
1942
+ checkoutState: 'already_current_plan',
1943
+ planVersionId,
1944
+ message: 'You are already on this plan.'
1945
+ }));
1946
+ return;
1947
+ }
1819
1948
  if (!itemId) {
1820
1949
  res.writeHead(400, { 'Content-Type': 'application/json' });
1821
1950
  res.end(JSON.stringify({ error: 'Subscription has no updatable line item.' }));
@@ -1840,7 +1969,7 @@ export function registerBillingRoutes(deps) {
1840
1969
  res.end(JSON.stringify({
1841
1970
  checkoutState: 'updated',
1842
1971
  planVersionId,
1843
- message: 'Your subscription was updated successfully.'
1972
+ message: 'Your subscription update was submitted. Changes will appear after Stripe confirms the update.'
1844
1973
  }));
1845
1974
  return;
1846
1975
  }
@@ -1857,10 +1986,17 @@ export function registerBillingRoutes(deps) {
1857
1986
  planVersionId,
1858
1987
  interval
1859
1988
  });
1989
+ const successParams = new URLSearchParams({
1990
+ screen: 'plans',
1991
+ checkout: 'success',
1992
+ planId: String(planId || ''),
1993
+ planVersionId,
1994
+ interval
1995
+ });
1860
1996
  const session = await stripe.checkout.sessions.create({
1861
1997
  mode: 'subscription',
1862
1998
  line_items: [{ price: priceId, quantity: 1 }],
1863
- success_url: `${baseUrl}/?screen=plans&checkout=success&session_id={CHECKOUT_SESSION_ID}`,
1999
+ success_url: `${baseUrl}/?${successParams.toString()}&session_id={CHECKOUT_SESSION_ID}`,
1864
2000
  cancel_url: `${baseUrl}/?${cancelParams.toString()}`,
1865
2001
  ...(resolvedCustomerId
1866
2002
  ? { customer: resolvedCustomerId }
@@ -1979,7 +2115,10 @@ export function registerBillingRoutes(deps) {
1979
2115
  }
1980
2116
  if (resolvedUserId !== expectedUserId) {
1981
2117
  res.writeHead(403, { 'Content-Type': 'application/json' });
1982
- res.end(JSON.stringify({ error: 'Checkout session does not belong to the current account.' }));
2118
+ res.end(JSON.stringify({
2119
+ error: 'Checkout session does not belong to the current account.',
2120
+ code: 'CHECKOUT_SESSION_ACCOUNT_MISMATCH'
2121
+ }));
1983
2122
  return;
1984
2123
  }
1985
2124
  let resolvedPlanVersionId = null;
@@ -1999,7 +2138,8 @@ export function registerBillingRoutes(deps) {
1999
2138
  userId: resolvedUserId,
2000
2139
  planVersionId: resolvedPlanVersionId,
2001
2140
  sessionId,
2002
- subscriptionId
2141
+ subscriptionId,
2142
+ source: 'checkout-confirm'
2003
2143
  });
2004
2144
  if (lifecycleGuard.blocked) {
2005
2145
  res.writeHead(409, { 'Content-Type': 'application/json' });
@@ -2010,20 +2150,54 @@ export function registerBillingRoutes(deps) {
2010
2150
  return;
2011
2151
  }
2012
2152
  }
2013
- core.createUserBillingRecord(resolvedUserId);
2014
- upsertBillingMirror(core, {
2153
+ if (!subscriptionId || !subscription) {
2154
+ res.writeHead(409, { 'Content-Type': 'application/json' });
2155
+ res.end(JSON.stringify({
2156
+ error: 'Checkout session is still finalizing your subscription.',
2157
+ code: 'CHECKOUT_SESSION_INCOMPLETE'
2158
+ }));
2159
+ return;
2160
+ }
2161
+ if (!resolvedPlanVersionId) {
2162
+ res.writeHead(409, { 'Content-Type': 'application/json' });
2163
+ res.end(JSON.stringify({
2164
+ error: 'Checkout completed, but the purchased plan could not be resolved yet.',
2165
+ code: 'CHECKOUT_PLAN_VERSION_UNRESOLVED'
2166
+ }));
2167
+ return;
2168
+ }
2169
+ const snapshot = applySubscriptionSnapshot(core, {
2015
2170
  userId: resolvedUserId,
2016
- stripeCustomerId: customerId || null,
2017
- stripeSubscriptionId: subscriptionId || null
2171
+ subscription,
2172
+ source: 'checkout-confirm'
2018
2173
  });
2019
- if (subscription) {
2020
- resolvedPlanVersionId = applySubscriptionSnapshot(core, {
2021
- userId: resolvedUserId,
2022
- subscription,
2023
- source: 'checkout-confirm'
2024
- }).planVersionId;
2025
- updateCheckoutAttemptStatus(core, sessionId, 'confirmed');
2174
+ resolvedPlanVersionId = snapshot.planVersionId;
2175
+ if (!resolvedPlanVersionId) {
2176
+ res.writeHead(409, { 'Content-Type': 'application/json' });
2177
+ res.end(JSON.stringify({
2178
+ error: 'Checkout completed, but the purchased plan could not be resolved yet.',
2179
+ code: 'CHECKOUT_PLAN_VERSION_UNRESOLVED'
2180
+ }));
2181
+ return;
2182
+ }
2183
+ const updatedEntitlement = getEntitlementRow(core, resolvedUserId);
2184
+ const updatedPlanVersionId = String(updatedEntitlement?.plan_version_id || '').trim();
2185
+ const updatedState = String(updatedEntitlement?.entitlement_state || '').trim().toLowerCase();
2186
+ const canonicalSettled = snapshot.canonicalState !== 'checkout_pending'
2187
+ && updatedPlanVersionId === resolvedPlanVersionId
2188
+ && updatedState === snapshot.canonicalState;
2189
+ if (!canonicalSettled) {
2190
+ console.warn(`[Taskforce Billing] Checkout confirmation still settling for user=${resolvedUserId} session=${sessionId} subscription=${subscriptionId || '(missing)'} `
2191
+ + `planVersionId=${resolvedPlanVersionId} canonicalState=${snapshot.canonicalState} persistedPlanVersionId=${updatedPlanVersionId || '(missing)'} `
2192
+ + `persistedState=${updatedState || '(missing)'}`);
2193
+ res.writeHead(409, { 'Content-Type': 'application/json' });
2194
+ res.end(JSON.stringify({
2195
+ error: 'Checkout session is still finalizing your plan.',
2196
+ code: 'CHECKOUT_SESSION_INCOMPLETE'
2197
+ }));
2198
+ return;
2026
2199
  }
2200
+ updateCheckoutAttemptStatus(core, sessionId, 'confirmed');
2027
2201
  res.writeHead(200, { 'Content-Type': 'application/json' });
2028
2202
  res.end(JSON.stringify({
2029
2203
  success: true,
@@ -2088,7 +2262,7 @@ export function registerBillingRoutes(deps) {
2088
2262
  const baseUrl = resolveBaseUrl(req);
2089
2263
  const session = await stripe.billingPortal.sessions.create({
2090
2264
  customer: customerId,
2091
- return_url: `${baseUrl}/settings`
2265
+ return_url: `${baseUrl}/?screen=plans`
2092
2266
  });
2093
2267
  res.writeHead(200, { 'Content-Type': 'application/json' });
2094
2268
  res.end(JSON.stringify({ url: session.url, returnBaseUrl: baseUrl }));
@@ -2249,7 +2423,8 @@ export function registerBillingRoutes(deps) {
2249
2423
  const lifecycleGuard = resolveCheckoutLifecycleGuard(core, {
2250
2424
  userId,
2251
2425
  planVersionId,
2252
- subscriptionId: String(subscription.id || '').trim()
2426
+ subscriptionId: String(subscription.id || '').trim(),
2427
+ source: 'stripe-webhook'
2253
2428
  });
2254
2429
  if (lifecycleGuard.blocked) {
2255
2430
  recordWebhookFailure(core, {
@@ -2294,7 +2469,8 @@ export function registerBillingRoutes(deps) {
2294
2469
  const lifecycleGuard = resolveCheckoutLifecycleGuard(core, {
2295
2470
  userId,
2296
2471
  planVersionId,
2297
- subscriptionId
2472
+ subscriptionId,
2473
+ source: 'stripe-webhook'
2298
2474
  });
2299
2475
  if (lifecycleGuard.blocked) {
2300
2476
  recordWebhookFailure(core, {
@@ -2376,6 +2552,15 @@ export function registerBillingRoutes(deps) {
2376
2552
  try {
2377
2553
  const db = core.getDatabaseAdapter();
2378
2554
  const tenantId = core.getTenantId();
2555
+ const environment = requestPricingEnvironment(req);
2556
+ const cacheKey = `${tenantId}:${environment}`;
2557
+ const cacheVersion = buildPublicPricingCacheVersion(core);
2558
+ const cached = publicPricingCache.get(cacheKey);
2559
+ if (cached && cached.version === cacheVersion && cached.expiresAt > Date.now()) {
2560
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2561
+ res.end(JSON.stringify(cached.payload));
2562
+ return;
2563
+ }
2379
2564
  // Get all active plans
2380
2565
  const activePlans = core.listPlanCatalog().filter(p => p.status === 'active');
2381
2566
  // Get all active price mappings with their amounts
@@ -2384,6 +2569,17 @@ export function registerBillingRoutes(deps) {
2384
2569
  FROM billing_price_mappings
2385
2570
  WHERE tenant_id = ? AND active = 1
2386
2571
  `).all(tenantId);
2572
+ const pricingByVersionAndInterval = new Map();
2573
+ for (const price of activePrices) {
2574
+ const planVersionId = String(price.plan_version_id || '').trim();
2575
+ const interval = parseInterval(price.billing_interval);
2576
+ if (!planVersionId || !interval)
2577
+ continue;
2578
+ pricingByVersionAndInterval.set(`${planVersionId}:${interval}`, price);
2579
+ }
2580
+ const featureCatalog = core.listPlanFeatureCatalog();
2581
+ const featureCatalogByKey = new Map(featureCatalog.map((feature) => [feature.featureKey, feature]));
2582
+ const featureCatalogIndexByKey = new Map(featureCatalog.map((feature, index) => [feature.featureKey, index]));
2387
2583
  const plansPayload = activePlans.map((plan) => {
2388
2584
  const versions = core.listPlanVersions(plan.planId);
2389
2585
  // Prefer default, else fallback to highest version number
@@ -2391,11 +2587,12 @@ export function registerBillingRoutes(deps) {
2391
2587
  if (!defaultVersion) {
2392
2588
  return null; // Skip if no valid version exists (edge case)
2393
2589
  }
2394
- const monthPrice = activePrices.find(p => p.plan_version_id === defaultVersion.planVersionId && p.billing_interval === 'month');
2395
- const yearPrice = activePrices.find(p => p.plan_version_id === defaultVersion.planVersionId && p.billing_interval === 'year');
2590
+ const monthPrice = pricingByVersionAndInterval.get(`${defaultVersion.planVersionId}:month`);
2591
+ const yearPrice = pricingByVersionAndInterval.get(`${defaultVersion.planVersionId}:year`);
2396
2592
  return {
2397
2593
  planId: plan.planId,
2398
2594
  displayName: plan.displayName,
2595
+ description: plan.description ?? null,
2399
2596
  status: plan.status,
2400
2597
  defaultVersion: {
2401
2598
  planVersionId: defaultVersion.planVersionId,
@@ -2404,9 +2601,48 @@ export function registerBillingRoutes(deps) {
2404
2601
  trialDays: defaultVersion.trialDays,
2405
2602
  metadata: defaultVersion.metadata
2406
2603
  },
2407
- features: Object.values(defaultVersion.features || {}).filter((feature) => {
2604
+ features: Object.values(defaultVersion.features || {})
2605
+ .reduce((acc, feature) => {
2408
2606
  const access = String(feature?.access || '').trim().toLowerCase();
2409
- return access !== 'disabled';
2607
+ if (access === 'disabled') {
2608
+ return acc;
2609
+ }
2610
+ const featureKey = String(feature?.featureKey || '').trim();
2611
+ if (!featureKey || INTERNAL_PUBLIC_PRICING_FEATURE_KEYS.has(featureKey)) {
2612
+ return acc;
2613
+ }
2614
+ const catalogEntry = featureCatalogByKey.get(featureKey);
2615
+ acc.push({
2616
+ featureKey,
2617
+ access,
2618
+ config: feature?.config && typeof feature.config === 'object'
2619
+ ? feature.config
2620
+ : {},
2621
+ label: catalogEntry?.label ?? null,
2622
+ description: catalogEntry?.description ?? null,
2623
+ publicLabel: catalogEntry?.publicLabel ?? null,
2624
+ publicDescription: catalogEntry?.publicDescription ?? null
2625
+ });
2626
+ return acc;
2627
+ }, [])
2628
+ .sort((a, b) => {
2629
+ const orderA = featureCatalogByKey.get(a.featureKey)?.publicDisplayOrder;
2630
+ const orderB = featureCatalogByKey.get(b.featureKey)?.publicDisplayOrder;
2631
+ const normalizedOrderA = typeof orderA === 'number' && Number.isFinite(orderA)
2632
+ ? orderA
2633
+ : Number.MAX_SAFE_INTEGER;
2634
+ const normalizedOrderB = typeof orderB === 'number' && Number.isFinite(orderB)
2635
+ ? orderB
2636
+ : Number.MAX_SAFE_INTEGER;
2637
+ if (normalizedOrderA !== normalizedOrderB) {
2638
+ return normalizedOrderA - normalizedOrderB;
2639
+ }
2640
+ const catalogIndexA = featureCatalogIndexByKey.get(a.featureKey) ?? Number.MAX_SAFE_INTEGER;
2641
+ const catalogIndexB = featureCatalogIndexByKey.get(b.featureKey) ?? Number.MAX_SAFE_INTEGER;
2642
+ if (catalogIndexA !== catalogIndexB) {
2643
+ return catalogIndexA - catalogIndexB;
2644
+ }
2645
+ return a.featureKey.localeCompare(b.featureKey);
2410
2646
  }),
2411
2647
  pricing: {
2412
2648
  month: monthPrice ? {
@@ -2426,15 +2662,21 @@ export function registerBillingRoutes(deps) {
2426
2662
  }
2427
2663
  };
2428
2664
  }).filter(Boolean);
2429
- res.writeHead(200, { 'Content-Type': 'application/json' });
2430
- res.end(JSON.stringify({
2665
+ const payload = {
2431
2666
  asOf: new Date().toISOString(),
2432
2667
  pricingSource: {
2433
- environment: requestPricingEnvironment(req),
2668
+ environment,
2434
2669
  runtimeMode: 'cloud'
2435
2670
  },
2436
2671
  plans: plansPayload
2437
- }));
2672
+ };
2673
+ publicPricingCache.set(cacheKey, {
2674
+ payload,
2675
+ version: cacheVersion,
2676
+ expiresAt: Date.now() + PUBLIC_PRICING_CACHE_TTL_MS
2677
+ });
2678
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2679
+ res.end(JSON.stringify(payload));
2438
2680
  }
2439
2681
  catch (error) {
2440
2682
  const statusCode = resolveErrorStatusCode(error, 500);