@taskforcehq/taskforce 0.3.304 → 0.3.306
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -247
- package/dist/Taskforce.module.css +295 -89
- package/dist/TaskforceCore.js +378 -111
- package/dist/TaskforceCore.test.js +1221 -107
- package/dist/assets/fonts/CourierPrime-Bold.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-BoldItalic.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-Italic.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-Regular.woff2 +0 -0
- package/dist/assets/fonts/Noir_medium.woff2 +0 -0
- package/dist/assets/fonts/Noir_regular.woff2 +0 -0
- package/dist/assets/fonts/RussoOne-Regular.woff2 +0 -0
- package/dist/assets/fonts/SpecialElite-Regular.woff2 +0 -0
- package/dist/compat/workspaceSyncCompat.js +52 -2
- package/dist/compat/workspaceSyncCompat.test.js +38 -1
- package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
- package/dist/components/context/ContextAttachmentManager.js +549 -0
- package/dist/components/features/AgentsModule.d.ts +10 -1
- package/dist/components/features/AgentsModule.js +260 -15
- package/dist/components/features/AgentsModule.test.js +255 -5
- package/dist/components/features/DocumentIndex.d.ts +1 -1
- package/dist/components/features/DocumentIndex.js +1 -1
- package/dist/components/features/DocumentViewer.d.ts +6 -2
- package/dist/components/features/DocumentViewer.js +90 -10
- package/dist/components/features/DocumentWorkspace.d.ts +5 -22
- package/dist/components/features/DocumentWorkspace.js +12 -155
- package/dist/components/features/DocumentWorkspace.test.js +226 -6
- package/dist/components/features/TaskSettings.js +29 -3
- package/dist/components/features/TaskSettings.test.js +42 -1
- package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
- package/dist/components/features/documentWorkspaceModel.js +57 -0
- package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
- package/dist/components/features/useDocumentWorkspaceController.js +113 -0
- package/dist/components/task/TaskCard.js +19 -28
- package/dist/components/task/TaskCard.test.js +38 -3
- package/dist/components/task/TaskContextUpload.js +4 -526
- package/dist/components/task/TaskForm.d.ts +8 -8
- package/dist/components/task/TaskForm.js +362 -230
- package/dist/components/task/TaskForm.test.js +558 -381
- package/dist/components/task/TaskKanban.d.ts +2 -1
- package/dist/components/task/TaskKanban.js +96 -14
- package/dist/components/task/TaskKanban.test.js +36 -0
- package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
- package/dist/components/ui/EditableAvatarButton.js +18 -0
- package/dist/components/views/PlanComparisonPage.d.ts +9 -1
- package/dist/components/views/PlanComparisonPage.js +70 -17
- package/dist/components/views/PlanComparisonPage.test.js +289 -41
- package/dist/components/views/PlansPage.js +239 -106
- package/dist/components/views/PlansPage.test.js +593 -42
- package/dist/components/views/StandaloneLayout.js +310 -554
- package/dist/components/views/panels/FilterToolbar.test.js +6 -4
- package/dist/components/views/panels/PlanningDrawer.d.ts +8 -1
- package/dist/components/views/panels/PlanningDrawer.js +18 -11
- package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
- package/dist/components/views/panels/PlanningDrawer.test.js +141 -0
- package/dist/components/views/planningScope.d.ts +20 -0
- package/dist/components/views/planningScope.js +25 -0
- package/dist/components/views/planningScope.test.d.ts +1 -0
- package/dist/components/views/planningScope.test.js +60 -0
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +27 -14
- package/dist/components/views/standalone/modals/AccountHubModal.js +87 -15
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +42 -2
- package/dist/components/views/standalone/modals/AvatarImageManagerModal.d.ts +20 -0
- package/dist/components/views/standalone/modals/AvatarImageManagerModal.js +127 -0
- package/dist/components/views/standalone/modals/EditProfileModal.d.ts +2 -9
- package/dist/components/views/standalone/modals/EditProfileModal.js +3 -2
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.js +6 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +1 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +24 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +2 -1
- package/dist/components/views/standalone/modals/SyncStatusModal.js +2 -2
- package/dist/config/envSchema.js +1 -1
- package/dist/core/AiProfileService.d.ts +45 -3
- package/dist/core/AiProfileService.js +774 -63
- package/dist/core/AiProfileService.test.js +22 -2
- package/dist/core/AiProfiles.test.js +761 -2
- package/dist/core/AttachmentLinkService.js +57 -15
- package/dist/core/AuthIdentityService.d.ts +107 -0
- package/dist/core/AuthIdentityService.js +284 -0
- package/dist/core/AuthTokenService.d.ts +4 -0
- package/dist/core/AuthTokenService.js +34 -6
- package/dist/core/AuthTokenService.test.d.ts +1 -0
- package/dist/core/AuthTokenService.test.js +78 -0
- package/dist/core/EntitlementsPolicy.test.js +77 -15
- package/dist/core/GlobalSettingsService.js +115 -6
- package/dist/core/PlanEntitlementService.d.ts +30 -3
- package/dist/core/PlanEntitlementService.js +454 -93
- package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
- package/dist/core/PlanFeatureCatalog.test.js +84 -0
- package/dist/core/PlanVersionPolicy.test.js +54 -28
- package/dist/core/PlanningEntities.test.js +52 -0
- package/dist/core/SignupMonetizationSettings.test.js +241 -44
- package/dist/core/SyncReconciliationService.js +2 -65
- package/dist/core/SystemAdmin.test.js +244 -88
- package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
- package/dist/core/TaskTaxonomyValidation.test.js +53 -0
- package/dist/core/Taskforce.d.ts +228 -14
- package/dist/core/Taskforce.js +814 -201
- package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
- package/dist/core/Taskforce.listTasksSlim.test.js +91 -0
- package/dist/core/UserProfileAvatarDrafts.test.js +30 -3
- package/dist/core/WorkspaceLifecycleService.js +0 -5
- package/dist/core/WorkspacePermissions.test.js +25 -1
- package/dist/core/WorkspacePlanMode.test.js +128 -27
- package/dist/core/shared.d.ts +3 -1
- package/dist/core/shared.js +16 -1
- package/dist/core/types.d.ts +68 -2
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +26 -1
- package/dist/hooks/sync/auth.js +3 -1
- package/dist/hooks/sync/bootstrap.js +2 -0
- package/dist/hooks/sync/controlPlane.d.ts +37 -0
- package/dist/hooks/sync/controlPlane.js +78 -0
- package/dist/hooks/sync/controlPlane.test.d.ts +1 -0
- package/dist/hooks/sync/controlPlane.test.js +121 -0
- package/dist/hooks/sync/lifecyclePolicy.d.ts +45 -0
- package/dist/hooks/sync/lifecyclePolicy.js +93 -0
- package/dist/hooks/sync/lifecyclePolicy.test.d.ts +1 -0
- package/dist/hooks/sync/lifecyclePolicy.test.js +124 -0
- package/dist/hooks/sync/orchestratorShared.d.ts +10 -1
- package/dist/hooks/sync/orchestratorShared.js +68 -47
- package/dist/hooks/sync/orchestratorShared.test.js +50 -1
- package/dist/hooks/sync/pullLifecycle.d.ts +31 -0
- package/dist/hooks/sync/pullLifecycle.js +24 -0
- package/dist/hooks/sync/pullLifecycle.test.d.ts +1 -0
- package/dist/hooks/sync/pullLifecycle.test.js +80 -0
- package/dist/hooks/sync/recovery.d.ts +16 -2
- package/dist/hooks/sync/recovery.js +171 -53
- package/dist/hooks/sync/recovery.test.d.ts +1 -0
- package/dist/hooks/sync/recovery.test.js +292 -0
- package/dist/hooks/sync/transfers.d.ts +16 -3
- package/dist/hooks/sync/transfers.js +186 -65
- package/dist/hooks/sync/transfers.test.d.ts +1 -0
- package/dist/hooks/sync/transfers.test.js +396 -0
- package/dist/hooks/sync/useRecentSyncEvents.d.ts +21 -0
- package/dist/hooks/sync/useRecentSyncEvents.js +92 -0
- package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +1 -0
- package/dist/hooks/sync/useRecentSyncEvents.test.js +124 -0
- package/dist/hooks/sync/useSyncStatusActions.d.ts +30 -0
- package/dist/hooks/sync/useSyncStatusActions.js +88 -0
- package/dist/hooks/sync/useSyncStatusActions.test.d.ts +1 -0
- package/dist/hooks/sync/useSyncStatusActions.test.js +133 -0
- package/dist/hooks/sync/useSyncStatusControls.d.ts +25 -0
- package/dist/hooks/sync/useSyncStatusControls.js +60 -0
- package/dist/hooks/sync/useSyncStatusControls.test.d.ts +1 -0
- package/dist/hooks/sync/useSyncStatusControls.test.js +88 -0
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +12 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +16 -6
- package/dist/hooks/useSyncOrchestrator.js +424 -175
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +487 -10
- package/dist/hooks/useTaskData.js +8 -3
- package/dist/hooks/useTaskData.test.js +45 -0
- package/dist/hooks/useTaskMutations.d.ts +2 -1
- package/dist/hooks/useTaskMutations.js +12 -1
- package/dist/hooks/useTaskMutations.test.js +30 -1
- package/dist/hooks/useTaskforce.d.ts +65 -4
- package/dist/hooks/useTaskforce.js +564 -243
- package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
- package/dist/hooks/useTaskforce.runtime-routing.test.js +152 -0
- package/dist/hooks/useTaskforce.sync-behavior.test.js +2454 -208
- package/dist/hooks/useWorkspaceSyncController.d.ts +4 -1
- package/dist/hooks/useWorkspaceSyncController.js +17 -2
- package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +316 -35
- package/dist/localization/locales/en-US.d.ts +0 -1
- package/dist/localization/locales/en-US.js +0 -1
- package/dist/localization/locales/es-419.js +0 -1
- package/dist/localization/locales/pt-BR.js +0 -1
- package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
- package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
- package/dist/mcp/collaborationRegistrar.d.ts +2 -0
- package/dist/mcp/collaborationRegistrar.js +71 -0
- package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
- package/dist/mcp/documentAssetRegistrar.js +346 -0
- package/dist/mcp/runtime.d.ts +2 -0
- package/dist/mcp/runtime.js +2433 -3534
- package/dist/mcp/runtime.test.js +440 -2
- package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
- package/dist/mcp/taskPlanningRegistrar.js +418 -0
- package/dist/mcp/toolCatalog.d.ts +35 -0
- package/dist/mcp/toolCatalog.js +3 -0
- package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
- package/dist/migrations/taskSchemaMigrations.js +171 -61
- package/dist/migrations/taskSchemaMigrations.test.js +15 -0
- package/dist/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
- package/dist/resources/templates/workflows/collaborate.yaml +1 -1
- package/dist/resources/templates/workflows/evaluate.yaml +2 -2
- package/dist/resources/templates/workflows/plan.yaml +1 -1
- package/dist/resources/templates/workflows/review.yaml +2 -2
- package/dist/server/annotatedAttachmentsRoutes.test.js +3 -3
- package/dist/server/auth/providers/apple.d.ts +33 -0
- package/dist/server/auth/providers/apple.js +82 -0
- package/dist/server/auth/providers/appleClientSecret.d.ts +37 -0
- package/dist/server/auth/providers/appleClientSecret.js +65 -0
- package/dist/server/auth/providers/github.d.ts +26 -0
- package/dist/server/auth/providers/github.js +86 -0
- package/dist/server/auth/providers/google.d.ts +21 -0
- package/dist/server/auth/providers/google.js +56 -0
- package/dist/server/auth/providers/types.d.ts +21 -0
- package/dist/server/auth/providers/types.js +1 -0
- package/dist/server/auth.d.ts +2 -0
- package/dist/server/auth.js +6 -3
- package/dist/server/documentReviewRoutes.test.js +36 -3
- package/dist/server/index.cookieProxy.test.js +1 -0
- package/dist/server/index.d.ts +12 -0
- package/dist/server/index.js +120 -9
- package/dist/server/index.rateLimit.test.js +3 -0
- package/dist/server/index.test.js +106 -3
- package/dist/server/routes/admin.d.ts +4 -5
- package/dist/server/routes/admin.js +410 -80
- package/dist/server/routes/annotatedAttachments.d.ts +1 -1
- package/dist/server/routes/annotatedAttachments.js +1 -1
- package/dist/server/routes/auth.d.ts +16 -6
- package/dist/server/routes/auth.js +1015 -118
- package/dist/server/routes/authSupport.d.ts +30 -0
- package/dist/server/routes/authSupport.js +81 -0
- package/dist/server/routes/billing.d.ts +1 -1
- package/dist/server/routes/billing.js +276 -34
- package/dist/server/routes/billing.test.js +760 -52
- package/dist/server/routes/documentReviews.d.ts +1 -1
- package/dist/server/routes/documentReviews.js +1 -1
- package/dist/server/routes/documents.d.ts +5 -2
- package/dist/server/routes/documents.js +242 -74
- package/dist/server/routes/primitives.d.ts +11 -0
- package/dist/server/routes/primitives.js +73 -0
- package/dist/server/routes/resources.d.ts +1 -1
- package/dist/server/routes/resources.js +1 -1
- package/dist/server/routes/shared.d.ts +29 -3
- package/dist/server/routes/shared.js +69 -4
- package/dist/server/routes/sync.d.ts +1 -1
- package/dist/server/routes/sync.integration.test.js +437 -39
- package/dist/server/routes/sync.js +62 -606
- package/dist/server/routes/syncAuxRoutes.d.ts +15 -0
- package/dist/server/routes/syncAuxRoutes.js +91 -0
- package/dist/server/routes/syncAuxRoutes.test.d.ts +1 -0
- package/dist/server/routes/syncAuxRoutes.test.js +158 -0
- package/dist/server/routes/syncPullApplyRoutes.d.ts +36 -0
- package/dist/server/routes/syncPullApplyRoutes.js +204 -0
- package/dist/server/routes/syncPushRoutes.d.ts +24 -0
- package/dist/server/routes/syncPushRoutes.js +212 -0
- package/dist/server/routes/syncRouteGuards.d.ts +36 -0
- package/dist/server/routes/syncRouteGuards.js +67 -0
- package/dist/server/routes/syncRouteGuards.test.d.ts +1 -0
- package/dist/server/routes/syncRouteGuards.test.js +94 -0
- package/dist/server/routes/syncRouteTypes.d.ts +13 -0
- package/dist/server/routes/syncRouteTypes.js +1 -0
- package/dist/server/routes/syncSnapshotRoutes.d.ts +66 -0
- package/dist/server/routes/syncSnapshotRoutes.js +180 -0
- package/dist/server/routes/tasks.d.ts +1 -1
- package/dist/server/routes/tasks.js +44 -2
- package/dist/server/routes/workspaces.d.ts +1 -1
- package/dist/server/routes/workspaces.js +37 -6
- package/dist/server/routes.d.ts +2 -9
- package/dist/server/routes.js +136 -165
- package/dist/server/routes.test.js +2457 -162
- package/dist/services/workflowExportSnapshots.test.js +2 -0
- package/dist/shared/aiProfileSeatScope.d.ts +5 -0
- package/dist/shared/aiProfileSeatScope.js +21 -0
- package/dist/shared/passwordPolicy.d.ts +13 -0
- package/dist/shared/passwordPolicy.js +84 -0
- package/dist/shared/passwordPolicy.test.d.ts +1 -0
- package/dist/shared/passwordPolicy.test.js +34 -0
- package/dist/storage/workspaceAssetStore.d.ts +15 -3
- package/dist/storage/workspaceAssetStore.js +132 -44
- package/dist/storage/workspaceAssetStore.test.js +93 -6
- package/dist/styles/fonts.css +70 -0
- package/dist/sync/collaborationSyncPayload.d.ts +6 -1
- package/dist/sync/collaborationSyncPayload.js +17 -11
- package/dist/sync/collaborationSyncPayload.test.js +13 -1
- package/dist/sync/contentSyncState.d.ts +4 -8
- package/dist/sync/contentSyncState.js +33 -77
- package/dist/sync/syncApplyHandlers.d.ts +7 -0
- package/dist/sync/syncApplyHandlers.js +28 -13
- package/dist/sync/syncApplyHandlers.test.js +72 -3
- package/dist/sync/syncFieldSemantics.d.ts +12 -0
- package/dist/sync/syncFieldSemantics.js +55 -0
- package/dist/sync/syncResourceAdapters.d.ts +44 -0
- package/dist/sync/syncResourceAdapters.js +77 -0
- package/dist/sync/syncService.d.ts +6 -0
- package/dist/sync/syncService.js +60 -0
- package/dist/sync/syncService.test.js +71 -3
- package/dist/sync/taskSyncChangeKey.d.ts +2 -0
- package/dist/sync/taskSyncChangeKey.js +143 -0
- package/dist/sync/taskSyncPayload.js +3 -8
- package/dist/sync/workspacePullFeed.js +4 -3
- package/dist/sync/workspacePullFeed.test.js +43 -0
- package/dist/sync/workspaceRepair.js +5 -2
- package/dist/sync/workspaceSyncModel.d.ts +48 -0
- package/dist/sync/workspaceSyncModel.js +195 -31
- package/dist/sync/workspaceSyncModel.test.js +325 -12
- package/dist/sync/workspaceSyncState.d.ts +19 -0
- package/dist/sync/workspaceSyncState.js +3 -1
- package/dist/sync/workspaceSyncSurface.js +3 -8
- package/dist/types.d.ts +17 -0
- package/dist/ui/assets/AgentsModule-DrLawwNl.js +1 -0
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BlS-cqnl.js → AnnotatedAttachmentWorkspace-Z9_whf7F.js} +1 -1
- package/dist/ui/assets/CourierPrime-Bold-BuLj_dpw.woff2 +0 -0
- package/dist/ui/assets/CourierPrime-BoldItalic-BrK2pUkZ.woff2 +0 -0
- package/dist/ui/assets/CourierPrime-Italic-DFUZK5Ey.woff2 +0 -0
- package/dist/ui/assets/CourierPrime-Regular-BsKphzVf.woff2 +0 -0
- package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +252 -0
- package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +1 -0
- package/dist/ui/assets/{InitiativesModule-BjR6iBf9.js → InitiativesModule-9E6ParrY.js} +1 -1
- package/dist/ui/assets/Noir_medium-bxQwKGzB.woff2 +0 -0
- package/dist/ui/assets/Noir_regular-ojf2kxlG.woff2 +0 -0
- package/dist/ui/assets/PlansPage-CvfnMiWq.css +1 -0
- package/dist/ui/assets/PlansPage-DRXscYxH.js +1 -0
- package/dist/ui/assets/RussoOne-Regular-Cu_qq_qC.woff2 +0 -0
- package/dist/ui/assets/SpecialElite-Regular-Di6gSvAS.woff2 +0 -0
- package/dist/ui/assets/TaskSettings-DZX4jk7e.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-DnFqdUME.js → WorkflowsModule-BA7jcEpH.js} +1 -1
- package/dist/ui/assets/index-9eT8e0Lu.css +1 -0
- package/dist/ui/assets/index-C6k75jr8.js +6 -0
- package/dist/ui/assets/{vendor-icons-I7ZwG1z_.js → vendor-icons-DuEd_65c.js} +1 -1
- package/dist/ui/fonts/CourierPrime-Bold.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-Bold.woff2 +0 -0
- package/dist/ui/fonts/CourierPrime-BoldItalic.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-BoldItalic.woff2 +0 -0
- package/dist/ui/fonts/CourierPrime-Italic.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-Italic.woff2 +0 -0
- package/dist/ui/fonts/CourierPrime-Regular.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-Regular.woff2 +0 -0
- package/dist/ui/fonts/Noir_medium.otf +0 -0
- package/dist/ui/fonts/Noir_medium.woff2 +0 -0
- package/dist/ui/fonts/Noir_regular.otf +0 -0
- package/dist/ui/fonts/Noir_regular.woff2 +0 -0
- package/dist/ui/fonts/RussoOne-Regular.woff2 +0 -0
- package/dist/ui/fonts/SpecialElite-Regular.ttf +0 -0
- package/dist/ui/fonts/SpecialElite-Regular.woff2 +0 -0
- package/dist/ui/index.html +3 -3
- package/dist/utils/accountProfileSummaryCache.d.ts +5 -0
- package/dist/utils/assignees.d.ts +2 -0
- package/dist/utils/assignees.js +2 -0
- package/dist/utils/avatarUpload.d.ts +27 -0
- package/dist/utils/avatarUpload.js +132 -0
- package/dist/utils/avatarUpload.test.d.ts +1 -0
- package/dist/utils/avatarUpload.test.js +40 -0
- package/dist/utils/bootstrapTrace.d.ts +7 -0
- package/dist/utils/bootstrapTrace.js +29 -0
- package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
- package/dist/utils/commercialLifecyclePresentation.js +199 -0
- package/dist/utils/contextAssetEvents.d.ts +2 -0
- package/dist/utils/syncEventDetails.d.ts +3 -0
- package/dist/utils/syncEventDetails.js +64 -0
- package/dist/utils/syncEventDetails.test.d.ts +1 -0
- package/dist/utils/syncEventDetails.test.js +23 -0
- package/dist/utils/syncEventPresentation.d.ts +15 -0
- package/dist/utils/syncEventPresentation.js +129 -0
- package/dist/utils/syncEventPresentation.test.d.ts +1 -0
- package/dist/utils/syncEventPresentation.test.js +64 -0
- package/dist/utils/syncStatusPresentation.d.ts +72 -0
- package/dist/utils/syncStatusPresentation.js +217 -0
- package/dist/utils/syncStatusPresentation.test.d.ts +1 -0
- package/dist/utils/syncStatusPresentation.test.js +164 -0
- package/dist/utils/taskActivity.d.ts +2 -0
- package/dist/utils/taskActivity.js +228 -22
- package/dist/utils/taskActivity.test.js +119 -8
- package/dist/utils/workspaceSyncPresentation.d.ts +4 -0
- package/dist/utils/workspaceSyncPresentation.js +68 -2
- package/dist/utils/workspaceSyncPresentation.test.js +36 -1
- package/package.json +10 -2
- package/scripts/check-cloud-mcp.mjs +83 -0
- package/scripts/export-sqlite-to-postgres.mjs +7 -9
- package/scripts/playwright-auth.sh +5 -1
- package/scripts/run-billing-performance-smoke.sh +24 -0
- package/scripts/run-e2e-realtime.sh +17 -3
- package/scripts/run-smoke.sh +17 -5
- package/scripts/run-soak.sh +17 -5
- package/src/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
- package/src/resources/templates/workflows/collaborate.yaml +1 -1
- package/src/resources/templates/workflows/evaluate.yaml +2 -2
- package/src/resources/templates/workflows/plan.yaml +1 -1
- package/src/resources/templates/workflows/review.yaml +2 -2
- package/dist/ui/assets/AgentsModule-BLWVbuKP.js +0 -1
- package/dist/ui/assets/DocumentWorkspace-BH_6DF6x.js +0 -250
- package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
- package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
- package/dist/ui/assets/PlansPage-CSDQ4sLa.js +0 -1
- package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
- package/dist/ui/assets/TaskSettings-CEDy34Jo.js +0 -9
- package/dist/ui/assets/index-DXUdtH1B.js +0 -6
- package/dist/ui/assets/index-Ii0B0rTO.css +0 -1
- package/scripts/migrate-planning-model.ts +0 -233
|
@@ -6,9 +6,20 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Routes registered here:
|
|
8
8
|
* GET /api/taskforce/auth/runtime-config
|
|
9
|
+
* GET /api/taskforce/auth/providers
|
|
10
|
+
* GET /api/taskforce/auth/oauth/:provider/start
|
|
11
|
+
* GET /api/taskforce/auth/oauth/:provider/callback
|
|
12
|
+
* POST /api/taskforce/auth/oauth/apple/callback
|
|
13
|
+
* GET /api/taskforce/account/login-methods
|
|
14
|
+
* DELETE /api/taskforce/account/login-methods/:provider
|
|
15
|
+
* POST /api/taskforce/account/login-methods/password
|
|
9
16
|
* GET /api/taskforce/auth/session
|
|
10
17
|
* GET /api/taskforce/auth/admin-session
|
|
11
18
|
* POST /api/taskforce/auth/profile
|
|
19
|
+
* POST /api/taskforce/auth/profile/avatar/init
|
|
20
|
+
* POST /api/taskforce/auth/profile/avatar/upload
|
|
21
|
+
* POST /api/taskforce/auth/profile/avatar/finalize
|
|
22
|
+
* POST /api/taskforce/auth/profile/avatar/discard
|
|
12
23
|
* POST /api/taskforce/auth/login
|
|
13
24
|
* POST /api/taskforce/auth/register
|
|
14
25
|
* POST /api/taskforce/auth/verify-email/request
|
|
@@ -26,6 +37,7 @@
|
|
|
26
37
|
* GET /api/taskforce/account/plan-preview
|
|
27
38
|
* GET /api/taskforce/auth/workspace-members
|
|
28
39
|
* GET /api/taskforce/workspace/assignee-options
|
|
40
|
+
* POST /api/taskforce/workspace/ai-profiles/avatar
|
|
29
41
|
* GET /api/taskforce/admin/users
|
|
30
42
|
* POST /api/taskforce/admin/users/invite
|
|
31
43
|
* PATCH /api/taskforce/admin/users/:id/role
|
|
@@ -38,36 +50,26 @@
|
|
|
38
50
|
import * as fs from 'fs';
|
|
39
51
|
import * as path from 'path';
|
|
40
52
|
import { randomUUID } from 'crypto';
|
|
41
|
-
import { parseJsonBody } from '
|
|
53
|
+
import { parseJsonBody, parseFormBody } from './primitives.js';
|
|
54
|
+
import { buildGoogleAuthUrl, exchangeGoogleCode } from '../auth/providers/google.js';
|
|
55
|
+
import { buildAppleAuthUrl, exchangeAppleCode } from '../auth/providers/apple.js';
|
|
56
|
+
import { buildGithubAuthUrl, exchangeGithubCode } from '../auth/providers/github.js';
|
|
42
57
|
import { hasValidSession, createClearSessionCookie, createSessionTokenCookie, resolveRequestAccess, resolveSessionCookieAttributesForRequest, } from '../auth.js';
|
|
43
|
-
import {
|
|
58
|
+
import { sendEmail } from '../email.js';
|
|
44
59
|
import { resolveDeploymentConfig } from '../../config/deployment.js';
|
|
45
60
|
import { resolveRuntimeModeContract } from '../../shared/runtimeContract.js';
|
|
46
|
-
import { requestHeader, resolveErrorStatusCode } from './shared.js';
|
|
61
|
+
import { getRouteTimingNow, logSlowBootstrapRoute, requestBootstrapTraceId, requestHeader, resolveErrorStatusCode, serializeJsonWithTiming, setServerTimingHeader } from './shared.js';
|
|
62
|
+
import { buildAuthFlowBehavior, buildSiteBehavior, getRouteEmailConfig, resolvePublicAuthBaseUrl, resolveRuntimeConfigBaseUrl, shouldExposeDevAuthTokens } from './authSupport.js';
|
|
47
63
|
import { normalizeTaskDataRelativePath } from '../../storage/objectStorageClient.js';
|
|
48
64
|
// === SECTION: Registration ===
|
|
49
65
|
export function registerAuthRoutes(deps) {
|
|
50
|
-
const { addRoute, core, context, requestWorkspaceId, requestUserId, ensureAppAccess, ensureAuthenticatedUser, ensureWorkspaceAdminRole, ensureCloudPlanData, requireCloudRuntime, auditAuthAction, resolveAdminRequestActor,
|
|
66
|
+
const { addRoute, core, context, requestWorkspaceId, requestUserId, resolveAuthenticatedUserId, ensureAppAccess, ensureAuthenticatedUser, ensureWorkspaceAdminRole, ensureCloudPlanData, requireCloudRuntime, auditAuthAction, resolveAdminRequestActor, resolveEffectiveObjectStorageConfig, getObjectStorageClient, } = deps;
|
|
51
67
|
const normalizeTeamWorkspaceRole = (value) => {
|
|
52
68
|
const role = String(value || '').trim().toLowerCase();
|
|
53
69
|
if (role === 'owner' || role === 'admin' || role === 'member' || role === 'read-only')
|
|
54
70
|
return role;
|
|
55
71
|
return null;
|
|
56
72
|
};
|
|
57
|
-
const resolveBooleanSetting = (value, fallback) => {
|
|
58
|
-
if (typeof value === 'boolean')
|
|
59
|
-
return value;
|
|
60
|
-
if (typeof value === 'number')
|
|
61
|
-
return value !== 0;
|
|
62
|
-
const normalized = String(value || '').trim().toLowerCase();
|
|
63
|
-
if (!normalized)
|
|
64
|
-
return fallback;
|
|
65
|
-
if (['1', 'true', 'yes', 'on'].includes(normalized))
|
|
66
|
-
return true;
|
|
67
|
-
if (['0', 'false', 'no', 'off'].includes(normalized))
|
|
68
|
-
return false;
|
|
69
|
-
return fallback;
|
|
70
|
-
};
|
|
71
73
|
const resolveTeamPlanModeFromEntitlements = (payload) => {
|
|
72
74
|
const state = String(payload?.state || '').trim().toLowerCase();
|
|
73
75
|
const teamAllowed = payload?.features?.['collaboration.team_management']?.allowed === true;
|
|
@@ -116,7 +118,7 @@ export function registerAuthRoutes(deps) {
|
|
|
116
118
|
return null;
|
|
117
119
|
};
|
|
118
120
|
const buildAccountProfileSummary = (req) => {
|
|
119
|
-
const userId =
|
|
121
|
+
const userId = resolveAuthenticatedUserId(req);
|
|
120
122
|
const workspaceId = requestWorkspaceId(req);
|
|
121
123
|
const access = core.getAccountAccessStatus(userId);
|
|
122
124
|
const resolvedAccess = core.resolveUserAccess(userId, workspaceId, 'member');
|
|
@@ -138,8 +140,12 @@ export function registerAuthRoutes(deps) {
|
|
|
138
140
|
catch {
|
|
139
141
|
billingRow = undefined;
|
|
140
142
|
}
|
|
141
|
-
const
|
|
143
|
+
const allowBillingPlanFallback = access.gate === 'plan_selection_required' || access.gate === 'checkout_pending';
|
|
144
|
+
const planId = String(access.entitlement?.planId
|
|
145
|
+
|| (allowBillingPlanFallback ? billingRow?.tier : '')
|
|
146
|
+
|| '').trim() || null;
|
|
142
147
|
const planVersionId = String(access.entitlement?.planVersionId || '').trim() || null;
|
|
148
|
+
const entitlementState = access.entitlement?.state || null;
|
|
143
149
|
let planRow;
|
|
144
150
|
if (planId) {
|
|
145
151
|
try {
|
|
@@ -164,6 +170,7 @@ export function registerAuthRoutes(deps) {
|
|
|
164
170
|
teamPlanMode = 'personal';
|
|
165
171
|
}
|
|
166
172
|
}
|
|
173
|
+
const aiProfileSeatUsage = core.countAiProfileUsage(workspaceId);
|
|
167
174
|
const billingConflictCode = resolveBillingConflictCodeForUser(userId, access.gate);
|
|
168
175
|
return {
|
|
169
176
|
stripeStatus: billingRow?.stripe_status || null,
|
|
@@ -174,7 +181,7 @@ export function registerAuthRoutes(deps) {
|
|
|
174
181
|
trialEnd: billingRow?.trial_end || null,
|
|
175
182
|
currentPeriodEnd: billingRow?.current_period_end || null,
|
|
176
183
|
effectiveUntil: billingRow?.effective_until || null,
|
|
177
|
-
entitlementState
|
|
184
|
+
entitlementState,
|
|
178
185
|
planVersionId,
|
|
179
186
|
planId,
|
|
180
187
|
planName: planRow?.display_name ? String(planRow.display_name) : planId,
|
|
@@ -186,46 +193,11 @@ export function registerAuthRoutes(deps) {
|
|
|
186
193
|
workspaceRole,
|
|
187
194
|
canManageWorkspace,
|
|
188
195
|
teamPlanMode,
|
|
189
|
-
teamManagementAllowed: canManageWorkspace && teamPlanMode === 'team'
|
|
196
|
+
teamManagementAllowed: canManageWorkspace && teamPlanMode === 'team',
|
|
197
|
+
aiProfileSeatUsage
|
|
190
198
|
};
|
|
191
199
|
};
|
|
192
200
|
// === SECTION: Email delivery helpers ===
|
|
193
|
-
const getEmailConfig = () => {
|
|
194
|
-
if (context.authConfig?.runtimeMode !== 'cloud') {
|
|
195
|
-
return resolveEffectiveEmailConfig(process.env, { provider: 'console' });
|
|
196
|
-
}
|
|
197
|
-
const globalSettings = core.getGlobalSettings();
|
|
198
|
-
return resolveEffectiveEmailConfig(process.env, globalSettings.email || null);
|
|
199
|
-
};
|
|
200
|
-
const getAuthBehavior = () => {
|
|
201
|
-
const globalSettings = core.getGlobalSettings();
|
|
202
|
-
const authSettings = globalSettings.auth || {};
|
|
203
|
-
const authRateLimitMaxRequestsRaw = Number(authSettings.authRateLimitMaxRequests);
|
|
204
|
-
const authRateLimitWindowMsRaw = Number(authSettings.authRateLimitWindowMs);
|
|
205
|
-
const onboardingPolicy = core.getOnboardingPolicy();
|
|
206
|
-
return {
|
|
207
|
-
allowSelfRegistration: resolveBooleanSetting(authSettings.allowSelfRegistration, true),
|
|
208
|
-
requireVerifiedEmail: resolveBooleanSetting(authSettings.requireVerifiedEmail, true),
|
|
209
|
-
defaultNewUserBetaAccess: resolveBooleanSetting(authSettings.defaultNewUserBetaAccess, false),
|
|
210
|
-
authRateLimitEnabled: resolveBooleanSetting(authSettings.authRateLimitEnabled, true),
|
|
211
|
-
authRateLimitMaxRequests: Number.isFinite(authRateLimitMaxRequestsRaw) && authRateLimitMaxRequestsRaw >= 1
|
|
212
|
-
? Math.floor(authRateLimitMaxRequestsRaw)
|
|
213
|
-
: 10,
|
|
214
|
-
authRateLimitWindowMs: Number.isFinite(authRateLimitWindowMsRaw) && authRateLimitWindowMsRaw >= 1000
|
|
215
|
-
? Math.floor(authRateLimitWindowMsRaw)
|
|
216
|
-
: 15 * 60_000,
|
|
217
|
-
onboardingPolicyMode: onboardingPolicy.mode,
|
|
218
|
-
onboardingPlanVersionId: onboardingPolicy.planVersionId
|
|
219
|
-
};
|
|
220
|
-
};
|
|
221
|
-
const getSiteBehavior = () => {
|
|
222
|
-
const globalSettings = core.getGlobalSettings();
|
|
223
|
-
const siteSettings = globalSettings.site || {};
|
|
224
|
-
return {
|
|
225
|
-
showRunTaskforceLocally: resolveBooleanSetting(siteSettings.showRunTaskforceLocally, true),
|
|
226
|
-
pricingPageEnabled: resolveBooleanSetting(siteSettings.pricingPageEnabled, true)
|
|
227
|
-
};
|
|
228
|
-
};
|
|
229
201
|
const buildPasswordSessionCookieForUser = (req, authConfig, userIdRaw, options) => {
|
|
230
202
|
const userId = String(userIdRaw || '').trim();
|
|
231
203
|
const fallbackRole = String(options?.fallbackRole || authConfig.defaultRole || 'member').trim() || 'member';
|
|
@@ -236,7 +208,7 @@ export function registerAuthRoutes(deps) {
|
|
|
236
208
|
const access = resolvedWorkspaceId
|
|
237
209
|
? core.resolveUserAccess(userId, resolvedWorkspaceId, fallbackRole)
|
|
238
210
|
: null;
|
|
239
|
-
return createSessionTokenCookie(`
|
|
211
|
+
return createSessionTokenCookie(`usr:${userId}`, authConfig, {
|
|
240
212
|
role: (access?.role || fallbackRole),
|
|
241
213
|
workspaceId: access?.workspaceId || '',
|
|
242
214
|
userId,
|
|
@@ -290,6 +262,29 @@ export function registerAuthRoutes(deps) {
|
|
|
290
262
|
await deleteStoredAvatarObject(draft.storageKey);
|
|
291
263
|
}
|
|
292
264
|
};
|
|
265
|
+
const parseBinaryBody = async (req, maxBytes) => {
|
|
266
|
+
const safeMaxBytes = Number.isFinite(maxBytes) && maxBytes > 0
|
|
267
|
+
? Math.floor(maxBytes)
|
|
268
|
+
: MAX_AVATAR_UPLOAD_BYTES;
|
|
269
|
+
return new Promise((resolve, reject) => {
|
|
270
|
+
const chunks = [];
|
|
271
|
+
let receivedBytes = 0;
|
|
272
|
+
req.on('data', (chunk) => {
|
|
273
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
274
|
+
receivedBytes += buffer.length;
|
|
275
|
+
if (receivedBytes > safeMaxBytes) {
|
|
276
|
+
req.pause();
|
|
277
|
+
reject(Object.assign(new Error(`Payload too large (max ${safeMaxBytes} bytes)`), { statusCode: 413 }));
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
chunks.push(buffer);
|
|
281
|
+
});
|
|
282
|
+
req.on('end', () => {
|
|
283
|
+
resolve(Buffer.concat(chunks));
|
|
284
|
+
});
|
|
285
|
+
req.on('error', reject);
|
|
286
|
+
});
|
|
287
|
+
};
|
|
293
288
|
const logAuthLink = (kind, email, token, req) => {
|
|
294
289
|
const base = resolvePublicAuthBaseUrl(req);
|
|
295
290
|
const urlPath = kind === 'verify-email'
|
|
@@ -314,7 +309,7 @@ export function registerAuthRoutes(deps) {
|
|
|
314
309
|
to: email,
|
|
315
310
|
subject,
|
|
316
311
|
text
|
|
317
|
-
},
|
|
312
|
+
}, getRouteEmailConfig(core, context));
|
|
318
313
|
if (result.ok) {
|
|
319
314
|
console.info(`[Taskforce Auth Email] kind=${kind} provider=${result.provider} to=${email} messageId=${result.messageId || '-'}`);
|
|
320
315
|
}
|
|
@@ -332,7 +327,7 @@ export function registerAuthRoutes(deps) {
|
|
|
332
327
|
to: email,
|
|
333
328
|
subject,
|
|
334
329
|
text
|
|
335
|
-
},
|
|
330
|
+
}, getRouteEmailConfig(core, context));
|
|
336
331
|
if (result.ok) {
|
|
337
332
|
console.info(`[Taskforce Auth Email] kind=invite provider=${result.provider} to=${email} messageId=${result.messageId || '-'}`);
|
|
338
333
|
}
|
|
@@ -368,7 +363,7 @@ export function registerAuthRoutes(deps) {
|
|
|
368
363
|
}));
|
|
369
364
|
});
|
|
370
365
|
addRoute('GET', '/api/taskforce/public/site-config', async (_req, res) => {
|
|
371
|
-
const siteBehavior =
|
|
366
|
+
const siteBehavior = buildSiteBehavior(core);
|
|
372
367
|
res.writeHead(200, {
|
|
373
368
|
'Content-Type': 'application/json',
|
|
374
369
|
'Cross-Origin-Resource-Policy': 'cross-origin'
|
|
@@ -379,7 +374,7 @@ export function registerAuthRoutes(deps) {
|
|
|
379
374
|
}));
|
|
380
375
|
});
|
|
381
376
|
addRoute('GET', '/api/taskforce/public/site-config-script', async (_req, res) => {
|
|
382
|
-
const siteBehavior =
|
|
377
|
+
const siteBehavior = buildSiteBehavior(core);
|
|
383
378
|
const payload = JSON.stringify(siteBehavior).replace(/</g, '\\u003c');
|
|
384
379
|
res.writeHead(200, {
|
|
385
380
|
'Content-Type': 'application/javascript; charset=utf-8',
|
|
@@ -388,11 +383,650 @@ export function registerAuthRoutes(deps) {
|
|
|
388
383
|
});
|
|
389
384
|
res.end(`window.__TASKFORCE_SITE_CONFIG__ = ${payload};`);
|
|
390
385
|
});
|
|
386
|
+
// GET /api/taskforce/auth/providers - Public: list enabled OAuth login providers (no secrets)
|
|
387
|
+
addRoute('GET', '/api/taskforce/auth/providers', async (_req, res) => {
|
|
388
|
+
const providers = core.getGlobalSettings().oauthProviders || {};
|
|
389
|
+
const enabled = [];
|
|
390
|
+
if (providers.google?.enabled === true)
|
|
391
|
+
enabled.push('google');
|
|
392
|
+
if (providers.github?.enabled === true)
|
|
393
|
+
enabled.push('github');
|
|
394
|
+
if (providers.apple?.enabled === true)
|
|
395
|
+
enabled.push('apple');
|
|
396
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
397
|
+
res.end(JSON.stringify({ providers: enabled }));
|
|
398
|
+
});
|
|
399
|
+
// === SECTION: OAuth 2.0 / OpenID Connect ===
|
|
400
|
+
const OAUTH_RETURN_TO_PATTERN = /^\/[A-Za-z0-9_\-./?=&%]*$/;
|
|
401
|
+
/**
|
|
402
|
+
* Validates a `return_to` value and defaults to '/' on failure.
|
|
403
|
+
* Prevents open-redirect attacks by only allowing same-site relative paths.
|
|
404
|
+
*/
|
|
405
|
+
const sanitizeReturnTo = (raw) => {
|
|
406
|
+
const value = String(raw || '').trim();
|
|
407
|
+
if (!value || !OAUTH_RETURN_TO_PATTERN.test(value))
|
|
408
|
+
return '/';
|
|
409
|
+
return value;
|
|
410
|
+
};
|
|
411
|
+
/**
|
|
412
|
+
* Provider-agnostic session cookie builder for OAuth-authenticated users.
|
|
413
|
+
* Resolves workspace/role and mints the signed session token.
|
|
414
|
+
*/
|
|
415
|
+
const buildOAuthSessionCookie = (req, authConfig, userId, identityId) => {
|
|
416
|
+
const resolvedWorkspaceId = core.resolvePreferredUserWorkspaceId(userId, {
|
|
417
|
+
preferredWorkspaceId: null,
|
|
418
|
+
persistedWorkspaceId: null
|
|
419
|
+
});
|
|
420
|
+
const fallbackRole = String(authConfig.defaultRole || 'member').trim() || 'member';
|
|
421
|
+
const access = resolvedWorkspaceId
|
|
422
|
+
? core.resolveUserAccess(userId, resolvedWorkspaceId, fallbackRole)
|
|
423
|
+
: null;
|
|
424
|
+
return createSessionTokenCookie(`usr:${userId}`, authConfig, {
|
|
425
|
+
role: (access?.role || fallbackRole),
|
|
426
|
+
workspaceId: access?.workspaceId || '',
|
|
427
|
+
userId,
|
|
428
|
+
identityId,
|
|
429
|
+
...resolveSessionCookieAttributesForRequest(req, authConfig),
|
|
430
|
+
});
|
|
431
|
+
};
|
|
432
|
+
/**
|
|
433
|
+
* Handles the post-exchange OAuth profile: find or create the user,
|
|
434
|
+
* link the provider identity if needed, and return { userId, identityId }.
|
|
435
|
+
*
|
|
436
|
+
* Resolution order:
|
|
437
|
+
* 1. Existing identity matching (provider, providerSubject) → login.
|
|
438
|
+
* 2. Existing user with same email (provider must have verified it) → link + login.
|
|
439
|
+
* 3. Self-registration allowed → register new user → login.
|
|
440
|
+
* 4. Otherwise → reject.
|
|
441
|
+
*/
|
|
442
|
+
const resolveOAuthUser = (profile) => {
|
|
443
|
+
// Case 1: known identity
|
|
444
|
+
const found = core.findUserByOAuthProvider(profile.provider, profile.providerSubject);
|
|
445
|
+
if (found) {
|
|
446
|
+
// Update last_login_at snapshot
|
|
447
|
+
try {
|
|
448
|
+
const db = core.getDatabaseAdapter();
|
|
449
|
+
const tenantId = core.getTenantId();
|
|
450
|
+
db.prepare(`
|
|
451
|
+
UPDATE user_auth_identities
|
|
452
|
+
SET last_login_at = ?, avatar_url_snapshot = COALESCE(?, avatar_url_snapshot),
|
|
453
|
+
updated_at = ?
|
|
454
|
+
WHERE tenant_id = ? AND id = ?
|
|
455
|
+
`).run(new Date().toISOString(), profile.avatarUrl ?? null, new Date().toISOString(), tenantId, found.identityId);
|
|
456
|
+
}
|
|
457
|
+
catch { /* non-fatal */ }
|
|
458
|
+
return { userId: found.userId, identityId: found.identityId, isNew: false };
|
|
459
|
+
}
|
|
460
|
+
// Case 2: email match (provider must have verified it)
|
|
461
|
+
if (profile.email && profile.emailVerified) {
|
|
462
|
+
const byEmail = core.findUserByEmail(profile.email);
|
|
463
|
+
if (byEmail) {
|
|
464
|
+
try {
|
|
465
|
+
const identityId = core.linkOAuthProviderToUser({
|
|
466
|
+
userId: byEmail.userId,
|
|
467
|
+
provider: profile.provider,
|
|
468
|
+
providerSubject: profile.providerSubject,
|
|
469
|
+
providerEmail: profile.email,
|
|
470
|
+
providerEmailVerified: profile.emailVerified,
|
|
471
|
+
displayNameSnapshot: profile.displayName,
|
|
472
|
+
avatarUrlSnapshot: profile.avatarUrl,
|
|
473
|
+
});
|
|
474
|
+
return { userId: byEmail.userId, identityId, isNew: false };
|
|
475
|
+
}
|
|
476
|
+
catch (e) {
|
|
477
|
+
if (e?.code !== 'IDENTITY_ALREADY_EXISTS')
|
|
478
|
+
throw e;
|
|
479
|
+
// Race condition: already linked — re-query
|
|
480
|
+
const refound = core.findUserByOAuthProvider(profile.provider, profile.providerSubject);
|
|
481
|
+
if (refound)
|
|
482
|
+
return { ...refound, isNew: false };
|
|
483
|
+
throw e;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
// Case 3: self-registration
|
|
488
|
+
const authBehavior = buildAuthFlowBehavior(core);
|
|
489
|
+
if (!authBehavior.allowSelfRegistration) {
|
|
490
|
+
throw Object.assign(new Error('Self-registration is currently disabled.'), {
|
|
491
|
+
code: 'REGISTRATION_DISABLED',
|
|
492
|
+
statusCode: 403
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
const created = core.registerOAuthUser({
|
|
496
|
+
email: profile.email,
|
|
497
|
+
emailVerified: profile.emailVerified,
|
|
498
|
+
provider: profile.provider,
|
|
499
|
+
providerSubject: profile.providerSubject,
|
|
500
|
+
displayName: profile.displayName,
|
|
501
|
+
avatarUrl: profile.avatarUrl,
|
|
502
|
+
betaAccess: authBehavior.defaultNewUserBetaAccess,
|
|
503
|
+
});
|
|
504
|
+
return { userId: created.userId, identityId: created.identityId, isNew: true };
|
|
505
|
+
};
|
|
506
|
+
// GET /api/taskforce/auth/oauth/:provider/start
|
|
507
|
+
// Initiates the OAuth authorisation redirect for Google, GitHub, or Apple.
|
|
508
|
+
addRoute('GET', '/api/taskforce/auth/oauth/:provider/start', async (req, res, params) => {
|
|
509
|
+
if (!requireCloudRuntime(res, 'OAuth Login'))
|
|
510
|
+
return;
|
|
511
|
+
const authConfig = context.authConfig;
|
|
512
|
+
if (!authConfig?.enabled) {
|
|
513
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
514
|
+
res.end(JSON.stringify({ success: false, error: 'Auth is not enabled.' }));
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
const provider = String(params.provider || '').toLowerCase();
|
|
518
|
+
if (provider !== 'google' && provider !== 'github' && provider !== 'apple') {
|
|
519
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
520
|
+
res.end(JSON.stringify({ success: false, error: 'Unknown OAuth provider.' }));
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const oauthProviders = core.getGlobalSettings().oauthProviders || {};
|
|
524
|
+
const providerConfig = oauthProviders[provider];
|
|
525
|
+
if (!providerConfig?.enabled) {
|
|
526
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
527
|
+
res.end(JSON.stringify({ success: false, error: `${provider} login is not enabled.` }));
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
const queryString = (req.url || '').split('?')[1] || '';
|
|
531
|
+
const queryParams = new URLSearchParams(queryString);
|
|
532
|
+
const returnTo = sanitizeReturnTo(queryParams.get('return_to'));
|
|
533
|
+
const clientIp = requestHeader(req, 'x-forwarded-for', '') || requestHeader(req, 'x-real-ip', '') || null;
|
|
534
|
+
const rawInviteToken = String(queryParams.get('invite_token') || '').trim();
|
|
535
|
+
const rawFlowHint = String(queryParams.get('flow') || '').trim().toLowerCase();
|
|
536
|
+
// Determine flow mode: link > invite_claim > login
|
|
537
|
+
let flowMode = 'login';
|
|
538
|
+
let validatedInviteToken = null;
|
|
539
|
+
let linkUserId = null;
|
|
540
|
+
if (rawFlowHint === 'link') {
|
|
541
|
+
// Provider-link flow: require an authenticated session
|
|
542
|
+
if (!authConfig || !hasValidSession(req, authConfig)) {
|
|
543
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
544
|
+
res.end(JSON.stringify({ success: false, error: 'Sign in required to link a provider.', code: 'AUTH_REQUIRED' }));
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
const access = resolveRequestAccess(req, authConfig);
|
|
548
|
+
linkUserId = access?.userId || null;
|
|
549
|
+
if (!linkUserId) {
|
|
550
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
551
|
+
res.end(JSON.stringify({ success: false, error: 'Unable to resolve authenticated user.', code: 'AUTH_REQUIRED' }));
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
flowMode = 'link';
|
|
555
|
+
}
|
|
556
|
+
else if (rawInviteToken) {
|
|
557
|
+
// Invite-claim flow
|
|
558
|
+
const inviteDetails = core.inspectInviteAcceptance(rawInviteToken);
|
|
559
|
+
if (inviteDetails.state !== 'valid') {
|
|
560
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
561
|
+
res.end(JSON.stringify({ success: false, error: 'Invite token is invalid or expired.', code: 'INVITE_TOKEN_INVALID' }));
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (inviteDetails.inviteeState !== 'pending_setup') {
|
|
565
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
566
|
+
res.end(JSON.stringify({ success: false, error: 'This invite has already been claimed.', code: 'INVITE_ALREADY_CLAIMED' }));
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
flowMode = 'invite_claim';
|
|
570
|
+
validatedInviteToken = rawInviteToken;
|
|
571
|
+
}
|
|
572
|
+
const { state, codeChallenge, nonce } = core.createOAuthLoginState({
|
|
573
|
+
provider,
|
|
574
|
+
flowMode,
|
|
575
|
+
userId: linkUserId,
|
|
576
|
+
returnTo,
|
|
577
|
+
inviteToken: validatedInviteToken,
|
|
578
|
+
createdIp: clientIp,
|
|
579
|
+
});
|
|
580
|
+
const baseUrl = resolvePublicAuthBaseUrl(req);
|
|
581
|
+
const redirectUri = `${baseUrl}/api/taskforce/auth/oauth/${provider}/callback`;
|
|
582
|
+
let authUrl;
|
|
583
|
+
if (provider === 'google') {
|
|
584
|
+
const cfg = providerConfig;
|
|
585
|
+
if (!cfg.clientId) {
|
|
586
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
587
|
+
res.end(JSON.stringify({ success: false, error: 'Google OAuth is not fully configured.' }));
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
authUrl = buildGoogleAuthUrl({
|
|
591
|
+
clientId: cfg.clientId,
|
|
592
|
+
redirectUri,
|
|
593
|
+
state,
|
|
594
|
+
codeChallenge,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
else if (provider === 'github') {
|
|
598
|
+
const cfg = providerConfig;
|
|
599
|
+
if (!cfg.clientId) {
|
|
600
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
601
|
+
res.end(JSON.stringify({ success: false, error: 'GitHub OAuth is not fully configured.' }));
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
authUrl = buildGithubAuthUrl({
|
|
605
|
+
clientId: cfg.clientId,
|
|
606
|
+
redirectUri,
|
|
607
|
+
state,
|
|
608
|
+
codeChallenge,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
// Apple
|
|
613
|
+
const cfg = providerConfig;
|
|
614
|
+
if (!cfg.servicesId) {
|
|
615
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
616
|
+
res.end(JSON.stringify({ success: false, error: 'Apple Sign In is not fully configured.' }));
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
authUrl = buildAppleAuthUrl({
|
|
620
|
+
servicesId: cfg.servicesId,
|
|
621
|
+
redirectUri,
|
|
622
|
+
state,
|
|
623
|
+
nonce,
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
res.writeHead(302, { Location: authUrl });
|
|
627
|
+
res.end();
|
|
628
|
+
});
|
|
629
|
+
// GET /api/taskforce/auth/oauth/:provider/callback
|
|
630
|
+
// Handles the OAuth callback for Google and GitHub (GET redirect with `code` + `state`).
|
|
631
|
+
addRoute('GET', '/api/taskforce/auth/oauth/:provider/callback', async (req, res, params) => {
|
|
632
|
+
if (!requireCloudRuntime(res, 'OAuth Login'))
|
|
633
|
+
return;
|
|
634
|
+
const authConfig = context.authConfig;
|
|
635
|
+
if (!authConfig?.enabled) {
|
|
636
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
637
|
+
res.end(JSON.stringify({ success: false, error: 'Auth is not enabled.' }));
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
const provider = String(params.provider || '').toLowerCase();
|
|
641
|
+
if (provider !== 'google' && provider !== 'github') {
|
|
642
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
643
|
+
res.end(JSON.stringify({ success: false, error: 'Unknown OAuth provider.' }));
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
const queryString = (req.url || '').split('?')[1] || '';
|
|
647
|
+
const queryParams = new URLSearchParams(queryString);
|
|
648
|
+
const stateParam = String(queryParams.get('state') || '').trim();
|
|
649
|
+
const codeParam = String(queryParams.get('code') || '').trim();
|
|
650
|
+
const errorParam = String(queryParams.get('error') || '').trim();
|
|
651
|
+
const baseUrl = resolvePublicAuthBaseUrl(req);
|
|
652
|
+
const redirectUri = `${baseUrl}/api/taskforce/auth/oauth/${provider}/callback`;
|
|
653
|
+
if (errorParam) {
|
|
654
|
+
auditAuthAction(req, 'auth-oauth-callback-denied', { provider, error: errorParam });
|
|
655
|
+
res.writeHead(302, { Location: `/login?error=oauth_denied&provider=${encodeURIComponent(provider)}` });
|
|
656
|
+
res.end();
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
if (!stateParam || !codeParam) {
|
|
660
|
+
res.writeHead(302, { Location: `/login?error=oauth_invalid&provider=${encodeURIComponent(provider)}` });
|
|
661
|
+
res.end();
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
// Consume state (single-use, validates expiry)
|
|
665
|
+
const stateRecord = core.consumeOAuthLoginState(stateParam);
|
|
666
|
+
if (!stateRecord) {
|
|
667
|
+
auditAuthAction(req, 'auth-oauth-state-invalid', { provider, stateProvided: true });
|
|
668
|
+
res.writeHead(302, { Location: `/login?error=oauth_state_invalid&provider=${encodeURIComponent(provider)}` });
|
|
669
|
+
res.end();
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
if (stateRecord.provider !== provider) {
|
|
673
|
+
auditAuthAction(req, 'auth-oauth-state-provider-mismatch', { provider, stateProvider: stateRecord.provider });
|
|
674
|
+
res.writeHead(302, { Location: `/login?error=oauth_state_invalid&provider=${encodeURIComponent(provider)}` });
|
|
675
|
+
res.end();
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
const codeVerifier = stateRecord.pkceVerifier || '';
|
|
679
|
+
const returnTo = sanitizeReturnTo(stateRecord.returnTo);
|
|
680
|
+
const oauthProviders = core.getGlobalSettings().oauthProviders || {};
|
|
681
|
+
try {
|
|
682
|
+
let profile;
|
|
683
|
+
if (provider === 'google') {
|
|
684
|
+
const cfg = (oauthProviders.google || {});
|
|
685
|
+
if (!cfg.clientId || !cfg.clientSecret) {
|
|
686
|
+
throw Object.assign(new Error('Google OAuth is not configured.'), { statusCode: 500 });
|
|
687
|
+
}
|
|
688
|
+
profile = await exchangeGoogleCode({
|
|
689
|
+
code: codeParam,
|
|
690
|
+
codeVerifier,
|
|
691
|
+
redirectUri,
|
|
692
|
+
clientId: cfg.clientId,
|
|
693
|
+
clientSecret: cfg.clientSecret,
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
const cfg = (oauthProviders.github || {});
|
|
698
|
+
if (!cfg.clientId || !cfg.clientSecret) {
|
|
699
|
+
throw Object.assign(new Error('GitHub OAuth is not configured.'), { statusCode: 500 });
|
|
700
|
+
}
|
|
701
|
+
profile = await exchangeGithubCode({
|
|
702
|
+
code: codeParam,
|
|
703
|
+
codeVerifier,
|
|
704
|
+
redirectUri,
|
|
705
|
+
clientId: cfg.clientId,
|
|
706
|
+
clientSecret: cfg.clientSecret,
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
let userId;
|
|
710
|
+
let identityId;
|
|
711
|
+
let isNew = false;
|
|
712
|
+
if (stateRecord.flowMode === 'link') {
|
|
713
|
+
if (!stateRecord.userId) {
|
|
714
|
+
throw Object.assign(new Error('User ID missing from OAuth link state.'), { code: 'AUTH_REQUIRED', statusCode: 401 });
|
|
715
|
+
}
|
|
716
|
+
identityId = core.linkOAuthProviderToUser({
|
|
717
|
+
userId: stateRecord.userId,
|
|
718
|
+
provider: profile.provider,
|
|
719
|
+
providerSubject: profile.providerSubject,
|
|
720
|
+
providerEmail: profile.email,
|
|
721
|
+
providerEmailVerified: profile.emailVerified,
|
|
722
|
+
displayNameSnapshot: profile.displayName,
|
|
723
|
+
avatarUrlSnapshot: profile.avatarUrl,
|
|
724
|
+
});
|
|
725
|
+
userId = stateRecord.userId;
|
|
726
|
+
isNew = false;
|
|
727
|
+
}
|
|
728
|
+
else if (stateRecord.flowMode === 'invite_claim') {
|
|
729
|
+
if (!stateRecord.inviteToken) {
|
|
730
|
+
throw Object.assign(new Error('Invite token missing from OAuth state.'), { code: 'INVITE_TOKEN_MISSING', statusCode: 400 });
|
|
731
|
+
}
|
|
732
|
+
const claimed = core.claimInviteWithOAuthProvider({
|
|
733
|
+
token: stateRecord.inviteToken,
|
|
734
|
+
provider: profile.provider,
|
|
735
|
+
providerSubject: profile.providerSubject,
|
|
736
|
+
providerEmail: profile.email,
|
|
737
|
+
providerEmailVerified: profile.emailVerified,
|
|
738
|
+
displayNameSnapshot: profile.displayName,
|
|
739
|
+
avatarUrlSnapshot: profile.avatarUrl,
|
|
740
|
+
});
|
|
741
|
+
userId = claimed.userId;
|
|
742
|
+
identityId = claimed.identityId;
|
|
743
|
+
isNew = true;
|
|
744
|
+
}
|
|
745
|
+
else {
|
|
746
|
+
({ userId, identityId, isNew } = resolveOAuthUser(profile));
|
|
747
|
+
}
|
|
748
|
+
const cookie = buildOAuthSessionCookie(req, authConfig, userId, identityId);
|
|
749
|
+
res.setHeader('Set-Cookie', cookie);
|
|
750
|
+
auditAuthAction(req, 'auth-oauth-login-success', {
|
|
751
|
+
provider,
|
|
752
|
+
userId,
|
|
753
|
+
isNew,
|
|
754
|
+
flowMode: stateRecord.flowMode,
|
|
755
|
+
email: profile.email || null,
|
|
756
|
+
});
|
|
757
|
+
res.writeHead(302, { Location: returnTo });
|
|
758
|
+
res.end();
|
|
759
|
+
}
|
|
760
|
+
catch (err) {
|
|
761
|
+
const code = err?.code || 'OAUTH_ERROR';
|
|
762
|
+
auditAuthAction(req, 'auth-oauth-callback-error', { provider, code, message: String(err?.message || '') });
|
|
763
|
+
if (code === 'REGISTRATION_DISABLED') {
|
|
764
|
+
res.writeHead(302, { Location: `/login?error=registration_disabled&provider=${encodeURIComponent(provider)}` });
|
|
765
|
+
}
|
|
766
|
+
else if (code === 'IDENTITY_ALREADY_EXISTS') {
|
|
767
|
+
res.writeHead(302, { Location: `${returnTo}?error=provider_already_linked&provider=${encodeURIComponent(provider)}` });
|
|
768
|
+
}
|
|
769
|
+
else if (code === 'INVITE_TOKEN_EXPIRED' || code === 'INVITE_TOKEN_REVOKED' || code === 'INVITE_TOKEN_USED' || code === 'INVITE_TOKEN_INVALID') {
|
|
770
|
+
res.writeHead(302, { Location: `/login?error=invite_invalid&provider=${encodeURIComponent(provider)}` });
|
|
771
|
+
}
|
|
772
|
+
else {
|
|
773
|
+
res.writeHead(302, { Location: `/login?error=oauth_failed&provider=${encodeURIComponent(provider)}` });
|
|
774
|
+
}
|
|
775
|
+
res.end();
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
// POST /api/taskforce/auth/oauth/apple/callback
|
|
779
|
+
// Apple Sign In sends `code`, `state`, `id_token` and optional `user` JSON via form_post.
|
|
780
|
+
addRoute('POST', '/api/taskforce/auth/oauth/apple/callback', async (req, res) => {
|
|
781
|
+
if (!requireCloudRuntime(res, 'Apple Sign In'))
|
|
782
|
+
return;
|
|
783
|
+
const authConfig = context.authConfig;
|
|
784
|
+
if (!authConfig?.enabled) {
|
|
785
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
786
|
+
res.end(JSON.stringify({ success: false, error: 'Auth is not enabled.' }));
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
let formData;
|
|
790
|
+
try {
|
|
791
|
+
formData = await parseFormBody(req);
|
|
792
|
+
}
|
|
793
|
+
catch {
|
|
794
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
795
|
+
res.end(JSON.stringify({ success: false, error: 'Invalid form body.' }));
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
const stateParam = String(formData.state || '').trim();
|
|
799
|
+
const codeParam = String(formData.code || '').trim();
|
|
800
|
+
const errorParam = String(formData.error || '').trim();
|
|
801
|
+
const userJson = formData.user ? String(formData.user).trim() : null;
|
|
802
|
+
if (errorParam) {
|
|
803
|
+
auditAuthAction(req, 'auth-oauth-callback-denied', { provider: 'apple', error: errorParam });
|
|
804
|
+
res.writeHead(302, { Location: '/login?error=oauth_denied&provider=apple' });
|
|
805
|
+
res.end();
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
if (!stateParam || !codeParam) {
|
|
809
|
+
res.writeHead(302, { Location: '/login?error=oauth_invalid&provider=apple' });
|
|
810
|
+
res.end();
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
const stateRecord = core.consumeOAuthLoginState(stateParam);
|
|
814
|
+
if (!stateRecord || stateRecord.provider !== 'apple') {
|
|
815
|
+
auditAuthAction(req, 'auth-oauth-state-invalid', { provider: 'apple', stateProvided: true });
|
|
816
|
+
res.writeHead(302, { Location: '/login?error=oauth_state_invalid&provider=apple' });
|
|
817
|
+
res.end();
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
const returnTo = sanitizeReturnTo(stateRecord.returnTo);
|
|
821
|
+
const oauthProviders = core.getGlobalSettings().oauthProviders || {};
|
|
822
|
+
const appleCfg = (oauthProviders.apple || {});
|
|
823
|
+
if (!appleCfg.servicesId || !appleCfg.teamId || !appleCfg.keyId || !appleCfg.privateKeyPem) {
|
|
824
|
+
auditAuthAction(req, 'auth-oauth-callback-error', { provider: 'apple', code: 'APPLE_NOT_CONFIGURED' });
|
|
825
|
+
res.writeHead(302, { Location: '/login?error=oauth_failed&provider=apple' });
|
|
826
|
+
res.end();
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
const baseUrl = resolvePublicAuthBaseUrl(req);
|
|
830
|
+
const redirectUri = `${baseUrl}/api/taskforce/auth/oauth/apple/callback`;
|
|
831
|
+
try {
|
|
832
|
+
const profile = await exchangeAppleCode({
|
|
833
|
+
code: codeParam,
|
|
834
|
+
redirectUri,
|
|
835
|
+
servicesId: appleCfg.servicesId,
|
|
836
|
+
teamId: appleCfg.teamId,
|
|
837
|
+
keyId: appleCfg.keyId,
|
|
838
|
+
privateKeyPem: appleCfg.privateKeyPem,
|
|
839
|
+
userJson,
|
|
840
|
+
});
|
|
841
|
+
let userId;
|
|
842
|
+
let identityId;
|
|
843
|
+
let isNew = false;
|
|
844
|
+
if (stateRecord.flowMode === 'link') {
|
|
845
|
+
if (!stateRecord.userId) {
|
|
846
|
+
throw Object.assign(new Error('User ID missing from OAuth link state.'), { code: 'AUTH_REQUIRED', statusCode: 401 });
|
|
847
|
+
}
|
|
848
|
+
identityId = core.linkOAuthProviderToUser({
|
|
849
|
+
userId: stateRecord.userId,
|
|
850
|
+
provider: 'apple',
|
|
851
|
+
providerSubject: profile.providerSubject,
|
|
852
|
+
providerEmail: profile.email,
|
|
853
|
+
providerEmailVerified: profile.emailVerified,
|
|
854
|
+
displayNameSnapshot: profile.displayName,
|
|
855
|
+
avatarUrlSnapshot: profile.avatarUrl,
|
|
856
|
+
});
|
|
857
|
+
userId = stateRecord.userId;
|
|
858
|
+
isNew = false;
|
|
859
|
+
}
|
|
860
|
+
else if (stateRecord.flowMode === 'invite_claim') {
|
|
861
|
+
if (!stateRecord.inviteToken) {
|
|
862
|
+
throw Object.assign(new Error('Invite token missing from OAuth state.'), { code: 'INVITE_TOKEN_MISSING', statusCode: 400 });
|
|
863
|
+
}
|
|
864
|
+
const claimed = core.claimInviteWithOAuthProvider({
|
|
865
|
+
token: stateRecord.inviteToken,
|
|
866
|
+
provider: 'apple',
|
|
867
|
+
providerSubject: profile.providerSubject,
|
|
868
|
+
providerEmail: profile.email,
|
|
869
|
+
providerEmailVerified: profile.emailVerified,
|
|
870
|
+
displayNameSnapshot: profile.displayName,
|
|
871
|
+
avatarUrlSnapshot: profile.avatarUrl,
|
|
872
|
+
});
|
|
873
|
+
userId = claimed.userId;
|
|
874
|
+
identityId = claimed.identityId;
|
|
875
|
+
isNew = true;
|
|
876
|
+
}
|
|
877
|
+
else {
|
|
878
|
+
({ userId, identityId, isNew } = resolveOAuthUser(profile));
|
|
879
|
+
}
|
|
880
|
+
const cookie = buildOAuthSessionCookie(req, authConfig, userId, identityId);
|
|
881
|
+
res.setHeader('Set-Cookie', cookie);
|
|
882
|
+
auditAuthAction(req, 'auth-oauth-login-success', {
|
|
883
|
+
provider: 'apple',
|
|
884
|
+
userId,
|
|
885
|
+
isNew,
|
|
886
|
+
flowMode: stateRecord.flowMode,
|
|
887
|
+
email: profile.email || null,
|
|
888
|
+
});
|
|
889
|
+
res.writeHead(302, { Location: returnTo });
|
|
890
|
+
res.end();
|
|
891
|
+
}
|
|
892
|
+
catch (err) {
|
|
893
|
+
const code = err?.code || 'OAUTH_ERROR';
|
|
894
|
+
auditAuthAction(req, 'auth-oauth-callback-error', { provider: 'apple', code, message: String(err?.message || '') });
|
|
895
|
+
if (code === 'REGISTRATION_DISABLED') {
|
|
896
|
+
res.writeHead(302, { Location: '/login?error=registration_disabled&provider=apple' });
|
|
897
|
+
}
|
|
898
|
+
else if (code === 'IDENTITY_ALREADY_EXISTS') {
|
|
899
|
+
res.writeHead(302, { Location: `${returnTo}?error=provider_already_linked&provider=apple` });
|
|
900
|
+
}
|
|
901
|
+
else if (code === 'INVITE_TOKEN_EXPIRED' || code === 'INVITE_TOKEN_REVOKED' || code === 'INVITE_TOKEN_USED' || code === 'INVITE_TOKEN_INVALID') {
|
|
902
|
+
res.writeHead(302, { Location: '/login?error=invite_invalid&provider=apple' });
|
|
903
|
+
}
|
|
904
|
+
else {
|
|
905
|
+
res.writeHead(302, { Location: '/login?error=oauth_failed&provider=apple' });
|
|
906
|
+
}
|
|
907
|
+
res.end();
|
|
908
|
+
}
|
|
909
|
+
});
|
|
910
|
+
// GET /api/taskforce/account/login-methods
|
|
911
|
+
// Returns the list of active login identities for the authenticated user.
|
|
912
|
+
addRoute('GET', '/api/taskforce/account/login-methods', async (req, res) => {
|
|
913
|
+
if (!requireCloudRuntime(res, 'Login methods'))
|
|
914
|
+
return;
|
|
915
|
+
const authConfig = context.authConfig;
|
|
916
|
+
if (!authConfig?.enabled || !hasValidSession(req, authConfig)) {
|
|
917
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
918
|
+
res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
const access = resolveRequestAccess(req, authConfig);
|
|
922
|
+
const userId = access?.userId || '';
|
|
923
|
+
if (!userId) {
|
|
924
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
925
|
+
res.end(JSON.stringify({ success: false, error: 'Unable to resolve user.' }));
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
const identities = core.listLoginMethodsForUser(userId);
|
|
929
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
930
|
+
res.end(JSON.stringify({
|
|
931
|
+
success: true,
|
|
932
|
+
methods: identities.map(id => ({
|
|
933
|
+
provider: id.provider,
|
|
934
|
+
providerEmail: id.providerEmail || null,
|
|
935
|
+
displayNameSnapshot: id.displayNameSnapshot || null,
|
|
936
|
+
avatarUrlSnapshot: id.avatarUrlSnapshot || null,
|
|
937
|
+
linkedAt: id.linkedAt || id.createdAt,
|
|
938
|
+
lastLoginAt: id.lastLoginAt || null,
|
|
939
|
+
}))
|
|
940
|
+
}));
|
|
941
|
+
});
|
|
942
|
+
// DELETE /api/taskforce/account/login-methods/:provider
|
|
943
|
+
// Unlinks a provider identity from the authenticated user.
|
|
944
|
+
// Rejects with 409 if it would be the last active login method.
|
|
945
|
+
addRoute('DELETE', '/api/taskforce/account/login-methods/:provider', async (req, res, params) => {
|
|
946
|
+
if (!requireCloudRuntime(res, 'Login methods'))
|
|
947
|
+
return;
|
|
948
|
+
const authConfig = context.authConfig;
|
|
949
|
+
if (!authConfig?.enabled || !hasValidSession(req, authConfig)) {
|
|
950
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
951
|
+
res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
const access = resolveRequestAccess(req, authConfig);
|
|
955
|
+
const userId = access?.userId || '';
|
|
956
|
+
if (!userId) {
|
|
957
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
958
|
+
res.end(JSON.stringify({ success: false, error: 'Unable to resolve user.' }));
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
const provider = String(params.provider || '').toLowerCase();
|
|
962
|
+
if (!provider) {
|
|
963
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
964
|
+
res.end(JSON.stringify({ success: false, error: 'Provider is required.' }));
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
try {
|
|
968
|
+
core.unlinkLoginMethod(userId, provider);
|
|
969
|
+
auditAuthAction(req, 'account-login-method-unlinked', { userId, provider });
|
|
970
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
971
|
+
res.end(JSON.stringify({ success: true }));
|
|
972
|
+
}
|
|
973
|
+
catch (err) {
|
|
974
|
+
const code = err?.code || 'UNLINK_ERROR';
|
|
975
|
+
const status = code === 'LAST_IDENTITY_REQUIRED' ? 409 : resolveErrorStatusCode(err);
|
|
976
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
977
|
+
res.end(JSON.stringify({ success: false, error: err?.message || 'Unlink failed.', code }));
|
|
978
|
+
}
|
|
979
|
+
});
|
|
980
|
+
// POST /api/taskforce/account/login-methods/password
|
|
981
|
+
// Adds (or replaces) a password identity for the authenticated user.
|
|
982
|
+
// Social-only accounts use this to add a password login method.
|
|
983
|
+
addRoute('POST', '/api/taskforce/account/login-methods/password', async (req, res) => {
|
|
984
|
+
if (!requireCloudRuntime(res, 'Login methods'))
|
|
985
|
+
return;
|
|
986
|
+
const authConfig = context.authConfig;
|
|
987
|
+
if (!authConfig?.enabled || !hasValidSession(req, authConfig)) {
|
|
988
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
989
|
+
res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
const access = resolveRequestAccess(req, authConfig);
|
|
993
|
+
const userId = access?.userId || '';
|
|
994
|
+
if (!userId) {
|
|
995
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
996
|
+
res.end(JSON.stringify({ success: false, error: 'Unable to resolve user.' }));
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
let body;
|
|
1000
|
+
try {
|
|
1001
|
+
body = await parseJsonBody(req);
|
|
1002
|
+
}
|
|
1003
|
+
catch {
|
|
1004
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1005
|
+
res.end(JSON.stringify({ success: false, error: 'Invalid JSON body.' }));
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
const password = String(body?.password || '');
|
|
1009
|
+
try {
|
|
1010
|
+
core.addPasswordToAccount(userId, password);
|
|
1011
|
+
auditAuthAction(req, 'account-password-added', { userId });
|
|
1012
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1013
|
+
res.end(JSON.stringify({ success: true }));
|
|
1014
|
+
}
|
|
1015
|
+
catch (err) {
|
|
1016
|
+
const code = err?.code || 'ADD_PASSWORD_ERROR';
|
|
1017
|
+
const status = resolveErrorStatusCode(err);
|
|
1018
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
1019
|
+
res.end(JSON.stringify({ success: false, error: err?.message || 'Failed to add password.', code }));
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
391
1022
|
// GET /api/taskforce/auth/session - Return current auth/session state
|
|
392
1023
|
addRoute('GET', '/api/taskforce/auth/session', async (req, res) => {
|
|
1024
|
+
const routeStartedAt = getRouteTimingNow();
|
|
1025
|
+
const traceId = requestBootstrapTraceId(req);
|
|
393
1026
|
const authConfig = context.authConfig;
|
|
394
1027
|
const access = authConfig ? resolveRequestAccess(req, authConfig) : null;
|
|
395
1028
|
const authenticated = Boolean(authConfig && hasValidSession(req, authConfig));
|
|
1029
|
+
const authResolvedAt = getRouteTimingNow();
|
|
396
1030
|
let role = access?.role || authConfig?.defaultRole || 'member';
|
|
397
1031
|
let workspaceId = String(access?.workspaceId || authConfig?.defaultWorkspaceId || '').trim();
|
|
398
1032
|
let userId = access?.userId || 'anonymous';
|
|
@@ -400,11 +1034,16 @@ export function registerAuthRoutes(deps) {
|
|
|
400
1034
|
let displayName = null;
|
|
401
1035
|
let avatarUrl = null;
|
|
402
1036
|
let betaAccess = true;
|
|
1037
|
+
let betaAccessResolvedAt = authResolvedAt;
|
|
1038
|
+
let userAccessResolvedAt = authResolvedAt;
|
|
1039
|
+
let identityResolvedAt = authResolvedAt;
|
|
403
1040
|
if (authenticated && access?.userId) {
|
|
404
1041
|
// Keep the session endpoint fast on reload; avatar draft cleanup is best-effort housekeeping.
|
|
405
1042
|
void cleanupStaleAvatarDrafts(access.userId).catch(() => { });
|
|
406
1043
|
betaAccess = core.getUserBetaAccess(access.userId);
|
|
1044
|
+
betaAccessResolvedAt = getRouteTimingNow();
|
|
407
1045
|
const resolved = core.resolveUserAccess(access.userId, workspaceId, role);
|
|
1046
|
+
userAccessResolvedAt = getRouteTimingNow();
|
|
408
1047
|
if (resolved) {
|
|
409
1048
|
role = resolved.role;
|
|
410
1049
|
workspaceId = resolved.workspaceId;
|
|
@@ -419,9 +1058,9 @@ export function registerAuthRoutes(deps) {
|
|
|
419
1058
|
email = identity?.email || null;
|
|
420
1059
|
displayName = identity?.displayName || null;
|
|
421
1060
|
avatarUrl = identity?.avatarUrl || null;
|
|
1061
|
+
identityResolvedAt = getRouteTimingNow();
|
|
422
1062
|
}
|
|
423
|
-
|
|
424
|
-
res.end(JSON.stringify({
|
|
1063
|
+
const payload = {
|
|
425
1064
|
authenticated,
|
|
426
1065
|
userId,
|
|
427
1066
|
workspaceId,
|
|
@@ -430,7 +1069,34 @@ export function registerAuthRoutes(deps) {
|
|
|
430
1069
|
displayName,
|
|
431
1070
|
avatarUrl,
|
|
432
1071
|
betaAccess
|
|
433
|
-
}
|
|
1072
|
+
};
|
|
1073
|
+
const serialized = serializeJsonWithTiming(payload, identityResolvedAt);
|
|
1074
|
+
const totalDurationMs = getRouteTimingNow() - routeStartedAt;
|
|
1075
|
+
setServerTimingHeader(res, [
|
|
1076
|
+
{ name: 'auth-session', durationMs: totalDurationMs },
|
|
1077
|
+
{ name: 'auth-session-cookie', durationMs: authResolvedAt - routeStartedAt },
|
|
1078
|
+
{ name: 'auth-session-beta-access', durationMs: betaAccessResolvedAt - authResolvedAt },
|
|
1079
|
+
{ name: 'auth-session-user-access', durationMs: userAccessResolvedAt - betaAccessResolvedAt },
|
|
1080
|
+
{ name: 'auth-session-identity', durationMs: identityResolvedAt - userAccessResolvedAt },
|
|
1081
|
+
{ name: 'auth-session-json', durationMs: serialized.durationMs },
|
|
1082
|
+
]);
|
|
1083
|
+
logSlowBootstrapRoute({
|
|
1084
|
+
route: '/api/taskforce/auth/session',
|
|
1085
|
+
workspaceId: workspaceId || access?.workspaceId || authConfig?.defaultWorkspaceId || 'default',
|
|
1086
|
+
traceId,
|
|
1087
|
+
durationMs: totalDurationMs,
|
|
1088
|
+
details: {
|
|
1089
|
+
authenticated,
|
|
1090
|
+
payloadBytes: serialized.byteLength,
|
|
1091
|
+
cookieMs: authResolvedAt - routeStartedAt,
|
|
1092
|
+
betaAccessMs: betaAccessResolvedAt - authResolvedAt,
|
|
1093
|
+
userAccessMs: userAccessResolvedAt - betaAccessResolvedAt,
|
|
1094
|
+
identityMs: identityResolvedAt - userAccessResolvedAt,
|
|
1095
|
+
serializeMs: serialized.durationMs,
|
|
1096
|
+
}
|
|
1097
|
+
});
|
|
1098
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1099
|
+
res.end(serialized.body);
|
|
434
1100
|
});
|
|
435
1101
|
// GET /api/taskforce/auth/admin-session - Return global admin auth/session state
|
|
436
1102
|
addRoute('GET', '/api/taskforce/auth/admin-session', async (req, res) => {
|
|
@@ -586,6 +1252,63 @@ export function registerAuthRoutes(deps) {
|
|
|
586
1252
|
path: `/api/taskforce/context/${encodeURIComponent(relativePath)}`
|
|
587
1253
|
}));
|
|
588
1254
|
});
|
|
1255
|
+
addRoute('POST', '/api/taskforce/auth/profile/avatar/upload', async (req, res) => {
|
|
1256
|
+
if (!requireCloudRuntime(res, 'Authentication'))
|
|
1257
|
+
return;
|
|
1258
|
+
const authConfig = context.authConfig;
|
|
1259
|
+
if (!authConfig) {
|
|
1260
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1261
|
+
res.end(JSON.stringify({ success: false, error: 'Auth context unavailable.' }));
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
const access = resolveRequestAccess(req, authConfig);
|
|
1265
|
+
const userId = String(access.userId || '').trim();
|
|
1266
|
+
if (!access.authenticated || !userId || userId === 'anonymous') {
|
|
1267
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
1268
|
+
res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
const draftId = String(requestHeader(req, 'x-taskforce-avatar-draft-id', '') || '').trim();
|
|
1272
|
+
if (!draftId) {
|
|
1273
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1274
|
+
res.end(JSON.stringify({ success: false, error: 'Avatar draft id is required.', code: 'AVATAR_DRAFT_REQUIRED' }));
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
await cleanupStaleAvatarDrafts(userId);
|
|
1278
|
+
const draft = core.getUserAvatarUploadDraft(userId, draftId);
|
|
1279
|
+
if (!draft || draft.discardedAt) {
|
|
1280
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1281
|
+
res.end(JSON.stringify({ success: false, error: 'Avatar draft is invalid or expired.', code: 'AVATAR_DRAFT_INVALID' }));
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
const objectStorageClient = getObjectStorageClient();
|
|
1285
|
+
const effectiveObjectStorage = resolveEffectiveObjectStorageConfig();
|
|
1286
|
+
if (!objectStorageClient || effectiveObjectStorage.provider !== 'r2') {
|
|
1287
|
+
res.writeHead(501, { 'Content-Type': 'application/json' });
|
|
1288
|
+
res.end(JSON.stringify({ success: false, error: 'Avatar uploads require object storage.' }));
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
try {
|
|
1292
|
+
const body = await parseBinaryBody(req, MAX_AVATAR_UPLOAD_BYTES);
|
|
1293
|
+
if (body.length === 0) {
|
|
1294
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1295
|
+
res.end(JSON.stringify({ success: false, error: 'Avatar upload body is empty.', code: 'AVATAR_UPLOAD_EMPTY' }));
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
await objectStorageClient.putObject(draft.storageKey, body, draft.mimeType || 'application/octet-stream');
|
|
1299
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1300
|
+
res.end(JSON.stringify({ success: true, draftId, relativePath: draft.storageKey }));
|
|
1301
|
+
}
|
|
1302
|
+
catch (error) {
|
|
1303
|
+
const statusCode = resolveErrorStatusCode(error, 400);
|
|
1304
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
1305
|
+
res.end(JSON.stringify({
|
|
1306
|
+
success: false,
|
|
1307
|
+
error: String(error?.message || 'Failed to upload avatar.'),
|
|
1308
|
+
code: error?.code
|
|
1309
|
+
}));
|
|
1310
|
+
}
|
|
1311
|
+
});
|
|
589
1312
|
addRoute('POST', '/api/taskforce/auth/profile/avatar/finalize', async (req, res) => {
|
|
590
1313
|
if (!requireCloudRuntime(res, 'Authentication'))
|
|
591
1314
|
return;
|
|
@@ -705,7 +1428,7 @@ export function registerAuthRoutes(deps) {
|
|
|
705
1428
|
const workspaceId = workspaceIdCandidate || 'default';
|
|
706
1429
|
// Password flow (preferred)
|
|
707
1430
|
if (email && password) {
|
|
708
|
-
const authBehavior =
|
|
1431
|
+
const authBehavior = buildAuthFlowBehavior(core);
|
|
709
1432
|
let authResult = core.authenticatePasswordUserDetailed({
|
|
710
1433
|
email,
|
|
711
1434
|
password,
|
|
@@ -757,10 +1480,11 @@ export function registerAuthRoutes(deps) {
|
|
|
757
1480
|
};
|
|
758
1481
|
}
|
|
759
1482
|
}
|
|
760
|
-
const cookie = createSessionTokenCookie(`
|
|
1483
|
+
const cookie = createSessionTokenCookie(`usr:${access.userId}`, authConfig, {
|
|
761
1484
|
role: access.role,
|
|
762
1485
|
workspaceId: access.workspaceId,
|
|
763
1486
|
userId: access.userId,
|
|
1487
|
+
identityId: authResult.access?.identityId,
|
|
764
1488
|
...resolveSessionCookieAttributesForRequest(req, authConfig)
|
|
765
1489
|
});
|
|
766
1490
|
auditAuthAction(req, 'auth-login-success', {
|
|
@@ -802,7 +1526,7 @@ export function registerAuthRoutes(deps) {
|
|
|
802
1526
|
const selectedPlanVersionId = typeof body?.planVersionId === 'string'
|
|
803
1527
|
? body.planVersionId.trim()
|
|
804
1528
|
: '';
|
|
805
|
-
const authBehavior =
|
|
1529
|
+
const authBehavior = buildAuthFlowBehavior(core);
|
|
806
1530
|
if (!authBehavior.allowSelfRegistration) {
|
|
807
1531
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
808
1532
|
res.end(JSON.stringify({ success: false, error: 'Self-registration is currently disabled.' }));
|
|
@@ -875,12 +1599,14 @@ export function registerAuthRoutes(deps) {
|
|
|
875
1599
|
&& authBehavior.requireVerifiedEmail
|
|
876
1600
|
&& email
|
|
877
1601
|
&& !core.isEmailVerified(email)) {
|
|
878
|
-
let emailSent =
|
|
1602
|
+
let emailSent = false;
|
|
1603
|
+
let deliveryAttempted = false;
|
|
879
1604
|
let emailError;
|
|
880
1605
|
let verificationToken;
|
|
881
1606
|
const verification = core.requestEmailVerification({ email });
|
|
882
1607
|
verificationToken = verification.token || null;
|
|
883
1608
|
if (verification.token) {
|
|
1609
|
+
deliveryAttempted = true;
|
|
884
1610
|
const delivery = await deliverAuthEmail('verify-email', email, verification.token, req);
|
|
885
1611
|
emailSent = delivery.ok;
|
|
886
1612
|
emailError = delivery.error;
|
|
@@ -898,6 +1624,7 @@ export function registerAuthRoutes(deps) {
|
|
|
898
1624
|
code: 'EMAIL_ALREADY_EXISTS_UNVERIFIED',
|
|
899
1625
|
verificationRequired: true,
|
|
900
1626
|
emailSent,
|
|
1627
|
+
deliveryAttempted,
|
|
901
1628
|
emailError,
|
|
902
1629
|
verificationToken: shouldExposeDevAuthTokens()
|
|
903
1630
|
? (verificationToken || null)
|
|
@@ -930,10 +1657,12 @@ export function registerAuthRoutes(deps) {
|
|
|
930
1657
|
return;
|
|
931
1658
|
}
|
|
932
1659
|
try {
|
|
933
|
-
let emailSent =
|
|
1660
|
+
let emailSent = false;
|
|
1661
|
+
let deliveryAttempted = false;
|
|
934
1662
|
let emailError;
|
|
935
1663
|
const verification = core.requestEmailVerification({ email });
|
|
936
1664
|
if (verification.token) {
|
|
1665
|
+
deliveryAttempted = true;
|
|
937
1666
|
const delivery = await deliverAuthEmail('verify-email', email, verification.token, req);
|
|
938
1667
|
emailSent = delivery.ok;
|
|
939
1668
|
emailError = delivery.error;
|
|
@@ -946,6 +1675,7 @@ export function registerAuthRoutes(deps) {
|
|
|
946
1675
|
res.end(JSON.stringify({
|
|
947
1676
|
success: true,
|
|
948
1677
|
emailSent,
|
|
1678
|
+
deliveryAttempted,
|
|
949
1679
|
emailError,
|
|
950
1680
|
verificationToken: shouldExposeDevAuthTokens() ? (verification.token || null) : undefined
|
|
951
1681
|
}));
|
|
@@ -1018,10 +1748,12 @@ export function registerAuthRoutes(deps) {
|
|
|
1018
1748
|
return;
|
|
1019
1749
|
}
|
|
1020
1750
|
try {
|
|
1021
|
-
let emailSent =
|
|
1751
|
+
let emailSent = false;
|
|
1752
|
+
let deliveryAttempted = false;
|
|
1022
1753
|
let emailError;
|
|
1023
1754
|
const reset = core.requestPasswordReset({ email });
|
|
1024
1755
|
if (reset.token) {
|
|
1756
|
+
deliveryAttempted = true;
|
|
1025
1757
|
const delivery = await deliverAuthEmail('password-reset', email, reset.token, req);
|
|
1026
1758
|
emailSent = delivery.ok;
|
|
1027
1759
|
emailError = delivery.error;
|
|
@@ -1034,6 +1766,7 @@ export function registerAuthRoutes(deps) {
|
|
|
1034
1766
|
res.end(JSON.stringify({
|
|
1035
1767
|
success: true,
|
|
1036
1768
|
emailSent,
|
|
1769
|
+
deliveryAttempted,
|
|
1037
1770
|
emailError,
|
|
1038
1771
|
resetToken: shouldExposeDevAuthTokens() ? (reset.token || null) : undefined
|
|
1039
1772
|
}));
|
|
@@ -1056,7 +1789,7 @@ export function registerAuthRoutes(deps) {
|
|
|
1056
1789
|
const password = String(body?.password || '');
|
|
1057
1790
|
if (!token || !password) {
|
|
1058
1791
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1059
|
-
res.end(JSON.stringify({ success: false, error: 'Token and password are required.' }));
|
|
1792
|
+
res.end(JSON.stringify({ success: false, error: 'Token and password are required.', code: 'TOKEN_PASSWORD_REQUIRED' }));
|
|
1060
1793
|
return;
|
|
1061
1794
|
}
|
|
1062
1795
|
try {
|
|
@@ -1081,7 +1814,7 @@ export function registerAuthRoutes(deps) {
|
|
|
1081
1814
|
statusCode: resolveErrorStatusCode(error, 400)
|
|
1082
1815
|
});
|
|
1083
1816
|
res.writeHead(resolveErrorStatusCode(error, 400), { 'Content-Type': 'application/json' });
|
|
1084
|
-
res.end(JSON.stringify({ success: false, error: String(error?.message || 'Unable to reset password') }));
|
|
1817
|
+
res.end(JSON.stringify({ success: false, error: String(error?.message || 'Unable to reset password'), code: error?.code }));
|
|
1085
1818
|
}
|
|
1086
1819
|
});
|
|
1087
1820
|
// === SECTION: Invite flows ===
|
|
@@ -1105,6 +1838,18 @@ export function registerAuthRoutes(deps) {
|
|
|
1105
1838
|
res.end(JSON.stringify({ success: false, state: details.state, error: 'Invalid or expired invite token.' }));
|
|
1106
1839
|
return;
|
|
1107
1840
|
}
|
|
1841
|
+
// Augment availableMethods with enabled OAuth providers for pending_setup invitees
|
|
1842
|
+
const oauthProviders = core.getGlobalSettings().oauthProviders || {};
|
|
1843
|
+
const enabledOAuthProviders = [];
|
|
1844
|
+
if (oauthProviders.google?.enabled)
|
|
1845
|
+
enabledOAuthProviders.push('google');
|
|
1846
|
+
if (oauthProviders.github?.enabled)
|
|
1847
|
+
enabledOAuthProviders.push('github');
|
|
1848
|
+
if (oauthProviders.apple?.enabled)
|
|
1849
|
+
enabledOAuthProviders.push('apple');
|
|
1850
|
+
const augmentedMethods = details.inviteeState === 'pending_setup'
|
|
1851
|
+
? [...(details.availableMethods || []), ...enabledOAuthProviders.filter(p => !(details.availableMethods || []).includes(p))]
|
|
1852
|
+
: (details.availableMethods || []);
|
|
1108
1853
|
auditAuthAction(req, 'auth-invite-opened', {
|
|
1109
1854
|
userId: details.userId,
|
|
1110
1855
|
email: details.email,
|
|
@@ -1118,6 +1863,8 @@ export function registerAuthRoutes(deps) {
|
|
|
1118
1863
|
state: details.state,
|
|
1119
1864
|
email: details.email,
|
|
1120
1865
|
workspaceId: details.workspaceId,
|
|
1866
|
+
inviteeState: details.inviteeState,
|
|
1867
|
+
availableMethods: augmentedMethods,
|
|
1121
1868
|
inviteeKind: details.inviteeKind,
|
|
1122
1869
|
passwordRequired: details.passwordRequired === true
|
|
1123
1870
|
}));
|
|
@@ -1147,10 +1894,11 @@ export function registerAuthRoutes(deps) {
|
|
|
1147
1894
|
}
|
|
1148
1895
|
try {
|
|
1149
1896
|
const accepted = core.acceptInviteWithToken({ token, password });
|
|
1150
|
-
const cookie = createSessionTokenCookie(`
|
|
1897
|
+
const cookie = createSessionTokenCookie(`usr:${accepted.userId}`, authConfig, {
|
|
1151
1898
|
role: accepted.role,
|
|
1152
1899
|
workspaceId: accepted.workspaceId,
|
|
1153
1900
|
userId: accepted.userId,
|
|
1901
|
+
identityId: accepted.identityId,
|
|
1154
1902
|
...resolveSessionCookieAttributesForRequest(req, authConfig)
|
|
1155
1903
|
});
|
|
1156
1904
|
res.setHeader('Set-Cookie', cookie);
|
|
@@ -1203,11 +1951,12 @@ export function registerAuthRoutes(deps) {
|
|
|
1203
1951
|
}
|
|
1204
1952
|
try {
|
|
1205
1953
|
const accepted = core.joinInviteWithAuthenticatedUser({ token, userId: access.userId });
|
|
1206
|
-
const sessionId = String(access.sessionId || `
|
|
1954
|
+
const sessionId = String(access.sessionId || `usr:${accepted.userId}`).trim() || `usr:${accepted.userId}`;
|
|
1207
1955
|
const cookie = createSessionTokenCookie(sessionId, authConfig, {
|
|
1208
1956
|
role: accepted.role,
|
|
1209
1957
|
workspaceId: accepted.workspaceId,
|
|
1210
1958
|
userId: accepted.userId,
|
|
1959
|
+
identityId: access.identityId || undefined,
|
|
1211
1960
|
...resolveSessionCookieAttributesForRequest(req, authConfig)
|
|
1212
1961
|
});
|
|
1213
1962
|
res.setHeader('Set-Cookie', cookie);
|
|
@@ -1276,7 +2025,7 @@ export function registerAuthRoutes(deps) {
|
|
|
1276
2025
|
return;
|
|
1277
2026
|
}
|
|
1278
2027
|
try {
|
|
1279
|
-
const userId =
|
|
2028
|
+
const userId = resolveAuthenticatedUserId(req);
|
|
1280
2029
|
const payload = core.getAccountAccessStatus(userId);
|
|
1281
2030
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1282
2031
|
res.end(JSON.stringify(payload));
|
|
@@ -1389,7 +2138,7 @@ export function registerAuthRoutes(deps) {
|
|
|
1389
2138
|
seatUsage,
|
|
1390
2139
|
upgradeHints: canInvite
|
|
1391
2140
|
? []
|
|
1392
|
-
: ['Team management requires
|
|
2141
|
+
: ['Team management requires an active plan with collaboration access.'],
|
|
1393
2142
|
supportLink: 'mailto:support@taskforcehq.ai'
|
|
1394
2143
|
}));
|
|
1395
2144
|
}
|
|
@@ -1399,6 +2148,31 @@ export function registerAuthRoutes(deps) {
|
|
|
1399
2148
|
res.end(JSON.stringify({ error: String(error?.message || 'Unable to build plan preview'), code: error?.code, details: error?.details }));
|
|
1400
2149
|
}
|
|
1401
2150
|
});
|
|
2151
|
+
// GET /api/taskforce/account/ai-profile-seat-usage - Read cloud-authoritative AI seat usage for the current workspace.
|
|
2152
|
+
addRoute('GET', '/api/taskforce/account/ai-profile-seat-usage', async (req, res) => {
|
|
2153
|
+
if (!ensureCloudPlanData(req, res))
|
|
2154
|
+
return;
|
|
2155
|
+
if (!ensureAuthenticatedUser(req)) {
|
|
2156
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
2157
|
+
res.end(JSON.stringify({ error: 'Authentication required.' }));
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
try {
|
|
2161
|
+
const wid = requestWorkspaceId(req);
|
|
2162
|
+
const aiProfileSeatUsage = core.countAiProfileUsage(wid);
|
|
2163
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2164
|
+
res.end(JSON.stringify({ aiProfileSeatUsage }));
|
|
2165
|
+
}
|
|
2166
|
+
catch (error) {
|
|
2167
|
+
const statusCode = resolveErrorStatusCode(error, 400);
|
|
2168
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
2169
|
+
res.end(JSON.stringify({
|
|
2170
|
+
error: String(error?.message || 'Unable to build AI profile seat usage'),
|
|
2171
|
+
code: error?.code,
|
|
2172
|
+
details: error?.details
|
|
2173
|
+
}));
|
|
2174
|
+
}
|
|
2175
|
+
});
|
|
1402
2176
|
// === SECTION: Workspace team management (workspace admin) ===
|
|
1403
2177
|
// GET /api/taskforce/auth/workspace-members - Auth-backed workspace member list for local runtime identity hydration.
|
|
1404
2178
|
addRoute('GET', '/api/taskforce/auth/workspace-members', async (req, res) => {
|
|
@@ -1423,12 +2197,17 @@ export function registerAuthRoutes(deps) {
|
|
|
1423
2197
|
res.end(JSON.stringify({ error: 'Authentication required.' }));
|
|
1424
2198
|
return;
|
|
1425
2199
|
}
|
|
2200
|
+
const startedAt = getRouteTimingNow();
|
|
1426
2201
|
const wid = requestWorkspaceId(req);
|
|
2202
|
+
const traceId = requestBootstrapTraceId(req);
|
|
2203
|
+
const adminRoleStartedAt = getRouteTimingNow();
|
|
1427
2204
|
const includeProfileToken = context.authConfig?.enabled !== true || ensureWorkspaceAdminRole(req);
|
|
1428
|
-
const
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
.
|
|
2205
|
+
const adminRoleResolvedAt = getRouteTimingNow();
|
|
2206
|
+
const usersStartedAt = getRouteTimingNow();
|
|
2207
|
+
const activeWorkspaceUsers = core.listWorkspaceUsers(wid)
|
|
2208
|
+
.filter((user) => String(user.status || '').trim().toLowerCase() === 'active' && user.disabled !== true);
|
|
2209
|
+
const usersLoadedAt = getRouteTimingNow();
|
|
2210
|
+
const workspaceUsers = activeWorkspaceUsers.map((user) => ({
|
|
1432
2211
|
value: user.userId,
|
|
1433
2212
|
label: String(user.displayName || user.email || user.userId).trim() || user.userId,
|
|
1434
2213
|
icon: 'User',
|
|
@@ -1437,11 +2216,15 @@ export function registerAuthRoutes(deps) {
|
|
|
1437
2216
|
userId: user.userId,
|
|
1438
2217
|
email: user.email,
|
|
1439
2218
|
displayName: user.displayName ?? null,
|
|
2219
|
+
avatarUrl: user.avatarUrl ?? null,
|
|
1440
2220
|
status: user.status,
|
|
1441
2221
|
disabled: user.disabled === true
|
|
1442
2222
|
}));
|
|
2223
|
+
const usersMappedAt = getRouteTimingNow();
|
|
2224
|
+
const aiProfileDiagnostics = { metadataBackfillCount: 0 };
|
|
2225
|
+
const aiProfilesStartedAt = getRouteTimingNow();
|
|
1443
2226
|
const aiProfiles = core
|
|
1444
|
-
.listAiProfiles(wid)
|
|
2227
|
+
.listAiProfiles(wid, { diagnostics: aiProfileDiagnostics })
|
|
1445
2228
|
.map((profile) => ({
|
|
1446
2229
|
value: profile.id,
|
|
1447
2230
|
label: profile.name,
|
|
@@ -1449,19 +2232,90 @@ export function registerAuthRoutes(deps) {
|
|
|
1449
2232
|
username: profile.username,
|
|
1450
2233
|
icon: profile.icon,
|
|
1451
2234
|
color: profile.color,
|
|
2235
|
+
avatarUrl: profile.avatarUrl ?? null,
|
|
2236
|
+
avatarSourceUrl: profile.avatarSourceUrl ?? null,
|
|
1452
2237
|
kind: 'agent',
|
|
1453
2238
|
description: profile.description ?? null,
|
|
1454
2239
|
role: profile.role ?? null,
|
|
1455
2240
|
provider: profile.provider ?? null,
|
|
1456
2241
|
model: profile.model ?? null,
|
|
1457
2242
|
surfaceType: profile.surfaceType ?? null,
|
|
2243
|
+
seatScope: profile.seatScope ?? null,
|
|
2244
|
+
archivedAt: profile.archivedAt ?? null,
|
|
1458
2245
|
createdAt: profile.createdAt,
|
|
1459
2246
|
updatedAt: profile.updatedAt,
|
|
1460
2247
|
lastActiveAt: profile.lastActiveAt ?? null
|
|
1461
2248
|
}));
|
|
2249
|
+
const aiProfilesLoadedAt = getRouteTimingNow();
|
|
2250
|
+
const seatUsageStartedAt = getRouteTimingNow();
|
|
2251
|
+
const aiProfileSeatUsage = core.countAiProfileUsage(wid);
|
|
2252
|
+
const seatUsageLoadedAt = getRouteTimingNow();
|
|
1462
2253
|
const assignees = [...workspaceUsers, ...aiProfiles];
|
|
2254
|
+
const serializeStartedAt = getRouteTimingNow();
|
|
2255
|
+
const serialized = serializeJsonWithTiming({ assignees, aiProfileSeatUsage }, serializeStartedAt);
|
|
2256
|
+
const totalDurationMs = Math.max(0, getRouteTimingNow() - startedAt);
|
|
2257
|
+
setServerTimingHeader(res, [
|
|
2258
|
+
{ name: 'assignee-options', durationMs: totalDurationMs },
|
|
2259
|
+
{ name: 'assignee-options-admin-role', durationMs: adminRoleResolvedAt - adminRoleStartedAt },
|
|
2260
|
+
{ name: 'assignee-options-users', durationMs: usersLoadedAt - usersStartedAt },
|
|
2261
|
+
{ name: 'assignee-options-users-map', durationMs: usersMappedAt - usersLoadedAt },
|
|
2262
|
+
{ name: 'assignee-options-ai-profiles', durationMs: aiProfilesLoadedAt - aiProfilesStartedAt },
|
|
2263
|
+
{ name: 'assignee-options-seat-usage', durationMs: seatUsageLoadedAt - seatUsageStartedAt },
|
|
2264
|
+
{ name: 'assignee-options-json', durationMs: serialized.durationMs },
|
|
2265
|
+
]);
|
|
2266
|
+
logSlowBootstrapRoute({
|
|
2267
|
+
route: '/api/taskforce/workspace/assignee-options',
|
|
2268
|
+
workspaceId: wid,
|
|
2269
|
+
traceId,
|
|
2270
|
+
durationMs: totalDurationMs,
|
|
2271
|
+
details: {
|
|
2272
|
+
humanAssigneeCount: workspaceUsers.length,
|
|
2273
|
+
workspaceUserCount: activeWorkspaceUsers.length,
|
|
2274
|
+
aiProfileCount: aiProfiles.length,
|
|
2275
|
+
metadataBackfillCount: aiProfileDiagnostics.metadataBackfillCount,
|
|
2276
|
+
responseBytes: serialized.byteLength,
|
|
2277
|
+
adminRoleMs: Math.max(0, adminRoleResolvedAt - adminRoleStartedAt),
|
|
2278
|
+
listWorkspaceUsersMs: Math.max(0, usersLoadedAt - usersStartedAt),
|
|
2279
|
+
workspaceUsersMapMs: Math.max(0, usersMappedAt - usersLoadedAt),
|
|
2280
|
+
listAiProfilesMs: Math.max(0, aiProfilesLoadedAt - aiProfilesStartedAt),
|
|
2281
|
+
countAiProfileUsageMs: Math.max(0, seatUsageLoadedAt - seatUsageStartedAt),
|
|
2282
|
+
jsonSerializeMs: serialized.durationMs,
|
|
2283
|
+
},
|
|
2284
|
+
});
|
|
1463
2285
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1464
|
-
res.end(
|
|
2286
|
+
res.end(serialized.body);
|
|
2287
|
+
});
|
|
2288
|
+
// POST /api/taskforce/workspace/ai-profiles/avatar - Assign or clear an AI profile avatar image.
|
|
2289
|
+
addRoute('POST', '/api/taskforce/workspace/ai-profiles/avatar', async (req, res) => {
|
|
2290
|
+
if (!ensureWorkspaceAdminRole(req)) {
|
|
2291
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
2292
|
+
res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
|
|
2293
|
+
return;
|
|
2294
|
+
}
|
|
2295
|
+
const body = await parseJsonBody(req);
|
|
2296
|
+
const wid = requestWorkspaceId(req);
|
|
2297
|
+
const profileId = String(body?.profileId || '').trim();
|
|
2298
|
+
const avatarUrl = body?.avatarUrl === null ? null : String(body?.avatarUrl || '').trim();
|
|
2299
|
+
const avatarSourceUrl = body?.avatarSourceUrl === undefined
|
|
2300
|
+
? undefined
|
|
2301
|
+
: body?.avatarSourceUrl === null
|
|
2302
|
+
? null
|
|
2303
|
+
: String(body?.avatarSourceUrl || '').trim();
|
|
2304
|
+
if (!profileId) {
|
|
2305
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2306
|
+
res.end(JSON.stringify({ error: 'profileId is required.' }));
|
|
2307
|
+
return;
|
|
2308
|
+
}
|
|
2309
|
+
try {
|
|
2310
|
+
const profile = core.updateAiProfileAvatar({ workspaceId: wid, profileId, avatarUrl, avatarSourceUrl });
|
|
2311
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2312
|
+
res.end(JSON.stringify({ success: true, profile }));
|
|
2313
|
+
}
|
|
2314
|
+
catch (err) {
|
|
2315
|
+
const status = err?.statusCode ?? 500;
|
|
2316
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
2317
|
+
res.end(JSON.stringify({ error: err?.message ?? 'Failed to update AI profile avatar.' }));
|
|
2318
|
+
}
|
|
1465
2319
|
});
|
|
1466
2320
|
// POST /api/taskforce/workspace/ai-profiles/merge - Merge a duplicate AI profile into another, reassigning all references.
|
|
1467
2321
|
addRoute('POST', '/api/taskforce/workspace/ai-profiles/merge', async (req, res) => {
|
|
@@ -1490,6 +2344,41 @@ export function registerAuthRoutes(deps) {
|
|
|
1490
2344
|
res.end(JSON.stringify({ error: err?.message ?? 'Merge failed.' }));
|
|
1491
2345
|
}
|
|
1492
2346
|
});
|
|
2347
|
+
// POST /api/taskforce/workspace/ai-profiles/archive - Remove an AI profile from the active roster without deleting its history.
|
|
2348
|
+
addRoute('POST', '/api/taskforce/workspace/ai-profiles/archive', async (req, res) => {
|
|
2349
|
+
if (!ensureWorkspaceAdminRole(req)) {
|
|
2350
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
2351
|
+
res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
|
|
2352
|
+
return;
|
|
2353
|
+
}
|
|
2354
|
+
const body = await parseJsonBody(req);
|
|
2355
|
+
const wid = requestWorkspaceId(req);
|
|
2356
|
+
const profileId = String(body.profileId || '').trim();
|
|
2357
|
+
const reason = String(body.reason || '').trim() || 'manual_archive';
|
|
2358
|
+
if (!profileId) {
|
|
2359
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2360
|
+
res.end(JSON.stringify({ error: 'profileId is required.' }));
|
|
2361
|
+
return;
|
|
2362
|
+
}
|
|
2363
|
+
try {
|
|
2364
|
+
const profile = core.archiveAiProfile({
|
|
2365
|
+
workspaceId: wid,
|
|
2366
|
+
profileId,
|
|
2367
|
+
archivedBy: requestUserId(req),
|
|
2368
|
+
reason
|
|
2369
|
+
});
|
|
2370
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2371
|
+
res.end(JSON.stringify({ profile }));
|
|
2372
|
+
}
|
|
2373
|
+
catch (err) {
|
|
2374
|
+
const status = err?.statusCode ?? 500;
|
|
2375
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
2376
|
+
res.end(JSON.stringify({
|
|
2377
|
+
error: err?.message ?? 'Failed to archive AI profile.',
|
|
2378
|
+
details: err?.details ?? null
|
|
2379
|
+
}));
|
|
2380
|
+
}
|
|
2381
|
+
});
|
|
1493
2382
|
// GET /api/taskforce/admin/users - List workspace users for team management.
|
|
1494
2383
|
addRoute('GET', '/api/taskforce/admin/users', async (req, res) => {
|
|
1495
2384
|
if (!ensureWorkspaceAdminRole(req)) {
|
|
@@ -1498,8 +2387,9 @@ export function registerAuthRoutes(deps) {
|
|
|
1498
2387
|
return;
|
|
1499
2388
|
}
|
|
1500
2389
|
const wid = requestWorkspaceId(req);
|
|
2390
|
+
const actorUserId = resolveAuthenticatedUserId(req);
|
|
1501
2391
|
const gate = core.evaluateFeatureAccess({
|
|
1502
|
-
userId:
|
|
2392
|
+
userId: actorUserId,
|
|
1503
2393
|
workspaceId: wid,
|
|
1504
2394
|
featureKey: 'collaboration.team_management'
|
|
1505
2395
|
});
|
|
@@ -1514,13 +2404,14 @@ export function registerAuthRoutes(deps) {
|
|
|
1514
2404
|
});
|
|
1515
2405
|
// POST /api/taskforce/admin/users/invite - Invite a user into a workspace.
|
|
1516
2406
|
addRoute('POST', '/api/taskforce/admin/users/invite', async (req, res) => {
|
|
1517
|
-
|
|
2407
|
+
const body = await parseJsonBody(req);
|
|
2408
|
+
const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
|
|
2409
|
+
if (!ensureWorkspaceAdminRole(req, wid)) {
|
|
1518
2410
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
1519
2411
|
res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
|
|
1520
2412
|
return;
|
|
1521
2413
|
}
|
|
1522
|
-
const
|
|
1523
|
-
const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
|
|
2414
|
+
const actorUserId = resolveAuthenticatedUserId(req);
|
|
1524
2415
|
const email = String(body?.email || '').trim().toLowerCase();
|
|
1525
2416
|
const role = String(body?.role || 'member').trim().toLowerCase();
|
|
1526
2417
|
const permissionMode = String(body?.permissionMode || 'read-write').trim().toLowerCase();
|
|
@@ -1530,7 +2421,7 @@ export function registerAuthRoutes(deps) {
|
|
|
1530
2421
|
return;
|
|
1531
2422
|
}
|
|
1532
2423
|
const inviteGate = core.evaluateFeatureAccess({
|
|
1533
|
-
userId:
|
|
2424
|
+
userId: actorUserId,
|
|
1534
2425
|
workspaceId: wid,
|
|
1535
2426
|
featureKey: 'collaboration.invites'
|
|
1536
2427
|
});
|
|
@@ -1553,8 +2444,8 @@ export function registerAuthRoutes(deps) {
|
|
|
1553
2444
|
let inviteEmailError = null;
|
|
1554
2445
|
if (inviteToken.token) {
|
|
1555
2446
|
const workspace = core.getWorkspaceProfile(wid);
|
|
1556
|
-
const actorIdentity = core.resolveUserIdentity(
|
|
1557
|
-
const inviterEmailOrLabel = String(actorIdentity?.email ||
|
|
2447
|
+
const actorIdentity = core.resolveUserIdentity(actorUserId);
|
|
2448
|
+
const inviterEmailOrLabel = String(actorIdentity?.email || actorUserId || 'Taskforce').trim();
|
|
1558
2449
|
const delivery = await deliverInviteEmail(email, inviteToken.token, req, {
|
|
1559
2450
|
workspaceName: String(workspace?.name || wid),
|
|
1560
2451
|
inviterEmailOrLabel
|
|
@@ -1569,7 +2460,7 @@ export function registerAuthRoutes(deps) {
|
|
|
1569
2460
|
role,
|
|
1570
2461
|
permissionMode: role === 'member' ? (permissionMode === 'read-only' ? 'read-only' : 'read-write') : 'read-write',
|
|
1571
2462
|
inviteEmailSent
|
|
1572
|
-
});
|
|
2463
|
+
}, wid);
|
|
1573
2464
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1574
2465
|
res.end(JSON.stringify({
|
|
1575
2466
|
success: true,
|
|
@@ -1586,13 +2477,14 @@ export function registerAuthRoutes(deps) {
|
|
|
1586
2477
|
});
|
|
1587
2478
|
// PATCH /api/taskforce/admin/users/:id/role - Update workspace role for a user.
|
|
1588
2479
|
addRoute('PATCH', '/api/taskforce/admin/users/:id/role', async (req, res, params) => {
|
|
1589
|
-
|
|
2480
|
+
const body = await parseJsonBody(req);
|
|
2481
|
+
const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
|
|
2482
|
+
if (!ensureWorkspaceAdminRole(req, wid)) {
|
|
1590
2483
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
1591
2484
|
res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
|
|
1592
2485
|
return;
|
|
1593
2486
|
}
|
|
1594
|
-
const
|
|
1595
|
-
const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
|
|
2487
|
+
const actorUserId = resolveAuthenticatedUserId(req);
|
|
1596
2488
|
const role = String(body?.role || '').trim().toLowerCase();
|
|
1597
2489
|
if (role !== 'owner' && role !== 'admin' && role !== 'member') {
|
|
1598
2490
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
@@ -1600,7 +2492,7 @@ export function registerAuthRoutes(deps) {
|
|
|
1600
2492
|
return;
|
|
1601
2493
|
}
|
|
1602
2494
|
const gate = core.evaluateFeatureAccess({
|
|
1603
|
-
userId:
|
|
2495
|
+
userId: actorUserId,
|
|
1604
2496
|
workspaceId: wid,
|
|
1605
2497
|
featureKey: 'collaboration.team_management'
|
|
1606
2498
|
});
|
|
@@ -1616,7 +2508,7 @@ export function registerAuthRoutes(deps) {
|
|
|
1616
2508
|
res.end(JSON.stringify({ error: 'User membership not found.' }));
|
|
1617
2509
|
return;
|
|
1618
2510
|
}
|
|
1619
|
-
auditAuthAction(req, 'update-workspace-user-role', { workspaceId: wid, targetUserId: params.id, role });
|
|
2511
|
+
auditAuthAction(req, 'update-workspace-user-role', { workspaceId: wid, targetUserId: params.id, role }, wid);
|
|
1620
2512
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1621
2513
|
res.end(JSON.stringify({ success: true }));
|
|
1622
2514
|
}
|
|
@@ -1628,13 +2520,14 @@ export function registerAuthRoutes(deps) {
|
|
|
1628
2520
|
});
|
|
1629
2521
|
// PATCH /api/taskforce/admin/users/:id/permission-mode - Update member permission mode.
|
|
1630
2522
|
addRoute('PATCH', '/api/taskforce/admin/users/:id/permission-mode', async (req, res, params) => {
|
|
1631
|
-
|
|
2523
|
+
const body = await parseJsonBody(req);
|
|
2524
|
+
const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
|
|
2525
|
+
if (!ensureWorkspaceAdminRole(req, wid)) {
|
|
1632
2526
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
1633
2527
|
res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
|
|
1634
2528
|
return;
|
|
1635
2529
|
}
|
|
1636
|
-
const
|
|
1637
|
-
const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
|
|
2530
|
+
const actorUserId = resolveAuthenticatedUserId(req);
|
|
1638
2531
|
const mode = String(body?.mode || '').trim().toLowerCase();
|
|
1639
2532
|
if (mode !== 'read-only' && mode !== 'read-write') {
|
|
1640
2533
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
@@ -1642,7 +2535,7 @@ export function registerAuthRoutes(deps) {
|
|
|
1642
2535
|
return;
|
|
1643
2536
|
}
|
|
1644
2537
|
const gate = core.evaluateFeatureAccess({
|
|
1645
|
-
userId:
|
|
2538
|
+
userId: actorUserId,
|
|
1646
2539
|
workspaceId: wid,
|
|
1647
2540
|
featureKey: 'collaboration.team_management'
|
|
1648
2541
|
});
|
|
@@ -1658,7 +2551,7 @@ export function registerAuthRoutes(deps) {
|
|
|
1658
2551
|
res.end(JSON.stringify({ error: 'User membership not found.' }));
|
|
1659
2552
|
return;
|
|
1660
2553
|
}
|
|
1661
|
-
auditAuthAction(req, 'update-workspace-user-permission-mode', { workspaceId: wid, targetUserId: params.id, mode });
|
|
2554
|
+
auditAuthAction(req, 'update-workspace-user-permission-mode', { workspaceId: wid, targetUserId: params.id, mode }, wid);
|
|
1662
2555
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1663
2556
|
res.end(JSON.stringify({ success: true }));
|
|
1664
2557
|
}
|
|
@@ -1670,16 +2563,17 @@ export function registerAuthRoutes(deps) {
|
|
|
1670
2563
|
});
|
|
1671
2564
|
// PATCH /api/taskforce/admin/users/:id/disable - Disable or reactivate a workspace user.
|
|
1672
2565
|
addRoute('PATCH', '/api/taskforce/admin/users/:id/disable', async (req, res, params) => {
|
|
1673
|
-
|
|
2566
|
+
const body = await parseJsonBody(req);
|
|
2567
|
+
const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
|
|
2568
|
+
if (!ensureWorkspaceAdminRole(req, wid)) {
|
|
1674
2569
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
1675
2570
|
res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
|
|
1676
2571
|
return;
|
|
1677
2572
|
}
|
|
1678
|
-
const
|
|
1679
|
-
const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
|
|
2573
|
+
const actorUserId = resolveAuthenticatedUserId(req);
|
|
1680
2574
|
const disabled = body?.disabled === true;
|
|
1681
2575
|
const gate = core.evaluateFeatureAccess({
|
|
1682
|
-
userId:
|
|
2576
|
+
userId: actorUserId,
|
|
1683
2577
|
workspaceId: wid,
|
|
1684
2578
|
featureKey: 'collaboration.team_management'
|
|
1685
2579
|
});
|
|
@@ -1695,7 +2589,7 @@ export function registerAuthRoutes(deps) {
|
|
|
1695
2589
|
res.end(JSON.stringify({ error: 'User membership not found.' }));
|
|
1696
2590
|
return;
|
|
1697
2591
|
}
|
|
1698
|
-
auditAuthAction(req, 'set-workspace-user-disabled', { workspaceId: wid, targetUserId: params.id, disabled });
|
|
2592
|
+
auditAuthAction(req, 'set-workspace-user-disabled', { workspaceId: wid, targetUserId: params.id, disabled }, wid);
|
|
1699
2593
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1700
2594
|
res.end(JSON.stringify({ success: true }));
|
|
1701
2595
|
}
|
|
@@ -1707,15 +2601,16 @@ export function registerAuthRoutes(deps) {
|
|
|
1707
2601
|
});
|
|
1708
2602
|
// POST /api/taskforce/admin/users/:id/invite/revoke - Revoke invite acceptance token.
|
|
1709
2603
|
addRoute('POST', '/api/taskforce/admin/users/:id/invite/revoke', async (req, res, params) => {
|
|
1710
|
-
|
|
2604
|
+
const body = await parseJsonBody(req);
|
|
2605
|
+
const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
|
|
2606
|
+
if (!ensureWorkspaceAdminRole(req, wid)) {
|
|
1711
2607
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
1712
2608
|
res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
|
|
1713
2609
|
return;
|
|
1714
2610
|
}
|
|
1715
|
-
const
|
|
1716
|
-
const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
|
|
2611
|
+
const actorUserId = resolveAuthenticatedUserId(req);
|
|
1717
2612
|
const gate = core.evaluateFeatureAccess({
|
|
1718
|
-
userId:
|
|
2613
|
+
userId: actorUserId,
|
|
1719
2614
|
workspaceId: wid,
|
|
1720
2615
|
featureKey: 'collaboration.team_management'
|
|
1721
2616
|
});
|
|
@@ -1725,21 +2620,22 @@ export function registerAuthRoutes(deps) {
|
|
|
1725
2620
|
return;
|
|
1726
2621
|
}
|
|
1727
2622
|
const revoked = core.revokeInviteAcceptanceForUser(params.id);
|
|
1728
|
-
auditAuthAction(req, 'revoke-invite', { workspaceId: wid, targetUserId: params.id, revoked: revoked.revoked });
|
|
2623
|
+
auditAuthAction(req, 'revoke-invite', { workspaceId: wid, targetUserId: params.id, revoked: revoked.revoked }, wid);
|
|
1729
2624
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1730
2625
|
res.end(JSON.stringify({ success: true, revoked: revoked.revoked }));
|
|
1731
2626
|
});
|
|
1732
2627
|
// DELETE /api/taskforce/admin/users/:id - Remove workspace membership.
|
|
1733
2628
|
addRoute('DELETE', '/api/taskforce/admin/users/:id', async (req, res, params) => {
|
|
1734
|
-
|
|
2629
|
+
const reqUrl = new URL(req.url || '/api/taskforce/admin/users/:id', 'http://localhost');
|
|
2630
|
+
const wid = String(reqUrl.searchParams.get('workspaceId') || requestWorkspaceId(req)).trim() || 'default';
|
|
2631
|
+
if (!ensureWorkspaceAdminRole(req, wid)) {
|
|
1735
2632
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
1736
2633
|
res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
|
|
1737
2634
|
return;
|
|
1738
2635
|
}
|
|
1739
|
-
const
|
|
1740
|
-
const wid = String(reqUrl.searchParams.get('workspaceId') || requestWorkspaceId(req)).trim() || 'default';
|
|
2636
|
+
const actorUserId = resolveAuthenticatedUserId(req);
|
|
1741
2637
|
const gate = core.evaluateFeatureAccess({
|
|
1742
|
-
userId:
|
|
2638
|
+
userId: actorUserId,
|
|
1743
2639
|
workspaceId: wid,
|
|
1744
2640
|
featureKey: 'collaboration.team_management'
|
|
1745
2641
|
});
|
|
@@ -1755,7 +2651,7 @@ export function registerAuthRoutes(deps) {
|
|
|
1755
2651
|
res.end(JSON.stringify({ error: 'User membership not found.' }));
|
|
1756
2652
|
return;
|
|
1757
2653
|
}
|
|
1758
|
-
auditAuthAction(req, 'remove-workspace-user', { workspaceId: wid, targetUserId: params.id });
|
|
2654
|
+
auditAuthAction(req, 'remove-workspace-user', { workspaceId: wid, targetUserId: params.id }, wid);
|
|
1759
2655
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1760
2656
|
res.end(JSON.stringify({ success: true }));
|
|
1761
2657
|
}
|
|
@@ -1773,8 +2669,9 @@ export function registerAuthRoutes(deps) {
|
|
|
1773
2669
|
return;
|
|
1774
2670
|
}
|
|
1775
2671
|
const wid = requestWorkspaceId(req);
|
|
2672
|
+
const actorUserId = resolveAuthenticatedUserId(req);
|
|
1776
2673
|
const gate = core.evaluateFeatureAccess({
|
|
1777
|
-
userId:
|
|
2674
|
+
userId: actorUserId,
|
|
1778
2675
|
workspaceId: wid,
|
|
1779
2676
|
featureKey: 'collaboration.team_management'
|
|
1780
2677
|
});
|