Skip to content

Changelog

All notable changes to @daomessage_sdk/sdk are documented here.

Format: Keep a Changelog


[1.0.39] / PWA [1.0.48] · 2026-05-13

SDK 1.0.39 配套 PWA 1.0.48,主题 iOS PWA 推送可观测性 + 错误诊断

🔒 关于 iOS 推送 key 的澄清

承接 1.0.47, 用户问"iOS 推送需要填什么 key"。答: 仅 VAPID 公私钥 (你自己用 webpush.GenerateVAPIDKeys() 本地生成, 免费, 放服务端 .env)。 不需要 Apple Developer 账号 / APNs Auth Key / .p8 / Team ID

原因: Apple 在 iOS 16.4 (2023-03) 实现 Web Push 时, 主动接入 W3C 标准 — 我们的服务端只发标准 VAPID + Web Push, Apple Safari Push Service 在 Apple 内部 自动转 APNs 推送到 iOS 设备, 开发者无感。

iOS / Android Chrome / Firefox / 桌面 Chrome 全部走同一套服务端代码。

✨ Added · SDK 1.0.39

PushNotificationError 语义化错误类型

之前 enablePushNotifications 失败只 console.warn, UI 拿到 generic Error 无法定位是哪一步出错。现在抛出 PushNotificationError 带 6 种 kind:

typescript
class PushNotificationError extends Error {
  kind: 'no_push_manager' | 'no_vapid_key' | 'subscribe_failed' |
        'permission_denied' | 'register_failed' | 'unknown'
  cause?: unknown
}

UI 可按 kind 分支显示具体修复建议(参见下面 PWA 1.0.48 SettingsTab)。

client.push.diagnose() 推送链路体检

typescript
const diag = await client.push.diagnose()
// diag.overall: 'ok' | 'warn' | 'bad' | 'unknown'
// diag.checks: {
//   pushApiSupport, serviceWorker, permission,
//   subscription, serverVapid, standalone
// }

每项检查返回 { ok: boolean, detail: string }, 让 UI 一次拿到完整诊断, 不用 散在多处自行实现。

subscribe() 失败按 DOMException 区分

  • NotAllowedErrorpermission_denied
  • 其他 → subscribe_failed 并提示"iOS 必须 standalone 模式"

新导出

  • PushNotificationError (class)
  • PushDiagnostics (type)

✨ Added · PWA 1.0.48

SettingsTab「离线推送」点击失败显示语义化 toast

之前点了开推送如果失败, 用户只看到 console.warn 没反馈。现在按 PushNotificationError.kind 显示具体提示 + 修复路径:

kind用户看到的提示
no_push_manager浏览器不支持推送 — 用 iOS Safari / Android Chrome / 桌面 Chrome
no_vapid_key服务端配置缺失 — 联系运营方
permission_denied通知权限被拒 — iOS 设置 / 浏览器锁图标 → 通知 → 允许
subscribe_failed订阅失败 — iOS 必须装到主屏后再开
register_failed上报服务端失败 — 检查网络

加 iOS pre-check 提前拦下两种死局:

  • iOS 浏览器模式(非 standalone) → 不调 requestPermission 直接提示装主屏
  • iOS < 16.4 → 不调 requestPermission 直接提示升级系统

避免用户点完才发现走不通。

PwaDiagnostics 重构 + 增强

  • 复用 lib/platform.ts 替代之前本地重复实现的 isIOS / isStandalone
  • 接入 client.push.diagnose() 拿推送链路 6 项检查
  • 9 项总检查(原 6 项 + iOS 版本 / 订阅 / VAPID 可达性)
  • 快捷修复按钮:
    • 「请求通知权限」(权限 default 时显示)
    • 「开启推送」(已授权但本地未订阅时显示)
    • 「锁定存储」(Safari ITP 7 天清理防护)
    • 「重新检查」(刷新所有诊断项)
  • iOS 专项 howTo:
    • 未装主屏 → "点 Safari 分享按钮 → 添加到主屏幕..."
    • iOS < 16.4 → "升级到 iOS 16.4 或更新版本"
    • 权限拒了 → "iOS 设置 → 通知 → DAO Message → 打开「允许通知」"
  • 关键修复: PwaDiagnostics 之前写好了但没人引用, 现在挂到 SettingsTab

