@taskforcehq/taskforce 0.3.305 → 0.3.306

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) hide show
  1. package/dist/Taskforce.module.css +295 -89
  2. package/dist/TaskforceCore.js +215 -43
  3. package/dist/TaskforceCore.test.js +433 -28
  4. package/dist/assets/fonts/CourierPrime-Bold.woff2 +0 -0
  5. package/dist/assets/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  6. package/dist/assets/fonts/CourierPrime-Italic.woff2 +0 -0
  7. package/dist/assets/fonts/CourierPrime-Regular.woff2 +0 -0
  8. package/dist/assets/fonts/Noir_medium.woff2 +0 -0
  9. package/dist/assets/fonts/Noir_regular.woff2 +0 -0
  10. package/dist/assets/fonts/RussoOne-Regular.woff2 +0 -0
  11. package/dist/assets/fonts/SpecialElite-Regular.woff2 +0 -0
  12. package/dist/compat/workspaceSyncCompat.js +46 -2
  13. package/dist/compat/workspaceSyncCompat.test.js +38 -1
  14. package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
  15. package/dist/components/context/ContextAttachmentManager.js +549 -0
  16. package/dist/components/features/AgentsModule.d.ts +10 -1
  17. package/dist/components/features/AgentsModule.js +260 -15
  18. package/dist/components/features/AgentsModule.test.js +255 -5
  19. package/dist/components/features/DocumentViewer.d.ts +5 -1
  20. package/dist/components/features/DocumentViewer.js +60 -3
  21. package/dist/components/features/DocumentWorkspace.js +1 -1
  22. package/dist/components/features/DocumentWorkspace.test.js +33 -0
  23. package/dist/components/features/documentWorkspaceModel.d.ts +1 -1
  24. package/dist/components/task/TaskCard.js +19 -28
  25. package/dist/components/task/TaskCard.test.js +38 -3
  26. package/dist/components/task/TaskContextUpload.js +4 -526
  27. package/dist/components/task/TaskForm.js +85 -90
  28. package/dist/components/task/TaskForm.test.js +110 -8
  29. package/dist/components/task/TaskKanban.d.ts +2 -1
  30. package/dist/components/task/TaskKanban.js +94 -13
  31. package/dist/components/task/TaskKanban.test.js +36 -0
  32. package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
  33. package/dist/components/ui/EditableAvatarButton.js +18 -0
  34. package/dist/components/views/PlanComparisonPage.d.ts +9 -1
  35. package/dist/components/views/PlanComparisonPage.js +70 -14
  36. package/dist/components/views/PlanComparisonPage.test.js +238 -0
  37. package/dist/components/views/PlansPage.js +127 -36
  38. package/dist/components/views/PlansPage.test.js +294 -12
  39. package/dist/components/views/StandaloneLayout.js +283 -533
  40. package/dist/components/views/panels/PlanningDrawer.d.ts +8 -1
  41. package/dist/components/views/panels/PlanningDrawer.js +18 -11
  42. package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
  43. package/dist/components/views/panels/PlanningDrawer.test.js +141 -0
  44. package/dist/components/views/planningScope.d.ts +20 -0
  45. package/dist/components/views/planningScope.js +25 -0
  46. package/dist/components/views/planningScope.test.d.ts +1 -0
  47. package/dist/components/views/planningScope.test.js +60 -0
  48. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +26 -1
  49. package/dist/components/views/standalone/modals/AccountHubModal.js +76 -2
  50. package/dist/components/views/standalone/modals/AccountHubModal.test.js +20 -3
  51. package/dist/components/views/standalone/modals/AvatarImageManagerModal.d.ts +20 -0
  52. package/dist/components/views/standalone/modals/AvatarImageManagerModal.js +127 -0
  53. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +2 -9
  54. package/dist/components/views/standalone/modals/EditProfileModal.js +3 -2
  55. package/dist/components/views/standalone/modals/SyncEnableWarningModal.d.ts +9 -0
  56. package/dist/components/views/standalone/modals/SyncEnableWarningModal.js +6 -0
  57. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +1 -0
  58. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +24 -0
  59. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +2 -1
  60. package/dist/components/views/standalone/modals/SyncStatusModal.js +2 -2
  61. package/dist/core/AiProfileService.d.ts +39 -2
  62. package/dist/core/AiProfileService.js +639 -73
  63. package/dist/core/AiProfileService.test.js +19 -1
  64. package/dist/core/AiProfiles.test.js +566 -7
  65. package/dist/core/AttachmentLinkService.js +57 -15
  66. package/dist/core/AuthIdentityService.d.ts +107 -0
  67. package/dist/core/AuthIdentityService.js +284 -0
  68. package/dist/core/AuthTokenService.d.ts +4 -0
  69. package/dist/core/AuthTokenService.js +34 -6
  70. package/dist/core/AuthTokenService.test.js +3 -3
  71. package/dist/core/EntitlementsPolicy.test.js +2 -2
  72. package/dist/core/GlobalSettingsService.js +115 -6
  73. package/dist/core/PlanEntitlementService.d.ts +23 -2
  74. package/dist/core/PlanEntitlementService.js +343 -57
  75. package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
  76. package/dist/core/PlanFeatureCatalog.test.js +84 -0
  77. package/dist/core/PlanVersionPolicy.test.js +35 -2
  78. package/dist/core/PlanningEntities.test.js +52 -0
  79. package/dist/core/SignupMonetizationSettings.test.js +199 -27
  80. package/dist/core/SyncReconciliationService.js +2 -65
  81. package/dist/core/SystemAdmin.test.js +42 -42
  82. package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
  83. package/dist/core/Taskforce.d.ts +225 -14
  84. package/dist/core/Taskforce.js +765 -192
  85. package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
  86. package/dist/core/Taskforce.listTasksSlim.test.js +91 -0
  87. package/dist/core/UserProfileAvatarDrafts.test.js +30 -3
  88. package/dist/core/WorkspaceLifecycleService.js +0 -5
  89. package/dist/core/WorkspacePermissions.test.js +25 -1
  90. package/dist/core/WorkspacePlanMode.test.js +117 -16
  91. package/dist/core/shared.d.ts +1 -1
  92. package/dist/core/shared.js +5 -1
  93. package/dist/core/types.d.ts +68 -2
  94. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +26 -1
  95. package/dist/hooks/sync/bootstrap.js +2 -0
  96. package/dist/hooks/sync/controlPlane.d.ts +37 -0
  97. package/dist/hooks/sync/controlPlane.js +78 -0
  98. package/dist/hooks/sync/controlPlane.test.d.ts +1 -0
  99. package/dist/hooks/sync/controlPlane.test.js +121 -0
  100. package/dist/hooks/sync/lifecyclePolicy.d.ts +45 -0
  101. package/dist/hooks/sync/lifecyclePolicy.js +93 -0
  102. package/dist/hooks/sync/lifecyclePolicy.test.d.ts +1 -0
  103. package/dist/hooks/sync/lifecyclePolicy.test.js +124 -0
  104. package/dist/hooks/sync/orchestratorShared.d.ts +10 -1
  105. package/dist/hooks/sync/orchestratorShared.js +68 -47
  106. package/dist/hooks/sync/orchestratorShared.test.js +50 -1
  107. package/dist/hooks/sync/pullLifecycle.d.ts +31 -0
  108. package/dist/hooks/sync/pullLifecycle.js +24 -0
  109. package/dist/hooks/sync/pullLifecycle.test.d.ts +1 -0
  110. package/dist/hooks/sync/pullLifecycle.test.js +80 -0
  111. package/dist/hooks/sync/recovery.d.ts +16 -2
  112. package/dist/hooks/sync/recovery.js +171 -53
  113. package/dist/hooks/sync/recovery.test.d.ts +1 -0
  114. package/dist/hooks/sync/recovery.test.js +292 -0
  115. package/dist/hooks/sync/transfers.d.ts +15 -3
  116. package/dist/hooks/sync/transfers.js +179 -62
  117. package/dist/hooks/sync/transfers.test.d.ts +1 -0
  118. package/dist/hooks/sync/transfers.test.js +396 -0
  119. package/dist/hooks/sync/useRecentSyncEvents.d.ts +21 -0
  120. package/dist/hooks/sync/useRecentSyncEvents.js +92 -0
  121. package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +1 -0
  122. package/dist/hooks/sync/useRecentSyncEvents.test.js +124 -0
  123. package/dist/hooks/sync/useSyncStatusActions.d.ts +30 -0
  124. package/dist/hooks/sync/useSyncStatusActions.js +88 -0
  125. package/dist/hooks/sync/useSyncStatusActions.test.d.ts +1 -0
  126. package/dist/hooks/sync/useSyncStatusActions.test.js +133 -0
  127. package/dist/hooks/sync/useSyncStatusControls.d.ts +25 -0
  128. package/dist/hooks/sync/useSyncStatusControls.js +60 -0
  129. package/dist/hooks/sync/useSyncStatusControls.test.d.ts +1 -0
  130. package/dist/hooks/sync/useSyncStatusControls.test.js +88 -0
  131. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +12 -0
  132. package/dist/hooks/useSyncOrchestrator.d.ts +9 -2
  133. package/dist/hooks/useSyncOrchestrator.js +359 -178
  134. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +407 -0
  135. package/dist/hooks/useTaskData.js +8 -3
  136. package/dist/hooks/useTaskData.test.js +45 -0
  137. package/dist/hooks/useTaskforce.d.ts +60 -2
  138. package/dist/hooks/useTaskforce.js +549 -240
  139. package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
  140. package/dist/hooks/useTaskforce.runtime-routing.test.js +152 -0
  141. package/dist/hooks/useTaskforce.sync-behavior.test.js +2380 -212
  142. package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
  143. package/dist/hooks/useWorkspaceSyncController.js +6 -1
  144. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
  145. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +316 -35
  146. package/dist/localization/locales/en-US.d.ts +0 -1
  147. package/dist/localization/locales/en-US.js +0 -1
  148. package/dist/localization/locales/es-419.js +0 -1
  149. package/dist/localization/locales/pt-BR.js +0 -1
  150. package/dist/mcp/documentAssetRegistrar.js +3 -3
  151. package/dist/mcp/runtime.d.ts +2 -0
  152. package/dist/mcp/runtime.js +131 -64
  153. package/dist/mcp/runtime.test.js +291 -12
  154. package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
  155. package/dist/migrations/taskSchemaMigrations.js +171 -61
  156. package/dist/migrations/taskSchemaMigrations.test.js +15 -0
  157. package/dist/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  158. package/dist/resources/templates/workflows/collaborate.yaml +1 -1
  159. package/dist/resources/templates/workflows/evaluate.yaml +2 -2
  160. package/dist/resources/templates/workflows/plan.yaml +1 -1
  161. package/dist/resources/templates/workflows/review.yaml +2 -2
  162. package/dist/server/annotatedAttachmentsRoutes.test.js +3 -3
  163. package/dist/server/auth/providers/apple.d.ts +33 -0
  164. package/dist/server/auth/providers/apple.js +82 -0
  165. package/dist/server/auth/providers/appleClientSecret.d.ts +37 -0
  166. package/dist/server/auth/providers/appleClientSecret.js +65 -0
  167. package/dist/server/auth/providers/github.d.ts +26 -0
  168. package/dist/server/auth/providers/github.js +86 -0
  169. package/dist/server/auth/providers/google.d.ts +21 -0
  170. package/dist/server/auth/providers/google.js +56 -0
  171. package/dist/server/auth/providers/types.d.ts +21 -0
  172. package/dist/server/auth/providers/types.js +1 -0
  173. package/dist/server/auth.d.ts +2 -0
  174. package/dist/server/auth.js +6 -3
  175. package/dist/server/documentReviewRoutes.test.js +36 -3
  176. package/dist/server/index.cookieProxy.test.js +1 -0
  177. package/dist/server/index.d.ts +12 -0
  178. package/dist/server/index.js +120 -9
  179. package/dist/server/index.rateLimit.test.js +3 -0
  180. package/dist/server/index.test.js +106 -3
  181. package/dist/server/routes/admin.d.ts +3 -0
  182. package/dist/server/routes/admin.js +272 -19
  183. package/dist/server/routes/auth.d.ts +15 -2
  184. package/dist/server/routes/auth.js +994 -57
  185. package/dist/server/routes/authSupport.d.ts +1 -1
  186. package/dist/server/routes/authSupport.js +1 -1
  187. package/dist/server/routes/billing.js +361 -35
  188. package/dist/server/routes/billing.test.js +592 -12
  189. package/dist/server/routes/documents.d.ts +1 -0
  190. package/dist/server/routes/documents.js +172 -70
  191. package/dist/server/routes/shared.d.ts +28 -2
  192. package/dist/server/routes/shared.js +69 -4
  193. package/dist/server/routes/sync.integration.test.js +437 -39
  194. package/dist/server/routes/sync.js +61 -605
  195. package/dist/server/routes/syncAuxRoutes.d.ts +15 -0
  196. package/dist/server/routes/syncAuxRoutes.js +91 -0
  197. package/dist/server/routes/syncAuxRoutes.test.d.ts +1 -0
  198. package/dist/server/routes/syncAuxRoutes.test.js +158 -0
  199. package/dist/server/routes/syncPullApplyRoutes.d.ts +36 -0
  200. package/dist/server/routes/syncPullApplyRoutes.js +204 -0
  201. package/dist/server/routes/syncPushRoutes.d.ts +24 -0
  202. package/dist/server/routes/syncPushRoutes.js +212 -0
  203. package/dist/server/routes/syncRouteGuards.d.ts +36 -0
  204. package/dist/server/routes/syncRouteGuards.js +67 -0
  205. package/dist/server/routes/syncRouteGuards.test.d.ts +1 -0
  206. package/dist/server/routes/syncRouteGuards.test.js +94 -0
  207. package/dist/server/routes/syncRouteTypes.d.ts +13 -0
  208. package/dist/server/routes/syncRouteTypes.js +1 -0
  209. package/dist/server/routes/syncSnapshotRoutes.d.ts +66 -0
  210. package/dist/server/routes/syncSnapshotRoutes.js +180 -0
  211. package/dist/server/routes/tasks.js +43 -1
  212. package/dist/server/routes/workspaces.js +36 -5
  213. package/dist/server/routes.js +60 -3
  214. package/dist/server/routes.test.js +1824 -171
  215. package/dist/services/workflowExportSnapshots.test.js +2 -0
  216. package/dist/shared/aiProfileSeatScope.d.ts +5 -0
  217. package/dist/shared/aiProfileSeatScope.js +21 -0
  218. package/dist/shared/passwordPolicy.d.ts +13 -0
  219. package/dist/shared/passwordPolicy.js +84 -0
  220. package/dist/shared/passwordPolicy.test.d.ts +1 -0
  221. package/dist/shared/passwordPolicy.test.js +34 -0
  222. package/dist/storage/workspaceAssetStore.d.ts +15 -3
  223. package/dist/storage/workspaceAssetStore.js +132 -44
  224. package/dist/storage/workspaceAssetStore.test.js +93 -6
  225. package/dist/styles/fonts.css +70 -0
  226. package/dist/sync/collaborationSyncPayload.d.ts +6 -1
  227. package/dist/sync/collaborationSyncPayload.js +17 -11
  228. package/dist/sync/collaborationSyncPayload.test.js +13 -1
  229. package/dist/sync/contentSyncState.d.ts +4 -8
  230. package/dist/sync/contentSyncState.js +17 -70
  231. package/dist/sync/syncApplyHandlers.d.ts +7 -0
  232. package/dist/sync/syncApplyHandlers.js +28 -13
  233. package/dist/sync/syncApplyHandlers.test.js +70 -3
  234. package/dist/sync/syncFieldSemantics.d.ts +12 -0
  235. package/dist/sync/syncFieldSemantics.js +55 -0
  236. package/dist/sync/syncResourceAdapters.d.ts +44 -0
  237. package/dist/sync/syncResourceAdapters.js +77 -0
  238. package/dist/sync/syncService.d.ts +5 -0
  239. package/dist/sync/syncService.js +52 -0
  240. package/dist/sync/syncService.test.js +57 -3
  241. package/dist/sync/taskSyncChangeKey.d.ts +2 -0
  242. package/dist/sync/taskSyncChangeKey.js +143 -0
  243. package/dist/sync/taskSyncPayload.js +3 -8
  244. package/dist/sync/workspacePullFeed.js +4 -3
  245. package/dist/sync/workspacePullFeed.test.js +43 -0
  246. package/dist/sync/workspaceRepair.js +5 -2
  247. package/dist/sync/workspaceSyncModel.d.ts +45 -0
  248. package/dist/sync/workspaceSyncModel.js +183 -28
  249. package/dist/sync/workspaceSyncModel.test.js +291 -12
  250. package/dist/sync/workspaceSyncState.d.ts +17 -0
  251. package/dist/sync/workspaceSyncState.js +2 -1
  252. package/dist/sync/workspaceSyncSurface.js +3 -8
  253. package/dist/types.d.ts +17 -0
  254. package/dist/ui/assets/AgentsModule-DrLawwNl.js +1 -0
  255. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BG6P_GVW.js → AnnotatedAttachmentWorkspace-Z9_whf7F.js} +1 -1
  256. package/dist/ui/assets/CourierPrime-Bold-BuLj_dpw.woff2 +0 -0
  257. package/dist/ui/assets/CourierPrime-BoldItalic-BrK2pUkZ.woff2 +0 -0
  258. package/dist/ui/assets/CourierPrime-Italic-DFUZK5Ey.woff2 +0 -0
  259. package/dist/ui/assets/CourierPrime-Regular-BsKphzVf.woff2 +0 -0
  260. package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +252 -0
  261. package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +1 -0
  262. package/dist/ui/assets/{InitiativesModule-Dhm7M8e7.js → InitiativesModule-9E6ParrY.js} +1 -1
  263. package/dist/ui/assets/Noir_medium-bxQwKGzB.woff2 +0 -0
  264. package/dist/ui/assets/Noir_regular-ojf2kxlG.woff2 +0 -0
  265. package/dist/ui/assets/PlansPage-CvfnMiWq.css +1 -0
  266. package/dist/ui/assets/PlansPage-DRXscYxH.js +1 -0
  267. package/dist/ui/assets/RussoOne-Regular-Cu_qq_qC.woff2 +0 -0
  268. package/dist/ui/assets/SpecialElite-Regular-Di6gSvAS.woff2 +0 -0
  269. package/dist/ui/assets/{TaskSettings-ln0aF7xc.js → TaskSettings-DZX4jk7e.js} +1 -1
  270. package/dist/ui/assets/{WorkflowsModule-BcS4afRn.js → WorkflowsModule-BA7jcEpH.js} +1 -1
  271. package/dist/ui/assets/index-9eT8e0Lu.css +1 -0
  272. package/dist/ui/assets/index-C6k75jr8.js +6 -0
  273. package/dist/ui/assets/{vendor-icons-BSUaRwF8.js → vendor-icons-DuEd_65c.js} +1 -1
  274. package/dist/ui/fonts/CourierPrime-Bold.ttf +0 -0
  275. package/dist/ui/fonts/CourierPrime-Bold.woff2 +0 -0
  276. package/dist/ui/fonts/CourierPrime-BoldItalic.ttf +0 -0
  277. package/dist/ui/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  278. package/dist/ui/fonts/CourierPrime-Italic.ttf +0 -0
  279. package/dist/ui/fonts/CourierPrime-Italic.woff2 +0 -0
  280. package/dist/ui/fonts/CourierPrime-Regular.ttf +0 -0
  281. package/dist/ui/fonts/CourierPrime-Regular.woff2 +0 -0
  282. package/dist/ui/fonts/Noir_medium.otf +0 -0
  283. package/dist/ui/fonts/Noir_medium.woff2 +0 -0
  284. package/dist/ui/fonts/Noir_regular.otf +0 -0
  285. package/dist/ui/fonts/Noir_regular.woff2 +0 -0
  286. package/dist/ui/fonts/RussoOne-Regular.woff2 +0 -0
  287. package/dist/ui/fonts/SpecialElite-Regular.ttf +0 -0
  288. package/dist/ui/fonts/SpecialElite-Regular.woff2 +0 -0
  289. package/dist/ui/index.html +3 -3
  290. package/dist/utils/accountProfileSummaryCache.d.ts +5 -0
  291. package/dist/utils/assignees.d.ts +2 -0
  292. package/dist/utils/assignees.js +2 -0
  293. package/dist/utils/avatarUpload.d.ts +27 -0
  294. package/dist/utils/avatarUpload.js +132 -0
  295. package/dist/utils/avatarUpload.test.d.ts +1 -0
  296. package/dist/utils/avatarUpload.test.js +40 -0
  297. package/dist/utils/bootstrapTrace.d.ts +7 -0
  298. package/dist/utils/bootstrapTrace.js +29 -0
  299. package/dist/utils/commercialLifecyclePresentation.js +3 -0
  300. package/dist/utils/contextAssetEvents.d.ts +2 -0
  301. package/dist/utils/syncEventDetails.d.ts +3 -0
  302. package/dist/utils/syncEventDetails.js +64 -0
  303. package/dist/utils/syncEventDetails.test.d.ts +1 -0
  304. package/dist/utils/syncEventDetails.test.js +23 -0
  305. package/dist/utils/syncEventPresentation.d.ts +15 -0
  306. package/dist/utils/syncEventPresentation.js +129 -0
  307. package/dist/utils/syncEventPresentation.test.d.ts +1 -0
  308. package/dist/utils/syncEventPresentation.test.js +64 -0
  309. package/dist/utils/syncStatusPresentation.d.ts +72 -0
  310. package/dist/utils/syncStatusPresentation.js +217 -0
  311. package/dist/utils/syncStatusPresentation.test.d.ts +1 -0
  312. package/dist/utils/syncStatusPresentation.test.js +164 -0
  313. package/dist/utils/taskActivity.d.ts +2 -0
  314. package/dist/utils/taskActivity.js +228 -22
  315. package/dist/utils/taskActivity.test.js +119 -8
  316. package/dist/utils/workspaceSyncPresentation.d.ts +4 -0
  317. package/dist/utils/workspaceSyncPresentation.js +68 -2
  318. package/dist/utils/workspaceSyncPresentation.test.js +36 -1
  319. package/package.json +3 -2
  320. package/scripts/export-sqlite-to-postgres.mjs +7 -9
  321. package/scripts/run-e2e-realtime.sh +17 -3
  322. package/scripts/run-smoke.sh +17 -5
  323. package/scripts/run-soak.sh +17 -5
  324. package/src/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  325. package/src/resources/templates/workflows/collaborate.yaml +1 -1
  326. package/src/resources/templates/workflows/evaluate.yaml +2 -2
  327. package/src/resources/templates/workflows/plan.yaml +1 -1
  328. package/src/resources/templates/workflows/review.yaml +2 -2
  329. package/dist/ui/assets/AgentsModule-N2MnQBZk.js +0 -1
  330. package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +0 -1
  331. package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +0 -252
  332. package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
  333. package/dist/ui/assets/PlansPage-Cq0zXj3I.js +0 -1
  334. package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
  335. package/dist/ui/assets/index-C2-OXZV7.css +0 -1
  336. package/dist/ui/assets/index-DcSI5F86.js +0 -6
  337. package/scripts/migrate-planning-model.ts +0 -233
