@agentunion/fastaun 0.3.3 → 0.3.4

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/_packed_docs/CHANGELOG.md +21 -0
  3. package/_packed_docs/INDEX.md +81 -0
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +55 -0
  5. 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
  6. package/_packed_docs/cli/AUN-CLI/350/256/276/350/256/241/346/226/207/346/241/243.md +686 -0
  7. 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
  8. 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
  9. package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +5 -5
  10. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +1 -1
  11. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +2 -2
  12. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +454 -429
  13. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +1410 -1398
  14. package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +19 -1
  15. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +20 -5
  16. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +8 -8
  17. 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
  18. package/_packed_docs/sdk/INDEX.md +22 -22
  19. package/_packed_docs/sdk/README.md +3 -3
  20. package/dist/auth.d.ts +41 -8
  21. package/dist/auth.js +380 -101
  22. package/dist/auth.js.map +1 -1
  23. package/dist/client.d.ts +60 -19
  24. package/dist/client.js +1049 -443
  25. package/dist/client.js.map +1 -1
  26. package/dist/errors.d.ts +4 -0
  27. package/dist/errors.js +7 -0
  28. package/dist/errors.js.map +1 -1
  29. package/dist/events.d.ts +9 -0
  30. package/dist/events.js +42 -12
  31. package/dist/events.js.map +1 -1
  32. package/dist/index.d.ts +2 -2
  33. package/dist/index.js +2 -2
  34. package/dist/index.js.map +1 -1
  35. package/dist/keystore/file.d.ts +20 -0
  36. package/dist/keystore/file.js +91 -1
  37. package/dist/keystore/file.js.map +1 -1
  38. package/dist/namespaces/auth.d.ts +33 -4
  39. package/dist/namespaces/auth.js +170 -65
  40. package/dist/namespaces/auth.js.map +1 -1
  41. package/dist/secret-store/file-store.d.ts +21 -2
  42. package/dist/secret-store/file-store.js +166 -11
  43. package/dist/secret-store/file-store.js.map +1 -1
  44. package/dist/tools/cross-sdk-agent.js +2 -2
  45. package/dist/tools/cross-sdk-agent.js.map +1 -1
  46. package/dist/transport.d.ts +8 -1
  47. package/dist/transport.js +151 -32
  48. package/dist/transport.js.map +1 -1
  49. package/dist/v2/e2ee/decrypt.js +1 -1
  50. package/dist/v2/e2ee/decrypt.js.map +1 -1
  51. package/dist/v2/e2ee/encrypt-p2p.js +3 -2
  52. package/dist/v2/e2ee/encrypt-p2p.js.map +1 -1
  53. package/dist/v2/session/session.d.ts +1 -0
  54. package/dist/v2/session/session.js +7 -1
  55. package/dist/v2/session/session.js.map +1 -1
  56. 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, AUNError, mapRemoteError } from './errors.js';
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.4';
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
- * 创建 AID 并注册到 Gateway。
314
- * 如果 AID 已在服务端注册但本地证书丢失,尝试从 PKI 端点下载恢复。
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 createAid(gatewayUrl, aid) {
327
+ async registerAid(gatewayUrl, aid) {
317
328
  const tStart = Date.now();
318
329
  AuthFlow._validateAidName(aid);
319
- this._logger.debug(`createAid enter: aid=${aid}, gateway=${gatewayUrl}`);
330
+ this._logger.debug(`registerAid enter: aid=${aid}, gateway=${gatewayUrl}`);
320
331
  try {
321
- let identity = this._ensureLocalIdentity(aid);
322
- if (identity.cert) {
323
- this._logger.debug(`createAid exit: elapsed=${Date.now() - tStart}ms (already has cert) aid=${aid}`);
324
- return { aid: identity.aid, cert: identity.cert };
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
- const created = await this._createAid(gatewayUrl, identity);
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
- if (!(e instanceof AUNError) || !String(e.message).includes('already exists')) {
334
- this._logger.error(`AID register failed: aid=${aid}, error=${e instanceof Error ? e.message : String(e)}`);
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 = String(identity.aid);
350
- this._logger.debug(`createAid exit: elapsed=${Date.now() - tStart}ms aid=${aid}`);
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(`createAid exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
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
- this._logger.debug(`authenticate reusing cached token: aid=${identity.aid} expires_at=${identityWithState.access_token_expires_at}`);
377
- this._aid = String(identity.aid);
378
- return {
379
- aid: identity.aid,
380
- access_token: cachedToken,
381
- refresh_token: cachedRefresh,
382
- expires_at: identityWithState.access_token_expires_at,
383
- gateway: gatewayUrl,
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.createAid() to register a new identity.`);
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
- if (e instanceof AuthError && (String(e.message).includes('not registered') || String(e.message).includes('public key mismatch'))) {
405
- this._logger.warn(`cert not registered on server, auto re-register: aid=${identity.aid}`);
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
- const identity = this._ensureIdentity();
635
+ // 注册和登录彻底分离:无身份直接抛错,绝不再隐式生成密钥。
636
+ const identity = this._loadIdentityOrRaise();
442
637
  if (!identity.cert) {
443
- const created = await this._createAid(gatewayUrl, identity);
444
- Object.assign(identity, created);
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: { slot_id: String(opts?.slotId ?? '') },
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 certUrl = _gatewayHttpUrl(gatewayUrl, `/pki/cert/${identity.aid}`);
1484
- const certPem = await _fetchText(certUrl, this._verifySsl);
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
- /** 确保本地有指定 AID 的身份(不存在则创建密钥对) */
1647
- _ensureLocalIdentity(aid) {
1648
- const existing = this._keystore.loadIdentity(aid);
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.createAid() first');
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') {