@taskforcehq/taskforce 0.3.303 → 0.3.305

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/README.md +140 -247
  2. package/dist/Taskforce.module.css +102 -5
  3. package/dist/TaskforceCore.js +635 -238
  4. package/dist/TaskforceCore.test.js +2243 -495
  5. package/dist/annotatedAttachments/service.d.ts +1 -1
  6. package/dist/annotatedAttachments/types.d.ts +1 -1
  7. package/dist/compat/workspaceSyncCompat.js +6 -0
  8. package/dist/components/features/AgentsModule.js +1 -1
  9. package/dist/components/features/AgentsModule.test.js +1 -1
  10. package/dist/components/features/DocumentIndex.d.ts +1 -1
  11. package/dist/components/features/DocumentIndex.js +1 -1
  12. package/dist/components/features/DocumentViewer.d.ts +1 -1
  13. package/dist/components/features/DocumentViewer.js +32 -9
  14. package/dist/components/features/DocumentWorkspace.d.ts +5 -22
  15. package/dist/components/features/DocumentWorkspace.js +12 -155
  16. package/dist/components/features/DocumentWorkspace.test.js +193 -6
  17. package/dist/components/features/TaskSettings.js +87 -41
  18. package/dist/components/features/TaskSettings.test.js +180 -40
  19. package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
  20. package/dist/components/features/documentWorkspaceModel.js +57 -0
  21. package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
  22. package/dist/components/features/useDocumentWorkspaceController.js +113 -0
  23. package/dist/components/task/TaskActionHeader.d.ts +2 -1
  24. package/dist/components/task/TaskActionHeader.js +3 -3
  25. package/dist/components/task/TaskActionHeader.test.js +8 -0
  26. package/dist/components/task/TaskCard.js +16 -1
  27. package/dist/components/task/TaskCard.test.js +31 -0
  28. package/dist/components/task/TaskForm.d.ts +9 -8
  29. package/dist/components/task/TaskForm.js +374 -219
  30. package/dist/components/task/TaskForm.test.js +482 -361
  31. package/dist/components/task/TaskKanban.d.ts +1 -1
  32. package/dist/components/task/TaskKanban.js +33 -161
  33. package/dist/components/task/TaskStatusActions.d.ts +6 -1
  34. package/dist/components/task/TaskStatusActions.js +8 -3
  35. package/dist/components/task/TaskStatusActions.test.js +9 -0
  36. package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
  37. package/dist/components/task/taskKanbanDropRules.js +187 -0
  38. package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
  39. package/dist/components/task/taskKanbanDropRules.test.js +114 -0
  40. package/dist/components/ui/Modal.d.ts +1 -1
  41. package/dist/components/ui/Modal.js +1 -0
  42. package/dist/components/views/AppShellHeader.js +5 -1
  43. package/dist/components/views/AppShellHeader.test.js +4 -0
  44. package/dist/components/views/PlanComparisonPage.d.ts +3 -2
  45. package/dist/components/views/PlanComparisonPage.js +9 -10
  46. package/dist/components/views/PlanComparisonPage.test.js +55 -41
  47. package/dist/components/views/PlansPage.d.ts +5 -1
  48. package/dist/components/views/PlansPage.js +150 -81
  49. package/dist/components/views/PlansPage.test.js +367 -41
  50. package/dist/components/views/StandaloneLayout.js +135 -54
  51. package/dist/components/views/WidgetView.js +11 -2
  52. package/dist/components/views/panels/FilterToolbar.test.js +6 -4
  53. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +2 -14
  54. package/dist/components/views/standalone/modals/AccountHubModal.js +12 -14
  55. package/dist/components/views/standalone/modals/AccountHubModal.test.js +24 -1
  56. package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
  57. package/dist/config/envSchema.js +1 -1
  58. package/dist/core/AiProfileService.d.ts +6 -1
  59. package/dist/core/AiProfileService.js +161 -16
  60. package/dist/core/AiProfileService.test.js +3 -1
  61. package/dist/core/AiProfiles.test.js +200 -0
  62. package/dist/core/AuthTokenService.test.d.ts +1 -0
  63. package/dist/core/AuthTokenService.test.js +78 -0
  64. package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
  65. package/dist/core/EntitlementsPolicy.test.js +75 -13
  66. package/dist/core/PlanEntitlementService.d.ts +9 -1
  67. package/dist/core/PlanEntitlementService.js +144 -19
  68. package/dist/core/PlanVersionPolicy.test.js +19 -26
  69. package/dist/core/SignupMonetizationSettings.test.js +75 -15
  70. package/dist/core/SystemAdmin.test.js +212 -56
  71. package/dist/core/TaskActivityService.d.ts +4 -1
  72. package/dist/core/TaskActivityService.js +45 -0
  73. package/dist/core/TaskAuditTimeline.test.js +52 -1
  74. package/dist/core/TaskTaxonomyValidation.test.js +53 -0
  75. package/dist/core/Taskforce.d.ts +18 -11
  76. package/dist/core/Taskforce.js +93 -12
  77. package/dist/core/WorkspacePlanMode.test.js +73 -8
  78. package/dist/core/shared.d.ts +2 -0
  79. package/dist/core/shared.js +11 -0
  80. package/dist/documentReviews/service.d.ts +1 -1
  81. package/dist/documentReviews/types.d.ts +1 -1
  82. package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
  83. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
  84. package/dist/hooks/sync/auth.d.ts +37 -0
  85. package/dist/hooks/sync/auth.js +63 -0
  86. package/dist/hooks/sync/bootstrap.d.ts +34 -0
  87. package/dist/hooks/sync/bootstrap.js +52 -0
  88. package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
  89. package/dist/hooks/sync/orchestratorShared.js +209 -0
  90. package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
  91. package/dist/hooks/sync/orchestratorShared.test.js +49 -0
  92. package/dist/hooks/sync/realtime.d.ts +25 -0
  93. package/dist/hooks/sync/realtime.js +60 -0
  94. package/dist/hooks/sync/recovery.d.ts +69 -0
  95. package/dist/hooks/sync/recovery.js +118 -0
  96. package/dist/hooks/sync/transfers.d.ts +123 -0
  97. package/dist/hooks/sync/transfers.js +435 -0
  98. package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
  99. package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
  100. package/dist/hooks/ui/useResourceExport.d.ts +3 -1
  101. package/dist/hooks/ui/useResourceExport.js +5 -2
  102. package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
  103. package/dist/hooks/useRealtimeSync.js +2 -1
  104. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
  105. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
  106. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
  107. package/dist/hooks/useSyncOrchestrator.d.ts +12 -8
  108. package/dist/hooks/useSyncOrchestrator.js +388 -879
  109. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +641 -6
  110. package/dist/hooks/useTaskMutations.d.ts +2 -1
  111. package/dist/hooks/useTaskMutations.js +57 -7
  112. package/dist/hooks/useTaskMutations.test.js +176 -0
  113. package/dist/hooks/useTaskforce.d.ts +13 -4
  114. package/dist/hooks/useTaskforce.js +302 -817
  115. package/dist/hooks/useTaskforce.sync-behavior.test.js +78 -0
  116. package/dist/hooks/useWorkspaceSyncController.d.ts +4 -1
  117. package/dist/hooks/useWorkspaceSyncController.js +33 -11
  118. package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
  119. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
  120. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
  121. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
  122. package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
  123. package/dist/hooks/workspace/workspaceLocalState.js +100 -0
  124. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
  125. package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
  126. package/dist/localization/locales/en-US.js +2 -2
  127. package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
  128. package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
  129. package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
  130. package/dist/mcp/canonicalAssetHelpers.js +259 -0
  131. package/dist/mcp/clientIntegrations.d.ts +12 -1
  132. package/dist/mcp/clientIntegrations.js +161 -6
  133. package/dist/mcp/clientIntegrations.test.js +76 -4
  134. package/dist/mcp/collaborationRegistrar.d.ts +2 -0
  135. package/dist/mcp/collaborationRegistrar.js +71 -0
  136. package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
  137. package/dist/mcp/documentAssetRegistrar.js +346 -0
  138. package/dist/mcp/documentHelpers.d.ts +100 -0
  139. package/dist/mcp/documentHelpers.js +161 -0
  140. package/dist/mcp/runtime.js +2455 -3925
  141. package/dist/mcp/runtime.test.js +240 -8
  142. package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
  143. package/dist/mcp/taskAttachmentHelpers.js +60 -0
  144. package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
  145. package/dist/mcp/taskAttachmentTypes.js +1 -0
  146. package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
  147. package/dist/mcp/taskPlanningRegistrar.js +418 -0
  148. package/dist/mcp/toolCatalog.d.ts +35 -0
  149. package/dist/mcp/toolCatalog.js +3 -0
  150. package/dist/plugins/vite.js +1 -1
  151. package/dist/runtime/appGateDefinitions.d.ts +1 -1
  152. package/dist/runtime/appGateDefinitions.js +1 -1
  153. package/dist/server/index.d.ts +1 -0
  154. package/dist/server/index.js +46 -3
  155. package/dist/server/index.test.js +12 -1
  156. package/dist/server/routes/admin.d.ts +1 -5
  157. package/dist/server/routes/admin.js +151 -80
  158. package/dist/server/routes/annotatedAttachments.d.ts +1 -1
  159. package/dist/server/routes/annotatedAttachments.js +1 -1
  160. package/dist/server/routes/auth.d.ts +1 -4
  161. package/dist/server/routes/auth.js +59 -64
  162. package/dist/server/routes/authSupport.d.ts +30 -0
  163. package/dist/server/routes/authSupport.js +81 -0
  164. package/dist/server/routes/billing.d.ts +1 -1
  165. package/dist/server/routes/billing.js +122 -255
  166. package/dist/server/routes/billing.test.js +257 -86
  167. package/dist/server/routes/documentReviews.d.ts +1 -1
  168. package/dist/server/routes/documentReviews.js +1 -1
  169. package/dist/server/routes/documents.d.ts +4 -2
  170. package/dist/server/routes/documents.js +73 -7
  171. package/dist/server/routes/primitives.d.ts +11 -0
  172. package/dist/server/routes/primitives.js +73 -0
  173. package/dist/server/routes/resources.d.ts +1 -1
  174. package/dist/server/routes/resources.js +1 -1
  175. package/dist/server/routes/shared.d.ts +31 -1
  176. package/dist/server/routes/shared.js +26 -0
  177. package/dist/server/routes/sync.d.ts +1 -1
  178. package/dist/server/routes/sync.integration.test.js +20 -0
  179. package/dist/server/routes/sync.js +111 -238
  180. package/dist/server/routes/tasks.d.ts +1 -1
  181. package/dist/server/routes/tasks.js +50 -8
  182. package/dist/server/routes/workspaces.d.ts +1 -1
  183. package/dist/server/routes/workspaces.js +1 -1
  184. package/dist/server/routes.d.ts +2 -9
  185. package/dist/server/routes.js +84 -181
  186. package/dist/server/routes.test.js +912 -32
  187. package/dist/shared/taskActor.d.ts +10 -0
  188. package/dist/shared/taskActor.js +1 -0
  189. package/dist/sync/collaborationSyncPayload.js +4 -0
  190. package/dist/sync/collaborationSyncPayload.test.js +8 -0
  191. package/dist/sync/collaborationSyncState.d.ts +3 -1
  192. package/dist/sync/collaborationSyncState.js +19 -0
  193. package/dist/sync/contentSyncState.js +28 -17
  194. package/dist/sync/syncApplyHandlers.d.ts +87 -1
  195. package/dist/sync/syncApplyHandlers.js +370 -8
  196. package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
  197. package/dist/sync/syncApplyHandlers.test.js +379 -0
  198. package/dist/sync/syncService.d.ts +9 -1
  199. package/dist/sync/syncService.js +44 -2
  200. package/dist/sync/syncService.test.js +62 -0
  201. package/dist/sync/workspacePullFeed.d.ts +1 -0
  202. package/dist/sync/workspacePullFeed.js +66 -59
  203. package/dist/sync/workspacePullFeed.test.js +72 -4
  204. package/dist/sync/workspaceSyncModel.d.ts +25 -1
  205. package/dist/sync/workspaceSyncModel.js +77 -3
  206. package/dist/sync/workspaceSyncModel.test.js +153 -0
  207. package/dist/sync/workspaceSyncState.d.ts +2 -0
  208. package/dist/sync/workspaceSyncState.js +1 -0
  209. package/dist/types.d.ts +2 -10
  210. package/dist/ui/assets/AgentsModule-N2MnQBZk.js +1 -0
  211. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-C_TmXZwA.js → AnnotatedAttachmentWorkspace-BG6P_GVW.js} +1 -1
  212. package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +1 -0
  213. package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +252 -0
  214. package/dist/ui/assets/{InitiativesModule-4-Rh2K2n.js → InitiativesModule-Dhm7M8e7.js} +1 -1
  215. package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
  216. package/dist/ui/assets/PlansPage-Cq0zXj3I.js +1 -0
  217. package/dist/ui/assets/TaskSettings-ln0aF7xc.js +9 -0
  218. package/dist/ui/assets/{WorkflowsModule-CGk9s3NJ.js → WorkflowsModule-BcS4afRn.js} +1 -1
  219. package/dist/ui/assets/index-C2-OXZV7.css +1 -0
  220. package/dist/ui/assets/index-DcSI5F86.js +6 -0
  221. package/dist/ui/assets/{vendor-icons-pWocEipT.js → vendor-icons-BSUaRwF8.js} +1 -1
  222. package/dist/ui/index.html +4 -4
  223. package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
  224. package/dist/utils/commercialLifecyclePresentation.js +196 -0
  225. package/dist/utils/taskActivity.js +4 -0
  226. package/dist/utils/taskActivity.test.js +25 -1
  227. package/package.json +10 -1
  228. package/scripts/check-cloud-mcp.mjs +83 -0
  229. package/scripts/playwright-auth.sh +9 -1
  230. package/scripts/run-billing-performance-smoke.sh +24 -0
  231. package/dist/ui/assets/AgentsModule-CpsTmrIW.js +0 -1
  232. package/dist/ui/assets/DocumentWorkspace-C3GyzrWm.js +0 -250
  233. package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
  234. package/dist/ui/assets/PlansPage-BP7AYOqr.js +0 -1
  235. package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +0 -8
  236. package/dist/ui/assets/index-CLIMgR6g.js +0 -6
  237. package/dist/ui/assets/index-DneETxcu.css +0 -1
