Chuyển đến nội dung chính

dsh-whale-post-bus

Đã xác minh

dsh-whale-post-bus · v0.6.1 · MIT

核心:信封/签名/握手/幂等/落盘 · Core: envelope, signing, handshake, idempotence, on-disk store

Cài đặt

dsh plugin add dsh-whale-post-bus

Xác nhận layer đã áp bằng dsh --profile default --dump-config — xem hướng dẫn cài plugin.

Mã nguồn

Thẻ

Tác giả

Readme

dsh-whale-post-bus

English: bus README in English

核心:信封 / 摘要+HMAC 签名 / 握手 / 幂等 / 落盘

  • 提供接口:whale.bus
  • 依赖接口:whale.roster、whale.types、whale.gate、whale.deliver、whale.verify (全部可选 —— 拿不到就退化成内置样例/跳过对应策略,绝不把名单写进核心)
  • 自测:node selftest.mjs(只看退出码:0 过/非 0 不过)

接口带 apiVersion;核心不认识任何成员名与类型标识 —— 名单与类型一律从接口取。

它回答哪三个问题(都写明了为什么)

  1. 核心不认识任何名字与类型 —— 名单与类型一律从接口取;拿不到接口就退化成内置样例。
  2. 默认离线 —— 离线 = 信留在对方信箱里等人来收;在线 = 立刻投出去(要叫醒对方)。 mode 取值写错一律拒发,不许悄悄降级("急件写成 ONLINE 就永远叫不醒人"这种事故我们栽过)。
  3. 只投活体;不活就不投也不消费 —— 对方没有活体会话 ⇒ 信原样留在信箱里 ⇒ 信只会晚到,不会不到。

接口

方法 一句话
send(letter) 发一封:{ as, to, subject, body, mode, type, re, force };拒发抛错(CLI 里=退出码非 0)
pump({ as, keep, inject, reader }) 收信(见下"收信语义")
verify(letter) 查信封的问题,返回问题数组(空=没问题)
hello({ as }) 握手;写 hello/<as>.json
format(env) 把信封排成给人看的文本
paths()/root()/keyHex()/digest(s)/seal(f)/sign(env)/loadState(as) 底层零件(自测、工具、闸用)

收信语义(这一段最容易被误解)

pump 默认会消费(搬进 seen/ + 写 ack)—— 但当收信人此刻没有活体会话、也没有任何东西声明"我就是读者"时,它一封都不消费: 信原样留在 inbox/(不搬 seen、不删原件、不写 ack)。

keep 行为
不传 没有读者就不消费(默认)
true 只看不消费
false 显式消费
函数 (letter) => boolean 逐封判

另外:reader: true ⇒ 显式声明"我就是读者"(CLI 把信打到终端时就是这么做的); 给了 inject 就会真调它,它抛异常 ⇒ 不消费(交不出去的信,绝不当成交出去了)。

配置

字段 默认 含义
root WHALE_POST_ROOT ⇒ ./.whale-mail 信箱根(信就落在这里)
keyFile <root>/signing.key 签名密钥(首用时自动生成 32 字节)
maxBody 64 KiB 正文上限
requireHello true 三态:默认 true = 照发 + 明示"降级为离线"(见下"握手");false = 不检查;'reject' = 旧版的"拒发"(老部署要旧行为就写它)。离线件根本不看握手
helloMaxAgeMs 24 小时 握手多旧算过期
helloClockSkewMs 60 秒 时钟容差:hello 的 sentAtMs 比"现在"还晚多少以内不算未来(机器差几秒很常见)。未来时间戳一律判不新鲜(设计文档 S6h-④:否则伪造一个 2099 年的 hello 就能让握手闸形同虚设;fail-safe:误判只许偏向离线)
defaultType 'direct' 配 null ⇒ 没写类型的信直接拒发(不替调用方猜)
offlineOnlyFlag 不配 三态:不配 ⇒ 不启用;配属性名 ⇒ 照发 + 明示wakePrediction.offlineOnly(设计文档判据 58-62:不再拒发 —— "叫不醒就留箱等人,但要大声说明");offlineOnlyMode: 'reject' ⇒ 旧版的拒发
dormantFlag 不配 对被明确标成休眠的成员 ⇒ 退信(S11):不落它信箱/进班级 退信/(没删)/留一份 *.why.txt 说明/结果里 verdict: 'bounced' + bounced 名单明示/不占配额(它在 gate.record 之前就返回)。不许根据"多久没 hello"自己猜休眠。只部分休眠(发给组)⇒ 只投醒着的 + skippedDormant

结构化回执(2026-10-10 按设计文档加:今天的设计目标是 2026-10-06 01:5x)