文件变更

SDK:
  sdk-typescript/src/push/manager.ts   (PushNotificationError + diagnose + 错误分类)
  sdk-typescript/src/index.ts          (新增导出)
  sdk-typescript/package.json          (1.0.38 → 1.0.39)

PWA:
  template-app-pwa/src/components/pwa/PwaDiagnostics.tsx  (重构 + 增强)
  template-app-pwa/src/components/tabs/SettingsTab.tsx    (错误 toast + 挂 PwaDiagnostics)
  template-app-pwa/src/App.tsx                            (版本 banner)
  template-app-pwa/package.json                           (1.0.47 → 1.0.48, SDK ^1.0.39)
  template-app-pwa/package-lock.json                      (锁 1.0.39)

[1.0.47] · 2026-05-13 · PWA only

仅 PWA 模板改动, SDK 未发版 (npm @daomessage_sdk/sdk 仍是 1.0.38)。

✨ Added · iOS 第二条 PWA 安装路径: .mobileconfig 描述文件

背景: 当前 iOS 用户唯一安装路径是 Safari → 分享 → 添加到主屏(4 步, 找按钮), 转化率不理想。Apple 还提供一种官方路径: 通过 .mobileconfig 配置描述文件

  • com.apple.webClip.managed payload, 可以把网站打包成"系统级安装"快捷方式。

1.0.47 把这条路径作为 InstallGate iOS 分支的第二个 tab, 与原有"标准方式" 并列展示。用户自己选择用哪一种, 不强推。

新文件

src/lib/mobileconfigGenerator.ts

客户端 JS 生成 .mobileconfig XML (不走服务端) 的好处:

  • Fork 用户部署到自己域名时, 自动用 location.origin 适配
  • 与"零知识 / 零中介"铁律一致, 不需要服务端参与
  • 用户可自定义 Label / 域名 / 图标

功能:

  • generateWebClipMobileConfig(opts) — 生成完整 plist XML 字符串
  • downloadWebClipMobileConfig(opts) — 触发浏览器下载, MIME application/x-apple-aspen-config, iOS Safari 自动进入「下载描述文件」流程

XML 结构:

  • 外层 Configuration profile (UUID / Identifier / Organization / ConsentText 中英双语)
  • 内层 com.apple.webClip.managed payload (URL / Label / 180x180 PNG base64 / FullScreen=true / Precomposed=true / IsRemovable=true)
  • ConsentText 透明说明: 不获取设备数据 / 不修改系统设置 / 不安装证书 / 不进行设备管理

修改 InstallGate iOS UI

InstallGate.tsx iOS 分支重构:

  • 加 tab 切换 (标准方式 / 一键安装), 默认显示「标准方式」(降低误导)
  • 一键安装 tab 包含:
    • 显眼的「未签名警告」 amber 横幅 + 详细解释为什么不签名
    • 「下载描述文件」蓝色大按钮
    • 4 步安装说明 (下载 → 设置 App → 输密码 → 完成)
  • 底部加可折叠「两种方式有什么区别?」对比说明
    • 标准方式: iOS 内置无警告, 推荐多数用户
    • 一键安装: 图标质量更好但有未签名警告, 技术用户可选
    • 强调: 装出来是同一个 web app, 打开后行为完全一致

设计哲学

为什么不签名 .mobileconfig:

  • 签名需要 Apple Developer 账号 ($99/年) — 违背 DAO Message 零依赖铁律
  • Fork 用户做不到签名 — 一旦签名版成主流, fork 实例就被边缘化
  • 未签名警告其实是用户教育的机会: 内容 100% 透明 (XML 可用文本编辑器查看), 我们把警告原因解释清楚, 让用户自己评估

为什么不替换标准方式:

  • 标准方式无任何警告, 与 iOS 原生 UX 一致, 对普通用户最友好
  • 一键安装的「未签名警告」可能误导用户把 DAO Message 当作不可信
  • 让用户自己选: 怕找不到分享按钮 → 用一键安装; 对系统警告敏感 / 不熟悉描述文件 → 用标准方式

