@agentunion/fastaun 0.5.17 → 0.5.19

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 (95) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/_packed_docs/CHANGELOG.md +83 -0
  3. package/_packed_docs/INDEX.md +163 -16
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +52 -20
  5. package/_packed_docs/agent.md/SCHEMA.md +266 -207
  6. package/_packed_docs/aun/345/210/206/345/270/203/345/274/217/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +64 -31
  7. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +8 -6
  8. 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 +39 -39
  9. package/_packed_docs/protocol/07-/351/224/231/350/257/257/347/240/201/344/270/216/347/212/266/346/200/201/346/234/272.md +1 -1
  10. package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +28 -9
  11. package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +20 -2
  12. package/_packed_docs/protocol/agent.md/SCHEMA.md +4 -0
  13. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +5 -2
  14. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +246 -241
  15. package/_packed_docs/sdk/09-group-rpc-manual.md +46 -14
  16. package/_packed_docs/sdk/09-message-rpc-manual.md +40 -2
  17. package/_packed_docs/sdk/09-storage-rpc-manual.md +69 -38
  18. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +25 -5
  19. package/_packed_docs/sdk/INDEX.md +78 -18
  20. package/dist/agent-md-schema.d.ts +2 -0
  21. package/dist/agent-md-schema.js +46 -9
  22. package/dist/agent-md-schema.js.map +1 -1
  23. package/dist/agent-md.d.ts +46 -1
  24. package/dist/agent-md.js +492 -30
  25. package/dist/agent-md.js.map +1 -1
  26. package/dist/aid-store.d.ts +9 -1
  27. package/dist/aid-store.js +139 -18
  28. package/dist/aid-store.js.map +1 -1
  29. package/dist/aid.d.ts +17 -1
  30. package/dist/aid.js +67 -7
  31. package/dist/aid.js.map +1 -1
  32. package/dist/auth.d.ts +6 -1
  33. package/dist/auth.js +428 -15
  34. package/dist/auth.js.map +1 -1
  35. package/dist/cert-utils.d.ts +1 -1
  36. package/dist/cert-utils.js +19 -7
  37. package/dist/cert-utils.js.map +1 -1
  38. package/dist/client/delivery.d.ts +4 -1
  39. package/dist/client/delivery.js +264 -68
  40. package/dist/client/delivery.js.map +1 -1
  41. package/dist/client/rpc-pipeline.js +29 -14
  42. package/dist/client/rpc-pipeline.js.map +1 -1
  43. package/dist/client/v2-e2ee.d.ts +8 -1
  44. package/dist/client/v2-e2ee.js +893 -529
  45. package/dist/client/v2-e2ee.js.map +1 -1
  46. package/dist/client.d.ts +87 -1
  47. package/dist/client.js +816 -35
  48. package/dist/client.js.map +1 -1
  49. package/dist/errors.js +4 -3
  50. package/dist/errors.js.map +1 -1
  51. package/dist/events.d.ts +5 -1
  52. package/dist/events.js +47 -6
  53. package/dist/events.js.map +1 -1
  54. package/dist/facades.d.ts +85 -3
  55. package/dist/facades.js +104 -4
  56. package/dist/facades.js.map +1 -1
  57. package/dist/index.d.ts +9 -4
  58. package/dist/index.js +3 -1
  59. package/dist/index.js.map +1 -1
  60. package/dist/keystore/aid-db.d.ts +5 -0
  61. package/dist/keystore/aid-db.js +48 -0
  62. package/dist/keystore/aid-db.js.map +1 -1
  63. package/dist/keystore/index.d.ts +8 -0
  64. package/dist/keystore/local-identity-store.js +10 -3
  65. package/dist/keystore/local-identity-store.js.map +1 -1
  66. package/dist/keystore/local-token-store.d.ts +6 -1
  67. package/dist/keystore/local-token-store.js +24 -1
  68. package/dist/keystore/local-token-store.js.map +1 -1
  69. package/dist/org-protocol.d.ts +98 -0
  70. package/dist/org-protocol.js +1689 -0
  71. package/dist/org-protocol.js.map +1 -0
  72. package/dist/ownership-proof.d.ts +117 -0
  73. package/dist/ownership-proof.js +436 -0
  74. package/dist/ownership-proof.js.map +1 -0
  75. package/dist/ownership-v21.d.ts +178 -0
  76. package/dist/ownership-v21.js +635 -0
  77. package/dist/ownership-v21.js.map +1 -0
  78. package/dist/ownership-workflow.d.ts +82 -0
  79. package/dist/ownership-workflow.js +872 -0
  80. package/dist/ownership-workflow.js.map +1 -0
  81. package/dist/seq-tracker.d.ts +1 -0
  82. package/dist/seq-tracker.js +10 -1
  83. package/dist/seq-tracker.js.map +1 -1
  84. package/dist/tools/cross-sdk-agent.d.ts +89 -1
  85. package/dist/tools/cross-sdk-agent.js +338 -7
  86. package/dist/tools/cross-sdk-agent.js.map +1 -1
  87. package/dist/transport.d.ts +7 -0
  88. package/dist/transport.js +108 -4
  89. package/dist/transport.js.map +1 -1
  90. package/dist/v2/e2ee/decrypt.d.ts +1 -0
  91. package/dist/v2/e2ee/decrypt.js +9 -1
  92. package/dist/v2/e2ee/decrypt.js.map +1 -1
  93. package/dist/version.d.ts +1 -1
  94. package/dist/version.js +1 -1
  95. package/package.json +8 -8
package/dist/client.js CHANGED
@@ -26,7 +26,7 @@ import { normalizeGroupId } from './group-id.js';
26
26
  import { validateAIDFormat, validateGroupIDFormat } from './validators.js';
27
27
  import { RPCTransport } from './transport.js';
28
28
  import { AuthFlow } from './auth.js';
29
- import { AgentMdManager, buildDefaultAgentMd } from './agent-md.js';
29
+ import { AgentMdManager, buildDefaultAgentMd, updateOwnershipAgentMd } from './agent-md.js';
30
30
  import { GroupIndexMetaCache } from './group-index.js';
31
31
  import { SeqTracker } from './seq-tracker.js';
32
32
  import { ClientRuntime } from './client/runtime.js';
@@ -37,15 +37,20 @@ import { LifecycleController } from './client/lifecycle.js';
37
37
  import { PeerDirectory } from './client/peers.js';
38
38
  import { RpcPipeline } from './client/rpc-pipeline.js';
39
39
  import { CollabClient } from './collab/client.js';
40
- import { GroupFacade, MailFacade, MessageFacade, StreamFacade } from './facades.js';
40
+ import { CredentialFacade, OrgFacade, GroupFacade, MailFacade, MessageFacade, StreamFacade } from './facades.js';
41
41
  import { StorageVFS } from './storage/vfs.js';
42
42
  import { V2E2EECoordinator } from './client/v2-e2ee.js';
43
43
  import { GroupStateCoordinator } from './client/group-state.js';
44
44
  import { decryptMessage, } from './v2/e2ee/index.js';
45
45
  import { ecdsaVerifyRaw } from './v2/crypto/ecdsa.js';
46
+ import { decryptedOwnershipJsonText } from './v2/e2ee/decrypt.js';
46
47
  import { isJsonObject, ConnectionState, STATE_TO_PUBLIC, } from './types.js';
47
48
  import { AID } from './aid.js';
48
- import { certMatchesFingerprint, normalizeFingerprintHex, publicKeyMatchesFingerprint } from './cert-utils.js';
49
+ import { certFingerprint, certMatchesFingerprint, normalizeFingerprintHex, publicKeyMatchesFingerprint } from './cert-utils.js';
50
+ import { validateAgentMdDocument } from './agent-md-schema.js';
51
+ import { OwnershipFiles, OwnershipStorageAdapter, ownershipFailure, signOwnershipFile } from './ownership-v21.js';
52
+ import { OwnershipWorkflow } from './ownership-workflow.js';
53
+ import { canonicalJson, createCredential, createMemberCredential, signMembershipBinding, verifyServiceEvent } from './org-protocol.js';
49
54
  function isPromiseLike(value) {
50
55
  return Boolean(value && typeof value.then === 'function');
51
56
  }
