@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,109 @@
1
+ /**
2
+ * m-self:插件自身 UI 偏好持久化(ui-prefs.json)。
3
+ *
4
+ * 与 sync-config.json / sync-selection.json 并列独立文件:语义清楚、schema 演进独立。
5
+ * 当前存 lastSyncChannel(用户上次选择的同步通道 git/webdav)+ Star 引导弹窗状态
6
+ * (starPromptFirstSeenAt / starPromptDismissed / starPromptClicked,见 src/ui/star-prompt.ts)。
7
+ *
8
+ * 背景(self 分区设计):此前该偏好只存浏览器 localStorage(键
9
+ * dsh.configManager.syncChannel),换浏览器/换机器即丢失,且 Host 进程读不到
10
+ * (自动同步在浏览器关闭时也运行)。迁移到磁盘后:
11
+ * - Host 侧可读可写(status 响应回填、POST /sync/ui-prefs 保存);
12
+ * - 随 self 分区进入导出备份,迁移到新机器时恢复;
13
+ * - localStorage 仅保留为前端同步读取的降级通道(status 未带回填时的兜底)。
14
+ *
15
+ * 字段 { schemaVersion, lastSyncChannel?: 'git' | 'webdav',
16
+ * starPromptFirstSeenAt?: number, starPromptDismissed?: boolean,
17
+ * starPromptClicked?: boolean }:
18
+ * - 缺省/未配置 = undefined(UI 回退到 sync-config.transport / 首次进入页面);
19
+ * - 原子写(临时文件 + rename),损坏/不支持 schema 回退缺省;
20
+ * - 多端点共写同一文件:一律经 updateUiPrefs(read → merge → write)原子更新,
21
+ * 避免「sync/ui-prefs 写 lastSyncChannel」与「star-prompt 写弹窗状态」互相覆盖。
22
+ */
23
+ import fs from 'node:fs/promises';
24
+ import path from 'node:path';
25
+ import crypto from 'node:crypto';
26
+ import { parseJsonSafe, stringifyJsonSafe } from '../utils/json.js';
27
+ import { atomicWriteFile } from '../utils/atomic-write.js';
28
+ export const UI_PREFS_FILE = 'ui-prefs.json';
29
+ export const UI_PREFS_SCHEMA_VERSION = 1;
30
+ /** 缺省配置(首次无文件 / 损坏 / 不支持 schema 时回退) */
31
+ export function defaultUiPrefs() {
32
+ return { schemaVersion: UI_PREFS_SCHEMA_VERSION };
33
+ }
34
+ /** 读取 UI 偏好;文件不存在 / 损坏 / 不支持 schema → 缺省值(不抛错)。 */
35
+ export async function readUiPrefs(dir) {
36
+ const file = path.join(dir, UI_PREFS_FILE);
37
+ let raw;
38
+ try {
39
+ raw = await fs.readFile(file, 'utf8');
40
+ }
41
+ catch {
42
+ return defaultUiPrefs();
43
+ }
44
+ let parsed;
45
+ try {
46
+ parsed = parseJsonSafe(raw);
47
+ }
48
+ catch {
49
+ return defaultUiPrefs();
50
+ }
51
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
52
+ return defaultUiPrefs();
53
+ }
54
+ const obj = parsed;
55
+ if (obj['schemaVersion'] !== undefined && obj['schemaVersion'] !== UI_PREFS_SCHEMA_VERSION) {
56
+ return defaultUiPrefs();
57
+ }
58
+ if (obj['schemaVersion'] === undefined) {
59
+ return defaultUiPrefs();
60
+ }
61
+ const prefs = defaultUiPrefs();
62
+ if (obj['lastSyncChannel'] === 'git' || obj['lastSyncChannel'] === 'webdav') {
63
+ prefs.lastSyncChannel = obj['lastSyncChannel'];
64
+ }
65
+ if (typeof obj['starPromptFirstSeenAt'] === 'number' && Number.isFinite(obj['starPromptFirstSeenAt'])) {
66
+ prefs.starPromptFirstSeenAt = obj['starPromptFirstSeenAt'];
67
+ }
68
+ if (obj['starPromptDismissed'] === true) {
69
+ prefs.starPromptDismissed = true;
70
+ }
71
+ if (obj['starPromptClicked'] === true) {
72
+ prefs.starPromptClicked = true;
73
+ }
74
+ if (typeof obj['releaseNotesLastSeenVersion'] === 'string' && obj['releaseNotesLastSeenVersion'].trim().length > 0) {
75
+ prefs.releaseNotesLastSeenVersion = obj['releaseNotesLastSeenVersion'].trim();
76
+ }
77
+ if (obj['releaseNotesDismissed'] === true) {
78
+ prefs.releaseNotesDismissed = true;
79
+ }
80
+ return prefs;
81
+ }
82
+ /** 写入 UI 偏好(原子写:临时文件 + rename;自动创建目录)。 */
83
+ export async function writeUiPrefs(dir, prefs) {
84
+ await fs.mkdir(dir, { recursive: true });
85
+ const payload = {
86
+ schemaVersion: UI_PREFS_SCHEMA_VERSION,
87
+ ...(prefs.lastSyncChannel !== undefined ? { lastSyncChannel: prefs.lastSyncChannel } : {}),
88
+ ...(prefs.starPromptFirstSeenAt !== undefined ? { starPromptFirstSeenAt: prefs.starPromptFirstSeenAt } : {}),
89
+ ...(prefs.starPromptDismissed === true ? { starPromptDismissed: true } : {}),
90
+ ...(prefs.starPromptClicked === true ? { starPromptClicked: true } : {}),
91
+ ...(prefs.releaseNotesLastSeenVersion !== undefined ? { releaseNotesLastSeenVersion: prefs.releaseNotesLastSeenVersion } : {}),
92
+ ...(prefs.releaseNotesDismissed === true ? { releaseNotesDismissed: true } : {}),
93
+ };
94
+ const target = path.join(dir, UI_PREFS_FILE);
95
+ const data = stringifyJsonSafe(payload, { space: 2 });
96
+ await atomicWriteFile(target, data, { mode: 0o600 });
97
+ }
98
+ /**
99
+ * 局部原子更新:read → merge → write。
100
+ * 多端点共写 ui-prefs.json 时(sync/ui-prefs 与 star-prompt),任一端点写全量对象
101
+ * 都会丢掉另一端点刚写入的字段;统一走本函数按补丁合并,杜绝互相覆盖。
102
+ */
103
+ export async function updateUiPrefs(dir, patch) {
104
+ const current = await readUiPrefs(dir);
105
+ const next = { ...current, ...patch };
106
+ await writeUiPrefs(dir, next);
107
+ return next;
108
+ }
109
+ //# sourceMappingURL=ui-prefs.js.map
@@ -0,0 +1,77 @@
1
+ import type { MsgFunc } from '../../core/messages.ts';
2
+ import type { SyncSnapshot, SyncSnapshotMeta, SyncTransport } from '../transport.ts';
3
+ /** 请求选项:headers / body / 覆盖默认超时(ms;0 = 不超时) */
4
+ export interface WebDavRequestOptions {
5
+ headers?: Record<string, string>;
6
+ body?: string;
7
+ timeoutMs?: number;
8
+ }
9
+ /** 请求响应最小形状(兼容 fetch Response 的 status/ok/text();headers 供重定向读 Location) */
10
+ export interface WebDavResponse {
11
+ readonly status: number;
12
+ readonly ok: boolean;
13
+ /** 响应头(可选:注入的 mock 可省略,默认 request 总会带上,重定向跟随需要 location) */
14
+ readonly headers?: Record<string, string>;
15
+ text(): Promise<string>;
16
+ }
17
+ /** 可注入的请求函数(测试 mock 用);method/url/options */
18
+ export type WebDavRequestFn = (method: string, url: string, options?: WebDavRequestOptions) => Promise<WebDavResponse>;
19
+ /** 凭据提供者:password 只从这里读取,绝不落盘/进日志 */
20
+ export interface WebDavCredentialProvider {
21
+ getPassword(): Promise<string>;
22
+ }
23
+ export interface WebDavTransportOptions {
24
+ /** WebDAV 远端根 URL(http/https),如 https://dav.example.com/dav/config */
25
+ baseUrl: string;
26
+ /** HTTP Basic 认证用户名 */
27
+ username: string;
28
+ /** 密码提供者(HTTP Basic 密码) */
29
+ credentials: WebDavCredentialProvider;
30
+ /** 可注入 request(测试 mock 用);缺省 = 全局 fetch + AbortController 超时 */
31
+ request?: WebDavRequestFn;
32
+ /** 单请求超时 ms,默认 120000(慢速 WebDAV 上传大快照需要宽裕窗口;0 = 不超时) */
33
+ timeoutMs?: number;
34
+ /** 消息翻译器(缺省 zh) */
35
+ msg?: MsgFunc;
36
+ }
37
+ export declare class WebDavTransportError extends Error {
38
+ constructor(message: string);
39
+ }
40
+ /** 实现 SyncTransport 的 WebDAV 通道。所有网络操作经 request 注入(缺省 defaultRequest,自动跟随重定向)。 */
41
+ export declare class WebDavTransport implements SyncTransport {
42
+ readonly type = "webdav";
43
+ private readonly o;
44
+ constructor(options: WebDavTransportOptions);
45
+ /** 列出远端已有快照(按 createdAt 升序)。index.json 缺失视为空。 */
46
+ list(): Promise<SyncSnapshotMeta[]>;
47
+ /** 上传快照:幂等 MKCOL → 快照级跳过判定(同 id 且内容全等则免上传)→ PUT <id>.json
48
+ * → 合并写回 index.json;返回远端 meta(跳过时)或 computeSnapshotMeta。
49
+ * 序列化经 snapshot-json:文件类分区字节以 base64 传输(JSON 无法直传 Uint8Array)。 */
50
+ upload(snapshot: SyncSnapshot): Promise<SyncSnapshotMeta>;
51
+ /** 下载快照完整载荷。不存在的 id 必须抛错(契约)。 */
52
+ download(id: string): Promise<SyncSnapshot>;
53
+ /** 删除远端快照并从 index 摘除(写回合并后的 index);文件不存在视为成功。 */
54
+ delete(id: string): Promise<void>;
55
+ private validateOptions;
56
+ private assertSafeId;
57
+ private snapshotsBase;
58
+ private snapshotsColUrl;
59
+ private snapshotUrl;
60
+ private indexUrl;
61
+ /** 读取一次 password(错误脱敏用) */
62
+ private passwordOnce;
63
+ /** 幂等创建 snapshots 集合:MKCOL;已存在(405/2xx/3xx)视为成功。 */
64
+ private ensureCollection;
65
+ /** 读 index(缺失 → [];非法 → 抛错)。 */
66
+ private readIndex;
67
+ private parseIndex;
68
+ private parseSnapshot;
69
+ /** 发送请求:注入 Basic 认证头 + 调用 request;网络错误/超时归一 */
70
+ private send;
71
+ /** 构造 HTTP 非 2xx 失败消息:附上脱敏后的响应体片段(截断) */
72
+ private failText;
73
+ /** 识别超时错误(AbortController 抛出的 AbortError / DOMException timeout / TimeoutError) */
74
+ private isTimeout;
75
+ /** 错误消息脱敏:password(原文与 URL 编码形态)一律替换 */
76
+ private mask;
77
+ }
@@ -0,0 +1,451 @@
1
+ /**
2
+ * m-webdav-channel:WebDAV 通道(SyncTransport 的 webdav 实现)。
3
+ *
4
+ * 远端布局(单文件 JSON 快照 + 索引):
5
+ * <base>/dsh-config-manager/<id>.json —— 单个快照的完整载荷(SyncSnapshot 序列化)
6
+ * <base>/dsh-config-manager/index.json —— 索引(SyncSnapshotMeta 数组,每个 id 一条)
7
+ *
8
+ * 设计:
9
+ * - upload:先幂等 MKCOL snapshots 集合,再读 index 做「快照级跳过」判定
10
+ * (同 id 条目且 sections hash 全等 → 内容未变,跳过 PUT 直接返回远端 meta);
11
+ * 否则 PUT <id>.json(先快照文件)→ 合并(保留其它 id、覆盖同 id)→ PUT 写回 index
12
+ * (meta 最后落盘:快照文件成功后才写索引)。
13
+ * - list:GET index.json,缺失(404)视为空;按 createdAt 升序返回。
14
+ * - download:GET <id>.json 解析成 SyncSnapshot;不存在必须抛错(契约)。
15
+ * - delete:DELETE <id>.json 并从 index 摘除条目(写回合并后 index);文件不存在视为成功。
16
+ * - 二进制安全:快照序列化经 snapshot-json(文件分区 Uint8Array → base64 标记对象),
17
+ * JSON 往返字节无损(否则 JSON.stringify 把 TypedArray 变成数字索引对象,拉取还原
18
+ * 成普通对象 → Buffer.from(对象) 报错)。
19
+ * - 认证:HTTP Basic(username 配置项 + 注入 credentials 提供者 getPassword())。
20
+ * 密码绝不进 URL/日志;错误消息中的响应体统一脱敏(password → [REDACTED])。
21
+ * - 重定向:默认 request 自动跟随 301/302/303/307/308(网盘 WebDAV 会把下载 GET 302 到
22
+ * 带时效签名的 CDN 直链,如 123pan;不自建反代返回 301 也一样)。303 且非 GET/HEAD 时
23
+ * 降级为 GET 并丢弃请求体;跨源跳转剥离 Authorization(CDN 直链是预签名 URL,不得把
24
+ * Basic 凭据转发给第三方域);上限 5 跳,超出抛错。无 Location / Location 非法的 3xx
25
+ * 按最终响应返回(上层如实报 HTTP 状态失败)。
26
+ * - 可注入 request 便于测试;注入实现负责自己的重定向语义(默认实现才自动跟随)。
27
+ */
28
+ import { requestOnce } from '../../utils/proxy.js';
29
+ import { zhMsg } from '../../core/messages.js';
30
+ import { deserializeSnapshot, serializeSnapshot } from '../snapshot-json.js';
31
+ import { computeSnapshotMeta, sectionsEqual } from '../transport.js';
32
+ import { parseJsonSafe } from '../../utils/json.js';
33
+ /** 快照 id 安全字符集:字母数字开头,仅 . _ -;防路径穿越与 URL 注入 */
34
+ const SAFE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
35
+ /** 保留 id:与 index.json 冲突(id 'index' 会占用索引文件路径) */
36
+ const RESERVED_IDS = new Set(['index']);
37
+ /**
38
+ * 默认单请求超时(ms)。WebDAV 上传大快照(含多个分区配置)与读写索引在慢速
39
+ * 服务器(如坚果云限速、自建 NAS)下较慢,30s 常不够 → 提高至 120s;
40
+ * 业务侧(makeSyncEngine)还会显式传 timeoutMs 覆盖默认值。
41
+ */
42
+ const DEFAULT_TIMEOUT_MS = 120_000;
43
+ /** 错误消息里截取的响应体最大长度(防超大/二进制响应撑爆消息) */
44
+ const ERR_BODY_MAX = 500;
45
+ const SNAPSHOTS_SEG = 'dsh-config-manager';
46
+ const INDEX_FILE = 'index.json';
47
+ const REDACTED = '[REDACTED]';
48
+ /** 自动跟随的重定向状态码(RFC 7231/9110) */
49
+ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
50
+ /** 重定向最大跳数(RFC 7231 建议 ≤5;防 302 循环拖死请求) */
51
+ const MAX_REDIRECTS = 5;
52
+ export class WebDavTransportError extends Error {
53
+ constructor(message) {
54
+ super(message);
55
+ this.name = 'WebDavTransportError';
56
+ }
57
+ }
58
+ /** 默认请求实现:node:https/http 原生流式请求(支持全部 WebDAV 方法、准确 Content-Length 与 User-Agent)。
59
+ * 自动跟随 301/302/303/307/308 重定向(网盘 WebDAV 的 GET/PUT 会 302 到 CDN 预签名直链):
60
+ * - 301/302/307/308 保持原方法与请求体;303 且非 GET/HEAD 降级为 GET 并丢弃请求体;
61
+ * - 相对 Location 用当前 URL 解析;跨源跳转剥离 Authorization(预签名 URL 不应收到 Basic 凭据);
62
+ * - 上限 MAX_REDIRECTS 跳,超出抛错;无 Location / Location 非法的 3xx 按最终响应返回。 */
63
+ const defaultRequest = async (method, url, options = {}) => {
64
+ let currentMethod = method;
65
+ let currentUrl = url;
66
+ let currentHeaders = { ...(options.headers ?? {}) };
67
+ let currentBody = options.body;
68
+ for (let redirects = 0;;) {
69
+ const res = await rawRequest(currentMethod, currentUrl, {
70
+ ...options,
71
+ headers: currentHeaders,
72
+ body: currentBody,
73
+ });
74
+ const status = res.status;
75
+ if (!REDIRECT_STATUSES.has(status))
76
+ return res;
77
+ // 3xx 但没有 Location(或 Location 非法)→ 无法跟随,作为最终响应返回(上层如实报 HTTP 状态)
78
+ const rawLocation = res.headers?.location;
79
+ const location = Array.isArray(rawLocation) ? rawLocation[0] : rawLocation;
80
+ if (!location)
81
+ return res;
82
+ let nextUrl;
83
+ try {
84
+ nextUrl = new URL(location, currentUrl).toString();
85
+ }
86
+ catch {
87
+ return res;
88
+ }
89
+ if (redirects >= MAX_REDIRECTS) {
90
+ const err = new Error(`Too many redirects (${MAX_REDIRECTS} max) for ${url}`);
91
+ err.name = 'RedirectError';
92
+ throw err;
93
+ }
94
+ redirects += 1;
95
+ // 303:非 GET/HEAD 降级 GET 并丢弃请求体(303 See Other 语义)
96
+ let nextMethod = currentMethod;
97
+ let nextHeaders = currentHeaders;
98
+ let nextBody = currentBody;
99
+ if (status === 303 && currentMethod !== 'GET' && currentMethod !== 'HEAD') {
100
+ nextMethod = 'GET';
101
+ nextBody = undefined;
102
+ }
103
+ // 跨源:剥离 Authorization(同源保留,服务器可能按 Basic 认证续用)
104
+ if (!sameOrigin(currentUrl, nextUrl)) {
105
+ nextHeaders = { ...currentHeaders };
106
+ delete nextHeaders.Authorization;
107
+ }
108
+ currentMethod = nextMethod;
109
+ currentUrl = nextUrl;
110
+ currentHeaders = nextHeaders;
111
+ currentBody = nextBody;
112
+ }
113
+ };
114
+ /** 单次裸请求(不跟随重定向)。
115
+ *
116
+ * 实现已统一到 `utils/proxy.requestOnce`(issue #30 ②级方案):同一份实现同时服务 WebDAV 与
117
+ * GitHub 出站,并在此处获得**插件私有**的代理能力(HTTP 代理 absolute-form / HTTPS 代理 CONNECT
118
+ * 隧道 + TLS),且不改动任何全局状态。未配置代理时行为与原先的 node:http 直连完全一致。
119
+ */
120
+ async function rawRequest(method, url, options) {
121
+ const timeoutMs = options.timeoutMs ?? 0;
122
+ const headers = {
123
+ 'User-Agent': 'DSH-Config-Manager/0.1.55 (WebDAV Client)',
124
+ ...(options.headers ?? {}),
125
+ };
126
+ if (options.body === undefined) {
127
+ // 303 降级后已丢弃 body → 清除残留的 Content-Length,避免 GET 带错误长度头
128
+ delete headers['Content-Length'];
129
+ delete headers['content-length'];
130
+ }
131
+ let res;
132
+ try {
133
+ res = await requestOnce({
134
+ method,
135
+ url,
136
+ headers,
137
+ ...(options.body !== undefined ? { body: options.body } : {}),
138
+ ...(timeoutMs > 0 ? { timeoutMs } : {}),
139
+ });
140
+ }
141
+ catch (err) {
142
+ // 超时错误名保持 'TimeoutError'(上层 isTimeout 依赖它做文案归一)
143
+ if (err instanceof Error && /timed out/i.test(err.message) && err.name !== 'TimeoutError') {
144
+ const timeoutError = new Error(err.message);
145
+ timeoutError.name = 'TimeoutError';
146
+ throw timeoutError;
147
+ }
148
+ throw err;
149
+ }
150
+ const responseHeaders = {};
151
+ res.headers.forEach((value, key) => { responseHeaders[key] = value; });
152
+ return {
153
+ status: res.status,
154
+ ok: res.status >= 200 && res.status < 300,
155
+ headers: responseHeaders,
156
+ text: async () => res.body.toString('utf8'),
157
+ };
158
+ }
159
+ /** 同源判断(协议 + host,host 含端口;子域名不同视为跨源) */
160
+ function sameOrigin(a, b) {
161
+ try {
162
+ const ua = new URL(a);
163
+ const ub = new URL(b);
164
+ return ua.protocol === ub.protocol && ua.host === ub.host;
165
+ }
166
+ catch {
167
+ return false;
168
+ }
169
+ }
170
+ /** 实现 SyncTransport 的 WebDAV 通道。所有网络操作经 request 注入(缺省 defaultRequest,自动跟随重定向)。 */
171
+ export class WebDavTransport {
172
+ type = 'webdav';
173
+ o;
174
+ constructor(options) {
175
+ this.o = {
176
+ baseUrl: '',
177
+ username: '',
178
+ credentials: { getPassword: async () => '' },
179
+ request: defaultRequest,
180
+ timeoutMs: DEFAULT_TIMEOUT_MS,
181
+ msg: zhMsg,
182
+ };
183
+ this.validateOptions(options);
184
+ this.o.baseUrl = options.baseUrl;
185
+ this.o.username = options.username;
186
+ this.o.credentials = options.credentials;
187
+ if (options.request !== undefined)
188
+ this.o.request = options.request;
189
+ if (options.timeoutMs !== undefined)
190
+ this.o.timeoutMs = options.timeoutMs;
191
+ if (options.msg !== undefined)
192
+ this.o.msg = options.msg;
193
+ }
194
+ /** 列出远端已有快照(按 createdAt 升序)。index.json 缺失视为空。 */
195
+ async list() {
196
+ const pwd = await this.passwordOnce();
197
+ const url = this.indexUrl();
198
+ const res = await this.send('GET', url, pwd);
199
+ if (res.status === 404)
200
+ return []; // 缺失视为空
201
+ if (!res.ok) {
202
+ throw new WebDavTransportError(await this.failText('GET', url, res, pwd));
203
+ }
204
+ return this.parseIndex(await res.text(), url, pwd);
205
+ }
206
+ /** 上传快照:幂等 MKCOL → 快照级跳过判定(同 id 且内容全等则免上传)→ PUT <id>.json
207
+ * → 合并写回 index.json;返回远端 meta(跳过时)或 computeSnapshotMeta。
208
+ * 序列化经 snapshot-json:文件类分区字节以 base64 传输(JSON 无法直传 Uint8Array)。 */
209
+ async upload(snapshot) {
210
+ this.assertSafeId(snapshot.id);
211
+ const pwd = await this.passwordOnce();
212
+ await this.ensureCollection(pwd);
213
+ // 快照级跳过(增量优化):先读远端 index,同 id 条目与本地内容全等(sections hash 全等)
214
+ // → 内容未变,直接返回远端 meta,跳过 PUT 快照文件与 index 写回(幂等契约不变)。
215
+ // 加密快照(computeSnapshotMeta.sections 为空对象)经 sectionsEqual 判定为「无法比较」
216
+ // → 必须照常上传,绝不跳过。
217
+ const meta = computeSnapshotMeta(snapshot);
218
+ const idxBefore = await this.readIndex(pwd);
219
+ const existing = idxBefore.find((m) => m.id === snapshot.id);
220
+ if (existing !== undefined && sectionsEqual(existing, meta)) {
221
+ return existing;
222
+ }
223
+ // 先写快照文件(JSON 序列化:文件字节 base64 编码,往返无损)
224
+ const snapUrl = this.snapshotUrl(snapshot.id);
225
+ const snapRes = await this.send('PUT', snapUrl, pwd, { body: serializeSnapshot(snapshot) });
226
+ if (!snapRes.ok) {
227
+ throw new WebDavTransportError(await this.failText('PUT', snapUrl, snapRes, pwd));
228
+ }
229
+ // 再写合并后的 index(保留其它 id、覆盖同 id)—— meta 最后落盘
230
+ const idxUrl = this.indexUrl();
231
+ const merged = idxBefore.filter((m) => m.id !== snapshot.id);
232
+ merged.push(meta);
233
+ merged.sort((a, b) => (a.createdAt < b.createdAt ? -1 : a.createdAt > b.createdAt ? 1 : 0));
234
+ const putIdx = await this.send('PUT', idxUrl, pwd, { body: JSON.stringify(merged) });
235
+ if (!putIdx.ok) {
236
+ throw new WebDavTransportError(await this.failText('PUT', idxUrl, putIdx, pwd));
237
+ }
238
+ return meta;
239
+ }
240
+ /** 下载快照完整载荷。不存在的 id 必须抛错(契约)。 */
241
+ async download(id) {
242
+ this.assertSafeId(id);
243
+ const pwd = await this.passwordOnce();
244
+ const url = this.snapshotUrl(id);
245
+ const res = await this.send('GET', url, pwd);
246
+ if (res.status === 404) {
247
+ throw new WebDavTransportError(this.o.msg('sync.webdav.snapshotMissing', { id, url }));
248
+ }
249
+ if (!res.ok) {
250
+ throw new WebDavTransportError(await this.failText('GET', url, res, pwd));
251
+ }
252
+ return this.parseSnapshot(await res.text(), id);
253
+ }
254
+ /** 删除远端快照并从 index 摘除(写回合并后的 index);文件不存在视为成功。 */
255
+ async delete(id) {
256
+ this.assertSafeId(id);
257
+ const pwd = await this.passwordOnce();
258
+ const idxUrl = this.indexUrl();
259
+ // 先读现有 index(不存在 → 空),以便摘除条目
260
+ let idx = [];
261
+ try {
262
+ idx = await this.readIndex(pwd);
263
+ }
264
+ catch {
265
+ idx = []; // index 缺失/损坏时按无条目处理(不阻塞删除)
266
+ }
267
+ // delete 快照文件:404 = 不存在,视为成功
268
+ const url = this.snapshotUrl(id);
269
+ const res = await this.send('DELETE', url, pwd);
270
+ if (!res.ok && res.status !== 404) {
271
+ throw new WebDavTransportError(await this.failText('DELETE', url, res, pwd));
272
+ }
273
+ // 若 index 中无该 id,则无需写回
274
+ const remaining = idx.filter((m) => m.id !== id);
275
+ if (remaining.length === idx.length && res.status === 404) {
276
+ return; // 文件与 index 都不存在 → 静默成功,无写回
277
+ }
278
+ const putIdx = await this.send('PUT', idxUrl, pwd, { body: JSON.stringify(remaining) });
279
+ if (!putIdx.ok) {
280
+ throw new WebDavTransportError(await this.failText('PUT', idxUrl, putIdx, pwd));
281
+ }
282
+ }
283
+ /* ---------------- 内部实现 ---------------- */
284
+ validateOptions(options) {
285
+ const msg = options.msg ?? zhMsg;
286
+ if (typeof options.baseUrl !== 'string' || options.baseUrl.trim() === '') {
287
+ throw new WebDavTransportError(msg('sync.webdav.baseUrlRequired'));
288
+ }
289
+ let parsed;
290
+ try {
291
+ parsed = new URL(options.baseUrl);
292
+ }
293
+ catch {
294
+ throw new WebDavTransportError(msg('sync.webdav.baseUrlInvalid', { url: options.baseUrl }));
295
+ }
296
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
297
+ throw new WebDavTransportError(msg('sync.webdav.baseUrlInvalid', { url: options.baseUrl }));
298
+ }
299
+ if (parsed.username !== '' || parsed.password !== '') {
300
+ throw new WebDavTransportError(msg('sync.webdav.baseUrlUserinfo'));
301
+ }
302
+ if (typeof options.username !== 'string' || options.username === '') {
303
+ throw new WebDavTransportError(msg('sync.webdav.usernameRequired'));
304
+ }
305
+ if (options.credentials === null || typeof options.credentials !== 'object'
306
+ || typeof options.credentials.getPassword !== 'function') {
307
+ throw new WebDavTransportError(msg('sync.webdav.credentialsRequired'));
308
+ }
309
+ }
310
+ assertSafeId(id) {
311
+ if (typeof id !== 'string' || !SAFE_ID_RE.test(id) || RESERVED_IDS.has(id)) {
312
+ throw new WebDavTransportError(this.o.msg('sync.webdav.invalidSnapshotId', { id: JSON.stringify(id) }));
313
+ }
314
+ }
315
+ snapshotsBase() {
316
+ return `${this.o.baseUrl.replace(/\/+$/, '')}/${SNAPSHOTS_SEG}`;
317
+ }
318
+ snapshotsColUrl() {
319
+ return `${this.snapshotsBase()}/`;
320
+ }
321
+ snapshotUrl(id) {
322
+ return `${this.snapshotsBase()}/${id}.json`;
323
+ }
324
+ indexUrl() {
325
+ return `${this.snapshotsBase()}/${INDEX_FILE}`;
326
+ }
327
+ /** 读取一次 password(错误脱敏用) */
328
+ async passwordOnce() {
329
+ try {
330
+ return await this.o.credentials.getPassword();
331
+ }
332
+ catch {
333
+ return '';
334
+ }
335
+ }
336
+ /** 幂等创建 snapshots 集合:MKCOL;已存在(405/2xx/3xx)视为成功。 */
337
+ async ensureCollection(pwd) {
338
+ const url = this.snapshotsColUrl();
339
+ const res = await this.send('MKCOL', url, pwd);
340
+ const okStatuses = new Set([200, 201, 204, 301, 302, 303, 405]);
341
+ if (okStatuses.has(res.status))
342
+ return;
343
+ throw new WebDavTransportError(await this.failText('MKCOL', url, res, pwd));
344
+ }
345
+ /** 读 index(缺失 → [];非法 → 抛错)。 */
346
+ async readIndex(pwd) {
347
+ const url = this.indexUrl();
348
+ const res = await this.send('GET', url, pwd);
349
+ if (res.status === 404)
350
+ return [];
351
+ if (!res.ok) {
352
+ throw new WebDavTransportError(await this.failText('GET', url, res, pwd));
353
+ }
354
+ return this.parseIndex(await res.text(), url, pwd);
355
+ }
356
+ parseIndex(raw, url, pwd) {
357
+ let parsed;
358
+ try {
359
+ parsed = parseJsonSafe(raw);
360
+ }
361
+ catch (err) {
362
+ throw new WebDavTransportError(this.o.msg('sync.webdav.indexInvalid', { url, err: this.mask(String(err?.message ?? ''), pwd) }));
363
+ }
364
+ if (!Array.isArray(parsed)) {
365
+ throw new WebDavTransportError(this.o.msg('sync.webdav.indexInvalid', { url, err: 'not an array' }));
366
+ }
367
+ const valid = (m) => typeof m === 'object' && m !== null
368
+ && typeof m.id === 'string'
369
+ && typeof m.createdAt === 'string'
370
+ && typeof m.manifest === 'object' && m.manifest !== null
371
+ && typeof m.sections === 'object' && m.sections !== null;
372
+ if (!parsed.every(valid)) {
373
+ throw new WebDavTransportError(this.o.msg('sync.webdav.indexInvalid', { url, err: 'invalid entry' }));
374
+ }
375
+ const metas = parsed;
376
+ metas.sort((a, b) => (a.createdAt < b.createdAt ? -1 : a.createdAt > b.createdAt ? 1 : 0));
377
+ return metas;
378
+ }
379
+ parseSnapshot(raw, id) {
380
+ try {
381
+ return deserializeSnapshot(raw);
382
+ }
383
+ catch (err) {
384
+ throw new WebDavTransportError(this.o.msg('sync.webdav.snapshotInvalid', { id, err: this.mask(String(err?.message ?? ''), '') }));
385
+ }
386
+ }
387
+ /** 发送请求:注入 Basic 认证头 + 调用 request;网络错误/超时归一 */
388
+ async send(method, url, pwd, opts = {}) {
389
+ const auth = 'Basic ' + Buffer.from(`${this.o.username}:${pwd}`, 'utf8').toString('base64');
390
+ const headers = {
391
+ Authorization: auth,
392
+ 'Content-Type': 'application/json',
393
+ 'User-Agent': 'DSH-Config-Manager/0.1.55 (WebDAV Client)',
394
+ };
395
+ if (opts.body !== undefined)
396
+ headers['Content-Length'] = String(Buffer.byteLength(opts.body, 'utf8'));
397
+ try {
398
+ return await this.o.request(method, url, { headers, body: opts.body, timeoutMs: this.o.timeoutMs });
399
+ }
400
+ catch (err) {
401
+ if (this.isTimeout(err)) {
402
+ throw new WebDavTransportError(this.o.msg('sync.webdav.timeout', { method, url, timeout: String(this.o.timeoutMs) }));
403
+ }
404
+ if (err instanceof Error && err.name === 'RedirectError') {
405
+ // 默认 request 的重定向跳数超限(如 302 循环)→ 归一为清晰消息,避免暴露内部跳转详情
406
+ throw new WebDavTransportError(this.o.msg('sync.webdav.tooManyRedirects', { method, url, n: String(MAX_REDIRECTS) }));
407
+ }
408
+ const rawMsg = err instanceof Error
409
+ ? (err.cause ? `${err.message} (${err.cause.message || err.cause})` : err.message)
410
+ : String(err);
411
+ throw new WebDavTransportError(this.o.msg('sync.webdav.requestError', { method, url, err: this.mask(rawMsg, pwd) }));
412
+ }
413
+ }
414
+ /** 构造 HTTP 非 2xx 失败消息:附上脱敏后的响应体片段(截断) */
415
+ async failText(method, url, res, pwd) {
416
+ let body = '';
417
+ try {
418
+ body = (await res.text()).slice(0, ERR_BODY_MAX);
419
+ }
420
+ catch {
421
+ body = '';
422
+ }
423
+ return this.o.msg('sync.webdav.requestFailed', {
424
+ method,
425
+ url,
426
+ status: String(res.status),
427
+ err: this.mask(body, pwd),
428
+ });
429
+ }
430
+ /** 识别超时错误(AbortController 抛出的 AbortError / DOMException timeout / TimeoutError) */
431
+ isTimeout(err) {
432
+ const e = err;
433
+ const name = e?.name ?? '';
434
+ if (name === 'TimeoutError')
435
+ return true;
436
+ if (name === 'AbortError')
437
+ return true;
438
+ if (typeof DOMException !== 'undefined' && err instanceof DOMException && name === 'AbortError')
439
+ return true;
440
+ return false;
441
+ }
442
+ /** 错误消息脱敏:password(原文与 URL 编码形态)一律替换 */
443
+ mask(text, pwd) {
444
+ if (!pwd)
445
+ return text;
446
+ let out = text.split(pwd).join(REDACTED);
447
+ out = out.split(encodeURIComponent(pwd)).join(REDACTED);
448
+ return out;
449
+ }
450
+ }
451
+ //# sourceMappingURL=webdav-transport.js.map
@@ -0,0 +1,19 @@
1
+ import { type UiT } from './i18n.ts';
2
+ /** 可操作错误(UI 渲染为 Reason / Suggested action 两段) */
3
+ export interface ActionableError {
4
+ title: string;
5
+ reason: string;
6
+ suggestedAction?: string;
7
+ /** 相关项(如 MCP 服务器名 / 插件包名);可空 */
8
+ item?: string;
9
+ /** 是否可重试(true 显示重试按钮) */
10
+ retryable: boolean;
11
+ }
12
+ /** 将任意错误转换为可操作错误(消息已脱敏,不泄漏 Secret) */
13
+ export declare function toActionableError(err: unknown, opts?: {
14
+ item?: string;
15
+ fallbackTitle?: string;
16
+ t?: UiT;
17
+ }): ActionableError;
18
+ /** 格式化为用户可读的多行文本(Reason / Suggested action) */
19
+ export declare function formatActionableError(e: ActionableError, t?: UiT): string;