注意

  • 此功能仅 iOS Safari 触发的 InstallGate iOS 分支可见; 非 iOS 用户看不到也用不上
  • .mobileconfigBlob + a[download] 触发下载, iOS Safari 看到 MIME 会 自动进入系统描述文件流程, 无需服务端
  • Icon 来源 /apple-touch-icon.png (PWA 已有), 180x180 嵌入 base64 约 8KB, 打包后 .mobileconfig 文件 ~10KB

文件变更摘要

新增:
  template-app-pwa/src/lib/mobileconfigGenerator.ts

修改:
  template-app-pwa/src/components/pwa/InstallGate.tsx       (iOS 分支加 tab 切换)
  template-app-pwa/src/App.tsx                              (版本 banner)
  template-app-pwa/package.json                             (1.0.46 → 1.0.47)

[1.0.46] · 2026-05-12 · PWA only

仅 PWA 模板改动, SDK 未发版 (npm @daomessage_sdk/sdk 仍是 1.0.38)。

🐛 Fixed · iOS PWA 路径加固

平台检测单一真相源

之前 InstallGate / InstallBanner / IOSPushHint / MessagesTab 各自实现了 isStandalone / isIOS / isIPad / isIOSSafari, 文案和阈值不一致, 升级很麻烦。

新增 template-app-pwa/src/lib/platform.ts 作为单一真相源, 所有组件 import。

新增检测函数:

  • iOSVersion() — 解析 iOS / iPadOS 主次版本号
  • iOSSupportsWebPush() — iOS 16.4+ 才返回 true (Apple 2023-03 才加 Web Push)
  • browserSupportsPWAInstall() — 当前浏览器是否原生支持 add-to-home
  • 各函数都对 SSR / 不支持的浏览器返回保守 false, 不抛 ReferenceError

Debug 旁路统一:

  • ?ios_test=1 强制 iOS 路径
  • ?ios_version=15.4 强制 iOS 版本号 (测旧 iOS 无 push 分支)
  • ?force_standalone=1 强制 standalone

删 InstallBanner.tsx (重复路径)

之前 InstallGate (全屏拦截) + InstallBanner (顶部横幅) 两个组件都在引导用户装 PWA, dismiss key 还不一样 (install_gate_dismissed_at vs pwa_install_dismissed_at), 用户在 Gate 关了之后 Banner 还会再打扰一次。

1.0.46 统一只用 InstallGate, 删 InstallBanner 文件。

同时清理 MessagesTab.tsx 里更老一版的本地 showPwaBanner 横幅 — 浏览器模式 用户已被 Gate 全屏挡住, 根本进不到 MessagesTab, 这横幅永远不会触发, 纯垃圾代码。

iOS 16.4 版本分支

InstallGate 现在区分两种 iOS:

  • iOS 16.4+ (有 Web Push) — 走完整三步教程, 装完后能收后台通知
  • iOS < 16.4 (无 Web Push) — 加 amber 警告条 "你的 iOS 不支持 Web 推送, 装到主屏可以全屏使用但收不到后台通知, 建议升级到 iOS 16.4+"

避免用户费劲装完才发现没推送。

修正 iPhone / iPad 分享按钮位置文案

之前文案: "点击 Safari 底部 / 右上角 的分享按钮"

  • iPhone: 实际在底部地址栏中央 (不是底部某处)
  • iPad: 实际在右上角地址栏右侧 (不是右上角某处)

加详细位置描述, 避免老人 / 新用户找不到。

iOS 非 Safari 用户重点提示

iOS 上的 Chrome / Firefox / Edge 都不支持 add-to-home + Web Push (Apple 限制只允许 WebKit), 但跌到 UnsupportedBrowser 文案太弱用户看不懂。1.0.46 加专门分支:

iOS 限制 — 你当前用的不是 Safari 浏览器。 只有 Safari 能把网站添加到主屏 + 收 Web 推送, Chrome / Firefox / Edge for iOS 都不支持(Apple 系统限制)。请用 Safari 重新打开 chat.daomessage.com。

「为什么必须装主屏?」可折叠说明

InstallGate 加可折叠 WhyInstall 子组件, 解释 4 个原因:

  • 推送通知 (PWA 才能收后台通知)
  • 全屏体验
  • 持久存储 (浏览器存储紧张时清理网站数据, PWA 优先级更高)
  • iOS 系统级限制 (Apple 强制 standalone 才能开通知权限)

✨ Added · UpdateBanner 检测新版本

