documentation-hub 5.7.2
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/.eslintrc.json +43 -0
- package/.github/workflows/build.yml +64 -0
- package/.github/workflows/ci.yml +39 -0
- package/.vscode/extensions.json +3 -0
- package/Current.md +97 -0
- package/DocHub_Image.png +0 -0
- package/README.md +666 -0
- package/USER_GUIDE.md +1173 -0
- package/Updater.md +311 -0
- package/build/256x256.png +0 -0
- package/build/512x512.png +0 -0
- package/build/app-update.yml +4 -0
- package/build/create-icon.js +208 -0
- package/build/icon.ico +0 -0
- package/build/icon.png +0 -0
- package/build/icon_1024x1024.png +0 -0
- package/dist/assets/Analytics-BpsG9895.js +1 -0
- package/dist/assets/Card-IAZin8kp.js +1 -0
- package/dist/assets/CurrentSession-B-rFkHvf.js +12 -0
- package/dist/assets/Dashboard-C_5gMb0q.js +1 -0
- package/dist/assets/Documents-CqZ25axS.js +1 -0
- package/dist/assets/Input-l89xwXBi.js +1 -0
- package/dist/assets/Reporting-DqdHJY_a.js +1 -0
- package/dist/assets/Search-XNbu5z_3.js +1 -0
- package/dist/assets/SessionManager-lH9hZfzH.js +1 -0
- package/dist/assets/Sessions-ClZOPYNc.js +1 -0
- package/dist/assets/Settings-DUEHGURa.js +11 -0
- package/dist/assets/index-8xUe8ptc.js +24 -0
- package/dist/assets/index-RYyJqF7O.css +1 -0
- package/dist/assets/path-BkOl0AGO.js +1 -0
- package/dist/assets/promises-ID_B9S-h.js +1 -0
- package/dist/assets/urlHelpers-TvgahX0r.js +1 -0
- package/dist/assets/useToast-yRSO1dkm.js +1 -0
- package/dist/assets/vendor-charts-RkGK5ROP.js +36 -0
- package/dist/assets/vendor-db-l0sNRNKZ.js +1 -0
- package/dist/assets/vendor-react-BVZ_anCF.js +4 -0
- package/dist/assets/vendor-search-Dw8P0qyA.js +1 -0
- package/dist/assets/vendor-ui-BU7NfluV.js +53 -0
- package/dist/electron/PowerAutomateApiService-LfW09ZGr.js +147 -0
- package/dist/electron/main-CXkNtyv-.js +19789 -0
- package/dist/electron/main.js +5 -0
- package/dist/electron/preload.js +1 -0
- package/dist/icon.png +0 -0
- package/dist/index.html +27 -0
- package/docs/CODEBASE_ANALYSIS_REPORT.md +309 -0
- package/docs/DEBUG_LOGGING_GUIDE.md +244 -0
- package/docs/README.md +115 -0
- package/docs/TOC_WIRING_GUIDE.md +344 -0
- package/docs/analysis/Bullet_Symbol_Bug_Analysis.md +136 -0
- package/docs/analysis/DOCXMLATER_ANALYSIS_SUMMARY.txt +169 -0
- package/docs/analysis/Document_Processing_Issues_Analysis.md +704 -0
- package/docs/analysis/FIELD_PRESERVATION_ANALYSIS.md +1200 -0
- package/docs/analysis/INDENTATION_PRESERVE_ANALYSIS.md +181 -0
- package/docs/analysis/INDENTATION_PRESERVE_IMPLEMENTATION.md +207 -0
- package/docs/analysis/List_Implementation.md +206 -0
- package/docs/analysis/List_Implementation_Accuracy_Report.md +366 -0
- package/docs/analysis/PROCESSING_OPTIONS_UI_UPDATES.md +220 -0
- package/docs/analysis/RefactorStyles.md +852 -0
- package/docs/analysis/STYLE_PARAMETER_ENHANCEMENT.md +143 -0
- package/docs/analysis/docxmlater-comparison-todo-2025-11-13.md +636 -0
- package/docs/analysis/docxmlater-implementation-analysis-2025-11-13.md +340 -0
- package/docs/analysis/docxmlater-template_ui-integration-analysis.md +263 -0
- package/docs/analysis/github-issues-to-create.md +237 -0
- package/docs/api/API_README.md +538 -0
- package/docs/api/API_REFERENCE.md +751 -0
- package/docs/api/TYPE_DEFINITIONS.md +869 -0
- package/docs/architecture/FONT_EMBEDDING_GUIDE.md +318 -0
- package/docs/architecture/docxmlater-functions-and-structure.md +726 -0
- package/docs/docxmlater-readme.md +1341 -0
- package/docs/fixes/EXECUTION_LOG_TEST_BASE.md +573 -0
- package/docs/fixes/HYPERLINK_TEXT_SANITIZATION.md +253 -0
- package/docs/fixes/README.md +37 -0
- package/docs/github-issues/issue-1-body.md +125 -0
- package/docs/github-issues/issue-10-body.md +850 -0
- package/docs/github-issues/issue-2-body.md +200 -0
- package/docs/github-issues/issue-3-body.md +270 -0
- package/docs/github-issues/issue-4-body.md +169 -0
- package/docs/github-issues/issue-5-body.md +173 -0
- package/docs/github-issues/issue-6-body.md +158 -0
- package/docs/github-issues/issue-7-body.md +171 -0
- package/docs/github-issues/issue-8-body.md +407 -0
- package/docs/github-issues/issue-9-body.md +515 -0
- package/docs/github-issues/issue-tracker.md +274 -0
- package/docs/github-issues/predictive-analysis-2025-10-18.md +2131 -0
- package/docs/implementation/List_Framework_Refactor_Plan.md +336 -0
- package/docs/implementation/PRIMARY_TEXT_COLOR_FEATURE.md +217 -0
- package/docs/implementation/RELEASE_PLAN_v2.1.0.md +362 -0
- package/docs/implementation/RefactorStyles.md +588 -0
- package/docs/implementation/implement-plan.md +489 -0
- package/docs/implementation/missing-helpers-implementation.md +391 -0
- package/docs/implementation/refactor-plan.md +520 -0
- package/docs/implementation/session-implementation-complete.md +233 -0
- package/docs/implementation/session-management-plan.md +250 -0
- package/docs/setup-checklist.md +77 -0
- package/docs/versions/changelog.md +345 -0
- package/electron/customUpdater.ts +656 -0
- package/electron/main.ts +2441 -0
- package/electron/memoryConfig.ts +187 -0
- package/electron/preload.ts +394 -0
- package/electron/proxyConfig.ts +340 -0
- package/electron/services/BackupService.ts +452 -0
- package/electron/services/DictionaryService.ts +402 -0
- package/electron/services/LocalDictionaryLookupService.ts +147 -0
- package/electron/services/PowerAutomateApiService.ts +231 -0
- package/electron/services/SharePointSyncService.ts +474 -0
- package/electron/windowsCertStore.ts +427 -0
- package/electron/zscalerConfig.ts +381 -0
- package/eslint.config.js +92 -0
- package/jest.config.js +52 -0
- package/package.json +214 -0
- package/postcss.config.mjs +6 -0
- package/public/icon.png +0 -0
- package/publish-release.ps1 +5 -0
- package/renovate.json +30 -0
- package/src/App.tsx +216 -0
- package/src/__mocks__/p-limit.js +12 -0
- package/src/__mocks__/styleMock.js +1 -0
- package/src/components/common/BugReportButton.tsx +44 -0
- package/src/components/common/BugReportDialog.tsx +193 -0
- package/src/components/common/Button.tsx +153 -0
- package/src/components/common/Card.tsx +86 -0
- package/src/components/common/ColorPickerDialog.tsx +177 -0
- package/src/components/common/ConfirmDialog.tsx +96 -0
- package/src/components/common/DebugConsole.tsx +275 -0
- package/src/components/common/EmptyState.tsx +183 -0
- package/src/components/common/ErrorBoundary.tsx +98 -0
- package/src/components/common/ErrorDetailsDialog.tsx +153 -0
- package/src/components/common/ErrorFallback.tsx +218 -0
- package/src/components/common/Input.tsx +109 -0
- package/src/components/common/Skeleton.tsx +184 -0
- package/src/components/common/SplashScreen.tsx +81 -0
- package/src/components/common/Toast.tsx +155 -0
- package/src/components/common/Tooltip.tsx +79 -0
- package/src/components/common/UpdateNotification.tsx +320 -0
- package/src/components/comparison/ComparisonWindow.tsx +374 -0
- package/src/components/comparison/SideBySideDiff.tsx +486 -0
- package/src/components/comparison/index.ts +8 -0
- package/src/components/document/DocumentUploader.tsx +288 -0
- package/src/components/document/HyperlinkPreview.tsx +430 -0
- package/src/components/document/HyperlinkService.md +1484 -0
- package/src/components/document/Hyperlink_Technical_Documentation.md +496 -0
- package/src/components/document/InlineChangesView.tsx +707 -0
- package/src/components/document/ProcessingProgress.tsx +303 -0
- package/src/components/document/ProcessingResults.tsx +256 -0
- package/src/components/document/TrackedChangesDetail.tsx +530 -0
- package/src/components/document/TrackedChangesPanel.tsx +546 -0
- package/src/components/document/VirtualDocumentList.tsx +240 -0
- package/src/components/editor/DocumentEditor.tsx +723 -0
- package/src/components/editor/DocumentEditorModal.tsx +640 -0
- package/src/components/editor/EditorQuickActions.tsx +502 -0
- package/src/components/editor/EditorToolbar.tsx +312 -0
- package/src/components/editor/TableEditor.tsx +926 -0
- package/src/components/editor/index.ts +18 -0
- package/src/components/layout/Header.tsx +190 -0
- package/src/components/layout/Sidebar.tsx +313 -0
- package/src/components/layout/TitleBar.tsx +190 -0
- package/src/components/navigation/CommandPalette.tsx +233 -0
- package/src/components/navigation/KeyboardShortcutsModal.tsx +173 -0
- package/src/components/sessions/ChangeItem.tsx +408 -0
- package/src/components/sessions/ChangeViewer.tsx +1155 -0
- package/src/components/sessions/DocumentComparisonModal.tsx +314 -0
- package/src/components/sessions/ProcessingOptions.tsx +297 -0
- package/src/components/sessions/ReplacementsTab.tsx +438 -0
- package/src/components/sessions/RevisionHandlingOptions.tsx +87 -0
- package/src/components/sessions/SessionManager.tsx +188 -0
- package/src/components/sessions/StylesEditor.tsx +1335 -0
- package/src/components/sessions/TabContainer.tsx +151 -0
- package/src/components/sessions/VirtualSessionList.tsx +157 -0
- package/src/components/sessions/sessionToProcessorManager.tsx +420 -0
- package/src/components/settings/CertificateManager.tsx +410 -0
- package/src/components/settings/SegmentedControl.tsx +88 -0
- package/src/components/settings/SettingRow.tsx +52 -0
- package/src/contexts/GlobalStatsContext.tsx +396 -0
- package/src/contexts/SessionContext.tsx +2129 -0
- package/src/contexts/ThemeContext.tsx +428 -0
- package/src/contexts/UserSettingsContext.tsx +290 -0
- package/src/contexts/__tests__/GlobalStatsContext.test.tsx +390 -0
- package/src/global.d.ts +273 -0
- package/src/hooks/useDocumentQueue.tsx +210 -0
- package/src/hooks/useToast.tsx +55 -0
- package/src/main.tsx +10 -0
- package/src/pages/Analytics.tsx +386 -0
- package/src/pages/CurrentSession.tsx +1174 -0
- package/src/pages/Dashboard.tsx +319 -0
- package/src/pages/Documents.tsx +317 -0
- package/src/pages/Projects.tsx +250 -0
- package/src/pages/Reporting.tsx +386 -0
- package/src/pages/Search.tsx +349 -0
- package/src/pages/Sessions.tsx +285 -0
- package/src/pages/Settings.tsx +2662 -0
- package/src/services/HyperlinkService.ts +1085 -0
- package/src/services/document/DocXMLaterProcessor.ts +617 -0
- package/src/services/document/DocumentProcessingComparison.ts +856 -0
- package/src/services/document/DocumentSnapshotService.ts +575 -0
- package/src/services/document/WordDocumentProcessor.ts +10509 -0
- package/src/services/document/__tests__/DocXMLaterProcessor.hyperlinks.test.md +311 -0
- package/src/services/document/__tests__/WordDocumentProcessor.integration.test.ts +515 -0
- package/src/services/document/__tests__/WordDocumentProcessor.test.ts +812 -0
- package/src/services/document/blanklines/BlankLineManager.ts +658 -0
- package/src/services/document/blanklines/__tests__/paragraphChecks.test.ts +281 -0
- package/src/services/document/blanklines/helpers/blankLineInsertion.ts +87 -0
- package/src/services/document/blanklines/helpers/blankLineSnapshot.ts +251 -0
- package/src/services/document/blanklines/helpers/clearCustom.ts +121 -0
- package/src/services/document/blanklines/helpers/contextChecks.ts +117 -0
- package/src/services/document/blanklines/helpers/imageChecks.ts +51 -0
- package/src/services/document/blanklines/helpers/paragraphChecks.ts +236 -0
- package/src/services/document/blanklines/helpers/removeBlanksBetweenListItems.ts +91 -0
- package/src/services/document/blanklines/helpers/removeTrailingBlanks.ts +35 -0
- package/src/services/document/blanklines/helpers/tableGuards.ts +21 -0
- package/src/services/document/blanklines/index.ts +67 -0
- package/src/services/document/blanklines/rules/additionRules.ts +337 -0
- package/src/services/document/blanklines/rules/indentationRules.ts +317 -0
- package/src/services/document/blanklines/rules/removalRules.ts +362 -0
- package/src/services/document/blanklines/rules/ruleTypes.ts +92 -0
- package/src/services/document/blanklines/types.ts +29 -0
- package/src/services/document/helpers/ImageBorderCropper.ts +377 -0
- package/src/services/document/helpers/__tests__/whitespace.test.ts +272 -0
- package/src/services/document/helpers/whitespace.ts +117 -0
- package/src/services/document/list/ListNormalizer.ts +947 -0
- package/src/services/document/list/index.ts +45 -0
- package/src/services/document/list/list-detection.ts +275 -0
- package/src/services/document/list/list-types.ts +162 -0
- package/src/services/document/processors/HyperlinkProcessor.ts +370 -0
- package/src/services/document/processors/ListProcessor.ts +257 -0
- package/src/services/document/processors/StructureProcessor.ts +176 -0
- package/src/services/document/processors/StyleProcessor.ts +389 -0
- package/src/services/document/processors/TableProcessor.ts +2238 -0
- package/src/services/document/processors/__tests__/HyperlinkProcessor.test.ts +314 -0
- package/src/services/document/processors/__tests__/ListProcessor.test.ts +291 -0
- package/src/services/document/processors/__tests__/StructureProcessor.test.ts +257 -0
- package/src/services/document/processors/__tests__/TableProcessor.hlp-tips-bullets.test.ts +459 -0
- package/src/services/document/processors/__tests__/TableProcessor.test.ts +1604 -0
- package/src/services/document/processors/index.ts +28 -0
- package/src/services/document/types/docx-processing.ts +310 -0
- package/src/services/editor/EditorActionHandlers.ts +901 -0
- package/src/services/editor/index.ts +13 -0
- package/src/setupTests.ts +47 -0
- package/src/styles/global.css +782 -0
- package/src/types/backup.ts +132 -0
- package/src/types/dictionary.ts +125 -0
- package/src/types/document-processing.ts +331 -0
- package/src/types/docxmlater-augments.d.ts +142 -0
- package/src/types/editor.ts +280 -0
- package/src/types/electron.ts +340 -0
- package/src/types/globalStats.ts +155 -0
- package/src/types/hyperlink.ts +471 -0
- package/src/types/operations.ts +354 -0
- package/src/types/session.ts +427 -0
- package/src/types/settings.ts +112 -0
- package/src/utils/MemoryMonitor.ts +248 -0
- package/src/utils/cn.ts +6 -0
- package/src/utils/colorConvert.ts +306 -0
- package/src/utils/diffUtils.ts +347 -0
- package/src/utils/documentUtils.ts +202 -0
- package/src/utils/electronGuard.ts +62 -0
- package/src/utils/indexedDB.ts +915 -0
- package/src/utils/logger.ts +717 -0
- package/src/utils/pathSecurity.ts +232 -0
- package/src/utils/pathValidator.ts +236 -0
- package/src/utils/processingTimeEstimator.ts +153 -0
- package/src/utils/safeJsonParse.ts +62 -0
- package/src/utils/textSanitizer.ts +162 -0
- package/src/utils/urlHelpers.ts +304 -0
- package/src/utils/urlPatterns.ts +198 -0
- package/src/utils/urlSanitizer.ts +152 -0
- package/src/vite-env.d.ts +11 -0
- package/tsconfig.electron.json +19 -0
- package/tsconfig.json +36 -0
- package/tsconfig.node.json +12 -0
- package/typedoc.json +45 -0
- package/vite.config.ts +152 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { logger } from './logger';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Path Security Utilities
|
|
5
|
+
* Provides comprehensive path validation to prevent directory traversal attacks
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const log = logger.namespace('PathSecurity');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Validates that a file path is safe and doesn't contain traversal attempts
|
|
12
|
+
* @param filePath The path to validate
|
|
13
|
+
* @param allowedExtensions Optional array of allowed file extensions (e.g., ['.docx', '.doc'])
|
|
14
|
+
* @returns true if path is safe, false otherwise
|
|
15
|
+
*/
|
|
16
|
+
export function isPathSafe(filePath: string, allowedExtensions?: string[]): boolean {
|
|
17
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
18
|
+
log.warn('Invalid path: empty or not a string');
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Check for null bytes (poison null byte attack)
|
|
23
|
+
if (filePath.includes('\0')) {
|
|
24
|
+
log.error('Security: Path contains null byte', filePath);
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Check for directory traversal patterns
|
|
29
|
+
// NOTE: We need to be careful here - don't reject paths with ".." in folder names
|
|
30
|
+
// Only reject actual path traversal attempts (/../ or \..\)
|
|
31
|
+
// IMPORTANT: Match only when ".." is surrounded by path separators to avoid false positives
|
|
32
|
+
// For example, "DiaTech" should NOT trigger (contains "..") but "C:/Users/../Admin" should
|
|
33
|
+
const traversalPatterns = [
|
|
34
|
+
'/../', // Unix-style parent directory traversal
|
|
35
|
+
'\\..\\', // Windows-style parent directory traversal
|
|
36
|
+
'%2e%2e%2f', // URL-encoded traversal
|
|
37
|
+
'%2e%2e%5c',
|
|
38
|
+
'%252e%252e',
|
|
39
|
+
'%c0%ae%c0%ae',
|
|
40
|
+
'0x2e0x2e',
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const lowerPath = filePath.toLowerCase();
|
|
44
|
+
|
|
45
|
+
// Check for patterns that are clearly traversal attempts
|
|
46
|
+
for (const pattern of traversalPatterns) {
|
|
47
|
+
if (lowerPath.includes(pattern)) {
|
|
48
|
+
log.error('Security: Path contains traversal pattern', { path: filePath, pattern });
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check for leading traversal attempts (must be at start or after separator)
|
|
54
|
+
if (lowerPath.startsWith('../') || lowerPath.startsWith('..\\')) {
|
|
55
|
+
log.error('Security: Path starts with parent directory traversal', filePath);
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Check for trailing traversal attempts (must be at end or before separator)
|
|
60
|
+
if (lowerPath.endsWith('/..') || lowerPath.endsWith('\\..')) {
|
|
61
|
+
log.error('Security: Path ends with parent directory traversal', filePath);
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check for absolute path indicators on different platforms
|
|
66
|
+
const isAbsolute =
|
|
67
|
+
filePath.startsWith('/') || // Unix absolute
|
|
68
|
+
filePath.startsWith('\\') || // Windows UNC
|
|
69
|
+
/^[a-zA-Z]:[\\/]/.test(filePath); // Windows drive letter
|
|
70
|
+
|
|
71
|
+
if (!isAbsolute) {
|
|
72
|
+
log.warn('Security: Path is not absolute', filePath);
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Validate file extension if specified
|
|
77
|
+
if (allowedExtensions && allowedExtensions.length > 0) {
|
|
78
|
+
const hasValidExtension = allowedExtensions.some((ext) =>
|
|
79
|
+
filePath.toLowerCase().endsWith(ext.toLowerCase())
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
if (!hasValidExtension) {
|
|
83
|
+
log.error('Security: File extension not allowed', {
|
|
84
|
+
path: filePath,
|
|
85
|
+
allowedExtensions,
|
|
86
|
+
});
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check for suspicious double extensions that might bypass filters
|
|
92
|
+
const suspiciousDoubleExtensions = [
|
|
93
|
+
'.docx.exe',
|
|
94
|
+
'.docx.scr',
|
|
95
|
+
'.docx.bat',
|
|
96
|
+
'.docx.cmd',
|
|
97
|
+
'.docx.com',
|
|
98
|
+
'.docx.pif',
|
|
99
|
+
'.docx.vbs',
|
|
100
|
+
'.docx.js',
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
for (const ext of suspiciousDoubleExtensions) {
|
|
104
|
+
if (lowerPath.endsWith(ext)) {
|
|
105
|
+
log.error('Security: Suspicious double extension detected', {
|
|
106
|
+
path: filePath,
|
|
107
|
+
extension: ext,
|
|
108
|
+
});
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Check path length (Windows has 260 char limit by default)
|
|
114
|
+
if (filePath.length > 260) {
|
|
115
|
+
log.warn('Path exceeds maximum length (260 characters)', filePath.length);
|
|
116
|
+
// This is a warning not error as some systems support longer paths
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Check for special Windows device names
|
|
120
|
+
const windowsDeviceNames = [
|
|
121
|
+
'CON',
|
|
122
|
+
'PRN',
|
|
123
|
+
'AUX',
|
|
124
|
+
'NUL',
|
|
125
|
+
'COM1',
|
|
126
|
+
'COM2',
|
|
127
|
+
'COM3',
|
|
128
|
+
'COM4',
|
|
129
|
+
'COM5',
|
|
130
|
+
'COM6',
|
|
131
|
+
'COM7',
|
|
132
|
+
'COM8',
|
|
133
|
+
'COM9',
|
|
134
|
+
'LPT1',
|
|
135
|
+
'LPT2',
|
|
136
|
+
'LPT3',
|
|
137
|
+
'LPT4',
|
|
138
|
+
'LPT5',
|
|
139
|
+
'LPT6',
|
|
140
|
+
'LPT7',
|
|
141
|
+
'LPT8',
|
|
142
|
+
'LPT9',
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
const fileName = filePath.split(/[\\/]/).pop()?.split('.')[0]?.toUpperCase();
|
|
146
|
+
if (fileName && windowsDeviceNames.includes(fileName)) {
|
|
147
|
+
log.error('Security: Windows device name detected', { path: filePath, deviceName: fileName });
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Check for URL protocols that shouldn't be in file paths
|
|
152
|
+
const dangerousProtocols = ['file://', 'http://', 'https://', 'ftp://', 'javascript:', 'data:'];
|
|
153
|
+
|
|
154
|
+
for (const protocol of dangerousProtocols) {
|
|
155
|
+
if (lowerPath.includes(protocol)) {
|
|
156
|
+
log.error('Security: URL protocol in file path', { path: filePath, protocol });
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// All checks passed
|
|
162
|
+
log.debug('Path validation passed', filePath);
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Sanitizes a file path by removing dangerous characters
|
|
168
|
+
* Note: This should be used with caution as it modifies the path
|
|
169
|
+
* @param filePath The path to sanitize
|
|
170
|
+
* @returns Sanitized path or null if path is unsafe
|
|
171
|
+
*/
|
|
172
|
+
export function sanitizePath(filePath: string): string | null {
|
|
173
|
+
if (!isPathSafe(filePath)) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Additional sanitization could go here if needed
|
|
178
|
+
// For now, we just return the validated path
|
|
179
|
+
return filePath;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Checks if a path is within an allowed directory
|
|
184
|
+
* @param filePath The path to check
|
|
185
|
+
* @param allowedPaths Array of allowed base directories
|
|
186
|
+
* @returns true if path is within an allowed directory
|
|
187
|
+
*/
|
|
188
|
+
export function isPathWithinAllowed(filePath: string, allowedPaths: string[]): boolean {
|
|
189
|
+
if (!filePath || allowedPaths.length === 0) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Normalize the file path for comparison
|
|
194
|
+
const normalizedFilePath = filePath.replace(/\\/g, '/').toLowerCase();
|
|
195
|
+
|
|
196
|
+
// Check if file path starts with any allowed path
|
|
197
|
+
return allowedPaths.some((allowed) => {
|
|
198
|
+
const normalizedAllowed = allowed.replace(/\\/g, '/').toLowerCase();
|
|
199
|
+
return normalizedFilePath.startsWith(normalizedAllowed);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Validates a batch of file paths
|
|
205
|
+
* @param filePaths Array of paths to validate
|
|
206
|
+
* @param allowedExtensions Optional array of allowed extensions
|
|
207
|
+
* @returns Object with valid and invalid paths
|
|
208
|
+
*/
|
|
209
|
+
export function validateBatchPaths(
|
|
210
|
+
filePaths: string[],
|
|
211
|
+
allowedExtensions?: string[]
|
|
212
|
+
): {
|
|
213
|
+
valid: string[];
|
|
214
|
+
invalid: { path: string; reason: string }[];
|
|
215
|
+
} {
|
|
216
|
+
const valid: string[] = [];
|
|
217
|
+
const invalid: { path: string; reason: string }[] = [];
|
|
218
|
+
|
|
219
|
+
for (const path of filePaths) {
|
|
220
|
+
if (isPathSafe(path, allowedExtensions)) {
|
|
221
|
+
valid.push(path);
|
|
222
|
+
} else {
|
|
223
|
+
invalid.push({
|
|
224
|
+
path,
|
|
225
|
+
reason: 'Failed security validation',
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
log.info(`Batch validation complete: ${valid.length} valid, ${invalid.length} invalid`);
|
|
231
|
+
return { valid, invalid };
|
|
232
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path validation utilities for secure file operations
|
|
3
|
+
*
|
|
4
|
+
* Prevents path traversal attacks and ensures file operations
|
|
5
|
+
* are restricted to safe directories.
|
|
6
|
+
*
|
|
7
|
+
* Security considerations:
|
|
8
|
+
* - Blocks path traversal attempts (../)
|
|
9
|
+
* - Validates paths are within allowed directories
|
|
10
|
+
* - Sanitizes file names
|
|
11
|
+
* - Prevents access to system files
|
|
12
|
+
*
|
|
13
|
+
* Note: Browser-compatible implementation without Node.js path module
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Browser-compatible path utilities
|
|
18
|
+
*/
|
|
19
|
+
const pathUtils = {
|
|
20
|
+
normalize(p: string): string {
|
|
21
|
+
// Replace backslashes with forward slashes for consistency
|
|
22
|
+
let normalized = p.replace(/\\/g, '/');
|
|
23
|
+
|
|
24
|
+
// Remove duplicate slashes
|
|
25
|
+
normalized = normalized.replace(/\/+/g, '/');
|
|
26
|
+
|
|
27
|
+
// Handle . and .. segments
|
|
28
|
+
const parts = normalized.split('/');
|
|
29
|
+
const result: string[] = [];
|
|
30
|
+
|
|
31
|
+
for (const part of parts) {
|
|
32
|
+
if (part === '..' && result.length > 0 && result[result.length - 1] !== '..') {
|
|
33
|
+
result.pop();
|
|
34
|
+
} else if (part !== '.' && part !== '') {
|
|
35
|
+
result.push(part);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return result.join('/');
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
resolve(...paths: string[]): string {
|
|
43
|
+
let resolved = '';
|
|
44
|
+
let isAbsolute = false;
|
|
45
|
+
|
|
46
|
+
for (let i = paths.length - 1; i >= 0 && !isAbsolute; i--) {
|
|
47
|
+
const p = paths[i];
|
|
48
|
+
if (!p) continue;
|
|
49
|
+
|
|
50
|
+
resolved = p + '/' + resolved;
|
|
51
|
+
isAbsolute = /^([a-zA-Z]:)?\//.test(p);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
resolved = this.normalize(resolved);
|
|
55
|
+
return resolved || '.';
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
basename(p: string): string {
|
|
59
|
+
const normalized = p.replace(/\\/g, '/');
|
|
60
|
+
const parts = normalized.split('/');
|
|
61
|
+
return parts[parts.length - 1] || '';
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
dirname(p: string): string {
|
|
65
|
+
const normalized = p.replace(/\\/g, '/');
|
|
66
|
+
const parts = normalized.split('/');
|
|
67
|
+
parts.pop();
|
|
68
|
+
return parts.join('/') || '.';
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
join(...paths: string[]): string {
|
|
72
|
+
return this.normalize(paths.join('/'));
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Checks if a path contains path traversal attempts
|
|
78
|
+
*/
|
|
79
|
+
export function hasPathTraversal(filePath: string): boolean {
|
|
80
|
+
// Normalize path to resolve any .. or . segments
|
|
81
|
+
const normalized = pathUtils.normalize(filePath);
|
|
82
|
+
|
|
83
|
+
// Check for parent directory references
|
|
84
|
+
if (normalized.includes('..')) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check for absolute path attempts on Windows
|
|
89
|
+
const isWindows = navigator.userAgent.includes('Windows');
|
|
90
|
+
if (isWindows) {
|
|
91
|
+
// Check for drive letter changes (C:, D:, etc.)
|
|
92
|
+
const driveLetter = filePath.match(/^[a-zA-Z]:/);
|
|
93
|
+
const normalizedDrive = normalized.match(/^[a-zA-Z]:/);
|
|
94
|
+
|
|
95
|
+
if (driveLetter && normalizedDrive) {
|
|
96
|
+
if (driveLetter[0].toLowerCase() !== normalizedDrive[0].toLowerCase()) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Checks if a path is within an allowed directory
|
|
107
|
+
*/
|
|
108
|
+
export function isWithinDirectory(filePath: string, allowedDir: string): boolean {
|
|
109
|
+
const resolvedPath = pathUtils.resolve(filePath);
|
|
110
|
+
const resolvedAllowedDir = pathUtils.resolve(allowedDir);
|
|
111
|
+
|
|
112
|
+
// On Windows, compare case-insensitively
|
|
113
|
+
const isWindows = navigator.userAgent.includes('Windows');
|
|
114
|
+
if (isWindows) {
|
|
115
|
+
return resolvedPath.toLowerCase().startsWith(resolvedAllowedDir.toLowerCase());
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return resolvedPath.startsWith(resolvedAllowedDir);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Sanitizes a file name by removing dangerous characters
|
|
123
|
+
*/
|
|
124
|
+
export function sanitizeFileName(fileName: string): string {
|
|
125
|
+
// Remove or replace dangerous characters
|
|
126
|
+
// Allow: letters, numbers, spaces, dots, dashes, underscores
|
|
127
|
+
return fileName.replace(/[^a-zA-Z0-9\s.\-_]/g, '_');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Validates if a file path is safe to use
|
|
132
|
+
*
|
|
133
|
+
* @param filePath - The file path to validate
|
|
134
|
+
* @param allowedDirectories - Optional array of allowed base directories
|
|
135
|
+
* @returns Object with validation result and error message if invalid
|
|
136
|
+
*/
|
|
137
|
+
export function validateFilePath(
|
|
138
|
+
filePath: string,
|
|
139
|
+
allowedDirectories?: string[]
|
|
140
|
+
): { isValid: boolean; error?: string } {
|
|
141
|
+
// Check for empty or null paths
|
|
142
|
+
if (!filePath || filePath.trim() === '') {
|
|
143
|
+
return {
|
|
144
|
+
isValid: false,
|
|
145
|
+
error: 'File path is empty',
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Check for path traversal attempts
|
|
150
|
+
if (hasPathTraversal(filePath)) {
|
|
151
|
+
return {
|
|
152
|
+
isValid: false,
|
|
153
|
+
error: 'Path traversal attempt detected',
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Check for dangerous characters in filename
|
|
158
|
+
const fileName = pathUtils.basename(filePath);
|
|
159
|
+
const dangerousChars = /[<>:"|?*]/;
|
|
160
|
+
|
|
161
|
+
if (dangerousChars.test(fileName)) {
|
|
162
|
+
return {
|
|
163
|
+
isValid: false,
|
|
164
|
+
error: 'File name contains illegal characters',
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Validate against allowed directories if provided
|
|
169
|
+
if (allowedDirectories && allowedDirectories.length > 0) {
|
|
170
|
+
const isInAllowedDir = allowedDirectories.some((allowedDir) =>
|
|
171
|
+
isWithinDirectory(filePath, allowedDir)
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
if (!isInAllowedDir) {
|
|
175
|
+
return {
|
|
176
|
+
isValid: false,
|
|
177
|
+
error: 'File path is outside allowed directories',
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Check for system file access attempts
|
|
183
|
+
const systemPaths = ['/etc', '/sys', '/proc', 'C:\\Windows\\System32', 'C:\\Program Files'];
|
|
184
|
+
|
|
185
|
+
const resolvedPath = pathUtils.resolve(filePath);
|
|
186
|
+
const isWindows = navigator.userAgent.includes('Windows');
|
|
187
|
+
const isSystemPath = systemPaths.some((sysPath) => {
|
|
188
|
+
if (isWindows) {
|
|
189
|
+
return resolvedPath.toLowerCase().startsWith(sysPath.toLowerCase());
|
|
190
|
+
}
|
|
191
|
+
return resolvedPath.startsWith(sysPath);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
if (isSystemPath) {
|
|
195
|
+
return {
|
|
196
|
+
isValid: false,
|
|
197
|
+
error: 'Access to system files is not allowed',
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Path is valid
|
|
202
|
+
return { isValid: true };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Gets common allowed directories for document operations
|
|
207
|
+
* Note: Browser environment doesn't have access to environment variables
|
|
208
|
+
* This function returns empty array in browser, should use IPC in Electron
|
|
209
|
+
*/
|
|
210
|
+
export function getDefaultAllowedDirectories(): string[] {
|
|
211
|
+
// In browser context, we can't access environment variables or filesystem
|
|
212
|
+
// This function should be called from Electron main process via IPC
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Validates and sanitizes a file path
|
|
218
|
+
* Returns the sanitized path or throws an error
|
|
219
|
+
*/
|
|
220
|
+
export function validateAndSanitizePath(filePath: string, allowedDirectories?: string[]): string {
|
|
221
|
+
const validation = validateFilePath(filePath, allowedDirectories);
|
|
222
|
+
|
|
223
|
+
if (!validation.isValid) {
|
|
224
|
+
throw new Error(`Invalid file path: ${validation.error}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Resolve to absolute path to prevent any relative path tricks
|
|
228
|
+
const absolutePath = pathUtils.resolve(filePath);
|
|
229
|
+
|
|
230
|
+
// Sanitize the filename
|
|
231
|
+
const dir = pathUtils.dirname(absolutePath);
|
|
232
|
+
const fileName = pathUtils.basename(absolutePath);
|
|
233
|
+
const sanitizedFileName = sanitizeFileName(fileName);
|
|
234
|
+
|
|
235
|
+
return pathUtils.join(dir, sanitizedFileName);
|
|
236
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Processing Time Estimator
|
|
3
|
+
*
|
|
4
|
+
* Estimates remaining time for document processing based on:
|
|
5
|
+
* - Number of documents remaining
|
|
6
|
+
* - Historical processing times
|
|
7
|
+
* - Power Automate API response time (baseline: 12 seconds)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
interface ProcessingTiming {
|
|
11
|
+
documentId: string;
|
|
12
|
+
startTime: number;
|
|
13
|
+
endTime?: number;
|
|
14
|
+
apiCallCount: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const POWER_AUTOMATE_BASELINE_MS = 12000; // 12 seconds baseline for API response
|
|
18
|
+
const MIN_PROCESSING_TIME_MS = 3000; // Minimum time per document (local processing)
|
|
19
|
+
const MAX_HISTORY_SIZE = 20; // Keep last 20 timings for averaging
|
|
20
|
+
|
|
21
|
+
class ProcessingTimeEstimator {
|
|
22
|
+
private timings: ProcessingTiming[] = [];
|
|
23
|
+
private currentTiming: ProcessingTiming | null = null;
|
|
24
|
+
private averageApiTime: number = POWER_AUTOMATE_BASELINE_MS;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Start timing a document
|
|
28
|
+
*/
|
|
29
|
+
startDocument(documentId: string, apiCallCount: number = 1): void {
|
|
30
|
+
this.currentTiming = {
|
|
31
|
+
documentId,
|
|
32
|
+
startTime: Date.now(),
|
|
33
|
+
apiCallCount,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* End timing for current document
|
|
39
|
+
*/
|
|
40
|
+
endDocument(): void {
|
|
41
|
+
if (!this.currentTiming) return;
|
|
42
|
+
|
|
43
|
+
this.currentTiming.endTime = Date.now();
|
|
44
|
+
this.timings.push(this.currentTiming);
|
|
45
|
+
|
|
46
|
+
// Keep only recent timings
|
|
47
|
+
if (this.timings.length > MAX_HISTORY_SIZE) {
|
|
48
|
+
this.timings = this.timings.slice(-MAX_HISTORY_SIZE);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Update average API time based on actual performance
|
|
52
|
+
this.updateAverageApiTime();
|
|
53
|
+
this.currentTiming = null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Record an API call timing
|
|
58
|
+
*/
|
|
59
|
+
recordApiCallTime(durationMs: number): void {
|
|
60
|
+
// Exponential moving average with 0.3 weight for new values
|
|
61
|
+
// This allows the estimate to catch up or slow down based on actual performance
|
|
62
|
+
const alpha = 0.3;
|
|
63
|
+
this.averageApiTime = alpha * durationMs + (1 - alpha) * this.averageApiTime;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Update the average API time based on completed documents
|
|
68
|
+
*/
|
|
69
|
+
private updateAverageApiTime(): void {
|
|
70
|
+
const completedTimings = this.timings.filter(t => t.endTime);
|
|
71
|
+
if (completedTimings.length === 0) return;
|
|
72
|
+
|
|
73
|
+
// Calculate average time per API call
|
|
74
|
+
const recentTimings = completedTimings.slice(-5); // Use last 5 for responsiveness
|
|
75
|
+
const totalTime = recentTimings.reduce((sum, t) => sum + (t.endTime! - t.startTime), 0);
|
|
76
|
+
const totalApiCalls = recentTimings.reduce((sum, t) => sum + t.apiCallCount, 0);
|
|
77
|
+
|
|
78
|
+
if (totalApiCalls > 0) {
|
|
79
|
+
const measuredAverage = totalTime / totalApiCalls;
|
|
80
|
+
// Blend with baseline to avoid wild swings
|
|
81
|
+
const alpha = 0.4;
|
|
82
|
+
this.averageApiTime = alpha * measuredAverage + (1 - alpha) * this.averageApiTime;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Estimate remaining time for pending documents
|
|
88
|
+
*
|
|
89
|
+
* @param documentsRemaining Number of documents left to process
|
|
90
|
+
* @param estimatedApiCallsPerDoc Average API calls per document (default 1)
|
|
91
|
+
* @returns Estimated time remaining in milliseconds
|
|
92
|
+
*/
|
|
93
|
+
estimateRemainingTime(documentsRemaining: number, estimatedApiCallsPerDoc: number = 1): number {
|
|
94
|
+
if (documentsRemaining <= 0) return 0;
|
|
95
|
+
|
|
96
|
+
// Time for current document if in progress
|
|
97
|
+
let currentDocRemaining = 0;
|
|
98
|
+
if (this.currentTiming) {
|
|
99
|
+
const elapsed = Date.now() - this.currentTiming.startTime;
|
|
100
|
+
const estimatedTotal = this.currentTiming.apiCallCount * this.averageApiTime + MIN_PROCESSING_TIME_MS;
|
|
101
|
+
currentDocRemaining = Math.max(0, estimatedTotal - elapsed);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Time for remaining documents (not including current)
|
|
105
|
+
const remainingDocsTime = (documentsRemaining - (this.currentTiming ? 1 : 0)) *
|
|
106
|
+
(estimatedApiCallsPerDoc * this.averageApiTime + MIN_PROCESSING_TIME_MS);
|
|
107
|
+
|
|
108
|
+
return currentDocRemaining + remainingDocsTime;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get the current average API response time
|
|
113
|
+
*/
|
|
114
|
+
getAverageApiTime(): number {
|
|
115
|
+
return this.averageApiTime;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Format milliseconds to human-readable string
|
|
120
|
+
*/
|
|
121
|
+
static formatTime(ms: number): string {
|
|
122
|
+
if (ms <= 0) return '0s';
|
|
123
|
+
|
|
124
|
+
const seconds = Math.ceil(ms / 1000);
|
|
125
|
+
if (seconds < 60) return `${seconds}s`;
|
|
126
|
+
|
|
127
|
+
const minutes = Math.floor(seconds / 60);
|
|
128
|
+
const remainingSeconds = seconds % 60;
|
|
129
|
+
|
|
130
|
+
if (minutes < 60) {
|
|
131
|
+
return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const hours = Math.floor(minutes / 60);
|
|
135
|
+
const remainingMinutes = minutes % 60;
|
|
136
|
+
return `${hours}h ${remainingMinutes}m`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Reset all timings
|
|
141
|
+
*/
|
|
142
|
+
reset(): void {
|
|
143
|
+
this.timings = [];
|
|
144
|
+
this.currentTiming = null;
|
|
145
|
+
this.averageApiTime = POWER_AUTOMATE_BASELINE_MS;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Singleton instance
|
|
150
|
+
export const processingTimeEstimator = new ProcessingTimeEstimator();
|
|
151
|
+
|
|
152
|
+
// Export class for testing
|
|
153
|
+
export { ProcessingTimeEstimator };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { logger } from './logger';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Safely parse JSON with error handling
|
|
5
|
+
* Returns the parsed value or the default value if parsing fails
|
|
6
|
+
*/
|
|
7
|
+
export function safeJsonParse<T>(
|
|
8
|
+
jsonString: string | null | undefined,
|
|
9
|
+
defaultValue: T,
|
|
10
|
+
context?: string
|
|
11
|
+
): T {
|
|
12
|
+
if (!jsonString) {
|
|
13
|
+
return defaultValue;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(jsonString) as T;
|
|
18
|
+
} catch (error) {
|
|
19
|
+
const log = logger.namespace('SafeJsonParse');
|
|
20
|
+
log.error(
|
|
21
|
+
`Failed to parse JSON${context ? ` in ${context}` : ''}:`,
|
|
22
|
+
error instanceof Error ? error.message : 'Unknown error'
|
|
23
|
+
);
|
|
24
|
+
log.debug('Invalid JSON string:', jsonString.substring(0, 100));
|
|
25
|
+
return defaultValue;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Safely stringify JSON with error handling
|
|
31
|
+
* Returns null if stringification fails
|
|
32
|
+
*/
|
|
33
|
+
export function safeJsonStringify(
|
|
34
|
+
value: unknown,
|
|
35
|
+
space?: string | number,
|
|
36
|
+
context?: string
|
|
37
|
+
): string | null {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.stringify(value, null, space);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
const log = logger.namespace('SafeJsonStringify');
|
|
42
|
+
log.error(
|
|
43
|
+
`Failed to stringify JSON${context ? ` in ${context}` : ''}:`,
|
|
44
|
+
error instanceof Error ? error.message : 'Unknown error'
|
|
45
|
+
);
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Type guard to check if a value is a valid JSON object
|
|
52
|
+
*/
|
|
53
|
+
export function isValidJsonObject(value: unknown): value is Record<string, unknown> {
|
|
54
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Type guard to check if a value is a valid JSON array
|
|
59
|
+
*/
|
|
60
|
+
export function isValidJsonArray(value: unknown): value is unknown[] {
|
|
61
|
+
return Array.isArray(value);
|
|
62
|
+
}
|