@logictan/dsh-config-manager 0.1.60

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 (429) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +105 -0
  3. package/cordis.patch.yml +21 -0
  4. package/lib/adapters/agent-instructions.d.ts +13 -0
  5. package/lib/adapters/agent-instructions.js +46 -0
  6. package/lib/adapters/agent-presets.d.ts +13 -0
  7. package/lib/adapters/agent-presets.js +14 -0
  8. package/lib/adapters/credentials.d.ts +37 -0
  9. package/lib/adapters/credentials.js +122 -0
  10. package/lib/adapters/file-collection.d.ts +15 -0
  11. package/lib/adapters/file-collection.js +127 -0
  12. package/lib/adapters/index.d.ts +57 -0
  13. package/lib/adapters/index.js +55 -0
  14. package/lib/adapters/link-report.d.ts +16 -0
  15. package/lib/adapters/link-report.js +47 -0
  16. package/lib/adapters/mcp.d.ts +28 -0
  17. package/lib/adapters/mcp.js +178 -0
  18. package/lib/adapters/plugin-files.d.ts +18 -0
  19. package/lib/adapters/plugin-files.js +142 -0
  20. package/lib/adapters/plugins.d.ts +41 -0
  21. package/lib/adapters/plugins.js +603 -0
  22. package/lib/adapters/pnpm-workspace.d.ts +26 -0
  23. package/lib/adapters/pnpm-workspace.js +147 -0
  24. package/lib/adapters/prompts.d.ts +30 -0
  25. package/lib/adapters/prompts.js +231 -0
  26. package/lib/adapters/providers.d.ts +34 -0
  27. package/lib/adapters/providers.js +155 -0
  28. package/lib/adapters/self.d.ts +17 -0
  29. package/lib/adapters/self.js +71 -0
  30. package/lib/adapters/sessions.d.ts +14 -0
  31. package/lib/adapters/sessions.js +15 -0
  32. package/lib/adapters/settings.d.ts +28 -0
  33. package/lib/adapters/settings.js +165 -0
  34. package/lib/adapters/skills.d.ts +13 -0
  35. package/lib/adapters/skills.js +14 -0
  36. package/lib/adapters/test-helpers.d.ts +125 -0
  37. package/lib/adapters/test-helpers.js +236 -0
  38. package/lib/adapters/ui.d.ts +27 -0
  39. package/lib/adapters/ui.js +89 -0
  40. package/lib/adapters/workspaces.d.ts +13 -0
  41. package/lib/adapters/workspaces.js +106 -0
  42. package/lib/client.d.ts +1026 -0
  43. package/lib/client.js +10047 -0
  44. package/lib/core/analyzer.d.ts +99 -0
  45. package/lib/core/analyzer.js +934 -0
  46. package/lib/core/backup.d.ts +138 -0
  47. package/lib/core/backup.js +513 -0
  48. package/lib/core/boot-rescue.d.ts +146 -0
  49. package/lib/core/boot-rescue.js +618 -0
  50. package/lib/core/cache-cleaner.d.ts +31 -0
  51. package/lib/core/cache-cleaner.js +95 -0
  52. package/lib/core/config-lifecycle.d.ts +180 -0
  53. package/lib/core/config-lifecycle.js +481 -0
  54. package/lib/core/config-snapshot.d.ts +135 -0
  55. package/lib/core/config-snapshot.js +424 -0
  56. package/lib/core/config-state.d.ts +70 -0
  57. package/lib/core/config-state.js +177 -0
  58. package/lib/core/consult-source.d.ts +58 -0
  59. package/lib/core/consult-source.js +260 -0
  60. package/lib/core/crash-report.d.ts +84 -0
  61. package/lib/core/crash-report.js +317 -0
  62. package/lib/core/exporter.d.ts +68 -0
  63. package/lib/core/exporter.js +375 -0
  64. package/lib/core/ghost-sweep.d.ts +19 -0
  65. package/lib/core/ghost-sweep.js +52 -0
  66. package/lib/core/importer.d.ts +60 -0
  67. package/lib/core/importer.js +29 -0
  68. package/lib/core/index.d.ts +14 -0
  69. package/lib/core/index.js +14 -0
  70. package/lib/core/journal.d.ts +197 -0
  71. package/lib/core/journal.js +418 -0
  72. package/lib/core/local-plugin-host.d.ts +35 -0
  73. package/lib/core/local-plugin-host.js +120 -0
  74. package/lib/core/local-plugin-pack.d.ts +135 -0
  75. package/lib/core/local-plugin-pack.js +278 -0
  76. package/lib/core/messages.d.ts +310 -0
  77. package/lib/core/messages.js +621 -0
  78. package/lib/core/migration-consult.d.ts +117 -0
  79. package/lib/core/migration-consult.js +363 -0
  80. package/lib/core/migration-history.d.ts +153 -0
  81. package/lib/core/migration-history.js +411 -0
  82. package/lib/core/model-tools.d.ts +50 -0
  83. package/lib/core/model-tools.js +148 -0
  84. package/lib/core/msg-types.d.ts +8 -0
  85. package/lib/core/msg-types.js +6 -0
  86. package/lib/core/phase3-child-crash.d.ts +1 -0
  87. package/lib/core/phase3-child-crash.js +87 -0
  88. package/lib/core/phase3-host.d.ts +138 -0
  89. package/lib/core/phase3-host.js +359 -0
  90. package/lib/core/phase3-prod-child.d.ts +1 -0
  91. package/lib/core/phase3-prod-child.js +35 -0
  92. package/lib/core/phase4-crash-child.d.ts +1 -0
  93. package/lib/core/phase4-crash-child.js +103 -0
  94. package/lib/core/plugin-cli.d.ts +150 -0
  95. package/lib/core/plugin-cli.js +497 -0
  96. package/lib/core/reconcile.d.ts +120 -0
  97. package/lib/core/reconcile.js +389 -0
  98. package/lib/core/recovery-orchestrator.d.ts +94 -0
  99. package/lib/core/recovery-orchestrator.js +327 -0
  100. package/lib/core/restore.d.ts +162 -0
  101. package/lib/core/restore.js +703 -0
  102. package/lib/core/rollback.d.ts +12 -0
  103. package/lib/core/rollback.js +145 -0
  104. package/lib/core/run-registry.d.ts +85 -0
  105. package/lib/core/run-registry.js +157 -0
  106. package/lib/core/startup-barrier.d.ts +53 -0
  107. package/lib/core/startup-barrier.js +77 -0
  108. package/lib/core/types.d.ts +515 -0
  109. package/lib/core/types.js +27 -0
  110. package/lib/core/undo.d.ts +73 -0
  111. package/lib/core/undo.js +87 -0
  112. package/lib/core/validator.d.ts +18 -0
  113. package/lib/core/validator.js +77 -0
  114. package/lib/core/verify-recovery.d.ts +21 -0
  115. package/lib/core/verify-recovery.js +256 -0
  116. package/lib/core/watcher.d.ts +101 -0
  117. package/lib/core/watcher.js +219 -0
  118. package/lib/index.d.ts +238 -0
  119. package/lib/index.js +2764 -0
  120. package/lib/migrations/index.d.ts +29 -0
  121. package/lib/migrations/index.js +56 -0
  122. package/lib/migrations/v1-to-v2.d.ts +9 -0
  123. package/lib/migrations/v1-to-v2.js +10 -0
  124. package/lib/profiles/index.d.ts +4 -0
  125. package/lib/profiles/index.js +5 -0
  126. package/lib/profiles/profile-manager.d.ts +106 -0
  127. package/lib/profiles/profile-manager.js +543 -0
  128. package/lib/schema/config.d.ts +28 -0
  129. package/lib/schema/config.js +165 -0
  130. package/lib/schema/index.d.ts +24 -0
  131. package/lib/schema/index.js +26 -0
  132. package/lib/schema/manifest.d.ts +28 -0
  133. package/lib/schema/manifest.js +109 -0
  134. package/lib/schema/tombstones.d.ts +46 -0
  135. package/lib/schema/tombstones.js +64 -0
  136. package/lib/schema/types.d.ts +232 -0
  137. package/lib/schema/types.js +8 -0
  138. package/lib/schema/versions.d.ts +27 -0
  139. package/lib/schema/versions.js +50 -0
  140. package/lib/security/encryption.d.ts +85 -0
  141. package/lib/security/encryption.js +279 -0
  142. package/lib/security/index.d.ts +16 -0
  143. package/lib/security/index.js +17 -0
  144. package/lib/security/integrity.d.ts +30 -0
  145. package/lib/security/integrity.js +78 -0
  146. package/lib/security/redaction.d.ts +26 -0
  147. package/lib/security/redaction.js +95 -0
  148. package/lib/security/secret-scanner.d.ts +86 -0
  149. package/lib/security/secret-scanner.js +404 -0
  150. package/lib/security/vault.d.ts +59 -0
  151. package/lib/security/vault.js +151 -0
  152. package/lib/security/zip-security.d.ts +27 -0
  153. package/lib/security/zip-security.js +154 -0
  154. package/lib/sync/autosync-config.d.ts +39 -0
  155. package/lib/sync/autosync-config.js +151 -0
  156. package/lib/sync/autosync-scheduler.d.ts +139 -0
  157. package/lib/sync/autosync-scheduler.js +564 -0
  158. package/lib/sync/fs.d.ts +18 -0
  159. package/lib/sync/fs.js +49 -0
  160. package/lib/sync/git/git-transport.d.ts +100 -0
  161. package/lib/sync/git/git-transport.js +437 -0
  162. package/lib/sync/github-auth.d.ts +131 -0
  163. package/lib/sync/github-auth.js +264 -0
  164. package/lib/sync/layout.d.ts +43 -0
  165. package/lib/sync/layout.js +203 -0
  166. package/lib/sync/snapshot-json.d.ts +9 -0
  167. package/lib/sync/snapshot-json.js +107 -0
  168. package/lib/sync/sync-config.d.ts +81 -0
  169. package/lib/sync/sync-config.js +281 -0
  170. package/lib/sync/sync-engine.d.ts +269 -0
  171. package/lib/sync/sync-engine.js +591 -0
  172. package/lib/sync/sync-history.d.ts +42 -0
  173. package/lib/sync/sync-history.js +78 -0
  174. package/lib/sync/sync-selection.d.ts +29 -0
  175. package/lib/sync/sync-selection.js +127 -0
  176. package/lib/sync/sync-session.d.ts +40 -0
  177. package/lib/sync/sync-session.js +54 -0
  178. package/lib/sync/sync-state.d.ts +37 -0
  179. package/lib/sync/sync-state.js +108 -0
  180. package/lib/sync/transport.d.ts +75 -0
  181. package/lib/sync/transport.js +55 -0
  182. package/lib/sync/ui-prefs.d.ts +34 -0
  183. package/lib/sync/ui-prefs.js +109 -0
  184. package/lib/sync/webdav/webdav-transport.d.ts +77 -0
  185. package/lib/sync/webdav/webdav-transport.js +451 -0
  186. package/lib/ui/errors.d.ts +19 -0
  187. package/lib/ui/errors.js +81 -0
  188. package/lib/ui/export-flow.d.ts +82 -0
  189. package/lib/ui/export-flow.js +98 -0
  190. package/lib/ui/i18n.d.ts +289 -0
  191. package/lib/ui/i18n.js +581 -0
  192. package/lib/ui/migration-consult-view.d.ts +91 -0
  193. package/lib/ui/migration-consult-view.js +115 -0
  194. package/lib/ui/progress.d.ts +38 -0
  195. package/lib/ui/progress.js +87 -0
  196. package/lib/ui/report.d.ts +23 -0
  197. package/lib/ui/report.js +153 -0
  198. package/lib/ui/test-helpers.d.ts +69 -0
  199. package/lib/ui/test-helpers.js +154 -0
  200. package/lib/ui/types.d.ts +257 -0
  201. package/lib/ui/types.js +21 -0
  202. package/lib/utils/atomic-write.d.ts +88 -0
  203. package/lib/utils/atomic-write.js +399 -0
  204. package/lib/utils/env-lock.d.ts +393 -0
  205. package/lib/utils/env-lock.js +930 -0
  206. package/lib/utils/hashing.d.ts +17 -0
  207. package/lib/utils/hashing.js +41 -0
  208. package/lib/utils/json.d.ts +24 -0
  209. package/lib/utils/json.js +76 -0
  210. package/lib/utils/logger.d.ts +39 -0
  211. package/lib/utils/logger.js +82 -0
  212. package/lib/utils/paths.d.ts +42 -0
  213. package/lib/utils/paths.js +189 -0
  214. package/lib/utils/proxy.d.ts +69 -0
  215. package/lib/utils/proxy.js +455 -0
  216. package/lib/utils/recursive-walk.d.ts +24 -0
  217. package/lib/utils/recursive-walk.js +134 -0
  218. package/lib/utils/zip.d.ts +54 -0
  219. package/lib/utils/zip.js +285 -0
  220. package/package.json +188 -0
  221. package/src/adapters/agent-instructions.ts +48 -0
  222. package/src/adapters/agent-presets.ts +14 -0
  223. package/src/adapters/credentials.test.ts +90 -0
  224. package/src/adapters/credentials.ts +143 -0
  225. package/src/adapters/file-collection.ts +137 -0
  226. package/src/adapters/files.test.ts +353 -0
  227. package/src/adapters/index.ts +94 -0
  228. package/src/adapters/link-report.test.ts +65 -0
  229. package/src/adapters/link-report.ts +61 -0
  230. package/src/adapters/mcp.test.ts +96 -0
  231. package/src/adapters/mcp.ts +185 -0
  232. package/src/adapters/plugin-files.ts +146 -0
  233. package/src/adapters/plugins.test.ts +326 -0
  234. package/src/adapters/plugins.ts +615 -0
  235. package/src/adapters/pnpm-workspace.test.ts +87 -0
  236. package/src/adapters/pnpm-workspace.ts +150 -0
  237. package/src/adapters/prompts.test.ts +110 -0
  238. package/src/adapters/prompts.ts +230 -0
  239. package/src/adapters/providers.test.ts +126 -0
  240. package/src/adapters/providers.ts +173 -0
  241. package/src/adapters/roundtrip.test.ts +190 -0
  242. package/src/adapters/self.test.ts +92 -0
  243. package/src/adapters/self.ts +75 -0
  244. package/src/adapters/sessions.ts +15 -0
  245. package/src/adapters/settings.test.ts +100 -0
  246. package/src/adapters/settings.ts +187 -0
  247. package/src/adapters/skills.ts +14 -0
  248. package/src/adapters/test-helpers.ts +248 -0
  249. package/src/adapters/ui.test.ts +57 -0
  250. package/src/adapters/ui.ts +108 -0
  251. package/src/adapters/workspaces.test.ts +106 -0
  252. package/src/adapters/workspaces.ts +109 -0
  253. package/src/client/ConfigManagerSection.tsx +90 -0
  254. package/src/client/api.ts +66 -0
  255. package/src/client/client-types.ts +18 -0
  256. package/src/client/common/ErrorBanner.tsx +73 -0
  257. package/src/client/common/Icon.tsx +129 -0
  258. package/src/client/common/Modal.tsx +173 -0
  259. package/src/client/common/ToastViewport.tsx +73 -0
  260. package/src/client/common/toast-store.test.ts +192 -0
  261. package/src/client/common/toast-store.ts +181 -0
  262. package/src/client/common/toast-types.ts +7 -0
  263. package/src/client/common/ui.tsx +342 -0
  264. package/src/client/config-manager.module.css +2971 -0
  265. package/src/client/css-modules.d.ts +8 -0
  266. package/src/client/index.ts +65 -0
  267. package/src/client/locales.ts +56 -0
  268. package/src/client/lucide-icons.d.ts +38 -0
  269. package/src/client/run-store.test.ts +223 -0
  270. package/src/client/run-store.ts +372 -0
  271. package/src/client/sync/SyncConfirmView.tsx +379 -0
  272. package/src/client/sync/SyncHistoryView.test.ts +40 -0
  273. package/src/client/sync/SyncHistoryView.tsx +220 -0
  274. package/src/client/sync/SyncSettingsView.tsx +1535 -0
  275. package/src/client/sync/history-model.test.ts +194 -0
  276. package/src/client/sync/history-model.ts +195 -0
  277. package/src/client/sync/sync-api.test.ts +585 -0
  278. package/src/client/sync/sync-api.ts +543 -0
  279. package/src/client/sync/sync-locales.ts +416 -0
  280. package/src/client/sync/sync-push-preview.test.ts +52 -0
  281. package/src/client/sync/sync-view-v2.test.ts +213 -0
  282. package/src/client/sync/sync-view.test.ts +487 -0
  283. package/src/client/sync/sync-view.ts +759 -0
  284. package/src/core/analyzer.ts +1068 -0
  285. package/src/core/backup.ts +605 -0
  286. package/src/core/boot-rescue.test.ts +847 -0
  287. package/src/core/boot-rescue.ts +737 -0
  288. package/src/core/cache-cleaner.test.ts +169 -0
  289. package/src/core/cache-cleaner.ts +121 -0
  290. package/src/core/config-lifecycle.test.ts +727 -0
  291. package/src/core/config-lifecycle.ts +573 -0
  292. package/src/core/config-snapshot.test.ts +369 -0
  293. package/src/core/config-snapshot.ts +522 -0
  294. package/src/core/config-state.test.ts +381 -0
  295. package/src/core/config-state.ts +207 -0
  296. package/src/core/consult-source.test.ts +199 -0
  297. package/src/core/consult-source.ts +293 -0
  298. package/src/core/crash-report.test.ts +380 -0
  299. package/src/core/crash-report.ts +369 -0
  300. package/src/core/exporter.ts +417 -0
  301. package/src/core/ghost-sweep.test.ts +94 -0
  302. package/src/core/ghost-sweep.ts +67 -0
  303. package/src/core/importer.ts +83 -0
  304. package/src/core/index.ts +56 -0
  305. package/src/core/journal.test.ts +257 -0
  306. package/src/core/journal.ts +518 -0
  307. package/src/core/local-plugin-host.ts +152 -0
  308. package/src/core/local-plugin-pack.test.ts +359 -0
  309. package/src/core/local-plugin-pack.ts +363 -0
  310. package/src/core/messages.test.ts +54 -0
  311. package/src/core/messages.ts +664 -0
  312. package/src/core/migration-consult.test.ts +315 -0
  313. package/src/core/migration-consult.ts +494 -0
  314. package/src/core/migration-history.test.ts +319 -0
  315. package/src/core/migration-history.ts +484 -0
  316. package/src/core/model-tools.test.ts +190 -0
  317. package/src/core/model-tools.ts +208 -0
  318. package/src/core/msg-types.ts +10 -0
  319. package/src/core/phase3-child-crash.ts +95 -0
  320. package/src/core/phase3-host.ts +411 -0
  321. package/src/core/phase3-p1.test.ts +212 -0
  322. package/src/core/phase3-p2b-fingerprint.test.ts +189 -0
  323. package/src/core/phase3-prod-child.ts +36 -0
  324. package/src/core/phase3-production-integration.test.ts +286 -0
  325. package/src/core/phase4-crash-child.ts +101 -0
  326. package/src/core/phase4-crash-injection.test.ts +166 -0
  327. package/src/core/plugin-cli.fs.test.ts +184 -0
  328. package/src/core/plugin-cli.test.ts +137 -0
  329. package/src/core/plugin-cli.ts +563 -0
  330. package/src/core/reconcile.test.ts +392 -0
  331. package/src/core/reconcile.ts +498 -0
  332. package/src/core/recovery-orchestrator.test.ts +145 -0
  333. package/src/core/recovery-orchestrator.ts +366 -0
  334. package/src/core/restore-manage.test.ts +94 -0
  335. package/src/core/restore.ts +849 -0
  336. package/src/core/rollback.ts +151 -0
  337. package/src/core/run-progress.test.ts +237 -0
  338. package/src/core/run-registry.test.ts +232 -0
  339. package/src/core/run-registry.ts +209 -0
  340. package/src/core/smoke.test.ts +842 -0
  341. package/src/core/startup-barrier.ts +99 -0
  342. package/src/core/types.ts +542 -0
  343. package/src/core/undo.ts +116 -0
  344. package/src/core/validator.ts +84 -0
  345. package/src/core/verify-recovery.ts +265 -0
  346. package/src/core/watcher.test.ts +326 -0
  347. package/src/core/watcher.ts +263 -0
  348. package/src/index.facade.test.ts +265 -0
  349. package/src/index.sync.test.ts +217 -0
  350. package/src/index.ts +2975 -0
  351. package/src/migrations/index.ts +71 -0
  352. package/src/migrations/v1-to-v2.ts +17 -0
  353. package/src/profiles/index.ts +8 -0
  354. package/src/profiles/profile-manager.test.ts +282 -0
  355. package/src/profiles/profile-manager.ts +630 -0
  356. package/src/schema/config.ts +165 -0
  357. package/src/schema/index.ts +71 -0
  358. package/src/schema/manifest-schema.test.ts +519 -0
  359. package/src/schema/manifest.ts +124 -0
  360. package/src/schema/tombstones.test.ts +131 -0
  361. package/src/schema/tombstones.ts +89 -0
  362. package/src/schema/types.ts +245 -0
  363. package/src/schema/versions.ts +55 -0
  364. package/src/security/encryption.ts +335 -0
  365. package/src/security/index.ts +16 -0
  366. package/src/security/integrity.ts +102 -0
  367. package/src/security/redaction.ts +101 -0
  368. package/src/security/secret-scanner.ts +445 -0
  369. package/src/security/security.test.ts +1215 -0
  370. package/src/security/vault.test.ts +164 -0
  371. package/src/security/vault.ts +198 -0
  372. package/src/security/zip-security.ts +180 -0
  373. package/src/sync/autosync-config.test.ts +147 -0
  374. package/src/sync/autosync-config.ts +179 -0
  375. package/src/sync/autosync-scheduler.test.ts +490 -0
  376. package/src/sync/autosync-scheduler.ts +646 -0
  377. package/src/sync/fs.ts +48 -0
  378. package/src/sync/git/git-transport.test.ts +673 -0
  379. package/src/sync/git/git-transport.ts +491 -0
  380. package/src/sync/github-auth.test.ts +280 -0
  381. package/src/sync/github-auth.ts +356 -0
  382. package/src/sync/layout.test.ts +270 -0
  383. package/src/sync/layout.ts +222 -0
  384. package/src/sync/snapshot-json.test.ts +119 -0
  385. package/src/sync/snapshot-json.ts +128 -0
  386. package/src/sync/sync-config.test.ts +310 -0
  387. package/src/sync/sync-config.ts +299 -0
  388. package/src/sync/sync-engine.test.ts +1074 -0
  389. package/src/sync/sync-engine.ts +754 -0
  390. package/src/sync/sync-history.test.ts +116 -0
  391. package/src/sync/sync-history.ts +120 -0
  392. package/src/sync/sync-selection.test.ts +186 -0
  393. package/src/sync/sync-selection.ts +143 -0
  394. package/src/sync/sync-session.test.ts +143 -0
  395. package/src/sync/sync-session.ts +77 -0
  396. package/src/sync/sync-state.test.ts +164 -0
  397. package/src/sync/sync-state.ts +131 -0
  398. package/src/sync/transport.test.ts +111 -0
  399. package/src/sync/transport.ts +118 -0
  400. package/src/sync/ui-prefs.test.ts +152 -0
  401. package/src/sync/ui-prefs.ts +135 -0
  402. package/src/sync/webdav/webdav-transport.test.ts +871 -0
  403. package/src/sync/webdav/webdav-transport.ts +535 -0
  404. package/src/ui/errors.test.ts +47 -0
  405. package/src/ui/errors.ts +100 -0
  406. package/src/ui/export-flow.test.ts +87 -0
  407. package/src/ui/export-flow.ts +156 -0
  408. package/src/ui/i18n.ts +590 -0
  409. package/src/ui/migration-consult-view.test.ts +151 -0
  410. package/src/ui/migration-consult-view.ts +178 -0
  411. package/src/ui/progress.test.ts +47 -0
  412. package/src/ui/progress.ts +102 -0
  413. package/src/ui/report.test.ts +101 -0
  414. package/src/ui/report.ts +146 -0
  415. package/src/ui/test-helpers.ts +184 -0
  416. package/src/ui/types.ts +325 -0
  417. package/src/utils/atomic-write.test.ts +312 -0
  418. package/src/utils/atomic-write.ts +422 -0
  419. package/src/utils/env-lock.test.ts +999 -0
  420. package/src/utils/env-lock.ts +1086 -0
  421. package/src/utils/hashing.ts +53 -0
  422. package/src/utils/json.ts +77 -0
  423. package/src/utils/logger.ts +114 -0
  424. package/src/utils/paths.ts +176 -0
  425. package/src/utils/proxy.test.ts +465 -0
  426. package/src/utils/proxy.ts +493 -0
  427. package/src/utils/recursive-walk.test.ts +148 -0
  428. package/src/utils/recursive-walk.ts +145 -0
  429. package/src/utils/zip.ts +332 -0