iOS PWA 在 standalone 模式下 Service Worker 缓存非常激进, 用户经常开几天还是旧版, 错过修复 / 新功能。Android Chrome 类似但好一些。

新增 UpdateBanner.tsx:

  • main.tsxregisterSWonNeedRefresh 回调, 触发时 dispatch window 自定义事件 pwa:update-available
  • UpdateBanner 监听该事件, 显示蓝色横幅 "发现新版本 / 立即刷新"
  • 用户点「立即刷新」→ 调全局 __pwa_update_sw(true) 触发 SW skipWaiting + reload
  • 关键: 不存 dismiss key (与 InstallGate/IOSPushHint 不同) — 用户可关闭, 但每次启动 SW 检测到新版仍会再提示, 防止用户长期跑旧版

挂在 MainLayout 顶部, NetworkBanner 下面 (优先级最高的横幅类型)。

📚 文件变更摘要

新增:
  template-app-pwa/src/lib/platform.ts
  template-app-pwa/src/components/pwa/UpdateBanner.tsx

删除:
  template-app-pwa/src/components/pwa/InstallBanner.tsx

修改:
  template-app-pwa/src/components/pwa/InstallGate.tsx       (复用 platform + 加版本分支 + why-install)
  template-app-pwa/src/components/pwa/IOSPushHint.tsx       (复用 platform)
  template-app-pwa/src/components/tabs/MessagesTab.tsx      (删 showPwaBanner)
  template-app-pwa/src/components/main/MainLayout.tsx       (删 InstallBanner, 加 UpdateBanner)
  template-app-pwa/src/main.tsx                             (registerSW 加 onNeedRefresh)
  template-app-pwa/src/env.d.ts                             (registerSW 类型补全)

[1.0.38] · 2026-05-12

🔒 Protocol · 三态 trustState 贯通 SDK / App / 文档

承接 1.0.36-1.0.37 的强制双边密钥核对功能,这一版把"三态变更通知"做成一等公民,不再依赖 UI 端反射 hack。

✨ Added

MessagesModule.onTrustStateChange(listener) 公开 API

typescript
const unsub = client.messages.onTrustStateChange(({ conversationId, trustState }) => {
  if (conversationId !== activeChatId) return
  if (trustState === 'verified') { setShowModal(false); unlockChat() }
  else if (trustState === 'my_side_verified') { switchToWaitingView() }
  else { showVerifyOverlay() }
})
useEffect(() => unsub, [])

返回 unsubscribe 函数。支持多个 listener 同时订阅。替代之前 (client.messages as any).inner.onTrustStateChange = ... 这种反射赋值写法 — 反射方式不支持多订阅者、不可解绑、且类型签名 'verified' | 'unverified'my_side_verified

对应 Android:client.messages.onTrustStateChange { conversationId, trustState -> ... } 返回 () -> Unit

🐛 Fixed

markMyVerified() 不再吞掉 UI 通知

之前 markMyVerified() 写本地 IndexedDB 后没触发 onTrustStateChange,UI 端必须靠 await loadSession() 兜底刷新。现在通过模块级 registry 自动 emit my_side_verified,UI 立即响应。

verifyReset() 同样修复 — 立即 emit unverified 不依赖 NATS RTT。

onTrustStateChange 类型签名补齐

之前 callback 数据类型只声明 'verified' | 'unverified' 两态,缺 'my_side_verified' —— runtime 也确实没发过这个值。这一版补齐 runtime + 类型为完整 SessionTrustState 三态。

⚠️ Migration

之前用反射设置回调的代码仍然能跑(老属性 onTrustStateChange? 还在,向后兼容),但强烈建议迁移到 client.messages.onTrustStateChange(listener):

  • 类型完整
  • 支持多订阅者
  • 返回 unsubscribe
  • 不依赖 as any

兼容性

  • 1.0.38 ↔ 1.0.36/1.0.37:✅ 协议层完全兼容(没动 wire format)
  • 1.0.38 ↔ 1.0.35 及更早:❌ 不兼容(必须双端 ≥ 1.0.36 才能聊天,见 1.0.36/1.0.37 升级说明)

[1.0.36] / [1.0.37] · 2026-05-10 / 2026-05-11 ⚠️ BREAKING

🔒 Protocol · 强制双边密钥核对(方案 Y)

