@taskforcehq/taskforce 0.3.305 → 0.3.307
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 +0 -2
- package/dist/Taskforce.module.css +603 -107
- package/dist/TaskforceCore.js +216 -43
- package/dist/TaskforceCore.test.js +611 -28
- 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 +46 -2
- package/dist/compat/workspaceSyncCompat.test.js +38 -1
- package/dist/components/activity/EntityActivityTimeline.d.ts +34 -0
- package/dist/components/activity/EntityActivityTimeline.js +81 -0
- package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
- package/dist/components/context/ContextAttachmentManager.js +561 -0
- package/dist/components/features/AgentsModule.d.ts +14 -1
- package/dist/components/features/AgentsModule.js +366 -18
- package/dist/components/features/AgentsModule.test.js +507 -8
- package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +3 -1
- package/dist/components/features/AnnotatedAttachmentWorkspace.js +118 -4
- package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +52 -0
- package/dist/components/features/DocumentIndex.d.ts +2 -2
- package/dist/components/features/DocumentIndex.js +3 -4
- package/dist/components/features/DocumentViewer.d.ts +7 -1
- package/dist/components/features/DocumentViewer.js +62 -4
- package/dist/components/features/DocumentWorkspace.d.ts +10 -2
- package/dist/components/features/DocumentWorkspace.js +15 -4
- package/dist/components/features/DocumentWorkspace.test.js +51 -1
- package/dist/components/features/TaskSettings.d.ts +2 -0
- package/dist/components/features/TaskSettings.js +83 -78
- package/dist/components/features/TaskSettings.test.js +171 -82
- package/dist/components/features/documentWorkspaceModel.d.ts +1 -1
- package/dist/components/task/TaskActionHeader.js +11 -5
- package/dist/components/task/TaskActionHeader.test.js +10 -0
- package/dist/components/task/TaskCard.js +24 -29
- package/dist/components/task/TaskCard.test.js +38 -3
- package/dist/components/task/TaskContextUpload.js +4 -526
- package/dist/components/task/TaskContextUpload.test.js +3 -3
- package/dist/components/task/TaskForm.js +61 -116
- package/dist/components/task/TaskForm.test.js +110 -8
- package/dist/components/task/TaskKanban.d.ts +2 -1
- package/dist/components/task/TaskKanban.js +94 -21
- package/dist/components/task/TaskKanban.test.js +36 -0
- package/dist/components/task/TaskList.js +5 -1
- package/dist/components/task/TaskList.test.js +4 -0
- package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
- package/dist/components/ui/EditableAvatarButton.js +18 -0
- package/dist/components/ui/HierarchyActionBadge.d.ts +10 -0
- package/dist/components/ui/HierarchyActionBadge.js +5 -0
- package/dist/components/views/AppShellHeader.d.ts +3 -1
- package/dist/components/views/AppShellHeader.js +27 -11
- package/dist/components/views/AppShellHeader.test.js +18 -1
- package/dist/components/views/PlanComparisonPage.d.ts +9 -1
- package/dist/components/views/PlanComparisonPage.js +78 -14
- package/dist/components/views/PlanComparisonPage.test.js +257 -0
- package/dist/components/views/PlansPage.js +127 -36
- package/dist/components/views/PlansPage.test.js +294 -12
- package/dist/components/views/StandaloneLayout.js +529 -592
- package/dist/components/views/WidgetView.js +10 -8
- package/dist/components/views/panels/PlanningDrawer.d.ts +25 -5
- package/dist/components/views/panels/PlanningDrawer.js +270 -55
- package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
- package/dist/components/views/panels/PlanningDrawer.test.js +529 -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 +26 -1
- package/dist/components/views/standalone/modals/AccountHubModal.js +76 -2
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +20 -3
- 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 -0
- package/dist/core/AiProfileService.d.ts +46 -2
- package/dist/core/AiProfileService.js +1073 -82
- package/dist/core/AiProfileService.test.js +19 -1
- package/dist/core/AiProfiles.test.js +1249 -44
- package/dist/core/AttachmentLinkService.d.ts +3 -0
- package/dist/core/AttachmentLinkService.js +110 -23
- 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.js +3 -3
- package/dist/core/EntitlementsPolicy.test.js +2 -2
- package/dist/core/GlobalSettingsService.js +127 -6
- package/dist/core/McpTokenService.d.ts +12 -0
- package/dist/core/McpTokenService.js +44 -1
- package/dist/core/PlanEntitlementService.d.ts +23 -2
- package/dist/core/PlanEntitlementService.js +343 -57
- package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
- package/dist/core/PlanFeatureCatalog.test.js +84 -0
- package/dist/core/PlanVersionPolicy.test.js +35 -2
- package/dist/core/PlanningEntities.test.js +418 -4
- package/dist/core/SignupMonetizationSettings.test.js +199 -27
- package/dist/core/SyncReconciliationService.js +2 -65
- package/dist/core/SystemAdmin.test.js +42 -42
- package/dist/core/TaskActivityService.d.ts +28 -1
- package/dist/core/TaskActivityService.js +227 -46
- package/dist/core/TaskActivitySync.test.d.ts +1 -0
- package/dist/core/TaskActivitySync.test.js +90 -0
- package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
- package/dist/core/TaskAuditTimeline.test.js +96 -0
- package/dist/core/Taskforce.d.ts +415 -40
- package/dist/core/Taskforce.ids.test.js +58 -0
- package/dist/core/Taskforce.js +1543 -266
- package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
- package/dist/core/Taskforce.listTasksSlim.test.js +91 -0
- package/dist/core/Taskforce.mcpAuth.test.js +109 -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 +117 -16
- package/dist/core/shared.d.ts +1 -1
- package/dist/core/shared.js +5 -1
- package/dist/core/types.d.ts +75 -2
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +28 -2
- package/dist/hooks/bootstrapTimeouts.d.ts +4 -0
- package/dist/hooks/bootstrapTimeouts.js +8 -0
- package/dist/hooks/bootstrapTimeouts.test.d.ts +1 -0
- package/dist/hooks/bootstrapTimeouts.test.js +12 -0
- 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 +13 -4
- package/dist/hooks/sync/orchestratorShared.js +129 -48
- 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 +179 -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 +210 -66
- package/dist/hooks/sync/transfers.test.d.ts +1 -0
- package/dist/hooks/sync/transfers.test.js +928 -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/useFilterSort.js +13 -13
- package/dist/hooks/useFilterSort.test.js +87 -1
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +12 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +9 -2
- package/dist/hooks/useSyncOrchestrator.js +409 -183
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +407 -0
- package/dist/hooks/useTaskData.js +8 -3
- package/dist/hooks/useTaskData.test.js +45 -0
- package/dist/hooks/useTaskforce.d.ts +145 -52
- package/dist/hooks/useTaskforce.js +592 -253
- package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
- package/dist/hooks/useTaskforce.runtime-routing.test.js +246 -0
- package/dist/hooks/useTaskforce.sync-behavior.test.js +2468 -201
- package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
- package/dist/hooks/useWorkspaceSyncController.js +13 -2
- package/dist/hooks/useWorkspaceSyncController.test.js +48 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +328 -40
- 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/canonicalAssetHelpers.d.ts +16 -4
- package/dist/mcp/canonicalAssetHelpers.js +18 -10
- package/dist/mcp/canonicalAssetHelpers.test.d.ts +1 -0
- package/dist/mcp/canonicalAssetHelpers.test.js +67 -0
- package/dist/mcp/clientIntegrations.d.ts +1 -0
- package/dist/mcp/clientIntegrations.js +25 -0
- package/dist/mcp/clientIntegrations.test.js +14 -3
- package/dist/mcp/clientProfileDefaults.js +3 -3
- package/dist/mcp/collaborationRegistrar.js +7 -7
- package/dist/mcp/documentAssetRegistrar.js +116 -10
- package/dist/mcp/runtime.d.ts +3 -0
- package/dist/mcp/runtime.js +1099 -185
- package/dist/mcp/runtime.test.js +1491 -140
- package/dist/mcp/taskAttachmentTypes.d.ts +1 -1
- package/dist/mcp/taskPlanningRegistrar.js +48 -0
- package/dist/mcp/toolRegistry.d.ts +56 -0
- package/dist/mcp/toolRegistry.js +8 -0
- package/dist/mcp/toolRegistry.test.js +4 -0
- package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
- package/dist/migrations/taskSchemaMigrations.js +192 -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/aiProfileAvatarAssets.d.ts +49 -0
- package/dist/server/aiProfileAvatarAssets.js +319 -0
- package/dist/server/aiProfileAvatarAssets.test.d.ts +1 -0
- package/dist/server/aiProfileAvatarAssets.test.js +213 -0
- package/dist/server/annotatedAttachmentsRoutes.test.js +69 -10
- 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 +124 -9
- package/dist/server/index.rateLimit.test.js +3 -0
- package/dist/server/index.test.js +111 -3
- package/dist/server/routes/admin.d.ts +3 -0
- package/dist/server/routes/admin.js +459 -28
- package/dist/server/routes/annotatedAttachments.js +51 -0
- package/dist/server/routes/auth.d.ts +26 -2
- package/dist/server/routes/auth.js +1170 -71
- package/dist/server/routes/authSupport.d.ts +5 -1
- package/dist/server/routes/authSupport.js +13 -2
- package/dist/server/routes/billing.js +372 -35
- package/dist/server/routes/billing.test.js +595 -12
- package/dist/server/routes/documents.d.ts +1 -0
- package/dist/server/routes/documents.js +182 -70
- package/dist/server/routes/shared.d.ts +28 -2
- package/dist/server/routes/shared.js +69 -14
- package/dist/server/routes/sync.integration.test.js +658 -39
- package/dist/server/routes/sync.js +74 -608
- 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 +182 -0
- package/dist/server/routes/tasks.js +55 -10
- package/dist/server/routes/workspaces.js +36 -5
- package/dist/server/routes.js +147 -11
- package/dist/server/routes.test.js +2704 -183
- 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/aiProfileSurfaceType.d.ts +1 -1
- package/dist/shared/aiProfileSurfaceType.js +10 -8
- 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/objectStorage.d.ts +1 -0
- package/dist/storage/objectStorage.js +2 -1
- package/dist/storage/objectStorageClient.d.ts +1 -0
- package/dist/storage/objectStorageClient.js +41 -8
- package/dist/storage/objectStorageClient.test.js +26 -1
- package/dist/storage/objectStorageConfig.js +3 -1
- package/dist/storage/objectStorageConfig.test.js +13 -1
- 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 +19 -11
- package/dist/sync/collaborationSyncPayload.test.js +14 -1
- package/dist/sync/collaborationSyncState.d.ts +3 -3
- package/dist/sync/collaborationSyncState.js +19 -12
- package/dist/sync/contentSyncPayload.d.ts +5 -1
- package/dist/sync/contentSyncPayload.js +9 -0
- package/dist/sync/contentSyncPayload.test.js +39 -0
- package/dist/sync/contentSyncState.d.ts +9 -9
- package/dist/sync/contentSyncState.js +248 -107
- package/dist/sync/planningSyncPayload.js +5 -0
- package/dist/sync/planningSyncPayload.test.js +24 -0
- package/dist/sync/syncApplyHandlers.d.ts +11 -4
- package/dist/sync/syncApplyHandlers.js +115 -42
- package/dist/sync/syncApplyHandlers.test.js +251 -4
- package/dist/sync/syncFieldSemantics.d.ts +12 -0
- package/dist/sync/syncFieldSemantics.js +55 -0
- package/dist/sync/syncResourceAdapters.d.ts +46 -0
- package/dist/sync/syncResourceAdapters.js +79 -0
- package/dist/sync/syncService.d.ts +6 -0
- package/dist/sync/syncService.js +65 -1
- package/dist/sync/syncService.test.js +96 -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.d.ts +4 -0
- package/dist/sync/workspacePullFeed.js +132 -72
- package/dist/sync/workspacePullFeed.test.js +92 -0
- package/dist/sync/workspaceRepair.js +5 -2
- package/dist/sync/workspaceSyncModel.d.ts +73 -6
- package/dist/sync/workspaceSyncModel.js +342 -73
- package/dist/sync/workspaceSyncModel.test.js +559 -14
- package/dist/sync/workspaceSyncState.d.ts +17 -0
- package/dist/sync/workspaceSyncState.js +2 -1
- package/dist/sync/workspaceSyncSurface.js +3 -8
- package/dist/types.d.ts +50 -2
- package/dist/ui/assets/AgentsModule-2y82_3DO.js +1 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BTLZZCiQ.js +3 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BVKew-mH.css +1 -0
- package/dist/ui/assets/ContextAttachmentManager-D2epuNnG.js +3 -0
- 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-D4rJKXzd.css +1 -0
- package/dist/ui/assets/DocumentWorkspace-sVa-3Do6.js +252 -0
- package/dist/ui/assets/EntityActivityTimeline-DyqvvhuF.css +1 -0
- package/dist/ui/assets/EntityActivityTimeline-Zods27y_.js +1 -0
- package/dist/ui/assets/{InitiativesModule-Dhm7M8e7.js → InitiativesModule-D4XseTwW.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-D5AcbO0L.js +1 -0
- package/dist/ui/assets/PlansPage-Dvqsz5zc.css +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/TaskContextUpload-uC5qx4Bv.js +1 -0
- package/dist/ui/assets/TaskSettings-CqvPAsTv.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-BcS4afRn.js → WorkflowsModule-oOA9bcdK.js} +4 -4
- package/dist/ui/assets/documentReferences-DPZuTgHh.js +1 -0
- package/dist/ui/assets/index-607WPo_X.js +4 -0
- package/dist/ui/assets/index-CYyodXsg.css +1 -0
- package/dist/ui/assets/{vendor-icons-BSUaRwF8.js → vendor-icons-B32hGuF2.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/ui/product/agent manager.png +0 -0
- package/dist/ui/product/agent_manager_02.png +0 -0
- package/dist/utils/accountProfileSummaryCache.d.ts +5 -0
- package/dist/utils/aiProfileEvents.d.ts +7 -0
- package/dist/utils/aiProfileEvents.js +6 -0
- package/dist/utils/assignees.d.ts +8 -0
- package/dist/utils/assignees.js +10 -1
- package/dist/utils/assignees.test.d.ts +1 -0
- package/dist/utils/assignees.test.js +25 -0
- package/dist/utils/avatarDisplay.d.ts +1 -0
- package/dist/utils/avatarDisplay.js +15 -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.js +3 -0
- package/dist/utils/contextAssetEvents.d.ts +2 -0
- package/dist/utils/contextFiles.d.ts +3 -1
- package/dist/utils/contextFiles.js +2 -1
- 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 +9 -1
- package/dist/utils/taskActivity.js +290 -22
- package/dist/utils/taskActivity.test.js +141 -8
- package/dist/utils/taskSearch.d.ts +22 -0
- package/dist/utils/taskSearch.js +87 -0
- package/dist/utils/taskSearch.test.d.ts +1 -0
- package/dist/utils/taskSearch.test.js +69 -0
- package/dist/utils/taskUtils.js +10 -3
- package/dist/utils/taskUtils.test.d.ts +1 -0
- package/dist/utils/taskUtils.test.js +19 -0
- 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 +3 -2
- package/scripts/export-sqlite-to-postgres.mjs +7 -9
- 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-N2MnQBZk.js +0 -1
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BG6P_GVW.js +0 -3
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +0 -1
- package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +0 -1
- package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +0 -252
- package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
- package/dist/ui/assets/PlansPage-Cq0zXj3I.js +0 -1
- package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
- package/dist/ui/assets/TaskSettings-ln0aF7xc.js +0 -9
- package/dist/ui/assets/index-C2-OXZV7.css +0 -1
- package/dist/ui/assets/index-DcSI5F86.js +0 -6
- 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,9 @@
|
|
|
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
|
|
41
|
+
* POST /api/taskforce/workspace/ai-profiles/avatar/upload
|
|
42
|
+
* POST /api/taskforce/workspace/ai-profiles/surface-type
|
|
29
43
|
* GET /api/taskforce/admin/users
|
|
30
44
|
* POST /api/taskforce/admin/users/invite
|
|
31
45
|
* PATCH /api/taskforce/admin/users/:id/role
|
|
@@ -38,17 +52,22 @@
|
|
|
38
52
|
import * as fs from 'fs';
|
|
39
53
|
import * as path from 'path';
|
|
40
54
|
import { randomUUID } from 'crypto';
|
|
41
|
-
import { parseJsonBody } from './primitives.js';
|
|
55
|
+
import { parseJsonBody, parseFormBody } from './primitives.js';
|
|
56
|
+
import { buildGoogleAuthUrl, exchangeGoogleCode } from '../auth/providers/google.js';
|
|
57
|
+
import { buildAppleAuthUrl, exchangeAppleCode } from '../auth/providers/apple.js';
|
|
58
|
+
import { buildGithubAuthUrl, exchangeGithubCode } from '../auth/providers/github.js';
|
|
42
59
|
import { hasValidSession, createClearSessionCookie, createSessionTokenCookie, resolveRequestAccess, resolveSessionCookieAttributesForRequest, } from '../auth.js';
|
|
43
60
|
import { sendEmail } from '../email.js';
|
|
44
61
|
import { resolveDeploymentConfig } from '../../config/deployment.js';
|
|
45
62
|
import { resolveRuntimeModeContract } from '../../shared/runtimeContract.js';
|
|
46
|
-
import { requestHeader, resolveErrorStatusCode } from './shared.js';
|
|
63
|
+
import { getRouteTimingNow, logSlowBootstrapRoute, requestBootstrapTraceId, requestHeader, resolveErrorStatusCode, serializeJsonWithTiming, setServerTimingHeader } from './shared.js';
|
|
47
64
|
import { buildAuthFlowBehavior, buildSiteBehavior, getRouteEmailConfig, resolvePublicAuthBaseUrl, resolveRuntimeConfigBaseUrl, shouldExposeDevAuthTokens } from './authSupport.js';
|
|
48
65
|
import { normalizeTaskDataRelativePath } from '../../storage/objectStorageClient.js';
|
|
66
|
+
import { clearAiProfileAvatarAssets, saveAiProfileAvatarAssets } from '../aiProfileAvatarAssets.js';
|
|
67
|
+
const AI_PROFILE_AVATAR_UPLOAD_MAX_JSON_BYTES = 16 * 1024 * 1024;
|
|
49
68
|
// === SECTION: Registration ===
|
|
50
69
|
export function registerAuthRoutes(deps) {
|
|
51
|
-
const { addRoute, core, context, requestWorkspaceId, requestUserId, ensureAppAccess, ensureAuthenticatedUser, ensureWorkspaceAdminRole, ensureCloudPlanData, requireCloudRuntime, auditAuthAction, resolveAdminRequestActor, resolveEffectiveObjectStorageConfig, getObjectStorageClient, } = deps;
|
|
70
|
+
const { addRoute, core, context, requestWorkspaceId, requestUserId, resolveAuthenticatedUserId, ensureAppAccess, ensureAuthenticatedUser, ensureWorkspaceAdminRole, ensureCloudPlanData, requireCloudRuntime, auditAuthAction, resolveAdminRequestActor, resolveEffectiveObjectStorageConfig, getObjectStorageClient, workspaceAssetStore, ensureWithinDir, markAssetDeleted, appendStorageTelemetry, } = deps;
|
|
52
71
|
const normalizeTeamWorkspaceRole = (value) => {
|
|
53
72
|
const role = String(value || '').trim().toLowerCase();
|
|
54
73
|
if (role === 'owner' || role === 'admin' || role === 'member' || role === 'read-only')
|
|
@@ -61,6 +80,56 @@ export function registerAuthRoutes(deps) {
|
|
|
61
80
|
const stateAllows = state === 'active' || state === 'trialing' || state === 'grace';
|
|
62
81
|
return teamAllowed && stateAllows ? 'team' : 'personal';
|
|
63
82
|
};
|
|
83
|
+
const serializeAiProfilePublicRecord = (profile) => ({
|
|
84
|
+
id: profile.id,
|
|
85
|
+
workspaceId: profile.workspaceId,
|
|
86
|
+
name: profile.name,
|
|
87
|
+
username: profile.username,
|
|
88
|
+
icon: profile.icon,
|
|
89
|
+
color: profile.color,
|
|
90
|
+
avatarUrl: profile.avatarUrl ?? null,
|
|
91
|
+
avatarSourceUrl: profile.avatarSourceUrl ?? null,
|
|
92
|
+
avatarRevision: profile.avatarRevision ?? 0,
|
|
93
|
+
avatarUpdatedAt: profile.avatarUpdatedAt ?? null,
|
|
94
|
+
description: profile.description ?? null,
|
|
95
|
+
role: profile.role ?? null,
|
|
96
|
+
provider: profile.provider ?? null,
|
|
97
|
+
model: profile.model ?? null,
|
|
98
|
+
surfaceType: profile.surfaceType ?? null,
|
|
99
|
+
seatScope: profile.seatScope ?? null,
|
|
100
|
+
archivedAt: profile.archivedAt ?? null,
|
|
101
|
+
archivedBy: profile.archivedBy ?? null,
|
|
102
|
+
archivedReason: profile.archivedReason ?? null,
|
|
103
|
+
mergedIntoProfileId: profile.mergedIntoProfileId ?? null,
|
|
104
|
+
rosterStateUpdatedAt: profile.rosterStateUpdatedAt ?? null,
|
|
105
|
+
createdAt: profile.createdAt,
|
|
106
|
+
updatedAt: profile.updatedAt,
|
|
107
|
+
lastActiveAt: profile.lastActiveAt ?? null
|
|
108
|
+
});
|
|
109
|
+
const parseAiAvatarDataUrlPayload = (value, fallbackOriginalName) => {
|
|
110
|
+
if (!value || typeof value !== 'object')
|
|
111
|
+
return null;
|
|
112
|
+
const record = value;
|
|
113
|
+
const dataUrl = String(record.dataUrl || record.file || '').trim();
|
|
114
|
+
const dataMatch = dataUrl.match(/^data:([^;]+);base64,(.+)$/);
|
|
115
|
+
if (!dataMatch)
|
|
116
|
+
return null;
|
|
117
|
+
return {
|
|
118
|
+
mimeType: String(record.mimeType || dataMatch[1] || 'application/octet-stream').trim().toLowerCase(),
|
|
119
|
+
originalName: typeof record.originalName === 'string' && record.originalName.trim().length > 0
|
|
120
|
+
? record.originalName.trim()
|
|
121
|
+
: fallbackOriginalName,
|
|
122
|
+
buffer: Buffer.from(dataMatch[2], 'base64')
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
const aiProfileAvatarAssetDeps = {
|
|
126
|
+
core,
|
|
127
|
+
workspaceAssetStore,
|
|
128
|
+
getObjectStorageClient,
|
|
129
|
+
ensureWithinDir,
|
|
130
|
+
markAssetDeleted,
|
|
131
|
+
appendStorageTelemetry,
|
|
132
|
+
};
|
|
64
133
|
const resolveBillingConflictCodeForUser = (userIdRaw, gateRaw) => {
|
|
65
134
|
const userId = String(userIdRaw || '').trim();
|
|
66
135
|
const gate = String(gateRaw || '').trim().toLowerCase();
|
|
@@ -103,7 +172,7 @@ export function registerAuthRoutes(deps) {
|
|
|
103
172
|
return null;
|
|
104
173
|
};
|
|
105
174
|
const buildAccountProfileSummary = (req) => {
|
|
106
|
-
const userId =
|
|
175
|
+
const userId = resolveAuthenticatedUserId(req);
|
|
107
176
|
const workspaceId = requestWorkspaceId(req);
|
|
108
177
|
const access = core.getAccountAccessStatus(userId);
|
|
109
178
|
const resolvedAccess = core.resolveUserAccess(userId, workspaceId, 'member');
|
|
@@ -125,8 +194,12 @@ export function registerAuthRoutes(deps) {
|
|
|
125
194
|
catch {
|
|
126
195
|
billingRow = undefined;
|
|
127
196
|
}
|
|
128
|
-
const
|
|
197
|
+
const allowBillingPlanFallback = access.gate === 'plan_selection_required' || access.gate === 'checkout_pending';
|
|
198
|
+
const planId = String(access.entitlement?.planId
|
|
199
|
+
|| (allowBillingPlanFallback ? billingRow?.tier : '')
|
|
200
|
+
|| '').trim() || null;
|
|
129
201
|
const planVersionId = String(access.entitlement?.planVersionId || '').trim() || null;
|
|
202
|
+
const entitlementState = access.entitlement?.state || null;
|
|
130
203
|
let planRow;
|
|
131
204
|
if (planId) {
|
|
132
205
|
try {
|
|
@@ -151,6 +224,7 @@ export function registerAuthRoutes(deps) {
|
|
|
151
224
|
teamPlanMode = 'personal';
|
|
152
225
|
}
|
|
153
226
|
}
|
|
227
|
+
const aiProfileSeatUsage = core.countAiProfileUsage(workspaceId);
|
|
154
228
|
const billingConflictCode = resolveBillingConflictCodeForUser(userId, access.gate);
|
|
155
229
|
return {
|
|
156
230
|
stripeStatus: billingRow?.stripe_status || null,
|
|
@@ -161,7 +235,7 @@ export function registerAuthRoutes(deps) {
|
|
|
161
235
|
trialEnd: billingRow?.trial_end || null,
|
|
162
236
|
currentPeriodEnd: billingRow?.current_period_end || null,
|
|
163
237
|
effectiveUntil: billingRow?.effective_until || null,
|
|
164
|
-
entitlementState
|
|
238
|
+
entitlementState,
|
|
165
239
|
planVersionId,
|
|
166
240
|
planId,
|
|
167
241
|
planName: planRow?.display_name ? String(planRow.display_name) : planId,
|
|
@@ -173,7 +247,8 @@ export function registerAuthRoutes(deps) {
|
|
|
173
247
|
workspaceRole,
|
|
174
248
|
canManageWorkspace,
|
|
175
249
|
teamPlanMode,
|
|
176
|
-
teamManagementAllowed: canManageWorkspace && teamPlanMode === 'team'
|
|
250
|
+
teamManagementAllowed: canManageWorkspace && teamPlanMode === 'team',
|
|
251
|
+
aiProfileSeatUsage
|
|
177
252
|
};
|
|
178
253
|
};
|
|
179
254
|
// === SECTION: Email delivery helpers ===
|
|
@@ -187,7 +262,7 @@ export function registerAuthRoutes(deps) {
|
|
|
187
262
|
const access = resolvedWorkspaceId
|
|
188
263
|
? core.resolveUserAccess(userId, resolvedWorkspaceId, fallbackRole)
|
|
189
264
|
: null;
|
|
190
|
-
return createSessionTokenCookie(`
|
|
265
|
+
return createSessionTokenCookie(`usr:${userId}`, authConfig, {
|
|
191
266
|
role: (access?.role || fallbackRole),
|
|
192
267
|
workspaceId: access?.workspaceId || '',
|
|
193
268
|
userId,
|
|
@@ -241,6 +316,29 @@ export function registerAuthRoutes(deps) {
|
|
|
241
316
|
await deleteStoredAvatarObject(draft.storageKey);
|
|
242
317
|
}
|
|
243
318
|
};
|
|
319
|
+
const parseBinaryBody = async (req, maxBytes) => {
|
|
320
|
+
const safeMaxBytes = Number.isFinite(maxBytes) && maxBytes > 0
|
|
321
|
+
? Math.floor(maxBytes)
|
|
322
|
+
: MAX_AVATAR_UPLOAD_BYTES;
|
|
323
|
+
return new Promise((resolve, reject) => {
|
|
324
|
+
const chunks = [];
|
|
325
|
+
let receivedBytes = 0;
|
|
326
|
+
req.on('data', (chunk) => {
|
|
327
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
328
|
+
receivedBytes += buffer.length;
|
|
329
|
+
if (receivedBytes > safeMaxBytes) {
|
|
330
|
+
req.pause();
|
|
331
|
+
reject(Object.assign(new Error(`Payload too large (max ${safeMaxBytes} bytes)`), { statusCode: 413 }));
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
chunks.push(buffer);
|
|
335
|
+
});
|
|
336
|
+
req.on('end', () => {
|
|
337
|
+
resolve(Buffer.concat(chunks));
|
|
338
|
+
});
|
|
339
|
+
req.on('error', reject);
|
|
340
|
+
});
|
|
341
|
+
};
|
|
244
342
|
const logAuthLink = (kind, email, token, req) => {
|
|
245
343
|
const base = resolvePublicAuthBaseUrl(req);
|
|
246
344
|
const urlPath = kind === 'verify-email'
|
|
@@ -339,11 +437,650 @@ export function registerAuthRoutes(deps) {
|
|
|
339
437
|
});
|
|
340
438
|
res.end(`window.__TASKFORCE_SITE_CONFIG__ = ${payload};`);
|
|
341
439
|
});
|
|
440
|
+
// GET /api/taskforce/auth/providers - Public: list enabled OAuth login providers (no secrets)
|
|
441
|
+
addRoute('GET', '/api/taskforce/auth/providers', async (_req, res) => {
|
|
442
|
+
const providers = core.getGlobalSettings().oauthProviders || {};
|
|
443
|
+
const enabled = [];
|
|
444
|
+
if (providers.google?.enabled === true)
|
|
445
|
+
enabled.push('google');
|
|
446
|
+
if (providers.github?.enabled === true)
|
|
447
|
+
enabled.push('github');
|
|
448
|
+
if (providers.apple?.enabled === true)
|
|
449
|
+
enabled.push('apple');
|
|
450
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
451
|
+
res.end(JSON.stringify({ providers: enabled }));
|
|
452
|
+
});
|
|
453
|
+
// === SECTION: OAuth 2.0 / OpenID Connect ===
|
|
454
|
+
const OAUTH_RETURN_TO_PATTERN = /^\/[A-Za-z0-9_\-./?=&%]*$/;
|
|
455
|
+
/**
|
|
456
|
+
* Validates a `return_to` value and defaults to '/' on failure.
|
|
457
|
+
* Prevents open-redirect attacks by only allowing same-site relative paths.
|
|
458
|
+
*/
|
|
459
|
+
const sanitizeReturnTo = (raw) => {
|
|
460
|
+
const value = String(raw || '').trim();
|
|
461
|
+
if (!value || !OAUTH_RETURN_TO_PATTERN.test(value))
|
|
462
|
+
return '/';
|
|
463
|
+
return value;
|
|
464
|
+
};
|
|
465
|
+
/**
|
|
466
|
+
* Provider-agnostic session cookie builder for OAuth-authenticated users.
|
|
467
|
+
* Resolves workspace/role and mints the signed session token.
|
|
468
|
+
*/
|
|
469
|
+
const buildOAuthSessionCookie = (req, authConfig, userId, identityId) => {
|
|
470
|
+
const resolvedWorkspaceId = core.resolvePreferredUserWorkspaceId(userId, {
|
|
471
|
+
preferredWorkspaceId: null,
|
|
472
|
+
persistedWorkspaceId: null
|
|
473
|
+
});
|
|
474
|
+
const fallbackRole = String(authConfig.defaultRole || 'member').trim() || 'member';
|
|
475
|
+
const access = resolvedWorkspaceId
|
|
476
|
+
? core.resolveUserAccess(userId, resolvedWorkspaceId, fallbackRole)
|
|
477
|
+
: null;
|
|
478
|
+
return createSessionTokenCookie(`usr:${userId}`, authConfig, {
|
|
479
|
+
role: (access?.role || fallbackRole),
|
|
480
|
+
workspaceId: access?.workspaceId || '',
|
|
481
|
+
userId,
|
|
482
|
+
identityId,
|
|
483
|
+
...resolveSessionCookieAttributesForRequest(req, authConfig),
|
|
484
|
+
});
|
|
485
|
+
};
|
|
486
|
+
/**
|
|
487
|
+
* Handles the post-exchange OAuth profile: find or create the user,
|
|
488
|
+
* link the provider identity if needed, and return { userId, identityId }.
|
|
489
|
+
*
|
|
490
|
+
* Resolution order:
|
|
491
|
+
* 1. Existing identity matching (provider, providerSubject) → login.
|
|
492
|
+
* 2. Existing user with same email (provider must have verified it) → link + login.
|
|
493
|
+
* 3. Self-registration allowed → register new user → login.
|
|
494
|
+
* 4. Otherwise → reject.
|
|
495
|
+
*/
|
|
496
|
+
const resolveOAuthUser = (profile) => {
|
|
497
|
+
// Case 1: known identity
|
|
498
|
+
const found = core.findUserByOAuthProvider(profile.provider, profile.providerSubject);
|
|
499
|
+
if (found) {
|
|
500
|
+
// Update last_login_at snapshot
|
|
501
|
+
try {
|
|
502
|
+
const db = core.getDatabaseAdapter();
|
|
503
|
+
const tenantId = core.getTenantId();
|
|
504
|
+
db.prepare(`
|
|
505
|
+
UPDATE user_auth_identities
|
|
506
|
+
SET last_login_at = ?, avatar_url_snapshot = COALESCE(?, avatar_url_snapshot),
|
|
507
|
+
updated_at = ?
|
|
508
|
+
WHERE tenant_id = ? AND id = ?
|
|
509
|
+
`).run(new Date().toISOString(), profile.avatarUrl ?? null, new Date().toISOString(), tenantId, found.identityId);
|
|
510
|
+
}
|
|
511
|
+
catch { /* non-fatal */ }
|
|
512
|
+
return { userId: found.userId, identityId: found.identityId, isNew: false };
|
|
513
|
+
}
|
|
514
|
+
// Case 2: email match (provider must have verified it)
|
|
515
|
+
if (profile.email && profile.emailVerified) {
|
|
516
|
+
const byEmail = core.findUserByEmail(profile.email);
|
|
517
|
+
if (byEmail) {
|
|
518
|
+
try {
|
|
519
|
+
const identityId = core.linkOAuthProviderToUser({
|
|
520
|
+
userId: byEmail.userId,
|
|
521
|
+
provider: profile.provider,
|
|
522
|
+
providerSubject: profile.providerSubject,
|
|
523
|
+
providerEmail: profile.email,
|
|
524
|
+
providerEmailVerified: profile.emailVerified,
|
|
525
|
+
displayNameSnapshot: profile.displayName,
|
|
526
|
+
avatarUrlSnapshot: profile.avatarUrl,
|
|
527
|
+
});
|
|
528
|
+
return { userId: byEmail.userId, identityId, isNew: false };
|
|
529
|
+
}
|
|
530
|
+
catch (e) {
|
|
531
|
+
if (e?.code !== 'IDENTITY_ALREADY_EXISTS')
|
|
532
|
+
throw e;
|
|
533
|
+
// Race condition: already linked — re-query
|
|
534
|
+
const refound = core.findUserByOAuthProvider(profile.provider, profile.providerSubject);
|
|
535
|
+
if (refound)
|
|
536
|
+
return { ...refound, isNew: false };
|
|
537
|
+
throw e;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
// Case 3: self-registration
|
|
542
|
+
const authBehavior = buildAuthFlowBehavior(core);
|
|
543
|
+
if (!authBehavior.allowSelfRegistration) {
|
|
544
|
+
throw Object.assign(new Error('Self-registration is currently disabled.'), {
|
|
545
|
+
code: 'REGISTRATION_DISABLED',
|
|
546
|
+
statusCode: 403
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
const created = core.registerOAuthUser({
|
|
550
|
+
email: profile.email,
|
|
551
|
+
emailVerified: profile.emailVerified,
|
|
552
|
+
provider: profile.provider,
|
|
553
|
+
providerSubject: profile.providerSubject,
|
|
554
|
+
displayName: profile.displayName,
|
|
555
|
+
avatarUrl: profile.avatarUrl,
|
|
556
|
+
betaAccess: authBehavior.defaultNewUserBetaAccess,
|
|
557
|
+
});
|
|
558
|
+
return { userId: created.userId, identityId: created.identityId, isNew: true };
|
|
559
|
+
};
|
|
560
|
+
// GET /api/taskforce/auth/oauth/:provider/start
|
|
561
|
+
// Initiates the OAuth authorisation redirect for Google, GitHub, or Apple.
|
|
562
|
+
addRoute('GET', '/api/taskforce/auth/oauth/:provider/start', async (req, res, params) => {
|
|
563
|
+
if (!requireCloudRuntime(res, 'OAuth Login'))
|
|
564
|
+
return;
|
|
565
|
+
const authConfig = context.authConfig;
|
|
566
|
+
if (!authConfig?.enabled) {
|
|
567
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
568
|
+
res.end(JSON.stringify({ success: false, error: 'Auth is not enabled.' }));
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
const provider = String(params.provider || '').toLowerCase();
|
|
572
|
+
if (provider !== 'google' && provider !== 'github' && provider !== 'apple') {
|
|
573
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
574
|
+
res.end(JSON.stringify({ success: false, error: 'Unknown OAuth provider.' }));
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
const oauthProviders = core.getGlobalSettings().oauthProviders || {};
|
|
578
|
+
const providerConfig = oauthProviders[provider];
|
|
579
|
+
if (!providerConfig?.enabled) {
|
|
580
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
581
|
+
res.end(JSON.stringify({ success: false, error: `${provider} login is not enabled.` }));
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
const queryString = (req.url || '').split('?')[1] || '';
|
|
585
|
+
const queryParams = new URLSearchParams(queryString);
|
|
586
|
+
const returnTo = sanitizeReturnTo(queryParams.get('return_to'));
|
|
587
|
+
const clientIp = requestHeader(req, 'x-forwarded-for', '') || requestHeader(req, 'x-real-ip', '') || null;
|
|
588
|
+
const rawInviteToken = String(queryParams.get('invite_token') || '').trim();
|
|
589
|
+
const rawFlowHint = String(queryParams.get('flow') || '').trim().toLowerCase();
|
|
590
|
+
// Determine flow mode: link > invite_claim > login
|
|
591
|
+
let flowMode = 'login';
|
|
592
|
+
let validatedInviteToken = null;
|
|
593
|
+
let linkUserId = null;
|
|
594
|
+
if (rawFlowHint === 'link') {
|
|
595
|
+
// Provider-link flow: require an authenticated session
|
|
596
|
+
if (!authConfig || !hasValidSession(req, authConfig)) {
|
|
597
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
598
|
+
res.end(JSON.stringify({ success: false, error: 'Sign in required to link a provider.', code: 'AUTH_REQUIRED' }));
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
const access = resolveRequestAccess(req, authConfig);
|
|
602
|
+
linkUserId = access?.userId || null;
|
|
603
|
+
if (!linkUserId) {
|
|
604
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
605
|
+
res.end(JSON.stringify({ success: false, error: 'Unable to resolve authenticated user.', code: 'AUTH_REQUIRED' }));
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
flowMode = 'link';
|
|
609
|
+
}
|
|
610
|
+
else if (rawInviteToken) {
|
|
611
|
+
// Invite-claim flow
|
|
612
|
+
const inviteDetails = core.inspectInviteAcceptance(rawInviteToken);
|
|
613
|
+
if (inviteDetails.state !== 'valid') {
|
|
614
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
615
|
+
res.end(JSON.stringify({ success: false, error: 'Invite token is invalid or expired.', code: 'INVITE_TOKEN_INVALID' }));
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if (inviteDetails.inviteeState !== 'pending_setup') {
|
|
619
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
620
|
+
res.end(JSON.stringify({ success: false, error: 'This invite has already been claimed.', code: 'INVITE_ALREADY_CLAIMED' }));
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
flowMode = 'invite_claim';
|
|
624
|
+
validatedInviteToken = rawInviteToken;
|
|
625
|
+
}
|
|
626
|
+
const { state, codeChallenge, nonce } = core.createOAuthLoginState({
|
|
627
|
+
provider,
|
|
628
|
+
flowMode,
|
|
629
|
+
userId: linkUserId,
|
|
630
|
+
returnTo,
|
|
631
|
+
inviteToken: validatedInviteToken,
|
|
632
|
+
createdIp: clientIp,
|
|
633
|
+
});
|
|
634
|
+
const baseUrl = resolvePublicAuthBaseUrl(req);
|
|
635
|
+
const redirectUri = `${baseUrl}/api/taskforce/auth/oauth/${provider}/callback`;
|
|
636
|
+
let authUrl;
|
|
637
|
+
if (provider === 'google') {
|
|
638
|
+
const cfg = providerConfig;
|
|
639
|
+
if (!cfg.clientId) {
|
|
640
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
641
|
+
res.end(JSON.stringify({ success: false, error: 'Google OAuth is not fully configured.' }));
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
authUrl = buildGoogleAuthUrl({
|
|
645
|
+
clientId: cfg.clientId,
|
|
646
|
+
redirectUri,
|
|
647
|
+
state,
|
|
648
|
+
codeChallenge,
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
else if (provider === 'github') {
|
|
652
|
+
const cfg = providerConfig;
|
|
653
|
+
if (!cfg.clientId) {
|
|
654
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
655
|
+
res.end(JSON.stringify({ success: false, error: 'GitHub OAuth is not fully configured.' }));
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
authUrl = buildGithubAuthUrl({
|
|
659
|
+
clientId: cfg.clientId,
|
|
660
|
+
redirectUri,
|
|
661
|
+
state,
|
|
662
|
+
codeChallenge,
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
// Apple
|
|
667
|
+
const cfg = providerConfig;
|
|
668
|
+
if (!cfg.servicesId) {
|
|
669
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
670
|
+
res.end(JSON.stringify({ success: false, error: 'Apple Sign In is not fully configured.' }));
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
authUrl = buildAppleAuthUrl({
|
|
674
|
+
servicesId: cfg.servicesId,
|
|
675
|
+
redirectUri,
|
|
676
|
+
state,
|
|
677
|
+
nonce,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
res.writeHead(302, { Location: authUrl });
|
|
681
|
+
res.end();
|
|
682
|
+
});
|
|
683
|
+
// GET /api/taskforce/auth/oauth/:provider/callback
|
|
684
|
+
// Handles the OAuth callback for Google and GitHub (GET redirect with `code` + `state`).
|
|
685
|
+
addRoute('GET', '/api/taskforce/auth/oauth/:provider/callback', async (req, res, params) => {
|
|
686
|
+
if (!requireCloudRuntime(res, 'OAuth Login'))
|
|
687
|
+
return;
|
|
688
|
+
const authConfig = context.authConfig;
|
|
689
|
+
if (!authConfig?.enabled) {
|
|
690
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
691
|
+
res.end(JSON.stringify({ success: false, error: 'Auth is not enabled.' }));
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
const provider = String(params.provider || '').toLowerCase();
|
|
695
|
+
if (provider !== 'google' && provider !== 'github') {
|
|
696
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
697
|
+
res.end(JSON.stringify({ success: false, error: 'Unknown OAuth provider.' }));
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
const queryString = (req.url || '').split('?')[1] || '';
|
|
701
|
+
const queryParams = new URLSearchParams(queryString);
|
|
702
|
+
const stateParam = String(queryParams.get('state') || '').trim();
|
|
703
|
+
const codeParam = String(queryParams.get('code') || '').trim();
|
|
704
|
+
const errorParam = String(queryParams.get('error') || '').trim();
|
|
705
|
+
const baseUrl = resolvePublicAuthBaseUrl(req);
|
|
706
|
+
const redirectUri = `${baseUrl}/api/taskforce/auth/oauth/${provider}/callback`;
|
|
707
|
+
if (errorParam) {
|
|
708
|
+
auditAuthAction(req, 'auth-oauth-callback-denied', { provider, error: errorParam });
|
|
709
|
+
res.writeHead(302, { Location: `/login?error=oauth_denied&provider=${encodeURIComponent(provider)}` });
|
|
710
|
+
res.end();
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
if (!stateParam || !codeParam) {
|
|
714
|
+
res.writeHead(302, { Location: `/login?error=oauth_invalid&provider=${encodeURIComponent(provider)}` });
|
|
715
|
+
res.end();
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
// Consume state (single-use, validates expiry)
|
|
719
|
+
const stateRecord = core.consumeOAuthLoginState(stateParam);
|
|
720
|
+
if (!stateRecord) {
|
|
721
|
+
auditAuthAction(req, 'auth-oauth-state-invalid', { provider, stateProvided: true });
|
|
722
|
+
res.writeHead(302, { Location: `/login?error=oauth_state_invalid&provider=${encodeURIComponent(provider)}` });
|
|
723
|
+
res.end();
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
if (stateRecord.provider !== provider) {
|
|
727
|
+
auditAuthAction(req, 'auth-oauth-state-provider-mismatch', { provider, stateProvider: stateRecord.provider });
|
|
728
|
+
res.writeHead(302, { Location: `/login?error=oauth_state_invalid&provider=${encodeURIComponent(provider)}` });
|
|
729
|
+
res.end();
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
const codeVerifier = stateRecord.pkceVerifier || '';
|
|
733
|
+
const returnTo = sanitizeReturnTo(stateRecord.returnTo);
|
|
734
|
+
const oauthProviders = core.getGlobalSettings().oauthProviders || {};
|
|
735
|
+
try {
|
|
736
|
+
let profile;
|
|
737
|
+
if (provider === 'google') {
|
|
738
|
+
const cfg = (oauthProviders.google || {});
|
|
739
|
+
if (!cfg.clientId || !cfg.clientSecret) {
|
|
740
|
+
throw Object.assign(new Error('Google OAuth is not configured.'), { statusCode: 500 });
|
|
741
|
+
}
|
|
742
|
+
profile = await exchangeGoogleCode({
|
|
743
|
+
code: codeParam,
|
|
744
|
+
codeVerifier,
|
|
745
|
+
redirectUri,
|
|
746
|
+
clientId: cfg.clientId,
|
|
747
|
+
clientSecret: cfg.clientSecret,
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
else {
|
|
751
|
+
const cfg = (oauthProviders.github || {});
|
|
752
|
+
if (!cfg.clientId || !cfg.clientSecret) {
|
|
753
|
+
throw Object.assign(new Error('GitHub OAuth is not configured.'), { statusCode: 500 });
|
|
754
|
+
}
|
|
755
|
+
profile = await exchangeGithubCode({
|
|
756
|
+
code: codeParam,
|
|
757
|
+
codeVerifier,
|
|
758
|
+
redirectUri,
|
|
759
|
+
clientId: cfg.clientId,
|
|
760
|
+
clientSecret: cfg.clientSecret,
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
let userId;
|
|
764
|
+
let identityId;
|
|
765
|
+
let isNew = false;
|
|
766
|
+
if (stateRecord.flowMode === 'link') {
|
|
767
|
+
if (!stateRecord.userId) {
|
|
768
|
+
throw Object.assign(new Error('User ID missing from OAuth link state.'), { code: 'AUTH_REQUIRED', statusCode: 401 });
|
|
769
|
+
}
|
|
770
|
+
identityId = core.linkOAuthProviderToUser({
|
|
771
|
+
userId: stateRecord.userId,
|
|
772
|
+
provider: profile.provider,
|
|
773
|
+
providerSubject: profile.providerSubject,
|
|
774
|
+
providerEmail: profile.email,
|
|
775
|
+
providerEmailVerified: profile.emailVerified,
|
|
776
|
+
displayNameSnapshot: profile.displayName,
|
|
777
|
+
avatarUrlSnapshot: profile.avatarUrl,
|
|
778
|
+
});
|
|
779
|
+
userId = stateRecord.userId;
|
|
780
|
+
isNew = false;
|
|
781
|
+
}
|
|
782
|
+
else if (stateRecord.flowMode === 'invite_claim') {
|
|
783
|
+
if (!stateRecord.inviteToken) {
|
|
784
|
+
throw Object.assign(new Error('Invite token missing from OAuth state.'), { code: 'INVITE_TOKEN_MISSING', statusCode: 400 });
|
|
785
|
+
}
|
|
786
|
+
const claimed = core.claimInviteWithOAuthProvider({
|
|
787
|
+
token: stateRecord.inviteToken,
|
|
788
|
+
provider: profile.provider,
|
|
789
|
+
providerSubject: profile.providerSubject,
|
|
790
|
+
providerEmail: profile.email,
|
|
791
|
+
providerEmailVerified: profile.emailVerified,
|
|
792
|
+
displayNameSnapshot: profile.displayName,
|
|
793
|
+
avatarUrlSnapshot: profile.avatarUrl,
|
|
794
|
+
});
|
|
795
|
+
userId = claimed.userId;
|
|
796
|
+
identityId = claimed.identityId;
|
|
797
|
+
isNew = true;
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
({ userId, identityId, isNew } = resolveOAuthUser(profile));
|
|
801
|
+
}
|
|
802
|
+
const cookie = buildOAuthSessionCookie(req, authConfig, userId, identityId);
|
|
803
|
+
res.setHeader('Set-Cookie', cookie);
|
|
804
|
+
auditAuthAction(req, 'auth-oauth-login-success', {
|
|
805
|
+
provider,
|
|
806
|
+
userId,
|
|
807
|
+
isNew,
|
|
808
|
+
flowMode: stateRecord.flowMode,
|
|
809
|
+
email: profile.email || null,
|
|
810
|
+
});
|
|
811
|
+
res.writeHead(302, { Location: returnTo });
|
|
812
|
+
res.end();
|
|
813
|
+
}
|
|
814
|
+
catch (err) {
|
|
815
|
+
const code = err?.code || 'OAUTH_ERROR';
|
|
816
|
+
auditAuthAction(req, 'auth-oauth-callback-error', { provider, code, message: String(err?.message || '') });
|
|
817
|
+
if (code === 'REGISTRATION_DISABLED') {
|
|
818
|
+
res.writeHead(302, { Location: `/login?error=registration_disabled&provider=${encodeURIComponent(provider)}` });
|
|
819
|
+
}
|
|
820
|
+
else if (code === 'IDENTITY_ALREADY_EXISTS') {
|
|
821
|
+
res.writeHead(302, { Location: `${returnTo}?error=provider_already_linked&provider=${encodeURIComponent(provider)}` });
|
|
822
|
+
}
|
|
823
|
+
else if (code === 'INVITE_TOKEN_EXPIRED' || code === 'INVITE_TOKEN_REVOKED' || code === 'INVITE_TOKEN_USED' || code === 'INVITE_TOKEN_INVALID') {
|
|
824
|
+
res.writeHead(302, { Location: `/login?error=invite_invalid&provider=${encodeURIComponent(provider)}` });
|
|
825
|
+
}
|
|
826
|
+
else {
|
|
827
|
+
res.writeHead(302, { Location: `/login?error=oauth_failed&provider=${encodeURIComponent(provider)}` });
|
|
828
|
+
}
|
|
829
|
+
res.end();
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
// POST /api/taskforce/auth/oauth/apple/callback
|
|
833
|
+
// Apple Sign In sends `code`, `state`, `id_token` and optional `user` JSON via form_post.
|
|
834
|
+
addRoute('POST', '/api/taskforce/auth/oauth/apple/callback', async (req, res) => {
|
|
835
|
+
if (!requireCloudRuntime(res, 'Apple Sign In'))
|
|
836
|
+
return;
|
|
837
|
+
const authConfig = context.authConfig;
|
|
838
|
+
if (!authConfig?.enabled) {
|
|
839
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
840
|
+
res.end(JSON.stringify({ success: false, error: 'Auth is not enabled.' }));
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
let formData;
|
|
844
|
+
try {
|
|
845
|
+
formData = await parseFormBody(req);
|
|
846
|
+
}
|
|
847
|
+
catch {
|
|
848
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
849
|
+
res.end(JSON.stringify({ success: false, error: 'Invalid form body.' }));
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
const stateParam = String(formData.state || '').trim();
|
|
853
|
+
const codeParam = String(formData.code || '').trim();
|
|
854
|
+
const errorParam = String(formData.error || '').trim();
|
|
855
|
+
const userJson = formData.user ? String(formData.user).trim() : null;
|
|
856
|
+
if (errorParam) {
|
|
857
|
+
auditAuthAction(req, 'auth-oauth-callback-denied', { provider: 'apple', error: errorParam });
|
|
858
|
+
res.writeHead(302, { Location: '/login?error=oauth_denied&provider=apple' });
|
|
859
|
+
res.end();
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
if (!stateParam || !codeParam) {
|
|
863
|
+
res.writeHead(302, { Location: '/login?error=oauth_invalid&provider=apple' });
|
|
864
|
+
res.end();
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
const stateRecord = core.consumeOAuthLoginState(stateParam);
|
|
868
|
+
if (!stateRecord || stateRecord.provider !== 'apple') {
|
|
869
|
+
auditAuthAction(req, 'auth-oauth-state-invalid', { provider: 'apple', stateProvided: true });
|
|
870
|
+
res.writeHead(302, { Location: '/login?error=oauth_state_invalid&provider=apple' });
|
|
871
|
+
res.end();
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
const returnTo = sanitizeReturnTo(stateRecord.returnTo);
|
|
875
|
+
const oauthProviders = core.getGlobalSettings().oauthProviders || {};
|
|
876
|
+
const appleCfg = (oauthProviders.apple || {});
|
|
877
|
+
if (!appleCfg.servicesId || !appleCfg.teamId || !appleCfg.keyId || !appleCfg.privateKeyPem) {
|
|
878
|
+
auditAuthAction(req, 'auth-oauth-callback-error', { provider: 'apple', code: 'APPLE_NOT_CONFIGURED' });
|
|
879
|
+
res.writeHead(302, { Location: '/login?error=oauth_failed&provider=apple' });
|
|
880
|
+
res.end();
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
const baseUrl = resolvePublicAuthBaseUrl(req);
|
|
884
|
+
const redirectUri = `${baseUrl}/api/taskforce/auth/oauth/apple/callback`;
|
|
885
|
+
try {
|
|
886
|
+
const profile = await exchangeAppleCode({
|
|
887
|
+
code: codeParam,
|
|
888
|
+
redirectUri,
|
|
889
|
+
servicesId: appleCfg.servicesId,
|
|
890
|
+
teamId: appleCfg.teamId,
|
|
891
|
+
keyId: appleCfg.keyId,
|
|
892
|
+
privateKeyPem: appleCfg.privateKeyPem,
|
|
893
|
+
userJson,
|
|
894
|
+
});
|
|
895
|
+
let userId;
|
|
896
|
+
let identityId;
|
|
897
|
+
let isNew = false;
|
|
898
|
+
if (stateRecord.flowMode === 'link') {
|
|
899
|
+
if (!stateRecord.userId) {
|
|
900
|
+
throw Object.assign(new Error('User ID missing from OAuth link state.'), { code: 'AUTH_REQUIRED', statusCode: 401 });
|
|
901
|
+
}
|
|
902
|
+
identityId = core.linkOAuthProviderToUser({
|
|
903
|
+
userId: stateRecord.userId,
|
|
904
|
+
provider: 'apple',
|
|
905
|
+
providerSubject: profile.providerSubject,
|
|
906
|
+
providerEmail: profile.email,
|
|
907
|
+
providerEmailVerified: profile.emailVerified,
|
|
908
|
+
displayNameSnapshot: profile.displayName,
|
|
909
|
+
avatarUrlSnapshot: profile.avatarUrl,
|
|
910
|
+
});
|
|
911
|
+
userId = stateRecord.userId;
|
|
912
|
+
isNew = false;
|
|
913
|
+
}
|
|
914
|
+
else if (stateRecord.flowMode === 'invite_claim') {
|
|
915
|
+
if (!stateRecord.inviteToken) {
|
|
916
|
+
throw Object.assign(new Error('Invite token missing from OAuth state.'), { code: 'INVITE_TOKEN_MISSING', statusCode: 400 });
|
|
917
|
+
}
|
|
918
|
+
const claimed = core.claimInviteWithOAuthProvider({
|
|
919
|
+
token: stateRecord.inviteToken,
|
|
920
|
+
provider: 'apple',
|
|
921
|
+
providerSubject: profile.providerSubject,
|
|
922
|
+
providerEmail: profile.email,
|
|
923
|
+
providerEmailVerified: profile.emailVerified,
|
|
924
|
+
displayNameSnapshot: profile.displayName,
|
|
925
|
+
avatarUrlSnapshot: profile.avatarUrl,
|
|
926
|
+
});
|
|
927
|
+
userId = claimed.userId;
|
|
928
|
+
identityId = claimed.identityId;
|
|
929
|
+
isNew = true;
|
|
930
|
+
}
|
|
931
|
+
else {
|
|
932
|
+
({ userId, identityId, isNew } = resolveOAuthUser(profile));
|
|
933
|
+
}
|
|
934
|
+
const cookie = buildOAuthSessionCookie(req, authConfig, userId, identityId);
|
|
935
|
+
res.setHeader('Set-Cookie', cookie);
|
|
936
|
+
auditAuthAction(req, 'auth-oauth-login-success', {
|
|
937
|
+
provider: 'apple',
|
|
938
|
+
userId,
|
|
939
|
+
isNew,
|
|
940
|
+
flowMode: stateRecord.flowMode,
|
|
941
|
+
email: profile.email || null,
|
|
942
|
+
});
|
|
943
|
+
res.writeHead(302, { Location: returnTo });
|
|
944
|
+
res.end();
|
|
945
|
+
}
|
|
946
|
+
catch (err) {
|
|
947
|
+
const code = err?.code || 'OAUTH_ERROR';
|
|
948
|
+
auditAuthAction(req, 'auth-oauth-callback-error', { provider: 'apple', code, message: String(err?.message || '') });
|
|
949
|
+
if (code === 'REGISTRATION_DISABLED') {
|
|
950
|
+
res.writeHead(302, { Location: '/login?error=registration_disabled&provider=apple' });
|
|
951
|
+
}
|
|
952
|
+
else if (code === 'IDENTITY_ALREADY_EXISTS') {
|
|
953
|
+
res.writeHead(302, { Location: `${returnTo}?error=provider_already_linked&provider=apple` });
|
|
954
|
+
}
|
|
955
|
+
else if (code === 'INVITE_TOKEN_EXPIRED' || code === 'INVITE_TOKEN_REVOKED' || code === 'INVITE_TOKEN_USED' || code === 'INVITE_TOKEN_INVALID') {
|
|
956
|
+
res.writeHead(302, { Location: '/login?error=invite_invalid&provider=apple' });
|
|
957
|
+
}
|
|
958
|
+
else {
|
|
959
|
+
res.writeHead(302, { Location: '/login?error=oauth_failed&provider=apple' });
|
|
960
|
+
}
|
|
961
|
+
res.end();
|
|
962
|
+
}
|
|
963
|
+
});
|
|
964
|
+
// GET /api/taskforce/account/login-methods
|
|
965
|
+
// Returns the list of active login identities for the authenticated user.
|
|
966
|
+
addRoute('GET', '/api/taskforce/account/login-methods', async (req, res) => {
|
|
967
|
+
if (!requireCloudRuntime(res, 'Login methods'))
|
|
968
|
+
return;
|
|
969
|
+
const authConfig = context.authConfig;
|
|
970
|
+
if (!authConfig?.enabled || !hasValidSession(req, authConfig)) {
|
|
971
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
972
|
+
res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
const access = resolveRequestAccess(req, authConfig);
|
|
976
|
+
const userId = access?.userId || '';
|
|
977
|
+
if (!userId) {
|
|
978
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
979
|
+
res.end(JSON.stringify({ success: false, error: 'Unable to resolve user.' }));
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
const identities = core.listLoginMethodsForUser(userId);
|
|
983
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
984
|
+
res.end(JSON.stringify({
|
|
985
|
+
success: true,
|
|
986
|
+
methods: identities.map(id => ({
|
|
987
|
+
provider: id.provider,
|
|
988
|
+
providerEmail: id.providerEmail || null,
|
|
989
|
+
displayNameSnapshot: id.displayNameSnapshot || null,
|
|
990
|
+
avatarUrlSnapshot: id.avatarUrlSnapshot || null,
|
|
991
|
+
linkedAt: id.linkedAt || id.createdAt,
|
|
992
|
+
lastLoginAt: id.lastLoginAt || null,
|
|
993
|
+
}))
|
|
994
|
+
}));
|
|
995
|
+
});
|
|
996
|
+
// DELETE /api/taskforce/account/login-methods/:provider
|
|
997
|
+
// Unlinks a provider identity from the authenticated user.
|
|
998
|
+
// Rejects with 409 if it would be the last active login method.
|
|
999
|
+
addRoute('DELETE', '/api/taskforce/account/login-methods/:provider', async (req, res, params) => {
|
|
1000
|
+
if (!requireCloudRuntime(res, 'Login methods'))
|
|
1001
|
+
return;
|
|
1002
|
+
const authConfig = context.authConfig;
|
|
1003
|
+
if (!authConfig?.enabled || !hasValidSession(req, authConfig)) {
|
|
1004
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
1005
|
+
res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
const access = resolveRequestAccess(req, authConfig);
|
|
1009
|
+
const userId = access?.userId || '';
|
|
1010
|
+
if (!userId) {
|
|
1011
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
1012
|
+
res.end(JSON.stringify({ success: false, error: 'Unable to resolve user.' }));
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
const provider = String(params.provider || '').toLowerCase();
|
|
1016
|
+
if (!provider) {
|
|
1017
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1018
|
+
res.end(JSON.stringify({ success: false, error: 'Provider is required.' }));
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
try {
|
|
1022
|
+
core.unlinkLoginMethod(userId, provider);
|
|
1023
|
+
auditAuthAction(req, 'account-login-method-unlinked', { userId, provider });
|
|
1024
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1025
|
+
res.end(JSON.stringify({ success: true }));
|
|
1026
|
+
}
|
|
1027
|
+
catch (err) {
|
|
1028
|
+
const code = err?.code || 'UNLINK_ERROR';
|
|
1029
|
+
const status = code === 'LAST_IDENTITY_REQUIRED' ? 409 : resolveErrorStatusCode(err);
|
|
1030
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
1031
|
+
res.end(JSON.stringify({ success: false, error: err?.message || 'Unlink failed.', code }));
|
|
1032
|
+
}
|
|
1033
|
+
});
|
|
1034
|
+
// POST /api/taskforce/account/login-methods/password
|
|
1035
|
+
// Adds (or replaces) a password identity for the authenticated user.
|
|
1036
|
+
// Social-only accounts use this to add a password login method.
|
|
1037
|
+
addRoute('POST', '/api/taskforce/account/login-methods/password', async (req, res) => {
|
|
1038
|
+
if (!requireCloudRuntime(res, 'Login methods'))
|
|
1039
|
+
return;
|
|
1040
|
+
const authConfig = context.authConfig;
|
|
1041
|
+
if (!authConfig?.enabled || !hasValidSession(req, authConfig)) {
|
|
1042
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
1043
|
+
res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
const access = resolveRequestAccess(req, authConfig);
|
|
1047
|
+
const userId = access?.userId || '';
|
|
1048
|
+
if (!userId) {
|
|
1049
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
1050
|
+
res.end(JSON.stringify({ success: false, error: 'Unable to resolve user.' }));
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
let body;
|
|
1054
|
+
try {
|
|
1055
|
+
body = await parseJsonBody(req);
|
|
1056
|
+
}
|
|
1057
|
+
catch {
|
|
1058
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1059
|
+
res.end(JSON.stringify({ success: false, error: 'Invalid JSON body.' }));
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
const password = String(body?.password || '');
|
|
1063
|
+
try {
|
|
1064
|
+
core.addPasswordToAccount(userId, password);
|
|
1065
|
+
auditAuthAction(req, 'account-password-added', { userId });
|
|
1066
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1067
|
+
res.end(JSON.stringify({ success: true }));
|
|
1068
|
+
}
|
|
1069
|
+
catch (err) {
|
|
1070
|
+
const code = err?.code || 'ADD_PASSWORD_ERROR';
|
|
1071
|
+
const status = resolveErrorStatusCode(err);
|
|
1072
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
1073
|
+
res.end(JSON.stringify({ success: false, error: err?.message || 'Failed to add password.', code }));
|
|
1074
|
+
}
|
|
1075
|
+
});
|
|
342
1076
|
// GET /api/taskforce/auth/session - Return current auth/session state
|
|
343
1077
|
addRoute('GET', '/api/taskforce/auth/session', async (req, res) => {
|
|
1078
|
+
const routeStartedAt = getRouteTimingNow();
|
|
1079
|
+
const traceId = requestBootstrapTraceId(req);
|
|
344
1080
|
const authConfig = context.authConfig;
|
|
345
1081
|
const access = authConfig ? resolveRequestAccess(req, authConfig) : null;
|
|
346
1082
|
const authenticated = Boolean(authConfig && hasValidSession(req, authConfig));
|
|
1083
|
+
const authResolvedAt = getRouteTimingNow();
|
|
347
1084
|
let role = access?.role || authConfig?.defaultRole || 'member';
|
|
348
1085
|
let workspaceId = String(access?.workspaceId || authConfig?.defaultWorkspaceId || '').trim();
|
|
349
1086
|
let userId = access?.userId || 'anonymous';
|
|
@@ -351,11 +1088,16 @@ export function registerAuthRoutes(deps) {
|
|
|
351
1088
|
let displayName = null;
|
|
352
1089
|
let avatarUrl = null;
|
|
353
1090
|
let betaAccess = true;
|
|
1091
|
+
let betaAccessResolvedAt = authResolvedAt;
|
|
1092
|
+
let userAccessResolvedAt = authResolvedAt;
|
|
1093
|
+
let identityResolvedAt = authResolvedAt;
|
|
354
1094
|
if (authenticated && access?.userId) {
|
|
355
1095
|
// Keep the session endpoint fast on reload; avatar draft cleanup is best-effort housekeeping.
|
|
356
1096
|
void cleanupStaleAvatarDrafts(access.userId).catch(() => { });
|
|
357
1097
|
betaAccess = core.getUserBetaAccess(access.userId);
|
|
1098
|
+
betaAccessResolvedAt = getRouteTimingNow();
|
|
358
1099
|
const resolved = core.resolveUserAccess(access.userId, workspaceId, role);
|
|
1100
|
+
userAccessResolvedAt = getRouteTimingNow();
|
|
359
1101
|
if (resolved) {
|
|
360
1102
|
role = resolved.role;
|
|
361
1103
|
workspaceId = resolved.workspaceId;
|
|
@@ -370,10 +1112,12 @@ export function registerAuthRoutes(deps) {
|
|
|
370
1112
|
email = identity?.email || null;
|
|
371
1113
|
displayName = identity?.displayName || null;
|
|
372
1114
|
avatarUrl = identity?.avatarUrl || null;
|
|
1115
|
+
identityResolvedAt = getRouteTimingNow();
|
|
373
1116
|
}
|
|
374
|
-
|
|
375
|
-
res.end(JSON.stringify({
|
|
1117
|
+
const payload = {
|
|
376
1118
|
authenticated,
|
|
1119
|
+
authRequiredForApi: Boolean(authConfig?.enabled && authConfig.requiredForApi),
|
|
1120
|
+
runtimeMode: authConfig?.runtimeMode || 'local',
|
|
377
1121
|
userId,
|
|
378
1122
|
workspaceId,
|
|
379
1123
|
role,
|
|
@@ -381,7 +1125,34 @@ export function registerAuthRoutes(deps) {
|
|
|
381
1125
|
displayName,
|
|
382
1126
|
avatarUrl,
|
|
383
1127
|
betaAccess
|
|
384
|
-
}
|
|
1128
|
+
};
|
|
1129
|
+
const serialized = serializeJsonWithTiming(payload, identityResolvedAt);
|
|
1130
|
+
const totalDurationMs = getRouteTimingNow() - routeStartedAt;
|
|
1131
|
+
setServerTimingHeader(res, [
|
|
1132
|
+
{ name: 'auth-session', durationMs: totalDurationMs },
|
|
1133
|
+
{ name: 'auth-session-cookie', durationMs: authResolvedAt - routeStartedAt },
|
|
1134
|
+
{ name: 'auth-session-beta-access', durationMs: betaAccessResolvedAt - authResolvedAt },
|
|
1135
|
+
{ name: 'auth-session-user-access', durationMs: userAccessResolvedAt - betaAccessResolvedAt },
|
|
1136
|
+
{ name: 'auth-session-identity', durationMs: identityResolvedAt - userAccessResolvedAt },
|
|
1137
|
+
{ name: 'auth-session-json', durationMs: serialized.durationMs },
|
|
1138
|
+
]);
|
|
1139
|
+
logSlowBootstrapRoute({
|
|
1140
|
+
route: '/api/taskforce/auth/session',
|
|
1141
|
+
workspaceId: workspaceId || access?.workspaceId || authConfig?.defaultWorkspaceId || 'default',
|
|
1142
|
+
traceId,
|
|
1143
|
+
durationMs: totalDurationMs,
|
|
1144
|
+
details: {
|
|
1145
|
+
authenticated,
|
|
1146
|
+
payloadBytes: serialized.byteLength,
|
|
1147
|
+
cookieMs: authResolvedAt - routeStartedAt,
|
|
1148
|
+
betaAccessMs: betaAccessResolvedAt - authResolvedAt,
|
|
1149
|
+
userAccessMs: userAccessResolvedAt - betaAccessResolvedAt,
|
|
1150
|
+
identityMs: identityResolvedAt - userAccessResolvedAt,
|
|
1151
|
+
serializeMs: serialized.durationMs,
|
|
1152
|
+
}
|
|
1153
|
+
});
|
|
1154
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1155
|
+
res.end(serialized.body);
|
|
385
1156
|
});
|
|
386
1157
|
// GET /api/taskforce/auth/admin-session - Return global admin auth/session state
|
|
387
1158
|
addRoute('GET', '/api/taskforce/auth/admin-session', async (req, res) => {
|
|
@@ -537,6 +1308,63 @@ export function registerAuthRoutes(deps) {
|
|
|
537
1308
|
path: `/api/taskforce/context/${encodeURIComponent(relativePath)}`
|
|
538
1309
|
}));
|
|
539
1310
|
});
|
|
1311
|
+
addRoute('POST', '/api/taskforce/auth/profile/avatar/upload', async (req, res) => {
|
|
1312
|
+
if (!requireCloudRuntime(res, 'Authentication'))
|
|
1313
|
+
return;
|
|
1314
|
+
const authConfig = context.authConfig;
|
|
1315
|
+
if (!authConfig) {
|
|
1316
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1317
|
+
res.end(JSON.stringify({ success: false, error: 'Auth context unavailable.' }));
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
const access = resolveRequestAccess(req, authConfig);
|
|
1321
|
+
const userId = String(access.userId || '').trim();
|
|
1322
|
+
if (!access.authenticated || !userId || userId === 'anonymous') {
|
|
1323
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
1324
|
+
res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
const draftId = String(requestHeader(req, 'x-taskforce-avatar-draft-id', '') || '').trim();
|
|
1328
|
+
if (!draftId) {
|
|
1329
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1330
|
+
res.end(JSON.stringify({ success: false, error: 'Avatar draft id is required.', code: 'AVATAR_DRAFT_REQUIRED' }));
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
await cleanupStaleAvatarDrafts(userId);
|
|
1334
|
+
const draft = core.getUserAvatarUploadDraft(userId, draftId);
|
|
1335
|
+
if (!draft || draft.discardedAt) {
|
|
1336
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1337
|
+
res.end(JSON.stringify({ success: false, error: 'Avatar draft is invalid or expired.', code: 'AVATAR_DRAFT_INVALID' }));
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
const objectStorageClient = getObjectStorageClient();
|
|
1341
|
+
const effectiveObjectStorage = resolveEffectiveObjectStorageConfig();
|
|
1342
|
+
if (!objectStorageClient || effectiveObjectStorage.provider !== 'r2') {
|
|
1343
|
+
res.writeHead(501, { 'Content-Type': 'application/json' });
|
|
1344
|
+
res.end(JSON.stringify({ success: false, error: 'Avatar uploads require object storage.' }));
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
try {
|
|
1348
|
+
const body = await parseBinaryBody(req, MAX_AVATAR_UPLOAD_BYTES);
|
|
1349
|
+
if (body.length === 0) {
|
|
1350
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1351
|
+
res.end(JSON.stringify({ success: false, error: 'Avatar upload body is empty.', code: 'AVATAR_UPLOAD_EMPTY' }));
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
await objectStorageClient.putObject(draft.storageKey, body, draft.mimeType || 'application/octet-stream');
|
|
1355
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1356
|
+
res.end(JSON.stringify({ success: true, draftId, relativePath: draft.storageKey }));
|
|
1357
|
+
}
|
|
1358
|
+
catch (error) {
|
|
1359
|
+
const statusCode = resolveErrorStatusCode(error, 400);
|
|
1360
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
1361
|
+
res.end(JSON.stringify({
|
|
1362
|
+
success: false,
|
|
1363
|
+
error: String(error?.message || 'Failed to upload avatar.'),
|
|
1364
|
+
code: error?.code
|
|
1365
|
+
}));
|
|
1366
|
+
}
|
|
1367
|
+
});
|
|
540
1368
|
addRoute('POST', '/api/taskforce/auth/profile/avatar/finalize', async (req, res) => {
|
|
541
1369
|
if (!requireCloudRuntime(res, 'Authentication'))
|
|
542
1370
|
return;
|
|
@@ -708,10 +1536,11 @@ export function registerAuthRoutes(deps) {
|
|
|
708
1536
|
};
|
|
709
1537
|
}
|
|
710
1538
|
}
|
|
711
|
-
const cookie = createSessionTokenCookie(`
|
|
1539
|
+
const cookie = createSessionTokenCookie(`usr:${access.userId}`, authConfig, {
|
|
712
1540
|
role: access.role,
|
|
713
1541
|
workspaceId: access.workspaceId,
|
|
714
1542
|
userId: access.userId,
|
|
1543
|
+
identityId: authResult.access?.identityId,
|
|
715
1544
|
...resolveSessionCookieAttributesForRequest(req, authConfig)
|
|
716
1545
|
});
|
|
717
1546
|
auditAuthAction(req, 'auth-login-success', {
|
|
@@ -1016,7 +1845,7 @@ export function registerAuthRoutes(deps) {
|
|
|
1016
1845
|
const password = String(body?.password || '');
|
|
1017
1846
|
if (!token || !password) {
|
|
1018
1847
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1019
|
-
res.end(JSON.stringify({ success: false, error: 'Token and password are required.' }));
|
|
1848
|
+
res.end(JSON.stringify({ success: false, error: 'Token and password are required.', code: 'TOKEN_PASSWORD_REQUIRED' }));
|
|
1020
1849
|
return;
|
|
1021
1850
|
}
|
|
1022
1851
|
try {
|
|
@@ -1041,7 +1870,7 @@ export function registerAuthRoutes(deps) {
|
|
|
1041
1870
|
statusCode: resolveErrorStatusCode(error, 400)
|
|
1042
1871
|
});
|
|
1043
1872
|
res.writeHead(resolveErrorStatusCode(error, 400), { 'Content-Type': 'application/json' });
|
|
1044
|
-
res.end(JSON.stringify({ success: false, error: String(error?.message || 'Unable to reset password') }));
|
|
1873
|
+
res.end(JSON.stringify({ success: false, error: String(error?.message || 'Unable to reset password'), code: error?.code }));
|
|
1045
1874
|
}
|
|
1046
1875
|
});
|
|
1047
1876
|
// === SECTION: Invite flows ===
|
|
@@ -1065,6 +1894,18 @@ export function registerAuthRoutes(deps) {
|
|
|
1065
1894
|
res.end(JSON.stringify({ success: false, state: details.state, error: 'Invalid or expired invite token.' }));
|
|
1066
1895
|
return;
|
|
1067
1896
|
}
|
|
1897
|
+
// Augment availableMethods with enabled OAuth providers for pending_setup invitees
|
|
1898
|
+
const oauthProviders = core.getGlobalSettings().oauthProviders || {};
|
|
1899
|
+
const enabledOAuthProviders = [];
|
|
1900
|
+
if (oauthProviders.google?.enabled)
|
|
1901
|
+
enabledOAuthProviders.push('google');
|
|
1902
|
+
if (oauthProviders.github?.enabled)
|
|
1903
|
+
enabledOAuthProviders.push('github');
|
|
1904
|
+
if (oauthProviders.apple?.enabled)
|
|
1905
|
+
enabledOAuthProviders.push('apple');
|
|
1906
|
+
const augmentedMethods = details.inviteeState === 'pending_setup'
|
|
1907
|
+
? [...(details.availableMethods || []), ...enabledOAuthProviders.filter(p => !(details.availableMethods || []).includes(p))]
|
|
1908
|
+
: (details.availableMethods || []);
|
|
1068
1909
|
auditAuthAction(req, 'auth-invite-opened', {
|
|
1069
1910
|
userId: details.userId,
|
|
1070
1911
|
email: details.email,
|
|
@@ -1078,6 +1919,8 @@ export function registerAuthRoutes(deps) {
|
|
|
1078
1919
|
state: details.state,
|
|
1079
1920
|
email: details.email,
|
|
1080
1921
|
workspaceId: details.workspaceId,
|
|
1922
|
+
inviteeState: details.inviteeState,
|
|
1923
|
+
availableMethods: augmentedMethods,
|
|
1081
1924
|
inviteeKind: details.inviteeKind,
|
|
1082
1925
|
passwordRequired: details.passwordRequired === true
|
|
1083
1926
|
}));
|
|
@@ -1107,10 +1950,11 @@ export function registerAuthRoutes(deps) {
|
|
|
1107
1950
|
}
|
|
1108
1951
|
try {
|
|
1109
1952
|
const accepted = core.acceptInviteWithToken({ token, password });
|
|
1110
|
-
const cookie = createSessionTokenCookie(`
|
|
1953
|
+
const cookie = createSessionTokenCookie(`usr:${accepted.userId}`, authConfig, {
|
|
1111
1954
|
role: accepted.role,
|
|
1112
1955
|
workspaceId: accepted.workspaceId,
|
|
1113
1956
|
userId: accepted.userId,
|
|
1957
|
+
identityId: accepted.identityId,
|
|
1114
1958
|
...resolveSessionCookieAttributesForRequest(req, authConfig)
|
|
1115
1959
|
});
|
|
1116
1960
|
res.setHeader('Set-Cookie', cookie);
|
|
@@ -1163,11 +2007,12 @@ export function registerAuthRoutes(deps) {
|
|
|
1163
2007
|
}
|
|
1164
2008
|
try {
|
|
1165
2009
|
const accepted = core.joinInviteWithAuthenticatedUser({ token, userId: access.userId });
|
|
1166
|
-
const sessionId = String(access.sessionId || `
|
|
2010
|
+
const sessionId = String(access.sessionId || `usr:${accepted.userId}`).trim() || `usr:${accepted.userId}`;
|
|
1167
2011
|
const cookie = createSessionTokenCookie(sessionId, authConfig, {
|
|
1168
2012
|
role: accepted.role,
|
|
1169
2013
|
workspaceId: accepted.workspaceId,
|
|
1170
2014
|
userId: accepted.userId,
|
|
2015
|
+
identityId: access.identityId || undefined,
|
|
1171
2016
|
...resolveSessionCookieAttributesForRequest(req, authConfig)
|
|
1172
2017
|
});
|
|
1173
2018
|
res.setHeader('Set-Cookie', cookie);
|
|
@@ -1236,7 +2081,7 @@ export function registerAuthRoutes(deps) {
|
|
|
1236
2081
|
return;
|
|
1237
2082
|
}
|
|
1238
2083
|
try {
|
|
1239
|
-
const userId =
|
|
2084
|
+
const userId = resolveAuthenticatedUserId(req);
|
|
1240
2085
|
const payload = core.getAccountAccessStatus(userId);
|
|
1241
2086
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1242
2087
|
res.end(JSON.stringify(payload));
|
|
@@ -1359,6 +2204,31 @@ export function registerAuthRoutes(deps) {
|
|
|
1359
2204
|
res.end(JSON.stringify({ error: String(error?.message || 'Unable to build plan preview'), code: error?.code, details: error?.details }));
|
|
1360
2205
|
}
|
|
1361
2206
|
});
|
|
2207
|
+
// GET /api/taskforce/account/ai-profile-seat-usage - Read cloud-authoritative AI seat usage for the current workspace.
|
|
2208
|
+
addRoute('GET', '/api/taskforce/account/ai-profile-seat-usage', async (req, res) => {
|
|
2209
|
+
if (!ensureCloudPlanData(req, res))
|
|
2210
|
+
return;
|
|
2211
|
+
if (!ensureAuthenticatedUser(req)) {
|
|
2212
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
2213
|
+
res.end(JSON.stringify({ error: 'Authentication required.' }));
|
|
2214
|
+
return;
|
|
2215
|
+
}
|
|
2216
|
+
try {
|
|
2217
|
+
const wid = requestWorkspaceId(req);
|
|
2218
|
+
const aiProfileSeatUsage = core.countAiProfileUsage(wid);
|
|
2219
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2220
|
+
res.end(JSON.stringify({ aiProfileSeatUsage }));
|
|
2221
|
+
}
|
|
2222
|
+
catch (error) {
|
|
2223
|
+
const statusCode = resolveErrorStatusCode(error, 400);
|
|
2224
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
2225
|
+
res.end(JSON.stringify({
|
|
2226
|
+
error: String(error?.message || 'Unable to build AI profile seat usage'),
|
|
2227
|
+
code: error?.code,
|
|
2228
|
+
details: error?.details
|
|
2229
|
+
}));
|
|
2230
|
+
}
|
|
2231
|
+
});
|
|
1362
2232
|
// === SECTION: Workspace team management (workspace admin) ===
|
|
1363
2233
|
// GET /api/taskforce/auth/workspace-members - Auth-backed workspace member list for local runtime identity hydration.
|
|
1364
2234
|
addRoute('GET', '/api/taskforce/auth/workspace-members', async (req, res) => {
|
|
@@ -1375,6 +2245,7 @@ export function registerAuthRoutes(deps) {
|
|
|
1375
2245
|
res.end(JSON.stringify({ users }));
|
|
1376
2246
|
});
|
|
1377
2247
|
// GET /api/taskforce/workspace/assignee-options - Read active workspace humans and registered AI collaborators for task assignee selection.
|
|
2248
|
+
// Use ?kind=agent for Agent Manager roster loads that do not need human workspace members.
|
|
1378
2249
|
addRoute('GET', '/api/taskforce/workspace/assignee-options', async (req, res) => {
|
|
1379
2250
|
if (!ensureAppAccess(req, res))
|
|
1380
2251
|
return;
|
|
@@ -1383,45 +2254,191 @@ export function registerAuthRoutes(deps) {
|
|
|
1383
2254
|
res.end(JSON.stringify({ error: 'Authentication required.' }));
|
|
1384
2255
|
return;
|
|
1385
2256
|
}
|
|
2257
|
+
const startedAt = getRouteTimingNow();
|
|
1386
2258
|
const wid = requestWorkspaceId(req);
|
|
1387
|
-
const
|
|
1388
|
-
const
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
2259
|
+
const traceId = requestBootstrapTraceId(req);
|
|
2260
|
+
const reqUrl = new URL(req.url || '/api/taskforce/workspace/assignee-options', 'http://localhost');
|
|
2261
|
+
const kindFilter = String(reqUrl.searchParams.get('kind') || '').trim().toLowerCase();
|
|
2262
|
+
const agentsOnly = kindFilter === 'agent' || kindFilter === 'agents' || kindFilter === 'ai-profile' || kindFilter === 'ai-profiles';
|
|
2263
|
+
const usersStartedAt = getRouteTimingNow();
|
|
2264
|
+
const activeWorkspaceUsers = agentsOnly
|
|
2265
|
+
? []
|
|
2266
|
+
: core.listWorkspaceUsers(wid)
|
|
2267
|
+
.filter((user) => String(user.status || '').trim().toLowerCase() === 'active' && user.disabled !== true);
|
|
2268
|
+
const usersLoadedAt = getRouteTimingNow();
|
|
2269
|
+
const workspaceUsers = agentsOnly
|
|
2270
|
+
? []
|
|
2271
|
+
: activeWorkspaceUsers.map((user) => ({
|
|
2272
|
+
value: user.userId,
|
|
2273
|
+
label: String(user.displayName || user.email || user.userId).trim() || user.userId,
|
|
2274
|
+
icon: 'User',
|
|
2275
|
+
color: '#22c55e',
|
|
2276
|
+
kind: 'member',
|
|
2277
|
+
userId: user.userId,
|
|
2278
|
+
email: user.email,
|
|
2279
|
+
displayName: user.displayName ?? null,
|
|
2280
|
+
avatarUrl: user.avatarUrl ?? null,
|
|
2281
|
+
status: user.status,
|
|
2282
|
+
disabled: user.disabled === true
|
|
2283
|
+
}));
|
|
2284
|
+
const usersMappedAt = getRouteTimingNow();
|
|
2285
|
+
const aiProfileDiagnostics = { metadataBackfillCount: 0 };
|
|
2286
|
+
const aiProfilesStartedAt = getRouteTimingNow();
|
|
1403
2287
|
const aiProfiles = core
|
|
1404
|
-
.listAiProfiles(wid)
|
|
2288
|
+
.listAiProfiles(wid, { diagnostics: aiProfileDiagnostics })
|
|
1405
2289
|
.map((profile) => ({
|
|
1406
2290
|
value: profile.id,
|
|
1407
2291
|
label: profile.name,
|
|
1408
|
-
...(includeProfileToken ? { profileToken: profile.profileToken } : {}),
|
|
1409
2292
|
username: profile.username,
|
|
1410
2293
|
icon: profile.icon,
|
|
1411
2294
|
color: profile.color,
|
|
2295
|
+
avatarUrl: profile.avatarUrl ?? null,
|
|
2296
|
+
avatarSourceUrl: profile.avatarSourceUrl ?? null,
|
|
2297
|
+
avatarRevision: profile.avatarRevision ?? 0,
|
|
2298
|
+
avatarUpdatedAt: profile.avatarUpdatedAt ?? null,
|
|
1412
2299
|
kind: 'agent',
|
|
1413
2300
|
description: profile.description ?? null,
|
|
1414
2301
|
role: profile.role ?? null,
|
|
1415
2302
|
provider: profile.provider ?? null,
|
|
1416
2303
|
model: profile.model ?? null,
|
|
1417
2304
|
surfaceType: profile.surfaceType ?? null,
|
|
2305
|
+
seatScope: profile.seatScope ?? null,
|
|
2306
|
+
archivedAt: profile.archivedAt ?? null,
|
|
1418
2307
|
createdAt: profile.createdAt,
|
|
1419
2308
|
updatedAt: profile.updatedAt,
|
|
1420
2309
|
lastActiveAt: profile.lastActiveAt ?? null
|
|
1421
2310
|
}));
|
|
2311
|
+
const aiProfilesLoadedAt = getRouteTimingNow();
|
|
2312
|
+
const seatUsageStartedAt = getRouteTimingNow();
|
|
2313
|
+
const aiProfileSeatUsage = core.countAiProfileUsage(wid);
|
|
2314
|
+
const seatUsageLoadedAt = getRouteTimingNow();
|
|
1422
2315
|
const assignees = [...workspaceUsers, ...aiProfiles];
|
|
2316
|
+
const serializeStartedAt = getRouteTimingNow();
|
|
2317
|
+
const serialized = serializeJsonWithTiming({ assignees, aiProfileSeatUsage }, serializeStartedAt);
|
|
2318
|
+
const totalDurationMs = Math.max(0, getRouteTimingNow() - startedAt);
|
|
2319
|
+
setServerTimingHeader(res, [
|
|
2320
|
+
{ name: 'assignee-options', durationMs: totalDurationMs },
|
|
2321
|
+
{ name: 'assignee-options-users', durationMs: usersLoadedAt - usersStartedAt },
|
|
2322
|
+
{ name: 'assignee-options-users-map', durationMs: usersMappedAt - usersLoadedAt },
|
|
2323
|
+
{ name: 'assignee-options-ai-profiles', durationMs: aiProfilesLoadedAt - aiProfilesStartedAt },
|
|
2324
|
+
{ name: 'assignee-options-seat-usage', durationMs: seatUsageLoadedAt - seatUsageStartedAt },
|
|
2325
|
+
{ name: 'assignee-options-json', durationMs: serialized.durationMs },
|
|
2326
|
+
]);
|
|
2327
|
+
logSlowBootstrapRoute({
|
|
2328
|
+
route: '/api/taskforce/workspace/assignee-options',
|
|
2329
|
+
workspaceId: wid,
|
|
2330
|
+
traceId,
|
|
2331
|
+
durationMs: totalDurationMs,
|
|
2332
|
+
details: {
|
|
2333
|
+
humanAssigneeCount: workspaceUsers.length,
|
|
2334
|
+
workspaceUserCount: activeWorkspaceUsers.length,
|
|
2335
|
+
aiProfileCount: aiProfiles.length,
|
|
2336
|
+
agentsOnly,
|
|
2337
|
+
metadataBackfillCount: aiProfileDiagnostics.metadataBackfillCount,
|
|
2338
|
+
responseBytes: serialized.byteLength,
|
|
2339
|
+
listWorkspaceUsersMs: Math.max(0, usersLoadedAt - usersStartedAt),
|
|
2340
|
+
workspaceUsersMapMs: Math.max(0, usersMappedAt - usersLoadedAt),
|
|
2341
|
+
listAiProfilesMs: Math.max(0, aiProfilesLoadedAt - aiProfilesStartedAt),
|
|
2342
|
+
countAiProfileUsageMs: Math.max(0, seatUsageLoadedAt - seatUsageStartedAt),
|
|
2343
|
+
jsonSerializeMs: serialized.durationMs,
|
|
2344
|
+
},
|
|
2345
|
+
});
|
|
1423
2346
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1424
|
-
res.end(
|
|
2347
|
+
res.end(serialized.body);
|
|
2348
|
+
});
|
|
2349
|
+
// POST /api/taskforce/workspace/ai-profiles/avatar - Clear an AI profile avatar image.
|
|
2350
|
+
addRoute('POST', '/api/taskforce/workspace/ai-profiles/avatar', async (req, res) => {
|
|
2351
|
+
if (!ensureWorkspaceAdminRole(req)) {
|
|
2352
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
2353
|
+
res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
|
|
2354
|
+
return;
|
|
2355
|
+
}
|
|
2356
|
+
const body = await parseJsonBody(req);
|
|
2357
|
+
const wid = requestWorkspaceId(req);
|
|
2358
|
+
const profileId = String(body?.profileId || '').trim();
|
|
2359
|
+
const avatarUrl = body?.avatarUrl === null ? null : String(body?.avatarUrl || '').trim();
|
|
2360
|
+
const avatarSourceUrl = body?.avatarSourceUrl === undefined
|
|
2361
|
+
? undefined
|
|
2362
|
+
: body?.avatarSourceUrl === null
|
|
2363
|
+
? null
|
|
2364
|
+
: String(body?.avatarSourceUrl || '').trim();
|
|
2365
|
+
if (!profileId) {
|
|
2366
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2367
|
+
res.end(JSON.stringify({ error: 'profileId is required.' }));
|
|
2368
|
+
return;
|
|
2369
|
+
}
|
|
2370
|
+
try {
|
|
2371
|
+
const shouldClearAvatar = avatarUrl === null && (avatarSourceUrl === null || avatarSourceUrl === undefined);
|
|
2372
|
+
if (!shouldClearAvatar) {
|
|
2373
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2374
|
+
res.end(JSON.stringify({
|
|
2375
|
+
error: 'AI profile avatar updates must use the stable avatar upload endpoint.',
|
|
2376
|
+
code: 'AI_PROFILE_AVATAR_STABLE_UPLOAD_REQUIRED'
|
|
2377
|
+
}));
|
|
2378
|
+
return;
|
|
2379
|
+
}
|
|
2380
|
+
const result = await clearAiProfileAvatarAssets(aiProfileAvatarAssetDeps, { workspaceId: wid, profileId });
|
|
2381
|
+
const profile = result.profile;
|
|
2382
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2383
|
+
res.end(JSON.stringify({ success: true, profile: serializeAiProfilePublicRecord(profile) }));
|
|
2384
|
+
}
|
|
2385
|
+
catch (err) {
|
|
2386
|
+
const status = err?.statusCode ?? 500;
|
|
2387
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
2388
|
+
res.end(JSON.stringify({ error: err?.message ?? 'Failed to update AI profile avatar.' }));
|
|
2389
|
+
}
|
|
2390
|
+
});
|
|
2391
|
+
// POST /api/taskforce/workspace/ai-profiles/avatar/upload - Upload stable AI profile avatar slots.
|
|
2392
|
+
addRoute('POST', '/api/taskforce/workspace/ai-profiles/avatar/upload', async (req, res) => {
|
|
2393
|
+
if (!ensureWorkspaceAdminRole(req)) {
|
|
2394
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
2395
|
+
res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
|
|
2396
|
+
return;
|
|
2397
|
+
}
|
|
2398
|
+
const body = await parseJsonBody(req, AI_PROFILE_AVATAR_UPLOAD_MAX_JSON_BYTES);
|
|
2399
|
+
const wid = requestWorkspaceId(req);
|
|
2400
|
+
const profileId = String(body?.profileId || '').trim();
|
|
2401
|
+
if (!profileId) {
|
|
2402
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2403
|
+
res.end(JSON.stringify({ error: 'profileId is required.' }));
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
const display = parseAiAvatarDataUrlPayload(body?.displayImage || body?.display, 'display-avatar');
|
|
2407
|
+
if (!display) {
|
|
2408
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2409
|
+
res.end(JSON.stringify({ error: 'displayImage data URL is required.' }));
|
|
2410
|
+
return;
|
|
2411
|
+
}
|
|
2412
|
+
const source = parseAiAvatarDataUrlPayload(body?.sourceImage || body?.source, 'source-avatar');
|
|
2413
|
+
try {
|
|
2414
|
+
const result = await saveAiProfileAvatarAssets(aiProfileAvatarAssetDeps, {
|
|
2415
|
+
workspaceId: wid,
|
|
2416
|
+
profileId,
|
|
2417
|
+
display,
|
|
2418
|
+
source,
|
|
2419
|
+
actorId: resolveAuthenticatedUserId(req)
|
|
2420
|
+
});
|
|
2421
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2422
|
+
res.end(JSON.stringify({
|
|
2423
|
+
success: true,
|
|
2424
|
+
profile: serializeAiProfilePublicRecord(result.profile),
|
|
2425
|
+
assets: {
|
|
2426
|
+
display: {
|
|
2427
|
+
assetId: result.displayAsset.assetId,
|
|
2428
|
+
storageKey: result.displayStorageKey,
|
|
2429
|
+
},
|
|
2430
|
+
source: {
|
|
2431
|
+
assetId: result.sourceAsset.assetId,
|
|
2432
|
+
storageKey: result.sourceStorageKey,
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
}));
|
|
2436
|
+
}
|
|
2437
|
+
catch (err) {
|
|
2438
|
+
const status = err?.statusCode ?? 500;
|
|
2439
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
2440
|
+
res.end(JSON.stringify({ error: err?.message ?? 'Failed to upload AI profile avatar.' }));
|
|
2441
|
+
}
|
|
1425
2442
|
});
|
|
1426
2443
|
// POST /api/taskforce/workspace/ai-profiles/merge - Merge a duplicate AI profile into another, reassigning all references.
|
|
1427
2444
|
addRoute('POST', '/api/taskforce/workspace/ai-profiles/merge', async (req, res) => {
|
|
@@ -1442,7 +2459,7 @@ export function registerAuthRoutes(deps) {
|
|
|
1442
2459
|
try {
|
|
1443
2460
|
const profile = core.mergeAiProfiles({ workspaceId: wid, keepId, mergeId });
|
|
1444
2461
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1445
|
-
res.end(JSON.stringify({ profile }));
|
|
2462
|
+
res.end(JSON.stringify({ profile: serializeAiProfilePublicRecord(profile) }));
|
|
1446
2463
|
}
|
|
1447
2464
|
catch (err) {
|
|
1448
2465
|
const status = err?.statusCode ?? 500;
|
|
@@ -1450,6 +2467,80 @@ export function registerAuthRoutes(deps) {
|
|
|
1450
2467
|
res.end(JSON.stringify({ error: err?.message ?? 'Merge failed.' }));
|
|
1451
2468
|
}
|
|
1452
2469
|
});
|
|
2470
|
+
// POST /api/taskforce/workspace/ai-profiles/surface-type - Manually re-categorize an active AI profile.
|
|
2471
|
+
addRoute('POST', '/api/taskforce/workspace/ai-profiles/surface-type', async (req, res) => {
|
|
2472
|
+
if (!ensureWorkspaceAdminRole(req)) {
|
|
2473
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
2474
|
+
res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
const body = await parseJsonBody(req);
|
|
2478
|
+
const wid = requestWorkspaceId(req);
|
|
2479
|
+
const profileId = String(body.profileId || '').trim();
|
|
2480
|
+
const hasSurfaceType = Boolean(body && Object.prototype.hasOwnProperty.call(body, 'surfaceType'));
|
|
2481
|
+
if (!profileId) {
|
|
2482
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2483
|
+
res.end(JSON.stringify({ error: 'profileId is required.' }));
|
|
2484
|
+
return;
|
|
2485
|
+
}
|
|
2486
|
+
if (!hasSurfaceType) {
|
|
2487
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2488
|
+
res.end(JSON.stringify({ error: 'surfaceType is required.' }));
|
|
2489
|
+
return;
|
|
2490
|
+
}
|
|
2491
|
+
try {
|
|
2492
|
+
const profile = core.updateAiProfileSurfaceType({
|
|
2493
|
+
workspaceId: wid,
|
|
2494
|
+
profileId,
|
|
2495
|
+
surfaceType: body.surfaceType
|
|
2496
|
+
});
|
|
2497
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2498
|
+
res.end(JSON.stringify({ success: true, profile: serializeAiProfilePublicRecord(profile) }));
|
|
2499
|
+
}
|
|
2500
|
+
catch (err) {
|
|
2501
|
+
const status = err?.statusCode ?? 500;
|
|
2502
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
2503
|
+
res.end(JSON.stringify({
|
|
2504
|
+
error: err?.message ?? 'Failed to update AI profile category.',
|
|
2505
|
+
code: err?.code
|
|
2506
|
+
}));
|
|
2507
|
+
}
|
|
2508
|
+
});
|
|
2509
|
+
// POST /api/taskforce/workspace/ai-profiles/archive - Remove an AI profile from the active roster without deleting its history.
|
|
2510
|
+
addRoute('POST', '/api/taskforce/workspace/ai-profiles/archive', async (req, res) => {
|
|
2511
|
+
if (!ensureWorkspaceAdminRole(req)) {
|
|
2512
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
2513
|
+
res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
|
|
2514
|
+
return;
|
|
2515
|
+
}
|
|
2516
|
+
const body = await parseJsonBody(req);
|
|
2517
|
+
const wid = requestWorkspaceId(req);
|
|
2518
|
+
const profileId = String(body.profileId || '').trim();
|
|
2519
|
+
const reason = String(body.reason || '').trim() || 'manual_archive';
|
|
2520
|
+
if (!profileId) {
|
|
2521
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
2522
|
+
res.end(JSON.stringify({ error: 'profileId is required.' }));
|
|
2523
|
+
return;
|
|
2524
|
+
}
|
|
2525
|
+
try {
|
|
2526
|
+
const profile = core.archiveAiProfile({
|
|
2527
|
+
workspaceId: wid,
|
|
2528
|
+
profileId,
|
|
2529
|
+
archivedBy: requestUserId(req),
|
|
2530
|
+
reason
|
|
2531
|
+
});
|
|
2532
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2533
|
+
res.end(JSON.stringify({ profile: serializeAiProfilePublicRecord(profile) }));
|
|
2534
|
+
}
|
|
2535
|
+
catch (err) {
|
|
2536
|
+
const status = err?.statusCode ?? 500;
|
|
2537
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
2538
|
+
res.end(JSON.stringify({
|
|
2539
|
+
error: err?.message ?? 'Failed to archive AI profile.',
|
|
2540
|
+
details: err?.details ?? null
|
|
2541
|
+
}));
|
|
2542
|
+
}
|
|
2543
|
+
});
|
|
1453
2544
|
// GET /api/taskforce/admin/users - List workspace users for team management.
|
|
1454
2545
|
addRoute('GET', '/api/taskforce/admin/users', async (req, res) => {
|
|
1455
2546
|
if (!ensureWorkspaceAdminRole(req)) {
|
|
@@ -1458,8 +2549,9 @@ export function registerAuthRoutes(deps) {
|
|
|
1458
2549
|
return;
|
|
1459
2550
|
}
|
|
1460
2551
|
const wid = requestWorkspaceId(req);
|
|
2552
|
+
const actorUserId = resolveAuthenticatedUserId(req);
|
|
1461
2553
|
const gate = core.evaluateFeatureAccess({
|
|
1462
|
-
userId:
|
|
2554
|
+
userId: actorUserId,
|
|
1463
2555
|
workspaceId: wid,
|
|
1464
2556
|
featureKey: 'collaboration.team_management'
|
|
1465
2557
|
});
|
|
@@ -1474,13 +2566,14 @@ export function registerAuthRoutes(deps) {
|
|
|
1474
2566
|
});
|
|
1475
2567
|
// POST /api/taskforce/admin/users/invite - Invite a user into a workspace.
|
|
1476
2568
|
addRoute('POST', '/api/taskforce/admin/users/invite', async (req, res) => {
|
|
1477
|
-
|
|
2569
|
+
const body = await parseJsonBody(req);
|
|
2570
|
+
const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
|
|
2571
|
+
if (!ensureWorkspaceAdminRole(req, wid)) {
|
|
1478
2572
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
1479
2573
|
res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
|
|
1480
2574
|
return;
|
|
1481
2575
|
}
|
|
1482
|
-
const
|
|
1483
|
-
const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
|
|
2576
|
+
const actorUserId = resolveAuthenticatedUserId(req);
|
|
1484
2577
|
const email = String(body?.email || '').trim().toLowerCase();
|
|
1485
2578
|
const role = String(body?.role || 'member').trim().toLowerCase();
|
|
1486
2579
|
const permissionMode = String(body?.permissionMode || 'read-write').trim().toLowerCase();
|
|
@@ -1490,7 +2583,7 @@ export function registerAuthRoutes(deps) {
|
|
|
1490
2583
|
return;
|
|
1491
2584
|
}
|
|
1492
2585
|
const inviteGate = core.evaluateFeatureAccess({
|
|
1493
|
-
userId:
|
|
2586
|
+
userId: actorUserId,
|
|
1494
2587
|
workspaceId: wid,
|
|
1495
2588
|
featureKey: 'collaboration.invites'
|
|
1496
2589
|
});
|
|
@@ -1513,8 +2606,8 @@ export function registerAuthRoutes(deps) {
|
|
|
1513
2606
|
let inviteEmailError = null;
|
|
1514
2607
|
if (inviteToken.token) {
|
|
1515
2608
|
const workspace = core.getWorkspaceProfile(wid);
|
|
1516
|
-
const actorIdentity = core.resolveUserIdentity(
|
|
1517
|
-
const inviterEmailOrLabel = String(actorIdentity?.email ||
|
|
2609
|
+
const actorIdentity = core.resolveUserIdentity(actorUserId);
|
|
2610
|
+
const inviterEmailOrLabel = String(actorIdentity?.email || actorUserId || 'Taskforce').trim();
|
|
1518
2611
|
const delivery = await deliverInviteEmail(email, inviteToken.token, req, {
|
|
1519
2612
|
workspaceName: String(workspace?.name || wid),
|
|
1520
2613
|
inviterEmailOrLabel
|
|
@@ -1529,7 +2622,7 @@ export function registerAuthRoutes(deps) {
|
|
|
1529
2622
|
role,
|
|
1530
2623
|
permissionMode: role === 'member' ? (permissionMode === 'read-only' ? 'read-only' : 'read-write') : 'read-write',
|
|
1531
2624
|
inviteEmailSent
|
|
1532
|
-
});
|
|
2625
|
+
}, wid);
|
|
1533
2626
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1534
2627
|
res.end(JSON.stringify({
|
|
1535
2628
|
success: true,
|
|
@@ -1546,13 +2639,14 @@ export function registerAuthRoutes(deps) {
|
|
|
1546
2639
|
});
|
|
1547
2640
|
// PATCH /api/taskforce/admin/users/:id/role - Update workspace role for a user.
|
|
1548
2641
|
addRoute('PATCH', '/api/taskforce/admin/users/:id/role', async (req, res, params) => {
|
|
1549
|
-
|
|
2642
|
+
const body = await parseJsonBody(req);
|
|
2643
|
+
const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
|
|
2644
|
+
if (!ensureWorkspaceAdminRole(req, wid)) {
|
|
1550
2645
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
1551
2646
|
res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
|
|
1552
2647
|
return;
|
|
1553
2648
|
}
|
|
1554
|
-
const
|
|
1555
|
-
const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
|
|
2649
|
+
const actorUserId = resolveAuthenticatedUserId(req);
|
|
1556
2650
|
const role = String(body?.role || '').trim().toLowerCase();
|
|
1557
2651
|
if (role !== 'owner' && role !== 'admin' && role !== 'member') {
|
|
1558
2652
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
@@ -1560,7 +2654,7 @@ export function registerAuthRoutes(deps) {
|
|
|
1560
2654
|
return;
|
|
1561
2655
|
}
|
|
1562
2656
|
const gate = core.evaluateFeatureAccess({
|
|
1563
|
-
userId:
|
|
2657
|
+
userId: actorUserId,
|
|
1564
2658
|
workspaceId: wid,
|
|
1565
2659
|
featureKey: 'collaboration.team_management'
|
|
1566
2660
|
});
|
|
@@ -1576,7 +2670,7 @@ export function registerAuthRoutes(deps) {
|
|
|
1576
2670
|
res.end(JSON.stringify({ error: 'User membership not found.' }));
|
|
1577
2671
|
return;
|
|
1578
2672
|
}
|
|
1579
|
-
auditAuthAction(req, 'update-workspace-user-role', { workspaceId: wid, targetUserId: params.id, role });
|
|
2673
|
+
auditAuthAction(req, 'update-workspace-user-role', { workspaceId: wid, targetUserId: params.id, role }, wid);
|
|
1580
2674
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1581
2675
|
res.end(JSON.stringify({ success: true }));
|
|
1582
2676
|
}
|
|
@@ -1588,13 +2682,14 @@ export function registerAuthRoutes(deps) {
|
|
|
1588
2682
|
});
|
|
1589
2683
|
// PATCH /api/taskforce/admin/users/:id/permission-mode - Update member permission mode.
|
|
1590
2684
|
addRoute('PATCH', '/api/taskforce/admin/users/:id/permission-mode', async (req, res, params) => {
|
|
1591
|
-
|
|
2685
|
+
const body = await parseJsonBody(req);
|
|
2686
|
+
const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
|
|
2687
|
+
if (!ensureWorkspaceAdminRole(req, wid)) {
|
|
1592
2688
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
1593
2689
|
res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
|
|
1594
2690
|
return;
|
|
1595
2691
|
}
|
|
1596
|
-
const
|
|
1597
|
-
const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
|
|
2692
|
+
const actorUserId = resolveAuthenticatedUserId(req);
|
|
1598
2693
|
const mode = String(body?.mode || '').trim().toLowerCase();
|
|
1599
2694
|
if (mode !== 'read-only' && mode !== 'read-write') {
|
|
1600
2695
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
@@ -1602,7 +2697,7 @@ export function registerAuthRoutes(deps) {
|
|
|
1602
2697
|
return;
|
|
1603
2698
|
}
|
|
1604
2699
|
const gate = core.evaluateFeatureAccess({
|
|
1605
|
-
userId:
|
|
2700
|
+
userId: actorUserId,
|
|
1606
2701
|
workspaceId: wid,
|
|
1607
2702
|
featureKey: 'collaboration.team_management'
|
|
1608
2703
|
});
|
|
@@ -1618,7 +2713,7 @@ export function registerAuthRoutes(deps) {
|
|
|
1618
2713
|
res.end(JSON.stringify({ error: 'User membership not found.' }));
|
|
1619
2714
|
return;
|
|
1620
2715
|
}
|
|
1621
|
-
auditAuthAction(req, 'update-workspace-user-permission-mode', { workspaceId: wid, targetUserId: params.id, mode });
|
|
2716
|
+
auditAuthAction(req, 'update-workspace-user-permission-mode', { workspaceId: wid, targetUserId: params.id, mode }, wid);
|
|
1622
2717
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1623
2718
|
res.end(JSON.stringify({ success: true }));
|
|
1624
2719
|
}
|
|
@@ -1630,16 +2725,17 @@ export function registerAuthRoutes(deps) {
|
|
|
1630
2725
|
});
|
|
1631
2726
|
// PATCH /api/taskforce/admin/users/:id/disable - Disable or reactivate a workspace user.
|
|
1632
2727
|
addRoute('PATCH', '/api/taskforce/admin/users/:id/disable', async (req, res, params) => {
|
|
1633
|
-
|
|
2728
|
+
const body = await parseJsonBody(req);
|
|
2729
|
+
const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
|
|
2730
|
+
if (!ensureWorkspaceAdminRole(req, wid)) {
|
|
1634
2731
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
1635
2732
|
res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
|
|
1636
2733
|
return;
|
|
1637
2734
|
}
|
|
1638
|
-
const
|
|
1639
|
-
const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
|
|
2735
|
+
const actorUserId = resolveAuthenticatedUserId(req);
|
|
1640
2736
|
const disabled = body?.disabled === true;
|
|
1641
2737
|
const gate = core.evaluateFeatureAccess({
|
|
1642
|
-
userId:
|
|
2738
|
+
userId: actorUserId,
|
|
1643
2739
|
workspaceId: wid,
|
|
1644
2740
|
featureKey: 'collaboration.team_management'
|
|
1645
2741
|
});
|
|
@@ -1655,7 +2751,7 @@ export function registerAuthRoutes(deps) {
|
|
|
1655
2751
|
res.end(JSON.stringify({ error: 'User membership not found.' }));
|
|
1656
2752
|
return;
|
|
1657
2753
|
}
|
|
1658
|
-
auditAuthAction(req, 'set-workspace-user-disabled', { workspaceId: wid, targetUserId: params.id, disabled });
|
|
2754
|
+
auditAuthAction(req, 'set-workspace-user-disabled', { workspaceId: wid, targetUserId: params.id, disabled }, wid);
|
|
1659
2755
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1660
2756
|
res.end(JSON.stringify({ success: true }));
|
|
1661
2757
|
}
|
|
@@ -1667,15 +2763,16 @@ export function registerAuthRoutes(deps) {
|
|
|
1667
2763
|
});
|
|
1668
2764
|
// POST /api/taskforce/admin/users/:id/invite/revoke - Revoke invite acceptance token.
|
|
1669
2765
|
addRoute('POST', '/api/taskforce/admin/users/:id/invite/revoke', async (req, res, params) => {
|
|
1670
|
-
|
|
2766
|
+
const body = await parseJsonBody(req);
|
|
2767
|
+
const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
|
|
2768
|
+
if (!ensureWorkspaceAdminRole(req, wid)) {
|
|
1671
2769
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
1672
2770
|
res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
|
|
1673
2771
|
return;
|
|
1674
2772
|
}
|
|
1675
|
-
const
|
|
1676
|
-
const wid = String(body?.workspaceId || requestWorkspaceId(req)).trim() || 'default';
|
|
2773
|
+
const actorUserId = resolveAuthenticatedUserId(req);
|
|
1677
2774
|
const gate = core.evaluateFeatureAccess({
|
|
1678
|
-
userId:
|
|
2775
|
+
userId: actorUserId,
|
|
1679
2776
|
workspaceId: wid,
|
|
1680
2777
|
featureKey: 'collaboration.team_management'
|
|
1681
2778
|
});
|
|
@@ -1685,21 +2782,22 @@ export function registerAuthRoutes(deps) {
|
|
|
1685
2782
|
return;
|
|
1686
2783
|
}
|
|
1687
2784
|
const revoked = core.revokeInviteAcceptanceForUser(params.id);
|
|
1688
|
-
auditAuthAction(req, 'revoke-invite', { workspaceId: wid, targetUserId: params.id, revoked: revoked.revoked });
|
|
2785
|
+
auditAuthAction(req, 'revoke-invite', { workspaceId: wid, targetUserId: params.id, revoked: revoked.revoked }, wid);
|
|
1689
2786
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1690
2787
|
res.end(JSON.stringify({ success: true, revoked: revoked.revoked }));
|
|
1691
2788
|
});
|
|
1692
2789
|
// DELETE /api/taskforce/admin/users/:id - Remove workspace membership.
|
|
1693
2790
|
addRoute('DELETE', '/api/taskforce/admin/users/:id', async (req, res, params) => {
|
|
1694
|
-
|
|
2791
|
+
const reqUrl = new URL(req.url || '/api/taskforce/admin/users/:id', 'http://localhost');
|
|
2792
|
+
const wid = String(reqUrl.searchParams.get('workspaceId') || requestWorkspaceId(req)).trim() || 'default';
|
|
2793
|
+
if (!ensureWorkspaceAdminRole(req, wid)) {
|
|
1695
2794
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
1696
2795
|
res.end(JSON.stringify({ error: 'Forbidden: owner or admin role required.' }));
|
|
1697
2796
|
return;
|
|
1698
2797
|
}
|
|
1699
|
-
const
|
|
1700
|
-
const wid = String(reqUrl.searchParams.get('workspaceId') || requestWorkspaceId(req)).trim() || 'default';
|
|
2798
|
+
const actorUserId = resolveAuthenticatedUserId(req);
|
|
1701
2799
|
const gate = core.evaluateFeatureAccess({
|
|
1702
|
-
userId:
|
|
2800
|
+
userId: actorUserId,
|
|
1703
2801
|
workspaceId: wid,
|
|
1704
2802
|
featureKey: 'collaboration.team_management'
|
|
1705
2803
|
});
|
|
@@ -1715,7 +2813,7 @@ export function registerAuthRoutes(deps) {
|
|
|
1715
2813
|
res.end(JSON.stringify({ error: 'User membership not found.' }));
|
|
1716
2814
|
return;
|
|
1717
2815
|
}
|
|
1718
|
-
auditAuthAction(req, 'remove-workspace-user', { workspaceId: wid, targetUserId: params.id });
|
|
2816
|
+
auditAuthAction(req, 'remove-workspace-user', { workspaceId: wid, targetUserId: params.id }, wid);
|
|
1719
2817
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1720
2818
|
res.end(JSON.stringify({ success: true }));
|
|
1721
2819
|
}
|
|
@@ -1733,8 +2831,9 @@ export function registerAuthRoutes(deps) {
|
|
|
1733
2831
|
return;
|
|
1734
2832
|
}
|
|
1735
2833
|
const wid = requestWorkspaceId(req);
|
|
2834
|
+
const actorUserId = resolveAuthenticatedUserId(req);
|
|
1736
2835
|
const gate = core.evaluateFeatureAccess({
|
|
1737
|
-
userId:
|
|
2836
|
+
userId: actorUserId,
|
|
1738
2837
|
workspaceId: wid,
|
|
1739
2838
|
featureKey: 'collaboration.team_management'
|
|
1740
2839
|
});
|