@agentunion/fastaun 0.3.0 → 0.3.1

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,63 @@ 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
+ const 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
+ if (pushSeq > 0 && ns) {
3754
+ if (pushSeq <= contigBefore) {
3755
+ // 消息已在有序队列中,直接回播
3756
+ this._clientLog.debug(`_onV2PushNotification: push_seq=${pushSeq} <= contiguous_seq=${contigBefore}, 回播有序队列`);
3757
+ try {
3758
+ await this._drainOrderedMessages(ns);
3759
+ }
3760
+ catch (exc) {
3761
+ this._clientLog.warn(`V2 push drain ordered messages failed: ${formatCaughtError(exc)}`);
3762
+ }
3763
+ return;
3764
+ }
3765
+ else {
3766
+ // 不带 payload:上界 = push_seq - 1(push_seq 本身还需要 pull)
3767
+ this._seqTracker.onMessageSeq(ns, pushSeq);
3768
+ this._clientLog.debug(`_onV2PushNotification: 纯通知 push_seq=${pushSeq} > contiguous_seq=${contigBefore}, 标记上界(seq-1=${pushSeq - 1}) 触发 pull`);
3769
+ }
3770
+ }
3648
3771
  if (this._v2PullInflight) {
3649
3772
  this._v2PullPending = true;
3650
3773
  return;
@@ -3654,10 +3777,13 @@ export class AUNClient {
3654
3777
  do {
3655
3778
  this._v2PullPending = false;
3656
3779
  await this.pullV2();
3780
+ const newContig = ns ? this._seqTracker.getContiguousSeq(ns) : -1;
3781
+ this._clientLog.debug(`_onV2PushNotification pull done: contiguous_seq=${contigBefore}->${newContig} (push_seq=${pushSeq || 'null'})`);
3657
3782
  } while (this._v2PullPending);
3658
3783
  }
3659
3784
  catch (exc) {
3660
- this._clientLog.warn(`V2 push auto-pull failed: ${formatCaughtError(exc)}`);
3785
+ const newContig = ns ? this._seqTracker.getContiguousSeq(ns) : -1;
3786
+ this._clientLog.warn(`V2 push auto-pull failed: contiguous_seq=${contigBefore}->${newContig} err=${formatCaughtError(exc)}`);
3661
3787
  }
3662
3788
  finally {
3663
3789
  this._v2PullInflight = false;
@@ -3726,6 +3852,12 @@ export class AUNClient {
3726
3852
  this._gapFillDone.delete(dedupKey);
3727
3853
  }
3728
3854
  }
3855
+ /** Push 通知带 payload 时的就地解密(复用 _decryptV2Message) */
3856
+ async _decryptV2PushMessage(data) {
3857
+ if (!isJsonObject(data))
3858
+ return null;
3859
+ return await this._decryptV2Message(data);
3860
+ }
3729
3861
  async _onV2EpochRotated(data) {
3730
3862
  if (!isJsonObject(data))
3731
3863
  return;