@@ -120,6 +125,8 @@ const DEFAULT_SESSION_OPTIONS = {
120
125
  const RECONNECT_MIN_BASE_DELAY_MS = 1_000;
121
126
  const RECONNECT_MAX_BASE_DELAY_MS = 64_000;
122
127
  const TOKEN_REFRESH_CHECK_INTERVAL_MS = 30_000;
128
+ const PENDING_P2P_PULL_DELAY_MS = 100;
129
+ const PENDING_P2P_PULL_MAX_RETRIES = 20;
123
130
  const MAX_NOTIFY_PAYLOAD_SIZE = 64 * 1024;
124
131
  // P1-23: 非幂等方法使用更长超时(35s),避免 SDK 10s 超时 < gateway 30s 处理时间
125
132
  const NON_IDEMPOTENT_TIMEOUT_MS = 35_000;
@@ -340,6 +347,9 @@ function createAgentMdManagerForRuntime(opts) {
340
347
  logger: opts.logger(),
341
348
  ownerAidGetter: opts.ownerAid,
342
349
  currentAidGetter: opts.currentAid,
350
+ entityOwnerResolver: opts.resolveEntityOwner,
351
+ entityIdentityGetter: opts.fetchEntityIdentity,
352
+ entitySignerResolver: opts.fetchEntitySigner,
343
353
  gatewayResolver: async (aid) => {
344
354
  const gatewayUrl = await opts.gateway.resolve(aid);
345
355
  opts.gateway.set(gatewayUrl);
@@ -386,7 +396,47 @@ function createAgentMdManagerForRuntime(opts) {
386
396
  opts.identity.set(identity);
387
397
  return token;
388
398
  },
389
- peerResolver: async (aid, certFingerprint) => {
399
+ uploadAccessTokenResolver: async (_aid, gatewayUrl) => {
400
+ const localAid = String(opts.ownerAid() ?? '').trim();
401
+ let identity = opts.identity.get();
402
+ if (!identity || String(identity.aid ?? '') !== localAid) {
403
+ throw new StateError('no local identity found, register or load an AID first');
404
+ }
405
+ const auth = opts.auth();
406
+ const cachedToken = String(identity.access_token ?? '');
407
+ const expiresAt = auth.getAccessTokenExpiry(identity);
408
+ if (cachedToken && (expiresAt === null || expiresAt > Date.now() / 1000 + 30))
409
+ return cachedToken;
410
+ if (identity.refresh_token) {
411
+ try {
412
+ const refreshed = await auth.refreshCachedTokens(gatewayUrl, identity);
413
+ const refreshedToken = String(refreshed.access_token ?? '');
414
+ const refreshedExpiry = auth.getAccessTokenExpiry(refreshed);
415
+ if (refreshedToken && (refreshedExpiry === null || refreshedExpiry > Date.now() / 1000 + 30)) {
416
+ opts.identity.set(refreshed);
417
+ return refreshedToken;
418
+ }
419
+ }
420
+ catch {
421
+ // refresh 失败时回退到完整 authenticate。
422
+ }
423
+ }
424
+ const result = await auth.authenticate(gatewayUrl, { aid: localAid });
425
+ const token = String(result.access_token ?? '');
426
+ if (!token)
427
+ throw new StateError('authenticate did not return access_token');
428
+ identity = auth.loadIdentityOrNone(localAid) ?? {
429
+ ...identity,
430
+ access_token: token,
431
+ refresh_token: String(result.refresh_token ?? identity.refresh_token ?? ''),
432
+ access_token_expires_at: typeof result.expires_at === 'number' ? result.expires_at : identity.access_token_expires_at,
433
+ token_exp: typeof result.expires_at === 'number' ? result.expires_at : identity.token_exp,
434
+ expires_at: typeof result.expires_at === 'number' ? result.expires_at : identity.expires_at,
435
+ };
436
+ opts.identity.set(identity);
437
+ return token;
438
+ },
439
+ peerResolver: async (aid, certFingerprint, signedAt) => {
390
440
  const target = String(aid ?? '').trim();
391
441
  const expectedFp = String(certFingerprint ?? '').trim().toLowerCase();
392
442
  const current = opts.currentAid();
@@ -400,7 +450,7 @@ function createAgentMdManagerForRuntime(opts) {
400
450
  }
401
451
  catch { /* best effort before cert fetch */ }
402
452
  }
403
- const certPem = String(await opts.fetchPeerCert(target, expectedFp || undefined) ?? '').trim();
453
+ const certPem = String(await opts.fetchPeerCert(target, expectedFp || undefined, signedAt) ?? '').trim();
404
454
  if (!certPem)
405
455
  throw new NotFoundError(`certificate not found for aid: ${target}`);
406
456
  return AID._create({
@@ -427,6 +477,90 @@ export class AUNClient {
427
477
  _state = 'no_identity';
428
478
  /** 当前 AID 值对象(新 API) */
429
479
  _currentAid = null;
480
+ _serviceEventIdentityVersion = 0;
481
+ /** ownership 关系只在 SDK 内存中编排;服务端不保存状态机。 */
482
+ _ownershipRecords = new Map();
483
+ _ownershipType = 'agent';
484
+ _ownershipStorage = null;
485
+ _ownershipFiles = null;
486
+ _ownershipWorkflow = null;
487
+ _getOwnershipStorage() {
488
+ return this._ownershipStorage ??= new OwnershipStorageAdapter({ currentAid: () => this._aid, call: (method, params) => this.call(method, params) });
489
+ }
490
+ _getOwnershipFiles() {
491
+ return this._ownershipFiles ??= new OwnershipFiles({
492
+ currentAid: () => { if (!this._currentAid)
493
+ throw ownershipFailure('forbidden', 'ownership requires a loaded identity'); return this._currentAid; },
494
+ currentType: () => this.getType(),
495
+ now: () => Math.trunc(Date.now() / 1000),
496
+ readAgentMdFresh: (subAid) => this._agentMdManager.readOwnershipFresh(subAid),
497
+ submitAgentMdRaw: (subAid, raw, condition) => this._agentMdManager.submitOwnershipRaw(subAid, raw, condition),
498
+ readOwnerMd: (ownerAid, subAid) => this._getOwnershipStorage().readOwnerMd(ownerAid, subAid),
499
+ writeOwnerMd: (ownerAid, subAid, raw, revision) => this._getOwnershipStorage().writeOwnerMd(ownerAid, subAid, raw, revision),
500
+ deleteOwnerMd: (ownerAid, subAid, revision) => this._getOwnershipStorage().deleteOwnerMd(ownerAid, subAid, revision),
501
+ resolveCertificate: async (aid, fingerprint, purpose) => this._auth.resolveOwnershipCertificate(await this._resolveOwnershipCertificateGateway(aid), aid, fingerprint, purpose),
502
+ });
503
+ }
504
+ _getOwnershipWorkflow() {
505
+ return this._ownershipWorkflow ??= new OwnershipWorkflow({
506
+ currentAid: () => this._aid, store: this._tokenStore, files: this._getOwnershipFiles(),
507
+ currentType: () => this.getType(),
508
+ send: (to, payload) => this.call('message.send', { to, encrypt: true, payload: payload }),
509
+ emit: (event, payload) => this._dispatcher.enqueue(event, payload),
510
+ });
511
+ }
512
+ _persistOwnershipRecords() {
513
+ const aid = String(this._aid ?? '').trim();
514
+ if (!aid)
515
+ return;
516
+ const metadata = this._tokenStore.loadMetadata?.(aid) ?? {};
517
+ const existing = metadata.ownership_records;
518
+ const records = existing && typeof existing === 'object' && !Array.isArray(existing)
519
+ ? { ...existing } : {};
520
+ for (const [key, record] of this._ownershipRecords) {
521
+ if (![record.subject_aid, record.owner_aid].some((value) => String(value ?? '').trim().toLowerCase() === aid.toLowerCase()))
522
+ continue;
523
+ records[key] = Object.fromEntries(Object.entries(record).filter(([name]) => !['subject_identity', 'escrow_password', 'escrow_download_url'].includes(name)));
524
+ }
525
+ this._tokenStore.saveMetadata?.(aid, { ownership_records: records });
526
+ }
527
+ _restoreOwnershipRecords() {
528
+ const aid = String(this._aid ?? '').trim();
529
+ if (!aid)
530
+ return;
531
+ const metadata = this._tokenStore.loadMetadata?.(aid);
532
+ const intent = metadata?.registration_intent;
533
+ if (intent?.type)
534
+ this._ownershipType = String(intent.type);
535
+ const records = metadata?.ownership_records;
536
+ if (!records || typeof records !== 'object' || Array.isArray(records))
537
+ return;
538
+ for (const [key, value] of Object.entries(records)) {
539
+ if (!value || typeof value !== 'object' || Array.isArray(value))
540
+ continue;
541
+ const record = value;
542
+ this._ownershipRecords.set(key, record);
543
+ if (String(record.subject_aid ?? '').toLowerCase() === aid.toLowerCase())
544
+ this._ownershipType = String(record.type ?? this._ownershipType);
545
+ }
546
+ }
547
+ async _resumeOwnershipAfterConnect() {
548
+ await this._checkIdentityAdmissionAfterConnect();
549
+ await this._resumeRegistrationIntent();
550
+ await this._resumeOwnership();
551
+ }
552
+ async _resumeRegistrationIntent() {
553
+ const intent = this._tokenStore.loadMetadata?.(this._aid)?.registration_intent;
554
+ if (['group', 'org'].includes(String(intent?.type ?? this.getType())))
555
+ return;
556
+ if (intent?.protocol !== '2.1' || intent.pending !== true)
557
+ return;
558
+ await this._getOwnershipWorkflow().resumeRegistration(String(intent.owner), String(intent.operation_id));
559
+ this._tokenStore.saveMetadata?.(this._aid, { registration_intent: { ...intent, pending: false } });
560
+ }
561
+ async _resumeOwnership() {
562
+ await this._getOwnershipWorkflow().resume();
563
+ }
430
564
  /** 实例级 protected_headers */
431
565
  _instanceProtectedHeaders = null;
432
566
  /** 重连退避时间戳(ms) */
@@ -449,6 +583,7 @@ export class AUNClient {
449
583
  _transport;
450
584
  /** 认证流程 */
451
585
  _auth;
586
+ resolveCredentialService;
452
587
  /** 密钥存储 */
453
588
  _tokenStore;
454
589
  /** 会话参数(重连用) */
@@ -494,10 +629,17 @@ export class AUNClient {
494
629
  _pushProcessQueues = new Map();
495
630
  /** P2P pull 进行中到达的纯通知 push 上界;pull gate 释放后需要补拉一次。 */
496
631
  _pendingP2pPullUpper = new Map();
632
+ /** P2P pending pull 的延迟重试定时器及次数,覆盖服务端异步落库窗口。 */
633
+ _pendingP2pPullTimers = new Map();
634
+ _pendingP2pPullRetries = new Map();
497
635
  /** 缺 sender IK 时暂存原始 V2 消息,后台补齐 IK 后重试解密。 */
498
636
  _v2SenderIKPending = new Map();
499
637
  /** sender IK 后台补齐任务去重。 */
500
638
  _v2SenderIKFetching = new Set();
639
+ _readStateCache = new Map();
640
+ _readStateQueries = new Map();
641
+ _readStateIdentity = null;
642
+ _readStateGeneration = 0;
501
643
  // ── 后台任务定时器 ──────────────────────────────────────────
502
644
  _heartbeatTimer = null;
503
645
  _tokenRefreshTimer = null;
@@ -546,6 +688,8 @@ export class AUNClient {
546
688
  _mail;
547
689
  _group;
548
690
  _stream;
691
+ _credential;
692
+ _org;
549
693
  constructor(aid) {
550
694
  if (aid !== null && aid !== undefined && !isAIDObject(aid)) {
551
695
  throw new ValidationError('AUNClient only accepts an AID object or no argument');
@@ -581,7 +725,7 @@ export class AUNClient {
581
725
  if (debugFlag) {
582
726
  this._clientLog.info(`AUNClient initialized (debug=true, aunPath=${this._configModel.aunPath})`);
583
727
  }
584
- this._dispatcher = new EventDispatcher(this._logger.for('aun_core.events'));
728
+ this._dispatcher = new EventDispatcher(this._logger.for('aun_core.events'), () => this._identity?.aid ?? '');
585
729
  const dnsNet = new DnsResilientNet({
586
730
  verifySsl: this._configModel.verifySsl,
587
731
  logger: this._clientLog,
@@ -591,6 +735,9 @@ export class AUNClient {
591
735
  logger: this._logger.for('aun_core.keystore'),
592
736
  });
593
737
  this._tokenStore = tokenStore;
738
+ this._ownershipStorage = null;
739
+ this._ownershipFiles = null;
740
+ this._ownershipWorkflow = null;
594
741
  this._slotId = inputAid?.slotId || 'default';
595
742
  this._connectDeliveryMode = normalizeDeliveryModeConfig({ mode: 'fanout' });
596
743
  this._defaultConnectDeliveryMode = { ...this._connectDeliveryMode };
@@ -606,6 +753,7 @@ export class AUNClient {
606
753
  net: dnsNet,
607
754
  });
608
755
  this._aid = initAid;
756
+ this._restoreOwnershipRecords();
609
757
  this._agentMdManager = createAgentMdManagerForRuntime({
610
758
  config: () => this._configModel,
611
759
  logger: () => this._logger.for('aun_core.agent_md'),
@@ -623,7 +771,10 @@ export class AUNClient {
623
771
  },
624
772
  auth: () => this._auth,
625
773
  tokenStore: () => this._tokenStore,
626
- fetchPeerCert: (target, certFingerprint) => this._fetchPeerCert(target, certFingerprint || undefined),
774
+ fetchPeerCert: (target, certFingerprint, signedAt) => this._fetchPeerCert(target, certFingerprint || undefined, undefined, signedAt),
775
+ resolveEntityOwner: (target, type) => this._resolveEntityAgentMdOwner(target, type),
776
+ fetchEntityIdentity: (target, type) => this._freshEntityCertificate(target, type),
777
+ fetchEntitySigner: (target, fingerprint) => this._freshEntitySigner(target, fingerprint),
627
778
  });
628
779
  this._transport = new RPCTransport({
629
780
  eventDispatcher: this._dispatcher,
@@ -633,7 +784,9 @@ export class AUNClient {
633
784
  dnsNet,
634
785
  });
635
786
  this._transport.setProtocolMetaObserver((meta) => this._observeRpcMeta(meta));
787
+ this._transport.setServiceEventVerifier((event, payload) => verifyServiceEvent(event, payload, { targetAid: this._identity?.aid ?? '', resolveEventCertificate: (aid, fingerprint) => this._fetchServiceEventCertificate(aid, fingerprint), resolveCredentialService: this.resolveCredentialService }), () => this._identity?.aid ?? '', () => this._serviceEventIdentityVersion);
636
788
  this._transport.setProtocolDisconnectCallback((err, closeCode) => this._handleTransportDisconnect(err, closeCode));
789
+ this._transport._onForegroundDrained = () => this._delivery.onForegroundPullsDrained();
637
790
  this._runtime = new ClientRuntime(this);
638
791
  this._identityRuntime = new IdentityRuntimeManager(this._runtime);
639
792
  this._peerDirectory = new PeerDirectory(this._runtime);
@@ -660,6 +813,7 @@ export class AUNClient {
660
813
  this._dispatcher.subscribeProtocol('_raw.message.received', (data) => this._onRawMessageReceived(data));
661
814
  // P2P 消息撤回推送:在线 push 与 pull tombstone 去重后再发布给应用层
662
815
  this._dispatcher.subscribeProtocol('_raw.message.recalled', (data) => this._safeAsync(this._onRawMessageRecalled(data)));
816
+ this._dispatcher.subscribeProtocol('_raw.message.read_changed', (data) => this._onRawReadChanged(data, 'p2p'));
663
817
  // V2 P2P 推送通知:收到通知后自动走 message.v2.pull 拉取并解密
664
818
  this._dispatcher.subscribeProtocol('_raw.peer.v2.message_received', (data) => {
665
819
  const ns = this._aid ? `p2p:${this._aid}` : '';
@@ -687,6 +841,7 @@ export class AUNClient {
687
841
  this._dispatcher.subscribeProtocol('_raw.group.message_created', (data) => this._onRawGroupMessageCreated(data));
688
842
  // 群组消息撤回推送:在线 push 通道,与 pull 双 tombstone 兜底互补(SDK 去重只回调一次)
689
843
  this._dispatcher.subscribeProtocol('_raw.group.message_recalled', (data) => this._safeAsync(this._onRawGroupMessageRecalled(data)));
844
+ this._dispatcher.subscribeProtocol('_raw.group.read_changed', (data) => this._onRawReadChanged(data, 'group'));
690
845
  // 群组变更事件:透传并触发 V2 state/SPK 维护
691
846
  this._dispatcher.subscribeProtocol('_raw.group.changed', (data) => this._onRawGroupChanged(data));
692
847
  // 定向邀请底层事件:按群 + 目标 AID 聚合后向应用层只提示一次。
@@ -719,6 +874,16 @@ export class AUNClient {
719
874
  return sessionToken || null;
720
875
  }
721
876
  /** AUN Storage VFS 入口 */
877
+ get credential() {
878
+ if (!this._credential)
879
+ this._credential = new CredentialFacade(this);
880
+ return this._credential;
881
+ }
882
+ get org() {
883
+ if (!this._org)
884
+ this._org = new OrgFacade(this);
885
+ return this._org;
886
+ }
722
887
  get storage() {
723
888
  if (!this._storage)
724
889
  this._storage = new StorageVFS(this);
@@ -752,6 +917,88 @@ export class AUNClient {
752
917
  get currentAid() {
753
918
  return this._currentAid;
754
919
  }
920
+ _ownershipKey(subjectAid, ownerAid) {
921
+ return `${String(subjectAid ?? '').trim().toLowerCase()}|${String(ownerAid ?? '').trim().toLowerCase()}`;
922
+ }
923
+ getType() {
924
+ return String(this._ownershipType || 'agent').trim() || 'agent';
925
+ }
926
+ get_type() {
927
+ return this.getType();
928
+ }
929
+ getOwner() {
930
+ const aid = String(this._aid ?? '').trim().toLowerCase();
931
+ let best = null;
932
+ let bestActive = false;
933
+ for (const record of this._ownershipRecords.values()) {
934
+ if (String(record.subject_aid ?? '').trim().toLowerCase() === aid) {
935
+ if (String(record.status ?? '').trim().toLowerCase() === 'released')
936
+ continue;
937
+ const owner = String(record.owner_aid ?? '').trim();
938
+ if (!owner)
939
+ continue;
940
+ const active = String(record.status ?? '').trim().toLowerCase() === 'active';
941
+ if (!best || active && !bestActive) {
942
+ best = owner;
943
+ bestActive = active;
944
+ }
945
+ }
946
+ }
947
+ return best;
948
+ }
949
+ get_owner() {
950
+ return this.getOwner();
951
+ }
952
+ getOwnershipStatus() {
953
+ const aid = String(this._aid ?? '').trim().toLowerCase();
954
+ let best = null;
955
+ let bestActive = false;
956
+ for (const record of this._ownershipRecords.values()) {
957
+ const subject = String(record.subject_aid ?? '').trim().toLowerCase();
958
+ const owner = String(record.owner_aid ?? '').trim().toLowerCase();
959
+ if (aid !== subject && aid !== owner)
960
+ continue;
961
+ const active = String(record.status ?? '').trim().toLowerCase() === 'active';
962
+ if (!best || active && !bestActive) {
963
+ best = record;
964
+ bestActive = active;
965
+ }
966
+ }
967
+ if (best)
968
+ return Object.fromEntries(Object.entries(best).filter(([key]) => key !== 'subject_identity' && key !== 'escrow_password' && key !== 'escrow_download_url'));
969
+ return { subject_aid: this._aid ?? '', owner_aid: null, status: 'legacy_unbound', type: this.getType() };
970
+ }
971
+ get_ownership_status() {
972
+ return this.getOwnershipStatus();
973
+ }
974
+ async bindOwneraid(ownerAid) { return this._getOwnershipWorkflow().bindOwneraid(ownerAid); }
975
+ async bindSubaid(subAid) { return this._getOwnershipWorkflow().bindSubaid(subAid); }
976
+ async releaseSubaid(subAid) { return this._getOwnershipWorkflow().releaseSubaid(subAid); }
977
+ async transferSubaid(subAid, newOwnerAid) { return this._getOwnershipWorkflow().transferSubaid(subAid, newOwnerAid); }
978
+ async checkOwnershipConsensus(subAid) { return this._getOwnershipWorkflow().checkOwnershipConsensus(subAid); }
979
+ async bindAid(subjectAid, options = {}) {
980
+ throw ownershipFailure('unsupported_protocol', 'legacy bindAid is unsupported; use bindSubaid');
981
+ }
982
+ async bind_aid(subjectAid, options = {}) { return this.bindAid(subjectAid, options); }
983
+ async bindOwner(ownerAid, options = {}) {
984
+ throw ownershipFailure('unsupported_protocol', 'legacy bindOwner is unsupported; use bindOwneraid');
985
+ }
986
+ async bind_owner(ownerAid, options = {}) { return this.bindOwner(ownerAid, options); }
987
+ async releaseOwnership(subjectAid) { throw ownershipFailure('unsupported_protocol', 'legacy releaseOwnership is unsupported; use releaseSubaid'); }
988
+ async release_ownership(subjectAid) { return this.releaseOwnership(subjectAid); }
989
+ async acceptOwnershipRequest(operationId) {
990
+ return this._getOwnershipWorkflow().acceptOwnershipRequest(operationId);
991
+ }
992
+ async rejectOwnershipRequest(operationId, reason) {
993
+ return this._getOwnershipWorkflow().rejectOwnershipRequest(operationId, reason);
994
+ }
995
+ async _handleOwnershipMessage(message) {
996
+ if (!message || typeof message !== 'object' || Array.isArray(message))
997
+ return false;
998
+ const envelope = message;
999
+ const content = 'payload' in envelope ? envelope.payload : message;
1000
+ return this._getOwnershipWorkflow().receive(String(envelope.from_aid ?? ''), decryptedOwnershipJsonText(content) ?? content);
1001
+ }
755
1002
  get hasIdentity() {
756
1003
  return this._currentAid !== null && this.state !== ConnectionState.CLOSED;
757
1004
  }
@@ -881,6 +1128,9 @@ export class AUNClient {
881
1128
  logger: this._logger.for('aun_core.keystore'),
882
1129
  });
883
1130
  this._tokenStore = tokenStore;
1131
+ this._ownershipStorage = null;
1132
+ this._ownershipFiles = null;
1133
+ this._ownershipWorkflow = null;
884
1134
  this._auth = new AuthFlow({
885
1135
  tokenStore,
886
1136
  crypto: new CryptoProvider(),
@@ -909,7 +1159,10 @@ export class AUNClient {
909
1159
  },
910
1160
  auth: () => this._auth,
911
1161
  tokenStore: () => this._tokenStore,
912
- fetchPeerCert: (target, certFingerprint) => this._fetchPeerCert(target, certFingerprint || undefined),
1162
+ fetchPeerCert: (target, certFingerprint, signedAt) => this._fetchPeerCert(target, certFingerprint || undefined, undefined, signedAt),
1163
+ resolveEntityOwner: (target, type) => this._resolveEntityAgentMdOwner(target, type),
1164
+ fetchEntityIdentity: (target, type) => this._freshEntityCertificate(target, type),
1165
+ fetchEntitySigner: (target, fingerprint) => this._freshEntitySigner(target, fingerprint),
913
1166
  });
914
1167
  this._transport = new RPCTransport({
915
1168
  eventDispatcher: this._dispatcher,
@@ -919,10 +1172,20 @@ export class AUNClient {
919
1172
  dnsNet,
920
1173
  });
921
1174
  this._transport.setProtocolMetaObserver((meta) => this._observeRpcMeta(meta));
1175
+ this._transport.setServiceEventVerifier((event, payload) => verifyServiceEvent(event, payload, { targetAid: this._identity?.aid ?? '', resolveEventCertificate: (aid, fingerprint) => this._fetchServiceEventCertificate(aid, fingerprint), resolveCredentialService: this.resolveCredentialService }), () => this._identity?.aid ?? '', () => this._serviceEventIdentityVersion);
922
1176
  this._transport.setProtocolDisconnectCallback((err, closeCode) => this._handleTransportDisconnect(err, closeCode));
1177
+ this._transport._onForegroundDrained = () => this._delivery.onForegroundPullsDrained();
923
1178
  }
924
1179
  loadIdentity(aid) {
1180
+ const previousAid = this._aid;
925
1181
  this._identityRuntime.loadIdentity(aid);
1182
+ this._serviceEventIdentityVersion++;
1183
+ this._ensureReadStateIdentity();
1184
+ if (previousAid !== this._aid) {
1185
+ this._ownershipRecords.clear();
1186
+ this._ownershipType = 'agent';
1187
+ }
1188
+ this._restoreOwnershipRecords();
926
1189
  }
927
1190
  setProtectedHeaders(headers) {
928
1191
  if (!headers) {
@@ -1001,7 +1264,7 @@ export class AUNClient {
1001
1264
  }
1002
1265
  /** 连接到 Gateway;身份来自构造函数或 loadIdentity(aid),认证由 SDK 内部自动完成。 */
1003
1266
  async connect(opts) {
1004
- return this._lifecycle.connect(opts);
1267
+ await this._lifecycle.connect(opts);
1005
1268
  }
1006
1269
  /** 关闭连接 */
1007
1270
  async close() {
@@ -1020,8 +1283,13 @@ export class AUNClient {
1020
1283
  * 自动处理内部方法限制、E2EE 加解密、客户端签名等。
1021
1284
  */
1022
1285
  async call(method, params) {
1286
+ const readIdentity = ['message.read', 'message.read_state', 'group.read', 'group.read_state'].includes(method)
1287
+ ? this._ensureReadStateIdentity() : null;
1023
1288
  try {
1024
- return await this._rpcPipeline.call(method, params);
1289
+ const result = await this._rpcPipeline.call(method, params);
1290
+ if (readIdentity !== null && readIdentity === this._ensureReadStateIdentity())
1291
+ this._observeReadStateResult(method, result, params ?? {});
1292
+ return result;
1025
1293
  }
1026
1294
  catch (err) {
1027
1295
  await this._maybeHandleHalfOpenRpcFailure(err);
@@ -1074,12 +1342,23 @@ export class AUNClient {
1074
1342
  const target = message.slice(agentPrefix.length).trim();
1075
1343
  if (!target || target.toLowerCase() !== String(this._aid ?? '').trim().toLowerCase())
1076
1344
  return false;
1077
- let agentMd = buildDefaultAgentMd(target);
1345
+ return this._ensureInitialAgentMd();
1346
+ }
1347
+ async _ensureInitialAgentMd() {
1348
+ const aid = this._aid;
1349
+ if (!aid || !this._currentAid)
1350
+ return false;
1351
+ const observed = await this._getOwnershipFiles().readAgent(aid);
1352
+ if (observed.exists)
1353
+ return false;
1354
+ let agentMd = buildDefaultAgentMd(aid, { type: this.getType() });
1078
1355
  try {
1079
- agentMd = this._agentMdManager.readContent(target);
1356
+ agentMd = this._agentMdManager.readContent(aid) || agentMd;
1080
1357
  }
1081
1358
  catch { }
1082
- await this._agentMdManager.upload(agentMd);
1359
+ const payload = updateOwnershipAgentMd(agentMd, {});
1360
+ const raw = signOwnershipFile(this._currentAid, payload.endsWith('\n') || payload.endsWith('\r') ? payload : `${payload}\n`, 'agent');
1361
+ await this._agentMdManager.submitOwnershipRaw(aid, raw, { if_none_match: '*' });
1083
1362
  return true;
1084
1363
  }
1085
1364
  async _checkIdentityAdmissionAfterConnect() {
@@ -1087,10 +1366,12 @@ export class AUNClient {
1087
1366
  if (!aid)
1088
1367
  return;
1089
1368
  try {
1090
- const checked = await this._agentMdManager.check(aid);
1369
+ const checked = await this._agentMdManager.check(aid, 0);
1091
1370
  if (!checked.remote_found) {
1092
- const content = this._agentMdManager.readContent(aid) || buildDefaultAgentMd(aid);
1093
- await this._agentMdManager.upload(content);
1371
+ await this._ensureInitialAgentMd();
1372
+ }
1373
+ else if (checked.in_sync === false) {
1374
+ await this._agentMdManager.download(aid);
1094
1375
  }
1095
1376
  }
1096
1377
  catch (exc) {
@@ -1099,6 +1380,7 @@ export class AUNClient {
1099
1380
  const store = this._aidStore;
1100
1381
  if (!store)
1101
1382
  return;
1383
+ await this._resumeOrgIdentityAfterConnect(store);
1102
1384
  let listed;
1103
1385
  try {
1104
1386
  listed = await this.call('group.list_my', {});
@@ -1135,6 +1417,8 @@ export class AUNClient {
1135
1417
  await this.bindGroupAid({ group_id: groupId, group_aid: groupAid }, { aidStore: store });
1136
1418
  return;
1137
1419
  }
1420
+ if (await this._resumeGroupAgentMdUpload(store, groupAid))
1421
+ return;
1138
1422
  const checked = await this._agentMdManager.check(groupAid);
1139
1423
  if (!checked.remote_found) {
1140
1424
  await this._uploadGroupAgentMd(store, groupAid, {}, {});
@@ -1165,6 +1449,192 @@ export class AUNClient {
1165
1449
  this._groupIdentityOperations.delete(key);
1166
1450
  }
1167
1451
  }
1452
+ async createOrg(params = {}, options = {}) {
1453
+ const store = options.aidStore;
1454
+ const owner = this._currentAid;
1455
+ const orgAid = String(params.org_id ?? '').trim();
1456
+ if (!store || !owner?.isPrivateKeyValid() || !orgAid)
1457
+ throw new ValidationError('createOrg requires aidStore, org_id and current owner identity');
1458
+ this._aidStore = store;
1459
+ let identity = store.load(orgAid);
1460
+ if (!identity.ok) {
1461
+ const registered = await store.register(orgAid, { type: 'org', owner: owner.aid });
1462
+ if (!registered.ok)
1463
+ throw ownershipFailure('unavailable', registered.error.message, true);
1464
+ identity = store.load(orgAid);
1465
+ }
1466
+ if (!identity.ok)
1467
+ throw ownershipFailure('unavailable', identity.error.message, true);
1468
+ const target = options.serviceAid ?? `org.${orgAid.slice(orgAid.indexOf('.') + 1)}`;
1469
+ const tokens = store._tokenStore;
1470
+ const inputHash = crypto.createHash('sha256').update(canonicalJson(params)).digest('hex');
1471
+ let checkpoint = tokens.loadMetadata(orgAid)?.org_identity_creation;
1472
+ if (checkpoint && (checkpoint.owner_aid !== owner.aid || checkpoint.input_hash !== inputHash))
1473
+ throw ownershipFailure('conflict', 'pending Org creation context changed');
1474
+ if (!checkpoint) {
1475
+ const prepared = this.org.prepareRequest('org.lifecycle.create', { ...params, owner_aid: owner.aid }, target, owner, { orgSigner: identity.data.aid });
1476
+ checkpoint = { owner_aid: owner.aid, input_hash: inputHash, source_params: params, prepared, phase: 'create' };
1477
+ tokens.saveMetadata(orgAid, { org_identity_creation: checkpoint });
1478
+ }
1479
+ if (checkpoint.phase === 'create') {
1480
+ const result = await this.call('org.lifecycle.create', checkpoint.prepared);
1481
+ checkpoint = { ...checkpoint, result, phase: 'upload' };
1482
+ tokens.saveMetadata(orgAid, { org_identity_creation: checkpoint });
1483
+ }
1484
+ await this._uploadOrgAgentMd(store, orgAid, params);
1485
+ tokens.saveMetadata(orgAid, { org_identity_creation: null });
1486
+ return checkpoint.result;
1487
+ }
1488
+ async _resumeOrgIdentityAfterConnect(store) {
1489
+ const keyStore = store._keystore;
1490
+ const tokens = store._tokenStore;
1491
+ if (typeof keyStore?.listIdentities !== 'function' || typeof tokens?.loadMetadata !== 'function')
1492
+ return;
1493
+ for (const aid of keyStore.listIdentities()) {
1494
+ const metadata = tokens.loadMetadata(aid);
1495
+ const creation = metadata?.org_identity_creation;
1496
+ const transfer = metadata?.org_identity_transfer;
1497
+ try {
1498
+ if (creation?.owner_aid === this._aid) {
1499
+ await this.createOrg(creation.source_params, { aidStore: store });
1500
+ continue;
1501
+ }
1502
+ if (transfer?.owner_aid === this._aid) {
1503
+ await this.completeOrgTransfer(transfer.source_params, { aidStore: store });
1504
+ continue;
1505
+ }
1506
+ const pending = store._agentMdManager.pendingUpload(aid);
1507
+ if (!pending)
1508
+ continue;
1509
+ const fields = validateAgentMdDocument(pending.content, { expectedAid: aid }).frontmatter;
1510
+ if (fields.type === 'org' && fields.owner === this._aid)
1511
+ await this._resumeGroupAgentMdUpload(store, aid);
1512
+ }
1513
+ catch (error) {
1514
+ this._clientLog.warn(`post-connect Org identity recovery failed: aid=${aid} err=${formatCaughtError(error)}`);
1515
+ }
1516
+ }
1517
+ }
1518
+ async completeOrgTransfer(params = {}, options = {}) {
1519
+ const store = options.aidStore;
1520
+ const owner = this._currentAid;
1521
+ const orgAid = String(params.org_id ?? '').trim();
1522
+ const transferId = String(params.transfer_id ?? '').trim();
1523
+ if (!store || !owner?.isPrivateKeyValid() || !orgAid || !transferId)
1524
+ throw new ValidationError('completeOrgTransfer requires aidStore, org_id, transfer_id and current accepting owner');
1525
+ this._aidStore = store;
1526
+ const localStore = store._keystore;
1527
+ const tokenStore = store._tokenStore;
1528
+ const pendingKey = `org-transfer:${orgAid}:${transferId}`;
1529
+ const inputHash = crypto.createHash('sha256').update(canonicalJson(params)).digest('hex');
1530
+ let keyPair = localStore.loadPendingGroupBind(pendingKey);
1531
+ if (!keyPair) {
1532
+ keyPair = new CryptoProvider().generateIdentity();
1533
+ localStore.savePendingGroupBind(pendingKey, keyPair);
1534
+ }
1535
+ const target = options.serviceAid ?? `org.${orgAid.slice(orgAid.indexOf('.') + 1)}`;
1536
+ const publicKey = crypto.createPublicKey({ key: Buffer.from(keyPair.public_key_der_b64, 'base64'), format: 'der', type: 'spki' }).export({ format: 'pem', type: 'spki' }).toString();
1537
+ let pending = tokenStore.loadMetadata(orgAid)?.org_identity_transfer;
1538
+ if (pending && (pending.transfer_id !== transferId || pending.owner_aid !== owner.aid || pending.public_key !== publicKey || pending.input_hash !== inputHash))
1539
+ throw ownershipFailure('conflict', 'pending Org transfer context changed');
1540
+ if (!pending) {
1541
+ const rekey = { ...params, stage: 'rekey', public_key: publicKey, curve: 'P-256' };
1542
+ delete rekey.new_owner;
1543
+ delete rekey.request_binding;
1544
+ delete rekey.client_signature;
1545
+ pending = { transfer_id: transferId, owner_aid: owner.aid, public_key: publicKey, input_hash: inputHash, phase: 'rekey', source_params: params, rekey_params: this.org.prepareRequest('org.lifecycle.transferAccept', rekey, target, owner) };
1546
+ tokenStore.saveMetadata(orgAid, { org_identity_transfer: pending });
1547
+ }
1548
+ if (pending.phase === 'rekey') {
1549
+ const result = await this.call('org.lifecycle.transferAccept', pending.rekey_params);
1550
+ pending = { ...pending, phase: 'finalize', rekey_result: result };
1551
+ tokenStore.saveMetadata(orgAid, { org_identity_transfer: pending });
1552
+ }
1553
+ const rekeyResult = pending.rekey_result;
1554
+ const certPem = String(rekeyResult?.cert_pem ?? rekeyResult?.cert ?? '').trim();
1555
+ if (!certPem)
1556
+ throw ownershipFailure('unavailable', 'Org rekey response has no certificate', true);
1557
+ const imported = store.importGroupIdentity(orgAid, { ...keyPair, cert_pem: certPem });
1558
+ if (!imported.ok)
1559
+ throw ownershipFailure('invalid', imported.error.message);
1560
+ const orgIdentity = this._localEntityCertificate(orgAid);
1561
+ if (!pending.finalize_params) {
1562
+ const final = { ...pending.source_params, stage: 'finalize', public_key: publicKey, curve: 'P-256', request_id: crypto.randomUUID().replace(/-/g, ''), nonce: crypto.randomBytes(32).toString('base64url') };
1563
+ delete final.request_binding;
1564
+ delete final.client_signature;
1565
+ for (const field of ['previous_owner', 'new_owner']) {
1566
+ const material = isJsonObject(final[field]) ? final[field] : {};
1567
+ let binding = isJsonObject(material.membership_binding) ? material.membership_binding : undefined;
1568
+ if (!binding || !isJsonObject(material.terms))
1569
+ throw ownershipFailure('invalid', 'Org transfer requires both replacement membership bindings and terms');
1570
+ if (!binding.member_signature && binding.member_aid === owner.aid)
1571
+ binding = signMembershipBinding(binding, owner, 'member');
1572
+ binding = signMembershipBinding(binding, orgIdentity, 'org');
1573
+ const oldCredential = isJsonObject(material.credential_template) ? { ...material.credential_template } : undefined;
1574
+ if (oldCredential) {
1575
+ delete oldCredential.signature;
1576
+ oldCredential.key_version = orgIdentity.certFingerprint;
1577
+ }
1578
+ const credential = oldCredential ? createCredential(oldCredential, orgIdentity) : createMemberCredential(binding, orgIdentity, { key_version: orgIdentity.certFingerprint });
1579
+ final[field] = { ...material, membership_binding: binding, credential_template: credential };
1580
+ delete final[field].registration_authorization;
1581
+ }
1582
+ pending = { ...pending, finalize_params: this.org.prepareRequest('org.lifecycle.transferAccept', final, target, owner, { orgSigner: orgIdentity }) };
1583
+ tokenStore.saveMetadata(orgAid, { org_identity_transfer: pending });
1584
+ }
1585
+ const result = await this.call('org.lifecycle.transferAccept', pending.finalize_params);
1586
+ await this._uploadOrgAgentMd(store, orgAid, pending.source_params);
1587
+ localStore.clearPendingGroupBind(pendingKey);
1588
+ tokenStore.saveMetadata(orgAid, { org_identity_transfer: null });
1589
+ return result;
1590
+ }
1591
+ async dissolveOrg(params = {}, options = {}) {
1592
+ const store = options.aidStore ?? this._aidStore;
1593
+ if (store)
1594
+ this._aidStore = store;
1595
+ const orgAid = String(params.org_id ?? '').trim();
1596
+ const owner = this._currentAid;
1597
+ const target = options.serviceAid ?? `org.${orgAid.slice(orgAid.indexOf('.') + 1)}`;
1598
+ const payload = owner ? this.org.prepareRequest('org.lifecycle.dissolve', params, target, owner) : params;
1599
+ const result = await this.call('org.lifecycle.dissolve', payload);
1600
+ const organization = isJsonObject(result?.organization) ? result.organization : {};
1601
+ const status = String(organization.status ?? result?.status ?? '');
1602
+ if (store && orgAid && status === 'dissolved') {
1603
+ const tokenStore = store._tokenStore;
1604
+ const keyStore = store._keystore;
1605
+ const certPem = keyStore?.loadCert?.(orgAid);
1606
+ const metadata = tokenStore?.loadMetadata?.(orgAid) ?? {};
1607
+ const transfer = metadata.org_identity_transfer;
1608
+ if (transfer?.transfer_id && typeof keyStore?.clearPendingGroupBind === 'function') {
1609
+ keyStore.clearPendingGroupBind(`org-transfer:${orgAid}:${String(transfer.transfer_id)}`);
1610
+ }
1611
+ tokenStore?.saveMetadata?.(orgAid, {
1612
+ ...(certPem ? { invalidated_entity_certificate: certFingerprint(certPem) } : {}),
1613
+ org_identity_creation: undefined,
1614
+ org_identity_transfer: undefined,
1615
+ });
1616
+ tokenStore?.saveInstanceState?.(orgAid, store.deviceId, store.slotId, {});
1617
+ store._agentMdManager?.saveRecord?.(orgAid, {
1618
+ pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '',
1619
+ pending_condition: {}, pending_relation: {}, pending_authorization: '',
1620
+ });
1621
+ }
1622
+ return result;
1623
+ }
1624
+ async _uploadOrgAgentMd(store, orgAid, params) {
1625
+ const owner = this._currentAid;
1626
+ if (!owner)
1627
+ throw ownershipFailure('forbidden', 'Org agent.md requires current owner');
1628
+ if (await this._resumeGroupAgentMdUpload(store, orgAid))
1629
+ return;
1630
+ const identity = this._localEntityCertificate(orgAid);
1631
+ const content = String(params.org_agent_md ?? buildDefaultAgentMd(orgAid, { type: 'org', owner: owner.aid, entityCertFingerprint: identity.certFingerprint, name: String(params.name ?? orgAid) }));
1632
+ const uploaded = await store.uploadAgentMd(orgAid, content, { uploaderAid: owner.aid, entityOwnerResolver: (aid, type) => this._resolveEntityAgentMdOwner(aid, type), entityIdentityGetter: (aid) => this._freshEntityCertificate(aid, 'org') });
1633
+ if (!uploaded.ok) {
1634
+ const code = ['invalid', 'unavailable', 'forbidden', 'conflict', 'persistence_unavailable'].includes(uploaded.error.code) ? uploaded.error.code : uploaded.error.code === 'NETWORK_ERROR' ? 'unavailable' : 'invalid';
1635
+ throw ownershipFailure(code, uploaded.error.message, code === 'unavailable');
1636
+ }
1637
+ }
1168
1638
  async createGroup(params = {}, options = {}) {
1169
1639
  if (!isJsonObject(params)) {
1170
1640
  throw new ValidationError('createGroup params must be an object');
@@ -1333,6 +1803,8 @@ export class AUNClient {
1333
1803
  `name: ${JSON.stringify(name)}`,
1334
1804
  'type: "group"',
1335
1805
  'version: "1.0.0"',
1806
+ `owner: ${JSON.stringify(String(group.owner_aid ?? group.owner ?? this._aid ?? ''))}`,
1807
+ `entity_cert_fingerprint: ${JSON.stringify(this._localEntityCertificate(groupAid).certFingerprint)}`,
1336
1808
  `description: ${JSON.stringify(description)}`,
1337
1809
  ...(avatar ? [`avatar: ${JSON.stringify(avatar)}`] : []),
1338
1810
  '---',
@@ -1341,16 +1813,71 @@ export class AUNClient {
1341
1813
  '',
1342
1814
  ].join('\n');
1343
1815
  }
1816
+ async _resumeGroupAgentMdUpload(store, groupAid) {
1817
+ const identityStore = store;
1818
+ const pendingManager = identityStore._agentMdManager ?? this._agentMdManager;
1819
+ const pending = pendingManager.pendingUpload(groupAid);
1820
+ if (!pending)
1821
+ return false;
1822
+ const fields = validateAgentMdDocument(pending.content, { expectedAid: groupAid, validateOwnership: false }).frontmatter;
1823
+ if (['ownership', 'ownership_proof', 'ownership_release'].some((field) => Object.hasOwn(fields, field))) {
1824
+ throw new StateError('ownership pending agent.md requires its persisted 2.1 task', { stringCode: 'unsupported_protocol' });
1825
+ }
1826
+ const identity = this._currentAid;
1827
+ if (!identity || fields.owner !== identity.aid)
1828
+ throw ownershipFailure('forbidden', 'group agent.md recovery requires its original current owner');
1829
+ const manager = new AgentMdManager({
1830
+ aunPath: store.aunPath,
1831
+ verifySsl: identityStore._verifySsl,
1832
+ logger: identityStore._log.for('aun_core.agent_md'),
1833
+ uploadViaGatewayHost: true,
1834
+ ownerAidGetter: () => identity.aid,
1835
+ currentAidGetter: () => identity,
1836
+ entityOwnerResolver: (aid, type) => this._resolveEntityAgentMdOwner(aid, type),
1837
+ entityIdentityGetter: (aid, type) => this._freshEntityCertificate(aid, type),
1838
+ entitySignerResolver: (aid, fingerprint) => this._freshEntitySigner(aid, fingerprint),
1839
+ gatewayResolver: (aid) => identityStore._resolveGateway(aid),
1840
+ accessTokenResolver: (_aid, gatewayUrl) => identityStore._uploadAgentMdToken(identity, gatewayUrl),
1841
+ aidValidator: (aid) => identityStore._registerFlow.validateAidName(aid),
1842
+ });
1843
+ return await manager.resumePendingUpload(groupAid);
1844
+ }
1344
1845
  async _uploadGroupAgentMd(store, groupAid, params, group) {
1345
1846
  const uploaderAid = String(this._aid ?? '').trim();
1346
1847
  if (!uploaderAid) {
1347
1848
  throw new ValidationError('group agent.md upload requires current owner AID');
1348
1849
  }
1349
- const uploaded = await store.uploadAgentMd(groupAid, this._groupAgentMdContent(groupAid, params, group), { uploaderAid });
1850
+ this._aidStore = store;
1851
+ if (await this._resumeGroupAgentMdUpload(store, groupAid))
1852
+ return;
1853
+ const uploaded = await store.uploadAgentMd(groupAid, this._groupAgentMdContent(groupAid, params, group), { uploaderAid, entityOwnerResolver: (aid, type) => this._resolveEntityAgentMdOwner(aid, type), entityIdentityGetter: (aid) => this._freshEntityCertificate(aid) });
1350
1854
  if (!uploaded.ok) {
1351
- throw new ValidationError(`group agent.md upload failed: ${uploaded.error.message}`);
1855
+ const code = ['invalid', 'unavailable', 'forbidden', 'conflict', 'persistence_unavailable'].includes(uploaded.error.code) ? uploaded.error.code : uploaded.error.code === 'NETWORK_ERROR' ? 'unavailable' : 'invalid';
1856
+ throw ownershipFailure(code, `group agent.md upload failed: ${uploaded.error.message}`, code === 'unavailable');
1352
1857
  }
1353
1858
  }
1859
+ _localEntityCertificate(aid) {
1860
+ const loaded = this._aidStore?.load(aid);
1861
+ if (!loaded?.ok)
1862
+ throw ownershipFailure('unavailable', 'entity local certificate is unavailable', true);
1863
+ return loaded.data.aid;
1864
+ }
1865
+ async _freshEntityCertificate(aid, type = 'group') {
1866
+ const material = await this._auth.resolveCurrentEntityCertificate(await this._resolveOwnershipCertificateGateway(aid), aid, type);
1867
+ const certPem = material.cert_pem;
1868
+ return AID._create({ aid, aunPath: this._configModel.aunPath, certPem, certValid: true, privateKeyPem: null, privateKeyValid: false });
1869
+ }
1870
+ async _freshEntitySigner(aid, fingerprint) {
1871
+ const material = await this._auth.resolveOwnershipCertificate(await this._resolveOwnershipCertificateGateway(aid), aid, fingerprint, 'signing');
1872
+ return AID._create({ aid, aunPath: this._configModel.aunPath, certPem: material.cert_pem, certValid: true, privateKeyPem: null, privateKeyValid: false });
1873
+ }
1874
+ async _resolveEntityAgentMdOwner(aid, type) {
1875
+ const authority = await this._agentMdManager.readEntityAuthorityFresh(aid);
1876
+ const identity = await this._freshEntityCertificate(aid, type);
1877
+ if (authority.entity_cert_fingerprint !== identity.certFingerprint)
1878
+ throw ownershipFailure('conflict', 'entity authority changed during current-certificate verification');
1879
+ return authority.owner_aid;
1880
+ }
1354
1881
  async bindGroupAid(params = {}, options = {}) {
1355
1882
  if (!isJsonObject(params)) {
1356
1883
  throw new ValidationError('bindGroupAid params must be an object');
@@ -1788,6 +2315,168 @@ export class AUNClient {
1788
2315
  this._clientLog.debug(`on exit: elapsed=${Date.now() - tStart}ms event=${event}`);
1789
2316
  return result;
1790
2317
  }
2318
+ _readStateIsNewer(previous, current) {
2319
+ if (!previous)
2320
+ return true;
2321
+ const oldOrder = BigInt(String(previous.last_read_order ?? 0));
2322
+ const newOrder = BigInt(String(current.last_read_order ?? 0));
2323
+ if (oldOrder !== newOrder)
2324
+ return newOrder > oldOrder;
2325
+ const oldId = String(previous.last_read_message_id ?? '');
2326
+ const newId = String(current.last_read_message_id ?? '');
2327
+ if (oldOrder === 0n)
2328
+ return newId === oldId;
2329
+ const oldPoints = Array.from(oldId);
2330
+ const newPoints = Array.from(newId);
2331
+ for (let index = 0; index < Math.min(oldPoints.length, newPoints.length); index++) {
2332
+ const oldPoint = oldPoints[index].codePointAt(0);
2333
+ const newPoint = newPoints[index].codePointAt(0);
2334
+ if (oldPoint !== newPoint)
2335
+ return newPoint > oldPoint;
2336
+ }
2337
+ return newPoints.length >= oldPoints.length;
2338
+ }
2339
+ _ensureReadStateIdentity() {
2340
+ const identity = `${this._aid ?? ''}\u0000${this._deviceId ?? ''}`;
2341
+ if (identity !== this._readStateIdentity) {
2342
+ this._readStateCache.clear();
2343
+ this._readStateQueries.clear();
2344
+ this._readStateIdentity = identity;
2345
+ this._readStateGeneration += 1;
2346
+ }
2347
+ return `${identity}\u0000${this._readStateGeneration}`;
2348
+ }
2349
+ _registerReadStateQuery(method, params) {
2350
+ const key = method + '\u0000' + JSON.stringify(Object.keys(params).sort().map((field) => [field, params[field]]));
2351
+ this._readStateQueries.set(key, { method, params: { ...params } });
2352
+ }
2353
+ _readStateItems(result) {
2354
+ const resultObject = isJsonObject(result) ? result : null;
2355
+ return Array.isArray(resultObject?.states)
2356
+ ? resultObject.states.filter((item) => isJsonObject(item))
2357
+ : [];
2358
+ }
2359
+ _observeReadStateResult(method, result, params) {
2360
+ if (!['message.read', 'message.read_state', 'group.read', 'group.read_state'].includes(method))
2361
+ return;
2362
+ this._ensureReadStateIdentity();
2363
+ const isGroup = method.startsWith('group.');
2364
+ if (method.endsWith('.read')) {
2365
+ for (const state of this._readStateItems(params)) {
2366
+ const query = { topic_key: state.topic_key ?? '' };
2367
+ if (isGroup)
2368
+ query.group_id = state.group_id ?? params.group_id ?? '';
2369
+ else
2370
+ query.scope = state.scope || params.scope || 'p2p';
2371
+ this._registerReadStateQuery(`${isGroup ? 'group' : 'message'}.read_state`, query);
2372
+ }
2373
+ if (!Array.isArray(params.states) && params.topic_key) {
2374
+ const query = {};
2375
+ for (const field of ['scope', 'group_id', 'topic_key']) {
2376
+ if (field in params)
2377
+ query[field] = params[field];
2378
+ }
2379
+ this._registerReadStateQuery(`${isGroup ? 'group' : 'message'}.read_state`, query);
2380
+ }
2381
+ return;
2382
+ }
2383
+ this._registerReadStateQuery(method, params);
2384
+ for (const state of this._readStateItems(result)) {
2385
+ const scope = String(state.scope ?? (isGroup ? 'group' : 'p2p')).trim();
2386
+ const topicKey = String(state.topic_key ?? params.topic_key ?? '');
2387
+ const groupId = isGroup ? String(state.group_id ?? params.group_id ?? '').trim() : '';
2388
+ const lastRead = String(state.last_read_message_id ?? '').trim();
2389
+ if (!scope || !topicKey || !lastRead || (isGroup && !groupId))
2390
+ continue;
2391
+ const key = `${scope}\u0000${groupId}\u0000${topicKey}`;
2392
+ const query = { topic_key: topicKey };
2393
+ if (isGroup)
2394
+ query.group_id = groupId;
2395
+ else
2396
+ query.scope = scope;
2397
+ const previous = this._readStateCache.get(key)?.state;
2398
+ if (previous && !this._readStateIsNewer(previous, state))
2399
+ continue;
2400
+ const samePosition = previous?.last_read_message_id === state.last_read_message_id
2401
+ && BigInt(String(previous?.last_read_order ?? 0)) === BigInt(String(state.last_read_order ?? 0));
2402
+ this._readStateCache.set(key, {
2403
+ method: `${isGroup ? 'group' : 'message'}.read_state`,
2404
+ params: query,
2405
+ state: samePosition ? { ...previous, ...state } : { ...state },
2406
+ });
2407
+ }
2408
+ }
2409
+ _onRawReadChanged(data, expectedScope) {
2410
+ if (!isJsonObject(data))
2411
+ return;
2412
+ const eventData = data;
2413
+ if (eventData.scope !== expectedScope || eventData.owner_aid !== this._aid)
2414
+ return;
2415
+ const fields = ['owner_aid', 'topic_key', 'last_read_message_id', ...(expectedScope === 'group' ? ['group_id'] : [])];
2416
+ if (fields.some((field) => {
2417
+ const value = eventData[field];
2418
+ return typeof value !== 'string' || !value || Array.from(value).length > 128;
2419
+ }))
2420
+ return;
2421
+ const order = eventData.last_read_order;
2422
+ if (!(typeof order === 'number' && Number.isSafeInteger(order) && order > 0)
2423
+ && !(typeof order === 'string' && /^[1-9]\d*$/.test(order)))
2424
+ return;
2425
+ this._ensureReadStateIdentity();
2426
+ const scope = expectedScope;
2427
+ const topicKey = String(eventData.topic_key ?? '');
2428
+ const groupId = scope === 'group' ? String(eventData.group_id) : '';
2429
+ const lastRead = String(eventData.last_read_message_id);
2430
+ if (scope && topicKey && lastRead && (scope !== 'group' || groupId)) {
2431
+ const key = `${scope}\u0000${groupId}\u0000${topicKey}`;
2432
+ const query = { topic_key: topicKey };
2433
+ if (scope === 'group')
2434
+ query.group_id = groupId;
2435
+ else
2436
+ query.scope = scope;
2437
+ const method = `${scope === 'group' ? 'group' : 'message'}.read_state`;
2438
+ this._registerReadStateQuery(method, query);
2439
+ const previous = this._readStateCache.get(key)?.state;
2440
+ if (previous && (!this._readStateIsNewer(previous, eventData)
2441
+ || (previous.last_read_message_id === eventData.last_read_message_id
2442
+ && BigInt(String(previous.last_read_order ?? 0)) === BigInt(String(eventData.last_read_order ?? 0)))))
2443
+ return;
2444
+ this._readStateCache.set(key, { method, params: query, state: { ...eventData } });
2445
+ }
2446
+ this._dispatcher.enqueue(scope === 'group' ? 'group.read_changed' : 'message.read_changed', eventData);
2447
+ }
2448
+ async _reconcileReadStates() {
2449
+ const identity = this._ensureReadStateIdentity();
2450
+ const keys = new Set();
2451
+ const errors = [];
2452
+ for (const entry of [...this._readStateQueries.values()]) {
2453
+ try {
2454
+ const result = await this.call(entry.method, { ...entry.params });
2455
+ if (identity !== this._ensureReadStateIdentity())
2456
+ return;
2457
+ this._observeReadStateResult(entry.method, result, entry.params);
2458
+ for (const state of this._readStateItems(result)) {
2459
+ const scope = String(state.scope ?? (entry.method.startsWith('group.') ? 'group' : 'p2p'));
2460
+ const groupId = scope === 'group' ? String(state.group_id ?? entry.params.group_id ?? '') : '';
2461
+ const topicKey = String(state.topic_key ?? entry.params.topic_key ?? '');
2462
+ const key = `${scope}\u0000${groupId}\u0000${topicKey}`;
2463
+ if (this._readStateCache.has(key))
2464
+ keys.add(key);
2465
+ }
2466
+ }
2467
+ catch (error) {
2468
+ errors.push({ method: entry.method, params: { ...entry.params }, error: error instanceof Error ? error.message : String(error) });
2469
+ this._clientLog.debug(`read state reconnect reconciliation failed: ${error instanceof Error ? error.message : String(error)}`);
2470
+ }
2471
+ if (identity !== this._ensureReadStateIdentity())
2472
+ return;
2473
+ }
2474
+ this._dispatcher.enqueue('read_state_sync', {
2475
+ owner_aid: this._aid ?? '',
2476
+ states: [...keys].map((key) => ({ ...this._readStateCache.get(key).state })),
2477
+ errors,
2478
+ });
2479
+ }
1791
2480
  async _callRawV2Rpc(method, params, pullGateKey = '') {
1792
2481
  const p = { ...(params ?? {}) };
1793
2482
  const forceForeground = Boolean(p._rpc_foreground);
@@ -1876,6 +2565,7 @@ export class AUNClient {
1876
2565
  const previous = this._pendingP2pPullUpper.get(ns) ?? 0;
1877
2566
  if (seq > previous) {
1878
2567
  this._pendingP2pPullUpper.set(ns, seq);
2568
+ this._pendingP2pPullRetries.delete(ns);
1879
2569
  }
1880
2570
  this._clientLog.debug(`P2P pending pull upper recorded: ns=${ns}, seq=${seq}, previous=${previous}, contiguous=${this._seqTracker.getContiguousSeq(ns)}`);
1881
2571
  }
@@ -1890,6 +2580,7 @@ export class AUNClient {
1890
2580
  const contig = this._seqTracker.getContiguousSeq(ns);
1891
2581
  if (upperSeq <= contig) {
1892
2582
  this._pendingP2pPullUpper.delete(ns);
2583
+ this._pendingP2pPullRetries.delete(ns);
1893
2584
  this._clientLog.debug(`P2P pending pull upper already covered: ns=${ns}, upper_seq=${upperSeq}, contiguous=${contig}, reason=${reason}`);
1894
2585
  return false;
1895
2586
  }
@@ -1897,18 +2588,63 @@ export class AUNClient {
1897
2588
  this._clientLog.debug(`P2P pending pull postponed: ns=${ns}, upper_seq=${upperSeq}, contiguous=${contig}, state=${this._state}, closing=${this._closing}, reason=${reason}`);
1898
2589
  return false;
1899
2590
  }
1900
- if (this._rpcPipeline?.hasPullActivity?.(ns, false))
2591
+ if (this._sessionOptions.background_sync === false)
2592
+ return false;
2593
+ if (this._rpcPipeline?.hasPullActivity?.(ns, false) || this._pendingP2pPullTimers.has(ns))
2594
+ return false;
2595
+ const retries = (this._pendingP2pPullRetries.get(ns) ?? 0) + 1;
2596
+ if (retries > PENDING_P2P_PULL_MAX_RETRIES) {
2597
+ this._pendingP2pPullUpper.delete(ns);
2598
+ this._pendingP2pPullRetries.delete(ns);
2599
+ this._clientLog.warn(`P2P pending pull retries exhausted: ns=${ns}, upper_seq=${upperSeq}, contiguous=${contig}, reason=${reason}`);
1901
2600
  return false;
1902
- this._pendingP2pPullUpper.delete(ns);
1903
- this._clientLog.info(`P2P pending push follow-up pull scheduled: ns=${ns}, upper_seq=${upperSeq}, contiguous=${contig}, reason=${reason}`);
1904
- this._safeAsync(this._onV2PushNotification({ seq: upperSeq }, 'v2'));
2601
+ }
2602
+ this._pendingP2pPullRetries.set(ns, retries);
2603
+ this._clientLog.info(`P2P pending push follow-up pull scheduled: ns=${ns}, upper_seq=${upperSeq}, contiguous=${contig}, retry=${retries}, reason=${reason}`);
2604
+ const timer = setTimeout(() => {
2605
+ this._pendingP2pPullTimers.delete(ns);
2606
+ const currentUpper = this._pendingP2pPullUpper.get(ns) ?? 0;
2607
+ if (currentUpper <= 0 || currentUpper <= this._seqTracker.getContiguousSeq(ns)) {
2608
+ this._pendingP2pPullUpper.delete(ns);
2609
+ this._pendingP2pPullRetries.delete(ns);
2610
+ return;
2611
+ }
2612
+ // 暂时移除上界,避免 synthetic push 被 hasPullActivity 当成自身的 pending pull 拦截。
2613
+ this._pendingP2pPullUpper.delete(ns);
2614
+ const retry = this._onV2PushNotification({ seq: currentUpper }, 'v2')
2615
+ .then(() => {
2616
+ if (this._seqTracker.getContiguousSeq(ns) < currentUpper) {
2617
+ const pending = this._pendingP2pPullUpper.get(ns) ?? 0;
2618
+ if (currentUpper > pending)
2619
+ this._pendingP2pPullUpper.set(ns, currentUpper);
2620
+ }
2621
+ })
2622
+ .catch((error) => {
2623
+ const pending = this._pendingP2pPullUpper.get(ns) ?? 0;
2624
+ if (currentUpper > pending)
2625
+ this._pendingP2pPullUpper.set(ns, currentUpper);
2626
+ throw error;
2627
+ })
2628
+ .finally(() => {
2629
+ this._schedulePendingP2pPullIfNeeded(ns, 'pending-pull-retry-complete');
2630
+ });
2631
+ this._safeAsync(retry);
2632
+ }, PENDING_P2P_PULL_DELAY_MS);
2633
+ this._pendingP2pPullTimers.set(ns, timer);
1905
2634
  return true;
1906
2635
  }
1907
2636
  _markPublishedSeq(ns, seq) {
1908
2637
  this._delivery.markPublishedSeq(ns, seq);
1909
2638
  }
1910
- _publishAppEvent(event, payload, source = 'direct') {
1911
- return this._delivery.publishAppEvent(event, payload, source);
2639
+ _publishAppEvent(event, payload, source = 'direct', deliveryContext) {
2640
+ if (event === 'message.received') {
2641
+ return this._handleOwnershipMessage(payload).then((handled) => {
2642
+ if (!handled)
2643
+ return this._delivery.publishAppEvent(event, payload, source, deliveryContext);
2644
+ return undefined;
2645
+ });
2646
+ }
2647
+ return this._delivery.publishAppEvent(event, payload, source, deliveryContext);
1912
2648
  }
1913
2649
  _echoTimestamp() {
1914
2650
  const now = new Date();
@@ -2087,8 +2823,8 @@ export class AUNClient {
2087
2823
  }
2088
2824
  return true;
2089
2825
  }
2090
- async _drainOrderedMessages(ns, beforeSeq, pullResponse = false, persist = true, source = 'push') {
2091
- return this._delivery.drainOrderedMessages(ns, beforeSeq, pullResponse, persist, source);
2826
+ async _drainOrderedMessages(ns, beforeSeq, pullResponse = false, persist = true, source = 'push', throughSeq) {
2827
+ return this._delivery.drainOrderedMessages(ns, beforeSeq, pullResponse, persist, source, undefined, throughSeq);
2092
2828
  }
2093
2829
  async _publishOrderedMessage(event, ns, seq, payload, source = 'push') {
2094
2830
  return this._delivery.publishOrderedMessage(event, ns, seq, payload, source);
@@ -2213,6 +2949,10 @@ export class AUNClient {
2213
2949
  }
2214
2950
  const paramsHash = String(cs.params_hash ?? '');
2215
2951
  const timestamp = String(cs.timestamp ?? '');
2952
+ const signedAt = Number(timestamp) * 1000;
2953
+ if (!/^\d+$/.test(timestamp) || !Number.isSafeInteger(Number(timestamp)) || signedAt < new Date(certObj.validFrom).getTime() || signedAt > new Date(certObj.validTo).getTime()) {
2954
+ return false;
2955
+ }
2216
2956
  const signData = Buffer.from(`${method}|${sigAid}|${timestamp}|${paramsHash}`, 'utf-8');
2217
2957
  const sigB64 = String(cs.signature ?? '');
2218
2958
  const pubKey = certObj.publicKey;
@@ -2273,7 +3013,10 @@ export class AUNClient {
2273
3013
  }
2274
3014
  }
2275
3015
  try {
2276
- await this._auth.verifyPeerCertificate(peerGatewayUrl, certPem, aid);
3016
+ if (opts.signedAt === undefined)
3017
+ await this._auth.verifyPeerCertificate(peerGatewayUrl, certPem, aid);
3018
+ else
3019
+ await this._auth.verifyPeerCertificate(peerGatewayUrl, certPem, aid, opts.signedAt);
2277
3020
  }
2278
3021
  catch (exc) {
2279
3022
  if (cachedBootstrapChain) {
@@ -2287,9 +3030,9 @@ export class AUNClient {
2287
3030
  validatedAt: nowSec,
2288
3031
  refreshAfter: nowSec + PEER_CERT_CACHE_TTL,
2289
3032
  };
2290
- const cacheKey = AUNClient._certCacheKey(aid, certFingerprint);
3033
+ const cacheKey = AUNClient._certCacheKey(aid, certFingerprint) + (opts.signedAt === undefined ? '' : `:signed:${opts.signedAt}`);
2291
3034
  this._certCache.set(cacheKey, entry);
2292
- if (!certFingerprint) {
3035
+ if (!certFingerprint && opts.signedAt === undefined) {
2293
3036
  const bareKey = AUNClient._certCacheKey(aid);
2294
3037
  this._certCache.set(bareKey, entry);
2295
3038
  const actualFp = `sha256:${x509Cert.fingerprint256.replace(/:/g, '').toLowerCase()}`;
@@ -2304,12 +3047,19 @@ export class AUNClient {
2304
3047
  }
2305
3048
  return certPem;
2306
3049
  }
3050
+ async _fetchServiceEventCertificate(aid, fingerprint) {
3051
+ const certificate = await this._fetchPeerCert(aid, fingerprint);
3052
+ if (!this._gatewayUrl)
3053
+ throw new ValidationError('gateway url unavailable for service event certificate validation');
3054
+ await this._auth.verifyPeerCertificate(AUNClient._resolvePeerGatewayUrl(this._gatewayUrl, aid), certificate, aid, undefined, true);
3055
+ return certificate;
3056
+ }
2307
3057
  /** 获取对方证书(带缓存 + 完整 PKI 验证),跨域时自动路由到 peer 所在域。 */
2308
- async _fetchPeerCert(aid, certFingerprint, timeoutMs = 30_000) {
3058
+ async _fetchPeerCert(aid, certFingerprint, timeoutMs = 30_000, signedAt) {
2309
3059
  const tStart = Date.now();
2310
3060
  this._clientLog.debug(`_fetchPeerCert enter: aid=${aid}, fp=${certFingerprint ?? ''}`);
2311
3061
  try {
2312
- const cacheKey = AUNClient._certCacheKey(aid, certFingerprint);
3062
+ const cacheKey = AUNClient._certCacheKey(aid, certFingerprint) + (signedAt === undefined ? '' : `:signed:${signedAt}`);
2313
3063
  const cached = this._certCache.get(cacheKey);
2314
3064
  const now = Date.now() / 1000;
2315
3065
  if (cached && now < cached.refreshAfter) {
@@ -2327,6 +3077,7 @@ export class AUNClient {
2327
3077
  certPem,
2328
3078
  certFingerprint,
2329
3079
  source: 'fetch',
3080
+ signedAt,
2330
3081
  });
2331
3082
  this._clientLog.debug(`_fetchPeerCert exit: elapsed=${Date.now() - tStart}ms aid=${aid} (fetched)`);
2332
3083
  return validated;
@@ -2441,6 +3192,10 @@ export class AUNClient {
2441
3192
  this._pushedSeqs.clear();
2442
3193
  this._pendingOrderedMsgs.clear();
2443
3194
  this._pendingP2pPullUpper.clear();
3195
+ for (const timer of this._pendingP2pPullTimers.values())
3196
+ clearTimeout(timer);
3197
+ this._pendingP2pPullTimers.clear();
3198
+ this._pendingP2pPullRetries.clear();
2444
3199
  this._v2SenderIKPending.clear();
2445
3200
  this._v2SenderIKFetching.clear();
2446
3201
  this._groupSynced.clear();
@@ -2457,6 +3212,10 @@ export class AUNClient {
2457
3212
  this._pushedSeqs.clear();
2458
3213
  this._pendingOrderedMsgs.clear();
2459
3214
  this._pendingP2pPullUpper.clear();
3215
+ for (const timer of this._pendingP2pPullTimers.values())
3216
+ clearTimeout(timer);
3217
+ this._pendingP2pPullTimers.clear();
3218
+ this._pendingP2pPullRetries.clear();
2460
3219
  this._v2SenderIKPending.clear();
2461
3220
  this._v2SenderIKFetching.clear();
2462
3221
  this._groupSynced.clear();
@@ -2851,6 +3610,7 @@ export class AUNClient {
2851
3610
  }
2852
3611
  this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
2853
3612
  this._startBackgroundTasks();
3613
+ this._safeAsync(this._reconcileReadStates());
2854
3614
  const connectionKind = String(params.connection_kind ?? 'long');
2855
3615
  const isShortConnection = connectionKind === 'short';
2856
3616
  const hasExplicitBackgroundSync = Object.prototype.hasOwnProperty.call(params, 'background_sync');
@@ -2877,7 +3637,7 @@ export class AUNClient {
2877
3637
  this._clientLog.warn(`schedule post-connect P2P gap fill failed: ${formatCaughtError(exc)}`);
2878
3638
  });
2879
3639
  }
2880
- this._safeAsync(this._checkIdentityAdmissionAfterConnect());
3640
+ this._safeAsync(this._resumeOwnershipAfterConnect());
2881
3641
  this._clientLog.debug(`_connectOnce exit: elapsed=${Date.now() - tStart}ms gateway=${gatewayUrl}, aid=${this._aid ?? ''}`);
2882
3642
  }
2883
3643
  catch (err) {
@@ -3276,10 +4036,15 @@ export class AUNClient {
3276
4036
  encrypted: true,
3277
4037
  e2ee,
3278
4038
  };
4039
+ if (typeof plaintext.type === 'string' && plaintext.type.startsWith('aun.ownership.'))
4040
+ result.from_aid = fromAid;
3279
4041
  if (senderDeviceId)
3280
4042
  result.sender_device_id = senderDeviceId;
3281
4043
  const explicitDirection = String(msg.direction ?? '').trim();
3282
- result.direction = explicitDirection || (fromAid && fromAid === this._aid ? 'outbound_sync' : 'inbound');
4044
+ if (explicitDirection)
4045
+ result.direction = explicitDirection;
4046
+ else if (fromAid && fromAid !== this._aid)
4047
+ result.direction = 'inbound';
3283
4048
  if (msg.device_id !== undefined)
3284
4049
  result.device_id = msg.device_id;
3285
4050
  attachGatewayProximity(result, msg);
@@ -3478,14 +4243,30 @@ export class AUNClient {
3478
4243
  return this._delivery.onRawGroupV2MessageCreated(data, expectedVersion, inlineGeneration);
3479
4244
  }
3480
4245
  /** Push 通知带 payload 时的就地解密(复用 _decryptV2Message) */
3481
- async _decryptV2PushMessage(data) {
3482
- return await this._v2E2EE.decryptV2PushMessage(data);
4246
+ async _decryptV2PushMessage(data, deferStatus) {
4247
+ return await this._v2E2EE.decryptV2PushMessage(data, deferStatus);
3483
4248
  }
3484
4249
  /** 按当前 AID 发现 Gateway;用于 authenticate()/connect() 的新入口。 */
3485
4250
  async _resolveGatewayForAid(aid) {
3486
4251
  const gateways = await this._resolveGatewayCandidatesForAid(aid);
3487
4252
  return gateways[0];
3488
4253
  }
4254
+ async _resolveOwnershipCertificateGateway(aid) {
4255
+ const issuer = aid.slice(aid.indexOf('.') + 1);
4256
+ if (issuer === this._aid?.slice(this._aid.indexOf('.') + 1))
4257
+ return this._resolveGatewayForAid(aid);
4258
+ const port = this._configModel.discoveryPort ? `:${this._configModel.discoveryPort}` : '';
4259
+ let lastError;
4260
+ for (const host of [aid, `gateway.${issuer}`]) {
4261
+ try {
4262
+ return await this._discovery.discover(`https://${host}${port}/.well-known/aun-gateway`);
4263
+ }
4264
+ catch (error) {
4265
+ lastError = error;
4266
+ }
4267
+ }
4268
+ throw ownershipFailure('unavailable', lastError instanceof Error ? lastError.message : 'signer Gateway discovery failed', true);
4269
+ }
3489
4270
  /** 发现并缓存全部 Gateway 候选,供首连和自动重连故障转移使用。 */
3490
4271
  async _resolveGatewayCandidatesForAid(aid) {
3491
4272
  const resolvedAid = String(aid ?? this._aid ?? '').trim();