@taskforcehq/taskforce 0.3.314 → 0.3.315
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/components/features/AnnotatedAttachmentWorkspace.js +51 -11
- package/dist/core/PlanEntitlementService.js +6 -1
- package/dist/core/Taskforce.d.ts +2 -0
- package/dist/core/Taskforce.js +16 -2
- package/dist/hooks/useTaskforce.js +13 -6
- package/dist/mcp/canonicalAssetHelpers.js +33 -19
- package/dist/mcp/runtime.js +21 -9
- package/dist/mcp/taskAttachmentHelpers.js +29 -7
- package/dist/server/index.js +1 -1
- package/dist/server/routes/documents.js +2 -1
- package/dist/server/routes.js +10 -6
- package/dist/storage/documentIntegrity.js +1 -6
- package/dist/storage/documentPurge.js +11 -12
- package/dist/sync/workspaceRepair.js +11 -9
- package/dist/ui/.well-known/mcp-registry-auth +1 -0
- package/dist/ui/assets/{AgentsModule-jZnjzh-I.js → AgentsModule-CNBWCIXk.js} +1 -1
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-9qxB0e6A.js +3 -0
- package/dist/ui/assets/{ContextAttachmentManager-OnjnW5fC.js → ContextAttachmentManager-CRyuFYlg.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-Btxo9quS.js → DocumentWorkspace-Dx7wb9NF.js} +1 -1
- package/dist/ui/assets/{EntityActivityTimeline-C8BjU7yO.js → EntityActivityTimeline-Cmal2OrJ.js} +1 -1
- package/dist/ui/assets/{InitiativesModule-DYBB7WD-.js → InitiativesModule-wzbYPQQL.js} +1 -1
- package/dist/ui/assets/{PlansPage-p0gvF4qR.js → PlansPage-BAnSfbTf.js} +1 -1
- package/dist/ui/assets/{TaskContextUpload-EGeOdrPm.js → TaskContextUpload-DENyMyUk.js} +1 -1
- package/dist/ui/assets/{TaskSettings-CJpF1CMF.js → TaskSettings-DASuVwpY.js} +1 -1
- package/dist/ui/assets/{WorkflowsModule-IQOmxPXX.js → WorkflowsModule-DJMEA_yt.js} +1 -1
- package/dist/ui/assets/documentReferences-BhNx80zO.js +1 -0
- package/dist/ui/assets/{index-DUt7ifSO.js → index-CWg2olz9.js} +5 -5
- package/dist/ui/index.html +1 -1
- package/dist/utils/pathContainment.d.ts +7 -0
- package/dist/utils/pathContainment.js +53 -0
- package/dist/utils/pathSafety.d.ts +6 -0
- package/dist/utils/pathSafety.js +73 -0
- package/package.json +3 -1
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-CGFrFF0m.js +0 -3
- package/dist/ui/assets/documentReferences-DXW5aT08.js +0 -1
|
@@ -118,6 +118,14 @@ function isEditableKeyboardTarget(target) {
|
|
|
118
118
|
return true;
|
|
119
119
|
return tagName === 'input' || tagName === 'textarea' || tagName === 'select';
|
|
120
120
|
}
|
|
121
|
+
function isInteractiveKeyboardTarget(target) {
|
|
122
|
+
if (!(target instanceof HTMLElement))
|
|
123
|
+
return false;
|
|
124
|
+
if (isEditableKeyboardTarget(target))
|
|
125
|
+
return true;
|
|
126
|
+
const tagName = target.tagName.toLowerCase();
|
|
127
|
+
return tagName === 'button' || tagName === 'a' || target.getAttribute('role') === 'button';
|
|
128
|
+
}
|
|
121
129
|
function normalizeOrder(annotations) {
|
|
122
130
|
return annotations.map((annotation, index) => ({
|
|
123
131
|
...annotation,
|
|
@@ -438,6 +446,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
|
|
|
438
446
|
const [imageLoadError, setImageLoadError] = useState(false);
|
|
439
447
|
const [zoomLevel, setZoomLevel] = useState(1);
|
|
440
448
|
const [panMode, setPanMode] = useState(false);
|
|
449
|
+
const [spacePanActive, setSpacePanActive] = useState(false);
|
|
441
450
|
const [pastingImage, setPastingImage] = useState(false);
|
|
442
451
|
const [naturalImageSize, setNaturalImageSize] = useState({ width: 0, height: 0 });
|
|
443
452
|
const overlayRef = useRef(null);
|
|
@@ -542,6 +551,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
|
|
|
542
551
|
|| canvasMediaSize.height > (scroller.clientHeight + 1)));
|
|
543
552
|
}, [canvasMediaSize.height, canvasMediaSize.width, zoomLevel]);
|
|
544
553
|
const zoomLabel = `${Math.round(zoomLevel * 100)}%`;
|
|
554
|
+
const isPanActive = canPan && (panMode || spacePanActive);
|
|
545
555
|
const annotationOrderNumbers = useMemo(() => {
|
|
546
556
|
const orderMap = new Map();
|
|
547
557
|
draftAnnotations.forEach((annotation, index) => {
|
|
@@ -1232,6 +1242,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
|
|
|
1232
1242
|
setNaturalImageSize({ width: 0, height: 0 });
|
|
1233
1243
|
setZoomLevel(1);
|
|
1234
1244
|
setPanMode(false);
|
|
1245
|
+
setSpacePanActive(false);
|
|
1235
1246
|
}, [activeTarget?.path]);
|
|
1236
1247
|
useEffect(() => {
|
|
1237
1248
|
if (!imageLoading)
|
|
@@ -1250,6 +1261,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
|
|
|
1250
1261
|
if (canPan)
|
|
1251
1262
|
return;
|
|
1252
1263
|
setPanMode(false);
|
|
1264
|
+
setSpacePanActive(false);
|
|
1253
1265
|
}, [canPan]);
|
|
1254
1266
|
useEffect(() => {
|
|
1255
1267
|
setPayloadPreview(null);
|
|
@@ -1657,6 +1669,34 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
|
|
|
1657
1669
|
window.addEventListener('keydown', handleKeyDown);
|
|
1658
1670
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
1659
1671
|
}, [deleteSelectedAnnotation, selectedAnnotationId]);
|
|
1672
|
+
useEffect(() => {
|
|
1673
|
+
const handleKeyDown = (event) => {
|
|
1674
|
+
if (event.code !== 'Space')
|
|
1675
|
+
return;
|
|
1676
|
+
if (!canPan)
|
|
1677
|
+
return;
|
|
1678
|
+
if (isInteractiveKeyboardTarget(event.target))
|
|
1679
|
+
return;
|
|
1680
|
+
event.preventDefault();
|
|
1681
|
+
setSpacePanActive(true);
|
|
1682
|
+
};
|
|
1683
|
+
const handleKeyUp = (event) => {
|
|
1684
|
+
if (event.code !== 'Space')
|
|
1685
|
+
return;
|
|
1686
|
+
setSpacePanActive(false);
|
|
1687
|
+
};
|
|
1688
|
+
const handleWindowBlur = () => {
|
|
1689
|
+
setSpacePanActive(false);
|
|
1690
|
+
};
|
|
1691
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
1692
|
+
window.addEventListener('keyup', handleKeyUp);
|
|
1693
|
+
window.addEventListener('blur', handleWindowBlur);
|
|
1694
|
+
return () => {
|
|
1695
|
+
window.removeEventListener('keydown', handleKeyDown);
|
|
1696
|
+
window.removeEventListener('keyup', handleKeyUp);
|
|
1697
|
+
window.removeEventListener('blur', handleWindowBlur);
|
|
1698
|
+
};
|
|
1699
|
+
}, [canPan]);
|
|
1660
1700
|
const readPoint = useCallback((event) => {
|
|
1661
1701
|
const rect = overlayRef.current?.getBoundingClientRect();
|
|
1662
1702
|
if (!rect || rect.width <= 0 || rect.height <= 0)
|
|
@@ -1667,7 +1707,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
|
|
|
1667
1707
|
};
|
|
1668
1708
|
}, []);
|
|
1669
1709
|
const handleCanvasPointerDown = useCallback((event) => {
|
|
1670
|
-
if (
|
|
1710
|
+
if (isPanActive) {
|
|
1671
1711
|
const scroller = canvasScrollerRef.current;
|
|
1672
1712
|
if (!scroller)
|
|
1673
1713
|
return;
|
|
@@ -1709,7 +1749,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
|
|
|
1709
1749
|
setDraftAnnotations((current) => normalizeOrder([...current, annotation]));
|
|
1710
1750
|
setSelectedAnnotationId(annotation.id);
|
|
1711
1751
|
markDraftChanged(300);
|
|
1712
|
-
}, [
|
|
1752
|
+
}, [defaultMarkerColor, isPanActive, markDraftChanged, readPoint, selectedSessionId, toolMode]);
|
|
1713
1753
|
const handleCanvasPointerUp = useCallback((event) => {
|
|
1714
1754
|
if (panRef.current?.pointerId === event.pointerId) {
|
|
1715
1755
|
event.currentTarget.releasePointerCapture?.(event.pointerId);
|
|
@@ -1726,7 +1766,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
|
|
|
1726
1766
|
setToolMode('select');
|
|
1727
1767
|
}, [readPoint, toolMode]);
|
|
1728
1768
|
const startDragAnnotation = useCallback((event, annotation) => {
|
|
1729
|
-
if (toolMode !== 'select' ||
|
|
1769
|
+
if (toolMode !== 'select' || isPanActive)
|
|
1730
1770
|
return;
|
|
1731
1771
|
const point = readPoint(event);
|
|
1732
1772
|
if (!point)
|
|
@@ -1738,7 +1778,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
|
|
|
1738
1778
|
};
|
|
1739
1779
|
setSelectedAnnotationId(annotation.id);
|
|
1740
1780
|
event.stopPropagation();
|
|
1741
|
-
}, [
|
|
1781
|
+
}, [isPanActive, readPoint, toolMode]);
|
|
1742
1782
|
const handleCanvasPointerMove = useCallback((event) => {
|
|
1743
1783
|
if (panRef.current?.pointerId === event.pointerId) {
|
|
1744
1784
|
const scroller = canvasScrollerRef.current;
|
|
@@ -1872,7 +1912,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
|
|
|
1872
1912
|
panRef.current = null;
|
|
1873
1913
|
}, []);
|
|
1874
1914
|
const startResizeBox = useCallback((event, annotation) => {
|
|
1875
|
-
if (toolMode !== 'select' ||
|
|
1915
|
+
if (toolMode !== 'select' || isPanActive)
|
|
1876
1916
|
return;
|
|
1877
1917
|
const point = readPoint(event);
|
|
1878
1918
|
if (!point)
|
|
@@ -1884,9 +1924,9 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
|
|
|
1884
1924
|
};
|
|
1885
1925
|
setSelectedAnnotationId(annotation.id);
|
|
1886
1926
|
event.stopPropagation();
|
|
1887
|
-
}, [
|
|
1927
|
+
}, [isPanActive, readPoint, toolMode]);
|
|
1888
1928
|
const startArrowHandleDrag = useCallback((event, annotation, endpoint) => {
|
|
1889
|
-
if (toolMode !== 'select' ||
|
|
1929
|
+
if (toolMode !== 'select' || isPanActive)
|
|
1890
1930
|
return;
|
|
1891
1931
|
const point = readPoint(event);
|
|
1892
1932
|
if (!point)
|
|
@@ -1899,7 +1939,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
|
|
|
1899
1939
|
};
|
|
1900
1940
|
setSelectedAnnotationId(annotation.id);
|
|
1901
1941
|
event.stopPropagation();
|
|
1902
|
-
}, [
|
|
1942
|
+
}, [isPanActive, readPoint, toolMode]);
|
|
1903
1943
|
const activeSessionTimestamp = selectedSession?.updatedAt ? formatTimestamp(selectedSession.updatedAt) : 'Not saved yet';
|
|
1904
1944
|
return (_jsxs("div", { className: `${styles.shell} ${showImageTray ? styles.shellWithImageTray : ''} ${showImageTray && imageTrayOpen ? styles.shellImageTrayOpen : ''}`.trim(), children: [showImageTray ? (_jsxs("aside", { className: `${styles.imageTrayPanel} ${imageTrayOpen ? styles.imageTrayPanelOpen : ''}`.trim(), "aria-label": "Image tray", "aria-hidden": !imageTrayOpen, children: [_jsxs("div", { className: styles.imageTrayHeader, children: [_jsxs("span", { className: styles.imageTrayTitle, children: [_jsx(FolderOpen, { size: 14 }), "Images"] }), _jsx("button", { type: "button", className: "tf-control-icon", onClick: onCloseImageTray, title: "Collapse image tray", "aria-label": "Collapse image tray", children: _jsx(ChevronLeft, { size: 16 }) })] }), _jsxs("div", { className: styles.imageTraySearch, children: [_jsx(Search, { size: 12, className: styles.imageTraySearchIcon }), _jsx("input", { type: "text", placeholder: "Search images...", value: imageTraySearchQuery, onChange: (event) => setImageTraySearchQuery(event.target.value), className: styles.imageTraySearchInput })] }), _jsx("div", { className: `tf-scrollbar ${styles.imageTrayList}`, children: imageTrayLoading ? (_jsx("div", { className: styles.imageTrayState, children: "Loading images..." })) : imageTrayError ? (_jsx("div", { className: styles.imageTrayStateError, children: imageTrayError })) : filteredImageTrayItems.length === 0 ? (_jsx("div", { className: styles.imageTrayState, children: imageTraySearchQuery.trim() ? 'No images match your search.' : 'No images found.' })) : filteredImageTrayItems.map((item) => {
|
|
1905
1945
|
const isSelected = activeTarget?.assetId === item.assetId;
|
|
@@ -1957,7 +1997,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
|
|
|
1957
1997
|
if (!selectedSession)
|
|
1958
1998
|
return;
|
|
1959
1999
|
restoreLastSavedDraft();
|
|
1960
|
-
}, disabled: !selectedSession || !hasUnsavedChanges, "aria-label": "Reset unsaved changes", title: "Reset unsaved changes", children: _jsx(RotateCcw, { size: 18 }) }), _jsx("button", { type: "button", className: `tf-button-ghost ${styles.toolRailButton} ${styles.iconButton}`, onClick: zoomOut, disabled: !activeTarget || imageLoading || zoomLevel <= 0.25, "aria-label": "Zoom out", children: _jsx(ZoomOut, { size: 18 }) }), _jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton}`, onClick: resetZoom, disabled: !activeTarget || imageLoading || zoomLevel === 1, "aria-label": `Reset zoom to 100 percent (currently ${zoomLabel})`, title: `Reset zoom to 100% (currently ${zoomLabel})`, children: _jsx("span", { children: zoomLabel }) }), _jsx("button", { type: "button", className: `tf-button-ghost ${styles.toolRailButton} ${styles.iconButton}`, onClick: zoomIn, disabled: !activeTarget || imageLoading || zoomLevel >= 4, "aria-label": "Zoom in", children: _jsx(ZoomIn, { size: 18 }) }), _jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.toolBtn} ${styles.toolbarButton} ${
|
|
2000
|
+
}, disabled: !selectedSession || !hasUnsavedChanges, "aria-label": "Reset unsaved changes", title: "Reset unsaved changes", children: _jsx(RotateCcw, { size: 18 }) }), _jsx("button", { type: "button", className: `tf-button-ghost ${styles.toolRailButton} ${styles.iconButton}`, onClick: zoomOut, disabled: !activeTarget || imageLoading || zoomLevel <= 0.25, "aria-label": "Zoom out", children: _jsx(ZoomOut, { size: 18 }) }), _jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton}`, onClick: resetZoom, disabled: !activeTarget || imageLoading || zoomLevel === 1, "aria-label": `Reset zoom to 100 percent (currently ${zoomLabel})`, title: `Reset zoom to 100% (currently ${zoomLabel})`, children: _jsx("span", { children: zoomLabel }) }), _jsx("button", { type: "button", className: `tf-button-ghost ${styles.toolRailButton} ${styles.iconButton}`, onClick: zoomIn, disabled: !activeTarget || imageLoading || zoomLevel >= 4, "aria-label": "Zoom in", children: _jsx(ZoomIn, { size: 18 }) }), _jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.toolBtn} ${styles.toolbarButton} ${isPanActive ? styles.toolBtnActive : ''}`, onClick: () => setPanMode((current) => !current), disabled: !activeTarget || imageLoading || !canPan, "aria-label": "Pan canvas", title: "Pan canvas", children: _jsx(Hand, { size: 18 }) })] }), _jsx("span", { className: styles.toolbarSeparator, "aria-hidden": "true" }), _jsxs("div", { className: styles.toolbarActions, children: [_jsx("div", { className: styles.toolbarSelectionActions, children: selectedAnnotation ? (_jsxs(_Fragment, { children: [_jsxs("div", { ref: colorPickerRef, className: styles.toolbarColorPicker, children: [_jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton} ${styles.colorPickerButton}`, onClick: () => setColorPickerOpen((current) => !current), "aria-label": "Marker color", "aria-expanded": colorPickerOpen, title: "Marker color", children: _jsx("span", { className: styles.colorPickerSwatch, style: { backgroundColor: selectedMarkerColor }, "aria-hidden": "true" }) }), colorPickerOpen ? (_jsx("div", { className: styles.colorPickerPopover, role: "menu", "aria-label": "Marker color options", children: MARKER_COLOR_SWATCHES.map((color) => (_jsx("button", { type: "button", className: `${styles.colorOption} ${selectedMarkerColor === color ? styles.colorOptionActive : ''}`, style: { backgroundColor: color }, onClick: () => applyMarkerColor(color), "aria-label": `Use marker color ${color}`, "aria-pressed": selectedMarkerColor === color }, color))) })) : null] }), _jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton}`, onClick: deleteSelectedAnnotation, "aria-label": "Delete marker", title: "Delete selected marker", children: _jsx(Trash2, { size: 18 }) }), _jsx("div", { className: styles.toolbarGeometryFields, "aria-label": "Marker geometry percent controls", children: getGeometryFields(selectedAnnotation).map((field) => (_jsxs("label", { className: styles.toolbarGeometryField, children: [_jsx("span", { className: `tf-text-meta ${styles.toolbarGeometryLabel}`, children: field.label }), _jsx("input", { className: `tf-field-shell ${styles.toolbarGeometryInput}`, type: "number", min: 0, max: 100, step: 0.1, value: field.value, onChange: (event) => {
|
|
1961
2001
|
const nextValue = parsePercentField(event.target.value);
|
|
1962
2002
|
if (nextValue === null)
|
|
1963
2003
|
return;
|
|
@@ -1980,7 +2020,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
|
|
|
1980
2020
|
setNaturalImageSize({ width: 0, height: 0 });
|
|
1981
2021
|
setImageLoading(false);
|
|
1982
2022
|
setImageLoadError(true);
|
|
1983
|
-
} }), !imageLoading && !imageLoadError && selectedSession ? (_jsxs("div", { ref: overlayRef, className: `${styles.overlay} ${toolMode === 'select' ? styles.overlaySelect : ''} ${
|
|
2023
|
+
} }), !imageLoading && !imageLoadError && selectedSession ? (_jsxs("div", { ref: overlayRef, className: `${styles.overlay} ${toolMode === 'select' ? styles.overlaySelect : ''} ${isPanActive ? styles.overlayPan : ''}`, onPointerDown: handleCanvasPointerDown, onPointerMove: handleCanvasPointerMove, onPointerUp: (event) => {
|
|
1984
2024
|
handleCanvasPointerUp(event);
|
|
1985
2025
|
handleCanvasPointerEnd();
|
|
1986
2026
|
}, "data-testid": "annotated-attachment-overlay", onPointerLeave: handleCanvasPointerLeave, onPointerCancel: handleCanvasPointerCancel, children: [_jsx("svg", { className: styles.overlaySvg, viewBox: arrowOverlayViewBox, preserveAspectRatio: "none", "aria-hidden": "true", children: draftAnnotations.filter((annotation) => annotation.kind === 'arrow').map((annotation) => {
|
|
@@ -2022,7 +2062,7 @@ export function AnnotatedAttachmentWorkspaceShell({ runtimeMode = 'local', apiBa
|
|
|
2022
2062
|
? 'Save failed. Retry now.'
|
|
2023
2063
|
: saveState === 'saving' || saveState === 'pending'
|
|
2024
2064
|
? 'Saving…'
|
|
2025
|
-
: 'Saved' }), error ? _jsx("span", { children: error }) : _jsx("span", { children: !selectedSession ? 'Create a session to begin placing markers on this image.' :
|
|
2065
|
+
: 'Saved' }), error ? _jsx("span", { children: error }) : _jsx("span", { children: !selectedSession ? 'Create a session to begin placing markers on this image.' : isPanActive ? 'Drag on the image to pan.' : toolMode === 'select' ? 'Select a marker to edit it.' : `Click on the image to place a ${toolMode}.` }), activeTarget && !selectedSession && showCreateSessionPrompt ? (_jsx("button", { type: "button", className: `tf-button-ghost tf-button-compact ${styles.ghostBtn} ${styles.toolbarButton}`, onClick: () => void createSession(), disabled: saving || loading, children: saving ? 'Creating…' : 'Create one now' })) : null] })] }), _jsx("section", { className: `tf-surface-panel ${styles.panel} ${styles.detailPanel}`, children: selectedSession ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: styles.panelHeader, children: [_jsxs("div", { className: styles.panelHeaderText, children: [_jsx("div", { className: `tf-heading-card ${styles.panelTitle}`, children: "Markers" }), _jsxs("div", { className: "tf-text-secondary", children: [draftAnnotations.length, " in this session"] })] }), _jsxs("div", { className: styles.sessionActions, children: [_jsx("button", { type: "button", className: `tf-control-icon ${styles.iconButton}`, onClick: () => moveSelectedAnnotation('up'), disabled: !selectedAnnotation || selectedAnnotationIndex <= 0, "aria-label": "Move marker up", children: _jsx(ArrowUp, { size: 16 }) }), _jsx("button", { type: "button", className: `tf-control-icon ${styles.iconButton}`, onClick: () => moveSelectedAnnotation('down'), disabled: !selectedAnnotation || selectedAnnotationIndex === -1 || selectedAnnotationIndex >= draftAnnotations.length - 1, "aria-label": "Move marker down", children: _jsx(ArrowDown, { size: 16 }) })] })] }), _jsx("div", { className: `tf-scrollbar ${styles.annotationList}`, children: draftAnnotations.length === 0 ? (_jsx("div", { className: styles.detailEmpty, children: _jsx("p", { className: "tf-empty-copy", children: "Add a marker on the image to begin." }) })) : draftAnnotations.map((annotation, index) => {
|
|
2026
2066
|
const AnnotationKindIcon = ANNOTATION_KIND_ICON[annotation.kind];
|
|
2027
2067
|
const annotationKindLabel = ANNOTATION_KIND_LABELS[annotation.kind];
|
|
2028
2068
|
const annotationSequenceLabel = `${annotationKindLabel} ${index + 1}`;
|
|
@@ -258,12 +258,17 @@ export class PlanEntitlementService {
|
|
|
258
258
|
};
|
|
259
259
|
}
|
|
260
260
|
if (normalizedFeatureKey === AI_PROFILES_FEATURE_KEY) {
|
|
261
|
+
const limit = this.readPositiveIntegerConfig(config[AI_PROFILES_LIMIT_CONFIG_KEY], 1);
|
|
261
262
|
if (access === 'limited') {
|
|
262
|
-
const limit = this.readPositiveIntegerConfig(config[AI_PROFILES_LIMIT_CONFIG_KEY], 1);
|
|
263
263
|
return {
|
|
264
264
|
[AI_PROFILES_LIMIT_CONFIG_KEY]: limit ?? 1
|
|
265
265
|
};
|
|
266
266
|
}
|
|
267
|
+
if (limit !== null) {
|
|
268
|
+
return {
|
|
269
|
+
[AI_PROFILES_LIMIT_CONFIG_KEY]: limit
|
|
270
|
+
};
|
|
271
|
+
}
|
|
267
272
|
return {};
|
|
268
273
|
}
|
|
269
274
|
return config && typeof config === 'object' ? { ...config } : {};
|
package/dist/core/Taskforce.d.ts
CHANGED
|
@@ -911,6 +911,8 @@ export declare class TaskforceCore implements TaskforceSyncCapable {
|
|
|
911
911
|
private tenantId;
|
|
912
912
|
private db;
|
|
913
913
|
private globalSettingsDb;
|
|
914
|
+
private readonly globalSettingsSchemaEnsuredStores;
|
|
915
|
+
private readonly structuredSettingsSchemaEnsuredStores;
|
|
914
916
|
private authTokenService;
|
|
915
917
|
private identityService;
|
|
916
918
|
private mcpTokenService;
|
package/dist/core/Taskforce.js
CHANGED
|
@@ -40,6 +40,7 @@ import { formatInitiativeReference, formatWorkstreamReference, } from '../utils/
|
|
|
40
40
|
import { getDocumentReferenceLabel } from '../utils/documentReferences.js';
|
|
41
41
|
import { getImageReferenceLabel } from '../utils/imageReferences.js';
|
|
42
42
|
import { getCanonicalDocumentName } from '../utils/documentNames.js';
|
|
43
|
+
import { isPathInsideRoot, resolveStorageKeyPathInsideRoot } from '../utils/pathSafety.js';
|
|
43
44
|
import { DEFAULT_TASKFORCE_THEME } from '../utils/theme.js';
|
|
44
45
|
import { ensureAssigneeTaskSchema, ensureAnnotatedAttachmentSessionsSchema, ensureAuthIdentitySchema, ensureBillingSchema, ensureDocumentReviewSessionsSchema, ensureDocumentReferenceSchema, ensureEntitlementsSchema, ensureScheduleTaskSchema, ensureTaskAssetsSchema, ensureTaskReferenceSchema, ensureTenantSchema, ensureWorkspaceAssetsSchema, ensureWorkspaceSchema } from '../migrations/taskSchemaMigrations.js';
|
|
45
46
|
export class TaskforceRuleError extends Error {
|
|
@@ -142,6 +143,8 @@ export class TaskforceCore {
|
|
|
142
143
|
tenantId;
|
|
143
144
|
db;
|
|
144
145
|
globalSettingsDb = null;
|
|
146
|
+
globalSettingsSchemaEnsuredStores = new WeakSet();
|
|
147
|
+
structuredSettingsSchemaEnsuredStores = new WeakSet();
|
|
145
148
|
authTokenService;
|
|
146
149
|
identityService;
|
|
147
150
|
mcpTokenService;
|
|
@@ -2961,6 +2964,8 @@ export class TaskforceCore {
|
|
|
2961
2964
|
return this.globalSettingsDb || this.db;
|
|
2962
2965
|
}
|
|
2963
2966
|
ensureGlobalSettingsTable(store) {
|
|
2967
|
+
if (this.globalSettingsSchemaEnsuredStores.has(store))
|
|
2968
|
+
return;
|
|
2964
2969
|
store.exec(`
|
|
2965
2970
|
CREATE TABLE IF NOT EXISTS global_settings (
|
|
2966
2971
|
tenant_id TEXT PRIMARY KEY,
|
|
@@ -2968,8 +2973,11 @@ export class TaskforceCore {
|
|
|
2968
2973
|
updated_at TEXT NOT NULL
|
|
2969
2974
|
);
|
|
2970
2975
|
`);
|
|
2976
|
+
this.globalSettingsSchemaEnsuredStores.add(store);
|
|
2971
2977
|
}
|
|
2972
2978
|
ensureStructuredSettingsTables(store) {
|
|
2979
|
+
if (this.structuredSettingsSchemaEnsuredStores.has(store))
|
|
2980
|
+
return;
|
|
2973
2981
|
store.exec(`
|
|
2974
2982
|
CREATE TABLE IF NOT EXISTS system_settings (
|
|
2975
2983
|
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
@@ -3002,6 +3010,7 @@ export class TaskforceCore {
|
|
|
3002
3010
|
CREATE INDEX IF NOT EXISTS idx_user_preferences_user_key
|
|
3003
3011
|
ON user_preferences(user_id, prefs_key);
|
|
3004
3012
|
`);
|
|
3013
|
+
this.structuredSettingsSchemaEnsuredStores.add(store);
|
|
3005
3014
|
}
|
|
3006
3015
|
readSettingsPayloadRow(store, tableName, whereValues) {
|
|
3007
3016
|
try {
|
|
@@ -3260,8 +3269,13 @@ export class TaskforceCore {
|
|
|
3260
3269
|
};
|
|
3261
3270
|
}
|
|
3262
3271
|
if (storageProvider !== 'r2') {
|
|
3263
|
-
const candidatePath =
|
|
3264
|
-
if (!fs.existsSync(candidatePath) || !fs.statSync(candidatePath).isFile()) {
|
|
3272
|
+
const candidatePath = resolveStorageKeyPathInsideRoot(this.basePath, storageKey);
|
|
3273
|
+
if (!candidatePath || !fs.existsSync(candidatePath) || !fs.statSync(candidatePath).isFile()) {
|
|
3274
|
+
return null;
|
|
3275
|
+
}
|
|
3276
|
+
const storageRoot = fs.existsSync(this.basePath) ? fs.realpathSync(this.basePath) : path.resolve(this.basePath);
|
|
3277
|
+
const realCandidatePath = fs.realpathSync(candidatePath);
|
|
3278
|
+
if (!isPathInsideRoot(realCandidatePath, storageRoot)) {
|
|
3265
3279
|
return null;
|
|
3266
3280
|
}
|
|
3267
3281
|
}
|
|
@@ -2708,14 +2708,21 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
|
|
|
2708
2708
|
onTaskCountChange(tasks.length);
|
|
2709
2709
|
}
|
|
2710
2710
|
}, [tasks.length, onTaskCountChange]);
|
|
2711
|
+
const handledInitialTaskIdRef = useRef(null);
|
|
2711
2712
|
// Deep linking handler
|
|
2712
2713
|
useEffect(() => {
|
|
2713
|
-
if (initialTaskId
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2714
|
+
if (!initialTaskId) {
|
|
2715
|
+
handledInitialTaskIdRef.current = null;
|
|
2716
|
+
return;
|
|
2717
|
+
}
|
|
2718
|
+
if (handledInitialTaskIdRef.current === initialTaskId || tasks.length === 0) {
|
|
2719
|
+
return;
|
|
2720
|
+
}
|
|
2721
|
+
const task = tasks.find(t => t.id.endsWith(initialTaskId) || t.id === initialTaskId);
|
|
2722
|
+
if (task) {
|
|
2723
|
+
handledInitialTaskIdRef.current = initialTaskId;
|
|
2724
|
+
handleEdit(task);
|
|
2725
|
+
setActiveTab('add');
|
|
2719
2726
|
}
|
|
2720
2727
|
}, [initialTaskId, tasks]);
|
|
2721
2728
|
// Update active category logic
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
+
import { normalizeRelativeStorageKey, resolveExistingStorageKeyFilePathInsideRoot, resolveStorageKeyWritePathInsideRoot } from '../utils/pathSafety.js';
|
|
4
5
|
export function createCanonicalAssetHelpers(deps) {
|
|
5
6
|
const normalizeAttachmentStorageKey = (pathValue, fsPathValue) => {
|
|
6
7
|
const fsPathRaw = String(fsPathValue || '').trim().replace(/\\/g, '/');
|
|
7
8
|
if (fsPathRaw) {
|
|
8
|
-
return fsPathRaw.replace(/^\.taskforce\//, '').trim()
|
|
9
|
+
return normalizeRelativeStorageKey(fsPathRaw.replace(/^\.taskforce\//, '').trim());
|
|
9
10
|
}
|
|
10
11
|
const pathRaw = String(pathValue || '').trim();
|
|
11
12
|
if (!pathRaw)
|
|
@@ -13,10 +14,18 @@ export function createCanonicalAssetHelpers(deps) {
|
|
|
13
14
|
if (/^https?:\/\//i.test(pathRaw))
|
|
14
15
|
return pathRaw;
|
|
15
16
|
if (pathRaw.startsWith('/api/taskforce/context/')) {
|
|
16
|
-
return decodeURIComponent(pathRaw.slice('/api/taskforce/context/'.length)).trim()
|
|
17
|
+
return normalizeRelativeStorageKey(decodeURIComponent(pathRaw.slice('/api/taskforce/context/'.length)).trim());
|
|
17
18
|
}
|
|
18
19
|
return null;
|
|
19
20
|
};
|
|
21
|
+
const isSafeCanonicalAssetStorage = (asset) => {
|
|
22
|
+
if (asset.storageProvider === 'external')
|
|
23
|
+
return true;
|
|
24
|
+
return Boolean(normalizeRelativeStorageKey(asset.storageKey));
|
|
25
|
+
};
|
|
26
|
+
const resolveCanonicalLocalFilePath = (storageKey) => {
|
|
27
|
+
return resolveExistingStorageKeyFilePathInsideRoot(deps.basePath, storageKey);
|
|
28
|
+
};
|
|
20
29
|
const buildSyntheticAttachmentId = (taskId, attachment) => {
|
|
21
30
|
const stableSource = [
|
|
22
31
|
taskId,
|
|
@@ -32,14 +41,14 @@ export function createCanonicalAssetHelpers(deps) {
|
|
|
32
41
|
const assetId = String(attachment.assetId || '').trim();
|
|
33
42
|
if (assetId) {
|
|
34
43
|
const byId = deps.workspaceAssetStore.get(assetId, deps.activeWorkspaceId);
|
|
35
|
-
if (byId && !byId.deletedAt)
|
|
44
|
+
if (byId && !byId.deletedAt && isSafeCanonicalAssetStorage(byId))
|
|
36
45
|
return byId;
|
|
37
46
|
}
|
|
38
47
|
const storageKey = normalizeAttachmentStorageKey(attachment.path, attachment.fsPath);
|
|
39
48
|
if (!storageKey || /^https?:\/\//i.test(storageKey))
|
|
40
49
|
return null;
|
|
41
50
|
const byStorageKey = deps.workspaceAssetStore.getByStorageKey(storageKey, deps.activeWorkspaceId);
|
|
42
|
-
if (byStorageKey && !byStorageKey.deletedAt)
|
|
51
|
+
if (byStorageKey && !byStorageKey.deletedAt && isSafeCanonicalAssetStorage(byStorageKey))
|
|
43
52
|
return byStorageKey;
|
|
44
53
|
return null;
|
|
45
54
|
};
|
|
@@ -80,6 +89,9 @@ export function createCanonicalAssetHelpers(deps) {
|
|
|
80
89
|
throw new Error('Attachment scan pending');
|
|
81
90
|
}
|
|
82
91
|
if (asset.storageProvider === 'r2') {
|
|
92
|
+
if (!normalizeRelativeStorageKey(asset.storageKey)) {
|
|
93
|
+
throw new Error(`Attachment ${attachment.attachmentId} not found on ${targetLabel} ${taskId}`);
|
|
94
|
+
}
|
|
83
95
|
const attachmentContext = describeAttachmentStorageContext(attachment, asset.storageKey);
|
|
84
96
|
if (!deps.objectStorageClient) {
|
|
85
97
|
throw new Error(`Attachment read failed from R2 because object storage is not configured in this MCP runtime (${attachmentContext})`);
|
|
@@ -97,11 +109,8 @@ export function createCanonicalAssetHelpers(deps) {
|
|
|
97
109
|
}
|
|
98
110
|
return object.body;
|
|
99
111
|
}
|
|
100
|
-
const resolvedPath =
|
|
101
|
-
if (!resolvedPath
|
|
102
|
-
throw new Error(`Attachment ${attachment.attachmentId} not found on ${targetLabel} ${taskId}`);
|
|
103
|
-
}
|
|
104
|
-
if (!fs.existsSync(resolvedPath) || !fs.statSync(resolvedPath).isFile()) {
|
|
112
|
+
const resolvedPath = resolveCanonicalLocalFilePath(asset.storageKey);
|
|
113
|
+
if (!resolvedPath) {
|
|
105
114
|
throw new Error(`Attachment ${attachment.attachmentId} not found on ${targetLabel} ${taskId}`);
|
|
106
115
|
}
|
|
107
116
|
return fs.readFileSync(resolvedPath);
|
|
@@ -117,6 +126,9 @@ export function createCanonicalAssetHelpers(deps) {
|
|
|
117
126
|
throw new Error('Document scan pending');
|
|
118
127
|
}
|
|
119
128
|
if (asset.storageProvider === 'r2') {
|
|
129
|
+
if (!normalizeRelativeStorageKey(asset.storageKey)) {
|
|
130
|
+
throw new Error(`Document ${asset.assetId} not found in workspace ${deps.activeWorkspaceId}.`);
|
|
131
|
+
}
|
|
120
132
|
if (!deps.objectStorageClient) {
|
|
121
133
|
throw new Error(`Document read failed from R2 because object storage is not configured in this MCP runtime (assetId=${asset.assetId}, storageKey=${asset.storageKey || 'unknown'})`);
|
|
122
134
|
}
|
|
@@ -133,11 +145,8 @@ export function createCanonicalAssetHelpers(deps) {
|
|
|
133
145
|
}
|
|
134
146
|
return object.body;
|
|
135
147
|
}
|
|
136
|
-
const resolvedPath =
|
|
137
|
-
if (!resolvedPath
|
|
138
|
-
throw new Error(`Document ${asset.assetId} not found in workspace ${deps.activeWorkspaceId}.`);
|
|
139
|
-
}
|
|
140
|
-
if (!fs.existsSync(resolvedPath) || !fs.statSync(resolvedPath).isFile()) {
|
|
148
|
+
const resolvedPath = resolveCanonicalLocalFilePath(asset.storageKey);
|
|
149
|
+
if (!resolvedPath) {
|
|
141
150
|
throw new Error(`Document ${asset.assetId} not found in workspace ${deps.activeWorkspaceId}.`);
|
|
142
151
|
}
|
|
143
152
|
return fs.readFileSync(resolvedPath);
|
|
@@ -176,14 +185,17 @@ export function createCanonicalAssetHelpers(deps) {
|
|
|
176
185
|
const logicalFilename = resolveAttachmentLogicalFilename(attachment, asset);
|
|
177
186
|
if (logicalFilename !== requestedFilename)
|
|
178
187
|
continue;
|
|
179
|
-
const
|
|
188
|
+
const absolutePath = resolveStorageKeyWritePathInsideRoot(deps.basePath, asset.storageKey);
|
|
189
|
+
if (!absolutePath)
|
|
190
|
+
continue;
|
|
191
|
+
const relativePath = deps.toPosixPath(path.relative(deps.projectRoot, absolutePath));
|
|
180
192
|
return {
|
|
181
193
|
index,
|
|
182
194
|
attachment,
|
|
183
195
|
asset,
|
|
184
196
|
descriptor,
|
|
185
197
|
relativePath,
|
|
186
|
-
absolutePath
|
|
198
|
+
absolutePath,
|
|
187
199
|
logicalFilename,
|
|
188
200
|
};
|
|
189
201
|
}
|
|
@@ -197,10 +209,12 @@ export function createCanonicalAssetHelpers(deps) {
|
|
|
197
209
|
const targetId = String(targetIdRaw || '').trim();
|
|
198
210
|
if (!relativePath || !targetId)
|
|
199
211
|
return null;
|
|
200
|
-
const
|
|
201
|
-
if (!
|
|
212
|
+
const storageKey = normalizeRelativeStorageKey(relativePath.replace(/^\.taskforce\//, '').trim() || relativePath);
|
|
213
|
+
if (!storageKey)
|
|
214
|
+
return null;
|
|
215
|
+
const absolutePath = resolveExistingStorageKeyFilePathInsideRoot(deps.basePath, storageKey);
|
|
216
|
+
if (!absolutePath)
|
|
202
217
|
return null;
|
|
203
|
-
const storageKey = relativePath.replace(/^\.taskforce\//, '').trim() || relativePath;
|
|
204
218
|
const buffer = fs.readFileSync(absolutePath);
|
|
205
219
|
const stats = fs.statSync(absolutePath);
|
|
206
220
|
const mimeType = deps.inferMimeTypeFromPath(relativePath);
|
package/dist/mcp/runtime.js
CHANGED
|
@@ -11,6 +11,7 @@ import { buildResolveAiProfileInputFromBootstrap } from './aiProfileBootstrap.js
|
|
|
11
11
|
import { createCanonicalAssetHelpers } from './canonicalAssetHelpers.js';
|
|
12
12
|
import { createDocumentHelpers } from './documentHelpers.js';
|
|
13
13
|
import { createTaskAttachmentHelpers } from './taskAttachmentHelpers.js';
|
|
14
|
+
import { isPathWithinRoot } from '../utils/pathContainment.js';
|
|
14
15
|
import { validateStructuredDocForAttach } from './structuredDocValidation.js';
|
|
15
16
|
import { validateStructuredCommentForAdd } from './structuredCommentValidation.js';
|
|
16
17
|
import { resolveDatabaseConfigFromEnv } from '../storage/providerConfig.js';
|
|
@@ -23,6 +24,7 @@ import { getDocumentReferenceLabel, parseDocumentReference } from '../utils/docu
|
|
|
23
24
|
import { getImageReferenceLabel, parseImageReference } from '../utils/imageReferences.js';
|
|
24
25
|
import { inferCanonicalAssetKind, isValidCanonicalDocumentAsset } from '../utils/canonicalAssetKind.js';
|
|
25
26
|
import { getTaskReferenceLabel, shouldUseProvisionalTaskReferences } from '../utils/taskReferences.js';
|
|
27
|
+
import { resolveStorageKeyPathInsideRoot } from '../utils/pathSafety.js';
|
|
26
28
|
import { parseAiProfileSurfaceType } from '../shared/aiProfileSurfaceType.js';
|
|
27
29
|
import { parseAiProfileSeatScope } from '../shared/aiProfileSeatScope.js';
|
|
28
30
|
import { formatInitiativeReference, formatWorkstreamReference, resolveInitiativeReference, parseWorkstreamReference, } from '../utils/planningReferences.js';
|
|
@@ -1426,12 +1428,8 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
1426
1428
|
function isHttpUrl(value) {
|
|
1427
1429
|
return /^https?:\/\//i.test(value);
|
|
1428
1430
|
}
|
|
1429
|
-
function isWithinRoot(filePath, root) {
|
|
1430
|
-
const relative = path.relative(root, filePath);
|
|
1431
|
-
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
1432
|
-
}
|
|
1433
1431
|
function isApprovedAttachmentSourcePath(filePath, projectRootReal) {
|
|
1434
|
-
return getApprovedAttachmentSourceRoots(projectRootReal).some((root) =>
|
|
1432
|
+
return getApprovedAttachmentSourceRoots(projectRootReal).some((root) => isPathWithinRoot(filePath, root));
|
|
1435
1433
|
}
|
|
1436
1434
|
function toPosixPath(filePath) {
|
|
1437
1435
|
return filePath.split(path.sep).join('/');
|
|
@@ -2976,9 +2974,24 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
2976
2974
|
case "replace_task_checklist": {
|
|
2977
2975
|
const { taskId, items } = args;
|
|
2978
2976
|
const { id: resolvedTaskId } = resolveTaskByIdentifierOrThrow(taskId);
|
|
2977
|
+
const now = new Date().toISOString();
|
|
2978
|
+
const checklistItems = Array.isArray(items)
|
|
2979
|
+
? items.map((item, index) => ({
|
|
2980
|
+
id: item?.id,
|
|
2981
|
+
taskId: resolvedTaskId,
|
|
2982
|
+
title: String(item?.title ?? item?.text ?? '').trim() || `Checklist item ${index + 1}`,
|
|
2983
|
+
isCompleted: Boolean(item?.isCompleted ?? item?.done),
|
|
2984
|
+
order: Number.isFinite(Number(item?.order)) ? Number(item.order) : index,
|
|
2985
|
+
createdAt: String(item?.createdAt || now),
|
|
2986
|
+
updatedAt: item?.updatedAt ? String(item.updatedAt) : now,
|
|
2987
|
+
}))
|
|
2988
|
+
: [];
|
|
2979
2989
|
const updatedTask = core.updateTask(resolvedTaskId, {
|
|
2980
|
-
checklistItems
|
|
2990
|
+
checklistItems,
|
|
2981
2991
|
}, ACTIVE_WORKSPACE_ID, { actorRef: getActiveMcpActorRef() });
|
|
2992
|
+
if (!updatedTask) {
|
|
2993
|
+
throw new Error(`Task ${resolvedTaskId} not found`);
|
|
2994
|
+
}
|
|
2982
2995
|
const replaced = core.listChecklistItems(resolvedTaskId, ACTIVE_WORKSPACE_ID);
|
|
2983
2996
|
recordMcpWriteAudit('replace_task_checklist', {
|
|
2984
2997
|
taskId: resolvedTaskId,
|
|
@@ -2994,7 +3007,6 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
2994
3007
|
text: JSON.stringify({
|
|
2995
3008
|
taskId: resolvedTaskId,
|
|
2996
3009
|
items: replaced,
|
|
2997
|
-
...(updatedTask ? { task: updatedTask } : {}),
|
|
2998
3010
|
}, null, 2),
|
|
2999
3011
|
}],
|
|
3000
3012
|
};
|
|
@@ -4880,8 +4892,8 @@ export function createTaskforceMcpServer(options = {}) {
|
|
|
4880
4892
|
await objectStorageClient.deleteObject(replacedStorageKey).catch(() => { });
|
|
4881
4893
|
}
|
|
4882
4894
|
else if (overwriteTarget.asset.storageProvider === 'local') {
|
|
4883
|
-
const replacedAbsolutePath =
|
|
4884
|
-
if (replacedAbsolutePath
|
|
4895
|
+
const replacedAbsolutePath = resolveStorageKeyPathInsideRoot(basePath, replacedStorageKey);
|
|
4896
|
+
if (replacedAbsolutePath && fs.existsSync(replacedAbsolutePath)) {
|
|
4885
4897
|
fs.rmSync(replacedAbsolutePath, { force: true });
|
|
4886
4898
|
}
|
|
4887
4899
|
}
|
|
@@ -1,18 +1,40 @@
|
|
|
1
1
|
export function createTaskAttachmentHelpers(deps) {
|
|
2
2
|
const isCanonicalDocumentAssetRecord = (asset) => deps.isValidCanonicalDocumentAsset(asset);
|
|
3
|
+
const inferSpecificMimeType = (...candidates) => {
|
|
4
|
+
for (const candidate of candidates) {
|
|
5
|
+
const value = String(candidate || '').trim();
|
|
6
|
+
if (!value)
|
|
7
|
+
continue;
|
|
8
|
+
const inferred = deps.inferMimeTypeFromPath(value);
|
|
9
|
+
if (inferred && inferred !== 'application/octet-stream')
|
|
10
|
+
return inferred;
|
|
11
|
+
}
|
|
12
|
+
return '';
|
|
13
|
+
};
|
|
3
14
|
const describeTaskAttachment = (taskId, attachmentInput) => {
|
|
4
15
|
const attachment = deps.canonicalAssetHelpers.toTaskAttachmentRecord(attachmentInput);
|
|
5
16
|
const asset = deps.canonicalAssetHelpers.getAttachmentAssetRecord(attachment);
|
|
6
17
|
const storageKey = deps.canonicalAssetHelpers.normalizeAttachmentStorageKey(attachment.path, attachment.fsPath);
|
|
7
|
-
const assetId = String(
|
|
8
|
-
const source = (asset?.storageProvider === 'external'
|
|
18
|
+
const assetId = String(asset?.assetId || '').trim() || undefined;
|
|
19
|
+
const source = (asset?.storageProvider === 'external'
|
|
20
|
+
|| /^https?:\/\//i.test(String(attachment.path || '').trim())
|
|
21
|
+
|| (!asset && !storageKey))
|
|
9
22
|
? 'external'
|
|
10
23
|
: 'canonical';
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
24
|
+
const storedMimeType = String(asset?.mimeType || '').trim();
|
|
25
|
+
const inferredMimeType = inferSpecificMimeType(asset?.originalFilename, asset?.storageKey, storageKey && !/^https?:\/\//i.test(storageKey) ? storageKey : '', attachment.originalFilename, attachment.displayName, attachment.path);
|
|
26
|
+
const mimeType = (storedMimeType && storedMimeType !== 'application/octet-stream'
|
|
27
|
+
? storedMimeType
|
|
28
|
+
: inferredMimeType) || storedMimeType || 'application/octet-stream';
|
|
29
|
+
const inferredKind = deps.inferAssetKind(asset?.originalFilename || asset?.storageKey || storageKey || String(attachment.originalFilename || attachment.path || ''), mimeType);
|
|
30
|
+
const kind = asset?.kind && asset.kind !== 'file' ? asset.kind : inferredKind;
|
|
31
|
+
const isCanonicalDocument = kind === 'document' && (!asset
|
|
32
|
+
|| isCanonicalDocumentAssetRecord({
|
|
33
|
+
kind,
|
|
34
|
+
storageKey: String(asset.storageKey || storageKey || '').trim(),
|
|
35
|
+
originalFilename: String(asset.originalFilename || attachment.originalFilename || '').trim(),
|
|
36
|
+
mimeType,
|
|
37
|
+
}));
|
|
16
38
|
const referenceNumber = typeof attachment.referenceNumber === 'number'
|
|
17
39
|
? attachment.referenceNumber
|
|
18
40
|
: (typeof asset?.referenceNumber === 'number' ? asset.referenceNumber : null);
|
package/dist/server/index.js
CHANGED
|
@@ -784,7 +784,7 @@ export function createStandaloneServer(options) {
|
|
|
784
784
|
return;
|
|
785
785
|
}
|
|
786
786
|
}
|
|
787
|
-
if (
|
|
787
|
+
if (shouldApplyAuthRateLimit) {
|
|
788
788
|
const latestAuthRateSettings = resolveAuthRateLimitSettings();
|
|
789
789
|
if (latestAuthRateSettings.enabled !== authRateLimitSettings.enabled
|
|
790
790
|
|| latestAuthRateSettings.maxRequests !== authRateLimitSettings.maxRequests
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
import * as fs from 'fs';
|
|
26
26
|
import * as path from 'path';
|
|
27
27
|
import { normalizeTaskDataRelativePath } from '../../storage/objectStorageClient.js';
|
|
28
|
+
import { resolveExistingStorageKeyFilePathInsideRoot } from '../../utils/pathSafety.js';
|
|
28
29
|
import { shouldUseProvisionalTaskReferences } from '../../utils/taskReferences.js';
|
|
29
30
|
import { purgeExpiredDocuments } from '../../storage/documentPurge.js';
|
|
30
31
|
import { parsePlan } from '../../utils/planParser.js';
|
|
@@ -427,7 +428,7 @@ export function registerDocumentRoutes(deps) {
|
|
|
427
428
|
return;
|
|
428
429
|
}
|
|
429
430
|
const filePath = normalizedRelative
|
|
430
|
-
?
|
|
431
|
+
? resolveExistingStorageKeyFilePathInsideRoot(basePath, normalizedRelative)
|
|
431
432
|
: null;
|
|
432
433
|
const ext = normalizedRelative ? path.extname(normalizedRelative).toLowerCase() : '';
|
|
433
434
|
const resolvedDownloadNameBase = sanitizeDownloadFilename(requestedFilename
|