@agentunion/fastaun 0.5.18 → 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 (94) hide show
  1. package/CHANGELOG.md +44 -2
  2. package/_packed_docs/CHANGELOG.md +44 -2
  3. package/_packed_docs/INDEX.md +136 -14
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +42 -16
  5. package/_packed_docs/agent.md/SCHEMA.md +245 -205
  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 +42 -15
  7. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +4 -4
  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 +45 -134
  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 +5 -20
  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 +22 -27
  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 +250 -283
  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 +74 -64
  18. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +25 -5
  19. package/_packed_docs/sdk/INDEX.md +76 -17
  20. package/dist/agent-md-schema.d.ts +2 -0
  21. package/dist/agent-md-schema.js +45 -10
  22. package/dist/agent-md-schema.js.map +1 -1
  23. package/dist/agent-md.d.ts +47 -2
  24. package/dist/agent-md.js +493 -33
  25. package/dist/agent-md.js.map +1 -1
  26. package/dist/aid-store.d.ts +9 -59
  27. package/dist/aid-store.js +135 -245
  28. package/dist/aid-store.js.map +1 -1
  29. package/dist/aid.js +19 -5
  30. package/dist/aid.js.map +1 -1
  31. package/dist/auth.d.ts +6 -1
  32. package/dist/auth.js +428 -15
  33. package/dist/auth.js.map +1 -1
  34. package/dist/cert-utils.d.ts +1 -1
  35. package/dist/cert-utils.js +19 -7
  36. package/dist/cert-utils.js.map +1 -1
  37. package/dist/client/delivery.d.ts +2 -1
  38. package/dist/client/delivery.js +143 -52
  39. package/dist/client/delivery.js.map +1 -1
  40. package/dist/client/rpc-pipeline.js +22 -5
  41. package/dist/client/rpc-pipeline.js.map +1 -1
  42. package/dist/client/v2-e2ee.d.ts +1 -0
  43. package/dist/client/v2-e2ee.js +93 -29
  44. package/dist/client/v2-e2ee.js.map +1 -1
  45. package/dist/client.d.ts +84 -1
  46. package/dist/client.js +749 -28
  47. package/dist/client.js.map +1 -1
  48. package/dist/errors.js +4 -3
  49. package/dist/errors.js.map +1 -1
  50. package/dist/events.d.ts +5 -1
  51. package/dist/events.js +47 -6
  52. package/dist/events.js.map +1 -1
  53. package/dist/facades.d.ts +83 -3
  54. package/dist/facades.js +98 -4
  55. package/dist/facades.js.map +1 -1
  56. package/dist/index.d.ts +8 -4
  57. package/dist/index.js +3 -2
  58. package/dist/index.js.map +1 -1
  59. package/dist/keystore/aid-db.d.ts +5 -0
  60. package/dist/keystore/aid-db.js +48 -0
  61. package/dist/keystore/aid-db.js.map +1 -1
  62. package/dist/keystore/index.d.ts +8 -0
  63. package/dist/keystore/local-identity-store.js +10 -3
  64. package/dist/keystore/local-identity-store.js.map +1 -1
  65. package/dist/keystore/local-token-store.d.ts +6 -1
  66. package/dist/keystore/local-token-store.js +24 -1
  67. package/dist/keystore/local-token-store.js.map +1 -1
  68. package/dist/org-protocol.d.ts +98 -0
  69. package/dist/org-protocol.js +1689 -0
  70. package/dist/org-protocol.js.map +1 -0
  71. package/dist/ownership-proof.d.ts +82 -5
  72. package/dist/ownership-proof.js +327 -31
  73. package/dist/ownership-proof.js.map +1 -1
  74. package/dist/ownership-v21.d.ts +178 -0
  75. package/dist/ownership-v21.js +635 -0
  76. package/dist/ownership-v21.js.map +1 -0
  77. package/dist/ownership-workflow.d.ts +82 -0
  78. package/dist/ownership-workflow.js +872 -0
  79. package/dist/ownership-workflow.js.map +1 -0
  80. package/dist/seq-tracker.d.ts +1 -0
  81. package/dist/seq-tracker.js +10 -1
  82. package/dist/seq-tracker.js.map +1 -1
  83. package/dist/tools/cross-sdk-agent.d.ts +89 -1
  84. package/dist/tools/cross-sdk-agent.js +338 -7
  85. package/dist/tools/cross-sdk-agent.js.map +1 -1
  86. package/dist/transport.d.ts +7 -0
  87. package/dist/transport.js +108 -4
  88. package/dist/transport.js.map +1 -1
  89. package/dist/v2/e2ee/decrypt.d.ts +1 -0
  90. package/dist/v2/e2ee/decrypt.js +9 -1
  91. package/dist/v2/e2ee/decrypt.js.map +1 -1
  92. package/dist/version.d.ts +1 -1
  93. package/dist/version.js +1 -1
  94. 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 } 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
  }
