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/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 { access, mkdir, readFile, readdir, rename, stat, writeFile } from "node:fs/promises";
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").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 === void 0 || !state.exists) bits.push("⚠️ 已删除/不存在");
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
- }, 500);
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
- }, 500);
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
- }, 500);
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
- }, 500);
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
- }, 500);
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
- }, 500);
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
- }, 500);
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
- }, 500);
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
- }, 500);
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
- }, 500);
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
- }, 500);
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
- }, 500);
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
- }, 500);
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 = name;
4616
- for (let i = 1; i <= 1e4; i++) {
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 access(join(filesDir, candidate));
4619
- } catch {
4620
- break;
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
- await writeFile(join(filesDir, candidate), buf);
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(3e4)
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", () => resolveP());
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: (req, res) => {
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: (req, res) => {
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: (req, res) => {
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: (req, res) => {
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: (req, res) => {
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: (req, res) => {
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: (req, res) => {
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: (req, res) => {
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: (req, res) => {
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: (req, res) => {
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: (req, res) => {
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: (req, res) => {
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: (req, res) => {
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: (req, res) => {
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: (req, res) => {
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: (req, res) => {
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: (req, res) => {
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: (req, res) => {
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
- ...parsed,
5010
- description: parsed.description,
5011
- plugins: Array.isArray(parsed.plugins) ? parsed.plugins : [],
5012
- themes: Array.isArray(parsed.themes) ? parsed.themes : [],
5013
- languages: Array.isArray(parsed.languages) ? parsed.languages : []
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
- info.plugins = applyList("plugins", body.plugins);
5245
- let activatedTheme;
5246
- if (Array.isArray(body.themes)) {
5247
- const themeDeps = {};
5248
- for (const theme of catalog.themes) if (theme.dependents && theme.dependents.length > 0) themeDeps[theme.name] = theme.dependents;
5249
- let selected = applyList("themes", body.themes);
5250
- if (typeof body.themeActive === "string" && body.themeActive.length > 0) {
5251
- const activeName = body.themeActive;
5252
- if (known.has(activeName) || info.themes.includes(activeName)) {
5253
- if (!selected.includes(activeName)) selected.push(activeName);
5254
- activatedTheme = activeName;
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
- info.themes = normalizeThemes(selected, themeDeps);
5258
- if (activatedTheme === void 0 && info.themes.length > 0) activatedTheme = info.themes[info.themes.length - 1];
5259
- } else info.themes = applyList("themes", body.themes);
5260
- if (Array.isArray(body.languages)) info.languages = applyLanguages(body.languages);
5261
- await writeWikiInfo(wikiPath, info);
5262
- await deps.server.restart();
5263
- if (activatedTheme !== void 0) {
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(() => void 0);
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(() => void 0);
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
- }, 400);
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: (req, res) => {
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: (req, res) => {
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: (req, res) => {
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: (req, res) => {
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: (req, res) => {
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: (req, res) => {
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: (req, res) => {
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
- server.close(() => resolve());
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