dsh-tiddlywiki 0.19.2 → 0.19.3
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 +4 -0
- package/lib/client.js +1 -1
- package/lib/index.js +333 -141
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
- package/src/client/settings-page.ts +2 -2
- package/src/host/admin.ts +153 -52
- package/src/host/clip-bridge.ts +29 -2
- package/src/host/http.ts +35 -0
- package/src/host/routes.ts +141 -44
- package/src/host/session-summary.ts +30 -8
- package/src/index.ts +2 -1
package/lib/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { existsSync, readFileSync, readdirSync, statSync, watch } from "node:fs";
|
|
3
|
-
import {
|
|
3
|
+
import { mkdir, readFile, readdir, rename, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
|
|
5
5
|
import { execFile, spawn } from "node:child_process";
|
|
6
6
|
import { promisify } from "node:util";
|
|
@@ -1315,7 +1315,7 @@ function scanSnapshot(snap, subagent, collected) {
|
|
|
1315
1315
|
title: args.newTitle,
|
|
1316
1316
|
action: "produced",
|
|
1317
1317
|
time: t,
|
|
1318
|
-
detail: old !== void 0 ? `重命名自「${old}」` : void 0,
|
|
1318
|
+
detail: old !== void 0 && isPlausibleTitle(old) ? `重命名自「${old}」` : void 0,
|
|
1319
1319
|
subagent
|
|
1320
1320
|
});
|
|
1321
1321
|
}
|
|
@@ -1362,6 +1362,14 @@ function scanSnapshot(snap, subagent, collected) {
|
|
|
1362
1362
|
}
|
|
1363
1363
|
}
|
|
1364
1364
|
}
|
|
1365
|
+
/**
|
|
1366
|
+
* 内联文本净化(v0.19.3):汇总 wikitext 会被 TW 渲染成 HTML 片段再注入 DSH 页面,
|
|
1367
|
+
* 而 TW 的解析器**原样透传 HTML**。凡是来自会话日志/请求体的字符串(sessionId、
|
|
1368
|
+
* rename 的旧标题、检索词)都要先转义 HTML 元字符并去掉控制字符,别指望上层净化。
|
|
1369
|
+
*/
|
|
1370
|
+
function escapeInline(text, max = 200) {
|
|
1371
|
+
return text.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/[[\]|]/g, " ").slice(0, max);
|
|
1372
|
+
}
|
|
1365
1373
|
/** 并发(8 路)查询每篇笔记的当前状态(404 → 不存在/已删除)。 */
|
|
1366
1374
|
async function enrichTitles(client, titles) {
|
|
1367
1375
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -1382,7 +1390,8 @@ async function enrichTitles(client, titles) {
|
|
|
1382
1390
|
} catch {
|
|
1383
1391
|
return [title, {
|
|
1384
1392
|
exists: false,
|
|
1385
|
-
tags: []
|
|
1393
|
+
tags: [],
|
|
1394
|
+
unknown: true
|
|
1386
1395
|
}];
|
|
1387
1396
|
}
|
|
1388
1397
|
};
|
|
@@ -1410,7 +1419,7 @@ function buildWikitext(sessionId, collected, producedTitles, readTitles, stateBy
|
|
|
1410
1419
|
const lines = [];
|
|
1411
1420
|
lines.push("! 会话相关 wiki 汇总");
|
|
1412
1421
|
lines.push("");
|
|
1413
|
-
lines.push(`本页列出会话 \`${sessionId}\`(含其后代子代理)在本会话中产生 / 读取 / 检索过的知识库笔记。`);
|
|
1422
|
+
lines.push(`本页列出会话 \`${escapeInline(sessionId, 120)}\`(含其后代子代理)在本会话中产生 / 读取 / 检索过的知识库笔记。`);
|
|
1414
1423
|
lines.push("");
|
|
1415
1424
|
const producedCount = producedTitles.length;
|
|
1416
1425
|
const readCount = readTitles.length;
|
|
@@ -1426,15 +1435,16 @@ function buildWikitext(sessionId, collected, producedTitles, readTitles, stateBy
|
|
|
1426
1435
|
lines.push("");
|
|
1427
1436
|
const entryLine = (title, state, entry) => {
|
|
1428
1437
|
const bits = [`[[${title}]]`];
|
|
1429
|
-
if (state
|
|
1438
|
+
if (state !== void 0 && state.unknown === true) bits.push("⚠️ 状态未知(查询失败)");
|
|
1439
|
+
else if (state === void 0 || !state.exists) bits.push("⚠️ 已删除/不存在");
|
|
1430
1440
|
else {
|
|
1431
|
-
if (state.tags.length > 0) bits.push(`标签 ${state.tags.slice(0, 6).join("、")}${state.tags.length > 6 ? "…" : ""}`);
|
|
1441
|
+
if (state.tags.length > 0) bits.push(`标签 ${state.tags.slice(0, 6).map((tag) => escapeInline(tag, 40)).join("、")}${state.tags.length > 6 ? "…" : ""}`);
|
|
1432
1442
|
const mod = fmtModified(state.modified);
|
|
1433
1443
|
if (mod.length > 0) bits.push(`修改 ${mod}`);
|
|
1434
1444
|
}
|
|
1435
1445
|
const t = fmtTime(entry.time);
|
|
1436
1446
|
if (t.length > 0) bits.push(`会话内 ${t}`);
|
|
1437
|
-
if (entry.detail !== void 0) bits.push(entry.detail);
|
|
1447
|
+
if (entry.detail !== void 0) bits.push(escapeInline(entry.detail, 200));
|
|
1438
1448
|
if (entry.subagent) bits.push("(子代理)");
|
|
1439
1449
|
return `* ${bits.join(" · ")}`;
|
|
1440
1450
|
};
|
|
@@ -1459,8 +1469,8 @@ function buildWikitext(sessionId, collected, producedTitles, readTitles, stateBy
|
|
|
1459
1469
|
for (const r of collected.searches) {
|
|
1460
1470
|
const bits = [];
|
|
1461
1471
|
if (r.kind === "search") {
|
|
1462
|
-
bits.push(`\`tiddlywiki_search\` query=「${r.query.length > 0 ? r.query : "(空)"}」`);
|
|
1463
|
-
if (r.tags.length > 0) bits.push(`tags ${r.tags.join("、")}`);
|
|
1472
|
+
bits.push(`\`tiddlywiki_search\` query=「${r.query.length > 0 ? escapeInline(r.query, 120) : "(空)"}」`);
|
|
1473
|
+
if (r.tags.length > 0) bits.push(`tags ${r.tags.map((tag) => escapeInline(tag, 40)).join("、")}`);
|
|
1464
1474
|
} else bits.push("`tiddlywiki_recent`(最近修改)");
|
|
1465
1475
|
const t = fmtTime(r.time);
|
|
1466
1476
|
if (t.length > 0) bits.push(t);
|
|
@@ -1575,6 +1585,39 @@ function json(res, payload, status = 200) {
|
|
|
1575
1585
|
res.end(JSON.stringify(payload));
|
|
1576
1586
|
}
|
|
1577
1587
|
/**
|
|
1588
|
+
* Map a thrown error to an HTTP status. Oversized bodies are a client problem
|
|
1589
|
+
* (413), everything else is ours (500) — the routes used to answer 500 for
|
|
1590
|
+
* `/note` and `/admin/*` but 413 for `/upload` for the identical condition.
|
|
1591
|
+
*/
|
|
1592
|
+
function errorStatus(err) {
|
|
1593
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1594
|
+
return /body too large/i.test(message) ? 413 : 500;
|
|
1595
|
+
}
|
|
1596
|
+
/**
|
|
1597
|
+
* Wrap an async route handler so a rejected promise can never become an
|
|
1598
|
+
* unhandled rejection (v0.19.3).
|
|
1599
|
+
*
|
|
1600
|
+
* WHY: every registration used to be `void handleX(req, res)`, and the host
|
|
1601
|
+
* installs no `unhandledRejection` handler — Node's default policy then EXITS
|
|
1602
|
+
* the whole dsh web process, while the client request hangs with no response.
|
|
1603
|
+
* A handler that throws (including a sync throw before its own try block, e.g.
|
|
1604
|
+
* `new URL(req.url)` outside the try in the proxies) now always ends in a
|
|
1605
|
+
* response: 413/500 JSON, or a bare `end()` when headers are already sent.
|
|
1606
|
+
*/
|
|
1607
|
+
function guardHandler(handler) {
|
|
1608
|
+
return (req, res) => {
|
|
1609
|
+
handler(req, res).catch((err) => {
|
|
1610
|
+
try {
|
|
1611
|
+
if (!res.headersSent) json(res, {
|
|
1612
|
+
ok: false,
|
|
1613
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1614
|
+
}, errorStatus(err));
|
|
1615
|
+
else res.end();
|
|
1616
|
+
} catch {}
|
|
1617
|
+
});
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1578
1621
|
* True when a NON-GET/HEAD/OPTIONS request is a cross-site (CSRF) request.
|
|
1579
1622
|
*
|
|
1580
1623
|
* Every legitimate caller of these routes is same-origin: the DSH GUI, the
|
|
@@ -3667,6 +3710,8 @@ function sanitizeUploadName(input) {
|
|
|
3667
3710
|
if (typeof input !== "string") return "";
|
|
3668
3711
|
let name = basename(input.trim().replace(/[\\/]+/g, "/")).replace(/[\u0000-\u001f\u007f]/g, "").replace(/[<>:"|?*]/g, "_").replace(/^\.+/, "").trim();
|
|
3669
3712
|
if (name.length === 0 || name === "." || name === "..") return "";
|
|
3713
|
+
name = name.replace(/[. ]+$/, "");
|
|
3714
|
+
if (name.length === 0) return "";
|
|
3670
3715
|
if (WINDOWS_RESERVED_NAMES.test(name)) name = `_${name}`;
|
|
3671
3716
|
if (name.length > 160) name = name.slice(0, 160);
|
|
3672
3717
|
return name;
|
|
@@ -3868,6 +3913,13 @@ function registerRoutes(ctx, deps) {
|
|
|
3868
3913
|
}, 400);
|
|
3869
3914
|
return;
|
|
3870
3915
|
}
|
|
3916
|
+
if (!/^[A-Za-z0-9._:-]{1,120}$/.test(session)) {
|
|
3917
|
+
json(res, {
|
|
3918
|
+
ok: false,
|
|
3919
|
+
error: "session id has an unsupported format"
|
|
3920
|
+
}, 400);
|
|
3921
|
+
return;
|
|
3922
|
+
}
|
|
3871
3923
|
const client = deps.getClient();
|
|
3872
3924
|
if (client === void 0) {
|
|
3873
3925
|
json(res, {
|
|
@@ -3893,7 +3945,7 @@ function registerRoutes(ctx, deps) {
|
|
|
3893
3945
|
json(res, {
|
|
3894
3946
|
ok: false,
|
|
3895
3947
|
error: err instanceof Error ? err.message : String(err)
|
|
3896
|
-
},
|
|
3948
|
+
}, errorStatus(err));
|
|
3897
3949
|
}
|
|
3898
3950
|
};
|
|
3899
3951
|
/**
|
|
@@ -3937,7 +3989,7 @@ function registerRoutes(ctx, deps) {
|
|
|
3937
3989
|
json(res, {
|
|
3938
3990
|
ok: false,
|
|
3939
3991
|
error: err instanceof Error ? err.message : String(err)
|
|
3940
|
-
},
|
|
3992
|
+
}, errorStatus(err));
|
|
3941
3993
|
}
|
|
3942
3994
|
};
|
|
3943
3995
|
/**
|
|
@@ -4015,7 +4067,7 @@ function registerRoutes(ctx, deps) {
|
|
|
4015
4067
|
json(res, {
|
|
4016
4068
|
ok: false,
|
|
4017
4069
|
error: err instanceof Error ? err.message : String(err)
|
|
4018
|
-
},
|
|
4070
|
+
}, errorStatus(err));
|
|
4019
4071
|
}
|
|
4020
4072
|
};
|
|
4021
4073
|
/**
|
|
@@ -4073,7 +4125,7 @@ function registerRoutes(ctx, deps) {
|
|
|
4073
4125
|
json(res, {
|
|
4074
4126
|
ok: false,
|
|
4075
4127
|
error: err instanceof Error ? err.message : String(err)
|
|
4076
|
-
},
|
|
4128
|
+
}, errorStatus(err));
|
|
4077
4129
|
}
|
|
4078
4130
|
};
|
|
4079
4131
|
/**
|
|
@@ -4129,6 +4181,32 @@ function registerRoutes(ctx, deps) {
|
|
|
4129
4181
|
}, 503);
|
|
4130
4182
|
return;
|
|
4131
4183
|
}
|
|
4184
|
+
if (mode !== void 0) {
|
|
4185
|
+
const ap = deps.getAgentPresets();
|
|
4186
|
+
if (ap === void 0) {
|
|
4187
|
+
json(res, {
|
|
4188
|
+
ok: false,
|
|
4189
|
+
error: "agent presets service unavailable"
|
|
4190
|
+
}, 503);
|
|
4191
|
+
return;
|
|
4192
|
+
}
|
|
4193
|
+
try {
|
|
4194
|
+
const presets = await ap.list();
|
|
4195
|
+
if (!presets.some((preset) => preset.id === mode)) {
|
|
4196
|
+
json(res, {
|
|
4197
|
+
ok: false,
|
|
4198
|
+
error: `unknown agent preset "${mode}" (available: ${presets.map((preset) => preset.id).join(", ")})`
|
|
4199
|
+
}, 400);
|
|
4200
|
+
return;
|
|
4201
|
+
}
|
|
4202
|
+
} catch (err) {
|
|
4203
|
+
json(res, {
|
|
4204
|
+
ok: false,
|
|
4205
|
+
error: `cannot validate agent preset: ${err instanceof Error ? err.message : String(err)}`
|
|
4206
|
+
}, 503);
|
|
4207
|
+
return;
|
|
4208
|
+
}
|
|
4209
|
+
}
|
|
4132
4210
|
const ws = deps.getWorkspaceRegistry();
|
|
4133
4211
|
if (cwd.length > 0) {
|
|
4134
4212
|
if (!isAbsolute(cwd)) {
|
|
@@ -4186,7 +4264,7 @@ function registerRoutes(ctx, deps) {
|
|
|
4186
4264
|
json(res, {
|
|
4187
4265
|
ok: false,
|
|
4188
4266
|
error: err instanceof Error ? err.message : String(err)
|
|
4189
|
-
},
|
|
4267
|
+
}, errorStatus(err));
|
|
4190
4268
|
}
|
|
4191
4269
|
};
|
|
4192
4270
|
const handleNote = async (req, res) => {
|
|
@@ -4250,7 +4328,7 @@ function registerRoutes(ctx, deps) {
|
|
|
4250
4328
|
json(res, {
|
|
4251
4329
|
ok: false,
|
|
4252
4330
|
error: err instanceof Error ? err.message : String(err)
|
|
4253
|
-
},
|
|
4331
|
+
}, errorStatus(err));
|
|
4254
4332
|
}
|
|
4255
4333
|
};
|
|
4256
4334
|
const handleEdit = async (req, res) => {
|
|
@@ -4296,7 +4374,7 @@ function registerRoutes(ctx, deps) {
|
|
|
4296
4374
|
json(res, {
|
|
4297
4375
|
ok: false,
|
|
4298
4376
|
error: err instanceof Error ? err.message : String(err)
|
|
4299
|
-
},
|
|
4377
|
+
}, errorStatus(err));
|
|
4300
4378
|
}
|
|
4301
4379
|
};
|
|
4302
4380
|
/** Distinct non-system tags for the quick-note tag autocomplete. The flat
|
|
@@ -4330,7 +4408,7 @@ function registerRoutes(ctx, deps) {
|
|
|
4330
4408
|
json(res, {
|
|
4331
4409
|
ok: false,
|
|
4332
4410
|
error: err instanceof Error ? err.message : String(err)
|
|
4333
|
-
},
|
|
4411
|
+
}, errorStatus(err));
|
|
4334
4412
|
}
|
|
4335
4413
|
};
|
|
4336
4414
|
/** Recent non-system tiddlers for the quick-note "最近" picker (newest first). */
|
|
@@ -4362,7 +4440,7 @@ function registerRoutes(ctx, deps) {
|
|
|
4362
4440
|
json(res, {
|
|
4363
4441
|
ok: false,
|
|
4364
4442
|
error: err instanceof Error ? err.message : String(err)
|
|
4365
|
-
},
|
|
4443
|
+
}, errorStatus(err));
|
|
4366
4444
|
}
|
|
4367
4445
|
};
|
|
4368
4446
|
/** Full tiddler for the quick-note "最近" picker (load into the editor). */
|
|
@@ -4418,7 +4496,7 @@ function registerRoutes(ctx, deps) {
|
|
|
4418
4496
|
json(res, {
|
|
4419
4497
|
ok: false,
|
|
4420
4498
|
error: err instanceof Error ? err.message : String(err)
|
|
4421
|
-
},
|
|
4499
|
+
}, errorStatus(err));
|
|
4422
4500
|
}
|
|
4423
4501
|
};
|
|
4424
4502
|
/**
|
|
@@ -4473,7 +4551,7 @@ function registerRoutes(ctx, deps) {
|
|
|
4473
4551
|
json(res, {
|
|
4474
4552
|
ok: false,
|
|
4475
4553
|
error: err instanceof Error ? err.message : String(err)
|
|
4476
|
-
},
|
|
4554
|
+
}, errorStatus(err));
|
|
4477
4555
|
}
|
|
4478
4556
|
};
|
|
4479
4557
|
const handleRestart = async (req, res) => {
|
|
@@ -4499,7 +4577,7 @@ function registerRoutes(ctx, deps) {
|
|
|
4499
4577
|
json(res, {
|
|
4500
4578
|
ok: false,
|
|
4501
4579
|
error: err instanceof Error ? err.message : String(err)
|
|
4502
|
-
},
|
|
4580
|
+
}, errorStatus(err));
|
|
4503
4581
|
}
|
|
4504
4582
|
};
|
|
4505
4583
|
/** One-click git sync for the floating button / settings page: pull →
|
|
@@ -4568,7 +4646,7 @@ function registerRoutes(ctx, deps) {
|
|
|
4568
4646
|
json(res, {
|
|
4569
4647
|
ok: false,
|
|
4570
4648
|
error: err instanceof Error ? err.message : String(err)
|
|
4571
|
-
},
|
|
4649
|
+
}, errorStatus(err));
|
|
4572
4650
|
} finally {
|
|
4573
4651
|
endMutation();
|
|
4574
4652
|
}
|
|
@@ -4612,16 +4690,24 @@ function registerRoutes(ctx, deps) {
|
|
|
4612
4690
|
await mkdir(filesDir, { recursive: true });
|
|
4613
4691
|
const ext = extname(name);
|
|
4614
4692
|
const stem = ext.length > 0 ? name.slice(0, -ext.length) : name;
|
|
4615
|
-
let candidate =
|
|
4616
|
-
|
|
4693
|
+
let candidate = "";
|
|
4694
|
+
let wrote = false;
|
|
4695
|
+
for (let i = 0; i <= 1e4 && !wrote; i++) {
|
|
4696
|
+
candidate = i === 0 ? name : `${stem}-${i}${ext}`;
|
|
4617
4697
|
try {
|
|
4618
|
-
await
|
|
4619
|
-
|
|
4620
|
-
|
|
4698
|
+
await writeFile(join(filesDir, candidate), buf, { flag: "wx" });
|
|
4699
|
+
wrote = true;
|
|
4700
|
+
} catch (err) {
|
|
4701
|
+
if (err.code !== "EEXIST") throw err;
|
|
4621
4702
|
}
|
|
4622
|
-
candidate = `${stem}-${i}${ext}`;
|
|
4623
4703
|
}
|
|
4624
|
-
|
|
4704
|
+
if (!wrote) {
|
|
4705
|
+
json(res, {
|
|
4706
|
+
ok: false,
|
|
4707
|
+
error: "同名文件过多,请换一个文件名"
|
|
4708
|
+
}, 409);
|
|
4709
|
+
return;
|
|
4710
|
+
}
|
|
4625
4711
|
deps.autoCommit();
|
|
4626
4712
|
invalidateGitStatus();
|
|
4627
4713
|
json(res, {
|
|
@@ -4714,6 +4800,29 @@ function registerRoutes(ctx, deps) {
|
|
|
4714
4800
|
}, 502);
|
|
4715
4801
|
}
|
|
4716
4802
|
};
|
|
4803
|
+
/**
|
|
4804
|
+
* Titles the browser-facing TW proxies must never serve (v0.19.3).
|
|
4805
|
+
*
|
|
4806
|
+
* `$:/plugins/dsh-tiddlywiki/config` holds the shared tokens and the git
|
|
4807
|
+
* remote (possibly with a PAT); both `/tw` and `/api` forward ANY path to the
|
|
4808
|
+
* loopback TW child, whose TiddlyWeb REST answers for `$:/…` titles — a
|
|
4809
|
+
* route-level guard on `/get` was therefore trivially bypassed by
|
|
4810
|
+
* `GET /dsh-tiddlywiki/tw/recipes/default/tiddlers/%24%3A%2Fplugins%2F…`
|
|
4811
|
+
* (verified). The whole plugin namespace is blocked: nothing under it is
|
|
4812
|
+
* needed by the TW frontend, and the host itself talks to TW directly.
|
|
4813
|
+
*/
|
|
4814
|
+
const BLOCKED_PROXY_TITLE_PREFIXES = ["$:/plugins/dsh-tiddlywiki/"];
|
|
4815
|
+
/** True when a proxied pathname addresses a blocked (secret-bearing) tiddler. */
|
|
4816
|
+
const isBlockedProxyPath = (pathname) => {
|
|
4817
|
+
const at = pathname.indexOf("/tiddlers/");
|
|
4818
|
+
if (at < 0) return false;
|
|
4819
|
+
const raw = pathname.slice(at + 10);
|
|
4820
|
+
let title = raw;
|
|
4821
|
+
try {
|
|
4822
|
+
title = decodeURIComponent(raw);
|
|
4823
|
+
} catch {}
|
|
4824
|
+
return BLOCKED_PROXY_TITLE_PREFIXES.some((prefix) => title.startsWith(prefix));
|
|
4825
|
+
};
|
|
4717
4826
|
/** Passthrough /dsh-tiddlywiki/api/<rest> → TW root /<rest>. */
|
|
4718
4827
|
const handleApiProxy = async (req, res) => {
|
|
4719
4828
|
if (rejectCrossSiteWrite(req, res)) return;
|
|
@@ -4724,9 +4833,16 @@ function registerRoutes(ctx, deps) {
|
|
|
4724
4833
|
}, 503);
|
|
4725
4834
|
return;
|
|
4726
4835
|
}
|
|
4727
|
-
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
4728
|
-
const rest = url.pathname.replace(/^\/dsh-tiddlywiki\/api/, "") || "/";
|
|
4729
4836
|
try {
|
|
4837
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
4838
|
+
const rest = url.pathname.replace(/^\/dsh-tiddlywiki\/api/, "") || "/";
|
|
4839
|
+
if (isBlockedProxyPath(rest)) {
|
|
4840
|
+
json(res, {
|
|
4841
|
+
ok: false,
|
|
4842
|
+
error: "system tiddler not exposed"
|
|
4843
|
+
}, 403);
|
|
4844
|
+
return;
|
|
4845
|
+
}
|
|
4730
4846
|
const headers = forwardHeaders(req.headers);
|
|
4731
4847
|
const method = (req.method ?? "GET").toUpperCase();
|
|
4732
4848
|
if (method === "PUT" || method === "DELETE" || method === "POST") headers["x-requested-with"] = "TiddlyWiki";
|
|
@@ -4770,14 +4886,23 @@ function registerRoutes(ctx, deps) {
|
|
|
4770
4886
|
}
|
|
4771
4887
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
4772
4888
|
const rest = url.pathname.replace(new RegExp(`^${TW_PROXY_PREFIX}(?=/|$)`), "") || "/";
|
|
4889
|
+
if (isBlockedProxyPath(rest)) {
|
|
4890
|
+
json(res, {
|
|
4891
|
+
ok: false,
|
|
4892
|
+
error: "system tiddler not exposed"
|
|
4893
|
+
}, 403);
|
|
4894
|
+
return;
|
|
4895
|
+
}
|
|
4773
4896
|
try {
|
|
4774
4897
|
const method = (req.method ?? "GET").toUpperCase();
|
|
4775
4898
|
const headers = forwardHeaders(req.headers);
|
|
4776
4899
|
if (method === "PUT" || method === "DELETE" || method === "POST") headers["x-requested-with"] = "TiddlyWiki";
|
|
4900
|
+
const abort = new AbortController();
|
|
4901
|
+
const timeout = AbortSignal.timeout(3e4);
|
|
4777
4902
|
const init = {
|
|
4778
4903
|
method,
|
|
4779
4904
|
headers,
|
|
4780
|
-
signal: AbortSignal.timeout
|
|
4905
|
+
signal: typeof AbortSignal.any === "function" ? AbortSignal.any([abort.signal, timeout]) : timeout
|
|
4781
4906
|
};
|
|
4782
4907
|
if (method === "PUT" || method === "POST") init.body = await readBodyBuffer(req, MAX_UPLOAD_BYTES);
|
|
4783
4908
|
const upstream = await fetch(`${deps.server.url}${rest}${url.search}`, init);
|
|
@@ -4805,7 +4930,15 @@ function registerRoutes(ctx, deps) {
|
|
|
4805
4930
|
await new Promise((resolveP, rejectP) => {
|
|
4806
4931
|
body.on("error", rejectP);
|
|
4807
4932
|
res.on("error", rejectP);
|
|
4808
|
-
res.on("close", () =>
|
|
4933
|
+
res.on("close", () => {
|
|
4934
|
+
try {
|
|
4935
|
+
abort.abort();
|
|
4936
|
+
} catch {}
|
|
4937
|
+
try {
|
|
4938
|
+
body.destroy();
|
|
4939
|
+
} catch {}
|
|
4940
|
+
resolveP();
|
|
4941
|
+
});
|
|
4809
4942
|
res.on("finish", () => resolveP());
|
|
4810
4943
|
body.pipe(res);
|
|
4811
4944
|
});
|
|
@@ -4824,128 +4957,92 @@ function registerRoutes(ctx, deps) {
|
|
|
4824
4957
|
ctx.webServer.register({
|
|
4825
4958
|
kind: "exact",
|
|
4826
4959
|
path: `${ROUTE_PREFIX}/status`,
|
|
4827
|
-
handler: (
|
|
4828
|
-
handleStatus(req, res);
|
|
4829
|
-
}
|
|
4960
|
+
handler: guardHandler(handleStatus)
|
|
4830
4961
|
}),
|
|
4831
4962
|
ctx.webServer.register({
|
|
4832
4963
|
kind: "exact",
|
|
4833
4964
|
path: `${ROUTE_PREFIX}/note`,
|
|
4834
|
-
handler: (
|
|
4835
|
-
handleNote(req, res);
|
|
4836
|
-
}
|
|
4965
|
+
handler: guardHandler(handleNote)
|
|
4837
4966
|
}),
|
|
4838
4967
|
ctx.webServer.register({
|
|
4839
4968
|
kind: "exact",
|
|
4840
4969
|
path: `${ROUTE_PREFIX}/edit`,
|
|
4841
|
-
handler: (
|
|
4842
|
-
handleEdit(req, res);
|
|
4843
|
-
}
|
|
4970
|
+
handler: guardHandler(handleEdit)
|
|
4844
4971
|
}),
|
|
4845
4972
|
ctx.webServer.register({
|
|
4846
4973
|
kind: "exact",
|
|
4847
4974
|
path: `${ROUTE_PREFIX}/tags`,
|
|
4848
|
-
handler: (
|
|
4849
|
-
handleTags(req, res);
|
|
4850
|
-
}
|
|
4975
|
+
handler: guardHandler(handleTags)
|
|
4851
4976
|
}),
|
|
4852
4977
|
ctx.webServer.register({
|
|
4853
4978
|
kind: "exact",
|
|
4854
4979
|
path: `${ROUTE_PREFIX}/recent`,
|
|
4855
|
-
handler: (
|
|
4856
|
-
handleRecent(req, res);
|
|
4857
|
-
}
|
|
4980
|
+
handler: guardHandler(handleRecent)
|
|
4858
4981
|
}),
|
|
4859
4982
|
ctx.webServer.register({
|
|
4860
4983
|
kind: "exact",
|
|
4861
4984
|
path: `${ROUTE_PREFIX}/get`,
|
|
4862
|
-
handler: (
|
|
4863
|
-
handleGet(req, res);
|
|
4864
|
-
}
|
|
4985
|
+
handler: guardHandler(handleGet)
|
|
4865
4986
|
}),
|
|
4866
4987
|
ctx.webServer.register({
|
|
4867
4988
|
kind: "exact",
|
|
4868
4989
|
path: `${ROUTE_PREFIX}/search`,
|
|
4869
|
-
handler: (
|
|
4870
|
-
handleSearch(req, res);
|
|
4871
|
-
}
|
|
4990
|
+
handler: guardHandler(handleSearch)
|
|
4872
4991
|
}),
|
|
4873
4992
|
ctx.webServer.register({
|
|
4874
4993
|
kind: "exact",
|
|
4875
4994
|
path: `${ROUTE_PREFIX}/render`,
|
|
4876
|
-
handler: (
|
|
4877
|
-
handleRender(req, res);
|
|
4878
|
-
}
|
|
4995
|
+
handler: guardHandler(handleRender)
|
|
4879
4996
|
}),
|
|
4880
4997
|
ctx.webServer.register({
|
|
4881
4998
|
kind: "exact",
|
|
4882
4999
|
path: `${ROUTE_PREFIX}/sync`,
|
|
4883
|
-
handler: (
|
|
4884
|
-
handleSync(req, res);
|
|
4885
|
-
}
|
|
5000
|
+
handler: guardHandler(handleSync)
|
|
4886
5001
|
}),
|
|
4887
5002
|
ctx.webServer.register({
|
|
4888
5003
|
kind: "exact",
|
|
4889
5004
|
path: `${ROUTE_PREFIX}/upload`,
|
|
4890
|
-
handler: (
|
|
4891
|
-
handleUpload(req, res);
|
|
4892
|
-
}
|
|
5005
|
+
handler: guardHandler(handleUpload)
|
|
4893
5006
|
}),
|
|
4894
5007
|
ctx.webServer.register({
|
|
4895
5008
|
kind: "exact",
|
|
4896
5009
|
path: `${ROUTE_PREFIX}/restart`,
|
|
4897
|
-
handler: (
|
|
4898
|
-
handleRestart(req, res);
|
|
4899
|
-
}
|
|
5010
|
+
handler: guardHandler(handleRestart)
|
|
4900
5011
|
}),
|
|
4901
5012
|
ctx.webServer.register({
|
|
4902
5013
|
kind: "exact",
|
|
4903
5014
|
path: `${ROUTE_PREFIX}/session/summary`,
|
|
4904
|
-
handler: (
|
|
4905
|
-
handleSessionSummary(req, res);
|
|
4906
|
-
}
|
|
5015
|
+
handler: guardHandler(handleSessionSummary)
|
|
4907
5016
|
}),
|
|
4908
5017
|
ctx.webServer.register({
|
|
4909
5018
|
kind: "exact",
|
|
4910
5019
|
path: `${ROUTE_PREFIX}/agent/sessions`,
|
|
4911
|
-
handler: (
|
|
4912
|
-
handleAgentSessions(req, res);
|
|
4913
|
-
}
|
|
5020
|
+
handler: guardHandler(handleAgentSessions)
|
|
4914
5021
|
}),
|
|
4915
5022
|
ctx.webServer.register({
|
|
4916
5023
|
kind: "exact",
|
|
4917
5024
|
path: `${ROUTE_PREFIX}/agent/modes`,
|
|
4918
|
-
handler: (
|
|
4919
|
-
handleAgentModes(req, res);
|
|
4920
|
-
}
|
|
5025
|
+
handler: guardHandler(handleAgentModes)
|
|
4921
5026
|
}),
|
|
4922
5027
|
ctx.webServer.register({
|
|
4923
5028
|
kind: "exact",
|
|
4924
5029
|
path: `${ROUTE_PREFIX}/agent/send`,
|
|
4925
|
-
handler: (
|
|
4926
|
-
handleAgentSend(req, res);
|
|
4927
|
-
}
|
|
5030
|
+
handler: guardHandler(handleAgentSend)
|
|
4928
5031
|
}),
|
|
4929
5032
|
ctx.webServer.register({
|
|
4930
5033
|
kind: "exact",
|
|
4931
5034
|
path: `${ROUTE_PREFIX}/agent/create`,
|
|
4932
|
-
handler: (
|
|
4933
|
-
handleAgentCreate(req, res);
|
|
4934
|
-
}
|
|
5035
|
+
handler: guardHandler(handleAgentCreate)
|
|
4935
5036
|
}),
|
|
4936
5037
|
ctx.webServer.register({
|
|
4937
5038
|
kind: "prefix",
|
|
4938
5039
|
path: `${ROUTE_PREFIX}/api`,
|
|
4939
|
-
handler: (
|
|
4940
|
-
handleApiProxy(req, res);
|
|
4941
|
-
}
|
|
5040
|
+
handler: guardHandler(handleApiProxy)
|
|
4942
5041
|
}),
|
|
4943
5042
|
ctx.webServer.register({
|
|
4944
5043
|
kind: "prefix",
|
|
4945
5044
|
path: `${TW_PROXY_PREFIX}`,
|
|
4946
|
-
handler: (
|
|
4947
|
-
handleTwProxy(req, res);
|
|
4948
|
-
}
|
|
5045
|
+
handler: guardHandler(handleTwProxy)
|
|
4949
5046
|
})
|
|
4950
5047
|
];
|
|
4951
5048
|
return () => {
|
|
@@ -5005,12 +5102,14 @@ async function readWikiInfo(wikiPath) {
|
|
|
5005
5102
|
} catch {
|
|
5006
5103
|
throw new Error("tiddlywiki.info 不是合法 JSON;请先修复该文件再保存设置(已保留原文件)");
|
|
5007
5104
|
}
|
|
5105
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("tiddlywiki.info 顶层不是对象;请先修复该文件再保存设置(已保留原文件)");
|
|
5106
|
+
const obj = parsed;
|
|
5008
5107
|
return {
|
|
5009
|
-
...
|
|
5010
|
-
description:
|
|
5011
|
-
plugins: Array.isArray(
|
|
5012
|
-
themes: Array.isArray(
|
|
5013
|
-
languages: Array.isArray(
|
|
5108
|
+
...obj,
|
|
5109
|
+
description: obj.description,
|
|
5110
|
+
plugins: Array.isArray(obj.plugins) ? obj.plugins : [],
|
|
5111
|
+
themes: Array.isArray(obj.themes) ? obj.themes : [],
|
|
5112
|
+
languages: Array.isArray(obj.languages) ? obj.languages : []
|
|
5014
5113
|
};
|
|
5015
5114
|
}
|
|
5016
5115
|
/**
|
|
@@ -5164,6 +5263,73 @@ async function ensureLanguage(wikiPath, twRoot, lang) {
|
|
|
5164
5263
|
await writeWikiInfo(wikiPath, info);
|
|
5165
5264
|
return true;
|
|
5166
5265
|
}
|
|
5266
|
+
/**
|
|
5267
|
+
* Sentinel returned instead of a stored secret (`bridge.token` /
|
|
5268
|
+
* `ui.sendToAgent.token`) by `GET /admin/state` and by `POST /admin/config`'s
|
|
5269
|
+
* echo. `/admin/*` is an unauthenticated surface (only CSRF-hardened), and the
|
|
5270
|
+
* config tiddler legitimately carries shared tokens — handing them to any
|
|
5271
|
+
* caller of the loopback/LAN-reachable GUI is a real leak (v0.19.3). The
|
|
5272
|
+
* settings page renders the sentinel verbatim; posting it back is a no-op
|
|
5273
|
+
* (`stripMaskedSecrets` drops it), so the real value never leaves the host.
|
|
5274
|
+
*/
|
|
5275
|
+
const MASKED_SECRET = "********";
|
|
5276
|
+
/** Mask secrets (and credentials inside the git remote URL) for an HTTP caller. */
|
|
5277
|
+
function maskConfigSecrets(config) {
|
|
5278
|
+
const bridge = config.bridge ?? {};
|
|
5279
|
+
const ui = config.ui ?? {};
|
|
5280
|
+
const sendToAgent = ui.sendToAgent ?? {};
|
|
5281
|
+
const git = config.git ?? {};
|
|
5282
|
+
const maskToken = (value) => typeof value === "string" && value.length > 0 ? MASKED_SECRET : "";
|
|
5283
|
+
return {
|
|
5284
|
+
...config,
|
|
5285
|
+
git: {
|
|
5286
|
+
...git,
|
|
5287
|
+
remote: typeof git.remote === "string" ? redactRemoteUrl(git.remote) : git.remote
|
|
5288
|
+
},
|
|
5289
|
+
bridge: {
|
|
5290
|
+
...bridge,
|
|
5291
|
+
token: maskToken(bridge.token),
|
|
5292
|
+
tokenSet: typeof bridge.token === "string" && bridge.token.length > 0
|
|
5293
|
+
},
|
|
5294
|
+
ui: {
|
|
5295
|
+
...ui,
|
|
5296
|
+
sendToAgent: {
|
|
5297
|
+
...sendToAgent,
|
|
5298
|
+
token: maskToken(sendToAgent.token),
|
|
5299
|
+
tokenSet: typeof sendToAgent.token === "string" && sendToAgent.token.length > 0
|
|
5300
|
+
}
|
|
5301
|
+
}
|
|
5302
|
+
};
|
|
5303
|
+
}
|
|
5304
|
+
/**
|
|
5305
|
+
* Drop the masked placeholders from an incoming config patch so saving the
|
|
5306
|
+
* settings page never overwrites a stored secret with `********` (or the
|
|
5307
|
+
* redacted git remote with `https://***@…`). `bridge.token`/`ui.sendToAgent.token`
|
|
5308
|
+
* are cleared only when the caller actually sends an empty string.
|
|
5309
|
+
*/
|
|
5310
|
+
function stripMaskedSecrets(patch, current) {
|
|
5311
|
+
const copy = { ...patch };
|
|
5312
|
+
const cleanToken = (container) => {
|
|
5313
|
+
if (typeof container !== "object" || container === null) return container;
|
|
5314
|
+
const obj = { ...container };
|
|
5315
|
+
if (obj.token === "********") delete obj.token;
|
|
5316
|
+
delete obj.tokenSet;
|
|
5317
|
+
return obj;
|
|
5318
|
+
};
|
|
5319
|
+
if (copy.bridge !== void 0) copy.bridge = cleanToken(copy.bridge);
|
|
5320
|
+
if (copy.ui !== void 0) {
|
|
5321
|
+
const ui = typeof copy.ui === "object" && copy.ui !== null ? { ...copy.ui } : copy.ui;
|
|
5322
|
+
if (typeof ui === "object" && ui !== null && ui.sendToAgent !== void 0) ui.sendToAgent = cleanToken(ui.sendToAgent);
|
|
5323
|
+
copy.ui = ui;
|
|
5324
|
+
}
|
|
5325
|
+
if (copy.git !== void 0 && typeof copy.git === "object" && copy.git !== null) {
|
|
5326
|
+
const git = { ...copy.git };
|
|
5327
|
+
const storedRemote = typeof current.git?.remote === "string" ? current.git.remote : "";
|
|
5328
|
+
if (typeof git.remote === "string" && storedRemote.length > 0 && git.remote === redactRemoteUrl(storedRemote)) delete git.remote;
|
|
5329
|
+
copy.git = git;
|
|
5330
|
+
}
|
|
5331
|
+
return copy;
|
|
5332
|
+
}
|
|
5167
5333
|
function registerAdminRoutes(ctx, deps) {
|
|
5168
5334
|
const handleState = async (req, res) => {
|
|
5169
5335
|
try {
|
|
@@ -5193,7 +5359,7 @@ function registerAdminRoutes(ctx, deps) {
|
|
|
5193
5359
|
languages: info.languages ?? []
|
|
5194
5360
|
},
|
|
5195
5361
|
catalog,
|
|
5196
|
-
config: deps.config.get(),
|
|
5362
|
+
config: maskConfigSecrets(deps.config.get()),
|
|
5197
5363
|
git
|
|
5198
5364
|
});
|
|
5199
5365
|
} catch (err) {
|
|
@@ -5221,6 +5387,10 @@ function registerAdminRoutes(ctx, deps) {
|
|
|
5221
5387
|
const catalog = await bundledCatalog(deps.twRoot());
|
|
5222
5388
|
const known = new Set([...catalog.plugins, ...catalog.themes].map((c) => c.name));
|
|
5223
5389
|
const knownLangs = new Set(catalog.languages.map((c) => c.name));
|
|
5390
|
+
/** Snapshot for the no-op guard below (JSON compare is enough here). */
|
|
5391
|
+
const beforeInfo = JSON.stringify(info);
|
|
5392
|
+
/** Set only when the request carried a themes array with a resolvable active pick. */
|
|
5393
|
+
let activatedTheme;
|
|
5224
5394
|
const applyList = (field, raw) => {
|
|
5225
5395
|
if (!Array.isArray(raw)) return info[field];
|
|
5226
5396
|
const next = [];
|
|
@@ -5241,33 +5411,45 @@ function registerAdminRoutes(ctx, deps) {
|
|
|
5241
5411
|
}
|
|
5242
5412
|
return next;
|
|
5243
5413
|
};
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5414
|
+
try {
|
|
5415
|
+
info.plugins = applyList("plugins", body.plugins);
|
|
5416
|
+
if (Array.isArray(body.themes)) {
|
|
5417
|
+
const themeDeps = {};
|
|
5418
|
+
for (const theme of catalog.themes) if (theme.dependents && theme.dependents.length > 0) themeDeps[theme.name] = theme.dependents;
|
|
5419
|
+
const selected = applyList("themes", body.themes);
|
|
5420
|
+
if (typeof body.themeActive === "string" && body.themeActive.length > 0) {
|
|
5421
|
+
const activeName = body.themeActive;
|
|
5422
|
+
if (known.has(activeName) || info.themes.includes(activeName)) {
|
|
5423
|
+
if (!selected.includes(activeName)) selected.push(activeName);
|
|
5424
|
+
activatedTheme = activeName;
|
|
5425
|
+
}
|
|
5255
5426
|
}
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5427
|
+
info.themes = normalizeThemes(selected, themeDeps);
|
|
5428
|
+
if (activatedTheme === void 0 && info.themes.length > 0) activatedTheme = info.themes[info.themes.length - 1];
|
|
5429
|
+
} else info.themes = applyList("themes", body.themes);
|
|
5430
|
+
if (Array.isArray(body.languages)) info.languages = applyLanguages(body.languages);
|
|
5431
|
+
} catch (err) {
|
|
5432
|
+
json(res, {
|
|
5433
|
+
ok: false,
|
|
5434
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5435
|
+
}, 400);
|
|
5436
|
+
return;
|
|
5437
|
+
}
|
|
5438
|
+
const changed = JSON.stringify(info) !== beforeInfo;
|
|
5439
|
+
if (changed) {
|
|
5440
|
+
await writeWikiInfo(wikiPath, info);
|
|
5441
|
+
await deps.server.restart();
|
|
5442
|
+
}
|
|
5443
|
+
if (Array.isArray(body.themes) && activatedTheme !== void 0) {
|
|
5264
5444
|
const client = deps.getClient();
|
|
5265
5445
|
if (client !== void 0) await client.put({
|
|
5266
5446
|
title: "$:/theme",
|
|
5267
5447
|
text: `$:/themes/${activatedTheme}`,
|
|
5268
5448
|
type: "text/vnd.tiddlywiki",
|
|
5269
5449
|
tags: []
|
|
5270
|
-
}).catch(() =>
|
|
5450
|
+
}).catch((err) => {
|
|
5451
|
+
console.warn("[dsh-tiddlywiki] activating theme failed:", err);
|
|
5452
|
+
});
|
|
5271
5453
|
}
|
|
5272
5454
|
if (Array.isArray(body.languages)) {
|
|
5273
5455
|
const client = deps.getClient();
|
|
@@ -5279,13 +5461,16 @@ function registerAdminRoutes(ctx, deps) {
|
|
|
5279
5461
|
text: active,
|
|
5280
5462
|
type: "text/plain",
|
|
5281
5463
|
tags: []
|
|
5282
|
-
}).catch(() =>
|
|
5464
|
+
}).catch((err) => {
|
|
5465
|
+
console.warn("[dsh-tiddlywiki] pinning $:/language failed:", err);
|
|
5466
|
+
});
|
|
5283
5467
|
const hint = langs.length > 0 ? langs[0] : "";
|
|
5284
5468
|
if ((deps.config.get().uiLanguage ?? "") !== hint) await deps.config.set(client, { uiLanguage: hint }).catch(() => void 0);
|
|
5285
5469
|
}
|
|
5286
5470
|
}
|
|
5287
5471
|
json(res, {
|
|
5288
5472
|
ok: true,
|
|
5473
|
+
changed,
|
|
5289
5474
|
info: {
|
|
5290
5475
|
plugins: info.plugins,
|
|
5291
5476
|
themes: info.themes,
|
|
@@ -5296,7 +5481,7 @@ function registerAdminRoutes(ctx, deps) {
|
|
|
5296
5481
|
json(res, {
|
|
5297
5482
|
ok: false,
|
|
5298
5483
|
error: err instanceof Error ? err.message : String(err)
|
|
5299
|
-
},
|
|
5484
|
+
}, 500);
|
|
5300
5485
|
}
|
|
5301
5486
|
};
|
|
5302
5487
|
const handleConfig = async (req, res) => {
|
|
@@ -5311,10 +5496,10 @@ function registerAdminRoutes(ctx, deps) {
|
|
|
5311
5496
|
}, 503);
|
|
5312
5497
|
return;
|
|
5313
5498
|
}
|
|
5314
|
-
await deps.config.set(client, body);
|
|
5499
|
+
await deps.config.set(client, stripMaskedSecrets(body, deps.config.get()));
|
|
5315
5500
|
json(res, {
|
|
5316
5501
|
ok: true,
|
|
5317
|
-
config: deps.config.get()
|
|
5502
|
+
config: maskConfigSecrets(deps.config.get())
|
|
5318
5503
|
});
|
|
5319
5504
|
} catch (err) {
|
|
5320
5505
|
json(res, {
|
|
@@ -5447,51 +5632,37 @@ function registerAdminRoutes(ctx, deps) {
|
|
|
5447
5632
|
ctx.webServer.register({
|
|
5448
5633
|
kind: "exact",
|
|
5449
5634
|
path: `${ROUTE_PREFIX}/admin/state`,
|
|
5450
|
-
handler: (
|
|
5451
|
-
handleState(req, res);
|
|
5452
|
-
}
|
|
5635
|
+
handler: guardHandler(handleState)
|
|
5453
5636
|
}),
|
|
5454
5637
|
ctx.webServer.register({
|
|
5455
5638
|
kind: "exact",
|
|
5456
5639
|
path: `${ROUTE_PREFIX}/admin/info`,
|
|
5457
|
-
handler: (
|
|
5458
|
-
handleInfo(req, res);
|
|
5459
|
-
}
|
|
5640
|
+
handler: guardHandler(handleInfo)
|
|
5460
5641
|
}),
|
|
5461
5642
|
ctx.webServer.register({
|
|
5462
5643
|
kind: "exact",
|
|
5463
5644
|
path: `${ROUTE_PREFIX}/admin/config`,
|
|
5464
|
-
handler: (
|
|
5465
|
-
handleConfig(req, res);
|
|
5466
|
-
}
|
|
5645
|
+
handler: guardHandler(handleConfig)
|
|
5467
5646
|
}),
|
|
5468
5647
|
ctx.webServer.register({
|
|
5469
5648
|
kind: "exact",
|
|
5470
5649
|
path: `${ROUTE_PREFIX}/admin/restart`,
|
|
5471
|
-
handler: (
|
|
5472
|
-
handleRestart(req, res);
|
|
5473
|
-
}
|
|
5650
|
+
handler: guardHandler(handleRestart)
|
|
5474
5651
|
}),
|
|
5475
5652
|
ctx.webServer.register({
|
|
5476
5653
|
kind: "exact",
|
|
5477
5654
|
path: `${ROUTE_PREFIX}/admin/seeds`,
|
|
5478
|
-
handler: (
|
|
5479
|
-
handleSeeds(req, res);
|
|
5480
|
-
}
|
|
5655
|
+
handler: guardHandler(handleSeeds)
|
|
5481
5656
|
}),
|
|
5482
5657
|
ctx.webServer.register({
|
|
5483
5658
|
kind: "exact",
|
|
5484
5659
|
path: `${ROUTE_PREFIX}/admin/seeds/run`,
|
|
5485
|
-
handler: (
|
|
5486
|
-
handleSeedsRun(req, res);
|
|
5487
|
-
}
|
|
5660
|
+
handler: guardHandler(handleSeedsRun)
|
|
5488
5661
|
}),
|
|
5489
5662
|
ctx.webServer.register({
|
|
5490
5663
|
kind: "exact",
|
|
5491
5664
|
path: `${ROUTE_PREFIX}/admin/seeds/remove`,
|
|
5492
|
-
handler: (
|
|
5493
|
-
handleSeedsRemove(req, res);
|
|
5494
|
-
}
|
|
5665
|
+
handler: guardHandler(handleSeedsRemove)
|
|
5495
5666
|
})
|
|
5496
5667
|
];
|
|
5497
5668
|
return () => {
|
|
@@ -5565,8 +5736,16 @@ const PRIVATE_V4_PATTERNS = [
|
|
|
5565
5736
|
/^169\.254\./,
|
|
5566
5737
|
/^172\.(1[6-9]|2\d|3[01])\./,
|
|
5567
5738
|
/^192\.0\.0\./,
|
|
5739
|
+
/^192\.0\.2\./,
|
|
5740
|
+
/^192\.88\.99\./,
|
|
5568
5741
|
/^192\.168\./,
|
|
5569
|
-
/^198\.1[89]
|
|
5742
|
+
/^198\.1[89]\./,
|
|
5743
|
+
/^198\.51\.100\./,
|
|
5744
|
+
/^203\.0\.113\./,
|
|
5745
|
+
/^22[4-9]\./,
|
|
5746
|
+
/^23\d\./,
|
|
5747
|
+
/^24\d\./,
|
|
5748
|
+
/^25[0-5]\./
|
|
5570
5749
|
];
|
|
5571
5750
|
/** Parse a dotted-quad IPv4 into 4 bytes, or null. */
|
|
5572
5751
|
function parseIpv4(input) {
|
|
@@ -6045,6 +6224,9 @@ var ClipBridge = class {
|
|
|
6045
6224
|
};
|
|
6046
6225
|
const onListening = () => {
|
|
6047
6226
|
server.removeListener("error", onError);
|
|
6227
|
+
server.on("error", (err) => {
|
|
6228
|
+
this.deps.log?.(`server error after listen: ${err.message}`);
|
|
6229
|
+
});
|
|
6048
6230
|
const addr = server.address();
|
|
6049
6231
|
this.boundPort = addr.port;
|
|
6050
6232
|
resolve();
|
|
@@ -6061,7 +6243,17 @@ var ClipBridge = class {
|
|
|
6061
6243
|
this.boundPort = 0;
|
|
6062
6244
|
if (server === void 0) return Promise.resolve();
|
|
6063
6245
|
return new Promise((resolve) => {
|
|
6064
|
-
|
|
6246
|
+
let done = false;
|
|
6247
|
+
const finish = () => {
|
|
6248
|
+
if (done) return;
|
|
6249
|
+
done = true;
|
|
6250
|
+
resolve();
|
|
6251
|
+
};
|
|
6252
|
+
try {
|
|
6253
|
+
server.closeAllConnections?.();
|
|
6254
|
+
} catch {}
|
|
6255
|
+
server.close(() => finish());
|
|
6256
|
+
setTimeout(finish, 1e3).unref?.();
|
|
6065
6257
|
});
|
|
6066
6258
|
}
|
|
6067
6259
|
respond(res, payload, status = 200) {
|
|
@@ -8318,6 +8510,6 @@ function apply(ctx, rawConfig = {}) {
|
|
|
8318
8510
|
}, "dsh-tiddlywiki: host teardown");
|
|
8319
8511
|
}
|
|
8320
8512
|
//#endregion
|
|
8321
|
-
export { AGENT_WRITTEN_TAG, ALL_ARTICLES_MARKER_TITLE, ALL_ARTICLES_TEXT, ALL_ARTICLES_TITLE, AutoCommitter, CLIP_BRIDGE_BOOKMARKLET, CLIP_BRIDGE_DEFAULT_PORT, CLIP_BRIDGE_DOC_TEXT, CLIP_BRIDGE_DOC_TITLE, CLIP_BRIDGE_DRAG_HREF, CLIP_BRIDGE_MARKER_TITLE, ClipBridge, ConfigStore, DEFAULT_NOTE_TYPE, DOC_NOTE_TAG, DOC_NOTE_TEXT, DOC_NOTE_TITLE, DSH_DOCS_TAG, GitFace, HOME_DEFAULT_TIDDLERS, HOME_INDEX_ITEMS, HOME_INDEX_MARKER_TITLE, HUMAN_EDITED_TAG, MARKDOWN_PLUGIN, MENUBAR_THEME_MARKER_TITLE, MENUBAR_THEME_TEXT, MENUBAR_THEME_TIDDLER, MISSING_TYPE_FILTER, PATH_PREFIX, RENDER_BUNDLE_TEXT, RENDER_MARKER_TITLE, RENDER_PLUGIN_FILE, RENDER_PLUGIN_TITLE, SEED_DEFS, SEND_TO_AGENT_BUNDLE_TEXT, SEND_TO_AGENT_MARKER_TITLE, SEND_TO_AGENT_PLUGIN_TITLE, SESSION_SUMMARY_PREFIX, STARTER_DOCS_ITEMS, STARTER_DOCS_MARKER_TITLE, TEXT_LIST_FILTER, TW_PROXY_PATH, TW_PROXY_PREFIX, TW_WEB_HOST_DEFAULT, TW_WEB_HOST_TIDDLER, TiddlyWebClient, UI_STYLES_MARKER_TITLE, UI_STYLE_ITEMS, WikiServer, WriteConflictError, apply, assertNoConflict, assertPublicImageUrl, buildBinaryTiddler, buildClipTiddler, buildImageNoteTiddler, buildWriteTiddler, bundledCatalog, checkAllSeeds, cleanTiddler, deepMerge, defineTool, downloadClipImage, dshHomePath, ensureLanguage, ensurePlugin, ensureTwWebHost, flattenTiddlerFields, flushPendingWrites, hostAllowed, imageExtensionForMime, inject, isBinaryType, isPrivateAddress, isSafeUrl, name, needsRestartAfterSeeds, normalizeThemes, openInTwEditor, parseClipPayload, pickImageMime, readWikiInfo, registerAdminRoutes, registerRoutes, registerTiddlywikiTools, removeSeedById, resolveClipTitle, resolveTwRoot, runAllSeeds, runSeedById, sanitizeTwFragment, seedAllArticles, seedClipBridge, seedDocNote, seedHomeIndex, seedMenubarTheme, seedRenderRoute, seedSendToAgent, seedStarterDocs, seedUiStyles, unseedClipBridge, waitForFileWrite, writeSessionSummary, writeWikiInfo };
|
|
8513
|
+
export { AGENT_WRITTEN_TAG, ALL_ARTICLES_MARKER_TITLE, ALL_ARTICLES_TEXT, ALL_ARTICLES_TITLE, AutoCommitter, CLIP_BRIDGE_BOOKMARKLET, CLIP_BRIDGE_DEFAULT_PORT, CLIP_BRIDGE_DOC_TEXT, CLIP_BRIDGE_DOC_TITLE, CLIP_BRIDGE_DRAG_HREF, CLIP_BRIDGE_MARKER_TITLE, ClipBridge, ConfigStore, DEFAULT_NOTE_TYPE, DOC_NOTE_TAG, DOC_NOTE_TEXT, DOC_NOTE_TITLE, DSH_DOCS_TAG, GitFace, HOME_DEFAULT_TIDDLERS, HOME_INDEX_ITEMS, HOME_INDEX_MARKER_TITLE, HUMAN_EDITED_TAG, MARKDOWN_PLUGIN, MASKED_SECRET, MENUBAR_THEME_MARKER_TITLE, MENUBAR_THEME_TEXT, MENUBAR_THEME_TIDDLER, MISSING_TYPE_FILTER, PATH_PREFIX, RENDER_BUNDLE_TEXT, RENDER_MARKER_TITLE, RENDER_PLUGIN_FILE, RENDER_PLUGIN_TITLE, SEED_DEFS, SEND_TO_AGENT_BUNDLE_TEXT, SEND_TO_AGENT_MARKER_TITLE, SEND_TO_AGENT_PLUGIN_TITLE, SESSION_SUMMARY_PREFIX, STARTER_DOCS_ITEMS, STARTER_DOCS_MARKER_TITLE, TEXT_LIST_FILTER, TW_PROXY_PATH, TW_PROXY_PREFIX, TW_WEB_HOST_DEFAULT, TW_WEB_HOST_TIDDLER, TiddlyWebClient, UI_STYLES_MARKER_TITLE, UI_STYLE_ITEMS, WikiServer, WriteConflictError, apply, assertNoConflict, assertPublicImageUrl, buildBinaryTiddler, buildClipTiddler, buildImageNoteTiddler, buildWriteTiddler, bundledCatalog, checkAllSeeds, cleanTiddler, deepMerge, defineTool, downloadClipImage, dshHomePath, ensureLanguage, ensurePlugin, ensureTwWebHost, escapeInline, flattenTiddlerFields, flushPendingWrites, hostAllowed, imageExtensionForMime, inject, isBinaryType, isPrivateAddress, isSafeUrl, maskConfigSecrets, name, needsRestartAfterSeeds, normalizeThemes, openInTwEditor, parseClipPayload, pickImageMime, readWikiInfo, registerAdminRoutes, registerRoutes, registerTiddlywikiTools, removeSeedById, resolveClipTitle, resolveTwRoot, runAllSeeds, runSeedById, sanitizeTwFragment, seedAllArticles, seedClipBridge, seedDocNote, seedHomeIndex, seedMenubarTheme, seedRenderRoute, seedSendToAgent, seedStarterDocs, seedUiStyles, stripMaskedSecrets, unseedClipBridge, waitForFileWrite, writeSessionSummary, writeWikiInfo };
|
|
8322
8514
|
|
|
8323
8515
|
//# sourceMappingURL=index.js.map
|