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,137 @@
1
+ /**
2
+ * 各分区数据结构:ZIP 内 JSON 分区文件 → 解析/校验,以及分区文件路径表。
3
+ * 类型本体在 types.ts;本文件是「分区数据如何落盘/读回」的唯一出口。
4
+ */
5
+ import { parseJsonSafe } from '../utils/json.ts';
6
+ import type {
7
+ CredentialsSection, FilesSection, McpSection, PluginsSection,
8
+ PromptsSection, ProvidersSection, SectionData, SectionId,
9
+ SettingsSection, UiSection, WorkspacesSection,
10
+ } from './types.ts';
11
+
12
+ /** 全部分区 id(manifest 校验与 adapter registry 共用) */
13
+ export const SECTION_IDS: readonly SectionId[] = [
14
+ 'settings', 'ui', 'providers', 'plugins', 'mcp', 'prompts',
15
+ 'skills', 'agentPresets', 'workspaces', 'pluginFiles',
16
+ 'credentialsStatus', 'secrets', 'sessions',
17
+ ];
18
+
19
+ /** JSON 分区在 ZIP 内的相对路径;文件类分区(skills 等)走目录前缀,不在此表 */
20
+ export const SECTION_JSON_PATHS: Partial<Record<SectionId, string>> = {
21
+ settings: 'config/settings.json',
22
+ ui: 'config/ui.json',
23
+ providers: 'ai/providers.json',
24
+ plugins: 'plugins/plugins.json',
25
+ mcp: 'mcp/servers.json',
26
+ prompts: 'custom/prompts.json',
27
+ workspaces: 'workspaces/workspaces.json',
28
+ credentialsStatus: 'security/credentials.json',
29
+ };
30
+
31
+ /** 文件类分区在 ZIP 内的目录前缀 */
32
+ export const SECTION_FILE_PREFIXES: Partial<Record<SectionId, string>> = {
33
+ skills: 'custom/skills/',
34
+ agentPresets: 'agents/presets/',
35
+ pluginFiles: 'plugin-files/',
36
+ sessions: 'sessions/',
37
+ };
38
+
39
+ /** 该分区是否是「文件类」分区(ZIP 内以真实文件存放而非 JSON) */
40
+ export function isFileSection(sectionId: SectionId): boolean {
41
+ return sectionId in SECTION_FILE_PREFIXES;
42
+ }
43
+
44
+ /** 从 ZIP 内 JSON 解析分区数据(深度保护 + 结构校验) */
45
+ export function parseSectionJson<T extends SectionData>(sectionId: SectionId, raw: string): T {
46
+ const parsed = parseJsonSafe(raw) as T;
47
+ const issues = validateSectionData(sectionId, parsed);
48
+ const errors = issues.filter((i) => i.severity === 'error');
49
+ if (errors.length > 0) {
50
+ throw new Error(`分区 ${sectionId} 数据无效: ${errors.map((e) => e.message).join('; ')}`);
51
+ }
52
+ return parsed;
53
+ }
54
+
55
+ export interface SectionIssue { path: string; message: string; severity: 'error' | 'warning'; }
56
+
57
+ /** 分区数据结构基础校验(version 字段 + 顶层形状) */
58
+ export function validateSectionData(sectionId: SectionId, data: unknown): SectionIssue[] {
59
+ const issues: SectionIssue[] = [];
60
+ if (data === null || typeof data !== 'object') {
61
+ return [{ path: '$', message: `分区 ${sectionId} 数据必须是对象`, severity: 'error' }];
62
+ }
63
+ const obj = data as Record<string, unknown>;
64
+ if (obj['version'] !== 1) {
65
+ issues.push({ path: 'version', message: `分区 ${sectionId} 的 version 必须为 1(收到 ${String(obj['version'])})`, severity: 'error' });
66
+ return issues;
67
+ }
68
+ switch (sectionId) {
69
+ case 'settings':
70
+ case 'ui': {
71
+ const ns = obj['namespaces'];
72
+ if (ns === null || typeof ns !== 'object') {
73
+ issues.push({ path: 'namespaces', message: `分区 ${sectionId} 缺少 namespaces 对象`, severity: 'error' });
74
+ } else {
75
+ for (const [name, rec] of Object.entries(ns as Record<string, unknown>)) {
76
+ if (rec === null || typeof rec !== 'object') {
77
+ issues.push({ path: `namespaces.${name}`, message: 'namespace 记录必须是对象', severity: 'error' });
78
+ continue;
79
+ }
80
+ const r = rec as Record<string, unknown>;
81
+ if (typeof r['revision'] !== 'number') issues.push({ path: `namespaces.${name}.revision`, message: 'revision 必须是数字', severity: 'error' });
82
+ if (!('value' in r)) issues.push({ path: `namespaces.${name}.value`, message: '缺少 value', severity: 'error' });
83
+ const secrets = r['secrets'];
84
+ if (secrets !== undefined && !Array.isArray(secrets)) issues.push({ path: `namespaces.${name}.secrets`, message: 'secrets 必须是数组', severity: 'error' });
85
+ }
86
+ }
87
+ break;
88
+ }
89
+ case 'providers': {
90
+ const p = obj['providers'];
91
+ if (p === null || typeof p !== 'object') issues.push({ path: 'providers', message: '缺少 providers 对象', severity: 'error' });
92
+ break;
93
+ }
94
+ case 'plugins': {
95
+ if (!Array.isArray(obj['plugins'])) issues.push({ path: 'plugins', message: 'plugins 必须是数组', severity: 'error' });
96
+ if (obj['patch'] !== undefined && !Array.isArray(obj['patch'])) issues.push({ path: 'patch', message: 'patch 必须是数组', severity: 'error' });
97
+ break;
98
+ }
99
+ case 'mcp': {
100
+ if (!Array.isArray(obj['servers'])) issues.push({ path: 'servers', message: 'servers 必须是数组', severity: 'error' });
101
+ break;
102
+ }
103
+ case 'prompts': {
104
+ if (!Array.isArray(obj['prompts'])) issues.push({ path: 'prompts', message: 'prompts 必须是数组', severity: 'error' });
105
+ break;
106
+ }
107
+ case 'workspaces': {
108
+ if (!Array.isArray(obj['workspaces'])) issues.push({ path: 'workspaces', message: 'workspaces 必须是数组', severity: 'error' });
109
+ break;
110
+ }
111
+ case 'credentialsStatus': {
112
+ if (!Array.isArray(obj['credentials'])) issues.push({ path: 'credentials', message: 'credentials 必须是数组', severity: 'error' });
113
+ break;
114
+ }
115
+ case 'skills':
116
+ case 'agentPresets':
117
+ case 'pluginFiles':
118
+ case 'sessions': {
119
+ if (!Array.isArray(obj['files'])) issues.push({ path: 'files', message: 'files 必须是数组', severity: 'error' });
120
+ break;
121
+ }
122
+ default:
123
+ issues.push({ path: '$', message: `未知分区 ${sectionId}`, severity: 'error' });
124
+ }
125
+ return issues;
126
+ }
127
+
128
+ /** 分区 JSON 载荷的类型收窄(供 adapter / analyzer 使用) */
129
+ export function asSettingsSection(data: unknown): SettingsSection { return data as SettingsSection; }
130
+ export function asUiSection(data: unknown): UiSection { return data as UiSection; }
131
+ export function asProvidersSection(data: unknown): ProvidersSection { return data as ProvidersSection; }
132
+ export function asPluginsSection(data: unknown): PluginsSection { return data as PluginsSection; }
133
+ export function asMcpSection(data: unknown): McpSection { return data as McpSection; }
134
+ export function asPromptsSection(data: unknown): PromptsSection { return data as PromptsSection; }
135
+ export function asWorkspacesSection(data: unknown): WorkspacesSection { return data as WorkspacesSection; }
136
+ export function asCredentialsSection(data: unknown): CredentialsSection { return data as CredentialsSection; }
137
+ export function asFilesSection(data: unknown): FilesSection { return data as FilesSection; }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * manifest.json 的类型、解析、序列化与结构校验。
3
+ * 类型本体在 types.ts(§13.1);本文件负责 manifest 的读写与结构完整性。
4
+ */
5
+ import { parseJsonSafe } from '../utils/json.ts';
6
+ import { CURRENT_SCHEMA_VERSION } from './versions.ts';
7
+ import type { Manifest, Platform, SectionId } from './types.ts';
8
+ import { SECTION_IDS } from './config.ts';
9
+
10
+ export const MANIFEST_FILE = 'manifest.json';
11
+ export const CHECKSUMS_FILE = 'integrity/checksums.json';
12
+ export const EXPORTER_NAME = 'DSH Config Manager';
13
+
14
+ export interface BuildManifestInput {
15
+ exporterVersion: string;
16
+ dshVersion: string;
17
+ platform: Platform;
18
+ arch: string;
19
+ sections: Record<SectionId, boolean>;
20
+ containsSecrets: boolean;
21
+ encrypted: boolean;
22
+ encryption: Manifest['security']['encryption'];
23
+ exportedAt?: string; // 测试可注入固定时间
24
+ }
25
+
26
+ /** 构造 manifest(schemaVersion 恒为当前版本,集中于此) */
27
+ export function buildManifest(input: BuildManifestInput): Manifest {
28
+ return {
29
+ schemaVersion: CURRENT_SCHEMA_VERSION,
30
+ exporter: { name: EXPORTER_NAME, version: input.exporterVersion },
31
+ source: {
32
+ dshVersion: input.dshVersion,
33
+ platform: input.platform,
34
+ arch: input.arch,
35
+ },
36
+ exportedAt: input.exportedAt ?? new Date().toISOString(),
37
+ sections: input.sections,
38
+ security: {
39
+ containsSecrets: input.containsSecrets,
40
+ encrypted: input.encrypted,
41
+ encryption: input.encryption,
42
+ },
43
+ };
44
+ }
45
+
46
+ /** 序列化(pretty JSON,供 ZIP 内 manifest.json) */
47
+ export function serializeManifest(manifest: Manifest): string {
48
+ return JSON.stringify(manifest, null, 2);
49
+ }
50
+
51
+ /** 解析 + 结构校验;非法输入抛错(导入第一道闸之一) */
52
+ export function parseManifest(raw: string): Manifest {
53
+ const parsed = parseJsonSafe(raw);
54
+ const issues = validateManifest(parsed);
55
+ const errors = issues.filter((i) => i.severity === 'error');
56
+ if (errors.length > 0) {
57
+ throw new Error(`manifest.json 无效: ${errors.map((e) => e.message).join('; ')}`);
58
+ }
59
+ return parsed as Manifest;
60
+ }
61
+
62
+ export interface ManifestIssue { path: string; message: string; severity: 'error' | 'warning'; }
63
+
64
+ /** 结构校验:字段类型 / sections 键集合 / security 形状 */
65
+ export function validateManifest(m: unknown): ManifestIssue[] {
66
+ const issues: ManifestIssue[] = [];
67
+ if (m === null || typeof m !== 'object') {
68
+ return [{ path: '$', message: 'manifest 必须是对象', severity: 'error' }];
69
+ }
70
+ const obj = m as Record<string, unknown>;
71
+
72
+ if (typeof obj['schemaVersion'] !== 'number') {
73
+ issues.push({ path: 'schemaVersion', message: 'schemaVersion 必须是数字', severity: 'error' });
74
+ }
75
+
76
+ const exporter = obj['exporter'] as Record<string, unknown> | undefined;
77
+ if (!exporter || typeof exporter !== 'object') {
78
+ issues.push({ path: 'exporter', message: 'exporter 缺失', severity: 'error' });
79
+ } else {
80
+ if (typeof exporter['name'] !== 'string') issues.push({ path: 'exporter.name', message: 'exporter.name 必须是字符串', severity: 'error' });
81
+ if (typeof exporter['version'] !== 'string') issues.push({ path: 'exporter.version', message: 'exporter.version 必须是字符串', severity: 'error' });
82
+ }
83
+
84
+ const source = obj['source'] as Record<string, unknown> | undefined;
85
+ if (!source || typeof source !== 'object') {
86
+ issues.push({ path: 'source', message: 'source 缺失', severity: 'error' });
87
+ } else {
88
+ if (typeof source['dshVersion'] !== 'string') issues.push({ path: 'source.dshVersion', message: 'source.dshVersion 必须是字符串', severity: 'error' });
89
+ if (typeof source['platform'] !== 'string') issues.push({ path: 'source.platform', message: 'source.platform 必须是字符串', severity: 'error' });
90
+ if (typeof source['arch'] !== 'string') issues.push({ path: 'source.arch', message: 'source.arch 必须是字符串', severity: 'error' });
91
+ }
92
+
93
+ if (typeof obj['exportedAt'] !== 'string' || Number.isNaN(Date.parse(obj['exportedAt']))) {
94
+ issues.push({ path: 'exportedAt', message: 'exportedAt 必须是合法 ISO-8601 时间', severity: 'error' });
95
+ }
96
+
97
+ const sections = obj['sections'] as Record<string, unknown> | undefined;
98
+ if (!sections || typeof sections !== 'object') {
99
+ issues.push({ path: 'sections', message: 'sections 缺失', severity: 'error' });
100
+ } else {
101
+ for (const key of Object.keys(sections)) {
102
+ if (!(SECTION_IDS as readonly string[]).includes(key)) {
103
+ issues.push({ path: `sections.${key}`, message: `未知分区 "${key}"`, severity: 'warning' });
104
+ }
105
+ if (typeof sections[key] !== 'boolean') {
106
+ issues.push({ path: `sections.${key}`, message: `sections.${key} 必须是布尔`, severity: 'error' });
107
+ }
108
+ }
109
+ }
110
+
111
+ const security = obj['security'] as Record<string, unknown> | undefined;
112
+ if (!security || typeof security !== 'object') {
113
+ issues.push({ path: 'security', message: 'security 缺失', severity: 'error' });
114
+ } else {
115
+ if (typeof security['containsSecrets'] !== 'boolean') issues.push({ path: 'security.containsSecrets', message: 'containsSecrets 必须是布尔', severity: 'error' });
116
+ if (typeof security['encrypted'] !== 'boolean') issues.push({ path: 'security.encrypted', message: 'encrypted 必须是布尔', severity: 'error' });
117
+ const enc = security['encryption'];
118
+ if (enc !== null && (typeof enc !== 'object' || enc === undefined)) {
119
+ issues.push({ path: 'security.encryption', message: 'encryption 必须是对象或 null', severity: 'error' });
120
+ }
121
+ }
122
+
123
+ return issues;
124
+ }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * 核心领域类型(Export Schema v1 的纯类型定义)。
3
+ *
4
+ * 对齐 Docs/design/architecture.md §13.1 —— m3/m5 的共同契约。
5
+ * 本文件只含与 DSH 运行时无关的纯数据形状;DSH Service 门面类型见 src/core/types.ts。
6
+ */
7
+
8
+ /** 运行平台(等价 NodeJS.Platform,但保持类型自包含,可在浏览器侧引用) */
9
+ export type Platform =
10
+ | 'win32' | 'darwin' | 'linux' | 'freebsd' | 'openbsd' | 'aix' | 'sunos'
11
+ | 'android' | 'cygwin' | 'haiku' | 'netbsd' | 'other';
12
+
13
+ /**
14
+ * 分区标识,与 ZIP 目录结构、manifest.sections 键一一对应。
15
+ * 明确不实现的分区(keybindings/workflows/commands/rules)不在此列。
16
+ */
17
+ export type SectionId =
18
+ | 'settings' | 'ui' | 'providers' | 'plugins' | 'mcp' | 'prompts'
19
+ | 'skills' | 'agentPresets' | 'workspaces' | 'pluginFiles'
20
+ | 'credentialsStatus' | 'secrets' | 'sessions';
21
+
22
+ /** 加密备份的算法参数(非秘密:salt/iv/authTag 仅用于解密与完整性校验) */
23
+ export interface EncryptionInfo {
24
+ algorithm: 'aes-256-gcm';
25
+ kdf: 'scrypt';
26
+ kdfParams: { N: number; r: number; p: number; keyLength: number };
27
+ salt: string; // base64
28
+ iv: string; // base64
29
+ authTag: string;// base64(GCM 认证标签)
30
+ version: number;
31
+ }
32
+
33
+ /** 每个导出 ZIP 根部的 manifest.json */
34
+ export interface Manifest {
35
+ schemaVersion: number;
36
+ exporter: { name: string; version: string };
37
+ source: { dshVersion: string; platform: Platform; arch: string };
38
+ exportedAt: string; // ISO-8601 UTC
39
+ sections: Record<SectionId, boolean>;
40
+ security: {
41
+ containsSecrets: boolean;
42
+ encrypted: boolean;
43
+ encryption: EncryptionInfo | null;
44
+ };
45
+ }
46
+
47
+ /* —— settings / ui 分区 —— */
48
+
49
+ /** settings.yaml 中单个 namespace 的导出记录(redacted 值 + 乐观锁 revision) */
50
+ export interface NamespaceRecord {
51
+ value: unknown;
52
+ base?: unknown;
53
+ revision: number;
54
+ applies?: string[];
55
+ /** DSH describe({redactSecrets:true}) 报告的 secrets 位置(只含路径标记,不含值) */
56
+ secrets: { path: string[]; set: boolean }[];
57
+ }
58
+
59
+ export interface SettingsSection { version: 1; namespaces: Record<string, NamespaceRecord>; }
60
+
61
+ export interface UiMigrationNote {
62
+ plugin: string;
63
+ storage: string;
64
+ key: string;
65
+ migratable: false;
66
+ reason: string;
67
+ }
68
+
69
+ export interface UiSection {
70
+ version: 1;
71
+ namespaces: Record<string, NamespaceRecord>;
72
+ /** localStorage 等 Host 不可迁移项的说明(纯说明,不含任何值) */
73
+ uiMigrationNotes: UiMigrationNote[];
74
+ }
75
+
76
+ /* —— providers 分区 —— */
77
+
78
+ export interface ProviderEntry {
79
+ route: string;
80
+ apiKeyEnv?: string; // 只记环境变量名,值在 credentials
81
+ displayName?: string;
82
+ baseURL?: string;
83
+ models?: unknown[];
84
+ modelOverrides?: unknown;
85
+ reasoning?: unknown;
86
+ transport?: unknown;
87
+ retryPolicy?: unknown;
88
+ }
89
+
90
+ export interface ProvidersSection { version: 1; providers: Record<string, ProviderEntry>; }
91
+
92
+ /* —— plugins 分区 —— */
93
+
94
+ export interface PluginEntry {
95
+ name: string; // 包名
96
+ version: string;
97
+ isBundle: boolean;
98
+ inBundles: string[];
99
+ enabled: boolean;
100
+ fiberPhase?: string;
101
+ }
102
+
103
+ export interface PatchLine { file: string; lineId: string; raw: unknown; }
104
+
105
+ export interface PluginsSection {
106
+ version: 1;
107
+ plugins: PluginEntry[];
108
+ patch: PatchLine[];
109
+ }
110
+
111
+ /* —— mcp 分区 —— */
112
+
113
+ export interface McpServerEntry {
114
+ serverName: string;
115
+ type: 'stdio' | 'streamable-http';
116
+ command?: string;
117
+ args?: string[];
118
+ env?: Record<string, string>;
119
+ cwd?: string;
120
+ url?: string;
121
+ headers?: Record<string, string>;
122
+ }
123
+
124
+ export interface McpSection { version: 1; servers: McpServerEntry[]; }
125
+
126
+ /* —— prompts 分区 —— */
127
+
128
+ export interface PromptEntry {
129
+ id: string;
130
+ name: string;
131
+ kind: 'systemPrompt' | 'planMode';
132
+ text: string;
133
+ sourceLineId?: string;
134
+ }
135
+
136
+ export interface PromptsSection { version: 1; prompts: PromptEntry[]; }
137
+
138
+ /* —— workspaces 分区(绝对路径 → 跨设备必须路径映射) —— */
139
+
140
+ export interface WorkspaceRecord {
141
+ id: string;
142
+ path: string;
143
+ title?: string;
144
+ sessionIds: string[];
145
+ createdAt?: string;
146
+ updatedAt?: string;
147
+ }
148
+
149
+ export interface WorkspacesSection { version: 1; workspaces: WorkspaceRecord[]; }
150
+
151
+ /* —— credentials 状态分区(永不含值) —— */
152
+
153
+ export interface CredentialStatus {
154
+ ref: string;
155
+ required: boolean;
156
+ configured: boolean;
157
+ source?: 'env' | 'file' | 'projectEnv' | 'other';
158
+ hasValue: boolean; // 普通备份恒 false(值未导出)
159
+ }
160
+
161
+ export interface CredentialsSection { version: 1; credentials: CredentialStatus[]; }
162
+
163
+ /* —— 文件类分区(skills/agentPresets/pluginFiles/sessions) ——
164
+ * 这些分区以真实文件形式进入 ZIP(custom/skills/… 等),
165
+ * 内存中统一表示为 FilesSection(ZIP 内不落此 JSON,见 exporter)。 */
166
+
167
+ export interface FileEntry {
168
+ relativePath: string;
169
+ data: Uint8Array;
170
+ contentHash: string; // SHA-256 hex
171
+ }
172
+
173
+ export interface FilesSection { version: 1; files: FileEntry[]; }
174
+
175
+ /** 分区 JSON 文件的统一载荷(schema 校验用) */
176
+ export type SectionData =
177
+ | SettingsSection | UiSection | ProvidersSection | PluginsSection
178
+ | McpSection | PromptsSection | WorkspacesSection | CredentialsSection
179
+ | FilesSection;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Export Schema 版本判定 —— 集中唯一出口(对齐 Docs/design/architecture.md §10.3)。
3
+ *
4
+ * 业务代码零版本判断:所有模块一律通过本文件函数判定,
5
+ * 任何 `if (v === 1)` 式散落判断均为禁止项。
6
+ */
7
+
8
+ /** 当前 Export Schema 版本(v1 起点;未来随迁移链演进) */
9
+ export const CURRENT_SCHEMA_VERSION = 1 as const;
10
+
11
+ /** 本插件支持导入的最低 schema 版本 */
12
+ export const MIN_SUPPORTED_SCHEMA_VERSION = 1 as const;
13
+
14
+ /** 版本不受支持(过新或过旧)时的错误;携带版本号便于 UI 提示升级/更新备份 */
15
+ export class UnsupportedSchemaError extends Error {
16
+ readonly version: number;
17
+ constructor(version: number, message = `Export schema version ${version} is not supported (current: ${CURRENT_SCHEMA_VERSION})`) {
18
+ super(message);
19
+ this.name = 'UnsupportedSchemaError';
20
+ this.version = version;
21
+ }
22
+ }
23
+
24
+ /** 恰好是当前版本 */
25
+ export function isCurrent(v: number): boolean {
26
+ return v === CURRENT_SCHEMA_VERSION;
27
+ }
28
+
29
+ /** 版本在支持范围内(含需要迁移的旧版本) */
30
+ export function isSupported(v: number): boolean {
31
+ return v >= MIN_SUPPORTED_SCHEMA_VERSION && v <= CURRENT_SCHEMA_VERSION;
32
+ }
33
+
34
+ /** 旧版本:低于当前但仍在支持范围内 → 需要沿迁移链升级 */
35
+ export function needsMigration(v: number): boolean {
36
+ return v >= MIN_SUPPORTED_SCHEMA_VERSION && v < CURRENT_SCHEMA_VERSION;
37
+ }
38
+
39
+ /** 过新版本:高于当前 → 必须升级本插件才能导入 */
40
+ export function isTooNew(v: number): boolean {
41
+ return v > CURRENT_SCHEMA_VERSION;
42
+ }
43
+
44
+ /** 是否可导入(当前版本或可迁移的旧版本) */
45
+ export function canImport(v: number): boolean {
46
+ return isCurrent(v) || needsMigration(v);
47
+ }
48
+
49
+ /** 版本的可读描述(报告/日志用) */
50
+ export function describeVersion(v: number): string {
51
+ if (isTooNew(v)) return `schema v${v}(高于当前 ${CURRENT_SCHEMA_VERSION},需升级插件)`;
52
+ if (needsMigration(v)) return `schema v${v}(旧版,将迁移到 v${CURRENT_SCHEMA_VERSION})`;
53
+ if (isCurrent(v)) return `schema v${v}(当前版本)`;
54
+ return `schema v${v}(低于最低支持 ${MIN_SUPPORTED_SCHEMA_VERSION},不受支持)`;
55
+ }
@@ -0,0 +1,180 @@
1
+ /**
2
+ * 凭据加密(规范 §7 / 设计 §7.3 / core `EncryptionProvider` 契约实现,零依赖 node:crypto)。
3
+ *
4
+ * 方案:scrypt(KDF)+ AES-256-GCM(AEAD),每次导出 salt/iv 全随机,密码绝不落盘。
5
+ * secrets.enc 二进制布局(设计 §7.3 细化):
6
+ *
7
+ * magic "DSC1"(4B) + version(1B) + salt(16B) + iv(12B) + authTag(16B) + ciphertext
8
+ *
9
+ * - magic/version 不符 → `UNSUPPORTED_FORMAT`(不是本插件产物 / 版本过新)
10
+ * - blob 内嵌参数与 manifest 的 EncryptionInfo 不一致 → `TAMPERED`(元数据被篡改)
11
+ * - GCM 认证失败(密码错误或密文被改) → `BAD_PASSWORD`(提示重输密码或核对备份来源)
12
+ *
13
+ * 与 core 契约的关系:
14
+ * - `createEncryptionProvider(password)` 返回 core `EncryptionProvider`(encrypt 无密码参数,
15
+ * 闭包持有导出密码;decrypt 用调用方传入的密码 → 支持「换密码解密」)。
16
+ * - 底层纯函数 encryptCredentials/decryptCredentials 对齐设计 §13.4,供直接调用与测试。
17
+ * - plaintext 语义:`.credentials.yaml` 文件原文(m5 经 HostContext.fs 文件级读取提供;
18
+ * ctx.credentials 永不回读值——研究报告 §3.2 硬约束),本模块只做字节级加解密。
19
+ */
20
+ import crypto from 'node:crypto';
21
+ import { promisify } from 'node:util';
22
+ import type { EncryptionInfo } from '../schema/types.ts';
23
+ import type { EncryptionProvider } from '../core/types.ts';
24
+
25
+ const scryptAsync = promisify(crypto.scrypt) as (
26
+ password: crypto.BinaryLike,
27
+ salt: crypto.BinaryLike,
28
+ keylen: number,
29
+ options: crypto.ScryptOptions,
30
+ ) => Promise<Buffer>;
31
+
32
+ export const SCHEMA_MAGIC = 'DSC1';
33
+ export const SCHEMA_VERSION = 1;
34
+ export const SCRYPT_PARAMS = { N: 16384, r: 8, p: 1, keyLength: 32 } as const;
35
+ export const SALT_LENGTH = 16;
36
+ export const IV_LENGTH = 12;
37
+ export const TAG_LENGTH = 16;
38
+ /** magic(4) + version(1) + salt(16) + iv(12) + authTag(16) */
39
+ export const HEADER_LENGTH = 4 + 1 + SALT_LENGTH + IV_LENGTH + TAG_LENGTH;
40
+
41
+ export type SecurityErrorCode = 'BAD_PASSWORD' | 'TAMPERED' | 'UNSUPPORTED_FORMAT';
42
+
43
+ export class SecurityError extends Error {
44
+ readonly code: SecurityErrorCode;
45
+ constructor(code: SecurityErrorCode, message: string) {
46
+ super(message);
47
+ this.name = 'SecurityError';
48
+ this.code = code;
49
+ }
50
+ }
51
+
52
+ /** KDF 参数值域校验(防 manifest 被篡改成超大 N 导致 DoS;非法即拒绝) */
53
+ export function validateKdfParams(params: unknown): params is EncryptionInfo['kdfParams'] {
54
+ if (params === null || typeof params !== 'object') return false;
55
+ const p = params as Record<string, unknown>;
56
+ if (typeof p['N'] !== 'number' || p['N'] < 2 ** 14 || p['N'] > 2 ** 20) return false;
57
+ if (typeof p['r'] !== 'number' || p['r'] < 1 || p['r'] > 32) return false;
58
+ if (typeof p['p'] !== 'number' || p['p'] < 1 || p['p'] > 32) return false;
59
+ return typeof p['keyLength'] === 'number' && p['keyLength'] === 32;
60
+ }
61
+
62
+ /** 派生密钥(scrypt,参数可来自 manifest;默认常量) */
63
+ export async function deriveKey(
64
+ password: string,
65
+ salt: Uint8Array,
66
+ params: EncryptionInfo['kdfParams'] = SCRYPT_PARAMS,
67
+ ): Promise<Buffer> {
68
+ return scryptAsync(password, salt, params.keyLength, { N: params.N, r: params.r, p: params.p });
69
+ }
70
+
71
+ /** 加密 .credentials.yaml 原文 → { blob, info }(info 直接进 manifest.security.encryption) */
72
+ export async function encryptCredentials(
73
+ plaintext: string,
74
+ password: string,
75
+ ): Promise<{ blob: Uint8Array; info: EncryptionInfo }> {
76
+ if (password === '') throw new SecurityError('BAD_PASSWORD', '加密密码不能为空');
77
+ const salt = crypto.randomBytes(SALT_LENGTH);
78
+ const iv = crypto.randomBytes(IV_LENGTH);
79
+ const key = await deriveKey(password, salt);
80
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
81
+ const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
82
+ const authTag = cipher.getAuthTag();
83
+
84
+ const header = Buffer.alloc(HEADER_LENGTH);
85
+ header.write(SCHEMA_MAGIC, 0, 'ascii');
86
+ header[4] = SCHEMA_VERSION;
87
+ salt.copy(header, 5);
88
+ iv.copy(header, 5 + SALT_LENGTH);
89
+ authTag.copy(header, 5 + SALT_LENGTH + IV_LENGTH);
90
+
91
+ const info: EncryptionInfo = {
92
+ algorithm: 'aes-256-gcm',
93
+ kdf: 'scrypt',
94
+ kdfParams: { ...SCRYPT_PARAMS },
95
+ salt: salt.toString('base64'),
96
+ iv: iv.toString('base64'),
97
+ authTag: authTag.toString('base64'),
98
+ version: SCHEMA_VERSION,
99
+ };
100
+ return { blob: Buffer.concat([header, encrypted]), info };
101
+ }
102
+
103
+ /** 解密 secrets.enc(authTag 校验失败抛 BAD_PASSWORD;元数据不一致抛 TAMPERED) */
104
+ export async function decryptCredentials(
105
+ blob: Uint8Array,
106
+ info: EncryptionInfo,
107
+ password: string,
108
+ ): Promise<string> {
109
+ if (blob.length < HEADER_LENGTH) {
110
+ throw new SecurityError('TAMPERED', 'secrets.enc 体积过小(截断或损坏)');
111
+ }
112
+ const magic = Buffer.from(blob.subarray(0, 4)).toString('ascii');
113
+ if (magic !== SCHEMA_MAGIC) {
114
+ throw new SecurityError('UNSUPPORTED_FORMAT', `未知文件格式 magic "${magic}",不是本插件产物`);
115
+ }
116
+ const version = blob[4]!;
117
+ if (version !== SCHEMA_VERSION) {
118
+ throw new SecurityError('UNSUPPORTED_FORMAT', `不支持的 secrets.enc 版本 ${version}`);
119
+ }
120
+ if (info.algorithm !== 'aes-256-gcm' || info.kdf !== 'scrypt' || !validateKdfParams(info.kdfParams)) {
121
+ throw new SecurityError('UNSUPPORTED_FORMAT', 'manifest 加密参数非法或不受支持');
122
+ }
123
+
124
+ const blobSalt = blob.subarray(5, 5 + SALT_LENGTH);
125
+ const blobIv = blob.subarray(5 + SALT_LENGTH, 5 + SALT_LENGTH + IV_LENGTH);
126
+ const blobTag = blob.subarray(5 + SALT_LENGTH + IV_LENGTH, HEADER_LENGTH);
127
+ if (
128
+ info.salt !== Buffer.from(blobSalt).toString('base64') ||
129
+ info.iv !== Buffer.from(blobIv).toString('base64') ||
130
+ info.authTag !== Buffer.from(blobTag).toString('base64')
131
+ ) {
132
+ throw new SecurityError('TAMPERED', 'secrets.enc 与 manifest 加密参数不一致(可能被篡改)');
133
+ }
134
+
135
+ let key: Buffer;
136
+ try {
137
+ key = await deriveKey(password, blobSalt, info.kdfParams);
138
+ } catch (err) {
139
+ throw new SecurityError('BAD_PASSWORD', `密钥派生失败: ${err instanceof Error ? err.message : String(err)}`);
140
+ }
141
+
142
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, blobIv);
143
+ decipher.setAuthTag(blobTag);
144
+ try {
145
+ const decrypted = Buffer.concat([
146
+ decipher.update(blob.subarray(HEADER_LENGTH)),
147
+ decipher.final(),
148
+ ]);
149
+ return decrypted.toString('utf8');
150
+ } catch {
151
+ throw new SecurityError('BAD_PASSWORD', '解密认证失败:密码错误或密文被篡改');
152
+ }
153
+ }
154
+
155
+ /** 密码强度校验(导出 UI 用;≥8 字符,建议 12+ 混合) */
156
+ export function validatePasswordStrength(password: string): { ok: boolean; message: string } {
157
+ if (password.length < 8) {
158
+ return { ok: false, message: '密码至少 8 个字符(建议 12+ 且含大小写与数字)' };
159
+ }
160
+ const variety = /[a-z]/.test(password) && /[A-Z]/.test(password) && /[0-9]/.test(password);
161
+ if (password.length < 12 && !variety) {
162
+ return { ok: true, message: '密码强度偏弱:建议 12+ 字符且混合大小写与数字' };
163
+ }
164
+ return { ok: true, message: '' };
165
+ }
166
+
167
+ /**
168
+ * 创建 core `EncryptionProvider`(对齐 core/types.ts 契约)。
169
+ * encrypt 使用闭包持有密码;decrypt 使用调用方传入的密码(支持换密码解密)。
170
+ */
171
+ export function createEncryptionProvider(password: string): EncryptionProvider {
172
+ return {
173
+ async encrypt(plaintext: string): Promise<{ blob: Uint8Array; info: EncryptionInfo }> {
174
+ return encryptCredentials(plaintext, password);
175
+ },
176
+ async decrypt(blob: Uint8Array, info: EncryptionInfo, pw: string): Promise<string> {
177
+ return decryptCredentials(blob, info, pw);
178
+ },
179
+ };
180
+ }