@taskforcehq/taskforce 0.3.168
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.
- package/README.md +241 -0
- package/dist/Taskforce.d.ts +3 -0
- package/dist/Taskforce.js +3 -0
- package/dist/Taskforce.module.css +7587 -0
- package/dist/TaskforceCore.CategoryFallback.test.d.ts +1 -0
- package/dist/TaskforceCore.CategoryFallback.test.js +98 -0
- package/dist/TaskforceCore.d.ts +12 -0
- package/dist/TaskforceCore.js +729 -0
- package/dist/TaskforceCore.test.d.ts +1 -0
- package/dist/TaskforceCore.test.js +2132 -0
- package/dist/TaskforceScreenshot.d.ts +11 -0
- package/dist/TaskforceScreenshot.js +113 -0
- package/dist/TaskforceWidget.d.ts +6 -0
- package/dist/TaskforceWidget.js +101 -0
- package/dist/TaskforceWidget.test.d.ts +1 -0
- package/dist/TaskforceWidget.test.js +92 -0
- package/dist/UnsavedChangesRelaxed.test.d.ts +1 -0
- package/dist/UnsavedChangesRelaxed.test.js +16 -0
- package/dist/__tests__/localizationCatalogs.test.d.ts +1 -0
- package/dist/__tests__/localizationCatalogs.test.js +37 -0
- package/dist/__tests__/planParser.test.d.ts +1 -0
- package/dist/__tests__/planParser.test.js +183 -0
- package/dist/__tests__/releaseVersion.test.d.ts +1 -0
- package/dist/__tests__/releaseVersion.test.js +32 -0
- package/dist/__tests__/structuredCommentValidation.test.d.ts +1 -0
- package/dist/__tests__/structuredCommentValidation.test.js +31 -0
- package/dist/__tests__/structuredDocValidation.test.d.ts +1 -0
- package/dist/__tests__/structuredDocValidation.test.js +30 -0
- package/dist/__tests__/templateEngine.test.d.ts +1 -0
- package/dist/__tests__/templateEngine.test.js +150 -0
- package/dist/__tests__/templates.test.d.ts +1 -0
- package/dist/__tests__/templates.test.js +45 -0
- package/dist/__tests__/workflowDocumentFixtures.test.d.ts +1 -0
- package/dist/__tests__/workflowDocumentFixtures.test.js +78 -0
- package/dist/__tests__/workflowExportMatrix.test.d.ts +1 -0
- package/dist/__tests__/workflowExportMatrix.test.js +113 -0
- package/dist/__tests__/workflowExportSnapshots.test.d.ts +1 -0
- package/dist/__tests__/workflowExportSnapshots.test.js +28 -0
- package/dist/__tests__/workflowGeneration.test.d.ts +1 -0
- package/dist/__tests__/workflowGeneration.test.js +12 -0
- package/dist/__tests__/workflowSimulation.test.d.ts +1 -0
- package/dist/__tests__/workflowSimulation.test.js +113 -0
- package/dist/__tests__/workflowV2Policy.test.d.ts +1 -0
- package/dist/__tests__/workflowV2Policy.test.js +80 -0
- package/dist/api.d.ts +30 -0
- package/dist/api.js +85 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +217 -0
- package/dist/compat/priorityCompat.d.ts +2 -0
- package/dist/compat/priorityCompat.js +24 -0
- package/dist/components/features/DocumentGenerateModal.d.ts +9 -0
- package/dist/components/features/DocumentGenerateModal.js +114 -0
- package/dist/components/features/DocumentIndex.d.ts +19 -0
- package/dist/components/features/DocumentIndex.js +51 -0
- package/dist/components/features/DocumentViewer.d.ts +9 -0
- package/dist/components/features/DocumentViewer.js +253 -0
- package/dist/components/features/DocumentWorkspace.d.ts +21 -0
- package/dist/components/features/DocumentWorkspace.js +88 -0
- package/dist/components/features/TaskSettings.d.ts +209 -0
- package/dist/components/features/TaskSettings.js +500 -0
- package/dist/components/features/TaskSettings.test.d.ts +1 -0
- package/dist/components/features/TaskSettings.test.js +308 -0
- package/dist/components/features/settings/CategoryManager.d.ts +19 -0
- package/dist/components/features/settings/CategoryManager.js +113 -0
- package/dist/components/features/settings/TaxonomyEditor.d.ts +8 -0
- package/dist/components/features/settings/TaxonomyEditor.js +98 -0
- package/dist/components/features/settings/TaxonomyManager 2.d.ts +52 -0
- package/dist/components/features/settings/TaxonomyManager 2.js +75 -0
- package/dist/components/features/settings/TaxonomyManager.d.ts +54 -0
- package/dist/components/features/settings/TaxonomyManager.js +77 -0
- package/dist/components/features/settings/TypeManager.d.ts +10 -0
- package/dist/components/features/settings/TypeManager.js +50 -0
- package/dist/components/features/settings/WorkflowEditorPrototype.d.ts +15 -0
- package/dist/components/features/settings/WorkflowEditorPrototype.js +245 -0
- package/dist/components/task/TaskActionHeader.d.ts +20 -0
- package/dist/components/task/TaskActionHeader.js +13 -0
- package/dist/components/task/TaskCard.d.ts +47 -0
- package/dist/components/task/TaskCard.js +326 -0
- package/dist/components/task/TaskCard.test.d.ts +1 -0
- package/dist/components/task/TaskCard.test.js +95 -0
- package/dist/components/task/TaskContextUpload.d.ts +12 -0
- package/dist/components/task/TaskContextUpload.js +393 -0
- package/dist/components/task/TaskContextUpload.test.d.ts +1 -0
- package/dist/components/task/TaskContextUpload.test.js +189 -0
- package/dist/components/task/TaskForm.d.ts +105 -0
- package/dist/components/task/TaskForm.js +241 -0
- package/dist/components/task/TaskForm.test.d.ts +1 -0
- package/dist/components/task/TaskForm.test.js +288 -0
- package/dist/components/task/TaskKanban 2.d.ts +76 -0
- package/dist/components/task/TaskKanban 2.js +1004 -0
- package/dist/components/task/TaskKanban.d.ts +76 -0
- package/dist/components/task/TaskKanban.empty-column-drop.test.d.ts +1 -0
- package/dist/components/task/TaskKanban.empty-column-drop.test.js +66 -0
- package/dist/components/task/TaskKanban.js +1026 -0
- package/dist/components/task/TaskKanban.test.d.ts +1 -0
- package/dist/components/task/TaskKanban.test.js +46 -0
- package/dist/components/task/TaskList.d.ts +74 -0
- package/dist/components/task/TaskList.js +38 -0
- package/dist/components/task/TaskList.test.d.ts +1 -0
- package/dist/components/task/TaskList.test.js +270 -0
- package/dist/components/task/TaskScreenshotSection.d.ts +15 -0
- package/dist/components/task/TaskScreenshotSection.js +22 -0
- package/dist/components/ui/LevelSelector.d.ts +13 -0
- package/dist/components/ui/LevelSelector.js +25 -0
- package/dist/components/ui/LevelSelector.test.d.ts +1 -0
- package/dist/components/ui/LevelSelector.test.js +55 -0
- package/dist/components/ui/Markdown.d.ts +6 -0
- package/dist/components/ui/Markdown.js +94 -0
- package/dist/components/ui/Markdown.test.d.ts +1 -0
- package/dist/components/ui/Markdown.test.js +38 -0
- package/dist/components/ui/Modal.d.ts +16 -0
- package/dist/components/ui/Modal.js +49 -0
- package/dist/components/ui/MultiSelectFilter.d.ts +10 -0
- package/dist/components/ui/MultiSelectFilter.js +71 -0
- package/dist/components/ui/MultiSelectFilter.test.d.ts +1 -0
- package/dist/components/ui/MultiSelectFilter.test.js +74 -0
- package/dist/components/ui/TaxonomyDropdown.d.ts +17 -0
- package/dist/components/ui/TaxonomyDropdown.js +145 -0
- package/dist/components/ui/TaxonomyDropdown.test.d.ts +1 -0
- package/dist/components/ui/TaxonomyDropdown.test.js +130 -0
- package/dist/components/views/StandaloneLayout 2.d.ts +8 -0
- package/dist/components/views/StandaloneLayout 2.js +1660 -0
- package/dist/components/views/StandaloneLayout.d.ts +8 -0
- package/dist/components/views/StandaloneLayout.js +2117 -0
- package/dist/components/views/WidgetView.d.ts +8 -0
- package/dist/components/views/WidgetView.js +221 -0
- package/dist/config/envSchema.d.ts +28 -0
- package/dist/config/envSchema.js +374 -0
- package/dist/config/envSchema.test.d.ts +1 -0
- package/dist/config/envSchema.test.js +97 -0
- package/dist/config/surfaceBase.d.ts +2 -0
- package/dist/config/surfaceBase.js +8 -0
- package/dist/core/BulkUpdateFieldsPreview.test.d.ts +1 -0
- package/dist/core/BulkUpdateFieldsPreview.test.js +62 -0
- package/dist/core/CompletedAt.test.d.ts +1 -0
- package/dist/core/CompletedAt.test.js +44 -0
- package/dist/core/CreatedBy.test.d.ts +1 -0
- package/dist/core/CreatedBy.test.js +60 -0
- package/dist/core/EntitlementsPolicy.test.d.ts +1 -0
- package/dist/core/EntitlementsPolicy.test.js +145 -0
- package/dist/core/InitiativeTemplates.test.d.ts +1 -0
- package/dist/core/InitiativeTemplates.test.js +50 -0
- package/dist/core/ParentLifecycle.test.d.ts +1 -0
- package/dist/core/ParentLifecycle.test.js +98 -0
- package/dist/core/ScheduleFields.test.d.ts +1 -0
- package/dist/core/ScheduleFields.test.js +197 -0
- package/dist/core/SetupState.test.d.ts +1 -0
- package/dist/core/SetupState.test.js +113 -0
- package/dist/core/SystemAdmin.test.d.ts +1 -0
- package/dist/core/SystemAdmin.test.js +295 -0
- package/dist/core/TaskTaxonomyValidation.test.d.ts +1 -0
- package/dist/core/TaskTaxonomyValidation.test.js +64 -0
- package/dist/core/Taskforce.d.ts +1063 -0
- package/dist/core/Taskforce.ids.test.d.ts +1 -0
- package/dist/core/Taskforce.ids.test.js +168 -0
- package/dist/core/Taskforce.js +5981 -0
- package/dist/core/Taskforce.realtimeMutation.test.d.ts +1 -0
- package/dist/core/Taskforce.realtimeMutation.test.js +76 -0
- package/dist/core/WorkspaceDelete.test.d.ts +1 -0
- package/dist/core/WorkspaceDelete.test.js +140 -0
- package/dist/core/WorkspacePermissions.test.d.ts +1 -0
- package/dist/core/WorkspacePermissions.test.js +151 -0
- package/dist/core/WorkspacePlanMode.test.d.ts +1 -0
- package/dist/core/WorkspacePlanMode.test.js +104 -0
- package/dist/hooks/authGuardPolicy.d.ts +26 -0
- package/dist/hooks/authGuardPolicy.js +32 -0
- package/dist/hooks/authGuardPolicy.test.d.ts +1 -0
- package/dist/hooks/authGuardPolicy.test.js +76 -0
- package/dist/hooks/useDraggable.d.ts +12 -0
- package/dist/hooks/useDraggable.js +65 -0
- package/dist/hooks/useRealtimeSync.d.ts +24 -0
- package/dist/hooks/useRealtimeSync.js +275 -0
- package/dist/hooks/useRealtimeSync.test.d.ts +1 -0
- package/dist/hooks/useRealtimeSync.test.js +284 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +164 -0
- package/dist/hooks/useSyncOrchestrator.js +1427 -0
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.d.ts +1 -0
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +367 -0
- package/dist/hooks/useTaskforce.d.ts +727 -0
- package/dist/hooks/useTaskforce.js +4396 -0
- package/dist/hooks/useTaskforce.priority-filters.test.d.ts +1 -0
- package/dist/hooks/useTaskforce.priority-filters.test.js +33 -0
- package/dist/hooks/useWorkspaceSyncController.d.ts +80 -0
- package/dist/hooks/useWorkspaceSyncController.js +598 -0
- package/dist/hooks/useWorkspaceSyncController.test.d.ts +1 -0
- package/dist/hooks/useWorkspaceSyncController.test.js +236 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +11 -0
- package/dist/localization/index.d.ts +8 -0
- package/dist/localization/index.js +81 -0
- package/dist/localization/locales/en-US.d.ts +151 -0
- package/dist/localization/locales/en-US.js +150 -0
- package/dist/localization/locales/es-419.d.ts +2 -0
- package/dist/localization/locales/es-419.js +150 -0
- package/dist/localization/locales/pt-BR.d.ts +2 -0
- package/dist/localization/locales/pt-BR.js +150 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +33 -0
- package/dist/mcp/optionResolution.d.ts +14 -0
- package/dist/mcp/optionResolution.js +126 -0
- package/dist/mcp/optionResolution.test.d.ts +1 -0
- package/dist/mcp/optionResolution.test.js +39 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +1793 -0
- package/dist/mcp/structuredCommentValidation.d.ts +6 -0
- package/dist/mcp/structuredCommentValidation.js +56 -0
- package/dist/mcp/structuredDocValidation.d.ts +6 -0
- package/dist/mcp/structuredDocValidation.js +75 -0
- package/dist/migrations/billingSchemaParity.test.d.ts +1 -0
- package/dist/migrations/billingSchemaParity.test.js +46 -0
- package/dist/migrations/clientMigrations.d.ts +9 -0
- package/dist/migrations/clientMigrations.js +74 -0
- package/dist/migrations/flags.d.ts +6 -0
- package/dist/migrations/flags.js +8 -0
- package/dist/migrations/taskMigrations.d.ts +71 -0
- package/dist/migrations/taskMigrations.js +236 -0
- package/dist/migrations/taskMigrations.test.d.ts +1 -0
- package/dist/migrations/taskMigrations.test.js +34 -0
- package/dist/migrations/taskSchemaMigrations.d.ts +11 -0
- package/dist/migrations/taskSchemaMigrations.js +453 -0
- package/dist/plugins/index.d.ts +10 -0
- package/dist/plugins/index.js +10 -0
- package/dist/plugins/vite.d.ts +19 -0
- package/dist/plugins/vite.js +271 -0
- package/dist/public/android-chrome-192x192.png +0 -0
- package/dist/public/android-chrome-512x512.png +0 -0
- package/dist/public/apple-touch-icon.png +0 -0
- package/dist/public/favicon-16x16.png +0 -0
- package/dist/public/favicon-32x32.png +0 -0
- package/dist/public/favicon.ico +0 -0
- package/dist/public/favicon.png +0 -0
- package/dist/public/site.webmanifest +1 -0
- package/dist/public/taskforce.png +0 -0
- package/dist/resources/templates/WORKFLOW_V2_POLICY.md +55 -0
- package/dist/resources/templates/environments/aider.yaml +14 -0
- package/dist/resources/templates/environments/antigravity.yaml +10 -0
- package/dist/resources/templates/environments/claude.yaml +10 -0
- package/dist/resources/templates/environments/cline.yaml +10 -0
- package/dist/resources/templates/environments/codex.yaml +13 -0
- package/dist/resources/templates/environments/copilot.yaml +10 -0
- package/dist/resources/templates/environments/cursor.yaml +9 -0
- package/dist/resources/templates/environments/gemini.yaml +9 -0
- package/dist/resources/templates/environments/windsurf.yaml +10 -0
- package/dist/resources/templates/workflow-sources/README.md +9 -0
- package/dist/resources/templates/workflow-sources/workflowDocs.mjs +520 -0
- package/dist/resources/templates/workflow-sources/workflowEditorOverrides.json +1 -0
- package/dist/resources/templates/workflows/collaborate.yaml +104 -0
- package/dist/resources/templates/workflows/do.yaml +82 -0
- package/dist/resources/templates/workflows/evaluate.yaml +111 -0
- package/dist/resources/templates/workflows/execute.yaml +142 -0
- package/dist/resources/templates/workflows/plan.yaml +112 -0
- package/dist/resources/templates/workflows/review.yaml +109 -0
- package/dist/runtime/capabilities.d.ts +8 -0
- package/dist/runtime/capabilities.js +16 -0
- package/dist/security/virusScan.d.ts +16 -0
- package/dist/security/virusScan.js +83 -0
- package/dist/server/auth.d.ts +51 -0
- package/dist/server/auth.js +266 -0
- package/dist/server/auth.test.d.ts +1 -0
- package/dist/server/auth.test.js +111 -0
- package/dist/server/buildInfo.d.ts +7 -0
- package/dist/server/buildInfo.js +100 -0
- package/dist/server/buildInfo.test.d.ts +1 -0
- package/dist/server/buildInfo.test.js +29 -0
- package/dist/server/cors.d.ts +7 -0
- package/dist/server/cors.js +76 -0
- package/dist/server/cors.test.d.ts +1 -0
- package/dist/server/cors.test.js +71 -0
- package/dist/server/email.d.ts +28 -0
- package/dist/server/email.js +138 -0
- package/dist/server/email.test.d.ts +1 -0
- package/dist/server/email.test.js +113 -0
- package/dist/server/index.cookieProxy.test.d.ts +1 -0
- package/dist/server/index.cookieProxy.test.js +19 -0
- package/dist/server/index.d.ts +17 -0
- package/dist/server/index.js +757 -0
- package/dist/server/index.rateLimit.test.d.ts +1 -0
- package/dist/server/index.rateLimit.test.js +13 -0
- package/dist/server/index.test.d.ts +1 -0
- package/dist/server/index.test.js +19 -0
- package/dist/server/rateLimit.d.ts +29 -0
- package/dist/server/rateLimit.js +99 -0
- package/dist/server/rateLimit.test.d.ts +1 -0
- package/dist/server/rateLimit.test.js +107 -0
- package/dist/server/realtimeSync.auth.test.d.ts +1 -0
- package/dist/server/realtimeSync.auth.test.js +62 -0
- package/dist/server/realtimeSync.replay.test.d.ts +1 -0
- package/dist/server/realtimeSync.replay.test.js +192 -0
- package/dist/server/realtimeSync.workspaceIsolation.test.d.ts +1 -0
- package/dist/server/realtimeSync.workspaceIsolation.test.js +101 -0
- package/dist/server/realtimeSyncWs.d.ts +30 -0
- package/dist/server/realtimeSyncWs.js +257 -0
- package/dist/server/routes.billing.d.ts +15 -0
- package/dist/server/routes.billing.js +991 -0
- package/dist/server/routes.billing.test.d.ts +1 -0
- package/dist/server/routes.billing.test.js +387 -0
- package/dist/server/routes.d.ts +72 -0
- package/dist/server/routes.js +4820 -0
- package/dist/server/routes.sync.d.ts +17 -0
- package/dist/server/routes.sync.integration.test.d.ts +1 -0
- package/dist/server/routes.sync.integration.test.js +470 -0
- package/dist/server/routes.sync.js +1141 -0
- package/dist/server/routes.test.d.ts +1 -0
- package/dist/server/routes.test.js +4945 -0
- package/dist/server/runtimeMode.d.ts +4 -0
- package/dist/server/runtimeMode.js +25 -0
- package/dist/server/runtimeMode.test.d.ts +1 -0
- package/dist/server/runtimeMode.test.js +26 -0
- package/dist/server/securityHeaders.d.ts +3 -0
- package/dist/server/securityHeaders.js +25 -0
- package/dist/server/securityHeaders.test.d.ts +1 -0
- package/dist/server/securityHeaders.test.js +32 -0
- package/dist/server/smtpTransport.d.ts +21 -0
- package/dist/server/smtpTransport.js +11 -0
- package/dist/server/start.d.ts +1 -0
- package/dist/server/start.js +17 -0
- package/dist/services/formatTransformers.d.ts +29 -0
- package/dist/services/formatTransformers.js +56 -0
- package/dist/services/templateEngine.d.ts +46 -0
- package/dist/services/templateEngine.js +148 -0
- package/dist/shared/runtimeContract.d.ts +12 -0
- package/dist/shared/runtimeContract.js +23 -0
- package/dist/shared/runtimeContract.test.d.ts +1 -0
- package/dist/shared/runtimeContract.test.js +23 -0
- package/dist/soak/preflight.cli.d.ts +1 -0
- package/dist/soak/preflight.cli.js +28 -0
- package/dist/soak/preflight.d.ts +26 -0
- package/dist/soak/preflight.js +113 -0
- package/dist/soak/preflight.test.d.ts +1 -0
- package/dist/soak/preflight.test.js +39 -0
- package/dist/soak/syncSoakHarness.d.ts +6 -0
- package/dist/soak/syncSoakHarness.js +10 -0
- package/dist/storage/createDatabaseAdapter.d.ts +2 -0
- package/dist/storage/createDatabaseAdapter.js +9 -0
- package/dist/storage/databaseAdapter.d.ts +22 -0
- package/dist/storage/databaseAdapter.js +1 -0
- package/dist/storage/documentIntegrity.d.ts +28 -0
- package/dist/storage/documentIntegrity.js +103 -0
- package/dist/storage/documentPurge.d.ts +14 -0
- package/dist/storage/documentPurge.js +46 -0
- package/dist/storage/documentRegistry.d.ts +39 -0
- package/dist/storage/documentRegistry.js +115 -0
- package/dist/storage/integritySchedule.d.ts +17 -0
- package/dist/storage/integritySchedule.js +62 -0
- package/dist/storage/objectStorage.d.ts +20 -0
- package/dist/storage/objectStorage.js +15 -0
- package/dist/storage/objectStorageClient.d.ts +18 -0
- package/dist/storage/objectStorageClient.js +301 -0
- package/dist/storage/objectStorageConfig.d.ts +2 -0
- package/dist/storage/objectStorageConfig.js +45 -0
- package/dist/storage/postgresAdapter.d.ts +20 -0
- package/dist/storage/postgresAdapter.js +204 -0
- package/dist/storage/postgresWorker.d.ts +1 -0
- package/dist/storage/postgresWorker.js +110 -0
- package/dist/storage/providerConfig.d.ts +6 -0
- package/dist/storage/providerConfig.js +11 -0
- package/dist/storage/sqliteAdapter.d.ts +14 -0
- package/dist/storage/sqliteAdapter.js +28 -0
- package/dist/storage/taskAssetStore.d.ts +56 -0
- package/dist/storage/taskAssetStore.js +297 -0
- package/dist/sync/cloudSyncApi.d.ts +47 -0
- package/dist/sync/cloudSyncApi.js +138 -0
- package/dist/sync/cloudSyncApi.test.d.ts +1 -0
- package/dist/sync/cloudSyncApi.test.js +54 -0
- package/dist/sync/encryptionService.d.ts +11 -0
- package/dist/sync/encryptionService.js +61 -0
- package/dist/sync/encryptionService.test.d.ts +1 -0
- package/dist/sync/encryptionService.test.js +52 -0
- package/dist/sync/realtimeFeature.d.ts +6 -0
- package/dist/sync/realtimeFeature.js +20 -0
- package/dist/sync/realtimeFeature.test.d.ts +1 -0
- package/dist/sync/realtimeFeature.test.js +30 -0
- package/dist/sync/realtimeReconcile.d.ts +30 -0
- package/dist/sync/realtimeReconcile.js +90 -0
- package/dist/sync/realtimeReconcile.test.d.ts +1 -0
- package/dist/sync/realtimeReconcile.test.js +43 -0
- package/dist/sync/syncService.d.ts +96 -0
- package/dist/sync/syncService.js +423 -0
- package/dist/sync/syncService.test.d.ts +1 -0
- package/dist/sync/syncService.test.js +484 -0
- package/dist/sync/workspaceSyncModel.d.ts +61 -0
- package/dist/sync/workspaceSyncModel.js +136 -0
- package/dist/sync/workspaceSyncModel.test.d.ts +1 -0
- package/dist/sync/workspaceSyncModel.test.js +73 -0
- package/dist/test/setup.d.ts +1 -0
- package/dist/test/setup.js +15 -0
- package/dist/types.d.ts +157 -0
- package/dist/types.js +41 -0
- package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
- package/dist/ui/assets/index-0W5vyCpn.css +1 -0
- package/dist/ui/assets/index-MCZuNNfn.js +23 -0
- package/dist/ui/assets/logo_white_01-0Vx-iIIv.png +0 -0
- package/dist/ui/assets/taskforce-BxLPokNB.png +0 -0
- package/dist/ui/assets/vendor-dnd-BI1K77C1.js +5 -0
- package/dist/ui/assets/vendor-icons-ZFG3SGwo.js +1 -0
- package/dist/ui/assets/vendor-markdown-CKYkQHfj.js +29 -0
- package/dist/ui/assets/vendor-react-DdIDsh7f.js +9 -0
- package/dist/ui/assets/vendor-router-BtiNU7Dl.js +3 -0
- package/dist/ui/branding/logos/logo_black_01.png +0 -0
- package/dist/ui/branding/logos/logo_black_02.png +0 -0
- package/dist/ui/branding/logos/logo_white_01.png +0 -0
- package/dist/ui/branding/logos/logo_white_02.png +0 -0
- package/dist/ui/favicon/android-chrome-192x192.png +0 -0
- package/dist/ui/favicon/android-chrome-512x512.png +0 -0
- package/dist/ui/favicon/apple-touch-icon.png +0 -0
- package/dist/ui/favicon/favicon-16x16.png +0 -0
- package/dist/ui/favicon/favicon-32x32.png +0 -0
- package/dist/ui/favicon/favicon.ico +0 -0
- package/dist/ui/favicon/favicon.png +0 -0
- package/dist/ui/favicon/site.webmanifest +1 -0
- package/dist/ui/fonts/RussoOne-Regular.ttf +0 -0
- package/dist/ui/images/taskforce.png +0 -0
- package/dist/ui/index.html +19 -0
- package/dist/utils/checklistMarkdown.d.ts +9 -0
- package/dist/utils/checklistMarkdown.js +46 -0
- package/dist/utils/constants.d.ts +21 -0
- package/dist/utils/constants.icons.test.d.ts +1 -0
- package/dist/utils/constants.icons.test.js +40 -0
- package/dist/utils/constants.js +100 -0
- package/dist/utils/contextFiles.d.ts +8 -0
- package/dist/utils/contextFiles.js +54 -0
- package/dist/utils/formatting.d.ts +5 -0
- package/dist/utils/formatting.js +30 -0
- package/dist/utils/planParser.d.ts +22 -0
- package/dist/utils/planParser.js +151 -0
- package/dist/utils/taskDragZones.d.ts +8 -0
- package/dist/utils/taskDragZones.js +14 -0
- package/dist/utils/taskDragZones.test.d.ts +1 -0
- package/dist/utils/taskDragZones.test.js +14 -0
- package/dist/utils/taskHierarchy.d.ts +4 -0
- package/dist/utils/taskHierarchy.js +69 -0
- package/dist/utils/taskHierarchy.test.d.ts +1 -0
- package/dist/utils/taskHierarchy.test.js +38 -0
- package/dist/utils/taskParenting.d.ts +3 -0
- package/dist/utils/taskParenting.js +24 -0
- package/dist/utils/taskParenting.test.d.ts +1 -0
- package/dist/utils/taskParenting.test.js +21 -0
- package/dist/utils/taskRelations.d.ts +6 -0
- package/dist/utils/taskRelations.js +24 -0
- package/dist/utils/taskRelations.test.d.ts +1 -0
- package/dist/utils/taskRelations.test.js +34 -0
- package/dist/utils/taskUtils.d.ts +2 -0
- package/dist/utils/taskUtils.js +20 -0
- package/dist/utils/taskforceApiClient.d.ts +7 -0
- package/dist/utils/taskforceApiClient.js +42 -0
- package/dist/utils/workspaceIdentity.d.ts +8 -0
- package/dist/utils/workspaceIdentity.js +47 -0
- package/dist/utils/workspaceIdentity.test.d.ts +1 -0
- package/dist/utils/workspaceIdentity.test.js +32 -0
- package/dist/utils/workspaceLifecycle.d.ts +26 -0
- package/dist/utils/workspaceLifecycle.js +69 -0
- package/dist/utils/workspaceLifecycle.test.d.ts +1 -0
- package/dist/utils/workspaceLifecycle.test.js +64 -0
- package/package.json +133 -0
- package/scripts/check-compat-boundaries.js +48 -0
- package/scripts/check-env.ts +24 -0
- package/scripts/check-localization-strings.mjs +49 -0
- package/scripts/check-runtime-boundaries.mjs +44 -0
- package/scripts/debug-global.mjs +34 -0
- package/scripts/debug-server.mjs +36 -0
- package/scripts/export-sqlite-to-postgres.mjs +125 -0
- package/scripts/generate-env-example.ts +8 -0
- package/scripts/generate-workflow-templates.mjs +134 -0
- package/scripts/mcp-stdio-adapter.mjs +160 -0
- package/scripts/postinstall.js +60 -0
- package/scripts/release-version-lib.mjs +88 -0
- package/scripts/release-version.mjs +72 -0
- package/scripts/run-dev-proxy.sh +49 -0
- package/scripts/run-e2e-realtime.sh +16 -0
- package/scripts/run-smoke.sh +23 -0
- package/scripts/run-soak.sh +29 -0
- package/scripts/test-global.js +143 -0
- package/scripts/update-homebrew-formula.sh +38 -0
- package/src/resources/templates/WORKFLOW_V2_POLICY.md +55 -0
- package/src/resources/templates/environments/aider.yaml +14 -0
- package/src/resources/templates/environments/antigravity.yaml +10 -0
- package/src/resources/templates/environments/claude.yaml +10 -0
- package/src/resources/templates/environments/cline.yaml +10 -0
- package/src/resources/templates/environments/codex.yaml +13 -0
- package/src/resources/templates/environments/copilot.yaml +10 -0
- package/src/resources/templates/environments/cursor.yaml +9 -0
- package/src/resources/templates/environments/gemini.yaml +9 -0
- package/src/resources/templates/environments/windsurf.yaml +10 -0
- package/src/resources/templates/workflow-sources/README.md +9 -0
- package/src/resources/templates/workflow-sources/workflowDocs.mjs +520 -0
- package/src/resources/templates/workflow-sources/workflowEditorOverrides.json +1 -0
- package/src/resources/templates/workflows/collaborate.yaml +104 -0
- package/src/resources/templates/workflows/do.yaml +82 -0
- package/src/resources/templates/workflows/evaluate.yaml +111 -0
- package/src/resources/templates/workflows/execute.yaml +142 -0
- package/src/resources/templates/workflows/plan.yaml +112 -0
- package/src/resources/templates/workflows/review.yaml +109 -0
|
@@ -0,0 +1,991 @@
|
|
|
1
|
+
import Stripe from 'stripe';
|
|
2
|
+
import { parseJsonBody } from './routes.js';
|
|
3
|
+
const BILLING_CONFIG_KEY_PLAN = 'billing.config.v1';
|
|
4
|
+
const PRICE_METADATA_PLAN_VERSION = 'taskforce_plan_version_id';
|
|
5
|
+
const BILLING_DIAGNOSTIC_RETENTION_DAYS = 30;
|
|
6
|
+
const BILLING_DIAGNOSTIC_MAX_PAYLOAD_BYTES = 8 * 1024;
|
|
7
|
+
function isoFromUnixSeconds(raw) {
|
|
8
|
+
const value = Number(raw);
|
|
9
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
10
|
+
return null;
|
|
11
|
+
return new Date(Math.floor(value) * 1000).toISOString();
|
|
12
|
+
}
|
|
13
|
+
function parseInterval(raw) {
|
|
14
|
+
const value = String(raw || '').trim().toLowerCase();
|
|
15
|
+
if (value === 'month' || value === 'year')
|
|
16
|
+
return value;
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
function resolveSubscriptionCurrentPeriodEnd(subscription) {
|
|
20
|
+
const itemPeriods = Array.isArray(subscription.items?.data)
|
|
21
|
+
? subscription.items.data
|
|
22
|
+
.map((item) => isoFromUnixSeconds(item?.current_period_end))
|
|
23
|
+
.filter((value) => Boolean(value))
|
|
24
|
+
: [];
|
|
25
|
+
if (itemPeriods.length === 0)
|
|
26
|
+
return null;
|
|
27
|
+
return itemPeriods.sort()[itemPeriods.length - 1] || null;
|
|
28
|
+
}
|
|
29
|
+
function resolveInvoiceSubscriptionId(invoice) {
|
|
30
|
+
const parentSubscription = String(invoice?.parent?.subscription_details?.subscription || '').trim();
|
|
31
|
+
if (parentSubscription)
|
|
32
|
+
return parentSubscription;
|
|
33
|
+
const lineSubscription = String(invoice.lines?.data?.[0]?.subscription || '').trim();
|
|
34
|
+
return lineSubscription;
|
|
35
|
+
}
|
|
36
|
+
function requestRuntimeMode(req) {
|
|
37
|
+
const value = String(req.headers['x-taskforce-runtime-mode'] || '').trim().toLowerCase();
|
|
38
|
+
return value === 'cloud' ? 'cloud' : 'local';
|
|
39
|
+
}
|
|
40
|
+
function parseBoolean(value, fallback = false) {
|
|
41
|
+
const normalized = String(value ?? '').trim().toLowerCase();
|
|
42
|
+
if (normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on')
|
|
43
|
+
return true;
|
|
44
|
+
if (normalized === 'false' || normalized === '0' || normalized === 'no' || normalized === 'off')
|
|
45
|
+
return false;
|
|
46
|
+
return fallback;
|
|
47
|
+
}
|
|
48
|
+
function parsePositiveIntBounded(raw, fallback, min, max) {
|
|
49
|
+
const parsed = Number.parseInt(String(raw || '').trim(), 10);
|
|
50
|
+
if (!Number.isFinite(parsed))
|
|
51
|
+
return fallback;
|
|
52
|
+
return Math.max(min, Math.min(max, parsed));
|
|
53
|
+
}
|
|
54
|
+
function resolveBaseUrl(req) {
|
|
55
|
+
const host = String(req.headers['x-forwarded-host'] || req.headers.host || 'localhost').trim();
|
|
56
|
+
const proto = String(req.headers['x-forwarded-proto'] || '').trim().toLowerCase() === 'http' ? 'http' : 'https';
|
|
57
|
+
return `${proto}://${host.replace(/\/+$/, '')}`;
|
|
58
|
+
}
|
|
59
|
+
async function readRawBody(req, maxBytes = 1_048_576) {
|
|
60
|
+
const chunks = [];
|
|
61
|
+
let total = 0;
|
|
62
|
+
for await (const chunk of req) {
|
|
63
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
64
|
+
total += buf.length;
|
|
65
|
+
if (total > maxBytes) {
|
|
66
|
+
const error = new Error(`Payload too large (max ${maxBytes} bytes)`);
|
|
67
|
+
error.statusCode = 413;
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
chunks.push(buf);
|
|
71
|
+
}
|
|
72
|
+
return Buffer.concat(chunks);
|
|
73
|
+
}
|
|
74
|
+
function mapStripeStatusToCanonicalState(statusRaw) {
|
|
75
|
+
const status = String(statusRaw || '').trim().toLowerCase();
|
|
76
|
+
if (status === 'trialing')
|
|
77
|
+
return 'trialing';
|
|
78
|
+
if (status === 'active')
|
|
79
|
+
return 'active';
|
|
80
|
+
if (status === 'past_due')
|
|
81
|
+
return 'grace';
|
|
82
|
+
return 'canceled';
|
|
83
|
+
}
|
|
84
|
+
function redactStripePayload(input) {
|
|
85
|
+
const blockedKeys = new Set([
|
|
86
|
+
'billing_details',
|
|
87
|
+
'customer_details',
|
|
88
|
+
'payment_method',
|
|
89
|
+
'payment_method_details',
|
|
90
|
+
'payment_intent',
|
|
91
|
+
'charges',
|
|
92
|
+
'source',
|
|
93
|
+
'sources',
|
|
94
|
+
'card',
|
|
95
|
+
'default_source'
|
|
96
|
+
]);
|
|
97
|
+
const visit = (value, depth) => {
|
|
98
|
+
if (depth > 6)
|
|
99
|
+
return '[truncated-depth]';
|
|
100
|
+
if (value === null || value === undefined)
|
|
101
|
+
return value;
|
|
102
|
+
if (Array.isArray(value))
|
|
103
|
+
return value.slice(0, 30).map((item) => visit(item, depth + 1));
|
|
104
|
+
if (typeof value !== 'object')
|
|
105
|
+
return value;
|
|
106
|
+
const source = value;
|
|
107
|
+
const out = {};
|
|
108
|
+
for (const [key, child] of Object.entries(source)) {
|
|
109
|
+
if (blockedKeys.has(key))
|
|
110
|
+
continue;
|
|
111
|
+
out[key] = visit(child, depth + 1);
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
};
|
|
115
|
+
const redacted = visit(input, 0);
|
|
116
|
+
return redacted && typeof redacted === 'object' && !Array.isArray(redacted)
|
|
117
|
+
? redacted
|
|
118
|
+
: {};
|
|
119
|
+
}
|
|
120
|
+
function boundedPayloadExcerpt(payload) {
|
|
121
|
+
const raw = JSON.stringify(payload);
|
|
122
|
+
if (Buffer.byteLength(raw, 'utf8') <= BILLING_DIAGNOSTIC_MAX_PAYLOAD_BYTES)
|
|
123
|
+
return raw;
|
|
124
|
+
const truncated = raw.slice(0, BILLING_DIAGNOSTIC_MAX_PAYLOAD_BYTES);
|
|
125
|
+
return `${truncated}...[truncated]`;
|
|
126
|
+
}
|
|
127
|
+
function parseBillingConfig(raw) {
|
|
128
|
+
const fallback = {
|
|
129
|
+
trialDays: 30,
|
|
130
|
+
tiers: {},
|
|
131
|
+
taxEnabled: false,
|
|
132
|
+
billingAddress: 'auto'
|
|
133
|
+
};
|
|
134
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
135
|
+
return fallback;
|
|
136
|
+
const record = raw;
|
|
137
|
+
const trialDaysRaw = Number(record.trial_days);
|
|
138
|
+
const trialDays = Number.isFinite(trialDaysRaw) ? Math.max(1, Math.min(30, Math.floor(trialDaysRaw))) : 30;
|
|
139
|
+
const billingAddress = String(record.billing_address || '').trim().toLowerCase() === 'required'
|
|
140
|
+
? 'required'
|
|
141
|
+
: 'auto';
|
|
142
|
+
return {
|
|
143
|
+
trialDays,
|
|
144
|
+
tiers: record.tiers && typeof record.tiers === 'object' && !Array.isArray(record.tiers)
|
|
145
|
+
? record.tiers
|
|
146
|
+
: {},
|
|
147
|
+
taxEnabled: parseBoolean(record.tax_enabled, false),
|
|
148
|
+
billingAddress
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function billingDisabledResponse(res) {
|
|
152
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
153
|
+
res.end(JSON.stringify({ error: 'Billing is disabled.' }));
|
|
154
|
+
}
|
|
155
|
+
function cloudOnlyResponse(res) {
|
|
156
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
157
|
+
res.end(JSON.stringify({ error: 'Billing is only available in cloud runtime.' }));
|
|
158
|
+
}
|
|
159
|
+
function getStripeClient() {
|
|
160
|
+
const key = String(process.env.TASKFORCE_STRIPE_SECRET_KEY || '').trim();
|
|
161
|
+
if (!key) {
|
|
162
|
+
const error = new Error('TASKFORCE_STRIPE_SECRET_KEY is not configured.');
|
|
163
|
+
error.statusCode = 500;
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
return new Stripe(key);
|
|
167
|
+
}
|
|
168
|
+
function readBillingConfig(core) {
|
|
169
|
+
const db = core.getDatabaseAdapter();
|
|
170
|
+
const tenantId = core.getTenantId();
|
|
171
|
+
const row = db.prepare(`
|
|
172
|
+
SELECT payload
|
|
173
|
+
FROM system_settings
|
|
174
|
+
WHERE tenant_id = ? AND settings_key = ?
|
|
175
|
+
LIMIT 1
|
|
176
|
+
`).get(tenantId, BILLING_CONFIG_KEY_PLAN);
|
|
177
|
+
if (!row?.payload)
|
|
178
|
+
return parseBillingConfig({});
|
|
179
|
+
try {
|
|
180
|
+
return parseBillingConfig(JSON.parse(row.payload));
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return parseBillingConfig({});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function writeBillingConfig(core, payload) {
|
|
187
|
+
const db = core.getDatabaseAdapter();
|
|
188
|
+
const tenantId = core.getTenantId();
|
|
189
|
+
const now = new Date().toISOString();
|
|
190
|
+
db.prepare(`
|
|
191
|
+
INSERT INTO system_settings (tenant_id, settings_key, payload, updated_at)
|
|
192
|
+
VALUES (?, ?, ?, ?)
|
|
193
|
+
ON CONFLICT (tenant_id, settings_key) DO UPDATE SET
|
|
194
|
+
payload = excluded.payload,
|
|
195
|
+
updated_at = excluded.updated_at
|
|
196
|
+
`).run(tenantId, BILLING_CONFIG_KEY_PLAN, JSON.stringify({
|
|
197
|
+
trial_days: payload.trialDays,
|
|
198
|
+
tiers: payload.tiers,
|
|
199
|
+
tax_enabled: payload.taxEnabled,
|
|
200
|
+
billing_address: payload.billingAddress
|
|
201
|
+
}), now);
|
|
202
|
+
}
|
|
203
|
+
function resolveDefaultPlanVersionId(core, tierRaw) {
|
|
204
|
+
const tier = String(tierRaw || '').trim().toLowerCase();
|
|
205
|
+
if (!tier)
|
|
206
|
+
return null;
|
|
207
|
+
const db = core.getDatabaseAdapter();
|
|
208
|
+
const tenantId = core.getTenantId();
|
|
209
|
+
const row = db.prepare(`
|
|
210
|
+
SELECT plan_version_id
|
|
211
|
+
FROM plan_versions
|
|
212
|
+
WHERE tenant_id = ? AND plan_id = ? AND is_default = 1
|
|
213
|
+
LIMIT 1
|
|
214
|
+
`).get(tenantId, tier);
|
|
215
|
+
return row?.plan_version_id ? String(row.plan_version_id) : null;
|
|
216
|
+
}
|
|
217
|
+
function resolvePriceIdForPlanVersion(core, planVersionIdRaw, intervalRaw) {
|
|
218
|
+
const planVersionId = String(planVersionIdRaw || '').trim();
|
|
219
|
+
const interval = parseInterval(intervalRaw);
|
|
220
|
+
if (!planVersionId || !interval)
|
|
221
|
+
return null;
|
|
222
|
+
const db = core.getDatabaseAdapter();
|
|
223
|
+
const tenantId = core.getTenantId();
|
|
224
|
+
const row = db.prepare(`
|
|
225
|
+
SELECT stripe_price_id
|
|
226
|
+
FROM billing_price_mappings
|
|
227
|
+
WHERE tenant_id = ?
|
|
228
|
+
AND plan_version_id = ?
|
|
229
|
+
AND billing_interval = ?
|
|
230
|
+
AND active = 1
|
|
231
|
+
LIMIT 1
|
|
232
|
+
`).get(tenantId, planVersionId, interval);
|
|
233
|
+
return row?.stripe_price_id ? String(row.stripe_price_id) : null;
|
|
234
|
+
}
|
|
235
|
+
function resolvePlanVersionIdForPriceId(core, priceIdRaw) {
|
|
236
|
+
const priceId = String(priceIdRaw || '').trim();
|
|
237
|
+
if (!priceId)
|
|
238
|
+
return null;
|
|
239
|
+
const db = core.getDatabaseAdapter();
|
|
240
|
+
const tenantId = core.getTenantId();
|
|
241
|
+
const row = db.prepare(`
|
|
242
|
+
SELECT plan_version_id
|
|
243
|
+
FROM billing_price_mappings
|
|
244
|
+
WHERE tenant_id = ?
|
|
245
|
+
AND stripe_price_id = ?
|
|
246
|
+
AND active = 1
|
|
247
|
+
LIMIT 1
|
|
248
|
+
`).get(tenantId, priceId);
|
|
249
|
+
return row?.plan_version_id ? String(row.plan_version_id) : null;
|
|
250
|
+
}
|
|
251
|
+
function resolvePlanIdForVersion(core, planVersionIdRaw) {
|
|
252
|
+
const planVersionId = String(planVersionIdRaw || '').trim();
|
|
253
|
+
if (!planVersionId)
|
|
254
|
+
return null;
|
|
255
|
+
const db = core.getDatabaseAdapter();
|
|
256
|
+
const tenantId = core.getTenantId();
|
|
257
|
+
const row = db.prepare(`
|
|
258
|
+
SELECT plan_id
|
|
259
|
+
FROM plan_versions
|
|
260
|
+
WHERE tenant_id = ? AND plan_version_id = ?
|
|
261
|
+
LIMIT 1
|
|
262
|
+
`).get(tenantId, planVersionId);
|
|
263
|
+
return row?.plan_id ? String(row.plan_id) : null;
|
|
264
|
+
}
|
|
265
|
+
function getEntitlementRow(core, userIdRaw) {
|
|
266
|
+
const userId = String(userIdRaw || '').trim();
|
|
267
|
+
if (!userId)
|
|
268
|
+
return null;
|
|
269
|
+
const db = core.getDatabaseAdapter();
|
|
270
|
+
const tenantId = core.getTenantId();
|
|
271
|
+
const row = db.prepare(`
|
|
272
|
+
SELECT account_id, plan_version_id, entitlement_state
|
|
273
|
+
FROM account_entitlements
|
|
274
|
+
WHERE tenant_id = ? AND account_id = ?
|
|
275
|
+
LIMIT 1
|
|
276
|
+
`).get(tenantId, userId);
|
|
277
|
+
return row || null;
|
|
278
|
+
}
|
|
279
|
+
function upsertCanonicalEntitlement(core, input) {
|
|
280
|
+
const db = core.getDatabaseAdapter();
|
|
281
|
+
const tenantId = core.getTenantId();
|
|
282
|
+
const now = new Date().toISOString();
|
|
283
|
+
db.prepare(`
|
|
284
|
+
INSERT INTO account_entitlements (account_id, tenant_id, plan_version_id, entitlement_state, effective_at, effective_until, updated_by, created_at, updated_at)
|
|
285
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
286
|
+
ON CONFLICT (account_id) DO UPDATE SET
|
|
287
|
+
plan_version_id = excluded.plan_version_id,
|
|
288
|
+
entitlement_state = excluded.entitlement_state,
|
|
289
|
+
effective_at = excluded.effective_at,
|
|
290
|
+
effective_until = excluded.effective_until,
|
|
291
|
+
updated_by = excluded.updated_by,
|
|
292
|
+
updated_at = excluded.updated_at
|
|
293
|
+
`).run(input.userId, tenantId, input.planVersionId, input.state, input.effectiveAt || now, input.effectiveUntil || null, input.updatedBy || 'stripe-webhook', now, now);
|
|
294
|
+
}
|
|
295
|
+
function upsertBillingMirror(core, input) {
|
|
296
|
+
const db = core.getDatabaseAdapter();
|
|
297
|
+
const tenantId = core.getTenantId();
|
|
298
|
+
const now = new Date().toISOString();
|
|
299
|
+
const existingRow = db.prepare(`
|
|
300
|
+
SELECT tier
|
|
301
|
+
FROM user_billing
|
|
302
|
+
WHERE tenant_id = ? AND user_id = ?
|
|
303
|
+
LIMIT 1
|
|
304
|
+
`).get(tenantId, input.userId);
|
|
305
|
+
const resolvedTier = String(input.planId || existingRow?.tier || 'free').trim() || 'free';
|
|
306
|
+
db.prepare(`
|
|
307
|
+
INSERT INTO user_billing (
|
|
308
|
+
user_id, tenant_id, stripe_customer_id, stripe_subscription_id, stripe_price_id, stripe_status, tier, billing_interval,
|
|
309
|
+
entitlement_state, trial_end, current_period_end, effective_until, created_at, updated_at
|
|
310
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
311
|
+
ON CONFLICT (user_id) DO UPDATE SET
|
|
312
|
+
stripe_customer_id = COALESCE(excluded.stripe_customer_id, user_billing.stripe_customer_id),
|
|
313
|
+
stripe_subscription_id = COALESCE(excluded.stripe_subscription_id, user_billing.stripe_subscription_id),
|
|
314
|
+
stripe_price_id = COALESCE(excluded.stripe_price_id, user_billing.stripe_price_id),
|
|
315
|
+
stripe_status = COALESCE(excluded.stripe_status, user_billing.stripe_status),
|
|
316
|
+
tier = COALESCE(excluded.tier, user_billing.tier),
|
|
317
|
+
billing_interval = COALESCE(excluded.billing_interval, user_billing.billing_interval),
|
|
318
|
+
entitlement_state = COALESCE(excluded.entitlement_state, user_billing.entitlement_state),
|
|
319
|
+
trial_end = COALESCE(excluded.trial_end, user_billing.trial_end),
|
|
320
|
+
current_period_end = COALESCE(excluded.current_period_end, user_billing.current_period_end),
|
|
321
|
+
effective_until = COALESCE(excluded.effective_until, user_billing.effective_until),
|
|
322
|
+
updated_at = excluded.updated_at
|
|
323
|
+
`).run(input.userId, tenantId, input.stripeCustomerId || null, input.stripeSubscriptionId || null, input.stripePriceId || null, input.stripeStatus || null, resolvedTier, input.billingInterval || null, input.canonicalState || null, input.trialEnd || null, input.currentPeriodEnd || null, input.effectiveUntil || null, now, now);
|
|
324
|
+
}
|
|
325
|
+
function resolveUserIdByCustomer(core, customerIdRaw) {
|
|
326
|
+
const customerId = String(customerIdRaw || '').trim();
|
|
327
|
+
if (!customerId)
|
|
328
|
+
return null;
|
|
329
|
+
const db = core.getDatabaseAdapter();
|
|
330
|
+
const tenantId = core.getTenantId();
|
|
331
|
+
const row = db.prepare(`
|
|
332
|
+
SELECT user_id
|
|
333
|
+
FROM user_billing
|
|
334
|
+
WHERE tenant_id = ? AND stripe_customer_id = ?
|
|
335
|
+
LIMIT 1
|
|
336
|
+
`).get(tenantId, customerId);
|
|
337
|
+
return row?.user_id ? String(row.user_id) : null;
|
|
338
|
+
}
|
|
339
|
+
function resolveUserIdBySubscription(core, subscriptionIdRaw) {
|
|
340
|
+
const subscriptionId = String(subscriptionIdRaw || '').trim();
|
|
341
|
+
if (!subscriptionId)
|
|
342
|
+
return null;
|
|
343
|
+
const db = core.getDatabaseAdapter();
|
|
344
|
+
const tenantId = core.getTenantId();
|
|
345
|
+
const row = db.prepare(`
|
|
346
|
+
SELECT user_id
|
|
347
|
+
FROM user_billing
|
|
348
|
+
WHERE tenant_id = ? AND stripe_subscription_id = ?
|
|
349
|
+
LIMIT 1
|
|
350
|
+
`).get(tenantId, subscriptionId);
|
|
351
|
+
return row?.user_id ? String(row.user_id) : null;
|
|
352
|
+
}
|
|
353
|
+
function purgeExpiredWebhookFailures(core) {
|
|
354
|
+
const db = core.getDatabaseAdapter();
|
|
355
|
+
const tenantId = core.getTenantId();
|
|
356
|
+
const result = db.prepare(`
|
|
357
|
+
DELETE FROM stripe_webhook_failures
|
|
358
|
+
WHERE tenant_id = ?
|
|
359
|
+
AND purge_after < ?
|
|
360
|
+
`).run(tenantId, new Date().toISOString());
|
|
361
|
+
return Number(result?.changes || 0);
|
|
362
|
+
}
|
|
363
|
+
function recordWebhookFailure(core, input) {
|
|
364
|
+
try {
|
|
365
|
+
const db = core.getDatabaseAdapter();
|
|
366
|
+
const tenantId = core.getTenantId();
|
|
367
|
+
const now = new Date();
|
|
368
|
+
const purgeAfter = new Date(now.getTime() + BILLING_DIAGNOSTIC_RETENTION_DAYS * 24 * 60 * 60 * 1000).toISOString();
|
|
369
|
+
const redacted = redactStripePayload(input.payload);
|
|
370
|
+
const excerpt = boundedPayloadExcerpt(redacted);
|
|
371
|
+
const id = `whf-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
372
|
+
db.prepare(`
|
|
373
|
+
INSERT INTO stripe_webhook_failures (id, tenant_id, event_id, event_type, reason, payload_excerpt, purge_after, created_at)
|
|
374
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
375
|
+
`).run(id, tenantId, input.eventId, input.eventType, input.reason, excerpt, purgeAfter, now.toISOString());
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
// diagnostics are best-effort only
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function resolvePriceAndIntervalFromInvoice(invoice) {
|
|
382
|
+
const line = invoice.lines?.data?.find((entry) => !!entry?.price?.id || !!entry?.pricing?.price_details?.price);
|
|
383
|
+
const legacyPriceId = String(line?.price?.id || '').trim();
|
|
384
|
+
const modernPriceId = String(line?.pricing?.price_details?.price || '').trim();
|
|
385
|
+
const priceId = modernPriceId || legacyPriceId || null;
|
|
386
|
+
const recurringInterval = parseInterval(line?.price?.recurring?.interval || '');
|
|
387
|
+
return { priceId, interval: recurringInterval };
|
|
388
|
+
}
|
|
389
|
+
function lockWebhookEvent(core, event) {
|
|
390
|
+
const db = core.getDatabaseAdapter();
|
|
391
|
+
const tenantId = core.getTenantId();
|
|
392
|
+
const result = db.prepare(`
|
|
393
|
+
INSERT INTO stripe_webhook_events (event_id, tenant_id, event_type, processed_at)
|
|
394
|
+
VALUES (?, ?, ?, ?)
|
|
395
|
+
ON CONFLICT (event_id) DO NOTHING
|
|
396
|
+
`).run(event.id, tenantId, event.type, new Date().toISOString());
|
|
397
|
+
return Number(result?.changes || 0) > 0;
|
|
398
|
+
}
|
|
399
|
+
async function importStripePrices(core) {
|
|
400
|
+
const stripe = getStripeClient();
|
|
401
|
+
const db = core.getDatabaseAdapter();
|
|
402
|
+
const tenantId = core.getTenantId();
|
|
403
|
+
const now = new Date().toISOString();
|
|
404
|
+
const validPlanVersionIds = new Set(db.prepare(`
|
|
405
|
+
SELECT plan_version_id
|
|
406
|
+
FROM plan_versions
|
|
407
|
+
WHERE tenant_id = ?
|
|
408
|
+
`).all(tenantId).map((row) => String(row.plan_version_id || '')).filter(Boolean));
|
|
409
|
+
const importedPriceIds = new Set();
|
|
410
|
+
const summary = {
|
|
411
|
+
imported: 0,
|
|
412
|
+
skipped: 0,
|
|
413
|
+
deactivated: 0,
|
|
414
|
+
details: []
|
|
415
|
+
};
|
|
416
|
+
let startingAfter;
|
|
417
|
+
while (true) {
|
|
418
|
+
const page = await stripe.prices.list({
|
|
419
|
+
active: true,
|
|
420
|
+
limit: 100,
|
|
421
|
+
starting_after: startingAfter,
|
|
422
|
+
type: 'recurring'
|
|
423
|
+
});
|
|
424
|
+
for (const price of page.data) {
|
|
425
|
+
const interval = parseInterval(price.recurring?.interval || '');
|
|
426
|
+
const planVersionId = String(price.metadata?.[PRICE_METADATA_PLAN_VERSION] || '').trim();
|
|
427
|
+
if (!interval || !planVersionId || !validPlanVersionIds.has(planVersionId)) {
|
|
428
|
+
summary.skipped += 1;
|
|
429
|
+
summary.details.push({
|
|
430
|
+
priceId: price.id,
|
|
431
|
+
status: 'skipped',
|
|
432
|
+
reason: !planVersionId
|
|
433
|
+
? `Missing metadata ${PRICE_METADATA_PLAN_VERSION}`
|
|
434
|
+
: (!interval ? 'Unsupported interval' : 'Unknown plan_version_id')
|
|
435
|
+
});
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
db.prepare(`
|
|
439
|
+
INSERT INTO billing_price_mappings (
|
|
440
|
+
tenant_id, plan_version_id, billing_interval, stripe_price_id, active, created_at, updated_at
|
|
441
|
+
) VALUES (?, ?, ?, ?, 1, ?, ?)
|
|
442
|
+
ON CONFLICT (tenant_id, stripe_price_id) DO UPDATE SET
|
|
443
|
+
plan_version_id = excluded.plan_version_id,
|
|
444
|
+
billing_interval = excluded.billing_interval,
|
|
445
|
+
active = 1,
|
|
446
|
+
updated_at = excluded.updated_at
|
|
447
|
+
`).run(tenantId, planVersionId, interval, price.id, now, now);
|
|
448
|
+
importedPriceIds.add(price.id);
|
|
449
|
+
summary.imported += 1;
|
|
450
|
+
summary.details.push({ priceId: price.id, status: 'imported' });
|
|
451
|
+
}
|
|
452
|
+
if (!page.has_more || page.data.length === 0)
|
|
453
|
+
break;
|
|
454
|
+
startingAfter = page.data[page.data.length - 1]?.id;
|
|
455
|
+
if (!startingAfter)
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
const existing = db.prepare(`
|
|
459
|
+
SELECT stripe_price_id
|
|
460
|
+
FROM billing_price_mappings
|
|
461
|
+
WHERE tenant_id = ? AND active = 1
|
|
462
|
+
`).all(tenantId);
|
|
463
|
+
for (const row of existing) {
|
|
464
|
+
const priceId = String(row.stripe_price_id || '').trim();
|
|
465
|
+
if (!priceId || importedPriceIds.has(priceId))
|
|
466
|
+
continue;
|
|
467
|
+
db.prepare(`
|
|
468
|
+
UPDATE billing_price_mappings
|
|
469
|
+
SET active = 0, updated_at = ?
|
|
470
|
+
WHERE tenant_id = ? AND stripe_price_id = ?
|
|
471
|
+
`).run(now, tenantId, priceId);
|
|
472
|
+
summary.deactivated += 1;
|
|
473
|
+
}
|
|
474
|
+
return summary;
|
|
475
|
+
}
|
|
476
|
+
export function registerBillingRoutes(deps) {
|
|
477
|
+
const { addRoute, core, context, ensureAuthenticatedUser, ensureAdminRole, requestUserId, resolveErrorStatusCode, auditAdminAction } = deps;
|
|
478
|
+
const billingEnabled = () => parseBoolean(process.env.TASKFORCE_BILLING_ENABLED, false);
|
|
479
|
+
const ensureCloudBilling = (req, res) => {
|
|
480
|
+
if (!billingEnabled()) {
|
|
481
|
+
billingDisabledResponse(res);
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
const runtime = context.authConfig?.runtimeMode || requestRuntimeMode(req);
|
|
485
|
+
if (runtime !== 'cloud') {
|
|
486
|
+
cloudOnlyResponse(res);
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
return true;
|
|
490
|
+
};
|
|
491
|
+
addRoute('GET', '/api/taskforce/billing/status', async (req, res) => {
|
|
492
|
+
if (!ensureCloudBilling(req, res))
|
|
493
|
+
return;
|
|
494
|
+
if (!ensureAuthenticatedUser(req)) {
|
|
495
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
496
|
+
res.end(JSON.stringify({ error: 'Authentication required.' }));
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
try {
|
|
500
|
+
const userId = requestUserId(req);
|
|
501
|
+
const entitlement = core.getAccountEntitlement(userId);
|
|
502
|
+
const db = core.getDatabaseAdapter();
|
|
503
|
+
const tenantId = core.getTenantId();
|
|
504
|
+
const row = db.prepare(`
|
|
505
|
+
SELECT stripe_status, stripe_customer_id, stripe_subscription_id, stripe_price_id, billing_interval, trial_end, current_period_end, effective_until
|
|
506
|
+
FROM user_billing
|
|
507
|
+
WHERE tenant_id = ? AND user_id = ?
|
|
508
|
+
LIMIT 1
|
|
509
|
+
`).get(tenantId, userId);
|
|
510
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
511
|
+
res.end(JSON.stringify({
|
|
512
|
+
stripeStatus: row?.stripe_status || null,
|
|
513
|
+
stripeCustomerId: row?.stripe_customer_id || null,
|
|
514
|
+
stripeSubscriptionId: row?.stripe_subscription_id || null,
|
|
515
|
+
stripePriceId: row?.stripe_price_id || null,
|
|
516
|
+
billingInterval: row?.billing_interval || null,
|
|
517
|
+
trialEnd: row?.trial_end || null,
|
|
518
|
+
currentPeriodEnd: row?.current_period_end || null,
|
|
519
|
+
effectiveUntil: row?.effective_until || null,
|
|
520
|
+
entitlementState: entitlement.state,
|
|
521
|
+
planVersionId: entitlement.planVersionId,
|
|
522
|
+
planId: entitlement.planId
|
|
523
|
+
}));
|
|
524
|
+
}
|
|
525
|
+
catch (error) {
|
|
526
|
+
const statusCode = resolveErrorStatusCode(error, 400);
|
|
527
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
528
|
+
res.end(JSON.stringify({ error: String(error?.message || 'Unable to read billing status'), code: error?.code }));
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
addRoute('GET', '/api/taskforce/admin/billing/config', async (req, res) => {
|
|
532
|
+
if (!ensureCloudBilling(req, res))
|
|
533
|
+
return;
|
|
534
|
+
if (!ensureAdminRole(req)) {
|
|
535
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
536
|
+
res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const config = readBillingConfig(core);
|
|
540
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
541
|
+
res.end(JSON.stringify(config));
|
|
542
|
+
});
|
|
543
|
+
addRoute('PUT', '/api/taskforce/admin/billing/config', async (req, res) => {
|
|
544
|
+
if (!ensureCloudBilling(req, res))
|
|
545
|
+
return;
|
|
546
|
+
if (!ensureAdminRole(req)) {
|
|
547
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
548
|
+
res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
const body = await parseJsonBody(req);
|
|
552
|
+
const trialDaysRaw = Number(body?.trial_days);
|
|
553
|
+
if (!Number.isFinite(trialDaysRaw) || trialDaysRaw < 1 || trialDaysRaw > 30) {
|
|
554
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
555
|
+
res.end(JSON.stringify({ error: 'trial_days must be an integer between 1 and 30.' }));
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
const nextConfig = {
|
|
559
|
+
trialDays: Math.floor(trialDaysRaw),
|
|
560
|
+
tiers: body?.tiers && typeof body.tiers === 'object' && !Array.isArray(body.tiers) ? body.tiers : {},
|
|
561
|
+
taxEnabled: parseBoolean(body?.tax_enabled, false),
|
|
562
|
+
billingAddress: String(body?.billing_address || '').trim().toLowerCase() === 'required' ? 'required' : 'auto'
|
|
563
|
+
};
|
|
564
|
+
writeBillingConfig(core, nextConfig);
|
|
565
|
+
auditAdminAction(req, 'billing-config-updated', { trialDays: nextConfig.trialDays, billingAddress: nextConfig.billingAddress });
|
|
566
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
567
|
+
res.end(JSON.stringify(nextConfig));
|
|
568
|
+
});
|
|
569
|
+
addRoute('GET', '/api/taskforce/admin/billing/prices', async (req, res) => {
|
|
570
|
+
if (!ensureCloudBilling(req, res))
|
|
571
|
+
return;
|
|
572
|
+
if (!ensureAdminRole(req)) {
|
|
573
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
574
|
+
res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
const db = core.getDatabaseAdapter();
|
|
578
|
+
const tenantId = core.getTenantId();
|
|
579
|
+
const rows = db.prepare(`
|
|
580
|
+
SELECT plan_version_id, billing_interval, stripe_price_id, active, created_at, updated_at
|
|
581
|
+
FROM billing_price_mappings
|
|
582
|
+
WHERE tenant_id = ?
|
|
583
|
+
ORDER BY plan_version_id ASC, billing_interval ASC, stripe_price_id ASC
|
|
584
|
+
`).all(tenantId);
|
|
585
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
586
|
+
res.end(JSON.stringify({ prices: rows }));
|
|
587
|
+
});
|
|
588
|
+
addRoute('GET', '/api/taskforce/admin/billing/webhook-failures', async (req, res) => {
|
|
589
|
+
if (!ensureCloudBilling(req, res))
|
|
590
|
+
return;
|
|
591
|
+
if (!ensureAdminRole(req)) {
|
|
592
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
593
|
+
res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
const db = core.getDatabaseAdapter();
|
|
597
|
+
const tenantId = core.getTenantId();
|
|
598
|
+
const queryLimit = req.url
|
|
599
|
+
? new URL(req.url, 'https://taskforce.local').searchParams.get('limit')
|
|
600
|
+
: null;
|
|
601
|
+
const limit = parsePositiveIntBounded(queryLimit, 50, 1, 200);
|
|
602
|
+
const now = new Date().toISOString();
|
|
603
|
+
const purged = purgeExpiredWebhookFailures(core);
|
|
604
|
+
const rows = db.prepare(`
|
|
605
|
+
SELECT id, event_id, event_type, reason, payload_excerpt, created_at, purge_after
|
|
606
|
+
FROM stripe_webhook_failures
|
|
607
|
+
WHERE tenant_id = ?
|
|
608
|
+
ORDER BY created_at DESC
|
|
609
|
+
LIMIT ?
|
|
610
|
+
`).all(tenantId, limit);
|
|
611
|
+
const total = db.prepare(`
|
|
612
|
+
SELECT COUNT(*) AS count
|
|
613
|
+
FROM stripe_webhook_failures
|
|
614
|
+
WHERE tenant_id = ?
|
|
615
|
+
`).get(tenantId);
|
|
616
|
+
const expiringSoon = db.prepare(`
|
|
617
|
+
SELECT COUNT(*) AS count
|
|
618
|
+
FROM stripe_webhook_failures
|
|
619
|
+
WHERE tenant_id = ?
|
|
620
|
+
AND purge_after <= ?
|
|
621
|
+
`).get(tenantId, new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString());
|
|
622
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
623
|
+
res.end(JSON.stringify({
|
|
624
|
+
asOf: now,
|
|
625
|
+
purgedExpiredRows: purged,
|
|
626
|
+
total: Number(total?.count || 0),
|
|
627
|
+
expiringWithin24h: Number(expiringSoon?.count || 0),
|
|
628
|
+
rows
|
|
629
|
+
}));
|
|
630
|
+
});
|
|
631
|
+
addRoute('POST', '/api/taskforce/admin/billing/prices/import', async (req, res) => {
|
|
632
|
+
if (!ensureCloudBilling(req, res))
|
|
633
|
+
return;
|
|
634
|
+
if (!ensureAdminRole(req)) {
|
|
635
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
636
|
+
res.end(JSON.stringify({ error: 'Forbidden: system admin role required.' }));
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
try {
|
|
640
|
+
const summary = await importStripePrices(core);
|
|
641
|
+
auditAdminAction(req, 'billing-prices-import', {
|
|
642
|
+
imported: summary.imported,
|
|
643
|
+
skipped: summary.skipped,
|
|
644
|
+
deactivated: summary.deactivated
|
|
645
|
+
});
|
|
646
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
647
|
+
res.end(JSON.stringify(summary));
|
|
648
|
+
}
|
|
649
|
+
catch (error) {
|
|
650
|
+
const statusCode = resolveErrorStatusCode(error, 400);
|
|
651
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
652
|
+
res.end(JSON.stringify({ error: String(error?.message || 'Unable to import Stripe prices'), code: error?.code }));
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
addRoute('POST', '/api/taskforce/billing/checkout-session', async (req, res) => {
|
|
656
|
+
if (!ensureCloudBilling(req, res))
|
|
657
|
+
return;
|
|
658
|
+
if (!ensureAuthenticatedUser(req)) {
|
|
659
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
660
|
+
res.end(JSON.stringify({ error: 'Authentication required.' }));
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
const body = await parseJsonBody(req);
|
|
664
|
+
const interval = parseInterval(body?.interval);
|
|
665
|
+
const planVersionId = String(body?.planVersionId || '').trim() || resolveDefaultPlanVersionId(core, body?.tier);
|
|
666
|
+
if (!interval || !planVersionId) {
|
|
667
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
668
|
+
res.end(JSON.stringify({ error: 'planVersionId (or tier) and interval are required.' }));
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
const priceId = resolvePriceIdForPlanVersion(core, planVersionId, interval);
|
|
672
|
+
if (!priceId) {
|
|
673
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
674
|
+
res.end(JSON.stringify({ error: 'No active Stripe price mapping found for requested plan/interval.' }));
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
try {
|
|
678
|
+
const stripe = getStripeClient();
|
|
679
|
+
const userId = requestUserId(req);
|
|
680
|
+
const baseUrl = resolveBaseUrl(req);
|
|
681
|
+
const config = readBillingConfig(core);
|
|
682
|
+
const session = await stripe.checkout.sessions.create({
|
|
683
|
+
mode: 'subscription',
|
|
684
|
+
line_items: [{ price: priceId, quantity: 1 }],
|
|
685
|
+
success_url: `${baseUrl}/billing/success?session_id={CHECKOUT_SESSION_ID}`,
|
|
686
|
+
cancel_url: `${baseUrl}/billing/cancel`,
|
|
687
|
+
subscription_data: {
|
|
688
|
+
trial_period_days: config.trialDays
|
|
689
|
+
},
|
|
690
|
+
billing_address_collection: config.billingAddress,
|
|
691
|
+
automatic_tax: { enabled: config.taxEnabled },
|
|
692
|
+
customer_creation: 'always',
|
|
693
|
+
client_reference_id: userId,
|
|
694
|
+
metadata: {
|
|
695
|
+
userId,
|
|
696
|
+
tenantId: core.getTenantId(),
|
|
697
|
+
planVersionId
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
701
|
+
res.end(JSON.stringify({ url: session.url || null, id: session.id }));
|
|
702
|
+
}
|
|
703
|
+
catch (error) {
|
|
704
|
+
const statusCode = resolveErrorStatusCode(error, 400);
|
|
705
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
706
|
+
res.end(JSON.stringify({ error: String(error?.message || 'Unable to create checkout session'), code: error?.code }));
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
addRoute('POST', '/api/taskforce/billing/portal-session', async (req, res) => {
|
|
710
|
+
if (!ensureCloudBilling(req, res))
|
|
711
|
+
return;
|
|
712
|
+
if (!ensureAuthenticatedUser(req)) {
|
|
713
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
714
|
+
res.end(JSON.stringify({ error: 'Authentication required.' }));
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const userId = requestUserId(req);
|
|
718
|
+
const db = core.getDatabaseAdapter();
|
|
719
|
+
const tenantId = core.getTenantId();
|
|
720
|
+
const row = db.prepare(`
|
|
721
|
+
SELECT stripe_customer_id
|
|
722
|
+
FROM user_billing
|
|
723
|
+
WHERE tenant_id = ? AND user_id = ?
|
|
724
|
+
LIMIT 1
|
|
725
|
+
`).get(tenantId, userId);
|
|
726
|
+
const customerId = String(row?.stripe_customer_id || '').trim();
|
|
727
|
+
if (!customerId) {
|
|
728
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
729
|
+
res.end(JSON.stringify({ error: 'No Stripe customer is linked for this account.' }));
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
try {
|
|
733
|
+
const stripe = getStripeClient();
|
|
734
|
+
const baseUrl = resolveBaseUrl(req);
|
|
735
|
+
const session = await stripe.billingPortal.sessions.create({
|
|
736
|
+
customer: customerId,
|
|
737
|
+
return_url: `${baseUrl}/settings`
|
|
738
|
+
});
|
|
739
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
740
|
+
res.end(JSON.stringify({ url: session.url }));
|
|
741
|
+
}
|
|
742
|
+
catch (error) {
|
|
743
|
+
const statusCode = resolveErrorStatusCode(error, 400);
|
|
744
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
745
|
+
res.end(JSON.stringify({ error: String(error?.message || 'Unable to create portal session'), code: error?.code }));
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
addRoute('POST', '/api/taskforce/billing/subscription', async (req, res) => {
|
|
749
|
+
if (!ensureCloudBilling(req, res))
|
|
750
|
+
return;
|
|
751
|
+
if (!ensureAuthenticatedUser(req)) {
|
|
752
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
753
|
+
res.end(JSON.stringify({ error: 'Authentication required.' }));
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
const body = await parseJsonBody(req);
|
|
757
|
+
const interval = parseInterval(body?.interval);
|
|
758
|
+
const planVersionId = String(body?.planVersionId || '').trim() || resolveDefaultPlanVersionId(core, body?.tier);
|
|
759
|
+
if (!interval || !planVersionId) {
|
|
760
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
761
|
+
res.end(JSON.stringify({ error: 'planVersionId (or tier) and interval are required.' }));
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
const nextPriceId = resolvePriceIdForPlanVersion(core, planVersionId, interval);
|
|
765
|
+
if (!nextPriceId) {
|
|
766
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
767
|
+
res.end(JSON.stringify({ error: 'No active Stripe price mapping found for requested plan/interval.' }));
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
const userId = requestUserId(req);
|
|
771
|
+
const db = core.getDatabaseAdapter();
|
|
772
|
+
const tenantId = core.getTenantId();
|
|
773
|
+
const row = db.prepare(`
|
|
774
|
+
SELECT stripe_subscription_id
|
|
775
|
+
FROM user_billing
|
|
776
|
+
WHERE tenant_id = ? AND user_id = ?
|
|
777
|
+
LIMIT 1
|
|
778
|
+
`).get(tenantId, userId);
|
|
779
|
+
const subscriptionId = String(row?.stripe_subscription_id || '').trim();
|
|
780
|
+
if (!subscriptionId) {
|
|
781
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
782
|
+
res.end(JSON.stringify({ error: 'No Stripe subscription is linked for this account.' }));
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
try {
|
|
786
|
+
const stripe = getStripeClient();
|
|
787
|
+
const current = await stripe.subscriptions.retrieve(subscriptionId);
|
|
788
|
+
const itemId = current.items.data[0]?.id;
|
|
789
|
+
if (!itemId) {
|
|
790
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
791
|
+
res.end(JSON.stringify({ error: 'Subscription has no updatable line item.' }));
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
const updated = await stripe.subscriptions.update(subscriptionId, {
|
|
795
|
+
items: [{ id: itemId, price: nextPriceId }],
|
|
796
|
+
proration_behavior: 'create_prorations',
|
|
797
|
+
metadata: {
|
|
798
|
+
planVersionId
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
802
|
+
res.end(JSON.stringify({ subscription: updated }));
|
|
803
|
+
}
|
|
804
|
+
catch (error) {
|
|
805
|
+
const statusCode = resolveErrorStatusCode(error, 400);
|
|
806
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
807
|
+
res.end(JSON.stringify({ error: String(error?.message || 'Unable to update subscription'), code: error?.code }));
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
addRoute('POST', '/api/taskforce/billing/webhook', async (req, res) => {
|
|
811
|
+
if (!ensureCloudBilling(req, res))
|
|
812
|
+
return;
|
|
813
|
+
const webhookSecret = String(process.env.TASKFORCE_STRIPE_WEBHOOK_SECRET || '').trim();
|
|
814
|
+
if (!webhookSecret) {
|
|
815
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
816
|
+
res.end(JSON.stringify({ error: 'TASKFORCE_STRIPE_WEBHOOK_SECRET is not configured.' }));
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
const signature = String(req.headers['stripe-signature'] || '').trim();
|
|
820
|
+
if (!signature) {
|
|
821
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
822
|
+
res.end(JSON.stringify({ error: 'Missing Stripe signature.' }));
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
let event;
|
|
826
|
+
let rawBody;
|
|
827
|
+
try {
|
|
828
|
+
rawBody = await readRawBody(req);
|
|
829
|
+
const stripe = getStripeClient();
|
|
830
|
+
event = stripe.webhooks.constructEvent(rawBody, signature, webhookSecret);
|
|
831
|
+
}
|
|
832
|
+
catch (error) {
|
|
833
|
+
const code = Number(error?.statusCode || 400);
|
|
834
|
+
res.writeHead(code >= 400 && code < 600 ? code : 400, { 'Content-Type': 'application/json' });
|
|
835
|
+
res.end(JSON.stringify({ error: String(error?.message || 'Invalid webhook payload/signature') }));
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
try {
|
|
839
|
+
const db = core.getDatabaseAdapter();
|
|
840
|
+
const tenantId = core.getTenantId();
|
|
841
|
+
purgeExpiredWebhookFailures(core);
|
|
842
|
+
let duplicate = false;
|
|
843
|
+
const tx = db.transaction(() => {
|
|
844
|
+
const acquired = lockWebhookEvent(core, event);
|
|
845
|
+
if (!acquired) {
|
|
846
|
+
duplicate = true;
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
if (event.type === 'checkout.session.completed') {
|
|
850
|
+
const session = event.data.object;
|
|
851
|
+
const metadata = session.metadata || {};
|
|
852
|
+
const customerId = String(session.customer || '').trim();
|
|
853
|
+
const subscriptionId = String(session.subscription || '').trim();
|
|
854
|
+
const userId = String(metadata.userId || metadata.user_id || '').trim()
|
|
855
|
+
|| resolveUserIdByCustomer(core, customerId)
|
|
856
|
+
|| resolveUserIdBySubscription(core, subscriptionId);
|
|
857
|
+
if (!userId || !customerId) {
|
|
858
|
+
throw new Error('Unable to link checkout completion to user/customer.');
|
|
859
|
+
}
|
|
860
|
+
core.createUserBillingRecord(userId);
|
|
861
|
+
upsertBillingMirror(core, {
|
|
862
|
+
userId,
|
|
863
|
+
stripeCustomerId: customerId,
|
|
864
|
+
stripeSubscriptionId: subscriptionId || null
|
|
865
|
+
});
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
if (event.type === 'customer.subscription.created' || event.type === 'customer.subscription.updated' || event.type === 'customer.subscription.deleted') {
|
|
869
|
+
const subscription = event.data.object;
|
|
870
|
+
const customerId = String(subscription.customer || '').trim();
|
|
871
|
+
const userId = resolveUserIdByCustomer(core, customerId)
|
|
872
|
+
|| String(subscription.metadata?.userId || subscription.metadata?.user_id || '').trim();
|
|
873
|
+
if (!userId) {
|
|
874
|
+
throw new Error('Unable to resolve user for subscription event.');
|
|
875
|
+
}
|
|
876
|
+
core.createUserBillingRecord(userId);
|
|
877
|
+
const stripeStatus = String(subscription.status || '').trim().toLowerCase();
|
|
878
|
+
const priceId = String(subscription.items?.data?.[0]?.price?.id || '').trim();
|
|
879
|
+
const billingInterval = parseInterval(subscription.items?.data?.[0]?.price?.recurring?.interval || '');
|
|
880
|
+
const planVersionId = resolvePlanVersionIdForPriceId(core, priceId);
|
|
881
|
+
const existingEntitlement = getEntitlementRow(core, userId);
|
|
882
|
+
const nowIso = new Date().toISOString();
|
|
883
|
+
const currentPeriodEnd = resolveSubscriptionCurrentPeriodEnd(subscription);
|
|
884
|
+
const trialEnd = isoFromUnixSeconds(subscription.trial_end);
|
|
885
|
+
const canceledState = stripeStatus === 'canceled'
|
|
886
|
+
? (currentPeriodEnd && currentPeriodEnd > nowIso ? 'active' : 'canceled')
|
|
887
|
+
: null;
|
|
888
|
+
const canonicalState = canceledState || mapStripeStatusToCanonicalState(stripeStatus);
|
|
889
|
+
const effectiveUntil = stripeStatus === 'canceled'
|
|
890
|
+
? currentPeriodEnd
|
|
891
|
+
: (currentPeriodEnd || null);
|
|
892
|
+
upsertBillingMirror(core, {
|
|
893
|
+
userId,
|
|
894
|
+
stripeCustomerId: customerId || null,
|
|
895
|
+
stripeSubscriptionId: String(subscription.id || '').trim() || null,
|
|
896
|
+
stripePriceId: priceId || null,
|
|
897
|
+
stripeStatus: stripeStatus || null,
|
|
898
|
+
planId: planVersionId ? resolvePlanIdForVersion(core, planVersionId) : null,
|
|
899
|
+
billingInterval: billingInterval || null,
|
|
900
|
+
canonicalState,
|
|
901
|
+
trialEnd,
|
|
902
|
+
currentPeriodEnd,
|
|
903
|
+
effectiveUntil
|
|
904
|
+
});
|
|
905
|
+
if (!planVersionId) {
|
|
906
|
+
recordWebhookFailure(core, {
|
|
907
|
+
eventId: event.id,
|
|
908
|
+
eventType: event.type,
|
|
909
|
+
reason: `Unknown price mapping for price_id=${priceId || '(empty)'}`,
|
|
910
|
+
payload: event
|
|
911
|
+
});
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
upsertCanonicalEntitlement(core, {
|
|
915
|
+
userId,
|
|
916
|
+
planVersionId: planVersionId || existingEntitlement?.plan_version_id || 'free-v1',
|
|
917
|
+
state: canonicalState,
|
|
918
|
+
effectiveAt: trialEnd || nowIso,
|
|
919
|
+
effectiveUntil
|
|
920
|
+
});
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
if (event.type === 'invoice.paid' || event.type === 'invoice.payment_failed') {
|
|
924
|
+
const invoice = event.data.object;
|
|
925
|
+
const customerId = String(invoice.customer || '').trim();
|
|
926
|
+
const subscriptionId = resolveInvoiceSubscriptionId(invoice);
|
|
927
|
+
const userId = resolveUserIdByCustomer(core, customerId)
|
|
928
|
+
|| resolveUserIdBySubscription(core, subscriptionId);
|
|
929
|
+
if (!userId) {
|
|
930
|
+
throw new Error('Unable to resolve user for invoice event.');
|
|
931
|
+
}
|
|
932
|
+
core.createUserBillingRecord(userId);
|
|
933
|
+
const { priceId, interval } = resolvePriceAndIntervalFromInvoice(invoice);
|
|
934
|
+
const planVersionId = resolvePlanVersionIdForPriceId(core, priceId);
|
|
935
|
+
const existingEntitlement = getEntitlementRow(core, userId);
|
|
936
|
+
const canonicalState = event.type === 'invoice.payment_failed' ? 'grace' : 'active';
|
|
937
|
+
const currentPeriodEnd = invoice.lines?.data?.[0]?.period?.end
|
|
938
|
+
? isoFromUnixSeconds(invoice.lines.data[0].period.end)
|
|
939
|
+
: null;
|
|
940
|
+
upsertBillingMirror(core, {
|
|
941
|
+
userId,
|
|
942
|
+
stripeCustomerId: customerId || null,
|
|
943
|
+
stripeSubscriptionId: subscriptionId || null,
|
|
944
|
+
stripePriceId: priceId || null,
|
|
945
|
+
stripeStatus: event.type === 'invoice.payment_failed' ? 'past_due' : 'active',
|
|
946
|
+
planId: planVersionId ? resolvePlanIdForVersion(core, planVersionId) : null,
|
|
947
|
+
billingInterval: interval || null,
|
|
948
|
+
canonicalState,
|
|
949
|
+
currentPeriodEnd,
|
|
950
|
+
effectiveUntil: currentPeriodEnd
|
|
951
|
+
});
|
|
952
|
+
if (!planVersionId) {
|
|
953
|
+
recordWebhookFailure(core, {
|
|
954
|
+
eventId: event.id,
|
|
955
|
+
eventType: event.type,
|
|
956
|
+
reason: `Unknown price mapping for price_id=${priceId || '(empty)'}`,
|
|
957
|
+
payload: event
|
|
958
|
+
});
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
upsertCanonicalEntitlement(core, {
|
|
962
|
+
userId,
|
|
963
|
+
planVersionId: planVersionId || existingEntitlement?.plan_version_id || 'free-v1',
|
|
964
|
+
state: canonicalState,
|
|
965
|
+
effectiveAt: new Date().toISOString(),
|
|
966
|
+
effectiveUntil: currentPeriodEnd
|
|
967
|
+
});
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
tx();
|
|
972
|
+
if (duplicate) {
|
|
973
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
974
|
+
res.end(JSON.stringify({ received: true, duplicate: true }));
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
978
|
+
res.end(JSON.stringify({ received: true }));
|
|
979
|
+
}
|
|
980
|
+
catch (error) {
|
|
981
|
+
recordWebhookFailure(core, {
|
|
982
|
+
eventId: event.id,
|
|
983
|
+
eventType: event.type,
|
|
984
|
+
reason: String(error?.message || error || 'WEBHOOK_PROCESSING_FAILED'),
|
|
985
|
+
payload: event
|
|
986
|
+
});
|
|
987
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
988
|
+
res.end(JSON.stringify({ error: 'Webhook processing failed. Retry expected.' }));
|
|
989
|
+
}
|
|
990
|
+
});
|
|
991
|
+
}
|