@agentunion/fastaun 0.5.18 → 0.5.20
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 +172 -79
- package/_packed_docs/CHANGELOG.md +172 -79
- package/_packed_docs/INDEX.md +244 -116
- package/_packed_docs/KITE_DOCS_GUIDE.md +98 -69
- package/_packed_docs/agent.md/SCHEMA.md +245 -205
- package/_packed_docs/agent.md//350/277/234/347/250/213agent.md/347/274/223/345/255/230/344/270/216etag/351/200/217/344/274/240/346/226/271/346/241/210.md +11 -11
- 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/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1072 -1072
- 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 +687 -668
- package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +19 -28
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +443 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.0.md +161 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +658 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2113.0.md +233 -0
- package/_packed_docs/protocol/agent.md/SCHEMA.md +4 -0
- package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +1 -1
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +68 -61
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +1 -1
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +279 -285
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +3 -3
- package/_packed_docs/sdk/09-group-rpc-manual.md +271 -235
- package/_packed_docs/sdk/09-message-rpc-manual.md +47 -6
- package/_packed_docs/sdk/09-storage-rpc-manual.md +70 -68
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +63 -34
- package/_packed_docs/sdk/INDEX.md +105 -34
- package/_packed_docs/sdk/Ownership-SDK/345/256/236/347/216/260/346/214/207/345/215/227.md +169 -0
- package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/227.md +349 -0
- package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +922 -0
- package/_packed_docs/sdk/Ownership-SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +270 -0
- package/_packed_docs/sdk/Ownership2.1-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +300 -0
- package/_packed_docs/sdk/Ownership2.1-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +382 -0
- package/_packed_docs/sdk/Ownership2.1-TDD/345/210/206/351/230/266/346/256/265/345/256/236/346/226/275/350/256/241/345/210/222.md +708 -0
- package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +476 -0
- package/_packed_docs/sdk/Ownership/347/256/241/347/220/206/344/270/223/351/242/230.md +559 -0
- package/_packed_docs/sdk/ProtectedHeaders/347/224/250/346/263/225/344/270/223/351/242/230.md +299 -0
- package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +173 -0
- package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +211 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/345/256/236/347/216/260/347/247/273/346/244/215/346/214/207/345/215/227.md +331 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +207 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/344/273/243/347/240/201/345/256/241/346/237/245/346/212/245/345/221/212.md +252 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/345/256/236/346/226/275/350/256/241/345/210/222.md +156 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/346/226/271/346/241/210.md +273 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/350/257/246/347/273/206/350/256/276/350/256/241.md +211 -0
- package/dist/agent-md-schema.d.ts +15 -1
- package/dist/agent-md-schema.js +87 -24
- package/dist/agent-md-schema.js.map +1 -1
- package/dist/agent-md.d.ts +54 -2
- package/dist/agent-md.js +637 -53
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +38 -59
- package/dist/aid-store.js +300 -265
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.js +30 -11
- package/dist/aid.js.map +1 -1
- package/dist/auth.d.ts +6 -1
- package/dist/auth.js +500 -15
- package/dist/auth.js.map +1 -1
- package/dist/cert-utils.d.ts +44 -2
- package/dist/cert-utils.js +149 -49
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +11 -1
- package/dist/client/delivery.js +321 -62
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts +1 -0
- package/dist/client/rpc-pipeline.js +142 -82
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +1 -0
- package/dist/client/v2-e2ee.js +166 -96
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +84 -1
- package/dist/client.js +780 -30
- 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 +9 -3
- package/dist/events.js +69 -11
- package/dist/events.js.map +1 -1
- package/dist/facades.d.ts +83 -3
- package/dist/facades.js +100 -6
- package/dist/facades.js.map +1 -1
- package/dist/index.d.ts +9 -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 +57 -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 +1753 -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 +196 -0
- package/dist/ownership-v21.js +712 -0
- package/dist/ownership-v21.js.map +1 -0
- package/dist/ownership-workflow.d.ts +87 -0
- package/dist/ownership-workflow.js +1182 -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 +90 -1
- package/dist/tools/cross-sdk-agent.js +441 -7
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +8 -0
- package/dist/transport.js +118 -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 { validateAgentMdCertificate, 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,11 +396,51 @@ 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();
|
|
395
|
-
if (current?.aid === target) {
|
|
443
|
+
if (signedAt === undefined && current?.aid === target) {
|
|
396
444
|
if (!expectedFp || certMatchesFingerprint(current.certPem, expectedFp))
|
|
397
445
|
return current;
|
|
398
446
|
}
|
|
@@ -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), httpPut: (url, data, headers) => this.storage.lowlevel.httpPut(url, data, headers) });
|
|
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) => this._getOwnershipStorage().writeOwnerMd(ownerAid, subAid, raw),
|
|
500
|
+
deleteOwnerMd: (ownerAid, subAid) => this._getOwnershipStorage().deleteOwnerMd(ownerAid, subAid),
|
|
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 ?? '', () => this._ensureReadStateIdentity());
|
|
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, true),
|
|
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,10 @@ 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();
|
|
790
|
+
this._transport._onRpcResponse = () => this._delivery.requestBackgroundPullRecovery();
|
|
642
791
|
this._runtime = new ClientRuntime(this);
|
|
643
792
|
this._identityRuntime = new IdentityRuntimeManager(this._runtime);
|
|
644
793
|
this._peerDirectory = new PeerDirectory(this._runtime);
|
|
@@ -665,6 +814,7 @@ export class AUNClient {
|
|
|
665
814
|
this._dispatcher.subscribeProtocol('_raw.message.received', (data) => this._onRawMessageReceived(data));
|
|
666
815
|
// P2P 消息撤回推送:在线 push 与 pull tombstone 去重后再发布给应用层
|
|
667
816
|
this._dispatcher.subscribeProtocol('_raw.message.recalled', (data) => this._safeAsync(this._onRawMessageRecalled(data)));
|
|
817
|
+
this._dispatcher.subscribeProtocol('_raw.message.read_changed', (data) => this._onRawReadChanged(data, 'p2p'));
|
|
668
818
|
// V2 P2P 推送通知:收到通知后自动走 message.v2.pull 拉取并解密
|
|
669
819
|
this._dispatcher.subscribeProtocol('_raw.peer.v2.message_received', (data) => {
|
|
670
820
|
const ns = this._aid ? `p2p:${this._aid}` : '';
|
|
@@ -692,6 +842,7 @@ export class AUNClient {
|
|
|
692
842
|
this._dispatcher.subscribeProtocol('_raw.group.message_created', (data) => this._onRawGroupMessageCreated(data));
|
|
693
843
|
// 群组消息撤回推送:在线 push 通道,与 pull 双 tombstone 兜底互补(SDK 去重只回调一次)
|
|
694
844
|
this._dispatcher.subscribeProtocol('_raw.group.message_recalled', (data) => this._safeAsync(this._onRawGroupMessageRecalled(data)));
|
|
845
|
+
this._dispatcher.subscribeProtocol('_raw.group.read_changed', (data) => this._onRawReadChanged(data, 'group'));
|
|
695
846
|
// 群组变更事件:透传并触发 V2 state/SPK 维护
|
|
696
847
|
this._dispatcher.subscribeProtocol('_raw.group.changed', (data) => this._onRawGroupChanged(data));
|
|
697
848
|
// 定向邀请底层事件:按群 + 目标 AID 聚合后向应用层只提示一次。
|
|
@@ -724,6 +875,16 @@ export class AUNClient {
|
|
|
724
875
|
return sessionToken || null;
|
|
725
876
|
}
|
|
726
877
|
/** AUN Storage VFS 入口 */
|
|
878
|
+
get credential() {
|
|
879
|
+
if (!this._credential)
|
|
880
|
+
this._credential = new CredentialFacade(this);
|
|
881
|
+
return this._credential;
|
|
882
|
+
}
|
|
883
|
+
get org() {
|
|
884
|
+
if (!this._org)
|
|
885
|
+
this._org = new OrgFacade(this);
|
|
886
|
+
return this._org;
|
|
887
|
+
}
|
|
727
888
|
get storage() {
|
|
728
889
|
if (!this._storage)
|
|
729
890
|
this._storage = new StorageVFS(this);
|
|
@@ -757,6 +918,88 @@ export class AUNClient {
|
|
|
757
918
|
get currentAid() {
|
|
758
919
|
return this._currentAid;
|
|
759
920
|
}
|
|
921
|
+
_ownershipKey(subjectAid, ownerAid) {
|
|
922
|
+
return `${String(subjectAid ?? '').trim().toLowerCase()}|${String(ownerAid ?? '').trim().toLowerCase()}`;
|
|
923
|
+
}
|
|
924
|
+
getType() {
|
|
925
|
+
return String(this._ownershipType || 'agent').trim() || 'agent';
|
|
926
|
+
}
|
|
927
|
+
get_type() {
|
|
928
|
+
return this.getType();
|
|
929
|
+
}
|
|
930
|
+
getOwner() {
|
|
931
|
+
const aid = String(this._aid ?? '').trim().toLowerCase();
|
|
932
|
+
let best = null;
|
|
933
|
+
let bestActive = false;
|
|
934
|
+
for (const record of this._ownershipRecords.values()) {
|
|
935
|
+
if (String(record.subject_aid ?? '').trim().toLowerCase() === aid) {
|
|
936
|
+
if (String(record.status ?? '').trim().toLowerCase() === 'released')
|
|
937
|
+
continue;
|
|
938
|
+
const owner = String(record.owner_aid ?? '').trim();
|
|
939
|
+
if (!owner)
|
|
940
|
+
continue;
|
|
941
|
+
const active = String(record.status ?? '').trim().toLowerCase() === 'active';
|
|
942
|
+
if (!best || active && !bestActive) {
|
|
943
|
+
best = owner;
|
|
944
|
+
bestActive = active;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
return best;
|
|
949
|
+
}
|
|
950
|
+
get_owner() {
|
|
951
|
+
return this.getOwner();
|
|
952
|
+
}
|
|
953
|
+
getOwnershipStatus() {
|
|
954
|
+
const aid = String(this._aid ?? '').trim().toLowerCase();
|
|
955
|
+
let best = null;
|
|
956
|
+
let bestActive = false;
|
|
957
|
+
for (const record of this._ownershipRecords.values()) {
|
|
958
|
+
const subject = String(record.subject_aid ?? '').trim().toLowerCase();
|
|
959
|
+
const owner = String(record.owner_aid ?? '').trim().toLowerCase();
|
|
960
|
+
if (aid !== subject && aid !== owner)
|
|
961
|
+
continue;
|
|
962
|
+
const active = String(record.status ?? '').trim().toLowerCase() === 'active';
|
|
963
|
+
if (!best || active && !bestActive) {
|
|
964
|
+
best = record;
|
|
965
|
+
bestActive = active;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
if (best)
|
|
969
|
+
return Object.fromEntries(Object.entries(best).filter(([key]) => key !== 'subject_identity' && key !== 'escrow_password' && key !== 'escrow_download_url'));
|
|
970
|
+
return { subject_aid: this._aid ?? '', owner_aid: null, status: 'legacy_unbound', type: this.getType() };
|
|
971
|
+
}
|
|
972
|
+
get_ownership_status() {
|
|
973
|
+
return this.getOwnershipStatus();
|
|
974
|
+
}
|
|
975
|
+
async bindOwneraid(ownerAid) { return this._getOwnershipWorkflow().bindOwneraid(ownerAid); }
|
|
976
|
+
async bindSubaid(subAid) { return this._getOwnershipWorkflow().bindSubaid(subAid); }
|
|
977
|
+
async releaseSubaid(subAid) { return this._getOwnershipWorkflow().releaseSubaid(subAid); }
|
|
978
|
+
async transferSubaid(subAid, newOwnerAid) { return this._getOwnershipWorkflow().transferSubaid(subAid, newOwnerAid); }
|
|
979
|
+
async checkOwnershipConsensus(subAid) { return this._getOwnershipWorkflow().checkOwnershipConsensus(subAid); }
|
|
980
|
+
async bindAid(subjectAid, options = {}) {
|
|
981
|
+
throw ownershipFailure('unsupported_protocol', 'legacy bindAid is unsupported; use bindSubaid');
|
|
982
|
+
}
|
|
983
|
+
async bind_aid(subjectAid, options = {}) { return this.bindAid(subjectAid, options); }
|
|
984
|
+
async bindOwner(ownerAid, options = {}) {
|
|
985
|
+
throw ownershipFailure('unsupported_protocol', 'legacy bindOwner is unsupported; use bindOwneraid');
|
|
986
|
+
}
|
|
987
|
+
async bind_owner(ownerAid, options = {}) { return this.bindOwner(ownerAid, options); }
|
|
988
|
+
async releaseOwnership(subjectAid) { throw ownershipFailure('unsupported_protocol', 'legacy releaseOwnership is unsupported; use releaseSubaid'); }
|
|
989
|
+
async release_ownership(subjectAid) { return this.releaseOwnership(subjectAid); }
|
|
990
|
+
async acceptOwnershipRequest(operationId) {
|
|
991
|
+
return this._getOwnershipWorkflow().acceptOwnershipRequest(operationId);
|
|
992
|
+
}
|
|
993
|
+
async rejectOwnershipRequest(operationId, reason) {
|
|
994
|
+
return this._getOwnershipWorkflow().rejectOwnershipRequest(operationId, reason);
|
|
995
|
+
}
|
|
996
|
+
async _handleOwnershipMessage(message) {
|
|
997
|
+
if (!message || typeof message !== 'object' || Array.isArray(message))
|
|
998
|
+
return false;
|
|
999
|
+
const envelope = message;
|
|
1000
|
+
const content = 'payload' in envelope ? envelope.payload : message;
|
|
1001
|
+
return this._getOwnershipWorkflow().receive(String(envelope.from_aid ?? ''), decryptedOwnershipJsonText(content) ?? content);
|
|
1002
|
+
}
|
|
760
1003
|
get hasIdentity() {
|
|
761
1004
|
return this._currentAid !== null && this.state !== ConnectionState.CLOSED;
|
|
762
1005
|
}
|
|
@@ -886,6 +1129,9 @@ export class AUNClient {
|
|
|
886
1129
|
logger: this._logger.for('aun_core.keystore'),
|
|
887
1130
|
});
|
|
888
1131
|
this._tokenStore = tokenStore;
|
|
1132
|
+
this._ownershipStorage = null;
|
|
1133
|
+
this._ownershipFiles = null;
|
|
1134
|
+
this._ownershipWorkflow = null;
|
|
889
1135
|
this._auth = new AuthFlow({
|
|
890
1136
|
tokenStore,
|
|
891
1137
|
crypto: new CryptoProvider(),
|
|
@@ -914,7 +1160,10 @@ export class AUNClient {
|
|
|
914
1160
|
},
|
|
915
1161
|
auth: () => this._auth,
|
|
916
1162
|
tokenStore: () => this._tokenStore,
|
|
917
|
-
fetchPeerCert: (target, certFingerprint) => this._fetchPeerCert(target, certFingerprint || undefined),
|
|
1163
|
+
fetchPeerCert: (target, certFingerprint, signedAt) => this._fetchPeerCert(target, certFingerprint || undefined, undefined, signedAt, true),
|
|
1164
|
+
resolveEntityOwner: (target, type) => this._resolveEntityAgentMdOwner(target, type),
|
|
1165
|
+
fetchEntityIdentity: (target, type) => this._freshEntityCertificate(target, type),
|
|
1166
|
+
fetchEntitySigner: (target, fingerprint) => this._freshEntitySigner(target, fingerprint),
|
|
918
1167
|
});
|
|
919
1168
|
this._transport = new RPCTransport({
|
|
920
1169
|
eventDispatcher: this._dispatcher,
|
|
@@ -924,10 +1173,21 @@ export class AUNClient {
|
|
|
924
1173
|
dnsNet,
|
|
925
1174
|
});
|
|
926
1175
|
this._transport.setProtocolMetaObserver((meta) => this._observeRpcMeta(meta));
|
|
1176
|
+
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
1177
|
this._transport.setProtocolDisconnectCallback((err, closeCode) => this._handleTransportDisconnect(err, closeCode));
|
|
1178
|
+
this._transport._onForegroundDrained = () => this._delivery.onForegroundPullsDrained();
|
|
1179
|
+
this._transport._onRpcResponse = () => this._delivery.requestBackgroundPullRecovery();
|
|
928
1180
|
}
|
|
929
1181
|
loadIdentity(aid) {
|
|
1182
|
+
const previousAid = this._aid;
|
|
930
1183
|
this._identityRuntime.loadIdentity(aid);
|
|
1184
|
+
this._serviceEventIdentityVersion++;
|
|
1185
|
+
this._ensureReadStateIdentity();
|
|
1186
|
+
if (previousAid !== this._aid) {
|
|
1187
|
+
this._ownershipRecords.clear();
|
|
1188
|
+
this._ownershipType = 'agent';
|
|
1189
|
+
}
|
|
1190
|
+
this._restoreOwnershipRecords();
|
|
931
1191
|
}
|
|
932
1192
|
setProtectedHeaders(headers) {
|
|
933
1193
|
if (!headers) {
|
|
@@ -1006,7 +1266,7 @@ export class AUNClient {
|
|
|
1006
1266
|
}
|
|
1007
1267
|
/** 连接到 Gateway;身份来自构造函数或 loadIdentity(aid),认证由 SDK 内部自动完成。 */
|
|
1008
1268
|
async connect(opts) {
|
|
1009
|
-
|
|
1269
|
+
await this._lifecycle.connect(opts);
|
|
1010
1270
|
}
|
|
1011
1271
|
/** 关闭连接 */
|
|
1012
1272
|
async close() {
|
|
@@ -1025,8 +1285,13 @@ export class AUNClient {
|
|
|
1025
1285
|
* 自动处理内部方法限制、E2EE 加解密、客户端签名等。
|
|
1026
1286
|
*/
|
|
1027
1287
|
async call(method, params) {
|
|
1288
|
+
const readIdentity = ['message.read', 'message.read_state', 'group.read', 'group.read_state'].includes(method)
|
|
1289
|
+
? this._ensureReadStateIdentity() : null;
|
|
1028
1290
|
try {
|
|
1029
|
-
|
|
1291
|
+
const result = await this._rpcPipeline.call(method, params);
|
|
1292
|
+
if (readIdentity !== null && readIdentity === this._ensureReadStateIdentity())
|
|
1293
|
+
this._observeReadStateResult(method, result, params ?? {});
|
|
1294
|
+
return result;
|
|
1030
1295
|
}
|
|
1031
1296
|
catch (err) {
|
|
1032
1297
|
await this._maybeHandleHalfOpenRpcFailure(err);
|
|
@@ -1070,6 +1335,9 @@ export class AUNClient {
|
|
|
1070
1335
|
const loaded = store.load(groupAid);
|
|
1071
1336
|
if (!loaded.ok || !loaded.data?.aid?.isPrivateKeyValid())
|
|
1072
1337
|
return false;
|
|
1338
|
+
const groupManager = store._agentMdManager ?? this._agentMdManager;
|
|
1339
|
+
if (String((groupManager._loadRecord?.(groupAid, true) ?? groupManager.loadRecord(groupAid))?.local_draft_content ?? ''))
|
|
1340
|
+
return false;
|
|
1073
1341
|
await this._runGroupIdentityOperation(groupAid, () => this._uploadGroupAgentMd(store, groupAid, {}, {}));
|
|
1074
1342
|
return true;
|
|
1075
1343
|
}
|
|
@@ -1079,12 +1347,23 @@ export class AUNClient {
|
|
|
1079
1347
|
const target = message.slice(agentPrefix.length).trim();
|
|
1080
1348
|
if (!target || target.toLowerCase() !== String(this._aid ?? '').trim().toLowerCase())
|
|
1081
1349
|
return false;
|
|
1082
|
-
|
|
1350
|
+
return this._ensureInitialAgentMd();
|
|
1351
|
+
}
|
|
1352
|
+
async _ensureInitialAgentMd() {
|
|
1353
|
+
const aid = this._aid;
|
|
1354
|
+
if (!aid || !this._currentAid)
|
|
1355
|
+
return false;
|
|
1356
|
+
const observed = await this._getOwnershipFiles().readAgent(aid);
|
|
1357
|
+
if (observed.exists || String(this._agentMdManager._loadRecord?.(aid, true)?.local_draft_content ?? this._agentMdManager.loadRecord(aid)?.local_draft_content ?? ''))
|
|
1358
|
+
return false;
|
|
1359
|
+
let agentMd = buildDefaultAgentMd(aid, { type: this.getType() });
|
|
1083
1360
|
try {
|
|
1084
|
-
agentMd = this._agentMdManager.readContent(
|
|
1361
|
+
agentMd = this._agentMdManager.readContent(aid) || agentMd;
|
|
1085
1362
|
}
|
|
1086
1363
|
catch { }
|
|
1087
|
-
|
|
1364
|
+
const payload = updateOwnershipAgentMd(agentMd, {});
|
|
1365
|
+
const raw = signOwnershipFile(this._currentAid, payload.endsWith('\n') || payload.endsWith('\r') ? payload : `${payload}\n`, 'agent');
|
|
1366
|
+
await this._agentMdManager.submitOwnershipRaw(aid, raw, { if_none_match: '*' });
|
|
1088
1367
|
return true;
|
|
1089
1368
|
}
|
|
1090
1369
|
async _checkIdentityAdmissionAfterConnect() {
|
|
@@ -1092,10 +1371,12 @@ export class AUNClient {
|
|
|
1092
1371
|
if (!aid)
|
|
1093
1372
|
return;
|
|
1094
1373
|
try {
|
|
1095
|
-
const checked = await this._agentMdManager.check(aid);
|
|
1374
|
+
const checked = await this._agentMdManager.check(aid, 0);
|
|
1096
1375
|
if (!checked.remote_found) {
|
|
1097
|
-
|
|
1098
|
-
|
|
1376
|
+
await this._ensureInitialAgentMd();
|
|
1377
|
+
}
|
|
1378
|
+
else if (checked.in_sync === false) {
|
|
1379
|
+
await this._agentMdManager.download(aid);
|
|
1099
1380
|
}
|
|
1100
1381
|
}
|
|
1101
1382
|
catch (exc) {
|
|
@@ -1104,6 +1385,7 @@ export class AUNClient {
|
|
|
1104
1385
|
const store = this._aidStore;
|
|
1105
1386
|
if (!store)
|
|
1106
1387
|
return;
|
|
1388
|
+
await this._resumeOrgIdentityAfterConnect(store);
|
|
1107
1389
|
let listed;
|
|
1108
1390
|
try {
|
|
1109
1391
|
listed = await this.call('group.list_my', {});
|
|
@@ -1135,11 +1417,18 @@ export class AUNClient {
|
|
|
1135
1417
|
await this.completeGroupTransfer({ group_id: groupId, group_aid: groupAid }, { aidStore: store });
|
|
1136
1418
|
return;
|
|
1137
1419
|
}
|
|
1420
|
+
const groupManager = store._agentMdManager ?? this._agentMdManager;
|
|
1421
|
+
if (String((groupManager._loadRecord?.(groupAid, true) ?? groupManager.loadRecord(groupAid))?.local_draft_content ?? '')) {
|
|
1422
|
+
await this._agentMdManager.check(groupAid);
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1138
1425
|
const loaded = store.load(groupAid);
|
|
1139
1426
|
if (!loaded.ok || !loaded.data?.aid?.isPrivateKeyValid()) {
|
|
1140
1427
|
await this.bindGroupAid({ group_id: groupId, group_aid: groupAid }, { aidStore: store });
|
|
1141
1428
|
return;
|
|
1142
1429
|
}
|
|
1430
|
+
if (await this._resumeGroupAgentMdUpload(store, groupAid))
|
|
1431
|
+
return;
|
|
1143
1432
|
const checked = await this._agentMdManager.check(groupAid);
|
|
1144
1433
|
if (!checked.remote_found) {
|
|
1145
1434
|
await this._uploadGroupAgentMd(store, groupAid, {}, {});
|
|
@@ -1170,6 +1459,204 @@ export class AUNClient {
|
|
|
1170
1459
|
this._groupIdentityOperations.delete(key);
|
|
1171
1460
|
}
|
|
1172
1461
|
}
|
|
1462
|
+
async createOrg(params = {}, options = {}) {
|
|
1463
|
+
const store = options.aidStore;
|
|
1464
|
+
const owner = this._currentAid;
|
|
1465
|
+
const orgAid = String(params.org_id ?? '').trim();
|
|
1466
|
+
if (!store || !owner?.isPrivateKeyValid() || !orgAid)
|
|
1467
|
+
throw new ValidationError('createOrg requires aidStore, org_id and current owner identity');
|
|
1468
|
+
this._aidStore = store;
|
|
1469
|
+
let identity = store.load(orgAid);
|
|
1470
|
+
if (!identity.ok) {
|
|
1471
|
+
const registered = await store.register(orgAid, { type: 'org', owner: owner.aid });
|
|
1472
|
+
if (!registered.ok)
|
|
1473
|
+
throw ownershipFailure('unavailable', registered.error.message, true);
|
|
1474
|
+
identity = store.load(orgAid);
|
|
1475
|
+
}
|
|
1476
|
+
if (!identity.ok)
|
|
1477
|
+
throw ownershipFailure('unavailable', identity.error.message, true);
|
|
1478
|
+
const target = options.serviceAid ?? `org.${orgAid.slice(orgAid.indexOf('.') + 1)}`;
|
|
1479
|
+
const tokens = store._tokenStore;
|
|
1480
|
+
const inputHash = crypto.createHash('sha256').update(canonicalJson(params)).digest('hex');
|
|
1481
|
+
let checkpoint = tokens.loadMetadata(orgAid)?.org_identity_creation;
|
|
1482
|
+
if (checkpoint && (checkpoint.owner_aid !== owner.aid || checkpoint.input_hash !== inputHash))
|
|
1483
|
+
throw ownershipFailure('conflict', 'pending Org creation context changed');
|
|
1484
|
+
if (!checkpoint) {
|
|
1485
|
+
const prepared = this.org.prepareRequest('org.lifecycle.create', { ...params, owner_aid: owner.aid }, target, owner, { orgSigner: identity.data.aid });
|
|
1486
|
+
checkpoint = { owner_aid: owner.aid, input_hash: inputHash, source_params: params, prepared, phase: 'create' };
|
|
1487
|
+
tokens.saveMetadata(orgAid, { org_identity_creation: checkpoint });
|
|
1488
|
+
}
|
|
1489
|
+
if (checkpoint.phase === 'create') {
|
|
1490
|
+
const result = await this.call('org.lifecycle.create', checkpoint.prepared);
|
|
1491
|
+
checkpoint = { ...checkpoint, result, phase: 'upload' };
|
|
1492
|
+
tokens.saveMetadata(orgAid, { org_identity_creation: checkpoint });
|
|
1493
|
+
}
|
|
1494
|
+
await this._uploadOrgAgentMd(store, orgAid, params);
|
|
1495
|
+
tokens.saveMetadata(orgAid, { org_identity_creation: null });
|
|
1496
|
+
return checkpoint.result;
|
|
1497
|
+
}
|
|
1498
|
+
async _resumeOrgIdentityAfterConnect(store) {
|
|
1499
|
+
const keyStore = store._keystore;
|
|
1500
|
+
const tokens = store._tokenStore;
|
|
1501
|
+
if (typeof keyStore?.listIdentities !== 'function' || typeof tokens?.loadMetadata !== 'function')
|
|
1502
|
+
return;
|
|
1503
|
+
for (const aid of keyStore.listIdentities()) {
|
|
1504
|
+
const metadata = tokens.loadMetadata(aid);
|
|
1505
|
+
const creation = metadata?.org_identity_creation;
|
|
1506
|
+
const transfer = metadata?.org_identity_transfer;
|
|
1507
|
+
try {
|
|
1508
|
+
if (creation?.owner_aid === this._aid) {
|
|
1509
|
+
await this.createOrg(creation.source_params, { aidStore: store });
|
|
1510
|
+
continue;
|
|
1511
|
+
}
|
|
1512
|
+
if (transfer?.owner_aid === this._aid) {
|
|
1513
|
+
await this.completeOrgTransfer(transfer.source_params, { aidStore: store });
|
|
1514
|
+
continue;
|
|
1515
|
+
}
|
|
1516
|
+
const orgManager = store._agentMdManager;
|
|
1517
|
+
if (String((orgManager?._loadRecord?.(aid, true) ?? orgManager?.loadRecord?.(aid))?.local_draft_content ?? ''))
|
|
1518
|
+
continue;
|
|
1519
|
+
const pending = store._agentMdManager.pendingUpload(aid);
|
|
1520
|
+
if (!pending)
|
|
1521
|
+
continue;
|
|
1522
|
+
const fields = validateAgentMdDocument(pending.content, { expectedAid: aid }).frontmatter;
|
|
1523
|
+
if (fields.type === 'org' && fields.owner === this._aid)
|
|
1524
|
+
await this._resumeGroupAgentMdUpload(store, aid);
|
|
1525
|
+
}
|
|
1526
|
+
catch (error) {
|
|
1527
|
+
this._clientLog.warn(`post-connect Org identity recovery failed: aid=${aid} err=${formatCaughtError(error)}`);
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
async completeOrgTransfer(params = {}, options = {}) {
|
|
1532
|
+
const store = options.aidStore;
|
|
1533
|
+
const owner = this._currentAid;
|
|
1534
|
+
const orgAid = String(params.org_id ?? '').trim();
|
|
1535
|
+
const transferId = String(params.transfer_id ?? '').trim();
|
|
1536
|
+
if (!store || !owner?.isPrivateKeyValid() || !orgAid || !transferId)
|
|
1537
|
+
throw new ValidationError('completeOrgTransfer requires aidStore, org_id, transfer_id and current accepting owner');
|
|
1538
|
+
for (const field of ['previous_owner', 'new_owner']) {
|
|
1539
|
+
const material = params[field];
|
|
1540
|
+
if (!isJsonObject(material) || !isJsonObject(material.membership_binding) || !isJsonObject(material.terms) || field === 'previous_owner' && !isJsonObject(material.credential_template))
|
|
1541
|
+
throw ownershipFailure('invalid', 'Org transfer requires both replacement membership bindings, terms and previous-owner credential');
|
|
1542
|
+
}
|
|
1543
|
+
this._aidStore = store;
|
|
1544
|
+
const localStore = store._keystore;
|
|
1545
|
+
const tokenStore = store._tokenStore;
|
|
1546
|
+
const pendingKey = `org-transfer:${orgAid}:${transferId}`;
|
|
1547
|
+
const inputHash = crypto.createHash('sha256').update(canonicalJson(params)).digest('hex');
|
|
1548
|
+
let pending = tokenStore.loadMetadata(orgAid)?.org_identity_transfer;
|
|
1549
|
+
if (pending && (pending.transfer_id !== transferId || pending.owner_aid !== owner.aid || pending.input_hash !== inputHash))
|
|
1550
|
+
throw ownershipFailure('conflict', 'pending Org transfer context changed');
|
|
1551
|
+
let keyPair = localStore.loadPendingGroupBind(pendingKey);
|
|
1552
|
+
if (!keyPair) {
|
|
1553
|
+
if (pending)
|
|
1554
|
+
throw ownershipFailure('persistence_unavailable', 'Org transfer private key checkpoint is unavailable');
|
|
1555
|
+
keyPair = new CryptoProvider().generateIdentity();
|
|
1556
|
+
localStore.savePendingGroupBind(pendingKey, keyPair);
|
|
1557
|
+
}
|
|
1558
|
+
const target = options.serviceAid ?? `org.${orgAid.slice(orgAid.indexOf('.') + 1)}`;
|
|
1559
|
+
const publicKey = crypto.createPublicKey({ key: Buffer.from(keyPair.public_key_der_b64, 'base64'), format: 'der', type: 'spki' }).export({ format: 'pem', type: 'spki' }).toString();
|
|
1560
|
+
if (pending && pending.public_key !== publicKey)
|
|
1561
|
+
throw ownershipFailure('conflict', 'pending Org transfer context changed');
|
|
1562
|
+
if (!pending) {
|
|
1563
|
+
const rekey = { ...params, stage: 'rekey', public_key: publicKey, curve: 'P-256' };
|
|
1564
|
+
delete rekey.new_owner;
|
|
1565
|
+
delete rekey.request_binding;
|
|
1566
|
+
delete rekey.client_signature;
|
|
1567
|
+
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) };
|
|
1568
|
+
tokenStore.saveMetadata(orgAid, { org_identity_transfer: pending });
|
|
1569
|
+
}
|
|
1570
|
+
if (pending.phase === 'rekey') {
|
|
1571
|
+
const result = await this.call('org.lifecycle.transferAccept', pending.rekey_params);
|
|
1572
|
+
pending = { ...pending, phase: 'finalize', rekey_result: result };
|
|
1573
|
+
tokenStore.saveMetadata(orgAid, { org_identity_transfer: pending });
|
|
1574
|
+
}
|
|
1575
|
+
const rekeyResult = pending.rekey_result;
|
|
1576
|
+
const certPem = String(rekeyResult?.cert_pem ?? rekeyResult?.cert ?? '').trim();
|
|
1577
|
+
if (!certPem)
|
|
1578
|
+
throw ownershipFailure('unavailable', 'Org rekey response has no certificate', true);
|
|
1579
|
+
const imported = store.importGroupIdentity(orgAid, { ...keyPair, cert_pem: certPem });
|
|
1580
|
+
if (!imported.ok)
|
|
1581
|
+
throw ownershipFailure('invalid', imported.error.message);
|
|
1582
|
+
const orgIdentity = this._localEntityCertificate(orgAid);
|
|
1583
|
+
if (!pending.finalize_params) {
|
|
1584
|
+
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') };
|
|
1585
|
+
delete final.request_binding;
|
|
1586
|
+
delete final.client_signature;
|
|
1587
|
+
for (const field of ['previous_owner', 'new_owner']) {
|
|
1588
|
+
const material = isJsonObject(final[field]) ? final[field] : {};
|
|
1589
|
+
let binding = isJsonObject(material.membership_binding) ? material.membership_binding : undefined;
|
|
1590
|
+
if (!binding || !isJsonObject(material.terms))
|
|
1591
|
+
throw ownershipFailure('invalid', 'Org transfer requires both replacement membership bindings and terms');
|
|
1592
|
+
if (!binding.member_signature && binding.member_aid === owner.aid)
|
|
1593
|
+
binding = signMembershipBinding(binding, owner, 'member');
|
|
1594
|
+
binding = signMembershipBinding(binding, orgIdentity, 'org');
|
|
1595
|
+
const oldCredential = isJsonObject(material.credential_template) ? { ...material.credential_template } : undefined;
|
|
1596
|
+
if (oldCredential) {
|
|
1597
|
+
delete oldCredential.signature;
|
|
1598
|
+
oldCredential.key_version = orgIdentity.certFingerprint;
|
|
1599
|
+
}
|
|
1600
|
+
const credential = oldCredential ? createCredential(oldCredential, orgIdentity) : createMemberCredential(binding, orgIdentity, { key_version: orgIdentity.certFingerprint });
|
|
1601
|
+
final[field] = { ...material, membership_binding: binding, credential_template: credential };
|
|
1602
|
+
delete final[field].registration_authorization;
|
|
1603
|
+
}
|
|
1604
|
+
pending = { ...pending, finalize_params: this.org.prepareRequest('org.lifecycle.transferAccept', final, target, owner, { orgSigner: orgIdentity }) };
|
|
1605
|
+
tokenStore.saveMetadata(orgAid, { org_identity_transfer: pending });
|
|
1606
|
+
}
|
|
1607
|
+
const result = await this.call('org.lifecycle.transferAccept', pending.finalize_params);
|
|
1608
|
+
await this._uploadOrgAgentMd(store, orgAid, pending.source_params);
|
|
1609
|
+
tokenStore.saveMetadata(orgAid, { org_identity_transfer: null });
|
|
1610
|
+
localStore.clearPendingGroupBind(pendingKey);
|
|
1611
|
+
return result;
|
|
1612
|
+
}
|
|
1613
|
+
async dissolveOrg(params = {}, options = {}) {
|
|
1614
|
+
const store = options.aidStore ?? this._aidStore;
|
|
1615
|
+
if (store)
|
|
1616
|
+
this._aidStore = store;
|
|
1617
|
+
const orgAid = String(params.org_id ?? '').trim();
|
|
1618
|
+
const owner = this._currentAid;
|
|
1619
|
+
const target = options.serviceAid ?? `org.${orgAid.slice(orgAid.indexOf('.') + 1)}`;
|
|
1620
|
+
const payload = owner ? this.org.prepareRequest('org.lifecycle.dissolve', params, target, owner) : params;
|
|
1621
|
+
const result = await this.call('org.lifecycle.dissolve', payload);
|
|
1622
|
+
const organization = isJsonObject(result?.organization) ? result.organization : {};
|
|
1623
|
+
const status = String(organization.status ?? result?.status ?? '');
|
|
1624
|
+
if (store && orgAid && status === 'dissolved') {
|
|
1625
|
+
const tokenStore = store._tokenStore;
|
|
1626
|
+
const keyStore = store._keystore;
|
|
1627
|
+
const certPem = keyStore?.loadCert?.(orgAid);
|
|
1628
|
+
const metadata = tokenStore?.loadMetadata?.(orgAid) ?? {};
|
|
1629
|
+
const transfer = metadata.org_identity_transfer;
|
|
1630
|
+
if (transfer?.transfer_id && typeof keyStore?.clearPendingGroupBind === 'function') {
|
|
1631
|
+
keyStore.clearPendingGroupBind(`org-transfer:${orgAid}:${String(transfer.transfer_id)}`);
|
|
1632
|
+
}
|
|
1633
|
+
tokenStore?.saveMetadata?.(orgAid, {
|
|
1634
|
+
...(certPem ? { invalidated_entity_certificate: certFingerprint(certPem) } : {}),
|
|
1635
|
+
org_identity_creation: undefined,
|
|
1636
|
+
org_identity_transfer: undefined,
|
|
1637
|
+
});
|
|
1638
|
+
tokenStore?.saveInstanceState?.(orgAid, store.deviceId, store.slotId, {});
|
|
1639
|
+
store._agentMdManager?.saveRecord?.(orgAid, {
|
|
1640
|
+
pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '',
|
|
1641
|
+
pending_condition: {}, pending_relation: {}, pending_authorization: '',
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
return result;
|
|
1645
|
+
}
|
|
1646
|
+
async _uploadOrgAgentMd(store, orgAid, params) {
|
|
1647
|
+
const owner = this._currentAid;
|
|
1648
|
+
if (!owner)
|
|
1649
|
+
throw ownershipFailure('forbidden', 'Org agent.md requires current owner');
|
|
1650
|
+
if (await this._resumeGroupAgentMdUpload(store, orgAid))
|
|
1651
|
+
return;
|
|
1652
|
+
const identity = this._localEntityCertificate(orgAid);
|
|
1653
|
+
const content = String(params.org_agent_md ?? buildDefaultAgentMd(orgAid, { type: 'org', owner: owner.aid, entityCertFingerprint: identity.certFingerprint, name: String(params.name ?? orgAid) }));
|
|
1654
|
+
const uploaded = await store.uploadAgentMd(orgAid, content, { uploaderAid: owner.aid, entityOwnerResolver: (aid, type) => this._resolveEntityAgentMdOwner(aid, type), entityIdentityGetter: (aid) => this._freshEntityCertificate(aid, 'org') });
|
|
1655
|
+
if (!uploaded.ok) {
|
|
1656
|
+
const code = ['invalid', 'unavailable', 'forbidden', 'conflict', 'persistence_unavailable'].includes(uploaded.error.code) ? uploaded.error.code : uploaded.error.code === 'NETWORK_ERROR' ? 'unavailable' : 'invalid';
|
|
1657
|
+
throw ownershipFailure(code, uploaded.error.message, code === 'unavailable');
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1173
1660
|
async createGroup(params = {}, options = {}) {
|
|
1174
1661
|
if (!isJsonObject(params)) {
|
|
1175
1662
|
throw new ValidationError('createGroup params must be an object');
|
|
@@ -1338,6 +1825,8 @@ export class AUNClient {
|
|
|
1338
1825
|
`name: ${JSON.stringify(name)}`,
|
|
1339
1826
|
'type: "group"',
|
|
1340
1827
|
'version: "1.0.0"',
|
|
1828
|
+
`owner: ${JSON.stringify(String(group.owner_aid ?? group.owner ?? this._aid ?? ''))}`,
|
|
1829
|
+
`entity_cert_fingerprint: ${JSON.stringify(this._localEntityCertificate(groupAid).certFingerprint)}`,
|
|
1341
1830
|
`description: ${JSON.stringify(description)}`,
|
|
1342
1831
|
...(avatar ? [`avatar: ${JSON.stringify(avatar)}`] : []),
|
|
1343
1832
|
'---',
|
|
@@ -1346,16 +1835,71 @@ export class AUNClient {
|
|
|
1346
1835
|
'',
|
|
1347
1836
|
].join('\n');
|
|
1348
1837
|
}
|
|
1838
|
+
async _resumeGroupAgentMdUpload(store, groupAid) {
|
|
1839
|
+
const identityStore = store;
|
|
1840
|
+
const pendingManager = identityStore._agentMdManager ?? this._agentMdManager;
|
|
1841
|
+
const pending = pendingManager.pendingUpload(groupAid);
|
|
1842
|
+
if (!pending)
|
|
1843
|
+
return false;
|
|
1844
|
+
const fields = validateAgentMdDocument(pending.content, { expectedAid: groupAid, validateOwnership: false }).frontmatter;
|
|
1845
|
+
if (['ownership', 'ownership_proof', 'ownership_release'].some((field) => Object.hasOwn(fields, field))) {
|
|
1846
|
+
throw new StateError('ownership pending agent.md requires its persisted 2.1 task', { stringCode: 'unsupported_protocol' });
|
|
1847
|
+
}
|
|
1848
|
+
const identity = this._currentAid;
|
|
1849
|
+
if (!identity || fields.owner !== identity.aid)
|
|
1850
|
+
throw ownershipFailure('forbidden', 'group agent.md recovery requires its original current owner');
|
|
1851
|
+
const manager = new AgentMdManager({
|
|
1852
|
+
aunPath: store.aunPath,
|
|
1853
|
+
verifySsl: identityStore._verifySsl,
|
|
1854
|
+
logger: identityStore._log.for('aun_core.agent_md'),
|
|
1855
|
+
uploadViaGatewayHost: true,
|
|
1856
|
+
ownerAidGetter: () => identity.aid,
|
|
1857
|
+
currentAidGetter: () => identity,
|
|
1858
|
+
entityOwnerResolver: (aid, type) => this._resolveEntityAgentMdOwner(aid, type),
|
|
1859
|
+
entityIdentityGetter: (aid, type) => this._freshEntityCertificate(aid, type),
|
|
1860
|
+
entitySignerResolver: (aid, fingerprint) => this._freshEntitySigner(aid, fingerprint),
|
|
1861
|
+
gatewayResolver: (aid) => identityStore._resolveGateway(aid),
|
|
1862
|
+
accessTokenResolver: (_aid, gatewayUrl) => identityStore._uploadAgentMdToken(identity, gatewayUrl),
|
|
1863
|
+
aidValidator: (aid) => identityStore._registerFlow.validateAidName(aid),
|
|
1864
|
+
});
|
|
1865
|
+
return await manager.resumePendingUpload(groupAid);
|
|
1866
|
+
}
|
|
1349
1867
|
async _uploadGroupAgentMd(store, groupAid, params, group) {
|
|
1350
1868
|
const uploaderAid = String(this._aid ?? '').trim();
|
|
1351
1869
|
if (!uploaderAid) {
|
|
1352
1870
|
throw new ValidationError('group agent.md upload requires current owner AID');
|
|
1353
1871
|
}
|
|
1354
|
-
|
|
1872
|
+
this._aidStore = store;
|
|
1873
|
+
if (await this._resumeGroupAgentMdUpload(store, groupAid))
|
|
1874
|
+
return;
|
|
1875
|
+
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
1876
|
if (!uploaded.ok) {
|
|
1356
|
-
|
|
1877
|
+
const code = ['invalid', 'unavailable', 'forbidden', 'conflict', 'persistence_unavailable'].includes(uploaded.error.code) ? uploaded.error.code : uploaded.error.code === 'NETWORK_ERROR' ? 'unavailable' : 'invalid';
|
|
1878
|
+
throw ownershipFailure(code, `group agent.md upload failed: ${uploaded.error.message}`, code === 'unavailable');
|
|
1357
1879
|
}
|
|
1358
1880
|
}
|
|
1881
|
+
_localEntityCertificate(aid) {
|
|
1882
|
+
const loaded = this._aidStore?.load(aid);
|
|
1883
|
+
if (!loaded?.ok)
|
|
1884
|
+
throw ownershipFailure('unavailable', 'entity local certificate is unavailable', true);
|
|
1885
|
+
return loaded.data.aid;
|
|
1886
|
+
}
|
|
1887
|
+
async _freshEntityCertificate(aid, type = 'group') {
|
|
1888
|
+
const material = await this._auth.resolveCurrentEntityCertificate(await this._resolveOwnershipCertificateGateway(aid), aid, type);
|
|
1889
|
+
const certPem = material.cert_pem;
|
|
1890
|
+
return AID._create({ aid, aunPath: this._configModel.aunPath, certPem, certValid: true, privateKeyPem: null, privateKeyValid: false });
|
|
1891
|
+
}
|
|
1892
|
+
async _freshEntitySigner(aid, fingerprint) {
|
|
1893
|
+
const material = await this._auth.resolveOwnershipCertificate(await this._resolveOwnershipCertificateGateway(aid), aid, fingerprint, 'signing');
|
|
1894
|
+
return AID._create({ aid, aunPath: this._configModel.aunPath, certPem: material.cert_pem, certValid: true, privateKeyPem: null, privateKeyValid: false });
|
|
1895
|
+
}
|
|
1896
|
+
async _resolveEntityAgentMdOwner(aid, type) {
|
|
1897
|
+
const authority = await this._agentMdManager.readEntityAuthorityFresh(aid);
|
|
1898
|
+
const identity = await this._freshEntityCertificate(aid, type);
|
|
1899
|
+
if (authority.entity_cert_fingerprint !== identity.certFingerprint)
|
|
1900
|
+
throw ownershipFailure('conflict', 'entity authority changed during current-certificate verification');
|
|
1901
|
+
return authority.owner_aid;
|
|
1902
|
+
}
|
|
1359
1903
|
async bindGroupAid(params = {}, options = {}) {
|
|
1360
1904
|
if (!isJsonObject(params)) {
|
|
1361
1905
|
throw new ValidationError('bindGroupAid params must be an object');
|
|
@@ -1793,6 +2337,169 @@ export class AUNClient {
|
|
|
1793
2337
|
this._clientLog.debug(`on exit: elapsed=${Date.now() - tStart}ms event=${event}`);
|
|
1794
2338
|
return result;
|
|
1795
2339
|
}
|
|
2340
|
+
_readStateIsNewer(previous, current) {
|
|
2341
|
+
if (!previous)
|
|
2342
|
+
return true;
|
|
2343
|
+
const oldOrder = BigInt(String(previous.last_read_order ?? 0));
|
|
2344
|
+
const newOrder = BigInt(String(current.last_read_order ?? 0));
|
|
2345
|
+
if (oldOrder !== newOrder)
|
|
2346
|
+
return newOrder > oldOrder;
|
|
2347
|
+
const oldId = String(previous.last_read_message_id ?? '');
|
|
2348
|
+
const newId = String(current.last_read_message_id ?? '');
|
|
2349
|
+
if (oldOrder === 0n)
|
|
2350
|
+
return newId === oldId;
|
|
2351
|
+
const oldPoints = Array.from(oldId);
|
|
2352
|
+
const newPoints = Array.from(newId);
|
|
2353
|
+
for (let index = 0; index < Math.min(oldPoints.length, newPoints.length); index++) {
|
|
2354
|
+
const oldPoint = oldPoints[index].codePointAt(0);
|
|
2355
|
+
const newPoint = newPoints[index].codePointAt(0);
|
|
2356
|
+
if (oldPoint !== newPoint)
|
|
2357
|
+
return newPoint > oldPoint;
|
|
2358
|
+
}
|
|
2359
|
+
return newPoints.length >= oldPoints.length;
|
|
2360
|
+
}
|
|
2361
|
+
_ensureReadStateIdentity() {
|
|
2362
|
+
const identity = `${this._aid ?? ''}\u0000${this._deviceId ?? ''}`;
|
|
2363
|
+
if (identity !== this._readStateIdentity) {
|
|
2364
|
+
this._readStateCache.clear();
|
|
2365
|
+
this._readStateQueries.clear();
|
|
2366
|
+
this._readStateIdentity = identity;
|
|
2367
|
+
this._readStateGeneration += 1;
|
|
2368
|
+
}
|
|
2369
|
+
return `${identity}\u0000${this._readStateGeneration}`;
|
|
2370
|
+
}
|
|
2371
|
+
_registerReadStateQuery(method, params) {
|
|
2372
|
+
const key = method + '\u0000' + JSON.stringify(Object.keys(params).sort().map((field) => [field, params[field]]));
|
|
2373
|
+
this._readStateQueries.set(key, { method, params: { ...params } });
|
|
2374
|
+
}
|
|
2375
|
+
_readStateItems(result) {
|
|
2376
|
+
const resultObject = isJsonObject(result) ? result : null;
|
|
2377
|
+
return Array.isArray(resultObject?.states)
|
|
2378
|
+
? resultObject.states.filter((item) => isJsonObject(item))
|
|
2379
|
+
: [];
|
|
2380
|
+
}
|
|
2381
|
+
_observeReadStateResult(method, result, params) {
|
|
2382
|
+
if (!['message.read', 'message.read_state', 'group.read', 'group.read_state'].includes(method))
|
|
2383
|
+
return;
|
|
2384
|
+
this._ensureReadStateIdentity();
|
|
2385
|
+
const isGroup = method.startsWith('group.');
|
|
2386
|
+
if (method.endsWith('.read')) {
|
|
2387
|
+
for (const state of this._readStateItems(params)) {
|
|
2388
|
+
const query = { topic_key: state.topic_key ?? '' };
|
|
2389
|
+
if (isGroup)
|
|
2390
|
+
query.group_id = state.group_id ?? params.group_id ?? '';
|
|
2391
|
+
else
|
|
2392
|
+
query.scope = state.scope || params.scope || 'p2p';
|
|
2393
|
+
this._registerReadStateQuery(`${isGroup ? 'group' : 'message'}.read_state`, query);
|
|
2394
|
+
}
|
|
2395
|
+
if (!Array.isArray(params.states) && params.topic_key) {
|
|
2396
|
+
const query = {};
|
|
2397
|
+
for (const field of ['scope', 'group_id', 'topic_key']) {
|
|
2398
|
+
if (field in params)
|
|
2399
|
+
query[field] = params[field];
|
|
2400
|
+
}
|
|
2401
|
+
this._registerReadStateQuery(`${isGroup ? 'group' : 'message'}.read_state`, query);
|
|
2402
|
+
}
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
this._registerReadStateQuery(method, params);
|
|
2406
|
+
for (const state of this._readStateItems(result)) {
|
|
2407
|
+
const scope = String(state.scope ?? (isGroup ? 'group' : 'p2p')).trim();
|
|
2408
|
+
const topicKey = String(state.topic_key ?? params.topic_key ?? '');
|
|
2409
|
+
const groupId = isGroup ? String(state.group_id ?? params.group_id ?? '').trim() : '';
|
|
2410
|
+
const lastRead = String(state.last_read_message_id ?? '').trim();
|
|
2411
|
+
if (!scope || !topicKey || !lastRead || (isGroup && !groupId))
|
|
2412
|
+
continue;
|
|
2413
|
+
const key = `${scope}\u0000${groupId}\u0000${topicKey}`;
|
|
2414
|
+
const query = { topic_key: topicKey };
|
|
2415
|
+
if (isGroup)
|
|
2416
|
+
query.group_id = groupId;
|
|
2417
|
+
else
|
|
2418
|
+
query.scope = scope;
|
|
2419
|
+
const previous = this._readStateCache.get(key)?.state;
|
|
2420
|
+
if (previous && !this._readStateIsNewer(previous, state))
|
|
2421
|
+
continue;
|
|
2422
|
+
const samePosition = previous?.last_read_message_id === state.last_read_message_id
|
|
2423
|
+
&& BigInt(String(previous?.last_read_order ?? 0)) === BigInt(String(state.last_read_order ?? 0));
|
|
2424
|
+
this._readStateCache.set(key, {
|
|
2425
|
+
method: `${isGroup ? 'group' : 'message'}.read_state`,
|
|
2426
|
+
params: query,
|
|
2427
|
+
state: samePosition ? { ...previous, ...state } : { ...state },
|
|
2428
|
+
});
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
_onRawReadChanged(data, expectedScope) {
|
|
2432
|
+
if (!isJsonObject(data))
|
|
2433
|
+
return;
|
|
2434
|
+
const eventData = data;
|
|
2435
|
+
if (eventData.scope !== expectedScope || eventData.owner_aid !== this._aid)
|
|
2436
|
+
return;
|
|
2437
|
+
const fields = ['owner_aid', 'topic_key', 'last_read_message_id', ...(expectedScope === 'group' ? ['group_id'] : [])];
|
|
2438
|
+
if (fields.some((field) => {
|
|
2439
|
+
const value = eventData[field];
|
|
2440
|
+
return typeof value !== 'string' || !value || Array.from(value).length > 128;
|
|
2441
|
+
}))
|
|
2442
|
+
return;
|
|
2443
|
+
const order = eventData.last_read_order;
|
|
2444
|
+
if (!(typeof order === 'number' && Number.isSafeInteger(order) && order > 0)
|
|
2445
|
+
&& !(typeof order === 'string' && /^[1-9]\d*$/.test(order)))
|
|
2446
|
+
return;
|
|
2447
|
+
this._ensureReadStateIdentity();
|
|
2448
|
+
const scope = expectedScope;
|
|
2449
|
+
const topicKey = String(eventData.topic_key ?? '');
|
|
2450
|
+
const groupId = scope === 'group' ? String(eventData.group_id) : '';
|
|
2451
|
+
const lastRead = String(eventData.last_read_message_id);
|
|
2452
|
+
if (scope && topicKey && lastRead && (scope !== 'group' || groupId)) {
|
|
2453
|
+
const key = `${scope}\u0000${groupId}\u0000${topicKey}`;
|
|
2454
|
+
const query = { topic_key: topicKey };
|
|
2455
|
+
if (scope === 'group')
|
|
2456
|
+
query.group_id = groupId;
|
|
2457
|
+
else
|
|
2458
|
+
query.scope = scope;
|
|
2459
|
+
const method = `${scope === 'group' ? 'group' : 'message'}.read_state`;
|
|
2460
|
+
this._registerReadStateQuery(method, query);
|
|
2461
|
+
const previous = this._readStateCache.get(key)?.state;
|
|
2462
|
+
if (previous && (!this._readStateIsNewer(previous, eventData)
|
|
2463
|
+
|| (previous.last_read_message_id === eventData.last_read_message_id
|
|
2464
|
+
&& BigInt(String(previous.last_read_order ?? 0)) === BigInt(String(eventData.last_read_order ?? 0)))))
|
|
2465
|
+
return;
|
|
2466
|
+
this._readStateCache.set(key, { method, params: query, state: { ...eventData } });
|
|
2467
|
+
}
|
|
2468
|
+
const identity = this._ensureReadStateIdentity();
|
|
2469
|
+
this._dispatcher.enqueue(scope === 'group' ? 'group.read_changed' : 'message.read_changed', eventData, () => identity === this._ensureReadStateIdentity());
|
|
2470
|
+
}
|
|
2471
|
+
async _reconcileReadStates() {
|
|
2472
|
+
const identity = this._ensureReadStateIdentity();
|
|
2473
|
+
const keys = new Set();
|
|
2474
|
+
const errors = [];
|
|
2475
|
+
for (const entry of [...this._readStateQueries.values()]) {
|
|
2476
|
+
try {
|
|
2477
|
+
const result = await this.call(entry.method, { ...entry.params });
|
|
2478
|
+
if (identity !== this._ensureReadStateIdentity())
|
|
2479
|
+
return;
|
|
2480
|
+
this._observeReadStateResult(entry.method, result, entry.params);
|
|
2481
|
+
for (const state of this._readStateItems(result)) {
|
|
2482
|
+
const scope = String(state.scope ?? (entry.method.startsWith('group.') ? 'group' : 'p2p'));
|
|
2483
|
+
const groupId = scope === 'group' ? String(state.group_id ?? entry.params.group_id ?? '') : '';
|
|
2484
|
+
const topicKey = String(state.topic_key ?? entry.params.topic_key ?? '');
|
|
2485
|
+
const key = `${scope}\u0000${groupId}\u0000${topicKey}`;
|
|
2486
|
+
if (this._readStateCache.has(key))
|
|
2487
|
+
keys.add(key);
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
catch (error) {
|
|
2491
|
+
errors.push({ method: entry.method, params: { ...entry.params }, error: error instanceof Error ? error.message : String(error) });
|
|
2492
|
+
this._clientLog.debug(`read state reconnect reconciliation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2493
|
+
}
|
|
2494
|
+
if (identity !== this._ensureReadStateIdentity())
|
|
2495
|
+
return;
|
|
2496
|
+
}
|
|
2497
|
+
this._dispatcher.enqueue('read_state_sync', {
|
|
2498
|
+
owner_aid: this._aid ?? '',
|
|
2499
|
+
states: [...keys].map((key) => ({ ...this._readStateCache.get(key).state })),
|
|
2500
|
+
errors,
|
|
2501
|
+
}, () => identity === this._ensureReadStateIdentity());
|
|
2502
|
+
}
|
|
1796
2503
|
async _callRawV2Rpc(method, params, pullGateKey = '') {
|
|
1797
2504
|
const p = { ...(params ?? {}) };
|
|
1798
2505
|
const forceForeground = Boolean(p._rpc_foreground);
|
|
@@ -1952,8 +2659,15 @@ export class AUNClient {
|
|
|
1952
2659
|
_markPublishedSeq(ns, seq) {
|
|
1953
2660
|
this._delivery.markPublishedSeq(ns, seq);
|
|
1954
2661
|
}
|
|
1955
|
-
_publishAppEvent(event, payload, source = 'direct') {
|
|
1956
|
-
|
|
2662
|
+
_publishAppEvent(event, payload, source = 'direct', deliveryContext) {
|
|
2663
|
+
if (event === 'message.received') {
|
|
2664
|
+
return this._handleOwnershipMessage(payload).then((handled) => {
|
|
2665
|
+
if (!handled)
|
|
2666
|
+
return this._delivery.publishAppEvent(event, payload, source, deliveryContext);
|
|
2667
|
+
return undefined;
|
|
2668
|
+
});
|
|
2669
|
+
}
|
|
2670
|
+
return this._delivery.publishAppEvent(event, payload, source, deliveryContext);
|
|
1957
2671
|
}
|
|
1958
2672
|
_echoTimestamp() {
|
|
1959
2673
|
const now = new Date();
|
|
@@ -2132,8 +2846,8 @@ export class AUNClient {
|
|
|
2132
2846
|
}
|
|
2133
2847
|
return true;
|
|
2134
2848
|
}
|
|
2135
|
-
async _drainOrderedMessages(ns, beforeSeq, pullResponse = false, persist = true, source = 'push') {
|
|
2136
|
-
return this._delivery.drainOrderedMessages(ns, beforeSeq, pullResponse, persist, source);
|
|
2849
|
+
async _drainOrderedMessages(ns, beforeSeq, pullResponse = false, persist = true, source = 'push', throughSeq) {
|
|
2850
|
+
return this._delivery.drainOrderedMessages(ns, beforeSeq, pullResponse, persist, source, undefined, throughSeq);
|
|
2137
2851
|
}
|
|
2138
2852
|
async _publishOrderedMessage(event, ns, seq, payload, source = 'push') {
|
|
2139
2853
|
return this._delivery.publishOrderedMessage(event, ns, seq, payload, source);
|
|
@@ -2322,7 +3036,15 @@ export class AUNClient {
|
|
|
2322
3036
|
}
|
|
2323
3037
|
}
|
|
2324
3038
|
try {
|
|
2325
|
-
|
|
3039
|
+
if (opts.signedAt === undefined)
|
|
3040
|
+
await this._auth.verifyPeerCertificate(peerGatewayUrl, certPem, aid);
|
|
3041
|
+
else if (opts.agentMdRead) {
|
|
3042
|
+
validateAgentMdCertificate(certPem, aid, opts.signedAt, { requireCurrentValidity: false, readCompatibility: true });
|
|
3043
|
+
await this._auth._verifyAuthCertChain(peerGatewayUrl, new crypto.X509Certificate(certPem), aid, opts.signedAt);
|
|
3044
|
+
}
|
|
3045
|
+
else {
|
|
3046
|
+
await this._auth.verifyPeerCertificate(peerGatewayUrl, certPem, aid, opts.signedAt);
|
|
3047
|
+
}
|
|
2326
3048
|
}
|
|
2327
3049
|
catch (exc) {
|
|
2328
3050
|
if (cachedBootstrapChain) {
|
|
@@ -2336,9 +3058,9 @@ export class AUNClient {
|
|
|
2336
3058
|
validatedAt: nowSec,
|
|
2337
3059
|
refreshAfter: nowSec + PEER_CERT_CACHE_TTL,
|
|
2338
3060
|
};
|
|
2339
|
-
const cacheKey = AUNClient._certCacheKey(aid, certFingerprint);
|
|
3061
|
+
const cacheKey = AUNClient._certCacheKey(aid, certFingerprint) + (opts.signedAt === undefined ? '' : `:signed:${opts.signedAt}${opts.agentMdRead ? ':agentmd' : ''}`);
|
|
2340
3062
|
this._certCache.set(cacheKey, entry);
|
|
2341
|
-
if (!certFingerprint) {
|
|
3063
|
+
if (!certFingerprint && opts.signedAt === undefined) {
|
|
2342
3064
|
const bareKey = AUNClient._certCacheKey(aid);
|
|
2343
3065
|
this._certCache.set(bareKey, entry);
|
|
2344
3066
|
const actualFp = `sha256:${x509Cert.fingerprint256.replace(/:/g, '').toLowerCase()}`;
|
|
@@ -2346,19 +3068,26 @@ export class AUNClient {
|
|
|
2346
3068
|
}
|
|
2347
3069
|
try {
|
|
2348
3070
|
// peer 证书只存版本目录,不覆盖 cert.pem
|
|
2349
|
-
this._tokenStore.saveCert(aid, certPem, certFingerprint, { makeActive: false });
|
|
3071
|
+
this._tokenStore.saveCert(aid, certPem, opts.agentMdRead && opts.signedAt !== undefined ? `sha256:${x509Cert.fingerprint256.replace(/:/g, '').toLowerCase()}` : certFingerprint, { makeActive: false });
|
|
2350
3072
|
}
|
|
2351
3073
|
catch (exc) {
|
|
2352
3074
|
this._clientLog.error(`failed to write cert to keystore (aid=${aid}, fp=${certFingerprint ?? ''}): ${formatCaughtError(exc)}`, exc instanceof Error ? exc : undefined);
|
|
2353
3075
|
}
|
|
2354
3076
|
return certPem;
|
|
2355
3077
|
}
|
|
3078
|
+
async _fetchServiceEventCertificate(aid, fingerprint) {
|
|
3079
|
+
const certificate = await this._fetchPeerCert(aid, fingerprint);
|
|
3080
|
+
if (!this._gatewayUrl)
|
|
3081
|
+
throw new ValidationError('gateway url unavailable for service event certificate validation');
|
|
3082
|
+
await this._auth.verifyPeerCertificate(AUNClient._resolvePeerGatewayUrl(this._gatewayUrl, aid), certificate, aid, undefined, true);
|
|
3083
|
+
return certificate;
|
|
3084
|
+
}
|
|
2356
3085
|
/** 获取对方证书(带缓存 + 完整 PKI 验证),跨域时自动路由到 peer 所在域。 */
|
|
2357
|
-
async _fetchPeerCert(aid, certFingerprint, timeoutMs = 30_000) {
|
|
3086
|
+
async _fetchPeerCert(aid, certFingerprint, timeoutMs = 30_000, signedAt, agentMdRead = false) {
|
|
2358
3087
|
const tStart = Date.now();
|
|
2359
3088
|
this._clientLog.debug(`_fetchPeerCert enter: aid=${aid}, fp=${certFingerprint ?? ''}`);
|
|
2360
3089
|
try {
|
|
2361
|
-
const cacheKey = AUNClient._certCacheKey(aid, certFingerprint);
|
|
3090
|
+
const cacheKey = AUNClient._certCacheKey(aid, certFingerprint) + (signedAt === undefined ? '' : `:signed:${signedAt}${agentMdRead ? ':agentmd' : ''}`);
|
|
2362
3091
|
const cached = this._certCache.get(cacheKey);
|
|
2363
3092
|
const now = Date.now() / 1000;
|
|
2364
3093
|
if (cached && now < cached.refreshAfter) {
|
|
@@ -2376,6 +3105,8 @@ export class AUNClient {
|
|
|
2376
3105
|
certPem,
|
|
2377
3106
|
certFingerprint,
|
|
2378
3107
|
source: 'fetch',
|
|
3108
|
+
signedAt,
|
|
3109
|
+
agentMdRead,
|
|
2379
3110
|
});
|
|
2380
3111
|
this._clientLog.debug(`_fetchPeerCert exit: elapsed=${Date.now() - tStart}ms aid=${aid} (fetched)`);
|
|
2381
3112
|
return validated;
|
|
@@ -2908,6 +3639,7 @@ export class AUNClient {
|
|
|
2908
3639
|
}
|
|
2909
3640
|
this._lifecycle.assertConnectionAttemptOwner(connectionOwner);
|
|
2910
3641
|
this._startBackgroundTasks();
|
|
3642
|
+
this._safeAsync(this._reconcileReadStates());
|
|
2911
3643
|
const connectionKind = String(params.connection_kind ?? 'long');
|
|
2912
3644
|
const isShortConnection = connectionKind === 'short';
|
|
2913
3645
|
const hasExplicitBackgroundSync = Object.prototype.hasOwnProperty.call(params, 'background_sync');
|
|
@@ -2934,7 +3666,7 @@ export class AUNClient {
|
|
|
2934
3666
|
this._clientLog.warn(`schedule post-connect P2P gap fill failed: ${formatCaughtError(exc)}`);
|
|
2935
3667
|
});
|
|
2936
3668
|
}
|
|
2937
|
-
this._safeAsync(this.
|
|
3669
|
+
this._safeAsync(this._resumeOwnershipAfterConnect());
|
|
2938
3670
|
this._clientLog.debug(`_connectOnce exit: elapsed=${Date.now() - tStart}ms gateway=${gatewayUrl}, aid=${this._aid ?? ''}`);
|
|
2939
3671
|
}
|
|
2940
3672
|
catch (err) {
|
|
@@ -3333,6 +4065,8 @@ export class AUNClient {
|
|
|
3333
4065
|
encrypted: true,
|
|
3334
4066
|
e2ee,
|
|
3335
4067
|
};
|
|
4068
|
+
if (typeof plaintext.type === 'string' && plaintext.type.startsWith('aun.ownership.'))
|
|
4069
|
+
result.from_aid = fromAid;
|
|
3336
4070
|
if (senderDeviceId)
|
|
3337
4071
|
result.sender_device_id = senderDeviceId;
|
|
3338
4072
|
const explicitDirection = String(msg.direction ?? '').trim();
|
|
@@ -3546,6 +4280,22 @@ export class AUNClient {
|
|
|
3546
4280
|
const gateways = await this._resolveGatewayCandidatesForAid(aid);
|
|
3547
4281
|
return gateways[0];
|
|
3548
4282
|
}
|
|
4283
|
+
async _resolveOwnershipCertificateGateway(aid) {
|
|
4284
|
+
const issuer = aid.slice(aid.indexOf('.') + 1);
|
|
4285
|
+
if (issuer === this._aid?.slice(this._aid.indexOf('.') + 1))
|
|
4286
|
+
return this._resolveGatewayForAid(aid);
|
|
4287
|
+
const port = this._configModel.discoveryPort ? `:${this._configModel.discoveryPort}` : '';
|
|
4288
|
+
let lastError;
|
|
4289
|
+
for (const host of [aid, `gateway.${issuer}`]) {
|
|
4290
|
+
try {
|
|
4291
|
+
return await this._discovery.discover(`https://${host}${port}/.well-known/aun-gateway`);
|
|
4292
|
+
}
|
|
4293
|
+
catch (error) {
|
|
4294
|
+
lastError = error;
|
|
4295
|
+
}
|
|
4296
|
+
}
|
|
4297
|
+
throw ownershipFailure('unavailable', lastError instanceof Error ? lastError.message : 'signer Gateway discovery failed', true);
|
|
4298
|
+
}
|
|
3549
4299
|
/** 发现并缓存全部 Gateway 候选,供首连和自动重连故障转移使用。 */
|
|
3550
4300
|
async _resolveGatewayCandidatesForAid(aid) {
|
|
3551
4301
|
const resolvedAid = String(aid ?? this._aid ?? '').trim();
|