@agentunion/fastaun 0.3.2 → 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 (85) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/_packed_docs/CHANGELOG.md +43 -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 -396
  13. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +1410 -1244
  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 +6 -4
  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 +9 -4
  19. package/_packed_docs/sdk/README.md +3 -3
  20. package/dist/auth.d.ts +44 -8
  21. package/dist/auth.js +398 -119
  22. package/dist/auth.js.map +1 -1
  23. package/dist/client.d.ts +123 -19
  24. package/dist/client.js +2650 -673
  25. package/dist/client.js.map +1 -1
  26. package/dist/discovery.d.ts +4 -0
  27. package/dist/discovery.js +28 -13
  28. package/dist/discovery.js.map +1 -1
  29. package/dist/errors.d.ts +4 -0
  30. package/dist/errors.js +7 -0
  31. package/dist/errors.js.map +1 -1
  32. package/dist/events.d.ts +9 -0
  33. package/dist/events.js +42 -12
  34. package/dist/events.js.map +1 -1
  35. package/dist/index.d.ts +2 -2
  36. package/dist/index.js +2 -2
  37. package/dist/index.js.map +1 -1
  38. package/dist/keystore/aid-db.d.ts +4 -0
  39. package/dist/keystore/aid-db.js +94 -0
  40. package/dist/keystore/aid-db.js.map +1 -1
  41. package/dist/keystore/file.d.ts +23 -1
  42. package/dist/keystore/file.js +109 -1
  43. package/dist/keystore/file.js.map +1 -1
  44. package/dist/keystore/index.d.ts +20 -0
  45. package/dist/logger.d.ts +2 -0
  46. package/dist/logger.js +7 -4
  47. package/dist/logger.js.map +1 -1
  48. package/dist/namespaces/auth.d.ts +34 -4
  49. package/dist/namespaces/auth.js +194 -51
  50. package/dist/namespaces/auth.js.map +1 -1
  51. package/dist/net.d.ts +43 -0
  52. package/dist/net.js +192 -0
  53. package/dist/net.js.map +1 -0
  54. package/dist/secret-store/file-store.d.ts +21 -2
  55. package/dist/secret-store/file-store.js +166 -11
  56. package/dist/secret-store/file-store.js.map +1 -1
  57. package/dist/seq-tracker.d.ts +32 -3
  58. package/dist/seq-tracker.js +60 -3
  59. package/dist/seq-tracker.js.map +1 -1
  60. package/dist/tools/cross-sdk-agent.d.ts +2 -0
  61. package/dist/tools/cross-sdk-agent.js +695 -0
  62. package/dist/tools/cross-sdk-agent.js.map +1 -0
  63. package/dist/transport.d.ts +10 -1
  64. package/dist/transport.js +196 -32
  65. package/dist/transport.js.map +1 -1
  66. package/dist/v2/crypto/canonical.d.ts +1 -1
  67. package/dist/v2/crypto/canonical.js +42 -17
  68. package/dist/v2/crypto/canonical.js.map +1 -1
  69. package/dist/v2/e2ee/decrypt.js +57 -3
  70. package/dist/v2/e2ee/decrypt.js.map +1 -1
  71. package/dist/v2/e2ee/encrypt-group.js +16 -7
  72. package/dist/v2/e2ee/encrypt-group.js.map +1 -1
  73. package/dist/v2/e2ee/encrypt-p2p.js +42 -9
  74. package/dist/v2/e2ee/encrypt-p2p.js.map +1 -1
  75. package/dist/v2/e2ee/metadata-auth.d.ts +1 -0
  76. package/dist/v2/e2ee/metadata-auth.js +37 -1
  77. package/dist/v2/e2ee/metadata-auth.js.map +1 -1
  78. package/dist/v2/e2ee/types.d.ts +2 -2
  79. package/dist/v2/session/keystore.d.ts +10 -3
  80. package/dist/v2/session/keystore.js +158 -30
  81. package/dist/v2/session/keystore.js.map +1 -1
  82. package/dist/v2/session/session.d.ts +7 -3
  83. package/dist/v2/session/session.js +64 -12
  84. package/dist/v2/session/session.js.map +1 -1
  85. 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。
@@ -159,8 +161,11 @@ function _gatewayHttpUrl(gatewayUrl, urlPath) {
159
161
  return `${scheme}//${parsed.host}${urlPath}`;
160
162
  }
