@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.
- package/README.md +140 -247
- package/dist/TaskforceCore.js +165 -70
- package/dist/TaskforceCore.test.js +799 -90
- package/dist/compat/workspaceSyncCompat.js +6 -0
- package/dist/components/features/DocumentIndex.d.ts +1 -1
- package/dist/components/features/DocumentIndex.js +1 -1
- package/dist/components/features/DocumentViewer.d.ts +1 -1
- package/dist/components/features/DocumentViewer.js +32 -9
- package/dist/components/features/DocumentWorkspace.d.ts +5 -22
- package/dist/components/features/DocumentWorkspace.js +12 -155
- package/dist/components/features/DocumentWorkspace.test.js +193 -6
- package/dist/components/features/TaskSettings.js +29 -3
- package/dist/components/features/TaskSettings.test.js +42 -1
- package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
- package/dist/components/features/documentWorkspaceModel.js +57 -0
- package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
- package/dist/components/features/useDocumentWorkspaceController.js +113 -0
- package/dist/components/task/TaskForm.d.ts +8 -8
- package/dist/components/task/TaskForm.js +358 -221
- package/dist/components/task/TaskForm.test.js +456 -381
- package/dist/components/task/TaskKanban.js +2 -1
- package/dist/components/views/PlanComparisonPage.js +5 -8
- package/dist/components/views/PlanComparisonPage.test.js +51 -41
- package/dist/components/views/PlansPage.js +126 -84
- package/dist/components/views/PlansPage.test.js +308 -39
- package/dist/components/views/StandaloneLayout.js +28 -22
- package/dist/components/views/panels/FilterToolbar.test.js +6 -4
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +2 -14
- package/dist/components/views/standalone/modals/AccountHubModal.js +12 -14
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +24 -1
- package/dist/config/envSchema.js +1 -1
- package/dist/core/AiProfileService.d.ts +6 -1
- package/dist/core/AiProfileService.js +161 -16
- package/dist/core/AiProfileService.test.js +3 -1
- package/dist/core/AiProfiles.test.js +200 -0
- package/dist/core/AuthTokenService.test.d.ts +1 -0
- package/dist/core/AuthTokenService.test.js +78 -0
- package/dist/core/EntitlementsPolicy.test.js +75 -13
- package/dist/core/PlanEntitlementService.d.ts +7 -1
- package/dist/core/PlanEntitlementService.js +117 -42
- package/dist/core/PlanVersionPolicy.test.js +19 -26
- package/dist/core/SignupMonetizationSettings.test.js +46 -21
- package/dist/core/SystemAdmin.test.js +212 -56
- package/dist/core/TaskTaxonomyValidation.test.js +53 -0
- package/dist/core/Taskforce.d.ts +3 -0
- package/dist/core/Taskforce.js +50 -10
- package/dist/core/WorkspacePlanMode.test.js +12 -12
- package/dist/core/shared.d.ts +2 -0
- package/dist/core/shared.js +11 -0
- package/dist/hooks/sync/auth.js +3 -1
- package/dist/hooks/sync/transfers.d.ts +2 -1
- package/dist/hooks/sync/transfers.js +8 -4
- package/dist/hooks/useSyncOrchestrator.d.ts +7 -4
- package/dist/hooks/useSyncOrchestrator.js +75 -7
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +80 -10
- package/dist/hooks/useTaskMutations.d.ts +2 -1
- package/dist/hooks/useTaskMutations.js +12 -1
- package/dist/hooks/useTaskMutations.test.js +30 -1
- package/dist/hooks/useTaskforce.d.ts +5 -2
- package/dist/hooks/useTaskforce.js +17 -5
- package/dist/hooks/useTaskforce.sync-behavior.test.js +78 -0
- package/dist/hooks/useWorkspaceSyncController.d.ts +3 -1
- package/dist/hooks/useWorkspaceSyncController.js +11 -1
- package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
- package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
- package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
- package/dist/mcp/collaborationRegistrar.d.ts +2 -0
- package/dist/mcp/collaborationRegistrar.js +71 -0
- package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
- package/dist/mcp/documentAssetRegistrar.js +346 -0
- package/dist/mcp/runtime.js +2362 -3530
- package/dist/mcp/runtime.test.js +159 -0
- package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
- package/dist/mcp/taskPlanningRegistrar.js +418 -0
- package/dist/mcp/toolCatalog.d.ts +35 -0
- package/dist/mcp/toolCatalog.js +3 -0
- package/dist/server/routes/admin.d.ts +1 -5
- package/dist/server/routes/admin.js +138 -61
- package/dist/server/routes/annotatedAttachments.d.ts +1 -1
- package/dist/server/routes/annotatedAttachments.js +1 -1
- package/dist/server/routes/auth.d.ts +1 -4
- package/dist/server/routes/auth.js +23 -63
- package/dist/server/routes/authSupport.d.ts +30 -0
- package/dist/server/routes/authSupport.js +81 -0
- package/dist/server/routes/billing.d.ts +1 -1
- package/dist/server/routes/billing.js +104 -188
- package/dist/server/routes/billing.test.js +214 -86
- package/dist/server/routes/documentReviews.d.ts +1 -1
- package/dist/server/routes/documentReviews.js +1 -1
- package/dist/server/routes/documents.d.ts +4 -2
- package/dist/server/routes/documents.js +73 -7
- package/dist/server/routes/primitives.d.ts +11 -0
- package/dist/server/routes/primitives.js +73 -0
- package/dist/server/routes/resources.d.ts +1 -1
- package/dist/server/routes/resources.js +1 -1
- package/dist/server/routes/shared.d.ts +1 -1
- package/dist/server/routes/sync.d.ts +1 -1
- package/dist/server/routes/sync.js +1 -1
- package/dist/server/routes/tasks.d.ts +1 -1
- package/dist/server/routes/tasks.js +1 -1
- package/dist/server/routes/workspaces.d.ts +1 -1
- package/dist/server/routes/workspaces.js +1 -1
- package/dist/server/routes.d.ts +2 -9
- package/dist/server/routes.js +76 -162
- package/dist/server/routes.test.js +672 -30
- package/dist/sync/contentSyncState.js +16 -7
- package/dist/sync/syncApplyHandlers.test.js +2 -0
- package/dist/sync/syncService.d.ts +1 -0
- package/dist/sync/syncService.js +8 -0
- package/dist/sync/syncService.test.js +14 -0
- package/dist/sync/workspaceSyncModel.d.ts +3 -0
- package/dist/sync/workspaceSyncModel.js +12 -3
- package/dist/sync/workspaceSyncModel.test.js +34 -0
- package/dist/sync/workspaceSyncState.d.ts +2 -0
- package/dist/sync/workspaceSyncState.js +1 -0
- package/dist/ui/assets/{AgentsModule-BLWVbuKP.js → AgentsModule-N2MnQBZk.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BlS-cqnl.js → AnnotatedAttachmentWorkspace-BG6P_GVW.js} +1 -1
- package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +1 -0
- package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +252 -0
- package/dist/ui/assets/{InitiativesModule-BjR6iBf9.js → InitiativesModule-Dhm7M8e7.js} +1 -1
- package/dist/ui/assets/PlansPage-Cq0zXj3I.js +1 -0
- package/dist/ui/assets/TaskSettings-ln0aF7xc.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-DnFqdUME.js → WorkflowsModule-BcS4afRn.js} +1 -1
- package/dist/ui/assets/{index-Ii0B0rTO.css → index-C2-OXZV7.css} +1 -1
- package/dist/ui/assets/index-DcSI5F86.js +6 -0
- package/dist/ui/assets/{vendor-icons-I7ZwG1z_.js → vendor-icons-BSUaRwF8.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
- package/dist/utils/commercialLifecyclePresentation.js +196 -0
- package/package.json +8 -1
- package/scripts/check-cloud-mcp.mjs +83 -0
- package/scripts/playwright-auth.sh +5 -1
- package/scripts/run-billing-performance-smoke.sh +24 -0
- package/dist/ui/assets/DocumentWorkspace-BH_6DF6x.js +0 -250
- package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
- package/dist/ui/assets/PlansPage-CSDQ4sLa.js +0 -1
- package/dist/ui/assets/TaskSettings-CEDy34Jo.js +0 -9
- package/dist/ui/assets/index-DXUdtH1B.js +0 -6
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const DEFAULT_MAX_JSON_BODY_BYTES = 1024 * 1024; // 1MB
|
|
2
|
+
function createHttpError(message, statusCode) {
|
|
3
|
+
const error = new Error(message);
|
|
4
|
+
error.statusCode = statusCode;
|
|
5
|
+
return error;
|
|
6
|
+
}
|
|
7
|
+
export async function parseJsonBody(req, maxBytes = DEFAULT_MAX_JSON_BODY_BYTES) {
|
|
8
|
+
const safeMaxBytes = Number.isFinite(maxBytes) && maxBytes > 0
|
|
9
|
+
? Math.floor(maxBytes)
|
|
10
|
+
: DEFAULT_MAX_JSON_BODY_BYTES;
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
let body = '';
|
|
13
|
+
let receivedBytes = 0;
|
|
14
|
+
req.on('data', chunk => {
|
|
15
|
+
receivedBytes += chunk.length;
|
|
16
|
+
if (receivedBytes > safeMaxBytes) {
|
|
17
|
+
req.pause();
|
|
18
|
+
reject(createHttpError(`Payload too large (max ${safeMaxBytes} bytes)`, 413));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
body += chunk;
|
|
22
|
+
});
|
|
23
|
+
req.on('end', () => {
|
|
24
|
+
try {
|
|
25
|
+
resolve(body ? JSON.parse(body) : {});
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
reject(createHttpError('Invalid JSON', 400));
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
req.on('error', reject);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export async function parseFormBody(req, maxBytes = DEFAULT_MAX_JSON_BODY_BYTES) {
|
|
35
|
+
const safeMaxBytes = Number.isFinite(maxBytes) && maxBytes > 0
|
|
36
|
+
? Math.floor(maxBytes)
|
|
37
|
+
: DEFAULT_MAX_JSON_BODY_BYTES;
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
let body = '';
|
|
40
|
+
let receivedBytes = 0;
|
|
41
|
+
req.on('data', chunk => {
|
|
42
|
+
receivedBytes += chunk.length;
|
|
43
|
+
if (receivedBytes > safeMaxBytes) {
|
|
44
|
+
req.pause();
|
|
45
|
+
reject(createHttpError(`Payload too large (max ${safeMaxBytes} bytes)`, 413));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
body += chunk;
|
|
49
|
+
});
|
|
50
|
+
req.on('end', () => {
|
|
51
|
+
try {
|
|
52
|
+
const params = new URLSearchParams(body);
|
|
53
|
+
const parsed = {};
|
|
54
|
+
params.forEach((value, key) => {
|
|
55
|
+
parsed[String(key)] = String(value);
|
|
56
|
+
});
|
|
57
|
+
resolve(parsed);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
reject(createHttpError('Invalid form body', 400));
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
req.on('error', reject);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
export function escapeHtml(value) {
|
|
67
|
+
return String(value || '')
|
|
68
|
+
.replace(/&/g, '&')
|
|
69
|
+
.replace(/</g, '<')
|
|
70
|
+
.replace(/>/g, '>')
|
|
71
|
+
.replace(/"/g, '"')
|
|
72
|
+
.replace(/'/g, ''');
|
|
73
|
+
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* GET /api/taskforce/environments
|
|
18
18
|
*/
|
|
19
19
|
import type { TaskforceCore } from '../../core/Taskforce.js';
|
|
20
|
-
import { type RouteHandler } from '
|
|
20
|
+
import { type RouteHandler } from './primitives.js';
|
|
21
21
|
export interface ResourceRouteDependencies {
|
|
22
22
|
addRoute: (method: string, path: string, handler: RouteHandler) => void;
|
|
23
23
|
core: TaskforceCore;
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import * as fs from 'fs';
|
|
20
20
|
import * as path from 'path';
|
|
21
21
|
import { execFileSync } from 'child_process';
|
|
22
|
-
import { parseJsonBody } from '
|
|
22
|
+
import { parseJsonBody } from './primitives.js';
|
|
23
23
|
const WORKFLOW_EDITOR_LOCKED_FIELDS = {
|
|
24
24
|
do: ['aliasesText', 'dispatchDefault', 'criticalToolsText', 'contentDefault'],
|
|
25
25
|
execute: ['aliasesText', 'dispatchDefault', 'criticalToolsText', 'contentDefault']
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { IncomingMessage, ServerResponse } from 'http';
|
|
9
9
|
import { TaskforceCore } from '../../core/Taskforce.js';
|
|
10
10
|
import type { McpAccessTokenAuthResult, McpOAuthAccessTokenAuthResult, McpAccessTokenScope } from '../../core/Taskforce.js';
|
|
11
|
-
import type { RouteContext } from '
|
|
11
|
+
import type { RouteContext } from './primitives.js';
|
|
12
12
|
import type { AiProfileBootstrapConfig } from '../../mcp/aiProfileBootstrap.js';
|
|
13
13
|
export declare function resolveErrorStatusCode(error: unknown, fallback?: number): number;
|
|
14
14
|
export declare function buildCloudMcpRuntimeAuth(input: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { IncomingMessage } from 'http';
|
|
2
2
|
import { TaskforceCore, TaskforceSyncCapable } from '../../core/Taskforce.js';
|
|
3
|
-
import {
|
|
3
|
+
import { type RouteContext, type RouteHandler } from './primitives.js';
|
|
4
4
|
import { resolveRequestAccess, createSessionTokenCookie } from '../auth.js';
|
|
5
5
|
import { isReservedWorkspaceId } from '../../utils/workspaceIdentity.js';
|
|
6
6
|
export interface SyncRouteDependencies {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as path from 'path';
|
|
2
|
-
import { parseJsonBody } from '
|
|
2
|
+
import { parseJsonBody } from './primitives.js';
|
|
3
3
|
import { normalizeTaskDataRelativePath, ObjectStorageClient } from '../../storage/objectStorageClient.js';
|
|
4
4
|
import { WorkspaceAssetStore, } from '../../storage/workspaceAssetStore.js';
|
|
5
5
|
import { resolveSessionCookieAttributesForRequest } from '../auth.js';
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
import { IncomingMessage, ServerResponse } from 'http';
|
|
53
53
|
import { TaskforceCore } from '../../core/Taskforce.js';
|
|
54
54
|
import { type ResolvedObjectStorageConfig } from '../../storage/objectStorage.js';
|
|
55
|
-
import { type RouteHandler, type RouteContext } from '
|
|
55
|
+
import { type RouteHandler, type RouteContext } from './primitives.js';
|
|
56
56
|
export interface TaskRouteDependencies {
|
|
57
57
|
addRoute: (method: string, path: string, handler: RouteHandler) => void;
|
|
58
58
|
core: TaskforceCore;
|
|
@@ -54,7 +54,7 @@ import { toPublicObjectStorageConfig } from '../../storage/objectStorage.js';
|
|
|
54
54
|
import { getRuntimeCapabilities } from '../../runtime/capabilities.js';
|
|
55
55
|
import { resolveBuildInfo } from '../buildInfo.js';
|
|
56
56
|
import { resolveRealtimeSyncFlag } from '../../sync/realtimeFeature.js';
|
|
57
|
-
import { parseJsonBody } from '
|
|
57
|
+
import { parseJsonBody } from './primitives.js';
|
|
58
58
|
import { resolveRequestAccess } from '../auth.js';
|
|
59
59
|
import { resolveErrorStatusCode, requestHeader, requestRuntimeMode } from './shared.js';
|
|
60
60
|
import { listBuiltInCustomTaxonomyLibraryPacks } from '../../shared/customTaxonomyLibrary.js';
|
|
@@ -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
|
|
16
|
+
import { type RouteHandler, type RouteContext } from './primitives.js';
|
|
17
17
|
import { ObjectStorageClient } from '../../storage/objectStorageClient.js';
|
|
18
18
|
export interface WorkspaceRouteDependencies {
|
|
19
19
|
addRoute: (method: string, path: string, handler: RouteHandler) => void;
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import * as fs from 'fs';
|
|
15
15
|
import * as path from 'path';
|
|
16
|
-
import { parseJsonBody } from '
|
|
16
|
+
import { parseJsonBody } from './primitives.js';
|
|
17
17
|
import { resolveRequestAccess, createSessionTokenCookie, resolveSessionCookieAttributesForRequest, } from '../auth.js';
|
|
18
18
|
import { isWorkspaceSwitchingEnabledForRuntime } from '../../shared/runtimeContract.js';
|
|
19
19
|
import { resolveErrorStatusCode } from './shared.js';
|
package/dist/server/routes.d.ts
CHANGED
|
@@ -4,17 +4,14 @@
|
|
|
4
4
|
* Extracted from vite.ts plugin to be reusable in both Vite plugin and standalone server.
|
|
5
5
|
* This is a minimal implementation with core routes - additional routes can be added incrementally.
|
|
6
6
|
*/
|
|
7
|
-
import { IncomingMessage, ServerResponse } from 'http';
|
|
8
7
|
import { TaskforceCore } from '../core/Taskforce.js';
|
|
9
|
-
import { type
|
|
10
|
-
|
|
11
|
-
export type RouteHandler = (req: IncomingMessage, res: ServerResponse, params: Record<string, string>) => Promise<void>;
|
|
8
|
+
import { type RouteContext, type RouteHandler } from './routes/primitives.js';
|
|
9
|
+
export { parseJsonBody } from './routes/primitives.js';
|
|
12
10
|
export interface Route {
|
|
13
11
|
pattern: RegExp;
|
|
14
12
|
keys: string[];
|
|
15
13
|
handler: RouteHandler;
|
|
16
14
|
}
|
|
17
|
-
export declare function parseJsonBody(req: IncomingMessage, maxBytes?: number): Promise<any>;
|
|
18
15
|
/**
|
|
19
16
|
* Create API routes for Taskforce
|
|
20
17
|
*
|
|
@@ -60,10 +57,6 @@ export declare function parseJsonBody(req: IncomingMessage, maxBytes?: number):
|
|
|
60
57
|
* - POST /api/taskforce/export-resources
|
|
61
58
|
* - GET /api/taskforce/health
|
|
62
59
|
*/
|
|
63
|
-
export interface RouteContext {
|
|
64
|
-
authConfig?: TaskforceAuthConfig;
|
|
65
|
-
objectStorage?: ResolvedObjectStorageConfig;
|
|
66
|
-
}
|
|
67
60
|
export declare function createRoutes(core: TaskforceCore, context?: RouteContext): Route[];
|
|
68
61
|
export declare function matchRoute(method: string, pathname: string, routes: Route[]): {
|
|
69
62
|
handler: RouteHandler;
|
package/dist/server/routes.js
CHANGED
|
@@ -14,7 +14,7 @@ import { parseAiProfileBootstrapHeaders } from '../mcp/aiProfileBootstrap.js';
|
|
|
14
14
|
import { createTaskforceMcpServer, isMcpWriteToolName } from '../mcp/runtime.js';
|
|
15
15
|
import { applyPrefixedRateLimitHeaders } from './rateLimit.js';
|
|
16
16
|
import { createSessionTokenCookie, resolveRequestAccess } from './auth.js';
|
|
17
|
-
import {
|
|
17
|
+
import { sendEmail } from './email.js';
|
|
18
18
|
import { normalizeTaskDataRelativePath, ObjectStorageClient } from '../storage/objectStorageClient.js';
|
|
19
19
|
import { DocumentRegistry, createDocumentHash } from '../storage/documentRegistry.js';
|
|
20
20
|
import { TaskAssetStore } from '../storage/taskAssetStore.js';
|
|
@@ -34,12 +34,14 @@ import { registerResourceRoutes } from './routes/resources.js';
|
|
|
34
34
|
import { registerDocumentRoutes } from './routes/documents.js';
|
|
35
35
|
import { registerDocumentReviewRoutes } from './routes/documentReviews.js';
|
|
36
36
|
import { registerAnnotatedAttachmentRoutes } from './routes/annotatedAttachments.js';
|
|
37
|
+
import { escapeHtml, parseJsonBody } from './routes/primitives.js';
|
|
38
|
+
import { getRouteEmailConfig, resolvePublicAuthBaseUrl, resolveRuntimeConfigBaseUrl } from './routes/authSupport.js';
|
|
37
39
|
import { buildCloudMcpRuntimeAuth, requestHeader, requestRole, requestRuntimeMode, requestUserId, makeAuditAdminAction, makeAuditAuthAction, makeEnsureAdminRole, makeEnsureAppAccess, makeEnsureAuthenticatedUser, makeEnsureCloudPlanData, makeRequestWorkspaceId, makeRequireCloudRuntime, makeResolveAdminRequestActor, makeResolveAuthenticatedUserId, makeResolveAuthenticatedMutationUserId, ensureWorkspaceAdminRole, } from './routes/shared.js';
|
|
38
40
|
import { resolveDeploymentConfig } from '../config/deployment.js';
|
|
39
41
|
import { getTaskforceCloudEnvironmentConfig, resolveTaskforceCloudEnvironmentFromBaseUrl } from '../config/cloudEnvironment.js';
|
|
40
42
|
import { getCanonicalDocumentName } from '../utils/documentNames.js';
|
|
41
43
|
import { MCP_CLOUD_TOOL_SCOPE_REQUIREMENTS } from '../mcp/toolRegistry.js';
|
|
42
|
-
|
|
44
|
+
export { parseJsonBody } from './routes/primitives.js';
|
|
43
45
|
const DEFAULT_DOCUMENT_RETENTION_DAYS = 7;
|
|
44
46
|
const STORAGE_INTEGRITY_HISTORY_LIMIT = 50;
|
|
45
47
|
const MCP_TOKEN_SCOPE_VALUES = new Set(['tasks:read', 'tasks:write', 'workspace:read']);
|
|
@@ -70,20 +72,6 @@ function readPositiveInteger(value, fallback) {
|
|
|
70
72
|
return fallback;
|
|
71
73
|
return parsed;
|
|
72
74
|
}
|
|
73
|
-
function resolveBooleanSetting(value, fallback) {
|
|
74
|
-
if (typeof value === 'boolean')
|
|
75
|
-
return value;
|
|
76
|
-
if (typeof value === 'number')
|
|
77
|
-
return value !== 0;
|
|
78
|
-
const normalized = String(value || '').trim().toLowerCase();
|
|
79
|
-
if (!normalized)
|
|
80
|
-
return fallback;
|
|
81
|
-
if (['1', 'true', 'yes', 'on'].includes(normalized))
|
|
82
|
-
return true;
|
|
83
|
-
if (['0', 'false', 'no', 'off'].includes(normalized))
|
|
84
|
-
return false;
|
|
85
|
-
return fallback;
|
|
86
|
-
}
|
|
87
75
|
// Convert route pattern like '/api/taskforce/tasks/:id' to regex
|
|
88
76
|
function createRoutePattern(path) {
|
|
89
77
|
const keys = [];
|
|
@@ -93,74 +81,6 @@ function createRoutePattern(path) {
|
|
|
93
81
|
});
|
|
94
82
|
return { pattern: new RegExp(`^${pattern}$`), keys };
|
|
95
83
|
}
|
|
96
|
-
// Helper to parse JSON body from request with size guard.
|
|
97
|
-
export async function parseJsonBody(req, maxBytes = DEFAULT_MAX_JSON_BODY_BYTES) {
|
|
98
|
-
const safeMaxBytes = Number.isFinite(maxBytes) && maxBytes > 0
|
|
99
|
-
? Math.floor(maxBytes)
|
|
100
|
-
: DEFAULT_MAX_JSON_BODY_BYTES;
|
|
101
|
-
return new Promise((resolve, reject) => {
|
|
102
|
-
let body = '';
|
|
103
|
-
let receivedBytes = 0;
|
|
104
|
-
req.on('data', chunk => {
|
|
105
|
-
receivedBytes += chunk.length;
|
|
106
|
-
if (receivedBytes > safeMaxBytes) {
|
|
107
|
-
req.pause();
|
|
108
|
-
reject(createHttpError(`Payload too large (max ${safeMaxBytes} bytes)`, 413));
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
body += chunk;
|
|
112
|
-
});
|
|
113
|
-
req.on('end', () => {
|
|
114
|
-
try {
|
|
115
|
-
resolve(body ? JSON.parse(body) : {});
|
|
116
|
-
}
|
|
117
|
-
catch (e) {
|
|
118
|
-
reject(createHttpError('Invalid JSON', 400));
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
req.on('error', reject);
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
async function parseFormBody(req, maxBytes = DEFAULT_MAX_JSON_BODY_BYTES) {
|
|
125
|
-
const safeMaxBytes = Number.isFinite(maxBytes) && maxBytes > 0
|
|
126
|
-
? Math.floor(maxBytes)
|
|
127
|
-
: DEFAULT_MAX_JSON_BODY_BYTES;
|
|
128
|
-
return new Promise((resolve, reject) => {
|
|
129
|
-
let body = '';
|
|
130
|
-
let receivedBytes = 0;
|
|
131
|
-
req.on('data', chunk => {
|
|
132
|
-
receivedBytes += chunk.length;
|
|
133
|
-
if (receivedBytes > safeMaxBytes) {
|
|
134
|
-
req.pause();
|
|
135
|
-
reject(createHttpError(`Payload too large (max ${safeMaxBytes} bytes)`, 413));
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
body += chunk;
|
|
139
|
-
});
|
|
140
|
-
req.on('end', () => {
|
|
141
|
-
try {
|
|
142
|
-
const params = new URLSearchParams(body);
|
|
143
|
-
const parsed = {};
|
|
144
|
-
params.forEach((value, key) => {
|
|
145
|
-
parsed[String(key)] = String(value);
|
|
146
|
-
});
|
|
147
|
-
resolve(parsed);
|
|
148
|
-
}
|
|
149
|
-
catch {
|
|
150
|
-
reject(createHttpError('Invalid form body', 400));
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
req.on('error', reject);
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
function escapeHtml(value) {
|
|
157
|
-
return String(value || '')
|
|
158
|
-
.replace(/&/g, '&')
|
|
159
|
-
.replace(/</g, '<')
|
|
160
|
-
.replace(/>/g, '>')
|
|
161
|
-
.replace(/"/g, '"')
|
|
162
|
-
.replace(/'/g, ''');
|
|
163
|
-
}
|
|
164
84
|
function resolveErrorStatusCode(error, fallback = 400) {
|
|
165
85
|
if (error instanceof TaskforceRuleError)
|
|
166
86
|
return error.statusCode;
|
|
@@ -206,6 +126,51 @@ function inferMcpOauthProviderFromRedirectUris(redirectUris) {
|
|
|
206
126
|
}
|
|
207
127
|
return MCP_OAUTH_DEFAULT_PROVIDER;
|
|
208
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Create API routes for Taskforce
|
|
131
|
+
*
|
|
132
|
+
* Core routes (29 total):
|
|
133
|
+
*
|
|
134
|
+
* **Task Management (12 routes)**
|
|
135
|
+
* - GET /api/taskforce/config
|
|
136
|
+
* - GET /api/taskforce/tasks
|
|
137
|
+
* - POST /api/taskforce/task
|
|
138
|
+
* - PATCH /api/taskforce/task/:id
|
|
139
|
+
* - DELETE /api/taskforce/task/:id
|
|
140
|
+
* - GET /api/taskforce/archive
|
|
141
|
+
* - POST /api/taskforce/task/:id/complete
|
|
142
|
+
* - POST /api/taskforce/task/:id/cancel
|
|
143
|
+
* - POST /api/taskforce/task/:id/comment
|
|
144
|
+
* - POST /api/taskforce/task/:id/unarchive
|
|
145
|
+
* - POST /api/taskforce/bulk-create
|
|
146
|
+
* - POST /api/taskforce/bulk-archive
|
|
147
|
+
* - POST /api/taskforce/bulk-update-fields
|
|
148
|
+
* - POST /api/taskforce/bulk-update-fields/preview
|
|
149
|
+
*
|
|
150
|
+
* **Configuration Management**
|
|
151
|
+
* - GET /api/taskforce/categories
|
|
152
|
+
* - POST /api/taskforce/categories
|
|
153
|
+
* - GET /api/taskforce/types
|
|
154
|
+
* - POST /api/taskforce/types
|
|
155
|
+
* - GET /api/taskforce/priorities
|
|
156
|
+
* - POST /api/taskforce/priorities
|
|
157
|
+
* - GET /api/taskforce/taxonomies
|
|
158
|
+
* - POST /api/taskforce/taxonomies
|
|
159
|
+
*
|
|
160
|
+
* **Workflow Settings**
|
|
161
|
+
* - GET /api/taskforce/approaches
|
|
162
|
+
* - POST /api/taskforce/approaches
|
|
163
|
+
*
|
|
164
|
+
* **Templates & Setup**
|
|
165
|
+
* - GET /api/taskforce/initiative-templates
|
|
166
|
+
* - POST /api/taskforce/initiative-templates/create
|
|
167
|
+
*
|
|
168
|
+
* **Advanced Features (3 routes)**
|
|
169
|
+
* - GET /api/taskforce/folders
|
|
170
|
+
* - POST /api/taskforce/validate-paths
|
|
171
|
+
* - POST /api/taskforce/export-resources
|
|
172
|
+
* - GET /api/taskforce/health
|
|
173
|
+
*/
|
|
209
174
|
export function createRoutes(core, context = {}) {
|
|
210
175
|
const routes = [];
|
|
211
176
|
const uiStateCache = new Map();
|
|
@@ -299,33 +264,6 @@ export function createRoutes(core, context = {}) {
|
|
|
299
264
|
return null;
|
|
300
265
|
return resolved;
|
|
301
266
|
};
|
|
302
|
-
const shouldExposeDevAuthTokens = () => {
|
|
303
|
-
const explicit = String(process.env.TASKFORCE_AUTH_EXPOSE_DEV_TOKENS || '').trim().toLowerCase();
|
|
304
|
-
if (explicit === '1' || explicit === 'true' || explicit === 'yes')
|
|
305
|
-
return true;
|
|
306
|
-
if (explicit === '0' || explicit === 'false' || explicit === 'no')
|
|
307
|
-
return false;
|
|
308
|
-
return String(process.env.NODE_ENV || '').trim().toLowerCase() !== 'production';
|
|
309
|
-
};
|
|
310
|
-
const resolveRuntimeConfigBaseUrl = (req) => {
|
|
311
|
-
const host = String(req.headers['x-forwarded-host'] || req.headers.host || '').trim();
|
|
312
|
-
if (!host)
|
|
313
|
-
return '';
|
|
314
|
-
const proto = String(req.headers['x-forwarded-proto'] || '').trim().toLowerCase() === 'https'
|
|
315
|
-
? 'https'
|
|
316
|
-
: (req.socket?.encrypted ? 'https' : 'http');
|
|
317
|
-
return `${proto}://${host}`.replace(/\/+$/, '');
|
|
318
|
-
};
|
|
319
|
-
const resolvePublicAuthBaseUrl = (req) => {
|
|
320
|
-
const configured = String(process.env.TASKFORCE_AUTH_PUBLIC_BASE_URL || '').trim().replace(/\/+$/, '');
|
|
321
|
-
if (configured)
|
|
322
|
-
return configured;
|
|
323
|
-
const host = String(req.headers['x-forwarded-host'] || req.headers.host || '').trim();
|
|
324
|
-
const proto = String(req.headers['x-forwarded-proto'] || '').trim().toLowerCase() === 'http' ? 'http' : 'https';
|
|
325
|
-
if (!host)
|
|
326
|
-
return resolveDeploymentConfig(process.env).cloudBaseUrl || 'https://app.taskforcehq.ai';
|
|
327
|
-
return `${proto}://${host}`;
|
|
328
|
-
};
|
|
329
267
|
const logAuthLink = (kind, email, token, req) => {
|
|
330
268
|
const base = resolvePublicAuthBaseUrl(req);
|
|
331
269
|
const path = kind === 'verify-email'
|
|
@@ -333,34 +271,6 @@ export function createRoutes(core, context = {}) {
|
|
|
333
271
|
: `/login?mode=reset&token=${encodeURIComponent(token)}`;
|
|
334
272
|
console.info(`[Taskforce Auth] ${kind} email=${email} link=${base}${path}`);
|
|
335
273
|
};
|
|
336
|
-
const getEmailConfig = () => {
|
|
337
|
-
if (context.authConfig?.runtimeMode !== 'cloud') {
|
|
338
|
-
return resolveEffectiveEmailConfig(process.env, { provider: 'console' });
|
|
339
|
-
}
|
|
340
|
-
const globalSettings = core.getGlobalSettings();
|
|
341
|
-
return resolveEffectiveEmailConfig(process.env, globalSettings.email || null);
|
|
342
|
-
};
|
|
343
|
-
const getAuthBehavior = () => {
|
|
344
|
-
const globalSettings = core.getGlobalSettings();
|
|
345
|
-
const authSettings = globalSettings.auth || {};
|
|
346
|
-
const authRateLimitMaxRequestsRaw = Number(authSettings.authRateLimitMaxRequests);
|
|
347
|
-
const authRateLimitWindowMsRaw = Number(authSettings.authRateLimitWindowMs);
|
|
348
|
-
const onboardingPolicy = core.getOnboardingPolicy();
|
|
349
|
-
return {
|
|
350
|
-
allowSelfRegistration: resolveBooleanSetting(authSettings.allowSelfRegistration, true),
|
|
351
|
-
requireVerifiedEmail: resolveBooleanSetting(authSettings.requireVerifiedEmail, true),
|
|
352
|
-
defaultNewUserBetaAccess: resolveBooleanSetting(authSettings.defaultNewUserBetaAccess, false),
|
|
353
|
-
authRateLimitEnabled: resolveBooleanSetting(authSettings.authRateLimitEnabled, true),
|
|
354
|
-
authRateLimitMaxRequests: Number.isFinite(authRateLimitMaxRequestsRaw) && authRateLimitMaxRequestsRaw >= 1
|
|
355
|
-
? Math.floor(authRateLimitMaxRequestsRaw)
|
|
356
|
-
: 10,
|
|
357
|
-
authRateLimitWindowMs: Number.isFinite(authRateLimitWindowMsRaw) && authRateLimitWindowMsRaw >= 1000
|
|
358
|
-
? Math.floor(authRateLimitWindowMsRaw)
|
|
359
|
-
: 15 * 60_000,
|
|
360
|
-
onboardingPolicyMode: onboardingPolicy.mode,
|
|
361
|
-
onboardingPlanVersionId: onboardingPolicy.planVersionId
|
|
362
|
-
};
|
|
363
|
-
};
|
|
364
274
|
const deliverAuthEmail = async (kind, email, token, req) => {
|
|
365
275
|
const base = resolvePublicAuthBaseUrl(req);
|
|
366
276
|
const path = kind === 'verify-email'
|
|
@@ -378,7 +288,7 @@ export function createRoutes(core, context = {}) {
|
|
|
378
288
|
to: email,
|
|
379
289
|
subject,
|
|
380
290
|
text
|
|
381
|
-
},
|
|
291
|
+
}, getRouteEmailConfig(core, context));
|
|
382
292
|
if (result.ok) {
|
|
383
293
|
console.info(`[Taskforce Auth Email] kind=${kind} provider=${result.provider} to=${email} messageId=${result.messageId || '-'}`);
|
|
384
294
|
}
|
|
@@ -396,7 +306,7 @@ export function createRoutes(core, context = {}) {
|
|
|
396
306
|
to: email,
|
|
397
307
|
subject,
|
|
398
308
|
text
|
|
399
|
-
},
|
|
309
|
+
}, getRouteEmailConfig(core, context));
|
|
400
310
|
if (result.ok) {
|
|
401
311
|
console.info(`[Taskforce Auth Email] kind=invite provider=${result.provider} to=${email} messageId=${result.messageId || '-'}`);
|
|
402
312
|
}
|
|
@@ -799,7 +709,10 @@ export function createRoutes(core, context = {}) {
|
|
|
799
709
|
const contentDisposition = params.forceDownload
|
|
800
710
|
? `attachment; filename="${resolvedDownloadName.replace(/"/g, '')}"`
|
|
801
711
|
: 'inline';
|
|
802
|
-
|
|
712
|
+
const readFromObjectStorage = async () => {
|
|
713
|
+
if (!objectStorageClient || resolvedObjectStorage.provider !== 'r2' || !resolvedObjectStorage.r2) {
|
|
714
|
+
return { statusCode: 404 };
|
|
715
|
+
}
|
|
803
716
|
if (!params.forceDownload) {
|
|
804
717
|
const object = await objectStorageClient.getObject(params.asset.storageKey);
|
|
805
718
|
if (!object) {
|
|
@@ -820,19 +733,22 @@ export function createRoutes(core, context = {}) {
|
|
|
820
733
|
Location: objectStorageClient.createSignedGetUrl(params.asset.storageKey, resolvedObjectStorage.r2.signedDownloadTtlSeconds, { responseContentDisposition: contentDisposition })
|
|
821
734
|
}
|
|
822
735
|
};
|
|
736
|
+
};
|
|
737
|
+
if (params.asset.storageProvider === 'r2' && objectStorageClient && resolvedObjectStorage.provider === 'r2' && resolvedObjectStorage.r2) {
|
|
738
|
+
return readFromObjectStorage();
|
|
823
739
|
}
|
|
824
740
|
const filePath = ensureWithinDir(path.join(basePath, params.asset.storageKey), basePath);
|
|
825
|
-
if (
|
|
826
|
-
return {
|
|
741
|
+
if (filePath && fs.existsSync(filePath)) {
|
|
742
|
+
return {
|
|
743
|
+
statusCode: 200,
|
|
744
|
+
headers: {
|
|
745
|
+
'Content-Type': params.asset.mimeType || contentTypeForExt(ext),
|
|
746
|
+
'Content-Disposition': contentDisposition
|
|
747
|
+
},
|
|
748
|
+
body: fs.readFileSync(filePath)
|
|
749
|
+
};
|
|
827
750
|
}
|
|
828
|
-
return
|
|
829
|
-
statusCode: 200,
|
|
830
|
-
headers: {
|
|
831
|
-
'Content-Type': params.asset.mimeType || contentTypeForExt(ext),
|
|
832
|
-
'Content-Disposition': contentDisposition
|
|
833
|
-
},
|
|
834
|
-
body: fs.readFileSync(filePath)
|
|
835
|
-
};
|
|
751
|
+
return readFromObjectStorage();
|
|
836
752
|
};
|
|
837
753
|
const getAssetMetadata = (pathValue) => {
|
|
838
754
|
const canonical = getCanonicalAssetMetadata(pathValue);
|
|
@@ -1109,7 +1025,7 @@ export function createRoutes(core, context = {}) {
|
|
|
1109
1025
|
return null;
|
|
1110
1026
|
return workspaceId;
|
|
1111
1027
|
};
|
|
1112
|
-
const ensureContextUploadAllowed = (req, res, workspaceId) => {
|
|
1028
|
+
const ensureContextUploadAllowed = (req, res, workspaceId, options) => {
|
|
1113
1029
|
if (requestRuntimeMode(req) !== 'cloud')
|
|
1114
1030
|
return true;
|
|
1115
1031
|
if (!ensureAuthenticatedUser(req)) {
|
|
@@ -1120,12 +1036,17 @@ export function createRoutes(core, context = {}) {
|
|
|
1120
1036
|
const gate = core.evaluateFeatureAccess({
|
|
1121
1037
|
userId: requestUserId(req),
|
|
1122
1038
|
workspaceId,
|
|
1123
|
-
featureKey: 'context.uploads'
|
|
1039
|
+
featureKey: 'context.uploads',
|
|
1040
|
+
context: {
|
|
1041
|
+
requestedBytes: options?.requestedBytes
|
|
1042
|
+
}
|
|
1124
1043
|
});
|
|
1125
1044
|
if (!gate.allowed) {
|
|
1126
1045
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
1127
1046
|
res.end(JSON.stringify({
|
|
1128
|
-
error:
|
|
1047
|
+
error: gate.reasonCode === 'UPLOAD_STORAGE_LIMIT_REACHED'
|
|
1048
|
+
? 'This upload exceeds the document cloud storage limit for the current plan.'
|
|
1049
|
+
: 'Document cloud storage is unavailable for this account entitlement.',
|
|
1129
1050
|
code: gate.reasonCode
|
|
1130
1051
|
}));
|
|
1131
1052
|
return false;
|
|
@@ -1551,9 +1472,6 @@ export function createRoutes(core, context = {}) {
|
|
|
1551
1472
|
requireCloudRuntime,
|
|
1552
1473
|
auditAuthAction,
|
|
1553
1474
|
resolveAdminRequestActor,
|
|
1554
|
-
shouldExposeDevAuthTokens,
|
|
1555
|
-
resolveRuntimeConfigBaseUrl,
|
|
1556
|
-
resolvePublicAuthBaseUrl,
|
|
1557
1475
|
resolveEffectiveObjectStorageConfig,
|
|
1558
1476
|
getObjectStorageClient,
|
|
1559
1477
|
});
|
|
@@ -1604,10 +1522,6 @@ export function createRoutes(core, context = {}) {
|
|
|
1604
1522
|
appendDocumentPurgeHistory,
|
|
1605
1523
|
getCollaborationTelemetry,
|
|
1606
1524
|
isCollaborationPolicyEnabled,
|
|
1607
|
-
resolveRuntimeConfigBaseUrl,
|
|
1608
|
-
resolvePublicAuthBaseUrl,
|
|
1609
|
-
parseFormBody,
|
|
1610
|
-
escapeHtml,
|
|
1611
1525
|
normalizeMcpOauthScopeString,
|
|
1612
1526
|
MCP_TOKEN_SCOPE_VALUES,
|
|
1613
1527
|
MCP_CLOUD_TOOL_SCOPE_REQUIREMENTS,
|