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.
Files changed (271) hide show
  1. package/.eslintrc.json +43 -0
  2. package/.github/workflows/build.yml +64 -0
  3. package/.github/workflows/ci.yml +39 -0
  4. package/.vscode/extensions.json +3 -0
  5. package/Current.md +97 -0
  6. package/DocHub_Image.png +0 -0
  7. package/README.md +666 -0
  8. package/USER_GUIDE.md +1173 -0
  9. package/Updater.md +311 -0
  10. package/build/256x256.png +0 -0
  11. package/build/512x512.png +0 -0
  12. package/build/app-update.yml +4 -0
  13. package/build/create-icon.js +208 -0
  14. package/build/icon.ico +0 -0
  15. package/build/icon.png +0 -0
  16. package/build/icon_1024x1024.png +0 -0
  17. package/dist/assets/Analytics-BpsG9895.js +1 -0
  18. package/dist/assets/Card-IAZin8kp.js +1 -0
  19. package/dist/assets/CurrentSession-B-rFkHvf.js +12 -0
  20. package/dist/assets/Dashboard-C_5gMb0q.js +1 -0
  21. package/dist/assets/Documents-CqZ25axS.js +1 -0
  22. package/dist/assets/Input-l89xwXBi.js +1 -0
  23. package/dist/assets/Reporting-DqdHJY_a.js +1 -0
  24. package/dist/assets/Search-XNbu5z_3.js +1 -0
  25. package/dist/assets/SessionManager-lH9hZfzH.js +1 -0
  26. package/dist/assets/Sessions-ClZOPYNc.js +1 -0
  27. package/dist/assets/Settings-DUEHGURa.js +11 -0
  28. package/dist/assets/index-8xUe8ptc.js +24 -0
  29. package/dist/assets/index-RYyJqF7O.css +1 -0
  30. package/dist/assets/path-BkOl0AGO.js +1 -0
  31. package/dist/assets/promises-ID_B9S-h.js +1 -0
  32. package/dist/assets/urlHelpers-TvgahX0r.js +1 -0
  33. package/dist/assets/useToast-yRSO1dkm.js +1 -0
  34. package/dist/assets/vendor-charts-RkGK5ROP.js +36 -0
  35. package/dist/assets/vendor-db-l0sNRNKZ.js +1 -0
  36. package/dist/assets/vendor-react-BVZ_anCF.js +4 -0
  37. package/dist/assets/vendor-search-Dw8P0qyA.js +1 -0
  38. package/dist/assets/vendor-ui-BU7NfluV.js +53 -0
  39. package/dist/electron/PowerAutomateApiService-LfW09ZGr.js +147 -0
  40. package/dist/electron/main-CXkNtyv-.js +19789 -0
  41. package/dist/electron/main.js +5 -0
  42. package/dist/electron/preload.js +1 -0
  43. package/dist/icon.png +0 -0
  44. package/dist/index.html +27 -0
  45. package/docs/CODEBASE_ANALYSIS_REPORT.md +309 -0
  46. package/docs/DEBUG_LOGGING_GUIDE.md +244 -0
  47. package/docs/README.md +115 -0
  48. package/docs/TOC_WIRING_GUIDE.md +344 -0
  49. package/docs/analysis/Bullet_Symbol_Bug_Analysis.md +136 -0
  50. package/docs/analysis/DOCXMLATER_ANALYSIS_SUMMARY.txt +169 -0
  51. package/docs/analysis/Document_Processing_Issues_Analysis.md +704 -0
  52. package/docs/analysis/FIELD_PRESERVATION_ANALYSIS.md +1200 -0
  53. package/docs/analysis/INDENTATION_PRESERVE_ANALYSIS.md +181 -0
  54. package/docs/analysis/INDENTATION_PRESERVE_IMPLEMENTATION.md +207 -0
  55. package/docs/analysis/List_Implementation.md +206 -0
  56. package/docs/analysis/List_Implementation_Accuracy_Report.md +366 -0
  57. package/docs/analysis/PROCESSING_OPTIONS_UI_UPDATES.md +220 -0
  58. package/docs/analysis/RefactorStyles.md +852 -0
  59. package/docs/analysis/STYLE_PARAMETER_ENHANCEMENT.md +143 -0
  60. package/docs/analysis/docxmlater-comparison-todo-2025-11-13.md +636 -0
  61. package/docs/analysis/docxmlater-implementation-analysis-2025-11-13.md +340 -0
  62. package/docs/analysis/docxmlater-template_ui-integration-analysis.md +263 -0
  63. package/docs/analysis/github-issues-to-create.md +237 -0
  64. package/docs/api/API_README.md +538 -0
  65. package/docs/api/API_REFERENCE.md +751 -0
  66. package/docs/api/TYPE_DEFINITIONS.md +869 -0
  67. package/docs/architecture/FONT_EMBEDDING_GUIDE.md +318 -0
  68. package/docs/architecture/docxmlater-functions-and-structure.md +726 -0
  69. package/docs/docxmlater-readme.md +1341 -0
  70. package/docs/fixes/EXECUTION_LOG_TEST_BASE.md +573 -0
  71. package/docs/fixes/HYPERLINK_TEXT_SANITIZATION.md +253 -0
  72. package/docs/fixes/README.md +37 -0
  73. package/docs/github-issues/issue-1-body.md +125 -0
  74. package/docs/github-issues/issue-10-body.md +850 -0
  75. package/docs/github-issues/issue-2-body.md +200 -0
  76. package/docs/github-issues/issue-3-body.md +270 -0
  77. package/docs/github-issues/issue-4-body.md +169 -0
  78. package/docs/github-issues/issue-5-body.md +173 -0
  79. package/docs/github-issues/issue-6-body.md +158 -0
  80. package/docs/github-issues/issue-7-body.md +171 -0
  81. package/docs/github-issues/issue-8-body.md +407 -0
  82. package/docs/github-issues/issue-9-body.md +515 -0
  83. package/docs/github-issues/issue-tracker.md +274 -0
  84. package/docs/github-issues/predictive-analysis-2025-10-18.md +2131 -0
  85. package/docs/implementation/List_Framework_Refactor_Plan.md +336 -0
  86. package/docs/implementation/PRIMARY_TEXT_COLOR_FEATURE.md +217 -0
  87. package/docs/implementation/RELEASE_PLAN_v2.1.0.md +362 -0
  88. package/docs/implementation/RefactorStyles.md +588 -0
  89. package/docs/implementation/implement-plan.md +489 -0
  90. package/docs/implementation/missing-helpers-implementation.md +391 -0
  91. package/docs/implementation/refactor-plan.md +520 -0
  92. package/docs/implementation/session-implementation-complete.md +233 -0
  93. package/docs/implementation/session-management-plan.md +250 -0
  94. package/docs/setup-checklist.md +77 -0
  95. package/docs/versions/changelog.md +345 -0
  96. package/electron/customUpdater.ts +656 -0
  97. package/electron/main.ts +2441 -0
  98. package/electron/memoryConfig.ts +187 -0
  99. package/electron/preload.ts +394 -0
  100. package/electron/proxyConfig.ts +340 -0
  101. package/electron/services/BackupService.ts +452 -0
  102. package/electron/services/DictionaryService.ts +402 -0
  103. package/electron/services/LocalDictionaryLookupService.ts +147 -0
  104. package/electron/services/PowerAutomateApiService.ts +231 -0
  105. package/electron/services/SharePointSyncService.ts +474 -0
  106. package/electron/windowsCertStore.ts +427 -0
  107. package/electron/zscalerConfig.ts +381 -0
  108. package/eslint.config.js +92 -0
  109. package/jest.config.js +52 -0
  110. package/package.json +214 -0
  111. package/postcss.config.mjs +6 -0
  112. package/public/icon.png +0 -0
  113. package/publish-release.ps1 +5 -0
  114. package/renovate.json +30 -0
  115. package/src/App.tsx +216 -0
  116. package/src/__mocks__/p-limit.js +12 -0
  117. package/src/__mocks__/styleMock.js +1 -0
  118. package/src/components/common/BugReportButton.tsx +44 -0
  119. package/src/components/common/BugReportDialog.tsx +193 -0
  120. package/src/components/common/Button.tsx +153 -0
  121. package/src/components/common/Card.tsx +86 -0
  122. package/src/components/common/ColorPickerDialog.tsx +177 -0
  123. package/src/components/common/ConfirmDialog.tsx +96 -0
  124. package/src/components/common/DebugConsole.tsx +275 -0
  125. package/src/components/common/EmptyState.tsx +183 -0
  126. package/src/components/common/ErrorBoundary.tsx +98 -0
  127. package/src/components/common/ErrorDetailsDialog.tsx +153 -0
  128. package/src/components/common/ErrorFallback.tsx +218 -0
  129. package/src/components/common/Input.tsx +109 -0
  130. package/src/components/common/Skeleton.tsx +184 -0
  131. package/src/components/common/SplashScreen.tsx +81 -0
  132. package/src/components/common/Toast.tsx +155 -0
  133. package/src/components/common/Tooltip.tsx +79 -0
  134. package/src/components/common/UpdateNotification.tsx +320 -0
  135. package/src/components/comparison/ComparisonWindow.tsx +374 -0
  136. package/src/components/comparison/SideBySideDiff.tsx +486 -0
  137. package/src/components/comparison/index.ts +8 -0
  138. package/src/components/document/DocumentUploader.tsx +288 -0
  139. package/src/components/document/HyperlinkPreview.tsx +430 -0
  140. package/src/components/document/HyperlinkService.md +1484 -0
  141. package/src/components/document/Hyperlink_Technical_Documentation.md +496 -0
  142. package/src/components/document/InlineChangesView.tsx +707 -0
  143. package/src/components/document/ProcessingProgress.tsx +303 -0
  144. package/src/components/document/ProcessingResults.tsx +256 -0
  145. package/src/components/document/TrackedChangesDetail.tsx +530 -0
  146. package/src/components/document/TrackedChangesPanel.tsx +546 -0
  147. package/src/components/document/VirtualDocumentList.tsx +240 -0
  148. package/src/components/editor/DocumentEditor.tsx +723 -0
  149. package/src/components/editor/DocumentEditorModal.tsx +640 -0
  150. package/src/components/editor/EditorQuickActions.tsx +502 -0
  151. package/src/components/editor/EditorToolbar.tsx +312 -0
  152. package/src/components/editor/TableEditor.tsx +926 -0
  153. package/src/components/editor/index.ts +18 -0
  154. package/src/components/layout/Header.tsx +190 -0
  155. package/src/components/layout/Sidebar.tsx +313 -0
  156. package/src/components/layout/TitleBar.tsx +190 -0
  157. package/src/components/navigation/CommandPalette.tsx +233 -0
  158. package/src/components/navigation/KeyboardShortcutsModal.tsx +173 -0
  159. package/src/components/sessions/ChangeItem.tsx +408 -0
  160. package/src/components/sessions/ChangeViewer.tsx +1155 -0
  161. package/src/components/sessions/DocumentComparisonModal.tsx +314 -0
  162. package/src/components/sessions/ProcessingOptions.tsx +297 -0
  163. package/src/components/sessions/ReplacementsTab.tsx +438 -0
  164. package/src/components/sessions/RevisionHandlingOptions.tsx +87 -0
  165. package/src/components/sessions/SessionManager.tsx +188 -0
  166. package/src/components/sessions/StylesEditor.tsx +1335 -0
  167. package/src/components/sessions/TabContainer.tsx +151 -0
  168. package/src/components/sessions/VirtualSessionList.tsx +157 -0
  169. package/src/components/sessions/sessionToProcessorManager.tsx +420 -0
  170. package/src/components/settings/CertificateManager.tsx +410 -0
  171. package/src/components/settings/SegmentedControl.tsx +88 -0
  172. package/src/components/settings/SettingRow.tsx +52 -0
  173. package/src/contexts/GlobalStatsContext.tsx +396 -0
  174. package/src/contexts/SessionContext.tsx +2129 -0
  175. package/src/contexts/ThemeContext.tsx +428 -0
  176. package/src/contexts/UserSettingsContext.tsx +290 -0
  177. package/src/contexts/__tests__/GlobalStatsContext.test.tsx +390 -0
  178. package/src/global.d.ts +273 -0
  179. package/src/hooks/useDocumentQueue.tsx +210 -0
  180. package/src/hooks/useToast.tsx +55 -0
  181. package/src/main.tsx +10 -0
  182. package/src/pages/Analytics.tsx +386 -0
  183. package/src/pages/CurrentSession.tsx +1174 -0
  184. package/src/pages/Dashboard.tsx +319 -0
  185. package/src/pages/Documents.tsx +317 -0
  186. package/src/pages/Projects.tsx +250 -0
  187. package/src/pages/Reporting.tsx +386 -0
  188. package/src/pages/Search.tsx +349 -0
  189. package/src/pages/Sessions.tsx +285 -0
  190. package/src/pages/Settings.tsx +2662 -0
  191. package/src/services/HyperlinkService.ts +1085 -0
  192. package/src/services/document/DocXMLaterProcessor.ts +617 -0
  193. package/src/services/document/DocumentProcessingComparison.ts +856 -0
  194. package/src/services/document/DocumentSnapshotService.ts +575 -0
  195. package/src/services/document/WordDocumentProcessor.ts +10509 -0
  196. package/src/services/document/__tests__/DocXMLaterProcessor.hyperlinks.test.md +311 -0
  197. package/src/services/document/__tests__/WordDocumentProcessor.integration.test.ts +515 -0
  198. package/src/services/document/__tests__/WordDocumentProcessor.test.ts +812 -0
  199. package/src/services/document/blanklines/BlankLineManager.ts +658 -0
  200. package/src/services/document/blanklines/__tests__/paragraphChecks.test.ts +281 -0
  201. package/src/services/document/blanklines/helpers/blankLineInsertion.ts +87 -0
  202. package/src/services/document/blanklines/helpers/blankLineSnapshot.ts +251 -0
  203. package/src/services/document/blanklines/helpers/clearCustom.ts +121 -0
  204. package/src/services/document/blanklines/helpers/contextChecks.ts +117 -0
  205. package/src/services/document/blanklines/helpers/imageChecks.ts +51 -0
  206. package/src/services/document/blanklines/helpers/paragraphChecks.ts +236 -0
  207. package/src/services/document/blanklines/helpers/removeBlanksBetweenListItems.ts +91 -0
  208. package/src/services/document/blanklines/helpers/removeTrailingBlanks.ts +35 -0
  209. package/src/services/document/blanklines/helpers/tableGuards.ts +21 -0
  210. package/src/services/document/blanklines/index.ts +67 -0
  211. package/src/services/document/blanklines/rules/additionRules.ts +337 -0
  212. package/src/services/document/blanklines/rules/indentationRules.ts +317 -0
  213. package/src/services/document/blanklines/rules/removalRules.ts +362 -0
  214. package/src/services/document/blanklines/rules/ruleTypes.ts +92 -0
  215. package/src/services/document/blanklines/types.ts +29 -0
  216. package/src/services/document/helpers/ImageBorderCropper.ts +377 -0
  217. package/src/services/document/helpers/__tests__/whitespace.test.ts +272 -0
  218. package/src/services/document/helpers/whitespace.ts +117 -0
  219. package/src/services/document/list/ListNormalizer.ts +947 -0
  220. package/src/services/document/list/index.ts +45 -0
  221. package/src/services/document/list/list-detection.ts +275 -0
  222. package/src/services/document/list/list-types.ts +162 -0
  223. package/src/services/document/processors/HyperlinkProcessor.ts +370 -0
  224. package/src/services/document/processors/ListProcessor.ts +257 -0
  225. package/src/services/document/processors/StructureProcessor.ts +176 -0
  226. package/src/services/document/processors/StyleProcessor.ts +389 -0
  227. package/src/services/document/processors/TableProcessor.ts +2238 -0
  228. package/src/services/document/processors/__tests__/HyperlinkProcessor.test.ts +314 -0
  229. package/src/services/document/processors/__tests__/ListProcessor.test.ts +291 -0
  230. package/src/services/document/processors/__tests__/StructureProcessor.test.ts +257 -0
  231. package/src/services/document/processors/__tests__/TableProcessor.hlp-tips-bullets.test.ts +459 -0
  232. package/src/services/document/processors/__tests__/TableProcessor.test.ts +1604 -0
  233. package/src/services/document/processors/index.ts +28 -0
  234. package/src/services/document/types/docx-processing.ts +310 -0
  235. package/src/services/editor/EditorActionHandlers.ts +901 -0
  236. package/src/services/editor/index.ts +13 -0
  237. package/src/setupTests.ts +47 -0
  238. package/src/styles/global.css +782 -0
  239. package/src/types/backup.ts +132 -0
  240. package/src/types/dictionary.ts +125 -0
  241. package/src/types/document-processing.ts +331 -0
  242. package/src/types/docxmlater-augments.d.ts +142 -0
  243. package/src/types/editor.ts +280 -0
  244. package/src/types/electron.ts +340 -0
  245. package/src/types/globalStats.ts +155 -0
  246. package/src/types/hyperlink.ts +471 -0
  247. package/src/types/operations.ts +354 -0
  248. package/src/types/session.ts +427 -0
  249. package/src/types/settings.ts +112 -0
  250. package/src/utils/MemoryMonitor.ts +248 -0
  251. package/src/utils/cn.ts +6 -0
  252. package/src/utils/colorConvert.ts +306 -0
  253. package/src/utils/diffUtils.ts +347 -0
  254. package/src/utils/documentUtils.ts +202 -0
  255. package/src/utils/electronGuard.ts +62 -0
  256. package/src/utils/indexedDB.ts +915 -0
  257. package/src/utils/logger.ts +717 -0
  258. package/src/utils/pathSecurity.ts +232 -0
  259. package/src/utils/pathValidator.ts +236 -0
  260. package/src/utils/processingTimeEstimator.ts +153 -0
  261. package/src/utils/safeJsonParse.ts +62 -0
  262. package/src/utils/textSanitizer.ts +162 -0
  263. package/src/utils/urlHelpers.ts +304 -0
  264. package/src/utils/urlPatterns.ts +198 -0
  265. package/src/utils/urlSanitizer.ts +152 -0
  266. package/src/vite-env.d.ts +11 -0
  267. package/tsconfig.electron.json +19 -0
  268. package/tsconfig.json +36 -0
  269. package/tsconfig.node.json +12 -0
  270. package/typedoc.json +45 -0
  271. 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
+ }