@agentunion/fastaun-browser 0.4.3 → 0.4.5

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.
Files changed (71) hide show
  1. package/CHANGELOG.md +203 -178
  2. package/_packed_docs/CHANGELOG.md +203 -178
  3. package/_packed_docs/INDEX.md +17 -17
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +11 -11
  5. package/_packed_docs/agent.md/SCHEMA.md +49 -49
  6. package/_packed_docs/agent.md/examples/signed-openclaw-lobster.md +22 -22
  7. package/_packed_docs/agent.md//350/277/234/347/250/213agent.md/347/274/223/345/255/230/344/270/216etag/351/200/217/344/274/240/346/226/271/346/241/210.md +327 -327
  8. package/_packed_docs/cli/AUN-CLI/350/256/276/350/256/241/346/226/207/346/241/243.md +686 -686
  9. package/_packed_docs/design/2026-05-22-aun-rpc-trace-enhancement.md +542 -542
  10. package/_packed_docs/design/E2EE_V2/347/256/200/345/214/226/344/270/2721DH/345/212/240Per-AID_Wrap/346/226/271/346/241/210.md +124 -124
  11. package/_packed_docs/design//350/267/250/350/257/255/350/250/200/345/256/271/345/231/250E2E/346/265/213/350/257/225/346/226/271/346/241/210.md +665 -665
  12. package/_packed_docs/protocol/01-/350/272/253/344/273/275/344/270/216/345/207/255/350/257/201/345/215/217/350/256/256-auth.md +2 -2
  13. package/_packed_docs/protocol/14-/344/272/244/344/272/222/346/234/272/345/210/266-/345/223/215/345/272/224/346/250/241/345/274/217/344/270/216/350/207/252/344/270/273/346/250/241/345/274/217.md +170 -170
  14. package/_packed_docs/protocol/15-/347/246/273/347/272/277/346/216/250/351/200/201/351/200/232/347/237/245/345/215/217/350/256/256.md +419 -419
  15. package/_packed_docs/protocol/README.md +1 -1
  16. package/_packed_docs/protocol/aun-docs-guide.md +1 -1
  17. package/_packed_docs/protocol//351/231/204/345/275/225A-/346/234/257/350/257/255/350/241/250.md +15 -15
  18. package/_packed_docs/protocol//351/231/204/345/275/225B-/346/211/251/345/261/225/346/200/247/346/214/207/345/215/227.md +4 -4
  19. package/_packed_docs/protocol//351/231/204/345/275/225J-/345/256/242/346/210/267/347/253/257/346/216/245/345/205/245/347/244/272/344/276/213.md +98 -98
  20. package/_packed_docs/protocol//351/231/204/345/275/225M-JWT/350/256/244/350/257/201/345/256/236/347/216/260/346/214/207/345/215/227.md +46 -46
  21. package/_packed_docs/protocol//351/231/204/345/275/225N-/345/210/206/345/270/203/345/274/217Trace/345/215/217/350/256/256.md +257 -257
  22. package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +1 -1
  23. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +1 -1
  24. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +1 -0
  25. package/_packed_docs/sdk/09-payload-reference.md +13 -13
  26. package/_packed_docs/sdk/E2EE_V2/346/266/210/346/201/257/351/200/232/344/277/241/346/227/266/345/272/217/345/233/276.md +171 -171
  27. package/dist/aid-store.d.ts +1 -0
  28. package/dist/aid-store.d.ts.map +1 -1
  29. package/dist/aid-store.js +26 -9
  30. package/dist/aid-store.js.map +1 -1
  31. package/dist/aid.d.ts +2 -1
  32. package/dist/aid.d.ts.map +1 -1
  33. package/dist/aid.js +7 -6
  34. package/dist/aid.js.map +1 -1
  35. package/dist/auth.d.ts +8 -13
  36. package/dist/auth.d.ts.map +1 -1
  37. package/dist/auth.js +38 -127
  38. package/dist/auth.js.map +1 -1
  39. package/dist/bundle.js +872 -350
  40. package/dist/client.d.ts +12 -5
  41. package/dist/client.d.ts.map +1 -1
  42. package/dist/client.js +296 -213
  43. package/dist/client.js.map +1 -1
  44. package/dist/index.d.ts +1 -0
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +1 -0
  47. package/dist/index.js.map +1 -1
  48. package/dist/keystore/index.d.ts +45 -22
  49. package/dist/keystore/index.d.ts.map +1 -1
  50. package/dist/keystore/index.js +6 -1
  51. package/dist/keystore/index.js.map +1 -1
  52. package/dist/keystore/indexeddb.d.ts +11 -1
  53. package/dist/keystore/indexeddb.d.ts.map +1 -1
  54. package/dist/keystore/indexeddb.js +167 -18
  55. package/dist/keystore/indexeddb.js.map +1 -1
  56. package/dist/register-flow.d.ts +34 -0
  57. package/dist/register-flow.d.ts.map +1 -0
  58. package/dist/register-flow.js +355 -0
  59. package/dist/register-flow.js.map +1 -0
  60. package/dist/v2/session/keystore.d.ts +5 -0
  61. package/dist/v2/session/keystore.d.ts.map +1 -1
  62. package/dist/v2/session/keystore.js +29 -0
  63. package/dist/v2/session/keystore.js.map +1 -1
  64. package/dist/version.d.ts +1 -1
  65. package/dist/version.js +1 -1
  66. package/package.json +1 -1
  67. package/_packed_docs/0.4.0_/345/267/256/345/274/202/346/240/270/345/256/236/345/206/263/347/255/226/350/256/260/345/275/225.md +0 -302
  68. package/_packed_docs/AUN_SDK_0.4.0_/350/256/276/350/256/241/345/257/271/346/257/224/345/210/206/346/236/220.md +0 -194
  69. package/_packed_docs/AUN_SDK_/351/207/215/346/236/204/345/256/236/346/226/275/350/256/241/345/210/222.md +0 -596
  70. package/_packed_docs/AUN_SDK_/351/207/215/346/236/204/350/256/276/350/256/241/346/226/271/346/241/210_v3.md +0 -1697
  71. package/_packed_docs/python-sdk-v2-only-changelog.md +0 -189
package/dist/client.js CHANGED
@@ -52,6 +52,14 @@ function getV2DeviceId(dev) {
52
52
  }
53
53
  return { present: false, value: '' };
54
54
  }
55
+ function isAIDObject(value) {
56
+ const candidate = value;
57
+ return Boolean(candidate
58
+ && typeof candidate === 'object'
59
+ && typeof candidate.aid === 'string'
60
+ && typeof candidate.aunPath === 'string'
61
+ && typeof candidate.isPrivateKeyValid === 'function');
62
+ }
55
63
  function sortObjectKeys(obj) {
56
64
  if (obj === null || obj === undefined || typeof obj !== 'object')
57
65
  return obj;
@@ -169,6 +177,20 @@ const DEFAULT_SESSION_OPTIONS = {
169
177
  http: 30.0,
170
178
  },
171
179
  };
