@taskforcehq/taskforce 0.3.170 → 0.3.301

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 (698) hide show
  1. package/README.md +71 -21
  2. package/dist/Taskforce.module.css +3394 -5981
  3. package/dist/TaskforceCore.CategoryFallback.test.js +5 -2
  4. package/dist/TaskforceCore.filter-persistence.test.js +402 -0
  5. package/dist/TaskforceCore.js +388 -71
  6. package/dist/TaskforceCore.routing.test.js +311 -0
  7. package/dist/TaskforceCore.test.js +3344 -615
  8. package/dist/TaskforceWidget.js +3 -2
  9. package/dist/__tests__/localizationCatalogs.test.js +48 -31
  10. package/dist/__tests__/workflowSimulation.test.js +2 -2
  11. package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.d.ts +60 -0
  12. package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.js +324 -0
  13. package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.test.js +278 -0
  14. package/dist/annotatedAttachments/payload.d.ts +2 -0
  15. package/dist/annotatedAttachments/payload.js +54 -0
  16. package/dist/annotatedAttachments/payload.test.js +67 -0
  17. package/dist/annotatedAttachments/service.d.ts +49 -0
  18. package/dist/annotatedAttachments/service.js +121 -0
  19. package/dist/annotatedAttachments/types.d.ts +86 -0
  20. package/dist/cli.js +74 -25
  21. package/dist/compat/deploymentCompat.d.ts +12 -0
  22. package/dist/compat/deploymentCompat.js +43 -0
  23. package/dist/compat/workspaceSyncCompat.d.ts +6 -0
  24. package/dist/compat/workspaceSyncCompat.js +79 -0
  25. package/dist/compat/workspaceSyncCompat.test.js +87 -0
  26. package/dist/components/features/AgentsModule.d.ts +5 -0
  27. package/dist/components/features/AgentsModule.js +131 -0
  28. package/dist/components/features/AgentsModule.test.js +102 -0
  29. package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +24 -0
  30. package/dist/components/features/AnnotatedAttachmentWorkspace.js +1942 -0
  31. package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +2831 -0
  32. package/dist/components/features/DocumentGenerateModal.js +8 -16
  33. package/dist/components/features/DocumentIndex.d.ts +3 -4
  34. package/dist/components/features/DocumentIndex.js +22 -10
  35. package/dist/components/features/DocumentViewer.d.ts +6 -2
  36. package/dist/components/features/DocumentViewer.js +1000 -18
  37. package/dist/components/features/DocumentWorkspace.d.ts +40 -2
  38. package/dist/components/features/DocumentWorkspace.js +96 -16
  39. package/dist/components/features/DocumentWorkspace.test.js +1202 -0
  40. package/dist/components/features/InitiativesModule.d.ts +22 -0
  41. package/dist/components/features/InitiativesModule.js +33 -0
  42. package/dist/components/features/TaskSettings.d.ts +41 -32
  43. package/dist/components/features/TaskSettings.js +798 -313
  44. package/dist/components/features/TaskSettings.test.js +1221 -47
  45. package/dist/components/features/WorkflowsModule.d.ts +27 -0
  46. package/dist/components/features/WorkflowsModule.js +21 -0
  47. package/dist/components/features/documentWorkspaceFilters.d.ts +10 -0
  48. package/dist/components/features/documentWorkspaceFilters.js +11 -0
  49. package/dist/components/features/settings/CategoryManager.d.ts +2 -0
  50. package/dist/components/features/settings/CategoryManager.js +15 -8
  51. package/dist/components/features/settings/PriorityManager.d.ts +18 -0
  52. package/dist/components/features/settings/PriorityManager.js +51 -0
  53. package/dist/components/features/settings/TaxonomyEditor.d.ts +2 -1
  54. package/dist/components/features/settings/TaxonomyEditor.js +41 -18
  55. package/dist/components/features/settings/TaxonomyLibraryManager.d.ts +26 -0
  56. package/dist/components/features/settings/TaxonomyLibraryManager.js +301 -0
  57. package/dist/components/features/settings/TaxonomyLibraryManager.test.js +141 -0
  58. package/dist/components/features/settings/TaxonomyManager.d.ts +35 -12
  59. package/dist/components/features/settings/TaxonomyManager.js +59 -27
  60. package/dist/components/features/settings/TypeManager.d.ts +11 -0
  61. package/dist/components/features/settings/TypeManager.js +48 -11
  62. package/dist/components/features/settings/{WorkflowEditorPrototype.d.ts → WorkflowEditor.d.ts} +2 -2
  63. package/dist/components/features/settings/{WorkflowEditorPrototype.js → WorkflowEditor.js} +3 -3
  64. package/dist/components/task/TaskActionHeader.d.ts +12 -2
  65. package/dist/components/task/TaskActionHeader.js +64 -8
  66. package/dist/components/task/TaskActionHeader.test.js +73 -0
  67. package/dist/components/task/TaskCard.d.ts +14 -16
  68. package/dist/components/task/TaskCard.js +144 -174
  69. package/dist/components/task/TaskCard.test.js +416 -64
  70. package/dist/components/task/TaskContextUpload.d.ts +6 -5
  71. package/dist/components/task/TaskContextUpload.js +199 -64
  72. package/dist/components/task/TaskContextUpload.test.js +331 -14
  73. package/dist/components/task/TaskForm.d.ts +23 -29
  74. package/dist/components/task/TaskForm.js +348 -169
  75. package/dist/components/task/TaskForm.test.js +419 -23
  76. package/dist/components/task/TaskKanban.d.ts +20 -21
  77. package/dist/components/task/TaskKanban.js +176 -316
  78. package/dist/components/task/TaskKanban.planning-drop.test.js +93 -0
  79. package/dist/components/task/TaskList.d.ts +22 -11
  80. package/dist/components/task/TaskList.js +98 -12
  81. package/dist/components/task/TaskList.test.js +206 -7
  82. package/dist/components/task/TaskReferenceBadge.d.ts +12 -0
  83. package/dist/components/task/TaskReferenceBadge.js +5 -0
  84. package/dist/components/task/TaskStatusActions.d.ts +12 -0
  85. package/dist/components/task/TaskStatusActions.js +18 -0
  86. package/dist/components/task/TaskStatusActions.test.js +45 -0
  87. package/dist/components/ui/ImageReferenceBadge.d.ts +12 -0
  88. package/dist/components/ui/ImageReferenceBadge.js +5 -0
  89. package/dist/components/ui/LevelSelector.d.ts +2 -1
  90. package/dist/components/ui/LevelSelector.js +2 -2
  91. package/dist/components/ui/Markdown.d.ts +1 -2
  92. package/dist/components/ui/Markdown.js +2 -13
  93. package/dist/components/ui/Modal.d.ts +3 -2
  94. package/dist/components/ui/Modal.js +4 -3
  95. package/dist/components/ui/MultiSelectFilter.d.ts +3 -1
  96. package/dist/components/ui/MultiSelectFilter.js +2 -2
  97. package/dist/components/ui/ReferenceBadgeButton.d.ts +13 -0
  98. package/dist/components/ui/ReferenceBadgeButton.js +6 -0
  99. package/dist/components/ui/TaxonomyDropdown.d.ts +7 -1
  100. package/dist/components/ui/TaxonomyDropdown.js +58 -23
  101. package/dist/components/ui/TopNoticeLayer.d.ts +7 -0
  102. package/dist/components/ui/TopNoticeLayer.js +14 -0
  103. package/dist/components/ui/TopNoticeLayer.test.js +25 -0
  104. package/dist/components/views/AppShellHeader.d.ts +13 -0
  105. package/dist/components/views/AppShellHeader.js +19 -0
  106. package/dist/components/views/AppShellHeader.test.js +21 -0
  107. package/dist/components/views/PlanComparisonPage.d.ts +73 -0
  108. package/dist/components/views/PlanComparisonPage.js +196 -0
  109. package/dist/components/views/PlanComparisonPage.test.js +251 -0
  110. package/dist/components/views/PlansPage.d.ts +14 -0
  111. package/dist/components/views/PlansPage.js +355 -0
  112. package/dist/components/views/StandaloneLayout.d.ts +2 -2
  113. package/dist/components/views/StandaloneLayout.js +2227 -1188
  114. package/dist/components/views/WidgetView.d.ts +2 -2
  115. package/dist/components/views/WidgetView.js +149 -43
  116. package/dist/components/views/chrome/WorkspaceChromeSections.d.ts +57 -0
  117. package/dist/components/views/chrome/WorkspaceChromeSections.js +38 -0
  118. package/dist/components/views/chrome/WorkspaceChromeSections.test.js +54 -0
  119. package/dist/components/views/chrome/workspaceChrome.d.ts +31 -0
  120. package/dist/components/views/chrome/workspaceChrome.js +108 -0
  121. package/dist/components/views/chrome/workspaceChrome.test.js +52 -0
  122. package/dist/components/views/panels/DocumentFilterToolbar.d.ts +10 -0
  123. package/dist/components/views/panels/DocumentFilterToolbar.js +7 -0
  124. package/dist/components/views/panels/DocumentFilterToolbar.test.js +19 -0
  125. package/dist/components/views/panels/FilterToolbar.d.ts +68 -0
  126. package/dist/components/views/panels/FilterToolbar.js +21 -0
  127. package/dist/components/views/panels/FilterToolbar.test.d.ts +1 -0
  128. package/dist/components/views/panels/FilterToolbar.test.js +45 -0
  129. package/dist/components/views/panels/FilterToolbarControls.d.ts +47 -0
  130. package/dist/components/views/panels/FilterToolbarControls.js +61 -0
  131. package/dist/components/views/panels/PlanningDrawer.d.ts +115 -0
  132. package/dist/components/views/panels/PlanningDrawer.js +220 -0
  133. package/dist/components/views/panels/ScheduleSidebar.d.ts +52 -0
  134. package/dist/components/views/panels/ScheduleSidebar.js +127 -0
  135. package/dist/components/views/panels/scheduleUtils.d.ts +14 -0
  136. package/dist/components/views/panels/scheduleUtils.js +79 -0
  137. package/dist/components/views/teamManagementAccess.d.ts +6 -0
  138. package/dist/components/views/teamManagementAccess.js +18 -0
  139. package/dist/components/views/teamManagementAccess.test.d.ts +1 -0
  140. package/dist/components/views/teamManagementAccess.test.js +44 -0
  141. package/dist/config/clientDeployment.d.ts +2 -0
  142. package/dist/config/clientDeployment.js +13 -0
  143. package/dist/config/cloudEnvironment.d.ts +15 -0
  144. package/dist/config/cloudEnvironment.js +60 -0
  145. package/dist/config/deployment.d.ts +20 -0
  146. package/dist/config/deployment.js +67 -0
  147. package/dist/config/deployment.test.d.ts +1 -0
  148. package/dist/config/deployment.test.js +88 -0
  149. package/dist/config/envSchema.js +36 -8
  150. package/dist/config/envSchema.test.js +28 -0
  151. package/dist/core/AiProfileService.d.ts +66 -0
  152. package/dist/core/AiProfileService.js +320 -0
  153. package/dist/core/AiProfileService.test.d.ts +1 -0
  154. package/dist/core/AiProfileService.test.js +164 -0
  155. package/dist/core/AiProfiles.test.d.ts +1 -0
  156. package/dist/core/AiProfiles.test.js +449 -0
  157. package/dist/core/AttachmentLinkService.d.ts +15 -0
  158. package/dist/core/AttachmentLinkService.js +157 -0
  159. package/dist/core/AuthTokenService.d.ts +44 -0
  160. package/dist/core/AuthTokenService.js +207 -0
  161. package/dist/core/CreatedBy.test.js +134 -6
  162. package/dist/core/DeletedTaskLifecycleService.d.ts +33 -0
  163. package/dist/core/DeletedTaskLifecycleService.js +158 -0
  164. package/dist/core/DeletedTaskTrash.test.d.ts +1 -0
  165. package/dist/core/DeletedTaskTrash.test.js +104 -0
  166. package/dist/core/EntitlementsPolicy.test.js +78 -5
  167. package/dist/core/GlobalSettingsService.d.ts +16 -0
  168. package/dist/core/GlobalSettingsService.js +370 -0
  169. package/dist/core/JsonBackupRuntime.test.d.ts +1 -0
  170. package/dist/core/JsonBackupRuntime.test.js +155 -0
  171. package/dist/core/McpTokenService.d.ts +118 -0
  172. package/dist/core/McpTokenService.js +921 -0
  173. package/dist/core/PlanEntitlementService.d.ts +201 -0
  174. package/dist/core/PlanEntitlementService.js +1442 -0
  175. package/dist/core/PlanVersionPolicy.test.d.ts +1 -0
  176. package/dist/core/PlanVersionPolicy.test.js +178 -0
  177. package/dist/core/PlanningEntities.test.d.ts +1 -0
  178. package/dist/core/PlanningEntities.test.js +95 -0
  179. package/dist/core/ScheduleFields.test.js +15 -19
  180. package/dist/core/SignupMonetizationSettings.test.d.ts +1 -0
  181. package/dist/core/SignupMonetizationSettings.test.js +159 -0
  182. package/dist/core/Stats.test.d.ts +1 -0
  183. package/dist/core/Stats.test.js +36 -0
  184. package/dist/core/SyncReconciliationService.d.ts +93 -0
  185. package/dist/core/SyncReconciliationService.js +406 -0
  186. package/dist/core/SystemAdmin.test.js +578 -13
  187. package/dist/core/TaskActivityService.d.ts +21 -0
  188. package/dist/core/TaskActivityService.js +130 -0
  189. package/dist/core/TaskAttachmentsCanonical.test.d.ts +1 -0
  190. package/dist/core/TaskAttachmentsCanonical.test.js +306 -0
  191. package/dist/core/TaskAuditTimeline.test.d.ts +1 -0
  192. package/dist/core/TaskAuditTimeline.test.js +132 -0
  193. package/dist/core/TaskTaxonomyValidation.test.js +338 -15
  194. package/dist/core/Taskforce.d.ts +930 -183
  195. package/dist/core/Taskforce.ids.test.js +252 -0
  196. package/dist/core/Taskforce.js +3848 -2532
  197. package/dist/core/Taskforce.mcpAuth.test.d.ts +1 -0
  198. package/dist/core/Taskforce.mcpAuth.test.js +343 -0
  199. package/dist/core/Taskforce.realtimeMutation.test.js +57 -2
  200. package/dist/core/TaxonomySettingsService.d.ts +18 -0
  201. package/dist/core/TaxonomySettingsService.js +162 -0
  202. package/dist/core/WorkspaceDelete.test.js +32 -13
  203. package/dist/core/WorkspaceLifecycleService.d.ts +11 -0
  204. package/dist/core/WorkspaceLifecycleService.js +88 -0
  205. package/dist/core/WorkspacePermissions.test.js +95 -0
  206. package/dist/core/WorkspacePlanMode.test.js +114 -1
  207. package/dist/core/WorkspacePreferenceService.d.ts +41 -0
  208. package/dist/core/WorkspacePreferenceService.js +246 -0
  209. package/dist/core/WorkspaceSyncSettings.test.d.ts +1 -0
  210. package/dist/core/WorkspaceSyncSettings.test.js +65 -0
  211. package/dist/core/__tests__/TaskAttachmentsCanonical.test.d.ts +1 -0
  212. package/dist/core/__tests__/TaskAttachmentsCanonical.test.js +212 -0
  213. package/dist/core/shared.d.ts +36 -0
  214. package/dist/core/shared.js +128 -0
  215. package/dist/core/types.d.ts +614 -0
  216. package/dist/core/types.js +27 -0
  217. package/dist/documentReviews/DocumentReviewCommentStore.d.ts +56 -0
  218. package/dist/documentReviews/DocumentReviewCommentStore.js +230 -0
  219. package/dist/documentReviews/DocumentReviewSessionStore.d.ts +52 -0
  220. package/dist/documentReviews/DocumentReviewSessionStore.js +193 -0
  221. package/dist/documentReviews/DocumentReviewSessionStore.test.d.ts +1 -0
  222. package/dist/documentReviews/DocumentReviewSessionStore.test.js +127 -0
  223. package/dist/documentReviews/service.d.ts +68 -0
  224. package/dist/documentReviews/service.js +210 -0
  225. package/dist/documentReviews/types.d.ts +58 -0
  226. package/dist/documentReviews/types.js +1 -0
  227. package/dist/hooks/auth/useAuthGuardPolicy.d.ts +2 -0
  228. package/dist/hooks/auth/useAuthGuardPolicy.js +1 -0
  229. package/dist/hooks/tasks/useCurrentTaskRelations.d.ts +15 -0
  230. package/dist/hooks/tasks/useCurrentTaskRelations.js +37 -0
  231. package/dist/hooks/tasks/useTaskActions.d.ts +3 -0
  232. package/dist/hooks/tasks/useTaskActions.js +2 -0
  233. package/dist/hooks/tasks/useTaskEditorNavigation.d.ts +65 -0
  234. package/dist/hooks/tasks/useTaskEditorNavigation.js +235 -0
  235. package/dist/hooks/tasks/useTaskFilters.d.ts +2 -0
  236. package/dist/hooks/tasks/useTaskFilters.js +2 -0
  237. package/dist/hooks/tasks/useTaskFormActions.d.ts +58 -0
  238. package/dist/hooks/tasks/useTaskFormActions.js +365 -0
  239. package/dist/hooks/tasks/useTaskRelationships.d.ts +16 -0
  240. package/dist/hooks/tasks/useTaskRelationships.js +64 -0
  241. package/dist/hooks/ui/useResourceExport.d.ts +19 -0
  242. package/dist/hooks/ui/useResourceExport.js +84 -0
  243. package/dist/hooks/ui/useSettingsModel.d.ts +203 -0
  244. package/dist/hooks/ui/useSettingsModel.js +125 -0
  245. package/dist/hooks/ui/useSettingsPersistence.d.ts +36 -0
  246. package/dist/hooks/ui/useSettingsPersistence.js +254 -0
  247. package/dist/hooks/ui/useSettingsTaxonomyManagement.d.ts +119 -0
  248. package/dist/hooks/ui/useSettingsTaxonomyManagement.js +536 -0
  249. package/dist/hooks/ui/useUiNotice.d.ts +1 -0
  250. package/dist/hooks/ui/useUiNotice.js +1 -0
  251. package/dist/hooks/ui/useWorkflowTemplates.d.ts +37 -0
  252. package/dist/hooks/ui/useWorkflowTemplates.js +111 -0
  253. package/dist/hooks/ui/useZenMode.d.ts +5 -0
  254. package/dist/hooks/ui/useZenMode.js +32 -0
  255. package/dist/hooks/useFilterSort.d.ts +60 -0
  256. package/dist/hooks/useFilterSort.js +362 -0
  257. package/dist/hooks/useFilterSort.test.d.ts +1 -0
  258. package/dist/hooks/useFilterSort.test.js +333 -0
  259. package/dist/hooks/useRealtimeSync.d.ts +1 -0
  260. package/dist/hooks/useRealtimeSync.js +24 -9
  261. package/dist/hooks/useRealtimeSync.test.js +28 -1
  262. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.d.ts +1 -0
  263. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +34 -0
  264. package/dist/hooks/useSyncOrchestrator.d.ts +65 -103
  265. package/dist/hooks/useSyncOrchestrator.js +1538 -670
  266. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +700 -174
  267. package/dist/hooks/useTaskData.d.ts +35 -0
  268. package/dist/hooks/useTaskData.js +243 -0
  269. package/dist/hooks/useTaskData.test.d.ts +1 -0
  270. package/dist/hooks/useTaskData.test.js +180 -0
  271. package/dist/hooks/useTaskMutations.d.ts +46 -0
  272. package/dist/hooks/useTaskMutations.js +472 -0
  273. package/dist/hooks/useTaskMutations.test.d.ts +1 -0
  274. package/dist/hooks/useTaskMutations.test.js +80 -0
  275. package/dist/hooks/useTaskforce.contract.test.d.ts +1 -0
  276. package/dist/hooks/useTaskforce.contract.test.js +7 -0
  277. package/dist/hooks/useTaskforce.d.ts +260 -295
  278. package/dist/hooks/useTaskforce.js +1954 -2623
  279. package/dist/hooks/useTaskforce.priority-filters.test.js +6 -11
  280. package/dist/hooks/useTaskforce.sync-behavior.test.d.ts +1 -0
  281. package/dist/hooks/useTaskforce.sync-behavior.test.js +1120 -0
  282. package/dist/hooks/useUiNotice.d.ts +16 -0
  283. package/dist/hooks/useUiNotice.js +49 -0
  284. package/dist/hooks/useWorkspaceSyncController.d.ts +23 -46
  285. package/dist/hooks/useWorkspaceSyncController.js +280 -461
  286. package/dist/hooks/useWorkspaceSyncController.test.js +106 -199
  287. package/dist/hooks/workspace/useTaskforceApiRouting.d.ts +8 -0
  288. package/dist/hooks/workspace/useTaskforceApiRouting.js +91 -0
  289. package/dist/hooks/workspace/useTaskforceApiRouting.test.d.ts +1 -0
  290. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +60 -0
  291. package/dist/hooks/workspace/useWorkspaceSyncController.d.ts +1 -0
  292. package/dist/hooks/workspace/useWorkspaceSyncController.js +1 -0
  293. package/dist/hooks/workspace/workspaceLocalState.d.ts +30 -0
  294. package/dist/hooks/workspace/workspaceLocalState.js +101 -0
  295. package/dist/localization/locales/en-US.d.ts +1 -8
  296. package/dist/localization/locales/en-US.js +4 -11
  297. package/dist/localization/locales/es-419.js +2 -9
  298. package/dist/localization/locales/pt-BR.js +2 -9
  299. package/dist/localization/localizationCatalogs.test.d.ts +1 -0
  300. package/dist/localization/localizationCatalogs.test.js +37 -0
  301. package/dist/main.d.ts +1 -0
  302. package/dist/main.js +9 -15
  303. package/dist/mcp/clientIntegrations.d.ts +33 -0
  304. package/dist/mcp/clientIntegrations.js +199 -0
  305. package/dist/mcp/clientIntegrations.test.d.ts +1 -0
  306. package/dist/mcp/clientIntegrations.test.js +110 -0
  307. package/dist/mcp/index.d.ts +2 -0
  308. package/dist/mcp/index.js +2 -0
  309. package/dist/mcp/optionResolution.test.js +6 -2
  310. package/dist/mcp/runtime.d.ts +120 -0
  311. package/dist/mcp/runtime.js +5668 -0
  312. package/dist/mcp/runtime.test.d.ts +1 -0
  313. package/dist/mcp/runtime.test.js +3299 -0
  314. package/dist/mcp/server.js +2 -1793
  315. package/dist/mcp/structuredCommentValidation.test.d.ts +1 -0
  316. package/dist/{__tests__ → mcp}/structuredCommentValidation.test.js +1 -1
  317. package/dist/mcp/structuredDocValidation.js +1 -2
  318. package/dist/mcp/structuredDocValidation.test.d.ts +1 -0
  319. package/dist/{__tests__ → mcp}/structuredDocValidation.test.js +9 -4
  320. package/dist/mcp/toolRegistry.d.ts +570 -0
  321. package/dist/mcp/toolRegistry.js +90 -0
  322. package/dist/mcp/toolRegistry.test.d.ts +1 -0
  323. package/dist/mcp/toolRegistry.test.js +30 -0
  324. package/dist/migrations/billingSchemaParity.test.js +111 -0
  325. package/dist/migrations/taskSchemaMigrations.d.ts +5 -0
  326. package/dist/migrations/taskSchemaMigrations.js +681 -39
  327. package/dist/migrations/taskSchemaMigrations.test.d.ts +1 -0
  328. package/dist/migrations/taskSchemaMigrations.test.js +210 -0
  329. package/dist/plugins/vite.js +13 -7
  330. package/dist/public/apple-touch-icon.png +0 -0
  331. package/dist/public/archive/phoenix/apple-touch-icon.png +0 -0
  332. package/dist/public/archive/phoenix/favicon.ico +0 -0
  333. package/dist/public/archive/phoenix/site.webmanifest +1 -0
  334. package/dist/public/favicon-96x96.png +0 -0
  335. package/dist/public/favicon.ico +0 -0
  336. package/dist/public/favicon.svg +17 -0
  337. package/dist/public/site.webmanifest +21 -1
  338. package/dist/public/web-app-manifest-192x192.png +0 -0
  339. package/dist/public/web-app-manifest-512x512.png +0 -0
  340. package/dist/releaseVersion.test.d.ts +1 -0
  341. package/dist/{__tests__/releaseVersion.test.js → releaseVersion.test.js} +1 -1
  342. package/dist/resources/templates/WORKFLOW_V2_POLICY.md +1 -1
  343. package/dist/resources/templates/workflow-sources/workflowDocs.mjs +62 -52
  344. package/dist/resources/templates/workflows/collaborate.yaml +11 -5
  345. package/dist/resources/templates/workflows/do.yaml +8 -2
  346. package/dist/resources/templates/workflows/evaluate.yaml +13 -7
  347. package/dist/resources/templates/workflows/execute.yaml +40 -23
  348. package/dist/resources/templates/workflows/plan.yaml +15 -8
  349. package/dist/resources/templates/workflows/review.yaml +14 -8
  350. package/dist/runtime/appGateDefinitions.d.ts +32 -0
  351. package/dist/runtime/appGateDefinitions.js +32 -0
  352. package/dist/runtime/appGates.d.ts +29 -0
  353. package/dist/runtime/appGates.js +53 -0
  354. package/dist/runtime/appGates.test.d.ts +1 -0
  355. package/dist/runtime/appGates.test.js +42 -0
  356. package/dist/runtime/migrationFlags.d.ts +1 -0
  357. package/dist/runtime/migrationFlags.js +1 -0
  358. package/dist/server/annotatedAttachmentsRoutes.test.d.ts +1 -0
  359. package/dist/server/annotatedAttachmentsRoutes.test.js +473 -0
  360. package/dist/server/auth.js +14 -2
  361. package/dist/server/auth.test.js +21 -1
  362. package/dist/server/cors.js +11 -5
  363. package/dist/server/cors.test.js +6 -0
  364. package/dist/server/documentReviewRoutes.test.d.ts +1 -0
  365. package/dist/server/documentReviewRoutes.test.js +157 -0
  366. package/dist/server/index.cookieProxy.test.js +46 -1
  367. package/dist/server/index.d.ts +20 -0
  368. package/dist/server/index.js +168 -41
  369. package/dist/server/index.rateLimit.test.js +6 -0
  370. package/dist/server/index.test.js +133 -2
  371. package/dist/server/mockStripe.d.ts +72 -0
  372. package/dist/server/mockStripe.js +357 -0
  373. package/dist/server/realtimeSyncWs.js +4 -0
  374. package/dist/server/routes/admin.d.ts +114 -0
  375. package/dist/server/routes/admin.js +2226 -0
  376. package/dist/server/routes/annotatedAttachments.d.ts +24 -0
  377. package/dist/server/routes/annotatedAttachments.js +235 -0
  378. package/dist/server/routes/auth.d.ts +61 -0
  379. package/dist/server/routes/auth.js +1296 -0
  380. package/dist/server/{routes.billing.d.ts → routes/billing.d.ts} +3 -2
  381. package/dist/server/routes/billing.js +2001 -0
  382. package/dist/server/routes/billing.test.d.ts +1 -0
  383. package/dist/server/routes/billing.test.js +1631 -0
  384. package/dist/server/routes/documentReviews.d.ts +11 -0
  385. package/dist/server/routes/documentReviews.js +191 -0
  386. package/dist/server/routes/documents.d.ts +118 -0
  387. package/dist/server/routes/documents.js +1113 -0
  388. package/dist/server/routes/resources.d.ts +25 -0
  389. package/dist/server/routes/resources.js +225 -0
  390. package/dist/server/routes/shared.d.ts +35 -0
  391. package/dist/server/routes/shared.js +305 -0
  392. package/dist/server/routes/shared.test.d.ts +1 -0
  393. package/dist/server/routes/shared.test.js +32 -0
  394. package/dist/server/{routes.sync.d.ts → routes/sync.d.ts} +5 -5
  395. package/dist/server/routes/sync.integration.test.d.ts +1 -0
  396. package/dist/server/routes/sync.integration.test.js +2003 -0
  397. package/dist/server/routes/sync.js +3143 -0
  398. package/dist/server/routes/tasks.d.ts +72 -0
  399. package/dist/server/routes/tasks.js +1252 -0
  400. package/dist/server/routes/workspaces.d.ts +32 -0
  401. package/dist/server/routes/workspaces.js +307 -0
  402. package/dist/server/routes.d.ts +8 -9
  403. package/dist/server/routes.js +968 -4092
  404. package/dist/server/routes.test.js +9043 -2415
  405. package/dist/server/runtimeMode.d.ts +1 -1
  406. package/dist/server/runtimeMode.js +3 -18
  407. package/dist/server/runtimeMode.test.js +3 -3
  408. package/dist/server/securityHeaders.d.ts +1 -1
  409. package/dist/server/securityHeaders.js +65 -6
  410. package/dist/server/securityHeaders.test.js +40 -2
  411. package/dist/services/templateEngine.test.d.ts +1 -0
  412. package/dist/{__tests__ → services}/templateEngine.test.js +1 -1
  413. package/dist/services/templates.test.d.ts +1 -0
  414. package/dist/{__tests__ → services}/templates.test.js +1 -1
  415. package/dist/services/workflowDocumentFixtures.test.d.ts +1 -0
  416. package/dist/{__tests__ → services}/workflowDocumentFixtures.test.js +4 -4
  417. package/dist/services/workflowExportMatrix.test.d.ts +1 -0
  418. package/dist/{__tests__ → services}/workflowExportMatrix.test.js +1 -1
  419. package/dist/services/workflowExportSnapshots.test.d.ts +1 -0
  420. package/dist/services/workflowGeneration.test.d.ts +1 -0
  421. package/dist/services/workflowSimulation.test.d.ts +1 -0
  422. package/dist/services/workflowSimulation.test.js +113 -0
  423. package/dist/services/workflowV2Policy.test.d.ts +1 -0
  424. package/dist/{__tests__ → services}/workflowV2Policy.test.js +2 -2
  425. package/dist/shared/aiProfileSurfaceType.d.ts +9 -0
  426. package/dist/shared/aiProfileSurfaceType.js +49 -0
  427. package/dist/shared/customTaxonomyLibrary.d.ts +11 -0
  428. package/dist/shared/customTaxonomyLibrary.js +276 -0
  429. package/dist/shared/customTaxonomyLibrary.test.d.ts +1 -0
  430. package/dist/shared/customTaxonomyLibrary.test.js +20 -0
  431. package/dist/shared/taxonomyCatalog.d.ts +19 -0
  432. package/dist/shared/taxonomyCatalog.js +36 -0
  433. package/dist/shared/taxonomyDefaults.d.ts +29 -0
  434. package/dist/shared/taxonomyDefaults.js +26 -0
  435. package/dist/shared/taxonomyLibrary.d.ts +15 -0
  436. package/dist/shared/taxonomyLibrary.js +98 -0
  437. package/dist/shared/taxonomyLibrary.test.d.ts +1 -0
  438. package/dist/shared/taxonomyLibrary.test.js +26 -0
  439. package/dist/shared/taxonomyState.d.ts +36 -0
  440. package/dist/shared/taxonomyState.js +73 -0
  441. package/dist/shared/taxonomyState.test.d.ts +1 -0
  442. package/dist/shared/taxonomyState.test.js +71 -0
  443. package/dist/shared/workflowDefaults.d.ts +9 -0
  444. package/dist/shared/workflowDefaults.js +11 -0
  445. package/dist/storage/documentIntegrity.d.ts +2 -0
  446. package/dist/storage/documentIntegrity.js +34 -17
  447. package/dist/storage/documentPurge.d.ts +2 -1
  448. package/dist/storage/documentPurge.js +35 -8
  449. package/dist/storage/index.d.ts +4 -0
  450. package/dist/storage/index.js +2 -0
  451. package/dist/storage/objectStorage.d.ts +1 -1
  452. package/dist/storage/objectStorageClient.d.ts +6 -4
  453. package/dist/storage/objectStorageClient.js +77 -29
  454. package/dist/storage/objectStorageClient.test.d.ts +1 -0
  455. package/dist/storage/objectStorageClient.test.js +57 -0
  456. package/dist/storage/objectStorageConfig.js +3 -1
  457. package/dist/storage/objectStorageConfig.test.d.ts +1 -0
  458. package/dist/storage/objectStorageConfig.test.js +38 -0
  459. package/dist/storage/postgresAdapter.d.ts +5 -0
  460. package/dist/storage/postgresAdapter.js +2 -2
  461. package/dist/storage/postgresAdapter.test.d.ts +1 -0
  462. package/dist/storage/postgresAdapter.test.js +29 -0
  463. package/dist/storage/taskAssetStore.d.ts +7 -4
  464. package/dist/storage/taskAssetStore.js +65 -60
  465. package/dist/storage/workspaceAssetStore.d.ts +122 -0
  466. package/dist/storage/workspaceAssetStore.js +496 -0
  467. package/dist/storage/workspaceAssetStore.test.d.ts +1 -0
  468. package/dist/storage/workspaceAssetStore.test.js +501 -0
  469. package/dist/sync/cloudSyncApi.d.ts +33 -2
  470. package/dist/sync/cloudSyncApi.js +37 -15
  471. package/dist/sync/cloudSyncApi.test.js +60 -3
  472. package/dist/sync/index.d.ts +2 -0
  473. package/dist/sync/index.js +3 -0
  474. package/dist/sync/syncService.d.ts +25 -0
  475. package/dist/sync/syncService.js +184 -16
  476. package/dist/sync/syncService.test.js +289 -25
  477. package/dist/sync/workspaceRepair.d.ts +114 -0
  478. package/dist/sync/workspaceRepair.js +566 -0
  479. package/dist/sync/workspaceSyncModel.d.ts +230 -2
  480. package/dist/sync/workspaceSyncModel.js +456 -17
  481. package/dist/sync/workspaceSyncModel.test.js +642 -0
  482. package/dist/sync/workspaceSyncState.d.ts +12 -0
  483. package/dist/sync/workspaceSyncState.js +12 -0
  484. package/dist/sync/workspaceSyncSurface.d.ts +33 -0
  485. package/dist/sync/workspaceSyncSurface.js +74 -0
  486. package/dist/types.d.ts +143 -18
  487. package/dist/types.js +9 -24
  488. package/dist/ui/assets/AgentsModule-Bdq3T1Ts.js +1 -0
  489. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +1 -0
  490. package/dist/ui/assets/AnnotatedAttachmentWorkspace-jKUVo-PJ.js +3 -0
  491. package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +1 -0
  492. package/dist/ui/assets/DocumentWorkspace-DuQa-uIC.js +250 -0
  493. package/dist/ui/assets/InitiativesModule-C8Pi2VTk.js +1 -0
  494. package/dist/ui/assets/PlansPage-B6BDECSi.js +1 -0
  495. package/dist/ui/assets/PlansPage-BlVC_lRq.css +1 -0
  496. package/dist/ui/assets/TaskSettings-BwB9NtmJ.css +1 -0
  497. package/dist/ui/assets/TaskSettings-DUJgRTPU.js +8 -0
  498. package/dist/ui/assets/WorkflowsModule-b6TLuHwJ.js +5 -0
  499. package/dist/ui/assets/index-BiiF1cLQ.css +1 -0
  500. package/dist/ui/assets/index-xBWLPOEb.js +6 -0
  501. package/dist/ui/assets/vendor-dnd-DRzYolkg.js +5 -0
  502. package/dist/ui/assets/{vendor-icons-ZFG3SGwo.js → vendor-icons-QZyhEwgT.js} +1 -1
  503. package/dist/ui/assets/{vendor-markdown-CKYkQHfj.js → vendor-markdown-BUxTU7dS.js} +1 -1
  504. package/dist/ui/assets/vendor-react-CKJs5o3c.js +61 -0
  505. package/dist/ui/assets/vendor-router-BbWMxlnO.js +3 -0
  506. package/dist/ui/branding/logos/logo.png +0 -0
  507. package/dist/ui/branding/logos/logo_dark.svg +27 -0
  508. package/dist/ui/branding/logos/logo_favicon.svg +35 -0
  509. package/dist/ui/branding/logos/logo_light.svg +27 -0
  510. package/dist/ui/favicon/apple-touch-icon.png +0 -0
  511. package/dist/ui/favicon/archive/phoenix/apple-touch-icon.png +0 -0
  512. package/dist/ui/favicon/archive/phoenix/favicon.ico +0 -0
  513. package/dist/ui/favicon/archive/phoenix/site.webmanifest +1 -0
  514. package/dist/ui/favicon/favicon-96x96.png +0 -0
  515. package/dist/ui/favicon/favicon.ico +0 -0
  516. package/dist/ui/favicon/favicon.svg +17 -0
  517. package/dist/ui/favicon/site.webmanifest +21 -1
  518. package/dist/ui/favicon/web-app-manifest-192x192.png +0 -0
  519. package/dist/ui/favicon/web-app-manifest-512x512.png +0 -0
  520. package/dist/ui/index.html +10 -7
  521. package/dist/ui/product/screen_01.png +0 -0
  522. package/dist/ui/product/screen_02.png +0 -0
  523. package/dist/ui/product/screen_03.png +0 -0
  524. package/dist/utils/annotatedAttachments.d.ts +20 -0
  525. package/dist/utils/annotatedAttachments.js +74 -0
  526. package/dist/utils/annotatedAttachments.test.d.ts +1 -0
  527. package/dist/utils/annotatedAttachments.test.js +108 -0
  528. package/dist/utils/assignees.d.ts +26 -0
  529. package/dist/utils/assignees.js +132 -0
  530. package/dist/utils/canonicalAssetKind.d.ts +16 -0
  531. package/dist/utils/canonicalAssetKind.js +39 -0
  532. package/dist/utils/canonicalAssetKind.test.d.ts +1 -0
  533. package/dist/utils/canonicalAssetKind.test.js +24 -0
  534. package/dist/utils/coalescedRefresh.d.ts +5 -0
  535. package/dist/utils/coalescedRefresh.js +16 -0
  536. package/dist/utils/coalescedRefresh.test.d.ts +1 -0
  537. package/dist/utils/coalescedRefresh.test.js +30 -0
  538. package/dist/utils/constants.d.ts +16 -3
  539. package/dist/utils/constants.icons.test.js +2 -1
  540. package/dist/utils/constants.js +70 -13
  541. package/dist/utils/contextAssetEvents.d.ts +7 -0
  542. package/dist/utils/contextAssetEvents.js +6 -0
  543. package/dist/utils/contextFiles.d.ts +14 -3
  544. package/dist/utils/contextFiles.js +40 -8
  545. package/dist/utils/documentManagerApi.d.ts +6 -0
  546. package/dist/utils/documentManagerApi.js +19 -0
  547. package/dist/utils/documentNames.d.ts +24 -0
  548. package/dist/utils/documentNames.js +91 -0
  549. package/dist/utils/documentReferences.d.ts +9 -0
  550. package/dist/utils/documentReferences.js +11 -0
  551. package/dist/utils/entityReferences.d.ts +9 -0
  552. package/dist/utils/entityReferences.js +32 -0
  553. package/dist/utils/imageReferences.d.ts +9 -0
  554. package/dist/utils/imageReferences.js +11 -0
  555. package/dist/utils/planParser.d.ts +2 -2
  556. package/dist/utils/planParser.js +11 -11
  557. package/dist/utils/planParser.test.d.ts +1 -0
  558. package/dist/{__tests__ → utils}/planParser.test.js +22 -22
  559. package/dist/utils/planningReferences.d.ts +17 -0
  560. package/dist/utils/planningReferences.js +30 -0
  561. package/dist/utils/planningReferences.test.d.ts +1 -0
  562. package/dist/utils/planningReferences.test.js +17 -0
  563. package/dist/utils/taskActivity.d.ts +9 -0
  564. package/dist/utils/taskActivity.js +147 -0
  565. package/dist/utils/taskActivity.test.d.ts +1 -0
  566. package/dist/utils/taskActivity.test.js +34 -0
  567. package/dist/utils/taskDraftDefaults.d.ts +21 -0
  568. package/dist/utils/taskDraftDefaults.js +40 -0
  569. package/dist/utils/taskDraftDefaults.test.d.ts +1 -0
  570. package/dist/utils/taskDraftDefaults.test.js +34 -0
  571. package/dist/utils/taskNormalization.d.ts +25 -0
  572. package/dist/utils/taskNormalization.js +161 -0
  573. package/dist/utils/taskNormalization.test.d.ts +1 -0
  574. package/dist/utils/taskNormalization.test.js +158 -0
  575. package/dist/utils/taskReferences.d.ts +5 -0
  576. package/dist/utils/taskReferences.js +11 -0
  577. package/dist/utils/taskforceApiClient.d.ts +2 -0
  578. package/dist/utils/taskforceApiClient.js +101 -3
  579. package/dist/utils/taskforceApiClient.test.d.ts +1 -0
  580. package/dist/utils/taskforceApiClient.test.js +44 -0
  581. package/dist/utils/taxonomyFilterOptions.d.ts +2 -0
  582. package/dist/utils/taxonomyFilterOptions.js +17 -0
  583. package/dist/utils/taxonomyFormDefaults.d.ts +3 -0
  584. package/dist/utils/taxonomyFormDefaults.js +28 -0
  585. package/dist/utils/taxonomyFormDefaults.test.d.ts +1 -0
  586. package/dist/utils/taxonomyFormDefaults.test.js +63 -0
  587. package/dist/utils/taxonomyLifecycle.d.ts +3 -0
  588. package/dist/utils/taxonomyLifecycle.js +17 -0
  589. package/dist/utils/taxonomySorting.d.ts +7 -0
  590. package/dist/utils/taxonomySorting.js +74 -0
  591. package/dist/utils/theme.d.ts +15 -0
  592. package/dist/utils/theme.js +23 -0
  593. package/dist/utils/uiNotice.d.ts +7 -0
  594. package/dist/utils/uiNotice.js +6 -0
  595. package/dist/utils/uiNotice.test.d.ts +1 -0
  596. package/dist/utils/uiNotice.test.js +18 -0
  597. package/dist/utils/workspaceIdentity.d.ts +14 -0
  598. package/dist/utils/workspaceIdentity.js +21 -0
  599. package/dist/utils/workspaceIdentity.test.js +30 -1
  600. package/dist/utils/workspaceSyncPresentation.d.ts +20 -0
  601. package/dist/utils/workspaceSyncPresentation.js +21 -0
  602. package/dist/utils/workspaceSyncPresentation.test.d.ts +1 -0
  603. package/dist/utils/workspaceSyncPresentation.test.js +36 -0
  604. package/package.json +30 -23
  605. package/scripts/bootstrap-playwright-auth.mjs +155 -0
  606. package/scripts/check-compat-boundaries.mjs +41 -0
  607. package/scripts/check-env.ts +9 -2
  608. package/scripts/debug-login.mjs +32 -0
  609. package/scripts/deploy-prod.sh +41 -0
  610. package/scripts/deploy-staging.sh +23 -0
  611. package/scripts/migrate-planning-model.ts +233 -0
  612. package/scripts/migrate-task-data-assets.mjs +408 -0
  613. package/scripts/playwright-auth.sh +11 -0
  614. package/scripts/run-dev-proxy.sh +2 -0
  615. package/scripts/update-homebrew-formula.sh +2 -2
  616. package/scripts/with-esbuild.sh +11 -0
  617. package/src/resources/templates/WORKFLOW_V2_POLICY.md +1 -1
  618. package/src/resources/templates/workflow-sources/workflowDocs.mjs +62 -52
  619. package/src/resources/templates/workflows/collaborate.yaml +11 -5
  620. package/src/resources/templates/workflows/do.yaml +8 -2
  621. package/src/resources/templates/workflows/evaluate.yaml +13 -7
  622. package/src/resources/templates/workflows/execute.yaml +40 -23
  623. package/src/resources/templates/workflows/plan.yaml +15 -8
  624. package/src/resources/templates/workflows/review.yaml +14 -8
  625. package/dist/TaskforceScreenshot.d.ts +0 -11
  626. package/dist/TaskforceScreenshot.js +0 -113
  627. package/dist/compat/priorityCompat.d.ts +0 -2
  628. package/dist/compat/priorityCompat.js +0 -24
  629. package/dist/components/features/settings/TaxonomyManager 2.d.ts +0 -52
  630. package/dist/components/features/settings/TaxonomyManager 2.js +0 -75
  631. package/dist/components/task/TaskKanban 2.d.ts +0 -76
  632. package/dist/components/task/TaskKanban 2.js +0 -1004
  633. package/dist/components/task/TaskScreenshotSection.d.ts +0 -15
  634. package/dist/components/task/TaskScreenshotSection.js +0 -22
  635. package/dist/components/views/StandaloneLayout 2.d.ts +0 -8
  636. package/dist/components/views/StandaloneLayout 2.js +0 -1660
  637. package/dist/core/InitiativeTemplates.test.js +0 -50
  638. package/dist/core/ParentLifecycle.test.js +0 -98
  639. package/dist/migrations/clientMigrations.d.ts +0 -9
  640. package/dist/migrations/clientMigrations.js +0 -74
  641. package/dist/migrations/flags.d.ts +0 -6
  642. package/dist/migrations/flags.js +0 -8
  643. package/dist/migrations/taskMigrations.d.ts +0 -71
  644. package/dist/migrations/taskMigrations.js +0 -236
  645. package/dist/migrations/taskMigrations.test.js +0 -34
  646. package/dist/server/routes.billing.js +0 -991
  647. package/dist/server/routes.billing.test.js +0 -387
  648. package/dist/server/routes.sync.integration.test.js +0 -470
  649. package/dist/server/routes.sync.js +0 -1141
  650. package/dist/soak/syncSoakHarness.d.ts +0 -6
  651. package/dist/soak/syncSoakHarness.js +0 -10
  652. package/dist/ui/assets/index-0W5vyCpn.css +0 -1
  653. package/dist/ui/assets/index-MCZuNNfn.js +0 -23
  654. package/dist/ui/assets/logo_white_01-0Vx-iIIv.png +0 -0
  655. package/dist/ui/assets/vendor-dnd-BI1K77C1.js +0 -5
  656. package/dist/ui/assets/vendor-react-DdIDsh7f.js +0 -9
  657. package/dist/ui/assets/vendor-router-BtiNU7Dl.js +0 -3
  658. package/dist/utils/taskHierarchy.d.ts +0 -4
  659. package/dist/utils/taskHierarchy.js +0 -69
  660. package/dist/utils/taskHierarchy.test.js +0 -38
  661. package/dist/utils/taskParenting.d.ts +0 -3
  662. package/dist/utils/taskParenting.js +0 -24
  663. package/dist/utils/taskParenting.test.js +0 -21
  664. package/dist/utils/taskRelations.d.ts +0 -6
  665. package/dist/utils/taskRelations.js +0 -24
  666. package/dist/utils/taskRelations.test.js +0 -34
  667. package/scripts/check-compat-boundaries.js +0 -48
  668. /package/dist/{__tests__/planParser.test.d.ts → TaskforceCore.filter-persistence.test.d.ts} +0 -0
  669. /package/dist/{__tests__/releaseVersion.test.d.ts → TaskforceCore.routing.test.d.ts} +0 -0
  670. /package/dist/{__tests__/structuredCommentValidation.test.d.ts → annotatedAttachments/AnnotatedAttachmentSessionStore.test.d.ts} +0 -0
  671. /package/dist/{__tests__/structuredDocValidation.test.d.ts → annotatedAttachments/payload.test.d.ts} +0 -0
  672. /package/dist/{__tests__/templateEngine.test.d.ts → annotatedAttachments/types.js} +0 -0
  673. /package/dist/{__tests__/templates.test.d.ts → compat/workspaceSyncCompat.test.d.ts} +0 -0
  674. /package/dist/{__tests__/workflowDocumentFixtures.test.d.ts → components/features/AgentsModule.test.d.ts} +0 -0
  675. /package/dist/{__tests__/workflowExportMatrix.test.d.ts → components/features/AnnotatedAttachmentWorkspace.test.d.ts} +0 -0
  676. /package/dist/{__tests__/workflowExportSnapshots.test.d.ts → components/features/DocumentWorkspace.test.d.ts} +0 -0
  677. /package/dist/{__tests__/workflowGeneration.test.d.ts → components/features/settings/TaxonomyLibraryManager.test.d.ts} +0 -0
  678. /package/dist/{__tests__/workflowV2Policy.test.d.ts → components/task/TaskActionHeader.test.d.ts} +0 -0
  679. /package/dist/{core/InitiativeTemplates.test.d.ts → components/task/TaskKanban.planning-drop.test.d.ts} +0 -0
  680. /package/dist/{core/ParentLifecycle.test.d.ts → components/task/TaskStatusActions.test.d.ts} +0 -0
  681. /package/dist/{migrations/taskMigrations.test.d.ts → components/ui/TopNoticeLayer.test.d.ts} +0 -0
  682. /package/dist/{server/routes.billing.test.d.ts → components/views/AppShellHeader.test.d.ts} +0 -0
  683. /package/dist/{server/routes.sync.integration.test.d.ts → components/views/PlanComparisonPage.test.d.ts} +0 -0
  684. /package/dist/{utils/taskHierarchy.test.d.ts → components/views/chrome/WorkspaceChromeSections.test.d.ts} +0 -0
  685. /package/dist/{utils/taskParenting.test.d.ts → components/views/chrome/workspaceChrome.test.d.ts} +0 -0
  686. /package/dist/{utils/taskRelations.test.d.ts → components/views/panels/DocumentFilterToolbar.test.d.ts} +0 -0
  687. /package/dist/public/{android-chrome-192x192.png → archive/phoenix/android-chrome-192x192.png} +0 -0
  688. /package/dist/public/{android-chrome-512x512.png → archive/phoenix/android-chrome-512x512.png} +0 -0
  689. /package/dist/public/{favicon-16x16.png → archive/phoenix/favicon-16x16.png} +0 -0
  690. /package/dist/public/{favicon-32x32.png → archive/phoenix/favicon-32x32.png} +0 -0
  691. /package/dist/public/{favicon.png → archive/phoenix/favicon.png} +0 -0
  692. /package/dist/{__tests__ → services}/workflowExportSnapshots.test.js +0 -0
  693. /package/dist/{__tests__ → services}/workflowGeneration.test.js +0 -0
  694. /package/dist/ui/favicon/{android-chrome-192x192.png → archive/phoenix/android-chrome-192x192.png} +0 -0
  695. /package/dist/ui/favicon/{android-chrome-512x512.png → archive/phoenix/android-chrome-512x512.png} +0 -0
  696. /package/dist/ui/favicon/{favicon-16x16.png → archive/phoenix/favicon-16x16.png} +0 -0
  697. /package/dist/ui/favicon/{favicon-32x32.png → archive/phoenix/favicon-32x32.png} +0 -0
  698. /package/dist/ui/favicon/{favicon.png → archive/phoenix/favicon.png} +0 -0
