@taskforcehq/taskforce 0.3.305 → 0.3.307

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (440) hide show
  1. package/README.md +0 -2
  2. package/dist/Taskforce.module.css +603 -107
  3. package/dist/TaskforceCore.js +216 -43
  4. package/dist/TaskforceCore.test.js +611 -28
  5. package/dist/assets/fonts/CourierPrime-Bold.woff2 +0 -0
  6. package/dist/assets/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  7. package/dist/assets/fonts/CourierPrime-Italic.woff2 +0 -0
  8. package/dist/assets/fonts/CourierPrime-Regular.woff2 +0 -0
  9. package/dist/assets/fonts/Noir_medium.woff2 +0 -0
  10. package/dist/assets/fonts/Noir_regular.woff2 +0 -0
  11. package/dist/assets/fonts/RussoOne-Regular.woff2 +0 -0
  12. package/dist/assets/fonts/SpecialElite-Regular.woff2 +0 -0
  13. package/dist/compat/workspaceSyncCompat.js +46 -2
  14. package/dist/compat/workspaceSyncCompat.test.js +38 -1
  15. package/dist/components/activity/EntityActivityTimeline.d.ts +34 -0
  16. package/dist/components/activity/EntityActivityTimeline.js +81 -0
  17. package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
  18. package/dist/components/context/ContextAttachmentManager.js +561 -0
  19. package/dist/components/features/AgentsModule.d.ts +14 -1
  20. package/dist/components/features/AgentsModule.js +366 -18
  21. package/dist/components/features/AgentsModule.test.js +507 -8
  22. package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +3 -1
  23. package/dist/components/features/AnnotatedAttachmentWorkspace.js +118 -4
  24. package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +52 -0
  25. package/dist/components/features/DocumentIndex.d.ts +2 -2
  26. package/dist/components/features/DocumentIndex.js +3 -4
  27. package/dist/components/features/DocumentViewer.d.ts +7 -1
  28. package/dist/components/features/DocumentViewer.js +62 -4
  29. package/dist/components/features/DocumentWorkspace.d.ts +10 -2
  30. package/dist/components/features/DocumentWorkspace.js +15 -4
  31. package/dist/components/features/DocumentWorkspace.test.js +51 -1
  32. package/dist/components/features/TaskSettings.d.ts +2 -0
  33. package/dist/components/features/TaskSettings.js +83 -78
  34. package/dist/components/features/TaskSettings.test.js +171 -82
  35. package/dist/components/features/documentWorkspaceModel.d.ts +1 -1
  36. package/dist/components/task/TaskActionHeader.js +11 -5
  37. package/dist/components/task/TaskActionHeader.test.js +10 -0
  38. package/dist/components/task/TaskCard.js +24 -29
  39. package/dist/components/task/TaskCard.test.js +38 -3
  40. package/dist/components/task/TaskContextUpload.js +4 -526
  41. package/dist/components/task/TaskContextUpload.test.js +3 -3
  42. package/dist/components/task/TaskForm.js +61 -116
  43. package/dist/components/task/TaskForm.test.js +110 -8
  44. package/dist/components/task/TaskKanban.d.ts +2 -1
  45. package/dist/components/task/TaskKanban.js +94 -21
  46. package/dist/components/task/TaskKanban.test.js +36 -0
  47. package/dist/components/task/TaskList.js +5 -1
  48. package/dist/components/task/TaskList.test.js +4 -0
  49. package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
  50. package/dist/components/ui/EditableAvatarButton.js +18 -0
  51. package/dist/components/ui/HierarchyActionBadge.d.ts +10 -0
  52. package/dist/components/ui/HierarchyActionBadge.js +5 -0
  53. package/dist/components/views/AppShellHeader.d.ts +3 -1
  54. package/dist/components/views/AppShellHeader.js +27 -11
  55. package/dist/components/views/AppShellHeader.test.js +18 -1
  56. package/dist/components/views/PlanComparisonPage.d.ts +9 -1
  57. package/dist/components/views/PlanComparisonPage.js +78 -14
  58. package/dist/components/views/PlanComparisonPage.test.js +257 -0
  59. package/dist/components/views/PlansPage.js +127 -36
  60. package/dist/components/views/PlansPage.test.js +294 -12
  61. package/dist/components/views/StandaloneLayout.js +529 -592
  62. package/dist/components/views/WidgetView.js +10 -8
  63. package/dist/components/views/panels/PlanningDrawer.d.ts +25 -5
  64. package/dist/components/views/panels/PlanningDrawer.js +270 -55
  65. package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
  66. package/dist/components/views/panels/PlanningDrawer.test.js +529 -0
  67. package/dist/components/views/planningScope.d.ts +20 -0
  68. package/dist/components/views/planningScope.js +25 -0
  69. package/dist/components/views/planningScope.test.d.ts +1 -0
  70. package/dist/components/views/planningScope.test.js +60 -0
  71. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +26 -1
  72. package/dist/components/views/standalone/modals/AccountHubModal.js +76 -2
  73. package/dist/components/views/standalone/modals/AccountHubModal.test.js +20 -3
  74. package/dist/components/views/standalone/modals/AvatarImageManagerModal.d.ts +20 -0
  75. package/dist/components/views/standalone/modals/AvatarImageManagerModal.js +127 -0
  76. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +2 -9
  77. package/dist/components/views/standalone/modals/EditProfileModal.js +3 -2
  78. package/dist/components/views/standalone/modals/SyncEnableWarningModal.d.ts +9 -0
  79. package/dist/components/views/standalone/modals/SyncEnableWarningModal.js +6 -0
  80. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +1 -0
  81. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +24 -0
  82. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +2 -1
  83. package/dist/components/views/standalone/modals/SyncStatusModal.js +2 -2
  84. package/dist/config/envSchema.js +1 -0
  85. package/dist/core/AiProfileService.d.ts +46 -2
  86. package/dist/core/AiProfileService.js +1073 -82
  87. package/dist/core/AiProfileService.test.js +19 -1
  88. package/dist/core/AiProfiles.test.js +1249 -44
  89. package/dist/core/AttachmentLinkService.d.ts +3 -0
  90. package/dist/core/AttachmentLinkService.js +110 -23
  91. package/dist/core/AuthIdentityService.d.ts +107 -0
  92. package/dist/core/AuthIdentityService.js +284 -0
  93. package/dist/core/AuthTokenService.d.ts +4 -0
  94. package/dist/core/AuthTokenService.js +34 -6
  95. package/dist/core/AuthTokenService.test.js +3 -3
  96. package/dist/core/EntitlementsPolicy.test.js +2 -2
  97. package/dist/core/GlobalSettingsService.js +127 -6
  98. package/dist/core/McpTokenService.d.ts +12 -0
  99. package/dist/core/McpTokenService.js +44 -1
  100. package/dist/core/PlanEntitlementService.d.ts +23 -2
  101. package/dist/core/PlanEntitlementService.js +343 -57
  102. package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
  103. package/dist/core/PlanFeatureCatalog.test.js +84 -0
  104. package/dist/core/PlanVersionPolicy.test.js +35 -2
  105. package/dist/core/PlanningEntities.test.js +418 -4
  106. package/dist/core/SignupMonetizationSettings.test.js +199 -27
  107. package/dist/core/SyncReconciliationService.js +2 -65
  108. package/dist/core/SystemAdmin.test.js +42 -42
  109. package/dist/core/TaskActivityService.d.ts +28 -1
  110. package/dist/core/TaskActivityService.js +227 -46
  111. package/dist/core/TaskActivitySync.test.d.ts +1 -0
  112. package/dist/core/TaskActivitySync.test.js +90 -0
  113. package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
  114. package/dist/core/TaskAuditTimeline.test.js +96 -0
  115. package/dist/core/Taskforce.d.ts +415 -40
  116. package/dist/core/Taskforce.ids.test.js +58 -0
  117. package/dist/core/Taskforce.js +1543 -266
  118. package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
  119. package/dist/core/Taskforce.listTasksSlim.test.js +91 -0
  120. package/dist/core/Taskforce.mcpAuth.test.js +109 -0
  121. package/dist/core/UserProfileAvatarDrafts.test.js +30 -3
  122. package/dist/core/WorkspaceLifecycleService.js +0 -5
  123. package/dist/core/WorkspacePermissions.test.js +25 -1
  124. package/dist/core/WorkspacePlanMode.test.js +117 -16
  125. package/dist/core/shared.d.ts +1 -1
  126. package/dist/core/shared.js +5 -1
  127. package/dist/core/types.d.ts +75 -2
  128. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +28 -2
  129. package/dist/hooks/bootstrapTimeouts.d.ts +4 -0
  130. package/dist/hooks/bootstrapTimeouts.js +8 -0
  131. package/dist/hooks/bootstrapTimeouts.test.d.ts +1 -0
  132. package/dist/hooks/bootstrapTimeouts.test.js +12 -0
  133. package/dist/hooks/sync/bootstrap.js +2 -0
  134. package/dist/hooks/sync/controlPlane.d.ts +37 -0
  135. package/dist/hooks/sync/controlPlane.js +78 -0
  136. package/dist/hooks/sync/controlPlane.test.d.ts +1 -0
  137. package/dist/hooks/sync/controlPlane.test.js +121 -0
  138. package/dist/hooks/sync/lifecyclePolicy.d.ts +45 -0
  139. package/dist/hooks/sync/lifecyclePolicy.js +93 -0
  140. package/dist/hooks/sync/lifecyclePolicy.test.d.ts +1 -0
  141. package/dist/hooks/sync/lifecyclePolicy.test.js +124 -0
  142. package/dist/hooks/sync/orchestratorShared.d.ts +13 -4
  143. package/dist/hooks/sync/orchestratorShared.js +129 -48
  144. package/dist/hooks/sync/orchestratorShared.test.js +50 -1
  145. package/dist/hooks/sync/pullLifecycle.d.ts +31 -0
  146. package/dist/hooks/sync/pullLifecycle.js +24 -0
  147. package/dist/hooks/sync/pullLifecycle.test.d.ts +1 -0
  148. package/dist/hooks/sync/pullLifecycle.test.js +80 -0
  149. package/dist/hooks/sync/recovery.d.ts +16 -2
  150. package/dist/hooks/sync/recovery.js +179 -53
  151. package/dist/hooks/sync/recovery.test.d.ts +1 -0
  152. package/dist/hooks/sync/recovery.test.js +292 -0
  153. package/dist/hooks/sync/transfers.d.ts +16 -3
  154. package/dist/hooks/sync/transfers.js +210 -66
  155. package/dist/hooks/sync/transfers.test.d.ts +1 -0
  156. package/dist/hooks/sync/transfers.test.js +928 -0
  157. package/dist/hooks/sync/useRecentSyncEvents.d.ts +21 -0
  158. package/dist/hooks/sync/useRecentSyncEvents.js +92 -0
  159. package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +1 -0
  160. package/dist/hooks/sync/useRecentSyncEvents.test.js +124 -0
  161. package/dist/hooks/sync/useSyncStatusActions.d.ts +30 -0
  162. package/dist/hooks/sync/useSyncStatusActions.js +88 -0
  163. package/dist/hooks/sync/useSyncStatusActions.test.d.ts +1 -0
  164. package/dist/hooks/sync/useSyncStatusActions.test.js +133 -0
  165. package/dist/hooks/sync/useSyncStatusControls.d.ts +25 -0
  166. package/dist/hooks/sync/useSyncStatusControls.js +60 -0
  167. package/dist/hooks/sync/useSyncStatusControls.test.d.ts +1 -0
  168. package/dist/hooks/sync/useSyncStatusControls.test.js +88 -0
  169. package/dist/hooks/useFilterSort.js +13 -13
  170. package/dist/hooks/useFilterSort.test.js +87 -1
  171. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +12 -0
  172. package/dist/hooks/useSyncOrchestrator.d.ts +9 -2
  173. package/dist/hooks/useSyncOrchestrator.js +409 -183
  174. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +407 -0
  175. package/dist/hooks/useTaskData.js +8 -3
  176. package/dist/hooks/useTaskData.test.js +45 -0
  177. package/dist/hooks/useTaskforce.d.ts +145 -52
  178. package/dist/hooks/useTaskforce.js +592 -253
  179. package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
  180. package/dist/hooks/useTaskforce.runtime-routing.test.js +246 -0
  181. package/dist/hooks/useTaskforce.sync-behavior.test.js +2468 -201
  182. package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
  183. package/dist/hooks/useWorkspaceSyncController.js +13 -2
  184. package/dist/hooks/useWorkspaceSyncController.test.js +48 -0
  185. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
  186. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +328 -40
  187. package/dist/localization/locales/en-US.d.ts +0 -1
  188. package/dist/localization/locales/en-US.js +0 -1
  189. package/dist/localization/locales/es-419.js +0 -1
  190. package/dist/localization/locales/pt-BR.js +0 -1
  191. package/dist/mcp/canonicalAssetHelpers.d.ts +16 -4
  192. package/dist/mcp/canonicalAssetHelpers.js +18 -10
  193. package/dist/mcp/canonicalAssetHelpers.test.d.ts +1 -0
  194. package/dist/mcp/canonicalAssetHelpers.test.js +67 -0
  195. package/dist/mcp/clientIntegrations.d.ts +1 -0
  196. package/dist/mcp/clientIntegrations.js +25 -0
  197. package/dist/mcp/clientIntegrations.test.js +14 -3
  198. package/dist/mcp/clientProfileDefaults.js +3 -3
  199. package/dist/mcp/collaborationRegistrar.js +7 -7
  200. package/dist/mcp/documentAssetRegistrar.js +116 -10
  201. package/dist/mcp/runtime.d.ts +3 -0
  202. package/dist/mcp/runtime.js +1099 -185
  203. package/dist/mcp/runtime.test.js +1491 -140
  204. package/dist/mcp/taskAttachmentTypes.d.ts +1 -1
  205. package/dist/mcp/taskPlanningRegistrar.js +48 -0
  206. package/dist/mcp/toolRegistry.d.ts +56 -0
  207. package/dist/mcp/toolRegistry.js +8 -0
  208. package/dist/mcp/toolRegistry.test.js +4 -0
  209. package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
  210. package/dist/migrations/taskSchemaMigrations.js +192 -61
  211. package/dist/migrations/taskSchemaMigrations.test.js +15 -0
  212. package/dist/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  213. package/dist/resources/templates/workflows/collaborate.yaml +1 -1
  214. package/dist/resources/templates/workflows/evaluate.yaml +2 -2
  215. package/dist/resources/templates/workflows/plan.yaml +1 -1
  216. package/dist/resources/templates/workflows/review.yaml +2 -2
  217. package/dist/server/aiProfileAvatarAssets.d.ts +49 -0
  218. package/dist/server/aiProfileAvatarAssets.js +319 -0
  219. package/dist/server/aiProfileAvatarAssets.test.d.ts +1 -0
  220. package/dist/server/aiProfileAvatarAssets.test.js +213 -0
  221. package/dist/server/annotatedAttachmentsRoutes.test.js +69 -10
  222. package/dist/server/auth/providers/apple.d.ts +33 -0
  223. package/dist/server/auth/providers/apple.js +82 -0
  224. package/dist/server/auth/providers/appleClientSecret.d.ts +37 -0
  225. package/dist/server/auth/providers/appleClientSecret.js +65 -0
  226. package/dist/server/auth/providers/github.d.ts +26 -0
  227. package/dist/server/auth/providers/github.js +86 -0
  228. package/dist/server/auth/providers/google.d.ts +21 -0
  229. package/dist/server/auth/providers/google.js +56 -0
  230. package/dist/server/auth/providers/types.d.ts +21 -0
  231. package/dist/server/auth/providers/types.js +1 -0
  232. package/dist/server/auth.d.ts +2 -0
  233. package/dist/server/auth.js +6 -3
  234. package/dist/server/documentReviewRoutes.test.js +36 -3
  235. package/dist/server/index.cookieProxy.test.js +1 -0
  236. package/dist/server/index.d.ts +12 -0
  237. package/dist/server/index.js +124 -9
  238. package/dist/server/index.rateLimit.test.js +3 -0
  239. package/dist/server/index.test.js +111 -3
  240. package/dist/server/routes/admin.d.ts +3 -0
  241. package/dist/server/routes/admin.js +459 -28
  242. package/dist/server/routes/annotatedAttachments.js +51 -0
  243. package/dist/server/routes/auth.d.ts +26 -2
  244. package/dist/server/routes/auth.js +1170 -71
  245. package/dist/server/routes/authSupport.d.ts +5 -1
  246. package/dist/server/routes/authSupport.js +13 -2
  247. package/dist/server/routes/billing.js +372 -35
  248. package/dist/server/routes/billing.test.js +595 -12
  249. package/dist/server/routes/documents.d.ts +1 -0
  250. package/dist/server/routes/documents.js +182 -70
  251. package/dist/server/routes/shared.d.ts +28 -2
  252. package/dist/server/routes/shared.js +69 -14
  253. package/dist/server/routes/sync.integration.test.js +658 -39
  254. package/dist/server/routes/sync.js +74 -608
  255. package/dist/server/routes/syncAuxRoutes.d.ts +15 -0
  256. package/dist/server/routes/syncAuxRoutes.js +91 -0
  257. package/dist/server/routes/syncAuxRoutes.test.d.ts +1 -0
  258. package/dist/server/routes/syncAuxRoutes.test.js +158 -0
  259. package/dist/server/routes/syncPullApplyRoutes.d.ts +36 -0
  260. package/dist/server/routes/syncPullApplyRoutes.js +204 -0
  261. package/dist/server/routes/syncPushRoutes.d.ts +24 -0
  262. package/dist/server/routes/syncPushRoutes.js +212 -0
  263. package/dist/server/routes/syncRouteGuards.d.ts +36 -0
  264. package/dist/server/routes/syncRouteGuards.js +67 -0
  265. package/dist/server/routes/syncRouteGuards.test.d.ts +1 -0
  266. package/dist/server/routes/syncRouteGuards.test.js +94 -0
  267. package/dist/server/routes/syncRouteTypes.d.ts +13 -0
  268. package/dist/server/routes/syncRouteTypes.js +1 -0
  269. package/dist/server/routes/syncSnapshotRoutes.d.ts +66 -0
  270. package/dist/server/routes/syncSnapshotRoutes.js +182 -0
  271. package/dist/server/routes/tasks.js +55 -10
  272. package/dist/server/routes/workspaces.js +36 -5
  273. package/dist/server/routes.js +147 -11
  274. package/dist/server/routes.test.js +2704 -183
  275. package/dist/services/workflowExportSnapshots.test.js +2 -0
  276. package/dist/shared/aiProfileSeatScope.d.ts +5 -0
  277. package/dist/shared/aiProfileSeatScope.js +21 -0
  278. package/dist/shared/aiProfileSurfaceType.d.ts +1 -1
  279. package/dist/shared/aiProfileSurfaceType.js +10 -8
  280. package/dist/shared/passwordPolicy.d.ts +13 -0
  281. package/dist/shared/passwordPolicy.js +84 -0
  282. package/dist/shared/passwordPolicy.test.d.ts +1 -0
  283. package/dist/shared/passwordPolicy.test.js +34 -0
  284. package/dist/storage/objectStorage.d.ts +1 -0
  285. package/dist/storage/objectStorage.js +2 -1
  286. package/dist/storage/objectStorageClient.d.ts +1 -0
  287. package/dist/storage/objectStorageClient.js +41 -8
  288. package/dist/storage/objectStorageClient.test.js +26 -1
  289. package/dist/storage/objectStorageConfig.js +3 -1
  290. package/dist/storage/objectStorageConfig.test.js +13 -1
  291. package/dist/storage/workspaceAssetStore.d.ts +15 -3
  292. package/dist/storage/workspaceAssetStore.js +132 -44
  293. package/dist/storage/workspaceAssetStore.test.js +93 -6
  294. package/dist/styles/fonts.css +70 -0
  295. package/dist/sync/collaborationSyncPayload.d.ts +6 -1
  296. package/dist/sync/collaborationSyncPayload.js +19 -11
  297. package/dist/sync/collaborationSyncPayload.test.js +14 -1
  298. package/dist/sync/collaborationSyncState.d.ts +3 -3
  299. package/dist/sync/collaborationSyncState.js +19 -12
  300. package/dist/sync/contentSyncPayload.d.ts +5 -1
  301. package/dist/sync/contentSyncPayload.js +9 -0
  302. package/dist/sync/contentSyncPayload.test.js +39 -0
  303. package/dist/sync/contentSyncState.d.ts +9 -9
  304. package/dist/sync/contentSyncState.js +248 -107
  305. package/dist/sync/planningSyncPayload.js +5 -0
  306. package/dist/sync/planningSyncPayload.test.js +24 -0
  307. package/dist/sync/syncApplyHandlers.d.ts +11 -4
  308. package/dist/sync/syncApplyHandlers.js +115 -42
  309. package/dist/sync/syncApplyHandlers.test.js +251 -4
  310. package/dist/sync/syncFieldSemantics.d.ts +12 -0
  311. package/dist/sync/syncFieldSemantics.js +55 -0
  312. package/dist/sync/syncResourceAdapters.d.ts +46 -0
  313. package/dist/sync/syncResourceAdapters.js +79 -0
  314. package/dist/sync/syncService.d.ts +6 -0
  315. package/dist/sync/syncService.js +65 -1
  316. package/dist/sync/syncService.test.js +96 -3
  317. package/dist/sync/taskSyncChangeKey.d.ts +2 -0
  318. package/dist/sync/taskSyncChangeKey.js +143 -0
  319. package/dist/sync/taskSyncPayload.js +3 -8
  320. package/dist/sync/workspacePullFeed.d.ts +4 -0
  321. package/dist/sync/workspacePullFeed.js +132 -72
  322. package/dist/sync/workspacePullFeed.test.js +92 -0
  323. package/dist/sync/workspaceRepair.js +5 -2
  324. package/dist/sync/workspaceSyncModel.d.ts +73 -6
  325. package/dist/sync/workspaceSyncModel.js +342 -73
  326. package/dist/sync/workspaceSyncModel.test.js +559 -14
  327. package/dist/sync/workspaceSyncState.d.ts +17 -0
  328. package/dist/sync/workspaceSyncState.js +2 -1
  329. package/dist/sync/workspaceSyncSurface.js +3 -8
  330. package/dist/types.d.ts +50 -2
  331. package/dist/ui/assets/AgentsModule-2y82_3DO.js +1 -0
  332. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BTLZZCiQ.js +3 -0
  333. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BVKew-mH.css +1 -0
  334. package/dist/ui/assets/ContextAttachmentManager-D2epuNnG.js +3 -0
  335. package/dist/ui/assets/CourierPrime-Bold-BuLj_dpw.woff2 +0 -0
  336. package/dist/ui/assets/CourierPrime-BoldItalic-BrK2pUkZ.woff2 +0 -0
  337. package/dist/ui/assets/CourierPrime-Italic-DFUZK5Ey.woff2 +0 -0
  338. package/dist/ui/assets/CourierPrime-Regular-BsKphzVf.woff2 +0 -0
  339. package/dist/ui/assets/DocumentWorkspace-D4rJKXzd.css +1 -0
  340. package/dist/ui/assets/DocumentWorkspace-sVa-3Do6.js +252 -0
  341. package/dist/ui/assets/EntityActivityTimeline-DyqvvhuF.css +1 -0
  342. package/dist/ui/assets/EntityActivityTimeline-Zods27y_.js +1 -0
  343. package/dist/ui/assets/{InitiativesModule-Dhm7M8e7.js → InitiativesModule-D4XseTwW.js} +1 -1
  344. package/dist/ui/assets/Noir_medium-bxQwKGzB.woff2 +0 -0
  345. package/dist/ui/assets/Noir_regular-ojf2kxlG.woff2 +0 -0
  346. package/dist/ui/assets/PlansPage-D5AcbO0L.js +1 -0
  347. package/dist/ui/assets/PlansPage-Dvqsz5zc.css +1 -0
  348. package/dist/ui/assets/RussoOne-Regular-Cu_qq_qC.woff2 +0 -0
  349. package/dist/ui/assets/SpecialElite-Regular-Di6gSvAS.woff2 +0 -0
  350. package/dist/ui/assets/TaskContextUpload-uC5qx4Bv.js +1 -0
  351. package/dist/ui/assets/TaskSettings-CqvPAsTv.js +9 -0
  352. package/dist/ui/assets/{WorkflowsModule-BcS4afRn.js → WorkflowsModule-oOA9bcdK.js} +4 -4
  353. package/dist/ui/assets/documentReferences-DPZuTgHh.js +1 -0
  354. package/dist/ui/assets/index-607WPo_X.js +4 -0
  355. package/dist/ui/assets/index-CYyodXsg.css +1 -0
  356. package/dist/ui/assets/{vendor-icons-BSUaRwF8.js → vendor-icons-B32hGuF2.js} +1 -1
  357. package/dist/ui/fonts/CourierPrime-Bold.ttf +0 -0
  358. package/dist/ui/fonts/CourierPrime-Bold.woff2 +0 -0
  359. package/dist/ui/fonts/CourierPrime-BoldItalic.ttf +0 -0
  360. package/dist/ui/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  361. package/dist/ui/fonts/CourierPrime-Italic.ttf +0 -0
  362. package/dist/ui/fonts/CourierPrime-Italic.woff2 +0 -0
  363. package/dist/ui/fonts/CourierPrime-Regular.ttf +0 -0
  364. package/dist/ui/fonts/CourierPrime-Regular.woff2 +0 -0
  365. package/dist/ui/fonts/Noir_medium.otf +0 -0
  366. package/dist/ui/fonts/Noir_medium.woff2 +0 -0
  367. package/dist/ui/fonts/Noir_regular.otf +0 -0
  368. package/dist/ui/fonts/Noir_regular.woff2 +0 -0
  369. package/dist/ui/fonts/RussoOne-Regular.woff2 +0 -0
  370. package/dist/ui/fonts/SpecialElite-Regular.ttf +0 -0
  371. package/dist/ui/fonts/SpecialElite-Regular.woff2 +0 -0
  372. package/dist/ui/index.html +3 -3
  373. package/dist/ui/product/agent manager.png +0 -0
  374. package/dist/ui/product/agent_manager_02.png +0 -0
  375. package/dist/utils/accountProfileSummaryCache.d.ts +5 -0
  376. package/dist/utils/aiProfileEvents.d.ts +7 -0
  377. package/dist/utils/aiProfileEvents.js +6 -0
  378. package/dist/utils/assignees.d.ts +8 -0
  379. package/dist/utils/assignees.js +10 -1
  380. package/dist/utils/assignees.test.d.ts +1 -0
  381. package/dist/utils/assignees.test.js +25 -0
  382. package/dist/utils/avatarDisplay.d.ts +1 -0
  383. package/dist/utils/avatarDisplay.js +15 -0
  384. package/dist/utils/avatarUpload.d.ts +27 -0
  385. package/dist/utils/avatarUpload.js +132 -0
  386. package/dist/utils/avatarUpload.test.d.ts +1 -0
  387. package/dist/utils/avatarUpload.test.js +40 -0
  388. package/dist/utils/bootstrapTrace.d.ts +7 -0
  389. package/dist/utils/bootstrapTrace.js +29 -0
  390. package/dist/utils/commercialLifecyclePresentation.js +3 -0
  391. package/dist/utils/contextAssetEvents.d.ts +2 -0
  392. package/dist/utils/contextFiles.d.ts +3 -1
  393. package/dist/utils/contextFiles.js +2 -1
  394. package/dist/utils/syncEventDetails.d.ts +3 -0
  395. package/dist/utils/syncEventDetails.js +64 -0
  396. package/dist/utils/syncEventDetails.test.d.ts +1 -0
  397. package/dist/utils/syncEventDetails.test.js +23 -0
  398. package/dist/utils/syncEventPresentation.d.ts +15 -0
  399. package/dist/utils/syncEventPresentation.js +129 -0
  400. package/dist/utils/syncEventPresentation.test.d.ts +1 -0
  401. package/dist/utils/syncEventPresentation.test.js +64 -0
  402. package/dist/utils/syncStatusPresentation.d.ts +72 -0
  403. package/dist/utils/syncStatusPresentation.js +217 -0
  404. package/dist/utils/syncStatusPresentation.test.d.ts +1 -0
  405. package/dist/utils/syncStatusPresentation.test.js +164 -0
  406. package/dist/utils/taskActivity.d.ts +9 -1
  407. package/dist/utils/taskActivity.js +290 -22
  408. package/dist/utils/taskActivity.test.js +141 -8
  409. package/dist/utils/taskSearch.d.ts +22 -0
  410. package/dist/utils/taskSearch.js +87 -0
  411. package/dist/utils/taskSearch.test.d.ts +1 -0
  412. package/dist/utils/taskSearch.test.js +69 -0
  413. package/dist/utils/taskUtils.js +10 -3
  414. package/dist/utils/taskUtils.test.d.ts +1 -0
  415. package/dist/utils/taskUtils.test.js +19 -0
  416. package/dist/utils/workspaceSyncPresentation.d.ts +4 -0
  417. package/dist/utils/workspaceSyncPresentation.js +68 -2
  418. package/dist/utils/workspaceSyncPresentation.test.js +36 -1
  419. package/package.json +3 -2
  420. package/scripts/export-sqlite-to-postgres.mjs +7 -9
  421. package/scripts/run-e2e-realtime.sh +17 -3
  422. package/scripts/run-smoke.sh +17 -5
  423. package/scripts/run-soak.sh +17 -5
  424. package/src/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  425. package/src/resources/templates/workflows/collaborate.yaml +1 -1
  426. package/src/resources/templates/workflows/evaluate.yaml +2 -2
  427. package/src/resources/templates/workflows/plan.yaml +1 -1
  428. package/src/resources/templates/workflows/review.yaml +2 -2
  429. package/dist/ui/assets/AgentsModule-N2MnQBZk.js +0 -1
  430. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BG6P_GVW.js +0 -3
  431. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +0 -1
  432. package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +0 -1
  433. package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +0 -252
  434. package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
  435. package/dist/ui/assets/PlansPage-Cq0zXj3I.js +0 -1
  436. package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
  437. package/dist/ui/assets/TaskSettings-ln0aF7xc.js +0 -9
  438. package/dist/ui/assets/index-C2-OXZV7.css +0 -1
  439. package/dist/ui/assets/index-DcSI5F86.js +0 -6
  440. package/scripts/migrate-planning-model.ts +0 -233
