@agentunion/fastaun 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 +2 -0
- package/dist/agent-md-schema.js +46 -9
- package/dist/agent-md-schema.js.map +1 -1
- package/dist/agent-md.d.ts +46 -1
- package/dist/agent-md.js +492 -30
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +9 -1
- package/dist/aid-store.js +139 -18
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.d.ts +17 -1
- package/dist/aid.js +67 -7
- package/dist/aid.js.map +1 -1
- package/dist/auth.d.ts +6 -1
- package/dist/auth.js +428 -15
- package/dist/auth.js.map +1 -1
- package/dist/cert-utils.d.ts +1 -1
- package/dist/cert-utils.js +19 -7
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +4 -1
- package/dist/client/delivery.js +264 -68
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/rpc-pipeline.js +29 -14
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +8 -1
- package/dist/client/v2-e2ee.js +893 -529
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +87 -1
- package/dist/client.js +816 -35
- package/dist/client.js.map +1 -1
- package/dist/errors.js +4 -3
- package/dist/errors.js.map +1 -1
- package/dist/events.d.ts +5 -1
- package/dist/events.js +47 -6
- package/dist/events.js.map +1 -1
- package/dist/facades.d.ts +85 -3
- package/dist/facades.js +104 -4
- package/dist/facades.js.map +1 -1
- package/dist/index.d.ts +9 -4
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/keystore/aid-db.d.ts +5 -0
- package/dist/keystore/aid-db.js +48 -0
- package/dist/keystore/aid-db.js.map +1 -1
- package/dist/keystore/index.d.ts +8 -0
- package/dist/keystore/local-identity-store.js +10 -3
- package/dist/keystore/local-identity-store.js.map +1 -1
- package/dist/keystore/local-token-store.d.ts +6 -1
- package/dist/keystore/local-token-store.js +24 -1
- package/dist/keystore/local-token-store.js.map +1 -1
- package/dist/org-protocol.d.ts +98 -0
- package/dist/org-protocol.js +1689 -0
- package/dist/org-protocol.js.map +1 -0
- package/dist/ownership-proof.d.ts +117 -0
- package/dist/ownership-proof.js +436 -0
- package/dist/ownership-proof.js.map +1 -0
- package/dist/ownership-v21.d.ts +178 -0
- package/dist/ownership-v21.js +635 -0
- package/dist/ownership-v21.js.map +1 -0
- package/dist/ownership-workflow.d.ts +82 -0
- package/dist/ownership-workflow.js +872 -0
- package/dist/ownership-workflow.js.map +1 -0
- package/dist/seq-tracker.d.ts +1 -0
- package/dist/seq-tracker.js +10 -1
- package/dist/seq-tracker.js.map +1 -1
- package/dist/tools/cross-sdk-agent.d.ts +89 -1
- package/dist/tools/cross-sdk-agent.js +338 -7
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +7 -0
- package/dist/transport.js +108 -4
- package/dist/transport.js.map +1 -1
- package/dist/v2/e2ee/decrypt.d.ts +1 -0
- 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 +8 -8
package/dist/client.js
CHANGED
|
@@ -26,7 +26,7 @@ import { normalizeGroupId } from './group-id.js';
|
|
|
26
26
|
import { validateAIDFormat, validateGroupIDFormat } from './validators.js';
|
|
27
27
|
import { RPCTransport } from './transport.js';
|
|
28
28
|
import { AuthFlow } from './auth.js';
|
|
29
|
-
import { AgentMdManager, buildDefaultAgentMd } from './agent-md.js';
|
|
29
|
+
import { AgentMdManager, buildDefaultAgentMd, updateOwnershipAgentMd } from './agent-md.js';
|
|
30
30
|
import { GroupIndexMetaCache } from './group-index.js';
|
|
31
31
|
import { SeqTracker } from './seq-tracker.js';
|
|
32
32
|
import { ClientRuntime } from './client/runtime.js';
|
|
@@ -37,15 +37,20 @@ import { LifecycleController } from './client/lifecycle.js';
|
|
|
37
37
|
import { PeerDirectory } from './client/peers.js';
|
|
38
38
|
import { RpcPipeline } from './client/rpc-pipeline.js';
|
|
39
39
|
import { CollabClient } from './collab/client.js';
|
|
40
|
-
import { GroupFacade, MailFacade, MessageFacade, StreamFacade } from './facades.js';
|
|
40
|
+
import { CredentialFacade, OrgFacade, GroupFacade, MailFacade, MessageFacade, StreamFacade } from './facades.js';
|
|
41
41
|
import { StorageVFS } from './storage/vfs.js';
|
|
42
42
|
import { V2E2EECoordinator } from './client/v2-e2ee.js';
|
|
43
43
|
import { GroupStateCoordinator } from './client/group-state.js';
|
|
44
44
|
import { decryptMessage, } from './v2/e2ee/index.js';
|
|
45
45
|
import { ecdsaVerifyRaw } from './v2/crypto/ecdsa.js';
|
|
46
|
+
import { decryptedOwnershipJsonText } from './v2/e2ee/decrypt.js';
|
|
46
47
|
import { isJsonObject, ConnectionState, STATE_TO_PUBLIC, } from './types.js';
|
|
47
48
|
import { AID } from './aid.js';
|
|
48
|
-
import { certMatchesFingerprint, normalizeFingerprintHex, publicKeyMatchesFingerprint } from './cert-utils.js';
|
|
49
|
+
import { certFingerprint, certMatchesFingerprint, normalizeFingerprintHex, publicKeyMatchesFingerprint } from './cert-utils.js';
|
|
50
|
+
import { validateAgentMdDocument } from './agent-md-schema.js';
|
|
51
|
+
import { OwnershipFiles, OwnershipStorageAdapter, ownershipFailure, signOwnershipFile } from './ownership-v21.js';
|
|
52
|
+
import { OwnershipWorkflow } from './ownership-workflow.js';
|
|
53
|
+
import { canonicalJson, createCredential, createMemberCredential, signMembershipBinding, verifyServiceEvent } from './org-protocol.js';
|
|
49
54
|
function isPromiseLike(value) {
|
|
50
55
|
return Boolean(value && typeof value.then === 'function');
|
|
51
56
|
}
|
|
@@ -120,6 +125,8 @@ const DEFAULT_SESSION_OPTIONS = {
|
|
|
120
125
|
const RECONNECT_MIN_BASE_DELAY_MS = 1_000;
|
|
121
126
|
const RECONNECT_MAX_BASE_DELAY_MS = 64_000;
|
|
122
127
|
const TOKEN_REFRESH_CHECK_INTERVAL_MS = 30_000;
|
|
128
|
+
const PENDING_P2P_PULL_DELAY_MS = 100;
|
|
129
|
+
const PENDING_P2P_PULL_MAX_RETRIES = 20;
|
|
123
130
|
const MAX_NOTIFY_PAYLOAD_SIZE = 64 * 1024;
|
|
124
131
|
// P1-23: 非幂等方法使用更长超时(35s),避免 SDK 10s 超时 < gateway 30s 处理时间
|
|
125
132
|
const NON_IDEMPOTENT_TIMEOUT_MS = 35_000;
|
|
@@ -340,6 +347,9 @@ function createAgentMdManagerForRuntime(opts) {
|
|
|
340
347
|
logger: opts.logger(),
|
|
341
348
|
ownerAidGetter: opts.ownerAid,
|
|
342
349
|
currentAidGetter: opts.currentAid,
|
|
350
|
+
entityOwnerResolver: opts.resolveEntityOwner,
|
|
351
|
+
entityIdentityGetter: opts.fetchEntityIdentity,
|
|
352
|
+
entitySignerResolver: opts.fetchEntitySigner,
|
|
343
353
|
gatewayResolver: async (aid) => {
|
|
344
354
|
const gatewayUrl = await opts.gateway.resolve(aid);
|
|
345
355
|
opts.gateway.set(gatewayUrl);
|
|
@@ -386,7 +396,47 @@ function createAgentMdManagerForRuntime(opts) {
|
|
|
386
396
|
opts.identity.set(identity);
|
|
387
397
|
return token;
|
|
388
398
|
},
|
|
389
|
-
|
|
399
|
+
uploadAccessTokenResolver: async (_aid, gatewayUrl) => {
|
|
400
|
+
const localAid = String(opts.ownerAid() ?? '').trim();
|
|
401
|
+
let identity = opts.identity.get();
|
|
402
|
+
if (!identity || String(identity.aid ?? '') !== localAid) {
|
|
403
|
+
throw new StateError('no local identity found, register or load an AID first');
|
|
404
|
+
}
|
|
405
|
+
const auth = opts.auth();
|
|
406
|
+
const cachedToken = String(identity.access_token ?? '');
|
|
407
|
+
const expiresAt = auth.getAccessTokenExpiry(identity);
|
|
408
|
+
if (cachedToken && (expiresAt === null || expiresAt > Date.now() / 1000 + 30))
|
|
409
|
+
return cachedToken;
|
|
410
|
+
if (identity.refresh_token) {
|
|
411
|
+
try {
|
|
412
|
+
const refreshed = await auth.refreshCachedTokens(gatewayUrl, identity);
|
|
413
|
+
const refreshedToken = String(refreshed.access_token ?? '');
|
|
414
|
+
const refreshedExpiry = auth.getAccessTokenExpiry(refreshed);
|
|
415
|
+
if (refreshedToken && (refreshedExpiry === null || refreshedExpiry > Date.now() / 1000 + 30)) {
|
|
416
|
+
opts.identity.set(refreshed);
|
|
417
|
+
return refreshedToken;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
// refresh 失败时回退到完整 authenticate。
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const result = await auth.authenticate(gatewayUrl, { aid: localAid });
|
|
425
|
+
const token = String(result.access_token ?? '');
|
|
426
|
+
if (!token)
|
|
427
|
+
throw new StateError('authenticate did not return access_token');
|
|
428
|
+
identity = auth.loadIdentityOrNone(localAid) ?? {
|
|
429
|
+
...identity,
|
|
430
|
+
access_token: token,
|
|
431
|
+
refresh_token: String(result.refresh_token ?? identity.refresh_token ?? ''),
|
|
432
|
+
access_token_expires_at: typeof result.expires_at === 'number' ? result.expires_at : identity.access_token_expires_at,
|
|
433
|
+
token_exp: typeof result.expires_at === 'number' ? result.expires_at : identity.token_exp,
|
|
434
|
+
expires_at: typeof result.expires_at === 'number' ? result.expires_at : identity.expires_at,
|
|
435
|
+
};
|
|
436
|
+
opts.identity.set(identity);
|
|
437
|
+
return token;
|
|
438
|
+
},
|
|
439
|
+
peerResolver: async (aid, certFingerprint, signedAt) => {
|
|
390
440
|
const target = String(aid ?? '').trim();
|
|
391
441
|
const expectedFp = String(certFingerprint ?? '').trim().toLowerCase();
|
|
392
442
|
const current = opts.currentAid();
|
|
@@ -400,7 +450,7 @@ function createAgentMdManagerForRuntime(opts) {
|
|
|
400
450
|
}
|
|
401
451
|
catch { /* best effort before cert fetch */ }
|
|
402
452
|
}
|
|
403
|
-
const certPem = String(await opts.fetchPeerCert(target, expectedFp || undefined) ?? '').trim();
|
|
453
|
+
const certPem = String(await opts.fetchPeerCert(target, expectedFp || undefined, signedAt) ?? '').trim();
|
|
404
454
|
if (!certPem)
|
|
405
455
|
throw new NotFoundError(`certificate not found for aid: ${target}`);
|
|
406
456
|
return AID._create({
|
|
@@ -427,6 +477,90 @@ export class AUNClient {
|
|
|
427
477
|
_state = 'no_identity';
|
|
428
478
|
/** 当前 AID 值对象(新 API) */
|
|
429
479
|
_currentAid = null;
|
|
480
|
+
_serviceEventIdentityVersion = 0;
|
|
481
|
+
/** ownership 关系只在 SDK 内存中编排;服务端不保存状态机。 */
|
|
482
|
+
_ownershipRecords = new Map();
|
|
483
|
+
_ownershipType = 'agent';
|
|
484
|
+
_ownershipStorage = null;
|
|
485
|
+
_ownershipFiles = null;
|
|
486
|
+
_ownershipWorkflow = null;
|
|
487
|
+
_getOwnershipStorage() {
|
|
488
|
+
return this._ownershipStorage ??= new OwnershipStorageAdapter({ currentAid: () => this._aid, call: (method, params) => this.call(method, params) });
|
|
489
|
+
}
|
|
490
|
+
_getOwnershipFiles() {
|
|
491
|
+
return this._ownershipFiles ??= new OwnershipFiles({
|
|
492
|
+
currentAid: () => { if (!this._currentAid)
|
|
493
|
+
throw ownershipFailure('forbidden', 'ownership requires a loaded identity'); return this._currentAid; },
|
|
494
|
+
currentType: () => this.getType(),
|
|
495
|
+
now: () => Math.trunc(Date.now() / 1000),
|
|
496
|
+
readAgentMdFresh: (subAid) => this._agentMdManager.readOwnershipFresh(subAid),
|
|
497
|
+
submitAgentMdRaw: (subAid, raw, condition) => this._agentMdManager.submitOwnershipRaw(subAid, raw, condition),
|
|
498
|
+
readOwnerMd: (ownerAid, subAid) => this._getOwnershipStorage().readOwnerMd(ownerAid, subAid),
|
|
499
|
+
writeOwnerMd: (ownerAid, subAid, raw, revision) => this._getOwnershipStorage().writeOwnerMd(ownerAid, subAid, raw, revision),
|
|
500
|
+
deleteOwnerMd: (ownerAid, subAid, revision) => this._getOwnershipStorage().deleteOwnerMd(ownerAid, subAid, revision),
|
|
501
|
+
resolveCertificate: async (aid, fingerprint, purpose) => this._auth.resolveOwnershipCertificate(await this._resolveOwnershipCertificateGateway(aid), aid, fingerprint, purpose),
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
_getOwnershipWorkflow() {
|
|
505
|
+
return this._ownershipWorkflow ??= new OwnershipWorkflow({
|
|
506
|
+
currentAid: () => this._aid, store: this._tokenStore, files: this._getOwnershipFiles(),
|
|
507
|
+
currentType: () => this.getType(),
|
|
508
|
+
send: (to, payload) => this.call('message.send', { to, encrypt: true, payload: payload }),
|
|
509
|
+
emit: (event, payload) => this._dispatcher.enqueue(event, payload),
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
_persistOwnershipRecords() {
|
|
513
|
+
const aid = String(this._aid ?? '').trim();
|
|
514
|
+
if (!aid)
|
|
515
|
+
return;
|
|
516
|
+
const metadata = this._tokenStore.loadMetadata?.(aid) ?? {};
|
|
517
|
+
const existing = metadata.ownership_records;
|
|
518
|
+
const records = existing && typeof existing === 'object' && !Array.isArray(existing)
|
|
519
|
+
? { ...existing } : {};
|
|
520
|
+
for (const [key, record] of this._ownershipRecords) {
|
|
521
|
+
if (![record.subject_aid, record.owner_aid].some((value) => String(value ?? '').trim().toLowerCase() === aid.toLowerCase()))
|
|
522
|
+
continue;
|
|
523
|
+
records[key] = Object.fromEntries(Object.entries(record).filter(([name]) => !['subject_identity', 'escrow_password', 'escrow_download_url'].includes(name)));
|
|
524
|
+
}
|
|
525
|
+
this._tokenStore.saveMetadata?.(aid, { ownership_records: records });
|
|
526
|
+
}
|
|
527
|
+
_restoreOwnershipRecords() {
|
|
528
|
+
const aid = String(this._aid ?? '').trim();
|
|
529
|
+
if (!aid)
|
|
530
|
+
return;
|
|
531
|
+
const metadata = this._tokenStore.loadMetadata?.(aid);
|
|
532
|
+
const intent = metadata?.registration_intent;
|
|
533
|
+
if (intent?.type)
|
|
534
|
+
this._ownershipType = String(intent.type);
|
|
535
|
+
const records = metadata?.ownership_records;
|
|
536
|
+
if (!records || typeof records !== 'object' || Array.isArray(records))
|
|
537
|
+
return;
|
|
538
|
+
for (const [key, value] of Object.entries(records)) {
|
|
539
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
540
|
+
continue;
|
|
541
|
+
const record = value;
|
|
542
|
+
this._ownershipRecords.set(key, record);
|
|
543
|
+
if (String(record.subject_aid ?? '').toLowerCase() === aid.toLowerCase())
|
|
544
|
+
this._ownershipType = String(record.type ?? this._ownershipType);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
async _resumeOwnershipAfterConnect() {
|
|
548
|
+
await this._checkIdentityAdmissionAfterConnect();
|
|
549
|
+
await this._resumeRegistrationIntent();
|
|
550
|
+
await this._resumeOwnership();
|
|
551
|
+
}
|
|
552
|
+
async _resumeRegistrationIntent() {
|
|
553
|
+
const intent = this._tokenStore.loadMetadata?.(this._aid)?.registration_intent;
|
|
554
|
+
if (['group', 'org'].includes(String(intent?.type ?? this.getType())))
|
|
555
|
+
return;
|
|
556
|
+
if (intent?.protocol !== '2.1' || intent.pending !== true)
|
|
557
|
+
return;
|
|
558
|
+
await this._getOwnershipWorkflow().resumeRegistration(String(intent.owner), String(intent.operation_id));
|
|
559
|
+
this._tokenStore.saveMetadata?.(this._aid, { registration_intent: { ...intent, pending: false } });
|
|
560
|
+
}
|
|
561
|
+
async _resumeOwnership() {
|
|
562
|
+
await this._getOwnershipWorkflow().resume();
|
|
563
|
+
}
|
|
430
564
|
/** 实例级 protected_headers */
|
|
431
565
|
_instanceProtectedHeaders = null;
|
|
432
566
|
/** 重连退避时间戳(ms) */
|
|
@@ -449,6 +583,7 @@ export class AUNClient {
|
|
|
449
583
|
_transport;
|
|
450
584
|
/** 认证流程 */
|
|
451
585
|
_auth;
|
|
586
|
+
resolveCredentialService;
|
|
452
587
|
/** 密钥存储 */
|
|
453
588
|
_tokenStore;
|
|
454
589
|
/** 会话参数(重连用) */
|
|
@@ -494,10 +629,17 @@ export class AUNClient {
|
|
|
494
629
|
_pushProcessQueues = new Map();
|
|
495
630
|
/** P2P pull 进行中到达的纯通知 push 上界;pull gate 释放后需要补拉一次。 */
|
|
496
631
|
_pendingP2pPullUpper = new Map();
|
|
632
|
+
/** P2P pending pull 的延迟重试定时器及次数,覆盖服务端异步落库窗口。 */
|
|
633
|
+
_pendingP2pPullTimers = new Map();
|
|
634
|
+
_pendingP2pPullRetries = new Map();
|
|
497
635
|
/** 缺 sender IK 时暂存原始 V2 消息,后台补齐 IK 后重试解密。 */
|
|
498
636
|
_v2SenderIKPending = new Map();
|
|
499
637
|
/** sender IK 后台补齐任务去重。 */
|
|
500
638
|
_v2SenderIKFetching = new Set();
|
|
639
|
+
_readStateCache = new Map();
|
|
640
|
+
_readStateQueries = new Map();
|
|
641
|
+
_readStateIdentity = null;
|
|
642
|
+
_readStateGeneration = 0;
|
|
501
643
|
// ── 后台任务定时器 ──────────────────────────────────────────
|
|
502
644
|
_heartbeatTimer = null;
|
|
503
645
|
_tokenRefreshTimer = null;
|
|
@@ -546,6 +688,8 @@ export class AUNClient {
|
|
|
546
688
|
_mail;
|
|
547
689
|
_group;
|
|
548
690
|
_stream;
|
|
691
|
+
_credential;
|
|
692
|
+
_org;
|
|
549
693
|
constructor(aid) {
|
|
550
694
|
if (aid !== null && aid !== undefined && !isAIDObject(aid)) {
|
|
551
695
|
throw new ValidationError('AUNClient only accepts an AID object or no argument');
|
|
@@ -581,7 +725,7 @@ export class AUNClient {
|
|
|
581
725
|
if (debugFlag) {
|
|
582
726
|
this._clientLog.info(`AUNClient initialized (debug=true, aunPath=${this._configModel.aunPath})`);
|
|
583
727
|
}
|
|
584
|
-
this._dispatcher = new EventDispatcher(this._logger.for('aun_core.events'));
|
|
728
|
+
this._dispatcher = new EventDispatcher(this._logger.for('aun_core.events'), () => this._identity?.aid ?? '');
|
|
585
729
|
const dnsNet = new DnsResilientNet({
|
|
586
730
|
verifySsl: this._configModel.verifySsl,
|
|
587
731
|
logger: this._clientLog,
|
|
@@ -591,6 +735,9 @@ export class AUNClient {
|
|
|
591
735
|
logger: this._logger.for('aun_core.keystore'),
|
|
592
736
|
});
|
|
593
737
|
this._tokenStore = tokenStore;
|
|
738
|
+
this._ownershipStorage = null;
|
|
739
|
+
this._ownershipFiles = null;
|
|
740
|
+
this._ownershipWorkflow = null;
|
|
594
741
|
this._slotId = inputAid?.slotId || 'default';
|
|
595
742
|
this._connectDeliveryMode = normalizeDeliveryModeConfig({ mode: 'fanout' });
|
|
596
743
|
this._defaultConnectDeliveryMode = { ...this._connectDeliveryMode };
|
|
@@ -606,6 +753,7 @@ export class AUNClient {
|
|
|
606
753
|
net: dnsNet,
|
|
607
754
|
});
|
|
608
755
|
this._aid = initAid;
|
|
756
|
+
this._restoreOwnershipRecords();
|
|
609
757
|
this._agentMdManager = createAgentMdManagerForRuntime({
|
|
610
758
|
config: () => this._configModel,
|
|
611
759
|
logger: () => this._logger.for('aun_core.agent_md'),
|
|
@@ -623,7 +771,10 @@ export class AUNClient {
|
|
|
623
771
|
},
|
|
624
772
|
auth: () => this._auth,
|
|
625
773
|
tokenStore: () => this._tokenStore,
|
|
626
|
-
fetchPeerCert: (target, certFingerprint) => this._fetchPeerCert(target, certFingerprint || undefined),
|
|
774
|
+
fetchPeerCert: (target, certFingerprint, signedAt) => this._fetchPeerCert(target, certFingerprint || undefined, undefined, signedAt),
|
|
775
|
+
resolveEntityOwner: (target, type) => this._resolveEntityAgentMdOwner(target, type),
|
|
776
|
+
fetchEntityIdentity: (target, type) => this._freshEntityCertificate(target, type),
|
|
777
|
+
fetchEntitySigner: (target, fingerprint) => this._freshEntitySigner(target, fingerprint),
|
|
627
778
|
});
|
|
628
779
|
this._transport = new RPCTransport({
|
|
629
780
|
eventDispatcher: this._dispatcher,
|
|
@@ -633,7 +784,9 @@ export class AUNClient {
|
|
|
633
784
|
dnsNet,
|
|
634
785
|
});
|
|
635
786
|
this._transport.setProtocolMetaObserver((meta) => this._observeRpcMeta(meta));
|
|
787
|
+
this._transport.setServiceEventVerifier((event, payload) => verifyServiceEvent(event, payload, { targetAid: this._identity?.aid ?? '', resolveEventCertificate: (aid, fingerprint) => this._fetchServiceEventCertificate(aid, fingerprint), resolveCredentialService: this.resolveCredentialService }), () => this._identity?.aid ?? '', () => this._serviceEventIdentityVersion);
|
|
636
788
|
this._transport.setProtocolDisconnectCallback((err, closeCode) => this._handleTransportDisconnect(err, closeCode));
|
|
789
|
+
this._transport._onForegroundDrained = () => this._delivery.onForegroundPullsDrained();
|
|
637
790
|
this._runtime = new ClientRuntime(this);
|
|
638
791
|
this._identityRuntime = new IdentityRuntimeManager(this._runtime);
|
|
639
792
|
this._peerDirectory = new PeerDirectory(this._runtime);
|
|
@@ -660,6 +813,7 @@ export class AUNClient {
|
|
|
660
813
|
this._dispatcher.subscribeProtocol('_raw.message.received', (data) => this._onRawMessageReceived(data));
|
|
661
814
|
// P2P 消息撤回推送:在线 push 与 pull tombstone 去重后再发布给应用层
|
|
662
815
|
this._dispatcher.subscribeProtocol('_raw.message.recalled', (data) => this._safeAsync(this._onRawMessageRecalled(data)));
|
|
816
|
+
this._dispatcher.subscribeProtocol('_raw.message.read_changed', (data) => this._onRawReadChanged(data, 'p2p'));
|
|
663
817
|
// V2 P2P 推送通知:收到通知后自动走 message.v2.pull 拉取并解密
|
|
664
818
|
this._dispatcher.subscribeProtocol('_raw.peer.v2.message_received', (data) => {
|
|
665
819
|
const ns = this._aid ? `p2p:${this._aid}` : '';
|
|
@@ -687,6 +841,7 @@ export class AUNClient {
|
|
|
687
841
|
this._dispatcher.subscribeProtocol('_raw.group.message_created', (data) => this._onRawGroupMessageCreated(data));
|
|
688
842
|
// 群组消息撤回推送:在线 push 通道,与 pull 双 tombstone 兜底互补(SDK 去重只回调一次)
|
|
689
843
|
this._dispatcher.subscribeProtocol('_raw.group.message_recalled', (data) => this._safeAsync(this._onRawGroupMessageRecalled(data)));
|
|
844
|
+
this._dispatcher.subscribeProtocol('_raw.group.read_changed', (data) => this._onRawReadChanged(data, 'group'));
|
|
690
845
|
// 群组变更事件:透传并触发 V2 state/SPK 维护
|
|
691
846
|
this._dispatcher.subscribeProtocol('_raw.group.changed', (data) => this._onRawGroupChanged(data));
|
|
692
847
|
// 定向邀请底层事件:按群 + 目标 AID 聚合后向应用层只提示一次。
|
|
@@ -719,6 +874,16 @@ export class AUNClient {
|
|
|
719
874
|
return sessionToken || null;
|
|
720
875
|
}
|
|
721
876
|
/** AUN Storage VFS 入口 */
|
|
877
|
+
get credential() {
|
|
878
|
+
if (!this._credential)
|
|
879
|
+
this._credential = new CredentialFacade(this);
|
|
880
|
+
return this._credential;
|
|
881
|
+
}
|
|
882
|
+
get org() {
|
|
883
|
+
if (!this._org)
|
|
884
|
+
this._org = new OrgFacade(this);
|
|
885
|
+
return this._org;
|
|
886
|
+
}
|
|
722
887
|
get storage() {
|
|
723
888
|
if (!this._storage)
|
|
724
889
|
this._storage = new StorageVFS(this);
|
|
@@ -752,6 +917,88 @@ export class AUNClient {
|
|
|
752
917
|
get currentAid() {
|
|
753
918
|
return this._currentAid;
|
|
754
919
|
}
|
|
920
|
+
_ownershipKey(subjectAid, ownerAid) {
|
|
921
|
+
return `${String(subjectAid ?? '').trim().toLowerCase()}|${String(ownerAid ?? '').trim().toLowerCase()}`;
|
|
922
|
+
}
|
|
923
|
+
getType() {
|
|
924
|
+
return String(this._ownershipType || 'agent').trim() || 'agent';
|
|
925
|
+
}
|
|
926
|
+
get_type() {
|
|
927
|
+
return this.getType();
|
|
928
|
+
}
|
|
929
|
+
getOwner() {
|
|
930
|
+
const aid = String(this._aid ?? '').trim().toLowerCase();
|
|
931
|
+
let best = null;
|
|
932
|
+
let bestActive = false;
|
|
933
|
+
for (const record of this._ownershipRecords.values()) {
|
|
934
|
+
if (String(record.subject_aid ?? '').trim().toLowerCase() === aid) {
|
|
935
|
+
if (String(record.status ?? '').trim().toLowerCase() === 'released')
|
|
936
|
+
continue;
|
|
937
|
+
const owner = String(record.owner_aid ?? '').trim();
|
|
938
|
+
if (!owner)
|
|
939
|
+
continue;
|
|
940
|
+
const active = String(record.status ?? '').trim().toLowerCase() === 'active';
|
|
941
|
+
if (!best || active && !bestActive) {
|
|
942
|
+
best = owner;
|
|
943
|
+
bestActive = active;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
return best;
|
|
948
|
+
}
|
|
949
|
+
get_owner() {
|
|
950
|
+
return this.getOwner();
|
|
951
|
+
}
|
|
952
|
+
getOwnershipStatus() {
|
|
953
|
+
const aid = String(this._aid ?? '').trim().toLowerCase();
|
|
954
|
+
let best = null;
|
|
955
|
+
let bestActive = false;
|
|
956
|
+
for (const record of this._ownershipRecords.values()) {
|
|
957
|
+
const subject = String(record.subject_aid ?? '').trim().toLowerCase();
|
|
958
|
+
const owner = String(record.owner_aid ?? '').trim().toLowerCase();
|
|
959
|
+
if (aid !== subject && aid !== owner)
|
|
960
|
+
continue;
|
|
961
|
+
const active = String(record.status ?? '').trim().toLowerCase() === 'active';
|
|
962
|
+
if (!best || active && !bestActive) {
|
|
963
|
+
best = record;
|
|
964
|
+
bestActive = active;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
if (best)
|
|
968
|
+
return Object.fromEntries(Object.entries(best).filter(([key]) => key !== 'subject_identity' && key !== 'escrow_password' && key !== 'escrow_download_url'));
|
|
969
|
+
return { subject_aid: this._aid ?? '', owner_aid: null, status: 'legacy_unbound', type: this.getType() };
|
|
970
|
+
}
|
|
971
|
+
get_ownership_status() {
|
|
972
|
+
return this.getOwnershipStatus();
|
|
973
|
+
}
|
|
974
|
+
async bindOwneraid(ownerAid) { return this._getOwnershipWorkflow().bindOwneraid(ownerAid); }
|
|
975
|
+
async bindSubaid(subAid) { return this._getOwnershipWorkflow().bindSubaid(subAid); }
|
|
976
|
+
async releaseSubaid(subAid) { return this._getOwnershipWorkflow().releaseSubaid(subAid); }
|
|
977
|
+
async transferSubaid(subAid, newOwnerAid) { return this._getOwnershipWorkflow().transferSubaid(subAid, newOwnerAid); }
|
|
978
|
+
async checkOwnershipConsensus(subAid) { return this._getOwnershipWorkflow().checkOwnershipConsensus(subAid); }
|
|
979
|
+
async bindAid(subjectAid, options = {}) {
|
|
980
|
+
throw ownershipFailure('unsupported_protocol', 'legacy bindAid is unsupported; use bindSubaid');
|
|
981
|
+
}
|
|
982
|
+
async bind_aid(subjectAid, options = {}) { return this.bindAid(subjectAid, options); }
|
|
983
|
+
async bindOwner(ownerAid, options = {}) {
|
|
984
|
+
throw ownershipFailure('unsupported_protocol', 'legacy bindOwner is unsupported; use bindOwneraid');
|
|
985
|
+
}
|
|
986
|
+
async bind_owner(ownerAid, options = {}) { return this.bindOwner(ownerAid, options); }
|
|
987
|
+
async releaseOwnership(subjectAid) { throw ownershipFailure('unsupported_protocol', 'legacy releaseOwnership is unsupported; use releaseSubaid'); }
|
|
988
|
+
async release_ownership(subjectAid) { return this.releaseOwnership(subjectAid); }
|
|
989
|
+
async acceptOwnershipRequest(operationId) {
|
|
990
|
+
return this._getOwnershipWorkflow().acceptOwnershipRequest(operationId);
|
|
991
|
+
}
|
|
992
|
+
async rejectOwnershipRequest(operationId, reason) {
|
|
993
|
+
return this._getOwnershipWorkflow().rejectOwnershipRequest(operationId, reason);
|
|
994
|
+
}
|
|
995
|
+
async _handleOwnershipMessage(message) {
|
|
996
|
+
if (!message || typeof message !== 'object' || Array.isArray(message))
|
|
997
|
+
return false;
|
|
998
|
+
const envelope = message;
|
|
999
|
+
const content = 'payload' in envelope ? envelope.payload : message;
|
|
1000
|
+
return this._getOwnershipWorkflow().receive(String(envelope.from_aid ?? ''), decryptedOwnershipJsonText(content) ?? content);
|
|
1001
|
+
}
|
|
755
1002
|
get hasIdentity() {
|
|
756
1003
|
return this._currentAid !== null && this.state !== ConnectionState.CLOSED;
|
|
757
1004
|
}
|
|
@@ -881,6 +1128,9 @@ export class AUNClient {
|
|
|
881
1128
|
logger: this._logger.for('aun_core.keystore'),
|
|
882
1129
|
});
|
|
883
1130
|
this._tokenStore = tokenStore;
|
|
1131
|
+
this._ownershipStorage = null;
|
|
1132
|
+
this._ownershipFiles = null;
|
|
1133
|
+
this._ownershipWorkflow = null;
|
|
884
1134
|
this._auth = new AuthFlow({
|
|
885
1135
|
tokenStore,
|
|
886
1136
|
crypto: new CryptoProvider(),
|
|
@@ -909,7 +1159,10 @@ export class AUNClient {
|
|
|
909
1159
|
},
|
|
910
1160
|
auth: () => this._auth,
|
|
911
1161
|
tokenStore: () => this._tokenStore,
|
|
912
|
-
fetchPeerCert: (target, certFingerprint) => this._fetchPeerCert(target, certFingerprint || undefined),
|
|
1162
|
+
fetchPeerCert: (target, certFingerprint, signedAt) => this._fetchPeerCert(target, certFingerprint || undefined, undefined, signedAt),
|
|
1163
|
+
resolveEntityOwner: (target, type) => this._resolveEntityAgentMdOwner(target, type),
|
|
1164
|
+
fetchEntityIdentity: (target, type) => this._freshEntityCertificate(target, type),
|
|
1165
|
+
fetchEntitySigner: (target, fingerprint) => this._freshEntitySigner(target, fingerprint),
|
|
913
1166
|
});
|
|
914
1167
|
this._transport = new RPCTransport({
|
|
915
1168
|
eventDispatcher: this._dispatcher,
|
|
@@ -919,10 +1172,20 @@ export class AUNClient {
|
|
|
919
1172
|
dnsNet,
|
|
920
1173
|
});
|
|
921
1174
|
this._transport.setProtocolMetaObserver((meta) => this._observeRpcMeta(meta));
|
|
1175
|
+
this._transport.setServiceEventVerifier((event, payload) => verifyServiceEvent(event, payload, { targetAid: this._identity?.aid ?? '', resolveEventCertificate: (aid, fingerprint) => this._fetchServiceEventCertificate(aid, fingerprint), resolveCredentialService: this.resolveCredentialService }), () => this._identity?.aid ?? '', () => this._serviceEventIdentityVersion);
|
|
922
1176
|
this._transport.setProtocolDisconnectCallback((err, closeCode) => this._handleTransportDisconnect(err, closeCode));
|
|
1177
|
+
this._transport._onForegroundDrained = () => this._delivery.onForegroundPullsDrained();
|
|
923
1178
|
}
|
|
924
1179
|
loadIdentity(aid) {
|
|
1180
|
+
const previousAid = this._aid;
|
|
925
1181
|
this._identityRuntime.loadIdentity(aid);
|
|
1182
|
+
this._serviceEventIdentityVersion++;
|
|
1183
|
+
this._ensureReadStateIdentity();
|
|
1184
|
+
if (previousAid !== this._aid) {
|
|
1185
|
+
this._ownershipRecords.clear();
|
|
1186
|
+
this._ownershipType = 'agent';
|
|
1187
|
+
}
|
|
1188
|
+
this._restoreOwnershipRecords();
|
|
926
1189
|
}
|
|
927
1190
|
setProtectedHeaders(headers) {
|
|
928
1191
|
if (!headers) {
|
|
@@ -1001,7 +1264,7 @@ export class AUNClient {
|
|
|
1001
1264
|
}
|
|
1002
1265
|
/** 连接到 Gateway;身份来自构造函数或 loadIdentity(aid),认证由 SDK 内部自动完成。 */
|
|
1003
1266
|
async connect(opts) {
|
|
1004
|
-
|
|
1267
|
+
await this._lifecycle.connect(opts);
|
|
1005
1268
|
}
|
|
1006
1269
|
/** 关闭连接 */
|
|
1007
1270
|
async close() {
|
|
@@ -1020,8 +1283,13 @@ export class AUNClient {
|
|
|
1020
1283
|
* 自动处理内部方法限制、E2EE 加解密、客户端签名等。
|
|
1021
1284
|
*/
|
|
1022
1285
|
async call(method, params) {
|
|
1286
|
+
const readIdentity = ['message.read', 'message.read_state', 'group.read', 'group.read_state'].includes(method)
|
|
1287
|
+
? this._ensureReadStateIdentity() : null;
|
|
1023
1288
|
try {
|
|
1024
|
-
|
|
1289
|
+
const result = await this._rpcPipeline.call(method, params);
|
|
1290
|
+
if (readIdentity !== null && readIdentity === this._ensureReadStateIdentity())
|
|
1291
|
+
this._observeReadStateResult(method, result, params ?? {});
|
|
1292
|
+
return result;
|
|
1025
1293
|
}
|
|
1026
1294
|
catch (err) {
|
|
1027
1295
|
await this._maybeHandleHalfOpenRpcFailure(err);
|
|
@@ -1074,12 +1342,23 @@ export class AUNClient {
|
|
|
1074
1342
|
const target = message.slice(agentPrefix.length).trim();
|
|
1075
1343
|
if (!target || target.toLowerCase() !== String(this._aid ?? '').trim().toLowerCase())
|
|
1076
1344
|
return false;
|
|
1077
|
-
|
|
1345
|
+
return this._ensureInitialAgentMd();
|
|
1346
|
+
}
|
|
1347
|
+
async _ensureInitialAgentMd() {
|
|
1348
|
+
const aid = this._aid;
|
|
1349
|
+
if (!aid || !this._currentAid)
|
|
1350
|
+
return false;
|
|
1351
|
+
const observed = await this._getOwnershipFiles().readAgent(aid);
|
|
1352
|
+
if (observed.exists)
|
|
1353
|
+
return false;
|
|
1354
|
+
let agentMd = buildDefaultAgentMd(aid, { type: this.getType() });
|
|
1078
1355
|
try {
|
|
1079
|
-
agentMd = this._agentMdManager.readContent(
|
|
1356
|
+
agentMd = this._agentMdManager.readContent(aid) || agentMd;
|
|
1080
1357
|
}
|
|
1081
1358
|
catch { }
|
|
1082
|
-
|
|
1359
|
+
const payload = updateOwnershipAgentMd(agentMd, {});
|
|
1360
|
+
const raw = signOwnershipFile(this._currentAid, payload.endsWith('\n') || payload.endsWith('\r') ? payload : `${payload}\n`, 'agent');
|
|
1361
|
+
await this._agentMdManager.submitOwnershipRaw(aid, raw, { if_none_match: '*' });
|
|
1083
1362
|
return true;
|
|
1084
1363
|
}
|
|
1085
1364
|
async _checkIdentityAdmissionAfterConnect() {
|
|
@@ -1087,10 +1366,12 @@ export class AUNClient {
|
|
|
1087
1366
|
if (!aid)
|
|
1088
1367
|
return;
|
|
1089
1368
|
try {
|
|
1090
|
-
const checked = await this._agentMdManager.check(aid);
|
|
1369
|
+
const checked = await this._agentMdManager.check(aid, 0);
|
|
1091
1370
|
if (!checked.remote_found) {
|
|
1092
|
-
|
|
1093
|
-
|
|
1371
|
+
await this._ensureInitialAgentMd();
|
|
1372
|
+
}
|
|
1373
|
+
else if (checked.in_sync === false) {
|
|
1374
|
+
await this._agentMdManager.download(aid);
|
|
1094
1375
|
}
|
|
1095
1376
|
}
|
|
1096
1377
|
catch (exc) {
|
|
@@ -1099,6 +1380,7 @@ export class AUNClient {
|
|
|
1099
1380
|
const store = this._aidStore;
|
|
1100
1381
|
if (!store)
|
|
1101
1382
|
return;
|
|
1383
|
+
await this._resumeOrgIdentityAfterConnect(store);
|
|
1102
1384
|
let listed;
|
|
1103
1385
|
try {
|
|
1104
1386
|
listed = await this.call('group.list_my', {});
|
|
@@ -1135,6 +1417,8 @@ export class AUNClient {
|
|
|
1135
1417
|
await this.bindGroupAid({ group_id: groupId, group_aid: groupAid }, { aidStore: store });
|
|
1136
1418
|
return;
|
|
1137
1419
|
}
|
|
1420
|
+
if (await this._resumeGroupAgentMdUpload(store, groupAid))
|
|
1421
|
+
return;
|
|
1138
1422
|
const checked = await this._agentMdManager.check(groupAid);
|
|
1139
1423
|
if (!checked.remote_found) {
|
|
1140
1424
|
await this._uploadGroupAgentMd(store, groupAid, {}, {});
|
|
@@ -1165,6 +1449,192 @@ export class AUNClient {
|
|
|
1165
1449
|
this._groupIdentityOperations.delete(key);
|
|
1166
1450
|
}
|
|
1167
1451
|
}
|
|
1452
|
+
async createOrg(params = {}, options = {}) {
|
|
1453
|
+
const store = options.aidStore;
|
|
1454
|
+
const owner = this._currentAid;
|
|
1455
|
+
const orgAid = String(params.org_id ?? '').trim();
|
|
1456
|
+
if (!store || !owner?.isPrivateKeyValid() || !orgAid)
|
|
1457
|
+
throw new ValidationError('createOrg requires aidStore, org_id and current owner identity');
|
|
1458
|
+
this._aidStore = store;
|
|
1459
|
+
let identity = store.load(orgAid);
|
|
1460
|
+
if (!identity.ok) {
|
|
1461
|
+
const registered = await store.register(orgAid, { type: 'org', owner: owner.aid });
|
|
1462
|
+
if (!registered.ok)
|
|
1463
|
+
throw ownershipFailure('unavailable', registered.error.message, true);
|
|
1464
|
+
identity = store.load(orgAid);
|
|
1465
|
+
}
|
|
1466
|
+
if (!identity.ok)
|
|
1467
|
+
throw ownershipFailure('unavailable', identity.error.message, true);
|
|
1468
|
+
const target = options.serviceAid ?? `org.${orgAid.slice(orgAid.indexOf('.') + 1)}`;
|
|
1469
|
+
const tokens = store._tokenStore;
|
|
1470
|
+
const inputHash = crypto.createHash('sha256').update(canonicalJson(params)).digest('hex');
|
|
1471
|
+
let checkpoint = tokens.loadMetadata(orgAid)?.org_identity_creation;
|
|
1472
|
+
if (checkpoint && (checkpoint.owner_aid !== owner.aid || checkpoint.input_hash !== inputHash))
|
|
1473
|
+
throw ownershipFailure('conflict', 'pending Org creation context changed');
|
|
1474
|
+
if (!checkpoint) {
|
|
1475
|
+
const prepared = this.org.prepareRequest('org.lifecycle.create', { ...params, owner_aid: owner.aid }, target, owner, { orgSigner: identity.data.aid });
|
|
1476
|
+
checkpoint = { owner_aid: owner.aid, input_hash: inputHash, source_params: params, prepared, phase: 'create' };
|
|
1477
|
+
tokens.saveMetadata(orgAid, { org_identity_creation: checkpoint });
|
|
1478
|
+
}
|
|
1479
|
+
if (checkpoint.phase === 'create') {
|
|
1480
|
+
const result = await this.call('org.lifecycle.create', checkpoint.prepared);
|
|
1481
|
+
checkpoint = { ...checkpoint, result, phase: 'upload' };
|
|
1482
|
+
tokens.saveMetadata(orgAid, { org_identity_creation: checkpoint });
|
|
1483
|
+
}
|
|
1484
|
+
await this._uploadOrgAgentMd(store, orgAid, params);
|
|
1485
|
+
tokens.saveMetadata(orgAid, { org_identity_creation: null });
|
|
1486
|
+
return checkpoint.result;
|
|
1487
|
+
}
|
|
1488
|
+
async _resumeOrgIdentityAfterConnect(store) {
|
|
1489
|
+
const keyStore = store._keystore;
|
|
1490
|
+
const tokens = store._tokenStore;
|
|
1491
|
+
if (typeof keyStore?.listIdentities !== 'function' || typeof tokens?.loadMetadata !== 'function')
|
|
1492
|
+
return;
|
|
1493
|
+
for (const aid of keyStore.listIdentities()) {
|
|
1494
|
+
const metadata = tokens.loadMetadata(aid);
|
|
1495
|
+
const creation = metadata?.org_identity_creation;
|
|
1496
|
+
const transfer = metadata?.org_identity_transfer;
|
|
1497
|
+
try {
|
|
1498
|
+
if (creation?.owner_aid === this._aid) {
|
|
1499
|
+
await this.createOrg(creation.source_params, { aidStore: store });
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
if (transfer?.owner_aid === this._aid) {
|
|
1503
|
+
await this.completeOrgTransfer(transfer.source_params, { aidStore: store });
|
|
1504
|
+
continue;
|
|
1505
|
+
}
|
|
1506
|
+
const pending = store._agentMdManager.pendingUpload(aid);
|
|
1507
|
+
if (!pending)
|
|
1508
|
+
continue;
|
|
1509
|
+
const fields = validateAgentMdDocument(pending.content, { expectedAid: aid }).frontmatter;
|
|
1510
|
+
if (fields.type === 'org' && fields.owner === this._aid)
|
|
1511
|
+
await this._resumeGroupAgentMdUpload(store, aid);
|
|
1512
|
+
}
|
|
1513
|
+
catch (error) {
|
|
1514
|
+
this._clientLog.warn(`post-connect Org identity recovery failed: aid=${aid} err=${formatCaughtError(error)}`);
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
async completeOrgTransfer(params = {}, options = {}) {
|
|
1519
|
+
const store = options.aidStore;
|
|
1520
|
+
const owner = this._currentAid;
|
|
1521
|
+
const orgAid = String(params.org_id ?? '').trim();
|
|
1522
|
+
const transferId = String(params.transfer_id ?? '').trim();
|
|
1523
|
+
if (!store || !owner?.isPrivateKeyValid() || !orgAid || !transferId)
|
|
1524
|
+
throw new ValidationError('completeOrgTransfer requires aidStore, org_id, transfer_id and current accepting owner');
|
|
1525
|
+
this._aidStore = store;
|
|
1526
|
+
const localStore = store._keystore;
|
|
1527
|
+
const tokenStore = store._tokenStore;
|
|
1528
|
+
const pendingKey = `org-transfer:${orgAid}:${transferId}`;
|
|
1529
|
+
const inputHash = crypto.createHash('sha256').update(canonicalJson(params)).digest('hex');
|
|
1530
|
+
let keyPair = localStore.loadPendingGroupBind(pendingKey);
|
|
1531
|
+
if (!keyPair) {
|
|
1532
|
+
keyPair = new CryptoProvider().generateIdentity();
|
|
1533
|
+
localStore.savePendingGroupBind(pendingKey, keyPair);
|
|
1534
|
+
}
|
|
1535
|
+
const target = options.serviceAid ?? `org.${orgAid.slice(orgAid.indexOf('.') + 1)}`;
|
|
1536
|
+
const publicKey = crypto.createPublicKey({ key: Buffer.from(keyPair.public_key_der_b64, 'base64'), format: 'der', type: 'spki' }).export({ format: 'pem', type: 'spki' }).toString();
|
|
1537
|
+
let pending = tokenStore.loadMetadata(orgAid)?.org_identity_transfer;
|
|
1538
|
+
if (pending && (pending.transfer_id !== transferId || pending.owner_aid !== owner.aid || pending.public_key !== publicKey || pending.input_hash !== inputHash))
|
|
1539
|
+
throw ownershipFailure('conflict', 'pending Org transfer context changed');
|
|
1540
|
+
if (!pending) {
|
|
1541
|
+
const rekey = { ...params, stage: 'rekey', public_key: publicKey, curve: 'P-256' };
|
|
1542
|
+
delete rekey.new_owner;
|
|
1543
|
+
delete rekey.request_binding;
|
|
1544
|
+
delete rekey.client_signature;
|
|
1545
|
+
pending = { transfer_id: transferId, owner_aid: owner.aid, public_key: publicKey, input_hash: inputHash, phase: 'rekey', source_params: params, rekey_params: this.org.prepareRequest('org.lifecycle.transferAccept', rekey, target, owner) };
|
|
1546
|
+
tokenStore.saveMetadata(orgAid, { org_identity_transfer: pending });
|
|
1547
|
+
}
|
|
1548
|
+
if (pending.phase === 'rekey') {
|
|
1549
|
+
const result = await this.call('org.lifecycle.transferAccept', pending.rekey_params);
|
|
1550
|
+
pending = { ...pending, phase: 'finalize', rekey_result: result };
|
|
1551
|
+
tokenStore.saveMetadata(orgAid, { org_identity_transfer: pending });
|
|
1552
|
+
}
|
|
1553
|
+
const rekeyResult = pending.rekey_result;
|
|
1554
|
+
const certPem = String(rekeyResult?.cert_pem ?? rekeyResult?.cert ?? '').trim();
|
|
1555
|
+
if (!certPem)
|
|
1556
|
+
throw ownershipFailure('unavailable', 'Org rekey response has no certificate', true);
|
|
1557
|
+
const imported = store.importGroupIdentity(orgAid, { ...keyPair, cert_pem: certPem });
|
|
1558
|
+
if (!imported.ok)
|
|
1559
|
+
throw ownershipFailure('invalid', imported.error.message);
|
|
1560
|
+
const orgIdentity = this._localEntityCertificate(orgAid);
|
|
1561
|
+
if (!pending.finalize_params) {
|
|
1562
|
+
const final = { ...pending.source_params, stage: 'finalize', public_key: publicKey, curve: 'P-256', request_id: crypto.randomUUID().replace(/-/g, ''), nonce: crypto.randomBytes(32).toString('base64url') };
|
|
1563
|
+
delete final.request_binding;
|
|
1564
|
+
delete final.client_signature;
|
|
1565
|
+
for (const field of ['previous_owner', 'new_owner']) {
|
|
1566
|
+
const material = isJsonObject(final[field]) ? final[field] : {};
|
|
1567
|
+
let binding = isJsonObject(material.membership_binding) ? material.membership_binding : undefined;
|
|
1568
|
+
if (!binding || !isJsonObject(material.terms))
|
|
1569
|
+
throw ownershipFailure('invalid', 'Org transfer requires both replacement membership bindings and terms');
|
|
1570
|
+
if (!binding.member_signature && binding.member_aid === owner.aid)
|
|
1571
|
+
binding = signMembershipBinding(binding, owner, 'member');
|
|
1572
|
+
binding = signMembershipBinding(binding, orgIdentity, 'org');
|
|
1573
|
+
const oldCredential = isJsonObject(material.credential_template) ? { ...material.credential_template } : undefined;
|
|
1574
|
+
if (oldCredential) {
|
|
1575
|
+
delete oldCredential.signature;
|
|
1576
|
+
oldCredential.key_version = orgIdentity.certFingerprint;
|
|
1577
|
+
}
|
|
1578
|
+
const credential = oldCredential ? createCredential(oldCredential, orgIdentity) : createMemberCredential(binding, orgIdentity, { key_version: orgIdentity.certFingerprint });
|
|
1579
|
+
final[field] = { ...material, membership_binding: binding, credential_template: credential };
|
|
1580
|
+
delete final[field].registration_authorization;
|
|
1581
|
+
}
|
|
1582
|
+
pending = { ...pending, finalize_params: this.org.prepareRequest('org.lifecycle.transferAccept', final, target, owner, { orgSigner: orgIdentity }) };
|
|
1583
|
+
tokenStore.saveMetadata(orgAid, { org_identity_transfer: pending });
|
|
1584
|
+
}
|
|
1585
|
+
const result = await this.call('org.lifecycle.transferAccept', pending.finalize_params);
|
|
1586
|
+
await this._uploadOrgAgentMd(store, orgAid, pending.source_params);
|
|
1587
|
+
localStore.clearPendingGroupBind(pendingKey);
|
|
1588
|
+
tokenStore.saveMetadata(orgAid, { org_identity_transfer: null });
|
|
1589
|
+
return result;
|
|
1590
|
+
}
|
|
1591
|
+
async dissolveOrg(params = {}, options = {}) {
|
|
1592
|
+
const store = options.aidStore ?? this._aidStore;
|
|
1593
|
+
if (store)
|
|
1594
|
+
this._aidStore = store;
|
|
1595
|
+
const orgAid = String(params.org_id ?? '').trim();
|
|
1596
|
+
const owner = this._currentAid;
|
|
1597
|
+
const target = options.serviceAid ?? `org.${orgAid.slice(orgAid.indexOf('.') + 1)}`;
|
|
1598
|
+
const payload = owner ? this.org.prepareRequest('org.lifecycle.dissolve', params, target, owner) : params;
|
|
1599
|
+
const result = await this.call('org.lifecycle.dissolve', payload);
|
|
1600
|
+
const organization = isJsonObject(result?.organization) ? result.organization : {};
|
|
1601
|
+
const status = String(organization.status ?? result?.status ?? '');
|
|
1602
|
+
if (store && orgAid && status === 'dissolved') {
|
|
1603
|
+
const tokenStore = store._tokenStore;
|
|
1604
|
+
const keyStore = store._keystore;
|
|
1605
|
+
const certPem = keyStore?.loadCert?.(orgAid);
|
|
1606
|
+
const metadata = tokenStore?.loadMetadata?.(orgAid) ?? {};
|
|
1607
|
+
const transfer = metadata.org_identity_transfer;
|
|
1608
|
+
if (transfer?.transfer_id && typeof keyStore?.clearPendingGroupBind === 'function') {
|
|
1609
|
+
keyStore.clearPendingGroupBind(`org-transfer:${orgAid}:${String(transfer.transfer_id)}`);
|
|
1610
|
+
}
|
|
1611
|
+
tokenStore?.saveMetadata?.(orgAid, {
|
|
1612
|
+
...(certPem ? { invalidated_entity_certificate: certFingerprint(certPem) } : {}),
|
|
1613
|
+
org_identity_creation: undefined,
|
|
1614
|
+
org_identity_transfer: undefined,
|
|
1615
|
+
});
|
|
1616
|
+
tokenStore?.saveInstanceState?.(orgAid, store.deviceId, store.slotId, {});
|
|
1617
|
+
store._agentMdManager?.saveRecord?.(orgAid, {
|
|
1618
|
+
pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '',
|
|
1619
|
+
pending_condition: {}, pending_relation: {}, pending_authorization: '',
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1622
|
+
return result;
|
|
1623
|
+
}
|
|
1624
|
+
async _uploadOrgAgentMd(store, orgAid, params) {
|
|
1625
|
+
const owner = this._currentAid;
|
|
1626
|
+
if (!owner)
|
|
1627
|
+
throw ownershipFailure('forbidden', 'Org agent.md requires current owner');
|
|
1628
|
+
if (await this._resumeGroupAgentMdUpload(store, orgAid))
|
|
1629
|
+
return;
|
|
1630
|
+
const identity = this._localEntityCertificate(orgAid);
|
|
1631
|
+
const content = String(params.org_agent_md ?? buildDefaultAgentMd(orgAid, { type: 'org', owner: owner.aid, entityCertFingerprint: identity.certFingerprint, name: String(params.name ?? orgAid) }));
|
|
1632
|
+
const uploaded = await store.uploadAgentMd(orgAid, content, { uploaderAid: owner.aid, entityOwnerResolver: (aid, type) => this._resolveEntityAgentMdOwner(aid, type), entityIdentityGetter: (aid) => this._freshEntityCertificate(aid, 'org') });
|
|
1633
|
+
if (!uploaded.ok) {
|
|
1634
|
+
const code = ['invalid', 'unavailable', 'forbidden', 'conflict', 'persistence_unavailable'].includes(uploaded.error.code) ? uploaded.error.code : uploaded.error.code === 'NETWORK_ERROR' ? 'unavailable' : 'invalid';
|
|
1635
|
+
throw ownershipFailure(code, uploaded.error.message, code === 'unavailable');
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1168
1638
|
async createGroup(params = {}, options = {}) {
|
|
1169
1639
|
if (!isJsonObject(params)) {
|
|
1170
1640
|
throw new ValidationError('createGroup params must be an object');
|
|
@@ -1333,6 +1803,8 @@ export class AUNClient {
|
|
|
1333
1803
|
`name: ${JSON.stringify(name)}`,
|
|
1334
1804
|
'type: "group"',
|
|
1335
1805
|
'version: "1.0.0"',
|
|
1806
|
+
`owner: ${JSON.stringify(String(group.owner_aid ?? group.owner ?? this._aid ?? ''))}`,
|
|
1807
|
+
`entity_cert_fingerprint: ${JSON.stringify(this._localEntityCertificate(groupAid).certFingerprint)}`,
|
|
1336
1808
|
`description: ${JSON.stringify(description)}`,
|
|
1337
1809
|
...(avatar ? [`avatar: ${JSON.stringify(avatar)}`] : []),
|
|
1338
1810
|
'---',
|
|
@@ -1341,16 +1813,71 @@ export class AUNClient {
|
|
|
1341
1813
|
'',
|
|
1342
1814
|
].join('\n');
|
|
1343
1815
|
}
|
|
1816
|
+
async _resumeGroupAgentMdUpload(store, groupAid) {
|
|
1817
|
+
const identityStore = store;
|
|
1818
|
+
const pendingManager = identityStore._agentMdManager ?? this._agentMdManager;
|
|
1819
|
+
const pending = pendingManager.pendingUpload(groupAid);
|
|
1820
|
+
if (!pending)
|
|
1821
|
+
return false;
|
|
1822
|
+
const fields = validateAgentMdDocument(pending.content, { expectedAid: groupAid, validateOwnership: false }).frontmatter;
|
|
1823
|
+
if (['ownership', 'ownership_proof', 'ownership_release'].some((field) => Object.hasOwn(fields, field))) {
|
|
1824
|
+
throw new StateError('ownership pending agent.md requires its persisted 2.1 task', { stringCode: 'unsupported_protocol' });
|
|
1825
|
+
}
|
|
1826
|
+
const identity = this._currentAid;
|
|
1827
|
+
if (!identity || fields.owner !== identity.aid)
|
|
1828
|
+
throw ownershipFailure('forbidden', 'group agent.md recovery requires its original current owner');
|
|
1829
|
+
const manager = new AgentMdManager({
|
|
1830
|
+
aunPath: store.aunPath,
|
|
1831
|
+
verifySsl: identityStore._verifySsl,
|
|
1832
|
+
logger: identityStore._log.for('aun_core.agent_md'),
|
|
1833
|
+
uploadViaGatewayHost: true,
|
|
1834
|
+
ownerAidGetter: () => identity.aid,
|
|
1835
|
+
currentAidGetter: () => identity,
|
|
1836
|
+
entityOwnerResolver: (aid, type) => this._resolveEntityAgentMdOwner(aid, type),
|
|
1837
|
+
entityIdentityGetter: (aid, type) => this._freshEntityCertificate(aid, type),
|
|
1838
|
+
entitySignerResolver: (aid, fingerprint) => this._freshEntitySigner(aid, fingerprint),
|
|
1839
|
+
gatewayResolver: (aid) => identityStore._resolveGateway(aid),
|
|
1840
|
+
accessTokenResolver: (_aid, gatewayUrl) => identityStore._uploadAgentMdToken(identity, gatewayUrl),
|
|
1841
|
+
aidValidator: (aid) => identityStore._registerFlow.validateAidName(aid),
|
|
1842
|
+
});
|
|
1843
|
+
return await manager.resumePendingUpload(groupAid);
|
|
1844
|
+
}
|
|
1344
1845
|
async _uploadGroupAgentMd(store, groupAid, params, group) {
|
|
1345
1846
|
const uploaderAid = String(this._aid ?? '').trim();
|
|
1346
1847
|
if (!uploaderAid) {
|
|
1347
1848
|
throw new ValidationError('group agent.md upload requires current owner AID');
|
|
1348
1849
|
}
|
|
1349
|
-
|
|
1850
|
+
this._aidStore = store;
|
|
1851
|
+
if (await this._resumeGroupAgentMdUpload(store, groupAid))
|
|
1852
|
+
return;
|
|
1853
|
+
const uploaded = await store.uploadAgentMd(groupAid, this._groupAgentMdContent(groupAid, params, group), { uploaderAid, entityOwnerResolver: (aid, type) => this._resolveEntityAgentMdOwner(aid, type), entityIdentityGetter: (aid) => this._freshEntityCertificate(aid) });
|
|
1350
1854
|
if (!uploaded.ok) {
|
|
1351
|
-
|
|
1855
|
+
const code = ['invalid', 'unavailable', 'forbidden', 'conflict', 'persistence_unavailable'].includes(uploaded.error.code) ? uploaded.error.code : uploaded.error.code === 'NETWORK_ERROR' ? 'unavailable' : 'invalid';
|
|
1856
|
+
throw ownershipFailure(code, `group agent.md upload failed: ${uploaded.error.message}`, code === 'unavailable');
|
|
1352
1857
|
}
|
|
1353
1858
|
}
|
|
1859
|
+
_localEntityCertificate(aid) {
|
|
1860
|
+
const loaded = this._aidStore?.load(aid);
|
|
1861
|
+
if (!loaded?.ok)
|
|
1862
|
+
throw ownershipFailure('unavailable', 'entity local certificate is unavailable', true);
|
|
1863
|
+
return loaded.data.aid;
|
|
1864
|
+
}
|
|
1865
|
+
async _freshEntityCertificate(aid, type = 'group') {
|
|
1866
|
+
const material = await this._auth.resolveCurrentEntityCertificate(await this._resolveOwnershipCertificateGateway(aid), aid, type);
|
|
1867
|
+
const certPem = material.cert_pem;
|
|
1868
|
+
return AID._create({ aid, aunPath: this._configModel.aunPath, certPem, certValid: true, privateKeyPem: null, privateKeyValid: false });
|
|
1869
|
+
}
|
|
1870
|
+
async _freshEntitySigner(aid, fingerprint) {
|
|
1871
|
+
const material = await this._auth.resolveOwnershipCertificate(await this._resolveOwnershipCertificateGateway(aid), aid, fingerprint, 'signing');
|
|
1872
|
+
return AID._create({ aid, aunPath: this._configModel.aunPath, certPem: material.cert_pem, certValid: true, privateKeyPem: null, privateKeyValid: false });
|
|
1873
|
+
}
|
|
1874
|
+
async _resolveEntityAgentMdOwner(aid, type) {
|
|
1875
|
+
const authority = await this._agentMdManager.readEntityAuthorityFresh(aid);
|
|
1876
|
+
const identity = await this._freshEntityCertificate(aid, type);
|
|
1877
|
+
if (authority.entity_cert_fingerprint !== identity.certFingerprint)
|
|
1878
|
+
throw ownershipFailure('conflict', 'entity authority changed during current-certificate verification');
|
|
1879
|
+
return authority.owner_aid;
|
|
1880
|
+
}
|
|
1354
1881
|
async bindGroupAid(params = {}, options = {}) {
|
|
1355
1882
|
if (!isJsonObject(params)) {
|
|
1356
1883
|
throw new ValidationError('bindGroupAid params must be an object');
|
|
@@ -1788,6 +2315,168 @@ export class AUNClient {
|
|
|
1788
2315
|
this._clientLog.debug(`on exit: elapsed=${Date.now() - tStart}ms event=${event}`);
|
|
1789
2316
|
return result;
|
|
1790
2317
|
}
|
|
2318
|
+
_readStateIsNewer(previous, current) {
|
|
2319
|
+
if (!previous)
|
|
2320
|
+
return true;
|
|
2321
|
+
const oldOrder = BigInt(String(previous.last_read_order ?? 0));
|
|
2322
|
+
const newOrder = BigInt(String(current.last_read_order ?? 0));
|
|
2323
|
+
if (oldOrder !== newOrder)
|
|
2324
|
+
return newOrder > oldOrder;
|
|
2325
|
+
const oldId = String(previous.last_read_message_id ?? '');
|
|
2326
|
+
const newId = String(current.last_read_message_id ?? '');
|
|
2327
|
+
if (oldOrder === 0n)
|
|
2328
|
+
return newId === oldId;
|
|
2329
|
+
const oldPoints = Array.from(oldId);
|
|
2330
|
+
const newPoints = Array.from(newId);
|
|
2331
|
+
for (let index = 0; index < Math.min(oldPoints.length, newPoints.length); index++) {
|
|
2332
|
+
const oldPoint = oldPoints[index].codePointAt(0);
|
|
2333
|
+
const newPoint = newPoints[index].codePointAt(0);
|
|
2334
|
+
if (oldPoint !== newPoint)
|
|
2335
|
+
return newPoint > oldPoint;
|
|
2336
|
+
}
|
|
2337
|
+
return newPoints.length >= oldPoints.length;
|
|
2338
|
+
}
|
|
2339
|
+
_ensureReadStateIdentity() {
|
|
2340
|
+
const identity = `${this._aid ?? ''}\u0000${this._deviceId ?? ''}`;
|
|
2341
|
+
if (identity !== this._readStateIdentity) {
|
|
2342
|
+
this._readStateCache.clear();
|
|
2343
|
+
this._readStateQueries.clear();
|
|
2344
|
+
this._readStateIdentity = identity;
|
|
2345
|
+
this._readStateGeneration += 1;
|
|
2346
|
+
}
|
|
2347
|
+
return `${identity}\u0000${this._readStateGeneration}`;
|
|
2348
|
+
}
|
|
2349
|
+
_registerReadStateQuery(method, params) {
|
|
2350
|
+
const key = method + '\u0000' + JSON.stringify(Object.keys(params).sort().map((field) => [field, params[field]]));
|
|
2351
|
+
this._readStateQueries.set(key, { method, params: { ...params } });
|
|
2352
|
+
}
|
|
2353
|
+
_readStateItems(result) {
|
|
2354
|
+
const resultObject = isJsonObject(result) ? result : null;
|
|
2355
|
+
return Array.isArray(resultObject?.states)
|
|
2356
|
+
? resultObject.states.filter((item) => isJsonObject(item))
|
|
2357
|
+
: [];
|
|
2358
|
+
}
|
|
2359
|
+
_observeReadStateResult(method, result, params) {
|
|
2360
|
+
if (!['message.read', 'message.read_state', 'group.read', 'group.read_state'].includes(method))
|
|
2361
|
+
return;
|
|
2362
|
+
this._ensureReadStateIdentity();
|
|
2363
|
+
const isGroup = method.startsWith('group.');
|
|
2364
|
+
if (method.endsWith('.read')) {
|
|
2365
|
+
for (const state of this._readStateItems(params)) {
|
|
2366
|
+
const query = { topic_key: state.topic_key ?? '' };
|
|
2367
|
+
if (isGroup)
|
|
2368
|
+
query.group_id = state.group_id ?? params.group_id ?? '';
|
|
2369
|
+
else
|
|
2370
|
+
query.scope = state.scope || params.scope || 'p2p';
|
|
2371
|
+
this._registerReadStateQuery(`${isGroup ? 'group' : 'message'}.read_state`, query);
|
|
2372
|
+
}
|
|
2373
|
+
if (!Array.isArray(params.states) && params.topic_key) {
|
|
2374
|
+
const query = {};
|
|
2375
|
+
for (const field of ['scope', 'group_id', 'topic_key']) {
|
|
2376
|
+
if (field in params)
|
|
2377
|
+
query[field] = params[field];
|
|
2378
|
+
}
|
|
2379
|
+
this._registerReadStateQuery(`${isGroup ? 'group' : 'message'}.read_state`, query);
|
|
2380
|
+
}
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2383
|
+
this._registerReadStateQuery(method, params);
|
|
2384
|
+
for (const state of this._readStateItems(result)) {
|
|
2385
|
+
const scope = String(state.scope ?? (isGroup ? 'group' : 'p2p')).trim();
|
|
2386
|
+
const topicKey = String(state.topic_key ?? params.topic_key ?? '');
|
|
2387
|
+
const groupId = isGroup ? String(state.group_id ?? params.group_id ?? '').trim() : '';
|
|
2388
|
+
const lastRead = String(state.last_read_message_id ?? '').trim();
|
|
2389
|
+
if (!scope || !topicKey || !lastRead || (isGroup && !groupId))
|
|
2390
|
+
continue;
|
|
2391
|
+
const key = `${scope}\u0000${groupId}\u0000${topicKey}`;
|
|
2392
|
+
const query = { topic_key: topicKey };
|
|
2393
|
+
if (isGroup)
|
|
2394
|
+
query.group_id = groupId;
|
|
2395
|
+
else
|
|
2396
|
+
query.scope = scope;
|
|
2397
|
+
const previous = this._readStateCache.get(key)?.state;
|
|
2398
|
+
if (previous && !this._readStateIsNewer(previous, state))
|
|
2399
|
+
continue;
|
|
2400
|
+
const samePosition = previous?.last_read_message_id === state.last_read_message_id
|
|
2401
|
+
&& BigInt(String(previous?.last_read_order ?? 0)) === BigInt(String(state.last_read_order ?? 0));
|
|
2402
|
+
this._readStateCache.set(key, {
|
|
2403
|
+
method: `${isGroup ? 'group' : 'message'}.read_state`,
|
|
2404
|
+
params: query,
|
|
2405
|
+
state: samePosition ? { ...previous, ...state } : { ...state },
|
|
2406
|
+
});
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
_onRawReadChanged(data, expectedScope) {
|
|
2410
|
+
if (!isJsonObject(data))
|
|
2411
|
+
return;
|
|
2412
|
+
const eventData = data;
|
|
2413
|
+
if (eventData.scope !== expectedScope || eventData.owner_aid !== this._aid)
|
|
2414
|
+
return;
|
|
2415
|
+
const fields = ['owner_aid', 'topic_key', 'last_read_message_id', ...(expectedScope === 'group' ? ['group_id'] : [])];
|
|
2416
|
+
if (fields.some((field) => {
|
|
2417
|
+
const value = eventData[field];
|
|
2418
|
+
return typeof value !== 'string' || !value || Array.from(value).length > 128;
|
|
2419
|
+
}))
|
|
2420
|
+
return;
|
|
2421
|
+
const order = eventData.last_read_order;
|
|
2422
|
+
if (!(typeof order === 'number' && Number.isSafeInteger(order) && order > 0)
|
|
2423
|
+
&& !(typeof order === 'string' && /^[1-9]\d*$/.test(order)))
|
|
2424
|
+
return;
|
|
2425
|
+
this._ensureReadStateIdentity();
|
|
2426
|
+
const scope = expectedScope;
|
|
2427
|
+
const topicKey = String(eventData.topic_key ?? '');
|
|
2428
|
+
const groupId = scope === 'group' ? String(eventData.group_id) : '';
|
|
2429
|
+
const lastRead = String(eventData.last_read_message_id);
|
|
2430
|
+
if (scope && topicKey && lastRead && (scope !== 'group' || groupId)) {
|
|
2431
|
+
const key = `${scope}\u0000${groupId}\u0000${topicKey}`;
|
|
2432
|
+
const query = { topic_key: topicKey };
|
|
2433
|
+
if (scope === 'group')
|
|
2434
|
+
query.group_id = groupId;
|
|
2435
|
+
else
|
|
2436
|
+
query.scope = scope;
|
|
2437
|
+
const method = `${scope === 'group' ? 'group' : 'message'}.read_state`;
|
|
2438
|
+
this._registerReadStateQuery(method, query);
|
|
2439
|
+
const previous = this._readStateCache.get(key)?.state;
|
|
2440
|
+
if (previous && (!this._readStateIsNewer(previous, eventData)
|
|
2441
|
+
|| (previous.last_read_message_id === eventData.last_read_message_id
|
|
2442
|
+
&& BigInt(String(previous.last_read_order ?? 0)) === BigInt(String(eventData.last_read_order ?? 0)))))
|
|
2443
|
+
return;
|
|
2444
|
+
this._readStateCache.set(key, { method, params: query, state: { ...eventData } });
|
|
2445
|
+
}
|
|
2446
|
+
this._dispatcher.enqueue(scope === 'group' ? 'group.read_changed' : 'message.read_changed', eventData);
|
|
2447
|
+
}
|
|
2448
|
+
async _reconcileReadStates() {
|
|
2449
|
+
const identity = this._ensureReadStateIdentity();
|
|
2450
|
+
const keys = new Set();
|
|
2451
|
+
const errors = [];
|
|
2452
|
+
for (const entry of [...this._readStateQueries.values()]) {
|
|
2453
|
+
try {
|
|
2454
|
+
const result = await this.call(entry.method, { ...entry.params });
|
|
2455
|
+
if (identity !== this._ensureReadStateIdentity())
|
|
2456
|
+
return;
|
|
2457
|
+
this._observeReadStateResult(entry.method, result, entry.params);
|
|
2458
|
+
for (const state of this._readStateItems(result)) {
|
|
2459
|
+
const scope = String(state.scope ?? (entry.method.startsWith('group.') ? 'group' : 'p2p'));
|
|
2460
|
+
const groupId = scope === 'group' ? String(state.group_id ?? entry.params.group_id ?? '') : '';
|
|
2461
|
+
const topicKey = String(state.topic_key ?? entry.params.topic_key ?? '');
|
|
2462
|
+
const key = `${scope}\u0000${groupId}\u0000${topicKey}`;
|
|
2463
|
+
if (this._readStateCache.has(key))
|
|
2464
|
+
keys.add(key);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
catch (error) {
|
|
2468
|
+
errors.push({ method: entry.method, params: { ...entry.params }, error: error instanceof Error ? error.message : String(error) });
|
|
2469
|
+
this._clientLog.debug(`read state reconnect reconciliation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2470
|
+
}
|
|
2471
|
+
if (identity !== this._ensureReadStateIdentity())
|
|
2472
|
+
return;
|
|
2473
|
+
}
|
|
2474
|
+
this._dispatcher.enqueue('read_state_sync', {
|
|
2475
|
+
owner_aid: this._aid ?? '',
|
|
2476
|
+
states: [...keys].map((key) => ({ ...this._readStateCache.get(key).state })),
|
|
2477
|
+
errors,
|
|
2478
|
+
});
|
|
2479
|
+
}
|
|
1791
2480
|
async _callRawV2Rpc(method, params, pullGateKey = '') {
|
|
1792
2481
|
const p = { ...(params ?? {}) };
|
|
1793
2482
|
const forceForeground = Boolean(p._rpc_foreground);
|
|
@@ -1876,6 +2565,7 @@ export class AUNClient {
|
|
|
1876
2565
|
const previous = this._pendingP2pPullUpper.get(ns) ?? 0;
|
|
1877
2566
|
if (seq > previous) {
|
|
1878
2567
|
this._pendingP2pPullUpper.set(ns, seq);
|
|
2568
|
+
this._pendingP2pPullRetries.delete(ns);
|
|
1879
2569
|
}
|
|
1880
2570
|
this._clientLog.debug(`P2P pending pull upper recorded: ns=${ns}, seq=${seq}, previous=${previous}, contiguous=${this._seqTracker.getContiguousSeq(ns)}`);
|
|
1881
2571
|
}
|
|
@@ -1890,6 +2580,7 @@ export class AUNClient {
|
|
|
1890
2580
|
const contig = this._seqTracker.getContiguousSeq(ns);
|
|
1891
2581
|
if (upperSeq <= contig) {
|
|
1892
2582
|
this._pendingP2pPullUpper.delete(ns);
|
|
2583
|
+
this._pendingP2pPullRetries.delete(ns);
|
|
1893
2584
|
this._clientLog.debug(`P2P pending pull upper already covered: ns=${ns}, upper_seq=${upperSeq}, contiguous=${contig}, reason=${reason}`);
|
|
1894
2585
|
return false;
|
|
1895
2586
|
}
|
|
@@ -1897,18 +2588,63 @@ export class AUNClient {
|
|
|
1897
2588
|
this._clientLog.debug(`P2P pending pull postponed: ns=${ns}, upper_seq=${upperSeq}, contiguous=${contig}, state=${this._state}, closing=${this._closing}, reason=${reason}`);
|
|
1898
2589
|
return false;
|
|
1899
2590
|
}
|
|
1900
|
-
if (this.
|
|
2591
|
+
if (this._sessionOptions.background_sync === false)
|
|
2592
|
+
return false;
|
|
2593
|
+
if (this._rpcPipeline?.hasPullActivity?.(ns, false) || this._pendingP2pPullTimers.has(ns))
|
|
2594
|
+
return false;
|
|
2595
|
+
const retries = (this._pendingP2pPullRetries.get(ns) ?? 0) + 1;
|
|
2596
|
+
if (retries > PENDING_P2P_PULL_MAX_RETRIES) {
|
|
2597
|
+
this._pendingP2pPullUpper.delete(ns);
|
|
2598
|
+
this._pendingP2pPullRetries.delete(ns);
|
|
2599
|
+
this._clientLog.warn(`P2P pending pull retries exhausted: ns=${ns}, upper_seq=${upperSeq}, contiguous=${contig}, reason=${reason}`);
|
|
1901
2600
|
return false;
|
|
1902
|
-
|
|
1903
|
-
this.
|
|
1904
|
-
this.
|
|
2601
|
+
}
|
|
2602
|
+
this._pendingP2pPullRetries.set(ns, retries);
|
|
2603
|
+
this._clientLog.info(`P2P pending push follow-up pull scheduled: ns=${ns}, upper_seq=${upperSeq}, contiguous=${contig}, retry=${retries}, reason=${reason}`);
|
|
2604
|
+
const timer = setTimeout(() => {
|
|
2605
|
+
this._pendingP2pPullTimers.delete(ns);
|
|
2606
|
+
const currentUpper = this._pendingP2pPullUpper.get(ns) ?? 0;
|
|
2607
|
+
if (currentUpper <= 0 || currentUpper <= this._seqTracker.getContiguousSeq(ns)) {
|
|
2608
|
+
this._pendingP2pPullUpper.delete(ns);
|
|
2609
|
+
this._pendingP2pPullRetries.delete(ns);
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2612
|
+
// 暂时移除上界,避免 synthetic push 被 hasPullActivity 当成自身的 pending pull 拦截。
|
|
2613
|
+
this._pendingP2pPullUpper.delete(ns);
|
|
2614
|
+
const retry = this._onV2PushNotification({ seq: currentUpper }, 'v2')
|
|
2615
|
+
.then(() => {
|
|
2616
|
+
if (this._seqTracker.getContiguousSeq(ns) < currentUpper) {
|
|
2617
|
+
const pending = this._pendingP2pPullUpper.get(ns) ?? 0;
|
|
2618
|
+
if (currentUpper > pending)
|
|
2619
|
+
this._pendingP2pPullUpper.set(ns, currentUpper);
|
|
2620
|
+
}
|
|
2621
|
+
})
|
|
2622
|
+
.catch((error) => {
|
|
2623
|
+
const pending = this._pendingP2pPullUpper.get(ns) ?? 0;
|
|
2624
|
+
if (currentUpper > pending)
|
|
2625
|
+
this._pendingP2pPullUpper.set(ns, currentUpper);
|
|
2626
|
+
throw error;
|
|
2627
|
+
})
|
|
2628
|
+
.finally(() => {
|
|
2629
|
+
this._schedulePendingP2pPullIfNeeded(ns, 'pending-pull-retry-complete');
|
|
2630
|
+
});
|
|
2631
|
+
this._safeAsync(retry);
|
|
2632
|
+
}, PENDING_P2P_PULL_DELAY_MS);
|
|
2633
|
+
this._pendingP2pPullTimers.set(ns, timer);
|
|
1905
2634
|
return true;
|
|
1906
2635
|
}
|
|
1907
2636
|
_markPublishedSeq(ns, seq) {
|
|
1908
2637
|
this._delivery.markPublishedSeq(ns, seq);
|
|
1909
2638
|
}
|
|
1910
|
-
_publishAppEvent(event, payload, source = 'direct') {
|
|
1911
|
-
|
|
2639
|
+
_publishAppEvent(event, payload, source = 'direct', deliveryContext) {
|
|
2640
|
+
if (event === 'message.received') {
|
|
2641
|
+
return this._handleOwnershipMessage(payload).then((handled) => {
|
|
2642
|
+
if (!handled)
|
|
2643
|
+
return this._delivery.publishAppEvent(event, payload, source, deliveryContext);
|
|
2644
|
+
return undefined;
|
|
2645
|
+
});
|
|
2646
|
+
}
|
|
2647
|
+
return this._delivery.publishAppEvent(event, payload, source, deliveryContext);
|
|
1912
2648
|
}
|
|
1913
2649
|
_echoTimestamp() {
|
|
1914
2650
|
const now = new Date();
|
|
@@ -2087,8 +2823,8 @@ export class AUNClient {
|
|
|
2087
2823
|
}
|
|
2088
2824
|
return true;
|
|
2089
2825
|
}
|
|
2090
|
-
async _drainOrderedMessages(ns, beforeSeq, pullResponse = false, persist = true, source = 'push') {
|
|
2091
|
-
return this._delivery.drainOrderedMessages(ns, beforeSeq, pullResponse, persist, source);
|
|
2826
|
+
async _drainOrderedMessages(ns, beforeSeq, pullResponse = false, persist = true, source = 'push', throughSeq) {
|
|
2827
|
+
return this._delivery.drainOrderedMessages(ns, beforeSeq, pullResponse, persist, source, undefined, throughSeq);
|
|
2092
2828
|
}
|
|
2093
2829
|
async _publishOrderedMessage(event, ns, seq, payload, source = 'push') {
|
|
2094
2830
|
return this._delivery.publishOrderedMessage(event, ns, seq, payload, source);
|
|
@@ -2213,6 +2949,10 @@ export class AUNClient {
|
|
|
2213
2949
|
}
|
|
2214
2950
|
const paramsHash = String(cs.params_hash ?? '');
|
|
2215
2951
|
const timestamp = String(cs.timestamp ?? '');
|
|
2952
|
+
const signedAt = Number(timestamp) * 1000;
|
|
2953
|
+
if (!/^\d+$/.test(timestamp) || !Number.isSafeInteger(Number(timestamp)) || signedAt < new Date(certObj.validFrom).getTime() || signedAt > new Date(certObj.validTo).getTime()) {
|
|
2954
|
+
return false;
|
|
2955
|
+
}
|
|
2216
2956
|
const signData = Buffer.from(`${method}|${sigAid}|${timestamp}|${paramsHash}`, 'utf-8');
|
|
2217
2957
|
const sigB64 = String(cs.signature ?? '');
|
|
2218
2958
|
const pubKey = certObj.publicKey;
|
|
@@ -2273,7 +3013,10 @@ export class AUNClient {
|
|
|
2273
3013
|
}
|
|
2274
3014
|
}
|
|
2275
3015
|
try {
|
|
2276
|
-
|
|
3016
|
+
if (opts.signedAt === undefined)
|
|
3017
|
+
await this._auth.verifyPeerCertificate(peerGatewayUrl, certPem, aid);
|
|
3018
|
+
else
|
|
3019
|
+
await this._auth.verifyPeerCertificate(peerGatewayUrl, certPem, aid, opts.signedAt);
|
|
2277
3020
|
}
|
|
2278
3021
|
catch (exc) {
|
|
2279
3022
|
if (cachedBootstrapChain) {
|
|
@@ -2287,9 +3030,9 @@ export class AUNClient {
|
|
|
2287
3030
|
validatedAt: nowSec,
|
|
2288
3031
|
refreshAfter: nowSec + PEER_CERT_CACHE_TTL,
|
|
2289
3032
|
};
|
|
2290
|
-
const cacheKey = AUNClient._certCacheKey(aid, certFingerprint);
|
|
3033
|
+
const cacheKey = AUNClient._certCacheKey(aid, certFingerprint) + (opts.signedAt === undefined ? '' : `:signed:${opts.signedAt}`);
|
|
2291
3034
|
this._certCache.set(cacheKey, entry);
|
|
2292
|
-
if (!certFingerprint) {
|
|
3035
|
+
if (!certFingerprint && opts.signedAt === undefined) {
|
|
2293
3036
|
const bareKey = AUNClient._certCacheKey(aid);
|
|
2294
3037
|
this._certCache.set(bareKey, entry);
|
|
2295
3038
|
const actualFp = `sha256:${x509Cert.fingerprint256.replace(/:/g, '').toLowerCase()}`;
|
|
@@ -2304,12 +3047,19 @@ export class AUNClient {
|
|
|
2304
3047
|
}
|
|
2305
3048
|
return certPem;
|
|
2306
3049
|
}
|
|
3050
|
+
async _fetchServiceEventCertificate(aid, fingerprint) {
|
|
3051
|
+
const certificate = await this._fetchPeerCert(aid, fingerprint);
|
|
3052
|
+
if (!this._gatewayUrl)
|
|
3053
|
+
throw new ValidationError('gateway url unavailable for service event certificate validation');
|
|
3054
|
+
await this._auth.verifyPeerCertificate(AUNClient._resolvePeerGatewayUrl(this._gatewayUrl, aid), certificate, aid, undefined, true);
|
|
3055
|
+
return certificate;
|
|
3056
|
+
}
|
|
2307
3057
|
/** 获取对方证书(带缓存 + 完整 PKI 验证),跨域时自动路由到 peer 所在域。 */
|
|
2308
|
-
async _fetchPeerCert(aid, certFingerprint, timeoutMs = 30_000) {
|
|
3058
|
+
async _fetchPeerCert(aid, certFingerprint, timeoutMs = 30_000, signedAt) {
|
|
2309
3059
|
const tStart = Date.now();
|
|
2310
3060
|
this._clientLog.debug(`_fetchPeerCert enter: aid=${aid}, fp=${certFingerprint ?? ''}`);
|
|
2311
3061
|
try {
|
|
2312
|
-
const cacheKey = AUNClient._certCacheKey(aid, certFingerprint);
|
|
3062
|
+
const cacheKey = AUNClient._certCacheKey(aid, certFingerprint) + (signedAt === undefined ? '' : `:signed:${signedAt}`);
|
|
2313
3063
|
const cached = this._certCache.get(cacheKey);
|
|
2314
3064
|
const now = Date.now() / 1000;
|
|
2315
3065
|
if (cached && now < cached.refreshAfter) {
|
|
@@ -2327,6 +3077,7 @@ export class AUNClient {
|
|
|
2327
3077
|
certPem,
|
|
2328
3078
|
certFingerprint,
|
|
2329
3079
|
source: 'fetch',
|
|
3080
|
+
signedAt,
|
|
2330
3081
|
});
|
|
2331
3082
|
this._clientLog.debug(`_fetchPeerCert exit: elapsed=${Date.now() - tStart}ms aid=${aid} (fetched)`);
|
|
2332
3083
|
return validated;
|
|
@@ -2441,6 +3192,10 @@ export class AUNClient {
|
|
|
2441
3192
|
this._pushedSeqs.clear();
|
|
2442
3193
|
this._pendingOrderedMsgs.clear();
|
|
2443
3194
|
this._pendingP2pPullUpper.clear();
|
|
3195
|
+
for (const timer of this._pendingP2pPullTimers.values())
|
|
3196
|
+
clearTimeout(timer);
|
|
3197
|
+
this._pendingP2pPullTimers.clear();
|
|
3198
|
+
this._pendingP2pPullRetries.clear();
|
|
2444
3199
|
this._v2SenderIKPending.clear();
|
|
2445
3200
|
this._v2SenderIKFetching.clear();
|
|
2446
3201
|
this._groupSynced.clear();
|
|
@@ -2457,6 +3212,10 @@ export class AUNClient {
|
|
|
2457
3212
|
this._pushedSeqs.clear();
|
|
2458
3213
|
this._pendingOrderedMsgs.clear();
|
|
2459
3214
|
this._pendingP2pPullUpper.clear();
|
|
3215
|
+
for (const timer of this._pendingP2pPullTimers.values())
|
|
3216
|
+
clearTimeout(timer);
|
|
3217
|
+
this._pendingP2pPullTimers.clear();
|
|
3218
|
+
this._pendingP2pPullRetries.clear();
|
|
2460
3219
|
this._v2SenderIKPending.clear();
|
|
2461
3220
|
this._v2SenderIKFetching.clear();
|
|
2462
3221
|
this._groupSynced.clear();
|
|
@@ -2851,6 +3610,7 @@ export class AUNClient {
|
|
|
2851
3610
|
}
|
|
2852
3611
|
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
2853
3612
|
this._startBackgroundTasks();
|
|
3613
|
+
this._safeAsync(this._reconcileReadStates());
|
|
2854
3614
|
const connectionKind = String(params.connection_kind ?? 'long');
|
|
2855
3615
|
const isShortConnection = connectionKind === 'short';
|
|
2856
3616
|
const hasExplicitBackgroundSync = Object.prototype.hasOwnProperty.call(params, 'background_sync');
|
|
@@ -2877,7 +3637,7 @@ export class AUNClient {
|
|
|
2877
3637
|
this._clientLog.warn(`schedule post-connect P2P gap fill failed: ${formatCaughtError(exc)}`);
|
|
2878
3638
|
});
|
|
2879
3639
|
}
|
|
2880
|
-
this._safeAsync(this.
|
|
3640
|
+
this._safeAsync(this._resumeOwnershipAfterConnect());
|
|
2881
3641
|
this._clientLog.debug(`_connectOnce exit: elapsed=${Date.now() - tStart}ms gateway=${gatewayUrl}, aid=${this._aid ?? ''}`);
|
|
2882
3642
|
}
|
|
2883
3643
|
catch (err) {
|
|
@@ -3276,10 +4036,15 @@ export class AUNClient {
|
|
|
3276
4036
|
encrypted: true,
|
|
3277
4037
|
e2ee,
|
|
3278
4038
|
};
|
|
4039
|
+
if (typeof plaintext.type === 'string' && plaintext.type.startsWith('aun.ownership.'))
|
|
4040
|
+
result.from_aid = fromAid;
|
|
3279
4041
|
if (senderDeviceId)
|
|
3280
4042
|
result.sender_device_id = senderDeviceId;
|
|
3281
4043
|
const explicitDirection = String(msg.direction ?? '').trim();
|
|
3282
|
-
|
|
4044
|
+
if (explicitDirection)
|
|
4045
|
+
result.direction = explicitDirection;
|
|
4046
|
+
else if (fromAid && fromAid !== this._aid)
|
|
4047
|
+
result.direction = 'inbound';
|
|
3283
4048
|
if (msg.device_id !== undefined)
|
|
3284
4049
|
result.device_id = msg.device_id;
|
|
3285
4050
|
attachGatewayProximity(result, msg);
|
|
@@ -3478,14 +4243,30 @@ export class AUNClient {
|
|
|
3478
4243
|
return this._delivery.onRawGroupV2MessageCreated(data, expectedVersion, inlineGeneration);
|
|
3479
4244
|
}
|
|
3480
4245
|
/** Push 通知带 payload 时的就地解密(复用 _decryptV2Message) */
|
|
3481
|
-
async _decryptV2PushMessage(data) {
|
|
3482
|
-
return await this._v2E2EE.decryptV2PushMessage(data);
|
|
4246
|
+
async _decryptV2PushMessage(data, deferStatus) {
|
|
4247
|
+
return await this._v2E2EE.decryptV2PushMessage(data, deferStatus);
|
|
3483
4248
|
}
|
|
3484
4249
|
/** 按当前 AID 发现 Gateway;用于 authenticate()/connect() 的新入口。 */
|
|
3485
4250
|
async _resolveGatewayForAid(aid) {
|
|
3486
4251
|
const gateways = await this._resolveGatewayCandidatesForAid(aid);
|
|
3487
4252
|
return gateways[0];
|
|
3488
4253
|
}
|
|
4254
|
+
async _resolveOwnershipCertificateGateway(aid) {
|
|
4255
|
+
const issuer = aid.slice(aid.indexOf('.') + 1);
|
|
4256
|
+
if (issuer === this._aid?.slice(this._aid.indexOf('.') + 1))
|
|
4257
|
+
return this._resolveGatewayForAid(aid);
|
|
4258
|
+
const port = this._configModel.discoveryPort ? `:${this._configModel.discoveryPort}` : '';
|
|
4259
|
+
let lastError;
|
|
4260
|
+
for (const host of [aid, `gateway.${issuer}`]) {
|
|
4261
|
+
try {
|
|
4262
|
+
return await this._discovery.discover(`https://${host}${port}/.well-known/aun-gateway`);
|
|
4263
|
+
}
|
|
4264
|
+
catch (error) {
|
|
4265
|
+
lastError = error;
|
|
4266
|
+
}
|
|
4267
|
+
}
|
|
4268
|
+
throw ownershipFailure('unavailable', lastError instanceof Error ? lastError.message : 'signer Gateway discovery failed', true);
|
|
4269
|
+
}
|
|
3489
4270
|
/** 发现并缓存全部 Gateway 候选,供首连和自动重连故障转移使用。 */
|
|
3490
4271
|
async _resolveGatewayCandidatesForAid(aid) {
|
|
3491
4272
|
const resolvedAid = String(aid ?? this._aid ?? '').trim();
|