@agentunion/fastaun 0.5.20 → 0.5.21

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 (107) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/_packed_docs/CHANGELOG.md +47 -0
  3. package/_packed_docs/INDEX.md +453 -440
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +164 -161
  5. package/_packed_docs/agent.md/working/AID/347/261/273/345/236/213-owner/345/205/263/347/263/273/344/270/216/346/215/242owner/347/274/226/346/216/222/346/226/271/346/241/210.md +530 -0
  6. package/_packed_docs/agent.md/working/AID/347/261/273/345/236/213-owner/345/205/263/347/263/273/345/256/214/346/225/264/350/256/276/350/256/241/344/270/216/345/256/236/346/226/275/350/256/241/345/210/222.md +685 -0
  7. package/_packed_docs/agent.md/working/agent.md-schema/344/270/216/347/255/276/345/217/221/351/252/214/347/255/276/346/265/201/347/250/213/346/276/204/346/270/205.md +349 -0
  8. package/_packed_docs/agent.md/working//346/234/254/345/234/260Docker/346/265/213/350/257/225Bug/346/270/205/345/215/225.md +40 -0
  9. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1433 -1392
  10. package/_packed_docs/protocol/08-AUN-E2EE.md +17 -12
  11. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +2 -0
  12. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +666 -658
  13. package/_packed_docs/sdk/0.5.21-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +368 -0
  14. package/_packed_docs/sdk/0.5.21-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +350 -0
  15. package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +1 -1
  16. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +5 -1
  17. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +64 -64
  18. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +4 -1
  19. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +42 -39
  20. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +147 -14
  21. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +48 -47
  22. package/_packed_docs/sdk/09-group-rpc-manual.md +5 -2
  23. package/_packed_docs/sdk/09-message-rpc-manual.md +250 -160
  24. package/_packed_docs/sdk/09-/345/207/255/350/257/201/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +71 -0
  25. package/_packed_docs/sdk/09-/347/273/204/347/273/207/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +88 -0
  26. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +134 -130
  27. package/_packed_docs/sdk/INDEX.md +239 -221
  28. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +930 -922
  29. package/_packed_docs/sdk/Ownership2.1-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +106 -43
  30. package/_packed_docs/sdk/Ownership2.1-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +180 -9
  31. package/_packed_docs/sdk/Ownership2.1-TDD/345/210/206/351/230/266/346/256/265/345/256/236/346/226/275/350/256/241/345/210/222.md +708 -708
  32. package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +480 -476
  33. package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +1 -1
  34. package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +2 -2
  35. package/dist/agent-md-schema.js +6 -4
  36. package/dist/agent-md-schema.js.map +1 -1
  37. package/dist/agent-md.d.ts +22 -2
  38. package/dist/agent-md.js +84 -67
  39. package/dist/agent-md.js.map +1 -1
  40. package/dist/aid-store.d.ts +13 -0
  41. package/dist/aid-store.js +50 -15
  42. package/dist/aid-store.js.map +1 -1
  43. package/dist/auth.js +4 -0
  44. package/dist/auth.js.map +1 -1
  45. package/dist/cert-utils.d.ts +1 -0
  46. package/dist/cert-utils.js +13 -0
  47. package/dist/cert-utils.js.map +1 -1
  48. package/dist/client/delivery.d.ts +59 -1
  49. package/dist/client/delivery.js +1018 -125
  50. package/dist/client/delivery.js.map +1 -1
  51. package/dist/client/rpc-pipeline.d.ts +4 -1
  52. package/dist/client/rpc-pipeline.js +123 -37
  53. package/dist/client/rpc-pipeline.js.map +1 -1
  54. package/dist/client/v2-e2ee.d.ts +12 -2
  55. package/dist/client/v2-e2ee.js +751 -294
  56. package/dist/client/v2-e2ee.js.map +1 -1
  57. package/dist/client.d.ts +26 -2
  58. package/dist/client.js +379 -63
  59. package/dist/client.js.map +1 -1
  60. package/dist/config.d.ts +4 -0
  61. package/dist/config.js +1 -0
  62. package/dist/config.js.map +1 -1
  63. package/dist/events.d.ts +18 -1
  64. package/dist/events.js +148 -9
  65. package/dist/events.js.map +1 -1
  66. package/dist/facades.d.ts +43 -1
  67. package/dist/facades.js +363 -7
  68. package/dist/facades.js.map +1 -1
  69. package/dist/index.d.ts +5 -3
  70. package/dist/index.js +1 -1
  71. package/dist/index.js.map +1 -1
  72. package/dist/keystore/aid-db.d.ts +6 -1
  73. package/dist/keystore/aid-db.js +184 -42
  74. package/dist/keystore/aid-db.js.map +1 -1
  75. package/dist/keystore/index.d.ts +14 -0
  76. package/dist/keystore/local-token-store.d.ts +9 -1
  77. package/dist/keystore/local-token-store.js +79 -14
  78. package/dist/keystore/local-token-store.js.map +1 -1
  79. package/dist/org-protocol.d.ts +45 -0
  80. package/dist/org-protocol.js +754 -11
  81. package/dist/org-protocol.js.map +1 -1
  82. package/dist/ownership-v21.d.ts +16 -3
  83. package/dist/ownership-v21.js +35 -17
  84. package/dist/ownership-v21.js.map +1 -1
  85. package/dist/ownership-workflow.d.ts +17 -0
  86. package/dist/ownership-workflow.js +511 -26
  87. package/dist/ownership-workflow.js.map +1 -1
  88. package/dist/secret-store/file-store.js +84 -0
  89. package/dist/secret-store/file-store.js.map +1 -1
  90. package/dist/seq-tracker.d.ts +1 -0
  91. package/dist/seq-tracker.js +2 -2
  92. package/dist/seq-tracker.js.map +1 -1
  93. package/dist/tools/cross-sdk-agent.d.ts +66 -3
  94. package/dist/tools/cross-sdk-agent.js +466 -36
  95. package/dist/tools/cross-sdk-agent.js.map +1 -1
  96. package/dist/transport.d.ts +3 -0
  97. package/dist/transport.js +98 -44
  98. package/dist/transport.js.map +1 -1
  99. package/dist/types.d.ts +1 -0
  100. package/dist/v2/e2ee/encrypt-group.js +1 -0
  101. package/dist/v2/e2ee/encrypt-group.js.map +1 -1
  102. package/dist/v2/e2ee/encrypt-p2p.js +1 -0
  103. package/dist/v2/e2ee/encrypt-p2p.js.map +1 -1
  104. package/dist/v2/e2ee/types.d.ts +1 -0
  105. package/dist/version.d.ts +1 -1
  106. package/dist/version.js +1 -1
  107. package/package.json +1 -1
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, updateOwnershipAgentMd } from './agent-md.js';
29
+ import { AgentMdManager, buildDefaultAgentMd } 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,7 +37,7 @@ 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 { CredentialFacade, OrgFacade, GroupFacade, MailFacade, MessageFacade, StreamFacade } from './facades.js';
40
+ import { DebugFacade, 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';
@@ -48,9 +48,9 @@ import { isJsonObject, ConnectionState, STATE_TO_PUBLIC, } from './types.js';
48
48
  import { AID } from './aid.js';
49
49
  import { certFingerprint, certMatchesFingerprint, normalizeFingerprintHex, publicKeyMatchesFingerprint } from './cert-utils.js';
50
50
  import { validateAgentMdCertificate, validateAgentMdDocument } from './agent-md-schema.js';
51
- import { OwnershipFiles, OwnershipStorageAdapter, ownershipFailure, signOwnershipFile } from './ownership-v21.js';
51
+ import { OwnershipFiles, OwnershipStorageAdapter, ownershipFailure } from './ownership-v21.js';
52
52
  import { OwnershipWorkflow } from './ownership-workflow.js';
53
- import { canonicalJson, createCredential, createMemberCredential, signMembershipBinding, verifyServiceEvent } from './org-protocol.js';
53
+ import { canonicalJson, createCredential, createMemberCredential, signMembershipBinding, digest, validateMembershipBinding, verifyMembershipBinding, verifyCredentialSignature, createRepresentationProof, verifyServiceEvent, buildRequestBinding, validateRequestBinding, CredentialRenewal } from './org-protocol.js';
54
54
  function isPromiseLike(value) {
55
55
  return Boolean(value && typeof value.then === 'function');
56
56
  }
@@ -478,6 +478,7 @@ export class AUNClient {
478
478
  /** 当前 AID 值对象(新 API) */
479
479
  _currentAid = null;
480
480
  _serviceEventIdentityVersion = 0;
481
+ _serviceEventIdentityAbort = new AbortController();
481
482
  /** ownership 关系只在 SDK 内存中编排;服务端不保存状态机。 */
482
483
  _ownershipRecords = new Map();
483
484
  _ownershipType = 'agent';
@@ -494,9 +495,9 @@ export class AUNClient {
494
495
  currentType: () => this.getType(),
495
496
  now: () => Math.trunc(Date.now() / 1000),
496
497
  readAgentMdFresh: (subAid) => this._agentMdManager.readOwnershipFresh(subAid),
497
- submitAgentMdRaw: (subAid, raw, condition) => this._agentMdManager.submitOwnershipRaw(subAid, raw, condition),
498
+ submitAgentMdRaw: (subAid, raw, condition, assertActive) => this._agentMdManager.submitOwnershipRaw(subAid, raw, condition, assertActive),
498
499
  readOwnerMd: (ownerAid, subAid) => this._getOwnershipStorage().readOwnerMd(ownerAid, subAid),
499
- writeOwnerMd: (ownerAid, subAid, raw) => this._getOwnershipStorage().writeOwnerMd(ownerAid, subAid, raw),
500
+ writeOwnerMd: (ownerAid, subAid, raw, original, assertActive) => this._getOwnershipStorage().writeOwnerMd(ownerAid, subAid, raw, original, assertActive),
500
501
  deleteOwnerMd: (ownerAid, subAid) => this._getOwnershipStorage().deleteOwnerMd(ownerAid, subAid),
501
502
  resolveCertificate: async (aid, fingerprint, purpose) => this._auth.resolveOwnershipCertificate(await this._resolveOwnershipCertificateGateway(aid), aid, fingerprint, purpose),
502
503
  });
@@ -549,6 +550,188 @@ export class AUNClient {
549
550
  await this._resumeRegistrationIntent();
550
551
  await this._resumeOwnership();
551
552
  }
553
+ _credentialRenewal;
554
+ _credentialRenewalTimer = null;
555
+ _credentialRenewalAbort = null;
556
+ _credentialRenewalScan = Promise.resolve();
557
+ _getCredentialRenewal() {
558
+ return this._credentialRenewal ??= new CredentialRenewal({
559
+ currentAid: () => this._aid ?? '',
560
+ atomicCheckpointSave: () => this._tokenStore instanceof LocalTokenStore,
561
+ load: (aid) => (this._tokenStore._loadMetadataStrict?.(aid) ?? this._tokenStore.loadMetadata?.(aid))?.credential_renewals_v1 ?? {},
562
+ ...(typeof this._tokenStore._loadMetadataStrict === 'function' ? {
563
+ loadRevocations: (aid) => this._tokenStore._loadMetadataStrict(aid)?.credential_revocations_v1 ?? {},
564
+ saveRevocations: (aid, state, guard) => {
565
+ if (guard && !guard.current())
566
+ throw new ValidationError('credential revocation synchronization was superseded');
567
+ this._tokenStore.saveMetadata?.(aid, { credential_revocations_v1: state });
568
+ },
569
+ } : {}),
570
+ save: (aid, records, guard) => {
571
+ const version = this._serviceEventIdentityVersion;
572
+ const current = () => version === this._serviceEventIdentityVersion && aid === this._aid && (!guard || guard.current());
573
+ if (!current())
574
+ throw new ValidationError('membership event receiver identity changed');
575
+ const saved = this._tokenStore instanceof LocalTokenStore ? records : Object.fromEntries(Object.entries(records).map(([cid, value]) => {
576
+ const record = { ...value };
577
+ for (const key of ['submitted_at', 'submission_hash', 'registration_submitted_at', 'registration_submission_hash'])
578
+ delete record[key];
579
+ return [cid, record];
580
+ }));
581
+ this._tokenStore.saveMetadata?.(aid, { credential_renewals_v1: saved });
582
+ if (!current())
583
+ throw new ValidationError('membership event receiver identity changed');
584
+ },
585
+ signer: (aid, guard) => this._credentialRenewalSigner(aid, guard),
586
+ certificate: (aid, fingerprint, signedAt) => signedAt === undefined
587
+ ? this._fetchServiceEventCertificate(aid, fingerprint) : this._fetchPeerCert(aid, fingerprint, undefined, signedAt),
588
+ call: (method, params, onSent) => {
589
+ if (!onSent)
590
+ return this._rpcPipeline.call(method, params);
591
+ const version = this._serviceEventIdentityVersion, aid = this._aid;
592
+ return this._rpcPipeline['_callWithSent'](method, params, async () => {
593
+ if (version !== this._serviceEventIdentityVersion || aid !== this._aid)
594
+ throw new ValidationError('credential sender identity changed');
595
+ await onSent();
596
+ });
597
+ },
598
+ send: (to, payload) => this.call('message.send', { to, encrypt: true, payload }),
599
+ });
600
+ }
601
+ _assertCredentialNotRevoked(credential, proofChain) {
602
+ const version = this._serviceEventIdentityVersion, aid = this._aid;
603
+ const current = () => { if (aid !== this._aid || version !== this._serviceEventIdentityVersion)
604
+ throw new ValidationError('credential receiver identity changed'); };
605
+ this._getCredentialRenewal()._assertNotRevoked(credential, proofChain);
606
+ current();
607
+ return current;
608
+ }
609
+ async _credentialRenewalSigner(aid, guard) {
610
+ const check = () => { if (guard && !guard.current())
611
+ throw new ValidationError('credential renewal was superseded'); };
612
+ check();
613
+ const stored = this._aidStore?.load(aid);
614
+ let identity = aid === this._aid && this._identity?.private_key_pem
615
+ ? AID._create({ aid, aunPath: this._configModel.aunPath, certPem: String(this._identity.cert ?? this._currentAid?.certPem ?? ''), privateKeyPem: String(this._identity.private_key_pem), certValid: true, privateKeyValid: true })
616
+ : stored?.ok ? stored.data.aid : undefined;
617
+ if (!identity?.isPrivateKeyValid())
618
+ return null;
619
+ const certificate = new crypto.X509Certificate(identity.certPem);
620
+ const end = Date.parse(certificate.validTo), start = Date.parse(certificate.validFrom);
621
+ if (end > Date.now() && end - Date.now() < (end - start) / 2 && this._aidStore) {
622
+ const renewed = await (guard ? this._aidStore._renewCert(aid, guard) : this._aidStore.renewCert(aid));
623
+ check();
624
+ if (!renewed.ok)
625
+ throw new ValidationError(renewed.error.message);
626
+ const loaded = this._aidStore.load(aid);
627
+ if (!loaded.ok)
628
+ throw new ValidationError(loaded.error.message);
629
+ identity = loaded.data.aid;
630
+ if (aid === this._aid && this._identity) {
631
+ this._identity.cert = identity.certPem;
632
+ this._currentAid = identity;
633
+ }
634
+ }
635
+ return identity;
636
+ }
637
+ _currentCredential(input) {
638
+ const records = this._tokenStore.loadMetadata?.(this._aid ?? '')?.credential_renewals_v1 ?? {};
639
+ let cid = typeof input === 'string' ? input : String(input.cid);
640
+ const seen = new Set();
641
+ while (records[cid]?.successor_cid && !seen.has(cid)) {
642
+ seen.add(cid);
643
+ cid = String(records[cid].successor_cid);
644
+ }
645
+ return records[cid] ? structuredClone(records[cid]) : null;
646
+ }
647
+ _credentialCurrent(input) {
648
+ return this._getCredentialRenewal().current(input);
649
+ }
650
+ async _resumeCredentialRenewalAfterConnect(guard) {
651
+ if (!this._aid || (guard && !guard.current()))
652
+ return;
653
+ await this._safeAsync(guard ? this._credentialRenewalSigner(this._aid, guard) : this._credentialRenewalSigner(this._aid));
654
+ if (!guard || guard.current())
655
+ await this._safeAsync(this._getCredentialRenewal()._syncRevocations?.(guard) ?? Promise.resolve());
656
+ if (!guard || guard.current())
657
+ await this._getCredentialRenewal().resume(guard);
658
+ }
659
+ _startCredentialRenewal() {
660
+ this._stopCredentialRenewal();
661
+ const aid = this._aid, version = this._serviceEventIdentityVersion;
662
+ if (!aid || !['ready', 'connected'].includes(this._state) || this._closing || this._disconnecting)
663
+ return;
664
+ const abort = new AbortController();
665
+ this._credentialRenewalAbort = abort;
666
+ const current = () => this._credentialRenewalAbort === abort && !abort.signal.aborted && aid === this._aid && version === this._serviceEventIdentityVersion && ['ready', 'connected'].includes(this._state) && !this._closing && !this._disconnecting;
667
+ const guard = { current, signal: abort.signal };
668
+ const scan = async () => {
669
+ const task = this._credentialRenewalScan.then(async () => {
670
+ if (!current())
671
+ return;
672
+ await this._resumeCredentialRenewalAfterConnect(guard);
673
+ });
674
+ this._credentialRenewalScan = task.catch(() => { });
675
+ await this._safeAsync(task);
676
+ if (current())
677
+ this._credentialRenewalTimer = setTimeout(() => { void scan(); }, 60_000);
678
+ };
679
+ void scan();
680
+ }
681
+ _stopCredentialRenewal() {
682
+ this._credentialRenewalAbort?.abort();
683
+ this._credentialRenewalAbort = null;
684
+ if (this._credentialRenewalTimer !== null)
685
+ clearTimeout(this._credentialRenewalTimer);
686
+ this._credentialRenewalTimer = null;
687
+ }
688
+ _rememberCredential(credential, material, certificate) {
689
+ if (!material.long_term && certificate) {
690
+ const cert = new crypto.X509Certificate(typeof certificate === 'string' && !certificate.includes('-----BEGIN CERTIFICATE-----') ? Buffer.from(certificate, 'base64') : certificate);
691
+ const parent = Array.isArray(credential.proof_chain) ? credential.proof_chain[0] : undefined;
692
+ const certificateEnd = Math.floor(Date.parse(cert.validTo) / 1000);
693
+ const defaultEnd = credential.parent != null && isJsonObject(parent) && typeof parent.exp === 'number'
694
+ ? Math.min(certificateEnd, Number(parent.exp)) : certificateEnd;
695
+ material = { ...material, long_term: credential.exp === defaultEnd };
696
+ }
697
+ this._safeAsync(this._getCredentialRenewal().remember(credential, material));
698
+ }
699
+ async _verifyIncomingServiceEvent(event, payload) {
700
+ const targetAid = this._identity?.aid ?? '';
701
+ const version = this._serviceEventIdentityVersion;
702
+ const signal = this._serviceEventIdentityAbort.signal;
703
+ const expiresAt = Number(payload?.expires_at);
704
+ const current = () => version === this._serviceEventIdentityVersion && targetAid === (this._identity?.aid ?? '') && Date.now() / 1000 < expiresAt;
705
+ const verified = await verifyServiceEvent(event, payload, { targetAid, resolveEventCertificate: (aid, fingerprint) => this._fetchServiceEventCertificate(aid, fingerprint), resolveCredentialService: this.resolveCredentialService });
706
+ if (!current())
707
+ throw new ValidationError('service event receiver identity changed');
708
+ if (event.replace(/^event\//, '') === 'org.member.changed' && isJsonObject(payload) && Object.hasOwn(payload, 'membership_package')) {
709
+ await this._getCredentialRenewal().importMembershipPackage(payload, { current, signal });
710
+ if (!current())
711
+ throw new ValidationError('service event receiver identity changed');
712
+ }
713
+ return verified;
714
+ }
715
+ async _handleCredentialRenewal(message) {
716
+ if (!isJsonObject(message))
717
+ return false;
718
+ const envelope = message;
719
+ const content = 'payload' in envelope ? envelope.payload : envelope;
720
+ const payload = decryptedOwnershipJsonText(content) ?? content;
721
+ let parsed = payload;
722
+ if (typeof payload === 'string') {
723
+ try {
724
+ parsed = JSON.parse(payload);
725
+ }
726
+ catch {
727
+ return false;
728
+ }
729
+ }
730
+ if (!isJsonObject(parsed) || !['credential.renewal.request', 'credential.renewal.result'].includes(String(parsed.type)))
731
+ return false;
732
+ await this._safeAsync(this._getCredentialRenewal().receive(String(envelope.from ?? envelope.from_aid ?? envelope.sender_aid ?? ''), parsed));
733
+ return true;
734
+ }
552
735
  async _resumeRegistrationIntent() {
553
736
  const intent = this._tokenStore.loadMetadata?.(this._aid)?.registration_intent;
554
737
  if (['group', 'org'].includes(String(intent?.type ?? this.getType())))
@@ -598,11 +781,13 @@ export class AUNClient {
598
781
  _defaultConnectDeliveryMode;
599
782
  /** peer 证书缓存 */
600
783
  _certCache = new Map();
784
+ _certFetchInFlight = new Map();
601
785
  _agentMdManager;
602
786
  _aidStore;
603
787
  _groupIdentityOperations = new Map();
604
788
  _groupIndexMetaCache = new GroupIndexMetaCache();
605
789
  /** 消息序列号跟踪器(群消息 + P2P 空洞检测) */
790
+ _p2pPeerPullEnabled = true;
606
791
  _seqTracker = new SeqTracker();
607
792
  _seqTrackerContext = null;
608
793
  /** 惰性群同步:已同步过的 group_id 集合 */
@@ -685,6 +870,7 @@ export class AUNClient {
685
870
  _storage;
686
871
  _collab;
687
872
  _message;
873
+ _debug;
688
874
  _mail;
689
875
  _group;
690
876
  _stream;
@@ -784,7 +970,7 @@ export class AUNClient {
784
970
  dnsNet,
785
971
  });
786
972
  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);
973
+ this._transport.setServiceEventVerifier((event, payload) => this._verifyIncomingServiceEvent(event, payload), () => this._identity?.aid ?? '', () => this._serviceEventIdentityVersion);
788
974
  this._transport.setProtocolDisconnectCallback((err, closeCode) => this._handleTransportDisconnect(err, closeCode));
789
975
  this._transport._onForegroundDrained = () => this._delivery.onForegroundPullsDrained();
790
976
  this._transport._onRpcResponse = () => this._delivery.requestBackgroundPullRecovery();
@@ -817,7 +1003,8 @@ export class AUNClient {
817
1003
  this._dispatcher.subscribeProtocol('_raw.message.read_changed', (data) => this._onRawReadChanged(data, 'p2p'));
818
1004
  // V2 P2P 推送通知:收到通知后自动走 message.v2.pull 拉取并解密
819
1005
  this._dispatcher.subscribeProtocol('_raw.peer.v2.message_received', (data) => {
820
- const ns = this._aid ? `p2p:${this._aid}` : '';
1006
+ const sender = isJsonObject(data) ? String(data.sender_aid ?? '') : '';
1007
+ const ns = this._p2pPeerPullEnabled ? (sender ? this._delivery.peerNamespace(sender) : '') : this._aid ? `p2p:${this._aid}` : '';
821
1008
  const hasInline = isJsonObject(data)
822
1009
  && isJsonObject(data.inline_message);
823
1010
  const inlineGeneration = hasInline ? this._delivery.captureInlineAckGeneration() : undefined;
@@ -896,6 +1083,21 @@ export class AUNClient {
896
1083
  this._collab = new CollabClient(this);
897
1084
  return this._collab;
898
1085
  }
1086
+ get debug() {
1087
+ if (!this._debug)
1088
+ this._debug = new DebugFacade(this, async (operation) => {
1089
+ const transport = this._transport;
1090
+ const previous = transport.getTraceMode();
1091
+ transport.setTraceMode('diag');
1092
+ try {
1093
+ return await operation();
1094
+ }
1095
+ finally {
1096
+ transport.setTraceMode(previous);
1097
+ }
1098
+ });
1099
+ return this._debug;
1100
+ }
899
1101
  /** Message facade 入口 */
900
1102
  get message() {
901
1103
  if (!this._message)
@@ -974,6 +1176,7 @@ export class AUNClient {
974
1176
  }
975
1177
  async bindOwneraid(ownerAid) { return this._getOwnershipWorkflow().bindOwneraid(ownerAid); }
976
1178
  async bindSubaid(subAid) { return this._getOwnershipWorkflow().bindSubaid(subAid); }
1179
+ async resetOwnershipTask() { return this._getOwnershipWorkflow().resetOwnershipTask(); }
977
1180
  async releaseSubaid(subAid) { return this._getOwnershipWorkflow().releaseSubaid(subAid); }
978
1181
  async transferSubaid(subAid, newOwnerAid) { return this._getOwnershipWorkflow().transferSubaid(subAid, newOwnerAid); }
979
1182
  async checkOwnershipConsensus(subAid) { return this._getOwnershipWorkflow().checkOwnershipConsensus(subAid); }
@@ -1173,7 +1376,7 @@ export class AUNClient {
1173
1376
  dnsNet,
1174
1377
  });
1175
1378
  this._transport.setProtocolMetaObserver((meta) => this._observeRpcMeta(meta));
1176
- 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);
1379
+ this._transport.setServiceEventVerifier((event, payload) => this._verifyIncomingServiceEvent(event, payload), () => this._identity?.aid ?? '', () => this._serviceEventIdentityVersion);
1177
1380
  this._transport.setProtocolDisconnectCallback((err, closeCode) => this._handleTransportDisconnect(err, closeCode));
1178
1381
  this._transport._onForegroundDrained = () => this._delivery.onForegroundPullsDrained();
1179
1382
  this._transport._onRpcResponse = () => this._delivery.requestBackgroundPullRecovery();
@@ -1181,7 +1384,10 @@ export class AUNClient {
1181
1384
  loadIdentity(aid) {
1182
1385
  const previousAid = this._aid;
1183
1386
  this._identityRuntime.loadIdentity(aid);
1387
+ this._stopCredentialRenewal();
1184
1388
  this._serviceEventIdentityVersion++;
1389
+ this._serviceEventIdentityAbort.abort();
1390
+ this._serviceEventIdentityAbort = new AbortController();
1185
1391
  this._ensureReadStateIdentity();
1186
1392
  if (previousAid !== this._aid) {
1187
1393
  this._ownershipRecords.clear();
@@ -1289,6 +1495,7 @@ export class AUNClient {
1289
1495
  ? this._ensureReadStateIdentity() : null;
1290
1496
  try {
1291
1497
  const result = await this._rpcPipeline.call(method, params);
1498
+ await this._safeAsync(this._getCredentialRenewal().observe(method, params ?? {}, result));
1292
1499
  if (readIdentity !== null && readIdentity === this._ensureReadStateIdentity())
1293
1500
  this._observeReadStateResult(method, result, params ?? {});
1294
1501
  return result;
@@ -1318,6 +1525,13 @@ export class AUNClient {
1318
1525
  const groupId = String(params.group_id ?? params.groupId ?? params.group_aid ?? params.groupAid ?? fallback).trim();
1319
1526
  if (!groupId || !store)
1320
1527
  return false;
1528
+ const groupManager = store._agentMdManager ?? this._agentMdManager;
1529
+ const keystore = store._keystore;
1530
+ let pendingBind = keystore?.loadPendingGroupBind?.(`bind:${groupId}`);
1531
+ if (!pendingBind?.public_key_der_b64 || !pendingBind?.private_key_pem)
1532
+ pendingBind = keystore?.loadPendingGroupBind?.(groupId);
1533
+ if (groupManager.pendingUpload(fallback) && (!pendingBind?.public_key_der_b64 || !pendingBind?.private_key_pem))
1534
+ return false;
1321
1535
  try {
1322
1536
  await this._runGroupIdentityOperation(groupId, () => this.bindGroupAid({ group_id: groupId }, { aidStore: store }));
1323
1537
  }
@@ -1336,6 +1550,8 @@ export class AUNClient {
1336
1550
  if (!loaded.ok || !loaded.data?.aid?.isPrivateKeyValid())
1337
1551
  return false;
1338
1552
  const groupManager = store._agentMdManager ?? this._agentMdManager;
1553
+ if (groupManager.pendingUpload(groupAid))
1554
+ return false;
1339
1555
  if (String((groupManager._loadRecord?.(groupAid, true) ?? groupManager.loadRecord(groupAid))?.local_draft_content ?? ''))
1340
1556
  return false;
1341
1557
  await this._runGroupIdentityOperation(groupAid, () => this._uploadGroupAgentMd(store, groupAid, {}, {}));
@@ -1353,17 +1569,15 @@ export class AUNClient {
1353
1569
  const aid = this._aid;
1354
1570
  if (!aid || !this._currentAid)
1355
1571
  return false;
1356
- const observed = await this._getOwnershipFiles().readAgent(aid);
1357
- if (observed.exists || String(this._agentMdManager._loadRecord?.(aid, true)?.local_draft_content ?? this._agentMdManager.loadRecord(aid)?.local_draft_content ?? ''))
1572
+ const observed = await this._agentMdManager.readAgentMdFresh(aid);
1573
+ if (observed.exists || String(this._agentMdManager._loadRecord?.(aid, true)?.local_draft_content ?? this._agentMdManager.loadRecord(aid)?.local_draft_content ?? '') || this._agentMdManager.pendingUpload(aid))
1358
1574
  return false;
1359
1575
  let agentMd = buildDefaultAgentMd(aid, { type: this.getType() });
1360
1576
  try {
1361
1577
  agentMd = this._agentMdManager.readContent(aid) || agentMd;
1362
1578
  }
1363
1579
  catch { }
1364
- const payload = updateOwnershipAgentMd(agentMd, {});
1365
- const raw = signOwnershipFile(this._currentAid, payload.endsWith('\n') || payload.endsWith('\r') ? payload : `${payload}\n`, 'agent');
1366
- await this._agentMdManager.submitOwnershipRaw(aid, raw, { if_none_match: '*' });
1580
+ await this._agentMdManager.upload(agentMd, { if_none_match: '*' });
1367
1581
  return true;
1368
1582
  }
1369
1583
  async _checkIdentityAdmissionAfterConnect() {
@@ -1418,6 +1632,8 @@ export class AUNClient {
1418
1632
  return;
1419
1633
  }
1420
1634
  const groupManager = store._agentMdManager ?? this._agentMdManager;
1635
+ if (groupManager.pendingUpload(groupAid))
1636
+ return;
1421
1637
  if (String((groupManager._loadRecord?.(groupAid, true) ?? groupManager.loadRecord(groupAid))?.local_draft_content ?? '')) {
1422
1638
  await this._agentMdManager.check(groupAid);
1423
1639
  return;
@@ -1427,8 +1643,6 @@ export class AUNClient {
1427
1643
  await this.bindGroupAid({ group_id: groupId, group_aid: groupAid }, { aidStore: store });
1428
1644
  return;
1429
1645
  }
1430
- if (await this._resumeGroupAgentMdUpload(store, groupAid))
1431
- return;
1432
1646
  const checked = await this._agentMdManager.check(groupAid);
1433
1647
  if (!checked.remote_found) {
1434
1648
  await this._uploadGroupAgentMd(store, groupAid, {}, {});
@@ -1459,12 +1673,45 @@ export class AUNClient {
1459
1673
  this._groupIdentityOperations.delete(key);
1460
1674
  }
1461
1675
  }
1676
+ async _resumeOrgRequest(method, params, target) {
1677
+ const binding = params.request_binding;
1678
+ const now = Math.trunc(Date.now() / 1000);
1679
+ if (!isJsonObject(binding) || now < Number(binding.exp))
1680
+ return params;
1681
+ const credentials = isJsonObject(params.credentials) ? params.credentials : {};
1682
+ const credential = ['member_credential', 'approval_credential', 'audit_credential'].map((field) => credentials[field]).find(isJsonObject) ?? params.credential_template;
1683
+ const cid = isJsonObject(credential) ? String(credential.cid) : '';
1684
+ validateRequestBinding(binding, method, target, params, cid, String(params.nonce), String(params.request_id), Number(binding.iat));
1685
+ let exp = now + 300;
1686
+ for (const material of [credential, ...Object.values(credentials).filter((value) => isJsonObject(value) && value.ver === 'aun-cap/1'), ...[params, params.previous_owner, params.new_owner].filter(isJsonObject).flatMap((value) => ['membership_binding', 'credential_template', 'registration_authorization'].map((field) => value[field]))]) {
1687
+ if (isJsonObject(material)) {
1688
+ if (!Number.isInteger(material.exp) || Number(material.exp) <= now)
1689
+ throw new ValidationError('Org recovery authorization material has expired');
1690
+ exp = Math.min(exp, Number(material.exp));
1691
+ }
1692
+ }
1693
+ const refreshed = { ...params, request_binding: buildRequestBinding(method, target, params, cid, String(params.nonce), String(params.request_id), now, exp) };
1694
+ delete refreshed.client_signature;
1695
+ if (isJsonObject(credentials.representation_proof)) {
1696
+ if (!this._currentAid || !isJsonObject(credential))
1697
+ throw new ValidationError('Org recovery representation signer is unavailable');
1698
+ refreshed.credentials = { ...credentials, representation_proof: createRepresentationProof(credential, refreshed, refreshed.request_binding, this._currentAid, refreshed.proof_chain) };
1699
+ }
1700
+ return refreshed;
1701
+ }
1462
1702
  async createOrg(params = {}, options = {}) {
1463
1703
  const store = options.aidStore;
1464
1704
  const owner = this._currentAid;
1465
1705
  const orgAid = String(params.org_id ?? '').trim();
1466
1706
  if (!store || !owner?.isPrivateKeyValid() || !orgAid)
1467
1707
  throw new ValidationError('createOrg requires aidStore, org_id and current owner identity');
1708
+ const tokens = store._tokenStore;
1709
+ const inputHash = crypto.createHash('sha256').update(canonicalJson(params)).digest('hex');
1710
+ let checkpoint = (typeof tokens._loadMetadataStrict === 'function' ? tokens._loadMetadataStrict(orgAid) : tokens.loadMetadata(orgAid))?.org_identity_creation;
1711
+ if (checkpoint != null && !isJsonObject(checkpoint))
1712
+ throw ownershipFailure('persistence_unavailable', 'Org creation checkpoint is invalid');
1713
+ if (checkpoint && (checkpoint.owner_aid !== owner.aid || checkpoint.input_hash !== inputHash))
1714
+ throw ownershipFailure('conflict', 'pending Org creation context changed');
1468
1715
  this._aidStore = store;
1469
1716
  let identity = store.load(orgAid);
1470
1717
  if (!identity.ok) {
@@ -1476,18 +1723,13 @@ export class AUNClient {
1476
1723
  if (!identity.ok)
1477
1724
  throw ownershipFailure('unavailable', identity.error.message, true);
1478
1725
  const target = options.serviceAid ?? `org.${orgAid.slice(orgAid.indexOf('.') + 1)}`;
1479
- const tokens = store._tokenStore;
1480
- const inputHash = crypto.createHash('sha256').update(canonicalJson(params)).digest('hex');
1481
- let checkpoint = tokens.loadMetadata(orgAid)?.org_identity_creation;
1482
- if (checkpoint && (checkpoint.owner_aid !== owner.aid || checkpoint.input_hash !== inputHash))
1483
- throw ownershipFailure('conflict', 'pending Org creation context changed');
1484
1726
  if (!checkpoint) {
1485
1727
  const prepared = this.org.prepareRequest('org.lifecycle.create', { ...params, owner_aid: owner.aid }, target, owner, { orgSigner: identity.data.aid });
1486
1728
  checkpoint = { owner_aid: owner.aid, input_hash: inputHash, source_params: params, prepared, phase: 'create' };
1487
1729
  tokens.saveMetadata(orgAid, { org_identity_creation: checkpoint });
1488
1730
  }
1489
1731
  if (checkpoint.phase === 'create') {
1490
- const result = await this.call('org.lifecycle.create', checkpoint.prepared);
1732
+ const result = await this.call('org.lifecycle.create', await this._resumeOrgRequest('org.lifecycle.create', checkpoint.prepared, target));
1491
1733
  checkpoint = { ...checkpoint, result, phase: 'upload' };
1492
1734
  tokens.saveMetadata(orgAid, { org_identity_creation: checkpoint });
1493
1735
  }
@@ -1501,9 +1743,11 @@ export class AUNClient {
1501
1743
  if (typeof keyStore?.listIdentities !== 'function' || typeof tokens?.loadMetadata !== 'function')
1502
1744
  return;
1503
1745
  for (const aid of keyStore.listIdentities()) {
1504
- const metadata = tokens.loadMetadata(aid);
1746
+ const metadata = typeof tokens._loadMetadataStrict === 'function' ? tokens._loadMetadataStrict(aid) : tokens.loadMetadata(aid);
1505
1747
  const creation = metadata?.org_identity_creation;
1506
1748
  const transfer = metadata?.org_identity_transfer;
1749
+ if (creation != null && !isJsonObject(creation) || transfer != null && !isJsonObject(transfer))
1750
+ throw ownershipFailure('persistence_unavailable', 'Org identity checkpoint is invalid');
1507
1751
  try {
1508
1752
  if (creation?.owner_aid === this._aid) {
1509
1753
  await this.createOrg(creation.source_params, { aidStore: store });
@@ -1513,21 +1757,46 @@ export class AUNClient {
1513
1757
  await this.completeOrgTransfer(transfer.source_params, { aidStore: store });
1514
1758
  continue;
1515
1759
  }
1516
- const orgManager = store._agentMdManager;
1517
- if (String((orgManager?._loadRecord?.(aid, true) ?? orgManager?.loadRecord?.(aid))?.local_draft_content ?? ''))
1518
- continue;
1519
- const pending = store._agentMdManager.pendingUpload(aid);
1520
- if (!pending)
1521
- continue;
1522
- const fields = validateAgentMdDocument(pending.content, { expectedAid: aid }).frontmatter;
1523
- if (fields.type === 'org' && fields.owner === this._aid)
1524
- await this._resumeGroupAgentMdUpload(store, aid);
1525
1760
  }
1526
1761
  catch (error) {
1527
1762
  this._clientLog.warn(`post-connect Org identity recovery failed: aid=${aid} err=${formatCaughtError(error)}`);
1528
1763
  }
1529
1764
  }
1530
1765
  }
1766
+ async _verifyOrgTransferMaterials(params, ownerAid) {
1767
+ const orgAid = String(params.org_id);
1768
+ const previous = params.previous_owner;
1769
+ const next = params.new_owner;
1770
+ const binding = previous.membership_binding;
1771
+ const nextBinding = next.membership_binding;
1772
+ const credential = previous.credential_template;
1773
+ const ext = isJsonObject(credential.ext) ? credential.ext : {};
1774
+ const capability = Array.isArray(credential.cap) && credential.cap.length === 1 && isJsonObject(credential.cap[0]) ? credential.cap[0] : {};
1775
+ const now = Math.trunc(Date.now() / 1000);
1776
+ if (binding.org_id !== orgAid || nextBinding.org_id !== orgAid || binding.member_aid === ownerAid || nextBinding.member_aid !== ownerAid
1777
+ || binding.role !== 'member' || nextBinding.role !== 'owner' || digest(previous.terms) !== binding.terms_hash || digest(next.terms) !== nextBinding.terms_hash
1778
+ || !Number.isSafeInteger(nextBinding.iat) || !Number.isSafeInteger(nextBinding.exp) || Number(nextBinding.iat) > now || Number(nextBinding.exp) <= now
1779
+ || credential.iss !== orgAid || credential.sub !== binding.member_aid || credential.parent != null || credential.delegate !== false
1780
+ || ext.credential_type !== 'member' || ext.bound_to_membership !== true || ext.org_id !== orgAid || ext.membership_id !== binding.membership_id
1781
+ || ext.binding_version !== binding.binding_version || ext.membership_terms_hash !== binding.terms_hash
1782
+ || capability.ns !== 'org' || capability.action !== 'role.member' || capability.resource !== orgAid)
1783
+ throw ownershipFailure('invalid', 'Org transfer membership materials do not match');
1784
+ validateMembershipBinding(binding);
1785
+ const certificates = new Map();
1786
+ for (const [aid, signature] of [[binding.member_aid, binding.member_signature], [orgAid, binding.org_signature], [orgAid, credential.signature]]) {
1787
+ if (!isJsonObject(signature))
1788
+ throw ownershipFailure('invalid', 'Org transfer membership materials do not match');
1789
+ const fingerprint = String(signature.cert_fingerprint);
1790
+ const certificate = await this._auth.resolveOwnershipCertificate(await this._resolveOwnershipCertificateGateway(String(aid)), String(aid), fingerprint);
1791
+ certificates.set(fingerprint, certificate.cert_pem);
1792
+ }
1793
+ const resolve = (fingerprint) => certificates.get(fingerprint);
1794
+ const checkedAt = Math.trunc(Date.now() / 1000);
1795
+ verifyMembershipBinding(binding, resolve, checkedAt, orgAid);
1796
+ verifyCredentialSignature(credential, resolve, checkedAt, orgAid);
1797
+ if (Number(nextBinding.exp) <= checkedAt)
1798
+ throw ownershipFailure('invalid', 'Org transfer membership materials do not match');
1799
+ }
1531
1800
  async completeOrgTransfer(params = {}, options = {}) {
1532
1801
  const store = options.aidStore;
1533
1802
  const owner = this._currentAid;
@@ -1545,9 +1814,13 @@ export class AUNClient {
1545
1814
  const tokenStore = store._tokenStore;
1546
1815
  const pendingKey = `org-transfer:${orgAid}:${transferId}`;
1547
1816
  const inputHash = crypto.createHash('sha256').update(canonicalJson(params)).digest('hex');
1548
- let pending = tokenStore.loadMetadata(orgAid)?.org_identity_transfer;
1817
+ let pending = (typeof tokenStore._loadMetadataStrict === 'function' ? tokenStore._loadMetadataStrict(orgAid) : tokenStore.loadMetadata(orgAid))?.org_identity_transfer;
1818
+ if (pending != null && !isJsonObject(pending))
1819
+ throw ownershipFailure('persistence_unavailable', 'Org transfer checkpoint is invalid');
1549
1820
  if (pending && (pending.transfer_id !== transferId || pending.owner_aid !== owner.aid || pending.input_hash !== inputHash))
1550
1821
  throw ownershipFailure('conflict', 'pending Org transfer context changed');
1822
+ if (!pending)
1823
+ await this._verifyOrgTransferMaterials(params, owner.aid);
1551
1824
  let keyPair = localStore.loadPendingGroupBind(pendingKey);
1552
1825
  if (!keyPair) {
1553
1826
  if (pending)
@@ -1568,7 +1841,7 @@ export class AUNClient {
1568
1841
  tokenStore.saveMetadata(orgAid, { org_identity_transfer: pending });
1569
1842
  }
1570
1843
  if (pending.phase === 'rekey') {
1571
- const result = await this.call('org.lifecycle.transferAccept', pending.rekey_params);
1844
+ const result = await this.call('org.lifecycle.transferAccept', await this._resumeOrgRequest('org.lifecycle.transferAccept', pending.rekey_params, target));
1572
1845
  pending = { ...pending, phase: 'finalize', rekey_result: result };
1573
1846
  tokenStore.saveMetadata(orgAid, { org_identity_transfer: pending });
1574
1847
  }
@@ -1604,7 +1877,7 @@ export class AUNClient {
1604
1877
  pending = { ...pending, finalize_params: this.org.prepareRequest('org.lifecycle.transferAccept', final, target, owner, { orgSigner: orgIdentity }) };
1605
1878
  tokenStore.saveMetadata(orgAid, { org_identity_transfer: pending });
1606
1879
  }
1607
- const result = await this.call('org.lifecycle.transferAccept', pending.finalize_params);
1880
+ const result = await this.call('org.lifecycle.transferAccept', await this._resumeOrgRequest('org.lifecycle.transferAccept', pending.finalize_params, target));
1608
1881
  await this._uploadOrgAgentMd(store, orgAid, pending.source_params);
1609
1882
  tokenStore.saveMetadata(orgAid, { org_identity_transfer: null });
1610
1883
  localStore.clearPendingGroupBind(pendingKey);
@@ -2663,7 +2936,10 @@ export class AUNClient {
2663
2936
  if (event === 'message.received') {
2664
2937
  return this._handleOwnershipMessage(payload).then((handled) => {
2665
2938
  if (!handled)
2666
- return this._delivery.publishAppEvent(event, payload, source, deliveryContext);
2939
+ return this._handleCredentialRenewal(payload).then((renewed) => {
2940
+ if (!renewed)
2941
+ return this._delivery.publishAppEvent(event, payload, source, deliveryContext);
2942
+ });
2667
2943
  return undefined;
2668
2944
  });
2669
2945
  }
@@ -3094,22 +3370,36 @@ export class AUNClient {
3094
3370
  this._clientLog.debug(`_fetchPeerCert exit: elapsed=${Date.now() - tStart}ms aid=${aid} (cache_hit)`);
3095
3371
  return cached.certPem;
3096
3372
  }
3097
- const gatewayUrl = this._gatewayUrl;
3098
- if (!gatewayUrl) {
3099
- throw new ValidationError('gateway url unavailable for e2ee cert fetch');
3373
+ const inFlightKey = `${cacheKey}:timeout:${timeoutMs}`;
3374
+ const pending = this._certFetchInFlight.get(inFlightKey);
3375
+ if (pending)
3376
+ return await pending;
3377
+ const request = (async () => {
3378
+ const gatewayUrl = this._gatewayUrl;
3379
+ if (!gatewayUrl) {
3380
+ throw new ValidationError('gateway url unavailable for e2ee cert fetch');
3381
+ }
3382
+ const peerGatewayUrl = AUNClient._resolvePeerGatewayUrl(gatewayUrl, aid);
3383
+ const certPem = await _httpGetText(AUNClient._buildCertUrl(peerGatewayUrl, aid, certFingerprint), this._configModel.verifySsl, timeoutMs);
3384
+ const validated = await this._validateAndCachePeerCert({
3385
+ aid,
3386
+ certPem,
3387
+ certFingerprint,
3388
+ source: 'fetch',
3389
+ signedAt,
3390
+ agentMdRead,
3391
+ });
3392
+ this._clientLog.debug(`_fetchPeerCert exit: elapsed=${Date.now() - tStart}ms aid=${aid} (fetched)`);
3393
+ return validated;
3394
+ })();
3395
+ this._certFetchInFlight.set(inFlightKey, request);
3396
+ try {
3397
+ return await request;
3398
+ }
3399
+ finally {
3400
+ if (this._certFetchInFlight.get(inFlightKey) === request)
3401
+ this._certFetchInFlight.delete(inFlightKey);
3100
3402
  }
3101
- const peerGatewayUrl = AUNClient._resolvePeerGatewayUrl(gatewayUrl, aid);
3102
- const certPem = await _httpGetText(AUNClient._buildCertUrl(peerGatewayUrl, aid, certFingerprint), this._configModel.verifySsl, timeoutMs);
3103
- const validated = await this._validateAndCachePeerCert({
3104
- aid,
3105
- certPem,
3106
- certFingerprint,
3107
- source: 'fetch',
3108
- signedAt,
3109
- agentMdRead,
3110
- });
3111
- this._clientLog.debug(`_fetchPeerCert exit: elapsed=${Date.now() - tStart}ms aid=${aid} (fetched)`);
3112
- return validated;
3113
3403
  }
3114
3404
  catch (err) {
3115
3405
  this._clientLog.debug(`_fetchPeerCert exit (error): elapsed=${Date.now() - tStart}ms aid=${aid} err=${err instanceof Error ? err.message : String(err)}`);
@@ -3685,7 +3975,7 @@ export class AUNClient {
3685
3975
  }
3686
3976
  /** 后台 Promise 统一兜底,避免事件回调里的异步异常变成未处理拒绝。 */
3687
3977
  _safeAsync(promise) {
3688
- promise.catch((exc) => {
3978
+ return promise.then(() => undefined).catch((exc) => {
3689
3979
  this._clientLog.warn(`background task exception: ${formatCaughtError(exc)}`);
3690
3980
  });
3691
3981
  }
@@ -3880,7 +4170,7 @@ export class AUNClient {
3880
4170
  return await this._v2E2EE.ackGroupV2(groupId, upToSeq, groupAid);
3881
4171
  }
3882
4172
  /** 解密单条 V2 pull 消息。缺 sender IK 时先入 pending,后台补齐后重试。 */
3883
- async _decryptV2Message(msg, allowPending = true, emitUndecryptable = true, rotateKeys = true, observeAgentMd = true, deferStatus, deferKeyFetch = false, source = 'pull') {
4173
+ async _decryptV2Message(msg, allowPending = true, emitUndecryptable = true, rotateKeys = true, observeAgentMd = true, deferStatus, deferKeyFetch = false, source = 'pull', deferredEffects) {
3884
4174
  const session = this._v2Session;
3885
4175
  if (!session)
3886
4176
  return null;
@@ -3896,8 +4186,12 @@ export class AUNClient {
3896
4186
  return null;
3897
4187
  }
3898
4188
  const e2eeMeta = this._v2E2eeMeta(envelope);
3899
- if (observeAgentMd)
3900
- this._agentMdManager.observeEnvelope(envelope);
4189
+ if (observeAgentMd) {
4190
+ if (deferredEffects)
4191
+ deferredEffects.push(() => this._agentMdManager.observeEnvelope(envelope));
4192
+ else
4193
+ this._agentMdManager.observeEnvelope(envelope);
4194
+ }
3901
4195
  let spkId = '';
3902
4196
  let recipientKeySource = '';
3903
4197
  if (isJsonObject(envelope.recipient)) {
@@ -3976,8 +4270,12 @@ export class AUNClient {
3976
4270
  };
3977
4271
  this._attachV2EnvelopeMetadata(event, e2eeMeta);
3978
4272
  this._logMessageDebug('decrypt-fail', 'v2.decrypt', undecryptableEvent, event);
3979
- if (emitUndecryptable)
3980
- this._dispatcher.enqueue(undecryptableEvent, event);
4273
+ if (emitUndecryptable) {
4274
+ if (deferredEffects)
4275
+ deferredEffects.push(() => { this._dispatcher.enqueue(undecryptableEvent, event); });
4276
+ else
4277
+ this._dispatcher.enqueue(undecryptableEvent, event);
4278
+ }
3981
4279
  return null;
3982
4280
  }
3983
4281
  this._clientLog.debug(`V2 decrypt key lookup ok: seq=${String(msg.seq ?? '')}, group=${groupIdForKeys || '<p2p>'}, ik_len=${ikPriv.byteLength}, spk_len=${spkPriv?.byteLength ?? 0}`);
@@ -4015,8 +4313,12 @@ export class AUNClient {
4015
4313
  };
4016
4314
  this._attachV2EnvelopeMetadata(event, e2eeMeta);
4017
4315
  this._logMessageDebug('decrypt-fail', 'v2.decrypt', undecryptableEvent, event);
4018
- if (emitUndecryptable)
4019
- this._dispatcher.enqueue(undecryptableEvent, event);
4316
+ if (emitUndecryptable) {
4317
+ if (deferredEffects)
4318
+ deferredEffects.push(() => { this._dispatcher.enqueue(undecryptableEvent, event); });
4319
+ else
4320
+ this._dispatcher.enqueue(undecryptableEvent, event);
4321
+ }
4020
4322
  return null;
4021
4323
  }
4022
4324
  let plaintext;
@@ -4044,16 +4346,24 @@ export class AUNClient {
4044
4346
  };
4045
4347
  this._attachV2EnvelopeMetadata(event, e2eeMeta);
4046
4348
  this._logMessageDebug('decrypt-fail', 'v2.decrypt', undecryptableEvent, event);
4047
- if (emitUndecryptable)
4048
- this._dispatcher.enqueue(undecryptableEvent, event);
4349
+ if (emitUndecryptable) {
4350
+ if (deferredEffects)
4351
+ deferredEffects.push(() => { this._dispatcher.enqueue(undecryptableEvent, event); });
4352
+ else
4353
+ this._dispatcher.enqueue(undecryptableEvent, event);
4354
+ }
4049
4355
  return null;
4050
4356
  }
4051
4357
  if (plaintext === null) {
4052
4358
  this._clientLog.debug(`V2 decrypt returned null plaintext: seq=${String(msg.seq ?? '')}, group=${groupIdForKeys || '<p2p>'}`);
4053
4359
  return null;
4054
4360
  }
4055
- if (rotateKeys)
4056
- this._scheduleV2KeyRotationForMessage(msg, envelope);
4361
+ if (rotateKeys) {
4362
+ if (deferredEffects)
4363
+ deferredEffects.push(() => this._scheduleV2KeyRotationForMessage(msg, envelope));
4364
+ else
4365
+ this._scheduleV2KeyRotationForMessage(msg, envelope);
4366
+ }
4057
4367
  const e2ee = this._v2E2eeMeta(envelope);
4058
4368
  let result = {
4059
4369
  message_id: String(msg.message_id ?? ''),
@@ -4149,6 +4459,8 @@ export class AUNClient {
4149
4459
  if (protectedHeaders && Object.keys(protectedHeaders).length > 0) {
4150
4460
  meta.protected_headers = protectedHeaders;
4151
4461
  }
4462
+ if (typeof envelope.need_render === 'boolean')
4463
+ meta.need_render = envelope.need_render;
4152
4464
  const payloadType = String(envelope.payload_type ?? protectedHeaders?.payload_type ?? '').trim();
4153
4465
  if (payloadType) {
4154
4466
  meta.payload_type = payloadType;
@@ -4164,6 +4476,8 @@ export class AUNClient {
4164
4476
  return meta;
4165
4477
  }
4166
4478
  _attachV2EnvelopeMetadata(message, meta) {
4479
+ if (typeof meta.need_render === 'boolean')
4480
+ message.need_render = meta.need_render;
4167
4481
  const payloadType = typeof meta.payload_type === 'string' ? meta.payload_type.trim() : '';
4168
4482
  if (payloadType)
4169
4483
  message.payload_type = payloadType;
@@ -4521,6 +4835,7 @@ export class AUNClient {
4521
4835
  // ── 内部:后台任务 ────────────────────────────────────────
4522
4836
  /** 启动所有后台任务 */
4523
4837
  _startBackgroundTasks() {
4838
+ this._startCredentialRenewal();
4524
4839
  // WebSocket Ping/Pong 由 Gateway 与传输层处理;SDK 不发送 RPC 心跳。
4525
4840
  // 短连接生命周期短,不需要 token 刷新。
4526
4841
  if (this._sessionOptions.connection_kind !== 'short') {
@@ -4530,6 +4845,7 @@ export class AUNClient {
4530
4845
  }
4531
4846
  /** 停止所有后台任务 */
4532
4847
  _stopBackgroundTasks() {
4848
+ this._stopCredentialRenewal();
4533
4849
  if (this._heartbeatTimer !== null) {
4534
4850
  clearInterval(this._heartbeatTimer);
4535
4851
  this._heartbeatTimer = null;