@agentunion/fastaun 0.4.12 → 0.4.13

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 CHANGED
@@ -6,6 +6,33 @@
6
6
 
7
7
  ---
8
8
 
9
+ ## 0.4.13 — 2026-06-09
10
+
11
+ ### 新功能
12
+ - **发送结果回填 envelope**:`message.send` / `group.send` / `message.thought.put` / `group.thought.put` 的返回结果新增规范化 `envelope` 字段,使发送方回执与接收端信封元数据一致;新增 `APP_SEND_ENVELOPE_METHODS` 与 `sendResultEnvelope()` / `attachSendResultEnvelope()`,rpc-pipeline 在 `postprocess` 后统一附加,V2 加密路径经 `_skipSendResultEnvelope` 标志跳过明文附加、由外层 V2 协调器在加密后补挂避免重复(四语言对齐)
13
+
14
+ ### 修复
15
+ - **refresh_token 失效自愈**:新增需重登判定(命中 `relogin_required` 或 `missing refresh_token` / `invalid_or_expired_refresh_token` / `refresh not supported`)与清缓存逻辑;刷新失败需重登时清空本地 `access_token`/`refresh_token`/`kite_token` 并持久化,client 层 token 刷新循环检测到需重登时发布 `token.refresh_exhausted` 事件(带 `relogin_required: true`)、断连触发重连,下次 `connectSession` 因无可用 token 自动走两步登录;refresh RPC 补传 `aid`/`device_id`/`slot_id`/`access_token`/`sdk_lang`/`sdk_version`,`AuthError` 携带 `data` 透传服务端响应(四语言对齐)
16
+ - **重连状态位修复**:断连分支补齐 `_reconnectActive = false`,token 循环中 `publish`/`_handleTransportDisconnect` 改为 `await`,避免重连标志残留导致后续重连被跳过
17
+ - **protected_headers 类型归一**:V2 发送方法的 envelope 元数据归一时识别 `ProtectedHeaders` 对象(经 `toObject()` 探测)并转 dict,修复传入对象(非 dict)时 protected_headers 被丢弃的问题(四语言对齐)
18
+
19
+ ### 优化
20
+ - **app_message_envelope 字段收窄**:envelope 键收窄为可转发元数据(`from`/`to`/`group_id`/`type`/`kind`/`version`/`timestamp`/`encrypted`/`context`/`protected_headers`/`payload_type`),移除 `message_id`/`seq`/`device_id`/`slot_id` 等本地与传输层字段,不再向应用层泄漏内部字段并剔除 `_auth`(四语言对齐)
21
+ - **RPC 日志 token 脱敏**:短 RPC 请求/响应 debug 日志对 `access_token`/`refresh_token`/`kite_token`/`token` 及 `_token` 结尾键递归脱敏(`<redacted len=.. sha256=..>`),避免明文 token 落日志(四语言对齐)
22
+
23
+ ### 测试
24
+ - `refresh 业务失败且要求重登时应清掉本地 token 缓存`
25
+ - `connectSession 在 refresh 失败后应继续走两步登录`
26
+ - `应用层消息 envelope 只保留可转发字段并归一化 headers`
27
+ - 更新 `publishAppEvent` 与群撤回相关用例断言 envelope 收窄、不含 `message_id`/`seq`/`device_id`/`slot_id`
28
+
29
+ ### 服务端协同(本版本配套)
30
+ - **`auth.refresh_token` 响应结构化**:新增 `relogin_required` / `retryable` / `diagnostic` / `aid` / `refresh_count` 字段,SDK 据 `relogin_required`/`retryable` 区分"重新登录"与"退避重试",不再单纯按 `error` 字符串判断
31
+ - **JWT 老 SDK 兼容与即时吊销**:`AUN_JWT_LEGACY_ACCESS_TOKEN_COMPAT`(默认开)放行合法 JWT 避免迁移期重连风暴;`auth.token.revoked` 事件携带 `jti`/`expires_at`,gateway 按真实 TTL 缓存吊销
32
+ - **V2 P2P 写入幂等**:服务端 `v2_write_peer_message`/`v2_write_peer_wrap` 捕获唯一键冲突逐字段比对,重发相同 message_id 幂等返回、不再产生 seq 空洞;自发自收跳过重复 self-sync 推送
33
+
34
+ ---
35
+
9
36
  ## 0.4.12 — 2026-06-08
10
37
 
11
38
  ### 新功能
@@ -6,6 +6,33 @@
6
6
 
7
7
  ---
