@agentunion/fastaun-browser 0.5.17 → 0.5.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +83 -0
- package/_packed_docs/CHANGELOG.md +83 -0
- package/_packed_docs/INDEX.md +163 -16
- package/_packed_docs/KITE_DOCS_GUIDE.md +52 -20
- package/_packed_docs/agent.md/SCHEMA.md +266 -207
- package/_packed_docs/aun/345/210/206/345/270/203/345/274/217/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +64 -31
- package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +8 -6
- package/_packed_docs/protocol/01-/350/272/253/344/273/275/344/270/216/345/207/255/350/257/201/345/215/217/350/256/256-auth.md +39 -39
- package/_packed_docs/protocol/07-/351/224/231/350/257/257/347/240/201/344/270/216/347/212/266/346/200/201/346/234/272.md +1 -1
- package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +28 -9
- package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +20 -2
- package/_packed_docs/protocol/agent.md/SCHEMA.md +4 -0
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +5 -2
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +246 -241
- package/_packed_docs/sdk/09-group-rpc-manual.md +46 -14
- package/_packed_docs/sdk/09-message-rpc-manual.md +40 -2
- package/_packed_docs/sdk/09-storage-rpc-manual.md +69 -38
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +25 -5
- package/_packed_docs/sdk/INDEX.md +78 -18
- package/dist/agent-md-schema.d.ts +3 -0
- package/dist/agent-md-schema.d.ts.map +1 -1
- package/dist/agent-md-schema.js +46 -10
- package/dist/agent-md-schema.js.map +1 -1
- package/dist/agent-md.d.ts +44 -1
- package/dist/agent-md.d.ts.map +1 -1
- package/dist/agent-md.js +518 -26
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +12 -1
- package/dist/aid-store.d.ts.map +1 -1
- package/dist/aid-store.js +113 -18
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.d.ts +17 -1
- package/dist/aid.d.ts.map +1 -1
- package/dist/aid.js +69 -9
- package/dist/aid.js.map +1 -1
- package/dist/auth.d.ts +4 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +343 -23
- package/dist/auth.js.map +1 -1
- package/dist/bundle.js +37513 -31501
- package/dist/cert-utils.d.ts +1 -1
- package/dist/cert-utils.d.ts.map +1 -1
- package/dist/cert-utils.js +24 -10
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +16 -1
- package/dist/client/delivery.d.ts.map +1 -1
- package/dist/client/delivery.js +198 -58
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts.map +1 -1
- package/dist/client/rpc-pipeline.js +28 -6
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +1 -0
- package/dist/client/v2-e2ee.d.ts.map +1 -1
- package/dist/client/v2-e2ee.js +858 -484
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +100 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +1216 -24
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +3 -2
- package/dist/errors.js.map +1 -1
- package/dist/events.d.ts +4 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +41 -1
- package/dist/events.js.map +1 -1
- package/dist/facades.d.ts +84 -2
- package/dist/facades.d.ts.map +1 -1
- package/dist/facades.js +104 -4
- package/dist/facades.js.map +1 -1
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/keystore/index.d.ts +17 -0
- package/dist/keystore/index.d.ts.map +1 -1
- package/dist/keystore/indexeddb-shared.d.ts +10 -1
- package/dist/keystore/indexeddb-shared.d.ts.map +1 -1
- package/dist/keystore/indexeddb-shared.js +111 -0
- package/dist/keystore/indexeddb-shared.js.map +1 -1
- package/dist/keystore/indexeddb-token-store.d.ts +7 -1
- package/dist/keystore/indexeddb-token-store.d.ts.map +1 -1
- package/dist/keystore/indexeddb-token-store.js +30 -1
- package/dist/keystore/indexeddb-token-store.js.map +1 -1
- package/dist/org-protocol.d.ts +98 -0
- package/dist/org-protocol.d.ts.map +1 -0
- package/dist/org-protocol.js +1742 -0
- package/dist/org-protocol.js.map +1 -0
- package/dist/ownership-proof.d.ts +118 -0
- package/dist/ownership-proof.d.ts.map +1 -0
- package/dist/ownership-proof.js +283 -0
- package/dist/ownership-proof.js.map +1 -0
- package/dist/ownership-v21.d.ts +177 -0
- package/dist/ownership-v21.d.ts.map +1 -0
- package/dist/ownership-v21.js +609 -0
- package/dist/ownership-v21.js.map +1 -0
- package/dist/ownership-workflow.d.ts +146 -0
- package/dist/ownership-workflow.d.ts.map +1 -0
- package/dist/ownership-workflow.js +1066 -0
- package/dist/ownership-workflow.js.map +1 -0
- package/dist/seq-tracker.d.ts +1 -0
- package/dist/seq-tracker.d.ts.map +1 -1
- package/dist/seq-tracker.js +10 -2
- package/dist/seq-tracker.js.map +1 -1
- package/dist/tools/cross-sdk-agent.d.ts +82 -0
- package/dist/tools/cross-sdk-agent.d.ts.map +1 -1
- package/dist/tools/cross-sdk-agent.js +332 -7
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +6 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +88 -2
- package/dist/transport.js.map +1 -1
- package/dist/v2/e2ee/decrypt.d.ts +1 -0
- package/dist/v2/e2ee/decrypt.d.ts.map +1 -1
- package/dist/v2/e2ee/decrypt.js +9 -1
- package/dist/v2/e2ee/decrypt.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -5
package/dist/client.js
CHANGED
|
@@ -9,6 +9,7 @@ import { EventDispatcher } from './events.js';
|
|
|
9
9
|
import { normalizeGroupId } from './group-id.js';
|
|
10
10
|
import { validateAIDFormat, validateGroupIDFormat } from './validators.js';
|
|
11
11
|
import { GatewayDiscovery } from './discovery.js';
|
|
12
|
+
import { AIDStore } from './aid-store.js';
|
|
12
13
|
import { RPCTransport } from './transport.js';
|
|
13
14
|
import { AuthFlow } from './auth.js';
|
|
14
15
|
import { SeqTracker } from './seq-tracker.js';
|
|
@@ -19,22 +20,28 @@ import { IdentityRuntimeManager } from './client/identity.js';
|
|
|
19
20
|
import { LifecycleController } from './client/lifecycle.js';
|
|
20
21
|
import { PeerDirectory } from './client/peers.js';
|
|
21
22
|
import { RpcPipeline } from './client/rpc-pipeline.js';
|
|
22
|
-
import { GroupFacade, MailFacade, MessageFacade, StreamFacade } from './facades.js';
|
|
23
|
+
import { CredentialFacade, GroupFacade, MailFacade, MessageFacade, OrgFacade, StreamFacade } from './facades.js';
|
|
23
24
|
import { StorageVFS } from './storage/vfs.js';
|
|
24
25
|
import { CollabClient } from './collab/client.js';
|
|
25
26
|
import { V2E2EECoordinator } from './client/v2-e2ee.js';
|
|
26
27
|
import { GroupStateCoordinator } from './client/group-state.js';
|
|
27
|
-
import { CryptoProvider, base64ToUint8, pemToArrayBuffer, ecdsaVerifyDer, importCertPublicKeyEcdsa, } from './crypto.js';
|
|
28
|
+
import { CryptoProvider, base64ToUint8, pemToArrayBuffer, certificateSha256Fingerprint, ecdsaVerifyDer, importCertPublicKeyEcdsa, arrayBufferToPem, uint8ToBase64, } from './crypto.js';
|
|
28
29
|
import { IndexedDBTokenStore } from './keystore/indexeddb-token-store.js';
|
|
29
30
|
import { decryptMessage, } from './v2/e2ee/index.js';
|
|
30
31
|
import { ecdsaVerifyRaw } from './v2/crypto/ecdsa.js';
|
|
32
|
+
import { decryptedOwnershipJsonText } from './v2/e2ee/decrypt.js';
|
|
31
33
|
import { AUNLogger } from './logger.js';
|
|
32
|
-
import { AgentMdManager, buildDefaultAgentMd } from './agent-md.js';
|
|
34
|
+
import { AgentMdManager, buildDefaultAgentMd, buildOwnershipAgentMd, buildOwnershipReleaseAgentMd } from './agent-md.js';
|
|
33
35
|
import { GroupIndexMetaCache } from './group-index.js';
|
|
34
|
-
import { certMatchesFingerprint, normalizeFingerprintHex } from './cert-utils.js';
|
|
36
|
+
import { certMatchesFingerprint, normalizeFingerprintHex, parseAgentMdTailSignature, parseCertMetadata } from './cert-utils.js';
|
|
37
|
+
import { validateAgentMdDocument } from './agent-md-schema.js';
|
|
35
38
|
import { AUNError, AuthError, ConnectionError, E2EEError, NotFoundError, PermissionError, StateError, TimeoutError, ValidationError, } from './errors.js';
|
|
36
39
|
import { isJsonObject, ConnectionState, STATE_TO_PUBLIC, } from './types.js';
|
|
37
40
|
import { AID } from './aid.js';
|
|
41
|
+
import { OwnershipFiles, OwnershipStorageAdapter, ownershipSha256 } from './ownership-v21.js';
|
|
42
|
+
import { canonicalJson, createCredential, createMemberCredential, signMembershipBinding, verifyServiceEvent } from './org-protocol.js';
|
|
43
|
+
import { OwnershipWorkflow, parseOwnershipRegistrationIntent } from './ownership-workflow.js';
|
|
44
|
+
import { buildOwnershipMessage, encryptIdentity, decryptIdentity, } from './ownership-proof.js';
|
|
38
45
|
function getV2DeviceId(dev) {
|
|
39
46
|
if (Object.prototype.hasOwnProperty.call(dev, 'device_id')) {
|
|
40
47
|
return { present: true, value: String(dev.device_id ?? '').trim() };
|
|
@@ -52,9 +59,22 @@ function isAIDObject(value) {
|
|
|
52
59
|
&& typeof candidate.aunPath === 'string'
|
|
53
60
|
&& typeof candidate.isPrivateKeyValid === 'function');
|
|
54
61
|
}
|
|
62
|
+
function ownershipRandomUUID() {
|
|
63
|
+
return globalThis.crypto?.randomUUID?.()
|
|
64
|
+
?? 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (char) => {
|
|
65
|
+
const random = Math.floor(Math.random() * 16);
|
|
66
|
+
const value = char === 'x' ? random : (random & 0x3) | 0x8;
|
|
67
|
+
return value.toString(16);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
55
70
|
function bytesToHex(bytes) {
|
|
56
71
|
return Array.from(bytes).map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
57
72
|
}
|
|
73
|
+
function randomBase64Url(byteLength) {
|
|
74
|
+
const bytes = new Uint8Array(byteLength);
|
|
75
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
76
|
+
return uint8ToBase64(bytes).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
|
77
|
+
}
|
|
58
78
|
function exactArrayBuffer(bytes) {
|
|
59
79
|
return bytes.slice().buffer;
|
|
60
80
|
}
|
|
@@ -240,6 +260,9 @@ function createAgentMdManagerForRuntime(opts) {
|
|
|
240
260
|
logger: opts.logger(),
|
|
241
261
|
ownerAidGetter: opts.ownerAid,
|
|
242
262
|
currentAidGetter: opts.currentAid,
|
|
263
|
+
entityOwnerResolver: opts.entityOwnerResolver,
|
|
264
|
+
entityIdentityGetter: opts.entityIdentityGetter,
|
|
265
|
+
entitySignerResolver: opts.entitySignerResolver,
|
|
243
266
|
gatewayResolver: async (aid) => {
|
|
244
267
|
const gatewayUrl = await opts.gateway.resolve(aid);
|
|
245
268
|
opts.gateway.set(gatewayUrl);
|
|
@@ -289,7 +312,46 @@ function createAgentMdManagerForRuntime(opts) {
|
|
|
289
312
|
opts.identity.set(await auth.loadIdentityOrNone(target) ?? fallbackIdentity);
|
|
290
313
|
return token;
|
|
291
314
|
},
|
|
292
|
-
|
|
315
|
+
uploadAccessTokenResolver: async (_aid, gatewayUrl) => {
|
|
316
|
+
const target = String(opts.ownerAid() ?? '').trim();
|
|
317
|
+
let identity = opts.identity.get();
|
|
318
|
+
if (!identity || String(identity.aid ?? '') !== target)
|
|
319
|
+
throw new StateError('no local identity found, register or load an AID first');
|
|
320
|
+
const auth = opts.auth();
|
|
321
|
+
const cachedToken = String(identity.access_token ?? '');
|
|
322
|
+
const expiresAt = auth.getAccessTokenExpiry(identity);
|
|
323
|
+
if (cachedToken && (expiresAt === null || expiresAt > Date.now() / 1000 + 30))
|
|
324
|
+
return cachedToken;
|
|
325
|
+
if (identity.refresh_token) {
|
|
326
|
+
try {
|
|
327
|
+
const refreshed = await auth.refreshCachedTokens(gatewayUrl, identity);
|
|
328
|
+
const refreshedToken = String(refreshed.access_token ?? '');
|
|
329
|
+
const refreshedExpiry = auth.getAccessTokenExpiry(refreshed);
|
|
330
|
+
if (refreshedToken && (refreshedExpiry === null || refreshedExpiry > Date.now() / 1000 + 30)) {
|
|
331
|
+
opts.identity.set(refreshed);
|
|
332
|
+
return refreshedToken;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
// refresh 失败时回退到完整 authenticate。
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const result = await auth.authenticate(gatewayUrl, target);
|
|
340
|
+
const token = String(result.access_token ?? '');
|
|
341
|
+
if (!token)
|
|
342
|
+
throw new StateError('authenticate did not return access_token');
|
|
343
|
+
const fallbackIdentity = {
|
|
344
|
+
...identity,
|
|
345
|
+
access_token: token,
|
|
346
|
+
refresh_token: String(result.refresh_token ?? identity.refresh_token ?? ''),
|
|
347
|
+
};
|
|
348
|
+
const fallbackExpiresAt = Number(result.expires_at ?? identity.expires_at ?? NaN);
|
|
349
|
+
if (Number.isFinite(fallbackExpiresAt))
|
|
350
|
+
fallbackIdentity.expires_at = fallbackExpiresAt;
|
|
351
|
+
opts.identity.set(await auth.loadIdentityOrNone(target) ?? fallbackIdentity);
|
|
352
|
+
return token;
|
|
353
|
+
},
|
|
354
|
+
peerResolver: async (aid, certFingerprint, signedAt) => {
|
|
293
355
|
const target = String(aid ?? '').trim();
|
|
294
356
|
const expectedFp = String(certFingerprint ?? '').trim().toLowerCase();
|
|
295
357
|
const current = opts.currentAid();
|
|
@@ -303,7 +365,7 @@ function createAgentMdManagerForRuntime(opts) {
|
|
|
303
365
|
}
|
|
304
366
|
catch { /* best effort */ }
|
|
305
367
|
}
|
|
306
|
-
const certPem = String(await opts.fetchPeerCert(target, expectedFp || undefined) ?? '').trim();
|
|
368
|
+
const certPem = String(await opts.fetchPeerCert(target, expectedFp || undefined, signedAt) ?? '').trim();
|
|
307
369
|
if (!certPem)
|
|
308
370
|
throw new NotFoundError(`certificate not found for aid: ${target}`);
|
|
309
371
|
return await AID.create({
|
|
@@ -520,14 +582,79 @@ function normalizeDeliveryModeConfig(raw, opts = {}) {
|
|
|
520
582
|
*
|
|
521
583
|
*/
|
|
522
584
|
export class AUNClient {
|
|
585
|
+
resolveCredentialService;
|
|
523
586
|
/** SDK 配置模型 */
|
|
524
587
|
configModel;
|
|
525
588
|
/** 原始配置字典 */
|
|
526
589
|
config;
|
|
527
590
|
_aid = null;
|
|
591
|
+
_serviceEventIdentityVersion = 0;
|
|
528
592
|
_identity = null;
|
|
529
593
|
_state = 'idle';
|
|
530
594
|
_currentAid = null;
|
|
595
|
+
_ownershipRecords = new Map();
|
|
596
|
+
_ownershipType = 'agent';
|
|
597
|
+
_ownershipRecordsLoad = null;
|
|
598
|
+
_ownershipWorkflow = null;
|
|
599
|
+
_ownershipRecoveryTimer = null;
|
|
600
|
+
_ownershipRecoveryGeneration = 0;
|
|
601
|
+
_ownershipReleaseTasks = new Map();
|
|
602
|
+
async _persistOwnershipRecords() {
|
|
603
|
+
const aid = String(this._aid ?? '').trim();
|
|
604
|
+
if (!aid)
|
|
605
|
+
return;
|
|
606
|
+
const store = this._tokenStore;
|
|
607
|
+
if (!store.getMetadata || !store.setMetadata)
|
|
608
|
+
return;
|
|
609
|
+
let records = {};
|
|
610
|
+
try {
|
|
611
|
+
const raw = await store.getMetadata.call(store, aid, 'ownership_records');
|
|
612
|
+
const parsed = raw ? JSON.parse(raw) : {};
|
|
613
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
614
|
+
records = parsed;
|
|
615
|
+
}
|
|
616
|
+
catch {
|
|
617
|
+
records = {};
|
|
618
|
+
}
|
|
619
|
+
for (const [key, record] of this._ownershipRecords) {
|
|
620
|
+
if (![record.subject_aid, record.owner_aid].some((value) => String(value ?? '').trim().toLowerCase() === aid.toLowerCase()))
|
|
621
|
+
continue;
|
|
622
|
+
records[key] = Object.fromEntries(Object.entries(record).filter(([name]) => !['subject_identity', 'escrow_password', 'escrow_download_url'].includes(name)));
|
|
623
|
+
}
|
|
624
|
+
await store.setMetadata.call(store, aid, 'ownership_records', JSON.stringify(records));
|
|
625
|
+
}
|
|
626
|
+
async _restoreOwnershipRecords() {
|
|
627
|
+
const aid = String(this._aid ?? '').trim();
|
|
628
|
+
if (!aid)
|
|
629
|
+
return;
|
|
630
|
+
const store = this._tokenStore;
|
|
631
|
+
if (!store.getMetadata)
|
|
632
|
+
return;
|
|
633
|
+
try {
|
|
634
|
+
const intentRaw = await store.getMetadata.call(store, aid, 'registration_intent');
|
|
635
|
+
if (this._aid !== aid)
|
|
636
|
+
return;
|
|
637
|
+
if (intentRaw)
|
|
638
|
+
this._ownershipType = String(JSON.parse(intentRaw).type || this._ownershipType);
|
|
639
|
+
const raw = await store.getMetadata.call(store, aid, 'ownership_records');
|
|
640
|
+
if (this._aid !== aid)
|
|
641
|
+
return;
|
|
642
|
+
const records = raw ? JSON.parse(raw) : {};
|
|
643
|
+
if (!records || typeof records !== 'object' || Array.isArray(records))
|
|
644
|
+
return;
|
|
645
|
+
for (const [key, value] of Object.entries(records)) {
|
|
646
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
647
|
+
continue;
|
|
648
|
+
const record = value;
|
|
649
|
+
this._ownershipRecords.set(key, record);
|
|
650
|
+
if (String(record.subject_aid ?? '').trim().toLowerCase() === aid.toLowerCase())
|
|
651
|
+
this._ownershipType = String(record.type ?? this._ownershipType);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
catch {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
531
658
|
_instanceProtectedHeaders = null;
|
|
532
659
|
_gatewayUrl = null;
|
|
533
660
|
_gatewayCandidates = [];
|
|
@@ -537,6 +664,7 @@ export class AUNClient {
|
|
|
537
664
|
_connectDeliveryMode;
|
|
538
665
|
_defaultConnectDeliveryMode;
|
|
539
666
|
_closing = false;
|
|
667
|
+
_disconnecting = false;
|
|
540
668
|
_sessionParams = null;
|
|
541
669
|
_sessionOptions = { ...DEFAULT_SESSION_OPTIONS };
|
|
542
670
|
_dispatcher;
|
|
@@ -560,6 +688,10 @@ export class AUNClient {
|
|
|
560
688
|
_v2BootstrapCache = new Map();
|
|
561
689
|
_v2SenderIKPending = new Map();
|
|
562
690
|
_v2SenderIKFetching = new Set();
|
|
691
|
+
_readStateCache = new Map();
|
|
692
|
+
_readStateQueries = new Map();
|
|
693
|
+
_readStateIdentity = null;
|
|
694
|
+
_readStateGeneration = 0;
|
|
563
695
|
static V2_BOOTSTRAP_TTL_MS = 60 * 60 * 1000;
|
|
564
696
|
/** V2 state 签名验证缓存:cacheKey(hex) → expiry_unix_ms */
|
|
565
697
|
_v2SigCache = new Map();
|
|
@@ -643,6 +775,8 @@ export class AUNClient {
|
|
|
643
775
|
_mailFacade;
|
|
644
776
|
_groupFacade;
|
|
645
777
|
_streamFacade;
|
|
778
|
+
_credentialFacade;
|
|
779
|
+
_orgFacade;
|
|
646
780
|
_groupInvitePending = new Map();
|
|
647
781
|
_groupInviteStateAid = null;
|
|
648
782
|
_groupInviteStateLoad = null;
|
|
@@ -691,6 +825,9 @@ export class AUNClient {
|
|
|
691
825
|
verifySsl: this.configModel.verifySsl,
|
|
692
826
|
});
|
|
693
827
|
this._aid = initAid;
|
|
828
|
+
this._dispatcher.setEventIdentity(initAid ?? '');
|
|
829
|
+
if (initAid)
|
|
830
|
+
this._ownershipRecordsLoad = this._restoreOwnershipRecords();
|
|
694
831
|
this._agentMdManager = createAgentMdManagerForRuntime({
|
|
695
832
|
config: () => this.configModel,
|
|
696
833
|
logger: () => this._logger.for('aun_core.agent_md'),
|
|
@@ -708,13 +845,18 @@ export class AUNClient {
|
|
|
708
845
|
},
|
|
709
846
|
auth: () => this._auth,
|
|
710
847
|
tokenStore: () => this._tokenStore,
|
|
711
|
-
fetchPeerCert: (target, certFingerprint) => this._fetchPeerCert(target, certFingerprint || undefined),
|
|
848
|
+
fetchPeerCert: (target, certFingerprint, signedAt) => this._fetchPeerCert(target, certFingerprint || undefined, undefined, signedAt),
|
|
849
|
+
entityOwnerResolver: (target, type) => this._resolveEntityAgentMdOwner(target, type),
|
|
850
|
+
entityIdentityGetter: (target, type) => this._freshEntityCertificate(target, type),
|
|
851
|
+
entitySignerResolver: (target, fingerprint) => this._freshEntitySigner(target, fingerprint),
|
|
712
852
|
});
|
|
713
853
|
this._transport = new RPCTransport({
|
|
714
854
|
eventDispatcher: this._dispatcher,
|
|
715
855
|
timeout: DEFAULT_SESSION_OPTIONS.timeouts.call,
|
|
716
856
|
});
|
|
857
|
+
this._transport.setServiceEventVerifier((event, payload) => this._verifyIncomingServiceEvent(event, payload));
|
|
717
858
|
this._transport.setProtocolDisconnectCallback((error, closeCode) => this._handleTransportDisconnect(error, closeCode));
|
|
859
|
+
this._transport._onForegroundDrained = () => this._delivery.onForegroundPullsDrained();
|
|
718
860
|
this._transport.setProtocolMetaObserver((meta) => this._observeRpcMeta(meta).catch((exc) => {
|
|
719
861
|
this._clientLog.debug(`rpc meta observer skipped: ${String(exc)}`);
|
|
720
862
|
}));
|
|
@@ -736,6 +878,7 @@ export class AUNClient {
|
|
|
736
878
|
cert: inputAid.certPem,
|
|
737
879
|
};
|
|
738
880
|
this._auth.setIdentity(this._identity);
|
|
881
|
+
this._ownershipRecordsLoad = this._restoreOwnershipRecords();
|
|
739
882
|
this._state = 'standby';
|
|
740
883
|
}
|
|
741
884
|
}
|
|
@@ -756,6 +899,9 @@ export class AUNClient {
|
|
|
756
899
|
this._dispatcher.subscribeProtocol('_raw.message.recalled', (data) => {
|
|
757
900
|
this._safeAsync(this._onRawMessageRecalled(data));
|
|
758
901
|
});
|
|
902
|
+
this._dispatcher.subscribeProtocol('_raw.message.read_changed', (data) => {
|
|
903
|
+
this._onRawReadChanged(data, 'p2p');
|
|
904
|
+
});
|
|
759
905
|
// 群组消息推送:re-publish(V2 加密消息走 V2 push 路径)
|
|
760
906
|
this._dispatcher.subscribeProtocol('_raw.group.message_created', (data) => {
|
|
761
907
|
this._onRawGroupMessageCreated(data);
|
|
@@ -764,6 +910,9 @@ export class AUNClient {
|
|
|
764
910
|
this._dispatcher.subscribeProtocol('_raw.group.message_recalled', (data) => {
|
|
765
911
|
this._safeAsync(this._onRawGroupMessageRecalled(data));
|
|
766
912
|
});
|
|
913
|
+
this._dispatcher.subscribeProtocol('_raw.group.read_changed', (data) => {
|
|
914
|
+
this._onRawReadChanged(data, 'group');
|
|
915
|
+
});
|
|
767
916
|
// 群组变更事件:验签 + 透传 + gap 检测 + V2 state/SPK 维护
|
|
768
917
|
this._dispatcher.subscribeProtocol('_raw.group.changed', (data) => {
|
|
769
918
|
this._onRawGroupChanged(data);
|
|
@@ -858,6 +1007,9 @@ export class AUNClient {
|
|
|
858
1007
|
const groupIndexes = isJsonObject(meta.group_indexes)
|
|
859
1008
|
? meta.group_indexes
|
|
860
1009
|
: {};
|
|
1010
|
+
// 先同步更新内存中的远端指纹,让 RPC 返回后的调用方立即看到 stale 状态;
|
|
1011
|
+
// 持久化缓存加载完成后再应用一次,避免旧缓存覆盖本次 RPC 元数据。
|
|
1012
|
+
this._groupIndexMetaCache.observeRpcMeta(meta, { localAid: this._aid ?? '' });
|
|
861
1013
|
for (const groupAid of Object.keys(groupIndexes)) {
|
|
862
1014
|
await this._loadGroupIndexCache(groupAid);
|
|
863
1015
|
}
|
|
@@ -891,6 +1043,18 @@ export class AUNClient {
|
|
|
891
1043
|
this._mailFacade = new MailFacade(this);
|
|
892
1044
|
return this._mailFacade;
|
|
893
1045
|
}
|
|
1046
|
+
/** Credential RPC facade 入口 */
|
|
1047
|
+
get credential() {
|
|
1048
|
+
if (!this._credentialFacade)
|
|
1049
|
+
this._credentialFacade = new CredentialFacade(this);
|
|
1050
|
+
return this._credentialFacade;
|
|
1051
|
+
}
|
|
1052
|
+
/** Org RPC facade 入口 */
|
|
1053
|
+
get org() {
|
|
1054
|
+
if (!this._orgFacade)
|
|
1055
|
+
this._orgFacade = new OrgFacade(this);
|
|
1056
|
+
return this._orgFacade;
|
|
1057
|
+
}
|
|
894
1058
|
getGroupIndexRemoteMeta(groupAid) {
|
|
895
1059
|
return this._groupIndexMetaCache.remoteMeta(this._aid ?? '', groupAid);
|
|
896
1060
|
}
|
|
@@ -975,6 +1139,501 @@ export class AUNClient {
|
|
|
975
1139
|
get currentAid() {
|
|
976
1140
|
return this._currentAid;
|
|
977
1141
|
}
|
|
1142
|
+
_ownershipKey(subjectAid, ownerAid) {
|
|
1143
|
+
return `${String(subjectAid ?? '').trim().toLowerCase()}|${String(ownerAid ?? '').trim().toLowerCase()}`;
|
|
1144
|
+
}
|
|
1145
|
+
getType() { return String(this._ownershipType || 'agent').trim() || 'agent'; }
|
|
1146
|
+
get_type() { return this.getType(); }
|
|
1147
|
+
getOwner() {
|
|
1148
|
+
const aid = String(this._aid ?? '').trim().toLowerCase();
|
|
1149
|
+
let best = null;
|
|
1150
|
+
let bestActive = false;
|
|
1151
|
+
for (const record of this._ownershipRecords.values()) {
|
|
1152
|
+
if (String(record.subject_aid ?? '').trim().toLowerCase() === aid) {
|
|
1153
|
+
const owner = String(record.owner_aid ?? '').trim();
|
|
1154
|
+
if (!owner)
|
|
1155
|
+
continue;
|
|
1156
|
+
const active = String(record.status ?? '').trim().toLowerCase() === 'active';
|
|
1157
|
+
if (!best || active && !bestActive) {
|
|
1158
|
+
best = owner;
|
|
1159
|
+
bestActive = active;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
return best;
|
|
1164
|
+
}
|
|
1165
|
+
get_owner() { return this.getOwner(); }
|
|
1166
|
+
getOwnershipStatus() {
|
|
1167
|
+
const aid = String(this._aid ?? '').trim().toLowerCase();
|
|
1168
|
+
let best = null;
|
|
1169
|
+
let bestActive = false;
|
|
1170
|
+
for (const record of this._ownershipRecords.values()) {
|
|
1171
|
+
const subject = String(record.subject_aid ?? '').trim().toLowerCase();
|
|
1172
|
+
const owner = String(record.owner_aid ?? '').trim().toLowerCase();
|
|
1173
|
+
if (aid !== subject && aid !== owner)
|
|
1174
|
+
continue;
|
|
1175
|
+
const active = String(record.status ?? '').trim().toLowerCase() === 'active';
|
|
1176
|
+
if (!best || active && !bestActive) {
|
|
1177
|
+
best = record;
|
|
1178
|
+
bestActive = active;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
if (best)
|
|
1182
|
+
return Object.fromEntries(Object.entries(best).filter(([key]) => key !== 'subject_identity' && key !== 'escrow_password' && key !== 'escrow_download_url'));
|
|
1183
|
+
return { subject_aid: this._aid ?? '', owner_aid: null, status: 'legacy_unbound', type: this.getType() };
|
|
1184
|
+
}
|
|
1185
|
+
get_ownership_status() { return this.getOwnershipStatus(); }
|
|
1186
|
+
_ownershipEscrowPath(subjectAid, proofId) {
|
|
1187
|
+
return `/.aun/ownership/${String(subjectAid ?? '').trim().toLowerCase()}/${String(proofId ?? '').trim()}.json`;
|
|
1188
|
+
}
|
|
1189
|
+
async _uploadOwnershipEscrow(subjectAid, ownerAid, proofId) {
|
|
1190
|
+
const current = this._currentAid;
|
|
1191
|
+
if (!current || !current.isPrivateKeyValid())
|
|
1192
|
+
throw new StateError('ownership escrow requires a loaded identity with a valid private key');
|
|
1193
|
+
const packageValue = { aid: current.aid, private_key_pem: current.privateKeyPem, public_key_der_b64: current.publicKey, curve: 'P-256', cert: current.certPem };
|
|
1194
|
+
const { envelope, password } = await encryptIdentity(packageValue);
|
|
1195
|
+
const path = this._ownershipEscrowPath(subjectAid, proofId);
|
|
1196
|
+
await this.storage.writeBytes(path, new TextEncoder().encode(JSON.stringify(envelope)), { overwrite: true, contentType: 'application/json' });
|
|
1197
|
+
await this.storage.setAcl(path, { granteeAid: ownerAid, perms: 'r' });
|
|
1198
|
+
const ticket = await this.storage.lowlevel.call('storage.create_download_ticket', {
|
|
1199
|
+
owner_aid: subjectAid, bucket: 'default', object_key: path.replace(/^\/+/, ''), viewer_aid: ownerAid,
|
|
1200
|
+
}, path);
|
|
1201
|
+
const downloadUrl = String(ticket.logical_url ?? ticket.download_url ?? ticket.url ?? '').trim();
|
|
1202
|
+
if (!downloadUrl)
|
|
1203
|
+
throw new ValidationError('ownership escrow download ticket did not return a URL');
|
|
1204
|
+
return { escrow_path: path, escrow_download_url: downloadUrl, escrow_password: password, escrow_sha256: envelope.sha256 };
|
|
1205
|
+
}
|
|
1206
|
+
async _downloadOwnershipEscrow(record) {
|
|
1207
|
+
const subject = String(record.subject_aid ?? '').trim().toLowerCase();
|
|
1208
|
+
const downloadUrl = String(record.escrow_download_url ?? '').trim();
|
|
1209
|
+
const password = String(record.escrow_password ?? '');
|
|
1210
|
+
if (!subject || !downloadUrl || !password)
|
|
1211
|
+
throw new ValidationError('ownership escrow metadata is incomplete');
|
|
1212
|
+
const currentAid = String(this._aid ?? '').trim().toLowerCase();
|
|
1213
|
+
const currentIssuer = currentAid.includes('.') ? currentAid.slice(currentAid.indexOf('.') + 1) : '';
|
|
1214
|
+
const subjectIssuer = subject.includes('.') ? subject.slice(subject.indexOf('.') + 1) : '';
|
|
1215
|
+
const sameIssuer = currentAid === subject || Boolean(currentIssuer && subjectIssuer && currentIssuer === subjectIssuer);
|
|
1216
|
+
const token = sameIssuer ? this.getAccessToken() : null;
|
|
1217
|
+
const raw = await this.storage.lowlevel.httpGet(downloadUrl, token ? { Authorization: 'Bearer ' + token } : undefined);
|
|
1218
|
+
const envelope = JSON.parse(new TextDecoder().decode(raw));
|
|
1219
|
+
const identity = await decryptIdentity(envelope, password);
|
|
1220
|
+
if (String(identity.aid ?? '').trim().toLowerCase() !== subject)
|
|
1221
|
+
throw new ValidationError('ownership escrow identity AID mismatch');
|
|
1222
|
+
const store = this._aidStore ?? new AIDStore({ aunPath: this.configModel.aunPath, encryptionSeed: this.configModel.seedPassword ?? '' });
|
|
1223
|
+
this._aidStore = store;
|
|
1224
|
+
const imported = await store.importGroupIdentity(subject, identity);
|
|
1225
|
+
if (!imported.ok)
|
|
1226
|
+
throw new StateError(imported.error?.message ?? 'ownership custody identity persistence failed');
|
|
1227
|
+
record.subject_identity = identity;
|
|
1228
|
+
record.escrow_sha256 = String(envelope.sha256 ?? '');
|
|
1229
|
+
return identity;
|
|
1230
|
+
}
|
|
1231
|
+
_ownershipV21() {
|
|
1232
|
+
if (!this._ownershipWorkflow) {
|
|
1233
|
+
const storage = new OwnershipStorageAdapter({ currentAid: () => this._aid ?? '', call: (method, params) => this.call(method, params) });
|
|
1234
|
+
const files = new OwnershipFiles({
|
|
1235
|
+
currentAidType: () => this.getType(),
|
|
1236
|
+
currentAid: () => {
|
|
1237
|
+
if (!this._currentAid?.isPrivateKeyValid() || this._currentAid.aid !== this._aid)
|
|
1238
|
+
throw new StateError('ownership signing requires the current local identity', { stringCode: 'forbidden' });
|
|
1239
|
+
return this._currentAid;
|
|
1240
|
+
},
|
|
1241
|
+
readAgentMdFresh: (aid) => this._agentMdManager.readOwnershipFresh(aid),
|
|
1242
|
+
submitAgentMdRaw: (aid, raw, condition) => this._agentMdManager.submitOwnershipRaw(aid, raw, condition),
|
|
1243
|
+
readOwnerMd: (owner, sub) => storage.readOwnerMd(owner, sub),
|
|
1244
|
+
writeOwnerMd: (owner, sub, raw, revision) => storage.writeOwnerMd(owner, sub, raw, revision),
|
|
1245
|
+
deleteOwnerMd: (owner, sub, revision) => storage.deleteOwnerMd(owner, sub, revision),
|
|
1246
|
+
resolveCertificate: async (aid, fingerprint, keyPurpose) => this._auth.resolveOwnershipCertificate(await this._resolveOwnershipCertificateGateway(aid), aid, fingerprint, keyPurpose),
|
|
1247
|
+
});
|
|
1248
|
+
this._ownershipWorkflow = new OwnershipWorkflow({
|
|
1249
|
+
currentAidType: () => this.getType(),
|
|
1250
|
+
currentAid: () => this._aid ?? '', store: this._tokenStore, files,
|
|
1251
|
+
send: (to, payload, messageId) => this.call('message.send', { to, payload: payload, encrypt: true, message_id: messageId }),
|
|
1252
|
+
emit: (event, payload) => this._dispatcher.publish(event, payload),
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
return this._ownershipWorkflow;
|
|
1256
|
+
}
|
|
1257
|
+
async bindOwneraid(ownerAid) { return await this._ownershipV21().bindOwneraid(ownerAid); }
|
|
1258
|
+
async bindSubaid(subAid) { return await this._ownershipV21().bindSubaid(subAid); }
|
|
1259
|
+
async releaseSubaid(subAid) { return await this._ownershipV21().releaseSubaid(subAid); }
|
|
1260
|
+
async transferSubaid(subAid, newOwnerAid) { return await this._ownershipV21().transferSubaid(subAid, newOwnerAid); }
|
|
1261
|
+
async checkOwnershipConsensus(subAid) { return await this._ownershipV21().checkOwnershipConsensus(subAid); }
|
|
1262
|
+
async _resumeOwnershipAfterConnect() {
|
|
1263
|
+
await this._checkIdentityAdmissionAfterConnect();
|
|
1264
|
+
await this._resumeRegistrationIntent();
|
|
1265
|
+
await this._ownershipV21().resume();
|
|
1266
|
+
}
|
|
1267
|
+
async _resumeRegistrationIntent(guard) {
|
|
1268
|
+
guard?.();
|
|
1269
|
+
if (!this._aid)
|
|
1270
|
+
return;
|
|
1271
|
+
const store = this._tokenStore;
|
|
1272
|
+
if (typeof store.getMetadata !== 'function' || typeof store.setMetadata !== 'function')
|
|
1273
|
+
throw new StateError('ownership registration storage unavailable', { stringCode: 'persistence_unavailable' });
|
|
1274
|
+
let raw;
|
|
1275
|
+
try {
|
|
1276
|
+
raw = await store.getMetadata(this._aid, 'registration_intent');
|
|
1277
|
+
}
|
|
1278
|
+
catch (error) {
|
|
1279
|
+
throw new StateError(`ownership registration read failed: ${String(error)}`, { stringCode: 'persistence_unavailable' });
|
|
1280
|
+
}
|
|
1281
|
+
if (!raw)
|
|
1282
|
+
return;
|
|
1283
|
+
const intent = parseOwnershipRegistrationIntent(raw, this._aid);
|
|
1284
|
+
if (intent.protocol === 'entity')
|
|
1285
|
+
return;
|
|
1286
|
+
if (!intent.pending)
|
|
1287
|
+
return;
|
|
1288
|
+
if (!intent.operation_id)
|
|
1289
|
+
throw new StateError('ownership registration intent is damaged', { stringCode: 'persistence_unavailable' });
|
|
1290
|
+
guard?.();
|
|
1291
|
+
await this._ownershipV21().startRegistrationIntent(intent.owner, intent.operation_id, guard);
|
|
1292
|
+
guard?.();
|
|
1293
|
+
try {
|
|
1294
|
+
await store.setMetadata(this._aid, 'registration_intent', JSON.stringify({ ...intent, pending: false }));
|
|
1295
|
+
}
|
|
1296
|
+
catch (error) {
|
|
1297
|
+
throw new StateError(`ownership registration save failed: ${String(error)}`, { stringCode: 'persistence_unavailable' });
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
async _recoverOwnershipRekeyBeforeConnect() {
|
|
1301
|
+
await this._ownershipRecordsLoad;
|
|
1302
|
+
if (!['standby', 'authenticated', 'retry_backoff', 'connection_failed'].includes(this.state))
|
|
1303
|
+
return;
|
|
1304
|
+
for (const [key, record] of this._ownershipRecords) {
|
|
1305
|
+
if (record.subject_aid !== this._aid || !['pending_rekey', 'pending_agent_md', 'pending_upload'].includes(String(record.release_status ?? '')))
|
|
1306
|
+
continue;
|
|
1307
|
+
const store = this._aidStore ?? new AIDStore({ aunPath: this.configModel.aunPath, encryptionSeed: this.configModel.seedPassword ?? '', verifySsl: this.configModel.verifySsl });
|
|
1308
|
+
this._aidStore = store;
|
|
1309
|
+
let loaded = await store.load(String(record.subject_aid));
|
|
1310
|
+
if (!loaded.ok)
|
|
1311
|
+
throw new StateError(loaded.error.message);
|
|
1312
|
+
if (record.release_status === 'pending_rekey' && loaded.data.aid.certFingerprint === record.release_old_fingerprint) {
|
|
1313
|
+
const rekeyed = await store.rekey(loaded.data.aid.aid);
|
|
1314
|
+
if (!rekeyed.ok)
|
|
1315
|
+
throw new StateError(rekeyed.error.message);
|
|
1316
|
+
loaded = await store.load(String(record.subject_aid));
|
|
1317
|
+
if (!loaded.ok)
|
|
1318
|
+
throw new StateError(loaded.error.message);
|
|
1319
|
+
}
|
|
1320
|
+
if (record.release_status === 'pending_rekey')
|
|
1321
|
+
record.release_status = 'pending_agent_md';
|
|
1322
|
+
this._ownershipRecords.set(key, record);
|
|
1323
|
+
await this._persistOwnershipRecords();
|
|
1324
|
+
if (this._currentAid?.certFingerprint !== loaded.data.aid.certFingerprint) {
|
|
1325
|
+
this._state = 'no_identity';
|
|
1326
|
+
this.loadIdentity(loaded.data.aid);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
async _sendOwnershipControl(params) {
|
|
1331
|
+
const payload = params.payload;
|
|
1332
|
+
const key = this._ownershipKey(String(payload.subject_aid), String(payload.owner_aid));
|
|
1333
|
+
const record = this._ownershipRecords.get(key);
|
|
1334
|
+
record.pending_message = params;
|
|
1335
|
+
if (payload.action === 'bind.response' || payload.action === 'bind.reject')
|
|
1336
|
+
delete record.ownership_request_pending;
|
|
1337
|
+
if (payload.action === 'bind.reject')
|
|
1338
|
+
record.status = 'rejected';
|
|
1339
|
+
await this._persistOwnershipRecords();
|
|
1340
|
+
const result = await this.call('message.send', params);
|
|
1341
|
+
delete record.pending_message;
|
|
1342
|
+
await this._persistOwnershipRecords();
|
|
1343
|
+
return result;
|
|
1344
|
+
}
|
|
1345
|
+
async _resumeOwnership() {
|
|
1346
|
+
await this._ownershipRecordsLoad;
|
|
1347
|
+
const current = String(this._aid ?? '').toLowerCase();
|
|
1348
|
+
for (const [key, record] of this._ownershipRecords) {
|
|
1349
|
+
const subject = String(record.subject_aid ?? ''), owner = String(record.owner_aid ?? '');
|
|
1350
|
+
try {
|
|
1351
|
+
if (record.pending_message)
|
|
1352
|
+
await this._sendOwnershipControl(record.pending_message);
|
|
1353
|
+
if (record.ownership_request_pending) {
|
|
1354
|
+
const role = current === subject ? 'owner' : 'subject';
|
|
1355
|
+
await this._dispatcher.publish('ownership.request', {
|
|
1356
|
+
request_id: record.request_id, proof_id: record.proof_id, subject_aid: subject, owner_aid: owner, subject_type: record.type,
|
|
1357
|
+
requester_aid: current === subject ? owner : subject, requester_role: role, statement: record[`${role}_statement`], signature: record[`${role}_signature`],
|
|
1358
|
+
});
|
|
1359
|
+
continue;
|
|
1360
|
+
}
|
|
1361
|
+
if (record.status !== 'active' || record.release_status)
|
|
1362
|
+
continue;
|
|
1363
|
+
if (subject === owner) {
|
|
1364
|
+
if (!record.agent_md_uploaded)
|
|
1365
|
+
await this._publishSubjectOwnershipAgentMd(record);
|
|
1366
|
+
}
|
|
1367
|
+
else if (current === subject && !record.agent_md_synced) {
|
|
1368
|
+
await this._publishSubjectOwnershipAgentMd(record);
|
|
1369
|
+
Object.assign(record, await this._uploadOwnershipEscrow(subject, owner, String(record.proof_id)));
|
|
1370
|
+
await this.call('message.send', { to: owner, encrypt: true, payload: buildOwnershipMessage('escrow.ready', {
|
|
1371
|
+
requestId: String(record.request_id), proofId: String(record.proof_id), subjectAid: subject, ownerAid: owner, subject_type: record.type,
|
|
1372
|
+
escrow_path: record.escrow_path, escrow_download_url: record.escrow_download_url, escrow_password: record.escrow_password, escrow_sha256: record.escrow_sha256,
|
|
1373
|
+
}) });
|
|
1374
|
+
}
|
|
1375
|
+
else if (current === owner && !record.agent_md_notified) {
|
|
1376
|
+
const store = this._aidStore ?? new AIDStore({ aunPath: this.configModel.aunPath, encryptionSeed: this.configModel.seedPassword ?? '' });
|
|
1377
|
+
this._aidStore = store;
|
|
1378
|
+
const loaded = await store.load(subject);
|
|
1379
|
+
const signature = record.subject_signature;
|
|
1380
|
+
if (!loaded.ok || !loaded.data.aid.isPrivateKeyValid() || loaded.data.aid.certFingerprint !== signature?.cert_fingerprint) {
|
|
1381
|
+
await this._sendOwnershipControl({ to: subject, encrypt: true, payload: buildOwnershipMessage('bind.response', {
|
|
1382
|
+
requestId: String(record.request_id), proofId: String(record.proof_id), subjectAid: subject, ownerAid: owner,
|
|
1383
|
+
subject_type: record.type, statement: record.owner_statement, signature: record.owner_signature,
|
|
1384
|
+
}) });
|
|
1385
|
+
continue;
|
|
1386
|
+
}
|
|
1387
|
+
await this._publishOwnershipAgentMd(record);
|
|
1388
|
+
await this.call('message.send', { to: subject, encrypt: true, payload: buildOwnershipMessage('agent_md.updated', {
|
|
1389
|
+
requestId: String(record.request_id), proofId: String(record.proof_id), subjectAid: subject, ownerAid: owner,
|
|
1390
|
+
}) });
|
|
1391
|
+
record.agent_md_notified = true;
|
|
1392
|
+
}
|
|
1393
|
+
delete record.last_error;
|
|
1394
|
+
this._ownershipRecords.set(key, record);
|
|
1395
|
+
await this._persistOwnershipRecords();
|
|
1396
|
+
}
|
|
1397
|
+
catch (error) {
|
|
1398
|
+
record.last_error = error instanceof Error ? error.message : String(error);
|
|
1399
|
+
this._ownershipRecords.set(key, record);
|
|
1400
|
+
await this._persistOwnershipRecords();
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
async _ownershipAgentMdContent(record, content = '') {
|
|
1405
|
+
const subject = String(record.subject_aid ?? '').trim().toLowerCase();
|
|
1406
|
+
const owner = String(record.owner_aid ?? '').trim().toLowerCase();
|
|
1407
|
+
const proof = { proof_id: String(record.proof_id ?? ''), subject_aid: subject, owner_aid: owner };
|
|
1408
|
+
if (record.subject_signature)
|
|
1409
|
+
proof.subject_signature = record.subject_signature;
|
|
1410
|
+
if (record.owner_signature && subject !== owner)
|
|
1411
|
+
proof.owner_signature = record.owner_signature;
|
|
1412
|
+
const base = content || await this._agentMdManager.readContent(subject) || buildDefaultAgentMd(subject, { type: String(record.type ?? 'agent') || 'agent' });
|
|
1413
|
+
return buildOwnershipAgentMd(base, owner, proof);
|
|
1414
|
+
}
|
|
1415
|
+
async _publishSubjectOwnershipAgentMd(record, uploadPartial = false) {
|
|
1416
|
+
const subject = String(record.subject_aid ?? '').trim().toLowerCase();
|
|
1417
|
+
if (!this._currentAid?.isPrivateKeyValid() || this._currentAid.aid.toLowerCase() !== subject)
|
|
1418
|
+
return;
|
|
1419
|
+
if (record.subject_proof_published && !uploadPartial)
|
|
1420
|
+
return;
|
|
1421
|
+
const content = await this._ownershipAgentMdContent(record);
|
|
1422
|
+
try {
|
|
1423
|
+
const pending = await this._agentMdManager.pendingUpload(subject);
|
|
1424
|
+
const proof = validateAgentMdDocument(content).frontmatter.ownership_proof;
|
|
1425
|
+
if (pending && JSON.stringify(validateAgentMdDocument(pending.content).frontmatter.ownership_proof) === JSON.stringify(proof))
|
|
1426
|
+
await this._agentMdManager.resumePendingUpload(subject, this._currentAid);
|
|
1427
|
+
else {
|
|
1428
|
+
await this._agentMdManager.writeContent(subject, content);
|
|
1429
|
+
await this._agentMdManager.upload(content);
|
|
1430
|
+
}
|
|
1431
|
+
record.agent_md_uploaded = true;
|
|
1432
|
+
if (record.subject_signature && (record.owner_signature || subject === record.owner_aid))
|
|
1433
|
+
record.subject_proof_published = true;
|
|
1434
|
+
await this._persistOwnershipRecords();
|
|
1435
|
+
}
|
|
1436
|
+
catch (error) {
|
|
1437
|
+
this._clientLog.debug('ownership partial agent.md upload deferred: ' + (error instanceof Error ? error.message : String(error)));
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
async _publishOwnershipAgentMd(record) {
|
|
1441
|
+
if (record.agent_md_uploaded || !record.owner_signature || !record.subject_signature)
|
|
1442
|
+
return;
|
|
1443
|
+
if (!record.subject_identity) {
|
|
1444
|
+
const store = this._aidStore ?? new AIDStore({ aunPath: this.configModel.aunPath, encryptionSeed: this.configModel.seedPassword ?? '' });
|
|
1445
|
+
this._aidStore = store;
|
|
1446
|
+
const loaded = await store.load(String(record.subject_aid ?? ''));
|
|
1447
|
+
if (!loaded.ok || !loaded.data.aid.isPrivateKeyValid())
|
|
1448
|
+
throw new StateError('ownership subject signing identity is unavailable');
|
|
1449
|
+
if (!await this._agentMdManager.resumePendingUpload(String(record.subject_aid ?? ''), loaded.data.aid)) {
|
|
1450
|
+
const remote = await this._agentMdManager.download(String(record.subject_aid ?? ''));
|
|
1451
|
+
await this._agentMdManager.uploadAs(String(record.subject_aid ?? ''), await this._ownershipAgentMdContent(record, remote.content), loaded.data.aid);
|
|
1452
|
+
}
|
|
1453
|
+
record.agent_md_uploaded = true;
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
const identity = record.subject_identity;
|
|
1457
|
+
const subject = String(record.subject_aid ?? '').trim().toLowerCase();
|
|
1458
|
+
const signer = await AID.create({
|
|
1459
|
+
aid: String(identity.aid ?? subject).trim().toLowerCase(),
|
|
1460
|
+
aunPath: this.configModel.aunPath,
|
|
1461
|
+
certPem: String(identity.cert ?? '').trim(),
|
|
1462
|
+
privateKeyPem: String(identity.private_key_pem ?? ''),
|
|
1463
|
+
certValid: true,
|
|
1464
|
+
privateKeyValid: true,
|
|
1465
|
+
});
|
|
1466
|
+
if (!await this._agentMdManager.resumePendingUpload(subject, signer)) {
|
|
1467
|
+
const remote = await this._agentMdManager.download(subject);
|
|
1468
|
+
await this._agentMdManager.uploadAs(subject, await this._ownershipAgentMdContent(record, remote.content), signer);
|
|
1469
|
+
}
|
|
1470
|
+
record.agent_md_uploaded = true;
|
|
1471
|
+
}
|
|
1472
|
+
async releaseOwnership(_subjectAid) {
|
|
1473
|
+
throw new StateError('legacy ownership workflow is unsupported', { stringCode: 'unsupported_protocol' });
|
|
1474
|
+
}
|
|
1475
|
+
async release_ownership(subjectAid) { return await this.releaseOwnership(subjectAid); }
|
|
1476
|
+
_scheduleOwnershipRelease(key) {
|
|
1477
|
+
if (this._ownershipReleaseTasks.has(key))
|
|
1478
|
+
return;
|
|
1479
|
+
const task = this._runOwnershipRelease(key).finally(() => {
|
|
1480
|
+
if (this._ownershipReleaseTasks.get(key) === task)
|
|
1481
|
+
this._ownershipReleaseTasks.delete(key);
|
|
1482
|
+
});
|
|
1483
|
+
this._ownershipReleaseTasks.set(key, task);
|
|
1484
|
+
}
|
|
1485
|
+
async _runOwnershipRelease(key) {
|
|
1486
|
+
const record = this._ownershipRecords.get(key);
|
|
1487
|
+
if (!record || !['pending_rekey', 'pending_agent_md', 'pending_upload'].includes(String(record.release_status ?? '')))
|
|
1488
|
+
return;
|
|
1489
|
+
const active = !['no_identity', 'standby', 'closed'].includes(String(this._state).toLowerCase());
|
|
1490
|
+
const reconnect = active && record.release_status === 'pending_rekey' && this._currentAid?.certFingerprint === record.release_old_fingerprint;
|
|
1491
|
+
const options = { ...this._sessionOptions };
|
|
1492
|
+
try {
|
|
1493
|
+
if (reconnect)
|
|
1494
|
+
await this.disconnect();
|
|
1495
|
+
await this._resumeOwnershipRelease(key);
|
|
1496
|
+
}
|
|
1497
|
+
catch (error) {
|
|
1498
|
+
record.last_error = error instanceof Error ? error.message : String(error);
|
|
1499
|
+
this._ownershipRecords.set(key, record);
|
|
1500
|
+
await this._persistOwnershipRecords();
|
|
1501
|
+
this._clientLog.debug(`ownership release deferred: ${error instanceof Error ? error.message : String(error)}`);
|
|
1502
|
+
}
|
|
1503
|
+
finally {
|
|
1504
|
+
if (reconnect && this._state !== 'closed') {
|
|
1505
|
+
try {
|
|
1506
|
+
await this._lifecycle.connect(options);
|
|
1507
|
+
await this._resumeOwnershipRelease(key);
|
|
1508
|
+
}
|
|
1509
|
+
catch (error) {
|
|
1510
|
+
this._clientLog.debug(`ownership release reconnect deferred: ${error instanceof Error ? error.message : String(error)}`);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
_resumePendingOwnershipReleases() {
|
|
1516
|
+
for (const [key, record] of this._ownershipRecords) {
|
|
1517
|
+
if (['pending_rekey', 'pending_agent_md', 'pending_upload'].includes(String(record.release_status ?? '')))
|
|
1518
|
+
this._scheduleOwnershipRelease(key);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
async _resumeOwnershipRelease(key) {
|
|
1522
|
+
const record = this._ownershipRecords.get(key);
|
|
1523
|
+
if (!record || !['pending_rekey', 'pending_agent_md', 'pending_upload'].includes(String(record.release_status ?? '')))
|
|
1524
|
+
return;
|
|
1525
|
+
const subject = String(record.subject_aid ?? '').trim().toLowerCase();
|
|
1526
|
+
const owner = String(record.owner_aid ?? '').trim().toLowerCase();
|
|
1527
|
+
const release = record.ownership_release;
|
|
1528
|
+
const store = this._aidStore;
|
|
1529
|
+
if (!subject || !owner || !release || !store)
|
|
1530
|
+
return;
|
|
1531
|
+
const loaded = await store.load(subject);
|
|
1532
|
+
if (!loaded.ok || !loaded.data)
|
|
1533
|
+
throw new StateError(`ownership release could not load subject identity: ${subject}`);
|
|
1534
|
+
const oldFingerprint = String(record.release_old_fingerprint ?? '');
|
|
1535
|
+
if (oldFingerprint && loaded.data.aid.certFingerprint === oldFingerprint) {
|
|
1536
|
+
const result = await store.rekey(subject);
|
|
1537
|
+
if (!result.ok)
|
|
1538
|
+
throw new StateError(`ownership release rekey failed: ${result.error.message}`);
|
|
1539
|
+
const refreshed = await store.load(subject);
|
|
1540
|
+
if (!refreshed.ok || !refreshed.data)
|
|
1541
|
+
throw new StateError(`ownership release rekeyed identity could not be loaded: ${subject}`);
|
|
1542
|
+
record.release_status = 'pending_agent_md';
|
|
1543
|
+
this._ownershipRecords.set(key, record);
|
|
1544
|
+
await this._persistOwnershipRecords();
|
|
1545
|
+
this._state = 'no_identity';
|
|
1546
|
+
this.loadIdentity(refreshed.data.aid);
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
if (record.release_status === 'pending_rekey')
|
|
1550
|
+
record.release_status = 'pending_agent_md';
|
|
1551
|
+
this._ownershipRecords.set(key, record);
|
|
1552
|
+
await this._persistOwnershipRecords();
|
|
1553
|
+
if (this._currentAid?.certFingerprint !== loaded.data.aid.certFingerprint) {
|
|
1554
|
+
this._state = 'no_identity';
|
|
1555
|
+
this.loadIdentity(loaded.data.aid);
|
|
1556
|
+
}
|
|
1557
|
+
let payload = String(record.release_agent_md_payload ?? '');
|
|
1558
|
+
if (!payload) {
|
|
1559
|
+
const localContent = String(record.release_local_content ?? '');
|
|
1560
|
+
let remoteContent = '';
|
|
1561
|
+
try {
|
|
1562
|
+
const downloaded = await this._agentMdManager.download(subject);
|
|
1563
|
+
remoteContent = downloaded.content;
|
|
1564
|
+
const remoteFrontmatter = validateAgentMdDocument(remoteContent, { expectedAid: subject, validateOwnership: false }).frontmatter;
|
|
1565
|
+
const remoteStatus = String(downloaded.verification.ownership_status ?? '');
|
|
1566
|
+
const currentRelease = remoteFrontmatter.ownership_release;
|
|
1567
|
+
if (currentRelease && currentRelease.statement.proof_id === release.statement.proof_id && remoteStatus === 'released') {
|
|
1568
|
+
record.release_status = 'released';
|
|
1569
|
+
record.status = 'released';
|
|
1570
|
+
delete record.release_agent_md_payload;
|
|
1571
|
+
delete record.release_local_content;
|
|
1572
|
+
this._ownershipRecords.set(key, record);
|
|
1573
|
+
await this._persistOwnershipRecords();
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
const proof = remoteFrontmatter.ownership_proof;
|
|
1577
|
+
if (remoteFrontmatter.owner !== owner || String(proof?.proof_id ?? '') !== String(record.proof_id ?? '') || remoteStatus !== 'complete') {
|
|
1578
|
+
throw new StateError('remote agent.md does not contain the active ownership relation being released');
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
catch (error) {
|
|
1582
|
+
if (!(error instanceof NotFoundError))
|
|
1583
|
+
throw error;
|
|
1584
|
+
}
|
|
1585
|
+
if (!localContent && !remoteContent)
|
|
1586
|
+
throw new StateError('ownership release has no local or remote agent.md to preserve');
|
|
1587
|
+
const localTimestamp = Number(parseAgentMdTailSignature(localContent).fields?.timestamp ?? 0) || 0;
|
|
1588
|
+
const remoteTimestamp = Number(parseAgentMdTailSignature(remoteContent).fields?.timestamp ?? 0) || 0;
|
|
1589
|
+
const base = localContent && localTimestamp > remoteTimestamp ? localContent : remoteContent || localContent;
|
|
1590
|
+
payload = buildOwnershipReleaseAgentMd(base, release);
|
|
1591
|
+
record.release_agent_md_payload = payload;
|
|
1592
|
+
record.release_status = 'pending_upload';
|
|
1593
|
+
this._ownershipRecords.set(key, record);
|
|
1594
|
+
await this._persistOwnershipRecords();
|
|
1595
|
+
await this._agentMdManager.writeContent(subject, payload);
|
|
1596
|
+
}
|
|
1597
|
+
const pending = await this._agentMdManager.pendingUpload(subject);
|
|
1598
|
+
const pendingRelease = pending ? validateAgentMdDocument(pending.content, { validateOwnership: false }).frontmatter.ownership_release : undefined;
|
|
1599
|
+
if (pendingRelease?.statement.proof_id === release.statement.proof_id)
|
|
1600
|
+
await this._agentMdManager.resumePendingUpload(subject);
|
|
1601
|
+
else
|
|
1602
|
+
await this._agentMdManager.upload(payload);
|
|
1603
|
+
record.release_status = 'released';
|
|
1604
|
+
record.status = 'released';
|
|
1605
|
+
delete record.release_agent_md_payload;
|
|
1606
|
+
delete record.release_local_content;
|
|
1607
|
+
this._ownershipRecords.set(key, record);
|
|
1608
|
+
await this._persistOwnershipRecords();
|
|
1609
|
+
}
|
|
1610
|
+
async bindAid(_subjectAid, _options = {}) {
|
|
1611
|
+
throw new StateError('legacy ownership workflow is unsupported', { stringCode: 'unsupported_protocol' });
|
|
1612
|
+
}
|
|
1613
|
+
async bind_aid(subjectAid, options = {}) { return await this.bindAid(subjectAid, options); }
|
|
1614
|
+
async bindOwner(_ownerAid, _options = {}) {
|
|
1615
|
+
throw new StateError('legacy ownership workflow is unsupported', { stringCode: 'unsupported_protocol' });
|
|
1616
|
+
}
|
|
1617
|
+
async bind_owner(ownerAid, options = {}) { return await this.bindOwner(ownerAid, options); }
|
|
1618
|
+
_ownershipPendingRequest(requestId) {
|
|
1619
|
+
const id = String(requestId ?? '').trim();
|
|
1620
|
+
for (const [key, record] of this._ownershipRecords) {
|
|
1621
|
+
if (record.ownership_request_pending === true && String(record.request_id ?? '').trim() === id)
|
|
1622
|
+
return [key, { ...record }];
|
|
1623
|
+
}
|
|
1624
|
+
throw new StateError('no pending ownership request found');
|
|
1625
|
+
}
|
|
1626
|
+
async acceptOwnershipRequest(operationId) {
|
|
1627
|
+
return await this._ownershipV21().acceptOwnershipRequest(operationId);
|
|
1628
|
+
}
|
|
1629
|
+
async rejectOwnershipRequest(operationId, reason) {
|
|
1630
|
+
return await this._ownershipV21().rejectOwnershipRequest(operationId, reason);
|
|
1631
|
+
}
|
|
1632
|
+
async _handleOwnershipMessage(message) {
|
|
1633
|
+
const envelope = message && typeof message === 'object' && !Array.isArray(message) ? message : null;
|
|
1634
|
+
const content = envelope && Object.hasOwn(envelope, 'payload') ? envelope.payload : message;
|
|
1635
|
+
return await this._ownershipV21().receive(String(envelope?.from_aid ?? envelope?.from ?? ''), decryptedOwnershipJsonText(content) ?? content);
|
|
1636
|
+
}
|
|
978
1637
|
get hasIdentity() {
|
|
979
1638
|
return this._currentAid !== null && this.state !== ConnectionState.CLOSED;
|
|
980
1639
|
}
|
|
@@ -1116,13 +1775,18 @@ export class AUNClient {
|
|
|
1116
1775
|
},
|
|
1117
1776
|
auth: () => this._auth,
|
|
1118
1777
|
tokenStore: () => this._tokenStore,
|
|
1119
|
-
fetchPeerCert: (target, certFingerprint) => this._fetchPeerCert(target, certFingerprint || undefined),
|
|
1778
|
+
fetchPeerCert: (target, certFingerprint, signedAt) => this._fetchPeerCert(target, certFingerprint || undefined, undefined, signedAt),
|
|
1779
|
+
entityOwnerResolver: (target, type) => this._resolveEntityAgentMdOwner(target, type),
|
|
1780
|
+
entityIdentityGetter: (target, type) => this._freshEntityCertificate(target, type),
|
|
1781
|
+
entitySignerResolver: (target, fingerprint) => this._freshEntitySigner(target, fingerprint),
|
|
1120
1782
|
});
|
|
1121
1783
|
this._transport = new RPCTransport({
|
|
1122
1784
|
eventDispatcher: this._dispatcher,
|
|
1123
1785
|
timeout: DEFAULT_SESSION_OPTIONS.timeouts.call,
|
|
1124
1786
|
});
|
|
1787
|
+
this._transport.setServiceEventVerifier((event, payload) => this._verifyIncomingServiceEvent(event, payload));
|
|
1125
1788
|
this._transport.setProtocolDisconnectCallback((error, closeCode) => this._handleTransportDisconnect(error, closeCode));
|
|
1789
|
+
this._transport._onForegroundDrained = () => this._delivery.onForegroundPullsDrained();
|
|
1126
1790
|
this._transport.setProtocolMetaObserver((meta) => this._observeRpcMeta(meta).catch((exc) => {
|
|
1127
1791
|
this._clientLog.debug(`rpc meta observer skipped: ${String(exc)}`);
|
|
1128
1792
|
}));
|
|
@@ -1137,7 +1801,17 @@ export class AUNClient {
|
|
|
1137
1801
|
}
|
|
1138
1802
|
}
|
|
1139
1803
|
loadIdentity(aid) {
|
|
1804
|
+
const previousAid = this._aid;
|
|
1140
1805
|
this._identityRuntime.loadIdentity(aid);
|
|
1806
|
+
this._dispatcher.setEventIdentity(this._aid ?? '');
|
|
1807
|
+
if (previousAid !== this._aid)
|
|
1808
|
+
this._serviceEventIdentityVersion++;
|
|
1809
|
+
this._ensureReadStateIdentity();
|
|
1810
|
+
if (previousAid !== this._aid) {
|
|
1811
|
+
this._ownershipRecords.clear();
|
|
1812
|
+
this._ownershipType = 'agent';
|
|
1813
|
+
}
|
|
1814
|
+
this._ownershipRecordsLoad = this._restoreOwnershipRecords();
|
|
1141
1815
|
}
|
|
1142
1816
|
setProtectedHeaders(headers) {
|
|
1143
1817
|
if (!headers) {
|
|
@@ -1184,7 +1858,7 @@ export class AUNClient {
|
|
|
1184
1858
|
}
|
|
1185
1859
|
/** 连接到 Gateway;身份来自构造函数或 loadIdentity(aid),认证由 SDK 内部自动完成。 */
|
|
1186
1860
|
async connect(opts) {
|
|
1187
|
-
|
|
1861
|
+
await this._lifecycle.connect(opts);
|
|
1188
1862
|
}
|
|
1189
1863
|
/** 断开连接但保留本地状态,可再次 connect */
|
|
1190
1864
|
async disconnect() {
|
|
@@ -1202,8 +1876,13 @@ export class AUNClient {
|
|
|
1202
1876
|
* 自动解密 message.pull/group.pull、Group E2EE 生命周期编排。
|
|
1203
1877
|
*/
|
|
1204
1878
|
async call(method, params) {
|
|
1879
|
+
const readIdentity = ['message.read', 'message.read_state', 'group.read', 'group.read_state'].includes(method)
|
|
1880
|
+
? this._ensureReadStateIdentity() : null;
|
|
1205
1881
|
try {
|
|
1206
|
-
|
|
1882
|
+
const result = await this._rpcPipeline.call(method, params);
|
|
1883
|
+
if (readIdentity !== null && readIdentity === this._ensureReadStateIdentity())
|
|
1884
|
+
this._observeReadStateResult(method, result, params ?? {});
|
|
1885
|
+
return result;
|
|
1207
1886
|
}
|
|
1208
1887
|
catch (err) {
|
|
1209
1888
|
await this._maybeHandleHalfOpenRpcFailure(err);
|
|
@@ -1259,18 +1938,36 @@ export class AUNClient {
|
|
|
1259
1938
|
const target = message.slice(agentPrefix.length).trim();
|
|
1260
1939
|
if (!target || target.toLowerCase() !== String(this._aid ?? '').trim().toLowerCase())
|
|
1261
1940
|
return false;
|
|
1262
|
-
|
|
1941
|
+
const content = await this._agentMdManager.readContent(target) ?? buildDefaultAgentMd(target);
|
|
1942
|
+
if (!this._ordinaryAdmissionAgentMd(content, target))
|
|
1943
|
+
return false;
|
|
1944
|
+
await this._agentMdManager.upload(content);
|
|
1263
1945
|
return true;
|
|
1264
1946
|
}
|
|
1947
|
+
_ordinaryAdmissionAgentMd(content, aid) {
|
|
1948
|
+
const meta = validateAgentMdDocument(content, { expectedAid: aid, validateOwnership: false, checkTimestamp: false }).frontmatter;
|
|
1949
|
+
return !['owner', 'ownership', 'ownership_proof', 'ownership_release'].some((field) => Object.hasOwn(meta, field));
|
|
1950
|
+
}
|
|
1265
1951
|
async _checkIdentityAdmissionAfterConnect() {
|
|
1266
1952
|
const aid = String(this._aid ?? this._currentAid?.aid ?? '').trim();
|
|
1267
1953
|
if (!aid)
|
|
1268
1954
|
return;
|
|
1269
1955
|
try {
|
|
1270
|
-
const
|
|
1271
|
-
if (
|
|
1272
|
-
|
|
1273
|
-
|
|
1956
|
+
const pending = await this._agentMdManager.pendingUpload(aid);
|
|
1957
|
+
if (pending) {
|
|
1958
|
+
if (this._ordinaryAdmissionAgentMd(pending.content, aid))
|
|
1959
|
+
await this._agentMdManager.resumePendingUpload(aid);
|
|
1960
|
+
}
|
|
1961
|
+
else {
|
|
1962
|
+
const checked = await this._agentMdManager.check(aid, 0);
|
|
1963
|
+
if (!checked.remote_found) {
|
|
1964
|
+
const content = await this._agentMdManager.readContent(aid) ?? buildDefaultAgentMd(aid);
|
|
1965
|
+
if (this._ordinaryAdmissionAgentMd(content, aid))
|
|
1966
|
+
await this._agentMdManager.upload(content);
|
|
1967
|
+
}
|
|
1968
|
+
else if (checked.in_sync === false) {
|
|
1969
|
+
await this._agentMdManager.download(aid);
|
|
1970
|
+
}
|
|
1274
1971
|
}
|
|
1275
1972
|
}
|
|
1276
1973
|
catch (exc) {
|
|
@@ -1279,6 +1976,7 @@ export class AUNClient {
|
|
|
1279
1976
|
const store = this._aidStore;
|
|
1280
1977
|
if (!store)
|
|
1281
1978
|
return;
|
|
1979
|
+
await this._resumeOrgIdentityAfterConnect(store);
|
|
1282
1980
|
let listed;
|
|
1283
1981
|
try {
|
|
1284
1982
|
listed = await this.call('group.list_my', {});
|
|
@@ -1314,6 +2012,8 @@ export class AUNClient {
|
|
|
1314
2012
|
await this.bindGroupAid({ group_id: groupId, group_aid: groupAid }, { aidStore: store });
|
|
1315
2013
|
return;
|
|
1316
2014
|
}
|
|
2015
|
+
if (await this._resumeGroupAgentMdUpload(store, groupAid))
|
|
2016
|
+
return;
|
|
1317
2017
|
const checked = await this._agentMdManager.check(groupAid);
|
|
1318
2018
|
if (!checked.remote_found) {
|
|
1319
2019
|
await this._uploadGroupAgentMd(store, groupAid, {}, {}, aid);
|
|
@@ -1344,6 +2044,206 @@ export class AUNClient {
|
|
|
1344
2044
|
this._groupIdentityOperations.delete(key);
|
|
1345
2045
|
}
|
|
1346
2046
|
}
|
|
2047
|
+
async createOrg(params = {}, options = {}) {
|
|
2048
|
+
const store = options.aidStore;
|
|
2049
|
+
const owner = this._currentAid;
|
|
2050
|
+
const orgAid = String(params.org_id ?? '').trim();
|
|
2051
|
+
if (!store || !owner?.isPrivateKeyValid() || !orgAid)
|
|
2052
|
+
throw new ValidationError('createOrg requires aidStore, org_id and current owner identity');
|
|
2053
|
+
this._aidStore = store;
|
|
2054
|
+
let identity = await store.load(orgAid);
|
|
2055
|
+
if (!identity.ok) {
|
|
2056
|
+
const registered = await store.register(orgAid, { type: 'org', owner: owner.aid });
|
|
2057
|
+
if (!registered.ok)
|
|
2058
|
+
throw new StateError(registered.error.message, { stringCode: 'unavailable', retryable: true });
|
|
2059
|
+
identity = await store.load(orgAid);
|
|
2060
|
+
}
|
|
2061
|
+
if (!identity.ok)
|
|
2062
|
+
throw new StateError(identity.error.message, { stringCode: 'unavailable', retryable: true });
|
|
2063
|
+
const target = options.serviceAid ?? `org.${orgAid.slice(orgAid.indexOf('.') + 1)}`;
|
|
2064
|
+
const tokens = store._tokenStore;
|
|
2065
|
+
const inputHash = await ownershipSha256(canonicalJson(params));
|
|
2066
|
+
const raw = await tokens.getMetadata(orgAid, 'org_identity_creation');
|
|
2067
|
+
let checkpoint = raw ? JSON.parse(raw) : undefined;
|
|
2068
|
+
if (checkpoint && (checkpoint.owner_aid !== owner.aid || checkpoint.input_hash !== inputHash))
|
|
2069
|
+
throw new StateError('pending Org creation context changed', { stringCode: 'conflict', retryable: false });
|
|
2070
|
+
if (!checkpoint) {
|
|
2071
|
+
const prepared = await this.org.prepareRequest('org.lifecycle.create', { ...params, owner_aid: owner.aid }, target, owner, { orgSigner: identity.data.aid });
|
|
2072
|
+
checkpoint = { owner_aid: owner.aid, input_hash: inputHash, source_params: structuredClone(params), prepared, phase: 'create' };
|
|
2073
|
+
await tokens.setMetadata(orgAid, 'org_identity_creation', JSON.stringify(checkpoint));
|
|
2074
|
+
}
|
|
2075
|
+
if (checkpoint.phase === 'create') {
|
|
2076
|
+
const result = await this.call('org.lifecycle.create', checkpoint.prepared);
|
|
2077
|
+
checkpoint = { ...checkpoint, result, phase: 'upload' };
|
|
2078
|
+
await tokens.setMetadata(orgAid, 'org_identity_creation', JSON.stringify(checkpoint));
|
|
2079
|
+
}
|
|
2080
|
+
await this._uploadOrgAgentMd(store, orgAid, checkpoint.source_params);
|
|
2081
|
+
await tokens.setMetadata(orgAid, 'org_identity_creation', '');
|
|
2082
|
+
return checkpoint.result;
|
|
2083
|
+
}
|
|
2084
|
+
async _resumeOrgIdentityAfterConnect(store) {
|
|
2085
|
+
const keyStore = store._keystore;
|
|
2086
|
+
const tokens = store._tokenStore;
|
|
2087
|
+
if (typeof keyStore?.listIdentities !== 'function' || typeof tokens?.getMetadata !== 'function')
|
|
2088
|
+
return;
|
|
2089
|
+
for (const aid of await keyStore.listIdentities()) {
|
|
2090
|
+
try {
|
|
2091
|
+
const creationRaw = await tokens.getMetadata(aid, 'org_identity_creation');
|
|
2092
|
+
const transferRaw = await tokens.getMetadata(aid, 'org_identity_transfer');
|
|
2093
|
+
const creation = creationRaw ? JSON.parse(creationRaw) : undefined;
|
|
2094
|
+
const transfer = transferRaw ? JSON.parse(transferRaw) : undefined;
|
|
2095
|
+
if (creation?.owner_aid === this._aid) {
|
|
2096
|
+
await this.createOrg(creation.source_params, { aidStore: store });
|
|
2097
|
+
continue;
|
|
2098
|
+
}
|
|
2099
|
+
if (transfer?.owner_aid === this._aid) {
|
|
2100
|
+
await this.completeOrgTransfer(transfer.source_params, { aidStore: store });
|
|
2101
|
+
continue;
|
|
2102
|
+
}
|
|
2103
|
+
const pending = await store._agentMdManager.pendingUpload(aid);
|
|
2104
|
+
if (!pending)
|
|
2105
|
+
continue;
|
|
2106
|
+
const fields = validateAgentMdDocument(pending.content, { expectedAid: aid }).frontmatter;
|
|
2107
|
+
if (fields.type === 'org' && fields.owner === this._aid)
|
|
2108
|
+
await this._resumeGroupAgentMdUpload(store, aid);
|
|
2109
|
+
}
|
|
2110
|
+
catch (error) {
|
|
2111
|
+
this._clientLog.warn(`post-connect Org identity recovery failed: aid=${aid} err=${String(error)}`);
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
async completeOrgTransfer(params = {}, options = {}) {
|
|
2116
|
+
const store = options.aidStore;
|
|
2117
|
+
const owner = this._currentAid;
|
|
2118
|
+
const orgAid = String(params.org_id ?? '').trim();
|
|
2119
|
+
const transferId = String(params.transfer_id ?? '').trim();
|
|
2120
|
+
if (!store || !owner?.isPrivateKeyValid() || !orgAid || !transferId)
|
|
2121
|
+
throw new ValidationError('completeOrgTransfer requires aidStore, org_id, transfer_id and current accepting owner');
|
|
2122
|
+
this._aidStore = store;
|
|
2123
|
+
const localStore = store._keystore;
|
|
2124
|
+
const tokenStore = store._tokenStore;
|
|
2125
|
+
const inputHash = await ownershipSha256(canonicalJson(params));
|
|
2126
|
+
const raw = await tokenStore.getMetadata(orgAid, 'org_identity_transfer');
|
|
2127
|
+
let pending = raw ? JSON.parse(raw) : undefined;
|
|
2128
|
+
if (pending && (pending.transfer_id !== transferId || pending.owner_aid !== owner.aid || pending.input_hash !== inputHash))
|
|
2129
|
+
throw new StateError('pending Org transfer context changed', { stringCode: 'conflict', retryable: false });
|
|
2130
|
+
for (const field of ['previous_owner', 'new_owner']) {
|
|
2131
|
+
const material = params[field];
|
|
2132
|
+
if (!isJsonObject(material) || !isJsonObject(material.membership_binding) || !isJsonObject(material.terms) || field === 'previous_owner' && !isJsonObject(material.credential_template))
|
|
2133
|
+
throw new StateError('Org transfer requires both replacement membership bindings, terms and previous-owner credential', { stringCode: 'invalid', retryable: false });
|
|
2134
|
+
}
|
|
2135
|
+
const pendingKey = `org-transfer:${orgAid}:${transferId}`;
|
|
2136
|
+
let keyPair = await localStore.loadPendingGroupBind(pendingKey);
|
|
2137
|
+
if (!keyPair) {
|
|
2138
|
+
if (pending)
|
|
2139
|
+
throw new StateError('Org transfer private key checkpoint is unavailable', { stringCode: 'persistence_unavailable', retryable: false });
|
|
2140
|
+
keyPair = await new CryptoProvider().generateIdentity();
|
|
2141
|
+
await localStore.savePendingGroupBind(pendingKey, keyPair);
|
|
2142
|
+
}
|
|
2143
|
+
const target = options.serviceAid ?? `org.${orgAid.slice(orgAid.indexOf('.') + 1)}`;
|
|
2144
|
+
const publicKey = arrayBufferToPem(base64ToUint8(keyPair.public_key_der_b64).buffer, 'PUBLIC KEY');
|
|
2145
|
+
if (pending && pending.public_key !== publicKey)
|
|
2146
|
+
throw new StateError('pending Org transfer key changed', { stringCode: 'conflict', retryable: false });
|
|
2147
|
+
if (!pending) {
|
|
2148
|
+
const rekey = { ...params, stage: 'rekey', public_key: publicKey, curve: 'P-256' };
|
|
2149
|
+
delete rekey.new_owner;
|
|
2150
|
+
delete rekey.request_binding;
|
|
2151
|
+
delete rekey.client_signature;
|
|
2152
|
+
pending = { transfer_id: transferId, owner_aid: owner.aid, public_key: publicKey, input_hash: inputHash, phase: 'rekey', source_params: structuredClone(params), rekey_params: await this.org.prepareRequest('org.lifecycle.transferAccept', rekey, target, owner) };
|
|
2153
|
+
await tokenStore.setMetadata(orgAid, 'org_identity_transfer', JSON.stringify(pending));
|
|
2154
|
+
}
|
|
2155
|
+
if (pending.phase === 'rekey') {
|
|
2156
|
+
const result = await this.call('org.lifecycle.transferAccept', pending.rekey_params);
|
|
2157
|
+
pending = { ...pending, phase: 'finalize', rekey_result: result };
|
|
2158
|
+
await tokenStore.setMetadata(orgAid, 'org_identity_transfer', JSON.stringify(pending));
|
|
2159
|
+
}
|
|
2160
|
+
const rekeyResult = pending.rekey_result;
|
|
2161
|
+
const certPem = String(rekeyResult?.cert_pem ?? rekeyResult?.cert ?? '').trim();
|
|
2162
|
+
if (!certPem)
|
|
2163
|
+
throw new StateError('Org rekey response has no certificate', { stringCode: 'unavailable', retryable: true });
|
|
2164
|
+
const imported = await store.importGroupIdentity(orgAid, { ...keyPair, cert_pem: certPem });
|
|
2165
|
+
if (!imported.ok)
|
|
2166
|
+
throw new StateError(imported.error.message, { stringCode: 'invalid', retryable: false });
|
|
2167
|
+
const loaded = await store.load(orgAid);
|
|
2168
|
+
if (!loaded.ok)
|
|
2169
|
+
throw new StateError(loaded.error.message, { stringCode: 'unavailable', retryable: true });
|
|
2170
|
+
const orgIdentity = loaded.data.aid;
|
|
2171
|
+
if (!pending.finalize_params) {
|
|
2172
|
+
const final = { ...pending.source_params, stage: 'finalize', public_key: publicKey, curve: 'P-256', request_id: crypto.randomUUID().replace(/-/g, ''), nonce: randomBase64Url(32) };
|
|
2173
|
+
delete final.request_binding;
|
|
2174
|
+
delete final.client_signature;
|
|
2175
|
+
for (const field of ['previous_owner', 'new_owner']) {
|
|
2176
|
+
const material = final[field];
|
|
2177
|
+
let binding = material.membership_binding;
|
|
2178
|
+
if (!binding.member_signature && binding.member_aid === owner.aid)
|
|
2179
|
+
binding = await signMembershipBinding(binding, owner, 'member');
|
|
2180
|
+
binding = await signMembershipBinding(binding, orgIdentity, 'org');
|
|
2181
|
+
const oldCredential = isJsonObject(material.credential_template) ? { ...material.credential_template } : undefined;
|
|
2182
|
+
if (oldCredential) {
|
|
2183
|
+
delete oldCredential.signature;
|
|
2184
|
+
oldCredential.key_version = orgIdentity.certFingerprint;
|
|
2185
|
+
}
|
|
2186
|
+
const credential = oldCredential ? await createCredential(oldCredential, orgIdentity) : await createMemberCredential(binding, orgIdentity, { key_version: orgIdentity.certFingerprint });
|
|
2187
|
+
final[field] = { ...material, membership_binding: binding, credential_template: credential };
|
|
2188
|
+
delete final[field].registration_authorization;
|
|
2189
|
+
}
|
|
2190
|
+
pending = { ...pending, finalize_params: await this.org.prepareRequest('org.lifecycle.transferAccept', final, target, owner, { orgSigner: orgIdentity }) };
|
|
2191
|
+
await tokenStore.setMetadata(orgAid, 'org_identity_transfer', JSON.stringify(pending));
|
|
2192
|
+
}
|
|
2193
|
+
const result = await this.call('org.lifecycle.transferAccept', pending.finalize_params);
|
|
2194
|
+
await this._uploadOrgAgentMd(store, orgAid, pending.source_params);
|
|
2195
|
+
await tokenStore.setMetadata(orgAid, 'org_identity_transfer', '');
|
|
2196
|
+
await localStore.clearPendingGroupBind(pendingKey);
|
|
2197
|
+
return result;
|
|
2198
|
+
}
|
|
2199
|
+
async dissolveOrg(params = {}, options = {}) {
|
|
2200
|
+
const store = options.aidStore ?? this._aidStore;
|
|
2201
|
+
if (store)
|
|
2202
|
+
this._aidStore = store;
|
|
2203
|
+
const orgAid = String(params.org_id ?? '').trim();
|
|
2204
|
+
const owner = this._currentAid;
|
|
2205
|
+
const target = options.serviceAid ?? `org.${orgAid.slice(orgAid.indexOf('.') + 1)}`;
|
|
2206
|
+
const payload = owner ? await this.org.prepareRequest('org.lifecycle.dissolve', params, target, owner) : params;
|
|
2207
|
+
const result = await this.call('org.lifecycle.dissolve', payload);
|
|
2208
|
+
const organization = isJsonObject(result?.organization) ? result.organization : {};
|
|
2209
|
+
const status = String(organization.status ?? result?.status ?? '');
|
|
2210
|
+
if (store && orgAid && status === 'dissolved') {
|
|
2211
|
+
const tokenStore = store._tokenStore;
|
|
2212
|
+
const keyStore = store._keystore;
|
|
2213
|
+
const certPem = await keyStore?.loadCert?.(orgAid);
|
|
2214
|
+
const transferRaw = await tokenStore?.getMetadata?.(orgAid, 'org_identity_transfer');
|
|
2215
|
+
const transfer = transferRaw ? JSON.parse(transferRaw) : undefined;
|
|
2216
|
+
if (transfer?.transfer_id && typeof keyStore?.clearPendingGroupBind === 'function') {
|
|
2217
|
+
await keyStore.clearPendingGroupBind(`org-transfer:${orgAid}:${String(transfer.transfer_id)}`);
|
|
2218
|
+
}
|
|
2219
|
+
if (certPem)
|
|
2220
|
+
await tokenStore?.setMetadata?.(orgAid, 'invalidated_entity_certificate', await certificateSha256Fingerprint(certPem));
|
|
2221
|
+
await tokenStore?.setMetadata?.(orgAid, 'org_identity_creation', '');
|
|
2222
|
+
await tokenStore?.setMetadata?.(orgAid, 'org_identity_transfer', '');
|
|
2223
|
+
await tokenStore?.saveInstanceState?.(orgAid, store.deviceId, store.slotId, {});
|
|
2224
|
+
await store._agentMdManager?.saveRecord?.(orgAid, {
|
|
2225
|
+
pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '',
|
|
2226
|
+
pending_condition: {}, pending_relation: {}, pending_authorization: '',
|
|
2227
|
+
});
|
|
2228
|
+
}
|
|
2229
|
+
return result;
|
|
2230
|
+
}
|
|
2231
|
+
async _uploadOrgAgentMd(store, orgAid, params) {
|
|
2232
|
+
const owner = this._currentAid;
|
|
2233
|
+
if (!owner)
|
|
2234
|
+
throw new StateError('Org agent.md requires current owner', { stringCode: 'forbidden', retryable: false });
|
|
2235
|
+
if (await this._resumeGroupAgentMdUpload(store, orgAid))
|
|
2236
|
+
return;
|
|
2237
|
+
const loaded = await store.load(orgAid);
|
|
2238
|
+
if (!loaded.ok)
|
|
2239
|
+
throw new StateError(loaded.error.message, { stringCode: 'unavailable', retryable: true });
|
|
2240
|
+
const content = String(params.org_agent_md ?? buildDefaultAgentMd(orgAid, { type: 'org', owner: owner.aid, entityCertFingerprint: loaded.data.aid.certFingerprint, name: String(params.name ?? orgAid) }));
|
|
2241
|
+
const uploaded = await store.uploadAgentMd(orgAid, content, { uploaderAid: owner.aid, entityOwnerResolver: (aid, type) => this._resolveEntityAgentMdOwner(aid, type), entityIdentityGetter: (aid) => this._freshEntityCertificate(aid, 'org'), entitySignerResolver: (aid, fingerprint) => this._freshEntitySigner(aid, fingerprint) });
|
|
2242
|
+
if (!uploaded.ok) {
|
|
2243
|
+
const code = ['invalid', 'unavailable', 'forbidden', 'conflict', 'persistence_unavailable'].includes(uploaded.error.code) ? uploaded.error.code : uploaded.error.code === 'NETWORK_ERROR' ? 'unavailable' : 'invalid';
|
|
2244
|
+
throw new StateError(uploaded.error.message, { stringCode: code, retryable: code === 'unavailable' });
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
1347
2247
|
async createGroup(params = {}, options = {}) {
|
|
1348
2248
|
if (!isJsonObject(params)) {
|
|
1349
2249
|
throw new ValidationError('createGroup params must be an object');
|
|
@@ -1502,7 +2402,7 @@ export class AUNClient {
|
|
|
1502
2402
|
}
|
|
1503
2403
|
return { ...(isJsonObject(result) ? result : {}), invites, invite_errors: inviteErrors };
|
|
1504
2404
|
}
|
|
1505
|
-
_groupAgentMdContent(groupAid, params = {}, group = {}) {
|
|
2405
|
+
async _groupAgentMdContent(groupAid, params = {}, group = {}) {
|
|
1506
2406
|
const explicit = params.group_agent_md ?? params.groupAgentMd ?? params.content;
|
|
1507
2407
|
if (explicit !== undefined && explicit !== null)
|
|
1508
2408
|
return String(explicit);
|
|
@@ -1521,27 +2421,77 @@ export class AUNClient {
|
|
|
1521
2421
|
.split(/\s+/).filter(Boolean).join(' ');
|
|
1522
2422
|
const description = Array.from(normalizedDescription).slice(0, 100).join('');
|
|
1523
2423
|
const avatar = Array.from(value('avatar')).slice(0, 256).join('');
|
|
2424
|
+
const loaded = await this._aidStore?.load(groupAid);
|
|
2425
|
+
if (!loaded?.ok)
|
|
2426
|
+
throw new StateError('entity local certificate is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
1524
2427
|
return [
|
|
1525
2428
|
'---\n',
|
|
1526
2429
|
`aid: ${JSON.stringify(groupAid)}\n`,
|
|
1527
2430
|
`name: ${JSON.stringify(name)}\n`,
|
|
1528
2431
|
'type: "group"\n',
|
|
1529
2432
|
'version: "1.0.0"\n',
|
|
2433
|
+
`owner: ${JSON.stringify(String(group.owner_aid ?? group.owner ?? this._aid ?? ''))}\n`,
|
|
2434
|
+
`entity_cert_fingerprint: ${JSON.stringify(loaded.data.aid.certFingerprint)}\n`,
|
|
1530
2435
|
`description: ${JSON.stringify(description)}\n`,
|
|
1531
2436
|
...(avatar ? [`avatar: ${JSON.stringify(avatar)}\n`] : []),
|
|
1532
2437
|
'---\n\n',
|
|
1533
2438
|
`# ${name}\n`,
|
|
1534
2439
|
].join('');
|
|
1535
2440
|
}
|
|
2441
|
+
async _resumeGroupAgentMdUpload(store, groupAid) {
|
|
2442
|
+
const identityStore = store;
|
|
2443
|
+
const pendingManager = identityStore._agentMdManager ?? this._agentMdManager;
|
|
2444
|
+
const pending = await pendingManager.pendingUpload(groupAid);
|
|
2445
|
+
if (!pending)
|
|
2446
|
+
return false;
|
|
2447
|
+
const fields = validateAgentMdDocument(pending.content, { expectedAid: groupAid, validateOwnership: false }).frontmatter;
|
|
2448
|
+
if (['ownership', 'ownership_proof', 'ownership_release'].some((field) => Object.hasOwn(fields, field))) {
|
|
2449
|
+
throw new StateError('ownership pending agent.md requires its persisted 2.1 task', { stringCode: 'unsupported_protocol' });
|
|
2450
|
+
}
|
|
2451
|
+
const identity = this._currentAid;
|
|
2452
|
+
if (!identity || fields.owner !== identity.aid)
|
|
2453
|
+
throw new StateError('group agent.md recovery requires its original current owner', { stringCode: 'forbidden' });
|
|
2454
|
+
const manager = new AgentMdManager({
|
|
2455
|
+
aunPath: store.aunPath,
|
|
2456
|
+
tokenStore: identityStore._tokenStore,
|
|
2457
|
+
uploadViaGatewayHost: true,
|
|
2458
|
+
ownerAidGetter: () => identity.aid,
|
|
2459
|
+
currentAidGetter: () => identity,
|
|
2460
|
+
entityOwnerResolver: (aid, type) => this._resolveEntityAgentMdOwner(aid, type),
|
|
2461
|
+
entityIdentityGetter: (aid, type) => this._freshEntityCertificate(aid, type),
|
|
2462
|
+
entitySignerResolver: (aid, fingerprint) => this._freshEntitySigner(aid, fingerprint),
|
|
2463
|
+
gatewayResolver: (aid) => identityStore._resolveGateway(aid),
|
|
2464
|
+
accessTokenResolver: (_aid, gatewayUrl) => identityStore._uploadAgentMdToken(identity, gatewayUrl),
|
|
2465
|
+
aidValidator: (aid) => identityStore._registerFlow.validateAidName(aid),
|
|
2466
|
+
});
|
|
2467
|
+
return await manager.resumePendingUpload(groupAid);
|
|
2468
|
+
}
|
|
1536
2469
|
async _uploadGroupAgentMd(store, groupAid, params, group, uploaderAid) {
|
|
2470
|
+
if (await this._resumeGroupAgentMdUpload(store, groupAid))
|
|
2471
|
+
return;
|
|
1537
2472
|
if (typeof store.uploadAgentMd !== 'function') {
|
|
1538
2473
|
throw new ValidationError('group agent.md upload is unavailable');
|
|
1539
2474
|
}
|
|
1540
|
-
const uploaded = await store.uploadAgentMd(groupAid, this._groupAgentMdContent(groupAid, params, group), { uploaderAid });
|
|
2475
|
+
const uploaded = await store.uploadAgentMd(groupAid, await this._groupAgentMdContent(groupAid, params, group), { uploaderAid, entityOwnerResolver: (aid, type) => this._resolveEntityAgentMdOwner(aid, type), entityIdentityGetter: (aid, type) => this._freshEntityCertificate(aid, type), entitySignerResolver: (aid, fingerprint) => this._freshEntitySigner(aid, fingerprint) });
|
|
1541
2476
|
if (!uploaded.ok) {
|
|
1542
2477
|
throw new ValidationError(`group agent.md upload failed: ${uploaded.error.message}`);
|
|
1543
2478
|
}
|
|
1544
2479
|
}
|
|
2480
|
+
async _freshEntityCertificate(aid, type = 'group') {
|
|
2481
|
+
const cert = await this._auth.resolveCurrentEntityCertificate(await this._resolveOwnershipCertificateGateway(aid), aid, type);
|
|
2482
|
+
return AID.create({ aid, aunPath: this.configModel.aunPath, certPem: cert.cert_pem, certValid: true, privateKeyPem: null, privateKeyValid: false });
|
|
2483
|
+
}
|
|
2484
|
+
async _freshEntitySigner(aid, fingerprint) {
|
|
2485
|
+
const cert = await this._auth.resolveOwnershipCertificate(await this._resolveOwnershipCertificateGateway(aid), aid, fingerprint);
|
|
2486
|
+
return AID.create({ aid, aunPath: this.configModel.aunPath, certPem: cert.cert_pem, certValid: true, privateKeyPem: null, privateKeyValid: false });
|
|
2487
|
+
}
|
|
2488
|
+
async _resolveEntityAgentMdOwner(aid, type) {
|
|
2489
|
+
const authority = await this._agentMdManager.readEntityAuthorityFresh(aid);
|
|
2490
|
+
const certificate = await this._freshEntityCertificate(aid, type);
|
|
2491
|
+
if (authority.entity_cert_fingerprint !== certificate.certFingerprint)
|
|
2492
|
+
throw new StateError('entity certificate authority changed', { stringCode: 'conflict' });
|
|
2493
|
+
return authority.owner_aid;
|
|
2494
|
+
}
|
|
1545
2495
|
async bindGroupAid(params = {}, options = {}) {
|
|
1546
2496
|
if (!isJsonObject(params)) {
|
|
1547
2497
|
throw new ValidationError('bindGroupAid params must be an object');
|
|
@@ -1999,6 +2949,168 @@ export class AUNClient {
|
|
|
1999
2949
|
on(event, handler) {
|
|
2000
2950
|
return this._dispatcher.subscribe(event, handler);
|
|
2001
2951
|
}
|
|
2952
|
+
_readStateIsNewer(previous, current) {
|
|
2953
|
+
if (!previous)
|
|
2954
|
+
return true;
|
|
2955
|
+
const oldOrder = BigInt(String(previous.last_read_order ?? 0));
|
|
2956
|
+
const newOrder = BigInt(String(current.last_read_order ?? 0));
|
|
2957
|
+
if (oldOrder !== newOrder)
|
|
2958
|
+
return newOrder > oldOrder;
|
|
2959
|
+
const oldId = String(previous.last_read_message_id ?? '');
|
|
2960
|
+
const newId = String(current.last_read_message_id ?? '');
|
|
2961
|
+
if (oldOrder === 0n)
|
|
2962
|
+
return newId === oldId;
|
|
2963
|
+
const oldPoints = Array.from(oldId);
|
|
2964
|
+
const newPoints = Array.from(newId);
|
|
2965
|
+
for (let index = 0; index < Math.min(oldPoints.length, newPoints.length); index++) {
|
|
2966
|
+
const oldPoint = oldPoints[index].codePointAt(0);
|
|
2967
|
+
const newPoint = newPoints[index].codePointAt(0);
|
|
2968
|
+
if (oldPoint !== newPoint)
|
|
2969
|
+
return newPoint > oldPoint;
|
|
2970
|
+
}
|
|
2971
|
+
return newPoints.length >= oldPoints.length;
|
|
2972
|
+
}
|
|
2973
|
+
_ensureReadStateIdentity() {
|
|
2974
|
+
const identity = `${this._aid ?? ''}\u0000${this._deviceId ?? ''}`;
|
|
2975
|
+
if (identity !== this._readStateIdentity) {
|
|
2976
|
+
this._readStateCache.clear();
|
|
2977
|
+
this._readStateQueries.clear();
|
|
2978
|
+
this._readStateIdentity = identity;
|
|
2979
|
+
this._readStateGeneration += 1;
|
|
2980
|
+
}
|
|
2981
|
+
return `${identity}\u0000${this._readStateGeneration}`;
|
|
2982
|
+
}
|
|
2983
|
+
_registerReadStateQuery(method, params) {
|
|
2984
|
+
const key = method + '\u0000' + JSON.stringify(Object.keys(params).sort().map((field) => [field, params[field]]));
|
|
2985
|
+
this._readStateQueries.set(key, { method, params: { ...params } });
|
|
2986
|
+
}
|
|
2987
|
+
_readStateItems(result) {
|
|
2988
|
+
const resultObject = isJsonObject(result) ? result : null;
|
|
2989
|
+
return Array.isArray(resultObject?.states)
|
|
2990
|
+
? resultObject.states.filter((item) => isJsonObject(item))
|
|
2991
|
+
: [];
|
|
2992
|
+
}
|
|
2993
|
+
_observeReadStateResult(method, result, params) {
|
|
2994
|
+
if (!['message.read', 'message.read_state', 'group.read', 'group.read_state'].includes(method))
|
|
2995
|
+
return;
|
|
2996
|
+
this._ensureReadStateIdentity();
|
|
2997
|
+
const isGroup = method.startsWith('group.');
|
|
2998
|
+
if (method.endsWith('.read')) {
|
|
2999
|
+
for (const state of this._readStateItems(params)) {
|
|
3000
|
+
const query = { topic_key: state.topic_key ?? '' };
|
|
3001
|
+
if (isGroup)
|
|
3002
|
+
query.group_id = state.group_id ?? params.group_id ?? '';
|
|
3003
|
+
else
|
|
3004
|
+
query.scope = state.scope || params.scope || 'p2p';
|
|
3005
|
+
this._registerReadStateQuery(`${isGroup ? 'group' : 'message'}.read_state`, query);
|
|
3006
|
+
}
|
|
3007
|
+
if (!Array.isArray(params.states) && params.topic_key) {
|
|
3008
|
+
const query = {};
|
|
3009
|
+
for (const field of ['scope', 'group_id', 'topic_key']) {
|
|
3010
|
+
if (field in params)
|
|
3011
|
+
query[field] = params[field];
|
|
3012
|
+
}
|
|
3013
|
+
this._registerReadStateQuery(`${isGroup ? 'group' : 'message'}.read_state`, query);
|
|
3014
|
+
}
|
|
3015
|
+
return;
|
|
3016
|
+
}
|
|
3017
|
+
this._registerReadStateQuery(method, params);
|
|
3018
|
+
for (const state of this._readStateItems(result)) {
|
|
3019
|
+
const scope = String(state.scope ?? (isGroup ? 'group' : 'p2p')).trim();
|
|
3020
|
+
const topicKey = String(state.topic_key ?? params.topic_key ?? '');
|
|
3021
|
+
const groupId = isGroup ? String(state.group_id ?? params.group_id ?? '').trim() : '';
|
|
3022
|
+
const lastRead = String(state.last_read_message_id ?? '').trim();
|
|
3023
|
+
if (!scope || !topicKey || !lastRead || (isGroup && !groupId))
|
|
3024
|
+
continue;
|
|
3025
|
+
const key = `${scope}\u0000${groupId}\u0000${topicKey}`;
|
|
3026
|
+
const query = { topic_key: topicKey };
|
|
3027
|
+
if (isGroup)
|
|
3028
|
+
query.group_id = groupId;
|
|
3029
|
+
else
|
|
3030
|
+
query.scope = scope;
|
|
3031
|
+
const previous = this._readStateCache.get(key)?.state;
|
|
3032
|
+
if (previous && !this._readStateIsNewer(previous, state))
|
|
3033
|
+
continue;
|
|
3034
|
+
const samePosition = previous?.last_read_message_id === state.last_read_message_id
|
|
3035
|
+
&& BigInt(String(previous?.last_read_order ?? 0)) === BigInt(String(state.last_read_order ?? 0));
|
|
3036
|
+
this._readStateCache.set(key, {
|
|
3037
|
+
method: `${isGroup ? 'group' : 'message'}.read_state`,
|
|
3038
|
+
params: query,
|
|
3039
|
+
state: samePosition ? { ...previous, ...state } : { ...state },
|
|
3040
|
+
});
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
_onRawReadChanged(data, expectedScope) {
|
|
3044
|
+
if (!isJsonObject(data))
|
|
3045
|
+
return;
|
|
3046
|
+
const eventData = data;
|
|
3047
|
+
if (eventData.scope !== expectedScope || eventData.owner_aid !== this._aid)
|
|
3048
|
+
return;
|
|
3049
|
+
const fields = ['owner_aid', 'topic_key', 'last_read_message_id', ...(expectedScope === 'group' ? ['group_id'] : [])];
|
|
3050
|
+
if (fields.some((field) => {
|
|
3051
|
+
const value = eventData[field];
|
|
3052
|
+
return typeof value !== 'string' || !value || Array.from(value).length > 128;
|
|
3053
|
+
}))
|
|
3054
|
+
return;
|
|
3055
|
+
const order = eventData.last_read_order;
|
|
3056
|
+
if (!(typeof order === 'number' && Number.isSafeInteger(order) && order > 0)
|
|
3057
|
+
&& !(typeof order === 'string' && /^[1-9]\d*$/.test(order)))
|
|
3058
|
+
return;
|
|
3059
|
+
this._ensureReadStateIdentity();
|
|
3060
|
+
const scope = expectedScope;
|
|
3061
|
+
const topicKey = String(eventData.topic_key ?? '');
|
|
3062
|
+
const groupId = scope === 'group' ? String(eventData.group_id) : '';
|
|
3063
|
+
const lastRead = String(eventData.last_read_message_id);
|
|
3064
|
+
if (scope && topicKey && lastRead && (scope !== 'group' || groupId)) {
|
|
3065
|
+
const key = `${scope}\u0000${groupId}\u0000${topicKey}`;
|
|
3066
|
+
const query = { topic_key: topicKey };
|
|
3067
|
+
if (scope === 'group')
|
|
3068
|
+
query.group_id = groupId;
|
|
3069
|
+
else
|
|
3070
|
+
query.scope = scope;
|
|
3071
|
+
const method = `${scope === 'group' ? 'group' : 'message'}.read_state`;
|
|
3072
|
+
this._registerReadStateQuery(method, query);
|
|
3073
|
+
const previous = this._readStateCache.get(key)?.state;
|
|
3074
|
+
if (previous && (!this._readStateIsNewer(previous, eventData)
|
|
3075
|
+
|| (previous.last_read_message_id === eventData.last_read_message_id
|
|
3076
|
+
&& BigInt(String(previous.last_read_order ?? 0)) === BigInt(String(eventData.last_read_order ?? 0)))))
|
|
3077
|
+
return;
|
|
3078
|
+
this._readStateCache.set(key, { method, params: query, state: { ...eventData } });
|
|
3079
|
+
}
|
|
3080
|
+
this._dispatcher.enqueue(scope === 'group' ? 'group.read_changed' : 'message.read_changed', eventData);
|
|
3081
|
+
}
|
|
3082
|
+
async _reconcileReadStates() {
|
|
3083
|
+
const identity = this._ensureReadStateIdentity();
|
|
3084
|
+
const keys = new Set();
|
|
3085
|
+
const errors = [];
|
|
3086
|
+
for (const entry of [...this._readStateQueries.values()]) {
|
|
3087
|
+
try {
|
|
3088
|
+
const result = await this.call(entry.method, { ...entry.params });
|
|
3089
|
+
if (identity !== this._ensureReadStateIdentity())
|
|
3090
|
+
return;
|
|
3091
|
+
this._observeReadStateResult(entry.method, result, entry.params);
|
|
3092
|
+
for (const state of this._readStateItems(result)) {
|
|
3093
|
+
const scope = String(state.scope ?? (entry.method.startsWith('group.') ? 'group' : 'p2p'));
|
|
3094
|
+
const groupId = scope === 'group' ? String(state.group_id ?? entry.params.group_id ?? '') : '';
|
|
3095
|
+
const topicKey = String(state.topic_key ?? entry.params.topic_key ?? '');
|
|
3096
|
+
const key = `${scope}\u0000${groupId}\u0000${topicKey}`;
|
|
3097
|
+
if (this._readStateCache.has(key))
|
|
3098
|
+
keys.add(key);
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
catch (error) {
|
|
3102
|
+
errors.push({ method: entry.method, params: { ...entry.params }, error: error instanceof Error ? error.message : String(error) });
|
|
3103
|
+
this._clientLog.debug(`read state reconnect reconciliation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3104
|
+
}
|
|
3105
|
+
if (identity !== this._ensureReadStateIdentity())
|
|
3106
|
+
return;
|
|
3107
|
+
}
|
|
3108
|
+
this._dispatcher.enqueue('read_state_sync', {
|
|
3109
|
+
owner_aid: this._aid ?? '',
|
|
3110
|
+
states: [...keys].map((key) => ({ ...this._readStateCache.get(key).state })),
|
|
3111
|
+
errors,
|
|
3112
|
+
});
|
|
3113
|
+
}
|
|
2002
3114
|
/** 取消订阅事件 */
|
|
2003
3115
|
off(event, handler) {
|
|
2004
3116
|
this._dispatcher.unsubscribe(event, handler);
|
|
@@ -2042,6 +3154,8 @@ export class AUNClient {
|
|
|
2042
3154
|
this._delivery.markPublishedSeq(ns, seq);
|
|
2043
3155
|
}
|
|
2044
3156
|
async _publishAppEvent(event, payload, source = 'direct', ns = '', batch) {
|
|
3157
|
+
if (event === 'message.received' && await this._handleOwnershipMessage(payload))
|
|
3158
|
+
return;
|
|
2045
3159
|
await this._delivery.publishAppEvent(event, payload, source, ns, batch);
|
|
2046
3160
|
}
|
|
2047
3161
|
_echoTimestamp() {
|
|
@@ -2083,8 +3197,8 @@ export class AUNClient {
|
|
|
2083
3197
|
const trace = `${this._echoTimestamp()} [AUN-SDK.receive] aid=${this._aid ?? '-'} conn_uptime=${uptime}s`;
|
|
2084
3198
|
msg.payload = { ...payload, text: payload.text + '\n' + trace };
|
|
2085
3199
|
}
|
|
2086
|
-
async _drainOrderedMessages(ns, beforeSeq, pullResponse = false, persist = true) {
|
|
2087
|
-
await this._delivery.drainOrderedMessages(ns, beforeSeq, pullResponse, persist);
|
|
3200
|
+
async _drainOrderedMessages(ns, beforeSeq, pullResponse = false, persist = true, throughSeq) {
|
|
3201
|
+
await this._delivery.drainOrderedMessages(ns, beforeSeq, pullResponse, persist, undefined, 'pull', throughSeq);
|
|
2088
3202
|
}
|
|
2089
3203
|
async _publishOrderedMessage(event, ns, seq, payload, source = 'push', batch) {
|
|
2090
3204
|
return this._delivery.publishOrderedMessage(event, ns, seq, payload, source, batch);
|
|
@@ -2486,6 +3600,11 @@ export class AUNClient {
|
|
|
2486
3600
|
if (!paramsHash || !timestamp || !sigB64) {
|
|
2487
3601
|
return false;
|
|
2488
3602
|
}
|
|
3603
|
+
const signedAt = Number(timestamp) * 1000;
|
|
3604
|
+
const certMeta = parseCertMetadata(cached.certPem);
|
|
3605
|
+
if (!/^\d+$/.test(timestamp) || !Number.isSafeInteger(Number(timestamp)) || signedAt < certMeta.notBefore.getTime() || signedAt > certMeta.notAfter.getTime()) {
|
|
3606
|
+
return false;
|
|
3607
|
+
}
|
|
2489
3608
|
const pubKey = await importCertPublicKeyEcdsa(cached.certPem);
|
|
2490
3609
|
const signData = new TextEncoder().encode(`${method}|${sigAid}|${timestamp}|${paramsHash}`);
|
|
2491
3610
|
const sigBytes = base64ToUint8(sigB64);
|
|
@@ -2538,11 +3657,26 @@ export class AUNClient {
|
|
|
2538
3657
|
* 获取对方证书(带缓存 + 完整 PKI 验证:链 + CRL + OCSP + AID 绑定)。
|
|
2539
3658
|
* 跨域时自动将请求路由到 peer 所在域的 Gateway。
|
|
2540
3659
|
*/
|
|
2541
|
-
async
|
|
3660
|
+
async _resolveServiceEventCertificate(aid, fingerprint) {
|
|
3661
|
+
const certPem = await this._fetchPeerCert(aid, fingerprint);
|
|
3662
|
+
if (!this._gatewayUrl)
|
|
3663
|
+
throw new ValidationError('gateway url unavailable for service event certificate verification');
|
|
3664
|
+
await this._auth.verifyPeerCertificate(resolvePeerGatewayUrl(this._gatewayUrl, aid), certPem, aid, undefined, true);
|
|
3665
|
+
return certPem;
|
|
3666
|
+
}
|
|
3667
|
+
async _verifyIncomingServiceEvent(event, payload) {
|
|
3668
|
+
const targetAid = this._aid || this._currentAid?.aid || '';
|
|
3669
|
+
const version = this._serviceEventIdentityVersion;
|
|
3670
|
+
const verified = await verifyServiceEvent(event, payload, { targetAid, resolveEventCertificate: (service, fingerprint) => this._resolveServiceEventCertificate(service, fingerprint), resolveCredentialService: (cid) => this.resolveCredentialService?.(cid) });
|
|
3671
|
+
if (version !== this._serviceEventIdentityVersion || targetAid !== (this._aid || this._currentAid?.aid || ''))
|
|
3672
|
+
throw new ValidationError('service event receiver identity changed');
|
|
3673
|
+
return verified;
|
|
3674
|
+
}
|
|
3675
|
+
async _fetchPeerCert(aid, certFingerprint, timeoutMs = 5000, signedAt) {
|
|
2542
3676
|
const tStart = Date.now();
|
|
2543
3677
|
this._clientLog.debug(`_fetchPeerCert enter: aid=${aid} fingerprint=${certFingerprint ?? '<none>'}`);
|
|
2544
3678
|
try {
|
|
2545
|
-
const cacheKey = certCacheKey(aid, certFingerprint);
|
|
3679
|
+
const cacheKey = certCacheKey(aid, certFingerprint) + (signedAt === undefined ? '' : `:signed:${signedAt}`);
|
|
2546
3680
|
const cached = this._certCache.get(cacheKey);
|
|
2547
3681
|
const now = Date.now() / 1000;
|
|
2548
3682
|
if (cached && now < cached.refreshAfter) {
|
|
@@ -2581,7 +3715,10 @@ export class AUNClient {
|
|
|
2581
3715
|
}
|
|
2582
3716
|
// 完整 PKI 验证:链 + CRL + OCSP + AID 绑定
|
|
2583
3717
|
try {
|
|
2584
|
-
|
|
3718
|
+
if (signedAt === undefined)
|
|
3719
|
+
await this._auth.verifyPeerCertificate(peerGatewayUrl, certPem, aid);
|
|
3720
|
+
else
|
|
3721
|
+
await this._auth.verifyPeerCertificate(peerGatewayUrl, certPem, aid, signedAt);
|
|
2585
3722
|
}
|
|
2586
3723
|
catch (exc) {
|
|
2587
3724
|
throw new ValidationError(`peer cert verification failed for ${aid}: ${exc}`);
|
|
@@ -2656,6 +3793,8 @@ export class AUNClient {
|
|
|
2656
3793
|
if (identity && isJsonObject(identity)) {
|
|
2657
3794
|
this._identity = identity;
|
|
2658
3795
|
this._aid = String(identity.aid ?? this._aid ?? '');
|
|
3796
|
+
this._ownershipRecordsLoad = this._restoreOwnershipRecords();
|
|
3797
|
+
await this._ownershipRecordsLoad;
|
|
2659
3798
|
this._logger.bindAid(this._aid);
|
|
2660
3799
|
if (this._sessionParams) {
|
|
2661
3800
|
this._sessionParams.access_token = String(auth.token ?? params.access_token ?? '');
|
|
@@ -2738,6 +3877,7 @@ export class AUNClient {
|
|
|
2738
3877
|
}
|
|
2739
3878
|
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
2740
3879
|
this._startBackgroundTasks();
|
|
3880
|
+
this._safeAsync(this._reconcileReadStates());
|
|
2741
3881
|
this._safeAsync(this.restoreGroupInvitesAfterConnect());
|
|
2742
3882
|
const connectionKind = String(params.connection_kind ?? 'long');
|
|
2743
3883
|
const isShortConnection = connectionKind === 'short';
|
|
@@ -2756,7 +3896,7 @@ export class AUNClient {
|
|
|
2756
3896
|
if (backgroundSyncEnabled) {
|
|
2757
3897
|
this._safeAsync(this._fillP2pGap());
|
|
2758
3898
|
}
|
|
2759
|
-
this._safeAsync(this.
|
|
3899
|
+
this._safeAsync(this._resumeOwnershipAfterConnect());
|
|
2760
3900
|
this._clientLog.debug(`_connectOnce exit: elapsed=${Date.now() - tStart}ms aid=${this._aid ?? '-'}`);
|
|
2761
3901
|
}
|
|
2762
3902
|
catch (err) {
|
|
@@ -2775,6 +3915,23 @@ export class AUNClient {
|
|
|
2775
3915
|
const gateways = await this._resolveGatewayCandidatesForAid(aid);
|
|
2776
3916
|
return gateways[0];
|
|
2777
3917
|
}
|
|
3918
|
+
async _resolveOwnershipCertificateGateway(aid) {
|
|
3919
|
+
const issuer = aid.slice(aid.indexOf('.') + 1);
|
|
3920
|
+
if (issuer === this._aid?.slice(this._aid.indexOf('.') + 1))
|
|
3921
|
+
return this._resolveGatewayForAid(aid);
|
|
3922
|
+
let lastError;
|
|
3923
|
+
for (const host of [aid, `gateway.${issuer}`]) {
|
|
3924
|
+
try {
|
|
3925
|
+
const gateways = await this._discovery.discoverAll(`https://${host}/.well-known/aun-gateway`);
|
|
3926
|
+
if (gateways.length)
|
|
3927
|
+
return gateways[0];
|
|
3928
|
+
}
|
|
3929
|
+
catch (error) {
|
|
3930
|
+
lastError = error;
|
|
3931
|
+
}
|
|
3932
|
+
}
|
|
3933
|
+
throw new StateError(lastError instanceof Error ? lastError.message : 'signer Gateway discovery failed', { stringCode: 'unavailable', retryable: true });
|
|
3934
|
+
}
|
|
2778
3935
|
async _resolveGatewayCandidatesForAid(aid) {
|
|
2779
3936
|
const target = String(aid ?? this._aid ?? '').trim();
|
|
2780
3937
|
if (!target)
|
|
@@ -2998,6 +4155,7 @@ export class AUNClient {
|
|
|
2998
4155
|
}
|
|
2999
4156
|
// ── 内部:后台任务 ────────────────────────────────
|
|
3000
4157
|
_startBackgroundTasks() {
|
|
4158
|
+
this._startOwnershipRecovery();
|
|
3001
4159
|
// WebSocket Ping/Pong 由 Gateway 与传输层处理;SDK 不发送 RPC 心跳。
|
|
3002
4160
|
// 短连接生命周期短,不需要 token 刷新。
|
|
3003
4161
|
if (this._sessionOptions?.connection_kind !== 'short') {
|
|
@@ -3026,6 +4184,11 @@ export class AUNClient {
|
|
|
3026
4184
|
}
|
|
3027
4185
|
}
|
|
3028
4186
|
_stopBackgroundTasks() {
|
|
4187
|
+
this._ownershipRecoveryGeneration += 1;
|
|
4188
|
+
if (this._ownershipRecoveryTimer !== null) {
|
|
4189
|
+
clearTimeout(this._ownershipRecoveryTimer);
|
|
4190
|
+
this._ownershipRecoveryTimer = null;
|
|
4191
|
+
}
|
|
3029
4192
|
if (this._heartbeatTimer !== null) {
|
|
3030
4193
|
clearInterval(this._heartbeatTimer);
|
|
3031
4194
|
this._heartbeatTimer = null;
|
|
@@ -3039,6 +4202,32 @@ export class AUNClient {
|
|
|
3039
4202
|
this._cacheCleanupTimer = null;
|
|
3040
4203
|
}
|
|
3041
4204
|
}
|
|
4205
|
+
_startOwnershipRecovery() {
|
|
4206
|
+
if (this._ownershipRecoveryTimer !== null || !this._aid || this._state !== 'connected' || this._closing || this._disconnecting)
|
|
4207
|
+
return;
|
|
4208
|
+
const aid = this._aid, generation = ++this._ownershipRecoveryGeneration;
|
|
4209
|
+
const active = () => generation === this._ownershipRecoveryGeneration && aid === this._aid && this._state === 'connected' && !this._closing && !this._disconnecting;
|
|
4210
|
+
const guard = () => { if (!active())
|
|
4211
|
+
throw new StateError('ownership recovery was superseded', { stringCode: 'forbidden' }); };
|
|
4212
|
+
const recover = async () => {
|
|
4213
|
+
try {
|
|
4214
|
+
if (!active())
|
|
4215
|
+
return;
|
|
4216
|
+
await this._withBackgroundRpc(async () => {
|
|
4217
|
+
await this._resumeRegistrationIntent(guard);
|
|
4218
|
+
guard();
|
|
4219
|
+
await this._ownershipV21().resume(guard);
|
|
4220
|
+
});
|
|
4221
|
+
}
|
|
4222
|
+
finally {
|
|
4223
|
+
if (active())
|
|
4224
|
+
this._ownershipRecoveryTimer = setTimeout(() => this._safeAsync(recover()), 5000);
|
|
4225
|
+
else if (generation === this._ownershipRecoveryGeneration)
|
|
4226
|
+
this._ownershipRecoveryTimer = null;
|
|
4227
|
+
}
|
|
4228
|
+
};
|
|
4229
|
+
this._ownershipRecoveryTimer = setTimeout(() => this._safeAsync(recover()), 5000);
|
|
4230
|
+
}
|
|
3042
4231
|
/** 心跳定时器 */
|
|
3043
4232
|
_startHeartbeat() {
|
|
3044
4233
|
if (this._heartbeatTimer !== null) {
|
|
@@ -3948,7 +5137,10 @@ export class AUNClient {
|
|
|
3948
5137
|
if (senderDeviceId)
|
|
3949
5138
|
result.sender_device_id = senderDeviceId;
|
|
3950
5139
|
const explicitDirection = String(msg.direction ?? '').trim();
|
|
3951
|
-
|
|
5140
|
+
if (explicitDirection)
|
|
5141
|
+
result.direction = explicitDirection;
|
|
5142
|
+
else if (fromAid && fromAid !== this._aid)
|
|
5143
|
+
result.direction = 'inbound';
|
|
3952
5144
|
if (msg.device_id !== undefined)
|
|
3953
5145
|
result.device_id = msg.device_id;
|
|
3954
5146
|
attachGatewayProximity(result, msg);
|