@taskforcehq/taskforce 0.3.327 → 0.3.328
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/dist/Taskforce.module.css +24 -53
- package/dist/components/activity/EntityActivityTimeline.js +15 -5
- package/dist/components/context/AttachmentPreviewHost.js +1 -1
- package/dist/components/context/ContextAttachmentManager.d.ts +3 -0
- package/dist/components/context/ContextAttachmentManager.js +116 -20
- package/dist/components/features/planning/PlanningEntitySummary.d.ts +3 -1
- package/dist/components/features/planning/PlanningEntitySummary.js +3 -2
- package/dist/components/features/planning/PlanningLinkPicker.d.ts +3 -1
- package/dist/components/features/planning/PlanningLinkPicker.js +5 -3
- package/dist/components/features/planning/PlanningModule.js +26 -9
- package/dist/components/features/planning/PlanningVisualIdentity.d.ts +14 -0
- package/dist/components/features/planning/PlanningVisualIdentity.js +106 -0
- package/dist/components/features/planning/planningWorkspaceModel.d.ts +11 -0
- package/dist/components/task/TaskActionHeader.d.ts +2 -1
- package/dist/components/task/TaskActionHeader.js +2 -2
- package/dist/components/task/TaskCard.js +37 -4
- package/dist/components/task/TaskContextUpload.d.ts +3 -0
- package/dist/components/task/TaskContextUpload.js +2 -2
- package/dist/components/task/TaskForm.d.ts +4 -0
- package/dist/components/task/TaskForm.js +32 -7
- package/dist/components/task/TaskImageReviews.js +16 -16
- package/dist/components/task/TaskStatusActions.js +11 -2
- package/dist/components/task/TaskWorkstreamPicker.js +2 -1
- package/dist/components/ui/DocumentTypeIcon.d.ts +7 -0
- package/dist/components/ui/DocumentTypeIcon.js +8 -0
- package/dist/components/ui/TaxonomyDropdown.d.ts +9 -1
- package/dist/components/ui/TaxonomyDropdown.js +30 -3
- package/dist/components/views/StandaloneLayout.js +62 -9
- package/dist/components/views/WidgetView.js +8 -5
- package/dist/components/views/panels/PlanningDrawer.d.ts +7 -1
- package/dist/components/views/panels/PlanningDrawer.js +48 -44
- package/dist/config/envSchema.js +1 -0
- package/dist/config/localServiceCloudTarget.d.ts +10 -0
- package/dist/config/localServiceCloudTarget.js +76 -0
- package/dist/core/AiProfileService.d.ts +1 -0
- package/dist/core/AiProfileService.js +2 -1
- package/dist/core/AttachmentLinkService.js +7 -4
- package/dist/core/DeletedTaskLifecycleService.d.ts +4 -0
- package/dist/core/DeletedTaskLifecycleService.js +21 -0
- package/dist/core/McpTokenService.d.ts +4 -0
- package/dist/core/McpTokenService.js +52 -0
- package/dist/core/TaskAttachmentLinksDurableMutationService.d.ts +5 -0
- package/dist/core/TaskAttachmentLinksDurableMutationService.js +20 -11
- package/dist/core/Taskforce.d.ts +63 -1
- package/dist/core/Taskforce.js +558 -73
- package/dist/core/types.d.ts +2 -1
- package/dist/documentReviews/DocumentReviewCommentStore.d.ts +11 -0
- package/dist/documentReviews/DocumentReviewCommentStore.js +35 -2
- package/dist/documentReviews/service.d.ts +4 -0
- package/dist/documentReviews/service.js +141 -46
- package/dist/hooks/tasks/useTaskFormActions.d.ts +3 -0
- package/dist/hooks/tasks/useTaskFormActions.js +33 -7
- package/dist/hooks/useTaskforce.d.ts +59 -50
- package/dist/hooks/useTaskforce.js +103 -1
- package/dist/mcp/canonicalAssetHelpers.d.ts +13 -0
- package/dist/mcp/canonicalAssetHelpers.js +39 -5
- package/dist/mcp/collaborationRegistrar.js +15 -13
- package/dist/mcp/documentAssetRegistrar.js +16 -9
- package/dist/mcp/localServiceProxy.js +4 -1
- package/dist/mcp/runtime.d.ts +16 -0
- package/dist/mcp/runtime.js +566 -171
- package/dist/mcp/taskAttachmentCommandAdapter.d.ts +2 -0
- package/dist/mcp/taskAttachmentCommandAdapter.js +14 -8
- package/dist/mcp/taskPlanningRegistrar.js +42 -34
- package/dist/mcp/toolRegistry.js +2 -2
- package/dist/migrations/taskSchemaMigrations.js +35 -0
- package/dist/runtime/appGateDefinitions.d.ts +2 -2
- package/dist/runtime/appGateDefinitions.js +2 -2
- package/dist/server/appAccess.d.ts +12 -0
- package/dist/server/appAccess.js +21 -0
- package/dist/server/authorizedWorkspaceAccess.d.ts +12 -0
- package/dist/server/authorizedWorkspaceAccess.js +40 -0
- package/dist/server/cloudCollectionRead.d.ts +25 -0
- package/dist/server/cloudCollectionRead.js +120 -0
- package/dist/server/cloudCollectionReadPool.d.ts +52 -0
- package/dist/server/cloudCollectionReadPool.js +140 -0
- package/dist/server/cloudCollectionReadWorker.d.ts +1 -0
- package/dist/server/cloudCollectionReadWorker.js +36 -0
- package/dist/server/index.d.ts +3 -11
- package/dist/server/index.js +53 -40
- package/dist/server/localServiceLease.d.ts +3 -0
- package/dist/server/localServiceLease.js +9 -1
- package/dist/server/routes/documents.d.ts +5 -2
- package/dist/server/routes/documents.js +160 -13
- package/dist/server/routes/durableTaskHttpRouters.d.ts +2 -0
- package/dist/server/routes/durableTaskHttpRouters.js +2 -2
- package/dist/server/routes/localService.js +2 -0
- package/dist/server/routes/primitives.d.ts +2 -0
- package/dist/server/routes/shared.d.ts +24 -0
- package/dist/server/routes/shared.js +114 -18
- package/dist/server/routes/sync.js +22 -0
- package/dist/server/routes/syncV3FeedRoutes.js +4 -2
- package/dist/server/routes/syncV3LocalCaptureRoutes.js +20 -2
- package/dist/server/routes/syncV3OperationRoutes.js +17 -2
- package/dist/server/routes/tasks.js +199 -89
- package/dist/server/routes.js +35 -7
- package/dist/service/localServiceCli.js +36 -25
- package/dist/service/localServiceClientLifecycle.js +67 -6
- package/dist/shared/planningIdentity.d.ts +20 -0
- package/dist/shared/planningIdentity.js +79 -0
- package/dist/storage/postgresAdapter.js +4 -0
- package/dist/storage/postgresRequestDiagnostics.d.ts +8 -0
- package/dist/storage/postgresRequestDiagnostics.js +24 -0
- package/dist/storage/workspaceAssetStore.d.ts +32 -0
- package/dist/storage/workspaceAssetStore.js +70 -0
- package/dist/sync/coordinator/localSyncExecutionPermitStore.d.ts +16 -0
- package/dist/sync/coordinator/localSyncExecutionPermitStore.js +70 -22
- package/dist/sync/coordinator/workspaceSyncTransportGateway.js +4 -2
- package/dist/sync/engine/workspaceSyncEngineRemoteServices.d.ts +5 -0
- package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.d.ts +1 -1
- package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.js +15 -0
- package/dist/sync/engine/workspaceSyncEngineServerRunnerOperations.js +26 -3
- package/dist/sync/engine/workspaceSyncV2ChangeDispatcher.js +2 -0
- package/dist/sync/syncApplyHandlers.d.ts +3 -1
- package/dist/sync/syncApplyHandlers.js +29 -0
- package/dist/sync/syncService.js +9 -0
- package/dist/sync/taskSyncPayload.d.ts +1 -0
- package/dist/sync/taskSyncPayload.js +6 -0
- package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.d.ts +1 -0
- package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.js +21 -3
- package/dist/sync/v3/durableTaskHttpBulkMutationRouter.js +17 -4
- package/dist/sync/v3/localInitiativeOutboxService.js +4 -2
- package/dist/sync/v3/localOutboxDispatcher.js +5 -1
- package/dist/sync/v3/localTaskAttachmentLinksOutboxHandler.js +12 -2
- package/dist/sync/v3/localTaskAttachmentLinksOutboxService.d.ts +1 -0
- package/dist/sync/v3/localTaskAttachmentLinksOutboxService.js +15 -3
- package/dist/sync/v3/localWorkstreamOutboxService.js +4 -2
- package/dist/sync/v3/syncDurabilityStore.d.ts +8 -0
- package/dist/sync/v3/syncDurabilityStore.js +171 -33
- package/dist/sync/v3/taskAttachmentLinksMutationService.d.ts +1 -0
- package/dist/sync/v3/taskAttachmentLinksMutationService.js +18 -2
- package/dist/sync/v3/taskMetadataCoexistence.js +4 -0
- package/dist/sync/v3/workspaceSyncV2ProjectionExclusions.js +1 -9
- package/dist/sync/v3/workspaceSyncV3CloudOperationHandler.js +1 -0
- package/dist/sync/v3/workspaceSyncV3CombinedFeedOrchestrator.js +7 -1
- package/dist/sync/v3/workspaceSyncV3CombinedLocalApply.js +15 -3
- package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.d.ts +4 -0
- package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.js +29 -6
- package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.d.ts +1 -0
- package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.js +29 -7
- package/dist/sync/v3/workspaceSyncV3CombinedRepairSnapshot.js +62 -7
- package/dist/sync/v3/workspaceSyncV3OperationProtocol.d.ts +1 -0
- package/dist/sync/v3/workspaceSyncV3RepairSqlBatching.d.ts +2 -0
- package/dist/sync/v3/workspaceSyncV3RepairSqlBatching.js +10 -0
- package/dist/sync/v3/workspaceSyncV3TaskRootProjection.d.ts +1 -1
- package/dist/sync/v3/workspaceSyncV3TaskRootProjection.js +1 -1
- package/dist/sync/workspacePullFeed.d.ts +6 -1
- package/dist/sync/workspacePullFeed.js +172 -161
- package/dist/sync/workspaceSyncModel.d.ts +11 -0
- package/dist/sync/workspaceSyncModel.js +27 -2
- package/dist/types.d.ts +8 -0
- package/dist/ui/assets/{AiIdentityRosterCard-DB1BLO_j.js → AiIdentityRosterCard-MZs7lVED.js} +1 -1
- package/dist/ui/assets/{AiProfilesModule-DPxQeF9N.js → AiProfilesModule-C3i0LOxe.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-DHNYaNg6.js → AnnotatedAttachmentWorkspace-DVZk5Tln.js} +1 -1
- package/dist/ui/assets/AssetTraySortControl-Kf1pelSy.js +1 -0
- package/dist/ui/assets/ContextAttachmentManager-B8boOs6Q.js +3 -0
- package/dist/ui/assets/{DocumentWorkspace-BC7sy5TG.js → DocumentWorkspace-DXH4XUsB.js} +4 -4
- package/dist/ui/assets/EntityActivityTimeline-BtSTq0ON.js +1 -0
- package/dist/ui/assets/PlanningModule-BPVyEa-f.js +1 -0
- package/dist/ui/assets/PlanningModule-CoIR9hbV.css +1 -0
- package/dist/ui/assets/{PlansPage-BVH_NfgT.js → PlansPage-ChqgxALc.js} +1 -1
- package/dist/ui/assets/TaskContextUpload-CB3ol0sN.js +1 -0
- package/dist/ui/assets/TaskContextUpload-qmBeUw3u.css +1 -0
- package/dist/ui/assets/{TaskSettings-xVsxSa0g.js → TaskSettings-g4ECKNOz.js} +1 -1
- package/dist/ui/assets/{TaskforceAgentsModule-CkAxW19K.js → TaskforceAgentsModule-BazdUwxZ.js} +1 -1
- package/dist/ui/assets/WorkflowManagerModule-DWFspC-o.js +1 -0
- package/dist/ui/assets/index-BUplSsv_.js +7 -0
- package/dist/ui/assets/index-gz2EXuoK.css +1 -0
- package/dist/ui/assets/{vendor-icons-B8ZNsH1g.js → vendor-icons-BkFLXavV.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/utils/constants.d.ts +2 -2
- package/dist/utils/constants.js +1 -1
- package/dist/utils/contextAttachmentUpload.d.ts +2 -0
- package/dist/utils/contextAttachmentUpload.js +7 -0
- package/dist/utils/contextFiles.d.ts +1 -1
- package/dist/utils/contextFiles.js +4 -4
- package/package.json +6 -1
- package/dist/ui/assets/AssetTraySortControl-DphbyHVc.js +0 -1
- package/dist/ui/assets/ContextAttachmentManager-CMxSzHqc.js +0 -3
- package/dist/ui/assets/EntityActivityTimeline-pJ2ZjZ-k.js +0 -1
- package/dist/ui/assets/PlanningModule-CeTqXMMz.css +0 -1
- package/dist/ui/assets/PlanningModule-t5GbvVR9.js +0 -1
- package/dist/ui/assets/TaskContextUpload-Ds4LnDzV.css +0 -1
- package/dist/ui/assets/TaskContextUpload-bl4fOOUz.js +0 -1
- package/dist/ui/assets/WorkflowManagerModule-BKqIGVnF.js +0 -1
- package/dist/ui/assets/index-BzJlVnNS.css +0 -1
- package/dist/ui/assets/index-Cx7vZJHL.js +0 -7
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { getRequestPerformanceDiagnostics, runWithRequestPerformanceDiagnostics, } from '../storage/postgresRequestDiagnostics.js';
|
|
2
|
+
import { resolveAppAccessDeniedResponse } from './appAccess.js';
|
|
3
|
+
import { resolveAuthorizedWorkspaceAccess, resolveDisabledAccessError, } from './authorizedWorkspaceAccess.js';
|
|
4
|
+
const ROUTE_BY_OPERATION = {
|
|
5
|
+
tasks: '/api/taskforce/tasks',
|
|
6
|
+
archive: '/api/taskforce/archive',
|
|
7
|
+
deleted: '/api/taskforce/deleted',
|
|
8
|
+
'planning-bootstrap': '/api/taskforce/planning/bootstrap',
|
|
9
|
+
};
|
|
10
|
+
export function resolveCloudCollectionReadOperation(method, pathname) {
|
|
11
|
+
if (String(method || 'GET').toUpperCase() !== 'GET')
|
|
12
|
+
return null;
|
|
13
|
+
if (pathname === '/api/taskforce/tasks')
|
|
14
|
+
return 'tasks';
|
|
15
|
+
if (pathname === '/api/taskforce/archive')
|
|
16
|
+
return 'archive';
|
|
17
|
+
if (pathname === '/api/taskforce/deleted' || pathname === '/api/taskforce/trash')
|
|
18
|
+
return 'deleted';
|
|
19
|
+
if (pathname === '/api/taskforce/planning/bootstrap')
|
|
20
|
+
return 'planning-bootstrap';
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
function serverTimingMetric(name, durationMs) {
|
|
24
|
+
return `${name};dur=${Math.max(0, Math.round(durationMs))}`;
|
|
25
|
+
}
|
|
26
|
+
function performanceHeaders(diagnostics, responseBytes, metrics) {
|
|
27
|
+
const headers = {
|
|
28
|
+
'Content-Type': 'application/json',
|
|
29
|
+
'Server-Timing': metrics.map((metric) => serverTimingMetric(metric.name, metric.durationMs)).join(', '),
|
|
30
|
+
};
|
|
31
|
+
if (diagnostics) {
|
|
32
|
+
headers['X-Taskforce-Postgres-Query-Count'] = String(diagnostics.postgresQueryCount);
|
|
33
|
+
headers['X-Taskforce-Postgres-Worker-Call-Count'] = String(diagnostics.postgresWorkerCallCount);
|
|
34
|
+
headers['X-Taskforce-Response-Bytes'] = String(responseBytes);
|
|
35
|
+
}
|
|
36
|
+
return headers;
|
|
37
|
+
}
|
|
38
|
+
export function executeCloudCollectionRead(core, request) {
|
|
39
|
+
return runWithRequestPerformanceDiagnostics(request.performanceTrace, () => {
|
|
40
|
+
const access = resolveAuthorizedWorkspaceAccess(core, request.access, request.requestedWorkspaceId);
|
|
41
|
+
const workspaceId = String(access.workspaceId || '').trim();
|
|
42
|
+
const disabledAccessError = resolveDisabledAccessError(access, request.pathname);
|
|
43
|
+
if (disabledAccessError) {
|
|
44
|
+
return {
|
|
45
|
+
statusCode: 403,
|
|
46
|
+
headers: { 'Content-Type': 'application/json' },
|
|
47
|
+
body: JSON.stringify({ error: disabledAccessError }),
|
|
48
|
+
workspaceId,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (access.authenticated && access.userId && access.userId !== 'anonymous') {
|
|
52
|
+
const denied = resolveAppAccessDeniedResponse(core.getAccountAccessStatus(access.userId));
|
|
53
|
+
if (denied) {
|
|
54
|
+
return {
|
|
55
|
+
statusCode: denied.statusCode,
|
|
56
|
+
headers: { 'Content-Type': 'application/json' },
|
|
57
|
+
body: JSON.stringify(denied.body),
|
|
58
|
+
workspaceId,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const startedAt = performance.now();
|
|
63
|
+
let data;
|
|
64
|
+
const planningDiagnostics = {};
|
|
65
|
+
if (request.operation === 'tasks')
|
|
66
|
+
data = core.listTasks(workspaceId);
|
|
67
|
+
else if (request.operation === 'archive')
|
|
68
|
+
data = core.listArchive(workspaceId);
|
|
69
|
+
else if (request.operation === 'deleted')
|
|
70
|
+
data = core.listDeletedTasksReadOnly(workspaceId);
|
|
71
|
+
else
|
|
72
|
+
data = core.listPlanningBootstrap(workspaceId, planningDiagnostics);
|
|
73
|
+
const loadCompletedAt = performance.now();
|
|
74
|
+
const serializeStartedAt = performance.now();
|
|
75
|
+
const body = JSON.stringify(data);
|
|
76
|
+
const serializeDurationMs = performance.now() - serializeStartedAt;
|
|
77
|
+
const totalDurationMs = performance.now() - startedAt;
|
|
78
|
+
const responseBytes = Buffer.byteLength(body, 'utf8');
|
|
79
|
+
const diagnostics = getRequestPerformanceDiagnostics();
|
|
80
|
+
const metrics = request.operation === 'planning-bootstrap'
|
|
81
|
+
? [
|
|
82
|
+
{ name: 'planning-bootstrap', durationMs: totalDurationMs },
|
|
83
|
+
{ name: 'planning-bootstrap-initiatives', durationMs: planningDiagnostics.initiativeLoadMs || 0 },
|
|
84
|
+
{ name: 'planning-bootstrap-workstreams', durationMs: planningDiagnostics.workstreamLoadMs || 0 },
|
|
85
|
+
{ name: 'planning-bootstrap-tasks', durationMs: planningDiagnostics.taskSummaryQueryMs || 0 },
|
|
86
|
+
{ name: 'planning-bootstrap-build', durationMs: planningDiagnostics.taskSummaryBuildMs || 0 },
|
|
87
|
+
{ name: 'planning-bootstrap-json', durationMs: serializeDurationMs },
|
|
88
|
+
]
|
|
89
|
+
: [
|
|
90
|
+
{ name: 'route-total', durationMs: totalDurationMs },
|
|
91
|
+
{ name: 'route-load', durationMs: loadCompletedAt - startedAt },
|
|
92
|
+
{ name: 'route-json', durationMs: serializeDurationMs },
|
|
93
|
+
];
|
|
94
|
+
if (diagnostics) {
|
|
95
|
+
metrics.push({ name: 'postgres-worker-wait', durationMs: diagnostics.postgresWorkerWaitMs });
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
statusCode: 200,
|
|
99
|
+
headers: performanceHeaders(diagnostics, responseBytes, metrics),
|
|
100
|
+
body,
|
|
101
|
+
workspaceId,
|
|
102
|
+
performance: {
|
|
103
|
+
durationMs: totalDurationMs,
|
|
104
|
+
details: {
|
|
105
|
+
responseBytes,
|
|
106
|
+
jsonSerializeMs: serializeDurationMs,
|
|
107
|
+
...(diagnostics ? {
|
|
108
|
+
postgresQueryCount: diagnostics.postgresQueryCount,
|
|
109
|
+
postgresWorkerCallCount: diagnostics.postgresWorkerCallCount,
|
|
110
|
+
postgresWorkerWaitMs: Math.round(diagnostics.postgresWorkerWaitMs),
|
|
111
|
+
} : {}),
|
|
112
|
+
...(request.operation === 'planning-bootstrap' ? planningDiagnostics : {}),
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
export function cloudCollectionReadRoute(operation) {
|
|
119
|
+
return ROUTE_BY_OPERATION[operation];
|
|
120
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { CloudCollectionReadRequest, CloudCollectionReadResponse } from './cloudCollectionRead.js';
|
|
2
|
+
export interface CloudCollectionReadPoolOptions {
|
|
3
|
+
projectRoot: string;
|
|
4
|
+
tenantId: string;
|
|
5
|
+
connectionString: string;
|
|
6
|
+
size?: number;
|
|
7
|
+
requestTimeoutMs?: number;
|
|
8
|
+
workerFactory?: () => CloudCollectionReadWorker;
|
|
9
|
+
}
|
|
10
|
+
export interface CloudCollectionReadWorker {
|
|
11
|
+
postMessage(message: unknown): void;
|
|
12
|
+
terminate(): Promise<number>;
|
|
13
|
+
on(event: 'message', listener: (message: CloudCollectionReadWorkerResponse) => void): this;
|
|
14
|
+
on(event: 'error', listener: (error: unknown) => void): this;
|
|
15
|
+
on(event: 'exit', listener: (code: number) => void): this;
|
|
16
|
+
}
|
|
17
|
+
export type CloudCollectionReadWorkerResponse = {
|
|
18
|
+
type: 'ready';
|
|
19
|
+
} | {
|
|
20
|
+
type: 'result';
|
|
21
|
+
requestId: number;
|
|
22
|
+
response: CloudCollectionReadResponse;
|
|
23
|
+
} | {
|
|
24
|
+
type: 'error';
|
|
25
|
+
requestId: number;
|
|
26
|
+
error: string;
|
|
27
|
+
};
|
|
28
|
+
export declare class CloudCollectionReadPool {
|
|
29
|
+
private readonly slots;
|
|
30
|
+
private readonly queue;
|
|
31
|
+
private readonly requestTimeoutMs;
|
|
32
|
+
private readonly desiredSize;
|
|
33
|
+
private readonly workerData;
|
|
34
|
+
private readonly workerFactory;
|
|
35
|
+
private nextRequestId;
|
|
36
|
+
private closing;
|
|
37
|
+
constructor(options: CloudCollectionReadPoolOptions);
|
|
38
|
+
execute(request: CloudCollectionReadRequest): Promise<CloudCollectionReadResponse>;
|
|
39
|
+
close(): Promise<void>;
|
|
40
|
+
private spawnSlot;
|
|
41
|
+
private handleMessage;
|
|
42
|
+
private handleWorkerFailure;
|
|
43
|
+
private ensureSlots;
|
|
44
|
+
private pump;
|
|
45
|
+
}
|
|
46
|
+
export declare function resolveCloudCollectionReadPoolSize(env?: NodeJS.ProcessEnv): number;
|
|
47
|
+
export declare function shouldUseCloudCollectionReadPool(input: {
|
|
48
|
+
runtimeMode: 'local' | 'cloud';
|
|
49
|
+
databaseProvider: 'sqlite' | 'postgres';
|
|
50
|
+
connectionString?: string;
|
|
51
|
+
workerCount: number;
|
|
52
|
+
}): boolean;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Worker } from 'node:worker_threads';
|
|
2
|
+
const DEFAULT_POOL_SIZE = 2;
|
|
3
|
+
const MAX_POOL_SIZE = 8;
|
|
4
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
5
|
+
function positiveInteger(value, fallback) {
|
|
6
|
+
if (!Number.isFinite(value) || Number(value) <= 0)
|
|
7
|
+
return fallback;
|
|
8
|
+
return Math.max(1, Math.floor(Number(value)));
|
|
9
|
+
}
|
|
10
|
+
export class CloudCollectionReadPool {
|
|
11
|
+
slots = [];
|
|
12
|
+
queue = [];
|
|
13
|
+
requestTimeoutMs;
|
|
14
|
+
desiredSize;
|
|
15
|
+
workerData;
|
|
16
|
+
workerFactory;
|
|
17
|
+
nextRequestId = 1;
|
|
18
|
+
closing = false;
|
|
19
|
+
constructor(options) {
|
|
20
|
+
this.requestTimeoutMs = positiveInteger(options.requestTimeoutMs, DEFAULT_REQUEST_TIMEOUT_MS);
|
|
21
|
+
this.workerData = {
|
|
22
|
+
projectRoot: options.projectRoot,
|
|
23
|
+
tenantId: options.tenantId,
|
|
24
|
+
connectionString: options.connectionString,
|
|
25
|
+
};
|
|
26
|
+
this.workerFactory = options.workerFactory || (() => new Worker(new URL('./cloudCollectionReadWorker.js', import.meta.url), { workerData: this.workerData }));
|
|
27
|
+
this.desiredSize = Math.min(MAX_POOL_SIZE, positiveInteger(options.size, DEFAULT_POOL_SIZE));
|
|
28
|
+
}
|
|
29
|
+
execute(request) {
|
|
30
|
+
if (this.closing) {
|
|
31
|
+
return Promise.reject(new Error('[Taskforce] Cloud collection read pool is closed.'));
|
|
32
|
+
}
|
|
33
|
+
this.ensureSlots();
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
this.queue.push({ request, resolve, reject });
|
|
36
|
+
this.pump();
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
async close() {
|
|
40
|
+
if (this.closing)
|
|
41
|
+
return;
|
|
42
|
+
this.closing = true;
|
|
43
|
+
const closedError = new Error('[Taskforce] Cloud collection read pool closed before the request completed.');
|
|
44
|
+
for (const pending of this.queue.splice(0))
|
|
45
|
+
pending.reject(closedError);
|
|
46
|
+
await Promise.all(this.slots.splice(0).map(async (slot) => {
|
|
47
|
+
if (slot.active) {
|
|
48
|
+
clearTimeout(slot.active.timeout);
|
|
49
|
+
slot.active.reject(closedError);
|
|
50
|
+
slot.active = null;
|
|
51
|
+
}
|
|
52
|
+
slot.worker.postMessage({ type: 'close' });
|
|
53
|
+
await slot.worker.terminate();
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
spawnSlot() {
|
|
57
|
+
if (this.closing)
|
|
58
|
+
return;
|
|
59
|
+
const worker = this.workerFactory();
|
|
60
|
+
const slot = { worker, ready: false, active: null };
|
|
61
|
+
this.slots.push(slot);
|
|
62
|
+
worker.on('message', (message) => this.handleMessage(slot, message));
|
|
63
|
+
worker.on('error', (error) => this.handleWorkerFailure(slot, error instanceof Error ? error : new Error(String(error))));
|
|
64
|
+
worker.on('exit', (code) => {
|
|
65
|
+
if (!this.closing && this.slots.includes(slot)) {
|
|
66
|
+
this.handleWorkerFailure(slot, new Error(`[Taskforce] Cloud collection read worker exited with code ${code}.`));
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
handleMessage(slot, message) {
|
|
71
|
+
if (message.type === 'ready') {
|
|
72
|
+
slot.ready = true;
|
|
73
|
+
this.ensureSlots();
|
|
74
|
+
this.pump();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const active = slot.active;
|
|
78
|
+
if (!active || active.requestId !== message.requestId)
|
|
79
|
+
return;
|
|
80
|
+
clearTimeout(active.timeout);
|
|
81
|
+
slot.active = null;
|
|
82
|
+
if (message.type === 'result')
|
|
83
|
+
active.resolve(message.response);
|
|
84
|
+
else
|
|
85
|
+
active.reject(new Error(message.error || '[Taskforce] Cloud collection read worker failed.'));
|
|
86
|
+
this.pump();
|
|
87
|
+
}
|
|
88
|
+
handleWorkerFailure(slot, error) {
|
|
89
|
+
const slotIndex = this.slots.indexOf(slot);
|
|
90
|
+
if (slotIndex < 0)
|
|
91
|
+
return;
|
|
92
|
+
this.slots.splice(slotIndex, 1);
|
|
93
|
+
if (slot.active) {
|
|
94
|
+
clearTimeout(slot.active.timeout);
|
|
95
|
+
slot.active.reject(error);
|
|
96
|
+
slot.active = null;
|
|
97
|
+
}
|
|
98
|
+
void slot.worker.terminate();
|
|
99
|
+
if (!this.closing && this.slots.length === 0) {
|
|
100
|
+
for (const pending of this.queue.splice(0))
|
|
101
|
+
pending.reject(error);
|
|
102
|
+
}
|
|
103
|
+
this.pump();
|
|
104
|
+
}
|
|
105
|
+
ensureSlots() {
|
|
106
|
+
if (this.closing)
|
|
107
|
+
return;
|
|
108
|
+
if (this.slots.some((slot) => !slot.ready))
|
|
109
|
+
return;
|
|
110
|
+
if (this.slots.length < this.desiredSize)
|
|
111
|
+
this.spawnSlot();
|
|
112
|
+
}
|
|
113
|
+
pump() {
|
|
114
|
+
if (this.closing || this.queue.length === 0)
|
|
115
|
+
return;
|
|
116
|
+
for (const slot of this.slots) {
|
|
117
|
+
if (!slot.ready || slot.active || this.queue.length === 0)
|
|
118
|
+
continue;
|
|
119
|
+
const pending = this.queue.shift();
|
|
120
|
+
const requestId = this.nextRequestId++;
|
|
121
|
+
const timeout = setTimeout(() => {
|
|
122
|
+
this.handleWorkerFailure(slot, new Error(`[Taskforce] Cloud collection read worker timed out after ${this.requestTimeoutMs}ms.`));
|
|
123
|
+
}, this.requestTimeoutMs);
|
|
124
|
+
timeout.unref?.();
|
|
125
|
+
slot.active = { ...pending, requestId, timeout };
|
|
126
|
+
slot.worker.postMessage({ type: 'execute', requestId, request: pending.request });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
export function resolveCloudCollectionReadPoolSize(env = process.env) {
|
|
131
|
+
if (String(env.TASKFORCE_CLOUD_READ_WORKERS || '').trim() === '0')
|
|
132
|
+
return 0;
|
|
133
|
+
return Math.min(MAX_POOL_SIZE, positiveInteger(Number(env.TASKFORCE_CLOUD_READ_WORKERS), DEFAULT_POOL_SIZE));
|
|
134
|
+
}
|
|
135
|
+
export function shouldUseCloudCollectionReadPool(input) {
|
|
136
|
+
return input.runtimeMode === 'cloud'
|
|
137
|
+
&& input.databaseProvider === 'postgres'
|
|
138
|
+
&& Boolean(String(input.connectionString || '').trim())
|
|
139
|
+
&& input.workerCount > 0;
|
|
140
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
2
|
+
import { TaskforceCore } from '../core/Taskforce.js';
|
|
3
|
+
import { executeCloudCollectionRead, } from './cloudCollectionRead.js';
|
|
4
|
+
if (!parentPort) {
|
|
5
|
+
throw new Error('[Taskforce] Cloud collection read worker requires a parent port.');
|
|
6
|
+
}
|
|
7
|
+
const config = workerData;
|
|
8
|
+
const core = new TaskforceCore({
|
|
9
|
+
projectRoot: config.projectRoot,
|
|
10
|
+
tenantId: config.tenantId,
|
|
11
|
+
runtimeMode: 'cloud',
|
|
12
|
+
initializationMode: 'cloud-read-worker',
|
|
13
|
+
database: {
|
|
14
|
+
provider: 'postgres',
|
|
15
|
+
connectionString: config.connectionString,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
parentPort.on('message', (message) => {
|
|
19
|
+
if (message.type === 'close') {
|
|
20
|
+
core.close();
|
|
21
|
+
parentPort?.close();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const response = executeCloudCollectionRead(core, message.request);
|
|
26
|
+
parentPort?.postMessage({ type: 'result', requestId: message.requestId, response });
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
parentPort?.postMessage({
|
|
30
|
+
type: 'error',
|
|
31
|
+
requestId: message.requestId,
|
|
32
|
+
error: error instanceof Error ? error.message : String(error),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
parentPort.postMessage({ type: 'ready' });
|
package/dist/server/index.d.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as http from 'http';
|
|
8
8
|
import { WebSocketServer } from 'ws';
|
|
9
|
-
|
|
10
|
-
import type {
|
|
9
|
+
export { resolveAuthorizedWorkspaceAccess, resolveDisabledAccessError, isWorkspaceAccessRecoveryRoute, } from './authorizedWorkspaceAccess.js';
|
|
10
|
+
import type { LocalServiceCloudTarget } from '../config/localServiceCloudTarget.js';
|
|
11
11
|
import type { LocalSyncCoordinatorHost } from '../sync/coordinator/localSyncCoordinatorHost.js';
|
|
12
12
|
import type { LocalSyncProcessLock } from '../sync/coordinator/localSyncProcessLock.js';
|
|
13
13
|
import type { LocalDataOwnerPermit } from './localDataOwnerPermit.js';
|
|
@@ -25,6 +25,7 @@ export interface StandaloneServerOptions {
|
|
|
25
25
|
ownerPid: number;
|
|
26
26
|
bootstrapCapability: string;
|
|
27
27
|
ownershipKey: string;
|
|
28
|
+
cloudTarget: LocalServiceCloudTarget;
|
|
28
29
|
};
|
|
29
30
|
}
|
|
30
31
|
export interface TaskforceStandaloneServer extends http.Server {
|
|
@@ -45,17 +46,9 @@ export declare const TASKFORCE_PROXY_ORIGIN_PROTO_HEADER = "x-taskforce-origin-p
|
|
|
45
46
|
export declare const TASKFORCE_PROXY_ORIGIN_BASE_URL_HEADER = "x-taskforce-origin-base-url";
|
|
46
47
|
export declare const TASKFORCE_PROXY_ORIGIN_RUNTIME_HEADER = "x-taskforce-origin-runtime";
|
|
47
48
|
export declare const TASKFORCE_PROXY_CLOUD_ENVIRONMENT_HEADER = "x-taskforce-cloud-environment";
|
|
48
|
-
interface AuthorizedRequestAccess extends RequestAccessContext {
|
|
49
|
-
disabled?: boolean;
|
|
50
|
-
accountDisabled?: boolean;
|
|
51
|
-
workspaceDisabled?: boolean;
|
|
52
|
-
}
|
|
53
|
-
export declare function isWorkspaceAccessRecoveryRoute(pathname: string): boolean;
|
|
54
|
-
export declare function resolveDisabledAccessError(access: Pick<AuthorizedRequestAccess, 'disabled' | 'accountDisabled' | 'workspaceDisabled'>, pathname: string): string | null;
|
|
55
49
|
export declare function shouldProxyCloudPath(pathname: string): boolean;
|
|
56
50
|
export declare function shouldProxyCloudRequest(url: Pick<URL, 'pathname' | 'searchParams'>, env?: NodeJS.ProcessEnv): boolean;
|
|
57
51
|
export declare function resolveCloudProxyUpstreamBaseUrl(req: http.IncomingMessage, fallbackBaseUrl: string): string;
|
|
58
|
-
export declare function resolveAuthorizedWorkspaceAccess(core: Pick<TaskforceCore, 'resolveUserAccess' | 'getSystemRole'>, access: AuthorizedRequestAccess, requestedWorkspaceId: string): AuthorizedRequestAccess;
|
|
59
52
|
export declare function shouldApplyReadOnlyMutationGuard(methodRaw: string | undefined, pathname: string): boolean;
|
|
60
53
|
export declare function resolveEffectiveRequestUserId(accessUserId: string, explicitUserId: string): string;
|
|
61
54
|
export declare function resolveMaxPayloadBytesForPath(pathname: string, defaultMaxBytes: number): number;
|
|
@@ -72,4 +65,3 @@ export declare function ensurePublicPricingAuthExclusion(excludedPaths: string[]
|
|
|
72
65
|
export declare function ensurePublicSiteConfigAuthExclusion(excludedPaths: string[]): string[];
|
|
73
66
|
export declare function isBillingWebhookPath(pathname: string): boolean;
|
|
74
67
|
export declare function createStandaloneServer(options: StandaloneServerOptions): TaskforceStandaloneServer;
|
|
75
|
-
export {};
|
package/dist/server/index.js
CHANGED
|
@@ -14,9 +14,12 @@ import chokidar from 'chokidar';
|
|
|
14
14
|
import { TaskforceCore } from '../core/Taskforce.js';
|
|
15
15
|
import { createRoutes, matchRoute } from './routes.js';
|
|
16
16
|
import { formatApiErrorResponse } from './routes/primitives.js';
|
|
17
|
+
import { logSlowBootstrapRoute } from './routes/shared.js';
|
|
17
18
|
import { applyCorsHeaders, getAllowedOriginsFromEnv } from './cors.js';
|
|
18
19
|
import { applyPrefixedRateLimitHeaders, applyRateLimitHeaders, InMemoryRateLimiter, resolveRateLimitKey } from './rateLimit.js';
|
|
19
20
|
import { isRequestAuthorized, resolveAuthConfigFromEnv, resolveRequestAccess } from './auth.js';
|
|
21
|
+
import { resolveAuthorizedWorkspaceAccess, resolveDisabledAccessError, } from './authorizedWorkspaceAccess.js';
|
|
22
|
+
export { resolveAuthorizedWorkspaceAccess, resolveDisabledAccessError, isWorkspaceAccessRecoveryRoute, } from './authorizedWorkspaceAccess.js';
|
|
20
23
|
import { assertCloudDatabaseProvider, resolveRuntimeModeFromEnv } from './runtimeMode.js';
|
|
21
24
|
import { applySecurityHeaders } from './securityHeaders.js';
|
|
22
25
|
import { resolveDatabaseConfigFromEnv } from '../storage/providerConfig.js';
|
|
@@ -37,6 +40,8 @@ import { createWorkspaceSyncServerComposition, shutdownWorkspaceSyncServerCompos
|
|
|
37
40
|
import { LocalServiceSessionManager } from './localServiceSessions.js';
|
|
38
41
|
import { LocalSyncIdentityStore } from '../sync/v3/localSyncIdentityStore.js';
|
|
39
42
|
import { shouldBroadcastLocalFileInvalidation } from './localRealtimeFileInvalidation.js';
|
|
43
|
+
import { resolveCloudCollectionReadOperation, } from './cloudCollectionRead.js';
|
|
44
|
+
import { CloudCollectionReadPool, resolveCloudCollectionReadPoolSize, shouldUseCloudCollectionReadPool, } from './cloudCollectionReadPool.js';
|
|
40
45
|
const __filename = fileURLToPath(import.meta.url);
|
|
41
46
|
const __dirname = path.dirname(__filename);
|
|
42
47
|
const MIME_TYPES = {
|
|
@@ -173,21 +178,6 @@ export const TASKFORCE_PROXY_ORIGIN_PROTO_HEADER = 'x-taskforce-origin-proto';
|
|
|
173
178
|
export const TASKFORCE_PROXY_ORIGIN_BASE_URL_HEADER = 'x-taskforce-origin-base-url';
|
|
174
179
|
export const TASKFORCE_PROXY_ORIGIN_RUNTIME_HEADER = 'x-taskforce-origin-runtime';
|
|
175
180
|
export const TASKFORCE_PROXY_CLOUD_ENVIRONMENT_HEADER = 'x-taskforce-cloud-environment';
|
|
176
|
-
export function isWorkspaceAccessRecoveryRoute(pathname) {
|
|
177
|
-
return pathname === '/api/taskforce/workspaces'
|
|
178
|
-
|| pathname === '/api/taskforce/session/workspace';
|
|
179
|
-
}
|
|
180
|
-
export function resolveDisabledAccessError(access, pathname) {
|
|
181
|
-
if (pathname.startsWith('/api/taskforce/auth/'))
|
|
182
|
-
return null;
|
|
183
|
-
if (access.accountDisabled ?? access.disabled) {
|
|
184
|
-
return 'Forbidden: user account is disabled.';
|
|
185
|
-
}
|
|
186
|
-
if (access.workspaceDisabled && !isWorkspaceAccessRecoveryRoute(pathname)) {
|
|
187
|
-
return 'Forbidden: workspace access is not active for this user.';
|
|
188
|
-
}
|
|
189
|
-
return null;
|
|
190
|
-
}
|
|
191
181
|
function shouldLogRefreshDiagnostic(pathname) {
|
|
192
182
|
return REFRESH_DIAGNOSTIC_PATH_PREFIXES.some((prefix) => pathname === prefix || pathname.startsWith(prefix));
|
|
193
183
|
}
|
|
@@ -301,31 +291,6 @@ export function resolveCloudProxyUpstreamBaseUrl(req, fallbackBaseUrl) {
|
|
|
301
291
|
return fallbackBaseUrl;
|
|
302
292
|
return TASKFORCE_CLOUD_ENVIRONMENTS[requestedEnvironment].appBaseUrl;
|
|
303
293
|
}
|
|
304
|
-
export function resolveAuthorizedWorkspaceAccess(core, access, requestedWorkspaceId) {
|
|
305
|
-
if (!access.authenticated || !access.userId || access.userId === 'anonymous')
|
|
306
|
-
return access;
|
|
307
|
-
const trimmedRequestedWorkspaceId = String(requestedWorkspaceId || '').trim();
|
|
308
|
-
const candidateWorkspaceIds = [
|
|
309
|
-
trimmedRequestedWorkspaceId,
|
|
310
|
-
String(access.workspaceId || '').trim()
|
|
311
|
-
].filter((workspaceId, index, values) => Boolean(workspaceId) && values.indexOf(workspaceId) === index);
|
|
312
|
-
for (const workspaceId of candidateWorkspaceIds) {
|
|
313
|
-
const resolved = core.resolveUserAccess(access.userId, workspaceId, access.role);
|
|
314
|
-
if (!resolved)
|
|
315
|
-
continue;
|
|
316
|
-
return {
|
|
317
|
-
...access,
|
|
318
|
-
role: resolved.role,
|
|
319
|
-
workspaceId: resolved.workspaceId,
|
|
320
|
-
userId: resolved.userId,
|
|
321
|
-
systemRole: core.getSystemRole(resolved.userId),
|
|
322
|
-
disabled: resolved.disabled,
|
|
323
|
-
accountDisabled: resolved.accountDisabled ?? resolved.disabled,
|
|
324
|
-
workspaceDisabled: resolved.workspaceDisabled ?? false
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
|
-
return access;
|
|
328
|
-
}
|
|
329
294
|
export function shouldApplyReadOnlyMutationGuard(methodRaw, pathname) {
|
|
330
295
|
const method = String(methodRaw || 'GET').toUpperCase();
|
|
331
296
|
if (method !== 'POST' && method !== 'PATCH' && method !== 'DELETE')
|
|
@@ -673,6 +638,20 @@ export function createStandaloneServer(options) {
|
|
|
673
638
|
},
|
|
674
639
|
} : undefined,
|
|
675
640
|
});
|
|
641
|
+
const cloudCollectionReadWorkerCount = resolveCloudCollectionReadPoolSize(process.env);
|
|
642
|
+
const cloudCollectionReadPool = shouldUseCloudCollectionReadPool({
|
|
643
|
+
runtimeMode,
|
|
644
|
+
databaseProvider: db.provider,
|
|
645
|
+
connectionString: db.connectionString,
|
|
646
|
+
workerCount: cloudCollectionReadWorkerCount,
|
|
647
|
+
})
|
|
648
|
+
? new CloudCollectionReadPool({
|
|
649
|
+
projectRoot: options.projectRoot,
|
|
650
|
+
tenantId: core.getTenantId(),
|
|
651
|
+
connectionString: db.connectionString,
|
|
652
|
+
size: cloudCollectionReadWorkerCount,
|
|
653
|
+
})
|
|
654
|
+
: null;
|
|
676
655
|
const allowedOrigins = getAllowedOriginsFromEnv(process.env.TASKFORCE_ALLOWED_ORIGINS, runtimeMode);
|
|
677
656
|
const rateLimitEnabled = typeof process.env.TASKFORCE_API_RATE_LIMIT_ENABLED === 'string'
|
|
678
657
|
? process.env.TASKFORCE_API_RATE_LIMIT_ENABLED !== 'false'
|
|
@@ -1066,6 +1045,39 @@ export function createStandaloneServer(options) {
|
|
|
1066
1045
|
res.end(JSON.stringify({ error: auth.reason || 'Unauthorized' }));
|
|
1067
1046
|
return;
|
|
1068
1047
|
}
|
|
1048
|
+
const cloudCollectionReadOperation = cloudCollectionReadPool
|
|
1049
|
+
? resolveCloudCollectionReadOperation(req.method, url.pathname)
|
|
1050
|
+
: null;
|
|
1051
|
+
if (cloudCollectionReadPool && cloudCollectionReadOperation) {
|
|
1052
|
+
try {
|
|
1053
|
+
const requestedWorkspaceId = Array.isArray(req.headers['x-taskforce-workspace-id'])
|
|
1054
|
+
? String(req.headers['x-taskforce-workspace-id'][0] || '')
|
|
1055
|
+
: String(req.headers['x-taskforce-workspace-id'] || '');
|
|
1056
|
+
const response = await cloudCollectionReadPool.execute({
|
|
1057
|
+
operation: cloudCollectionReadOperation,
|
|
1058
|
+
pathname: url.pathname,
|
|
1059
|
+
requestedWorkspaceId,
|
|
1060
|
+
access: accessForRateLimit,
|
|
1061
|
+
performanceTrace: String(req.headers['x-taskforce-performance-trace'] || '').trim() === '1',
|
|
1062
|
+
});
|
|
1063
|
+
if (response.performance) {
|
|
1064
|
+
logSlowBootstrapRoute({
|
|
1065
|
+
route: url.pathname,
|
|
1066
|
+
workspaceId: response.workspaceId,
|
|
1067
|
+
traceId: String(req.headers['x-taskforce-bootstrap-trace-id'] || '').trim(),
|
|
1068
|
+
durationMs: response.performance.durationMs,
|
|
1069
|
+
details: response.performance.details,
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
response.headers['X-Taskforce-Cloud-Read-Execution'] = 'worker';
|
|
1073
|
+
res.writeHead(response.statusCode, response.headers);
|
|
1074
|
+
res.end(response.body);
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
catch (error) {
|
|
1078
|
+
console.error(`[Taskforce] Cloud collection read worker failed; using synchronous fallback path=${url.pathname}:`, error instanceof Error ? error.message : String(error));
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1069
1081
|
const pathname = url.pathname;
|
|
1070
1082
|
const isMutation = shouldApplyReadOnlyMutationGuard(req.method, pathname);
|
|
1071
1083
|
let access = accessForRateLimit;
|
|
@@ -1203,6 +1215,7 @@ export function createStandaloneServer(options) {
|
|
|
1203
1215
|
if (serverResult.status === 'rejected') {
|
|
1204
1216
|
throw serverResult.reason;
|
|
1205
1217
|
}
|
|
1218
|
+
await cloudCollectionReadPool?.close();
|
|
1206
1219
|
},
|
|
1207
1220
|
closeCore: () => core.close(),
|
|
1208
1221
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type LocalServiceCloudTarget } from '../config/localServiceCloudTarget.js';
|
|
1
2
|
import { type WindowsAclCommandRunner } from './localServiceWindowsAcl.js';
|
|
2
3
|
export interface LocalServiceDescriptor {
|
|
3
4
|
schemaVersion: 1;
|
|
@@ -9,6 +10,7 @@ export interface LocalServiceDescriptor {
|
|
|
9
10
|
generation: string;
|
|
10
11
|
bootstrapCapability: string;
|
|
11
12
|
publishedAt: string;
|
|
13
|
+
cloudTarget?: LocalServiceCloudTarget | null;
|
|
12
14
|
}
|
|
13
15
|
export interface LocalServiceLeaseIdentityInput {
|
|
14
16
|
projectRoot: string;
|
|
@@ -27,6 +29,7 @@ export interface LocalServiceDescriptorPublication {
|
|
|
27
29
|
endpoint: string;
|
|
28
30
|
protocolVersion: string;
|
|
29
31
|
appVersion: string;
|
|
32
|
+
cloudTarget: LocalServiceCloudTarget;
|
|
30
33
|
}
|
|
31
34
|
export interface LocalServiceLease {
|
|
32
35
|
readonly identity: LocalServiceLeaseIdentity;
|
|
@@ -3,6 +3,7 @@ import * as fs from 'fs';
|
|
|
3
3
|
import * as net from 'net';
|
|
4
4
|
import * as os from 'os';
|
|
5
5
|
import * as path from 'path';
|
|
6
|
+
import { parseLocalServiceCloudTarget, } from '../config/localServiceCloudTarget.js';
|
|
6
7
|
import { resolveLocalSyncProcessLockIdentity } from './localDatabaseIdentity.js';
|
|
7
8
|
import { ensureWindowsUserOnlyDirectoryAcl, } from './localServiceWindowsAcl.js';
|
|
8
9
|
const DESCRIPTOR_SCHEMA_VERSION = 1;
|
|
@@ -161,8 +162,14 @@ function parseDescriptor(value, ownershipKey) {
|
|
|
161
162
|
|| typeof value.publishedAt !== 'string') {
|
|
162
163
|
throw new Error('Local service descriptor is invalid.');
|
|
163
164
|
}
|
|
165
|
+
const cloudTarget = value.cloudTarget == null
|
|
166
|
+
? null
|
|
167
|
+
: parseLocalServiceCloudTarget(value.cloudTarget);
|
|
168
|
+
if (value.cloudTarget != null && !cloudTarget) {
|
|
169
|
+
throw new Error('Local service descriptor cloud target is invalid.');
|
|
170
|
+
}
|
|
164
171
|
assertLoopbackEndpoint(value.endpoint);
|
|
165
|
-
return value;
|
|
172
|
+
return { ...value, cloudTarget };
|
|
166
173
|
}
|
|
167
174
|
function readOptionalDescriptor(identity) {
|
|
168
175
|
try {
|
|
@@ -363,6 +370,7 @@ export async function acquireLocalServiceLease(input, dependencies = {}) {
|
|
|
363
370
|
generation,
|
|
364
371
|
bootstrapCapability,
|
|
365
372
|
publishedAt: (dependencies.now?.() || new Date()).toISOString(),
|
|
373
|
+
cloudTarget: publication.cloudTarget,
|
|
366
374
|
};
|
|
367
375
|
publishJsonFileAtomically(identity.descriptorPath, descriptor, platform);
|
|
368
376
|
return descriptor;
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* POST /api/taskforce/context-upload/init
|
|
9
9
|
* POST /api/taskforce/context-upload/finalize
|
|
10
10
|
* POST /api/taskforce/context-upload
|
|
11
|
+
* POST /api/taskforce/context-upload/draft/promote
|
|
12
|
+
* POST /api/taskforce/context-upload/draft/discard
|
|
11
13
|
* GET /api/taskforce/context/:filename
|
|
12
14
|
* GET /api/taskforce/documents/:assetId/content
|
|
13
15
|
* GET /api/taskforce/context-link
|
|
@@ -39,7 +41,7 @@ type AssetMetadata = DocumentRegistryEntry & {
|
|
|
39
41
|
originalFilename?: string | null;
|
|
40
42
|
};
|
|
41
43
|
interface CanonicalReadResult {
|
|
42
|
-
statusCode: 200 | 302 | 404;
|
|
44
|
+
statusCode: 200 | 302 | 404 | 409;
|
|
43
45
|
headers?: Record<string, string>;
|
|
44
46
|
body?: Buffer;
|
|
45
47
|
}
|
|
@@ -81,6 +83,7 @@ export interface DocumentRouteDependencies {
|
|
|
81
83
|
workspaceId?: string;
|
|
82
84
|
taskId?: string | null;
|
|
83
85
|
originalName?: string | null;
|
|
86
|
+
actorRef?: string | null;
|
|
84
87
|
}) => Promise<{
|
|
85
88
|
scan?: {
|
|
86
89
|
verdict?: string;
|
|
@@ -110,7 +113,7 @@ export interface DocumentRouteDependencies {
|
|
|
110
113
|
sanitizeDownloadFilename: (nameRaw: string, fallback?: string) => string;
|
|
111
114
|
contentTypeForExt: (ext: string) => string;
|
|
112
115
|
inferInlineDocumentFromPath: (pathValue: string | null | undefined) => boolean;
|
|
113
|
-
|
|
116
|
+
resolveCanonicalContentRead: (params: {
|
|
114
117
|
asset: WorkspaceAssetEntry;
|
|
115
118
|
forceDownload: boolean;
|
|
116
119
|
requestedFilename?: string | null;
|