dsh-tiddlywiki 0.22.8 → 0.22.9
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 +2 -0
- package/lib/index.js +14 -2
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
- package/src/host/wiki.ts +39 -4
- package/src/index.ts +2 -2
package/README.md
CHANGED
|
@@ -352,6 +352,8 @@ lib/ # 预构建产物(发布含 lib/**,提交入库;
|
|
|
352
352
|
|
|
353
353
|
> 最近几个主要版本的一句话记录(完整变更见 [Releases](https://github.com/bbqisbbq/dsh-tiddlywiki/releases) / git log)。
|
|
354
354
|
|
|
355
|
+
- **v0.22.9**(2026-09-16):**修「TiddlyWiki 编辑者署名被静默清空」**(用户实测报障:控制面板 →「信息」→「基础」里的「编辑者署名」填完保存不了、刷新就没了)。根因是 DSH 启动 TW 子进程时(匿名 loopback 分支)**没传 `anon-username`**:`get-status.js:21` 回落到 `""`,而 TiddlyWeb adaptor 判定登录用的是 `isLoggedIn = json.username !== "GUEST"`(`tiddlywebadaptor.js:93`)——`"" !== "GUEST"` 为真,于是**匿名读者被当成已登录用户**;`syncer.js:281-283` 随即在**每次页面加载**时把 `$:/status/UserName` 覆写成那个空串。名字在当次会话里看着还在,一刷新就没了;而一旦 `$:/config/SyncFilter` 放行了 `UserName`(本仓库 wiki 的现状,有一篇专门的踩坑笔记记录),这个空串还会**同步落盘并进 git**,从「内存里丢了」升级成**真实、可追溯的数据丢失**。现在匿名分支显式传 `anon-username=GUEST`(`wiki.ts` 的 `ANON_USERNAME` 常量):`isLoggedIn` 变 false → syncer **根本不再写** `UserName` → 用户填的名字稳定保留。⚠️ `GUEST` 是**承重的魔法值**,不是随便取的名字:TW 判的就是 `!== "GUEST"` 这个哨兵,填任何人名(含你自己的名字)都仍然算「已登录」,只会把署名覆盖成人名而不是空串。**为什么拖到现在**:`src/host/wiki.ts` 从 v0.16 起就写着一句注释「Passing anon-username/readers/writers here was verified to 401 every request」——它把**两个独立参数**混为一谈,等于给后来每个维护者发了张「此路不通」的牌子。实测(TW 5.4.1)只有 `readers`/`writers` 会关掉匿名访问(`server.js:63-66` 里显式的 `readers` 会让 `(anon)` 不再是 principal,于是 `isAuthorized()` 为 false,`requestHandler` 用当时为 `undefined` 的 `authenticatedUsername` 拼出 `'undefined' is not authorized` 的 401);`anon-username` **单独**传完全安全(`/status` 200、GET 200、PUT 204)。那句注释已改正并写明真实机制。**副作用(外观级)**:`$:/status/IsLoggedIn` 恒为 `no`,TW 的 SyncerDropdown 会多出一个用不上的 Login 按钮;`wiki.js:1690` 的 `generateDraftTitle` 会从 `Draft/Title` 形态切到 `Draft/Attribution` 形态(内核既有行为,与「快速笔记」的 `/edit` 草稿路由无关——后者按 `draft.of` 字段匹配,不依赖标题字面量)。**影响面**:默认 SyncFilter 的用户**不会**丢盘(`$:/status/` 被默认过滤器整个排除,空串走不到落盘),实际受数据丢失影响的只有**已经放行 `UserName` 的人**——所以这一版主要是消除一颗静默的数据丢失地雷 + 拔掉那句误导注释。守门:新增 `scripts/verify-anon-username.mjs`(进 `verify:e2e`,真实 TW 子进程)——`ANON_USERNAME` 必须字面量 `GUEST`、`/status` 必须回该哨兵、落盘署名不得被覆盖、匿名模式仍可正常写条目、auth 模式不受影响且对匿名请求仍 401,外加源码级断言(旧误导注释必须已删、鉴权分支不得顺手传 `anon-username`);**已反向验证**:把 `anon-username` 那行去掉即红 3 条。
|
|
356
|
+
|
|
355
357
|
- **v0.22.8**(2026-09-15):**第六轮代码审计修复**——host 与 client 各做一份只读审计,逐条核实后修 10 处真缺陷,并清掉一轮重复实现与死代码。① **数据丢失(P0)**:`/edit` 仍会把「调用方文本或笔记已保存正文」PUT 进**刚复用**的那个草稿,而那个草稿里装的是用户在 TW 原生编辑器里敲的**未保存内容**——触发路径极常见:打开快速笔记(标题精确到分钟)→ 写 → 关掉 → 同一分钟内再打开 → 草稿被覆盖。现在只有**我们创建的**草稿才由我们填充,复用的仅在调用方显式给了文本时才写。② **设置页超时(P0)**:`fetchJson` 展开 `init` 后又硬编码 `signal`,于是「知识库切换 / 恢复默认 / 重启 TW」的 120s 预算被 15s 掐断——宿主其实切成功了,用户却看到「切换失败」(大 wiki 冷启动实测 44s+),且重试会再走一遍停/启。③ **净化器崩服务**:`�` 这类越界数字实体会让 `String.fromCodePoint()` 抛 `RangeError`,而净化器没有 try——一条笔记正文就能让 `POST /render` 502,打坏回复流工具卡与会话「知识库」Tab。④ **「幽灵草稿」**:保存/「丢弃」后重置编辑器会触发防抖,把「空正文 + 新时间戳标题」又写成草稿,下次打开误报「已恢复未保存草稿」并跳过默认 tag。⑤ `/recent` 一直丢掉 `since`(工具与卡片都发它),于是模型看到过滤结果、人看到未过滤列表——与 v0.20.0 修过的 `/search` 同类漂移。⑥ 隐藏的 TW 面板会因为一个在途 `/status` 响应重新起一轮 30×1.5s 轮询。⑦ `/tw` 代理补齐显式方法白名单。⑧ `/render` 的 404 改为结构化异常(原先按错误消息文本正则,改词即把「不存在」变 502)。⑨ `/get` 改用共享的 `isBlockedProxyTitle()`(原为硬编码字面量副本)。⑩ 侧边栏显示名改完设置即时生效(原只挂载时读一次)。**去重/死代码**:新增 `render-fetch.ts`(唯一渲染调用)、`describeSyncResult()`(同步回执)、`formatLocalMinute()`,`ui-config.ts` 承担全部 `ui.*` 投影;删除生产零调用的 `ensureTwWebHost()`(与 `tw-web-host` seed 重复)与 `listTags()`,多个仅供模块内部使用的 `export` 与「算了却从不显示」的字段一并清除。守门:`verify-render-sanitizer.mjs` +1、`verify-frame-guards.mjs` +3、`verify-audit-fixes.mjs` +3(草稿覆盖那条**已反向验证**:还原修复即红)。
|
|
356
358
|
|
|
357
359
|
- **v0.22.7**(2026-09-14):**设置页「预览注入文本」改为按表单当前值渲染**(用户实测提问「两种内置文本形态差别是什么?为什么选了不同形态后查看当前注入文本显示的东西一样?」)。两种形态的差别其实只在 intro 一段:`slim`(默认)是一句能力概览(实测 1735 字符),`full` 换成由工具注册表实时生成的 15 行参数索引(2504 字符),三块治理约定(写入与并发 / 同步纪律 / 笔记约定)完全相同。但预览按钮此前只发 `GET /admin/prompt`,读的是 **host 已保存的有效配置**,而下拉框的值只活在浏览器 DOM 里——于是「切换形态 → 不点保存 → 点预览」看到的是逐字节相同的旧文本,用户自然以为两种形态没差别。现在:① `prompt.ts` 新增 `normalizePromptPreview()`(把不可信的 body 白名单化成 4 个字段,未知键/错类型丢弃并回落内置默认)+ `describePrompt()`(`applyPrompt()` 与预览**共用同一份拼装**,预览不可能与保存后的注入不一致);② `POST /admin/prompt` 接收 `{enabled,mode,extra,override}` 草稿并渲染全文,**写入零副作用**(不落盘、不触发 `onConfigChanged`),同源守卫 + 畸形 JSON 400;GET 保留为「已保存/正在注入」的读取;③ 设置页按钮改名「预览注入文本(按表单当前值)」并 POST 草稿,标题栏在表单有未保存改动时标注「含未保存的修改,保存后才真正注入」。守门:`verify-prompt.mjs` 新增 3 条纯函数断言(白名单、`describePrompt` 与 `buildPromptText` 逐字节一致、两种形态在草稿里必须不同),`verify-seeds-admin.mjs` 第 8 段把「POST 必须 405」换成 7 条草稿断言(slim/full 草稿不同、extra 追加、`enabled:false` 为空、GET 仍是旧的已保存状态且 `onConfigChanged` 未触发、垃圾字段回落、畸形 JSON 400、跨站 403)。
|
package/lib/index.js
CHANGED
|
@@ -1020,6 +1020,18 @@ const KILL_GRACE_MS = 3e3;
|
|
|
1020
1020
|
const LOG_BUFFER_LIMIT = 200;
|
|
1021
1021
|
/** One-shot scaffold timeout for `--init server`. */
|
|
1022
1022
|
const INIT_TIMEOUT_MS = 3e4;
|
|
1023
|
+
/**
|
|
1024
|
+
* `anon-username` for the anonymous loopback spawn (v0.22.9).
|
|
1025
|
+
*
|
|
1026
|
+
* This MUST stay the literal `"GUEST"`. TW's TiddlyWeb adaptor derives its
|
|
1027
|
+
* login flag as `json.username !== "GUEST"` (tiddlywebadaptor.js:93), so any
|
|
1028
|
+
* other value — including any real person's name — still reports as logged in,
|
|
1029
|
+
* and syncer.js then overwrites `$:/status/UserName` with whatever `/status`
|
|
1030
|
+
* returns. Only this sentinel makes the syncer leave the user's signature
|
|
1031
|
+
* alone. Exported so the regression gate asserts the exact string instead of
|
|
1032
|
+
* re-typing it.
|
|
1033
|
+
*/
|
|
1034
|
+
const ANON_USERNAME = "GUEST";
|
|
1023
1035
|
/** Resolve the absolute entry of the installed `tiddlywiki` package. */
|
|
1024
1036
|
function resolveTwEntry() {
|
|
1025
1037
|
return createRequire(import.meta.url).resolve("tiddlywiki/tiddlywiki.js");
|
|
@@ -1217,7 +1229,7 @@ var WikiServer = class {
|
|
|
1217
1229
|
args.push(`password=${this.options.password ?? ""}`);
|
|
1218
1230
|
args.push(`readers=${this.options.username}`);
|
|
1219
1231
|
args.push(`writers=${this.options.username}`);
|
|
1220
|
-
}
|
|
1232
|
+
} else args.push(`anon-username=${ANON_USERNAME}`);
|
|
1221
1233
|
this.log(`spawn: ${process.execPath} ${args.map((a) => /^password=/.test(a) ? "password=***" : a).join(" ")}`);
|
|
1222
1234
|
const child = spawn(process.execPath, args, {
|
|
1223
1235
|
cwd: this.wikiPath,
|
|
@@ -10036,6 +10048,6 @@ function apply(ctx, rawConfig = {}) {
|
|
|
10036
10048
|
}, "dsh-tiddlywiki: host teardown");
|
|
10037
10049
|
}
|
|
10038
10050
|
//#endregion
|
|
10039
|
-
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, DEFAULT_PROMPT_MODE, 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, LOCATION_STATE_VERSION, MARKDOWN_PLUGIN, MASKED_SECRET, MENUBAR_THEME_MARKER_TITLE, MENUBAR_THEME_TEXT, MENUBAR_THEME_TIDDLER, MISSING_TYPE_FILTER, PATH_PREFIX, PROMPT_GOVERNANCE_BLOCKS, PROMPT_MODES, PROMPT_SECTION_NAME, PROMPT_SECTION_ORDER, READY_HARD_FACTOR, READY_POLL_MS, READY_SLOW_POLL_MS, READY_TIMEOUT_DEFAULT_MS, READY_TIMEOUT_MAX_MS, READY_TIMEOUT_MIN_MS, RENDER_BUNDLE_TEXT, RENDER_MARKER_TITLE, RENDER_PLUGIN_FILE, RENDER_PLUGIN_TITLE, SEED_DEFS, SEED_MARKER_VERSION, 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, TRASH_INDEX_TITLE, TRASH_PREFIX, TW_PROXY_PATH, TW_PROXY_PREFIX, TW_WEB_HOST_DEFAULT, TW_WEB_HOST_TIDDLER, TiddlyWebClient, TrashIndexUnavailableError, UI_STYLES_MARKER_TITLE, UI_STYLE_ITEMS, WikiServer, WriteConflictError, apply, assertNoConflict, assertPublicImageUrl, awaitReady, buildBinaryTiddler, buildClipTiddler, buildImageNoteTiddler, buildPromptText, buildWriteTiddler, bundledCatalog, checkAllSeeds, cleanTiddler, clearLocationState, deepMerge, defaultLocationStateFile, defineTool, describePrompt, docNoteText, downloadClipImage, dshHomePath, ensureLanguage, ensurePlugin, escapeInline, escapePromptBraces, expandEnvPath, flattenTiddlerFields, flushPendingWrites, hashText, hostAllowed, imageExtensionForMime, inject, isBinaryType, isPrivateAddress, isSafeUrl, listWikiCandidates, locationPath, maskConfigSecrets, name, needsRestartAfterSeeds, normalizeLocation, normalizePromptMode, normalizePromptPreview, normalizeReadyTimeoutMs, normalizeThemes, openInTwEditor, parseClipPayload, parseSeedMarker, pickImageMime, readActiveThemeName, readLocationState, readSeedMarker, readWikiInfo, readyHardTimeoutMs, registerAdminRoutes, registerRoutes, registerTiddlywikiTools, removeSeedById, resolveClipTitle, resolveTwRoot, runAllSeeds, runSeedById, sanitizeTwFragment, seedAllArticles, seedClipBridge, seedDocNote, seedHomeIndex, seedMenubarTheme, seedRenderRoute, seedSendToAgent, seedStarterDocs, seedUiStyles, stripMaskedSecrets, switchWiki, tiddlywikiToolSummary, toolSignatureLines, unseedClipBridge, waitForFileWrite, writeLocationState, writeSeedMarker, writeSessionSummary, writeWikiInfo };
|
|
10051
|
+
export { AGENT_WRITTEN_TAG, ALL_ARTICLES_MARKER_TITLE, ALL_ARTICLES_TEXT, ALL_ARTICLES_TITLE, ANON_USERNAME, 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, DEFAULT_PROMPT_MODE, 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, LOCATION_STATE_VERSION, MARKDOWN_PLUGIN, MASKED_SECRET, MENUBAR_THEME_MARKER_TITLE, MENUBAR_THEME_TEXT, MENUBAR_THEME_TIDDLER, MISSING_TYPE_FILTER, PATH_PREFIX, PROMPT_GOVERNANCE_BLOCKS, PROMPT_MODES, PROMPT_SECTION_NAME, PROMPT_SECTION_ORDER, READY_HARD_FACTOR, READY_POLL_MS, READY_SLOW_POLL_MS, READY_TIMEOUT_DEFAULT_MS, READY_TIMEOUT_MAX_MS, READY_TIMEOUT_MIN_MS, RENDER_BUNDLE_TEXT, RENDER_MARKER_TITLE, RENDER_PLUGIN_FILE, RENDER_PLUGIN_TITLE, SEED_DEFS, SEED_MARKER_VERSION, 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, TRASH_INDEX_TITLE, TRASH_PREFIX, TW_PROXY_PATH, TW_PROXY_PREFIX, TW_WEB_HOST_DEFAULT, TW_WEB_HOST_TIDDLER, TiddlyWebClient, TrashIndexUnavailableError, UI_STYLES_MARKER_TITLE, UI_STYLE_ITEMS, WikiServer, WriteConflictError, apply, assertNoConflict, assertPublicImageUrl, awaitReady, buildBinaryTiddler, buildClipTiddler, buildImageNoteTiddler, buildPromptText, buildWriteTiddler, bundledCatalog, checkAllSeeds, cleanTiddler, clearLocationState, deepMerge, defaultLocationStateFile, defineTool, describePrompt, docNoteText, downloadClipImage, dshHomePath, ensureLanguage, ensurePlugin, escapeInline, escapePromptBraces, expandEnvPath, flattenTiddlerFields, flushPendingWrites, hashText, hostAllowed, imageExtensionForMime, inject, isBinaryType, isPrivateAddress, isSafeUrl, listWikiCandidates, locationPath, maskConfigSecrets, name, needsRestartAfterSeeds, normalizeLocation, normalizePromptMode, normalizePromptPreview, normalizeReadyTimeoutMs, normalizeThemes, openInTwEditor, parseClipPayload, parseSeedMarker, pickImageMime, readActiveThemeName, readLocationState, readSeedMarker, readWikiInfo, readyHardTimeoutMs, registerAdminRoutes, registerRoutes, registerTiddlywikiTools, removeSeedById, resolveClipTitle, resolveTwRoot, runAllSeeds, runSeedById, sanitizeTwFragment, seedAllArticles, seedClipBridge, seedDocNote, seedHomeIndex, seedMenubarTheme, seedRenderRoute, seedSendToAgent, seedStarterDocs, seedUiStyles, stripMaskedSecrets, switchWiki, tiddlywikiToolSummary, toolSignatureLines, unseedClipBridge, waitForFileWrite, writeLocationState, writeSeedMarker, writeSessionSummary, writeWikiInfo };
|
|
10040
10052
|
|
|
10041
10053
|
//# sourceMappingURL=index.js.map
|