@agentunion/fastaun-browser 0.2.17 → 0.2.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/dist/auth.d.ts +12 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +370 -215
- package/dist/auth.js.map +1 -1
- package/dist/client.d.ts +24 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +1307 -849
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -1
- package/dist/config.js.map +1 -1
- package/dist/discovery.d.ts +3 -0
- package/dist/discovery.d.ts.map +1 -1
- package/dist/discovery.js +15 -1
- package/dist/discovery.js.map +1 -1
- package/dist/e2ee-group.d.ts +4 -0
- package/dist/e2ee-group.d.ts.map +1 -1
- package/dist/e2ee-group.js +327 -201
- package/dist/e2ee-group.js.map +1 -1
- package/dist/e2ee.d.ts +4 -0
- package/dist/e2ee.d.ts.map +1 -1
- package/dist/e2ee.js +196 -117
- package/dist/e2ee.js.map +1 -1
- package/dist/events.d.ts +3 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +4 -1
- package/dist/events.js.map +1 -1
- package/dist/keystore/index.d.ts +11 -0
- package/dist/keystore/index.d.ts.map +1 -1
- package/dist/keystore/indexeddb.d.ts +38 -0
- package/dist/keystore/indexeddb.d.ts.map +1 -1
- package/dist/keystore/indexeddb.js +245 -98
- package/dist/keystore/indexeddb.js.map +1 -1
- package/dist/logger.d.ts +37 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +112 -0
- package/dist/logger.js.map +1 -0
- package/dist/namespaces/auth.d.ts +13 -3
- package/dist/namespaces/auth.d.ts.map +1 -1
- package/dist/namespaces/auth.js +284 -106
- package/dist/namespaces/auth.js.map +1 -1
- package/dist/namespaces/custody.d.ts +3 -0
- package/dist/namespaces/custody.d.ts.map +1 -1
- package/dist/namespaces/custody.js +147 -75
- package/dist/namespaces/custody.js.map +1 -1
- package/dist/namespaces/meta.d.ts +3 -0
- package/dist/namespaces/meta.d.ts.map +1 -1
- package/dist/namespaces/meta.js +94 -43
- package/dist/namespaces/meta.js.map +1 -1
- package/dist/secret-store/indexeddb-store.d.ts +3 -0
- package/dist/secret-store/indexeddb-store.d.ts.map +1 -1
- package/dist/secret-store/indexeddb-store.js +57 -29
- package/dist/secret-store/indexeddb-store.js.map +1 -1
- package/dist/transport.d.ts +3 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +74 -4
- package/dist/transport.js.map +1 -1
- package/package.json +37 -37
package/dist/auth.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { base64ToUint8, uint8ToBase64, pemToArrayBuffer, toBufferSource } from './crypto.js';
|
|
5
5
|
import { AuthError, StateError, ValidationError, mapRemoteError } from './errors.js';
|
|
6
6
|
import { ROOT_CA_PEM } from './certs/root.js';
|
|
7
|
+
const _noopLog = { error: () => { }, warn: () => { }, info: () => { }, debug: () => { } };
|
|
7
8
|
import { isJsonObject, } from './types.js';
|
|
8
9
|
// ── ASN.1 / PEM 工具 ────────────────────────────────────
|
|
9
10
|
/** 拆分 PEM bundle 为独立的 PEM 字符串数组 */
|
|
@@ -334,6 +335,8 @@ function gatewayHttpUrl(gatewayUrl, path) {
|
|
|
334
335
|
* - 证书自动续期
|
|
335
336
|
*/
|
|
336
337
|
export class AuthFlow {
|
|
338
|
+
_log = _noopLog;
|
|
339
|
+
setLogger(log) { this._log = log; }
|
|
337
340
|
static _INSTANCE_STATE_FIELDS = [
|
|
338
341
|
'access_token',
|
|
339
342
|
'refresh_token',
|
|
@@ -368,14 +371,23 @@ export class AuthFlow {
|
|
|
368
371
|
// ── 公开 API ──────────────────────────────────────
|
|
369
372
|
/** 加载本地身份信息 */
|
|
370
373
|
async loadIdentity(aid) {
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
identity
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
374
|
+
const tStart = Date.now();
|
|
375
|
+
this._log.debug(`loadIdentity enter: aid=${aid ?? '<current>'}`);
|
|
376
|
+
try {
|
|
377
|
+
const identity = await this._loadIdentityOrRaise(aid);
|
|
378
|
+
const cert = await this._keystore.loadCert(identity.aid);
|
|
379
|
+
if (cert)
|
|
380
|
+
identity.cert = cert;
|
|
381
|
+
const instanceState = await this._loadInstanceState(identity.aid);
|
|
382
|
+
if (instanceState)
|
|
383
|
+
Object.assign(identity, instanceState);
|
|
384
|
+
this._log.debug(`loadIdentity exit: elapsed=${Date.now() - tStart}ms aid=${identity.aid} has_cert=${!!identity.cert}`);
|
|
385
|
+
return identity;
|
|
386
|
+
}
|
|
387
|
+
catch (err) {
|
|
388
|
+
this._log.debug(`loadIdentity exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
389
|
+
throw err;
|
|
390
|
+
}
|
|
379
391
|
}
|
|
380
392
|
/** 加载身份,不存在时返回 null */
|
|
381
393
|
async loadIdentityOrNull(aid) {
|
|
@@ -410,265 +422,381 @@ export class AuthFlow {
|
|
|
410
422
|
* 3. 保存返回的证书
|
|
411
423
|
*/
|
|
412
424
|
async createAid(gatewayUrl, aid) {
|
|
425
|
+
const tStart = Date.now();
|
|
426
|
+
this._log.debug(`createAid enter: aid=${aid} gateway=${gatewayUrl}`);
|
|
413
427
|
AuthFlow._validateAidName(aid);
|
|
414
|
-
const identity = await this._ensureLocalIdentity(aid);
|
|
415
|
-
if (identity.cert) {
|
|
416
|
-
return { aid: identity.aid, cert: identity.cert };
|
|
417
|
-
}
|
|
418
|
-
// 本地有密钥但无证书 — 尝试注册
|
|
419
428
|
try {
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
+
const identity = await this._ensureLocalIdentity(aid);
|
|
430
|
+
if (identity.cert) {
|
|
431
|
+
this._log.debug(`createAid exit: elapsed=${Date.now() - tStart}ms aid=${aid} reason=already_has_cert`);
|
|
432
|
+
return { aid: identity.aid, cert: identity.cert };
|
|
433
|
+
}
|
|
434
|
+
// 本地有密钥但无证书 — 尝试注册
|
|
435
|
+
try {
|
|
436
|
+
const created = await this._createAid(gatewayUrl, identity);
|
|
437
|
+
Object.assign(identity, created);
|
|
438
|
+
}
|
|
439
|
+
catch (e) {
|
|
440
|
+
if (e instanceof Error && e.message.includes('already exists')) {
|
|
441
|
+
// AID 已在服务端注册,尝试下载证书恢复
|
|
442
|
+
try {
|
|
443
|
+
const recovered = await this._recoverCertViaDownload(gatewayUrl, identity);
|
|
444
|
+
Object.assign(identity, recovered);
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
this._log.debug(`createAid exit (error): elapsed=${Date.now() - tStart}ms aid=${aid} err=already_registered_recover_failed`);
|
|
448
|
+
throw new StateError(`AID ${aid} already registered on server but local certificate is missing. ` +
|
|
449
|
+
`Certificate download recovery failed. Options: ` +
|
|
450
|
+
`(1) use a different AID name, or ` +
|
|
451
|
+
`(2) restart server to clear registration.`);
|
|
452
|
+
}
|
|
429
453
|
}
|
|
430
|
-
|
|
431
|
-
throw
|
|
432
|
-
`Certificate download recovery failed. Options: ` +
|
|
433
|
-
`(1) use a different AID name, or ` +
|
|
434
|
-
`(2) restart server to clear registration.`);
|
|
454
|
+
else {
|
|
455
|
+
throw e;
|
|
435
456
|
}
|
|
436
457
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
}
|
|
458
|
+
await this._persistIdentity(identity);
|
|
459
|
+
this._aid = identity.aid;
|
|
460
|
+
this._log.debug(`createAid exit: elapsed=${Date.now() - tStart}ms aid=${aid}`);
|
|
461
|
+
return { aid: identity.aid, cert: identity.cert };
|
|
462
|
+
}
|
|
463
|
+
catch (err) {
|
|
464
|
+
this._log.debug(`createAid exit (error): elapsed=${Date.now() - tStart}ms aid=${aid} err=${err instanceof Error ? err.message : String(err)}`);
|
|
465
|
+
throw err;
|
|
440
466
|
}
|
|
441
|
-
await this._persistIdentity(identity);
|
|
442
|
-
this._aid = identity.aid;
|
|
443
|
-
return { aid: identity.aid, cert: identity.cert };
|
|
444
467
|
}
|
|
445
468
|
/**
|
|
446
469
|
* 认证已有 AID — login1/login2 双阶段流程。
|
|
470
|
+
*
|
|
471
|
+
* 优先复用 keystore 里的 cached access_token(未过期且有 refresh_token),
|
|
472
|
+
* 避免每次 authenticate 都走两阶段重登的网络往返。与 Python SDK 行为对齐。
|
|
447
473
|
*/
|
|
448
474
|
async authenticate(gatewayUrl, aid) {
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
475
|
+
const tStart = Date.now();
|
|
476
|
+
this._log.debug(`authenticate enter: aid=${aid ?? '<current>'} gateway=${gatewayUrl}`);
|
|
477
|
+
try {
|
|
478
|
+
const identity = await this._loadIdentityOrRaise(aid);
|
|
479
|
+
// 优先复用 cached access_token(未过期且有 refresh_token)
|
|
480
|
+
// 避免每次调 authenticate 都走两阶段重登
|
|
481
|
+
// 注意:_loadIdentityOrRaise 直接走 keystore.loadIdentity,不包含 instance_state
|
|
482
|
+
// (IndexedDB 把 access_token / refresh_token / expires_at 拆到 STORE_INSTANCE_STATE)
|
|
483
|
+
// 这里需要主动 _loadInstanceState 拿到 token,否则永远走 _login。
|
|
484
|
+
const instanceState = await this._loadInstanceState(identity.aid);
|
|
485
|
+
const identityWithState = instanceState
|
|
486
|
+
? { ...identity, ...instanceState }
|
|
487
|
+
: identity;
|
|
488
|
+
const cachedToken = this._getCachedAccessToken(identityWithState);
|
|
489
|
+
const cachedRefresh = String(identityWithState.refresh_token ?? '');
|
|
490
|
+
if (cachedToken && cachedRefresh) {
|
|
491
|
+
this._log.debug(`authenticate reusing cached token: aid=${identity.aid} expires_at=${identityWithState.access_token_expires_at}`);
|
|
492
|
+
this._aid = identity.aid;
|
|
493
|
+
return {
|
|
494
|
+
aid: identity.aid,
|
|
495
|
+
access_token: cachedToken,
|
|
496
|
+
refresh_token: cachedRefresh,
|
|
497
|
+
expires_at: identityWithState.access_token_expires_at,
|
|
498
|
+
gateway: gatewayUrl,
|
|
499
|
+
};
|
|
456
500
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
501
|
+
if (!identity.cert) {
|
|
502
|
+
// 尝试下载恢复证书
|
|
503
|
+
try {
|
|
504
|
+
const recovered = await this._recoverCertViaDownload(gatewayUrl, identity);
|
|
505
|
+
Object.assign(identity, recovered);
|
|
506
|
+
await this._persistIdentity(identity);
|
|
507
|
+
}
|
|
508
|
+
catch (e) {
|
|
509
|
+
throw new StateError(`local certificate missing and recovery failed: ${e}. ` +
|
|
510
|
+
`Run auth.createAid() to register a new identity.`);
|
|
511
|
+
}
|
|
460
512
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
try {
|
|
464
|
-
login = await this._login(gatewayUrl, identity);
|
|
465
|
-
}
|
|
466
|
-
catch (e) {
|
|
467
|
-
// 证书未在服务端注册或公钥不匹配 — 自动重新注册
|
|
468
|
-
if (e instanceof AuthError && (String(e.message).includes('not registered') || String(e.message).includes('public key mismatch'))) {
|
|
469
|
-
console.warn(`[auth] 证书未在服务端注册,自动重新注册: aid=${identity.aid}`);
|
|
470
|
-
const created = await this._createAid(gatewayUrl, identity);
|
|
471
|
-
identity.cert = created.cert;
|
|
472
|
-
await this._persistIdentity(identity);
|
|
513
|
+
let login;
|
|
514
|
+
try {
|
|
473
515
|
login = await this._login(gatewayUrl, identity);
|
|
474
516
|
}
|
|
475
|
-
|
|
476
|
-
|
|
517
|
+
catch (e) {
|
|
518
|
+
// 证书未在服务端注册或公钥不匹配 — 自动重新注册
|
|
519
|
+
if (e instanceof AuthError && (String(e.message).includes('not registered') || String(e.message).includes('public key mismatch'))) {
|
|
520
|
+
this._log.warn(`[auth] cert not registered on server, auto re-register: aid=${identity.aid}`);
|
|
521
|
+
const created = await this._createAid(gatewayUrl, identity);
|
|
522
|
+
identity.cert = created.cert;
|
|
523
|
+
await this._persistIdentity(identity);
|
|
524
|
+
login = await this._login(gatewayUrl, identity);
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
throw e;
|
|
528
|
+
}
|
|
477
529
|
}
|
|
530
|
+
this._rememberTokens(identity, login);
|
|
531
|
+
await this._validateNewCert(identity, gatewayUrl);
|
|
532
|
+
await this._persistIdentity(identity);
|
|
533
|
+
this._aid = identity.aid;
|
|
534
|
+
this._log.debug(`authenticate exit: elapsed=${Date.now() - tStart}ms aid=${identity.aid}`);
|
|
535
|
+
return {
|
|
536
|
+
aid: identity.aid,
|
|
537
|
+
access_token: identity.access_token,
|
|
538
|
+
refresh_token: identity.refresh_token,
|
|
539
|
+
expires_at: identity.access_token_expires_at,
|
|
540
|
+
gateway: gatewayUrl,
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
catch (err) {
|
|
544
|
+
this._log.debug(`authenticate exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
545
|
+
throw err;
|
|
478
546
|
}
|
|
479
|
-
this._rememberTokens(identity, login);
|
|
480
|
-
await this._validateNewCert(identity, gatewayUrl);
|
|
481
|
-
await this._persistIdentity(identity);
|
|
482
|
-
this._aid = identity.aid;
|
|
483
|
-
return {
|
|
484
|
-
aid: identity.aid,
|
|
485
|
-
access_token: identity.access_token,
|
|
486
|
-
refresh_token: identity.refresh_token,
|
|
487
|
-
expires_at: identity.access_token_expires_at,
|
|
488
|
-
gateway: gatewayUrl,
|
|
489
|
-
};
|
|
490
547
|
}
|
|
491
548
|
/**
|
|
492
549
|
* 确保已认证(如无身份则先注册再登录)。
|
|
493
550
|
*/
|
|
494
551
|
async ensureAuthenticated(gatewayUrl) {
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
552
|
+
const tStart = Date.now();
|
|
553
|
+
this._log.debug(`ensureAuthenticated enter: gateway=${gatewayUrl}`);
|
|
554
|
+
try {
|
|
555
|
+
const identity = await this._ensureIdentity();
|
|
556
|
+
if (!identity.cert) {
|
|
557
|
+
const created = await this._createAid(gatewayUrl, identity);
|
|
558
|
+
Object.assign(identity, created);
|
|
559
|
+
await this._persistIdentity(identity);
|
|
560
|
+
}
|
|
561
|
+
const login = await this._login(gatewayUrl, identity);
|
|
562
|
+
this._rememberTokens(identity, login);
|
|
563
|
+
await this._validateNewCert(identity, gatewayUrl);
|
|
499
564
|
await this._persistIdentity(identity);
|
|
565
|
+
const token = (identity.access_token || identity.token || identity.kite_token);
|
|
566
|
+
if (!token)
|
|
567
|
+
throw new AuthError('login2 did not return access token');
|
|
568
|
+
this._log.debug(`ensureAuthenticated exit: elapsed=${Date.now() - tStart}ms aid=${identity.aid}`);
|
|
569
|
+
return { token, identity };
|
|
570
|
+
}
|
|
571
|
+
catch (err) {
|
|
572
|
+
this._log.debug(`ensureAuthenticated exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
573
|
+
throw err;
|
|
500
574
|
}
|
|
501
|
-
const login = await this._login(gatewayUrl, identity);
|
|
502
|
-
this._rememberTokens(identity, login);
|
|
503
|
-
await this._validateNewCert(identity, gatewayUrl);
|
|
504
|
-
await this._persistIdentity(identity);
|
|
505
|
-
const token = (identity.access_token || identity.token || identity.kite_token);
|
|
506
|
-
if (!token)
|
|
507
|
-
throw new AuthError('login2 did not return access token');
|
|
508
|
-
return { token, identity };
|
|
509
575
|
}
|
|
510
576
|
/**
|
|
511
577
|
* 使用已有 token 初始化 WebSocket 会话。
|
|
512
578
|
*/
|
|
513
579
|
async initializeWithToken(transport, challenge, accessToken, opts) {
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
580
|
+
const tStart = Date.now();
|
|
581
|
+
this._log.debug(`initializeWithToken enter: device_id=${opts?.deviceId ?? this._deviceId} slot_id=${opts?.slotId ?? this._slotId}`);
|
|
582
|
+
try {
|
|
583
|
+
const params = isJsonObject(challenge?.params) ? challenge.params : {};
|
|
584
|
+
const nonce = String(params.nonce ?? '');
|
|
585
|
+
if (!nonce)
|
|
586
|
+
throw new AuthError('gateway challenge missing nonce');
|
|
587
|
+
this.setInstanceContext({
|
|
588
|
+
deviceId: String(opts?.deviceId ?? this._deviceId ?? ''),
|
|
589
|
+
slotId: String(opts?.slotId ?? this._slotId ?? ''),
|
|
590
|
+
});
|
|
591
|
+
await this._initializeSession(transport, nonce, accessToken, {
|
|
592
|
+
deviceId: String(opts?.deviceId ?? this._deviceId ?? ''),
|
|
593
|
+
slotId: String(opts?.slotId ?? this._slotId ?? ''),
|
|
594
|
+
deliveryMode: opts?.deliveryMode ?? null,
|
|
595
|
+
connectionKind: opts?.connectionKind ?? 'long',
|
|
596
|
+
shortTtlMs: opts?.shortTtlMs ?? 0,
|
|
597
|
+
extraInfo: opts?.extraInfo,
|
|
598
|
+
});
|
|
599
|
+
this._log.debug(`initializeWithToken exit: elapsed=${Date.now() - tStart}ms`);
|
|
600
|
+
}
|
|
601
|
+
catch (err) {
|
|
602
|
+
this._log.debug(`initializeWithToken exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
603
|
+
throw err;
|
|
604
|
+
}
|
|
527
605
|
}
|
|
528
606
|
/**
|
|
529
607
|
* 连接会话 — 多策略认证:显式 token → 缓存 token → refresh → 重新登录。
|
|
530
608
|
*/
|
|
531
609
|
async connectSession(transport, challenge, gatewayUrl, accessToken) {
|
|
532
|
-
const
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
throw new AuthError('gateway challenge missing nonce');
|
|
536
|
-
const connectOptions = isJsonObject(accessToken)
|
|
537
|
-
? accessToken
|
|
538
|
-
: { accessToken };
|
|
539
|
-
const deviceId = String(connectOptions.deviceId ?? this._deviceId ?? '');
|
|
540
|
-
const slotId = String(connectOptions.slotId ?? this._slotId ?? '');
|
|
541
|
-
const deliveryMode = connectOptions.deliveryMode ?? null;
|
|
542
|
-
this.setInstanceContext({ deviceId, slotId });
|
|
543
|
-
let identity;
|
|
610
|
+
const tStart = Date.now();
|
|
611
|
+
const explicitTokenInput = isJsonObject(accessToken) ? !!accessToken.accessToken : !!accessToken;
|
|
612
|
+
this._log.debug(`connectSession enter: gateway=${gatewayUrl} has_explicit_token=${explicitTokenInput}`);
|
|
544
613
|
try {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
614
|
+
const params = isJsonObject(challenge?.params) ? challenge.params : {};
|
|
615
|
+
const nonce = String(params.nonce ?? '');
|
|
616
|
+
if (!nonce)
|
|
617
|
+
throw new AuthError('gateway challenge missing nonce');
|
|
618
|
+
const connectOptions = (isJsonObject(accessToken)
|
|
619
|
+
? accessToken
|
|
620
|
+
: { accessToken });
|
|
621
|
+
const deviceId = String(connectOptions.deviceId ?? this._deviceId ?? '');
|
|
622
|
+
const slotId = String(connectOptions.slotId ?? this._slotId ?? '');
|
|
623
|
+
const deliveryMode = connectOptions.deliveryMode ?? null;
|
|
624
|
+
const connectionKind = connectOptions.connectionKind ?? 'long';
|
|
625
|
+
const shortTtlMs = connectOptions.shortTtlMs ?? 0;
|
|
626
|
+
const extraInfo = connectOptions.extraInfo;
|
|
627
|
+
this.setInstanceContext({ deviceId, slotId });
|
|
628
|
+
let identity;
|
|
553
629
|
try {
|
|
554
|
-
await this.
|
|
555
|
-
deviceId,
|
|
556
|
-
slotId,
|
|
557
|
-
deliveryMode,
|
|
558
|
-
});
|
|
559
|
-
identity.access_token = explicitToken;
|
|
560
|
-
await this._persistIdentity(identity);
|
|
561
|
-
return { token: explicitToken, identity };
|
|
630
|
+
identity = await this.loadIdentity();
|
|
562
631
|
}
|
|
563
|
-
catch
|
|
564
|
-
|
|
565
|
-
throw e;
|
|
566
|
-
// 显式 token 失败,继续尝试其他方式
|
|
632
|
+
catch {
|
|
633
|
+
identity = null;
|
|
567
634
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
635
|
+
// 策略 1: 显式 token
|
|
636
|
+
const explicitToken = String(connectOptions.accessToken ?? '');
|
|
637
|
+
if (explicitToken && identity !== null) {
|
|
638
|
+
try {
|
|
639
|
+
await this._initializeSession(transport, nonce, explicitToken, {
|
|
640
|
+
deviceId,
|
|
641
|
+
slotId,
|
|
642
|
+
deliveryMode,
|
|
643
|
+
connectionKind,
|
|
644
|
+
shortTtlMs,
|
|
645
|
+
extraInfo,
|
|
646
|
+
});
|
|
647
|
+
identity.access_token = explicitToken;
|
|
648
|
+
await this._persistIdentity(identity);
|
|
649
|
+
this._log.debug(`connectSession exit: elapsed=${Date.now() - tStart}ms strategy=explicit_token aid=${identity.aid}`);
|
|
650
|
+
return { token: explicitToken, identity };
|
|
651
|
+
}
|
|
652
|
+
catch (e) {
|
|
653
|
+
if (!(e instanceof AuthError))
|
|
654
|
+
throw e;
|
|
655
|
+
// 显式 token 失败,继续尝试其他方式
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
// 无本地身份 — 先注册 + 登录
|
|
659
|
+
if (identity === null) {
|
|
660
|
+
const authContext = await this.ensureAuthenticated(gatewayUrl);
|
|
661
|
+
const token = authContext.token;
|
|
662
|
+
await this._initializeSession(transport, nonce, token, {
|
|
585
663
|
deviceId,
|
|
586
664
|
slotId,
|
|
587
665
|
deliveryMode,
|
|
666
|
+
connectionKind,
|
|
667
|
+
shortTtlMs,
|
|
668
|
+
extraInfo,
|
|
588
669
|
});
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
catch (e) {
|
|
592
|
-
if (!(e instanceof AuthError))
|
|
593
|
-
throw e;
|
|
594
|
-
// 缓存 token 失败,尝试刷新
|
|
670
|
+
this._log.debug(`connectSession exit: elapsed=${Date.now() - tStart}ms strategy=ensure_authenticated`);
|
|
671
|
+
return authContext;
|
|
595
672
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
identity = await this.refreshCachedTokens(gatewayUrl, identity);
|
|
602
|
-
const refreshedToken = this._getCachedAccessToken(identity);
|
|
603
|
-
if (refreshedToken) {
|
|
604
|
-
await this._initializeSession(transport, nonce, refreshedToken, {
|
|
673
|
+
// 策略 2: 缓存 token
|
|
674
|
+
const cachedToken = this._getCachedAccessToken(identity);
|
|
675
|
+
if (cachedToken) {
|
|
676
|
+
try {
|
|
677
|
+
await this._initializeSession(transport, nonce, cachedToken, {
|
|
605
678
|
deviceId,
|
|
606
679
|
slotId,
|
|
607
680
|
deliveryMode,
|
|
681
|
+
connectionKind,
|
|
682
|
+
shortTtlMs,
|
|
683
|
+
extraInfo,
|
|
608
684
|
});
|
|
609
|
-
|
|
685
|
+
this._log.debug(`connectSession exit: elapsed=${Date.now() - tStart}ms strategy=cached_token aid=${identity.aid}`);
|
|
686
|
+
return { token: cachedToken, identity };
|
|
687
|
+
}
|
|
688
|
+
catch (e) {
|
|
689
|
+
if (!(e instanceof AuthError))
|
|
690
|
+
throw e;
|
|
691
|
+
// 缓存 token 失败,尝试刷新
|
|
610
692
|
}
|
|
611
693
|
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
694
|
+
// 策略 3: refresh token
|
|
695
|
+
const refreshToken = String(identity.refresh_token ?? '');
|
|
696
|
+
if (refreshToken) {
|
|
697
|
+
try {
|
|
698
|
+
identity = await this.refreshCachedTokens(gatewayUrl, identity);
|
|
699
|
+
const refreshedToken = this._getCachedAccessToken(identity);
|
|
700
|
+
if (refreshedToken) {
|
|
701
|
+
await this._initializeSession(transport, nonce, refreshedToken, {
|
|
702
|
+
deviceId,
|
|
703
|
+
slotId,
|
|
704
|
+
deliveryMode,
|
|
705
|
+
connectionKind,
|
|
706
|
+
shortTtlMs,
|
|
707
|
+
extraInfo,
|
|
708
|
+
});
|
|
709
|
+
this._log.debug(`connectSession exit: elapsed=${Date.now() - tStart}ms strategy=refresh aid=${identity.aid}`);
|
|
710
|
+
return { token: refreshedToken, identity };
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
catch (e) {
|
|
714
|
+
if (!(e instanceof AuthError))
|
|
715
|
+
throw e;
|
|
716
|
+
// refresh 失败,重新登录
|
|
717
|
+
}
|
|
616
718
|
}
|
|
719
|
+
// 策略 4: 重新登录
|
|
720
|
+
const login = await this.authenticate(gatewayUrl, identity.aid);
|
|
721
|
+
const token = String(login.access_token ?? '');
|
|
722
|
+
if (!token)
|
|
723
|
+
throw new AuthError('authenticate did not return access_token');
|
|
724
|
+
await this._initializeSession(transport, nonce, token, {
|
|
725
|
+
deviceId,
|
|
726
|
+
slotId,
|
|
727
|
+
deliveryMode,
|
|
728
|
+
connectionKind,
|
|
729
|
+
shortTtlMs,
|
|
730
|
+
extraInfo,
|
|
731
|
+
});
|
|
732
|
+
identity = await this.loadIdentity(identity.aid);
|
|
733
|
+
this._log.debug(`connectSession exit: elapsed=${Date.now() - tStart}ms strategy=re_login aid=${identity.aid}`);
|
|
734
|
+
return { token, identity };
|
|
735
|
+
}
|
|
736
|
+
catch (err) {
|
|
737
|
+
this._log.debug(`connectSession exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
738
|
+
throw err;
|
|
617
739
|
}
|
|
618
|
-
// 策略 4: 重新登录
|
|
619
|
-
const login = await this.authenticate(gatewayUrl, identity.aid);
|
|
620
|
-
const token = String(login.access_token ?? '');
|
|
621
|
-
if (!token)
|
|
622
|
-
throw new AuthError('authenticate did not return access_token');
|
|
623
|
-
await this._initializeSession(transport, nonce, token, {
|
|
624
|
-
deviceId,
|
|
625
|
-
slotId,
|
|
626
|
-
deliveryMode,
|
|
627
|
-
});
|
|
628
|
-
identity = await this.loadIdentity(identity.aid);
|
|
629
|
-
return { token, identity };
|
|
630
740
|
}
|
|
631
741
|
/**
|
|
632
742
|
* 刷新 token。
|
|
633
743
|
*/
|
|
634
744
|
async refreshCachedTokens(gatewayUrl, identity) {
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
745
|
+
const tStart = Date.now();
|
|
746
|
+
this._log.debug(`refreshCachedTokens enter: aid=${identity.aid} gateway=${gatewayUrl}`);
|
|
747
|
+
try {
|
|
748
|
+
const refreshToken = String(identity.refresh_token ?? '');
|
|
749
|
+
if (!refreshToken)
|
|
750
|
+
throw new AuthError('missing refresh_token');
|
|
751
|
+
const refreshed = await this._refreshAccessToken(gatewayUrl, refreshToken);
|
|
752
|
+
this._rememberTokens(identity, refreshed);
|
|
753
|
+
await this._validateNewCert(identity, gatewayUrl);
|
|
754
|
+
await this._persistIdentity(identity);
|
|
755
|
+
this._log.debug(`refreshCachedTokens exit: elapsed=${Date.now() - tStart}ms aid=${identity.aid}`);
|
|
756
|
+
return identity;
|
|
757
|
+
}
|
|
758
|
+
catch (err) {
|
|
759
|
+
this._log.debug(`refreshCachedTokens exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
760
|
+
throw err;
|
|
761
|
+
}
|
|
643
762
|
}
|
|
644
763
|
/**
|
|
645
764
|
* 统一的对端证书验证入口:时间有效性 + 链验证 + CRL + OCSP + AID 绑定。
|
|
646
765
|
*/
|
|
647
766
|
async verifyPeerCertificate(gatewayUrl, certPem, expectedAid) {
|
|
648
|
-
const
|
|
649
|
-
|
|
650
|
-
ensureCertTimeValid(cert, 'peer certificate', now);
|
|
651
|
-
await this._verifyAuthCertChain(gatewayUrl, cert, expectedAid);
|
|
767
|
+
const tStart = Date.now();
|
|
768
|
+
this._log.debug(`verifyPeerCertificate enter: aid=${expectedAid} gateway=${gatewayUrl}`);
|
|
652
769
|
try {
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
770
|
+
const cert = parseCertDer(certPem);
|
|
771
|
+
const now = Date.now() / 1000;
|
|
772
|
+
ensureCertTimeValid(cert, 'peer certificate', now);
|
|
773
|
+
await this._verifyAuthCertChain(gatewayUrl, cert, expectedAid);
|
|
774
|
+
try {
|
|
775
|
+
await this._verifyAuthCertRevocation(gatewayUrl, cert, expectedAid);
|
|
776
|
+
}
|
|
777
|
+
catch (e) {
|
|
778
|
+
const errMsg = e instanceof Error ? e.message : String(e);
|
|
779
|
+
if (/revoked/i.test(errMsg))
|
|
780
|
+
throw e instanceof AuthError ? e : new AuthError(errMsg);
|
|
781
|
+
this._log.warn('[aun_core.auth] CRL check unavailable, degrade and continue:', errMsg);
|
|
782
|
+
}
|
|
783
|
+
try {
|
|
784
|
+
await this._verifyAuthCertOcsp(gatewayUrl, cert, expectedAid);
|
|
785
|
+
}
|
|
786
|
+
catch (e) {
|
|
787
|
+
const errMsg = e instanceof Error ? e.message : String(e);
|
|
788
|
+
if (/revoked/i.test(errMsg))
|
|
789
|
+
throw e instanceof AuthError ? e : new AuthError(errMsg);
|
|
790
|
+
this._log.warn('[aun_core.auth] OCSP check unavailable, degrade and continue:', errMsg);
|
|
791
|
+
}
|
|
792
|
+
if (cert.subjectCN !== expectedAid) {
|
|
793
|
+
throw new AuthError(`peer cert CN mismatch: expected ${expectedAid}, got ${cert.subjectCN || 'none'}`);
|
|
794
|
+
}
|
|
795
|
+
this._log.debug(`verifyPeerCertificate exit: elapsed=${Date.now() - tStart}ms aid=${expectedAid}`);
|
|
669
796
|
}
|
|
670
|
-
|
|
671
|
-
|
|
797
|
+
catch (err) {
|
|
798
|
+
this._log.debug(`verifyPeerCertificate exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
799
|
+
throw err;
|
|
672
800
|
}
|
|
673
801
|
}
|
|
674
802
|
// ── 内部方法:短连接 RPC ──────────────────────────
|
|
@@ -850,7 +978,9 @@ export class AuthFlow {
|
|
|
850
978
|
}
|
|
851
979
|
/** 初始化 WebSocket 会话(auth.connect RPC) */
|
|
852
980
|
async _initializeSession(transport, nonce, token, opts) {
|
|
853
|
-
const
|
|
981
|
+
const connectionKind = opts?.connectionKind ?? 'long';
|
|
982
|
+
const shortTtlMs = opts?.shortTtlMs ?? 0;
|
|
983
|
+
const request = {
|
|
854
984
|
nonce,
|
|
855
985
|
auth: { method: 'kite_token', token },
|
|
856
986
|
protocol: { min: '1.0', max: '1.0' },
|
|
@@ -858,7 +988,22 @@ export class AuthFlow {
|
|
|
858
988
|
client: { slot_id: String(opts?.slotId ?? this._slotId ?? '') },
|
|
859
989
|
delivery_mode: opts?.deliveryMode ?? { mode: 'fanout' },
|
|
860
990
|
capabilities: { e2ee: true, group_e2ee: true },
|
|
861
|
-
}
|
|
991
|
+
};
|
|
992
|
+
// 长短连接选项:默认 long 时不写入 options(保持 wire 兼容)
|
|
993
|
+
if (connectionKind === 'short') {
|
|
994
|
+
const options = { kind: 'short' };
|
|
995
|
+
if (shortTtlMs > 0) {
|
|
996
|
+
options.short_ttl_ms = shortTtlMs;
|
|
997
|
+
}
|
|
998
|
+
request.options = options;
|
|
999
|
+
}
|
|
1000
|
+
// extra_info:应用层自定义信息(PID/HOME/备注等),踢人时透传给被踢方
|
|
1001
|
+
const extraInfo = opts?.extraInfo;
|
|
1002
|
+
if (extraInfo && Object.keys(extraInfo).length > 0) {
|
|
1003
|
+
request.extra_info = extraInfo;
|
|
1004
|
+
}
|
|
1005
|
+
this._log.debug(`auth.connect send: device_id=${opts?.deviceId ?? ''} slot_id=${opts?.slotId ?? ''} kind=${connectionKind}`);
|
|
1006
|
+
const result = await transport.call('auth.connect', request);
|
|
862
1007
|
const status = isJsonObject(result) ? result.status : undefined;
|
|
863
1008
|
if (status !== 'ok') {
|
|
864
1009
|
throw new AuthError(`initialize failed: ${JSON.stringify(result)}`);
|
|
@@ -889,7 +1034,7 @@ export class AuthFlow {
|
|
|
889
1034
|
const errMsg = e instanceof Error ? e.message : String(e);
|
|
890
1035
|
if (/revoked/i.test(errMsg))
|
|
891
1036
|
throw e instanceof AuthError ? e : new AuthError(errMsg);
|
|
892
|
-
|
|
1037
|
+
this._log.warn('[aun_core.auth] CRL check unavailable, degrade and continue:', errMsg);
|
|
893
1038
|
}
|
|
894
1039
|
// 验证 OCSP
|
|
895
1040
|
try {
|
|
@@ -899,7 +1044,7 @@ export class AuthFlow {
|
|
|
899
1044
|
const errMsg = e instanceof Error ? e.message : String(e);
|
|
900
1045
|
if (/revoked/i.test(errMsg))
|
|
901
1046
|
throw e instanceof AuthError ? e : new AuthError(errMsg);
|
|
902
|
-
|
|
1047
|
+
this._log.warn('[aun_core.auth] OCSP check unavailable, degrade and continue:', errMsg);
|
|
903
1048
|
}
|
|
904
1049
|
// 验证 client_nonce 签名
|
|
905
1050
|
try {
|
|
@@ -1224,7 +1369,7 @@ export class AuthFlow {
|
|
|
1224
1369
|
const errMsg = e instanceof Error ? e.message : String(e);
|
|
1225
1370
|
if (/revoked/i.test(errMsg))
|
|
1226
1371
|
throw e instanceof AuthError ? e : new AuthError(errMsg);
|
|
1227
|
-
|
|
1372
|
+
this._log.warn('[aun_core.auth] CRL check unavailable, degrade and continue:', errMsg);
|
|
1228
1373
|
}
|
|
1229
1374
|
try {
|
|
1230
1375
|
await this._verifyAuthCertOcsp(gatewayUrl, cert);
|
|
@@ -1233,7 +1378,7 @@ export class AuthFlow {
|
|
|
1233
1378
|
const errMsg = e instanceof Error ? e.message : String(e);
|
|
1234
1379
|
if (/revoked/i.test(errMsg))
|
|
1235
1380
|
throw e instanceof AuthError ? e : new AuthError(errMsg);
|
|
1236
|
-
|
|
1381
|
+
this._log.warn('[aun_core.auth] OCSP check unavailable, degrade and continue:', errMsg);
|
|
1237
1382
|
}
|
|
1238
1383
|
}
|
|
1239
1384
|
// 验证通过,正式接受
|
|
@@ -1241,7 +1386,7 @@ export class AuthFlow {
|
|
|
1241
1386
|
}
|
|
1242
1387
|
catch (e) {
|
|
1243
1388
|
// 验证失败,静默拒绝(不影响主流程)
|
|
1244
|
-
|
|
1389
|
+
this._log.warn(`reject server-returned new_cert (${identity.aid}):`, e);
|
|
1245
1390
|
}
|
|
1246
1391
|
// active_cert 同步:验证公钥匹配后更新本地 cert
|
|
1247
1392
|
const activeCertPem = identity._pending_active_cert;
|
|
@@ -1256,12 +1401,12 @@ export class AuthFlow {
|
|
|
1256
1401
|
identity.cert = activeCertPem;
|
|
1257
1402
|
}
|
|
1258
1403
|
else {
|
|
1259
|
-
|
|
1404
|
+
this._log.warn(`[auth] active_cert public key mismatches local private key, reject sync (aid=${identity.aid})`);
|
|
1260
1405
|
}
|
|
1261
1406
|
}
|
|
1262
1407
|
}
|
|
1263
1408
|
catch (e) {
|
|
1264
|
-
|
|
1409
|
+
this._log.warn(`[auth] active_cert sync exception (${identity.aid}):`, e);
|
|
1265
1410
|
}
|
|
1266
1411
|
}
|
|
1267
1412
|
}
|
|
@@ -1291,13 +1436,23 @@ export class AuthFlow {
|
|
|
1291
1436
|
/** 确保本地有密钥对(没有则生成) */
|
|
1292
1437
|
async _ensureLocalIdentity(aid) {
|
|
1293
1438
|
const existing = await this._keystore.loadIdentity(aid);
|
|
1294
|
-
|
|
1439
|
+
// 必须确认有 keypair(private_key_pem + public_key_der_b64)才算"已存在"
|
|
1440
|
+
// 否则 keystore 可能只有 metadata(如 gateway_url)但没有真正的密钥材料
|
|
1441
|
+
if (existing && existing.private_key_pem && existing.public_key_der_b64) {
|
|
1295
1442
|
this._aid = aid;
|
|
1296
1443
|
return existing;
|
|
1297
1444
|
}
|
|
1298
1445
|
const identity = await this._crypto.generateIdentity();
|
|
1299
1446
|
identity.aid = aid;
|
|
1300
|
-
|
|
1447
|
+
// 保留 keystore 已有的 metadata(如 gateway_url),避免覆盖
|
|
1448
|
+
if (existing) {
|
|
1449
|
+
for (const [k, v] of Object.entries(existing)) {
|
|
1450
|
+
if (k !== 'aid' && !(k in identity)) {
|
|
1451
|
+
identity[k] = v;
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
await this._persistIdentity(identity); // 立即持久化 keypair,避免服务端拒绝后丢失
|
|
1301
1456
|
this._aid = aid;
|
|
1302
1457
|
return identity;
|
|
1303
1458
|
}
|