8
8
 
9
+ ## 0.4.13 — 2026-06-09
10
+
11
+ ### 新功能
12
+ - **发送结果回填 envelope**:`message.send` / `group.send` / `message.thought.put` / `group.thought.put` 的返回结果新增规范化 `envelope` 字段,使发送方回执与接收端信封元数据一致;新增 `APP_SEND_ENVELOPE_METHODS` 与 `sendResultEnvelope()` / `attachSendResultEnvelope()`,rpc-pipeline 在 `postprocess` 后统一附加,V2 加密路径经 `_skipSendResultEnvelope` 标志跳过明文附加、由外层 V2 协调器在加密后补挂避免重复(四语言对齐)
13
+
14
+ ### 修复
15
+ - **refresh_token 失效自愈**:新增需重登判定(命中 `relogin_required` 或 `missing refresh_token` / `invalid_or_expired_refresh_token` / `refresh not supported`)与清缓存逻辑;刷新失败需重登时清空本地 `access_token`/`refresh_token`/`kite_token` 并持久化,client 层 token 刷新循环检测到需重登时发布 `token.refresh_exhausted` 事件(带 `relogin_required: true`)、断连触发重连,下次 `connectSession` 因无可用 token 自动走两步登录;refresh RPC 补传 `aid`/`device_id`/`slot_id`/`access_token`/`sdk_lang`/`sdk_version`,`AuthError` 携带 `data` 透传服务端响应(四语言对齐)
16
+ - **重连状态位修复**:断连分支补齐 `_reconnectActive = false`,token 循环中 `publish`/`_handleTransportDisconnect` 改为 `await`,避免重连标志残留导致后续重连被跳过
17
+ - **protected_headers 类型归一**:V2 发送方法的 envelope 元数据归一时识别 `ProtectedHeaders` 对象(经 `toObject()` 探测)并转 dict,修复传入对象(非 dict)时 protected_headers 被丢弃的问题(四语言对齐)
18
+
19
+ ### 优化
20
+ - **app_message_envelope 字段收窄**:envelope 键收窄为可转发元数据(`from`/`to`/`group_id`/`type`/`kind`/`version`/`timestamp`/`encrypted`/`context`/`protected_headers`/`payload_type`),移除 `message_id`/`seq`/`device_id`/`slot_id` 等本地与传输层字段,不再向应用层泄漏内部字段并剔除 `_auth`(四语言对齐)
21
+ - **RPC 日志 token 脱敏**:短 RPC 请求/响应 debug 日志对 `access_token`/`refresh_token`/`kite_token`/`token` 及 `_token` 结尾键递归脱敏(`<redacted len=.. sha256=..>`),避免明文 token 落日志(四语言对齐)
22
+
23
+ ### 测试
24
+ - `refresh 业务失败且要求重登时应清掉本地 token 缓存`
25
+ - `connectSession 在 refresh 失败后应继续走两步登录`
26
+ - `应用层消息 envelope 只保留可转发字段并归一化 headers`
27
+ - 更新 `publishAppEvent` 与群撤回相关用例断言 envelope 收窄、不含 `message_id`/`seq`/`device_id`/`slot_id`
28
+
29
+ ### 服务端协同(本版本配套)
30
+ - **`auth.refresh_token` 响应结构化**:新增 `relogin_required` / `retryable` / `diagnostic` / `aid` / `refresh_count` 字段,SDK 据 `relogin_required`/`retryable` 区分"重新登录"与"退避重试",不再单纯按 `error` 字符串判断
31
+ - **JWT 老 SDK 兼容与即时吊销**:`AUN_JWT_LEGACY_ACCESS_TOKEN_COMPAT`(默认开)放行合法 JWT 避免迁移期重连风暴;`auth.token.revoked` 事件携带 `jti`/`expires_at`,gateway 按真实 TTL 缓存吊销
32
+ - **V2 P2P 写入幂等**:服务端 `v2_write_peer_message`/`v2_write_peer_wrap` 捕获唯一键冲突逐字段比对,重发相同 message_id 幂等返回、不再产生 seq 空洞;自发自收跳过重复 self-sync 推送
33
+
34
+ ---
35
+
9
36
  ## 0.4.12 — 2026-06-08
10
37
 
11
38
  ### 新功能
