dsh-config-manager 0.1.0

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 (253) hide show
  1. package/README.md +216 -0
  2. package/README.zh-CN.md +218 -0
  3. package/cordis.patch.yml +45 -0
  4. package/dsh.bundle.patch +21 -0
  5. package/dsh.client +39 -0
  6. package/lib/adapters/agent-presets.d.ts +13 -0
  7. package/lib/adapters/agent-presets.js +14 -0
  8. package/lib/adapters/agent-presets.js.map +1 -0
  9. package/lib/adapters/credentials.d.ts +36 -0
  10. package/lib/adapters/credentials.js +121 -0
  11. package/lib/adapters/credentials.js.map +1 -0
  12. package/lib/adapters/file-collection.d.ts +14 -0
  13. package/lib/adapters/file-collection.js +102 -0
  14. package/lib/adapters/file-collection.js.map +1 -0
  15. package/lib/adapters/index.d.ts +38 -0
  16. package/lib/adapters/index.js +46 -0
  17. package/lib/adapters/index.js.map +1 -0
  18. package/lib/adapters/mcp.d.ts +27 -0
  19. package/lib/adapters/mcp.js +175 -0
  20. package/lib/adapters/mcp.js.map +1 -0
  21. package/lib/adapters/plugin-files.d.ts +15 -0
  22. package/lib/adapters/plugin-files.js +91 -0
  23. package/lib/adapters/plugin-files.js.map +1 -0
  24. package/lib/adapters/plugins.d.ts +13 -0
  25. package/lib/adapters/plugins.js +177 -0
  26. package/lib/adapters/plugins.js.map +1 -0
  27. package/lib/adapters/prompts.d.ts +29 -0
  28. package/lib/adapters/prompts.js +228 -0
  29. package/lib/adapters/prompts.js.map +1 -0
  30. package/lib/adapters/providers.d.ts +33 -0
  31. package/lib/adapters/providers.js +143 -0
  32. package/lib/adapters/providers.js.map +1 -0
  33. package/lib/adapters/sessions.d.ts +14 -0
  34. package/lib/adapters/sessions.js +15 -0
  35. package/lib/adapters/sessions.js.map +1 -0
  36. package/lib/adapters/settings.d.ts +23 -0
  37. package/lib/adapters/settings.js +142 -0
  38. package/lib/adapters/settings.js.map +1 -0
  39. package/lib/adapters/skills.d.ts +13 -0
  40. package/lib/adapters/skills.js +14 -0
  41. package/lib/adapters/skills.js.map +1 -0
  42. package/lib/adapters/test-helpers.d.ts +99 -0
  43. package/lib/adapters/test-helpers.js +192 -0
  44. package/lib/adapters/test-helpers.js.map +1 -0
  45. package/lib/adapters/ui.d.ts +26 -0
  46. package/lib/adapters/ui.js +88 -0
  47. package/lib/adapters/ui.js.map +1 -0
  48. package/lib/adapters/workspaces.d.ts +12 -0
  49. package/lib/adapters/workspaces.js +92 -0
  50. package/lib/adapters/workspaces.js.map +1 -0
  51. package/lib/client.d.ts +377 -0
  52. package/lib/client.js +2598 -0
  53. package/lib/client.js.map +1 -0
  54. package/lib/core/analyzer.d.ts +35 -0
  55. package/lib/core/analyzer.js +563 -0
  56. package/lib/core/analyzer.js.map +1 -0
  57. package/lib/core/backup.d.ts +29 -0
  58. package/lib/core/backup.js +184 -0
  59. package/lib/core/backup.js.map +1 -0
  60. package/lib/core/exporter.d.ts +37 -0
  61. package/lib/core/exporter.js +222 -0
  62. package/lib/core/exporter.js.map +1 -0
  63. package/lib/core/importer.d.ts +42 -0
  64. package/lib/core/importer.js +25 -0
  65. package/lib/core/importer.js.map +1 -0
  66. package/lib/core/index.d.ts +12 -0
  67. package/lib/core/index.js +11 -0
  68. package/lib/core/index.js.map +1 -0
  69. package/lib/core/rollback.d.ts +9 -0
  70. package/lib/core/rollback.js +136 -0
  71. package/lib/core/rollback.js.map +1 -0
  72. package/lib/core/types.d.ts +331 -0
  73. package/lib/core/types.js +18 -0
  74. package/lib/core/types.js.map +1 -0
  75. package/lib/core/validator.d.ts +18 -0
  76. package/lib/core/validator.js +77 -0
  77. package/lib/core/validator.js.map +1 -0
  78. package/lib/index.d.ts +51 -0
  79. package/lib/index.js +875 -0
  80. package/lib/index.js.map +1 -0
  81. package/lib/migrations/index.d.ts +29 -0
  82. package/lib/migrations/index.js +56 -0
  83. package/lib/migrations/index.js.map +1 -0
  84. package/lib/migrations/v1-to-v2.d.ts +9 -0
  85. package/lib/migrations/v1-to-v2.js +10 -0
  86. package/lib/migrations/v1-to-v2.js.map +1 -0
  87. package/lib/profiles/index.d.ts +4 -0
  88. package/lib/profiles/index.js +5 -0
  89. package/lib/profiles/index.js.map +1 -0
  90. package/lib/profiles/profile-manager.d.ts +102 -0
  91. package/lib/profiles/profile-manager.js +522 -0
  92. package/lib/profiles/profile-manager.js.map +1 -0
  93. package/lib/schema/config.d.ts +28 -0
  94. package/lib/schema/config.js +136 -0
  95. package/lib/schema/config.js.map +1 -0
  96. package/lib/schema/manifest.d.ts +28 -0
  97. package/lib/schema/manifest.js +109 -0
  98. package/lib/schema/manifest.js.map +1 -0
  99. package/lib/schema/types.d.ts +169 -0
  100. package/lib/schema/types.js +8 -0
  101. package/lib/schema/types.js.map +1 -0
  102. package/lib/schema/versions.d.ts +27 -0
  103. package/lib/schema/versions.js +50 -0
  104. package/lib/schema/versions.js.map +1 -0
  105. package/lib/security/encryption.d.ts +41 -0
  106. package/lib/security/encryption.js +152 -0
  107. package/lib/security/encryption.js.map +1 -0
  108. package/lib/security/index.d.ts +16 -0
  109. package/lib/security/index.js +17 -0
  110. package/lib/security/index.js.map +1 -0
  111. package/lib/security/integrity.d.ts +30 -0
  112. package/lib/security/integrity.js +78 -0
  113. package/lib/security/integrity.js.map +1 -0
  114. package/lib/security/redaction.d.ts +26 -0
  115. package/lib/security/redaction.js +95 -0
  116. package/lib/security/redaction.js.map +1 -0
  117. package/lib/security/secret-scanner.d.ts +48 -0
  118. package/lib/security/secret-scanner.js +253 -0
  119. package/lib/security/secret-scanner.js.map +1 -0
  120. package/lib/security/zip-security.d.ts +27 -0
  121. package/lib/security/zip-security.js +154 -0
  122. package/lib/security/zip-security.js.map +1 -0
  123. package/lib/ui/conflict-view.d.ts +28 -0
  124. package/lib/ui/conflict-view.js +49 -0
  125. package/lib/ui/conflict-view.js.map +1 -0
  126. package/lib/ui/errors.d.ts +17 -0
  127. package/lib/ui/errors.js +76 -0
  128. package/lib/ui/errors.js.map +1 -0
  129. package/lib/ui/export-flow.d.ts +62 -0
  130. package/lib/ui/export-flow.js +79 -0
  131. package/lib/ui/export-flow.js.map +1 -0
  132. package/lib/ui/import-wizard.d.ts +67 -0
  133. package/lib/ui/import-wizard.js +169 -0
  134. package/lib/ui/import-wizard.js.map +1 -0
  135. package/lib/ui/path-mapping.d.ts +38 -0
  136. package/lib/ui/path-mapping.js +75 -0
  137. package/lib/ui/path-mapping.js.map +1 -0
  138. package/lib/ui/progress.d.ts +27 -0
  139. package/lib/ui/progress.js +76 -0
  140. package/lib/ui/progress.js.map +1 -0
  141. package/lib/ui/report.d.ts +21 -0
  142. package/lib/ui/report.js +146 -0
  143. package/lib/ui/report.js.map +1 -0
  144. package/lib/ui/test-helpers.d.ts +62 -0
  145. package/lib/ui/test-helpers.js +147 -0
  146. package/lib/ui/test-helpers.js.map +1 -0
  147. package/lib/ui/types.d.ts +129 -0
  148. package/lib/ui/types.js +21 -0
  149. package/lib/ui/types.js.map +1 -0
  150. package/lib/utils/hashing.d.ts +17 -0
  151. package/lib/utils/hashing.js +41 -0
  152. package/lib/utils/hashing.js.map +1 -0
  153. package/lib/utils/json.d.ts +24 -0
  154. package/lib/utils/json.js +76 -0
  155. package/lib/utils/json.js.map +1 -0
  156. package/lib/utils/logger.d.ts +39 -0
  157. package/lib/utils/logger.js +82 -0
  158. package/lib/utils/logger.js.map +1 -0
  159. package/lib/utils/paths.d.ts +31 -0
  160. package/lib/utils/paths.js +132 -0
  161. package/lib/utils/paths.js.map +1 -0
  162. package/lib/utils/zip.d.ts +54 -0
  163. package/lib/utils/zip.js +285 -0
  164. package/lib/utils/zip.js.map +1 -0
  165. package/package.json +109 -0
  166. package/src/adapters/agent-presets.ts +14 -0
  167. package/src/adapters/credentials.test.ts +90 -0
  168. package/src/adapters/credentials.ts +141 -0
  169. package/src/adapters/file-collection.ts +110 -0
  170. package/src/adapters/files.test.ts +113 -0
  171. package/src/adapters/index.ts +69 -0
  172. package/src/adapters/mcp.test.ts +96 -0
  173. package/src/adapters/mcp.ts +181 -0
  174. package/src/adapters/plugin-files.ts +96 -0
  175. package/src/adapters/plugins.test.ts +72 -0
  176. package/src/adapters/plugins.ts +176 -0
  177. package/src/adapters/prompts.test.ts +110 -0
  178. package/src/adapters/prompts.ts +226 -0
  179. package/src/adapters/providers.test.ts +125 -0
  180. package/src/adapters/providers.ts +161 -0
  181. package/src/adapters/roundtrip.test.ts +188 -0
  182. package/src/adapters/sessions.ts +15 -0
  183. package/src/adapters/settings.test.ts +93 -0
  184. package/src/adapters/settings.ts +165 -0
  185. package/src/adapters/skills.ts +14 -0
  186. package/src/adapters/test-helpers.ts +207 -0
  187. package/src/adapters/ui.test.ts +56 -0
  188. package/src/adapters/ui.ts +106 -0
  189. package/src/adapters/workspaces.test.ts +79 -0
  190. package/src/adapters/workspaces.ts +95 -0
  191. package/src/client/CLIENT_DEPENDENCIES.md +101 -0
  192. package/src/client/ConfigManagerSection.tsx +60 -0
  193. package/src/client/api.ts +270 -0
  194. package/src/client/client-types.ts +27 -0
  195. package/src/client/common/ErrorBanner.tsx +68 -0
  196. package/src/client/common/ProgressBar.tsx +49 -0
  197. package/src/client/common/ReportView.tsx +117 -0
  198. package/src/client/common/ui.tsx +165 -0
  199. package/src/client/config-manager.module.css +759 -0
  200. package/src/client/css-modules.d.ts +8 -0
  201. package/src/client/export/ExportView.tsx +222 -0
  202. package/src/client/import/ConflictList.tsx +72 -0
  203. package/src/client/import/ImportWizardView.tsx +406 -0
  204. package/src/client/import/PathMappingForm.tsx +77 -0
  205. package/src/client/index.ts +65 -0
  206. package/src/client/locales.ts +182 -0
  207. package/src/core/analyzer.ts +631 -0
  208. package/src/core/backup.ts +212 -0
  209. package/src/core/exporter.ts +246 -0
  210. package/src/core/importer.ts +66 -0
  211. package/src/core/index.ts +39 -0
  212. package/src/core/rollback.ts +140 -0
  213. package/src/core/smoke.test.ts +717 -0
  214. package/src/core/types.ts +343 -0
  215. package/src/core/validator.ts +84 -0
  216. package/src/index.ts +942 -0
  217. package/src/migrations/index.ts +71 -0
  218. package/src/migrations/v1-to-v2.ts +17 -0
  219. package/src/profiles/index.ts +8 -0
  220. package/src/profiles/profile-manager.test.ts +280 -0
  221. package/src/profiles/profile-manager.ts +606 -0
  222. package/src/schema/config.ts +137 -0
  223. package/src/schema/manifest.ts +124 -0
  224. package/src/schema/types.ts +179 -0
  225. package/src/schema/versions.ts +55 -0
  226. package/src/security/encryption.ts +180 -0
  227. package/src/security/index.ts +16 -0
  228. package/src/security/integrity.ts +102 -0
  229. package/src/security/redaction.ts +101 -0
  230. package/src/security/secret-scanner.ts +275 -0
  231. package/src/security/security.test.ts +796 -0
  232. package/src/security/zip-security.ts +180 -0
  233. package/src/ui/conflict-view.test.ts +62 -0
  234. package/src/ui/conflict-view.ts +65 -0
  235. package/src/ui/errors.test.ts +44 -0
  236. package/src/ui/errors.ts +95 -0
  237. package/src/ui/export-flow.test.ts +75 -0
  238. package/src/ui/export-flow.ts +128 -0
  239. package/src/ui/import-wizard.test.ts +128 -0
  240. package/src/ui/import-wizard.ts +208 -0
  241. package/src/ui/path-mapping.test.ts +67 -0
  242. package/src/ui/path-mapping.ts +103 -0
  243. package/src/ui/progress.test.ts +42 -0
  244. package/src/ui/progress.ts +90 -0
  245. package/src/ui/report.test.ts +93 -0
  246. package/src/ui/report.ts +140 -0
  247. package/src/ui/test-helpers.ts +177 -0
  248. package/src/ui/types.ts +193 -0
  249. package/src/utils/hashing.ts +53 -0
  250. package/src/utils/json.ts +77 -0
  251. package/src/utils/logger.ts +114 -0
  252. package/src/utils/paths.ts +122 -0
  253. package/src/utils/zip.ts +332 -0
