@logictan/dsh-config-manager 0.1.60 → 0.1.62

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 (77) hide show
  1. package/lib/adapters/credentials.d.ts +1 -1
  2. package/lib/adapters/index.d.ts +1 -1
  3. package/lib/adapters/index.js +1 -1
  4. package/lib/adapters/mcp.js +4 -4
  5. package/lib/adapters/plugins.d.ts +0 -1
  6. package/lib/adapters/plugins.js +35 -19
  7. package/lib/adapters/prompts.js +6 -6
  8. package/lib/adapters/test-helpers.d.ts +27 -0
  9. package/lib/adapters/test-helpers.js +40 -0
  10. package/lib/client.d.ts +1 -13
  11. package/lib/client.js +297 -304
  12. package/lib/core/analyzer.js +5 -4
  13. package/lib/core/backup.js +5 -3
  14. package/lib/core/exporter.d.ts +1 -4
  15. package/lib/core/exporter.js +14 -45
  16. package/lib/core/importer.d.ts +1 -1
  17. package/lib/core/index.d.ts +1 -1
  18. package/lib/core/messages.d.ts +1 -5
  19. package/lib/core/messages.js +2 -10
  20. package/lib/core/patch-layers.d.ts +32 -0
  21. package/lib/core/patch-layers.js +39 -0
  22. package/lib/core/rollback.js +4 -4
  23. package/lib/core/types.d.ts +8 -12
  24. package/lib/index.d.ts +23 -14
  25. package/lib/index.js +31 -92
  26. package/lib/security/index.d.ts +1 -3
  27. package/lib/security/index.js +1 -3
  28. package/lib/sync/transport.d.ts +2 -2
  29. package/lib/sync/transport.js +1 -1
  30. package/lib/ui/i18n.d.ts +0 -3
  31. package/lib/ui/i18n.js +0 -6
  32. package/lib/ui/report.js +1 -2
  33. package/lib/ui/test-helpers.d.ts +0 -6
  34. package/lib/ui/test-helpers.js +1 -5
  35. package/lib/ui/types.d.ts +0 -13
  36. package/lib/utils/env-lock.d.ts +16 -0
  37. package/lib/utils/env-lock.js +27 -6
  38. package/package.json +2 -2
  39. package/src/adapters/credentials.ts +1 -1
  40. package/src/adapters/index.ts +1 -1
  41. package/src/adapters/mcp.ts +4 -4
  42. package/src/adapters/patch-layers.test.ts +201 -0
  43. package/src/adapters/plugins.test.ts +13 -5
  44. package/src/adapters/plugins.ts +33 -17
  45. package/src/adapters/prompts.ts +6 -6
  46. package/src/adapters/test-helpers.ts +48 -0
  47. package/src/client/api.ts +1 -10
  48. package/src/client/config-manager.module.css +1 -1
  49. package/src/client/run-store.test.ts +1 -1
  50. package/src/client/sync/SyncSettingsView.tsx +5 -5
  51. package/src/client/sync/sync-api.ts +0 -1
  52. package/src/client/sync/sync-locales.ts +0 -1
  53. package/src/core/analyzer.ts +5 -4
  54. package/src/core/backup.ts +5 -3
  55. package/src/core/config-lifecycle.test.ts +4 -1
  56. package/src/core/exporter.ts +15 -47
  57. package/src/core/importer.ts +1 -1
  58. package/src/core/index.ts +1 -1
  59. package/src/core/messages.ts +2 -10
  60. package/src/core/patch-layers.ts +42 -0
  61. package/src/core/rollback.ts +4 -4
  62. package/src/core/smoke.test.ts +5 -5
  63. package/src/core/types.ts +8 -12
  64. package/src/index.facade.test.ts +25 -2
  65. package/src/index.ts +30 -104
  66. package/src/security/index.ts +1 -3
  67. package/src/security/security.test.ts +0 -361
  68. package/src/sync/transport.ts +3 -3
  69. package/src/ui/i18n.ts +0 -6
  70. package/src/ui/report.ts +1 -2
  71. package/src/ui/test-helpers.ts +3 -7
  72. package/src/ui/types.ts +0 -10
  73. package/src/utils/env-lock.test.ts +29 -0
  74. package/src/utils/env-lock.ts +27 -6
  75. package/lib/security/encryption.d.ts +0 -85
  76. package/lib/security/encryption.js +0 -279
  77. package/src/security/encryption.ts +0 -335