一张回执说清两件事(设计文档判据 87-90):

字段 说什么
recipientState 我(收件人)当时的状态 —— online/stale-online/offline("我在不在线"=我自己发出去的 hello 新不新鲜)
disposition 这封信的去向 —— 见下表
disposition 什么时候
accepted-online 在线签收
delivered-offline-by-stale 发件人没等到我的新鲜 hello ⇒ 已降级为离线寄达
delivered-offline-by-declaration 我声明过"只收离线" ⇒ 这封在线件按离线寄达
delivered-offline 本来就是离线件("降级"只对在线件有意义)
refused 拒收(写在"验签不过"那条路上)

disposition 优先取发件人写下的投递说明(设计文档原话:"那才是当时怎么判的")—— 所以信封里带一个 deliveryNote('offline-only'/'no-handshake'):"发件人写下的"必须随信过去, 光看 mode 推不出"为什么走了离线"。

还有两个字段:by(谁回的)/ok(true;拒收时为 false)/note(说明)。 每份 ack 各写各的文件(<id>.<by>.ack.json —— 群发时多个收端不会撞车)。

握手(2026-10-10 按设计文档改:未握手不再拒发)

设计文档判据 1-3 +「今天的设计目标是 2026-10-06 01:5x」:"叫不醒就留箱等人,但要大声说明" 为什么"拒发"是错的:"拒发"是更早的版本 —— 而它是反的:信根本没出去, 而发信人还以为"协议不让发" —— 这跟"信只会晚到,不会不到"是拧着的。

requireHello 行为
其它真值(默认 true) 照发 + 明示"对它们降级为离线" —— 返回值里 wakePrediction.willWait 列出"叫不醒、信会留在箱里等"的人(不许静默)
false 不检查握手(也不明示)
'reject' 旧版的"拒发"(要旧行为就写它 ⇒ 老部署一字不变)

三条配套口径:① 离线件根本不看握手(离线件躺着等人,握手管不着它); ② 握过手的人不该出现在 willWait 里(别乱报警);③ --force 照旧跳过检查。

信封与签名(想自己实现一个端,照这个来)

  • 签名域(FIELD_ORDER):v, kind, id, from, to, seq, subject, body, sha256, sentAtMs, type, mode, re, hop mode 也在里面(漏签它 ⇒ 别人能把它从离线改成在线)。
  • fail-closed 双向:seal() 见未登记字段直接抛;verify() 见未登记字段直接拒 ("加字段忘了进签名域"=那个字段可被随便改、验签照样过)。
  • 已知但已废弃的字段(LEGACY_FIELDS,2026-10-10 从共享邮局根里的真信上学到的): 班级里那套的过渡期真把 auth 写进过信封(更晚的决定是"桶分只看 mode、不看 auth ⇒ 信封格式一字未改"), 于是那些历史信在我们这儿被判"没进签名域"直接退掉 —— 我们的实现在共享根里真退过三封。 口径:旧信放行(当它是历史),新信照样 fail-closed —— seal() 仍然拒 auth, 而 verify() 放过它(它已无语义 ⇒ 改它也没用);别的未知字段仍然拒。
  • 落盘一律先 .tmp 再 rename(半截文件=一次假死,我们吃过这个亏)。
  • 幂等靠 seen/ 目录本身(状态数组会被截断/丢;只靠数组 ⇒ 重投同一封就会二次交付)。
  • 发号在闸之后(被拒的信不该烧掉一个序号 ⇒ 水位与真实发信量对得上)。
  • 老信没有 mode ⇒ 按"在线"(不把旧信闷死)。

边界(写清楚,别当成保证)

  • 它不认识任何名字 ⇒ provider 撒谎,核心拦不住(这是有意的设计选择 ⇒ 要防就在 provider 那层加校验)。
  • 并发抢号:已测(2026-10-10) —— node scripts/racetest.mjs:12 路真子进程同时 send ⇒ 退出码全 0 + 投出 12 封 + seq 全唯一(1..12);还复现"state 被写倒退"(把 nextSeq 写回 1 ⇒ 新信仍拿新号,因为水位线 state/<as>.seq 兜底)。 负向测试:把认领机制整个短路(纯"读改写")⇒ 24 路跑 3 轮、轮轮撞号(唯一 seq 8/7/8)。 截断路径:已测(2026-10-10 从“未测”改成“已测”)—— seen/ 目录本身 就是永久证据(状态数组会被 slice(-2000) 截断);判据把数组截空、 再把同一封信放回信箱 ⇒ 必须仍判重复;负向证明:只去掉 existsSync 那一路 ⇒ 判据立刻红。
  • 只解决"同一台机器" —— 信箱是盘上的目录;跨机器要靠共享目录。