@agentunion/fastaun-browser 0.3.5 → 0.3.6

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
@@ -453,6 +453,85 @@ function extractV2EnvelopeFromSource(source) {
453
453
  }
454
454
  return null;
455
455
  }
456
+ function truthyBool(value) {
457
+ if (value === true || value === 1)
458
+ return true;
459
+ if (typeof value === 'string') {
460
+ const normalized = value.trim().toLowerCase();
461
+ return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on';
462
+ }
463
+ return false;
464
+ }
465
+ function isEncryptedEnvelopePayload(payload) {
466
+ if (!isJsonObject(payload))
467
+ return false;
468
+ const payloadType = String(payload.type ?? '').trim();
469
+ if (payloadType.startsWith('e2ee.'))
470
+ return true;
471
+ if (!String(payload.ciphertext ?? '').trim())
472
+ return false;
473
+ return payload.nonce !== undefined
474
+ || payload.tag !== undefined
475
+ || payload.recipient !== undefined
476
+ || payload.recipients !== undefined
477
+ || payload.wrapped_key !== undefined
478
+ || payload.recipients_digest !== undefined;
479
+ }
480
+ function encryptedPushEnvelope(msg) {
481
+ if (isEncryptedEnvelopePayload(msg.payload))
482
+ return msg.payload;
483
+ if (typeof msg.envelope_json === 'string' && msg.envelope_json.trim()) {
484
+ try {
485
+ const parsed = JSON.parse(msg.envelope_json);
486
+ if (isEncryptedEnvelopePayload(parsed))
487
+ return parsed;
488
+ }
489
+ catch {
490
+ return null;
491
+ }
492
+ }
493
+ return null;
494
+ }
495
+ function isEncryptedPushMessage(msg) {
496
+ if (truthyBool(msg.encrypted))
497
+ return true;
498
+ return encryptedPushEnvelope(msg) !== null;
499
+ }
500
+ function isV2EncryptedEnvelopePayload(envelope) {
501
+ if (!envelope)
502
+ return false;
503
+ const payloadType = String(envelope.type ?? '').trim();
504
+ if (payloadType === 'e2ee.p2p_encrypted' || payloadType === 'e2ee.group_encrypted')
505
+ return true;
506
+ return String(envelope.version ?? '').trim().toLowerCase() === 'v2' && payloadType.startsWith('e2ee.');
507
+ }
508
+ function safeUndecryptablePushEvent(msg, group) {
509
+ const event = {
510
+ message_id: msg.message_id ?? null,
511
+ from: msg.from ?? null,
512
+ seq: msg.seq ?? null,
513
+ timestamp: msg.timestamp ?? msg.t_server ?? null,
514
+ device_id: msg.device_id ?? null,
515
+ slot_id: msg.slot_id ?? null,
516
+ _decrypt_error: 'encrypted push payload is not decryptable on raw push path',
517
+ _decrypt_stage: 'push_envelope',
518
+ };
519
+ if (group) {
520
+ event.group_id = msg.group_id ?? null;
521
+ }
522
+ else {
523
+ event.to = msg.to ?? null;
524
+ }
525
+ const envelope = encryptedPushEnvelope(msg);
526
+ if (envelope) {
527
+ event._envelope_type = String(envelope.type ?? '');
528
+ event._suite = String(envelope.suite ?? '');
529
+ if (isV2EncryptedEnvelopePayload(envelope)) {
530
+ attachV2EnvelopeMetadata(event, v2E2eeMeta(envelope));
531
+ }
532
+ }
533
+ return event;
534
+ }
456
535
  function metadataWithoutAuth(value) {
457
536
  if (!isJsonObject(value))
458
537
  return null;
@@ -1533,7 +1612,7 @@ export class AUNClient {
1533
1612
  // message.pull:V2 就绪时走 V2 pull
1534
1613
  if (method === 'message.pull' && this._v2Session) {
1535
1614
  this._clientLog.debug('call route: message.pull → V2 pull');
1536
- const messages = await this.pullV2(Number(p.after_seq ?? 0) || 0, Number(p.limit ?? 50) || 50);
1615
+ const messages = await this.pullV2(Number(p.after_seq ?? 0) || 0, Number(p.limit ?? 50) || 50, { force: p.force === true });
1537
1616
  return { messages };
1538
1617
  }
1539
1618
  // message.ack:V2 就绪时走 V2 ack
@@ -1651,6 +1730,30 @@ export class AUNClient {
1651
1730
  // ── Group E2EE 自动编排已移除(V2-only:由 group.v2.bootstrap 驱动)────────
1652
1731
  return result;
1653
1732
  }
1733
+ async _callRawV2Rpc(method, params) {
1734
+ const p = { ...(params ?? {}) };
1735
+ delete p._pull_gate_locked;
1736
+ delete p._skip_auto_ack;
1737
+ delete p.skip_auto_ack;
1738
+ if (method.startsWith('group.') && p.group_id !== undefined && p.group_id !== null) {
1739
+ p.group_id = normalizeGroupId(String(p.group_id)) || String(p.group_id);
1740
+ }
1741
+ if (method.startsWith('group.') && p.device_id === undefined) {
1742
+ p.device_id = this._deviceId;
1743
+ }
1744
+ if (method.startsWith('group.') && p.slot_id === undefined) {
1745
+ p.slot_id = this._slotId;
1746
+ }
1747
+ if (SIGNED_METHODS.has(method)) {
1748
+ if (this._shouldSkipClientSignature(method, p)) {
1749
+ delete p.client_signature;
1750
+ }
1751
+ else {
1752
+ await this._signClientOperation(method, p);
1753
+ }
1754
+ }
1755
+ return await this._transport.call(method, p);
1756
+ }
1654
1757
  // ── 便利方法 ──────────────────────────────────────
1655
1758
  async ping(params) {
1656
1759
  return this.meta.ping(params);
@@ -1695,12 +1798,19 @@ export class AUNClient {
1695
1798
  }
1696
1799
  // P2P 空洞检测
1697
1800
  const seq = msg.seq;
1801
+ const encryptedPush = isEncryptedPushMessage(msg);
1698
1802
  if (seq !== undefined && seq !== null && this._aid) {
1699
1803
  const ns = `p2p:${this._aid}`;
1700
1804
  // Push 修上界:先更新 maxSeenSeq
1701
1805
  if (seq > 0)
1702
1806
  this._seqTracker.updateMaxSeen(ns, seq);
1703
- const needPull = this._seqTracker.onMessageSeq(ns, seq);
1807
+ const contigBefore = this._seqTracker.getContiguousSeq(ns);
1808
+ const seqNeedsPull = this._seqTracker.onMessageSeq(ns, seq);
1809
+ const published = encryptedPush
1810
+ ? await this._publishEncryptedPushMessage('message.received', 'message.undecryptable', ns, seq, msg, false)
1811
+ : await this._publishOrderedMessage('message.received', ns, seq, msg);
1812
+ const contigAfter = this._seqTracker.getContiguousSeq(ns);
1813
+ const needPull = seqNeedsPull && !published;
1704
1814
  if (needPull) {
1705
1815
  this._safeAsync(this._fillP2pGap());
1706
1816
  }
@@ -1716,14 +1826,16 @@ export class AUNClient {
1716
1826
  }).catch((e) => { this._clientLog.warn(`P2P auto-ack failed:${String(e)}`); });
1717
1827
  }
1718
1828
  // 即时持久化 cursor,异常断连后不回退
1719
- this._saveSeqTrackerState();
1720
- }
1721
- // 明文消息直接透传
1722
- if (seq !== undefined && seq !== null && this._aid) {
1723
- const ns = `p2p:${this._aid}`;
1724
- await this._publishOrderedMessage('message.received', ns, seq, msg);
1829
+ if (contigAfter !== contigBefore)
1830
+ this._saveSeqTrackerState();
1831
+ if (encryptedPush)
1832
+ return;
1725
1833
  }
1726
1834
  else {
1835
+ if (encryptedPush) {
1836
+ await this._publishEncryptedPushMessage('message.received', 'message.undecryptable', '', seq ?? 0, msg, false);
1837
+ return;
1838
+ }
1727
1839
  await this._publishAppEvent('message.received', msg);
1728
1840
  }
1729
1841
  }