@@ -0,0 +1,285 @@
1
+ /**
2
+ * ZIP 读写封装:node:zlib(deflate raw)+ 自实现 CRC32,零依赖。
3
+ * 同时是 ZIP 安全的第一道防线(规范 §19,m4 的 zip-security 可在此基础上强化):
4
+ * - 条目名 isPathSafe(拒绝 ../、绝对路径、盘符、UNC、NUL)
5
+ * - 条目数 / 压缩体积 / 解压体积 / 单条 / 压缩比(zip bomb)上限
6
+ * - 解压时逐条 CRC32 与尺寸校验,损坏即整体拒绝
7
+ * 解压只允许写入受控目标目录(safeExtract),绝不落任意路径。
8
+ */
9
+ import fs from 'node:fs/promises';
10
+ import path from 'node:path';
11
+ import zlib from 'node:zlib';
12
+ import { parseJsonSafe } from "./json.js";
13
+ import { isPathSafe, isSameOrChild } from "./paths.js";
14
+ export const DEFAULT_ZIP_SAFETY_LIMITS = {
15
+ maxEntries: 10_000,
16
+ maxTotalBytes: 500 * 1024 * 1024,
17
+ maxCompressedBytes: 200 * 1024 * 1024,
18
+ maxSingleBytes: 100 * 1024 * 1024,
19
+ maxRatio: 200,
20
+ };
21
+ export class ZipSafetyError extends Error {
22
+ constructor(message) {
23
+ super(message);
24
+ this.name = 'ZipSafetyError';
25
+ }
26
+ }
27
+ /* ---------------- CRC32(表驱动) ---------------- */
28
+ let crcTable = null;
29
+ function getCrcTable() {
30
+ if (crcTable)
31
+ return crcTable;
32
+ const table = new Uint32Array(256);
33
+ for (let n = 0; n < 256; n++) {
34
+ let c = n;
35
+ for (let k = 0; k < 8; k++) {
36
+ c = (c & 1) !== 0 ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1);
37
+ }
38
+ table[n] = c >>> 0;
39
+ }
40
+ crcTable = table;
41
+ return table;
42
+ }
43
+ export function crc32(data) {
44
+ const table = getCrcTable();
45
+ let crc = 0xffffffff;
46
+ for (let i = 0; i < data.length; i++) {
47
+ crc = table[(crc ^ data[i]) & 0xff] ^ (crc >>> 8);
48
+ }
49
+ return (crc ^ 0xffffffff) >>> 0;
50
+ }
51
+ /* ---------------- 写入 ---------------- */
52
+ function dosDateTime() {
53
+ const now = new Date();
54
+ const time = ((now.getHours() & 0x1f) << 11) | ((now.getMinutes() & 0x3f) << 5) | ((now.getSeconds() >> 1) & 0x1f);
55
+ const date = (((now.getFullYear() - 1980) & 0x7f) << 9) | (((now.getMonth() + 1) & 0x0f) << 5) | (now.getDate() & 0x1f);
56
+ return { time, date };
57
+ }
58
+ /** 组装 ZIP 字节(全部条目 method=8 deflate,UTF-8 文件名,含中央目录与 EOCD) */
59
+ export function zipToBuffer(entries) {
60
+ const chunks = [];
61
+ const central = [];
62
+ let offset = 0;
63
+ const { time, date } = dosDateTime();
64
+ for (const entry of entries) {
65
+ if (!isPathSafe(entry.name)) {
66
+ throw new ZipSafetyError(`拒绝写入不安全的条目名: ${entry.name}`);
67
+ }
68
+ const nameBuf = Buffer.from(entry.name, 'utf8');
69
+ const data = Buffer.isBuffer(entry.data) ? entry.data : Buffer.from(entry.data);
70
+ const compressed = zlib.deflateRawSync(data);
71
+ const crc = crc32(data);
72
+ const local = Buffer.alloc(30);
73
+ local.writeUInt32LE(0x04034b50, 0);
74
+ local.writeUInt16LE(20, 4);
75
+ local.writeUInt16LE(0x0800, 6); // UTF-8 文件名
76
+ local.writeUInt16LE(8, 8); // deflate
77
+ local.writeUInt16LE(time, 10);
78
+ local.writeUInt16LE(date, 12);
79
+ local.writeUInt32LE(crc, 14);
80
+ local.writeUInt32LE(compressed.length, 18);
81
+ local.writeUInt32LE(data.length, 22);
82
+ local.writeUInt16LE(nameBuf.length, 26);
83
+ local.writeUInt16LE(0, 28);
84
+ chunks.push(local, nameBuf, compressed);
85
+ const cen = Buffer.alloc(46);
86
+ cen.writeUInt32LE(0x02014b50, 0);
87
+ cen.writeUInt16LE(20, 4);
88
+ cen.writeUInt16LE(20, 6);
89
+ cen.writeUInt16LE(0x0800, 8);
90
+ cen.writeUInt16LE(8, 10);
91
+ cen.writeUInt16LE(time, 12);
92
+ cen.writeUInt16LE(date, 14);
93
+ cen.writeUInt32LE(crc, 16);
94
+ cen.writeUInt32LE(compressed.length, 20);
95
+ cen.writeUInt32LE(data.length, 24);
96
+ cen.writeUInt16LE(nameBuf.length, 28);
97
+ cen.writeUInt16LE(0, 30);
98
+ cen.writeUInt16LE(0, 32);
99
+ cen.writeUInt16LE(0, 34);
100
+ cen.writeUInt16LE(0, 36);
101
+ cen.writeUInt32LE(0, 38);
102
+ cen.writeUInt32LE(offset, 42);
103
+ central.push(cen, nameBuf);
104
+ offset += local.length + nameBuf.length + compressed.length;
105
+ }
106
+ const cd = Buffer.concat(central);
107
+ const eocd = Buffer.alloc(22);
108
+ eocd.writeUInt32LE(0x06054b50, 0);
109
+ eocd.writeUInt16LE(0, 4);
110
+ eocd.writeUInt16LE(0, 6);
111
+ eocd.writeUInt16LE(entries.length, 8);
112
+ eocd.writeUInt16LE(entries.length, 10);
113
+ eocd.writeUInt32LE(cd.length, 12);
114
+ eocd.writeUInt32LE(offset, 16);
115
+ eocd.writeUInt16LE(0, 20);
116
+ return Buffer.concat([...chunks, cd, eocd]);
117
+ }
118
+ /** 写出 ZIP 文件 */
119
+ export async function writeZip(zipPath, entries) {
120
+ const buf = zipToBuffer(entries);
121
+ await fs.mkdir(path.dirname(zipPath), { recursive: true });
122
+ await fs.writeFile(zipPath, buf);
123
+ }
124
+ /* ---------------- 读取 ---------------- */
125
+ const EOCD_SIG = Buffer.from([0x50, 0x4b, 0x05, 0x06]);
126
+ const LOCAL_SIG = 0x04034b50;
127
+ const CENTRAL_SIG = 0x02014b50;
128
+ /** 内存 ZIP 归档:构造时校验全部条目名与上限;读取时逐条解压 + CRC/尺寸/比预算校验 */
129
+ export class ZipArchive {
130
+ buf;
131
+ metas;
132
+ limits;
133
+ totalUncompressed = 0;
134
+ constructor(buf, metas, limits) {
135
+ this.buf = Buffer.isBuffer(buf) ? buf : Buffer.from(buf);
136
+ this.metas = metas;
137
+ this.limits = limits;
138
+ }
139
+ names() {
140
+ return this.metas.map((m) => m.name);
141
+ }
142
+ entries() {
143
+ return this.metas.map((m) => ({ ...m }));
144
+ }
145
+ has(name) {
146
+ return this.metas.some((m) => m.name === name);
147
+ }
148
+ /** 读取并解压单条目(带 CRC32 / 尺寸 / 体积预算 / 压缩比校验) */
149
+ readEntry(name) {
150
+ const meta = this.metas.find((m) => m.name === name);
151
+ if (!meta)
152
+ throw new ZipSafetyError(`ZIP 中不存在条目: ${name}`);
153
+ if (meta.isDirectory)
154
+ return Buffer.alloc(0);
155
+ const lhOffset = meta.localOffset;
156
+ const lh = this.buf.subarray(lhOffset, lhOffset + 30);
157
+ if (lh.length < 30 || lh.readUInt32LE(0) !== LOCAL_SIG) {
158
+ throw new ZipSafetyError(`条目 "${name}" 的本地文件头损坏`);
159
+ }
160
+ const nameLen = lh.readUInt16LE(26);
161
+ const extraLen = lh.readUInt16LE(28);
162
+ const dataStart = lhOffset + 30 + nameLen + extraLen;
163
+ if (dataStart + meta.compressedSize > this.buf.length) {
164
+ throw new ZipSafetyError(`条目 "${name}" 数据越界`);
165
+ }
166
+ const raw = this.buf.subarray(dataStart, dataStart + meta.compressedSize);
167
+ let out;
168
+ if (meta.method === 0) {
169
+ out = raw;
170
+ }
171
+ else if (meta.method === 8) {
172
+ try {
173
+ out = zlib.inflateRawSync(raw, { maxOutputLength: this.limits.maxSingleBytes });
174
+ }
175
+ catch (err) {
176
+ throw new ZipSafetyError(`条目 "${name}" 解压失败: ${err instanceof Error ? err.message : String(err)}`);
177
+ }
178
+ }
179
+ else {
180
+ throw new ZipSafetyError(`条目 "${name}" 使用未知压缩方法 ${meta.method}`);
181
+ }
182
+ if (out.length !== meta.uncompressedSize) {
183
+ throw new ZipSafetyError(`条目 "${name}" 解压尺寸不符(${out.length} ≠ ${meta.uncompressedSize})`);
184
+ }
185
+ if (crc32(out) !== meta.crc32) {
186
+ throw new ZipSafetyError(`条目 "${name}" CRC32 校验失败(ZIP 已损坏)`);
187
+ }
188
+ this.totalUncompressed += out.length;
189
+ if (this.totalUncompressed > this.limits.maxTotalBytes) {
190
+ throw new ZipSafetyError('ZIP 解压总字节数超过上限');
191
+ }
192
+ if (meta.compressedSize > 0) {
193
+ const ratio = out.length / meta.compressedSize;
194
+ if (ratio > this.limits.maxRatio) {
195
+ throw new ZipSafetyError(`条目 "${name}" 压缩比 ${ratio.toFixed(1)} 超过上限(疑似 zip bomb)`);
196
+ }
197
+ }
198
+ return out;
199
+ }
200
+ readEntryText(name) {
201
+ return Buffer.from(this.readEntry(name)).toString('utf8');
202
+ }
203
+ /** 读取并深度保护解析 JSON 条目 */
204
+ readEntryJson(name) {
205
+ return parseJsonSafe(this.readEntryText(name));
206
+ }
207
+ }
208
+ /** 解析 ZIP(含安全校验:条目名 / 条目数 / 压缩体积) */
209
+ export function parseZip(buf, limits = {}) {
210
+ const merged = { ...DEFAULT_ZIP_SAFETY_LIMITS, ...limits };
211
+ const b = Buffer.isBuffer(buf) ? buf : Buffer.from(buf);
212
+ if (b.length < 22)
213
+ throw new ZipSafetyError('不是合法的 ZIP 文件(体积过小)');
214
+ const eocdIdx = b.lastIndexOf(EOCD_SIG);
215
+ if (eocdIdx < 0 || eocdIdx + 22 > b.length) {
216
+ throw new ZipSafetyError('不是合法的 ZIP 文件(缺少中央目录结束记录)');
217
+ }
218
+ const totalEntries = b.readUInt16LE(eocdIdx + 10);
219
+ const cdSize = b.readUInt32LE(eocdIdx + 12);
220
+ const cdOffset = b.readUInt32LE(eocdIdx + 16);
221
+ if (cdOffset + cdSize > b.length) {
222
+ throw new ZipSafetyError('中央目录越界(ZIP 损坏)');
223
+ }
224
+ if (totalEntries > merged.maxEntries) {
225
+ throw new ZipSafetyError(`ZIP 条目数 ${totalEntries} 超过上限 ${merged.maxEntries}`);
226
+ }
227
+ const metas = [];
228
+ let pos = cdOffset;
229
+ let compressedTotal = 0;
230
+ for (let i = 0; i < totalEntries; i++) {
231
+ if (pos + 46 > cdOffset + cdSize)
232
+ throw new ZipSafetyError('中央目录条目越界(ZIP 损坏)');
233
+ if (b.readUInt32LE(pos) !== CENTRAL_SIG)
234
+ throw new ZipSafetyError('中央目录签名损坏');
235
+ const method = b.readUInt16LE(pos + 10);
236
+ const crc = b.readUInt32LE(pos + 16);
237
+ const compSize = b.readUInt32LE(pos + 20);
238
+ const uncompSize = b.readUInt32LE(pos + 24);
239
+ const nameLen = b.readUInt16LE(pos + 28);
240
+ const extraLen = b.readUInt16LE(pos + 30);
241
+ const commentLen = b.readUInt16LE(pos + 32);
242
+ const localOffset = b.readUInt32LE(pos + 42);
243
+ const name = b.subarray(pos + 46, pos + 46 + nameLen).toString('utf8');
244
+ if (!isPathSafe(name))
245
+ throw new ZipSafetyError(`ZIP 条目名不安全: ${name}`);
246
+ compressedTotal += compSize;
247
+ if (compressedTotal > merged.maxCompressedBytes) {
248
+ throw new ZipSafetyError('ZIP 压缩数据总量超过上限');
249
+ }
250
+ metas.push({
251
+ name,
252
+ method,
253
+ compressedSize: compSize,
254
+ uncompressedSize: uncompSize,
255
+ crc32: crc,
256
+ isDirectory: name.endsWith('/'),
257
+ localOffset,
258
+ });
259
+ pos += 46 + nameLen + extraLen + commentLen;
260
+ }
261
+ return new ZipArchive(b, metas, merged);
262
+ }
263
+ /**
264
+ * 安全解压到受控目录(对齐设计 §13.4 safeExtract 签名)。
265
+ * 返回条目相对路径列表;任何越界/损坏即整体拒绝(不部分落盘)。
266
+ * m4 的 zip-security 可替换/强化本实现。
267
+ */
268
+ export async function safeExtract(zipPath, destDir, limits = {}) {
269
+ const data = await fs.readFile(zipPath);
270
+ const archive = parseZip(data, limits);
271
+ const extracted = [];
272
+ for (const meta of archive.entries()) {
273
+ if (meta.isDirectory)
274
+ continue;
275
+ const target = path.join(destDir, ...meta.name.split('/'));
276
+ if (!isSameOrChild(target, destDir)) {
277
+ throw new ZipSafetyError(`解压目标越界: ${meta.name}`);
278
+ }
279
+ await fs.mkdir(path.dirname(target), { recursive: true });
280
+ await fs.writeFile(target, archive.readEntry(meta.name));
281
+ extracted.push(meta.name);
282
+ }
283
+ return extracted;
284
+ }
285
+ //# sourceMappingURL=zip.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zip.js","sourceRoot":"","sources":["../../src/utils/zip.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,109 @@
1
+ {
2
+ "name": "dsh-config-manager",
3
+ "version": "0.1.0",
4
+ "description": "DSH Config Manager — backup / export / import / migrate DSH configuration (dual-face Cordis plugin: host engine + web UI). Manifest shape mirrors @linxin666/dsh-ssh@0.1.12.",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": "^22.19.0 || >=24.0.0"
8
+ },
9
+ "main": "./lib/index.js",
10
+ "types": "./lib/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./lib/index.d.ts",
14
+ "default": "./lib/index.js"
15
+ },
16
+ "./core": {
17
+ "types": "./lib/core/index.d.ts",
18
+ "default": "./lib/core/index.js"
19
+ },
20
+ "./schema": {
21
+ "types": "./lib/schema/types.d.ts",
22
+ "default": "./lib/schema/types.js"
23
+ },
24
+ "./client": {
25
+ "types": "./lib/client.d.ts",
26
+ "default": "./lib/client.js"
27
+ },
28
+ "./package.json": "./package.json"
29
+ },
30
+ "dsh": {
31
+ "bundle": {
32
+ "patch": "./cordis.patch.yml"
33
+ },
34
+ "client": {
35
+ "inject": [
36
+ "@deepseek-ai/dsh-client-runtime",
37
+ "@deepseek-ai/dsh-client-connection",
38
+ "@deepseek-ai/dsh-client-ui-settings"
39
+ ],
40
+ "platform": "web"
41
+ }
42
+ },
43
+ "files": [
44
+ "lib",
45
+ "src",
46
+ "cordis.patch.yml",
47
+ "dsh.bundle.patch",
48
+ "dsh.client",
49
+ "README.md",
50
+ "README.zh-CN.md"
51
+ ],
52
+ "scripts": {
53
+ "test": "node --test \"src/**/*.test.ts\" \"tests/**/*.test.ts\"",
54
+ "smoke": "node --test src/core/smoke.test.ts",
55
+ "typecheck": "tsc --noEmit",
56
+ "build": "tsc -p tsconfig.build.json && tsdown",
57
+ "bundle": "tsdown",
58
+ "watch": "tsdown --watch",
59
+ "clean": "node -e \"require('node:fs').rmSync('lib', { recursive: true, force: true })\""
60
+ },
61
+ "dependencies": {
62
+ "js-yaml": "^4.1.0"
63
+ },
64
+ "peerDependencies": {
65
+ "@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.6",
66
+ "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.6",
67
+ "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
68
+ "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
69
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6",
70
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
71
+ "@deepseek-ai/dsh-credentials": "^0.1.0-rc.6",
72
+ "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6",
73
+ "@deepseek-ai/dsh-host-plugin-inventory": "^0.1.0-rc.6",
74
+ "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
75
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
76
+ "@deepseek-ai/dsh-plugin-marketplace": "^0.1.0-rc.6",
77
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
78
+ "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
79
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
80
+ "react": "^18.2.0",
81
+ "react-dom": "^18.2.0"
82
+ },
83
+ "devDependencies": {
84
+ "@deepseek-ai/cordis": "^4.0.1",
85
+ "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.6",
86
+ "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
87
+ "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
88
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6",
89
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
90
+ "@deepseek-ai/dsh-credentials": "^0.1.0-rc.6",
91
+ "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6",
92
+ "@deepseek-ai/dsh-host-plugin-inventory": "^0.1.0-rc.6",
93
+ "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
94
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
95
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
96
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
97
+ "@deepseek-ai/dsh-workspace": "^0.1.0-rc.6",
98
+ "@types/js-yaml": "^4.0.9",
99
+ "@types/node": "^24.0.0",
100
+ "@types/react": "~18.3.1",
101
+ "@types/react-dom": "^18.3.5",
102
+ "lightningcss": "1.32.0",
103
+ "react": "^18.3.1",
104
+ "react-dom": "^18.3.1",
105
+ "schemastery": "^3.18.0",
106
+ "tsdown": "0.22.2",
107
+ "typescript": "^5.9.0"
108
+ }
109
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * agentPresets 分区 adapter(设计 §3.3):
3
+ * 数据源 = ~/.dsh/.agent-presets/(用户可写目录,每预设一个目录:agent.cordis.yml + preset.yml)。
4
+ * system 预设(安装目录)只记引用不复制(本 adapter 只读用户目录)。
5
+ */
6
+ import { FileCollectionAdapter } from './file-collection.ts';
7
+
8
+ export class AgentPresetsAdapter extends FileCollectionAdapter {
9
+ readonly id = 'agentPresets' as const;
10
+ readonly displayName = 'Agent Presets';
11
+ readonly defaultIncluded = true;
12
+ readonly portability = 'portable' as const;
13
+ readonly baseDir = '.agent-presets';
14
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * credentials adapter 测试:状态导出(永不泄值)、ref 收集、MissingSecret 由引擎兜底、
3
+ * applyItem 经 secretInputs/decryptedCredentials 补录(仅内存)。
4
+ */
5
+ import test from 'node:test';
6
+ import assert from 'node:assert/strict';
7
+ import { CredentialsAdapter, defaultCredentialRefs } from './credentials.ts';
8
+ import { makeContext, makeImportContext } from './test-helpers.ts';
9
+ import type { PlanItem } from '../core/types.ts';
10
+
11
+ const NS = ['llm-deepseek', 'llm-pi-ai'];
12
+
13
+ test('credentials: 状态导出(configured/source/hasValue=false,值绝不含)', async () => {
14
+ const ctx = makeContext('win32', 'C:\\Users\\alice');
15
+ ctx.settings.ns.set('llm-deepseek', {
16
+ value: { apiKeyEnv: 'DEEPSEEK_API_KEY' },
17
+ revision: 5,
18
+ secrets: [{ path: ['apiKey'], set: true }],
19
+ });
20
+ ctx.settings.ns.set('llm-pi-ai', {
21
+ value: { providers: { openai: { apiKeyEnv: 'OPENAI_API_KEY' } } },
22
+ revision: 2,
23
+ secrets: [],
24
+ });
25
+ ctx.credentials.values.set('DEEPSEEK_API_KEY', 'sk-super-secret-123');
26
+
27
+ const adapter = new CredentialsAdapter({ namespaces: NS });
28
+ const out = await adapter.export(ctx, { includeSecrets: false });
29
+ const refs = out.data.credentials.map((c) => c.ref).sort();
30
+ assert.deepEqual(refs, ['DEEPSEEK_API_KEY', 'OPENAI_API_KEY'], '从 apiKeyEnv + secrets 标记收集');
31
+ const dk = out.data.credentials.find((c) => c.ref === 'DEEPSEEK_API_KEY');
32
+ assert.equal(dk?.configured, true);
33
+ assert.equal(dk?.hasValue, false, '值未导出(安全不变量)');
34
+ const ok = out.data.credentials.find((c) => c.ref === 'OPENAI_API_KEY');
35
+ assert.equal(ok?.configured, false);
36
+ // 序列化后的导出数据不得包含秘密值
37
+ const serialized = JSON.stringify(out.data);
38
+ assert.ok(!serialized.includes('sk-super-secret-123'), '凭据值不得进入导出数据');
39
+
40
+ const v = await adapter.validate(out.data);
41
+ assert.equal(v.valid, true);
42
+ });
43
+
44
+ test('credentials: defaultCredentialRefs 从 settings 收集', async () => {
45
+ const ctx = makeContext('win32', 'C:\\Users\\alice');
46
+ // 顶层 apiKeyEnv + secrets 引用类字段(apiKeyEnv)的值 = 凭据 ref
47
+ ctx.settings.ns.set('llm-deepseek', { value: { apiKeyEnv: 'DEEPSEEK_API_KEY' }, revision: 1, secrets: [{ path: ['apiKeyEnv'], set: true }] });
48
+ const refs = await defaultCredentialRefs(NS)(ctx);
49
+ assert.ok(refs.includes('DEEPSEEK_API_KEY'));
50
+ // secrets[].path[0] 若为非引用类字段(如 apiKey 字段路径),不收集为 ref
51
+ const ctx2 = makeContext('win32', 'C:\\Users\\alice');
52
+ ctx2.settings.ns.set('llm-deepseek', { value: { apiKeyEnv: 'DEEPSEEK_API_KEY' }, revision: 1, secrets: [{ path: ['apiKey'], set: true }] });
53
+ const refs2 = await defaultCredentialRefs(NS)(ctx2);
54
+ assert.ok(!refs2.includes('apiKey'), '字段路径不是凭据 ref');
55
+ });
56
+
57
+ test('credentials: applyItem 用 secretInputs / decryptedCredentials 补录(仅内存,不落盘值)', async () => {
58
+ const src = makeContext('win32', 'C:\\Users\\alice');
59
+ src.settings.ns.set('llm-deepseek', { value: { apiKeyEnv: 'DEEPSEEK_API_KEY' }, revision: 1, secrets: [] });
60
+ const adapter = new CredentialsAdapter({ namespaces: NS });
61
+ const exported = await adapter.export(src, { includeSecrets: false });
62
+ const sections = new Map([['credentialsStatus', exported.data]]);
63
+
64
+ const dst = makeContext('linux', '/home/bob');
65
+ const ictx = makeImportContext(dst, sections, { secretInputs: { DEEPSEEK_API_KEY: 'sk-from-user-input' } });
66
+ const item: PlanItem = {
67
+ id: 'secret:DEEPSEEK_API_KEY', kind: 'MissingSecret', adapter: 'credentialsStatus',
68
+ description: '凭据补录', severity: 'warning', target: { adapter: 'credentialsStatus', ref: 'DEEPSEEK_API_KEY' },
69
+ };
70
+ const r = await adapter.applyItem(item, ictx);
71
+ assert.equal(r.ok, true);
72
+ assert.equal(dst.credentials.values.get('DEEPSEEK_API_KEY'), 'sk-from-user-input');
73
+
74
+ // 解密通道(加密备份)
75
+ const dst2 = makeContext('linux', '/home/bob');
76
+ const dec = new Map<string, string>([['DEEPSEEK_API_KEY', 'sk-decrypted']]);
77
+ const r2 = await adapter.applyItem(item, makeImportContext(dst2, sections, { decryptedCredentials: dec }));
78
+ assert.equal(r2.ok, true);
79
+ assert.equal(dst2.credentials.values.get('DEEPSEEK_API_KEY'), 'sk-decrypted');
80
+
81
+ // 无值 → 失败(引擎在 MissingSecret 无值时先跳过,这里兜底)
82
+ const r3 = await adapter.applyItem(item, makeImportContext(dst2, sections));
83
+ assert.equal(r3.ok, false);
84
+ });
85
+
86
+ test('credentials: validate 拒绝 hasValue=true(安全不变量)', async () => {
87
+ const adapter = new CredentialsAdapter();
88
+ const bad = await adapter.validate({ version: 1, credentials: [{ ref: 'X', required: true, configured: true, hasValue: true } as never] });
89
+ assert.equal(bad.valid, false);
90
+ });
@@ -0,0 +1,141 @@
1
+ /**
2
+ * credentialsStatus 分区 adapter(设计 §3.3/§7.1):
3
+ * 数据源 = ctx.credentials.describe(ref) 的状态(configured/source/writable),
4
+ * 以及 settings secrets 标记 / llm apiKeyEnv 中引用的凭据 ref 名。
5
+ *
6
+ * 安全不变量:永不导出值(hasValue 恒 false);导入生成 MissingSecret 清单,
7
+ * 用户补录值经 ctx.secretInputs / decryptedCredentials(仅内存)→ credentials.set()。
8
+ * .credentials.yaml 文件字节交由 m4 加密层处理,本 adapter 不触碰。
9
+ */
10
+ import type { CredentialStatus, CredentialsSection } from '../schema/types.ts';
11
+ import type {
12
+ ApplyResult, ConfigAdapter, ExportOptions, ExportSection, HostContext,
13
+ ImportContext, PlanItem, ValidationResult,
14
+ } from '../core/types.ts';
15
+ import { resolveNamespaces, type NamespaceProvider } from './settings.ts';
16
+
17
+ export type CredentialRefsProvider = (ctx: HostContext) => Promise<string[]>;
18
+
19
+ /** 缺省 ref 收集:遍历 settings namespace,收集 llm apiKeyEnv / providers[].apiKeyEnv,
20
+ * 以及 secrets 标记中「引用类字段」(apiKeyEnv/tokenEnv…)的字段值。
21
+ * 注意 secrets[].path[0] 是 settings 文档内的字段路径(如 ['apiKey']),本身不是凭据 ref(设计 §4.2),
22
+ * 只有指向 env 名的引用字段才值得收集。 */
23
+ export function defaultCredentialRefs(namespaces: string[] | NamespaceProvider): CredentialRefsProvider {
24
+ return async (ctx: HostContext): Promise<string[]> => {
25
+ const refs = new Set<string>();
26
+ for (const ns of await resolveNamespaces(namespaces, ctx)) {
27
+ try {
28
+ const info = await ctx.settings.describe(ns, { redactSecrets: true });
29
+ const value = (info.value ?? {}) as Record<string, unknown>;
30
+ // 引用类字段名(值 = env/凭据引用名,非秘密本身)
31
+ const REFERENCE_REF_FIELDS = new Set([
32
+ 'apikeyenv', 'api_key_env', 'apikeyname', 'tokenenv', 'accesstokenenv',
33
+ 'refreshtokenenv', 'clientsecretenv', 'passwordenv',
34
+ ]);
35
+ for (const s of info.secrets) {
36
+ const first = s.path?.[0];
37
+ if (typeof first !== 'string' || first === '') continue;
38
+ const norm = first.toLowerCase().replace(/[^a-z0-9]/g, '');
39
+ if (!REFERENCE_REF_FIELDS.has(norm)) continue;
40
+ const val = value[first];
41
+ if (typeof val === 'string' && val !== '') refs.add(val);
42
+ }
43
+ if (typeof value['apiKeyEnv'] === 'string' && value['apiKeyEnv'] !== '') refs.add(value['apiKeyEnv'] as string);
44
+ const providers = value['providers'];
45
+ if (providers !== null && typeof providers === 'object') {
46
+ for (const pv of Object.values(providers as Record<string, { apiKeyEnv?: unknown }>)) {
47
+ if (pv !== null && typeof pv === 'object' && typeof pv.apiKeyEnv === 'string' && pv.apiKeyEnv !== '') {
48
+ refs.add(pv.apiKeyEnv);
49
+ }
50
+ }
51
+ }
52
+ } catch {
53
+ // namespace 不存在则跳过
54
+ }
55
+ }
56
+ return [...refs];
57
+ };
58
+ }
59
+
60
+ export interface CredentialsAdapterOptions {
61
+ /** 凭据 ref 名收集器(缺省从 settings 推断) */
62
+ refs?: CredentialRefsProvider;
63
+ /** 供缺省 refs 使用的 namespace 清单 */
64
+ namespaces?: string[] | NamespaceProvider;
65
+ }
66
+
67
+ export class CredentialsAdapter implements ConfigAdapter<CredentialsSection> {
68
+ readonly id = 'credentialsStatus' as const;
69
+ readonly displayName = 'Credentials';
70
+ readonly defaultIncluded = true;
71
+ readonly portability = 'deviceSpecific' as const;
72
+ private readonly refs: CredentialRefsProvider;
73
+
74
+ constructor(options: CredentialsAdapterOptions = {}) {
75
+ this.refs = options.refs ?? defaultCredentialRefs(options.namespaces ?? []);
76
+ }
77
+
78
+ async export(ctx: HostContext, _options: ExportOptions): Promise<ExportSection<CredentialsSection>> {
79
+ const credentials: CredentialStatus[] = [];
80
+ const warnings: string[] = [];
81
+ for (const ref of await this.refs(ctx)) {
82
+ try {
83
+ const status = await ctx.credentials.describe(ref);
84
+ credentials.push({
85
+ ref,
86
+ required: true,
87
+ configured: status.configured,
88
+ source: (status.source as CredentialStatus['source']) ?? 'file',
89
+ hasValue: false, // 值未导出(安全不变量)
90
+ });
91
+ } catch (err) {
92
+ warnings.push(`凭据 ${ref} 状态读取失败: ${err instanceof Error ? err.message : String(err)}`);
93
+ }
94
+ }
95
+ return {
96
+ sectionId: 'credentialsStatus',
97
+ data: { version: 1, credentials },
98
+ counts: { credentials: credentials.length },
99
+ warnings,
100
+ };
101
+ }
102
+
103
+ async analyzeImport(_data: CredentialsSection, _ctx: ImportContext): Promise<PlanItem[]> {
104
+ // MissingSecret 计划项由引擎在 createImportPlan 兜底生成(analyzer.ensureMissingSecrets),
105
+ // 依据 = 本分区里 configured=true 的凭据 → 用户补录清单。这里保持零写入纯计算。
106
+ return [];
107
+ }
108
+
109
+ async applyItem(item: PlanItem, ctx: ImportContext): Promise<ApplyResult> {
110
+ const ref = item.target?.ref;
111
+ if (!ref) return { ok: false, message: '缺少 target.ref' };
112
+ const value = ctx.secretInputs[ref] ?? ctx.decryptedCredentials?.get(ref);
113
+ if (value === undefined || value === '') return { ok: false, message: '凭据值未提供(需补录)' };
114
+ await ctx.target.credentials.set(ref, value);
115
+ return { ok: true };
116
+ }
117
+
118
+ async validate(data: CredentialsSection): Promise<ValidationResult> {
119
+ const issues: ValidationResult['issues'] = [];
120
+ if (data === null || typeof data !== 'object') {
121
+ return { valid: false, issues: [{ path: '$', message: 'credentials 数据必须是对象', severity: 'error' }] };
122
+ }
123
+ if (data.version !== 1) {
124
+ issues.push({ path: 'version', message: `version 必须为 1(收到 ${String(data.version)})`, severity: 'error' });
125
+ }
126
+ if (!Array.isArray(data.credentials)) {
127
+ issues.push({ path: 'credentials', message: 'credentials 必须是数组', severity: 'error' });
128
+ } else {
129
+ for (const c of data.credentials) {
130
+ if (c === null || typeof c !== 'object' || typeof c.ref !== 'string' || c.ref === '') {
131
+ issues.push({ path: 'credentials[]', message: '凭据记录必须含非空 ref', severity: 'error' });
132
+ }
133
+ if (c.hasValue === true) {
134
+ // 安全不变量:普通导出恒不携带值;若备份声称有值,视为结构异常
135
+ issues.push({ path: `credentials.${c.ref}.hasValue`, message: '值未导出,hasValue 必须为 false', severity: 'error' });
136
+ }
137
+ }
138
+ }
139
+ return { valid: issues.filter((i) => i.severity === 'error').length === 0, issues };
140
+ }
141
+ }