@agentunion/fastaun 0.5.25 → 0.5.27

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 (76) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/_packed_docs/CHANGELOG.md +41 -0
  3. package/_packed_docs/INDEX.md +22 -14
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +11 -6
  5. 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 +16 -1
  6. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +37 -9
  7. package/_packed_docs/cli/AUN-CLI/350/256/276/350/256/241/346/226/207/346/241/243.md +20 -9
  8. package/_packed_docs/cli/CLI/346/211/213/345/206/214.md +57 -12
  9. package/_packed_docs/cli//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241CLI/345/274/200/345/217/221/344/270/216/344/275/277/347/224/250/346/214/207/345/215/227.md +869 -0
  10. package/_packed_docs/sdk/0.5.22-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +2 -2
  11. package/_packed_docs/sdk/0.5.27-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +408 -0
  12. package/_packed_docs/sdk/0.5.27-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +410 -0
  13. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +2 -2
  14. package/_packed_docs/sdk/09-/345/207/255/350/257/201/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +187 -73
  15. package/_packed_docs/sdk/09-/347/273/204/347/273/207/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +1 -1
  16. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +13 -9
  17. package/_packed_docs/sdk/INDEX.md +21 -13
  18. package/_packed_docs/sdk/agent.md/344/270/216group.index/347/232/204ETag/350/247/202/345/257/237/344/270/216/345/220/214/346/255/245/346/234/272/345/210/266.md +240 -0
  19. 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 +973 -266
  20. 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 +950 -242
  21. package/dist/agent-md-schema.js +1 -1
  22. package/dist/agent-md-schema.js.map +1 -1
  23. package/dist/agent-md.js +5 -0
  24. package/dist/agent-md.js.map +1 -1
  25. package/dist/aid.js +3 -3
  26. package/dist/aid.js.map +1 -1
  27. package/dist/auth.js +21 -19
  28. package/dist/auth.js.map +1 -1
  29. package/dist/cert-utils.d.ts +1 -1
  30. package/dist/cert-utils.js +4 -4
  31. package/dist/cert-utils.js.map +1 -1
  32. package/dist/client/lifecycle.d.ts +1 -1
  33. package/dist/client/lifecycle.js.map +1 -1
  34. package/dist/client/rpc-pipeline.js +8 -1
  35. package/dist/client/rpc-pipeline.js.map +1 -1
  36. package/dist/client/v2-e2ee.js +7 -0
  37. package/dist/client/v2-e2ee.js.map +1 -1
  38. package/dist/client.d.ts +12 -0
  39. package/dist/client.js +305 -29
  40. package/dist/client.js.map +1 -1
  41. package/dist/facades.d.ts +38 -0
  42. package/dist/facades.js +49 -5
  43. package/dist/facades.js.map +1 -1
  44. package/dist/index.d.ts +4 -0
  45. package/dist/index.js +3 -0
  46. package/dist/index.js.map +1 -1
  47. package/dist/keystore/local-identity-store.js +16 -1
  48. package/dist/keystore/local-identity-store.js.map +1 -1
  49. package/dist/keystore/local-token-store.js +16 -1
  50. package/dist/keystore/local-token-store.js.map +1 -1
  51. package/dist/org-protocol.d.ts +1 -0
  52. package/dist/org-protocol.js +39 -36
  53. package/dist/org-protocol.js.map +1 -1
  54. package/dist/ownership-v21.js +7 -7
  55. package/dist/ownership-v21.js.map +1 -1
  56. package/dist/register-flow.js +2 -2
  57. package/dist/register-flow.js.map +1 -1
  58. package/dist/semantic-facade.d.ts +176 -0
  59. package/dist/semantic-facade.js +611 -0
  60. package/dist/semantic-facade.js.map +1 -0
  61. package/dist/semantic-protocol.d.ts +810 -0
  62. package/dist/semantic-protocol.js +894 -0
  63. package/dist/semantic-protocol.js.map +1 -0
  64. package/dist/tools/cross-sdk-agent.d.ts +11 -1
  65. package/dist/tools/cross-sdk-agent.js +388 -7
  66. package/dist/tools/cross-sdk-agent.js.map +1 -1
  67. package/dist/transport.d.ts +1 -0
  68. package/dist/transport.js +28 -2
  69. package/dist/transport.js.map +1 -1
  70. package/dist/v2/e2ee/decrypt.d.ts +5 -0
  71. package/dist/v2/e2ee/decrypt.js +22 -2
  72. package/dist/v2/e2ee/decrypt.js.map +1 -1
  73. package/dist/version.d.ts +1 -1
  74. package/dist/version.js +1 -1
  75. package/package.json +2 -1
  76. package/src/certs/root.crt +13 -0
package/dist/client.d.ts CHANGED
@@ -20,6 +20,8 @@ import { AID } from './aid.js';
20
20
  import { type OwnershipConsensusResult } from './ownership-v21.js';
21
21
  import { type OwnershipTaskResult } from './ownership-workflow.js';
22
22
  import { type CertificateMaterial } from './org-protocol.js';
23
+ import { type RecordVerificationContext as SemanticRecordVerificationContext, type SemanticMessageVerificationContext } from './semantic-protocol.js';
24
+ import type { SemanticPersistentState } from './semantic-facade.js';
23
25
  /**
24
26
  * 递归排序键的 JSON 序列化(Canonical JSON for AUN)
25
27
  * 等价于 Python json.dumps(sort_keys=True, separators=(",",":"), ensure_ascii=False)
@@ -108,6 +110,11 @@ export declare class AUNClient {
108
110
  private _startCredentialRenewal;
109
111
  private _stopCredentialRenewal;
110
112
  _rememberCredential(credential: JsonObject, material: Record<string, unknown>, certificate?: CertificateMaterial): void;
113
+ _semanticContextKey(): string;
114
+ _loadSemanticState(): SemanticPersistentState;
115
+ _saveSemanticState(state: SemanticPersistentState): void;
116
+ _semanticVerificationContext(): SemanticRecordVerificationContext;
117
+ _semanticMessageVerificationContext(): SemanticMessageVerificationContext;
111
118
  private _verifyIncomingServiceEvent;
112
119
  private _handleCredentialRenewal;
113
120
  /** 实例级 protected_headers */