@@ -180,6 +180,16 @@ function setupBillingSchema(db) {
180
180
  created_at TEXT NOT NULL,
181
181
  updated_at TEXT NOT NULL
182
182
  );
183
+ CREATE TABLE plan_feature_matrix (
184
+ tenant_id TEXT NOT NULL,
185
+ plan_version_id TEXT NOT NULL,
186
+ feature_key TEXT NOT NULL,
187
+ access TEXT NOT NULL DEFAULT 'disabled',
188
+ config_json TEXT NOT NULL DEFAULT '{}',
189
+ created_at TEXT NOT NULL,
190
+ updated_at TEXT NOT NULL,
191
+ PRIMARY KEY (tenant_id, plan_version_id, feature_key)
192
+ );
183
193
  CREATE TABLE plan_catalog (
184
194
  plan_id TEXT PRIMARY KEY,
185
195
  tenant_id TEXT NOT NULL,
@@ -188,6 +198,16 @@ function setupBillingSchema(db) {
188
198
  created_at TEXT NOT NULL,
189
199
  updated_at TEXT NOT NULL
190
200
  );
201
+ CREATE TABLE plan_feature_catalog_overrides (
202
+ tenant_id TEXT NOT NULL,
203
+ feature_key TEXT NOT NULL,
204
+ public_label TEXT,
205
+ public_description TEXT,
206
+ public_display_order INTEGER,
207
+ created_at TEXT NOT NULL,
208
+ updated_at TEXT NOT NULL,
209
+ PRIMARY KEY (tenant_id, feature_key)
210
+ );
191
211
  CREATE TABLE account_entitlements (
192
212
  account_id TEXT PRIMARY KEY,
193
213
  tenant_id TEXT NOT NULL,
@@ -208,6 +228,32 @@ function setupBillingSchema(db) {
208
228
  );
209
229
  `);
210
230
  }
231
+ function readMockPlanVersionFeatures(db, tenantId, planVersionId) {
232
+ try {
233
+ return db.prepare(`
234
+ SELECT feature_key, access, config_json
235
+ FROM plan_feature_matrix
236
+ WHERE tenant_id = ? AND plan_version_id = ?
237
+ `).all(tenantId, planVersionId).reduce((acc, row) => {
238
+ const featureKey = String(row?.feature_key || '').trim();
239
+ if (!featureKey)
240
+ return acc;
241
+ acc[featureKey] = {
242
+ featureKey,
243
+ access: String(row?.access || '').trim(),
244
+ config: row?.config_json ? JSON.parse(String(row.config_json)) : {}
245
+ };
246
+ return acc;
247
+ }, {});
248
+ }
249
+ catch (error) {
250
+ const message = String(error?.message || '');
251
+ if (message.toLowerCase().includes('no such table: plan_feature_matrix')) {
252
+ return {};
253
+ }
254
+ throw error;
255
+ }
256
+ }
211
257
  function buildBillingRouteHandlers(db, opts) {
212
258
  const routes = new Map();
213
259
  const tenantId = 'default';
@@ -288,12 +334,69 @@ function buildBillingRouteHandlers(db, opts) {
288
334
  seatLimit: row?.seat_limit === null || row?.seat_limit === undefined ? null : Number(row.seat_limit),
289
335
  trialDays: Number.isFinite(Number(metadata?.trialDays)) ? Number(metadata.trialDays) : 0,
290
336
  metadata,
291
- features: {},
337
+ features: readMockPlanVersionFeatures(db, tenantId, String(row?.plan_version_id || '')),
292
338
  createdAt: '',
293
339
  updatedAt: ''
294
340
  };
295
341
  });
296
342
  },
343
+ listPlanFeatureCatalog: () => {
344
+ const overrides = db.prepare(`
345
+ SELECT feature_key, public_label, public_description, public_display_order
346
+ FROM plan_feature_catalog_overrides
347
+ WHERE tenant_id = ?
348
+ `).all(tenantId);
349
+ const overrideMap = new Map(overrides.map((row) => [
350
+ String(row.feature_key || '').trim(),
351
+ {
352
+ publicLabel: row.public_label ?? null,
353
+ publicDescription: row.public_description ?? null,
354
+ publicDisplayOrder: row.public_display_order ?? null
355
+ }
356
+ ]));
357
+ return [
358
+ {
359
+ featureKey: 'collaboration.team_management',
360
+ label: 'Team Management',
361
+ description: 'Allow managing members, invites, roles, and collaboration settings.',
362
+ publicLabel: overrideMap.get('collaboration.team_management')?.publicLabel ?? null,
363
+ publicDescription: overrideMap.get('collaboration.team_management')?.publicDescription ?? null,
364
+ publicDisplayOrder: overrideMap.get('collaboration.team_management')?.publicDisplayOrder ?? 40,
365
+ allowedAccessModes: ['enabled', 'disabled'],
366
+ configTemplate: {}
367
+ },
368
+ {
369
+ featureKey: 'context.uploads',
370
+ label: 'Document Cloud Storage',
371
+ description: 'Allow cloud document storage with an optional storage allowance in MB.',
372
+ publicLabel: overrideMap.get('context.uploads')?.publicLabel ?? null,
373
+ publicDescription: overrideMap.get('context.uploads')?.publicDescription ?? null,
374
+ publicDisplayOrder: overrideMap.get('context.uploads')?.publicDisplayOrder ?? 30,
375
+ allowedAccessModes: ['enabled', 'disabled'],
376
+ configTemplate: { storageLimitMb: 1024 }
377
+ },
378
+ {
379
+ featureKey: 'workspace.workspaces',
380
+ label: 'Workspaces',
381
+ description: 'Allow multiple workspaces with a configurable workspace limit.',
382
+ publicLabel: overrideMap.get('workspace.workspaces')?.publicLabel ?? null,
383
+ publicDescription: overrideMap.get('workspace.workspaces')?.publicDescription ?? null,
384
+ publicDisplayOrder: overrideMap.get('workspace.workspaces')?.publicDisplayOrder ?? 10,
385
+ allowedAccessModes: ['enabled', 'disabled'],
386
+ configTemplate: { maxWorkspaces: 1 }
387
+ },
388
+ {
389
+ featureKey: 'collaboration.ai_profiles',
390
+ label: 'AI Profiles',
391
+ description: 'Allow registered AI collaborator profiles with an optional per-workspace limit.',
392
+ publicLabel: overrideMap.get('collaboration.ai_profiles')?.publicLabel ?? null,
393
+ publicDescription: overrideMap.get('collaboration.ai_profiles')?.publicDescription ?? null,
394
+ publicDisplayOrder: overrideMap.get('collaboration.ai_profiles')?.publicDisplayOrder ?? 20,
395
+ allowedAccessModes: ['enabled', 'limited', 'disabled'],
396
+ configTemplate: { maxAiProfiles: 1 }
397
+ }
398
+ ];
399
+ },
297
400
  getAccountEntitlement: () => {
298
401
  const entitlement = opts?.accessStatus?.entitlement;
299
402
  return {
@@ -691,6 +794,72 @@ describe('Billing Webhook Routes', () => {
691
794
  `).get();
692
795
  expect(String(failure?.reason || '')).toContain('no longer current for this account');
693
796
  });