DAO Message 最大的协议级安全升级。每对会话必须双方都完成核对才能聊天,不再允许"先聊后核"。

✨ Added · 反 MITM 核对算法

核心:A 看到的码 ≠ B 看到的码(方向性 HKDF 派生),数学上根除"服务端中间人伪造相同码"的攻击面。

shared_secret = X25519_ECDH(myPriv, theirPub)
code = HKDF-SHA256(
         ikm  = shared_secret,
         salt = fromPub || toPub,        // 方向敏感
         info = "dao-message-verify-v1",
         L    = 10 bytes,
       )
     → base32 encode → 16 chars → 加横线分组显示

如果服务端在中间替换公钥,双方算出的码不匹配 → 用户察觉 → 拒绝核对。

✨ Added · 三状态机

unverified → my_side_verified → verified
状态含义可否发消息
unverified无人核对❌(SDK 抛 UNVERIFIED_SESSION,服务端拒转发);收到密文进 quarantine
my_side_verified本端已核对,等对端
verified双方都核对✅,聊天解锁

关键守卫:maybeMarkSessionVerified() 在收到服务端 NATS friend_verified 时严格检查本地必须是 my_side_verified 才能升 verified。防止服务端伪造通知把 unverified 直接拉到 verified

✨ Added · 三层防御

  1. UI overlay 阻塞输入(不可靠,可被绕过)
  2. SDK send() 协议层 gate 抛 UNVERIFIED_SESSION(可被改 SDK 绕过)
  3. Relay requireVerified 拒转发未核对会话密文(最后兜底,改不了)

任何一层都假设其他层失守,纵深防御。

✨ Added · Quarantine 队列

未核对期间收到的密文进 IndexedDB quarantineMessages store(SDK 自动管理)。

  • verified → 自动回放解密 + 入消息流
  • verifyReset直接丢弃(不回放,窗口内消息可能是攻击注入的伪造)

Android Room v3 → v4 加 quarantine 表(destructiveMigration,用户升 1.0.36 时本地数据被清,需重新注册)。

✨ New APIs

typescript
// 核对码计算
computeDirectionalCode(shared: Uint8Array, fromPub: Uint8Array, toPub: Uint8Array): string
normalizeDirectionalCode(input: string): string
computeSharedSecret(myPriv: Uint8Array, theirPub: Uint8Array): Uint8Array
computeSecurityCode(myPub: Uint8Array, theirPub: Uint8Array): string  // 60 hex 完整指纹(高级模式)
formatSecurityCode(hex60: string): string

// 核对状态切换
markMyVerified(apiBase: string, token: string, friendshipId: number, conversationId: string): Promise<{...}>
verifyReset(apiBase: string, token: string, friendshipId: number, conversationId: string): Promise<void>

// IndexedDB 守卫(内部用,导出仅供高级场景)
markSessionMyVerified(convId: string): Promise<void>
maybeMarkSessionVerified(convId: string): Promise<boolean>
resetSessionTrust(convId: string): Promise<void>

新类型:SessionTrustStateQuarantinedMessageSessionRecord 新增 trustStatefriendshipId 字段。

🔒 服务端协议变更

  • friendship 表加三列:user_a_verified_at / user_b_verified_at / verified_at
  • 新增 endpoint:POST /api/v1/friends/{id}/verify-mark / verify-reset
  • 单条原子 SQL 防双方同时核对的并发竞争
  • NATS 新事件:friend_verified / friend_verify_reset
  • gateway/router.gohandleMsg + call_offer 都加 requireVerified gate

⚠️ BREAKING

  • 1.0.36 必须配合 ≥ 1.0.36 服务端(否则 friendship 表新列不存在)
  • 客户端必须双端 ≥ 1.0.36 才能聊天(老版本不知道核对,新版本拒收老版本未核对消息)
  • Android 端首次升级清本地数据(Room destructiveMigration)
  • 老 API verifySession() / markSessionVerified() 保留但已不推荐(它们直接升 verified 跳过 my_side_verified 中间态,破坏 MITM 防御)

📚 文档

  • 重写 sdk-typescript/docs/security.md(三态机 / 方案 Y 算法 / E2E 流程 / quarantine / reset / 3 层防御)
  • 新增 spec:docs/spec/强制双边密钥核对-设计方案-V1.md
  • Vibecoding Protocol 加铁律 11 + Part C.9
  • Vibecoding Web React §10.3 / Android §17.8 完全重写