@@ -342,6 +347,9 @@ function createAgentMdManagerForRuntime(opts) {
342
347
  logger: opts.logger(),
343
348
  ownerAidGetter: opts.ownerAid,
344
349
  currentAidGetter: opts.currentAid,
350
+ entityOwnerResolver: opts.resolveEntityOwner,
351
+ entityIdentityGetter: opts.fetchEntityIdentity,
352
+ entitySignerResolver: opts.fetchEntitySigner,
345
353
  gatewayResolver: async (aid) => {
346
354
  const gatewayUrl = await opts.gateway.resolve(aid);
347
355
  opts.gateway.set(gatewayUrl);
@@ -388,7 +396,47 @@ function createAgentMdManagerForRuntime(opts) {
388
396
  opts.identity.set(identity);
389
397
  return token;
390
398
  },
391
- 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) => {
392
440
  const target = String(aid ?? '').trim();
393
441
  const expectedFp = String(certFingerprint ?? '').trim().toLowerCase();
394
442
  const current = opts.currentAid();
@@ -402,7 +450,7 @@ function createAgentMdManagerForRuntime(opts) {
402
450
  }
403
451
  catch { /* best effort before cert fetch */ }
404
452
  }
405
- const certPem = String(await opts.fetchPeerCert(target, expectedFp || undefined) ?? '').trim();
453
+ const certPem = String(await opts.fetchPeerCert(target, expectedFp || undefined, signedAt) ?? '').trim();
406
454
  if (!certPem)
407
455
  throw new NotFoundError(`certificate not found for aid: ${target}`);
408
456
  return AID._create({
@@ -429,6 +477,90 @@ export class AUNClient {
429
477
  _state = 'no_identity';
430
478
  /** 当前 AID 值对象(新 API) */
431
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
+ }
432
564
  /** 实例级 protected_headers */
433
565
  _instanceProtectedHeaders = null;
434
566
  /** 重连退避时间戳(ms) */
@@ -451,6 +583,7 @@ export class AUNClient {
451
583
  _transport;
452
584
  /** 认证流程 */
453
585
  _auth;
586
+ resolveCredentialService;
454
587
  /** 密钥存储 */
455
588
  _tokenStore;
456
589
  /** 会话参数(重连用) */
@@ -503,6 +636,10 @@ export class AUNClient {
503
636
  _v2SenderIKPending = new Map();
504
637
  /** sender IK 后台补齐任务去重。 */
505
638
  _v2SenderIKFetching = new Set();
639
+ _readStateCache = new Map();
640
+ _readStateQueries = new Map();
641
+ _readStateIdentity = null;
642
+ _readStateGeneration = 0;
506
643
  // ── 后台任务定时器 ──────────────────────────────────────────
507
644
  _heartbeatTimer = null;
508
645
  _tokenRefreshTimer = null;
@@ -551,6 +688,8 @@ export class AUNClient {
551
688
  _mail;
552
689
  _group;
553
690
  _stream;
691
+ _credential;
692
+ _org;
554
693
  constructor(aid) {
555
694
  if (aid !== null && aid !== undefined && !isAIDObject(aid)) {
556
695
  throw new ValidationError('AUNClient only accepts an AID object or no argument');
@@ -586,7 +725,7 @@ export class AUNClient {
586
725
  if (debugFlag) {
587
726
  this._clientLog.info(`AUNClient initialized (debug=true, aunPath=${this._configModel.aunPath})`);
588
727
  }
589
- this._dispatcher = new EventDispatcher(this._logger.for('aun_core.events'));
728
+ this._dispatcher = new EventDispatcher(this._logger.for('aun_core.events'), () => this._identity?.aid ?? '');
590
729
  const dnsNet = new DnsResilientNet({
591
730
  verifySsl: this._configModel.verifySsl,
592
731
  logger: this._clientLog,
@@ -596,6 +735,9 @@ export class AUNClient {
596
735
  logger: this._logger.for('aun_core.keystore'),
597
736
  });
598
737
  this._tokenStore = tokenStore;
738
+ this._ownershipStorage = null;
739
+ this._ownershipFiles = null;
740
+ this._ownershipWorkflow = null;
599
741
  this._slotId = inputAid?.slotId || 'default';
600
742
  this._connectDeliveryMode = normalizeDeliveryModeConfig({ mode: 'fanout' });
601
743
  this._defaultConnectDeliveryMode = { ...this._connectDeliveryMode };
@@ -611,6 +753,7 @@ export class AUNClient {
611
753
  net: dnsNet,
612
754
  });
613
755
  this._aid = initAid;
756
+ this._restoreOwnershipRecords();
614
757
  this._agentMdManager = createAgentMdManagerForRuntime({
615
758
  config: () => this._configModel,
616
759
  logger: () => this._logger.for('aun_core.agent_md'),
@@ -628,7 +771,10 @@ export class AUNClient {
628
771
  },
629
772
  auth: () => this._auth,
630
773
  tokenStore: () => this._tokenStore,
631
- 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),
632
778
  });
633
779
  this._transport = new RPCTransport({
634
780
  eventDispatcher: this._dispatcher,
@@ -638,7 +784,9 @@ export class AUNClient {
638
784
  dnsNet,
639
785
  });
640
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);
641
788
  this._transport.setProtocolDisconnectCallback((err, closeCode) => this._handleTransportDisconnect(err, closeCode));
789
+ this._transport._onForegroundDrained = () => this._delivery.onForegroundPullsDrained();
642
790
  this._runtime = new ClientRuntime(this);
643
791
  this._identityRuntime = new IdentityRuntimeManager(this._runtime);
644
792
  this._peerDirectory = new PeerDirectory(this._runtime);
@@ -665,6 +813,7 @@ export class AUNClient {
665
813
  this._dispatcher.subscribeProtocol('_raw.message.received', (data) => this._onRawMessageReceived(data));
666
814
  // P2P 消息撤回推送:在线 push 与 pull tombstone 去重后再发布给应用层
667
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'));
668
817
  // V2 P2P 推送通知:收到通知后自动走 message.v2.pull 拉取并解密
669
818
  this._dispatcher.subscribeProtocol('_raw.peer.v2.message_received', (data) => {
670
819
  const ns = this._aid ? `p2p:${this._aid}` : '';
@@ -692,6 +841,7 @@ export class AUNClient {
692
841
  this._dispatcher.subscribeProtocol('_raw.group.message_created', (data) => this._onRawGroupMessageCreated(data));
693
842
  // 群组消息撤回推送:在线 push 通道,与 pull 双 tombstone 兜底互补(SDK 去重只回调一次)
694
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'));
695
845
  // 群组变更事件:透传并触发 V2 state/SPK 维护
696
846
  this._dispatcher.subscribeProtocol('_raw.group.changed', (data) => this._onRawGroupChanged(data));
697
847
  // 定向邀请底层事件:按群 + 目标 AID 聚合后向应用层只提示一次。
@@ -724,6 +874,16 @@ export class AUNClient {
724
874
  return sessionToken || null;
725
875
  }
726
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
+ }
727
887
  get storage() {
728
888
  if (!this._storage)
729
889
  this._storage = new StorageVFS(this);
@@ -757,6 +917,88 @@ export class AUNClient {
757
917
  get currentAid() {
758
918
  return this._currentAid;
759
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
+ }
760
1002
  get hasIdentity() {
761
1003
  return this._currentAid !== null && this.state !== ConnectionState.CLOSED;
762
1004
  }
@@ -886,6 +1128,9 @@ export class AUNClient {
886
1128
  logger: this._logger.for('aun_core.keystore'),
887
1129
  });
888
1130
  this._tokenStore = tokenStore;
1131
+ this._ownershipStorage = null;
1132
+ this._ownershipFiles = null;
1133
+ this._ownershipWorkflow = null;
889
1134
  this._auth = new AuthFlow({
890
1135
  tokenStore,
891
1136
  crypto: new CryptoProvider(),
@@ -914,7 +1159,10 @@ export class AUNClient {
914
1159
  },
915
1160
  auth: () => this._auth,
916
1161
  tokenStore: () => this._tokenStore,
917
- 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),
918
1166
  });
919
1167
  this._transport = new RPCTransport({
920
1168
  eventDispatcher: this._dispatcher,
@@ -924,10 +1172,20 @@ export class AUNClient {
924
1172
  dnsNet,
925
1173
  });
926
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);
927
1176
  this._transport.setProtocolDisconnectCallback((err, closeCode) => this._handleTransportDisconnect(err, closeCode));
1177
+ this._transport._onForegroundDrained = () => this._delivery.onForegroundPullsDrained();
928
1178
  }
929
1179
  loadIdentity(aid) {
1180
+ const previousAid = this._aid;
930
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();
931
1189
  }
932
1190
  setProtectedHeaders(headers) {
933
1191
  if (!headers) {
@@ -1006,7 +1264,7 @@ export class AUNClient {
1006
1264
  }
1007
1265
  /** 连接到 Gateway;身份来自构造函数或 loadIdentity(aid),认证由 SDK 内部自动完成。 */
1008
1266
  async connect(opts) {
1009
- return this._lifecycle.connect(opts);
1267
+ await this._lifecycle.connect(opts);
1010
1268
  }
1011
1269
  /** 关闭连接 */
1012
1270
  async close() {
@@ -1025,8 +1283,13 @@ export class AUNClient {
1025
1283
  * 自动处理内部方法限制、E2EE 加解密、客户端签名等。
1026
1284
  */
1027
1285
  async call(method, params) {
1286
+ const readIdentity = ['message.read', 'message.read_state', 'group.read', 'group.read_state'].includes(method)
1287
+ ? this._ensureReadStateIdentity() : null;
1028
1288
  try {
1029
- 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;
1030
1293
  }
1031
1294
  catch (err) {
1032
1295
  await this._maybeHandleHalfOpenRpcFailure(err);
@@ -1079,12 +1342,23 @@ export class AUNClient {
1079
1342
  const target = message.slice(agentPrefix.length).trim();
1080
1343
  if (!target || target.toLowerCase() !== String(this._aid ?? '').trim().toLowerCase())
1081
1344
  return false;
1082
- 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() });
1083
1355
  try {
1084
- agentMd = this._agentMdManager.readContent(target);
1356
+ agentMd = this._agentMdManager.readContent(aid) || agentMd;
1085
1357
  }
1086
1358
  catch { }
1087
- 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: '*' });
1088
1362
  return true;
1089
1363
  }
1090
1364
  async _checkIdentityAdmissionAfterConnect() {
@@ -1092,10 +1366,12 @@ export class AUNClient {
1092
1366
  if (!aid)
1093
1367
  return;
1094
1368
  try {
1095
- const checked = await this._agentMdManager.check(aid);
1369
+ const checked = await this._agentMdManager.check(aid, 0);
1096
1370
  if (!checked.remote_found) {
1097
- const content = this._agentMdManager.readContent(aid) || buildDefaultAgentMd(aid);
1098
- await this._agentMdManager.upload(content);
1371
+ await this._ensureInitialAgentMd();
1372
+ }
1373
+ else if (checked.in_sync === false) {
1374
+ await this._agentMdManager.download(aid);
1099
1375
  }
1100
1376
  }
1101
1377
  catch (exc) {
@@ -1104,6 +1380,7 @@ export class AUNClient {
1104
1380
  const store = this._aidStore;
1105
1381
  if (!store)
1106
1382
  return;
1383
+ await this._resumeOrgIdentityAfterConnect(store);
1107
1384
  let listed;
1108
1385
  try {
1109
1386
  listed = await this.call('group.list_my', {});
@@ -1140,6 +1417,8 @@ export class AUNClient {
1140
1417
  await this.bindGroupAid({ group_id: groupId, group_aid: groupAid }, { aidStore: store });
1141
1418
  return;
1142
1419
  }
1420
+ if (await this._resumeGroupAgentMdUpload(store, groupAid))
1421
+ return;
1143
1422
  const checked = await this._agentMdManager.check(groupAid);
1144
1423
  if (!checked.remote_found) {
1145
1424
  await this._uploadGroupAgentMd(store, groupAid, {}, {});
@@ -1170,6 +1449,192 @@ export class AUNClient {
1170
1449
  this._groupIdentityOperations.delete(key);
1171
1450
  }
1172
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
+ }
1173
1638
  async createGroup(params = {}, options = {}) {
1174
1639
  if (!isJsonObject(params)) {
1175
1640
  throw new ValidationError('createGroup params must be an object');
@@ -1338,6 +1803,8 @@ export class AUNClient {
1338
1803
  `name: ${JSON.stringify(name)}`,
1339
1804
  'type: "group"',
1340
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)}`,
1341
1808
  `description: ${JSON.stringify(description)}`,
1342
1809
  ...(avatar ? [`avatar: ${JSON.stringify(avatar)}`] : []),
1343
1810
  '---',
@@ -1346,16 +1813,71 @@ export class AUNClient {
1346
1813
  '',
1347
1814
  ].join('\n');
1348
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
+ }
1349
1845
  async _uploadGroupAgentMd(store, groupAid, params, group) {
1350
1846
  const uploaderAid = String(this._aid ?? '').trim();
1351
1847
  if (!uploaderAid) {
1352
1848
  throw new ValidationError('group agent.md upload requires current owner AID');
1353
1849
  }
1354
- 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) });
1355
1854
  if (!uploaded.ok) {
1356
- 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');
1357
1857
  }
1358
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
+ }
1359
1881
  async bindGroupAid(params = {}, options = {}) {
1360
1882
  if (!isJsonObject(params)) {
1361
1883
  throw new ValidationError('bindGroupAid params must be an object');
@@ -1793,6 +2315,168 @@ export class AUNClient {
1793
2315
  this._clientLog.debug(`on exit: elapsed=${Date.now() - tStart}ms event=${event}`);
1794
2316
  return result;
1795
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
+ }
1796
2480
  async _callRawV2Rpc(method, params, pullGateKey = '') {
1797
2481
  const p = { ...(params ?? {}) };
1798
2482
  const forceForeground = Boolean(p._rpc_foreground);
@@ -1952,8 +2636,15 @@ export class AUNClient {
1952
2636
  _markPublishedSeq(ns, seq) {
1953
2637
  this._delivery.markPublishedSeq(ns, seq);
1954
2638
  }
1955
- _publishAppEvent(event, payload, source = 'direct') {
1956
- 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);
1957
2648
  }
1958
2649
  _echoTimestamp() {
1959
2650
  const now = new Date();
@@ -2132,8 +2823,8 @@ export class AUNClient {
2132
2823
  }
2133
2824
  return true;
2134
2825
  }
2135
- async _drainOrderedMessages(ns, beforeSeq, pullResponse = false, persist = true, source = 'push') {
2136
- 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);
2137
2828
  }
2138
2829
  async _publishOrderedMessage(event, ns, seq, payload, source = 'push') {
2139
2830
  return this._delivery.publishOrderedMessage(event, ns, seq, payload, source);
@@ -2322,7 +3013,10 @@ export class AUNClient {
2322
3013
  }
2323
3014
  }
2324
3015
  try {
2325
- 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);
2326
3020
  }
2327
3021
  catch (exc) {
2328
3022
  if (cachedBootstrapChain) {
@@ -2336,9 +3030,9 @@ export class AUNClient {
2336
3030
  validatedAt: nowSec,
2337
3031
  refreshAfter: nowSec + PEER_CERT_CACHE_TTL,
2338
3032
  };
2339
- const cacheKey = AUNClient._certCacheKey(aid, certFingerprint);
3033
+ const cacheKey = AUNClient._certCacheKey(aid, certFingerprint) + (opts.signedAt === undefined ? '' : `:signed:${opts.signedAt}`);
2340
3034
  this._certCache.set(cacheKey, entry);
2341
- if (!certFingerprint) {
3035
+ if (!certFingerprint && opts.signedAt === undefined) {
2342
3036
  const bareKey = AUNClient._certCacheKey(aid);
2343
3037
  this._certCache.set(bareKey, entry);
2344
3038
  const actualFp = `sha256:${x509Cert.fingerprint256.replace(/:/g, '').toLowerCase()}`;
@@ -2353,12 +3047,19 @@ export class AUNClient {
2353
3047
  }
2354
3048
  return certPem;
2355
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
+ }
2356
3057
  /** 获取对方证书(带缓存 + 完整 PKI 验证),跨域时自动路由到 peer 所在域。 */
2357
- async _fetchPeerCert(aid, certFingerprint, timeoutMs = 30_000) {
3058
+ async _fetchPeerCert(aid, certFingerprint, timeoutMs = 30_000, signedAt) {
2358
3059
  const tStart = Date.now();
2359
3060
  this._clientLog.debug(`_fetchPeerCert enter: aid=${aid}, fp=${certFingerprint ?? ''}`);
2360
3061
  try {
2361
- const cacheKey = AUNClient._certCacheKey(aid, certFingerprint);
3062
+ const cacheKey = AUNClient._certCacheKey(aid, certFingerprint) + (signedAt === undefined ? '' : `:signed:${signedAt}`);
2362
3063
  const cached = this._certCache.get(cacheKey);
2363
3064
  const now = Date.now() / 1000;
2364
3065
  if (cached && now < cached.refreshAfter) {
@@ -2376,6 +3077,7 @@ export class AUNClient {
2376
3077
  certPem,
2377
3078
  certFingerprint,
2378
3079
  source: 'fetch',
3080
+ signedAt,
2379
3081
  });
2380
3082
  this._clientLog.debug(`_fetchPeerCert exit: elapsed=${Date.now() - tStart}ms aid=${aid} (fetched)`);
2381
3083
  return validated;
@@ -2908,6 +3610,7 @@ export class AUNClient {
2908
3610
  }
2909
3611
  this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
2910
3612
  this._startBackgroundTasks();
3613
+ this._safeAsync(this._reconcileReadStates());
2911
3614
  const connectionKind = String(params.connection_kind ?? 'long');
2912
3615
  const isShortConnection = connectionKind === 'short';
2913
3616
  const hasExplicitBackgroundSync = Object.prototype.hasOwnProperty.call(params, 'background_sync');
@@ -2934,7 +3637,7 @@ export class AUNClient {
2934
3637
  this._clientLog.warn(`schedule post-connect P2P gap fill failed: ${formatCaughtError(exc)}`);
2935
3638
  });
2936
3639
  }
2937
- this._safeAsync(this._checkIdentityAdmissionAfterConnect());
3640
+ this._safeAsync(this._resumeOwnershipAfterConnect());
2938
3641
  this._clientLog.debug(`_connectOnce exit: elapsed=${Date.now() - tStart}ms gateway=${gatewayUrl}, aid=${this._aid ?? ''}`);
2939
3642
  }
2940
3643
  catch (err) {
@@ -3333,6 +4036,8 @@ export class AUNClient {
3333
4036
  encrypted: true,
3334
4037
  e2ee,
3335
4038
  };
4039
+ if (typeof plaintext.type === 'string' && plaintext.type.startsWith('aun.ownership.'))
4040
+ result.from_aid = fromAid;
3336
4041
  if (senderDeviceId)
3337
4042
  result.sender_device_id = senderDeviceId;
3338
4043
  const explicitDirection = String(msg.direction ?? '').trim();
@@ -3546,6 +4251,22 @@ export class AUNClient {
3546
4251
  const gateways = await this._resolveGatewayCandidatesForAid(aid);
3547
4252
  return gateways[0];
3548
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
+ }
3549
4270
  /** 发现并缓存全部 Gateway 候选,供首连和自动重连故障转移使用。 */
3550
4271
  async _resolveGatewayCandidatesForAid(aid) {
3551
4272
  const resolvedAid = String(aid ?? this._aid ?? '').trim();