@@ -219,6 +226,9 @@ export declare class AUNClient {
219
226
  private _reconnectAbort;
220
227
  private _reconnectTask;
221
228
  private _reconnectEventDispatchDepth;
229
+ private _gatewayConnectionId;
230
+ private _migrationPending;
231
+ private _migrationRevision;
222
232
  private _serverKicked;
223
233
  /** 缓存最近一次 gateway.disconnect 信息(含服务端附带的 detail),用于后续 connection.state 透传 */
224
234
  private _lastDisconnectInfo;
@@ -612,6 +622,8 @@ export declare class AUNClient {
612
622
  private _unrefTimer;
613
623
  /** 不重连 close code 集合:认证失败/权限错误/被踢等,重连无意义 */
614
624
  private static readonly _NO_RECONNECT_CODES;
625
+ private _onGatewayMigrate;
626
+ private _runGatewayMigration;
615
627
  /** 处理服务端主动断开通知 event/gateway.disconnect。
616
628
  *
617
629
  * 服务端可能附带结构化 detail 字段(如配额超限时含 aid/device_id/slot_id/quota_kind/evicted_by)。
package/dist/client.js CHANGED
@@ -43,7 +43,7 @@ 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
+ import { decryptedOwnershipJsonText, decryptedMessagePayloadBytes } from './v2/e2ee/decrypt.js';
47
47
  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';
@@ -51,6 +51,7 @@ import { validateAgentMdCertificate, validateAgentMdDocument } from './agent-md-
51
51
  import { OwnershipFiles, OwnershipStorageAdapter, ownershipAid, ownershipFailure } from './ownership-v21.js';
52
52
  import { OwnershipWorkflow, OWNERSHIP_SDK_MESSAGE_TYPE, parseOwnershipMessage } from './ownership-workflow.js';
53
53
  import { canonicalJson, createCredential, createMemberCredential, signMembershipBinding, digest, validateMembershipBinding, verifyMembershipBinding, verifyCredentialSignature, createRepresentationProof, verifyServiceEvent, buildRequestBinding, validateRequestBinding, CredentialRenewal } from './org-protocol.js';
54
+ import { SemanticProtocolError } from './semantic-protocol.js';
54
55
  function isPromiseLike(value) {
55
56
  return Boolean(value && typeof value.then === 'function');
56
57
  }
@@ -675,12 +676,12 @@ export class AUNClient {
675
676
  this._credentialRenewalAbort = abort;
676
677
  const current = () => this._credentialRenewalAbort === abort && !abort.signal.aborted && aid === this._aid && version === this._serviceEventIdentityVersion && ['ready', 'connected'].includes(this._state) && !this._closing && !this._disconnecting;
677
678
  const guard = { current, signal: abort.signal };
678
- this._nextCredentialRevocationSyncAt ||= Date.now() + 3_600_000;
679
+ this._nextCredentialRevocationSyncAt = Date.now();
679
680
  const scan = async () => {
680
681
  const task = this._credentialRenewalScan.then(async () => {
681
682
  if (!current())
682
683
  return;
683
- const syncRevocations = Date.now() >= this._nextCredentialRevocationSyncAt;
684
+ const syncRevocations = this._sessionOptions.connection_kind !== 'short' && Date.now() >= this._nextCredentialRevocationSyncAt;
684
685
  if (syncRevocations)
685
686
  this._nextCredentialRevocationSyncAt = Date.now() + 3_600_000;
686
687
  await this._resumeCredentialRenewalAfterConnect(guard, syncRevocations);
@@ -710,13 +711,95 @@ export class AUNClient {
710
711
  }
711
712
  this._safeAsync(this._getCredentialRenewal().remember(credential, material));
712
713
  }
714
+ _semanticContextKey() {
715
+ return JSON.stringify([this._aid, this._currentAid?.certFingerprint, this._serviceEventIdentityVersion]);
716
+ }
717
+ _loadSemanticState() {
718
+ const state = (this._tokenStore._loadMetadataStrict?.(this._aid ?? '') ?? this._tokenStore.loadMetadata?.(this._aid ?? ''))?.semantic_credentials_v1;
719
+ return structuredClone(state ?? {});
720
+ }
721
+ _saveSemanticState(state) {
722
+ if (!this._aid)
723
+ throw new ValidationError('semantic credential persistence requires a loaded identity');
724
+ if (!this._tokenStore.saveMetadata)
725
+ throw new ValidationError('semantic credential metadata storage is unavailable');
726
+ this._tokenStore.saveMetadata(this._aid, { semantic_credentials_v1: state });
727
+ }
728
+ _semanticVerificationContext() {
729
+ const scope = this._semanticContextKey();
730
+ const current = () => { if (this._semanticContextKey() !== scope)
731
+ throw new ValidationError('semantic credential identity changed'); };
732
+ return {
733
+ resolveCertificate: async (fingerprint, aid, signedAt) => {
734
+ current();
735
+ const certificate = await this._fetchPeerCert(aid, fingerprint, undefined, signedAt);
736
+ current();
737
+ return certificate;
738
+ },
739
+ verifyCertificateTrust: async (material, context) => {
740
+ current();
741
+ const certificate = typeof material === 'string' && material.startsWith('-----BEGIN CERTIFICATE-----')
742
+ ? material : new crypto.X509Certificate(typeof material === 'string' ? Buffer.from(material, 'base64') : material).toString();
743
+ const gateway = await this._resolveOwnershipCertificateGateway(context.expectedAID);
744
+ await this._auth.verifyPeerCertificate(gateway, certificate, context.expectedAID, context.signedAt);
745
+ await this._auth.verifyPeerCertificate(gateway, certificate, context.expectedAID, undefined, true);
746
+ current();
747
+ },
748
+ };
749
+ }
750
+ _semanticMessageVerificationContext() {
751
+ const actor = this._aid, device = this._deviceId, scope = this._semanticContextKey(), session = this._v2Session;
752
+ const trust = this._semanticVerificationContext();
753
+ const current = () => {
754
+ if (!actor || this._semanticContextKey() !== scope || device !== this._deviceId || session !== this._v2Session)
755
+ throw new SemanticProtocolError('SEMANTIC_IDENTITY_INVALID', 'semantic message verification identity changed');
756
+ };
757
+ const senderCertificate = async (sender, fingerprint, signedAt) => {
758
+ current();
759
+ const certificate = await this._fetchPeerCert(sender, fingerprint, undefined, signedAt);
760
+ if (!publicKeyMatchesFingerprint(certificate, fingerprint))
761
+ throw new SemanticProtocolError('SEMANTIC_IDENTITY_INVALID', 'historical sender key fingerprint differs');
762
+ current();
763
+ return certificate;
764
+ };
765
+ return {
766
+ selfAid: actor ?? '', selfDeviceId: device,
767
+ resolveDecryptionKeys: async (envelope) => {
768
+ current();
769
+ if (!session || session.aid !== actor || session.deviceId !== device)
770
+ throw new SemanticProtocolError('SEMANTIC_STATE_UNAVAILABLE', 'authenticated V2 receiver session is unavailable');
771
+ const recipient = envelope.recipient;
772
+ const row = Array.isArray(envelope.recipients) ? envelope.recipients.find((item) => Array.isArray(item) && item.length >= 6 && item[0] === actor && (item[1] === device || item[1] === '')) : undefined;
773
+ if (!row && (!recipient || recipient.aid !== actor || recipient.device_id !== device && recipient.device_id !== ''))
774
+ throw new SemanticProtocolError('SEMANTIC_IDENTITY_INVALID', 'original receiver row is unavailable');
775
+ const spkId = String(row ? row[5] ?? '' : recipient.spk_id ?? ''), aad = envelope.aad;
776
+ if (envelope.type === 'e2ee.group_encrypted') {
777
+ const keys = session.getGroupDecryptKeys(String(aad.group_id ?? ''), spkId);
778
+ return { ikPriv: keys.ikPriv, spkPriv: keys.spkPriv ?? undefined };
779
+ }
780
+ return session.getDecryptKeys(spkId);
781
+ },
782
+ resolveSenderKey: async (sender, _device, fingerprint, signedAt) => {
783
+ const certificate = await senderCertificate(sender, fingerprint, signedAt);
784
+ return new Uint8Array(new crypto.X509Certificate(certificate).publicKey.export({ type: 'spki', format: 'der' }));
785
+ },
786
+ verifySenderIdentity: async (sender, _device, fingerprint, signedAt, publicKey) => {
787
+ const certificate = await senderCertificate(sender, fingerprint, signedAt);
788
+ await trust.verifyCertificateTrust(certificate, { expectedAID: sender, signedAt, now: Math.trunc(Date.now() / 1000) });
789
+ const expected = new crypto.X509Certificate(certificate).publicKey.export({ type: 'spki', format: 'der' });
790
+ if (!expected.equals(Buffer.from(publicKey)))
791
+ throw new SemanticProtocolError('SEMANTIC_IDENTITY_INVALID', 'historical sender identity differs from message key');
792
+ current();
793
+ },
794
+ };
795
+ }
713
796
  async _verifyIncomingServiceEvent(event, payload) {
714
797
  const targetAid = this._identity?.aid ?? '';
715
798
  const version = this._serviceEventIdentityVersion;
716
799
  const signal = this._serviceEventIdentityAbort.signal;
717
800
  const expiresAt = Number(payload?.expires_at);
718
801
  const transport = this._transport, connectionGeneration = transport?._connectionGeneration, workerGeneration = transport?._workerGeneration;
719
- const current = () => version === this._serviceEventIdentityVersion && targetAid === (this._identity?.aid ?? '') && transport === this._transport && connectionGeneration === this._transport?._connectionGeneration && workerGeneration === this._transport?._workerGeneration && Date.now() / 1000 < expiresAt;
802
+ const current = () => version === this._serviceEventIdentityVersion && targetAid === (this._identity?.aid ?? '') && transport === this._transport && connectionGeneration === this._transport?._connectionGeneration && workerGeneration === this._transport?._workerGeneration && Date.now() / 1000 <= expiresAt + 300;
720
803
  const verified = await verifyServiceEvent(event, payload, { targetAid, resolveEventCertificate: (aid, fingerprint) => this._fetchServiceEventCertificate(aid, fingerprint), resolveCredentialService: this.resolveCredentialService });
721
804
  if (!current())
722
805
  throw new ValidationError('service event receiver identity changed');
@@ -865,6 +948,9 @@ export class AUNClient {
865
948
  _reconnectAbort = null;
866
949
  _reconnectTask = null;
867
950
  _reconnectEventDispatchDepth = 0;
951
+ _gatewayConnectionId = '';
952
+ _migrationPending = false;
953
+ _migrationRevision = new Map();
868
954
  _serverKicked = false;
869
955
  /** 缓存最近一次 gateway.disconnect 信息(含服务端附带的 detail),用于后续 connection.state 透传 */
870
956
  _lastDisconnectInfo = null;
@@ -1058,6 +1144,10 @@ export class AUNClient {
1058
1144
  }
1059
1145
  // 服务端主动断开通知:记录日志并标记不重连
1060
1146
  this._dispatcher.subscribeProtocol('_raw.gateway.disconnect', (data) => this._onGatewayDisconnect(data));
1147
+ this._dispatcher.subscribeProtocol('notification', (data) => {
1148
+ if (isJsonObject(data) && data.method === 'gateway.migrate' && !('id' in data))
1149
+ this._onGatewayMigrate(data.params);
1150
+ });
1061
1151
  }
1062
1152
  // ── 属性 ──────────────────────────────────────────────────
1063
1153
  /** 当前 AID */
@@ -1730,7 +1820,7 @@ export class AUNClient {
1730
1820
  async _resumeOrgRequest(method, params, target) {
1731
1821
  const binding = params.request_binding;
1732
1822
  const now = Math.trunc(Date.now() / 1000);
1733
- if (!isJsonObject(binding) || now < Number(binding.exp))
1823
+ if (!isJsonObject(binding) || now <= Number(binding.exp) + 300)
1734
1824
  return params;
1735
1825
  const credentials = isJsonObject(params.credentials) ? params.credentials : {};
1736
1826
  const credential = ['member_credential', 'approval_credential', 'audit_credential'].map((field) => credentials[field]).find(isJsonObject) ?? params.credential_template;
@@ -1787,7 +1877,7 @@ export class AUNClient {
1787
1877
  checkpoint = { ...checkpoint, result, phase: 'upload' };
1788
1878
  tokens.saveMetadata(orgAid, { org_identity_creation: checkpoint });
1789
1879
  }
1790
- await this._uploadOrgAgentMd(store, orgAid, params);
1880
+ await this._uploadOrgAgentMd(store, orgAid, params, checkpoint.result);
1791
1881
  tokens.saveMetadata(orgAid, { org_identity_creation: null });
1792
1882
  return checkpoint.result;
1793
1883
  }
@@ -1829,7 +1919,7 @@ export class AUNClient {
1829
1919
  const now = Math.trunc(Date.now() / 1000);
1830
1920
  if (binding.org_id !== orgAid || nextBinding.org_id !== orgAid || binding.member_aid === ownerAid || nextBinding.member_aid !== ownerAid
1831
1921
  || binding.role !== 'member' || nextBinding.role !== 'owner' || digest(previous.terms) !== binding.terms_hash || digest(next.terms) !== nextBinding.terms_hash
1832
- || !Number.isSafeInteger(nextBinding.iat) || !Number.isSafeInteger(nextBinding.exp) || Number(nextBinding.iat) > now || Number(nextBinding.exp) <= now
1922
+ || !Number.isSafeInteger(nextBinding.iat) || !Number.isSafeInteger(nextBinding.exp) || Number(nextBinding.iat) > now + 300 || now > Number(nextBinding.exp) + 300
1833
1923
  || credential.iss !== orgAid || credential.sub !== binding.member_aid || credential.parent != null || credential.delegate !== false
1834
1924
  || ext.credential_type !== 'member' || ext.bound_to_membership !== true || ext.org_id !== orgAid || ext.membership_id !== binding.membership_id
1835
1925
  || ext.binding_version !== binding.binding_version || ext.membership_terms_hash !== binding.terms_hash
@@ -1848,7 +1938,7 @@ export class AUNClient {
1848
1938
  const checkedAt = Math.trunc(Date.now() / 1000);
1849
1939
  verifyMembershipBinding(binding, resolve, checkedAt, orgAid);
1850
1940
  verifyCredentialSignature(credential, resolve, checkedAt, orgAid);
1851
- if (Number(nextBinding.exp) <= checkedAt)
1941
+ if (checkedAt > Number(nextBinding.exp) + 300)
1852
1942
  throw ownershipFailure('invalid', 'Org transfer membership materials do not match');
1853
1943
  }
1854
1944
  async completeOrgTransfer(params = {}, options = {}) {
@@ -1938,7 +2028,7 @@ export class AUNClient {
1938
2028
  tokenStore.saveMetadata(orgAid, { org_identity_transfer: pending });
1939
2029
  }
1940
2030
  const result = await this.call('org.lifecycle.transferAccept', await this._resumeOrgRequest('org.lifecycle.transferAccept', pending.finalize_params, target));
1941
- await this._uploadOrgAgentMd(store, orgAid, pending.source_params);
2031
+ await this._uploadOrgAgentMd(store, orgAid, pending.source_params, result);
1942
2032
  tokenStore.saveMetadata(orgAid, { org_identity_transfer: { transfer_id: transferId, owner_aid: owner.aid, input_hash: inputHash, phase: 'completed', result } });
1943
2033
  localStore.clearPendingGroupBind(pendingKey);
1944
2034
  return result;
@@ -1976,15 +2066,18 @@ export class AUNClient {
1976
2066
  }
1977
2067
  return result;
1978
2068
  }
1979
- async _uploadOrgAgentMd(store, orgAid, params) {
2069
+ async _uploadOrgAgentMd(store, orgAid, params, initialResult) {
1980
2070
  const owner = this._currentAid;
1981
2071
  if (!owner)
1982
2072
  throw ownershipFailure('forbidden', 'Org agent.md requires current owner');
1983
- if (await this._resumeGroupAgentMdUpload(store, orgAid))
2073
+ const organization = isJsonObject(initialResult) && isJsonObject(initialResult.organization) ? initialResult.organization : undefined;
2074
+ const bootstrap = organization?.org_id === orgAid && organization.owner_aid === owner.aid && organization.status === 'active'
2075
+ ? { aid: orgAid, ownerAid: owner.aid, certFingerprint: this._localEntityCertificate(orgAid).certFingerprint, completedTransfer: Boolean(params.transfer_id && isJsonObject(initialResult) && initialResult.transfer_id === params.transfer_id) } : undefined;
2076
+ if (await this._resumeGroupAgentMdUpload(store, orgAid, bootstrap))
1984
2077
  return;
1985
2078
  const identity = this._localEntityCertificate(orgAid);
1986
2079
  const content = String(params.org_agent_md ?? buildDefaultAgentMd(orgAid, { type: 'org', owner: owner.aid, entityCertFingerprint: identity.certFingerprint, name: String(params.name ?? orgAid) }));
1987
- const uploaded = await store.uploadAgentMd(orgAid, content, { uploaderAid: owner.aid, entityOwnerResolver: (aid, type) => this._resolveEntityAgentMdOwner(aid, type), entityIdentityGetter: (aid) => this._freshEntityCertificate(aid, 'org') });
2080
+ const uploaded = await store.uploadAgentMd(orgAid, content, { uploaderAid: owner.aid, entityOwnerResolver: (aid, type) => this._resolveEntityAgentMdOwner(aid, type, bootstrap), entityIdentityGetter: (aid) => this._freshEntityCertificate(aid, 'org') });
1988
2081
  if (!uploaded.ok) {
1989
2082
  const code = ['invalid', 'unavailable', 'forbidden', 'conflict', 'persistence_unavailable'].includes(uploaded.error.code) ? uploaded.error.code : uploaded.error.code === 'NETWORK_ERROR' ? 'unavailable' : 'invalid';
1990
2083
  throw ownershipFailure(code, uploaded.error.message, code === 'unavailable');
@@ -2100,7 +2193,7 @@ export class AUNClient {
2100
2193
  if (!imported.ok) {
2101
2194
  throw new ValidationError(imported.error.message);
2102
2195
  }
2103
- await this._uploadGroupAgentMd(store, groupAid, payload, group);
2196
+ await this._uploadGroupAgentMd(store, groupAid, payload, group, certPem);
2104
2197
  if (keystore && typeof keystore.clearPendingGroupBind === 'function') {
2105
2198
  keystore.clearPendingGroupBind(pendingKey);
2106
2199
  }
@@ -2168,7 +2261,7 @@ export class AUNClient {
2168
2261
  '',
2169
2262
  ].join('\n');
2170
2263
  }
2171
- async _resumeGroupAgentMdUpload(store, groupAid) {
2264
+ async _resumeGroupAgentMdUpload(store, groupAid, bootstrap) {
2172
2265
  const identityStore = store;
2173
2266
  const pendingManager = identityStore._agentMdManager ?? this._agentMdManager;
2174
2267
  const pending = pendingManager.pendingUpload(groupAid);
@@ -2188,7 +2281,7 @@ export class AUNClient {
2188
2281
  uploadViaGatewayHost: true,
2189
2282
  ownerAidGetter: () => identity.aid,
2190
2283
  currentAidGetter: () => identity,
2191
- entityOwnerResolver: (aid, type) => this._resolveEntityAgentMdOwner(aid, type),
2284
+ entityOwnerResolver: (aid, type) => this._resolveEntityAgentMdOwner(aid, type, bootstrap?.completedTransfer || pending.condition?.if_none_match === '*' ? bootstrap : undefined),
2192
2285
  entityIdentityGetter: (aid, type) => this._freshEntityCertificate(aid, type),
2193
2286
  entitySignerResolver: (aid, fingerprint) => this._freshEntitySigner(aid, fingerprint),
2194
2287
  gatewayResolver: (aid) => identityStore._resolveGateway(aid),
@@ -2203,15 +2296,16 @@ export class AUNClient {
2203
2296
  });
2204
2297
  return await manager.resumePendingUpload(groupAid);
2205
2298
  }
2206
- async _uploadGroupAgentMd(store, groupAid, params, group) {
2299
+ async _uploadGroupAgentMd(store, groupAid, params, group, initialCertificate) {
2207
2300
  const uploaderAid = String(this._aid ?? '').trim();
2208
2301
  if (!uploaderAid) {
2209
2302
  throw new ValidationError('group agent.md upload requires current owner AID');
2210
2303
  }
2211
2304
  this._aidStore = store;
2212
- if (await this._resumeGroupAgentMdUpload(store, groupAid))
2305
+ const bootstrap = initialCertificate ? { aid: groupAid, ownerAid: String(group.owner_aid ?? '').trim(), certFingerprint: certFingerprint(initialCertificate), type: 'group' } : undefined;
2306
+ if (await this._resumeGroupAgentMdUpload(store, groupAid, bootstrap))
2213
2307
  return;
2214
- const uploaded = await store.uploadAgentMd(groupAid, this._groupAgentMdContent(groupAid, params, group), { uploaderAid, entityOwnerResolver: (aid, type) => this._resolveEntityAgentMdOwner(aid, type), entityIdentityGetter: (aid) => this._freshEntityCertificate(aid) });
2308
+ const uploaded = await store.uploadAgentMd(groupAid, this._groupAgentMdContent(groupAid, params, group), { uploaderAid, entityOwnerResolver: (aid, type) => this._resolveEntityAgentMdOwner(aid, type, bootstrap), entityIdentityGetter: (aid) => this._freshEntityCertificate(aid) });
2215
2309
  if (!uploaded.ok) {
2216
2310
  const code = ['invalid', 'unavailable', 'forbidden', 'conflict', 'persistence_unavailable'].includes(uploaded.error.code) ? uploaded.error.code : uploaded.error.code === 'NETWORK_ERROR' ? 'unavailable' : 'invalid';
2217
2311
  throw ownershipFailure(code, `group agent.md upload failed: ${uploaded.error.message}`, code === 'unavailable');
@@ -2232,8 +2326,20 @@ export class AUNClient {
2232
2326
  const material = await this._auth.resolveOwnershipCertificate(await this._resolveOwnershipCertificateGateway(aid), aid, fingerprint, 'signing');
2233
2327
  return AID._create({ aid, aunPath: this._configModel.aunPath, certPem: material.cert_pem, certValid: true, privateKeyPem: null, privateKeyValid: false });
2234
2328
  }
2235
- async _resolveEntityAgentMdOwner(aid, type) {
2236
- const authority = await this._agentMdManager.readEntityAuthorityFresh(aid);
2329
+ async _resolveEntityAgentMdOwner(aid, type, bootstrap) {
2330
+ let authority;
2331
+ try {
2332
+ authority = bootstrap?.completedTransfer && type === 'org' && bootstrap.aid === aid && bootstrap.ownerAid === this._currentAid?.aid
2333
+ ? { owner_aid: bootstrap.ownerAid, entity_cert_fingerprint: bootstrap.certFingerprint }
2334
+ : await this._agentMdManager.readEntityAuthorityFresh(aid);
2335
+ }
2336
+ catch (error) {
2337
+ if (!bootstrap || bootstrap.aid !== aid || type !== (bootstrap.type ?? 'org') || !(error instanceof AUNError) || !isJsonObject(error.data) || error.data.http_status !== 404)
2338
+ throw error;
2339
+ if (!bootstrap.ownerAid || bootstrap.ownerAid !== this._currentAid?.aid)
2340
+ throw ownershipFailure('forbidden', 'entity initial owner does not match uploader');
2341
+ authority = { owner_aid: bootstrap.ownerAid, entity_cert_fingerprint: bootstrap.certFingerprint };
2342
+ }
2237
2343
  const identity = await this._freshEntityCertificate(aid, type);
2238
2344
  if (authority.entity_cert_fingerprint !== identity.certFingerprint)
2239
2345
  throw ownershipFailure('conflict', 'entity authority changed during current-certificate verification');
@@ -2301,7 +2407,7 @@ export class AUNClient {
2301
2407
  if (!imported.ok) {
2302
2408
  throw new ValidationError(imported.error.message);
2303
2409
  }
2304
- await this._uploadGroupAgentMd(store, groupAid, payload, group);
2410
+ await this._uploadGroupAgentMd(store, groupAid, payload, group, certPem);
2305
2411
  // 落盘成功,清除 pending 槽位
2306
2412
  if (groupId && keystore && typeof keystore.clearPendingGroupBind === 'function') {
2307
2413
  keystore.clearPendingGroupBind(pendingKey);
@@ -2600,6 +2706,8 @@ export class AUNClient {
2600
2706
  * 发送轻量在线通知,不走离线存储、seq/pull 或 ack。
2601
2707
  */
2602
2708
  async notify(method, params, options = {}) {
2709
+ if (this._migrationPending)
2710
+ throw new ConnectionError('client is migrating');
2603
2711
  if (params !== undefined && params !== null && !isJsonObject(params)) {
2604
2712
  throw new ValidationError('notify params must be an object');
2605
2713
  }
@@ -3953,10 +4061,16 @@ export class AUNClient {
3953
4061
  const challenge = await this._transport.connect(gatewayUrl);
3954
4062
  this._assertReconnectOwner(reconnectOwner);
3955
4063
  this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
4064
+ const authTransport = reconnectOwner ? {
4065
+ call: (...args) => {
4066
+ this._assertReconnectOwner(reconnectOwner);
4067
+ return this._transport.call(...args);
4068
+ },
4069
+ } : this._transport;
3956
4070
  this._clientLog.debug(`WebSocket connection established: gateway=${gatewayUrl}`);
3957
4071
  this._state = 'authenticating';
3958
4072
  if (allowReauth) {
3959
- const authContext = await this._auth.connectSession(this._transport, challenge, gatewayUrl, {
4073
+ const authContext = await this._auth.connectSession(authTransport, challenge, gatewayUrl, {
3960
4074
  accessToken: String(params.access_token ?? ''),
3961
4075
  deviceId: this._deviceId,
3962
4076
  slotId: this._slotId,
@@ -3982,10 +4096,11 @@ export class AUNClient {
3982
4096
  if (isJsonObject(auth.hello) && 'heartbeat_interval' in auth.hello) {
3983
4097
  this._applyServerHeartbeatInterval(auth.hello.heartbeat_interval, 'auth');
3984
4098
  }
4099
+ this._gatewayConnectionId = isJsonObject(auth.hello) && isJsonObject(auth.hello.connection) ? String(auth.hello.connection.id ?? '') : '';
3985
4100
  }
3986
4101
  }
3987
4102
  else {
3988
- const hello = await this._auth.initializeWithToken(this._transport, challenge, String(params.access_token), {
4103
+ const hello = await this._auth.initializeWithToken(authTransport, challenge, String(params.access_token), {
3989
4104
  deviceId: this._deviceId,
3990
4105
  slotId: this._slotId,
3991
4106
  deliveryMode: this._connectDeliveryMode,
@@ -3999,6 +4114,7 @@ export class AUNClient {
3999
4114
  if (isJsonObject(hello) && 'heartbeat_interval' in hello) {
4000
4115
  this._applyServerHeartbeatInterval(hello.heartbeat_interval, 'auth');
4001
4116
  }
4117
+ this._gatewayConnectionId = isJsonObject(hello) && isJsonObject(hello.connection) ? String(hello.connection.id ?? '') : '';
4002
4118
  }
4003
4119
  this._assertReconnectOwner(reconnectOwner);
4004
4120
  this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
@@ -4006,6 +4122,7 @@ export class AUNClient {
4006
4122
  throw new ConnectionError('websocket disconnected during authentication');
4007
4123
  }
4008
4124
  this._serverKicked = false;
4125
+ this._migrationPending = false;
4009
4126
  this._lastDisconnectInfo = null;
4010
4127
  this._state = 'ready';
4011
4128
  this._connectedAt = Date.now();
@@ -4435,8 +4552,10 @@ export class AUNClient {
4435
4552
  return null;
4436
4553
  }
4437
4554
  let plaintext;
4555
+ let payloadBytes;
4438
4556
  try {
4439
4557
  plaintext = decryptMessage(envelope, this._aid ?? '', this._deviceId, ikPriv, spkPriv, senderPubDer);
4558
+ payloadBytes = decryptedMessagePayloadBytes(plaintext);
4440
4559
  if (plaintext === null)
4441
4560
  throw new Error('recipient_not_found_or_envelope_verification_failed');
4442
4561
  }
@@ -4488,6 +4607,11 @@ export class AUNClient {
4488
4607
  encrypted: true,
4489
4608
  e2ee,
4490
4609
  };
4610
+ if (!this._v2E2EE.isEncryptedEnvelopePayload(msg.payload))
4611
+ result._semantic_evidence = {
4612
+ envelope_bytes_base64: Buffer.from(envelopeJson, 'utf8').toString('base64'), payload_bytes_base64: Buffer.from(payloadBytes).toString('base64'),
4613
+ received_by: this._aid ?? '', received_at: Math.trunc(Date.now() / 1000), source,
4614
+ };
4491
4615
  if (typeof plaintext.type === 'string' && plaintext.type.startsWith('aun.ownership.'))
4492
4616
  result.from_aid = fromAid;
4493
4617
  if (senderDeviceId)
@@ -4719,7 +4843,7 @@ export class AUNClient {
4719
4843
  }
4720
4844
  }
4721
4845
  /** 发现并缓存全部 Gateway 候选,供首连和自动重连故障转移使用。 */
4722
- async _resolveGatewayCandidatesForAid(aid) {
4846
+ async _resolveGatewayCandidatesForAid(aid, fresh = false, reconnectOwner) {
4723
4847
  const resolvedAid = String(aid ?? this._aid ?? '').trim();
4724
4848
  if (!resolvedAid) {
4725
4849
  throw new StateError('gateway discovery requires a loaded AID');
@@ -4732,14 +4856,15 @@ export class AUNClient {
4732
4856
  const tokenStore = this._tokenStore;
4733
4857
  const discoveryIsCurrent = () => (this._discovery === discovery
4734
4858
  && this._tokenStore === tokenStore
4735
- && !this._closing);
4736
- if (sameIssuer && this._gatewayCandidates.length > 0)
4859
+ && !this._closing
4860
+ && (!reconnectOwner || this._ownsReconnect(reconnectOwner)));
4861
+ if (!fresh && sameIssuer && this._gatewayCandidates.length > 0)
4737
4862
  return [...this._gatewayCandidates];
4738
- if (sameIssuer && this._gatewayUrl) {
4863
+ if (!fresh && sameIssuer && this._gatewayUrl) {
4739
4864
  this._gatewayCandidates = [this._gatewayUrl];
4740
4865
  return [...this._gatewayCandidates];
4741
4866
  }
4742
- if (!sameIssuer) {
4867
+ if (!fresh && !sameIssuer) {
4743
4868
  const cached = this._peerGatewayCache.get(issuerDomain);
4744
4869
  if (cached)
4745
4870
  return [...cached];
@@ -4754,7 +4879,7 @@ export class AUNClient {
4754
4879
  : [];
4755
4880
  const cachedGateway = cachedList[0]
4756
4881
  ?? (isJsonObject(metadata) ? String(metadata.gateway_url ?? '').trim() : '');
4757
- if (cachedGateway) {
4882
+ if (!fresh && cachedGateway) {
4758
4883
  const gatewayUrls = [...new Set([cachedGateway, ...cachedList])];
4759
4884
  if (sameIssuer) {
4760
4885
  this._gatewayCandidates = gatewayUrls;
@@ -5135,6 +5260,157 @@ export class AUNClient {
5135
5260
  // ── 内部:断线重连 ────────────────────────────────────────
5136
5261
  /** 不重连 close code 集合:认证失败/权限错误/被踢等,重连无意义 */
5137
5262
  static _NO_RECONNECT_CODES = new Set([4001, 4003, 4008, 4009, 4010, 4011, 4013, 4014, 4015]);
5263
+ _onGatewayMigrate(input) {
5264
+ const data = input;
5265
+ if (!isJsonObject(data) || this._closing || this._disconnecting || this._connectAbort || this._reconnectActive
5266
+ || this._state !== 'ready' || !this._sessionParams || !this._gatewayConnectionId)
5267
+ return;
5268
+ const aid = this._aid ?? '';
5269
+ const issuer = aid.slice(aid.indexOf('.') + 1).toLowerCase().replace(/\.$/, '');
5270
+ const now = Date.now() / 1000;
5271
+ if (typeof data.migration_id !== 'string' || !data.migration_id.trim()
5272
+ || typeof data.revision !== 'number' || !Number.isSafeInteger(data.revision) || data.revision <= 0
5273
+ || typeof data.issuer !== 'string' || data.issuer.trim().toLowerCase().replace(/\.$/, '') !== issuer
5274
+ || data.aid !== aid || data.device_id !== this._deviceId || data.slot_id !== this._slotId
5275
+ || data.connection_id !== this._gatewayConnectionId || typeof data.gateway_url !== 'string'
5276
+ || typeof data.expires_at !== 'number' || !Number.isFinite(data.expires_at)
5277
+ || data.expires_at <= now || data.expires_at > now + 300)
5278
+ return;
5279
+ let target;
5280
+ try {
5281
+ target = new URL(data.gateway_url);
5282
+ }
5283
+ catch {
5284
+ return;
5285
+ }
5286
+ const hostname = target.hostname.toLowerCase();
5287
+ if (target.protocol !== 'wss:' || target.username || target.password || target.hash
5288
+ || /^\w+:\/\/[^/]*@/.test(data.gateway_url) || target.pathname !== '/aun'
5289
+ || !(hostname === issuer || hostname.endsWith(`.${issuer}`))
5290
+ || !hostname.split('.').every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label)))
5291
+ return;
5292
+ const identity = JSON.stringify([this._aid, this._deviceId, this._slotId]);
5293
+ const previous = this._migrationRevision.get(identity);
5294
+ if (previous && (data.revision <= previous.revision || data.migration_id === previous.migrationId))
5295
+ return;
5296
+ this._migrationRevision.set(identity, { revision: data.revision, migrationId: data.migration_id });
5297
+ this._migrationPending = true;
5298
+ this._reconnectActive = true;
5299
+ const owner = new AbortController();
5300
+ this._reconnectAbort = owner;
5301
+ this._state = 'standby';
5302
+ this._stopBackgroundTasks();
5303
+ this._dispatcher.enqueue('gateway.migrate', data);
5304
+ const task = Promise.resolve().then(() => this._runGatewayMigration(data, owner, identity));
5305
+ this._reconnectTask = task;
5306
+ this._safeAsync(task);
5307
+ }
5308
+ async _runGatewayMigration(data, owner, identity) {
5309
+ const expiresAt = Number(data.expires_at) * 1000;
5310
+ const current = () => this._ownsReconnect(owner) && !this._disconnecting && !this._connectAbort
5311
+ && identity === JSON.stringify([this._aid, this._deviceId, this._slotId]);
5312
+ const wait = (operation) => this._lifecycle.waitForAttempt(operation, undefined, owner);
5313
+ const retry = this._sessionOptions.retry;
5314
+ const maxAttempts = Number(retry.max_attempts ?? 0);
5315
+ let delay = clampReconnectDelayMs(Number(retry.initial_delay ?? 1) * 1000, RECONNECT_MIN_BASE_DELAY_MS);
5316
+ const maxDelay = clampReconnectDelayMs(Number(retry.max_delay ?? 64) * 1000, RECONNECT_MAX_BASE_DELAY_MS);
5317
+ const expiryTimer = setTimeout(() => {
5318
+ if (!current())
5319
+ return;
5320
+ owner.abort();
5321
+ this._discovery._cancelPending?.();
5322
+ this._auth._cancelPending?.();
5323
+ void this._transport.close().catch(() => { });
5324
+ }, Math.max(0, expiresAt - Date.now()));
5325
+ let lastError = new ConnectionError('gateway migration expired');
5326
+ try {
5327
+ if (!current())
5328
+ return;
5329
+ await this._transport.waitForPending(Math.min(5000, Math.max(0, expiresAt - Date.now())), owner.signal);
5330
+ if (!current())
5331
+ return;
5332
+ await this._saveSeqTrackerState();
5333
+ await this._transport.close();
5334
+ if (!current())
5335
+ return;
5336
+ this._delivery.resetInlineAckState();
5337
+ for (let attempt = 1; current() && Date.now() < expiresAt; attempt++) {
5338
+ if (maxAttempts > 0 && attempt > maxAttempts)
5339
+ throw lastError;
5340
+ let candidates = attempt === 1 ? [String(data.gateway_url)] : [];
5341
+ if (attempt > 1) {
5342
+ try {
5343
+ candidates = await wait(this._resolveGatewayCandidatesForAid(String(data.aid), true, owner));
5344
+ }
5345
+ catch (error) {
5346
+ lastError = error;
5347
+ }
5348
+ }
5349
+ for (let index = 0; index < candidates.length; index++) {
5350
+ const gateway = candidates[index];
5351
+ if (!current() || Date.now() >= expiresAt)
5352
+ return;
5353
+ this._state = 'reconnecting';
5354
+ this._migrationPending = true;
5355
+ try {
5356
+ await wait(this._connectOnce({ ...this._sessionParams, gateway, gateways: candidates }, true, owner));
5357
+ if (!current())
5358
+ return;
5359
+ if (!this._transport.isConnected())
5360
+ throw new ConnectionError('websocket disconnected while migrating');
5361
+ this._sessionParams.gateway = gateway;
5362
+ this._sessionParams.gateways = candidates;
5363
+ return;
5364
+ }
5365
+ catch (error) {
5366
+ lastError = error;
5367
+ if (!current())
5368
+ return;
5369
+ this._migrationPending = true;
5370
+ await this._transport.close().catch(() => { });
5371
+ if (!current())
5372
+ return;
5373
+ if (attempt === 1 && index === 0) {
5374
+ try {
5375
+ const discovered = await wait(this._resolveGatewayCandidatesForAid(String(data.aid), true, owner));
5376
+ candidates = [...candidates, ...discovered.filter((item) => !candidates.includes(item))];
5377
+ }
5378
+ catch (discoveryError) {
5379
+ lastError = discoveryError;
5380
+ }
5381
+ }
5382
+ }
5383
+ }
5384
+ if (!AUNClient._shouldRetryReconnect(lastError))
5385
+ throw lastError;
5386
+ if (maxAttempts > 0 && attempt >= maxAttempts)
5387
+ throw lastError;
5388
+ await this._sleep(Math.min(delay, Math.max(0, expiresAt - Date.now())), owner.signal);
5389
+ if (!current())
5390
+ return;
5391
+ delay = Math.min(delay * 2, maxDelay);
5392
+ }
5393
+ }
5394
+ catch (error) {
5395
+ lastError = error;
5396
+ }
5397
+ finally {
5398
+ clearTimeout(expiryTimer);
5399
+ if (this._reconnectAbort === owner) {
5400
+ this._migrationPending = false;
5401
+ this._reconnectActive = false;
5402
+ this._reconnectAbort = null;
5403
+ this._reconnectTask = null;
5404
+ this._nextRetryAt = null;
5405
+ if ((this._state !== 'ready' || owner.signal.aborted) && !this._closing) {
5406
+ this._stopBackgroundTasks();
5407
+ this._state = 'standby';
5408
+ if (!owner.signal.aborted)
5409
+ this._dispatcher.enqueue('connection.error', { error: formatCaughtError(lastError) });
5410
+ }
5411
+ }
5412
+ }
5413
+ }
5138
5414
  /** 处理服务端主动断开通知 event/gateway.disconnect。
5139
5415
  *
5140
5416
  * 服务端可能附带结构化 detail 字段(如配额超限时含 aid/device_id/slot_id/quota_kind/evicted_by)。