@agentunion/fastaun 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.js CHANGED
@@ -1577,6 +1577,20 @@ export class AUNClient {
1577
1577
  if (groupId && action === 'upsert' && this._v2Session) {
1578
1578
  this._safeAsync(this._v2AutoProposeState(groupId, { leaderDelay: true }));
1579
1579
  }
1580
+ // Group SPK 编排:成员变更触发注册/轮换
1581
+ if (this._v2Session && groupId) {
1582
+ const callFn = async (method, params) => this.call(method, params);
1583
+ if (action === 'joined' || action === 'join_approved') {
1584
+ this._v2Session.ensureGroupRegistered?.(groupId, callFn)?.catch(exc => {
1585
+ this._clientLog.debug(`group SPK registration failed (non-fatal): group=${groupId} action=${action} err=${formatCaughtError(exc)}`);
1586
+ });
1587
+ }
1588
+ else if (['member_added', 'member_left', 'member_removed', 'role_changed', 'owner_transferred'].includes(action)) {
1589
+ this._v2Session.rotateGroupSPK?.(groupId, callFn)?.catch(exc => {
1590
+ this._clientLog.debug(`group SPK rotation failed (non-fatal): group=${groupId} action=${action} err=${formatCaughtError(exc)}`);
1591
+ });
1592
+ }
1593
+ }
1580
1594
  // event_seq 空洞检测:持久化后的 group.changed 会携带 event_seq。
1581
1595
  // 用 onMessageSeq 返回值决定是否补拉,与 P2P / group.message 路径对齐。
1582
1596
  let needPull = false;
@@ -2641,13 +2655,22 @@ export class AUNClient {
2641
2655
  const result = isJsonObject(raw)
2642
2656
  ? { ...raw }
2643
2657
  : { result: raw };
2644
- result.ack_seq = seq;
2658
+ let actualAckSeq = seq;
2659
+ if ('effective_ack_seq' in result)
2660
+ actualAckSeq = Number(result.effective_ack_seq ?? 0);
2661
+ else if ('ack_seq' in result)
2662
+ actualAckSeq = Number(result.ack_seq ?? 0);
2663
+ else if ('cursor' in result)
2664
+ actualAckSeq = Number(result.cursor ?? 0);
2665
+ if (!Number.isFinite(actualAckSeq))
2666
+ actualAckSeq = seq;
2667
+ result.ack_seq = actualAckSeq;
2645
2668
  result.success = true;
2646
2669
  if (Number(result.acked ?? 0) === 0)
2647
- result.acked = seq;
2670
+ result.acked = actualAckSeq;
2648
2671
  if (this._v2Session) {
2649
2672
  try {
2650
- const destroyed = this._v2Session.maybeDestroyOldSPKs(seq);
2673
+ const destroyed = this._v2Session.maybeDestroyOldSPKs(actualAckSeq);
2651
2674
  if (destroyed.length > 0) {
2652
2675
  this._clientLog.info(`V2 destroyed old SPKs after ack: ${destroyed.slice(0, 3).join(',')} (PFS)`);
2653
2676
  }
@@ -2932,15 +2955,44 @@ export class AUNClient {
2932
2955
  return null;
2933
2956
  }
2934
2957
  let spkId = '';
2958
+ let recipientKeySource = '';
2935
2959
  if (isJsonObject(envelope.recipient)) {
2936
- spkId = String(envelope.recipient.spk_id ?? '');
2960
+ const recipient = envelope.recipient;
2961
+ spkId = String(recipient.spk_id ?? '');
2962
+ recipientKeySource = String(recipient.key_source ?? '');
2937
2963
  }
2938
2964
  else if (Array.isArray(envelope.recipients)) {
2939
2965
  spkId = String(msg.spk_id ?? '');
2966
+ // 从 recipients 数组中查找本设备对应行,提取 key_source(index 3)
2967
+ const recipients = envelope.recipients;
2968
+ for (const row of recipients) {
2969
+ if (Array.isArray(row) && row.length >= 6
2970
+ && String(row[0] ?? '') === this._aid
2971
+ && String(row[1] ?? '') === this._deviceId) {
2972
+ if (!spkId)
2973
+ spkId = String(row[5] ?? '');
2974
+ if (row.length > 3)
2975
+ recipientKeySource = String(row[3] ?? '');
2976
+ break;
2977
+ }
2978
+ }
2940
2979
  }
2941
- const { ikPriv, spkPriv } = session.getDecryptKeys(spkId);
2942
- const fromAid = String(msg.from_aid ?? '');
2980
+ // 根据 aad.group_id 判断使用 group SPK 还是 P2P SPK
2943
2981
  const aad = isJsonObject(envelope.aad) ? envelope.aad : {};
2982
+ const groupIdForKeys = String(aad.group_id ?? msg.group_id ?? '').trim();
2983
+ let ikPriv;
2984
+ let spkPriv;
2985
+ if (groupIdForKeys) {
2986
+ const keys = session.getGroupDecryptKeys(groupIdForKeys, spkId);
2987
+ ikPriv = keys.ikPriv;
2988
+ spkPriv = keys.spkPriv ?? undefined;
2989
+ }
2990
+ else {
2991
+ const keys = session.getDecryptKeys(spkId);
2992
+ ikPriv = keys.ikPriv;
2993
+ spkPriv = keys.spkPriv;
2994
+ }
2995
+ const fromAid = String(msg.from_aid ?? '');
2944
2996
  const senderDeviceId = String(aad.from_device ?? '');
2945
2997
  const senderPubDer = await this._getV2SenderPubDer(fromAid, senderDeviceId);
2946
2998
  if (!senderPubDer) {
@@ -2968,10 +3020,27 @@ export class AUNClient {
2968
3020
  }
2969
3021
  if (plaintext === null)
2970
3022
  return null;
2971
- if (session.isCurrentSPK(spkId)) {
2972
- this._safeAsync(session.rotateSPK(this._v2CallFn()).then(() => {
2973
- this._clientLog.debug(`V2 SPK rotated after consumption: aid=${this._aid ?? ''}`);
2974
- }));
3023
+ // 消费触发 SPK 轮换
3024
+ if (groupIdForKeys && recipientKeySource === 'group_device_prekey' && session.isLastUploadedGroupSPK(groupIdForKeys, spkId)) {
3025
+ // Group SPK 消费触发轮换
3026
+ const callFn = async (method, params) => this.call(method, params);
3027
+ session.rotateGroupSPK(groupIdForKeys, callFn).catch(exc => {
3028
+ this._clientLog.debug(`V2 group SPK rotation failed (non-fatal): group=${groupIdForKeys} err=${formatCaughtError(exc)}`);
3029
+ });
3030
+ }
3031
+ else if (groupIdForKeys && recipientKeySource === 'peer_device_prekey') {
3032
+ // peer_device_prekey fallback:补注册 group SPK
3033
+ const callFn = async (method, params) => this.call(method, params);
3034
+ session.ensureGroupRegistered(groupIdForKeys, callFn).catch(exc => {
3035
+ this._clientLog.debug(`V2 group SPK registration after peer fallback failed (non-fatal): group=${groupIdForKeys} err=${formatCaughtError(exc)}`);
3036
+ });
3037
+ }
3038
+ else if (!groupIdForKeys && session.isLastUploadedSPK(spkId)) {
3039
+ // P2P SPK 消费触发轮换
3040
+ const callFn = async (method, params) => this.call(method, params);
3041
+ session.rotateSPK(callFn).catch(exc => {
3042
+ this._clientLog.debug(`V2 SPK rotation failed (non-fatal): ${formatCaughtError(exc)}`);
3043
+ });
2975
3044
  }
2976
3045
  const suite = String(envelope.suite ?? '');
2977
3046
  return {
@@ -3642,9 +3711,55 @@ export class AUNClient {
3642
3711
  this._clientLog.debug(`V2 auto confirm pending proposals failed (non-fatal): ${formatCaughtError(exc)}`);
3643
3712
  }
3644
3713
  }
3645
- async _onV2PushNotification(_data) {
3714
+ async _onV2PushNotification(data) {
3646
3715
  if (!this._v2Session)
3647
3716
  return;
3717
+ // 提取 push 通知中的元数据
3718
+ const pushSeq = isJsonObject(data) ? Number(data.seq ?? 0) || 0 : 0;
3719
+ const pushFrom = isJsonObject(data) ? String(data.from_aid ?? '') : '';
3720
+ const pushMsgId = isJsonObject(data) ? String(data.message_id ?? '') : '';
3721
+ const envelopeJson = isJsonObject(data) ? data.envelope_json : undefined;
3722
+ const ns = this._aid ? `p2p:${this._aid}` : '';
3723
+ let contigBefore = ns ? this._seqTracker.getContiguousSeq(ns) : 0;
3724
+ this._clientLog.debug(`_onV2PushNotification: push_seq=${pushSeq || 'null'} push_from=${pushFrom} push_msg_id=${pushMsgId} has_payload=${!!envelopeJson} contiguous_seq=${contigBefore}`);
3725
+ // ── 带 payload 的 push:尝试就地解密 ──
3726
+ if (envelopeJson && pushSeq > 0 && ns) {
3727
+ try {
3728
+ const decrypted = await this._decryptV2PushMessage(data);
3729
+ if (decrypted) {
3730
+ // 解密成功:contiguous_seq 上界 = push_seq
3731
+ this._seqTracker.onMessageSeq(ns, pushSeq);
3732
+ if (pushSeq === contigBefore + 1) {
3733
+ this._seqTracker.forceContiguousSeq(ns, pushSeq);
3734
+ }
3735
+ await this._publishOrderedMessage('message.received', ns, pushSeq, decrypted);
3736
+ const newContig = this._seqTracker.getContiguousSeq(ns);
3737
+ if (newContig !== contigBefore) {
3738
+ this._saveSeqTrackerState();
3739
+ }
3740
+ if (newContig > 0 && newContig !== contigBefore) {
3741
+ this._transport.call('message.v2.ack', { up_to_seq: newContig })
3742
+ .catch(e => this._clientLog.debug(`V2 P2P push-ack failed: ${formatCaughtError(e)}`));
3743
+ }
3744
+ this._clientLog.debug(`_onV2PushNotification: push 带 payload 解密成功, contiguous_seq=${contigBefore}->${newContig} push_seq=${pushSeq}`);
3745
+ return;
3746
+ }
3747
+ }
3748
+ catch (exc) {
3749
+ this._clientLog.debug(`_onV2PushNotification: push payload 解密失败, fallback to pull: ${formatCaughtError(exc)}`);
3750
+ }
3751
+ }
3752
+ // ── 不带 payload 或解密失败:触发 pull ──
3753
+ // 纯通知只表示服务端已有 pushSeq 这条消息,内容还没有进入本地,不能先推进 contiguousSeq。
3754
+ // 后续 pull 必须从当前 contiguousSeq 开始,否则会跳过 pushSeq 本身。
3755
+ if (pushSeq > 0 && ns) {
3756
+ if (contigBefore >= pushSeq) {
3757
+ this._clientLog.warn(`_onV2PushNotification: contiguous_seq=${contigBefore} 越界(>= push_seq=${pushSeq}),强制修复为 ${pushSeq - 1}`);
3758
+ this._seqTracker.forceContiguousSeq(ns, pushSeq - 1);
3759
+ contigBefore = pushSeq - 1;
3760
+ }
3761
+ this._clientLog.debug(`_onV2PushNotification: 纯通知 push_seq=${pushSeq} > contiguous_seq=${contigBefore}, 触发 pull(after_seq=${contigBefore})`);
3762
+ }
3648
3763
  if (this._v2PullInflight) {
3649
3764
  this._v2PullPending = true;
3650
3765
  return;
@@ -3654,10 +3769,13 @@ export class AUNClient {
3654
3769
  do {
3655
3770
  this._v2PullPending = false;
3656
3771
  await this.pullV2();
3772
+ const newContig = ns ? this._seqTracker.getContiguousSeq(ns) : -1;
3773
+ this._clientLog.debug(`_onV2PushNotification pull done: contiguous_seq=${contigBefore}->${newContig} (push_seq=${pushSeq || 'null'})`);
3657
3774
  } while (this._v2PullPending);
3658
3775
  }
3659
3776
  catch (exc) {
3660
- this._clientLog.warn(`V2 push auto-pull failed: ${formatCaughtError(exc)}`);
3777
+ const newContig = ns ? this._seqTracker.getContiguousSeq(ns) : -1;
3778
+ this._clientLog.warn(`V2 push auto-pull failed: contiguous_seq=${contigBefore}->${newContig} err=${formatCaughtError(exc)}`);
3661
3779
  }
3662
3780
  finally {
3663
3781
  this._v2PullInflight = false;
@@ -3726,6 +3844,12 @@ export class AUNClient {
3726
3844
  this._gapFillDone.delete(dedupKey);
3727
3845
  }
3728
3846
  }
3847
+ /** Push 通知带 payload 时的就地解密(复用 _decryptV2Message) */
3848
+ async _decryptV2PushMessage(data) {
3849
+ if (!isJsonObject(data))
3850
+ return null;
3851
+ return await this._decryptV2Message(data);
3852
+ }
3729
3853
  async _onV2EpochRotated(data) {
3730
3854
  if (!isJsonObject(data))
3731
3855
  return;