@logictan/dsh-config-manager 0.1.61 → 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.
- package/lib/adapters/credentials.d.ts +1 -1
- package/lib/client.d.ts +0 -3
- package/lib/client.js +262 -268
- package/lib/core/analyzer.js +5 -4
- package/lib/core/exporter.d.ts +1 -4
- package/lib/core/exporter.js +14 -45
- package/lib/core/importer.d.ts +1 -1
- package/lib/core/index.d.ts +1 -1
- package/lib/core/messages.d.ts +1 -5
- package/lib/core/messages.js +2 -10
- package/lib/core/types.d.ts +8 -12
- package/lib/index.d.ts +2 -2
- package/lib/index.js +4 -57
- package/lib/security/index.d.ts +1 -3
- package/lib/security/index.js +1 -3
- package/lib/sync/transport.d.ts +2 -2
- package/lib/sync/transport.js +1 -1
- package/lib/ui/i18n.d.ts +0 -3
- package/lib/ui/i18n.js +0 -6
- package/lib/ui/report.js +1 -2
- package/lib/ui/test-helpers.d.ts +0 -6
- package/lib/ui/test-helpers.js +1 -5
- package/lib/ui/types.d.ts +0 -13
- package/package.json +1 -1
- package/src/adapters/credentials.ts +1 -1
- package/src/client/config-manager.module.css +1 -1
- package/src/client/run-store.test.ts +1 -1
- package/src/client/sync/SyncSettingsView.tsx +5 -5
- package/src/client/sync/sync-locales.ts +0 -1
- package/src/core/analyzer.ts +5 -4
- package/src/core/exporter.ts +15 -47
- package/src/core/importer.ts +1 -1
- package/src/core/index.ts +1 -1
- package/src/core/messages.ts +2 -10
- package/src/core/smoke.test.ts +5 -5
- package/src/core/types.ts +8 -12
- package/src/index.ts +5 -56
- package/src/security/index.ts +1 -3
- package/src/security/security.test.ts +0 -361
- package/src/sync/transport.ts +3 -3
- package/src/ui/i18n.ts +0 -6
- package/src/ui/report.ts +1 -2
- package/src/ui/test-helpers.ts +3 -7
- package/src/ui/types.ts +0 -10
- package/lib/security/encryption.d.ts +0 -85
- package/lib/security/encryption.js +0 -279
- package/src/security/encryption.ts +0 -335
package/lib/core/analyzer.js
CHANGED
|
@@ -408,8 +408,9 @@ export class Analyzer {
|
|
|
408
408
|
if (opts.confirm !== true) {
|
|
409
409
|
throw new ImportNotConfirmedError(this.msg);
|
|
410
410
|
}
|
|
411
|
-
// 10b.
|
|
412
|
-
//
|
|
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
|
|
611
|
-
//
|
|
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');
|
package/lib/core/exporter.d.ts
CHANGED
|
@@ -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,
|
|
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;
|
package/lib/core/exporter.js
CHANGED
|
@@ -3,11 +3,9 @@
|
|
|
3
3
|
* adapter 收集各分区 → Secret 过滤 → manifest → checksum → ZIP。
|
|
4
4
|
*
|
|
5
5
|
* 安全不变量:
|
|
6
|
-
* - Secret 值默认永不进入导出数据(结构化分区逐一过 SecretScanner
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
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 分区 + 文件类分区 +
|
|
238
|
+
// 4. 组装 ZIP 条目(JSON 分区 + 文件类分区 + checksums + manifest)
|
|
243
239
|
const entries = [];
|
|
244
240
|
const sectionFlags = buildSectionFlags(sections);
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
// 尽力而为:任何失败仅记警告,不中断导出。
|
|
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
|
},
|
package/lib/core/importer.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ export interface ExecuteOptions {
|
|
|
30
30
|
confirm: boolean;
|
|
31
31
|
/** 用户补录的秘密值(仅内存) */
|
|
32
32
|
secretInputs?: Record<string, string>;
|
|
33
|
-
/**
|
|
33
|
+
/** 旧版加密备份的解密结果(仅内存;宿主解密后注入,本插件不再产生加密备份) */
|
|
34
34
|
decryptedCredentials?: Map<string, string>;
|
|
35
35
|
/** 任一项失败立即整体回滚(默认 false:单项失败如实记录并继续其余项) */
|
|
36
36
|
rollbackOnError?: boolean;
|
package/lib/core/index.d.ts
CHANGED
|
@@ -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,
|
|
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';
|
package/lib/core/messages.d.ts
CHANGED
|
@@ -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 回填';
|
package/lib/core/messages.js
CHANGED
|
@@ -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
|
|
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',
|
package/lib/core/types.d.ts
CHANGED
|
@@ -5,14 +5,18 @@
|
|
|
5
5
|
* HostContext 是 m3 定义、m5 实现的 DSH Service 门面(研究报告 §3.2 的叶子方法最小集),
|
|
6
6
|
* 测试用内存 mock 即可驱动完整导出→导入往返。
|
|
7
7
|
*/
|
|
8
|
-
import type {
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
* -
|
|
24
|
-
*
|
|
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
|
*
|
package/lib/index.js
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
|
-
* -
|
|
24
|
-
*
|
|
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
|
*
|
|
@@ -70,7 +70,6 @@ import { ImportNotConfirmedError, ImportUserSkippedError } from './core/types.js
|
|
|
70
70
|
import { createAdapters } from './adapters/index.js';
|
|
71
71
|
import { HOME_PATCH_FILE, PROFILE_PATCH_FILE } from './core/patch-layers.js';
|
|
72
72
|
import { createLocalPluginPackHook } from './core/local-plugin-host.js';
|
|
73
|
-
import { createEncryptionProvider, decryptCredentials, decryptArchive, SecurityError, encryptArchive, isArchiveBlob, verifyEncryptedBlob } from './security/index.js';
|
|
74
73
|
import { createHardenedZipParser } from './security/zip-security.js';
|
|
75
74
|
import { atomicCopyFile, atomicWriteFile } from './utils/atomic-write.js';
|
|
76
75
|
import { EnvironmentLockManager, runWithMutationLock, EnvironmentLockUnavailableError } from './utils/env-lock.js';
|
|
@@ -94,10 +93,9 @@ import { readUiPrefs, updateUiPrefs } from './sync/ui-prefs.js';
|
|
|
94
93
|
import { redact } from './security/redaction.js';
|
|
95
94
|
import { createConfiguredSecretScanner } from './security/secret-scanner.js';
|
|
96
95
|
import { sha256Hex } from './utils/hashing.js';
|
|
97
|
-
import { MANIFEST_FILE, parseManifest } from './schema/manifest.js';
|
|
98
96
|
import { isFileSection, SECTION_IDS } from './schema/config.js';
|
|
99
97
|
import { stringifyJsonSafe } from './utils/json.js';
|
|
100
|
-
import {
|
|
98
|
+
import { zipToBuffer } from './utils/zip.js';
|
|
101
99
|
import { isSameOrChild, normalizePath } from './utils/paths.js';
|
|
102
100
|
import { createLogger } from './utils/logger.js';
|
|
103
101
|
/* ---------------------------------------------------------------- identity */
|
|
@@ -106,7 +104,7 @@ export const name = 'config-manager';
|
|
|
106
104
|
/** Services required before the engine can mount (present in every profile). */
|
|
107
105
|
export const inject = ['settings', 'credentials'];
|
|
108
106
|
/** Plugin version, kept in sync with package.json ("version"). */
|
|
109
|
-
const PLUGIN_VERSION = '0.1.
|
|
107
|
+
const PLUGIN_VERSION = '0.1.62';
|
|
110
108
|
/** Plugin own package name — excluded from its own exported plugins list. */
|
|
111
109
|
const PLUGIN_NAME = 'dsh-config-manager';
|
|
112
110
|
/**
|
|
@@ -785,50 +783,6 @@ async function withTimeout(promise, ms, message) {
|
|
|
785
783
|
clearTimeout(timer);
|
|
786
784
|
}
|
|
787
785
|
}
|
|
788
|
-
/** Decrypt an encrypted backup's credentials (in-memory only; undefined when not applicable). */
|
|
789
|
-
async function tryDecryptCredentials(zipPath, password) {
|
|
790
|
-
if (password === undefined || password === '')
|
|
791
|
-
return undefined;
|
|
792
|
-
const raw = await fs.readFile(zipPath);
|
|
793
|
-
const archive = parseZip(raw);
|
|
794
|
-
if (!archive.has(MANIFEST_FILE))
|
|
795
|
-
return undefined;
|
|
796
|
-
let manifest;
|
|
797
|
-
try {
|
|
798
|
-
manifest = parseManifest(archive.readEntryText(MANIFEST_FILE));
|
|
799
|
-
}
|
|
800
|
-
catch {
|
|
801
|
-
return undefined;
|
|
802
|
-
}
|
|
803
|
-
if (!manifest.security.encrypted || manifest.security.encryption === null)
|
|
804
|
-
return undefined;
|
|
805
|
-
if (!archive.has('security/secrets.enc'))
|
|
806
|
-
return undefined;
|
|
807
|
-
const blob = archive.readEntry('security/secrets.enc');
|
|
808
|
-
const plaintext = await decryptCredentials(blob, manifest.security.encryption, password);
|
|
809
|
-
let parsed;
|
|
810
|
-
try {
|
|
811
|
-
parsed = yaml.load(plaintext);
|
|
812
|
-
}
|
|
813
|
-
catch {
|
|
814
|
-
return undefined;
|
|
815
|
-
}
|
|
816
|
-
const map = new Map();
|
|
817
|
-
if (parsed !== null && typeof parsed === 'object') {
|
|
818
|
-
for (const [k, v] of Object.entries(parsed)) {
|
|
819
|
-
if (typeof v === 'string' && v !== '')
|
|
820
|
-
map.set(k, v);
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
return map;
|
|
824
|
-
}
|
|
825
|
-
/** 解密错误 → 用户可读文本:BAD_PASSWORD 只报「密码错误」(不泄内部细节),其余原文 */
|
|
826
|
-
function decryptErrorText(error, msg) {
|
|
827
|
-
if (error instanceof SecurityError && error.code === 'BAD_PASSWORD') {
|
|
828
|
-
return msg('import.encryptedPasswordWrong');
|
|
829
|
-
}
|
|
830
|
-
return error instanceof Error ? error.message : String(error);
|
|
831
|
-
}
|
|
832
786
|
/* -------------------------------------------------- sync 路由(m-sync-ui) */
|
|
833
787
|
/** 同步路由可预期的请求级错误(status 缺省 400;引擎/传输失败走 500) */
|
|
834
788
|
export class SyncRouteError extends Error {
|
|
@@ -1604,13 +1558,6 @@ function makeRoutes(deps) {
|
|
|
1604
1558
|
// 零写入;loopback fence 必备。
|
|
1605
1559
|
// ------------------------------------------------------------ download
|
|
1606
1560
|
// -------------------------------------------------------------- upload
|
|
1607
|
-
// ------------------------------------------------------ decrypt-archive
|
|
1608
|
-
// 整体加密备份容器的解锁(只读,零写入到任何配置):用备份密码解密上传的加密容器,
|
|
1609
|
-
// 得到明文 ZIP 写入受控临时目录并返回新 zipPath,供 analyze/plan/execute 引用。
|
|
1610
|
-
// 导出时容器密码与内部 secrets.enc 密码同源(同一 password 派生两层加密),
|
|
1611
|
-
// 因此顺带在明文 ZIP 上解出内部凭据覆盖清单(refs,非值)一并返回——
|
|
1612
|
-
// 导入全程只需输入这一次密码,无需第二个密码校验页面。
|
|
1613
|
-
// 密码仅内存随请求体传入,绝不落盘/落日志;解出的明文 ZIP 亦为临时文件,导入结束后清理。
|
|
1614
1561
|
// ------------------------------------------------------------- analyze
|
|
1615
1562
|
// ---------------------------------------------------------------- plan
|
|
1616
1563
|
// ------------------------------------------------------------ progress
|
package/lib/security/index.d.ts
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 安全模块公共出口(m4-security):
|
|
3
|
-
* secret-scanner /
|
|
3
|
+
* secret-scanner / integrity / zip-security / redaction。
|
|
4
4
|
*
|
|
5
5
|
* 与 core 的注入点对齐:
|
|
6
6
|
* - `createSecretScanner()` → ExporterOptions.scanner(SecretScanner 契约)
|
|
7
|
-
* - `createEncryptionProvider(pw)` → ExporterOptions.encryption(EncryptionProvider 契约)
|
|
8
7
|
* - `createHardenedZipParser()` → ImporterOptions.parseZipOverride((buf, limits?) => ZipArchive)
|
|
9
8
|
* - `safeExtractHardened()` → m5 导入文件类分区落盘通道
|
|
10
9
|
* - `verifyChecksumsJson()` → 完整性校验通道(core analyzer 已有内置校验,本模块供独立调用)
|
|
11
10
|
*/
|
|
12
11
|
export * from './secret-scanner.ts';
|
|
13
|
-
export * from './encryption.ts';
|
|
14
12
|
export * from './integrity.ts';
|
|
15
13
|
export * from './zip-security.ts';
|
|
16
14
|
export * from './redaction.ts';
|
package/lib/security/index.js
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 安全模块公共出口(m4-security):
|
|
3
|
-
* secret-scanner /
|
|
3
|
+
* secret-scanner / integrity / zip-security / redaction。
|
|
4
4
|
*
|
|
5
5
|
* 与 core 的注入点对齐:
|
|
6
6
|
* - `createSecretScanner()` → ExporterOptions.scanner(SecretScanner 契约)
|
|
7
|
-
* - `createEncryptionProvider(pw)` → ExporterOptions.encryption(EncryptionProvider 契约)
|
|
8
7
|
* - `createHardenedZipParser()` → ImporterOptions.parseZipOverride((buf, limits?) => ZipArchive)
|
|
9
8
|
* - `safeExtractHardened()` → m5 导入文件类分区落盘通道
|
|
10
9
|
* - `verifyChecksumsJson()` → 完整性校验通道(core analyzer 已有内置校验,本模块供独立调用)
|
|
11
10
|
*/
|
|
12
11
|
export * from './secret-scanner.js';
|
|
13
|
-
export * from './encryption.js';
|
|
14
12
|
export * from './integrity.js';
|
|
15
13
|
export * from './zip-security.js';
|
|
16
14
|
export * from './redaction.js';
|
package/lib/sync/transport.d.ts
CHANGED
|
@@ -29,9 +29,9 @@ export interface SyncSnapshotMeta {
|
|
|
29
29
|
/** 加密快照的 sections 载荷:整个明文 sections 对象序列化后整体加密(AES-256-GCM)。 */
|
|
30
30
|
export interface EncryptedSections {
|
|
31
31
|
encrypted: {
|
|
32
|
-
/** 加密参数(salt/iv/authTag base64
|
|
32
|
+
/** 加密参数(salt/iv/authTag base64);本插件已不生成,仅供读取历史快照 */
|
|
33
33
|
info: EncryptionInfo;
|
|
34
|
-
/** base64
|
|
34
|
+
/** base64 密文(明文 = 序列化的 sections Record) */
|
|
35
35
|
data: string;
|
|
36
36
|
};
|
|
37
37
|
}
|
package/lib/sync/transport.js
CHANGED
|
@@ -21,7 +21,7 @@ export function sectionsEqual(remote, local) {
|
|
|
21
21
|
const r = remote.sections;
|
|
22
22
|
const l = local.sections;
|
|
23
23
|
if (Object.keys(l).length === 0)
|
|
24
|
-
return false; //
|
|
24
|
+
return false; // 本地无分区(空快照)→ 无法比较
|
|
25
25
|
if (Object.keys(r).length !== Object.keys(l).length)
|
|
26
26
|
return false;
|
|
27
27
|
for (const key of Object.keys(r)) {
|
package/lib/ui/i18n.d.ts
CHANGED
|
@@ -35,11 +35,9 @@ export declare const uiZh: {
|
|
|
35
35
|
readonly 'report.security': '安全:';
|
|
36
36
|
readonly 'report.apiKeysExcluded': 'API 密钥已排除:';
|
|
37
37
|
readonly 'report.containsSecrets': '包含密钥:';
|
|
38
|
-
readonly 'report.encrypted': '已加密:';
|
|
39
38
|
readonly 'report.redacted': '{count} 个敏感字段已脱敏';
|
|
40
39
|
readonly 'report.file': '文件:';
|
|
41
40
|
readonly 'report.yes': '是';
|
|
42
|
-
readonly 'report.yesEncrypted': '是(加密)';
|
|
43
41
|
readonly 'report.no': '否';
|
|
44
42
|
readonly 'report.importedRestored': '已导入/恢复';
|
|
45
43
|
readonly 'report.skipped': '跳过';
|
|
@@ -151,7 +149,6 @@ export declare const uiZh: {
|
|
|
151
149
|
readonly 'sync.pushOk': '推送成功(快照 {id})';
|
|
152
150
|
readonly 'sync.pushPreviewHeadline': '将推送 {total} 个分区({changed} 个有变化)';
|
|
153
151
|
readonly 'sync.pushPreviewHint': '以上为只读预览,不会写入远端。确认后点击「推送」才真正上传。';
|
|
154
|
-
readonly 'sync.pushPreviewEncrypted': '加密快照:载荷将整体加密,各分区相对基线的变化不可比对。';
|
|
155
152
|
readonly 'sync.pullFailed': '拉取失败';
|
|
156
153
|
readonly 'sync.pullOk': '远端快照 {id} 差异预览:共 {count} 项变更';
|
|
157
154
|
readonly 'sync.pullEmpty': '远端快照与本地一致(无变更)';
|
package/lib/ui/i18n.js
CHANGED
|
@@ -38,11 +38,9 @@ export const uiZh = {
|
|
|
38
38
|
'report.security': '安全:',
|
|
39
39
|
'report.apiKeysExcluded': 'API 密钥已排除:',
|
|
40
40
|
'report.containsSecrets': '包含密钥:',
|
|
41
|
-
'report.encrypted': '已加密:',
|
|
42
41
|
'report.redacted': '{count} 个敏感字段已脱敏',
|
|
43
42
|
'report.file': '文件:',
|
|
44
43
|
'report.yes': '是',
|
|
45
|
-
'report.yesEncrypted': '是(加密)',
|
|
46
44
|
'report.no': '否',
|
|
47
45
|
'report.importedRestored': '已导入/恢复',
|
|
48
46
|
'report.skipped': '跳过',
|
|
@@ -158,7 +156,6 @@ export const uiZh = {
|
|
|
158
156
|
'sync.pushOk': '推送成功(快照 {id})',
|
|
159
157
|
'sync.pushPreviewHeadline': '将推送 {total} 个分区({changed} 个有变化)',
|
|
160
158
|
'sync.pushPreviewHint': '以上为只读预览,不会写入远端。确认后点击「推送」才真正上传。',
|
|
161
|
-
'sync.pushPreviewEncrypted': '加密快照:载荷将整体加密,各分区相对基线的变化不可比对。',
|
|
162
159
|
'sync.pullFailed': '拉取失败',
|
|
163
160
|
'sync.pullOk': '远端快照 {id} 差异预览:共 {count} 项变更',
|
|
164
161
|
'sync.pullEmpty': '远端快照与本地一致(无变更)',
|
|
@@ -320,11 +317,9 @@ export const uiEn = {
|
|
|
320
317
|
'report.security': 'Security:',
|
|
321
318
|
'report.apiKeysExcluded': 'API Keys excluded:',
|
|
322
319
|
'report.containsSecrets': 'Contains secrets:',
|
|
323
|
-
'report.encrypted': 'Encrypted:',
|
|
324
320
|
'report.redacted': '{count} sensitive field(s) redacted',
|
|
325
321
|
'report.file': 'File:',
|
|
326
322
|
'report.yes': 'yes',
|
|
327
|
-
'report.yesEncrypted': 'yes (encrypted)',
|
|
328
323
|
'report.no': 'no',
|
|
329
324
|
'report.importedRestored': 'imported/restored',
|
|
330
325
|
'report.skipped': 'skipped',
|
|
@@ -436,7 +431,6 @@ export const uiEn = {
|
|
|
436
431
|
'sync.pushOk': 'Push succeeded (snapshot {id})',
|
|
437
432
|
'sync.pushPreviewHeadline': 'Will push {total} section(s) ({changed} changed)',
|
|
438
433
|
'sync.pushPreviewHint': 'Read-only preview above — nothing is written to the remote. Click "Push" to actually upload.',
|
|
439
|
-
'sync.pushPreviewEncrypted': 'Encrypted snapshot: the payload is encrypted as a whole; per-section changes vs the baseline cannot be compared.',
|
|
440
434
|
'sync.pullFailed': 'Pull failed',
|
|
441
435
|
'sync.pullOk': 'Remote snapshot {id} diff preview: {count} change(s)',
|
|
442
436
|
'sync.pullEmpty': 'Remote snapshot matches local (no changes)',
|
package/lib/ui/report.js
CHANGED
|
@@ -18,8 +18,7 @@ export function renderExportReport(report, t = zhUiT) {
|
|
|
18
18
|
}
|
|
19
19
|
lines.push(t('report.security'));
|
|
20
20
|
lines.push(` ✓ ${t('report.apiKeysExcluded')} ${report.security.secretsExcluded ? t('report.yes') : t('report.no')}`);
|
|
21
|
-
lines.push(` ✓ ${t('report.containsSecrets')} ${report.security.containsSecrets ? t('report.
|
|
22
|
-
lines.push(` ✓ ${t('report.encrypted')} ${report.security.encrypted ? t('report.yes') : t('report.no')}`);
|
|
21
|
+
lines.push(` ✓ ${t('report.containsSecrets')} ${report.security.containsSecrets ? t('report.yes') : t('report.no')}`);
|
|
23
22
|
if (report.security.redactedHits > 0)
|
|
24
23
|
lines.push(` ⚠ ${t('report.redacted', { count: String(report.security.redactedHits) })}`);
|
|
25
24
|
lines.push('');
|
package/lib/ui/test-helpers.d.ts
CHANGED
|
@@ -46,7 +46,6 @@ export declare class MockImportPort implements ImportPort {
|
|
|
46
46
|
confirm: boolean;
|
|
47
47
|
secretInputs?: Record<string, string>;
|
|
48
48
|
rollbackOnError: boolean;
|
|
49
|
-
decryptPassword?: string;
|
|
50
49
|
plan?: ImportPlan;
|
|
51
50
|
}[];
|
|
52
51
|
constructor(opts?: {
|
|
@@ -56,14 +55,9 @@ export declare class MockImportPort implements ImportPort {
|
|
|
56
55
|
});
|
|
57
56
|
analyzeImport(): Promise<ImportAnalysis>;
|
|
58
57
|
createImportPlan(_zip: string, decisions: ImportDecisions): Promise<ImportPlan>;
|
|
59
|
-
decryptArchive(zipPath: string): Promise<{
|
|
60
|
-
zipPath: string;
|
|
61
|
-
refs: string[];
|
|
62
|
-
}>;
|
|
63
58
|
executeImportPlan(_zip: string, plan: ImportPlan, opts: {
|
|
64
59
|
confirm: boolean;
|
|
65
60
|
secretInputs?: Record<string, string>;
|
|
66
61
|
rollbackOnError: boolean;
|
|
67
|
-
decryptPassword?: string;
|
|
68
62
|
}): Promise<ImportResult>;
|
|
69
63
|
}
|
package/lib/ui/test-helpers.js
CHANGED
|
@@ -20,7 +20,7 @@ export function makeExportReport(overrides = {}) {
|
|
|
20
20
|
{ section: 'plugins', counts: { plugins: 8 } },
|
|
21
21
|
],
|
|
22
22
|
excluded: ['sessions', 'pluginFiles'],
|
|
23
|
-
security: { secretsExcluded: true, containsSecrets: false,
|
|
23
|
+
security: { secretsExcluded: true, containsSecrets: false, redactedHits: 2 },
|
|
24
24
|
file: { name: 'dsh-config-2026-08-14.zip', sizeBytes: 20480 },
|
|
25
25
|
warnings: [],
|
|
26
26
|
...overrides,
|
|
@@ -142,10 +142,6 @@ export class MockImportPort {
|
|
|
142
142
|
this.planCalls.push(decisions);
|
|
143
143
|
return this.plan;
|
|
144
144
|
}
|
|
145
|
-
async decryptArchive(zipPath) {
|
|
146
|
-
// 测试用:把传入路径视为已解锁的明文 ZIP(不真正解密),无内部凭据
|
|
147
|
-
return { zipPath, refs: [] };
|
|
148
|
-
}
|
|
149
145
|
async executeImportPlan(_zip, plan, opts) {
|
|
150
146
|
this.executeCalls.push({ ...opts, plan });
|
|
151
147
|
return this.result;
|