797
+ it('allows authoritative subscription webhooks to recover accounts after the local checkout expired', async () => {
798
+ const now = new Date().toISOString();
799
+ db.prepare(`
800
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
801
+ VALUES ('team-v1', 'default', 'team', 1, 1, '{}', ?, ?)
802
+ `).run(now, now);
803
+ db.prepare(`
804
+ INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, stripe_price_id, active, created_at, updated_at)
805
+ VALUES ('default', 'team-v1', 'month', 'price_team_month', 1, ?, ?)
806
+ `).run(now, now);
807
+ db.prepare(`
808
+ INSERT INTO user_billing (
809
+ user_id, tenant_id, stripe_customer_id, entitlement_state, created_at, updated_at
810
+ ) VALUES ('u1', 'default', 'cus_123', 'pending_plan_selection', ?, ?)
811
+ `).run(now, now);
812
+ db.prepare(`
813
+ INSERT INTO account_entitlements (
814
+ account_id, tenant_id, plan_version_id, entitlement_state, effective_at, effective_until, updated_by, created_at, updated_at
815
+ ) VALUES ('u1', 'default', 'team-v1', 'pending_plan_selection', ?, NULL, 'checkout-expired', ?, ?)
816
+ `).run(now, now, now);
817
+ mockConstructEvent.mockReturnValue({
818
+ id: 'evt_checkout_expired_recovery',
819
+ type: 'customer.subscription.updated',
820
+ data: {
821
+ object: {
822
+ id: 'sub_123',
823
+ customer: 'cus_123',
824
+ status: 'active',
825
+ trial_end: null,
826
+ metadata: { planVersionId: 'team-v1' },
827
+ items: {
828
+ data: [{
829
+ current_period_end: Math.floor(Date.now() / 1000) + 86400,
830
+ price: { id: 'price_team_month', recurring: { interval: 'month' } }
831
+ }]
832
+ }
833
+ }
834
+ }
835
+ });
836
+ const req = createMockReq('{"id":"evt_checkout_expired_recovery"}', {
837
+ 'x-taskforce-runtime-mode': 'cloud',
838
+ 'stripe-signature': 'sig'
839
+ });
840
+ const res = createMockRes();
841
+ await webhookHandler(req, res, {});
842
+ expect(res.statusCode).toBe(200);
843
+ const entitlement = db.prepare(`
844
+ SELECT plan_version_id, entitlement_state, updated_by
845
+ FROM account_entitlements
846
+ WHERE account_id = 'u1' AND tenant_id = 'default'
847
+ LIMIT 1
848
+ `).get();
849
+ expect(entitlement).toEqual(expect.objectContaining({
850
+ plan_version_id: 'team-v1',
851
+ entitlement_state: 'active',
852
+ updated_by: 'stripe-webhook'
853
+ }));
854
+ const failure = db.prepare(`
855
+ SELECT reason
856
+ FROM stripe_webhook_failures
857
+ WHERE event_id = 'evt_checkout_expired_recovery'
858
+ ORDER BY created_at DESC
859
+ LIMIT 1
860
+ `).get();
861
+ expect(failure).toBeUndefined();
862
+ });
694
863
  it('ignores stale subscription webhooks for active accounts when a newer checkout attempt is open', async () => {
695
864
  const now = new Date().toISOString();
696
865
  db.prepare(`
@@ -821,6 +990,9 @@ describe('Billing Webhook Routes', () => {
821
990
  const firstCall = mockCheckoutCreate.mock.calls[0];
822
991
  const args = (Array.isArray(firstCall) ? firstCall[0] : {});
823
992
  expect(String(args.success_url || '')).toContain('/?screen=plans&checkout=success');
993
+ expect(String(args.success_url || '')).toContain('planId=collab');
994
+ expect(String(args.success_url || '')).toContain('planVersionId=collab-v1');
995
+ expect(String(args.success_url || '')).toContain('interval=month');
824
996
  expect(String(args.cancel_url || '')).toContain('/?screen=plans&checkout=cancel');
825
997
  expect(String(args.cancel_url || '')).toContain('planId=collab');
826
998
  expect(String(args.cancel_url || '')).toContain('planVersionId=collab-v1');
@@ -1133,6 +1305,9 @@ describe('Billing Webhook Routes', () => {
1133
1305
  expect(res.statusCode).toBe(200);
1134
1306
  const args = mockCheckoutCreate.mock.calls.at(-1)?.[0];
1135
1307
  expect(String(args.success_url || '')).toContain('http://localhost:5174/?screen=plans&checkout=success');
1308
+ expect(String(args.success_url || '')).toContain('planId=collab');
1309
+ expect(String(args.success_url || '')).toContain('planVersionId=collab-v1');
1310
+ expect(String(args.success_url || '')).toContain('interval=month');
1136
1311
  expect(String(args.cancel_url || '')).toContain('http://localhost:5174/?screen=plans&checkout=cancel');
1137
1312
  expect(JSON.parse(res.body)).toEqual(expect.objectContaining({
1138
1313
  checkoutState: 'checkout',
@@ -1167,6 +1342,9 @@ describe('Billing Webhook Routes', () => {
1167
1342
  expect(res.statusCode).toBe(200);
1168
1343
  const args = mockCheckoutCreate.mock.calls.at(-1)?.[0];
1169
1344
  expect(String(args.success_url || '')).toContain('http://localhost:5174/?screen=plans&checkout=success');
1345
+ expect(String(args.success_url || '')).toContain('planId=collab');
1346
+ expect(String(args.success_url || '')).toContain('planVersionId=collab-v1');
1347
+ expect(String(args.success_url || '')).toContain('interval=month');
1170
1348
  });
1171
1349
  it('builds direct cloud checkout return URLs from the actual request protocol when forwarded proto is absent', async () => {
1172
1350
  const now = new Date().toISOString();
@@ -1192,6 +1370,9 @@ describe('Billing Webhook Routes', () => {
1192
1370
  expect(res.statusCode).toBe(200);
1193
1371
  const args = mockCheckoutCreate.mock.calls.at(-1)?.[0];
1194
1372
  expect(String(args.success_url || '')).toContain('http://127.0.0.1:4173/?screen=plans&checkout=success');
1373
+ expect(String(args.success_url || '')).toContain('planId=collab');
1374
+ expect(String(args.success_url || '')).toContain('planVersionId=collab-v1');
1375
+ expect(String(args.success_url || '')).toContain('interval=month');
1195
1376
  expect(String(args.cancel_url || '')).toContain('http://127.0.0.1:4173/?screen=plans&checkout=cancel');
1196
1377
  });
1197
1378
  it('applies a free plan immediately without creating a Stripe checkout session', async () => {
@@ -1822,6 +2003,118 @@ describe('Billing Webhook Routes', () => {
1822
2003
  status: 'open'
1823
2004
  }));
1824
2005
  });
2006
+ it('does not mutate billing mirror or canonical entitlement when the purchased plan version is still unresolved', async () => {
2007
+ const now = new Date().toISOString();
2008
+ db.prepare(`
2009
+ INSERT INTO billing_checkout_attempts (
2010
+ session_id, tenant_id, user_id, plan_version_id, billing_interval, status, expires_at, created_at, updated_at
2011
+ ) VALUES ('cs_plan_unresolved', 'default', 'u1', 'pro-v1', 'month', 'open', ?, ?, ?)
2012
+ `).run(new Date(Date.now() + 60 * 60 * 1000).toISOString(), now, now);
2013
+ db.prepare(`
2014
+ INSERT INTO account_entitlements (
2015
+ account_id, tenant_id, plan_version_id, entitlement_state, effective_at, updated_by, created_at, updated_at
2016
+ ) VALUES ('u1', 'default', 'free-v1', 'active', ?, 'seed', ?, ?)
2017
+ `).run(now, now, now);
2018
+ db.prepare(`
2019
+ INSERT INTO user_billing (
2020
+ user_id, tenant_id, stripe_customer_id, stripe_subscription_id, tier, entitlement_state, created_at, updated_at
2021
+ ) VALUES ('u1', 'default', 'cus_existing', 'sub_existing', 'free', 'active', ?, ?)
2022
+ `).run(now, now);
2023
+ mockCheckoutRetrieve.mockResolvedValueOnce({
2024
+ id: 'cs_plan_unresolved',
2025
+ mode: 'subscription',
2026
+ status: 'complete',
2027
+ payment_status: 'paid',
2028
+ customer: 'cus_123',
2029
+ subscription: 'sub_unmapped',
2030
+ metadata: { userId: 'u1' }
2031
+ });
2032
+ mockSubscriptionRetrieve.mockResolvedValueOnce({
2033
+ id: 'sub_unmapped',
2034
+ customer: 'cus_123',
2035
+ status: 'active',
2036
+ trial_end: null,
2037
+ metadata: { userId: 'u1' },
2038
+ items: {
2039
+ data: [{
2040
+ id: 'si_unmapped',
2041
+ current_period_end: Math.floor(Date.now() / 1000) + 86400,
2042
+ price: { id: 'price_unmapped', recurring: { interval: 'month' } }
2043
+ }]
2044
+ }
2045
+ });
2046
+ const confirmHandler = routeHandlers.get('POST /api/taskforce/billing/checkout-session/confirm');
2047
+ const req = createMockReq(JSON.stringify({
2048
+ sessionId: 'cs_plan_unresolved'
2049
+ }), {
2050
+ 'x-taskforce-runtime-mode': 'cloud',
2051
+ host: 'app.taskforcehq.ai'
2052
+ });
2053
+ const res = createMockRes();
2054
+ await confirmHandler(req, res, {});
2055
+ expect(res.statusCode).toBe(409);
2056
+ expect(JSON.parse(res.body)).toEqual({
2057
+ error: 'Checkout completed, but the purchased plan could not be resolved yet.',
2058
+ code: 'CHECKOUT_PLAN_VERSION_UNRESOLVED'
2059
+ });
2060
+ const entitlement = db.prepare(`
2061
+ SELECT plan_version_id, entitlement_state
2062
+ FROM account_entitlements
2063
+ WHERE tenant_id = 'default' AND account_id = 'u1'
2064
+ LIMIT 1
2065
+ `).get();
2066
+ expect(entitlement).toEqual(expect.objectContaining({
2067
+ plan_version_id: 'free-v1',
2068
+ entitlement_state: 'active'
2069
+ }));
2070
+ const billingRow = db.prepare(`
2071
+ SELECT stripe_customer_id, stripe_subscription_id, tier, entitlement_state
2072
+ FROM user_billing
2073
+ WHERE tenant_id = 'default' AND user_id = 'u1'
2074
+ LIMIT 1
2075
+ `).get();
2076
+ expect(billingRow).toEqual(expect.objectContaining({
2077
+ stripe_customer_id: 'cus_existing',
2078
+ stripe_subscription_id: 'sub_existing',
2079
+ tier: 'free',
2080
+ entitlement_state: 'active'
2081
+ }));
2082
+ const attempt = db.prepare(`
2083
+ SELECT status
2084
+ FROM billing_checkout_attempts
2085
+ WHERE tenant_id = 'default' AND session_id = 'cs_plan_unresolved'
2086
+ LIMIT 1
2087
+ `).get();
2088
+ expect(attempt).toEqual(expect.objectContaining({
2089
+ status: 'open'
2090
+ }));
2091
+ });
2092
+ it('rejects checkout confirmation when the session belongs to a different account', async () => {
2093
+ mockCheckoutRetrieve.mockResolvedValueOnce({
2094
+ id: 'cs_wrong_account',
2095
+ mode: 'subscription',
2096
+ status: 'complete',
2097
+ payment_status: 'paid',
2098
+ customer: 'cus_other',
2099
+ subscription: 'sub_other',
2100
+ metadata: { userId: 'u2' }
2101
+ });
2102
+ const confirmHandler = routeHandlers.get('POST /api/taskforce/billing/checkout-session/confirm');
2103
+ const req = createMockReq(JSON.stringify({
2104
+ sessionId: 'cs_wrong_account'
2105
+ }), {
2106
+ 'x-taskforce-runtime-mode': 'cloud',
2107
+ host: 'app.taskforcehq.ai'
2108
+ });
2109
+ const res = createMockRes();
2110
+ await confirmHandler(req, res, {});
2111
+ expect(res.statusCode).toBe(403);
2112
+ expect(JSON.parse(res.body)).toEqual({
2113
+ error: 'Checkout session does not belong to the current account.',
2114
+ code: 'CHECKOUT_SESSION_ACCOUNT_MISMATCH'
2115
+ });
2116
+ expect(mockSubscriptionRetrieve).not.toHaveBeenCalled();
2117
+ });
1825
2118
  it('rejects checkout confirmation when a newer pending checkout superseded the session plan', async () => {
1826
2119
  const now = new Date().toISOString();
1827
2120
  db.prepare(`
@@ -2119,7 +2412,30 @@ describe('Billing Webhook Routes', () => {
2119
2412
  `).get();
2120
2413
  expect(purged).toBeUndefined();
2121
2414
  });
2122
- it('rejects manual stripe price option listing now that pricing is import-only', async () => {
2415
+ it('lists active Stripe price options for the requested interval', async () => {
2416
+ mockPricesList.mockResolvedValueOnce({
2417
+ data: [
2418
+ {
2419
+ id: 'price_month_1',
2420
+ active: true,
2421
+ currency: 'usd',
2422
+ unit_amount: 1900,
2423
+ nickname: 'Monthly Core',
2424
+ recurring: { interval: 'month' },
2425
+ product: { id: 'prod_core', name: 'Core' }
2426
+ },
2427
+ {
2428
+ id: 'price_year_1',
2429
+ active: true,
2430
+ currency: 'usd',
2431
+ unit_amount: 19000,
2432
+ nickname: 'Yearly Core',
2433
+ recurring: { interval: 'year' },
2434
+ product: { id: 'prod_core', name: 'Core' }
2435
+ }
2436
+ ],
2437
+ has_more: false
2438
+ });
2123
2439
  const route = routeHandlers.get('GET /api/taskforce/admin/billing/stripe-price-options');
2124
2440
  expect(route).toBeTypeOf('function');
2125
2441
  const req = createMockReq('', {
@@ -2128,9 +2444,19 @@ describe('Billing Webhook Routes', () => {
2128
2444
  req.url = '/api/taskforce/admin/billing/stripe-price-options?interval=month';
2129
2445
  const res = createMockRes();
2130
2446
  await route(req, res, {});
2131
- expect(res.statusCode).toBe(410);
2447
+ expect(res.statusCode).toBe(200);
2132
2448
  const body = JSON.parse(res.body);
2133
- expect(body.code).toBe('BILLING_PRICE_MANUAL_MAPPING_REMOVED');
2449
+ expect(body.options).toEqual([
2450
+ expect.objectContaining({
2451
+ stripePriceId: 'price_month_1',
2452
+ interval: 'month',
2453
+ unitAmount: 1900,
2454
+ currency: 'usd',
2455
+ nickname: 'Monthly Core',
2456
+ productId: 'prod_core',
2457
+ productName: 'Core'
2458
+ })
2459
+ ]);
2134
2460
  });
2135
2461
  it('keeps active imported price mappings during Stripe import when the Stripe price is still active but lacks plan metadata', async () => {
2136
2462
  const now = new Date().toISOString();
@@ -2171,12 +2497,23 @@ describe('Billing Webhook Routes', () => {
2171
2497
  `).get();
2172
2498
  expect(row.active).toBe(1);
2173
2499
  });
2174
- it('rejects manual price mapping upserts now that pricing is import-only', async () => {
2500
+ it('upserts manual Stripe price mappings and deactivates the prior active mapping', async () => {
2175
2501
  const now = new Date().toISOString();
2176
2502
  db.prepare(`
2177
2503
  INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
2178
2504
  VALUES ('collab-v1', 'default', 'collab', 1, 1, '{}', ?, ?)
2179
2505
  `).run(now, now);
2506
+ db.prepare(`
2507
+ INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at)
2508
+ VALUES ('default', 'collab-v1', 'month', 'price_old', 'stripe', 1, 1500, 'usd', ?, ?)
2509
+ `).run(now, now);
2510
+ mockPriceRetrieve.mockResolvedValueOnce({
2511
+ id: 'price_new',
2512
+ active: true,
2513
+ currency: 'usd',
2514
+ unit_amount: 2400,
2515
+ recurring: { interval: 'month' }
2516
+ });
2180
2517
  const upsertRoute = routeHandlers.get('PUT /api/taskforce/admin/billing/price-mappings');
2181
2518
  expect(upsertRoute).toBeTypeOf('function');
2182
2519
  const upsertReq = createMockReq(JSON.stringify({
@@ -2187,10 +2524,27 @@ describe('Billing Webhook Routes', () => {
2187
2524
  }), { 'x-taskforce-runtime-mode': 'cloud' });
2188
2525
  const upsertRes = createMockRes();
2189
2526
  await upsertRoute(upsertReq, upsertRes, {});
2190
- expect(upsertRes.statusCode).toBe(410);
2191
- expect(JSON.parse(upsertRes.body)).toEqual(expect.objectContaining({
2192
- code: 'BILLING_PRICE_MANUAL_MAPPING_REMOVED'
2527
+ expect(upsertRes.statusCode).toBe(200);
2528
+ const body = JSON.parse(upsertRes.body);
2529
+ expect(body.mapping).toEqual(expect.objectContaining({
2530
+ plan_version_id: 'collab-v1',
2531
+ billing_interval: 'month',
2532
+ stripe_price_id: 'price_new',
2533
+ pricing_type: 'stripe',
2534
+ active: 1,
2535
+ unit_amount: 2400,
2536
+ currency: 'usd'
2193
2537
  }));
2538
+ const rows = db.prepare(`
2539
+ SELECT stripe_price_id, active, unit_amount
2540
+ FROM billing_price_mappings
2541
+ WHERE tenant_id = 'default' AND plan_version_id = 'collab-v1' AND billing_interval = 'month'
2542
+ ORDER BY stripe_price_id ASC
2543
+ `).all();
2544
+ expect(rows).toEqual([
2545
+ { stripe_price_id: 'price_new', active: 1, unit_amount: 2400 },
2546
+ { stripe_price_id: 'price_old', active: 0, unit_amount: 1500 }
2547
+ ]);
2194
2548
  });
2195
2549
  it('rejects manual reuse of a Stripe price id across multiple plan versions', async () => {
2196
2550
  const now = new Date().toISOString();
@@ -2211,7 +2565,19 @@ describe('Billing Webhook Routes', () => {
2211
2565
  }), { 'x-taskforce-runtime-mode': 'cloud' });
2212
2566
  const firstRes = createMockRes();
2213
2567
  await upsertRoute(firstReq, firstRes, {});
2214
- expect(firstRes.statusCode).toBe(410);
2568
+ expect(firstRes.statusCode).toBe(200);
2569
+ const secondReq = createMockReq(JSON.stringify({
2570
+ planVersionId: 'team-v1',
2571
+ interval: 'month',
2572
+ stripePriceId: 'price_shared',
2573
+ active: true
2574
+ }), { 'x-taskforce-runtime-mode': 'cloud' });
2575
+ const secondRes = createMockRes();
2576
+ await upsertRoute(secondReq, secondRes, {});
2577
+ expect(secondRes.statusCode).toBe(409);
2578
+ expect(JSON.parse(secondRes.body)).toEqual(expect.objectContaining({
2579
+ code: 'BILLING_PRICE_ALREADY_ASSIGNED'
2580
+ }));
2215
2581
  });
2216
2582
  it('keeps reused active mappings during Stripe import when the shared Stripe price is still active', async () => {
2217
2583
  const now = new Date().toISOString();
@@ -2260,12 +2626,16 @@ describe('Billing Webhook Routes', () => {
2260
2626
  { plan_version_id: 'team-v1', stripe_price_id: 'price_shared', active: 1 }
2261
2627
  ]);
2262
2628
  });
2263
- it('rejects manual free price mapping upserts now that pricing is import-only', async () => {
2629
+ it('supports manual free price mappings and deactivation', async () => {
2264
2630
  const now = new Date().toISOString();
2265
2631
  db.prepare(`
2266
2632
  INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
2267
2633
  VALUES ('starter-v1', 'default', 'starter', 1, 1, '{}', ?, ?)
2268
2634
  `).run(now, now);
2635
+ db.prepare(`
2636
+ INSERT INTO billing_price_mappings (tenant_id, plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at)
2637
+ VALUES ('default', 'starter-v1', 'month', 'price_old', 'stripe', 1, 1500, 'usd', ?, ?)
2638
+ `).run(now, now);
2269
2639
  const upsertRoute = routeHandlers.get('PUT /api/taskforce/admin/billing/price-mappings');
2270
2640
  expect(upsertRoute).toBeTypeOf('function');
2271
2641
  const upsertReq = createMockReq(JSON.stringify({
@@ -2276,10 +2646,41 @@ describe('Billing Webhook Routes', () => {
2276
2646
  }), { 'x-taskforce-runtime-mode': 'cloud' });
2277
2647
  const upsertRes = createMockRes();
2278
2648
  await upsertRoute(upsertReq, upsertRes, {});
2279
- expect(upsertRes.statusCode).toBe(410);
2649
+ expect(upsertRes.statusCode).toBe(200);
2280
2650
  expect(JSON.parse(upsertRes.body)).toEqual(expect.objectContaining({
2281
- code: 'BILLING_PRICE_MANUAL_MAPPING_REMOVED'
2651
+ mapping: expect.objectContaining({
2652
+ plan_version_id: 'starter-v1',
2653
+ billing_interval: 'month',
2654
+ stripe_price_id: 'free:starter-v1:month',
2655
+ pricing_type: 'free',
2656
+ active: 1,
2657
+ unit_amount: 0,
2658
+ currency: null
2659
+ })
2282
2660
  }));
2661
+ const deactivateRoute = routeHandlers.get('POST /api/taskforce/admin/billing/price-mappings/deactivate');
2662
+ expect(deactivateRoute).toBeTypeOf('function');
2663
+ const deactivateReq = createMockReq(JSON.stringify({
2664
+ planVersionId: 'starter-v1',
2665
+ interval: 'month'
2666
+ }), { 'x-taskforce-runtime-mode': 'cloud' });
2667
+ const deactivateRes = createMockRes();
2668
+ await deactivateRoute(deactivateReq, deactivateRes, {});
2669
+ expect(deactivateRes.statusCode).toBe(200);
2670
+ expect(JSON.parse(deactivateRes.body)).toEqual(expect.objectContaining({
2671
+ success: true,
2672
+ changedRows: 2
2673
+ }));
2674
+ const rows = db.prepare(`
2675
+ SELECT stripe_price_id, pricing_type, active
2676
+ FROM billing_price_mappings
2677
+ WHERE tenant_id = 'default' AND plan_version_id = 'starter-v1' AND billing_interval = 'month'
2678
+ ORDER BY stripe_price_id ASC
2679
+ `).all();
2680
+ expect(rows).toEqual([
2681
+ { stripe_price_id: 'free:starter-v1:month', pricing_type: 'free', active: 0 },
2682
+ { stripe_price_id: 'price_old', pricing_type: 'stripe', active: 0 }
2683
+ ]);
2283
2684
  });
2284
2685
  it('rejects webhook failure diagnostics for non-admin', async () => {
2285
2686
  const nonAdminHandlers = buildBillingRouteHandlers(db, { isAdmin: false });
@@ -2332,6 +2733,8 @@ describe('Billing Webhook Routes', () => {
2332
2733
  expect(res.statusCode).toBe(200);
2333
2734
  const body = JSON.parse(res.body);
2334
2735
  expect(body.plans).toBeInstanceOf(Array);
2736
+ expect(body.pageTitle).toBe('Subscription Plans');
2737
+ expect(body.pageDescription).toBe('Manage your subscription and explore available features. Your current plan is highlighted below.');
2335
2738
  expect(body.pricingSource).toEqual({
2336
2739
  environment: 'production',
2337
2740
  runtimeMode: 'cloud'
@@ -2357,6 +2760,185 @@ describe('Billing Webhook Routes', () => {
2357
2760
  expect(freePlan.pricing.year).toBeNull();
2358
2761
  expect(proPlan.defaultVersion.trialDays).toBe(0);
2359
2762
  });
2763
+ it('invalidates cached public pricing when billing mappings change', async () => {
2764
+ const now = new Date().toISOString();
2765
+ routeHandlers = buildBillingRouteHandlers(db);
2766
+ const pricingRoute = routeHandlers.get('GET /api/taskforce/public/pricing');
2767
+ expect(pricingRoute).toBeTypeOf('function');
2768
+ db.prepare(`
2769
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
2770
+ VALUES ('pro-v1', 'default', 'pro', 1, 1, '{}', ?, ?)
2771
+ `).run(now, now);
2772
+ db.prepare(`
2773
+ INSERT INTO billing_price_mappings (
2774
+ tenant_id, plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at
2775
+ ) VALUES ('default', 'pro-v1', 'month', 'price_pro_month', 'stripe', 1, 1200, 'usd', ?, ?)
2776
+ `).run(now, now);
2777
+ const firstReq = createMockReq('', { 'x-taskforce-runtime-mode': 'cloud' });
2778
+ const firstRes = createMockRes();
2779
+ await pricingRoute(firstReq, firstRes, {});
2780
+ expect(firstRes.statusCode).toBe(200);
2781
+ const firstBody = JSON.parse(firstRes.body);
2782
+ expect(firstBody.plans.find((p) => p.planId === 'pro')?.pricing?.month?.unitAmount).toBe(1200);
2783
+ const updatedAt = new Date(Date.now() + 1000).toISOString();
2784
+ db.prepare(`
2785
+ UPDATE billing_price_mappings
2786
+ SET unit_amount = 2400, updated_at = ?
2787
+ WHERE tenant_id = 'default' AND plan_version_id = 'pro-v1' AND billing_interval = 'month'
2788
+ `).run(updatedAt);
2789
+ const secondReq = createMockReq('', { 'x-taskforce-runtime-mode': 'cloud' });
2790
+ const secondRes = createMockRes();
2791
+ await pricingRoute(secondReq, secondRes, {});
2792
+ expect(secondRes.statusCode).toBe(200);
2793
+ const secondBody = JSON.parse(secondRes.body);
2794
+ expect(secondBody.plans.find((p) => p.planId === 'pro')?.pricing?.month?.unitAmount).toBe(2400);
2795
+ });
2796
+ it('invalidates cached public pricing when plan features change', async () => {
2797
+ const now = new Date().toISOString();
2798
+ routeHandlers = buildBillingRouteHandlers(db);
2799
+ const pricingRoute = routeHandlers.get('GET /api/taskforce/public/pricing');
2800
+ expect(pricingRoute).toBeTypeOf('function');
2801
+ db.exec(`
2802
+ CREATE TABLE IF NOT EXISTS plan_feature_matrix (
2803
+ tenant_id TEXT NOT NULL,
2804
+ plan_version_id TEXT NOT NULL,
2805
+ feature_key TEXT NOT NULL,
2806
+ access TEXT NOT NULL,
2807
+ config_json TEXT,
2808
+ created_at TEXT NOT NULL,
2809
+ updated_at TEXT NOT NULL,
2810
+ PRIMARY KEY (tenant_id, plan_version_id, feature_key)
2811
+ )
2812
+ `);
2813
+ db.prepare(`
2814
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
2815
+ VALUES ('pro-v1', 'default', 'pro', 1, 1, '{}', ?, ?)
2816
+ `).run(now, now);
2817
+ db.prepare(`
2818
+ INSERT INTO billing_price_mappings (
2819
+ tenant_id, plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at
2820
+ ) VALUES ('default', 'pro-v1', 'month', 'price_pro_month', 'stripe', 1, 1200, 'usd', ?, ?)
2821
+ `).run(now, now);
2822
+ db.prepare(`
2823
+ INSERT INTO plan_feature_matrix (tenant_id, plan_version_id, feature_key, access, config_json, created_at, updated_at)
2824
+ VALUES ('default', 'pro-v1', 'ai.chat', 'enabled', '{}', ?, ?)
2825
+ `).run(now, now);
2826
+ const firstReq = createMockReq('', { 'x-taskforce-runtime-mode': 'cloud' });
2827
+ const firstRes = createMockRes();
2828
+ await pricingRoute(firstReq, firstRes, {});
2829
+ expect(firstRes.statusCode).toBe(200);
2830
+ const firstBody = JSON.parse(firstRes.body);
2831
+ expect(firstBody.plans.find((p) => p.planId === 'pro')?.features).toEqual([
2832
+ expect.objectContaining({ featureKey: 'ai.chat' })
2833
+ ]);
2834
+ const updatedAt = new Date(Date.now() + 1000).toISOString();
2835
+ db.prepare(`
2836
+ UPDATE plan_feature_matrix
2837
+ SET access = 'disabled', updated_at = ?
2838
+ WHERE tenant_id = 'default' AND plan_version_id = 'pro-v1' AND feature_key = 'ai.chat'
2839
+ `).run(updatedAt);
2840
+ const secondReq = createMockReq('', { 'x-taskforce-runtime-mode': 'cloud' });
2841
+ const secondRes = createMockRes();
2842
+ await pricingRoute(secondReq, secondRes, {});
2843
+ expect(secondRes.statusCode).toBe(200);
2844
+ const secondBody = JSON.parse(secondRes.body);
2845
+ expect(secondBody.plans.find((p) => p.planId === 'pro')?.features).toEqual([]);
2846
+ });
2847
+ it('includes public feature copy and invalidates cached public pricing when catalog overrides change', async () => {
2848
+ const now = new Date().toISOString();
2849
+ routeHandlers = buildBillingRouteHandlers(db);
2850
+ const pricingRoute = routeHandlers.get('GET /api/taskforce/public/pricing');
2851
+ expect(pricingRoute).toBeTypeOf('function');
2852
+ db.prepare(`
2853
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, metadata_json, created_at, updated_at)
2854
+ VALUES ('pro-v1', 'default', 'pro', 1, 1, '{}', ?, ?)
2855
+ `).run(now, now);
2856
+ db.prepare(`
2857
+ INSERT INTO billing_price_mappings (
2858
+ tenant_id, plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at
2859
+ ) VALUES ('default', 'pro-v1', 'month', 'price_pro_month', 'stripe', 1, 1200, 'usd', ?, ?)
2860
+ `).run(now, now);
2861
+ db.prepare(`
2862
+ INSERT INTO plan_feature_matrix (tenant_id, plan_version_id, feature_key, access, config_json, created_at, updated_at)
2863
+ VALUES ('default', 'pro-v1', 'collaboration.ai_profiles', 'enabled', '{}', ?, ?)
2864
+ `).run(now, now);
2865
+ db.prepare(`
2866
+ INSERT INTO plan_feature_catalog_overrides (tenant_id, feature_key, public_label, public_description, created_at, updated_at)
2867
+ VALUES ('default', 'collaboration.ai_profiles', 'AI teammates', 'Add reusable AI teammates to your workspace.', ?, ?)
2868
+ `).run(now, now);
2869
+ const firstReq = createMockReq('', { 'x-taskforce-runtime-mode': 'cloud' });
2870
+ const firstRes = createMockRes();
2871
+ await pricingRoute(firstReq, firstRes, {});
2872
+ expect(firstRes.statusCode).toBe(200);
2873
+ const firstBody = JSON.parse(firstRes.body);
2874
+ expect(firstBody.plans.find((p) => p.planId === 'pro')?.features).toEqual([
2875
+ expect.objectContaining({
2876
+ featureKey: 'collaboration.ai_profiles',
2877
+ label: 'AI Profiles',
2878
+ publicLabel: 'AI teammates',
2879
+ publicDescription: 'Add reusable AI teammates to your workspace.'
2880
+ })
2881
+ ]);
2882
+ const updatedAt = new Date(Date.now() + 1000).toISOString();
2883
+ db.prepare(`
2884
+ UPDATE plan_feature_catalog_overrides
2885
+ SET public_label = 'AI copilots', updated_at = ?
2886
+ WHERE tenant_id = 'default' AND feature_key = 'collaboration.ai_profiles'
2887
+ `).run(updatedAt);
2888
+ const secondReq = createMockReq('', { 'x-taskforce-runtime-mode': 'cloud' });
2889
+ const secondRes = createMockRes();
2890
+ await pricingRoute(secondReq, secondRes, {});
2891
+ expect(secondRes.statusCode).toBe(200);
2892
+ const secondBody = JSON.parse(secondRes.body);
2893
+ expect(secondBody.plans.find((p) => p.planId === 'pro')?.features).toEqual([
2894
+ expect.objectContaining({
2895
+ featureKey: 'collaboration.ai_profiles',
2896
+ publicLabel: 'AI copilots'
2897
+ })
2898
+ ]);
2899
+ });
2900
+ it('orders public pricing features by catalog display order', async () => {
2901
+ const now = new Date().toISOString();
2902
+ routeHandlers = buildBillingRouteHandlers(db);
2903
+ const pricingRoute = routeHandlers.get('GET /api/taskforce/public/pricing');
2904
+ expect(pricingRoute).toBeTypeOf('function');
2905
+ db.prepare(`
2906
+ INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, seat_limit, metadata_json, created_at, updated_at)
2907
+ VALUES ('pro-v1', 'default', 'pro', 1, 1, 5, '{}', ?, ?)
2908
+ `).run(now, now);
2909
+ db.prepare(`
2910
+ INSERT INTO billing_price_mappings (
2911
+ tenant_id, plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at
2912
+ ) VALUES ('default', 'pro-v1', 'month', 'price_pro_month', 'stripe', 1, 1800, 'usd', ?, ?)
2913
+ `).run(now, now);
2914
+ for (const feature of [
2915
+ { key: 'collaboration.team_management', access: 'enabled', config: '{}' },
2916
+ { key: 'collaboration.invites', access: 'enabled', config: '{}' },
2917
+ { key: 'collaboration.seat_limit', access: 'limited', config: '{"seat_limit":5}' },
2918
+ { key: 'context.uploads', access: 'enabled', config: '{"storageLimitMb":24}' },
2919
+ { key: 'workspace.workspaces', access: 'enabled', config: '{"maxWorkspaces":1}' },
2920
+ { key: 'collaboration.ai_profiles', access: 'limited', config: '{"maxAiProfiles":4}' }
2921
+ ]) {
2922
+ db.prepare(`
2923
+ INSERT INTO plan_feature_matrix (tenant_id, plan_version_id, feature_key, access, config_json, created_at, updated_at)
2924
+ VALUES ('default', 'pro-v1', ?, ?, ?, ?, ?)
2925
+ `).run(feature.key, feature.access, feature.config, now, now);
2926
+ }
2927
+ const req = createMockReq('', { 'x-taskforce-runtime-mode': 'cloud' });
2928
+ const res = createMockRes();
2929
+ await pricingRoute(req, res, {});
2930
+ expect(res.statusCode).toBe(200);
2931
+ const body = JSON.parse(res.body);
2932
+ const orderedPlan = body.plans.find((plan) => plan.planId === 'pro');
2933
+ expect(orderedPlan.features.map((feature) => feature.featureKey)).toEqual([
2934
+ 'workspace.workspaces',
2935
+ 'collaboration.ai_profiles',
2936
+ 'context.uploads',
2937
+ 'collaboration.team_management'
2938
+ ]);
2939
+ expect(orderedPlan.features.map((feature) => feature.featureKey)).not.toContain('collaboration.invites');
2940
+ expect(orderedPlan.features.map((feature) => feature.featureKey)).not.toContain('collaboration.seat_limit');
2941
+ });
2360
2942
  it('returns the pricing environment derived from the cloud host', async () => {
2361
2943
  routeHandlers = buildBillingRouteHandlers(db);
2362
2944
  const pricingRoute = routeHandlers.get('GET /api/taskforce/public/pricing');
@@ -2369,6 +2951,7 @@ describe('Billing Webhook Routes', () => {
2369
2951
  await pricingRoute(req, res, {});
2370
2952
  expect(res.statusCode).toBe(200);
2371
2953
  const body = JSON.parse(res.body);
2954
+ expect(body.pageTitle).toBe('Subscription Plans');
2372
2955
  expect(body.pricingSource).toEqual({
2373
2956
  environment: 'performance',
2374
2957
  runtimeMode: 'cloud'