Skip to content

dsh-whale-post-bus

Verified

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

核心:信封/签名/握手/幂等/落盘

Install

dsh plugin add dsh-whale-post-bus

Confirm the layer applied with dsh --profile default --dump-config — see the install guide.

Source

Tags

Creators

Readme

dsh-whale-post-bus

核心:信封 / 摘要+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 ✓)。 ★★仍未测 ✗:seen 状态数组超长后的截断路径(只做了人工造件)。
  • ★只解决"同一台机器" ✓ —— 信箱是盘上的目录;跨机器要靠共享目录 ✓。