161
163
  /** 发起 HTTP GET 请求,返回文本内容 */
162
- async function _fetchText(url, verifySsl) {
164
+ async function _fetchText(url, verifySsl, net) {
163
165
  try {
166
+ if (net) {
167
+ return await net.httpGetText(url, 5_000);
168
+ }
164
169
  return await _httpGet(url, verifySsl);
165
170
  }
166
171
  catch (err) {
@@ -168,8 +173,8 @@ async function _fetchText(url, verifySsl) {
168
173
  }
169
174
  }
170
175
  /** 发起 HTTP GET 请求,返回 JSON 对象 */
171
- async function _fetchJson(url, verifySsl) {
172
- const text = await _fetchText(url, verifySsl);
176
+ async function _fetchJson(url, verifySsl, net) {
177
+ const text = await _fetchText(url, verifySsl, net);
173
178
  try {
174
179
  const payload = JSON.parse(text);
175
180
  if (!isJsonObject(payload)) {
@@ -237,6 +242,7 @@ export class AuthFlow {
237
242
  _deviceId;
238
243
  _slotId;
239
244
  _verifySsl;
245
+ _net;
240
246
  _connectionFactory;
241
247
  _rootCaPath;
242
248
  _chainCacheTtl;
@@ -261,6 +267,7 @@ export class AuthFlow {
261
267
  this._slotId = String(opts.slotId ?? '').trim();
262
268
  this._connectionFactory = opts.connectionFactory ?? _defaultConnectionFactory;
263
269
  this._rootCaPath = opts.rootCaPath ?? null;
270
+ this._net = opts.net ?? null;
264
271
  this._verifySsl = opts.verifySsl ?? false;
265
272
  this._chainCacheTtl = (opts.chainCacheTtl ?? 86400) * 1000; // 转为毫秒
266
273
  this._rootCerts = this._loadRootCerts(this._rootCaPath);
@@ -305,51 +312,240 @@ export class AuthFlow {
305
312
  this._slotId = String(opts.slotId ?? '').trim();
306
313
  }
307
314
  /**
308
- * 创建 AID 并注册到 Gateway。
309
- * 如果 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(防止"已注册但本地没保存")
310
326
  */
311
- async createAid(gatewayUrl, aid) {
327
+ async registerAid(gatewayUrl, aid) {
312
328
  const tStart = Date.now();
313
329
  AuthFlow._validateAidName(aid);
314
- this._logger.debug(`createAid enter: aid=${aid}, gateway=${gatewayUrl}`);
330
+ this._logger.debug(`registerAid enter: aid=${aid}, gateway=${gatewayUrl}`);
315
331
  try {
316
- let identity = this._ensureLocalIdentity(aid);
317
- if (identity.cert) {
318
- this._logger.debug(`createAid exit: elapsed=${Date.now() - tStart}ms (already has cert) aid=${aid}`);
319
- 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;
320
406
  }
321
- // 本地有密钥但无证书——尝试注册
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
322
417
  try {
323
- const created = await this._createAid(gatewayUrl, identity);
324
- Object.assign(identity, created);
325
- this._logger.debug(`AID register ok: aid=${aid}`);
418
+ this._keystore.promotePendingIdentity(pendingDir, aid);
326
419
  }
327
420
  catch (e) {
328
- if (!(e instanceof AUNError) || !String(e.message).includes('already exists')) {
329
- this._logger.error(`AID register failed: aid=${aid}, error=${e instanceof Error ? e.message : String(e)}`);
330
- throw e;
331
- }
332
- // AID 已在服务端注册,尝试从 PKI 下载恢复证书
333
- try {
334
- identity = await this._recoverCertViaDownload(gatewayUrl, identity);
335
- }
336
- catch {
337
- throw new StateError(`AID ${aid} already registered on server but local certificate is missing. ` +
338
- `Certificate download recovery failed. Options: ` +
339
- `(1) use a different AID name, or ` +
340
- `(2) restart Kite server to clear registration.`);
341
- }
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.`);
342
423
  }
424
+ // Step 9: 标准持久化
343
425
  this._persistIdentity(identity);
344
- this._aid = String(identity.aid);
345
- 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}`);
346
428
  return { aid: identity.aid, cert: identity.cert };
347
429
  }
348
430
  catch (err) {
349
- 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)}`);
350
432
  throw err;
351
433
  }
352
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
+ }
353
549
  /**
354
550
  * 认证(登录)到 Gateway。
355
551
  * 执行两阶段挑战-应答认证,返回 token 信息。
@@ -368,15 +564,24 @@ export class AuthFlow {
368
564
  const cachedToken = AuthFlow._getCachedAccessToken(identityWithState);
369
565
  const cachedRefresh = String(identityWithState.refresh_token ?? '');
370
566
  if (cachedToken && cachedRefresh) {
371
- this._logger.debug(`authenticate reusing cached token: aid=${identity.aid} expires_at=${identityWithState.access_token_expires_at}`);
372
- this._aid = String(identity.aid);
373
- return {
374
- aid: identity.aid,
375
- access_token: cachedToken,
376
- refresh_token: cachedRefresh,
377
- expires_at: identityWithState.access_token_expires_at,
378
- gateway: gatewayUrl,
379
- };
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
+ }
380
585
  }
381
586
  if (!identity.cert) {
382
587
  // 本地有密钥但无证书——尝试从 PKI 下载恢复
@@ -386,26 +591,20 @@ export class AuthFlow {
386
591
  }
387
592
  catch (e) {
388
593
  throw new StateError(`local certificate missing and recovery failed: ${e instanceof Error ? e.message : String(e)}. ` +
389
- `Run auth.createAid() to register a new identity.`);
594
+ `Run auth.registerAid() to register a new identity.`);
390
595
  }
391
596
  }
597
+ // 防线 B:发起两步登录前显式校验 cert 公钥与本地 keypair 公钥一致。
598
+ this._assertCertMatchesLocalKeypair(identity);
392
599
  let login;
393
600
  try {
394
601
  login = await this._login(gatewayUrl, identity);
395
602
  this._logger.debug(`auth login ok: aid=${identity.aid}`);
396
603
  }
397
604
  catch (e) {
398
- // 证书未在服务端注册或公钥不匹配 — 自动重新注册
399
- if (e instanceof AuthError && (String(e.message).includes('not registered') || String(e.message).includes('public key mismatch'))) {
400
- this._logger.warn(`cert not registered on server, auto re-register: aid=${identity.aid}`);
401
- const created = await this._createAid(gatewayUrl, identity);
402
- identity.cert = created.cert;
403
- this._persistIdentity(identity);
404
- login = await this._login(gatewayUrl, identity);
405
- }
406
- else {
407
- throw e;
408
- }
605
+ // 注册和登录彻底分离:登录失败绝不触发自动注册。
606
+ // 服务端报"not registered"时,应用层应当显式调 registerAid。
607
+ throw e;
409
608
  }
410
609
  AuthFlow._rememberTokens(identity, login);
411
610
  await this._validateNewCert(identity, gatewayUrl);
@@ -433,12 +632,14 @@ export class AuthFlow {
433
632
  const tStart = Date.now();
434
633
  this._logger.debug(`ensureAuthenticated enter: gateway=${gatewayUrl}`);
435
634
  try {
436
- const identity = this._ensureIdentity();
635
+ // 注册和登录彻底分离:无身份直接抛错,绝不再隐式生成密钥。
636
+ const identity = this._loadIdentityOrRaise();
437
637
  if (!identity.cert) {
438
- const created = await this._createAid(gatewayUrl, identity);
439
- Object.assign(identity, created);
440
- 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.`);
441
640
  }
641
+ // 防线 B:发起两步登录前显式校验
642
+ this._assertCertMatchesLocalKeypair(identity);
442
643
  const login = await this._login(gatewayUrl, identity);
443
644
  AuthFlow._rememberTokens(identity, login);
444
645
  await this._validateNewCert(identity, gatewayUrl);
@@ -681,6 +882,25 @@ export class AuthFlow {
681
882
  throw err;
682
883
  }
683
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
+ }
684
904
  // ── 内部方法:短连接 RPC ────────────────────────────────────
685
905
  /**
686
906
  * 通过临时 WebSocket 发送单次 JSON-RPC 请求。
@@ -840,7 +1060,11 @@ export class AuthFlow {
840
1060
  auth: { method: 'kite_token', token },
841
1061
  protocol: { min: '1.0', max: '1.0' },
842
1062
  device: { id: String(opts?.deviceId ?? ''), type: 'sdk' },
843
- 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
+ },
844
1068
  delivery_mode: opts?.deliveryMode ?? { mode: 'fanout' },
845
1069
  capabilities,
846
1070
  };
@@ -1016,7 +1240,7 @@ export class AuthFlow {
1016
1240
  /** 从 Gateway PKI 端点获取 CA 链 */
1017
1241
  async _fetchGatewayCaChain(gatewayUrl, _chainAid = '') {
1018
1242
  const url = _gatewayHttpUrl(gatewayUrl, '/pki/chain');
1019
- const text = await _fetchText(url, this._verifySsl);
1243
+ const text = await _fetchText(url, this._verifySsl, this._net);
1020
1244
  return _splitPemBundle(text);
1021
1245
  }
1022
1246
  /**
@@ -1069,7 +1293,7 @@ export class AuthFlow {
1069
1293
  */
1070
1294
  async _fetchGatewayCrl(gatewayUrl, _issuerCert) {
1071
1295
  const url = _gatewayHttpUrl(gatewayUrl, '/pki/crl.json');
1072
- const payload = await _fetchJson(url, this._verifySsl);
1296
+ const payload = await _fetchJson(url, this._verifySsl, this._net);
1073
1297
  const crlPem = String(payload.crl_pem || '');
1074
1298
  if (!crlPem) {
1075
1299
  throw new AuthError('gateway CRL endpoint returned no signed CRL');
@@ -1246,7 +1470,7 @@ export class AuthFlow {
1246
1470
  async _fetchGatewayOcspStatus(gatewayUrl, authCert, issuerCert) {
1247
1471
  const serialHex = _certSerialHex(authCert);
1248
1472
  const url = _gatewayHttpUrl(gatewayUrl, `/pki/ocsp/${serialHex}`);
1249
- const payload = await _fetchJson(url, this._verifySsl);
1473
+ const payload = await _fetchJson(url, this._verifySsl, this._net);
1250
1474
  const status = String(payload.status || '');
1251
1475
  const ocspB64 = String(payload.ocsp_response || '');
1252
1476
  if (!ocspB64) {
@@ -1475,9 +1699,8 @@ export class AuthFlow {
1475
1699
  * 本地有密钥但无证书、服务端已注册时使用。
1476
1700
  */
1477
1701
  async _recoverCertViaDownload(gatewayUrl, identity) {
1478
- const certUrl = _gatewayHttpUrl(gatewayUrl, `/pki/cert/${identity.aid}`);
1479
- const certPem = await _fetchText(certUrl, this._verifySsl);
1480
- if (!certPem || !certPem.includes('BEGIN CERTIFICATE')) {
1702
+ const certPem = await this._downloadRegisteredCert(gatewayUrl, String(identity.aid));
1703
+ if (!certPem) {
1481
1704
  throw new AuthError(`failed to download certificate for ${identity.aid}`);
1482
1705
  }
1483
1706
  // 验证下载的证书公钥与本地密钥对匹配
@@ -1492,6 +1715,93 @@ export class AuthFlow {
1492
1715
  identity.cert = certPem;
1493
1716
  return identity;
1494
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
+ }
1495
1805
  // ── 内部方法:new_cert 验证 ────────────────────────────────
1496
1806
  /**
1497
1807
  * 验证服务端返回的 new_cert,通过后才正式接受。
@@ -1638,30 +1948,9 @@ export class AuthFlow {
1638
1948
  throw new ValidationError("AID name must not start with 'guest'");
1639
1949
  }
1640
1950
  }
1641
- /** 确保本地有指定 AID 的身份(不存在则创建密钥对) */
1642
- _ensureLocalIdentity(aid) {
1643
- const existing = this._keystore.loadIdentity(aid);
1644
- // 必须确认有 keypair(private_key_pem + public_key_der_b64)才算"已存在"
1645
- // 否则 keystore 可能只有 metadata(如 gateway_url)但没有真正的密钥材料
1646
- if (existing && existing.private_key_pem && existing.public_key_der_b64) {
1647
- this._aid = aid;
1648
- return existing;
1649
- }
1650
- const identity = this._crypto.generateIdentity();
1651
- identity.aid = aid;
1652
- // 保留 keystore 已有的 metadata(如 gateway_url),避免覆盖
1653
- if (existing) {
1654
- for (const [k, v] of Object.entries(existing)) {
1655
- if (k !== 'aid' && !(k in identity)) {
1656
- identity[k] = v;
1657
- }
1658
- }
1659
- }
1660
- this._persistIdentity(identity); // 立即持久化密钥对
1661
- this._aid = aid;
1662
- return identity;
1663
- }
1664
- /** 加载身份信息,不存在时抛出 StateError */
1951
+ // (_ensureLocalIdentity 已硬移除:注册和登录彻底分离,登录路径绝不再
1952
+ // 隐式生成密钥;新身份必须由应用层显式调 registerAid)
1953
+ /** 加载身份信息,不存在或半成品时抛出 StateError */
1665
1954
  _loadIdentityOrRaise(aid) {
1666
1955
  const requestedAid = aid ?? this._aid;
1667
1956
  if (requestedAid) {
@@ -1669,6 +1958,13 @@ export class AuthFlow {
1669
1958
  if (existing === null) {
1670
1959
  throw new StateError(`identity not found for aid: ${requestedAid}`);
1671
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
+ }
1672
1968
  this._aid = requestedAid;
1673
1969
  if (!existing.aid)
1674
1970
  existing.aid = requestedAid;
@@ -1679,6 +1975,9 @@ export class AuthFlow {
1679
1975
  if (typeof ks.loadAnyIdentity === 'function') {
1680
1976
  const existing = ks.loadAnyIdentity();
1681
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
+ }
1682
1981
  const loadedAid = existing.aid;
1683
1982
  if (typeof loadedAid === 'string' && loadedAid) {
1684
1983
  this._aid = loadedAid;
@@ -1686,29 +1985,11 @@ export class AuthFlow {
1686
1985
  return existing;
1687
1986
  }
1688
1987
  }
1689
- throw new StateError('no local identity found, call auth.createAid() first');
1690
- }
1691
- /** 确保有身份(不存在时自动创建密钥对) */
1692
- _ensureIdentity() {
1693
- try {
1694
- return this._loadIdentityOrRaise();
1695
- }
1696
- catch (e) {
1697
- if (!(e instanceof StateError))
1698
- throw e;
1699
- if (!this._aid) {
1700
- throw new StateError('no local identity found, call auth.createAid() first');
1701
- }
1702
- const identity = this._crypto.generateIdentity();
1703
- identity.aid = this._aid;
1704
- this._persistIdentity(identity); // 立即持久化
1705
- return identity;
1706
- }
1988
+ throw new StateError('no local identity found, call auth.registerAid() first');
1707
1989
  }
1990
+ // (_ensureIdentity 已硬移除:注册和登录彻底分离,登录路径绝不再隐式
1991
+ // 生成密钥;调用方应改用 _loadIdentityOrRaise 获取已注册身份)
1708
1992
  _loadInstanceState(aid) {
1709
- if (!this._deviceId) {
1710
- return null;
1711
- }
1712
1993
  const loader = this._keystore.loadInstanceState;
1713
1994
  if (typeof loader !== 'function') {
1714
1995
  return null;
@@ -1729,17 +2010,15 @@ export class AuthFlow {
1729
2010
  }
1730
2011
  }
1731
2012
  this._keystore.saveIdentity(aid, persisted);
1732
- if (this._deviceId) {
1733
- // 从共享 metadata_kv 中移除实例级字段(它们已保存到 instance_state)
1734
- const db = this._keystore._getDB?.(aid);
1735
- if (db && typeof db.deleteMetadata === 'function') {
1736
- for (const key of AuthFlow._INSTANCE_STATE_FIELDS) {
1737
- db.deleteMetadata(key);
1738
- db.deleteMetadata(`${key}_protection`);
1739
- }
2013
+ // 从共享 metadata_kv 中移除实例级字段(它们已保存到 instance_state)
2014
+ const db = this._keystore._getDB?.(aid);
2015
+ if (db && typeof db.deleteMetadata === 'function') {
2016
+ for (const key of AuthFlow._INSTANCE_STATE_FIELDS) {
2017
+ db.deleteMetadata(key);
2018
+ db.deleteMetadata(`${key}_protection`);
1740
2019
  }
1741
2020
  }
1742
- if (!this._deviceId || Object.keys(instanceState).length === 0) {
2021
+ if (Object.keys(instanceState).length === 0) {
1743
2022
  return;
1744
2023
  }
1745
2024
  const updater = this._keystore.updateInstanceState;