@@ -1830,12 +1942,19 @@ export class AUNClient {
1830
1942
  return;
1831
1943
  }
1832
1944
  // seq 跟踪 + auto-ack
1945
+ const encryptedPush = isEncryptedPushMessage(msg);
1833
1946
  if (groupId && seq !== undefined && seq !== null) {
1834
1947
  const ns = `group:${groupId}`;
1835
1948
  // Push 修上界:先更新 maxSeenSeq
1836
1949
  if (seq > 0)
1837
1950
  this._seqTracker.updateMaxSeen(ns, seq);
1838
- const needPull = this._seqTracker.onMessageSeq(ns, seq);
1951
+ const contigBefore = this._seqTracker.getContiguousSeq(ns);
1952
+ const seqNeedsPull = this._seqTracker.onMessageSeq(ns, seq);
1953
+ const published = encryptedPush
1954
+ ? await this._publishEncryptedPushMessage('group.message_created', 'group.message_undecryptable', ns, seq, msg, true)
1955
+ : await this._publishOrderedMessage('group.message_created', ns, seq, msg);
1956
+ const contigAfter = this._seqTracker.getContiguousSeq(ns);
1957
+ const needPull = seqNeedsPull && !published;
1839
1958
  if (needPull) {
1840
1959
  this._safeAsync(this._fillGroupGap(groupId));
1841
1960
  }
@@ -1850,14 +1969,16 @@ export class AUNClient {
1850
1969
  slot_id: this._slotId,
1851
1970
  }).catch((e) => { this._clientLog.warn('group message auto-ack failed: group=' + groupId, e); });
