@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.
- package/lib/adapters/credentials.d.ts +1 -1
- package/lib/adapters/index.d.ts +1 -1
- package/lib/adapters/index.js +1 -1
- package/lib/adapters/mcp.js +4 -4
- package/lib/adapters/plugins.d.ts +0 -1
- package/lib/adapters/plugins.js +35 -19
- package/lib/adapters/prompts.js +6 -6
- package/lib/adapters/test-helpers.d.ts +27 -0
- package/lib/adapters/test-helpers.js +40 -0
- package/lib/client.d.ts +1 -13
- package/lib/client.js +297 -304
- package/lib/core/analyzer.js +5 -4
- package/lib/core/backup.js +5 -3
- 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/patch-layers.d.ts +32 -0
- package/lib/core/patch-layers.js +39 -0
- package/lib/core/rollback.js +4 -4
- package/lib/core/types.d.ts +8 -12
- package/lib/index.d.ts +23 -14
- package/lib/index.js +31 -92
- 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/lib/utils/env-lock.d.ts +16 -0
- package/lib/utils/env-lock.js +27 -6
- package/package.json +2 -2
- package/src/adapters/credentials.ts +1 -1
- package/src/adapters/index.ts +1 -1
- package/src/adapters/mcp.ts +4 -4
- package/src/adapters/patch-layers.test.ts +201 -0
- package/src/adapters/plugins.test.ts +13 -5
- package/src/adapters/plugins.ts +33 -17
- package/src/adapters/prompts.ts +6 -6
- package/src/adapters/test-helpers.ts +48 -0
- package/src/client/api.ts +1 -10
- 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-api.ts +0 -1
- package/src/client/sync/sync-locales.ts +0 -1
- package/src/core/analyzer.ts +5 -4
- package/src/core/backup.ts +5 -3
- package/src/core/config-lifecycle.test.ts +4 -1
- 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/patch-layers.ts +42 -0
- package/src/core/rollback.ts +4 -4
- package/src/core/smoke.test.ts +5 -5
- package/src/core/types.ts +8 -12
- package/src/index.facade.test.ts +25 -2
- package/src/index.ts +30 -104
- 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/src/utils/env-lock.test.ts +29 -0
- package/src/utils/env-lock.ts +27 -6
- package/lib/security/encryption.d.ts +0 -85
- package/lib/security/encryption.js +0 -279
- package/src/security/encryption.ts +0 -335
package/src/core/exporter.ts
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';
|
|
@@ -21,7 +19,7 @@ import { msgOf } from './messages.ts';
|
|
|
21
19
|
import type { MsgFunc } from './messages.ts';
|
|
22
20
|
import type { Manifest, SectionId } from '../schema/types.ts';
|
|
23
21
|
import type {
|
|
24
|
-
ConfigAdapter,
|
|
22
|
+
ConfigAdapter, ExportOptions, ExportReport,
|
|
25
23
|
ExportSection, HostContext, SecretScanner, SensitiveHit,
|
|
26
24
|
} from './types.ts';
|
|
27
25
|
|
|
@@ -39,8 +37,6 @@ export interface ExporterOptions {
|
|
|
39
37
|
adapters: ConfigAdapter[];
|
|
40
38
|
/** Secret 扫描器;缺省用字段名黑名单剥离(m4 可注入强化版) */
|
|
41
39
|
scanner?: SecretScanner;
|
|
42
|
-
/** 加密提供者(m4 用 node:crypto 实现);includeSecrets 时必填;提供时备份标记 encrypted=true */
|
|
43
|
-
encryption?: EncryptionProvider | null;
|
|
44
40
|
/** 插件自身版本(manifest.exporter.version) */
|
|
45
41
|
exporterVersion?: string;
|
|
46
42
|
now?: () => Date;
|
|
@@ -176,7 +172,6 @@ export class Exporter {
|
|
|
176
172
|
private readonly ctx: HostContext;
|
|
177
173
|
private readonly adapters: ConfigAdapter[];
|
|
178
174
|
private readonly scanner: SecretScanner;
|
|
179
|
-
private readonly encryption: EncryptionProvider | null;
|
|
180
175
|
private readonly exporterVersion: string;
|
|
181
176
|
private readonly now: () => Date;
|
|
182
177
|
private readonly msg: MsgFunc;
|
|
@@ -187,7 +182,6 @@ export class Exporter {
|
|
|
187
182
|
this.ctx = opts.ctx;
|
|
188
183
|
this.adapters = opts.adapters;
|
|
189
184
|
this.scanner = opts.scanner ?? defaultSecretScanner();
|
|
190
|
-
this.encryption = opts.encryption ?? null;
|
|
191
185
|
this.exporterVersion = opts.exporterVersion ?? '0.1.0';
|
|
192
186
|
this.now = opts.now ?? (() => new Date());
|
|
193
187
|
this.msg = opts.msg ?? msgOf(opts.ctx);
|
|
@@ -201,9 +195,6 @@ export class Exporter {
|
|
|
201
195
|
*/
|
|
202
196
|
async export(options: ExportOptions): Promise<{ zipPath: string; manifest: Manifest; report: ExportReport }> {
|
|
203
197
|
const { includeSecrets, only } = options;
|
|
204
|
-
if (includeSecrets && !this.encryption) {
|
|
205
|
-
throw new Error(this.msg('export.encryptionRequired'));
|
|
206
|
-
}
|
|
207
198
|
|
|
208
199
|
// 1. 选定分区(only 过滤 + 默认包含)
|
|
209
200
|
const selected = this.adapters
|
|
@@ -214,6 +205,8 @@ export class Exporter {
|
|
|
214
205
|
const sections: ExportSection[] = [];
|
|
215
206
|
const warnings: string[] = [];
|
|
216
207
|
const redactedHits: SensitiveHit[] = [];
|
|
208
|
+
/** 文件类分区实扫到的命中数:只有它能让 containsSecrets 为真(结构化分区已被剥离) */
|
|
209
|
+
let fileSectionSecretHits = 0;
|
|
217
210
|
const included: ExportReport['included'] = [];
|
|
218
211
|
const excluded: SectionId[] = this.adapters.filter((a) => !selected.includes(a.id)).map((a) => a.id);
|
|
219
212
|
|
|
@@ -249,6 +242,7 @@ export class Exporter {
|
|
|
249
242
|
if (fileHits.length > 0) {
|
|
250
243
|
// redactedHits 是**报告统计**通道(非告警通道):仍计入全量命中(含同一文件的多行/多形态命中)。
|
|
251
244
|
redactedHits.push(...fileHits);
|
|
245
|
+
fileSectionSecretHits += fileHits.length;
|
|
252
246
|
// 告警按**文件**去重(G-09/H1):同一路径只告警一次。
|
|
253
247
|
// 修复前按 hit 计数,同一行同时命中「字段名」与「值形状」会产出两条同路径告警,
|
|
254
248
|
// 使少数文件就吃满上限,导致含真实明文凭据的其它文件被静默淹没(实测 redactedHits=8 而 5 条告警全属一个文件)。
|
|
@@ -278,12 +272,13 @@ export class Exporter {
|
|
|
278
272
|
warnings.push(...section.warnings);
|
|
279
273
|
}
|
|
280
274
|
|
|
281
|
-
// 4. 组装 ZIP 条目(JSON 分区 + 文件类分区 +
|
|
275
|
+
// 4. 组装 ZIP 条目(JSON 分区 + 文件类分区 + checksums + manifest)
|
|
282
276
|
const entries: { name: string; data: Uint8Array }[] = [];
|
|
283
277
|
const sectionFlags = buildSectionFlags(sections);
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
278
|
+
// 备份恒为明文:本插件不再有加密层。containsSecrets 只认「文件类分区实扫到的命中」——
|
|
279
|
+
// 结构化分区的敏感值已被 scanner 剥离,不构成「含秘密」;文件类分区只报告不改写,
|
|
280
|
+
// 命中即代表归档里确有明文,必须如实标注。
|
|
281
|
+
const containsSecrets = fileSectionSecretHits > 0;
|
|
287
282
|
|
|
288
283
|
for (const section of sections) {
|
|
289
284
|
if (isFileSection(section.sectionId)) {
|
|
@@ -302,33 +297,8 @@ export class Exporter {
|
|
|
302
297
|
});
|
|
303
298
|
}
|
|
304
299
|
|
|
305
|
-
// secrets.enc:有加密提供者即生成。includeSecrets=true 时加密真实的凭据原文;
|
|
306
|
-
// 只勾选加密(不导出密钥)时加密空内容占位,备份仍标记 encrypted(导入需密码),
|
|
307
|
-
// 但绝不把凭据值放进去(containsSecrets 保持 false;安全不变量不破)。
|
|
308
|
-
if (this.encryption) {
|
|
309
|
-
const credentialsFile = path.join(this.ctx.homeDir, '.credentials.yaml');
|
|
310
|
-
let plaintext: string;
|
|
311
|
-
if (includeSecrets) {
|
|
312
|
-
try {
|
|
313
|
-
const raw = await this.ctx.fs.readFile(credentialsFile);
|
|
314
|
-
plaintext = Buffer.from(raw).toString('utf8');
|
|
315
|
-
} catch (err) {
|
|
316
|
-
warnings.push(this.msg('export.credentialsReadFailed', { reason: err instanceof Error ? err.message : String(err) }));
|
|
317
|
-
plaintext = '';
|
|
318
|
-
}
|
|
319
|
-
} else {
|
|
320
|
-
plaintext = '';
|
|
321
|
-
}
|
|
322
|
-
const result = await this.encryption.encrypt(plaintext);
|
|
323
|
-
entries.push({ name: 'security/secrets.enc', data: result.blob });
|
|
324
|
-
encryption = result.info;
|
|
325
|
-
containsSecrets = includeSecrets && plaintext !== '';
|
|
326
|
-
encrypted = true;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
300
|
// 4b. 文件级 vault(includeSecrets=false:敏感文件明文不进归档 → 镜像到本机 vault)。
|
|
330
|
-
// 尽力而为:任何失败仅记警告,不中断导出。
|
|
331
|
-
// 无需镜像(vault 只服务于「明文不进备份」的本机留存场景)。
|
|
301
|
+
// 尽力而为:任何失败仅记警告,不中断导出。
|
|
332
302
|
let vaultRefreshed = 0;
|
|
333
303
|
if (!includeSecrets) {
|
|
334
304
|
try {
|
|
@@ -357,8 +327,8 @@ export class Exporter {
|
|
|
357
327
|
arch: this.ctx.arch,
|
|
358
328
|
sections: sectionFlags,
|
|
359
329
|
containsSecrets,
|
|
360
|
-
encrypted,
|
|
361
|
-
encryption,
|
|
330
|
+
encrypted: false,
|
|
331
|
+
encryption: null,
|
|
362
332
|
exportedAt: this.now().toISOString(),
|
|
363
333
|
});
|
|
364
334
|
entries.push({ name: MANIFEST_FILE, data: Buffer.from(stringifyJsonSafe(manifest, { space: 2 }), 'utf8') });
|
|
@@ -375,7 +345,6 @@ export class Exporter {
|
|
|
375
345
|
sections: Object.keys(sectionFlags).filter((k) => sectionFlags[k as SectionId]),
|
|
376
346
|
redactedFields: redactedHits.length,
|
|
377
347
|
containsSecrets,
|
|
378
|
-
encrypted,
|
|
379
348
|
vaultRefreshed,
|
|
380
349
|
});
|
|
381
350
|
|
|
@@ -385,7 +354,6 @@ export class Exporter {
|
|
|
385
354
|
security: {
|
|
386
355
|
secretsExcluded: !includeSecrets,
|
|
387
356
|
containsSecrets,
|
|
388
|
-
encrypted,
|
|
389
357
|
redactedHits: redactedHits.length,
|
|
390
358
|
vaultRefreshed,
|
|
391
359
|
},
|
package/src/core/importer.ts
CHANGED
|
@@ -36,7 +36,7 @@ export interface ExecuteOptions {
|
|
|
36
36
|
confirm: boolean;
|
|
37
37
|
/** 用户补录的秘密值(仅内存) */
|
|
38
38
|
secretInputs?: Record<string, string>;
|
|
39
|
-
/**
|
|
39
|
+
/** 旧版加密备份的解密结果(仅内存;宿主解密后注入,本插件不再产生加密备份) */
|
|
40
40
|
decryptedCredentials?: Map<string, string>;
|
|
41
41
|
/** 任一项失败立即整体回滚(默认 false:单项失败如实记录并继续其余项) */
|
|
42
42
|
rollbackOnError?: boolean;
|
package/src/core/index.ts
CHANGED
|
@@ -47,7 +47,7 @@ export type {
|
|
|
47
47
|
ExportOptions, ExportSection, ValidationResult, HostContext,
|
|
48
48
|
SettingsFacade, CredentialsFacade, PluginsFacade, WorkspaceFacade,
|
|
49
49
|
PatchFileFacade, FileSystemFacade, NamespaceInfo, PluginInfo,
|
|
50
|
-
ConfigAdapter, Portability, SecretScanner, SensitiveHit,
|
|
50
|
+
ConfigAdapter, Portability, SecretScanner, SensitiveHit,
|
|
51
51
|
PlanItem, PlanItemKind, ItemResolution, GlobalConflictStrategy,
|
|
52
52
|
ImportAnalysis, ImportDecisions, ImportPlan, ImportResult, ExecutedItem,
|
|
53
53
|
ImportContext, SnapshotTarget, SnapshotEntry, Snapshot, SnapshotStore,
|
package/src/core/messages.ts
CHANGED
|
@@ -18,9 +18,7 @@ export type { MsgFunc, MsgParams } from './msg-types.ts';
|
|
|
18
18
|
|
|
19
19
|
export const zh = {
|
|
20
20
|
// ---------- 导出 ----------
|
|
21
|
-
'export.encryptionRequired': '导出包含秘密需要注入 EncryptionProvider(m4 实现),拒绝明文导出秘密',
|
|
22
21
|
'export.sectionFailed': '分区 {adapter} 导出失败: {reason}',
|
|
23
|
-
'export.credentialsReadFailed': '读取凭据文件失败,跳过秘密导出: {reason}',
|
|
24
22
|
'export.vaultRefreshed': '凭据明文未进入备份,已镜像到本机 vault({count} 个文件,恢复时可回填)',
|
|
25
23
|
'export.vaultRefreshSkipped': 'vault 镜像跳过 {rel}: {reason}',
|
|
26
24
|
'export.vaultRefreshFailed': 'vault 镜像刷新失败(不影响导出): {reason}',
|
|
@@ -53,9 +51,7 @@ export const zh = {
|
|
|
53
51
|
'import.secretMissingDesc': '凭据 {ref} 需要补录',
|
|
54
52
|
'import.secretNotProvided': '凭据未提供,需补录',
|
|
55
53
|
'import.notConfirmed': '导入未确认:必须显式 confirm 后才允许修改任何数据',
|
|
56
|
-
'import.encryptedPasswordRequired': '
|
|
57
|
-
'import.encryptedPasswordWrong': '解密密码错误,请重试',
|
|
58
|
-
'import.notEncryptedContainer': '该文件不是加密备份容器,无法解锁',
|
|
54
|
+
'import.encryptedPasswordRequired': '该备份含上游历史加密凭据(security.encrypted=true),本插件无解密能力:须由宿主解密后注入凭据(decryptedCredentials)才能导入',
|
|
59
55
|
'import.userSkipped': '用户跳过(导入中点击「跳过当前插件」)',
|
|
60
56
|
'import.userSkippedDetail': '插件 {name} 已由用户跳过,未安装',
|
|
61
57
|
'import.vaultRestored': '凭据文件 {rel} 已从本机 vault 回填',
|
|
@@ -327,9 +323,7 @@ export const zh = {
|
|
|
327
323
|
|
|
328
324
|
export const en: Record<keyof typeof zh, string> = {
|
|
329
325
|
// ---------- export ----------
|
|
330
|
-
'export.encryptionRequired': 'Encrypted export requires an EncryptionProvider (m4); refusing to export secrets in plaintext',
|
|
331
326
|
'export.sectionFailed': 'Section {adapter} export failed: {reason}',
|
|
332
|
-
'export.credentialsReadFailed': 'Failed to read the credentials file; secrets export skipped: {reason}',
|
|
333
327
|
'export.vaultRefreshed': 'Credential plaintext did not enter the backup; mirrored to the local vault ({count} file(s), available for restore backfill)',
|
|
334
328
|
'export.vaultRefreshSkipped': 'Vault mirror skipped {rel}: {reason}',
|
|
335
329
|
'export.vaultRefreshFailed': 'Vault mirror refresh failed (export unaffected): {reason}',
|
|
@@ -362,9 +356,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
362
356
|
'import.secretMissingDesc': 'Credential {ref} needs to be re-entered',
|
|
363
357
|
'import.secretNotProvided': 'Credential not provided; requires re-entry',
|
|
364
358
|
'import.notConfirmed': 'Import not confirmed: explicit confirm is required before any data is modified',
|
|
365
|
-
'import.encryptedPasswordRequired': 'This backup
|
|
366
|
-
'import.encryptedPasswordWrong': 'Wrong decryption password, please try again',
|
|
367
|
-
'import.notEncryptedContainer': 'This file is not an encrypted backup container and cannot be unlocked',
|
|
359
|
+
'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',
|
|
368
360
|
'import.userSkipped': 'Skipped by user (clicked "Skip current plugin" during import)',
|
|
369
361
|
'import.userSkippedDetail': 'Plugin {name} was skipped by the user and not installed',
|
|
370
362
|
'import.vaultRestored': 'Credential file {rel} backfilled from the local vault',
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
|
|
18
|
+
/** home 层 token(`$DSH_HOME/cordis.patch.yml`);取值与历史快照的 file 字段一致。 */
|
|
19
|
+
export const HOME_PATCH_FILE = 'cordis.patch.yml';
|
|
20
|
+
|
|
21
|
+
/** profile 层 token(`$DSH_HOME/profiles/<profile>/cordis.patch.yml`)。 */
|
|
22
|
+
export const PROFILE_PATCH_FILE = 'profile:cordis.patch.yml';
|
|
23
|
+
|
|
24
|
+
/** 层限定复合键的分隔符(两个层 token 与 lineId 都不含它)。 */
|
|
25
|
+
const PATCH_LAYER_SEPARATOR = '#';
|
|
26
|
+
|
|
27
|
+
/** 层限定复合键:`<file>#<lineId>`。 */
|
|
28
|
+
export function patchLayerKey(file: string, lineId: string): string {
|
|
29
|
+
return `${file}${PATCH_LAYER_SEPARATOR}${lineId}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 解析层限定复合键。
|
|
34
|
+
*
|
|
35
|
+
* 兼容分支:旧快照的 `target.ref` / `SnapshotEntry.ref` 是裸 `lineId`(当时只有 home 层),
|
|
36
|
+
* 不含 `#` 即视作 home 层。lineId 本身不含 `#`,因此该分支无歧义。
|
|
37
|
+
*/
|
|
38
|
+
export function parsePatchLayerKey(ref: string): { file: string; lineId: string } {
|
|
39
|
+
const at = ref.indexOf(PATCH_LAYER_SEPARATOR);
|
|
40
|
+
if (at < 0) return { file: HOME_PATCH_FILE, lineId: ref };
|
|
41
|
+
return { file: ref.slice(0, at), lineId: ref.slice(at + 1) };
|
|
42
|
+
}
|
package/src/core/rollback.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* - settings 回滚仍走 expectedRevision 乐观锁,避免覆盖导入后用户的新修改。
|
|
9
9
|
*/
|
|
10
10
|
import { resolveFileTarget } from './backup.ts';
|
|
11
|
+
import { parsePatchLayerKey } from './patch-layers.ts';
|
|
11
12
|
import { msgOf } from './messages.ts';
|
|
12
13
|
import type {
|
|
13
14
|
ConfigAdapter, HostContext, RollbackReport, Snapshot, SnapshotEntry, SnapshotStore,
|
|
@@ -71,10 +72,9 @@ async function compensateOne(
|
|
|
71
72
|
}
|
|
72
73
|
case 'patchLine': {
|
|
73
74
|
try {
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
const file =
|
|
77
|
-
const lineId = entry.ref;
|
|
75
|
+
// 快照条目的 ref 是层限定复合键(<file>#<lineId>)——必须写回**同一层**,
|
|
76
|
+
// 否则 profile 层的原值会被写进 home 层。旧快照的裸 lineId 兼容为 home 层。
|
|
77
|
+
const { file, lineId } = parsePatchLayerKey(entry.ref);
|
|
78
78
|
await ctx.patchFile.applyPatchChanges(file, [
|
|
79
79
|
{ lineId, raw: entry.before, action: entry.before === null ? 'remove' : 'update' },
|
|
80
80
|
]);
|
package/src/core/smoke.test.ts
CHANGED
|
@@ -827,15 +827,15 @@ test('兼容性评分规则', () => {
|
|
|
827
827
|
assert.equal(computeCompatibility({ sourceDsh: '0.1.0-rc.6', targetDsh: '0.1.0-rc.6', sourcePlatform: 'win32', targetPlatform: 'win32', schemaVersion: 999, missingSections: [] }), 'unsupported');
|
|
828
828
|
});
|
|
829
829
|
|
|
830
|
-
test('
|
|
830
|
+
test('包含秘密导出:本插件无加密层,明文导出照常进行且备份恒标记未加密', async () => {
|
|
831
831
|
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'dsh-cm-sec-'));
|
|
832
832
|
try {
|
|
833
833
|
const src = makeContext('win32', 'C:\\Users\\alice');
|
|
834
834
|
const exporter = new Exporter({ ctx: src, adapters: makeAdapters(), now: () => new Date() });
|
|
835
|
-
await
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
);
|
|
835
|
+
const { manifest, report } = await exporter.export({ includeSecrets: true, outPath: path.join(tmp, 'x.zip') });
|
|
836
|
+
assert.equal(manifest.security.encrypted, false, '备份恒为明文');
|
|
837
|
+
assert.equal(manifest.security.encryption, null, '不存在加密参数');
|
|
838
|
+
assert.equal(report.security.secretsExcluded, false, 'includeSecrets=true 时不走 vault 留存路径');
|
|
839
839
|
} finally {
|
|
840
840
|
await fs.rm(tmp, { recursive: true, force: true });
|
|
841
841
|
}
|
package/src/core/types.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
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';
|
|
@@ -16,7 +16,11 @@ import type { MsgFunc } from './messages.ts';
|
|
|
16
16
|
/* ---------------- 导出选项与分区产出 ---------------- */
|
|
17
17
|
|
|
18
18
|
export interface ExportOptions {
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* 是否把真实秘密写入备份。本插件不再有加密层(备份恒为明文),结构化分区的秘密值
|
|
21
|
+
* 始终由 SecretScanner 剥离,故本开关当前的实际作用只剩「是否刷新本机 vault 镜像」:
|
|
22
|
+
* false → 导出后把敏感文件镜像到本机 vault(明文不进归档)。
|
|
23
|
+
*/
|
|
20
24
|
includeSecrets: boolean;
|
|
21
25
|
/** 仅导出指定分区(缺省 = 全部默认包含分区) */
|
|
22
26
|
only?: SectionId[];
|
|
@@ -235,7 +239,7 @@ export interface ImportAnalysis {
|
|
|
235
239
|
pathIssues: PathIssue[];
|
|
236
240
|
secretCount: number;
|
|
237
241
|
dependencyIssues: { item: string; dependency: string }[];
|
|
238
|
-
/**
|
|
242
|
+
/** 旧版加密备份标记(manifest.security.encrypted):本插件不再产生,仅历史产物为 true */
|
|
239
243
|
encrypted: boolean;
|
|
240
244
|
}
|
|
241
245
|
|
|
@@ -298,6 +302,7 @@ export interface ImportContext {
|
|
|
298
302
|
resolutions: Record<string, ItemResolution>;
|
|
299
303
|
/** 用户补录的秘密值(仅内存,永不落盘/日志) */
|
|
300
304
|
secretInputs: Record<string, string>;
|
|
305
|
+
/** 旧版加密备份的解密结果(仅内存;宿主解密后注入) */
|
|
301
306
|
decryptedCredentials?: Map<string, string>;
|
|
302
307
|
log: Logger;
|
|
303
308
|
/** 消息翻译器(analyzer 注入;适配器用它生成计划项描述/校验/结果消息) */
|
|
@@ -466,7 +471,6 @@ export interface ExportReport {
|
|
|
466
471
|
security: {
|
|
467
472
|
secretsExcluded: boolean;
|
|
468
473
|
containsSecrets: boolean;
|
|
469
|
-
encrypted: boolean;
|
|
470
474
|
redactedHits: number;
|
|
471
475
|
/** 本次导出镜像到本机 vault 的敏感文件数(文件级 vault;0 或缺失 = 未镜像) */
|
|
472
476
|
vaultRefreshed?: number;
|
|
@@ -504,14 +508,6 @@ export interface ConfigAdapter<TSection = unknown> {
|
|
|
504
508
|
rollback?(entries: SnapshotEntry[], ctx: HostContext): Promise<void>;
|
|
505
509
|
}
|
|
506
510
|
|
|
507
|
-
/* ---------------- 加密提供者(m4 用 node:crypto 实现) ---------------- */
|
|
508
|
-
|
|
509
|
-
export interface EncryptionProvider {
|
|
510
|
-
encrypt(plaintext: string): Promise<{ blob: Uint8Array; info: EncryptionInfo }>;
|
|
511
|
-
/** authTag 校验失败必须抛错 */
|
|
512
|
-
decrypt(blob: Uint8Array, info: EncryptionInfo, password: string): Promise<string>;
|
|
513
|
-
}
|
|
514
|
-
|
|
515
511
|
/* ---------------- 错误类型 ---------------- */
|
|
516
512
|
|
|
517
513
|
/** 导入被确认前拒绝执行(安全阀) */
|
package/src/index.facade.test.ts
CHANGED
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import test from 'node:test';
|
|
12
12
|
import assert from 'node:assert/strict';
|
|
13
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
14
14
|
import { tmpdir } from 'node:os';
|
|
15
15
|
import { join } from 'node:path';
|
|
16
16
|
|
|
17
|
-
import { DshPluginsFacade, ensureActivationRow } from './index.ts';
|
|
17
|
+
import { DshPatchFileFacade, DshPluginsFacade, ensureActivationRow } from './index.ts';
|
|
18
18
|
import { resolveProfileDir } from './core/plugin-cli.ts';
|
|
19
19
|
import type { DshPluginResult } from './core/plugin-cli.ts';
|
|
20
20
|
import type { PatchChange, PatchFileFacade } from './core/types.ts';
|
|
@@ -250,6 +250,29 @@ test('ensureActivationRow: bundle 包跳过(reconcile 已维护 bundles)', a
|
|
|
250
250
|
}
|
|
251
251
|
});
|
|
252
252
|
|
|
253
|
+
test('ensureActivationRow: 非 bundle 插件补行落在 profile 层(home 层文件不受影响)', async () => {
|
|
254
|
+
const { homeDir, profileDir, cleanup } = makeTempProfile();
|
|
255
|
+
try {
|
|
256
|
+
writeInstalledPkg(profileDir, 'pkg-a', '1.0.0');
|
|
257
|
+
const patchFile = new DshPatchFileFacade(homeDir, 'web');
|
|
258
|
+
const homePatch = join(homeDir, 'cordis.patch.yml');
|
|
259
|
+
const profilePatch = join(profileDir, 'cordis.patch.yml');
|
|
260
|
+
writeFileSync(homePatch, '- id: home-line\n', 'utf8');
|
|
261
|
+
const homeBefore = readFileSync(homePatch, 'utf8');
|
|
262
|
+
|
|
263
|
+
await ensureActivationRow(patchFile, join(profileDir, 'node_modules', 'pkg-a'), 'pkg-a');
|
|
264
|
+
|
|
265
|
+
assert.match(
|
|
266
|
+
readFileSync(profilePatch, 'utf8'),
|
|
267
|
+
/id: pm-pkg-a/,
|
|
268
|
+
'激活行必须落在 profile 层(home 层对每个 profile 生效,会让只在 web 装过的插件污染其他 profile)',
|
|
269
|
+
);
|
|
270
|
+
assert.equal(readFileSync(homePatch, 'utf8'), homeBefore, 'home 层文件必须原样不变');
|
|
271
|
+
} finally {
|
|
272
|
+
cleanup();
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
253
276
|
test('ensureActivationRow: scope 包名 slug 形态正确(@scope/name → pm-scope-name)', async () => {
|
|
254
277
|
const { profileDir, cleanup } = makeTempProfile();
|
|
255
278
|
try {
|
package/src/index.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
|
*
|
|
@@ -84,7 +84,7 @@ import { makeMsg, msgOf, zhMsg } from './core/messages.ts'
|
|
|
84
84
|
import type { MsgFunc } from './core/messages.ts'
|
|
85
85
|
import {
|
|
86
86
|
cleanupAbortedInstall, hasDshBundlePatch, installErrorFor, installSpecFor, listInstalledPlugins,
|
|
87
|
-
resolveProfileDir, resolveProfileNameFromArgv,
|
|
87
|
+
resolveProfileDir, resolveProfileNameFromArgv, runDshPlugin, validateProfileName,
|
|
88
88
|
} from './core/plugin-cli.ts'
|
|
89
89
|
import type {
|
|
90
90
|
ConfigAdapter, CredentialsFacade, FileSystemFacade, HostContext, ImportDecisions,
|
|
@@ -92,9 +92,9 @@ import type {
|
|
|
92
92
|
SettingsFacade, Snapshot, WorkspaceFacade,
|
|
93
93
|
} from './core/types.ts'
|
|
94
94
|
import { ImportNotConfirmedError, ImportUserSkippedError } from './core/types.ts'
|
|
95
|
-
import { createAdapters
|
|
95
|
+
import { createAdapters } from './adapters/index.ts'
|
|
96
|
+
import { HOME_PATCH_FILE, PROFILE_PATCH_FILE } from './core/patch-layers.ts'
|
|
96
97
|
import { createLocalPluginPackHook } from './core/local-plugin-host.ts'
|
|
97
|
-
import { createEncryptionProvider, decryptCredentials, decryptArchive, SecurityError, encryptArchive, isArchiveBlob, verifyEncryptedBlob } from './security/index.ts'
|
|
98
98
|
import { createHardenedZipParser } from './security/zip-security.ts'
|
|
99
99
|
import { atomicCopyFile, atomicWriteFile } from './utils/atomic-write.ts'
|
|
100
100
|
import { EnvironmentLockManager, runWithMutationLock, EnvironmentLockUnavailableError, type MutationLockContext } from './utils/env-lock.ts'
|
|
@@ -139,11 +139,10 @@ import { createConfiguredSecretScanner } from './security/secret-scanner.ts'
|
|
|
139
139
|
import type { ConfiguredSecretPatterns } from './security/secret-scanner.ts'
|
|
140
140
|
import type { SecretScanner } from './core/types.ts'
|
|
141
141
|
import { sha256Hex } from './utils/hashing.ts'
|
|
142
|
-
import { MANIFEST_FILE, parseManifest } from './schema/manifest.ts'
|
|
143
142
|
import { isFileSection, SECTION_IDS } from './schema/config.ts'
|
|
144
143
|
import { stringifyJsonSafe } from './utils/json.ts'
|
|
145
|
-
import type {
|
|
146
|
-
import {
|
|
144
|
+
import type { SectionId, WorkspaceRecord } from './schema/types.ts'
|
|
145
|
+
import { zipToBuffer } from './utils/zip.ts'
|
|
147
146
|
import { isSameOrChild, normalizePath } from './utils/paths.ts'
|
|
148
147
|
import { createLogger, type Logger } from './utils/logger.ts'
|
|
149
148
|
|
|
@@ -156,7 +155,7 @@ export const name = 'config-manager'
|
|
|
156
155
|
export const inject = ['settings', 'credentials']
|
|
157
156
|
|
|
158
157
|
/** Plugin version, kept in sync with package.json ("version"). */
|
|
159
|
-
const PLUGIN_VERSION = '0.1.
|
|
158
|
+
const PLUGIN_VERSION = '0.1.62'
|
|
160
159
|
|
|
161
160
|
/** Plugin own package name — excluded from its own exported plugins list. */
|
|
162
161
|
const PLUGIN_NAME = 'dsh-config-manager'
|
|
@@ -220,6 +219,8 @@ export interface Config {
|
|
|
220
219
|
|
|
221
220
|
/** Route family — must match the browser half's CONFIG_MANAGER_API exactly. */
|
|
222
221
|
const API = {
|
|
222
|
+
// 设置页页脚版本行(pluginVersion / dshVersion)。只读,loopback fence。
|
|
223
|
+
status: '/api/dsh-config-manager/status',
|
|
223
224
|
// P2-⑫:导出前只读预览(不落盘 ZIP;返回各分区 counts + 估算大小)
|
|
224
225
|
// P1-⑧:快照管理(手动删除 + 置顶豁免自动清理)
|
|
225
226
|
// m-backup-schedule:定时全量备份(读/存 backup-schedule.json + 立即执行一次)
|
|
@@ -644,12 +645,12 @@ class DshWorkspaceFacade implements WorkspaceFacade {
|
|
|
644
645
|
}
|
|
645
646
|
}
|
|
646
647
|
|
|
647
|
-
/** Profile 目录内的 patch 文件(非 bundle 插件激活行写入处,marketplace 同款路径)。 */
|
|
648
|
-
const PROFILE_PATCH_FILE = 'cordis.patch.yml'
|
|
649
|
-
|
|
650
648
|
/** Patch-file facade:用户 patch 层($DSH_HOME/cordis.patch.yml)+ profile patch 层
|
|
651
|
-
* ($DSH_HOME/profiles/<name>/cordis.patch.yml),两者都在 home 根内。
|
|
652
|
-
|
|
649
|
+
* ($DSH_HOME/profiles/<name>/cordis.patch.yml),两者都在 home 根内。
|
|
650
|
+
*
|
|
651
|
+
* 导出仅为让测试能用**真实门面**(而非 mock)钉住层寻址——mock 会让两个层 token 的取值
|
|
652
|
+
* 撞车无处暴露(DshPluginsFacade 同款做法)。 */
|
|
653
|
+
export class DshPatchFileFacade implements PatchFileFacade {
|
|
653
654
|
private readonly homeDir: string
|
|
654
655
|
private readonly profile: string
|
|
655
656
|
private readonly msg: MsgFunc
|
|
@@ -661,9 +662,9 @@ class DshPatchFileFacade implements PatchFileFacade {
|
|
|
661
662
|
}
|
|
662
663
|
|
|
663
664
|
private patchPath(file: string): string {
|
|
664
|
-
if (file ===
|
|
665
|
-
if (file === PROFILE_PATCH_FILE) return join(this.homeDir, 'profiles', this.profile,
|
|
666
|
-
throw new Error(this.msg('host.patchUnsupported', { user:
|
|
665
|
+
if (file === HOME_PATCH_FILE) return join(this.homeDir, HOME_PATCH_FILE)
|
|
666
|
+
if (file === PROFILE_PATCH_FILE) return join(this.homeDir, 'profiles', this.profile, HOME_PATCH_FILE)
|
|
667
|
+
throw new Error(this.msg('host.patchUnsupported', { user: HOME_PATCH_FILE, profile: PROFILE_PATCH_FILE, file }))
|
|
667
668
|
}
|
|
668
669
|
|
|
669
670
|
async readPatchLines(file: string): Promise<{ lineId: string; raw: unknown }[]> {
|
|
@@ -916,48 +917,6 @@ async function withTimeout<T>(promise: Promise<T>, ms: number, message: string):
|
|
|
916
917
|
}
|
|
917
918
|
}
|
|
918
919
|
|
|
919
|
-
/** Decrypt an encrypted backup's credentials (in-memory only; undefined when not applicable). */
|
|
920
|
-
async function tryDecryptCredentials(
|
|
921
|
-
zipPath: string,
|
|
922
|
-
password: string | undefined,
|
|
923
|
-
): Promise<Map<string, string> | undefined> {
|
|
924
|
-
if (password === undefined || password === '') return undefined
|
|
925
|
-
const raw = await fs.readFile(zipPath)
|
|
926
|
-
const archive = parseZip(raw)
|
|
927
|
-
if (!archive.has(MANIFEST_FILE)) return undefined
|
|
928
|
-
let manifest: Manifest
|
|
929
|
-
try {
|
|
930
|
-
manifest = parseManifest(archive.readEntryText(MANIFEST_FILE))
|
|
931
|
-
} catch {
|
|
932
|
-
return undefined
|
|
933
|
-
}
|
|
934
|
-
if (!manifest.security.encrypted || manifest.security.encryption === null) return undefined
|
|
935
|
-
if (!archive.has('security/secrets.enc')) return undefined
|
|
936
|
-
const blob = archive.readEntry('security/secrets.enc')
|
|
937
|
-
const plaintext = await decryptCredentials(blob, manifest.security.encryption, password)
|
|
938
|
-
let parsed: unknown
|
|
939
|
-
try {
|
|
940
|
-
parsed = yaml.load(plaintext)
|
|
941
|
-
} catch {
|
|
942
|
-
return undefined
|
|
943
|
-
}
|
|
944
|
-
const map = new Map<string, string>()
|
|
945
|
-
if (parsed !== null && typeof parsed === 'object') {
|
|
946
|
-
for (const [k, v] of Object.entries(parsed as Record<string, unknown>)) {
|
|
947
|
-
if (typeof v === 'string' && v !== '') map.set(k, v)
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
return map
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
/** 解密错误 → 用户可读文本:BAD_PASSWORD 只报「密码错误」(不泄内部细节),其余原文 */
|
|
954
|
-
function decryptErrorText(error: unknown, msg: MsgFunc): string {
|
|
955
|
-
if (error instanceof SecurityError && error.code === 'BAD_PASSWORD') {
|
|
956
|
-
return msg('import.encryptedPasswordWrong')
|
|
957
|
-
}
|
|
958
|
-
return error instanceof Error ? error.message : String(error)
|
|
959
|
-
}
|
|
960
|
-
|
|
961
920
|
interface RoutesDeps {
|
|
962
921
|
host: ConfigManagerHostContext
|
|
963
922
|
adapters: ConfigAdapter[]
|
|
@@ -1423,44 +1382,6 @@ export function isGitHubAuthMissing(error: unknown): boolean {
|
|
|
1423
1382
|
return error instanceof GitHubAuthError && (error.code === 'unauthorized' || error.code === 'no_token')
|
|
1424
1383
|
}
|
|
1425
1384
|
|
|
1426
|
-
/** /status 的插件诊断位(issue #28)。仅回非敏感元信息:目录、profile 名、计数。 */
|
|
1427
|
-
export interface PluginDiagnostics {
|
|
1428
|
-
homeDir: string
|
|
1429
|
-
profile: string
|
|
1430
|
-
/** profile 目录的 package.json 是否可读(不可读 → 清单必然为空) */
|
|
1431
|
-
profileManifestReadable: boolean
|
|
1432
|
-
/** 插件清单来源 = package.json 的 dependencies 里非 in-box 的包 */
|
|
1433
|
-
installedPluginCount: number
|
|
1434
|
-
installedPluginNames: string[]
|
|
1435
|
-
/** dsh.profile.bundles 声明(非空即「替换默认插件栈」) */
|
|
1436
|
-
bundles: string[]
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
/**
|
|
1440
|
-
* 读取插件诊断信息(issue #28):把「插件到底读了哪个目录 / 哪个 profile / 看到什么」变成
|
|
1441
|
-
* 用户可自查的数据——此前只存在于宿主内部,导致「装了插件却识别不到」无从定位。
|
|
1442
|
-
* best-effort:失败不抛出(诊断位缺失不应拖垮 /status)。
|
|
1443
|
-
*/
|
|
1444
|
-
async function readPluginDiagnostics(host: HostContext): Promise<Partial<PluginDiagnostics>> {
|
|
1445
|
-
try {
|
|
1446
|
-
const profileDir = resolveProfileDir(host.homeDir, host.profile ?? 'web')
|
|
1447
|
-
const manifest = readProfileManifest(profileDir)
|
|
1448
|
-
const installed = await host.plugins.listInstalled()
|
|
1449
|
-
const bundles = manifest?.dsh?.profile?.bundles
|
|
1450
|
-
return {
|
|
1451
|
-
homeDir: host.homeDir,
|
|
1452
|
-
profile: host.profile ?? 'web',
|
|
1453
|
-
profileManifestReadable: manifest !== null,
|
|
1454
|
-
installedPluginCount: installed.length,
|
|
1455
|
-
installedPluginNames: installed.map((p) => p.name),
|
|
1456
|
-
bundles: Array.isArray(bundles) ? bundles : [],
|
|
1457
|
-
}
|
|
1458
|
-
} catch (err) {
|
|
1459
|
-
host.log.warn(`plugin diagnostics unavailable: ${err instanceof Error ? err.message : String(err)}`)
|
|
1460
|
-
return {}
|
|
1461
|
-
}
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
1385
|
/** Build the /api/dsh-config-manager route family. */
|
|
1465
1386
|
function makeRoutes(deps: RoutesDeps): { routes: WebRoute[]; scheduler: AutoSyncScheduler; makeSyncEngine: (cfg: SyncConfig) => SyncEngine; lifecycle: ConfigLifecycle } {
|
|
1466
1387
|
const { host, adapters, exportsDir, tmpDir, snapshotsDir, runs, syncDir, dataDir, credentials, githubClientId, githubClientSecret, history } = deps
|
|
@@ -1856,6 +1777,18 @@ function makeRoutes(deps: RoutesDeps): { routes: WebRoute[]; scheduler: AutoSync
|
|
|
1856
1777
|
|
|
1857
1778
|
const routesList: WebRoute[] = [
|
|
1858
1779
|
// ------------------------------------------------------------- status
|
|
1780
|
+
// 设置页页脚版本行:插件版本 + DSH 版本。只读、无 secret,loopback fence。
|
|
1781
|
+
{
|
|
1782
|
+
kind: 'exact',
|
|
1783
|
+
path: API.status,
|
|
1784
|
+
handler: async (req, res) => {
|
|
1785
|
+
if (!guard(req, res, 'GET')) return
|
|
1786
|
+
writeJson(res, 200, {
|
|
1787
|
+
pluginVersion: PLUGIN_VERSION,
|
|
1788
|
+
dshVersion: host.dshVersion,
|
|
1789
|
+
})
|
|
1790
|
+
},
|
|
1791
|
+
},
|
|
1859
1792
|
// ------------------------------------------------------------- export
|
|
1860
1793
|
// ---------------------------------------------------- export-preview
|
|
1861
1794
|
// P2-⑫:导出前只读预览(不落盘 ZIP):对选中分区逐个 adapter.export 收集 counts
|
|
@@ -1863,13 +1796,6 @@ function makeRoutes(deps: RoutesDeps): { routes: WebRoute[]; scheduler: AutoSync
|
|
|
1863
1796
|
// 零写入;loopback fence 必备。
|
|
1864
1797
|
// ------------------------------------------------------------ download
|
|
1865
1798
|
// -------------------------------------------------------------- upload
|
|
1866
|
-
// ------------------------------------------------------ decrypt-archive
|
|
1867
|
-
// 整体加密备份容器的解锁(只读,零写入到任何配置):用备份密码解密上传的加密容器,
|
|
1868
|
-
// 得到明文 ZIP 写入受控临时目录并返回新 zipPath,供 analyze/plan/execute 引用。
|
|
1869
|
-
// 导出时容器密码与内部 secrets.enc 密码同源(同一 password 派生两层加密),
|
|
1870
|
-
// 因此顺带在明文 ZIP 上解出内部凭据覆盖清单(refs,非值)一并返回——
|
|
1871
|
-
// 导入全程只需输入这一次密码,无需第二个密码校验页面。
|
|
1872
|
-
// 密码仅内存随请求体传入,绝不落盘/落日志;解出的明文 ZIP 亦为临时文件,导入结束后清理。
|
|
1873
1799
|
// ------------------------------------------------------------- analyze
|
|
1874
1800
|
// ---------------------------------------------------------------- plan
|
|
1875
1801
|
// ------------------------------------------------------------ progress
|
package/src/security/index.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';
|