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:
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() 推送链路体检
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 区分
NotAllowedError→permission_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.managedpayload, 可以把网站打包成"系统级安装"快捷方式。
1.0.47 把这条路径作为 InstallGate iOS 分支的第二个 tab, 与原有"标准方式" 并列展示。用户自己选择用哪一种, 不强推。
新文件
src/lib/mobileconfigGenerator.ts
客户端 JS 生成 .mobileconfig XML (不走服务端) 的好处:
- Fork 用户部署到自己域名时, 自动用
location.origin适配 - 与"零知识 / 零中介"铁律一致, 不需要服务端参与
- 用户可自定义 Label / 域名 / 图标
功能:
generateWebClipMobileConfig(opts)— 生成完整 plist XML 字符串downloadWebClipMobileConfig(opts)— 触发浏览器下载, MIMEapplication/x-apple-aspen-config, iOS Safari 自动进入「下载描述文件」流程
XML 结构:
- 外层 Configuration profile (UUID / Identifier / Organization / ConsentText 中英双语)
- 内层
com.apple.webClip.managedpayload (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 用户看不到也用不上
.mobileconfig用Blob+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.tsx给registerSW加onNeedRefresh回调, 触发时 dispatchwindow自定义事件pwa:update-availableUpdateBanner监听该事件, 显示蓝色横幅 "发现新版本 / 立即刷新"- 用户点「立即刷新」→ 调全局
__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
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 · 三层防御
- UI overlay 阻塞输入(不可靠,可被绕过)
- SDK
send()协议层 gate 抛UNVERIFIED_SESSION(可被改 SDK 绕过) - Relay
requireVerified拒转发未核对会话密文(最后兜底,改不了)
任何一层都假设其他层失守,纵深防御。
✨ Added · Quarantine 队列
未核对期间收到的密文进 IndexedDB quarantineMessages store(SDK 自动管理)。
- 升
verified→ 自动回放解密 + 入消息流 verifyReset→ 直接丢弃(不回放,窗口内消息可能是攻击注入的伪造)
Android Room v3 → v4 加 quarantine 表(destructiveMigration,用户升 1.0.36 时本地数据被清,需重新注册)。
✨ New APIs
// 核对码计算
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>新类型:SessionTrustState、QuarantinedMessage。SessionRecord 新增 trustState 和 friendshipId 字段。
🔒 服务端协议变更
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.go的handleMsg+call_offer都加requireVerifiedgate
⚠️ 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 路由
- Service Worker
- PWA 冷启动 Splash 防误点:之前冷启动直接显示 Welcome 页,用户在 SDK 还没加载完时点「创建账户」会导致 Mnemonic 生成异常。加
<Splash />启动屏。
♻️ Refactored
dismissPushNotifications从 SDK 挪到 PWA App 层。原因:SDK 不该决定"通知是否清空"这种纯 UI 行为,且 SDK 的Notification.permissionAPI 在 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
getUserMediavideo 约束改{ 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-android的canonicalMapToJson/sdk-ios后续实现保持字节级一致。 - 回归测试:
tests/unit/crypto.test.ts新增"嵌套对象 key 顺序不影响验证"用例,模拟 Androidcall_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 不再因
addIceCandidate抛TypeError: 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 exposesObservable<T>streams for friends, conversations, messages, media progress, call state, and trust.
✨ Added
Reactive primitives
src/reactive/— self-containedObservable<T>+BehaviorSubject<T>+ operators (map,filter,distinctUntilChanged,combineLatest). Zero dependencies, ~3KB gzipped.- Types exported:
Observable,Observer,Subscribable,Subscription.
Event bus
- New
client.eventsfamily (viaattachReactive(client).events):network:Observable<NetworkState>sync:Observable<SyncState>(idle / syncing / done)error:Observable<SDKError | null>(withkind,message,at)message:Observable<StoredMessage | null>(global incoming message firehose)
Contacts
ReactiveContactsModule.observeFriends()— live friend list.observeAcceptedFriends(),observePendingIncoming(),observePendingCount()— derived streams.acceptFriendRequestandrejectFriendRequestnow 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 / sendVoicereturn a messageId.observeUpload(messageId)emitsUploadProgress { phase, loaded, total }.- Phases:
encrypting → uploading → done | failed.
Security
ReactiveSecurityModule.observeTrustState(contactId)— liveunverified | verifiedstate.getSafetyNumber / verifyInputCode / markAsVerified / resetTrustState— thin wrappers that auto-emit state changes to subscribers.
Calls
ReactiveCallsModule.observeCallState()— liveCallState.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)
SecureChatClientclass, 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.mdcovering the 0.3.0 API. CHANGELOG.md(this file) introduced.docs/index.mdgets a "Reactive API" entry under the table of contents.- VitePress publish pipeline picks up
reactive.mdautomatically.
🛠 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) tsupentry list includessrc/index-reactive.ts→dist/index-reactive.{js,cjs,d.ts}.package.jsonexports map gains a./reactivesubpath.
🙅 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.