1852
1971
  }
1853
- this._saveSeqTrackerState();
1854
- }
1855
- // 明文消息直接透传
1856
- if (groupId && seq !== undefined && seq !== null) {
1857
- const nsKey = `group:${groupId}`;
1858
- await this._publishOrderedMessage('group.message_created', nsKey, seq, msg);
1972
+ if (contigAfter !== contigBefore)
1973
+ this._saveSeqTrackerState();
1974
+ if (encryptedPush)
1975
+ return;
1859
1976
  }
1860
1977
  else {
1978
+ if (encryptedPush) {
1979
+ await this._publishEncryptedPushMessage('group.message_created', 'group.message_undecryptable', '', seq ?? 0, msg, true);
1980
+ return;
1981
+ }
1861
1982
  await this._publishAppEvent('group.message_created', msg);
1862
1983
  }
1863
1984
  }
@@ -1878,6 +1999,59 @@ export class AUNClient {
1878
1999
  }
1879
2000
  }
1880
2001
  }
2002
+ async _decryptEncryptedPushPayload(msg, group) {
2003
+ const envelope = encryptedPushEnvelope(msg);
2004
+ if (!isV2EncryptedEnvelopePayload(envelope))
2005
+ return null;
2006
+ const aad = isJsonObject(envelope.aad) ? envelope.aad : {};
2007
+ const fromAid = String(msg.from_aid ?? msg.from ?? msg.sender_aid ?? aad.from ?? '').trim();
2008
+ const plaintext = await this._decryptV2EnvelopeForThought({ envelope, fromAid });
2009
+ if (!plaintext)
2010
+ return null;
2011
+ const e2eeMeta = v2E2eeMeta(envelope);
2012
+ const result = {
2013
+ message_id: String(msg.message_id ?? ''),
2014
+ from: fromAid,
2015
+ seq: msg.seq ?? null,
2016
+ timestamp: msg.t_server ?? msg.timestamp ?? null,
2017
+ payload: plaintext,
2018
+ encrypted: true,
2019
+ e2ee: e2eeMeta,
2020
+ };
2021
+ result.direction = fromAid && fromAid === this._aid ? 'outbound_sync' : 'inbound';
2022
+ if (msg.t_server !== undefined)
2023
+ result.t_server = msg.t_server;
2024
+ if (msg.device_id !== undefined)
2025
+ result.device_id = msg.device_id;
2026
+ if (msg.slot_id !== undefined)
2027
+ result.slot_id = msg.slot_id;
2028
+ if (group) {
2029
+ result.group_id = msg.group_id ?? aad.group_id ?? envelope.group_id ?? null;
2030
+ }
2031
+ else {
2032
+ result.to = msg.to ?? this._aid ?? '';
2033
+ }
2034
+ attachV2EnvelopeMetadata(result, e2eeMeta);
2035
+ return result;
2036
+ }
2037
+ async _publishEncryptedPushAsUndecryptable(event, ns, seq, msg, group) {
2038
+ const safeEvent = safeUndecryptablePushEvent(msg, group);
2039
+ if (ns) {
2040
+ return this._publishOrderedMessage(event, ns, seq, safeEvent);
2041
+ }
2042
+ await this._publishAppEvent(event, safeEvent);
2043
+ return true;
2044
+ }
2045
+ async _publishEncryptedPushMessage(normalEvent, undecryptableEvent, ns, seq, msg, group) {
2046
+ const decrypted = await this._decryptEncryptedPushPayload(msg, group);
2047
+ if (decrypted) {
2048
+ if (ns)
2049
+ return this._publishOrderedMessage(normalEvent, ns, seq, decrypted);
2050
+ await this._publishAppEvent(normalEvent, decrypted);
2051
+ return true;
2052
+ }
2053
+ return this._publishEncryptedPushAsUndecryptable(undecryptableEvent, ns, seq, msg, group);
2054
+ }
1881
2055
  /** 收到不带 payload 的 group.message_created 通知后,自动 pull 最新消息 */