180
+ const PUBLIC_CONNECTION_OPTION_KEYS = new Set([
181
+ 'auto_reconnect',
182
+ 'connect_timeout',
183
+ 'retry_initial_delay',
184
+ 'retry_max_delay',
185
+ 'retry_max_attempts',
186
+ 'heartbeat_interval',
187
+ 'call_timeout',
188
+ 'connection_kind',
189
+ 'short_ttl_ms',
190
+ 'delivery_mode',
191
+ 'extra_info',
192
+ 'background_sync',
193
+ ]);
172
194
  const PROTECTED_HEADERS_METHODS = new Set([
173
195
  'message.send',
174
196
  'group.send',
@@ -642,7 +664,7 @@ export class AUNClient {
642
664
  _sessionOptions = { ...DEFAULT_SESSION_OPTIONS };
643
665
  _dispatcher;
644
666
  _discovery;
645
- _keystore;
667
+ _tokenStore;
646
668
  _auth;
647
669
  _transport;
648
670
  // E2EE 编排状态(内存缓存)
@@ -656,6 +678,7 @@ export class AUNClient {
656
678
  // V2 E2EE 状态
657
679
  _v2Session;
658
680
  _v2KeyStore;
681
+ _v2SessionInitInFlight = null;
659
682
  _v2BootstrapCache = new Map();
660
683
  _v2SenderIKPending = new Map();
661
684
  _v2SenderIKFetching = new Set();
@@ -728,14 +751,14 @@ export class AUNClient {
728
751
  _clientLog;
729
752
  _logAuth;
730
753
  _logTransport;
731
- _logKeystore;
754
+ _tokenStoreLog;
732
755
  _logDiscovery;
733
756
  _logEvents;
734
757
  constructor(aid) {
735
- const inputAid = (aid !== null && aid !== undefined && typeof aid.aunPath === 'string' && typeof aid.isPrivateKeyValid === 'function') ? aid : null;
736
- if (typeof aid === 'string') {
737
- throw new ValidationError('AUNClient aid must be an AID object, not a string');
758
+ if (aid !== null && aid !== undefined && !isAIDObject(aid)) {
759
+ throw new ValidationError('AUNClient only accepts an AID object or no argument');
738
760
  }
761
+ const inputAid = aid ?? null;
739
762
  const rawConfig = {};
740
763
  if (inputAid)
741
764
  rawConfig.aun_path = inputAid.aunPath;
@@ -755,18 +778,18 @@ export class AUNClient {
755
778
  this._clientLog = this._logger.for('aun_core.client');
756
779
  this._logAuth = this._logger.for('aun_core.auth');
757
780
  this._logTransport = this._logger.for('aun_core.transport');
758
- this._logKeystore = this._logger.for('aun_core.keystore');
781
+ this._tokenStoreLog = this._logger.for('aun_core.keystore');
759
782
  this._logDiscovery = this._logger.for('aun_core.discovery');
760
783
  this._logEvents = this._logger.for('aun_core.events');
761
784
  this._clientLog.info(`AUNClient initialized: debug=${_debug} aunPath=${this.configModel.aunPath} aid=${initAid ?? '-'}`);
762
785
  this._dispatcher = new EventDispatcher();
763
786
  this._discovery = new GatewayDiscovery();
764
- this._keystore = new IndexedDBKeyStore({ encryptionSeed: this.configModel.seedPassword ?? undefined });
787
+ this._tokenStore = new IndexedDBKeyStore({});
765
788
  this._slotId = inputAid?.slotId || 'default';
766
789
  this._connectDeliveryMode = normalizeDeliveryModeConfig({ mode: 'fanout' });
767
790
  this._defaultConnectDeliveryMode = { ...this._connectDeliveryMode };
768
791
  this._auth = new AuthFlow({
769
- keystore: this._keystore,
792
+ tokenStore: this._tokenStore,
770
793
  crypto: new CryptoProvider(),
771
794
  aid: initAid,
772
795
  deviceId: this._deviceId,
@@ -790,10 +813,11 @@ export class AUNClient {
790
813
  this._currentAid = inputAid;
791
814
  this._identity = {
792
815
  aid: inputAid.aid,
793
- private_key_pem: inputAid._privateKeyPem ?? '',
816
+ private_key_pem: inputAid.privateKeyPem,
794
817
  public_key_der_b64: inputAid.publicKey,
795
818
  cert: inputAid.certPem,
796
819
  };
820
+ this._auth.setIdentity(this._identity);
797
821
  this._state = 'disconnected';
798
822
  }
799
823
  }
@@ -804,8 +828,8 @@ export class AUNClient {
804
828
  if (typeof this._discovery.setLogger === 'function') {
805
829
  this._discovery.setLogger(this._logger.for('aun_core.discovery'));
806
830
  }
807
- if (typeof this._keystore.setLogger === 'function') {
808
- this._keystore.setLogger(this._logKeystore);
831
+ if (typeof this._tokenStore.setLogger === 'function') {
832
+ this._tokenStore.setLogger(this._tokenStoreLog);
809
833
  }
810
834
  // 内部订阅:推送消息 re-publish 给用户(V2 加密消息走 _raw.peer.v2.message_received)
811
835
  this._dispatcher.subscribe('_raw.message.received', (data) => {
@@ -1014,7 +1038,7 @@ export class AUNClient {
1014
1038
  throw new ValidationError('verifyAgentMd requires non-empty aid');
1015
1039
  let peer = target === this._currentAid?.aid ? this._currentAid : null;
1016
1040
  if (!peer) {
1017
- let certPem = String(await this._keystore.loadCert(target) ?? '').trim();
1041
+ let certPem = String(await this._tokenStore.loadCert(target) ?? '').trim();
1018
1042
  if (!certPem) {
1019
1043
  certPem = String(await this._fetchPeerCert(target) ?? '').trim();
1020
1044
  }
@@ -1166,11 +1190,11 @@ export class AUNClient {
1166
1190
  const key = String(logicalKey ?? '').trim();
1167
1191
  if (!key)
1168
1192
  return null;
1169
- const load = this._keystore.loadAgentMdCache;
1193
+ const load = this._tokenStore.loadAgentMdCache;
1170
1194
  if (typeof load !== 'function') {
1171
1195
  throw new Error('IndexedDB agent.md storage unavailable');
1172
1196
  }
1173
- const record = await load.call(this._keystore, this._agentMdRoot(), key);
1197
+ const record = await load.call(this._tokenStore, this._agentMdRoot(), key);
1174
1198
  if (record && Object.prototype.hasOwnProperty.call(record, 'content')) {
1175
1199
  return String(record.content ?? '');
1176
1200
  }
@@ -1180,12 +1204,12 @@ export class AUNClient {
1180
1204
  const key = String(logicalKey ?? '').trim();
1181
1205
  if (!key)
1182
1206
  return;
1183
- const save = this._keystore.upsertAgentMdCache;
1207
+ const save = this._tokenStore.upsertAgentMdCache;
1184
1208
  if (typeof save !== 'function') {
1185
1209
  throw new Error('IndexedDB agent.md storage unavailable');
1186
1210
  }
1187
1211
  const text = String(content ?? '');
1188
- await save.call(this._keystore, this._agentMdRoot(), key, {
1212
+ await save.call(this._tokenStore, this._agentMdRoot(), key, {
1189
1213
  content: text,
1190
1214
  local_etag: await this._agentMdContentEtag(text),
1191
1215
  fetched_at: Date.now(),
@@ -1564,6 +1588,63 @@ export class AUNClient {
1564
1588
  get lastError() { return this._lastError; }
1565
1589
  /** 最近一次错误码(对齐 Python last_error_code) */
1566
1590
  get lastErrorCode() { return this._lastErrorCode; }
1591
+ _applyAidRuntimeContext(aid) {
1592
+ const nextConfig = createConfig({
1593
+ aunPath: aid.aunPath,
1594
+ rootCaPem: aid.rootCaPath,
1595
+ verifySsl: aid.verifySsl,
1596
+ });
1597
+ Object.assign(this.configModel, nextConfig);
1598
+ this.config.aun_path = nextConfig.aunPath;
1599
+ this.config.root_ca_path = nextConfig.rootCaPem;
1600
+ this.config.seed_password = nextConfig.seedPassword;
1601
+ this._agentMdPath = this._agentMdDefaultRoot();
1602
+ this._agentMdCache.clear();
1603
+ this._agentMdFetchInflight.clear();
1604
+ this._peerCache.clear();
1605
+ this._certCache.clear();
1606
+ this._gatewayUrl = null;
1607
+ this._deviceId = aid.deviceId || getDeviceId();
1608
+ this._slotId = aid.slotId || 'default';
1609
+ this._logger = new AUNLogger({ debug: aid.debug, aunPath: nextConfig.aunPath });
1610
+ this._logger.bindDeviceId(this._deviceId);
1611
+ this._clientLog = this._logger.for('aun_core.client');
1612
+ this._logAuth = this._logger.for('aun_core.auth');
1613
+ this._logTransport = this._logger.for('aun_core.transport');
1614
+ this._tokenStoreLog = this._logger.for('aun_core.keystore');
1615
+ this._logDiscovery = this._logger.for('aun_core.discovery');
1616
+ this._logEvents = this._logger.for('aun_core.events');
1617
+ this._discovery = new GatewayDiscovery();
1618
+ this._tokenStore = new IndexedDBKeyStore({});
1619
+ this._auth = new AuthFlow({
1620
+ tokenStore: this._tokenStore,
1621
+ crypto: new CryptoProvider(),
1622
+ aid: aid.aid,
1623
+ deviceId: this._deviceId,
1624
+ slotId: this._slotId,
1625
+ rootCaPem: nextConfig.rootCaPem,
1626
+ verifySsl: nextConfig.verifySsl,
1627
+ });
1628
+ this._transport = new RPCTransport({
1629
+ eventDispatcher: this._dispatcher,
1630
+ timeout: DEFAULT_SESSION_OPTIONS.timeouts.call,
1631
+ onDisconnect: (error, closeCode) => this._handleTransportDisconnect(error, closeCode),
1632
+ });
1633
+ this._transport.setMetaObserver((meta) => {
1634
+ void this._observeRpcMeta(meta).catch((exc) => {
1635
+ this._clientLog.debug(`agent.md meta observer skipped: ${String(exc)}`);
1636
+ });
1637
+ });
1638
+ this._auth.setLogger(this._logAuth);
1639
+ this._transport.setLogger(this._logTransport);
1640
+ this._dispatcher.setLogger(this._logEvents);
1641
+ if (typeof this._discovery.setLogger === 'function') {
1642
+ this._discovery.setLogger(this._logDiscovery);
1643
+ }
1644
+ if (typeof this._tokenStore.setLogger === 'function') {
1645
+ this._tokenStore.setLogger(this._tokenStoreLog);
1646
+ }
1647
+ }
1567
1648
  loadIdentity(aid) {
1568
1649
  if (!aid?.isPrivateKeyValid())
1569
1650
  throw new StateError('loadIdentity requires an AID with a valid private key');
@@ -1571,16 +1652,17 @@ export class AUNClient {
1571
1652
  if (publicState !== ConnectionState.NO_IDENTITY && publicState !== ConnectionState.CLOSED) {
1572
1653
  throw new StateError(`loadIdentity not allowed in state ${publicState}`);
1573
1654
  }
1655
+ this._applyAidRuntimeContext(aid);
1574
1656
  this._currentAid = aid;
1575
1657
  this._aid = aid.aid;
1576
1658
  this._identity = {
1577
1659
  aid: aid.aid,
1578
- private_key_pem: aid._privateKeyPem ?? '',
1660
+ private_key_pem: aid.privateKeyPem,
1579
1661
  public_key_der_b64: aid.publicKey,
1580
1662
  cert: aid.certPem,
1581
1663
  };
1582
- this._auth._aid = aid.aid;
1583
- this._slotId = aid.slotId || 'default';
1664
+ // 注入内存私钥到 AuthFlow,禁止 AuthFlow 内部再走 keystore 解密
1665
+ this._auth.setIdentity(this._identity);
1584
1666
  this._state = 'disconnected';
1585
1667
  this._closing = false;
1586
1668
  }
@@ -1632,9 +1714,6 @@ export class AUNClient {
1632
1714
  get gatewayUrl() {
1633
1715
  return this._gatewayUrl;
1634
1716
  }
1635
- set gatewayUrl(url) {
1636
- this._gatewayUrl = url;
1637
- }
1638
1717
  get discovery() {
1639
1718
  return this._discovery;
1640
1719
  }
@@ -1676,10 +1755,11 @@ export class AUNClient {
1676
1755
  /** 连接到 Gateway;身份来自构造函数或 loadIdentity(aid),认证由 SDK 内部自动完成。 */
1677
1756
  async connect(opts) {
1678
1757
  const tStart = Date.now();
1679
- if (opts !== undefined && typeof opts === 'object') {
1758
+ if (opts !== undefined && opts !== null && typeof opts === 'object') {
1680
1759
  const raw = opts;
1681
- if ('access_token' in raw || 'aid' in raw || 'token' in raw) {
1682
- throw new ValidationError('connect options must not include access_token/aid; these are managed internally');
1760
+ const invalid = Object.keys(raw).filter((key) => !PUBLIC_CONNECTION_OPTION_KEYS.has(key)).sort();
1761
+ if (invalid.length > 0) {
1762
+ throw new ValidationError(`connect options contain unsupported field(s): ${invalid.join(', ')}`);
1683
1763
  }
1684
1764
  }
1685
1765
  const target = this._currentAid?.aid ?? this._aid ?? '';
@@ -1855,6 +1935,14 @@ export class AUNClient {
1855
1935
  }
1856
1936
  this._validateOutboundCall(method, p);
1857
1937
  this._injectMessageCursorContext(method, p);
1938
+ if (method.startsWith('group.')
1939
+ && !('_group_cursor_params' in p)
1940
+ && !Boolean(p._pull_gate_locked)) {
1941
+ const explicitCursorParams = this._groupCursorParams(p);
1942
+ if (Object.keys(explicitCursorParams).length > 0) {
1943
+ p._group_cursor_params = explicitCursorParams;
1944
+ }
1945
+ }
1858
1946
  // group.* 方法的 group_id 归一化为 canonical 格式(兼容老/污染数据)
1859
1947
  if (method.startsWith('group.') && p.group_id !== undefined && p.group_id !== null) {
1860
1948
  const rawGroupId = String(p.group_id);
@@ -1876,9 +1964,7 @@ export class AUNClient {
1876
1964
  const encrypt = p.encrypt !== undefined ? p.encrypt : true;
1877
1965
  delete p.encrypt;
1878
1966
  if (encrypt) {
1879
- if (!this._v2Session) {
1880
- throw new StateError('V2 session not initialized; encrypted message.send requires V2 (V1 E2EE removed)');
1881
- }
1967
+ await this._ensureV2SessionReady('message.send', 'V2 session not initialized; encrypted message.send requires V2 (V1 E2EE removed)');
1882
1968
  this._clientLog.debug('call route: message.send → V2 encrypted send');
1883
1969
  return await this._sendV2(String(p.to ?? ''), p.payload ?? {}, {
1884
1970
  messageId: String(p.message_id ?? '') || undefined,
@@ -1895,9 +1981,7 @@ export class AUNClient {
1895
1981
  const encrypt = p.encrypt !== undefined ? p.encrypt : true;
1896
1982
  delete p.encrypt;
1897
1983
  if (encrypt) {
1898
- if (!this._v2Session) {
1899
- throw new StateError('V2 session not initialized; encrypted group.send requires V2 (V1 E2EE removed)');
1900
- }
1984
+ await this._ensureV2SessionReady('group.send', 'V2 session not initialized; encrypted group.send requires V2 (V1 E2EE removed)');
1901
1985
  this._clientLog.debug('call route: group.send → V2 encrypted send');
1902
1986
  return await this._sendGroupV2(String(p.group_id ?? ''), p.payload ?? {}, {
1903
1987
  messageId: String(p.message_id ?? '') || undefined,
@@ -1912,9 +1996,7 @@ export class AUNClient {
1912
1996
  const encrypt = p.encrypt !== undefined ? p.encrypt : true;
1913
1997
  delete p.encrypt;
1914
1998
  if (encrypt) {
1915
- if (!this._v2Session) {
1916
- throw new StateError('V2 session not initialized; encrypted group.thought.put requires V2 (V1 E2EE removed)');
1917
- }
1999
+ await this._ensureV2SessionReady('group.thought.put', 'V2 session not initialized; encrypted group.thought.put requires V2 (V1 E2EE removed)');
1918
2000
  this._clientLog.debug('call route: group.thought.put → V2 encrypted put');
1919
2001
  return this._putGroupThoughtEncryptedV2(p);
1920
2002
  }
@@ -1923,9 +2005,7 @@ export class AUNClient {
1923
2005
  const encrypt = p.encrypt !== undefined ? p.encrypt : true;
1924
2006
  delete p.encrypt;
1925
2007
  if (encrypt) {
1926
- if (!this._v2Session) {
1927
- throw new StateError('V2 session not initialized; encrypted message.thought.put requires V2 (V1 E2EE removed)');
1928
- }
2008
+ await this._ensureV2SessionReady('message.thought.put', 'V2 session not initialized; encrypted message.thought.put requires V2 (V1 E2EE removed)');
1929
2009
  this._clientLog.debug('call route: message.thought.put → V2 encrypted put');
1930
2010
  return this._putMessageThoughtEncryptedV2(p);
1931
2011
  }
@@ -1944,26 +2024,45 @@ export class AUNClient {
1944
2024
  * 拆分出来以便 pull gate 包裹整个操作。
1945
2025
  */
1946
2026
  async _callImplInner(method, p) {
1947
- // message.pull:V2 就绪时走 V2 pull
1948
- if (method === 'message.pull' && this._v2Session) {
2027
+ // message.pull:V2-only,按需初始化后走 V2 pull
2028
+ if (method === 'message.pull') {
2029
+ await this._ensureV2SessionReady('message.pull');
1949
2030
  this._clientLog.debug('call route: message.pull → V2 pull');
1950
2031
  const messages = await this._pullV2(Number(p.after_seq ?? 0) || 0, Number(p.limit ?? 50) || 50, { force: p.force === true });
1951
2032
  return { messages };
1952
2033
  }
1953
- // message.ack:V2 就绪时走 V2 ack
1954
- if (method === 'message.ack' && this._v2Session) {
2034
+ // message.ack:V2-only,按需初始化后走 V2 ack
2035
+ if (method === 'message.ack') {
2036
+ await this._ensureV2SessionReady('message.ack');
1955
2037
  this._clientLog.debug('call route: message.ack → V2 ack');
1956
2038
  return await this._ackV2(Number(p.seq ?? p.up_to_seq ?? 0) || undefined);
1957
2039
  }
1958
- // group.pull:V2 就绪时走 V2 pull
1959
- if (method === 'group.pull' && this._v2Session && p.group_id) {
2040
+ // group.pull:V2-only,按需初始化后走 V2 pull
2041
+ if (method === 'group.pull' && p.group_id) {
2042
+ await this._ensureV2SessionReady('group.pull');
1960
2043
  this._clientLog.debug('call route: group.pull → V2 pull');
1961
- const messages = await this._pullGroupV2(String(p.group_id), Number(p.after_seq ?? p.after_message_seq ?? 0) || 0, Number(p.limit ?? 50) || 50);
2044
+ const hasExplicitAfterSeq = 'after_seq' in p || 'after_message_seq' in p;
2045
+ const cursorParams = this._explicitGroupCursorParams(p);
2046
+ const ownsCursor = Object.keys(cursorParams).length === 0 || this._groupCursorTargetsCurrentInstance(cursorParams);
2047
+ const pullOpts = {};
2048
+ if (hasExplicitAfterSeq)
2049
+ pullOpts.explicitAfterSeq = true;
2050
+ if (Object.keys(cursorParams).length > 0)
2051
+ pullOpts.cursorParams = cursorParams;
2052
+ if (!ownsCursor)
2053
+ pullOpts.ownsCursor = false;
2054
+ const messages = await this._pullGroupV2(String(p.group_id), Number(p.after_seq ?? p.after_message_seq ?? 0) || 0, Number(p.limit ?? 50) || 50, Object.keys(pullOpts).length > 0 ? pullOpts : undefined);
1962
2055
  return { messages };
1963
2056
  }
1964
- // group.ack_messages:V2 就绪时走 V2 ack
1965
- if (method === 'group.ack_messages' && this._v2Session && p.group_id) {
2057
+ // group.ack_messages:V2-only,按需初始化后走 V2 ack
2058
+ if (method === 'group.ack_messages' && p.group_id) {
2059
+ await this._ensureV2SessionReady('group.ack_messages');
1966
2060
  this._clientLog.debug('call route: group.ack_messages → V2 ack');
2061
+ const cursorParams = this._explicitGroupCursorParams(p);
2062
+ const ownsCursor = Object.keys(cursorParams).length === 0 || this._groupCursorTargetsCurrentInstance(cursorParams);
2063
+ if (!ownsCursor) {
2064
+ return await this._rawGroupAckMessages(p);
2065
+ }
1967
2066
  return await this._ackGroupV2(String(p.group_id), Number(p.seq ?? p.msg_seq ?? p.up_to_seq ?? 0) || undefined);
1968
2067
  }
1969
2068
  // 关键操作自动附加客户端签名
@@ -2070,6 +2169,7 @@ export class AUNClient {
2070
2169
  delete p._pull_gate_locked;
2071
2170
  delete p._skip_auto_ack;
2072
2171
  delete p.skip_auto_ack;
2172
+ delete p._group_cursor_params;
2073
2173
  if (method.startsWith('group.') && p.group_id !== undefined && p.group_id !== null) {
2074
2174
  p.group_id = normalizeGroupId(String(p.group_id)) || String(p.group_id);
2075
2175
  }
@@ -2433,6 +2533,9 @@ export class AUNClient {
2433
2533
  // 状态保护:非 connected 或正在关闭时跳过(与 Python 对齐)
2434
2534
  if (this._state !== 'connected' || this._closing)
2435
2535
  return;
2536
+ groupId = normalizeGroupId(groupId) || String(groupId ?? '').trim();
2537
+ if (!groupId)
2538
+ return;
2436
2539
  const ns = `group:${groupId}`;
2437
2540
  const afterSeq = this._seqTracker.getContiguousSeq(ns);
2438
2541
  // per-namespace 去重:同一 group namespace 只允许 1 个 in-flight pull
@@ -2441,45 +2544,11 @@ export class AUNClient {
2441
2544
  return;
2442
2545
  this._gapFillDone.add(dedupKey);
2443
2546
  this._gapFillActive = true;
2547
+ let filled = 0;
2444
2548
  try {
2445
- const result = await this.call('group.pull', {
2446
- group_id: groupId,
2447
- after_message_seq: afterSeq,
2448
- device_id: this._deviceId,
2449
- limit: 50,
2450
- });
2451
- if (isJsonObject(result)) {
2452
- const messages = result.messages;
2453
- if (Array.isArray(messages)) {
2454
- // ⚠️ 不再重复调用 onPullResult:call('group.pull') 拦截器已在内部调用过一次
2455
- const pushed = this._pushedSeqs.get(ns);
2456
- for (const msg of messages) {
2457
- if (isJsonObject(msg)) {
2458
- const s = msg.seq;
2459
- if (pushed && s !== undefined && s !== null && pushed.has(s))
2460
- continue;
2461
- if (s !== undefined && s !== null) {
2462
- await this._publishPulledMessage('group.message_created', ns, s, msg);
2463
- }
2464
- else {
2465
- await this._publishAppEvent('group.message_created', msg);
2466
- }
2467
- }
2468
- }
2469
- this._prunePushedSeqs(ns);
2470
- // publish 完成后 auto-ack
2471
- const contig = this._seqTracker.getContiguousSeq(ns);
2472
- if (contig > 0) {
2473
- const gid = groupId;
2474
- this._transport.call('group.ack_messages', {
2475
- group_id: gid,
2476
- msg_seq: contig,
2477
- device_id: this._deviceId,
2478
- slot_id: this._slotId,
2479
- }).catch((e) => { this._clientLog.warn(`group gap-fill auto-ack failed: group=${gid}`, e); });
2480
- }
2481
- }
2482
- }
2549
+ const messages = await this._pullGroupV2(groupId, afterSeq, 50);
2550
+ filled = messages.length;
2551
+ this._prunePushedSeqs(ns);
2483
2552
  }
2484
2553
  catch (exc) {
2485
2554
  this._clientLog.warn(`group message gap-fill failed:${String(exc)}`);
@@ -2488,6 +2557,9 @@ export class AUNClient {
2488
2557
  // S1: 成功 / 失败路径都必须清理飞行标记
2489
2558
  this._gapFillDone.delete(dedupKey);
2490
2559
  this._gapFillActive = false;
2560
+ if (filled > 0 && this._seqTracker.getContiguousSeq(ns) > afterSeq) {
2561
+ this._safeAsync(this._fillGroupGap(groupId));
2562
+ }
2491
2563
  }
2492
2564
  }
2493
2565
  /** 后台补齐群事件空洞 */
@@ -2604,42 +2676,11 @@ export class AUNClient {
2604
2676
  return;
2605
2677
  this._gapFillDone.add(dedupKey);
2606
2678
  this._gapFillActive = true;
2679
+ let filled = 0;
2607
2680
  try {
2608
- const result = await this.call('message.pull', {
2609
- after_seq: afterSeq,
2610
- limit: 50,
2611
- });
2612
- if (isJsonObject(result)) {
2613
- const messages = result.messages;
2614
- if (Array.isArray(messages)) {
2615
- // ⚠️ 不再重复调用 onPullResult:call('message.pull') 拦截器已在内部调用过一次
2616
- // 与 _fillGroupGap 路径对齐,避免双重 tracker 推进。
2617
- const pushed = this._pushedSeqs.get(ns);
2618
- for (const msg of messages) {
2619
- if (isJsonObject(msg)) {
2620
- const s = msg.seq;
2621
- if (pushed && s !== undefined && s !== null && pushed.has(s))
2622
- continue;
2623
- if (s !== undefined && s !== null) {
2624
- await this._publishPulledMessage('message.received', ns, s, msg);
2625
- }
2626
- else {
2627
- await this._publishAppEvent('message.received', msg);
2628
- }
2629
- }
2630
- }
2631
- this._prunePushedSeqs(ns);
2632
- // publish 完成后 auto-ack
2633
- const contig = this._seqTracker.getContiguousSeq(ns);
2634
- if (contig > 0) {
2635
- this._transport.call('message.ack', {
2636
- seq: contig,
2637
- device_id: this._deviceId,
2638
- slot_id: this._slotId,
2639
- }).catch((e) => { this._clientLog.warn(`P2P gap-fill auto-ack failed:${String(e)}`); });
2640
- }
2641
- }
2642
- }
2681
+ const messages = await this._pullV2(afterSeq, 50);
2682
+ filled = messages.length;
2683
+ this._prunePushedSeqs(ns);
2643
2684
  }
2644
2685
  catch (exc) {
2645
2686
  this._clientLog.warn(`P2P message gap-fill failed:${String(exc)}`);
@@ -2648,6 +2689,9 @@ export class AUNClient {
2648
2689
  // S1: 成功 / 失败路径都必须清理飞行标记
2649
2690
  this._gapFillDone.delete(dedupKey);
2650
2691
  this._gapFillActive = false;
2692
+ if (filled > 0 && this._seqTracker.getContiguousSeq(ns) > afterSeq) {
2693
+ this._safeAsync(this._fillP2pGap());
2694
+ }
2651
2695
  }
2652
2696
  }
2653
2697
  /** 只按硬上限裁剪 published guard,不能按 contiguousSeq 清理。 */
@@ -3007,9 +3051,9 @@ export class AUNClient {
3007
3051
  const membershipSnapshot = String(d.membership_snapshot ?? '').trim();
3008
3052
  const policySnapshot = String(d.policy_snapshot ?? '').trim();
3009
3053
  // 1. 验证 prev_state_hash 连续性
3010
- const loadFn = this._keystore.loadGroupState;
3054
+ const loadFn = this._tokenStore.loadGroupState;
3011
3055
  const localState = loadFn
3012
- ? await loadFn.call(this._keystore, groupId)
3056
+ ? await loadFn.call(this._tokenStore, groupId)
3013
3057
  : null;
3014
3058
  if (localState && localState.state_hash && localState.state_hash !== prevStateHash) {
3015
3059
  this._clientLog.warn('[aun_core] state_hash 链不连续 group=%s local_sv=%d event_sv=%d', groupId, localState.state_version, stateVersion);
@@ -3036,9 +3080,9 @@ export class AUNClient {
3036
3080
  return;
3037
3081
  }
3038
3082
  }
3039
- const saveFn = this._keystore.saveGroupState;
3083
+ const saveFn = this._tokenStore.saveGroupState;
3040
3084
  if (saveFn) {
3041
- await saveFn.call(this._keystore, groupId, {
3085
+ await saveFn.call(this._tokenStore, groupId, {
3042
3086
  group_id: groupId,
3043
3087
  state_version: sv,
3044
3088
  state_hash: sHash,
@@ -3067,9 +3111,9 @@ export class AUNClient {
3067
3111
  return;
3068
3112
  }
3069
3113
  // 3. 更新本地存储
3070
- const saveFn = this._keystore.saveGroupState;
3114
+ const saveFn = this._tokenStore.saveGroupState;
3071
3115
  if (saveFn) {
3072
- await saveFn.call(this._keystore, groupId, {
3116
+ await saveFn.call(this._tokenStore, groupId, {
3073
3117
  group_id: groupId,
3074
3118
  state_version: stateVersion,
3075
3119
  state_hash: stateHash,
@@ -3502,7 +3546,7 @@ export class AUNClient {
3502
3546
  });
3503
3547
  try {
3504
3548
  // peer 证书只存版本目录,不覆盖 cert.pem
3505
- await this._keystore.saveCert(aid, certPem, certFingerprint, { makeActive: false });
3549
+ await this._tokenStore.saveCert(aid, certPem, certFingerprint, { makeActive: false });
3506
3550
  }
3507
3551
  catch (exc) {
3508
3552
  this._clientLog.error(`write cert to keystore failed (aid=${aid}): ${String(exc)}`, exc instanceof Error ? exc : undefined);
@@ -3522,7 +3566,7 @@ export class AUNClient {
3522
3566
  const now = Date.now() / 1000;
3523
3567
  if (cached && now < cached.refreshAfter)
3524
3568
  return true;
3525
- const localCert = await this._keystore.loadCert(aid, certFingerprint);
3569
+ const localCert = await this._tokenStore.loadCert(aid, certFingerprint);
3526
3570
  if (localCert) {
3527
3571
  if (certFingerprint) {
3528
3572
  const actualFingerprint = await this._certFingerprint(localCert);
@@ -3547,7 +3591,7 @@ export class AUNClient {
3547
3591
  try {
3548
3592
  const certPem = await this._fetchPeerCert(aid, certFingerprint);
3549
3593
  // peer 证书只存版本目录,不覆盖 cert.pem
3550
- await this._keystore.saveCert(aid, certPem, certFingerprint, { makeActive: false });
3594
+ await this._tokenStore.saveCert(aid, certPem, certFingerprint, { makeActive: false });
3551
3595
  return true;
3552
3596
  }
3553
3597
  catch (exc) {
@@ -3582,11 +3626,11 @@ export class AUNClient {
3582
3626
  * 使用 SubtleCrypto 异步签名。
3583
3627
  */
3584
3628
  async _signClientOperation(method, params) {
3585
- const identity = this._identity;
3586
- if (!identity || !identity.private_key_pem)
3629
+ const currentAid = this._currentAid;
3630
+ if (!currentAid?.privateKeyPem)
3587
3631
  return;
3588
3632
  try {
3589
- const aid = (identity.aid ?? '');
3633
+ const aid = currentAid.aid;
3590
3634
  const ts = String(Math.floor(Date.now() / 1000));
3591
3635
  // 计算 params hash:覆盖所有非 _ 前缀且非 client_signature 的业务字段
3592
3636
  const paramsForHash = {};
@@ -3603,14 +3647,14 @@ export class AUNClient {
3603
3647
  .join('');
3604
3648
  const signData = new TextEncoder().encode(`${method}|${aid}|${ts}|${paramsHash}`);
3605
3649
  // 导入私钥并签名
3606
- const pkcs8 = pemToArrayBuffer(identity.private_key_pem);
3650
+ const pkcs8 = pemToArrayBuffer(currentAid.privateKeyPem);
3607
3651
  const cryptoKey = await crypto.subtle.importKey('pkcs8', pkcs8, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign']);
3608
3652
  const sigP1363 = await crypto.subtle.sign({ name: 'ECDSA', hash: 'SHA-256' }, cryptoKey, signData);
3609
3653
  // P1363 → DER 格式(与 Python 兼容)
3610
3654
  const sigDer = p1363ToDer(new Uint8Array(sigP1363));
3611
3655
  // 证书指纹
3612
3656
  let certFingerprint = '';
3613
- const certPem = (identity.cert ?? '');
3657
+ const certPem = currentAid.certPem;
3614
3658
  if (certPem) {
3615
3659
  const certDer = pemToArrayBuffer(certPem);
3616
3660
  const fpBuf = await crypto.subtle.digest('SHA-256', certDer);
@@ -3706,15 +3750,25 @@ export class AUNClient {
3706
3750
  await this._restoreSeqTrackerState();
3707
3751
  }
3708
3752
  this._startBackgroundTasks();
3709
- // V2 E2EE: 初始化 session 并注册设备 SPK(与 Python `_init_v2_session` 对齐)
3710
- try {
3711
- await this._initV2Session();
3753
+ const connectionKind = String(params.connection_kind ?? 'long');
3754
+ const isShortConnection = connectionKind === 'short';
3755
+ if (!isShortConnection) {
3756
+ // V2 E2EE: 长连接上线时初始化 session 并注册设备 SPK(与 Python `_init_v2_session` 对齐)
3757
+ try {
3758
+ await this._initV2Session();
3759
+ }
3760
+ catch (exc) {
3761
+ this._clientLog.warn(`V2 session init failed (non-fatal): ${String(exc)}`);
3762
+ }
3712
3763
  }
3713
- catch (exc) {
3714
- this._clientLog.warn(`V2 session init failed (non-fatal): ${String(exc)}`);
3764
+ else {
3765
+ this._clientLog.debug('V2 session init deferred for short connection');
3715
3766
  }
3716
3767
  // connect/reconnect 成功后自动触发一次 P2P message.pull,补齐离线期间积压
3717
- if (this._sessionOptions?.background_sync !== false) {
3768
+ const hasExplicitBackgroundSync = Object.prototype.hasOwnProperty.call(params, 'background_sync');
3769
+ const backgroundSyncEnabled = this._sessionOptions?.background_sync !== false
3770
+ && (!isShortConnection || hasExplicitBackgroundSync);
3771
+ if (backgroundSyncEnabled) {
3718
3772
  this._safeAsync(this._fillP2pGap());
3719
3773
  }
3720
3774
  this._clientLog.debug(`_connectOnce exit: elapsed=${Date.now() - tStart}ms aid=${this._aid ?? '-'}`);
@@ -3735,9 +3789,9 @@ export class AUNClient {
3735
3789
  if (this._gatewayUrl)
3736
3790
  return this._gatewayUrl;
3737
3791
  try {
3738
- const getMetadata = this._keystore.getMetadata;
3792
+ const getMetadata = this._tokenStore.getMetadata;
3739
3793
  const raw = typeof getMetadata === 'function'
3740
- ? String(await getMetadata.call(this._keystore, target, 'gateway_url') ?? '').trim()
3794
+ ? String(await getMetadata.call(this._tokenStore, target, 'gateway_url') ?? '').trim()
3741
3795
  : '';
3742
3796
  if (raw) {
3743
3797
  const gateway = raw.startsWith('"') && raw.endsWith('"') ? String(JSON.parse(raw)).trim() : raw;
@@ -3763,9 +3817,9 @@ export class AUNClient {
3763
3817
  const gateway = await this._discovery.discover(url);
3764
3818
  this._gatewayUrl = gateway;
3765
3819
  try {
3766
- const setMetadata = this._keystore.setMetadata;
3820
+ const setMetadata = this._tokenStore.setMetadata;
3767
3821
  if (typeof setMetadata === 'function') {
3768
- await setMetadata.call(this._keystore, target, 'gateway_url', gateway);
3822
+ await setMetadata.call(this._tokenStore, target, 'gateway_url', gateway);
3769
3823
  }
3770
3824
  }
3771
3825
  catch {
@@ -3802,13 +3856,8 @@ export class AUNClient {
3802
3856
  return [gateway];
3803
3857
  }
3804
3858
  async _syncIdentityAfterConnect(accessToken) {
3805
- let identity = null;
3806
- try {
3807
- identity = await this._auth.loadIdentityOrNone(this._aid ?? undefined);
3808
- }
3809
- catch { /* 忽略 */ }
3859
+ const identity = this._identity;
3810
3860
  if (!identity) {
3811
- this._identity = null;
3812
3861
  return;
3813
3862
  }
3814
3863
  identity.access_token = accessToken;
@@ -3819,9 +3868,6 @@ export class AUNClient {
3819
3868
  if (typeof persistIdentity === 'function') {
3820
3869
  await persistIdentity.call(this._auth, identity);
3821
3870
  }
3822
- else {
3823
- await this._keystore.saveIdentity(String(identity.aid), identity);
3824
- }
3825
3871
  }
3826
3872
  }
3827
3873
  // ── 内部:参数处理 ────────────────────────────────
@@ -4331,8 +4377,7 @@ export class AUNClient {
4331
4377
  }
4332
4378
  // ── Named Group(命名群)高层 API ────────────────────────────
4333
4379
  /**
4334
- * 创建命名群:本地生成 P-256 keypair,调用 group.create 传入 public_key,
4335
- * 服务端签发群 AID 证书,返回后将证书和私钥存入 keystore。
4380
+ * 创建命名群:群/P2P 私钥由 V2 数据库存储,不写入 AID 身份私钥存储。
4336
4381
  */
4337
4382
  async createNamedGroup(groupName, opts = {}) {
4338
4383
  const tStart = Date.now();
@@ -4352,15 +4397,10 @@ export class AUNClient {
4352
4397
  const aidCert = result?.aid_cert;
4353
4398
  const groupAid = String(groupInfo?.group_aid ?? '');
4354
4399
  if (groupAid && aidCert) {
4355
- await this._keystore.saveIdentity(groupAid, {
4356
- private_key_pem: identity.private_key_pem,
4357
- public_key: identity.public_key_der_b64,
4358
- curve: 'P-256',
4359
- type: 'group_identity',
4360
- });
4400
+ await this._saveGroupIdentityToV2(groupAid, identity);
4361
4401
  const certPem = String(aidCert.cert ?? '');
4362
4402
  if (certPem) {
4363
- await this._keystore.saveCert(groupAid, certPem);
4403
+ await this._tokenStore.saveCert(groupAid, certPem);
4364
4404
  }
4365
4405
  }
4366
4406
  this._clientLog.debug(`createNamedGroup exit: elapsed=${Date.now() - tStart}ms group_aid=${groupAid}`);
@@ -4391,15 +4431,10 @@ export class AUNClient {
4391
4431
  const aidCert = result?.aid_cert;
4392
4432
  const groupAid = String(groupInfo?.group_aid ?? '');
4393
4433
  if (groupAid && aidCert) {
4394
- await this._keystore.saveIdentity(groupAid, {
4395
- private_key_pem: identity.private_key_pem,
4396
- public_key: identity.public_key_der_b64,
4397
- curve: 'P-256',
4398
- type: 'group_identity',
4399
- });
4434
+ await this._saveGroupIdentityToV2(groupAid, identity);
4400
4435
  const certPem = String(aidCert.cert ?? '');
4401
4436
  if (certPem) {
4402
- await this._keystore.saveCert(groupAid, certPem);
4437
+ await this._tokenStore.saveCert(groupAid, certPem);
4403
4438
  }
4404
4439
  }
4405
4440
  this._clientLog.debug(`bindGroupAid exit: elapsed=${Date.now() - tStart}ms group_aid=${groupAid}`);
@@ -4443,7 +4478,7 @@ export class AUNClient {
4443
4478
  const slotId = this._slotId;
4444
4479
  try {
4445
4480
  // 优先从 seq_tracker 表按行读取
4446
- const loadAll = this._keystore.loadAllSeqs?.bind(this._keystore);
4481
+ const loadAll = this._tokenStore.loadAllSeqs?.bind(this._tokenStore);
4447
4482
  if (typeof loadAll === 'function') {
4448
4483
  let state = await loadAll(aid, deviceId, slotId);
4449
4484
  if (this._seqTrackerContext !== context)
@@ -4455,7 +4490,7 @@ export class AUNClient {
4455
4490
  return;
4456
4491
  }
4457
4492
  // fallback: 从旧 instance_state JSON blob 恢复
4458
- const loader = this._keystore.loadInstanceState?.bind(this._keystore);
4493
+ const loader = this._tokenStore.loadInstanceState?.bind(this._tokenStore);
4459
4494
  if (typeof loader !== 'function')
4460
4495
  return;
4461
4496
  const stateHolder = await loader(aid, deviceId, slotId);
@@ -4513,8 +4548,8 @@ export class AUNClient {
4513
4548
  const aid = this._aid;
4514
4549
  const deviceId = this._deviceId;
4515
4550
  const slotId = this._slotId;
4516
- const saver = this._keystore.saveSeq?.bind(this._keystore);
4517
- const deleter = this._keystore.deleteSeq?.bind(this._keystore);
4551
+ const saver = this._tokenStore.saveSeq?.bind(this._tokenStore);
4552
+ const deleter = this._tokenStore.deleteSeq?.bind(this._tokenStore);
4518
4553
  if (typeof saver === 'function') {
4519
4554
  for (const [oldNs, newNs] of Object.entries(renameMap)) {
4520
4555
  if (typeof deleter === 'function') {
@@ -4578,7 +4613,7 @@ export class AUNClient {
4578
4613
  return;
4579
4614
  try {
4580
4615
  // 优先按行写入 seq_tracker 表
4581
- const saveFn = this._keystore.saveSeq?.bind(this._keystore);
4616
+ const saveFn = this._tokenStore.saveSeq?.bind(this._tokenStore);
4582
4617
  if (typeof saveFn === 'function') {
4583
4618
  for (const [ns, seq] of Object.entries(state)) {
4584
4619
  saveFn(this._aid, this._deviceId, this._slotId, ns, seq).catch((exc) => {
@@ -4594,8 +4629,8 @@ export class AUNClient {
4594
4629
  return;
4595
4630
  }
4596
4631
  // fallback: 旧版 updateInstanceState JSON blob
4597
- if (typeof this._keystore.updateInstanceState === 'function') {
4598
- this._keystore.updateInstanceState(this._aid, this._deviceId, this._slotId, (current) => {
4632
+ if (typeof this._tokenStore.updateInstanceState === 'function') {
4633
+ this._tokenStore.updateInstanceState(this._aid, this._deviceId, this._slotId, (current) => {
4599
4634
  current.seq_tracker_state = state;
4600
4635
  return current;
4601
4636
  }).catch((exc) => {
@@ -4624,15 +4659,15 @@ export class AUNClient {
4624
4659
  return;
4625
4660
  const seq = this._seqTracker.getContiguousSeq(ns);
4626
4661
  try {
4627
- if (seq > 0 && typeof this._keystore.saveSeq === 'function') {
4628
- this._keystore.saveSeq(this._aid, this._deviceId, this._slotId, ns, seq).catch((exc) => {
4662
+ if (seq > 0 && typeof this._tokenStore.saveSeq === 'function') {
4663
+ this._tokenStore.saveSeq(this._aid, this._deviceId, this._slotId, ns, seq).catch((exc) => {
4629
4664
  this._clientLog.debug(`persist repaired seq failed: ns=${ns} err=${formatCaughtError(exc)}`);
4630
4665
  });
4631
4666
  return;
4632
4667
  }
4633
- const deleteSeq = this._keystore.deleteSeq;
4668
+ const deleteSeq = this._tokenStore.deleteSeq;
4634
4669
  if (seq <= 0 && typeof deleteSeq === 'function') {
4635
- deleteSeq.call(this._keystore, this._aid, this._deviceId, this._slotId, ns).catch((exc) => {
4670
+ deleteSeq.call(this._tokenStore, this._aid, this._deviceId, this._slotId, ns).catch((exc) => {
4636
4671
  this._clientLog.debug(`delete repaired seq failed: ns=${ns} err=${formatCaughtError(exc)}`);
4637
4672
  });
4638
4673
  return;
@@ -4660,6 +4695,20 @@ export class AUNClient {
4660
4695
  this._clientLog.warn(`${label} push repaired contiguous_seq: ns=${ns} payload=${hasPayload} push_seq=${pushSeq} contiguous=${contig}->${repaired}`);
4661
4696
  return repaired;
4662
4697
  }
4698
+ async _ensureV2SessionReady(method, errorMessage) {
4699
+ if (!this._v2Session) {
4700
+ if (!this._v2SessionInitInFlight) {
4701
+ this._v2SessionInitInFlight = this._initV2Session()
4702
+ .finally(() => {
4703
+ this._v2SessionInitInFlight = null;
4704
+ });
4705
+ }
4706
+ await this._v2SessionInitInFlight;
4707
+ }
4708
+ if (!this._v2Session) {
4709
+ throw new StateError(errorMessage ?? `V2 session not initialized; encrypted ${method} requires E2EE V2`);
4710
+ }
4711
+ }
4663
4712
  // ── V2 E2EE API(async,与 Python `client.py` `_init_v2_session` / `send_v2` / `pull_v2` / `ack_v2` 对齐) ──
4664
4713
  /**
4665
4714
  * 初始化 V2 session:从 AID PEM 私钥提取 raw scalar + DER 公钥,
@@ -4670,35 +4719,16 @@ export class AUNClient {
4670
4719
  async _initV2Session() {
4671
4720
  if (!this._aid)
4672
4721
  return;
4673
- let identity = this._identity;
4674
- if (!identity?.private_key_pem) {
4675
- // fallback:缓存的 identity 可能被 instance_state 污染,重新从 keystore 加载
4676
- try {
4677
- const reloaded = await this._keystore.loadIdentity(this._aid);
4678
- if (reloaded?.private_key_pem) {
4679
- this._identity = reloaded;
4680
- identity = reloaded;
4681
- this._clientLog.warn('V2 session init: identity cache was stale, reloaded from keystore');
4682
- // 自愈:重新持久化,清理 instance_state 中的脏数据
4683
- try {
4684
- const persistIdentity = this._auth._persistIdentity;
4685
- if (typeof persistIdentity === 'function') {
4686
- await persistIdentity.call(this._auth, reloaded);
4687
- }
4688
- }
4689
- catch { /* best-effort */ }
4690
- }
4691
- }
4692
- catch { /* ignore */ }
4693
- }
4694
- if (!identity?.private_key_pem) {
4722
+ // 私钥由 AIDStore 管理,直接从 _currentAid 读取明文私钥
4723
+ const currentAid = this._currentAid;
4724
+ if (!currentAid?.privateKeyPem) {
4695
4725
  this._clientLog.warn('V2 session init skipped: no AID private key');
4696
4726
  return;
4697
4727
  }
4698
4728
  if (this._v2Session)
4699
4729
  return;
4700
4730
  // 1. PEM → DER PKCS8(去掉 header/footer 后 base64 解码)
4701
- const pem = String(identity.private_key_pem).trim();
4731
+ const pem = currentAid.privateKeyPem.trim();
4702
4732
  const pemBody = pem
4703
4733
  .replace(/-----BEGIN [^-]+-----/g, '')
4704
4734
  .replace(/-----END [^-]+-----/g, '')
@@ -4730,6 +4760,20 @@ export class AUNClient {
4730
4760
  // 上线时自动确认 pending state proposals
4731
4761
  this._safeAsync(this._v2AutoConfirmPendingProposals());
4732
4762
  }
4763
+ _v2StoreDeviceId() {
4764
+ return `aid:${encodeURIComponent(String(this._aid ?? ''))}|device:${encodeURIComponent(String(this._deviceId ?? ''))}`;
4765
+ }
4766
+ async _saveGroupIdentityToV2(groupAid, identity) {
4767
+ const privateKeyPem = String(identity.private_key_pem ?? '').trim();
4768
+ const publicKeyDerB64 = String(identity.public_key_der_b64 ?? '').trim();
4769
+ if (!groupAid || !privateKeyPem || !publicKeyDerB64) {
4770
+ throw new StateError('group identity is incomplete');
4771
+ }
4772
+ if (!this._v2KeyStore) {
4773
+ this._v2KeyStore = await V2KeyStore.open();
4774
+ }
4775
+ await this._v2KeyStore.saveGroupIdentity(this._v2StoreDeviceId(), groupAid, privateKeyPem, base64ToUint8(publicKeyDerB64));
4776
+ }
4733
4777
  async _v2TrustedIKPubDer(aid) {
4734
4778
  const normalizedAid = String(aid ?? '').trim();
4735
4779
  if (!normalizedAid)
@@ -5096,6 +5140,7 @@ export class AUNClient {
5096
5140
  decrypted.push(plaintext);
5097
5141
  }
5098
5142
  }
5143
+ const hasServerAckSeq = Object.prototype.hasOwnProperty.call(result, 'server_ack_seq');
5099
5144
  const serverAckSeq = Number(result.server_ack_seq ?? 0);
5100
5145
  if (ns && Number.isFinite(serverAckSeq) && serverAckSeq > 0) {
5101
5146
  const contig = this._seqTracker.getContiguousSeq(ns);
@@ -5111,7 +5156,10 @@ export class AUNClient {
5111
5156
  await this._drainOrderedMessages(ns);
5112
5157
  this._saveSeqTrackerState();
5113
5158
  }
5114
- if (messages.length > 0 && contigAdvanced && ackSeq > 0) {
5159
+ const ackNeeded = messages.length > 0
5160
+ && ackSeq > 0
5161
+ && (contigAdvanced || (hasServerAckSeq && ackSeq > serverAckSeq));
5162
+ if (ackNeeded) {
5115
5163
  this._safeAsync(this._ackV2(ackSeq).then(() => undefined));
5116
5164
  }
5117
5165
  }
@@ -5143,7 +5191,7 @@ export class AUNClient {
5143
5191
  seq = maxSeen;
5144
5192
  }
5145
5193
  }
5146
- const raw = await this.call('message.v2.ack', { up_to_seq: seq });
5194
+ const raw = await this._callRawV2Rpc('message.v2.ack', { up_to_seq: seq });
5147
5195
  const result = isJsonObject(raw)
5148
5196
  ? { ...raw }
5149
5197
  : { result: raw };
@@ -5435,7 +5483,7 @@ export class AUNClient {
5435
5483
  * @param afterSeq 从此 seq 之后开始拉取(0/省略 = 从当前 contiguous 开始)
5436
5484
  * @param limit 最多拉取条数
5437
5485
  */
5438
- async _pullGroupV2(groupId, afterSeq = 0, limit = 50) {
5486
+ async _pullGroupV2(groupId, afterSeq = 0, limit = 50, opts) {
5439
5487
  if (!this._v2Session) {
5440
5488
  throw new StateError('V2 session not initialized (not connected?)');
5441
5489
  }
@@ -5444,15 +5492,18 @@ export class AUNClient {
5444
5492
  throw new ValidationError('group.pull requires group_id');
5445
5493
  const ns = `group:${gid}`;
5446
5494
  const decrypted = [];
5447
- let nextAfterSeq = afterSeq || this._seqTracker.getContiguousSeq(ns);
5495
+ const cursorParams = opts?.cursorParams ?? {};
5496
+ const ownsCursor = opts?.ownsCursor !== false;
5497
+ let nextAfterSeq = opts?.explicitAfterSeq ? afterSeq : (afterSeq || this._seqTracker.getContiguousSeq(ns));
5448
5498
  let pageCount = 0;
5449
5499
  const maxPages = 100;
5450
5500
  while (pageCount < maxPages) {
5451
5501
  pageCount += 1;
5452
- const result = await this.call('group.v2.pull', {
5502
+ const result = await this._callRawV2Rpc('group.v2.pull', {
5453
5503
  group_id: gid,
5454
5504
  after_seq: nextAfterSeq,
5455
5505
  limit,
5506
+ ...cursorParams,
5456
5507
  });
5457
5508
  const messages = (Array.isArray(result?.messages) ? result.messages : []);
5458
5509
  const seqs = messages
@@ -5519,6 +5570,7 @@ export class AUNClient {
5519
5570
  decrypted.push(plaintext);
5520
5571
  }
5521
5572
  const cursor = isJsonObject(result.cursor) ? result.cursor : null;
5573
+ const hasServerCursor = cursor !== null && Object.prototype.hasOwnProperty.call(cursor, 'current_seq');
5522
5574
  const serverAckSeq = Number(cursor?.current_seq ?? 0);
5523
5575
  if (Number.isFinite(serverAckSeq) && serverAckSeq > 0) {
5524
5576
  const contig = this._seqTracker.getContiguousSeq(ns);
@@ -5533,10 +5585,16 @@ export class AUNClient {
5533
5585
  await this._drainOrderedMessages(ns);
5534
5586
  this._saveSeqTrackerState();
5535
5587
  }
5536
- if (messages.length > 0 && contigAdvanced && ackSeq > 0) {
5588
+ const ackNeeded = messages.length > 0
5589
+ && ackSeq > 0
5590
+ && ownsCursor
5591
+ && (contigAdvanced || (hasServerCursor && ackSeq > serverAckSeq));
5592
+ if (ackNeeded) {
5537
5593
  this._safeAsync(this._ackGroupV2(gid, ackSeq).then(() => undefined));
5538
5594
  }
5539
5595
  const nextAfter = Math.max(pageMaxSeq, nextAfterSeq);
5596
+ if (!ownsCursor)
5597
+ break;
5540
5598
  if (messages.length === 0 || nextAfter <= nextAfterSeq || result.has_more === false)
5541
5599
  break;
5542
5600
  nextAfterSeq = nextAfter;
@@ -5546,6 +5604,31 @@ export class AUNClient {
5546
5604
  }
5547
5605
  return decrypted;
5548
5606
  }
5607
+ _groupCursorParams(params) {
5608
+ const cursorParams = {};
5609
+ for (const key of ['device_id', 'slot_id', 'device_name', 'device_type']) {
5610
+ const value = params[key];
5611
+ if (value !== undefined && value !== null)
5612
+ cursorParams[key] = value;
5613
+ }
5614
+ return cursorParams;
5615
+ }
5616
+ _explicitGroupCursorParams(params) {
5617
+ const value = params._group_cursor_params;
5618
+ if (!isJsonObject(value))
5619
+ return {};
5620
+ return { ...value };
5621
+ }
5622
+ _groupCursorTargetsCurrentInstance(params) {
5623
+ const deviceId = String(params.device_id ?? '').trim();
5624
+ const slotId = String(params.slot_id ?? '').trim();
5625
+ return (!deviceId || deviceId === (this._deviceId ?? ''))
5626
+ && (!slotId || slotId === (this._slotId ?? ''));
5627
+ }
5628
+ async _rawGroupAckMessages(params) {
5629
+ const p = { ...params };
5630
+ return await this._callRawV2Rpc('group.ack_messages', p);
5631
+ }
5549
5632
  /**
5550
5633
  * 确认 V2 群消息已消费。
5551
5634
  *
@@ -5566,7 +5649,7 @@ export class AUNClient {
5566
5649
  this._clientLog.warn(`ackGroupV2 clamp: group=${gid} up_to_seq=${seq} > max_seen=${maxSeen}, clamp`);
5567
5650
  seq = maxSeen;
5568
5651
  }
5569
- return this.call('group.v2.ack', { group_id: gid, up_to_seq: seq });
5652
+ return this._callRawV2Rpc('group.v2.ack', { group_id: gid, up_to_seq: seq });
5570
5653
  }
5571
5654
  // ── V2 thought(per-device wrap,服务端透传,不持久化)──────────
5572
5655
  /**
@@ -6387,8 +6470,8 @@ export class AUNClient {
6387
6470
  return;
6388
6471
  }
6389
6472
  let signature = '';
6390
- const identity = this._identity;
6391
- if (identity?.private_key_pem) {
6473
+ const currentAid = this._currentAid;
6474
+ if (currentAid?.privateKeyPem) {
6392
6475
  try {
6393
6476
  const signPayloadObj = {
6394
6477
  group_id: groupId,
@@ -6398,7 +6481,7 @@ export class AUNClient {
6398
6481
  };
6399
6482
  const signPayload = stableStringify(signPayloadObj);
6400
6483
  const signPayloadBytes = new TextEncoder().encode(signPayload);
6401
- const privKey = await importPrivateKeyEcdsa(identity.private_key_pem);
6484
+ const privKey = await importPrivateKeyEcdsa(currentAid.privateKeyPem);
6402
6485
  const sigBytes = await ecdsaSignDer(privKey, signPayloadBytes);
6403
6486
  signature = uint8ToBase64(sigBytes);
6404
6487
  }
@@ -6594,7 +6677,7 @@ export class AUNClient {
6594
6677
  if (newContig > 0 && newContig !== contigBefore) {
6595
6678
  const maxSeen = this._seqTracker.getMaxSeenSeq(ns);
6596
6679
  const ackSeq = maxSeen > 0 ? Math.min(newContig, maxSeen) : newContig;
6597
- this.call('message.v2.ack', { up_to_seq: ackSeq })
6680
+ this._callRawV2Rpc('message.v2.ack', { up_to_seq: ackSeq })
6598
6681
  .catch(e => this._clientLog.debug(`V2 P2P push-ack failed: ${e}`));
6599
6682
  }
6600
6683
  this._clientLog.debug(`_onV2PushNotification: push 带 payload 解密成功, contiguous_seq=${contigBefore}->${newContig} push_seq=${pushSeq}`);