@@ -2082,11 +2082,9 @@ SDK 收到后自动调用 `group.pull` 拉取最新消息并逐条解密后交
2082
2082
  {
2083
2083
  "envelope": {
2084
2084
  "group_id": "g-abc123.agentid.pub",
2085
- "seq": 42,
2086
- "message_id": "uuid",
2087
- "sender_aid": "alice.agentid.pub",
2088
- "message_type": "group.message",
2089
- "dispatch_mode": "broadcast"
2085
+ "from": "alice.agentid.pub",
2086
+ "type": "text",
2087
+ "timestamp": 1234567890000
2090
2088
  },
2091
2089
  "group_id": "g-abc123.agentid.pub",
2092
2090
  "seq": 42,
@@ -2098,7 +2096,7 @@ SDK 收到后自动调用 `group.pull` 拉取最新消息并逐条解密后交
2098
2096
  }
2099
2097
  ```
2100
2098
 
2101
- SDK 交付给应用层的 `payload` 是明文业务 JSON 对象;群消息信封字段统一放在 `envelope`。0.4.x 兼容期仍保留顶层 `group_id` / `seq` / `message_id` / `sender_aid` 等别名,下一个大版本 0.5.* 将移除这些顶层别名,请通过 `msg["envelope"]["message_id"]` 等路径访问。
2099
+ SDK 交付给应用层的 `payload` 是明文业务 JSON 对象;群消息信封字段统一放在 `envelope`。`envelope` 只保留可转发的归一化元数据,`from` 由 `sender_aid` 归一化而来,`timestamp` 由 `created_at` / `t_server` 归一化而来。0.4.x 兼容期仍保留顶层 `group_id` / `seq` / `message_id` / `sender_aid` / `dispatch_mode` 等别名,下一个大版本 0.5.* 将移除这些顶层别名;请通过 `msg["envelope"]["from"]`、`msg["envelope"]["timestamp"]` 等路径访问。
2102
2100
 
2103
2101
  ### event/group.message_recalled
2104
2102
 
@@ -2109,11 +2107,11 @@ SDK 交付给应用层的 `payload` 是明文业务 JSON 对象;群消息信
2109
2107
  ```json
2110
2108
  {
2111
2109
  "envelope": {
2112
- "module_id": "group",
2113
2110
  "group_id": "g-abc123.agentid.pub",
2114
- "seq": 43,
2115
- "message_id": "grm-uuid",
2116
- "sender_aid": "alice.agentid.pub"
2111
+ "from": "alice.agentid.pub",
2112
+ "type": "group.message_recalled",
2113
+ "kind": "group.message_recalled",
2114
+ "timestamp": 1234567890000
2117
2115
  },
2118
2116
  "module_id": "group",
2119
2117
  "group_id": "g-abc123.agentid.pub",
@@ -2130,11 +2128,11 @@ SDK 交付给应用层的 `payload` 是明文业务 JSON 对象;群消息信
2130
2128
  }
2131
2129
  ```
2132
2130
 
2133
- SDK 交付给应用层的撤回事件同样带 `envelope`。`envelope` 表示当前交付的撤回 tombstone / 通知自身信封,不是被撤回原消息的信封;业务侧被撤回的原消息列表继续使用 `message_ids` / `target_message_seqs`。0.4.x 兼容期仍保留顶层 `group_id` / `seq` / `message_id` / `sender_aid` 等别名,下一个大版本 0.5.* 将移除这些顶层别名。
2131
+ SDK 交付给应用层的撤回事件同样带 `envelope`。`envelope` 表示当前交付的撤回 tombstone / 通知自身信封,不是被撤回原消息的信封;业务侧被撤回的原消息列表继续使用 `message_ids` / `target_message_seqs`。`message_id` / `seq` 继续只保留在顶层兼容字段中,不进入 `envelope`。0.4.x 兼容期仍保留顶层 `group_id` / `seq` / `message_id` / `sender_aid` 等别名,下一个大版本 0.5.* 将移除这些顶层别名。
2134
2132
 
2135
2133
  | 字段 | 类型 | 说明 |
2136
2134
  |------|------|------|
2137
- | `envelope` | object | 撤回 tombstone / 通知自身信封,包含 `module_id`、`group_id`、`seq`、`message_id`、`sender_aid`、`device_id`、`slot_id` 等存在的字段 |
2135
+ | `envelope` | object | 撤回 tombstone / 通知自身信封,包含 `group_id`、`from`、`type`、`kind`、`timestamp`、`encrypted`、`context`、`protected_headers` 等存在的字段 |
2138
2136
  | `module_id` | string | 固定 `"group"` |
2139
2137
  | `group_id` | string | 群组 ID |
2140
2138
  | `seq` | integer | 当前交付的撤回 tombstone / 通知 seq;在线 push 为 notice_seq,原 seq 占位 tombstone 为原消息 seq |
@@ -513,10 +513,9 @@ result = await client.call("message.ack", {"seq": 150})
513
513
  ```json