1882
2056
  async _autoPullGroupMessages(notification) {
1883
2057
  const groupId = (notification.group_id ?? '');
@@ -4088,7 +4262,27 @@ export class AUNClient {
4088
4262
  async initV2Session() {
4089
4263
  if (!this._aid)
4090
4264
  return;
4091
- const identity = this._identity;
4265
+ let identity = this._identity;
4266
+ if (!identity?.private_key_pem) {
4267
+ // fallback:缓存的 identity 可能被 instance_state 污染,重新从 keystore 加载
4268
+ try {
4269
+ const reloaded = await this._keystore.loadIdentity(this._aid);
4270
+ if (reloaded?.private_key_pem) {
4271
+ this._identity = reloaded;
4272
+ identity = reloaded;
4273
+ this._clientLog.warn('V2 session init: identity cache was stale, reloaded from keystore');
4274
+ // 自愈:重新持久化,清理 instance_state 中的脏数据
4275
+ try {
4276
+ const persistIdentity = this._auth._persistIdentity;
4277
+ if (typeof persistIdentity === 'function') {
4278
+ await persistIdentity.call(this._auth, reloaded);
4279
+ }
4280
+ }
4281
+ catch { /* best-effort */ }
4282
+ }
4283
+ }
4284
+ catch { /* ignore */ }
4285
+ }
4092
4286
  if (!identity?.private_key_pem) {
4093
4287
  this._clientLog.warn('V2 session init skipped: no AID private key');
4094
4288
  return;
@@ -4414,20 +4608,21 @@ export class AUNClient {
4414
4608
  * @param afterSeq 从此 seq 之后开始拉取(0/省略 = 从当前 contiguous 开始)
4415
4609
  * @param limit 最多拉取条数
4416
4610
  */
4417
- async pullV2(afterSeq = 0, limit = 50) {
4611
+ async pullV2(afterSeq = 0, limit = 50, opts) {
4418
4612
  if (!this._v2Session) {
4419
4613
  throw new StateError('V2 session not initialized (not connected?)');
4420
4614
  }
4421
4615
  const ns = this._aid ? `p2p:${this._aid}` : '';
4422
4616
  const decrypted = [];
4423
- let nextAfterSeq = afterSeq || (ns ? this._seqTracker.getContiguousSeq(ns) : 0);
4617
+ let nextAfterSeq = opts?.force ? afterSeq : (afterSeq || (ns ? this._seqTracker.getContiguousSeq(ns) : 0));
4424
4618
  let pageCount = 0;
4425
4619
  const maxPages = 100;
4426
4620
  while (pageCount < maxPages) {
4427
4621
  pageCount += 1;
4428
- const result = await this.call('message.v2.pull', {
4622
+ const result = await this._callRawV2Rpc('message.v2.pull', {
4429
4623
  after_seq: nextAfterSeq,
4430
4624
  limit,
4625
+ ...(opts?.force ? { force: true } : {}),
4431
4626
  });
4432
4627
  const messages = (Array.isArray(result?.messages) ? result.messages : []);
4433
4628
  const seqs = messages
@@ -4747,6 +4942,12 @@ export class AUNClient {
4747
4942
  encrypted: true,
4748
4943
  e2ee: e2ee,
4749
4944
  };
4945
+ const explicitDirection = String(msg.direction ?? '').trim();
4946
+ result.direction = explicitDirection || (fromAid && fromAid === this._aid ? 'outbound_sync' : 'inbound');
4947
+ if (msg.device_id !== undefined)
4948
+ result.device_id = msg.device_id;
4949
+ if (msg.slot_id !== undefined)
4950
+ result.slot_id = msg.slot_id;
4750
4951
  attachV2EnvelopeMetadata(result, e2ee);
4751
4952
  return result;
4752
4953
  }