@@ -50,9 +50,9 @@
50
50
  import * as fs from 'fs';
51
51
  import * as path from 'path';
52
52
  import { TaskforceRuleError } from '../../core/Taskforce.js';
53
- import { parseJsonBody } from '../routes.js';
53
+ import { escapeHtml, parseFormBody, parseJsonBody } from './primitives.js';
54
54
  import { resolveRequestAccess } from '../auth.js';
55
- import { resolveEffectiveEmailConfig, sendEmail } from '../email.js';
55
+ import { sendEmail } from '../email.js';
56
56
  import { WorkspaceAssetStore } from '../../storage/workspaceAssetStore.js';
57
57
  import { scanDocumentIntegrity } from '../../storage/documentIntegrity.js';
58
58
  import { normalizeStorageIntegrityScheduleSettings, resolveStorageIntegrityScheduleSettings, } from '../../storage/integritySchedule.js';
@@ -64,67 +64,18 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/
64
64
  import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
65
65
  import { randomUUID } from 'crypto';
66
66
  import { resolveDeploymentConfig } from '../../config/deployment.js';
67
- import { getTaskforceCloudEnvironmentConfig, resolveTaskforceCloudEnvironmentFromBaseUrl } from '../../config/cloudEnvironment.js';
68
- import { resolveErrorStatusCode } from './shared.js';
67
+ import { getTaskforceCloudEnvironmentConfig, resolveTaskforceCloudEnvironmentFromBaseUrl, resolveTaskforceMcpBaseUrlFromBaseUrl } from '../../config/cloudEnvironment.js';
68
+ import { buildCloudMcpRuntimeAuth, resolveErrorStatusCode } from './shared.js';
69
+ import { buildAuthBehavior, buildSiteBehavior, getRouteEmailConfig, resolvePublicAuthBaseUrl, resolveRuntimeConfigBaseUrl, } from './authSupport.js';
69
70
  import { applyWorkspaceRepair, scanWorkspaceRepair } from '../../sync/workspaceRepair.js';
