@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
@@ -1,8 +1,9 @@
1
1
  import { TaskforceRuleError } from './types.js';
2
- import { DOCUMENT_CLOUD_STORAGE_FEATURE_KEY, WORKSPACES_FEATURE_KEY, DOCUMENT_CLOUD_STORAGE_LIMIT_CONFIG_KEY, WORKSPACES_LIMIT_CONFIG_KEY, DEFAULT_PLAN_FEATURE_CATALOG, PLAN_FEATURE_DEPENDENCIES, generateUuidV7, } from './shared.js';
2
+ import { DOCUMENT_CLOUD_STORAGE_FEATURE_KEY, WORKSPACES_FEATURE_KEY, AI_PROFILES_FEATURE_KEY, DOCUMENT_CLOUD_STORAGE_LIMIT_CONFIG_KEY, WORKSPACES_LIMIT_CONFIG_KEY, AI_PROFILES_LIMIT_CONFIG_KEY, DEFAULT_PLAN_FEATURE_CATALOG, PLAN_FEATURE_DEPENDENCIES, generateUuidV7, } from './shared.js';
3
3
  const LEGACY_REMOVED_PLAN_FEATURE_KEYS = Object.freeze([
4
4
  'documents.workspace_access'
5
5
  ]);
6
+ const LEGACY_WORKSPACES_FEATURE_KEY = 'workspace.switching';
6
7
  /**
7
8
  * Handles all plan catalog, plan version, and account entitlement logic.
8
9
  * Extracted from Taskforce.ts to reduce monolith size.
@@ -32,6 +33,45 @@ export class PlanEntitlementService {
32
33
  return {};
33
34
  }
34
35
  }
36
+ normalizeOptionalCatalogMarketingCopy(raw) {
37
+ const value = String(raw ?? '').trim();
38
+ return value || null;
39
+ }
40
+ canonicalizePlanFeatureKey(raw) {
41
+ const featureKey = String(raw || '').trim();
42
+ return featureKey === LEGACY_WORKSPACES_FEATURE_KEY ? WORKSPACES_FEATURE_KEY : featureKey;
43
+ }
44
+ normalizeOptionalCatalogDisplayOrder(raw) {
45
+ if (raw === null || raw === undefined || raw === '')
46
+ return null;
47
+ const numeric = Number(raw);
48
+ if (!Number.isFinite(numeric))
49
+ return null;
50
+ return Math.max(0, Math.floor(numeric));
51
+ }
52
+ listPlanFeatureCatalogOverrideMap() {
53
+ const rows = this.db.prepare(`
54
+ SELECT feature_key, public_label, public_description, public_display_order
55
+ FROM plan_feature_catalog_overrides
56
+ WHERE tenant_id = ?
57
+ `).all(this.tenantId);
58
+ const overrides = new Map();
59
+ for (const row of rows) {
60
+ const originalFeatureKey = String(row.feature_key || '').trim();
61
+ const featureKey = this.canonicalizePlanFeatureKey(originalFeatureKey);
62
+ if (!featureKey)
63
+ continue;
64
+ if (featureKey === WORKSPACES_FEATURE_KEY && originalFeatureKey === LEGACY_WORKSPACES_FEATURE_KEY && overrides.has(featureKey)) {
65
+ continue;
66
+ }
67
+ overrides.set(featureKey, {
68
+ publicLabel: this.normalizeOptionalCatalogMarketingCopy(row.public_label),
69
+ publicDescription: this.normalizeOptionalCatalogMarketingCopy(row.public_description),
70
+ publicDisplayOrder: this.normalizeOptionalCatalogDisplayOrder(row.public_display_order)
71
+ });
72
+ }
73
+ return overrides;
74
+ }
35
75
  // ─── Normalizers ─────────────────────────────────────────────────────────
36
76
  normalizeEntitlementState(value) {
37
77
  const normalized = String(value || '').trim().toLowerCase();
@@ -91,9 +131,10 @@ export class PlanEntitlementService {
91
131
  return nextMetadata;
92
132
  }
93
133
  sanitizePlanFeatureConfig(featureKey, access, config) {
94
- if (featureKey === 'collaboration.seat_limit')
134
+ const normalizedFeatureKey = this.canonicalizePlanFeatureKey(featureKey);
135
+ if (normalizedFeatureKey === 'collaboration.seat_limit')
95
136
  return {};
96
- if (featureKey === DOCUMENT_CLOUD_STORAGE_FEATURE_KEY) {
137
+ if (normalizedFeatureKey === DOCUMENT_CLOUD_STORAGE_FEATURE_KEY) {
97
138
  const limit = this.readPositiveIntegerConfig(config[DOCUMENT_CLOUD_STORAGE_LIMIT_CONFIG_KEY], 1);
98
139
  if (access === 'limited') {
99
140
  return {
@@ -107,12 +148,21 @@ export class PlanEntitlementService {
107
148
  }
108
149
  return {};
109
150
  }
110
- if (featureKey === WORKSPACES_FEATURE_KEY) {
151
+ if (normalizedFeatureKey === WORKSPACES_FEATURE_KEY) {
111
152
  const limit = this.readPositiveIntegerConfig(config[WORKSPACES_LIMIT_CONFIG_KEY], 1);
112
153
  return {
113
154
  [WORKSPACES_LIMIT_CONFIG_KEY]: limit ?? 1
114
155
  };
115
156
  }
157
+ if (normalizedFeatureKey === AI_PROFILES_FEATURE_KEY) {
158
+ if (access === 'limited') {
159
+ const limit = this.readPositiveIntegerConfig(config[AI_PROFILES_LIMIT_CONFIG_KEY], 1);
160
+ return {
161
+ [AI_PROFILES_LIMIT_CONFIG_KEY]: limit ?? 1
162
+ };
163
+ }
164
+ return {};
165
+ }
116
166
  return config && typeof config === 'object' ? { ...config } : {};
117
167
  }
118
168
  isEntitlementStateAllowed(state) {
@@ -135,21 +185,25 @@ export class PlanEntitlementService {
135
185
  LIMIT 1
136
186
  `).get(this.tenantId, accountId);
137
187
  }
138
- normalizeExpiredCheckoutPendingRow(accountId) {
188
+ normalizeExpiredLifecycleRow(accountId) {
139
189
  const row = this.readAccountEntitlementRow(accountId);
140
190
  if (!row?.account_id || !row.plan_version_id) {
141
191
  return row;
142
192
  }
143
193
  const currentState = this.normalizeEntitlementState(row.entitlement_state);
144
- if (currentState !== 'checkout_pending' || !this.isIsoTimestampExpired(row.effective_until || null)) {
194
+ if (!this.isIsoTimestampExpired(row.effective_until || null)) {
195
+ return row;
196
+ }
197
+ if (currentState !== 'checkout_pending' && currentState !== 'trialing') {
145
198
  return row;
146
199
  }
200
+ const updatedBy = currentState === 'trialing' ? 'trial-expired' : 'checkout-expired';
147
201
  const now = new Date().toISOString();
148
202
  this.db.prepare(`
149
203
  UPDATE account_entitlements
150
204
  SET entitlement_state = ?, effective_until = NULL, updated_by = ?, updated_at = ?
151
205
  WHERE tenant_id = ? AND account_id = ?
152
- `).run('pending_plan_selection', 'checkout-expired', now, this.tenantId, accountId);
206
+ `).run('pending_plan_selection', updatedBy, now, this.tenantId, accountId);
153
207
  this.createUserBillingRecord(accountId, {
154
208
  entitlementState: 'pending_plan_selection',
155
209
  planId: null,
@@ -160,6 +214,20 @@ export class PlanEntitlementService {
160
214
  isBillingConfirmedTransitionSource(source) {
161
215
  return source === 'checkout-confirm' || source === 'stripe-webhook';
162
216
  }
217
+ countWorkspaceStorageUsageBytes(workspaceIdRaw) {
218
+ const workspaceId = String(workspaceIdRaw || '').trim();
219
+ if (!workspaceId)
220
+ return 0;
221
+ const row = this.db.prepare(`
222
+ SELECT COALESCE(SUM(size_bytes), 0) AS total_bytes
223
+ FROM workspace_assets
224
+ WHERE tenant_id = ?
225
+ AND workspace_id = ?
226
+ AND deleted_at IS NULL
227
+ `).get(this.tenantId, workspaceId);
228
+ const total = Number(row?.total_bytes || 0);
229
+ return Number.isFinite(total) ? Math.max(0, Math.floor(total)) : 0;
230
+ }
163
231
  resolveCanonicalTransitionSource(updatedBy) {
164
232
  if (updatedBy === 'registration'
165
233
  || updatedBy === 'checkout-start'
@@ -232,8 +300,48 @@ export class PlanEntitlementService {
232
300
  normalizeLegacyPlanDataIfNeeded() {
233
301
  this.migrateLegacySeatLimitFeatureConfigToVersionSeatLimit();
234
302
  this.normalizeLegacyBillingMirrorRows();
303
+ this.renameLegacyWorkspacesFeatureKey();
235
304
  this.removeLegacyPlanFeatureRows();
236
305
  }
306
+ renameLegacyWorkspacesFeatureKey() {
307
+ const now = new Date().toISOString();
308
+ const tx = this.db.transaction(() => {
309
+ this.db.prepare(`
310
+ DELETE FROM plan_feature_matrix
311
+ WHERE tenant_id = ?
312
+ AND feature_key = ?
313
+ AND EXISTS (
314
+ SELECT 1
315
+ FROM plan_feature_matrix canonical
316
+ WHERE canonical.tenant_id = plan_feature_matrix.tenant_id
317
+ AND canonical.plan_version_id = plan_feature_matrix.plan_version_id
318
+ AND canonical.feature_key = ?
319
+ )
320
+ `).run(this.tenantId, LEGACY_WORKSPACES_FEATURE_KEY, WORKSPACES_FEATURE_KEY);
321
+ this.db.prepare(`
322
+ UPDATE plan_feature_matrix
323
+ SET feature_key = ?, updated_at = ?
324
+ WHERE tenant_id = ? AND feature_key = ?
325
+ `).run(WORKSPACES_FEATURE_KEY, now, this.tenantId, LEGACY_WORKSPACES_FEATURE_KEY);
326
+ this.db.prepare(`
327
+ DELETE FROM plan_feature_catalog_overrides
328
+ WHERE tenant_id = ?
329
+ AND feature_key = ?
330
+ AND EXISTS (
331
+ SELECT 1
332
+ FROM plan_feature_catalog_overrides canonical
333
+ WHERE canonical.tenant_id = plan_feature_catalog_overrides.tenant_id
334
+ AND canonical.feature_key = ?
335
+ )
336
+ `).run(this.tenantId, LEGACY_WORKSPACES_FEATURE_KEY, WORKSPACES_FEATURE_KEY);
337
+ this.db.prepare(`
338
+ UPDATE plan_feature_catalog_overrides
339
+ SET feature_key = ?, updated_at = ?
340
+ WHERE tenant_id = ? AND feature_key = ?
341
+ `).run(WORKSPACES_FEATURE_KEY, now, this.tenantId, LEGACY_WORKSPACES_FEATURE_KEY);
342
+ });
343
+ tx();
344
+ }
237
345
  removeLegacyPlanFeatureRows() {
238
346
  const featureKeys = [...LEGACY_REMOVED_PLAN_FEATURE_KEYS];
239
347
  if (featureKeys.length < 1)
@@ -524,7 +632,9 @@ export class PlanEntitlementService {
524
632
  return { limit: 1, feature: null };
525
633
  }
526
634
  const entitlement = this.getAccountEntitlement(normalizedUserId);
527
- const feature = entitlement.version.features[WORKSPACES_FEATURE_KEY] || null;
635
+ const feature = entitlement.version.features[WORKSPACES_FEATURE_KEY]
636
+ || entitlement.version.features[LEGACY_WORKSPACES_FEATURE_KEY]
637
+ || null;
528
638
  if (!feature || feature.access === 'disabled') {
529
639
  return { limit: 1, feature };
530
640
  }
@@ -534,6 +644,38 @@ export class PlanEntitlementService {
534
644
  }
535
645
  return { limit: null, feature };
536
646
  }
647
+ resolveAiProfileLimitForWorkspace(workspaceId) {
648
+ const normalizedWorkspaceId = String(workspaceId || '').trim();
649
+ if (!normalizedWorkspaceId) {
650
+ return { limit: null, feature: null };
651
+ }
652
+ const accountId = this.deps.getWorkspaceOwnerUserId(normalizedWorkspaceId);
653
+ if (!accountId) {
654
+ return { limit: null, feature: null };
655
+ }
656
+ let entitlement;
657
+ try {
658
+ entitlement = this.getAccountEntitlement(accountId);
659
+ }
660
+ catch (error) {
661
+ if (error instanceof TaskforceRuleError && error.code === 'ENTITLEMENT_NOT_FOUND') {
662
+ return { limit: null, feature: null };
663
+ }
664
+ throw error;
665
+ }
666
+ const feature = entitlement.version.features[AI_PROFILES_FEATURE_KEY] || null;
667
+ if (!feature || feature.access === 'disabled') {
668
+ return { limit: null, feature };
669
+ }
670
+ const configuredLimit = this.readPositiveIntegerConfig(feature.config?.[AI_PROFILES_LIMIT_CONFIG_KEY], 1);
671
+ if (configuredLimit !== null) {
672
+ return { limit: configuredLimit, feature };
673
+ }
674
+ if (feature.access === 'limited') {
675
+ return { limit: 1, feature };
676
+ }
677
+ return { limit: null, feature };
678
+ }
537
679
  planHasAnyActivePricing(planIdRaw) {
538
680
  const planId = String(planIdRaw || '').trim().toLowerCase();
539
681
  if (!planId)
@@ -629,32 +771,15 @@ export class PlanEntitlementService {
629
771
  throw error;
630
772
  }
631
773
  }
632
- tryGetImplicitFreeOnboardingPlanVersion() {
633
- const rows = this.db.prepare(`
634
- SELECT DISTINCT v.plan_version_id
635
- FROM plan_versions v
636
- JOIN plan_catalog p
637
- ON p.tenant_id = v.tenant_id
638
- AND p.plan_id = v.plan_id
639
- JOIN billing_price_mappings bpm
640
- ON bpm.tenant_id = v.tenant_id
641
- AND bpm.plan_version_id = v.plan_version_id
642
- WHERE v.tenant_id = ?
643
- AND COALESCE(p.status, 'active') = 'active'
644
- AND bpm.active = 1
645
- AND bpm.pricing_type = 'free'
646
- ORDER BY v.is_default DESC, p.updated_at DESC, p.created_at DESC, v.version_number DESC, v.created_at DESC
647
- `).all(this.tenantId);
648
- for (const row of rows) {
649
- const planVersionId = String(row.plan_version_id || '').trim();
650
- if (!planVersionId)
651
- continue;
652
- const version = this.getPlanVersionSnapshot(planVersionId);
653
- if (!this.isTeamPlanVersion(version)) {
654
- return version;
655
- }
656
- }
657
- return null;
774
+ getOnboardingPlanSnapshot(planIdRaw) {
775
+ const planId = String(planIdRaw || '').trim().toLowerCase();
776
+ if (!planId)
777
+ return null;
778
+ const plan = this.listPlanCatalog().find((entry) => entry.planId === planId) || null;
779
+ if (!plan)
780
+ return null;
781
+ const defaultVersion = this.listPlanVersions(planId).find((version) => version.isDefault) || null;
782
+ return { plan, defaultVersion };
658
783
  }
659
784
  getOnboardingPolicy() {
660
785
  const billing = this.deps.readGlobalConfig().billing || {};
@@ -664,23 +789,32 @@ export class PlanEntitlementService {
664
789
  if (policy?.mode === 'require_plan_selection'
665
790
  || policy?.mode === 'auto_grant_free'
666
791
  || policy?.mode === 'auto_start_trial') {
792
+ const rawPolicy = policy;
793
+ const explicitPlanId = typeof rawPolicy.planId === 'string'
794
+ ? (rawPolicy.planId.trim() || null)
795
+ : null;
796
+ const legacyPlanVersionId = typeof rawPolicy.planVersionId === 'string'
797
+ ? (rawPolicy.planVersionId.trim() || null)
798
+ : null;
799
+ const resolvedLegacyPlanId = !explicitPlanId && legacyPlanVersionId
800
+ ? (() => {
801
+ const row = this.db.prepare(`
802
+ SELECT plan_id
803
+ FROM plan_versions
804
+ WHERE tenant_id = ? AND plan_version_id = ?
805
+ LIMIT 1
806
+ `).get(this.tenantId, legacyPlanVersionId);
807
+ return typeof row?.plan_id === 'string' ? (row.plan_id.trim() || null) : null;
808
+ })()
809
+ : null;
667
810
  return {
668
811
  mode: policy.mode,
669
- planVersionId: typeof policy.planVersionId === 'string'
670
- ? (policy.planVersionId.trim() || null)
671
- : null
672
- };
673
- }
674
- const implicitFreeVersion = this.tryGetImplicitFreeOnboardingPlanVersion();
675
- if (implicitFreeVersion) {
676
- return {
677
- mode: 'auto_grant_free',
678
- planVersionId: implicitFreeVersion.planVersionId
812
+ planId: explicitPlanId || resolvedLegacyPlanId
679
813
  };
680
814
  }
681
815
  return {
682
816
  mode: 'require_plan_selection',
683
- planVersionId: null
817
+ planId: null
684
818
  };
685
819
  }
686
820
  validateOnboardingPolicy(input) {
@@ -690,64 +824,58 @@ export class PlanEntitlementService {
690
824
  : input?.mode === 'require_plan_selection'
691
825
  ? 'require_plan_selection'
692
826
  : current.mode;
693
- const planVersionId = input?.planVersionId !== undefined
694
- ? (typeof input.planVersionId === 'string' ? input.planVersionId.trim() || null : null)
695
- : current.planVersionId;
827
+ const planId = input?.planId !== undefined
828
+ ? (typeof input.planId === 'string' ? input.planId.trim().toLowerCase() || null : null)
829
+ : current.planId;
696
830
  if (mode === 'require_plan_selection') {
697
831
  return { valid: true };
698
832
  }
699
- if (!planVersionId) {
833
+ if (!planId) {
700
834
  return {
701
835
  valid: false,
702
- code: 'ONBOARDING_PLAN_VERSION_REQUIRED',
703
- message: 'An onboarding plan version is required for the selected onboarding mode.'
836
+ code: 'ONBOARDING_PLAN_REQUIRED',
837
+ message: 'An onboarding plan is required for the selected onboarding mode.'
704
838
  };
705
839
  }
706
- const row = this.db.prepare(`
707
- SELECT v.plan_version_id, v.plan_id, COALESCE(p.status, 'active') AS plan_status
708
- FROM plan_versions v
709
- JOIN plan_catalog p ON p.tenant_id = v.tenant_id AND p.plan_id = v.plan_id
710
- WHERE v.tenant_id = ? AND v.plan_version_id = ?
711
- LIMIT 1
712
- `).get(this.tenantId, planVersionId);
713
- if (!row?.plan_version_id || !row.plan_id) {
840
+ const onboardingPlan = this.getOnboardingPlanSnapshot(planId);
841
+ if (!onboardingPlan?.plan || !onboardingPlan.defaultVersion) {
714
842
  return {
715
843
  valid: false,
716
- code: 'ONBOARDING_PLAN_VERSION_NOT_FOUND',
717
- message: `Configured onboarding plan version "${planVersionId}" was not found.`,
718
- details: { planVersionId }
844
+ code: 'ONBOARDING_PLAN_NOT_FOUND',
845
+ message: `Configured onboarding plan "${planId}" was not found or has no default version.`,
846
+ details: { planId }
719
847
  };
720
848
  }
721
- if (String(row.plan_status || '').trim().toLowerCase() !== 'active') {
849
+ if (onboardingPlan.plan.status !== 'active') {
722
850
  return {
723
851
  valid: false,
724
852
  code: 'ONBOARDING_PLAN_NOT_ENABLED',
725
- message: `Configured onboarding plan version "${planVersionId}" belongs to a disabled plan.`,
853
+ message: `Configured onboarding plan "${planId}" is disabled.`,
726
854
  details: {
727
- planVersionId,
728
- planId: String(row.plan_id)
855
+ planId: onboardingPlan.plan.planId,
856
+ planVersionId: onboardingPlan.defaultVersion.planVersionId
729
857
  }
730
858
  };
731
859
  }
732
- if (mode === 'auto_grant_free' && !this.planVersionHasActiveFreePricing(planVersionId)) {
860
+ if (mode === 'auto_grant_free' && !this.planVersionHasActiveFreePricing(onboardingPlan.defaultVersion.planVersionId)) {
733
861
  return {
734
862
  valid: false,
735
863
  code: 'ONBOARDING_PLAN_MUST_BE_FREE',
736
- message: `Configured onboarding plan version "${planVersionId}" does not have active free pricing.`,
864
+ message: `Configured onboarding plan "${planId}" does not have active free pricing on its default version.`,
737
865
  details: {
738
- planVersionId,
739
- planId: String(row.plan_id)
866
+ planId: onboardingPlan.plan.planId,
867
+ planVersionId: onboardingPlan.defaultVersion.planVersionId
740
868
  }
741
869
  };
742
870
  }
743
- if (mode === 'auto_start_trial' && !this.planVersionIsTrialEligible(planVersionId)) {
871
+ if (mode === 'auto_start_trial' && !this.planVersionIsTrialEligible(onboardingPlan.defaultVersion.planVersionId)) {
744
872
  return {
745
873
  valid: false,
746
874
  code: 'ONBOARDING_PLAN_TRIAL_REQUIRED',
747
- message: `Configured onboarding plan version "${planVersionId}" is not trial eligible.`,
875
+ message: `Configured onboarding plan "${planId}" is not trial eligible on its default version.`,
748
876
  details: {
749
- planVersionId,
750
- planId: String(row.plan_id)
877
+ planId: onboardingPlan.plan.planId,
878
+ planVersionId: onboardingPlan.defaultVersion.planVersionId
751
879
  }
752
880
  };
753
881
  }
@@ -755,7 +883,12 @@ export class PlanEntitlementService {
755
883
  }
756
884
  resolveRegistrationOnboardingOutcome(selectedPlanVersionIdRaw) {
757
885
  const selectedPlanVersionId = String(selectedPlanVersionIdRaw || '').trim();
758
- if (selectedPlanVersionId) {
886
+ const onboardingPolicy = this.getOnboardingPolicy();
887
+ const validation = this.validateOnboardingPolicy(onboardingPolicy);
888
+ if (!validation.valid) {
889
+ throw new TaskforceRuleError(validation.message || 'Onboarding policy is misconfigured.', 500, validation.code || 'ONBOARDING_POLICY_INVALID', validation.details);
890
+ }
891
+ if (onboardingPolicy.mode === 'require_plan_selection' && selectedPlanVersionId) {
759
892
  const planVersion = this.getPlanVersionSnapshot(selectedPlanVersionId);
760
893
  const commercialState = this.planVersionHasActiveFreePricing(planVersion.planVersionId)
761
894
  ? 'active'
@@ -768,11 +901,6 @@ export class PlanEntitlementService {
768
901
  planId: planVersion.planId
769
902
  };
770
903
  }
771
- const onboardingPolicy = this.getOnboardingPolicy();
772
- const validation = this.validateOnboardingPolicy(onboardingPolicy);
773
- if (!validation.valid) {
774
- throw new TaskforceRuleError(validation.message || 'Onboarding policy is misconfigured.', 500, validation.code || 'ONBOARDING_POLICY_INVALID', validation.details);
775
- }
776
904
  if (onboardingPolicy.mode === 'require_plan_selection') {
777
905
  return {
778
906
  requiresPlanSelection: true,
@@ -782,11 +910,11 @@ export class PlanEntitlementService {
782
910
  planId: null
783
911
  };
784
912
  }
785
- const configuredVersion = onboardingPolicy.planVersionId
786
- ? this.getPlanVersionSnapshot(onboardingPolicy.planVersionId)
913
+ const configuredVersion = onboardingPolicy.planId
914
+ ? this.getOnboardingPlanSnapshot(onboardingPolicy.planId)?.defaultVersion || null
787
915
  : null;
788
916
  if (!configuredVersion) {
789
- throw new TaskforceRuleError('Configured onboarding plan version is required.', 500, 'ONBOARDING_PLAN_VERSION_REQUIRED');
917
+ throw new TaskforceRuleError('Configured onboarding plan is required.', 500, 'ONBOARDING_PLAN_REQUIRED');
790
918
  }
791
919
  return {
792
920
  requiresPlanSelection: false,
@@ -796,6 +924,26 @@ export class PlanEntitlementService {
796
924
  planId: configuredVersion.planId
797
925
  };
798
926
  }
927
+ listOnboardingPlanOptions() {
928
+ return this.listPlanCatalog()
929
+ .filter((plan) => plan.status === 'active')
930
+ .map((plan) => {
931
+ const defaultVersion = this.listPlanVersions(plan.planId).find((version) => version.isDefault) || null;
932
+ if (!defaultVersion)
933
+ return null;
934
+ return {
935
+ planId: plan.planId,
936
+ planDisplayName: plan.displayName || plan.planId,
937
+ defaultPlanVersionId: defaultVersion.planVersionId,
938
+ defaultVersionNumber: defaultVersion.versionNumber,
939
+ planStatus: plan.status,
940
+ supportsAutoGrantFree: this.planVersionHasActiveFreePricing(defaultVersion.planVersionId),
941
+ supportsAutoStartTrial: this.planVersionIsTrialEligible(defaultVersion.planVersionId)
942
+ };
943
+ })
944
+ .filter((option) => Boolean(option))
945
+ .sort((a, b) => a.planDisplayName.localeCompare(b.planDisplayName));
946
+ }
799
947
  // ─── Public billing methods ───────────────────────────────────────────────
800
948
  createUserBillingRecord(userId, options) {
801
949
  const now = new Date().toISOString();
@@ -844,6 +992,108 @@ export class PlanEntitlementService {
844
992
  throw error;
845
993
  }
846
994
  }
995
+ tryAutoHealLegacyPersonalFreeEntitlement(accountId, entitlement) {
996
+ if (entitlement.state !== 'canceled')
997
+ return null;
998
+ if (this.isTeamPlanVersion(entitlement.version))
999
+ return null;
1000
+ const defaultPersonalVersion = this.tryGetDefaultPlanVersionByMode('personal');
1001
+ if (!defaultPersonalVersion)
1002
+ return null;
1003
+ if (!this.planVersionHasActiveFreePricing(defaultPersonalVersion.planVersionId))
1004
+ return null;
1005
+ if (String(entitlement.planId || '').trim().toLowerCase() !== defaultPersonalVersion.planId)
1006
+ return null;
1007
+ if (entitlement.planVersionId === defaultPersonalVersion.planVersionId)
1008
+ return null;
1009
+ if (entitlement.version.versionNumber >= defaultPersonalVersion.versionNumber)
1010
+ return null;
1011
+ return this.transitionCanonicalCommercialState({
1012
+ accountId,
1013
+ planVersionId: defaultPersonalVersion.planVersionId,
1014
+ state: 'active',
1015
+ effectiveAt: entitlement.effectiveAt,
1016
+ effectiveUntil: null,
1017
+ billingMirrorPlanId: defaultPersonalVersion.planId,
1018
+ source: 'system',
1019
+ updatedBy: 'legacy-free-auto-heal',
1020
+ createdAt: entitlement.effectiveAt
1021
+ });
1022
+ }
1023
+ tryAutoHealCommercialEntitlementFromBillingMirror(accountId, entitlement) {
1024
+ const currentState = entitlement ? this.normalizeEntitlementState(entitlement.state) : null;
1025
+ if (currentState && this.isEntitlementStateAllowed(currentState))
1026
+ return null;
1027
+ if (currentState && currentState !== 'pending_plan_selection')
1028
+ return null;
1029
+ const billingRow = this.db.prepare(`
1030
+ SELECT stripe_customer_id, stripe_subscription_id, stripe_price_id, stripe_status, entitlement_state, tier, effective_until
1031
+ FROM user_billing
1032
+ WHERE tenant_id = ? AND user_id = ?
1033
+ LIMIT 1
1034
+ `).get(this.tenantId, accountId);
1035
+ const hasStripeLink = Boolean(String(billingRow?.stripe_customer_id || '').trim()
1036
+ || String(billingRow?.stripe_subscription_id || '').trim());
1037
+ if (!hasStripeLink)
1038
+ return null;
1039
+ if (this.isIsoTimestampExpired(billingRow?.effective_until || null))
1040
+ return null;
1041
+ const billingStatus = String(billingRow?.stripe_status || '').trim().toLowerCase();
1042
+ const billingStateFromMirror = this.normalizeEntitlementState(billingRow?.entitlement_state);
1043
+ const derivedBillingState = this.isEntitlementStateAllowed(billingStateFromMirror)
1044
+ ? billingStateFromMirror
1045
+ : (billingStatus === 'trialing' ? 'trialing'
1046
+ : billingStatus === 'active' ? 'active'
1047
+ : billingStatus === 'past_due' ? 'grace'
1048
+ : null);
1049
+ if (!derivedBillingState || !this.isEntitlementStateAllowed(derivedBillingState))
1050
+ return null;
1051
+ const stripePriceId = String(billingRow?.stripe_price_id || '').trim();
1052
+ let healedPlanVersionId = '';
1053
+ if (stripePriceId) {
1054
+ const mappingRow = this.db.prepare(`
1055
+ SELECT plan_version_id
1056
+ FROM billing_price_mappings
1057
+ WHERE tenant_id = ? AND stripe_price_id = ? AND active = 1
1058
+ ORDER BY updated_at DESC, created_at DESC
1059
+ LIMIT 1
1060
+ `).get(this.tenantId, stripePriceId);
1061
+ healedPlanVersionId = String(mappingRow?.plan_version_id || '').trim();
1062
+ }
1063
+ if (!healedPlanVersionId) {
1064
+ const billingPlanId = String(billingRow?.tier || '').trim().toLowerCase();
1065
+ if (!billingPlanId)
1066
+ return null;
1067
+ try {
1068
+ healedPlanVersionId = this.getRequiredDefaultPlanVersion(billingPlanId).planVersionId;
1069
+ }
1070
+ catch {
1071
+ return null;
1072
+ }
1073
+ }
1074
+ let healedVersion;
1075
+ try {
1076
+ healedVersion = this.getPlanVersionSnapshot(healedPlanVersionId);
1077
+ }
1078
+ catch {
1079
+ return null;
1080
+ }
1081
+ const effectiveAt = entitlement?.effectiveAt || new Date().toISOString();
1082
+ console.info(`[Taskforce Billing] Auto-healing canonical entitlement from billing mirror for account=${accountId} `
1083
+ + `previousState=${currentState || '(none)'} mirrorState=${billingStateFromMirror || '(none)'} stripeStatus=${billingStatus || '(none)'} `
1084
+ + `planVersionId=${healedVersion.planVersionId} planId=${healedVersion.planId} stripePriceId=${stripePriceId || '(none)'}`);
1085
+ return this.transitionCanonicalCommercialState({
1086
+ accountId,
1087
+ planVersionId: healedVersion.planVersionId,
1088
+ state: derivedBillingState,
1089
+ effectiveAt,
1090
+ effectiveUntil: billingRow?.effective_until || null,
1091
+ billingMirrorPlanId: healedVersion.planId,
1092
+ source: 'backfill',
1093
+ updatedBy: 'billing-mirror-auto-heal',
1094
+ createdAt: effectiveAt
1095
+ });
1096
+ }
847
1097
  getAccountAccessStatus(accountId) {
848
1098
  const normalizedAccountId = String(accountId || '').trim();
849
1099
  if (!normalizedAccountId) {
@@ -851,7 +1101,12 @@ export class PlanEntitlementService {
851
1101
  }
852
1102
  const onboardingPolicy = this.getOnboardingPolicy();
853
1103
  const validation = this.validateOnboardingPolicy(onboardingPolicy);
854
- const entitlement = this.tryGetAccountEntitlement(normalizedAccountId);
1104
+ const currentEntitlement = this.tryGetAccountEntitlement(normalizedAccountId);
1105
+ const entitlement = currentEntitlement
1106
+ ? (this.tryAutoHealLegacyPersonalFreeEntitlement(normalizedAccountId, currentEntitlement)
1107
+ || this.tryAutoHealCommercialEntitlementFromBillingMirror(normalizedAccountId, currentEntitlement)
1108
+ || currentEntitlement)
1109
+ : this.tryAutoHealCommercialEntitlementFromBillingMirror(normalizedAccountId, null);
855
1110
  if (!entitlement) {
856
1111
  return !validation.valid
857
1112
  ? this.buildBlockedAccessStatus({
@@ -949,7 +1204,7 @@ export class PlanEntitlementService {
949
1204
  if (!normalizedAccountId) {
950
1205
  throw new TaskforceRuleError('accountId is required', 400, 'ACCOUNT_ID_REQUIRED');
951
1206
  }
952
- const row = this.normalizeExpiredCheckoutPendingRow(normalizedAccountId);
1207
+ const row = this.normalizeExpiredLifecycleRow(normalizedAccountId);
953
1208
  if (!row?.account_id || !row.plan_version_id) {
954
1209
  throw new TaskforceRuleError('Entitlement not found', 404, 'ENTITLEMENT_NOT_FOUND');
955
1210
  }
@@ -989,6 +1244,24 @@ export class PlanEntitlementService {
989
1244
  const remaining = limit === null ? null : Math.max(0, limit - used);
990
1245
  return { used, limit, remaining };
991
1246
  }
1247
+ countAiProfileUsage(workspaceId) {
1248
+ const normalizedWorkspaceId = String(workspaceId || '').trim();
1249
+ if (!normalizedWorkspaceId) {
1250
+ throw new TaskforceRuleError('workspaceId is required', 400, 'WORKSPACE_ID_REQUIRED');
1251
+ }
1252
+ const row = this.db.prepare(`
1253
+ SELECT COUNT(*) AS c
1254
+ FROM ai_profiles
1255
+ WHERE tenant_id = ?
1256
+ AND workspace_id = ?
1257
+ AND archived_at IS NULL
1258
+ AND seat_scope = 'cloud_metered'
1259
+ `).get(this.tenantId, normalizedWorkspaceId);
1260
+ const used = Math.max(0, Number(row?.c || 0));
1261
+ const { limit } = this.resolveAiProfileLimitForWorkspace(normalizedWorkspaceId);
1262
+ const remaining = limit === null ? null : Math.max(0, limit - used);
1263
+ return { used, limit, remaining };
1264
+ }
992
1265
  assertSeatAvailable(workspaceId) {
993
1266
  const seatUsage = this.countWorkspaceSeatUsage(workspaceId, { includeInvited: true });
994
1267
  if (seatUsage.limit !== null && seatUsage.used >= seatUsage.limit) {
@@ -1026,6 +1299,19 @@ export class PlanEntitlementService {
1026
1299
  }
1027
1300
  return { allowed: true, reasonCode: 'OK', state: entitlement.state, seatUsage, feature };
1028
1301
  }
1302
+ if (featureKey === DOCUMENT_CLOUD_STORAGE_FEATURE_KEY && feature.access === 'limited') {
1303
+ const configuredLimitMb = this.readPositiveIntegerConfig(feature.config?.[DOCUMENT_CLOUD_STORAGE_LIMIT_CONFIG_KEY], 1) ?? 1;
1304
+ const storageLimitBytes = configuredLimitMb * 1024 * 1024;
1305
+ const rawRequestedBytes = input.context?.requestedBytes ?? input.context?.sizeBytes ?? input.context?.pendingSizeBytes;
1306
+ const requestedBytesNumber = Number(rawRequestedBytes);
1307
+ const requestedBytes = Number.isFinite(requestedBytesNumber)
1308
+ ? Math.max(0, Math.floor(requestedBytesNumber))
1309
+ : 0;
1310
+ const usedBytes = this.countWorkspaceStorageUsageBytes(workspaceId);
1311
+ if ((usedBytes + requestedBytes) > storageLimitBytes) {
1312
+ return { allowed: false, reasonCode: 'UPLOAD_STORAGE_LIMIT_REACHED', state: entitlement.state, feature };
1313
+ }
1314
+ }
1029
1315
  return { allowed: true, reasonCode: 'OK', state: entitlement.state, feature };
1030
1316
  }
1031
1317
  getUserAccountPlanMode(userId) {
@@ -1067,6 +1353,20 @@ export class PlanEntitlementService {
1067
1353
  entitlementState: 'active'
1068
1354
  };
1069
1355
  }
1356
+ resolveInviteAcceptanceDefaultEntitlement() {
1357
+ const defaultPersonalVersion = this.tryGetDefaultPlanVersionByMode('personal', { allowFallback: true });
1358
+ if (!defaultPersonalVersion)
1359
+ return null;
1360
+ if (this.isTeamPlanVersion(defaultPersonalVersion))
1361
+ return null;
1362
+ if (!this.planVersionHasActiveFreePricing(defaultPersonalVersion.planVersionId))
1363
+ return null;
1364
+ return {
1365
+ planVersionId: defaultPersonalVersion.planVersionId,
1366
+ planId: defaultPersonalVersion.planId,
1367
+ commercialState: 'active'
1368
+ };
1369
+ }
1070
1370
  // ─── Plan catalog admin ───────────────────────────────────────────────────
1071
1371
  isValidEntitlementTransition(fromState, toState) {
1072
1372
  if (fromState === toState)
@@ -1094,6 +1394,7 @@ export class PlanEntitlementService {
1094
1394
  upsertPlanCatalog(input) {
1095
1395
  const planId = String(input.planId || '').trim().toLowerCase();
1096
1396
  const displayName = String(input.displayName || '').trim();
1397
+ const description = String(input.description || '').trim() || null;
1097
1398
  const initialMode = String(input.initialMode || '').trim().toLowerCase() === 'team'
1098
1399
  ? 'team'
1099
1400
  : 'personal';
@@ -1119,7 +1420,7 @@ export class PlanEntitlementService {
1119
1420
  });
1120
1421
  const now = new Date().toISOString();
1121
1422
  const existing = this.db.prepare(`
1122
- SELECT plan_id, status
1423
+ SELECT plan_id, status, description
1123
1424
  FROM plan_catalog
1124
1425
  WHERE tenant_id = ? AND plan_id = ?
1125
1426
  LIMIT 1
@@ -1136,13 +1437,14 @@ export class PlanEntitlementService {
1136
1437
  : 'archived';
1137
1438
  const tx = this.db.transaction(() => {
1138
1439
  this.db.prepare(`
1139
- INSERT INTO plan_catalog (plan_id, tenant_id, display_name, status, created_at, updated_at)
1140
- VALUES (?, ?, ?, ?, ?, ?)
1440
+ INSERT INTO plan_catalog (plan_id, tenant_id, display_name, description, status, created_at, updated_at)
1441
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1141
1442
  ON CONFLICT (plan_id) DO UPDATE SET
1142
1443
  display_name = excluded.display_name,
1444
+ description = excluded.description,
1143
1445
  status = excluded.status,
1144
1446
  updated_at = excluded.updated_at
1145
- `).run(planId, this.tenantId, displayName, status, now, now);
1447
+ `).run(planId, this.tenantId, displayName, description, status, now, now);
1146
1448
  if (isNewPlan) {
1147
1449
  this.db.prepare(`
1148
1450
  INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, seat_limit, metadata_json, created_at, updated_at)
@@ -1168,11 +1470,11 @@ export class PlanEntitlementService {
1168
1470
  });
1169
1471
  tx();
1170
1472
  this.repairPlanDefaultVersionIfNeeded(planId);
1171
- return { planId, displayName, status };
1473
+ return { planId, displayName, description, status };
1172
1474
  }
1173
1475
  listPlanCatalog() {
1174
1476
  const rows = this.db.prepare(`
1175
- SELECT plan_id, display_name, status, created_at, updated_at
1477
+ SELECT plan_id, display_name, description, status, created_at, updated_at
1176
1478
  FROM plan_catalog
1177
1479
  WHERE tenant_id = ?
1178
1480
  ORDER BY updated_at DESC, created_at DESC
@@ -1180,21 +1482,76 @@ export class PlanEntitlementService {
1180
1482
  return rows.map((row) => ({
1181
1483
  planId: String(row.plan_id || ''),
1182
1484
  displayName: String(row.display_name || ''),
1485
+ description: typeof row.description === 'string' && row.description.trim().length > 0 ? row.description.trim() : null,
1183
1486
  status: String(row.status || '').trim().toLowerCase() === 'archived' ? 'archived' : 'active',
1184
1487
  createdAt: String(row.created_at || ''),
1185
1488
  updatedAt: String(row.updated_at || '')
1186
1489
  }));
1187
1490
  }
1188
1491
  listPlanFeatureCatalog() {
1492
+ const overrides = this.listPlanFeatureCatalogOverrideMap();
1189
1493
  return DEFAULT_PLAN_FEATURE_CATALOG.map((feature) => ({
1190
1494
  featureKey: feature.featureKey,
1191
1495
  label: feature.label,
1192
1496
  description: feature.description,
1497
+ publicLabel: overrides.get(feature.featureKey)?.publicLabel ?? feature.publicLabel ?? null,
1498
+ publicDescription: overrides.get(feature.featureKey)?.publicDescription ?? feature.publicDescription ?? null,
1499
+ publicDisplayOrder: overrides.get(feature.featureKey)?.publicDisplayOrder ?? feature.publicDisplayOrder ?? null,
1193
1500
  allowedAccessModes: [...feature.allowedAccessModes],
1194
1501
  configTemplate: { ...feature.configTemplate },
1195
1502
  ...(feature.dependsOn?.length ? { dependsOn: [...feature.dependsOn] } : {})
1196
1503
  }));
1197
1504
  }
1505
+ updatePlanFeatureCatalogMarketingCopy(input) {
1506
+ const featureKey = this.canonicalizePlanFeatureKey(input.featureKey);
1507
+ if (!featureKey)
1508
+ throw new TaskforceRuleError('featureKey is required', 400, 'FEATURE_KEY_REQUIRED');
1509
+ const catalogFeature = DEFAULT_PLAN_FEATURE_CATALOG.find((feature) => feature.featureKey === featureKey);
1510
+ if (!catalogFeature)
1511
+ throw new TaskforceRuleError('Unknown plan feature key', 404, 'PLAN_FEATURE_NOT_FOUND');
1512
+ const publicLabel = this.normalizeOptionalCatalogMarketingCopy(input.publicLabel);
1513
+ const publicDescription = this.normalizeOptionalCatalogMarketingCopy(input.publicDescription);
1514
+ const publicDisplayOrder = this.normalizeOptionalCatalogDisplayOrder(input.publicDisplayOrder);
1515
+ const now = new Date().toISOString();
1516
+ const tx = this.db.transaction(() => {
1517
+ if (!publicLabel && !publicDescription && publicDisplayOrder === null) {
1518
+ this.db.prepare(`
1519
+ DELETE FROM plan_feature_catalog_overrides
1520
+ WHERE tenant_id = ? AND feature_key = ?
1521
+ `).run(this.tenantId, featureKey);
1522
+ return;
1523
+ }
1524
+ this.db.prepare(`
1525
+ INSERT INTO plan_feature_catalog_overrides (
1526
+ tenant_id,
1527
+ feature_key,
1528
+ public_label,
1529
+ public_description,
1530
+ public_display_order,
1531
+ created_at,
1532
+ updated_at
1533
+ )
1534
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1535
+ ON CONFLICT (tenant_id, feature_key) DO UPDATE SET
1536
+ public_label = excluded.public_label,
1537
+ public_description = excluded.public_description,
1538
+ public_display_order = excluded.public_display_order,
1539
+ updated_at = excluded.updated_at
1540
+ `).run(this.tenantId, featureKey, publicLabel, publicDescription, publicDisplayOrder, now, now);
1541
+ });
1542
+ tx();
1543
+ return this.listPlanFeatureCatalog().find((feature) => feature.featureKey === featureKey) || {
1544
+ featureKey: catalogFeature.featureKey,
1545
+ label: catalogFeature.label,
1546
+ description: catalogFeature.description,
1547
+ publicLabel,
1548
+ publicDescription,
1549
+ publicDisplayOrder,
1550
+ allowedAccessModes: [...catalogFeature.allowedAccessModes],
1551
+ configTemplate: { ...catalogFeature.configTemplate },
1552
+ ...(catalogFeature.dependsOn?.length ? { dependsOn: [...catalogFeature.dependsOn] } : {})
1553
+ };
1554
+ }
1198
1555
  listPlanVersions(planId) {
1199
1556
  const normalizedPlanId = String(planId || '').trim().toLowerCase();
1200
1557
  if (!normalizedPlanId)
@@ -1385,9 +1742,13 @@ export class PlanEntitlementService {
1385
1742
  `).all(this.tenantId, planVersionId);
1386
1743
  const features = {};
1387
1744
  for (const featureRow of featuresRows) {
1388
- const key = String(featureRow.feature_key || '').trim();
1745
+ const originalKey = String(featureRow.feature_key || '').trim();
1746
+ const key = this.canonicalizePlanFeatureKey(originalKey);
1389
1747
  if (!key)
1390
1748
  continue;
1749
+ if (key === WORKSPACES_FEATURE_KEY && originalKey === LEGACY_WORKSPACES_FEATURE_KEY && features[key]) {
1750
+ continue;
1751
+ }
1391
1752
  features[key] = {
1392
1753
  featureKey: key,
1393
1754
  access: this.normalizeFeatureAccess(featureRow.access),
@@ -1417,7 +1778,7 @@ export class PlanEntitlementService {
1417
1778
  ...currentVersion.features
1418
1779
  };
1419
1780
  for (const feature of features) {
1420
- const featureKey = String(feature.featureKey || '').trim();
1781
+ const featureKey = this.canonicalizePlanFeatureKey(feature.featureKey);
1421
1782
  if (!featureKey)
1422
1783
  continue;
1423
1784
  const access = this.normalizeFeatureAccess(feature.access);