@agentunion/fastaun-browser 0.4.5 → 0.4.6
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.
- package/CHANGELOG.md +28 -0
- package/_packed_docs/CHANGELOG.md +28 -0
- package/_packed_docs/INDEX.md +2 -2
- package/_packed_docs/KITE_DOCS_GUIDE.md +1 -1
- package/_packed_docs/agent.md//350/277/234/347/250/213agent.md/347/274/223/345/255/230/344/270/216etag/351/200/217/344/274/240/346/226/271/346/241/210.md +73 -84
- package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +15 -14
- package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +2 -2
- package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +22 -5
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +42 -26
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +1 -1
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +61 -35
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +3 -3
- package/_packed_docs/sdk/09-message-rpc-manual.md +6 -6
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +6 -4
- package/_packed_docs/sdk/INDEX.md +2 -2
- package/_packed_docs/sdk/README.md +3 -3
- package/dist/agent-md.d.ts +111 -0
- package/dist/agent-md.d.ts.map +1 -0
- package/dist/agent-md.js +656 -0
- package/dist/agent-md.js.map +1 -0
- package/dist/aid-store.d.ts +7 -40
- package/dist/aid-store.d.ts.map +1 -1
- package/dist/aid-store.js +71 -171
- package/dist/aid-store.js.map +1 -1
- package/dist/auth.js +1 -1
- package/dist/auth.js.map +1 -1
- package/dist/bundle.js +4224 -4151
- package/dist/client.d.ts +3 -61
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +136 -703
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/keystore/index.d.ts +6 -2
- package/dist/keystore/index.d.ts.map +1 -1
- package/dist/keystore/indexeddb-identity-store.d.ts +59 -0
- package/dist/keystore/indexeddb-identity-store.d.ts.map +1 -0
- package/dist/keystore/indexeddb-identity-store.js +489 -0
- package/dist/keystore/indexeddb-identity-store.js.map +1 -0
- package/dist/keystore/indexeddb-shared.d.ts +76 -0
- package/dist/keystore/indexeddb-shared.d.ts.map +1 -0
- package/dist/keystore/indexeddb-shared.js +382 -0
- package/dist/keystore/indexeddb-shared.js.map +1 -0
- package/dist/keystore/indexeddb-token-store.d.ts +119 -0
- package/dist/keystore/indexeddb-token-store.d.ts.map +1 -0
- package/dist/keystore/indexeddb-token-store.js +1086 -0
- package/dist/keystore/indexeddb-token-store.js.map +1 -0
- package/dist/register-flow.d.ts +22 -3
- package/dist/register-flow.d.ts.map +1 -1
- package/dist/register-flow.js +49 -3
- package/dist/register-flow.js.map +1 -1
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// - HTTP 使用 fetch() 而非 Node http
|
|
5
5
|
// - 无文件系统(IndexedDB via keystore)
|
|
6
6
|
// - 后台任务使用 setTimeout/setInterval
|
|
7
|
-
import { createConfig, getDeviceId,
|
|
7
|
+
import { createConfig, getDeviceId, normalizeSlotId, slotIsolationKey } from './config.js';
|
|
8
8
|
import { EventDispatcher } from './events.js';
|
|
9
9
|
import { normalizeGroupId } from './group-id.js';
|
|
10
10
|
import { GatewayDiscovery } from './discovery.js';
|
|
@@ -12,12 +12,13 @@ import { RPCTransport } from './transport.js';
|
|
|
12
12
|
import { AuthFlow } from './auth.js';
|
|
13
13
|
import { SeqTracker } from './seq-tracker.js';
|
|
14
14
|
import { CryptoProvider, uint8ToBase64, base64ToUint8, pemToArrayBuffer, p1363ToDer, certificateSha256Fingerprint, ecdsaSignDer, ecdsaVerifyDer, importCertPublicKeyEcdsa, importPrivateKeyEcdsa, } from './crypto.js';
|
|
15
|
-
import {
|
|
15
|
+
import { IndexedDBTokenStore } from './keystore/indexeddb-token-store.js';
|
|
16
16
|
import { V2Session, V2KeyStore } from './v2/session/index.js';
|
|
17
17
|
import { encryptP2PMessage, encryptGroupMessage, decryptMessage, } from './v2/e2ee/index.js';
|
|
18
18
|
import { ecdsaVerifyRaw } from './v2/crypto/ecdsa.js';
|
|
19
19
|
import { computeStateCommitment } from './v2/state/index.js';
|
|
20
20
|
import { AUNLogger } from './logger.js';
|
|
21
|
+
import { AgentMdManager } from './agent-md.js';
|
|
21
22
|
import { AUNError, AuthError, ConnectionError, E2EEError, NotFoundError, PermissionError, StateError, ValidationError, } from './errors.js';
|
|
22
23
|
import { isJsonObject, ConnectionState, STATE_TO_PUBLIC, } from './types.js';
|
|
23
24
|
import { AID } from './aid.js';
|
|
@@ -239,7 +240,6 @@ function reconnectSleepDelaySeconds(baseDelay, maxBaseDelay) {
|
|
|
239
240
|
/** 对端证书缓存 TTL(秒) */
|
|
240
241
|
const PEER_CERT_CACHE_TTL = 3600;
|
|
241
242
|
const PEER_PREKEYS_CACHE_TTL = 3600;
|
|
242
|
-
const AGENT_MD_HTTP_TIMEOUT_MS = 30_000;
|
|
243
243
|
/**
|
|
244
244
|
* 将 WebSocket URL 转为对应的 HTTP URL
|
|
245
245
|
*/
|
|
@@ -272,28 +272,89 @@ function buildCertUrl(gatewayUrl, aid, certFingerprint) {
|
|
|
272
272
|
}
|
|
273
273
|
return url.toString();
|
|
274
274
|
}
|
|
275
|
-
function
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
async
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
275
|
+
function createAgentMdManagerForRuntime(opts) {
|
|
276
|
+
return new AgentMdManager({
|
|
277
|
+
aunPath: opts.config().aunPath,
|
|
278
|
+
tokenStore: opts.tokenStore(),
|
|
279
|
+
logger: opts.logger(),
|
|
280
|
+
ownerAidGetter: opts.ownerAid,
|
|
281
|
+
currentAidGetter: opts.currentAid,
|
|
282
|
+
gatewayResolver: async (aid) => {
|
|
283
|
+
const gatewayUrl = await opts.gateway.resolve(aid);
|
|
284
|
+
opts.gateway.set(gatewayUrl);
|
|
285
|
+
return gatewayUrl;
|
|
286
|
+
},
|
|
287
|
+
accessTokenResolver: async (aid, gatewayUrl) => {
|
|
288
|
+
const target = String(aid ?? '').trim();
|
|
289
|
+
let identity = await opts.auth().loadIdentityOrNone(target);
|
|
290
|
+
if (!identity && opts.identity.get() && String(opts.identity.get()?.aid ?? '') === target) {
|
|
291
|
+
identity = opts.identity.get();
|
|
292
|
+
}
|
|
293
|
+
if (!identity) {
|
|
294
|
+
throw new StateError('no local identity found, register or load an AID first');
|
|
295
|
+
}
|
|
296
|
+
const auth = opts.auth();
|
|
297
|
+
const cachedToken = String(identity.access_token ?? '');
|
|
298
|
+
const expiresAt = auth.getAccessTokenExpiry(identity);
|
|
299
|
+
if (cachedToken && (expiresAt === null || expiresAt > Date.now() / 1000 + 30)) {
|
|
300
|
+
return cachedToken;
|
|
301
|
+
}
|
|
302
|
+
if (identity.refresh_token) {
|
|
303
|
+
try {
|
|
304
|
+
const refreshed = await auth.refreshCachedTokens(gatewayUrl, identity);
|
|
305
|
+
const refreshedToken = String(refreshed.access_token ?? '');
|
|
306
|
+
const refreshedExpiry = auth.getAccessTokenExpiry(refreshed);
|
|
307
|
+
if (refreshedToken && (refreshedExpiry === null || refreshedExpiry > Date.now() / 1000 + 30)) {
|
|
308
|
+
opts.identity.set(refreshed);
|
|
309
|
+
return refreshedToken;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
// refresh 失败时回退到完整 authenticate。
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const result = await auth.authenticate(gatewayUrl, target);
|
|
317
|
+
const token = String(result.access_token ?? '');
|
|
318
|
+
if (!token)
|
|
319
|
+
throw new StateError('authenticate did not return access_token');
|
|
320
|
+
const fallbackIdentity = {
|
|
321
|
+
...identity,
|
|
322
|
+
access_token: token,
|
|
323
|
+
refresh_token: String(result.refresh_token ?? identity.refresh_token ?? ''),
|
|
324
|
+
};
|
|
325
|
+
const fallbackExpiresAt = Number(result.expires_at ?? identity.expires_at ?? NaN);
|
|
326
|
+
if (Number.isFinite(fallbackExpiresAt))
|
|
327
|
+
fallbackIdentity.expires_at = fallbackExpiresAt;
|
|
328
|
+
opts.identity.set(await auth.loadIdentityOrNone(target) ?? fallbackIdentity);
|
|
329
|
+
return token;
|
|
330
|
+
},
|
|
331
|
+
peerResolver: async (aid) => {
|
|
332
|
+
const target = String(aid ?? '').trim();
|
|
333
|
+
const current = opts.currentAid();
|
|
334
|
+
if (current?.aid === target)
|
|
335
|
+
return current;
|
|
336
|
+
let certPem = String(await opts.tokenStore().loadCert(target) ?? '').trim();
|
|
337
|
+
if (!certPem) {
|
|
338
|
+
if (!opts.gateway.get()) {
|
|
339
|
+
try {
|
|
340
|
+
opts.gateway.set(await opts.gateway.resolve(target));
|
|
341
|
+
}
|
|
342
|
+
catch { /* best effort */ }
|
|
343
|
+
}
|
|
344
|
+
certPem = String(await opts.fetchPeerCert(target) ?? '').trim();
|
|
345
|
+
}
|
|
346
|
+
if (!certPem)
|
|
347
|
+
throw new NotFoundError(`certificate not found for aid: ${target}`);
|
|
348
|
+
return await AID.create({
|
|
349
|
+
aid: target,
|
|
350
|
+
aunPath: opts.config().aunPath,
|
|
351
|
+
certPem,
|
|
352
|
+
privateKeyPem: null,
|
|
353
|
+
certValid: true,
|
|
354
|
+
privateKeyValid: false,
|
|
355
|
+
});
|
|
356
|
+
},
|
|
357
|
+
});
|
|
297
358
|
}
|
|
298
359
|
/**
|
|
299
360
|
* 跨域时将 Gateway URL 替换为 peer 所在域的 Gateway URL。
|
|
@@ -699,20 +760,8 @@ export class AUNClient {
|
|
|
699
760
|
/** 最近一次已成功确认的 membership_snapshot;相同快照直接跳过。 */
|
|
700
761
|
_v2AutoProposeLastSnapshot = new Map();
|
|
701
762
|
_v2LazyProposeTriggered = new Map();
|
|
702
|
-
/**
|
|
703
|
-
|
|
704
|
-
*
|
|
705
|
-
* 由 publishAgentMd() / fetchAgentMd(自身 aid) 写入;用于跟服务端 RPC 注入的 _meta.agent_md_etag
|
|
706
|
-
* 比对,触发"本地未发布到服务端"或"服务端版本更新"的 UI 提示。
|
|
707
|
-
*/
|
|
708
|
-
_localAgentMdEtag = '';
|
|
709
|
-
/** gateway 在 RPC envelope._meta.agent_md_etag 注入的服务端 etag;纯观察,无下游依赖。 */
|
|
710
|
-
_remoteAgentMdEtag = '';
|
|
711
|
-
/** 浏览器侧 AIDs 逻辑根目录,正文映射到 IndexedDB 里的 {aid}/agent.md。 */
|
|
712
|
-
_agentMdPath = '';
|
|
713
|
-
_agentMdCache = new Map();
|
|
714
|
-
_agentMdFetchInflight = new Set();
|
|
715
|
-
_agentMdLock = Promise.resolve();
|
|
763
|
+
/** agent.md 运行时管理器,负责上传、下载、缓存和 RPC 元数据观察。 */
|
|
764
|
+
_agentMdManager;
|
|
716
765
|
/** 消息序列号跟踪器(群消息 + P2P 空洞检测) */
|
|
717
766
|
_seqTracker = new SeqTracker();
|
|
718
767
|
_seqTrackerContext = null;
|
|
@@ -770,7 +819,6 @@ export class AUNClient {
|
|
|
770
819
|
root_ca_path: this.configModel.rootCaPem,
|
|
771
820
|
seed_password: this.configModel.seedPassword,
|
|
772
821
|
};
|
|
773
|
-
this._agentMdPath = this._agentMdDefaultRoot();
|
|
774
822
|
this._deviceId = (inputAid?.deviceId) || getDeviceId();
|
|
775
823
|
// Logger 必须最早初始化(其他子模块构造时通过 logger 输出)
|
|
776
824
|
this._logger = new AUNLogger({ debug: _debug, aunPath: this.configModel.aunPath });
|
|
@@ -784,7 +832,7 @@ export class AUNClient {
|
|
|
784
832
|
this._clientLog.info(`AUNClient initialized: debug=${_debug} aunPath=${this.configModel.aunPath} aid=${initAid ?? '-'}`);
|
|
785
833
|
this._dispatcher = new EventDispatcher();
|
|
786
834
|
this._discovery = new GatewayDiscovery();
|
|
787
|
-
this._tokenStore = new
|
|
835
|
+
this._tokenStore = new IndexedDBTokenStore();
|
|
788
836
|
this._slotId = inputAid?.slotId || 'default';
|
|
789
837
|
this._connectDeliveryMode = normalizeDeliveryModeConfig({ mode: 'fanout' });
|
|
790
838
|
this._defaultConnectDeliveryMode = { ...this._connectDeliveryMode };
|
|
@@ -798,6 +846,24 @@ export class AUNClient {
|
|
|
798
846
|
verifySsl: this.configModel.verifySsl,
|
|
799
847
|
});
|
|
800
848
|
this._aid = initAid;
|
|
849
|
+
this._agentMdManager = createAgentMdManagerForRuntime({
|
|
850
|
+
config: () => this.configModel,
|
|
851
|
+
logger: () => this._logger.for('aun_core.agent_md'),
|
|
852
|
+
ownerAid: () => this._aid,
|
|
853
|
+
currentAid: () => this._currentAid,
|
|
854
|
+
gateway: {
|
|
855
|
+
resolve: (target) => this._resolveGatewayForAid(target),
|
|
856
|
+
get: () => this._gatewayUrl,
|
|
857
|
+
set: (gatewayUrl) => { this._gatewayUrl = gatewayUrl; },
|
|
858
|
+
},
|
|
859
|
+
identity: {
|
|
860
|
+
get: () => this._identity,
|
|
861
|
+
set: (identity) => { this._identity = identity; },
|
|
862
|
+
},
|
|
863
|
+
auth: () => this._auth,
|
|
864
|
+
tokenStore: () => this._tokenStore,
|
|
865
|
+
fetchPeerCert: (target) => this._fetchPeerCert(target),
|
|
866
|
+
});
|
|
801
867
|
this._transport = new RPCTransport({
|
|
802
868
|
eventDispatcher: this._dispatcher,
|
|
803
869
|
timeout: DEFAULT_SESSION_OPTIONS.timeouts.call,
|
|
@@ -881,664 +947,15 @@ export class AUNClient {
|
|
|
881
947
|
get aid() {
|
|
882
948
|
return this._aid;
|
|
883
949
|
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
this._agentMdPath = next;
|
|
887
|
-
this._agentMdCache.clear();
|
|
888
|
-
return next;
|
|
889
|
-
}
|
|
890
|
-
async _resolveAgentMdUrl(aid) {
|
|
891
|
-
const target = String(aid ?? '').trim();
|
|
892
|
-
if (!target)
|
|
893
|
-
throw new ValidationError('agent.md requires non-empty aid');
|
|
894
|
-
let gatewayUrl = String(this._gatewayUrl ?? '').trim();
|
|
895
|
-
if (!gatewayUrl) {
|
|
896
|
-
try {
|
|
897
|
-
gatewayUrl = await this._resolveGatewayForAid(target);
|
|
898
|
-
}
|
|
899
|
-
catch {
|
|
900
|
-
gatewayUrl = '';
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
const authority = agentMdAuthority(target);
|
|
904
|
-
return `${agentMdHttpScheme(gatewayUrl)}://${authority}/agent.md`;
|
|
905
|
-
}
|
|
906
|
-
async _ensureAgentMdUploadToken(aid, gatewayUrl) {
|
|
907
|
-
let identity = await this._auth.loadIdentityOrNone(aid);
|
|
908
|
-
if (!identity && this._identity && String(this._identity.aid ?? '') === aid) {
|
|
909
|
-
identity = this._identity;
|
|
910
|
-
}
|
|
911
|
-
if (!identity) {
|
|
912
|
-
throw new StateError('no local identity found, register or load an AID first');
|
|
913
|
-
}
|
|
914
|
-
const cachedToken = String(identity.access_token ?? '');
|
|
915
|
-
const expiresAt = this._auth.getAccessTokenExpiry(identity);
|
|
916
|
-
if (cachedToken && (expiresAt === null || expiresAt > Date.now() / 1000 + 30)) {
|
|
917
|
-
return cachedToken;
|
|
918
|
-
}
|
|
919
|
-
if (identity.refresh_token) {
|
|
920
|
-
try {
|
|
921
|
-
const refreshed = await this._auth.refreshCachedTokens(gatewayUrl, identity);
|
|
922
|
-
const refreshedToken = String(refreshed.access_token ?? '');
|
|
923
|
-
const refreshedExpiry = this._auth.getAccessTokenExpiry(refreshed);
|
|
924
|
-
if (refreshedToken && (refreshedExpiry === null || refreshedExpiry > Date.now() / 1000 + 30)) {
|
|
925
|
-
this._identity = refreshed;
|
|
926
|
-
return refreshedToken;
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
catch {
|
|
930
|
-
// refresh 失败时回退到完整 authenticate。
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
const result = await this._auth.authenticate(gatewayUrl, aid);
|
|
934
|
-
const token = String(result.access_token ?? '');
|
|
935
|
-
if (!token)
|
|
936
|
-
throw new StateError('authenticate did not return access_token');
|
|
937
|
-
const fallbackIdentity = {
|
|
938
|
-
...identity,
|
|
939
|
-
access_token: token,
|
|
940
|
-
refresh_token: String(result.refresh_token ?? identity.refresh_token ?? ''),
|
|
941
|
-
};
|
|
942
|
-
const fallbackExpiresAt = Number(result.expires_at ?? identity.expires_at ?? NaN);
|
|
943
|
-
if (Number.isFinite(fallbackExpiresAt))
|
|
944
|
-
fallbackIdentity.expires_at = fallbackExpiresAt;
|
|
945
|
-
this._identity = await this._auth.loadIdentityOrNone(aid) ?? fallbackIdentity;
|
|
946
|
-
return token;
|
|
947
|
-
}
|
|
948
|
-
async _uploadAgentMd(content) {
|
|
949
|
-
const target = String(this._aid ?? this._currentAid?.aid ?? '').trim();
|
|
950
|
-
if (!target)
|
|
951
|
-
throw new StateError('uploadAgentMd requires local AID');
|
|
952
|
-
const gatewayUrl = await this._resolveGatewayForAid(target);
|
|
953
|
-
this._gatewayUrl = gatewayUrl;
|
|
954
|
-
const token = await this._ensureAgentMdUploadToken(target, gatewayUrl);
|
|
955
|
-
const response = await fetchWithTimeout(await this._resolveAgentMdUrl(target), {
|
|
956
|
-
method: 'PUT',
|
|
957
|
-
headers: {
|
|
958
|
-
Authorization: `Bearer ${token}`,
|
|
959
|
-
'Content-Type': 'text/markdown; charset=utf-8',
|
|
960
|
-
},
|
|
961
|
-
body: content,
|
|
962
|
-
});
|
|
963
|
-
if (response.status === 404) {
|
|
964
|
-
throw new NotFoundError(`agent.md endpoint not found for aid: ${target}`);
|
|
965
|
-
}
|
|
966
|
-
if (!response.ok) {
|
|
967
|
-
const message = (await response.text()).trim();
|
|
968
|
-
throw new AUNError(`upload agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`);
|
|
969
|
-
}
|
|
970
|
-
const payload = await response.json();
|
|
971
|
-
if (!isJsonObject(payload))
|
|
972
|
-
throw new AUNError('upload agent.md returned invalid JSON payload');
|
|
973
|
-
return payload;
|
|
974
|
-
}
|
|
975
|
-
async _downloadAgentMd(aid) {
|
|
976
|
-
const target = String(aid ?? '').trim();
|
|
977
|
-
if (!target)
|
|
978
|
-
throw new ValidationError('downloadAgentMd requires non-empty aid');
|
|
979
|
-
const cached = this._agentMdCache.get(target);
|
|
980
|
-
const url = await this._resolveAgentMdUrl(target);
|
|
981
|
-
const response = await fetchWithTimeout(url, {
|
|
982
|
-
method: 'GET',
|
|
983
|
-
headers: { Accept: 'text/markdown' },
|
|
984
|
-
redirect: 'follow',
|
|
985
|
-
});
|
|
986
|
-
if (response.status === 304 && typeof cached?.text === 'string') {
|
|
987
|
-
return String(cached.text);
|
|
988
|
-
}
|
|
989
|
-
if (response.status === 404) {
|
|
990
|
-
throw new NotFoundError(`agent.md not found for aid: ${target}`);
|
|
991
|
-
}
|
|
992
|
-
if (!response.ok) {
|
|
993
|
-
const message = (await response.text()).trim();
|
|
994
|
-
throw new AUNError(`download agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`);
|
|
995
|
-
}
|
|
996
|
-
const text = await response.text();
|
|
997
|
-
const etag = String(response.headers?.get('ETag') ?? response.headers?.get('etag') ?? '').trim();
|
|
998
|
-
const lastModified = String(response.headers?.get('Last-Modified') ?? response.headers?.get('last-modified') ?? '').trim();
|
|
999
|
-
this._agentMdCache.set(target, {
|
|
1000
|
-
...(cached ?? {}),
|
|
1001
|
-
text,
|
|
1002
|
-
etag,
|
|
1003
|
-
lastModified,
|
|
1004
|
-
remote_etag: etag,
|
|
1005
|
-
last_modified: lastModified,
|
|
1006
|
-
});
|
|
1007
|
-
return text;
|
|
1008
|
-
}
|
|
1009
|
-
async _headAgentMd(aid) {
|
|
1010
|
-
const target = String(aid ?? '').trim();
|
|
1011
|
-
if (!target)
|
|
1012
|
-
throw new ValidationError('headAgentMd requires non-empty aid');
|
|
1013
|
-
const response = await fetchWithTimeout(await this._resolveAgentMdUrl(target), {
|
|
1014
|
-
method: 'HEAD',
|
|
1015
|
-
headers: { Accept: 'text/markdown' },
|
|
1016
|
-
});
|
|
1017
|
-
const etag = String(response.headers?.get('ETag') ?? response.headers?.get('etag') ?? '').trim();
|
|
1018
|
-
const lastModified = String(response.headers?.get('Last-Modified') ?? response.headers?.get('last-modified') ?? '').trim();
|
|
1019
|
-
if (response.status === 404) {
|
|
1020
|
-
return { aid: target, found: false, etag: '', last_modified: '', status: 404 };
|
|
1021
|
-
}
|
|
1022
|
-
if (!response.ok) {
|
|
1023
|
-
throw new AUNError(`head agent.md failed: HTTP ${response.status}`);
|
|
1024
|
-
}
|
|
1025
|
-
const cached = this._agentMdCache.get(target) ?? {};
|
|
1026
|
-
this._agentMdCache.set(target, {
|
|
1027
|
-
...cached,
|
|
1028
|
-
etag,
|
|
1029
|
-
lastModified,
|
|
1030
|
-
remote_etag: etag,
|
|
1031
|
-
last_modified: lastModified,
|
|
1032
|
-
});
|
|
1033
|
-
return { aid: target, found: true, etag, last_modified: lastModified, status: response.status };
|
|
1034
|
-
}
|
|
1035
|
-
async _verifyAgentMd(content, aid) {
|
|
1036
|
-
const target = String(aid ?? '').trim();
|
|
1037
|
-
if (!target)
|
|
1038
|
-
throw new ValidationError('verifyAgentMd requires non-empty aid');
|
|
1039
|
-
let peer = target === this._currentAid?.aid ? this._currentAid : null;
|
|
1040
|
-
if (!peer) {
|
|
1041
|
-
let certPem = String(await this._tokenStore.loadCert(target) ?? '').trim();
|
|
1042
|
-
if (!certPem) {
|
|
1043
|
-
certPem = String(await this._fetchPeerCert(target) ?? '').trim();
|
|
1044
|
-
}
|
|
1045
|
-
if (!certPem)
|
|
1046
|
-
throw new NotFoundError(`certificate not found for aid: ${target}`);
|
|
1047
|
-
peer = await AID.create({
|
|
1048
|
-
aid: target,
|
|
1049
|
-
aunPath: this.configModel.aunPath,
|
|
1050
|
-
certPem,
|
|
1051
|
-
privateKeyPem: null,
|
|
1052
|
-
certValid: true,
|
|
1053
|
-
privateKeyValid: false,
|
|
1054
|
-
});
|
|
1055
|
-
}
|
|
1056
|
-
const result = await peer.verifyAgentMd(content);
|
|
1057
|
-
if (!result.ok)
|
|
1058
|
-
throw new AUNError(result.error.message);
|
|
1059
|
-
return { ...result.data, verified: result.data.status === 'verified' };
|
|
1060
|
-
}
|
|
1061
|
-
/**
|
|
1062
|
-
* 浏览器版本 publishAgentMd。默认从 {agentMdPath}/{self_aid}/agent.md 的等价 IndexedDB 正文读取,
|
|
1063
|
-
* 然后签名、上传,并刷新 agentmd.json 元数据。
|
|
1064
|
-
*
|
|
1065
|
-
* 兼容旧浏览器调用:传入 content 时会先写入等价正文,再从该正文发布。
|
|
1066
|
-
*/
|
|
1067
|
-
async publishAgentMd(content) {
|
|
1068
|
-
const target = this._agentMdOwnerAid();
|
|
1069
|
-
if (!target || !this._currentAid) {
|
|
1070
|
-
throw new ValidationError('publishAgentMd requires local AID');
|
|
1071
|
-
}
|
|
1072
|
-
if (content !== undefined && content !== null) {
|
|
1073
|
-
const text = String(content ?? '');
|
|
1074
|
-
if (text.length === 0) {
|
|
1075
|
-
throw new ValidationError('publishAgentMd requires non-empty content');
|
|
1076
|
-
}
|
|
1077
|
-
await this._saveAgentMdRecord(target, {
|
|
1078
|
-
content: text,
|
|
1079
|
-
local_etag: await this._agentMdContentEtag(text),
|
|
1080
|
-
fetched_at: Date.now(),
|
|
1081
|
-
});
|
|
1082
|
-
}
|
|
1083
|
-
const localContent = await this._readAgentMdContent(target);
|
|
1084
|
-
if (localContent === null || localContent.length === 0) {
|
|
1085
|
-
throw new ValidationError('publishAgentMd requires local agent.md content');
|
|
1086
|
-
}
|
|
1087
|
-
const signedResult = await this._currentAid?.signAgentMd(localContent);
|
|
1088
|
-
if (!signedResult?.ok) {
|
|
1089
|
-
throw new StateError(signedResult?.error.message ?? 'publishAgentMd requires a valid local AID private key');
|
|
1090
|
-
}
|
|
1091
|
-
const signed = signedResult.data.signed;
|
|
1092
|
-
const result = await this._uploadAgentMd(signed);
|
|
1093
|
-
this._localAgentMdEtag = await this._agentMdContentEtag(signed);
|
|
1094
|
-
const remoteEtag = isJsonObject(result) ? String(result.etag ?? '').trim() : '';
|
|
1095
|
-
if (remoteEtag)
|
|
1096
|
-
this._remoteAgentMdEtag = remoteEtag;
|
|
1097
|
-
await this._saveAgentMdRecord(target, {
|
|
1098
|
-
content: signed,
|
|
1099
|
-
local_etag: this._localAgentMdEtag,
|
|
1100
|
-
remote_etag: remoteEtag || undefined,
|
|
1101
|
-
last_modified: isJsonObject(result) ? String(result.last_modified ?? '').trim() : '',
|
|
1102
|
-
fetched_at: Date.now(),
|
|
1103
|
-
remote_status: remoteEtag ? 'found' : 'unknown',
|
|
1104
|
-
last_error: '',
|
|
1105
|
-
});
|
|
1106
|
-
return result;
|
|
1107
|
-
}
|
|
1108
|
-
/**
|
|
1109
|
-
* 浏览器版本 fetchAgentMd。aid 缺省时取自身;下载后的正文固定写入
|
|
1110
|
-
* {agentMdPath}/{aid}/agent.md 的等价 IndexedDB 正文,agentmd.json 只保存元数据。
|
|
1111
|
-
*/
|
|
1112
|
-
async _fetchAgentMdCache(aid) {
|
|
1113
|
-
const target = String(aid ?? this._aid ?? '').trim();
|
|
1114
|
-
if (!target) {
|
|
1115
|
-
throw new ValidationError('fetchAgentMd requires aid (or local AID)');
|
|
1116
|
-
}
|
|
1117
|
-
const content = await this._downloadAgentMd(target);
|
|
1118
|
-
const signature = await this._verifyAgentMd(content, target);
|
|
1119
|
-
const isSelf = target === (this._aid ?? '');
|
|
1120
|
-
const localEtag = await this._agentMdContentEtag(content);
|
|
1121
|
-
const cacheMeta = this._agentMdAuthCacheMeta(target);
|
|
1122
|
-
const remoteEtag = String(cacheMeta.etag ?? '').trim();
|
|
1123
|
-
const lastModified = String(cacheMeta.lastModified ?? cacheMeta.last_modified ?? '').trim();
|
|
1124
|
-
if (isSelf) {
|
|
1125
|
-
this._localAgentMdEtag = localEtag;
|
|
1126
|
-
if (remoteEtag)
|
|
1127
|
-
this._remoteAgentMdEtag = remoteEtag;
|
|
1128
|
-
}
|
|
1129
|
-
await this._saveAgentMdRecord(target, {
|
|
1130
|
-
content,
|
|
1131
|
-
local_etag: localEtag,
|
|
1132
|
-
remote_etag: remoteEtag || undefined,
|
|
1133
|
-
last_modified: lastModified || undefined,
|
|
1134
|
-
fetched_at: Date.now(),
|
|
1135
|
-
remote_status: 'found',
|
|
1136
|
-
verify_status: isJsonObject(signature) ? String(signature.status ?? '') : '',
|
|
1137
|
-
verify_error: isJsonObject(signature) ? String(signature.reason ?? '') : '',
|
|
1138
|
-
last_error: '',
|
|
1139
|
-
});
|
|
1140
|
-
let in_sync = null;
|
|
1141
|
-
if (isSelf) {
|
|
1142
|
-
const remote = remoteEtag || this._remoteAgentMdEtag || '';
|
|
1143
|
-
in_sync = localEtag && remote ? localEtag === remote : false;
|
|
1144
|
-
}
|
|
1145
|
-
return {
|
|
1146
|
-
aid: target,
|
|
1147
|
-
content,
|
|
1148
|
-
signature: signature,
|
|
1149
|
-
in_sync,
|
|
1150
|
-
};
|
|
1151
|
-
}
|
|
1152
|
-
getLocalAgentMdEtag() {
|
|
1153
|
-
return this._localAgentMdEtag;
|
|
1154
|
-
}
|
|
1155
|
-
getRemoteAgentMdEtag() {
|
|
1156
|
-
return this._remoteAgentMdEtag;
|
|
1157
|
-
}
|
|
1158
|
-
async _agentMdContentEtag(content) {
|
|
1159
|
-
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(String(content ?? '')));
|
|
1160
|
-
const hex = Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
1161
|
-
return `"${hex}"`;
|
|
1162
|
-
}
|
|
1163
|
-
_agentMdOwnerAid() {
|
|
1164
|
-
return String(this._aid ?? '').trim();
|
|
1165
|
-
}
|
|
1166
|
-
_agentMdDefaultRoot() {
|
|
1167
|
-
return this._joinAgentMdPath(this.configModel.aunPath || '.', 'AIDs');
|
|
1168
|
-
}
|
|
1169
|
-
_joinAgentMdPath(base, name) {
|
|
1170
|
-
const left = String(base ?? '').trim().replace(/[\\/]+$/g, '');
|
|
1171
|
-
return left ? `${left}/${name}` : name;
|
|
1172
|
-
}
|
|
1173
|
-
_agentMdRoot() {
|
|
1174
|
-
return this._agentMdPath || this._agentMdDefaultRoot();
|
|
1175
|
-
}
|
|
1176
|
-
_agentMdSafeAid(aid) {
|
|
1177
|
-
const target = String(aid ?? '').trim();
|
|
1178
|
-
if (!target || target.includes('/') || target.includes('\\') || target.includes('\0')) {
|
|
1179
|
-
throw new ValidationError('agent.md aid is empty or contains path separators');
|
|
1180
|
-
}
|
|
1181
|
-
return target;
|
|
1182
|
-
}
|
|
1183
|
-
_agentMdMetaKey(aid) {
|
|
1184
|
-
return `${this._agentMdSafeAid(aid)}/agentmd.json`;
|
|
1185
|
-
}
|
|
1186
|
-
_agentMdContentKey(aid) {
|
|
1187
|
-
return `${this._agentMdSafeAid(aid)}/agent.md`;
|
|
1188
|
-
}
|
|
1189
|
-
async _readAgentMdStorage(logicalKey) {
|
|
1190
|
-
const key = String(logicalKey ?? '').trim();
|
|
1191
|
-
if (!key)
|
|
1192
|
-
return null;
|
|
1193
|
-
const load = this._tokenStore.loadAgentMdCache;
|
|
1194
|
-
if (typeof load !== 'function') {
|
|
1195
|
-
throw new Error('IndexedDB agent.md storage unavailable');
|
|
1196
|
-
}
|
|
1197
|
-
const record = await load.call(this._tokenStore, this._agentMdRoot(), key);
|
|
1198
|
-
if (record && Object.prototype.hasOwnProperty.call(record, 'content')) {
|
|
1199
|
-
return String(record.content ?? '');
|
|
1200
|
-
}
|
|
1201
|
-
return null;
|
|
1202
|
-
}
|
|
1203
|
-
async _writeAgentMdStorage(logicalKey, content) {
|
|
1204
|
-
const key = String(logicalKey ?? '').trim();
|
|
1205
|
-
if (!key)
|
|
1206
|
-
return;
|
|
1207
|
-
const save = this._tokenStore.upsertAgentMdCache;
|
|
1208
|
-
if (typeof save !== 'function') {
|
|
1209
|
-
throw new Error('IndexedDB agent.md storage unavailable');
|
|
1210
|
-
}
|
|
1211
|
-
const text = String(content ?? '');
|
|
1212
|
-
await save.call(this._tokenStore, this._agentMdRoot(), key, {
|
|
1213
|
-
content: text,
|
|
1214
|
-
local_etag: await this._agentMdContentEtag(text),
|
|
1215
|
-
fetched_at: Date.now(),
|
|
1216
|
-
});
|
|
1217
|
-
}
|
|
1218
|
-
async _withAgentMdLock(fn) {
|
|
1219
|
-
const previous = this._agentMdLock.catch(() => undefined);
|
|
1220
|
-
let release;
|
|
1221
|
-
const current = new Promise((resolve) => { release = resolve; });
|
|
1222
|
-
this._agentMdLock = previous.then(() => current);
|
|
1223
|
-
await previous;
|
|
1224
|
-
try {
|
|
1225
|
-
return await fn();
|
|
1226
|
-
}
|
|
1227
|
-
finally {
|
|
1228
|
-
release();
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
_normalizeAgentMdRecord(aid, data) {
|
|
1232
|
-
if (!isJsonObject(data))
|
|
1233
|
-
return {};
|
|
1234
|
-
const record = {};
|
|
1235
|
-
for (const [key, value] of Object.entries(data)) {
|
|
1236
|
-
if (key !== 'content')
|
|
1237
|
-
record[key] = value;
|
|
1238
|
-
}
|
|
1239
|
-
record.aid = this._agentMdSafeAid(String(record.aid ?? aid));
|
|
1240
|
-
for (const key of ['fetched_at', 'observed_at', 'checked_at', 'updated_at']) {
|
|
1241
|
-
record[key] = Number(record[key] ?? 0) || 0;
|
|
1242
|
-
}
|
|
1243
|
-
return record;
|
|
1244
|
-
}
|
|
1245
|
-
async _writeAgentMdRecordUnlocked(aid, record) {
|
|
1246
|
-
const payload = {};
|
|
1247
|
-
for (const [key, value] of Object.entries(record)) {
|
|
1248
|
-
if (key !== 'content' && value !== undefined && value !== null)
|
|
1249
|
-
payload[key] = value;
|
|
1250
|
-
}
|
|
1251
|
-
payload.aid = this._agentMdSafeAid(aid);
|
|
1252
|
-
await this._writeAgentMdStorage(this._agentMdMetaKey(aid), `${JSON.stringify(payload, null, 2)}\n`);
|
|
1253
|
-
}
|
|
1254
|
-
async _readAgentMdRecordUnlocked(aid) {
|
|
1255
|
-
const raw = await this._readAgentMdStorage(this._agentMdMetaKey(aid));
|
|
1256
|
-
if (raw === null)
|
|
1257
|
-
return {};
|
|
1258
|
-
try {
|
|
1259
|
-
return this._normalizeAgentMdRecord(aid, JSON.parse(raw));
|
|
1260
|
-
}
|
|
1261
|
-
catch (err) {
|
|
1262
|
-
this._clientLog.warn(`agent.md metadata damaged, ignoring: aid=${aid} err=${err instanceof Error ? err.message : String(err)}`);
|
|
1263
|
-
return {};
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
async _readAgentMdContent(aid) {
|
|
1267
|
-
return await this._readAgentMdStorage(this._agentMdContentKey(aid));
|
|
1268
|
-
}
|
|
1269
|
-
async _writeAgentMdContent(aid, content) {
|
|
1270
|
-
await this._writeAgentMdStorage(this._agentMdContentKey(aid), String(content ?? ''));
|
|
1271
|
-
}
|
|
1272
|
-
_agentMdAuthCacheMeta(aid) {
|
|
1273
|
-
try {
|
|
1274
|
-
const record = this._agentMdCache.get(String(aid ?? '').trim());
|
|
1275
|
-
return record && typeof record === 'object' ? { ...record } : {};
|
|
1276
|
-
}
|
|
1277
|
-
catch {
|
|
1278
|
-
return {};
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
async _loadAgentMdRecord(aid) {
|
|
1282
|
-
const target = String(aid ?? '').trim();
|
|
1283
|
-
if (!target)
|
|
1284
|
-
return null;
|
|
1285
|
-
try {
|
|
1286
|
-
const loaded = await this._withAgentMdLock(async () => {
|
|
1287
|
-
const record = await this._readAgentMdRecordUnlocked(target);
|
|
1288
|
-
const next = Object.keys(record).length > 0 ? { ...record, aid: target } : { aid: target };
|
|
1289
|
-
try {
|
|
1290
|
-
const content = await this._readAgentMdContent(target);
|
|
1291
|
-
if (content !== null) {
|
|
1292
|
-
next.content = content;
|
|
1293
|
-
next.local_etag = await this._agentMdContentEtag(content);
|
|
1294
|
-
}
|
|
1295
|
-
else {
|
|
1296
|
-
// 元数据存在但正文缺失
|
|
1297
|
-
const metaRaw = await this._readAgentMdStorage(this._agentMdMetaKey(target));
|
|
1298
|
-
if (metaRaw !== null) {
|
|
1299
|
-
this._clientLog.warn(`agent.md content read failed: aid=${target}`);
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
catch (err) {
|
|
1304
|
-
this._clientLog.warn(`agent.md content read failed: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
|
|
1305
|
-
}
|
|
1306
|
-
return next;
|
|
1307
|
-
});
|
|
1308
|
-
if (Object.keys(loaded).length <= 1)
|
|
1309
|
-
return null;
|
|
1310
|
-
this._agentMdCache.set(target, { ...loaded });
|
|
1311
|
-
return { ...loaded };
|
|
1312
|
-
}
|
|
1313
|
-
catch (err) {
|
|
1314
|
-
this._clientLog.debug(`agent.md cache load skipped: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
|
|
1315
|
-
}
|
|
1316
|
-
return null;
|
|
1317
|
-
}
|
|
1318
|
-
async _saveAgentMdRecord(aid, fields) {
|
|
1319
|
-
const target = String(aid ?? '').trim();
|
|
1320
|
-
if (!target)
|
|
1321
|
-
return {};
|
|
1322
|
-
try {
|
|
1323
|
-
const inputFields = { ...fields };
|
|
1324
|
-
const hasContent = Object.prototype.hasOwnProperty.call(inputFields, 'content') && inputFields.content !== undefined && inputFields.content !== null;
|
|
1325
|
-
if (hasContent) {
|
|
1326
|
-
const text = String(inputFields.content ?? '');
|
|
1327
|
-
await this._writeAgentMdContent(target, text);
|
|
1328
|
-
if (!inputFields.local_etag)
|
|
1329
|
-
inputFields.local_etag = await this._agentMdContentEtag(text);
|
|
1330
|
-
if (!inputFields.fetched_at)
|
|
1331
|
-
inputFields.fetched_at = Date.now();
|
|
1332
|
-
}
|
|
1333
|
-
delete inputFields.content;
|
|
1334
|
-
const record = await this._withAgentMdLock(async () => {
|
|
1335
|
-
const next = { ...(await this._readAgentMdRecordUnlocked(target)), aid: target };
|
|
1336
|
-
for (const [key, value] of Object.entries(inputFields)) {
|
|
1337
|
-
if (value !== undefined && value !== null)
|
|
1338
|
-
next[key] = value;
|
|
1339
|
-
}
|
|
1340
|
-
next.updated_at = Date.now();
|
|
1341
|
-
await this._writeAgentMdRecordUnlocked(target, next);
|
|
1342
|
-
return next;
|
|
1343
|
-
});
|
|
1344
|
-
const loaded = { ...record };
|
|
1345
|
-
if (hasContent)
|
|
1346
|
-
loaded.content = String(fields.content ?? '');
|
|
1347
|
-
this._agentMdCache.set(target, { ...loaded });
|
|
1348
|
-
const owner = this._agentMdOwnerAid();
|
|
1349
|
-
if (target === owner) {
|
|
1350
|
-
const localEtag = String(loaded.local_etag ?? '').trim();
|
|
1351
|
-
const remoteEtag = String(loaded.remote_etag ?? '').trim();
|
|
1352
|
-
if (localEtag)
|
|
1353
|
-
this._localAgentMdEtag = localEtag;
|
|
1354
|
-
if (remoteEtag)
|
|
1355
|
-
this._remoteAgentMdEtag = remoteEtag;
|
|
1356
|
-
}
|
|
1357
|
-
return { ...loaded };
|
|
1358
|
-
}
|
|
1359
|
-
catch (err) {
|
|
1360
|
-
this._clientLog.debug(`agent.md cache save skipped: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
|
|
1361
|
-
}
|
|
1362
|
-
return {};
|
|
1363
|
-
}
|
|
1364
|
-
async _agentMdHasLocalContent(aid, record) {
|
|
1365
|
-
if (record && typeof record.content === 'string' && record.content.length > 0)
|
|
1366
|
-
return true;
|
|
1367
|
-
try {
|
|
1368
|
-
return (await this._readAgentMdContent(aid)) !== null;
|
|
1369
|
-
}
|
|
1370
|
-
catch {
|
|
1371
|
-
return false;
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
_agentMdCheckedAtFresh(checkedAtMs, maxUnsyncedDays) {
|
|
1375
|
-
const days = Number(maxUnsyncedDays || 0);
|
|
1376
|
-
if (!Number.isFinite(days) || days <= 0)
|
|
1377
|
-
return false;
|
|
1378
|
-
if (!Number.isFinite(checkedAtMs) || checkedAtMs <= 0)
|
|
1379
|
-
return false;
|
|
1380
|
-
return Date.now() - checkedAtMs <= days * 86400000;
|
|
1381
|
-
}
|
|
1382
|
-
_agentMdLastModifiedFresh(lastModified, maxUnsyncedDays) {
|
|
1383
|
-
const days = Number(maxUnsyncedDays || 0);
|
|
1384
|
-
if (!Number.isFinite(days) || days <= 0)
|
|
1385
|
-
return false;
|
|
1386
|
-
const ts = Date.parse(String(lastModified ?? '').trim());
|
|
1387
|
-
if (!Number.isFinite(ts))
|
|
1388
|
-
return false;
|
|
1389
|
-
return Date.now() <= ts + days * 86400000;
|
|
1390
|
-
}
|
|
1391
|
-
async _scheduleAgentMdFetchIfMissing(aid, record, source = '') {
|
|
1392
|
-
const target = String(aid ?? '').trim();
|
|
1393
|
-
if (!target || await this._agentMdHasLocalContent(target, record))
|
|
1394
|
-
return;
|
|
1395
|
-
if (this._agentMdFetchInflight.has(target))
|
|
1396
|
-
return;
|
|
1397
|
-
this._agentMdFetchInflight.add(target);
|
|
1398
|
-
try {
|
|
1399
|
-
await this._fetchAgentMdCache(target);
|
|
1400
|
-
}
|
|
1401
|
-
catch (err) {
|
|
1402
|
-
await this._saveAgentMdRecord(target, {
|
|
1403
|
-
last_error: err instanceof Error ? err.message : String(err),
|
|
1404
|
-
remote_status: 'found',
|
|
1405
|
-
});
|
|
1406
|
-
this._clientLog.debug(`agent.md auto fetch failed: aid=${target} source=${source || '-'} err=${err instanceof Error ? err.message : String(err)}`);
|
|
1407
|
-
}
|
|
1408
|
-
finally {
|
|
1409
|
-
this._agentMdFetchInflight.delete(target);
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
async _observeAgentMdMeta(aid, etag = '', lastModified = '', source = '') {
|
|
1413
|
-
const target = String(aid ?? '').trim();
|
|
1414
|
-
const remoteEtag = String(etag ?? '').trim();
|
|
1415
|
-
const remoteLastModified = String(lastModified ?? '').trim();
|
|
1416
|
-
if (!target || (!remoteEtag && !remoteLastModified))
|
|
1417
|
-
return;
|
|
1418
|
-
let before = this._agentMdCache.get(target);
|
|
1419
|
-
if (!before || typeof before !== 'object')
|
|
1420
|
-
before = await this._loadAgentMdRecord(target) ?? {};
|
|
1421
|
-
const same = (!remoteEtag || String(before.remote_etag ?? '').trim() === remoteEtag) &&
|
|
1422
|
-
(!remoteLastModified || String(before.last_modified ?? '').trim() === remoteLastModified);
|
|
1423
|
-
let record = { ...before };
|
|
1424
|
-
if (!same || Object.keys(before).length === 0) {
|
|
1425
|
-
const fields = {
|
|
1426
|
-
observed_at: Date.now(),
|
|
1427
|
-
remote_status: 'found',
|
|
1428
|
-
};
|
|
1429
|
-
if (remoteEtag)
|
|
1430
|
-
fields.remote_etag = remoteEtag;
|
|
1431
|
-
if (remoteLastModified)
|
|
1432
|
-
fields.last_modified = remoteLastModified;
|
|
1433
|
-
record = await this._saveAgentMdRecord(target, fields) || record;
|
|
1434
|
-
}
|
|
1435
|
-
if (target === this._agentMdOwnerAid() && remoteEtag)
|
|
1436
|
-
this._remoteAgentMdEtag = remoteEtag;
|
|
1437
|
-
await this._scheduleAgentMdFetchIfMissing(target, record, source);
|
|
1438
|
-
this._clientLog.debug(`agent.md meta observed: aid=${target} etag=${remoteEtag || '-'} last_modified=${remoteLastModified || '-'} source=${source || '-'}`);
|
|
1439
|
-
}
|
|
1440
|
-
async _observeAgentMdEtag(aid, etag, source = '') {
|
|
1441
|
-
await this._observeAgentMdMeta(aid, etag, '', source);
|
|
950
|
+
async uploadAgentMd(content) {
|
|
951
|
+
return await this._agentMdManager.upload(content);
|
|
1442
952
|
}
|
|
1443
953
|
async _observeAgentMdFromEnvelope(envelope) {
|
|
1444
|
-
|
|
1445
|
-
return;
|
|
1446
|
-
const env = envelope;
|
|
1447
|
-
if (!isJsonObject(env.agent_md))
|
|
1448
|
-
return;
|
|
1449
|
-
const agentMd = env.agent_md;
|
|
1450
|
-
if (!isJsonObject(agentMd.sender))
|
|
1451
|
-
return;
|
|
1452
|
-
const sender = agentMd.sender;
|
|
1453
|
-
let senderAid = String(sender.aid ?? '').trim();
|
|
1454
|
-
if (!senderAid) {
|
|
1455
|
-
const aad = isJsonObject(env.aad) ? env.aad : {};
|
|
1456
|
-
senderAid = String(aad.from ?? env.from ?? '').trim();
|
|
1457
|
-
}
|
|
1458
|
-
await this._observeAgentMdMeta(senderAid, String(sender.etag ?? '').trim(), String(sender.last_modified ?? sender.lastModified ?? '').trim(), 'envelope');
|
|
1459
|
-
}
|
|
1460
|
-
async _checkAgentMdCache(aid, maxUnsyncedDays = 0) {
|
|
1461
|
-
const target = String(aid ?? this._aid ?? '').trim();
|
|
1462
|
-
if (!target)
|
|
1463
|
-
throw new ValidationError('checkAgentMd requires aid (or local AID)');
|
|
1464
|
-
const before = await this._loadAgentMdRecord(target) ?? {};
|
|
1465
|
-
const localEtag = String(before.local_etag ?? '').trim();
|
|
1466
|
-
const localFound = !!(Object.keys(before).length > 0 && (String(before.content ?? '') || localEtag));
|
|
1467
|
-
const remoteEtagCached = String(before.remote_etag ?? '').trim();
|
|
1468
|
-
const lastModifiedCached = String(before.last_modified ?? '').trim();
|
|
1469
|
-
const checkedAtCached = Number(before.checked_at ?? 0);
|
|
1470
|
-
const cachedInSync = !!(localFound && localEtag && remoteEtagCached && localEtag === remoteEtagCached);
|
|
1471
|
-
// max_unsynced_days > 0 且距上次 HEAD 在窗口内 → 直接返回缓存;否则强制 HEAD。
|
|
1472
|
-
if (cachedInSync && this._agentMdCheckedAtFresh(checkedAtCached, maxUnsyncedDays)) {
|
|
1473
|
-
return {
|
|
1474
|
-
aid: target,
|
|
1475
|
-
local_found: true,
|
|
1476
|
-
remote_found: true,
|
|
1477
|
-
local_etag: localEtag,
|
|
1478
|
-
remote_etag: remoteEtagCached,
|
|
1479
|
-
in_sync: true,
|
|
1480
|
-
last_modified: lastModifiedCached,
|
|
1481
|
-
status: 200,
|
|
1482
|
-
cached: true,
|
|
1483
|
-
verify_status: String(before.verify_status ?? ''),
|
|
1484
|
-
verify_error: String(before.verify_error ?? ''),
|
|
1485
|
-
};
|
|
1486
|
-
}
|
|
1487
|
-
const now = Date.now();
|
|
1488
|
-
let remote;
|
|
1489
|
-
try {
|
|
1490
|
-
remote = await this._headAgentMd(target);
|
|
1491
|
-
}
|
|
1492
|
-
catch (err) {
|
|
1493
|
-
await this._saveAgentMdRecord(target, { checked_at: now, remote_status: 'error', last_error: err instanceof Error ? err.message : String(err) });
|
|
1494
|
-
throw err;
|
|
1495
|
-
}
|
|
1496
|
-
const remoteFound = !!remote.found;
|
|
1497
|
-
const remoteEtag = String(remote.etag ?? '').trim();
|
|
1498
|
-
const lastModified = String(remote.last_modified ?? remote.lastModified ?? '').trim();
|
|
1499
|
-
const saved = await this._saveAgentMdRecord(target, {
|
|
1500
|
-
remote_etag: remoteFound ? remoteEtag : '',
|
|
1501
|
-
last_modified: lastModified,
|
|
1502
|
-
checked_at: now,
|
|
1503
|
-
remote_status: remoteFound ? 'found' : 'missing',
|
|
1504
|
-
last_error: '',
|
|
1505
|
-
});
|
|
1506
|
-
if (target === this._agentMdOwnerAid() && remoteEtag)
|
|
1507
|
-
this._remoteAgentMdEtag = remoteEtag;
|
|
1508
|
-
const inSync = !!(localFound && remoteFound && localEtag && remoteEtag && localEtag === remoteEtag);
|
|
1509
|
-
return {
|
|
1510
|
-
aid: target,
|
|
1511
|
-
local_found: localFound,
|
|
1512
|
-
remote_found: remoteFound,
|
|
1513
|
-
local_etag: localEtag,
|
|
1514
|
-
remote_etag: remoteEtag,
|
|
1515
|
-
in_sync: inSync,
|
|
1516
|
-
last_modified: lastModified,
|
|
1517
|
-
status: Number(remote.status ?? (remoteFound ? 200 : 404)),
|
|
1518
|
-
cached: false,
|
|
1519
|
-
verify_status: String(saved.verify_status ?? before.verify_status ?? ''),
|
|
1520
|
-
verify_error: String(saved.verify_error ?? before.verify_error ?? ''),
|
|
1521
|
-
};
|
|
954
|
+
await this._agentMdManager.observeEnvelope(envelope);
|
|
1522
955
|
}
|
|
1523
956
|
/** transport 的 meta observer:吸收 gateway 注入的 _meta 字段。失败不影响业务。 */
|
|
1524
957
|
async _observeRpcMeta(meta) {
|
|
1525
|
-
|
|
1526
|
-
return;
|
|
1527
|
-
const etag = String(meta.agent_md_etag ?? '').trim();
|
|
1528
|
-
if (etag) {
|
|
1529
|
-
this._remoteAgentMdEtag = etag;
|
|
1530
|
-
await this._observeAgentMdMeta(this._aid ?? '', etag, '', 'rpc.self');
|
|
1531
|
-
}
|
|
1532
|
-
const etags = meta.agent_md_etags;
|
|
1533
|
-
if (isJsonObject(etags)) {
|
|
1534
|
-
// role key 优先级:requester / peer 是新规范,其余是兼容旧 SDK 的别名。
|
|
1535
|
-
for (const key of ['requester', 'peer', 'receiver', 'target', 'to', 'sender', 'from']) {
|
|
1536
|
-
const item = etags[key];
|
|
1537
|
-
if (!isJsonObject(item))
|
|
1538
|
-
continue;
|
|
1539
|
-
await this._observeAgentMdMeta(String(item.aid ?? ''), String(item.etag ?? ''), String(item.last_modified ?? item.lastModified ?? ''), `rpc.${key}`);
|
|
1540
|
-
}
|
|
1541
|
-
}
|
|
958
|
+
await this._agentMdManager.observeRpcMeta(meta, this._aid);
|
|
1542
959
|
}
|
|
1543
960
|
get state() {
|
|
1544
961
|
return this._publicState(this._state);
|
|
@@ -1598,9 +1015,6 @@ export class AUNClient {
|
|
|
1598
1015
|
this.config.aun_path = nextConfig.aunPath;
|
|
1599
1016
|
this.config.root_ca_path = nextConfig.rootCaPem;
|
|
1600
1017
|
this.config.seed_password = nextConfig.seedPassword;
|
|
1601
|
-
this._agentMdPath = this._agentMdDefaultRoot();
|
|
1602
|
-
this._agentMdCache.clear();
|
|
1603
|
-
this._agentMdFetchInflight.clear();
|
|
1604
1018
|
this._peerCache.clear();
|
|
1605
1019
|
this._certCache.clear();
|
|
1606
1020
|
this._gatewayUrl = null;
|
|
@@ -1615,7 +1029,7 @@ export class AUNClient {
|
|
|
1615
1029
|
this._logDiscovery = this._logger.for('aun_core.discovery');
|
|
1616
1030
|
this._logEvents = this._logger.for('aun_core.events');
|
|
1617
1031
|
this._discovery = new GatewayDiscovery();
|
|
1618
|
-
this._tokenStore = new
|
|
1032
|
+
this._tokenStore = new IndexedDBTokenStore();
|
|
1619
1033
|
this._auth = new AuthFlow({
|
|
1620
1034
|
tokenStore: this._tokenStore,
|
|
1621
1035
|
crypto: new CryptoProvider(),
|
|
@@ -1625,6 +1039,24 @@ export class AUNClient {
|
|
|
1625
1039
|
rootCaPem: nextConfig.rootCaPem,
|
|
1626
1040
|
verifySsl: nextConfig.verifySsl,
|
|
1627
1041
|
});
|
|
1042
|
+
this._agentMdManager = createAgentMdManagerForRuntime({
|
|
1043
|
+
config: () => this.configModel,
|
|
1044
|
+
logger: () => this._logger.for('aun_core.agent_md'),
|
|
1045
|
+
ownerAid: () => this._aid,
|
|
1046
|
+
currentAid: () => this._currentAid,
|
|
1047
|
+
gateway: {
|
|
1048
|
+
resolve: (target) => this._resolveGatewayForAid(target),
|
|
1049
|
+
get: () => this._gatewayUrl,
|
|
1050
|
+
set: (gatewayUrl) => { this._gatewayUrl = gatewayUrl; },
|
|
1051
|
+
},
|
|
1052
|
+
identity: {
|
|
1053
|
+
get: () => this._identity,
|
|
1054
|
+
set: (identity) => { this._identity = identity; },
|
|
1055
|
+
},
|
|
1056
|
+
auth: () => this._auth,
|
|
1057
|
+
tokenStore: () => this._tokenStore,
|
|
1058
|
+
fetchPeerCert: (target) => this._fetchPeerCert(target),
|
|
1059
|
+
});
|
|
1628
1060
|
this._transport = new RPCTransport({
|
|
1629
1061
|
eventDispatcher: this._dispatcher,
|
|
1630
1062
|
timeout: DEFAULT_SESSION_OPTIONS.timeouts.call,
|
|
@@ -2759,8 +2191,9 @@ export class AUNClient {
|
|
|
2759
2191
|
// 注入本地/远端 agent.md etag,让应用层判断版本一致性;失败不影响业务。
|
|
2760
2192
|
if (isJsonObject(payload)) {
|
|
2761
2193
|
try {
|
|
2762
|
-
const
|
|
2763
|
-
const
|
|
2194
|
+
const snapshot = this._agentMdManager.eventSnapshot();
|
|
2195
|
+
const localEtag = snapshot?.local_etag || '';
|
|
2196
|
+
const remoteEtag = snapshot?.remote_etag || '';
|
|
2764
2197
|
if ((localEtag || remoteEtag) && payload._agent_md === undefined) {
|
|
2765
2198
|
payload._agent_md = {
|
|
2766
2199
|
local_etag: localEtag,
|
|
@@ -4208,8 +3641,8 @@ export class AUNClient {
|
|
|
4208
3641
|
if ('device_id' in params && String(params.device_id ?? '').trim() !== this._deviceId) {
|
|
4209
3642
|
throw new ValidationError('message.pull/message.ack device_id must match the current client instance');
|
|
4210
3643
|
}
|
|
4211
|
-
const slotId =
|
|
4212
|
-
if (slotId !== this._slotId) {
|
|
3644
|
+
const slotId = normalizeSlotId(params.slot_id ?? this._slotId, this._slotId);
|
|
3645
|
+
if (slotIsolationKey(slotId) !== slotIsolationKey(this._slotId)) {
|
|
4213
3646
|
throw new ValidationError('message.pull/message.ack slot_id must match the current client instance');
|
|
4214
3647
|
}
|
|
4215
3648
|
params.device_id = this._deviceId;
|
|
@@ -4719,7 +4152,7 @@ export class AUNClient {
|
|
|
4719
4152
|
async _initV2Session() {
|
|
4720
4153
|
if (!this._aid)
|
|
4721
4154
|
return;
|
|
4722
|
-
//
|
|
4155
|
+
// 私钥来自当前 AID 值对象,AUNClient 不从持久化存储读取私钥。
|
|
4723
4156
|
const currentAid = this._currentAid;
|
|
4724
4157
|
if (!currentAid?.privateKeyPem) {
|
|
4725
4158
|
this._clientLog.warn('V2 session init skipped: no AID private key');
|