514
514
  {
515
515
  "envelope": {
516
- "message_id": "uuid-1",
517
516
  "from": "alice.agentid.pub",
518
517
  "to": "bob.agentid.pub",
519
- "seq": 42,
518
+ "type": "text",
520
519
  "timestamp": 1234567890000,
521
520
  "encrypted": false
522
521
  },
@@ -531,7 +530,7 @@ result = await client.call("message.ack", {"seq": 150})
531
530
  }
532
531
  ```
533
532
 
534
- SDK 交付给应用层的 `payload` 是明文业务 JSON 对象;信封字段统一放在 `envelope`。0.4.x 兼容期仍保留顶层 `message_id` / `from` / `to` / `seq` / `timestamp` 等别名,下一个大版本 0.5.* 将移除这些顶层别名,请通过 `msg["envelope"]["seq"]` 等路径访问。
533
+ SDK 交付给应用层的 `payload` 是明文业务 JSON 对象;信封字段统一放在 `envelope`。`envelope` 只保留可转发的归一化元数据,`from` 由 `from_aid` / `sender_aid` 归一化而来,`timestamp` 由 `created_at` / `t_server` 归一化而来。0.4.x 兼容期仍保留顶层 `message_id` / `from` / `to` / `seq` / `timestamp` 等别名,下一个大版本 0.5.* 将移除这些顶层别名;请通过 `msg["envelope"]["from"]`、`msg["envelope"]["timestamp"]` 等路径访问。
535
534
 
536
535
  ### 订阅
537
536
 
@@ -550,10 +549,10 @@ client.on("message.received", lambda msg: print(msg["payload"]))
550
549
  ```json
551
550
  {
552
551
  "envelope": {
553
- "message_id": "recall-uuid",
554
552
  "from": "alice.agentid.pub",
555
553
  "to": "bob.agentid.pub",
556
- "seq": 43,
554
+ "type": "message.recalled",
555
+ "kind": "message.recalled",
557
556
  "timestamp": 1234567890000
558
557
  },
559
558
  "message_id": "recall-uuid",
@@ -565,11 +564,11 @@ client.on("message.received", lambda msg: print(msg["payload"]))
565
564
  }
