pi-webdav-backup 1.0.0 → 1.0.1

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/README.md CHANGED
@@ -326,7 +326,7 @@ npm test
326
326
  node extensions/webdav-backup/verify-load.mjs
327
327
  ```
328
328
 
329
- 6 个测试文件、277 项断言,用内置的内存 WebDAV 服务端做真实 HTTP 往返:
329
+ 6 个测试文件、284 项断言,用内置的内存 WebDAV 服务端做真实 HTTP 往返:
330
330
 
331
331
  | 文件 | 覆盖 |
332
332
  |---|---|
@@ -200,7 +200,11 @@ export function redactSensitive(relPath, buf, opts = {}) {
200
200
  const aggressive = opts.aggressive !== false;
201
201
  const base = relPath.split("/").pop() ?? "";
202
202
 
203
- const SECRET_KEY_RE = /key|token|secret|password|passwd|credential|api[-_]?key|auth|cookie|session|bearer|private/i;
203
+ const SECRET_KEY_RE = /key|token|secret|password|passwd|credential|api[-_]?key|auth(?!or)|cookie|session[-_]?(token|key|secret|id)|bearer|private/i;
204
+ // 密钥引用($ENV / !cmd / dpapi: / file:)是“密钥放在哪”的指针,不是秘密本身,应保留
205
+ const SECRET_REF_SCHEMES = ["$", "!", "dpapi:", "file:"];
206
+ const isRef = (v) =>
207
+ typeof v === "string" && SECRET_REF_SCHEMES.some((p) => v.trim().startsWith(p));
204
208
  // 常见凭据前缀,以及“长且像随机串”的值
205
209
  const SECRET_VALUE_RE =
206
210
  /^(sk-[A-Za-z0-9_-]{8,}|ghp_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_-]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|AIza[0-9A-Za-z_-]{30,}|eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}|[A-Za-z0-9_+/=-]{32,})$/;
@@ -214,8 +218,15 @@ export function redactSensitive(relPath, buf, opts = {}) {
214
218
  const out = {};
215
219
  for (const [k, val] of Object.entries(v)) {
216
220
  if (SECRET_KEY_RE.test(k)) {
217
- // 保留结构,抹掉值
218
- out[k] = typeof val === "object" && val !== null ? walk(val, k) : "__REDACTED__";
221
+ // 保留结构,抹掉值。
222
+ // 只替换字符串:布尔/数字不可能是凭据,
223
+ // 把 false 替换成字符串会破坏恢复后的配置语义(回归:includeSessions 曾被误伤)
224
+ out[k] =
225
+ typeof val === "object" && val !== null
226
+ ? walk(val, k)
227
+ : typeof val === "string"
228
+ ? "__REDACTED__"
229
+ : val;
219
230
  } else {
220
231
  out[k] = walk(val, k);
221
232
  }
@@ -236,10 +247,6 @@ export function redactSensitive(relPath, buf, opts = {}) {
236
247
  if (base.startsWith("auth.json")) {
237
248
  try {
238
249
  const obj = JSON.parse(buf.toString("utf8"));
239
- const SECRET_REF_SCHEMES = ["$", "!", "dpapi:", "file:"];
240
- const isRef = (v) =>
241
- typeof v === "string" && SECRET_REF_SCHEMES.some((p) => v.trim().startsWith(p));
242
-
243
250
  const walkCredential = (cred) => {
244
251
  if (!cred || typeof cred !== "object") return redactJson(cred, { aggressiveValues: aggressive });
245
252
  const out = {};
@@ -270,7 +277,11 @@ export function redactSensitive(relPath, buf, opts = {}) {
270
277
  try {
271
278
  const obj = JSON.parse(buf.toString("utf8"));
272
279
  const cleaned = redactJson(obj, { aggressiveValues: false });
273
- if (obj.encryptKey) cleaned.encryptKey = "__REDACTED__";
280
+ // 与 auth.json 同一约定:密钥引用保留(指针非秘密),
281
+ // 明文由字段名规则抹掉,这里只需把被误抹的引用还原
282
+ if (typeof obj.encryptKey === "string" && isRef(obj.encryptKey)) cleaned.encryptKey = obj.encryptKey;
283
+ const pw = obj.remote && typeof obj.remote === "object" ? obj.remote.password : undefined;
284
+ if (typeof pw === "string" && isRef(pw) && cleaned.remote) cleaned.remote.password = pw;
274
285
  return Buffer.from(JSON.stringify(cleaned, null, 2), "utf8");
275
286
  } catch {
276
287
  return buf;
@@ -107,6 +107,30 @@ check("嵌套路径 auth.json 也被脱敏", !r12.includes("sk-nested-canary"),
107
107
  check("空对象不报错", (() => { try { redact("auth.json", "{}"); return true; } catch { return false; } })());
108
108
  check("null 值不报错", (() => { try { redact("auth.json", '{"a":null,"b":1}'); return true; } catch { return false; } })());
109
109
 
110
+ // ── 10b. 回归:布尔/数字不被误伤,密钥引用保留(本地实测发现)
111
+ const rBool = redact(
112
+ "webdav-backup.json",
113
+ JSON.stringify({ includeSessions: false, snapshot: true, keepVersions: 10, sessionDir: "C:/sessions" }),
114
+ );
115
+ check("includeSessions:false 不被误伤(回归)", !rBool.includes("__REDACTED__"), rBool);
116
+ check("sessionDir 不被误伤", rBool.includes("C:/sessions"), rBool);
117
+
118
+ const rTokFlag = redact("webdav-backup.json", JSON.stringify({ tokenEnabled: false, keyCount: 3 }));
119
+ check("布尔/数字在敏感字段名下也不误替换", !rTokFlag.includes("__REDACTED__"), rTokFlag);
120
+
121
+ const rSessTok = redact("webdav-backup.json", JSON.stringify({ sessionToken: "tok-123456789012" }));
122
+ check("sessionToken 仍被脱敏", !rSessTok.includes("tok-123456789012"), rSessTok);
123
+
124
+ const rRef2 = redact(
125
+ "webdav-backup.json",
126
+ JSON.stringify({ remote: { password: "dpapi:webdav-password" }, encryptKey: "$PI_WEBDAV_BACKUP_KEY" }),
127
+ );
128
+ check("password 的 dpapi: 引用保留", rRef2.includes("dpapi:webdav-password"), rRef2);
129
+ check("encryptKey 的 $ENV 引用保留", rRef2.includes("$PI_WEBDAV_BACKUP_KEY"), rRef2);
130
+
131
+ const rPlain2 = redact("webdav-backup.json", JSON.stringify({ remote: { password: "MyPassword123" } }));
132
+ check("password 明文仍被脱敏", !rPlain2.includes("MyPassword123"), rPlain2);
133
+
110
134
  // ── 12. 明文审计(防止备份副本把密钥带上云)
111
135
  {
112
136
  const { auditForPlaintext, looksCredentialFile } = await import("./backup.mjs");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-webdav-backup",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "pi 配置自动备份到 WebDAV(坚果云 / Nextcloud / 群晖 / 自建):归档与快照两种模式、AES-256-GCM 加密、DPAPI 密钥管理、敏感字段脱敏、退出自动备份与每日定时备份。",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -33,7 +33,9 @@
33
33
  "test": "node extensions/webdav-backup/run-tests.mjs"
34
34
  },
35
35
  "pi": {
36
- "extensions": ["./extensions/webdav-backup.ts"]
36
+ "extensions": [
37
+ "./extensions/webdav-backup.ts"
38
+ ]
37
39
  },
38
40
  "peerDependencies": {
39
41
  "@earendil-works/pi-coding-agent": "*"