[1.0.34] · 2026-05-05

🐛 Fixed

  • 服务端 ICE candidate 限流 从 10/s 调到 50/s。原值在 WebRTC 通话握手期间太严,典型一次通话 30+ candidate 集中在 1 秒内,触发限流导致 ICE 收集不完整 → 连不上。
  • PWA iOS 顶栏避刘海:聊天页 / 设置页顶栏加 paddingTop: max(0.75rem, env(safe-area-inset-top)),iPhone 14 Pro 之后的 Dynamic Island 不再遮挡返回按钮。
  • 服务端 call_invite 路由补齐:1v1 通话邀请之前在 gateway/router.go 缺一条转发分支,导致主叫发出 call_offer 后对端永远收不到,通话失败。

[1.0.33] · 2026-05-04

🐛 Fixed

  • PWA 后台收不到 Web Push 通知 + 前台双份通知 —— 全链路诊断 + 修复:
    • Service Worker push 事件 listener 应在 self.addEventListener 而非 self.onpush,后者在 SW 重启后丢失
    • 前台双份:SW 重复转发 OPEN_LATEST_UNREAD 给 App 时,App 已经因为 visibility 在前台拉了一次,导致显示两次
    • 修复:SW 在前台时直接 skip showNotification,只发 OPEN_LATEST_UNREAD 给 App 路由
  • PWA 冷启动 Splash 防误点:之前冷启动直接显示 Welcome 页,用户在 SDK 还没加载完时点「创建账户」会导致 Mnemonic 生成异常。加 <Splash /> 启动屏。

♻️ Refactored

  • dismissPushNotifications 从 SDK 挪到 PWA App 层。原因:SDK 不该决定"通知是否清空"这种纯 UI 行为,且 SDK 的 Notification.permission API 在 SW 上下文里不可靠。

🐛 Fixed

"幽灵接通" 修复 — armAnswerTimeout 超时主动通知对端

  • 问题:PWA 主叫 Android,对方一直不接听。PWA 自己 15s armAnswerTimeout 触发后 cleanup('ended'),但 cleanup 是纯本地清理,不发任何信令。Android 永远不知道 PWA 取消了。如果 Android 用户后来点接听,会走完整 answer 流程 → CONNECTING → 19s 后 ICE FAILED → 兜底 hangup,用户感知"幽灵接通在计时" 实际是 19s 的"建立加密通道..."。
  • 修复:armAnswerTimeout 触发时,先 sendSignal(remoteAlias, 'call_hangup') 再 cleanup,Android 收到 → INCOMING 状态立即 dismiss 来电界面。
  • 配套(Android 模板):handleIncoming("call_hangup")_state.value 分流:INCOMING 直接回 IDLE 不走 2.5s 冻结(从未接通);OUTGOING/CONNECTING/CONNECTED 走 finalizeCall。callId 校验防延迟信令误伤新通话。

[1.0.29] · 2026-05-03

✨ Added — 通话生命周期 + 媒体质量

自动终止兜底(双端必须同步实施)

触发超时动作
RTCPeerConnection.connectionState === 'disconnected' 持续8 秒cleanup('ended')
入站 RTP bytesReceived / framesDecoded 无增长15 秒cleanup('ended')

覆盖场景:对端 App 被 OS 杀掉、网络断、路由器死掉。这些场景之前 call_hangup 信令永远到不了,通话双端状态卡死。

进入 'connected' / 'connecting' 时 SDK 自动清掉 disconnected 计时器,与 ICE restart 协作不冲突。

视频通话默认 720p30 + 1.5Mbps

  • getUserMedia video 约束改 { width: { ideal: 1280 }, height: { ideal: 720 }, frameRate: { ideal: 30 } },允许设备能力不足时降级,不抛 OverconstrainedError
  • 接通后 RTCRtpSender.setParameters({ encodings: [{ maxBitrate: 1_500_000 }], degradationPreference: 'maintain-framerate' })。弱网时优先保帧率降分辨率。
  • 未实施:setCodecPreferences VP9 → H264 → VP8 留下次单独发版。原因:首次合并实施(回滚的 1.0.28)疑似引入 SDP 协商时序问题,拆分排查。