566
565
  ```
567
566
 
568
- SDK 交付给应用层的撤回事件同样带 `envelope`。`envelope` 表示撤回 tombstone / 通知自身的信封,不是被撤回原消息的信封;被撤回的原消息继续通过 `message_ids` 表达。0.4.x 兼容期仍保留顶层 `message_id` / `from` / `to` / `seq` / `timestamp` 等别名,下一个大版本 0.5.* 将移除这些顶层别名。
567
+ SDK 交付给应用层的撤回事件同样带 `envelope`。`envelope` 表示撤回 tombstone / 通知自身的信封,不是被撤回原消息的信封;被撤回的原消息继续通过 `message_ids` 表达。`message_id` / `seq` 继续只保留在顶层兼容字段中,不进入 `envelope`。0.4.x 兼容期仍保留顶层 `message_id` / `from` / `to` / `seq` / `timestamp` 等别名,下一个大版本 0.5.* 将移除这些顶层别名。
569
568
 
570
569
  | 字段 | 类型 | 说明 |
571
570
  |------|------|------|
572
- | `envelope` | object | 撤回 tombstone / 通知自身信封,包含 `message_id`、`from`、`to`、`seq`、`timestamp`、`device_id`、`slot_id` 等存在的字段 |
571
+ | `envelope` | object | 撤回 tombstone / 通知自身信封,包含 `from`、`to`、`type`、`kind`、`timestamp`、`encrypted`、`context`、`protected_headers` 等存在的字段 |
573
572
  | `message_id` | string | 撤回 tombstone / 通知自身的 message_id |
574
573
  | `tombstone_message_id` | string | 兼容别名,等同于撤回 tombstone / 通知自身的 `message_id` |
575
574
  | `from` | string | 发送方(撤回者)AID |
package/dist/auth.d.ts CHANGED
@@ -302,6 +302,8 @@ export declare class AuthFlow {
302
302
  * 与 Python SDK 的 _remember_tokens 对齐。
303
303
  */
304
304
  private static _rememberTokens;
305
+ private _refreshFailureRequiresRelogin;
306
+ private _clearCachedTokens;
305
307
  /**
306
308
  * 获取缓存的 access_token(30 秒提前过期余量)。
307
309
  * 返回空字符串表示 token 不可用。
package/dist/auth.js CHANGED
@@ -27,6 +27,35 @@ const _noopLogger = {
27
27
  debug: () => { },
28
28
  };
29
29
  const AUN_SDK_LANG = 'typescript';
30
+ const SENSITIVE_RPC_LOG_KEYS = new Set([
31
+ 'access_token',
32
+ 'refresh_token',
33
+ 'kite_token',
34
+ 'token',
35
+ ]);
36
+ function _redactRpcLogPayload(value, key = '', depth = 0) {
37
+ const keyLower = key.toLowerCase();
38
+ if (SENSITIVE_RPC_LOG_KEYS.has(keyLower) || keyLower.endsWith('_token')) {
39
+ const text = String(value ?? '');
40
+ if (!text)
41
+ return '';
42
+ const digest = crypto.createHash('sha256').update(text).digest('hex').slice(0, 12);
43
+ return `<redacted len=${text.length} sha256=${digest}>`;
44
+ }
45
+ if (depth >= 6)
46
+ return '<max-depth>';
47
+ if (Array.isArray(value)) {
48
+ return value.map((item) => _redactRpcLogPayload(item, '', depth + 1));
49
+ }
50
+ if (value && typeof value === 'object') {
51
+ const out = {};
52
+ for (const [childKey, childValue] of Object.entries(value)) {
53
+ out[childKey] = _redactRpcLogPayload(childValue, childKey, depth + 1);
54
+ }
55
+ return out;
56
+ }
57
+ return value;
58
+ }
30
59
  // ── 签名验证辅助 ──────────────────────────────────────────────
31
60
  /**
32
61
  * 验证签名:支持 ECDSA P-256 (SHA256)、ECDSA P-384 (SHA384)、Ed25519。
@@ -447,11 +476,12 @@ export class AuthFlow {
447
476
  const refreshToken = String(identity.refresh_token || '');
448
477
  if (!refreshToken) {
449
478
  this._logger.error(`token refresh failed: missing refresh_token, aid=${identity.aid}`);
479
+ this._clearCachedTokens(identity, 'missing refresh_token');
450
480
  throw new AuthError('missing refresh_token');
451
481
  }
452
482
  this._logger.debug(`refreshCachedTokens enter: aid=${identity.aid}`);
453
483
  try {
454
- const refreshed = await this._refreshAccessToken(gatewayUrl, refreshToken);
484
+ const refreshed = await this._refreshAccessToken(gatewayUrl, refreshToken, identity);
455
485
  AuthFlow._rememberTokens(identity, refreshed);
456
486
  await this._validateNewCert(identity, gatewayUrl);
457
487
  this._persistIdentity(identity);
@@ -459,6 +489,9 @@ export class AuthFlow {
459
489
  return identity;
460
490
  }
461
491
  catch (err) {
492
+ if (this._refreshFailureRequiresRelogin(err)) {
493
+ this._clearCachedTokens(identity, err instanceof Error ? err.message : String(err));
494
+ }
462
495
  this._logger.debug(`refreshCachedTokens exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
463
496
  throw err;
464
497
  }
@@ -596,6 +629,9 @@ export class AuthFlow {
596
629
  catch (exc) {
597
630
  if (!(exc instanceof AuthError))
598
631
  throw exc;
632
+ if (this._refreshFailureRequiresRelogin(exc)) {
633
+ this._clearCachedTokens(identity, exc.message);
634
+ }
599
635
  this._logger.debug(`refresh_token auth failed, will re-login: ${exc.message}`);
600
636
  }
601
637
  }
@@ -701,12 +737,12 @@ export class AuthFlow {
701
737
  method,
702
738
  params,
703
739
  });
704
- this._logger.debug(`short RPC request full: ${payload}`);
740
+ this._logger.debug(`short RPC request full: ${JSON.stringify(_redactRpcLogPayload(JSON.parse(payload)))}`);
705
741
  ws.send(payload);
706
742
  // 接收响应
707
743
  const raw = await this._wsRecv(ws);
708
744
  const message = typeof raw === 'string' ? JSON.parse(raw) : raw;
709
- this._logger.debug(`short RPC response full: method=${method} ${JSON.stringify(message)}`);
745
+ this._logger.debug(`short RPC response full: method=${method} ${JSON.stringify(_redactRpcLogPayload(message))}`);
710
746
  if (message.error) {
711
747
  throw mapRemoteError(message.error);
712
748
  }
