Skip to content

Releases: caishengshuo/beforewrite

BeforeWrite v5.1.0 — 排掉上一版的雷:越权回档、二进制毁文件

Choose a tag to compare

@caishengshuo caishengshuo released this 25 Oct 03:36

这一版只做一件事:把上一版埋的雷排掉

v5.0.0 把界面做出来了,但审的时候发现七个问题,其中两个会直接毁数据。v5.1.0 全部修完,并在本机实测通过。

严重

1. 回档接口是 GET,且不校验来源(越权写)

GET /beforewrite/api/restore?id=N 会直接改磁盘,而这条前缀路由不带 GUI 的授权。也就是说,任何网页只要写一句

<img src="http://127.0.0.1:3080/beforewrite/api/restore?id=3">

浏览器就会替你发出请求,你的文件被静默回滚,页面上什么都不用发生。

现在:回档只认 POST(其余动词 405),并且所有接口都做同源校验(sec-fetch-site 必须是 same-origin/none;带 origin 时要求 host 相等),跨站一律 403。

2. 二进制文件被回档 → 文件报废

快照对任何 ≤2 MiB 的文件都存字节,但回档是「把字节当 UTF-8 文本写回去」。对 PNG / JPG / zip 回档一次,得到的是一堆 U+FFFD,原图永久损坏。

现在:存的时候按内容判定二进制(出现 NUL,或严格 UTF-8 解码失败),打 binary 标记;diff 和 restore 直接拒绝并给出内容块路径,让人工取走;界面上这些行也不再画「回档」按钮。

中等

3. 回档不带版本号 —— fs.writeText(target, text) 是无条件覆盖。回档那一秒如果别的程序刚改过文件,那笔改动无声消失。现在先 stat 取 version 再带 expected 写入,冲突就明确报错「文件刚被别处改过,再点一次」。

4. 诊断在 256KB 处静默断线 —— debug.log 写满 256KB 后直接 return,之后一条都不写,排查时线索凭空消失。现在滚动成 debug.log.1 继续写。

5. 时间轴一次返回整条会话 —— 几百上千条事件全丢给浏览器渲染。现在默认返回最近 400 条,附 total 与 truncated,界面明确写出「更早的 N 条没画出来」。

轻

6. 对不存在的路径打点仍报成功 —— 现在明确说明「这个路径现在不存在,已记下这件事,但没有内容可回滚」。

7. 快照同步写盘阻塞事件循环 —— 最多 2 MiB 的 writeFileSync 改成 fs/promises 异步写。

实测

  • node --check 两个文件通过,离线冒烟测试全绿(含「rm 掉文件后从快照救回」这条,返回 operation=create)。
  • 本机重启后实测:ping 返回 version:"v5";GET /restore → 405;带 Origin: https://evil.example 的请求 → 403;timeline 带 total / truncated。

接口变更(如果你写过调用)

之前 现在
GET /beforewrite/api/restore?id=N POST /beforewrite/api/restore?id=N
timeline 返回全部 timeline?sid=…&limit=…,默认 400,附 total/truncated

English

v5.1.0 is a hardening release. Two of the seven fixes are data-safety critical:

  • The restore endpoint was a plain GET with no origin check, so any web page could roll local files back with an <img> tag. It is now POST-only and same-origin-checked (403 otherwise, 405 for other verbs).
  • Restoring a binary snapshot wrote raw bytes back as UTF-8 text, destroying images. Binary content is now detected at snapshot time, flagged, and refused by both diff and restore with the blob path handed back instead.

Also: restores carry an expected version (concurrent external edits are reported, not overwritten), debug.log rotates instead of going silent at 256 KB, the timeline API is capped at the 400 most recent rows and reports total/truncated, manual snapshots of non-existent paths say so, and blob writes are asynchronous.

BeforeWrite v5.0.0 — 图形时间轴:一条时间轴,点一下就回到那一刻

Choose a tag to compare

@caishengshuo caishengshuo released this 24 Oct 22:32

BeforeWrite v5.0.0 — 有界面了:一条时间轴,点一下就回到那一刻

这一版把 BeforeWrite 从「只能命令行查」变成「打开就能看、点一下就能退」。

新增

图形时间轴(界面半边 client.js)

  • 界面右上角(顶栏下方)多一个 ⏱ 时光机 按钮,点开是全屏面板。
  • 左栏:这台机器上跑过的每个会话,带「最后活动时间 / 事件数 / 台词数 / 快照数」,点一个切一条时间轴。
  • 右栏:该会话的完整时间轴,按时间合并三类记录——
    • 说过 你在这一轮真正打出去的话(注入型上下文会被过滤,不算台词)
    • 动作 DSH 调了哪个工具、参数前 200 字
    • 快照 这一轮它改了哪个文件、多少字节
  • 每条快照右侧一个 回档 按钮,点了就把那个文件还原到那一刻;旁边的 diff 可以先看差在哪。

HTTP 接口(宿主半边自带,界面就靠它取数)

接口 作用
GET /beforewrite/api/sessions 会话列表
GET /beforewrite/api/timeline?sid=… 某会话的时间轴
GET /beforewrite/api/diff?id=… 某条快照的 diff
GET /beforewrite/api/restore?id=… 把文件还原到该快照

第 7 个动作 snapshot:给 shell 里 rm / sed -i / 重定向这类绕过文件服务的破坏性操作,提前打点。

修复(都是实测踩出来的)

  • 客户端模块 id 必须逐字等于包名,否则 __ModuleLoader__.load 报 loaded without registering,而且会拖死整个 bundle(宿主半边一起不上线)。
  • apply 执行时 ctx.get('webServer') 还是 undefined,路由挂不上 → 改成每 500ms 轮询、最多等 2 分钟。
  • require('styles') 在宿主里取不到,只靠它整块面板会变成无样式裸文字盖在对话上 → 改成自己往 document.head 插 <style>。
  • 悬浮按钮从右下角移到右上角,不再盖住 dsh-purge 的 dock 按钮。
  • 时间轴不再把注入型消息当作用户台词。

安装

# 方式一:本地目录
# 在 DSH 里执行 plugin_manager install_bundle,目标是这个目录
# 方式二:先克隆再装
git clone https://github.com/caishengshuo/beforewrite.git

装完刷新页面,右上角出现「⏱ 时光机」。

仍然有的局限

  1. 拦不住 shell —— rm、sed -i、重定向都不经过文件服务,需要手动 snapshot。
  2. 只管文本,二进制文件无法 diff。
  3. 仓库是本地的,不跨机器同步。
  4. 索引只增不减,磁盘会慢慢长。
  5. 工具名固定 time_machine,不随项目名变化。

English

BeforeWrite v5 adds a GUI: a per-session timeline (what you said / which tools ran / which files were snapshotted) with a one-click restore button on every snapshot. The timeline is fed by the host plugin's own HTTP API, and the UI half is plain JS mounted on the shell.overlay slot — no build step.

Also fixed: client module id must equal the package name (otherwise the whole bundle fails to activate), webServer is not yet available during apply (now polled), and require('styles') is unavailable in the host (styles are now injected into document.head directly).