兼容性

  • 1.0.29 ↔ 1.0.29:✅ 正常
  • 1.0.29 ↔ 1.0.27/1.0.28 老版本:⚠️ 老版本不会发 disconnected/RTP 兜底,新版本会自动结束;但 maxBitrate 双端不一致时 BWE 取低值,改动等于失效 — 强烈建议双端同步升级

工程方法说明

首次实施(1.0.28)采用"7 处改动一次性合并发版才测",出 bug 整段不可用,回滚。 1.0.29 改为"每步独立 commit + 真机测通过才下一步 + 用 file: 链接代替 npm publish 防污染 registry"。可作为后续大改动的标准流程。


[1.0.28] · 2026-05-03 [YANKED]

此版本因 setCodecPreferences 引入 SDP 协商时序问题被回滚,请勿使用。 直接升级到 1.0.29+。


[1.0.20] · 2026-04-26

🐛 Fixed

signSignal / verifySignal 跨平台 canonical 一致性

  • 问题:之前用 JSON.stringify(payload, Object.keys(payload).sort()) 做 canonical 序列化。JSON.stringify 第二个数组参数只控制顶层 key 的输出顺序,嵌套对象保持插入顺序——这是 ECMA-262 明确规定的行为,不是 bug。
  • 后果:当 Android SDK 发 call_ice 信令时,candidate 字段是嵌套对象({candidate, sdpMid, sdpMLineIndex}),Android 端 canonicalMapToJson 递归排序所有嵌套 key,PWA 端 JSON.stringify 不排嵌套 key → 两端 canonical 字节流不一致 → Ed25519 签名验证失败 → 通话信令被静默丢弃 → 收到 offer 但无法回应 ICE → 通话挂死无音视频。
  • 修复crypto/index.ts 新增内部 canonicalStringify(value) 递归实现,对所有层级的对象 key 按字典序排序;signSignal / verifySignal 都改用它。与 sdk-androidcanonicalMapToJson / sdk-ios 后续实现保持字节级一致。
  • 回归测试tests/unit/crypto.test.ts 新增"嵌套对象 key 顺序不影响验证"用例,模拟 Android call_ice 真实 payload。

兼容性

  • 1.0.20 ↔ 1.0.20:✅ 正常
  • 1.0.20 ↔ 1.0.19 及以下(同为 PWA):✅ 仅顶层 key 时行为一致;带嵌套对象的信令(call_ice)需双方都升级
  • 1.0.20 ↔ sdk-android(含 canonicalMapToJson 修复):✅ 跨平台通话首次通畅

[1.0.19] · 2026-04-26

🐛 Fixed

Cross-platform calls (Android ↔ PWA)

  • call_ice 信令兼容:Android SDK 发送 ICE candidate 时使用 candidate=string + sdp_mid + sdp_mline 三字段,而 PWA SDK 之前仅接受 RTCIceCandidateInit 对象(e.candidate.toJSON())格式。现在 CallModule.handleSignal('call_ice') 会自动归一化两种格式:
    • env.candidate 是对象 → 直接当 RTCIceCandidateInit 透传;
    • env.candidate 是字符串 → 用 env.sdp_mid / env.sdp_mline 组装成 RTCIceCandidateInit
  • 修复后 PWA 不再因 addIceCandidateTypeError: provided value is not of type 'RTCIceCandidateInit' 而拒绝 Android 端的候选者,跨平台音视频通话首次可用。

兼容性

  • 纯 PWA ↔ PWA 场景行为不变(保持发送对象格式)。
  • 老版本 Android(仍发字符串格式)打到新 PWA → ✅ 正常;新 Android(已统一发对象格式)打到老 PWA → ⚠️ 仍会 TypeError,需双方都升级到 1.0.19+。

[0.3.0] · 2026-04-XX

Reactive API layer arrives. Old command-style APIs remain compatible; new attachReactive(client) facade exposes Observable<T> streams for friends, conversations, messages, media progress, call state, and trust.

✨ Added

Reactive primitives

  • src/reactive/ — self-contained Observable<T> + BehaviorSubject<T> + operators (map, filter, distinctUntilChanged, combineLatest). Zero dependencies, ~3KB gzipped.
  • Types exported: Observable, Observer, Subscribable, Subscription.

