@taskforcehq/taskforce 0.3.304 → 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 (138) hide show
  1. package/README.md +140 -247
  2. package/dist/TaskforceCore.js +165 -70
  3. package/dist/TaskforceCore.test.js +799 -90
  4. package/dist/compat/workspaceSyncCompat.js +6 -0
  5. package/dist/components/features/DocumentIndex.d.ts +1 -1
  6. package/dist/components/features/DocumentIndex.js +1 -1
  7. package/dist/components/features/DocumentViewer.d.ts +1 -1
  8. package/dist/components/features/DocumentViewer.js +32 -9
  9. package/dist/components/features/DocumentWorkspace.d.ts +5 -22
  10. package/dist/components/features/DocumentWorkspace.js +12 -155
  11. package/dist/components/features/DocumentWorkspace.test.js +193 -6
  12. package/dist/components/features/TaskSettings.js +29 -3
  13. package/dist/components/features/TaskSettings.test.js +42 -1
  14. package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
  15. package/dist/components/features/documentWorkspaceModel.js +57 -0
  16. package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
  17. package/dist/components/features/useDocumentWorkspaceController.js +113 -0
  18. package/dist/components/task/TaskForm.d.ts +8 -8
  19. package/dist/components/task/TaskForm.js +358 -221
  20. package/dist/components/task/TaskForm.test.js +456 -381
  21. package/dist/components/task/TaskKanban.js +2 -1
  22. package/dist/components/views/PlanComparisonPage.js +5 -8
  23. package/dist/components/views/PlanComparisonPage.test.js +51 -41
  24. package/dist/components/views/PlansPage.js +126 -84
  25. package/dist/components/views/PlansPage.test.js +308 -39
  26. package/dist/components/views/StandaloneLayout.js +28 -22
  27. package/dist/components/views/panels/FilterToolbar.test.js +6 -4
  28. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +2 -14
  29. package/dist/components/views/standalone/modals/AccountHubModal.js +12 -14
  30. package/dist/components/views/standalone/modals/AccountHubModal.test.js +24 -1
  31. package/dist/config/envSchema.js +1 -1
  32. package/dist/core/AiProfileService.d.ts +6 -1
  33. package/dist/core/AiProfileService.js +161 -16
  34. package/dist/core/AiProfileService.test.js +3 -1
  35. package/dist/core/AiProfiles.test.js +200 -0
  36. package/dist/core/AuthTokenService.test.d.ts +1 -0
  37. package/dist/core/AuthTokenService.test.js +78 -0
  38. package/dist/core/EntitlementsPolicy.test.js +75 -13
  39. package/dist/core/PlanEntitlementService.d.ts +7 -1
  40. package/dist/core/PlanEntitlementService.js +117 -42
  41. package/dist/core/PlanVersionPolicy.test.js +19 -26
  42. package/dist/core/SignupMonetizationSettings.test.js +46 -21
  43. package/dist/core/SystemAdmin.test.js +212 -56
  44. package/dist/core/TaskTaxonomyValidation.test.js +53 -0
  45. package/dist/core/Taskforce.d.ts +3 -0
  46. package/dist/core/Taskforce.js +50 -10
  47. package/dist/core/WorkspacePlanMode.test.js +12 -12
  48. package/dist/core/shared.d.ts +2 -0
  49. package/dist/core/shared.js +11 -0
  50. package/dist/hooks/sync/auth.js +3 -1
  51. package/dist/hooks/sync/transfers.d.ts +2 -1
  52. package/dist/hooks/sync/transfers.js +8 -4
  53. package/dist/hooks/useSyncOrchestrator.d.ts +7 -4
  54. package/dist/hooks/useSyncOrchestrator.js +75 -7
  55. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +80 -10
  56. package/dist/hooks/useTaskMutations.d.ts +2 -1
  57. package/dist/hooks/useTaskMutations.js +12 -1
  58. package/dist/hooks/useTaskMutations.test.js +30 -1
  59. package/dist/hooks/useTaskforce.d.ts +5 -2
  60. package/dist/hooks/useTaskforce.js +17 -5
  61. package/dist/hooks/useTaskforce.sync-behavior.test.js +78 -0
  62. package/dist/hooks/useWorkspaceSyncController.d.ts +3 -1
  63. package/dist/hooks/useWorkspaceSyncController.js +11 -1
  64. package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
  65. package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
  66. package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
  67. package/dist/mcp/collaborationRegistrar.d.ts +2 -0
  68. package/dist/mcp/collaborationRegistrar.js +71 -0
  69. package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
  70. package/dist/mcp/documentAssetRegistrar.js +346 -0
  71. package/dist/mcp/runtime.js +2362 -3530
  72. package/dist/mcp/runtime.test.js +159 -0
  73. package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
  74. package/dist/mcp/taskPlanningRegistrar.js +418 -0
  75. package/dist/mcp/toolCatalog.d.ts +35 -0
  76. package/dist/mcp/toolCatalog.js +3 -0
  77. package/dist/server/routes/admin.d.ts +1 -5
  78. package/dist/server/routes/admin.js +138 -61
  79. package/dist/server/routes/annotatedAttachments.d.ts +1 -1
  80. package/dist/server/routes/annotatedAttachments.js +1 -1
  81. package/dist/server/routes/auth.d.ts +1 -4
  82. package/dist/server/routes/auth.js +23 -63
  83. package/dist/server/routes/authSupport.d.ts +30 -0
  84. package/dist/server/routes/authSupport.js +81 -0
  85. package/dist/server/routes/billing.d.ts +1 -1
  86. package/dist/server/routes/billing.js +104 -188
  87. package/dist/server/routes/billing.test.js +214 -86
  88. package/dist/server/routes/documentReviews.d.ts +1 -1
  89. package/dist/server/routes/documentReviews.js +1 -1
  90. package/dist/server/routes/documents.d.ts +4 -2
  91. package/dist/server/routes/documents.js +73 -7
  92. package/dist/server/routes/primitives.d.ts +11 -0
  93. package/dist/server/routes/primitives.js +73 -0
  94. package/dist/server/routes/resources.d.ts +1 -1
  95. package/dist/server/routes/resources.js +1 -1
  96. package/dist/server/routes/shared.d.ts +1 -1
  97. package/dist/server/routes/sync.d.ts +1 -1
  98. package/dist/server/routes/sync.js +1 -1
  99. package/dist/server/routes/tasks.d.ts +1 -1
  100. package/dist/server/routes/tasks.js +1 -1
  101. package/dist/server/routes/workspaces.d.ts +1 -1
  102. package/dist/server/routes/workspaces.js +1 -1
  103. package/dist/server/routes.d.ts +2 -9
  104. package/dist/server/routes.js +76 -162
  105. package/dist/server/routes.test.js +672 -30
  106. package/dist/sync/contentSyncState.js +16 -7
  107. package/dist/sync/syncApplyHandlers.test.js +2 -0
  108. package/dist/sync/syncService.d.ts +1 -0
  109. package/dist/sync/syncService.js +8 -0
  110. package/dist/sync/syncService.test.js +14 -0
  111. package/dist/sync/workspaceSyncModel.d.ts +3 -0
  112. package/dist/sync/workspaceSyncModel.js +12 -3
  113. package/dist/sync/workspaceSyncModel.test.js +34 -0
  114. package/dist/sync/workspaceSyncState.d.ts +2 -0
  115. package/dist/sync/workspaceSyncState.js +1 -0
  116. package/dist/ui/assets/{AgentsModule-BLWVbuKP.js → AgentsModule-N2MnQBZk.js} +1 -1
  117. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BlS-cqnl.js → AnnotatedAttachmentWorkspace-BG6P_GVW.js} +1 -1
  118. package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +1 -0
  119. package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +252 -0
  120. package/dist/ui/assets/{InitiativesModule-BjR6iBf9.js → InitiativesModule-Dhm7M8e7.js} +1 -1
  121. package/dist/ui/assets/PlansPage-Cq0zXj3I.js +1 -0
  122. package/dist/ui/assets/TaskSettings-ln0aF7xc.js +9 -0
  123. package/dist/ui/assets/{WorkflowsModule-DnFqdUME.js → WorkflowsModule-BcS4afRn.js} +1 -1
  124. package/dist/ui/assets/{index-Ii0B0rTO.css → index-C2-OXZV7.css} +1 -1
  125. package/dist/ui/assets/index-DcSI5F86.js +6 -0
  126. package/dist/ui/assets/{vendor-icons-I7ZwG1z_.js → vendor-icons-BSUaRwF8.js} +1 -1
  127. package/dist/ui/index.html +3 -3
  128. package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
  129. package/dist/utils/commercialLifecyclePresentation.js +196 -0
  130. package/package.json +8 -1
  131. package/scripts/check-cloud-mcp.mjs +83 -0
  132. package/scripts/playwright-auth.sh +5 -1
  133. package/scripts/run-billing-performance-smoke.sh +24 -0
  134. package/dist/ui/assets/DocumentWorkspace-BH_6DF6x.js +0 -250
  135. package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
  136. package/dist/ui/assets/PlansPage-CSDQ4sLa.js +0 -1
  137. package/dist/ui/assets/TaskSettings-CEDy34Jo.js +0 -9
  138. package/dist/ui/assets/index-DXUdtH1B.js +0 -6