@@ -13,7 +13,7 @@ export declare function buildAuthFlowBehavior(core: TaskforceCore): {
13
13
  };
14
14
  export declare function buildAuthBehavior(core: TaskforceCore): {
15
15
  onboardingPolicyMode: import("../../core/types.js").OnboardingPolicyMode;
16
- onboardingPlanVersionId: string | null;
16
+ onboardingPlanId: string | null;
17
17
  allowSelfRegistration: boolean;
18
18
  requireVerifiedEmail: boolean;
19
19
  defaultNewUserBetaAccess: boolean;
@@ -45,7 +45,7 @@ export function buildAuthBehavior(core) {
45
45
  return {
46
46
  ...authBehavior,
47
47
  onboardingPolicyMode: onboardingPolicy.mode,
48
- onboardingPlanVersionId: onboardingPolicy.planVersionId
48
+ onboardingPlanId: onboardingPolicy.planId
49
49
  };
50
50
  }
51
51
  export function buildSiteBehavior(core) {
@@ -8,16 +8,63 @@ const BILLING_DIAGNOSTIC_RETENTION_DAYS = 30;
8
8
  const BILLING_DIAGNOSTIC_MAX_PAYLOAD_BYTES = 8 * 1024;
9
9
  const BILLING_CHECKOUT_ATTEMPT_RETENTION_DAYS = 14;
10
10
  const BILLING_STATUS_RECONCILIATION_TTL_MS = 30_000;
11
+ const PUBLIC_PRICING_CACHE_TTL_MS = 60_000;
11
12
  const TASKFORCE_PROXY_ORIGIN_HOST_HEADER = 'x-taskforce-origin-host';
12
13
  const TASKFORCE_PROXY_ORIGIN_PROTO_HEADER = 'x-taskforce-origin-proto';
13
14
  const TASKFORCE_PROXY_ORIGIN_BASE_URL_HEADER = 'x-taskforce-origin-base-url';
14
15
  const TASKFORCE_PROXY_ORIGIN_RUNTIME_HEADER = 'x-taskforce-origin-runtime';
16
+ const INTERNAL_PUBLIC_PRICING_FEATURE_KEYS = new Set([
17
+ 'collaboration.invites',
18
+ 'collaboration.seat_limit'
19
+ ]);
20
+ const publicPricingCache = new Map();
15
21
  function parsePricingType(raw) {
16
22
  const value = String(raw || '').trim().toLowerCase();
17
23
  if (value === 'stripe' || value === 'free')
18
24
  return value;
19
25
  return null;
20
26
  }
27
+ function readTableVersionStats(core, tableName) {
28
+ const db = core.getDatabaseAdapter();
29
+ const tenantId = core.getTenantId();
30
+ try {
31
+ const row = db.prepare(`
32
+ SELECT COUNT(*) AS count, MAX(updated_at) AS max_updated_at
33
+ FROM ${tableName}
34
+ WHERE tenant_id = ?
35
+ `).get(tenantId);
36
+ return {
37
+ count: Number(row?.count || 0),
38
+ maxUpdatedAt: String(row?.max_updated_at || '')
39
+ };
40
+ }
41
+ catch (error) {
42
+ const message = String(error?.message || '');
43
+ if (message.toLowerCase().includes(`no such table: ${tableName}`)) {
44
+ return { count: 0, maxUpdatedAt: '' };
45
+ }
46
+ throw error;
47
+ }
48
+ }
49
+ function buildPublicPricingCacheVersion(core) {
50
+ const catalog = readTableVersionStats(core, 'plan_catalog');
51
+ const versions = readTableVersionStats(core, 'plan_versions');
52
+ const prices = readTableVersionStats(core, 'billing_price_mappings');
53
+ const features = readTableVersionStats(core, 'plan_feature_matrix');
54
+ const featureCatalogOverrides = readTableVersionStats(core, 'plan_feature_catalog_overrides');
55
+ return [
56
+ catalog.count,
57
+ catalog.maxUpdatedAt,
58
+ versions.count,
59
+ versions.maxUpdatedAt,
60
+ prices.count,
61
+ prices.maxUpdatedAt,
62
+ features.count,
63
+ features.maxUpdatedAt,
64
+ featureCatalogOverrides.count,
65
+ featureCatalogOverrides.maxUpdatedAt
66
+ ].join('|');
67
+ }
21
68
  function buildFreePricingSyntheticId(planVersionId, interval) {
22
69
  return `free:${planVersionId}:${interval}`;
23
70
  }
@@ -664,7 +711,11 @@ function resolveCheckoutLifecycleGuard(core, input) {
664
711
  : currentOpenAttempt;
665
712
  const attemptStatus = String(checkoutAttempt?.status || '').trim().toLowerCase();
666
713
  const attemptPlanVersionId = String(checkoutAttempt?.plan_version_id || '').trim();
714
+ const sourceLabel = String(input.source || 'checkout-confirm').trim() || 'checkout-confirm';
667
715
  if (attemptStatus === 'expired') {
716
+ console.warn(`[Taskforce Billing] Lifecycle guard blocked expired checkout for user=${input.userId} source=${sourceLabel} `
717
+ + `session=${String(input.sessionId || '').trim() || '(none)'} subscription=${String(input.subscriptionId || '').trim() || '(none)'} `
718
+ + `requestedPlanVersionId=${String(input.planVersionId || '').trim() || '(none)'} attemptPlanVersionId=${attemptPlanVersionId || '(none)'}`);
668
719
  return {
669
720
  blocked: true,
670
721
  code: 'CHECKOUT_SESSION_EXPIRED',
@@ -672,6 +723,9 @@ function resolveCheckoutLifecycleGuard(core, input) {
672
723
  };
673
724
  }
674
725
  if (attemptStatus === 'superseded') {
726
+ console.warn(`[Taskforce Billing] Lifecycle guard blocked superseded checkout for user=${input.userId} source=${sourceLabel} `
727
+ + `session=${String(input.sessionId || '').trim() || '(none)'} subscription=${String(input.subscriptionId || '').trim() || '(none)'} `
728
+ + `requestedPlanVersionId=${String(input.planVersionId || '').trim() || '(none)'} attemptPlanVersionId=${attemptPlanVersionId || '(none)'}`);
675
729
  return {
676
730
  blocked: true,
677
731
  code: 'CHECKOUT_SESSION_SUPERSEDED',
@@ -679,6 +733,9 @@ function resolveCheckoutLifecycleGuard(core, input) {
679
733
  };
680
734
  }
681
735
  if (attemptStatus === 'open' && attemptPlanVersionId && input.planVersionId && attemptPlanVersionId !== input.planVersionId) {
736
+ console.warn(`[Taskforce Billing] Lifecycle guard blocked mismatched open checkout for user=${input.userId} source=${sourceLabel} `
737
+ + `session=${String(input.sessionId || '').trim() || '(none)'} subscription=${String(input.subscriptionId || '').trim() || '(none)'} `
738
+ + `requestedPlanVersionId=${String(input.planVersionId || '').trim() || '(none)'} attemptPlanVersionId=${attemptPlanVersionId}`);
682
739
  return {
683
740
  blocked: true,
684
741
  code: 'CHECKOUT_SESSION_SUPERSEDED',
@@ -688,6 +745,9 @@ function resolveCheckoutLifecycleGuard(core, input) {
688
745
  const currentBilling = getBillingMirrorRow(core, input.userId);
689
746
  const currentSubscriptionId = String(currentBilling?.stripe_subscription_id || '').trim();
690
747
  if (!currentOpenAttempt && input.subscriptionId && currentSubscriptionId && currentSubscriptionId !== input.subscriptionId) {
748
+ console.warn(`[Taskforce Billing] Lifecycle guard blocked stale subscription linkage for user=${input.userId} source=${sourceLabel} `
749
+ + `currentSubscriptionId=${currentSubscriptionId} requestedSubscriptionId=${String(input.subscriptionId || '').trim() || '(none)'} `
750
+ + `requestedPlanVersionId=${String(input.planVersionId || '').trim() || '(none)'}`);
691
751
  return {
692
752
  blocked: true,
693
753
  code: 'CHECKOUT_SESSION_SUPERSEDED',
@@ -701,17 +761,28 @@ function resolveCheckoutLifecycleGuard(core, input) {
701
761
  const currentPlanVersionId = String(entitlement.plan_version_id || '').trim();
702
762
  const updatedBy = String(entitlement.updated_by || '').trim().toLowerCase();
703
763
  const expiredPendingCheckout = currentState === 'checkout_pending' && isIsoTimestampExpired(entitlement.effective_until);
704
- if (expiredPendingCheckout || (currentState === 'pending_plan_selection' && updatedBy === 'checkout-expired')) {
764
+ const fromStripeWebhook = input.source === 'stripe-webhook';
765
+ if (!fromStripeWebhook && (expiredPendingCheckout || (currentState === 'pending_plan_selection' && updatedBy === 'checkout-expired'))) {
766
+ console.warn(`[Taskforce Billing] Lifecycle guard blocked expired entitlement recovery for user=${input.userId} source=${sourceLabel} `
767
+ + `entitlementState=${currentState || '(none)'} updatedBy=${updatedBy || '(none)'} `
768
+ + `requestedPlanVersionId=${String(input.planVersionId || '').trim() || '(none)'} currentPlanVersionId=${currentPlanVersionId || '(none)'}`);
705
769
  return {
706
770
  blocked: true,
707
771
  code: 'CHECKOUT_SESSION_EXPIRED',
708
772
  message: 'This checkout session expired. Choose a plan and start checkout again.'
709
773
  };
710
774
  }
775
+ if (fromStripeWebhook && (expiredPendingCheckout || (currentState === 'pending_plan_selection' && updatedBy === 'checkout-expired'))) {
776
+ console.info(`[Taskforce Billing] Lifecycle guard allowed authoritative webhook recovery for user=${input.userId} `
777
+ + `subscription=${String(input.subscriptionId || '').trim() || '(none)'} requestedPlanVersionId=${String(input.planVersionId || '').trim() || '(none)'} `
778
+ + `entitlementState=${currentState || '(none)'} updatedBy=${updatedBy || '(none)'}`);
779
+ }
711
780
  if (currentState === 'checkout_pending'
712
781
  && currentPlanVersionId
713
782
  && input.planVersionId
714
783
  && currentPlanVersionId !== input.planVersionId) {
784
+ console.warn(`[Taskforce Billing] Lifecycle guard blocked superseded checkout_pending entitlement for user=${input.userId} source=${sourceLabel} `
785
+ + `currentPlanVersionId=${currentPlanVersionId} requestedPlanVersionId=${String(input.planVersionId || '').trim() || '(none)'}`);
715
786
  return {
716
787
  blocked: true,
717
788
  code: 'CHECKOUT_SESSION_SUPERSEDED',
@@ -1330,11 +1401,18 @@ export function registerBillingRoutes(deps) {
1330
1401
  res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
1331
1402
  return;
1332
1403
  }
1333
- res.writeHead(410, { 'Content-Type': 'application/json' });
1334
- res.end(JSON.stringify({
1335
- error: 'Manual Stripe price selection has been removed. Refresh prices from Stripe metadata instead.',
1336
- code: 'BILLING_PRICE_MANUAL_MAPPING_REMOVED'
1337
- }));
1404
+ try {
1405
+ const requestUrl = parseRequestUrl(req);
1406
+ const interval = parseInterval(requestUrl.searchParams.get('interval'));
1407
+ const options = await listStripePriceOptions(interval);
1408
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1409
+ res.end(JSON.stringify({ options }));
1410
+ }
1411
+ catch (error) {
1412
+ const statusCode = resolveErrorStatusCode(error, 400);
1413
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
1414
+ res.end(JSON.stringify({ error: String(error?.message || 'Unable to list Stripe prices'), code: error?.code }));
1415
+ }
1338
1416
  });
1339
1417
  addRoute('PUT', '/api/taskforce/admin/billing/price-mappings', async (req, res) => {
1340
1418
  if (!ensureCloudBilling(req, res))
@@ -1344,11 +1422,138 @@ export function registerBillingRoutes(deps) {
1344
1422
  res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
1345
1423
  return;
1346
1424
  }
1347
- res.writeHead(410, { 'Content-Type': 'application/json' });
1348
- res.end(JSON.stringify({
1349
- error: 'Manual billing price mappings have been removed. Import Stripe prices instead.',
1350
- code: 'BILLING_PRICE_MANUAL_MAPPING_REMOVED'
1351
- }));
1425
+ const body = await parseJsonBody(req);
1426
+ const planVersionId = String(body?.planVersionId || '').trim();
1427
+ const interval = parseInterval(body?.interval);
1428
+ const pricingType = parsePricingType(body?.pricingType) || 'stripe';
1429
+ const stripePriceId = String(body?.stripePriceId || '').trim();
1430
+ const setActive = body?.active === false ? false : true;
1431
+ if (!planVersionId || !interval) {
1432
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1433
+ res.end(JSON.stringify({ error: 'planVersionId and interval are required.', code: 'BILLING_MAPPING_INPUT_REQUIRED' }));
1434
+ return;
1435
+ }
1436
+ if (pricingType === 'stripe' && !stripePriceId) {
1437
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1438
+ res.end(JSON.stringify({ error: 'stripePriceId is required for Stripe pricing.', code: 'BILLING_MAPPING_STRIPE_PRICE_REQUIRED' }));
1439
+ return;
1440
+ }
1441
+ const db = core.getDatabaseAdapter();
1442
+ const tenantId = core.getTenantId();
1443
+ const versionExists = db.prepare(`
1444
+ SELECT plan_version_id
1445
+ FROM plan_versions
1446
+ WHERE tenant_id = ? AND plan_version_id = ?
1447
+ LIMIT 1
1448
+ `).get(tenantId, planVersionId);
1449
+ if (!versionExists?.plan_version_id) {
1450
+ res.writeHead(404, { 'Content-Type': 'application/json' });
1451
+ res.end(JSON.stringify({ error: 'Plan version not found.', code: 'PLAN_VERSION_NOT_FOUND' }));
1452
+ return;
1453
+ }
1454
+ try {
1455
+ const now = new Date().toISOString();
1456
+ const targetStripePriceId = pricingType === 'free'
1457
+ ? buildFreePricingSyntheticId(planVersionId, interval)
1458
+ : stripePriceId;
1459
+ let unitAmount = 0;
1460
+ let currency = null;
1461
+ if (pricingType === 'stripe') {
1462
+ const stripe = getStripeClient();
1463
+ const stripePrice = await stripe.prices.retrieve(stripePriceId);
1464
+ const stripeInterval = parseInterval(stripePrice.recurring?.interval || '');
1465
+ if (!stripePrice.active) {
1466
+ res.writeHead(422, { 'Content-Type': 'application/json' });
1467
+ res.end(JSON.stringify({ error: 'Stripe price is not active.', code: 'STRIPE_PRICE_INACTIVE' }));
1468
+ return;
1469
+ }
1470
+ if (!stripeInterval) {
1471
+ res.writeHead(422, { 'Content-Type': 'application/json' });
1472
+ res.end(JSON.stringify({ error: 'Stripe price must be recurring month/year.', code: 'STRIPE_PRICE_INTERVAL_UNSUPPORTED' }));
1473
+ return;
1474
+ }
1475
+ if (stripeInterval !== interval) {
1476
+ res.writeHead(422, { 'Content-Type': 'application/json' });
1477
+ res.end(JSON.stringify({ error: `Stripe price interval (${stripeInterval}) does not match requested interval (${interval}).`, code: 'STRIPE_PRICE_INTERVAL_MISMATCH' }));
1478
+ return;
1479
+ }
1480
+ const conflictingMapping = db.prepare(`
1481
+ SELECT plan_version_id
1482
+ FROM billing_price_mappings
1483
+ WHERE tenant_id = ?
1484
+ AND stripe_price_id = ?
1485
+ AND active = 1
1486
+ AND plan_version_id <> ?
1487
+ LIMIT 1
1488
+ `).get(tenantId, stripePriceId, planVersionId);
1489
+ if (conflictingMapping?.plan_version_id) {
1490
+ res.writeHead(409, { 'Content-Type': 'application/json' });
1491
+ res.end(JSON.stringify({
1492
+ error: `Stripe price ${stripePriceId} is already mapped to ${String(conflictingMapping.plan_version_id)}.`,
1493
+ code: 'BILLING_PRICE_ALREADY_ASSIGNED'
1494
+ }));
1495
+ return;
1496
+ }
1497
+ unitAmount = typeof stripePrice.unit_amount === 'number' ? stripePrice.unit_amount : null;
1498
+ currency = stripePrice.currency || null;
1499
+ }
1500
+ const beforeRows = db.prepare(`
1501
+ SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency
1502
+ FROM billing_price_mappings
1503
+ WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ?
1504
+ ORDER BY stripe_price_id ASC
1505
+ `).all(tenantId, planVersionId, interval);
1506
+ if (setActive) {
1507
+ db.prepare(`
1508
+ UPDATE billing_price_mappings
1509
+ SET active = 0, updated_at = ?
1510
+ WHERE tenant_id = ?
1511
+ AND plan_version_id = ?
1512
+ AND billing_interval = ?
1513
+ AND stripe_price_id <> ?
1514
+ AND active = 1
1515
+ `).run(now, tenantId, planVersionId, interval, targetStripePriceId);
1516
+ }
1517
+ db.prepare(`
1518
+ INSERT INTO billing_price_mappings (
1519
+ tenant_id, plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at
1520
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1521
+ ON CONFLICT (tenant_id, plan_version_id, billing_interval, stripe_price_id) DO UPDATE SET
1522
+ pricing_type = excluded.pricing_type,
1523
+ active = excluded.active,
1524
+ unit_amount = excluded.unit_amount,
1525
+ currency = excluded.currency,
1526
+ updated_at = excluded.updated_at
1527
+ `).run(tenantId, planVersionId, interval, targetStripePriceId, pricingType, setActive ? 1 : 0, unitAmount, currency, now, now);
1528
+ const mapping = db.prepare(`
1529
+ SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at
1530
+ FROM billing_price_mappings
1531
+ WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ? AND stripe_price_id = ?
1532
+ LIMIT 1
1533
+ `).get(tenantId, planVersionId, interval, targetStripePriceId);
1534
+ const afterRows = db.prepare(`
1535
+ SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency
1536
+ FROM billing_price_mappings
1537
+ WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ?
1538
+ ORDER BY stripe_price_id ASC
1539
+ `).all(tenantId, planVersionId, interval);
1540
+ auditAdminAction(req, 'billing-price-mapping-upsert', {
1541
+ planVersionId,
1542
+ interval,
1543
+ pricingType,
1544
+ stripePriceId: pricingType === 'free' ? null : stripePriceId,
1545
+ active: setActive,
1546
+ before: beforeRows,
1547
+ after: afterRows
1548
+ });
1549
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1550
+ res.end(JSON.stringify({ mapping }));
1551
+ }
1552
+ catch (error) {
1553
+ const statusCode = resolveErrorStatusCode(error, 400);
1554
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
1555
+ res.end(JSON.stringify({ error: String(error?.message || 'Unable to upsert price mapping'), code: error?.code }));
1556
+ }
1352
1557
  });
1353
1558
  addRoute('POST', '/api/taskforce/admin/billing/price-mappings/deactivate', async (req, res) => {
1354
1559
  if (!ensureCloudBilling(req, res))
@@ -1358,11 +1563,51 @@ export function registerBillingRoutes(deps) {
1358
1563
  res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
1359
1564
  return;
1360
1565
  }
1361
- res.writeHead(410, { 'Content-Type': 'application/json' });
1362
- res.end(JSON.stringify({
1363
- error: 'Manual billing price mappings have been removed. Refresh Stripe prices instead.',
1364
- code: 'BILLING_PRICE_MANUAL_MAPPING_REMOVED'
1365
- }));
1566
+ const body = await parseJsonBody(req);
1567
+ const planVersionId = String(body?.planVersionId || '').trim();
1568
+ const interval = parseInterval(body?.interval);
1569
+ const stripePriceId = String(body?.stripePriceId || '').trim();
1570
+ if (!planVersionId || !interval) {
1571
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1572
+ res.end(JSON.stringify({ error: 'planVersionId and interval are required.', code: 'BILLING_MAPPING_TARGET_REQUIRED' }));
1573
+ return;
1574
+ }
1575
+ const db = core.getDatabaseAdapter();
1576
+ const tenantId = core.getTenantId();
1577
+ const now = new Date().toISOString();
1578
+ const beforeRows = db.prepare(`
1579
+ SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency
1580
+ FROM billing_price_mappings
1581
+ WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ?
1582
+ ORDER BY stripe_price_id ASC
1583
+ `).all(tenantId, planVersionId, interval);
1584
+ const result = stripePriceId
1585
+ ? db.prepare(`
1586
+ UPDATE billing_price_mappings
1587
+ SET active = 0, updated_at = ?
1588
+ WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ? AND stripe_price_id = ?
1589
+ `).run(now, tenantId, planVersionId, interval, stripePriceId)
1590
+ : db.prepare(`
1591
+ UPDATE billing_price_mappings
1592
+ SET active = 0, updated_at = ?
1593
+ WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ?
1594
+ `).run(now, tenantId, planVersionId, interval);
1595
+ const afterRows = db.prepare(`
1596
+ SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency
1597
+ FROM billing_price_mappings
1598
+ WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ?
1599
+ ORDER BY stripe_price_id ASC
1600
+ `).all(tenantId, planVersionId, interval);
1601
+ auditAdminAction(req, 'billing-price-mapping-deactivate', {
1602
+ planVersionId,
1603
+ interval,
1604
+ stripePriceId: stripePriceId || null,
1605
+ changedRows: Number(result?.changes || 0),
1606
+ before: beforeRows,
1607
+ after: afterRows
1608
+ });
1609
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1610
+ res.end(JSON.stringify({ success: true, changedRows: Number(result?.changes || 0) }));
1366
1611
  });
1367
1612
  addRoute('GET', '/api/taskforce/admin/billing/webhook-failures', async (req, res) => {
1368
1613
  if (!ensureCloudBilling(req, res))
@@ -1741,10 +1986,17 @@ export function registerBillingRoutes(deps) {
1741
1986
  planVersionId,
1742
1987
  interval
1743
1988
  });
1989
+ const successParams = new URLSearchParams({
1990
+ screen: 'plans',
1991
+ checkout: 'success',
1992
+ planId: String(planId || ''),
1993
+ planVersionId,
1994
+ interval
1995
+ });
1744
1996
  const session = await stripe.checkout.sessions.create({
1745
1997
  mode: 'subscription',
1746
1998
  line_items: [{ price: priceId, quantity: 1 }],
1747
- success_url: `${baseUrl}/?screen=plans&checkout=success&session_id={CHECKOUT_SESSION_ID}`,
1999
+ success_url: `${baseUrl}/?${successParams.toString()}&session_id={CHECKOUT_SESSION_ID}`,
1748
2000
  cancel_url: `${baseUrl}/?${cancelParams.toString()}`,
1749
2001
  ...(resolvedCustomerId
1750
2002
  ? { customer: resolvedCustomerId }
@@ -1863,7 +2115,10 @@ export function registerBillingRoutes(deps) {
1863
2115
  }
1864
2116
  if (resolvedUserId !== expectedUserId) {
1865
2117
  res.writeHead(403, { 'Content-Type': 'application/json' });
1866
- res.end(JSON.stringify({ error: 'Checkout session does not belong to the current account.' }));
2118
+ res.end(JSON.stringify({
2119
+ error: 'Checkout session does not belong to the current account.',
2120
+ code: 'CHECKOUT_SESSION_ACCOUNT_MISMATCH'
2121
+ }));
1867
2122
  return;
1868
2123
  }
1869
2124
  let resolvedPlanVersionId = null;
@@ -1883,7 +2138,8 @@ export function registerBillingRoutes(deps) {
1883
2138
  userId: resolvedUserId,
1884
2139
  planVersionId: resolvedPlanVersionId,
1885
2140
  sessionId,
1886
- subscriptionId
2141
+ subscriptionId,
2142
+ source: 'checkout-confirm'
1887
2143
  });
1888
2144
  if (lifecycleGuard.blocked) {
1889
2145
  res.writeHead(409, { 'Content-Type': 'application/json' });
@@ -1902,12 +2158,14 @@ export function registerBillingRoutes(deps) {
1902
2158
  }));
1903
2159
  return;
1904
2160
  }
1905
- core.createUserBillingRecord(resolvedUserId);
1906
- upsertBillingMirror(core, {
1907
- userId: resolvedUserId,
1908
- stripeCustomerId: customerId || null,
1909
- stripeSubscriptionId: subscriptionId || null
1910
- });
2161
+ if (!resolvedPlanVersionId) {
2162
+ res.writeHead(409, { 'Content-Type': 'application/json' });
2163
+ res.end(JSON.stringify({
2164
+ error: 'Checkout completed, but the purchased plan could not be resolved yet.',
2165
+ code: 'CHECKOUT_PLAN_VERSION_UNRESOLVED'
2166
+ }));
2167
+ return;
2168
+ }
1911
2169
  const snapshot = applySubscriptionSnapshot(core, {
1912
2170
  userId: resolvedUserId,
1913
2171
  subscription,
@@ -2165,7 +2423,8 @@ export function registerBillingRoutes(deps) {
2165
2423
  const lifecycleGuard = resolveCheckoutLifecycleGuard(core, {
2166
2424
  userId,
2167
2425
  planVersionId,
2168
- subscriptionId: String(subscription.id || '').trim()
2426
+ subscriptionId: String(subscription.id || '').trim(),
2427
+ source: 'stripe-webhook'
2169
2428
  });
2170
2429
  if (lifecycleGuard.blocked) {
2171
2430
  recordWebhookFailure(core, {
@@ -2210,7 +2469,8 @@ export function registerBillingRoutes(deps) {
2210
2469
  const lifecycleGuard = resolveCheckoutLifecycleGuard(core, {
2211
2470
  userId,
2212
2471
  planVersionId,
2213
- subscriptionId
2472
+ subscriptionId,
2473
+ source: 'stripe-webhook'
2214
2474
  });
2215
2475
  if (lifecycleGuard.blocked) {
2216
2476
  recordWebhookFailure(core, {
@@ -2292,6 +2552,15 @@ export function registerBillingRoutes(deps) {
2292
2552
  try {
2293
2553
  const db = core.getDatabaseAdapter();
2294
2554
  const tenantId = core.getTenantId();
2555
+ const environment = requestPricingEnvironment(req);
2556
+ const cacheKey = `${tenantId}:${environment}`;
2557
+ const cacheVersion = buildPublicPricingCacheVersion(core);
2558
+ const cached = publicPricingCache.get(cacheKey);
2559
+ if (cached && cached.version === cacheVersion && cached.expiresAt > Date.now()) {
2560
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2561
+ res.end(JSON.stringify(cached.payload));
2562
+ return;
2563
+ }
2295
2564
  // Get all active plans
2296
2565
  const activePlans = core.listPlanCatalog().filter(p => p.status === 'active');
2297
2566
  // Get all active price mappings with their amounts
@@ -2300,6 +2569,17 @@ export function registerBillingRoutes(deps) {
2300
2569
  FROM billing_price_mappings
2301
2570
  WHERE tenant_id = ? AND active = 1
2302
2571
  `).all(tenantId);
2572
+ const pricingByVersionAndInterval = new Map();
2573
+ for (const price of activePrices) {
2574
+ const planVersionId = String(price.plan_version_id || '').trim();
2575
+ const interval = parseInterval(price.billing_interval);
2576
+ if (!planVersionId || !interval)
2577
+ continue;
2578
+ pricingByVersionAndInterval.set(`${planVersionId}:${interval}`, price);
2579
+ }
2580
+ const featureCatalog = core.listPlanFeatureCatalog();
2581
+ const featureCatalogByKey = new Map(featureCatalog.map((feature) => [feature.featureKey, feature]));
2582
+ const featureCatalogIndexByKey = new Map(featureCatalog.map((feature, index) => [feature.featureKey, index]));
2303
2583
  const plansPayload = activePlans.map((plan) => {
2304
2584
  const versions = core.listPlanVersions(plan.planId);
2305
2585
  // Prefer default, else fallback to highest version number
@@ -2307,11 +2587,12 @@ export function registerBillingRoutes(deps) {
2307
2587
  if (!defaultVersion) {
2308
2588
  return null; // Skip if no valid version exists (edge case)
2309
2589
  }
2310
- const monthPrice = activePrices.find(p => p.plan_version_id === defaultVersion.planVersionId && p.billing_interval === 'month');
2311
- const yearPrice = activePrices.find(p => p.plan_version_id === defaultVersion.planVersionId && p.billing_interval === 'year');
2590
+ const monthPrice = pricingByVersionAndInterval.get(`${defaultVersion.planVersionId}:month`);
2591
+ const yearPrice = pricingByVersionAndInterval.get(`${defaultVersion.planVersionId}:year`);
2312
2592
  return {
2313
2593
  planId: plan.planId,
2314
2594
  displayName: plan.displayName,
2595
+ description: plan.description ?? null,
2315
2596
  status: plan.status,
2316
2597
  defaultVersion: {
2317
2598
  planVersionId: defaultVersion.planVersionId,
@@ -2320,9 +2601,48 @@ export function registerBillingRoutes(deps) {
2320
2601
  trialDays: defaultVersion.trialDays,
2321
2602
  metadata: defaultVersion.metadata
2322
2603
  },
2323
- features: Object.values(defaultVersion.features || {}).filter((feature) => {
2604
+ features: Object.values(defaultVersion.features || {})
2605
+ .reduce((acc, feature) => {
2324
2606
  const access = String(feature?.access || '').trim().toLowerCase();
2325
- return access !== 'disabled';
2607
+ if (access === 'disabled') {
2608
+ return acc;
2609
+ }
2610
+ const featureKey = String(feature?.featureKey || '').trim();
2611
+ if (!featureKey || INTERNAL_PUBLIC_PRICING_FEATURE_KEYS.has(featureKey)) {
2612
+ return acc;
2613
+ }
2614
+ const catalogEntry = featureCatalogByKey.get(featureKey);
2615
+ acc.push({
2616
+ featureKey,
2617
+ access,
2618
+ config: feature?.config && typeof feature.config === 'object'
2619
+ ? feature.config
2620
+ : {},
2621
+ label: catalogEntry?.label ?? null,
2622
+ description: catalogEntry?.description ?? null,
2623
+ publicLabel: catalogEntry?.publicLabel ?? null,
2624
+ publicDescription: catalogEntry?.publicDescription ?? null
2625
+ });
2626
+ return acc;
2627
+ }, [])
2628
+ .sort((a, b) => {
2629
+ const orderA = featureCatalogByKey.get(a.featureKey)?.publicDisplayOrder;
2630
+ const orderB = featureCatalogByKey.get(b.featureKey)?.publicDisplayOrder;
2631
+ const normalizedOrderA = typeof orderA === 'number' && Number.isFinite(orderA)
2632
+ ? orderA
2633
+ : Number.MAX_SAFE_INTEGER;
2634
+ const normalizedOrderB = typeof orderB === 'number' && Number.isFinite(orderB)
2635
+ ? orderB
2636
+ : Number.MAX_SAFE_INTEGER;
2637
+ if (normalizedOrderA !== normalizedOrderB) {
2638
+ return normalizedOrderA - normalizedOrderB;
2639
+ }
2640
+ const catalogIndexA = featureCatalogIndexByKey.get(a.featureKey) ?? Number.MAX_SAFE_INTEGER;
2641
+ const catalogIndexB = featureCatalogIndexByKey.get(b.featureKey) ?? Number.MAX_SAFE_INTEGER;
2642
+ if (catalogIndexA !== catalogIndexB) {
2643
+ return catalogIndexA - catalogIndexB;
2644
+ }
2645
+ return a.featureKey.localeCompare(b.featureKey);
2326
2646
  }),
2327
2647
  pricing: {
2328
2648
  month: monthPrice ? {
@@ -2342,15 +2662,21 @@ export function registerBillingRoutes(deps) {
2342
2662
  }
2343
2663
  };
2344
2664
  }).filter(Boolean);
2345
- res.writeHead(200, { 'Content-Type': 'application/json' });
2346
- res.end(JSON.stringify({
2665
+ const payload = {
2347
2666
  asOf: new Date().toISOString(),
2348
2667
  pricingSource: {
2349
- environment: requestPricingEnvironment(req),
2668
+ environment,
2350
2669
  runtimeMode: 'cloud'
2351
2670
  },
2352
2671
  plans: plansPayload
2353
- }));
2672
+ };
2673
+ publicPricingCache.set(cacheKey, {
2674
+ payload,
2675
+ version: cacheVersion,
2676
+ expiresAt: Date.now() + PUBLIC_PRICING_CACHE_TTL_MS
2677
+ });
2678
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2679
+ res.end(JSON.stringify(payload));
2354
2680
  }
2355
2681
  catch (error) {
2356
2682
  const statusCode = resolveErrorStatusCode(error, 500);