@@ -0,0 +1,164 @@
1
+ /**
2
+ * 文件级 vault 单元测试(src/security/vault.ts)。
3
+ *
4
+ * 覆盖:镜像 → 删除源 → 恢复 → 缺失提示;幂等;清理旧镜像;不覆盖已存在目标;
5
+ * 路径穿越防御。全部基于内存 FileSystemFacade mock(对齐 core 层解耦,node 可测)。
6
+ */
7
+ import test from 'node:test';
8
+ import assert from 'node:assert/strict';
9
+ import path from 'node:path';
10
+
11
+ import { refreshVault, restoreVaultFiles, listVault, vaultRootOf, DEFAULT_SENSITIVE_RELS } from './vault.ts';
12
+ import type { FileSystemFacade } from '../core/types.ts';
13
+ import { normalizePath } from '../utils/paths.ts';
14
+
15
+ /* ================= 内存 fs mock(key 与宿主一致:home 内绝对路径归一化) ================= */
16
+
17
+ class MemFs implements FileSystemFacade {
18
+ files = new Map<string, Uint8Array>();
19
+ private readonly homeDir: string;
20
+ constructor(homeDir: string) {
21
+ this.homeDir = homeDir;
22
+ }
23
+ private key(p: string): string {
24
+ return normalizePath(path.resolve(this.homeDir, p));
25
+ }
26
+ async readFile(relPath: string): Promise<Uint8Array> {
27
+ const v = this.files.get(this.key(relPath));
28
+ if (v === undefined) throw new Error(`ENOENT: ${relPath}`);
29
+ return v;
30
+ }
31
+ async writeFile(relPath: string, data: Uint8Array): Promise<void> {
32
+ this.files.set(this.key(relPath), data);
33
+ }
34
+ async exists(relPath: string): Promise<boolean> {
35
+ return this.files.has(this.key(relPath));
36
+ }
37
+ async copy(from: string, to: string): Promise<void> {
38
+ const v = this.files.get(this.key(from));
39
+ if (v === undefined) throw new Error(`ENOENT: ${from}`);
40
+ this.files.set(this.key(to), v);
41
+ }
42
+ async remove(relPath: string): Promise<void> {
43
+ this.files.delete(this.key(relPath));
44
+ }
45
+ async listRecursive(dir: string): Promise<string[]> {
46
+ const base = normalizePath(path.resolve(this.homeDir));
47
+ const prefix = normalizePath(path.resolve(this.homeDir, dir));
48
+ const out: string[] = [];
49
+ for (const k of this.files.keys()) {
50
+ if (k === prefix || k.startsWith(prefix + '/')) {
51
+ out.push(k.slice(base.length).replace(/^[\\/]+/, ''));
52
+ }
53
+ }
54
+ return out.sort();
55
+ }
56
+ async mkdir(): Promise<void> { /* 内存实现无需建目录 */ }
57
+ }
58
+
59
+ const HOME = path.resolve('/home/alice'); // POSIX 风格绝对路径,跨平台 key 一致
60
+ const DATA_DIR = path.join(HOME, 'dsh-config-manager');
61
+ const CRED = '.credentials.yaml';
62
+ const CRED_SRC = path.join(HOME, CRED);
63
+ const CRED_VAULT = path.join(DATA_DIR, 'vault', CRED);
64
+ const CRED_BYTES = Buffer.from('apiKey: sk-super-secret-123\n', 'utf8');
65
+
66
+ test('refreshVault:现存敏感文件镜像入库,重复调用幂等', async () => {
67
+ const fs = new MemFs(HOME);
68
+ await fs.writeFile(CRED_SRC, CRED_BYTES);
69
+
70
+ const r1 = await refreshVault(fs, DATA_DIR, HOME, DEFAULT_SENSITIVE_RELS);
71
+ assert.deepEqual(r1.mirrored, [CRED], '应镜像 .credentials.yaml');
72
+ assert.deepEqual(r1.cleaned, [], '首次刷新无清理');
73
+ assert.deepEqual(r1.skipped, [], '无跳过项');
74
+ // vault 内容与源字节一致
75
+ const mirrored = await fs.readFile(CRED_VAULT);
76
+ assert.deepEqual(Buffer.from(mirrored), CRED_BYTES, 'vault 镜像字节应与源一致');
77
+
78
+ // 幂等:再次刷新结果一致,无重复条目
79
+ const r2 = await refreshVault(fs, DATA_DIR, HOME, DEFAULT_SENSITIVE_RELS);
80
+ assert.deepEqual(r2.mirrored, [CRED]);
81
+ const entries = await listVault(fs, DATA_DIR, HOME);
82
+ assert.equal(entries.length, 1, '重复刷新不应产生重复条目');
83
+ assert.equal(entries[0]!.rel, CRED);
84
+ assert.equal(entries[0]!.vaultPath, CRED_VAULT);
85
+ });
86
+
87
+ test('refreshVault:源缺失跳过;源删除与清单移除触发旧镜像清理', async () => {
88
+ const fs = new MemFs(HOME);
89
+
90
+ // 源缺失:不报错、不入库
91
+ const r0 = await refreshVault(fs, DATA_DIR, HOME, DEFAULT_SENSITIVE_RELS);
92
+ assert.deepEqual(r0.mirrored, []);
93
+ assert.deepEqual(r0.cleaned, []);
94
+
95
+ // 先镜像 → 删除源 → 再刷新:清理旧镜像
96
+ await fs.writeFile(CRED_SRC, CRED_BYTES);
97
+ await refreshVault(fs, DATA_DIR, HOME, DEFAULT_SENSITIVE_RELS);
98
+ assert.equal(await fs.exists(CRED_VAULT), true, '镜像应先入库');
99
+ await fs.remove(CRED_SRC); // 模拟凭据文件被删除
100
+ const r1 = await refreshVault(fs, DATA_DIR, HOME, DEFAULT_SENSITIVE_RELS);
101
+ assert.deepEqual(r1.mirrored, [], '源不存在不再镜像');
102
+ assert.deepEqual(r1.cleaned, [CRED], '源已删除 → 旧镜像被清理');
103
+ assert.equal(await fs.exists(CRED_VAULT), false, 'vault 旧镜像应已删除');
104
+
105
+ // 清单移除(rels 不再包含):旧镜像同样被清理
106
+ await fs.writeFile(CRED_SRC, CRED_BYTES);
107
+ await refreshVault(fs, DATA_DIR, HOME, DEFAULT_SENSITIVE_RELS);
108
+ const r2 = await refreshVault(fs, DATA_DIR, HOME, []);
109
+ assert.deepEqual(r2.cleaned, [CRED], 'rel 不在清单 → 旧镜像被清理');
110
+ });
111
+
112
+ test('restoreVaultFiles:回填缺失目标、不覆盖已存在目标、缺失项提示重填', async () => {
113
+ const fs = new MemFs(HOME);
114
+ await fs.writeFile(CRED_VAULT, CRED_BYTES); // 只有 vault 有镜像
115
+
116
+ // 目标缺失 → 回填
117
+ const r1 = await restoreVaultFiles(fs, DATA_DIR, HOME, DEFAULT_SENSITIVE_RELS);
118
+ assert.deepEqual(r1.restored, [CRED]);
119
+ assert.deepEqual(r1.missing, []);
120
+ assert.deepEqual(Buffer.from(await fs.readFile(CRED_SRC)), CRED_BYTES, '目标应回填 vault 内容');
121
+
122
+ // 目标已存在 → 不覆盖(安全)
123
+ const newer = Buffer.from('apiKey: sk-newer-on-target\n', 'utf8');
124
+ await fs.writeFile(CRED_SRC, newer);
125
+ const r2 = await restoreVaultFiles(fs, DATA_DIR, HOME, DEFAULT_SENSITIVE_RELS);
126
+ assert.deepEqual(r2.restored, []);
127
+ assert.deepEqual(r2.skipped, [{ rel: CRED, reason: 'targetExists' }]);
128
+ assert.deepEqual(Buffer.from(await fs.readFile(CRED_SRC)), newer, '已存在目标不得被覆盖');
129
+
130
+ // vault 缺对应文件(跨机恢复/从未镜像)→ missing 提示重填
131
+ const OTHER = 'profiles/web/secrets.yaml';
132
+ const r3 = await restoreVaultFiles(fs, DATA_DIR, HOME, [CRED, OTHER]);
133
+ assert.deepEqual(r3.missing, [OTHER]);
134
+ assert.deepEqual(r3.restored, []);
135
+
136
+ // 跨机场景:vault 为空 → 全部 missing
137
+ const fs2 = new MemFs(HOME);
138
+ const r4 = await restoreVaultFiles(fs2, DATA_DIR, HOME, DEFAULT_SENSITIVE_RELS);
139
+ assert.deepEqual(r4.restored, []);
140
+ assert.deepEqual(r4.missing, [CRED], 'vault 空 = 跨机恢复,提示重填');
141
+ });
142
+
143
+ test('listVault:按 rel 排序列出镜像内容', async () => {
144
+ const fs = new MemFs(HOME);
145
+ await fs.writeFile(CRED_VAULT, CRED_BYTES);
146
+ await fs.writeFile(path.join(DATA_DIR, 'vault', 'profiles', 'web', 'secrets.yaml'), Buffer.from('token: t\n', 'utf8'));
147
+
148
+ const entries = await listVault(fs, DATA_DIR, HOME);
149
+ assert.deepEqual(entries.map((e) => e.rel), ['.credentials.yaml', 'profiles/web/secrets.yaml']);
150
+ assert.ok(entries.every((e) => e.vaultPath.startsWith(vaultRootOf(DATA_DIR))), 'vaultPath 应在 vault 根下');
151
+
152
+ // 空 vault → 空数组
153
+ const fs2 = new MemFs(HOME);
154
+ assert.deepEqual(await listVault(fs2, DATA_DIR, HOME), []);
155
+ });
156
+
157
+ test('非法 rel(路径穿越 / 绝对路径 / 盘符)被拒绝', async () => {
158
+ const fs = new MemFs(HOME);
159
+ const badRels = ['../escape.yaml', '/etc/passwd', 'C:\\evil.yaml', 'a/../../b.yaml'];
160
+ for (const rel of badRels) {
161
+ await assert.rejects(refreshVault(fs, DATA_DIR, HOME, [rel]), /非法敏感文件路径/, `refresh 应拒绝 ${rel}`);
162
+ await assert.rejects(restoreVaultFiles(fs, DATA_DIR, HOME, [rel]), /非法敏感文件路径/, `restore 应拒绝 ${rel}`);
163
+ }
164
+ });
@@ -0,0 +1,198 @@
1
+ /**
2
+ * 文件级 vault(敏感文件本机镜像,设计参考 dsh-backup 的「文件级 vault」思路)。
3
+ *
4
+ * 动机:字段级 Secret 扫描只能剥离结构化数据里的敏感值;像 .credentials.yaml 这类
5
+ * 「整文件即秘密」的文件,字节明文不进归档、不进同步,而是只镜像到本机
6
+ * <dataDir>/vault/ 目录(vault 目录本身绝不进入导出/同步,与 self 分区白名单互斥),
7
+ * 恢复(如跨机导入)时从 vault 回填 $DSH_HOME;vault 无对应文件(跨机恢复、
8
+ * 从未镜像过)则提示用户重填。比字段级扫描更彻底(整文件隔离)。
9
+ *
10
+ * 纯函数式设计:fs 一律经 FileSystemFacade 参数注入(对齐 core 层解耦原则),
11
+ * 不直接 import node:fs;node 环境可用内存 mock 全量测试。
12
+ * 路径约定:dshHome / dataDir 为绝对路径;敏感文件清单 rel 一律为相对 $DSH_HOME
13
+ * 的斜杠分隔相对路径(与 FileSystemFacade.listRecursive 的 home-relative 产出一致)。
14
+ *
15
+ * 安全不变量:
16
+ * - vault 内容 = 敏感文件明文,仅存在于本机 dataDir,永不进入 ZIP / 同步 / 日志;
17
+ * - rel 入参必须通过 isPathSafe(拒绝绝对路径 / .. / 盘符 / NUL),防路径穿越;
18
+ * - 恢复不覆盖已存在的目标文件(避免覆盖目标机更新后的凭据),缺失项记入 missing。
19
+ *
20
+ * 权限说明:FileSystemFacade 无 chmod 能力(宿主 mkdir 已递归),vault 目录权限
21
+ * 跟随宿主 dataDir 既有策略;Windows 不强制 POSIX 权限位。
22
+ */
23
+ import path from 'node:path';
24
+ import type { FileSystemFacade } from '../core/types.ts';
25
+ import { isPathSafe, normalizePath } from '../utils/paths.ts';
26
+
27
+ /**
28
+ * 默认敏感文件清单(相对 $DSH_HOME):当前 DSH 唯一「整文件即秘密」的文件
29
+ * (.credentials.yaml = 凭据明文 token/password/密钥)。
30
+ * 宿主可传入扩展清单(如 profiles/<p>/secrets.yaml、.env 等),默认清单保持最小。
31
+ */
32
+ export const DEFAULT_SENSITIVE_RELS: readonly string[] = [
33
+ '.credentials.yaml',
34
+ ];
35
+
36
+ /** vault 根目录 = <dataDir>/vault */
37
+ export function vaultRootOf(dataDir: string): string {
38
+ return path.join(dataDir, 'vault');
39
+ }
40
+
41
+ /** refreshVault 结果 */
42
+ export interface VaultRefreshResult {
43
+ /** 本次实际镜像入库的敏感文件(相对 $DSH_HOME) */
44
+ mirrored: string[];
45
+ /** 因源文件已不存在 / rel 不再属于清单而被清理的旧镜像(相对 $DSH_HOME) */
46
+ cleaned: string[];
47
+ /** 单个文件镜像失败(非致命:读源 / 写镜像异常),reason 为错误消息 */
48
+ skipped: { rel: string; reason: string }[];
49
+ }
50
+
51
+ /** restoreVaultFiles 结果 */
52
+ export interface VaultRestoreResult {
53
+ /** 已从 vault 回填到 $DSH_HOME 的文件(相对 $DSH_HOME) */
54
+ restored: string[];
55
+ /** vault 中缺失、需要用户重填的文件(跨机恢复 vault 为空 → 全部缺失) */
56
+ missing: string[];
57
+ /** 跳过项:reason 为 'targetExists'(目标已存在不覆盖)或错误消息(拷贝失败) */
58
+ skipped: { rel: string; reason: string }[];
59
+ }
60
+
61
+ /** listVault 条目 */
62
+ export interface VaultEntry {
63
+ /** 原始敏感文件路径(相对 $DSH_HOME) */
64
+ rel: string;
65
+ /** vault 内镜像的绝对路径 */
66
+ vaultPath: string;
67
+ }
68
+
69
+ /** rel 必须为安全相对路径(拒绝绝对路径 / .. / 盘符 / NUL),防路径穿越 */
70
+ function assertSafeRel(rel: string): void {
71
+ if (!isPathSafe(rel)) throw new Error(`vault: 非法敏感文件路径 "${rel}"`);
72
+ }
73
+
74
+ /** 校验 + 归一化 rel 清单(统一 / 分隔,与 FileSystemFacade.listRecursive 产出对齐) */
75
+ function normalizeRels(rels: readonly string[]): string[] {
76
+ const out: string[] = [];
77
+ for (const rel of rels) {
78
+ assertSafeRel(rel);
79
+ out.push(normalizePath(rel));
80
+ }
81
+ return out;
82
+ }
83
+
84
+ /**
85
+ * 刷新 vault:把 $DSH_HOME 下现存敏感文件镜像到 <dataDir>/vault/<rel>。
86
+ * 幂等:重复调用结果一致(同字节覆盖写,不产生重复条目)。
87
+ * 清理:源文件已不存在、或 rel 不再属于传入清单的旧镜像会被删除。
88
+ * 返回实际入库清单(供报告 / 提示「已镜像到本机 vault」)。
89
+ */
90
+ export async function refreshVault(
91
+ fs: FileSystemFacade,
92
+ dataDir: string,
93
+ dshHome: string,
94
+ rels: readonly string[],
95
+ ): Promise<VaultRefreshResult> {
96
+ const normRels = normalizeRels(rels);
97
+ const vaultRoot = vaultRootOf(dataDir);
98
+ await fs.mkdir(vaultRoot); // 宿主 mkdir 递归;失败向上抛(调用方按尽力而为处理)
99
+ const mirrored: string[] = [];
100
+ const skipped: { rel: string; reason: string }[] = [];
101
+
102
+ // 1. 镜像现存敏感文件(源不存在则跳过;读/写失败按文件记录,不中断整体)
103
+ for (const rel of normRels) {
104
+ const src = path.join(dshHome, rel);
105
+ const dst = path.join(vaultRoot, rel);
106
+ try {
107
+ if (!(await fs.exists(src))) continue;
108
+ const data = await fs.readFile(src);
109
+ await fs.writeFile(dst, data);
110
+ mirrored.push(rel);
111
+ } catch (err) {
112
+ skipped.push({ rel, reason: err instanceof Error ? err.message : String(err) });
113
+ }
114
+ }
115
+
116
+ // 2. 清理旧镜像:源已不存在 或 rel 不再属于清单(listRecursive 产出为 home-relative)
117
+ const cleaned: string[] = [];
118
+ try {
119
+ const vaultFiles = await fs.listRecursive(vaultRoot);
120
+ for (const homeRel of vaultFiles) {
121
+ const vaultRel = normalizePath(path.relative(vaultRoot, path.join(dshHome, homeRel)));
122
+ if (vaultRel === '' || vaultRel.startsWith('..') || path.isAbsolute(vaultRel)) continue; // 防御:理论上不会发生
123
+ if (!normRels.includes(vaultRel)) {
124
+ await fs.remove(path.join(vaultRoot, vaultRel));
125
+ cleaned.push(vaultRel);
126
+ continue;
127
+ }
128
+ if (!(await fs.exists(path.join(dshHome, vaultRel)))) {
129
+ await fs.remove(path.join(vaultRoot, vaultRel));
130
+ cleaned.push(vaultRel);
131
+ }
132
+ }
133
+ } catch {
134
+ // 清理是尽力而为:listRecursive / remove 失败不影响镜像结果
135
+ }
136
+
137
+ return { mirrored, cleaned, skipped };
138
+ }
139
+
140
+ /**
141
+ * 从 vault 回填敏感文件到 $DSH_HOME(跨机恢复 / 本机误删后的恢复)。
142
+ * 不覆盖已存在的目标文件(安全:避免覆盖目标机更新后的凭据);
143
+ * vault 无对应文件(跨机恢复、从未镜像过)记入 missing,由调用方提示用户重填。
144
+ */
145
+ export async function restoreVaultFiles(
146
+ fs: FileSystemFacade,
147
+ dataDir: string,
148
+ dshHome: string,
149
+ rels: readonly string[],
150
+ ): Promise<VaultRestoreResult> {
151
+ const normRels = normalizeRels(rels);
152
+ const vaultRoot = vaultRootOf(dataDir);
153
+ const restored: string[] = [];
154
+ const missing: string[] = [];
155
+ const skipped: { rel: string; reason: string }[] = [];
156
+
157
+ for (const rel of normRels) {
158
+ const vaultPath = path.join(vaultRoot, rel);
159
+ const target = path.join(dshHome, rel);
160
+ try {
161
+ if (!(await fs.exists(vaultPath))) {
162
+ missing.push(rel);
163
+ continue;
164
+ }
165
+ if (await fs.exists(target)) {
166
+ skipped.push({ rel, reason: 'targetExists' });
167
+ continue;
168
+ }
169
+ await fs.copy(vaultPath, target);
170
+ restored.push(rel);
171
+ } catch (err) {
172
+ skipped.push({ rel, reason: err instanceof Error ? err.message : String(err) });
173
+ }
174
+ }
175
+
176
+ return { restored, missing, skipped };
177
+ }
178
+
179
+ /**
180
+ * 列出 vault 内容(镜像结构 = 相对 $DSH_HOME 的 rel 平铺在 vault 根下,
181
+ * 例如 vault/.credentials.yaml 即 $DSH_HOME/.credentials.yaml 的镜像)。
182
+ * vault 不存在 / 为空时返回空数组。
183
+ */
184
+ export async function listVault(
185
+ fs: FileSystemFacade,
186
+ dataDir: string,
187
+ dshHome: string,
188
+ ): Promise<VaultEntry[]> {
189
+ const vaultRoot = vaultRootOf(dataDir);
190
+ const homeRels = await fs.listRecursive(vaultRoot); // home-relative;vault 不存在时宿主返回 []
191
+ const entries: VaultEntry[] = [];
192
+ for (const homeRel of homeRels) {
193
+ const rel = normalizePath(path.relative(vaultRoot, path.join(dshHome, homeRel)));
194
+ if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) continue; // 防御
195
+ entries.push({ rel, vaultPath: path.join(vaultRoot, rel) });
196
+ }
197
+ return entries.sort((a, b) => (a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0));
198
+ }
@@ -0,0 +1,180 @@
1
+ /**
2
+ * ZIP 解压安全(规范 §19 / 设计 §9 / core utils/zip.ts 的强化层)。
3
+ *
4
+ * 分工(避免与 core 重复):
5
+ * - core `utils/zip.ts` 已实现:条目名 isPathSafe、条目数/压缩体积/解压体积/单条/压缩比限额、
6
+ * CRC32 与尺寸校验、ZipArchive.readEntry 预算、safeExtract 基础解压。
7
+ * - 本模块在其上强化 core 未覆盖的攻击面:
8
+ * 1. **symlink 条目拒绝**:解析中央目录 external attrs 的 Unix mode(S_IFLNK 0xA000)→ 拒绝;
9
+ * 2. **重复条目名拒绝**:同名条目(含目录/文件互撞)→ 拒绝(防读取歧义攻击);
10
+ * 3. **安全解压强化**:解压后逐条 lstat 复查「必须是普通文件」,任何异常 → 中止并**完整清理**目标目录;
11
+ * 4. **可执行文件告警**:扩展名黑名单 → warnings(本插件从不执行 ZIP 内文件)。
12
+ *
13
+ * 与 core 注入点对齐:`parseZipHardened` / `createHardenedZipParser` 满足
14
+ * `ImporterOptions.parseZipOverride` 签名 `(buf, limits?) => ZipArchive`。
15
+ */
16
+ import fs from 'node:fs/promises';
17
+ import path from 'node:path';
18
+ import { isPathSafe, isSameOrChild } from '../utils/paths.ts';
19
+ import {
20
+ DEFAULT_ZIP_SAFETY_LIMITS,
21
+ ZipArchive,
22
+ ZipSafetyError,
23
+ type ZipEntryMeta,
24
+ type ZipSafetyLimits,
25
+ } from '../utils/zip.ts';
26
+
27
+ export { isPathSafe } from '../utils/paths.ts';
28
+
29
+ /* ---------------- 常量 ---------------- */
30
+
31
+ const EOCD_SIG = Buffer.from([0x50, 0x4b, 0x05, 0x06]);
32
+ const CENTRAL_SIG = 0x02014b50;
33
+ /** Unix 文件类型掩码(external attrs 高 16 位) */
34
+ const S_IFMT = 0xf000;
35
+ /** 符号链接 */
36
+ const S_IFLNK = 0xa000;
37
+
38
+ /** 可执行文件扩展名黑名单(§19.6:只警告,本插件不执行任何脚本) */
39
+ export const EXECUTABLE_EXTENSIONS = new Set([
40
+ '.exe', '.bat', '.cmd', '.sh', '.ps1', '.dll', '.so', '.dylib', '.bin', '.jar',
41
+ ]);
42
+
43
+ /** 条目名是否疑似可执行文件(按扩展名) */
44
+ export function isExecutableName(name: string): boolean {
45
+ const ext = name.slice(name.lastIndexOf('.')).toLowerCase();
46
+ return EXECUTABLE_EXTENSIONS.has(ext);
47
+ }
48
+
49
+ /* ---------------- 强化解析(parseZipOverride 注入点) ---------------- */
50
+
51
+ /**
52
+ * 强化版 ZIP 解析:在 core 全部安全校验(条目名/条目数/压缩体积)之上,
53
+ * 增加 symlink 拒绝与重复条目名拒绝。返回 core `ZipArchive`(解压校验完全复用)。
54
+ */
55
+ export function parseZipHardened(buf: Uint8Array, limits: ZipSafetyLimits = {}): ZipArchive {
56
+ const merged: Required<ZipSafetyLimits> = { ...DEFAULT_ZIP_SAFETY_LIMITS, ...limits };
57
+ const b = Buffer.isBuffer(buf) ? buf : Buffer.from(buf);
58
+ if (b.length < 22) throw new ZipSafetyError('不是合法的 ZIP 文件(体积过小)');
59
+
60
+ const eocdIdx = b.lastIndexOf(EOCD_SIG);
61
+ if (eocdIdx < 0 || eocdIdx + 22 > b.length) {
62
+ throw new ZipSafetyError('不是合法的 ZIP 文件(缺少中央目录结束记录)');
63
+ }
64
+ const totalEntries = b.readUInt16LE(eocdIdx + 10);
65
+ const cdSize = b.readUInt32LE(eocdIdx + 12);
66
+ const cdOffset = b.readUInt32LE(eocdIdx + 16);
67
+ if (cdOffset + cdSize > b.length) throw new ZipSafetyError('中央目录越界(ZIP 损坏)');
68
+ if (totalEntries > merged.maxEntries) {
69
+ throw new ZipSafetyError(`ZIP 条目数 ${totalEntries} 超过上限 ${merged.maxEntries}`);
70
+ }
71
+
72
+ const metas: ZipEntryMeta[] = [];
73
+ const seenNames = new Set<string>();
74
+ let pos = cdOffset;
75
+ let compressedTotal = 0;
76
+ for (let i = 0; i < totalEntries; i++) {
77
+ if (pos + 46 > cdOffset + cdSize) throw new ZipSafetyError('中央目录条目越界(ZIP 损坏)');
78
+ if (b.readUInt32LE(pos) !== CENTRAL_SIG) throw new ZipSafetyError('中央目录签名损坏');
79
+
80
+ const method = b.readUInt16LE(pos + 10);
81
+ const crc = b.readUInt32LE(pos + 16);
82
+ const compSize = b.readUInt32LE(pos + 20);
83
+ const uncompSize = b.readUInt32LE(pos + 24);
84
+ const nameLen = b.readUInt16LE(pos + 28);
85
+ const extraLen = b.readUInt16LE(pos + 30);
86
+ const commentLen = b.readUInt16LE(pos + 32);
87
+ const externalAttrs = b.readUInt32LE(pos + 38);
88
+ const localOffset = b.readUInt32LE(pos + 42);
89
+ const name = b.subarray(pos + 46, pos + 46 + nameLen).toString('utf8');
90
+
91
+ // —— 强化检查 ——
92
+ if (!isPathSafe(name)) throw new ZipSafetyError(`ZIP 条目名不安全: ${name}`);
93
+ if (seenNames.has(name)) throw new ZipSafetyError(`ZIP 条目名重复: ${name}`);
94
+ seenNames.add(name);
95
+ const mode = (externalAttrs >>> 16) & S_IFMT;
96
+ if (mode === S_IFLNK) {
97
+ throw new ZipSafetyError(`ZIP 含符号链接条目,已拒绝: ${name}`);
98
+ }
99
+ if (localOffset + 30 > b.length) {
100
+ throw new ZipSafetyError(`条目 "${name}" 本地文件头越界`);
101
+ }
102
+
103
+ compressedTotal += compSize;
104
+ if (compressedTotal > merged.maxCompressedBytes) {
105
+ throw new ZipSafetyError('ZIP 压缩数据总量超过上限');
106
+ }
107
+ metas.push({
108
+ name,
109
+ method,
110
+ compressedSize: compSize,
111
+ uncompressedSize: uncompSize,
112
+ crc32: crc,
113
+ isDirectory: name.endsWith('/'),
114
+ localOffset,
115
+ });
116
+ pos += 46 + nameLen + extraLen + commentLen;
117
+ }
118
+ return new ZipArchive(b, metas, merged);
119
+ }
120
+
121
+ /** 工厂:带默认限额的强化解析器(对齐 ImporterOptions.parseZipOverride 签名) */
122
+ export function createHardenedZipParser(defaultLimits?: ZipSafetyLimits) {
123
+ return (buf: Uint8Array, limits?: ZipSafetyLimits): ZipArchive =>
124
+ parseZipHardened(buf, { ...defaultLimits, ...limits });
125
+ }
126
+
127
+ /* ---------------- 强化安全解压(m5 导入文件类分区用) ---------------- */
128
+
129
+ export interface SafeExtractResult {
130
+ /** 已解压条目的 ZIP 相对路径(正斜杠) */
131
+ files: string[];
132
+ /** 非阻塞告警(可执行文件条目等) */
133
+ warnings: string[];
134
+ }
135
+
136
+ /**
137
+ * 安全解压到受控目录(对齐 core safeExtract 语义 + 强化):
138
+ * - 条目名/限额/symlink/重复名检查(parseZipHardened);
139
+ * - 逐条 CRC32/尺寸/预算校验(ZipArchive.readEntry);
140
+ * - 解压后 lstat 复查「全部产物必须是普通文件」(防符号链接/非常规写入);
141
+ * - 任何异常 → 中止并**完整清理** destDir 后抛出(不残留部分落盘)。
142
+ */
143
+ export async function safeExtractHardened(
144
+ zipPath: string,
145
+ destDir: string,
146
+ limits: ZipSafetyLimits = {},
147
+ ): Promise<SafeExtractResult> {
148
+ const data = await fs.readFile(zipPath);
149
+ const archive = parseZipHardened(data, limits);
150
+ const extracted: string[] = [];
151
+ const warnings: string[] = [];
152
+
153
+ try {
154
+ for (const meta of archive.entries()) {
155
+ if (meta.isDirectory) continue;
156
+ const target = path.join(destDir, ...meta.name.split('/'));
157
+ if (!isSameOrChild(target, destDir)) {
158
+ throw new ZipSafetyError(`解压目标越界: ${meta.name}`);
159
+ }
160
+ await fs.mkdir(path.dirname(target), { recursive: true });
161
+ await fs.writeFile(target, archive.readEntry(meta.name));
162
+ extracted.push(meta.name);
163
+ if (isExecutableName(meta.name)) {
164
+ warnings.push(`条目 "${meta.name}" 是潜在可执行文件,本插件不会执行它`);
165
+ }
166
+ }
167
+ // 解压后复查:全部产物必须是普通文件(防 symlink 逃逸 / 非常规写入)
168
+ for (const rel of extracted) {
169
+ const p = path.join(destDir, ...rel.split('/'));
170
+ const st = await fs.lstat(p);
171
+ if (!st.isFile()) {
172
+ throw new ZipSafetyError(`解压产物不是普通文件: ${rel}`);
173
+ }
174
+ }
175
+ return { files: extracted, warnings };
176
+ } catch (err) {
177
+ await fs.rm(destDir, { recursive: true, force: true });
178
+ throw err;
179
+ }
180
+ }