@@ -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';
67
+ import { getTaskforceCloudEnvironmentConfig, resolveTaskforceCloudEnvironmentFromBaseUrl, resolveTaskforceMcpBaseUrlFromBaseUrl } from '../../config/cloudEnvironment.js';
68
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' : '';
@@ -1013,7 +1071,7 @@ export function registerAdminRoutes(deps) {
1013
1071
  return;
1014
1072
  }
1015
1073
  if (context.authConfig?.runtimeMode !== 'cloud') {
1016
- const effective = getEmailConfig();
1074
+ const effective = getRouteEmailConfig(core, context);
1017
1075
  res.writeHead(200, { 'Content-Type': 'application/json' });
1018
1076
  res.end(JSON.stringify({
1019
1077
  settings: { provider: 'console', from: effective.from, replyTo: '' },
@@ -1022,7 +1080,7 @@ export function registerAdminRoutes(deps) {
1022
1080
  return;
1023
1081
  }
1024
1082
  const settings = core.getGlobalSettings().email || {};
1025
- const effective = getEmailConfig();
1083
+ const effective = getRouteEmailConfig(core, context);
1026
1084
  res.writeHead(200, { 'Content-Type': 'application/json' });
1027
1085
  res.end(JSON.stringify({
1028
1086
  settings: {
@@ -1047,7 +1105,7 @@ export function registerAdminRoutes(deps) {
1047
1105
  return;
1048
1106
  }
1049
1107
  const settings = core.getGlobalSettings().email || {};
1050
- const effective = getEmailConfig();
1108
+ const effective = getRouteEmailConfig(core, context);
1051
1109
  const keyRequired = effective.provider === 'resend' || effective.provider === 'google-workspace';
1052
1110
  const ready = keyRequired ? Boolean(effective.providerApiKey) : true;
1053
1111
  const reason = ready
@@ -1079,7 +1137,7 @@ export function registerAdminRoutes(deps) {
1079
1137
  const globalSettings = core.getGlobalSettings();
1080
1138
  const authSettings = globalSettings.auth || {};
1081
1139
  const billingSettings = globalSettings.billing || {};
1082
- const effective = getAuthBehavior();
1140
+ const effective = buildAuthBehavior(core);
1083
1141
  res.writeHead(200, { 'Content-Type': 'application/json' });
1084
1142
  res.end(JSON.stringify({
1085
1143
  settings: {
@@ -1117,7 +1175,7 @@ export function registerAdminRoutes(deps) {
1117
1175
  return;
1118
1176
  }
1119
1177
  const siteSettings = core.getGlobalSettings().site || {};
1120
- const siteBehavior = getSiteBehavior();
1178
+ const siteBehavior = buildSiteBehavior(core);
1121
1179
  res.writeHead(200, { 'Content-Type': 'application/json' });
1122
1180
  res.end(JSON.stringify({
1123
1181
  settings: {
@@ -1842,7 +1900,7 @@ export function registerAdminRoutes(deps) {
1842
1900
  res.end(JSON.stringify({ success: false, error: 'Valid test email address is required.' }));
1843
1901
  return;
1844
1902
  }
1845
- const config = getEmailConfig();
1903
+ const config = getRouteEmailConfig(core, context);
1846
1904
  const result = await sendEmail({
1847
1905
  to,
1848
1906
  subject: 'Taskforce email settings test',
@@ -2267,9 +2325,28 @@ export function registerAdminRoutes(deps) {
2267
2325
  res.end(JSON.stringify({ success: false, error: 'Token is missing required scopes.', code: 'MCP_TOKEN_MISSING_SCOPES', details: { requiredScopes, missingScopes, grantedScopes: inspection.scopes } }));
2268
2326
  return;
2269
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
+ }
2270
2334
  const workspace = core.getWorkspaceProfile(wid);
2271
2335
  res.writeHead(200, { 'Content-Type': 'application/json' });
2272
- 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
+ }));
2273
2350
  }
2274
2351
  catch (error) {
2275
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';
@@ -314,7 +265,7 @@ export function registerAuthRoutes(deps) {
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',
@@ -705,7 +656,7 @@ export function registerAuthRoutes(deps) {
705
656
  const workspaceId = workspaceIdCandidate || 'default';
706
657
  // Password flow (preferred)
707
658
  if (email && password) {
708
- const authBehavior = getAuthBehavior();
659
+ const authBehavior = buildAuthFlowBehavior(core);
709
660
  let authResult = core.authenticatePasswordUserDetailed({
710
661
  email,
711
662
  password,
@@ -802,7 +753,7 @@ export function registerAuthRoutes(deps) {
802
753
  const selectedPlanVersionId = typeof body?.planVersionId === 'string'
803
754
  ? body.planVersionId.trim()
804
755
  : '';
805
- const authBehavior = getAuthBehavior();
756
+ const authBehavior = buildAuthFlowBehavior(core);
806
757
  if (!authBehavior.allowSelfRegistration) {
807
758
  res.writeHead(403, { 'Content-Type': 'application/json' });
808
759
  res.end(JSON.stringify({ success: false, error: 'Self-registration is currently disabled.' }));
@@ -875,12 +826,14 @@ export function registerAuthRoutes(deps) {
875
826
  && authBehavior.requireVerifiedEmail
876
827
  && email
877
828
  && !core.isEmailVerified(email)) {
878
- let emailSent = true;
829
+ let emailSent = false;
830
+ let deliveryAttempted = false;
879
831
  let emailError;
880
832
  let verificationToken;
881
833
  const verification = core.requestEmailVerification({ email });
882
834
  verificationToken = verification.token || null;
883
835
  if (verification.token) {
836
+ deliveryAttempted = true;
884
837
  const delivery = await deliverAuthEmail('verify-email', email, verification.token, req);
885
838
  emailSent = delivery.ok;
886
839
  emailError = delivery.error;
@@ -898,6 +851,7 @@ export function registerAuthRoutes(deps) {
898
851
  code: 'EMAIL_ALREADY_EXISTS_UNVERIFIED',
899
852
  verificationRequired: true,
900
853
  emailSent,
854
+ deliveryAttempted,
901
855
  emailError,
902
856
  verificationToken: shouldExposeDevAuthTokens()
903
857
  ? (verificationToken || null)
@@ -930,10 +884,12 @@ export function registerAuthRoutes(deps) {
930
884
  return;
931
885
  }
932
886
  try {
933
- let emailSent = true;
887
+ let emailSent = false;
888
+ let deliveryAttempted = false;
934
889
  let emailError;
935
890
  const verification = core.requestEmailVerification({ email });
936
891
  if (verification.token) {
892
+ deliveryAttempted = true;
937
893
  const delivery = await deliverAuthEmail('verify-email', email, verification.token, req);
938
894
  emailSent = delivery.ok;
939
895
  emailError = delivery.error;
@@ -946,6 +902,7 @@ export function registerAuthRoutes(deps) {
946
902
  res.end(JSON.stringify({
947
903
  success: true,
948
904
  emailSent,
905
+ deliveryAttempted,
949
906
  emailError,
950
907
  verificationToken: shouldExposeDevAuthTokens() ? (verification.token || null) : undefined
951
908
  }));
@@ -1018,10 +975,12 @@ export function registerAuthRoutes(deps) {
1018
975
  return;
1019
976
  }
1020
977
  try {
1021
- let emailSent = true;
978
+ let emailSent = false;
979
+ let deliveryAttempted = false;
1022
980
  let emailError;
1023
981
  const reset = core.requestPasswordReset({ email });
1024
982
  if (reset.token) {
983
+ deliveryAttempted = true;
1025
984
  const delivery = await deliverAuthEmail('password-reset', email, reset.token, req);
1026
985
  emailSent = delivery.ok;
1027
986
  emailError = delivery.error;
@@ -1034,6 +993,7 @@ export function registerAuthRoutes(deps) {
1034
993
  res.end(JSON.stringify({
1035
994
  success: true,
1036
995
  emailSent,
996
+ deliveryAttempted,
1037
997
  emailError,
1038
998
  resetToken: shouldExposeDevAuthTokens() ? (reset.token || null) : undefined
1039
999
  }));
@@ -1389,7 +1349,7 @@ export function registerAuthRoutes(deps) {
1389
1349
  seatUsage,
1390
1350
  upgradeHints: canInvite
1391
1351
  ? []
1392
- : ['Team management requires a TEAM plan in active/trialing/grace state.'],
1352
+ : ['Team management requires an active plan with collaboration access.'],
1393
1353
  supportLink: 'mailto:support@taskforcehq.ai'
1394
1354
  }));
1395
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;
@@ -0,0 +1,81 @@
1
+ import { resolveEffectiveEmailConfig } from '../email.js';
2
+ import { resolveDeploymentConfig } from '../../config/deployment.js';
3
+ export function resolveBooleanSetting(value, fallback) {
4
+ if (typeof value === 'boolean')
5
+ return value;
6
+ if (typeof value === 'number')
7
+ return value !== 0;
8
+ const normalized = String(value || '').trim().toLowerCase();
9
+ if (!normalized)
10
+ return fallback;
11
+ if (['1', 'true', 'yes', 'on'].includes(normalized))
12
+ return true;
13
+ if (['0', 'false', 'no', 'off'].includes(normalized))
14
+ return false;
15
+ return fallback;
16
+ }
17
+ export function getRouteEmailConfig(core, context) {
18
+ if (context.authConfig?.runtimeMode !== 'cloud') {
19
+ return resolveEffectiveEmailConfig(process.env, { provider: 'console' });
20
+ }
21
+ const globalSettings = core.getGlobalSettings();
22
+ return resolveEffectiveEmailConfig(process.env, globalSettings.email || null);
23
+ }
24
+ export function buildAuthFlowBehavior(core) {
25
+ const globalSettings = core.getGlobalSettings();
26
+ const authSettings = globalSettings.auth || {};
27
+ const authRateLimitMaxRequestsRaw = Number(authSettings.authRateLimitMaxRequests);
28
+ const authRateLimitWindowMsRaw = Number(authSettings.authRateLimitWindowMs);
29
+ return {
30
+ allowSelfRegistration: resolveBooleanSetting(authSettings.allowSelfRegistration, true),
31
+ requireVerifiedEmail: resolveBooleanSetting(authSettings.requireVerifiedEmail, true),
32
+ defaultNewUserBetaAccess: resolveBooleanSetting(authSettings.defaultNewUserBetaAccess, false),
33
+ authRateLimitEnabled: resolveBooleanSetting(authSettings.authRateLimitEnabled, true),
34
+ authRateLimitMaxRequests: Number.isFinite(authRateLimitMaxRequestsRaw) && authRateLimitMaxRequestsRaw >= 1
35
+ ? Math.floor(authRateLimitMaxRequestsRaw)
36
+ : 10,
37
+ authRateLimitWindowMs: Number.isFinite(authRateLimitWindowMsRaw) && authRateLimitWindowMsRaw >= 1000
38
+ ? Math.floor(authRateLimitWindowMsRaw)
39
+ : 15 * 60_000,
40
+ };
41
+ }
42
+ export function buildAuthBehavior(core) {
43
+ const authBehavior = buildAuthFlowBehavior(core);
44
+ const onboardingPolicy = core.getOnboardingPolicy();
45
+ return {
46
+ ...authBehavior,
47
+ onboardingPolicyMode: onboardingPolicy.mode,
48
+ onboardingPlanVersionId: onboardingPolicy.planVersionId
49
+ };
50
+ }
51
+ export function buildSiteBehavior(core) {
52
+ const globalSettings = core.getGlobalSettings();
53
+ const siteSettings = globalSettings.site || {};
54
+ return {
55
+ showRunTaskforceLocally: resolveBooleanSetting(siteSettings.showRunTaskforceLocally, true),
56
+ pricingPageEnabled: resolveBooleanSetting(siteSettings.pricingPageEnabled, true)
57
+ };
58
+ }
59
+ export function shouldExposeDevAuthTokens() {
60
+ const explicit = String(process.env.TASKFORCE_AUTH_EXPOSE_DEV_TOKENS || '').trim().toLowerCase();
61
+ return explicit === '1' || explicit === 'true' || explicit === 'yes';
62
+ }
63
+ export function resolveRuntimeConfigBaseUrl(req) {
64
+ const host = String(req.headers['x-forwarded-host'] || req.headers.host || '').trim();
65
+ if (!host)
66
+ return '';
67
+ const proto = String(req.headers['x-forwarded-proto'] || '').trim().toLowerCase() === 'https'
68
+ ? 'https'
69
+ : (req.socket?.encrypted ? 'https' : 'http');
70
+ return `${proto}://${host}`.replace(/\/+$/, '');
71
+ }
72
+ export function resolvePublicAuthBaseUrl(req) {
73
+ const configured = String(process.env.TASKFORCE_AUTH_PUBLIC_BASE_URL || '').trim().replace(/\/+$/, '');
74
+ if (configured)
75
+ return configured;
76
+ const host = String(req.headers['x-forwarded-host'] || req.headers.host || '').trim();
77
+ const proto = String(req.headers['x-forwarded-proto'] || '').trim().toLowerCase() === 'http' ? 'http' : 'https';
78
+ if (!host)
79
+ return resolveDeploymentConfig(process.env).cloudBaseUrl || 'https://app.taskforcehq.ai';
80
+ return `${proto}://${host}`;
81
+ }
@@ -1,6 +1,6 @@
1
1
  import type { IncomingMessage } from 'http';
2
2
  import type { TaskforceCore } from '../../core/Taskforce.js';
3
- import type { RouteHandler, RouteContext } from '../routes.js';
3
+ import { type RouteHandler, type RouteContext } from './primitives.js';
4
4
  export type BillingInterval = 'month' | 'year';
5
5
  export type BillingPricingType = 'stripe' | 'free';
6
6
  export interface BillingRouteDependencies {