70
71
  // === SECTION: Registration ===
71
72
  export function registerAdminRoutes(deps) {
72
- const { addRoute, core, context, requestWorkspaceId, requestUserId, requestRole, requestHeader, requestRuntimeMode, ensureAppAccess, ensureAdminRole, ensureCloudPlanData, requireCloudRuntime, resolveAuthenticatedUserId, resolveAdminRequestActor, auditAdminAction, resolveEffectiveObjectStorageConfig, getObjectStorageClient, taskAssetStore, normalizeDocumentPathInput, ensureWithinDir, getAssetMetadata, markAssetDeleted, clearAssetDeleted, removeAssetMetadata, getStorageTelemetry, appendStorageTelemetry, getStorageIntegrityHistory, appendStorageIntegrityHistory, getDocumentPurgeHistory, appendDocumentPurgeHistory, getCollaborationTelemetry, isCollaborationPolicyEnabled, resolveRuntimeConfigBaseUrl, resolvePublicAuthBaseUrl, parseFormBody, escapeHtml, normalizeMcpOauthScopeString, MCP_TOKEN_SCOPE_VALUES, MCP_CLOUD_TOOL_SCOPE_REQUIREMENTS, MCP_WRITE_RATE_LIMIT_ENABLED, MCP_WRITE_RATE_LIMIT_ACTOR_MAX_REQUESTS, MCP_WRITE_RATE_LIMIT_WORKSPACE_MAX_REQUESTS, MCP_WRITE_RATE_LIMIT_WINDOW_MS, } = deps;
73
+ const { addRoute, core, context, requestWorkspaceId, requestUserId, requestRole, requestHeader, requestRuntimeMode, ensureAppAccess, ensureAdminRole, ensureCloudPlanData, requireCloudRuntime, resolveAuthenticatedUserId, resolveAdminRequestActor, auditAdminAction, resolveEffectiveObjectStorageConfig, getObjectStorageClient, taskAssetStore, normalizeDocumentPathInput, ensureWithinDir, getAssetMetadata, markAssetDeleted, clearAssetDeleted, removeAssetMetadata, getStorageTelemetry, appendStorageTelemetry, getStorageIntegrityHistory, appendStorageIntegrityHistory, getDocumentPurgeHistory, appendDocumentPurgeHistory, getCollaborationTelemetry, isCollaborationPolicyEnabled, normalizeMcpOauthScopeString, MCP_TOKEN_SCOPE_VALUES, MCP_CLOUD_TOOL_SCOPE_REQUIREMENTS, MCP_WRITE_RATE_LIMIT_ENABLED, MCP_WRITE_RATE_LIMIT_ACTOR_MAX_REQUESTS, MCP_WRITE_RATE_LIMIT_WORKSPACE_MAX_REQUESTS, MCP_WRITE_RATE_LIMIT_WINDOW_MS, } = deps;
73
74
  const baseVirusScanConfig = resolveVirusScanConfigFromEnv(process.env);
74
75
  const workspaceAssetStore = typeof core.getDatabaseAdapter === 'function' && typeof core.getTenantId === 'function'
75
76
  ? new WorkspaceAssetStore(core.getDatabaseAdapter(), core.getTenantId())
76
77
  : null;
77
78
  // === SECTION: Email helpers ===
78
- const getEmailConfig = () => {
79
- if (context.authConfig?.runtimeMode !== 'cloud') {
80
- return resolveEffectiveEmailConfig(process.env, { provider: 'console' });
81
- }
82
- const globalSettings = core.getGlobalSettings();
83
- return resolveEffectiveEmailConfig(process.env, globalSettings.email || null);
84
- };
85
- const resolveBooleanSetting = (value, fallback) => {
86
- if (typeof value === 'boolean')
87
- return value;
88
- if (typeof value === 'number')
89
- return value !== 0;
90
- const normalized = String(value || '').trim().toLowerCase();
91
- if (!normalized)
92
- return fallback;
93
- if (['1', 'true', 'yes', 'on'].includes(normalized))
94
- return true;
95
- if (['0', 'false', 'no', 'off'].includes(normalized))
96
- return false;
97
- return fallback;
98
- };
99
- const getAuthBehavior = () => {
100
- const globalSettings = core.getGlobalSettings();
101
- const authSettings = globalSettings.auth || {};
102
- const authRateLimitMaxRequestsRaw = Number(authSettings.authRateLimitMaxRequests);
103
- const authRateLimitWindowMsRaw = Number(authSettings.authRateLimitWindowMs);
104
- const onboardingPolicy = core.getOnboardingPolicy();
105
- return {
106
- allowSelfRegistration: resolveBooleanSetting(authSettings.allowSelfRegistration, true),
107
- requireVerifiedEmail: resolveBooleanSetting(authSettings.requireVerifiedEmail, true),
108
- defaultNewUserBetaAccess: resolveBooleanSetting(authSettings.defaultNewUserBetaAccess, false),
109
- authRateLimitEnabled: resolveBooleanSetting(authSettings.authRateLimitEnabled, true),
110
- authRateLimitMaxRequests: Number.isFinite(authRateLimitMaxRequestsRaw) && authRateLimitMaxRequestsRaw >= 1
111
- ? Math.floor(authRateLimitMaxRequestsRaw)
112
- : 10,
113
- authRateLimitWindowMs: Number.isFinite(authRateLimitWindowMsRaw) && authRateLimitWindowMsRaw >= 1000
114
- ? Math.floor(authRateLimitWindowMsRaw)
115
- : 15 * 60_000,
116
- onboardingPolicyMode: onboardingPolicy.mode,
117
- onboardingPlanVersionId: onboardingPolicy.planVersionId
118
- };
119
- };
120
- const getSiteBehavior = () => {
121
- const globalSettings = core.getGlobalSettings();
122
- const siteSettings = globalSettings.site || {};
123
- return {
124
- showRunTaskforceLocally: resolveBooleanSetting(siteSettings.showRunTaskforceLocally, true),
125
- pricingPageEnabled: resolveBooleanSetting(siteSettings.pricingPageEnabled, true)
126
- };
127
- };
128
79
  const resolveEffectiveVirusScanSettings = () => {
129
80
  const settings = core.getGlobalSettings().objectStorage || {};
130
81
  const asyncEnabled = typeof settings.virusScanAsyncEnabled === 'boolean'
@@ -139,6 +90,7 @@ export function registerAdminRoutes(deps) {
139
90
  };
140
91
  };
141
92
  // === SECTION: MCP helpers ===
93
+ const MCP_CONNECTION_TEST_TIMEOUT_MS = 5000;
142
94
  const writeMcpHttpError = (res, statusCode, message) => {
143
95
  res.writeHead(statusCode, { 'Content-Type': 'application/json' });
144
96
  res.end(JSON.stringify({
@@ -171,6 +123,112 @@ export function registerAdminRoutes(deps) {
171
123
  const resolveMcpOauthTokenUrl = (req) => `${resolveMcpOauthIssuer(req)}/oauth/mcp/token`;
172
124
  const resolveMcpOauthRegisterUrl = (req) => `${resolveMcpOauthIssuer(req)}/oauth/mcp/register`;
173
125
  const resolveMcpOauthResourceUrl = (req) => `${resolveMcpOauthIssuer(req)}/mcp`;
126
+ const resolveCloudMcpEndpoint = (req) => {
127
+ const requestBaseUrl = resolveRuntimeConfigBaseUrl(req);
128
+ const deployment = resolveDeploymentConfig(process.env, { requestBaseUrl });
129
+ const inferredMcpBaseUrl = resolveTaskforceMcpBaseUrlFromBaseUrl(deployment.cloudMcpBaseUrl
130
+ || deployment.cloudAuthBaseUrl
131
+ || deployment.cloudBaseUrl
132
+ || requestBaseUrl
133
+ || '');
134
+ const baseUrl = String(inferredMcpBaseUrl || deployment.cloudMcpBaseUrl || resolveMcpOauthIssuer(req)).trim().replace(/\/+$/, '');
135
+ return `${baseUrl}/mcp`;
136
+ };
137
+ const probeCloudMcpEndpoint = async (req, token) => {
138
+ const endpoint = resolveCloudMcpEndpoint(req);
139
+ const controller = new AbortController();
140
+ const timeout = setTimeout(() => controller.abort(), MCP_CONNECTION_TEST_TIMEOUT_MS);
141
+ try {
142
+ const response = await fetch(endpoint, {
143
+ method: 'POST',
144
+ headers: {
145
+ Authorization: `Bearer ${token}`,
146
+ Accept: 'application/json, text/event-stream',
147
+ 'Content-Type': 'application/json'
148
+ },
149
+ body: JSON.stringify({
150
+ jsonrpc: '2.0',
151
+ id: `mcp-connection-test-${randomUUID()}`,
152
+ method: 'initialize',
153
+ params: {
154
+ protocolVersion: '2025-03-26',
155
+ capabilities: {},
156
+ clientInfo: {
157
+ name: 'taskforce-settings-test',
158
+ version: '1.0.0'
159
+ }
160
+ }
161
+ }),
162
+ signal: controller.signal
163
+ });
164
+ const responseText = await response.text();
165
+ let payload = null;
166
+ try {
167
+ payload = responseText ? JSON.parse(responseText) : null;
168
+ }
169
+ catch {
170
+ payload = null;
171
+ }
172
+ const remoteMessage = typeof payload?.error?.message === 'string'
173
+ ? payload.error.message.trim()
174
+ : '';
175
+ if (!response.ok) {
176
+ if (response.status >= 500) {
177
+ return {
178
+ ok: false,
179
+ statusCode: 502,
180
+ code: 'MCP_ENDPOINT_GATEWAY_ERROR',
181
+ error: `Cloud MCP endpoint ${endpoint} returned ${response.status}. The dedicated MCP host may be degraded or unavailable.`,
182
+ details: {
183
+ endpoint,
184
+ upstreamStatus: response.status,
185
+ remoteMessage: remoteMessage || null
186
+ }
187
+ };
188
+ }
189
+ return {
190
+ ok: false,
191
+ statusCode: 502,
192
+ code: 'MCP_ENDPOINT_REJECTED_VALIDATED_TOKEN',
193
+ error: `Cloud MCP endpoint ${endpoint} rejected a token that validated locally. This usually means the MCP service is stale, routed incorrectly, or out of sync with the auth database.`,
194
+ details: {
195
+ endpoint,
196
+ upstreamStatus: response.status,
197
+ remoteMessage: remoteMessage || null
198
+ }
199
+ };
200
+ }
201
+ return {
202
+ ok: true,
203
+ endpoint,
204
+ serverName: typeof payload?.result?.serverInfo?.name === 'string'
205
+ ? payload.result.serverInfo.name
206
+ : null,
207
+ protocolVersion: typeof payload?.result?.protocolVersion === 'string'
208
+ ? payload.result.protocolVersion
209
+ : null
210
+ };
211
+ }
212
+ catch (error) {
213
+ const timedOut = error?.name === 'AbortError';
214
+ return {
215
+ ok: false,
216
+ statusCode: 502,
217
+ code: timedOut ? 'MCP_ENDPOINT_TIMEOUT' : 'MCP_ENDPOINT_UNREACHABLE',
218
+ error: timedOut
219
+ ? `Cloud MCP endpoint ${endpoint} did not respond within ${MCP_CONNECTION_TEST_TIMEOUT_MS}ms.`
220
+ : `Could not reach cloud MCP endpoint ${endpoint}. The dedicated MCP host may be unavailable or misconfigured.`,
221
+ details: {
222
+ endpoint,
223
+ timeoutMs: MCP_CONNECTION_TEST_TIMEOUT_MS,
224
+ reason: String(error?.message || error || '')
225
+ }
226
+ };
227
+ }
228
+ finally {
229
+ clearTimeout(timeout);
230
+ }
231
+ };
174
232
  const renderMcpOauthAuthorizePage = (input) => {
175
233
  const options = input.workspaces.map((workspace) => {
176
234
  const selected = workspace.id === input.preselectedWorkspaceId ? ' selected' : '';
@@ -474,22 +532,13 @@ export function registerAdminRoutes(deps) {
474
532
  serverName: 'taskforce-cloud',
475
533
  logStartup: false,
476
534
  runtimeMode: 'cloud',
477
- auth: {
478
- scopes: authScopes,
479
- userId: auth.authenticatedViaToken?.userId || auth.authenticatedViaOAuth?.userId || auth.userId || null,
480
- tokenId: auth.authenticatedViaToken?.tokenId || auth.authenticatedViaOAuth?.grantId || null,
481
- aiProfileToken: auth.aiProfileToken,
482
- aiProfileName: auth.aiProfileName,
483
- aiProfileDescription: auth.aiProfileDescription,
484
- aiProfileRole: auth.aiProfileRole,
485
- aiProfileProvider: auth.aiProfileProvider,
486
- aiProfileModel: auth.aiProfileModel,
487
- aiProfileSurfaceType: auth.aiProfileSurfaceType,
488
- actorType: auth.authenticatedViaToken ? 'token' : auth.authenticatedViaOAuth ? 'oauth' : 'user',
535
+ auth: buildCloudMcpRuntimeAuth({
536
+ req,
537
+ auth,
538
+ requestHeader,
489
539
  requestId: String(req.headers['x-request-id'] || req.headers['x-railway-request-id'] || '').trim() || randomUUID(),
490
- ip: req.socket?.remoteAddress || null,
491
- userAgent: requestHeader(req, 'user-agent', '') || null,
492
- },
540
+ authScopes,
541
+ }),
493
542
  });
494
543
  const transport = new StreamableHTTPServerTransport({
495
544
  sessionIdGenerator: undefined,
@@ -1022,7 +1071,7 @@ export function registerAdminRoutes(deps) {
1022
1071
  return;
1023
1072
  }
1024
1073
  if (context.authConfig?.runtimeMode !== 'cloud') {
1025
- const effective = getEmailConfig();
1074
+ const effective = getRouteEmailConfig(core, context);
1026
1075
  res.writeHead(200, { 'Content-Type': 'application/json' });
1027
1076
  res.end(JSON.stringify({
1028
1077
  settings: { provider: 'console', from: effective.from, replyTo: '' },
@@ -1031,7 +1080,7 @@ export function registerAdminRoutes(deps) {
1031
1080
  return;
1032
1081
  }
1033
1082
  const settings = core.getGlobalSettings().email || {};
1034
- const effective = getEmailConfig();
1083
+ const effective = getRouteEmailConfig(core, context);
1035
1084
  res.writeHead(200, { 'Content-Type': 'application/json' });
1036
1085
  res.end(JSON.stringify({
1037
1086
  settings: {
@@ -1056,7 +1105,7 @@ export function registerAdminRoutes(deps) {
1056
1105
  return;
1057
1106
  }
1058
1107
  const settings = core.getGlobalSettings().email || {};
1059
- const effective = getEmailConfig();
1108
+ const effective = getRouteEmailConfig(core, context);
1060
1109
  const keyRequired = effective.provider === 'resend' || effective.provider === 'google-workspace';
1061
1110
  const ready = keyRequired ? Boolean(effective.providerApiKey) : true;
1062
1111
  const reason = ready
@@ -1088,7 +1137,7 @@ export function registerAdminRoutes(deps) {
1088
1137
  const globalSettings = core.getGlobalSettings();
1089
1138
  const authSettings = globalSettings.auth || {};
1090
1139
  const billingSettings = globalSettings.billing || {};
1091
- const effective = getAuthBehavior();
1140
+ const effective = buildAuthBehavior(core);
1092
1141
  res.writeHead(200, { 'Content-Type': 'application/json' });
1093
1142
  res.end(JSON.stringify({
1094
1143
  settings: {
@@ -1126,7 +1175,7 @@ export function registerAdminRoutes(deps) {
1126
1175
  return;
1127
1176
  }
1128
1177
  const siteSettings = core.getGlobalSettings().site || {};
1129
- const siteBehavior = getSiteBehavior();
1178
+ const siteBehavior = buildSiteBehavior(core);
1130
1179
  res.writeHead(200, { 'Content-Type': 'application/json' });
1131
1180
  res.end(JSON.stringify({
1132
1181
  settings: {
@@ -1741,17 +1790,20 @@ export function registerAdminRoutes(deps) {
1741
1790
  return;
1742
1791
  }
1743
1792
  const body = await parseJsonBody(req);
1793
+ const currentOnboardingPolicy = core.getOnboardingPolicy();
1744
1794
  const onboardingPolicyMode = body?.onboardingPolicyMode === 'auto_grant_free' || body?.onboardingPolicyMode === 'auto_start_trial'
1745
1795
  ? body.onboardingPolicyMode
1746
- : 'require_plan_selection';
1796
+ : body?.onboardingPolicyMode === 'require_plan_selection'
1797
+ ? 'require_plan_selection'
1798
+ : currentOnboardingPolicy.mode;
1747
1799
  const onboardingPlanVersionId = typeof body?.onboardingPlanVersionId === 'string'
1748
1800
  ? body.onboardingPlanVersionId.trim() || null
1749
1801
  : body?.onboardingPlanVersionId === null
1750
1802
  ? null
1751
- : undefined;
1803
+ : currentOnboardingPolicy.planVersionId;
1752
1804
  const validation = core.validateOnboardingPolicy({
1753
1805
  mode: onboardingPolicyMode,
1754
- ...(onboardingPlanVersionId !== undefined ? { planVersionId: onboardingPlanVersionId } : {})
1806
+ planVersionId: onboardingPlanVersionId
1755
1807
  });
1756
1808
  if (!validation.valid) {
1757
1809
  res.writeHead(400, { 'Content-Type': 'application/json' });
@@ -1848,7 +1900,7 @@ export function registerAdminRoutes(deps) {
1848
1900
  res.end(JSON.stringify({ success: false, error: 'Valid test email address is required.' }));
1849
1901
  return;
1850
1902
  }
1851
- const config = getEmailConfig();
1903
+ const config = getRouteEmailConfig(core, context);
1852
1904
  const result = await sendEmail({
1853
1905
  to,
1854
1906
  subject: 'Taskforce email settings test',
@@ -2273,9 +2325,28 @@ export function registerAdminRoutes(deps) {
2273
2325
  res.end(JSON.stringify({ success: false, error: 'Token is missing required scopes.', code: 'MCP_TOKEN_MISSING_SCOPES', details: { requiredScopes, missingScopes, grantedScopes: inspection.scopes } }));
2274
2326
  return;
2275
2327
  }
2328
+ const probe = await probeCloudMcpEndpoint(req, token);
2329
+ if (!probe.ok) {
2330
+ res.writeHead(probe.statusCode, { 'Content-Type': 'application/json' });
2331
+ res.end(JSON.stringify({ success: false, error: probe.error, code: probe.code, details: probe.details }));
2332
+ return;
2333
+ }
2276
2334
  const workspace = core.getWorkspaceProfile(wid);
2277
2335
  res.writeHead(200, { 'Content-Type': 'application/json' });
2278
- res.end(JSON.stringify({ success: true, result: { tokenId: inspection.tokenId, tokenName: inspection.name, workspaceId: wid, workspaceName: String(workspace?.name || wid), scopes: inspection.scopes, requiredScopes, endpoint: '/mcp' } }));
2336
+ res.end(JSON.stringify({
2337
+ success: true,
2338
+ result: {
2339
+ tokenId: inspection.tokenId,
2340
+ tokenName: inspection.name,
2341
+ workspaceId: wid,
2342
+ workspaceName: String(workspace?.name || wid),
2343
+ scopes: inspection.scopes,
2344
+ requiredScopes,
2345
+ endpoint: probe.endpoint,
2346
+ serverName: probe.serverName,
2347
+ protocolVersion: probe.protocolVersion
2348
+ }
2349
+ }));
2279
2350
  }
2280
2351
  catch (error) {
2281
2352
  const statusCode = resolveErrorStatusCode(error);
@@ -13,7 +13,7 @@
13
13
  */
14
14
  import type { IncomingMessage, ServerResponse } from 'http';
15
15
  import type { TaskforceCore } from '../../core/Taskforce.js';
16
- import type { RouteHandler } from '../routes.js';
16
+ import { type RouteHandler } from './primitives.js';
17
17
  export interface AnnotatedAttachmentRouteDependencies {
18
18
  addRoute: (method: string, path: string, handler: RouteHandler) => void;
19
19
  core: TaskforceCore;
@@ -12,7 +12,7 @@
12
12
  * GET /api/taskforce/annotated-attachments/sessions/:id/payload
13
13
  */
14
14
  import { ANNOTATED_ATTACHMENTS_WORKSPACE_FEATURE_KEY, resolveAppGateAccess, } from '../../runtime/appGates.js';
15
- import { parseJsonBody } from '../routes.js';
15
+ import { parseJsonBody } from './primitives.js';
16
16
  import { resolveErrorStatusCode } from './shared.js';
17
17
  import { getImageReferenceLabel, parseImageReference } from '../../utils/imageReferences.js';
18
18
  function writeJson(res, statusCode, payload) {
@@ -37,7 +37,7 @@
37
37
  */
38
38
  import type { IncomingMessage, ServerResponse } from 'http';
39
39
  import type { TaskforceCore } from '../../core/Taskforce.js';
40
- import type { RouteHandler, RouteContext } from '../routes.js';
40
+ import { type RouteHandler, type RouteContext } from './primitives.js';
41
41
  import type { ResolvedObjectStorageConfig } from '../../storage/objectStorage.js';
42
42
  import { ObjectStorageClient } from '../../storage/objectStorageClient.js';
43
43
  export interface AuthRouteDependencies {
@@ -58,9 +58,6 @@ export interface AuthRouteDependencies {
58
58
  userId: string;
59
59
  systemRole: 'system_admin' | 'user';
60
60
  };
61
- shouldExposeDevAuthTokens: () => boolean;
62
- resolveRuntimeConfigBaseUrl: (req: IncomingMessage) => string;
63
- resolvePublicAuthBaseUrl: (req: IncomingMessage) => string;
64
61
  resolveEffectiveObjectStorageConfig: () => ResolvedObjectStorageConfig;
65
62
  getObjectStorageClient: () => ObjectStorageClient | null;
66
63
  }
@@ -38,36 +38,23 @@
38
38
  import * as fs from 'fs';
39
39
  import * as path from 'path';
40
40
  import { randomUUID } from 'crypto';
41
- import { parseJsonBody } from '../routes.js';
41
+ import { parseJsonBody } from './primitives.js';
42
42
  import { hasValidSession, createClearSessionCookie, createSessionTokenCookie, resolveRequestAccess, resolveSessionCookieAttributesForRequest, } from '../auth.js';
43
- import { resolveEffectiveEmailConfig, sendEmail } from '../email.js';
43
+ import { sendEmail } from '../email.js';
44
44
  import { resolveDeploymentConfig } from '../../config/deployment.js';
45
45
  import { resolveRuntimeModeContract } from '../../shared/runtimeContract.js';
46
46
  import { requestHeader, resolveErrorStatusCode } from './shared.js';
47
+ import { buildAuthFlowBehavior, buildSiteBehavior, getRouteEmailConfig, resolvePublicAuthBaseUrl, resolveRuntimeConfigBaseUrl, shouldExposeDevAuthTokens } from './authSupport.js';
47
48
  import { normalizeTaskDataRelativePath } from '../../storage/objectStorageClient.js';
48
49
  // === SECTION: Registration ===
49
50
  export function registerAuthRoutes(deps) {
50
- const { addRoute, core, context, requestWorkspaceId, requestUserId, ensureAppAccess, ensureAuthenticatedUser, ensureWorkspaceAdminRole, ensureCloudPlanData, requireCloudRuntime, auditAuthAction, resolveAdminRequestActor, shouldExposeDevAuthTokens, resolveRuntimeConfigBaseUrl, resolvePublicAuthBaseUrl, resolveEffectiveObjectStorageConfig, getObjectStorageClient, } = deps;
51
+ const { addRoute, core, context, requestWorkspaceId, requestUserId, ensureAppAccess, ensureAuthenticatedUser, ensureWorkspaceAdminRole, ensureCloudPlanData, requireCloudRuntime, auditAuthAction, resolveAdminRequestActor, resolveEffectiveObjectStorageConfig, getObjectStorageClient, } = deps;
51
52
  const normalizeTeamWorkspaceRole = (value) => {
52
53
  const role = String(value || '').trim().toLowerCase();
53
54
  if (role === 'owner' || role === 'admin' || role === 'member' || role === 'read-only')
54
55
  return role;
55
56
  return null;
56
57
  };
57
- const resolveBooleanSetting = (value, fallback) => {
58
- if (typeof value === 'boolean')
59
- return value;
60
- if (typeof value === 'number')
61
- return value !== 0;
62
- const normalized = String(value || '').trim().toLowerCase();
63
- if (!normalized)
64
- return fallback;
65
- if (['1', 'true', 'yes', 'on'].includes(normalized))
66
- return true;
67
- if (['0', 'false', 'no', 'off'].includes(normalized))
68
- return false;
69
- return fallback;
70
- };
71
58
  const resolveTeamPlanModeFromEntitlements = (payload) => {
72
59
  const state = String(payload?.state || '').trim().toLowerCase();
73
60
  const teamAllowed = payload?.features?.['collaboration.team_management']?.allowed === true;
@@ -190,42 +177,6 @@ export function registerAuthRoutes(deps) {
190
177
  };
191
178
  };
192
179
  // === SECTION: Email delivery helpers ===
193
- const getEmailConfig = () => {
194
- if (context.authConfig?.runtimeMode !== 'cloud') {
195
- return resolveEffectiveEmailConfig(process.env, { provider: 'console' });
196
- }
197
- const globalSettings = core.getGlobalSettings();
198
- return resolveEffectiveEmailConfig(process.env, globalSettings.email || null);
199
- };
200
- const getAuthBehavior = () => {
201
- const globalSettings = core.getGlobalSettings();
202
- const authSettings = globalSettings.auth || {};
203
- const authRateLimitMaxRequestsRaw = Number(authSettings.authRateLimitMaxRequests);
204
- const authRateLimitWindowMsRaw = Number(authSettings.authRateLimitWindowMs);
205
- const onboardingPolicy = core.getOnboardingPolicy();
206
- return {
207
- allowSelfRegistration: resolveBooleanSetting(authSettings.allowSelfRegistration, true),
208
- requireVerifiedEmail: resolveBooleanSetting(authSettings.requireVerifiedEmail, true),
209
- defaultNewUserBetaAccess: resolveBooleanSetting(authSettings.defaultNewUserBetaAccess, false),
210
- authRateLimitEnabled: resolveBooleanSetting(authSettings.authRateLimitEnabled, true),
211
- authRateLimitMaxRequests: Number.isFinite(authRateLimitMaxRequestsRaw) && authRateLimitMaxRequestsRaw >= 1
212
- ? Math.floor(authRateLimitMaxRequestsRaw)
213
- : 10,
214
- authRateLimitWindowMs: Number.isFinite(authRateLimitWindowMsRaw) && authRateLimitWindowMsRaw >= 1000
215
- ? Math.floor(authRateLimitWindowMsRaw)
216
- : 15 * 60_000,
217
- onboardingPolicyMode: onboardingPolicy.mode,
218
- onboardingPlanVersionId: onboardingPolicy.planVersionId
219
- };
220
- };
221
- const getSiteBehavior = () => {
222
- const globalSettings = core.getGlobalSettings();
223
- const siteSettings = globalSettings.site || {};
224
- return {
225
- showRunTaskforceLocally: resolveBooleanSetting(siteSettings.showRunTaskforceLocally, true),
226
- pricingPageEnabled: resolveBooleanSetting(siteSettings.pricingPageEnabled, true)
227
- };
228
- };
229
180
  const buildPasswordSessionCookieForUser = (req, authConfig, userIdRaw, options) => {
230
181
  const userId = String(userIdRaw || '').trim();
231
182
  const fallbackRole = String(options?.fallbackRole || authConfig.defaultRole || 'member').trim() || 'member';
@@ -308,13 +259,13 @@ export function registerAuthRoutes(deps) {
308
259
  ? 'Verify your Taskforce account'
309
260
  : 'Reset your Taskforce password';
310
261
  const text = kind === 'verify-email'
311
- ? `Welcome to Taskforce.\n\nVerify your email by opening this link:\n${link}\n\nIf you did not create an account, you can ignore this message.`
262
+ ? `Welcome to Taskforce.\n\nYou're almost in. Enter this verification token on the verification screen to activate your account:\n\n${token}\n\nPrefer the faster route? Open the link below to verify your email automatically:\n${link}\n\nIf you didn't create a Taskforce account, you can safely ignore this email.`
312
263
  : `Reset your Taskforce password using this link:\n${link}\n\nIf you did not request this reset, you can ignore this message.`;
313
264
  const result = await sendEmail({
314
265
  to: email,
315
266
  subject,
316
267
  text
317
- }, getEmailConfig());
268
+ }, getRouteEmailConfig(core, context));
318
269
  if (result.ok) {
319
270
  console.info(`[Taskforce Auth Email] kind=${kind} provider=${result.provider} to=${email} messageId=${result.messageId || '-'}`);
320
271
  }
@@ -332,7 +283,7 @@ export function registerAuthRoutes(deps) {
332
283
  to: email,
333
284
  subject,
334
285
  text
335
- }, getEmailConfig());
286
+ }, getRouteEmailConfig(core, context));
336
287
  if (result.ok) {
337
288
  console.info(`[Taskforce Auth Email] kind=invite provider=${result.provider} to=${email} messageId=${result.messageId || '-'}`);
338
289
  }
@@ -368,7 +319,7 @@ export function registerAuthRoutes(deps) {
368
319
  }));
369
320
  });
370
321
  addRoute('GET', '/api/taskforce/public/site-config', async (_req, res) => {
371
- const siteBehavior = getSiteBehavior();
322
+ const siteBehavior = buildSiteBehavior(core);
372
323
  res.writeHead(200, {
373
324
  'Content-Type': 'application/json',
374
325
  'Cross-Origin-Resource-Policy': 'cross-origin'
@@ -379,7 +330,7 @@ export function registerAuthRoutes(deps) {
379
330
  }));
380
331
  });
381
332
  addRoute('GET', '/api/taskforce/public/site-config-script', async (_req, res) => {
382
- const siteBehavior = getSiteBehavior();
333
+ const siteBehavior = buildSiteBehavior(core);
383
334
  const payload = JSON.stringify(siteBehavior).replace(/</g, '\\u003c');
384
335
  res.writeHead(200, {
385
336
  'Content-Type': 'application/javascript; charset=utf-8',
@@ -401,7 +352,8 @@ export function registerAuthRoutes(deps) {
401
352
  let avatarUrl = null;
402
353
  let betaAccess = true;
403
354
  if (authenticated && access?.userId) {
404
- await cleanupStaleAvatarDrafts(access.userId);
355
+ // Keep the session endpoint fast on reload; avatar draft cleanup is best-effort housekeeping.
356
+ void cleanupStaleAvatarDrafts(access.userId).catch(() => { });
405
357
  betaAccess = core.getUserBetaAccess(access.userId);
406
358
  const resolved = core.resolveUserAccess(access.userId, workspaceId, role);
407
359
  if (resolved) {
@@ -704,7 +656,7 @@ export function registerAuthRoutes(deps) {
704
656
  const workspaceId = workspaceIdCandidate || 'default';
705
657
  // Password flow (preferred)
706
658
  if (email && password) {
707
- const authBehavior = getAuthBehavior();
659
+ const authBehavior = buildAuthFlowBehavior(core);
708
660
  let authResult = core.authenticatePasswordUserDetailed({
709
661
  email,
710
662
  password,
@@ -801,7 +753,7 @@ export function registerAuthRoutes(deps) {
801
753
  const selectedPlanVersionId = typeof body?.planVersionId === 'string'
802
754
  ? body.planVersionId.trim()
803
755
  : '';
804
- const authBehavior = getAuthBehavior();
756
+ const authBehavior = buildAuthFlowBehavior(core);
805
757
  if (!authBehavior.allowSelfRegistration) {
806
758
  res.writeHead(403, { 'Content-Type': 'application/json' });
807
759
  res.end(JSON.stringify({ success: false, error: 'Self-registration is currently disabled.' }));
@@ -870,6 +822,43 @@ export function registerAuthRoutes(deps) {
870
822
  }));
871
823
  }
872
824
  catch (error) {
825
+ if (error?.code === 'EMAIL_ALREADY_EXISTS'
826
+ && authBehavior.requireVerifiedEmail
827
+ && email
828
+ && !core.isEmailVerified(email)) {
829
+ let emailSent = false;
830
+ let deliveryAttempted = false;
831
+ let emailError;
832
+ let verificationToken;
833
+ const verification = core.requestEmailVerification({ email });
834
+ verificationToken = verification.token || null;
835
+ if (verification.token) {
836
+ deliveryAttempted = true;
837
+ const delivery = await deliverAuthEmail('verify-email', email, verification.token, req);
838
+ emailSent = delivery.ok;
839
+ emailError = delivery.error;
840
+ }
841
+ auditAuthAction(req, 'auth-register-existing-unverified', {
842
+ email,
843
+ workspaceId: workspaceIdInput || null,
844
+ code: 'EMAIL_ALREADY_EXISTS_UNVERIFIED',
845
+ emailSent
846
+ });
847
+ res.writeHead(409, { 'Content-Type': 'application/json' });
848
+ res.end(JSON.stringify({
849
+ success: false,
850
+ error: 'This email already has an unverified account. Continue with email verification instead.',
851
+ code: 'EMAIL_ALREADY_EXISTS_UNVERIFIED',
852
+ verificationRequired: true,
853
+ emailSent,
854
+ deliveryAttempted,
855
+ emailError,
856
+ verificationToken: shouldExposeDevAuthTokens()
857
+ ? (verificationToken || null)
858
+ : undefined
859
+ }));
860
+ return;
861
+ }
873
862
  const statusCode = Number(error?.statusCode || 400);
874
863
  const message = String(error?.message || 'Unable to register');
875
864
  auditAuthAction(req, 'auth-register-failed', {
@@ -895,10 +884,12 @@ export function registerAuthRoutes(deps) {
895
884
  return;
896
885
  }
897
886
  try {
898
- let emailSent = true;
887
+ let emailSent = false;
888
+ let deliveryAttempted = false;
899
889
  let emailError;
900
890
  const verification = core.requestEmailVerification({ email });
901
891
  if (verification.token) {
892
+ deliveryAttempted = true;
902
893
  const delivery = await deliverAuthEmail('verify-email', email, verification.token, req);
903
894
  emailSent = delivery.ok;
904
895
  emailError = delivery.error;
@@ -911,6 +902,7 @@ export function registerAuthRoutes(deps) {
911
902
  res.end(JSON.stringify({
912
903
  success: true,
913
904
  emailSent,
905
+ deliveryAttempted,
914
906
  emailError,
915
907
  verificationToken: shouldExposeDevAuthTokens() ? (verification.token || null) : undefined
916
908
  }));
@@ -983,10 +975,12 @@ export function registerAuthRoutes(deps) {
983
975
  return;
984
976
  }
985
977
  try {
986
- let emailSent = true;
978
+ let emailSent = false;
979
+ let deliveryAttempted = false;
987
980
  let emailError;
988
981
  const reset = core.requestPasswordReset({ email });
989
982
  if (reset.token) {
983
+ deliveryAttempted = true;
990
984
  const delivery = await deliverAuthEmail('password-reset', email, reset.token, req);
991
985
  emailSent = delivery.ok;
992
986
  emailError = delivery.error;
@@ -999,6 +993,7 @@ export function registerAuthRoutes(deps) {
999
993
  res.end(JSON.stringify({
1000
994
  success: true,
1001
995
  emailSent,
996
+ deliveryAttempted,
1002
997
  emailError,
1003
998
  resetToken: shouldExposeDevAuthTokens() ? (reset.token || null) : undefined
1004
999
  }));
@@ -1354,7 +1349,7 @@ export function registerAuthRoutes(deps) {
1354
1349
  seatUsage,
1355
1350
  upgradeHints: canInvite
1356
1351
  ? []
1357
- : ['Team management requires a TEAM plan in active/trialing/grace state.'],
1352
+ : ['Team management requires an active plan with collaboration access.'],
1358
1353
  supportLink: 'mailto:support@taskforcehq.ai'
1359
1354
  }));
1360
1355
  }
@@ -0,0 +1,30 @@
1
+ import type { IncomingMessage } from 'http';
2
+ import type { TaskforceCore } from '../../core/Taskforce.js';
3
+ import type { RouteContext } from './primitives.js';
4
+ export declare function resolveBooleanSetting(value: unknown, fallback: boolean): boolean;
5
+ export declare function getRouteEmailConfig(core: TaskforceCore, context: RouteContext): import("../email.js").TaskforceEmailConfig;
6
+ export declare function buildAuthFlowBehavior(core: TaskforceCore): {
7
+ allowSelfRegistration: boolean;
8
+ requireVerifiedEmail: boolean;
9
+ defaultNewUserBetaAccess: boolean;
10
+ authRateLimitEnabled: boolean;
11
+ authRateLimitMaxRequests: number;
12
+ authRateLimitWindowMs: number;
13
+ };
14
+ export declare function buildAuthBehavior(core: TaskforceCore): {
15
+ onboardingPolicyMode: import("../../core/types.js").OnboardingPolicyMode;
16
+ onboardingPlanVersionId: string | null;
17
+ allowSelfRegistration: boolean;
18
+ requireVerifiedEmail: boolean;
19
+ defaultNewUserBetaAccess: boolean;
20
+ authRateLimitEnabled: boolean;
21
+ authRateLimitMaxRequests: number;
22
+ authRateLimitWindowMs: number;
23
+ };
24
+ export declare function buildSiteBehavior(core: TaskforceCore): {
25
+ showRunTaskforceLocally: boolean;
26
+ pricingPageEnabled: boolean;
27
+ };
28
+ export declare function shouldExposeDevAuthTokens(): boolean;
29
+ export declare function resolveRuntimeConfigBaseUrl(req: IncomingMessage): string;
30
+ export declare function resolvePublicAuthBaseUrl(req: IncomingMessage): string;