Event bus

  • New client.events family (via attachReactive(client).events):
    • network: Observable<NetworkState>
    • sync: Observable<SyncState> (idle / syncing / done)
    • error: Observable<SDKError | null> (with kind, message, at)
    • message: Observable<StoredMessage | null> (global incoming message firehose)

Contacts

  • ReactiveContactsModule.observeFriends() — live friend list.
  • observeAcceptedFriends(), observePendingIncoming(), observePendingCount() — derived streams.
  • acceptFriendRequest and rejectFriendRequest now have built-in optimistic updates with rollback.
  • refresh() is mutex-protected — concurrent calls coalesce to one HTTP.

Messages

  • ReactiveMessagesModule.observeConversations() — list of conversation summaries.
  • ReactiveMessagesModule.observeMessages(convId) — live messages for one conversation (lazy loads from IndexedDB on first subscribe).

Media

  • ReactiveMediaModule.sendImage / sendFile / sendVoice return a messageId.
  • observeUpload(messageId) emits UploadProgress { phase, loaded, total }.
  • Phases: encrypting → uploading → done | failed.

Security

  • ReactiveSecurityModule.observeTrustState(contactId) — live unverified | verified state.
  • getSafetyNumber / verifyInputCode / markAsVerified / resetTrustState — thin wrappers that auto-emit state changes to subscribers.

Calls

  • ReactiveCallsModule.observeCallState() — live CallState.
  • observeLocalStream() / observeRemoteStream()Observable<MediaStream | null>.

Facade

  • attachReactive(client: SecureChatClient): ReactiveFacade — top-level entry point.
  • Subpath export: import { attachReactive } from '@daomessage_sdk/sdk/reactive' (available after you install 0.3.0).

Tests

  • 46 new unit tests for reactive modules (reactive primitives, events bus, contacts, media, integration smoke).
  • Total suite: 89 tests passing (was 43 in 0.2.5).

🔒 Unchanged (non-breaking)

  • SecureChatClient class, all methods, all events: identical to 0.2.x.
  • MessageModule, ContactsModule, MediaModule, CallModule, SecurityModule, PushModule: unchanged.
  • IndexedDB schemas, WebSocket frame formats, crypto parameters: unchanged.
  • Network endpoints, JWT lifecycle, PoW challenge: unchanged.

📚 Docs

  • New docs/reactive.md covering the 0.3.0 API.
  • CHANGELOG.md (this file) introduced.
  • docs/index.md gets a "Reactive API" entry under the table of contents.
  • VitePress publish pipeline picks up reactive.md automatically.

🛠 Internal

  • New source modules:
    src/reactive/observable.ts
    src/reactive/index.ts
    src/events/index.ts
    src/contacts/reactive-manager.ts
    src/messaging/reactive-messages.ts
    src/media/reactive-media.ts
    src/security/reactive-security.ts
    src/calls/reactive-calls.ts
    src/reactive-client.ts
    src/index-reactive.ts
  • New tests:
    tests/unit/reactive.test.ts           (12 tests)
    tests/unit/events.test.ts             (6 tests)
    tests/unit/contacts-reactive.test.ts  (9 tests)
    tests/unit/media-reactive.test.ts     (6 tests)
    tests/unit/smoke-0.3.0.test.ts        (11 tests · integration)
  • tsup entry list includes src/index-reactive.tsdist/index-reactive.{js,cjs,d.ts}.
  • package.json exports map gains a ./reactive subpath.

🙅 Not included (planned for 0.4.0+)

  • Android / iOS SDK Flow/AsyncStream equivalents (this release is TypeScript-only reactive).
  • R2-backed media pipeline for encrypted large-file support.
  • CallKit / VoIP push for iOS.
  • Safety-number QR payload v2 (richer device binding).

[0.2.5] · 2026-04-12

  • Various iOS PWA fixes and 0.2 hardening patches.
  • CF-Connecting-IP support on nginx/relay for correct PoW IP matching.
  • (See git log for details.)

[0.2.0] · 2026-03-15

  • First public release: TypeScript + Android SDK with E2EE messaging, contacts, channels, calls (beta), vanity, push.

Zero-Knowledge E2EE Protocol — Decentralized Communication