@nihplod/sso-sdk 1.6.0 → 1.7.0

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.
@@ -326,7 +326,7 @@ declare class SsoClient {
326
326
  * SSO SDK 错误类型
327
327
  */
328
328
  /** SSO 错误码 */
329
- type SsoErrorCode = "invalid_config" | "state_mismatch" | "pkce_required" | "token_request_failed" | "session_expired" | "no_refresh_token" | "userinfo_failed" | "not_authenticated" | "authorization_code_expired" | "authorization_code_used" | "client_disabled" | "user_denied_authorization" | "account_disabled" | "sso_server_error" | "rate_limited" | "network_error" | "popup_blocked" | "popup_closed" | "id_token_invalid" | "id_token_unsupported_alg" | "id_token_hs256_unsupported" | "id_token_missing_secret" | "id_token_invalid_signature" | "id_token_issuer_mismatch" | "id_token_audience_mismatch" | "id_token_expired" | "id_token_missing_sub" | "id_token_at_hash_mismatch" | "id_token_nonce_mismatch" | "logout_token_invalid" | "logout_token_unsupported_alg" | "logout_token_invalid_signature" | "logout_token_issuer_mismatch" | "logout_token_audience_mismatch" | "logout_token_expired" | "logout_token_replay";
329
+ type SsoErrorCode = "invalid_config" | "state_mismatch" | "issuer_mismatch" | "pkce_required" | "token_request_failed" | "session_expired" | "no_refresh_token" | "userinfo_failed" | "not_authenticated" | "authorization_code_expired" | "authorization_code_used" | "client_disabled" | "user_denied_authorization" | "account_disabled" | "sso_server_error" | "rate_limited" | "network_error" | "popup_blocked" | "popup_closed" | "id_token_invalid" | "id_token_unsupported_alg" | "id_token_hs256_unsupported" | "id_token_missing_secret" | "id_token_invalid_signature" | "id_token_issuer_mismatch" | "id_token_audience_mismatch" | "id_token_expired" | "id_token_missing_sub" | "id_token_at_hash_mismatch" | "id_token_nonce_mismatch" | "logout_token_invalid" | "logout_token_unsupported_alg" | "logout_token_invalid_signature" | "logout_token_issuer_mismatch" | "logout_token_audience_mismatch" | "logout_token_expired" | "logout_token_replay";
330
330
  /**
331
331
  * SSO SDK 自定义错误
332
332
  */
@@ -456,6 +456,11 @@ interface RequireAuthProps {
456
456
  */
457
457
  renderLoginError?: (error: unknown, retry: () => void) => React.ReactNode;
458
458
  }
459
+ interface LoginErrorContentProps {
460
+ loginError: unknown;
461
+ retry: () => void;
462
+ renderLoginError?: RequireAuthProps["renderLoginError"];
463
+ }
459
464
  /**
460
465
  * 路由保护组件
461
466
  *
@@ -470,7 +475,7 @@ interface RequireAuthProps {
470
475
  * </RequireAuth>
471
476
  * ```
472
477
  */
473
- declare function RequireAuth({ children, fallback, autoLogin, usePopup, onError, renderLoginError, }: RequireAuthProps): string | number | bigint | true | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | React.FunctionComponentElement<React.FragmentProps>;
478
+ declare function RequireAuth({ children, fallback, autoLogin, usePopup, onError, renderLoginError, }: RequireAuthProps): string | number | bigint | true | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | React.FunctionComponentElement<React.FragmentProps> | React.FunctionComponentElement<LoginErrorContentProps>;
474
479
  /**
475
480
  * 高阶组件:包装页面组件,要求认证后才能访问
476
481
  *
@@ -326,7 +326,7 @@ declare class SsoClient {
326
326
  * SSO SDK 错误类型
327
327
  */
328
328
  /** SSO 错误码 */
329
- type SsoErrorCode = "invalid_config" | "state_mismatch" | "pkce_required" | "token_request_failed" | "session_expired" | "no_refresh_token" | "userinfo_failed" | "not_authenticated" | "authorization_code_expired" | "authorization_code_used" | "client_disabled" | "user_denied_authorization" | "account_disabled" | "sso_server_error" | "rate_limited" | "network_error" | "popup_blocked" | "popup_closed" | "id_token_invalid" | "id_token_unsupported_alg" | "id_token_hs256_unsupported" | "id_token_missing_secret" | "id_token_invalid_signature" | "id_token_issuer_mismatch" | "id_token_audience_mismatch" | "id_token_expired" | "id_token_missing_sub" | "id_token_at_hash_mismatch" | "id_token_nonce_mismatch" | "logout_token_invalid" | "logout_token_unsupported_alg" | "logout_token_invalid_signature" | "logout_token_issuer_mismatch" | "logout_token_audience_mismatch" | "logout_token_expired" | "logout_token_replay";
329
+ type SsoErrorCode = "invalid_config" | "state_mismatch" | "issuer_mismatch" | "pkce_required" | "token_request_failed" | "session_expired" | "no_refresh_token" | "userinfo_failed" | "not_authenticated" | "authorization_code_expired" | "authorization_code_used" | "client_disabled" | "user_denied_authorization" | "account_disabled" | "sso_server_error" | "rate_limited" | "network_error" | "popup_blocked" | "popup_closed" | "id_token_invalid" | "id_token_unsupported_alg" | "id_token_hs256_unsupported" | "id_token_missing_secret" | "id_token_invalid_signature" | "id_token_issuer_mismatch" | "id_token_audience_mismatch" | "id_token_expired" | "id_token_missing_sub" | "id_token_at_hash_mismatch" | "id_token_nonce_mismatch" | "logout_token_invalid" | "logout_token_unsupported_alg" | "logout_token_invalid_signature" | "logout_token_issuer_mismatch" | "logout_token_audience_mismatch" | "logout_token_expired" | "logout_token_replay";
330
330
  /**
331
331
  * SSO SDK 自定义错误
332
332
  */
@@ -456,6 +456,11 @@ interface RequireAuthProps {
456
456
  */
457
457
  renderLoginError?: (error: unknown, retry: () => void) => React.ReactNode;
458
458
  }
459
+ interface LoginErrorContentProps {
460
+ loginError: unknown;
461
+ retry: () => void;
462
+ renderLoginError?: RequireAuthProps["renderLoginError"];
463
+ }
459
464
  /**
460
465
  * 路由保护组件
461
466
  *
@@ -470,7 +475,7 @@ interface RequireAuthProps {
470
475
  * </RequireAuth>
471
476
  * ```
472
477
  */
473
- declare function RequireAuth({ children, fallback, autoLogin, usePopup, onError, renderLoginError, }: RequireAuthProps): string | number | bigint | true | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | React.FunctionComponentElement<React.FragmentProps>;
478
+ declare function RequireAuth({ children, fallback, autoLogin, usePopup, onError, renderLoginError, }: RequireAuthProps): string | number | bigint | true | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | React.FunctionComponentElement<React.FragmentProps> | React.FunctionComponentElement<LoginErrorContentProps>;
474
479
  /**
475
480
  * 高阶组件:包装页面组件,要求认证后才能访问
476
481
  *
@@ -124,24 +124,6 @@ var SILENT_PROBE_KEY = "silent_probe";
124
124
  function buildKey(base, clientId) {
125
125
  return clientId ? `${base}:${clientId}` : base;
126
126
  }
127
- function createMemoryStorageAdapter() {
128
- const store = /* @__PURE__ */ new Map();
129
- return {
130
- get(key) {
131
- return store.get(key) ?? null;
132
- },
133
- set(key, value) {
134
- store.set(key, value);
135
- },
136
- remove(key) {
137
- store.delete(key);
138
- },
139
- keys() {
140
- return [...store.keys()];
141
- }
142
- };
143
- }
144
- var memoryStorageAdapter = createMemoryStorageAdapter();
145
127
  function createSessionStorageAdapter() {
146
128
  const fallback = /* @__PURE__ */ new Map();
147
129
  return {
@@ -409,7 +391,19 @@ async function fetchJwks(baseUrl, options = {}) {
409
391
  return cachedJwks.jwks;
410
392
  }
411
393
  const discovery = await fetchDiscoveryDoc(baseUrl);
412
- const jwksUri = discovery?.jwks_uri || `${baseUrl}/api/oauth/jwks`;
394
+ let jwksUri = `${baseUrl}/api/oauth/jwks`;
395
+ if (discovery?.jwks_uri) {
396
+ try {
397
+ if (new URL(discovery.jwks_uri).origin === new URL(baseUrl).origin) {
398
+ jwksUri = discovery.jwks_uri;
399
+ } else {
400
+ console.warn(
401
+ "[SSO SDK] Discovery \u7684 jwks_uri \u4E0E ssoBaseUrl \u4E0D\u540C\u6E90\uFF0C\u5DF2\u5FFD\u7565\u5E76\u4F7F\u7528\u9ED8\u8BA4 JWKS \u7AEF\u70B9"
402
+ );
403
+ }
404
+ } catch {
405
+ }
406
+ }
413
407
  try {
414
408
  const res = await fetch(jwksUri, {
415
409
  signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
@@ -600,11 +594,7 @@ function assertValidTokenResponse(data, requireRefreshToken) {
600
594
  );
601
595
  }
602
596
  }
603
- var SILENT_PROBE_ERRORS = /* @__PURE__ */ new Set([
604
- "login_required",
605
- "consent_required",
606
- "interaction_required"
607
- ]);
597
+ var SILENT_PROBE_ERRORS = /* @__PURE__ */ new Set(["login_required", "consent_required", "interaction_required"]);
608
598
  var _SsoClient = class _SsoClient {
609
599
  constructor(config) {
610
600
  this._discovery = null;
@@ -670,6 +660,22 @@ var _SsoClient = class _SsoClient {
670
660
  if (!authorizationOk) bad.push("authorization_endpoint");
671
661
  if (!sameOrigin(d.token_endpoint)) bad.push("token_endpoint");
672
662
  if (!sameOrigin(d.userinfo_endpoint)) bad.push("userinfo_endpoint");
663
+ if (d.jwks_uri && !sameOrigin(d.jwks_uri)) bad.push("jwks_uri");
664
+ if (d.revocation_endpoint && !sameOrigin(d.revocation_endpoint)) {
665
+ bad.push("revocation_endpoint");
666
+ }
667
+ if (d.introspection_endpoint && !sameOrigin(d.introspection_endpoint)) {
668
+ bad.push("introspection_endpoint");
669
+ }
670
+ if (d.end_session_endpoint) {
671
+ try {
672
+ if (new URL(d.end_session_endpoint).origin !== publicOrigin) {
673
+ bad.push("end_session_endpoint");
674
+ }
675
+ } catch {
676
+ bad.push("end_session_endpoint");
677
+ }
678
+ }
673
679
  if (bad.length > 0) {
674
680
  if (!this._discoveryOriginWarned) {
675
681
  this._discoveryOriginWarned = true;
@@ -695,16 +701,11 @@ var _SsoClient = class _SsoClient {
695
701
  const url = `${this._serverBase}/api/oauth/.well-known/openid-configuration`;
696
702
  try {
697
703
  const controller = new AbortController();
698
- const timeoutId = setTimeout(
699
- () => controller.abort(),
700
- _SsoClient.DISCOVERY_TIMEOUT_MS
701
- );
704
+ const timeoutId = setTimeout(() => controller.abort(), _SsoClient.DISCOVERY_TIMEOUT_MS);
702
705
  const res = await fetch(url, { signal: controller.signal });
703
706
  clearTimeout(timeoutId);
704
707
  if (!res.ok) {
705
- console.warn(
706
- `[SSO SDK] OIDC Discovery \u8BF7\u6C42\u5931\u8D25: HTTP ${res.status}, \u56DE\u9000\u5230\u9ED8\u8BA4\u7AEF\u70B9`
707
- );
708
+ console.warn(`[SSO SDK] OIDC Discovery \u8BF7\u6C42\u5931\u8D25: HTTP ${res.status}, \u56DE\u9000\u5230\u9ED8\u8BA4\u7AEF\u70B9`);
708
709
  return null;
709
710
  }
710
711
  const doc = await res.json();
@@ -919,10 +920,7 @@ var _SsoClient = class _SsoClient {
919
920
  removeOAuthState(this.config.clientId);
920
921
  removeOAuthNonce(this.config.clientId);
921
922
  removePkceVerifier(`${this.config.clientId}_popup_nonce`);
922
- throw new SsoError(
923
- "popup_blocked",
924
- "\u5F39\u7A97\u88AB\u6D4F\u89C8\u5668\u62E6\u622A\uFF0C\u8BF7\u5141\u8BB8\u5F39\u7A97\u540E\u91CD\u8BD5"
925
- );
923
+ throw new SsoError("popup_blocked", "\u5F39\u7A97\u88AB\u6D4F\u89C8\u5668\u62E6\u622A\uFF0C\u8BF7\u5141\u8BB8\u5F39\u7A97\u540E\u91CD\u8BD5");
926
924
  }
927
925
  const redirectUriOrigin = new URL(this.config.redirectUri).origin;
928
926
  try {
@@ -951,9 +949,7 @@ var _SsoClient = class _SsoClient {
951
949
  }
952
950
  this.handleCallback(event.data.callbackUrl).then((tokenData) => {
953
951
  if (tokenData === null) {
954
- reject(
955
- new SsoError("not_authenticated", "SSO \u4F1A\u8BDD\u4E0D\u5B58\u5728\uFF08\u9759\u9ED8\u63A2\u6D4B\u65E0\u4F1A\u8BDD\uFF09")
956
- );
952
+ reject(new SsoError("not_authenticated", "SSO \u4F1A\u8BDD\u4E0D\u5B58\u5728\uFF08\u9759\u9ED8\u63A2\u6D4B\u65E0\u4F1A\u8BDD\uFF09"));
957
953
  return;
958
954
  }
959
955
  resolve(tokenData);
@@ -964,12 +960,7 @@ var _SsoClient = class _SsoClient {
964
960
  if (!completed) {
965
961
  completed = true;
966
962
  cleanup();
967
- reject(
968
- new SsoError(
969
- "popup_closed",
970
- "\u767B\u5F55\u7A97\u53E3\u5DF2\u5173\u95ED"
971
- )
972
- );
963
+ reject(new SsoError("popup_closed", "\u767B\u5F55\u7A97\u53E3\u5DF2\u5173\u95ED"));
973
964
  }
974
965
  }
975
966
  }, 500);
@@ -1036,18 +1027,24 @@ var _SsoClient = class _SsoClient {
1036
1027
  removeOAuthNonce(this.config.clientId);
1037
1028
  removePkceVerifier(this.config.clientId);
1038
1029
  removeSilentProbe(this.config.clientId);
1039
- throw new SsoError(
1040
- "state_mismatch",
1041
- "State \u53C2\u6570\u4E0D\u5339\u914D\uFF0C\u53EF\u80FD\u5B58\u5728 CSRF \u653B\u51FB"
1042
- );
1030
+ throw new SsoError("state_mismatch", "State \u53C2\u6570\u4E0D\u5339\u914D\uFF0C\u53EF\u80FD\u5B58\u5728 CSRF \u653B\u51FB");
1031
+ }
1032
+ const returnedIss = params.get("iss");
1033
+ if (returnedIss) {
1034
+ const discovery = await this._getDiscovery();
1035
+ const expectedIssuer = (discovery?.issuer || this.config.ssoBaseUrl).replace(/\/+$/, "");
1036
+ if (returnedIss.replace(/\/+$/, "") !== expectedIssuer) {
1037
+ removeOAuthState(this.config.clientId);
1038
+ removeOAuthNonce(this.config.clientId);
1039
+ removePkceVerifier(this.config.clientId);
1040
+ removeSilentProbe(this.config.clientId);
1041
+ throw new SsoError("issuer_mismatch", "\u6388\u6743\u54CD\u5E94 issuer \u4E0D\u5339\u914D\uFF0C\u53EF\u80FD\u5B58\u5728 IdP \u6DF7\u53E0\u653B\u51FB");
1042
+ }
1043
1043
  }
1044
1044
  const expectedNonce = getOAuthNonce(this.config.clientId) ?? void 0;
1045
1045
  const verifier = getPkceVerifier(this.config.clientId);
1046
1046
  if (!verifier) {
1047
- throw new SsoError(
1048
- "pkce_required",
1049
- "code_verifier \u4E0D\u5B58\u5728\uFF08\u53EF\u80FD\u5DF2\u8FC7\u671F\u6216\u6765\u81EA\u5176\u4ED6\u6807\u7B7E\u9875\uFF09"
1050
- );
1047
+ throw new SsoError("pkce_required", "code_verifier \u4E0D\u5B58\u5728\uFF08\u53EF\u80FD\u5DF2\u8FC7\u671F\u6216\u6765\u81EA\u5176\u4ED6\u6807\u7B7E\u9875\uFF09");
1051
1048
  }
1052
1049
  const tokenEndpoint = await this._getTokenEndpoint();
1053
1050
  const body = new URLSearchParams();
@@ -1094,10 +1091,7 @@ var _SsoClient = class _SsoClient {
1094
1091
  removePkceVerifier(this.config.clientId);
1095
1092
  removeSilentProbe(this.config.clientId);
1096
1093
  removeTokenData(this.config.clientId);
1097
- throw new SsoError(
1098
- "id_token_invalid",
1099
- "Token \u54CD\u5E94\u7F3A\u5C11 id_token\uFF08scope \u542B openid \u65F6\u5FC5\u9700\uFF09"
1100
- );
1094
+ throw new SsoError("id_token_invalid", "Token \u54CD\u5E94\u7F3A\u5C11 id_token\uFF08scope \u542B openid \u65F6\u5FC5\u9700\uFF09");
1101
1095
  }
1102
1096
  if (data.id_token) {
1103
1097
  try {
@@ -1162,19 +1156,14 @@ var _SsoClient = class _SsoClient {
1162
1156
  }
1163
1157
  let res = null;
1164
1158
  let lastErr;
1165
- for (let attempt = 0; attempt < 2; attempt++) {
1166
- try {
1167
- res = await fetchWithTimeout(tokenEndpoint, {
1168
- method: "POST",
1169
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
1170
- body: body.toString()
1171
- });
1172
- lastErr = null;
1173
- break;
1174
- } catch (err) {
1175
- lastErr = err;
1176
- if (attempt < 1) await new Promise((r) => setTimeout(r, 1e3));
1177
- }
1159
+ try {
1160
+ res = await fetchWithTimeout(tokenEndpoint, {
1161
+ method: "POST",
1162
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1163
+ body: body.toString()
1164
+ });
1165
+ } catch (err) {
1166
+ lastErr = err;
1178
1167
  }
1179
1168
  if (lastErr || !res) {
1180
1169
  throw new SsoError("network_error", "\u5237\u65B0 Token \u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25", lastErr);
@@ -1256,10 +1245,7 @@ var _SsoClient = class _SsoClient {
1256
1245
  removeTokenData(this.config.clientId);
1257
1246
  throw new SsoError("not_authenticated", "Token \u5DF2\u5931\u6548");
1258
1247
  }
1259
- throw new SsoError(
1260
- "userinfo_failed",
1261
- `\u83B7\u53D6\u7528\u6237\u4FE1\u606F\u5931\u8D25: HTTP ${res.status}`
1262
- );
1248
+ throw new SsoError("userinfo_failed", `\u83B7\u53D6\u7528\u6237\u4FE1\u606F\u5931\u8D25: HTTP ${res.status}`);
1263
1249
  }
1264
1250
  return await res.json();
1265
1251
  }
@@ -1319,7 +1305,7 @@ var _SsoClient = class _SsoClient {
1319
1305
  if (this.config.clientSecret) {
1320
1306
  revokeBody.set("client_secret", this.config.clientSecret);
1321
1307
  }
1322
- await fetchWithTimeout(
1308
+ const doRevoke = () => fetchWithTimeout(
1323
1309
  revokeUrl,
1324
1310
  {
1325
1311
  method: "POST",
@@ -1328,6 +1314,14 @@ var _SsoClient = class _SsoClient {
1328
1314
  },
1329
1315
  REVOKE_TIMEOUT_MS
1330
1316
  );
1317
+ let res = await doRevoke();
1318
+ if (!res.ok && (res.status >= 500 || res.status === 429)) {
1319
+ await new Promise((resolve) => setTimeout(resolve, 300));
1320
+ res = await doRevoke();
1321
+ }
1322
+ if (!res.ok) {
1323
+ this.debugLog("logout: \u670D\u52A1\u7AEF\u64A4\u9500 refresh_token \u5931\u8D25", { status: res.status });
1324
+ }
1331
1325
  } catch {
1332
1326
  }
1333
1327
  }
@@ -1338,10 +1332,7 @@ var _SsoClient = class _SsoClient {
1338
1332
  logoutUrl.searchParams.set("client_id", this.config.clientId);
1339
1333
  const postLogoutUri = this.config.postLogoutRedirectUri || this.config.redirectUri;
1340
1334
  if (postLogoutUri) {
1341
- logoutUrl.searchParams.set(
1342
- "post_logout_redirect_uri",
1343
- postLogoutUri
1344
- );
1335
+ logoutUrl.searchParams.set("post_logout_redirect_uri", postLogoutUri);
1345
1336
  }
1346
1337
  if (idTokenHint) {
1347
1338
  logoutUrl.searchParams.set("id_token_hint", idTokenHint);
@@ -1571,7 +1562,15 @@ function SsoProvider({
1571
1562
  const tokenData = getTokenData(client.config.clientId);
1572
1563
  if (!tokenData) return;
1573
1564
  const remainingSec = (tokenData.expires_at - Date.now()) / 1e3;
1574
- if (remainingSec <= 0 || remainingSec <= refreshThreshold) {
1565
+ const lifetimeSec = Math.max(
1566
+ 1,
1567
+ (tokenData.expires_at - tokenData.issued_at) / 1e3
1568
+ );
1569
+ const effectiveThreshold = Math.max(
1570
+ 1,
1571
+ Math.min(refreshThreshold, Math.floor(lifetimeSec / 3))
1572
+ );
1573
+ if (remainingSec <= 0 || remainingSec <= effectiveThreshold) {
1575
1574
  refreshTimerRef.current = setTimeout(
1576
1575
  () => {
1577
1576
  if (active) void attemptRefresh();
@@ -1580,13 +1579,13 @@ function SsoProvider({
1580
1579
  );
1581
1580
  return;
1582
1581
  }
1583
- const delayMs = Math.max((remainingSec - refreshThreshold) * 1e3, minDelayMs, 1e3);
1582
+ const delayMs = Math.max((remainingSec - effectiveThreshold) * 1e3, minDelayMs, 1e3);
1584
1583
  refreshTimerRef.current = setTimeout(() => {
1585
1584
  if (!active) return;
1586
1585
  const td = getTokenData(client.config.clientId);
1587
1586
  if (!td) return;
1588
1587
  const secLeft = (td.expires_at - Date.now()) / 1e3;
1589
- if (secLeft <= refreshThreshold) {
1588
+ if (secLeft <= effectiveThreshold) {
1590
1589
  void attemptRefresh();
1591
1590
  } else {
1592
1591
  scheduleNextRefresh();
@@ -1601,7 +1600,7 @@ function SsoProvider({
1601
1600
  refreshTimerRef.current = null;
1602
1601
  }
1603
1602
  };
1604
- }, [client, loadUser, refreshThreshold, onTokenRefreshed]);
1603
+ }, [client, loadUser, refreshThreshold, onTokenRefreshed, markSessionExpired]);
1605
1604
  (0, import_react.useEffect)(() => {
1606
1605
  const handleStorageChange = (e) => {
1607
1606
  if (e.key && e.key.startsWith("nihplod_sso_") && !e.key.startsWith(REFRESH_LOCK_PREFIX)) {
@@ -1696,6 +1695,26 @@ function useSso() {
1696
1695
 
1697
1696
  // src/react/RequireAuth.tsx
1698
1697
  var import_react2 = __toESM(require("react"));
1698
+ function LoginErrorContent({
1699
+ loginError,
1700
+ retry,
1701
+ renderLoginError
1702
+ }) {
1703
+ if (renderLoginError) {
1704
+ return import_react2.default.createElement(
1705
+ import_react2.default.Fragment,
1706
+ null,
1707
+ renderLoginError(loginError, retry)
1708
+ );
1709
+ }
1710
+ const message = loginError instanceof Error ? loginError.message : "\u767B\u5F55\u5931\u8D25";
1711
+ return import_react2.default.createElement(
1712
+ "div",
1713
+ null,
1714
+ import_react2.default.createElement("p", null, `\u767B\u5F55\u672A\u5B8C\u6210\uFF1A${message}`),
1715
+ import_react2.default.createElement("button", { onClick: retry }, "\u91CD\u8BD5\u767B\u5F55")
1716
+ );
1717
+ }
1699
1718
  function RequireAuth({
1700
1719
  children,
1701
1720
  fallback,
@@ -1741,20 +1760,11 @@ function RequireAuth({
1741
1760
  }
1742
1761
  if (!isAuthenticated) {
1743
1762
  if (loginError) {
1744
- if (renderLoginError) {
1745
- return import_react2.default.createElement(
1746
- import_react2.default.Fragment,
1747
- null,
1748
- renderLoginError(loginError, retry)
1749
- );
1750
- }
1751
- const message = loginError instanceof Error ? loginError.message : "\u767B\u5F55\u5931\u8D25";
1752
- return import_react2.default.createElement(
1753
- "div",
1754
- null,
1755
- import_react2.default.createElement("p", null, `\u767B\u5F55\u672A\u5B8C\u6210\uFF1A${message}`),
1756
- import_react2.default.createElement("button", { onClick: retry }, "\u91CD\u8BD5\u767B\u5F55")
1757
- );
1763
+ return import_react2.default.createElement(LoginErrorContent, {
1764
+ loginError,
1765
+ retry,
1766
+ renderLoginError
1767
+ });
1758
1768
  }
1759
1769
  if (fallback) return import_react2.default.createElement(import_react2.default.Fragment, null, fallback);
1760
1770
  return import_react2.default.createElement("div", null, "\u8BF7\u5148\u767B\u5F55");