@@ -0,0 +1,2001 @@
1
+ import Stripe from 'stripe';
2
+ import { parseJsonBody } from '../routes.js';
3
+ import { resolveTaskforceCloudEnvironmentFromHostname } from '../../config/cloudEnvironment.js';
4
+ import { buildMockStripeHostedCheckoutUrl, completeMockStripeCheckoutSession, createMockStripeClient, isMockStripeClient, isMockStripeEnabled, readMockStripeCheckoutSession, renderMockStripeCheckoutHtml } from '../mockStripe.js';
5
+ const BILLING_CONFIG_KEY_PLAN = 'billing.config.v1';
6
+ const PRICE_METADATA_PLAN_VERSION = 'taskforce_plan_version_id';
7
+ const BILLING_DIAGNOSTIC_RETENTION_DAYS = 30;
8
+ const BILLING_DIAGNOSTIC_MAX_PAYLOAD_BYTES = 8 * 1024;
9
+ const TASKFORCE_PROXY_ORIGIN_HOST_HEADER = 'x-taskforce-origin-host';
10
+ const TASKFORCE_PROXY_ORIGIN_PROTO_HEADER = 'x-taskforce-origin-proto';
11
+ const TASKFORCE_PROXY_ORIGIN_BASE_URL_HEADER = 'x-taskforce-origin-base-url';
12
+ const TASKFORCE_PROXY_ORIGIN_RUNTIME_HEADER = 'x-taskforce-origin-runtime';
13
+ function parsePricingType(raw) {
14
+ const value = String(raw || '').trim().toLowerCase();
15
+ if (value === 'stripe' || value === 'free')
16
+ return value;
17
+ return null;
18
+ }
19
+ function buildFreePricingSyntheticId(planVersionId, interval) {
20
+ return `free:${planVersionId}:${interval}`;
21
+ }
22
+ function isoFromUnixSeconds(raw) {
23
+ const value = Number(raw);
24
+ if (!Number.isFinite(value) || value <= 0)
25
+ return null;
26
+ return new Date(Math.floor(value) * 1000).toISOString();
27
+ }
28
+ function parseInterval(raw) {
29
+ const value = String(raw || '').trim().toLowerCase();
30
+ if (value === 'month' || value === 'year')
31
+ return value;
32
+ return null;
33
+ }
34
+ function normalizeEmail(raw) {
35
+ return String(raw || '').trim().toLowerCase();
36
+ }
37
+ function parseRequestUrl(req) {
38
+ return new URL(req.url || '/', 'https://taskforce.local');
39
+ }
40
+ async function listStripePriceOptions(intervalFilter) {
41
+ const stripe = getStripeClient();
42
+ const options = [];
43
+ let startingAfter;
44
+ while (true) {
45
+ const page = await stripe.prices.list({
46
+ active: true,
47
+ type: 'recurring',
48
+ limit: 100,
49
+ starting_after: startingAfter,
50
+ expand: ['data.product']
51
+ });
52
+ for (const price of page.data) {
53
+ const interval = parseInterval(price.recurring?.interval || '');
54
+ if (!interval)
55
+ continue;
56
+ if (intervalFilter && interval !== intervalFilter)
57
+ continue;
58
+ const product = typeof price.product === 'string' ? null : price.product;
59
+ options.push({
60
+ stripePriceId: price.id,
61
+ interval,
62
+ unitAmount: typeof price.unit_amount === 'number' ? price.unit_amount : null,
63
+ currency: price.currency ? String(price.currency) : null,
64
+ active: price.active === true,
65
+ nickname: price.nickname ? String(price.nickname) : null,
66
+ productId: typeof price.product === 'string' ? price.product : (product?.id ? String(product.id) : null),
67
+ productName: product?.deleted ? null : (product?.name ? String(product.name) : null)
68
+ });
69
+ }
70
+ if (!page.has_more || page.data.length === 0)
71
+ break;
72
+ startingAfter = page.data[page.data.length - 1]?.id;
73
+ if (!startingAfter)
74
+ break;
75
+ }
76
+ return options.sort((a, b) => {
77
+ if (a.interval !== b.interval)
78
+ return a.interval.localeCompare(b.interval);
79
+ return a.stripePriceId.localeCompare(b.stripePriceId);
80
+ });
81
+ }
82
+ function resolveSubscriptionCurrentPeriodEnd(subscription) {
83
+ const itemPeriods = Array.isArray(subscription.items?.data)
84
+ ? subscription.items.data
85
+ .map((item) => isoFromUnixSeconds(item?.current_period_end))
86
+ .filter((value) => Boolean(value))
87
+ : [];
88
+ if (itemPeriods.length === 0)
89
+ return null;
90
+ return itemPeriods.sort()[itemPeriods.length - 1] || null;
91
+ }
92
+ function resolveInvoiceSubscriptionId(invoice) {
93
+ const parentSubscription = String(invoice?.parent?.subscription_details?.subscription || '').trim();
94
+ if (parentSubscription)
95
+ return parentSubscription;
96
+ const lineSubscription = String(invoice.lines?.data?.[0]?.subscription || '').trim();
97
+ return lineSubscription;
98
+ }
99
+ function requestRuntimeMode(req) {
100
+ const value = String(req.headers['x-taskforce-runtime-mode'] || '').trim().toLowerCase();
101
+ return value === 'cloud' ? 'cloud' : 'local';
102
+ }
103
+ function requestPricingEnvironment(req) {
104
+ const hostCandidates = [
105
+ String(req.headers.host || '').trim(),
106
+ String(req.headers['x-forwarded-host'] || '').trim()
107
+ ];
108
+ for (const rawHost of hostCandidates) {
109
+ const hostname = rawHost.split(',')[0].trim().replace(/:\d+$/, '');
110
+ const environment = resolveTaskforceCloudEnvironmentFromHostname(hostname);
111
+ if (environment)
112
+ return environment;
113
+ }
114
+ return 'production';
115
+ }
116
+ function parseBoolean(value, fallback = false) {
117
+ const normalized = String(value ?? '').trim().toLowerCase();
118
+ if (normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on')
119
+ return true;
120
+ if (normalized === 'false' || normalized === '0' || normalized === 'no' || normalized === 'off')
121
+ return false;
122
+ return fallback;
123
+ }
124
+ function parsePositiveIntBounded(raw, fallback, min, max) {
125
+ const parsed = Number.parseInt(String(raw || '').trim(), 10);
126
+ if (!Number.isFinite(parsed))
127
+ return fallback;
128
+ return Math.max(min, Math.min(max, parsed));
129
+ }
130
+ function readHeaderFirstValue(raw) {
131
+ return String(raw || '').split(',').map((value) => value.trim()).find(Boolean) || '';
132
+ }
133
+ function normalizeOriginHost(raw) {
134
+ return readHeaderFirstValue(raw).replace(/\/+$/, '');
135
+ }
136
+ function normalizeOriginProto(raw, fallback = 'https') {
137
+ return readHeaderFirstValue(raw).toLowerCase() === 'http' ? 'http' : fallback;
138
+ }
139
+ function resolveRequestProto(req, raw, fallback = 'https') {
140
+ const normalized = readHeaderFirstValue(raw).toLowerCase();
141
+ if (normalized === 'http')
142
+ return 'http';
143
+ if (normalized === 'https')
144
+ return 'https';
145
+ return req.socket?.encrypted ? 'https' : fallback;
146
+ }
147
+ function isLocalDevelopmentHost(raw) {
148
+ const host = normalizeOriginHost(raw).replace(/:\d+$/, '').toLowerCase();
149
+ return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]';
150
+ }
151
+ function resolveRequestOrigin(req) {
152
+ const explicitBaseUrl = readHeaderFirstValue(req.headers[TASKFORCE_PROXY_ORIGIN_BASE_URL_HEADER]);
153
+ const explicitHost = normalizeOriginHost(req.headers[TASKFORCE_PROXY_ORIGIN_HOST_HEADER]);
154
+ const explicitProto = resolveRequestProto(req, req.headers[TASKFORCE_PROXY_ORIGIN_PROTO_HEADER], 'http');
155
+ const originRuntime = readHeaderFirstValue(req.headers[TASKFORCE_PROXY_ORIGIN_RUNTIME_HEADER]).toLowerCase();
156
+ if (explicitBaseUrl) {
157
+ try {
158
+ const parsed = new URL(explicitBaseUrl);
159
+ const proto = parsed.protocol === 'http:' ? 'http' : 'https';
160
+ return {
161
+ baseUrl: `${proto}://${parsed.host.replace(/\/+$/, '')}`,
162
+ host: parsed.host.replace(/\/+$/, ''),
163
+ proto,
164
+ source: 'explicit-proxy-origin',
165
+ localProxyRequest: originRuntime === 'local-proxy' || isLocalDevelopmentHost(parsed.host)
166
+ };
167
+ }
168
+ catch {
169
+ // Fall through to explicit host/proto parsing.
170
+ }
171
+ }
172
+ if (explicitHost) {
173
+ return {
174
+ baseUrl: `${explicitProto}://${explicitHost}`,
175
+ host: explicitHost,
176
+ proto: explicitProto,
177
+ source: 'explicit-proxy-origin',
178
+ localProxyRequest: originRuntime === 'local-proxy' || isLocalDevelopmentHost(explicitHost)
179
+ };
180
+ }
181
+ const forwardedHost = normalizeOriginHost(req.headers['x-forwarded-host']);
182
+ const forwardedProto = resolveRequestProto(req, req.headers['x-forwarded-proto'], 'http');
183
+ if (forwardedHost) {
184
+ return {
185
+ baseUrl: `${forwardedProto}://${forwardedHost}`,
186
+ host: forwardedHost,
187
+ proto: forwardedProto,
188
+ source: 'forwarded',
189
+ localProxyRequest: isLocalDevelopmentHost(forwardedHost)
190
+ };
191
+ }
192
+ const requestHost = normalizeOriginHost(req.headers.host || 'localhost');
193
+ const requestProto = resolveRequestProto(req, req.headers['x-forwarded-proto'], 'http');
194
+ return {
195
+ baseUrl: `${requestProto}://${requestHost}`,
196
+ host: requestHost,
197
+ proto: requestProto,
198
+ source: 'request-host',
199
+ localProxyRequest: isLocalDevelopmentHost(requestHost)
200
+ };
201
+ }
202
+ function resolveBaseUrl(req) {
203
+ const origin = resolveRequestOrigin(req);
204
+ if (origin.localProxyRequest && !isLocalDevelopmentHost(origin.host)) {
205
+ throw new Error(`Billing return URL origin mismatch: expected local development host but resolved ${origin.baseUrl}`);
206
+ }
207
+ return origin.baseUrl;
208
+ }
209
+ async function readRawBody(req, maxBytes = 1_048_576) {
210
+ const chunks = [];
211
+ let total = 0;
212
+ for await (const chunk of req) {
213
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
214
+ total += buf.length;
215
+ if (total > maxBytes) {
216
+ const error = new Error(`Payload too large (max ${maxBytes} bytes)`);
217
+ error.statusCode = 413;
218
+ throw error;
219
+ }
220
+ chunks.push(buf);
221
+ }
222
+ return Buffer.concat(chunks);
223
+ }
224
+ function mapStripeStatusToCanonicalState(statusRaw) {
225
+ const status = String(statusRaw || '').trim().toLowerCase();
226
+ if (status === 'trialing')
227
+ return 'trialing';
228
+ if (status === 'active')
229
+ return 'active';
230
+ if (status === 'past_due')
231
+ return 'grace';
232
+ return 'canceled';
233
+ }
234
+ function isMissingStripeCustomerError(error) {
235
+ const code = String(error?.code || '').trim().toLowerCase();
236
+ const message = String(error?.message || '').trim().toLowerCase();
237
+ return code === 'resource_missing' && message.includes('customer');
238
+ }
239
+ function redactStripePayload(input) {
240
+ const blockedKeys = new Set([
241
+ 'billing_details',
242
+ 'customer_details',
243
+ 'payment_method',
244
+ 'payment_method_details',
245
+ 'payment_intent',
246
+ 'charges',
247
+ 'source',
248
+ 'sources',
249
+ 'card',
250
+ 'default_source'
251
+ ]);
252
+ const visit = (value, depth) => {
253
+ if (depth > 6)
254
+ return '[truncated-depth]';
255
+ if (value === null || value === undefined)
256
+ return value;
257
+ if (Array.isArray(value))
258
+ return value.slice(0, 30).map((item) => visit(item, depth + 1));
259
+ if (typeof value !== 'object')
260
+ return value;
261
+ const source = value;
262
+ const out = {};
263
+ for (const [key, child] of Object.entries(source)) {
264
+ if (blockedKeys.has(key))
265
+ continue;
266
+ out[key] = visit(child, depth + 1);
267
+ }
268
+ return out;
269
+ };
270
+ const redacted = visit(input, 0);
271
+ return redacted && typeof redacted === 'object' && !Array.isArray(redacted)
272
+ ? redacted
273
+ : {};
274
+ }
275
+ function boundedPayloadExcerpt(payload) {
276
+ const raw = JSON.stringify(payload);
277
+ if (Buffer.byteLength(raw, 'utf8') <= BILLING_DIAGNOSTIC_MAX_PAYLOAD_BYTES)
278
+ return raw;
279
+ const truncated = raw.slice(0, BILLING_DIAGNOSTIC_MAX_PAYLOAD_BYTES);
280
+ return `${truncated}...[truncated]`;
281
+ }
282
+ function parseBillingConfig(raw) {
283
+ const fallback = {
284
+ tiers: {},
285
+ taxEnabled: false,
286
+ billingAddress: 'auto'
287
+ };
288
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
289
+ return fallback;
290
+ const record = raw;
291
+ const billingAddress = String(record.billing_address || '').trim().toLowerCase() === 'required'
292
+ ? 'required'
293
+ : 'auto';
294
+ return {
295
+ tiers: record.tiers && typeof record.tiers === 'object' && !Array.isArray(record.tiers)
296
+ ? record.tiers
297
+ : {},
298
+ taxEnabled: parseBoolean(record.tax_enabled, false),
299
+ billingAddress
300
+ };
301
+ }
302
+ function billingDisabledResponse(res) {
303
+ res.writeHead(403, { 'Content-Type': 'application/json' });
304
+ res.end(JSON.stringify({ error: 'Billing is disabled.' }));
305
+ }
306
+ function cloudOnlyResponse(res) {
307
+ res.writeHead(403, { 'Content-Type': 'application/json' });
308
+ res.end(JSON.stringify({ error: 'Billing is only available in cloud runtime.' }));
309
+ }
310
+ function localPricingProxyRequiredResponse(res) {
311
+ res.writeHead(500, { 'Content-Type': 'application/json' });
312
+ res.end(JSON.stringify({ error: 'Pricing must be served through the configured cloud pricing proxy in local runtime.' }));
313
+ }
314
+ function getStripeClient() {
315
+ const key = String(process.env.TASKFORCE_STRIPE_SECRET_KEY || '').trim();
316
+ if (!key) {
317
+ const error = new Error('TASKFORCE_STRIPE_SECRET_KEY is not configured.');
318
+ error.statusCode = 500;
319
+ throw error;
320
+ }
321
+ if (isMockStripeEnabled(key)) {
322
+ return createMockStripeClient();
323
+ }
324
+ return new Stripe(key);
325
+ }
326
+ function resolveHostedServerBaseUrl(req) {
327
+ const host = normalizeOriginHost(req.headers.host || '127.0.0.1');
328
+ const proto = normalizeOriginProto(req.headers['x-forwarded-proto'], 'http');
329
+ return `${proto}://${host}`;
330
+ }
331
+ function readBillingConfig(core) {
332
+ const db = core.getDatabaseAdapter();
333
+ const tenantId = core.getTenantId();
334
+ const row = db.prepare(`
335
+ SELECT payload
336
+ FROM system_settings
337
+ WHERE tenant_id = ? AND settings_key = ?
338
+ LIMIT 1
339
+ `).get(tenantId, BILLING_CONFIG_KEY_PLAN);
340
+ if (!row?.payload)
341
+ return parseBillingConfig({});
342
+ try {
343
+ return parseBillingConfig(JSON.parse(row.payload));
344
+ }
345
+ catch {
346
+ return parseBillingConfig({});
347
+ }
348
+ }
349
+ function writeBillingConfig(core, payload) {
350
+ const db = core.getDatabaseAdapter();
351
+ const tenantId = core.getTenantId();
352
+ const now = new Date().toISOString();
353
+ db.prepare(`
354
+ INSERT INTO system_settings (tenant_id, settings_key, payload, updated_at)
355
+ VALUES (?, ?, ?, ?)
356
+ ON CONFLICT (tenant_id, settings_key) DO UPDATE SET
357
+ payload = excluded.payload,
358
+ updated_at = excluded.updated_at
359
+ `).run(tenantId, BILLING_CONFIG_KEY_PLAN, JSON.stringify({
360
+ tiers: payload.tiers,
361
+ tax_enabled: payload.taxEnabled,
362
+ billing_address: payload.billingAddress
363
+ }), now);
364
+ }
365
+ function resolveStripePriceIdForPlanVersion(core, planVersionIdRaw, intervalRaw) {
366
+ const planVersionId = String(planVersionIdRaw || '').trim();
367
+ const interval = parseInterval(intervalRaw);
368
+ if (!planVersionId || !interval)
369
+ return null;
370
+ const db = core.getDatabaseAdapter();
371
+ const tenantId = core.getTenantId();
372
+ const row = db.prepare(`
373
+ SELECT stripe_price_id
374
+ FROM billing_price_mappings
375
+ WHERE tenant_id = ?
376
+ AND plan_version_id = ?
377
+ AND billing_interval = ?
378
+ AND active = 1
379
+ AND COALESCE(pricing_type, 'stripe') = 'stripe'
380
+ LIMIT 1
381
+ `).get(tenantId, planVersionId, interval);
382
+ return row?.stripe_price_id ? String(row.stripe_price_id) : null;
383
+ }
384
+ function resolveActivePricingForPlanVersion(core, planVersionIdRaw, intervalRaw) {
385
+ const planVersionId = String(planVersionIdRaw || '').trim();
386
+ const interval = parseInterval(intervalRaw);
387
+ if (!planVersionId || !interval)
388
+ return null;
389
+ const db = core.getDatabaseAdapter();
390
+ const tenantId = core.getTenantId();
391
+ const row = db.prepare(`
392
+ SELECT stripe_price_id, pricing_type
393
+ FROM billing_price_mappings
394
+ WHERE tenant_id = ?
395
+ AND plan_version_id = ?
396
+ AND billing_interval = ?
397
+ AND active = 1
398
+ LIMIT 1
399
+ `).get(tenantId, planVersionId, interval);
400
+ if (!row)
401
+ return null;
402
+ const pricingType = parsePricingType(row.pricing_type) || 'stripe';
403
+ return {
404
+ pricingType,
405
+ stripePriceId: pricingType === 'stripe' && row.stripe_price_id ? String(row.stripe_price_id) : null
406
+ };
407
+ }
408
+ function resolvePlanVersionIdForPriceIdFallback(core, priceIdRaw) {
409
+ const priceId = String(priceIdRaw || '').trim();
410
+ if (!priceId)
411
+ return { planVersionId: null, reason: 'missing' };
412
+ const db = core.getDatabaseAdapter();
413
+ const tenantId = core.getTenantId();
414
+ const rows = db.prepare(`
415
+ SELECT DISTINCT plan_version_id
416
+ FROM billing_price_mappings
417
+ WHERE tenant_id = ?
418
+ AND stripe_price_id = ?
419
+ AND active = 1
420
+ ORDER BY plan_version_id ASC
421
+ `).all(tenantId, priceId);
422
+ if (rows.length === 0)
423
+ return { planVersionId: null, reason: 'missing' };
424
+ if (rows.length > 1)
425
+ return { planVersionId: null, reason: 'ambiguous' };
426
+ return {
427
+ planVersionId: rows[0]?.plan_version_id ? String(rows[0].plan_version_id) : null,
428
+ reason: null
429
+ };
430
+ }
431
+ function resolvePlanVersionIdForBillingEvent(core, candidatePlanVersionIds, fallbackPriceId) {
432
+ const db = core.getDatabaseAdapter();
433
+ const tenantId = core.getTenantId();
434
+ for (const candidate of candidatePlanVersionIds) {
435
+ const planVersionId = String(candidate || '').trim();
436
+ if (!planVersionId)
437
+ continue;
438
+ const row = db.prepare(`
439
+ SELECT plan_version_id
440
+ FROM plan_versions
441
+ WHERE tenant_id = ? AND plan_version_id = ?
442
+ LIMIT 1
443
+ `).get(tenantId, planVersionId);
444
+ if (row?.plan_version_id) {
445
+ return { planVersionId: String(row.plan_version_id), source: 'metadata', reason: null };
446
+ }
447
+ }
448
+ const fallback = resolvePlanVersionIdForPriceIdFallback(core, fallbackPriceId);
449
+ return {
450
+ planVersionId: fallback.planVersionId,
451
+ source: fallback.planVersionId ? 'price' : null,
452
+ reason: fallback.reason
453
+ };
454
+ }
455
+ function resolvePlanIdForVersion(core, planVersionIdRaw) {
456
+ const planVersionId = String(planVersionIdRaw || '').trim();
457
+ if (!planVersionId)
458
+ return null;
459
+ const db = core.getDatabaseAdapter();
460
+ const tenantId = core.getTenantId();
461
+ const row = db.prepare(`
462
+ SELECT plan_id
463
+ FROM plan_versions
464
+ WHERE tenant_id = ? AND plan_version_id = ?
465
+ LIMIT 1
466
+ `).get(tenantId, planVersionId);
467
+ return row?.plan_id ? String(row.plan_id) : null;
468
+ }
469
+ function getEntitlementRow(core, userIdRaw) {
470
+ const userId = String(userIdRaw || '').trim();
471
+ if (!userId)
472
+ return null;
473
+ const db = core.getDatabaseAdapter();
474
+ const tenantId = core.getTenantId();
475
+ const row = db.prepare(`
476
+ SELECT account_id, plan_version_id, entitlement_state
477
+ FROM account_entitlements
478
+ WHERE tenant_id = ? AND account_id = ?
479
+ LIMIT 1
480
+ `).get(tenantId, userId);
481
+ return row || null;
482
+ }
483
+ function upsertCanonicalEntitlement(core, input) {
484
+ const db = core.getDatabaseAdapter();
485
+ const tenantId = core.getTenantId();
486
+ const now = new Date().toISOString();
487
+ db.prepare(`
488
+ INSERT INTO account_entitlements (account_id, tenant_id, plan_version_id, entitlement_state, effective_at, effective_until, updated_by, created_at, updated_at)
489
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
490
+ ON CONFLICT (account_id) DO UPDATE SET
491
+ plan_version_id = excluded.plan_version_id,
492
+ entitlement_state = excluded.entitlement_state,
493
+ effective_at = excluded.effective_at,
494
+ effective_until = excluded.effective_until,
495
+ updated_by = excluded.updated_by,
496
+ updated_at = excluded.updated_at
497
+ `).run(input.userId, tenantId, input.planVersionId, input.state, input.effectiveAt || now, input.effectiveUntil || null, input.updatedBy || 'stripe-webhook', now, now);
498
+ }
499
+ function upsertBillingMirror(core, input) {
500
+ const db = core.getDatabaseAdapter();
501
+ const tenantId = core.getTenantId();
502
+ const now = new Date().toISOString();
503
+ const existingRow = db.prepare(`
504
+ SELECT tier, entitlement_state
505
+ FROM user_billing
506
+ WHERE tenant_id = ? AND user_id = ?
507
+ LIMIT 1
508
+ `).get(tenantId, input.userId);
509
+ const resolvedPlanId = String(input.planId || existingRow?.tier || '').trim() || '';
510
+ const resolvedCanonicalState = String(input.canonicalState || existingRow?.entitlement_state || 'active').trim() || 'active';
511
+ db.prepare(`
512
+ INSERT INTO user_billing (
513
+ user_id, tenant_id, stripe_customer_id, stripe_subscription_id, stripe_price_id, stripe_status, tier, billing_interval,
514
+ entitlement_state, trial_end, current_period_end, effective_until, created_at, updated_at
515
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
516
+ ON CONFLICT (user_id) DO UPDATE SET
517
+ stripe_customer_id = COALESCE(excluded.stripe_customer_id, user_billing.stripe_customer_id),
518
+ stripe_subscription_id = COALESCE(excluded.stripe_subscription_id, user_billing.stripe_subscription_id),
519
+ stripe_price_id = COALESCE(excluded.stripe_price_id, user_billing.stripe_price_id),
520
+ stripe_status = COALESCE(excluded.stripe_status, user_billing.stripe_status),
521
+ tier = COALESCE(excluded.tier, user_billing.tier),
522
+ billing_interval = COALESCE(excluded.billing_interval, user_billing.billing_interval),
523
+ entitlement_state = COALESCE(excluded.entitlement_state, user_billing.entitlement_state),
524
+ trial_end = COALESCE(excluded.trial_end, user_billing.trial_end),
525
+ current_period_end = COALESCE(excluded.current_period_end, user_billing.current_period_end),
526
+ effective_until = COALESCE(excluded.effective_until, user_billing.effective_until),
527
+ updated_at = excluded.updated_at
528
+ `).run(input.userId, tenantId, input.stripeCustomerId || null, input.stripeSubscriptionId || null, input.stripePriceId || null, input.stripeStatus || null, resolvedPlanId, input.billingInterval || null, resolvedCanonicalState, input.trialEnd || null, input.currentPeriodEnd || null, input.effectiveUntil || null, now, now);
529
+ }
530
+ function applySubscriptionSnapshot(core, input) {
531
+ const { userId, subscription, source } = input;
532
+ const customerId = String(subscription.customer || '').trim();
533
+ const stripeStatus = String(subscription.status || '').trim().toLowerCase();
534
+ const priceId = String(subscription.items?.data?.[0]?.price?.id || '').trim();
535
+ const billingInterval = parseInterval(subscription.items?.data?.[0]?.price?.recurring?.interval || '');
536
+ const planVersionResolution = resolvePlanVersionIdForBillingEvent(core, [
537
+ subscription.metadata?.planVersionId,
538
+ subscription.metadata?.plan_version_id
539
+ ], priceId);
540
+ const planVersionId = planVersionResolution.planVersionId;
541
+ const existingEntitlement = getEntitlementRow(core, userId);
542
+ const nowIso = new Date().toISOString();
543
+ const currentPeriodEnd = resolveSubscriptionCurrentPeriodEnd(subscription);
544
+ const trialEnd = isoFromUnixSeconds(subscription.trial_end);
545
+ const canceledState = stripeStatus === 'canceled'
546
+ ? (currentPeriodEnd && currentPeriodEnd > nowIso ? 'active' : 'canceled')
547
+ : null;
548
+ const canonicalState = canceledState || mapStripeStatusToCanonicalState(stripeStatus);
549
+ const effectiveUntil = stripeStatus === 'canceled'
550
+ ? currentPeriodEnd
551
+ : (currentPeriodEnd || null);
552
+ upsertBillingMirror(core, {
553
+ userId,
554
+ stripeCustomerId: customerId || null,
555
+ stripeSubscriptionId: String(subscription.id || '').trim() || null,
556
+ stripePriceId: priceId || null,
557
+ stripeStatus: stripeStatus || null,
558
+ planId: planVersionId ? resolvePlanIdForVersion(core, planVersionId) : null,
559
+ billingInterval: billingInterval || null,
560
+ canonicalState,
561
+ trialEnd,
562
+ currentPeriodEnd,
563
+ effectiveUntil
564
+ });
565
+ if (planVersionId) {
566
+ upsertCanonicalEntitlement(core, {
567
+ userId,
568
+ planVersionId: planVersionId || existingEntitlement?.plan_version_id || core.getDefaultPlanVersionIdByMode('personal', { allowFallback: true }),
569
+ state: canonicalState,
570
+ effectiveAt: trialEnd || nowIso,
571
+ effectiveUntil,
572
+ updatedBy: source
573
+ });
574
+ }
575
+ return { planVersionId, canonicalState };
576
+ }
577
+ async function reconcileBillingFromStripe(core, userIdRaw, subscriptionIdRaw) {
578
+ const userId = String(userIdRaw || '').trim();
579
+ const subscriptionId = String(subscriptionIdRaw || '').trim();
580
+ if (!userId || !subscriptionId)
581
+ return false;
582
+ const stripe = getStripeClient();
583
+ const subscription = await stripe.subscriptions.retrieve(subscriptionId);
584
+ applySubscriptionSnapshot(core, {
585
+ userId,
586
+ subscription,
587
+ source: 'checkout-confirm'
588
+ });
589
+ return true;
590
+ }
591
+ function resolveUserIdByCustomer(core, customerIdRaw) {
592
+ const customerId = String(customerIdRaw || '').trim();
593
+ if (!customerId)
594
+ return null;
595
+ const db = core.getDatabaseAdapter();
596
+ const tenantId = core.getTenantId();
597
+ const row = db.prepare(`
598
+ SELECT user_id
599
+ FROM user_billing
600
+ WHERE tenant_id = ? AND stripe_customer_id = ?
601
+ LIMIT 1
602
+ `).get(tenantId, customerId);
603
+ return row?.user_id ? String(row.user_id) : null;
604
+ }
605
+ function resolveUserIdBySubscription(core, subscriptionIdRaw) {
606
+ const subscriptionId = String(subscriptionIdRaw || '').trim();
607
+ if (!subscriptionId)
608
+ return null;
609
+ const db = core.getDatabaseAdapter();
610
+ const tenantId = core.getTenantId();
611
+ const row = db.prepare(`
612
+ SELECT user_id
613
+ FROM user_billing
614
+ WHERE tenant_id = ? AND stripe_subscription_id = ?
615
+ LIMIT 1
616
+ `).get(tenantId, subscriptionId);
617
+ return row?.user_id ? String(row.user_id) : null;
618
+ }
619
+ function getBillingMirrorRow(core, userIdRaw) {
620
+ const userId = String(userIdRaw || '').trim();
621
+ if (!userId)
622
+ return null;
623
+ const db = core.getDatabaseAdapter();
624
+ const tenantId = core.getTenantId();
625
+ return db.prepare(`
626
+ SELECT stripe_status, stripe_customer_id, stripe_subscription_id, stripe_price_id, billing_interval, trial_end, current_period_end, effective_until, entitlement_state, tier
627
+ FROM user_billing
628
+ WHERE tenant_id = ? AND user_id = ?
629
+ LIMIT 1
630
+ `).get(tenantId, userId);
631
+ }
632
+ function isStripeManagedSubscriptionStatus(statusRaw) {
633
+ const status = String(statusRaw || '').trim().toLowerCase();
634
+ return status === 'trialing'
635
+ || status === 'active'
636
+ || status === 'past_due'
637
+ || status === 'unpaid'
638
+ || status === 'paused';
639
+ }
640
+ async function discoverStripeAccountStateForUser(core, input) {
641
+ const stripe = getStripeClient();
642
+ const customerIds = [];
643
+ const seenCustomerIds = new Set();
644
+ const pushCustomerId = (raw) => {
645
+ const customerId = String(raw || '').trim();
646
+ if (!customerId || seenCustomerIds.has(customerId))
647
+ return;
648
+ seenCustomerIds.add(customerId);
649
+ customerIds.push(customerId);
650
+ };
651
+ pushCustomerId(input.currentCustomerId);
652
+ const normalizedEmail = normalizeEmail(input.normalizedEmail);
653
+ if (normalizedEmail && stripe.customers?.list) {
654
+ const customers = await stripe.customers.list({
655
+ email: normalizedEmail,
656
+ limit: 20
657
+ });
658
+ if (Array.isArray(customers?.data)) {
659
+ for (const customer of customers.data) {
660
+ pushCustomerId(customer?.id);
661
+ }
662
+ }
663
+ }
664
+ const activeSubscriptions = [];
665
+ const seenSubscriptionIds = new Set();
666
+ for (const customerId of customerIds) {
667
+ const subscriptions = await stripe.subscriptions.list({
668
+ customer: customerId,
669
+ status: 'all',
670
+ limit: 10
671
+ });
672
+ if (!Array.isArray(subscriptions?.data))
673
+ continue;
674
+ for (const subscription of subscriptions.data) {
675
+ if (!isStripeManagedSubscriptionStatus(subscription?.status))
676
+ continue;
677
+ const subscriptionId = String(subscription?.id || '').trim();
678
+ if (!subscriptionId || seenSubscriptionIds.has(subscriptionId))
679
+ continue;
680
+ seenSubscriptionIds.add(subscriptionId);
681
+ const metadataUserId = String(subscription?.metadata?.userId || subscription?.metadata?.user_id || '').trim();
682
+ activeSubscriptions.push({
683
+ customerId,
684
+ subscription,
685
+ metadataUserMatch: Boolean(metadataUserId) && metadataUserId === input.userId
686
+ });
687
+ }
688
+ }
689
+ activeSubscriptions.sort((left, right) => {
690
+ if (left.metadataUserMatch !== right.metadataUserMatch)
691
+ return left.metadataUserMatch ? -1 : 1;
692
+ if (left.customerId === input.currentCustomerId && right.customerId !== input.currentCustomerId)
693
+ return -1;
694
+ if (right.customerId === input.currentCustomerId && left.customerId !== input.currentCustomerId)
695
+ return 1;
696
+ return String(left.subscription.id || '').localeCompare(String(right.subscription.id || ''));
697
+ });
698
+ return {
699
+ preferredCustomerId: String(input.currentCustomerId || '').trim()
700
+ || customerIds[0]
701
+ || activeSubscriptions[0]?.customerId
702
+ || null,
703
+ activeSubscriptions
704
+ };
705
+ }
706
+ async function reconcileBillingMirrorIfNeeded(core, userIdRaw) {
707
+ const userId = String(userIdRaw || '').trim();
708
+ if (!userId)
709
+ return { row: null, conflictCode: null };
710
+ let row = getBillingMirrorRow(core, userId);
711
+ if (String(row?.stripe_subscription_id || '').trim()) {
712
+ try {
713
+ await reconcileBillingFromStripe(core, userId, row?.stripe_subscription_id);
714
+ row = getBillingMirrorRow(core, userId);
715
+ }
716
+ catch {
717
+ // Fall back to stored billing state when Stripe refresh is unavailable.
718
+ }
719
+ }
720
+ const userIdentity = core.resolveUserIdentity(userId);
721
+ const discovery = await discoverStripeAccountStateForUser(core, {
722
+ userId,
723
+ normalizedEmail: userIdentity?.email,
724
+ currentCustomerId: row?.stripe_customer_id || null
725
+ });
726
+ if (discovery.activeSubscriptions.length > 1) {
727
+ return { row, conflictCode: 'BILLING_DUPLICATE_ACTIVE_SUBSCRIPTIONS' };
728
+ }
729
+ if (discovery.activeSubscriptions.length === 1) {
730
+ const active = discovery.activeSubscriptions[0];
731
+ const discoveredSubscriptionId = String(active.subscription?.id || '').trim();
732
+ const discoveredCustomerId = String(active.customerId || '').trim();
733
+ const currentSubscriptionId = String(row?.stripe_subscription_id || '').trim();
734
+ const currentCustomerId = String(row?.stripe_customer_id || '').trim();
735
+ if (!currentCustomerId || currentCustomerId !== discoveredCustomerId || !currentSubscriptionId || currentSubscriptionId !== discoveredSubscriptionId) {
736
+ upsertBillingMirror(core, {
737
+ userId,
738
+ stripeCustomerId: discoveredCustomerId || null,
739
+ stripeSubscriptionId: discoveredSubscriptionId || null
740
+ });
741
+ }
742
+ applySubscriptionSnapshot(core, {
743
+ userId,
744
+ subscription: active.subscription,
745
+ source: 'checkout-confirm'
746
+ });
747
+ row = getBillingMirrorRow(core, userId);
748
+ return { row, conflictCode: null };
749
+ }
750
+ if (!String(row?.stripe_customer_id || '').trim() && discovery.preferredCustomerId) {
751
+ upsertBillingMirror(core, {
752
+ userId,
753
+ stripeCustomerId: discovery.preferredCustomerId
754
+ });
755
+ row = getBillingMirrorRow(core, userId);
756
+ }
757
+ return { row, conflictCode: null };
758
+ }
759
+ function purgeExpiredWebhookFailures(core) {
760
+ const db = core.getDatabaseAdapter();
761
+ const tenantId = core.getTenantId();
762
+ const result = db.prepare(`
763
+ DELETE FROM stripe_webhook_failures
764
+ WHERE tenant_id = ?
765
+ AND purge_after < ?
766
+ `).run(tenantId, new Date().toISOString());
767
+ return Number(result?.changes || 0);
768
+ }
769
+ function recordWebhookFailure(core, input) {
770
+ try {
771
+ const db = core.getDatabaseAdapter();
772
+ const tenantId = core.getTenantId();
773
+ const now = new Date();
774
+ const purgeAfter = new Date(now.getTime() + BILLING_DIAGNOSTIC_RETENTION_DAYS * 24 * 60 * 60 * 1000).toISOString();
775
+ const redacted = redactStripePayload(input.payload);
776
+ const excerpt = boundedPayloadExcerpt(redacted);
777
+ const id = `whf-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
778
+ db.prepare(`
779
+ INSERT INTO stripe_webhook_failures (id, tenant_id, event_id, event_type, reason, payload_excerpt, purge_after, created_at)
780
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
781
+ `).run(id, tenantId, input.eventId, input.eventType, input.reason, excerpt, purgeAfter, now.toISOString());
782
+ }
783
+ catch {
784
+ // diagnostics are best-effort only
785
+ }
786
+ }
787
+ function resolvePriceAndIntervalFromInvoice(invoice) {
788
+ const line = invoice.lines?.data?.find((entry) => !!entry?.price?.id || !!entry?.pricing?.price_details?.price);
789
+ const legacyPriceId = String(line?.price?.id || '').trim();
790
+ const modernPriceId = String(line?.pricing?.price_details?.price || '').trim();
791
+ const priceId = modernPriceId || legacyPriceId || null;
792
+ const recurringInterval = parseInterval(line?.price?.recurring?.interval || '');
793
+ return { priceId, interval: recurringInterval };
794
+ }
795
+ function lockWebhookEvent(core, event) {
796
+ const db = core.getDatabaseAdapter();
797
+ const tenantId = core.getTenantId();
798
+ const result = db.prepare(`
799
+ INSERT INTO stripe_webhook_events (event_id, tenant_id, event_type, processed_at)
800
+ VALUES (?, ?, ?, ?)
801
+ ON CONFLICT (event_id) DO NOTHING
802
+ `).run(event.id, tenantId, event.type, new Date().toISOString());
803
+ return Number(result?.changes || 0) > 0;
804
+ }
805
+ async function importStripePrices(core) {
806
+ const stripe = getStripeClient();
807
+ const db = core.getDatabaseAdapter();
808
+ const tenantId = core.getTenantId();
809
+ const now = new Date().toISOString();
810
+ const validPlanVersionIds = new Set(db.prepare(`
811
+ SELECT plan_version_id
812
+ FROM plan_versions
813
+ WHERE tenant_id = ?
814
+ `).all(tenantId).map((row) => String(row.plan_version_id || '')).filter(Boolean));
815
+ const importedPriceIds = new Set();
816
+ const activeStripePriceIds = new Set();
817
+ const summary = {
818
+ imported: 0,
819
+ skipped: 0,
820
+ deactivated: 0,
821
+ details: []
822
+ };
823
+ let startingAfter;
824
+ while (true) {
825
+ const page = await stripe.prices.list({
826
+ active: true,
827
+ limit: 100,
828
+ starting_after: startingAfter,
829
+ type: 'recurring'
830
+ });
831
+ for (const price of page.data) {
832
+ activeStripePriceIds.add(String(price.id || '').trim());
833
+ const interval = parseInterval(price.recurring?.interval || '');
834
+ const planVersionId = String(price.metadata?.[PRICE_METADATA_PLAN_VERSION] || '').trim();
835
+ if (!interval || !planVersionId || !validPlanVersionIds.has(planVersionId)) {
836
+ summary.skipped += 1;
837
+ summary.details.push({
838
+ priceId: price.id,
839
+ status: 'skipped',
840
+ reason: !planVersionId
841
+ ? `Missing metadata ${PRICE_METADATA_PLAN_VERSION}`
842
+ : (!interval ? 'Unsupported interval' : 'Unknown plan_version_id')
843
+ });
844
+ continue;
845
+ }
846
+ db.prepare(`
847
+ INSERT INTO billing_price_mappings (
848
+ tenant_id, plan_version_id, billing_interval, stripe_price_id, active, unit_amount, currency, created_at, updated_at
849
+ ) VALUES (?, ?, ?, ?, 1, ?, ?, ?, ?)
850
+ ON CONFLICT (tenant_id, plan_version_id, billing_interval, stripe_price_id) DO UPDATE SET
851
+ active = 1,
852
+ unit_amount = excluded.unit_amount,
853
+ currency = excluded.currency,
854
+ updated_at = excluded.updated_at
855
+ `).run(tenantId, planVersionId, interval, price.id, price.unit_amount, price.currency, now, now);
856
+ importedPriceIds.add(price.id);
857
+ summary.imported += 1;
858
+ summary.details.push({ priceId: price.id, status: 'imported' });
859
+ }
860
+ if (!page.has_more || page.data.length === 0)
861
+ break;
862
+ startingAfter = page.data[page.data.length - 1]?.id;
863
+ if (!startingAfter)
864
+ break;
865
+ }
866
+ const existing = db.prepare(`
867
+ SELECT stripe_price_id, pricing_type
868
+ FROM billing_price_mappings
869
+ WHERE tenant_id = ? AND active = 1
870
+ `).all(tenantId);
871
+ for (const row of existing) {
872
+ const priceId = String(row.stripe_price_id || '').trim();
873
+ const pricingType = String(row.pricing_type || '').trim().toLowerCase();
874
+ if (!priceId)
875
+ continue;
876
+ if (pricingType === 'free')
877
+ continue;
878
+ if (importedPriceIds.has(priceId))
879
+ continue;
880
+ if (activeStripePriceIds.has(priceId))
881
+ continue;
882
+ db.prepare(`
883
+ UPDATE billing_price_mappings
884
+ SET active = 0, updated_at = ?
885
+ WHERE tenant_id = ? AND stripe_price_id = ?
886
+ `).run(now, tenantId, priceId);
887
+ summary.deactivated += 1;
888
+ }
889
+ return summary;
890
+ }
891
+ export function registerBillingRoutes(deps) {
892
+ const { addRoute, core, context, ensureAuthenticatedUser, ensureAdminRole, requestUserId, resolveErrorStatusCode, auditAdminAction } = deps;
893
+ const billingEnabled = () => parseBoolean(process.env.TASKFORCE_BILLING_ENABLED, false);
894
+ const ensureCloudBilling = (req, res) => {
895
+ if (!billingEnabled()) {
896
+ billingDisabledResponse(res);
897
+ return false;
898
+ }
899
+ const runtime = context.authConfig?.runtimeMode || requestRuntimeMode(req);
900
+ if (runtime !== 'cloud') {
901
+ cloudOnlyResponse(res);
902
+ return false;
903
+ }
904
+ return true;
905
+ };
906
+ const ensureCloudPricingRoute = (req, res) => {
907
+ if (!billingEnabled()) {
908
+ billingDisabledResponse(res);
909
+ return false;
910
+ }
911
+ const runtime = context.authConfig?.runtimeMode || requestRuntimeMode(req);
912
+ if (runtime !== 'cloud') {
913
+ localPricingProxyRequiredResponse(res);
914
+ return false;
915
+ }
916
+ return true;
917
+ };
918
+ addRoute('GET', '/api/taskforce/billing/status', async (req, res) => {
919
+ if (!ensureCloudBilling(req, res))
920
+ return;
921
+ if (!ensureAuthenticatedUser(req)) {
922
+ res.writeHead(401, { 'Content-Type': 'application/json' });
923
+ res.end(JSON.stringify({ error: 'Authentication required.' }));
924
+ return;
925
+ }
926
+ try {
927
+ const userId = requestUserId(req);
928
+ const db = core.getDatabaseAdapter();
929
+ const tenantId = core.getTenantId();
930
+ const billingReconciliation = await reconcileBillingMirrorIfNeeded(core, userId);
931
+ const row = billingReconciliation.row;
932
+ const access = core.getAccountAccessStatus(userId);
933
+ if (!access.hasEntitlement || !access.entitlement) {
934
+ res.writeHead(200, { 'Content-Type': 'application/json' });
935
+ res.end(JSON.stringify({
936
+ stripeStatus: row?.stripe_status || null,
937
+ stripeCustomerId: row?.stripe_customer_id || null,
938
+ stripeSubscriptionId: row?.stripe_subscription_id || null,
939
+ stripePriceId: row?.stripe_price_id || null,
940
+ billingInterval: row?.billing_interval || null,
941
+ trialEnd: row?.trial_end || null,
942
+ currentPeriodEnd: row?.current_period_end || null,
943
+ effectiveUntil: row?.effective_until || null,
944
+ entitlementState: null,
945
+ planVersionId: null,
946
+ planId: null,
947
+ planName: null,
948
+ gate: access.gate,
949
+ message: access.message,
950
+ planSelectionRequired: access.planSelectionRequired,
951
+ billingConflictCode: billingReconciliation.conflictCode
952
+ }));
953
+ return;
954
+ }
955
+ const entitlement = access.entitlement;
956
+ const planRow = db.prepare(`
957
+ SELECT display_name
958
+ FROM plan_catalog
959
+ WHERE tenant_id = ? AND plan_id = ?
960
+ LIMIT 1
961
+ `).get(tenantId, entitlement.planId);
962
+ res.writeHead(200, { 'Content-Type': 'application/json' });
963
+ res.end(JSON.stringify({
964
+ stripeStatus: row?.stripe_status || null,
965
+ stripeCustomerId: row?.stripe_customer_id || null,
966
+ stripeSubscriptionId: row?.stripe_subscription_id || null,
967
+ stripePriceId: row?.stripe_price_id || null,
968
+ billingInterval: row?.billing_interval || null,
969
+ trialEnd: row?.trial_end || null,
970
+ currentPeriodEnd: row?.current_period_end || null,
971
+ effectiveUntil: row?.effective_until || null,
972
+ entitlementState: entitlement.state,
973
+ planVersionId: entitlement.planVersionId,
974
+ planId: entitlement.planId,
975
+ planName: planRow?.display_name ? String(planRow.display_name) : entitlement.planId,
976
+ gate: access.gate,
977
+ message: access.message,
978
+ planSelectionRequired: access.planSelectionRequired,
979
+ billingConflictCode: billingReconciliation.conflictCode
980
+ }));
981
+ }
982
+ catch (error) {
983
+ const statusCode = resolveErrorStatusCode(error, 400);
984
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
985
+ res.end(JSON.stringify({ error: String(error?.message || 'Unable to read billing status'), code: error?.code }));
986
+ }
987
+ });
988
+ addRoute('GET', '/api/taskforce/admin/billing/config', async (req, res) => {
989
+ if (!ensureCloudBilling(req, res))
990
+ return;
991
+ if (!ensureAdminRole(req)) {
992
+ res.writeHead(403, { 'Content-Type': 'application/json' });
993
+ res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
994
+ return;
995
+ }
996
+ const config = readBillingConfig(core);
997
+ res.writeHead(200, { 'Content-Type': 'application/json' });
998
+ res.end(JSON.stringify(config));
999
+ });
1000
+ addRoute('PUT', '/api/taskforce/admin/billing/config', async (req, res) => {
1001
+ if (!ensureCloudBilling(req, res))
1002
+ return;
1003
+ if (!ensureAdminRole(req)) {
1004
+ res.writeHead(403, { 'Content-Type': 'application/json' });
1005
+ res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
1006
+ return;
1007
+ }
1008
+ const body = await parseJsonBody(req);
1009
+ const nextConfig = {
1010
+ tiers: body?.tiers && typeof body.tiers === 'object' && !Array.isArray(body.tiers) ? body.tiers : {},
1011
+ taxEnabled: parseBoolean(body?.tax_enabled, false),
1012
+ billingAddress: String(body?.billing_address || '').trim().toLowerCase() === 'required' ? 'required' : 'auto'
1013
+ };
1014
+ writeBillingConfig(core, nextConfig);
1015
+ auditAdminAction(req, 'billing-config-updated', { billingAddress: nextConfig.billingAddress });
1016
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1017
+ res.end(JSON.stringify(nextConfig));
1018
+ });
1019
+ addRoute('GET', '/api/taskforce/admin/billing/prices', async (req, res) => {
1020
+ if (!ensureCloudBilling(req, res))
1021
+ return;
1022
+ if (!ensureAdminRole(req)) {
1023
+ res.writeHead(403, { 'Content-Type': 'application/json' });
1024
+ res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
1025
+ return;
1026
+ }
1027
+ const db = core.getDatabaseAdapter();
1028
+ const tenantId = core.getTenantId();
1029
+ const rows = db.prepare(`
1030
+ SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at
1031
+ FROM billing_price_mappings
1032
+ WHERE tenant_id = ?
1033
+ ORDER BY plan_version_id ASC, billing_interval ASC, stripe_price_id ASC
1034
+ `).all(tenantId);
1035
+ if (rows.length === 0) {
1036
+ console.warn(`[Taskforce] Cloud billing returned no price mappings for tenant=${tenantId}.`);
1037
+ }
1038
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1039
+ res.end(JSON.stringify({ prices: rows }));
1040
+ });
1041
+ addRoute('GET', '/api/taskforce/admin/billing/stripe-price-options', async (req, res) => {
1042
+ if (!ensureCloudBilling(req, res))
1043
+ return;
1044
+ if (!ensureAdminRole(req)) {
1045
+ res.writeHead(403, { 'Content-Type': 'application/json' });
1046
+ res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
1047
+ return;
1048
+ }
1049
+ try {
1050
+ const requestUrl = parseRequestUrl(req);
1051
+ const interval = parseInterval(requestUrl.searchParams.get('interval'));
1052
+ const options = await listStripePriceOptions(interval);
1053
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1054
+ res.end(JSON.stringify({ options }));
1055
+ }
1056
+ catch (error) {
1057
+ const statusCode = resolveErrorStatusCode(error, 400);
1058
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
1059
+ res.end(JSON.stringify({ error: String(error?.message || 'Unable to list Stripe prices'), code: error?.code }));
1060
+ }
1061
+ });
1062
+ addRoute('PUT', '/api/taskforce/admin/billing/price-mappings', async (req, res) => {
1063
+ if (!ensureCloudBilling(req, res))
1064
+ return;
1065
+ if (!ensureAdminRole(req)) {
1066
+ res.writeHead(403, { 'Content-Type': 'application/json' });
1067
+ res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
1068
+ return;
1069
+ }
1070
+ const body = await parseJsonBody(req);
1071
+ const planVersionId = String(body?.planVersionId || '').trim();
1072
+ const interval = parseInterval(body?.interval);
1073
+ const pricingType = parsePricingType(body?.pricingType) || 'stripe';
1074
+ const stripePriceId = String(body?.stripePriceId || '').trim();
1075
+ const setActive = body?.active === false ? false : true;
1076
+ if (!planVersionId || !interval) {
1077
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1078
+ res.end(JSON.stringify({ error: 'planVersionId and interval are required.', code: 'BILLING_MAPPING_INPUT_REQUIRED' }));
1079
+ return;
1080
+ }
1081
+ if (pricingType === 'stripe' && !stripePriceId) {
1082
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1083
+ res.end(JSON.stringify({ error: 'stripePriceId is required for Stripe pricing.', code: 'BILLING_MAPPING_STRIPE_PRICE_REQUIRED' }));
1084
+ return;
1085
+ }
1086
+ const db = core.getDatabaseAdapter();
1087
+ const tenantId = core.getTenantId();
1088
+ const versionExists = db.prepare(`
1089
+ SELECT plan_version_id
1090
+ FROM plan_versions
1091
+ WHERE tenant_id = ? AND plan_version_id = ?
1092
+ LIMIT 1
1093
+ `).get(tenantId, planVersionId);
1094
+ if (!versionExists?.plan_version_id) {
1095
+ res.writeHead(404, { 'Content-Type': 'application/json' });
1096
+ res.end(JSON.stringify({ error: 'Plan version not found.', code: 'PLAN_VERSION_NOT_FOUND' }));
1097
+ return;
1098
+ }
1099
+ try {
1100
+ const now = new Date().toISOString();
1101
+ const targetStripePriceId = pricingType === 'free'
1102
+ ? buildFreePricingSyntheticId(planVersionId, interval)
1103
+ : stripePriceId;
1104
+ let unitAmount = 0;
1105
+ let currency = null;
1106
+ if (pricingType === 'stripe') {
1107
+ const stripe = getStripeClient();
1108
+ const stripePrice = await stripe.prices.retrieve(stripePriceId);
1109
+ const stripeInterval = parseInterval(stripePrice.recurring?.interval || '');
1110
+ if (!stripePrice.active) {
1111
+ res.writeHead(422, { 'Content-Type': 'application/json' });
1112
+ res.end(JSON.stringify({ error: 'Stripe price is not active.', code: 'STRIPE_PRICE_INACTIVE' }));
1113
+ return;
1114
+ }
1115
+ if (!stripeInterval) {
1116
+ res.writeHead(422, { 'Content-Type': 'application/json' });
1117
+ res.end(JSON.stringify({ error: 'Stripe price must be recurring month/year.', code: 'STRIPE_PRICE_INTERVAL_UNSUPPORTED' }));
1118
+ return;
1119
+ }
1120
+ if (stripeInterval !== interval) {
1121
+ res.writeHead(422, { 'Content-Type': 'application/json' });
1122
+ res.end(JSON.stringify({ error: `Stripe price interval (${stripeInterval}) does not match requested interval (${interval}).`, code: 'STRIPE_PRICE_INTERVAL_MISMATCH' }));
1123
+ return;
1124
+ }
1125
+ unitAmount = typeof stripePrice.unit_amount === 'number' ? stripePrice.unit_amount : null;
1126
+ currency = stripePrice.currency || null;
1127
+ }
1128
+ const beforeRows = db.prepare(`
1129
+ SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency
1130
+ FROM billing_price_mappings
1131
+ WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ?
1132
+ ORDER BY stripe_price_id ASC
1133
+ `).all(tenantId, planVersionId, interval);
1134
+ if (setActive) {
1135
+ db.prepare(`
1136
+ UPDATE billing_price_mappings
1137
+ SET active = 0, updated_at = ?
1138
+ WHERE tenant_id = ?
1139
+ AND plan_version_id = ?
1140
+ AND billing_interval = ?
1141
+ AND stripe_price_id <> ?
1142
+ AND active = 1
1143
+ `).run(now, tenantId, planVersionId, interval, targetStripePriceId);
1144
+ }
1145
+ db.prepare(`
1146
+ INSERT INTO billing_price_mappings (
1147
+ tenant_id, plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at
1148
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1149
+ ON CONFLICT (tenant_id, plan_version_id, billing_interval, stripe_price_id) DO UPDATE SET
1150
+ pricing_type = excluded.pricing_type,
1151
+ active = excluded.active,
1152
+ unit_amount = excluded.unit_amount,
1153
+ currency = excluded.currency,
1154
+ updated_at = excluded.updated_at
1155
+ `).run(tenantId, planVersionId, interval, targetStripePriceId, pricingType, setActive ? 1 : 0, unitAmount, currency, now, now);
1156
+ const mapping = db.prepare(`
1157
+ SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at
1158
+ FROM billing_price_mappings
1159
+ WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ? AND stripe_price_id = ?
1160
+ LIMIT 1
1161
+ `).get(tenantId, planVersionId, interval, targetStripePriceId);
1162
+ const afterRows = db.prepare(`
1163
+ SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency
1164
+ FROM billing_price_mappings
1165
+ WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ?
1166
+ ORDER BY stripe_price_id ASC
1167
+ `).all(tenantId, planVersionId, interval);
1168
+ auditAdminAction(req, 'billing-price-mapping-upsert', {
1169
+ planVersionId,
1170
+ interval,
1171
+ pricingType,
1172
+ stripePriceId: pricingType === 'free' ? null : stripePriceId,
1173
+ active: setActive,
1174
+ before: beforeRows,
1175
+ after: afterRows
1176
+ });
1177
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1178
+ res.end(JSON.stringify({ mapping }));
1179
+ }
1180
+ catch (error) {
1181
+ const statusCode = resolveErrorStatusCode(error, 400);
1182
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
1183
+ res.end(JSON.stringify({ error: String(error?.message || 'Unable to upsert price mapping'), code: error?.code }));
1184
+ }
1185
+ });
1186
+ addRoute('POST', '/api/taskforce/admin/billing/price-mappings/deactivate', async (req, res) => {
1187
+ if (!ensureCloudBilling(req, res))
1188
+ return;
1189
+ if (!ensureAdminRole(req)) {
1190
+ res.writeHead(403, { 'Content-Type': 'application/json' });
1191
+ res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
1192
+ return;
1193
+ }
1194
+ const body = await parseJsonBody(req);
1195
+ const planVersionId = String(body?.planVersionId || '').trim();
1196
+ const interval = parseInterval(body?.interval);
1197
+ const stripePriceId = String(body?.stripePriceId || '').trim();
1198
+ if (!planVersionId || !interval) {
1199
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1200
+ res.end(JSON.stringify({ error: 'planVersionId and interval are required.', code: 'BILLING_MAPPING_TARGET_REQUIRED' }));
1201
+ return;
1202
+ }
1203
+ const db = core.getDatabaseAdapter();
1204
+ const tenantId = core.getTenantId();
1205
+ const now = new Date().toISOString();
1206
+ const beforeRows = db.prepare(`
1207
+ SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency
1208
+ FROM billing_price_mappings
1209
+ WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ?
1210
+ ORDER BY stripe_price_id ASC
1211
+ `).all(tenantId, planVersionId, interval);
1212
+ const result = stripePriceId
1213
+ ? db.prepare(`
1214
+ UPDATE billing_price_mappings
1215
+ SET active = 0, updated_at = ?
1216
+ WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ? AND stripe_price_id = ?
1217
+ `).run(now, tenantId, planVersionId, interval, stripePriceId)
1218
+ : db.prepare(`
1219
+ UPDATE billing_price_mappings
1220
+ SET active = 0, updated_at = ?
1221
+ WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ?
1222
+ `).run(now, tenantId, planVersionId, interval);
1223
+ const afterRows = db.prepare(`
1224
+ SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency
1225
+ FROM billing_price_mappings
1226
+ WHERE tenant_id = ? AND plan_version_id = ? AND billing_interval = ?
1227
+ ORDER BY stripe_price_id ASC
1228
+ `).all(tenantId, planVersionId, interval);
1229
+ auditAdminAction(req, 'billing-price-mapping-deactivate', {
1230
+ planVersionId,
1231
+ interval,
1232
+ stripePriceId: stripePriceId || null,
1233
+ changedRows: Number(result?.changes || 0),
1234
+ before: beforeRows,
1235
+ after: afterRows
1236
+ });
1237
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1238
+ res.end(JSON.stringify({ success: true, changedRows: Number(result?.changes || 0) }));
1239
+ });
1240
+ addRoute('GET', '/api/taskforce/admin/billing/webhook-failures', async (req, res) => {
1241
+ if (!ensureCloudBilling(req, res))
1242
+ return;
1243
+ if (!ensureAdminRole(req)) {
1244
+ res.writeHead(403, { 'Content-Type': 'application/json' });
1245
+ res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
1246
+ return;
1247
+ }
1248
+ const db = core.getDatabaseAdapter();
1249
+ const tenantId = core.getTenantId();
1250
+ const queryLimit = req.url
1251
+ ? new URL(req.url, 'https://taskforce.local').searchParams.get('limit')
1252
+ : null;
1253
+ const limit = parsePositiveIntBounded(queryLimit, 50, 1, 200);
1254
+ const now = new Date().toISOString();
1255
+ const purged = purgeExpiredWebhookFailures(core);
1256
+ const rows = db.prepare(`
1257
+ SELECT id, event_id, event_type, reason, payload_excerpt, created_at, purge_after
1258
+ FROM stripe_webhook_failures
1259
+ WHERE tenant_id = ?
1260
+ ORDER BY created_at DESC
1261
+ LIMIT ?
1262
+ `).all(tenantId, limit);
1263
+ const total = db.prepare(`
1264
+ SELECT COUNT(*) AS count
1265
+ FROM stripe_webhook_failures
1266
+ WHERE tenant_id = ?
1267
+ `).get(tenantId);
1268
+ const expiringSoon = db.prepare(`
1269
+ SELECT COUNT(*) AS count
1270
+ FROM stripe_webhook_failures
1271
+ WHERE tenant_id = ?
1272
+ AND purge_after <= ?
1273
+ `).get(tenantId, new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString());
1274
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1275
+ res.end(JSON.stringify({
1276
+ asOf: now,
1277
+ purgedExpiredRows: purged,
1278
+ total: Number(total?.count || 0),
1279
+ expiringWithin24h: Number(expiringSoon?.count || 0),
1280
+ rows
1281
+ }));
1282
+ });
1283
+ addRoute('POST', '/api/taskforce/admin/billing/prices/import', async (req, res) => {
1284
+ if (!ensureCloudBilling(req, res))
1285
+ return;
1286
+ if (!ensureAdminRole(req)) {
1287
+ res.writeHead(403, { 'Content-Type': 'application/json' });
1288
+ res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
1289
+ return;
1290
+ }
1291
+ try {
1292
+ const summary = await importStripePrices(core);
1293
+ auditAdminAction(req, 'billing-prices-import', {
1294
+ imported: summary.imported,
1295
+ skipped: summary.skipped,
1296
+ deactivated: summary.deactivated
1297
+ });
1298
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1299
+ res.end(JSON.stringify(summary));
1300
+ }
1301
+ catch (error) {
1302
+ const statusCode = resolveErrorStatusCode(error, 400);
1303
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
1304
+ res.end(JSON.stringify({ error: String(error?.message || 'Unable to import Stripe prices'), code: error?.code }));
1305
+ }
1306
+ });
1307
+ addRoute('GET', '/__taskforce/mock-stripe/checkout', async (req, res) => {
1308
+ if (!billingEnabled() || (context.authConfig?.runtimeMode || requestRuntimeMode(req)) !== 'cloud' || !isMockStripeEnabled()) {
1309
+ res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
1310
+ res.end('Not found');
1311
+ return;
1312
+ }
1313
+ const sessionId = parseRequestUrl(req).searchParams.get('session_id') || '';
1314
+ const session = readMockStripeCheckoutSession(sessionId);
1315
+ if (!session) {
1316
+ res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
1317
+ res.end('Mock checkout session not found.');
1318
+ return;
1319
+ }
1320
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
1321
+ res.end(renderMockStripeCheckoutHtml({
1322
+ sessionId: session.id,
1323
+ planVersionId: session.subscriptionMetadata.planVersionId || session.subscriptionMetadata.plan_version_id || null,
1324
+ interval: session.interval
1325
+ }));
1326
+ });
1327
+ addRoute('GET', '/__taskforce/mock-stripe/checkout/complete', async (req, res) => {
1328
+ if (!billingEnabled() || (context.authConfig?.runtimeMode || requestRuntimeMode(req)) !== 'cloud' || !isMockStripeEnabled()) {
1329
+ res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
1330
+ res.end('Not found');
1331
+ return;
1332
+ }
1333
+ const sessionId = parseRequestUrl(req).searchParams.get('session_id') || '';
1334
+ try {
1335
+ const session = completeMockStripeCheckoutSession(sessionId);
1336
+ const nextUrl = String(session.successUrl || '').replace('{CHECKOUT_SESSION_ID}', encodeURIComponent(session.id));
1337
+ res.writeHead(302, { Location: nextUrl || '/' });
1338
+ res.end();
1339
+ }
1340
+ catch (error) {
1341
+ res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
1342
+ res.end(String(error?.message || 'Mock checkout session not found.'));
1343
+ }
1344
+ });
1345
+ addRoute('GET', '/__taskforce/mock-stripe/checkout/cancel', async (req, res) => {
1346
+ if (!billingEnabled() || (context.authConfig?.runtimeMode || requestRuntimeMode(req)) !== 'cloud' || !isMockStripeEnabled()) {
1347
+ res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
1348
+ res.end('Not found');
1349
+ return;
1350
+ }
1351
+ const sessionId = parseRequestUrl(req).searchParams.get('session_id') || '';
1352
+ const session = readMockStripeCheckoutSession(sessionId);
1353
+ res.writeHead(302, {
1354
+ Location: String(session?.cancelUrl || '/')
1355
+ });
1356
+ res.end();
1357
+ });
1358
+ addRoute('POST', '/api/taskforce/billing/checkout-session', async (req, res) => {
1359
+ if (!ensureCloudBilling(req, res))
1360
+ return;
1361
+ if (!ensureAuthenticatedUser(req)) {
1362
+ res.writeHead(401, { 'Content-Type': 'application/json' });
1363
+ res.end(JSON.stringify({ error: 'Authentication required.' }));
1364
+ return;
1365
+ }
1366
+ const body = await parseJsonBody(req);
1367
+ const interval = parseInterval(body?.interval);
1368
+ const planVersionId = String(body?.planVersionId || '').trim();
1369
+ if (!interval || !planVersionId) {
1370
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1371
+ res.end(JSON.stringify({ error: 'planVersionId and interval are required.' }));
1372
+ return;
1373
+ }
1374
+ const pricing = resolveActivePricingForPlanVersion(core, planVersionId, interval);
1375
+ if (!pricing) {
1376
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1377
+ res.end(JSON.stringify({ error: 'No active pricing mapping found for requested plan/interval.' }));
1378
+ return;
1379
+ }
1380
+ try {
1381
+ const userId = requestUserId(req);
1382
+ const currentBillingResult = await reconcileBillingMirrorIfNeeded(core, userId);
1383
+ const currentBilling = currentBillingResult.row;
1384
+ if (currentBillingResult.conflictCode === 'BILLING_DUPLICATE_ACTIVE_SUBSCRIPTIONS') {
1385
+ console.warn(`[Taskforce Billing] Duplicate active subscriptions detected for user=${userId}`);
1386
+ res.writeHead(409, { 'Content-Type': 'application/json' });
1387
+ res.end(JSON.stringify({
1388
+ error: 'Multiple active subscriptions are already linked to this account. Please contact support before making changes.',
1389
+ code: currentBillingResult.conflictCode
1390
+ }));
1391
+ return;
1392
+ }
1393
+ const currentEntitlement = getEntitlementRow(core, userId);
1394
+ const userIdentity = core.resolveUserIdentity(userId);
1395
+ const normalizedUserEmail = normalizeEmail(userIdentity?.email);
1396
+ if (pricing.pricingType === 'free') {
1397
+ if (currentEntitlement?.plan_version_id && String(currentEntitlement.plan_version_id).trim() === planVersionId) {
1398
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1399
+ res.end(JSON.stringify({
1400
+ checkoutState: 'already_current_plan',
1401
+ planVersionId,
1402
+ message: 'You are already on this plan.'
1403
+ }));
1404
+ return;
1405
+ }
1406
+ const billingProfile = core.getBillingMirrorProfileForPlanVersion(planVersionId);
1407
+ core.createUserBillingRecord(userId, billingProfile);
1408
+ core.updateAccountEntitlement({
1409
+ accountId: userId,
1410
+ planVersionId,
1411
+ entitlementState: 'active',
1412
+ actorUserId: userId,
1413
+ actorRole: 'user',
1414
+ reason: 'free-plan-selection'
1415
+ });
1416
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1417
+ res.end(JSON.stringify({ checkoutState: 'free', planVersionId }));
1418
+ return;
1419
+ }
1420
+ const priceId = pricing.stripePriceId;
1421
+ if (!priceId) {
1422
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1423
+ res.end(JSON.stringify({ error: 'No active Stripe price mapping found for requested plan/interval.' }));
1424
+ return;
1425
+ }
1426
+ const stripe = getStripeClient();
1427
+ let currentSubscriptionId = String(currentBilling?.stripe_subscription_id || '').trim();
1428
+ const currentCustomerId = String(currentBilling?.stripe_customer_id || '').trim();
1429
+ const currentPlanVersionId = String(currentEntitlement?.plan_version_id || '').trim();
1430
+ const currentInterval = parseInterval(String(currentBilling?.billing_interval || '').trim());
1431
+ const stripeAccountDiscovery = await discoverStripeAccountStateForUser(core, {
1432
+ userId,
1433
+ normalizedEmail: normalizedUserEmail,
1434
+ currentCustomerId
1435
+ });
1436
+ if (stripeAccountDiscovery.activeSubscriptions.length > 1) {
1437
+ console.warn(`[Taskforce Billing] Duplicate active subscriptions detected for user=${userId}`);
1438
+ res.writeHead(409, { 'Content-Type': 'application/json' });
1439
+ res.end(JSON.stringify({
1440
+ error: 'Multiple active subscriptions are already linked to this account. Please contact support before making changes.',
1441
+ code: 'BILLING_DUPLICATE_ACTIVE_SUBSCRIPTIONS'
1442
+ }));
1443
+ return;
1444
+ }
1445
+ let resolvedCustomerId = currentCustomerId || String(stripeAccountDiscovery.preferredCustomerId || '').trim();
1446
+ if (!currentSubscriptionId && stripeAccountDiscovery.activeSubscriptions.length === 1) {
1447
+ const activeSubscription = stripeAccountDiscovery.activeSubscriptions[0];
1448
+ currentSubscriptionId = String(activeSubscription.subscription?.id || '').trim();
1449
+ resolvedCustomerId = String(activeSubscription.customerId || '').trim() || resolvedCustomerId;
1450
+ if (currentSubscriptionId || resolvedCustomerId) {
1451
+ upsertBillingMirror(core, {
1452
+ userId,
1453
+ stripeCustomerId: resolvedCustomerId || null,
1454
+ stripeSubscriptionId: currentSubscriptionId || null
1455
+ });
1456
+ }
1457
+ }
1458
+ else if (!currentCustomerId && resolvedCustomerId) {
1459
+ upsertBillingMirror(core, {
1460
+ userId,
1461
+ stripeCustomerId: resolvedCustomerId
1462
+ });
1463
+ }
1464
+ if (currentSubscriptionId && currentPlanVersionId === planVersionId && currentInterval === interval) {
1465
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1466
+ res.end(JSON.stringify({
1467
+ checkoutState: 'already_current_plan',
1468
+ planVersionId,
1469
+ message: 'You are already on this plan.'
1470
+ }));
1471
+ return;
1472
+ }
1473
+ if (currentSubscriptionId) {
1474
+ const current = await stripe.subscriptions.retrieve(currentSubscriptionId);
1475
+ const itemId = current.items.data[0]?.id;
1476
+ if (!itemId) {
1477
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1478
+ res.end(JSON.stringify({ error: 'Subscription has no updatable line item.' }));
1479
+ return;
1480
+ }
1481
+ const updated = await stripe.subscriptions.update(currentSubscriptionId, {
1482
+ items: [{ id: itemId, price: priceId }],
1483
+ proration_behavior: 'create_prorations',
1484
+ metadata: {
1485
+ userId,
1486
+ tenantId: core.getTenantId(),
1487
+ planVersionId,
1488
+ billingInterval: interval
1489
+ }
1490
+ });
1491
+ applySubscriptionSnapshot(core, {
1492
+ userId,
1493
+ subscription: updated,
1494
+ source: 'checkout-confirm'
1495
+ });
1496
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1497
+ res.end(JSON.stringify({
1498
+ checkoutState: 'updated',
1499
+ planVersionId,
1500
+ message: 'Your subscription was updated successfully.'
1501
+ }));
1502
+ return;
1503
+ }
1504
+ const baseUrl = resolveBaseUrl(req);
1505
+ const config = readBillingConfig(core);
1506
+ const planId = resolvePlanIdForVersion(core, planVersionId);
1507
+ const selectedVersion = planId
1508
+ ? core.listPlanVersions(planId).find((version) => version.planVersionId === planVersionId) || null
1509
+ : null;
1510
+ const cancelParams = new URLSearchParams({
1511
+ screen: 'plans',
1512
+ checkout: 'cancel',
1513
+ planId: String(planId || ''),
1514
+ planVersionId,
1515
+ interval
1516
+ });
1517
+ const session = await stripe.checkout.sessions.create({
1518
+ mode: 'subscription',
1519
+ line_items: [{ price: priceId, quantity: 1 }],
1520
+ success_url: `${baseUrl}/?screen=plans&checkout=success&session_id={CHECKOUT_SESSION_ID}`,
1521
+ cancel_url: `${baseUrl}/?${cancelParams.toString()}`,
1522
+ ...(resolvedCustomerId
1523
+ ? { customer: resolvedCustomerId }
1524
+ : (normalizedUserEmail ? { customer_email: normalizedUserEmail } : {})),
1525
+ subscription_data: {
1526
+ ...(selectedVersion && selectedVersion.trialDays > 0 ? { trial_period_days: selectedVersion.trialDays } : {}),
1527
+ metadata: {
1528
+ userId,
1529
+ tenantId: core.getTenantId(),
1530
+ planVersionId,
1531
+ billingInterval: interval
1532
+ }
1533
+ },
1534
+ billing_address_collection: config.billingAddress,
1535
+ automatic_tax: { enabled: config.taxEnabled },
1536
+ client_reference_id: userId,
1537
+ metadata: {
1538
+ userId,
1539
+ tenantId: core.getTenantId(),
1540
+ planVersionId,
1541
+ billingInterval: interval
1542
+ }
1543
+ });
1544
+ const checkoutUrl = isMockStripeClient(stripe)
1545
+ ? buildMockStripeHostedCheckoutUrl(resolveHostedServerBaseUrl(req), session.id)
1546
+ : (session.url || null);
1547
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1548
+ res.end(JSON.stringify({
1549
+ checkoutState: 'checkout',
1550
+ url: checkoutUrl,
1551
+ id: session.id,
1552
+ returnBaseUrl: baseUrl
1553
+ }));
1554
+ }
1555
+ catch (error) {
1556
+ if (isMissingStripeCustomerError(error)) {
1557
+ res.writeHead(409, { 'Content-Type': 'application/json' });
1558
+ res.end(JSON.stringify({
1559
+ error: 'The saved Stripe customer for this account could not be found. Please contact support before retrying checkout.',
1560
+ code: 'BILLING_STRIPE_CUSTOMER_MISSING'
1561
+ }));
1562
+ return;
1563
+ }
1564
+ const statusCode = resolveErrorStatusCode(error, 400);
1565
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
1566
+ res.end(JSON.stringify({ error: String(error?.message || 'Unable to create checkout session'), code: error?.code }));
1567
+ }
1568
+ });
1569
+ addRoute('POST', '/api/taskforce/billing/checkout-session/confirm', async (req, res) => {
1570
+ if (!ensureCloudBilling(req, res))
1571
+ return;
1572
+ if (!ensureAuthenticatedUser(req)) {
1573
+ res.writeHead(401, { 'Content-Type': 'application/json' });
1574
+ res.end(JSON.stringify({ error: 'Authentication required.' }));
1575
+ return;
1576
+ }
1577
+ const body = await parseJsonBody(req);
1578
+ const sessionId = String(body?.sessionId || '').trim();
1579
+ if (!sessionId) {
1580
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1581
+ res.end(JSON.stringify({ error: 'sessionId is required.' }));
1582
+ return;
1583
+ }
1584
+ try {
1585
+ const stripe = getStripeClient();
1586
+ const expectedUserId = requestUserId(req);
1587
+ const session = await stripe.checkout.sessions.retrieve(sessionId, {
1588
+ expand: ['subscription']
1589
+ });
1590
+ const metadata = session.metadata || {};
1591
+ const customerId = String(session.customer || '').trim();
1592
+ const subscriptionId = typeof session.subscription === 'string'
1593
+ ? session.subscription.trim()
1594
+ : String(session.subscription?.id || '').trim();
1595
+ const resolvedUserId = String(metadata.userId || metadata.user_id || '').trim()
1596
+ || resolveUserIdByCustomer(core, customerId)
1597
+ || resolveUserIdBySubscription(core, subscriptionId);
1598
+ if (!resolvedUserId) {
1599
+ res.writeHead(409, { 'Content-Type': 'application/json' });
1600
+ res.end(JSON.stringify({ error: 'Unable to resolve checkout session to an account.' }));
1601
+ return;
1602
+ }
1603
+ if (resolvedUserId !== expectedUserId) {
1604
+ res.writeHead(403, { 'Content-Type': 'application/json' });
1605
+ res.end(JSON.stringify({ error: 'Checkout session does not belong to the current account.' }));
1606
+ return;
1607
+ }
1608
+ core.createUserBillingRecord(resolvedUserId);
1609
+ upsertBillingMirror(core, {
1610
+ userId: resolvedUserId,
1611
+ stripeCustomerId: customerId || null,
1612
+ stripeSubscriptionId: subscriptionId || null
1613
+ });
1614
+ let resolvedPlanVersionId = null;
1615
+ if (session.mode === 'subscription' && subscriptionId) {
1616
+ const subscription = typeof session.subscription === 'string' || !session.subscription
1617
+ ? await stripe.subscriptions.retrieve(subscriptionId)
1618
+ : session.subscription;
1619
+ resolvedPlanVersionId = applySubscriptionSnapshot(core, {
1620
+ userId: resolvedUserId,
1621
+ subscription,
1622
+ source: 'checkout-confirm'
1623
+ }).planVersionId;
1624
+ }
1625
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1626
+ res.end(JSON.stringify({
1627
+ success: true,
1628
+ sessionId,
1629
+ planVersionId: resolvedPlanVersionId
1630
+ }));
1631
+ }
1632
+ catch (error) {
1633
+ const statusCode = resolveErrorStatusCode(error, 400);
1634
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
1635
+ res.end(JSON.stringify({ error: String(error?.message || 'Unable to confirm checkout session'), code: error?.code }));
1636
+ }
1637
+ });
1638
+ addRoute('POST', '/api/taskforce/billing/portal-session', async (req, res) => {
1639
+ if (!ensureCloudBilling(req, res))
1640
+ return;
1641
+ if (!ensureAuthenticatedUser(req)) {
1642
+ res.writeHead(401, { 'Content-Type': 'application/json' });
1643
+ res.end(JSON.stringify({ error: 'Authentication required.' }));
1644
+ return;
1645
+ }
1646
+ const userId = requestUserId(req);
1647
+ const db = core.getDatabaseAdapter();
1648
+ const tenantId = core.getTenantId();
1649
+ const row = db.prepare(`
1650
+ SELECT stripe_customer_id
1651
+ FROM user_billing
1652
+ WHERE tenant_id = ? AND user_id = ?
1653
+ LIMIT 1
1654
+ `).get(tenantId, userId);
1655
+ let customerId = String(row?.stripe_customer_id || '').trim();
1656
+ if (!customerId) {
1657
+ const userIdentity = core.resolveUserIdentity(userId);
1658
+ const discovery = await discoverStripeAccountStateForUser(core, {
1659
+ userId,
1660
+ normalizedEmail: userIdentity?.email,
1661
+ currentCustomerId: null
1662
+ });
1663
+ if (discovery.activeSubscriptions.length > 1) {
1664
+ res.writeHead(409, { 'Content-Type': 'application/json' });
1665
+ res.end(JSON.stringify({
1666
+ error: 'Multiple active subscriptions are already linked to this account. Please contact support before opening the billing portal.',
1667
+ code: 'BILLING_DUPLICATE_ACTIVE_SUBSCRIPTIONS'
1668
+ }));
1669
+ return;
1670
+ }
1671
+ customerId = String(discovery.preferredCustomerId || '').trim();
1672
+ if (customerId) {
1673
+ upsertBillingMirror(core, {
1674
+ userId,
1675
+ stripeCustomerId: customerId
1676
+ });
1677
+ }
1678
+ }
1679
+ if (!customerId) {
1680
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1681
+ res.end(JSON.stringify({ error: 'No Stripe customer is linked for this account.' }));
1682
+ return;
1683
+ }
1684
+ try {
1685
+ const stripe = getStripeClient();
1686
+ const baseUrl = resolveBaseUrl(req);
1687
+ const session = await stripe.billingPortal.sessions.create({
1688
+ customer: customerId,
1689
+ return_url: `${baseUrl}/settings`
1690
+ });
1691
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1692
+ res.end(JSON.stringify({ url: session.url, returnBaseUrl: baseUrl }));
1693
+ }
1694
+ catch (error) {
1695
+ const statusCode = resolveErrorStatusCode(error, 400);
1696
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
1697
+ res.end(JSON.stringify({ error: String(error?.message || 'Unable to create portal session'), code: error?.code }));
1698
+ }
1699
+ });
1700
+ addRoute('POST', '/api/taskforce/billing/subscription', async (req, res) => {
1701
+ if (!ensureCloudBilling(req, res))
1702
+ return;
1703
+ if (!ensureAuthenticatedUser(req)) {
1704
+ res.writeHead(401, { 'Content-Type': 'application/json' });
1705
+ res.end(JSON.stringify({ error: 'Authentication required.' }));
1706
+ return;
1707
+ }
1708
+ const body = await parseJsonBody(req);
1709
+ const interval = parseInterval(body?.interval);
1710
+ const planVersionId = String(body?.planVersionId || '').trim();
1711
+ if (!interval || !planVersionId) {
1712
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1713
+ res.end(JSON.stringify({ error: 'planVersionId and interval are required.' }));
1714
+ return;
1715
+ }
1716
+ const nextPriceId = resolveStripePriceIdForPlanVersion(core, planVersionId, interval);
1717
+ if (!nextPriceId) {
1718
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1719
+ res.end(JSON.stringify({ error: 'No active Stripe price mapping found for requested plan/interval.' }));
1720
+ return;
1721
+ }
1722
+ const userId = requestUserId(req);
1723
+ const db = core.getDatabaseAdapter();
1724
+ const tenantId = core.getTenantId();
1725
+ const row = db.prepare(`
1726
+ SELECT stripe_subscription_id
1727
+ FROM user_billing
1728
+ WHERE tenant_id = ? AND user_id = ?
1729
+ LIMIT 1
1730
+ `).get(tenantId, userId);
1731
+ const subscriptionId = String(row?.stripe_subscription_id || '').trim();
1732
+ if (!subscriptionId) {
1733
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1734
+ res.end(JSON.stringify({ error: 'No Stripe subscription is linked for this account.' }));
1735
+ return;
1736
+ }
1737
+ try {
1738
+ const stripe = getStripeClient();
1739
+ const current = await stripe.subscriptions.retrieve(subscriptionId);
1740
+ const itemId = current.items.data[0]?.id;
1741
+ if (!itemId) {
1742
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1743
+ res.end(JSON.stringify({ error: 'Subscription has no updatable line item.' }));
1744
+ return;
1745
+ }
1746
+ const updated = await stripe.subscriptions.update(subscriptionId, {
1747
+ items: [{ id: itemId, price: nextPriceId }],
1748
+ proration_behavior: 'create_prorations',
1749
+ metadata: {
1750
+ planVersionId
1751
+ }
1752
+ });
1753
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1754
+ res.end(JSON.stringify({ subscription: updated }));
1755
+ }
1756
+ catch (error) {
1757
+ const statusCode = resolveErrorStatusCode(error, 400);
1758
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
1759
+ res.end(JSON.stringify({ error: String(error?.message || 'Unable to update subscription'), code: error?.code }));
1760
+ }
1761
+ });
1762
+ addRoute('POST', '/api/taskforce/billing/webhook', async (req, res) => {
1763
+ if (!ensureCloudBilling(req, res))
1764
+ return;
1765
+ const webhookSecret = String(process.env.TASKFORCE_STRIPE_WEBHOOK_SECRET || '').trim();
1766
+ if (!webhookSecret) {
1767
+ res.writeHead(500, { 'Content-Type': 'application/json' });
1768
+ res.end(JSON.stringify({ error: 'TASKFORCE_STRIPE_WEBHOOK_SECRET is not configured.' }));
1769
+ return;
1770
+ }
1771
+ const signature = String(req.headers['stripe-signature'] || '').trim();
1772
+ if (!signature) {
1773
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1774
+ res.end(JSON.stringify({ error: 'Missing Stripe signature.' }));
1775
+ return;
1776
+ }
1777
+ let event;
1778
+ let rawBody;
1779
+ try {
1780
+ rawBody = await readRawBody(req);
1781
+ const stripe = getStripeClient();
1782
+ event = stripe.webhooks.constructEvent(rawBody, signature, webhookSecret);
1783
+ }
1784
+ catch (error) {
1785
+ const code = Number(error?.statusCode || 400);
1786
+ res.writeHead(code >= 400 && code < 600 ? code : 400, { 'Content-Type': 'application/json' });
1787
+ res.end(JSON.stringify({ error: String(error?.message || 'Invalid webhook payload/signature') }));
1788
+ return;
1789
+ }
1790
+ try {
1791
+ const db = core.getDatabaseAdapter();
1792
+ const tenantId = core.getTenantId();
1793
+ purgeExpiredWebhookFailures(core);
1794
+ let duplicate = false;
1795
+ const tx = db.transaction(() => {
1796
+ const acquired = lockWebhookEvent(core, event);
1797
+ if (!acquired) {
1798
+ duplicate = true;
1799
+ return;
1800
+ }
1801
+ if (event.type === 'checkout.session.completed') {
1802
+ const session = event.data.object;
1803
+ const metadata = session.metadata || {};
1804
+ const customerId = String(session.customer || '').trim();
1805
+ const subscriptionId = String(session.subscription || '').trim();
1806
+ const userId = String(metadata.userId || metadata.user_id || '').trim()
1807
+ || resolveUserIdByCustomer(core, customerId)
1808
+ || resolveUserIdBySubscription(core, subscriptionId);
1809
+ if (!userId || !customerId) {
1810
+ throw new Error('Unable to link checkout completion to user/customer.');
1811
+ }
1812
+ core.createUserBillingRecord(userId);
1813
+ upsertBillingMirror(core, {
1814
+ userId,
1815
+ stripeCustomerId: customerId,
1816
+ stripeSubscriptionId: subscriptionId || null
1817
+ });
1818
+ return;
1819
+ }
1820
+ if (event.type === 'customer.subscription.created' || event.type === 'customer.subscription.updated' || event.type === 'customer.subscription.deleted') {
1821
+ const subscription = event.data.object;
1822
+ const customerId = String(subscription.customer || '').trim();
1823
+ const userId = resolveUserIdByCustomer(core, customerId)
1824
+ || String(subscription.metadata?.userId || subscription.metadata?.user_id || '').trim();
1825
+ if (!userId) {
1826
+ throw new Error('Unable to resolve user for subscription event.');
1827
+ }
1828
+ core.createUserBillingRecord(userId);
1829
+ const priceId = String(subscription.items?.data?.[0]?.price?.id || '').trim();
1830
+ const planVersionResolution = resolvePlanVersionIdForBillingEvent(core, [
1831
+ subscription.metadata?.planVersionId,
1832
+ subscription.metadata?.plan_version_id
1833
+ ], priceId);
1834
+ const planVersionId = planVersionResolution.planVersionId;
1835
+ if (!planVersionId) {
1836
+ recordWebhookFailure(core, {
1837
+ eventId: event.id,
1838
+ eventType: event.type,
1839
+ reason: planVersionResolution.reason === 'ambiguous'
1840
+ ? `Ambiguous price mapping for price_id=${priceId || '(empty)'}`
1841
+ : `Unknown price mapping for price_id=${priceId || '(empty)'}`,
1842
+ payload: event
1843
+ });
1844
+ return;
1845
+ }
1846
+ applySubscriptionSnapshot(core, {
1847
+ userId,
1848
+ subscription,
1849
+ source: 'stripe-webhook'
1850
+ });
1851
+ return;
1852
+ }
1853
+ if (event.type === 'invoice.paid' || event.type === 'invoice.payment_failed') {
1854
+ const invoice = event.data.object;
1855
+ const customerId = String(invoice.customer || '').trim();
1856
+ const subscriptionId = resolveInvoiceSubscriptionId(invoice);
1857
+ const userId = resolveUserIdByCustomer(core, customerId)
1858
+ || resolveUserIdBySubscription(core, subscriptionId);
1859
+ if (!userId) {
1860
+ throw new Error('Unable to resolve user for invoice event.');
1861
+ }
1862
+ core.createUserBillingRecord(userId);
1863
+ const { priceId, interval } = resolvePriceAndIntervalFromInvoice(invoice);
1864
+ const planVersionResolution = resolvePlanVersionIdForBillingEvent(core, [
1865
+ invoice.parent?.subscription_details?.metadata?.planVersionId,
1866
+ invoice.parent?.subscription_details?.metadata?.plan_version_id,
1867
+ invoice.subscription_details?.metadata?.planVersionId,
1868
+ invoice.subscription_details?.metadata?.plan_version_id
1869
+ ], priceId);
1870
+ const planVersionId = planVersionResolution.planVersionId;
1871
+ const existingEntitlement = getEntitlementRow(core, userId);
1872
+ const canonicalState = event.type === 'invoice.payment_failed' ? 'grace' : 'active';
1873
+ const currentPeriodEnd = invoice.lines?.data?.[0]?.period?.end
1874
+ ? isoFromUnixSeconds(invoice.lines.data[0].period.end)
1875
+ : null;
1876
+ upsertBillingMirror(core, {
1877
+ userId,
1878
+ stripeCustomerId: customerId || null,
1879
+ stripeSubscriptionId: subscriptionId || null,
1880
+ stripePriceId: priceId || null,
1881
+ stripeStatus: event.type === 'invoice.payment_failed' ? 'past_due' : 'active',
1882
+ planId: planVersionId ? resolvePlanIdForVersion(core, planVersionId) : null,
1883
+ billingInterval: interval || null,
1884
+ canonicalState,
1885
+ currentPeriodEnd,
1886
+ effectiveUntil: currentPeriodEnd
1887
+ });
1888
+ if (!planVersionId) {
1889
+ recordWebhookFailure(core, {
1890
+ eventId: event.id,
1891
+ eventType: event.type,
1892
+ reason: planVersionResolution.reason === 'ambiguous'
1893
+ ? `Ambiguous price mapping for price_id=${priceId || '(empty)'}`
1894
+ : `Unknown price mapping for price_id=${priceId || '(empty)'}`,
1895
+ payload: event
1896
+ });
1897
+ return;
1898
+ }
1899
+ upsertCanonicalEntitlement(core, {
1900
+ userId,
1901
+ planVersionId: planVersionId || existingEntitlement?.plan_version_id || core.getDefaultPlanVersionIdByMode('personal', { allowFallback: true }),
1902
+ state: canonicalState,
1903
+ effectiveAt: new Date().toISOString(),
1904
+ effectiveUntil: currentPeriodEnd
1905
+ });
1906
+ return;
1907
+ }
1908
+ });
1909
+ tx();
1910
+ if (duplicate) {
1911
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1912
+ res.end(JSON.stringify({ received: true, duplicate: true }));
1913
+ return;
1914
+ }
1915
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1916
+ res.end(JSON.stringify({ received: true }));
1917
+ }
1918
+ catch (error) {
1919
+ recordWebhookFailure(core, {
1920
+ eventId: event.id,
1921
+ eventType: event.type,
1922
+ reason: String(error?.message || error || 'WEBHOOK_PROCESSING_FAILED'),
1923
+ payload: event
1924
+ });
1925
+ res.writeHead(500, { 'Content-Type': 'application/json' });
1926
+ res.end(JSON.stringify({ error: 'Webhook processing failed. Retry expected.' }));
1927
+ }
1928
+ });
1929
+ addRoute('GET', '/api/taskforce/public/pricing', async (req, res) => {
1930
+ if (!ensureCloudPricingRoute(req, res))
1931
+ return;
1932
+ try {
1933
+ const db = core.getDatabaseAdapter();
1934
+ const tenantId = core.getTenantId();
1935
+ // Get all active plans
1936
+ const activePlans = core.listPlanCatalog().filter(p => p.status === 'active');
1937
+ // Get all active price mappings with their amounts
1938
+ const activePrices = db.prepare(`
1939
+ SELECT plan_version_id, billing_interval, stripe_price_id, pricing_type, unit_amount, currency
1940
+ FROM billing_price_mappings
1941
+ WHERE tenant_id = ? AND active = 1
1942
+ `).all(tenantId);
1943
+ const plansPayload = activePlans.map((plan) => {
1944
+ const versions = core.listPlanVersions(plan.planId);
1945
+ // Prefer default, else fallback to highest version number
1946
+ const defaultVersion = versions.find(v => v.isDefault) || [...versions].sort((a, b) => b.versionNumber - a.versionNumber)[0];
1947
+ if (!defaultVersion) {
1948
+ return null; // Skip if no valid version exists (edge case)
1949
+ }
1950
+ const monthPrice = activePrices.find(p => p.plan_version_id === defaultVersion.planVersionId && p.billing_interval === 'month');
1951
+ const yearPrice = activePrices.find(p => p.plan_version_id === defaultVersion.planVersionId && p.billing_interval === 'year');
1952
+ return {
1953
+ planId: plan.planId,
1954
+ displayName: plan.displayName,
1955
+ status: plan.status,
1956
+ defaultVersion: {
1957
+ planVersionId: defaultVersion.planVersionId,
1958
+ versionNumber: defaultVersion.versionNumber,
1959
+ seatLimit: defaultVersion.seatLimit,
1960
+ trialDays: defaultVersion.trialDays,
1961
+ metadata: defaultVersion.metadata
1962
+ },
1963
+ features: Object.values(defaultVersion.features || {}).filter((feature) => {
1964
+ const access = String(feature?.access || '').trim().toLowerCase();
1965
+ return access !== 'disabled';
1966
+ }),
1967
+ pricing: {
1968
+ month: monthPrice ? {
1969
+ pricingType: parsePricingType(monthPrice.pricing_type) || 'stripe',
1970
+ stripePriceId: (parsePricingType(monthPrice.pricing_type) || 'stripe') === 'stripe' ? monthPrice.stripe_price_id : null,
1971
+ unitAmount: (parsePricingType(monthPrice.pricing_type) || 'stripe') === 'free' ? 0 : monthPrice.unit_amount,
1972
+ currency: (parsePricingType(monthPrice.pricing_type) || 'stripe') === 'free' ? null : monthPrice.currency,
1973
+ interval: 'month'
1974
+ } : null,
1975
+ year: yearPrice ? {
1976
+ pricingType: parsePricingType(yearPrice.pricing_type) || 'stripe',
1977
+ stripePriceId: (parsePricingType(yearPrice.pricing_type) || 'stripe') === 'stripe' ? yearPrice.stripe_price_id : null,
1978
+ unitAmount: (parsePricingType(yearPrice.pricing_type) || 'stripe') === 'free' ? 0 : yearPrice.unit_amount,
1979
+ currency: (parsePricingType(yearPrice.pricing_type) || 'stripe') === 'free' ? null : yearPrice.currency,
1980
+ interval: 'year'
1981
+ } : null
1982
+ }
1983
+ };
1984
+ }).filter(Boolean);
1985
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1986
+ res.end(JSON.stringify({
1987
+ asOf: new Date().toISOString(),
1988
+ pricingSource: {
1989
+ environment: requestPricingEnvironment(req),
1990
+ runtimeMode: 'cloud'
1991
+ },
1992
+ plans: plansPayload
1993
+ }));
1994
+ }
1995
+ catch (error) {
1996
+ const statusCode = resolveErrorStatusCode(error, 500);
1997
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
1998
+ res.end(JSON.stringify({ error: String(error?.message || 'Unable to fetch pricing'), code: error?.code }));
1999
+ }
2000
+ });
2001
+ }