@@ -408,8 +408,9 @@ export class Analyzer {
408
408
  if (opts.confirm !== true) {
409
409
  throw new ImportNotConfirmedError(this.msg);
410
410
  }
411
- // 10b. 加密不变量:加密备份必须已成功解密(decryptedCredentials 由宿主用备份密码
412
- // 解开 security/secrets.enc 后注入)。未解密(undefined)一律拒绝执行——
411
+ // 10b. 旧版加密备份的兼容不变量:本插件已无加密层,但历史产物仍可能带
412
+ // manifest.security.encrypted=true。这类备份的凭据必须由宿主解密后注入
413
+ // (decryptedCredentials);未注入(undefined)一律拒绝执行——
413
414
  // 不允许把加密凭据静默降级为「缺凭据照常导入」,否则加密备份与普通备份无区别。
414
415
  if (bundle.manifest.security.encrypted && opts.decryptedCredentials === undefined) {
415
416
  throw new Error(this.msg('import.encryptedPasswordRequired'));
@@ -607,8 +608,8 @@ export class Analyzer {
607
608
  .map((s) => s.ref);
608
609
  // M1:导入成功 → 快照标记 done(元数据写失败只告警,不改变导入结论)
609
610
  await this.markSnapshotStatus(snapshot.id, 'done');
610
- // F1 vault 回填:导出时敏感文件(.credentials.yaml 等)明文未进备份(includeSecrets=false
611
- // 时镜像到 <dataDir>/vault),导入成功后从本机 vault 回填 $DSH_HOME;vault 缺失
611
+ // F1 vault 回填:includeSecrets=false 的导出会把敏感文件(.credentials.yaml 等)镜像到
612
+ // <dataDir>/vault 而不进备份,导入成功后从本机 vault 回填 $DSH_HOME;vault 缺失
612
613
  // (跨机恢复 / 从未镜像过)记入警告提示用户重填。尽力而为:失败仅警告,不影响导入结论。
613
614
  try {
614
615
  const vaultDataDir = path.join(this.ctx.homeDir, 'dsh-config-manager');
@@ -9,6 +9,7 @@ import fs from 'node:fs/promises';
9
9
  import path from 'node:path';
10
10
  import crypto from 'node:crypto';
11
11
  import { parseJsonSafe } from '../utils/json.js';
12
+ import { parsePatchLayerKey } from './patch-layers.js';
12
13
  import { sha256Hex } from '../utils/hashing.js';
13
14
  import { normalizePath } from '../utils/paths.js';
14
15
  import { atomicWriteFile } from '../utils/atomic-write.js';
@@ -180,10 +181,11 @@ async function engineSnapshotEntry(ctx, target) {
180
181
  copiedTo: `blobs/${crypto.randomUUID()}`,
181
182
  };
182
183
  }
183
- // patchLine:从组合 patch 文件读取原行(file 为必填的 file 字段约定为 'cordis.patch.yml')
184
- const file = 'cordis.patch.yml';
184
+ // patchLine:ref 是层限定复合键(<file>#<lineId>),据此到**对应层**读原行。
185
+ // 旧快照的裸 lineId 由 parsePatchLayerKey 兼容为 home 层。
186
+ const { file, lineId } = parsePatchLayerKey(target.ref);
185
187
  const lines = await ctx.patchFile.readPatchLines(file);
186
- const line = lines.find((l) => l.lineId === target.ref);
188
+ const line = lines.find((l) => l.lineId === lineId);
187
189
  return { kind: 'patchLine', adapter: target.adapter, ref: target.ref, before: line?.raw ?? null, existed: line !== undefined };
188
190
  }
189
191
  case 'skills':
@@ -1,6 +1,6 @@
1
1
  import type { MsgFunc } from './messages.ts';
2
2
  import type { Manifest } from '../schema/types.ts';
3
- import type { ConfigAdapter, EncryptionProvider, ExportOptions, ExportReport, HostContext, SecretScanner } from './types.ts';
3
+ import type { ConfigAdapter, ExportOptions, ExportReport, HostContext, SecretScanner } from './types.ts';
4
4
  /** m1:每导出一个分区前的进度回调信息(Host 侧 run 状态更新用;section = adapter id) */
5
5
  export interface SectionProgress {
6
6
  section: string;
@@ -14,8 +14,6 @@ export interface ExporterOptions {
14
14
  adapters: ConfigAdapter[];
15
15
  /** Secret 扫描器;缺省用字段名黑名单剥离(m4 可注入强化版) */
16
16
  scanner?: SecretScanner;
17
- /** 加密提供者(m4 用 node:crypto 实现);includeSecrets 时必填;提供时备份标记 encrypted=true */
18
- encryption?: EncryptionProvider | null;
19
17
  /** 插件自身版本(manifest.exporter.version) */
20
18
  exporterVersion?: string;
21
19
  now?: () => Date;
@@ -45,7 +43,6 @@ export declare class Exporter {
45
43
  private readonly ctx;
46
44
  private readonly adapters;
47
45
  private readonly scanner;
48
- private readonly encryption;
49
46
  private readonly exporterVersion;
50
47
  private readonly now;
51
48
  private readonly msg;
@@ -3,11 +3,9 @@
3
3
  * adapter 收集各分区 → Secret 过滤 → manifest → checksum → ZIP。
4
4
  *
5
5
  * 安全不变量:
6
- * - Secret 值默认永不进入导出数据(结构化分区逐一过 SecretScanner);
7
- * - includeSecrets=true 必须注入 EncryptionProvider(m4 实现),否则拒绝导出;
8
- * - 注入 EncryptionProvider 时备份标记为加密(encrypted=true):includeSecrets=false
9
- * 时 secrets.enc 加密空内容占位,备份仍需要密码导入,但不含任何凭据值;
10
- * - 加密密码/秘密值绝不写入 manifest 与日志。
6
+ * - Secret 值默认永不进入导出数据(结构化分区逐一过 SecretScanner 剥离值);
7
+ * - 备份恒为明文(encrypted=false / encryption=null):本插件不再有加密层;
8
+ * - 秘密值绝不写入 manifest 与日志。
11
9
  */
12
10
  import fs from 'node:fs/promises';
13
11
  import path from 'node:path';
@@ -138,7 +136,6 @@ export class Exporter {
138
136
  ctx;
139
137
  adapters;
140
138
  scanner;
141
- encryption;
142
139
  exporterVersion;
143
140
  now;
144
141
  msg;
@@ -148,7 +145,6 @@ export class Exporter {
148
145
  this.ctx = opts.ctx;
149
146
  this.adapters = opts.adapters;
150
147
  this.scanner = opts.scanner ?? defaultSecretScanner();
151
- this.encryption = opts.encryption ?? null;
152
148
  this.exporterVersion = opts.exporterVersion ?? '0.1.0';
153
149
  this.now = opts.now ?? (() => new Date());
154
150
  this.msg = opts.msg ?? msgOf(opts.ctx);
@@ -161,9 +157,6 @@ export class Exporter {
161
157
  */
162
158
  async export(options) {
163
159
  const { includeSecrets, only } = options;
164
- if (includeSecrets && !this.encryption) {
165
- throw new Error(this.msg('export.encryptionRequired'));
166
- }
167
160
  // 1. 选定分区(only 过滤 + 默认包含)
168
161
  const selected = this.adapters
169
162
  .filter((a) => (only === undefined ? a.defaultIncluded : only.includes(a.id)))
@@ -172,6 +165,8 @@ export class Exporter {
172
165
  const sections = [];
173
166
  const warnings = [];
174
167
  const redactedHits = [];
168
+ /** 文件类分区实扫到的命中数:只有它能让 containsSecrets 为真(结构化分区已被剥离) */
169
+ let fileSectionSecretHits = 0;
175
170
  const included = [];
176
171
  const excluded = this.adapters.filter((a) => !selected.includes(a.id)).map((a) => a.id);
177
172
  // m1 埋点:每导出一个分区前上报真实进度(onSection 抛错不得中断导出)
@@ -210,6 +205,7 @@ export class Exporter {
210
205
  if (fileHits.length > 0) {
211
206
  // redactedHits 是**报告统计**通道(非告警通道):仍计入全量命中(含同一文件的多行/多形态命中)。
212
207
  redactedHits.push(...fileHits);
208
+ fileSectionSecretHits += fileHits.length;
213
209
  // 告警按**文件**去重(G-09/H1):同一路径只告警一次。
214
210
  // 修复前按 hit 计数,同一行同时命中「字段名」与「值形状」会产出两条同路径告警,
215
211
  // 使少数文件就吃满上限,导致含真实明文凭据的其它文件被静默淹没(实测 redactedHits=8 而 5 条告警全属一个文件)。
@@ -239,12 +235,13 @@ export class Exporter {
239
235
  included.push({ section: adapter.id, counts: section.counts });
240
236
  warnings.push(...section.warnings);
241
237
  }
242
- // 4. 组装 ZIP 条目(JSON 分区 + 文件类分区 + secrets.enc + checksums + manifest)
238
+ // 4. 组装 ZIP 条目(JSON 分区 + 文件类分区 + checksums + manifest)
243
239
  const entries = [];
244
240
  const sectionFlags = buildSectionFlags(sections);
245
- let containsSecrets = false;
246
- let encrypted = false;
247
- let encryption = null;
241
+ // 备份恒为明文:本插件不再有加密层。containsSecrets 只认「文件类分区实扫到的命中」——
242
+ // 结构化分区的敏感值已被 scanner 剥离,不构成「含秘密」;文件类分区只报告不改写,
243
+ // 命中即代表归档里确有明文,必须如实标注。
244
+ const containsSecrets = fileSectionSecretHits > 0;
248
245
  for (const section of sections) {
249
246
  if (isFileSection(section.sectionId)) {
250
247
  const prefix = SECTION_FILE_PREFIXES[section.sectionId];
@@ -262,34 +259,8 @@ export class Exporter {
262
259
  data: Buffer.from(stringifyJsonSafe(section.data, { space: 2 }), 'utf8'),
263
260
  });
264
261
  }
265
- // secrets.enc:有加密提供者即生成。includeSecrets=true 时加密真实的凭据原文;
266
- // 只勾选加密(不导出密钥)时加密空内容占位,备份仍标记 encrypted(导入需密码),
267
- // 但绝不把凭据值放进去(containsSecrets 保持 false;安全不变量不破)。
268
- if (this.encryption) {
269
- const credentialsFile = path.join(this.ctx.homeDir, '.credentials.yaml');
270
- let plaintext;
271
- if (includeSecrets) {
272
- try {
273
- const raw = await this.ctx.fs.readFile(credentialsFile);
274
- plaintext = Buffer.from(raw).toString('utf8');
275
- }
276
- catch (err) {
277
- warnings.push(this.msg('export.credentialsReadFailed', { reason: err instanceof Error ? err.message : String(err) }));
278
- plaintext = '';
279
- }
280
- }
281
- else {
282
- plaintext = '';
283
- }
284
- const result = await this.encryption.encrypt(plaintext);
285
- entries.push({ name: 'security/secrets.enc', data: result.blob });
286
- encryption = result.info;
287
- containsSecrets = includeSecrets && plaintext !== '';
288
- encrypted = true;
289
- }
290
262
  // 4b. 文件级 vault(includeSecrets=false:敏感文件明文不进归档 → 镜像到本机 vault)。
291
- // 尽力而为:任何失败仅记警告,不中断导出。includeSecrets=true 时秘密已加密进归档,
292
- // 无需镜像(vault 只服务于「明文不进备份」的本机留存场景)。
263
+ // 尽力而为:任何失败仅记警告,不中断导出。
293
264
  let vaultRefreshed = 0;
294
265
  if (!includeSecrets) {
295
266
  try {
@@ -317,8 +288,8 @@ export class Exporter {
317
288
  arch: this.ctx.arch,
318
289
  sections: sectionFlags,
319
290
  containsSecrets,
320
- encrypted,
321
- encryption,
291
+ encrypted: false,
292
+ encryption: null,
322
293
  exportedAt: this.now().toISOString(),
323
294
  });
324
295
  entries.push({ name: MANIFEST_FILE, data: Buffer.from(stringifyJsonSafe(manifest, { space: 2 }), 'utf8') });
@@ -333,7 +304,6 @@ export class Exporter {
333
304
  sections: Object.keys(sectionFlags).filter((k) => sectionFlags[k]),
334
305
  redactedFields: redactedHits.length,
335
306
  containsSecrets,
336
- encrypted,
337
307
  vaultRefreshed,
338
308
  });
339
309
  const report = {
@@ -342,7 +312,6 @@ export class Exporter {
342
312
  security: {
343
313
  secretsExcluded: !includeSecrets,
344
314
  containsSecrets,
345
- encrypted,
346
315
  redactedHits: redactedHits.length,
347
316
  vaultRefreshed,
348
317
  },
@@ -30,7 +30,7 @@ export interface ExecuteOptions {
30
30
  confirm: boolean;
31
31
  /** 用户补录的秘密值(仅内存) */
32
32
  secretInputs?: Record<string, string>;
33
- /** 加密备份解密结果(仅内存;解密必须经 m4 encryption provider) */
33
+ /** 旧版加密备份的解密结果(仅内存;宿主解密后注入,本插件不再产生加密备份) */
34
34
  decryptedCredentials?: Map<string, string>;
35
35
  /** 任一项失败立即整体回滚(默认 false:单项失败如实记录并继续其余项) */
36
36
  rollbackOnError?: boolean;
@@ -11,4 +11,4 @@ export { computeCompatibility, describeCompatibility, describeSchemaStatus, vali
11
11
  export { ImportNotConfirmedError, ImportFailedError, } from './types.ts';
12
12
  export type * from './types.ts';
13
13
  export { MigrationStore, sanitizeEntry, queryHistory, summarizeHistory, renderExport, parseHistoryQuery, isValidMigrationKind, redactHistoryText, makeHistoryFilename, isHistoryBasename, MIGRATION_HISTORY_DIR, DEFAULT_MIGRATION_RETENTION, MIGRATION_HISTORY_SCHEMA_VERSION, type MigrationKind, type MigrationResult, type MigrationHistoryEntry, type StoredMigrationHistoryEntry, type MigrationQuery, type MigrationHistoryStats, type ExportFormat, type ReadMigrationResult, type AppendResult, type MigrationIo, type MigrationStoreOptions, type MigrationSource, } from './migration-history.ts';
14
- export type { ExportOptions, ExportSection, ValidationResult, HostContext, SettingsFacade, CredentialsFacade, PluginsFacade, WorkspaceFacade, PatchFileFacade, FileSystemFacade, NamespaceInfo, PluginInfo, ConfigAdapter, Portability, SecretScanner, SensitiveHit, EncryptionProvider, PlanItem, PlanItemKind, ItemResolution, GlobalConflictStrategy, ImportAnalysis, ImportDecisions, ImportPlan, ImportResult, ExecutedItem, ImportContext, SnapshotTarget, SnapshotEntry, Snapshot, SnapshotStore, RollbackReport, PathMapping, PathIssue, CompatibilityInput, CompatibilityScore, ExportReport, ApplyResult, ConflictDecision, } from './types.ts';
14
+ export type { ExportOptions, ExportSection, ValidationResult, HostContext, SettingsFacade, CredentialsFacade, PluginsFacade, WorkspaceFacade, PatchFileFacade, FileSystemFacade, NamespaceInfo, PluginInfo, ConfigAdapter, Portability, SecretScanner, SensitiveHit, PlanItem, PlanItemKind, ItemResolution, GlobalConflictStrategy, ImportAnalysis, ImportDecisions, ImportPlan, ImportResult, ExecutedItem, ImportContext, SnapshotTarget, SnapshotEntry, Snapshot, SnapshotStore, RollbackReport, PathMapping, PathIssue, CompatibilityInput, CompatibilityScore, ExportReport, ApplyResult, ConflictDecision, } from './types.ts';
@@ -15,9 +15,7 @@
15
15
  import type { MsgFunc } from './msg-types.ts';
16
16
  export type { MsgFunc, MsgParams } from './msg-types.ts';
17
17
  export declare const zh: {
18
- readonly 'export.encryptionRequired': '导出包含秘密需要注入 EncryptionProvider(m4 实现),拒绝明文导出秘密';
19
18
  readonly 'export.sectionFailed': '分区 {adapter} 导出失败: {reason}';
20
- readonly 'export.credentialsReadFailed': '读取凭据文件失败,跳过秘密导出: {reason}';
21
19
  readonly 'export.vaultRefreshed': '凭据明文未进入备份,已镜像到本机 vault({count} 个文件,恢复时可回填)';
22
20
  readonly 'export.vaultRefreshSkipped': 'vault 镜像跳过 {rel}: {reason}';
23
21
  readonly 'export.vaultRefreshFailed': 'vault 镜像刷新失败(不影响导出): {reason}';
@@ -48,9 +46,7 @@ export declare const zh: {
48
46
  readonly 'import.secretMissingDesc': '凭据 {ref} 需要补录';
49
47
  readonly 'import.secretNotProvided': '凭据未提供,需补录';
50
48
  readonly 'import.notConfirmed': '导入未确认:必须显式 confirm 后才允许修改任何数据';
51
- readonly 'import.encryptedPasswordRequired': '该备份已加密,必须提供解密密码才能导入(拒绝无密码导入)';
52
- readonly 'import.encryptedPasswordWrong': '解密密码错误,请重试';
53
- readonly 'import.notEncryptedContainer': '该文件不是加密备份容器,无法解锁';
49
+ readonly 'import.encryptedPasswordRequired': '该备份含上游历史加密凭据(security.encrypted=true),本插件无解密能力:须由宿主解密后注入凭据(decryptedCredentials)才能导入';
54
50
  readonly 'import.userSkipped': '用户跳过(导入中点击「跳过当前插件」)';
55
51
  readonly 'import.userSkippedDetail': '插件 {name} 已由用户跳过,未安装';
56
52
  readonly 'import.vaultRestored': '凭据文件 {rel} 已从本机 vault 回填';
@@ -1,8 +1,6 @@
1
1
  export const zh = {
2
2
  // ---------- 导出 ----------
3
- 'export.encryptionRequired': '导出包含秘密需要注入 EncryptionProvider(m4 实现),拒绝明文导出秘密',
4
3
  'export.sectionFailed': '分区 {adapter} 导出失败: {reason}',
5
- 'export.credentialsReadFailed': '读取凭据文件失败,跳过秘密导出: {reason}',
6
4
  'export.vaultRefreshed': '凭据明文未进入备份,已镜像到本机 vault({count} 个文件,恢复时可回填)',
7
5
  'export.vaultRefreshSkipped': 'vault 镜像跳过 {rel}: {reason}',
8
6
  'export.vaultRefreshFailed': 'vault 镜像刷新失败(不影响导出): {reason}',
@@ -34,9 +32,7 @@ export const zh = {
34
32
  'import.secretMissingDesc': '凭据 {ref} 需要补录',
35
33
  'import.secretNotProvided': '凭据未提供,需补录',
36
34
  'import.notConfirmed': '导入未确认:必须显式 confirm 后才允许修改任何数据',
37
- 'import.encryptedPasswordRequired': '该备份已加密,必须提供解密密码才能导入(拒绝无密码导入)',
38
- 'import.encryptedPasswordWrong': '解密密码错误,请重试',
39
- 'import.notEncryptedContainer': '该文件不是加密备份容器,无法解锁',
35
+ 'import.encryptedPasswordRequired': '该备份含上游历史加密凭据(security.encrypted=true),本插件无解密能力:须由宿主解密后注入凭据(decryptedCredentials)才能导入',
40
36
  'import.userSkipped': '用户跳过(导入中点击「跳过当前插件」)',
41
37
  'import.userSkippedDetail': '插件 {name} 已由用户跳过,未安装',
42
38
  'import.vaultRestored': '凭据文件 {rel} 已从本机 vault 回填',
@@ -297,9 +293,7 @@ export const zh = {
297
293
  };
298
294
  export const en = {
299
295
  // ---------- export ----------
300
- 'export.encryptionRequired': 'Encrypted export requires an EncryptionProvider (m4); refusing to export secrets in plaintext',
301
296
  'export.sectionFailed': 'Section {adapter} export failed: {reason}',
302
- 'export.credentialsReadFailed': 'Failed to read the credentials file; secrets export skipped: {reason}',
303
297
  'export.vaultRefreshed': 'Credential plaintext did not enter the backup; mirrored to the local vault ({count} file(s), available for restore backfill)',
304
298
  'export.vaultRefreshSkipped': 'Vault mirror skipped {rel}: {reason}',
305
299
  'export.vaultRefreshFailed': 'Vault mirror refresh failed (export unaffected): {reason}',
@@ -331,9 +325,7 @@ export const en = {
331
325
  'import.secretMissingDesc': 'Credential {ref} needs to be re-entered',
332
326
  'import.secretNotProvided': 'Credential not provided; requires re-entry',
333
327
  'import.notConfirmed': 'Import not confirmed: explicit confirm is required before any data is modified',
334
- 'import.encryptedPasswordRequired': 'This backup is encrypted; the decryption password is required to import it (refusing password-less import)',
335
- 'import.encryptedPasswordWrong': 'Wrong decryption password, please try again',
336
- 'import.notEncryptedContainer': 'This file is not an encrypted backup container and cannot be unlocked',
328
+ 'import.encryptedPasswordRequired': 'This backup carries upstream-encrypted credentials (security.encrypted=true) and this plugin cannot decrypt them: a host must decrypt and inject them (decryptedCredentials) before import',
337
329
  'import.userSkipped': 'Skipped by user (clicked "Skip current plugin" during import)',
338
330
  'import.userSkippedDetail': 'Plugin {name} was skipped by the user and not installed',
339
331
  'import.vaultRestored': 'Credential file {rel} backfilled from the local vault',
@@ -0,0 +1,32 @@
1
+ /**
2
+ * cordis patch 层的寻址契约(唯一真源)。
3
+ *
4
+ * 宿主组装 patch 栈的顺序是 `bundle → profile → home → --patch`(后者覆盖前者),两层分工不同:
5
+ * - home 层 `$DSH_HOME/cordis.patch.yml`:全机偏好,对每个 profile 生效;
6
+ * - profile 层 `$DSH_HOME/profiles/<profile>/cordis.patch.yml`:该 profile 专属(端口、trustedHosts、
7
+ * 挂载行…),换机恢复时必须跟着走。
8
+ *
9
+ * 两层的**持久化标识**是这里的两个逻辑 token,而不是相对路径:同步快照会跨机器、
10
+ * 跨 profile 导入,相对路径会把源机的 profile 名带过去。home 层 token 与历史快照里的
11
+ * `PatchLine.file` 取值逐字相同(存量快照零改动即可解析回 home 层)。
12
+ *
13
+ * 层限定复合键 `<file>#<lineId>`:两层可以存在同名 `lineId`,而计划项 id / `target.ref` /
14
+ * 快照条目必须唯一指向「哪一层的哪一行」,否则应用与回滚会写错层。两个 token 与 lineId 都不含
15
+ * `#`(后者由 `readPatchLines` 保证非空字符串,此处不额外校验),分隔符无歧义。
16
+ */
17
+ /** home 层 token(`$DSH_HOME/cordis.patch.yml`);取值与历史快照的 file 字段一致。 */
18
+ export declare const HOME_PATCH_FILE = "cordis.patch.yml";
19
+ /** profile 层 token(`$DSH_HOME/profiles/<profile>/cordis.patch.yml`)。 */
20
+ export declare const PROFILE_PATCH_FILE = "profile:cordis.patch.yml";
21
+ /** 层限定复合键:`<file>#<lineId>`。 */
22
+ export declare function patchLayerKey(file: string, lineId: string): string;
23
+ /**
24
+ * 解析层限定复合键。
25
+ *
26
+ * 兼容分支:旧快照的 `target.ref` / `SnapshotEntry.ref` 是裸 `lineId`(当时只有 home 层),
27
+ * 不含 `#` 即视作 home 层。lineId 本身不含 `#`,因此该分支无歧义。
28
+ */
29
+ export declare function parsePatchLayerKey(ref: string): {
30
+ file: string;
31
+ lineId: string;
32
+ };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * cordis patch 层的寻址契约(唯一真源)。
3
+ *
4
+ * 宿主组装 patch 栈的顺序是 `bundle → profile → home → --patch`(后者覆盖前者),两层分工不同:
5
+ * - home 层 `$DSH_HOME/cordis.patch.yml`:全机偏好,对每个 profile 生效;
6
+ * - profile 层 `$DSH_HOME/profiles/<profile>/cordis.patch.yml`:该 profile 专属(端口、trustedHosts、
7
+ * 挂载行…),换机恢复时必须跟着走。
8
+ *
9
+ * 两层的**持久化标识**是这里的两个逻辑 token,而不是相对路径:同步快照会跨机器、
10
+ * 跨 profile 导入,相对路径会把源机的 profile 名带过去。home 层 token 与历史快照里的
11
+ * `PatchLine.file` 取值逐字相同(存量快照零改动即可解析回 home 层)。
12
+ *
13
+ * 层限定复合键 `<file>#<lineId>`:两层可以存在同名 `lineId`,而计划项 id / `target.ref` /
14
+ * 快照条目必须唯一指向「哪一层的哪一行」,否则应用与回滚会写错层。两个 token 与 lineId 都不含
15
+ * `#`(后者由 `readPatchLines` 保证非空字符串,此处不额外校验),分隔符无歧义。
16
+ */
17
+ /** home 层 token(`$DSH_HOME/cordis.patch.yml`);取值与历史快照的 file 字段一致。 */
18
+ export const HOME_PATCH_FILE = 'cordis.patch.yml';
19
+ /** profile 层 token(`$DSH_HOME/profiles/<profile>/cordis.patch.yml`)。 */
20
+ export const PROFILE_PATCH_FILE = 'profile:cordis.patch.yml';
21
+ /** 层限定复合键的分隔符(两个层 token 与 lineId 都不含它)。 */
22
+ const PATCH_LAYER_SEPARATOR = '#';
23
+ /** 层限定复合键:`<file>#<lineId>`。 */
24
+ export function patchLayerKey(file, lineId) {
25
+ return `${file}${PATCH_LAYER_SEPARATOR}${lineId}`;
26
+ }
27
+ /**
28
+ * 解析层限定复合键。
29
+ *
30
+ * 兼容分支:旧快照的 `target.ref` / `SnapshotEntry.ref` 是裸 `lineId`(当时只有 home 层),
31
+ * 不含 `#` 即视作 home 层。lineId 本身不含 `#`,因此该分支无歧义。
32
+ */
33
+ export function parsePatchLayerKey(ref) {
34
+ const at = ref.indexOf(PATCH_LAYER_SEPARATOR);
35
+ if (at < 0)
36
+ return { file: HOME_PATCH_FILE, lineId: ref };
37
+ return { file: ref.slice(0, at), lineId: ref.slice(at + 1) };
38
+ }
39
+ //# sourceMappingURL=patch-layers.js.map
@@ -8,6 +8,7 @@
8
8
  * - settings 回滚仍走 expectedRevision 乐观锁,避免覆盖导入后用户的新修改。
9
9
  */
10
10
  import { resolveFileTarget } from './backup.js';
11
+ import { parsePatchLayerKey } from './patch-layers.js';
11
12
  import { msgOf } from './messages.js';
12
13
  /** 逆序补偿单条快照条目;返回 null=成功,否则为失败原因 */
13
14
  async function compensateOne(entry, ctx, store) {
@@ -57,10 +58,9 @@ async function compensateOne(entry, ctx, store) {
57
58
  }
58
59
  case 'patchLine': {
59
60
  try {
60
- // 引擎只管理 profile 的 cordis.patch.yml(backup.ts 的 patchLine 快照只记 lineId 作 ref,
61
- // 不含文件编码)——回滚固定写回该文件;切勿把 lineId 当文件名(否则 patchPath 抛「仅支持管理」)。
62
- const file = 'cordis.patch.yml';
63
- const lineId = entry.ref;
61
+ // 快照条目的 ref 是层限定复合键(<file>#<lineId>)——必须写回**同一层**,
62
+ // 否则 profile 层的原值会被写进 home 层。旧快照的裸 lineId 兼容为 home 层。
63
+ const { file, lineId } = parsePatchLayerKey(entry.ref);
64
64
  await ctx.patchFile.applyPatchChanges(file, [
65
65
  { lineId, raw: entry.before, action: entry.before === null ? 'remove' : 'update' },
66
66
  ]);
@@ -5,14 +5,18 @@
5
5
  * HostContext 是 m3 定义、m5 实现的 DSH Service 门面(研究报告 §3.2 的叶子方法最小集),
6
6
  * 测试用内存 mock 即可驱动完整导出→导入往返。
7
7
  */
8
- import type { EncryptionInfo, Manifest, SectionId, WorkspaceRecord } from '../schema/types.ts';
8
+ import type { Manifest, SectionId, WorkspaceRecord } from '../schema/types.ts';
9
9
  import type { TombstoneKind } from '../schema/tombstones.ts';
10
10
  import type { MutationLockPort } from '../utils/env-lock.ts';
11
11
  import type { RecursiveListing } from '../utils/recursive-walk.ts';
12
12
  import type { Logger } from '../utils/logger.ts';
13
13
  import type { MsgFunc } from './messages.ts';
14
14
  export interface ExportOptions {
15
- /** 是否包含真实秘密(必须配合 encryption 提供者;缺省 false = 只导状态) */
15
+ /**
16
+ * 是否把真实秘密写入备份。本插件不再有加密层(备份恒为明文),结构化分区的秘密值
17
+ * 始终由 SecretScanner 剥离,故本开关当前的实际作用只剩「是否刷新本机 vault 镜像」:
18
+ * false → 导出后把敏感文件镜像到本机 vault(明文不进归档)。
19
+ */
16
20
  includeSecrets: boolean;
17
21
  /** 仅导出指定分区(缺省 = 全部默认包含分区) */
18
22
  only?: SectionId[];
@@ -249,7 +253,7 @@ export interface ImportAnalysis {
249
253
  item: string;
250
254
  dependency: string;
251
255
  }[];
252
- /** 备份是否加密(manifest.security.encrypted):加密备份的凭据必须用解密密码恢复 */
256
+ /** 旧版加密备份标记(manifest.security.encrypted):本插件不再产生,仅历史产物为 true */
253
257
  encrypted: boolean;
254
258
  }
255
259
  export interface ImportDecisions {
@@ -307,6 +311,7 @@ export interface ImportContext {
307
311
  resolutions: Record<string, ItemResolution>;
308
312
  /** 用户补录的秘密值(仅内存,永不落盘/日志) */
309
313
  secretInputs: Record<string, string>;
314
+ /** 旧版加密备份的解密结果(仅内存;宿主解密后注入) */
310
315
  decryptedCredentials?: Map<string, string>;
311
316
  log: Logger;
312
317
  /** 消息翻译器(analyzer 注入;适配器用它生成计划项描述/校验/结果消息) */
@@ -461,7 +466,6 @@ export interface ExportReport {
461
466
  security: {
462
467
  secretsExcluded: boolean;
463
468
  containsSecrets: boolean;
464
- encrypted: boolean;
465
469
  redactedHits: number;
466
470
  /** 本次导出镜像到本机 vault 的敏感文件数(文件级 vault;0 或缺失 = 未镜像) */
467
471
  vaultRefreshed?: number;
@@ -491,14 +495,6 @@ export interface ConfigAdapter<TSection = unknown> {
491
495
  /** 可选:针对本 adapter 的补偿动作 */
492
496
  rollback?(entries: SnapshotEntry[], ctx: HostContext): Promise<void>;
493
497
  }
494
- export interface EncryptionProvider {
495
- encrypt(plaintext: string): Promise<{
496
- blob: Uint8Array;
497
- info: EncryptionInfo;
498
- }>;
499
- /** authTag 校验失败必须抛错 */
500
- decrypt(blob: Uint8Array, info: EncryptionInfo, password: string): Promise<string>;
501
- }
502
498
  /** 导入被确认前拒绝执行(安全阀) */
503
499
  export declare class ImportNotConfirmedError extends Error {
504
500
  constructor(msg?: MsgFunc);
package/lib/index.d.ts CHANGED
@@ -20,8 +20,8 @@
20
20
  * (isLoopbackRequest); LAN-exposed deployments never serve these endpoints;
21
21
  * - uploads/exported ZIPs are staged under $DSH_HOME/dsh-config-manager/{tmp,exports}
22
22
  * and every `path`/`zipPath` reference is confined to those roots;
23
- * - the encryption password is in-memory only: used to derive the AES-256-GCM
24
- * key for secrets.enc, never written to any file, manifest, or log;
23
+ * - there is no encryption layer: every backup is plaintext, and secret values are
24
+ * stripped by the secret scanner before they reach any file, manifest, or log;
25
25
  * - the import execute endpoint refuses to run without `confirm: true`
26
26
  * (core ImportNotConfirmedError safety valve).
27
27
  *
@@ -132,6 +132,27 @@ export declare class DshPluginsFacade implements PluginsFacade {
132
132
  needsRestart: boolean;
133
133
  }>;
134
134
  }
135
+ /** Patch-file facade:用户 patch 层($DSH_HOME/cordis.patch.yml)+ profile patch 层
136
+ * ($DSH_HOME/profiles/<name>/cordis.patch.yml),两者都在 home 根内。
137
+ *
138
+ * 导出仅为让测试能用**真实门面**(而非 mock)钉住层寻址——mock 会让两个层 token 的取值
139
+ * 撞车无处暴露(DshPluginsFacade 同款做法)。 */
140
+ export declare class DshPatchFileFacade implements PatchFileFacade {
141
+ private readonly homeDir;
142
+ private readonly profile;
143
+ private readonly msg;
144
+ constructor(homeDir: string, profile: string, msg?: MsgFunc);
145
+ private patchPath;
146
+ readPatchLines(file: string): Promise<{
147
+ lineId: string;
148
+ raw: unknown;
149
+ }[]>;
150
+ applyPatchChanges(file: string, changes: {
151
+ lineId: string;
152
+ raw: unknown;
153
+ action: 'insert' | 'update' | 'remove';
154
+ }[]): Promise<void>;
155
+ }
135
156
  /** 同步路由可预期的请求级错误(status 缺省 400;引擎/传输失败走 500) */
136
157
  export declare class SyncRouteError extends Error {
137
158
  readonly status: number;
@@ -217,18 +238,6 @@ export declare function executeRestorePlan(plan: RestorePlan, exec: RestoreExecu
217
238
  * 是真实故障,仍按 500 暴露,绝不伪装成「未登录」。
218
239
  */
219
240
  export declare function isGitHubAuthMissing(error: unknown): boolean;
220
- /** /status 的插件诊断位(issue #28)。仅回非敏感元信息:目录、profile 名、计数。 */
221
- export interface PluginDiagnostics {
222
- homeDir: string;
223
- profile: string;
224
- /** profile 目录的 package.json 是否可读(不可读 → 清单必然为空) */
225
- profileManifestReadable: boolean;
226
- /** 插件清单来源 = package.json 的 dependencies 里非 in-box 的包 */
227
- installedPluginCount: number;
228
- installedPluginNames: string[];
229
- /** dsh.profile.bundles 声明(非空即「替换默认插件栈」) */
230
- bundles: string[];
231
- }
232
241
  /**
233
242
  * Mount the config-manager engine: host context, adapters, and the
234
243
  * /api/dsh-config-manager routes (when a webServer is present).