@agentunion/fastaun 0.3.3 → 0.3.5
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 +108 -85
- package/_packed_docs/CHANGELOG.md +108 -85
- package/_packed_docs/INDEX.md +81 -0
- package/_packed_docs/KITE_DOCS_GUIDE.md +55 -0
- 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 +328 -0
- package/_packed_docs/cli/AUN-CLI/350/256/276/350/256/241/346/226/207/346/241/243.md +686 -0
- package/_packed_docs/design/E2EE_V2/347/256/200/345/214/226/344/270/2721DH/345/212/240Per-AID_Wrap/346/226/271/346/241/210.md +124 -0
- package/_packed_docs/design//350/267/250/350/257/255/350/250/200/345/256/271/345/231/250E2E/346/265/213/350/257/225/346/226/271/346/241/210.md +665 -0
- package/_packed_docs/protocol//351/231/204/345/275/225N-/345/210/206/345/270/203/345/274/217Trace/345/215/217/350/256/256.md +257 -0
- package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +5 -5
- package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +1 -1
- package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +2 -2
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +46 -6
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +89 -12
- package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +19 -1
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +20 -5
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +8 -8
- package/_packed_docs/sdk/E2EE_V2/346/266/210/346/201/257/351/200/232/344/277/241/346/227/266/345/272/217/345/233/276.md +171 -0
- package/_packed_docs/sdk/INDEX.md +22 -22
- package/_packed_docs/sdk/README.md +3 -3
- package/dist/auth.d.ts +41 -8
- package/dist/auth.js +380 -101
- package/dist/auth.js.map +1 -1
- package/dist/client.d.ts +64 -19
- package/dist/client.js +1094 -443
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +4 -0
- package/dist/errors.js +7 -0
- package/dist/errors.js.map +1 -1
- package/dist/events.d.ts +9 -0
- package/dist/events.js +42 -12
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/keystore/file.d.ts +20 -0
- package/dist/keystore/file.js +91 -1
- package/dist/keystore/file.js.map +1 -1
- package/dist/namespaces/auth.d.ts +35 -4
- package/dist/namespaces/auth.js +175 -65
- package/dist/namespaces/auth.js.map +1 -1
- package/dist/secret-store/file-store.d.ts +21 -2
- package/dist/secret-store/file-store.js +166 -11
- package/dist/secret-store/file-store.js.map +1 -1
- package/dist/tools/cross-sdk-agent.js +2 -2
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +8 -1
- package/dist/transport.js +151 -32
- package/dist/transport.js.map +1 -1
- package/dist/v2/e2ee/decrypt.js +1 -1
- package/dist/v2/e2ee/decrypt.js.map +1 -1
- package/dist/v2/e2ee/encrypt-p2p.js +3 -2
- package/dist/v2/e2ee/encrypt-p2p.js.map +1 -1
- package/dist/v2/session/session.d.ts +1 -0
- package/dist/v2/session/session.js +7 -1
- package/dist/v2/session/session.js.map +1 -1
- package/package.json +46 -46
package/dist/auth.js
CHANGED
|
@@ -17,7 +17,7 @@ import * as https from 'node:https';
|
|
|
17
17
|
import * as path from 'node:path';
|
|
18
18
|
import { URL, fileURLToPath } from 'node:url';
|
|
19
19
|
import WebSocket from 'ws';
|
|
20
|
-
import { AuthError, StateError, ValidationError,
|
|
20
|
+
import { AuthError, StateError, ValidationError, IdentityConflictError, mapRemoteError } from './errors.js';
|
|
21
21
|
import { isJsonObject, } from './types.js';
|
|
22
22
|
const _noopLogger = {
|
|
23
23
|
error: () => { },
|
|
@@ -25,6 +25,8 @@ const _noopLogger = {
|
|
|
25
25
|
info: () => { },
|
|
26
26
|
debug: () => { },
|
|
27
27
|
};
|
|
28
|
+
const AUN_SDK_LANG = 'typescript';
|
|
29
|
+
const AUN_SDK_VERSION = '0.3.5';
|
|
28
30
|
// ── 签名验证辅助 ──────────────────────────────────────────────
|
|
29
31
|
/**
|
|
30
32
|
* 验证签名:支持 ECDSA P-256 (SHA256)、ECDSA P-384 (SHA384)、Ed25519。
|
|
@@ -310,51 +312,240 @@ export class AuthFlow {
|
|
|
310
312
|
this._slotId = String(opts.slotId ?? '').trim();
|
|
311
313
|
}
|
|
312
314
|
/**
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
+
* 注册新 AID(原子流程,必须由应用层显式调用)。
|
|
316
|
+
*
|
|
317
|
+
* 安全约束(与 Python register_aid 对齐):
|
|
318
|
+
* - **绝不**被 SDK 内部任何路径自动调用(authenticate / connect / 等)
|
|
319
|
+
* - 本地已有任何痕迹 → 抛 IdentityConflictError,应用层应改用 loadIdentity
|
|
320
|
+
* - 服务端已注册同名 AID → 抛 IdentityConflictError
|
|
321
|
+
*
|
|
322
|
+
* 异常分支与恢复:
|
|
323
|
+
* A. 全新流程:临时目录生成 keypair → RPC → cert → 原子 rename
|
|
324
|
+
* B. 恢复流程:上次 RPC 没收到响应就崩溃;扫描 _pending,公钥匹配
|
|
325
|
+
* 则下载 cert 完成 promote(防止"已注册但本地没保存")
|
|
315
326
|
*/
|
|
316
|
-
async
|
|
327
|
+
async registerAid(gatewayUrl, aid) {
|
|
317
328
|
const tStart = Date.now();
|
|
318
329
|
AuthFlow._validateAidName(aid);
|
|
319
|
-
this._logger.debug(`
|
|
330
|
+
this._logger.debug(`registerAid enter: aid=${aid}, gateway=${gatewayUrl}`);
|
|
320
331
|
try {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
332
|
+
// Step 1: 本地完整身份的幂等处理
|
|
333
|
+
// - 服务端 cert 公钥匹配 → 视为已注册成功,幂等返回
|
|
334
|
+
// - 服务端 cert 公钥不匹配 → IdentityConflictError(被别人占)
|
|
335
|
+
// - 服务端无记录 → IdentityConflictError(要求清理本地)
|
|
336
|
+
const existing = this._keystore.loadIdentity(aid);
|
|
337
|
+
if (existing !== null && existing.public_key_der_b64) {
|
|
338
|
+
this._logger.debug(`registerAid: local keypair exists, checking server for idempotency: aid=${aid}`);
|
|
339
|
+
const localPubB64 = String(existing.public_key_der_b64);
|
|
340
|
+
const serverCertPem = await this._downloadRegisteredCert(gatewayUrl, aid);
|
|
341
|
+
if (!serverCertPem) {
|
|
342
|
+
// 服务端无记录 → 用现有 keypair 发起注册
|
|
343
|
+
this._logger.debug(`registerAid: server has no record, registering with existing keypair: aid=${aid}`);
|
|
344
|
+
const pendingDir = this._keystore.pendingIdentityDir?.(aid);
|
|
345
|
+
let created;
|
|
346
|
+
try {
|
|
347
|
+
created = await this._createAid(gatewayUrl, existing);
|
|
348
|
+
}
|
|
349
|
+
catch (e) {
|
|
350
|
+
this._logger.warn(`registerAid RPC failed (existing keypair): aid=${aid}, error=${e instanceof Error ? e.message : String(e)}`);
|
|
351
|
+
throw e;
|
|
352
|
+
}
|
|
353
|
+
const certPem = String(created.cert ?? '');
|
|
354
|
+
if (!certPem) {
|
|
355
|
+
throw new AuthError(`registerAid: server response missing cert for ${aid}`);
|
|
356
|
+
}
|
|
357
|
+
existing.cert = certPem;
|
|
358
|
+
this._assertCertMatchesLocalKeypair(existing);
|
|
359
|
+
this._persistIdentity(existing);
|
|
360
|
+
this._aid = aid;
|
|
361
|
+
this._logger.debug(`registerAid exit (recovered): elapsed=${Date.now() - tStart}ms aid=${aid}`);
|
|
362
|
+
return { aid, cert: certPem };
|
|
363
|
+
}
|
|
364
|
+
// 比对公钥
|
|
365
|
+
const cert = _loadX509(serverCertPem);
|
|
366
|
+
const serverPubDer = _extractPublicKey(cert).export({ type: 'spki', format: 'der' });
|
|
367
|
+
const localPubDer = Buffer.from(localPubB64, 'base64');
|
|
368
|
+
if (!serverPubDer.equals(localPubDer)) {
|
|
369
|
+
throw new IdentityConflictError(`AID '${aid}' is registered by another party on server (public key mismatch). ` +
|
|
370
|
+
`Choose a different name.`);
|
|
371
|
+
}
|
|
372
|
+
// 公钥匹配 → 幂等返回;如本地缺 cert,把服务端 cert 写入
|
|
373
|
+
this._logger.info(`registerAid: idempotent return for already-registered AID: aid=${aid}`);
|
|
374
|
+
if (!existing.cert) {
|
|
375
|
+
existing.cert = serverCertPem;
|
|
376
|
+
this._persistIdentity(existing);
|
|
377
|
+
}
|
|
378
|
+
this._aid = aid;
|
|
379
|
+
return { aid, cert: serverCertPem };
|
|
380
|
+
}
|
|
381
|
+
// Step 2: 检查 _pending/ 残留临时目录(崩溃恢复)
|
|
382
|
+
const recovered = await this._tryRecoverPendingRegistration(gatewayUrl, aid);
|
|
383
|
+
if (recovered !== null) {
|
|
384
|
+
this._logger.info(`registerAid recovered from pending: aid=${aid}`);
|
|
385
|
+
return recovered;
|
|
386
|
+
}
|
|
387
|
+
// Step 3: 服务端查重
|
|
388
|
+
const existingCert = await this._downloadRegisteredCert(gatewayUrl, aid);
|
|
389
|
+
if (existingCert) {
|
|
390
|
+
this._logger.warn(`registerAid aborted: AID already registered on server: aid=${aid}`);
|
|
391
|
+
throw new IdentityConflictError(`AID '${aid}' is already registered on server. Choose a different name, or if you own the keypair use a recovery flow.`);
|
|
392
|
+
}
|
|
393
|
+
// Step 4: 创建临时目录 + 生成 keypair + 写 priv
|
|
394
|
+
const identity = this._crypto.generateIdentity();
|
|
395
|
+
identity.aid = aid;
|
|
396
|
+
const pendingDir = this._keystore.pendingIdentityDir(aid);
|
|
397
|
+
this._writePendingKeypair(pendingDir, identity);
|
|
398
|
+
// Step 5: 服务端注册拿 cert
|
|
399
|
+
let created;
|
|
400
|
+
try {
|
|
401
|
+
created = await this._createAid(gatewayUrl, identity);
|
|
402
|
+
}
|
|
403
|
+
catch (e) {
|
|
404
|
+
this._logger.warn(`registerAid RPC failed (pending kept for recovery): aid=${aid}, pending=${pendingDir}, error=${e instanceof Error ? e.message : String(e)}`);
|
|
405
|
+
throw e;
|
|
325
406
|
}
|
|
326
|
-
|
|
407
|
+
const certPem = String(created.cert ?? '');
|
|
408
|
+
if (!certPem) {
|
|
409
|
+
throw new AuthError(`registerAid: server response missing cert for ${aid}`);
|
|
410
|
+
}
|
|
411
|
+
identity.cert = certPem;
|
|
412
|
+
// Step 6: 校验 cert 公钥 == 本地公钥
|
|
413
|
+
this._assertCertMatchesLocalKeypair(identity);
|
|
414
|
+
// Step 7: 写 cert.pem 到临时目录
|
|
415
|
+
this._writePendingCert(pendingDir, certPem);
|
|
416
|
+
// Step 8: 原子 rename
|
|
327
417
|
try {
|
|
328
|
-
|
|
329
|
-
Object.assign(identity, created);
|
|
330
|
-
this._logger.debug(`AID register ok: aid=${aid}`);
|
|
418
|
+
this._keystore.promotePendingIdentity(pendingDir, aid);
|
|
331
419
|
}
|
|
332
420
|
catch (e) {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
throw e;
|
|
336
|
-
}
|
|
337
|
-
// AID 已在服务端注册,尝试从 PKI 下载恢复证书
|
|
338
|
-
try {
|
|
339
|
-
identity = await this._recoverCertViaDownload(gatewayUrl, identity);
|
|
340
|
-
}
|
|
341
|
-
catch {
|
|
342
|
-
throw new StateError(`AID ${aid} already registered on server but local certificate is missing. ` +
|
|
343
|
-
`Certificate download recovery failed. Options: ` +
|
|
344
|
-
`(1) use a different AID name, or ` +
|
|
345
|
-
`(2) restart Kite server to clear registration.`);
|
|
346
|
-
}
|
|
421
|
+
this._logger.warn(`registerAid promote failed: aid=${aid}, error=${e instanceof Error ? e.message : String(e)}`);
|
|
422
|
+
throw new IdentityConflictError(`AID '${aid}' was created by another process during registration; pending dir kept for cleanup.`);
|
|
347
423
|
}
|
|
424
|
+
// Step 9: 标准持久化
|
|
348
425
|
this._persistIdentity(identity);
|
|
349
|
-
this._aid =
|
|
350
|
-
this._logger.debug(`
|
|
426
|
+
this._aid = aid;
|
|
427
|
+
this._logger.debug(`registerAid exit: elapsed=${Date.now() - tStart}ms aid=${aid}`);
|
|
351
428
|
return { aid: identity.aid, cert: identity.cert };
|
|
352
429
|
}
|
|
353
430
|
catch (err) {
|
|
354
|
-
this._logger.debug(`
|
|
431
|
+
this._logger.debug(`registerAid exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
355
432
|
throw err;
|
|
356
433
|
}
|
|
357
434
|
}
|
|
435
|
+
/**
|
|
436
|
+
* 检查 AIDs/_pending/ 下是否有该 aid 的残留,尝试恢复(崩溃恢复)。
|
|
437
|
+
* 返回 dict 表示恢复成功;null 表示无残留 / 残留已清理;抛错表示明确失败。
|
|
438
|
+
*/
|
|
439
|
+
async _tryRecoverPendingRegistration(gatewayUrl, aid) {
|
|
440
|
+
const path = require('node:path');
|
|
441
|
+
const fs = require('node:fs');
|
|
442
|
+
const aidsRoot = this._keystore._aidsRoot;
|
|
443
|
+
const pendingRoot = path.join(aidsRoot, '_pending');
|
|
444
|
+
if (!fs.existsSync(pendingRoot))
|
|
445
|
+
return null;
|
|
446
|
+
const safe = this._keystore.constructor === undefined ? aid : aid;
|
|
447
|
+
const safeAid = aid.replace(/\//g, '_').replace(/\\/g, '_').replace(/:/g, '_');
|
|
448
|
+
const prefix = safeAid + '-';
|
|
449
|
+
const candidates = fs.readdirSync(pendingRoot, { withFileTypes: true })
|
|
450
|
+
.filter((e) => e.isDirectory() && e.name.startsWith(prefix))
|
|
451
|
+
.map((e) => path.join(pendingRoot, e.name))
|
|
452
|
+
.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
|
|
453
|
+
if (candidates.length === 0)
|
|
454
|
+
return null;
|
|
455
|
+
for (const pendingDir of candidates) {
|
|
456
|
+
const privPath = path.join(pendingDir, 'private', 'key.json');
|
|
457
|
+
if (!fs.existsSync(privPath)) {
|
|
458
|
+
try {
|
|
459
|
+
fs.rmSync(pendingDir, { recursive: true, force: true });
|
|
460
|
+
}
|
|
461
|
+
catch { /* ignore */ }
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
let privData;
|
|
465
|
+
try {
|
|
466
|
+
privData = JSON.parse(fs.readFileSync(privPath, 'utf-8'));
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
try {
|
|
470
|
+
fs.rmSync(pendingDir, { recursive: true, force: true });
|
|
471
|
+
}
|
|
472
|
+
catch { /* ignore */ }
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
const localPriv = privData.private_key_pem ?? '';
|
|
476
|
+
const localPubB64 = privData.public_key_der_b64 ?? '';
|
|
477
|
+
if (!localPriv || !localPubB64) {
|
|
478
|
+
try {
|
|
479
|
+
fs.rmSync(pendingDir, { recursive: true, force: true });
|
|
480
|
+
}
|
|
481
|
+
catch { /* ignore */ }
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
const serverCert = await this._downloadRegisteredCert(gatewayUrl, aid);
|
|
485
|
+
if (!serverCert) {
|
|
486
|
+
// 上次 RPC 没生效 → 清理临时目录,由调用方走全新注册流程
|
|
487
|
+
this._logger.info(`pending dir found but server has no registration; cleaning up: ${pendingDir}`);
|
|
488
|
+
try {
|
|
489
|
+
fs.rmSync(pendingDir, { recursive: true, force: true });
|
|
490
|
+
}
|
|
491
|
+
catch { /* ignore */ }
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
// 比对公钥
|
|
495
|
+
const cert = _loadX509(serverCert);
|
|
496
|
+
const certPubKey = _extractPublicKey(cert);
|
|
497
|
+
const certPubDer = certPubKey.export({ type: 'spki', format: 'der' });
|
|
498
|
+
const localPubDer = Buffer.from(localPubB64, 'base64');
|
|
499
|
+
if (!certPubDer.equals(localPubDer)) {
|
|
500
|
+
this._logger.warn(`pending dir public key does not match server cert; AID '${aid}' was taken`);
|
|
501
|
+
try {
|
|
502
|
+
fs.rmSync(pendingDir, { recursive: true, force: true });
|
|
503
|
+
}
|
|
504
|
+
catch { /* ignore */ }
|
|
505
|
+
throw new IdentityConflictError(`AID '${aid}' has been registered by another party while local pending registration was incomplete; local pending key discarded.`);
|
|
506
|
+
}
|
|
507
|
+
// 公钥匹配 → 上次 RPC 成功,补 cert + promote
|
|
508
|
+
this._logger.info(`pending recovery: cert public key matches; finalizing registration: aid=${aid}`);
|
|
509
|
+
this._writePendingCert(pendingDir, serverCert);
|
|
510
|
+
const identity = {
|
|
511
|
+
aid,
|
|
512
|
+
private_key_pem: localPriv,
|
|
513
|
+
public_key_der_b64: localPubB64,
|
|
514
|
+
curve: privData.curve ?? 'P-256',
|
|
515
|
+
cert: serverCert,
|
|
516
|
+
};
|
|
517
|
+
try {
|
|
518
|
+
this._keystore.promotePendingIdentity(pendingDir, aid);
|
|
519
|
+
}
|
|
520
|
+
catch (e) {
|
|
521
|
+
throw new IdentityConflictError(`AID '${aid}' was created by another process during recovery; pending dir kept for cleanup.`);
|
|
522
|
+
}
|
|
523
|
+
this._persistIdentity(identity);
|
|
524
|
+
this._aid = aid;
|
|
525
|
+
return { aid, cert: serverCert };
|
|
526
|
+
}
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
_writePendingKeypair(pendingDir, identity) {
|
|
530
|
+
const fs = require('node:fs');
|
|
531
|
+
const path = require('node:path');
|
|
532
|
+
const priv = String(identity.private_key_pem ?? '');
|
|
533
|
+
const pub = String(identity.public_key_der_b64 ?? '');
|
|
534
|
+
const curve = String(identity.curve ?? 'P-256');
|
|
535
|
+
if (!priv || !pub) {
|
|
536
|
+
throw new AuthError('registerAid: generated identity missing keypair fields');
|
|
537
|
+
}
|
|
538
|
+
const dir = path.join(pendingDir, 'private');
|
|
539
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
540
|
+
fs.writeFileSync(path.join(dir, 'key.json'), JSON.stringify({ private_key_pem: priv, public_key_der_b64: pub, curve }, null, 2), { encoding: 'utf-8', mode: 0o600 });
|
|
541
|
+
}
|
|
542
|
+
_writePendingCert(pendingDir, certPem) {
|
|
543
|
+
const fs = require('node:fs');
|
|
544
|
+
const path = require('node:path');
|
|
545
|
+
const dir = path.join(pendingDir, 'public');
|
|
546
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
547
|
+
fs.writeFileSync(path.join(dir, 'cert.pem'), certPem, { encoding: 'utf-8', mode: 0o600 });
|
|
548
|
+
}
|
|
358
549
|
/**
|
|
359
550
|
* 认证(登录)到 Gateway。
|
|
360
551
|
* 执行两阶段挑战-应答认证,返回 token 信息。
|
|
@@ -373,15 +564,24 @@ export class AuthFlow {
|
|
|
373
564
|
const cachedToken = AuthFlow._getCachedAccessToken(identityWithState);
|
|
374
565
|
const cachedRefresh = String(identityWithState.refresh_token ?? '');
|
|
375
566
|
if (cachedToken && cachedRefresh) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
567
|
+
// 复用 cached token 前必须验证本地身份完整性:
|
|
568
|
+
// cert 存在 + cert 公钥 == keypair 公钥 + cert 时间窗口有效。
|
|
569
|
+
// 任何一项不通过 → 不复用,走两步重登(或抛错)。
|
|
570
|
+
const credIssue = this._validateCachedCredentials(identity);
|
|
571
|
+
if (!credIssue) {
|
|
572
|
+
this._logger.debug(`authenticate reusing cached token: aid=${identity.aid} expires_at=${identityWithState.access_token_expires_at}`);
|
|
573
|
+
this._aid = String(identity.aid);
|
|
574
|
+
return {
|
|
575
|
+
aid: identity.aid,
|
|
576
|
+
access_token: cachedToken,
|
|
577
|
+
refresh_token: cachedRefresh,
|
|
578
|
+
expires_at: identityWithState.access_token_expires_at,
|
|
579
|
+
gateway: gatewayUrl,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
this._logger.info(`cached token not reused (credential issue): aid=${identity.aid} reason=${credIssue}`);
|
|
584
|
+
}
|
|
385
585
|
}
|
|
386
586
|
if (!identity.cert) {
|
|
387
587
|
// 本地有密钥但无证书——尝试从 PKI 下载恢复
|
|
@@ -391,26 +591,20 @@ export class AuthFlow {
|
|
|
391
591
|
}
|
|
392
592
|
catch (e) {
|
|
393
593
|
throw new StateError(`local certificate missing and recovery failed: ${e instanceof Error ? e.message : String(e)}. ` +
|
|
394
|
-
`Run auth.
|
|
594
|
+
`Run auth.registerAid() to register a new identity.`);
|
|
395
595
|
}
|
|
396
596
|
}
|
|
597
|
+
// 防线 B:发起两步登录前显式校验 cert 公钥与本地 keypair 公钥一致。
|
|
598
|
+
this._assertCertMatchesLocalKeypair(identity);
|
|
397
599
|
let login;
|
|
398
600
|
try {
|
|
399
601
|
login = await this._login(gatewayUrl, identity);
|
|
400
602
|
this._logger.debug(`auth login ok: aid=${identity.aid}`);
|
|
401
603
|
}
|
|
402
604
|
catch (e) {
|
|
403
|
-
//
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
const created = await this._createAid(gatewayUrl, identity);
|
|
407
|
-
identity.cert = created.cert;
|
|
408
|
-
this._persistIdentity(identity);
|
|
409
|
-
login = await this._login(gatewayUrl, identity);
|
|
410
|
-
}
|
|
411
|
-
else {
|
|
412
|
-
throw e;
|
|
413
|
-
}
|
|
605
|
+
// 注册和登录彻底分离:登录失败绝不触发自动注册。
|
|
606
|
+
// 服务端报"not registered"时,应用层应当显式调 registerAid。
|
|
607
|
+
throw e;
|
|
414
608
|
}
|
|
415
609
|
AuthFlow._rememberTokens(identity, login);
|
|
416
610
|
await this._validateNewCert(identity, gatewayUrl);
|
|
@@ -438,12 +632,14 @@ export class AuthFlow {
|
|
|
438
632
|
const tStart = Date.now();
|
|
439
633
|
this._logger.debug(`ensureAuthenticated enter: gateway=${gatewayUrl}`);
|
|
440
634
|
try {
|
|
441
|
-
|
|
635
|
+
// 注册和登录彻底分离:无身份直接抛错,绝不再隐式生成密钥。
|
|
636
|
+
const identity = this._loadIdentityOrRaise();
|
|
442
637
|
if (!identity.cert) {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
this._persistIdentity(identity);
|
|
638
|
+
throw new StateError(`local identity for aid ${identity.aid} has no certificate; ` +
|
|
639
|
+
`call auth.authenticate() to attempt cert recovery, or auth.registerAid() if this is a fresh registration.`);
|
|
446
640
|
}
|
|
641
|
+
// 防线 B:发起两步登录前显式校验
|
|
642
|
+
this._assertCertMatchesLocalKeypair(identity);
|
|
447
643
|
const login = await this._login(gatewayUrl, identity);
|
|
448
644
|
AuthFlow._rememberTokens(identity, login);
|
|
449
645
|
await this._validateNewCert(identity, gatewayUrl);
|
|
@@ -686,6 +882,25 @@ export class AuthFlow {
|
|
|
686
882
|
throw err;
|
|
687
883
|
}
|
|
688
884
|
}
|
|
885
|
+
/** 预置 Gateway CA 链材料,但不标记为已验证。 */
|
|
886
|
+
cacheGatewayCaChain(gatewayUrl, caChainPems, chainAid = '') {
|
|
887
|
+
const normalized = caChainPems
|
|
888
|
+
.map((pem) => String(pem ?? '').trim())
|
|
889
|
+
.filter((pem) => pem.length > 0);
|
|
890
|
+
if (normalized.length === 0)
|
|
891
|
+
return;
|
|
892
|
+
for (const pem of normalized)
|
|
893
|
+
_loadX509(pem);
|
|
894
|
+
const cacheKey = chainAid ? `${gatewayUrl}:${chainAid}` : gatewayUrl;
|
|
895
|
+
this._gatewayChainCache.set(cacheKey, normalized);
|
|
896
|
+
this._gatewayCaVerified.delete(cacheKey);
|
|
897
|
+
}
|
|
898
|
+
/** 丢弃预置或缓存的 Gateway CA 链材料。 */
|
|
899
|
+
discardGatewayCaChain(gatewayUrl, chainAid = '') {
|
|
900
|
+
const cacheKey = chainAid ? `${gatewayUrl}:${chainAid}` : gatewayUrl;
|
|
901
|
+
this._gatewayChainCache.delete(cacheKey);
|
|
902
|
+
this._gatewayCaVerified.delete(cacheKey);
|
|
903
|
+
}
|
|
689
904
|
// ── 内部方法:短连接 RPC ────────────────────────────────────
|
|
690
905
|
/**
|
|
691
906
|
* 通过临时 WebSocket 发送单次 JSON-RPC 请求。
|
|
@@ -845,7 +1060,11 @@ export class AuthFlow {
|
|
|
845
1060
|
auth: { method: 'kite_token', token },
|
|
846
1061
|
protocol: { min: '1.0', max: '1.0' },
|
|
847
1062
|
device: { id: String(opts?.deviceId ?? ''), type: 'sdk' },
|
|
848
|
-
client: {
|
|
1063
|
+
client: {
|
|
1064
|
+
slot_id: String(opts?.slotId ?? ''),
|
|
1065
|
+
sdk_lang: AUN_SDK_LANG,
|
|
1066
|
+
sdk_version: AUN_SDK_VERSION,
|
|
1067
|
+
},
|
|
849
1068
|
delivery_mode: opts?.deliveryMode ?? { mode: 'fanout' },
|
|
850
1069
|
capabilities,
|
|
851
1070
|
};
|
|
@@ -1480,9 +1699,8 @@ export class AuthFlow {
|
|
|
1480
1699
|
* 本地有密钥但无证书、服务端已注册时使用。
|
|
1481
1700
|
*/
|
|
1482
1701
|
async _recoverCertViaDownload(gatewayUrl, identity) {
|
|
1483
|
-
const
|
|
1484
|
-
|
|
1485
|
-
if (!certPem || !certPem.includes('BEGIN CERTIFICATE')) {
|
|
1702
|
+
const certPem = await this._downloadRegisteredCert(gatewayUrl, String(identity.aid));
|
|
1703
|
+
if (!certPem) {
|
|
1486
1704
|
throw new AuthError(`failed to download certificate for ${identity.aid}`);
|
|
1487
1705
|
}
|
|
1488
1706
|
// 验证下载的证书公钥与本地密钥对匹配
|
|
@@ -1497,6 +1715,93 @@ export class AuthFlow {
|
|
|
1497
1715
|
identity.cert = certPem;
|
|
1498
1716
|
return identity;
|
|
1499
1717
|
}
|
|
1718
|
+
/**
|
|
1719
|
+
* 验证本地身份是否适合复用 cached token(不走网络)。
|
|
1720
|
+
* 返回空字符串表示通过;非空字符串表示不通过的原因。
|
|
1721
|
+
* 公钥不匹配时直接抛 AuthError(严重安全问题,不能静默降级)。
|
|
1722
|
+
*/
|
|
1723
|
+
_validateCachedCredentials(identity) {
|
|
1724
|
+
const aid = identity.aid ?? '?';
|
|
1725
|
+
const certPem = identity.cert;
|
|
1726
|
+
const localPubB64 = identity.public_key_der_b64;
|
|
1727
|
+
if (!certPem)
|
|
1728
|
+
return 'no certificate';
|
|
1729
|
+
if (!localPubB64)
|
|
1730
|
+
return 'no public key';
|
|
1731
|
+
try {
|
|
1732
|
+
const cert = _loadX509(certPem);
|
|
1733
|
+
const certPubKey = _extractPublicKey(cert);
|
|
1734
|
+
const certPubDer = certPubKey.export({ type: 'spki', format: 'der' });
|
|
1735
|
+
const localPubDer = Buffer.from(localPubB64, 'base64');
|
|
1736
|
+
if (!certPubDer.equals(localPubDer)) {
|
|
1737
|
+
throw new AuthError(`local certificate public key does not match local keypair for aid ${aid}; ` +
|
|
1738
|
+
`refusing to authenticate. Run auth.registerAid() to repair identity.`);
|
|
1739
|
+
}
|
|
1740
|
+
// cert 时间窗口
|
|
1741
|
+
const now = Date.now();
|
|
1742
|
+
const validFrom = new Date(cert.validFrom).getTime();
|
|
1743
|
+
const validTo = new Date(cert.validTo).getTime();
|
|
1744
|
+
if (now < validFrom)
|
|
1745
|
+
return 'cert not yet valid';
|
|
1746
|
+
if (now > validTo)
|
|
1747
|
+
return 'cert expired';
|
|
1748
|
+
return '';
|
|
1749
|
+
}
|
|
1750
|
+
catch (e) {
|
|
1751
|
+
if (e instanceof AuthError)
|
|
1752
|
+
throw e;
|
|
1753
|
+
return `cert/keypair parse error: ${e instanceof Error ? e.message : String(e)}`;
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
/**
|
|
1757
|
+
* 防线 B:authenticate 在调 _login 前显式校验 cert 与本地 keypair 公钥一致。
|
|
1758
|
+
* 命中即抛 AuthError,绝不发起两步登录。
|
|
1759
|
+
*/
|
|
1760
|
+
_assertCertMatchesLocalKeypair(identity) {
|
|
1761
|
+
const aid = identity.aid ?? '?';
|
|
1762
|
+
const certPem = identity.cert;
|
|
1763
|
+
const localPubB64 = identity.public_key_der_b64;
|
|
1764
|
+
if (!certPem || !localPubB64) {
|
|
1765
|
+
throw new AuthError(`identity for aid ${aid} missing cert or public key; refusing to start two-phase login`);
|
|
1766
|
+
}
|
|
1767
|
+
let certPubDer;
|
|
1768
|
+
let localPubDer;
|
|
1769
|
+
try {
|
|
1770
|
+
const cert = _loadX509(certPem);
|
|
1771
|
+
const certPubKey = _extractPublicKey(cert);
|
|
1772
|
+
certPubDer = certPubKey.export({ type: 'spki', format: 'der' });
|
|
1773
|
+
localPubDer = Buffer.from(localPubB64, 'base64');
|
|
1774
|
+
}
|
|
1775
|
+
catch (e) {
|
|
1776
|
+
throw new AuthError(`failed to parse local cert/keypair for aid ${aid}: ${e instanceof Error ? e.message : String(e)}`);
|
|
1777
|
+
}
|
|
1778
|
+
if (!certPubDer.equals(localPubDer)) {
|
|
1779
|
+
throw new AuthError(`local certificate public key does not match local keypair for aid ${aid}; ` +
|
|
1780
|
+
`refusing to start two-phase login. Run auth.registerAid() to repair identity.`);
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
/**
|
|
1784
|
+
* 通过 PKI HTTP 端点下载服务端登记的证书。
|
|
1785
|
+
* 404 / 找不到 → 返回 null(视为未注册);其它 HTTP 错抛 AuthError。
|
|
1786
|
+
* 该方法既用于 recover 路径,也用于 registerAid 全新注册前的查重前置。
|
|
1787
|
+
*/
|
|
1788
|
+
async _downloadRegisteredCert(gatewayUrl, aid) {
|
|
1789
|
+
const certUrl = _gatewayHttpUrl(gatewayUrl, `/pki/cert/${aid}`);
|
|
1790
|
+
try {
|
|
1791
|
+
const certPem = await _fetchText(certUrl, this._verifySsl);
|
|
1792
|
+
if (!certPem || !certPem.includes('BEGIN CERTIFICATE')) {
|
|
1793
|
+
return null;
|
|
1794
|
+
}
|
|
1795
|
+
return certPem;
|
|
1796
|
+
}
|
|
1797
|
+
catch (e) {
|
|
1798
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1799
|
+
if (msg.includes('404') || msg.toLowerCase().includes('not found')) {
|
|
1800
|
+
return null;
|
|
1801
|
+
}
|
|
1802
|
+
throw new AuthError(`failed to fetch ${certUrl}: ${msg}`);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1500
1805
|
// ── 内部方法:new_cert 验证 ────────────────────────────────
|
|
1501
1806
|
/**
|
|
1502
1807
|
* 验证服务端返回的 new_cert,通过后才正式接受。
|
|
@@ -1643,30 +1948,9 @@ export class AuthFlow {
|
|
|
1643
1948
|
throw new ValidationError("AID name must not start with 'guest'");
|
|
1644
1949
|
}
|
|
1645
1950
|
}
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
// 必须确认有 keypair(private_key_pem + public_key_der_b64)才算"已存在"
|
|
1650
|
-
// 否则 keystore 可能只有 metadata(如 gateway_url)但没有真正的密钥材料
|
|
1651
|
-
if (existing && existing.private_key_pem && existing.public_key_der_b64) {
|
|
1652
|
-
this._aid = aid;
|
|
1653
|
-
return existing;
|
|
1654
|
-
}
|
|
1655
|
-
const identity = this._crypto.generateIdentity();
|
|
1656
|
-
identity.aid = aid;
|
|
1657
|
-
// 保留 keystore 已有的 metadata(如 gateway_url),避免覆盖
|
|
1658
|
-
if (existing) {
|
|
1659
|
-
for (const [k, v] of Object.entries(existing)) {
|
|
1660
|
-
if (k !== 'aid' && !(k in identity)) {
|
|
1661
|
-
identity[k] = v;
|
|
1662
|
-
}
|
|
1663
|
-
}
|
|
1664
|
-
}
|
|
1665
|
-
this._persistIdentity(identity); // 立即持久化密钥对
|
|
1666
|
-
this._aid = aid;
|
|
1667
|
-
return identity;
|
|
1668
|
-
}
|
|
1669
|
-
/** 加载身份信息,不存在时抛出 StateError */
|
|
1951
|
+
// (_ensureLocalIdentity 已硬移除:注册和登录彻底分离,登录路径绝不再
|
|
1952
|
+
// 隐式生成密钥;新身份必须由应用层显式调 registerAid)
|
|
1953
|
+
/** 加载身份信息,不存在或半成品时抛出 StateError */
|
|
1670
1954
|
_loadIdentityOrRaise(aid) {
|
|
1671
1955
|
const requestedAid = aid ?? this._aid;
|
|
1672
1956
|
if (requestedAid) {
|
|
@@ -1674,6 +1958,13 @@ export class AuthFlow {
|
|
|
1674
1958
|
if (existing === null) {
|
|
1675
1959
|
throw new StateError(`identity not found for aid: ${requestedAid}`);
|
|
1676
1960
|
}
|
|
1961
|
+
// 防线 A:拒绝半成品 identity(缺 keypair 任一字段)。
|
|
1962
|
+
// 这是上次 registerAid 半完成 / keystore 损坏的常见症状;如果让它流到
|
|
1963
|
+
// _login,签名一定失败,且服务端可能记录一次无效登录尝试。
|
|
1964
|
+
if (!existing.private_key_pem || !existing.public_key_der_b64) {
|
|
1965
|
+
throw new StateError(`local identity for aid ${requestedAid} is incomplete (missing keypair); ` +
|
|
1966
|
+
`call auth.registerAid() first`);
|
|
1967
|
+
}
|
|
1677
1968
|
this._aid = requestedAid;
|
|
1678
1969
|
if (!existing.aid)
|
|
1679
1970
|
existing.aid = requestedAid;
|
|
@@ -1684,6 +1975,9 @@ export class AuthFlow {
|
|
|
1684
1975
|
if (typeof ks.loadAnyIdentity === 'function') {
|
|
1685
1976
|
const existing = ks.loadAnyIdentity();
|
|
1686
1977
|
if (existing !== null && existing !== undefined) {
|
|
1978
|
+
if (!existing.private_key_pem || !existing.public_key_der_b64) {
|
|
1979
|
+
throw new StateError(`local identity is incomplete (missing keypair); call auth.registerAid() first`);
|
|
1980
|
+
}
|
|
1687
1981
|
const loadedAid = existing.aid;
|
|
1688
1982
|
if (typeof loadedAid === 'string' && loadedAid) {
|
|
1689
1983
|
this._aid = loadedAid;
|
|
@@ -1691,25 +1985,10 @@ export class AuthFlow {
|
|
|
1691
1985
|
return existing;
|
|
1692
1986
|
}
|
|
1693
1987
|
}
|
|
1694
|
-
throw new StateError('no local identity found, call auth.
|
|
1695
|
-
}
|
|
1696
|
-
/** 确保有身份(不存在时自动创建密钥对) */
|
|
1697
|
-
_ensureIdentity() {
|
|
1698
|
-
try {
|
|
1699
|
-
return this._loadIdentityOrRaise();
|
|
1700
|
-
}
|
|
1701
|
-
catch (e) {
|
|
1702
|
-
if (!(e instanceof StateError))
|
|
1703
|
-
throw e;
|
|
1704
|
-
if (!this._aid) {
|
|
1705
|
-
throw new StateError('no local identity found, call auth.createAid() first');
|
|
1706
|
-
}
|
|
1707
|
-
const identity = this._crypto.generateIdentity();
|
|
1708
|
-
identity.aid = this._aid;
|
|
1709
|
-
this._persistIdentity(identity); // 立即持久化
|
|
1710
|
-
return identity;
|
|
1711
|
-
}
|
|
1988
|
+
throw new StateError('no local identity found, call auth.registerAid() first');
|
|
1712
1989
|
}
|
|
1990
|
+
// (_ensureIdentity 已硬移除:注册和登录彻底分离,登录路径绝不再隐式
|
|
1991
|
+
// 生成密钥;调用方应改用 _loadIdentityOrRaise 获取已注册身份)
|
|
1713
1992
|
_loadInstanceState(aid) {
|
|
1714
1993
|
const loader = this._keystore.loadInstanceState;
|
|
1715
1994
|
if (typeof loader !== 'function') {
|