@@ -715,7 +751,7 @@ export class AuthFlow {
715
751
  throw new ValidationError(`invalid pre-auth response for ${method}`);
716
752
  }
717
753
  if (result.success === false) {
718
- throw new AuthError(String(result.error || `${method} failed`));
754
+ throw new AuthError(String(result.error || `${method} failed`), { data: result });
719
755
  }
720
756
  return result;
721
757
  }
@@ -793,15 +829,35 @@ export class AuthFlow {
793
829
  }
794
830
  }
795
831
  /** 刷新 access_token */
796
- async _refreshAccessToken(gatewayUrl, refreshToken) {
832
+ async _refreshAccessToken(gatewayUrl, refreshToken, identity) {
797
833
  const tStart = Date.now();
798
834
  this._logger.debug(`_refreshAccessToken enter`);
799
835
  try {
800
- const result = await this._shortRpc(gatewayUrl, 'auth.refresh_token', {
836
+ const params = {
801
837
  refresh_token: refreshToken,
802
- });
838
+ sdk_lang: AUN_SDK_LANG,
839
+ sdk_version: AUN_SDK_VERSION,
840
+ };
841
+ if (identity) {
842
+ const aid = String(identity.aid ?? '').trim();
843
+ const deviceId = String(identity.device_id ?? '').trim();
844
+ const slotId = String(identity.slot_id ?? '').trim();
845
+ const accessToken = String(identity.access_token ?? '').trim();
846
+ if (aid)
847
+ params.aid = aid;
848
+ if (deviceId)
849
+ params.device_id = deviceId;
850
+ if (slotId)
851
+ params.slot_id = slotId;
852
+ if (accessToken)
853
+ params.access_token = accessToken;
854
+ if (typeof identity.access_token_expires_at === 'number') {
855
+ params.access_token_expires_at = identity.access_token_expires_at;
856
+ }
857
+ }
858
+ const result = await this._shortRpc(gatewayUrl, 'auth.refresh_token', params);
803
859
  if (!result.success) {
804
- throw new AuthError(String(result.error || 'refresh failed'));
860
+ throw new AuthError(String(result.error || 'refresh failed'), { data: result });
805
861
  }
806
862
  this._logger.debug(`_refreshAccessToken exit: elapsed=${Date.now() - tStart}ms`);
807
863
  return result;
@@ -1714,6 +1770,41 @@ export class AuthFlow {
1714
1770
  identity._pending_active_cert = activeCert;
1715
1771
  }
1716
1772
  }
1773
+ _refreshFailureRequiresRelogin(err) {
1774
+ if (!(err instanceof AuthError))
1775
+ return false;
1776
+ const data = err.data;
1777
+ if (isJsonObject(data)) {
1778
+ if (data.relogin_required === true)
1779
+ return true;
1780
+ const error = String(data.error ?? '').trim().toLowerCase();
1781
+ return ['missing refresh_token', 'invalid_or_expired_refresh_token', 'refresh not supported'].includes(error);
1782
+ }
1783
+ const message = err.message.trim().toLowerCase();
1784
+ return ['missing refresh_token', 'invalid_or_expired_refresh_token', 'refresh not supported'].includes(message);
1785
+ }
1786
+ _clearCachedTokens(identity, reason = '') {
1787
+ const aid = String(identity.aid ?? '');
1788
+ const hadToken = Boolean(identity.access_token ||
1789
+ identity.refresh_token ||
1790
+ identity.kite_token ||
1791
+ identity.token ||
1792
+ identity.access_token_expires_at);
1793
+ if (!hadToken)
1794
+ return;
1795
+ identity.access_token = '';
1796
+ identity.refresh_token = '';
1797
+ identity.kite_token = '';
1798
+ identity.token = '';
1799
+ identity.access_token_expires_at = 0;
1800
+ try {
1801
+ this._persistIdentity(identity);
1802
+ this._logger.warn(`cleared cached tokens after refresh failure: aid=${aid} reason=${reason}`);
1803
+ }
1804
+ catch (persistErr) {
1805
+ this._logger.warn(`failed to persist token cleanup: aid=${aid} err=${persistErr instanceof Error ? persistErr.message : String(persistErr)}`);
1806
+ }
1807
+ }
1717
1808
  /**
1718
1809
  * 获取缓存的 access_token(30 秒提前过期余量)。
1719
1810
  * 返回空字符串表示 token 不可用。