@agentunion/fastaun 0.5.18 → 0.5.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -2
- package/_packed_docs/CHANGELOG.md +44 -2
- package/_packed_docs/INDEX.md +136 -14
- package/_packed_docs/KITE_DOCS_GUIDE.md +42 -16
- package/_packed_docs/agent.md/SCHEMA.md +245 -205
- package/_packed_docs/aun/345/210/206/345/270/203/345/274/217/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +42 -15
- package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +4 -4
- package/_packed_docs/protocol/01-/350/272/253/344/273/275/344/270/216/345/207/255/350/257/201/345/215/217/350/256/256-auth.md +45 -134
- package/_packed_docs/protocol/07-/351/224/231/350/257/257/347/240/201/344/270/216/347/212/266/346/200/201/346/234/272.md +5 -20
- 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 +22 -27
- 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 +250 -283
- 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 +74 -64
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +25 -5
- package/_packed_docs/sdk/INDEX.md +76 -17
- package/dist/agent-md-schema.d.ts +2 -0
- package/dist/agent-md-schema.js +45 -10
- package/dist/agent-md-schema.js.map +1 -1
- package/dist/agent-md.d.ts +47 -2
- package/dist/agent-md.js +493 -33
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +9 -59
- package/dist/aid-store.js +135 -245
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.js +19 -5
- 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 +2 -1
- package/dist/client/delivery.js +143 -52
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/rpc-pipeline.js +22 -5
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +1 -0
- package/dist/client/v2-e2ee.js +93 -29
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +84 -1
- package/dist/client.js +749 -28
- 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 +83 -3
- package/dist/facades.js +98 -4
- package/dist/facades.js.map +1 -1
- package/dist/index.d.ts +8 -4
- package/dist/index.js +3 -2
- 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 +82 -5
- package/dist/ownership-proof.js +327 -31
- package/dist/ownership-proof.js.map +1 -1
- 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 +1 -1
package/dist/client.js
CHANGED
|
@@ -26,7 +26,7 @@ import { normalizeGroupId } from './group-id.js';
|
|
|
26
26
|
import { validateAIDFormat, validateGroupIDFormat } from './validators.js';
|
|
27
27
|
import { RPCTransport } from './transport.js';
|
|
28
28
|
import { AuthFlow } from './auth.js';
|
|
29
|
-
import { AgentMdManager, buildDefaultAgentMd } from './agent-md.js';
|
|
29
|
+
import { AgentMdManager, buildDefaultAgentMd, updateOwnershipAgentMd } from './agent-md.js';
|
|
30
30
|
import { GroupIndexMetaCache } from './group-index.js';
|
|
31
31
|
import { SeqTracker } from './seq-tracker.js';
|
|
32
32
|
import { ClientRuntime } from './client/runtime.js';
|
|
@@ -37,15 +37,20 @@ import { LifecycleController } from './client/lifecycle.js';
|
|
|
37
37
|
import { PeerDirectory } from './client/peers.js';
|
|
38
38
|
import { RpcPipeline } from './client/rpc-pipeline.js';
|
|
39
39
|
import { CollabClient } from './collab/client.js';
|
|
40
|
-
import { GroupFacade, MailFacade, MessageFacade, StreamFacade } from './facades.js';
|
|
40
|
+
import { CredentialFacade, OrgFacade, GroupFacade, MailFacade, MessageFacade, StreamFacade } from './facades.js';
|
|
41
41
|
import { StorageVFS } from './storage/vfs.js';
|
|
42
42
|
import { V2E2EECoordinator } from './client/v2-e2ee.js';
|
|
43
43
|
import { GroupStateCoordinator } from './client/group-state.js';
|
|
44
44
|
import { decryptMessage, } from './v2/e2ee/index.js';
|
|
45
45
|
import { ecdsaVerifyRaw } from './v2/crypto/ecdsa.js';
|
|
46
|
+
import { decryptedOwnershipJsonText } from './v2/e2ee/decrypt.js';
|
|
46
47
|
import { isJsonObject, ConnectionState, STATE_TO_PUBLIC, } from './types.js';
|
|
47
48
|
import { AID } from './aid.js';
|
|
48
|
-
import { certMatchesFingerprint, normalizeFingerprintHex, publicKeyMatchesFingerprint } from './cert-utils.js';
|
|
49
|
+
import { certFingerprint, certMatchesFingerprint, normalizeFingerprintHex, publicKeyMatchesFingerprint } from './cert-utils.js';
|
|
50
|
+
import { validateAgentMdDocument } from './agent-md-schema.js';
|
|
51
|
+
import { OwnershipFiles, OwnershipStorageAdapter, ownershipFailure, signOwnershipFile } from './ownership-v21.js';
|
|
52
|
+
import { OwnershipWorkflow } from './ownership-workflow.js';
|
|
53
|
+
import { canonicalJson, createCredential, createMemberCredential, signMembershipBinding, verifyServiceEvent } from './org-protocol.js';
|
|
49
54
|
function isPromiseLike(value) {
|
|
50
55
|
return Boolean(value && typeof value.then === 'function');
|
|
51
56
|
}
|
|
@@ -342,6 +347,9 @@ function createAgentMdManagerForRuntime(opts) {
|
|
|
342
347
|
logger: opts.logger(),
|
|
343
348
|
ownerAidGetter: opts.ownerAid,
|
|
344
349
|
currentAidGetter: opts.currentAid,
|
|
350
|
+
entityOwnerResolver: opts.resolveEntityOwner,
|
|
351
|
+
entityIdentityGetter: opts.fetchEntityIdentity,
|
|
352
|
+
entitySignerResolver: opts.fetchEntitySigner,
|
|
345
353
|
gatewayResolver: async (aid) => {
|
|
346
354
|
const gatewayUrl = await opts.gateway.resolve(aid);
|
|
347
355
|
opts.gateway.set(gatewayUrl);
|
|
@@ -388,7 +396,47 @@ function createAgentMdManagerForRuntime(opts) {
|
|
|
388
396
|
opts.identity.set(identity);
|
|
389
397
|
return token;
|
|
390
398
|
},
|
|
391
|
-
|
|
399
|
+
uploadAccessTokenResolver: async (_aid, gatewayUrl) => {
|
|
400
|
+
const localAid = String(opts.ownerAid() ?? '').trim();
|
|
401
|
+
let identity = opts.identity.get();
|
|
402
|
+
if (!identity || String(identity.aid ?? '') !== localAid) {
|
|
403
|
+
throw new StateError('no local identity found, register or load an AID first');
|
|
404
|
+
}
|
|
405
|
+
const auth = opts.auth();
|
|
406
|
+
const cachedToken = String(identity.access_token ?? '');
|
|
407
|
+
const expiresAt = auth.getAccessTokenExpiry(identity);
|
|
408
|
+
if (cachedToken && (expiresAt === null || expiresAt > Date.now() / 1000 + 30))
|
|
409
|
+
return cachedToken;
|
|
410
|
+
if (identity.refresh_token) {
|
|
411
|
+
try {
|
|
412
|
+
const refreshed = await auth.refreshCachedTokens(gatewayUrl, identity);
|
|
413
|
+
const refreshedToken = String(refreshed.access_token ?? '');
|
|
414
|
+
const refreshedExpiry = auth.getAccessTokenExpiry(refreshed);
|
|
415
|
+
if (refreshedToken && (refreshedExpiry === null || refreshedExpiry > Date.now() / 1000 + 30)) {
|
|
416
|
+
opts.identity.set(refreshed);
|
|
417
|
+
return refreshedToken;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
// refresh 失败时回退到完整 authenticate。
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const result = await auth.authenticate(gatewayUrl, { aid: localAid });
|
|
425
|
+
const token = String(result.access_token ?? '');
|
|
426
|
+
if (!token)
|
|
427
|
+
throw new StateError('authenticate did not return access_token');
|
|
428
|
+
identity = auth.loadIdentityOrNone(localAid) ?? {
|
|
429
|
+
...identity,
|
|
430
|
+
access_token: token,
|
|
431
|
+
refresh_token: String(result.refresh_token ?? identity.refresh_token ?? ''),
|
|
432
|
+
access_token_expires_at: typeof result.expires_at === 'number' ? result.expires_at : identity.access_token_expires_at,
|
|
433
|
+
token_exp: typeof result.expires_at === 'number' ? result.expires_at : identity.token_exp,
|
|
434
|
+
expires_at: typeof result.expires_at === 'number' ? result.expires_at : identity.expires_at,
|
|
435
|
+
};
|
|
436
|
+
opts.identity.set(identity);
|
|
437
|
+
return token;
|
|
438
|
+
},
|
|
439
|
+
peerResolver: async (aid, certFingerprint, signedAt) => {
|
|
392
440
|
const target = String(aid ?? '').trim();
|
|
393
441
|
const expectedFp = String(certFingerprint ?? '').trim().toLowerCase();
|
|
394
442
|
const current = opts.currentAid();
|
|
@@ -402,7 +450,7 @@ function createAgentMdManagerForRuntime(opts) {
|
|
|
402
450
|
}
|
|
403
451
|
catch { /* best effort before cert fetch */ }
|
|
404
452
|
}
|
|
405
|
-
const certPem = String(await opts.fetchPeerCert(target, expectedFp || undefined) ?? '').trim();
|
|
453
|
+
const certPem = String(await opts.fetchPeerCert(target, expectedFp || undefined, signedAt) ?? '').trim();
|
|
406
454
|
if (!certPem)
|
|
407
455
|
throw new NotFoundError(`certificate not found for aid: ${target}`);
|
|
408
456
|
return AID._create({
|
|
@@ -429,6 +477,90 @@ export class AUNClient {
|
|
|
429
477
|
_state = 'no_identity';
|
|
430
478
|
/** 当前 AID 值对象(新 API) */
|
|
431
479
|
_currentAid = null;
|
|
480
|
+
_serviceEventIdentityVersion = 0;
|
|
481
|
+
/** ownership 关系只在 SDK 内存中编排;服务端不保存状态机。 */
|
|
482
|
+
_ownershipRecords = new Map();
|
|
483
|
+
_ownershipType = 'agent';
|
|
484
|
+
_ownershipStorage = null;
|
|
485
|
+
_ownershipFiles = null;
|
|
486
|
+
_ownershipWorkflow = null;
|
|
487
|
+
_getOwnershipStorage() {
|
|
488
|
+
return this._ownershipStorage ??= new OwnershipStorageAdapter({ currentAid: () => this._aid, call: (method, params) => this.call(method, params) });
|
|
489
|
+
}
|
|
490
|
+
_getOwnershipFiles() {
|
|
491
|
+
return this._ownershipFiles ??= new OwnershipFiles({
|
|
492
|
+
currentAid: () => { if (!this._currentAid)
|
|
493
|
+
throw ownershipFailure('forbidden', 'ownership requires a loaded identity'); return this._currentAid; },
|
|
494
|
+
currentType: () => this.getType(),
|
|
495
|
+
now: () => Math.trunc(Date.now() / 1000),
|
|
496
|
+
readAgentMdFresh: (subAid) => this._agentMdManager.readOwnershipFresh(subAid),
|
|
497
|
+
submitAgentMdRaw: (subAid, raw, condition) => this._agentMdManager.submitOwnershipRaw(subAid, raw, condition),
|
|
498
|
+
readOwnerMd: (ownerAid, subAid) => this._getOwnershipStorage().readOwnerMd(ownerAid, subAid),
|
|
499
|
+
writeOwnerMd: (ownerAid, subAid, raw, revision) => this._getOwnershipStorage().writeOwnerMd(ownerAid, subAid, raw, revision),
|
|
500
|
+
deleteOwnerMd: (ownerAid, subAid, revision) => this._getOwnershipStorage().deleteOwnerMd(ownerAid, subAid, revision),
|
|
501
|
+
resolveCertificate: async (aid, fingerprint, purpose) => this._auth.resolveOwnershipCertificate(await this._resolveOwnershipCertificateGateway(aid), aid, fingerprint, purpose),
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
_getOwnershipWorkflow() {
|
|
505
|
+
return this._ownershipWorkflow ??= new OwnershipWorkflow({
|
|
506
|
+
currentAid: () => this._aid, store: this._tokenStore, files: this._getOwnershipFiles(),
|
|
507
|
+
currentType: () => this.getType(),
|
|
508
|
+
send: (to, payload) => this.call('message.send', { to, encrypt: true, payload: payload }),
|
|
509
|
+
emit: (event, payload) => this._dispatcher.enqueue(event, payload),
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
_persistOwnershipRecords() {
|
|
513
|
+
const aid = String(this._aid ?? '').trim();
|
|
514
|
+
if (!aid)
|
|
515
|
+
return;
|
|
516
|
+
const metadata = this._tokenStore.loadMetadata?.(aid) ?? {};
|
|
517
|
+
const existing = metadata.ownership_records;
|
|
518
|
+
const records = existing && typeof existing === 'object' && !Array.isArray(existing)
|
|
519
|
+
? { ...existing } : {};
|
|
520
|
+
for (const [key, record] of this._ownershipRecords) {
|
|
521
|
+
if (![record.subject_aid, record.owner_aid].some((value) => String(value ?? '').trim().toLowerCase() === aid.toLowerCase()))
|
|
522
|
+
continue;
|
|
523
|
+
records[key] = Object.fromEntries(Object.entries(record).filter(([name]) => !['subject_identity', 'escrow_password', 'escrow_download_url'].includes(name)));
|
|
524
|
+
}
|
|
525
|
+
this._tokenStore.saveMetadata?.(aid, { ownership_records: records });
|
|
526
|
+
}
|
|
527
|
+
_restoreOwnershipRecords() {
|
|
528
|
+
const aid = String(this._aid ?? '').trim();
|
|
529
|
+
if (!aid)
|
|
530
|
+
return;
|
|
531
|
+
const metadata = this._tokenStore.loadMetadata?.(aid);
|
|
532
|
+
const intent = metadata?.registration_intent;
|
|
533
|
+
if (intent?.type)
|
|
534
|
+
this._ownershipType = String(intent.type);
|
|
535
|
+
const records = metadata?.ownership_records;
|
|
536
|
+
if (!records || typeof records !== 'object' || Array.isArray(records))
|
|
537
|
+
return;
|
|
538
|
+
for (const [key, value] of Object.entries(records)) {
|
|
539
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
540
|
+
continue;
|
|
541
|
+
const record = value;
|
|
542
|
+
this._ownershipRecords.set(key, record);
|
|
543
|
+
if (String(record.subject_aid ?? '').toLowerCase() === aid.toLowerCase())
|
|
544
|
+
this._ownershipType = String(record.type ?? this._ownershipType);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
async _resumeOwnershipAfterConnect() {
|
|
548
|
+
await this._checkIdentityAdmissionAfterConnect();
|
|
549
|
+
await this._resumeRegistrationIntent();
|
|
550
|
+
await this._resumeOwnership();
|
|
551
|
+
}
|
|
552
|
+
async _resumeRegistrationIntent() {
|
|
553
|
+
const intent = this._tokenStore.loadMetadata?.(this._aid)?.registration_intent;
|
|
554
|
+
if (['group', 'org'].includes(String(intent?.type ?? this.getType())))
|
|
555
|
+
return;
|
|
556
|
+
if (intent?.protocol !== '2.1' || intent.pending !== true)
|
|
557
|
+
return;
|
|
558
|
+
await this._getOwnershipWorkflow().resumeRegistration(String(intent.owner), String(intent.operation_id));
|
|
559
|
+
this._tokenStore.saveMetadata?.(this._aid, { registration_intent: { ...intent, pending: false } });
|
|
560
|
+
}
|
|
561
|
+
async _resumeOwnership() {
|
|
562
|
+
await this._getOwnershipWorkflow().resume();
|
|
563
|
+
}
|
|
432
564
|
/** 实例级 protected_headers */
|
|
433
565
|
_instanceProtectedHeaders = null;
|
|
434
566
|
/** 重连退避时间戳(ms) */
|
|
@@ -451,6 +583,7 @@ export class AUNClient {
|
|
|
451
583
|
_transport;
|
|
452
584
|
/** 认证流程 */
|
|
453
585
|
_auth;
|
|
586
|
+
resolveCredentialService;
|
|
454
587
|
/** 密钥存储 */
|
|
455
588
|
_tokenStore;
|
|
456
589
|
/** 会话参数(重连用) */
|
|
@@ -503,6 +636,10 @@ export class AUNClient {
|
|
|
503
636
|
_v2SenderIKPending = new Map();
|
|
504
637
|
/** sender IK 后台补齐任务去重。 */
|
|
505
638
|
_v2SenderIKFetching = new Set();
|
|
639
|
+
_readStateCache = new Map();
|
|
640
|
+
_readStateQueries = new Map();
|
|
641
|
+
_readStateIdentity = null;
|
|
642
|
+
_readStateGeneration = 0;
|
|
506
643
|
// ── 后台任务定时器 ──────────────────────────────────────────
|
|
507
644
|
_heartbeatTimer = null;
|
|
508
645
|
_tokenRefreshTimer = null;
|
|
@@ -551,6 +688,8 @@ export class AUNClient {
|
|
|
551
688
|
_mail;
|
|
552
689
|
_group;
|
|
553
690
|
_stream;
|
|
691
|
+
_credential;
|
|
692
|
+
_org;
|
|
554
693
|
constructor(aid) {
|
|
555
694
|
if (aid !== null && aid !== undefined && !isAIDObject(aid)) {
|
|
556
695
|
throw new ValidationError('AUNClient only accepts an AID object or no argument');
|
|
@@ -586,7 +725,7 @@ export class AUNClient {
|
|
|
586
725
|
if (debugFlag) {
|
|
587
726
|
this._clientLog.info(`AUNClient initialized (debug=true, aunPath=${this._configModel.aunPath})`);
|
|
588
727
|
}
|
|
589
|
-
this._dispatcher = new EventDispatcher(this._logger.for('aun_core.events'));
|
|
728
|
+
this._dispatcher = new EventDispatcher(this._logger.for('aun_core.events'), () => this._identity?.aid ?? '');
|
|
590
729
|
const dnsNet = new DnsResilientNet({
|
|
591
730
|
verifySsl: this._configModel.verifySsl,
|
|
592
731
|
logger: this._clientLog,
|
|
@@ -596,6 +735,9 @@ export class AUNClient {
|
|
|
596
735
|
logger: this._logger.for('aun_core.keystore'),
|
|
597
736
|
});
|
|
598
737
|
this._tokenStore = tokenStore;
|
|
738
|
+
this._ownershipStorage = null;
|
|
739
|
+
this._ownershipFiles = null;
|
|
740
|
+
this._ownershipWorkflow = null;
|
|
599
741
|
this._slotId = inputAid?.slotId || 'default';
|
|
600
742
|
this._connectDeliveryMode = normalizeDeliveryModeConfig({ mode: 'fanout' });
|
|
601
743
|
this._defaultConnectDeliveryMode = { ...this._connectDeliveryMode };
|
|
@@ -611,6 +753,7 @@ export class AUNClient {
|
|
|
611
753
|
net: dnsNet,
|
|
612
754
|
});
|
|
613
755
|
this._aid = initAid;
|
|
756
|
+
this._restoreOwnershipRecords();
|
|
614
757
|
this._agentMdManager = createAgentMdManagerForRuntime({
|
|
615
758
|
config: () => this._configModel,
|
|
616
759
|
logger: () => this._logger.for('aun_core.agent_md'),
|
|
@@ -628,7 +771,10 @@ export class AUNClient {
|
|
|
628
771
|
},
|
|
629
772
|
auth: () => this._auth,
|
|
630
773
|
tokenStore: () => this._tokenStore,
|
|
631
|
-
fetchPeerCert: (target, certFingerprint) => this._fetchPeerCert(target, certFingerprint || undefined),
|
|
774
|
+
fetchPeerCert: (target, certFingerprint, signedAt) => this._fetchPeerCert(target, certFingerprint || undefined, undefined, signedAt),
|
|
775
|
+
resolveEntityOwner: (target, type) => this._resolveEntityAgentMdOwner(target, type),
|
|
776
|
+
fetchEntityIdentity: (target, type) => this._freshEntityCertificate(target, type),
|
|
777
|
+
fetchEntitySigner: (target, fingerprint) => this._freshEntitySigner(target, fingerprint),
|
|
632
778
|
});
|
|
633
779
|
this._transport = new RPCTransport({
|
|
634
780
|
eventDispatcher: this._dispatcher,
|
|
@@ -638,7 +784,9 @@ export class AUNClient {
|
|
|
638
784
|
dnsNet,
|
|
639
785
|
});
|
|
640
786
|
this._transport.setProtocolMetaObserver((meta) => this._observeRpcMeta(meta));
|
|
787
|
+
this._transport.setServiceEventVerifier((event, payload) => verifyServiceEvent(event, payload, { targetAid: this._identity?.aid ?? '', resolveEventCertificate: (aid, fingerprint) => this._fetchServiceEventCertificate(aid, fingerprint), resolveCredentialService: this.resolveCredentialService }), () => this._identity?.aid ?? '', () => this._serviceEventIdentityVersion);
|
|
641
788
|
this._transport.setProtocolDisconnectCallback((err, closeCode) => this._handleTransportDisconnect(err, closeCode));
|
|
789
|
+
this._transport._onForegroundDrained = () => this._delivery.onForegroundPullsDrained();
|
|
642
790
|
this._runtime = new ClientRuntime(this);
|
|
643
791
|
this._identityRuntime = new IdentityRuntimeManager(this._runtime);
|
|
644
792
|
this._peerDirectory = new PeerDirectory(this._runtime);
|
|
@@ -665,6 +813,7 @@ export class AUNClient {
|
|
|
665
813
|
this._dispatcher.subscribeProtocol('_raw.message.received', (data) => this._onRawMessageReceived(data));
|
|
666
814
|
// P2P 消息撤回推送:在线 push 与 pull tombstone 去重后再发布给应用层
|
|
667
815
|
this._dispatcher.subscribeProtocol('_raw.message.recalled', (data) => this._safeAsync(this._onRawMessageRecalled(data)));
|
|
816
|
+
this._dispatcher.subscribeProtocol('_raw.message.read_changed', (data) => this._onRawReadChanged(data, 'p2p'));
|
|
668
817
|
// V2 P2P 推送通知:收到通知后自动走 message.v2.pull 拉取并解密
|
|
669
818
|
this._dispatcher.subscribeProtocol('_raw.peer.v2.message_received', (data) => {
|
|
670
819
|
const ns = this._aid ? `p2p:${this._aid}` : '';
|
|
@@ -692,6 +841,7 @@ export class AUNClient {
|
|
|
692
841
|
this._dispatcher.subscribeProtocol('_raw.group.message_created', (data) => this._onRawGroupMessageCreated(data));
|
|
693
842
|
// 群组消息撤回推送:在线 push 通道,与 pull 双 tombstone 兜底互补(SDK 去重只回调一次)
|
|
694
843
|
this._dispatcher.subscribeProtocol('_raw.group.message_recalled', (data) => this._safeAsync(this._onRawGroupMessageRecalled(data)));
|
|
844
|
+
this._dispatcher.subscribeProtocol('_raw.group.read_changed', (data) => this._onRawReadChanged(data, 'group'));
|
|
695
845
|
// 群组变更事件:透传并触发 V2 state/SPK 维护
|
|
696
846
|
this._dispatcher.subscribeProtocol('_raw.group.changed', (data) => this._onRawGroupChanged(data));
|
|
697
847
|
// 定向邀请底层事件:按群 + 目标 AID 聚合后向应用层只提示一次。
|
|
@@ -724,6 +874,16 @@ export class AUNClient {
|
|
|
724
874
|
return sessionToken || null;
|
|
725
875
|
}
|
|
726
876
|
/** AUN Storage VFS 入口 */
|
|
877
|
+
get credential() {
|
|
878
|
+
if (!this._credential)
|
|
879
|
+
this._credential = new CredentialFacade(this);
|
|
880
|
+
return this._credential;
|
|
881
|
+
}
|
|
882
|
+
get org() {
|
|
883
|
+
if (!this._org)
|
|
884
|
+
this._org = new OrgFacade(this);
|
|
885
|
+
return this._org;
|
|
886
|
+
}
|
|
727
887
|
get storage() {
|
|
728
888
|
if (!this._storage)
|
|
729
889
|
this._storage = new StorageVFS(this);
|
|
@@ -757,6 +917,88 @@ export class AUNClient {
|
|
|
757
917
|
get currentAid() {
|
|
758
918
|
return this._currentAid;
|
|
759
919
|
}
|
|
920
|
+
_ownershipKey(subjectAid, ownerAid) {
|
|
921
|
+
return `${String(subjectAid ?? '').trim().toLowerCase()}|${String(ownerAid ?? '').trim().toLowerCase()}`;
|
|
922
|
+
}
|
|
923
|
+
getType() {
|
|
924
|
+
return String(this._ownershipType || 'agent').trim() || 'agent';
|
|
925
|
+
}
|
|
926
|
+
get_type() {
|
|
927
|
+
return this.getType();
|
|
928
|
+
}
|
|
929
|
+
getOwner() {
|
|
930
|
+
const aid = String(this._aid ?? '').trim().toLowerCase();
|
|
931
|
+
let best = null;
|
|
932
|
+
let bestActive = false;
|
|
933
|
+
for (const record of this._ownershipRecords.values()) {
|
|
934
|
+
if (String(record.subject_aid ?? '').trim().toLowerCase() === aid) {
|
|
935
|
+
if (String(record.status ?? '').trim().toLowerCase() === 'released')
|
|
936
|
+
continue;
|
|
937
|
+
const owner = String(record.owner_aid ?? '').trim();
|
|
938
|
+
if (!owner)
|
|
939
|
+
continue;
|
|
940
|
+
const active = String(record.status ?? '').trim().toLowerCase() === 'active';
|
|
941
|
+
if (!best || active && !bestActive) {
|
|
942
|
+
best = owner;
|
|
943
|
+
bestActive = active;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
return best;
|
|
948
|
+
}
|
|
949
|
+
get_owner() {
|
|
950
|
+
return this.getOwner();
|
|
951
|
+
}
|
|
952
|
+
getOwnershipStatus() {
|
|
953
|
+
const aid = String(this._aid ?? '').trim().toLowerCase();
|
|
954
|
+
let best = null;
|
|
955
|
+
let bestActive = false;
|
|
956
|
+
for (const record of this._ownershipRecords.values()) {
|
|
957
|
+
const subject = String(record.subject_aid ?? '').trim().toLowerCase();
|
|
958
|
+
const owner = String(record.owner_aid ?? '').trim().toLowerCase();
|
|
959
|
+
if (aid !== subject && aid !== owner)
|
|
960
|
+
continue;
|
|
961
|
+
const active = String(record.status ?? '').trim().toLowerCase() === 'active';
|
|
962
|
+
if (!best || active && !bestActive) {
|
|
963
|
+
best = record;
|
|
964
|
+
bestActive = active;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
if (best)
|
|
968
|
+
return Object.fromEntries(Object.entries(best).filter(([key]) => key !== 'subject_identity' && key !== 'escrow_password' && key !== 'escrow_download_url'));
|
|
969
|
+
return { subject_aid: this._aid ?? '', owner_aid: null, status: 'legacy_unbound', type: this.getType() };
|
|
970
|
+
}
|
|
971
|
+
get_ownership_status() {
|
|
972
|
+
return this.getOwnershipStatus();
|
|
973
|
+
}
|
|
974
|
+
async bindOwneraid(ownerAid) { return this._getOwnershipWorkflow().bindOwneraid(ownerAid); }
|
|
975
|
+
async bindSubaid(subAid) { return this._getOwnershipWorkflow().bindSubaid(subAid); }
|
|
976
|
+
async releaseSubaid(subAid) { return this._getOwnershipWorkflow().releaseSubaid(subAid); }
|
|
977
|
+
async transferSubaid(subAid, newOwnerAid) { return this._getOwnershipWorkflow().transferSubaid(subAid, newOwnerAid); }
|
|
978
|
+
async checkOwnershipConsensus(subAid) { return this._getOwnershipWorkflow().checkOwnershipConsensus(subAid); }
|
|
979
|
+
async bindAid(subjectAid, options = {}) {
|
|
980
|
+
throw ownershipFailure('unsupported_protocol', 'legacy bindAid is unsupported; use bindSubaid');
|
|
981
|
+
}
|
|
982
|
+
async bind_aid(subjectAid, options = {}) { return this.bindAid(subjectAid, options); }
|
|
983
|
+
async bindOwner(ownerAid, options = {}) {
|
|
984
|
+
throw ownershipFailure('unsupported_protocol', 'legacy bindOwner is unsupported; use bindOwneraid');
|
|
985
|
+
}
|
|
986
|
+
async bind_owner(ownerAid, options = {}) { return this.bindOwner(ownerAid, options); }
|
|
987
|
+
async releaseOwnership(subjectAid) { throw ownershipFailure('unsupported_protocol', 'legacy releaseOwnership is unsupported; use releaseSubaid'); }
|
|
988
|
+
async release_ownership(subjectAid) { return this.releaseOwnership(subjectAid); }
|
|
989
|
+
async acceptOwnershipRequest(operationId) {
|
|
990
|
+
return this._getOwnershipWorkflow().acceptOwnershipRequest(operationId);
|
|
991
|
+
}
|
|
992
|
+
async rejectOwnershipRequest(operationId, reason) {
|
|
993
|
+
return this._getOwnershipWorkflow().rejectOwnershipRequest(operationId, reason);
|
|
994
|
+
}
|
|
995
|
+
async _handleOwnershipMessage(message) {
|
|
996
|
+
if (!message || typeof message !== 'object' || Array.isArray(message))
|
|
997
|
+
return false;
|
|
998
|
+
const envelope = message;
|
|
999
|
+
const content = 'payload' in envelope ? envelope.payload : message;
|
|
1000
|
+
return this._getOwnershipWorkflow().receive(String(envelope.from_aid ?? ''), decryptedOwnershipJsonText(content) ?? content);
|
|
1001
|
+
}
|
|
760
1002
|
get hasIdentity() {
|
|
761
1003
|
return this._currentAid !== null && this.state !== ConnectionState.CLOSED;
|
|
762
1004
|
}
|
|
@@ -886,6 +1128,9 @@ export class AUNClient {
|
|
|
886
1128
|
logger: this._logger.for('aun_core.keystore'),
|
|
887
1129
|
});
|
|
888
1130
|
this._tokenStore = tokenStore;
|
|
1131
|
+
this._ownershipStorage = null;
|
|
1132
|
+
this._ownershipFiles = null;
|
|
1133
|
+
this._ownershipWorkflow = null;
|
|
889
1134
|
this._auth = new AuthFlow({
|
|
890
1135
|
tokenStore,
|
|
891
1136
|
crypto: new CryptoProvider(),
|
|
@@ -914,7 +1159,10 @@ export class AUNClient {
|
|
|
914
1159
|
},
|
|
915
1160
|
auth: () => this._auth,
|
|
916
1161
|
tokenStore: () => this._tokenStore,
|
|
917
|
-
fetchPeerCert: (target, certFingerprint) => this._fetchPeerCert(target, certFingerprint || undefined),
|
|
1162
|
+
fetchPeerCert: (target, certFingerprint, signedAt) => this._fetchPeerCert(target, certFingerprint || undefined, undefined, signedAt),
|
|
1163
|
+
resolveEntityOwner: (target, type) => this._resolveEntityAgentMdOwner(target, type),
|
|
1164
|
+
fetchEntityIdentity: (target, type) => this._freshEntityCertificate(target, type),
|
|
1165
|
+
fetchEntitySigner: (target, fingerprint) => this._freshEntitySigner(target, fingerprint),
|
|
918
1166
|
});
|
|
919
1167
|
this._transport = new RPCTransport({
|
|
920
1168
|
eventDispatcher: this._dispatcher,
|
|
@@ -924,10 +1172,20 @@ export class AUNClient {
|
|
|
924
1172
|
dnsNet,
|
|
925
1173
|
});
|
|
926
1174
|
this._transport.setProtocolMetaObserver((meta) => this._observeRpcMeta(meta));
|
|
1175
|
+
this._transport.setServiceEventVerifier((event, payload) => verifyServiceEvent(event, payload, { targetAid: this._identity?.aid ?? '', resolveEventCertificate: (aid, fingerprint) => this._fetchServiceEventCertificate(aid, fingerprint), resolveCredentialService: this.resolveCredentialService }), () => this._identity?.aid ?? '', () => this._serviceEventIdentityVersion);
|
|
927
1176
|
this._transport.setProtocolDisconnectCallback((err, closeCode) => this._handleTransportDisconnect(err, closeCode));
|
|
1177
|
+
this._transport._onForegroundDrained = () => this._delivery.onForegroundPullsDrained();
|
|
928
1178
|
}
|
|
929
1179
|
loadIdentity(aid) {
|
|
1180
|
+
const previousAid = this._aid;
|
|
930
1181
|
this._identityRuntime.loadIdentity(aid);
|
|
1182
|
+
this._serviceEventIdentityVersion++;
|
|
1183
|
+
this._ensureReadStateIdentity();
|
|
1184
|
+
if (previousAid !== this._aid) {
|
|
1185
|
+
this._ownershipRecords.clear();
|
|
1186
|
+
this._ownershipType = 'agent';
|
|
1187
|
+
}
|
|
1188
|
+
this._restoreOwnershipRecords();
|
|
931
1189
|
}
|
|
932
1190
|
setProtectedHeaders(headers) {
|
|
933
1191
|
if (!headers) {
|
|
@@ -1006,7 +1264,7 @@ export class AUNClient {
|
|
|
1006
1264
|
}
|
|
1007
1265
|
/** 连接到 Gateway;身份来自构造函数或 loadIdentity(aid),认证由 SDK 内部自动完成。 */
|
|
1008
1266
|
async connect(opts) {
|
|
1009
|
-
|
|
1267
|
+
await this._lifecycle.connect(opts);
|
|
1010
1268
|
}
|
|
1011
1269
|
/** 关闭连接 */
|
|
1012
1270
|
async close() {
|
|
@@ -1025,8 +1283,13 @@ export class AUNClient {
|
|
|
1025
1283
|
* 自动处理内部方法限制、E2EE 加解密、客户端签名等。
|
|
1026
1284
|
*/
|
|
1027
1285
|
async call(method, params) {
|
|
1286
|
+
const readIdentity = ['message.read', 'message.read_state', 'group.read', 'group.read_state'].includes(method)
|
|
1287
|
+
? this._ensureReadStateIdentity() : null;
|
|
1028
1288
|
try {
|
|
1029
|
-
|
|
1289
|
+
const result = await this._rpcPipeline.call(method, params);
|
|
1290
|
+
if (readIdentity !== null && readIdentity === this._ensureReadStateIdentity())
|
|
1291
|
+
this._observeReadStateResult(method, result, params ?? {});
|
|
1292
|
+
return result;
|
|
1030
1293
|
}
|
|
1031
1294
|
catch (err) {
|
|
1032
1295
|
await this._maybeHandleHalfOpenRpcFailure(err);
|
|
@@ -1079,12 +1342,23 @@ export class AUNClient {
|
|
|
1079
1342
|
const target = message.slice(agentPrefix.length).trim();
|
|
1080
1343
|
if (!target || target.toLowerCase() !== String(this._aid ?? '').trim().toLowerCase())
|
|
1081
1344
|
return false;
|
|
1082
|
-
|
|
1345
|
+
return this._ensureInitialAgentMd();
|
|
1346
|
+
}
|
|
1347
|
+
async _ensureInitialAgentMd() {
|
|
1348
|
+
const aid = this._aid;
|
|
1349
|
+
if (!aid || !this._currentAid)
|
|
1350
|
+
return false;
|
|
1351
|
+
const observed = await this._getOwnershipFiles().readAgent(aid);
|
|
1352
|
+
if (observed.exists)
|
|
1353
|
+
return false;
|
|
1354
|
+
let agentMd = buildDefaultAgentMd(aid, { type: this.getType() });
|
|
1083
1355
|
try {
|
|
1084
|
-
agentMd = this._agentMdManager.readContent(
|
|
1356
|
+
agentMd = this._agentMdManager.readContent(aid) || agentMd;
|
|
1085
1357
|
}
|
|
1086
1358
|
catch { }
|
|
1087
|
-
|
|
1359
|
+
const payload = updateOwnershipAgentMd(agentMd, {});
|
|
1360
|
+
const raw = signOwnershipFile(this._currentAid, payload.endsWith('\n') || payload.endsWith('\r') ? payload : `${payload}\n`, 'agent');
|
|
1361
|
+
await this._agentMdManager.submitOwnershipRaw(aid, raw, { if_none_match: '*' });
|
|
1088
1362
|
return true;
|
|
1089
1363
|
}
|
|
1090
1364
|
async _checkIdentityAdmissionAfterConnect() {
|
|
@@ -1092,10 +1366,12 @@ export class AUNClient {
|
|
|
1092
1366
|
if (!aid)
|
|
1093
1367
|
return;
|
|
1094
1368
|
try {
|
|
1095
|
-
const checked = await this._agentMdManager.check(aid);
|
|
1369
|
+
const checked = await this._agentMdManager.check(aid, 0);
|
|
1096
1370
|
if (!checked.remote_found) {
|
|
1097
|
-
|
|
1098
|
-
|
|
1371
|
+
await this._ensureInitialAgentMd();
|
|
1372
|
+
}
|
|
1373
|
+
else if (checked.in_sync === false) {
|
|
1374
|
+
await this._agentMdManager.download(aid);
|
|
1099
1375
|
}
|
|
1100
1376
|
}
|
|
1101
1377
|
catch (exc) {
|
|
@@ -1104,6 +1380,7 @@ export class AUNClient {
|
|
|
1104
1380
|
const store = this._aidStore;
|
|
1105
1381
|
if (!store)
|
|
1106
1382
|
return;
|
|
1383
|
+
await this._resumeOrgIdentityAfterConnect(store);
|
|
1107
1384
|
let listed;
|
|
1108
1385
|
try {
|
|
1109
1386
|
listed = await this.call('group.list_my', {});
|
|
@@ -1140,6 +1417,8 @@ export class AUNClient {
|
|
|
1140
1417
|
await this.bindGroupAid({ group_id: groupId, group_aid: groupAid }, { aidStore: store });
|
|
1141
1418
|
return;
|
|
1142
1419
|
}
|
|
1420
|
+
if (await this._resumeGroupAgentMdUpload(store, groupAid))
|
|
1421
|
+
return;
|
|
1143
1422
|
const checked = await this._agentMdManager.check(groupAid);
|
|
1144
1423
|
if (!checked.remote_found) {
|
|
1145
1424
|
await this._uploadGroupAgentMd(store, groupAid, {}, {});
|
|
@@ -1170,6 +1449,192 @@ export class AUNClient {
|
|
|
1170
1449
|
this._groupIdentityOperations.delete(key);
|
|
1171
1450
|
}
|
|
1172
1451
|
}
|
|
1452
|
+
async createOrg(params = {}, options = {}) {
|
|
1453
|
+
const store = options.aidStore;
|
|
1454
|
+
const owner = this._currentAid;
|
|
1455
|
+
const orgAid = String(params.org_id ?? '').trim();
|
|
1456
|
+
if (!store || !owner?.isPrivateKeyValid() || !orgAid)
|
|
1457
|
+
throw new ValidationError('createOrg requires aidStore, org_id and current owner identity');
|
|
1458
|
+
this._aidStore = store;
|
|
1459
|
+
let identity = store.load(orgAid);
|
|
1460
|
+
if (!identity.ok) {
|
|
1461
|
+
const registered = await store.register(orgAid, { type: 'org', owner: owner.aid });
|
|
1462
|
+
if (!registered.ok)
|
|
1463
|
+
throw ownershipFailure('unavailable', registered.error.message, true);
|
|
1464
|
+
identity = store.load(orgAid);
|
|
1465
|
+
}
|
|
1466
|
+
if (!identity.ok)
|
|
1467
|
+
throw ownershipFailure('unavailable', identity.error.message, true);
|
|
1468
|
+
const target = options.serviceAid ?? `org.${orgAid.slice(orgAid.indexOf('.') + 1)}`;
|
|
1469
|
+
const tokens = store._tokenStore;
|
|
1470
|
+
const inputHash = crypto.createHash('sha256').update(canonicalJson(params)).digest('hex');
|
|
1471
|
+
let checkpoint = tokens.loadMetadata(orgAid)?.org_identity_creation;
|
|
1472
|
+
if (checkpoint && (checkpoint.owner_aid !== owner.aid || checkpoint.input_hash !== inputHash))
|
|
1473
|
+
throw ownershipFailure('conflict', 'pending Org creation context changed');
|
|
1474
|
+
if (!checkpoint) {
|
|
1475
|
+
const prepared = this.org.prepareRequest('org.lifecycle.create', { ...params, owner_aid: owner.aid }, target, owner, { orgSigner: identity.data.aid });
|
|
1476
|
+
checkpoint = { owner_aid: owner.aid, input_hash: inputHash, source_params: params, prepared, phase: 'create' };
|
|
1477
|
+
tokens.saveMetadata(orgAid, { org_identity_creation: checkpoint });
|
|
1478
|
+
}
|
|
1479
|
+
if (checkpoint.phase === 'create') {
|
|
1480
|
+
const result = await this.call('org.lifecycle.create', checkpoint.prepared);
|
|
1481
|
+
checkpoint = { ...checkpoint, result, phase: 'upload' };
|
|
1482
|
+
tokens.saveMetadata(orgAid, { org_identity_creation: checkpoint });
|
|
1483
|
+
}
|
|
1484
|
+
await this._uploadOrgAgentMd(store, orgAid, params);
|
|
1485
|
+
tokens.saveMetadata(orgAid, { org_identity_creation: null });
|
|
1486
|
+
return checkpoint.result;
|
|
1487
|
+
}
|
|
1488
|
+
async _resumeOrgIdentityAfterConnect(store) {
|
|
1489
|
+
const keyStore = store._keystore;
|
|
1490
|
+
const tokens = store._tokenStore;
|
|
1491
|
+
if (typeof keyStore?.listIdentities !== 'function' || typeof tokens?.loadMetadata !== 'function')
|
|
1492
|
+
return;
|
|
1493
|
+
for (const aid of keyStore.listIdentities()) {
|
|
1494
|
+
const metadata = tokens.loadMetadata(aid);
|
|
1495
|
+
const creation = metadata?.org_identity_creation;
|
|
1496
|
+
const transfer = metadata?.org_identity_transfer;
|
|
1497
|
+
try {
|
|
1498
|
+
if (creation?.owner_aid === this._aid) {
|
|
1499
|
+
await this.createOrg(creation.source_params, { aidStore: store });
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
if (transfer?.owner_aid === this._aid) {
|
|
1503
|
+
await this.completeOrgTransfer(transfer.source_params, { aidStore: store });
|
|
1504
|
+
continue;
|
|
1505
|
+
}
|
|
1506
|
+
const pending = store._agentMdManager.pendingUpload(aid);
|
|
1507
|
+
if (!pending)
|
|
1508
|
+
continue;
|
|
1509
|
+
const fields = validateAgentMdDocument(pending.content, { expectedAid: aid }).frontmatter;
|
|
1510
|
+
if (fields.type === 'org' && fields.owner === this._aid)
|
|
1511
|
+
await this._resumeGroupAgentMdUpload(store, aid);
|
|
1512
|
+
}
|
|
1513
|
+
catch (error) {
|
|
1514
|
+
this._clientLog.warn(`post-connect Org identity recovery failed: aid=${aid} err=${formatCaughtError(error)}`);
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
async completeOrgTransfer(params = {}, options = {}) {
|
|
1519
|
+
const store = options.aidStore;
|
|
1520
|
+
const owner = this._currentAid;
|
|
1521
|
+
const orgAid = String(params.org_id ?? '').trim();
|
|
1522
|
+
const transferId = String(params.transfer_id ?? '').trim();
|
|
1523
|
+
if (!store || !owner?.isPrivateKeyValid() || !orgAid || !transferId)
|
|
1524
|
+
throw new ValidationError('completeOrgTransfer requires aidStore, org_id, transfer_id and current accepting owner');
|
|
1525
|
+
this._aidStore = store;
|
|
1526
|
+
const localStore = store._keystore;
|
|
1527
|
+
const tokenStore = store._tokenStore;
|
|
1528
|
+
const pendingKey = `org-transfer:${orgAid}:${transferId}`;
|
|
1529
|
+
const inputHash = crypto.createHash('sha256').update(canonicalJson(params)).digest('hex');
|
|
1530
|
+
let keyPair = localStore.loadPendingGroupBind(pendingKey);
|
|
1531
|
+
if (!keyPair) {
|
|
1532
|
+
keyPair = new CryptoProvider().generateIdentity();
|
|
1533
|
+
localStore.savePendingGroupBind(pendingKey, keyPair);
|
|
1534
|
+
}
|
|
1535
|
+
const target = options.serviceAid ?? `org.${orgAid.slice(orgAid.indexOf('.') + 1)}`;
|
|
1536
|
+
const publicKey = crypto.createPublicKey({ key: Buffer.from(keyPair.public_key_der_b64, 'base64'), format: 'der', type: 'spki' }).export({ format: 'pem', type: 'spki' }).toString();
|
|
1537
|
+
let pending = tokenStore.loadMetadata(orgAid)?.org_identity_transfer;
|
|
1538
|
+
if (pending && (pending.transfer_id !== transferId || pending.owner_aid !== owner.aid || pending.public_key !== publicKey || pending.input_hash !== inputHash))
|
|
1539
|
+
throw ownershipFailure('conflict', 'pending Org transfer context changed');
|
|
1540
|
+
if (!pending) {
|
|
1541
|
+
const rekey = { ...params, stage: 'rekey', public_key: publicKey, curve: 'P-256' };
|
|
1542
|
+
delete rekey.new_owner;
|
|
1543
|
+
delete rekey.request_binding;
|
|
1544
|
+
delete rekey.client_signature;
|
|
1545
|
+
pending = { transfer_id: transferId, owner_aid: owner.aid, public_key: publicKey, input_hash: inputHash, phase: 'rekey', source_params: params, rekey_params: this.org.prepareRequest('org.lifecycle.transferAccept', rekey, target, owner) };
|
|
1546
|
+
tokenStore.saveMetadata(orgAid, { org_identity_transfer: pending });
|
|
1547
|
+
}
|
|
1548
|
+
if (pending.phase === 'rekey') {
|
|
1549
|
+
const result = await this.call('org.lifecycle.transferAccept', pending.rekey_params);
|
|
1550
|
+
pending = { ...pending, phase: 'finalize', rekey_result: result };
|
|
1551
|
+
tokenStore.saveMetadata(orgAid, { org_identity_transfer: pending });
|
|
1552
|
+
}
|
|
1553
|
+
const rekeyResult = pending.rekey_result;
|
|
1554
|
+
const certPem = String(rekeyResult?.cert_pem ?? rekeyResult?.cert ?? '').trim();
|
|
1555
|
+
if (!certPem)
|
|
1556
|
+
throw ownershipFailure('unavailable', 'Org rekey response has no certificate', true);
|
|
1557
|
+
const imported = store.importGroupIdentity(orgAid, { ...keyPair, cert_pem: certPem });
|
|
1558
|
+
if (!imported.ok)
|
|
1559
|
+
throw ownershipFailure('invalid', imported.error.message);
|
|
1560
|
+
const orgIdentity = this._localEntityCertificate(orgAid);
|
|
1561
|
+
if (!pending.finalize_params) {
|
|
1562
|
+
const final = { ...pending.source_params, stage: 'finalize', public_key: publicKey, curve: 'P-256', request_id: crypto.randomUUID().replace(/-/g, ''), nonce: crypto.randomBytes(32).toString('base64url') };
|
|
1563
|
+
delete final.request_binding;
|
|
1564
|
+
delete final.client_signature;
|
|
1565
|
+
for (const field of ['previous_owner', 'new_owner']) {
|
|
1566
|
+
const material = isJsonObject(final[field]) ? final[field] : {};
|
|
1567
|
+
let binding = isJsonObject(material.membership_binding) ? material.membership_binding : undefined;
|
|
1568
|
+
if (!binding || !isJsonObject(material.terms))
|
|
1569
|
+
throw ownershipFailure('invalid', 'Org transfer requires both replacement membership bindings and terms');
|
|
1570
|
+
if (!binding.member_signature && binding.member_aid === owner.aid)
|
|
1571
|
+
binding = signMembershipBinding(binding, owner, 'member');
|
|
1572
|
+
binding = signMembershipBinding(binding, orgIdentity, 'org');
|
|
1573
|
+
const oldCredential = isJsonObject(material.credential_template) ? { ...material.credential_template } : undefined;
|
|
1574
|
+
if (oldCredential) {
|
|
1575
|
+
delete oldCredential.signature;
|
|
1576
|
+
oldCredential.key_version = orgIdentity.certFingerprint;
|
|
1577
|
+
}
|
|
1578
|
+
const credential = oldCredential ? createCredential(oldCredential, orgIdentity) : createMemberCredential(binding, orgIdentity, { key_version: orgIdentity.certFingerprint });
|
|
1579
|
+
final[field] = { ...material, membership_binding: binding, credential_template: credential };
|
|
1580
|
+
delete final[field].registration_authorization;
|
|
1581
|
+
}
|
|
1582
|
+
pending = { ...pending, finalize_params: this.org.prepareRequest('org.lifecycle.transferAccept', final, target, owner, { orgSigner: orgIdentity }) };
|
|
1583
|
+
tokenStore.saveMetadata(orgAid, { org_identity_transfer: pending });
|
|
1584
|
+
}
|
|
1585
|
+
const result = await this.call('org.lifecycle.transferAccept', pending.finalize_params);
|
|
1586
|
+
await this._uploadOrgAgentMd(store, orgAid, pending.source_params);
|
|
1587
|
+
localStore.clearPendingGroupBind(pendingKey);
|
|
1588
|
+
tokenStore.saveMetadata(orgAid, { org_identity_transfer: null });
|
|
1589
|
+
return result;
|
|
1590
|
+
}
|
|
1591
|
+
async dissolveOrg(params = {}, options = {}) {
|
|
1592
|
+
const store = options.aidStore ?? this._aidStore;
|
|
1593
|
+
if (store)
|
|
1594
|
+
this._aidStore = store;
|
|
1595
|
+
const orgAid = String(params.org_id ?? '').trim();
|
|
1596
|
+
const owner = this._currentAid;
|
|
1597
|
+
const target = options.serviceAid ?? `org.${orgAid.slice(orgAid.indexOf('.') + 1)}`;
|
|
1598
|
+
const payload = owner ? this.org.prepareRequest('org.lifecycle.dissolve', params, target, owner) : params;
|
|
1599
|
+
const result = await this.call('org.lifecycle.dissolve', payload);
|
|
1600
|
+
const organization = isJsonObject(result?.organization) ? result.organization : {};
|
|
1601
|
+
const status = String(organization.status ?? result?.status ?? '');
|
|
1602
|
+
if (store && orgAid && status === 'dissolved') {
|
|
1603
|
+
const tokenStore = store._tokenStore;
|
|
1604
|
+
const keyStore = store._keystore;
|
|
1605
|
+
const certPem = keyStore?.loadCert?.(orgAid);
|
|
1606
|
+
const metadata = tokenStore?.loadMetadata?.(orgAid) ?? {};
|
|
1607
|
+
const transfer = metadata.org_identity_transfer;
|
|
1608
|
+
if (transfer?.transfer_id && typeof keyStore?.clearPendingGroupBind === 'function') {
|
|
1609
|
+
keyStore.clearPendingGroupBind(`org-transfer:${orgAid}:${String(transfer.transfer_id)}`);
|
|
1610
|
+
}
|
|
1611
|
+
tokenStore?.saveMetadata?.(orgAid, {
|
|
1612
|
+
...(certPem ? { invalidated_entity_certificate: certFingerprint(certPem) } : {}),
|
|
1613
|
+
org_identity_creation: undefined,
|
|
1614
|
+
org_identity_transfer: undefined,
|
|
1615
|
+
});
|
|
1616
|
+
tokenStore?.saveInstanceState?.(orgAid, store.deviceId, store.slotId, {});
|
|
1617
|
+
store._agentMdManager?.saveRecord?.(orgAid, {
|
|
1618
|
+
pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '',
|
|
1619
|
+
pending_condition: {}, pending_relation: {}, pending_authorization: '',
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1622
|
+
return result;
|
|
1623
|
+
}
|
|
1624
|
+
async _uploadOrgAgentMd(store, orgAid, params) {
|
|
1625
|
+
const owner = this._currentAid;
|
|
1626
|
+
if (!owner)
|
|
1627
|
+
throw ownershipFailure('forbidden', 'Org agent.md requires current owner');
|
|
1628
|
+
if (await this._resumeGroupAgentMdUpload(store, orgAid))
|
|
1629
|
+
return;
|
|
1630
|
+
const identity = this._localEntityCertificate(orgAid);
|
|
1631
|
+
const content = String(params.org_agent_md ?? buildDefaultAgentMd(orgAid, { type: 'org', owner: owner.aid, entityCertFingerprint: identity.certFingerprint, name: String(params.name ?? orgAid) }));
|
|
1632
|
+
const uploaded = await store.uploadAgentMd(orgAid, content, { uploaderAid: owner.aid, entityOwnerResolver: (aid, type) => this._resolveEntityAgentMdOwner(aid, type), entityIdentityGetter: (aid) => this._freshEntityCertificate(aid, 'org') });
|
|
1633
|
+
if (!uploaded.ok) {
|
|
1634
|
+
const code = ['invalid', 'unavailable', 'forbidden', 'conflict', 'persistence_unavailable'].includes(uploaded.error.code) ? uploaded.error.code : uploaded.error.code === 'NETWORK_ERROR' ? 'unavailable' : 'invalid';
|
|
1635
|
+
throw ownershipFailure(code, uploaded.error.message, code === 'unavailable');
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1173
1638
|
async createGroup(params = {}, options = {}) {
|
|
1174
1639
|
if (!isJsonObject(params)) {
|
|
1175
1640
|
throw new ValidationError('createGroup params must be an object');
|
|
@@ -1338,6 +1803,8 @@ export class AUNClient {
|
|
|
1338
1803
|
`name: ${JSON.stringify(name)}`,
|
|
1339
1804
|
'type: "group"',
|
|
1340
1805
|
'version: "1.0.0"',
|
|
1806
|
+
`owner: ${JSON.stringify(String(group.owner_aid ?? group.owner ?? this._aid ?? ''))}`,
|
|
1807
|
+
`entity_cert_fingerprint: ${JSON.stringify(this._localEntityCertificate(groupAid).certFingerprint)}`,
|
|
1341
1808
|
`description: ${JSON.stringify(description)}`,
|
|
1342
1809
|
...(avatar ? [`avatar: ${JSON.stringify(avatar)}`] : []),
|
|
1343
1810
|
'---',
|
|
@@ -1346,16 +1813,71 @@ export class AUNClient {
|
|
|
1346
1813
|
'',
|
|
1347
1814
|
].join('\n');
|
|
1348
1815
|
}
|
|
1816
|
+
async _resumeGroupAgentMdUpload(store, groupAid) {
|
|
1817
|
+
const identityStore = store;
|
|
1818
|
+
const pendingManager = identityStore._agentMdManager ?? this._agentMdManager;
|
|
1819
|
+
const pending = pendingManager.pendingUpload(groupAid);
|
|
1820
|
+
if (!pending)
|
|
1821
|
+
return false;
|
|
1822
|
+
const fields = validateAgentMdDocument(pending.content, { expectedAid: groupAid, validateOwnership: false }).frontmatter;
|
|
1823
|
+
if (['ownership', 'ownership_proof', 'ownership_release'].some((field) => Object.hasOwn(fields, field))) {
|
|
1824
|
+
throw new StateError('ownership pending agent.md requires its persisted 2.1 task', { stringCode: 'unsupported_protocol' });
|
|
1825
|
+
}
|
|
1826
|
+
const identity = this._currentAid;
|
|
1827
|
+
if (!identity || fields.owner !== identity.aid)
|
|
1828
|
+
throw ownershipFailure('forbidden', 'group agent.md recovery requires its original current owner');
|
|
1829
|
+
const manager = new AgentMdManager({
|
|
1830
|
+
aunPath: store.aunPath,
|
|
1831
|
+
verifySsl: identityStore._verifySsl,
|
|
1832
|
+
logger: identityStore._log.for('aun_core.agent_md'),
|
|
1833
|
+
uploadViaGatewayHost: true,
|
|
1834
|
+
ownerAidGetter: () => identity.aid,
|
|
1835
|
+
currentAidGetter: () => identity,
|
|
1836
|
+
entityOwnerResolver: (aid, type) => this._resolveEntityAgentMdOwner(aid, type),
|
|
1837
|
+
entityIdentityGetter: (aid, type) => this._freshEntityCertificate(aid, type),
|
|
1838
|
+
entitySignerResolver: (aid, fingerprint) => this._freshEntitySigner(aid, fingerprint),
|
|
1839
|
+
gatewayResolver: (aid) => identityStore._resolveGateway(aid),
|
|
1840
|
+
accessTokenResolver: (_aid, gatewayUrl) => identityStore._uploadAgentMdToken(identity, gatewayUrl),
|
|
1841
|
+
aidValidator: (aid) => identityStore._registerFlow.validateAidName(aid),
|
|
1842
|
+
});
|
|
1843
|
+
return await manager.resumePendingUpload(groupAid);
|
|
1844
|
+
}
|
|
1349
1845
|
async _uploadGroupAgentMd(store, groupAid, params, group) {
|
|
1350
1846
|
const uploaderAid = String(this._aid ?? '').trim();
|
|
1351
1847
|
if (!uploaderAid) {
|
|
1352
1848
|
throw new ValidationError('group agent.md upload requires current owner AID');
|
|
1353
1849
|
}
|
|
1354
|
-
|
|
1850
|
+
this._aidStore = store;
|
|
1851
|
+
if (await this._resumeGroupAgentMdUpload(store, groupAid))
|
|
1852
|
+
return;
|
|
1853
|
+
const uploaded = await store.uploadAgentMd(groupAid, this._groupAgentMdContent(groupAid, params, group), { uploaderAid, entityOwnerResolver: (aid, type) => this._resolveEntityAgentMdOwner(aid, type), entityIdentityGetter: (aid) => this._freshEntityCertificate(aid) });
|
|
1355
1854
|
if (!uploaded.ok) {
|
|
1356
|
-
|
|
1855
|
+
const code = ['invalid', 'unavailable', 'forbidden', 'conflict', 'persistence_unavailable'].includes(uploaded.error.code) ? uploaded.error.code : uploaded.error.code === 'NETWORK_ERROR' ? 'unavailable' : 'invalid';
|
|
1856
|
+
throw ownershipFailure(code, `group agent.md upload failed: ${uploaded.error.message}`, code === 'unavailable');
|
|
1357
1857
|
}
|
|
1358
1858
|
}
|
|
1859
|
+
_localEntityCertificate(aid) {
|
|
1860
|
+
const loaded = this._aidStore?.load(aid);
|
|
1861
|
+
if (!loaded?.ok)
|
|
1862
|
+
throw ownershipFailure('unavailable', 'entity local certificate is unavailable', true);
|
|
1863
|
+
return loaded.data.aid;
|
|
1864
|
+
}
|
|
1865
|
+
async _freshEntityCertificate(aid, type = 'group') {
|
|
1866
|
+
const material = await this._auth.resolveCurrentEntityCertificate(await this._resolveOwnershipCertificateGateway(aid), aid, type);
|
|
1867
|
+
const certPem = material.cert_pem;
|
|
1868
|
+
return AID._create({ aid, aunPath: this._configModel.aunPath, certPem, certValid: true, privateKeyPem: null, privateKeyValid: false });
|
|
1869
|
+
}
|
|
1870
|
+
async _freshEntitySigner(aid, fingerprint) {
|
|
1871
|
+
const material = await this._auth.resolveOwnershipCertificate(await this._resolveOwnershipCertificateGateway(aid), aid, fingerprint, 'signing');
|
|
1872
|
+
return AID._create({ aid, aunPath: this._configModel.aunPath, certPem: material.cert_pem, certValid: true, privateKeyPem: null, privateKeyValid: false });
|
|
1873
|
+
}
|
|
1874
|
+
async _resolveEntityAgentMdOwner(aid, type) {
|
|
1875
|
+
const authority = await this._agentMdManager.readEntityAuthorityFresh(aid);
|
|
1876
|
+
const identity = await this._freshEntityCertificate(aid, type);
|
|
1877
|
+
if (authority.entity_cert_fingerprint !== identity.certFingerprint)
|
|
1878
|
+
throw ownershipFailure('conflict', 'entity authority changed during current-certificate verification');
|
|
1879
|
+
return authority.owner_aid;
|
|
1880
|
+
}
|
|
1359
1881
|
async bindGroupAid(params = {}, options = {}) {
|
|
1360
1882
|
if (!isJsonObject(params)) {
|
|
1361
1883
|
throw new ValidationError('bindGroupAid params must be an object');
|
|
@@ -1793,6 +2315,168 @@ export class AUNClient {
|
|
|
1793
2315
|
this._clientLog.debug(`on exit: elapsed=${Date.now() - tStart}ms event=${event}`);
|
|
1794
2316
|
return result;
|
|
1795
2317
|
}
|
|
2318
|
+
_readStateIsNewer(previous, current) {
|
|
2319
|
+
if (!previous)
|
|
2320
|
+
return true;
|
|
2321
|
+
const oldOrder = BigInt(String(previous.last_read_order ?? 0));
|
|
2322
|
+
const newOrder = BigInt(String(current.last_read_order ?? 0));
|
|
2323
|
+
if (oldOrder !== newOrder)
|
|
2324
|
+
return newOrder > oldOrder;
|
|
2325
|
+
const oldId = String(previous.last_read_message_id ?? '');
|
|
2326
|
+
const newId = String(current.last_read_message_id ?? '');
|
|
2327
|
+
if (oldOrder === 0n)
|
|
2328
|
+
return newId === oldId;
|
|
2329
|
+
const oldPoints = Array.from(oldId);
|
|
2330
|
+
const newPoints = Array.from(newId);
|
|
2331
|
+
for (let index = 0; index < Math.min(oldPoints.length, newPoints.length); index++) {
|
|
2332
|
+
const oldPoint = oldPoints[index].codePointAt(0);
|
|
2333
|
+
const newPoint = newPoints[index].codePointAt(0);
|
|
2334
|
+
if (oldPoint !== newPoint)
|
|
2335
|
+
return newPoint > oldPoint;
|
|
2336
|
+
}
|
|
2337
|
+
return newPoints.length >= oldPoints.length;
|
|
2338
|
+
}
|
|
2339
|
+
_ensureReadStateIdentity() {
|
|
2340
|
+
const identity = `${this._aid ?? ''}\u0000${this._deviceId ?? ''}`;
|
|
2341
|
+
if (identity !== this._readStateIdentity) {
|
|
2342
|
+
this._readStateCache.clear();
|
|
2343
|
+
this._readStateQueries.clear();
|
|
2344
|
+
this._readStateIdentity = identity;
|
|
2345
|
+
this._readStateGeneration += 1;
|
|
2346
|
+
}
|
|
2347
|
+
return `${identity}\u0000${this._readStateGeneration}`;
|
|
2348
|
+
}
|
|
2349
|
+
_registerReadStateQuery(method, params) {
|
|
2350
|
+
const key = method + '\u0000' + JSON.stringify(Object.keys(params).sort().map((field) => [field, params[field]]));
|
|
2351
|
+
this._readStateQueries.set(key, { method, params: { ...params } });
|
|
2352
|
+
}
|
|
2353
|
+
_readStateItems(result) {
|
|
2354
|
+
const resultObject = isJsonObject(result) ? result : null;
|
|
2355
|
+
return Array.isArray(resultObject?.states)
|
|
2356
|
+
? resultObject.states.filter((item) => isJsonObject(item))
|
|
2357
|
+
: [];
|
|
2358
|
+
}
|
|
2359
|
+
_observeReadStateResult(method, result, params) {
|
|
2360
|
+
if (!['message.read', 'message.read_state', 'group.read', 'group.read_state'].includes(method))
|
|
2361
|
+
return;
|
|
2362
|
+
this._ensureReadStateIdentity();
|
|
2363
|
+
const isGroup = method.startsWith('group.');
|
|
2364
|
+
if (method.endsWith('.read')) {
|
|
2365
|
+
for (const state of this._readStateItems(params)) {
|
|
2366
|
+
const query = { topic_key: state.topic_key ?? '' };
|
|
2367
|
+
if (isGroup)
|
|
2368
|
+
query.group_id = state.group_id ?? params.group_id ?? '';
|
|
2369
|
+
else
|
|
2370
|
+
query.scope = state.scope || params.scope || 'p2p';
|
|
2371
|
+
this._registerReadStateQuery(`${isGroup ? 'group' : 'message'}.read_state`, query);
|
|
2372
|
+
}
|
|
2373
|
+
if (!Array.isArray(params.states) && params.topic_key) {
|
|
2374
|
+
const query = {};
|
|
2375
|
+
for (const field of ['scope', 'group_id', 'topic_key']) {
|
|
2376
|
+
if (field in params)
|
|
2377
|
+
query[field] = params[field];
|
|
2378
|
+
}
|
|
2379
|
+
this._registerReadStateQuery(`${isGroup ? 'group' : 'message'}.read_state`, query);
|
|
2380
|
+
}
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2383
|
+
this._registerReadStateQuery(method, params);
|
|
2384
|
+
for (const state of this._readStateItems(result)) {
|
|
2385
|
+
const scope = String(state.scope ?? (isGroup ? 'group' : 'p2p')).trim();
|
|
2386
|
+
const topicKey = String(state.topic_key ?? params.topic_key ?? '');
|
|
2387
|
+
const groupId = isGroup ? String(state.group_id ?? params.group_id ?? '').trim() : '';
|
|
2388
|
+
const lastRead = String(state.last_read_message_id ?? '').trim();
|
|
2389
|
+
if (!scope || !topicKey || !lastRead || (isGroup && !groupId))
|
|
2390
|
+
continue;
|
|
2391
|
+
const key = `${scope}\u0000${groupId}\u0000${topicKey}`;
|
|
2392
|
+
const query = { topic_key: topicKey };
|
|
2393
|
+
if (isGroup)
|
|
2394
|
+
query.group_id = groupId;
|
|
2395
|
+
else
|
|
2396
|
+
query.scope = scope;
|
|
2397
|
+
const previous = this._readStateCache.get(key)?.state;
|
|
2398
|
+
if (previous && !this._readStateIsNewer(previous, state))
|
|
2399
|
+
continue;
|
|
2400
|
+
const samePosition = previous?.last_read_message_id === state.last_read_message_id
|
|
2401
|
+
&& BigInt(String(previous?.last_read_order ?? 0)) === BigInt(String(state.last_read_order ?? 0));
|
|
2402
|
+
this._readStateCache.set(key, {
|
|
2403
|
+
method: `${isGroup ? 'group' : 'message'}.read_state`,
|
|
2404
|
+
params: query,
|
|
2405
|
+
state: samePosition ? { ...previous, ...state } : { ...state },
|
|
2406
|
+
});
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
_onRawReadChanged(data, expectedScope) {
|
|
2410
|
+
if (!isJsonObject(data))
|
|
2411
|
+
return;
|
|
2412
|
+
const eventData = data;
|
|
2413
|
+
if (eventData.scope !== expectedScope || eventData.owner_aid !== this._aid)
|
|
2414
|
+
return;
|
|
2415
|
+
const fields = ['owner_aid', 'topic_key', 'last_read_message_id', ...(expectedScope === 'group' ? ['group_id'] : [])];
|
|
2416
|
+
if (fields.some((field) => {
|
|
2417
|
+
const value = eventData[field];
|
|
2418
|
+
return typeof value !== 'string' || !value || Array.from(value).length > 128;
|
|
2419
|
+
}))
|
|
2420
|
+
return;
|
|
2421
|
+
const order = eventData.last_read_order;
|
|
2422
|
+
if (!(typeof order === 'number' && Number.isSafeInteger(order) && order > 0)
|
|
2423
|
+
&& !(typeof order === 'string' && /^[1-9]\d*$/.test(order)))
|
|
2424
|
+
return;
|
|
2425
|
+
this._ensureReadStateIdentity();
|
|
2426
|
+
const scope = expectedScope;
|
|
2427
|
+
const topicKey = String(eventData.topic_key ?? '');
|
|
2428
|
+
const groupId = scope === 'group' ? String(eventData.group_id) : '';
|
|
2429
|
+
const lastRead = String(eventData.last_read_message_id);
|
|
2430
|
+
if (scope && topicKey && lastRead && (scope !== 'group' || groupId)) {
|
|
2431
|
+
const key = `${scope}\u0000${groupId}\u0000${topicKey}`;
|
|
2432
|
+
const query = { topic_key: topicKey };
|
|
2433
|
+
if (scope === 'group')
|
|
2434
|
+
query.group_id = groupId;
|
|
2435
|
+
else
|
|
2436
|
+
query.scope = scope;
|
|
2437
|
+
const method = `${scope === 'group' ? 'group' : 'message'}.read_state`;
|
|
2438
|
+
this._registerReadStateQuery(method, query);
|
|
2439
|
+
const previous = this._readStateCache.get(key)?.state;
|
|
2440
|
+
if (previous && (!this._readStateIsNewer(previous, eventData)
|
|
2441
|
+
|| (previous.last_read_message_id === eventData.last_read_message_id
|
|
2442
|
+
&& BigInt(String(previous.last_read_order ?? 0)) === BigInt(String(eventData.last_read_order ?? 0)))))
|
|
2443
|
+
return;
|
|
2444
|
+
this._readStateCache.set(key, { method, params: query, state: { ...eventData } });
|
|
2445
|
+
}
|
|
2446
|
+
this._dispatcher.enqueue(scope === 'group' ? 'group.read_changed' : 'message.read_changed', eventData);
|
|
2447
|
+
}
|
|
2448
|
+
async _reconcileReadStates() {
|
|
2449
|
+
const identity = this._ensureReadStateIdentity();
|
|
2450
|
+
const keys = new Set();
|
|
2451
|
+
const errors = [];
|
|
2452
|
+
for (const entry of [...this._readStateQueries.values()]) {
|
|
2453
|
+
try {
|
|
2454
|
+
const result = await this.call(entry.method, { ...entry.params });
|
|
2455
|
+
if (identity !== this._ensureReadStateIdentity())
|
|
2456
|
+
return;
|
|
2457
|
+
this._observeReadStateResult(entry.method, result, entry.params);
|
|
2458
|
+
for (const state of this._readStateItems(result)) {
|
|
2459
|
+
const scope = String(state.scope ?? (entry.method.startsWith('group.') ? 'group' : 'p2p'));
|
|
2460
|
+
const groupId = scope === 'group' ? String(state.group_id ?? entry.params.group_id ?? '') : '';
|
|
2461
|
+
const topicKey = String(state.topic_key ?? entry.params.topic_key ?? '');
|
|
2462
|
+
const key = `${scope}\u0000${groupId}\u0000${topicKey}`;
|
|
2463
|
+
if (this._readStateCache.has(key))
|
|
2464
|
+
keys.add(key);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
catch (error) {
|
|
2468
|
+
errors.push({ method: entry.method, params: { ...entry.params }, error: error instanceof Error ? error.message : String(error) });
|
|
2469
|
+
this._clientLog.debug(`read state reconnect reconciliation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2470
|
+
}
|
|
2471
|
+
if (identity !== this._ensureReadStateIdentity())
|
|
2472
|
+
return;
|
|
2473
|
+
}
|
|
2474
|
+
this._dispatcher.enqueue('read_state_sync', {
|
|
2475
|
+
owner_aid: this._aid ?? '',
|
|
2476
|
+
states: [...keys].map((key) => ({ ...this._readStateCache.get(key).state })),
|
|
2477
|
+
errors,
|
|
2478
|
+
});
|
|
2479
|
+
}
|
|
1796
2480
|
async _callRawV2Rpc(method, params, pullGateKey = '') {
|
|
1797
2481
|
const p = { ...(params ?? {}) };
|
|
1798
2482
|
const forceForeground = Boolean(p._rpc_foreground);
|
|
@@ -1952,8 +2636,15 @@ export class AUNClient {
|
|
|
1952
2636
|
_markPublishedSeq(ns, seq) {
|
|
1953
2637
|
this._delivery.markPublishedSeq(ns, seq);
|
|
1954
2638
|
}
|
|
1955
|
-
_publishAppEvent(event, payload, source = 'direct') {
|
|
1956
|
-
|
|
2639
|
+
_publishAppEvent(event, payload, source = 'direct', deliveryContext) {
|
|
2640
|
+
if (event === 'message.received') {
|
|
2641
|
+
return this._handleOwnershipMessage(payload).then((handled) => {
|
|
2642
|
+
if (!handled)
|
|
2643
|
+
return this._delivery.publishAppEvent(event, payload, source, deliveryContext);
|
|
2644
|
+
return undefined;
|
|
2645
|
+
});
|
|
2646
|
+
}
|
|
2647
|
+
return this._delivery.publishAppEvent(event, payload, source, deliveryContext);
|
|
1957
2648
|
}
|
|
1958
2649
|
_echoTimestamp() {
|
|
1959
2650
|
const now = new Date();
|
|
@@ -2132,8 +2823,8 @@ export class AUNClient {
|
|
|
2132
2823
|
}
|
|
2133
2824
|
return true;
|
|
2134
2825
|
}
|
|
2135
|
-
async _drainOrderedMessages(ns, beforeSeq, pullResponse = false, persist = true, source = 'push') {
|
|
2136
|
-
return this._delivery.drainOrderedMessages(ns, beforeSeq, pullResponse, persist, source);
|
|
2826
|
+
async _drainOrderedMessages(ns, beforeSeq, pullResponse = false, persist = true, source = 'push', throughSeq) {
|
|
2827
|
+
return this._delivery.drainOrderedMessages(ns, beforeSeq, pullResponse, persist, source, undefined, throughSeq);
|
|
2137
2828
|
}
|
|
2138
2829
|
async _publishOrderedMessage(event, ns, seq, payload, source = 'push') {
|
|
2139
2830
|
return this._delivery.publishOrderedMessage(event, ns, seq, payload, source);
|
|
@@ -2322,7 +3013,10 @@ export class AUNClient {
|
|
|
2322
3013
|
}
|
|
2323
3014
|
}
|
|
2324
3015
|
try {
|
|
2325
|
-
|
|
3016
|
+
if (opts.signedAt === undefined)
|
|
3017
|
+
await this._auth.verifyPeerCertificate(peerGatewayUrl, certPem, aid);
|
|
3018
|
+
else
|
|
3019
|
+
await this._auth.verifyPeerCertificate(peerGatewayUrl, certPem, aid, opts.signedAt);
|
|
2326
3020
|
}
|
|
2327
3021
|
catch (exc) {
|
|
2328
3022
|
if (cachedBootstrapChain) {
|
|
@@ -2336,9 +3030,9 @@ export class AUNClient {
|
|
|
2336
3030
|
validatedAt: nowSec,
|
|
2337
3031
|
refreshAfter: nowSec + PEER_CERT_CACHE_TTL,
|
|
2338
3032
|
};
|
|
2339
|
-
const cacheKey = AUNClient._certCacheKey(aid, certFingerprint);
|
|
3033
|
+
const cacheKey = AUNClient._certCacheKey(aid, certFingerprint) + (opts.signedAt === undefined ? '' : `:signed:${opts.signedAt}`);
|
|
2340
3034
|
this._certCache.set(cacheKey, entry);
|
|
2341
|
-
if (!certFingerprint) {
|
|
3035
|
+
if (!certFingerprint && opts.signedAt === undefined) {
|
|
2342
3036
|
const bareKey = AUNClient._certCacheKey(aid);
|
|
2343
3037
|
this._certCache.set(bareKey, entry);
|
|
2344
3038
|
const actualFp = `sha256:${x509Cert.fingerprint256.replace(/:/g, '').toLowerCase()}`;
|
|
@@ -2353,12 +3047,19 @@ export class AUNClient {
|
|
|
2353
3047
|
}
|
|
2354
3048
|
return certPem;
|
|
2355
3049
|
}
|
|
3050
|
+
async _fetchServiceEventCertificate(aid, fingerprint) {
|
|
3051
|
+
const certificate = await this._fetchPeerCert(aid, fingerprint);
|
|
3052
|
+
if (!this._gatewayUrl)
|
|
3053
|
+
throw new ValidationError('gateway url unavailable for service event certificate validation');
|
|
3054
|
+
await this._auth.verifyPeerCertificate(AUNClient._resolvePeerGatewayUrl(this._gatewayUrl, aid), certificate, aid, undefined, true);
|
|
3055
|
+
return certificate;
|
|
3056
|
+
}
|
|
2356
3057
|
/** 获取对方证书(带缓存 + 完整 PKI 验证),跨域时自动路由到 peer 所在域。 */
|
|
2357
|
-
async _fetchPeerCert(aid, certFingerprint, timeoutMs = 30_000) {
|
|
3058
|
+
async _fetchPeerCert(aid, certFingerprint, timeoutMs = 30_000, signedAt) {
|
|
2358
3059
|
const tStart = Date.now();
|
|
2359
3060
|
this._clientLog.debug(`_fetchPeerCert enter: aid=${aid}, fp=${certFingerprint ?? ''}`);
|
|
2360
3061
|
try {
|
|
2361
|
-
const cacheKey = AUNClient._certCacheKey(aid, certFingerprint);
|
|
3062
|
+
const cacheKey = AUNClient._certCacheKey(aid, certFingerprint) + (signedAt === undefined ? '' : `:signed:${signedAt}`);
|
|
2362
3063
|
const cached = this._certCache.get(cacheKey);
|
|
2363
3064
|
const now = Date.now() / 1000;
|
|
2364
3065
|
if (cached && now < cached.refreshAfter) {
|
|
@@ -2376,6 +3077,7 @@ export class AUNClient {
|
|
|
2376
3077
|
certPem,
|
|
2377
3078
|
certFingerprint,
|
|
2378
3079
|
source: 'fetch',
|
|
3080
|
+
signedAt,
|
|
2379
3081
|
});
|
|
2380
3082
|
this._clientLog.debug(`_fetchPeerCert exit: elapsed=${Date.now() - tStart}ms aid=${aid} (fetched)`);
|
|
2381
3083
|
return validated;
|
|
@@ -2908,6 +3610,7 @@ export class AUNClient {
|
|
|
2908
3610
|
}
|
|
2909
3611
|
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
2910
3612
|
this._startBackgroundTasks();
|
|
3613
|
+
this._safeAsync(this._reconcileReadStates());
|
|
2911
3614
|
const connectionKind = String(params.connection_kind ?? 'long');
|
|
2912
3615
|
const isShortConnection = connectionKind === 'short';
|
|
2913
3616
|
const hasExplicitBackgroundSync = Object.prototype.hasOwnProperty.call(params, 'background_sync');
|
|
@@ -2934,7 +3637,7 @@ export class AUNClient {
|
|
|
2934
3637
|
this._clientLog.warn(`schedule post-connect P2P gap fill failed: ${formatCaughtError(exc)}`);
|
|
2935
3638
|
});
|
|
2936
3639
|
}
|
|
2937
|
-
this._safeAsync(this.
|
|
3640
|
+
this._safeAsync(this._resumeOwnershipAfterConnect());
|
|
2938
3641
|
this._clientLog.debug(`_connectOnce exit: elapsed=${Date.now() - tStart}ms gateway=${gatewayUrl}, aid=${this._aid ?? ''}`);
|
|
2939
3642
|
}
|
|
2940
3643
|
catch (err) {
|
|
@@ -3333,6 +4036,8 @@ export class AUNClient {
|
|
|
3333
4036
|
encrypted: true,
|
|
3334
4037
|
e2ee,
|
|
3335
4038
|
};
|
|
4039
|
+
if (typeof plaintext.type === 'string' && plaintext.type.startsWith('aun.ownership.'))
|
|
4040
|
+
result.from_aid = fromAid;
|
|
3336
4041
|
if (senderDeviceId)
|
|
3337
4042
|
result.sender_device_id = senderDeviceId;
|
|
3338
4043
|
const explicitDirection = String(msg.direction ?? '').trim();
|
|
@@ -3546,6 +4251,22 @@ export class AUNClient {
|
|
|
3546
4251
|
const gateways = await this._resolveGatewayCandidatesForAid(aid);
|
|
3547
4252
|
return gateways[0];
|
|
3548
4253
|
}
|
|
4254
|
+
async _resolveOwnershipCertificateGateway(aid) {
|
|
4255
|
+
const issuer = aid.slice(aid.indexOf('.') + 1);
|
|
4256
|
+
if (issuer === this._aid?.slice(this._aid.indexOf('.') + 1))
|
|
4257
|
+
return this._resolveGatewayForAid(aid);
|
|
4258
|
+
const port = this._configModel.discoveryPort ? `:${this._configModel.discoveryPort}` : '';
|
|
4259
|
+
let lastError;
|
|
4260
|
+
for (const host of [aid, `gateway.${issuer}`]) {
|
|
4261
|
+
try {
|
|
4262
|
+
return await this._discovery.discover(`https://${host}${port}/.well-known/aun-gateway`);
|
|
4263
|
+
}
|
|
4264
|
+
catch (error) {
|
|
4265
|
+
lastError = error;
|
|
4266
|
+
}
|
|
4267
|
+
}
|
|
4268
|
+
throw ownershipFailure('unavailable', lastError instanceof Error ? lastError.message : 'signer Gateway discovery failed', true);
|
|
4269
|
+
}
|
|
3549
4270
|
/** 发现并缓存全部 Gateway 候选,供首连和自动重连故障转移使用。 */
|
|
3550
4271
|
async _resolveGatewayCandidatesForAid(aid) {
|
|
3551
4272
|
const resolvedAid = String(aid ?? this._aid ?? '').trim();
|